commit 0fab423a186881df10354088fdc9ac829e79c948 Author: lz_db Date: Sun Nov 16 12:31:03 2025 +0800 add diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..4cc1253 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright © 2024 Igor Kroitor + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..d378f5b --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,5 @@ +# Include the license file +include LICENSE.txt + +# Include the package.json file +include package.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..46daea4 --- /dev/null +++ b/README.md @@ -0,0 +1,738 @@ +# CCXT – CryptoCurrency eXchange Trading Library + +[![NPM Downloads](https://img.shields.io/npm/dy/ccxt.svg)](https://www.npmjs.com/package/ccxt) [![npm](https://img.shields.io/npm/v/ccxt.svg)](https://npmjs.com/package/ccxt) [![PyPI](https://img.shields.io/pypi/v/ccxt.svg)](https://pypi.python.org/pypi/ccxt) [![NuGet version](https://img.shields.io/nuget/v/ccxt)](https://www.nuget.org/packages/ccxt) [![GoDoc](https://pkg.go.dev/badge/github.com/ccxt/ccxt/go/v4?utm_source=godoc)](https://godoc.org/github.com/ccxt/ccxt/go/v4) [![Discord](https://img.shields.io/discord/690203284119617602?logo=discord&logoColor=white)](https://discord.gg/ccxt) [![Supported Exchanges](https://img.shields.io/badge/exchanges-107-blue.svg)](https://github.com/ccxt/ccxt/wiki/Exchange-Markets) [![Follow CCXT at x.com](https://img.shields.io/twitter/follow/ccxt_official.svg?style=social&label=CCXT)](https://x.com/ccxt_official) + +A cryptocurrency trading API with more than 100 exchanges in JavaScript / TypeScript / Python / C# / PHP / Go. + +### [Install](#install) · [Usage](#usage) · [Manual](https://github.com/ccxt/ccxt/wiki) · [FAQ](https://github.com/ccxt/ccxt/wiki/FAQ) · [Examples](https://github.com/ccxt/ccxt/tree/master/examples) · [Contributing](https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md) · [Disclaimer](#disclaimer) · [Social](#social) + +The **CCXT** library is used to connect and trade with cryptocurrency exchanges and payment processing services worldwide. It provides quick access to market data for storage, analysis, visualization, indicator development, algorithmic trading, strategy backtesting, bot programming, and related software engineering. + +It is intended to be used by **coders, developers, technically-skilled traders, data-scientists and financial analysts** for building trading algorithms. + +Current feature list: + +- support for many cryptocurrency exchanges — more coming soon +- fully implemented public and private APIs +- optional normalized data for cross-exchange analytics and arbitrage +- an out of the box unified API that is extremely easy to integrate +- works in Node 10.4+, Python 3, PHP 8.1+, netstandard2.0/2.1, Go 1.20+ and web browsers + +## See Also + +- [![TabTrader](https://user-images.githubusercontent.com/1294454/66755907-9c3e8880-eea1-11e9-846e-0bff349ceb87.png)](https://tab-trader.com/?utm_source=ccxt) **[TabTrader](https://tab-trader.com/?utm_source=ccxt)** – trading on all exchanges in one app. Available on **[Android](https://play.google.com/store/apps/details?id=com.tabtrader.android&referrer=utm_source%3Dccxt)** and **[iOS](https://itunes.apple.com/app/apple-store/id1095716562?mt=8)**! +- [![Freqtrade](https://user-images.githubusercontent.com/1294454/114340585-8e35fa80-9b60-11eb-860f-4379125e2db6.png)](https://www.freqtrade.io) **[Freqtrade](https://www.freqtrade.io)** – leading opensource cryptocurrency algorithmic trading software! +- [![OctoBot](https://user-images.githubusercontent.com/1294454/132113722-007fc092-7530-4b41-b929-b8ed380b7b2e.png)](https://www.octobot.online) **[OctoBot](https://www.octobot.online)** – cryptocurrency trading bot with an advanced web interface. +- [![TokenBot](https://user-images.githubusercontent.com/1294454/152720975-0522b803-70f0-4f18-a305-3c99b37cd990.png)](https://tokenbot.com/?utm_source=github&utm_medium=ccxt&utm_campaign=algodevs) **[TokenBot](https://tokenbot.com/?utm_source=github&utm_medium=ccxt&utm_campaign=algodevs)** – discover and copy the best algorithmic traders in the world. + +## Certified Cryptocurrency Exchanges + + +|logo |id |name |ver |type |certified |pro |discount | +|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------|-----------------------------------------------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------:|--------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------|:----------------------------------------------------------------------------:|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [![binance](https://github.com/user-attachments/assets/e9419b93-ccb0-46aa-9bff-c883f096274b)](https://accounts.binance.com/register?ref=CCXTCOM) | binance | [Binance](https://accounts.binance.com/register?ref=CCXTCOM) | [![API Version *](https://img.shields.io/badge/*-lightgray)](https://developers.binance.com/en) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | [![Sign up with Binance using CCXT's referral link for a 10% discount!](https://img.shields.io/static/v1?label=Fee&message=%2d10%25&color=orange)](https://accounts.binance.com/register?ref=CCXTCOM) | +| [![binanceusdm](https://github.com/user-attachments/assets/871cbea7-eebb-4b28-b260-c1c91df0487a)](https://accounts.binance.com/register?ref=CCXTCOM) | binanceusdm | [Binance USDⓈ-M](https://accounts.binance.com/register?ref=CCXTCOM) | [![API Version *](https://img.shields.io/badge/*-lightgray)](https://binance-docs.github.io/apidocs/futures/en/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | [![Sign up with Binance USDⓈ-M using CCXT's referral link for a 10% discount!](https://img.shields.io/static/v1?label=Fee&message=%2d10%25&color=orange)](https://accounts.binance.com/register?ref=CCXTCOM) | +| [![binancecoinm](https://github.com/user-attachments/assets/387cfc4e-5f33-48cd-8f5c-cd4854dabf0c)](https://accounts.binance.com/register?ref=CCXTCOM) | binancecoinm | [Binance COIN-M](https://accounts.binance.com/register?ref=CCXTCOM) | [![API Version *](https://img.shields.io/badge/*-lightgray)](https://binance-docs.github.io/apidocs/delivery/en/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | [![Sign up with Binance COIN-M using CCXT's referral link for a 10% discount!](https://img.shields.io/static/v1?label=Fee&message=%2d10%25&color=orange)](https://accounts.binance.com/register?ref=CCXTCOM) | +| [![bybit](https://github.com/user-attachments/assets/97a5d0b3-de10-423d-90e1-6620960025ed)](https://www.bybit.com/invite?ref=XDK12WP) | bybit | [Bybit](https://www.bybit.com/invite?ref=XDK12WP) | [![API Version 5](https://img.shields.io/badge/5-lightgray)](https://bybit-exchange.github.io/docs/inverse/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | | +| [![okx](https://user-images.githubusercontent.com/1294454/152485636-38b19e4a-bece-4dec-979a-5982859ffc04.jpg)](https://www.okx.com/join/CCXTCOM) | okx | [OKX](https://www.okx.com/join/CCXTCOM) | [![API Version 5](https://img.shields.io/badge/5-lightgray)](https://www.okx.com/docs-v5/en/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | [![Sign up with OKX using CCXT's referral link for a 20% discount!](https://img.shields.io/static/v1?label=Fee&message=%2d20%25&color=orange)](https://www.okx.com/join/CCXTCOM) | +| [![gate](https://github.com/user-attachments/assets/64f988c5-07b6-4652-b5c1-679a6bf67c85)](https://www.gate.com/share/CCXTGATE) | gate | [Gate](https://www.gate.com/share/CCXTGATE) | [![API Version 4](https://img.shields.io/badge/4-lightgray)](https://www.gate.com/docs/developers/apiv4/en/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | [![Sign up with Gate using CCXT's referral link for a 20% discount!](https://img.shields.io/static/v1?label=Fee&message=%2d20%25&color=orange)](https://www.gate.com/share/CCXTGATE) | +| [![kucoin](https://user-images.githubusercontent.com/51840849/87295558-132aaf80-c50e-11ea-9801-a2fb0c57c799.jpg)](https://www.kucoin.com/ucenter/signup?rcode=E5wkqe) | kucoin | [KuCoin](https://www.kucoin.com/ucenter/signup?rcode=E5wkqe) | [![API Version 2](https://img.shields.io/badge/2-lightgray)](https://docs.kucoin.com) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | | +| [![kucoinfutures](https://user-images.githubusercontent.com/1294454/147508995-9e35030a-d046-43a1-a006-6fabd981b554.jpg)](https://futures.kucoin.com/?rcode=E5wkqe) | kucoinfutures | [KuCoin Futures](https://futures.kucoin.com/?rcode=E5wkqe) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://docs.kucoin.com/futures) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | | +| [![bitget](https://github.com/user-attachments/assets/fbaa10cc-a277-441d-a5b7-997dd9a87658)](https://www.bitget.com/expressly?languageType=0&channelCode=ccxt&vipCode=tg9j) | bitget | [Bitget](https://www.bitget.com/expressly?languageType=0&channelCode=ccxt&vipCode=tg9j) | [![API Version 2](https://img.shields.io/badge/2-lightgray)](https://www.bitget.com/api-doc/common/intro) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | | +| [![hyperliquid](https://github.com/ccxt/ccxt/assets/43336371/b371bc6c-4a8c-489f-87f4-20a913dd8d4b)](https://app.hyperliquid.xyz/) | hyperliquid | [Hyperliquid](https://app.hyperliquid.xyz/) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api) | ![DEX - Distributed EXchange](https://img.shields.io/badge/DEX-blue.svg "DEX - Distributed EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | | +| [![bitmex](https://github.com/user-attachments/assets/c78425ab-78d5-49d6-bd14-db7734798f04)](https://www.bitmex.com/app/register/NZTR1q) | bitmex | [BitMEX](https://www.bitmex.com/app/register/NZTR1q) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://www.bitmex.com/app/apiOverview) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | [![Sign up with BitMEX using CCXT's referral link for a 10% discount!](https://img.shields.io/static/v1?label=Fee&message=%2d10%25&color=orange)](https://www.bitmex.com/app/register/NZTR1q) | +| [![bingx](https://github-production-user-asset-6210df.s3.amazonaws.com/1294454/253675376-6983b72e-4999-4549-b177-33b374c195e3.jpg)](https://bingx.com/invite/OHETOM) | bingx | [BingX](https://bingx.com/invite/OHETOM) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://bingx-api.github.io/docs/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | | +| [![htx](https://user-images.githubusercontent.com/1294454/76137448-22748a80-604e-11ea-8069-6e389271911d.jpg)](https://www.htx.com.vc/invite/en-us/1h?invite_code=6rmm2223) | htx | [HTX](https://www.htx.com.vc/invite/en-us/1h?invite_code=6rmm2223) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://huobiapi.github.io/docs/spot/v1/en/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | [![Sign up with HTX using CCXT's referral link for a 15% discount!](https://img.shields.io/static/v1?label=Fee&message=%2d15%25&color=orange)](https://www.htx.com.vc/invite/en-us/1h?invite_code=6rmm2223) | +| [![mexc](https://user-images.githubusercontent.com/1294454/137283979-8b2a818d-8633-461b-bfca-de89e8c446b2.jpg)](https://www.mexc.com/register?inviteCode=mexc-1FQ1GNu1) | mexc | [MEXC Global](https://www.mexc.com/register?inviteCode=mexc-1FQ1GNu1) | [![API Version 3](https://img.shields.io/badge/3-lightgray)](https://mexcdevelop.github.io/apidocs/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | | +| [![bitmart](https://github.com/user-attachments/assets/0623e9c4-f50e-48c9-82bd-65c3908c3a14)](http://www.bitmart.com/?r=rQCFLh) | bitmart | [BitMart](http://www.bitmart.com/?r=rQCFLh) | [![API Version 2](https://img.shields.io/badge/2-lightgray)](https://developer-pro.bitmart.com/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | [![Sign up with BitMart using CCXT's referral link for a 30% discount!](https://img.shields.io/static/v1?label=Fee&message=%2d30%25&color=orange)](http://www.bitmart.com/?r=rQCFLh) | +| [![cryptocom](https://user-images.githubusercontent.com/1294454/147792121-38ed5e36-c229-48d6-b49a-48d05fc19ed4.jpeg)](https://crypto.com/exch/kdacthrnxt) | cryptocom | [Crypto.com](https://crypto.com/exch/kdacthrnxt) | [![API Version 2](https://img.shields.io/badge/2-lightgray)](https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | [![Sign up with Crypto.com using CCXT's referral link for a 75% discount!](https://img.shields.io/static/v1?label=Fee&message=%2d75%25&color=orange)](https://crypto.com/exch/kdacthrnxt) | +| [![coinex](https://user-images.githubusercontent.com/51840849/87182089-1e05fa00-c2ec-11ea-8da9-cc73b45abbbc.jpg)](https://www.coinex.com/register?refer_code=yw5fz) | coinex | [CoinEx](https://www.coinex.com/register?refer_code=yw5fz) | [![API Version 2](https://img.shields.io/badge/2-lightgray)](https://docs.coinex.com/api/v2) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | | +| [![hashkey](https://github.com/user-attachments/assets/6dd6127b-cc19-4a13-9b29-a98d81f80e98)](https://global.hashkey.com/en-US/register/invite?invite_code=82FQUN) | hashkey | [HashKey Global](https://global.hashkey.com/en-US/register/invite?invite_code=82FQUN) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://hashkeyglobal-apidoc.readme.io/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | | +| [![woo](https://user-images.githubusercontent.com/1294454/150730761-1a00e5e0-d28c-480f-9e65-089ce3e6ef3b.jpg)](https://woox.io/register?ref=DIJT0CNL) | woo | [WOO X](https://woox.io/register?ref=DIJT0CNL) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://docs.woox.io/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | [![Sign up with WOO X using CCXT's referral link for a 35% discount!](https://img.shields.io/static/v1?label=Fee&message=%2d35%25&color=orange)](https://woox.io/register?ref=DIJT0CNL) | +| [![woofipro](https://github.com/user-attachments/assets/9ba21b8a-a9c7-4770-b7f1-ce3bcbde68c1)](https://dex.woo.org/en/trade?ref=CCXT) | woofipro | [WOOFI PRO](https://dex.woo.org/en/trade?ref=CCXT) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://orderly.network/docs/build-on-evm/building-on-evm) | ![DEX - Distributed EXchange](https://img.shields.io/badge/DEX-blue.svg "DEX - Distributed EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | [![Sign up with WOOFI PRO using CCXT's referral link for a 5% discount!](https://img.shields.io/static/v1?label=Fee&message=%2d5%25&color=orange)](https://dex.woo.org/en/trade?ref=CCXT) | + +## Supported Cryptocurrency Exchanges +The CCXT library currently supports the following 104 cryptocurrency exchange markets and trading APIs: + +|logo |id |name |ver |type |certified |pro | +|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------|-----------------------------------------------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------:|--------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------| +| [![alpaca](https://github.com/user-attachments/assets/e9476df8-a450-4c3e-ab9a-1a7794219e1b)](https://alpaca.markets) | alpaca | [Alpaca](https://alpaca.markets) | [![API Version *](https://img.shields.io/badge/*-lightgray)](https://alpaca.markets/docs/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![apex](https://github.com/user-attachments/assets/fef8f2f7-4265-46aa-965e-33a91881cb00)](https://omni.apex.exchange/trade) | apex | [Apex](https://omni.apex.exchange/trade) | [![API Version 3](https://img.shields.io/badge/3-lightgray)](https://api-docs.pro.apex.exchange) | ![DEX - Distributed EXchange](https://img.shields.io/badge/DEX-blue.svg "DEX - Distributed EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![arkham](https://github.com/user-attachments/assets/5cefdcfb-2c10-445b-835c-fa21317bf5ac)](https://arkm.com/register?ref=ccxt) | arkham | [ARKHAM](https://arkm.com/register?ref=ccxt) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://arkm.com/limits-api) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![ascendex](https://github.com/user-attachments/assets/55bab6b9-d4ca-42a8-a0e6-fac81ae557f1)](https://ascendex.com/en-us/register?inviteCode=EL6BXBQM) | ascendex | [AscendEX](https://ascendex.com/en-us/register?inviteCode=EL6BXBQM) | [![API Version 2](https://img.shields.io/badge/2-lightgray)](https://ascendex.github.io/ascendex-pro-api/#ascendex-pro-api-documentation) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![backpack](https://github.com/user-attachments/assets/cc04c278-679f-4554-9f72-930dd632b80f)](https://backpack.exchange/join/ccxt) | backpack | [Backpack](https://backpack.exchange/join/ccxt) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://docs.backpack.exchange/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![bequant](https://github.com/user-attachments/assets/0583ef1f-29fe-4b7c-8189-63565a0e2867)](https://bequant.io/referral/dd104e3bee7634ec) | bequant | [Bequant](https://bequant.io/referral/dd104e3bee7634ec) | [![API Version 3](https://img.shields.io/badge/3-lightgray)](https://api.bequant.io/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![bigone](https://github.com/user-attachments/assets/4e5cfd53-98cc-4b90-92cd-0d7b512653d1)](https://b1.run/users/new?code=D3LLBVFT) | bigone | [BigONE](https://b1.run/users/new?code=D3LLBVFT) | [![API Version 3](https://img.shields.io/badge/3-lightgray)](https://open.big.one/docs/api.html) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![binance](https://github.com/user-attachments/assets/e9419b93-ccb0-46aa-9bff-c883f096274b)](https://accounts.binance.com/register?ref=CCXTCOM) | binance | [Binance](https://accounts.binance.com/register?ref=CCXTCOM) | [![API Version *](https://img.shields.io/badge/*-lightgray)](https://developers.binance.com/en) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![binancecoinm](https://github.com/user-attachments/assets/387cfc4e-5f33-48cd-8f5c-cd4854dabf0c)](https://accounts.binance.com/register?ref=CCXTCOM) | binancecoinm | [Binance COIN-M](https://accounts.binance.com/register?ref=CCXTCOM) | [![API Version *](https://img.shields.io/badge/*-lightgray)](https://binance-docs.github.io/apidocs/delivery/en/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![binanceus](https://github.com/user-attachments/assets/a9667919-b632-4d52-a832-df89f8a35e8c)](https://www.binance.us/?ref=35005074) | binanceus | [Binance US](https://www.binance.us/?ref=35005074) | [![API Version *](https://img.shields.io/badge/*-lightgray)](https://github.com/binance-us/binance-official-api-docs) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![binanceusdm](https://github.com/user-attachments/assets/871cbea7-eebb-4b28-b260-c1c91df0487a)](https://accounts.binance.com/register?ref=CCXTCOM) | binanceusdm | [Binance USDⓈ-M](https://accounts.binance.com/register?ref=CCXTCOM) | [![API Version *](https://img.shields.io/badge/*-lightgray)](https://binance-docs.github.io/apidocs/futures/en/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![bingx](https://github-production-user-asset-6210df.s3.amazonaws.com/1294454/253675376-6983b72e-4999-4549-b177-33b374c195e3.jpg)](https://bingx.com/invite/OHETOM) | bingx | [BingX](https://bingx.com/invite/OHETOM) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://bingx-api.github.io/docs/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![bit2c](https://github.com/user-attachments/assets/db0bce50-6842-4c09-a1d5-0c87d22118aa)](https://bit2c.co.il/Aff/63bfed10-e359-420c-ab5a-ad368dab0baf) | bit2c | [Bit2C](https://bit2c.co.il/Aff/63bfed10-e359-420c-ab5a-ad368dab0baf) | [![API Version *](https://img.shields.io/badge/*-lightgray)](https://www.bit2c.co.il/home/api) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![bitbank](https://github.com/user-attachments/assets/9d616de0-8a88-4468-8e38-d269acab0348)](https://bitbank.cc/) | bitbank | [bitbank](https://bitbank.cc/) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://docs.bitbank.cc/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![bitbns](https://github.com/user-attachments/assets/a5b9a562-cdd8-4bea-9fa7-fd24c1dad3d9)](https://ref.bitbns.com/1090961) | bitbns | [Bitbns](https://ref.bitbns.com/1090961) | [![API Version 2](https://img.shields.io/badge/2-lightgray)](https://bitbns.com/trade/#/api-trading/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![bitfinex](https://github.com/user-attachments/assets/4a8e947f-ab46-481a-a8ae-8b20e9b03178)](https://www.bitfinex.com) | bitfinex | [Bitfinex](https://www.bitfinex.com) | [![API Version 2](https://img.shields.io/badge/2-lightgray)](https://docs.bitfinex.com/v2/docs/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![bitflyer](https://github.com/user-attachments/assets/d0217747-e54d-4533-8416-0d553dca74bb)](https://bitflyer.com) | bitflyer | [bitFlyer](https://bitflyer.com) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://lightning.bitflyer.com/docs?lang=en) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![bitget](https://github.com/user-attachments/assets/fbaa10cc-a277-441d-a5b7-997dd9a87658)](https://www.bitget.com/expressly?languageType=0&channelCode=ccxt&vipCode=tg9j) | bitget | [Bitget](https://www.bitget.com/expressly?languageType=0&channelCode=ccxt&vipCode=tg9j) | [![API Version 2](https://img.shields.io/badge/2-lightgray)](https://www.bitget.com/api-doc/common/intro) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![bithumb](https://github.com/user-attachments/assets/c9e0eefb-4777-46b9-8f09-9d7f7c4af82d)](https://www.bithumb.com) | bithumb | [Bithumb](https://www.bithumb.com) | [![API Version *](https://img.shields.io/badge/*-lightgray)](https://apidocs.bithumb.com) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![bitmart](https://github.com/user-attachments/assets/0623e9c4-f50e-48c9-82bd-65c3908c3a14)](http://www.bitmart.com/?r=rQCFLh) | bitmart | [BitMart](http://www.bitmart.com/?r=rQCFLh) | [![API Version 2](https://img.shields.io/badge/2-lightgray)](https://developer-pro.bitmart.com/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![bitmex](https://github.com/user-attachments/assets/c78425ab-78d5-49d6-bd14-db7734798f04)](https://www.bitmex.com/app/register/NZTR1q) | bitmex | [BitMEX](https://www.bitmex.com/app/register/NZTR1q) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://www.bitmex.com/app/apiOverview) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![bitopro](https://github.com/user-attachments/assets/affc6337-b95a-44bf-aacd-04f9722364f6)](https://www.bitopro.com) | bitopro | [BitoPro](https://www.bitopro.com) | [![API Version 3](https://img.shields.io/badge/3-lightgray)](https://github.com/bitoex/bitopro-offical-api-docs/blob/master/v3-1/rest-1/rest.md) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![bitrue](https://github.com/user-attachments/assets/67abe346-1273-461a-bd7c-42fa32907c8e)](https://www.bitrue.com/affiliate/landing?cn=600000&inviteCode=EZWETQE) | bitrue | [Bitrue](https://www.bitrue.com/affiliate/landing?cn=600000&inviteCode=EZWETQE) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://github.com/Bitrue-exchange/bitrue-official-api-docs) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![bitso](https://github.com/user-attachments/assets/178c8e56-9054-4107-b192-5e5053d4f975)](https://bitso.com/?ref=itej) | bitso | [Bitso](https://bitso.com/?ref=itej) | [![API Version 3](https://img.shields.io/badge/3-lightgray)](https://bitso.com/api_info) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![bitstamp](https://github.com/user-attachments/assets/d5480572-1fee-43cb-b900-d38c522d0024)](https://www.bitstamp.net) | bitstamp | [Bitstamp](https://www.bitstamp.net) | [![API Version 2](https://img.shields.io/badge/2-lightgray)](https://www.bitstamp.net/api) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![bitteam](https://github.com/user-attachments/assets/b41b5e0d-98e5-4bd3-8a6e-aeb230a4a135)](https://bit.team/auth/sign-up?ref=bitboy2023) | bitteam | [BIT.TEAM](https://bit.team/auth/sign-up?ref=bitboy2023) | [![API Version 2.0.6](https://img.shields.io/badge/2.0.6-lightgray)](https://bit.team/trade/api/documentation) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![bittrade](https://user-images.githubusercontent.com/1294454/85734211-85755480-b705-11ea-8b35-0b7f1db33a2f.jpg)](https://www.bittrade.co.jp/register/?invite_code=znnq3) | bittrade | [BitTrade](https://www.bittrade.co.jp/register/?invite_code=znnq3) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://api-doc.bittrade.co.jp) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![bitvavo](https://github.com/user-attachments/assets/d213155c-8c71-4701-9bd5-45351febc2a8)](https://bitvavo.com/?a=24F34952F7) | bitvavo | [Bitvavo](https://bitvavo.com/?a=24F34952F7) | [![API Version 2](https://img.shields.io/badge/2-lightgray)](https://docs.bitvavo.com/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![blockchaincom](https://github.com/user-attachments/assets/975e3054-3399-4363-bcee-ec3c6d63d4e8)](https://blockchain.com) | blockchaincom | [Blockchain.com](https://blockchain.com) | [![API Version 3](https://img.shields.io/badge/3-lightgray)](https://api.blockchain.com/v3) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![blofin](https://github.com/user-attachments/assets/518cdf80-f05d-4821-a3e3-d48ceb41d73b)](https://blofin.com/register?referral_code=f79EsS) | blofin | [BloFin](https://blofin.com/register?referral_code=f79EsS) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://blofin.com/docs) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![btcalpha](https://github.com/user-attachments/assets/dce49f3a-61e5-4ba0-a2fe-41d192fd0e5d)](https://btc-alpha.com/?r=123788) | btcalpha | [BTC-Alpha](https://btc-alpha.com/?r=123788) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://btc-alpha.github.io/api-docs) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![btcbox](https://github.com/user-attachments/assets/1e2cb499-8d0f-4f8f-9464-3c015cfbc76b)](https://www.btcbox.co.jp/) | btcbox | [BtcBox](https://www.btcbox.co.jp/) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://blog.btcbox.jp/en/archives/8762) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![btcmarkets](https://github.com/user-attachments/assets/8c8d6907-3873-4cc4-ad20-e22fba28247e)](https://btcmarkets.net) | btcmarkets | [BTC Markets](https://btcmarkets.net) | [![API Version 3](https://img.shields.io/badge/3-lightgray)](https://api.btcmarkets.net/doc/v3) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![btcturk](https://github.com/user-attachments/assets/10e0a238-9f60-4b06-9dda-edfc7602f1d6)](https://www.btcturk.com) | btcturk | [BTCTurk](https://www.btcturk.com) | [![API Version *](https://img.shields.io/badge/*-lightgray)](https://github.com/BTCTrader/broker-api-docs) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![bybit](https://github.com/user-attachments/assets/97a5d0b3-de10-423d-90e1-6620960025ed)](https://www.bybit.com/invite?ref=XDK12WP) | bybit | [Bybit](https://www.bybit.com/invite?ref=XDK12WP) | [![API Version 5](https://img.shields.io/badge/5-lightgray)](https://bybit-exchange.github.io/docs/inverse/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![cex](https://user-images.githubusercontent.com/1294454/27766442-8ddc33b0-5ed8-11e7-8b98-f786aef0f3c9.jpg)](https://cex.io/r/0/up105393824/0/) | cex | [CEX.IO](https://cex.io/r/0/up105393824/0/) | [![API Version *](https://img.shields.io/badge/*-lightgray)](https://trade.cex.io/docs/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![coinbase](https://user-images.githubusercontent.com/1294454/40811661-b6eceae2-653a-11e8-829e-10bfadb078cf.jpg)](https://www.coinbase.com/join/58cbe25a355148797479dbd2) | coinbase | [Coinbase Advanced](https://www.coinbase.com/join/58cbe25a355148797479dbd2) | [![API Version 2](https://img.shields.io/badge/2-lightgray)](https://developers.coinbase.com/api/v2) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![coinbaseexchange](https://github.com/ccxt/ccxt/assets/43336371/34a65553-88aa-4a38-a714-064bd228b97e)](https://coinbase.com/) | coinbaseexchange | [Coinbase Exchange](https://coinbase.com/) | [![API Version *](https://img.shields.io/badge/*-lightgray)](https://docs.cloud.coinbase.com/exchange/docs/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![coinbaseinternational](https://github.com/ccxt/ccxt/assets/43336371/866ae638-6ab5-4ebf-ab2c-cdcce9545625)](https://international.coinbase.com) | coinbaseinternational | [Coinbase International](https://international.coinbase.com) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://docs.cloud.coinbase.com/intx/docs) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![coincatch](https://github.com/user-attachments/assets/3d49065f-f05d-4573-88a2-1b5201ec6ff3)](https://partner.coincatch.cc/bg/92hy70391729607848548) | coincatch | [CoinCatch](https://partner.coincatch.cc/bg/92hy70391729607848548) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://coincatch.github.io/github.io/en/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![coincheck](https://user-images.githubusercontent.com/51840849/87182088-1d6d6380-c2ec-11ea-9c64-8ab9f9b289f5.jpg)](https://coincheck.com) | coincheck | [coincheck](https://coincheck.com) | [![API Version *](https://img.shields.io/badge/*-lightgray)](https://coincheck.com/documents/exchange/api) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![coinex](https://user-images.githubusercontent.com/51840849/87182089-1e05fa00-c2ec-11ea-8da9-cc73b45abbbc.jpg)](https://www.coinex.com/register?refer_code=yw5fz) | coinex | [CoinEx](https://www.coinex.com/register?refer_code=yw5fz) | [![API Version 2](https://img.shields.io/badge/2-lightgray)](https://docs.coinex.com/api/v2) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![coinmate](https://user-images.githubusercontent.com/51840849/87460806-1c9f3f00-c616-11ea-8c46-a77018a8f3f4.jpg)](https://coinmate.io?referral=YTFkM1RsOWFObVpmY1ZjMGREQmpTRnBsWjJJNVp3PT0) | coinmate | [CoinMate](https://coinmate.io?referral=YTFkM1RsOWFObVpmY1ZjMGREQmpTRnBsWjJJNVp3PT0) | [![API Version *](https://img.shields.io/badge/*-lightgray)](https://coinmate.docs.apiary.io) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![coinmetro](https://github.com/ccxt/ccxt/assets/43336371/e86f87ec-6ba3-4410-962b-f7988c5db539)](https://go.coinmetro.com/?ref=crypto24) | coinmetro | [Coinmetro](https://go.coinmetro.com/?ref=crypto24) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://documenter.getpostman.com/view/3653795/SVfWN6KS) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![coinone](https://user-images.githubusercontent.com/1294454/38003300-adc12fba-323f-11e8-8525-725f53c4a659.jpg)](https://coinone.co.kr) | coinone | [CoinOne](https://coinone.co.kr) | [![API Version 2](https://img.shields.io/badge/2-lightgray)](https://doc.coinone.co.kr) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![coinsph](https://user-images.githubusercontent.com/1294454/225719995-48ab2026-4ddb-496c-9da7-0d7566617c9b.jpg)](https://coins.ph/) | coinsph | [Coins.ph](https://coins.ph/) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://coins-docs.github.io/rest-api) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![coinspot](https://user-images.githubusercontent.com/1294454/28208429-3cacdf9a-6896-11e7-854e-4c79a772a30f.jpg)](https://www.coinspot.com.au/register?code=PJURCU) | coinspot | [CoinSpot](https://www.coinspot.com.au/register?code=PJURCU) | [![API Version *](https://img.shields.io/badge/*-lightgray)](https://www.coinspot.com.au/api) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![cryptocom](https://user-images.githubusercontent.com/1294454/147792121-38ed5e36-c229-48d6-b49a-48d05fc19ed4.jpeg)](https://crypto.com/exch/kdacthrnxt) | cryptocom | [Crypto.com](https://crypto.com/exch/kdacthrnxt) | [![API Version 2](https://img.shields.io/badge/2-lightgray)](https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![cryptomus](https://github.com/user-attachments/assets/8e0b1c48-7c01-4177-9224-f1b01d89d7e7)](https://app.cryptomus.com/signup/?ref=JRP4yj) | cryptomus | [Cryptomus](https://app.cryptomus.com/signup/?ref=JRP4yj) | [![API Version 2](https://img.shields.io/badge/2-lightgray)](https://doc.cryptomus.com/personal) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![deepcoin](https://github.com/user-attachments/assets/671bd35c-770e-4935-9070-f8fb114f79c4)](https://s.deepcoin.com/UzkyODgy) | deepcoin | [DeepCoin](https://s.deepcoin.com/UzkyODgy) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://www.deepcoin.com/docs) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![defx](https://github.com/user-attachments/assets/4e92bace-d7a9-45ea-92be-122168dc87e4)](https://app.defx.com/join/6I2CZ7) | defx | [Defx X](https://app.defx.com/join/6I2CZ7) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://docs.defx.com/docs) | ![DEX - Distributed EXchange](https://img.shields.io/badge/DEX-blue.svg "DEX - Distributed EXchange") | | | +| [![delta](https://user-images.githubusercontent.com/1294454/99450025-3be60a00-2931-11eb-9302-f4fd8d8589aa.jpg)](https://www.delta.exchange/app/signup/?code=IULYNB) | delta | [Delta Exchange](https://www.delta.exchange/app/signup/?code=IULYNB) | [![API Version 2](https://img.shields.io/badge/2-lightgray)](https://docs.delta.exchange) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![deribit](https://user-images.githubusercontent.com/1294454/41933112-9e2dd65a-798b-11e8-8440-5bab2959fcb8.jpg)](https://www.deribit.com/reg-1189.4038) | deribit | [Deribit](https://www.deribit.com/reg-1189.4038) | [![API Version 2](https://img.shields.io/badge/2-lightgray)](https://docs.deribit.com/v2) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![derive](https://github.com/user-attachments/assets/f835b95f-033a-43dd-b6bb-24e698fc498c)](https://www.derive.xyz/invite/3VB0B) | derive | [derive](https://www.derive.xyz/invite/3VB0B) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://docs.derive.xyz/docs/) | ![DEX - Distributed EXchange](https://img.shields.io/badge/DEX-blue.svg "DEX - Distributed EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![digifinex](https://user-images.githubusercontent.com/51840849/87443315-01283a00-c5fe-11ea-8628-c2a0feaf07ac.jpg)](https://www.digifinex.com/en-ww/from/DhOzBg?channelCode=ljaUPp) | digifinex | [DigiFinex](https://www.digifinex.com/en-ww/from/DhOzBg?channelCode=ljaUPp) | [![API Version 3](https://img.shields.io/badge/3-lightgray)](https://docs.digifinex.com) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![exmo](https://user-images.githubusercontent.com/1294454/27766491-1b0ea956-5eda-11e7-9225-40d67b481b8d.jpg)](https://exmo.me/?ref=131685) | exmo | [EXMO](https://exmo.me/?ref=131685) | [![API Version 1.1](https://img.shields.io/badge/1.1-lightgray)](https://exmo.me/en/api_doc?ref=131685) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![fmfwio](https://user-images.githubusercontent.com/1294454/159177712-b685b40c-5269-4cea-ac83-f7894c49525d.jpg)](https://fmfw.io/referral/da948b21d6c92d69) | fmfwio | [FMFW.io](https://fmfw.io/referral/da948b21d6c92d69) | [![API Version 3](https://img.shields.io/badge/3-lightgray)](https://api.fmfw.io/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![foxbit](https://github.com/user-attachments/assets/1f8faca2-ae2f-4222-b33e-5671e7d873dd)](https://app.foxbit.com.br) | foxbit | [Foxbit](https://app.foxbit.com.br) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://docs.foxbit.com.br) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![gate](https://github.com/user-attachments/assets/64f988c5-07b6-4652-b5c1-679a6bf67c85)](https://www.gate.com/share/CCXTGATE) | gate | [Gate](https://www.gate.com/share/CCXTGATE) | [![API Version 4](https://img.shields.io/badge/4-lightgray)](https://www.gate.com/docs/developers/apiv4/en/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![gemini](https://user-images.githubusercontent.com/1294454/27816857-ce7be644-6096-11e7-82d6-3c257263229c.jpg)](https://gemini.com/) | gemini | [Gemini](https://gemini.com/) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://docs.gemini.com/rest-api) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![hashkey](https://github.com/user-attachments/assets/6dd6127b-cc19-4a13-9b29-a98d81f80e98)](https://global.hashkey.com/en-US/register/invite?invite_code=82FQUN) | hashkey | [HashKey Global](https://global.hashkey.com/en-US/register/invite?invite_code=82FQUN) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://hashkeyglobal-apidoc.readme.io/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![hibachi](https://github.com/user-attachments/assets/7301bbb1-4f27-4167-8a55-75f74b14e973)](hibachi.xyz/r/ZBL2YFWIHU) | hibachi | [Hibachi](hibachi.xyz/r/ZBL2YFWIHU) | [![API Version *](https://img.shields.io/badge/*-lightgray)](undefined) | ![DEX - Distributed EXchange](https://img.shields.io/badge/DEX-blue.svg "DEX - Distributed EXchange") | | | +| [![hitbtc](https://user-images.githubusercontent.com/1294454/27766555-8eaec20e-5edc-11e7-9c5b-6dc69fc42f5e.jpg)](https://hitbtc.com/?ref_id=5a5d39a65d466) | hitbtc | [HitBTC](https://hitbtc.com/?ref_id=5a5d39a65d466) | [![API Version 3](https://img.shields.io/badge/3-lightgray)](https://api.hitbtc.com) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![hollaex](https://user-images.githubusercontent.com/1294454/75841031-ca375180-5ddd-11ea-8417-b975674c23cb.jpg)](https://pro.hollaex.com/signup?affiliation_code=QSWA6G) | hollaex | [HollaEx](https://pro.hollaex.com/signup?affiliation_code=QSWA6G) | [![API Version 2](https://img.shields.io/badge/2-lightgray)](https://apidocs.hollaex.com) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![htx](https://user-images.githubusercontent.com/1294454/76137448-22748a80-604e-11ea-8069-6e389271911d.jpg)](https://www.htx.com.vc/invite/en-us/1h?invite_code=6rmm2223) | htx | [HTX](https://www.htx.com.vc/invite/en-us/1h?invite_code=6rmm2223) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://huobiapi.github.io/docs/spot/v1/en/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![hyperliquid](https://github.com/ccxt/ccxt/assets/43336371/b371bc6c-4a8c-489f-87f4-20a913dd8d4b)](https://app.hyperliquid.xyz/) | hyperliquid | [Hyperliquid](https://app.hyperliquid.xyz/) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api) | ![DEX - Distributed EXchange](https://img.shields.io/badge/DEX-blue.svg "DEX - Distributed EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![independentreserve](https://user-images.githubusercontent.com/51840849/87182090-1e9e9080-c2ec-11ea-8e49-563db9a38f37.jpg)](https://www.independentreserve.com) | independentreserve | [Independent Reserve](https://www.independentreserve.com) | [![API Version *](https://img.shields.io/badge/*-lightgray)](https://www.independentreserve.com/API) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![indodax](https://user-images.githubusercontent.com/51840849/87070508-9358c880-c221-11ea-8dc5-5391afbbb422.jpg)](https://indodax.com/ref/testbitcoincoid/1) | indodax | [INDODAX](https://indodax.com/ref/testbitcoincoid/1) | [![API Version 2.0](https://img.shields.io/badge/2.0-lightgray)](https://github.com/btcid/indodax-official-api-docs) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![kraken](https://user-images.githubusercontent.com/51840849/76173629-fc67fb00-61b1-11ea-84fe-f2de582f58a3.jpg)](https://www.kraken.com) | kraken | [Kraken](https://www.kraken.com) | [![API Version 0](https://img.shields.io/badge/0-lightgray)](https://docs.kraken.com/rest/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![krakenfutures](https://user-images.githubusercontent.com/24300605/81436764-b22fd580-9172-11ea-9703-742783e6376d.jpg)](https://futures.kraken.com/) | krakenfutures | [Kraken Futures](https://futures.kraken.com/) | [![API Version 3](https://img.shields.io/badge/3-lightgray)](https://docs.kraken.com/api/docs/futures-api/trading/market-data/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![kucoin](https://user-images.githubusercontent.com/51840849/87295558-132aaf80-c50e-11ea-9801-a2fb0c57c799.jpg)](https://www.kucoin.com/ucenter/signup?rcode=E5wkqe) | kucoin | [KuCoin](https://www.kucoin.com/ucenter/signup?rcode=E5wkqe) | [![API Version 2](https://img.shields.io/badge/2-lightgray)](https://docs.kucoin.com) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![kucoinfutures](https://user-images.githubusercontent.com/1294454/147508995-9e35030a-d046-43a1-a006-6fabd981b554.jpg)](https://futures.kucoin.com/?rcode=E5wkqe) | kucoinfutures | [KuCoin Futures](https://futures.kucoin.com/?rcode=E5wkqe) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://docs.kucoin.com/futures) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![latoken](https://user-images.githubusercontent.com/1294454/61511972-24c39f00-aa01-11e9-9f7c-471f1d6e5214.jpg)](https://latoken.com/invite?r=mvgp2djk) | latoken | [Latoken](https://latoken.com/invite?r=mvgp2djk) | [![API Version 2](https://img.shields.io/badge/2-lightgray)](https://api.latoken.com) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![lbank](https://user-images.githubusercontent.com/1294454/38063602-9605e28a-3302-11e8-81be-64b1e53c4cfb.jpg)](https://www.lbank.com/login/?icode=7QCY) | lbank | [LBank](https://www.lbank.com/login/?icode=7QCY) | [![API Version 2](https://img.shields.io/badge/2-lightgray)](https://www.lbank.com/en-US/docs/index.html) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![luno](https://user-images.githubusercontent.com/1294454/27766607-8c1a69d8-5ede-11e7-930c-540b5eb9be24.jpg)](https://www.luno.com/invite/44893A) | luno | [luno](https://www.luno.com/invite/44893A) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://www.luno.com/en/api) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![mercado](https://user-images.githubusercontent.com/1294454/27837060-e7c58714-60ea-11e7-9192-f05e86adb83f.jpg)](https://www.mercadobitcoin.com.br) | mercado | [Mercado Bitcoin](https://www.mercadobitcoin.com.br) | [![API Version 3](https://img.shields.io/badge/3-lightgray)](https://www.mercadobitcoin.com.br/api-doc) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![mexc](https://user-images.githubusercontent.com/1294454/137283979-8b2a818d-8633-461b-bfca-de89e8c446b2.jpg)](https://www.mexc.com/register?inviteCode=mexc-1FQ1GNu1) | mexc | [MEXC Global](https://www.mexc.com/register?inviteCode=mexc-1FQ1GNu1) | [![API Version 3](https://img.shields.io/badge/3-lightgray)](https://mexcdevelop.github.io/apidocs/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![modetrade](https://github.com/user-attachments/assets/cec2b7f1-3b2b-4502-971b-447ee1937d6b)](https://trade.mode.network?ref=MODETRADE) | modetrade | [Mode Trade](https://trade.mode.network?ref=MODETRADE) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](undefined) | ![DEX - Distributed EXchange](https://img.shields.io/badge/DEX-blue.svg "DEX - Distributed EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![myokx](https://user-images.githubusercontent.com/1294454/152485636-38b19e4a-bece-4dec-979a-5982859ffc04.jpg)](https://www.my.okx.com/join/CCXT2023) | myokx | [MyOKX (EEA)](https://www.my.okx.com/join/CCXT2023) | [![API Version 5](https://img.shields.io/badge/5-lightgray)](https://my.okx.com/docs-v5/en/#overview) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![ndax](https://user-images.githubusercontent.com/1294454/108623144-67a3ef00-744e-11eb-8140-75c6b851e945.jpg)](https://one.ndax.io/bfQiSL) | ndax | [NDAX](https://one.ndax.io/bfQiSL) | [![API Version *](https://img.shields.io/badge/*-lightgray)](https://apidoc.ndax.io/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![novadax](https://user-images.githubusercontent.com/1294454/92337550-2b085500-f0b3-11ea-98e7-5794fb07dd3b.jpg)](https://www.novadax.com.br/?s=ccxt) | novadax | [NovaDAX](https://www.novadax.com.br/?s=ccxt) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://doc.novadax.com/pt-BR/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![oceanex](https://user-images.githubusercontent.com/1294454/58385970-794e2d80-8001-11e9-889c-0567cd79b78e.jpg)](https://oceanex.pro/signup?referral=VE24QX) | oceanex | [OceanEx](https://oceanex.pro/signup?referral=VE24QX) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://api.oceanex.pro/doc/v1) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![okx](https://user-images.githubusercontent.com/1294454/152485636-38b19e4a-bece-4dec-979a-5982859ffc04.jpg)](https://www.okx.com/join/CCXTCOM) | okx | [OKX](https://www.okx.com/join/CCXTCOM) | [![API Version 5](https://img.shields.io/badge/5-lightgray)](https://www.okx.com/docs-v5/en/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![okxus](https://user-images.githubusercontent.com/1294454/152485636-38b19e4a-bece-4dec-979a-5982859ffc04.jpg)](https://www.app.okx.com/join/CCXT2023) | okxus | [OKX (US)](https://www.app.okx.com/join/CCXT2023) | [![API Version 5](https://img.shields.io/badge/5-lightgray)](https://app.okx.com/docs-v5/en/#overview) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![onetrading](https://github.com/ccxt/ccxt/assets/43336371/bdbc26fd-02f2-4ca7-9f1e-17333690bb1c)](https://onetrading.com/) | onetrading | [One Trading](https://onetrading.com/) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://docs.onetrading.com) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![oxfun](https://github.com/ccxt/ccxt/assets/43336371/6a196124-c1ee-4fae-8573-962071b61a85)](https://ox.fun/register?shareAccountId=5ZUD4a7G) | oxfun | [OXFUN](https://ox.fun/register?shareAccountId=5ZUD4a7G) | [![API Version 3](https://img.shields.io/badge/3-lightgray)](https://docs.ox.fun/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![p2b](https://github.com/ccxt/ccxt/assets/43336371/8da13a80-1f0a-49be-bb90-ff8b25164755)](https://p2pb2b.com?referral=ee784c53) | p2b | [p2b](https://p2pb2b.com?referral=ee784c53) | [![API Version 2](https://img.shields.io/badge/2-lightgray)](https://github.com/P2B-team/p2b-api-docs/blob/master/api-doc.md) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![paradex](https://github.com/user-attachments/assets/84628770-784e-4ec4-a759-ec2fbb2244ea)](https://app.paradex.trade/r/ccxt24) | paradex | [Paradex](https://app.paradex.trade/r/ccxt24) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://docs.api.testnet.paradex.trade/) | ![DEX - Distributed EXchange](https://img.shields.io/badge/DEX-blue.svg "DEX - Distributed EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![paymium](https://user-images.githubusercontent.com/51840849/87153930-f0f02200-c2c0-11ea-9c0a-40337375ae89.jpg)](https://www.paymium.com/page/sign-up?referral=eDAzPoRQFMvaAB8sf-qj) | paymium | [Paymium](https://www.paymium.com/page/sign-up?referral=eDAzPoRQFMvaAB8sf-qj) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://github.com/Paymium/api-documentation) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![phemex](https://user-images.githubusercontent.com/1294454/85225056-221eb600-b3d7-11ea-930d-564d2690e3f6.jpg)](https://phemex.com/register?referralCode=EDNVJ) | phemex | [Phemex](https://phemex.com/register?referralCode=EDNVJ) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://phemex-docs.github.io/#overview) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![poloniex](https://user-images.githubusercontent.com/1294454/27766817-e9456312-5ee6-11e7-9b3c-b628ca5626a5.jpg)](https://poloniex.com/signup?c=UBFZJRPJ) | poloniex | [Poloniex](https://poloniex.com/signup?c=UBFZJRPJ) | [![API Version *](https://img.shields.io/badge/*-lightgray)](https://api-docs.poloniex.com/spot/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![probit](https://user-images.githubusercontent.com/51840849/79268032-c4379480-7ea2-11ea-80b3-dd96bb29fd0d.jpg)](https://www.probit.com/r/34608773) | probit | [ProBit](https://www.probit.com/r/34608773) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://docs-en.probit.com) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![timex](https://user-images.githubusercontent.com/1294454/70423869-6839ab00-1a7f-11ea-8f94-13ae72c31115.jpg)](https://timex.io/?refcode=1x27vNkTbP1uwkCck) | timex | [TimeX](https://timex.io/?refcode=1x27vNkTbP1uwkCck) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://plasma-relay-backend.timex.io/swagger-ui/index.html) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![tokocrypto](https://user-images.githubusercontent.com/1294454/183870484-d3398d0c-f6a1-4cce-91b8-d58792308716.jpg)](https://tokocrypto.com) | tokocrypto | [Tokocrypto](https://tokocrypto.com) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://www.tokocrypto.com/apidocs/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![toobit](https://github.com/user-attachments/assets/3fc13870-5406-431b-8be0-2aab69c4f225)](https://www.toobit.com/en-US/r?i=IFFPy0) | toobit | [Toobit](https://www.toobit.com/en-US/r?i=IFFPy0) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://toobit-docs.github.io/apidocs/spot/v1/en/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![upbit](https://user-images.githubusercontent.com/1294454/49245610-eeaabe00-f423-11e8-9cba-4b0aed794799.jpg)](https://upbit.com) | upbit | [Upbit](https://upbit.com) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://docs.upbit.com/docs/%EC%9A%94%EC%B2%AD-%EC%88%98-%EC%A0%9C%ED%95%9C) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![wavesexchange](https://user-images.githubusercontent.com/1294454/84547058-5fb27d80-ad0b-11ea-8711-78ac8b3c7f31.jpg)](https://wx.network) | wavesexchange | [Waves.Exchange](https://wx.network) | [![API Version *](https://img.shields.io/badge/*-lightgray)](https://docs.wx.network) | ![DEX - Distributed EXchange](https://img.shields.io/badge/DEX-blue.svg "DEX - Distributed EXchange") | | | +| [![whitebit](https://user-images.githubusercontent.com/1294454/66732963-8eb7dd00-ee66-11e9-849b-10d9282bb9e0.jpg)](https://whitebit.com/referral/d9bdf40e-28f2-4b52-b2f9-cd1415d82963) | whitebit | [WhiteBit](https://whitebit.com/referral/d9bdf40e-28f2-4b52-b2f9-cd1415d82963) | [![API Version 4](https://img.shields.io/badge/4-lightgray)](https://github.com/whitebit-exchange/api-docs) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![woo](https://user-images.githubusercontent.com/1294454/150730761-1a00e5e0-d28c-480f-9e65-089ce3e6ef3b.jpg)](https://woox.io/register?ref=DIJT0CNL) | woo | [WOO X](https://woox.io/register?ref=DIJT0CNL) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://docs.woox.io/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![woofipro](https://github.com/user-attachments/assets/9ba21b8a-a9c7-4770-b7f1-ce3bcbde68c1)](https://dex.woo.org/en/trade?ref=CCXT) | woofipro | [WOOFI PRO](https://dex.woo.org/en/trade?ref=CCXT) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://orderly.network/docs/build-on-evm/building-on-evm) | ![DEX - Distributed EXchange](https://img.shields.io/badge/DEX-blue.svg "DEX - Distributed EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![xt](https://user-images.githubusercontent.com/14319357/232636712-466df2fc-560a-4ca4-aab2-b1d954a58e24.jpg)](https://www.xt.com/en/accounts/register?ref=9PTM9VW) | xt | [XT](https://www.xt.com/en/accounts/register?ref=9PTM9VW) | [![API Version 4](https://img.shields.io/badge/4-lightgray)](https://doc.xt.com/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![yobit](https://user-images.githubusercontent.com/1294454/27766910-cdcbfdae-5eea-11e7-9859-03fea873272d.jpg)](https://www.yobit.net) | yobit | [YoBit](https://www.yobit.net) | [![API Version 3](https://img.shields.io/badge/3-lightgray)](https://www.yobit.net/en/api/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![zaif](https://user-images.githubusercontent.com/1294454/27766927-39ca2ada-5eeb-11e7-972f-1b4199518ca6.jpg)](https://zaif.jp) | zaif | [Zaif](https://zaif.jp) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://techbureau-api-document.readthedocs.io/ja/latest/index.html) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![zonda](https://user-images.githubusercontent.com/1294454/159202310-a0e38007-5e7c-4ba9-a32f-c8263a0291fe.jpg)](https://auth.zondaglobal.com/ref/jHlbB4mIkdS1) | zonda | [Zonda](https://auth.zondaglobal.com/ref/jHlbB4mIkdS1) | [![API Version *](https://img.shields.io/badge/*-lightgray)](https://docs.zondacrypto.exchange/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | + + +The list above is updated frequently, new crypto markets, exchanges, bug fixes, and API endpoints are introduced on a regular basis. See the [Manual](https://github.com/ccxt/ccxt/wiki/) for more details. If you can't find a cryptocurrency exchange in the list above and want it to be added, post a link to it by opening an issue here on GitHub or send us an email. + +The library is under [MIT license](https://github.com/ccxt/ccxt/blob/master/LICENSE.txt), that means it's absolutely free for any developer to build commercial and opensource software on top of it, but use it at your own risk with no warranties, as is. + +--- + +## Install + +The easiest way to install the CCXT library is to use a package manager: + +- [ccxt in **NPM**](https://www.npmjs.com/package/ccxt) (JavaScript / Node v7.6+) +- [ccxt in **PyPI**](https://pypi.python.org/pypi/ccxt) (Python 3.7.0+) +- [ccxt in **Packagist/Composer**](https://packagist.org/packages/ccxt/ccxt) (PHP 8.1+) +- [ccxt in **Nuget**](https://www.nuget.org/packages/ccxt) (netstandard 2.0) +- [ccxt in **GO**](https://pkg.go.dev/github.com/ccxt/ccxt/go/v4) + +This library is shipped as an all-in-one module implementation with minimalistic dependencies and requirements: + +- [js/](https://github.com/ccxt/ccxt/blob/master/js/) in JavaScript +- [python/](https://github.com/ccxt/ccxt/blob/master/python/) in Python (generated from TS) +- [php/](https://github.com/ccxt/ccxt/blob/master/php/) in PHP (generated from TS) +- [cs/](https://github.com/ccxt/ccxt/blob/master/cs/) in C# (generated from TS) +- [go/](https://github.com/ccxt/ccxt/blob/master/go/) in Go (generated from TS) + +You can also clone it into your project directory from [ccxt GitHub repository](https://github.com/ccxt/ccxt): + +```shell +git clone https://github.com/ccxt/ccxt.git # including 1GB of commit history + +# or + +git clone https://github.com/ccxt/ccxt.git --depth 1 # avoid downloading 1GB of commit history +``` + +### JavaScript (NPM) + +JavaScript version of CCXT works in both Node and web browsers. Requires ES6 and `async/await` syntax support (Node 7.6.0+). When compiling with Webpack and Babel, make sure it is [not excluded](https://github.com/ccxt/ccxt/issues/225#issuecomment-331905178) in your `babel-loader` config. + +[ccxt in **NPM**](https://www.npmjs.com/package/ccxt) + +```shell +npm install ccxt +``` + +```JavaScript +//cjs +var ccxt = require ('ccxt') +console.log (ccxt.exchanges) // print all available exchanges +``` +```Javascript +//esm +import {version, exchanges} from 'ccxt'; +console.log(version, Object.keys(exchanges)); +``` + +### JavaScript (for use with the ` +``` + +Creates a global `ccxt` object: + +```JavaScript +console.log (ccxt.exchanges) // print all available exchanges +``` + +### Python + +[ccxt in **PyPI**](https://pypi.python.org/pypi/ccxt) + +```shell +pip install ccxt +``` + +```Python +import ccxt +print(ccxt.exchanges) # print a list of all available exchange classes +``` + +The library supports concurrent asynchronous mode with asyncio and async/await in Python 3.7.0+ + +```Python +import ccxt.async_support as ccxt # link against the asynchronous version of ccxt +``` + +#### orjson support + +CCXT also supports `orjson` for parsing JSON since it is much faster than the builtin library. This is especially important when using websockets because some exchanges return big messages that need to be parsed and dispatched as quickly as possible. + +However, `orjson` is not enabled by default because it is not supported by every python interpreter. If you want to opt-in, you just need to install it (`pip install orjson`) on your local environment. CCXT will detect the installion and pick it up automatically. + +#### ECDSA Support + +Some exchanges, such as Hyperliquid, Binance, and Paradex use **ECDSA** for request signing. +By default, CCXT includes a pure Python ECDSA implementation that ensures compatibility across all environments. However, this implementation may not meet the performance requirements of latency-sensitive applications. + +To address this, CCXT also supports the Coincurve library, which dramatically reduces signing time from approximately 45 ms to under 0.05 ms. + +For optimal performance, we recommend installing Coincurve via: + +``` +pip install coincurve +``` + +Once installed, CCXT will automatically detect and use it. + +### PHP + +[ccxt in PHP with **Packagist/Composer**](https://packagist.org/packages/ccxt/ccxt) (PHP 8.1+) + +It requires common PHP modules: + +- cURL +- mbstring (using UTF-8 is highly recommended) +- PCRE +- iconv +- gmp + +```PHP +include "ccxt.php"; +var_dump (\ccxt\Exchange::$exchanges); // print a list of all available exchange classes +``` + +The library supports concurrent asynchronous mode using tools from [ReactPHP](https://reactphp.org/) in PHP 8.1+. Read the [Manual](https://github.com/ccxt/ccxt/wiki/) for more details. + +### .net/C# + +[ccxt in C# with **Nuget**](https://www.nuget.org/packages/ccxt) (netstandard 2.0 and netstandard 2.1) +```c# +using ccxt; +Console.WriteLine(ccxt.Exchanges) // check this later +``` + +### Go + +[ccxt in GO with **PKG**](https://pkg.go.dev/github.com/ccxt/ccxt/go/v4) + +```shell +go install github.com/ccxt/ccxt/go/v4@latest +``` + +```Go +import "ccxt" +fmt.Println(ccxt.Exchanges) +``` + +### Docker + +You can get CCXT installed in a container along with all the supported languages and dependencies. This may be useful if you want to contribute to CCXT (e.g. run the build scripts and tests — please see the [Contributing](https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md) document for the details on that). + +Using `docker-compose` (in the cloned CCXT repository): + +```shell +docker-compose run --rm ccxt +``` + +You don't need the Docker image if you're not going to develop CCXT. If you just want to use CCXT – just install it as a regular package into your project. + +--- + +## Usage + +### Intro + +The CCXT library consists of a public part and a private part. Anyone can use the public part immediately after installation. Public APIs provide unrestricted access to public information for all exchange markets without the need to register a user account or have an API key. + +Public APIs include the following: + +- market data +- instruments/trading pairs +- price feeds (exchange rates) +- order books +- trade history +- tickers +- OHLC(V) for charting +- other public endpoints + +In order to trade with private APIs you need to obtain API keys from an exchange's website. It usually means signing up to the exchange and creating API keys for your account. Some exchanges require personal info or identification. Sometimes verification may be necessary as well. In this case you will need to register yourself, this library will not create accounts or API keys for you. Some exchanges expose API endpoints for registering an account, but most exchanges don't. You will have to sign up and create API keys on their websites. + +Private APIs allow the following: + +- manage personal account info +- query account balances +- trade by making market and limit orders +- deposit and withdraw fiat and crypto funds +- query personal orders +- get ledger history +- transfer funds between accounts +- use merchant services + +This library implements full public and private REST and WebSocket APIs for all exchanges in TypeScript, JavaScript, PHP and Python. + +The CCXT library supports both camelcase notation (preferred in TypeScript and JavaScript) and underscore notation (preferred in Python and PHP), therefore all methods can be called in either notation or coding style in any language. + +```JavaScript +// both of these notations work in JavaScript/Python/PHP +exchange.methodName () // camelcase pseudocode +exchange.method_name () // underscore pseudocode +``` + +Read the [Manual](https://github.com/ccxt/ccxt/wiki/) for more details. + +### JavaScript + +**CCXT now supports ESM and CJS modules** + +#### CJS + +```JavaScript +// cjs example +'use strict'; +const ccxt = require ('ccxt'); + +(async function () { + let kraken = new ccxt.kraken () + let bitfinex = new ccxt.bitfinex ({ verbose: true }) + let huobipro = new ccxt.huobipro () + let okcoinusd = new ccxt.okcoin ({ + apiKey: 'YOUR_PUBLIC_API_KEY', + secret: 'YOUR_SECRET_PRIVATE_KEY', + }) + + const exchangeId = 'binance' + , exchangeClass = ccxt[exchangeId] + , exchange = new exchangeClass ({ + 'apiKey': 'YOUR_API_KEY', + 'secret': 'YOUR_SECRET', + }) + + console.log (kraken.id, await kraken.loadMarkets ()) + console.log (bitfinex.id, await bitfinex.loadMarkets ()) + console.log (huobipro.id, await huobipro.loadMarkets ()) + + console.log (kraken.id, await kraken.fetchOrderBook (kraken.symbols[0])) + console.log (bitfinex.id, await bitfinex.fetchTicker ('BTC/USD')) + console.log (huobipro.id, await huobipro.fetchTrades ('ETH/USDT')) + + console.log (okcoinusd.id, await okcoinusd.fetchBalance ()) + + // sell 1 BTC/USD for market price, sell a bitcoin for dollars immediately + console.log (okcoinusd.id, await okcoinusd.createMarketSellOrder ('BTC/USD', 1)) + + // buy 1 BTC/USD for $2500, you pay $2500 and receive ฿1 when the order is closed + console.log (okcoinusd.id, await okcoinusd.createLimitBuyOrder ('BTC/USD', 1, 2500.00)) + + // pass/redefine custom exchange-specific order params: type, amount, price or whatever + // use a custom order type + bitfinex.createLimitSellOrder ('BTC/USD', 1, 10, { 'type': 'trailing-stop' }) + +}) (); +``` +#### ESM + +```Javascript +//esm example +import {version, binance} from 'ccxt'; + +console.log(version); +const exchange = new binance(); +const ticker = await exchange.fetchTicker('BTC/USDT'); +console.log(ticker); +``` + +### Python + +```Python +# coding=utf-8 + +import ccxt + +hitbtc = ccxt.hitbtc({'verbose': True}) +bitmex = ccxt.bitmex() +huobipro = ccxt.huobipro() +exmo = ccxt.exmo({ + 'apiKey': 'YOUR_PUBLIC_API_KEY', + 'secret': 'YOUR_SECRET_PRIVATE_KEY', +}) +kraken = ccxt.kraken({ + 'apiKey': 'YOUR_PUBLIC_API_KEY', + 'secret': 'YOUR_SECRET_PRIVATE_KEY', +}) + +exchange_id = 'binance' +exchange_class = getattr(ccxt, exchange_id) +exchange = exchange_class({ + 'apiKey': 'YOUR_API_KEY', + 'secret': 'YOUR_SECRET', +}) + +hitbtc_markets = hitbtc.load_markets() + +print(hitbtc.id, hitbtc_markets) +print(bitmex.id, bitmex.load_markets()) +print(huobipro.id, huobipro.load_markets()) + +print(hitbtc.fetch_order_book(hitbtc.symbols[0])) +print(bitmex.fetch_ticker('BTC/USD')) +print(huobipro.fetch_trades('LTC/USDT')) + +print(exmo.fetch_balance()) + +# sell one ฿ for market price and receive $ right now +print(exmo.id, exmo.create_market_sell_order('BTC/USD', 1)) + +# limit buy BTC/EUR, you pay €2500 and receive ฿1 when the order is closed +print(exmo.id, exmo.create_limit_buy_order('BTC/EUR', 1, 2500.00)) + +# pass/redefine custom exchange-specific order params: type, amount, price, flags, etc... +kraken.create_market_buy_order('BTC/USD', 1, {'trading_agreement': 'agree'}) +``` + +### PHP + +```PHP +include 'ccxt.php'; + +$poloniex = new \ccxt\poloniex (); +$bittrex = new \ccxt\bittrex (array ('verbose' => true)); +$quoinex = new \ccxt\quoinex (); +$zaif = new \ccxt\zaif (array ( + 'apiKey' => 'YOUR_PUBLIC_API_KEY', + 'secret' => 'YOUR_SECRET_PRIVATE_KEY', +)); +$hitbtc = new \ccxt\hitbtc (array ( + 'apiKey' => 'YOUR_PUBLIC_API_KEY', + 'secret' => 'YOUR_SECRET_PRIVATE_KEY', +)); + +$exchange_id = 'binance'; +$exchange_class = "\\ccxt\\$exchange_id"; +$exchange = new $exchange_class (array ( + 'apiKey' => 'YOUR_API_KEY', + 'secret' => 'YOUR_SECRET', +)); + +$poloniex_markets = $poloniex->load_markets (); + +var_dump ($poloniex_markets); +var_dump ($bittrex->load_markets ()); +var_dump ($quoinex->load_markets ()); + +var_dump ($poloniex->fetch_order_book ($poloniex->symbols[0])); +var_dump ($bittrex->fetch_trades ('BTC/USD')); +var_dump ($quoinex->fetch_ticker ('ETH/EUR')); +var_dump ($zaif->fetch_ticker ('BTC/JPY')); + +var_dump ($zaif->fetch_balance ()); + +// sell 1 BTC/JPY for market price, you pay ¥ and receive ฿ immediately +var_dump ($zaif->id, $zaif->create_market_sell_order ('BTC/JPY', 1)); + +// buy BTC/JPY, you receive ฿1 for ¥285000 when the order closes +var_dump ($zaif->id, $zaif->create_limit_buy_order ('BTC/JPY', 1, 285000)); + +// set a custom user-defined id to your order +$hitbtc->create_order ('BTC/USD', 'limit', 'buy', 1, 3000, array ('clientOrderId' => '123')); +``` + +### .net/C# + +```C# +using ccxt; // importing ccxt +namespace Project; +class Project { + public async static Task CreateOrder() { + var exchange = new Binance(); + exchange.apiKey = "my api key"; + exchange.secret = "my secret"; + // always use the capitalized method (CreateOrder instead of createOrder) + var order = await exchange.CreateOrder("BTC/USDT", "limit", "buy", 1, 50); + Console.WriteLine("Placed Order, order id: " + order.id); + } +} +``` + +### Go + +```Go +package main +import ( + "github.com/ccxt/ccxt/go/v4/go" + "fmt" +) + +func main() { + exchange := ccxt.NewBinance(map[string]interface{}{ + "apiKey": "MY KEY", + "secret": "MY SECRET", + }) + orderParams := map[string]interface{}{ + "clientOrderId": "myOrderId68768678", + } + + exchange.LoadMarkets() + + order, err := exchange.CreateOrder("BTC/USDT", "limit", "buy", 0.001, ccxt.WithCreateOrderPrice(6000), ccxt.WithCreateOrderParams(orderParams)) + if err != nil { + if ccxtError, ok := err.(*ccxt.Error); ok { + if ccxtError.Type == "InvalidOrder" { + fmt.Println("Invalid order") + } else { + fmt.Println("Some other error") + } + } + } else { + fmt.Println(*order.Id) + } + + + // fetching OHLCV + ohlcv, err := exchange.FetchOHLCV("BTC/USDT", ccxt.WithFetchOHLCVTimeframe("5m"), ccxt.WithFetchOHLCVLimit(100)) + + if err != nil { + fmt.Println("Error: ", err) + } else { + fmt.Println("Got OHLCV!") + } +} +``` + +#### Optional parameters + +Unlike Javascript/Python/PHP/C# Go does not support "traditional" optional parameters like `function a(optional = false)`. However, the CCXT language and structure have some methods with optional params, and since the Go language is transpiled from the Typescript source, we had to find a way of representing them. + +We have decided to "go" (pun intended) with Option structs and the `WithX` methods. + +For example, this function `FetchMyTrades` supports 4 different "optional" parameters, symbol, since, limit, and params. + +```Golang +func (this *Binance) FetchMyTrades(options ...FetchMyTradesOptions) ([]Trade, error) +``` + +And we can provide them by doing + +```Golang +trades, error := exchange.FetchMyTrades(ccxt.withFetchMyTradesSymbol("BTC/USDT"), ccxt.WithFetchOHLCVLimit(5), ccxt.WithFetchMyTradesParams(orderParams)) +``` + +Lastly, just because the signature dictates that some argument like `symbol` is optional, it will depend from exchange to exchange and you might need to provide it to avoid getting a `SymbolRequired` error. + +You can check different examples in the `examples/go` folder. + +## CCXT CLI + +Read the documentation for more information and details: [docs](https://github.com/ccxt/ccxt/tree/master/cli/README.md) + +CCXT also provides a command-line interface (CLI) that enables direct interaction with any supported exchange from the terminal. You can quickly check balances, place orders, or fetch trade data—without the need to write or execute custom code. This is especially useful for simple or time-sensitive tasks that don’t warrant the overhead of building a full application. + +### Installation + +The CLI is available as a npm package and can be installed by doing + +``` +npm i ccxt-cli -g +``` + +### Usage + +You can use the `--help` option to view a general overview of how the CLI works. The tool allows you to invoke any CCXT method by specifying the exchange id, the methodName, and any required arguments. + +Examples: + +``` +ccxt binance createOrder BTC/USDT market buy 0.1 // places an order +``` +If you are not sure which arguments should be provided you can always use the `explain` command. + +``` +ccxt explain createOrder +``` + +result: + +``` +Method: createOrder +Usage: + binance createOrder [price] [params] + +Arguments: + - symbol (required) — Market symbol e.g., BTC/USDT + - type (required) — (no description available) + - side (required) — order side e.g., buy or sell + - amount (required) — (no description available) + - price (optional) — Price per unit of asset e.g., 26000.50 + - params (optional) — Extra parameters for the exchange e.g., { "recvWindow": 5000 } +``` + +You can easily provide API keys by setting them as environment varibales eg: `BINANCE_APIKEY="XXXX"` or adding them to the config file located at `$CACHE/config.json` + +## Contributing + +Please read the [CONTRIBUTING](https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md) document before making changes that you would like adopted in the code. Also, read the [Manual](https://github.com/ccxt/ccxt/wiki) for more details. + +## Support Developer Team + +We are investing a significant amount of time into the development of this library. If CCXT made your life easier and you want to help us improve it further, or if you want to speed up development of new features and exchanges, please support us with a tip. We appreciate all contributions! + +### Sponsors + +Support this project by becoming a sponsor. + +[[Become a sponsor](https://opencollective.com/ccxt#sponsor)] + + + + + + + + + + + + +### Supporters + +Support this project by becoming a supporter. Your avatar will show up here with a link to your website. + +[[Become a supporter](https://opencollective.com/ccxt#supporter)] + + + + + + + + + + + + +### Backers + +Thank you to all our backers! [[Become a backer](https://opencollective.com/ccxt#backer)] + + + +Thank you! + +## Social + +- [![Twitter](https://img.shields.io/twitter/follow/ccxt_official?style=social)](https://twitter.com/ccxt_official) Follow us on Twitter +- [![Medium](https://img.shields.io/badge/read-our%20blog-black?logo=medium)](https://medium.com/@ccxt) Read our blog on Medium +- [![Discord](https://img.shields.io/discord/690203284119617602?logo=discord&logoColor=white)](https://discord.gg/dhzSKYU) Join our Discord +- [![Telegram Announcements](https://img.shields.io/badge/CCXT-Channel-blue?logo=telegram)](https://t.me/ccxt_announcements) CCXT Channel on Telegram (important announcements) +- [![Telegram Chat](https://img.shields.io/badge/CCXT-Chat-blue?logo=telegram)](https://t.me/ccxt_chat) CCXT Chat on Telegram (technical support) + +## Star History + +[![Star History Chart](https://api.star-history.com/svg?repos=ccxt/ccxt&type=Date)](https://star-history.com/#ccxt/ccxt&Date) + +## Disclaimer + +CCXT is not a service nor a server. CCXT is a software. **CCXT is a free open source non-custodian API broker software under MIT license**. + +- **Non-custodian** means CCXT is not an intermediary in trading, it does not hold traders' money at any point in time, traders install CCXT and use CCXT to talk to exchanges directly. +- **MIT license** means CCXT can be used for any purpose, but use at your own risk without any warranties. +- **API broker** means CCXT is funded with rebates from exchanges' API broker programs and it is an official API broker with many exchanges, all rebates and related fees are handled by the exchanges solely in accordance with exchanges' respective terms and conditions established by each partner exchange. +- **Free software** means CCXT is free to use and has no hidden fees, with CCXT traders pay the same trading fees they would pay to the exchanges directly. +- **Open source** means anyone is allowed to use it, to look inside the code and to change everything, including other brokers. + +*CCXT has joined Hyperliquid’s Builder Codes program (see announcement) and may also utilize its referral code, which offers users a 4% fee discount on their first 25 million in trading volume.* + +## Contact Us + +For business inquiries: info@ccxt.trade diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..2f2d62f --- /dev/null +++ b/README.rst @@ -0,0 +1,702 @@ +CCXT – CryptoCurrency eXchange Trading Library +============================================== + +`Build Status `__ `npm `__ `PyPI `__ `NPM Downloads `__ `Discord `__ `Supported Exchanges `__ `Open Collective `__ +`Twitter Follow `__ + +A cryptocurrency trading API with more than 100 exchanges in JavaScript / TypeScript / Python / C# / PHP / Go. + +Install · Usage · `Manual `__ · `FAQ `__ · `Examples `__ · `Contributing `__ · Social +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The **CCXT** library is used to connect and trade with cryptocurrency exchanges and payment processing services worldwide. It provides quick access to market data for storage, analysis, visualization, indicator development, algorithmic trading, strategy backtesting, bot programming, and related software engineering. + +It is intended to be used by **coders, developers, technically-skilled traders, data-scientists and financial analysts** for building trading algorithms. + +Current feature list: + +- support for many cryptocurrency exchanges — more coming soon +- fully implemented public and private APIs +- optional normalized data for cross-exchange analytics and arbitrage +- an out of the box unified API that is extremely easy to integrate +- works in Node 7.6+, Python 3, PHP 5.4+, and web browsers + +Sponsored Promotion +------------------- + ++--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| `Bitvavo – Trade The Future `__ | ++==================================================================================================================================================================================================================================================+ +| `CCXT Pro – A JavaScript / Python / PHP cryptocurrency exchange trading WebSocket API for professionals `__ `A JavaScript / Python / PHP cryptocurrency exchange trading WebSocket API for professionals `__ | ++--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + +See Also +-------- + +- \ `Quadency `__\   `Quadency `__ — professional crypto terminal, algo trading, and unified streaming APIs. +- \ `TabTrader `__\   `TabTrader `__ — trading on all exchanges in one app. Avaliable on `Android `__ and `iOS `__. +- \ `Currency.com `__\   `Currency.com `__ — Award-winning regulated tokenized assets platform with 1500+ available tokens and cryptos. + +Certified Cryptocurrency Exchanges +---------------------------------- + ++-----------------------------------------------------------------------------+-----------+-----------------------------------------------------------------------------+-----+---------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +|        logo        | id | name | ver | doc | certified | pro | ++=============================================================================+===========+=============================================================================+=====+=======================================================================================+======================================================================+=================================+ +| `binance `__ | binance | `Binance `__ | \* | `API `__ | `CCXT Certified `__ | `CCXT Pro `__ | ++-----------------------------------------------------------------------------+-----------+-----------------------------------------------------------------------------+-----+---------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `bitfinex `__ | bitfinex | `Bitfinex `__ | 1 | `API `__ | `CCXT Certified `__ | `CCXT Pro `__ | ++-----------------------------------------------------------------------------+-----------+-----------------------------------------------------------------------------+-----+---------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `bittrex `__ | bittrex | `Bittrex `__ | 1.1 | `API `__ | `CCXT Certified `__ | `CCXT Pro `__ | ++-----------------------------------------------------------------------------+-----------+-----------------------------------------------------------------------------+-----+---------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `bitvavo `__ | bitvavo | `Bitvavo `__ | 2 | `API `__ | `CCXT Certified `__ | `CCXT Pro `__ | ++-----------------------------------------------------------------------------+-----------+-----------------------------------------------------------------------------+-----+---------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `bytetrade `__ | bytetrade | `ByteTrade `__ | \* | `API `__ | `CCXT Certified `__ | | ++-----------------------------------------------------------------------------+-----------+-----------------------------------------------------------------------------+-----+---------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `eterbase `__ | eterbase | `Eterbase `__ | 1 | `API `__ | `CCXT Certified `__ | | ++-----------------------------------------------------------------------------+-----------+-----------------------------------------------------------------------------+-----+---------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `ftx `__ | ftx | `FTX `__ | \* | `API `__ | `CCXT Certified `__ | `CCXT Pro `__ | ++-----------------------------------------------------------------------------+-----------+-----------------------------------------------------------------------------+-----+---------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `idex `__ | idex | `IDEX `__ | \* | `API `__ | `CCXT Certified `__ | | ++-----------------------------------------------------------------------------+-----------+-----------------------------------------------------------------------------+-----+---------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `kraken `__ | kraken | `Kraken `__ | 0 | `API `__ | `CCXT Certified `__ | `CCXT Pro `__ | ++-----------------------------------------------------------------------------+-----------+-----------------------------------------------------------------------------+-----+---------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `poloniex `__ | poloniex | `Poloniex `__ | \* | `API `__ | `CCXT Certified `__ | `CCXT Pro `__ | ++-----------------------------------------------------------------------------+-----------+-----------------------------------------------------------------------------+-----+---------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `upbit `__ | upbit | `Upbit `__ | 1 | `API `__ | `CCXT Certified `__ | `CCXT Pro `__ | ++-----------------------------------------------------------------------------+-----------+-----------------------------------------------------------------------------+-----+---------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ + +Supported Cryptocurrency Exchange Markets +----------------------------------------- + +The CCXT library currently supports the following 124 cryptocurrency exchange markets and trading APIs: + ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| name | ver | doc | certified | pro | ++=========================================================================================+=====+=================================================================================================+======================================================================+=================================+ +| `1BTCXE `__ | \* | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `ACX `__ | 2 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `ANXPro `__ | \* | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `AOFEX `__ | \* | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `BCEX `__ | 1 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `Bequant `__ | 2 | `API `__ | | `CCXT Pro `__ | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `Bibox `__ | 1 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `BigONE `__ | 3 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `Binance `__ | \* | `API `__ | `CCXT Certified `__ | `CCXT Pro `__ | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `Binance Jersey `__ | \* | `API `__ | | `CCXT Pro `__ | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `Binance US `__ | \* | `API `__ | | `CCXT Pro `__ | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `Bit2C `__ | \* | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `bitbank `__ | 1 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `BitBay `__ | \* | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `Bitfinex `__ | 1 | `API `__ | `CCXT Certified `__ | `CCXT Pro `__ | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `Bitfinex `__ | 2 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `bitFlyer `__ | 1 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `Bitforex `__ | 1 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `Bithumb `__ | \* | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `bitkk `__ | 1 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `BitMart `__ | 2 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `BitMax `__ | 1 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `BitMEX `__ | 1 | `API `__ | | `CCXT Pro `__ | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `Bitso `__ | 3 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `Bitstamp `__ | 2 | `API `__ | | `CCXT Pro `__ | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `Bitstamp `__ | 1 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `Bittrex `__ | 1.1 | `API `__ | `CCXT Certified `__ | `CCXT Pro `__ | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `Bitvavo `__ | 2 | `API `__ | `CCXT Certified `__ | `CCXT Pro `__ | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `Bit-Z `__ | 2 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `BL3P `__ | 1 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `Bleutrade `__ | \* | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `Braziliex `__ | \* | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `BTC-Alpha `__ | 1 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `BtcBox `__ | 1 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `BTC Markets `__ | \* | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `BtcTrade.im `__ | \* | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `BTC Trade UA `__ | \* | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `BTCTurk `__ | \* | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `Buda `__ | 2 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `BW `__ | 1 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `Bybit `__ | 2 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `ByteTrade `__ | \* | `API `__ | `CCXT Certified `__ | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `CEX.IO `__ | \* | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `ChileBit `__ | 1 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `Coinbase `__ | 2 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `Coinbase Prime `__ | \* | `API `__ | | `CCXT Pro `__ | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `Coinbase Pro `__ | \* | `API `__ | | `CCXT Pro `__ | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `coincheck `__ | \* | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `CoinEgg `__ | \* | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `CoinEx `__ | 1 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `CoinFalcon `__ | 1 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `coinfloor `__ | \* | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `Coingi `__ | \* | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `CoinMarketCap `__ | 1 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `CoinMate `__ | \* | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `CoinOne `__ | 2 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `CoinSpot `__ | \* | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `CoolCoin `__ | \* | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `COSS `__ | 1 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `CREX24 `__ | 2 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `Currency.com `__ | 1 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `Deribit `__ | 2 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `DigiFinex `__ | 3 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `DSX `__ | 3 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `Eterbase `__ | 1 | `API `__ | `CCXT Certified `__ | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `EXMO `__ | 1.1 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `EXX `__ | \* | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `FCoin `__ | 2 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `FCoinJP `__ | 2 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `flowBTC `__ | 1 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `FoxBit `__ | 1 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `FTX `__ | \* | `API `__ | `CCXT Certified `__ | `CCXT Pro `__ | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `FYB-SE `__ | \* | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `Gate.io `__ | 2 | `API `__ | | `CCXT Pro `__ | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `Gemini `__ | 1 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `HBTC `__ | 1 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `HitBTC `__ | 2 | `API `__ | | `CCXT Pro `__ | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `HollaEx `__ | 1 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `Huobi Pro `__ | 1 | `API `__ | | `CCXT Pro `__ | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `Huobi Russia `__ | 1 | `API `__ | | `CCXT Pro `__ | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `ICE3X `__ | 1 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `IDEX `__ | \* | `API `__ | `CCXT Certified `__ | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `Independent Reserve `__ | \* | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `INDODAX `__ | 2.0 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `itBit `__ | 1 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `KKEX `__ | 2 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `Kraken `__ | 0 | `API `__ | `CCXT Certified `__ | `CCXT Pro `__ | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `KuCoin `__ | 2 | `API `__ | | `CCXT Pro `__ | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `Kuna `__ | 2 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `LakeBTC `__ | 2 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `Latoken `__ | 1 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `LBank `__ | 1 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `Liquid `__ | 2 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `LiveCoin `__ | \* | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `luno `__ | 1 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `Lykke `__ | 1 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `Mercado Bitcoin `__ | 3 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `MixCoins `__ | 1 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `OceanEx `__ | 1 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `OKCoin `__ | 3 | `API `__ | | `CCXT Pro `__ | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `OKEX `__ | 3 | `API `__ | | `CCXT Pro `__ | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `Paymium `__ | 1 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `Poloniex `__ | \* | `API `__ | `CCXT Certified `__ | `CCXT Pro `__ | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `ProBit `__ | 1 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `qTrade `__ | 1 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `RightBTC `__ | \* | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `SouthXchange `__ | \* | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `STEX `__ | 3 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `Stronghold `__ | 1 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `SurBitcoin `__ | 1 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `The Ocean `__ | 1 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `TheRockTrading `__ | 1 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `TideBit `__ | 2 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `Tidex `__ | 3 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `TimeX `__ | 1 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `TOP.Q `__ | 1 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `Upbit `__ | 1 | `API `__ | `CCXT Certified `__ | `CCXT Pro `__ | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `Vaultoro `__ | 1 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `VBTC `__ | 1 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `WhiteBit `__ | 2 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `xBTCe `__ | 1 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `YoBit `__ | 3 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `Zaif `__ | 1 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ +| `ZB `__ | 1 | `API `__ | | | ++-----------------------------------------------------------------------------------------+-----+-------------------------------------------------------------------------------------------------+----------------------------------------------------------------------+---------------------------------+ + +The list above is updated frequently, new crypto markets, exchanges, bug fixes, and API endpoints are introduced on a regular basis. See the `Manual `__ for more details. If you can’t find a cryptocurrency exchange in the list above and want it to be added, post a link to it by opening an issue here on GitHub or send us an email. + +The library is under `MIT license `__, that means it’s absolutely free for any developer to build commercial and opensource software on top of it, but use it at your own risk with no warranties, as is. + +-------------- + +Install +------- + +The easiest way to install the CCXT library is to use a package manager: + +- `ccxt in NPM `__ (JavaScript / Node v7.6+) +- `ccxt in PyPI `__ (Python 3.5.3+) +- `ccxt in Packagist/Composer `__ (PHP 5.4+) + +This library is shipped as an all-in-one module implementation with minimalistic dependencies and requirements: + +- ```js/`` `__ in JavaScript +- ```python/`` `__ in Python (generated from JS) +- ```php/`` `__ in PHP (generated from JS) + +You can also clone it into your project directory from `ccxt GitHub repository `__: + +.. code:: shell + + git clone https://github.com/ccxt/ccxt.git + +JavaScript (NPM) +~~~~~~~~~~~~~~~~ + +JavaScript version of CCXT works in both Node and web browsers. Requires ES6 and ``async/await`` syntax support (Node 7.6.0+). When compiling with Webpack and Babel, make sure it is `not excluded `__ in your ``babel-loader`` config. + +`ccxt in NPM `__ + +.. code:: shell + + npm install ccxt + +.. code:: javascript + + var ccxt = require ('ccxt') + + console.log (ccxt.exchanges) // print all available exchanges + +JavaScript (for use with the `` + +Creates a global ``ccxt`` object: + +.. code:: javascript + + console.log (ccxt.exchanges) // print all available exchanges + +Python +~~~~~~ + +`ccxt in PyPI `__ + +.. code:: shell + + pip install ccxt + +.. code:: python + + import ccxt + print(ccxt.exchanges) # print a list of all available exchange classes + +The library supports concurrent asynchronous mode with asyncio and async/await in Python 3.5.3+ + +.. code:: python + + import ccxt.async_support as ccxt # link against the asynchronous version of ccxt + +PHP +~~~ + +`ccxt in PHP with Packagist/Composer `__ (PHP 5.4+) + +It requires common PHP modules: + +- cURL +- mbstring (using UTF-8 is highly recommended) +- PCRE +- iconv +- gmp (this is a built-in extension as of PHP 7.2+) + +.. code:: php + + include "ccxt.php"; + var_dump (\ccxt\Exchange::$exchanges); // print a list of all available exchange classes + +Docker +~~~~~~ + +You can get CCXT installed in a container along with all the supported languages and dependencies. This may be useful if you want to contribute to CCXT (e.g. run the build scripts and tests — please see the `Contributing `__ document for the details on that). + +Using ``docker-compose`` (in the cloned CCXT repository): + +.. code:: shell + + docker-compose run --rm ccxt + +You don’t need the Docker image if you’re not going to develop CCXT. If you just want to use CCXT – just install it as a regular package into your project. + +-------------- + +Documentation +------------- + +Read the `Manual `__ for more details. + +Usage +----- + +Intro +~~~~~ + +The CCXT library consists of a public part and a private part. Anyone can use the public part immediately after installation. Public APIs provide unrestricted access to public information for all exchange markets without the need to register a user account or have an API key. + +Public APIs include the following: + +- market data +- instruments/trading pairs +- price feeds (exchange rates) +- order books +- trade history +- tickers +- OHLC(V) for charting +- other public endpoints + +In order to trade with private APIs you need to obtain API keys from an exchange’s website. It usually means signing up to the exchange and creating API keys for your account. Some exchanges require personal info or identification. Sometimes verification may be necessary as well. In this case you will need to register yourself, this library will not create accounts or API keys for you. Some exchanges expose API endpoints for registering an account, but most exchanges don’t. You will have to sign up and create API keys on their websites. + +Private APIs allow the following: + +- manage personal account info +- query account balances +- trade by making market and limit orders +- deposit and withdraw fiat and crypto funds +- query personal orders +- get ledger history +- transfer funds between accounts +- use merchant services + +This library implements full public and private REST APIs for all exchanges. WebSocket and FIX implementations in JavaScript, PHP, Python are available in `CCXT Pro `__, which is a professional addon to CCXT with support for WebSocket streams. + +The CCXT library supports both camelcase notation (preferred in JavaScript) and underscore notation (preferred in Python and PHP), therefore all methods can be called in either notation or coding style in any language. + +.. code:: javascript + + // both of these notations work in JavaScript/Python/PHP + exchange.methodName () // camelcase pseudocode + exchange.method_name () // underscore pseudocode + +Read the `Manual `__ for more details. + +JavaScript +~~~~~~~~~~ + +.. code:: javascript + + 'use strict'; + const ccxt = require ('ccxt'); + + (async function () { + let kraken = new ccxt.kraken () + let bitfinex = new ccxt.bitfinex ({ verbose: true }) + let huobipro = new ccxt.huobipro () + let okcoinusd = new ccxt.okcoinusd ({ + apiKey: 'YOUR_PUBLIC_API_KEY', + secret: 'YOUR_SECRET_PRIVATE_KEY', + }) + + const exchangeId = 'binance' + , exchangeClass = ccxt[exchangeId] + , exchange = new exchangeClass ({ + 'apiKey': 'YOUR_API_KEY', + 'secret': 'YOUR_SECRET', + 'timeout': 30000, + 'enableRateLimit': true, + }) + + console.log (kraken.id, await kraken.loadMarkets ()) + console.log (bitfinex.id, await bitfinex.loadMarkets ()) + console.log (huobipro.id, await huobipro.loadMarkets ()) + + console.log (kraken.id, await kraken.fetchOrderBook (kraken.symbols[0])) + console.log (bitfinex.id, await bitfinex.fetchTicker ('BTC/USD')) + console.log (huobipro.id, await huobipro.fetchTrades ('ETH/CNY')) + + console.log (okcoinusd.id, await okcoinusd.fetchBalance ()) + + // sell 1 BTC/USD for market price, sell a bitcoin for dollars immediately + console.log (okcoinusd.id, await okcoinusd.createMarketSellOrder ('BTC/USD', 1)) + + // buy 1 BTC/USD for $2500, you pay $2500 and receive ฿1 when the order is closed + console.log (okcoinusd.id, await okcoinusd.createLimitBuyOrder ('BTC/USD', 1, 2500.00)) + + // pass/redefine custom exchange-specific order params: type, amount, price or whatever + // use a custom order type + bitfinex.createLimitSellOrder ('BTC/USD', 1, 10, { 'type': 'trailing-stop' }) + + }) (); + +.. _python-1: + +Python +~~~~~~ + +.. code:: python + + # coding=utf-8 + + import ccxt + + hitbtc = ccxt.hitbtc({'verbose': True}) + bitmex = ccxt.bitmex() + huobipro = ccxt.huobipro() + exmo = ccxt.exmo({ + 'apiKey': 'YOUR_PUBLIC_API_KEY', + 'secret': 'YOUR_SECRET_PRIVATE_KEY', + }) + kraken = ccxt.kraken({ + 'apiKey': 'YOUR_PUBLIC_API_KEY', + 'secret': 'YOUR_SECRET_PRIVATE_KEY', + }) + + exchange_id = 'binance' + exchange_class = getattr(ccxt, exchange_id) + exchange = exchange_class({ + 'apiKey': 'YOUR_API_KEY', + 'secret': 'YOUR_SECRET', + 'timeout': 30000, + 'enableRateLimit': True, + }) + + hitbtc_markets = hitbtc.load_markets() + + print(hitbtc.id, hitbtc_markets) + print(bitmex.id, bitmex.load_markets()) + print(huobipro.id, huobipro.load_markets()) + + print(hitbtc.fetch_order_book(hitbtc.symbols[0])) + print(bitmex.fetch_ticker('BTC/USD')) + print(huobipro.fetch_trades('LTC/CNY')) + + print(exmo.fetch_balance()) + + # sell one ฿ for market price and receive $ right now + print(exmo.id, exmo.create_market_sell_order('BTC/USD', 1)) + + # limit buy BTC/EUR, you pay €2500 and receive ฿1 when the order is closed + print(exmo.id, exmo.create_limit_buy_order('BTC/EUR', 1, 2500.00)) + + # pass/redefine custom exchange-specific order params: type, amount, price, flags, etc... + kraken.create_market_buy_order('BTC/USD', 1, {'trading_agreement': 'agree'}) + +.. _php-1: + +PHP +~~~ + +.. code:: php + + include 'ccxt.php'; + + $poloniex = new \ccxt\poloniex (); + $bittrex = new \ccxt\bittrex (array ('verbose' => true)); + $quoinex = new \ccxt\quoinex (); + $zaif = new \ccxt\zaif (array ( + 'apiKey' => 'YOUR_PUBLIC_API_KEY', + 'secret' => 'YOUR_SECRET_PRIVATE_KEY', + )); + $hitbtc = new \ccxt\hitbtc (array ( + 'apiKey' => 'YOUR_PUBLIC_API_KEY', + 'secret' => 'YOUR_SECRET_PRIVATE_KEY', + )); + + $exchange_id = 'binance'; + $exchange_class = "\\ccxt\\$exchange_id"; + $exchange = new $exchange_class (array ( + 'apiKey' => 'YOUR_API_KEY', + 'secret' => 'YOUR_SECRET', + 'timeout' => 30000, + 'enableRateLimit' => true, + )); + + $poloniex_markets = $poloniex->load_markets (); + + var_dump ($poloniex_markets); + var_dump ($bittrex->load_markets ()); + var_dump ($quoinex->load_markets ()); + + var_dump ($poloniex->fetch_order_book ($poloniex->symbols[0])); + var_dump ($bittrex->fetch_trades ('BTC/USD')); + var_dump ($quoinex->fetch_ticker ('ETH/EUR')); + var_dump ($zaif->fetch_ticker ('BTC/JPY')); + + var_dump ($zaif->fetch_balance ()); + + // sell 1 BTC/JPY for market price, you pay ¥ and receive ฿ immediately + var_dump ($zaif->id, $zaif->create_market_sell_order ('BTC/JPY', 1)); + + // buy BTC/JPY, you receive ฿1 for ¥285000 when the order closes + var_dump ($zaif->id, $zaif->create_limit_buy_order ('BTC/JPY', 1, 285000)); + + // set a custom user-defined id to your order + $hitbtc->create_order ('BTC/USD', 'limit', 'buy', 1, 3000, array ('clientOrderId' => '123')); + +Contributing +------------ + +Please read the `CONTRIBUTING `__ document before making changes that you would like adopted in the code. Also, read the `Manual `__ for more details. + +Support Developer Team +---------------------- + +We are investing a significant amount of time into the development of this library. If CCXT made your life easier and you want to help us improve it further, or if you want to speed up development of new features and exchanges, please support us with a tip. We appreciate all contributions! + +Sponsors +~~~~~~~~ + +Support this project by becoming a sponsor. Your logo will show up here with a link to your website. + +[`Become a sponsor `__] + +Supporters +~~~~~~~~~~ + +Support this project by becoming a supporter. Your avatar will show up here with a link to your website. + +[`Become a supporter `__] + +Backers +~~~~~~~ + +Thank you to all our backers! [`Become a backer `__] + +Crypto +~~~~~~ + +:: + + ETH 0x26a3CB49578F07000575405a57888681249c35Fd (ETH only) + BTC 33RmVRfhK2WZVQR1R83h2e9yXoqRNDvJva + BCH 1GN9p233TvNcNQFthCgfiHUnj5JRKEc2Ze + LTC LbT8mkAqQBphc4yxLXEDgYDfEax74et3bP + +Thank you! + +Social +------ + +- `Follow us on Twitter `__ +- `Read our blog on Medium `__ +- \ `Discord `__\ + +Team +---- + +- `Igor Kroitor `__ +- `Carlo Revelli `__ + +Contact Us +---------- + +For business inquiries: info@ccxt.trade diff --git a/ccxt.egg-info/PKG-INFO b/ccxt.egg-info/PKG-INFO new file mode 100644 index 0000000..fbafb7d --- /dev/null +++ b/ccxt.egg-info/PKG-INFO @@ -0,0 +1,801 @@ +Metadata-Version: 2.4 +Name: ccxt +Version: 4.5.18 +Summary: A cryptocurrency trading API with more than 100 exchanges in JavaScript / TypeScript / Python / C# / PHP / Go +Home-page: https://ccxt.com +Author: Igor Kroitor +Author-email: igor.kroitor@gmail.com +License: MIT +Project-URL: Homepage, https://ccxt.com +Project-URL: Documentation, https://github.com/ccxt/ccxt/wiki +Project-URL: Discord, https://discord.gg/ccxt +Project-URL: Twitter, https://twitter.com/ccxt_official +Project-URL: Funding, https://opencollective.com/ccxt +Keywords: algorithmic,algotrading,altcoin,altcoins,api,arbitrage,real-time,realtime,backtest,backtesting,bitcoin,bot,btc,cny,coin,coins,crypto,cryptocurrency,crypto currency,crypto market,currency,currencies,darkcoin,dash,digital currency,doge,dogecoin,e-commerce,etc,eth,ether,ethereum,exchange,exchanges,eur,framework,invest,investing,investor,library,light,litecoin,ltc,market,market data,markets,merchandise,merchant,minimal,ohlcv,order,orderbook,order book,price,price data,pricefeed,private,public,ripple,strategy,ticker,tickers,toolkit,trade,trader,trading,usd,volume,websocket,websockets,web socket,web sockets,ws,xbt,xrp,zec,zerocoin +Classifier: Development Status :: 4 - Beta +Classifier: Intended Audience :: Developers +Classifier: Intended Audience :: Financial and Insurance Industry +Classifier: Intended Audience :: Information Technology +Classifier: Topic :: Software Development :: Build Tools +Classifier: Topic :: Office/Business :: Financial :: Investment +Classifier: License :: OSI Approved :: MIT License +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: JavaScript +Classifier: Programming Language :: PHP +Classifier: Operating System :: OS Independent +Classifier: Environment :: Console +Description-Content-Type: text/markdown +License-File: LICENSE.txt +Requires-Dist: setuptools>=60.9.0 +Requires-Dist: certifi>=2018.1.18 +Requires-Dist: requests>=2.18.4 +Requires-Dist: cryptography>=2.6.1 +Requires-Dist: typing_extensions>=4.4.0 +Requires-Dist: aiohttp>=3.10.11; python_version >= "3.5.2" +Requires-Dist: aiodns>=1.1.1; python_version >= "3.5.2" +Requires-Dist: yarl>=1.7.2; python_version >= "3.5.2" +Requires-Dist: coincurve==21.0.0; python_version >= "3.9" and python_version <= "3.13" +Provides-Extra: qa +Requires-Dist: ruff==0.0.292; extra == "qa" +Requires-Dist: tox>=4.8.0; extra == "qa" +Provides-Extra: type +Requires-Dist: mypy==1.6.1; extra == "type" +Dynamic: author +Dynamic: author-email +Dynamic: classifier +Dynamic: description +Dynamic: description-content-type +Dynamic: home-page +Dynamic: keywords +Dynamic: license +Dynamic: license-file +Dynamic: project-url +Dynamic: provides-extra +Dynamic: requires-dist +Dynamic: summary + +# CCXT – CryptoCurrency eXchange Trading Library + +[![NPM Downloads](https://img.shields.io/npm/dy/ccxt.svg)](https://www.npmjs.com/package/ccxt) [![npm](https://img.shields.io/npm/v/ccxt.svg)](https://npmjs.com/package/ccxt) [![PyPI](https://img.shields.io/pypi/v/ccxt.svg)](https://pypi.python.org/pypi/ccxt) [![NuGet version](https://img.shields.io/nuget/v/ccxt)](https://www.nuget.org/packages/ccxt) [![GoDoc](https://pkg.go.dev/badge/github.com/ccxt/ccxt/go/v4?utm_source=godoc)](https://godoc.org/github.com/ccxt/ccxt/go/v4) [![Discord](https://img.shields.io/discord/690203284119617602?logo=discord&logoColor=white)](https://discord.gg/ccxt) [![Supported Exchanges](https://img.shields.io/badge/exchanges-107-blue.svg)](https://github.com/ccxt/ccxt/wiki/Exchange-Markets) [![Follow CCXT at x.com](https://img.shields.io/twitter/follow/ccxt_official.svg?style=social&label=CCXT)](https://x.com/ccxt_official) + +A cryptocurrency trading API with more than 100 exchanges in JavaScript / TypeScript / Python / C# / PHP / Go. + +### [Install](#install) · [Usage](#usage) · [Manual](https://github.com/ccxt/ccxt/wiki) · [FAQ](https://github.com/ccxt/ccxt/wiki/FAQ) · [Examples](https://github.com/ccxt/ccxt/tree/master/examples) · [Contributing](https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md) · [Disclaimer](#disclaimer) · [Social](#social) + +The **CCXT** library is used to connect and trade with cryptocurrency exchanges and payment processing services worldwide. It provides quick access to market data for storage, analysis, visualization, indicator development, algorithmic trading, strategy backtesting, bot programming, and related software engineering. + +It is intended to be used by **coders, developers, technically-skilled traders, data-scientists and financial analysts** for building trading algorithms. + +Current feature list: + +- support for many cryptocurrency exchanges — more coming soon +- fully implemented public and private APIs +- optional normalized data for cross-exchange analytics and arbitrage +- an out of the box unified API that is extremely easy to integrate +- works in Node 10.4+, Python 3, PHP 8.1+, netstandard2.0/2.1, Go 1.20+ and web browsers + +## See Also + +- [![TabTrader](https://user-images.githubusercontent.com/1294454/66755907-9c3e8880-eea1-11e9-846e-0bff349ceb87.png)](https://tab-trader.com/?utm_source=ccxt) **[TabTrader](https://tab-trader.com/?utm_source=ccxt)** – trading on all exchanges in one app. Available on **[Android](https://play.google.com/store/apps/details?id=com.tabtrader.android&referrer=utm_source%3Dccxt)** and **[iOS](https://itunes.apple.com/app/apple-store/id1095716562?mt=8)**! +- [![Freqtrade](https://user-images.githubusercontent.com/1294454/114340585-8e35fa80-9b60-11eb-860f-4379125e2db6.png)](https://www.freqtrade.io) **[Freqtrade](https://www.freqtrade.io)** – leading opensource cryptocurrency algorithmic trading software! +- [![OctoBot](https://user-images.githubusercontent.com/1294454/132113722-007fc092-7530-4b41-b929-b8ed380b7b2e.png)](https://www.octobot.online) **[OctoBot](https://www.octobot.online)** – cryptocurrency trading bot with an advanced web interface. +- [![TokenBot](https://user-images.githubusercontent.com/1294454/152720975-0522b803-70f0-4f18-a305-3c99b37cd990.png)](https://tokenbot.com/?utm_source=github&utm_medium=ccxt&utm_campaign=algodevs) **[TokenBot](https://tokenbot.com/?utm_source=github&utm_medium=ccxt&utm_campaign=algodevs)** – discover and copy the best algorithmic traders in the world. + +## Certified Cryptocurrency Exchanges + + +|logo |id |name |ver |type |certified |pro |discount | +|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------|-----------------------------------------------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------:|--------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------|:----------------------------------------------------------------------------:|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [![binance](https://github.com/user-attachments/assets/e9419b93-ccb0-46aa-9bff-c883f096274b)](https://accounts.binance.com/register?ref=CCXTCOM) | binance | [Binance](https://accounts.binance.com/register?ref=CCXTCOM) | [![API Version *](https://img.shields.io/badge/*-lightgray)](https://developers.binance.com/en) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | [![Sign up with Binance using CCXT's referral link for a 10% discount!](https://img.shields.io/static/v1?label=Fee&message=%2d10%25&color=orange)](https://accounts.binance.com/register?ref=CCXTCOM) | +| [![binanceusdm](https://github.com/user-attachments/assets/871cbea7-eebb-4b28-b260-c1c91df0487a)](https://accounts.binance.com/register?ref=CCXTCOM) | binanceusdm | [Binance USDⓈ-M](https://accounts.binance.com/register?ref=CCXTCOM) | [![API Version *](https://img.shields.io/badge/*-lightgray)](https://binance-docs.github.io/apidocs/futures/en/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | [![Sign up with Binance USDⓈ-M using CCXT's referral link for a 10% discount!](https://img.shields.io/static/v1?label=Fee&message=%2d10%25&color=orange)](https://accounts.binance.com/register?ref=CCXTCOM) | +| [![binancecoinm](https://github.com/user-attachments/assets/387cfc4e-5f33-48cd-8f5c-cd4854dabf0c)](https://accounts.binance.com/register?ref=CCXTCOM) | binancecoinm | [Binance COIN-M](https://accounts.binance.com/register?ref=CCXTCOM) | [![API Version *](https://img.shields.io/badge/*-lightgray)](https://binance-docs.github.io/apidocs/delivery/en/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | [![Sign up with Binance COIN-M using CCXT's referral link for a 10% discount!](https://img.shields.io/static/v1?label=Fee&message=%2d10%25&color=orange)](https://accounts.binance.com/register?ref=CCXTCOM) | +| [![bybit](https://github.com/user-attachments/assets/97a5d0b3-de10-423d-90e1-6620960025ed)](https://www.bybit.com/invite?ref=XDK12WP) | bybit | [Bybit](https://www.bybit.com/invite?ref=XDK12WP) | [![API Version 5](https://img.shields.io/badge/5-lightgray)](https://bybit-exchange.github.io/docs/inverse/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | | +| [![okx](https://user-images.githubusercontent.com/1294454/152485636-38b19e4a-bece-4dec-979a-5982859ffc04.jpg)](https://www.okx.com/join/CCXTCOM) | okx | [OKX](https://www.okx.com/join/CCXTCOM) | [![API Version 5](https://img.shields.io/badge/5-lightgray)](https://www.okx.com/docs-v5/en/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | [![Sign up with OKX using CCXT's referral link for a 20% discount!](https://img.shields.io/static/v1?label=Fee&message=%2d20%25&color=orange)](https://www.okx.com/join/CCXTCOM) | +| [![gate](https://github.com/user-attachments/assets/64f988c5-07b6-4652-b5c1-679a6bf67c85)](https://www.gate.com/share/CCXTGATE) | gate | [Gate](https://www.gate.com/share/CCXTGATE) | [![API Version 4](https://img.shields.io/badge/4-lightgray)](https://www.gate.com/docs/developers/apiv4/en/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | [![Sign up with Gate using CCXT's referral link for a 20% discount!](https://img.shields.io/static/v1?label=Fee&message=%2d20%25&color=orange)](https://www.gate.com/share/CCXTGATE) | +| [![kucoin](https://user-images.githubusercontent.com/51840849/87295558-132aaf80-c50e-11ea-9801-a2fb0c57c799.jpg)](https://www.kucoin.com/ucenter/signup?rcode=E5wkqe) | kucoin | [KuCoin](https://www.kucoin.com/ucenter/signup?rcode=E5wkqe) | [![API Version 2](https://img.shields.io/badge/2-lightgray)](https://docs.kucoin.com) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | | +| [![kucoinfutures](https://user-images.githubusercontent.com/1294454/147508995-9e35030a-d046-43a1-a006-6fabd981b554.jpg)](https://futures.kucoin.com/?rcode=E5wkqe) | kucoinfutures | [KuCoin Futures](https://futures.kucoin.com/?rcode=E5wkqe) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://docs.kucoin.com/futures) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | | +| [![bitget](https://github.com/user-attachments/assets/fbaa10cc-a277-441d-a5b7-997dd9a87658)](https://www.bitget.com/expressly?languageType=0&channelCode=ccxt&vipCode=tg9j) | bitget | [Bitget](https://www.bitget.com/expressly?languageType=0&channelCode=ccxt&vipCode=tg9j) | [![API Version 2](https://img.shields.io/badge/2-lightgray)](https://www.bitget.com/api-doc/common/intro) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | | +| [![hyperliquid](https://github.com/ccxt/ccxt/assets/43336371/b371bc6c-4a8c-489f-87f4-20a913dd8d4b)](https://app.hyperliquid.xyz/) | hyperliquid | [Hyperliquid](https://app.hyperliquid.xyz/) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api) | ![DEX - Distributed EXchange](https://img.shields.io/badge/DEX-blue.svg "DEX - Distributed EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | | +| [![bitmex](https://github.com/user-attachments/assets/c78425ab-78d5-49d6-bd14-db7734798f04)](https://www.bitmex.com/app/register/NZTR1q) | bitmex | [BitMEX](https://www.bitmex.com/app/register/NZTR1q) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://www.bitmex.com/app/apiOverview) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | [![Sign up with BitMEX using CCXT's referral link for a 10% discount!](https://img.shields.io/static/v1?label=Fee&message=%2d10%25&color=orange)](https://www.bitmex.com/app/register/NZTR1q) | +| [![bingx](https://github-production-user-asset-6210df.s3.amazonaws.com/1294454/253675376-6983b72e-4999-4549-b177-33b374c195e3.jpg)](https://bingx.com/invite/OHETOM) | bingx | [BingX](https://bingx.com/invite/OHETOM) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://bingx-api.github.io/docs/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | | +| [![htx](https://user-images.githubusercontent.com/1294454/76137448-22748a80-604e-11ea-8069-6e389271911d.jpg)](https://www.htx.com.vc/invite/en-us/1h?invite_code=6rmm2223) | htx | [HTX](https://www.htx.com.vc/invite/en-us/1h?invite_code=6rmm2223) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://huobiapi.github.io/docs/spot/v1/en/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | [![Sign up with HTX using CCXT's referral link for a 15% discount!](https://img.shields.io/static/v1?label=Fee&message=%2d15%25&color=orange)](https://www.htx.com.vc/invite/en-us/1h?invite_code=6rmm2223) | +| [![mexc](https://user-images.githubusercontent.com/1294454/137283979-8b2a818d-8633-461b-bfca-de89e8c446b2.jpg)](https://www.mexc.com/register?inviteCode=mexc-1FQ1GNu1) | mexc | [MEXC Global](https://www.mexc.com/register?inviteCode=mexc-1FQ1GNu1) | [![API Version 3](https://img.shields.io/badge/3-lightgray)](https://mexcdevelop.github.io/apidocs/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | | +| [![bitmart](https://github.com/user-attachments/assets/0623e9c4-f50e-48c9-82bd-65c3908c3a14)](http://www.bitmart.com/?r=rQCFLh) | bitmart | [BitMart](http://www.bitmart.com/?r=rQCFLh) | [![API Version 2](https://img.shields.io/badge/2-lightgray)](https://developer-pro.bitmart.com/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | [![Sign up with BitMart using CCXT's referral link for a 30% discount!](https://img.shields.io/static/v1?label=Fee&message=%2d30%25&color=orange)](http://www.bitmart.com/?r=rQCFLh) | +| [![cryptocom](https://user-images.githubusercontent.com/1294454/147792121-38ed5e36-c229-48d6-b49a-48d05fc19ed4.jpeg)](https://crypto.com/exch/kdacthrnxt) | cryptocom | [Crypto.com](https://crypto.com/exch/kdacthrnxt) | [![API Version 2](https://img.shields.io/badge/2-lightgray)](https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | [![Sign up with Crypto.com using CCXT's referral link for a 75% discount!](https://img.shields.io/static/v1?label=Fee&message=%2d75%25&color=orange)](https://crypto.com/exch/kdacthrnxt) | +| [![coinex](https://user-images.githubusercontent.com/51840849/87182089-1e05fa00-c2ec-11ea-8da9-cc73b45abbbc.jpg)](https://www.coinex.com/register?refer_code=yw5fz) | coinex | [CoinEx](https://www.coinex.com/register?refer_code=yw5fz) | [![API Version 2](https://img.shields.io/badge/2-lightgray)](https://docs.coinex.com/api/v2) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | | +| [![hashkey](https://github.com/user-attachments/assets/6dd6127b-cc19-4a13-9b29-a98d81f80e98)](https://global.hashkey.com/en-US/register/invite?invite_code=82FQUN) | hashkey | [HashKey Global](https://global.hashkey.com/en-US/register/invite?invite_code=82FQUN) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://hashkeyglobal-apidoc.readme.io/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | | +| [![woo](https://user-images.githubusercontent.com/1294454/150730761-1a00e5e0-d28c-480f-9e65-089ce3e6ef3b.jpg)](https://woox.io/register?ref=DIJT0CNL) | woo | [WOO X](https://woox.io/register?ref=DIJT0CNL) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://docs.woox.io/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | [![Sign up with WOO X using CCXT's referral link for a 35% discount!](https://img.shields.io/static/v1?label=Fee&message=%2d35%25&color=orange)](https://woox.io/register?ref=DIJT0CNL) | +| [![woofipro](https://github.com/user-attachments/assets/9ba21b8a-a9c7-4770-b7f1-ce3bcbde68c1)](https://dex.woo.org/en/trade?ref=CCXT) | woofipro | [WOOFI PRO](https://dex.woo.org/en/trade?ref=CCXT) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://orderly.network/docs/build-on-evm/building-on-evm) | ![DEX - Distributed EXchange](https://img.shields.io/badge/DEX-blue.svg "DEX - Distributed EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | [![Sign up with WOOFI PRO using CCXT's referral link for a 5% discount!](https://img.shields.io/static/v1?label=Fee&message=%2d5%25&color=orange)](https://dex.woo.org/en/trade?ref=CCXT) | + +## Supported Cryptocurrency Exchanges +The CCXT library currently supports the following 104 cryptocurrency exchange markets and trading APIs: + +|logo |id |name |ver |type |certified |pro | +|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------|-----------------------------------------------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------:|--------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------| +| [![alpaca](https://github.com/user-attachments/assets/e9476df8-a450-4c3e-ab9a-1a7794219e1b)](https://alpaca.markets) | alpaca | [Alpaca](https://alpaca.markets) | [![API Version *](https://img.shields.io/badge/*-lightgray)](https://alpaca.markets/docs/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![apex](https://github.com/user-attachments/assets/fef8f2f7-4265-46aa-965e-33a91881cb00)](https://omni.apex.exchange/trade) | apex | [Apex](https://omni.apex.exchange/trade) | [![API Version 3](https://img.shields.io/badge/3-lightgray)](https://api-docs.pro.apex.exchange) | ![DEX - Distributed EXchange](https://img.shields.io/badge/DEX-blue.svg "DEX - Distributed EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![arkham](https://github.com/user-attachments/assets/5cefdcfb-2c10-445b-835c-fa21317bf5ac)](https://arkm.com/register?ref=ccxt) | arkham | [ARKHAM](https://arkm.com/register?ref=ccxt) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://arkm.com/limits-api) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![ascendex](https://github.com/user-attachments/assets/55bab6b9-d4ca-42a8-a0e6-fac81ae557f1)](https://ascendex.com/en-us/register?inviteCode=EL6BXBQM) | ascendex | [AscendEX](https://ascendex.com/en-us/register?inviteCode=EL6BXBQM) | [![API Version 2](https://img.shields.io/badge/2-lightgray)](https://ascendex.github.io/ascendex-pro-api/#ascendex-pro-api-documentation) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![backpack](https://github.com/user-attachments/assets/cc04c278-679f-4554-9f72-930dd632b80f)](https://backpack.exchange/join/ccxt) | backpack | [Backpack](https://backpack.exchange/join/ccxt) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://docs.backpack.exchange/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![bequant](https://github.com/user-attachments/assets/0583ef1f-29fe-4b7c-8189-63565a0e2867)](https://bequant.io/referral/dd104e3bee7634ec) | bequant | [Bequant](https://bequant.io/referral/dd104e3bee7634ec) | [![API Version 3](https://img.shields.io/badge/3-lightgray)](https://api.bequant.io/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![bigone](https://github.com/user-attachments/assets/4e5cfd53-98cc-4b90-92cd-0d7b512653d1)](https://b1.run/users/new?code=D3LLBVFT) | bigone | [BigONE](https://b1.run/users/new?code=D3LLBVFT) | [![API Version 3](https://img.shields.io/badge/3-lightgray)](https://open.big.one/docs/api.html) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![binance](https://github.com/user-attachments/assets/e9419b93-ccb0-46aa-9bff-c883f096274b)](https://accounts.binance.com/register?ref=CCXTCOM) | binance | [Binance](https://accounts.binance.com/register?ref=CCXTCOM) | [![API Version *](https://img.shields.io/badge/*-lightgray)](https://developers.binance.com/en) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![binancecoinm](https://github.com/user-attachments/assets/387cfc4e-5f33-48cd-8f5c-cd4854dabf0c)](https://accounts.binance.com/register?ref=CCXTCOM) | binancecoinm | [Binance COIN-M](https://accounts.binance.com/register?ref=CCXTCOM) | [![API Version *](https://img.shields.io/badge/*-lightgray)](https://binance-docs.github.io/apidocs/delivery/en/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![binanceus](https://github.com/user-attachments/assets/a9667919-b632-4d52-a832-df89f8a35e8c)](https://www.binance.us/?ref=35005074) | binanceus | [Binance US](https://www.binance.us/?ref=35005074) | [![API Version *](https://img.shields.io/badge/*-lightgray)](https://github.com/binance-us/binance-official-api-docs) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![binanceusdm](https://github.com/user-attachments/assets/871cbea7-eebb-4b28-b260-c1c91df0487a)](https://accounts.binance.com/register?ref=CCXTCOM) | binanceusdm | [Binance USDⓈ-M](https://accounts.binance.com/register?ref=CCXTCOM) | [![API Version *](https://img.shields.io/badge/*-lightgray)](https://binance-docs.github.io/apidocs/futures/en/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![bingx](https://github-production-user-asset-6210df.s3.amazonaws.com/1294454/253675376-6983b72e-4999-4549-b177-33b374c195e3.jpg)](https://bingx.com/invite/OHETOM) | bingx | [BingX](https://bingx.com/invite/OHETOM) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://bingx-api.github.io/docs/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![bit2c](https://github.com/user-attachments/assets/db0bce50-6842-4c09-a1d5-0c87d22118aa)](https://bit2c.co.il/Aff/63bfed10-e359-420c-ab5a-ad368dab0baf) | bit2c | [Bit2C](https://bit2c.co.il/Aff/63bfed10-e359-420c-ab5a-ad368dab0baf) | [![API Version *](https://img.shields.io/badge/*-lightgray)](https://www.bit2c.co.il/home/api) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![bitbank](https://github.com/user-attachments/assets/9d616de0-8a88-4468-8e38-d269acab0348)](https://bitbank.cc/) | bitbank | [bitbank](https://bitbank.cc/) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://docs.bitbank.cc/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![bitbns](https://github.com/user-attachments/assets/a5b9a562-cdd8-4bea-9fa7-fd24c1dad3d9)](https://ref.bitbns.com/1090961) | bitbns | [Bitbns](https://ref.bitbns.com/1090961) | [![API Version 2](https://img.shields.io/badge/2-lightgray)](https://bitbns.com/trade/#/api-trading/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![bitfinex](https://github.com/user-attachments/assets/4a8e947f-ab46-481a-a8ae-8b20e9b03178)](https://www.bitfinex.com) | bitfinex | [Bitfinex](https://www.bitfinex.com) | [![API Version 2](https://img.shields.io/badge/2-lightgray)](https://docs.bitfinex.com/v2/docs/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![bitflyer](https://github.com/user-attachments/assets/d0217747-e54d-4533-8416-0d553dca74bb)](https://bitflyer.com) | bitflyer | [bitFlyer](https://bitflyer.com) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://lightning.bitflyer.com/docs?lang=en) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![bitget](https://github.com/user-attachments/assets/fbaa10cc-a277-441d-a5b7-997dd9a87658)](https://www.bitget.com/expressly?languageType=0&channelCode=ccxt&vipCode=tg9j) | bitget | [Bitget](https://www.bitget.com/expressly?languageType=0&channelCode=ccxt&vipCode=tg9j) | [![API Version 2](https://img.shields.io/badge/2-lightgray)](https://www.bitget.com/api-doc/common/intro) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![bithumb](https://github.com/user-attachments/assets/c9e0eefb-4777-46b9-8f09-9d7f7c4af82d)](https://www.bithumb.com) | bithumb | [Bithumb](https://www.bithumb.com) | [![API Version *](https://img.shields.io/badge/*-lightgray)](https://apidocs.bithumb.com) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![bitmart](https://github.com/user-attachments/assets/0623e9c4-f50e-48c9-82bd-65c3908c3a14)](http://www.bitmart.com/?r=rQCFLh) | bitmart | [BitMart](http://www.bitmart.com/?r=rQCFLh) | [![API Version 2](https://img.shields.io/badge/2-lightgray)](https://developer-pro.bitmart.com/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![bitmex](https://github.com/user-attachments/assets/c78425ab-78d5-49d6-bd14-db7734798f04)](https://www.bitmex.com/app/register/NZTR1q) | bitmex | [BitMEX](https://www.bitmex.com/app/register/NZTR1q) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://www.bitmex.com/app/apiOverview) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![bitopro](https://github.com/user-attachments/assets/affc6337-b95a-44bf-aacd-04f9722364f6)](https://www.bitopro.com) | bitopro | [BitoPro](https://www.bitopro.com) | [![API Version 3](https://img.shields.io/badge/3-lightgray)](https://github.com/bitoex/bitopro-offical-api-docs/blob/master/v3-1/rest-1/rest.md) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![bitrue](https://github.com/user-attachments/assets/67abe346-1273-461a-bd7c-42fa32907c8e)](https://www.bitrue.com/affiliate/landing?cn=600000&inviteCode=EZWETQE) | bitrue | [Bitrue](https://www.bitrue.com/affiliate/landing?cn=600000&inviteCode=EZWETQE) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://github.com/Bitrue-exchange/bitrue-official-api-docs) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![bitso](https://github.com/user-attachments/assets/178c8e56-9054-4107-b192-5e5053d4f975)](https://bitso.com/?ref=itej) | bitso | [Bitso](https://bitso.com/?ref=itej) | [![API Version 3](https://img.shields.io/badge/3-lightgray)](https://bitso.com/api_info) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![bitstamp](https://github.com/user-attachments/assets/d5480572-1fee-43cb-b900-d38c522d0024)](https://www.bitstamp.net) | bitstamp | [Bitstamp](https://www.bitstamp.net) | [![API Version 2](https://img.shields.io/badge/2-lightgray)](https://www.bitstamp.net/api) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![bitteam](https://github.com/user-attachments/assets/b41b5e0d-98e5-4bd3-8a6e-aeb230a4a135)](https://bit.team/auth/sign-up?ref=bitboy2023) | bitteam | [BIT.TEAM](https://bit.team/auth/sign-up?ref=bitboy2023) | [![API Version 2.0.6](https://img.shields.io/badge/2.0.6-lightgray)](https://bit.team/trade/api/documentation) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![bittrade](https://user-images.githubusercontent.com/1294454/85734211-85755480-b705-11ea-8b35-0b7f1db33a2f.jpg)](https://www.bittrade.co.jp/register/?invite_code=znnq3) | bittrade | [BitTrade](https://www.bittrade.co.jp/register/?invite_code=znnq3) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://api-doc.bittrade.co.jp) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![bitvavo](https://github.com/user-attachments/assets/d213155c-8c71-4701-9bd5-45351febc2a8)](https://bitvavo.com/?a=24F34952F7) | bitvavo | [Bitvavo](https://bitvavo.com/?a=24F34952F7) | [![API Version 2](https://img.shields.io/badge/2-lightgray)](https://docs.bitvavo.com/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![blockchaincom](https://github.com/user-attachments/assets/975e3054-3399-4363-bcee-ec3c6d63d4e8)](https://blockchain.com) | blockchaincom | [Blockchain.com](https://blockchain.com) | [![API Version 3](https://img.shields.io/badge/3-lightgray)](https://api.blockchain.com/v3) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![blofin](https://github.com/user-attachments/assets/518cdf80-f05d-4821-a3e3-d48ceb41d73b)](https://blofin.com/register?referral_code=f79EsS) | blofin | [BloFin](https://blofin.com/register?referral_code=f79EsS) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://blofin.com/docs) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![btcalpha](https://github.com/user-attachments/assets/dce49f3a-61e5-4ba0-a2fe-41d192fd0e5d)](https://btc-alpha.com/?r=123788) | btcalpha | [BTC-Alpha](https://btc-alpha.com/?r=123788) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://btc-alpha.github.io/api-docs) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![btcbox](https://github.com/user-attachments/assets/1e2cb499-8d0f-4f8f-9464-3c015cfbc76b)](https://www.btcbox.co.jp/) | btcbox | [BtcBox](https://www.btcbox.co.jp/) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://blog.btcbox.jp/en/archives/8762) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![btcmarkets](https://github.com/user-attachments/assets/8c8d6907-3873-4cc4-ad20-e22fba28247e)](https://btcmarkets.net) | btcmarkets | [BTC Markets](https://btcmarkets.net) | [![API Version 3](https://img.shields.io/badge/3-lightgray)](https://api.btcmarkets.net/doc/v3) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![btcturk](https://github.com/user-attachments/assets/10e0a238-9f60-4b06-9dda-edfc7602f1d6)](https://www.btcturk.com) | btcturk | [BTCTurk](https://www.btcturk.com) | [![API Version *](https://img.shields.io/badge/*-lightgray)](https://github.com/BTCTrader/broker-api-docs) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![bybit](https://github.com/user-attachments/assets/97a5d0b3-de10-423d-90e1-6620960025ed)](https://www.bybit.com/invite?ref=XDK12WP) | bybit | [Bybit](https://www.bybit.com/invite?ref=XDK12WP) | [![API Version 5](https://img.shields.io/badge/5-lightgray)](https://bybit-exchange.github.io/docs/inverse/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![cex](https://user-images.githubusercontent.com/1294454/27766442-8ddc33b0-5ed8-11e7-8b98-f786aef0f3c9.jpg)](https://cex.io/r/0/up105393824/0/) | cex | [CEX.IO](https://cex.io/r/0/up105393824/0/) | [![API Version *](https://img.shields.io/badge/*-lightgray)](https://trade.cex.io/docs/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![coinbase](https://user-images.githubusercontent.com/1294454/40811661-b6eceae2-653a-11e8-829e-10bfadb078cf.jpg)](https://www.coinbase.com/join/58cbe25a355148797479dbd2) | coinbase | [Coinbase Advanced](https://www.coinbase.com/join/58cbe25a355148797479dbd2) | [![API Version 2](https://img.shields.io/badge/2-lightgray)](https://developers.coinbase.com/api/v2) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![coinbaseexchange](https://github.com/ccxt/ccxt/assets/43336371/34a65553-88aa-4a38-a714-064bd228b97e)](https://coinbase.com/) | coinbaseexchange | [Coinbase Exchange](https://coinbase.com/) | [![API Version *](https://img.shields.io/badge/*-lightgray)](https://docs.cloud.coinbase.com/exchange/docs/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![coinbaseinternational](https://github.com/ccxt/ccxt/assets/43336371/866ae638-6ab5-4ebf-ab2c-cdcce9545625)](https://international.coinbase.com) | coinbaseinternational | [Coinbase International](https://international.coinbase.com) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://docs.cloud.coinbase.com/intx/docs) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![coincatch](https://github.com/user-attachments/assets/3d49065f-f05d-4573-88a2-1b5201ec6ff3)](https://partner.coincatch.cc/bg/92hy70391729607848548) | coincatch | [CoinCatch](https://partner.coincatch.cc/bg/92hy70391729607848548) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://coincatch.github.io/github.io/en/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![coincheck](https://user-images.githubusercontent.com/51840849/87182088-1d6d6380-c2ec-11ea-9c64-8ab9f9b289f5.jpg)](https://coincheck.com) | coincheck | [coincheck](https://coincheck.com) | [![API Version *](https://img.shields.io/badge/*-lightgray)](https://coincheck.com/documents/exchange/api) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![coinex](https://user-images.githubusercontent.com/51840849/87182089-1e05fa00-c2ec-11ea-8da9-cc73b45abbbc.jpg)](https://www.coinex.com/register?refer_code=yw5fz) | coinex | [CoinEx](https://www.coinex.com/register?refer_code=yw5fz) | [![API Version 2](https://img.shields.io/badge/2-lightgray)](https://docs.coinex.com/api/v2) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![coinmate](https://user-images.githubusercontent.com/51840849/87460806-1c9f3f00-c616-11ea-8c46-a77018a8f3f4.jpg)](https://coinmate.io?referral=YTFkM1RsOWFObVpmY1ZjMGREQmpTRnBsWjJJNVp3PT0) | coinmate | [CoinMate](https://coinmate.io?referral=YTFkM1RsOWFObVpmY1ZjMGREQmpTRnBsWjJJNVp3PT0) | [![API Version *](https://img.shields.io/badge/*-lightgray)](https://coinmate.docs.apiary.io) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![coinmetro](https://github.com/ccxt/ccxt/assets/43336371/e86f87ec-6ba3-4410-962b-f7988c5db539)](https://go.coinmetro.com/?ref=crypto24) | coinmetro | [Coinmetro](https://go.coinmetro.com/?ref=crypto24) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://documenter.getpostman.com/view/3653795/SVfWN6KS) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![coinone](https://user-images.githubusercontent.com/1294454/38003300-adc12fba-323f-11e8-8525-725f53c4a659.jpg)](https://coinone.co.kr) | coinone | [CoinOne](https://coinone.co.kr) | [![API Version 2](https://img.shields.io/badge/2-lightgray)](https://doc.coinone.co.kr) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![coinsph](https://user-images.githubusercontent.com/1294454/225719995-48ab2026-4ddb-496c-9da7-0d7566617c9b.jpg)](https://coins.ph/) | coinsph | [Coins.ph](https://coins.ph/) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://coins-docs.github.io/rest-api) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![coinspot](https://user-images.githubusercontent.com/1294454/28208429-3cacdf9a-6896-11e7-854e-4c79a772a30f.jpg)](https://www.coinspot.com.au/register?code=PJURCU) | coinspot | [CoinSpot](https://www.coinspot.com.au/register?code=PJURCU) | [![API Version *](https://img.shields.io/badge/*-lightgray)](https://www.coinspot.com.au/api) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![cryptocom](https://user-images.githubusercontent.com/1294454/147792121-38ed5e36-c229-48d6-b49a-48d05fc19ed4.jpeg)](https://crypto.com/exch/kdacthrnxt) | cryptocom | [Crypto.com](https://crypto.com/exch/kdacthrnxt) | [![API Version 2](https://img.shields.io/badge/2-lightgray)](https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![cryptomus](https://github.com/user-attachments/assets/8e0b1c48-7c01-4177-9224-f1b01d89d7e7)](https://app.cryptomus.com/signup/?ref=JRP4yj) | cryptomus | [Cryptomus](https://app.cryptomus.com/signup/?ref=JRP4yj) | [![API Version 2](https://img.shields.io/badge/2-lightgray)](https://doc.cryptomus.com/personal) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![deepcoin](https://github.com/user-attachments/assets/671bd35c-770e-4935-9070-f8fb114f79c4)](https://s.deepcoin.com/UzkyODgy) | deepcoin | [DeepCoin](https://s.deepcoin.com/UzkyODgy) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://www.deepcoin.com/docs) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![defx](https://github.com/user-attachments/assets/4e92bace-d7a9-45ea-92be-122168dc87e4)](https://app.defx.com/join/6I2CZ7) | defx | [Defx X](https://app.defx.com/join/6I2CZ7) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://docs.defx.com/docs) | ![DEX - Distributed EXchange](https://img.shields.io/badge/DEX-blue.svg "DEX - Distributed EXchange") | | | +| [![delta](https://user-images.githubusercontent.com/1294454/99450025-3be60a00-2931-11eb-9302-f4fd8d8589aa.jpg)](https://www.delta.exchange/app/signup/?code=IULYNB) | delta | [Delta Exchange](https://www.delta.exchange/app/signup/?code=IULYNB) | [![API Version 2](https://img.shields.io/badge/2-lightgray)](https://docs.delta.exchange) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![deribit](https://user-images.githubusercontent.com/1294454/41933112-9e2dd65a-798b-11e8-8440-5bab2959fcb8.jpg)](https://www.deribit.com/reg-1189.4038) | deribit | [Deribit](https://www.deribit.com/reg-1189.4038) | [![API Version 2](https://img.shields.io/badge/2-lightgray)](https://docs.deribit.com/v2) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![derive](https://github.com/user-attachments/assets/f835b95f-033a-43dd-b6bb-24e698fc498c)](https://www.derive.xyz/invite/3VB0B) | derive | [derive](https://www.derive.xyz/invite/3VB0B) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://docs.derive.xyz/docs/) | ![DEX - Distributed EXchange](https://img.shields.io/badge/DEX-blue.svg "DEX - Distributed EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![digifinex](https://user-images.githubusercontent.com/51840849/87443315-01283a00-c5fe-11ea-8628-c2a0feaf07ac.jpg)](https://www.digifinex.com/en-ww/from/DhOzBg?channelCode=ljaUPp) | digifinex | [DigiFinex](https://www.digifinex.com/en-ww/from/DhOzBg?channelCode=ljaUPp) | [![API Version 3](https://img.shields.io/badge/3-lightgray)](https://docs.digifinex.com) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![exmo](https://user-images.githubusercontent.com/1294454/27766491-1b0ea956-5eda-11e7-9225-40d67b481b8d.jpg)](https://exmo.me/?ref=131685) | exmo | [EXMO](https://exmo.me/?ref=131685) | [![API Version 1.1](https://img.shields.io/badge/1.1-lightgray)](https://exmo.me/en/api_doc?ref=131685) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![fmfwio](https://user-images.githubusercontent.com/1294454/159177712-b685b40c-5269-4cea-ac83-f7894c49525d.jpg)](https://fmfw.io/referral/da948b21d6c92d69) | fmfwio | [FMFW.io](https://fmfw.io/referral/da948b21d6c92d69) | [![API Version 3](https://img.shields.io/badge/3-lightgray)](https://api.fmfw.io/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![foxbit](https://github.com/user-attachments/assets/1f8faca2-ae2f-4222-b33e-5671e7d873dd)](https://app.foxbit.com.br) | foxbit | [Foxbit](https://app.foxbit.com.br) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://docs.foxbit.com.br) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![gate](https://github.com/user-attachments/assets/64f988c5-07b6-4652-b5c1-679a6bf67c85)](https://www.gate.com/share/CCXTGATE) | gate | [Gate](https://www.gate.com/share/CCXTGATE) | [![API Version 4](https://img.shields.io/badge/4-lightgray)](https://www.gate.com/docs/developers/apiv4/en/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![gemini](https://user-images.githubusercontent.com/1294454/27816857-ce7be644-6096-11e7-82d6-3c257263229c.jpg)](https://gemini.com/) | gemini | [Gemini](https://gemini.com/) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://docs.gemini.com/rest-api) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![hashkey](https://github.com/user-attachments/assets/6dd6127b-cc19-4a13-9b29-a98d81f80e98)](https://global.hashkey.com/en-US/register/invite?invite_code=82FQUN) | hashkey | [HashKey Global](https://global.hashkey.com/en-US/register/invite?invite_code=82FQUN) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://hashkeyglobal-apidoc.readme.io/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![hibachi](https://github.com/user-attachments/assets/7301bbb1-4f27-4167-8a55-75f74b14e973)](hibachi.xyz/r/ZBL2YFWIHU) | hibachi | [Hibachi](hibachi.xyz/r/ZBL2YFWIHU) | [![API Version *](https://img.shields.io/badge/*-lightgray)](undefined) | ![DEX - Distributed EXchange](https://img.shields.io/badge/DEX-blue.svg "DEX - Distributed EXchange") | | | +| [![hitbtc](https://user-images.githubusercontent.com/1294454/27766555-8eaec20e-5edc-11e7-9c5b-6dc69fc42f5e.jpg)](https://hitbtc.com/?ref_id=5a5d39a65d466) | hitbtc | [HitBTC](https://hitbtc.com/?ref_id=5a5d39a65d466) | [![API Version 3](https://img.shields.io/badge/3-lightgray)](https://api.hitbtc.com) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![hollaex](https://user-images.githubusercontent.com/1294454/75841031-ca375180-5ddd-11ea-8417-b975674c23cb.jpg)](https://pro.hollaex.com/signup?affiliation_code=QSWA6G) | hollaex | [HollaEx](https://pro.hollaex.com/signup?affiliation_code=QSWA6G) | [![API Version 2](https://img.shields.io/badge/2-lightgray)](https://apidocs.hollaex.com) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![htx](https://user-images.githubusercontent.com/1294454/76137448-22748a80-604e-11ea-8069-6e389271911d.jpg)](https://www.htx.com.vc/invite/en-us/1h?invite_code=6rmm2223) | htx | [HTX](https://www.htx.com.vc/invite/en-us/1h?invite_code=6rmm2223) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://huobiapi.github.io/docs/spot/v1/en/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![hyperliquid](https://github.com/ccxt/ccxt/assets/43336371/b371bc6c-4a8c-489f-87f4-20a913dd8d4b)](https://app.hyperliquid.xyz/) | hyperliquid | [Hyperliquid](https://app.hyperliquid.xyz/) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api) | ![DEX - Distributed EXchange](https://img.shields.io/badge/DEX-blue.svg "DEX - Distributed EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![independentreserve](https://user-images.githubusercontent.com/51840849/87182090-1e9e9080-c2ec-11ea-8e49-563db9a38f37.jpg)](https://www.independentreserve.com) | independentreserve | [Independent Reserve](https://www.independentreserve.com) | [![API Version *](https://img.shields.io/badge/*-lightgray)](https://www.independentreserve.com/API) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![indodax](https://user-images.githubusercontent.com/51840849/87070508-9358c880-c221-11ea-8dc5-5391afbbb422.jpg)](https://indodax.com/ref/testbitcoincoid/1) | indodax | [INDODAX](https://indodax.com/ref/testbitcoincoid/1) | [![API Version 2.0](https://img.shields.io/badge/2.0-lightgray)](https://github.com/btcid/indodax-official-api-docs) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![kraken](https://user-images.githubusercontent.com/51840849/76173629-fc67fb00-61b1-11ea-84fe-f2de582f58a3.jpg)](https://www.kraken.com) | kraken | [Kraken](https://www.kraken.com) | [![API Version 0](https://img.shields.io/badge/0-lightgray)](https://docs.kraken.com/rest/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![krakenfutures](https://user-images.githubusercontent.com/24300605/81436764-b22fd580-9172-11ea-9703-742783e6376d.jpg)](https://futures.kraken.com/) | krakenfutures | [Kraken Futures](https://futures.kraken.com/) | [![API Version 3](https://img.shields.io/badge/3-lightgray)](https://docs.kraken.com/api/docs/futures-api/trading/market-data/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![kucoin](https://user-images.githubusercontent.com/51840849/87295558-132aaf80-c50e-11ea-9801-a2fb0c57c799.jpg)](https://www.kucoin.com/ucenter/signup?rcode=E5wkqe) | kucoin | [KuCoin](https://www.kucoin.com/ucenter/signup?rcode=E5wkqe) | [![API Version 2](https://img.shields.io/badge/2-lightgray)](https://docs.kucoin.com) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![kucoinfutures](https://user-images.githubusercontent.com/1294454/147508995-9e35030a-d046-43a1-a006-6fabd981b554.jpg)](https://futures.kucoin.com/?rcode=E5wkqe) | kucoinfutures | [KuCoin Futures](https://futures.kucoin.com/?rcode=E5wkqe) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://docs.kucoin.com/futures) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![latoken](https://user-images.githubusercontent.com/1294454/61511972-24c39f00-aa01-11e9-9f7c-471f1d6e5214.jpg)](https://latoken.com/invite?r=mvgp2djk) | latoken | [Latoken](https://latoken.com/invite?r=mvgp2djk) | [![API Version 2](https://img.shields.io/badge/2-lightgray)](https://api.latoken.com) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![lbank](https://user-images.githubusercontent.com/1294454/38063602-9605e28a-3302-11e8-81be-64b1e53c4cfb.jpg)](https://www.lbank.com/login/?icode=7QCY) | lbank | [LBank](https://www.lbank.com/login/?icode=7QCY) | [![API Version 2](https://img.shields.io/badge/2-lightgray)](https://www.lbank.com/en-US/docs/index.html) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![luno](https://user-images.githubusercontent.com/1294454/27766607-8c1a69d8-5ede-11e7-930c-540b5eb9be24.jpg)](https://www.luno.com/invite/44893A) | luno | [luno](https://www.luno.com/invite/44893A) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://www.luno.com/en/api) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![mercado](https://user-images.githubusercontent.com/1294454/27837060-e7c58714-60ea-11e7-9192-f05e86adb83f.jpg)](https://www.mercadobitcoin.com.br) | mercado | [Mercado Bitcoin](https://www.mercadobitcoin.com.br) | [![API Version 3](https://img.shields.io/badge/3-lightgray)](https://www.mercadobitcoin.com.br/api-doc) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![mexc](https://user-images.githubusercontent.com/1294454/137283979-8b2a818d-8633-461b-bfca-de89e8c446b2.jpg)](https://www.mexc.com/register?inviteCode=mexc-1FQ1GNu1) | mexc | [MEXC Global](https://www.mexc.com/register?inviteCode=mexc-1FQ1GNu1) | [![API Version 3](https://img.shields.io/badge/3-lightgray)](https://mexcdevelop.github.io/apidocs/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![modetrade](https://github.com/user-attachments/assets/cec2b7f1-3b2b-4502-971b-447ee1937d6b)](https://trade.mode.network?ref=MODETRADE) | modetrade | [Mode Trade](https://trade.mode.network?ref=MODETRADE) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](undefined) | ![DEX - Distributed EXchange](https://img.shields.io/badge/DEX-blue.svg "DEX - Distributed EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![myokx](https://user-images.githubusercontent.com/1294454/152485636-38b19e4a-bece-4dec-979a-5982859ffc04.jpg)](https://www.my.okx.com/join/CCXT2023) | myokx | [MyOKX (EEA)](https://www.my.okx.com/join/CCXT2023) | [![API Version 5](https://img.shields.io/badge/5-lightgray)](https://my.okx.com/docs-v5/en/#overview) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![ndax](https://user-images.githubusercontent.com/1294454/108623144-67a3ef00-744e-11eb-8140-75c6b851e945.jpg)](https://one.ndax.io/bfQiSL) | ndax | [NDAX](https://one.ndax.io/bfQiSL) | [![API Version *](https://img.shields.io/badge/*-lightgray)](https://apidoc.ndax.io/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![novadax](https://user-images.githubusercontent.com/1294454/92337550-2b085500-f0b3-11ea-98e7-5794fb07dd3b.jpg)](https://www.novadax.com.br/?s=ccxt) | novadax | [NovaDAX](https://www.novadax.com.br/?s=ccxt) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://doc.novadax.com/pt-BR/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![oceanex](https://user-images.githubusercontent.com/1294454/58385970-794e2d80-8001-11e9-889c-0567cd79b78e.jpg)](https://oceanex.pro/signup?referral=VE24QX) | oceanex | [OceanEx](https://oceanex.pro/signup?referral=VE24QX) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://api.oceanex.pro/doc/v1) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![okx](https://user-images.githubusercontent.com/1294454/152485636-38b19e4a-bece-4dec-979a-5982859ffc04.jpg)](https://www.okx.com/join/CCXTCOM) | okx | [OKX](https://www.okx.com/join/CCXTCOM) | [![API Version 5](https://img.shields.io/badge/5-lightgray)](https://www.okx.com/docs-v5/en/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![okxus](https://user-images.githubusercontent.com/1294454/152485636-38b19e4a-bece-4dec-979a-5982859ffc04.jpg)](https://www.app.okx.com/join/CCXT2023) | okxus | [OKX (US)](https://www.app.okx.com/join/CCXT2023) | [![API Version 5](https://img.shields.io/badge/5-lightgray)](https://app.okx.com/docs-v5/en/#overview) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![onetrading](https://github.com/ccxt/ccxt/assets/43336371/bdbc26fd-02f2-4ca7-9f1e-17333690bb1c)](https://onetrading.com/) | onetrading | [One Trading](https://onetrading.com/) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://docs.onetrading.com) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![oxfun](https://github.com/ccxt/ccxt/assets/43336371/6a196124-c1ee-4fae-8573-962071b61a85)](https://ox.fun/register?shareAccountId=5ZUD4a7G) | oxfun | [OXFUN](https://ox.fun/register?shareAccountId=5ZUD4a7G) | [![API Version 3](https://img.shields.io/badge/3-lightgray)](https://docs.ox.fun/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![p2b](https://github.com/ccxt/ccxt/assets/43336371/8da13a80-1f0a-49be-bb90-ff8b25164755)](https://p2pb2b.com?referral=ee784c53) | p2b | [p2b](https://p2pb2b.com?referral=ee784c53) | [![API Version 2](https://img.shields.io/badge/2-lightgray)](https://github.com/P2B-team/p2b-api-docs/blob/master/api-doc.md) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![paradex](https://github.com/user-attachments/assets/84628770-784e-4ec4-a759-ec2fbb2244ea)](https://app.paradex.trade/r/ccxt24) | paradex | [Paradex](https://app.paradex.trade/r/ccxt24) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://docs.api.testnet.paradex.trade/) | ![DEX - Distributed EXchange](https://img.shields.io/badge/DEX-blue.svg "DEX - Distributed EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![paymium](https://user-images.githubusercontent.com/51840849/87153930-f0f02200-c2c0-11ea-9c0a-40337375ae89.jpg)](https://www.paymium.com/page/sign-up?referral=eDAzPoRQFMvaAB8sf-qj) | paymium | [Paymium](https://www.paymium.com/page/sign-up?referral=eDAzPoRQFMvaAB8sf-qj) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://github.com/Paymium/api-documentation) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![phemex](https://user-images.githubusercontent.com/1294454/85225056-221eb600-b3d7-11ea-930d-564d2690e3f6.jpg)](https://phemex.com/register?referralCode=EDNVJ) | phemex | [Phemex](https://phemex.com/register?referralCode=EDNVJ) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://phemex-docs.github.io/#overview) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![poloniex](https://user-images.githubusercontent.com/1294454/27766817-e9456312-5ee6-11e7-9b3c-b628ca5626a5.jpg)](https://poloniex.com/signup?c=UBFZJRPJ) | poloniex | [Poloniex](https://poloniex.com/signup?c=UBFZJRPJ) | [![API Version *](https://img.shields.io/badge/*-lightgray)](https://api-docs.poloniex.com/spot/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![probit](https://user-images.githubusercontent.com/51840849/79268032-c4379480-7ea2-11ea-80b3-dd96bb29fd0d.jpg)](https://www.probit.com/r/34608773) | probit | [ProBit](https://www.probit.com/r/34608773) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://docs-en.probit.com) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![timex](https://user-images.githubusercontent.com/1294454/70423869-6839ab00-1a7f-11ea-8f94-13ae72c31115.jpg)](https://timex.io/?refcode=1x27vNkTbP1uwkCck) | timex | [TimeX](https://timex.io/?refcode=1x27vNkTbP1uwkCck) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://plasma-relay-backend.timex.io/swagger-ui/index.html) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![tokocrypto](https://user-images.githubusercontent.com/1294454/183870484-d3398d0c-f6a1-4cce-91b8-d58792308716.jpg)](https://tokocrypto.com) | tokocrypto | [Tokocrypto](https://tokocrypto.com) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://www.tokocrypto.com/apidocs/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![toobit](https://github.com/user-attachments/assets/3fc13870-5406-431b-8be0-2aab69c4f225)](https://www.toobit.com/en-US/r?i=IFFPy0) | toobit | [Toobit](https://www.toobit.com/en-US/r?i=IFFPy0) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://toobit-docs.github.io/apidocs/spot/v1/en/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![upbit](https://user-images.githubusercontent.com/1294454/49245610-eeaabe00-f423-11e8-9cba-4b0aed794799.jpg)](https://upbit.com) | upbit | [Upbit](https://upbit.com) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://docs.upbit.com/docs/%EC%9A%94%EC%B2%AD-%EC%88%98-%EC%A0%9C%ED%95%9C) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![wavesexchange](https://user-images.githubusercontent.com/1294454/84547058-5fb27d80-ad0b-11ea-8711-78ac8b3c7f31.jpg)](https://wx.network) | wavesexchange | [Waves.Exchange](https://wx.network) | [![API Version *](https://img.shields.io/badge/*-lightgray)](https://docs.wx.network) | ![DEX - Distributed EXchange](https://img.shields.io/badge/DEX-blue.svg "DEX - Distributed EXchange") | | | +| [![whitebit](https://user-images.githubusercontent.com/1294454/66732963-8eb7dd00-ee66-11e9-849b-10d9282bb9e0.jpg)](https://whitebit.com/referral/d9bdf40e-28f2-4b52-b2f9-cd1415d82963) | whitebit | [WhiteBit](https://whitebit.com/referral/d9bdf40e-28f2-4b52-b2f9-cd1415d82963) | [![API Version 4](https://img.shields.io/badge/4-lightgray)](https://github.com/whitebit-exchange/api-docs) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![woo](https://user-images.githubusercontent.com/1294454/150730761-1a00e5e0-d28c-480f-9e65-089ce3e6ef3b.jpg)](https://woox.io/register?ref=DIJT0CNL) | woo | [WOO X](https://woox.io/register?ref=DIJT0CNL) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://docs.woox.io/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![woofipro](https://github.com/user-attachments/assets/9ba21b8a-a9c7-4770-b7f1-ce3bcbde68c1)](https://dex.woo.org/en/trade?ref=CCXT) | woofipro | [WOOFI PRO](https://dex.woo.org/en/trade?ref=CCXT) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://orderly.network/docs/build-on-evm/building-on-evm) | ![DEX - Distributed EXchange](https://img.shields.io/badge/DEX-blue.svg "DEX - Distributed EXchange") | [![CCXT Certified](https://img.shields.io/badge/CCXT-Certified-green.svg)](https://github.com/ccxt/ccxt/wiki/Certification) | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![xt](https://user-images.githubusercontent.com/14319357/232636712-466df2fc-560a-4ca4-aab2-b1d954a58e24.jpg)](https://www.xt.com/en/accounts/register?ref=9PTM9VW) | xt | [XT](https://www.xt.com/en/accounts/register?ref=9PTM9VW) | [![API Version 4](https://img.shields.io/badge/4-lightgray)](https://doc.xt.com/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | [![CCXT Pro](https://img.shields.io/badge/CCXT-Pro-black)](https://ccxt.pro) | +| [![yobit](https://user-images.githubusercontent.com/1294454/27766910-cdcbfdae-5eea-11e7-9859-03fea873272d.jpg)](https://www.yobit.net) | yobit | [YoBit](https://www.yobit.net) | [![API Version 3](https://img.shields.io/badge/3-lightgray)](https://www.yobit.net/en/api/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![zaif](https://user-images.githubusercontent.com/1294454/27766927-39ca2ada-5eeb-11e7-972f-1b4199518ca6.jpg)](https://zaif.jp) | zaif | [Zaif](https://zaif.jp) | [![API Version 1](https://img.shields.io/badge/1-lightgray)](https://techbureau-api-document.readthedocs.io/ja/latest/index.html) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | +| [![zonda](https://user-images.githubusercontent.com/1294454/159202310-a0e38007-5e7c-4ba9-a32f-c8263a0291fe.jpg)](https://auth.zondaglobal.com/ref/jHlbB4mIkdS1) | zonda | [Zonda](https://auth.zondaglobal.com/ref/jHlbB4mIkdS1) | [![API Version *](https://img.shields.io/badge/*-lightgray)](https://docs.zondacrypto.exchange/) | ![CEX – Centralized EXchange](https://img.shields.io/badge/CEX-green.svg "CEX – Centralized EXchange") | | | + + +The list above is updated frequently, new crypto markets, exchanges, bug fixes, and API endpoints are introduced on a regular basis. See the [Manual](https://github.com/ccxt/ccxt/wiki/) for more details. If you can't find a cryptocurrency exchange in the list above and want it to be added, post a link to it by opening an issue here on GitHub or send us an email. + +The library is under [MIT license](https://github.com/ccxt/ccxt/blob/master/LICENSE.txt), that means it's absolutely free for any developer to build commercial and opensource software on top of it, but use it at your own risk with no warranties, as is. + +--- + +## Install + +The easiest way to install the CCXT library is to use a package manager: + +- [ccxt in **NPM**](https://www.npmjs.com/package/ccxt) (JavaScript / Node v7.6+) +- [ccxt in **PyPI**](https://pypi.python.org/pypi/ccxt) (Python 3.7.0+) +- [ccxt in **Packagist/Composer**](https://packagist.org/packages/ccxt/ccxt) (PHP 8.1+) +- [ccxt in **Nuget**](https://www.nuget.org/packages/ccxt) (netstandard 2.0) +- [ccxt in **GO**](https://pkg.go.dev/github.com/ccxt/ccxt/go/v4) + +This library is shipped as an all-in-one module implementation with minimalistic dependencies and requirements: + +- [js/](https://github.com/ccxt/ccxt/blob/master/js/) in JavaScript +- [python/](https://github.com/ccxt/ccxt/blob/master/python/) in Python (generated from TS) +- [php/](https://github.com/ccxt/ccxt/blob/master/php/) in PHP (generated from TS) +- [cs/](https://github.com/ccxt/ccxt/blob/master/cs/) in C# (generated from TS) +- [go/](https://github.com/ccxt/ccxt/blob/master/go/) in Go (generated from TS) + +You can also clone it into your project directory from [ccxt GitHub repository](https://github.com/ccxt/ccxt): + +```shell +git clone https://github.com/ccxt/ccxt.git # including 1GB of commit history + +# or + +git clone https://github.com/ccxt/ccxt.git --depth 1 # avoid downloading 1GB of commit history +``` + +### JavaScript (NPM) + +JavaScript version of CCXT works in both Node and web browsers. Requires ES6 and `async/await` syntax support (Node 7.6.0+). When compiling with Webpack and Babel, make sure it is [not excluded](https://github.com/ccxt/ccxt/issues/225#issuecomment-331905178) in your `babel-loader` config. + +[ccxt in **NPM**](https://www.npmjs.com/package/ccxt) + +```shell +npm install ccxt +``` + +```JavaScript +//cjs +var ccxt = require ('ccxt') +console.log (ccxt.exchanges) // print all available exchanges +``` +```Javascript +//esm +import {version, exchanges} from 'ccxt'; +console.log(version, Object.keys(exchanges)); +``` + +### JavaScript (for use with the ` +``` + +Creates a global `ccxt` object: + +```JavaScript +console.log (ccxt.exchanges) // print all available exchanges +``` + +### Python + +[ccxt in **PyPI**](https://pypi.python.org/pypi/ccxt) + +```shell +pip install ccxt +``` + +```Python +import ccxt +print(ccxt.exchanges) # print a list of all available exchange classes +``` + +The library supports concurrent asynchronous mode with asyncio and async/await in Python 3.7.0+ + +```Python +import ccxt.async_support as ccxt # link against the asynchronous version of ccxt +``` + +#### orjson support + +CCXT also supports `orjson` for parsing JSON since it is much faster than the builtin library. This is especially important when using websockets because some exchanges return big messages that need to be parsed and dispatched as quickly as possible. + +However, `orjson` is not enabled by default because it is not supported by every python interpreter. If you want to opt-in, you just need to install it (`pip install orjson`) on your local environment. CCXT will detect the installion and pick it up automatically. + +#### ECDSA Support + +Some exchanges, such as Hyperliquid, Binance, and Paradex use **ECDSA** for request signing. +By default, CCXT includes a pure Python ECDSA implementation that ensures compatibility across all environments. However, this implementation may not meet the performance requirements of latency-sensitive applications. + +To address this, CCXT also supports the Coincurve library, which dramatically reduces signing time from approximately 45 ms to under 0.05 ms. + +For optimal performance, we recommend installing Coincurve via: + +``` +pip install coincurve +``` + +Once installed, CCXT will automatically detect and use it. + +### PHP + +[ccxt in PHP with **Packagist/Composer**](https://packagist.org/packages/ccxt/ccxt) (PHP 8.1+) + +It requires common PHP modules: + +- cURL +- mbstring (using UTF-8 is highly recommended) +- PCRE +- iconv +- gmp + +```PHP +include "ccxt.php"; +var_dump (\ccxt\Exchange::$exchanges); // print a list of all available exchange classes +``` + +The library supports concurrent asynchronous mode using tools from [ReactPHP](https://reactphp.org/) in PHP 8.1+. Read the [Manual](https://github.com/ccxt/ccxt/wiki/) for more details. + +### .net/C# + +[ccxt in C# with **Nuget**](https://www.nuget.org/packages/ccxt) (netstandard 2.0 and netstandard 2.1) +```c# +using ccxt; +Console.WriteLine(ccxt.Exchanges) // check this later +``` + +### Go + +[ccxt in GO with **PKG**](https://pkg.go.dev/github.com/ccxt/ccxt/go/v4) + +```shell +go install github.com/ccxt/ccxt/go/v4@latest +``` + +```Go +import "ccxt" +fmt.Println(ccxt.Exchanges) +``` + +### Docker + +You can get CCXT installed in a container along with all the supported languages and dependencies. This may be useful if you want to contribute to CCXT (e.g. run the build scripts and tests — please see the [Contributing](https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md) document for the details on that). + +Using `docker-compose` (in the cloned CCXT repository): + +```shell +docker-compose run --rm ccxt +``` + +You don't need the Docker image if you're not going to develop CCXT. If you just want to use CCXT – just install it as a regular package into your project. + +--- + +## Usage + +### Intro + +The CCXT library consists of a public part and a private part. Anyone can use the public part immediately after installation. Public APIs provide unrestricted access to public information for all exchange markets without the need to register a user account or have an API key. + +Public APIs include the following: + +- market data +- instruments/trading pairs +- price feeds (exchange rates) +- order books +- trade history +- tickers +- OHLC(V) for charting +- other public endpoints + +In order to trade with private APIs you need to obtain API keys from an exchange's website. It usually means signing up to the exchange and creating API keys for your account. Some exchanges require personal info or identification. Sometimes verification may be necessary as well. In this case you will need to register yourself, this library will not create accounts or API keys for you. Some exchanges expose API endpoints for registering an account, but most exchanges don't. You will have to sign up and create API keys on their websites. + +Private APIs allow the following: + +- manage personal account info +- query account balances +- trade by making market and limit orders +- deposit and withdraw fiat and crypto funds +- query personal orders +- get ledger history +- transfer funds between accounts +- use merchant services + +This library implements full public and private REST and WebSocket APIs for all exchanges in TypeScript, JavaScript, PHP and Python. + +The CCXT library supports both camelcase notation (preferred in TypeScript and JavaScript) and underscore notation (preferred in Python and PHP), therefore all methods can be called in either notation or coding style in any language. + +```JavaScript +// both of these notations work in JavaScript/Python/PHP +exchange.methodName () // camelcase pseudocode +exchange.method_name () // underscore pseudocode +``` + +Read the [Manual](https://github.com/ccxt/ccxt/wiki/) for more details. + +### JavaScript + +**CCXT now supports ESM and CJS modules** + +#### CJS + +```JavaScript +// cjs example +'use strict'; +const ccxt = require ('ccxt'); + +(async function () { + let kraken = new ccxt.kraken () + let bitfinex = new ccxt.bitfinex ({ verbose: true }) + let huobipro = new ccxt.huobipro () + let okcoinusd = new ccxt.okcoin ({ + apiKey: 'YOUR_PUBLIC_API_KEY', + secret: 'YOUR_SECRET_PRIVATE_KEY', + }) + + const exchangeId = 'binance' + , exchangeClass = ccxt[exchangeId] + , exchange = new exchangeClass ({ + 'apiKey': 'YOUR_API_KEY', + 'secret': 'YOUR_SECRET', + }) + + console.log (kraken.id, await kraken.loadMarkets ()) + console.log (bitfinex.id, await bitfinex.loadMarkets ()) + console.log (huobipro.id, await huobipro.loadMarkets ()) + + console.log (kraken.id, await kraken.fetchOrderBook (kraken.symbols[0])) + console.log (bitfinex.id, await bitfinex.fetchTicker ('BTC/USD')) + console.log (huobipro.id, await huobipro.fetchTrades ('ETH/USDT')) + + console.log (okcoinusd.id, await okcoinusd.fetchBalance ()) + + // sell 1 BTC/USD for market price, sell a bitcoin for dollars immediately + console.log (okcoinusd.id, await okcoinusd.createMarketSellOrder ('BTC/USD', 1)) + + // buy 1 BTC/USD for $2500, you pay $2500 and receive ฿1 when the order is closed + console.log (okcoinusd.id, await okcoinusd.createLimitBuyOrder ('BTC/USD', 1, 2500.00)) + + // pass/redefine custom exchange-specific order params: type, amount, price or whatever + // use a custom order type + bitfinex.createLimitSellOrder ('BTC/USD', 1, 10, { 'type': 'trailing-stop' }) + +}) (); +``` +#### ESM + +```Javascript +//esm example +import {version, binance} from 'ccxt'; + +console.log(version); +const exchange = new binance(); +const ticker = await exchange.fetchTicker('BTC/USDT'); +console.log(ticker); +``` + +### Python + +```Python +# coding=utf-8 + +import ccxt + +hitbtc = ccxt.hitbtc({'verbose': True}) +bitmex = ccxt.bitmex() +huobipro = ccxt.huobipro() +exmo = ccxt.exmo({ + 'apiKey': 'YOUR_PUBLIC_API_KEY', + 'secret': 'YOUR_SECRET_PRIVATE_KEY', +}) +kraken = ccxt.kraken({ + 'apiKey': 'YOUR_PUBLIC_API_KEY', + 'secret': 'YOUR_SECRET_PRIVATE_KEY', +}) + +exchange_id = 'binance' +exchange_class = getattr(ccxt, exchange_id) +exchange = exchange_class({ + 'apiKey': 'YOUR_API_KEY', + 'secret': 'YOUR_SECRET', +}) + +hitbtc_markets = hitbtc.load_markets() + +print(hitbtc.id, hitbtc_markets) +print(bitmex.id, bitmex.load_markets()) +print(huobipro.id, huobipro.load_markets()) + +print(hitbtc.fetch_order_book(hitbtc.symbols[0])) +print(bitmex.fetch_ticker('BTC/USD')) +print(huobipro.fetch_trades('LTC/USDT')) + +print(exmo.fetch_balance()) + +# sell one ฿ for market price and receive $ right now +print(exmo.id, exmo.create_market_sell_order('BTC/USD', 1)) + +# limit buy BTC/EUR, you pay €2500 and receive ฿1 when the order is closed +print(exmo.id, exmo.create_limit_buy_order('BTC/EUR', 1, 2500.00)) + +# pass/redefine custom exchange-specific order params: type, amount, price, flags, etc... +kraken.create_market_buy_order('BTC/USD', 1, {'trading_agreement': 'agree'}) +``` + +### PHP + +```PHP +include 'ccxt.php'; + +$poloniex = new \ccxt\poloniex (); +$bittrex = new \ccxt\bittrex (array ('verbose' => true)); +$quoinex = new \ccxt\quoinex (); +$zaif = new \ccxt\zaif (array ( + 'apiKey' => 'YOUR_PUBLIC_API_KEY', + 'secret' => 'YOUR_SECRET_PRIVATE_KEY', +)); +$hitbtc = new \ccxt\hitbtc (array ( + 'apiKey' => 'YOUR_PUBLIC_API_KEY', + 'secret' => 'YOUR_SECRET_PRIVATE_KEY', +)); + +$exchange_id = 'binance'; +$exchange_class = "\\ccxt\\$exchange_id"; +$exchange = new $exchange_class (array ( + 'apiKey' => 'YOUR_API_KEY', + 'secret' => 'YOUR_SECRET', +)); + +$poloniex_markets = $poloniex->load_markets (); + +var_dump ($poloniex_markets); +var_dump ($bittrex->load_markets ()); +var_dump ($quoinex->load_markets ()); + +var_dump ($poloniex->fetch_order_book ($poloniex->symbols[0])); +var_dump ($bittrex->fetch_trades ('BTC/USD')); +var_dump ($quoinex->fetch_ticker ('ETH/EUR')); +var_dump ($zaif->fetch_ticker ('BTC/JPY')); + +var_dump ($zaif->fetch_balance ()); + +// sell 1 BTC/JPY for market price, you pay ¥ and receive ฿ immediately +var_dump ($zaif->id, $zaif->create_market_sell_order ('BTC/JPY', 1)); + +// buy BTC/JPY, you receive ฿1 for ¥285000 when the order closes +var_dump ($zaif->id, $zaif->create_limit_buy_order ('BTC/JPY', 1, 285000)); + +// set a custom user-defined id to your order +$hitbtc->create_order ('BTC/USD', 'limit', 'buy', 1, 3000, array ('clientOrderId' => '123')); +``` + +### .net/C# + +```C# +using ccxt; // importing ccxt +namespace Project; +class Project { + public async static Task CreateOrder() { + var exchange = new Binance(); + exchange.apiKey = "my api key"; + exchange.secret = "my secret"; + // always use the capitalized method (CreateOrder instead of createOrder) + var order = await exchange.CreateOrder("BTC/USDT", "limit", "buy", 1, 50); + Console.WriteLine("Placed Order, order id: " + order.id); + } +} +``` + +### Go + +```Go +package main +import ( + "github.com/ccxt/ccxt/go/v4/go" + "fmt" +) + +func main() { + exchange := ccxt.NewBinance(map[string]interface{}{ + "apiKey": "MY KEY", + "secret": "MY SECRET", + }) + orderParams := map[string]interface{}{ + "clientOrderId": "myOrderId68768678", + } + + exchange.LoadMarkets() + + order, err := exchange.CreateOrder("BTC/USDT", "limit", "buy", 0.001, ccxt.WithCreateOrderPrice(6000), ccxt.WithCreateOrderParams(orderParams)) + if err != nil { + if ccxtError, ok := err.(*ccxt.Error); ok { + if ccxtError.Type == "InvalidOrder" { + fmt.Println("Invalid order") + } else { + fmt.Println("Some other error") + } + } + } else { + fmt.Println(*order.Id) + } + + + // fetching OHLCV + ohlcv, err := exchange.FetchOHLCV("BTC/USDT", ccxt.WithFetchOHLCVTimeframe("5m"), ccxt.WithFetchOHLCVLimit(100)) + + if err != nil { + fmt.Println("Error: ", err) + } else { + fmt.Println("Got OHLCV!") + } +} +``` + +#### Optional parameters + +Unlike Javascript/Python/PHP/C# Go does not support "traditional" optional parameters like `function a(optional = false)`. However, the CCXT language and structure have some methods with optional params, and since the Go language is transpiled from the Typescript source, we had to find a way of representing them. + +We have decided to "go" (pun intended) with Option structs and the `WithX` methods. + +For example, this function `FetchMyTrades` supports 4 different "optional" parameters, symbol, since, limit, and params. + +```Golang +func (this *Binance) FetchMyTrades(options ...FetchMyTradesOptions) ([]Trade, error) +``` + +And we can provide them by doing + +```Golang +trades, error := exchange.FetchMyTrades(ccxt.withFetchMyTradesSymbol("BTC/USDT"), ccxt.WithFetchOHLCVLimit(5), ccxt.WithFetchMyTradesParams(orderParams)) +``` + +Lastly, just because the signature dictates that some argument like `symbol` is optional, it will depend from exchange to exchange and you might need to provide it to avoid getting a `SymbolRequired` error. + +You can check different examples in the `examples/go` folder. + +## CCXT CLI + +Read the documentation for more information and details: [docs](https://github.com/ccxt/ccxt/tree/master/cli/README.md) + +CCXT also provides a command-line interface (CLI) that enables direct interaction with any supported exchange from the terminal. You can quickly check balances, place orders, or fetch trade data—without the need to write or execute custom code. This is especially useful for simple or time-sensitive tasks that don’t warrant the overhead of building a full application. + +### Installation + +The CLI is available as a npm package and can be installed by doing + +``` +npm i ccxt-cli -g +``` + +### Usage + +You can use the `--help` option to view a general overview of how the CLI works. The tool allows you to invoke any CCXT method by specifying the exchange id, the methodName, and any required arguments. + +Examples: + +``` +ccxt binance createOrder BTC/USDT market buy 0.1 // places an order +``` +If you are not sure which arguments should be provided you can always use the `explain` command. + +``` +ccxt explain createOrder +``` + +result: + +``` +Method: createOrder +Usage: + binance createOrder [price] [params] + +Arguments: + - symbol (required) — Market symbol e.g., BTC/USDT + - type (required) — (no description available) + - side (required) — order side e.g., buy or sell + - amount (required) — (no description available) + - price (optional) — Price per unit of asset e.g., 26000.50 + - params (optional) — Extra parameters for the exchange e.g., { "recvWindow": 5000 } +``` + +You can easily provide API keys by setting them as environment varibales eg: `BINANCE_APIKEY="XXXX"` or adding them to the config file located at `$CACHE/config.json` + +## Contributing + +Please read the [CONTRIBUTING](https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md) document before making changes that you would like adopted in the code. Also, read the [Manual](https://github.com/ccxt/ccxt/wiki) for more details. + +## Support Developer Team + +We are investing a significant amount of time into the development of this library. If CCXT made your life easier and you want to help us improve it further, or if you want to speed up development of new features and exchanges, please support us with a tip. We appreciate all contributions! + +### Sponsors + +Support this project by becoming a sponsor. + +[[Become a sponsor](https://opencollective.com/ccxt#sponsor)] + + + + + + + + + + + + +### Supporters + +Support this project by becoming a supporter. Your avatar will show up here with a link to your website. + +[[Become a supporter](https://opencollective.com/ccxt#supporter)] + + + + + + + + + + + + +### Backers + +Thank you to all our backers! [[Become a backer](https://opencollective.com/ccxt#backer)] + + + +Thank you! + +## Social + +- [![Twitter](https://img.shields.io/twitter/follow/ccxt_official?style=social)](https://twitter.com/ccxt_official) Follow us on Twitter +- [![Medium](https://img.shields.io/badge/read-our%20blog-black?logo=medium)](https://medium.com/@ccxt) Read our blog on Medium +- [![Discord](https://img.shields.io/discord/690203284119617602?logo=discord&logoColor=white)](https://discord.gg/dhzSKYU) Join our Discord +- [![Telegram Announcements](https://img.shields.io/badge/CCXT-Channel-blue?logo=telegram)](https://t.me/ccxt_announcements) CCXT Channel on Telegram (important announcements) +- [![Telegram Chat](https://img.shields.io/badge/CCXT-Chat-blue?logo=telegram)](https://t.me/ccxt_chat) CCXT Chat on Telegram (technical support) + +## Star History + +[![Star History Chart](https://api.star-history.com/svg?repos=ccxt/ccxt&type=Date)](https://star-history.com/#ccxt/ccxt&Date) + +## Disclaimer + +CCXT is not a service nor a server. CCXT is a software. **CCXT is a free open source non-custodian API broker software under MIT license**. + +- **Non-custodian** means CCXT is not an intermediary in trading, it does not hold traders' money at any point in time, traders install CCXT and use CCXT to talk to exchanges directly. +- **MIT license** means CCXT can be used for any purpose, but use at your own risk without any warranties. +- **API broker** means CCXT is funded with rebates from exchanges' API broker programs and it is an official API broker with many exchanges, all rebates and related fees are handled by the exchanges solely in accordance with exchanges' respective terms and conditions established by each partner exchange. +- **Free software** means CCXT is free to use and has no hidden fees, with CCXT traders pay the same trading fees they would pay to the exchanges directly. +- **Open source** means anyone is allowed to use it, to look inside the code and to change everything, including other brokers. + +*CCXT has joined Hyperliquid’s Builder Codes program (see announcement) and may also utilize its referral code, which offers users a 4% fee discount on their first 25 million in trading volume.* + +## Contact Us + +For business inquiries: info@ccxt.trade diff --git a/ccxt.egg-info/SOURCES.txt b/ccxt.egg-info/SOURCES.txt new file mode 100644 index 0000000..2d7e6b8 --- /dev/null +++ b/ccxt.egg-info/SOURCES.txt @@ -0,0 +1,677 @@ +LICENSE.txt +MANIFEST.in +README.rst +package.json +setup.cfg +setup.py +ccxt/__init__.py +ccxt/alpaca.py +ccxt/apex.py +ccxt/arkham.py +ccxt/ascendex.py +ccxt/backpack.py +ccxt/bequant.py +ccxt/bigone.py +ccxt/binance.py +ccxt/binancecoinm.py +ccxt/binanceus.py +ccxt/binanceusdm.py +ccxt/bingx.py +ccxt/bit2c.py +ccxt/bitbank.py +ccxt/bitbns.py +ccxt/bitfinex.py +ccxt/bitflyer.py +ccxt/bitget.py +ccxt/bithumb.py +ccxt/bitmart.py +ccxt/bitmex.py +ccxt/bitopro.py +ccxt/bitrue.py +ccxt/bitso.py +ccxt/bitstamp.py +ccxt/bitteam.py +ccxt/bittrade.py +ccxt/bitvavo.py +ccxt/blockchaincom.py +ccxt/blofin.py +ccxt/btcalpha.py +ccxt/btcbox.py +ccxt/btcmarkets.py +ccxt/btcturk.py +ccxt/bybit.py +ccxt/cex.py +ccxt/coinbase.py +ccxt/coinbaseadvanced.py +ccxt/coinbaseexchange.py +ccxt/coinbaseinternational.py +ccxt/coincatch.py +ccxt/coincheck.py +ccxt/coinex.py +ccxt/coinmate.py +ccxt/coinmetro.py +ccxt/coinone.py +ccxt/coinsph.py +ccxt/coinspot.py +ccxt/cryptocom.py +ccxt/cryptomus.py +ccxt/deepcoin.py +ccxt/defx.py +ccxt/delta.py +ccxt/deribit.py +ccxt/derive.py +ccxt/digifinex.py +ccxt/exmo.py +ccxt/fmfwio.py +ccxt/foxbit.py +ccxt/gate.py +ccxt/gateio.py +ccxt/gemini.py +ccxt/hashkey.py +ccxt/hibachi.py +ccxt/hitbtc.py +ccxt/hollaex.py +ccxt/htx.py +ccxt/huobi.py +ccxt/hyperliquid.py +ccxt/independentreserve.py +ccxt/indodax.py +ccxt/kraken.py +ccxt/krakenfutures.py +ccxt/kucoin.py +ccxt/kucoinfutures.py +ccxt/latoken.py +ccxt/lbank.py +ccxt/luno.py +ccxt/mercado.py +ccxt/mexc.py +ccxt/modetrade.py +ccxt/mt5.py +ccxt/myokx.py +ccxt/ndax.py +ccxt/novadax.py +ccxt/oceanex.py +ccxt/okx.py +ccxt/okxus.py +ccxt/onetrading.py +ccxt/oxfun.py +ccxt/p2b.py +ccxt/paradex.py +ccxt/paymium.py +ccxt/phemex.py +ccxt/poloniex.py +ccxt/probit.py +ccxt/timex.py +ccxt/tokocrypto.py +ccxt/toobit.py +ccxt/upbit.py +ccxt/wavesexchange.py +ccxt/whitebit.py +ccxt/woo.py +ccxt/woofipro.py +ccxt/xt.py +ccxt/yobit.py +ccxt/zaif.py +ccxt/zonda.py +ccxt.egg-info/PKG-INFO +ccxt.egg-info/SOURCES.txt +ccxt.egg-info/dependency_links.txt +ccxt.egg-info/requires.txt +ccxt.egg-info/top_level.txt +ccxt/abstract/__init__.py +ccxt/abstract/alpaca.py +ccxt/abstract/apex.py +ccxt/abstract/arkham.py +ccxt/abstract/ascendex.py +ccxt/abstract/backpack.py +ccxt/abstract/bequant.py +ccxt/abstract/bigone.py +ccxt/abstract/binance.py +ccxt/abstract/binancecoinm.py +ccxt/abstract/binanceus.py +ccxt/abstract/binanceusdm.py +ccxt/abstract/bingx.py +ccxt/abstract/bit2c.py +ccxt/abstract/bitbank.py +ccxt/abstract/bitbns.py +ccxt/abstract/bitfinex.py +ccxt/abstract/bitflyer.py +ccxt/abstract/bitget.py +ccxt/abstract/bithumb.py +ccxt/abstract/bitmart.py +ccxt/abstract/bitmex.py +ccxt/abstract/bitopro.py +ccxt/abstract/bitrue.py +ccxt/abstract/bitso.py +ccxt/abstract/bitstamp.py +ccxt/abstract/bitteam.py +ccxt/abstract/bittrade.py +ccxt/abstract/bitvavo.py +ccxt/abstract/blockchaincom.py +ccxt/abstract/blofin.py +ccxt/abstract/btcalpha.py +ccxt/abstract/btcbox.py +ccxt/abstract/btcmarkets.py +ccxt/abstract/btcturk.py +ccxt/abstract/bybit.py +ccxt/abstract/cex.py +ccxt/abstract/coinbase.py +ccxt/abstract/coinbaseadvanced.py +ccxt/abstract/coinbaseexchange.py +ccxt/abstract/coinbaseinternational.py +ccxt/abstract/coincatch.py +ccxt/abstract/coincheck.py +ccxt/abstract/coinex.py +ccxt/abstract/coinmate.py +ccxt/abstract/coinmetro.py +ccxt/abstract/coinone.py +ccxt/abstract/coinsph.py +ccxt/abstract/coinspot.py +ccxt/abstract/cryptocom.py +ccxt/abstract/cryptomus.py +ccxt/abstract/deepcoin.py +ccxt/abstract/defx.py +ccxt/abstract/delta.py +ccxt/abstract/deribit.py +ccxt/abstract/derive.py +ccxt/abstract/digifinex.py +ccxt/abstract/exmo.py +ccxt/abstract/fmfwio.py +ccxt/abstract/foxbit.py +ccxt/abstract/gate.py +ccxt/abstract/gateio.py +ccxt/abstract/gemini.py +ccxt/abstract/hashkey.py +ccxt/abstract/hibachi.py +ccxt/abstract/hitbtc.py +ccxt/abstract/hollaex.py +ccxt/abstract/htx.py +ccxt/abstract/huobi.py +ccxt/abstract/hyperliquid.py +ccxt/abstract/independentreserve.py +ccxt/abstract/indodax.py +ccxt/abstract/kraken.py +ccxt/abstract/krakenfutures.py +ccxt/abstract/kucoin.py +ccxt/abstract/kucoinfutures.py +ccxt/abstract/latoken.py +ccxt/abstract/lbank.py +ccxt/abstract/luno.py +ccxt/abstract/mercado.py +ccxt/abstract/mexc.py +ccxt/abstract/modetrade.py +ccxt/abstract/mt5.py +ccxt/abstract/myokx.py +ccxt/abstract/ndax.py +ccxt/abstract/novadax.py +ccxt/abstract/oceanex.py +ccxt/abstract/okx.py +ccxt/abstract/okxus.py +ccxt/abstract/onetrading.py +ccxt/abstract/oxfun.py +ccxt/abstract/p2b.py +ccxt/abstract/paradex.py +ccxt/abstract/paymium.py +ccxt/abstract/phemex.py +ccxt/abstract/poloniex.py +ccxt/abstract/probit.py +ccxt/abstract/timex.py +ccxt/abstract/tokocrypto.py +ccxt/abstract/toobit.py +ccxt/abstract/upbit.py +ccxt/abstract/wavesexchange.py +ccxt/abstract/whitebit.py +ccxt/abstract/woo.py +ccxt/abstract/woofipro.py +ccxt/abstract/xt.py +ccxt/abstract/yobit.py +ccxt/abstract/zaif.py +ccxt/abstract/zonda.py +ccxt/async_support/__init__.py +ccxt/async_support/alpaca.py +ccxt/async_support/apex.py +ccxt/async_support/arkham.py +ccxt/async_support/ascendex.py +ccxt/async_support/backpack.py +ccxt/async_support/bequant.py +ccxt/async_support/bigone.py +ccxt/async_support/binance.py +ccxt/async_support/binancecoinm.py +ccxt/async_support/binanceus.py +ccxt/async_support/binanceusdm.py +ccxt/async_support/bingx.py +ccxt/async_support/bit2c.py +ccxt/async_support/bitbank.py +ccxt/async_support/bitbns.py +ccxt/async_support/bitfinex.py +ccxt/async_support/bitflyer.py +ccxt/async_support/bitget.py +ccxt/async_support/bithumb.py +ccxt/async_support/bitmart.py +ccxt/async_support/bitmex.py +ccxt/async_support/bitopro.py +ccxt/async_support/bitrue.py +ccxt/async_support/bitso.py +ccxt/async_support/bitstamp.py +ccxt/async_support/bitteam.py +ccxt/async_support/bittrade.py +ccxt/async_support/bitvavo.py +ccxt/async_support/blockchaincom.py +ccxt/async_support/blofin.py +ccxt/async_support/btcalpha.py +ccxt/async_support/btcbox.py +ccxt/async_support/btcmarkets.py +ccxt/async_support/btcturk.py +ccxt/async_support/bybit.py +ccxt/async_support/cex.py +ccxt/async_support/coinbase.py +ccxt/async_support/coinbaseadvanced.py +ccxt/async_support/coinbaseexchange.py +ccxt/async_support/coinbaseinternational.py +ccxt/async_support/coincatch.py +ccxt/async_support/coincheck.py +ccxt/async_support/coinex.py +ccxt/async_support/coinmate.py +ccxt/async_support/coinmetro.py +ccxt/async_support/coinone.py +ccxt/async_support/coinsph.py +ccxt/async_support/coinspot.py +ccxt/async_support/cryptocom.py +ccxt/async_support/cryptomus.py +ccxt/async_support/deepcoin.py +ccxt/async_support/defx.py +ccxt/async_support/delta.py +ccxt/async_support/deribit.py +ccxt/async_support/derive.py +ccxt/async_support/digifinex.py +ccxt/async_support/exmo.py +ccxt/async_support/fmfwio.py +ccxt/async_support/foxbit.py +ccxt/async_support/gate.py +ccxt/async_support/gateio.py +ccxt/async_support/gemini.py +ccxt/async_support/hashkey.py +ccxt/async_support/hibachi.py +ccxt/async_support/hitbtc.py +ccxt/async_support/hollaex.py +ccxt/async_support/htx.py +ccxt/async_support/huobi.py +ccxt/async_support/hyperliquid.py +ccxt/async_support/independentreserve.py +ccxt/async_support/indodax.py +ccxt/async_support/kraken.py +ccxt/async_support/krakenfutures.py +ccxt/async_support/kucoin.py +ccxt/async_support/kucoinfutures.py +ccxt/async_support/latoken.py +ccxt/async_support/lbank.py +ccxt/async_support/luno.py +ccxt/async_support/mercado.py +ccxt/async_support/mexc.py +ccxt/async_support/modetrade.py +ccxt/async_support/myokx.py +ccxt/async_support/ndax.py +ccxt/async_support/novadax.py +ccxt/async_support/oceanex.py +ccxt/async_support/okx.py +ccxt/async_support/okxus.py +ccxt/async_support/onetrading.py +ccxt/async_support/oxfun.py +ccxt/async_support/p2b.py +ccxt/async_support/paradex.py +ccxt/async_support/paymium.py +ccxt/async_support/phemex.py +ccxt/async_support/poloniex.py +ccxt/async_support/probit.py +ccxt/async_support/timex.py +ccxt/async_support/tokocrypto.py +ccxt/async_support/toobit.py +ccxt/async_support/upbit.py +ccxt/async_support/wavesexchange.py +ccxt/async_support/whitebit.py +ccxt/async_support/woo.py +ccxt/async_support/woofipro.py +ccxt/async_support/xt.py +ccxt/async_support/yobit.py +ccxt/async_support/zaif.py +ccxt/async_support/zonda.py +ccxt/async_support/base/__init__.py +ccxt/async_support/base/exchange.py +ccxt/async_support/base/throttler.py +ccxt/async_support/base/ws/__init__.py +ccxt/async_support/base/ws/cache.py +ccxt/async_support/base/ws/client.py +ccxt/async_support/base/ws/functions.py +ccxt/async_support/base/ws/future.py +ccxt/async_support/base/ws/order_book.py +ccxt/async_support/base/ws/order_book_side.py +ccxt/base/__init__.py +ccxt/base/decimal_to_precision.py +ccxt/base/errors.py +ccxt/base/exchange.py +ccxt/base/precise.py +ccxt/base/types.py +ccxt/pro/__init__.py +ccxt/pro/alpaca.py +ccxt/pro/apex.py +ccxt/pro/arkham.py +ccxt/pro/ascendex.py +ccxt/pro/backpack.py +ccxt/pro/bequant.py +ccxt/pro/binance.py +ccxt/pro/binancecoinm.py +ccxt/pro/binanceus.py +ccxt/pro/binanceusdm.py +ccxt/pro/bingx.py +ccxt/pro/bitfinex.py +ccxt/pro/bitget.py +ccxt/pro/bithumb.py +ccxt/pro/bitmart.py +ccxt/pro/bitmex.py +ccxt/pro/bitopro.py +ccxt/pro/bitrue.py +ccxt/pro/bitstamp.py +ccxt/pro/bittrade.py +ccxt/pro/bitvavo.py +ccxt/pro/blockchaincom.py +ccxt/pro/blofin.py +ccxt/pro/bybit.py +ccxt/pro/cex.py +ccxt/pro/coinbase.py +ccxt/pro/coinbaseadvanced.py +ccxt/pro/coinbaseexchange.py +ccxt/pro/coinbaseinternational.py +ccxt/pro/coincatch.py +ccxt/pro/coincheck.py +ccxt/pro/coinex.py +ccxt/pro/coinone.py +ccxt/pro/cryptocom.py +ccxt/pro/deepcoin.py +ccxt/pro/defx.py +ccxt/pro/deribit.py +ccxt/pro/derive.py +ccxt/pro/exmo.py +ccxt/pro/gate.py +ccxt/pro/gateio.py +ccxt/pro/gemini.py +ccxt/pro/hashkey.py +ccxt/pro/hitbtc.py +ccxt/pro/hollaex.py +ccxt/pro/htx.py +ccxt/pro/huobi.py +ccxt/pro/hyperliquid.py +ccxt/pro/independentreserve.py +ccxt/pro/kraken.py +ccxt/pro/krakenfutures.py +ccxt/pro/kucoin.py +ccxt/pro/kucoinfutures.py +ccxt/pro/lbank.py +ccxt/pro/luno.py +ccxt/pro/mexc.py +ccxt/pro/modetrade.py +ccxt/pro/myokx.py +ccxt/pro/ndax.py +ccxt/pro/okx.py +ccxt/pro/okxus.py +ccxt/pro/onetrading.py +ccxt/pro/oxfun.py +ccxt/pro/p2b.py +ccxt/pro/paradex.py +ccxt/pro/phemex.py +ccxt/pro/poloniex.py +ccxt/pro/probit.py +ccxt/pro/toobit.py +ccxt/pro/upbit.py +ccxt/pro/whitebit.py +ccxt/pro/woo.py +ccxt/pro/woofipro.py +ccxt/pro/xt.py +ccxt/protobuf/__init__.py +ccxt/protobuf/mexc/PrivateAccountV3Api_pb2.py +ccxt/protobuf/mexc/PrivateDealsV3Api_pb2.py +ccxt/protobuf/mexc/PrivateOrdersV3Api_pb2.py +ccxt/protobuf/mexc/PublicAggreBookTickerV3Api_pb2.py +ccxt/protobuf/mexc/PublicAggreDealsV3Api_pb2.py +ccxt/protobuf/mexc/PublicAggreDepthsV3Api_pb2.py +ccxt/protobuf/mexc/PublicBookTickerBatchV3Api_pb2.py +ccxt/protobuf/mexc/PublicBookTickerV3Api_pb2.py +ccxt/protobuf/mexc/PublicDealsV3Api_pb2.py +ccxt/protobuf/mexc/PublicIncreaseDepthsBatchV3Api_pb2.py +ccxt/protobuf/mexc/PublicIncreaseDepthsV3Api_pb2.py +ccxt/protobuf/mexc/PublicLimitDepthsV3Api_pb2.py +ccxt/protobuf/mexc/PublicMiniTickerV3Api_pb2.py +ccxt/protobuf/mexc/PublicMiniTickersV3Api_pb2.py +ccxt/protobuf/mexc/PublicSpotKlineV3Api_pb2.py +ccxt/protobuf/mexc/PushDataV3ApiWrapper_pb2.py +ccxt/protobuf/mexc/__init__.py +ccxt/static_dependencies/__init__.py +ccxt/static_dependencies/ecdsa/__init__.py +ccxt/static_dependencies/ecdsa/_version.py +ccxt/static_dependencies/ecdsa/curves.py +ccxt/static_dependencies/ecdsa/der.py +ccxt/static_dependencies/ecdsa/ecdsa.py +ccxt/static_dependencies/ecdsa/ellipticcurve.py +ccxt/static_dependencies/ecdsa/keys.py +ccxt/static_dependencies/ecdsa/numbertheory.py +ccxt/static_dependencies/ecdsa/rfc6979.py +ccxt/static_dependencies/ecdsa/util.py +ccxt/static_dependencies/ethereum/__init__.py +ccxt/static_dependencies/ethereum/abi/__init__.py +ccxt/static_dependencies/ethereum/abi/abi.py +ccxt/static_dependencies/ethereum/abi/base.py +ccxt/static_dependencies/ethereum/abi/codec.py +ccxt/static_dependencies/ethereum/abi/constants.py +ccxt/static_dependencies/ethereum/abi/decoding.py +ccxt/static_dependencies/ethereum/abi/encoding.py +ccxt/static_dependencies/ethereum/abi/exceptions.py +ccxt/static_dependencies/ethereum/abi/grammar.py +ccxt/static_dependencies/ethereum/abi/packed.py +ccxt/static_dependencies/ethereum/abi/py.typed +ccxt/static_dependencies/ethereum/abi/registry.py +ccxt/static_dependencies/ethereum/abi/tools/__init__.py +ccxt/static_dependencies/ethereum/abi/tools/_strategies.py +ccxt/static_dependencies/ethereum/abi/utils/__init__.py +ccxt/static_dependencies/ethereum/abi/utils/numeric.py +ccxt/static_dependencies/ethereum/abi/utils/padding.py +ccxt/static_dependencies/ethereum/abi/utils/string.py +ccxt/static_dependencies/ethereum/account/__init__.py +ccxt/static_dependencies/ethereum/account/messages.py +ccxt/static_dependencies/ethereum/account/py.typed +ccxt/static_dependencies/ethereum/account/encode_typed_data/__init__.py +ccxt/static_dependencies/ethereum/account/encode_typed_data/encoding_and_hashing.py +ccxt/static_dependencies/ethereum/account/encode_typed_data/helpers.py +ccxt/static_dependencies/ethereum/hexbytes/__init__.py +ccxt/static_dependencies/ethereum/hexbytes/_utils.py +ccxt/static_dependencies/ethereum/hexbytes/main.py +ccxt/static_dependencies/ethereum/hexbytes/py.typed +ccxt/static_dependencies/ethereum/typing/__init__.py +ccxt/static_dependencies/ethereum/typing/abi.py +ccxt/static_dependencies/ethereum/typing/bls.py +ccxt/static_dependencies/ethereum/typing/discovery.py +ccxt/static_dependencies/ethereum/typing/encoding.py +ccxt/static_dependencies/ethereum/typing/enums.py +ccxt/static_dependencies/ethereum/typing/ethpm.py +ccxt/static_dependencies/ethereum/typing/evm.py +ccxt/static_dependencies/ethereum/typing/networks.py +ccxt/static_dependencies/ethereum/typing/py.typed +ccxt/static_dependencies/ethereum/utils/__init__.py +ccxt/static_dependencies/ethereum/utils/abi.py +ccxt/static_dependencies/ethereum/utils/address.py +ccxt/static_dependencies/ethereum/utils/applicators.py +ccxt/static_dependencies/ethereum/utils/conversions.py +ccxt/static_dependencies/ethereum/utils/currency.py +ccxt/static_dependencies/ethereum/utils/debug.py +ccxt/static_dependencies/ethereum/utils/decorators.py +ccxt/static_dependencies/ethereum/utils/encoding.py +ccxt/static_dependencies/ethereum/utils/exceptions.py +ccxt/static_dependencies/ethereum/utils/functional.py +ccxt/static_dependencies/ethereum/utils/hexadecimal.py +ccxt/static_dependencies/ethereum/utils/humanize.py +ccxt/static_dependencies/ethereum/utils/logging.py +ccxt/static_dependencies/ethereum/utils/module_loading.py +ccxt/static_dependencies/ethereum/utils/numeric.py +ccxt/static_dependencies/ethereum/utils/py.typed +ccxt/static_dependencies/ethereum/utils/toolz.py +ccxt/static_dependencies/ethereum/utils/types.py +ccxt/static_dependencies/ethereum/utils/units.py +ccxt/static_dependencies/ethereum/utils/curried/__init__.py +ccxt/static_dependencies/ethereum/utils/typing/__init__.py +ccxt/static_dependencies/ethereum/utils/typing/misc.py +ccxt/static_dependencies/keccak/__init__.py +ccxt/static_dependencies/keccak/keccak.py +ccxt/static_dependencies/lark/__init__.py +ccxt/static_dependencies/lark/ast_utils.py +ccxt/static_dependencies/lark/common.py +ccxt/static_dependencies/lark/exceptions.py +ccxt/static_dependencies/lark/grammar.py +ccxt/static_dependencies/lark/indenter.py +ccxt/static_dependencies/lark/lark.py +ccxt/static_dependencies/lark/lexer.py +ccxt/static_dependencies/lark/load_grammar.py +ccxt/static_dependencies/lark/parse_tree_builder.py +ccxt/static_dependencies/lark/parser_frontends.py +ccxt/static_dependencies/lark/py.typed +ccxt/static_dependencies/lark/reconstruct.py +ccxt/static_dependencies/lark/tree.py +ccxt/static_dependencies/lark/tree_matcher.py +ccxt/static_dependencies/lark/tree_templates.py +ccxt/static_dependencies/lark/utils.py +ccxt/static_dependencies/lark/visitors.py +ccxt/static_dependencies/lark/__pyinstaller/__init__.py +ccxt/static_dependencies/lark/__pyinstaller/hook-lark.py +ccxt/static_dependencies/lark/grammars/__init__.py +ccxt/static_dependencies/lark/parsers/__init__.py +ccxt/static_dependencies/lark/parsers/cyk.py +ccxt/static_dependencies/lark/parsers/earley.py +ccxt/static_dependencies/lark/parsers/earley_common.py +ccxt/static_dependencies/lark/parsers/earley_forest.py +ccxt/static_dependencies/lark/parsers/grammar_analysis.py +ccxt/static_dependencies/lark/parsers/lalr_analysis.py +ccxt/static_dependencies/lark/parsers/lalr_interactive_parser.py +ccxt/static_dependencies/lark/parsers/lalr_parser.py +ccxt/static_dependencies/lark/parsers/lalr_parser_state.py +ccxt/static_dependencies/lark/parsers/xearley.py +ccxt/static_dependencies/lark/tools/__init__.py +ccxt/static_dependencies/lark/tools/nearley.py +ccxt/static_dependencies/lark/tools/serialize.py +ccxt/static_dependencies/lark/tools/standalone.py +ccxt/static_dependencies/marshmallow/__init__.py +ccxt/static_dependencies/marshmallow/base.py +ccxt/static_dependencies/marshmallow/class_registry.py +ccxt/static_dependencies/marshmallow/decorators.py +ccxt/static_dependencies/marshmallow/error_store.py +ccxt/static_dependencies/marshmallow/exceptions.py +ccxt/static_dependencies/marshmallow/fields.py +ccxt/static_dependencies/marshmallow/orderedset.py +ccxt/static_dependencies/marshmallow/py.typed +ccxt/static_dependencies/marshmallow/schema.py +ccxt/static_dependencies/marshmallow/types.py +ccxt/static_dependencies/marshmallow/utils.py +ccxt/static_dependencies/marshmallow/validate.py +ccxt/static_dependencies/marshmallow/warnings.py +ccxt/static_dependencies/marshmallow_dataclass/__init__.py +ccxt/static_dependencies/marshmallow_dataclass/collection_field.py +ccxt/static_dependencies/marshmallow_dataclass/lazy_class_attribute.py +ccxt/static_dependencies/marshmallow_dataclass/mypy.py +ccxt/static_dependencies/marshmallow_dataclass/py.typed +ccxt/static_dependencies/marshmallow_dataclass/typing.py +ccxt/static_dependencies/marshmallow_dataclass/union_field.py +ccxt/static_dependencies/marshmallow_oneofschema/__init__.py +ccxt/static_dependencies/marshmallow_oneofschema/one_of_schema.py +ccxt/static_dependencies/marshmallow_oneofschema/py.typed +ccxt/static_dependencies/msgpack/__init__.py +ccxt/static_dependencies/msgpack/exceptions.py +ccxt/static_dependencies/msgpack/ext.py +ccxt/static_dependencies/msgpack/fallback.py +ccxt/static_dependencies/parsimonious/__init__.py +ccxt/static_dependencies/parsimonious/exceptions.py +ccxt/static_dependencies/parsimonious/expressions.py +ccxt/static_dependencies/parsimonious/grammar.py +ccxt/static_dependencies/parsimonious/nodes.py +ccxt/static_dependencies/parsimonious/utils.py +ccxt/static_dependencies/starknet/__init__.py +ccxt/static_dependencies/starknet/ccxt_utils.py +ccxt/static_dependencies/starknet/common.py +ccxt/static_dependencies/starknet/constants.py +ccxt/static_dependencies/starknet/cairo/__init__.py +ccxt/static_dependencies/starknet/cairo/data_types.py +ccxt/static_dependencies/starknet/cairo/felt.py +ccxt/static_dependencies/starknet/cairo/type_parser.py +ccxt/static_dependencies/starknet/cairo/deprecated_parse/__init__.py +ccxt/static_dependencies/starknet/cairo/deprecated_parse/cairo_types.py +ccxt/static_dependencies/starknet/cairo/deprecated_parse/parser.py +ccxt/static_dependencies/starknet/cairo/deprecated_parse/parser_transformer.py +ccxt/static_dependencies/starknet/cairo/v1/__init__.py +ccxt/static_dependencies/starknet/cairo/v1/type_parser.py +ccxt/static_dependencies/starknet/cairo/v2/__init__.py +ccxt/static_dependencies/starknet/cairo/v2/type_parser.py +ccxt/static_dependencies/starknet/hash/__init__.py +ccxt/static_dependencies/starknet/hash/address.py +ccxt/static_dependencies/starknet/hash/compiled_class_hash_objects.py +ccxt/static_dependencies/starknet/hash/selector.py +ccxt/static_dependencies/starknet/hash/storage.py +ccxt/static_dependencies/starknet/hash/utils.py +ccxt/static_dependencies/starknet/models/__init__.py +ccxt/static_dependencies/starknet/models/typed_data.py +ccxt/static_dependencies/starknet/serialization/__init__.py +ccxt/static_dependencies/starknet/serialization/_calldata_reader.py +ccxt/static_dependencies/starknet/serialization/_context.py +ccxt/static_dependencies/starknet/serialization/errors.py +ccxt/static_dependencies/starknet/serialization/factory.py +ccxt/static_dependencies/starknet/serialization/function_serialization_adapter.py +ccxt/static_dependencies/starknet/serialization/tuple_dataclass.py +ccxt/static_dependencies/starknet/serialization/data_serializers/__init__.py +ccxt/static_dependencies/starknet/serialization/data_serializers/_common.py +ccxt/static_dependencies/starknet/serialization/data_serializers/array_serializer.py +ccxt/static_dependencies/starknet/serialization/data_serializers/bool_serializer.py +ccxt/static_dependencies/starknet/serialization/data_serializers/byte_array_serializer.py +ccxt/static_dependencies/starknet/serialization/data_serializers/cairo_data_serializer.py +ccxt/static_dependencies/starknet/serialization/data_serializers/enum_serializer.py +ccxt/static_dependencies/starknet/serialization/data_serializers/felt_serializer.py +ccxt/static_dependencies/starknet/serialization/data_serializers/named_tuple_serializer.py +ccxt/static_dependencies/starknet/serialization/data_serializers/option_serializer.py +ccxt/static_dependencies/starknet/serialization/data_serializers/output_serializer.py +ccxt/static_dependencies/starknet/serialization/data_serializers/payload_serializer.py +ccxt/static_dependencies/starknet/serialization/data_serializers/struct_serializer.py +ccxt/static_dependencies/starknet/serialization/data_serializers/tuple_serializer.py +ccxt/static_dependencies/starknet/serialization/data_serializers/uint256_serializer.py +ccxt/static_dependencies/starknet/serialization/data_serializers/uint_serializer.py +ccxt/static_dependencies/starknet/serialization/data_serializers/unit_serializer.py +ccxt/static_dependencies/starknet/utils/__init__.py +ccxt/static_dependencies/starknet/utils/constructor_args_translator.py +ccxt/static_dependencies/starknet/utils/iterable.py +ccxt/static_dependencies/starknet/utils/schema.py +ccxt/static_dependencies/starknet/utils/typed_data.py +ccxt/static_dependencies/starkware/__init__.py +ccxt/static_dependencies/starkware/crypto/__init__.py +ccxt/static_dependencies/starkware/crypto/fast_pedersen_hash.py +ccxt/static_dependencies/starkware/crypto/math_utils.py +ccxt/static_dependencies/starkware/crypto/signature.py +ccxt/static_dependencies/starkware/crypto/utils.py +ccxt/static_dependencies/sympy/__init__.py +ccxt/static_dependencies/sympy/core/__init__.py +ccxt/static_dependencies/sympy/core/intfunc.py +ccxt/static_dependencies/sympy/external/__init__.py +ccxt/static_dependencies/sympy/external/gmpy.py +ccxt/static_dependencies/sympy/external/importtools.py +ccxt/static_dependencies/sympy/external/ntheory.py +ccxt/static_dependencies/sympy/external/pythonmpq.py +ccxt/static_dependencies/toolz/__init__.py +ccxt/static_dependencies/toolz/_signatures.py +ccxt/static_dependencies/toolz/_version.py +ccxt/static_dependencies/toolz/compatibility.py +ccxt/static_dependencies/toolz/dicttoolz.py +ccxt/static_dependencies/toolz/functoolz.py +ccxt/static_dependencies/toolz/itertoolz.py +ccxt/static_dependencies/toolz/recipes.py +ccxt/static_dependencies/toolz/utils.py +ccxt/static_dependencies/toolz/curried/__init__.py +ccxt/static_dependencies/toolz/curried/exceptions.py +ccxt/static_dependencies/toolz/curried/operator.py +ccxt/static_dependencies/typing_inspect/__init__.py +ccxt/static_dependencies/typing_inspect/typing_inspect.py +ccxt/test/__init__.py +ccxt/test/tests_async.py +ccxt/test/tests_helpers.py +ccxt/test/tests_init.py +ccxt/test/tests_sync.py \ No newline at end of file diff --git a/ccxt.egg-info/dependency_links.txt b/ccxt.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/ccxt.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/ccxt.egg-info/requires.txt b/ccxt.egg-info/requires.txt new file mode 100644 index 0000000..3a4705d --- /dev/null +++ b/ccxt.egg-info/requires.txt @@ -0,0 +1,20 @@ +setuptools>=60.9.0 +certifi>=2018.1.18 +requests>=2.18.4 +cryptography>=2.6.1 +typing_extensions>=4.4.0 + +[:python_version>="3.5.2"] +aiohttp>=3.10.11 +aiodns>=1.1.1 +yarl>=1.7.2 + +[:python_version>="3.9" and python_version<="3.13"] +coincurve==21.0.0 + +[qa] +ruff==0.0.292 +tox>=4.8.0 + +[type] +mypy==1.6.1 diff --git a/ccxt.egg-info/top_level.txt b/ccxt.egg-info/top_level.txt new file mode 100644 index 0000000..be8c2e9 --- /dev/null +++ b/ccxt.egg-info/top_level.txt @@ -0,0 +1 @@ +ccxt diff --git a/ccxt/__init__.py b/ccxt/__init__.py new file mode 100644 index 0000000..f412c96 --- /dev/null +++ b/ccxt/__init__.py @@ -0,0 +1,314 @@ +# -*- coding: utf-8 -*- + +"""CCXT: CryptoCurrency eXchange Trading Library""" + +# MIT License +# Copyright (c) 2017 Igor Kroitor +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +# ---------------------------------------------------------------------------- + +__version__ = '4.5.18' + +# ---------------------------------------------------------------------------- + +from ccxt.base.exchange import Exchange # noqa: F401 +from ccxt.base.precise import Precise # noqa: F401 + +from ccxt.base.decimal_to_precision import decimal_to_precision # noqa: F401 +from ccxt.base.decimal_to_precision import TRUNCATE # noqa: F401 +from ccxt.base.decimal_to_precision import ROUND # noqa: F401 +from ccxt.base.decimal_to_precision import ROUND_UP # noqa: F401 +from ccxt.base.decimal_to_precision import ROUND_DOWN # noqa: F401 +from ccxt.base.decimal_to_precision import DECIMAL_PLACES # noqa: F401 +from ccxt.base.decimal_to_precision import SIGNIFICANT_DIGITS # noqa: F401 +from ccxt.base.decimal_to_precision import TICK_SIZE # noqa: F401 +from ccxt.base.decimal_to_precision import NO_PADDING # noqa: F401 +from ccxt.base.decimal_to_precision import PAD_WITH_ZERO # noqa: F401 + +from ccxt.base import errors +from ccxt.base.errors import BaseError # noqa: F401 +from ccxt.base.errors import ExchangeError # noqa: F401 +from ccxt.base.errors import AuthenticationError # noqa: F401 +from ccxt.base.errors import PermissionDenied # noqa: F401 +from ccxt.base.errors import AccountNotEnabled # noqa: F401 +from ccxt.base.errors import AccountSuspended # noqa: F401 +from ccxt.base.errors import ArgumentsRequired # noqa: F401 +from ccxt.base.errors import BadRequest # noqa: F401 +from ccxt.base.errors import BadSymbol # noqa: F401 +from ccxt.base.errors import OperationRejected # noqa: F401 +from ccxt.base.errors import NoChange # noqa: F401 +from ccxt.base.errors import MarginModeAlreadySet # noqa: F401 +from ccxt.base.errors import MarketClosed # noqa: F401 +from ccxt.base.errors import ManualInteractionNeeded # noqa: F401 +from ccxt.base.errors import RestrictedLocation # noqa: F401 +from ccxt.base.errors import InsufficientFunds # noqa: F401 +from ccxt.base.errors import InvalidAddress # noqa: F401 +from ccxt.base.errors import AddressPending # noqa: F401 +from ccxt.base.errors import InvalidOrder # noqa: F401 +from ccxt.base.errors import OrderNotFound # noqa: F401 +from ccxt.base.errors import OrderNotCached # noqa: F401 +from ccxt.base.errors import OrderImmediatelyFillable # noqa: F401 +from ccxt.base.errors import OrderNotFillable # noqa: F401 +from ccxt.base.errors import DuplicateOrderId # noqa: F401 +from ccxt.base.errors import ContractUnavailable # noqa: F401 +from ccxt.base.errors import NotSupported # noqa: F401 +from ccxt.base.errors import InvalidProxySettings # noqa: F401 +from ccxt.base.errors import ExchangeClosedByUser # noqa: F401 +from ccxt.base.errors import OperationFailed # noqa: F401 +from ccxt.base.errors import NetworkError # noqa: F401 +from ccxt.base.errors import DDoSProtection # noqa: F401 +from ccxt.base.errors import RateLimitExceeded # noqa: F401 +from ccxt.base.errors import ExchangeNotAvailable # noqa: F401 +from ccxt.base.errors import OnMaintenance # noqa: F401 +from ccxt.base.errors import InvalidNonce # noqa: F401 +from ccxt.base.errors import ChecksumError # noqa: F401 +from ccxt.base.errors import RequestTimeout # noqa: F401 +from ccxt.base.errors import BadResponse # noqa: F401 +from ccxt.base.errors import NullResponse # noqa: F401 +from ccxt.base.errors import CancelPending # noqa: F401 +from ccxt.base.errors import UnsubscribeError # noqa: F401 +from ccxt.base.errors import error_hierarchy # noqa: F401 + +from ccxt.alpaca import alpaca # noqa: F401 +from ccxt.apex import apex # noqa: F401 +from ccxt.arkham import arkham # noqa: F401 +from ccxt.ascendex import ascendex # noqa: F401 +from ccxt.backpack import backpack # noqa: F401 +from ccxt.bequant import bequant # noqa: F401 +from ccxt.bigone import bigone # noqa: F401 +from ccxt.binance import binance # noqa: F401 +from ccxt.binancecoinm import binancecoinm # noqa: F401 +from ccxt.binanceus import binanceus # noqa: F401 +from ccxt.binanceusdm import binanceusdm # noqa: F401 +from ccxt.bingx import bingx # noqa: F401 +from ccxt.bit2c import bit2c # noqa: F401 +from ccxt.bitbank import bitbank # noqa: F401 +from ccxt.bitbns import bitbns # noqa: F401 +from ccxt.bitfinex import bitfinex # noqa: F401 +from ccxt.bitflyer import bitflyer # noqa: F401 +from ccxt.bitget import bitget # noqa: F401 +from ccxt.bithumb import bithumb # noqa: F401 +from ccxt.bitmart import bitmart # noqa: F401 +from ccxt.bitmex import bitmex # noqa: F401 +from ccxt.bitopro import bitopro # noqa: F401 +from ccxt.bitrue import bitrue # noqa: F401 +from ccxt.bitso import bitso # noqa: F401 +from ccxt.bitstamp import bitstamp # noqa: F401 +from ccxt.bitteam import bitteam # noqa: F401 +from ccxt.bittrade import bittrade # noqa: F401 +from ccxt.bitvavo import bitvavo # noqa: F401 +from ccxt.blockchaincom import blockchaincom # noqa: F401 +from ccxt.blofin import blofin # noqa: F401 +from ccxt.btcalpha import btcalpha # noqa: F401 +from ccxt.btcbox import btcbox # noqa: F401 +from ccxt.btcmarkets import btcmarkets # noqa: F401 +from ccxt.btcturk import btcturk # noqa: F401 +from ccxt.bybit import bybit # noqa: F401 +from ccxt.cex import cex # noqa: F401 +from ccxt.coinbase import coinbase # noqa: F401 +from ccxt.coinbaseadvanced import coinbaseadvanced # noqa: F401 +from ccxt.coinbaseexchange import coinbaseexchange # noqa: F401 +from ccxt.coinbaseinternational import coinbaseinternational # noqa: F401 +from ccxt.coincatch import coincatch # noqa: F401 +from ccxt.coincheck import coincheck # noqa: F401 +from ccxt.coinex import coinex # noqa: F401 +from ccxt.coinmate import coinmate # noqa: F401 +from ccxt.coinmetro import coinmetro # noqa: F401 +from ccxt.coinone import coinone # noqa: F401 +from ccxt.coinsph import coinsph # noqa: F401 +from ccxt.coinspot import coinspot # noqa: F401 +from ccxt.cryptocom import cryptocom # noqa: F401 +from ccxt.cryptomus import cryptomus # noqa: F401 +from ccxt.deepcoin import deepcoin # noqa: F401 +from ccxt.defx import defx # noqa: F401 +from ccxt.delta import delta # noqa: F401 +from ccxt.deribit import deribit # noqa: F401 +from ccxt.derive import derive # noqa: F401 +from ccxt.digifinex import digifinex # noqa: F401 +from ccxt.exmo import exmo # noqa: F401 +from ccxt.fmfwio import fmfwio # noqa: F401 +from ccxt.foxbit import foxbit # noqa: F401 +from ccxt.gate import gate # noqa: F401 +from ccxt.gateio import gateio # noqa: F401 +from ccxt.gemini import gemini # noqa: F401 +from ccxt.hashkey import hashkey # noqa: F401 +from ccxt.hibachi import hibachi # noqa: F401 +from ccxt.hitbtc import hitbtc # noqa: F401 +from ccxt.hollaex import hollaex # noqa: F401 +from ccxt.htx import htx # noqa: F401 +from ccxt.huobi import huobi # noqa: F401 +from ccxt.hyperliquid import hyperliquid # noqa: F401 +from ccxt.independentreserve import independentreserve # noqa: F401 +from ccxt.indodax import indodax # noqa: F401 +from ccxt.kraken import kraken # noqa: F401 +from ccxt.krakenfutures import krakenfutures # noqa: F401 +from ccxt.kucoin import kucoin # noqa: F401 +from ccxt.kucoinfutures import kucoinfutures # noqa: F401 +from ccxt.latoken import latoken # noqa: F401 +from ccxt.lbank import lbank # noqa: F401 +from ccxt.luno import luno # noqa: F401 +from ccxt.mercado import mercado # noqa: F401 +from ccxt.mexc import mexc # noqa: F401 +from ccxt.modetrade import modetrade # noqa: F401 +from ccxt.myokx import myokx # noqa: F401 +from ccxt.ndax import ndax # noqa: F401 +from ccxt.novadax import novadax # noqa: F401 +from ccxt.oceanex import oceanex # noqa: F401 +from ccxt.okx import okx # noqa: F401 +from ccxt.okxus import okxus # noqa: F401 +from ccxt.onetrading import onetrading # noqa: F401 +from ccxt.oxfun import oxfun # noqa: F401 +from ccxt.p2b import p2b # noqa: F401 +from ccxt.paradex import paradex # noqa: F401 +from ccxt.paymium import paymium # noqa: F401 +from ccxt.phemex import phemex # noqa: F401 +from ccxt.poloniex import poloniex # noqa: F401 +from ccxt.probit import probit # noqa: F401 +from ccxt.timex import timex # noqa: F401 +from ccxt.tokocrypto import tokocrypto # noqa: F401 +from ccxt.toobit import toobit # noqa: F401 +from ccxt.upbit import upbit # noqa: F401 +from ccxt.wavesexchange import wavesexchange # noqa: F401 +from ccxt.whitebit import whitebit # noqa: F401 +from ccxt.woo import woo # noqa: F401 +from ccxt.woofipro import woofipro # noqa: F401 +from ccxt.xt import xt # noqa: F401 +from ccxt.yobit import yobit # noqa: F401 +from ccxt.zaif import zaif # noqa: F401 +from ccxt.zonda import zonda # noqa: F401 +from ccxt.mt5 import mt5 # noqa: F401 + +exchanges = [ + 'alpaca', + 'apex', + 'arkham', + 'ascendex', + 'backpack', + 'bequant', + 'bigone', + 'binance', + 'binancecoinm', + 'binanceus', + 'binanceusdm', + 'bingx', + 'bit2c', + 'bitbank', + 'bitbns', + 'bitfinex', + 'bitflyer', + 'bitget', + 'bithumb', + 'bitmart', + 'bitmex', + 'bitopro', + 'bitrue', + 'bitso', + 'bitstamp', + 'bitteam', + 'bittrade', + 'bitvavo', + 'blockchaincom', + 'blofin', + 'btcalpha', + 'btcbox', + 'btcmarkets', + 'btcturk', + 'bybit', + 'cex', + 'coinbase', + 'coinbaseadvanced', + 'coinbaseexchange', + 'coinbaseinternational', + 'coincatch', + 'coincheck', + 'coinex', + 'coinmate', + 'coinmetro', + 'coinone', + 'coinsph', + 'coinspot', + 'cryptocom', + 'cryptomus', + 'deepcoin', + 'defx', + 'delta', + 'deribit', + 'derive', + 'digifinex', + 'exmo', + 'fmfwio', + 'foxbit', + 'gate', + 'gateio', + 'gemini', + 'hashkey', + 'hibachi', + 'hitbtc', + 'hollaex', + 'htx', + 'huobi', + 'hyperliquid', + 'independentreserve', + 'indodax', + 'kraken', + 'krakenfutures', + 'kucoin', + 'kucoinfutures', + 'latoken', + 'lbank', + 'luno', + 'mercado', + 'mexc', + 'modetrade', + 'myokx', + 'ndax', + 'novadax', + 'oceanex', + 'okx', + 'okxus', + 'onetrading', + 'oxfun', + 'p2b', + 'paradex', + 'paymium', + 'phemex', + 'poloniex', + 'probit', + 'timex', + 'tokocrypto', + 'toobit', + 'upbit', + 'wavesexchange', + 'whitebit', + 'woo', + 'woofipro', + 'xt', + 'yobit', + 'zaif', + 'zonda', + 'mt5', +] + +base = [ + 'Exchange', + 'Precise', + 'exchanges', + 'decimal_to_precision', +] + +__all__ = base + errors.__all__ + exchanges diff --git a/ccxt/__pycache__/__init__.cpython-311.pyc b/ccxt/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..919ffe5 Binary files /dev/null and b/ccxt/__pycache__/__init__.cpython-311.pyc differ diff --git a/ccxt/__pycache__/__init__.cpython-36.pyc b/ccxt/__pycache__/__init__.cpython-36.pyc new file mode 100644 index 0000000..1adddbb Binary files /dev/null and b/ccxt/__pycache__/__init__.cpython-36.pyc differ diff --git a/ccxt/__pycache__/alpaca.cpython-311.pyc b/ccxt/__pycache__/alpaca.cpython-311.pyc new file mode 100644 index 0000000..7fb8860 Binary files /dev/null and b/ccxt/__pycache__/alpaca.cpython-311.pyc differ diff --git a/ccxt/__pycache__/apex.cpython-311.pyc b/ccxt/__pycache__/apex.cpython-311.pyc new file mode 100644 index 0000000..00db162 Binary files /dev/null and b/ccxt/__pycache__/apex.cpython-311.pyc differ diff --git a/ccxt/__pycache__/arkham.cpython-311.pyc b/ccxt/__pycache__/arkham.cpython-311.pyc new file mode 100644 index 0000000..d6be0ce Binary files /dev/null and b/ccxt/__pycache__/arkham.cpython-311.pyc differ diff --git a/ccxt/__pycache__/ascendex.cpython-311.pyc b/ccxt/__pycache__/ascendex.cpython-311.pyc new file mode 100644 index 0000000..2c50e78 Binary files /dev/null and b/ccxt/__pycache__/ascendex.cpython-311.pyc differ diff --git a/ccxt/__pycache__/backpack.cpython-311.pyc b/ccxt/__pycache__/backpack.cpython-311.pyc new file mode 100644 index 0000000..99efa79 Binary files /dev/null and b/ccxt/__pycache__/backpack.cpython-311.pyc differ diff --git a/ccxt/__pycache__/bequant.cpython-311.pyc b/ccxt/__pycache__/bequant.cpython-311.pyc new file mode 100644 index 0000000..be3bac6 Binary files /dev/null and b/ccxt/__pycache__/bequant.cpython-311.pyc differ diff --git a/ccxt/__pycache__/bigone.cpython-311.pyc b/ccxt/__pycache__/bigone.cpython-311.pyc new file mode 100644 index 0000000..858e9a4 Binary files /dev/null and b/ccxt/__pycache__/bigone.cpython-311.pyc differ diff --git a/ccxt/__pycache__/binance.cpython-311.pyc b/ccxt/__pycache__/binance.cpython-311.pyc new file mode 100644 index 0000000..7491c30 Binary files /dev/null and b/ccxt/__pycache__/binance.cpython-311.pyc differ diff --git a/ccxt/__pycache__/binancecoinm.cpython-311.pyc b/ccxt/__pycache__/binancecoinm.cpython-311.pyc new file mode 100644 index 0000000..5a2ce02 Binary files /dev/null and b/ccxt/__pycache__/binancecoinm.cpython-311.pyc differ diff --git a/ccxt/__pycache__/binanceus.cpython-311.pyc b/ccxt/__pycache__/binanceus.cpython-311.pyc new file mode 100644 index 0000000..31b825c Binary files /dev/null and b/ccxt/__pycache__/binanceus.cpython-311.pyc differ diff --git a/ccxt/__pycache__/binanceusdm.cpython-311.pyc b/ccxt/__pycache__/binanceusdm.cpython-311.pyc new file mode 100644 index 0000000..1dde274 Binary files /dev/null and b/ccxt/__pycache__/binanceusdm.cpython-311.pyc differ diff --git a/ccxt/__pycache__/bingx.cpython-311.pyc b/ccxt/__pycache__/bingx.cpython-311.pyc new file mode 100644 index 0000000..5975a70 Binary files /dev/null and b/ccxt/__pycache__/bingx.cpython-311.pyc differ diff --git a/ccxt/__pycache__/bit2c.cpython-311.pyc b/ccxt/__pycache__/bit2c.cpython-311.pyc new file mode 100644 index 0000000..474abde Binary files /dev/null and b/ccxt/__pycache__/bit2c.cpython-311.pyc differ diff --git a/ccxt/__pycache__/bitbank.cpython-311.pyc b/ccxt/__pycache__/bitbank.cpython-311.pyc new file mode 100644 index 0000000..4841e7f Binary files /dev/null and b/ccxt/__pycache__/bitbank.cpython-311.pyc differ diff --git a/ccxt/__pycache__/bitbns.cpython-311.pyc b/ccxt/__pycache__/bitbns.cpython-311.pyc new file mode 100644 index 0000000..6e7a985 Binary files /dev/null and b/ccxt/__pycache__/bitbns.cpython-311.pyc differ diff --git a/ccxt/__pycache__/bitfinex.cpython-311.pyc b/ccxt/__pycache__/bitfinex.cpython-311.pyc new file mode 100644 index 0000000..beecb38 Binary files /dev/null and b/ccxt/__pycache__/bitfinex.cpython-311.pyc differ diff --git a/ccxt/__pycache__/bitflyer.cpython-311.pyc b/ccxt/__pycache__/bitflyer.cpython-311.pyc new file mode 100644 index 0000000..2830220 Binary files /dev/null and b/ccxt/__pycache__/bitflyer.cpython-311.pyc differ diff --git a/ccxt/__pycache__/bitget.cpython-311.pyc b/ccxt/__pycache__/bitget.cpython-311.pyc new file mode 100644 index 0000000..4674dec Binary files /dev/null and b/ccxt/__pycache__/bitget.cpython-311.pyc differ diff --git a/ccxt/__pycache__/bithumb.cpython-311.pyc b/ccxt/__pycache__/bithumb.cpython-311.pyc new file mode 100644 index 0000000..61004cf Binary files /dev/null and b/ccxt/__pycache__/bithumb.cpython-311.pyc differ diff --git a/ccxt/__pycache__/bitmart.cpython-311.pyc b/ccxt/__pycache__/bitmart.cpython-311.pyc new file mode 100644 index 0000000..bd4a968 Binary files /dev/null and b/ccxt/__pycache__/bitmart.cpython-311.pyc differ diff --git a/ccxt/__pycache__/bitmex.cpython-311.pyc b/ccxt/__pycache__/bitmex.cpython-311.pyc new file mode 100644 index 0000000..6dac7fa Binary files /dev/null and b/ccxt/__pycache__/bitmex.cpython-311.pyc differ diff --git a/ccxt/__pycache__/bitopro.cpython-311.pyc b/ccxt/__pycache__/bitopro.cpython-311.pyc new file mode 100644 index 0000000..9b256d6 Binary files /dev/null and b/ccxt/__pycache__/bitopro.cpython-311.pyc differ diff --git a/ccxt/__pycache__/bitrue.cpython-311.pyc b/ccxt/__pycache__/bitrue.cpython-311.pyc new file mode 100644 index 0000000..8957eef Binary files /dev/null and b/ccxt/__pycache__/bitrue.cpython-311.pyc differ diff --git a/ccxt/__pycache__/bitso.cpython-311.pyc b/ccxt/__pycache__/bitso.cpython-311.pyc new file mode 100644 index 0000000..190e04b Binary files /dev/null and b/ccxt/__pycache__/bitso.cpython-311.pyc differ diff --git a/ccxt/__pycache__/bitstamp.cpython-311.pyc b/ccxt/__pycache__/bitstamp.cpython-311.pyc new file mode 100644 index 0000000..8ad17fe Binary files /dev/null and b/ccxt/__pycache__/bitstamp.cpython-311.pyc differ diff --git a/ccxt/__pycache__/bitteam.cpython-311.pyc b/ccxt/__pycache__/bitteam.cpython-311.pyc new file mode 100644 index 0000000..402d3b2 Binary files /dev/null and b/ccxt/__pycache__/bitteam.cpython-311.pyc differ diff --git a/ccxt/__pycache__/bittrade.cpython-311.pyc b/ccxt/__pycache__/bittrade.cpython-311.pyc new file mode 100644 index 0000000..0492722 Binary files /dev/null and b/ccxt/__pycache__/bittrade.cpython-311.pyc differ diff --git a/ccxt/__pycache__/bitvavo.cpython-311.pyc b/ccxt/__pycache__/bitvavo.cpython-311.pyc new file mode 100644 index 0000000..49f61e1 Binary files /dev/null and b/ccxt/__pycache__/bitvavo.cpython-311.pyc differ diff --git a/ccxt/__pycache__/blockchaincom.cpython-311.pyc b/ccxt/__pycache__/blockchaincom.cpython-311.pyc new file mode 100644 index 0000000..2fb450f Binary files /dev/null and b/ccxt/__pycache__/blockchaincom.cpython-311.pyc differ diff --git a/ccxt/__pycache__/blofin.cpython-311.pyc b/ccxt/__pycache__/blofin.cpython-311.pyc new file mode 100644 index 0000000..37c9e0f Binary files /dev/null and b/ccxt/__pycache__/blofin.cpython-311.pyc differ diff --git a/ccxt/__pycache__/btcalpha.cpython-311.pyc b/ccxt/__pycache__/btcalpha.cpython-311.pyc new file mode 100644 index 0000000..4b3d80b Binary files /dev/null and b/ccxt/__pycache__/btcalpha.cpython-311.pyc differ diff --git a/ccxt/__pycache__/btcbox.cpython-311.pyc b/ccxt/__pycache__/btcbox.cpython-311.pyc new file mode 100644 index 0000000..d243103 Binary files /dev/null and b/ccxt/__pycache__/btcbox.cpython-311.pyc differ diff --git a/ccxt/__pycache__/btcmarkets.cpython-311.pyc b/ccxt/__pycache__/btcmarkets.cpython-311.pyc new file mode 100644 index 0000000..4d45754 Binary files /dev/null and b/ccxt/__pycache__/btcmarkets.cpython-311.pyc differ diff --git a/ccxt/__pycache__/btcturk.cpython-311.pyc b/ccxt/__pycache__/btcturk.cpython-311.pyc new file mode 100644 index 0000000..ba680ca Binary files /dev/null and b/ccxt/__pycache__/btcturk.cpython-311.pyc differ diff --git a/ccxt/__pycache__/bybit.cpython-311.pyc b/ccxt/__pycache__/bybit.cpython-311.pyc new file mode 100644 index 0000000..4a74472 Binary files /dev/null and b/ccxt/__pycache__/bybit.cpython-311.pyc differ diff --git a/ccxt/__pycache__/cex.cpython-311.pyc b/ccxt/__pycache__/cex.cpython-311.pyc new file mode 100644 index 0000000..f87cf26 Binary files /dev/null and b/ccxt/__pycache__/cex.cpython-311.pyc differ diff --git a/ccxt/__pycache__/coinbase.cpython-311.pyc b/ccxt/__pycache__/coinbase.cpython-311.pyc new file mode 100644 index 0000000..eadb260 Binary files /dev/null and b/ccxt/__pycache__/coinbase.cpython-311.pyc differ diff --git a/ccxt/__pycache__/coinbaseadvanced.cpython-311.pyc b/ccxt/__pycache__/coinbaseadvanced.cpython-311.pyc new file mode 100644 index 0000000..86d5a40 Binary files /dev/null and b/ccxt/__pycache__/coinbaseadvanced.cpython-311.pyc differ diff --git a/ccxt/__pycache__/coinbaseexchange.cpython-311.pyc b/ccxt/__pycache__/coinbaseexchange.cpython-311.pyc new file mode 100644 index 0000000..9faa46a Binary files /dev/null and b/ccxt/__pycache__/coinbaseexchange.cpython-311.pyc differ diff --git a/ccxt/__pycache__/coinbaseinternational.cpython-311.pyc b/ccxt/__pycache__/coinbaseinternational.cpython-311.pyc new file mode 100644 index 0000000..b20ce67 Binary files /dev/null and b/ccxt/__pycache__/coinbaseinternational.cpython-311.pyc differ diff --git a/ccxt/__pycache__/coincatch.cpython-311.pyc b/ccxt/__pycache__/coincatch.cpython-311.pyc new file mode 100644 index 0000000..85b2ab8 Binary files /dev/null and b/ccxt/__pycache__/coincatch.cpython-311.pyc differ diff --git a/ccxt/__pycache__/coincheck.cpython-311.pyc b/ccxt/__pycache__/coincheck.cpython-311.pyc new file mode 100644 index 0000000..32a7233 Binary files /dev/null and b/ccxt/__pycache__/coincheck.cpython-311.pyc differ diff --git a/ccxt/__pycache__/coinex.cpython-311.pyc b/ccxt/__pycache__/coinex.cpython-311.pyc new file mode 100644 index 0000000..2b3bf96 Binary files /dev/null and b/ccxt/__pycache__/coinex.cpython-311.pyc differ diff --git a/ccxt/__pycache__/coinmate.cpython-311.pyc b/ccxt/__pycache__/coinmate.cpython-311.pyc new file mode 100644 index 0000000..be096e8 Binary files /dev/null and b/ccxt/__pycache__/coinmate.cpython-311.pyc differ diff --git a/ccxt/__pycache__/coinmetro.cpython-311.pyc b/ccxt/__pycache__/coinmetro.cpython-311.pyc new file mode 100644 index 0000000..9a8ed85 Binary files /dev/null and b/ccxt/__pycache__/coinmetro.cpython-311.pyc differ diff --git a/ccxt/__pycache__/coinone.cpython-311.pyc b/ccxt/__pycache__/coinone.cpython-311.pyc new file mode 100644 index 0000000..7b8eb4c Binary files /dev/null and b/ccxt/__pycache__/coinone.cpython-311.pyc differ diff --git a/ccxt/__pycache__/coinsph.cpython-311.pyc b/ccxt/__pycache__/coinsph.cpython-311.pyc new file mode 100644 index 0000000..48f450e Binary files /dev/null and b/ccxt/__pycache__/coinsph.cpython-311.pyc differ diff --git a/ccxt/__pycache__/coinspot.cpython-311.pyc b/ccxt/__pycache__/coinspot.cpython-311.pyc new file mode 100644 index 0000000..e6539db Binary files /dev/null and b/ccxt/__pycache__/coinspot.cpython-311.pyc differ diff --git a/ccxt/__pycache__/cryptocom.cpython-311.pyc b/ccxt/__pycache__/cryptocom.cpython-311.pyc new file mode 100644 index 0000000..876cd38 Binary files /dev/null and b/ccxt/__pycache__/cryptocom.cpython-311.pyc differ diff --git a/ccxt/__pycache__/cryptomus.cpython-311.pyc b/ccxt/__pycache__/cryptomus.cpython-311.pyc new file mode 100644 index 0000000..e593b78 Binary files /dev/null and b/ccxt/__pycache__/cryptomus.cpython-311.pyc differ diff --git a/ccxt/__pycache__/deepcoin.cpython-311.pyc b/ccxt/__pycache__/deepcoin.cpython-311.pyc new file mode 100644 index 0000000..62e3484 Binary files /dev/null and b/ccxt/__pycache__/deepcoin.cpython-311.pyc differ diff --git a/ccxt/__pycache__/defx.cpython-311.pyc b/ccxt/__pycache__/defx.cpython-311.pyc new file mode 100644 index 0000000..301d091 Binary files /dev/null and b/ccxt/__pycache__/defx.cpython-311.pyc differ diff --git a/ccxt/__pycache__/delta.cpython-311.pyc b/ccxt/__pycache__/delta.cpython-311.pyc new file mode 100644 index 0000000..ca1b9ce Binary files /dev/null and b/ccxt/__pycache__/delta.cpython-311.pyc differ diff --git a/ccxt/__pycache__/deribit.cpython-311.pyc b/ccxt/__pycache__/deribit.cpython-311.pyc new file mode 100644 index 0000000..7c8f2dc Binary files /dev/null and b/ccxt/__pycache__/deribit.cpython-311.pyc differ diff --git a/ccxt/__pycache__/derive.cpython-311.pyc b/ccxt/__pycache__/derive.cpython-311.pyc new file mode 100644 index 0000000..187b11a Binary files /dev/null and b/ccxt/__pycache__/derive.cpython-311.pyc differ diff --git a/ccxt/__pycache__/digifinex.cpython-311.pyc b/ccxt/__pycache__/digifinex.cpython-311.pyc new file mode 100644 index 0000000..b0ea7be Binary files /dev/null and b/ccxt/__pycache__/digifinex.cpython-311.pyc differ diff --git a/ccxt/__pycache__/exmo.cpython-311.pyc b/ccxt/__pycache__/exmo.cpython-311.pyc new file mode 100644 index 0000000..8656611 Binary files /dev/null and b/ccxt/__pycache__/exmo.cpython-311.pyc differ diff --git a/ccxt/__pycache__/fmfwio.cpython-311.pyc b/ccxt/__pycache__/fmfwio.cpython-311.pyc new file mode 100644 index 0000000..635df85 Binary files /dev/null and b/ccxt/__pycache__/fmfwio.cpython-311.pyc differ diff --git a/ccxt/__pycache__/foxbit.cpython-311.pyc b/ccxt/__pycache__/foxbit.cpython-311.pyc new file mode 100644 index 0000000..8bf0abf Binary files /dev/null and b/ccxt/__pycache__/foxbit.cpython-311.pyc differ diff --git a/ccxt/__pycache__/gate.cpython-311.pyc b/ccxt/__pycache__/gate.cpython-311.pyc new file mode 100644 index 0000000..d28eebb Binary files /dev/null and b/ccxt/__pycache__/gate.cpython-311.pyc differ diff --git a/ccxt/__pycache__/gateio.cpython-311.pyc b/ccxt/__pycache__/gateio.cpython-311.pyc new file mode 100644 index 0000000..d133504 Binary files /dev/null and b/ccxt/__pycache__/gateio.cpython-311.pyc differ diff --git a/ccxt/__pycache__/gemini.cpython-311.pyc b/ccxt/__pycache__/gemini.cpython-311.pyc new file mode 100644 index 0000000..b31cbfa Binary files /dev/null and b/ccxt/__pycache__/gemini.cpython-311.pyc differ diff --git a/ccxt/__pycache__/hashkey.cpython-311.pyc b/ccxt/__pycache__/hashkey.cpython-311.pyc new file mode 100644 index 0000000..2efbcbb Binary files /dev/null and b/ccxt/__pycache__/hashkey.cpython-311.pyc differ diff --git a/ccxt/__pycache__/hibachi.cpython-311.pyc b/ccxt/__pycache__/hibachi.cpython-311.pyc new file mode 100644 index 0000000..4bad27a Binary files /dev/null and b/ccxt/__pycache__/hibachi.cpython-311.pyc differ diff --git a/ccxt/__pycache__/hitbtc.cpython-311.pyc b/ccxt/__pycache__/hitbtc.cpython-311.pyc new file mode 100644 index 0000000..c035d9f Binary files /dev/null and b/ccxt/__pycache__/hitbtc.cpython-311.pyc differ diff --git a/ccxt/__pycache__/hollaex.cpython-311.pyc b/ccxt/__pycache__/hollaex.cpython-311.pyc new file mode 100644 index 0000000..5da44d3 Binary files /dev/null and b/ccxt/__pycache__/hollaex.cpython-311.pyc differ diff --git a/ccxt/__pycache__/htx.cpython-311.pyc b/ccxt/__pycache__/htx.cpython-311.pyc new file mode 100644 index 0000000..861815b Binary files /dev/null and b/ccxt/__pycache__/htx.cpython-311.pyc differ diff --git a/ccxt/__pycache__/huobi.cpython-311.pyc b/ccxt/__pycache__/huobi.cpython-311.pyc new file mode 100644 index 0000000..21863f4 Binary files /dev/null and b/ccxt/__pycache__/huobi.cpython-311.pyc differ diff --git a/ccxt/__pycache__/hyperliquid.cpython-311.pyc b/ccxt/__pycache__/hyperliquid.cpython-311.pyc new file mode 100644 index 0000000..2e9a5d4 Binary files /dev/null and b/ccxt/__pycache__/hyperliquid.cpython-311.pyc differ diff --git a/ccxt/__pycache__/independentreserve.cpython-311.pyc b/ccxt/__pycache__/independentreserve.cpython-311.pyc new file mode 100644 index 0000000..ece6943 Binary files /dev/null and b/ccxt/__pycache__/independentreserve.cpython-311.pyc differ diff --git a/ccxt/__pycache__/indodax.cpython-311.pyc b/ccxt/__pycache__/indodax.cpython-311.pyc new file mode 100644 index 0000000..5106b87 Binary files /dev/null and b/ccxt/__pycache__/indodax.cpython-311.pyc differ diff --git a/ccxt/__pycache__/kraken.cpython-311.pyc b/ccxt/__pycache__/kraken.cpython-311.pyc new file mode 100644 index 0000000..4a738b8 Binary files /dev/null and b/ccxt/__pycache__/kraken.cpython-311.pyc differ diff --git a/ccxt/__pycache__/krakenfutures.cpython-311.pyc b/ccxt/__pycache__/krakenfutures.cpython-311.pyc new file mode 100644 index 0000000..7043b7f Binary files /dev/null and b/ccxt/__pycache__/krakenfutures.cpython-311.pyc differ diff --git a/ccxt/__pycache__/kucoin.cpython-311.pyc b/ccxt/__pycache__/kucoin.cpython-311.pyc new file mode 100644 index 0000000..526197e Binary files /dev/null and b/ccxt/__pycache__/kucoin.cpython-311.pyc differ diff --git a/ccxt/__pycache__/kucoinfutures.cpython-311.pyc b/ccxt/__pycache__/kucoinfutures.cpython-311.pyc new file mode 100644 index 0000000..7453587 Binary files /dev/null and b/ccxt/__pycache__/kucoinfutures.cpython-311.pyc differ diff --git a/ccxt/__pycache__/latoken.cpython-311.pyc b/ccxt/__pycache__/latoken.cpython-311.pyc new file mode 100644 index 0000000..3a51668 Binary files /dev/null and b/ccxt/__pycache__/latoken.cpython-311.pyc differ diff --git a/ccxt/__pycache__/lbank.cpython-311.pyc b/ccxt/__pycache__/lbank.cpython-311.pyc new file mode 100644 index 0000000..267ac85 Binary files /dev/null and b/ccxt/__pycache__/lbank.cpython-311.pyc differ diff --git a/ccxt/__pycache__/luno.cpython-311.pyc b/ccxt/__pycache__/luno.cpython-311.pyc new file mode 100644 index 0000000..adb3adc Binary files /dev/null and b/ccxt/__pycache__/luno.cpython-311.pyc differ diff --git a/ccxt/__pycache__/mercado.cpython-311.pyc b/ccxt/__pycache__/mercado.cpython-311.pyc new file mode 100644 index 0000000..9d1722e Binary files /dev/null and b/ccxt/__pycache__/mercado.cpython-311.pyc differ diff --git a/ccxt/__pycache__/mexc.cpython-311.pyc b/ccxt/__pycache__/mexc.cpython-311.pyc new file mode 100644 index 0000000..62a4471 Binary files /dev/null and b/ccxt/__pycache__/mexc.cpython-311.pyc differ diff --git a/ccxt/__pycache__/modetrade.cpython-311.pyc b/ccxt/__pycache__/modetrade.cpython-311.pyc new file mode 100644 index 0000000..3f72544 Binary files /dev/null and b/ccxt/__pycache__/modetrade.cpython-311.pyc differ diff --git a/ccxt/__pycache__/mt5.cpython-311.pyc b/ccxt/__pycache__/mt5.cpython-311.pyc new file mode 100644 index 0000000..f1192a9 Binary files /dev/null and b/ccxt/__pycache__/mt5.cpython-311.pyc differ diff --git a/ccxt/__pycache__/myokx.cpython-311.pyc b/ccxt/__pycache__/myokx.cpython-311.pyc new file mode 100644 index 0000000..dc4063e Binary files /dev/null and b/ccxt/__pycache__/myokx.cpython-311.pyc differ diff --git a/ccxt/__pycache__/ndax.cpython-311.pyc b/ccxt/__pycache__/ndax.cpython-311.pyc new file mode 100644 index 0000000..b282ae1 Binary files /dev/null and b/ccxt/__pycache__/ndax.cpython-311.pyc differ diff --git a/ccxt/__pycache__/novadax.cpython-311.pyc b/ccxt/__pycache__/novadax.cpython-311.pyc new file mode 100644 index 0000000..690de71 Binary files /dev/null and b/ccxt/__pycache__/novadax.cpython-311.pyc differ diff --git a/ccxt/__pycache__/oceanex.cpython-311.pyc b/ccxt/__pycache__/oceanex.cpython-311.pyc new file mode 100644 index 0000000..61d82dd Binary files /dev/null and b/ccxt/__pycache__/oceanex.cpython-311.pyc differ diff --git a/ccxt/__pycache__/okx.cpython-311.pyc b/ccxt/__pycache__/okx.cpython-311.pyc new file mode 100644 index 0000000..5e0b7b8 Binary files /dev/null and b/ccxt/__pycache__/okx.cpython-311.pyc differ diff --git a/ccxt/__pycache__/okxus.cpython-311.pyc b/ccxt/__pycache__/okxus.cpython-311.pyc new file mode 100644 index 0000000..b5729f5 Binary files /dev/null and b/ccxt/__pycache__/okxus.cpython-311.pyc differ diff --git a/ccxt/__pycache__/onetrading.cpython-311.pyc b/ccxt/__pycache__/onetrading.cpython-311.pyc new file mode 100644 index 0000000..17fd5fa Binary files /dev/null and b/ccxt/__pycache__/onetrading.cpython-311.pyc differ diff --git a/ccxt/__pycache__/oxfun.cpython-311.pyc b/ccxt/__pycache__/oxfun.cpython-311.pyc new file mode 100644 index 0000000..21b52f4 Binary files /dev/null and b/ccxt/__pycache__/oxfun.cpython-311.pyc differ diff --git a/ccxt/__pycache__/p2b.cpython-311.pyc b/ccxt/__pycache__/p2b.cpython-311.pyc new file mode 100644 index 0000000..b0845b3 Binary files /dev/null and b/ccxt/__pycache__/p2b.cpython-311.pyc differ diff --git a/ccxt/__pycache__/paradex.cpython-311.pyc b/ccxt/__pycache__/paradex.cpython-311.pyc new file mode 100644 index 0000000..8d75c09 Binary files /dev/null and b/ccxt/__pycache__/paradex.cpython-311.pyc differ diff --git a/ccxt/__pycache__/paymium.cpython-311.pyc b/ccxt/__pycache__/paymium.cpython-311.pyc new file mode 100644 index 0000000..04a79fc Binary files /dev/null and b/ccxt/__pycache__/paymium.cpython-311.pyc differ diff --git a/ccxt/__pycache__/phemex.cpython-311.pyc b/ccxt/__pycache__/phemex.cpython-311.pyc new file mode 100644 index 0000000..efd781b Binary files /dev/null and b/ccxt/__pycache__/phemex.cpython-311.pyc differ diff --git a/ccxt/__pycache__/poloniex.cpython-311.pyc b/ccxt/__pycache__/poloniex.cpython-311.pyc new file mode 100644 index 0000000..7d780c8 Binary files /dev/null and b/ccxt/__pycache__/poloniex.cpython-311.pyc differ diff --git a/ccxt/__pycache__/probit.cpython-311.pyc b/ccxt/__pycache__/probit.cpython-311.pyc new file mode 100644 index 0000000..bf0d83d Binary files /dev/null and b/ccxt/__pycache__/probit.cpython-311.pyc differ diff --git a/ccxt/__pycache__/timex.cpython-311.pyc b/ccxt/__pycache__/timex.cpython-311.pyc new file mode 100644 index 0000000..037aa54 Binary files /dev/null and b/ccxt/__pycache__/timex.cpython-311.pyc differ diff --git a/ccxt/__pycache__/tokocrypto.cpython-311.pyc b/ccxt/__pycache__/tokocrypto.cpython-311.pyc new file mode 100644 index 0000000..9dfb592 Binary files /dev/null and b/ccxt/__pycache__/tokocrypto.cpython-311.pyc differ diff --git a/ccxt/__pycache__/toobit.cpython-311.pyc b/ccxt/__pycache__/toobit.cpython-311.pyc new file mode 100644 index 0000000..a6d56c1 Binary files /dev/null and b/ccxt/__pycache__/toobit.cpython-311.pyc differ diff --git a/ccxt/__pycache__/upbit.cpython-311.pyc b/ccxt/__pycache__/upbit.cpython-311.pyc new file mode 100644 index 0000000..0b2ae1d Binary files /dev/null and b/ccxt/__pycache__/upbit.cpython-311.pyc differ diff --git a/ccxt/__pycache__/wavesexchange.cpython-311.pyc b/ccxt/__pycache__/wavesexchange.cpython-311.pyc new file mode 100644 index 0000000..604d676 Binary files /dev/null and b/ccxt/__pycache__/wavesexchange.cpython-311.pyc differ diff --git a/ccxt/__pycache__/whitebit.cpython-311.pyc b/ccxt/__pycache__/whitebit.cpython-311.pyc new file mode 100644 index 0000000..ebe4b56 Binary files /dev/null and b/ccxt/__pycache__/whitebit.cpython-311.pyc differ diff --git a/ccxt/__pycache__/woo.cpython-311.pyc b/ccxt/__pycache__/woo.cpython-311.pyc new file mode 100644 index 0000000..3bd9b64 Binary files /dev/null and b/ccxt/__pycache__/woo.cpython-311.pyc differ diff --git a/ccxt/__pycache__/woofipro.cpython-311.pyc b/ccxt/__pycache__/woofipro.cpython-311.pyc new file mode 100644 index 0000000..0c7c78a Binary files /dev/null and b/ccxt/__pycache__/woofipro.cpython-311.pyc differ diff --git a/ccxt/__pycache__/xt.cpython-311.pyc b/ccxt/__pycache__/xt.cpython-311.pyc new file mode 100644 index 0000000..1d45690 Binary files /dev/null and b/ccxt/__pycache__/xt.cpython-311.pyc differ diff --git a/ccxt/__pycache__/yobit.cpython-311.pyc b/ccxt/__pycache__/yobit.cpython-311.pyc new file mode 100644 index 0000000..7e6bce6 Binary files /dev/null and b/ccxt/__pycache__/yobit.cpython-311.pyc differ diff --git a/ccxt/__pycache__/zaif.cpython-311.pyc b/ccxt/__pycache__/zaif.cpython-311.pyc new file mode 100644 index 0000000..8a9674e Binary files /dev/null and b/ccxt/__pycache__/zaif.cpython-311.pyc differ diff --git a/ccxt/__pycache__/zonda.cpython-311.pyc b/ccxt/__pycache__/zonda.cpython-311.pyc new file mode 100644 index 0000000..5c2d9d2 Binary files /dev/null and b/ccxt/__pycache__/zonda.cpython-311.pyc differ diff --git a/ccxt/abstract/__init__.py b/ccxt/abstract/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ccxt/abstract/__pycache__/__init__.cpython-311.pyc b/ccxt/abstract/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..5e73453 Binary files /dev/null and b/ccxt/abstract/__pycache__/__init__.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/alpaca.cpython-311.pyc b/ccxt/abstract/__pycache__/alpaca.cpython-311.pyc new file mode 100644 index 0000000..401010a Binary files /dev/null and b/ccxt/abstract/__pycache__/alpaca.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/apex.cpython-311.pyc b/ccxt/abstract/__pycache__/apex.cpython-311.pyc new file mode 100644 index 0000000..e484c58 Binary files /dev/null and b/ccxt/abstract/__pycache__/apex.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/arkham.cpython-311.pyc b/ccxt/abstract/__pycache__/arkham.cpython-311.pyc new file mode 100644 index 0000000..1d2c93f Binary files /dev/null and b/ccxt/abstract/__pycache__/arkham.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/ascendex.cpython-311.pyc b/ccxt/abstract/__pycache__/ascendex.cpython-311.pyc new file mode 100644 index 0000000..540e137 Binary files /dev/null and b/ccxt/abstract/__pycache__/ascendex.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/backpack.cpython-311.pyc b/ccxt/abstract/__pycache__/backpack.cpython-311.pyc new file mode 100644 index 0000000..6dfd7e7 Binary files /dev/null and b/ccxt/abstract/__pycache__/backpack.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/bequant.cpython-311.pyc b/ccxt/abstract/__pycache__/bequant.cpython-311.pyc new file mode 100644 index 0000000..228c4eb Binary files /dev/null and b/ccxt/abstract/__pycache__/bequant.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/bigone.cpython-311.pyc b/ccxt/abstract/__pycache__/bigone.cpython-311.pyc new file mode 100644 index 0000000..c7c26ce Binary files /dev/null and b/ccxt/abstract/__pycache__/bigone.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/binance.cpython-311.pyc b/ccxt/abstract/__pycache__/binance.cpython-311.pyc new file mode 100644 index 0000000..ecd0cd0 Binary files /dev/null and b/ccxt/abstract/__pycache__/binance.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/binancecoinm.cpython-311.pyc b/ccxt/abstract/__pycache__/binancecoinm.cpython-311.pyc new file mode 100644 index 0000000..d77d7fe Binary files /dev/null and b/ccxt/abstract/__pycache__/binancecoinm.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/binanceus.cpython-311.pyc b/ccxt/abstract/__pycache__/binanceus.cpython-311.pyc new file mode 100644 index 0000000..037a72c Binary files /dev/null and b/ccxt/abstract/__pycache__/binanceus.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/binanceusdm.cpython-311.pyc b/ccxt/abstract/__pycache__/binanceusdm.cpython-311.pyc new file mode 100644 index 0000000..17ef0ad Binary files /dev/null and b/ccxt/abstract/__pycache__/binanceusdm.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/bingx.cpython-311.pyc b/ccxt/abstract/__pycache__/bingx.cpython-311.pyc new file mode 100644 index 0000000..538e4df Binary files /dev/null and b/ccxt/abstract/__pycache__/bingx.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/bit2c.cpython-311.pyc b/ccxt/abstract/__pycache__/bit2c.cpython-311.pyc new file mode 100644 index 0000000..2422d7c Binary files /dev/null and b/ccxt/abstract/__pycache__/bit2c.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/bitbank.cpython-311.pyc b/ccxt/abstract/__pycache__/bitbank.cpython-311.pyc new file mode 100644 index 0000000..bc364b8 Binary files /dev/null and b/ccxt/abstract/__pycache__/bitbank.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/bitbns.cpython-311.pyc b/ccxt/abstract/__pycache__/bitbns.cpython-311.pyc new file mode 100644 index 0000000..7481797 Binary files /dev/null and b/ccxt/abstract/__pycache__/bitbns.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/bitfinex.cpython-311.pyc b/ccxt/abstract/__pycache__/bitfinex.cpython-311.pyc new file mode 100644 index 0000000..1ab3304 Binary files /dev/null and b/ccxt/abstract/__pycache__/bitfinex.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/bitflyer.cpython-311.pyc b/ccxt/abstract/__pycache__/bitflyer.cpython-311.pyc new file mode 100644 index 0000000..89b4a7d Binary files /dev/null and b/ccxt/abstract/__pycache__/bitflyer.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/bitget.cpython-311.pyc b/ccxt/abstract/__pycache__/bitget.cpython-311.pyc new file mode 100644 index 0000000..c2e71f5 Binary files /dev/null and b/ccxt/abstract/__pycache__/bitget.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/bithumb.cpython-311.pyc b/ccxt/abstract/__pycache__/bithumb.cpython-311.pyc new file mode 100644 index 0000000..996ddac Binary files /dev/null and b/ccxt/abstract/__pycache__/bithumb.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/bitmart.cpython-311.pyc b/ccxt/abstract/__pycache__/bitmart.cpython-311.pyc new file mode 100644 index 0000000..f60f90d Binary files /dev/null and b/ccxt/abstract/__pycache__/bitmart.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/bitmex.cpython-311.pyc b/ccxt/abstract/__pycache__/bitmex.cpython-311.pyc new file mode 100644 index 0000000..c6d426c Binary files /dev/null and b/ccxt/abstract/__pycache__/bitmex.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/bitopro.cpython-311.pyc b/ccxt/abstract/__pycache__/bitopro.cpython-311.pyc new file mode 100644 index 0000000..6f360b9 Binary files /dev/null and b/ccxt/abstract/__pycache__/bitopro.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/bitrue.cpython-311.pyc b/ccxt/abstract/__pycache__/bitrue.cpython-311.pyc new file mode 100644 index 0000000..90771bd Binary files /dev/null and b/ccxt/abstract/__pycache__/bitrue.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/bitso.cpython-311.pyc b/ccxt/abstract/__pycache__/bitso.cpython-311.pyc new file mode 100644 index 0000000..9ed4ae1 Binary files /dev/null and b/ccxt/abstract/__pycache__/bitso.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/bitstamp.cpython-311.pyc b/ccxt/abstract/__pycache__/bitstamp.cpython-311.pyc new file mode 100644 index 0000000..dcc12ee Binary files /dev/null and b/ccxt/abstract/__pycache__/bitstamp.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/bitteam.cpython-311.pyc b/ccxt/abstract/__pycache__/bitteam.cpython-311.pyc new file mode 100644 index 0000000..6c2fdac Binary files /dev/null and b/ccxt/abstract/__pycache__/bitteam.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/bittrade.cpython-311.pyc b/ccxt/abstract/__pycache__/bittrade.cpython-311.pyc new file mode 100644 index 0000000..9f239c2 Binary files /dev/null and b/ccxt/abstract/__pycache__/bittrade.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/bitvavo.cpython-311.pyc b/ccxt/abstract/__pycache__/bitvavo.cpython-311.pyc new file mode 100644 index 0000000..a73012d Binary files /dev/null and b/ccxt/abstract/__pycache__/bitvavo.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/blockchaincom.cpython-311.pyc b/ccxt/abstract/__pycache__/blockchaincom.cpython-311.pyc new file mode 100644 index 0000000..0d5fc52 Binary files /dev/null and b/ccxt/abstract/__pycache__/blockchaincom.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/blofin.cpython-311.pyc b/ccxt/abstract/__pycache__/blofin.cpython-311.pyc new file mode 100644 index 0000000..6af61b0 Binary files /dev/null and b/ccxt/abstract/__pycache__/blofin.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/btcalpha.cpython-311.pyc b/ccxt/abstract/__pycache__/btcalpha.cpython-311.pyc new file mode 100644 index 0000000..b75293c Binary files /dev/null and b/ccxt/abstract/__pycache__/btcalpha.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/btcbox.cpython-311.pyc b/ccxt/abstract/__pycache__/btcbox.cpython-311.pyc new file mode 100644 index 0000000..2d9b36f Binary files /dev/null and b/ccxt/abstract/__pycache__/btcbox.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/btcmarkets.cpython-311.pyc b/ccxt/abstract/__pycache__/btcmarkets.cpython-311.pyc new file mode 100644 index 0000000..1486c77 Binary files /dev/null and b/ccxt/abstract/__pycache__/btcmarkets.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/btcturk.cpython-311.pyc b/ccxt/abstract/__pycache__/btcturk.cpython-311.pyc new file mode 100644 index 0000000..df094ab Binary files /dev/null and b/ccxt/abstract/__pycache__/btcturk.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/bybit.cpython-311.pyc b/ccxt/abstract/__pycache__/bybit.cpython-311.pyc new file mode 100644 index 0000000..2ea6008 Binary files /dev/null and b/ccxt/abstract/__pycache__/bybit.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/cex.cpython-311.pyc b/ccxt/abstract/__pycache__/cex.cpython-311.pyc new file mode 100644 index 0000000..9e8dd57 Binary files /dev/null and b/ccxt/abstract/__pycache__/cex.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/coinbase.cpython-311.pyc b/ccxt/abstract/__pycache__/coinbase.cpython-311.pyc new file mode 100644 index 0000000..44c27af Binary files /dev/null and b/ccxt/abstract/__pycache__/coinbase.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/coinbaseadvanced.cpython-311.pyc b/ccxt/abstract/__pycache__/coinbaseadvanced.cpython-311.pyc new file mode 100644 index 0000000..21bc9a0 Binary files /dev/null and b/ccxt/abstract/__pycache__/coinbaseadvanced.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/coinbaseexchange.cpython-311.pyc b/ccxt/abstract/__pycache__/coinbaseexchange.cpython-311.pyc new file mode 100644 index 0000000..0628464 Binary files /dev/null and b/ccxt/abstract/__pycache__/coinbaseexchange.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/coinbaseinternational.cpython-311.pyc b/ccxt/abstract/__pycache__/coinbaseinternational.cpython-311.pyc new file mode 100644 index 0000000..cc17eff Binary files /dev/null and b/ccxt/abstract/__pycache__/coinbaseinternational.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/coincatch.cpython-311.pyc b/ccxt/abstract/__pycache__/coincatch.cpython-311.pyc new file mode 100644 index 0000000..84dd6ac Binary files /dev/null and b/ccxt/abstract/__pycache__/coincatch.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/coincheck.cpython-311.pyc b/ccxt/abstract/__pycache__/coincheck.cpython-311.pyc new file mode 100644 index 0000000..f49862e Binary files /dev/null and b/ccxt/abstract/__pycache__/coincheck.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/coinex.cpython-311.pyc b/ccxt/abstract/__pycache__/coinex.cpython-311.pyc new file mode 100644 index 0000000..eacfc21 Binary files /dev/null and b/ccxt/abstract/__pycache__/coinex.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/coinmate.cpython-311.pyc b/ccxt/abstract/__pycache__/coinmate.cpython-311.pyc new file mode 100644 index 0000000..55d3594 Binary files /dev/null and b/ccxt/abstract/__pycache__/coinmate.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/coinmetro.cpython-311.pyc b/ccxt/abstract/__pycache__/coinmetro.cpython-311.pyc new file mode 100644 index 0000000..4ca54ba Binary files /dev/null and b/ccxt/abstract/__pycache__/coinmetro.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/coinone.cpython-311.pyc b/ccxt/abstract/__pycache__/coinone.cpython-311.pyc new file mode 100644 index 0000000..62bbbe6 Binary files /dev/null and b/ccxt/abstract/__pycache__/coinone.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/coinsph.cpython-311.pyc b/ccxt/abstract/__pycache__/coinsph.cpython-311.pyc new file mode 100644 index 0000000..dc6e0bc Binary files /dev/null and b/ccxt/abstract/__pycache__/coinsph.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/coinspot.cpython-311.pyc b/ccxt/abstract/__pycache__/coinspot.cpython-311.pyc new file mode 100644 index 0000000..980c913 Binary files /dev/null and b/ccxt/abstract/__pycache__/coinspot.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/cryptocom.cpython-311.pyc b/ccxt/abstract/__pycache__/cryptocom.cpython-311.pyc new file mode 100644 index 0000000..a97fb1a Binary files /dev/null and b/ccxt/abstract/__pycache__/cryptocom.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/cryptomus.cpython-311.pyc b/ccxt/abstract/__pycache__/cryptomus.cpython-311.pyc new file mode 100644 index 0000000..6109770 Binary files /dev/null and b/ccxt/abstract/__pycache__/cryptomus.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/deepcoin.cpython-311.pyc b/ccxt/abstract/__pycache__/deepcoin.cpython-311.pyc new file mode 100644 index 0000000..9914d5a Binary files /dev/null and b/ccxt/abstract/__pycache__/deepcoin.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/defx.cpython-311.pyc b/ccxt/abstract/__pycache__/defx.cpython-311.pyc new file mode 100644 index 0000000..02ce1bf Binary files /dev/null and b/ccxt/abstract/__pycache__/defx.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/delta.cpython-311.pyc b/ccxt/abstract/__pycache__/delta.cpython-311.pyc new file mode 100644 index 0000000..398a9a2 Binary files /dev/null and b/ccxt/abstract/__pycache__/delta.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/deribit.cpython-311.pyc b/ccxt/abstract/__pycache__/deribit.cpython-311.pyc new file mode 100644 index 0000000..05edbe3 Binary files /dev/null and b/ccxt/abstract/__pycache__/deribit.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/derive.cpython-311.pyc b/ccxt/abstract/__pycache__/derive.cpython-311.pyc new file mode 100644 index 0000000..8fe717c Binary files /dev/null and b/ccxt/abstract/__pycache__/derive.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/digifinex.cpython-311.pyc b/ccxt/abstract/__pycache__/digifinex.cpython-311.pyc new file mode 100644 index 0000000..1147247 Binary files /dev/null and b/ccxt/abstract/__pycache__/digifinex.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/exmo.cpython-311.pyc b/ccxt/abstract/__pycache__/exmo.cpython-311.pyc new file mode 100644 index 0000000..c05cfd7 Binary files /dev/null and b/ccxt/abstract/__pycache__/exmo.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/fmfwio.cpython-311.pyc b/ccxt/abstract/__pycache__/fmfwio.cpython-311.pyc new file mode 100644 index 0000000..1a998cd Binary files /dev/null and b/ccxt/abstract/__pycache__/fmfwio.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/foxbit.cpython-311.pyc b/ccxt/abstract/__pycache__/foxbit.cpython-311.pyc new file mode 100644 index 0000000..c93f690 Binary files /dev/null and b/ccxt/abstract/__pycache__/foxbit.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/gate.cpython-311.pyc b/ccxt/abstract/__pycache__/gate.cpython-311.pyc new file mode 100644 index 0000000..85d0d9a Binary files /dev/null and b/ccxt/abstract/__pycache__/gate.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/gateio.cpython-311.pyc b/ccxt/abstract/__pycache__/gateio.cpython-311.pyc new file mode 100644 index 0000000..94a4a3a Binary files /dev/null and b/ccxt/abstract/__pycache__/gateio.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/gemini.cpython-311.pyc b/ccxt/abstract/__pycache__/gemini.cpython-311.pyc new file mode 100644 index 0000000..cc958b3 Binary files /dev/null and b/ccxt/abstract/__pycache__/gemini.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/hashkey.cpython-311.pyc b/ccxt/abstract/__pycache__/hashkey.cpython-311.pyc new file mode 100644 index 0000000..835df2c Binary files /dev/null and b/ccxt/abstract/__pycache__/hashkey.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/hibachi.cpython-311.pyc b/ccxt/abstract/__pycache__/hibachi.cpython-311.pyc new file mode 100644 index 0000000..2b6e870 Binary files /dev/null and b/ccxt/abstract/__pycache__/hibachi.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/hitbtc.cpython-311.pyc b/ccxt/abstract/__pycache__/hitbtc.cpython-311.pyc new file mode 100644 index 0000000..85bb2bd Binary files /dev/null and b/ccxt/abstract/__pycache__/hitbtc.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/hollaex.cpython-311.pyc b/ccxt/abstract/__pycache__/hollaex.cpython-311.pyc new file mode 100644 index 0000000..118c724 Binary files /dev/null and b/ccxt/abstract/__pycache__/hollaex.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/htx.cpython-311.pyc b/ccxt/abstract/__pycache__/htx.cpython-311.pyc new file mode 100644 index 0000000..a4ef45b Binary files /dev/null and b/ccxt/abstract/__pycache__/htx.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/huobi.cpython-311.pyc b/ccxt/abstract/__pycache__/huobi.cpython-311.pyc new file mode 100644 index 0000000..698067f Binary files /dev/null and b/ccxt/abstract/__pycache__/huobi.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/hyperliquid.cpython-311.pyc b/ccxt/abstract/__pycache__/hyperliquid.cpython-311.pyc new file mode 100644 index 0000000..f168456 Binary files /dev/null and b/ccxt/abstract/__pycache__/hyperliquid.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/independentreserve.cpython-311.pyc b/ccxt/abstract/__pycache__/independentreserve.cpython-311.pyc new file mode 100644 index 0000000..3de0da4 Binary files /dev/null and b/ccxt/abstract/__pycache__/independentreserve.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/indodax.cpython-311.pyc b/ccxt/abstract/__pycache__/indodax.cpython-311.pyc new file mode 100644 index 0000000..5929039 Binary files /dev/null and b/ccxt/abstract/__pycache__/indodax.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/kraken.cpython-311.pyc b/ccxt/abstract/__pycache__/kraken.cpython-311.pyc new file mode 100644 index 0000000..d94c708 Binary files /dev/null and b/ccxt/abstract/__pycache__/kraken.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/krakenfutures.cpython-311.pyc b/ccxt/abstract/__pycache__/krakenfutures.cpython-311.pyc new file mode 100644 index 0000000..9d5da3d Binary files /dev/null and b/ccxt/abstract/__pycache__/krakenfutures.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/kucoin.cpython-311.pyc b/ccxt/abstract/__pycache__/kucoin.cpython-311.pyc new file mode 100644 index 0000000..4c4b4e3 Binary files /dev/null and b/ccxt/abstract/__pycache__/kucoin.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/kucoinfutures.cpython-311.pyc b/ccxt/abstract/__pycache__/kucoinfutures.cpython-311.pyc new file mode 100644 index 0000000..433275d Binary files /dev/null and b/ccxt/abstract/__pycache__/kucoinfutures.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/latoken.cpython-311.pyc b/ccxt/abstract/__pycache__/latoken.cpython-311.pyc new file mode 100644 index 0000000..b10f190 Binary files /dev/null and b/ccxt/abstract/__pycache__/latoken.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/lbank.cpython-311.pyc b/ccxt/abstract/__pycache__/lbank.cpython-311.pyc new file mode 100644 index 0000000..467a135 Binary files /dev/null and b/ccxt/abstract/__pycache__/lbank.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/luno.cpython-311.pyc b/ccxt/abstract/__pycache__/luno.cpython-311.pyc new file mode 100644 index 0000000..ccce41f Binary files /dev/null and b/ccxt/abstract/__pycache__/luno.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/mercado.cpython-311.pyc b/ccxt/abstract/__pycache__/mercado.cpython-311.pyc new file mode 100644 index 0000000..4802a35 Binary files /dev/null and b/ccxt/abstract/__pycache__/mercado.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/mexc.cpython-311.pyc b/ccxt/abstract/__pycache__/mexc.cpython-311.pyc new file mode 100644 index 0000000..becb068 Binary files /dev/null and b/ccxt/abstract/__pycache__/mexc.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/modetrade.cpython-311.pyc b/ccxt/abstract/__pycache__/modetrade.cpython-311.pyc new file mode 100644 index 0000000..f7d871a Binary files /dev/null and b/ccxt/abstract/__pycache__/modetrade.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/mt5.cpython-311.pyc b/ccxt/abstract/__pycache__/mt5.cpython-311.pyc new file mode 100644 index 0000000..0c85ae9 Binary files /dev/null and b/ccxt/abstract/__pycache__/mt5.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/myokx.cpython-311.pyc b/ccxt/abstract/__pycache__/myokx.cpython-311.pyc new file mode 100644 index 0000000..5b9846b Binary files /dev/null and b/ccxt/abstract/__pycache__/myokx.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/ndax.cpython-311.pyc b/ccxt/abstract/__pycache__/ndax.cpython-311.pyc new file mode 100644 index 0000000..b06079d Binary files /dev/null and b/ccxt/abstract/__pycache__/ndax.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/novadax.cpython-311.pyc b/ccxt/abstract/__pycache__/novadax.cpython-311.pyc new file mode 100644 index 0000000..3719fb0 Binary files /dev/null and b/ccxt/abstract/__pycache__/novadax.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/oceanex.cpython-311.pyc b/ccxt/abstract/__pycache__/oceanex.cpython-311.pyc new file mode 100644 index 0000000..30a4c8c Binary files /dev/null and b/ccxt/abstract/__pycache__/oceanex.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/okx.cpython-311.pyc b/ccxt/abstract/__pycache__/okx.cpython-311.pyc new file mode 100644 index 0000000..a63c55a Binary files /dev/null and b/ccxt/abstract/__pycache__/okx.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/okxus.cpython-311.pyc b/ccxt/abstract/__pycache__/okxus.cpython-311.pyc new file mode 100644 index 0000000..372cf75 Binary files /dev/null and b/ccxt/abstract/__pycache__/okxus.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/onetrading.cpython-311.pyc b/ccxt/abstract/__pycache__/onetrading.cpython-311.pyc new file mode 100644 index 0000000..9e0c164 Binary files /dev/null and b/ccxt/abstract/__pycache__/onetrading.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/oxfun.cpython-311.pyc b/ccxt/abstract/__pycache__/oxfun.cpython-311.pyc new file mode 100644 index 0000000..63a11e1 Binary files /dev/null and b/ccxt/abstract/__pycache__/oxfun.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/p2b.cpython-311.pyc b/ccxt/abstract/__pycache__/p2b.cpython-311.pyc new file mode 100644 index 0000000..5698d25 Binary files /dev/null and b/ccxt/abstract/__pycache__/p2b.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/paradex.cpython-311.pyc b/ccxt/abstract/__pycache__/paradex.cpython-311.pyc new file mode 100644 index 0000000..eea47d9 Binary files /dev/null and b/ccxt/abstract/__pycache__/paradex.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/paymium.cpython-311.pyc b/ccxt/abstract/__pycache__/paymium.cpython-311.pyc new file mode 100644 index 0000000..9c0d9a3 Binary files /dev/null and b/ccxt/abstract/__pycache__/paymium.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/phemex.cpython-311.pyc b/ccxt/abstract/__pycache__/phemex.cpython-311.pyc new file mode 100644 index 0000000..593e3d3 Binary files /dev/null and b/ccxt/abstract/__pycache__/phemex.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/poloniex.cpython-311.pyc b/ccxt/abstract/__pycache__/poloniex.cpython-311.pyc new file mode 100644 index 0000000..458e651 Binary files /dev/null and b/ccxt/abstract/__pycache__/poloniex.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/probit.cpython-311.pyc b/ccxt/abstract/__pycache__/probit.cpython-311.pyc new file mode 100644 index 0000000..7541d35 Binary files /dev/null and b/ccxt/abstract/__pycache__/probit.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/timex.cpython-311.pyc b/ccxt/abstract/__pycache__/timex.cpython-311.pyc new file mode 100644 index 0000000..01ec2b1 Binary files /dev/null and b/ccxt/abstract/__pycache__/timex.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/tokocrypto.cpython-311.pyc b/ccxt/abstract/__pycache__/tokocrypto.cpython-311.pyc new file mode 100644 index 0000000..00aa711 Binary files /dev/null and b/ccxt/abstract/__pycache__/tokocrypto.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/toobit.cpython-311.pyc b/ccxt/abstract/__pycache__/toobit.cpython-311.pyc new file mode 100644 index 0000000..ef4a3f8 Binary files /dev/null and b/ccxt/abstract/__pycache__/toobit.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/upbit.cpython-311.pyc b/ccxt/abstract/__pycache__/upbit.cpython-311.pyc new file mode 100644 index 0000000..2cb22f8 Binary files /dev/null and b/ccxt/abstract/__pycache__/upbit.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/wavesexchange.cpython-311.pyc b/ccxt/abstract/__pycache__/wavesexchange.cpython-311.pyc new file mode 100644 index 0000000..4bc9175 Binary files /dev/null and b/ccxt/abstract/__pycache__/wavesexchange.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/whitebit.cpython-311.pyc b/ccxt/abstract/__pycache__/whitebit.cpython-311.pyc new file mode 100644 index 0000000..730e6f2 Binary files /dev/null and b/ccxt/abstract/__pycache__/whitebit.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/woo.cpython-311.pyc b/ccxt/abstract/__pycache__/woo.cpython-311.pyc new file mode 100644 index 0000000..a1d5941 Binary files /dev/null and b/ccxt/abstract/__pycache__/woo.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/woofipro.cpython-311.pyc b/ccxt/abstract/__pycache__/woofipro.cpython-311.pyc new file mode 100644 index 0000000..4b12e04 Binary files /dev/null and b/ccxt/abstract/__pycache__/woofipro.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/xt.cpython-311.pyc b/ccxt/abstract/__pycache__/xt.cpython-311.pyc new file mode 100644 index 0000000..16fa449 Binary files /dev/null and b/ccxt/abstract/__pycache__/xt.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/yobit.cpython-311.pyc b/ccxt/abstract/__pycache__/yobit.cpython-311.pyc new file mode 100644 index 0000000..ff2274a Binary files /dev/null and b/ccxt/abstract/__pycache__/yobit.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/zaif.cpython-311.pyc b/ccxt/abstract/__pycache__/zaif.cpython-311.pyc new file mode 100644 index 0000000..897f78a Binary files /dev/null and b/ccxt/abstract/__pycache__/zaif.cpython-311.pyc differ diff --git a/ccxt/abstract/__pycache__/zonda.cpython-311.pyc b/ccxt/abstract/__pycache__/zonda.cpython-311.pyc new file mode 100644 index 0000000..fbd8354 Binary files /dev/null and b/ccxt/abstract/__pycache__/zonda.cpython-311.pyc differ diff --git a/ccxt/abstract/alpaca.py b/ccxt/abstract/alpaca.py new file mode 100644 index 0000000..c4702e7 --- /dev/null +++ b/ccxt/abstract/alpaca.py @@ -0,0 +1,74 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + trader_private_get_v2_account = traderPrivateGetV2Account = Entry('v2/account', ['trader', 'private'], 'GET', {}) + trader_private_get_v2_orders = traderPrivateGetV2Orders = Entry('v2/orders', ['trader', 'private'], 'GET', {}) + trader_private_get_v2_orders_order_id = traderPrivateGetV2OrdersOrderId = Entry('v2/orders/{order_id}', ['trader', 'private'], 'GET', {}) + trader_private_get_v2_positions = traderPrivateGetV2Positions = Entry('v2/positions', ['trader', 'private'], 'GET', {}) + trader_private_get_v2_positions_symbol_or_asset_id = traderPrivateGetV2PositionsSymbolOrAssetId = Entry('v2/positions/{symbol_or_asset_id}', ['trader', 'private'], 'GET', {}) + trader_private_get_v2_account_portfolio_history = traderPrivateGetV2AccountPortfolioHistory = Entry('v2/account/portfolio/history', ['trader', 'private'], 'GET', {}) + trader_private_get_v2_watchlists = traderPrivateGetV2Watchlists = Entry('v2/watchlists', ['trader', 'private'], 'GET', {}) + trader_private_get_v2_watchlists_watchlist_id = traderPrivateGetV2WatchlistsWatchlistId = Entry('v2/watchlists/{watchlist_id}', ['trader', 'private'], 'GET', {}) + trader_private_get_v2_watchlists_by_name = traderPrivateGetV2WatchlistsByName = Entry('v2/watchlists:by_name', ['trader', 'private'], 'GET', {}) + trader_private_get_v2_account_configurations = traderPrivateGetV2AccountConfigurations = Entry('v2/account/configurations', ['trader', 'private'], 'GET', {}) + trader_private_get_v2_account_activities = traderPrivateGetV2AccountActivities = Entry('v2/account/activities', ['trader', 'private'], 'GET', {}) + trader_private_get_v2_account_activities_activity_type = traderPrivateGetV2AccountActivitiesActivityType = Entry('v2/account/activities/{activity_type}', ['trader', 'private'], 'GET', {}) + trader_private_get_v2_calendar = traderPrivateGetV2Calendar = Entry('v2/calendar', ['trader', 'private'], 'GET', {}) + trader_private_get_v2_clock = traderPrivateGetV2Clock = Entry('v2/clock', ['trader', 'private'], 'GET', {}) + trader_private_get_v2_assets = traderPrivateGetV2Assets = Entry('v2/assets', ['trader', 'private'], 'GET', {}) + trader_private_get_v2_assets_symbol_or_asset_id = traderPrivateGetV2AssetsSymbolOrAssetId = Entry('v2/assets/{symbol_or_asset_id}', ['trader', 'private'], 'GET', {}) + trader_private_get_v2_corporate_actions_announcements_id = traderPrivateGetV2CorporateActionsAnnouncementsId = Entry('v2/corporate_actions/announcements/{id}', ['trader', 'private'], 'GET', {}) + trader_private_get_v2_corporate_actions_announcements = traderPrivateGetV2CorporateActionsAnnouncements = Entry('v2/corporate_actions/announcements', ['trader', 'private'], 'GET', {}) + trader_private_get_v2_wallets = traderPrivateGetV2Wallets = Entry('v2/wallets', ['trader', 'private'], 'GET', {}) + trader_private_get_v2_wallets_transfers = traderPrivateGetV2WalletsTransfers = Entry('v2/wallets/transfers', ['trader', 'private'], 'GET', {}) + trader_private_post_v2_orders = traderPrivatePostV2Orders = Entry('v2/orders', ['trader', 'private'], 'POST', {}) + trader_private_post_v2_watchlists = traderPrivatePostV2Watchlists = Entry('v2/watchlists', ['trader', 'private'], 'POST', {}) + trader_private_post_v2_watchlists_watchlist_id = traderPrivatePostV2WatchlistsWatchlistId = Entry('v2/watchlists/{watchlist_id}', ['trader', 'private'], 'POST', {}) + trader_private_post_v2_watchlists_by_name = traderPrivatePostV2WatchlistsByName = Entry('v2/watchlists:by_name', ['trader', 'private'], 'POST', {}) + trader_private_post_v2_wallets_transfers = traderPrivatePostV2WalletsTransfers = Entry('v2/wallets/transfers', ['trader', 'private'], 'POST', {}) + trader_private_put_v2_orders_order_id = traderPrivatePutV2OrdersOrderId = Entry('v2/orders/{order_id}', ['trader', 'private'], 'PUT', {}) + trader_private_put_v2_watchlists_watchlist_id = traderPrivatePutV2WatchlistsWatchlistId = Entry('v2/watchlists/{watchlist_id}', ['trader', 'private'], 'PUT', {}) + trader_private_put_v2_watchlists_by_name = traderPrivatePutV2WatchlistsByName = Entry('v2/watchlists:by_name', ['trader', 'private'], 'PUT', {}) + trader_private_patch_v2_orders_order_id = traderPrivatePatchV2OrdersOrderId = Entry('v2/orders/{order_id}', ['trader', 'private'], 'PATCH', {}) + trader_private_patch_v2_account_configurations = traderPrivatePatchV2AccountConfigurations = Entry('v2/account/configurations', ['trader', 'private'], 'PATCH', {}) + trader_private_delete_v2_orders = traderPrivateDeleteV2Orders = Entry('v2/orders', ['trader', 'private'], 'DELETE', {}) + trader_private_delete_v2_orders_order_id = traderPrivateDeleteV2OrdersOrderId = Entry('v2/orders/{order_id}', ['trader', 'private'], 'DELETE', {}) + trader_private_delete_v2_positions = traderPrivateDeleteV2Positions = Entry('v2/positions', ['trader', 'private'], 'DELETE', {}) + trader_private_delete_v2_positions_symbol_or_asset_id = traderPrivateDeleteV2PositionsSymbolOrAssetId = Entry('v2/positions/{symbol_or_asset_id}', ['trader', 'private'], 'DELETE', {}) + trader_private_delete_v2_watchlists_watchlist_id = traderPrivateDeleteV2WatchlistsWatchlistId = Entry('v2/watchlists/{watchlist_id}', ['trader', 'private'], 'DELETE', {}) + trader_private_delete_v2_watchlists_by_name = traderPrivateDeleteV2WatchlistsByName = Entry('v2/watchlists:by_name', ['trader', 'private'], 'DELETE', {}) + trader_private_delete_v2_watchlists_watchlist_id_symbol = traderPrivateDeleteV2WatchlistsWatchlistIdSymbol = Entry('v2/watchlists/{watchlist_id}/{symbol}', ['trader', 'private'], 'DELETE', {}) + market_public_get_v1beta3_crypto_loc_bars = marketPublicGetV1beta3CryptoLocBars = Entry('v1beta3/crypto/{loc}/bars', ['market', 'public'], 'GET', {}) + market_public_get_v1beta3_crypto_loc_latest_bars = marketPublicGetV1beta3CryptoLocLatestBars = Entry('v1beta3/crypto/{loc}/latest/bars', ['market', 'public'], 'GET', {}) + market_public_get_v1beta3_crypto_loc_latest_orderbooks = marketPublicGetV1beta3CryptoLocLatestOrderbooks = Entry('v1beta3/crypto/{loc}/latest/orderbooks', ['market', 'public'], 'GET', {}) + market_public_get_v1beta3_crypto_loc_latest_quotes = marketPublicGetV1beta3CryptoLocLatestQuotes = Entry('v1beta3/crypto/{loc}/latest/quotes', ['market', 'public'], 'GET', {}) + market_public_get_v1beta3_crypto_loc_latest_trades = marketPublicGetV1beta3CryptoLocLatestTrades = Entry('v1beta3/crypto/{loc}/latest/trades', ['market', 'public'], 'GET', {}) + market_public_get_v1beta3_crypto_loc_quotes = marketPublicGetV1beta3CryptoLocQuotes = Entry('v1beta3/crypto/{loc}/quotes', ['market', 'public'], 'GET', {}) + market_public_get_v1beta3_crypto_loc_snapshots = marketPublicGetV1beta3CryptoLocSnapshots = Entry('v1beta3/crypto/{loc}/snapshots', ['market', 'public'], 'GET', {}) + market_public_get_v1beta3_crypto_loc_trades = marketPublicGetV1beta3CryptoLocTrades = Entry('v1beta3/crypto/{loc}/trades', ['market', 'public'], 'GET', {}) + market_private_get_v1beta1_corporate_actions = marketPrivateGetV1beta1CorporateActions = Entry('v1beta1/corporate-actions', ['market', 'private'], 'GET', {}) + market_private_get_v1beta1_forex_latest_rates = marketPrivateGetV1beta1ForexLatestRates = Entry('v1beta1/forex/latest/rates', ['market', 'private'], 'GET', {}) + market_private_get_v1beta1_forex_rates = marketPrivateGetV1beta1ForexRates = Entry('v1beta1/forex/rates', ['market', 'private'], 'GET', {}) + market_private_get_v1beta1_logos_symbol = marketPrivateGetV1beta1LogosSymbol = Entry('v1beta1/logos/{symbol}', ['market', 'private'], 'GET', {}) + market_private_get_v1beta1_news = marketPrivateGetV1beta1News = Entry('v1beta1/news', ['market', 'private'], 'GET', {}) + market_private_get_v1beta1_screener_stocks_most_actives = marketPrivateGetV1beta1ScreenerStocksMostActives = Entry('v1beta1/screener/stocks/most-actives', ['market', 'private'], 'GET', {}) + market_private_get_v1beta1_screener_market_type_movers = marketPrivateGetV1beta1ScreenerMarketTypeMovers = Entry('v1beta1/screener/{market_type}/movers', ['market', 'private'], 'GET', {}) + market_private_get_v2_stocks_auctions = marketPrivateGetV2StocksAuctions = Entry('v2/stocks/auctions', ['market', 'private'], 'GET', {}) + market_private_get_v2_stocks_bars = marketPrivateGetV2StocksBars = Entry('v2/stocks/bars', ['market', 'private'], 'GET', {}) + market_private_get_v2_stocks_bars_latest = marketPrivateGetV2StocksBarsLatest = Entry('v2/stocks/bars/latest', ['market', 'private'], 'GET', {}) + market_private_get_v2_stocks_meta_conditions_ticktype = marketPrivateGetV2StocksMetaConditionsTicktype = Entry('v2/stocks/meta/conditions/{ticktype}', ['market', 'private'], 'GET', {}) + market_private_get_v2_stocks_meta_exchanges = marketPrivateGetV2StocksMetaExchanges = Entry('v2/stocks/meta/exchanges', ['market', 'private'], 'GET', {}) + market_private_get_v2_stocks_quotes = marketPrivateGetV2StocksQuotes = Entry('v2/stocks/quotes', ['market', 'private'], 'GET', {}) + market_private_get_v2_stocks_quotes_latest = marketPrivateGetV2StocksQuotesLatest = Entry('v2/stocks/quotes/latest', ['market', 'private'], 'GET', {}) + market_private_get_v2_stocks_snapshots = marketPrivateGetV2StocksSnapshots = Entry('v2/stocks/snapshots', ['market', 'private'], 'GET', {}) + market_private_get_v2_stocks_trades = marketPrivateGetV2StocksTrades = Entry('v2/stocks/trades', ['market', 'private'], 'GET', {}) + market_private_get_v2_stocks_trades_latest = marketPrivateGetV2StocksTradesLatest = Entry('v2/stocks/trades/latest', ['market', 'private'], 'GET', {}) + market_private_get_v2_stocks_symbol_auctions = marketPrivateGetV2StocksSymbolAuctions = Entry('v2/stocks/{symbol}/auctions', ['market', 'private'], 'GET', {}) + market_private_get_v2_stocks_symbol_bars = marketPrivateGetV2StocksSymbolBars = Entry('v2/stocks/{symbol}/bars', ['market', 'private'], 'GET', {}) + market_private_get_v2_stocks_symbol_bars_latest = marketPrivateGetV2StocksSymbolBarsLatest = Entry('v2/stocks/{symbol}/bars/latest', ['market', 'private'], 'GET', {}) + market_private_get_v2_stocks_symbol_quotes = marketPrivateGetV2StocksSymbolQuotes = Entry('v2/stocks/{symbol}/quotes', ['market', 'private'], 'GET', {}) + market_private_get_v2_stocks_symbol_quotes_latest = marketPrivateGetV2StocksSymbolQuotesLatest = Entry('v2/stocks/{symbol}/quotes/latest', ['market', 'private'], 'GET', {}) + market_private_get_v2_stocks_symbol_snapshot = marketPrivateGetV2StocksSymbolSnapshot = Entry('v2/stocks/{symbol}/snapshot', ['market', 'private'], 'GET', {}) + market_private_get_v2_stocks_symbol_trades = marketPrivateGetV2StocksSymbolTrades = Entry('v2/stocks/{symbol}/trades', ['market', 'private'], 'GET', {}) + market_private_get_v2_stocks_symbol_trades_latest = marketPrivateGetV2StocksSymbolTradesLatest = Entry('v2/stocks/{symbol}/trades/latest', ['market', 'private'], 'GET', {}) diff --git a/ccxt/abstract/apex.py b/ccxt/abstract/apex.py new file mode 100644 index 0000000..b291aa0 --- /dev/null +++ b/ccxt/abstract/apex.py @@ -0,0 +1,31 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_v3_symbols = publicGetV3Symbols = Entry('v3/symbols', 'public', 'GET', {'cost': 1}) + public_get_v3_history_funding = publicGetV3HistoryFunding = Entry('v3/history-funding', 'public', 'GET', {'cost': 1}) + public_get_v3_ticker = publicGetV3Ticker = Entry('v3/ticker', 'public', 'GET', {'cost': 1}) + public_get_v3_klines = publicGetV3Klines = Entry('v3/klines', 'public', 'GET', {'cost': 1}) + public_get_v3_trades = publicGetV3Trades = Entry('v3/trades', 'public', 'GET', {'cost': 1}) + public_get_v3_depth = publicGetV3Depth = Entry('v3/depth', 'public', 'GET', {'cost': 1}) + public_get_v3_time = publicGetV3Time = Entry('v3/time', 'public', 'GET', {'cost': 1}) + public_get_v3_data_all_ticker_info = publicGetV3DataAllTickerInfo = Entry('v3/data/all-ticker-info', 'public', 'GET', {'cost': 1}) + private_get_v3_account = privateGetV3Account = Entry('v3/account', 'private', 'GET', {'cost': 1}) + private_get_v3_account_balance = privateGetV3AccountBalance = Entry('v3/account-balance', 'private', 'GET', {'cost': 1}) + private_get_v3_fills = privateGetV3Fills = Entry('v3/fills', 'private', 'GET', {'cost': 1}) + private_get_v3_order_fills = privateGetV3OrderFills = Entry('v3/order-fills', 'private', 'GET', {'cost': 1}) + private_get_v3_order = privateGetV3Order = Entry('v3/order', 'private', 'GET', {'cost': 1}) + private_get_v3_history_orders = privateGetV3HistoryOrders = Entry('v3/history-orders', 'private', 'GET', {'cost': 1}) + private_get_v3_order_by_client_order_id = privateGetV3OrderByClientOrderId = Entry('v3/order-by-client-order-id', 'private', 'GET', {'cost': 1}) + private_get_v3_funding = privateGetV3Funding = Entry('v3/funding', 'private', 'GET', {'cost': 1}) + private_get_v3_historical_pnl = privateGetV3HistoricalPnl = Entry('v3/historical-pnl', 'private', 'GET', {'cost': 1}) + private_get_v3_open_orders = privateGetV3OpenOrders = Entry('v3/open-orders', 'private', 'GET', {'cost': 1}) + private_get_v3_transfers = privateGetV3Transfers = Entry('v3/transfers', 'private', 'GET', {'cost': 1}) + private_get_v3_transfer = privateGetV3Transfer = Entry('v3/transfer', 'private', 'GET', {'cost': 1}) + private_post_v3_delete_open_orders = privatePostV3DeleteOpenOrders = Entry('v3/delete-open-orders', 'private', 'POST', {'cost': 1}) + private_post_v3_delete_client_order_id = privatePostV3DeleteClientOrderId = Entry('v3/delete-client-order-id', 'private', 'POST', {'cost': 1}) + private_post_v3_delete_order = privatePostV3DeleteOrder = Entry('v3/delete-order', 'private', 'POST', {'cost': 1}) + private_post_v3_order = privatePostV3Order = Entry('v3/order', 'private', 'POST', {'cost': 1}) + private_post_v3_set_initial_margin_rate = privatePostV3SetInitialMarginRate = Entry('v3/set-initial-margin-rate', 'private', 'POST', {'cost': 1}) + private_post_v3_transfer_out = privatePostV3TransferOut = Entry('v3/transfer-out', 'private', 'POST', {'cost': 1}) + private_post_v3_contract_transfer_out = privatePostV3ContractTransferOut = Entry('v3/contract-transfer-out', 'private', 'POST', {'cost': 1}) diff --git a/ccxt/abstract/arkham.py b/ccxt/abstract/arkham.py new file mode 100644 index 0000000..3f02c65 --- /dev/null +++ b/ccxt/abstract/arkham.py @@ -0,0 +1,118 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + v1_public_get_alerts = v1PublicGetAlerts = Entry('alerts', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_announcements = v1PublicGetAnnouncements = Entry('announcements', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_assets = v1PublicGetAssets = Entry('assets', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_book = v1PublicGetBook = Entry('book', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_candles = v1PublicGetCandles = Entry('candles', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_chains = v1PublicGetChains = Entry('chains', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_contracts = v1PublicGetContracts = Entry('contracts', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_index_price = v1PublicGetIndexPrice = Entry('index-price', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_index_prices = v1PublicGetIndexPrices = Entry('index-prices', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_margin_schedules = v1PublicGetMarginSchedules = Entry('margin-schedules', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_marketcapchart = v1PublicGetMarketcapchart = Entry('marketcapchart', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_marketcaps = v1PublicGetMarketcaps = Entry('marketcaps', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_pair = v1PublicGetPair = Entry('pair', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_pairs = v1PublicGetPairs = Entry('pairs', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_server_time = v1PublicGetServerTime = Entry('server-time', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_ticker = v1PublicGetTicker = Entry('ticker', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_tickers = v1PublicGetTickers = Entry('tickers', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_trades = v1PublicGetTrades = Entry('trades', ['v1', 'public'], 'GET', {'cost': 1}) + v1_private_get_user = v1PrivateGetUser = Entry('user', ['v1', 'private'], 'GET', {'cost': 7.5}) + v1_private_get_orders = v1PrivateGetOrders = Entry('orders', ['v1', 'private'], 'GET', {'cost': 7.5}) + v1_private_get_orders_by_client_order_id = v1PrivateGetOrdersByClientOrderId = Entry('orders/by-client-order-id', ['v1', 'private'], 'GET', {'cost': 7.5}) + v1_private_get_orders_history = v1PrivateGetOrdersHistory = Entry('orders/history', ['v1', 'private'], 'GET', {'cost': 7.5}) + v1_private_get_orders_history_by_client_order_id = v1PrivateGetOrdersHistoryByClientOrderId = Entry('orders/history/by-client-order-id', ['v1', 'private'], 'GET', {'cost': 7.5}) + v1_private_get_orders_history_offset = v1PrivateGetOrdersHistoryOffset = Entry('orders/history_offset', ['v1', 'private'], 'GET', {'cost': 7.5}) + v1_private_get_orders_id = v1PrivateGetOrdersId = Entry('orders/{id}', ['v1', 'private'], 'GET', {'cost': 7.5}) + v1_private_get_trades = v1PrivateGetTrades = Entry('trades', ['v1', 'private'], 'GET', {'cost': 7.5}) + v1_private_get_trades_history = v1PrivateGetTradesHistory = Entry('trades/history', ['v1', 'private'], 'GET', {'cost': 7.5}) + v1_private_get_trades_time = v1PrivateGetTradesTime = Entry('trades/time', ['v1', 'private'], 'GET', {'cost': 7.5}) + v1_private_get_trigger_orders = v1PrivateGetTriggerOrders = Entry('trigger-orders', ['v1', 'private'], 'GET', {'cost': 7.5}) + v1_private_get_account_airdrops = v1PrivateGetAccountAirdrops = Entry('account/airdrops', ['v1', 'private'], 'GET', {'cost': 7.5}) + v1_private_get_account_balance_updates = v1PrivateGetAccountBalanceUpdates = Entry('account/balance-updates', ['v1', 'private'], 'GET', {'cost': 7.5}) + v1_private_get_account_balances = v1PrivateGetAccountBalances = Entry('account/balances', ['v1', 'private'], 'GET', {'cost': 7.5}) + v1_private_get_account_balances_ll = v1PrivateGetAccountBalancesLl = Entry('account/balances/ll', ['v1', 'private'], 'GET', {'cost': 7.5}) + v1_private_get_account_balances_history = v1PrivateGetAccountBalancesHistory = Entry('account/balances/history', ['v1', 'private'], 'GET', {'cost': 7.5}) + v1_private_get_account_balances_commissions = v1PrivateGetAccountBalancesCommissions = Entry('account/balances/commissions', ['v1', 'private'], 'GET', {'cost': 7.5}) + v1_private_get_account_deposit_addresses = v1PrivateGetAccountDepositAddresses = Entry('account/deposit/addresses', ['v1', 'private'], 'GET', {'cost': 7.5}) + v1_private_get_account_deposits = v1PrivateGetAccountDeposits = Entry('account/deposits', ['v1', 'private'], 'GET', {'cost': 7.5}) + v1_private_get_account_fees = v1PrivateGetAccountFees = Entry('account/fees', ['v1', 'private'], 'GET', {'cost': 7.5}) + v1_private_get_account_funding_rate_payments = v1PrivateGetAccountFundingRatePayments = Entry('account/funding-rate-payments', ['v1', 'private'], 'GET', {'cost': 7.5}) + v1_private_get_account_leverage = v1PrivateGetAccountLeverage = Entry('account/leverage', ['v1', 'private'], 'GET', {'cost': 7.5}) + v1_private_get_account_lsp_assignments = v1PrivateGetAccountLspAssignments = Entry('account/lsp-assignments', ['v1', 'private'], 'GET', {'cost': 7.5}) + v1_private_get_account_margin = v1PrivateGetAccountMargin = Entry('account/margin', ['v1', 'private'], 'GET', {'cost': 7.5}) + v1_private_get_account_margin_all = v1PrivateGetAccountMarginAll = Entry('account/margin/all', ['v1', 'private'], 'GET', {'cost': 7.5}) + v1_private_get_account_notifications = v1PrivateGetAccountNotifications = Entry('account/notifications', ['v1', 'private'], 'GET', {'cost': 7.5}) + v1_private_get_account_position_updates = v1PrivateGetAccountPositionUpdates = Entry('account/position-updates', ['v1', 'private'], 'GET', {'cost': 7.5}) + v1_private_get_account_positions = v1PrivateGetAccountPositions = Entry('account/positions', ['v1', 'private'], 'GET', {'cost': 7.5}) + v1_private_get_account_realized_pnl = v1PrivateGetAccountRealizedPnl = Entry('account/realized-pnl', ['v1', 'private'], 'GET', {'cost': 7.5}) + v1_private_get_account_rebates = v1PrivateGetAccountRebates = Entry('account/rebates', ['v1', 'private'], 'GET', {'cost': 7.5}) + v1_private_get_account_referral_links = v1PrivateGetAccountReferralLinks = Entry('account/referral-links', ['v1', 'private'], 'GET', {'cost': 7.5}) + v1_private_get_account_sessions = v1PrivateGetAccountSessions = Entry('account/sessions', ['v1', 'private'], 'GET', {'cost': 7.5}) + v1_private_get_account_settings = v1PrivateGetAccountSettings = Entry('account/settings', ['v1', 'private'], 'GET', {'cost': 7.5}) + v1_private_get_account_settings_price_alert = v1PrivateGetAccountSettingsPriceAlert = Entry('account/settings/price-alert', ['v1', 'private'], 'GET', {'cost': 7.5}) + v1_private_get_account_transfers = v1PrivateGetAccountTransfers = Entry('account/transfers', ['v1', 'private'], 'GET', {'cost': 7.5}) + v1_private_get_account_unsubscribe = v1PrivateGetAccountUnsubscribe = Entry('account/unsubscribe', ['v1', 'private'], 'GET', {'cost': 7.5}) + v1_private_get_account_watchlist = v1PrivateGetAccountWatchlist = Entry('account/watchlist', ['v1', 'private'], 'GET', {'cost': 7.5}) + v1_private_get_account_withdrawal_addresses = v1PrivateGetAccountWithdrawalAddresses = Entry('account/withdrawal/addresses', ['v1', 'private'], 'GET', {'cost': 7.5}) + v1_private_get_account_withdrawal_addresses_id = v1PrivateGetAccountWithdrawalAddressesId = Entry('account/withdrawal/addresses/{id}', ['v1', 'private'], 'GET', {'cost': 7.5}) + v1_private_get_account_withdrawals = v1PrivateGetAccountWithdrawals = Entry('account/withdrawals', ['v1', 'private'], 'GET', {'cost': 7.5}) + v1_private_get_subaccounts = v1PrivateGetSubaccounts = Entry('subaccounts', ['v1', 'private'], 'GET', {'cost': 7.5}) + v1_private_get_airdrop = v1PrivateGetAirdrop = Entry('airdrop', ['v1', 'private'], 'GET', {'cost': 7.5}) + v1_private_get_airdrop_claim = v1PrivateGetAirdropClaim = Entry('airdrop/claim', ['v1', 'private'], 'GET', {'cost': 7.5}) + v1_private_get_affiliate_dashboard_commission_earned = v1PrivateGetAffiliateDashboardCommissionEarned = Entry('affiliate-dashboard/commission-earned', ['v1', 'private'], 'GET', {'cost': 7.5}) + v1_private_get_affiliate_dashboard_min_arkm_last_30d = v1PrivateGetAffiliateDashboardMinArkmLast30d = Entry('affiliate-dashboard/min-arkm-last-30d', ['v1', 'private'], 'GET', {'cost': 7.5}) + v1_private_get_affiliate_dashboard_points = v1PrivateGetAffiliateDashboardPoints = Entry('affiliate-dashboard/points', ['v1', 'private'], 'GET', {'cost': 7.5}) + v1_private_get_affiliate_dashboard_points_season_1 = v1PrivateGetAffiliateDashboardPointsSeason1 = Entry('affiliate-dashboard/points-season-1', ['v1', 'private'], 'GET', {'cost': 7.5}) + v1_private_get_affiliate_dashboard_points_season_2 = v1PrivateGetAffiliateDashboardPointsSeason2 = Entry('affiliate-dashboard/points-season-2', ['v1', 'private'], 'GET', {'cost': 7.5}) + v1_private_get_affiliate_dashboard_realized_pnl = v1PrivateGetAffiliateDashboardRealizedPnl = Entry('affiliate-dashboard/realized-pnl', ['v1', 'private'], 'GET', {'cost': 7.5}) + v1_private_get_affiliate_dashboard_rebate_balance = v1PrivateGetAffiliateDashboardRebateBalance = Entry('affiliate-dashboard/rebate-balance', ['v1', 'private'], 'GET', {'cost': 7.5}) + v1_private_get_affiliate_dashboard_referral_count = v1PrivateGetAffiliateDashboardReferralCount = Entry('affiliate-dashboard/referral-count', ['v1', 'private'], 'GET', {'cost': 7.5}) + v1_private_get_affiliate_dashboard_referrals_season_1 = v1PrivateGetAffiliateDashboardReferralsSeason1 = Entry('affiliate-dashboard/referrals-season-1', ['v1', 'private'], 'GET', {'cost': 7.5}) + v1_private_get_affiliate_dashboard_referrals_season_2 = v1PrivateGetAffiliateDashboardReferralsSeason2 = Entry('affiliate-dashboard/referrals-season-2', ['v1', 'private'], 'GET', {'cost': 7.5}) + v1_private_get_affiliate_dashboard_trading_volume_stats = v1PrivateGetAffiliateDashboardTradingVolumeStats = Entry('affiliate-dashboard/trading-volume-stats', ['v1', 'private'], 'GET', {'cost': 7.5}) + v1_private_get_affiliate_dashboard_volume_season_1 = v1PrivateGetAffiliateDashboardVolumeSeason1 = Entry('affiliate-dashboard/volume-season-1', ['v1', 'private'], 'GET', {'cost': 7.5}) + v1_private_get_affiliate_dashboard_volume_season_2 = v1PrivateGetAffiliateDashboardVolumeSeason2 = Entry('affiliate-dashboard/volume-season-2', ['v1', 'private'], 'GET', {'cost': 7.5}) + v1_private_get_affiliate_dashboard_api_key = v1PrivateGetAffiliateDashboardApiKey = Entry('affiliate-dashboard/api-key', ['v1', 'private'], 'GET', {'cost': 7.5}) + v1_private_get_competitions_opt_in_status = v1PrivateGetCompetitionsOptInStatus = Entry('competitions/opt-in-status', ['v1', 'private'], 'GET', {'cost': 7.5}) + v1_private_get_rewards_info = v1PrivateGetRewardsInfo = Entry('rewards/info', ['v1', 'private'], 'GET', {'cost': 7.5}) + v1_private_get_rewards_vouchers = v1PrivateGetRewardsVouchers = Entry('rewards/vouchers', ['v1', 'private'], 'GET', {'cost': 7.5}) + v1_private_post_orders_new = v1PrivatePostOrdersNew = Entry('orders/new', ['v1', 'private'], 'POST', {'cost': 7.5}) + v1_private_post_trigger_orders_new = v1PrivatePostTriggerOrdersNew = Entry('trigger-orders/new', ['v1', 'private'], 'POST', {'cost': 7.5}) + v1_private_post_orders_cancel = v1PrivatePostOrdersCancel = Entry('orders/cancel', ['v1', 'private'], 'POST', {'cost': 7.5}) + v1_private_post_trigger_orders_cancel = v1PrivatePostTriggerOrdersCancel = Entry('trigger-orders/cancel', ['v1', 'private'], 'POST', {'cost': 7.5}) + v1_private_post_orders_cancel_all = v1PrivatePostOrdersCancelAll = Entry('orders/cancel/all', ['v1', 'private'], 'POST', {'cost': 7.5}) + v1_private_post_trigger_orders_cancel_all = v1PrivatePostTriggerOrdersCancelAll = Entry('trigger-orders/cancel/all', ['v1', 'private'], 'POST', {'cost': 7.5}) + v1_private_post_orders_new_simple = v1PrivatePostOrdersNewSimple = Entry('orders/new/simple', ['v1', 'private'], 'POST', {'cost': 7.5}) + v1_private_post_account_deposit_addresses_new = v1PrivatePostAccountDepositAddressesNew = Entry('account/deposit/addresses/new', ['v1', 'private'], 'POST', {'cost': 7.5}) + v1_private_post_account_leverage = v1PrivatePostAccountLeverage = Entry('account/leverage', ['v1', 'private'], 'POST', {'cost': 7.5}) + v1_private_post_account_notifications_read = v1PrivatePostAccountNotificationsRead = Entry('account/notifications/read', ['v1', 'private'], 'POST', {'cost': 7.5}) + v1_private_post_account_referral_links = v1PrivatePostAccountReferralLinks = Entry('account/referral-links', ['v1', 'private'], 'POST', {'cost': 7.5}) + v1_private_post_account_sessions_delete = v1PrivatePostAccountSessionsDelete = Entry('account/sessions/delete', ['v1', 'private'], 'POST', {'cost': 7.5}) + v1_private_post_account_sessions_terminate_all = v1PrivatePostAccountSessionsTerminateAll = Entry('account/sessions/terminate-all', ['v1', 'private'], 'POST', {'cost': 7.5}) + v1_private_post_account_settings_update = v1PrivatePostAccountSettingsUpdate = Entry('account/settings/update', ['v1', 'private'], 'POST', {'cost': 7.5}) + v1_private_post_account_watchlist_add = v1PrivatePostAccountWatchlistAdd = Entry('account/watchlist/add', ['v1', 'private'], 'POST', {'cost': 7.5}) + v1_private_post_account_watchlist_remove = v1PrivatePostAccountWatchlistRemove = Entry('account/watchlist/remove', ['v1', 'private'], 'POST', {'cost': 7.5}) + v1_private_post_account_withdraw = v1PrivatePostAccountWithdraw = Entry('account/withdraw', ['v1', 'private'], 'POST', {'cost': 7.5}) + v1_private_post_account_withdrawal_addresses_confirm = v1PrivatePostAccountWithdrawalAddressesConfirm = Entry('account/withdrawal/addresses/confirm', ['v1', 'private'], 'POST', {'cost': 7.5}) + v1_private_post_subaccounts = v1PrivatePostSubaccounts = Entry('subaccounts', ['v1', 'private'], 'POST', {'cost': 7.5}) + v1_private_post_subaccounts_transfer = v1PrivatePostSubaccountsTransfer = Entry('subaccounts/transfer', ['v1', 'private'], 'POST', {'cost': 7.5}) + v1_private_post_subaccounts_perp_transfer = v1PrivatePostSubaccountsPerpTransfer = Entry('subaccounts/perp-transfer', ['v1', 'private'], 'POST', {'cost': 7.5}) + v1_private_post_subaccounts_update_settings = v1PrivatePostSubaccountsUpdateSettings = Entry('subaccounts/update-settings', ['v1', 'private'], 'POST', {'cost': 7.5}) + v1_private_post_airdrop = v1PrivatePostAirdrop = Entry('airdrop', ['v1', 'private'], 'POST', {'cost': 7.5}) + v1_private_post_api_key_create = v1PrivatePostApiKeyCreate = Entry('api-key/create', ['v1', 'private'], 'POST', {'cost': 7.5}) + v1_private_post_authenticate = v1PrivatePostAuthenticate = Entry('authenticate', ['v1', 'private'], 'POST', {'cost': 7.5}) + v1_private_post_competitions_opt_in = v1PrivatePostCompetitionsOptIn = Entry('competitions/opt-in', ['v1', 'private'], 'POST', {'cost': 7.5}) + v1_private_post_rewards_vouchers_claim = v1PrivatePostRewardsVouchersClaim = Entry('rewards/vouchers/claim', ['v1', 'private'], 'POST', {'cost': 7.5}) + v1_private_put_account_referral_links_id_slug = v1PrivatePutAccountReferralLinksIdSlug = Entry('account/referral-links/{id}/slug', ['v1', 'private'], 'PUT', {'cost': 7.5}) + v1_private_put_account_settings_price_alert = v1PrivatePutAccountSettingsPriceAlert = Entry('account/settings/price-alert', ['v1', 'private'], 'PUT', {'cost': 7.5}) + v1_private_put_account_withdrawal_addresses_id = v1PrivatePutAccountWithdrawalAddressesId = Entry('account/withdrawal/addresses/{id}', ['v1', 'private'], 'PUT', {'cost': 7.5}) + v1_private_put_subaccounts = v1PrivatePutSubaccounts = Entry('subaccounts', ['v1', 'private'], 'PUT', {'cost': 7.5}) + v1_private_put_api_key_update_id = v1PrivatePutApiKeyUpdateId = Entry('api-key/update/{id}', ['v1', 'private'], 'PUT', {'cost': 7.5}) + v1_private_delete_account_settings_price_alert = v1PrivateDeleteAccountSettingsPriceAlert = Entry('account/settings/price-alert', ['v1', 'private'], 'DELETE', {'cost': 7.5}) + v1_private_delete_account_withdrawal_addresses_id = v1PrivateDeleteAccountWithdrawalAddressesId = Entry('account/withdrawal/addresses/{id}', ['v1', 'private'], 'DELETE', {'cost': 7.5}) + v1_private_delete_subaccounts_subaccountid = v1PrivateDeleteSubaccountsSubaccountId = Entry('subaccounts/{subaccountId}', ['v1', 'private'], 'DELETE', {'cost': 7.5}) + v1_private_delete_api_key_id = v1PrivateDeleteApiKeyId = Entry('api-key/{id}', ['v1', 'private'], 'DELETE', {'cost': 7.5}) diff --git a/ccxt/abstract/ascendex.py b/ccxt/abstract/ascendex.py new file mode 100644 index 0000000..413fad4 --- /dev/null +++ b/ccxt/abstract/ascendex.py @@ -0,0 +1,77 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + v1_public_get_assets = v1PublicGetAssets = Entry('assets', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_products = v1PublicGetProducts = Entry('products', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_ticker = v1PublicGetTicker = Entry('ticker', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_barhist_info = v1PublicGetBarhistInfo = Entry('barhist/info', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_barhist = v1PublicGetBarhist = Entry('barhist', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_depth = v1PublicGetDepth = Entry('depth', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_trades = v1PublicGetTrades = Entry('trades', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_cash_assets = v1PublicGetCashAssets = Entry('cash/assets', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_cash_products = v1PublicGetCashProducts = Entry('cash/products', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_margin_assets = v1PublicGetMarginAssets = Entry('margin/assets', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_margin_products = v1PublicGetMarginProducts = Entry('margin/products', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_futures_collateral = v1PublicGetFuturesCollateral = Entry('futures/collateral', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_futures_contracts = v1PublicGetFuturesContracts = Entry('futures/contracts', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_futures_ref_px = v1PublicGetFuturesRefPx = Entry('futures/ref-px', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_futures_market_data = v1PublicGetFuturesMarketData = Entry('futures/market-data', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_futures_funding_rates = v1PublicGetFuturesFundingRates = Entry('futures/funding-rates', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_risk_limit_info = v1PublicGetRiskLimitInfo = Entry('risk-limit-info', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_exchange_info = v1PublicGetExchangeInfo = Entry('exchange-info', ['v1', 'public'], 'GET', {'cost': 1}) + v1_private_get_info = v1PrivateGetInfo = Entry('info', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_wallet_transactions = v1PrivateGetWalletTransactions = Entry('wallet/transactions', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_wallet_deposit_address = v1PrivateGetWalletDepositAddress = Entry('wallet/deposit/address', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_data_balance_snapshot = v1PrivateGetDataBalanceSnapshot = Entry('data/balance/snapshot', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_data_balance_history = v1PrivateGetDataBalanceHistory = Entry('data/balance/history', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_accountcategory_get_balance = v1PrivateAccountCategoryGetBalance = Entry('balance', ['v1', 'private', 'accountCategory'], 'GET', {'cost': 1}) + v1_private_accountcategory_get_order_open = v1PrivateAccountCategoryGetOrderOpen = Entry('order/open', ['v1', 'private', 'accountCategory'], 'GET', {'cost': 1}) + v1_private_accountcategory_get_order_status = v1PrivateAccountCategoryGetOrderStatus = Entry('order/status', ['v1', 'private', 'accountCategory'], 'GET', {'cost': 1}) + v1_private_accountcategory_get_order_hist_current = v1PrivateAccountCategoryGetOrderHistCurrent = Entry('order/hist/current', ['v1', 'private', 'accountCategory'], 'GET', {'cost': 1}) + v1_private_accountcategory_get_risk = v1PrivateAccountCategoryGetRisk = Entry('risk', ['v1', 'private', 'accountCategory'], 'GET', {'cost': 1}) + v1_private_accountcategory_post_order = v1PrivateAccountCategoryPostOrder = Entry('order', ['v1', 'private', 'accountCategory'], 'POST', {'cost': 1}) + v1_private_accountcategory_post_order_batch = v1PrivateAccountCategoryPostOrderBatch = Entry('order/batch', ['v1', 'private', 'accountCategory'], 'POST', {'cost': 1}) + v1_private_accountcategory_delete_order = v1PrivateAccountCategoryDeleteOrder = Entry('order', ['v1', 'private', 'accountCategory'], 'DELETE', {'cost': 1}) + v1_private_accountcategory_delete_order_all = v1PrivateAccountCategoryDeleteOrderAll = Entry('order/all', ['v1', 'private', 'accountCategory'], 'DELETE', {'cost': 1}) + v1_private_accountcategory_delete_order_batch = v1PrivateAccountCategoryDeleteOrderBatch = Entry('order/batch', ['v1', 'private', 'accountCategory'], 'DELETE', {'cost': 1}) + v1_private_accountgroup_get_cash_balance = v1PrivateAccountGroupGetCashBalance = Entry('cash/balance', ['v1', 'private', 'accountGroup'], 'GET', {'cost': 1}) + v1_private_accountgroup_get_margin_balance = v1PrivateAccountGroupGetMarginBalance = Entry('margin/balance', ['v1', 'private', 'accountGroup'], 'GET', {'cost': 1}) + v1_private_accountgroup_get_margin_risk = v1PrivateAccountGroupGetMarginRisk = Entry('margin/risk', ['v1', 'private', 'accountGroup'], 'GET', {'cost': 1}) + v1_private_accountgroup_get_futures_collateral_balance = v1PrivateAccountGroupGetFuturesCollateralBalance = Entry('futures/collateral-balance', ['v1', 'private', 'accountGroup'], 'GET', {'cost': 1}) + v1_private_accountgroup_get_futures_position = v1PrivateAccountGroupGetFuturesPosition = Entry('futures/position', ['v1', 'private', 'accountGroup'], 'GET', {'cost': 1}) + v1_private_accountgroup_get_futures_risk = v1PrivateAccountGroupGetFuturesRisk = Entry('futures/risk', ['v1', 'private', 'accountGroup'], 'GET', {'cost': 1}) + v1_private_accountgroup_get_futures_funding_payments = v1PrivateAccountGroupGetFuturesFundingPayments = Entry('futures/funding-payments', ['v1', 'private', 'accountGroup'], 'GET', {'cost': 1}) + v1_private_accountgroup_get_order_hist = v1PrivateAccountGroupGetOrderHist = Entry('order/hist', ['v1', 'private', 'accountGroup'], 'GET', {'cost': 1}) + v1_private_accountgroup_get_spot_fee = v1PrivateAccountGroupGetSpotFee = Entry('spot/fee', ['v1', 'private', 'accountGroup'], 'GET', {'cost': 1}) + v1_private_accountgroup_post_transfer = v1PrivateAccountGroupPostTransfer = Entry('transfer', ['v1', 'private', 'accountGroup'], 'POST', {'cost': 1}) + v1_private_accountgroup_post_futures_transfer_deposit = v1PrivateAccountGroupPostFuturesTransferDeposit = Entry('futures/transfer/deposit', ['v1', 'private', 'accountGroup'], 'POST', {'cost': 1}) + v1_private_accountgroup_post_futures_transfer_withdraw = v1PrivateAccountGroupPostFuturesTransferWithdraw = Entry('futures/transfer/withdraw', ['v1', 'private', 'accountGroup'], 'POST', {'cost': 1}) + v2_public_get_assets = v2PublicGetAssets = Entry('assets', ['v2', 'public'], 'GET', {'cost': 1}) + v2_public_get_futures_contract = v2PublicGetFuturesContract = Entry('futures/contract', ['v2', 'public'], 'GET', {'cost': 1}) + v2_public_get_futures_collateral = v2PublicGetFuturesCollateral = Entry('futures/collateral', ['v2', 'public'], 'GET', {'cost': 1}) + v2_public_get_futures_pricing_data = v2PublicGetFuturesPricingData = Entry('futures/pricing-data', ['v2', 'public'], 'GET', {'cost': 1}) + v2_public_get_futures_ticker = v2PublicGetFuturesTicker = Entry('futures/ticker', ['v2', 'public'], 'GET', {'cost': 1}) + v2_public_get_risk_limit_info = v2PublicGetRiskLimitInfo = Entry('risk-limit-info', ['v2', 'public'], 'GET', {'cost': 1}) + v2_private_data_get_order_hist = v2PrivateDataGetOrderHist = Entry('order/hist', ['v2', 'private', 'data'], 'GET', {'cost': 1}) + v2_private_get_account_info = v2PrivateGetAccountInfo = Entry('account/info', ['v2', 'private'], 'GET', {'cost': 1}) + v2_private_accountgroup_get_order_hist = v2PrivateAccountGroupGetOrderHist = Entry('order/hist', ['v2', 'private', 'accountGroup'], 'GET', {'cost': 1}) + v2_private_accountgroup_get_futures_position = v2PrivateAccountGroupGetFuturesPosition = Entry('futures/position', ['v2', 'private', 'accountGroup'], 'GET', {'cost': 1}) + v2_private_accountgroup_get_futures_free_margin = v2PrivateAccountGroupGetFuturesFreeMargin = Entry('futures/free-margin', ['v2', 'private', 'accountGroup'], 'GET', {'cost': 1}) + v2_private_accountgroup_get_futures_order_hist_current = v2PrivateAccountGroupGetFuturesOrderHistCurrent = Entry('futures/order/hist/current', ['v2', 'private', 'accountGroup'], 'GET', {'cost': 1}) + v2_private_accountgroup_get_futures_funding_payments = v2PrivateAccountGroupGetFuturesFundingPayments = Entry('futures/funding-payments', ['v2', 'private', 'accountGroup'], 'GET', {'cost': 1}) + v2_private_accountgroup_get_futures_order_open = v2PrivateAccountGroupGetFuturesOrderOpen = Entry('futures/order/open', ['v2', 'private', 'accountGroup'], 'GET', {'cost': 1}) + v2_private_accountgroup_get_futures_order_status = v2PrivateAccountGroupGetFuturesOrderStatus = Entry('futures/order/status', ['v2', 'private', 'accountGroup'], 'GET', {'cost': 1}) + v2_private_accountgroup_post_futures_isolated_position_margin = v2PrivateAccountGroupPostFuturesIsolatedPositionMargin = Entry('futures/isolated-position-margin', ['v2', 'private', 'accountGroup'], 'POST', {'cost': 1}) + v2_private_accountgroup_post_futures_margin_type = v2PrivateAccountGroupPostFuturesMarginType = Entry('futures/margin-type', ['v2', 'private', 'accountGroup'], 'POST', {'cost': 1}) + v2_private_accountgroup_post_futures_leverage = v2PrivateAccountGroupPostFuturesLeverage = Entry('futures/leverage', ['v2', 'private', 'accountGroup'], 'POST', {'cost': 1}) + v2_private_accountgroup_post_futures_transfer_deposit = v2PrivateAccountGroupPostFuturesTransferDeposit = Entry('futures/transfer/deposit', ['v2', 'private', 'accountGroup'], 'POST', {'cost': 1}) + v2_private_accountgroup_post_futures_transfer_withdraw = v2PrivateAccountGroupPostFuturesTransferWithdraw = Entry('futures/transfer/withdraw', ['v2', 'private', 'accountGroup'], 'POST', {'cost': 1}) + v2_private_accountgroup_post_futures_order = v2PrivateAccountGroupPostFuturesOrder = Entry('futures/order', ['v2', 'private', 'accountGroup'], 'POST', {'cost': 1}) + v2_private_accountgroup_post_futures_order_batch = v2PrivateAccountGroupPostFuturesOrderBatch = Entry('futures/order/batch', ['v2', 'private', 'accountGroup'], 'POST', {'cost': 1}) + v2_private_accountgroup_post_futures_order_open = v2PrivateAccountGroupPostFuturesOrderOpen = Entry('futures/order/open', ['v2', 'private', 'accountGroup'], 'POST', {'cost': 1}) + v2_private_accountgroup_post_subuser_subuser_transfer = v2PrivateAccountGroupPostSubuserSubuserTransfer = Entry('subuser/subuser-transfer', ['v2', 'private', 'accountGroup'], 'POST', {'cost': 1}) + v2_private_accountgroup_post_subuser_subuser_transfer_hist = v2PrivateAccountGroupPostSubuserSubuserTransferHist = Entry('subuser/subuser-transfer-hist', ['v2', 'private', 'accountGroup'], 'POST', {'cost': 1}) + v2_private_accountgroup_delete_futures_order = v2PrivateAccountGroupDeleteFuturesOrder = Entry('futures/order', ['v2', 'private', 'accountGroup'], 'DELETE', {'cost': 1}) + v2_private_accountgroup_delete_futures_order_batch = v2PrivateAccountGroupDeleteFuturesOrderBatch = Entry('futures/order/batch', ['v2', 'private', 'accountGroup'], 'DELETE', {'cost': 1}) + v2_private_accountgroup_delete_futures_order_all = v2PrivateAccountGroupDeleteFuturesOrderAll = Entry('futures/order/all', ['v2', 'private', 'accountGroup'], 'DELETE', {'cost': 1}) diff --git a/ccxt/abstract/backpack.py b/ccxt/abstract/backpack.py new file mode 100644 index 0000000..72b0a90 --- /dev/null +++ b/ccxt/abstract/backpack.py @@ -0,0 +1,60 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_api_v1_assets = publicGetApiV1Assets = Entry('api/v1/assets', 'public', 'GET', {'cost': 1}) + public_get_api_v1_collateral = publicGetApiV1Collateral = Entry('api/v1/collateral', 'public', 'GET', {'cost': 1}) + public_get_api_v1_borrowlend_markets = publicGetApiV1BorrowLendMarkets = Entry('api/v1/borrowLend/markets', 'public', 'GET', {'cost': 1}) + public_get_api_v1_borrowlend_markets_history = publicGetApiV1BorrowLendMarketsHistory = Entry('api/v1/borrowLend/markets/history', 'public', 'GET', {'cost': 1}) + public_get_api_v1_markets = publicGetApiV1Markets = Entry('api/v1/markets', 'public', 'GET', {'cost': 1}) + public_get_api_v1_market = publicGetApiV1Market = Entry('api/v1/market', 'public', 'GET', {'cost': 1}) + public_get_api_v1_ticker = publicGetApiV1Ticker = Entry('api/v1/ticker', 'public', 'GET', {'cost': 1}) + public_get_api_v1_tickers = publicGetApiV1Tickers = Entry('api/v1/tickers', 'public', 'GET', {'cost': 1}) + public_get_api_v1_depth = publicGetApiV1Depth = Entry('api/v1/depth', 'public', 'GET', {'cost': 1}) + public_get_api_v1_klines = publicGetApiV1Klines = Entry('api/v1/klines', 'public', 'GET', {'cost': 1}) + public_get_api_v1_markprices = publicGetApiV1MarkPrices = Entry('api/v1/markPrices', 'public', 'GET', {'cost': 1}) + public_get_api_v1_openinterest = publicGetApiV1OpenInterest = Entry('api/v1/openInterest', 'public', 'GET', {'cost': 1}) + public_get_api_v1_fundingrates = publicGetApiV1FundingRates = Entry('api/v1/fundingRates', 'public', 'GET', {'cost': 1}) + public_get_api_v1_status = publicGetApiV1Status = Entry('api/v1/status', 'public', 'GET', {'cost': 1}) + public_get_api_v1_ping = publicGetApiV1Ping = Entry('api/v1/ping', 'public', 'GET', {'cost': 1}) + public_get_api_v1_time = publicGetApiV1Time = Entry('api/v1/time', 'public', 'GET', {'cost': 1}) + public_get_api_v1_wallets = publicGetApiV1Wallets = Entry('api/v1/wallets', 'public', 'GET', {'cost': 1}) + public_get_api_v1_trades = publicGetApiV1Trades = Entry('api/v1/trades', 'public', 'GET', {'cost': 1}) + public_get_api_v1_trades_history = publicGetApiV1TradesHistory = Entry('api/v1/trades/history', 'public', 'GET', {'cost': 1}) + private_get_api_v1_account = privateGetApiV1Account = Entry('api/v1/account', 'private', 'GET', {'cost': 1}) + private_get_api_v1_account_limits_borrow = privateGetApiV1AccountLimitsBorrow = Entry('api/v1/account/limits/borrow', 'private', 'GET', {'cost': 1}) + private_get_api_v1_account_limits_order = privateGetApiV1AccountLimitsOrder = Entry('api/v1/account/limits/order', 'private', 'GET', {'cost': 1}) + private_get_api_v1_account_limits_withdrawal = privateGetApiV1AccountLimitsWithdrawal = Entry('api/v1/account/limits/withdrawal', 'private', 'GET', {'cost': 1}) + private_get_api_v1_borrowlend_positions = privateGetApiV1BorrowLendPositions = Entry('api/v1/borrowLend/positions', 'private', 'GET', {'cost': 1}) + private_get_api_v1_capital = privateGetApiV1Capital = Entry('api/v1/capital', 'private', 'GET', {'cost': 1}) + private_get_api_v1_capital_collateral = privateGetApiV1CapitalCollateral = Entry('api/v1/capital/collateral', 'private', 'GET', {'cost': 1}) + private_get_wapi_v1_capital_deposits = privateGetWapiV1CapitalDeposits = Entry('wapi/v1/capital/deposits', 'private', 'GET', {'cost': 1}) + private_get_wapi_v1_capital_deposit_address = privateGetWapiV1CapitalDepositAddress = Entry('wapi/v1/capital/deposit/address', 'private', 'GET', {'cost': 1}) + private_get_wapi_v1_capital_withdrawals = privateGetWapiV1CapitalWithdrawals = Entry('wapi/v1/capital/withdrawals', 'private', 'GET', {'cost': 1}) + private_get_api_v1_position = privateGetApiV1Position = Entry('api/v1/position', 'private', 'GET', {'cost': 1}) + private_get_wapi_v1_history_borrowlend = privateGetWapiV1HistoryBorrowLend = Entry('wapi/v1/history/borrowLend', 'private', 'GET', {'cost': 1}) + private_get_wapi_v1_history_interest = privateGetWapiV1HistoryInterest = Entry('wapi/v1/history/interest', 'private', 'GET', {'cost': 1}) + private_get_wapi_v1_history_borrowlend_positions = privateGetWapiV1HistoryBorrowLendPositions = Entry('wapi/v1/history/borrowLend/positions', 'private', 'GET', {'cost': 1}) + private_get_wapi_v1_history_dust = privateGetWapiV1HistoryDust = Entry('wapi/v1/history/dust', 'private', 'GET', {'cost': 1}) + private_get_wapi_v1_history_fills = privateGetWapiV1HistoryFills = Entry('wapi/v1/history/fills', 'private', 'GET', {'cost': 1}) + private_get_wapi_v1_history_funding = privateGetWapiV1HistoryFunding = Entry('wapi/v1/history/funding', 'private', 'GET', {'cost': 1}) + private_get_wapi_v1_history_orders = privateGetWapiV1HistoryOrders = Entry('wapi/v1/history/orders', 'private', 'GET', {'cost': 1}) + private_get_wapi_v1_history_rfq = privateGetWapiV1HistoryRfq = Entry('wapi/v1/history/rfq', 'private', 'GET', {'cost': 1}) + private_get_wapi_v1_history_quote = privateGetWapiV1HistoryQuote = Entry('wapi/v1/history/quote', 'private', 'GET', {'cost': 1}) + private_get_wapi_v1_history_settlement = privateGetWapiV1HistorySettlement = Entry('wapi/v1/history/settlement', 'private', 'GET', {'cost': 1}) + private_get_wapi_v1_history_strategies = privateGetWapiV1HistoryStrategies = Entry('wapi/v1/history/strategies', 'private', 'GET', {'cost': 1}) + private_get_api_v1_order = privateGetApiV1Order = Entry('api/v1/order', 'private', 'GET', {'cost': 1}) + private_get_api_v1_orders = privateGetApiV1Orders = Entry('api/v1/orders', 'private', 'GET', {'cost': 1}) + private_post_api_v1_account_convertdust = privatePostApiV1AccountConvertDust = Entry('api/v1/account/convertDust', 'private', 'POST', {'cost': 1}) + private_post_api_v1_borrowlend = privatePostApiV1BorrowLend = Entry('api/v1/borrowLend', 'private', 'POST', {'cost': 1}) + private_post_wapi_v1_capital_withdrawals = privatePostWapiV1CapitalWithdrawals = Entry('wapi/v1/capital/withdrawals', 'private', 'POST', {'cost': 1}) + private_post_api_v1_order = privatePostApiV1Order = Entry('api/v1/order', 'private', 'POST', {'cost': 1}) + private_post_api_v1_orders = privatePostApiV1Orders = Entry('api/v1/orders', 'private', 'POST', {'cost': 1}) + private_post_api_v1_rfq = privatePostApiV1Rfq = Entry('api/v1/rfq', 'private', 'POST', {'cost': 1}) + private_post_api_v1_rfq_accept = privatePostApiV1RfqAccept = Entry('api/v1/rfq/accept', 'private', 'POST', {'cost': 1}) + private_post_api_v1_rfq_refresh = privatePostApiV1RfqRefresh = Entry('api/v1/rfq/refresh', 'private', 'POST', {'cost': 1}) + private_post_api_v1_rfq_cancel = privatePostApiV1RfqCancel = Entry('api/v1/rfq/cancel', 'private', 'POST', {'cost': 1}) + private_post_api_v1_rfq_quote = privatePostApiV1RfqQuote = Entry('api/v1/rfq/quote', 'private', 'POST', {'cost': 1}) + private_delete_api_v1_order = privateDeleteApiV1Order = Entry('api/v1/order', 'private', 'DELETE', {'cost': 1}) + private_delete_api_v1_orders = privateDeleteApiV1Orders = Entry('api/v1/orders', 'private', 'DELETE', {'cost': 1}) + private_patch_api_v1_account = privatePatchApiV1Account = Entry('api/v1/account', 'private', 'PATCH', {'cost': 1}) diff --git a/ccxt/abstract/bequant.py b/ccxt/abstract/bequant.py new file mode 100644 index 0000000..9507bfd --- /dev/null +++ b/ccxt/abstract/bequant.py @@ -0,0 +1,115 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_public_currency = publicGetPublicCurrency = Entry('public/currency', 'public', 'GET', {'cost': 10}) + public_get_public_currency_currency = publicGetPublicCurrencyCurrency = Entry('public/currency/{currency}', 'public', 'GET', {'cost': 10}) + public_get_public_symbol = publicGetPublicSymbol = Entry('public/symbol', 'public', 'GET', {'cost': 10}) + public_get_public_symbol_symbol = publicGetPublicSymbolSymbol = Entry('public/symbol/{symbol}', 'public', 'GET', {'cost': 10}) + public_get_public_ticker = publicGetPublicTicker = Entry('public/ticker', 'public', 'GET', {'cost': 10}) + public_get_public_ticker_symbol = publicGetPublicTickerSymbol = Entry('public/ticker/{symbol}', 'public', 'GET', {'cost': 10}) + public_get_public_price_rate = publicGetPublicPriceRate = Entry('public/price/rate', 'public', 'GET', {'cost': 10}) + public_get_public_price_history = publicGetPublicPriceHistory = Entry('public/price/history', 'public', 'GET', {'cost': 10}) + public_get_public_price_ticker = publicGetPublicPriceTicker = Entry('public/price/ticker', 'public', 'GET', {'cost': 10}) + public_get_public_price_ticker_symbol = publicGetPublicPriceTickerSymbol = Entry('public/price/ticker/{symbol}', 'public', 'GET', {'cost': 10}) + public_get_public_trades = publicGetPublicTrades = Entry('public/trades', 'public', 'GET', {'cost': 10}) + public_get_public_trades_symbol = publicGetPublicTradesSymbol = Entry('public/trades/{symbol}', 'public', 'GET', {'cost': 10}) + public_get_public_orderbook = publicGetPublicOrderbook = Entry('public/orderbook', 'public', 'GET', {'cost': 10}) + public_get_public_orderbook_symbol = publicGetPublicOrderbookSymbol = Entry('public/orderbook/{symbol}', 'public', 'GET', {'cost': 10}) + public_get_public_candles = publicGetPublicCandles = Entry('public/candles', 'public', 'GET', {'cost': 10}) + public_get_public_candles_symbol = publicGetPublicCandlesSymbol = Entry('public/candles/{symbol}', 'public', 'GET', {'cost': 10}) + public_get_public_converted_candles = publicGetPublicConvertedCandles = Entry('public/converted/candles', 'public', 'GET', {'cost': 10}) + public_get_public_converted_candles_symbol = publicGetPublicConvertedCandlesSymbol = Entry('public/converted/candles/{symbol}', 'public', 'GET', {'cost': 10}) + public_get_public_futures_info = publicGetPublicFuturesInfo = Entry('public/futures/info', 'public', 'GET', {'cost': 10}) + public_get_public_futures_info_symbol = publicGetPublicFuturesInfoSymbol = Entry('public/futures/info/{symbol}', 'public', 'GET', {'cost': 10}) + public_get_public_futures_history_funding = publicGetPublicFuturesHistoryFunding = Entry('public/futures/history/funding', 'public', 'GET', {'cost': 10}) + public_get_public_futures_history_funding_symbol = publicGetPublicFuturesHistoryFundingSymbol = Entry('public/futures/history/funding/{symbol}', 'public', 'GET', {'cost': 10}) + public_get_public_futures_candles_index_price = publicGetPublicFuturesCandlesIndexPrice = Entry('public/futures/candles/index_price', 'public', 'GET', {'cost': 10}) + public_get_public_futures_candles_index_price_symbol = publicGetPublicFuturesCandlesIndexPriceSymbol = Entry('public/futures/candles/index_price/{symbol}', 'public', 'GET', {'cost': 10}) + public_get_public_futures_candles_mark_price = publicGetPublicFuturesCandlesMarkPrice = Entry('public/futures/candles/mark_price', 'public', 'GET', {'cost': 10}) + public_get_public_futures_candles_mark_price_symbol = publicGetPublicFuturesCandlesMarkPriceSymbol = Entry('public/futures/candles/mark_price/{symbol}', 'public', 'GET', {'cost': 10}) + public_get_public_futures_candles_premium_index = publicGetPublicFuturesCandlesPremiumIndex = Entry('public/futures/candles/premium_index', 'public', 'GET', {'cost': 10}) + public_get_public_futures_candles_premium_index_symbol = publicGetPublicFuturesCandlesPremiumIndexSymbol = Entry('public/futures/candles/premium_index/{symbol}', 'public', 'GET', {'cost': 10}) + public_get_public_futures_candles_open_interest = publicGetPublicFuturesCandlesOpenInterest = Entry('public/futures/candles/open_interest', 'public', 'GET', {'cost': 10}) + public_get_public_futures_candles_open_interest_symbol = publicGetPublicFuturesCandlesOpenInterestSymbol = Entry('public/futures/candles/open_interest/{symbol}', 'public', 'GET', {'cost': 10}) + private_get_spot_balance = privateGetSpotBalance = Entry('spot/balance', 'private', 'GET', {'cost': 15}) + private_get_spot_balance_currency = privateGetSpotBalanceCurrency = Entry('spot/balance/{currency}', 'private', 'GET', {'cost': 15}) + private_get_spot_order = privateGetSpotOrder = Entry('spot/order', 'private', 'GET', {'cost': 1}) + private_get_spot_order_client_order_id = privateGetSpotOrderClientOrderId = Entry('spot/order/{client_order_id}', 'private', 'GET', {'cost': 1}) + private_get_spot_fee = privateGetSpotFee = Entry('spot/fee', 'private', 'GET', {'cost': 15}) + private_get_spot_fee_symbol = privateGetSpotFeeSymbol = Entry('spot/fee/{symbol}', 'private', 'GET', {'cost': 15}) + private_get_spot_history_order = privateGetSpotHistoryOrder = Entry('spot/history/order', 'private', 'GET', {'cost': 15}) + private_get_spot_history_trade = privateGetSpotHistoryTrade = Entry('spot/history/trade', 'private', 'GET', {'cost': 15}) + private_get_margin_account = privateGetMarginAccount = Entry('margin/account', 'private', 'GET', {'cost': 1}) + private_get_margin_account_isolated_symbol = privateGetMarginAccountIsolatedSymbol = Entry('margin/account/isolated/{symbol}', 'private', 'GET', {'cost': 1}) + private_get_margin_account_cross_currency = privateGetMarginAccountCrossCurrency = Entry('margin/account/cross/{currency}', 'private', 'GET', {'cost': 1}) + private_get_margin_order = privateGetMarginOrder = Entry('margin/order', 'private', 'GET', {'cost': 1}) + private_get_margin_order_client_order_id = privateGetMarginOrderClientOrderId = Entry('margin/order/{client_order_id}', 'private', 'GET', {'cost': 1}) + private_get_margin_config = privateGetMarginConfig = Entry('margin/config', 'private', 'GET', {'cost': 15}) + private_get_margin_history_order = privateGetMarginHistoryOrder = Entry('margin/history/order', 'private', 'GET', {'cost': 15}) + private_get_margin_history_trade = privateGetMarginHistoryTrade = Entry('margin/history/trade', 'private', 'GET', {'cost': 15}) + private_get_margin_history_positions = privateGetMarginHistoryPositions = Entry('margin/history/positions', 'private', 'GET', {'cost': 15}) + private_get_margin_history_clearing = privateGetMarginHistoryClearing = Entry('margin/history/clearing', 'private', 'GET', {'cost': 15}) + private_get_futures_balance = privateGetFuturesBalance = Entry('futures/balance', 'private', 'GET', {'cost': 15}) + private_get_futures_balance_currency = privateGetFuturesBalanceCurrency = Entry('futures/balance/{currency}', 'private', 'GET', {'cost': 15}) + private_get_futures_account = privateGetFuturesAccount = Entry('futures/account', 'private', 'GET', {'cost': 1}) + private_get_futures_account_isolated_symbol = privateGetFuturesAccountIsolatedSymbol = Entry('futures/account/isolated/{symbol}', 'private', 'GET', {'cost': 1}) + private_get_futures_order = privateGetFuturesOrder = Entry('futures/order', 'private', 'GET', {'cost': 1}) + private_get_futures_order_client_order_id = privateGetFuturesOrderClientOrderId = Entry('futures/order/{client_order_id}', 'private', 'GET', {'cost': 1}) + private_get_futures_config = privateGetFuturesConfig = Entry('futures/config', 'private', 'GET', {'cost': 15}) + private_get_futures_fee = privateGetFuturesFee = Entry('futures/fee', 'private', 'GET', {'cost': 15}) + private_get_futures_fee_symbol = privateGetFuturesFeeSymbol = Entry('futures/fee/{symbol}', 'private', 'GET', {'cost': 15}) + private_get_futures_history_order = privateGetFuturesHistoryOrder = Entry('futures/history/order', 'private', 'GET', {'cost': 15}) + private_get_futures_history_trade = privateGetFuturesHistoryTrade = Entry('futures/history/trade', 'private', 'GET', {'cost': 15}) + private_get_futures_history_positions = privateGetFuturesHistoryPositions = Entry('futures/history/positions', 'private', 'GET', {'cost': 15}) + private_get_futures_history_clearing = privateGetFuturesHistoryClearing = Entry('futures/history/clearing', 'private', 'GET', {'cost': 15}) + private_get_wallet_balance = privateGetWalletBalance = Entry('wallet/balance', 'private', 'GET', {'cost': 30}) + private_get_wallet_balance_currency = privateGetWalletBalanceCurrency = Entry('wallet/balance/{currency}', 'private', 'GET', {'cost': 30}) + private_get_wallet_crypto_address = privateGetWalletCryptoAddress = Entry('wallet/crypto/address', 'private', 'GET', {'cost': 30}) + private_get_wallet_crypto_address_recent_deposit = privateGetWalletCryptoAddressRecentDeposit = Entry('wallet/crypto/address/recent-deposit', 'private', 'GET', {'cost': 30}) + private_get_wallet_crypto_address_recent_withdraw = privateGetWalletCryptoAddressRecentWithdraw = Entry('wallet/crypto/address/recent-withdraw', 'private', 'GET', {'cost': 30}) + private_get_wallet_crypto_address_check_mine = privateGetWalletCryptoAddressCheckMine = Entry('wallet/crypto/address/check-mine', 'private', 'GET', {'cost': 30}) + private_get_wallet_transactions = privateGetWalletTransactions = Entry('wallet/transactions', 'private', 'GET', {'cost': 30}) + private_get_wallet_transactions_tx_id = privateGetWalletTransactionsTxId = Entry('wallet/transactions/{tx_id}', 'private', 'GET', {'cost': 30}) + private_get_wallet_crypto_fee_estimate = privateGetWalletCryptoFeeEstimate = Entry('wallet/crypto/fee/estimate', 'private', 'GET', {'cost': 30}) + private_get_wallet_airdrops = privateGetWalletAirdrops = Entry('wallet/airdrops', 'private', 'GET', {'cost': 30}) + private_get_wallet_amount_locks = privateGetWalletAmountLocks = Entry('wallet/amount-locks', 'private', 'GET', {'cost': 30}) + private_get_sub_account = privateGetSubAccount = Entry('sub-account', 'private', 'GET', {'cost': 15}) + private_get_sub_account_acl = privateGetSubAccountAcl = Entry('sub-account/acl', 'private', 'GET', {'cost': 15}) + private_get_sub_account_balance_subaccid = privateGetSubAccountBalanceSubAccID = Entry('sub-account/balance/{subAccID}', 'private', 'GET', {'cost': 15}) + private_get_sub_account_crypto_address_subaccid_currency = privateGetSubAccountCryptoAddressSubAccIDCurrency = Entry('sub-account/crypto/address/{subAccID}/{currency}', 'private', 'GET', {'cost': 15}) + private_post_spot_order = privatePostSpotOrder = Entry('spot/order', 'private', 'POST', {'cost': 1}) + private_post_spot_order_list = privatePostSpotOrderList = Entry('spot/order/list', 'private', 'POST', {'cost': 1}) + private_post_margin_order = privatePostMarginOrder = Entry('margin/order', 'private', 'POST', {'cost': 1}) + private_post_margin_order_list = privatePostMarginOrderList = Entry('margin/order/list', 'private', 'POST', {'cost': 1}) + private_post_futures_order = privatePostFuturesOrder = Entry('futures/order', 'private', 'POST', {'cost': 1}) + private_post_futures_order_list = privatePostFuturesOrderList = Entry('futures/order/list', 'private', 'POST', {'cost': 1}) + private_post_wallet_crypto_address = privatePostWalletCryptoAddress = Entry('wallet/crypto/address', 'private', 'POST', {'cost': 30}) + private_post_wallet_crypto_withdraw = privatePostWalletCryptoWithdraw = Entry('wallet/crypto/withdraw', 'private', 'POST', {'cost': 30}) + private_post_wallet_convert = privatePostWalletConvert = Entry('wallet/convert', 'private', 'POST', {'cost': 30}) + private_post_wallet_transfer = privatePostWalletTransfer = Entry('wallet/transfer', 'private', 'POST', {'cost': 30}) + private_post_wallet_internal_withdraw = privatePostWalletInternalWithdraw = Entry('wallet/internal/withdraw', 'private', 'POST', {'cost': 30}) + private_post_wallet_crypto_check_offchain_available = privatePostWalletCryptoCheckOffchainAvailable = Entry('wallet/crypto/check-offchain-available', 'private', 'POST', {'cost': 30}) + private_post_wallet_crypto_fees_estimate = privatePostWalletCryptoFeesEstimate = Entry('wallet/crypto/fees/estimate', 'private', 'POST', {'cost': 30}) + private_post_wallet_airdrops_id_claim = privatePostWalletAirdropsIdClaim = Entry('wallet/airdrops/{id}/claim', 'private', 'POST', {'cost': 30}) + private_post_sub_account_freeze = privatePostSubAccountFreeze = Entry('sub-account/freeze', 'private', 'POST', {'cost': 15}) + private_post_sub_account_activate = privatePostSubAccountActivate = Entry('sub-account/activate', 'private', 'POST', {'cost': 15}) + private_post_sub_account_transfer = privatePostSubAccountTransfer = Entry('sub-account/transfer', 'private', 'POST', {'cost': 15}) + private_post_sub_account_acl = privatePostSubAccountAcl = Entry('sub-account/acl', 'private', 'POST', {'cost': 15}) + private_patch_spot_order_client_order_id = privatePatchSpotOrderClientOrderId = Entry('spot/order/{client_order_id}', 'private', 'PATCH', {'cost': 1}) + private_patch_margin_order_client_order_id = privatePatchMarginOrderClientOrderId = Entry('margin/order/{client_order_id}', 'private', 'PATCH', {'cost': 1}) + private_patch_futures_order_client_order_id = privatePatchFuturesOrderClientOrderId = Entry('futures/order/{client_order_id}', 'private', 'PATCH', {'cost': 1}) + private_delete_spot_order = privateDeleteSpotOrder = Entry('spot/order', 'private', 'DELETE', {'cost': 1}) + private_delete_spot_order_client_order_id = privateDeleteSpotOrderClientOrderId = Entry('spot/order/{client_order_id}', 'private', 'DELETE', {'cost': 1}) + private_delete_margin_position = privateDeleteMarginPosition = Entry('margin/position', 'private', 'DELETE', {'cost': 1}) + private_delete_margin_position_isolated_symbol = privateDeleteMarginPositionIsolatedSymbol = Entry('margin/position/isolated/{symbol}', 'private', 'DELETE', {'cost': 1}) + private_delete_margin_order = privateDeleteMarginOrder = Entry('margin/order', 'private', 'DELETE', {'cost': 1}) + private_delete_margin_order_client_order_id = privateDeleteMarginOrderClientOrderId = Entry('margin/order/{client_order_id}', 'private', 'DELETE', {'cost': 1}) + private_delete_futures_position = privateDeleteFuturesPosition = Entry('futures/position', 'private', 'DELETE', {'cost': 1}) + private_delete_futures_position_margin_mode_symbol = privateDeleteFuturesPositionMarginModeSymbol = Entry('futures/position/{margin_mode}/{symbol}', 'private', 'DELETE', {'cost': 1}) + private_delete_futures_order = privateDeleteFuturesOrder = Entry('futures/order', 'private', 'DELETE', {'cost': 1}) + private_delete_futures_order_client_order_id = privateDeleteFuturesOrderClientOrderId = Entry('futures/order/{client_order_id}', 'private', 'DELETE', {'cost': 1}) + private_delete_wallet_crypto_withdraw_id = privateDeleteWalletCryptoWithdrawId = Entry('wallet/crypto/withdraw/{id}', 'private', 'DELETE', {'cost': 30}) + private_put_margin_account_isolated_symbol = privatePutMarginAccountIsolatedSymbol = Entry('margin/account/isolated/{symbol}', 'private', 'PUT', {'cost': 1}) + private_put_futures_account_isolated_symbol = privatePutFuturesAccountIsolatedSymbol = Entry('futures/account/isolated/{symbol}', 'private', 'PUT', {'cost': 1}) + private_put_wallet_crypto_withdraw_id = privatePutWalletCryptoWithdrawId = Entry('wallet/crypto/withdraw/{id}', 'private', 'PUT', {'cost': 30}) diff --git a/ccxt/abstract/bigone.py b/ccxt/abstract/bigone.py new file mode 100644 index 0000000..b49c09f --- /dev/null +++ b/ccxt/abstract/bigone.py @@ -0,0 +1,45 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_ping = publicGetPing = Entry('ping', 'public', 'GET', {}) + public_get_asset_pairs = publicGetAssetPairs = Entry('asset_pairs', 'public', 'GET', {}) + public_get_asset_pairs_asset_pair_name_depth = publicGetAssetPairsAssetPairNameDepth = Entry('asset_pairs/{asset_pair_name}/depth', 'public', 'GET', {}) + public_get_asset_pairs_asset_pair_name_trades = publicGetAssetPairsAssetPairNameTrades = Entry('asset_pairs/{asset_pair_name}/trades', 'public', 'GET', {}) + public_get_asset_pairs_asset_pair_name_ticker = publicGetAssetPairsAssetPairNameTicker = Entry('asset_pairs/{asset_pair_name}/ticker', 'public', 'GET', {}) + public_get_asset_pairs_asset_pair_name_candles = publicGetAssetPairsAssetPairNameCandles = Entry('asset_pairs/{asset_pair_name}/candles', 'public', 'GET', {}) + public_get_asset_pairs_tickers = publicGetAssetPairsTickers = Entry('asset_pairs/tickers', 'public', 'GET', {}) + private_get_accounts = privateGetAccounts = Entry('accounts', 'private', 'GET', {}) + private_get_fund_accounts = privateGetFundAccounts = Entry('fund/accounts', 'private', 'GET', {}) + private_get_assets_asset_symbol_address = privateGetAssetsAssetSymbolAddress = Entry('assets/{asset_symbol}/address', 'private', 'GET', {}) + private_get_orders = privateGetOrders = Entry('orders', 'private', 'GET', {}) + private_get_orders_id = privateGetOrdersId = Entry('orders/{id}', 'private', 'GET', {}) + private_get_orders_multi = privateGetOrdersMulti = Entry('orders/multi', 'private', 'GET', {}) + private_get_trades = privateGetTrades = Entry('trades', 'private', 'GET', {}) + private_get_withdrawals = privateGetWithdrawals = Entry('withdrawals', 'private', 'GET', {}) + private_get_deposits = privateGetDeposits = Entry('deposits', 'private', 'GET', {}) + private_post_orders = privatePostOrders = Entry('orders', 'private', 'POST', {}) + private_post_orders_id_cancel = privatePostOrdersIdCancel = Entry('orders/{id}/cancel', 'private', 'POST', {}) + private_post_orders_cancel = privatePostOrdersCancel = Entry('orders/cancel', 'private', 'POST', {}) + private_post_withdrawals = privatePostWithdrawals = Entry('withdrawals', 'private', 'POST', {}) + private_post_transfer = privatePostTransfer = Entry('transfer', 'private', 'POST', {}) + contractpublic_get_symbols = contractPublicGetSymbols = Entry('symbols', 'contractPublic', 'GET', {}) + contractpublic_get_instruments = contractPublicGetInstruments = Entry('instruments', 'contractPublic', 'GET', {}) + contractpublic_get_depth_symbol_snapshot = contractPublicGetDepthSymbolSnapshot = Entry('depth@{symbol}/snapshot', 'contractPublic', 'GET', {}) + contractpublic_get_instruments_difference = contractPublicGetInstrumentsDifference = Entry('instruments/difference', 'contractPublic', 'GET', {}) + contractpublic_get_instruments_prices = contractPublicGetInstrumentsPrices = Entry('instruments/prices', 'contractPublic', 'GET', {}) + contractprivate_get_accounts = contractPrivateGetAccounts = Entry('accounts', 'contractPrivate', 'GET', {}) + contractprivate_get_orders_id = contractPrivateGetOrdersId = Entry('orders/{id}', 'contractPrivate', 'GET', {}) + contractprivate_get_orders = contractPrivateGetOrders = Entry('orders', 'contractPrivate', 'GET', {}) + contractprivate_get_orders_opening = contractPrivateGetOrdersOpening = Entry('orders/opening', 'contractPrivate', 'GET', {}) + contractprivate_get_orders_count = contractPrivateGetOrdersCount = Entry('orders/count', 'contractPrivate', 'GET', {}) + contractprivate_get_orders_opening_count = contractPrivateGetOrdersOpeningCount = Entry('orders/opening/count', 'contractPrivate', 'GET', {}) + contractprivate_get_trades = contractPrivateGetTrades = Entry('trades', 'contractPrivate', 'GET', {}) + contractprivate_get_trades_count = contractPrivateGetTradesCount = Entry('trades/count', 'contractPrivate', 'GET', {}) + contractprivate_post_orders = contractPrivatePostOrders = Entry('orders', 'contractPrivate', 'POST', {}) + contractprivate_post_orders_batch = contractPrivatePostOrdersBatch = Entry('orders/batch', 'contractPrivate', 'POST', {}) + contractprivate_put_positions_symbol_margin = contractPrivatePutPositionsSymbolMargin = Entry('positions/{symbol}/margin', 'contractPrivate', 'PUT', {}) + contractprivate_put_positions_symbol_risk_limit = contractPrivatePutPositionsSymbolRiskLimit = Entry('positions/{symbol}/risk-limit', 'contractPrivate', 'PUT', {}) + contractprivate_delete_orders_id = contractPrivateDeleteOrdersId = Entry('orders/{id}', 'contractPrivate', 'DELETE', {}) + contractprivate_delete_orders_batch = contractPrivateDeleteOrdersBatch = Entry('orders/batch', 'contractPrivate', 'DELETE', {}) + webexchange_get_v3_assets = webExchangeGetV3Assets = Entry('v3/assets', 'webExchange', 'GET', {}) diff --git a/ccxt/abstract/binance.py b/ccxt/abstract/binance.py new file mode 100644 index 0000000..7893621 --- /dev/null +++ b/ccxt/abstract/binance.py @@ -0,0 +1,771 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + sapi_get_copytrading_futures_userstatus = sapiGetCopyTradingFuturesUserStatus = Entry('copyTrading/futures/userStatus', 'sapi', 'GET', {'cost': 2}) + sapi_get_copytrading_futures_leadsymbol = sapiGetCopyTradingFuturesLeadSymbol = Entry('copyTrading/futures/leadSymbol', 'sapi', 'GET', {'cost': 2}) + sapi_get_system_status = sapiGetSystemStatus = Entry('system/status', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_accountsnapshot = sapiGetAccountSnapshot = Entry('accountSnapshot', 'sapi', 'GET', {'cost': 240}) + sapi_get_account_info = sapiGetAccountInfo = Entry('account/info', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_margin_asset = sapiGetMarginAsset = Entry('margin/asset', 'sapi', 'GET', {'cost': 1}) + sapi_get_margin_pair = sapiGetMarginPair = Entry('margin/pair', 'sapi', 'GET', {'cost': 1}) + sapi_get_margin_allassets = sapiGetMarginAllAssets = Entry('margin/allAssets', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_margin_allpairs = sapiGetMarginAllPairs = Entry('margin/allPairs', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_margin_priceindex = sapiGetMarginPriceIndex = Entry('margin/priceIndex', 'sapi', 'GET', {'cost': 1}) + sapi_get_spot_delist_schedule = sapiGetSpotDelistSchedule = Entry('spot/delist-schedule', 'sapi', 'GET', {'cost': 10}) + sapi_get_asset_assetdividend = sapiGetAssetAssetDividend = Entry('asset/assetDividend', 'sapi', 'GET', {'cost': 1}) + sapi_get_asset_dribblet = sapiGetAssetDribblet = Entry('asset/dribblet', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_asset_transfer = sapiGetAssetTransfer = Entry('asset/transfer', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_asset_assetdetail = sapiGetAssetAssetDetail = Entry('asset/assetDetail', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_asset_tradefee = sapiGetAssetTradeFee = Entry('asset/tradeFee', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_asset_ledger_transfer_cloud_mining_querybypage = sapiGetAssetLedgerTransferCloudMiningQueryByPage = Entry('asset/ledger-transfer/cloud-mining/queryByPage', 'sapi', 'GET', {'cost': 4.0002}) + sapi_get_asset_convert_transfer_querybypage = sapiGetAssetConvertTransferQueryByPage = Entry('asset/convert-transfer/queryByPage', 'sapi', 'GET', {'cost': 0.033335}) + sapi_get_asset_wallet_balance = sapiGetAssetWalletBalance = Entry('asset/wallet/balance', 'sapi', 'GET', {'cost': 6}) + sapi_get_asset_custody_transfer_history = sapiGetAssetCustodyTransferHistory = Entry('asset/custody/transfer-history', 'sapi', 'GET', {'cost': 6}) + sapi_get_margin_borrow_repay = sapiGetMarginBorrowRepay = Entry('margin/borrow-repay', 'sapi', 'GET', {'cost': 1}) + sapi_get_margin_loan = sapiGetMarginLoan = Entry('margin/loan', 'sapi', 'GET', {'cost': 1}) + sapi_get_margin_repay = sapiGetMarginRepay = Entry('margin/repay', 'sapi', 'GET', {'cost': 1}) + sapi_get_margin_account = sapiGetMarginAccount = Entry('margin/account', 'sapi', 'GET', {'cost': 1}) + sapi_get_margin_transfer = sapiGetMarginTransfer = Entry('margin/transfer', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_margin_interesthistory = sapiGetMarginInterestHistory = Entry('margin/interestHistory', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_margin_forceliquidationrec = sapiGetMarginForceLiquidationRec = Entry('margin/forceLiquidationRec', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_margin_order = sapiGetMarginOrder = Entry('margin/order', 'sapi', 'GET', {'cost': 1}) + sapi_get_margin_openorders = sapiGetMarginOpenOrders = Entry('margin/openOrders', 'sapi', 'GET', {'cost': 1}) + sapi_get_margin_allorders = sapiGetMarginAllOrders = Entry('margin/allOrders', 'sapi', 'GET', {'cost': 20}) + sapi_get_margin_mytrades = sapiGetMarginMyTrades = Entry('margin/myTrades', 'sapi', 'GET', {'cost': 1}) + sapi_get_margin_maxborrowable = sapiGetMarginMaxBorrowable = Entry('margin/maxBorrowable', 'sapi', 'GET', {'cost': 5}) + sapi_get_margin_maxtransferable = sapiGetMarginMaxTransferable = Entry('margin/maxTransferable', 'sapi', 'GET', {'cost': 5}) + sapi_get_margin_tradecoeff = sapiGetMarginTradeCoeff = Entry('margin/tradeCoeff', 'sapi', 'GET', {'cost': 1}) + sapi_get_margin_isolated_transfer = sapiGetMarginIsolatedTransfer = Entry('margin/isolated/transfer', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_margin_isolated_account = sapiGetMarginIsolatedAccount = Entry('margin/isolated/account', 'sapi', 'GET', {'cost': 1}) + sapi_get_margin_isolated_pair = sapiGetMarginIsolatedPair = Entry('margin/isolated/pair', 'sapi', 'GET', {'cost': 1}) + sapi_get_margin_isolated_allpairs = sapiGetMarginIsolatedAllPairs = Entry('margin/isolated/allPairs', 'sapi', 'GET', {'cost': 1}) + sapi_get_margin_isolated_accountlimit = sapiGetMarginIsolatedAccountLimit = Entry('margin/isolated/accountLimit', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_margin_interestratehistory = sapiGetMarginInterestRateHistory = Entry('margin/interestRateHistory', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_margin_orderlist = sapiGetMarginOrderList = Entry('margin/orderList', 'sapi', 'GET', {'cost': 1}) + sapi_get_margin_allorderlist = sapiGetMarginAllOrderList = Entry('margin/allOrderList', 'sapi', 'GET', {'cost': 20}) + sapi_get_margin_openorderlist = sapiGetMarginOpenOrderList = Entry('margin/openOrderList', 'sapi', 'GET', {'cost': 1}) + sapi_get_margin_crossmargindata = sapiGetMarginCrossMarginData = Entry('margin/crossMarginData', 'sapi', 'GET', {'cost': 0.1, 'noCoin': 0.5}) + sapi_get_margin_isolatedmargindata = sapiGetMarginIsolatedMarginData = Entry('margin/isolatedMarginData', 'sapi', 'GET', {'cost': 0.1, 'noCoin': 1}) + sapi_get_margin_isolatedmargintier = sapiGetMarginIsolatedMarginTier = Entry('margin/isolatedMarginTier', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_margin_ratelimit_order = sapiGetMarginRateLimitOrder = Entry('margin/rateLimit/order', 'sapi', 'GET', {'cost': 2}) + sapi_get_margin_dribblet = sapiGetMarginDribblet = Entry('margin/dribblet', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_margin_dust = sapiGetMarginDust = Entry('margin/dust', 'sapi', 'GET', {'cost': 20.001}) + sapi_get_margin_crossmargincollateralratio = sapiGetMarginCrossMarginCollateralRatio = Entry('margin/crossMarginCollateralRatio', 'sapi', 'GET', {'cost': 10}) + sapi_get_margin_exchange_small_liability = sapiGetMarginExchangeSmallLiability = Entry('margin/exchange-small-liability', 'sapi', 'GET', {'cost': 0.6667}) + sapi_get_margin_exchange_small_liability_history = sapiGetMarginExchangeSmallLiabilityHistory = Entry('margin/exchange-small-liability-history', 'sapi', 'GET', {'cost': 0.6667}) + sapi_get_margin_next_hourly_interest_rate = sapiGetMarginNextHourlyInterestRate = Entry('margin/next-hourly-interest-rate', 'sapi', 'GET', {'cost': 0.6667}) + sapi_get_margin_capital_flow = sapiGetMarginCapitalFlow = Entry('margin/capital-flow', 'sapi', 'GET', {'cost': 10}) + sapi_get_margin_delist_schedule = sapiGetMarginDelistSchedule = Entry('margin/delist-schedule', 'sapi', 'GET', {'cost': 10}) + sapi_get_margin_available_inventory = sapiGetMarginAvailableInventory = Entry('margin/available-inventory', 'sapi', 'GET', {'cost': 0.3334}) + sapi_get_margin_leveragebracket = sapiGetMarginLeverageBracket = Entry('margin/leverageBracket', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_loan_vip_loanable_data = sapiGetLoanVipLoanableData = Entry('loan/vip/loanable/data', 'sapi', 'GET', {'cost': 40}) + sapi_get_loan_vip_collateral_data = sapiGetLoanVipCollateralData = Entry('loan/vip/collateral/data', 'sapi', 'GET', {'cost': 40}) + sapi_get_loan_vip_request_data = sapiGetLoanVipRequestData = Entry('loan/vip/request/data', 'sapi', 'GET', {'cost': 2.6668}) + sapi_get_loan_vip_request_interestrate = sapiGetLoanVipRequestInterestRate = Entry('loan/vip/request/interestRate', 'sapi', 'GET', {'cost': 2.6668}) + sapi_get_loan_income = sapiGetLoanIncome = Entry('loan/income', 'sapi', 'GET', {'cost': 40.002}) + sapi_get_loan_ongoing_orders = sapiGetLoanOngoingOrders = Entry('loan/ongoing/orders', 'sapi', 'GET', {'cost': 40}) + sapi_get_loan_ltv_adjustment_history = sapiGetLoanLtvAdjustmentHistory = Entry('loan/ltv/adjustment/history', 'sapi', 'GET', {'cost': 40}) + sapi_get_loan_borrow_history = sapiGetLoanBorrowHistory = Entry('loan/borrow/history', 'sapi', 'GET', {'cost': 40}) + sapi_get_loan_repay_history = sapiGetLoanRepayHistory = Entry('loan/repay/history', 'sapi', 'GET', {'cost': 40}) + sapi_get_loan_loanable_data = sapiGetLoanLoanableData = Entry('loan/loanable/data', 'sapi', 'GET', {'cost': 40}) + sapi_get_loan_collateral_data = sapiGetLoanCollateralData = Entry('loan/collateral/data', 'sapi', 'GET', {'cost': 40}) + sapi_get_loan_repay_collateral_rate = sapiGetLoanRepayCollateralRate = Entry('loan/repay/collateral/rate', 'sapi', 'GET', {'cost': 600}) + sapi_get_loan_flexible_ongoing_orders = sapiGetLoanFlexibleOngoingOrders = Entry('loan/flexible/ongoing/orders', 'sapi', 'GET', {'cost': 30}) + sapi_get_loan_flexible_borrow_history = sapiGetLoanFlexibleBorrowHistory = Entry('loan/flexible/borrow/history', 'sapi', 'GET', {'cost': 40}) + sapi_get_loan_flexible_repay_history = sapiGetLoanFlexibleRepayHistory = Entry('loan/flexible/repay/history', 'sapi', 'GET', {'cost': 40}) + sapi_get_loan_flexible_ltv_adjustment_history = sapiGetLoanFlexibleLtvAdjustmentHistory = Entry('loan/flexible/ltv/adjustment/history', 'sapi', 'GET', {'cost': 40}) + sapi_get_loan_vip_ongoing_orders = sapiGetLoanVipOngoingOrders = Entry('loan/vip/ongoing/orders', 'sapi', 'GET', {'cost': 40}) + sapi_get_loan_vip_repay_history = sapiGetLoanVipRepayHistory = Entry('loan/vip/repay/history', 'sapi', 'GET', {'cost': 40}) + sapi_get_loan_vip_collateral_account = sapiGetLoanVipCollateralAccount = Entry('loan/vip/collateral/account', 'sapi', 'GET', {'cost': 600}) + sapi_get_fiat_orders = sapiGetFiatOrders = Entry('fiat/orders', 'sapi', 'GET', {'cost': 600.03}) + sapi_get_fiat_payments = sapiGetFiatPayments = Entry('fiat/payments', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_futures_transfer = sapiGetFuturesTransfer = Entry('futures/transfer', 'sapi', 'GET', {'cost': 1}) + sapi_get_futures_histdatalink = sapiGetFuturesHistDataLink = Entry('futures/histDataLink', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_rebate_taxquery = sapiGetRebateTaxQuery = Entry('rebate/taxQuery', 'sapi', 'GET', {'cost': 80.004}) + sapi_get_capital_config_getall = sapiGetCapitalConfigGetall = Entry('capital/config/getall', 'sapi', 'GET', {'cost': 1}) + sapi_get_capital_deposit_address = sapiGetCapitalDepositAddress = Entry('capital/deposit/address', 'sapi', 'GET', {'cost': 1}) + sapi_get_capital_deposit_address_list = sapiGetCapitalDepositAddressList = Entry('capital/deposit/address/list', 'sapi', 'GET', {'cost': 1}) + sapi_get_capital_deposit_hisrec = sapiGetCapitalDepositHisrec = Entry('capital/deposit/hisrec', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_capital_deposit_subaddress = sapiGetCapitalDepositSubAddress = Entry('capital/deposit/subAddress', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_capital_deposit_subhisrec = sapiGetCapitalDepositSubHisrec = Entry('capital/deposit/subHisrec', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_capital_withdraw_history = sapiGetCapitalWithdrawHistory = Entry('capital/withdraw/history', 'sapi', 'GET', {'cost': 2}) + sapi_get_capital_withdraw_address_list = sapiGetCapitalWithdrawAddressList = Entry('capital/withdraw/address/list', 'sapi', 'GET', {'cost': 10}) + sapi_get_capital_contract_convertible_coins = sapiGetCapitalContractConvertibleCoins = Entry('capital/contract/convertible-coins', 'sapi', 'GET', {'cost': 4.0002}) + sapi_get_convert_tradeflow = sapiGetConvertTradeFlow = Entry('convert/tradeFlow', 'sapi', 'GET', {'cost': 20.001}) + sapi_get_convert_exchangeinfo = sapiGetConvertExchangeInfo = Entry('convert/exchangeInfo', 'sapi', 'GET', {'cost': 50}) + sapi_get_convert_assetinfo = sapiGetConvertAssetInfo = Entry('convert/assetInfo', 'sapi', 'GET', {'cost': 10}) + sapi_get_convert_orderstatus = sapiGetConvertOrderStatus = Entry('convert/orderStatus', 'sapi', 'GET', {'cost': 0.6667}) + sapi_get_convert_limit_queryopenorders = sapiGetConvertLimitQueryOpenOrders = Entry('convert/limit/queryOpenOrders', 'sapi', 'GET', {'cost': 20.001}) + sapi_get_account_status = sapiGetAccountStatus = Entry('account/status', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_account_apitradingstatus = sapiGetAccountApiTradingStatus = Entry('account/apiTradingStatus', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_account_apirestrictions_iprestriction = sapiGetAccountApiRestrictionsIpRestriction = Entry('account/apiRestrictions/ipRestriction', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_bnbburn = sapiGetBnbBurn = Entry('bnbBurn', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_sub_account_futures_account = sapiGetSubAccountFuturesAccount = Entry('sub-account/futures/account', 'sapi', 'GET', {'cost': 1}) + sapi_get_sub_account_futures_accountsummary = sapiGetSubAccountFuturesAccountSummary = Entry('sub-account/futures/accountSummary', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_sub_account_futures_positionrisk = sapiGetSubAccountFuturesPositionRisk = Entry('sub-account/futures/positionRisk', 'sapi', 'GET', {'cost': 1}) + sapi_get_sub_account_futures_internaltransfer = sapiGetSubAccountFuturesInternalTransfer = Entry('sub-account/futures/internalTransfer', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_sub_account_list = sapiGetSubAccountList = Entry('sub-account/list', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_sub_account_margin_account = sapiGetSubAccountMarginAccount = Entry('sub-account/margin/account', 'sapi', 'GET', {'cost': 1}) + sapi_get_sub_account_margin_accountsummary = sapiGetSubAccountMarginAccountSummary = Entry('sub-account/margin/accountSummary', 'sapi', 'GET', {'cost': 1}) + sapi_get_sub_account_spotsummary = sapiGetSubAccountSpotSummary = Entry('sub-account/spotSummary', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_sub_account_status = sapiGetSubAccountStatus = Entry('sub-account/status', 'sapi', 'GET', {'cost': 1}) + sapi_get_sub_account_sub_transfer_history = sapiGetSubAccountSubTransferHistory = Entry('sub-account/sub/transfer/history', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_sub_account_transfer_subuserhistory = sapiGetSubAccountTransferSubUserHistory = Entry('sub-account/transfer/subUserHistory', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_sub_account_universaltransfer = sapiGetSubAccountUniversalTransfer = Entry('sub-account/universalTransfer', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_sub_account_apirestrictions_iprestriction_thirdpartylist = sapiGetSubAccountApiRestrictionsIpRestrictionThirdPartyList = Entry('sub-account/apiRestrictions/ipRestriction/thirdPartyList', 'sapi', 'GET', {'cost': 1}) + sapi_get_sub_account_transaction_statistics = sapiGetSubAccountTransactionStatistics = Entry('sub-account/transaction-statistics', 'sapi', 'GET', {'cost': 0.40002}) + sapi_get_sub_account_subaccountapi_iprestriction = sapiGetSubAccountSubAccountApiIpRestriction = Entry('sub-account/subAccountApi/ipRestriction', 'sapi', 'GET', {'cost': 20.001}) + sapi_get_managed_subaccount_asset = sapiGetManagedSubaccountAsset = Entry('managed-subaccount/asset', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_managed_subaccount_accountsnapshot = sapiGetManagedSubaccountAccountSnapshot = Entry('managed-subaccount/accountSnapshot', 'sapi', 'GET', {'cost': 240}) + sapi_get_managed_subaccount_querytranslogforinvestor = sapiGetManagedSubaccountQueryTransLogForInvestor = Entry('managed-subaccount/queryTransLogForInvestor', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_managed_subaccount_querytranslogfortradeparent = sapiGetManagedSubaccountQueryTransLogForTradeParent = Entry('managed-subaccount/queryTransLogForTradeParent', 'sapi', 'GET', {'cost': 0.40002}) + sapi_get_managed_subaccount_fetch_future_asset = sapiGetManagedSubaccountFetchFutureAsset = Entry('managed-subaccount/fetch-future-asset', 'sapi', 'GET', {'cost': 0.40002}) + sapi_get_managed_subaccount_marginasset = sapiGetManagedSubaccountMarginAsset = Entry('managed-subaccount/marginAsset', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_managed_subaccount_info = sapiGetManagedSubaccountInfo = Entry('managed-subaccount/info', 'sapi', 'GET', {'cost': 0.40002}) + sapi_get_managed_subaccount_deposit_address = sapiGetManagedSubaccountDepositAddress = Entry('managed-subaccount/deposit/address', 'sapi', 'GET', {'cost': 0.006667}) + sapi_get_managed_subaccount_query_trans_log = sapiGetManagedSubaccountQueryTransLog = Entry('managed-subaccount/query-trans-log', 'sapi', 'GET', {'cost': 0.40002}) + sapi_get_lending_daily_product_list = sapiGetLendingDailyProductList = Entry('lending/daily/product/list', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_daily_userleftquota = sapiGetLendingDailyUserLeftQuota = Entry('lending/daily/userLeftQuota', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_daily_userredemptionquota = sapiGetLendingDailyUserRedemptionQuota = Entry('lending/daily/userRedemptionQuota', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_daily_token_position = sapiGetLendingDailyTokenPosition = Entry('lending/daily/token/position', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_union_account = sapiGetLendingUnionAccount = Entry('lending/union/account', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_union_purchaserecord = sapiGetLendingUnionPurchaseRecord = Entry('lending/union/purchaseRecord', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_union_redemptionrecord = sapiGetLendingUnionRedemptionRecord = Entry('lending/union/redemptionRecord', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_union_interesthistory = sapiGetLendingUnionInterestHistory = Entry('lending/union/interestHistory', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_project_list = sapiGetLendingProjectList = Entry('lending/project/list', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_project_position_list = sapiGetLendingProjectPositionList = Entry('lending/project/position/list', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_eth_staking_eth_history_stakinghistory = sapiGetEthStakingEthHistoryStakingHistory = Entry('eth-staking/eth/history/stakingHistory', 'sapi', 'GET', {'cost': 15}) + sapi_get_eth_staking_eth_history_redemptionhistory = sapiGetEthStakingEthHistoryRedemptionHistory = Entry('eth-staking/eth/history/redemptionHistory', 'sapi', 'GET', {'cost': 15}) + sapi_get_eth_staking_eth_history_rewardshistory = sapiGetEthStakingEthHistoryRewardsHistory = Entry('eth-staking/eth/history/rewardsHistory', 'sapi', 'GET', {'cost': 15}) + sapi_get_eth_staking_eth_quota = sapiGetEthStakingEthQuota = Entry('eth-staking/eth/quota', 'sapi', 'GET', {'cost': 15}) + sapi_get_eth_staking_eth_history_ratehistory = sapiGetEthStakingEthHistoryRateHistory = Entry('eth-staking/eth/history/rateHistory', 'sapi', 'GET', {'cost': 15}) + sapi_get_eth_staking_account = sapiGetEthStakingAccount = Entry('eth-staking/account', 'sapi', 'GET', {'cost': 15}) + sapi_get_eth_staking_wbeth_history_wraphistory = sapiGetEthStakingWbethHistoryWrapHistory = Entry('eth-staking/wbeth/history/wrapHistory', 'sapi', 'GET', {'cost': 15}) + sapi_get_eth_staking_wbeth_history_unwraphistory = sapiGetEthStakingWbethHistoryUnwrapHistory = Entry('eth-staking/wbeth/history/unwrapHistory', 'sapi', 'GET', {'cost': 15}) + sapi_get_eth_staking_eth_history_wbethrewardshistory = sapiGetEthStakingEthHistoryWbethRewardsHistory = Entry('eth-staking/eth/history/wbethRewardsHistory', 'sapi', 'GET', {'cost': 15}) + sapi_get_sol_staking_sol_history_stakinghistory = sapiGetSolStakingSolHistoryStakingHistory = Entry('sol-staking/sol/history/stakingHistory', 'sapi', 'GET', {'cost': 15}) + sapi_get_sol_staking_sol_history_redemptionhistory = sapiGetSolStakingSolHistoryRedemptionHistory = Entry('sol-staking/sol/history/redemptionHistory', 'sapi', 'GET', {'cost': 15}) + sapi_get_sol_staking_sol_history_bnsolrewardshistory = sapiGetSolStakingSolHistoryBnsolRewardsHistory = Entry('sol-staking/sol/history/bnsolRewardsHistory', 'sapi', 'GET', {'cost': 15}) + sapi_get_sol_staking_sol_history_ratehistory = sapiGetSolStakingSolHistoryRateHistory = Entry('sol-staking/sol/history/rateHistory', 'sapi', 'GET', {'cost': 15}) + sapi_get_sol_staking_account = sapiGetSolStakingAccount = Entry('sol-staking/account', 'sapi', 'GET', {'cost': 15}) + sapi_get_sol_staking_sol_quota = sapiGetSolStakingSolQuota = Entry('sol-staking/sol/quota', 'sapi', 'GET', {'cost': 15}) + sapi_get_mining_pub_algolist = sapiGetMiningPubAlgoList = Entry('mining/pub/algoList', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_mining_pub_coinlist = sapiGetMiningPubCoinList = Entry('mining/pub/coinList', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_mining_worker_detail = sapiGetMiningWorkerDetail = Entry('mining/worker/detail', 'sapi', 'GET', {'cost': 0.5}) + sapi_get_mining_worker_list = sapiGetMiningWorkerList = Entry('mining/worker/list', 'sapi', 'GET', {'cost': 0.5}) + sapi_get_mining_payment_list = sapiGetMiningPaymentList = Entry('mining/payment/list', 'sapi', 'GET', {'cost': 0.5}) + sapi_get_mining_statistics_user_status = sapiGetMiningStatisticsUserStatus = Entry('mining/statistics/user/status', 'sapi', 'GET', {'cost': 0.5}) + sapi_get_mining_statistics_user_list = sapiGetMiningStatisticsUserList = Entry('mining/statistics/user/list', 'sapi', 'GET', {'cost': 0.5}) + sapi_get_mining_payment_uid = sapiGetMiningPaymentUid = Entry('mining/payment/uid', 'sapi', 'GET', {'cost': 0.5}) + sapi_get_bswap_pools = sapiGetBswapPools = Entry('bswap/pools', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_bswap_liquidity = sapiGetBswapLiquidity = Entry('bswap/liquidity', 'sapi', 'GET', {'cost': 0.1, 'noPoolId': 1}) + sapi_get_bswap_liquidityops = sapiGetBswapLiquidityOps = Entry('bswap/liquidityOps', 'sapi', 'GET', {'cost': 20.001}) + sapi_get_bswap_quote = sapiGetBswapQuote = Entry('bswap/quote', 'sapi', 'GET', {'cost': 1.00005}) + sapi_get_bswap_swap = sapiGetBswapSwap = Entry('bswap/swap', 'sapi', 'GET', {'cost': 20.001}) + sapi_get_bswap_poolconfigure = sapiGetBswapPoolConfigure = Entry('bswap/poolConfigure', 'sapi', 'GET', {'cost': 1.00005}) + sapi_get_bswap_addliquiditypreview = sapiGetBswapAddLiquidityPreview = Entry('bswap/addLiquidityPreview', 'sapi', 'GET', {'cost': 1.00005}) + sapi_get_bswap_removeliquiditypreview = sapiGetBswapRemoveLiquidityPreview = Entry('bswap/removeLiquidityPreview', 'sapi', 'GET', {'cost': 1.00005}) + sapi_get_bswap_unclaimedrewards = sapiGetBswapUnclaimedRewards = Entry('bswap/unclaimedRewards', 'sapi', 'GET', {'cost': 6.667}) + sapi_get_bswap_claimedhistory = sapiGetBswapClaimedHistory = Entry('bswap/claimedHistory', 'sapi', 'GET', {'cost': 6.667}) + sapi_get_blvt_tokeninfo = sapiGetBlvtTokenInfo = Entry('blvt/tokenInfo', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_blvt_subscribe_record = sapiGetBlvtSubscribeRecord = Entry('blvt/subscribe/record', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_blvt_redeem_record = sapiGetBlvtRedeemRecord = Entry('blvt/redeem/record', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_blvt_userlimit = sapiGetBlvtUserLimit = Entry('blvt/userLimit', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_apireferral_ifnewuser = sapiGetApiReferralIfNewUser = Entry('apiReferral/ifNewUser', 'sapi', 'GET', {'cost': 1}) + sapi_get_apireferral_customization = sapiGetApiReferralCustomization = Entry('apiReferral/customization', 'sapi', 'GET', {'cost': 1}) + sapi_get_apireferral_usercustomization = sapiGetApiReferralUserCustomization = Entry('apiReferral/userCustomization', 'sapi', 'GET', {'cost': 1}) + sapi_get_apireferral_rebate_recentrecord = sapiGetApiReferralRebateRecentRecord = Entry('apiReferral/rebate/recentRecord', 'sapi', 'GET', {'cost': 1}) + sapi_get_apireferral_rebate_historicalrecord = sapiGetApiReferralRebateHistoricalRecord = Entry('apiReferral/rebate/historicalRecord', 'sapi', 'GET', {'cost': 1}) + sapi_get_apireferral_kickback_recentrecord = sapiGetApiReferralKickbackRecentRecord = Entry('apiReferral/kickback/recentRecord', 'sapi', 'GET', {'cost': 1}) + sapi_get_apireferral_kickback_historicalrecord = sapiGetApiReferralKickbackHistoricalRecord = Entry('apiReferral/kickback/historicalRecord', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_subaccountapi = sapiGetBrokerSubAccountApi = Entry('broker/subAccountApi', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_subaccount = sapiGetBrokerSubAccount = Entry('broker/subAccount', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_subaccountapi_commission_futures = sapiGetBrokerSubAccountApiCommissionFutures = Entry('broker/subAccountApi/commission/futures', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_subaccountapi_commission_coinfutures = sapiGetBrokerSubAccountApiCommissionCoinFutures = Entry('broker/subAccountApi/commission/coinFutures', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_info = sapiGetBrokerInfo = Entry('broker/info', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_transfer = sapiGetBrokerTransfer = Entry('broker/transfer', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_transfer_futures = sapiGetBrokerTransferFutures = Entry('broker/transfer/futures', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_rebate_recentrecord = sapiGetBrokerRebateRecentRecord = Entry('broker/rebate/recentRecord', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_rebate_historicalrecord = sapiGetBrokerRebateHistoricalRecord = Entry('broker/rebate/historicalRecord', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_subaccount_bnbburn_status = sapiGetBrokerSubAccountBnbBurnStatus = Entry('broker/subAccount/bnbBurn/status', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_subaccount_deposithist = sapiGetBrokerSubAccountDepositHist = Entry('broker/subAccount/depositHist', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_subaccount_spotsummary = sapiGetBrokerSubAccountSpotSummary = Entry('broker/subAccount/spotSummary', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_subaccount_marginsummary = sapiGetBrokerSubAccountMarginSummary = Entry('broker/subAccount/marginSummary', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_subaccount_futuressummary = sapiGetBrokerSubAccountFuturesSummary = Entry('broker/subAccount/futuresSummary', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_rebate_futures_recentrecord = sapiGetBrokerRebateFuturesRecentRecord = Entry('broker/rebate/futures/recentRecord', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_subaccountapi_iprestriction = sapiGetBrokerSubAccountApiIpRestriction = Entry('broker/subAccountApi/ipRestriction', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_universaltransfer = sapiGetBrokerUniversalTransfer = Entry('broker/universalTransfer', 'sapi', 'GET', {'cost': 1}) + sapi_get_account_apirestrictions = sapiGetAccountApiRestrictions = Entry('account/apiRestrictions', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_c2c_ordermatch_listuserorderhistory = sapiGetC2cOrderMatchListUserOrderHistory = Entry('c2c/orderMatch/listUserOrderHistory', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_nft_history_transactions = sapiGetNftHistoryTransactions = Entry('nft/history/transactions', 'sapi', 'GET', {'cost': 20.001}) + sapi_get_nft_history_deposit = sapiGetNftHistoryDeposit = Entry('nft/history/deposit', 'sapi', 'GET', {'cost': 20.001}) + sapi_get_nft_history_withdraw = sapiGetNftHistoryWithdraw = Entry('nft/history/withdraw', 'sapi', 'GET', {'cost': 20.001}) + sapi_get_nft_user_getasset = sapiGetNftUserGetAsset = Entry('nft/user/getAsset', 'sapi', 'GET', {'cost': 20.001}) + sapi_get_pay_transactions = sapiGetPayTransactions = Entry('pay/transactions', 'sapi', 'GET', {'cost': 20.001}) + sapi_get_giftcard_verify = sapiGetGiftcardVerify = Entry('giftcard/verify', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_giftcard_cryptography_rsa_public_key = sapiGetGiftcardCryptographyRsaPublicKey = Entry('giftcard/cryptography/rsa-public-key', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_giftcard_buycode_token_limit = sapiGetGiftcardBuyCodeTokenLimit = Entry('giftcard/buyCode/token-limit', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_algo_spot_openorders = sapiGetAlgoSpotOpenOrders = Entry('algo/spot/openOrders', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_algo_spot_historicalorders = sapiGetAlgoSpotHistoricalOrders = Entry('algo/spot/historicalOrders', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_algo_spot_suborders = sapiGetAlgoSpotSubOrders = Entry('algo/spot/subOrders', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_algo_futures_openorders = sapiGetAlgoFuturesOpenOrders = Entry('algo/futures/openOrders', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_algo_futures_historicalorders = sapiGetAlgoFuturesHistoricalOrders = Entry('algo/futures/historicalOrders', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_algo_futures_suborders = sapiGetAlgoFuturesSubOrders = Entry('algo/futures/subOrders', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_portfolio_account = sapiGetPortfolioAccount = Entry('portfolio/account', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_portfolio_collateralrate = sapiGetPortfolioCollateralRate = Entry('portfolio/collateralRate', 'sapi', 'GET', {'cost': 5}) + sapi_get_portfolio_pmloan = sapiGetPortfolioPmLoan = Entry('portfolio/pmLoan', 'sapi', 'GET', {'cost': 3.3335}) + sapi_get_portfolio_interest_history = sapiGetPortfolioInterestHistory = Entry('portfolio/interest-history', 'sapi', 'GET', {'cost': 0.6667}) + sapi_get_portfolio_asset_index_price = sapiGetPortfolioAssetIndexPrice = Entry('portfolio/asset-index-price', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_portfolio_repay_futures_switch = sapiGetPortfolioRepayFuturesSwitch = Entry('portfolio/repay-futures-switch', 'sapi', 'GET', {'cost': 3}) + sapi_get_portfolio_margin_asset_leverage = sapiGetPortfolioMarginAssetLeverage = Entry('portfolio/margin-asset-leverage', 'sapi', 'GET', {'cost': 5}) + sapi_get_portfolio_balance = sapiGetPortfolioBalance = Entry('portfolio/balance', 'sapi', 'GET', {'cost': 2}) + sapi_get_portfolio_negative_balance_exchange_record = sapiGetPortfolioNegativeBalanceExchangeRecord = Entry('portfolio/negative-balance-exchange-record', 'sapi', 'GET', {'cost': 2}) + sapi_get_portfolio_pmloan_history = sapiGetPortfolioPmloanHistory = Entry('portfolio/pmloan-history', 'sapi', 'GET', {'cost': 5}) + sapi_get_portfolio_earn_asset_balance = sapiGetPortfolioEarnAssetBalance = Entry('portfolio/earn-asset-balance', 'sapi', 'GET', {'cost': 150}) + sapi_get_staking_productlist = sapiGetStakingProductList = Entry('staking/productList', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_staking_position = sapiGetStakingPosition = Entry('staking/position', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_staking_stakingrecord = sapiGetStakingStakingRecord = Entry('staking/stakingRecord', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_staking_personalleftquota = sapiGetStakingPersonalLeftQuota = Entry('staking/personalLeftQuota', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_auto_invest_target_asset_list = sapiGetLendingAutoInvestTargetAssetList = Entry('lending/auto-invest/target-asset/list', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_auto_invest_target_asset_roi_list = sapiGetLendingAutoInvestTargetAssetRoiList = Entry('lending/auto-invest/target-asset/roi/list', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_auto_invest_all_asset = sapiGetLendingAutoInvestAllAsset = Entry('lending/auto-invest/all/asset', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_auto_invest_source_asset_list = sapiGetLendingAutoInvestSourceAssetList = Entry('lending/auto-invest/source-asset/list', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_auto_invest_plan_list = sapiGetLendingAutoInvestPlanList = Entry('lending/auto-invest/plan/list', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_auto_invest_plan_id = sapiGetLendingAutoInvestPlanId = Entry('lending/auto-invest/plan/id', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_auto_invest_history_list = sapiGetLendingAutoInvestHistoryList = Entry('lending/auto-invest/history/list', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_auto_invest_index_info = sapiGetLendingAutoInvestIndexInfo = Entry('lending/auto-invest/index/info', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_auto_invest_index_user_summary = sapiGetLendingAutoInvestIndexUserSummary = Entry('lending/auto-invest/index/user-summary', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_auto_invest_one_off_status = sapiGetLendingAutoInvestOneOffStatus = Entry('lending/auto-invest/one-off/status', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_auto_invest_redeem_history = sapiGetLendingAutoInvestRedeemHistory = Entry('lending/auto-invest/redeem/history', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_auto_invest_rebalance_history = sapiGetLendingAutoInvestRebalanceHistory = Entry('lending/auto-invest/rebalance/history', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_simple_earn_flexible_list = sapiGetSimpleEarnFlexibleList = Entry('simple-earn/flexible/list', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_locked_list = sapiGetSimpleEarnLockedList = Entry('simple-earn/locked/list', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_flexible_personalleftquota = sapiGetSimpleEarnFlexiblePersonalLeftQuota = Entry('simple-earn/flexible/personalLeftQuota', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_locked_personalleftquota = sapiGetSimpleEarnLockedPersonalLeftQuota = Entry('simple-earn/locked/personalLeftQuota', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_flexible_subscriptionpreview = sapiGetSimpleEarnFlexibleSubscriptionPreview = Entry('simple-earn/flexible/subscriptionPreview', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_locked_subscriptionpreview = sapiGetSimpleEarnLockedSubscriptionPreview = Entry('simple-earn/locked/subscriptionPreview', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_flexible_history_ratehistory = sapiGetSimpleEarnFlexibleHistoryRateHistory = Entry('simple-earn/flexible/history/rateHistory', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_flexible_position = sapiGetSimpleEarnFlexiblePosition = Entry('simple-earn/flexible/position', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_locked_position = sapiGetSimpleEarnLockedPosition = Entry('simple-earn/locked/position', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_account = sapiGetSimpleEarnAccount = Entry('simple-earn/account', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_flexible_history_subscriptionrecord = sapiGetSimpleEarnFlexibleHistorySubscriptionRecord = Entry('simple-earn/flexible/history/subscriptionRecord', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_locked_history_subscriptionrecord = sapiGetSimpleEarnLockedHistorySubscriptionRecord = Entry('simple-earn/locked/history/subscriptionRecord', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_flexible_history_redemptionrecord = sapiGetSimpleEarnFlexibleHistoryRedemptionRecord = Entry('simple-earn/flexible/history/redemptionRecord', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_locked_history_redemptionrecord = sapiGetSimpleEarnLockedHistoryRedemptionRecord = Entry('simple-earn/locked/history/redemptionRecord', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_flexible_history_rewardsrecord = sapiGetSimpleEarnFlexibleHistoryRewardsRecord = Entry('simple-earn/flexible/history/rewardsRecord', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_locked_history_rewardsrecord = sapiGetSimpleEarnLockedHistoryRewardsRecord = Entry('simple-earn/locked/history/rewardsRecord', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_flexible_history_collateralrecord = sapiGetSimpleEarnFlexibleHistoryCollateralRecord = Entry('simple-earn/flexible/history/collateralRecord', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_dci_product_list = sapiGetDciProductList = Entry('dci/product/list', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_dci_product_positions = sapiGetDciProductPositions = Entry('dci/product/positions', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_dci_product_accounts = sapiGetDciProductAccounts = Entry('dci/product/accounts', 'sapi', 'GET', {'cost': 0.1}) + sapi_post_asset_dust = sapiPostAssetDust = Entry('asset/dust', 'sapi', 'POST', {'cost': 0.06667}) + sapi_post_asset_dust_btc = sapiPostAssetDustBtc = Entry('asset/dust-btc', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_asset_transfer = sapiPostAssetTransfer = Entry('asset/transfer', 'sapi', 'POST', {'cost': 6.0003}) + sapi_post_asset_get_funding_asset = sapiPostAssetGetFundingAsset = Entry('asset/get-funding-asset', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_asset_convert_transfer = sapiPostAssetConvertTransfer = Entry('asset/convert-transfer', 'sapi', 'POST', {'cost': 0.033335}) + sapi_post_account_disablefastwithdrawswitch = sapiPostAccountDisableFastWithdrawSwitch = Entry('account/disableFastWithdrawSwitch', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_account_enablefastwithdrawswitch = sapiPostAccountEnableFastWithdrawSwitch = Entry('account/enableFastWithdrawSwitch', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_capital_withdraw_apply = sapiPostCapitalWithdrawApply = Entry('capital/withdraw/apply', 'sapi', 'POST', {'cost': 4.0002}) + sapi_post_capital_contract_convertible_coins = sapiPostCapitalContractConvertibleCoins = Entry('capital/contract/convertible-coins', 'sapi', 'POST', {'cost': 4.0002}) + sapi_post_capital_deposit_credit_apply = sapiPostCapitalDepositCreditApply = Entry('capital/deposit/credit-apply', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_margin_borrow_repay = sapiPostMarginBorrowRepay = Entry('margin/borrow-repay', 'sapi', 'POST', {'cost': 20.001}) + sapi_post_margin_transfer = sapiPostMarginTransfer = Entry('margin/transfer', 'sapi', 'POST', {'cost': 4.0002}) + sapi_post_margin_loan = sapiPostMarginLoan = Entry('margin/loan', 'sapi', 'POST', {'cost': 20.001}) + sapi_post_margin_repay = sapiPostMarginRepay = Entry('margin/repay', 'sapi', 'POST', {'cost': 20.001}) + sapi_post_margin_order = sapiPostMarginOrder = Entry('margin/order', 'sapi', 'POST', {'cost': 0.040002}) + sapi_post_margin_order_oco = sapiPostMarginOrderOco = Entry('margin/order/oco', 'sapi', 'POST', {'cost': 0.040002}) + sapi_post_margin_dust = sapiPostMarginDust = Entry('margin/dust', 'sapi', 'POST', {'cost': 20.001}) + sapi_post_margin_exchange_small_liability = sapiPostMarginExchangeSmallLiability = Entry('margin/exchange-small-liability', 'sapi', 'POST', {'cost': 20.001}) + sapi_post_margin_isolated_transfer = sapiPostMarginIsolatedTransfer = Entry('margin/isolated/transfer', 'sapi', 'POST', {'cost': 4.0002}) + sapi_post_margin_isolated_account = sapiPostMarginIsolatedAccount = Entry('margin/isolated/account', 'sapi', 'POST', {'cost': 2.0001}) + sapi_post_margin_max_leverage = sapiPostMarginMaxLeverage = Entry('margin/max-leverage', 'sapi', 'POST', {'cost': 300}) + sapi_post_bnbburn = sapiPostBnbBurn = Entry('bnbBurn', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_sub_account_virtualsubaccount = sapiPostSubAccountVirtualSubAccount = Entry('sub-account/virtualSubAccount', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_sub_account_margin_transfer = sapiPostSubAccountMarginTransfer = Entry('sub-account/margin/transfer', 'sapi', 'POST', {'cost': 4.0002}) + sapi_post_sub_account_margin_enable = sapiPostSubAccountMarginEnable = Entry('sub-account/margin/enable', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_sub_account_futures_enable = sapiPostSubAccountFuturesEnable = Entry('sub-account/futures/enable', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_sub_account_futures_transfer = sapiPostSubAccountFuturesTransfer = Entry('sub-account/futures/transfer', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_sub_account_futures_internaltransfer = sapiPostSubAccountFuturesInternalTransfer = Entry('sub-account/futures/internalTransfer', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_sub_account_transfer_subtosub = sapiPostSubAccountTransferSubToSub = Entry('sub-account/transfer/subToSub', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_sub_account_transfer_subtomaster = sapiPostSubAccountTransferSubToMaster = Entry('sub-account/transfer/subToMaster', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_sub_account_universaltransfer = sapiPostSubAccountUniversalTransfer = Entry('sub-account/universalTransfer', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_sub_account_options_enable = sapiPostSubAccountOptionsEnable = Entry('sub-account/options/enable', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_managed_subaccount_deposit = sapiPostManagedSubaccountDeposit = Entry('managed-subaccount/deposit', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_managed_subaccount_withdraw = sapiPostManagedSubaccountWithdraw = Entry('managed-subaccount/withdraw', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_userdatastream = sapiPostUserDataStream = Entry('userDataStream', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_userdatastream_isolated = sapiPostUserDataStreamIsolated = Entry('userDataStream/isolated', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_futures_transfer = sapiPostFuturesTransfer = Entry('futures/transfer', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_lending_customizedfixed_purchase = sapiPostLendingCustomizedFixedPurchase = Entry('lending/customizedFixed/purchase', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_lending_daily_purchase = sapiPostLendingDailyPurchase = Entry('lending/daily/purchase', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_lending_daily_redeem = sapiPostLendingDailyRedeem = Entry('lending/daily/redeem', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_bswap_liquidityadd = sapiPostBswapLiquidityAdd = Entry('bswap/liquidityAdd', 'sapi', 'POST', {'cost': 60}) + sapi_post_bswap_liquidityremove = sapiPostBswapLiquidityRemove = Entry('bswap/liquidityRemove', 'sapi', 'POST', {'cost': 60}) + sapi_post_bswap_swap = sapiPostBswapSwap = Entry('bswap/swap', 'sapi', 'POST', {'cost': 60}) + sapi_post_bswap_claimrewards = sapiPostBswapClaimRewards = Entry('bswap/claimRewards', 'sapi', 'POST', {'cost': 6.667}) + sapi_post_blvt_subscribe = sapiPostBlvtSubscribe = Entry('blvt/subscribe', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_blvt_redeem = sapiPostBlvtRedeem = Entry('blvt/redeem', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_apireferral_customization = sapiPostApiReferralCustomization = Entry('apiReferral/customization', 'sapi', 'POST', {'cost': 1}) + sapi_post_apireferral_usercustomization = sapiPostApiReferralUserCustomization = Entry('apiReferral/userCustomization', 'sapi', 'POST', {'cost': 1}) + sapi_post_apireferral_rebate_historicalrecord = sapiPostApiReferralRebateHistoricalRecord = Entry('apiReferral/rebate/historicalRecord', 'sapi', 'POST', {'cost': 1}) + sapi_post_apireferral_kickback_historicalrecord = sapiPostApiReferralKickbackHistoricalRecord = Entry('apiReferral/kickback/historicalRecord', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_subaccount = sapiPostBrokerSubAccount = Entry('broker/subAccount', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_subaccount_margin = sapiPostBrokerSubAccountMargin = Entry('broker/subAccount/margin', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_subaccount_futures = sapiPostBrokerSubAccountFutures = Entry('broker/subAccount/futures', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_subaccountapi = sapiPostBrokerSubAccountApi = Entry('broker/subAccountApi', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_subaccountapi_permission = sapiPostBrokerSubAccountApiPermission = Entry('broker/subAccountApi/permission', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_subaccountapi_commission = sapiPostBrokerSubAccountApiCommission = Entry('broker/subAccountApi/commission', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_subaccountapi_commission_futures = sapiPostBrokerSubAccountApiCommissionFutures = Entry('broker/subAccountApi/commission/futures', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_subaccountapi_commission_coinfutures = sapiPostBrokerSubAccountApiCommissionCoinFutures = Entry('broker/subAccountApi/commission/coinFutures', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_transfer = sapiPostBrokerTransfer = Entry('broker/transfer', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_transfer_futures = sapiPostBrokerTransferFutures = Entry('broker/transfer/futures', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_rebate_historicalrecord = sapiPostBrokerRebateHistoricalRecord = Entry('broker/rebate/historicalRecord', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_subaccount_bnbburn_spot = sapiPostBrokerSubAccountBnbBurnSpot = Entry('broker/subAccount/bnbBurn/spot', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_subaccount_bnbburn_margininterest = sapiPostBrokerSubAccountBnbBurnMarginInterest = Entry('broker/subAccount/bnbBurn/marginInterest', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_subaccount_blvt = sapiPostBrokerSubAccountBlvt = Entry('broker/subAccount/blvt', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_subaccountapi_iprestriction = sapiPostBrokerSubAccountApiIpRestriction = Entry('broker/subAccountApi/ipRestriction', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_subaccountapi_iprestriction_iplist = sapiPostBrokerSubAccountApiIpRestrictionIpList = Entry('broker/subAccountApi/ipRestriction/ipList', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_universaltransfer = sapiPostBrokerUniversalTransfer = Entry('broker/universalTransfer', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_subaccountapi_permission_universaltransfer = sapiPostBrokerSubAccountApiPermissionUniversalTransfer = Entry('broker/subAccountApi/permission/universalTransfer', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_subaccountapi_permission_vanillaoptions = sapiPostBrokerSubAccountApiPermissionVanillaOptions = Entry('broker/subAccountApi/permission/vanillaOptions', 'sapi', 'POST', {'cost': 1}) + sapi_post_giftcard_createcode = sapiPostGiftcardCreateCode = Entry('giftcard/createCode', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_giftcard_redeemcode = sapiPostGiftcardRedeemCode = Entry('giftcard/redeemCode', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_giftcard_buycode = sapiPostGiftcardBuyCode = Entry('giftcard/buyCode', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_algo_spot_newordertwap = sapiPostAlgoSpotNewOrderTwap = Entry('algo/spot/newOrderTwap', 'sapi', 'POST', {'cost': 20.001}) + sapi_post_algo_futures_newordervp = sapiPostAlgoFuturesNewOrderVp = Entry('algo/futures/newOrderVp', 'sapi', 'POST', {'cost': 20.001}) + sapi_post_algo_futures_newordertwap = sapiPostAlgoFuturesNewOrderTwap = Entry('algo/futures/newOrderTwap', 'sapi', 'POST', {'cost': 20.001}) + sapi_post_staking_purchase = sapiPostStakingPurchase = Entry('staking/purchase', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_staking_redeem = sapiPostStakingRedeem = Entry('staking/redeem', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_staking_setautostaking = sapiPostStakingSetAutoStaking = Entry('staking/setAutoStaking', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_eth_staking_eth_stake = sapiPostEthStakingEthStake = Entry('eth-staking/eth/stake', 'sapi', 'POST', {'cost': 15}) + sapi_post_eth_staking_eth_redeem = sapiPostEthStakingEthRedeem = Entry('eth-staking/eth/redeem', 'sapi', 'POST', {'cost': 15}) + sapi_post_eth_staking_wbeth_wrap = sapiPostEthStakingWbethWrap = Entry('eth-staking/wbeth/wrap', 'sapi', 'POST', {'cost': 15}) + sapi_post_sol_staking_sol_stake = sapiPostSolStakingSolStake = Entry('sol-staking/sol/stake', 'sapi', 'POST', {'cost': 15}) + sapi_post_sol_staking_sol_redeem = sapiPostSolStakingSolRedeem = Entry('sol-staking/sol/redeem', 'sapi', 'POST', {'cost': 15}) + sapi_post_mining_hash_transfer_config = sapiPostMiningHashTransferConfig = Entry('mining/hash-transfer/config', 'sapi', 'POST', {'cost': 0.5}) + sapi_post_mining_hash_transfer_config_cancel = sapiPostMiningHashTransferConfigCancel = Entry('mining/hash-transfer/config/cancel', 'sapi', 'POST', {'cost': 0.5}) + sapi_post_portfolio_repay = sapiPostPortfolioRepay = Entry('portfolio/repay', 'sapi', 'POST', {'cost': 20.001}) + sapi_post_loan_vip_renew = sapiPostLoanVipRenew = Entry('loan/vip/renew', 'sapi', 'POST', {'cost': 40.002}) + sapi_post_loan_vip_borrow = sapiPostLoanVipBorrow = Entry('loan/vip/borrow', 'sapi', 'POST', {'cost': 40.002}) + sapi_post_loan_borrow = sapiPostLoanBorrow = Entry('loan/borrow', 'sapi', 'POST', {'cost': 40.002}) + sapi_post_loan_repay = sapiPostLoanRepay = Entry('loan/repay', 'sapi', 'POST', {'cost': 40.002}) + sapi_post_loan_adjust_ltv = sapiPostLoanAdjustLtv = Entry('loan/adjust/ltv', 'sapi', 'POST', {'cost': 40.002}) + sapi_post_loan_customize_margin_call = sapiPostLoanCustomizeMarginCall = Entry('loan/customize/margin_call', 'sapi', 'POST', {'cost': 40.002}) + sapi_post_loan_flexible_repay = sapiPostLoanFlexibleRepay = Entry('loan/flexible/repay', 'sapi', 'POST', {'cost': 40.002}) + sapi_post_loan_flexible_adjust_ltv = sapiPostLoanFlexibleAdjustLtv = Entry('loan/flexible/adjust/ltv', 'sapi', 'POST', {'cost': 40.002}) + sapi_post_loan_vip_repay = sapiPostLoanVipRepay = Entry('loan/vip/repay', 'sapi', 'POST', {'cost': 40.002}) + sapi_post_convert_getquote = sapiPostConvertGetQuote = Entry('convert/getQuote', 'sapi', 'POST', {'cost': 1.3334}) + sapi_post_convert_acceptquote = sapiPostConvertAcceptQuote = Entry('convert/acceptQuote', 'sapi', 'POST', {'cost': 3.3335}) + sapi_post_convert_limit_placeorder = sapiPostConvertLimitPlaceOrder = Entry('convert/limit/placeOrder', 'sapi', 'POST', {'cost': 3.3335}) + sapi_post_convert_limit_cancelorder = sapiPostConvertLimitCancelOrder = Entry('convert/limit/cancelOrder', 'sapi', 'POST', {'cost': 1.3334}) + sapi_post_portfolio_auto_collection = sapiPostPortfolioAutoCollection = Entry('portfolio/auto-collection', 'sapi', 'POST', {'cost': 150}) + sapi_post_portfolio_asset_collection = sapiPostPortfolioAssetCollection = Entry('portfolio/asset-collection', 'sapi', 'POST', {'cost': 6}) + sapi_post_portfolio_bnb_transfer = sapiPostPortfolioBnbTransfer = Entry('portfolio/bnb-transfer', 'sapi', 'POST', {'cost': 150}) + sapi_post_portfolio_repay_futures_switch = sapiPostPortfolioRepayFuturesSwitch = Entry('portfolio/repay-futures-switch', 'sapi', 'POST', {'cost': 150}) + sapi_post_portfolio_repay_futures_negative_balance = sapiPostPortfolioRepayFuturesNegativeBalance = Entry('portfolio/repay-futures-negative-balance', 'sapi', 'POST', {'cost': 150}) + sapi_post_portfolio_mint = sapiPostPortfolioMint = Entry('portfolio/mint', 'sapi', 'POST', {'cost': 20}) + sapi_post_portfolio_redeem = sapiPostPortfolioRedeem = Entry('portfolio/redeem', 'sapi', 'POST', {'cost': 20}) + sapi_post_portfolio_earn_asset_transfer = sapiPostPortfolioEarnAssetTransfer = Entry('portfolio/earn-asset-transfer', 'sapi', 'POST', {'cost': 150}) + sapi_post_lending_auto_invest_plan_add = sapiPostLendingAutoInvestPlanAdd = Entry('lending/auto-invest/plan/add', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_lending_auto_invest_plan_edit = sapiPostLendingAutoInvestPlanEdit = Entry('lending/auto-invest/plan/edit', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_lending_auto_invest_plan_edit_status = sapiPostLendingAutoInvestPlanEditStatus = Entry('lending/auto-invest/plan/edit-status', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_lending_auto_invest_one_off = sapiPostLendingAutoInvestOneOff = Entry('lending/auto-invest/one-off', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_lending_auto_invest_redeem = sapiPostLendingAutoInvestRedeem = Entry('lending/auto-invest/redeem', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_simple_earn_flexible_subscribe = sapiPostSimpleEarnFlexibleSubscribe = Entry('simple-earn/flexible/subscribe', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_simple_earn_locked_subscribe = sapiPostSimpleEarnLockedSubscribe = Entry('simple-earn/locked/subscribe', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_simple_earn_flexible_redeem = sapiPostSimpleEarnFlexibleRedeem = Entry('simple-earn/flexible/redeem', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_simple_earn_locked_redeem = sapiPostSimpleEarnLockedRedeem = Entry('simple-earn/locked/redeem', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_simple_earn_flexible_setautosubscribe = sapiPostSimpleEarnFlexibleSetAutoSubscribe = Entry('simple-earn/flexible/setAutoSubscribe', 'sapi', 'POST', {'cost': 15}) + sapi_post_simple_earn_locked_setautosubscribe = sapiPostSimpleEarnLockedSetAutoSubscribe = Entry('simple-earn/locked/setAutoSubscribe', 'sapi', 'POST', {'cost': 15}) + sapi_post_simple_earn_locked_setredeemoption = sapiPostSimpleEarnLockedSetRedeemOption = Entry('simple-earn/locked/setRedeemOption', 'sapi', 'POST', {'cost': 5}) + sapi_post_dci_product_subscribe = sapiPostDciProductSubscribe = Entry('dci/product/subscribe', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_dci_product_auto_compound_edit = sapiPostDciProductAutoCompoundEdit = Entry('dci/product/auto_compound/edit', 'sapi', 'POST', {'cost': 0.1}) + sapi_put_userdatastream = sapiPutUserDataStream = Entry('userDataStream', 'sapi', 'PUT', {'cost': 0.1}) + sapi_put_userdatastream_isolated = sapiPutUserDataStreamIsolated = Entry('userDataStream/isolated', 'sapi', 'PUT', {'cost': 0.1}) + sapi_delete_margin_openorders = sapiDeleteMarginOpenOrders = Entry('margin/openOrders', 'sapi', 'DELETE', {'cost': 0.1}) + sapi_delete_margin_order = sapiDeleteMarginOrder = Entry('margin/order', 'sapi', 'DELETE', {'cost': 0.006667}) + sapi_delete_margin_orderlist = sapiDeleteMarginOrderList = Entry('margin/orderList', 'sapi', 'DELETE', {'cost': 0.006667}) + sapi_delete_margin_isolated_account = sapiDeleteMarginIsolatedAccount = Entry('margin/isolated/account', 'sapi', 'DELETE', {'cost': 2.0001}) + sapi_delete_userdatastream = sapiDeleteUserDataStream = Entry('userDataStream', 'sapi', 'DELETE', {'cost': 0.1}) + sapi_delete_userdatastream_isolated = sapiDeleteUserDataStreamIsolated = Entry('userDataStream/isolated', 'sapi', 'DELETE', {'cost': 0.1}) + sapi_delete_broker_subaccountapi = sapiDeleteBrokerSubAccountApi = Entry('broker/subAccountApi', 'sapi', 'DELETE', {'cost': 1}) + sapi_delete_broker_subaccountapi_iprestriction_iplist = sapiDeleteBrokerSubAccountApiIpRestrictionIpList = Entry('broker/subAccountApi/ipRestriction/ipList', 'sapi', 'DELETE', {'cost': 1}) + sapi_delete_algo_spot_order = sapiDeleteAlgoSpotOrder = Entry('algo/spot/order', 'sapi', 'DELETE', {'cost': 0.1}) + sapi_delete_algo_futures_order = sapiDeleteAlgoFuturesOrder = Entry('algo/futures/order', 'sapi', 'DELETE', {'cost': 0.1}) + sapi_delete_sub_account_subaccountapi_iprestriction_iplist = sapiDeleteSubAccountSubAccountApiIpRestrictionIpList = Entry('sub-account/subAccountApi/ipRestriction/ipList', 'sapi', 'DELETE', {'cost': 20.001}) + sapiv2_get_eth_staking_account = sapiV2GetEthStakingAccount = Entry('eth-staking/account', 'sapiV2', 'GET', {'cost': 15}) + sapiv2_get_sub_account_futures_account = sapiV2GetSubAccountFuturesAccount = Entry('sub-account/futures/account', 'sapiV2', 'GET', {'cost': 0.1}) + sapiv2_get_sub_account_futures_accountsummary = sapiV2GetSubAccountFuturesAccountSummary = Entry('sub-account/futures/accountSummary', 'sapiV2', 'GET', {'cost': 1}) + sapiv2_get_sub_account_futures_positionrisk = sapiV2GetSubAccountFuturesPositionRisk = Entry('sub-account/futures/positionRisk', 'sapiV2', 'GET', {'cost': 0.1}) + sapiv2_get_loan_flexible_ongoing_orders = sapiV2GetLoanFlexibleOngoingOrders = Entry('loan/flexible/ongoing/orders', 'sapiV2', 'GET', {'cost': 30}) + sapiv2_get_loan_flexible_borrow_history = sapiV2GetLoanFlexibleBorrowHistory = Entry('loan/flexible/borrow/history', 'sapiV2', 'GET', {'cost': 40}) + sapiv2_get_loan_flexible_repay_history = sapiV2GetLoanFlexibleRepayHistory = Entry('loan/flexible/repay/history', 'sapiV2', 'GET', {'cost': 40}) + sapiv2_get_loan_flexible_ltv_adjustment_history = sapiV2GetLoanFlexibleLtvAdjustmentHistory = Entry('loan/flexible/ltv/adjustment/history', 'sapiV2', 'GET', {'cost': 40}) + sapiv2_get_loan_flexible_loanable_data = sapiV2GetLoanFlexibleLoanableData = Entry('loan/flexible/loanable/data', 'sapiV2', 'GET', {'cost': 40}) + sapiv2_get_loan_flexible_collateral_data = sapiV2GetLoanFlexibleCollateralData = Entry('loan/flexible/collateral/data', 'sapiV2', 'GET', {'cost': 40}) + sapiv2_get_portfolio_account = sapiV2GetPortfolioAccount = Entry('portfolio/account', 'sapiV2', 'GET', {'cost': 2}) + sapiv2_post_eth_staking_eth_stake = sapiV2PostEthStakingEthStake = Entry('eth-staking/eth/stake', 'sapiV2', 'POST', {'cost': 15}) + sapiv2_post_sub_account_subaccountapi_iprestriction = sapiV2PostSubAccountSubAccountApiIpRestriction = Entry('sub-account/subAccountApi/ipRestriction', 'sapiV2', 'POST', {'cost': 20.001}) + sapiv2_post_loan_flexible_borrow = sapiV2PostLoanFlexibleBorrow = Entry('loan/flexible/borrow', 'sapiV2', 'POST', {'cost': 40.002}) + sapiv2_post_loan_flexible_repay = sapiV2PostLoanFlexibleRepay = Entry('loan/flexible/repay', 'sapiV2', 'POST', {'cost': 40.002}) + sapiv2_post_loan_flexible_adjust_ltv = sapiV2PostLoanFlexibleAdjustLtv = Entry('loan/flexible/adjust/ltv', 'sapiV2', 'POST', {'cost': 40.002}) + sapiv3_get_sub_account_assets = sapiV3GetSubAccountAssets = Entry('sub-account/assets', 'sapiV3', 'GET', {'cost': 0.40002}) + sapiv3_post_asset_getuserasset = sapiV3PostAssetGetUserAsset = Entry('asset/getUserAsset', 'sapiV3', 'POST', {'cost': 0.5}) + sapiv4_get_sub_account_assets = sapiV4GetSubAccountAssets = Entry('sub-account/assets', 'sapiV4', 'GET', {'cost': 0.40002}) + dapipublic_get_ping = dapiPublicGetPing = Entry('ping', 'dapiPublic', 'GET', {'cost': 1}) + dapipublic_get_time = dapiPublicGetTime = Entry('time', 'dapiPublic', 'GET', {'cost': 1}) + dapipublic_get_exchangeinfo = dapiPublicGetExchangeInfo = Entry('exchangeInfo', 'dapiPublic', 'GET', {'cost': 1}) + dapipublic_get_depth = dapiPublicGetDepth = Entry('depth', 'dapiPublic', 'GET', {'cost': 2, 'byLimit': [[50, 2], [100, 5], [500, 10], [1000, 20]]}) + dapipublic_get_trades = dapiPublicGetTrades = Entry('trades', 'dapiPublic', 'GET', {'cost': 5}) + dapipublic_get_historicaltrades = dapiPublicGetHistoricalTrades = Entry('historicalTrades', 'dapiPublic', 'GET', {'cost': 20}) + dapipublic_get_aggtrades = dapiPublicGetAggTrades = Entry('aggTrades', 'dapiPublic', 'GET', {'cost': 20}) + dapipublic_get_premiumindex = dapiPublicGetPremiumIndex = Entry('premiumIndex', 'dapiPublic', 'GET', {'cost': 10}) + dapipublic_get_fundingrate = dapiPublicGetFundingRate = Entry('fundingRate', 'dapiPublic', 'GET', {'cost': 1}) + dapipublic_get_klines = dapiPublicGetKlines = Entry('klines', 'dapiPublic', 'GET', {'cost': 1, 'byLimit': [[99, 1], [499, 2], [1000, 5], [10000, 10]]}) + dapipublic_get_continuousklines = dapiPublicGetContinuousKlines = Entry('continuousKlines', 'dapiPublic', 'GET', {'cost': 1, 'byLimit': [[99, 1], [499, 2], [1000, 5], [10000, 10]]}) + dapipublic_get_indexpriceklines = dapiPublicGetIndexPriceKlines = Entry('indexPriceKlines', 'dapiPublic', 'GET', {'cost': 1, 'byLimit': [[99, 1], [499, 2], [1000, 5], [10000, 10]]}) + dapipublic_get_markpriceklines = dapiPublicGetMarkPriceKlines = Entry('markPriceKlines', 'dapiPublic', 'GET', {'cost': 1, 'byLimit': [[99, 1], [499, 2], [1000, 5], [10000, 10]]}) + dapipublic_get_premiumindexklines = dapiPublicGetPremiumIndexKlines = Entry('premiumIndexKlines', 'dapiPublic', 'GET', {'cost': 1, 'byLimit': [[99, 1], [499, 2], [1000, 5], [10000, 10]]}) + dapipublic_get_ticker_24hr = dapiPublicGetTicker24hr = Entry('ticker/24hr', 'dapiPublic', 'GET', {'cost': 1, 'noSymbol': 40}) + dapipublic_get_ticker_price = dapiPublicGetTickerPrice = Entry('ticker/price', 'dapiPublic', 'GET', {'cost': 1, 'noSymbol': 2}) + dapipublic_get_ticker_bookticker = dapiPublicGetTickerBookTicker = Entry('ticker/bookTicker', 'dapiPublic', 'GET', {'cost': 2, 'noSymbol': 5}) + dapipublic_get_constituents = dapiPublicGetConstituents = Entry('constituents', 'dapiPublic', 'GET', {'cost': 2}) + dapipublic_get_openinterest = dapiPublicGetOpenInterest = Entry('openInterest', 'dapiPublic', 'GET', {'cost': 1}) + dapipublic_get_fundinginfo = dapiPublicGetFundingInfo = Entry('fundingInfo', 'dapiPublic', 'GET', {'cost': 1}) + dapidata_get_delivery_price = dapiDataGetDeliveryPrice = Entry('delivery-price', 'dapiData', 'GET', {'cost': 1}) + dapidata_get_openinteresthist = dapiDataGetOpenInterestHist = Entry('openInterestHist', 'dapiData', 'GET', {'cost': 1}) + dapidata_get_toplongshortaccountratio = dapiDataGetTopLongShortAccountRatio = Entry('topLongShortAccountRatio', 'dapiData', 'GET', {'cost': 1}) + dapidata_get_toplongshortpositionratio = dapiDataGetTopLongShortPositionRatio = Entry('topLongShortPositionRatio', 'dapiData', 'GET', {'cost': 1}) + dapidata_get_globallongshortaccountratio = dapiDataGetGlobalLongShortAccountRatio = Entry('globalLongShortAccountRatio', 'dapiData', 'GET', {'cost': 1}) + dapidata_get_takerbuysellvol = dapiDataGetTakerBuySellVol = Entry('takerBuySellVol', 'dapiData', 'GET', {'cost': 1}) + dapidata_get_basis = dapiDataGetBasis = Entry('basis', 'dapiData', 'GET', {'cost': 1}) + dapiprivate_get_positionside_dual = dapiPrivateGetPositionSideDual = Entry('positionSide/dual', 'dapiPrivate', 'GET', {'cost': 30}) + dapiprivate_get_orderamendment = dapiPrivateGetOrderAmendment = Entry('orderAmendment', 'dapiPrivate', 'GET', {'cost': 1}) + dapiprivate_get_order = dapiPrivateGetOrder = Entry('order', 'dapiPrivate', 'GET', {'cost': 1}) + dapiprivate_get_openorder = dapiPrivateGetOpenOrder = Entry('openOrder', 'dapiPrivate', 'GET', {'cost': 1}) + dapiprivate_get_openorders = dapiPrivateGetOpenOrders = Entry('openOrders', 'dapiPrivate', 'GET', {'cost': 1, 'noSymbol': 5}) + dapiprivate_get_allorders = dapiPrivateGetAllOrders = Entry('allOrders', 'dapiPrivate', 'GET', {'cost': 20, 'noSymbol': 40}) + dapiprivate_get_balance = dapiPrivateGetBalance = Entry('balance', 'dapiPrivate', 'GET', {'cost': 1}) + dapiprivate_get_account = dapiPrivateGetAccount = Entry('account', 'dapiPrivate', 'GET', {'cost': 5}) + dapiprivate_get_positionmargin_history = dapiPrivateGetPositionMarginHistory = Entry('positionMargin/history', 'dapiPrivate', 'GET', {'cost': 1}) + dapiprivate_get_positionrisk = dapiPrivateGetPositionRisk = Entry('positionRisk', 'dapiPrivate', 'GET', {'cost': 1}) + dapiprivate_get_usertrades = dapiPrivateGetUserTrades = Entry('userTrades', 'dapiPrivate', 'GET', {'cost': 20, 'noSymbol': 40}) + dapiprivate_get_income = dapiPrivateGetIncome = Entry('income', 'dapiPrivate', 'GET', {'cost': 20}) + dapiprivate_get_leveragebracket = dapiPrivateGetLeverageBracket = Entry('leverageBracket', 'dapiPrivate', 'GET', {'cost': 1}) + dapiprivate_get_forceorders = dapiPrivateGetForceOrders = Entry('forceOrders', 'dapiPrivate', 'GET', {'cost': 20, 'noSymbol': 50}) + dapiprivate_get_adlquantile = dapiPrivateGetAdlQuantile = Entry('adlQuantile', 'dapiPrivate', 'GET', {'cost': 5}) + dapiprivate_get_commissionrate = dapiPrivateGetCommissionRate = Entry('commissionRate', 'dapiPrivate', 'GET', {'cost': 20}) + dapiprivate_get_income_asyn = dapiPrivateGetIncomeAsyn = Entry('income/asyn', 'dapiPrivate', 'GET', {'cost': 5}) + dapiprivate_get_income_asyn_id = dapiPrivateGetIncomeAsynId = Entry('income/asyn/id', 'dapiPrivate', 'GET', {'cost': 5}) + dapiprivate_get_trade_asyn = dapiPrivateGetTradeAsyn = Entry('trade/asyn', 'dapiPrivate', 'GET', {'cost': 0.5}) + dapiprivate_get_trade_asyn_id = dapiPrivateGetTradeAsynId = Entry('trade/asyn/id', 'dapiPrivate', 'GET', {'cost': 0.5}) + dapiprivate_get_order_asyn = dapiPrivateGetOrderAsyn = Entry('order/asyn', 'dapiPrivate', 'GET', {'cost': 0.5}) + dapiprivate_get_order_asyn_id = dapiPrivateGetOrderAsynId = Entry('order/asyn/id', 'dapiPrivate', 'GET', {'cost': 0.5}) + dapiprivate_get_pmexchangeinfo = dapiPrivateGetPmExchangeInfo = Entry('pmExchangeInfo', 'dapiPrivate', 'GET', {'cost': 0.5}) + dapiprivate_get_pmaccountinfo = dapiPrivateGetPmAccountInfo = Entry('pmAccountInfo', 'dapiPrivate', 'GET', {'cost': 0.5}) + dapiprivate_post_positionside_dual = dapiPrivatePostPositionSideDual = Entry('positionSide/dual', 'dapiPrivate', 'POST', {'cost': 1}) + dapiprivate_post_order = dapiPrivatePostOrder = Entry('order', 'dapiPrivate', 'POST', {'cost': 4}) + dapiprivate_post_batchorders = dapiPrivatePostBatchOrders = Entry('batchOrders', 'dapiPrivate', 'POST', {'cost': 5}) + dapiprivate_post_countdowncancelall = dapiPrivatePostCountdownCancelAll = Entry('countdownCancelAll', 'dapiPrivate', 'POST', {'cost': 10}) + dapiprivate_post_leverage = dapiPrivatePostLeverage = Entry('leverage', 'dapiPrivate', 'POST', {'cost': 1}) + dapiprivate_post_margintype = dapiPrivatePostMarginType = Entry('marginType', 'dapiPrivate', 'POST', {'cost': 1}) + dapiprivate_post_positionmargin = dapiPrivatePostPositionMargin = Entry('positionMargin', 'dapiPrivate', 'POST', {'cost': 1}) + dapiprivate_post_listenkey = dapiPrivatePostListenKey = Entry('listenKey', 'dapiPrivate', 'POST', {'cost': 1}) + dapiprivate_put_listenkey = dapiPrivatePutListenKey = Entry('listenKey', 'dapiPrivate', 'PUT', {'cost': 1}) + dapiprivate_put_order = dapiPrivatePutOrder = Entry('order', 'dapiPrivate', 'PUT', {'cost': 1}) + dapiprivate_put_batchorders = dapiPrivatePutBatchOrders = Entry('batchOrders', 'dapiPrivate', 'PUT', {'cost': 5}) + dapiprivate_delete_order = dapiPrivateDeleteOrder = Entry('order', 'dapiPrivate', 'DELETE', {'cost': 1}) + dapiprivate_delete_allopenorders = dapiPrivateDeleteAllOpenOrders = Entry('allOpenOrders', 'dapiPrivate', 'DELETE', {'cost': 1}) + dapiprivate_delete_batchorders = dapiPrivateDeleteBatchOrders = Entry('batchOrders', 'dapiPrivate', 'DELETE', {'cost': 5}) + dapiprivate_delete_listenkey = dapiPrivateDeleteListenKey = Entry('listenKey', 'dapiPrivate', 'DELETE', {'cost': 1}) + dapiprivatev2_get_leveragebracket = dapiPrivateV2GetLeverageBracket = Entry('leverageBracket', 'dapiPrivateV2', 'GET', {'cost': 1}) + fapipublic_get_ping = fapiPublicGetPing = Entry('ping', 'fapiPublic', 'GET', {'cost': 1}) + fapipublic_get_time = fapiPublicGetTime = Entry('time', 'fapiPublic', 'GET', {'cost': 1}) + fapipublic_get_exchangeinfo = fapiPublicGetExchangeInfo = Entry('exchangeInfo', 'fapiPublic', 'GET', {'cost': 1}) + fapipublic_get_depth = fapiPublicGetDepth = Entry('depth', 'fapiPublic', 'GET', {'cost': 2, 'byLimit': [[50, 2], [100, 5], [500, 10], [1000, 20]]}) + fapipublic_get_trades = fapiPublicGetTrades = Entry('trades', 'fapiPublic', 'GET', {'cost': 5}) + fapipublic_get_historicaltrades = fapiPublicGetHistoricalTrades = Entry('historicalTrades', 'fapiPublic', 'GET', {'cost': 20}) + fapipublic_get_aggtrades = fapiPublicGetAggTrades = Entry('aggTrades', 'fapiPublic', 'GET', {'cost': 20}) + fapipublic_get_klines = fapiPublicGetKlines = Entry('klines', 'fapiPublic', 'GET', {'cost': 1, 'byLimit': [[99, 1], [499, 2], [1000, 5], [10000, 10]]}) + fapipublic_get_continuousklines = fapiPublicGetContinuousKlines = Entry('continuousKlines', 'fapiPublic', 'GET', {'cost': 1, 'byLimit': [[99, 1], [499, 2], [1000, 5], [10000, 10]]}) + fapipublic_get_markpriceklines = fapiPublicGetMarkPriceKlines = Entry('markPriceKlines', 'fapiPublic', 'GET', {'cost': 1, 'byLimit': [[99, 1], [499, 2], [1000, 5], [10000, 10]]}) + fapipublic_get_indexpriceklines = fapiPublicGetIndexPriceKlines = Entry('indexPriceKlines', 'fapiPublic', 'GET', {'cost': 1, 'byLimit': [[99, 1], [499, 2], [1000, 5], [10000, 10]]}) + fapipublic_get_premiumindexklines = fapiPublicGetPremiumIndexKlines = Entry('premiumIndexKlines', 'fapiPublic', 'GET', {'cost': 1, 'byLimit': [[99, 1], [499, 2], [1000, 5], [10000, 10]]}) + fapipublic_get_fundingrate = fapiPublicGetFundingRate = Entry('fundingRate', 'fapiPublic', 'GET', {'cost': 1}) + fapipublic_get_fundinginfo = fapiPublicGetFundingInfo = Entry('fundingInfo', 'fapiPublic', 'GET', {'cost': 1}) + fapipublic_get_premiumindex = fapiPublicGetPremiumIndex = Entry('premiumIndex', 'fapiPublic', 'GET', {'cost': 1}) + fapipublic_get_ticker_24hr = fapiPublicGetTicker24hr = Entry('ticker/24hr', 'fapiPublic', 'GET', {'cost': 1, 'noSymbol': 40}) + fapipublic_get_ticker_price = fapiPublicGetTickerPrice = Entry('ticker/price', 'fapiPublic', 'GET', {'cost': 1, 'noSymbol': 2}) + fapipublic_get_ticker_bookticker = fapiPublicGetTickerBookTicker = Entry('ticker/bookTicker', 'fapiPublic', 'GET', {'cost': 1, 'noSymbol': 2}) + fapipublic_get_openinterest = fapiPublicGetOpenInterest = Entry('openInterest', 'fapiPublic', 'GET', {'cost': 1}) + fapipublic_get_indexinfo = fapiPublicGetIndexInfo = Entry('indexInfo', 'fapiPublic', 'GET', {'cost': 1}) + fapipublic_get_assetindex = fapiPublicGetAssetIndex = Entry('assetIndex', 'fapiPublic', 'GET', {'cost': 1, 'noSymbol': 10}) + fapipublic_get_constituents = fapiPublicGetConstituents = Entry('constituents', 'fapiPublic', 'GET', {'cost': 2}) + fapipublic_get_apitradingstatus = fapiPublicGetApiTradingStatus = Entry('apiTradingStatus', 'fapiPublic', 'GET', {'cost': 1, 'noSymbol': 10}) + fapipublic_get_lvtklines = fapiPublicGetLvtKlines = Entry('lvtKlines', 'fapiPublic', 'GET', {'cost': 1}) + fapipublic_get_convert_exchangeinfo = fapiPublicGetConvertExchangeInfo = Entry('convert/exchangeInfo', 'fapiPublic', 'GET', {'cost': 4}) + fapipublic_get_insurancebalance = fapiPublicGetInsuranceBalance = Entry('insuranceBalance', 'fapiPublic', 'GET', {'cost': 1}) + fapidata_get_delivery_price = fapiDataGetDeliveryPrice = Entry('delivery-price', 'fapiData', 'GET', {'cost': 1}) + fapidata_get_openinteresthist = fapiDataGetOpenInterestHist = Entry('openInterestHist', 'fapiData', 'GET', {'cost': 1}) + fapidata_get_toplongshortaccountratio = fapiDataGetTopLongShortAccountRatio = Entry('topLongShortAccountRatio', 'fapiData', 'GET', {'cost': 1}) + fapidata_get_toplongshortpositionratio = fapiDataGetTopLongShortPositionRatio = Entry('topLongShortPositionRatio', 'fapiData', 'GET', {'cost': 1}) + fapidata_get_globallongshortaccountratio = fapiDataGetGlobalLongShortAccountRatio = Entry('globalLongShortAccountRatio', 'fapiData', 'GET', {'cost': 1}) + fapidata_get_takerlongshortratio = fapiDataGetTakerlongshortRatio = Entry('takerlongshortRatio', 'fapiData', 'GET', {'cost': 1}) + fapidata_get_basis = fapiDataGetBasis = Entry('basis', 'fapiData', 'GET', {'cost': 1}) + fapiprivate_get_forceorders = fapiPrivateGetForceOrders = Entry('forceOrders', 'fapiPrivate', 'GET', {'cost': 20, 'noSymbol': 50}) + fapiprivate_get_allorders = fapiPrivateGetAllOrders = Entry('allOrders', 'fapiPrivate', 'GET', {'cost': 5}) + fapiprivate_get_openorder = fapiPrivateGetOpenOrder = Entry('openOrder', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_openorders = fapiPrivateGetOpenOrders = Entry('openOrders', 'fapiPrivate', 'GET', {'cost': 1, 'noSymbol': 40}) + fapiprivate_get_order = fapiPrivateGetOrder = Entry('order', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_account = fapiPrivateGetAccount = Entry('account', 'fapiPrivate', 'GET', {'cost': 5}) + fapiprivate_get_balance = fapiPrivateGetBalance = Entry('balance', 'fapiPrivate', 'GET', {'cost': 5}) + fapiprivate_get_leveragebracket = fapiPrivateGetLeverageBracket = Entry('leverageBracket', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_positionmargin_history = fapiPrivateGetPositionMarginHistory = Entry('positionMargin/history', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_positionrisk = fapiPrivateGetPositionRisk = Entry('positionRisk', 'fapiPrivate', 'GET', {'cost': 5}) + fapiprivate_get_positionside_dual = fapiPrivateGetPositionSideDual = Entry('positionSide/dual', 'fapiPrivate', 'GET', {'cost': 30}) + fapiprivate_get_usertrades = fapiPrivateGetUserTrades = Entry('userTrades', 'fapiPrivate', 'GET', {'cost': 5}) + fapiprivate_get_income = fapiPrivateGetIncome = Entry('income', 'fapiPrivate', 'GET', {'cost': 30}) + fapiprivate_get_commissionrate = fapiPrivateGetCommissionRate = Entry('commissionRate', 'fapiPrivate', 'GET', {'cost': 20}) + fapiprivate_get_ratelimit_order = fapiPrivateGetRateLimitOrder = Entry('rateLimit/order', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_apitradingstatus = fapiPrivateGetApiTradingStatus = Entry('apiTradingStatus', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_multiassetsmargin = fapiPrivateGetMultiAssetsMargin = Entry('multiAssetsMargin', 'fapiPrivate', 'GET', {'cost': 30}) + fapiprivate_get_apireferral_ifnewuser = fapiPrivateGetApiReferralIfNewUser = Entry('apiReferral/ifNewUser', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_apireferral_customization = fapiPrivateGetApiReferralCustomization = Entry('apiReferral/customization', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_apireferral_usercustomization = fapiPrivateGetApiReferralUserCustomization = Entry('apiReferral/userCustomization', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_apireferral_tradernum = fapiPrivateGetApiReferralTraderNum = Entry('apiReferral/traderNum', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_apireferral_overview = fapiPrivateGetApiReferralOverview = Entry('apiReferral/overview', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_apireferral_tradevol = fapiPrivateGetApiReferralTradeVol = Entry('apiReferral/tradeVol', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_apireferral_rebatevol = fapiPrivateGetApiReferralRebateVol = Entry('apiReferral/rebateVol', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_apireferral_tradersummary = fapiPrivateGetApiReferralTraderSummary = Entry('apiReferral/traderSummary', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_adlquantile = fapiPrivateGetAdlQuantile = Entry('adlQuantile', 'fapiPrivate', 'GET', {'cost': 5}) + fapiprivate_get_pmaccountinfo = fapiPrivateGetPmAccountInfo = Entry('pmAccountInfo', 'fapiPrivate', 'GET', {'cost': 5}) + fapiprivate_get_orderamendment = fapiPrivateGetOrderAmendment = Entry('orderAmendment', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_income_asyn = fapiPrivateGetIncomeAsyn = Entry('income/asyn', 'fapiPrivate', 'GET', {'cost': 1000}) + fapiprivate_get_income_asyn_id = fapiPrivateGetIncomeAsynId = Entry('income/asyn/id', 'fapiPrivate', 'GET', {'cost': 10}) + fapiprivate_get_order_asyn = fapiPrivateGetOrderAsyn = Entry('order/asyn', 'fapiPrivate', 'GET', {'cost': 1000}) + fapiprivate_get_order_asyn_id = fapiPrivateGetOrderAsynId = Entry('order/asyn/id', 'fapiPrivate', 'GET', {'cost': 10}) + fapiprivate_get_trade_asyn = fapiPrivateGetTradeAsyn = Entry('trade/asyn', 'fapiPrivate', 'GET', {'cost': 1000}) + fapiprivate_get_trade_asyn_id = fapiPrivateGetTradeAsynId = Entry('trade/asyn/id', 'fapiPrivate', 'GET', {'cost': 10}) + fapiprivate_get_feeburn = fapiPrivateGetFeeBurn = Entry('feeBurn', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_symbolconfig = fapiPrivateGetSymbolConfig = Entry('symbolConfig', 'fapiPrivate', 'GET', {'cost': 5}) + fapiprivate_get_accountconfig = fapiPrivateGetAccountConfig = Entry('accountConfig', 'fapiPrivate', 'GET', {'cost': 5}) + fapiprivate_get_convert_orderstatus = fapiPrivateGetConvertOrderStatus = Entry('convert/orderStatus', 'fapiPrivate', 'GET', {'cost': 5}) + fapiprivate_post_batchorders = fapiPrivatePostBatchOrders = Entry('batchOrders', 'fapiPrivate', 'POST', {'cost': 5}) + fapiprivate_post_positionside_dual = fapiPrivatePostPositionSideDual = Entry('positionSide/dual', 'fapiPrivate', 'POST', {'cost': 1}) + fapiprivate_post_positionmargin = fapiPrivatePostPositionMargin = Entry('positionMargin', 'fapiPrivate', 'POST', {'cost': 1}) + fapiprivate_post_margintype = fapiPrivatePostMarginType = Entry('marginType', 'fapiPrivate', 'POST', {'cost': 1}) + fapiprivate_post_order = fapiPrivatePostOrder = Entry('order', 'fapiPrivate', 'POST', {'cost': 4}) + fapiprivate_post_leverage = fapiPrivatePostLeverage = Entry('leverage', 'fapiPrivate', 'POST', {'cost': 1}) + fapiprivate_post_listenkey = fapiPrivatePostListenKey = Entry('listenKey', 'fapiPrivate', 'POST', {'cost': 1}) + fapiprivate_post_countdowncancelall = fapiPrivatePostCountdownCancelAll = Entry('countdownCancelAll', 'fapiPrivate', 'POST', {'cost': 10}) + fapiprivate_post_multiassetsmargin = fapiPrivatePostMultiAssetsMargin = Entry('multiAssetsMargin', 'fapiPrivate', 'POST', {'cost': 1}) + fapiprivate_post_apireferral_customization = fapiPrivatePostApiReferralCustomization = Entry('apiReferral/customization', 'fapiPrivate', 'POST', {'cost': 1}) + fapiprivate_post_apireferral_usercustomization = fapiPrivatePostApiReferralUserCustomization = Entry('apiReferral/userCustomization', 'fapiPrivate', 'POST', {'cost': 1}) + fapiprivate_post_feeburn = fapiPrivatePostFeeBurn = Entry('feeBurn', 'fapiPrivate', 'POST', {'cost': 1}) + fapiprivate_post_convert_getquote = fapiPrivatePostConvertGetQuote = Entry('convert/getQuote', 'fapiPrivate', 'POST', {'cost': 200}) + fapiprivate_post_convert_acceptquote = fapiPrivatePostConvertAcceptQuote = Entry('convert/acceptQuote', 'fapiPrivate', 'POST', {'cost': 20}) + fapiprivate_put_listenkey = fapiPrivatePutListenKey = Entry('listenKey', 'fapiPrivate', 'PUT', {'cost': 1}) + fapiprivate_put_order = fapiPrivatePutOrder = Entry('order', 'fapiPrivate', 'PUT', {'cost': 1}) + fapiprivate_put_batchorders = fapiPrivatePutBatchOrders = Entry('batchOrders', 'fapiPrivate', 'PUT', {'cost': 5}) + fapiprivate_delete_batchorders = fapiPrivateDeleteBatchOrders = Entry('batchOrders', 'fapiPrivate', 'DELETE', {'cost': 1}) + fapiprivate_delete_order = fapiPrivateDeleteOrder = Entry('order', 'fapiPrivate', 'DELETE', {'cost': 1}) + fapiprivate_delete_allopenorders = fapiPrivateDeleteAllOpenOrders = Entry('allOpenOrders', 'fapiPrivate', 'DELETE', {'cost': 1}) + fapiprivate_delete_listenkey = fapiPrivateDeleteListenKey = Entry('listenKey', 'fapiPrivate', 'DELETE', {'cost': 1}) + fapipublicv2_get_ticker_price = fapiPublicV2GetTickerPrice = Entry('ticker/price', 'fapiPublicV2', 'GET', {'cost': 0}) + fapiprivatev2_get_account = fapiPrivateV2GetAccount = Entry('account', 'fapiPrivateV2', 'GET', {'cost': 1}) + fapiprivatev2_get_balance = fapiPrivateV2GetBalance = Entry('balance', 'fapiPrivateV2', 'GET', {'cost': 1}) + fapiprivatev2_get_positionrisk = fapiPrivateV2GetPositionRisk = Entry('positionRisk', 'fapiPrivateV2', 'GET', {'cost': 1}) + fapiprivatev3_get_account = fapiPrivateV3GetAccount = Entry('account', 'fapiPrivateV3', 'GET', {'cost': 1}) + fapiprivatev3_get_balance = fapiPrivateV3GetBalance = Entry('balance', 'fapiPrivateV3', 'GET', {'cost': 1}) + fapiprivatev3_get_positionrisk = fapiPrivateV3GetPositionRisk = Entry('positionRisk', 'fapiPrivateV3', 'GET', {'cost': 1}) + eapipublic_get_ping = eapiPublicGetPing = Entry('ping', 'eapiPublic', 'GET', {'cost': 1}) + eapipublic_get_time = eapiPublicGetTime = Entry('time', 'eapiPublic', 'GET', {'cost': 1}) + eapipublic_get_exchangeinfo = eapiPublicGetExchangeInfo = Entry('exchangeInfo', 'eapiPublic', 'GET', {'cost': 1}) + eapipublic_get_index = eapiPublicGetIndex = Entry('index', 'eapiPublic', 'GET', {'cost': 1}) + eapipublic_get_ticker = eapiPublicGetTicker = Entry('ticker', 'eapiPublic', 'GET', {'cost': 5}) + eapipublic_get_mark = eapiPublicGetMark = Entry('mark', 'eapiPublic', 'GET', {'cost': 5}) + eapipublic_get_depth = eapiPublicGetDepth = Entry('depth', 'eapiPublic', 'GET', {'cost': 1}) + eapipublic_get_klines = eapiPublicGetKlines = Entry('klines', 'eapiPublic', 'GET', {'cost': 1}) + eapipublic_get_trades = eapiPublicGetTrades = Entry('trades', 'eapiPublic', 'GET', {'cost': 5}) + eapipublic_get_historicaltrades = eapiPublicGetHistoricalTrades = Entry('historicalTrades', 'eapiPublic', 'GET', {'cost': 20}) + eapipublic_get_exercisehistory = eapiPublicGetExerciseHistory = Entry('exerciseHistory', 'eapiPublic', 'GET', {'cost': 3}) + eapipublic_get_openinterest = eapiPublicGetOpenInterest = Entry('openInterest', 'eapiPublic', 'GET', {'cost': 3}) + eapiprivate_get_account = eapiPrivateGetAccount = Entry('account', 'eapiPrivate', 'GET', {'cost': 3}) + eapiprivate_get_position = eapiPrivateGetPosition = Entry('position', 'eapiPrivate', 'GET', {'cost': 5}) + eapiprivate_get_openorders = eapiPrivateGetOpenOrders = Entry('openOrders', 'eapiPrivate', 'GET', {'cost': 1, 'noSymbol': 40}) + eapiprivate_get_historyorders = eapiPrivateGetHistoryOrders = Entry('historyOrders', 'eapiPrivate', 'GET', {'cost': 3}) + eapiprivate_get_usertrades = eapiPrivateGetUserTrades = Entry('userTrades', 'eapiPrivate', 'GET', {'cost': 5}) + eapiprivate_get_exerciserecord = eapiPrivateGetExerciseRecord = Entry('exerciseRecord', 'eapiPrivate', 'GET', {'cost': 5}) + eapiprivate_get_bill = eapiPrivateGetBill = Entry('bill', 'eapiPrivate', 'GET', {'cost': 1}) + eapiprivate_get_income_asyn = eapiPrivateGetIncomeAsyn = Entry('income/asyn', 'eapiPrivate', 'GET', {'cost': 5}) + eapiprivate_get_income_asyn_id = eapiPrivateGetIncomeAsynId = Entry('income/asyn/id', 'eapiPrivate', 'GET', {'cost': 5}) + eapiprivate_get_marginaccount = eapiPrivateGetMarginAccount = Entry('marginAccount', 'eapiPrivate', 'GET', {'cost': 3}) + eapiprivate_get_mmp = eapiPrivateGetMmp = Entry('mmp', 'eapiPrivate', 'GET', {'cost': 1}) + eapiprivate_get_countdowncancelall = eapiPrivateGetCountdownCancelAll = Entry('countdownCancelAll', 'eapiPrivate', 'GET', {'cost': 1}) + eapiprivate_get_order = eapiPrivateGetOrder = Entry('order', 'eapiPrivate', 'GET', {'cost': 1}) + eapiprivate_get_block_order_orders = eapiPrivateGetBlockOrderOrders = Entry('block/order/orders', 'eapiPrivate', 'GET', {'cost': 5}) + eapiprivate_get_block_order_execute = eapiPrivateGetBlockOrderExecute = Entry('block/order/execute', 'eapiPrivate', 'GET', {'cost': 5}) + eapiprivate_get_block_user_trades = eapiPrivateGetBlockUserTrades = Entry('block/user-trades', 'eapiPrivate', 'GET', {'cost': 5}) + eapiprivate_get_blocktrades = eapiPrivateGetBlockTrades = Entry('blockTrades', 'eapiPrivate', 'GET', {'cost': 5}) + eapiprivate_post_order = eapiPrivatePostOrder = Entry('order', 'eapiPrivate', 'POST', {'cost': 1}) + eapiprivate_post_batchorders = eapiPrivatePostBatchOrders = Entry('batchOrders', 'eapiPrivate', 'POST', {'cost': 5}) + eapiprivate_post_listenkey = eapiPrivatePostListenKey = Entry('listenKey', 'eapiPrivate', 'POST', {'cost': 1}) + eapiprivate_post_mmpset = eapiPrivatePostMmpSet = Entry('mmpSet', 'eapiPrivate', 'POST', {'cost': 1}) + eapiprivate_post_mmpreset = eapiPrivatePostMmpReset = Entry('mmpReset', 'eapiPrivate', 'POST', {'cost': 1}) + eapiprivate_post_countdowncancelall = eapiPrivatePostCountdownCancelAll = Entry('countdownCancelAll', 'eapiPrivate', 'POST', {'cost': 1}) + eapiprivate_post_countdowncancelallheartbeat = eapiPrivatePostCountdownCancelAllHeartBeat = Entry('countdownCancelAllHeartBeat', 'eapiPrivate', 'POST', {'cost': 10}) + eapiprivate_post_block_order_create = eapiPrivatePostBlockOrderCreate = Entry('block/order/create', 'eapiPrivate', 'POST', {'cost': 5}) + eapiprivate_post_block_order_execute = eapiPrivatePostBlockOrderExecute = Entry('block/order/execute', 'eapiPrivate', 'POST', {'cost': 5}) + eapiprivate_put_listenkey = eapiPrivatePutListenKey = Entry('listenKey', 'eapiPrivate', 'PUT', {'cost': 1}) + eapiprivate_put_block_order_create = eapiPrivatePutBlockOrderCreate = Entry('block/order/create', 'eapiPrivate', 'PUT', {'cost': 5}) + eapiprivate_delete_order = eapiPrivateDeleteOrder = Entry('order', 'eapiPrivate', 'DELETE', {'cost': 1}) + eapiprivate_delete_batchorders = eapiPrivateDeleteBatchOrders = Entry('batchOrders', 'eapiPrivate', 'DELETE', {'cost': 1}) + eapiprivate_delete_allopenorders = eapiPrivateDeleteAllOpenOrders = Entry('allOpenOrders', 'eapiPrivate', 'DELETE', {'cost': 1}) + eapiprivate_delete_allopenordersbyunderlying = eapiPrivateDeleteAllOpenOrdersByUnderlying = Entry('allOpenOrdersByUnderlying', 'eapiPrivate', 'DELETE', {'cost': 1}) + eapiprivate_delete_listenkey = eapiPrivateDeleteListenKey = Entry('listenKey', 'eapiPrivate', 'DELETE', {'cost': 1}) + eapiprivate_delete_block_order_create = eapiPrivateDeleteBlockOrderCreate = Entry('block/order/create', 'eapiPrivate', 'DELETE', {'cost': 5}) + public_get_ping = publicGetPing = Entry('ping', 'public', 'GET', {'cost': 0.2}) + public_get_time = publicGetTime = Entry('time', 'public', 'GET', {'cost': 0.2}) + public_get_depth = publicGetDepth = Entry('depth', 'public', 'GET', {'cost': 1, 'byLimit': [[100, 1], [500, 5], [1000, 10], [5000, 50]]}) + public_get_trades = publicGetTrades = Entry('trades', 'public', 'GET', {'cost': 2}) + public_get_aggtrades = publicGetAggTrades = Entry('aggTrades', 'public', 'GET', {'cost': 0.4}) + public_get_historicaltrades = publicGetHistoricalTrades = Entry('historicalTrades', 'public', 'GET', {'cost': 2}) + public_get_klines = publicGetKlines = Entry('klines', 'public', 'GET', {'cost': 0.4}) + public_get_uiklines = publicGetUiKlines = Entry('uiKlines', 'public', 'GET', {'cost': 0.4}) + public_get_ticker_24hr = publicGetTicker24hr = Entry('ticker/24hr', 'public', 'GET', {'cost': 0.4, 'noSymbol': 16}) + public_get_ticker = publicGetTicker = Entry('ticker', 'public', 'GET', {'cost': 0.4, 'noSymbol': 16}) + public_get_ticker_tradingday = publicGetTickerTradingDay = Entry('ticker/tradingDay', 'public', 'GET', {'cost': 0.8}) + public_get_ticker_price = publicGetTickerPrice = Entry('ticker/price', 'public', 'GET', {'cost': 0.4, 'noSymbol': 0.8}) + public_get_ticker_bookticker = publicGetTickerBookTicker = Entry('ticker/bookTicker', 'public', 'GET', {'cost': 0.4, 'noSymbol': 0.8}) + public_get_exchangeinfo = publicGetExchangeInfo = Entry('exchangeInfo', 'public', 'GET', {'cost': 4}) + public_get_avgprice = publicGetAvgPrice = Entry('avgPrice', 'public', 'GET', {'cost': 0.4}) + public_put_userdatastream = publicPutUserDataStream = Entry('userDataStream', 'public', 'PUT', {'cost': 0.4}) + public_post_userdatastream = publicPostUserDataStream = Entry('userDataStream', 'public', 'POST', {'cost': 0.4}) + public_delete_userdatastream = publicDeleteUserDataStream = Entry('userDataStream', 'public', 'DELETE', {'cost': 0.4}) + private_get_allorderlist = privateGetAllOrderList = Entry('allOrderList', 'private', 'GET', {'cost': 4}) + private_get_openorderlist = privateGetOpenOrderList = Entry('openOrderList', 'private', 'GET', {'cost': 1.2}) + private_get_orderlist = privateGetOrderList = Entry('orderList', 'private', 'GET', {'cost': 0.8}) + private_get_order = privateGetOrder = Entry('order', 'private', 'GET', {'cost': 0.8}) + private_get_openorders = privateGetOpenOrders = Entry('openOrders', 'private', 'GET', {'cost': 1.2, 'noSymbol': 16}) + private_get_allorders = privateGetAllOrders = Entry('allOrders', 'private', 'GET', {'cost': 4}) + private_get_account = privateGetAccount = Entry('account', 'private', 'GET', {'cost': 4}) + private_get_mytrades = privateGetMyTrades = Entry('myTrades', 'private', 'GET', {'cost': 4}) + private_get_ratelimit_order = privateGetRateLimitOrder = Entry('rateLimit/order', 'private', 'GET', {'cost': 8}) + private_get_mypreventedmatches = privateGetMyPreventedMatches = Entry('myPreventedMatches', 'private', 'GET', {'cost': 4}) + private_get_myallocations = privateGetMyAllocations = Entry('myAllocations', 'private', 'GET', {'cost': 4}) + private_get_account_commission = privateGetAccountCommission = Entry('account/commission', 'private', 'GET', {'cost': 4}) + private_post_order_oco = privatePostOrderOco = Entry('order/oco', 'private', 'POST', {'cost': 0.2}) + private_post_orderlist_oco = privatePostOrderListOco = Entry('orderList/oco', 'private', 'POST', {'cost': 0.2}) + private_post_orderlist_oto = privatePostOrderListOto = Entry('orderList/oto', 'private', 'POST', {'cost': 0.2}) + private_post_orderlist_otoco = privatePostOrderListOtoco = Entry('orderList/otoco', 'private', 'POST', {'cost': 0.2}) + private_post_sor_order = privatePostSorOrder = Entry('sor/order', 'private', 'POST', {'cost': 0.2}) + private_post_sor_order_test = privatePostSorOrderTest = Entry('sor/order/test', 'private', 'POST', {'cost': 0.2}) + private_post_order = privatePostOrder = Entry('order', 'private', 'POST', {'cost': 0.2}) + private_post_order_cancelreplace = privatePostOrderCancelReplace = Entry('order/cancelReplace', 'private', 'POST', {'cost': 0.2}) + private_post_order_test = privatePostOrderTest = Entry('order/test', 'private', 'POST', {'cost': 0.2}) + private_delete_openorders = privateDeleteOpenOrders = Entry('openOrders', 'private', 'DELETE', {'cost': 0.2}) + private_delete_orderlist = privateDeleteOrderList = Entry('orderList', 'private', 'DELETE', {'cost': 0.2}) + private_delete_order = privateDeleteOrder = Entry('order', 'private', 'DELETE', {'cost': 0.2}) + papi_get_ping = papiGetPing = Entry('ping', 'papi', 'GET', {'cost': 0.2}) + papi_get_um_order = papiGetUmOrder = Entry('um/order', 'papi', 'GET', {'cost': 1}) + papi_get_um_openorder = papiGetUmOpenOrder = Entry('um/openOrder', 'papi', 'GET', {'cost': 1}) + papi_get_um_openorders = papiGetUmOpenOrders = Entry('um/openOrders', 'papi', 'GET', {'cost': 1, 'noSymbol': 40}) + papi_get_um_allorders = papiGetUmAllOrders = Entry('um/allOrders', 'papi', 'GET', {'cost': 5}) + papi_get_cm_order = papiGetCmOrder = Entry('cm/order', 'papi', 'GET', {'cost': 1}) + papi_get_cm_openorder = papiGetCmOpenOrder = Entry('cm/openOrder', 'papi', 'GET', {'cost': 1}) + papi_get_cm_openorders = papiGetCmOpenOrders = Entry('cm/openOrders', 'papi', 'GET', {'cost': 1, 'noSymbol': 40}) + papi_get_cm_allorders = papiGetCmAllOrders = Entry('cm/allOrders', 'papi', 'GET', {'cost': 20}) + papi_get_um_conditional_openorder = papiGetUmConditionalOpenOrder = Entry('um/conditional/openOrder', 'papi', 'GET', {'cost': 1}) + papi_get_um_conditional_openorders = papiGetUmConditionalOpenOrders = Entry('um/conditional/openOrders', 'papi', 'GET', {'cost': 1, 'noSymbol': 40}) + papi_get_um_conditional_orderhistory = papiGetUmConditionalOrderHistory = Entry('um/conditional/orderHistory', 'papi', 'GET', {'cost': 1}) + papi_get_um_conditional_allorders = papiGetUmConditionalAllOrders = Entry('um/conditional/allOrders', 'papi', 'GET', {'cost': 1, 'noSymbol': 40}) + papi_get_cm_conditional_openorder = papiGetCmConditionalOpenOrder = Entry('cm/conditional/openOrder', 'papi', 'GET', {'cost': 1}) + papi_get_cm_conditional_openorders = papiGetCmConditionalOpenOrders = Entry('cm/conditional/openOrders', 'papi', 'GET', {'cost': 1, 'noSymbol': 40}) + papi_get_cm_conditional_orderhistory = papiGetCmConditionalOrderHistory = Entry('cm/conditional/orderHistory', 'papi', 'GET', {'cost': 1}) + papi_get_cm_conditional_allorders = papiGetCmConditionalAllOrders = Entry('cm/conditional/allOrders', 'papi', 'GET', {'cost': 40}) + papi_get_margin_order = papiGetMarginOrder = Entry('margin/order', 'papi', 'GET', {'cost': 10}) + papi_get_margin_openorders = papiGetMarginOpenOrders = Entry('margin/openOrders', 'papi', 'GET', {'cost': 5}) + papi_get_margin_allorders = papiGetMarginAllOrders = Entry('margin/allOrders', 'papi', 'GET', {'cost': 100}) + papi_get_margin_orderlist = papiGetMarginOrderList = Entry('margin/orderList', 'papi', 'GET', {'cost': 5}) + papi_get_margin_allorderlist = papiGetMarginAllOrderList = Entry('margin/allOrderList', 'papi', 'GET', {'cost': 100}) + papi_get_margin_openorderlist = papiGetMarginOpenOrderList = Entry('margin/openOrderList', 'papi', 'GET', {'cost': 5}) + papi_get_margin_mytrades = papiGetMarginMyTrades = Entry('margin/myTrades', 'papi', 'GET', {'cost': 5}) + papi_get_balance = papiGetBalance = Entry('balance', 'papi', 'GET', {'cost': 4}) + papi_get_account = papiGetAccount = Entry('account', 'papi', 'GET', {'cost': 4}) + papi_get_margin_maxborrowable = papiGetMarginMaxBorrowable = Entry('margin/maxBorrowable', 'papi', 'GET', {'cost': 1}) + papi_get_margin_maxwithdraw = papiGetMarginMaxWithdraw = Entry('margin/maxWithdraw', 'papi', 'GET', {'cost': 1}) + papi_get_um_positionrisk = papiGetUmPositionRisk = Entry('um/positionRisk', 'papi', 'GET', {'cost': 1}) + papi_get_cm_positionrisk = papiGetCmPositionRisk = Entry('cm/positionRisk', 'papi', 'GET', {'cost': 0.2}) + papi_get_um_positionside_dual = papiGetUmPositionSideDual = Entry('um/positionSide/dual', 'papi', 'GET', {'cost': 6}) + papi_get_cm_positionside_dual = papiGetCmPositionSideDual = Entry('cm/positionSide/dual', 'papi', 'GET', {'cost': 6}) + papi_get_um_usertrades = papiGetUmUserTrades = Entry('um/userTrades', 'papi', 'GET', {'cost': 5}) + papi_get_cm_usertrades = papiGetCmUserTrades = Entry('cm/userTrades', 'papi', 'GET', {'cost': 20}) + papi_get_um_leveragebracket = papiGetUmLeverageBracket = Entry('um/leverageBracket', 'papi', 'GET', {'cost': 0.2}) + papi_get_cm_leveragebracket = papiGetCmLeverageBracket = Entry('cm/leverageBracket', 'papi', 'GET', {'cost': 0.2}) + papi_get_margin_forceorders = papiGetMarginForceOrders = Entry('margin/forceOrders', 'papi', 'GET', {'cost': 1}) + papi_get_um_forceorders = papiGetUmForceOrders = Entry('um/forceOrders', 'papi', 'GET', {'cost': 20, 'noSymbol': 50}) + papi_get_cm_forceorders = papiGetCmForceOrders = Entry('cm/forceOrders', 'papi', 'GET', {'cost': 20, 'noSymbol': 50}) + papi_get_um_apitradingstatus = papiGetUmApiTradingStatus = Entry('um/apiTradingStatus', 'papi', 'GET', {'cost': 0.2, 'noSymbol': 2}) + papi_get_um_commissionrate = papiGetUmCommissionRate = Entry('um/commissionRate', 'papi', 'GET', {'cost': 4}) + papi_get_cm_commissionrate = papiGetCmCommissionRate = Entry('cm/commissionRate', 'papi', 'GET', {'cost': 4}) + papi_get_margin_marginloan = papiGetMarginMarginLoan = Entry('margin/marginLoan', 'papi', 'GET', {'cost': 2}) + papi_get_margin_repayloan = papiGetMarginRepayLoan = Entry('margin/repayLoan', 'papi', 'GET', {'cost': 2}) + papi_get_margin_margininteresthistory = papiGetMarginMarginInterestHistory = Entry('margin/marginInterestHistory', 'papi', 'GET', {'cost': 0.2}) + papi_get_portfolio_interest_history = papiGetPortfolioInterestHistory = Entry('portfolio/interest-history', 'papi', 'GET', {'cost': 10}) + papi_get_um_income = papiGetUmIncome = Entry('um/income', 'papi', 'GET', {'cost': 6}) + papi_get_cm_income = papiGetCmIncome = Entry('cm/income', 'papi', 'GET', {'cost': 6}) + papi_get_um_account = papiGetUmAccount = Entry('um/account', 'papi', 'GET', {'cost': 1}) + papi_get_cm_account = papiGetCmAccount = Entry('cm/account', 'papi', 'GET', {'cost': 1}) + papi_get_repay_futures_switch = papiGetRepayFuturesSwitch = Entry('repay-futures-switch', 'papi', 'GET', {'cost': 6}) + papi_get_um_adlquantile = papiGetUmAdlQuantile = Entry('um/adlQuantile', 'papi', 'GET', {'cost': 5}) + papi_get_cm_adlquantile = papiGetCmAdlQuantile = Entry('cm/adlQuantile', 'papi', 'GET', {'cost': 5}) + papi_get_um_trade_asyn = papiGetUmTradeAsyn = Entry('um/trade/asyn', 'papi', 'GET', {'cost': 300}) + papi_get_um_trade_asyn_id = papiGetUmTradeAsynId = Entry('um/trade/asyn/id', 'papi', 'GET', {'cost': 2}) + papi_get_um_order_asyn = papiGetUmOrderAsyn = Entry('um/order/asyn', 'papi', 'GET', {'cost': 300}) + papi_get_um_order_asyn_id = papiGetUmOrderAsynId = Entry('um/order/asyn/id', 'papi', 'GET', {'cost': 2}) + papi_get_um_income_asyn = papiGetUmIncomeAsyn = Entry('um/income/asyn', 'papi', 'GET', {'cost': 300}) + papi_get_um_income_asyn_id = papiGetUmIncomeAsynId = Entry('um/income/asyn/id', 'papi', 'GET', {'cost': 2}) + papi_get_um_orderamendment = papiGetUmOrderAmendment = Entry('um/orderAmendment', 'papi', 'GET', {'cost': 1}) + papi_get_cm_orderamendment = papiGetCmOrderAmendment = Entry('cm/orderAmendment', 'papi', 'GET', {'cost': 1}) + papi_get_um_feeburn = papiGetUmFeeBurn = Entry('um/feeBurn', 'papi', 'GET', {'cost': 30}) + papi_get_um_accountconfig = papiGetUmAccountConfig = Entry('um/accountConfig', 'papi', 'GET', {'cost': 1}) + papi_get_um_symbolconfig = papiGetUmSymbolConfig = Entry('um/symbolConfig', 'papi', 'GET', {'cost': 1}) + papi_get_cm_accountconfig = papiGetCmAccountConfig = Entry('cm/accountConfig', 'papi', 'GET', {'cost': 1}) + papi_get_cm_symbolconfig = papiGetCmSymbolConfig = Entry('cm/symbolConfig', 'papi', 'GET', {'cost': 1}) + papi_get_ratelimit_order = papiGetRateLimitOrder = Entry('rateLimit/order', 'papi', 'GET', {'cost': 1}) + papi_post_um_order = papiPostUmOrder = Entry('um/order', 'papi', 'POST', {'cost': 1}) + papi_post_um_conditional_order = papiPostUmConditionalOrder = Entry('um/conditional/order', 'papi', 'POST', {'cost': 1}) + papi_post_cm_order = papiPostCmOrder = Entry('cm/order', 'papi', 'POST', {'cost': 1}) + papi_post_cm_conditional_order = papiPostCmConditionalOrder = Entry('cm/conditional/order', 'papi', 'POST', {'cost': 1}) + papi_post_margin_order = papiPostMarginOrder = Entry('margin/order', 'papi', 'POST', {'cost': 1}) + papi_post_marginloan = papiPostMarginLoan = Entry('marginLoan', 'papi', 'POST', {'cost': 100}) + papi_post_repayloan = papiPostRepayLoan = Entry('repayLoan', 'papi', 'POST', {'cost': 100}) + papi_post_margin_order_oco = papiPostMarginOrderOco = Entry('margin/order/oco', 'papi', 'POST', {'cost': 1}) + papi_post_um_leverage = papiPostUmLeverage = Entry('um/leverage', 'papi', 'POST', {'cost': 0.2}) + papi_post_cm_leverage = papiPostCmLeverage = Entry('cm/leverage', 'papi', 'POST', {'cost': 0.2}) + papi_post_um_positionside_dual = papiPostUmPositionSideDual = Entry('um/positionSide/dual', 'papi', 'POST', {'cost': 0.2}) + papi_post_cm_positionside_dual = papiPostCmPositionSideDual = Entry('cm/positionSide/dual', 'papi', 'POST', {'cost': 0.2}) + papi_post_auto_collection = papiPostAutoCollection = Entry('auto-collection', 'papi', 'POST', {'cost': 150}) + papi_post_bnb_transfer = papiPostBnbTransfer = Entry('bnb-transfer', 'papi', 'POST', {'cost': 150}) + papi_post_repay_futures_switch = papiPostRepayFuturesSwitch = Entry('repay-futures-switch', 'papi', 'POST', {'cost': 150}) + papi_post_repay_futures_negative_balance = papiPostRepayFuturesNegativeBalance = Entry('repay-futures-negative-balance', 'papi', 'POST', {'cost': 150}) + papi_post_listenkey = papiPostListenKey = Entry('listenKey', 'papi', 'POST', {'cost': 0.2}) + papi_post_asset_collection = papiPostAssetCollection = Entry('asset-collection', 'papi', 'POST', {'cost': 6}) + papi_post_margin_repay_debt = papiPostMarginRepayDebt = Entry('margin/repay-debt', 'papi', 'POST', {'cost': 3000}) + papi_post_um_feeburn = papiPostUmFeeBurn = Entry('um/feeBurn', 'papi', 'POST', {'cost': 1}) + papi_put_listenkey = papiPutListenKey = Entry('listenKey', 'papi', 'PUT', {'cost': 0.2}) + papi_put_um_order = papiPutUmOrder = Entry('um/order', 'papi', 'PUT', {'cost': 1}) + papi_put_cm_order = papiPutCmOrder = Entry('cm/order', 'papi', 'PUT', {'cost': 1}) + papi_delete_um_order = papiDeleteUmOrder = Entry('um/order', 'papi', 'DELETE', {'cost': 1}) + papi_delete_um_conditional_order = papiDeleteUmConditionalOrder = Entry('um/conditional/order', 'papi', 'DELETE', {'cost': 1}) + papi_delete_um_allopenorders = papiDeleteUmAllOpenOrders = Entry('um/allOpenOrders', 'papi', 'DELETE', {'cost': 1}) + papi_delete_um_conditional_allopenorders = papiDeleteUmConditionalAllOpenOrders = Entry('um/conditional/allOpenOrders', 'papi', 'DELETE', {'cost': 1}) + papi_delete_cm_order = papiDeleteCmOrder = Entry('cm/order', 'papi', 'DELETE', {'cost': 1}) + papi_delete_cm_conditional_order = papiDeleteCmConditionalOrder = Entry('cm/conditional/order', 'papi', 'DELETE', {'cost': 1}) + papi_delete_cm_allopenorders = papiDeleteCmAllOpenOrders = Entry('cm/allOpenOrders', 'papi', 'DELETE', {'cost': 1}) + papi_delete_cm_conditional_allopenorders = papiDeleteCmConditionalAllOpenOrders = Entry('cm/conditional/allOpenOrders', 'papi', 'DELETE', {'cost': 1}) + papi_delete_margin_order = papiDeleteMarginOrder = Entry('margin/order', 'papi', 'DELETE', {'cost': 2}) + papi_delete_margin_allopenorders = papiDeleteMarginAllOpenOrders = Entry('margin/allOpenOrders', 'papi', 'DELETE', {'cost': 5}) + papi_delete_margin_orderlist = papiDeleteMarginOrderList = Entry('margin/orderList', 'papi', 'DELETE', {'cost': 2}) + papi_delete_listenkey = papiDeleteListenKey = Entry('listenKey', 'papi', 'DELETE', {'cost': 0.2}) + papiv2_get_um_account = papiV2GetUmAccount = Entry('um/account', 'papiV2', 'GET', {'cost': 1}) diff --git a/ccxt/abstract/binancecoinm.py b/ccxt/abstract/binancecoinm.py new file mode 100644 index 0000000..7893621 --- /dev/null +++ b/ccxt/abstract/binancecoinm.py @@ -0,0 +1,771 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + sapi_get_copytrading_futures_userstatus = sapiGetCopyTradingFuturesUserStatus = Entry('copyTrading/futures/userStatus', 'sapi', 'GET', {'cost': 2}) + sapi_get_copytrading_futures_leadsymbol = sapiGetCopyTradingFuturesLeadSymbol = Entry('copyTrading/futures/leadSymbol', 'sapi', 'GET', {'cost': 2}) + sapi_get_system_status = sapiGetSystemStatus = Entry('system/status', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_accountsnapshot = sapiGetAccountSnapshot = Entry('accountSnapshot', 'sapi', 'GET', {'cost': 240}) + sapi_get_account_info = sapiGetAccountInfo = Entry('account/info', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_margin_asset = sapiGetMarginAsset = Entry('margin/asset', 'sapi', 'GET', {'cost': 1}) + sapi_get_margin_pair = sapiGetMarginPair = Entry('margin/pair', 'sapi', 'GET', {'cost': 1}) + sapi_get_margin_allassets = sapiGetMarginAllAssets = Entry('margin/allAssets', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_margin_allpairs = sapiGetMarginAllPairs = Entry('margin/allPairs', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_margin_priceindex = sapiGetMarginPriceIndex = Entry('margin/priceIndex', 'sapi', 'GET', {'cost': 1}) + sapi_get_spot_delist_schedule = sapiGetSpotDelistSchedule = Entry('spot/delist-schedule', 'sapi', 'GET', {'cost': 10}) + sapi_get_asset_assetdividend = sapiGetAssetAssetDividend = Entry('asset/assetDividend', 'sapi', 'GET', {'cost': 1}) + sapi_get_asset_dribblet = sapiGetAssetDribblet = Entry('asset/dribblet', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_asset_transfer = sapiGetAssetTransfer = Entry('asset/transfer', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_asset_assetdetail = sapiGetAssetAssetDetail = Entry('asset/assetDetail', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_asset_tradefee = sapiGetAssetTradeFee = Entry('asset/tradeFee', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_asset_ledger_transfer_cloud_mining_querybypage = sapiGetAssetLedgerTransferCloudMiningQueryByPage = Entry('asset/ledger-transfer/cloud-mining/queryByPage', 'sapi', 'GET', {'cost': 4.0002}) + sapi_get_asset_convert_transfer_querybypage = sapiGetAssetConvertTransferQueryByPage = Entry('asset/convert-transfer/queryByPage', 'sapi', 'GET', {'cost': 0.033335}) + sapi_get_asset_wallet_balance = sapiGetAssetWalletBalance = Entry('asset/wallet/balance', 'sapi', 'GET', {'cost': 6}) + sapi_get_asset_custody_transfer_history = sapiGetAssetCustodyTransferHistory = Entry('asset/custody/transfer-history', 'sapi', 'GET', {'cost': 6}) + sapi_get_margin_borrow_repay = sapiGetMarginBorrowRepay = Entry('margin/borrow-repay', 'sapi', 'GET', {'cost': 1}) + sapi_get_margin_loan = sapiGetMarginLoan = Entry('margin/loan', 'sapi', 'GET', {'cost': 1}) + sapi_get_margin_repay = sapiGetMarginRepay = Entry('margin/repay', 'sapi', 'GET', {'cost': 1}) + sapi_get_margin_account = sapiGetMarginAccount = Entry('margin/account', 'sapi', 'GET', {'cost': 1}) + sapi_get_margin_transfer = sapiGetMarginTransfer = Entry('margin/transfer', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_margin_interesthistory = sapiGetMarginInterestHistory = Entry('margin/interestHistory', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_margin_forceliquidationrec = sapiGetMarginForceLiquidationRec = Entry('margin/forceLiquidationRec', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_margin_order = sapiGetMarginOrder = Entry('margin/order', 'sapi', 'GET', {'cost': 1}) + sapi_get_margin_openorders = sapiGetMarginOpenOrders = Entry('margin/openOrders', 'sapi', 'GET', {'cost': 1}) + sapi_get_margin_allorders = sapiGetMarginAllOrders = Entry('margin/allOrders', 'sapi', 'GET', {'cost': 20}) + sapi_get_margin_mytrades = sapiGetMarginMyTrades = Entry('margin/myTrades', 'sapi', 'GET', {'cost': 1}) + sapi_get_margin_maxborrowable = sapiGetMarginMaxBorrowable = Entry('margin/maxBorrowable', 'sapi', 'GET', {'cost': 5}) + sapi_get_margin_maxtransferable = sapiGetMarginMaxTransferable = Entry('margin/maxTransferable', 'sapi', 'GET', {'cost': 5}) + sapi_get_margin_tradecoeff = sapiGetMarginTradeCoeff = Entry('margin/tradeCoeff', 'sapi', 'GET', {'cost': 1}) + sapi_get_margin_isolated_transfer = sapiGetMarginIsolatedTransfer = Entry('margin/isolated/transfer', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_margin_isolated_account = sapiGetMarginIsolatedAccount = Entry('margin/isolated/account', 'sapi', 'GET', {'cost': 1}) + sapi_get_margin_isolated_pair = sapiGetMarginIsolatedPair = Entry('margin/isolated/pair', 'sapi', 'GET', {'cost': 1}) + sapi_get_margin_isolated_allpairs = sapiGetMarginIsolatedAllPairs = Entry('margin/isolated/allPairs', 'sapi', 'GET', {'cost': 1}) + sapi_get_margin_isolated_accountlimit = sapiGetMarginIsolatedAccountLimit = Entry('margin/isolated/accountLimit', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_margin_interestratehistory = sapiGetMarginInterestRateHistory = Entry('margin/interestRateHistory', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_margin_orderlist = sapiGetMarginOrderList = Entry('margin/orderList', 'sapi', 'GET', {'cost': 1}) + sapi_get_margin_allorderlist = sapiGetMarginAllOrderList = Entry('margin/allOrderList', 'sapi', 'GET', {'cost': 20}) + sapi_get_margin_openorderlist = sapiGetMarginOpenOrderList = Entry('margin/openOrderList', 'sapi', 'GET', {'cost': 1}) + sapi_get_margin_crossmargindata = sapiGetMarginCrossMarginData = Entry('margin/crossMarginData', 'sapi', 'GET', {'cost': 0.1, 'noCoin': 0.5}) + sapi_get_margin_isolatedmargindata = sapiGetMarginIsolatedMarginData = Entry('margin/isolatedMarginData', 'sapi', 'GET', {'cost': 0.1, 'noCoin': 1}) + sapi_get_margin_isolatedmargintier = sapiGetMarginIsolatedMarginTier = Entry('margin/isolatedMarginTier', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_margin_ratelimit_order = sapiGetMarginRateLimitOrder = Entry('margin/rateLimit/order', 'sapi', 'GET', {'cost': 2}) + sapi_get_margin_dribblet = sapiGetMarginDribblet = Entry('margin/dribblet', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_margin_dust = sapiGetMarginDust = Entry('margin/dust', 'sapi', 'GET', {'cost': 20.001}) + sapi_get_margin_crossmargincollateralratio = sapiGetMarginCrossMarginCollateralRatio = Entry('margin/crossMarginCollateralRatio', 'sapi', 'GET', {'cost': 10}) + sapi_get_margin_exchange_small_liability = sapiGetMarginExchangeSmallLiability = Entry('margin/exchange-small-liability', 'sapi', 'GET', {'cost': 0.6667}) + sapi_get_margin_exchange_small_liability_history = sapiGetMarginExchangeSmallLiabilityHistory = Entry('margin/exchange-small-liability-history', 'sapi', 'GET', {'cost': 0.6667}) + sapi_get_margin_next_hourly_interest_rate = sapiGetMarginNextHourlyInterestRate = Entry('margin/next-hourly-interest-rate', 'sapi', 'GET', {'cost': 0.6667}) + sapi_get_margin_capital_flow = sapiGetMarginCapitalFlow = Entry('margin/capital-flow', 'sapi', 'GET', {'cost': 10}) + sapi_get_margin_delist_schedule = sapiGetMarginDelistSchedule = Entry('margin/delist-schedule', 'sapi', 'GET', {'cost': 10}) + sapi_get_margin_available_inventory = sapiGetMarginAvailableInventory = Entry('margin/available-inventory', 'sapi', 'GET', {'cost': 0.3334}) + sapi_get_margin_leveragebracket = sapiGetMarginLeverageBracket = Entry('margin/leverageBracket', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_loan_vip_loanable_data = sapiGetLoanVipLoanableData = Entry('loan/vip/loanable/data', 'sapi', 'GET', {'cost': 40}) + sapi_get_loan_vip_collateral_data = sapiGetLoanVipCollateralData = Entry('loan/vip/collateral/data', 'sapi', 'GET', {'cost': 40}) + sapi_get_loan_vip_request_data = sapiGetLoanVipRequestData = Entry('loan/vip/request/data', 'sapi', 'GET', {'cost': 2.6668}) + sapi_get_loan_vip_request_interestrate = sapiGetLoanVipRequestInterestRate = Entry('loan/vip/request/interestRate', 'sapi', 'GET', {'cost': 2.6668}) + sapi_get_loan_income = sapiGetLoanIncome = Entry('loan/income', 'sapi', 'GET', {'cost': 40.002}) + sapi_get_loan_ongoing_orders = sapiGetLoanOngoingOrders = Entry('loan/ongoing/orders', 'sapi', 'GET', {'cost': 40}) + sapi_get_loan_ltv_adjustment_history = sapiGetLoanLtvAdjustmentHistory = Entry('loan/ltv/adjustment/history', 'sapi', 'GET', {'cost': 40}) + sapi_get_loan_borrow_history = sapiGetLoanBorrowHistory = Entry('loan/borrow/history', 'sapi', 'GET', {'cost': 40}) + sapi_get_loan_repay_history = sapiGetLoanRepayHistory = Entry('loan/repay/history', 'sapi', 'GET', {'cost': 40}) + sapi_get_loan_loanable_data = sapiGetLoanLoanableData = Entry('loan/loanable/data', 'sapi', 'GET', {'cost': 40}) + sapi_get_loan_collateral_data = sapiGetLoanCollateralData = Entry('loan/collateral/data', 'sapi', 'GET', {'cost': 40}) + sapi_get_loan_repay_collateral_rate = sapiGetLoanRepayCollateralRate = Entry('loan/repay/collateral/rate', 'sapi', 'GET', {'cost': 600}) + sapi_get_loan_flexible_ongoing_orders = sapiGetLoanFlexibleOngoingOrders = Entry('loan/flexible/ongoing/orders', 'sapi', 'GET', {'cost': 30}) + sapi_get_loan_flexible_borrow_history = sapiGetLoanFlexibleBorrowHistory = Entry('loan/flexible/borrow/history', 'sapi', 'GET', {'cost': 40}) + sapi_get_loan_flexible_repay_history = sapiGetLoanFlexibleRepayHistory = Entry('loan/flexible/repay/history', 'sapi', 'GET', {'cost': 40}) + sapi_get_loan_flexible_ltv_adjustment_history = sapiGetLoanFlexibleLtvAdjustmentHistory = Entry('loan/flexible/ltv/adjustment/history', 'sapi', 'GET', {'cost': 40}) + sapi_get_loan_vip_ongoing_orders = sapiGetLoanVipOngoingOrders = Entry('loan/vip/ongoing/orders', 'sapi', 'GET', {'cost': 40}) + sapi_get_loan_vip_repay_history = sapiGetLoanVipRepayHistory = Entry('loan/vip/repay/history', 'sapi', 'GET', {'cost': 40}) + sapi_get_loan_vip_collateral_account = sapiGetLoanVipCollateralAccount = Entry('loan/vip/collateral/account', 'sapi', 'GET', {'cost': 600}) + sapi_get_fiat_orders = sapiGetFiatOrders = Entry('fiat/orders', 'sapi', 'GET', {'cost': 600.03}) + sapi_get_fiat_payments = sapiGetFiatPayments = Entry('fiat/payments', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_futures_transfer = sapiGetFuturesTransfer = Entry('futures/transfer', 'sapi', 'GET', {'cost': 1}) + sapi_get_futures_histdatalink = sapiGetFuturesHistDataLink = Entry('futures/histDataLink', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_rebate_taxquery = sapiGetRebateTaxQuery = Entry('rebate/taxQuery', 'sapi', 'GET', {'cost': 80.004}) + sapi_get_capital_config_getall = sapiGetCapitalConfigGetall = Entry('capital/config/getall', 'sapi', 'GET', {'cost': 1}) + sapi_get_capital_deposit_address = sapiGetCapitalDepositAddress = Entry('capital/deposit/address', 'sapi', 'GET', {'cost': 1}) + sapi_get_capital_deposit_address_list = sapiGetCapitalDepositAddressList = Entry('capital/deposit/address/list', 'sapi', 'GET', {'cost': 1}) + sapi_get_capital_deposit_hisrec = sapiGetCapitalDepositHisrec = Entry('capital/deposit/hisrec', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_capital_deposit_subaddress = sapiGetCapitalDepositSubAddress = Entry('capital/deposit/subAddress', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_capital_deposit_subhisrec = sapiGetCapitalDepositSubHisrec = Entry('capital/deposit/subHisrec', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_capital_withdraw_history = sapiGetCapitalWithdrawHistory = Entry('capital/withdraw/history', 'sapi', 'GET', {'cost': 2}) + sapi_get_capital_withdraw_address_list = sapiGetCapitalWithdrawAddressList = Entry('capital/withdraw/address/list', 'sapi', 'GET', {'cost': 10}) + sapi_get_capital_contract_convertible_coins = sapiGetCapitalContractConvertibleCoins = Entry('capital/contract/convertible-coins', 'sapi', 'GET', {'cost': 4.0002}) + sapi_get_convert_tradeflow = sapiGetConvertTradeFlow = Entry('convert/tradeFlow', 'sapi', 'GET', {'cost': 20.001}) + sapi_get_convert_exchangeinfo = sapiGetConvertExchangeInfo = Entry('convert/exchangeInfo', 'sapi', 'GET', {'cost': 50}) + sapi_get_convert_assetinfo = sapiGetConvertAssetInfo = Entry('convert/assetInfo', 'sapi', 'GET', {'cost': 10}) + sapi_get_convert_orderstatus = sapiGetConvertOrderStatus = Entry('convert/orderStatus', 'sapi', 'GET', {'cost': 0.6667}) + sapi_get_convert_limit_queryopenorders = sapiGetConvertLimitQueryOpenOrders = Entry('convert/limit/queryOpenOrders', 'sapi', 'GET', {'cost': 20.001}) + sapi_get_account_status = sapiGetAccountStatus = Entry('account/status', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_account_apitradingstatus = sapiGetAccountApiTradingStatus = Entry('account/apiTradingStatus', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_account_apirestrictions_iprestriction = sapiGetAccountApiRestrictionsIpRestriction = Entry('account/apiRestrictions/ipRestriction', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_bnbburn = sapiGetBnbBurn = Entry('bnbBurn', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_sub_account_futures_account = sapiGetSubAccountFuturesAccount = Entry('sub-account/futures/account', 'sapi', 'GET', {'cost': 1}) + sapi_get_sub_account_futures_accountsummary = sapiGetSubAccountFuturesAccountSummary = Entry('sub-account/futures/accountSummary', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_sub_account_futures_positionrisk = sapiGetSubAccountFuturesPositionRisk = Entry('sub-account/futures/positionRisk', 'sapi', 'GET', {'cost': 1}) + sapi_get_sub_account_futures_internaltransfer = sapiGetSubAccountFuturesInternalTransfer = Entry('sub-account/futures/internalTransfer', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_sub_account_list = sapiGetSubAccountList = Entry('sub-account/list', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_sub_account_margin_account = sapiGetSubAccountMarginAccount = Entry('sub-account/margin/account', 'sapi', 'GET', {'cost': 1}) + sapi_get_sub_account_margin_accountsummary = sapiGetSubAccountMarginAccountSummary = Entry('sub-account/margin/accountSummary', 'sapi', 'GET', {'cost': 1}) + sapi_get_sub_account_spotsummary = sapiGetSubAccountSpotSummary = Entry('sub-account/spotSummary', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_sub_account_status = sapiGetSubAccountStatus = Entry('sub-account/status', 'sapi', 'GET', {'cost': 1}) + sapi_get_sub_account_sub_transfer_history = sapiGetSubAccountSubTransferHistory = Entry('sub-account/sub/transfer/history', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_sub_account_transfer_subuserhistory = sapiGetSubAccountTransferSubUserHistory = Entry('sub-account/transfer/subUserHistory', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_sub_account_universaltransfer = sapiGetSubAccountUniversalTransfer = Entry('sub-account/universalTransfer', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_sub_account_apirestrictions_iprestriction_thirdpartylist = sapiGetSubAccountApiRestrictionsIpRestrictionThirdPartyList = Entry('sub-account/apiRestrictions/ipRestriction/thirdPartyList', 'sapi', 'GET', {'cost': 1}) + sapi_get_sub_account_transaction_statistics = sapiGetSubAccountTransactionStatistics = Entry('sub-account/transaction-statistics', 'sapi', 'GET', {'cost': 0.40002}) + sapi_get_sub_account_subaccountapi_iprestriction = sapiGetSubAccountSubAccountApiIpRestriction = Entry('sub-account/subAccountApi/ipRestriction', 'sapi', 'GET', {'cost': 20.001}) + sapi_get_managed_subaccount_asset = sapiGetManagedSubaccountAsset = Entry('managed-subaccount/asset', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_managed_subaccount_accountsnapshot = sapiGetManagedSubaccountAccountSnapshot = Entry('managed-subaccount/accountSnapshot', 'sapi', 'GET', {'cost': 240}) + sapi_get_managed_subaccount_querytranslogforinvestor = sapiGetManagedSubaccountQueryTransLogForInvestor = Entry('managed-subaccount/queryTransLogForInvestor', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_managed_subaccount_querytranslogfortradeparent = sapiGetManagedSubaccountQueryTransLogForTradeParent = Entry('managed-subaccount/queryTransLogForTradeParent', 'sapi', 'GET', {'cost': 0.40002}) + sapi_get_managed_subaccount_fetch_future_asset = sapiGetManagedSubaccountFetchFutureAsset = Entry('managed-subaccount/fetch-future-asset', 'sapi', 'GET', {'cost': 0.40002}) + sapi_get_managed_subaccount_marginasset = sapiGetManagedSubaccountMarginAsset = Entry('managed-subaccount/marginAsset', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_managed_subaccount_info = sapiGetManagedSubaccountInfo = Entry('managed-subaccount/info', 'sapi', 'GET', {'cost': 0.40002}) + sapi_get_managed_subaccount_deposit_address = sapiGetManagedSubaccountDepositAddress = Entry('managed-subaccount/deposit/address', 'sapi', 'GET', {'cost': 0.006667}) + sapi_get_managed_subaccount_query_trans_log = sapiGetManagedSubaccountQueryTransLog = Entry('managed-subaccount/query-trans-log', 'sapi', 'GET', {'cost': 0.40002}) + sapi_get_lending_daily_product_list = sapiGetLendingDailyProductList = Entry('lending/daily/product/list', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_daily_userleftquota = sapiGetLendingDailyUserLeftQuota = Entry('lending/daily/userLeftQuota', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_daily_userredemptionquota = sapiGetLendingDailyUserRedemptionQuota = Entry('lending/daily/userRedemptionQuota', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_daily_token_position = sapiGetLendingDailyTokenPosition = Entry('lending/daily/token/position', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_union_account = sapiGetLendingUnionAccount = Entry('lending/union/account', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_union_purchaserecord = sapiGetLendingUnionPurchaseRecord = Entry('lending/union/purchaseRecord', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_union_redemptionrecord = sapiGetLendingUnionRedemptionRecord = Entry('lending/union/redemptionRecord', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_union_interesthistory = sapiGetLendingUnionInterestHistory = Entry('lending/union/interestHistory', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_project_list = sapiGetLendingProjectList = Entry('lending/project/list', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_project_position_list = sapiGetLendingProjectPositionList = Entry('lending/project/position/list', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_eth_staking_eth_history_stakinghistory = sapiGetEthStakingEthHistoryStakingHistory = Entry('eth-staking/eth/history/stakingHistory', 'sapi', 'GET', {'cost': 15}) + sapi_get_eth_staking_eth_history_redemptionhistory = sapiGetEthStakingEthHistoryRedemptionHistory = Entry('eth-staking/eth/history/redemptionHistory', 'sapi', 'GET', {'cost': 15}) + sapi_get_eth_staking_eth_history_rewardshistory = sapiGetEthStakingEthHistoryRewardsHistory = Entry('eth-staking/eth/history/rewardsHistory', 'sapi', 'GET', {'cost': 15}) + sapi_get_eth_staking_eth_quota = sapiGetEthStakingEthQuota = Entry('eth-staking/eth/quota', 'sapi', 'GET', {'cost': 15}) + sapi_get_eth_staking_eth_history_ratehistory = sapiGetEthStakingEthHistoryRateHistory = Entry('eth-staking/eth/history/rateHistory', 'sapi', 'GET', {'cost': 15}) + sapi_get_eth_staking_account = sapiGetEthStakingAccount = Entry('eth-staking/account', 'sapi', 'GET', {'cost': 15}) + sapi_get_eth_staking_wbeth_history_wraphistory = sapiGetEthStakingWbethHistoryWrapHistory = Entry('eth-staking/wbeth/history/wrapHistory', 'sapi', 'GET', {'cost': 15}) + sapi_get_eth_staking_wbeth_history_unwraphistory = sapiGetEthStakingWbethHistoryUnwrapHistory = Entry('eth-staking/wbeth/history/unwrapHistory', 'sapi', 'GET', {'cost': 15}) + sapi_get_eth_staking_eth_history_wbethrewardshistory = sapiGetEthStakingEthHistoryWbethRewardsHistory = Entry('eth-staking/eth/history/wbethRewardsHistory', 'sapi', 'GET', {'cost': 15}) + sapi_get_sol_staking_sol_history_stakinghistory = sapiGetSolStakingSolHistoryStakingHistory = Entry('sol-staking/sol/history/stakingHistory', 'sapi', 'GET', {'cost': 15}) + sapi_get_sol_staking_sol_history_redemptionhistory = sapiGetSolStakingSolHistoryRedemptionHistory = Entry('sol-staking/sol/history/redemptionHistory', 'sapi', 'GET', {'cost': 15}) + sapi_get_sol_staking_sol_history_bnsolrewardshistory = sapiGetSolStakingSolHistoryBnsolRewardsHistory = Entry('sol-staking/sol/history/bnsolRewardsHistory', 'sapi', 'GET', {'cost': 15}) + sapi_get_sol_staking_sol_history_ratehistory = sapiGetSolStakingSolHistoryRateHistory = Entry('sol-staking/sol/history/rateHistory', 'sapi', 'GET', {'cost': 15}) + sapi_get_sol_staking_account = sapiGetSolStakingAccount = Entry('sol-staking/account', 'sapi', 'GET', {'cost': 15}) + sapi_get_sol_staking_sol_quota = sapiGetSolStakingSolQuota = Entry('sol-staking/sol/quota', 'sapi', 'GET', {'cost': 15}) + sapi_get_mining_pub_algolist = sapiGetMiningPubAlgoList = Entry('mining/pub/algoList', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_mining_pub_coinlist = sapiGetMiningPubCoinList = Entry('mining/pub/coinList', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_mining_worker_detail = sapiGetMiningWorkerDetail = Entry('mining/worker/detail', 'sapi', 'GET', {'cost': 0.5}) + sapi_get_mining_worker_list = sapiGetMiningWorkerList = Entry('mining/worker/list', 'sapi', 'GET', {'cost': 0.5}) + sapi_get_mining_payment_list = sapiGetMiningPaymentList = Entry('mining/payment/list', 'sapi', 'GET', {'cost': 0.5}) + sapi_get_mining_statistics_user_status = sapiGetMiningStatisticsUserStatus = Entry('mining/statistics/user/status', 'sapi', 'GET', {'cost': 0.5}) + sapi_get_mining_statistics_user_list = sapiGetMiningStatisticsUserList = Entry('mining/statistics/user/list', 'sapi', 'GET', {'cost': 0.5}) + sapi_get_mining_payment_uid = sapiGetMiningPaymentUid = Entry('mining/payment/uid', 'sapi', 'GET', {'cost': 0.5}) + sapi_get_bswap_pools = sapiGetBswapPools = Entry('bswap/pools', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_bswap_liquidity = sapiGetBswapLiquidity = Entry('bswap/liquidity', 'sapi', 'GET', {'cost': 0.1, 'noPoolId': 1}) + sapi_get_bswap_liquidityops = sapiGetBswapLiquidityOps = Entry('bswap/liquidityOps', 'sapi', 'GET', {'cost': 20.001}) + sapi_get_bswap_quote = sapiGetBswapQuote = Entry('bswap/quote', 'sapi', 'GET', {'cost': 1.00005}) + sapi_get_bswap_swap = sapiGetBswapSwap = Entry('bswap/swap', 'sapi', 'GET', {'cost': 20.001}) + sapi_get_bswap_poolconfigure = sapiGetBswapPoolConfigure = Entry('bswap/poolConfigure', 'sapi', 'GET', {'cost': 1.00005}) + sapi_get_bswap_addliquiditypreview = sapiGetBswapAddLiquidityPreview = Entry('bswap/addLiquidityPreview', 'sapi', 'GET', {'cost': 1.00005}) + sapi_get_bswap_removeliquiditypreview = sapiGetBswapRemoveLiquidityPreview = Entry('bswap/removeLiquidityPreview', 'sapi', 'GET', {'cost': 1.00005}) + sapi_get_bswap_unclaimedrewards = sapiGetBswapUnclaimedRewards = Entry('bswap/unclaimedRewards', 'sapi', 'GET', {'cost': 6.667}) + sapi_get_bswap_claimedhistory = sapiGetBswapClaimedHistory = Entry('bswap/claimedHistory', 'sapi', 'GET', {'cost': 6.667}) + sapi_get_blvt_tokeninfo = sapiGetBlvtTokenInfo = Entry('blvt/tokenInfo', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_blvt_subscribe_record = sapiGetBlvtSubscribeRecord = Entry('blvt/subscribe/record', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_blvt_redeem_record = sapiGetBlvtRedeemRecord = Entry('blvt/redeem/record', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_blvt_userlimit = sapiGetBlvtUserLimit = Entry('blvt/userLimit', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_apireferral_ifnewuser = sapiGetApiReferralIfNewUser = Entry('apiReferral/ifNewUser', 'sapi', 'GET', {'cost': 1}) + sapi_get_apireferral_customization = sapiGetApiReferralCustomization = Entry('apiReferral/customization', 'sapi', 'GET', {'cost': 1}) + sapi_get_apireferral_usercustomization = sapiGetApiReferralUserCustomization = Entry('apiReferral/userCustomization', 'sapi', 'GET', {'cost': 1}) + sapi_get_apireferral_rebate_recentrecord = sapiGetApiReferralRebateRecentRecord = Entry('apiReferral/rebate/recentRecord', 'sapi', 'GET', {'cost': 1}) + sapi_get_apireferral_rebate_historicalrecord = sapiGetApiReferralRebateHistoricalRecord = Entry('apiReferral/rebate/historicalRecord', 'sapi', 'GET', {'cost': 1}) + sapi_get_apireferral_kickback_recentrecord = sapiGetApiReferralKickbackRecentRecord = Entry('apiReferral/kickback/recentRecord', 'sapi', 'GET', {'cost': 1}) + sapi_get_apireferral_kickback_historicalrecord = sapiGetApiReferralKickbackHistoricalRecord = Entry('apiReferral/kickback/historicalRecord', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_subaccountapi = sapiGetBrokerSubAccountApi = Entry('broker/subAccountApi', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_subaccount = sapiGetBrokerSubAccount = Entry('broker/subAccount', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_subaccountapi_commission_futures = sapiGetBrokerSubAccountApiCommissionFutures = Entry('broker/subAccountApi/commission/futures', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_subaccountapi_commission_coinfutures = sapiGetBrokerSubAccountApiCommissionCoinFutures = Entry('broker/subAccountApi/commission/coinFutures', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_info = sapiGetBrokerInfo = Entry('broker/info', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_transfer = sapiGetBrokerTransfer = Entry('broker/transfer', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_transfer_futures = sapiGetBrokerTransferFutures = Entry('broker/transfer/futures', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_rebate_recentrecord = sapiGetBrokerRebateRecentRecord = Entry('broker/rebate/recentRecord', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_rebate_historicalrecord = sapiGetBrokerRebateHistoricalRecord = Entry('broker/rebate/historicalRecord', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_subaccount_bnbburn_status = sapiGetBrokerSubAccountBnbBurnStatus = Entry('broker/subAccount/bnbBurn/status', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_subaccount_deposithist = sapiGetBrokerSubAccountDepositHist = Entry('broker/subAccount/depositHist', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_subaccount_spotsummary = sapiGetBrokerSubAccountSpotSummary = Entry('broker/subAccount/spotSummary', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_subaccount_marginsummary = sapiGetBrokerSubAccountMarginSummary = Entry('broker/subAccount/marginSummary', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_subaccount_futuressummary = sapiGetBrokerSubAccountFuturesSummary = Entry('broker/subAccount/futuresSummary', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_rebate_futures_recentrecord = sapiGetBrokerRebateFuturesRecentRecord = Entry('broker/rebate/futures/recentRecord', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_subaccountapi_iprestriction = sapiGetBrokerSubAccountApiIpRestriction = Entry('broker/subAccountApi/ipRestriction', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_universaltransfer = sapiGetBrokerUniversalTransfer = Entry('broker/universalTransfer', 'sapi', 'GET', {'cost': 1}) + sapi_get_account_apirestrictions = sapiGetAccountApiRestrictions = Entry('account/apiRestrictions', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_c2c_ordermatch_listuserorderhistory = sapiGetC2cOrderMatchListUserOrderHistory = Entry('c2c/orderMatch/listUserOrderHistory', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_nft_history_transactions = sapiGetNftHistoryTransactions = Entry('nft/history/transactions', 'sapi', 'GET', {'cost': 20.001}) + sapi_get_nft_history_deposit = sapiGetNftHistoryDeposit = Entry('nft/history/deposit', 'sapi', 'GET', {'cost': 20.001}) + sapi_get_nft_history_withdraw = sapiGetNftHistoryWithdraw = Entry('nft/history/withdraw', 'sapi', 'GET', {'cost': 20.001}) + sapi_get_nft_user_getasset = sapiGetNftUserGetAsset = Entry('nft/user/getAsset', 'sapi', 'GET', {'cost': 20.001}) + sapi_get_pay_transactions = sapiGetPayTransactions = Entry('pay/transactions', 'sapi', 'GET', {'cost': 20.001}) + sapi_get_giftcard_verify = sapiGetGiftcardVerify = Entry('giftcard/verify', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_giftcard_cryptography_rsa_public_key = sapiGetGiftcardCryptographyRsaPublicKey = Entry('giftcard/cryptography/rsa-public-key', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_giftcard_buycode_token_limit = sapiGetGiftcardBuyCodeTokenLimit = Entry('giftcard/buyCode/token-limit', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_algo_spot_openorders = sapiGetAlgoSpotOpenOrders = Entry('algo/spot/openOrders', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_algo_spot_historicalorders = sapiGetAlgoSpotHistoricalOrders = Entry('algo/spot/historicalOrders', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_algo_spot_suborders = sapiGetAlgoSpotSubOrders = Entry('algo/spot/subOrders', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_algo_futures_openorders = sapiGetAlgoFuturesOpenOrders = Entry('algo/futures/openOrders', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_algo_futures_historicalorders = sapiGetAlgoFuturesHistoricalOrders = Entry('algo/futures/historicalOrders', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_algo_futures_suborders = sapiGetAlgoFuturesSubOrders = Entry('algo/futures/subOrders', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_portfolio_account = sapiGetPortfolioAccount = Entry('portfolio/account', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_portfolio_collateralrate = sapiGetPortfolioCollateralRate = Entry('portfolio/collateralRate', 'sapi', 'GET', {'cost': 5}) + sapi_get_portfolio_pmloan = sapiGetPortfolioPmLoan = Entry('portfolio/pmLoan', 'sapi', 'GET', {'cost': 3.3335}) + sapi_get_portfolio_interest_history = sapiGetPortfolioInterestHistory = Entry('portfolio/interest-history', 'sapi', 'GET', {'cost': 0.6667}) + sapi_get_portfolio_asset_index_price = sapiGetPortfolioAssetIndexPrice = Entry('portfolio/asset-index-price', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_portfolio_repay_futures_switch = sapiGetPortfolioRepayFuturesSwitch = Entry('portfolio/repay-futures-switch', 'sapi', 'GET', {'cost': 3}) + sapi_get_portfolio_margin_asset_leverage = sapiGetPortfolioMarginAssetLeverage = Entry('portfolio/margin-asset-leverage', 'sapi', 'GET', {'cost': 5}) + sapi_get_portfolio_balance = sapiGetPortfolioBalance = Entry('portfolio/balance', 'sapi', 'GET', {'cost': 2}) + sapi_get_portfolio_negative_balance_exchange_record = sapiGetPortfolioNegativeBalanceExchangeRecord = Entry('portfolio/negative-balance-exchange-record', 'sapi', 'GET', {'cost': 2}) + sapi_get_portfolio_pmloan_history = sapiGetPortfolioPmloanHistory = Entry('portfolio/pmloan-history', 'sapi', 'GET', {'cost': 5}) + sapi_get_portfolio_earn_asset_balance = sapiGetPortfolioEarnAssetBalance = Entry('portfolio/earn-asset-balance', 'sapi', 'GET', {'cost': 150}) + sapi_get_staking_productlist = sapiGetStakingProductList = Entry('staking/productList', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_staking_position = sapiGetStakingPosition = Entry('staking/position', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_staking_stakingrecord = sapiGetStakingStakingRecord = Entry('staking/stakingRecord', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_staking_personalleftquota = sapiGetStakingPersonalLeftQuota = Entry('staking/personalLeftQuota', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_auto_invest_target_asset_list = sapiGetLendingAutoInvestTargetAssetList = Entry('lending/auto-invest/target-asset/list', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_auto_invest_target_asset_roi_list = sapiGetLendingAutoInvestTargetAssetRoiList = Entry('lending/auto-invest/target-asset/roi/list', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_auto_invest_all_asset = sapiGetLendingAutoInvestAllAsset = Entry('lending/auto-invest/all/asset', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_auto_invest_source_asset_list = sapiGetLendingAutoInvestSourceAssetList = Entry('lending/auto-invest/source-asset/list', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_auto_invest_plan_list = sapiGetLendingAutoInvestPlanList = Entry('lending/auto-invest/plan/list', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_auto_invest_plan_id = sapiGetLendingAutoInvestPlanId = Entry('lending/auto-invest/plan/id', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_auto_invest_history_list = sapiGetLendingAutoInvestHistoryList = Entry('lending/auto-invest/history/list', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_auto_invest_index_info = sapiGetLendingAutoInvestIndexInfo = Entry('lending/auto-invest/index/info', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_auto_invest_index_user_summary = sapiGetLendingAutoInvestIndexUserSummary = Entry('lending/auto-invest/index/user-summary', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_auto_invest_one_off_status = sapiGetLendingAutoInvestOneOffStatus = Entry('lending/auto-invest/one-off/status', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_auto_invest_redeem_history = sapiGetLendingAutoInvestRedeemHistory = Entry('lending/auto-invest/redeem/history', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_auto_invest_rebalance_history = sapiGetLendingAutoInvestRebalanceHistory = Entry('lending/auto-invest/rebalance/history', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_simple_earn_flexible_list = sapiGetSimpleEarnFlexibleList = Entry('simple-earn/flexible/list', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_locked_list = sapiGetSimpleEarnLockedList = Entry('simple-earn/locked/list', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_flexible_personalleftquota = sapiGetSimpleEarnFlexiblePersonalLeftQuota = Entry('simple-earn/flexible/personalLeftQuota', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_locked_personalleftquota = sapiGetSimpleEarnLockedPersonalLeftQuota = Entry('simple-earn/locked/personalLeftQuota', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_flexible_subscriptionpreview = sapiGetSimpleEarnFlexibleSubscriptionPreview = Entry('simple-earn/flexible/subscriptionPreview', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_locked_subscriptionpreview = sapiGetSimpleEarnLockedSubscriptionPreview = Entry('simple-earn/locked/subscriptionPreview', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_flexible_history_ratehistory = sapiGetSimpleEarnFlexibleHistoryRateHistory = Entry('simple-earn/flexible/history/rateHistory', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_flexible_position = sapiGetSimpleEarnFlexiblePosition = Entry('simple-earn/flexible/position', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_locked_position = sapiGetSimpleEarnLockedPosition = Entry('simple-earn/locked/position', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_account = sapiGetSimpleEarnAccount = Entry('simple-earn/account', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_flexible_history_subscriptionrecord = sapiGetSimpleEarnFlexibleHistorySubscriptionRecord = Entry('simple-earn/flexible/history/subscriptionRecord', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_locked_history_subscriptionrecord = sapiGetSimpleEarnLockedHistorySubscriptionRecord = Entry('simple-earn/locked/history/subscriptionRecord', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_flexible_history_redemptionrecord = sapiGetSimpleEarnFlexibleHistoryRedemptionRecord = Entry('simple-earn/flexible/history/redemptionRecord', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_locked_history_redemptionrecord = sapiGetSimpleEarnLockedHistoryRedemptionRecord = Entry('simple-earn/locked/history/redemptionRecord', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_flexible_history_rewardsrecord = sapiGetSimpleEarnFlexibleHistoryRewardsRecord = Entry('simple-earn/flexible/history/rewardsRecord', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_locked_history_rewardsrecord = sapiGetSimpleEarnLockedHistoryRewardsRecord = Entry('simple-earn/locked/history/rewardsRecord', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_flexible_history_collateralrecord = sapiGetSimpleEarnFlexibleHistoryCollateralRecord = Entry('simple-earn/flexible/history/collateralRecord', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_dci_product_list = sapiGetDciProductList = Entry('dci/product/list', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_dci_product_positions = sapiGetDciProductPositions = Entry('dci/product/positions', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_dci_product_accounts = sapiGetDciProductAccounts = Entry('dci/product/accounts', 'sapi', 'GET', {'cost': 0.1}) + sapi_post_asset_dust = sapiPostAssetDust = Entry('asset/dust', 'sapi', 'POST', {'cost': 0.06667}) + sapi_post_asset_dust_btc = sapiPostAssetDustBtc = Entry('asset/dust-btc', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_asset_transfer = sapiPostAssetTransfer = Entry('asset/transfer', 'sapi', 'POST', {'cost': 6.0003}) + sapi_post_asset_get_funding_asset = sapiPostAssetGetFundingAsset = Entry('asset/get-funding-asset', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_asset_convert_transfer = sapiPostAssetConvertTransfer = Entry('asset/convert-transfer', 'sapi', 'POST', {'cost': 0.033335}) + sapi_post_account_disablefastwithdrawswitch = sapiPostAccountDisableFastWithdrawSwitch = Entry('account/disableFastWithdrawSwitch', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_account_enablefastwithdrawswitch = sapiPostAccountEnableFastWithdrawSwitch = Entry('account/enableFastWithdrawSwitch', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_capital_withdraw_apply = sapiPostCapitalWithdrawApply = Entry('capital/withdraw/apply', 'sapi', 'POST', {'cost': 4.0002}) + sapi_post_capital_contract_convertible_coins = sapiPostCapitalContractConvertibleCoins = Entry('capital/contract/convertible-coins', 'sapi', 'POST', {'cost': 4.0002}) + sapi_post_capital_deposit_credit_apply = sapiPostCapitalDepositCreditApply = Entry('capital/deposit/credit-apply', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_margin_borrow_repay = sapiPostMarginBorrowRepay = Entry('margin/borrow-repay', 'sapi', 'POST', {'cost': 20.001}) + sapi_post_margin_transfer = sapiPostMarginTransfer = Entry('margin/transfer', 'sapi', 'POST', {'cost': 4.0002}) + sapi_post_margin_loan = sapiPostMarginLoan = Entry('margin/loan', 'sapi', 'POST', {'cost': 20.001}) + sapi_post_margin_repay = sapiPostMarginRepay = Entry('margin/repay', 'sapi', 'POST', {'cost': 20.001}) + sapi_post_margin_order = sapiPostMarginOrder = Entry('margin/order', 'sapi', 'POST', {'cost': 0.040002}) + sapi_post_margin_order_oco = sapiPostMarginOrderOco = Entry('margin/order/oco', 'sapi', 'POST', {'cost': 0.040002}) + sapi_post_margin_dust = sapiPostMarginDust = Entry('margin/dust', 'sapi', 'POST', {'cost': 20.001}) + sapi_post_margin_exchange_small_liability = sapiPostMarginExchangeSmallLiability = Entry('margin/exchange-small-liability', 'sapi', 'POST', {'cost': 20.001}) + sapi_post_margin_isolated_transfer = sapiPostMarginIsolatedTransfer = Entry('margin/isolated/transfer', 'sapi', 'POST', {'cost': 4.0002}) + sapi_post_margin_isolated_account = sapiPostMarginIsolatedAccount = Entry('margin/isolated/account', 'sapi', 'POST', {'cost': 2.0001}) + sapi_post_margin_max_leverage = sapiPostMarginMaxLeverage = Entry('margin/max-leverage', 'sapi', 'POST', {'cost': 300}) + sapi_post_bnbburn = sapiPostBnbBurn = Entry('bnbBurn', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_sub_account_virtualsubaccount = sapiPostSubAccountVirtualSubAccount = Entry('sub-account/virtualSubAccount', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_sub_account_margin_transfer = sapiPostSubAccountMarginTransfer = Entry('sub-account/margin/transfer', 'sapi', 'POST', {'cost': 4.0002}) + sapi_post_sub_account_margin_enable = sapiPostSubAccountMarginEnable = Entry('sub-account/margin/enable', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_sub_account_futures_enable = sapiPostSubAccountFuturesEnable = Entry('sub-account/futures/enable', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_sub_account_futures_transfer = sapiPostSubAccountFuturesTransfer = Entry('sub-account/futures/transfer', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_sub_account_futures_internaltransfer = sapiPostSubAccountFuturesInternalTransfer = Entry('sub-account/futures/internalTransfer', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_sub_account_transfer_subtosub = sapiPostSubAccountTransferSubToSub = Entry('sub-account/transfer/subToSub', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_sub_account_transfer_subtomaster = sapiPostSubAccountTransferSubToMaster = Entry('sub-account/transfer/subToMaster', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_sub_account_universaltransfer = sapiPostSubAccountUniversalTransfer = Entry('sub-account/universalTransfer', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_sub_account_options_enable = sapiPostSubAccountOptionsEnable = Entry('sub-account/options/enable', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_managed_subaccount_deposit = sapiPostManagedSubaccountDeposit = Entry('managed-subaccount/deposit', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_managed_subaccount_withdraw = sapiPostManagedSubaccountWithdraw = Entry('managed-subaccount/withdraw', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_userdatastream = sapiPostUserDataStream = Entry('userDataStream', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_userdatastream_isolated = sapiPostUserDataStreamIsolated = Entry('userDataStream/isolated', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_futures_transfer = sapiPostFuturesTransfer = Entry('futures/transfer', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_lending_customizedfixed_purchase = sapiPostLendingCustomizedFixedPurchase = Entry('lending/customizedFixed/purchase', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_lending_daily_purchase = sapiPostLendingDailyPurchase = Entry('lending/daily/purchase', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_lending_daily_redeem = sapiPostLendingDailyRedeem = Entry('lending/daily/redeem', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_bswap_liquidityadd = sapiPostBswapLiquidityAdd = Entry('bswap/liquidityAdd', 'sapi', 'POST', {'cost': 60}) + sapi_post_bswap_liquidityremove = sapiPostBswapLiquidityRemove = Entry('bswap/liquidityRemove', 'sapi', 'POST', {'cost': 60}) + sapi_post_bswap_swap = sapiPostBswapSwap = Entry('bswap/swap', 'sapi', 'POST', {'cost': 60}) + sapi_post_bswap_claimrewards = sapiPostBswapClaimRewards = Entry('bswap/claimRewards', 'sapi', 'POST', {'cost': 6.667}) + sapi_post_blvt_subscribe = sapiPostBlvtSubscribe = Entry('blvt/subscribe', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_blvt_redeem = sapiPostBlvtRedeem = Entry('blvt/redeem', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_apireferral_customization = sapiPostApiReferralCustomization = Entry('apiReferral/customization', 'sapi', 'POST', {'cost': 1}) + sapi_post_apireferral_usercustomization = sapiPostApiReferralUserCustomization = Entry('apiReferral/userCustomization', 'sapi', 'POST', {'cost': 1}) + sapi_post_apireferral_rebate_historicalrecord = sapiPostApiReferralRebateHistoricalRecord = Entry('apiReferral/rebate/historicalRecord', 'sapi', 'POST', {'cost': 1}) + sapi_post_apireferral_kickback_historicalrecord = sapiPostApiReferralKickbackHistoricalRecord = Entry('apiReferral/kickback/historicalRecord', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_subaccount = sapiPostBrokerSubAccount = Entry('broker/subAccount', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_subaccount_margin = sapiPostBrokerSubAccountMargin = Entry('broker/subAccount/margin', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_subaccount_futures = sapiPostBrokerSubAccountFutures = Entry('broker/subAccount/futures', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_subaccountapi = sapiPostBrokerSubAccountApi = Entry('broker/subAccountApi', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_subaccountapi_permission = sapiPostBrokerSubAccountApiPermission = Entry('broker/subAccountApi/permission', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_subaccountapi_commission = sapiPostBrokerSubAccountApiCommission = Entry('broker/subAccountApi/commission', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_subaccountapi_commission_futures = sapiPostBrokerSubAccountApiCommissionFutures = Entry('broker/subAccountApi/commission/futures', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_subaccountapi_commission_coinfutures = sapiPostBrokerSubAccountApiCommissionCoinFutures = Entry('broker/subAccountApi/commission/coinFutures', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_transfer = sapiPostBrokerTransfer = Entry('broker/transfer', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_transfer_futures = sapiPostBrokerTransferFutures = Entry('broker/transfer/futures', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_rebate_historicalrecord = sapiPostBrokerRebateHistoricalRecord = Entry('broker/rebate/historicalRecord', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_subaccount_bnbburn_spot = sapiPostBrokerSubAccountBnbBurnSpot = Entry('broker/subAccount/bnbBurn/spot', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_subaccount_bnbburn_margininterest = sapiPostBrokerSubAccountBnbBurnMarginInterest = Entry('broker/subAccount/bnbBurn/marginInterest', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_subaccount_blvt = sapiPostBrokerSubAccountBlvt = Entry('broker/subAccount/blvt', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_subaccountapi_iprestriction = sapiPostBrokerSubAccountApiIpRestriction = Entry('broker/subAccountApi/ipRestriction', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_subaccountapi_iprestriction_iplist = sapiPostBrokerSubAccountApiIpRestrictionIpList = Entry('broker/subAccountApi/ipRestriction/ipList', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_universaltransfer = sapiPostBrokerUniversalTransfer = Entry('broker/universalTransfer', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_subaccountapi_permission_universaltransfer = sapiPostBrokerSubAccountApiPermissionUniversalTransfer = Entry('broker/subAccountApi/permission/universalTransfer', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_subaccountapi_permission_vanillaoptions = sapiPostBrokerSubAccountApiPermissionVanillaOptions = Entry('broker/subAccountApi/permission/vanillaOptions', 'sapi', 'POST', {'cost': 1}) + sapi_post_giftcard_createcode = sapiPostGiftcardCreateCode = Entry('giftcard/createCode', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_giftcard_redeemcode = sapiPostGiftcardRedeemCode = Entry('giftcard/redeemCode', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_giftcard_buycode = sapiPostGiftcardBuyCode = Entry('giftcard/buyCode', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_algo_spot_newordertwap = sapiPostAlgoSpotNewOrderTwap = Entry('algo/spot/newOrderTwap', 'sapi', 'POST', {'cost': 20.001}) + sapi_post_algo_futures_newordervp = sapiPostAlgoFuturesNewOrderVp = Entry('algo/futures/newOrderVp', 'sapi', 'POST', {'cost': 20.001}) + sapi_post_algo_futures_newordertwap = sapiPostAlgoFuturesNewOrderTwap = Entry('algo/futures/newOrderTwap', 'sapi', 'POST', {'cost': 20.001}) + sapi_post_staking_purchase = sapiPostStakingPurchase = Entry('staking/purchase', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_staking_redeem = sapiPostStakingRedeem = Entry('staking/redeem', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_staking_setautostaking = sapiPostStakingSetAutoStaking = Entry('staking/setAutoStaking', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_eth_staking_eth_stake = sapiPostEthStakingEthStake = Entry('eth-staking/eth/stake', 'sapi', 'POST', {'cost': 15}) + sapi_post_eth_staking_eth_redeem = sapiPostEthStakingEthRedeem = Entry('eth-staking/eth/redeem', 'sapi', 'POST', {'cost': 15}) + sapi_post_eth_staking_wbeth_wrap = sapiPostEthStakingWbethWrap = Entry('eth-staking/wbeth/wrap', 'sapi', 'POST', {'cost': 15}) + sapi_post_sol_staking_sol_stake = sapiPostSolStakingSolStake = Entry('sol-staking/sol/stake', 'sapi', 'POST', {'cost': 15}) + sapi_post_sol_staking_sol_redeem = sapiPostSolStakingSolRedeem = Entry('sol-staking/sol/redeem', 'sapi', 'POST', {'cost': 15}) + sapi_post_mining_hash_transfer_config = sapiPostMiningHashTransferConfig = Entry('mining/hash-transfer/config', 'sapi', 'POST', {'cost': 0.5}) + sapi_post_mining_hash_transfer_config_cancel = sapiPostMiningHashTransferConfigCancel = Entry('mining/hash-transfer/config/cancel', 'sapi', 'POST', {'cost': 0.5}) + sapi_post_portfolio_repay = sapiPostPortfolioRepay = Entry('portfolio/repay', 'sapi', 'POST', {'cost': 20.001}) + sapi_post_loan_vip_renew = sapiPostLoanVipRenew = Entry('loan/vip/renew', 'sapi', 'POST', {'cost': 40.002}) + sapi_post_loan_vip_borrow = sapiPostLoanVipBorrow = Entry('loan/vip/borrow', 'sapi', 'POST', {'cost': 40.002}) + sapi_post_loan_borrow = sapiPostLoanBorrow = Entry('loan/borrow', 'sapi', 'POST', {'cost': 40.002}) + sapi_post_loan_repay = sapiPostLoanRepay = Entry('loan/repay', 'sapi', 'POST', {'cost': 40.002}) + sapi_post_loan_adjust_ltv = sapiPostLoanAdjustLtv = Entry('loan/adjust/ltv', 'sapi', 'POST', {'cost': 40.002}) + sapi_post_loan_customize_margin_call = sapiPostLoanCustomizeMarginCall = Entry('loan/customize/margin_call', 'sapi', 'POST', {'cost': 40.002}) + sapi_post_loan_flexible_repay = sapiPostLoanFlexibleRepay = Entry('loan/flexible/repay', 'sapi', 'POST', {'cost': 40.002}) + sapi_post_loan_flexible_adjust_ltv = sapiPostLoanFlexibleAdjustLtv = Entry('loan/flexible/adjust/ltv', 'sapi', 'POST', {'cost': 40.002}) + sapi_post_loan_vip_repay = sapiPostLoanVipRepay = Entry('loan/vip/repay', 'sapi', 'POST', {'cost': 40.002}) + sapi_post_convert_getquote = sapiPostConvertGetQuote = Entry('convert/getQuote', 'sapi', 'POST', {'cost': 1.3334}) + sapi_post_convert_acceptquote = sapiPostConvertAcceptQuote = Entry('convert/acceptQuote', 'sapi', 'POST', {'cost': 3.3335}) + sapi_post_convert_limit_placeorder = sapiPostConvertLimitPlaceOrder = Entry('convert/limit/placeOrder', 'sapi', 'POST', {'cost': 3.3335}) + sapi_post_convert_limit_cancelorder = sapiPostConvertLimitCancelOrder = Entry('convert/limit/cancelOrder', 'sapi', 'POST', {'cost': 1.3334}) + sapi_post_portfolio_auto_collection = sapiPostPortfolioAutoCollection = Entry('portfolio/auto-collection', 'sapi', 'POST', {'cost': 150}) + sapi_post_portfolio_asset_collection = sapiPostPortfolioAssetCollection = Entry('portfolio/asset-collection', 'sapi', 'POST', {'cost': 6}) + sapi_post_portfolio_bnb_transfer = sapiPostPortfolioBnbTransfer = Entry('portfolio/bnb-transfer', 'sapi', 'POST', {'cost': 150}) + sapi_post_portfolio_repay_futures_switch = sapiPostPortfolioRepayFuturesSwitch = Entry('portfolio/repay-futures-switch', 'sapi', 'POST', {'cost': 150}) + sapi_post_portfolio_repay_futures_negative_balance = sapiPostPortfolioRepayFuturesNegativeBalance = Entry('portfolio/repay-futures-negative-balance', 'sapi', 'POST', {'cost': 150}) + sapi_post_portfolio_mint = sapiPostPortfolioMint = Entry('portfolio/mint', 'sapi', 'POST', {'cost': 20}) + sapi_post_portfolio_redeem = sapiPostPortfolioRedeem = Entry('portfolio/redeem', 'sapi', 'POST', {'cost': 20}) + sapi_post_portfolio_earn_asset_transfer = sapiPostPortfolioEarnAssetTransfer = Entry('portfolio/earn-asset-transfer', 'sapi', 'POST', {'cost': 150}) + sapi_post_lending_auto_invest_plan_add = sapiPostLendingAutoInvestPlanAdd = Entry('lending/auto-invest/plan/add', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_lending_auto_invest_plan_edit = sapiPostLendingAutoInvestPlanEdit = Entry('lending/auto-invest/plan/edit', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_lending_auto_invest_plan_edit_status = sapiPostLendingAutoInvestPlanEditStatus = Entry('lending/auto-invest/plan/edit-status', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_lending_auto_invest_one_off = sapiPostLendingAutoInvestOneOff = Entry('lending/auto-invest/one-off', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_lending_auto_invest_redeem = sapiPostLendingAutoInvestRedeem = Entry('lending/auto-invest/redeem', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_simple_earn_flexible_subscribe = sapiPostSimpleEarnFlexibleSubscribe = Entry('simple-earn/flexible/subscribe', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_simple_earn_locked_subscribe = sapiPostSimpleEarnLockedSubscribe = Entry('simple-earn/locked/subscribe', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_simple_earn_flexible_redeem = sapiPostSimpleEarnFlexibleRedeem = Entry('simple-earn/flexible/redeem', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_simple_earn_locked_redeem = sapiPostSimpleEarnLockedRedeem = Entry('simple-earn/locked/redeem', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_simple_earn_flexible_setautosubscribe = sapiPostSimpleEarnFlexibleSetAutoSubscribe = Entry('simple-earn/flexible/setAutoSubscribe', 'sapi', 'POST', {'cost': 15}) + sapi_post_simple_earn_locked_setautosubscribe = sapiPostSimpleEarnLockedSetAutoSubscribe = Entry('simple-earn/locked/setAutoSubscribe', 'sapi', 'POST', {'cost': 15}) + sapi_post_simple_earn_locked_setredeemoption = sapiPostSimpleEarnLockedSetRedeemOption = Entry('simple-earn/locked/setRedeemOption', 'sapi', 'POST', {'cost': 5}) + sapi_post_dci_product_subscribe = sapiPostDciProductSubscribe = Entry('dci/product/subscribe', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_dci_product_auto_compound_edit = sapiPostDciProductAutoCompoundEdit = Entry('dci/product/auto_compound/edit', 'sapi', 'POST', {'cost': 0.1}) + sapi_put_userdatastream = sapiPutUserDataStream = Entry('userDataStream', 'sapi', 'PUT', {'cost': 0.1}) + sapi_put_userdatastream_isolated = sapiPutUserDataStreamIsolated = Entry('userDataStream/isolated', 'sapi', 'PUT', {'cost': 0.1}) + sapi_delete_margin_openorders = sapiDeleteMarginOpenOrders = Entry('margin/openOrders', 'sapi', 'DELETE', {'cost': 0.1}) + sapi_delete_margin_order = sapiDeleteMarginOrder = Entry('margin/order', 'sapi', 'DELETE', {'cost': 0.006667}) + sapi_delete_margin_orderlist = sapiDeleteMarginOrderList = Entry('margin/orderList', 'sapi', 'DELETE', {'cost': 0.006667}) + sapi_delete_margin_isolated_account = sapiDeleteMarginIsolatedAccount = Entry('margin/isolated/account', 'sapi', 'DELETE', {'cost': 2.0001}) + sapi_delete_userdatastream = sapiDeleteUserDataStream = Entry('userDataStream', 'sapi', 'DELETE', {'cost': 0.1}) + sapi_delete_userdatastream_isolated = sapiDeleteUserDataStreamIsolated = Entry('userDataStream/isolated', 'sapi', 'DELETE', {'cost': 0.1}) + sapi_delete_broker_subaccountapi = sapiDeleteBrokerSubAccountApi = Entry('broker/subAccountApi', 'sapi', 'DELETE', {'cost': 1}) + sapi_delete_broker_subaccountapi_iprestriction_iplist = sapiDeleteBrokerSubAccountApiIpRestrictionIpList = Entry('broker/subAccountApi/ipRestriction/ipList', 'sapi', 'DELETE', {'cost': 1}) + sapi_delete_algo_spot_order = sapiDeleteAlgoSpotOrder = Entry('algo/spot/order', 'sapi', 'DELETE', {'cost': 0.1}) + sapi_delete_algo_futures_order = sapiDeleteAlgoFuturesOrder = Entry('algo/futures/order', 'sapi', 'DELETE', {'cost': 0.1}) + sapi_delete_sub_account_subaccountapi_iprestriction_iplist = sapiDeleteSubAccountSubAccountApiIpRestrictionIpList = Entry('sub-account/subAccountApi/ipRestriction/ipList', 'sapi', 'DELETE', {'cost': 20.001}) + sapiv2_get_eth_staking_account = sapiV2GetEthStakingAccount = Entry('eth-staking/account', 'sapiV2', 'GET', {'cost': 15}) + sapiv2_get_sub_account_futures_account = sapiV2GetSubAccountFuturesAccount = Entry('sub-account/futures/account', 'sapiV2', 'GET', {'cost': 0.1}) + sapiv2_get_sub_account_futures_accountsummary = sapiV2GetSubAccountFuturesAccountSummary = Entry('sub-account/futures/accountSummary', 'sapiV2', 'GET', {'cost': 1}) + sapiv2_get_sub_account_futures_positionrisk = sapiV2GetSubAccountFuturesPositionRisk = Entry('sub-account/futures/positionRisk', 'sapiV2', 'GET', {'cost': 0.1}) + sapiv2_get_loan_flexible_ongoing_orders = sapiV2GetLoanFlexibleOngoingOrders = Entry('loan/flexible/ongoing/orders', 'sapiV2', 'GET', {'cost': 30}) + sapiv2_get_loan_flexible_borrow_history = sapiV2GetLoanFlexibleBorrowHistory = Entry('loan/flexible/borrow/history', 'sapiV2', 'GET', {'cost': 40}) + sapiv2_get_loan_flexible_repay_history = sapiV2GetLoanFlexibleRepayHistory = Entry('loan/flexible/repay/history', 'sapiV2', 'GET', {'cost': 40}) + sapiv2_get_loan_flexible_ltv_adjustment_history = sapiV2GetLoanFlexibleLtvAdjustmentHistory = Entry('loan/flexible/ltv/adjustment/history', 'sapiV2', 'GET', {'cost': 40}) + sapiv2_get_loan_flexible_loanable_data = sapiV2GetLoanFlexibleLoanableData = Entry('loan/flexible/loanable/data', 'sapiV2', 'GET', {'cost': 40}) + sapiv2_get_loan_flexible_collateral_data = sapiV2GetLoanFlexibleCollateralData = Entry('loan/flexible/collateral/data', 'sapiV2', 'GET', {'cost': 40}) + sapiv2_get_portfolio_account = sapiV2GetPortfolioAccount = Entry('portfolio/account', 'sapiV2', 'GET', {'cost': 2}) + sapiv2_post_eth_staking_eth_stake = sapiV2PostEthStakingEthStake = Entry('eth-staking/eth/stake', 'sapiV2', 'POST', {'cost': 15}) + sapiv2_post_sub_account_subaccountapi_iprestriction = sapiV2PostSubAccountSubAccountApiIpRestriction = Entry('sub-account/subAccountApi/ipRestriction', 'sapiV2', 'POST', {'cost': 20.001}) + sapiv2_post_loan_flexible_borrow = sapiV2PostLoanFlexibleBorrow = Entry('loan/flexible/borrow', 'sapiV2', 'POST', {'cost': 40.002}) + sapiv2_post_loan_flexible_repay = sapiV2PostLoanFlexibleRepay = Entry('loan/flexible/repay', 'sapiV2', 'POST', {'cost': 40.002}) + sapiv2_post_loan_flexible_adjust_ltv = sapiV2PostLoanFlexibleAdjustLtv = Entry('loan/flexible/adjust/ltv', 'sapiV2', 'POST', {'cost': 40.002}) + sapiv3_get_sub_account_assets = sapiV3GetSubAccountAssets = Entry('sub-account/assets', 'sapiV3', 'GET', {'cost': 0.40002}) + sapiv3_post_asset_getuserasset = sapiV3PostAssetGetUserAsset = Entry('asset/getUserAsset', 'sapiV3', 'POST', {'cost': 0.5}) + sapiv4_get_sub_account_assets = sapiV4GetSubAccountAssets = Entry('sub-account/assets', 'sapiV4', 'GET', {'cost': 0.40002}) + dapipublic_get_ping = dapiPublicGetPing = Entry('ping', 'dapiPublic', 'GET', {'cost': 1}) + dapipublic_get_time = dapiPublicGetTime = Entry('time', 'dapiPublic', 'GET', {'cost': 1}) + dapipublic_get_exchangeinfo = dapiPublicGetExchangeInfo = Entry('exchangeInfo', 'dapiPublic', 'GET', {'cost': 1}) + dapipublic_get_depth = dapiPublicGetDepth = Entry('depth', 'dapiPublic', 'GET', {'cost': 2, 'byLimit': [[50, 2], [100, 5], [500, 10], [1000, 20]]}) + dapipublic_get_trades = dapiPublicGetTrades = Entry('trades', 'dapiPublic', 'GET', {'cost': 5}) + dapipublic_get_historicaltrades = dapiPublicGetHistoricalTrades = Entry('historicalTrades', 'dapiPublic', 'GET', {'cost': 20}) + dapipublic_get_aggtrades = dapiPublicGetAggTrades = Entry('aggTrades', 'dapiPublic', 'GET', {'cost': 20}) + dapipublic_get_premiumindex = dapiPublicGetPremiumIndex = Entry('premiumIndex', 'dapiPublic', 'GET', {'cost': 10}) + dapipublic_get_fundingrate = dapiPublicGetFundingRate = Entry('fundingRate', 'dapiPublic', 'GET', {'cost': 1}) + dapipublic_get_klines = dapiPublicGetKlines = Entry('klines', 'dapiPublic', 'GET', {'cost': 1, 'byLimit': [[99, 1], [499, 2], [1000, 5], [10000, 10]]}) + dapipublic_get_continuousklines = dapiPublicGetContinuousKlines = Entry('continuousKlines', 'dapiPublic', 'GET', {'cost': 1, 'byLimit': [[99, 1], [499, 2], [1000, 5], [10000, 10]]}) + dapipublic_get_indexpriceklines = dapiPublicGetIndexPriceKlines = Entry('indexPriceKlines', 'dapiPublic', 'GET', {'cost': 1, 'byLimit': [[99, 1], [499, 2], [1000, 5], [10000, 10]]}) + dapipublic_get_markpriceklines = dapiPublicGetMarkPriceKlines = Entry('markPriceKlines', 'dapiPublic', 'GET', {'cost': 1, 'byLimit': [[99, 1], [499, 2], [1000, 5], [10000, 10]]}) + dapipublic_get_premiumindexklines = dapiPublicGetPremiumIndexKlines = Entry('premiumIndexKlines', 'dapiPublic', 'GET', {'cost': 1, 'byLimit': [[99, 1], [499, 2], [1000, 5], [10000, 10]]}) + dapipublic_get_ticker_24hr = dapiPublicGetTicker24hr = Entry('ticker/24hr', 'dapiPublic', 'GET', {'cost': 1, 'noSymbol': 40}) + dapipublic_get_ticker_price = dapiPublicGetTickerPrice = Entry('ticker/price', 'dapiPublic', 'GET', {'cost': 1, 'noSymbol': 2}) + dapipublic_get_ticker_bookticker = dapiPublicGetTickerBookTicker = Entry('ticker/bookTicker', 'dapiPublic', 'GET', {'cost': 2, 'noSymbol': 5}) + dapipublic_get_constituents = dapiPublicGetConstituents = Entry('constituents', 'dapiPublic', 'GET', {'cost': 2}) + dapipublic_get_openinterest = dapiPublicGetOpenInterest = Entry('openInterest', 'dapiPublic', 'GET', {'cost': 1}) + dapipublic_get_fundinginfo = dapiPublicGetFundingInfo = Entry('fundingInfo', 'dapiPublic', 'GET', {'cost': 1}) + dapidata_get_delivery_price = dapiDataGetDeliveryPrice = Entry('delivery-price', 'dapiData', 'GET', {'cost': 1}) + dapidata_get_openinteresthist = dapiDataGetOpenInterestHist = Entry('openInterestHist', 'dapiData', 'GET', {'cost': 1}) + dapidata_get_toplongshortaccountratio = dapiDataGetTopLongShortAccountRatio = Entry('topLongShortAccountRatio', 'dapiData', 'GET', {'cost': 1}) + dapidata_get_toplongshortpositionratio = dapiDataGetTopLongShortPositionRatio = Entry('topLongShortPositionRatio', 'dapiData', 'GET', {'cost': 1}) + dapidata_get_globallongshortaccountratio = dapiDataGetGlobalLongShortAccountRatio = Entry('globalLongShortAccountRatio', 'dapiData', 'GET', {'cost': 1}) + dapidata_get_takerbuysellvol = dapiDataGetTakerBuySellVol = Entry('takerBuySellVol', 'dapiData', 'GET', {'cost': 1}) + dapidata_get_basis = dapiDataGetBasis = Entry('basis', 'dapiData', 'GET', {'cost': 1}) + dapiprivate_get_positionside_dual = dapiPrivateGetPositionSideDual = Entry('positionSide/dual', 'dapiPrivate', 'GET', {'cost': 30}) + dapiprivate_get_orderamendment = dapiPrivateGetOrderAmendment = Entry('orderAmendment', 'dapiPrivate', 'GET', {'cost': 1}) + dapiprivate_get_order = dapiPrivateGetOrder = Entry('order', 'dapiPrivate', 'GET', {'cost': 1}) + dapiprivate_get_openorder = dapiPrivateGetOpenOrder = Entry('openOrder', 'dapiPrivate', 'GET', {'cost': 1}) + dapiprivate_get_openorders = dapiPrivateGetOpenOrders = Entry('openOrders', 'dapiPrivate', 'GET', {'cost': 1, 'noSymbol': 5}) + dapiprivate_get_allorders = dapiPrivateGetAllOrders = Entry('allOrders', 'dapiPrivate', 'GET', {'cost': 20, 'noSymbol': 40}) + dapiprivate_get_balance = dapiPrivateGetBalance = Entry('balance', 'dapiPrivate', 'GET', {'cost': 1}) + dapiprivate_get_account = dapiPrivateGetAccount = Entry('account', 'dapiPrivate', 'GET', {'cost': 5}) + dapiprivate_get_positionmargin_history = dapiPrivateGetPositionMarginHistory = Entry('positionMargin/history', 'dapiPrivate', 'GET', {'cost': 1}) + dapiprivate_get_positionrisk = dapiPrivateGetPositionRisk = Entry('positionRisk', 'dapiPrivate', 'GET', {'cost': 1}) + dapiprivate_get_usertrades = dapiPrivateGetUserTrades = Entry('userTrades', 'dapiPrivate', 'GET', {'cost': 20, 'noSymbol': 40}) + dapiprivate_get_income = dapiPrivateGetIncome = Entry('income', 'dapiPrivate', 'GET', {'cost': 20}) + dapiprivate_get_leveragebracket = dapiPrivateGetLeverageBracket = Entry('leverageBracket', 'dapiPrivate', 'GET', {'cost': 1}) + dapiprivate_get_forceorders = dapiPrivateGetForceOrders = Entry('forceOrders', 'dapiPrivate', 'GET', {'cost': 20, 'noSymbol': 50}) + dapiprivate_get_adlquantile = dapiPrivateGetAdlQuantile = Entry('adlQuantile', 'dapiPrivate', 'GET', {'cost': 5}) + dapiprivate_get_commissionrate = dapiPrivateGetCommissionRate = Entry('commissionRate', 'dapiPrivate', 'GET', {'cost': 20}) + dapiprivate_get_income_asyn = dapiPrivateGetIncomeAsyn = Entry('income/asyn', 'dapiPrivate', 'GET', {'cost': 5}) + dapiprivate_get_income_asyn_id = dapiPrivateGetIncomeAsynId = Entry('income/asyn/id', 'dapiPrivate', 'GET', {'cost': 5}) + dapiprivate_get_trade_asyn = dapiPrivateGetTradeAsyn = Entry('trade/asyn', 'dapiPrivate', 'GET', {'cost': 0.5}) + dapiprivate_get_trade_asyn_id = dapiPrivateGetTradeAsynId = Entry('trade/asyn/id', 'dapiPrivate', 'GET', {'cost': 0.5}) + dapiprivate_get_order_asyn = dapiPrivateGetOrderAsyn = Entry('order/asyn', 'dapiPrivate', 'GET', {'cost': 0.5}) + dapiprivate_get_order_asyn_id = dapiPrivateGetOrderAsynId = Entry('order/asyn/id', 'dapiPrivate', 'GET', {'cost': 0.5}) + dapiprivate_get_pmexchangeinfo = dapiPrivateGetPmExchangeInfo = Entry('pmExchangeInfo', 'dapiPrivate', 'GET', {'cost': 0.5}) + dapiprivate_get_pmaccountinfo = dapiPrivateGetPmAccountInfo = Entry('pmAccountInfo', 'dapiPrivate', 'GET', {'cost': 0.5}) + dapiprivate_post_positionside_dual = dapiPrivatePostPositionSideDual = Entry('positionSide/dual', 'dapiPrivate', 'POST', {'cost': 1}) + dapiprivate_post_order = dapiPrivatePostOrder = Entry('order', 'dapiPrivate', 'POST', {'cost': 4}) + dapiprivate_post_batchorders = dapiPrivatePostBatchOrders = Entry('batchOrders', 'dapiPrivate', 'POST', {'cost': 5}) + dapiprivate_post_countdowncancelall = dapiPrivatePostCountdownCancelAll = Entry('countdownCancelAll', 'dapiPrivate', 'POST', {'cost': 10}) + dapiprivate_post_leverage = dapiPrivatePostLeverage = Entry('leverage', 'dapiPrivate', 'POST', {'cost': 1}) + dapiprivate_post_margintype = dapiPrivatePostMarginType = Entry('marginType', 'dapiPrivate', 'POST', {'cost': 1}) + dapiprivate_post_positionmargin = dapiPrivatePostPositionMargin = Entry('positionMargin', 'dapiPrivate', 'POST', {'cost': 1}) + dapiprivate_post_listenkey = dapiPrivatePostListenKey = Entry('listenKey', 'dapiPrivate', 'POST', {'cost': 1}) + dapiprivate_put_listenkey = dapiPrivatePutListenKey = Entry('listenKey', 'dapiPrivate', 'PUT', {'cost': 1}) + dapiprivate_put_order = dapiPrivatePutOrder = Entry('order', 'dapiPrivate', 'PUT', {'cost': 1}) + dapiprivate_put_batchorders = dapiPrivatePutBatchOrders = Entry('batchOrders', 'dapiPrivate', 'PUT', {'cost': 5}) + dapiprivate_delete_order = dapiPrivateDeleteOrder = Entry('order', 'dapiPrivate', 'DELETE', {'cost': 1}) + dapiprivate_delete_allopenorders = dapiPrivateDeleteAllOpenOrders = Entry('allOpenOrders', 'dapiPrivate', 'DELETE', {'cost': 1}) + dapiprivate_delete_batchorders = dapiPrivateDeleteBatchOrders = Entry('batchOrders', 'dapiPrivate', 'DELETE', {'cost': 5}) + dapiprivate_delete_listenkey = dapiPrivateDeleteListenKey = Entry('listenKey', 'dapiPrivate', 'DELETE', {'cost': 1}) + dapiprivatev2_get_leveragebracket = dapiPrivateV2GetLeverageBracket = Entry('leverageBracket', 'dapiPrivateV2', 'GET', {'cost': 1}) + fapipublic_get_ping = fapiPublicGetPing = Entry('ping', 'fapiPublic', 'GET', {'cost': 1}) + fapipublic_get_time = fapiPublicGetTime = Entry('time', 'fapiPublic', 'GET', {'cost': 1}) + fapipublic_get_exchangeinfo = fapiPublicGetExchangeInfo = Entry('exchangeInfo', 'fapiPublic', 'GET', {'cost': 1}) + fapipublic_get_depth = fapiPublicGetDepth = Entry('depth', 'fapiPublic', 'GET', {'cost': 2, 'byLimit': [[50, 2], [100, 5], [500, 10], [1000, 20]]}) + fapipublic_get_trades = fapiPublicGetTrades = Entry('trades', 'fapiPublic', 'GET', {'cost': 5}) + fapipublic_get_historicaltrades = fapiPublicGetHistoricalTrades = Entry('historicalTrades', 'fapiPublic', 'GET', {'cost': 20}) + fapipublic_get_aggtrades = fapiPublicGetAggTrades = Entry('aggTrades', 'fapiPublic', 'GET', {'cost': 20}) + fapipublic_get_klines = fapiPublicGetKlines = Entry('klines', 'fapiPublic', 'GET', {'cost': 1, 'byLimit': [[99, 1], [499, 2], [1000, 5], [10000, 10]]}) + fapipublic_get_continuousklines = fapiPublicGetContinuousKlines = Entry('continuousKlines', 'fapiPublic', 'GET', {'cost': 1, 'byLimit': [[99, 1], [499, 2], [1000, 5], [10000, 10]]}) + fapipublic_get_markpriceklines = fapiPublicGetMarkPriceKlines = Entry('markPriceKlines', 'fapiPublic', 'GET', {'cost': 1, 'byLimit': [[99, 1], [499, 2], [1000, 5], [10000, 10]]}) + fapipublic_get_indexpriceklines = fapiPublicGetIndexPriceKlines = Entry('indexPriceKlines', 'fapiPublic', 'GET', {'cost': 1, 'byLimit': [[99, 1], [499, 2], [1000, 5], [10000, 10]]}) + fapipublic_get_premiumindexklines = fapiPublicGetPremiumIndexKlines = Entry('premiumIndexKlines', 'fapiPublic', 'GET', {'cost': 1, 'byLimit': [[99, 1], [499, 2], [1000, 5], [10000, 10]]}) + fapipublic_get_fundingrate = fapiPublicGetFundingRate = Entry('fundingRate', 'fapiPublic', 'GET', {'cost': 1}) + fapipublic_get_fundinginfo = fapiPublicGetFundingInfo = Entry('fundingInfo', 'fapiPublic', 'GET', {'cost': 1}) + fapipublic_get_premiumindex = fapiPublicGetPremiumIndex = Entry('premiumIndex', 'fapiPublic', 'GET', {'cost': 1}) + fapipublic_get_ticker_24hr = fapiPublicGetTicker24hr = Entry('ticker/24hr', 'fapiPublic', 'GET', {'cost': 1, 'noSymbol': 40}) + fapipublic_get_ticker_price = fapiPublicGetTickerPrice = Entry('ticker/price', 'fapiPublic', 'GET', {'cost': 1, 'noSymbol': 2}) + fapipublic_get_ticker_bookticker = fapiPublicGetTickerBookTicker = Entry('ticker/bookTicker', 'fapiPublic', 'GET', {'cost': 1, 'noSymbol': 2}) + fapipublic_get_openinterest = fapiPublicGetOpenInterest = Entry('openInterest', 'fapiPublic', 'GET', {'cost': 1}) + fapipublic_get_indexinfo = fapiPublicGetIndexInfo = Entry('indexInfo', 'fapiPublic', 'GET', {'cost': 1}) + fapipublic_get_assetindex = fapiPublicGetAssetIndex = Entry('assetIndex', 'fapiPublic', 'GET', {'cost': 1, 'noSymbol': 10}) + fapipublic_get_constituents = fapiPublicGetConstituents = Entry('constituents', 'fapiPublic', 'GET', {'cost': 2}) + fapipublic_get_apitradingstatus = fapiPublicGetApiTradingStatus = Entry('apiTradingStatus', 'fapiPublic', 'GET', {'cost': 1, 'noSymbol': 10}) + fapipublic_get_lvtklines = fapiPublicGetLvtKlines = Entry('lvtKlines', 'fapiPublic', 'GET', {'cost': 1}) + fapipublic_get_convert_exchangeinfo = fapiPublicGetConvertExchangeInfo = Entry('convert/exchangeInfo', 'fapiPublic', 'GET', {'cost': 4}) + fapipublic_get_insurancebalance = fapiPublicGetInsuranceBalance = Entry('insuranceBalance', 'fapiPublic', 'GET', {'cost': 1}) + fapidata_get_delivery_price = fapiDataGetDeliveryPrice = Entry('delivery-price', 'fapiData', 'GET', {'cost': 1}) + fapidata_get_openinteresthist = fapiDataGetOpenInterestHist = Entry('openInterestHist', 'fapiData', 'GET', {'cost': 1}) + fapidata_get_toplongshortaccountratio = fapiDataGetTopLongShortAccountRatio = Entry('topLongShortAccountRatio', 'fapiData', 'GET', {'cost': 1}) + fapidata_get_toplongshortpositionratio = fapiDataGetTopLongShortPositionRatio = Entry('topLongShortPositionRatio', 'fapiData', 'GET', {'cost': 1}) + fapidata_get_globallongshortaccountratio = fapiDataGetGlobalLongShortAccountRatio = Entry('globalLongShortAccountRatio', 'fapiData', 'GET', {'cost': 1}) + fapidata_get_takerlongshortratio = fapiDataGetTakerlongshortRatio = Entry('takerlongshortRatio', 'fapiData', 'GET', {'cost': 1}) + fapidata_get_basis = fapiDataGetBasis = Entry('basis', 'fapiData', 'GET', {'cost': 1}) + fapiprivate_get_forceorders = fapiPrivateGetForceOrders = Entry('forceOrders', 'fapiPrivate', 'GET', {'cost': 20, 'noSymbol': 50}) + fapiprivate_get_allorders = fapiPrivateGetAllOrders = Entry('allOrders', 'fapiPrivate', 'GET', {'cost': 5}) + fapiprivate_get_openorder = fapiPrivateGetOpenOrder = Entry('openOrder', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_openorders = fapiPrivateGetOpenOrders = Entry('openOrders', 'fapiPrivate', 'GET', {'cost': 1, 'noSymbol': 40}) + fapiprivate_get_order = fapiPrivateGetOrder = Entry('order', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_account = fapiPrivateGetAccount = Entry('account', 'fapiPrivate', 'GET', {'cost': 5}) + fapiprivate_get_balance = fapiPrivateGetBalance = Entry('balance', 'fapiPrivate', 'GET', {'cost': 5}) + fapiprivate_get_leveragebracket = fapiPrivateGetLeverageBracket = Entry('leverageBracket', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_positionmargin_history = fapiPrivateGetPositionMarginHistory = Entry('positionMargin/history', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_positionrisk = fapiPrivateGetPositionRisk = Entry('positionRisk', 'fapiPrivate', 'GET', {'cost': 5}) + fapiprivate_get_positionside_dual = fapiPrivateGetPositionSideDual = Entry('positionSide/dual', 'fapiPrivate', 'GET', {'cost': 30}) + fapiprivate_get_usertrades = fapiPrivateGetUserTrades = Entry('userTrades', 'fapiPrivate', 'GET', {'cost': 5}) + fapiprivate_get_income = fapiPrivateGetIncome = Entry('income', 'fapiPrivate', 'GET', {'cost': 30}) + fapiprivate_get_commissionrate = fapiPrivateGetCommissionRate = Entry('commissionRate', 'fapiPrivate', 'GET', {'cost': 20}) + fapiprivate_get_ratelimit_order = fapiPrivateGetRateLimitOrder = Entry('rateLimit/order', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_apitradingstatus = fapiPrivateGetApiTradingStatus = Entry('apiTradingStatus', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_multiassetsmargin = fapiPrivateGetMultiAssetsMargin = Entry('multiAssetsMargin', 'fapiPrivate', 'GET', {'cost': 30}) + fapiprivate_get_apireferral_ifnewuser = fapiPrivateGetApiReferralIfNewUser = Entry('apiReferral/ifNewUser', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_apireferral_customization = fapiPrivateGetApiReferralCustomization = Entry('apiReferral/customization', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_apireferral_usercustomization = fapiPrivateGetApiReferralUserCustomization = Entry('apiReferral/userCustomization', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_apireferral_tradernum = fapiPrivateGetApiReferralTraderNum = Entry('apiReferral/traderNum', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_apireferral_overview = fapiPrivateGetApiReferralOverview = Entry('apiReferral/overview', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_apireferral_tradevol = fapiPrivateGetApiReferralTradeVol = Entry('apiReferral/tradeVol', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_apireferral_rebatevol = fapiPrivateGetApiReferralRebateVol = Entry('apiReferral/rebateVol', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_apireferral_tradersummary = fapiPrivateGetApiReferralTraderSummary = Entry('apiReferral/traderSummary', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_adlquantile = fapiPrivateGetAdlQuantile = Entry('adlQuantile', 'fapiPrivate', 'GET', {'cost': 5}) + fapiprivate_get_pmaccountinfo = fapiPrivateGetPmAccountInfo = Entry('pmAccountInfo', 'fapiPrivate', 'GET', {'cost': 5}) + fapiprivate_get_orderamendment = fapiPrivateGetOrderAmendment = Entry('orderAmendment', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_income_asyn = fapiPrivateGetIncomeAsyn = Entry('income/asyn', 'fapiPrivate', 'GET', {'cost': 1000}) + fapiprivate_get_income_asyn_id = fapiPrivateGetIncomeAsynId = Entry('income/asyn/id', 'fapiPrivate', 'GET', {'cost': 10}) + fapiprivate_get_order_asyn = fapiPrivateGetOrderAsyn = Entry('order/asyn', 'fapiPrivate', 'GET', {'cost': 1000}) + fapiprivate_get_order_asyn_id = fapiPrivateGetOrderAsynId = Entry('order/asyn/id', 'fapiPrivate', 'GET', {'cost': 10}) + fapiprivate_get_trade_asyn = fapiPrivateGetTradeAsyn = Entry('trade/asyn', 'fapiPrivate', 'GET', {'cost': 1000}) + fapiprivate_get_trade_asyn_id = fapiPrivateGetTradeAsynId = Entry('trade/asyn/id', 'fapiPrivate', 'GET', {'cost': 10}) + fapiprivate_get_feeburn = fapiPrivateGetFeeBurn = Entry('feeBurn', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_symbolconfig = fapiPrivateGetSymbolConfig = Entry('symbolConfig', 'fapiPrivate', 'GET', {'cost': 5}) + fapiprivate_get_accountconfig = fapiPrivateGetAccountConfig = Entry('accountConfig', 'fapiPrivate', 'GET', {'cost': 5}) + fapiprivate_get_convert_orderstatus = fapiPrivateGetConvertOrderStatus = Entry('convert/orderStatus', 'fapiPrivate', 'GET', {'cost': 5}) + fapiprivate_post_batchorders = fapiPrivatePostBatchOrders = Entry('batchOrders', 'fapiPrivate', 'POST', {'cost': 5}) + fapiprivate_post_positionside_dual = fapiPrivatePostPositionSideDual = Entry('positionSide/dual', 'fapiPrivate', 'POST', {'cost': 1}) + fapiprivate_post_positionmargin = fapiPrivatePostPositionMargin = Entry('positionMargin', 'fapiPrivate', 'POST', {'cost': 1}) + fapiprivate_post_margintype = fapiPrivatePostMarginType = Entry('marginType', 'fapiPrivate', 'POST', {'cost': 1}) + fapiprivate_post_order = fapiPrivatePostOrder = Entry('order', 'fapiPrivate', 'POST', {'cost': 4}) + fapiprivate_post_leverage = fapiPrivatePostLeverage = Entry('leverage', 'fapiPrivate', 'POST', {'cost': 1}) + fapiprivate_post_listenkey = fapiPrivatePostListenKey = Entry('listenKey', 'fapiPrivate', 'POST', {'cost': 1}) + fapiprivate_post_countdowncancelall = fapiPrivatePostCountdownCancelAll = Entry('countdownCancelAll', 'fapiPrivate', 'POST', {'cost': 10}) + fapiprivate_post_multiassetsmargin = fapiPrivatePostMultiAssetsMargin = Entry('multiAssetsMargin', 'fapiPrivate', 'POST', {'cost': 1}) + fapiprivate_post_apireferral_customization = fapiPrivatePostApiReferralCustomization = Entry('apiReferral/customization', 'fapiPrivate', 'POST', {'cost': 1}) + fapiprivate_post_apireferral_usercustomization = fapiPrivatePostApiReferralUserCustomization = Entry('apiReferral/userCustomization', 'fapiPrivate', 'POST', {'cost': 1}) + fapiprivate_post_feeburn = fapiPrivatePostFeeBurn = Entry('feeBurn', 'fapiPrivate', 'POST', {'cost': 1}) + fapiprivate_post_convert_getquote = fapiPrivatePostConvertGetQuote = Entry('convert/getQuote', 'fapiPrivate', 'POST', {'cost': 200}) + fapiprivate_post_convert_acceptquote = fapiPrivatePostConvertAcceptQuote = Entry('convert/acceptQuote', 'fapiPrivate', 'POST', {'cost': 20}) + fapiprivate_put_listenkey = fapiPrivatePutListenKey = Entry('listenKey', 'fapiPrivate', 'PUT', {'cost': 1}) + fapiprivate_put_order = fapiPrivatePutOrder = Entry('order', 'fapiPrivate', 'PUT', {'cost': 1}) + fapiprivate_put_batchorders = fapiPrivatePutBatchOrders = Entry('batchOrders', 'fapiPrivate', 'PUT', {'cost': 5}) + fapiprivate_delete_batchorders = fapiPrivateDeleteBatchOrders = Entry('batchOrders', 'fapiPrivate', 'DELETE', {'cost': 1}) + fapiprivate_delete_order = fapiPrivateDeleteOrder = Entry('order', 'fapiPrivate', 'DELETE', {'cost': 1}) + fapiprivate_delete_allopenorders = fapiPrivateDeleteAllOpenOrders = Entry('allOpenOrders', 'fapiPrivate', 'DELETE', {'cost': 1}) + fapiprivate_delete_listenkey = fapiPrivateDeleteListenKey = Entry('listenKey', 'fapiPrivate', 'DELETE', {'cost': 1}) + fapipublicv2_get_ticker_price = fapiPublicV2GetTickerPrice = Entry('ticker/price', 'fapiPublicV2', 'GET', {'cost': 0}) + fapiprivatev2_get_account = fapiPrivateV2GetAccount = Entry('account', 'fapiPrivateV2', 'GET', {'cost': 1}) + fapiprivatev2_get_balance = fapiPrivateV2GetBalance = Entry('balance', 'fapiPrivateV2', 'GET', {'cost': 1}) + fapiprivatev2_get_positionrisk = fapiPrivateV2GetPositionRisk = Entry('positionRisk', 'fapiPrivateV2', 'GET', {'cost': 1}) + fapiprivatev3_get_account = fapiPrivateV3GetAccount = Entry('account', 'fapiPrivateV3', 'GET', {'cost': 1}) + fapiprivatev3_get_balance = fapiPrivateV3GetBalance = Entry('balance', 'fapiPrivateV3', 'GET', {'cost': 1}) + fapiprivatev3_get_positionrisk = fapiPrivateV3GetPositionRisk = Entry('positionRisk', 'fapiPrivateV3', 'GET', {'cost': 1}) + eapipublic_get_ping = eapiPublicGetPing = Entry('ping', 'eapiPublic', 'GET', {'cost': 1}) + eapipublic_get_time = eapiPublicGetTime = Entry('time', 'eapiPublic', 'GET', {'cost': 1}) + eapipublic_get_exchangeinfo = eapiPublicGetExchangeInfo = Entry('exchangeInfo', 'eapiPublic', 'GET', {'cost': 1}) + eapipublic_get_index = eapiPublicGetIndex = Entry('index', 'eapiPublic', 'GET', {'cost': 1}) + eapipublic_get_ticker = eapiPublicGetTicker = Entry('ticker', 'eapiPublic', 'GET', {'cost': 5}) + eapipublic_get_mark = eapiPublicGetMark = Entry('mark', 'eapiPublic', 'GET', {'cost': 5}) + eapipublic_get_depth = eapiPublicGetDepth = Entry('depth', 'eapiPublic', 'GET', {'cost': 1}) + eapipublic_get_klines = eapiPublicGetKlines = Entry('klines', 'eapiPublic', 'GET', {'cost': 1}) + eapipublic_get_trades = eapiPublicGetTrades = Entry('trades', 'eapiPublic', 'GET', {'cost': 5}) + eapipublic_get_historicaltrades = eapiPublicGetHistoricalTrades = Entry('historicalTrades', 'eapiPublic', 'GET', {'cost': 20}) + eapipublic_get_exercisehistory = eapiPublicGetExerciseHistory = Entry('exerciseHistory', 'eapiPublic', 'GET', {'cost': 3}) + eapipublic_get_openinterest = eapiPublicGetOpenInterest = Entry('openInterest', 'eapiPublic', 'GET', {'cost': 3}) + eapiprivate_get_account = eapiPrivateGetAccount = Entry('account', 'eapiPrivate', 'GET', {'cost': 3}) + eapiprivate_get_position = eapiPrivateGetPosition = Entry('position', 'eapiPrivate', 'GET', {'cost': 5}) + eapiprivate_get_openorders = eapiPrivateGetOpenOrders = Entry('openOrders', 'eapiPrivate', 'GET', {'cost': 1, 'noSymbol': 40}) + eapiprivate_get_historyorders = eapiPrivateGetHistoryOrders = Entry('historyOrders', 'eapiPrivate', 'GET', {'cost': 3}) + eapiprivate_get_usertrades = eapiPrivateGetUserTrades = Entry('userTrades', 'eapiPrivate', 'GET', {'cost': 5}) + eapiprivate_get_exerciserecord = eapiPrivateGetExerciseRecord = Entry('exerciseRecord', 'eapiPrivate', 'GET', {'cost': 5}) + eapiprivate_get_bill = eapiPrivateGetBill = Entry('bill', 'eapiPrivate', 'GET', {'cost': 1}) + eapiprivate_get_income_asyn = eapiPrivateGetIncomeAsyn = Entry('income/asyn', 'eapiPrivate', 'GET', {'cost': 5}) + eapiprivate_get_income_asyn_id = eapiPrivateGetIncomeAsynId = Entry('income/asyn/id', 'eapiPrivate', 'GET', {'cost': 5}) + eapiprivate_get_marginaccount = eapiPrivateGetMarginAccount = Entry('marginAccount', 'eapiPrivate', 'GET', {'cost': 3}) + eapiprivate_get_mmp = eapiPrivateGetMmp = Entry('mmp', 'eapiPrivate', 'GET', {'cost': 1}) + eapiprivate_get_countdowncancelall = eapiPrivateGetCountdownCancelAll = Entry('countdownCancelAll', 'eapiPrivate', 'GET', {'cost': 1}) + eapiprivate_get_order = eapiPrivateGetOrder = Entry('order', 'eapiPrivate', 'GET', {'cost': 1}) + eapiprivate_get_block_order_orders = eapiPrivateGetBlockOrderOrders = Entry('block/order/orders', 'eapiPrivate', 'GET', {'cost': 5}) + eapiprivate_get_block_order_execute = eapiPrivateGetBlockOrderExecute = Entry('block/order/execute', 'eapiPrivate', 'GET', {'cost': 5}) + eapiprivate_get_block_user_trades = eapiPrivateGetBlockUserTrades = Entry('block/user-trades', 'eapiPrivate', 'GET', {'cost': 5}) + eapiprivate_get_blocktrades = eapiPrivateGetBlockTrades = Entry('blockTrades', 'eapiPrivate', 'GET', {'cost': 5}) + eapiprivate_post_order = eapiPrivatePostOrder = Entry('order', 'eapiPrivate', 'POST', {'cost': 1}) + eapiprivate_post_batchorders = eapiPrivatePostBatchOrders = Entry('batchOrders', 'eapiPrivate', 'POST', {'cost': 5}) + eapiprivate_post_listenkey = eapiPrivatePostListenKey = Entry('listenKey', 'eapiPrivate', 'POST', {'cost': 1}) + eapiprivate_post_mmpset = eapiPrivatePostMmpSet = Entry('mmpSet', 'eapiPrivate', 'POST', {'cost': 1}) + eapiprivate_post_mmpreset = eapiPrivatePostMmpReset = Entry('mmpReset', 'eapiPrivate', 'POST', {'cost': 1}) + eapiprivate_post_countdowncancelall = eapiPrivatePostCountdownCancelAll = Entry('countdownCancelAll', 'eapiPrivate', 'POST', {'cost': 1}) + eapiprivate_post_countdowncancelallheartbeat = eapiPrivatePostCountdownCancelAllHeartBeat = Entry('countdownCancelAllHeartBeat', 'eapiPrivate', 'POST', {'cost': 10}) + eapiprivate_post_block_order_create = eapiPrivatePostBlockOrderCreate = Entry('block/order/create', 'eapiPrivate', 'POST', {'cost': 5}) + eapiprivate_post_block_order_execute = eapiPrivatePostBlockOrderExecute = Entry('block/order/execute', 'eapiPrivate', 'POST', {'cost': 5}) + eapiprivate_put_listenkey = eapiPrivatePutListenKey = Entry('listenKey', 'eapiPrivate', 'PUT', {'cost': 1}) + eapiprivate_put_block_order_create = eapiPrivatePutBlockOrderCreate = Entry('block/order/create', 'eapiPrivate', 'PUT', {'cost': 5}) + eapiprivate_delete_order = eapiPrivateDeleteOrder = Entry('order', 'eapiPrivate', 'DELETE', {'cost': 1}) + eapiprivate_delete_batchorders = eapiPrivateDeleteBatchOrders = Entry('batchOrders', 'eapiPrivate', 'DELETE', {'cost': 1}) + eapiprivate_delete_allopenorders = eapiPrivateDeleteAllOpenOrders = Entry('allOpenOrders', 'eapiPrivate', 'DELETE', {'cost': 1}) + eapiprivate_delete_allopenordersbyunderlying = eapiPrivateDeleteAllOpenOrdersByUnderlying = Entry('allOpenOrdersByUnderlying', 'eapiPrivate', 'DELETE', {'cost': 1}) + eapiprivate_delete_listenkey = eapiPrivateDeleteListenKey = Entry('listenKey', 'eapiPrivate', 'DELETE', {'cost': 1}) + eapiprivate_delete_block_order_create = eapiPrivateDeleteBlockOrderCreate = Entry('block/order/create', 'eapiPrivate', 'DELETE', {'cost': 5}) + public_get_ping = publicGetPing = Entry('ping', 'public', 'GET', {'cost': 0.2}) + public_get_time = publicGetTime = Entry('time', 'public', 'GET', {'cost': 0.2}) + public_get_depth = publicGetDepth = Entry('depth', 'public', 'GET', {'cost': 1, 'byLimit': [[100, 1], [500, 5], [1000, 10], [5000, 50]]}) + public_get_trades = publicGetTrades = Entry('trades', 'public', 'GET', {'cost': 2}) + public_get_aggtrades = publicGetAggTrades = Entry('aggTrades', 'public', 'GET', {'cost': 0.4}) + public_get_historicaltrades = publicGetHistoricalTrades = Entry('historicalTrades', 'public', 'GET', {'cost': 2}) + public_get_klines = publicGetKlines = Entry('klines', 'public', 'GET', {'cost': 0.4}) + public_get_uiklines = publicGetUiKlines = Entry('uiKlines', 'public', 'GET', {'cost': 0.4}) + public_get_ticker_24hr = publicGetTicker24hr = Entry('ticker/24hr', 'public', 'GET', {'cost': 0.4, 'noSymbol': 16}) + public_get_ticker = publicGetTicker = Entry('ticker', 'public', 'GET', {'cost': 0.4, 'noSymbol': 16}) + public_get_ticker_tradingday = publicGetTickerTradingDay = Entry('ticker/tradingDay', 'public', 'GET', {'cost': 0.8}) + public_get_ticker_price = publicGetTickerPrice = Entry('ticker/price', 'public', 'GET', {'cost': 0.4, 'noSymbol': 0.8}) + public_get_ticker_bookticker = publicGetTickerBookTicker = Entry('ticker/bookTicker', 'public', 'GET', {'cost': 0.4, 'noSymbol': 0.8}) + public_get_exchangeinfo = publicGetExchangeInfo = Entry('exchangeInfo', 'public', 'GET', {'cost': 4}) + public_get_avgprice = publicGetAvgPrice = Entry('avgPrice', 'public', 'GET', {'cost': 0.4}) + public_put_userdatastream = publicPutUserDataStream = Entry('userDataStream', 'public', 'PUT', {'cost': 0.4}) + public_post_userdatastream = publicPostUserDataStream = Entry('userDataStream', 'public', 'POST', {'cost': 0.4}) + public_delete_userdatastream = publicDeleteUserDataStream = Entry('userDataStream', 'public', 'DELETE', {'cost': 0.4}) + private_get_allorderlist = privateGetAllOrderList = Entry('allOrderList', 'private', 'GET', {'cost': 4}) + private_get_openorderlist = privateGetOpenOrderList = Entry('openOrderList', 'private', 'GET', {'cost': 1.2}) + private_get_orderlist = privateGetOrderList = Entry('orderList', 'private', 'GET', {'cost': 0.8}) + private_get_order = privateGetOrder = Entry('order', 'private', 'GET', {'cost': 0.8}) + private_get_openorders = privateGetOpenOrders = Entry('openOrders', 'private', 'GET', {'cost': 1.2, 'noSymbol': 16}) + private_get_allorders = privateGetAllOrders = Entry('allOrders', 'private', 'GET', {'cost': 4}) + private_get_account = privateGetAccount = Entry('account', 'private', 'GET', {'cost': 4}) + private_get_mytrades = privateGetMyTrades = Entry('myTrades', 'private', 'GET', {'cost': 4}) + private_get_ratelimit_order = privateGetRateLimitOrder = Entry('rateLimit/order', 'private', 'GET', {'cost': 8}) + private_get_mypreventedmatches = privateGetMyPreventedMatches = Entry('myPreventedMatches', 'private', 'GET', {'cost': 4}) + private_get_myallocations = privateGetMyAllocations = Entry('myAllocations', 'private', 'GET', {'cost': 4}) + private_get_account_commission = privateGetAccountCommission = Entry('account/commission', 'private', 'GET', {'cost': 4}) + private_post_order_oco = privatePostOrderOco = Entry('order/oco', 'private', 'POST', {'cost': 0.2}) + private_post_orderlist_oco = privatePostOrderListOco = Entry('orderList/oco', 'private', 'POST', {'cost': 0.2}) + private_post_orderlist_oto = privatePostOrderListOto = Entry('orderList/oto', 'private', 'POST', {'cost': 0.2}) + private_post_orderlist_otoco = privatePostOrderListOtoco = Entry('orderList/otoco', 'private', 'POST', {'cost': 0.2}) + private_post_sor_order = privatePostSorOrder = Entry('sor/order', 'private', 'POST', {'cost': 0.2}) + private_post_sor_order_test = privatePostSorOrderTest = Entry('sor/order/test', 'private', 'POST', {'cost': 0.2}) + private_post_order = privatePostOrder = Entry('order', 'private', 'POST', {'cost': 0.2}) + private_post_order_cancelreplace = privatePostOrderCancelReplace = Entry('order/cancelReplace', 'private', 'POST', {'cost': 0.2}) + private_post_order_test = privatePostOrderTest = Entry('order/test', 'private', 'POST', {'cost': 0.2}) + private_delete_openorders = privateDeleteOpenOrders = Entry('openOrders', 'private', 'DELETE', {'cost': 0.2}) + private_delete_orderlist = privateDeleteOrderList = Entry('orderList', 'private', 'DELETE', {'cost': 0.2}) + private_delete_order = privateDeleteOrder = Entry('order', 'private', 'DELETE', {'cost': 0.2}) + papi_get_ping = papiGetPing = Entry('ping', 'papi', 'GET', {'cost': 0.2}) + papi_get_um_order = papiGetUmOrder = Entry('um/order', 'papi', 'GET', {'cost': 1}) + papi_get_um_openorder = papiGetUmOpenOrder = Entry('um/openOrder', 'papi', 'GET', {'cost': 1}) + papi_get_um_openorders = papiGetUmOpenOrders = Entry('um/openOrders', 'papi', 'GET', {'cost': 1, 'noSymbol': 40}) + papi_get_um_allorders = papiGetUmAllOrders = Entry('um/allOrders', 'papi', 'GET', {'cost': 5}) + papi_get_cm_order = papiGetCmOrder = Entry('cm/order', 'papi', 'GET', {'cost': 1}) + papi_get_cm_openorder = papiGetCmOpenOrder = Entry('cm/openOrder', 'papi', 'GET', {'cost': 1}) + papi_get_cm_openorders = papiGetCmOpenOrders = Entry('cm/openOrders', 'papi', 'GET', {'cost': 1, 'noSymbol': 40}) + papi_get_cm_allorders = papiGetCmAllOrders = Entry('cm/allOrders', 'papi', 'GET', {'cost': 20}) + papi_get_um_conditional_openorder = papiGetUmConditionalOpenOrder = Entry('um/conditional/openOrder', 'papi', 'GET', {'cost': 1}) + papi_get_um_conditional_openorders = papiGetUmConditionalOpenOrders = Entry('um/conditional/openOrders', 'papi', 'GET', {'cost': 1, 'noSymbol': 40}) + papi_get_um_conditional_orderhistory = papiGetUmConditionalOrderHistory = Entry('um/conditional/orderHistory', 'papi', 'GET', {'cost': 1}) + papi_get_um_conditional_allorders = papiGetUmConditionalAllOrders = Entry('um/conditional/allOrders', 'papi', 'GET', {'cost': 1, 'noSymbol': 40}) + papi_get_cm_conditional_openorder = papiGetCmConditionalOpenOrder = Entry('cm/conditional/openOrder', 'papi', 'GET', {'cost': 1}) + papi_get_cm_conditional_openorders = papiGetCmConditionalOpenOrders = Entry('cm/conditional/openOrders', 'papi', 'GET', {'cost': 1, 'noSymbol': 40}) + papi_get_cm_conditional_orderhistory = papiGetCmConditionalOrderHistory = Entry('cm/conditional/orderHistory', 'papi', 'GET', {'cost': 1}) + papi_get_cm_conditional_allorders = papiGetCmConditionalAllOrders = Entry('cm/conditional/allOrders', 'papi', 'GET', {'cost': 40}) + papi_get_margin_order = papiGetMarginOrder = Entry('margin/order', 'papi', 'GET', {'cost': 10}) + papi_get_margin_openorders = papiGetMarginOpenOrders = Entry('margin/openOrders', 'papi', 'GET', {'cost': 5}) + papi_get_margin_allorders = papiGetMarginAllOrders = Entry('margin/allOrders', 'papi', 'GET', {'cost': 100}) + papi_get_margin_orderlist = papiGetMarginOrderList = Entry('margin/orderList', 'papi', 'GET', {'cost': 5}) + papi_get_margin_allorderlist = papiGetMarginAllOrderList = Entry('margin/allOrderList', 'papi', 'GET', {'cost': 100}) + papi_get_margin_openorderlist = papiGetMarginOpenOrderList = Entry('margin/openOrderList', 'papi', 'GET', {'cost': 5}) + papi_get_margin_mytrades = papiGetMarginMyTrades = Entry('margin/myTrades', 'papi', 'GET', {'cost': 5}) + papi_get_balance = papiGetBalance = Entry('balance', 'papi', 'GET', {'cost': 4}) + papi_get_account = papiGetAccount = Entry('account', 'papi', 'GET', {'cost': 4}) + papi_get_margin_maxborrowable = papiGetMarginMaxBorrowable = Entry('margin/maxBorrowable', 'papi', 'GET', {'cost': 1}) + papi_get_margin_maxwithdraw = papiGetMarginMaxWithdraw = Entry('margin/maxWithdraw', 'papi', 'GET', {'cost': 1}) + papi_get_um_positionrisk = papiGetUmPositionRisk = Entry('um/positionRisk', 'papi', 'GET', {'cost': 1}) + papi_get_cm_positionrisk = papiGetCmPositionRisk = Entry('cm/positionRisk', 'papi', 'GET', {'cost': 0.2}) + papi_get_um_positionside_dual = papiGetUmPositionSideDual = Entry('um/positionSide/dual', 'papi', 'GET', {'cost': 6}) + papi_get_cm_positionside_dual = papiGetCmPositionSideDual = Entry('cm/positionSide/dual', 'papi', 'GET', {'cost': 6}) + papi_get_um_usertrades = papiGetUmUserTrades = Entry('um/userTrades', 'papi', 'GET', {'cost': 5}) + papi_get_cm_usertrades = papiGetCmUserTrades = Entry('cm/userTrades', 'papi', 'GET', {'cost': 20}) + papi_get_um_leveragebracket = papiGetUmLeverageBracket = Entry('um/leverageBracket', 'papi', 'GET', {'cost': 0.2}) + papi_get_cm_leveragebracket = papiGetCmLeverageBracket = Entry('cm/leverageBracket', 'papi', 'GET', {'cost': 0.2}) + papi_get_margin_forceorders = papiGetMarginForceOrders = Entry('margin/forceOrders', 'papi', 'GET', {'cost': 1}) + papi_get_um_forceorders = papiGetUmForceOrders = Entry('um/forceOrders', 'papi', 'GET', {'cost': 20, 'noSymbol': 50}) + papi_get_cm_forceorders = papiGetCmForceOrders = Entry('cm/forceOrders', 'papi', 'GET', {'cost': 20, 'noSymbol': 50}) + papi_get_um_apitradingstatus = papiGetUmApiTradingStatus = Entry('um/apiTradingStatus', 'papi', 'GET', {'cost': 0.2, 'noSymbol': 2}) + papi_get_um_commissionrate = papiGetUmCommissionRate = Entry('um/commissionRate', 'papi', 'GET', {'cost': 4}) + papi_get_cm_commissionrate = papiGetCmCommissionRate = Entry('cm/commissionRate', 'papi', 'GET', {'cost': 4}) + papi_get_margin_marginloan = papiGetMarginMarginLoan = Entry('margin/marginLoan', 'papi', 'GET', {'cost': 2}) + papi_get_margin_repayloan = papiGetMarginRepayLoan = Entry('margin/repayLoan', 'papi', 'GET', {'cost': 2}) + papi_get_margin_margininteresthistory = papiGetMarginMarginInterestHistory = Entry('margin/marginInterestHistory', 'papi', 'GET', {'cost': 0.2}) + papi_get_portfolio_interest_history = papiGetPortfolioInterestHistory = Entry('portfolio/interest-history', 'papi', 'GET', {'cost': 10}) + papi_get_um_income = papiGetUmIncome = Entry('um/income', 'papi', 'GET', {'cost': 6}) + papi_get_cm_income = papiGetCmIncome = Entry('cm/income', 'papi', 'GET', {'cost': 6}) + papi_get_um_account = papiGetUmAccount = Entry('um/account', 'papi', 'GET', {'cost': 1}) + papi_get_cm_account = papiGetCmAccount = Entry('cm/account', 'papi', 'GET', {'cost': 1}) + papi_get_repay_futures_switch = papiGetRepayFuturesSwitch = Entry('repay-futures-switch', 'papi', 'GET', {'cost': 6}) + papi_get_um_adlquantile = papiGetUmAdlQuantile = Entry('um/adlQuantile', 'papi', 'GET', {'cost': 5}) + papi_get_cm_adlquantile = papiGetCmAdlQuantile = Entry('cm/adlQuantile', 'papi', 'GET', {'cost': 5}) + papi_get_um_trade_asyn = papiGetUmTradeAsyn = Entry('um/trade/asyn', 'papi', 'GET', {'cost': 300}) + papi_get_um_trade_asyn_id = papiGetUmTradeAsynId = Entry('um/trade/asyn/id', 'papi', 'GET', {'cost': 2}) + papi_get_um_order_asyn = papiGetUmOrderAsyn = Entry('um/order/asyn', 'papi', 'GET', {'cost': 300}) + papi_get_um_order_asyn_id = papiGetUmOrderAsynId = Entry('um/order/asyn/id', 'papi', 'GET', {'cost': 2}) + papi_get_um_income_asyn = papiGetUmIncomeAsyn = Entry('um/income/asyn', 'papi', 'GET', {'cost': 300}) + papi_get_um_income_asyn_id = papiGetUmIncomeAsynId = Entry('um/income/asyn/id', 'papi', 'GET', {'cost': 2}) + papi_get_um_orderamendment = papiGetUmOrderAmendment = Entry('um/orderAmendment', 'papi', 'GET', {'cost': 1}) + papi_get_cm_orderamendment = papiGetCmOrderAmendment = Entry('cm/orderAmendment', 'papi', 'GET', {'cost': 1}) + papi_get_um_feeburn = papiGetUmFeeBurn = Entry('um/feeBurn', 'papi', 'GET', {'cost': 30}) + papi_get_um_accountconfig = papiGetUmAccountConfig = Entry('um/accountConfig', 'papi', 'GET', {'cost': 1}) + papi_get_um_symbolconfig = papiGetUmSymbolConfig = Entry('um/symbolConfig', 'papi', 'GET', {'cost': 1}) + papi_get_cm_accountconfig = papiGetCmAccountConfig = Entry('cm/accountConfig', 'papi', 'GET', {'cost': 1}) + papi_get_cm_symbolconfig = papiGetCmSymbolConfig = Entry('cm/symbolConfig', 'papi', 'GET', {'cost': 1}) + papi_get_ratelimit_order = papiGetRateLimitOrder = Entry('rateLimit/order', 'papi', 'GET', {'cost': 1}) + papi_post_um_order = papiPostUmOrder = Entry('um/order', 'papi', 'POST', {'cost': 1}) + papi_post_um_conditional_order = papiPostUmConditionalOrder = Entry('um/conditional/order', 'papi', 'POST', {'cost': 1}) + papi_post_cm_order = papiPostCmOrder = Entry('cm/order', 'papi', 'POST', {'cost': 1}) + papi_post_cm_conditional_order = papiPostCmConditionalOrder = Entry('cm/conditional/order', 'papi', 'POST', {'cost': 1}) + papi_post_margin_order = papiPostMarginOrder = Entry('margin/order', 'papi', 'POST', {'cost': 1}) + papi_post_marginloan = papiPostMarginLoan = Entry('marginLoan', 'papi', 'POST', {'cost': 100}) + papi_post_repayloan = papiPostRepayLoan = Entry('repayLoan', 'papi', 'POST', {'cost': 100}) + papi_post_margin_order_oco = papiPostMarginOrderOco = Entry('margin/order/oco', 'papi', 'POST', {'cost': 1}) + papi_post_um_leverage = papiPostUmLeverage = Entry('um/leverage', 'papi', 'POST', {'cost': 0.2}) + papi_post_cm_leverage = papiPostCmLeverage = Entry('cm/leverage', 'papi', 'POST', {'cost': 0.2}) + papi_post_um_positionside_dual = papiPostUmPositionSideDual = Entry('um/positionSide/dual', 'papi', 'POST', {'cost': 0.2}) + papi_post_cm_positionside_dual = papiPostCmPositionSideDual = Entry('cm/positionSide/dual', 'papi', 'POST', {'cost': 0.2}) + papi_post_auto_collection = papiPostAutoCollection = Entry('auto-collection', 'papi', 'POST', {'cost': 150}) + papi_post_bnb_transfer = papiPostBnbTransfer = Entry('bnb-transfer', 'papi', 'POST', {'cost': 150}) + papi_post_repay_futures_switch = papiPostRepayFuturesSwitch = Entry('repay-futures-switch', 'papi', 'POST', {'cost': 150}) + papi_post_repay_futures_negative_balance = papiPostRepayFuturesNegativeBalance = Entry('repay-futures-negative-balance', 'papi', 'POST', {'cost': 150}) + papi_post_listenkey = papiPostListenKey = Entry('listenKey', 'papi', 'POST', {'cost': 0.2}) + papi_post_asset_collection = papiPostAssetCollection = Entry('asset-collection', 'papi', 'POST', {'cost': 6}) + papi_post_margin_repay_debt = papiPostMarginRepayDebt = Entry('margin/repay-debt', 'papi', 'POST', {'cost': 3000}) + papi_post_um_feeburn = papiPostUmFeeBurn = Entry('um/feeBurn', 'papi', 'POST', {'cost': 1}) + papi_put_listenkey = papiPutListenKey = Entry('listenKey', 'papi', 'PUT', {'cost': 0.2}) + papi_put_um_order = papiPutUmOrder = Entry('um/order', 'papi', 'PUT', {'cost': 1}) + papi_put_cm_order = papiPutCmOrder = Entry('cm/order', 'papi', 'PUT', {'cost': 1}) + papi_delete_um_order = papiDeleteUmOrder = Entry('um/order', 'papi', 'DELETE', {'cost': 1}) + papi_delete_um_conditional_order = papiDeleteUmConditionalOrder = Entry('um/conditional/order', 'papi', 'DELETE', {'cost': 1}) + papi_delete_um_allopenorders = papiDeleteUmAllOpenOrders = Entry('um/allOpenOrders', 'papi', 'DELETE', {'cost': 1}) + papi_delete_um_conditional_allopenorders = papiDeleteUmConditionalAllOpenOrders = Entry('um/conditional/allOpenOrders', 'papi', 'DELETE', {'cost': 1}) + papi_delete_cm_order = papiDeleteCmOrder = Entry('cm/order', 'papi', 'DELETE', {'cost': 1}) + papi_delete_cm_conditional_order = papiDeleteCmConditionalOrder = Entry('cm/conditional/order', 'papi', 'DELETE', {'cost': 1}) + papi_delete_cm_allopenorders = papiDeleteCmAllOpenOrders = Entry('cm/allOpenOrders', 'papi', 'DELETE', {'cost': 1}) + papi_delete_cm_conditional_allopenorders = papiDeleteCmConditionalAllOpenOrders = Entry('cm/conditional/allOpenOrders', 'papi', 'DELETE', {'cost': 1}) + papi_delete_margin_order = papiDeleteMarginOrder = Entry('margin/order', 'papi', 'DELETE', {'cost': 2}) + papi_delete_margin_allopenorders = papiDeleteMarginAllOpenOrders = Entry('margin/allOpenOrders', 'papi', 'DELETE', {'cost': 5}) + papi_delete_margin_orderlist = papiDeleteMarginOrderList = Entry('margin/orderList', 'papi', 'DELETE', {'cost': 2}) + papi_delete_listenkey = papiDeleteListenKey = Entry('listenKey', 'papi', 'DELETE', {'cost': 0.2}) + papiv2_get_um_account = papiV2GetUmAccount = Entry('um/account', 'papiV2', 'GET', {'cost': 1}) diff --git a/ccxt/abstract/binanceus.py b/ccxt/abstract/binanceus.py new file mode 100644 index 0000000..bd9eb1f --- /dev/null +++ b/ccxt/abstract/binanceus.py @@ -0,0 +1,823 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + sapi_get_copytrading_futures_userstatus = sapiGetCopyTradingFuturesUserStatus = Entry('copyTrading/futures/userStatus', 'sapi', 'GET', {'cost': 2}) + sapi_get_copytrading_futures_leadsymbol = sapiGetCopyTradingFuturesLeadSymbol = Entry('copyTrading/futures/leadSymbol', 'sapi', 'GET', {'cost': 2}) + sapi_get_system_status = sapiGetSystemStatus = Entry('system/status', 'sapi', 'GET', {'cost': 1}) + sapi_get_accountsnapshot = sapiGetAccountSnapshot = Entry('accountSnapshot', 'sapi', 'GET', {'cost': 240}) + sapi_get_account_info = sapiGetAccountInfo = Entry('account/info', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_margin_asset = sapiGetMarginAsset = Entry('margin/asset', 'sapi', 'GET', {'cost': 1}) + sapi_get_margin_pair = sapiGetMarginPair = Entry('margin/pair', 'sapi', 'GET', {'cost': 1}) + sapi_get_margin_allassets = sapiGetMarginAllAssets = Entry('margin/allAssets', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_margin_allpairs = sapiGetMarginAllPairs = Entry('margin/allPairs', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_margin_priceindex = sapiGetMarginPriceIndex = Entry('margin/priceIndex', 'sapi', 'GET', {'cost': 1}) + sapi_get_spot_delist_schedule = sapiGetSpotDelistSchedule = Entry('spot/delist-schedule', 'sapi', 'GET', {'cost': 10}) + sapi_get_asset_assetdividend = sapiGetAssetAssetDividend = Entry('asset/assetDividend', 'sapi', 'GET', {'cost': 1}) + sapi_get_asset_dribblet = sapiGetAssetDribblet = Entry('asset/dribblet', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_asset_transfer = sapiGetAssetTransfer = Entry('asset/transfer', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_asset_assetdetail = sapiGetAssetAssetDetail = Entry('asset/assetDetail', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_asset_tradefee = sapiGetAssetTradeFee = Entry('asset/tradeFee', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_asset_ledger_transfer_cloud_mining_querybypage = sapiGetAssetLedgerTransferCloudMiningQueryByPage = Entry('asset/ledger-transfer/cloud-mining/queryByPage', 'sapi', 'GET', {'cost': 4.0002}) + sapi_get_asset_convert_transfer_querybypage = sapiGetAssetConvertTransferQueryByPage = Entry('asset/convert-transfer/queryByPage', 'sapi', 'GET', {'cost': 0.033335}) + sapi_get_asset_wallet_balance = sapiGetAssetWalletBalance = Entry('asset/wallet/balance', 'sapi', 'GET', {'cost': 6}) + sapi_get_asset_custody_transfer_history = sapiGetAssetCustodyTransferHistory = Entry('asset/custody/transfer-history', 'sapi', 'GET', {'cost': 6}) + sapi_get_margin_borrow_repay = sapiGetMarginBorrowRepay = Entry('margin/borrow-repay', 'sapi', 'GET', {'cost': 1}) + sapi_get_margin_loan = sapiGetMarginLoan = Entry('margin/loan', 'sapi', 'GET', {'cost': 1}) + sapi_get_margin_repay = sapiGetMarginRepay = Entry('margin/repay', 'sapi', 'GET', {'cost': 1}) + sapi_get_margin_account = sapiGetMarginAccount = Entry('margin/account', 'sapi', 'GET', {'cost': 1}) + sapi_get_margin_transfer = sapiGetMarginTransfer = Entry('margin/transfer', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_margin_interesthistory = sapiGetMarginInterestHistory = Entry('margin/interestHistory', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_margin_forceliquidationrec = sapiGetMarginForceLiquidationRec = Entry('margin/forceLiquidationRec', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_margin_order = sapiGetMarginOrder = Entry('margin/order', 'sapi', 'GET', {'cost': 1}) + sapi_get_margin_openorders = sapiGetMarginOpenOrders = Entry('margin/openOrders', 'sapi', 'GET', {'cost': 1}) + sapi_get_margin_allorders = sapiGetMarginAllOrders = Entry('margin/allOrders', 'sapi', 'GET', {'cost': 20}) + sapi_get_margin_mytrades = sapiGetMarginMyTrades = Entry('margin/myTrades', 'sapi', 'GET', {'cost': 1}) + sapi_get_margin_maxborrowable = sapiGetMarginMaxBorrowable = Entry('margin/maxBorrowable', 'sapi', 'GET', {'cost': 5}) + sapi_get_margin_maxtransferable = sapiGetMarginMaxTransferable = Entry('margin/maxTransferable', 'sapi', 'GET', {'cost': 5}) + sapi_get_margin_tradecoeff = sapiGetMarginTradeCoeff = Entry('margin/tradeCoeff', 'sapi', 'GET', {'cost': 1}) + sapi_get_margin_isolated_transfer = sapiGetMarginIsolatedTransfer = Entry('margin/isolated/transfer', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_margin_isolated_account = sapiGetMarginIsolatedAccount = Entry('margin/isolated/account', 'sapi', 'GET', {'cost': 1}) + sapi_get_margin_isolated_pair = sapiGetMarginIsolatedPair = Entry('margin/isolated/pair', 'sapi', 'GET', {'cost': 1}) + sapi_get_margin_isolated_allpairs = sapiGetMarginIsolatedAllPairs = Entry('margin/isolated/allPairs', 'sapi', 'GET', {'cost': 1}) + sapi_get_margin_isolated_accountlimit = sapiGetMarginIsolatedAccountLimit = Entry('margin/isolated/accountLimit', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_margin_interestratehistory = sapiGetMarginInterestRateHistory = Entry('margin/interestRateHistory', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_margin_orderlist = sapiGetMarginOrderList = Entry('margin/orderList', 'sapi', 'GET', {'cost': 1}) + sapi_get_margin_allorderlist = sapiGetMarginAllOrderList = Entry('margin/allOrderList', 'sapi', 'GET', {'cost': 20}) + sapi_get_margin_openorderlist = sapiGetMarginOpenOrderList = Entry('margin/openOrderList', 'sapi', 'GET', {'cost': 1}) + sapi_get_margin_crossmargindata = sapiGetMarginCrossMarginData = Entry('margin/crossMarginData', 'sapi', 'GET', {'cost': 0.1, 'noCoin': 0.5}) + sapi_get_margin_isolatedmargindata = sapiGetMarginIsolatedMarginData = Entry('margin/isolatedMarginData', 'sapi', 'GET', {'cost': 0.1, 'noCoin': 1}) + sapi_get_margin_isolatedmargintier = sapiGetMarginIsolatedMarginTier = Entry('margin/isolatedMarginTier', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_margin_ratelimit_order = sapiGetMarginRateLimitOrder = Entry('margin/rateLimit/order', 'sapi', 'GET', {'cost': 2}) + sapi_get_margin_dribblet = sapiGetMarginDribblet = Entry('margin/dribblet', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_margin_dust = sapiGetMarginDust = Entry('margin/dust', 'sapi', 'GET', {'cost': 20.001}) + sapi_get_margin_crossmargincollateralratio = sapiGetMarginCrossMarginCollateralRatio = Entry('margin/crossMarginCollateralRatio', 'sapi', 'GET', {'cost': 10}) + sapi_get_margin_exchange_small_liability = sapiGetMarginExchangeSmallLiability = Entry('margin/exchange-small-liability', 'sapi', 'GET', {'cost': 0.6667}) + sapi_get_margin_exchange_small_liability_history = sapiGetMarginExchangeSmallLiabilityHistory = Entry('margin/exchange-small-liability-history', 'sapi', 'GET', {'cost': 0.6667}) + sapi_get_margin_next_hourly_interest_rate = sapiGetMarginNextHourlyInterestRate = Entry('margin/next-hourly-interest-rate', 'sapi', 'GET', {'cost': 0.6667}) + sapi_get_margin_capital_flow = sapiGetMarginCapitalFlow = Entry('margin/capital-flow', 'sapi', 'GET', {'cost': 10}) + sapi_get_margin_delist_schedule = sapiGetMarginDelistSchedule = Entry('margin/delist-schedule', 'sapi', 'GET', {'cost': 10}) + sapi_get_margin_available_inventory = sapiGetMarginAvailableInventory = Entry('margin/available-inventory', 'sapi', 'GET', {'cost': 0.3334}) + sapi_get_margin_leveragebracket = sapiGetMarginLeverageBracket = Entry('margin/leverageBracket', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_loan_vip_loanable_data = sapiGetLoanVipLoanableData = Entry('loan/vip/loanable/data', 'sapi', 'GET', {'cost': 40}) + sapi_get_loan_vip_collateral_data = sapiGetLoanVipCollateralData = Entry('loan/vip/collateral/data', 'sapi', 'GET', {'cost': 40}) + sapi_get_loan_vip_request_data = sapiGetLoanVipRequestData = Entry('loan/vip/request/data', 'sapi', 'GET', {'cost': 2.6668}) + sapi_get_loan_vip_request_interestrate = sapiGetLoanVipRequestInterestRate = Entry('loan/vip/request/interestRate', 'sapi', 'GET', {'cost': 2.6668}) + sapi_get_loan_income = sapiGetLoanIncome = Entry('loan/income', 'sapi', 'GET', {'cost': 40.002}) + sapi_get_loan_ongoing_orders = sapiGetLoanOngoingOrders = Entry('loan/ongoing/orders', 'sapi', 'GET', {'cost': 40}) + sapi_get_loan_ltv_adjustment_history = sapiGetLoanLtvAdjustmentHistory = Entry('loan/ltv/adjustment/history', 'sapi', 'GET', {'cost': 40}) + sapi_get_loan_borrow_history = sapiGetLoanBorrowHistory = Entry('loan/borrow/history', 'sapi', 'GET', {'cost': 40}) + sapi_get_loan_repay_history = sapiGetLoanRepayHistory = Entry('loan/repay/history', 'sapi', 'GET', {'cost': 40}) + sapi_get_loan_loanable_data = sapiGetLoanLoanableData = Entry('loan/loanable/data', 'sapi', 'GET', {'cost': 40}) + sapi_get_loan_collateral_data = sapiGetLoanCollateralData = Entry('loan/collateral/data', 'sapi', 'GET', {'cost': 40}) + sapi_get_loan_repay_collateral_rate = sapiGetLoanRepayCollateralRate = Entry('loan/repay/collateral/rate', 'sapi', 'GET', {'cost': 600}) + sapi_get_loan_flexible_ongoing_orders = sapiGetLoanFlexibleOngoingOrders = Entry('loan/flexible/ongoing/orders', 'sapi', 'GET', {'cost': 30}) + sapi_get_loan_flexible_borrow_history = sapiGetLoanFlexibleBorrowHistory = Entry('loan/flexible/borrow/history', 'sapi', 'GET', {'cost': 40}) + sapi_get_loan_flexible_repay_history = sapiGetLoanFlexibleRepayHistory = Entry('loan/flexible/repay/history', 'sapi', 'GET', {'cost': 40}) + sapi_get_loan_flexible_ltv_adjustment_history = sapiGetLoanFlexibleLtvAdjustmentHistory = Entry('loan/flexible/ltv/adjustment/history', 'sapi', 'GET', {'cost': 40}) + sapi_get_loan_vip_ongoing_orders = sapiGetLoanVipOngoingOrders = Entry('loan/vip/ongoing/orders', 'sapi', 'GET', {'cost': 40}) + sapi_get_loan_vip_repay_history = sapiGetLoanVipRepayHistory = Entry('loan/vip/repay/history', 'sapi', 'GET', {'cost': 40}) + sapi_get_loan_vip_collateral_account = sapiGetLoanVipCollateralAccount = Entry('loan/vip/collateral/account', 'sapi', 'GET', {'cost': 600}) + sapi_get_fiat_orders = sapiGetFiatOrders = Entry('fiat/orders', 'sapi', 'GET', {'cost': 600.03}) + sapi_get_fiat_payments = sapiGetFiatPayments = Entry('fiat/payments', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_futures_transfer = sapiGetFuturesTransfer = Entry('futures/transfer', 'sapi', 'GET', {'cost': 1}) + sapi_get_futures_histdatalink = sapiGetFuturesHistDataLink = Entry('futures/histDataLink', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_rebate_taxquery = sapiGetRebateTaxQuery = Entry('rebate/taxQuery', 'sapi', 'GET', {'cost': 80.004}) + sapi_get_capital_config_getall = sapiGetCapitalConfigGetall = Entry('capital/config/getall', 'sapi', 'GET', {'cost': 1}) + sapi_get_capital_deposit_address = sapiGetCapitalDepositAddress = Entry('capital/deposit/address', 'sapi', 'GET', {'cost': 1}) + sapi_get_capital_deposit_address_list = sapiGetCapitalDepositAddressList = Entry('capital/deposit/address/list', 'sapi', 'GET', {'cost': 1}) + sapi_get_capital_deposit_hisrec = sapiGetCapitalDepositHisrec = Entry('capital/deposit/hisrec', 'sapi', 'GET', {'cost': 1}) + sapi_get_capital_deposit_subaddress = sapiGetCapitalDepositSubAddress = Entry('capital/deposit/subAddress', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_capital_deposit_subhisrec = sapiGetCapitalDepositSubHisrec = Entry('capital/deposit/subHisrec', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_capital_withdraw_history = sapiGetCapitalWithdrawHistory = Entry('capital/withdraw/history', 'sapi', 'GET', {'cost': 1}) + sapi_get_capital_withdraw_address_list = sapiGetCapitalWithdrawAddressList = Entry('capital/withdraw/address/list', 'sapi', 'GET', {'cost': 10}) + sapi_get_capital_contract_convertible_coins = sapiGetCapitalContractConvertibleCoins = Entry('capital/contract/convertible-coins', 'sapi', 'GET', {'cost': 4.0002}) + sapi_get_convert_tradeflow = sapiGetConvertTradeFlow = Entry('convert/tradeFlow', 'sapi', 'GET', {'cost': 20.001}) + sapi_get_convert_exchangeinfo = sapiGetConvertExchangeInfo = Entry('convert/exchangeInfo', 'sapi', 'GET', {'cost': 50}) + sapi_get_convert_assetinfo = sapiGetConvertAssetInfo = Entry('convert/assetInfo', 'sapi', 'GET', {'cost': 10}) + sapi_get_convert_orderstatus = sapiGetConvertOrderStatus = Entry('convert/orderStatus', 'sapi', 'GET', {'cost': 0.6667}) + sapi_get_convert_limit_queryopenorders = sapiGetConvertLimitQueryOpenOrders = Entry('convert/limit/queryOpenOrders', 'sapi', 'GET', {'cost': 20.001}) + sapi_get_account_status = sapiGetAccountStatus = Entry('account/status', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_account_apitradingstatus = sapiGetAccountApiTradingStatus = Entry('account/apiTradingStatus', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_account_apirestrictions_iprestriction = sapiGetAccountApiRestrictionsIpRestriction = Entry('account/apiRestrictions/ipRestriction', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_bnbburn = sapiGetBnbBurn = Entry('bnbBurn', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_sub_account_futures_account = sapiGetSubAccountFuturesAccount = Entry('sub-account/futures/account', 'sapi', 'GET', {'cost': 1}) + sapi_get_sub_account_futures_accountsummary = sapiGetSubAccountFuturesAccountSummary = Entry('sub-account/futures/accountSummary', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_sub_account_futures_positionrisk = sapiGetSubAccountFuturesPositionRisk = Entry('sub-account/futures/positionRisk', 'sapi', 'GET', {'cost': 1}) + sapi_get_sub_account_futures_internaltransfer = sapiGetSubAccountFuturesInternalTransfer = Entry('sub-account/futures/internalTransfer', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_sub_account_list = sapiGetSubAccountList = Entry('sub-account/list', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_sub_account_margin_account = sapiGetSubAccountMarginAccount = Entry('sub-account/margin/account', 'sapi', 'GET', {'cost': 1}) + sapi_get_sub_account_margin_accountsummary = sapiGetSubAccountMarginAccountSummary = Entry('sub-account/margin/accountSummary', 'sapi', 'GET', {'cost': 1}) + sapi_get_sub_account_spotsummary = sapiGetSubAccountSpotSummary = Entry('sub-account/spotSummary', 'sapi', 'GET', {'cost': 1}) + sapi_get_sub_account_status = sapiGetSubAccountStatus = Entry('sub-account/status', 'sapi', 'GET', {'cost': 1}) + sapi_get_sub_account_sub_transfer_history = sapiGetSubAccountSubTransferHistory = Entry('sub-account/sub/transfer/history', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_sub_account_transfer_subuserhistory = sapiGetSubAccountTransferSubUserHistory = Entry('sub-account/transfer/subUserHistory', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_sub_account_universaltransfer = sapiGetSubAccountUniversalTransfer = Entry('sub-account/universalTransfer', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_sub_account_apirestrictions_iprestriction_thirdpartylist = sapiGetSubAccountApiRestrictionsIpRestrictionThirdPartyList = Entry('sub-account/apiRestrictions/ipRestriction/thirdPartyList', 'sapi', 'GET', {'cost': 1}) + sapi_get_sub_account_transaction_statistics = sapiGetSubAccountTransactionStatistics = Entry('sub-account/transaction-statistics', 'sapi', 'GET', {'cost': 0.40002}) + sapi_get_sub_account_subaccountapi_iprestriction = sapiGetSubAccountSubAccountApiIpRestriction = Entry('sub-account/subAccountApi/ipRestriction', 'sapi', 'GET', {'cost': 20.001}) + sapi_get_managed_subaccount_asset = sapiGetManagedSubaccountAsset = Entry('managed-subaccount/asset', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_managed_subaccount_accountsnapshot = sapiGetManagedSubaccountAccountSnapshot = Entry('managed-subaccount/accountSnapshot', 'sapi', 'GET', {'cost': 240}) + sapi_get_managed_subaccount_querytranslogforinvestor = sapiGetManagedSubaccountQueryTransLogForInvestor = Entry('managed-subaccount/queryTransLogForInvestor', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_managed_subaccount_querytranslogfortradeparent = sapiGetManagedSubaccountQueryTransLogForTradeParent = Entry('managed-subaccount/queryTransLogForTradeParent', 'sapi', 'GET', {'cost': 0.40002}) + sapi_get_managed_subaccount_fetch_future_asset = sapiGetManagedSubaccountFetchFutureAsset = Entry('managed-subaccount/fetch-future-asset', 'sapi', 'GET', {'cost': 0.40002}) + sapi_get_managed_subaccount_marginasset = sapiGetManagedSubaccountMarginAsset = Entry('managed-subaccount/marginAsset', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_managed_subaccount_info = sapiGetManagedSubaccountInfo = Entry('managed-subaccount/info', 'sapi', 'GET', {'cost': 0.40002}) + sapi_get_managed_subaccount_deposit_address = sapiGetManagedSubaccountDepositAddress = Entry('managed-subaccount/deposit/address', 'sapi', 'GET', {'cost': 0.006667}) + sapi_get_managed_subaccount_query_trans_log = sapiGetManagedSubaccountQueryTransLog = Entry('managed-subaccount/query-trans-log', 'sapi', 'GET', {'cost': 0.40002}) + sapi_get_lending_daily_product_list = sapiGetLendingDailyProductList = Entry('lending/daily/product/list', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_daily_userleftquota = sapiGetLendingDailyUserLeftQuota = Entry('lending/daily/userLeftQuota', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_daily_userredemptionquota = sapiGetLendingDailyUserRedemptionQuota = Entry('lending/daily/userRedemptionQuota', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_daily_token_position = sapiGetLendingDailyTokenPosition = Entry('lending/daily/token/position', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_union_account = sapiGetLendingUnionAccount = Entry('lending/union/account', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_union_purchaserecord = sapiGetLendingUnionPurchaseRecord = Entry('lending/union/purchaseRecord', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_union_redemptionrecord = sapiGetLendingUnionRedemptionRecord = Entry('lending/union/redemptionRecord', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_union_interesthistory = sapiGetLendingUnionInterestHistory = Entry('lending/union/interestHistory', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_project_list = sapiGetLendingProjectList = Entry('lending/project/list', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_project_position_list = sapiGetLendingProjectPositionList = Entry('lending/project/position/list', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_eth_staking_eth_history_stakinghistory = sapiGetEthStakingEthHistoryStakingHistory = Entry('eth-staking/eth/history/stakingHistory', 'sapi', 'GET', {'cost': 15}) + sapi_get_eth_staking_eth_history_redemptionhistory = sapiGetEthStakingEthHistoryRedemptionHistory = Entry('eth-staking/eth/history/redemptionHistory', 'sapi', 'GET', {'cost': 15}) + sapi_get_eth_staking_eth_history_rewardshistory = sapiGetEthStakingEthHistoryRewardsHistory = Entry('eth-staking/eth/history/rewardsHistory', 'sapi', 'GET', {'cost': 15}) + sapi_get_eth_staking_eth_quota = sapiGetEthStakingEthQuota = Entry('eth-staking/eth/quota', 'sapi', 'GET', {'cost': 15}) + sapi_get_eth_staking_eth_history_ratehistory = sapiGetEthStakingEthHistoryRateHistory = Entry('eth-staking/eth/history/rateHistory', 'sapi', 'GET', {'cost': 15}) + sapi_get_eth_staking_account = sapiGetEthStakingAccount = Entry('eth-staking/account', 'sapi', 'GET', {'cost': 15}) + sapi_get_eth_staking_wbeth_history_wraphistory = sapiGetEthStakingWbethHistoryWrapHistory = Entry('eth-staking/wbeth/history/wrapHistory', 'sapi', 'GET', {'cost': 15}) + sapi_get_eth_staking_wbeth_history_unwraphistory = sapiGetEthStakingWbethHistoryUnwrapHistory = Entry('eth-staking/wbeth/history/unwrapHistory', 'sapi', 'GET', {'cost': 15}) + sapi_get_eth_staking_eth_history_wbethrewardshistory = sapiGetEthStakingEthHistoryWbethRewardsHistory = Entry('eth-staking/eth/history/wbethRewardsHistory', 'sapi', 'GET', {'cost': 15}) + sapi_get_sol_staking_sol_history_stakinghistory = sapiGetSolStakingSolHistoryStakingHistory = Entry('sol-staking/sol/history/stakingHistory', 'sapi', 'GET', {'cost': 15}) + sapi_get_sol_staking_sol_history_redemptionhistory = sapiGetSolStakingSolHistoryRedemptionHistory = Entry('sol-staking/sol/history/redemptionHistory', 'sapi', 'GET', {'cost': 15}) + sapi_get_sol_staking_sol_history_bnsolrewardshistory = sapiGetSolStakingSolHistoryBnsolRewardsHistory = Entry('sol-staking/sol/history/bnsolRewardsHistory', 'sapi', 'GET', {'cost': 15}) + sapi_get_sol_staking_sol_history_ratehistory = sapiGetSolStakingSolHistoryRateHistory = Entry('sol-staking/sol/history/rateHistory', 'sapi', 'GET', {'cost': 15}) + sapi_get_sol_staking_account = sapiGetSolStakingAccount = Entry('sol-staking/account', 'sapi', 'GET', {'cost': 15}) + sapi_get_sol_staking_sol_quota = sapiGetSolStakingSolQuota = Entry('sol-staking/sol/quota', 'sapi', 'GET', {'cost': 15}) + sapi_get_mining_pub_algolist = sapiGetMiningPubAlgoList = Entry('mining/pub/algoList', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_mining_pub_coinlist = sapiGetMiningPubCoinList = Entry('mining/pub/coinList', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_mining_worker_detail = sapiGetMiningWorkerDetail = Entry('mining/worker/detail', 'sapi', 'GET', {'cost': 0.5}) + sapi_get_mining_worker_list = sapiGetMiningWorkerList = Entry('mining/worker/list', 'sapi', 'GET', {'cost': 0.5}) + sapi_get_mining_payment_list = sapiGetMiningPaymentList = Entry('mining/payment/list', 'sapi', 'GET', {'cost': 0.5}) + sapi_get_mining_statistics_user_status = sapiGetMiningStatisticsUserStatus = Entry('mining/statistics/user/status', 'sapi', 'GET', {'cost': 0.5}) + sapi_get_mining_statistics_user_list = sapiGetMiningStatisticsUserList = Entry('mining/statistics/user/list', 'sapi', 'GET', {'cost': 0.5}) + sapi_get_mining_payment_uid = sapiGetMiningPaymentUid = Entry('mining/payment/uid', 'sapi', 'GET', {'cost': 0.5}) + sapi_get_bswap_pools = sapiGetBswapPools = Entry('bswap/pools', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_bswap_liquidity = sapiGetBswapLiquidity = Entry('bswap/liquidity', 'sapi', 'GET', {'cost': 0.1, 'noPoolId': 1}) + sapi_get_bswap_liquidityops = sapiGetBswapLiquidityOps = Entry('bswap/liquidityOps', 'sapi', 'GET', {'cost': 20.001}) + sapi_get_bswap_quote = sapiGetBswapQuote = Entry('bswap/quote', 'sapi', 'GET', {'cost': 1.00005}) + sapi_get_bswap_swap = sapiGetBswapSwap = Entry('bswap/swap', 'sapi', 'GET', {'cost': 20.001}) + sapi_get_bswap_poolconfigure = sapiGetBswapPoolConfigure = Entry('bswap/poolConfigure', 'sapi', 'GET', {'cost': 1.00005}) + sapi_get_bswap_addliquiditypreview = sapiGetBswapAddLiquidityPreview = Entry('bswap/addLiquidityPreview', 'sapi', 'GET', {'cost': 1.00005}) + sapi_get_bswap_removeliquiditypreview = sapiGetBswapRemoveLiquidityPreview = Entry('bswap/removeLiquidityPreview', 'sapi', 'GET', {'cost': 1.00005}) + sapi_get_bswap_unclaimedrewards = sapiGetBswapUnclaimedRewards = Entry('bswap/unclaimedRewards', 'sapi', 'GET', {'cost': 6.667}) + sapi_get_bswap_claimedhistory = sapiGetBswapClaimedHistory = Entry('bswap/claimedHistory', 'sapi', 'GET', {'cost': 6.667}) + sapi_get_blvt_tokeninfo = sapiGetBlvtTokenInfo = Entry('blvt/tokenInfo', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_blvt_subscribe_record = sapiGetBlvtSubscribeRecord = Entry('blvt/subscribe/record', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_blvt_redeem_record = sapiGetBlvtRedeemRecord = Entry('blvt/redeem/record', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_blvt_userlimit = sapiGetBlvtUserLimit = Entry('blvt/userLimit', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_apireferral_ifnewuser = sapiGetApiReferralIfNewUser = Entry('apiReferral/ifNewUser', 'sapi', 'GET', {'cost': 1}) + sapi_get_apireferral_customization = sapiGetApiReferralCustomization = Entry('apiReferral/customization', 'sapi', 'GET', {'cost': 1}) + sapi_get_apireferral_usercustomization = sapiGetApiReferralUserCustomization = Entry('apiReferral/userCustomization', 'sapi', 'GET', {'cost': 1}) + sapi_get_apireferral_rebate_recentrecord = sapiGetApiReferralRebateRecentRecord = Entry('apiReferral/rebate/recentRecord', 'sapi', 'GET', {'cost': 1}) + sapi_get_apireferral_rebate_historicalrecord = sapiGetApiReferralRebateHistoricalRecord = Entry('apiReferral/rebate/historicalRecord', 'sapi', 'GET', {'cost': 1}) + sapi_get_apireferral_kickback_recentrecord = sapiGetApiReferralKickbackRecentRecord = Entry('apiReferral/kickback/recentRecord', 'sapi', 'GET', {'cost': 1}) + sapi_get_apireferral_kickback_historicalrecord = sapiGetApiReferralKickbackHistoricalRecord = Entry('apiReferral/kickback/historicalRecord', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_subaccountapi = sapiGetBrokerSubAccountApi = Entry('broker/subAccountApi', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_subaccount = sapiGetBrokerSubAccount = Entry('broker/subAccount', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_subaccountapi_commission_futures = sapiGetBrokerSubAccountApiCommissionFutures = Entry('broker/subAccountApi/commission/futures', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_subaccountapi_commission_coinfutures = sapiGetBrokerSubAccountApiCommissionCoinFutures = Entry('broker/subAccountApi/commission/coinFutures', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_info = sapiGetBrokerInfo = Entry('broker/info', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_transfer = sapiGetBrokerTransfer = Entry('broker/transfer', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_transfer_futures = sapiGetBrokerTransferFutures = Entry('broker/transfer/futures', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_rebate_recentrecord = sapiGetBrokerRebateRecentRecord = Entry('broker/rebate/recentRecord', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_rebate_historicalrecord = sapiGetBrokerRebateHistoricalRecord = Entry('broker/rebate/historicalRecord', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_subaccount_bnbburn_status = sapiGetBrokerSubAccountBnbBurnStatus = Entry('broker/subAccount/bnbBurn/status', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_subaccount_deposithist = sapiGetBrokerSubAccountDepositHist = Entry('broker/subAccount/depositHist', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_subaccount_spotsummary = sapiGetBrokerSubAccountSpotSummary = Entry('broker/subAccount/spotSummary', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_subaccount_marginsummary = sapiGetBrokerSubAccountMarginSummary = Entry('broker/subAccount/marginSummary', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_subaccount_futuressummary = sapiGetBrokerSubAccountFuturesSummary = Entry('broker/subAccount/futuresSummary', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_rebate_futures_recentrecord = sapiGetBrokerRebateFuturesRecentRecord = Entry('broker/rebate/futures/recentRecord', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_subaccountapi_iprestriction = sapiGetBrokerSubAccountApiIpRestriction = Entry('broker/subAccountApi/ipRestriction', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_universaltransfer = sapiGetBrokerUniversalTransfer = Entry('broker/universalTransfer', 'sapi', 'GET', {'cost': 1}) + sapi_get_account_apirestrictions = sapiGetAccountApiRestrictions = Entry('account/apiRestrictions', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_c2c_ordermatch_listuserorderhistory = sapiGetC2cOrderMatchListUserOrderHistory = Entry('c2c/orderMatch/listUserOrderHistory', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_nft_history_transactions = sapiGetNftHistoryTransactions = Entry('nft/history/transactions', 'sapi', 'GET', {'cost': 20.001}) + sapi_get_nft_history_deposit = sapiGetNftHistoryDeposit = Entry('nft/history/deposit', 'sapi', 'GET', {'cost': 20.001}) + sapi_get_nft_history_withdraw = sapiGetNftHistoryWithdraw = Entry('nft/history/withdraw', 'sapi', 'GET', {'cost': 20.001}) + sapi_get_nft_user_getasset = sapiGetNftUserGetAsset = Entry('nft/user/getAsset', 'sapi', 'GET', {'cost': 20.001}) + sapi_get_pay_transactions = sapiGetPayTransactions = Entry('pay/transactions', 'sapi', 'GET', {'cost': 20.001}) + sapi_get_giftcard_verify = sapiGetGiftcardVerify = Entry('giftcard/verify', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_giftcard_cryptography_rsa_public_key = sapiGetGiftcardCryptographyRsaPublicKey = Entry('giftcard/cryptography/rsa-public-key', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_giftcard_buycode_token_limit = sapiGetGiftcardBuyCodeTokenLimit = Entry('giftcard/buyCode/token-limit', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_algo_spot_openorders = sapiGetAlgoSpotOpenOrders = Entry('algo/spot/openOrders', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_algo_spot_historicalorders = sapiGetAlgoSpotHistoricalOrders = Entry('algo/spot/historicalOrders', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_algo_spot_suborders = sapiGetAlgoSpotSubOrders = Entry('algo/spot/subOrders', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_algo_futures_openorders = sapiGetAlgoFuturesOpenOrders = Entry('algo/futures/openOrders', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_algo_futures_historicalorders = sapiGetAlgoFuturesHistoricalOrders = Entry('algo/futures/historicalOrders', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_algo_futures_suborders = sapiGetAlgoFuturesSubOrders = Entry('algo/futures/subOrders', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_portfolio_account = sapiGetPortfolioAccount = Entry('portfolio/account', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_portfolio_collateralrate = sapiGetPortfolioCollateralRate = Entry('portfolio/collateralRate', 'sapi', 'GET', {'cost': 5}) + sapi_get_portfolio_pmloan = sapiGetPortfolioPmLoan = Entry('portfolio/pmLoan', 'sapi', 'GET', {'cost': 3.3335}) + sapi_get_portfolio_interest_history = sapiGetPortfolioInterestHistory = Entry('portfolio/interest-history', 'sapi', 'GET', {'cost': 0.6667}) + sapi_get_portfolio_asset_index_price = sapiGetPortfolioAssetIndexPrice = Entry('portfolio/asset-index-price', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_portfolio_repay_futures_switch = sapiGetPortfolioRepayFuturesSwitch = Entry('portfolio/repay-futures-switch', 'sapi', 'GET', {'cost': 3}) + sapi_get_portfolio_margin_asset_leverage = sapiGetPortfolioMarginAssetLeverage = Entry('portfolio/margin-asset-leverage', 'sapi', 'GET', {'cost': 5}) + sapi_get_portfolio_balance = sapiGetPortfolioBalance = Entry('portfolio/balance', 'sapi', 'GET', {'cost': 2}) + sapi_get_portfolio_negative_balance_exchange_record = sapiGetPortfolioNegativeBalanceExchangeRecord = Entry('portfolio/negative-balance-exchange-record', 'sapi', 'GET', {'cost': 2}) + sapi_get_portfolio_pmloan_history = sapiGetPortfolioPmloanHistory = Entry('portfolio/pmloan-history', 'sapi', 'GET', {'cost': 5}) + sapi_get_portfolio_earn_asset_balance = sapiGetPortfolioEarnAssetBalance = Entry('portfolio/earn-asset-balance', 'sapi', 'GET', {'cost': 150}) + sapi_get_staking_productlist = sapiGetStakingProductList = Entry('staking/productList', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_staking_position = sapiGetStakingPosition = Entry('staking/position', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_staking_stakingrecord = sapiGetStakingStakingRecord = Entry('staking/stakingRecord', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_staking_personalleftquota = sapiGetStakingPersonalLeftQuota = Entry('staking/personalLeftQuota', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_auto_invest_target_asset_list = sapiGetLendingAutoInvestTargetAssetList = Entry('lending/auto-invest/target-asset/list', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_auto_invest_target_asset_roi_list = sapiGetLendingAutoInvestTargetAssetRoiList = Entry('lending/auto-invest/target-asset/roi/list', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_auto_invest_all_asset = sapiGetLendingAutoInvestAllAsset = Entry('lending/auto-invest/all/asset', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_auto_invest_source_asset_list = sapiGetLendingAutoInvestSourceAssetList = Entry('lending/auto-invest/source-asset/list', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_auto_invest_plan_list = sapiGetLendingAutoInvestPlanList = Entry('lending/auto-invest/plan/list', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_auto_invest_plan_id = sapiGetLendingAutoInvestPlanId = Entry('lending/auto-invest/plan/id', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_auto_invest_history_list = sapiGetLendingAutoInvestHistoryList = Entry('lending/auto-invest/history/list', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_auto_invest_index_info = sapiGetLendingAutoInvestIndexInfo = Entry('lending/auto-invest/index/info', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_auto_invest_index_user_summary = sapiGetLendingAutoInvestIndexUserSummary = Entry('lending/auto-invest/index/user-summary', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_auto_invest_one_off_status = sapiGetLendingAutoInvestOneOffStatus = Entry('lending/auto-invest/one-off/status', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_auto_invest_redeem_history = sapiGetLendingAutoInvestRedeemHistory = Entry('lending/auto-invest/redeem/history', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_auto_invest_rebalance_history = sapiGetLendingAutoInvestRebalanceHistory = Entry('lending/auto-invest/rebalance/history', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_simple_earn_flexible_list = sapiGetSimpleEarnFlexibleList = Entry('simple-earn/flexible/list', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_locked_list = sapiGetSimpleEarnLockedList = Entry('simple-earn/locked/list', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_flexible_personalleftquota = sapiGetSimpleEarnFlexiblePersonalLeftQuota = Entry('simple-earn/flexible/personalLeftQuota', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_locked_personalleftquota = sapiGetSimpleEarnLockedPersonalLeftQuota = Entry('simple-earn/locked/personalLeftQuota', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_flexible_subscriptionpreview = sapiGetSimpleEarnFlexibleSubscriptionPreview = Entry('simple-earn/flexible/subscriptionPreview', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_locked_subscriptionpreview = sapiGetSimpleEarnLockedSubscriptionPreview = Entry('simple-earn/locked/subscriptionPreview', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_flexible_history_ratehistory = sapiGetSimpleEarnFlexibleHistoryRateHistory = Entry('simple-earn/flexible/history/rateHistory', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_flexible_position = sapiGetSimpleEarnFlexiblePosition = Entry('simple-earn/flexible/position', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_locked_position = sapiGetSimpleEarnLockedPosition = Entry('simple-earn/locked/position', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_account = sapiGetSimpleEarnAccount = Entry('simple-earn/account', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_flexible_history_subscriptionrecord = sapiGetSimpleEarnFlexibleHistorySubscriptionRecord = Entry('simple-earn/flexible/history/subscriptionRecord', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_locked_history_subscriptionrecord = sapiGetSimpleEarnLockedHistorySubscriptionRecord = Entry('simple-earn/locked/history/subscriptionRecord', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_flexible_history_redemptionrecord = sapiGetSimpleEarnFlexibleHistoryRedemptionRecord = Entry('simple-earn/flexible/history/redemptionRecord', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_locked_history_redemptionrecord = sapiGetSimpleEarnLockedHistoryRedemptionRecord = Entry('simple-earn/locked/history/redemptionRecord', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_flexible_history_rewardsrecord = sapiGetSimpleEarnFlexibleHistoryRewardsRecord = Entry('simple-earn/flexible/history/rewardsRecord', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_locked_history_rewardsrecord = sapiGetSimpleEarnLockedHistoryRewardsRecord = Entry('simple-earn/locked/history/rewardsRecord', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_flexible_history_collateralrecord = sapiGetSimpleEarnFlexibleHistoryCollateralRecord = Entry('simple-earn/flexible/history/collateralRecord', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_dci_product_list = sapiGetDciProductList = Entry('dci/product/list', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_dci_product_positions = sapiGetDciProductPositions = Entry('dci/product/positions', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_dci_product_accounts = sapiGetDciProductAccounts = Entry('dci/product/accounts', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_asset_assetdistributionhistory = sapiGetAssetAssetDistributionHistory = Entry('asset/assetDistributionHistory', 'sapi', 'GET', {'cost': 1}) + sapi_get_asset_query_trading_fee = sapiGetAssetQueryTradingFee = Entry('asset/query/trading-fee', 'sapi', 'GET', {'cost': 1}) + sapi_get_asset_query_trading_volume = sapiGetAssetQueryTradingVolume = Entry('asset/query/trading-volume', 'sapi', 'GET', {'cost': 1}) + sapi_get_otc_coinpairs = sapiGetOtcCoinPairs = Entry('otc/coinPairs', 'sapi', 'GET', {'cost': 1}) + sapi_get_otc_orders_orderid = sapiGetOtcOrdersOrderId = Entry('otc/orders/{orderId}', 'sapi', 'GET', {'cost': 1}) + sapi_get_otc_orders = sapiGetOtcOrders = Entry('otc/orders', 'sapi', 'GET', {'cost': 1}) + sapi_get_ocbs_orders = sapiGetOcbsOrders = Entry('ocbs/orders', 'sapi', 'GET', {'cost': 1}) + sapi_get_fiatpayment_query_withdraw_history = sapiGetFiatpaymentQueryWithdrawHistory = Entry('fiatpayment/query/withdraw/history', 'sapi', 'GET', {'cost': 1}) + sapi_get_fiatpayment_query_deposit_history = sapiGetFiatpaymentQueryDepositHistory = Entry('fiatpayment/query/deposit/history', 'sapi', 'GET', {'cost': 1}) + sapi_get_capital_sub_account_deposit_address = sapiGetCapitalSubAccountDepositAddress = Entry('capital/sub-account/deposit/address', 'sapi', 'GET', {'cost': 1}) + sapi_get_capital_sub_account_deposit_history = sapiGetCapitalSubAccountDepositHistory = Entry('capital/sub-account/deposit/history', 'sapi', 'GET', {'cost': 1}) + sapi_get_asset_query_dust_logs = sapiGetAssetQueryDustLogs = Entry('asset/query/dust-logs', 'sapi', 'GET', {'cost': 1}) + sapi_get_asset_query_dust_assets = sapiGetAssetQueryDustAssets = Entry('asset/query/dust-assets', 'sapi', 'GET', {'cost': 1}) + sapi_get_marketing_referral_reward_history = sapiGetMarketingReferralRewardHistory = Entry('marketing/referral/reward/history', 'sapi', 'GET', {'cost': 1}) + sapi_get_staking_asset = sapiGetStakingAsset = Entry('staking/asset', 'sapi', 'GET', {'cost': 1}) + sapi_get_staking_stakingbalance = sapiGetStakingStakingBalance = Entry('staking/stakingBalance', 'sapi', 'GET', {'cost': 1}) + sapi_get_staking_history = sapiGetStakingHistory = Entry('staking/history', 'sapi', 'GET', {'cost': 1}) + sapi_get_staking_stakingrewardshistory = sapiGetStakingStakingRewardsHistory = Entry('staking/stakingRewardsHistory', 'sapi', 'GET', {'cost': 1}) + sapi_get_custodian_balance = sapiGetCustodianBalance = Entry('custodian/balance', 'sapi', 'GET', {'cost': 1}) + sapi_get_custodian_supportedassetlist = sapiGetCustodianSupportedAssetList = Entry('custodian/supportedAssetList', 'sapi', 'GET', {'cost': 1}) + sapi_get_custodian_wallettransferhistory = sapiGetCustodianWalletTransferHistory = Entry('custodian/walletTransferHistory', 'sapi', 'GET', {'cost': 1}) + sapi_get_custodian_custodiantransferhistory = sapiGetCustodianCustodianTransferHistory = Entry('custodian/custodianTransferHistory', 'sapi', 'GET', {'cost': 1}) + sapi_get_custodian_openorders = sapiGetCustodianOpenOrders = Entry('custodian/openOrders', 'sapi', 'GET', {'cost': 1}) + sapi_get_custodian_order = sapiGetCustodianOrder = Entry('custodian/order', 'sapi', 'GET', {'cost': 1}) + sapi_get_custodian_orderhistory = sapiGetCustodianOrderHistory = Entry('custodian/orderHistory', 'sapi', 'GET', {'cost': 1}) + sapi_get_custodian_tradehistory = sapiGetCustodianTradeHistory = Entry('custodian/tradeHistory', 'sapi', 'GET', {'cost': 1}) + sapi_get_custodian_settlementsetting = sapiGetCustodianSettlementSetting = Entry('custodian/settlementSetting', 'sapi', 'GET', {'cost': 1}) + sapi_get_custodian_settlementhistory = sapiGetCustodianSettlementHistory = Entry('custodian/settlementHistory', 'sapi', 'GET', {'cost': 1}) + sapi_get_cl_transferhistory = sapiGetClTransferHistory = Entry('cl/transferHistory', 'sapi', 'GET', {'cost': 1}) + sapi_get_apipartner_checkeligibility = sapiGetApipartnerCheckEligibility = Entry('apipartner/checkEligibility', 'sapi', 'GET', {'cost': 1}) + sapi_get_apipartner_rebatehistory = sapiGetApipartnerRebateHistory = Entry('apipartner/rebateHistory', 'sapi', 'GET', {'cost': 1}) + sapi_post_asset_dust = sapiPostAssetDust = Entry('asset/dust', 'sapi', 'POST', {'cost': 10}) + sapi_post_asset_dust_btc = sapiPostAssetDustBtc = Entry('asset/dust-btc', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_asset_transfer = sapiPostAssetTransfer = Entry('asset/transfer', 'sapi', 'POST', {'cost': 6.0003}) + sapi_post_asset_get_funding_asset = sapiPostAssetGetFundingAsset = Entry('asset/get-funding-asset', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_asset_convert_transfer = sapiPostAssetConvertTransfer = Entry('asset/convert-transfer', 'sapi', 'POST', {'cost': 0.033335}) + sapi_post_account_disablefastwithdrawswitch = sapiPostAccountDisableFastWithdrawSwitch = Entry('account/disableFastWithdrawSwitch', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_account_enablefastwithdrawswitch = sapiPostAccountEnableFastWithdrawSwitch = Entry('account/enableFastWithdrawSwitch', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_capital_withdraw_apply = sapiPostCapitalWithdrawApply = Entry('capital/withdraw/apply', 'sapi', 'POST', {'cost': 1}) + sapi_post_capital_contract_convertible_coins = sapiPostCapitalContractConvertibleCoins = Entry('capital/contract/convertible-coins', 'sapi', 'POST', {'cost': 4.0002}) + sapi_post_capital_deposit_credit_apply = sapiPostCapitalDepositCreditApply = Entry('capital/deposit/credit-apply', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_margin_borrow_repay = sapiPostMarginBorrowRepay = Entry('margin/borrow-repay', 'sapi', 'POST', {'cost': 20.001}) + sapi_post_margin_transfer = sapiPostMarginTransfer = Entry('margin/transfer', 'sapi', 'POST', {'cost': 4.0002}) + sapi_post_margin_loan = sapiPostMarginLoan = Entry('margin/loan', 'sapi', 'POST', {'cost': 20.001}) + sapi_post_margin_repay = sapiPostMarginRepay = Entry('margin/repay', 'sapi', 'POST', {'cost': 20.001}) + sapi_post_margin_order = sapiPostMarginOrder = Entry('margin/order', 'sapi', 'POST', {'cost': 0.040002}) + sapi_post_margin_order_oco = sapiPostMarginOrderOco = Entry('margin/order/oco', 'sapi', 'POST', {'cost': 0.040002}) + sapi_post_margin_dust = sapiPostMarginDust = Entry('margin/dust', 'sapi', 'POST', {'cost': 20.001}) + sapi_post_margin_exchange_small_liability = sapiPostMarginExchangeSmallLiability = Entry('margin/exchange-small-liability', 'sapi', 'POST', {'cost': 20.001}) + sapi_post_margin_isolated_transfer = sapiPostMarginIsolatedTransfer = Entry('margin/isolated/transfer', 'sapi', 'POST', {'cost': 4.0002}) + sapi_post_margin_isolated_account = sapiPostMarginIsolatedAccount = Entry('margin/isolated/account', 'sapi', 'POST', {'cost': 2.0001}) + sapi_post_margin_max_leverage = sapiPostMarginMaxLeverage = Entry('margin/max-leverage', 'sapi', 'POST', {'cost': 300}) + sapi_post_bnbburn = sapiPostBnbBurn = Entry('bnbBurn', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_sub_account_virtualsubaccount = sapiPostSubAccountVirtualSubAccount = Entry('sub-account/virtualSubAccount', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_sub_account_margin_transfer = sapiPostSubAccountMarginTransfer = Entry('sub-account/margin/transfer', 'sapi', 'POST', {'cost': 4.0002}) + sapi_post_sub_account_margin_enable = sapiPostSubAccountMarginEnable = Entry('sub-account/margin/enable', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_sub_account_futures_enable = sapiPostSubAccountFuturesEnable = Entry('sub-account/futures/enable', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_sub_account_futures_transfer = sapiPostSubAccountFuturesTransfer = Entry('sub-account/futures/transfer', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_sub_account_futures_internaltransfer = sapiPostSubAccountFuturesInternalTransfer = Entry('sub-account/futures/internalTransfer', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_sub_account_transfer_subtosub = sapiPostSubAccountTransferSubToSub = Entry('sub-account/transfer/subToSub', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_sub_account_transfer_subtomaster = sapiPostSubAccountTransferSubToMaster = Entry('sub-account/transfer/subToMaster', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_sub_account_universaltransfer = sapiPostSubAccountUniversalTransfer = Entry('sub-account/universalTransfer', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_sub_account_options_enable = sapiPostSubAccountOptionsEnable = Entry('sub-account/options/enable', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_managed_subaccount_deposit = sapiPostManagedSubaccountDeposit = Entry('managed-subaccount/deposit', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_managed_subaccount_withdraw = sapiPostManagedSubaccountWithdraw = Entry('managed-subaccount/withdraw', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_userdatastream = sapiPostUserDataStream = Entry('userDataStream', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_userdatastream_isolated = sapiPostUserDataStreamIsolated = Entry('userDataStream/isolated', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_futures_transfer = sapiPostFuturesTransfer = Entry('futures/transfer', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_lending_customizedfixed_purchase = sapiPostLendingCustomizedFixedPurchase = Entry('lending/customizedFixed/purchase', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_lending_daily_purchase = sapiPostLendingDailyPurchase = Entry('lending/daily/purchase', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_lending_daily_redeem = sapiPostLendingDailyRedeem = Entry('lending/daily/redeem', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_bswap_liquidityadd = sapiPostBswapLiquidityAdd = Entry('bswap/liquidityAdd', 'sapi', 'POST', {'cost': 60}) + sapi_post_bswap_liquidityremove = sapiPostBswapLiquidityRemove = Entry('bswap/liquidityRemove', 'sapi', 'POST', {'cost': 60}) + sapi_post_bswap_swap = sapiPostBswapSwap = Entry('bswap/swap', 'sapi', 'POST', {'cost': 60}) + sapi_post_bswap_claimrewards = sapiPostBswapClaimRewards = Entry('bswap/claimRewards', 'sapi', 'POST', {'cost': 6.667}) + sapi_post_blvt_subscribe = sapiPostBlvtSubscribe = Entry('blvt/subscribe', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_blvt_redeem = sapiPostBlvtRedeem = Entry('blvt/redeem', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_apireferral_customization = sapiPostApiReferralCustomization = Entry('apiReferral/customization', 'sapi', 'POST', {'cost': 1}) + sapi_post_apireferral_usercustomization = sapiPostApiReferralUserCustomization = Entry('apiReferral/userCustomization', 'sapi', 'POST', {'cost': 1}) + sapi_post_apireferral_rebate_historicalrecord = sapiPostApiReferralRebateHistoricalRecord = Entry('apiReferral/rebate/historicalRecord', 'sapi', 'POST', {'cost': 1}) + sapi_post_apireferral_kickback_historicalrecord = sapiPostApiReferralKickbackHistoricalRecord = Entry('apiReferral/kickback/historicalRecord', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_subaccount = sapiPostBrokerSubAccount = Entry('broker/subAccount', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_subaccount_margin = sapiPostBrokerSubAccountMargin = Entry('broker/subAccount/margin', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_subaccount_futures = sapiPostBrokerSubAccountFutures = Entry('broker/subAccount/futures', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_subaccountapi = sapiPostBrokerSubAccountApi = Entry('broker/subAccountApi', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_subaccountapi_permission = sapiPostBrokerSubAccountApiPermission = Entry('broker/subAccountApi/permission', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_subaccountapi_commission = sapiPostBrokerSubAccountApiCommission = Entry('broker/subAccountApi/commission', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_subaccountapi_commission_futures = sapiPostBrokerSubAccountApiCommissionFutures = Entry('broker/subAccountApi/commission/futures', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_subaccountapi_commission_coinfutures = sapiPostBrokerSubAccountApiCommissionCoinFutures = Entry('broker/subAccountApi/commission/coinFutures', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_transfer = sapiPostBrokerTransfer = Entry('broker/transfer', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_transfer_futures = sapiPostBrokerTransferFutures = Entry('broker/transfer/futures', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_rebate_historicalrecord = sapiPostBrokerRebateHistoricalRecord = Entry('broker/rebate/historicalRecord', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_subaccount_bnbburn_spot = sapiPostBrokerSubAccountBnbBurnSpot = Entry('broker/subAccount/bnbBurn/spot', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_subaccount_bnbburn_margininterest = sapiPostBrokerSubAccountBnbBurnMarginInterest = Entry('broker/subAccount/bnbBurn/marginInterest', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_subaccount_blvt = sapiPostBrokerSubAccountBlvt = Entry('broker/subAccount/blvt', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_subaccountapi_iprestriction = sapiPostBrokerSubAccountApiIpRestriction = Entry('broker/subAccountApi/ipRestriction', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_subaccountapi_iprestriction_iplist = sapiPostBrokerSubAccountApiIpRestrictionIpList = Entry('broker/subAccountApi/ipRestriction/ipList', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_universaltransfer = sapiPostBrokerUniversalTransfer = Entry('broker/universalTransfer', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_subaccountapi_permission_universaltransfer = sapiPostBrokerSubAccountApiPermissionUniversalTransfer = Entry('broker/subAccountApi/permission/universalTransfer', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_subaccountapi_permission_vanillaoptions = sapiPostBrokerSubAccountApiPermissionVanillaOptions = Entry('broker/subAccountApi/permission/vanillaOptions', 'sapi', 'POST', {'cost': 1}) + sapi_post_giftcard_createcode = sapiPostGiftcardCreateCode = Entry('giftcard/createCode', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_giftcard_redeemcode = sapiPostGiftcardRedeemCode = Entry('giftcard/redeemCode', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_giftcard_buycode = sapiPostGiftcardBuyCode = Entry('giftcard/buyCode', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_algo_spot_newordertwap = sapiPostAlgoSpotNewOrderTwap = Entry('algo/spot/newOrderTwap', 'sapi', 'POST', {'cost': 20.001}) + sapi_post_algo_futures_newordervp = sapiPostAlgoFuturesNewOrderVp = Entry('algo/futures/newOrderVp', 'sapi', 'POST', {'cost': 20.001}) + sapi_post_algo_futures_newordertwap = sapiPostAlgoFuturesNewOrderTwap = Entry('algo/futures/newOrderTwap', 'sapi', 'POST', {'cost': 20.001}) + sapi_post_staking_purchase = sapiPostStakingPurchase = Entry('staking/purchase', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_staking_redeem = sapiPostStakingRedeem = Entry('staking/redeem', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_staking_setautostaking = sapiPostStakingSetAutoStaking = Entry('staking/setAutoStaking', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_eth_staking_eth_stake = sapiPostEthStakingEthStake = Entry('eth-staking/eth/stake', 'sapi', 'POST', {'cost': 15}) + sapi_post_eth_staking_eth_redeem = sapiPostEthStakingEthRedeem = Entry('eth-staking/eth/redeem', 'sapi', 'POST', {'cost': 15}) + sapi_post_eth_staking_wbeth_wrap = sapiPostEthStakingWbethWrap = Entry('eth-staking/wbeth/wrap', 'sapi', 'POST', {'cost': 15}) + sapi_post_sol_staking_sol_stake = sapiPostSolStakingSolStake = Entry('sol-staking/sol/stake', 'sapi', 'POST', {'cost': 15}) + sapi_post_sol_staking_sol_redeem = sapiPostSolStakingSolRedeem = Entry('sol-staking/sol/redeem', 'sapi', 'POST', {'cost': 15}) + sapi_post_mining_hash_transfer_config = sapiPostMiningHashTransferConfig = Entry('mining/hash-transfer/config', 'sapi', 'POST', {'cost': 0.5}) + sapi_post_mining_hash_transfer_config_cancel = sapiPostMiningHashTransferConfigCancel = Entry('mining/hash-transfer/config/cancel', 'sapi', 'POST', {'cost': 0.5}) + sapi_post_portfolio_repay = sapiPostPortfolioRepay = Entry('portfolio/repay', 'sapi', 'POST', {'cost': 20.001}) + sapi_post_loan_vip_renew = sapiPostLoanVipRenew = Entry('loan/vip/renew', 'sapi', 'POST', {'cost': 40.002}) + sapi_post_loan_vip_borrow = sapiPostLoanVipBorrow = Entry('loan/vip/borrow', 'sapi', 'POST', {'cost': 40.002}) + sapi_post_loan_borrow = sapiPostLoanBorrow = Entry('loan/borrow', 'sapi', 'POST', {'cost': 40.002}) + sapi_post_loan_repay = sapiPostLoanRepay = Entry('loan/repay', 'sapi', 'POST', {'cost': 40.002}) + sapi_post_loan_adjust_ltv = sapiPostLoanAdjustLtv = Entry('loan/adjust/ltv', 'sapi', 'POST', {'cost': 40.002}) + sapi_post_loan_customize_margin_call = sapiPostLoanCustomizeMarginCall = Entry('loan/customize/margin_call', 'sapi', 'POST', {'cost': 40.002}) + sapi_post_loan_flexible_repay = sapiPostLoanFlexibleRepay = Entry('loan/flexible/repay', 'sapi', 'POST', {'cost': 40.002}) + sapi_post_loan_flexible_adjust_ltv = sapiPostLoanFlexibleAdjustLtv = Entry('loan/flexible/adjust/ltv', 'sapi', 'POST', {'cost': 40.002}) + sapi_post_loan_vip_repay = sapiPostLoanVipRepay = Entry('loan/vip/repay', 'sapi', 'POST', {'cost': 40.002}) + sapi_post_convert_getquote = sapiPostConvertGetQuote = Entry('convert/getQuote', 'sapi', 'POST', {'cost': 1.3334}) + sapi_post_convert_acceptquote = sapiPostConvertAcceptQuote = Entry('convert/acceptQuote', 'sapi', 'POST', {'cost': 3.3335}) + sapi_post_convert_limit_placeorder = sapiPostConvertLimitPlaceOrder = Entry('convert/limit/placeOrder', 'sapi', 'POST', {'cost': 3.3335}) + sapi_post_convert_limit_cancelorder = sapiPostConvertLimitCancelOrder = Entry('convert/limit/cancelOrder', 'sapi', 'POST', {'cost': 1.3334}) + sapi_post_portfolio_auto_collection = sapiPostPortfolioAutoCollection = Entry('portfolio/auto-collection', 'sapi', 'POST', {'cost': 150}) + sapi_post_portfolio_asset_collection = sapiPostPortfolioAssetCollection = Entry('portfolio/asset-collection', 'sapi', 'POST', {'cost': 6}) + sapi_post_portfolio_bnb_transfer = sapiPostPortfolioBnbTransfer = Entry('portfolio/bnb-transfer', 'sapi', 'POST', {'cost': 150}) + sapi_post_portfolio_repay_futures_switch = sapiPostPortfolioRepayFuturesSwitch = Entry('portfolio/repay-futures-switch', 'sapi', 'POST', {'cost': 150}) + sapi_post_portfolio_repay_futures_negative_balance = sapiPostPortfolioRepayFuturesNegativeBalance = Entry('portfolio/repay-futures-negative-balance', 'sapi', 'POST', {'cost': 150}) + sapi_post_portfolio_mint = sapiPostPortfolioMint = Entry('portfolio/mint', 'sapi', 'POST', {'cost': 20}) + sapi_post_portfolio_redeem = sapiPostPortfolioRedeem = Entry('portfolio/redeem', 'sapi', 'POST', {'cost': 20}) + sapi_post_portfolio_earn_asset_transfer = sapiPostPortfolioEarnAssetTransfer = Entry('portfolio/earn-asset-transfer', 'sapi', 'POST', {'cost': 150}) + sapi_post_lending_auto_invest_plan_add = sapiPostLendingAutoInvestPlanAdd = Entry('lending/auto-invest/plan/add', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_lending_auto_invest_plan_edit = sapiPostLendingAutoInvestPlanEdit = Entry('lending/auto-invest/plan/edit', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_lending_auto_invest_plan_edit_status = sapiPostLendingAutoInvestPlanEditStatus = Entry('lending/auto-invest/plan/edit-status', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_lending_auto_invest_one_off = sapiPostLendingAutoInvestOneOff = Entry('lending/auto-invest/one-off', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_lending_auto_invest_redeem = sapiPostLendingAutoInvestRedeem = Entry('lending/auto-invest/redeem', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_simple_earn_flexible_subscribe = sapiPostSimpleEarnFlexibleSubscribe = Entry('simple-earn/flexible/subscribe', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_simple_earn_locked_subscribe = sapiPostSimpleEarnLockedSubscribe = Entry('simple-earn/locked/subscribe', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_simple_earn_flexible_redeem = sapiPostSimpleEarnFlexibleRedeem = Entry('simple-earn/flexible/redeem', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_simple_earn_locked_redeem = sapiPostSimpleEarnLockedRedeem = Entry('simple-earn/locked/redeem', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_simple_earn_flexible_setautosubscribe = sapiPostSimpleEarnFlexibleSetAutoSubscribe = Entry('simple-earn/flexible/setAutoSubscribe', 'sapi', 'POST', {'cost': 15}) + sapi_post_simple_earn_locked_setautosubscribe = sapiPostSimpleEarnLockedSetAutoSubscribe = Entry('simple-earn/locked/setAutoSubscribe', 'sapi', 'POST', {'cost': 15}) + sapi_post_simple_earn_locked_setredeemoption = sapiPostSimpleEarnLockedSetRedeemOption = Entry('simple-earn/locked/setRedeemOption', 'sapi', 'POST', {'cost': 5}) + sapi_post_dci_product_subscribe = sapiPostDciProductSubscribe = Entry('dci/product/subscribe', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_dci_product_auto_compound_edit = sapiPostDciProductAutoCompoundEdit = Entry('dci/product/auto_compound/edit', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_otc_quotes = sapiPostOtcQuotes = Entry('otc/quotes', 'sapi', 'POST', {'cost': 1}) + sapi_post_otc_orders = sapiPostOtcOrders = Entry('otc/orders', 'sapi', 'POST', {'cost': 1}) + sapi_post_fiatpayment_withdraw_apply = sapiPostFiatpaymentWithdrawApply = Entry('fiatpayment/withdraw/apply', 'sapi', 'POST', {'cost': 1}) + sapi_post_staking_stake = sapiPostStakingStake = Entry('staking/stake', 'sapi', 'POST', {'cost': 1}) + sapi_post_staking_unstake = sapiPostStakingUnstake = Entry('staking/unstake', 'sapi', 'POST', {'cost': 1}) + sapi_post_custodian_wallettransfer = sapiPostCustodianWalletTransfer = Entry('custodian/walletTransfer', 'sapi', 'POST', {'cost': 1}) + sapi_post_custodian_custodiantransfer = sapiPostCustodianCustodianTransfer = Entry('custodian/custodianTransfer', 'sapi', 'POST', {'cost': 1}) + sapi_post_custodian_undotransfer = sapiPostCustodianUndoTransfer = Entry('custodian/undoTransfer', 'sapi', 'POST', {'cost': 1}) + sapi_post_custodian_order = sapiPostCustodianOrder = Entry('custodian/order', 'sapi', 'POST', {'cost': 1}) + sapi_post_custodian_ocoorder = sapiPostCustodianOcoOrder = Entry('custodian/ocoOrder', 'sapi', 'POST', {'cost': 1}) + sapi_post_cl_transfer = sapiPostClTransfer = Entry('cl/transfer', 'sapi', 'POST', {'cost': 1}) + sapi_put_userdatastream = sapiPutUserDataStream = Entry('userDataStream', 'sapi', 'PUT', {'cost': 0.1}) + sapi_put_userdatastream_isolated = sapiPutUserDataStreamIsolated = Entry('userDataStream/isolated', 'sapi', 'PUT', {'cost': 0.1}) + sapi_delete_margin_openorders = sapiDeleteMarginOpenOrders = Entry('margin/openOrders', 'sapi', 'DELETE', {'cost': 0.1}) + sapi_delete_margin_order = sapiDeleteMarginOrder = Entry('margin/order', 'sapi', 'DELETE', {'cost': 0.006667}) + sapi_delete_margin_orderlist = sapiDeleteMarginOrderList = Entry('margin/orderList', 'sapi', 'DELETE', {'cost': 0.006667}) + sapi_delete_margin_isolated_account = sapiDeleteMarginIsolatedAccount = Entry('margin/isolated/account', 'sapi', 'DELETE', {'cost': 2.0001}) + sapi_delete_userdatastream = sapiDeleteUserDataStream = Entry('userDataStream', 'sapi', 'DELETE', {'cost': 0.1}) + sapi_delete_userdatastream_isolated = sapiDeleteUserDataStreamIsolated = Entry('userDataStream/isolated', 'sapi', 'DELETE', {'cost': 0.1}) + sapi_delete_broker_subaccountapi = sapiDeleteBrokerSubAccountApi = Entry('broker/subAccountApi', 'sapi', 'DELETE', {'cost': 1}) + sapi_delete_broker_subaccountapi_iprestriction_iplist = sapiDeleteBrokerSubAccountApiIpRestrictionIpList = Entry('broker/subAccountApi/ipRestriction/ipList', 'sapi', 'DELETE', {'cost': 1}) + sapi_delete_algo_spot_order = sapiDeleteAlgoSpotOrder = Entry('algo/spot/order', 'sapi', 'DELETE', {'cost': 0.1}) + sapi_delete_algo_futures_order = sapiDeleteAlgoFuturesOrder = Entry('algo/futures/order', 'sapi', 'DELETE', {'cost': 0.1}) + sapi_delete_sub_account_subaccountapi_iprestriction_iplist = sapiDeleteSubAccountSubAccountApiIpRestrictionIpList = Entry('sub-account/subAccountApi/ipRestriction/ipList', 'sapi', 'DELETE', {'cost': 20.001}) + sapi_delete_custodian_cancelorder = sapiDeleteCustodianCancelOrder = Entry('custodian/cancelOrder', 'sapi', 'DELETE', {'cost': 1}) + sapi_delete_custodian_cancelordersbysymbol = sapiDeleteCustodianCancelOrdersBySymbol = Entry('custodian/cancelOrdersBySymbol', 'sapi', 'DELETE', {'cost': 1}) + sapi_delete_custodian_cancelocoorder = sapiDeleteCustodianCancelOcoOrder = Entry('custodian/cancelOcoOrder', 'sapi', 'DELETE', {'cost': 1}) + sapiv2_get_eth_staking_account = sapiV2GetEthStakingAccount = Entry('eth-staking/account', 'sapiV2', 'GET', {'cost': 15}) + sapiv2_get_sub_account_futures_account = sapiV2GetSubAccountFuturesAccount = Entry('sub-account/futures/account', 'sapiV2', 'GET', {'cost': 0.1}) + sapiv2_get_sub_account_futures_accountsummary = sapiV2GetSubAccountFuturesAccountSummary = Entry('sub-account/futures/accountSummary', 'sapiV2', 'GET', {'cost': 1}) + sapiv2_get_sub_account_futures_positionrisk = sapiV2GetSubAccountFuturesPositionRisk = Entry('sub-account/futures/positionRisk', 'sapiV2', 'GET', {'cost': 0.1}) + sapiv2_get_loan_flexible_ongoing_orders = sapiV2GetLoanFlexibleOngoingOrders = Entry('loan/flexible/ongoing/orders', 'sapiV2', 'GET', {'cost': 30}) + sapiv2_get_loan_flexible_borrow_history = sapiV2GetLoanFlexibleBorrowHistory = Entry('loan/flexible/borrow/history', 'sapiV2', 'GET', {'cost': 40}) + sapiv2_get_loan_flexible_repay_history = sapiV2GetLoanFlexibleRepayHistory = Entry('loan/flexible/repay/history', 'sapiV2', 'GET', {'cost': 40}) + sapiv2_get_loan_flexible_ltv_adjustment_history = sapiV2GetLoanFlexibleLtvAdjustmentHistory = Entry('loan/flexible/ltv/adjustment/history', 'sapiV2', 'GET', {'cost': 40}) + sapiv2_get_loan_flexible_loanable_data = sapiV2GetLoanFlexibleLoanableData = Entry('loan/flexible/loanable/data', 'sapiV2', 'GET', {'cost': 40}) + sapiv2_get_loan_flexible_collateral_data = sapiV2GetLoanFlexibleCollateralData = Entry('loan/flexible/collateral/data', 'sapiV2', 'GET', {'cost': 40}) + sapiv2_get_portfolio_account = sapiV2GetPortfolioAccount = Entry('portfolio/account', 'sapiV2', 'GET', {'cost': 2}) + sapiv2_get_cl_account = sapiV2GetClAccount = Entry('cl/account', 'sapiV2', 'GET', {'cost': 10}) + sapiv2_get_cl_alerthistory = sapiV2GetClAlertHistory = Entry('cl/alertHistory', 'sapiV2', 'GET', {'cost': 1}) + sapiv2_post_eth_staking_eth_stake = sapiV2PostEthStakingEthStake = Entry('eth-staking/eth/stake', 'sapiV2', 'POST', {'cost': 15}) + sapiv2_post_sub_account_subaccountapi_iprestriction = sapiV2PostSubAccountSubAccountApiIpRestriction = Entry('sub-account/subAccountApi/ipRestriction', 'sapiV2', 'POST', {'cost': 20.001}) + sapiv2_post_loan_flexible_borrow = sapiV2PostLoanFlexibleBorrow = Entry('loan/flexible/borrow', 'sapiV2', 'POST', {'cost': 40.002}) + sapiv2_post_loan_flexible_repay = sapiV2PostLoanFlexibleRepay = Entry('loan/flexible/repay', 'sapiV2', 'POST', {'cost': 40.002}) + sapiv2_post_loan_flexible_adjust_ltv = sapiV2PostLoanFlexibleAdjustLtv = Entry('loan/flexible/adjust/ltv', 'sapiV2', 'POST', {'cost': 40.002}) + sapiv3_get_sub_account_assets = sapiV3GetSubAccountAssets = Entry('sub-account/assets', 'sapiV3', 'GET', {'cost': 1}) + sapiv3_get_accountstatus = sapiV3GetAccountStatus = Entry('accountStatus', 'sapiV3', 'GET', {'cost': 1}) + sapiv3_get_apitradingstatus = sapiV3GetApiTradingStatus = Entry('apiTradingStatus', 'sapiV3', 'GET', {'cost': 1}) + sapiv3_get_sub_account_list = sapiV3GetSubAccountList = Entry('sub-account/list', 'sapiV3', 'GET', {'cost': 1}) + sapiv3_get_sub_account_transfer_history = sapiV3GetSubAccountTransferHistory = Entry('sub-account/transfer/history', 'sapiV3', 'GET', {'cost': 1}) + sapiv3_post_asset_getuserasset = sapiV3PostAssetGetUserAsset = Entry('asset/getUserAsset', 'sapiV3', 'POST', {'cost': 0.5}) + sapiv3_post_sub_account_transfer = sapiV3PostSubAccountTransfer = Entry('sub-account/transfer', 'sapiV3', 'POST', {'cost': 1}) + sapiv4_get_sub_account_assets = sapiV4GetSubAccountAssets = Entry('sub-account/assets', 'sapiV4', 'GET', {'cost': 0.40002}) + dapipublic_get_ping = dapiPublicGetPing = Entry('ping', 'dapiPublic', 'GET', {'cost': 1}) + dapipublic_get_time = dapiPublicGetTime = Entry('time', 'dapiPublic', 'GET', {'cost': 1}) + dapipublic_get_exchangeinfo = dapiPublicGetExchangeInfo = Entry('exchangeInfo', 'dapiPublic', 'GET', {'cost': 1}) + dapipublic_get_depth = dapiPublicGetDepth = Entry('depth', 'dapiPublic', 'GET', {'cost': 2, 'byLimit': [[50, 2], [100, 5], [500, 10], [1000, 20]]}) + dapipublic_get_trades = dapiPublicGetTrades = Entry('trades', 'dapiPublic', 'GET', {'cost': 5}) + dapipublic_get_historicaltrades = dapiPublicGetHistoricalTrades = Entry('historicalTrades', 'dapiPublic', 'GET', {'cost': 20}) + dapipublic_get_aggtrades = dapiPublicGetAggTrades = Entry('aggTrades', 'dapiPublic', 'GET', {'cost': 20}) + dapipublic_get_premiumindex = dapiPublicGetPremiumIndex = Entry('premiumIndex', 'dapiPublic', 'GET', {'cost': 10}) + dapipublic_get_fundingrate = dapiPublicGetFundingRate = Entry('fundingRate', 'dapiPublic', 'GET', {'cost': 1}) + dapipublic_get_klines = dapiPublicGetKlines = Entry('klines', 'dapiPublic', 'GET', {'cost': 1, 'byLimit': [[99, 1], [499, 2], [1000, 5], [10000, 10]]}) + dapipublic_get_continuousklines = dapiPublicGetContinuousKlines = Entry('continuousKlines', 'dapiPublic', 'GET', {'cost': 1, 'byLimit': [[99, 1], [499, 2], [1000, 5], [10000, 10]]}) + dapipublic_get_indexpriceklines = dapiPublicGetIndexPriceKlines = Entry('indexPriceKlines', 'dapiPublic', 'GET', {'cost': 1, 'byLimit': [[99, 1], [499, 2], [1000, 5], [10000, 10]]}) + dapipublic_get_markpriceklines = dapiPublicGetMarkPriceKlines = Entry('markPriceKlines', 'dapiPublic', 'GET', {'cost': 1, 'byLimit': [[99, 1], [499, 2], [1000, 5], [10000, 10]]}) + dapipublic_get_premiumindexklines = dapiPublicGetPremiumIndexKlines = Entry('premiumIndexKlines', 'dapiPublic', 'GET', {'cost': 1, 'byLimit': [[99, 1], [499, 2], [1000, 5], [10000, 10]]}) + dapipublic_get_ticker_24hr = dapiPublicGetTicker24hr = Entry('ticker/24hr', 'dapiPublic', 'GET', {'cost': 1, 'noSymbol': 40}) + dapipublic_get_ticker_price = dapiPublicGetTickerPrice = Entry('ticker/price', 'dapiPublic', 'GET', {'cost': 1, 'noSymbol': 2}) + dapipublic_get_ticker_bookticker = dapiPublicGetTickerBookTicker = Entry('ticker/bookTicker', 'dapiPublic', 'GET', {'cost': 2, 'noSymbol': 5}) + dapipublic_get_constituents = dapiPublicGetConstituents = Entry('constituents', 'dapiPublic', 'GET', {'cost': 2}) + dapipublic_get_openinterest = dapiPublicGetOpenInterest = Entry('openInterest', 'dapiPublic', 'GET', {'cost': 1}) + dapipublic_get_fundinginfo = dapiPublicGetFundingInfo = Entry('fundingInfo', 'dapiPublic', 'GET', {'cost': 1}) + dapidata_get_delivery_price = dapiDataGetDeliveryPrice = Entry('delivery-price', 'dapiData', 'GET', {'cost': 1}) + dapidata_get_openinteresthist = dapiDataGetOpenInterestHist = Entry('openInterestHist', 'dapiData', 'GET', {'cost': 1}) + dapidata_get_toplongshortaccountratio = dapiDataGetTopLongShortAccountRatio = Entry('topLongShortAccountRatio', 'dapiData', 'GET', {'cost': 1}) + dapidata_get_toplongshortpositionratio = dapiDataGetTopLongShortPositionRatio = Entry('topLongShortPositionRatio', 'dapiData', 'GET', {'cost': 1}) + dapidata_get_globallongshortaccountratio = dapiDataGetGlobalLongShortAccountRatio = Entry('globalLongShortAccountRatio', 'dapiData', 'GET', {'cost': 1}) + dapidata_get_takerbuysellvol = dapiDataGetTakerBuySellVol = Entry('takerBuySellVol', 'dapiData', 'GET', {'cost': 1}) + dapidata_get_basis = dapiDataGetBasis = Entry('basis', 'dapiData', 'GET', {'cost': 1}) + dapiprivate_get_positionside_dual = dapiPrivateGetPositionSideDual = Entry('positionSide/dual', 'dapiPrivate', 'GET', {'cost': 30}) + dapiprivate_get_orderamendment = dapiPrivateGetOrderAmendment = Entry('orderAmendment', 'dapiPrivate', 'GET', {'cost': 1}) + dapiprivate_get_order = dapiPrivateGetOrder = Entry('order', 'dapiPrivate', 'GET', {'cost': 1}) + dapiprivate_get_openorder = dapiPrivateGetOpenOrder = Entry('openOrder', 'dapiPrivate', 'GET', {'cost': 1}) + dapiprivate_get_openorders = dapiPrivateGetOpenOrders = Entry('openOrders', 'dapiPrivate', 'GET', {'cost': 1, 'noSymbol': 5}) + dapiprivate_get_allorders = dapiPrivateGetAllOrders = Entry('allOrders', 'dapiPrivate', 'GET', {'cost': 20, 'noSymbol': 40}) + dapiprivate_get_balance = dapiPrivateGetBalance = Entry('balance', 'dapiPrivate', 'GET', {'cost': 1}) + dapiprivate_get_account = dapiPrivateGetAccount = Entry('account', 'dapiPrivate', 'GET', {'cost': 5}) + dapiprivate_get_positionmargin_history = dapiPrivateGetPositionMarginHistory = Entry('positionMargin/history', 'dapiPrivate', 'GET', {'cost': 1}) + dapiprivate_get_positionrisk = dapiPrivateGetPositionRisk = Entry('positionRisk', 'dapiPrivate', 'GET', {'cost': 1}) + dapiprivate_get_usertrades = dapiPrivateGetUserTrades = Entry('userTrades', 'dapiPrivate', 'GET', {'cost': 20, 'noSymbol': 40}) + dapiprivate_get_income = dapiPrivateGetIncome = Entry('income', 'dapiPrivate', 'GET', {'cost': 20}) + dapiprivate_get_leveragebracket = dapiPrivateGetLeverageBracket = Entry('leverageBracket', 'dapiPrivate', 'GET', {'cost': 1}) + dapiprivate_get_forceorders = dapiPrivateGetForceOrders = Entry('forceOrders', 'dapiPrivate', 'GET', {'cost': 20, 'noSymbol': 50}) + dapiprivate_get_adlquantile = dapiPrivateGetAdlQuantile = Entry('adlQuantile', 'dapiPrivate', 'GET', {'cost': 5}) + dapiprivate_get_commissionrate = dapiPrivateGetCommissionRate = Entry('commissionRate', 'dapiPrivate', 'GET', {'cost': 20}) + dapiprivate_get_income_asyn = dapiPrivateGetIncomeAsyn = Entry('income/asyn', 'dapiPrivate', 'GET', {'cost': 5}) + dapiprivate_get_income_asyn_id = dapiPrivateGetIncomeAsynId = Entry('income/asyn/id', 'dapiPrivate', 'GET', {'cost': 5}) + dapiprivate_get_trade_asyn = dapiPrivateGetTradeAsyn = Entry('trade/asyn', 'dapiPrivate', 'GET', {'cost': 0.5}) + dapiprivate_get_trade_asyn_id = dapiPrivateGetTradeAsynId = Entry('trade/asyn/id', 'dapiPrivate', 'GET', {'cost': 0.5}) + dapiprivate_get_order_asyn = dapiPrivateGetOrderAsyn = Entry('order/asyn', 'dapiPrivate', 'GET', {'cost': 0.5}) + dapiprivate_get_order_asyn_id = dapiPrivateGetOrderAsynId = Entry('order/asyn/id', 'dapiPrivate', 'GET', {'cost': 0.5}) + dapiprivate_get_pmexchangeinfo = dapiPrivateGetPmExchangeInfo = Entry('pmExchangeInfo', 'dapiPrivate', 'GET', {'cost': 0.5}) + dapiprivate_get_pmaccountinfo = dapiPrivateGetPmAccountInfo = Entry('pmAccountInfo', 'dapiPrivate', 'GET', {'cost': 0.5}) + dapiprivate_post_positionside_dual = dapiPrivatePostPositionSideDual = Entry('positionSide/dual', 'dapiPrivate', 'POST', {'cost': 1}) + dapiprivate_post_order = dapiPrivatePostOrder = Entry('order', 'dapiPrivate', 'POST', {'cost': 4}) + dapiprivate_post_batchorders = dapiPrivatePostBatchOrders = Entry('batchOrders', 'dapiPrivate', 'POST', {'cost': 5}) + dapiprivate_post_countdowncancelall = dapiPrivatePostCountdownCancelAll = Entry('countdownCancelAll', 'dapiPrivate', 'POST', {'cost': 10}) + dapiprivate_post_leverage = dapiPrivatePostLeverage = Entry('leverage', 'dapiPrivate', 'POST', {'cost': 1}) + dapiprivate_post_margintype = dapiPrivatePostMarginType = Entry('marginType', 'dapiPrivate', 'POST', {'cost': 1}) + dapiprivate_post_positionmargin = dapiPrivatePostPositionMargin = Entry('positionMargin', 'dapiPrivate', 'POST', {'cost': 1}) + dapiprivate_post_listenkey = dapiPrivatePostListenKey = Entry('listenKey', 'dapiPrivate', 'POST', {'cost': 1}) + dapiprivate_put_listenkey = dapiPrivatePutListenKey = Entry('listenKey', 'dapiPrivate', 'PUT', {'cost': 1}) + dapiprivate_put_order = dapiPrivatePutOrder = Entry('order', 'dapiPrivate', 'PUT', {'cost': 1}) + dapiprivate_put_batchorders = dapiPrivatePutBatchOrders = Entry('batchOrders', 'dapiPrivate', 'PUT', {'cost': 5}) + dapiprivate_delete_order = dapiPrivateDeleteOrder = Entry('order', 'dapiPrivate', 'DELETE', {'cost': 1}) + dapiprivate_delete_allopenorders = dapiPrivateDeleteAllOpenOrders = Entry('allOpenOrders', 'dapiPrivate', 'DELETE', {'cost': 1}) + dapiprivate_delete_batchorders = dapiPrivateDeleteBatchOrders = Entry('batchOrders', 'dapiPrivate', 'DELETE', {'cost': 5}) + dapiprivate_delete_listenkey = dapiPrivateDeleteListenKey = Entry('listenKey', 'dapiPrivate', 'DELETE', {'cost': 1}) + dapiprivatev2_get_leveragebracket = dapiPrivateV2GetLeverageBracket = Entry('leverageBracket', 'dapiPrivateV2', 'GET', {'cost': 1}) + fapipublic_get_ping = fapiPublicGetPing = Entry('ping', 'fapiPublic', 'GET', {'cost': 1}) + fapipublic_get_time = fapiPublicGetTime = Entry('time', 'fapiPublic', 'GET', {'cost': 1}) + fapipublic_get_exchangeinfo = fapiPublicGetExchangeInfo = Entry('exchangeInfo', 'fapiPublic', 'GET', {'cost': 1}) + fapipublic_get_depth = fapiPublicGetDepth = Entry('depth', 'fapiPublic', 'GET', {'cost': 2, 'byLimit': [[50, 2], [100, 5], [500, 10], [1000, 20]]}) + fapipublic_get_trades = fapiPublicGetTrades = Entry('trades', 'fapiPublic', 'GET', {'cost': 5}) + fapipublic_get_historicaltrades = fapiPublicGetHistoricalTrades = Entry('historicalTrades', 'fapiPublic', 'GET', {'cost': 20}) + fapipublic_get_aggtrades = fapiPublicGetAggTrades = Entry('aggTrades', 'fapiPublic', 'GET', {'cost': 20}) + fapipublic_get_klines = fapiPublicGetKlines = Entry('klines', 'fapiPublic', 'GET', {'cost': 1, 'byLimit': [[99, 1], [499, 2], [1000, 5], [10000, 10]]}) + fapipublic_get_continuousklines = fapiPublicGetContinuousKlines = Entry('continuousKlines', 'fapiPublic', 'GET', {'cost': 1, 'byLimit': [[99, 1], [499, 2], [1000, 5], [10000, 10]]}) + fapipublic_get_markpriceklines = fapiPublicGetMarkPriceKlines = Entry('markPriceKlines', 'fapiPublic', 'GET', {'cost': 1, 'byLimit': [[99, 1], [499, 2], [1000, 5], [10000, 10]]}) + fapipublic_get_indexpriceklines = fapiPublicGetIndexPriceKlines = Entry('indexPriceKlines', 'fapiPublic', 'GET', {'cost': 1, 'byLimit': [[99, 1], [499, 2], [1000, 5], [10000, 10]]}) + fapipublic_get_premiumindexklines = fapiPublicGetPremiumIndexKlines = Entry('premiumIndexKlines', 'fapiPublic', 'GET', {'cost': 1, 'byLimit': [[99, 1], [499, 2], [1000, 5], [10000, 10]]}) + fapipublic_get_fundingrate = fapiPublicGetFundingRate = Entry('fundingRate', 'fapiPublic', 'GET', {'cost': 1}) + fapipublic_get_fundinginfo = fapiPublicGetFundingInfo = Entry('fundingInfo', 'fapiPublic', 'GET', {'cost': 1}) + fapipublic_get_premiumindex = fapiPublicGetPremiumIndex = Entry('premiumIndex', 'fapiPublic', 'GET', {'cost': 1}) + fapipublic_get_ticker_24hr = fapiPublicGetTicker24hr = Entry('ticker/24hr', 'fapiPublic', 'GET', {'cost': 1, 'noSymbol': 40}) + fapipublic_get_ticker_price = fapiPublicGetTickerPrice = Entry('ticker/price', 'fapiPublic', 'GET', {'cost': 1, 'noSymbol': 2}) + fapipublic_get_ticker_bookticker = fapiPublicGetTickerBookTicker = Entry('ticker/bookTicker', 'fapiPublic', 'GET', {'cost': 1, 'noSymbol': 2}) + fapipublic_get_openinterest = fapiPublicGetOpenInterest = Entry('openInterest', 'fapiPublic', 'GET', {'cost': 1}) + fapipublic_get_indexinfo = fapiPublicGetIndexInfo = Entry('indexInfo', 'fapiPublic', 'GET', {'cost': 1}) + fapipublic_get_assetindex = fapiPublicGetAssetIndex = Entry('assetIndex', 'fapiPublic', 'GET', {'cost': 1, 'noSymbol': 10}) + fapipublic_get_constituents = fapiPublicGetConstituents = Entry('constituents', 'fapiPublic', 'GET', {'cost': 2}) + fapipublic_get_apitradingstatus = fapiPublicGetApiTradingStatus = Entry('apiTradingStatus', 'fapiPublic', 'GET', {'cost': 1, 'noSymbol': 10}) + fapipublic_get_lvtklines = fapiPublicGetLvtKlines = Entry('lvtKlines', 'fapiPublic', 'GET', {'cost': 1}) + fapipublic_get_convert_exchangeinfo = fapiPublicGetConvertExchangeInfo = Entry('convert/exchangeInfo', 'fapiPublic', 'GET', {'cost': 4}) + fapipublic_get_insurancebalance = fapiPublicGetInsuranceBalance = Entry('insuranceBalance', 'fapiPublic', 'GET', {'cost': 1}) + fapidata_get_delivery_price = fapiDataGetDeliveryPrice = Entry('delivery-price', 'fapiData', 'GET', {'cost': 1}) + fapidata_get_openinteresthist = fapiDataGetOpenInterestHist = Entry('openInterestHist', 'fapiData', 'GET', {'cost': 1}) + fapidata_get_toplongshortaccountratio = fapiDataGetTopLongShortAccountRatio = Entry('topLongShortAccountRatio', 'fapiData', 'GET', {'cost': 1}) + fapidata_get_toplongshortpositionratio = fapiDataGetTopLongShortPositionRatio = Entry('topLongShortPositionRatio', 'fapiData', 'GET', {'cost': 1}) + fapidata_get_globallongshortaccountratio = fapiDataGetGlobalLongShortAccountRatio = Entry('globalLongShortAccountRatio', 'fapiData', 'GET', {'cost': 1}) + fapidata_get_takerlongshortratio = fapiDataGetTakerlongshortRatio = Entry('takerlongshortRatio', 'fapiData', 'GET', {'cost': 1}) + fapidata_get_basis = fapiDataGetBasis = Entry('basis', 'fapiData', 'GET', {'cost': 1}) + fapiprivate_get_forceorders = fapiPrivateGetForceOrders = Entry('forceOrders', 'fapiPrivate', 'GET', {'cost': 20, 'noSymbol': 50}) + fapiprivate_get_allorders = fapiPrivateGetAllOrders = Entry('allOrders', 'fapiPrivate', 'GET', {'cost': 5}) + fapiprivate_get_openorder = fapiPrivateGetOpenOrder = Entry('openOrder', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_openorders = fapiPrivateGetOpenOrders = Entry('openOrders', 'fapiPrivate', 'GET', {'cost': 1, 'noSymbol': 40}) + fapiprivate_get_order = fapiPrivateGetOrder = Entry('order', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_account = fapiPrivateGetAccount = Entry('account', 'fapiPrivate', 'GET', {'cost': 5}) + fapiprivate_get_balance = fapiPrivateGetBalance = Entry('balance', 'fapiPrivate', 'GET', {'cost': 5}) + fapiprivate_get_leveragebracket = fapiPrivateGetLeverageBracket = Entry('leverageBracket', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_positionmargin_history = fapiPrivateGetPositionMarginHistory = Entry('positionMargin/history', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_positionrisk = fapiPrivateGetPositionRisk = Entry('positionRisk', 'fapiPrivate', 'GET', {'cost': 5}) + fapiprivate_get_positionside_dual = fapiPrivateGetPositionSideDual = Entry('positionSide/dual', 'fapiPrivate', 'GET', {'cost': 30}) + fapiprivate_get_usertrades = fapiPrivateGetUserTrades = Entry('userTrades', 'fapiPrivate', 'GET', {'cost': 5}) + fapiprivate_get_income = fapiPrivateGetIncome = Entry('income', 'fapiPrivate', 'GET', {'cost': 30}) + fapiprivate_get_commissionrate = fapiPrivateGetCommissionRate = Entry('commissionRate', 'fapiPrivate', 'GET', {'cost': 20}) + fapiprivate_get_ratelimit_order = fapiPrivateGetRateLimitOrder = Entry('rateLimit/order', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_apitradingstatus = fapiPrivateGetApiTradingStatus = Entry('apiTradingStatus', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_multiassetsmargin = fapiPrivateGetMultiAssetsMargin = Entry('multiAssetsMargin', 'fapiPrivate', 'GET', {'cost': 30}) + fapiprivate_get_apireferral_ifnewuser = fapiPrivateGetApiReferralIfNewUser = Entry('apiReferral/ifNewUser', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_apireferral_customization = fapiPrivateGetApiReferralCustomization = Entry('apiReferral/customization', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_apireferral_usercustomization = fapiPrivateGetApiReferralUserCustomization = Entry('apiReferral/userCustomization', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_apireferral_tradernum = fapiPrivateGetApiReferralTraderNum = Entry('apiReferral/traderNum', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_apireferral_overview = fapiPrivateGetApiReferralOverview = Entry('apiReferral/overview', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_apireferral_tradevol = fapiPrivateGetApiReferralTradeVol = Entry('apiReferral/tradeVol', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_apireferral_rebatevol = fapiPrivateGetApiReferralRebateVol = Entry('apiReferral/rebateVol', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_apireferral_tradersummary = fapiPrivateGetApiReferralTraderSummary = Entry('apiReferral/traderSummary', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_adlquantile = fapiPrivateGetAdlQuantile = Entry('adlQuantile', 'fapiPrivate', 'GET', {'cost': 5}) + fapiprivate_get_pmaccountinfo = fapiPrivateGetPmAccountInfo = Entry('pmAccountInfo', 'fapiPrivate', 'GET', {'cost': 5}) + fapiprivate_get_orderamendment = fapiPrivateGetOrderAmendment = Entry('orderAmendment', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_income_asyn = fapiPrivateGetIncomeAsyn = Entry('income/asyn', 'fapiPrivate', 'GET', {'cost': 1000}) + fapiprivate_get_income_asyn_id = fapiPrivateGetIncomeAsynId = Entry('income/asyn/id', 'fapiPrivate', 'GET', {'cost': 10}) + fapiprivate_get_order_asyn = fapiPrivateGetOrderAsyn = Entry('order/asyn', 'fapiPrivate', 'GET', {'cost': 1000}) + fapiprivate_get_order_asyn_id = fapiPrivateGetOrderAsynId = Entry('order/asyn/id', 'fapiPrivate', 'GET', {'cost': 10}) + fapiprivate_get_trade_asyn = fapiPrivateGetTradeAsyn = Entry('trade/asyn', 'fapiPrivate', 'GET', {'cost': 1000}) + fapiprivate_get_trade_asyn_id = fapiPrivateGetTradeAsynId = Entry('trade/asyn/id', 'fapiPrivate', 'GET', {'cost': 10}) + fapiprivate_get_feeburn = fapiPrivateGetFeeBurn = Entry('feeBurn', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_symbolconfig = fapiPrivateGetSymbolConfig = Entry('symbolConfig', 'fapiPrivate', 'GET', {'cost': 5}) + fapiprivate_get_accountconfig = fapiPrivateGetAccountConfig = Entry('accountConfig', 'fapiPrivate', 'GET', {'cost': 5}) + fapiprivate_get_convert_orderstatus = fapiPrivateGetConvertOrderStatus = Entry('convert/orderStatus', 'fapiPrivate', 'GET', {'cost': 5}) + fapiprivate_post_batchorders = fapiPrivatePostBatchOrders = Entry('batchOrders', 'fapiPrivate', 'POST', {'cost': 5}) + fapiprivate_post_positionside_dual = fapiPrivatePostPositionSideDual = Entry('positionSide/dual', 'fapiPrivate', 'POST', {'cost': 1}) + fapiprivate_post_positionmargin = fapiPrivatePostPositionMargin = Entry('positionMargin', 'fapiPrivate', 'POST', {'cost': 1}) + fapiprivate_post_margintype = fapiPrivatePostMarginType = Entry('marginType', 'fapiPrivate', 'POST', {'cost': 1}) + fapiprivate_post_order = fapiPrivatePostOrder = Entry('order', 'fapiPrivate', 'POST', {'cost': 4}) + fapiprivate_post_leverage = fapiPrivatePostLeverage = Entry('leverage', 'fapiPrivate', 'POST', {'cost': 1}) + fapiprivate_post_listenkey = fapiPrivatePostListenKey = Entry('listenKey', 'fapiPrivate', 'POST', {'cost': 1}) + fapiprivate_post_countdowncancelall = fapiPrivatePostCountdownCancelAll = Entry('countdownCancelAll', 'fapiPrivate', 'POST', {'cost': 10}) + fapiprivate_post_multiassetsmargin = fapiPrivatePostMultiAssetsMargin = Entry('multiAssetsMargin', 'fapiPrivate', 'POST', {'cost': 1}) + fapiprivate_post_apireferral_customization = fapiPrivatePostApiReferralCustomization = Entry('apiReferral/customization', 'fapiPrivate', 'POST', {'cost': 1}) + fapiprivate_post_apireferral_usercustomization = fapiPrivatePostApiReferralUserCustomization = Entry('apiReferral/userCustomization', 'fapiPrivate', 'POST', {'cost': 1}) + fapiprivate_post_feeburn = fapiPrivatePostFeeBurn = Entry('feeBurn', 'fapiPrivate', 'POST', {'cost': 1}) + fapiprivate_post_convert_getquote = fapiPrivatePostConvertGetQuote = Entry('convert/getQuote', 'fapiPrivate', 'POST', {'cost': 200}) + fapiprivate_post_convert_acceptquote = fapiPrivatePostConvertAcceptQuote = Entry('convert/acceptQuote', 'fapiPrivate', 'POST', {'cost': 20}) + fapiprivate_put_listenkey = fapiPrivatePutListenKey = Entry('listenKey', 'fapiPrivate', 'PUT', {'cost': 1}) + fapiprivate_put_order = fapiPrivatePutOrder = Entry('order', 'fapiPrivate', 'PUT', {'cost': 1}) + fapiprivate_put_batchorders = fapiPrivatePutBatchOrders = Entry('batchOrders', 'fapiPrivate', 'PUT', {'cost': 5}) + fapiprivate_delete_batchorders = fapiPrivateDeleteBatchOrders = Entry('batchOrders', 'fapiPrivate', 'DELETE', {'cost': 1}) + fapiprivate_delete_order = fapiPrivateDeleteOrder = Entry('order', 'fapiPrivate', 'DELETE', {'cost': 1}) + fapiprivate_delete_allopenorders = fapiPrivateDeleteAllOpenOrders = Entry('allOpenOrders', 'fapiPrivate', 'DELETE', {'cost': 1}) + fapiprivate_delete_listenkey = fapiPrivateDeleteListenKey = Entry('listenKey', 'fapiPrivate', 'DELETE', {'cost': 1}) + fapipublicv2_get_ticker_price = fapiPublicV2GetTickerPrice = Entry('ticker/price', 'fapiPublicV2', 'GET', {'cost': 0}) + fapiprivatev2_get_account = fapiPrivateV2GetAccount = Entry('account', 'fapiPrivateV2', 'GET', {'cost': 1}) + fapiprivatev2_get_balance = fapiPrivateV2GetBalance = Entry('balance', 'fapiPrivateV2', 'GET', {'cost': 1}) + fapiprivatev2_get_positionrisk = fapiPrivateV2GetPositionRisk = Entry('positionRisk', 'fapiPrivateV2', 'GET', {'cost': 1}) + fapiprivatev3_get_account = fapiPrivateV3GetAccount = Entry('account', 'fapiPrivateV3', 'GET', {'cost': 1}) + fapiprivatev3_get_balance = fapiPrivateV3GetBalance = Entry('balance', 'fapiPrivateV3', 'GET', {'cost': 1}) + fapiprivatev3_get_positionrisk = fapiPrivateV3GetPositionRisk = Entry('positionRisk', 'fapiPrivateV3', 'GET', {'cost': 1}) + eapipublic_get_ping = eapiPublicGetPing = Entry('ping', 'eapiPublic', 'GET', {'cost': 1}) + eapipublic_get_time = eapiPublicGetTime = Entry('time', 'eapiPublic', 'GET', {'cost': 1}) + eapipublic_get_exchangeinfo = eapiPublicGetExchangeInfo = Entry('exchangeInfo', 'eapiPublic', 'GET', {'cost': 1}) + eapipublic_get_index = eapiPublicGetIndex = Entry('index', 'eapiPublic', 'GET', {'cost': 1}) + eapipublic_get_ticker = eapiPublicGetTicker = Entry('ticker', 'eapiPublic', 'GET', {'cost': 5}) + eapipublic_get_mark = eapiPublicGetMark = Entry('mark', 'eapiPublic', 'GET', {'cost': 5}) + eapipublic_get_depth = eapiPublicGetDepth = Entry('depth', 'eapiPublic', 'GET', {'cost': 1}) + eapipublic_get_klines = eapiPublicGetKlines = Entry('klines', 'eapiPublic', 'GET', {'cost': 1}) + eapipublic_get_trades = eapiPublicGetTrades = Entry('trades', 'eapiPublic', 'GET', {'cost': 5}) + eapipublic_get_historicaltrades = eapiPublicGetHistoricalTrades = Entry('historicalTrades', 'eapiPublic', 'GET', {'cost': 20}) + eapipublic_get_exercisehistory = eapiPublicGetExerciseHistory = Entry('exerciseHistory', 'eapiPublic', 'GET', {'cost': 3}) + eapipublic_get_openinterest = eapiPublicGetOpenInterest = Entry('openInterest', 'eapiPublic', 'GET', {'cost': 3}) + eapiprivate_get_account = eapiPrivateGetAccount = Entry('account', 'eapiPrivate', 'GET', {'cost': 3}) + eapiprivate_get_position = eapiPrivateGetPosition = Entry('position', 'eapiPrivate', 'GET', {'cost': 5}) + eapiprivate_get_openorders = eapiPrivateGetOpenOrders = Entry('openOrders', 'eapiPrivate', 'GET', {'cost': 1, 'noSymbol': 40}) + eapiprivate_get_historyorders = eapiPrivateGetHistoryOrders = Entry('historyOrders', 'eapiPrivate', 'GET', {'cost': 3}) + eapiprivate_get_usertrades = eapiPrivateGetUserTrades = Entry('userTrades', 'eapiPrivate', 'GET', {'cost': 5}) + eapiprivate_get_exerciserecord = eapiPrivateGetExerciseRecord = Entry('exerciseRecord', 'eapiPrivate', 'GET', {'cost': 5}) + eapiprivate_get_bill = eapiPrivateGetBill = Entry('bill', 'eapiPrivate', 'GET', {'cost': 1}) + eapiprivate_get_income_asyn = eapiPrivateGetIncomeAsyn = Entry('income/asyn', 'eapiPrivate', 'GET', {'cost': 5}) + eapiprivate_get_income_asyn_id = eapiPrivateGetIncomeAsynId = Entry('income/asyn/id', 'eapiPrivate', 'GET', {'cost': 5}) + eapiprivate_get_marginaccount = eapiPrivateGetMarginAccount = Entry('marginAccount', 'eapiPrivate', 'GET', {'cost': 3}) + eapiprivate_get_mmp = eapiPrivateGetMmp = Entry('mmp', 'eapiPrivate', 'GET', {'cost': 1}) + eapiprivate_get_countdowncancelall = eapiPrivateGetCountdownCancelAll = Entry('countdownCancelAll', 'eapiPrivate', 'GET', {'cost': 1}) + eapiprivate_get_order = eapiPrivateGetOrder = Entry('order', 'eapiPrivate', 'GET', {'cost': 1}) + eapiprivate_get_block_order_orders = eapiPrivateGetBlockOrderOrders = Entry('block/order/orders', 'eapiPrivate', 'GET', {'cost': 5}) + eapiprivate_get_block_order_execute = eapiPrivateGetBlockOrderExecute = Entry('block/order/execute', 'eapiPrivate', 'GET', {'cost': 5}) + eapiprivate_get_block_user_trades = eapiPrivateGetBlockUserTrades = Entry('block/user-trades', 'eapiPrivate', 'GET', {'cost': 5}) + eapiprivate_get_blocktrades = eapiPrivateGetBlockTrades = Entry('blockTrades', 'eapiPrivate', 'GET', {'cost': 5}) + eapiprivate_post_order = eapiPrivatePostOrder = Entry('order', 'eapiPrivate', 'POST', {'cost': 1}) + eapiprivate_post_batchorders = eapiPrivatePostBatchOrders = Entry('batchOrders', 'eapiPrivate', 'POST', {'cost': 5}) + eapiprivate_post_listenkey = eapiPrivatePostListenKey = Entry('listenKey', 'eapiPrivate', 'POST', {'cost': 1}) + eapiprivate_post_mmpset = eapiPrivatePostMmpSet = Entry('mmpSet', 'eapiPrivate', 'POST', {'cost': 1}) + eapiprivate_post_mmpreset = eapiPrivatePostMmpReset = Entry('mmpReset', 'eapiPrivate', 'POST', {'cost': 1}) + eapiprivate_post_countdowncancelall = eapiPrivatePostCountdownCancelAll = Entry('countdownCancelAll', 'eapiPrivate', 'POST', {'cost': 1}) + eapiprivate_post_countdowncancelallheartbeat = eapiPrivatePostCountdownCancelAllHeartBeat = Entry('countdownCancelAllHeartBeat', 'eapiPrivate', 'POST', {'cost': 10}) + eapiprivate_post_block_order_create = eapiPrivatePostBlockOrderCreate = Entry('block/order/create', 'eapiPrivate', 'POST', {'cost': 5}) + eapiprivate_post_block_order_execute = eapiPrivatePostBlockOrderExecute = Entry('block/order/execute', 'eapiPrivate', 'POST', {'cost': 5}) + eapiprivate_put_listenkey = eapiPrivatePutListenKey = Entry('listenKey', 'eapiPrivate', 'PUT', {'cost': 1}) + eapiprivate_put_block_order_create = eapiPrivatePutBlockOrderCreate = Entry('block/order/create', 'eapiPrivate', 'PUT', {'cost': 5}) + eapiprivate_delete_order = eapiPrivateDeleteOrder = Entry('order', 'eapiPrivate', 'DELETE', {'cost': 1}) + eapiprivate_delete_batchorders = eapiPrivateDeleteBatchOrders = Entry('batchOrders', 'eapiPrivate', 'DELETE', {'cost': 1}) + eapiprivate_delete_allopenorders = eapiPrivateDeleteAllOpenOrders = Entry('allOpenOrders', 'eapiPrivate', 'DELETE', {'cost': 1}) + eapiprivate_delete_allopenordersbyunderlying = eapiPrivateDeleteAllOpenOrdersByUnderlying = Entry('allOpenOrdersByUnderlying', 'eapiPrivate', 'DELETE', {'cost': 1}) + eapiprivate_delete_listenkey = eapiPrivateDeleteListenKey = Entry('listenKey', 'eapiPrivate', 'DELETE', {'cost': 1}) + eapiprivate_delete_block_order_create = eapiPrivateDeleteBlockOrderCreate = Entry('block/order/create', 'eapiPrivate', 'DELETE', {'cost': 5}) + public_get_ping = publicGetPing = Entry('ping', 'public', 'GET', {'cost': 1}) + public_get_time = publicGetTime = Entry('time', 'public', 'GET', {'cost': 1}) + public_get_depth = publicGetDepth = Entry('depth', 'public', 'GET', {'cost': 1, 'byLimit': [[100, 1], [500, 5], [1000, 10], [5000, 50]]}) + public_get_trades = publicGetTrades = Entry('trades', 'public', 'GET', {'cost': 1}) + public_get_aggtrades = publicGetAggTrades = Entry('aggTrades', 'public', 'GET', {'cost': 1}) + public_get_historicaltrades = publicGetHistoricalTrades = Entry('historicalTrades', 'public', 'GET', {'cost': 5}) + public_get_klines = publicGetKlines = Entry('klines', 'public', 'GET', {'cost': 1}) + public_get_uiklines = publicGetUiKlines = Entry('uiKlines', 'public', 'GET', {'cost': 0.4}) + public_get_ticker_24hr = publicGetTicker24hr = Entry('ticker/24hr', 'public', 'GET', {'cost': 1, 'noSymbol': 40}) + public_get_ticker = publicGetTicker = Entry('ticker', 'public', 'GET', {'cost': 2, 'noSymbol': 100}) + public_get_ticker_tradingday = publicGetTickerTradingDay = Entry('ticker/tradingDay', 'public', 'GET', {'cost': 0.8}) + public_get_ticker_price = publicGetTickerPrice = Entry('ticker/price', 'public', 'GET', {'cost': 1, 'noSymbol': 2}) + public_get_ticker_bookticker = publicGetTickerBookTicker = Entry('ticker/bookTicker', 'public', 'GET', {'cost': 1, 'noSymbol': 2}) + public_get_exchangeinfo = publicGetExchangeInfo = Entry('exchangeInfo', 'public', 'GET', {'cost': 10}) + public_get_avgprice = publicGetAvgPrice = Entry('avgPrice', 'public', 'GET', {'cost': 1}) + public_put_userdatastream = publicPutUserDataStream = Entry('userDataStream', 'public', 'PUT', {'cost': 0.4}) + public_post_userdatastream = publicPostUserDataStream = Entry('userDataStream', 'public', 'POST', {'cost': 0.4}) + public_delete_userdatastream = publicDeleteUserDataStream = Entry('userDataStream', 'public', 'DELETE', {'cost': 0.4}) + private_get_allorderlist = privateGetAllOrderList = Entry('allOrderList', 'private', 'GET', {'cost': 10}) + private_get_openorderlist = privateGetOpenOrderList = Entry('openOrderList', 'private', 'GET', {'cost': 3}) + private_get_orderlist = privateGetOrderList = Entry('orderList', 'private', 'GET', {'cost': 2}) + private_get_order = privateGetOrder = Entry('order', 'private', 'GET', {'cost': 2}) + private_get_openorders = privateGetOpenOrders = Entry('openOrders', 'private', 'GET', {'cost': 3, 'noSymbol': 40}) + private_get_allorders = privateGetAllOrders = Entry('allOrders', 'private', 'GET', {'cost': 10}) + private_get_account = privateGetAccount = Entry('account', 'private', 'GET', {'cost': 10}) + private_get_mytrades = privateGetMyTrades = Entry('myTrades', 'private', 'GET', {'cost': 10}) + private_get_ratelimit_order = privateGetRateLimitOrder = Entry('rateLimit/order', 'private', 'GET', {'cost': 20}) + private_get_mypreventedmatches = privateGetMyPreventedMatches = Entry('myPreventedMatches', 'private', 'GET', {'cost': 10}) + private_get_myallocations = privateGetMyAllocations = Entry('myAllocations', 'private', 'GET', {'cost': 4}) + private_get_account_commission = privateGetAccountCommission = Entry('account/commission', 'private', 'GET', {'cost': 4}) + private_post_order_oco = privatePostOrderOco = Entry('order/oco', 'private', 'POST', {'cost': 1}) + private_post_orderlist_oco = privatePostOrderListOco = Entry('orderList/oco', 'private', 'POST', {'cost': 0.2}) + private_post_orderlist_oto = privatePostOrderListOto = Entry('orderList/oto', 'private', 'POST', {'cost': 0.2}) + private_post_orderlist_otoco = privatePostOrderListOtoco = Entry('orderList/otoco', 'private', 'POST', {'cost': 0.2}) + private_post_sor_order = privatePostSorOrder = Entry('sor/order', 'private', 'POST', {'cost': 0.2}) + private_post_sor_order_test = privatePostSorOrderTest = Entry('sor/order/test', 'private', 'POST', {'cost': 0.2}) + private_post_order = privatePostOrder = Entry('order', 'private', 'POST', {'cost': 1}) + private_post_order_cancelreplace = privatePostOrderCancelReplace = Entry('order/cancelReplace', 'private', 'POST', {'cost': 1}) + private_post_order_test = privatePostOrderTest = Entry('order/test', 'private', 'POST', {'cost': 1}) + private_delete_openorders = privateDeleteOpenOrders = Entry('openOrders', 'private', 'DELETE', {'cost': 1}) + private_delete_orderlist = privateDeleteOrderList = Entry('orderList', 'private', 'DELETE', {'cost': 1}) + private_delete_order = privateDeleteOrder = Entry('order', 'private', 'DELETE', {'cost': 1}) + papi_get_ping = papiGetPing = Entry('ping', 'papi', 'GET', {'cost': 0.2}) + papi_get_um_order = papiGetUmOrder = Entry('um/order', 'papi', 'GET', {'cost': 1}) + papi_get_um_openorder = papiGetUmOpenOrder = Entry('um/openOrder', 'papi', 'GET', {'cost': 1}) + papi_get_um_openorders = papiGetUmOpenOrders = Entry('um/openOrders', 'papi', 'GET', {'cost': 1, 'noSymbol': 40}) + papi_get_um_allorders = papiGetUmAllOrders = Entry('um/allOrders', 'papi', 'GET', {'cost': 5}) + papi_get_cm_order = papiGetCmOrder = Entry('cm/order', 'papi', 'GET', {'cost': 1}) + papi_get_cm_openorder = papiGetCmOpenOrder = Entry('cm/openOrder', 'papi', 'GET', {'cost': 1}) + papi_get_cm_openorders = papiGetCmOpenOrders = Entry('cm/openOrders', 'papi', 'GET', {'cost': 1, 'noSymbol': 40}) + papi_get_cm_allorders = papiGetCmAllOrders = Entry('cm/allOrders', 'papi', 'GET', {'cost': 20}) + papi_get_um_conditional_openorder = papiGetUmConditionalOpenOrder = Entry('um/conditional/openOrder', 'papi', 'GET', {'cost': 1}) + papi_get_um_conditional_openorders = papiGetUmConditionalOpenOrders = Entry('um/conditional/openOrders', 'papi', 'GET', {'cost': 1, 'noSymbol': 40}) + papi_get_um_conditional_orderhistory = papiGetUmConditionalOrderHistory = Entry('um/conditional/orderHistory', 'papi', 'GET', {'cost': 1}) + papi_get_um_conditional_allorders = papiGetUmConditionalAllOrders = Entry('um/conditional/allOrders', 'papi', 'GET', {'cost': 1, 'noSymbol': 40}) + papi_get_cm_conditional_openorder = papiGetCmConditionalOpenOrder = Entry('cm/conditional/openOrder', 'papi', 'GET', {'cost': 1}) + papi_get_cm_conditional_openorders = papiGetCmConditionalOpenOrders = Entry('cm/conditional/openOrders', 'papi', 'GET', {'cost': 1, 'noSymbol': 40}) + papi_get_cm_conditional_orderhistory = papiGetCmConditionalOrderHistory = Entry('cm/conditional/orderHistory', 'papi', 'GET', {'cost': 1}) + papi_get_cm_conditional_allorders = papiGetCmConditionalAllOrders = Entry('cm/conditional/allOrders', 'papi', 'GET', {'cost': 40}) + papi_get_margin_order = papiGetMarginOrder = Entry('margin/order', 'papi', 'GET', {'cost': 10}) + papi_get_margin_openorders = papiGetMarginOpenOrders = Entry('margin/openOrders', 'papi', 'GET', {'cost': 5}) + papi_get_margin_allorders = papiGetMarginAllOrders = Entry('margin/allOrders', 'papi', 'GET', {'cost': 100}) + papi_get_margin_orderlist = papiGetMarginOrderList = Entry('margin/orderList', 'papi', 'GET', {'cost': 5}) + papi_get_margin_allorderlist = papiGetMarginAllOrderList = Entry('margin/allOrderList', 'papi', 'GET', {'cost': 100}) + papi_get_margin_openorderlist = papiGetMarginOpenOrderList = Entry('margin/openOrderList', 'papi', 'GET', {'cost': 5}) + papi_get_margin_mytrades = papiGetMarginMyTrades = Entry('margin/myTrades', 'papi', 'GET', {'cost': 5}) + papi_get_balance = papiGetBalance = Entry('balance', 'papi', 'GET', {'cost': 4}) + papi_get_account = papiGetAccount = Entry('account', 'papi', 'GET', {'cost': 4}) + papi_get_margin_maxborrowable = papiGetMarginMaxBorrowable = Entry('margin/maxBorrowable', 'papi', 'GET', {'cost': 1}) + papi_get_margin_maxwithdraw = papiGetMarginMaxWithdraw = Entry('margin/maxWithdraw', 'papi', 'GET', {'cost': 1}) + papi_get_um_positionrisk = papiGetUmPositionRisk = Entry('um/positionRisk', 'papi', 'GET', {'cost': 1}) + papi_get_cm_positionrisk = papiGetCmPositionRisk = Entry('cm/positionRisk', 'papi', 'GET', {'cost': 0.2}) + papi_get_um_positionside_dual = papiGetUmPositionSideDual = Entry('um/positionSide/dual', 'papi', 'GET', {'cost': 6}) + papi_get_cm_positionside_dual = papiGetCmPositionSideDual = Entry('cm/positionSide/dual', 'papi', 'GET', {'cost': 6}) + papi_get_um_usertrades = papiGetUmUserTrades = Entry('um/userTrades', 'papi', 'GET', {'cost': 5}) + papi_get_cm_usertrades = papiGetCmUserTrades = Entry('cm/userTrades', 'papi', 'GET', {'cost': 20}) + papi_get_um_leveragebracket = papiGetUmLeverageBracket = Entry('um/leverageBracket', 'papi', 'GET', {'cost': 0.2}) + papi_get_cm_leveragebracket = papiGetCmLeverageBracket = Entry('cm/leverageBracket', 'papi', 'GET', {'cost': 0.2}) + papi_get_margin_forceorders = papiGetMarginForceOrders = Entry('margin/forceOrders', 'papi', 'GET', {'cost': 1}) + papi_get_um_forceorders = papiGetUmForceOrders = Entry('um/forceOrders', 'papi', 'GET', {'cost': 20, 'noSymbol': 50}) + papi_get_cm_forceorders = papiGetCmForceOrders = Entry('cm/forceOrders', 'papi', 'GET', {'cost': 20, 'noSymbol': 50}) + papi_get_um_apitradingstatus = papiGetUmApiTradingStatus = Entry('um/apiTradingStatus', 'papi', 'GET', {'cost': 0.2, 'noSymbol': 2}) + papi_get_um_commissionrate = papiGetUmCommissionRate = Entry('um/commissionRate', 'papi', 'GET', {'cost': 4}) + papi_get_cm_commissionrate = papiGetCmCommissionRate = Entry('cm/commissionRate', 'papi', 'GET', {'cost': 4}) + papi_get_margin_marginloan = papiGetMarginMarginLoan = Entry('margin/marginLoan', 'papi', 'GET', {'cost': 2}) + papi_get_margin_repayloan = papiGetMarginRepayLoan = Entry('margin/repayLoan', 'papi', 'GET', {'cost': 2}) + papi_get_margin_margininteresthistory = papiGetMarginMarginInterestHistory = Entry('margin/marginInterestHistory', 'papi', 'GET', {'cost': 0.2}) + papi_get_portfolio_interest_history = papiGetPortfolioInterestHistory = Entry('portfolio/interest-history', 'papi', 'GET', {'cost': 10}) + papi_get_um_income = papiGetUmIncome = Entry('um/income', 'papi', 'GET', {'cost': 6}) + papi_get_cm_income = papiGetCmIncome = Entry('cm/income', 'papi', 'GET', {'cost': 6}) + papi_get_um_account = papiGetUmAccount = Entry('um/account', 'papi', 'GET', {'cost': 1}) + papi_get_cm_account = papiGetCmAccount = Entry('cm/account', 'papi', 'GET', {'cost': 1}) + papi_get_repay_futures_switch = papiGetRepayFuturesSwitch = Entry('repay-futures-switch', 'papi', 'GET', {'cost': 6}) + papi_get_um_adlquantile = papiGetUmAdlQuantile = Entry('um/adlQuantile', 'papi', 'GET', {'cost': 5}) + papi_get_cm_adlquantile = papiGetCmAdlQuantile = Entry('cm/adlQuantile', 'papi', 'GET', {'cost': 5}) + papi_get_um_trade_asyn = papiGetUmTradeAsyn = Entry('um/trade/asyn', 'papi', 'GET', {'cost': 300}) + papi_get_um_trade_asyn_id = papiGetUmTradeAsynId = Entry('um/trade/asyn/id', 'papi', 'GET', {'cost': 2}) + papi_get_um_order_asyn = papiGetUmOrderAsyn = Entry('um/order/asyn', 'papi', 'GET', {'cost': 300}) + papi_get_um_order_asyn_id = papiGetUmOrderAsynId = Entry('um/order/asyn/id', 'papi', 'GET', {'cost': 2}) + papi_get_um_income_asyn = papiGetUmIncomeAsyn = Entry('um/income/asyn', 'papi', 'GET', {'cost': 300}) + papi_get_um_income_asyn_id = papiGetUmIncomeAsynId = Entry('um/income/asyn/id', 'papi', 'GET', {'cost': 2}) + papi_get_um_orderamendment = papiGetUmOrderAmendment = Entry('um/orderAmendment', 'papi', 'GET', {'cost': 1}) + papi_get_cm_orderamendment = papiGetCmOrderAmendment = Entry('cm/orderAmendment', 'papi', 'GET', {'cost': 1}) + papi_get_um_feeburn = papiGetUmFeeBurn = Entry('um/feeBurn', 'papi', 'GET', {'cost': 30}) + papi_get_um_accountconfig = papiGetUmAccountConfig = Entry('um/accountConfig', 'papi', 'GET', {'cost': 1}) + papi_get_um_symbolconfig = papiGetUmSymbolConfig = Entry('um/symbolConfig', 'papi', 'GET', {'cost': 1}) + papi_get_cm_accountconfig = papiGetCmAccountConfig = Entry('cm/accountConfig', 'papi', 'GET', {'cost': 1}) + papi_get_cm_symbolconfig = papiGetCmSymbolConfig = Entry('cm/symbolConfig', 'papi', 'GET', {'cost': 1}) + papi_get_ratelimit_order = papiGetRateLimitOrder = Entry('rateLimit/order', 'papi', 'GET', {'cost': 1}) + papi_post_um_order = papiPostUmOrder = Entry('um/order', 'papi', 'POST', {'cost': 1}) + papi_post_um_conditional_order = papiPostUmConditionalOrder = Entry('um/conditional/order', 'papi', 'POST', {'cost': 1}) + papi_post_cm_order = papiPostCmOrder = Entry('cm/order', 'papi', 'POST', {'cost': 1}) + papi_post_cm_conditional_order = papiPostCmConditionalOrder = Entry('cm/conditional/order', 'papi', 'POST', {'cost': 1}) + papi_post_margin_order = papiPostMarginOrder = Entry('margin/order', 'papi', 'POST', {'cost': 1}) + papi_post_marginloan = papiPostMarginLoan = Entry('marginLoan', 'papi', 'POST', {'cost': 100}) + papi_post_repayloan = papiPostRepayLoan = Entry('repayLoan', 'papi', 'POST', {'cost': 100}) + papi_post_margin_order_oco = papiPostMarginOrderOco = Entry('margin/order/oco', 'papi', 'POST', {'cost': 1}) + papi_post_um_leverage = papiPostUmLeverage = Entry('um/leverage', 'papi', 'POST', {'cost': 0.2}) + papi_post_cm_leverage = papiPostCmLeverage = Entry('cm/leverage', 'papi', 'POST', {'cost': 0.2}) + papi_post_um_positionside_dual = papiPostUmPositionSideDual = Entry('um/positionSide/dual', 'papi', 'POST', {'cost': 0.2}) + papi_post_cm_positionside_dual = papiPostCmPositionSideDual = Entry('cm/positionSide/dual', 'papi', 'POST', {'cost': 0.2}) + papi_post_auto_collection = papiPostAutoCollection = Entry('auto-collection', 'papi', 'POST', {'cost': 150}) + papi_post_bnb_transfer = papiPostBnbTransfer = Entry('bnb-transfer', 'papi', 'POST', {'cost': 150}) + papi_post_repay_futures_switch = papiPostRepayFuturesSwitch = Entry('repay-futures-switch', 'papi', 'POST', {'cost': 150}) + papi_post_repay_futures_negative_balance = papiPostRepayFuturesNegativeBalance = Entry('repay-futures-negative-balance', 'papi', 'POST', {'cost': 150}) + papi_post_listenkey = papiPostListenKey = Entry('listenKey', 'papi', 'POST', {'cost': 0.2}) + papi_post_asset_collection = papiPostAssetCollection = Entry('asset-collection', 'papi', 'POST', {'cost': 6}) + papi_post_margin_repay_debt = papiPostMarginRepayDebt = Entry('margin/repay-debt', 'papi', 'POST', {'cost': 3000}) + papi_post_um_feeburn = papiPostUmFeeBurn = Entry('um/feeBurn', 'papi', 'POST', {'cost': 1}) + papi_put_listenkey = papiPutListenKey = Entry('listenKey', 'papi', 'PUT', {'cost': 0.2}) + papi_put_um_order = papiPutUmOrder = Entry('um/order', 'papi', 'PUT', {'cost': 1}) + papi_put_cm_order = papiPutCmOrder = Entry('cm/order', 'papi', 'PUT', {'cost': 1}) + papi_delete_um_order = papiDeleteUmOrder = Entry('um/order', 'papi', 'DELETE', {'cost': 1}) + papi_delete_um_conditional_order = papiDeleteUmConditionalOrder = Entry('um/conditional/order', 'papi', 'DELETE', {'cost': 1}) + papi_delete_um_allopenorders = papiDeleteUmAllOpenOrders = Entry('um/allOpenOrders', 'papi', 'DELETE', {'cost': 1}) + papi_delete_um_conditional_allopenorders = papiDeleteUmConditionalAllOpenOrders = Entry('um/conditional/allOpenOrders', 'papi', 'DELETE', {'cost': 1}) + papi_delete_cm_order = papiDeleteCmOrder = Entry('cm/order', 'papi', 'DELETE', {'cost': 1}) + papi_delete_cm_conditional_order = papiDeleteCmConditionalOrder = Entry('cm/conditional/order', 'papi', 'DELETE', {'cost': 1}) + papi_delete_cm_allopenorders = papiDeleteCmAllOpenOrders = Entry('cm/allOpenOrders', 'papi', 'DELETE', {'cost': 1}) + papi_delete_cm_conditional_allopenorders = papiDeleteCmConditionalAllOpenOrders = Entry('cm/conditional/allOpenOrders', 'papi', 'DELETE', {'cost': 1}) + papi_delete_margin_order = papiDeleteMarginOrder = Entry('margin/order', 'papi', 'DELETE', {'cost': 2}) + papi_delete_margin_allopenorders = papiDeleteMarginAllOpenOrders = Entry('margin/allOpenOrders', 'papi', 'DELETE', {'cost': 5}) + papi_delete_margin_orderlist = papiDeleteMarginOrderList = Entry('margin/orderList', 'papi', 'DELETE', {'cost': 2}) + papi_delete_listenkey = papiDeleteListenKey = Entry('listenKey', 'papi', 'DELETE', {'cost': 0.2}) + papiv2_get_um_account = papiV2GetUmAccount = Entry('um/account', 'papiV2', 'GET', {'cost': 1}) diff --git a/ccxt/abstract/binanceusdm.py b/ccxt/abstract/binanceusdm.py new file mode 100644 index 0000000..7893621 --- /dev/null +++ b/ccxt/abstract/binanceusdm.py @@ -0,0 +1,771 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + sapi_get_copytrading_futures_userstatus = sapiGetCopyTradingFuturesUserStatus = Entry('copyTrading/futures/userStatus', 'sapi', 'GET', {'cost': 2}) + sapi_get_copytrading_futures_leadsymbol = sapiGetCopyTradingFuturesLeadSymbol = Entry('copyTrading/futures/leadSymbol', 'sapi', 'GET', {'cost': 2}) + sapi_get_system_status = sapiGetSystemStatus = Entry('system/status', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_accountsnapshot = sapiGetAccountSnapshot = Entry('accountSnapshot', 'sapi', 'GET', {'cost': 240}) + sapi_get_account_info = sapiGetAccountInfo = Entry('account/info', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_margin_asset = sapiGetMarginAsset = Entry('margin/asset', 'sapi', 'GET', {'cost': 1}) + sapi_get_margin_pair = sapiGetMarginPair = Entry('margin/pair', 'sapi', 'GET', {'cost': 1}) + sapi_get_margin_allassets = sapiGetMarginAllAssets = Entry('margin/allAssets', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_margin_allpairs = sapiGetMarginAllPairs = Entry('margin/allPairs', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_margin_priceindex = sapiGetMarginPriceIndex = Entry('margin/priceIndex', 'sapi', 'GET', {'cost': 1}) + sapi_get_spot_delist_schedule = sapiGetSpotDelistSchedule = Entry('spot/delist-schedule', 'sapi', 'GET', {'cost': 10}) + sapi_get_asset_assetdividend = sapiGetAssetAssetDividend = Entry('asset/assetDividend', 'sapi', 'GET', {'cost': 1}) + sapi_get_asset_dribblet = sapiGetAssetDribblet = Entry('asset/dribblet', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_asset_transfer = sapiGetAssetTransfer = Entry('asset/transfer', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_asset_assetdetail = sapiGetAssetAssetDetail = Entry('asset/assetDetail', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_asset_tradefee = sapiGetAssetTradeFee = Entry('asset/tradeFee', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_asset_ledger_transfer_cloud_mining_querybypage = sapiGetAssetLedgerTransferCloudMiningQueryByPage = Entry('asset/ledger-transfer/cloud-mining/queryByPage', 'sapi', 'GET', {'cost': 4.0002}) + sapi_get_asset_convert_transfer_querybypage = sapiGetAssetConvertTransferQueryByPage = Entry('asset/convert-transfer/queryByPage', 'sapi', 'GET', {'cost': 0.033335}) + sapi_get_asset_wallet_balance = sapiGetAssetWalletBalance = Entry('asset/wallet/balance', 'sapi', 'GET', {'cost': 6}) + sapi_get_asset_custody_transfer_history = sapiGetAssetCustodyTransferHistory = Entry('asset/custody/transfer-history', 'sapi', 'GET', {'cost': 6}) + sapi_get_margin_borrow_repay = sapiGetMarginBorrowRepay = Entry('margin/borrow-repay', 'sapi', 'GET', {'cost': 1}) + sapi_get_margin_loan = sapiGetMarginLoan = Entry('margin/loan', 'sapi', 'GET', {'cost': 1}) + sapi_get_margin_repay = sapiGetMarginRepay = Entry('margin/repay', 'sapi', 'GET', {'cost': 1}) + sapi_get_margin_account = sapiGetMarginAccount = Entry('margin/account', 'sapi', 'GET', {'cost': 1}) + sapi_get_margin_transfer = sapiGetMarginTransfer = Entry('margin/transfer', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_margin_interesthistory = sapiGetMarginInterestHistory = Entry('margin/interestHistory', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_margin_forceliquidationrec = sapiGetMarginForceLiquidationRec = Entry('margin/forceLiquidationRec', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_margin_order = sapiGetMarginOrder = Entry('margin/order', 'sapi', 'GET', {'cost': 1}) + sapi_get_margin_openorders = sapiGetMarginOpenOrders = Entry('margin/openOrders', 'sapi', 'GET', {'cost': 1}) + sapi_get_margin_allorders = sapiGetMarginAllOrders = Entry('margin/allOrders', 'sapi', 'GET', {'cost': 20}) + sapi_get_margin_mytrades = sapiGetMarginMyTrades = Entry('margin/myTrades', 'sapi', 'GET', {'cost': 1}) + sapi_get_margin_maxborrowable = sapiGetMarginMaxBorrowable = Entry('margin/maxBorrowable', 'sapi', 'GET', {'cost': 5}) + sapi_get_margin_maxtransferable = sapiGetMarginMaxTransferable = Entry('margin/maxTransferable', 'sapi', 'GET', {'cost': 5}) + sapi_get_margin_tradecoeff = sapiGetMarginTradeCoeff = Entry('margin/tradeCoeff', 'sapi', 'GET', {'cost': 1}) + sapi_get_margin_isolated_transfer = sapiGetMarginIsolatedTransfer = Entry('margin/isolated/transfer', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_margin_isolated_account = sapiGetMarginIsolatedAccount = Entry('margin/isolated/account', 'sapi', 'GET', {'cost': 1}) + sapi_get_margin_isolated_pair = sapiGetMarginIsolatedPair = Entry('margin/isolated/pair', 'sapi', 'GET', {'cost': 1}) + sapi_get_margin_isolated_allpairs = sapiGetMarginIsolatedAllPairs = Entry('margin/isolated/allPairs', 'sapi', 'GET', {'cost': 1}) + sapi_get_margin_isolated_accountlimit = sapiGetMarginIsolatedAccountLimit = Entry('margin/isolated/accountLimit', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_margin_interestratehistory = sapiGetMarginInterestRateHistory = Entry('margin/interestRateHistory', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_margin_orderlist = sapiGetMarginOrderList = Entry('margin/orderList', 'sapi', 'GET', {'cost': 1}) + sapi_get_margin_allorderlist = sapiGetMarginAllOrderList = Entry('margin/allOrderList', 'sapi', 'GET', {'cost': 20}) + sapi_get_margin_openorderlist = sapiGetMarginOpenOrderList = Entry('margin/openOrderList', 'sapi', 'GET', {'cost': 1}) + sapi_get_margin_crossmargindata = sapiGetMarginCrossMarginData = Entry('margin/crossMarginData', 'sapi', 'GET', {'cost': 0.1, 'noCoin': 0.5}) + sapi_get_margin_isolatedmargindata = sapiGetMarginIsolatedMarginData = Entry('margin/isolatedMarginData', 'sapi', 'GET', {'cost': 0.1, 'noCoin': 1}) + sapi_get_margin_isolatedmargintier = sapiGetMarginIsolatedMarginTier = Entry('margin/isolatedMarginTier', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_margin_ratelimit_order = sapiGetMarginRateLimitOrder = Entry('margin/rateLimit/order', 'sapi', 'GET', {'cost': 2}) + sapi_get_margin_dribblet = sapiGetMarginDribblet = Entry('margin/dribblet', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_margin_dust = sapiGetMarginDust = Entry('margin/dust', 'sapi', 'GET', {'cost': 20.001}) + sapi_get_margin_crossmargincollateralratio = sapiGetMarginCrossMarginCollateralRatio = Entry('margin/crossMarginCollateralRatio', 'sapi', 'GET', {'cost': 10}) + sapi_get_margin_exchange_small_liability = sapiGetMarginExchangeSmallLiability = Entry('margin/exchange-small-liability', 'sapi', 'GET', {'cost': 0.6667}) + sapi_get_margin_exchange_small_liability_history = sapiGetMarginExchangeSmallLiabilityHistory = Entry('margin/exchange-small-liability-history', 'sapi', 'GET', {'cost': 0.6667}) + sapi_get_margin_next_hourly_interest_rate = sapiGetMarginNextHourlyInterestRate = Entry('margin/next-hourly-interest-rate', 'sapi', 'GET', {'cost': 0.6667}) + sapi_get_margin_capital_flow = sapiGetMarginCapitalFlow = Entry('margin/capital-flow', 'sapi', 'GET', {'cost': 10}) + sapi_get_margin_delist_schedule = sapiGetMarginDelistSchedule = Entry('margin/delist-schedule', 'sapi', 'GET', {'cost': 10}) + sapi_get_margin_available_inventory = sapiGetMarginAvailableInventory = Entry('margin/available-inventory', 'sapi', 'GET', {'cost': 0.3334}) + sapi_get_margin_leveragebracket = sapiGetMarginLeverageBracket = Entry('margin/leverageBracket', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_loan_vip_loanable_data = sapiGetLoanVipLoanableData = Entry('loan/vip/loanable/data', 'sapi', 'GET', {'cost': 40}) + sapi_get_loan_vip_collateral_data = sapiGetLoanVipCollateralData = Entry('loan/vip/collateral/data', 'sapi', 'GET', {'cost': 40}) + sapi_get_loan_vip_request_data = sapiGetLoanVipRequestData = Entry('loan/vip/request/data', 'sapi', 'GET', {'cost': 2.6668}) + sapi_get_loan_vip_request_interestrate = sapiGetLoanVipRequestInterestRate = Entry('loan/vip/request/interestRate', 'sapi', 'GET', {'cost': 2.6668}) + sapi_get_loan_income = sapiGetLoanIncome = Entry('loan/income', 'sapi', 'GET', {'cost': 40.002}) + sapi_get_loan_ongoing_orders = sapiGetLoanOngoingOrders = Entry('loan/ongoing/orders', 'sapi', 'GET', {'cost': 40}) + sapi_get_loan_ltv_adjustment_history = sapiGetLoanLtvAdjustmentHistory = Entry('loan/ltv/adjustment/history', 'sapi', 'GET', {'cost': 40}) + sapi_get_loan_borrow_history = sapiGetLoanBorrowHistory = Entry('loan/borrow/history', 'sapi', 'GET', {'cost': 40}) + sapi_get_loan_repay_history = sapiGetLoanRepayHistory = Entry('loan/repay/history', 'sapi', 'GET', {'cost': 40}) + sapi_get_loan_loanable_data = sapiGetLoanLoanableData = Entry('loan/loanable/data', 'sapi', 'GET', {'cost': 40}) + sapi_get_loan_collateral_data = sapiGetLoanCollateralData = Entry('loan/collateral/data', 'sapi', 'GET', {'cost': 40}) + sapi_get_loan_repay_collateral_rate = sapiGetLoanRepayCollateralRate = Entry('loan/repay/collateral/rate', 'sapi', 'GET', {'cost': 600}) + sapi_get_loan_flexible_ongoing_orders = sapiGetLoanFlexibleOngoingOrders = Entry('loan/flexible/ongoing/orders', 'sapi', 'GET', {'cost': 30}) + sapi_get_loan_flexible_borrow_history = sapiGetLoanFlexibleBorrowHistory = Entry('loan/flexible/borrow/history', 'sapi', 'GET', {'cost': 40}) + sapi_get_loan_flexible_repay_history = sapiGetLoanFlexibleRepayHistory = Entry('loan/flexible/repay/history', 'sapi', 'GET', {'cost': 40}) + sapi_get_loan_flexible_ltv_adjustment_history = sapiGetLoanFlexibleLtvAdjustmentHistory = Entry('loan/flexible/ltv/adjustment/history', 'sapi', 'GET', {'cost': 40}) + sapi_get_loan_vip_ongoing_orders = sapiGetLoanVipOngoingOrders = Entry('loan/vip/ongoing/orders', 'sapi', 'GET', {'cost': 40}) + sapi_get_loan_vip_repay_history = sapiGetLoanVipRepayHistory = Entry('loan/vip/repay/history', 'sapi', 'GET', {'cost': 40}) + sapi_get_loan_vip_collateral_account = sapiGetLoanVipCollateralAccount = Entry('loan/vip/collateral/account', 'sapi', 'GET', {'cost': 600}) + sapi_get_fiat_orders = sapiGetFiatOrders = Entry('fiat/orders', 'sapi', 'GET', {'cost': 600.03}) + sapi_get_fiat_payments = sapiGetFiatPayments = Entry('fiat/payments', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_futures_transfer = sapiGetFuturesTransfer = Entry('futures/transfer', 'sapi', 'GET', {'cost': 1}) + sapi_get_futures_histdatalink = sapiGetFuturesHistDataLink = Entry('futures/histDataLink', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_rebate_taxquery = sapiGetRebateTaxQuery = Entry('rebate/taxQuery', 'sapi', 'GET', {'cost': 80.004}) + sapi_get_capital_config_getall = sapiGetCapitalConfigGetall = Entry('capital/config/getall', 'sapi', 'GET', {'cost': 1}) + sapi_get_capital_deposit_address = sapiGetCapitalDepositAddress = Entry('capital/deposit/address', 'sapi', 'GET', {'cost': 1}) + sapi_get_capital_deposit_address_list = sapiGetCapitalDepositAddressList = Entry('capital/deposit/address/list', 'sapi', 'GET', {'cost': 1}) + sapi_get_capital_deposit_hisrec = sapiGetCapitalDepositHisrec = Entry('capital/deposit/hisrec', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_capital_deposit_subaddress = sapiGetCapitalDepositSubAddress = Entry('capital/deposit/subAddress', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_capital_deposit_subhisrec = sapiGetCapitalDepositSubHisrec = Entry('capital/deposit/subHisrec', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_capital_withdraw_history = sapiGetCapitalWithdrawHistory = Entry('capital/withdraw/history', 'sapi', 'GET', {'cost': 2}) + sapi_get_capital_withdraw_address_list = sapiGetCapitalWithdrawAddressList = Entry('capital/withdraw/address/list', 'sapi', 'GET', {'cost': 10}) + sapi_get_capital_contract_convertible_coins = sapiGetCapitalContractConvertibleCoins = Entry('capital/contract/convertible-coins', 'sapi', 'GET', {'cost': 4.0002}) + sapi_get_convert_tradeflow = sapiGetConvertTradeFlow = Entry('convert/tradeFlow', 'sapi', 'GET', {'cost': 20.001}) + sapi_get_convert_exchangeinfo = sapiGetConvertExchangeInfo = Entry('convert/exchangeInfo', 'sapi', 'GET', {'cost': 50}) + sapi_get_convert_assetinfo = sapiGetConvertAssetInfo = Entry('convert/assetInfo', 'sapi', 'GET', {'cost': 10}) + sapi_get_convert_orderstatus = sapiGetConvertOrderStatus = Entry('convert/orderStatus', 'sapi', 'GET', {'cost': 0.6667}) + sapi_get_convert_limit_queryopenorders = sapiGetConvertLimitQueryOpenOrders = Entry('convert/limit/queryOpenOrders', 'sapi', 'GET', {'cost': 20.001}) + sapi_get_account_status = sapiGetAccountStatus = Entry('account/status', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_account_apitradingstatus = sapiGetAccountApiTradingStatus = Entry('account/apiTradingStatus', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_account_apirestrictions_iprestriction = sapiGetAccountApiRestrictionsIpRestriction = Entry('account/apiRestrictions/ipRestriction', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_bnbburn = sapiGetBnbBurn = Entry('bnbBurn', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_sub_account_futures_account = sapiGetSubAccountFuturesAccount = Entry('sub-account/futures/account', 'sapi', 'GET', {'cost': 1}) + sapi_get_sub_account_futures_accountsummary = sapiGetSubAccountFuturesAccountSummary = Entry('sub-account/futures/accountSummary', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_sub_account_futures_positionrisk = sapiGetSubAccountFuturesPositionRisk = Entry('sub-account/futures/positionRisk', 'sapi', 'GET', {'cost': 1}) + sapi_get_sub_account_futures_internaltransfer = sapiGetSubAccountFuturesInternalTransfer = Entry('sub-account/futures/internalTransfer', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_sub_account_list = sapiGetSubAccountList = Entry('sub-account/list', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_sub_account_margin_account = sapiGetSubAccountMarginAccount = Entry('sub-account/margin/account', 'sapi', 'GET', {'cost': 1}) + sapi_get_sub_account_margin_accountsummary = sapiGetSubAccountMarginAccountSummary = Entry('sub-account/margin/accountSummary', 'sapi', 'GET', {'cost': 1}) + sapi_get_sub_account_spotsummary = sapiGetSubAccountSpotSummary = Entry('sub-account/spotSummary', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_sub_account_status = sapiGetSubAccountStatus = Entry('sub-account/status', 'sapi', 'GET', {'cost': 1}) + sapi_get_sub_account_sub_transfer_history = sapiGetSubAccountSubTransferHistory = Entry('sub-account/sub/transfer/history', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_sub_account_transfer_subuserhistory = sapiGetSubAccountTransferSubUserHistory = Entry('sub-account/transfer/subUserHistory', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_sub_account_universaltransfer = sapiGetSubAccountUniversalTransfer = Entry('sub-account/universalTransfer', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_sub_account_apirestrictions_iprestriction_thirdpartylist = sapiGetSubAccountApiRestrictionsIpRestrictionThirdPartyList = Entry('sub-account/apiRestrictions/ipRestriction/thirdPartyList', 'sapi', 'GET', {'cost': 1}) + sapi_get_sub_account_transaction_statistics = sapiGetSubAccountTransactionStatistics = Entry('sub-account/transaction-statistics', 'sapi', 'GET', {'cost': 0.40002}) + sapi_get_sub_account_subaccountapi_iprestriction = sapiGetSubAccountSubAccountApiIpRestriction = Entry('sub-account/subAccountApi/ipRestriction', 'sapi', 'GET', {'cost': 20.001}) + sapi_get_managed_subaccount_asset = sapiGetManagedSubaccountAsset = Entry('managed-subaccount/asset', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_managed_subaccount_accountsnapshot = sapiGetManagedSubaccountAccountSnapshot = Entry('managed-subaccount/accountSnapshot', 'sapi', 'GET', {'cost': 240}) + sapi_get_managed_subaccount_querytranslogforinvestor = sapiGetManagedSubaccountQueryTransLogForInvestor = Entry('managed-subaccount/queryTransLogForInvestor', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_managed_subaccount_querytranslogfortradeparent = sapiGetManagedSubaccountQueryTransLogForTradeParent = Entry('managed-subaccount/queryTransLogForTradeParent', 'sapi', 'GET', {'cost': 0.40002}) + sapi_get_managed_subaccount_fetch_future_asset = sapiGetManagedSubaccountFetchFutureAsset = Entry('managed-subaccount/fetch-future-asset', 'sapi', 'GET', {'cost': 0.40002}) + sapi_get_managed_subaccount_marginasset = sapiGetManagedSubaccountMarginAsset = Entry('managed-subaccount/marginAsset', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_managed_subaccount_info = sapiGetManagedSubaccountInfo = Entry('managed-subaccount/info', 'sapi', 'GET', {'cost': 0.40002}) + sapi_get_managed_subaccount_deposit_address = sapiGetManagedSubaccountDepositAddress = Entry('managed-subaccount/deposit/address', 'sapi', 'GET', {'cost': 0.006667}) + sapi_get_managed_subaccount_query_trans_log = sapiGetManagedSubaccountQueryTransLog = Entry('managed-subaccount/query-trans-log', 'sapi', 'GET', {'cost': 0.40002}) + sapi_get_lending_daily_product_list = sapiGetLendingDailyProductList = Entry('lending/daily/product/list', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_daily_userleftquota = sapiGetLendingDailyUserLeftQuota = Entry('lending/daily/userLeftQuota', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_daily_userredemptionquota = sapiGetLendingDailyUserRedemptionQuota = Entry('lending/daily/userRedemptionQuota', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_daily_token_position = sapiGetLendingDailyTokenPosition = Entry('lending/daily/token/position', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_union_account = sapiGetLendingUnionAccount = Entry('lending/union/account', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_union_purchaserecord = sapiGetLendingUnionPurchaseRecord = Entry('lending/union/purchaseRecord', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_union_redemptionrecord = sapiGetLendingUnionRedemptionRecord = Entry('lending/union/redemptionRecord', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_union_interesthistory = sapiGetLendingUnionInterestHistory = Entry('lending/union/interestHistory', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_project_list = sapiGetLendingProjectList = Entry('lending/project/list', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_project_position_list = sapiGetLendingProjectPositionList = Entry('lending/project/position/list', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_eth_staking_eth_history_stakinghistory = sapiGetEthStakingEthHistoryStakingHistory = Entry('eth-staking/eth/history/stakingHistory', 'sapi', 'GET', {'cost': 15}) + sapi_get_eth_staking_eth_history_redemptionhistory = sapiGetEthStakingEthHistoryRedemptionHistory = Entry('eth-staking/eth/history/redemptionHistory', 'sapi', 'GET', {'cost': 15}) + sapi_get_eth_staking_eth_history_rewardshistory = sapiGetEthStakingEthHistoryRewardsHistory = Entry('eth-staking/eth/history/rewardsHistory', 'sapi', 'GET', {'cost': 15}) + sapi_get_eth_staking_eth_quota = sapiGetEthStakingEthQuota = Entry('eth-staking/eth/quota', 'sapi', 'GET', {'cost': 15}) + sapi_get_eth_staking_eth_history_ratehistory = sapiGetEthStakingEthHistoryRateHistory = Entry('eth-staking/eth/history/rateHistory', 'sapi', 'GET', {'cost': 15}) + sapi_get_eth_staking_account = sapiGetEthStakingAccount = Entry('eth-staking/account', 'sapi', 'GET', {'cost': 15}) + sapi_get_eth_staking_wbeth_history_wraphistory = sapiGetEthStakingWbethHistoryWrapHistory = Entry('eth-staking/wbeth/history/wrapHistory', 'sapi', 'GET', {'cost': 15}) + sapi_get_eth_staking_wbeth_history_unwraphistory = sapiGetEthStakingWbethHistoryUnwrapHistory = Entry('eth-staking/wbeth/history/unwrapHistory', 'sapi', 'GET', {'cost': 15}) + sapi_get_eth_staking_eth_history_wbethrewardshistory = sapiGetEthStakingEthHistoryWbethRewardsHistory = Entry('eth-staking/eth/history/wbethRewardsHistory', 'sapi', 'GET', {'cost': 15}) + sapi_get_sol_staking_sol_history_stakinghistory = sapiGetSolStakingSolHistoryStakingHistory = Entry('sol-staking/sol/history/stakingHistory', 'sapi', 'GET', {'cost': 15}) + sapi_get_sol_staking_sol_history_redemptionhistory = sapiGetSolStakingSolHistoryRedemptionHistory = Entry('sol-staking/sol/history/redemptionHistory', 'sapi', 'GET', {'cost': 15}) + sapi_get_sol_staking_sol_history_bnsolrewardshistory = sapiGetSolStakingSolHistoryBnsolRewardsHistory = Entry('sol-staking/sol/history/bnsolRewardsHistory', 'sapi', 'GET', {'cost': 15}) + sapi_get_sol_staking_sol_history_ratehistory = sapiGetSolStakingSolHistoryRateHistory = Entry('sol-staking/sol/history/rateHistory', 'sapi', 'GET', {'cost': 15}) + sapi_get_sol_staking_account = sapiGetSolStakingAccount = Entry('sol-staking/account', 'sapi', 'GET', {'cost': 15}) + sapi_get_sol_staking_sol_quota = sapiGetSolStakingSolQuota = Entry('sol-staking/sol/quota', 'sapi', 'GET', {'cost': 15}) + sapi_get_mining_pub_algolist = sapiGetMiningPubAlgoList = Entry('mining/pub/algoList', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_mining_pub_coinlist = sapiGetMiningPubCoinList = Entry('mining/pub/coinList', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_mining_worker_detail = sapiGetMiningWorkerDetail = Entry('mining/worker/detail', 'sapi', 'GET', {'cost': 0.5}) + sapi_get_mining_worker_list = sapiGetMiningWorkerList = Entry('mining/worker/list', 'sapi', 'GET', {'cost': 0.5}) + sapi_get_mining_payment_list = sapiGetMiningPaymentList = Entry('mining/payment/list', 'sapi', 'GET', {'cost': 0.5}) + sapi_get_mining_statistics_user_status = sapiGetMiningStatisticsUserStatus = Entry('mining/statistics/user/status', 'sapi', 'GET', {'cost': 0.5}) + sapi_get_mining_statistics_user_list = sapiGetMiningStatisticsUserList = Entry('mining/statistics/user/list', 'sapi', 'GET', {'cost': 0.5}) + sapi_get_mining_payment_uid = sapiGetMiningPaymentUid = Entry('mining/payment/uid', 'sapi', 'GET', {'cost': 0.5}) + sapi_get_bswap_pools = sapiGetBswapPools = Entry('bswap/pools', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_bswap_liquidity = sapiGetBswapLiquidity = Entry('bswap/liquidity', 'sapi', 'GET', {'cost': 0.1, 'noPoolId': 1}) + sapi_get_bswap_liquidityops = sapiGetBswapLiquidityOps = Entry('bswap/liquidityOps', 'sapi', 'GET', {'cost': 20.001}) + sapi_get_bswap_quote = sapiGetBswapQuote = Entry('bswap/quote', 'sapi', 'GET', {'cost': 1.00005}) + sapi_get_bswap_swap = sapiGetBswapSwap = Entry('bswap/swap', 'sapi', 'GET', {'cost': 20.001}) + sapi_get_bswap_poolconfigure = sapiGetBswapPoolConfigure = Entry('bswap/poolConfigure', 'sapi', 'GET', {'cost': 1.00005}) + sapi_get_bswap_addliquiditypreview = sapiGetBswapAddLiquidityPreview = Entry('bswap/addLiquidityPreview', 'sapi', 'GET', {'cost': 1.00005}) + sapi_get_bswap_removeliquiditypreview = sapiGetBswapRemoveLiquidityPreview = Entry('bswap/removeLiquidityPreview', 'sapi', 'GET', {'cost': 1.00005}) + sapi_get_bswap_unclaimedrewards = sapiGetBswapUnclaimedRewards = Entry('bswap/unclaimedRewards', 'sapi', 'GET', {'cost': 6.667}) + sapi_get_bswap_claimedhistory = sapiGetBswapClaimedHistory = Entry('bswap/claimedHistory', 'sapi', 'GET', {'cost': 6.667}) + sapi_get_blvt_tokeninfo = sapiGetBlvtTokenInfo = Entry('blvt/tokenInfo', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_blvt_subscribe_record = sapiGetBlvtSubscribeRecord = Entry('blvt/subscribe/record', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_blvt_redeem_record = sapiGetBlvtRedeemRecord = Entry('blvt/redeem/record', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_blvt_userlimit = sapiGetBlvtUserLimit = Entry('blvt/userLimit', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_apireferral_ifnewuser = sapiGetApiReferralIfNewUser = Entry('apiReferral/ifNewUser', 'sapi', 'GET', {'cost': 1}) + sapi_get_apireferral_customization = sapiGetApiReferralCustomization = Entry('apiReferral/customization', 'sapi', 'GET', {'cost': 1}) + sapi_get_apireferral_usercustomization = sapiGetApiReferralUserCustomization = Entry('apiReferral/userCustomization', 'sapi', 'GET', {'cost': 1}) + sapi_get_apireferral_rebate_recentrecord = sapiGetApiReferralRebateRecentRecord = Entry('apiReferral/rebate/recentRecord', 'sapi', 'GET', {'cost': 1}) + sapi_get_apireferral_rebate_historicalrecord = sapiGetApiReferralRebateHistoricalRecord = Entry('apiReferral/rebate/historicalRecord', 'sapi', 'GET', {'cost': 1}) + sapi_get_apireferral_kickback_recentrecord = sapiGetApiReferralKickbackRecentRecord = Entry('apiReferral/kickback/recentRecord', 'sapi', 'GET', {'cost': 1}) + sapi_get_apireferral_kickback_historicalrecord = sapiGetApiReferralKickbackHistoricalRecord = Entry('apiReferral/kickback/historicalRecord', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_subaccountapi = sapiGetBrokerSubAccountApi = Entry('broker/subAccountApi', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_subaccount = sapiGetBrokerSubAccount = Entry('broker/subAccount', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_subaccountapi_commission_futures = sapiGetBrokerSubAccountApiCommissionFutures = Entry('broker/subAccountApi/commission/futures', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_subaccountapi_commission_coinfutures = sapiGetBrokerSubAccountApiCommissionCoinFutures = Entry('broker/subAccountApi/commission/coinFutures', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_info = sapiGetBrokerInfo = Entry('broker/info', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_transfer = sapiGetBrokerTransfer = Entry('broker/transfer', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_transfer_futures = sapiGetBrokerTransferFutures = Entry('broker/transfer/futures', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_rebate_recentrecord = sapiGetBrokerRebateRecentRecord = Entry('broker/rebate/recentRecord', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_rebate_historicalrecord = sapiGetBrokerRebateHistoricalRecord = Entry('broker/rebate/historicalRecord', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_subaccount_bnbburn_status = sapiGetBrokerSubAccountBnbBurnStatus = Entry('broker/subAccount/bnbBurn/status', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_subaccount_deposithist = sapiGetBrokerSubAccountDepositHist = Entry('broker/subAccount/depositHist', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_subaccount_spotsummary = sapiGetBrokerSubAccountSpotSummary = Entry('broker/subAccount/spotSummary', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_subaccount_marginsummary = sapiGetBrokerSubAccountMarginSummary = Entry('broker/subAccount/marginSummary', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_subaccount_futuressummary = sapiGetBrokerSubAccountFuturesSummary = Entry('broker/subAccount/futuresSummary', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_rebate_futures_recentrecord = sapiGetBrokerRebateFuturesRecentRecord = Entry('broker/rebate/futures/recentRecord', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_subaccountapi_iprestriction = sapiGetBrokerSubAccountApiIpRestriction = Entry('broker/subAccountApi/ipRestriction', 'sapi', 'GET', {'cost': 1}) + sapi_get_broker_universaltransfer = sapiGetBrokerUniversalTransfer = Entry('broker/universalTransfer', 'sapi', 'GET', {'cost': 1}) + sapi_get_account_apirestrictions = sapiGetAccountApiRestrictions = Entry('account/apiRestrictions', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_c2c_ordermatch_listuserorderhistory = sapiGetC2cOrderMatchListUserOrderHistory = Entry('c2c/orderMatch/listUserOrderHistory', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_nft_history_transactions = sapiGetNftHistoryTransactions = Entry('nft/history/transactions', 'sapi', 'GET', {'cost': 20.001}) + sapi_get_nft_history_deposit = sapiGetNftHistoryDeposit = Entry('nft/history/deposit', 'sapi', 'GET', {'cost': 20.001}) + sapi_get_nft_history_withdraw = sapiGetNftHistoryWithdraw = Entry('nft/history/withdraw', 'sapi', 'GET', {'cost': 20.001}) + sapi_get_nft_user_getasset = sapiGetNftUserGetAsset = Entry('nft/user/getAsset', 'sapi', 'GET', {'cost': 20.001}) + sapi_get_pay_transactions = sapiGetPayTransactions = Entry('pay/transactions', 'sapi', 'GET', {'cost': 20.001}) + sapi_get_giftcard_verify = sapiGetGiftcardVerify = Entry('giftcard/verify', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_giftcard_cryptography_rsa_public_key = sapiGetGiftcardCryptographyRsaPublicKey = Entry('giftcard/cryptography/rsa-public-key', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_giftcard_buycode_token_limit = sapiGetGiftcardBuyCodeTokenLimit = Entry('giftcard/buyCode/token-limit', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_algo_spot_openorders = sapiGetAlgoSpotOpenOrders = Entry('algo/spot/openOrders', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_algo_spot_historicalorders = sapiGetAlgoSpotHistoricalOrders = Entry('algo/spot/historicalOrders', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_algo_spot_suborders = sapiGetAlgoSpotSubOrders = Entry('algo/spot/subOrders', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_algo_futures_openorders = sapiGetAlgoFuturesOpenOrders = Entry('algo/futures/openOrders', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_algo_futures_historicalorders = sapiGetAlgoFuturesHistoricalOrders = Entry('algo/futures/historicalOrders', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_algo_futures_suborders = sapiGetAlgoFuturesSubOrders = Entry('algo/futures/subOrders', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_portfolio_account = sapiGetPortfolioAccount = Entry('portfolio/account', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_portfolio_collateralrate = sapiGetPortfolioCollateralRate = Entry('portfolio/collateralRate', 'sapi', 'GET', {'cost': 5}) + sapi_get_portfolio_pmloan = sapiGetPortfolioPmLoan = Entry('portfolio/pmLoan', 'sapi', 'GET', {'cost': 3.3335}) + sapi_get_portfolio_interest_history = sapiGetPortfolioInterestHistory = Entry('portfolio/interest-history', 'sapi', 'GET', {'cost': 0.6667}) + sapi_get_portfolio_asset_index_price = sapiGetPortfolioAssetIndexPrice = Entry('portfolio/asset-index-price', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_portfolio_repay_futures_switch = sapiGetPortfolioRepayFuturesSwitch = Entry('portfolio/repay-futures-switch', 'sapi', 'GET', {'cost': 3}) + sapi_get_portfolio_margin_asset_leverage = sapiGetPortfolioMarginAssetLeverage = Entry('portfolio/margin-asset-leverage', 'sapi', 'GET', {'cost': 5}) + sapi_get_portfolio_balance = sapiGetPortfolioBalance = Entry('portfolio/balance', 'sapi', 'GET', {'cost': 2}) + sapi_get_portfolio_negative_balance_exchange_record = sapiGetPortfolioNegativeBalanceExchangeRecord = Entry('portfolio/negative-balance-exchange-record', 'sapi', 'GET', {'cost': 2}) + sapi_get_portfolio_pmloan_history = sapiGetPortfolioPmloanHistory = Entry('portfolio/pmloan-history', 'sapi', 'GET', {'cost': 5}) + sapi_get_portfolio_earn_asset_balance = sapiGetPortfolioEarnAssetBalance = Entry('portfolio/earn-asset-balance', 'sapi', 'GET', {'cost': 150}) + sapi_get_staking_productlist = sapiGetStakingProductList = Entry('staking/productList', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_staking_position = sapiGetStakingPosition = Entry('staking/position', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_staking_stakingrecord = sapiGetStakingStakingRecord = Entry('staking/stakingRecord', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_staking_personalleftquota = sapiGetStakingPersonalLeftQuota = Entry('staking/personalLeftQuota', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_auto_invest_target_asset_list = sapiGetLendingAutoInvestTargetAssetList = Entry('lending/auto-invest/target-asset/list', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_auto_invest_target_asset_roi_list = sapiGetLendingAutoInvestTargetAssetRoiList = Entry('lending/auto-invest/target-asset/roi/list', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_auto_invest_all_asset = sapiGetLendingAutoInvestAllAsset = Entry('lending/auto-invest/all/asset', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_auto_invest_source_asset_list = sapiGetLendingAutoInvestSourceAssetList = Entry('lending/auto-invest/source-asset/list', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_auto_invest_plan_list = sapiGetLendingAutoInvestPlanList = Entry('lending/auto-invest/plan/list', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_auto_invest_plan_id = sapiGetLendingAutoInvestPlanId = Entry('lending/auto-invest/plan/id', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_auto_invest_history_list = sapiGetLendingAutoInvestHistoryList = Entry('lending/auto-invest/history/list', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_auto_invest_index_info = sapiGetLendingAutoInvestIndexInfo = Entry('lending/auto-invest/index/info', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_auto_invest_index_user_summary = sapiGetLendingAutoInvestIndexUserSummary = Entry('lending/auto-invest/index/user-summary', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_auto_invest_one_off_status = sapiGetLendingAutoInvestOneOffStatus = Entry('lending/auto-invest/one-off/status', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_auto_invest_redeem_history = sapiGetLendingAutoInvestRedeemHistory = Entry('lending/auto-invest/redeem/history', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_lending_auto_invest_rebalance_history = sapiGetLendingAutoInvestRebalanceHistory = Entry('lending/auto-invest/rebalance/history', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_simple_earn_flexible_list = sapiGetSimpleEarnFlexibleList = Entry('simple-earn/flexible/list', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_locked_list = sapiGetSimpleEarnLockedList = Entry('simple-earn/locked/list', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_flexible_personalleftquota = sapiGetSimpleEarnFlexiblePersonalLeftQuota = Entry('simple-earn/flexible/personalLeftQuota', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_locked_personalleftquota = sapiGetSimpleEarnLockedPersonalLeftQuota = Entry('simple-earn/locked/personalLeftQuota', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_flexible_subscriptionpreview = sapiGetSimpleEarnFlexibleSubscriptionPreview = Entry('simple-earn/flexible/subscriptionPreview', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_locked_subscriptionpreview = sapiGetSimpleEarnLockedSubscriptionPreview = Entry('simple-earn/locked/subscriptionPreview', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_flexible_history_ratehistory = sapiGetSimpleEarnFlexibleHistoryRateHistory = Entry('simple-earn/flexible/history/rateHistory', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_flexible_position = sapiGetSimpleEarnFlexiblePosition = Entry('simple-earn/flexible/position', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_locked_position = sapiGetSimpleEarnLockedPosition = Entry('simple-earn/locked/position', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_account = sapiGetSimpleEarnAccount = Entry('simple-earn/account', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_flexible_history_subscriptionrecord = sapiGetSimpleEarnFlexibleHistorySubscriptionRecord = Entry('simple-earn/flexible/history/subscriptionRecord', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_locked_history_subscriptionrecord = sapiGetSimpleEarnLockedHistorySubscriptionRecord = Entry('simple-earn/locked/history/subscriptionRecord', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_flexible_history_redemptionrecord = sapiGetSimpleEarnFlexibleHistoryRedemptionRecord = Entry('simple-earn/flexible/history/redemptionRecord', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_locked_history_redemptionrecord = sapiGetSimpleEarnLockedHistoryRedemptionRecord = Entry('simple-earn/locked/history/redemptionRecord', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_flexible_history_rewardsrecord = sapiGetSimpleEarnFlexibleHistoryRewardsRecord = Entry('simple-earn/flexible/history/rewardsRecord', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_locked_history_rewardsrecord = sapiGetSimpleEarnLockedHistoryRewardsRecord = Entry('simple-earn/locked/history/rewardsRecord', 'sapi', 'GET', {'cost': 15}) + sapi_get_simple_earn_flexible_history_collateralrecord = sapiGetSimpleEarnFlexibleHistoryCollateralRecord = Entry('simple-earn/flexible/history/collateralRecord', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_dci_product_list = sapiGetDciProductList = Entry('dci/product/list', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_dci_product_positions = sapiGetDciProductPositions = Entry('dci/product/positions', 'sapi', 'GET', {'cost': 0.1}) + sapi_get_dci_product_accounts = sapiGetDciProductAccounts = Entry('dci/product/accounts', 'sapi', 'GET', {'cost': 0.1}) + sapi_post_asset_dust = sapiPostAssetDust = Entry('asset/dust', 'sapi', 'POST', {'cost': 0.06667}) + sapi_post_asset_dust_btc = sapiPostAssetDustBtc = Entry('asset/dust-btc', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_asset_transfer = sapiPostAssetTransfer = Entry('asset/transfer', 'sapi', 'POST', {'cost': 6.0003}) + sapi_post_asset_get_funding_asset = sapiPostAssetGetFundingAsset = Entry('asset/get-funding-asset', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_asset_convert_transfer = sapiPostAssetConvertTransfer = Entry('asset/convert-transfer', 'sapi', 'POST', {'cost': 0.033335}) + sapi_post_account_disablefastwithdrawswitch = sapiPostAccountDisableFastWithdrawSwitch = Entry('account/disableFastWithdrawSwitch', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_account_enablefastwithdrawswitch = sapiPostAccountEnableFastWithdrawSwitch = Entry('account/enableFastWithdrawSwitch', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_capital_withdraw_apply = sapiPostCapitalWithdrawApply = Entry('capital/withdraw/apply', 'sapi', 'POST', {'cost': 4.0002}) + sapi_post_capital_contract_convertible_coins = sapiPostCapitalContractConvertibleCoins = Entry('capital/contract/convertible-coins', 'sapi', 'POST', {'cost': 4.0002}) + sapi_post_capital_deposit_credit_apply = sapiPostCapitalDepositCreditApply = Entry('capital/deposit/credit-apply', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_margin_borrow_repay = sapiPostMarginBorrowRepay = Entry('margin/borrow-repay', 'sapi', 'POST', {'cost': 20.001}) + sapi_post_margin_transfer = sapiPostMarginTransfer = Entry('margin/transfer', 'sapi', 'POST', {'cost': 4.0002}) + sapi_post_margin_loan = sapiPostMarginLoan = Entry('margin/loan', 'sapi', 'POST', {'cost': 20.001}) + sapi_post_margin_repay = sapiPostMarginRepay = Entry('margin/repay', 'sapi', 'POST', {'cost': 20.001}) + sapi_post_margin_order = sapiPostMarginOrder = Entry('margin/order', 'sapi', 'POST', {'cost': 0.040002}) + sapi_post_margin_order_oco = sapiPostMarginOrderOco = Entry('margin/order/oco', 'sapi', 'POST', {'cost': 0.040002}) + sapi_post_margin_dust = sapiPostMarginDust = Entry('margin/dust', 'sapi', 'POST', {'cost': 20.001}) + sapi_post_margin_exchange_small_liability = sapiPostMarginExchangeSmallLiability = Entry('margin/exchange-small-liability', 'sapi', 'POST', {'cost': 20.001}) + sapi_post_margin_isolated_transfer = sapiPostMarginIsolatedTransfer = Entry('margin/isolated/transfer', 'sapi', 'POST', {'cost': 4.0002}) + sapi_post_margin_isolated_account = sapiPostMarginIsolatedAccount = Entry('margin/isolated/account', 'sapi', 'POST', {'cost': 2.0001}) + sapi_post_margin_max_leverage = sapiPostMarginMaxLeverage = Entry('margin/max-leverage', 'sapi', 'POST', {'cost': 300}) + sapi_post_bnbburn = sapiPostBnbBurn = Entry('bnbBurn', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_sub_account_virtualsubaccount = sapiPostSubAccountVirtualSubAccount = Entry('sub-account/virtualSubAccount', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_sub_account_margin_transfer = sapiPostSubAccountMarginTransfer = Entry('sub-account/margin/transfer', 'sapi', 'POST', {'cost': 4.0002}) + sapi_post_sub_account_margin_enable = sapiPostSubAccountMarginEnable = Entry('sub-account/margin/enable', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_sub_account_futures_enable = sapiPostSubAccountFuturesEnable = Entry('sub-account/futures/enable', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_sub_account_futures_transfer = sapiPostSubAccountFuturesTransfer = Entry('sub-account/futures/transfer', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_sub_account_futures_internaltransfer = sapiPostSubAccountFuturesInternalTransfer = Entry('sub-account/futures/internalTransfer', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_sub_account_transfer_subtosub = sapiPostSubAccountTransferSubToSub = Entry('sub-account/transfer/subToSub', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_sub_account_transfer_subtomaster = sapiPostSubAccountTransferSubToMaster = Entry('sub-account/transfer/subToMaster', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_sub_account_universaltransfer = sapiPostSubAccountUniversalTransfer = Entry('sub-account/universalTransfer', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_sub_account_options_enable = sapiPostSubAccountOptionsEnable = Entry('sub-account/options/enable', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_managed_subaccount_deposit = sapiPostManagedSubaccountDeposit = Entry('managed-subaccount/deposit', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_managed_subaccount_withdraw = sapiPostManagedSubaccountWithdraw = Entry('managed-subaccount/withdraw', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_userdatastream = sapiPostUserDataStream = Entry('userDataStream', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_userdatastream_isolated = sapiPostUserDataStreamIsolated = Entry('userDataStream/isolated', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_futures_transfer = sapiPostFuturesTransfer = Entry('futures/transfer', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_lending_customizedfixed_purchase = sapiPostLendingCustomizedFixedPurchase = Entry('lending/customizedFixed/purchase', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_lending_daily_purchase = sapiPostLendingDailyPurchase = Entry('lending/daily/purchase', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_lending_daily_redeem = sapiPostLendingDailyRedeem = Entry('lending/daily/redeem', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_bswap_liquidityadd = sapiPostBswapLiquidityAdd = Entry('bswap/liquidityAdd', 'sapi', 'POST', {'cost': 60}) + sapi_post_bswap_liquidityremove = sapiPostBswapLiquidityRemove = Entry('bswap/liquidityRemove', 'sapi', 'POST', {'cost': 60}) + sapi_post_bswap_swap = sapiPostBswapSwap = Entry('bswap/swap', 'sapi', 'POST', {'cost': 60}) + sapi_post_bswap_claimrewards = sapiPostBswapClaimRewards = Entry('bswap/claimRewards', 'sapi', 'POST', {'cost': 6.667}) + sapi_post_blvt_subscribe = sapiPostBlvtSubscribe = Entry('blvt/subscribe', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_blvt_redeem = sapiPostBlvtRedeem = Entry('blvt/redeem', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_apireferral_customization = sapiPostApiReferralCustomization = Entry('apiReferral/customization', 'sapi', 'POST', {'cost': 1}) + sapi_post_apireferral_usercustomization = sapiPostApiReferralUserCustomization = Entry('apiReferral/userCustomization', 'sapi', 'POST', {'cost': 1}) + sapi_post_apireferral_rebate_historicalrecord = sapiPostApiReferralRebateHistoricalRecord = Entry('apiReferral/rebate/historicalRecord', 'sapi', 'POST', {'cost': 1}) + sapi_post_apireferral_kickback_historicalrecord = sapiPostApiReferralKickbackHistoricalRecord = Entry('apiReferral/kickback/historicalRecord', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_subaccount = sapiPostBrokerSubAccount = Entry('broker/subAccount', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_subaccount_margin = sapiPostBrokerSubAccountMargin = Entry('broker/subAccount/margin', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_subaccount_futures = sapiPostBrokerSubAccountFutures = Entry('broker/subAccount/futures', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_subaccountapi = sapiPostBrokerSubAccountApi = Entry('broker/subAccountApi', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_subaccountapi_permission = sapiPostBrokerSubAccountApiPermission = Entry('broker/subAccountApi/permission', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_subaccountapi_commission = sapiPostBrokerSubAccountApiCommission = Entry('broker/subAccountApi/commission', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_subaccountapi_commission_futures = sapiPostBrokerSubAccountApiCommissionFutures = Entry('broker/subAccountApi/commission/futures', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_subaccountapi_commission_coinfutures = sapiPostBrokerSubAccountApiCommissionCoinFutures = Entry('broker/subAccountApi/commission/coinFutures', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_transfer = sapiPostBrokerTransfer = Entry('broker/transfer', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_transfer_futures = sapiPostBrokerTransferFutures = Entry('broker/transfer/futures', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_rebate_historicalrecord = sapiPostBrokerRebateHistoricalRecord = Entry('broker/rebate/historicalRecord', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_subaccount_bnbburn_spot = sapiPostBrokerSubAccountBnbBurnSpot = Entry('broker/subAccount/bnbBurn/spot', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_subaccount_bnbburn_margininterest = sapiPostBrokerSubAccountBnbBurnMarginInterest = Entry('broker/subAccount/bnbBurn/marginInterest', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_subaccount_blvt = sapiPostBrokerSubAccountBlvt = Entry('broker/subAccount/blvt', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_subaccountapi_iprestriction = sapiPostBrokerSubAccountApiIpRestriction = Entry('broker/subAccountApi/ipRestriction', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_subaccountapi_iprestriction_iplist = sapiPostBrokerSubAccountApiIpRestrictionIpList = Entry('broker/subAccountApi/ipRestriction/ipList', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_universaltransfer = sapiPostBrokerUniversalTransfer = Entry('broker/universalTransfer', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_subaccountapi_permission_universaltransfer = sapiPostBrokerSubAccountApiPermissionUniversalTransfer = Entry('broker/subAccountApi/permission/universalTransfer', 'sapi', 'POST', {'cost': 1}) + sapi_post_broker_subaccountapi_permission_vanillaoptions = sapiPostBrokerSubAccountApiPermissionVanillaOptions = Entry('broker/subAccountApi/permission/vanillaOptions', 'sapi', 'POST', {'cost': 1}) + sapi_post_giftcard_createcode = sapiPostGiftcardCreateCode = Entry('giftcard/createCode', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_giftcard_redeemcode = sapiPostGiftcardRedeemCode = Entry('giftcard/redeemCode', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_giftcard_buycode = sapiPostGiftcardBuyCode = Entry('giftcard/buyCode', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_algo_spot_newordertwap = sapiPostAlgoSpotNewOrderTwap = Entry('algo/spot/newOrderTwap', 'sapi', 'POST', {'cost': 20.001}) + sapi_post_algo_futures_newordervp = sapiPostAlgoFuturesNewOrderVp = Entry('algo/futures/newOrderVp', 'sapi', 'POST', {'cost': 20.001}) + sapi_post_algo_futures_newordertwap = sapiPostAlgoFuturesNewOrderTwap = Entry('algo/futures/newOrderTwap', 'sapi', 'POST', {'cost': 20.001}) + sapi_post_staking_purchase = sapiPostStakingPurchase = Entry('staking/purchase', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_staking_redeem = sapiPostStakingRedeem = Entry('staking/redeem', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_staking_setautostaking = sapiPostStakingSetAutoStaking = Entry('staking/setAutoStaking', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_eth_staking_eth_stake = sapiPostEthStakingEthStake = Entry('eth-staking/eth/stake', 'sapi', 'POST', {'cost': 15}) + sapi_post_eth_staking_eth_redeem = sapiPostEthStakingEthRedeem = Entry('eth-staking/eth/redeem', 'sapi', 'POST', {'cost': 15}) + sapi_post_eth_staking_wbeth_wrap = sapiPostEthStakingWbethWrap = Entry('eth-staking/wbeth/wrap', 'sapi', 'POST', {'cost': 15}) + sapi_post_sol_staking_sol_stake = sapiPostSolStakingSolStake = Entry('sol-staking/sol/stake', 'sapi', 'POST', {'cost': 15}) + sapi_post_sol_staking_sol_redeem = sapiPostSolStakingSolRedeem = Entry('sol-staking/sol/redeem', 'sapi', 'POST', {'cost': 15}) + sapi_post_mining_hash_transfer_config = sapiPostMiningHashTransferConfig = Entry('mining/hash-transfer/config', 'sapi', 'POST', {'cost': 0.5}) + sapi_post_mining_hash_transfer_config_cancel = sapiPostMiningHashTransferConfigCancel = Entry('mining/hash-transfer/config/cancel', 'sapi', 'POST', {'cost': 0.5}) + sapi_post_portfolio_repay = sapiPostPortfolioRepay = Entry('portfolio/repay', 'sapi', 'POST', {'cost': 20.001}) + sapi_post_loan_vip_renew = sapiPostLoanVipRenew = Entry('loan/vip/renew', 'sapi', 'POST', {'cost': 40.002}) + sapi_post_loan_vip_borrow = sapiPostLoanVipBorrow = Entry('loan/vip/borrow', 'sapi', 'POST', {'cost': 40.002}) + sapi_post_loan_borrow = sapiPostLoanBorrow = Entry('loan/borrow', 'sapi', 'POST', {'cost': 40.002}) + sapi_post_loan_repay = sapiPostLoanRepay = Entry('loan/repay', 'sapi', 'POST', {'cost': 40.002}) + sapi_post_loan_adjust_ltv = sapiPostLoanAdjustLtv = Entry('loan/adjust/ltv', 'sapi', 'POST', {'cost': 40.002}) + sapi_post_loan_customize_margin_call = sapiPostLoanCustomizeMarginCall = Entry('loan/customize/margin_call', 'sapi', 'POST', {'cost': 40.002}) + sapi_post_loan_flexible_repay = sapiPostLoanFlexibleRepay = Entry('loan/flexible/repay', 'sapi', 'POST', {'cost': 40.002}) + sapi_post_loan_flexible_adjust_ltv = sapiPostLoanFlexibleAdjustLtv = Entry('loan/flexible/adjust/ltv', 'sapi', 'POST', {'cost': 40.002}) + sapi_post_loan_vip_repay = sapiPostLoanVipRepay = Entry('loan/vip/repay', 'sapi', 'POST', {'cost': 40.002}) + sapi_post_convert_getquote = sapiPostConvertGetQuote = Entry('convert/getQuote', 'sapi', 'POST', {'cost': 1.3334}) + sapi_post_convert_acceptquote = sapiPostConvertAcceptQuote = Entry('convert/acceptQuote', 'sapi', 'POST', {'cost': 3.3335}) + sapi_post_convert_limit_placeorder = sapiPostConvertLimitPlaceOrder = Entry('convert/limit/placeOrder', 'sapi', 'POST', {'cost': 3.3335}) + sapi_post_convert_limit_cancelorder = sapiPostConvertLimitCancelOrder = Entry('convert/limit/cancelOrder', 'sapi', 'POST', {'cost': 1.3334}) + sapi_post_portfolio_auto_collection = sapiPostPortfolioAutoCollection = Entry('portfolio/auto-collection', 'sapi', 'POST', {'cost': 150}) + sapi_post_portfolio_asset_collection = sapiPostPortfolioAssetCollection = Entry('portfolio/asset-collection', 'sapi', 'POST', {'cost': 6}) + sapi_post_portfolio_bnb_transfer = sapiPostPortfolioBnbTransfer = Entry('portfolio/bnb-transfer', 'sapi', 'POST', {'cost': 150}) + sapi_post_portfolio_repay_futures_switch = sapiPostPortfolioRepayFuturesSwitch = Entry('portfolio/repay-futures-switch', 'sapi', 'POST', {'cost': 150}) + sapi_post_portfolio_repay_futures_negative_balance = sapiPostPortfolioRepayFuturesNegativeBalance = Entry('portfolio/repay-futures-negative-balance', 'sapi', 'POST', {'cost': 150}) + sapi_post_portfolio_mint = sapiPostPortfolioMint = Entry('portfolio/mint', 'sapi', 'POST', {'cost': 20}) + sapi_post_portfolio_redeem = sapiPostPortfolioRedeem = Entry('portfolio/redeem', 'sapi', 'POST', {'cost': 20}) + sapi_post_portfolio_earn_asset_transfer = sapiPostPortfolioEarnAssetTransfer = Entry('portfolio/earn-asset-transfer', 'sapi', 'POST', {'cost': 150}) + sapi_post_lending_auto_invest_plan_add = sapiPostLendingAutoInvestPlanAdd = Entry('lending/auto-invest/plan/add', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_lending_auto_invest_plan_edit = sapiPostLendingAutoInvestPlanEdit = Entry('lending/auto-invest/plan/edit', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_lending_auto_invest_plan_edit_status = sapiPostLendingAutoInvestPlanEditStatus = Entry('lending/auto-invest/plan/edit-status', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_lending_auto_invest_one_off = sapiPostLendingAutoInvestOneOff = Entry('lending/auto-invest/one-off', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_lending_auto_invest_redeem = sapiPostLendingAutoInvestRedeem = Entry('lending/auto-invest/redeem', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_simple_earn_flexible_subscribe = sapiPostSimpleEarnFlexibleSubscribe = Entry('simple-earn/flexible/subscribe', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_simple_earn_locked_subscribe = sapiPostSimpleEarnLockedSubscribe = Entry('simple-earn/locked/subscribe', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_simple_earn_flexible_redeem = sapiPostSimpleEarnFlexibleRedeem = Entry('simple-earn/flexible/redeem', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_simple_earn_locked_redeem = sapiPostSimpleEarnLockedRedeem = Entry('simple-earn/locked/redeem', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_simple_earn_flexible_setautosubscribe = sapiPostSimpleEarnFlexibleSetAutoSubscribe = Entry('simple-earn/flexible/setAutoSubscribe', 'sapi', 'POST', {'cost': 15}) + sapi_post_simple_earn_locked_setautosubscribe = sapiPostSimpleEarnLockedSetAutoSubscribe = Entry('simple-earn/locked/setAutoSubscribe', 'sapi', 'POST', {'cost': 15}) + sapi_post_simple_earn_locked_setredeemoption = sapiPostSimpleEarnLockedSetRedeemOption = Entry('simple-earn/locked/setRedeemOption', 'sapi', 'POST', {'cost': 5}) + sapi_post_dci_product_subscribe = sapiPostDciProductSubscribe = Entry('dci/product/subscribe', 'sapi', 'POST', {'cost': 0.1}) + sapi_post_dci_product_auto_compound_edit = sapiPostDciProductAutoCompoundEdit = Entry('dci/product/auto_compound/edit', 'sapi', 'POST', {'cost': 0.1}) + sapi_put_userdatastream = sapiPutUserDataStream = Entry('userDataStream', 'sapi', 'PUT', {'cost': 0.1}) + sapi_put_userdatastream_isolated = sapiPutUserDataStreamIsolated = Entry('userDataStream/isolated', 'sapi', 'PUT', {'cost': 0.1}) + sapi_delete_margin_openorders = sapiDeleteMarginOpenOrders = Entry('margin/openOrders', 'sapi', 'DELETE', {'cost': 0.1}) + sapi_delete_margin_order = sapiDeleteMarginOrder = Entry('margin/order', 'sapi', 'DELETE', {'cost': 0.006667}) + sapi_delete_margin_orderlist = sapiDeleteMarginOrderList = Entry('margin/orderList', 'sapi', 'DELETE', {'cost': 0.006667}) + sapi_delete_margin_isolated_account = sapiDeleteMarginIsolatedAccount = Entry('margin/isolated/account', 'sapi', 'DELETE', {'cost': 2.0001}) + sapi_delete_userdatastream = sapiDeleteUserDataStream = Entry('userDataStream', 'sapi', 'DELETE', {'cost': 0.1}) + sapi_delete_userdatastream_isolated = sapiDeleteUserDataStreamIsolated = Entry('userDataStream/isolated', 'sapi', 'DELETE', {'cost': 0.1}) + sapi_delete_broker_subaccountapi = sapiDeleteBrokerSubAccountApi = Entry('broker/subAccountApi', 'sapi', 'DELETE', {'cost': 1}) + sapi_delete_broker_subaccountapi_iprestriction_iplist = sapiDeleteBrokerSubAccountApiIpRestrictionIpList = Entry('broker/subAccountApi/ipRestriction/ipList', 'sapi', 'DELETE', {'cost': 1}) + sapi_delete_algo_spot_order = sapiDeleteAlgoSpotOrder = Entry('algo/spot/order', 'sapi', 'DELETE', {'cost': 0.1}) + sapi_delete_algo_futures_order = sapiDeleteAlgoFuturesOrder = Entry('algo/futures/order', 'sapi', 'DELETE', {'cost': 0.1}) + sapi_delete_sub_account_subaccountapi_iprestriction_iplist = sapiDeleteSubAccountSubAccountApiIpRestrictionIpList = Entry('sub-account/subAccountApi/ipRestriction/ipList', 'sapi', 'DELETE', {'cost': 20.001}) + sapiv2_get_eth_staking_account = sapiV2GetEthStakingAccount = Entry('eth-staking/account', 'sapiV2', 'GET', {'cost': 15}) + sapiv2_get_sub_account_futures_account = sapiV2GetSubAccountFuturesAccount = Entry('sub-account/futures/account', 'sapiV2', 'GET', {'cost': 0.1}) + sapiv2_get_sub_account_futures_accountsummary = sapiV2GetSubAccountFuturesAccountSummary = Entry('sub-account/futures/accountSummary', 'sapiV2', 'GET', {'cost': 1}) + sapiv2_get_sub_account_futures_positionrisk = sapiV2GetSubAccountFuturesPositionRisk = Entry('sub-account/futures/positionRisk', 'sapiV2', 'GET', {'cost': 0.1}) + sapiv2_get_loan_flexible_ongoing_orders = sapiV2GetLoanFlexibleOngoingOrders = Entry('loan/flexible/ongoing/orders', 'sapiV2', 'GET', {'cost': 30}) + sapiv2_get_loan_flexible_borrow_history = sapiV2GetLoanFlexibleBorrowHistory = Entry('loan/flexible/borrow/history', 'sapiV2', 'GET', {'cost': 40}) + sapiv2_get_loan_flexible_repay_history = sapiV2GetLoanFlexibleRepayHistory = Entry('loan/flexible/repay/history', 'sapiV2', 'GET', {'cost': 40}) + sapiv2_get_loan_flexible_ltv_adjustment_history = sapiV2GetLoanFlexibleLtvAdjustmentHistory = Entry('loan/flexible/ltv/adjustment/history', 'sapiV2', 'GET', {'cost': 40}) + sapiv2_get_loan_flexible_loanable_data = sapiV2GetLoanFlexibleLoanableData = Entry('loan/flexible/loanable/data', 'sapiV2', 'GET', {'cost': 40}) + sapiv2_get_loan_flexible_collateral_data = sapiV2GetLoanFlexibleCollateralData = Entry('loan/flexible/collateral/data', 'sapiV2', 'GET', {'cost': 40}) + sapiv2_get_portfolio_account = sapiV2GetPortfolioAccount = Entry('portfolio/account', 'sapiV2', 'GET', {'cost': 2}) + sapiv2_post_eth_staking_eth_stake = sapiV2PostEthStakingEthStake = Entry('eth-staking/eth/stake', 'sapiV2', 'POST', {'cost': 15}) + sapiv2_post_sub_account_subaccountapi_iprestriction = sapiV2PostSubAccountSubAccountApiIpRestriction = Entry('sub-account/subAccountApi/ipRestriction', 'sapiV2', 'POST', {'cost': 20.001}) + sapiv2_post_loan_flexible_borrow = sapiV2PostLoanFlexibleBorrow = Entry('loan/flexible/borrow', 'sapiV2', 'POST', {'cost': 40.002}) + sapiv2_post_loan_flexible_repay = sapiV2PostLoanFlexibleRepay = Entry('loan/flexible/repay', 'sapiV2', 'POST', {'cost': 40.002}) + sapiv2_post_loan_flexible_adjust_ltv = sapiV2PostLoanFlexibleAdjustLtv = Entry('loan/flexible/adjust/ltv', 'sapiV2', 'POST', {'cost': 40.002}) + sapiv3_get_sub_account_assets = sapiV3GetSubAccountAssets = Entry('sub-account/assets', 'sapiV3', 'GET', {'cost': 0.40002}) + sapiv3_post_asset_getuserasset = sapiV3PostAssetGetUserAsset = Entry('asset/getUserAsset', 'sapiV3', 'POST', {'cost': 0.5}) + sapiv4_get_sub_account_assets = sapiV4GetSubAccountAssets = Entry('sub-account/assets', 'sapiV4', 'GET', {'cost': 0.40002}) + dapipublic_get_ping = dapiPublicGetPing = Entry('ping', 'dapiPublic', 'GET', {'cost': 1}) + dapipublic_get_time = dapiPublicGetTime = Entry('time', 'dapiPublic', 'GET', {'cost': 1}) + dapipublic_get_exchangeinfo = dapiPublicGetExchangeInfo = Entry('exchangeInfo', 'dapiPublic', 'GET', {'cost': 1}) + dapipublic_get_depth = dapiPublicGetDepth = Entry('depth', 'dapiPublic', 'GET', {'cost': 2, 'byLimit': [[50, 2], [100, 5], [500, 10], [1000, 20]]}) + dapipublic_get_trades = dapiPublicGetTrades = Entry('trades', 'dapiPublic', 'GET', {'cost': 5}) + dapipublic_get_historicaltrades = dapiPublicGetHistoricalTrades = Entry('historicalTrades', 'dapiPublic', 'GET', {'cost': 20}) + dapipublic_get_aggtrades = dapiPublicGetAggTrades = Entry('aggTrades', 'dapiPublic', 'GET', {'cost': 20}) + dapipublic_get_premiumindex = dapiPublicGetPremiumIndex = Entry('premiumIndex', 'dapiPublic', 'GET', {'cost': 10}) + dapipublic_get_fundingrate = dapiPublicGetFundingRate = Entry('fundingRate', 'dapiPublic', 'GET', {'cost': 1}) + dapipublic_get_klines = dapiPublicGetKlines = Entry('klines', 'dapiPublic', 'GET', {'cost': 1, 'byLimit': [[99, 1], [499, 2], [1000, 5], [10000, 10]]}) + dapipublic_get_continuousklines = dapiPublicGetContinuousKlines = Entry('continuousKlines', 'dapiPublic', 'GET', {'cost': 1, 'byLimit': [[99, 1], [499, 2], [1000, 5], [10000, 10]]}) + dapipublic_get_indexpriceklines = dapiPublicGetIndexPriceKlines = Entry('indexPriceKlines', 'dapiPublic', 'GET', {'cost': 1, 'byLimit': [[99, 1], [499, 2], [1000, 5], [10000, 10]]}) + dapipublic_get_markpriceklines = dapiPublicGetMarkPriceKlines = Entry('markPriceKlines', 'dapiPublic', 'GET', {'cost': 1, 'byLimit': [[99, 1], [499, 2], [1000, 5], [10000, 10]]}) + dapipublic_get_premiumindexklines = dapiPublicGetPremiumIndexKlines = Entry('premiumIndexKlines', 'dapiPublic', 'GET', {'cost': 1, 'byLimit': [[99, 1], [499, 2], [1000, 5], [10000, 10]]}) + dapipublic_get_ticker_24hr = dapiPublicGetTicker24hr = Entry('ticker/24hr', 'dapiPublic', 'GET', {'cost': 1, 'noSymbol': 40}) + dapipublic_get_ticker_price = dapiPublicGetTickerPrice = Entry('ticker/price', 'dapiPublic', 'GET', {'cost': 1, 'noSymbol': 2}) + dapipublic_get_ticker_bookticker = dapiPublicGetTickerBookTicker = Entry('ticker/bookTicker', 'dapiPublic', 'GET', {'cost': 2, 'noSymbol': 5}) + dapipublic_get_constituents = dapiPublicGetConstituents = Entry('constituents', 'dapiPublic', 'GET', {'cost': 2}) + dapipublic_get_openinterest = dapiPublicGetOpenInterest = Entry('openInterest', 'dapiPublic', 'GET', {'cost': 1}) + dapipublic_get_fundinginfo = dapiPublicGetFundingInfo = Entry('fundingInfo', 'dapiPublic', 'GET', {'cost': 1}) + dapidata_get_delivery_price = dapiDataGetDeliveryPrice = Entry('delivery-price', 'dapiData', 'GET', {'cost': 1}) + dapidata_get_openinteresthist = dapiDataGetOpenInterestHist = Entry('openInterestHist', 'dapiData', 'GET', {'cost': 1}) + dapidata_get_toplongshortaccountratio = dapiDataGetTopLongShortAccountRatio = Entry('topLongShortAccountRatio', 'dapiData', 'GET', {'cost': 1}) + dapidata_get_toplongshortpositionratio = dapiDataGetTopLongShortPositionRatio = Entry('topLongShortPositionRatio', 'dapiData', 'GET', {'cost': 1}) + dapidata_get_globallongshortaccountratio = dapiDataGetGlobalLongShortAccountRatio = Entry('globalLongShortAccountRatio', 'dapiData', 'GET', {'cost': 1}) + dapidata_get_takerbuysellvol = dapiDataGetTakerBuySellVol = Entry('takerBuySellVol', 'dapiData', 'GET', {'cost': 1}) + dapidata_get_basis = dapiDataGetBasis = Entry('basis', 'dapiData', 'GET', {'cost': 1}) + dapiprivate_get_positionside_dual = dapiPrivateGetPositionSideDual = Entry('positionSide/dual', 'dapiPrivate', 'GET', {'cost': 30}) + dapiprivate_get_orderamendment = dapiPrivateGetOrderAmendment = Entry('orderAmendment', 'dapiPrivate', 'GET', {'cost': 1}) + dapiprivate_get_order = dapiPrivateGetOrder = Entry('order', 'dapiPrivate', 'GET', {'cost': 1}) + dapiprivate_get_openorder = dapiPrivateGetOpenOrder = Entry('openOrder', 'dapiPrivate', 'GET', {'cost': 1}) + dapiprivate_get_openorders = dapiPrivateGetOpenOrders = Entry('openOrders', 'dapiPrivate', 'GET', {'cost': 1, 'noSymbol': 5}) + dapiprivate_get_allorders = dapiPrivateGetAllOrders = Entry('allOrders', 'dapiPrivate', 'GET', {'cost': 20, 'noSymbol': 40}) + dapiprivate_get_balance = dapiPrivateGetBalance = Entry('balance', 'dapiPrivate', 'GET', {'cost': 1}) + dapiprivate_get_account = dapiPrivateGetAccount = Entry('account', 'dapiPrivate', 'GET', {'cost': 5}) + dapiprivate_get_positionmargin_history = dapiPrivateGetPositionMarginHistory = Entry('positionMargin/history', 'dapiPrivate', 'GET', {'cost': 1}) + dapiprivate_get_positionrisk = dapiPrivateGetPositionRisk = Entry('positionRisk', 'dapiPrivate', 'GET', {'cost': 1}) + dapiprivate_get_usertrades = dapiPrivateGetUserTrades = Entry('userTrades', 'dapiPrivate', 'GET', {'cost': 20, 'noSymbol': 40}) + dapiprivate_get_income = dapiPrivateGetIncome = Entry('income', 'dapiPrivate', 'GET', {'cost': 20}) + dapiprivate_get_leveragebracket = dapiPrivateGetLeverageBracket = Entry('leverageBracket', 'dapiPrivate', 'GET', {'cost': 1}) + dapiprivate_get_forceorders = dapiPrivateGetForceOrders = Entry('forceOrders', 'dapiPrivate', 'GET', {'cost': 20, 'noSymbol': 50}) + dapiprivate_get_adlquantile = dapiPrivateGetAdlQuantile = Entry('adlQuantile', 'dapiPrivate', 'GET', {'cost': 5}) + dapiprivate_get_commissionrate = dapiPrivateGetCommissionRate = Entry('commissionRate', 'dapiPrivate', 'GET', {'cost': 20}) + dapiprivate_get_income_asyn = dapiPrivateGetIncomeAsyn = Entry('income/asyn', 'dapiPrivate', 'GET', {'cost': 5}) + dapiprivate_get_income_asyn_id = dapiPrivateGetIncomeAsynId = Entry('income/asyn/id', 'dapiPrivate', 'GET', {'cost': 5}) + dapiprivate_get_trade_asyn = dapiPrivateGetTradeAsyn = Entry('trade/asyn', 'dapiPrivate', 'GET', {'cost': 0.5}) + dapiprivate_get_trade_asyn_id = dapiPrivateGetTradeAsynId = Entry('trade/asyn/id', 'dapiPrivate', 'GET', {'cost': 0.5}) + dapiprivate_get_order_asyn = dapiPrivateGetOrderAsyn = Entry('order/asyn', 'dapiPrivate', 'GET', {'cost': 0.5}) + dapiprivate_get_order_asyn_id = dapiPrivateGetOrderAsynId = Entry('order/asyn/id', 'dapiPrivate', 'GET', {'cost': 0.5}) + dapiprivate_get_pmexchangeinfo = dapiPrivateGetPmExchangeInfo = Entry('pmExchangeInfo', 'dapiPrivate', 'GET', {'cost': 0.5}) + dapiprivate_get_pmaccountinfo = dapiPrivateGetPmAccountInfo = Entry('pmAccountInfo', 'dapiPrivate', 'GET', {'cost': 0.5}) + dapiprivate_post_positionside_dual = dapiPrivatePostPositionSideDual = Entry('positionSide/dual', 'dapiPrivate', 'POST', {'cost': 1}) + dapiprivate_post_order = dapiPrivatePostOrder = Entry('order', 'dapiPrivate', 'POST', {'cost': 4}) + dapiprivate_post_batchorders = dapiPrivatePostBatchOrders = Entry('batchOrders', 'dapiPrivate', 'POST', {'cost': 5}) + dapiprivate_post_countdowncancelall = dapiPrivatePostCountdownCancelAll = Entry('countdownCancelAll', 'dapiPrivate', 'POST', {'cost': 10}) + dapiprivate_post_leverage = dapiPrivatePostLeverage = Entry('leverage', 'dapiPrivate', 'POST', {'cost': 1}) + dapiprivate_post_margintype = dapiPrivatePostMarginType = Entry('marginType', 'dapiPrivate', 'POST', {'cost': 1}) + dapiprivate_post_positionmargin = dapiPrivatePostPositionMargin = Entry('positionMargin', 'dapiPrivate', 'POST', {'cost': 1}) + dapiprivate_post_listenkey = dapiPrivatePostListenKey = Entry('listenKey', 'dapiPrivate', 'POST', {'cost': 1}) + dapiprivate_put_listenkey = dapiPrivatePutListenKey = Entry('listenKey', 'dapiPrivate', 'PUT', {'cost': 1}) + dapiprivate_put_order = dapiPrivatePutOrder = Entry('order', 'dapiPrivate', 'PUT', {'cost': 1}) + dapiprivate_put_batchorders = dapiPrivatePutBatchOrders = Entry('batchOrders', 'dapiPrivate', 'PUT', {'cost': 5}) + dapiprivate_delete_order = dapiPrivateDeleteOrder = Entry('order', 'dapiPrivate', 'DELETE', {'cost': 1}) + dapiprivate_delete_allopenorders = dapiPrivateDeleteAllOpenOrders = Entry('allOpenOrders', 'dapiPrivate', 'DELETE', {'cost': 1}) + dapiprivate_delete_batchorders = dapiPrivateDeleteBatchOrders = Entry('batchOrders', 'dapiPrivate', 'DELETE', {'cost': 5}) + dapiprivate_delete_listenkey = dapiPrivateDeleteListenKey = Entry('listenKey', 'dapiPrivate', 'DELETE', {'cost': 1}) + dapiprivatev2_get_leveragebracket = dapiPrivateV2GetLeverageBracket = Entry('leverageBracket', 'dapiPrivateV2', 'GET', {'cost': 1}) + fapipublic_get_ping = fapiPublicGetPing = Entry('ping', 'fapiPublic', 'GET', {'cost': 1}) + fapipublic_get_time = fapiPublicGetTime = Entry('time', 'fapiPublic', 'GET', {'cost': 1}) + fapipublic_get_exchangeinfo = fapiPublicGetExchangeInfo = Entry('exchangeInfo', 'fapiPublic', 'GET', {'cost': 1}) + fapipublic_get_depth = fapiPublicGetDepth = Entry('depth', 'fapiPublic', 'GET', {'cost': 2, 'byLimit': [[50, 2], [100, 5], [500, 10], [1000, 20]]}) + fapipublic_get_trades = fapiPublicGetTrades = Entry('trades', 'fapiPublic', 'GET', {'cost': 5}) + fapipublic_get_historicaltrades = fapiPublicGetHistoricalTrades = Entry('historicalTrades', 'fapiPublic', 'GET', {'cost': 20}) + fapipublic_get_aggtrades = fapiPublicGetAggTrades = Entry('aggTrades', 'fapiPublic', 'GET', {'cost': 20}) + fapipublic_get_klines = fapiPublicGetKlines = Entry('klines', 'fapiPublic', 'GET', {'cost': 1, 'byLimit': [[99, 1], [499, 2], [1000, 5], [10000, 10]]}) + fapipublic_get_continuousklines = fapiPublicGetContinuousKlines = Entry('continuousKlines', 'fapiPublic', 'GET', {'cost': 1, 'byLimit': [[99, 1], [499, 2], [1000, 5], [10000, 10]]}) + fapipublic_get_markpriceklines = fapiPublicGetMarkPriceKlines = Entry('markPriceKlines', 'fapiPublic', 'GET', {'cost': 1, 'byLimit': [[99, 1], [499, 2], [1000, 5], [10000, 10]]}) + fapipublic_get_indexpriceklines = fapiPublicGetIndexPriceKlines = Entry('indexPriceKlines', 'fapiPublic', 'GET', {'cost': 1, 'byLimit': [[99, 1], [499, 2], [1000, 5], [10000, 10]]}) + fapipublic_get_premiumindexklines = fapiPublicGetPremiumIndexKlines = Entry('premiumIndexKlines', 'fapiPublic', 'GET', {'cost': 1, 'byLimit': [[99, 1], [499, 2], [1000, 5], [10000, 10]]}) + fapipublic_get_fundingrate = fapiPublicGetFundingRate = Entry('fundingRate', 'fapiPublic', 'GET', {'cost': 1}) + fapipublic_get_fundinginfo = fapiPublicGetFundingInfo = Entry('fundingInfo', 'fapiPublic', 'GET', {'cost': 1}) + fapipublic_get_premiumindex = fapiPublicGetPremiumIndex = Entry('premiumIndex', 'fapiPublic', 'GET', {'cost': 1}) + fapipublic_get_ticker_24hr = fapiPublicGetTicker24hr = Entry('ticker/24hr', 'fapiPublic', 'GET', {'cost': 1, 'noSymbol': 40}) + fapipublic_get_ticker_price = fapiPublicGetTickerPrice = Entry('ticker/price', 'fapiPublic', 'GET', {'cost': 1, 'noSymbol': 2}) + fapipublic_get_ticker_bookticker = fapiPublicGetTickerBookTicker = Entry('ticker/bookTicker', 'fapiPublic', 'GET', {'cost': 1, 'noSymbol': 2}) + fapipublic_get_openinterest = fapiPublicGetOpenInterest = Entry('openInterest', 'fapiPublic', 'GET', {'cost': 1}) + fapipublic_get_indexinfo = fapiPublicGetIndexInfo = Entry('indexInfo', 'fapiPublic', 'GET', {'cost': 1}) + fapipublic_get_assetindex = fapiPublicGetAssetIndex = Entry('assetIndex', 'fapiPublic', 'GET', {'cost': 1, 'noSymbol': 10}) + fapipublic_get_constituents = fapiPublicGetConstituents = Entry('constituents', 'fapiPublic', 'GET', {'cost': 2}) + fapipublic_get_apitradingstatus = fapiPublicGetApiTradingStatus = Entry('apiTradingStatus', 'fapiPublic', 'GET', {'cost': 1, 'noSymbol': 10}) + fapipublic_get_lvtklines = fapiPublicGetLvtKlines = Entry('lvtKlines', 'fapiPublic', 'GET', {'cost': 1}) + fapipublic_get_convert_exchangeinfo = fapiPublicGetConvertExchangeInfo = Entry('convert/exchangeInfo', 'fapiPublic', 'GET', {'cost': 4}) + fapipublic_get_insurancebalance = fapiPublicGetInsuranceBalance = Entry('insuranceBalance', 'fapiPublic', 'GET', {'cost': 1}) + fapidata_get_delivery_price = fapiDataGetDeliveryPrice = Entry('delivery-price', 'fapiData', 'GET', {'cost': 1}) + fapidata_get_openinteresthist = fapiDataGetOpenInterestHist = Entry('openInterestHist', 'fapiData', 'GET', {'cost': 1}) + fapidata_get_toplongshortaccountratio = fapiDataGetTopLongShortAccountRatio = Entry('topLongShortAccountRatio', 'fapiData', 'GET', {'cost': 1}) + fapidata_get_toplongshortpositionratio = fapiDataGetTopLongShortPositionRatio = Entry('topLongShortPositionRatio', 'fapiData', 'GET', {'cost': 1}) + fapidata_get_globallongshortaccountratio = fapiDataGetGlobalLongShortAccountRatio = Entry('globalLongShortAccountRatio', 'fapiData', 'GET', {'cost': 1}) + fapidata_get_takerlongshortratio = fapiDataGetTakerlongshortRatio = Entry('takerlongshortRatio', 'fapiData', 'GET', {'cost': 1}) + fapidata_get_basis = fapiDataGetBasis = Entry('basis', 'fapiData', 'GET', {'cost': 1}) + fapiprivate_get_forceorders = fapiPrivateGetForceOrders = Entry('forceOrders', 'fapiPrivate', 'GET', {'cost': 20, 'noSymbol': 50}) + fapiprivate_get_allorders = fapiPrivateGetAllOrders = Entry('allOrders', 'fapiPrivate', 'GET', {'cost': 5}) + fapiprivate_get_openorder = fapiPrivateGetOpenOrder = Entry('openOrder', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_openorders = fapiPrivateGetOpenOrders = Entry('openOrders', 'fapiPrivate', 'GET', {'cost': 1, 'noSymbol': 40}) + fapiprivate_get_order = fapiPrivateGetOrder = Entry('order', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_account = fapiPrivateGetAccount = Entry('account', 'fapiPrivate', 'GET', {'cost': 5}) + fapiprivate_get_balance = fapiPrivateGetBalance = Entry('balance', 'fapiPrivate', 'GET', {'cost': 5}) + fapiprivate_get_leveragebracket = fapiPrivateGetLeverageBracket = Entry('leverageBracket', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_positionmargin_history = fapiPrivateGetPositionMarginHistory = Entry('positionMargin/history', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_positionrisk = fapiPrivateGetPositionRisk = Entry('positionRisk', 'fapiPrivate', 'GET', {'cost': 5}) + fapiprivate_get_positionside_dual = fapiPrivateGetPositionSideDual = Entry('positionSide/dual', 'fapiPrivate', 'GET', {'cost': 30}) + fapiprivate_get_usertrades = fapiPrivateGetUserTrades = Entry('userTrades', 'fapiPrivate', 'GET', {'cost': 5}) + fapiprivate_get_income = fapiPrivateGetIncome = Entry('income', 'fapiPrivate', 'GET', {'cost': 30}) + fapiprivate_get_commissionrate = fapiPrivateGetCommissionRate = Entry('commissionRate', 'fapiPrivate', 'GET', {'cost': 20}) + fapiprivate_get_ratelimit_order = fapiPrivateGetRateLimitOrder = Entry('rateLimit/order', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_apitradingstatus = fapiPrivateGetApiTradingStatus = Entry('apiTradingStatus', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_multiassetsmargin = fapiPrivateGetMultiAssetsMargin = Entry('multiAssetsMargin', 'fapiPrivate', 'GET', {'cost': 30}) + fapiprivate_get_apireferral_ifnewuser = fapiPrivateGetApiReferralIfNewUser = Entry('apiReferral/ifNewUser', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_apireferral_customization = fapiPrivateGetApiReferralCustomization = Entry('apiReferral/customization', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_apireferral_usercustomization = fapiPrivateGetApiReferralUserCustomization = Entry('apiReferral/userCustomization', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_apireferral_tradernum = fapiPrivateGetApiReferralTraderNum = Entry('apiReferral/traderNum', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_apireferral_overview = fapiPrivateGetApiReferralOverview = Entry('apiReferral/overview', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_apireferral_tradevol = fapiPrivateGetApiReferralTradeVol = Entry('apiReferral/tradeVol', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_apireferral_rebatevol = fapiPrivateGetApiReferralRebateVol = Entry('apiReferral/rebateVol', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_apireferral_tradersummary = fapiPrivateGetApiReferralTraderSummary = Entry('apiReferral/traderSummary', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_adlquantile = fapiPrivateGetAdlQuantile = Entry('adlQuantile', 'fapiPrivate', 'GET', {'cost': 5}) + fapiprivate_get_pmaccountinfo = fapiPrivateGetPmAccountInfo = Entry('pmAccountInfo', 'fapiPrivate', 'GET', {'cost': 5}) + fapiprivate_get_orderamendment = fapiPrivateGetOrderAmendment = Entry('orderAmendment', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_income_asyn = fapiPrivateGetIncomeAsyn = Entry('income/asyn', 'fapiPrivate', 'GET', {'cost': 1000}) + fapiprivate_get_income_asyn_id = fapiPrivateGetIncomeAsynId = Entry('income/asyn/id', 'fapiPrivate', 'GET', {'cost': 10}) + fapiprivate_get_order_asyn = fapiPrivateGetOrderAsyn = Entry('order/asyn', 'fapiPrivate', 'GET', {'cost': 1000}) + fapiprivate_get_order_asyn_id = fapiPrivateGetOrderAsynId = Entry('order/asyn/id', 'fapiPrivate', 'GET', {'cost': 10}) + fapiprivate_get_trade_asyn = fapiPrivateGetTradeAsyn = Entry('trade/asyn', 'fapiPrivate', 'GET', {'cost': 1000}) + fapiprivate_get_trade_asyn_id = fapiPrivateGetTradeAsynId = Entry('trade/asyn/id', 'fapiPrivate', 'GET', {'cost': 10}) + fapiprivate_get_feeburn = fapiPrivateGetFeeBurn = Entry('feeBurn', 'fapiPrivate', 'GET', {'cost': 1}) + fapiprivate_get_symbolconfig = fapiPrivateGetSymbolConfig = Entry('symbolConfig', 'fapiPrivate', 'GET', {'cost': 5}) + fapiprivate_get_accountconfig = fapiPrivateGetAccountConfig = Entry('accountConfig', 'fapiPrivate', 'GET', {'cost': 5}) + fapiprivate_get_convert_orderstatus = fapiPrivateGetConvertOrderStatus = Entry('convert/orderStatus', 'fapiPrivate', 'GET', {'cost': 5}) + fapiprivate_post_batchorders = fapiPrivatePostBatchOrders = Entry('batchOrders', 'fapiPrivate', 'POST', {'cost': 5}) + fapiprivate_post_positionside_dual = fapiPrivatePostPositionSideDual = Entry('positionSide/dual', 'fapiPrivate', 'POST', {'cost': 1}) + fapiprivate_post_positionmargin = fapiPrivatePostPositionMargin = Entry('positionMargin', 'fapiPrivate', 'POST', {'cost': 1}) + fapiprivate_post_margintype = fapiPrivatePostMarginType = Entry('marginType', 'fapiPrivate', 'POST', {'cost': 1}) + fapiprivate_post_order = fapiPrivatePostOrder = Entry('order', 'fapiPrivate', 'POST', {'cost': 4}) + fapiprivate_post_leverage = fapiPrivatePostLeverage = Entry('leverage', 'fapiPrivate', 'POST', {'cost': 1}) + fapiprivate_post_listenkey = fapiPrivatePostListenKey = Entry('listenKey', 'fapiPrivate', 'POST', {'cost': 1}) + fapiprivate_post_countdowncancelall = fapiPrivatePostCountdownCancelAll = Entry('countdownCancelAll', 'fapiPrivate', 'POST', {'cost': 10}) + fapiprivate_post_multiassetsmargin = fapiPrivatePostMultiAssetsMargin = Entry('multiAssetsMargin', 'fapiPrivate', 'POST', {'cost': 1}) + fapiprivate_post_apireferral_customization = fapiPrivatePostApiReferralCustomization = Entry('apiReferral/customization', 'fapiPrivate', 'POST', {'cost': 1}) + fapiprivate_post_apireferral_usercustomization = fapiPrivatePostApiReferralUserCustomization = Entry('apiReferral/userCustomization', 'fapiPrivate', 'POST', {'cost': 1}) + fapiprivate_post_feeburn = fapiPrivatePostFeeBurn = Entry('feeBurn', 'fapiPrivate', 'POST', {'cost': 1}) + fapiprivate_post_convert_getquote = fapiPrivatePostConvertGetQuote = Entry('convert/getQuote', 'fapiPrivate', 'POST', {'cost': 200}) + fapiprivate_post_convert_acceptquote = fapiPrivatePostConvertAcceptQuote = Entry('convert/acceptQuote', 'fapiPrivate', 'POST', {'cost': 20}) + fapiprivate_put_listenkey = fapiPrivatePutListenKey = Entry('listenKey', 'fapiPrivate', 'PUT', {'cost': 1}) + fapiprivate_put_order = fapiPrivatePutOrder = Entry('order', 'fapiPrivate', 'PUT', {'cost': 1}) + fapiprivate_put_batchorders = fapiPrivatePutBatchOrders = Entry('batchOrders', 'fapiPrivate', 'PUT', {'cost': 5}) + fapiprivate_delete_batchorders = fapiPrivateDeleteBatchOrders = Entry('batchOrders', 'fapiPrivate', 'DELETE', {'cost': 1}) + fapiprivate_delete_order = fapiPrivateDeleteOrder = Entry('order', 'fapiPrivate', 'DELETE', {'cost': 1}) + fapiprivate_delete_allopenorders = fapiPrivateDeleteAllOpenOrders = Entry('allOpenOrders', 'fapiPrivate', 'DELETE', {'cost': 1}) + fapiprivate_delete_listenkey = fapiPrivateDeleteListenKey = Entry('listenKey', 'fapiPrivate', 'DELETE', {'cost': 1}) + fapipublicv2_get_ticker_price = fapiPublicV2GetTickerPrice = Entry('ticker/price', 'fapiPublicV2', 'GET', {'cost': 0}) + fapiprivatev2_get_account = fapiPrivateV2GetAccount = Entry('account', 'fapiPrivateV2', 'GET', {'cost': 1}) + fapiprivatev2_get_balance = fapiPrivateV2GetBalance = Entry('balance', 'fapiPrivateV2', 'GET', {'cost': 1}) + fapiprivatev2_get_positionrisk = fapiPrivateV2GetPositionRisk = Entry('positionRisk', 'fapiPrivateV2', 'GET', {'cost': 1}) + fapiprivatev3_get_account = fapiPrivateV3GetAccount = Entry('account', 'fapiPrivateV3', 'GET', {'cost': 1}) + fapiprivatev3_get_balance = fapiPrivateV3GetBalance = Entry('balance', 'fapiPrivateV3', 'GET', {'cost': 1}) + fapiprivatev3_get_positionrisk = fapiPrivateV3GetPositionRisk = Entry('positionRisk', 'fapiPrivateV3', 'GET', {'cost': 1}) + eapipublic_get_ping = eapiPublicGetPing = Entry('ping', 'eapiPublic', 'GET', {'cost': 1}) + eapipublic_get_time = eapiPublicGetTime = Entry('time', 'eapiPublic', 'GET', {'cost': 1}) + eapipublic_get_exchangeinfo = eapiPublicGetExchangeInfo = Entry('exchangeInfo', 'eapiPublic', 'GET', {'cost': 1}) + eapipublic_get_index = eapiPublicGetIndex = Entry('index', 'eapiPublic', 'GET', {'cost': 1}) + eapipublic_get_ticker = eapiPublicGetTicker = Entry('ticker', 'eapiPublic', 'GET', {'cost': 5}) + eapipublic_get_mark = eapiPublicGetMark = Entry('mark', 'eapiPublic', 'GET', {'cost': 5}) + eapipublic_get_depth = eapiPublicGetDepth = Entry('depth', 'eapiPublic', 'GET', {'cost': 1}) + eapipublic_get_klines = eapiPublicGetKlines = Entry('klines', 'eapiPublic', 'GET', {'cost': 1}) + eapipublic_get_trades = eapiPublicGetTrades = Entry('trades', 'eapiPublic', 'GET', {'cost': 5}) + eapipublic_get_historicaltrades = eapiPublicGetHistoricalTrades = Entry('historicalTrades', 'eapiPublic', 'GET', {'cost': 20}) + eapipublic_get_exercisehistory = eapiPublicGetExerciseHistory = Entry('exerciseHistory', 'eapiPublic', 'GET', {'cost': 3}) + eapipublic_get_openinterest = eapiPublicGetOpenInterest = Entry('openInterest', 'eapiPublic', 'GET', {'cost': 3}) + eapiprivate_get_account = eapiPrivateGetAccount = Entry('account', 'eapiPrivate', 'GET', {'cost': 3}) + eapiprivate_get_position = eapiPrivateGetPosition = Entry('position', 'eapiPrivate', 'GET', {'cost': 5}) + eapiprivate_get_openorders = eapiPrivateGetOpenOrders = Entry('openOrders', 'eapiPrivate', 'GET', {'cost': 1, 'noSymbol': 40}) + eapiprivate_get_historyorders = eapiPrivateGetHistoryOrders = Entry('historyOrders', 'eapiPrivate', 'GET', {'cost': 3}) + eapiprivate_get_usertrades = eapiPrivateGetUserTrades = Entry('userTrades', 'eapiPrivate', 'GET', {'cost': 5}) + eapiprivate_get_exerciserecord = eapiPrivateGetExerciseRecord = Entry('exerciseRecord', 'eapiPrivate', 'GET', {'cost': 5}) + eapiprivate_get_bill = eapiPrivateGetBill = Entry('bill', 'eapiPrivate', 'GET', {'cost': 1}) + eapiprivate_get_income_asyn = eapiPrivateGetIncomeAsyn = Entry('income/asyn', 'eapiPrivate', 'GET', {'cost': 5}) + eapiprivate_get_income_asyn_id = eapiPrivateGetIncomeAsynId = Entry('income/asyn/id', 'eapiPrivate', 'GET', {'cost': 5}) + eapiprivate_get_marginaccount = eapiPrivateGetMarginAccount = Entry('marginAccount', 'eapiPrivate', 'GET', {'cost': 3}) + eapiprivate_get_mmp = eapiPrivateGetMmp = Entry('mmp', 'eapiPrivate', 'GET', {'cost': 1}) + eapiprivate_get_countdowncancelall = eapiPrivateGetCountdownCancelAll = Entry('countdownCancelAll', 'eapiPrivate', 'GET', {'cost': 1}) + eapiprivate_get_order = eapiPrivateGetOrder = Entry('order', 'eapiPrivate', 'GET', {'cost': 1}) + eapiprivate_get_block_order_orders = eapiPrivateGetBlockOrderOrders = Entry('block/order/orders', 'eapiPrivate', 'GET', {'cost': 5}) + eapiprivate_get_block_order_execute = eapiPrivateGetBlockOrderExecute = Entry('block/order/execute', 'eapiPrivate', 'GET', {'cost': 5}) + eapiprivate_get_block_user_trades = eapiPrivateGetBlockUserTrades = Entry('block/user-trades', 'eapiPrivate', 'GET', {'cost': 5}) + eapiprivate_get_blocktrades = eapiPrivateGetBlockTrades = Entry('blockTrades', 'eapiPrivate', 'GET', {'cost': 5}) + eapiprivate_post_order = eapiPrivatePostOrder = Entry('order', 'eapiPrivate', 'POST', {'cost': 1}) + eapiprivate_post_batchorders = eapiPrivatePostBatchOrders = Entry('batchOrders', 'eapiPrivate', 'POST', {'cost': 5}) + eapiprivate_post_listenkey = eapiPrivatePostListenKey = Entry('listenKey', 'eapiPrivate', 'POST', {'cost': 1}) + eapiprivate_post_mmpset = eapiPrivatePostMmpSet = Entry('mmpSet', 'eapiPrivate', 'POST', {'cost': 1}) + eapiprivate_post_mmpreset = eapiPrivatePostMmpReset = Entry('mmpReset', 'eapiPrivate', 'POST', {'cost': 1}) + eapiprivate_post_countdowncancelall = eapiPrivatePostCountdownCancelAll = Entry('countdownCancelAll', 'eapiPrivate', 'POST', {'cost': 1}) + eapiprivate_post_countdowncancelallheartbeat = eapiPrivatePostCountdownCancelAllHeartBeat = Entry('countdownCancelAllHeartBeat', 'eapiPrivate', 'POST', {'cost': 10}) + eapiprivate_post_block_order_create = eapiPrivatePostBlockOrderCreate = Entry('block/order/create', 'eapiPrivate', 'POST', {'cost': 5}) + eapiprivate_post_block_order_execute = eapiPrivatePostBlockOrderExecute = Entry('block/order/execute', 'eapiPrivate', 'POST', {'cost': 5}) + eapiprivate_put_listenkey = eapiPrivatePutListenKey = Entry('listenKey', 'eapiPrivate', 'PUT', {'cost': 1}) + eapiprivate_put_block_order_create = eapiPrivatePutBlockOrderCreate = Entry('block/order/create', 'eapiPrivate', 'PUT', {'cost': 5}) + eapiprivate_delete_order = eapiPrivateDeleteOrder = Entry('order', 'eapiPrivate', 'DELETE', {'cost': 1}) + eapiprivate_delete_batchorders = eapiPrivateDeleteBatchOrders = Entry('batchOrders', 'eapiPrivate', 'DELETE', {'cost': 1}) + eapiprivate_delete_allopenorders = eapiPrivateDeleteAllOpenOrders = Entry('allOpenOrders', 'eapiPrivate', 'DELETE', {'cost': 1}) + eapiprivate_delete_allopenordersbyunderlying = eapiPrivateDeleteAllOpenOrdersByUnderlying = Entry('allOpenOrdersByUnderlying', 'eapiPrivate', 'DELETE', {'cost': 1}) + eapiprivate_delete_listenkey = eapiPrivateDeleteListenKey = Entry('listenKey', 'eapiPrivate', 'DELETE', {'cost': 1}) + eapiprivate_delete_block_order_create = eapiPrivateDeleteBlockOrderCreate = Entry('block/order/create', 'eapiPrivate', 'DELETE', {'cost': 5}) + public_get_ping = publicGetPing = Entry('ping', 'public', 'GET', {'cost': 0.2}) + public_get_time = publicGetTime = Entry('time', 'public', 'GET', {'cost': 0.2}) + public_get_depth = publicGetDepth = Entry('depth', 'public', 'GET', {'cost': 1, 'byLimit': [[100, 1], [500, 5], [1000, 10], [5000, 50]]}) + public_get_trades = publicGetTrades = Entry('trades', 'public', 'GET', {'cost': 2}) + public_get_aggtrades = publicGetAggTrades = Entry('aggTrades', 'public', 'GET', {'cost': 0.4}) + public_get_historicaltrades = publicGetHistoricalTrades = Entry('historicalTrades', 'public', 'GET', {'cost': 2}) + public_get_klines = publicGetKlines = Entry('klines', 'public', 'GET', {'cost': 0.4}) + public_get_uiklines = publicGetUiKlines = Entry('uiKlines', 'public', 'GET', {'cost': 0.4}) + public_get_ticker_24hr = publicGetTicker24hr = Entry('ticker/24hr', 'public', 'GET', {'cost': 0.4, 'noSymbol': 16}) + public_get_ticker = publicGetTicker = Entry('ticker', 'public', 'GET', {'cost': 0.4, 'noSymbol': 16}) + public_get_ticker_tradingday = publicGetTickerTradingDay = Entry('ticker/tradingDay', 'public', 'GET', {'cost': 0.8}) + public_get_ticker_price = publicGetTickerPrice = Entry('ticker/price', 'public', 'GET', {'cost': 0.4, 'noSymbol': 0.8}) + public_get_ticker_bookticker = publicGetTickerBookTicker = Entry('ticker/bookTicker', 'public', 'GET', {'cost': 0.4, 'noSymbol': 0.8}) + public_get_exchangeinfo = publicGetExchangeInfo = Entry('exchangeInfo', 'public', 'GET', {'cost': 4}) + public_get_avgprice = publicGetAvgPrice = Entry('avgPrice', 'public', 'GET', {'cost': 0.4}) + public_put_userdatastream = publicPutUserDataStream = Entry('userDataStream', 'public', 'PUT', {'cost': 0.4}) + public_post_userdatastream = publicPostUserDataStream = Entry('userDataStream', 'public', 'POST', {'cost': 0.4}) + public_delete_userdatastream = publicDeleteUserDataStream = Entry('userDataStream', 'public', 'DELETE', {'cost': 0.4}) + private_get_allorderlist = privateGetAllOrderList = Entry('allOrderList', 'private', 'GET', {'cost': 4}) + private_get_openorderlist = privateGetOpenOrderList = Entry('openOrderList', 'private', 'GET', {'cost': 1.2}) + private_get_orderlist = privateGetOrderList = Entry('orderList', 'private', 'GET', {'cost': 0.8}) + private_get_order = privateGetOrder = Entry('order', 'private', 'GET', {'cost': 0.8}) + private_get_openorders = privateGetOpenOrders = Entry('openOrders', 'private', 'GET', {'cost': 1.2, 'noSymbol': 16}) + private_get_allorders = privateGetAllOrders = Entry('allOrders', 'private', 'GET', {'cost': 4}) + private_get_account = privateGetAccount = Entry('account', 'private', 'GET', {'cost': 4}) + private_get_mytrades = privateGetMyTrades = Entry('myTrades', 'private', 'GET', {'cost': 4}) + private_get_ratelimit_order = privateGetRateLimitOrder = Entry('rateLimit/order', 'private', 'GET', {'cost': 8}) + private_get_mypreventedmatches = privateGetMyPreventedMatches = Entry('myPreventedMatches', 'private', 'GET', {'cost': 4}) + private_get_myallocations = privateGetMyAllocations = Entry('myAllocations', 'private', 'GET', {'cost': 4}) + private_get_account_commission = privateGetAccountCommission = Entry('account/commission', 'private', 'GET', {'cost': 4}) + private_post_order_oco = privatePostOrderOco = Entry('order/oco', 'private', 'POST', {'cost': 0.2}) + private_post_orderlist_oco = privatePostOrderListOco = Entry('orderList/oco', 'private', 'POST', {'cost': 0.2}) + private_post_orderlist_oto = privatePostOrderListOto = Entry('orderList/oto', 'private', 'POST', {'cost': 0.2}) + private_post_orderlist_otoco = privatePostOrderListOtoco = Entry('orderList/otoco', 'private', 'POST', {'cost': 0.2}) + private_post_sor_order = privatePostSorOrder = Entry('sor/order', 'private', 'POST', {'cost': 0.2}) + private_post_sor_order_test = privatePostSorOrderTest = Entry('sor/order/test', 'private', 'POST', {'cost': 0.2}) + private_post_order = privatePostOrder = Entry('order', 'private', 'POST', {'cost': 0.2}) + private_post_order_cancelreplace = privatePostOrderCancelReplace = Entry('order/cancelReplace', 'private', 'POST', {'cost': 0.2}) + private_post_order_test = privatePostOrderTest = Entry('order/test', 'private', 'POST', {'cost': 0.2}) + private_delete_openorders = privateDeleteOpenOrders = Entry('openOrders', 'private', 'DELETE', {'cost': 0.2}) + private_delete_orderlist = privateDeleteOrderList = Entry('orderList', 'private', 'DELETE', {'cost': 0.2}) + private_delete_order = privateDeleteOrder = Entry('order', 'private', 'DELETE', {'cost': 0.2}) + papi_get_ping = papiGetPing = Entry('ping', 'papi', 'GET', {'cost': 0.2}) + papi_get_um_order = papiGetUmOrder = Entry('um/order', 'papi', 'GET', {'cost': 1}) + papi_get_um_openorder = papiGetUmOpenOrder = Entry('um/openOrder', 'papi', 'GET', {'cost': 1}) + papi_get_um_openorders = papiGetUmOpenOrders = Entry('um/openOrders', 'papi', 'GET', {'cost': 1, 'noSymbol': 40}) + papi_get_um_allorders = papiGetUmAllOrders = Entry('um/allOrders', 'papi', 'GET', {'cost': 5}) + papi_get_cm_order = papiGetCmOrder = Entry('cm/order', 'papi', 'GET', {'cost': 1}) + papi_get_cm_openorder = papiGetCmOpenOrder = Entry('cm/openOrder', 'papi', 'GET', {'cost': 1}) + papi_get_cm_openorders = papiGetCmOpenOrders = Entry('cm/openOrders', 'papi', 'GET', {'cost': 1, 'noSymbol': 40}) + papi_get_cm_allorders = papiGetCmAllOrders = Entry('cm/allOrders', 'papi', 'GET', {'cost': 20}) + papi_get_um_conditional_openorder = papiGetUmConditionalOpenOrder = Entry('um/conditional/openOrder', 'papi', 'GET', {'cost': 1}) + papi_get_um_conditional_openorders = papiGetUmConditionalOpenOrders = Entry('um/conditional/openOrders', 'papi', 'GET', {'cost': 1, 'noSymbol': 40}) + papi_get_um_conditional_orderhistory = papiGetUmConditionalOrderHistory = Entry('um/conditional/orderHistory', 'papi', 'GET', {'cost': 1}) + papi_get_um_conditional_allorders = papiGetUmConditionalAllOrders = Entry('um/conditional/allOrders', 'papi', 'GET', {'cost': 1, 'noSymbol': 40}) + papi_get_cm_conditional_openorder = papiGetCmConditionalOpenOrder = Entry('cm/conditional/openOrder', 'papi', 'GET', {'cost': 1}) + papi_get_cm_conditional_openorders = papiGetCmConditionalOpenOrders = Entry('cm/conditional/openOrders', 'papi', 'GET', {'cost': 1, 'noSymbol': 40}) + papi_get_cm_conditional_orderhistory = papiGetCmConditionalOrderHistory = Entry('cm/conditional/orderHistory', 'papi', 'GET', {'cost': 1}) + papi_get_cm_conditional_allorders = papiGetCmConditionalAllOrders = Entry('cm/conditional/allOrders', 'papi', 'GET', {'cost': 40}) + papi_get_margin_order = papiGetMarginOrder = Entry('margin/order', 'papi', 'GET', {'cost': 10}) + papi_get_margin_openorders = papiGetMarginOpenOrders = Entry('margin/openOrders', 'papi', 'GET', {'cost': 5}) + papi_get_margin_allorders = papiGetMarginAllOrders = Entry('margin/allOrders', 'papi', 'GET', {'cost': 100}) + papi_get_margin_orderlist = papiGetMarginOrderList = Entry('margin/orderList', 'papi', 'GET', {'cost': 5}) + papi_get_margin_allorderlist = papiGetMarginAllOrderList = Entry('margin/allOrderList', 'papi', 'GET', {'cost': 100}) + papi_get_margin_openorderlist = papiGetMarginOpenOrderList = Entry('margin/openOrderList', 'papi', 'GET', {'cost': 5}) + papi_get_margin_mytrades = papiGetMarginMyTrades = Entry('margin/myTrades', 'papi', 'GET', {'cost': 5}) + papi_get_balance = papiGetBalance = Entry('balance', 'papi', 'GET', {'cost': 4}) + papi_get_account = papiGetAccount = Entry('account', 'papi', 'GET', {'cost': 4}) + papi_get_margin_maxborrowable = papiGetMarginMaxBorrowable = Entry('margin/maxBorrowable', 'papi', 'GET', {'cost': 1}) + papi_get_margin_maxwithdraw = papiGetMarginMaxWithdraw = Entry('margin/maxWithdraw', 'papi', 'GET', {'cost': 1}) + papi_get_um_positionrisk = papiGetUmPositionRisk = Entry('um/positionRisk', 'papi', 'GET', {'cost': 1}) + papi_get_cm_positionrisk = papiGetCmPositionRisk = Entry('cm/positionRisk', 'papi', 'GET', {'cost': 0.2}) + papi_get_um_positionside_dual = papiGetUmPositionSideDual = Entry('um/positionSide/dual', 'papi', 'GET', {'cost': 6}) + papi_get_cm_positionside_dual = papiGetCmPositionSideDual = Entry('cm/positionSide/dual', 'papi', 'GET', {'cost': 6}) + papi_get_um_usertrades = papiGetUmUserTrades = Entry('um/userTrades', 'papi', 'GET', {'cost': 5}) + papi_get_cm_usertrades = papiGetCmUserTrades = Entry('cm/userTrades', 'papi', 'GET', {'cost': 20}) + papi_get_um_leveragebracket = papiGetUmLeverageBracket = Entry('um/leverageBracket', 'papi', 'GET', {'cost': 0.2}) + papi_get_cm_leveragebracket = papiGetCmLeverageBracket = Entry('cm/leverageBracket', 'papi', 'GET', {'cost': 0.2}) + papi_get_margin_forceorders = papiGetMarginForceOrders = Entry('margin/forceOrders', 'papi', 'GET', {'cost': 1}) + papi_get_um_forceorders = papiGetUmForceOrders = Entry('um/forceOrders', 'papi', 'GET', {'cost': 20, 'noSymbol': 50}) + papi_get_cm_forceorders = papiGetCmForceOrders = Entry('cm/forceOrders', 'papi', 'GET', {'cost': 20, 'noSymbol': 50}) + papi_get_um_apitradingstatus = papiGetUmApiTradingStatus = Entry('um/apiTradingStatus', 'papi', 'GET', {'cost': 0.2, 'noSymbol': 2}) + papi_get_um_commissionrate = papiGetUmCommissionRate = Entry('um/commissionRate', 'papi', 'GET', {'cost': 4}) + papi_get_cm_commissionrate = papiGetCmCommissionRate = Entry('cm/commissionRate', 'papi', 'GET', {'cost': 4}) + papi_get_margin_marginloan = papiGetMarginMarginLoan = Entry('margin/marginLoan', 'papi', 'GET', {'cost': 2}) + papi_get_margin_repayloan = papiGetMarginRepayLoan = Entry('margin/repayLoan', 'papi', 'GET', {'cost': 2}) + papi_get_margin_margininteresthistory = papiGetMarginMarginInterestHistory = Entry('margin/marginInterestHistory', 'papi', 'GET', {'cost': 0.2}) + papi_get_portfolio_interest_history = papiGetPortfolioInterestHistory = Entry('portfolio/interest-history', 'papi', 'GET', {'cost': 10}) + papi_get_um_income = papiGetUmIncome = Entry('um/income', 'papi', 'GET', {'cost': 6}) + papi_get_cm_income = papiGetCmIncome = Entry('cm/income', 'papi', 'GET', {'cost': 6}) + papi_get_um_account = papiGetUmAccount = Entry('um/account', 'papi', 'GET', {'cost': 1}) + papi_get_cm_account = papiGetCmAccount = Entry('cm/account', 'papi', 'GET', {'cost': 1}) + papi_get_repay_futures_switch = papiGetRepayFuturesSwitch = Entry('repay-futures-switch', 'papi', 'GET', {'cost': 6}) + papi_get_um_adlquantile = papiGetUmAdlQuantile = Entry('um/adlQuantile', 'papi', 'GET', {'cost': 5}) + papi_get_cm_adlquantile = papiGetCmAdlQuantile = Entry('cm/adlQuantile', 'papi', 'GET', {'cost': 5}) + papi_get_um_trade_asyn = papiGetUmTradeAsyn = Entry('um/trade/asyn', 'papi', 'GET', {'cost': 300}) + papi_get_um_trade_asyn_id = papiGetUmTradeAsynId = Entry('um/trade/asyn/id', 'papi', 'GET', {'cost': 2}) + papi_get_um_order_asyn = papiGetUmOrderAsyn = Entry('um/order/asyn', 'papi', 'GET', {'cost': 300}) + papi_get_um_order_asyn_id = papiGetUmOrderAsynId = Entry('um/order/asyn/id', 'papi', 'GET', {'cost': 2}) + papi_get_um_income_asyn = papiGetUmIncomeAsyn = Entry('um/income/asyn', 'papi', 'GET', {'cost': 300}) + papi_get_um_income_asyn_id = papiGetUmIncomeAsynId = Entry('um/income/asyn/id', 'papi', 'GET', {'cost': 2}) + papi_get_um_orderamendment = papiGetUmOrderAmendment = Entry('um/orderAmendment', 'papi', 'GET', {'cost': 1}) + papi_get_cm_orderamendment = papiGetCmOrderAmendment = Entry('cm/orderAmendment', 'papi', 'GET', {'cost': 1}) + papi_get_um_feeburn = papiGetUmFeeBurn = Entry('um/feeBurn', 'papi', 'GET', {'cost': 30}) + papi_get_um_accountconfig = papiGetUmAccountConfig = Entry('um/accountConfig', 'papi', 'GET', {'cost': 1}) + papi_get_um_symbolconfig = papiGetUmSymbolConfig = Entry('um/symbolConfig', 'papi', 'GET', {'cost': 1}) + papi_get_cm_accountconfig = papiGetCmAccountConfig = Entry('cm/accountConfig', 'papi', 'GET', {'cost': 1}) + papi_get_cm_symbolconfig = papiGetCmSymbolConfig = Entry('cm/symbolConfig', 'papi', 'GET', {'cost': 1}) + papi_get_ratelimit_order = papiGetRateLimitOrder = Entry('rateLimit/order', 'papi', 'GET', {'cost': 1}) + papi_post_um_order = papiPostUmOrder = Entry('um/order', 'papi', 'POST', {'cost': 1}) + papi_post_um_conditional_order = papiPostUmConditionalOrder = Entry('um/conditional/order', 'papi', 'POST', {'cost': 1}) + papi_post_cm_order = papiPostCmOrder = Entry('cm/order', 'papi', 'POST', {'cost': 1}) + papi_post_cm_conditional_order = papiPostCmConditionalOrder = Entry('cm/conditional/order', 'papi', 'POST', {'cost': 1}) + papi_post_margin_order = papiPostMarginOrder = Entry('margin/order', 'papi', 'POST', {'cost': 1}) + papi_post_marginloan = papiPostMarginLoan = Entry('marginLoan', 'papi', 'POST', {'cost': 100}) + papi_post_repayloan = papiPostRepayLoan = Entry('repayLoan', 'papi', 'POST', {'cost': 100}) + papi_post_margin_order_oco = papiPostMarginOrderOco = Entry('margin/order/oco', 'papi', 'POST', {'cost': 1}) + papi_post_um_leverage = papiPostUmLeverage = Entry('um/leverage', 'papi', 'POST', {'cost': 0.2}) + papi_post_cm_leverage = papiPostCmLeverage = Entry('cm/leverage', 'papi', 'POST', {'cost': 0.2}) + papi_post_um_positionside_dual = papiPostUmPositionSideDual = Entry('um/positionSide/dual', 'papi', 'POST', {'cost': 0.2}) + papi_post_cm_positionside_dual = papiPostCmPositionSideDual = Entry('cm/positionSide/dual', 'papi', 'POST', {'cost': 0.2}) + papi_post_auto_collection = papiPostAutoCollection = Entry('auto-collection', 'papi', 'POST', {'cost': 150}) + papi_post_bnb_transfer = papiPostBnbTransfer = Entry('bnb-transfer', 'papi', 'POST', {'cost': 150}) + papi_post_repay_futures_switch = papiPostRepayFuturesSwitch = Entry('repay-futures-switch', 'papi', 'POST', {'cost': 150}) + papi_post_repay_futures_negative_balance = papiPostRepayFuturesNegativeBalance = Entry('repay-futures-negative-balance', 'papi', 'POST', {'cost': 150}) + papi_post_listenkey = papiPostListenKey = Entry('listenKey', 'papi', 'POST', {'cost': 0.2}) + papi_post_asset_collection = papiPostAssetCollection = Entry('asset-collection', 'papi', 'POST', {'cost': 6}) + papi_post_margin_repay_debt = papiPostMarginRepayDebt = Entry('margin/repay-debt', 'papi', 'POST', {'cost': 3000}) + papi_post_um_feeburn = papiPostUmFeeBurn = Entry('um/feeBurn', 'papi', 'POST', {'cost': 1}) + papi_put_listenkey = papiPutListenKey = Entry('listenKey', 'papi', 'PUT', {'cost': 0.2}) + papi_put_um_order = papiPutUmOrder = Entry('um/order', 'papi', 'PUT', {'cost': 1}) + papi_put_cm_order = papiPutCmOrder = Entry('cm/order', 'papi', 'PUT', {'cost': 1}) + papi_delete_um_order = papiDeleteUmOrder = Entry('um/order', 'papi', 'DELETE', {'cost': 1}) + papi_delete_um_conditional_order = papiDeleteUmConditionalOrder = Entry('um/conditional/order', 'papi', 'DELETE', {'cost': 1}) + papi_delete_um_allopenorders = papiDeleteUmAllOpenOrders = Entry('um/allOpenOrders', 'papi', 'DELETE', {'cost': 1}) + papi_delete_um_conditional_allopenorders = papiDeleteUmConditionalAllOpenOrders = Entry('um/conditional/allOpenOrders', 'papi', 'DELETE', {'cost': 1}) + papi_delete_cm_order = papiDeleteCmOrder = Entry('cm/order', 'papi', 'DELETE', {'cost': 1}) + papi_delete_cm_conditional_order = papiDeleteCmConditionalOrder = Entry('cm/conditional/order', 'papi', 'DELETE', {'cost': 1}) + papi_delete_cm_allopenorders = papiDeleteCmAllOpenOrders = Entry('cm/allOpenOrders', 'papi', 'DELETE', {'cost': 1}) + papi_delete_cm_conditional_allopenorders = papiDeleteCmConditionalAllOpenOrders = Entry('cm/conditional/allOpenOrders', 'papi', 'DELETE', {'cost': 1}) + papi_delete_margin_order = papiDeleteMarginOrder = Entry('margin/order', 'papi', 'DELETE', {'cost': 2}) + papi_delete_margin_allopenorders = papiDeleteMarginAllOpenOrders = Entry('margin/allOpenOrders', 'papi', 'DELETE', {'cost': 5}) + papi_delete_margin_orderlist = papiDeleteMarginOrderList = Entry('margin/orderList', 'papi', 'DELETE', {'cost': 2}) + papi_delete_listenkey = papiDeleteListenKey = Entry('listenKey', 'papi', 'DELETE', {'cost': 0.2}) + papiv2_get_um_account = papiV2GetUmAccount = Entry('um/account', 'papiV2', 'GET', {'cost': 1}) diff --git a/ccxt/abstract/bingx.py b/ccxt/abstract/bingx.py new file mode 100644 index 0000000..bc4d2e0 --- /dev/null +++ b/ccxt/abstract/bingx.py @@ -0,0 +1,179 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + fund_v1_private_get_account_balance = fundV1PrivateGetAccountBalance = Entry('account/balance', ['fund', 'v1', 'private'], 'GET', {'cost': 1}) + spot_v1_public_get_server_time = spotV1PublicGetServerTime = Entry('server/time', ['spot', 'v1', 'public'], 'GET', {'cost': 1}) + spot_v1_public_get_common_symbols = spotV1PublicGetCommonSymbols = Entry('common/symbols', ['spot', 'v1', 'public'], 'GET', {'cost': 1}) + spot_v1_public_get_market_trades = spotV1PublicGetMarketTrades = Entry('market/trades', ['spot', 'v1', 'public'], 'GET', {'cost': 1}) + spot_v1_public_get_market_depth = spotV1PublicGetMarketDepth = Entry('market/depth', ['spot', 'v1', 'public'], 'GET', {'cost': 1}) + spot_v1_public_get_market_kline = spotV1PublicGetMarketKline = Entry('market/kline', ['spot', 'v1', 'public'], 'GET', {'cost': 1}) + spot_v1_public_get_ticker_24hr = spotV1PublicGetTicker24hr = Entry('ticker/24hr', ['spot', 'v1', 'public'], 'GET', {'cost': 1}) + spot_v1_public_get_ticker_price = spotV1PublicGetTickerPrice = Entry('ticker/price', ['spot', 'v1', 'public'], 'GET', {'cost': 1}) + spot_v1_public_get_ticker_bookticker = spotV1PublicGetTickerBookTicker = Entry('ticker/bookTicker', ['spot', 'v1', 'public'], 'GET', {'cost': 1}) + spot_v1_private_get_trade_query = spotV1PrivateGetTradeQuery = Entry('trade/query', ['spot', 'v1', 'private'], 'GET', {'cost': 1}) + spot_v1_private_get_trade_openorders = spotV1PrivateGetTradeOpenOrders = Entry('trade/openOrders', ['spot', 'v1', 'private'], 'GET', {'cost': 1}) + spot_v1_private_get_trade_historyorders = spotV1PrivateGetTradeHistoryOrders = Entry('trade/historyOrders', ['spot', 'v1', 'private'], 'GET', {'cost': 1}) + spot_v1_private_get_trade_mytrades = spotV1PrivateGetTradeMyTrades = Entry('trade/myTrades', ['spot', 'v1', 'private'], 'GET', {'cost': 2}) + spot_v1_private_get_user_commissionrate = spotV1PrivateGetUserCommissionRate = Entry('user/commissionRate', ['spot', 'v1', 'private'], 'GET', {'cost': 5}) + spot_v1_private_get_account_balance = spotV1PrivateGetAccountBalance = Entry('account/balance', ['spot', 'v1', 'private'], 'GET', {'cost': 2}) + spot_v1_private_get_oco_orderlist = spotV1PrivateGetOcoOrderList = Entry('oco/orderList', ['spot', 'v1', 'private'], 'GET', {'cost': 5}) + spot_v1_private_get_oco_openorderlist = spotV1PrivateGetOcoOpenOrderList = Entry('oco/openOrderList', ['spot', 'v1', 'private'], 'GET', {'cost': 5}) + spot_v1_private_get_oco_historyorderlist = spotV1PrivateGetOcoHistoryOrderList = Entry('oco/historyOrderList', ['spot', 'v1', 'private'], 'GET', {'cost': 5}) + spot_v1_private_post_trade_order = spotV1PrivatePostTradeOrder = Entry('trade/order', ['spot', 'v1', 'private'], 'POST', {'cost': 2}) + spot_v1_private_post_trade_cancel = spotV1PrivatePostTradeCancel = Entry('trade/cancel', ['spot', 'v1', 'private'], 'POST', {'cost': 2}) + spot_v1_private_post_trade_batchorders = spotV1PrivatePostTradeBatchOrders = Entry('trade/batchOrders', ['spot', 'v1', 'private'], 'POST', {'cost': 5}) + spot_v1_private_post_trade_order_cancelreplace = spotV1PrivatePostTradeOrderCancelReplace = Entry('trade/order/cancelReplace', ['spot', 'v1', 'private'], 'POST', {'cost': 5}) + spot_v1_private_post_trade_cancelorders = spotV1PrivatePostTradeCancelOrders = Entry('trade/cancelOrders', ['spot', 'v1', 'private'], 'POST', {'cost': 5}) + spot_v1_private_post_trade_cancelopenorders = spotV1PrivatePostTradeCancelOpenOrders = Entry('trade/cancelOpenOrders', ['spot', 'v1', 'private'], 'POST', {'cost': 5}) + spot_v1_private_post_trade_cancelallafter = spotV1PrivatePostTradeCancelAllAfter = Entry('trade/cancelAllAfter', ['spot', 'v1', 'private'], 'POST', {'cost': 5}) + spot_v1_private_post_oco_order = spotV1PrivatePostOcoOrder = Entry('oco/order', ['spot', 'v1', 'private'], 'POST', {'cost': 5}) + spot_v1_private_post_oco_cancel = spotV1PrivatePostOcoCancel = Entry('oco/cancel', ['spot', 'v1', 'private'], 'POST', {'cost': 5}) + spot_v2_public_get_market_depth = spotV2PublicGetMarketDepth = Entry('market/depth', ['spot', 'v2', 'public'], 'GET', {'cost': 1}) + spot_v2_public_get_market_kline = spotV2PublicGetMarketKline = Entry('market/kline', ['spot', 'v2', 'public'], 'GET', {'cost': 1}) + spot_v3_private_get_get_asset_transfer = spotV3PrivateGetGetAssetTransfer = Entry('get/asset/transfer', ['spot', 'v3', 'private'], 'GET', {'cost': 1}) + spot_v3_private_get_asset_transfer = spotV3PrivateGetAssetTransfer = Entry('asset/transfer', ['spot', 'v3', 'private'], 'GET', {'cost': 1}) + spot_v3_private_get_capital_deposit_hisrec = spotV3PrivateGetCapitalDepositHisrec = Entry('capital/deposit/hisrec', ['spot', 'v3', 'private'], 'GET', {'cost': 1}) + spot_v3_private_get_capital_withdraw_history = spotV3PrivateGetCapitalWithdrawHistory = Entry('capital/withdraw/history', ['spot', 'v3', 'private'], 'GET', {'cost': 1}) + spot_v3_private_post_post_asset_transfer = spotV3PrivatePostPostAssetTransfer = Entry('post/asset/transfer', ['spot', 'v3', 'private'], 'POST', {'cost': 5}) + swap_v1_public_get_ticker_price = swapV1PublicGetTickerPrice = Entry('ticker/price', ['swap', 'v1', 'public'], 'GET', {'cost': 1}) + swap_v1_public_get_market_historicaltrades = swapV1PublicGetMarketHistoricalTrades = Entry('market/historicalTrades', ['swap', 'v1', 'public'], 'GET', {'cost': 1}) + swap_v1_public_get_market_markpriceklines = swapV1PublicGetMarketMarkPriceKlines = Entry('market/markPriceKlines', ['swap', 'v1', 'public'], 'GET', {'cost': 1}) + swap_v1_public_get_trade_multiassetsrules = swapV1PublicGetTradeMultiAssetsRules = Entry('trade/multiAssetsRules', ['swap', 'v1', 'public'], 'GET', {'cost': 1}) + swap_v1_public_get_tradingrules = swapV1PublicGetTradingRules = Entry('tradingRules', ['swap', 'v1', 'public'], 'GET', {'cost': 1}) + swap_v1_private_get_positionside_dual = swapV1PrivateGetPositionSideDual = Entry('positionSide/dual', ['swap', 'v1', 'private'], 'GET', {'cost': 5}) + swap_v1_private_get_trade_batchcancelreplace = swapV1PrivateGetTradeBatchCancelReplace = Entry('trade/batchCancelReplace', ['swap', 'v1', 'private'], 'GET', {'cost': 5}) + swap_v1_private_get_trade_fullorder = swapV1PrivateGetTradeFullOrder = Entry('trade/fullOrder', ['swap', 'v1', 'private'], 'GET', {'cost': 2}) + swap_v1_private_get_maintmarginratio = swapV1PrivateGetMaintMarginRatio = Entry('maintMarginRatio', ['swap', 'v1', 'private'], 'GET', {'cost': 2}) + swap_v1_private_get_trade_positionhistory = swapV1PrivateGetTradePositionHistory = Entry('trade/positionHistory', ['swap', 'v1', 'private'], 'GET', {'cost': 2}) + swap_v1_private_get_positionmargin_history = swapV1PrivateGetPositionMarginHistory = Entry('positionMargin/history', ['swap', 'v1', 'private'], 'GET', {'cost': 2}) + swap_v1_private_get_twap_openorders = swapV1PrivateGetTwapOpenOrders = Entry('twap/openOrders', ['swap', 'v1', 'private'], 'GET', {'cost': 5}) + swap_v1_private_get_twap_historyorders = swapV1PrivateGetTwapHistoryOrders = Entry('twap/historyOrders', ['swap', 'v1', 'private'], 'GET', {'cost': 5}) + swap_v1_private_get_twap_orderdetail = swapV1PrivateGetTwapOrderDetail = Entry('twap/orderDetail', ['swap', 'v1', 'private'], 'GET', {'cost': 5}) + swap_v1_private_get_trade_assetmode = swapV1PrivateGetTradeAssetMode = Entry('trade/assetMode', ['swap', 'v1', 'private'], 'GET', {'cost': 5}) + swap_v1_private_get_user_marginassets = swapV1PrivateGetUserMarginAssets = Entry('user/marginAssets', ['swap', 'v1', 'private'], 'GET', {'cost': 5}) + swap_v1_private_post_trade_cancelreplace = swapV1PrivatePostTradeCancelReplace = Entry('trade/cancelReplace', ['swap', 'v1', 'private'], 'POST', {'cost': 2}) + swap_v1_private_post_positionside_dual = swapV1PrivatePostPositionSideDual = Entry('positionSide/dual', ['swap', 'v1', 'private'], 'POST', {'cost': 5}) + swap_v1_private_post_trade_batchcancelreplace = swapV1PrivatePostTradeBatchCancelReplace = Entry('trade/batchCancelReplace', ['swap', 'v1', 'private'], 'POST', {'cost': 5}) + swap_v1_private_post_trade_closeposition = swapV1PrivatePostTradeClosePosition = Entry('trade/closePosition', ['swap', 'v1', 'private'], 'POST', {'cost': 2}) + swap_v1_private_post_trade_getvst = swapV1PrivatePostTradeGetVst = Entry('trade/getVst', ['swap', 'v1', 'private'], 'POST', {'cost': 5}) + swap_v1_private_post_twap_order = swapV1PrivatePostTwapOrder = Entry('twap/order', ['swap', 'v1', 'private'], 'POST', {'cost': 5}) + swap_v1_private_post_twap_cancelorder = swapV1PrivatePostTwapCancelOrder = Entry('twap/cancelOrder', ['swap', 'v1', 'private'], 'POST', {'cost': 5}) + swap_v1_private_post_trade_assetmode = swapV1PrivatePostTradeAssetMode = Entry('trade/assetMode', ['swap', 'v1', 'private'], 'POST', {'cost': 5}) + swap_v1_private_post_trade_reverse = swapV1PrivatePostTradeReverse = Entry('trade/reverse', ['swap', 'v1', 'private'], 'POST', {'cost': 5}) + swap_v1_private_post_trade_autoaddmargin = swapV1PrivatePostTradeAutoAddMargin = Entry('trade/autoAddMargin', ['swap', 'v1', 'private'], 'POST', {'cost': 5}) + swap_v2_public_get_server_time = swapV2PublicGetServerTime = Entry('server/time', ['swap', 'v2', 'public'], 'GET', {'cost': 1}) + swap_v2_public_get_quote_contracts = swapV2PublicGetQuoteContracts = Entry('quote/contracts', ['swap', 'v2', 'public'], 'GET', {'cost': 1}) + swap_v2_public_get_quote_price = swapV2PublicGetQuotePrice = Entry('quote/price', ['swap', 'v2', 'public'], 'GET', {'cost': 1}) + swap_v2_public_get_quote_depth = swapV2PublicGetQuoteDepth = Entry('quote/depth', ['swap', 'v2', 'public'], 'GET', {'cost': 1}) + swap_v2_public_get_quote_trades = swapV2PublicGetQuoteTrades = Entry('quote/trades', ['swap', 'v2', 'public'], 'GET', {'cost': 1}) + swap_v2_public_get_quote_premiumindex = swapV2PublicGetQuotePremiumIndex = Entry('quote/premiumIndex', ['swap', 'v2', 'public'], 'GET', {'cost': 1}) + swap_v2_public_get_quote_fundingrate = swapV2PublicGetQuoteFundingRate = Entry('quote/fundingRate', ['swap', 'v2', 'public'], 'GET', {'cost': 1}) + swap_v2_public_get_quote_klines = swapV2PublicGetQuoteKlines = Entry('quote/klines', ['swap', 'v2', 'public'], 'GET', {'cost': 1}) + swap_v2_public_get_quote_openinterest = swapV2PublicGetQuoteOpenInterest = Entry('quote/openInterest', ['swap', 'v2', 'public'], 'GET', {'cost': 1}) + swap_v2_public_get_quote_ticker = swapV2PublicGetQuoteTicker = Entry('quote/ticker', ['swap', 'v2', 'public'], 'GET', {'cost': 1}) + swap_v2_public_get_quote_bookticker = swapV2PublicGetQuoteBookTicker = Entry('quote/bookTicker', ['swap', 'v2', 'public'], 'GET', {'cost': 1}) + swap_v2_private_get_user_balance = swapV2PrivateGetUserBalance = Entry('user/balance', ['swap', 'v2', 'private'], 'GET', {'cost': 2}) + swap_v2_private_get_user_positions = swapV2PrivateGetUserPositions = Entry('user/positions', ['swap', 'v2', 'private'], 'GET', {'cost': 2}) + swap_v2_private_get_user_income = swapV2PrivateGetUserIncome = Entry('user/income', ['swap', 'v2', 'private'], 'GET', {'cost': 2}) + swap_v2_private_get_trade_openorders = swapV2PrivateGetTradeOpenOrders = Entry('trade/openOrders', ['swap', 'v2', 'private'], 'GET', {'cost': 2}) + swap_v2_private_get_trade_openorder = swapV2PrivateGetTradeOpenOrder = Entry('trade/openOrder', ['swap', 'v2', 'private'], 'GET', {'cost': 2}) + swap_v2_private_get_trade_order = swapV2PrivateGetTradeOrder = Entry('trade/order', ['swap', 'v2', 'private'], 'GET', {'cost': 2}) + swap_v2_private_get_trade_margintype = swapV2PrivateGetTradeMarginType = Entry('trade/marginType', ['swap', 'v2', 'private'], 'GET', {'cost': 5}) + swap_v2_private_get_trade_leverage = swapV2PrivateGetTradeLeverage = Entry('trade/leverage', ['swap', 'v2', 'private'], 'GET', {'cost': 2}) + swap_v2_private_get_trade_forceorders = swapV2PrivateGetTradeForceOrders = Entry('trade/forceOrders', ['swap', 'v2', 'private'], 'GET', {'cost': 1}) + swap_v2_private_get_trade_allorders = swapV2PrivateGetTradeAllOrders = Entry('trade/allOrders', ['swap', 'v2', 'private'], 'GET', {'cost': 2}) + swap_v2_private_get_trade_allfillorders = swapV2PrivateGetTradeAllFillOrders = Entry('trade/allFillOrders', ['swap', 'v2', 'private'], 'GET', {'cost': 2}) + swap_v2_private_get_trade_fillhistory = swapV2PrivateGetTradeFillHistory = Entry('trade/fillHistory', ['swap', 'v2', 'private'], 'GET', {'cost': 2}) + swap_v2_private_get_user_income_export = swapV2PrivateGetUserIncomeExport = Entry('user/income/export', ['swap', 'v2', 'private'], 'GET', {'cost': 2}) + swap_v2_private_get_user_commissionrate = swapV2PrivateGetUserCommissionRate = Entry('user/commissionRate', ['swap', 'v2', 'private'], 'GET', {'cost': 2}) + swap_v2_private_get_quote_bookticker = swapV2PrivateGetQuoteBookTicker = Entry('quote/bookTicker', ['swap', 'v2', 'private'], 'GET', {'cost': 1}) + swap_v2_private_post_trade_order = swapV2PrivatePostTradeOrder = Entry('trade/order', ['swap', 'v2', 'private'], 'POST', {'cost': 2}) + swap_v2_private_post_trade_batchorders = swapV2PrivatePostTradeBatchOrders = Entry('trade/batchOrders', ['swap', 'v2', 'private'], 'POST', {'cost': 2}) + swap_v2_private_post_trade_closeallpositions = swapV2PrivatePostTradeCloseAllPositions = Entry('trade/closeAllPositions', ['swap', 'v2', 'private'], 'POST', {'cost': 2}) + swap_v2_private_post_trade_cancelallafter = swapV2PrivatePostTradeCancelAllAfter = Entry('trade/cancelAllAfter', ['swap', 'v2', 'private'], 'POST', {'cost': 5}) + swap_v2_private_post_trade_margintype = swapV2PrivatePostTradeMarginType = Entry('trade/marginType', ['swap', 'v2', 'private'], 'POST', {'cost': 5}) + swap_v2_private_post_trade_leverage = swapV2PrivatePostTradeLeverage = Entry('trade/leverage', ['swap', 'v2', 'private'], 'POST', {'cost': 5}) + swap_v2_private_post_trade_positionmargin = swapV2PrivatePostTradePositionMargin = Entry('trade/positionMargin', ['swap', 'v2', 'private'], 'POST', {'cost': 5}) + swap_v2_private_post_trade_order_test = swapV2PrivatePostTradeOrderTest = Entry('trade/order/test', ['swap', 'v2', 'private'], 'POST', {'cost': 2}) + swap_v2_private_delete_trade_order = swapV2PrivateDeleteTradeOrder = Entry('trade/order', ['swap', 'v2', 'private'], 'DELETE', {'cost': 2}) + swap_v2_private_delete_trade_batchorders = swapV2PrivateDeleteTradeBatchOrders = Entry('trade/batchOrders', ['swap', 'v2', 'private'], 'DELETE', {'cost': 2}) + swap_v2_private_delete_trade_allopenorders = swapV2PrivateDeleteTradeAllOpenOrders = Entry('trade/allOpenOrders', ['swap', 'v2', 'private'], 'DELETE', {'cost': 2}) + swap_v3_public_get_quote_klines = swapV3PublicGetQuoteKlines = Entry('quote/klines', ['swap', 'v3', 'public'], 'GET', {'cost': 1}) + swap_v3_private_get_user_balance = swapV3PrivateGetUserBalance = Entry('user/balance', ['swap', 'v3', 'private'], 'GET', {'cost': 2}) + cswap_v1_public_get_market_contracts = cswapV1PublicGetMarketContracts = Entry('market/contracts', ['cswap', 'v1', 'public'], 'GET', {'cost': 1}) + cswap_v1_public_get_market_premiumindex = cswapV1PublicGetMarketPremiumIndex = Entry('market/premiumIndex', ['cswap', 'v1', 'public'], 'GET', {'cost': 1}) + cswap_v1_public_get_market_openinterest = cswapV1PublicGetMarketOpenInterest = Entry('market/openInterest', ['cswap', 'v1', 'public'], 'GET', {'cost': 1}) + cswap_v1_public_get_market_klines = cswapV1PublicGetMarketKlines = Entry('market/klines', ['cswap', 'v1', 'public'], 'GET', {'cost': 1}) + cswap_v1_public_get_market_depth = cswapV1PublicGetMarketDepth = Entry('market/depth', ['cswap', 'v1', 'public'], 'GET', {'cost': 1}) + cswap_v1_public_get_market_ticker = cswapV1PublicGetMarketTicker = Entry('market/ticker', ['cswap', 'v1', 'public'], 'GET', {'cost': 1}) + cswap_v1_private_get_trade_leverage = cswapV1PrivateGetTradeLeverage = Entry('trade/leverage', ['cswap', 'v1', 'private'], 'GET', {'cost': 2}) + cswap_v1_private_get_trade_forceorders = cswapV1PrivateGetTradeForceOrders = Entry('trade/forceOrders', ['cswap', 'v1', 'private'], 'GET', {'cost': 2}) + cswap_v1_private_get_trade_allfillorders = cswapV1PrivateGetTradeAllFillOrders = Entry('trade/allFillOrders', ['cswap', 'v1', 'private'], 'GET', {'cost': 2}) + cswap_v1_private_get_trade_openorders = cswapV1PrivateGetTradeOpenOrders = Entry('trade/openOrders', ['cswap', 'v1', 'private'], 'GET', {'cost': 2}) + cswap_v1_private_get_trade_orderdetail = cswapV1PrivateGetTradeOrderDetail = Entry('trade/orderDetail', ['cswap', 'v1', 'private'], 'GET', {'cost': 2}) + cswap_v1_private_get_trade_orderhistory = cswapV1PrivateGetTradeOrderHistory = Entry('trade/orderHistory', ['cswap', 'v1', 'private'], 'GET', {'cost': 2}) + cswap_v1_private_get_trade_margintype = cswapV1PrivateGetTradeMarginType = Entry('trade/marginType', ['cswap', 'v1', 'private'], 'GET', {'cost': 2}) + cswap_v1_private_get_user_commissionrate = cswapV1PrivateGetUserCommissionRate = Entry('user/commissionRate', ['cswap', 'v1', 'private'], 'GET', {'cost': 2}) + cswap_v1_private_get_user_positions = cswapV1PrivateGetUserPositions = Entry('user/positions', ['cswap', 'v1', 'private'], 'GET', {'cost': 2}) + cswap_v1_private_get_user_balance = cswapV1PrivateGetUserBalance = Entry('user/balance', ['cswap', 'v1', 'private'], 'GET', {'cost': 2}) + cswap_v1_private_post_trade_order = cswapV1PrivatePostTradeOrder = Entry('trade/order', ['cswap', 'v1', 'private'], 'POST', {'cost': 2}) + cswap_v1_private_post_trade_leverage = cswapV1PrivatePostTradeLeverage = Entry('trade/leverage', ['cswap', 'v1', 'private'], 'POST', {'cost': 2}) + cswap_v1_private_post_trade_allopenorders = cswapV1PrivatePostTradeAllOpenOrders = Entry('trade/allOpenOrders', ['cswap', 'v1', 'private'], 'POST', {'cost': 2}) + cswap_v1_private_post_trade_closeallpositions = cswapV1PrivatePostTradeCloseAllPositions = Entry('trade/closeAllPositions', ['cswap', 'v1', 'private'], 'POST', {'cost': 2}) + cswap_v1_private_post_trade_margintype = cswapV1PrivatePostTradeMarginType = Entry('trade/marginType', ['cswap', 'v1', 'private'], 'POST', {'cost': 2}) + cswap_v1_private_post_trade_positionmargin = cswapV1PrivatePostTradePositionMargin = Entry('trade/positionMargin', ['cswap', 'v1', 'private'], 'POST', {'cost': 2}) + cswap_v1_private_delete_trade_allopenorders = cswapV1PrivateDeleteTradeAllOpenOrders = Entry('trade/allOpenOrders', ['cswap', 'v1', 'private'], 'DELETE', {'cost': 2}) + cswap_v1_private_delete_trade_cancelorder = cswapV1PrivateDeleteTradeCancelOrder = Entry('trade/cancelOrder', ['cswap', 'v1', 'private'], 'DELETE', {'cost': 2}) + contract_v1_private_get_allposition = contractV1PrivateGetAllPosition = Entry('allPosition', ['contract', 'v1', 'private'], 'GET', {'cost': 2}) + contract_v1_private_get_allorders = contractV1PrivateGetAllOrders = Entry('allOrders', ['contract', 'v1', 'private'], 'GET', {'cost': 2}) + contract_v1_private_get_balance = contractV1PrivateGetBalance = Entry('balance', ['contract', 'v1', 'private'], 'GET', {'cost': 2}) + wallets_v1_private_get_capital_config_getall = walletsV1PrivateGetCapitalConfigGetall = Entry('capital/config/getall', ['wallets', 'v1', 'private'], 'GET', {'cost': 5}) + wallets_v1_private_get_capital_deposit_address = walletsV1PrivateGetCapitalDepositAddress = Entry('capital/deposit/address', ['wallets', 'v1', 'private'], 'GET', {'cost': 5}) + wallets_v1_private_get_capital_innertransfer_records = walletsV1PrivateGetCapitalInnerTransferRecords = Entry('capital/innerTransfer/records', ['wallets', 'v1', 'private'], 'GET', {'cost': 1}) + wallets_v1_private_get_capital_subaccount_deposit_address = walletsV1PrivateGetCapitalSubAccountDepositAddress = Entry('capital/subAccount/deposit/address', ['wallets', 'v1', 'private'], 'GET', {'cost': 5}) + wallets_v1_private_get_capital_deposit_subhisrec = walletsV1PrivateGetCapitalDepositSubHisrec = Entry('capital/deposit/subHisrec', ['wallets', 'v1', 'private'], 'GET', {'cost': 2}) + wallets_v1_private_get_capital_subaccount_innertransfer_records = walletsV1PrivateGetCapitalSubAccountInnerTransferRecords = Entry('capital/subAccount/innerTransfer/records', ['wallets', 'v1', 'private'], 'GET', {'cost': 1}) + wallets_v1_private_get_capital_deposit_riskrecords = walletsV1PrivateGetCapitalDepositRiskRecords = Entry('capital/deposit/riskRecords', ['wallets', 'v1', 'private'], 'GET', {'cost': 5}) + wallets_v1_private_post_capital_withdraw_apply = walletsV1PrivatePostCapitalWithdrawApply = Entry('capital/withdraw/apply', ['wallets', 'v1', 'private'], 'POST', {'cost': 5}) + wallets_v1_private_post_capital_innertransfer_apply = walletsV1PrivatePostCapitalInnerTransferApply = Entry('capital/innerTransfer/apply', ['wallets', 'v1', 'private'], 'POST', {'cost': 5}) + wallets_v1_private_post_capital_subaccountinnertransfer_apply = walletsV1PrivatePostCapitalSubAccountInnerTransferApply = Entry('capital/subAccountInnerTransfer/apply', ['wallets', 'v1', 'private'], 'POST', {'cost': 2}) + wallets_v1_private_post_capital_deposit_createsubaddress = walletsV1PrivatePostCapitalDepositCreateSubAddress = Entry('capital/deposit/createSubAddress', ['wallets', 'v1', 'private'], 'POST', {'cost': 2}) + subaccount_v1_private_get_list = subAccountV1PrivateGetList = Entry('list', ['subAccount', 'v1', 'private'], 'GET', {'cost': 10}) + subaccount_v1_private_get_assets = subAccountV1PrivateGetAssets = Entry('assets', ['subAccount', 'v1', 'private'], 'GET', {'cost': 2}) + subaccount_v1_private_get_allaccountbalance = subAccountV1PrivateGetAllAccountBalance = Entry('allAccountBalance', ['subAccount', 'v1', 'private'], 'GET', {'cost': 2}) + subaccount_v1_private_post_create = subAccountV1PrivatePostCreate = Entry('create', ['subAccount', 'v1', 'private'], 'POST', {'cost': 10}) + subaccount_v1_private_post_apikey_create = subAccountV1PrivatePostApiKeyCreate = Entry('apiKey/create', ['subAccount', 'v1', 'private'], 'POST', {'cost': 2}) + subaccount_v1_private_post_apikey_edit = subAccountV1PrivatePostApiKeyEdit = Entry('apiKey/edit', ['subAccount', 'v1', 'private'], 'POST', {'cost': 2}) + subaccount_v1_private_post_apikey_del = subAccountV1PrivatePostApiKeyDel = Entry('apiKey/del', ['subAccount', 'v1', 'private'], 'POST', {'cost': 2}) + subaccount_v1_private_post_updatestatus = subAccountV1PrivatePostUpdateStatus = Entry('updateStatus', ['subAccount', 'v1', 'private'], 'POST', {'cost': 10}) + account_v1_private_get_uid = accountV1PrivateGetUid = Entry('uid', ['account', 'v1', 'private'], 'GET', {'cost': 1}) + account_v1_private_get_apikey_query = accountV1PrivateGetApiKeyQuery = Entry('apiKey/query', ['account', 'v1', 'private'], 'GET', {'cost': 2}) + account_v1_private_get_account_apipermissions = accountV1PrivateGetAccountApiPermissions = Entry('account/apiPermissions', ['account', 'v1', 'private'], 'GET', {'cost': 5}) + account_v1_private_get_allaccountbalance = accountV1PrivateGetAllAccountBalance = Entry('allAccountBalance', ['account', 'v1', 'private'], 'GET', {'cost': 2}) + account_v1_private_post_innertransfer_authorizesubaccount = accountV1PrivatePostInnerTransferAuthorizeSubAccount = Entry('innerTransfer/authorizeSubAccount', ['account', 'v1', 'private'], 'POST', {'cost': 1}) + account_transfer_v1_private_get_subaccount_asset_transferhistory = accountTransferV1PrivateGetSubAccountAssetTransferHistory = Entry('subAccount/asset/transferHistory', ['account', 'transfer', 'v1', 'private'], 'GET', {'cost': 1}) + account_transfer_v1_private_post_subaccount_transferasset_supportcoins = accountTransferV1PrivatePostSubAccountTransferAssetSupportCoins = Entry('subAccount/transferAsset/supportCoins', ['account', 'transfer', 'v1', 'private'], 'POST', {'cost': 1}) + account_transfer_v1_private_post_subaccount_transferasset = accountTransferV1PrivatePostSubAccountTransferAsset = Entry('subAccount/transferAsset', ['account', 'transfer', 'v1', 'private'], 'POST', {'cost': 1}) + user_auth_private_post_userdatastream = userAuthPrivatePostUserDataStream = Entry('userDataStream', ['user', 'auth', 'private'], 'POST', {'cost': 2}) + user_auth_private_put_userdatastream = userAuthPrivatePutUserDataStream = Entry('userDataStream', ['user', 'auth', 'private'], 'PUT', {'cost': 2}) + user_auth_private_delete_userdatastream = userAuthPrivateDeleteUserDataStream = Entry('userDataStream', ['user', 'auth', 'private'], 'DELETE', {'cost': 2}) + copytrading_v1_private_get_swap_trace_currenttrack = copyTradingV1PrivateGetSwapTraceCurrentTrack = Entry('swap/trace/currentTrack', ['copyTrading', 'v1', 'private'], 'GET', {'cost': 2}) + copytrading_v1_private_post_swap_trace_closetrackorder = copyTradingV1PrivatePostSwapTraceCloseTrackOrder = Entry('swap/trace/closeTrackOrder', ['copyTrading', 'v1', 'private'], 'POST', {'cost': 2}) + copytrading_v1_private_post_swap_trace_settpsl = copyTradingV1PrivatePostSwapTraceSetTPSL = Entry('swap/trace/setTPSL', ['copyTrading', 'v1', 'private'], 'POST', {'cost': 2}) + copytrading_v1_private_post_spot_trader_sellorder = copyTradingV1PrivatePostSpotTraderSellOrder = Entry('spot/trader/sellOrder', ['copyTrading', 'v1', 'private'], 'POST', {'cost': 10}) + api_v3_private_get_asset_transfer = apiV3PrivateGetAssetTransfer = Entry('asset/transfer', ['api', 'v3', 'private'], 'GET', {'cost': 1}) + api_v3_private_get_asset_transferrecord = apiV3PrivateGetAssetTransferRecord = Entry('asset/transferRecord', ['api', 'v3', 'private'], 'GET', {'cost': 5}) + api_v3_private_get_capital_deposit_hisrec = apiV3PrivateGetCapitalDepositHisrec = Entry('capital/deposit/hisrec', ['api', 'v3', 'private'], 'GET', {'cost': 1}) + api_v3_private_get_capital_withdraw_history = apiV3PrivateGetCapitalWithdrawHistory = Entry('capital/withdraw/history', ['api', 'v3', 'private'], 'GET', {'cost': 1}) + api_v3_private_post_post_asset_transfer = apiV3PrivatePostPostAssetTransfer = Entry('post/asset/transfer', ['api', 'v3', 'private'], 'POST', {'cost': 1}) + api_asset_v1_private_post_transfer = apiAssetV1PrivatePostTransfer = Entry('transfer', ['api', 'asset', 'v1', 'private'], 'POST', {'cost': 5}) + api_asset_v1_public_get_transfer_supportcoins = apiAssetV1PublicGetTransferSupportCoins = Entry('transfer/supportCoins', ['api', 'asset', 'v1', 'public'], 'GET', {'cost': 5}) + agent_v1_private_get_account_inviteaccountlist = agentV1PrivateGetAccountInviteAccountList = Entry('account/inviteAccountList', ['agent', 'v1', 'private'], 'GET', {'cost': 5}) + agent_v1_private_get_reward_commissiondatalist = agentV1PrivateGetRewardCommissionDataList = Entry('reward/commissionDataList', ['agent', 'v1', 'private'], 'GET', {'cost': 5}) + agent_v1_private_get_account_inviterelationcheck = agentV1PrivateGetAccountInviteRelationCheck = Entry('account/inviteRelationCheck', ['agent', 'v1', 'private'], 'GET', {'cost': 5}) + agent_v1_private_get_asset_depositdetaillist = agentV1PrivateGetAssetDepositDetailList = Entry('asset/depositDetailList', ['agent', 'v1', 'private'], 'GET', {'cost': 5}) + agent_v1_private_get_reward_third_commissiondatalist = agentV1PrivateGetRewardThirdCommissionDataList = Entry('reward/third/commissionDataList', ['agent', 'v1', 'private'], 'GET', {'cost': 5}) + agent_v1_private_get_asset_partnerdata = agentV1PrivateGetAssetPartnerData = Entry('asset/partnerData', ['agent', 'v1', 'private'], 'GET', {'cost': 5}) + agent_v1_private_get_commissiondatalist_referralcode = agentV1PrivateGetCommissionDataListReferralCode = Entry('commissionDataList/referralCode', ['agent', 'v1', 'private'], 'GET', {'cost': 5}) + agent_v1_private_get_account_superiorcheck = agentV1PrivateGetAccountSuperiorCheck = Entry('account/superiorCheck', ['agent', 'v1', 'private'], 'GET', {'cost': 5}) diff --git a/ccxt/abstract/bit2c.py b/ccxt/abstract/bit2c.py new file mode 100644 index 0000000..1353e72 --- /dev/null +++ b/ccxt/abstract/bit2c.py @@ -0,0 +1,27 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_exchanges_pair_ticker = publicGetExchangesPairTicker = Entry('Exchanges/{pair}/Ticker', 'public', 'GET', {}) + public_get_exchanges_pair_orderbook = publicGetExchangesPairOrderbook = Entry('Exchanges/{pair}/orderbook', 'public', 'GET', {}) + public_get_exchanges_pair_trades = publicGetExchangesPairTrades = Entry('Exchanges/{pair}/trades', 'public', 'GET', {}) + public_get_exchanges_pair_lasttrades = publicGetExchangesPairLasttrades = Entry('Exchanges/{pair}/lasttrades', 'public', 'GET', {}) + private_post_merchant_createcheckout = privatePostMerchantCreateCheckout = Entry('Merchant/CreateCheckout', 'private', 'POST', {}) + private_post_funds_addcoinfundsrequest = privatePostFundsAddCoinFundsRequest = Entry('Funds/AddCoinFundsRequest', 'private', 'POST', {}) + private_post_order_addfund = privatePostOrderAddFund = Entry('Order/AddFund', 'private', 'POST', {}) + private_post_order_addorder = privatePostOrderAddOrder = Entry('Order/AddOrder', 'private', 'POST', {}) + private_post_order_getbyid = privatePostOrderGetById = Entry('Order/GetById', 'private', 'POST', {}) + private_post_order_addordermarketpricebuy = privatePostOrderAddOrderMarketPriceBuy = Entry('Order/AddOrderMarketPriceBuy', 'private', 'POST', {}) + private_post_order_addordermarketpricesell = privatePostOrderAddOrderMarketPriceSell = Entry('Order/AddOrderMarketPriceSell', 'private', 'POST', {}) + private_post_order_cancelorder = privatePostOrderCancelOrder = Entry('Order/CancelOrder', 'private', 'POST', {}) + private_post_order_addcoinfundsrequest = privatePostOrderAddCoinFundsRequest = Entry('Order/AddCoinFundsRequest', 'private', 'POST', {}) + private_post_order_addstoporder = privatePostOrderAddStopOrder = Entry('Order/AddStopOrder', 'private', 'POST', {}) + private_post_payment_getmyid = privatePostPaymentGetMyId = Entry('Payment/GetMyId', 'private', 'POST', {}) + private_post_payment_send = privatePostPaymentSend = Entry('Payment/Send', 'private', 'POST', {}) + private_post_payment_pay = privatePostPaymentPay = Entry('Payment/Pay', 'private', 'POST', {}) + private_get_account_balance = privateGetAccountBalance = Entry('Account/Balance', 'private', 'GET', {}) + private_get_account_balance_v2 = privateGetAccountBalanceV2 = Entry('Account/Balance/v2', 'private', 'GET', {}) + private_get_order_myorders = privateGetOrderMyOrders = Entry('Order/MyOrders', 'private', 'GET', {}) + private_get_order_getbyid = privateGetOrderGetById = Entry('Order/GetById', 'private', 'GET', {}) + private_get_order_accounthistory = privateGetOrderAccountHistory = Entry('Order/AccountHistory', 'private', 'GET', {}) + private_get_order_orderhistory = privateGetOrderOrderHistory = Entry('Order/OrderHistory', 'private', 'GET', {}) diff --git a/ccxt/abstract/bitbank.py b/ccxt/abstract/bitbank.py new file mode 100644 index 0000000..f249b58 --- /dev/null +++ b/ccxt/abstract/bitbank.py @@ -0,0 +1,32 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_pair_ticker = publicGetPairTicker = Entry('{pair}/ticker', 'public', 'GET', {}) + public_get_tickers = publicGetTickers = Entry('tickers', 'public', 'GET', {}) + public_get_tickers_jpy = publicGetTickersJpy = Entry('tickers_jpy', 'public', 'GET', {}) + public_get_pair_depth = publicGetPairDepth = Entry('{pair}/depth', 'public', 'GET', {}) + public_get_pair_transactions = publicGetPairTransactions = Entry('{pair}/transactions', 'public', 'GET', {}) + public_get_pair_transactions_yyyymmdd = publicGetPairTransactionsYyyymmdd = Entry('{pair}/transactions/{yyyymmdd}', 'public', 'GET', {}) + public_get_pair_candlestick_candletype_yyyymmdd = publicGetPairCandlestickCandletypeYyyymmdd = Entry('{pair}/candlestick/{candletype}/{yyyymmdd}', 'public', 'GET', {}) + public_get_pair_circuit_break_info = publicGetPairCircuitBreakInfo = Entry('{pair}/circuit_break_info', 'public', 'GET', {}) + private_get_user_assets = privateGetUserAssets = Entry('user/assets', 'private', 'GET', {}) + private_get_user_spot_order = privateGetUserSpotOrder = Entry('user/spot/order', 'private', 'GET', {}) + private_get_user_spot_active_orders = privateGetUserSpotActiveOrders = Entry('user/spot/active_orders', 'private', 'GET', {}) + private_get_user_margin_positions = privateGetUserMarginPositions = Entry('user/margin/positions', 'private', 'GET', {}) + private_get_user_spot_trade_history = privateGetUserSpotTradeHistory = Entry('user/spot/trade_history', 'private', 'GET', {}) + private_get_user_deposit_history = privateGetUserDepositHistory = Entry('user/deposit_history', 'private', 'GET', {}) + private_get_user_unconfirmed_deposits = privateGetUserUnconfirmedDeposits = Entry('user/unconfirmed_deposits', 'private', 'GET', {}) + private_get_user_deposit_originators = privateGetUserDepositOriginators = Entry('user/deposit_originators', 'private', 'GET', {}) + private_get_user_withdrawal_account = privateGetUserWithdrawalAccount = Entry('user/withdrawal_account', 'private', 'GET', {}) + private_get_user_withdrawal_history = privateGetUserWithdrawalHistory = Entry('user/withdrawal_history', 'private', 'GET', {}) + private_get_spot_status = privateGetSpotStatus = Entry('spot/status', 'private', 'GET', {}) + private_get_spot_pairs = privateGetSpotPairs = Entry('spot/pairs', 'private', 'GET', {}) + private_post_user_spot_order = privatePostUserSpotOrder = Entry('user/spot/order', 'private', 'POST', {}) + private_post_user_spot_cancel_order = privatePostUserSpotCancelOrder = Entry('user/spot/cancel_order', 'private', 'POST', {}) + private_post_user_spot_cancel_orders = privatePostUserSpotCancelOrders = Entry('user/spot/cancel_orders', 'private', 'POST', {}) + private_post_user_spot_orders_info = privatePostUserSpotOrdersInfo = Entry('user/spot/orders_info', 'private', 'POST', {}) + private_post_user_confirm_deposits = privatePostUserConfirmDeposits = Entry('user/confirm_deposits', 'private', 'POST', {}) + private_post_user_confirm_deposits_all = privatePostUserConfirmDepositsAll = Entry('user/confirm_deposits_all', 'private', 'POST', {}) + private_post_user_request_withdrawal = privatePostUserRequestWithdrawal = Entry('user/request_withdrawal', 'private', 'POST', {}) + markets_get_spot_pairs = marketsGetSpotPairs = Entry('spot/pairs', 'markets', 'GET', {}) diff --git a/ccxt/abstract/bitbns.py b/ccxt/abstract/bitbns.py new file mode 100644 index 0000000..73e84e6 --- /dev/null +++ b/ccxt/abstract/bitbns.py @@ -0,0 +1,40 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + www_get_order_fetchmarkets = wwwGetOrderFetchMarkets = Entry('order/fetchMarkets', 'www', 'GET', {}) + www_get_order_fetchtickers = wwwGetOrderFetchTickers = Entry('order/fetchTickers', 'www', 'GET', {}) + www_get_order_fetchorderbook = wwwGetOrderFetchOrderbook = Entry('order/fetchOrderbook', 'www', 'GET', {}) + www_get_order_gettickerwithvolume = wwwGetOrderGetTickerWithVolume = Entry('order/getTickerWithVolume', 'www', 'GET', {}) + www_get_exchangedata_ohlc = wwwGetExchangeDataOhlc = Entry('exchangeData/ohlc', 'www', 'GET', {}) + www_get_exchangedata_orderbook = wwwGetExchangeDataOrderBook = Entry('exchangeData/orderBook', 'www', 'GET', {}) + www_get_exchangedata_tradedetails = wwwGetExchangeDataTradedetails = Entry('exchangeData/tradedetails', 'www', 'GET', {}) + v1_get_platform_status = v1GetPlatformStatus = Entry('platform/status', 'v1', 'GET', {}) + v1_get_tickers = v1GetTickers = Entry('tickers', 'v1', 'GET', {}) + v1_get_orderbook_sell_symbol = v1GetOrderbookSellSymbol = Entry('orderbook/sell/{symbol}', 'v1', 'GET', {}) + v1_get_orderbook_buy_symbol = v1GetOrderbookBuySymbol = Entry('orderbook/buy/{symbol}', 'v1', 'GET', {}) + v1_post_currentcoinbalance_everything = v1PostCurrentCoinBalanceEVERYTHING = Entry('currentCoinBalance/EVERYTHING', 'v1', 'POST', {}) + v1_post_getapiusagestatus_usage = v1PostGetApiUsageStatusUSAGE = Entry('getApiUsageStatus/USAGE', 'v1', 'POST', {}) + v1_post_getordersockettoken_usage = v1PostGetOrderSocketTokenUSAGE = Entry('getOrderSocketToken/USAGE', 'v1', 'POST', {}) + v1_post_currentcoinbalance_symbol = v1PostCurrentCoinBalanceSymbol = Entry('currentCoinBalance/{symbol}', 'v1', 'POST', {}) + v1_post_orderstatus_symbol = v1PostOrderStatusSymbol = Entry('orderStatus/{symbol}', 'v1', 'POST', {}) + v1_post_deposithistory_symbol = v1PostDepositHistorySymbol = Entry('depositHistory/{symbol}', 'v1', 'POST', {}) + v1_post_withdrawhistory_symbol = v1PostWithdrawHistorySymbol = Entry('withdrawHistory/{symbol}', 'v1', 'POST', {}) + v1_post_withdrawhistoryall_symbol = v1PostWithdrawHistoryAllSymbol = Entry('withdrawHistoryAll/{symbol}', 'v1', 'POST', {}) + v1_post_deposithistoryall_symbol = v1PostDepositHistoryAllSymbol = Entry('depositHistoryAll/{symbol}', 'v1', 'POST', {}) + v1_post_listopenorders_symbol = v1PostListOpenOrdersSymbol = Entry('listOpenOrders/{symbol}', 'v1', 'POST', {}) + v1_post_listopenstoporders_symbol = v1PostListOpenStopOrdersSymbol = Entry('listOpenStopOrders/{symbol}', 'v1', 'POST', {}) + v1_post_getcoinaddress_symbol = v1PostGetCoinAddressSymbol = Entry('getCoinAddress/{symbol}', 'v1', 'POST', {}) + v1_post_placesellorder_symbol = v1PostPlaceSellOrderSymbol = Entry('placeSellOrder/{symbol}', 'v1', 'POST', {}) + v1_post_placebuyorder_symbol = v1PostPlaceBuyOrderSymbol = Entry('placeBuyOrder/{symbol}', 'v1', 'POST', {}) + v1_post_buystoploss_symbol = v1PostBuyStopLossSymbol = Entry('buyStopLoss/{symbol}', 'v1', 'POST', {}) + v1_post_sellstoploss_symbol = v1PostSellStopLossSymbol = Entry('sellStopLoss/{symbol}', 'v1', 'POST', {}) + v1_post_cancelorder_symbol = v1PostCancelOrderSymbol = Entry('cancelOrder/{symbol}', 'v1', 'POST', {}) + v1_post_cancelstoplossorder_symbol = v1PostCancelStopLossOrderSymbol = Entry('cancelStopLossOrder/{symbol}', 'v1', 'POST', {}) + v1_post_listexecutedorders_symbol = v1PostListExecutedOrdersSymbol = Entry('listExecutedOrders/{symbol}', 'v1', 'POST', {}) + v1_post_placemarketorder_symbol = v1PostPlaceMarketOrderSymbol = Entry('placeMarketOrder/{symbol}', 'v1', 'POST', {}) + v1_post_placemarketorderqnty_symbol = v1PostPlaceMarketOrderQntySymbol = Entry('placeMarketOrderQnty/{symbol}', 'v1', 'POST', {}) + v2_post_orders = v2PostOrders = Entry('orders', 'v2', 'POST', {}) + v2_post_cancel = v2PostCancel = Entry('cancel', 'v2', 'POST', {}) + v2_post_getordersnew = v2PostGetordersnew = Entry('getordersnew', 'v2', 'POST', {}) + v2_post_marginorders = v2PostMarginOrders = Entry('marginOrders', 'v2', 'POST', {}) diff --git a/ccxt/abstract/bitfinex.py b/ccxt/abstract/bitfinex.py new file mode 100644 index 0000000..edfd613 --- /dev/null +++ b/ccxt/abstract/bitfinex.py @@ -0,0 +1,140 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_conf_config = publicGetConfConfig = Entry('conf/{config}', 'public', 'GET', {'cost': 2.7}) + public_get_conf_pub_action_object = publicGetConfPubActionObject = Entry('conf/pub:{action}:{object}', 'public', 'GET', {'cost': 2.7}) + public_get_conf_pub_action_object_detail = publicGetConfPubActionObjectDetail = Entry('conf/pub:{action}:{object}:{detail}', 'public', 'GET', {'cost': 2.7}) + public_get_conf_pub_map_object = publicGetConfPubMapObject = Entry('conf/pub:map:{object}', 'public', 'GET', {'cost': 2.7}) + public_get_conf_pub_map_object_detail = publicGetConfPubMapObjectDetail = Entry('conf/pub:map:{object}:{detail}', 'public', 'GET', {'cost': 2.7}) + public_get_conf_pub_map_currency_detail = publicGetConfPubMapCurrencyDetail = Entry('conf/pub:map:currency:{detail}', 'public', 'GET', {'cost': 2.7}) + public_get_conf_pub_map_currency_sym = publicGetConfPubMapCurrencySym = Entry('conf/pub:map:currency:sym', 'public', 'GET', {'cost': 2.7}) + public_get_conf_pub_map_currency_label = publicGetConfPubMapCurrencyLabel = Entry('conf/pub:map:currency:label', 'public', 'GET', {'cost': 2.7}) + public_get_conf_pub_map_currency_unit = publicGetConfPubMapCurrencyUnit = Entry('conf/pub:map:currency:unit', 'public', 'GET', {'cost': 2.7}) + public_get_conf_pub_map_currency_undl = publicGetConfPubMapCurrencyUndl = Entry('conf/pub:map:currency:undl', 'public', 'GET', {'cost': 2.7}) + public_get_conf_pub_map_currency_pool = publicGetConfPubMapCurrencyPool = Entry('conf/pub:map:currency:pool', 'public', 'GET', {'cost': 2.7}) + public_get_conf_pub_map_currency_explorer = publicGetConfPubMapCurrencyExplorer = Entry('conf/pub:map:currency:explorer', 'public', 'GET', {'cost': 2.7}) + public_get_conf_pub_map_currency_tx_fee = publicGetConfPubMapCurrencyTxFee = Entry('conf/pub:map:currency:tx:fee', 'public', 'GET', {'cost': 2.7}) + public_get_conf_pub_map_tx_method = publicGetConfPubMapTxMethod = Entry('conf/pub:map:tx:method', 'public', 'GET', {'cost': 2.7}) + public_get_conf_pub_list_object = publicGetConfPubListObject = Entry('conf/pub:list:{object}', 'public', 'GET', {'cost': 2.7}) + public_get_conf_pub_list_object_detail = publicGetConfPubListObjectDetail = Entry('conf/pub:list:{object}:{detail}', 'public', 'GET', {'cost': 2.7}) + public_get_conf_pub_list_currency = publicGetConfPubListCurrency = Entry('conf/pub:list:currency', 'public', 'GET', {'cost': 2.7}) + public_get_conf_pub_list_pair_exchange = publicGetConfPubListPairExchange = Entry('conf/pub:list:pair:exchange', 'public', 'GET', {'cost': 2.7}) + public_get_conf_pub_list_pair_margin = publicGetConfPubListPairMargin = Entry('conf/pub:list:pair:margin', 'public', 'GET', {'cost': 2.7}) + public_get_conf_pub_list_pair_futures = publicGetConfPubListPairFutures = Entry('conf/pub:list:pair:futures', 'public', 'GET', {'cost': 2.7}) + public_get_conf_pub_list_competitions = publicGetConfPubListCompetitions = Entry('conf/pub:list:competitions', 'public', 'GET', {'cost': 2.7}) + public_get_conf_pub_info_object = publicGetConfPubInfoObject = Entry('conf/pub:info:{object}', 'public', 'GET', {'cost': 2.7}) + public_get_conf_pub_info_object_detail = publicGetConfPubInfoObjectDetail = Entry('conf/pub:info:{object}:{detail}', 'public', 'GET', {'cost': 2.7}) + public_get_conf_pub_info_pair = publicGetConfPubInfoPair = Entry('conf/pub:info:pair', 'public', 'GET', {'cost': 2.7}) + public_get_conf_pub_info_pair_futures = publicGetConfPubInfoPairFutures = Entry('conf/pub:info:pair:futures', 'public', 'GET', {'cost': 2.7}) + public_get_conf_pub_info_tx_status = publicGetConfPubInfoTxStatus = Entry('conf/pub:info:tx:status', 'public', 'GET', {'cost': 2.7}) + public_get_conf_pub_fees = publicGetConfPubFees = Entry('conf/pub:fees', 'public', 'GET', {'cost': 2.7}) + public_get_platform_status = publicGetPlatformStatus = Entry('platform/status', 'public', 'GET', {'cost': 8}) + public_get_tickers = publicGetTickers = Entry('tickers', 'public', 'GET', {'cost': 2.7}) + public_get_ticker_symbol = publicGetTickerSymbol = Entry('ticker/{symbol}', 'public', 'GET', {'cost': 2.7}) + public_get_tickers_hist = publicGetTickersHist = Entry('tickers/hist', 'public', 'GET', {'cost': 2.7}) + public_get_trades_symbol_hist = publicGetTradesSymbolHist = Entry('trades/{symbol}/hist', 'public', 'GET', {'cost': 2.7}) + public_get_book_symbol_precision = publicGetBookSymbolPrecision = Entry('book/{symbol}/{precision}', 'public', 'GET', {'cost': 1}) + public_get_book_symbol_p0 = publicGetBookSymbolP0 = Entry('book/{symbol}/P0', 'public', 'GET', {'cost': 1}) + public_get_book_symbol_p1 = publicGetBookSymbolP1 = Entry('book/{symbol}/P1', 'public', 'GET', {'cost': 1}) + public_get_book_symbol_p2 = publicGetBookSymbolP2 = Entry('book/{symbol}/P2', 'public', 'GET', {'cost': 1}) + public_get_book_symbol_p3 = publicGetBookSymbolP3 = Entry('book/{symbol}/P3', 'public', 'GET', {'cost': 1}) + public_get_book_symbol_r0 = publicGetBookSymbolR0 = Entry('book/{symbol}/R0', 'public', 'GET', {'cost': 1}) + public_get_stats1_key_size_symbol_side_section = publicGetStats1KeySizeSymbolSideSection = Entry('stats1/{key}:{size}:{symbol}:{side}/{section}', 'public', 'GET', {'cost': 2.7}) + public_get_stats1_key_size_symbol_side_last = publicGetStats1KeySizeSymbolSideLast = Entry('stats1/{key}:{size}:{symbol}:{side}/last', 'public', 'GET', {'cost': 2.7}) + public_get_stats1_key_size_symbol_side_hist = publicGetStats1KeySizeSymbolSideHist = Entry('stats1/{key}:{size}:{symbol}:{side}/hist', 'public', 'GET', {'cost': 2.7}) + public_get_stats1_key_size_symbol_section = publicGetStats1KeySizeSymbolSection = Entry('stats1/{key}:{size}:{symbol}/{section}', 'public', 'GET', {'cost': 2.7}) + public_get_stats1_key_size_symbol_last = publicGetStats1KeySizeSymbolLast = Entry('stats1/{key}:{size}:{symbol}/last', 'public', 'GET', {'cost': 2.7}) + public_get_stats1_key_size_symbol_hist = publicGetStats1KeySizeSymbolHist = Entry('stats1/{key}:{size}:{symbol}/hist', 'public', 'GET', {'cost': 2.7}) + public_get_stats1_key_size_symbol_long_last = publicGetStats1KeySizeSymbolLongLast = Entry('stats1/{key}:{size}:{symbol}:long/last', 'public', 'GET', {'cost': 2.7}) + public_get_stats1_key_size_symbol_long_hist = publicGetStats1KeySizeSymbolLongHist = Entry('stats1/{key}:{size}:{symbol}:long/hist', 'public', 'GET', {'cost': 2.7}) + public_get_stats1_key_size_symbol_short_last = publicGetStats1KeySizeSymbolShortLast = Entry('stats1/{key}:{size}:{symbol}:short/last', 'public', 'GET', {'cost': 2.7}) + public_get_stats1_key_size_symbol_short_hist = publicGetStats1KeySizeSymbolShortHist = Entry('stats1/{key}:{size}:{symbol}:short/hist', 'public', 'GET', {'cost': 2.7}) + public_get_candles_trade_timeframe_symbol_period_section = publicGetCandlesTradeTimeframeSymbolPeriodSection = Entry('candles/trade:{timeframe}:{symbol}:{period}/{section}', 'public', 'GET', {'cost': 2.7}) + public_get_candles_trade_timeframe_symbol_section = publicGetCandlesTradeTimeframeSymbolSection = Entry('candles/trade:{timeframe}:{symbol}/{section}', 'public', 'GET', {'cost': 2.7}) + public_get_candles_trade_timeframe_symbol_last = publicGetCandlesTradeTimeframeSymbolLast = Entry('candles/trade:{timeframe}:{symbol}/last', 'public', 'GET', {'cost': 2.7}) + public_get_candles_trade_timeframe_symbol_hist = publicGetCandlesTradeTimeframeSymbolHist = Entry('candles/trade:{timeframe}:{symbol}/hist', 'public', 'GET', {'cost': 2.7}) + public_get_status_type = publicGetStatusType = Entry('status/{type}', 'public', 'GET', {'cost': 2.7}) + public_get_status_deriv = publicGetStatusDeriv = Entry('status/deriv', 'public', 'GET', {'cost': 2.7}) + public_get_status_deriv_symbol_hist = publicGetStatusDerivSymbolHist = Entry('status/deriv/{symbol}/hist', 'public', 'GET', {'cost': 2.7}) + public_get_liquidations_hist = publicGetLiquidationsHist = Entry('liquidations/hist', 'public', 'GET', {'cost': 80}) + public_get_rankings_key_timeframe_symbol_section = publicGetRankingsKeyTimeframeSymbolSection = Entry('rankings/{key}:{timeframe}:{symbol}/{section}', 'public', 'GET', {'cost': 2.7}) + public_get_rankings_key_timeframe_symbol_hist = publicGetRankingsKeyTimeframeSymbolHist = Entry('rankings/{key}:{timeframe}:{symbol}/hist', 'public', 'GET', {'cost': 2.7}) + public_get_pulse_hist = publicGetPulseHist = Entry('pulse/hist', 'public', 'GET', {'cost': 2.7}) + public_get_pulse_profile_nickname = publicGetPulseProfileNickname = Entry('pulse/profile/{nickname}', 'public', 'GET', {'cost': 2.7}) + public_get_funding_stats_symbol_hist = publicGetFundingStatsSymbolHist = Entry('funding/stats/{symbol}/hist', 'public', 'GET', {'cost': 10}) + public_get_ext_vasps = publicGetExtVasps = Entry('ext/vasps', 'public', 'GET', {'cost': 1}) + public_post_calc_trade_avg = publicPostCalcTradeAvg = Entry('calc/trade/avg', 'public', 'POST', {'cost': 2.7}) + public_post_calc_fx = publicPostCalcFx = Entry('calc/fx', 'public', 'POST', {'cost': 2.7}) + private_post_auth_r_wallets = privatePostAuthRWallets = Entry('auth/r/wallets', 'private', 'POST', {'cost': 2.7}) + private_post_auth_r_wallets_hist = privatePostAuthRWalletsHist = Entry('auth/r/wallets/hist', 'private', 'POST', {'cost': 2.7}) + private_post_auth_r_orders = privatePostAuthROrders = Entry('auth/r/orders', 'private', 'POST', {'cost': 2.7}) + private_post_auth_r_orders_symbol = privatePostAuthROrdersSymbol = Entry('auth/r/orders/{symbol}', 'private', 'POST', {'cost': 2.7}) + private_post_auth_w_order_submit = privatePostAuthWOrderSubmit = Entry('auth/w/order/submit', 'private', 'POST', {'cost': 2.7}) + private_post_auth_w_order_update = privatePostAuthWOrderUpdate = Entry('auth/w/order/update', 'private', 'POST', {'cost': 2.7}) + private_post_auth_w_order_cancel = privatePostAuthWOrderCancel = Entry('auth/w/order/cancel', 'private', 'POST', {'cost': 2.7}) + private_post_auth_w_order_multi = privatePostAuthWOrderMulti = Entry('auth/w/order/multi', 'private', 'POST', {'cost': 2.7}) + private_post_auth_w_order_cancel_multi = privatePostAuthWOrderCancelMulti = Entry('auth/w/order/cancel/multi', 'private', 'POST', {'cost': 2.7}) + private_post_auth_r_orders_symbol_hist = privatePostAuthROrdersSymbolHist = Entry('auth/r/orders/{symbol}/hist', 'private', 'POST', {'cost': 2.7}) + private_post_auth_r_orders_hist = privatePostAuthROrdersHist = Entry('auth/r/orders/hist', 'private', 'POST', {'cost': 2.7}) + private_post_auth_r_order_symbol_id_trades = privatePostAuthROrderSymbolIdTrades = Entry('auth/r/order/{symbol}:{id}/trades', 'private', 'POST', {'cost': 2.7}) + private_post_auth_r_trades_symbol_hist = privatePostAuthRTradesSymbolHist = Entry('auth/r/trades/{symbol}/hist', 'private', 'POST', {'cost': 2.7}) + private_post_auth_r_trades_hist = privatePostAuthRTradesHist = Entry('auth/r/trades/hist', 'private', 'POST', {'cost': 2.7}) + private_post_auth_r_ledgers_currency_hist = privatePostAuthRLedgersCurrencyHist = Entry('auth/r/ledgers/{currency}/hist', 'private', 'POST', {'cost': 2.7}) + private_post_auth_r_ledgers_hist = privatePostAuthRLedgersHist = Entry('auth/r/ledgers/hist', 'private', 'POST', {'cost': 2.7}) + private_post_auth_r_info_margin_key = privatePostAuthRInfoMarginKey = Entry('auth/r/info/margin/{key}', 'private', 'POST', {'cost': 2.7}) + private_post_auth_r_info_margin_base = privatePostAuthRInfoMarginBase = Entry('auth/r/info/margin/base', 'private', 'POST', {'cost': 2.7}) + private_post_auth_r_info_margin_sym_all = privatePostAuthRInfoMarginSymAll = Entry('auth/r/info/margin/sym_all', 'private', 'POST', {'cost': 2.7}) + private_post_auth_r_positions = privatePostAuthRPositions = Entry('auth/r/positions', 'private', 'POST', {'cost': 2.7}) + private_post_auth_w_position_claim = privatePostAuthWPositionClaim = Entry('auth/w/position/claim', 'private', 'POST', {'cost': 2.7}) + private_post_auth_w_position_increase = privatePostAuthWPositionIncrease = Entry('auth/w/position/increase:', 'private', 'POST', {'cost': 2.7}) + private_post_auth_r_position_increase_info = privatePostAuthRPositionIncreaseInfo = Entry('auth/r/position/increase/info', 'private', 'POST', {'cost': 2.7}) + private_post_auth_r_positions_hist = privatePostAuthRPositionsHist = Entry('auth/r/positions/hist', 'private', 'POST', {'cost': 2.7}) + private_post_auth_r_positions_audit = privatePostAuthRPositionsAudit = Entry('auth/r/positions/audit', 'private', 'POST', {'cost': 2.7}) + private_post_auth_r_positions_snap = privatePostAuthRPositionsSnap = Entry('auth/r/positions/snap', 'private', 'POST', {'cost': 2.7}) + private_post_auth_w_deriv_collateral_set = privatePostAuthWDerivCollateralSet = Entry('auth/w/deriv/collateral/set', 'private', 'POST', {'cost': 2.7}) + private_post_auth_w_deriv_collateral_limits = privatePostAuthWDerivCollateralLimits = Entry('auth/w/deriv/collateral/limits', 'private', 'POST', {'cost': 2.7}) + private_post_auth_r_funding_offers = privatePostAuthRFundingOffers = Entry('auth/r/funding/offers', 'private', 'POST', {'cost': 2.7}) + private_post_auth_r_funding_offers_symbol = privatePostAuthRFundingOffersSymbol = Entry('auth/r/funding/offers/{symbol}', 'private', 'POST', {'cost': 2.7}) + private_post_auth_w_funding_offer_submit = privatePostAuthWFundingOfferSubmit = Entry('auth/w/funding/offer/submit', 'private', 'POST', {'cost': 2.7}) + private_post_auth_w_funding_offer_cancel = privatePostAuthWFundingOfferCancel = Entry('auth/w/funding/offer/cancel', 'private', 'POST', {'cost': 2.7}) + private_post_auth_w_funding_offer_cancel_all = privatePostAuthWFundingOfferCancelAll = Entry('auth/w/funding/offer/cancel/all', 'private', 'POST', {'cost': 2.7}) + private_post_auth_w_funding_close = privatePostAuthWFundingClose = Entry('auth/w/funding/close', 'private', 'POST', {'cost': 2.7}) + private_post_auth_w_funding_auto = privatePostAuthWFundingAuto = Entry('auth/w/funding/auto', 'private', 'POST', {'cost': 2.7}) + private_post_auth_w_funding_keep = privatePostAuthWFundingKeep = Entry('auth/w/funding/keep', 'private', 'POST', {'cost': 2.7}) + private_post_auth_r_funding_offers_symbol_hist = privatePostAuthRFundingOffersSymbolHist = Entry('auth/r/funding/offers/{symbol}/hist', 'private', 'POST', {'cost': 2.7}) + private_post_auth_r_funding_offers_hist = privatePostAuthRFundingOffersHist = Entry('auth/r/funding/offers/hist', 'private', 'POST', {'cost': 2.7}) + private_post_auth_r_funding_loans = privatePostAuthRFundingLoans = Entry('auth/r/funding/loans', 'private', 'POST', {'cost': 2.7}) + private_post_auth_r_funding_loans_hist = privatePostAuthRFundingLoansHist = Entry('auth/r/funding/loans/hist', 'private', 'POST', {'cost': 2.7}) + private_post_auth_r_funding_loans_symbol = privatePostAuthRFundingLoansSymbol = Entry('auth/r/funding/loans/{symbol}', 'private', 'POST', {'cost': 2.7}) + private_post_auth_r_funding_loans_symbol_hist = privatePostAuthRFundingLoansSymbolHist = Entry('auth/r/funding/loans/{symbol}/hist', 'private', 'POST', {'cost': 2.7}) + private_post_auth_r_funding_credits = privatePostAuthRFundingCredits = Entry('auth/r/funding/credits', 'private', 'POST', {'cost': 2.7}) + private_post_auth_r_funding_credits_hist = privatePostAuthRFundingCreditsHist = Entry('auth/r/funding/credits/hist', 'private', 'POST', {'cost': 2.7}) + private_post_auth_r_funding_credits_symbol = privatePostAuthRFundingCreditsSymbol = Entry('auth/r/funding/credits/{symbol}', 'private', 'POST', {'cost': 2.7}) + private_post_auth_r_funding_credits_symbol_hist = privatePostAuthRFundingCreditsSymbolHist = Entry('auth/r/funding/credits/{symbol}/hist', 'private', 'POST', {'cost': 2.7}) + private_post_auth_r_funding_trades_symbol_hist = privatePostAuthRFundingTradesSymbolHist = Entry('auth/r/funding/trades/{symbol}/hist', 'private', 'POST', {'cost': 2.7}) + private_post_auth_r_funding_trades_hist = privatePostAuthRFundingTradesHist = Entry('auth/r/funding/trades/hist', 'private', 'POST', {'cost': 2.7}) + private_post_auth_r_info_funding_key = privatePostAuthRInfoFundingKey = Entry('auth/r/info/funding/{key}', 'private', 'POST', {'cost': 2.7}) + private_post_auth_r_info_user = privatePostAuthRInfoUser = Entry('auth/r/info/user', 'private', 'POST', {'cost': 2.7}) + private_post_auth_r_summary = privatePostAuthRSummary = Entry('auth/r/summary', 'private', 'POST', {'cost': 2.7}) + private_post_auth_r_logins_hist = privatePostAuthRLoginsHist = Entry('auth/r/logins/hist', 'private', 'POST', {'cost': 2.7}) + private_post_auth_r_permissions = privatePostAuthRPermissions = Entry('auth/r/permissions', 'private', 'POST', {'cost': 2.7}) + private_post_auth_w_token = privatePostAuthWToken = Entry('auth/w/token', 'private', 'POST', {'cost': 2.7}) + private_post_auth_r_audit_hist = privatePostAuthRAuditHist = Entry('auth/r/audit/hist', 'private', 'POST', {'cost': 2.7}) + private_post_auth_w_transfer = privatePostAuthWTransfer = Entry('auth/w/transfer', 'private', 'POST', {'cost': 2.7}) + private_post_auth_w_deposit_address = privatePostAuthWDepositAddress = Entry('auth/w/deposit/address', 'private', 'POST', {'cost': 24}) + private_post_auth_w_deposit_invoice = privatePostAuthWDepositInvoice = Entry('auth/w/deposit/invoice', 'private', 'POST', {'cost': 24}) + private_post_auth_w_withdraw = privatePostAuthWWithdraw = Entry('auth/w/withdraw', 'private', 'POST', {'cost': 24}) + private_post_auth_r_movements_currency_hist = privatePostAuthRMovementsCurrencyHist = Entry('auth/r/movements/{currency}/hist', 'private', 'POST', {'cost': 2.7}) + private_post_auth_r_movements_hist = privatePostAuthRMovementsHist = Entry('auth/r/movements/hist', 'private', 'POST', {'cost': 2.7}) + private_post_auth_r_alerts = privatePostAuthRAlerts = Entry('auth/r/alerts', 'private', 'POST', {'cost': 5.34}) + private_post_auth_w_alert_set = privatePostAuthWAlertSet = Entry('auth/w/alert/set', 'private', 'POST', {'cost': 2.7}) + private_post_auth_w_alert_price_symbol_price_del = privatePostAuthWAlertPriceSymbolPriceDel = Entry('auth/w/alert/price:{symbol}:{price}/del', 'private', 'POST', {'cost': 2.7}) + private_post_auth_w_alert_type_symbol_price_del = privatePostAuthWAlertTypeSymbolPriceDel = Entry('auth/w/alert/{type}:{symbol}:{price}/del', 'private', 'POST', {'cost': 2.7}) + private_post_auth_calc_order_avail = privatePostAuthCalcOrderAvail = Entry('auth/calc/order/avail', 'private', 'POST', {'cost': 2.7}) + private_post_auth_w_settings_set = privatePostAuthWSettingsSet = Entry('auth/w/settings/set', 'private', 'POST', {'cost': 2.7}) + private_post_auth_r_settings = privatePostAuthRSettings = Entry('auth/r/settings', 'private', 'POST', {'cost': 2.7}) + private_post_auth_w_settings_del = privatePostAuthWSettingsDel = Entry('auth/w/settings/del', 'private', 'POST', {'cost': 2.7}) + private_post_auth_r_pulse_hist = privatePostAuthRPulseHist = Entry('auth/r/pulse/hist', 'private', 'POST', {'cost': 2.7}) + private_post_auth_w_pulse_add = privatePostAuthWPulseAdd = Entry('auth/w/pulse/add', 'private', 'POST', {'cost': 16}) + private_post_auth_w_pulse_del = privatePostAuthWPulseDel = Entry('auth/w/pulse/del', 'private', 'POST', {'cost': 2.7}) diff --git a/ccxt/abstract/bitflyer.py b/ccxt/abstract/bitflyer.py new file mode 100644 index 0000000..0944ed5 --- /dev/null +++ b/ccxt/abstract/bitflyer.py @@ -0,0 +1,39 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_getmarkets_usa = publicGetGetmarketsUsa = Entry('getmarkets/usa', 'public', 'GET', {}) + public_get_getmarkets_eu = publicGetGetmarketsEu = Entry('getmarkets/eu', 'public', 'GET', {}) + public_get_getmarkets = publicGetGetmarkets = Entry('getmarkets', 'public', 'GET', {}) + public_get_getboard = publicGetGetboard = Entry('getboard', 'public', 'GET', {}) + public_get_getticker = publicGetGetticker = Entry('getticker', 'public', 'GET', {}) + public_get_getexecutions = publicGetGetexecutions = Entry('getexecutions', 'public', 'GET', {}) + public_get_gethealth = publicGetGethealth = Entry('gethealth', 'public', 'GET', {}) + public_get_getboardstate = publicGetGetboardstate = Entry('getboardstate', 'public', 'GET', {}) + public_get_getchats = publicGetGetchats = Entry('getchats', 'public', 'GET', {}) + public_get_getfundingrate = publicGetGetfundingrate = Entry('getfundingrate', 'public', 'GET', {}) + private_get_getpermissions = privateGetGetpermissions = Entry('getpermissions', 'private', 'GET', {}) + private_get_getbalance = privateGetGetbalance = Entry('getbalance', 'private', 'GET', {}) + private_get_getbalancehistory = privateGetGetbalancehistory = Entry('getbalancehistory', 'private', 'GET', {}) + private_get_getcollateral = privateGetGetcollateral = Entry('getcollateral', 'private', 'GET', {}) + private_get_getcollateralhistory = privateGetGetcollateralhistory = Entry('getcollateralhistory', 'private', 'GET', {}) + private_get_getcollateralaccounts = privateGetGetcollateralaccounts = Entry('getcollateralaccounts', 'private', 'GET', {}) + private_get_getaddresses = privateGetGetaddresses = Entry('getaddresses', 'private', 'GET', {}) + private_get_getcoinins = privateGetGetcoinins = Entry('getcoinins', 'private', 'GET', {}) + private_get_getcoinouts = privateGetGetcoinouts = Entry('getcoinouts', 'private', 'GET', {}) + private_get_getbankaccounts = privateGetGetbankaccounts = Entry('getbankaccounts', 'private', 'GET', {}) + private_get_getdeposits = privateGetGetdeposits = Entry('getdeposits', 'private', 'GET', {}) + private_get_getwithdrawals = privateGetGetwithdrawals = Entry('getwithdrawals', 'private', 'GET', {}) + private_get_getchildorders = privateGetGetchildorders = Entry('getchildorders', 'private', 'GET', {}) + private_get_getparentorders = privateGetGetparentorders = Entry('getparentorders', 'private', 'GET', {}) + private_get_getparentorder = privateGetGetparentorder = Entry('getparentorder', 'private', 'GET', {}) + private_get_getexecutions = privateGetGetexecutions = Entry('getexecutions', 'private', 'GET', {}) + private_get_getpositions = privateGetGetpositions = Entry('getpositions', 'private', 'GET', {}) + private_get_gettradingcommission = privateGetGettradingcommission = Entry('gettradingcommission', 'private', 'GET', {}) + private_post_sendcoin = privatePostSendcoin = Entry('sendcoin', 'private', 'POST', {}) + private_post_withdraw = privatePostWithdraw = Entry('withdraw', 'private', 'POST', {}) + private_post_sendchildorder = privatePostSendchildorder = Entry('sendchildorder', 'private', 'POST', {}) + private_post_cancelchildorder = privatePostCancelchildorder = Entry('cancelchildorder', 'private', 'POST', {}) + private_post_sendparentorder = privatePostSendparentorder = Entry('sendparentorder', 'private', 'POST', {}) + private_post_cancelparentorder = privatePostCancelparentorder = Entry('cancelparentorder', 'private', 'POST', {}) + private_post_cancelallchildorders = privatePostCancelallchildorders = Entry('cancelallchildorders', 'private', 'POST', {}) diff --git a/ccxt/abstract/bitget.py b/ccxt/abstract/bitget.py new file mode 100644 index 0000000..9bcc8e3 --- /dev/null +++ b/ccxt/abstract/bitget.py @@ -0,0 +1,576 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_common_get_v2_public_annoucements = publicCommonGetV2PublicAnnoucements = Entry('v2/public/annoucements', ['public', 'common'], 'GET', {'cost': 1}) + public_common_get_v2_public_time = publicCommonGetV2PublicTime = Entry('v2/public/time', ['public', 'common'], 'GET', {'cost': 1}) + public_spot_get_spot_v1_notice_queryallnotices = publicSpotGetSpotV1NoticeQueryAllNotices = Entry('spot/v1/notice/queryAllNotices', ['public', 'spot'], 'GET', {'cost': 1}) + public_spot_get_spot_v1_public_time = publicSpotGetSpotV1PublicTime = Entry('spot/v1/public/time', ['public', 'spot'], 'GET', {'cost': 1}) + public_spot_get_spot_v1_public_currencies = publicSpotGetSpotV1PublicCurrencies = Entry('spot/v1/public/currencies', ['public', 'spot'], 'GET', {'cost': 6.6667}) + public_spot_get_spot_v1_public_products = publicSpotGetSpotV1PublicProducts = Entry('spot/v1/public/products', ['public', 'spot'], 'GET', {'cost': 1}) + public_spot_get_spot_v1_public_product = publicSpotGetSpotV1PublicProduct = Entry('spot/v1/public/product', ['public', 'spot'], 'GET', {'cost': 1}) + public_spot_get_spot_v1_market_ticker = publicSpotGetSpotV1MarketTicker = Entry('spot/v1/market/ticker', ['public', 'spot'], 'GET', {'cost': 1}) + public_spot_get_spot_v1_market_tickers = publicSpotGetSpotV1MarketTickers = Entry('spot/v1/market/tickers', ['public', 'spot'], 'GET', {'cost': 1}) + public_spot_get_spot_v1_market_fills = publicSpotGetSpotV1MarketFills = Entry('spot/v1/market/fills', ['public', 'spot'], 'GET', {'cost': 2}) + public_spot_get_spot_v1_market_fills_history = publicSpotGetSpotV1MarketFillsHistory = Entry('spot/v1/market/fills-history', ['public', 'spot'], 'GET', {'cost': 2}) + public_spot_get_spot_v1_market_candles = publicSpotGetSpotV1MarketCandles = Entry('spot/v1/market/candles', ['public', 'spot'], 'GET', {'cost': 1}) + public_spot_get_spot_v1_market_depth = publicSpotGetSpotV1MarketDepth = Entry('spot/v1/market/depth', ['public', 'spot'], 'GET', {'cost': 1}) + public_spot_get_spot_v1_market_spot_vip_level = publicSpotGetSpotV1MarketSpotVipLevel = Entry('spot/v1/market/spot-vip-level', ['public', 'spot'], 'GET', {'cost': 2}) + public_spot_get_spot_v1_market_merge_depth = publicSpotGetSpotV1MarketMergeDepth = Entry('spot/v1/market/merge-depth', ['public', 'spot'], 'GET', {'cost': 1}) + public_spot_get_spot_v1_market_history_candles = publicSpotGetSpotV1MarketHistoryCandles = Entry('spot/v1/market/history-candles', ['public', 'spot'], 'GET', {'cost': 1}) + public_spot_get_spot_v1_public_loan_coininfos = publicSpotGetSpotV1PublicLoanCoinInfos = Entry('spot/v1/public/loan/coinInfos', ['public', 'spot'], 'GET', {'cost': 2}) + public_spot_get_spot_v1_public_loan_hour_interest = publicSpotGetSpotV1PublicLoanHourInterest = Entry('spot/v1/public/loan/hour-interest', ['public', 'spot'], 'GET', {'cost': 2}) + public_spot_get_v2_spot_public_coins = publicSpotGetV2SpotPublicCoins = Entry('v2/spot/public/coins', ['public', 'spot'], 'GET', {'cost': 6.6667}) + public_spot_get_v2_spot_public_symbols = publicSpotGetV2SpotPublicSymbols = Entry('v2/spot/public/symbols', ['public', 'spot'], 'GET', {'cost': 1}) + public_spot_get_v2_spot_market_vip_fee_rate = publicSpotGetV2SpotMarketVipFeeRate = Entry('v2/spot/market/vip-fee-rate', ['public', 'spot'], 'GET', {'cost': 2}) + public_spot_get_v2_spot_market_tickers = publicSpotGetV2SpotMarketTickers = Entry('v2/spot/market/tickers', ['public', 'spot'], 'GET', {'cost': 1}) + public_spot_get_v2_spot_market_merge_depth = publicSpotGetV2SpotMarketMergeDepth = Entry('v2/spot/market/merge-depth', ['public', 'spot'], 'GET', {'cost': 1}) + public_spot_get_v2_spot_market_orderbook = publicSpotGetV2SpotMarketOrderbook = Entry('v2/spot/market/orderbook', ['public', 'spot'], 'GET', {'cost': 1}) + public_spot_get_v2_spot_market_candles = publicSpotGetV2SpotMarketCandles = Entry('v2/spot/market/candles', ['public', 'spot'], 'GET', {'cost': 1}) + public_spot_get_v2_spot_market_history_candles = publicSpotGetV2SpotMarketHistoryCandles = Entry('v2/spot/market/history-candles', ['public', 'spot'], 'GET', {'cost': 1}) + public_spot_get_v2_spot_market_fills = publicSpotGetV2SpotMarketFills = Entry('v2/spot/market/fills', ['public', 'spot'], 'GET', {'cost': 2}) + public_spot_get_v2_spot_market_fills_history = publicSpotGetV2SpotMarketFillsHistory = Entry('v2/spot/market/fills-history', ['public', 'spot'], 'GET', {'cost': 2}) + public_mix_get_mix_v1_market_contracts = publicMixGetMixV1MarketContracts = Entry('mix/v1/market/contracts', ['public', 'mix'], 'GET', {'cost': 1}) + public_mix_get_mix_v1_market_depth = publicMixGetMixV1MarketDepth = Entry('mix/v1/market/depth', ['public', 'mix'], 'GET', {'cost': 1}) + public_mix_get_mix_v1_market_ticker = publicMixGetMixV1MarketTicker = Entry('mix/v1/market/ticker', ['public', 'mix'], 'GET', {'cost': 1}) + public_mix_get_mix_v1_market_tickers = publicMixGetMixV1MarketTickers = Entry('mix/v1/market/tickers', ['public', 'mix'], 'GET', {'cost': 1}) + public_mix_get_mix_v1_market_contract_vip_level = publicMixGetMixV1MarketContractVipLevel = Entry('mix/v1/market/contract-vip-level', ['public', 'mix'], 'GET', {'cost': 2}) + public_mix_get_mix_v1_market_fills = publicMixGetMixV1MarketFills = Entry('mix/v1/market/fills', ['public', 'mix'], 'GET', {'cost': 1}) + public_mix_get_mix_v1_market_fills_history = publicMixGetMixV1MarketFillsHistory = Entry('mix/v1/market/fills-history', ['public', 'mix'], 'GET', {'cost': 2}) + public_mix_get_mix_v1_market_candles = publicMixGetMixV1MarketCandles = Entry('mix/v1/market/candles', ['public', 'mix'], 'GET', {'cost': 1}) + public_mix_get_mix_v1_market_index = publicMixGetMixV1MarketIndex = Entry('mix/v1/market/index', ['public', 'mix'], 'GET', {'cost': 1}) + public_mix_get_mix_v1_market_funding_time = publicMixGetMixV1MarketFundingTime = Entry('mix/v1/market/funding-time', ['public', 'mix'], 'GET', {'cost': 1}) + public_mix_get_mix_v1_market_history_fundrate = publicMixGetMixV1MarketHistoryFundRate = Entry('mix/v1/market/history-fundRate', ['public', 'mix'], 'GET', {'cost': 1}) + public_mix_get_mix_v1_market_current_fundrate = publicMixGetMixV1MarketCurrentFundRate = Entry('mix/v1/market/current-fundRate', ['public', 'mix'], 'GET', {'cost': 1}) + public_mix_get_mix_v1_market_open_interest = publicMixGetMixV1MarketOpenInterest = Entry('mix/v1/market/open-interest', ['public', 'mix'], 'GET', {'cost': 1}) + public_mix_get_mix_v1_market_mark_price = publicMixGetMixV1MarketMarkPrice = Entry('mix/v1/market/mark-price', ['public', 'mix'], 'GET', {'cost': 1}) + public_mix_get_mix_v1_market_symbol_leverage = publicMixGetMixV1MarketSymbolLeverage = Entry('mix/v1/market/symbol-leverage', ['public', 'mix'], 'GET', {'cost': 1}) + public_mix_get_mix_v1_market_querypositionlever = publicMixGetMixV1MarketQueryPositionLever = Entry('mix/v1/market/queryPositionLever', ['public', 'mix'], 'GET', {'cost': 1}) + public_mix_get_mix_v1_market_open_limit = publicMixGetMixV1MarketOpenLimit = Entry('mix/v1/market/open-limit', ['public', 'mix'], 'GET', {'cost': 1}) + public_mix_get_mix_v1_market_history_candles = publicMixGetMixV1MarketHistoryCandles = Entry('mix/v1/market/history-candles', ['public', 'mix'], 'GET', {'cost': 1}) + public_mix_get_mix_v1_market_history_index_candles = publicMixGetMixV1MarketHistoryIndexCandles = Entry('mix/v1/market/history-index-candles', ['public', 'mix'], 'GET', {'cost': 1}) + public_mix_get_mix_v1_market_history_mark_candles = publicMixGetMixV1MarketHistoryMarkCandles = Entry('mix/v1/market/history-mark-candles', ['public', 'mix'], 'GET', {'cost': 1}) + public_mix_get_mix_v1_market_merge_depth = publicMixGetMixV1MarketMergeDepth = Entry('mix/v1/market/merge-depth', ['public', 'mix'], 'GET', {'cost': 1}) + public_mix_get_v2_mix_market_vip_fee_rate = publicMixGetV2MixMarketVipFeeRate = Entry('v2/mix/market/vip-fee-rate', ['public', 'mix'], 'GET', {'cost': 2}) + public_mix_get_v2_mix_market_merge_depth = publicMixGetV2MixMarketMergeDepth = Entry('v2/mix/market/merge-depth', ['public', 'mix'], 'GET', {'cost': 1}) + public_mix_get_v2_mix_market_ticker = publicMixGetV2MixMarketTicker = Entry('v2/mix/market/ticker', ['public', 'mix'], 'GET', {'cost': 1}) + public_mix_get_v2_mix_market_tickers = publicMixGetV2MixMarketTickers = Entry('v2/mix/market/tickers', ['public', 'mix'], 'GET', {'cost': 1}) + public_mix_get_v2_mix_market_fills = publicMixGetV2MixMarketFills = Entry('v2/mix/market/fills', ['public', 'mix'], 'GET', {'cost': 1}) + public_mix_get_v2_mix_market_fills_history = publicMixGetV2MixMarketFillsHistory = Entry('v2/mix/market/fills-history', ['public', 'mix'], 'GET', {'cost': 2}) + public_mix_get_v2_mix_market_candles = publicMixGetV2MixMarketCandles = Entry('v2/mix/market/candles', ['public', 'mix'], 'GET', {'cost': 1}) + public_mix_get_v2_mix_market_history_candles = publicMixGetV2MixMarketHistoryCandles = Entry('v2/mix/market/history-candles', ['public', 'mix'], 'GET', {'cost': 1}) + public_mix_get_v2_mix_market_history_index_candles = publicMixGetV2MixMarketHistoryIndexCandles = Entry('v2/mix/market/history-index-candles', ['public', 'mix'], 'GET', {'cost': 1}) + public_mix_get_v2_mix_market_history_mark_candles = publicMixGetV2MixMarketHistoryMarkCandles = Entry('v2/mix/market/history-mark-candles', ['public', 'mix'], 'GET', {'cost': 1}) + public_mix_get_v2_mix_market_open_interest = publicMixGetV2MixMarketOpenInterest = Entry('v2/mix/market/open-interest', ['public', 'mix'], 'GET', {'cost': 1}) + public_mix_get_v2_mix_market_funding_time = publicMixGetV2MixMarketFundingTime = Entry('v2/mix/market/funding-time', ['public', 'mix'], 'GET', {'cost': 1}) + public_mix_get_v2_mix_market_symbol_price = publicMixGetV2MixMarketSymbolPrice = Entry('v2/mix/market/symbol-price', ['public', 'mix'], 'GET', {'cost': 1}) + public_mix_get_v2_mix_market_history_fund_rate = publicMixGetV2MixMarketHistoryFundRate = Entry('v2/mix/market/history-fund-rate', ['public', 'mix'], 'GET', {'cost': 1}) + public_mix_get_v2_mix_market_current_fund_rate = publicMixGetV2MixMarketCurrentFundRate = Entry('v2/mix/market/current-fund-rate', ['public', 'mix'], 'GET', {'cost': 1}) + public_mix_get_v2_mix_market_contracts = publicMixGetV2MixMarketContracts = Entry('v2/mix/market/contracts', ['public', 'mix'], 'GET', {'cost': 1}) + public_mix_get_v2_mix_market_query_position_lever = publicMixGetV2MixMarketQueryPositionLever = Entry('v2/mix/market/query-position-lever', ['public', 'mix'], 'GET', {'cost': 2}) + public_mix_get_v2_mix_market_account_long_short = publicMixGetV2MixMarketAccountLongShort = Entry('v2/mix/market/account-long-short', ['public', 'mix'], 'GET', {'cost': 20}) + public_margin_get_margin_v1_cross_public_interestrateandlimit = publicMarginGetMarginV1CrossPublicInterestRateAndLimit = Entry('margin/v1/cross/public/interestRateAndLimit', ['public', 'margin'], 'GET', {'cost': 2}) + public_margin_get_margin_v1_isolated_public_interestrateandlimit = publicMarginGetMarginV1IsolatedPublicInterestRateAndLimit = Entry('margin/v1/isolated/public/interestRateAndLimit', ['public', 'margin'], 'GET', {'cost': 2}) + public_margin_get_margin_v1_cross_public_tierdata = publicMarginGetMarginV1CrossPublicTierData = Entry('margin/v1/cross/public/tierData', ['public', 'margin'], 'GET', {'cost': 2}) + public_margin_get_margin_v1_isolated_public_tierdata = publicMarginGetMarginV1IsolatedPublicTierData = Entry('margin/v1/isolated/public/tierData', ['public', 'margin'], 'GET', {'cost': 2}) + public_margin_get_margin_v1_public_currencies = publicMarginGetMarginV1PublicCurrencies = Entry('margin/v1/public/currencies', ['public', 'margin'], 'GET', {'cost': 1}) + public_margin_get_v2_margin_currencies = publicMarginGetV2MarginCurrencies = Entry('v2/margin/currencies', ['public', 'margin'], 'GET', {'cost': 2}) + public_margin_get_v2_margin_market_long_short_ratio = publicMarginGetV2MarginMarketLongShortRatio = Entry('v2/margin/market/long-short-ratio', ['public', 'margin'], 'GET', {'cost': 20}) + public_earn_get_v2_earn_loan_public_coininfos = publicEarnGetV2EarnLoanPublicCoinInfos = Entry('v2/earn/loan/public/coinInfos', ['public', 'earn'], 'GET', {'cost': 2}) + public_earn_get_v2_earn_loan_public_hour_interest = publicEarnGetV2EarnLoanPublicHourInterest = Entry('v2/earn/loan/public/hour-interest', ['public', 'earn'], 'GET', {'cost': 2}) + public_uta_get_v3_market_instruments = publicUtaGetV3MarketInstruments = Entry('v3/market/instruments', ['public', 'uta'], 'GET', {'cost': 1}) + public_uta_get_v3_market_tickers = publicUtaGetV3MarketTickers = Entry('v3/market/tickers', ['public', 'uta'], 'GET', {'cost': 1}) + public_uta_get_v3_market_orderbook = publicUtaGetV3MarketOrderbook = Entry('v3/market/orderbook', ['public', 'uta'], 'GET', {'cost': 1}) + public_uta_get_v3_market_fills = publicUtaGetV3MarketFills = Entry('v3/market/fills', ['public', 'uta'], 'GET', {'cost': 1}) + public_uta_get_v3_market_open_interest = publicUtaGetV3MarketOpenInterest = Entry('v3/market/open-interest', ['public', 'uta'], 'GET', {'cost': 1}) + public_uta_get_v3_market_candles = publicUtaGetV3MarketCandles = Entry('v3/market/candles', ['public', 'uta'], 'GET', {'cost': 1}) + public_uta_get_v3_market_history_candles = publicUtaGetV3MarketHistoryCandles = Entry('v3/market/history-candles', ['public', 'uta'], 'GET', {'cost': 1}) + public_uta_get_v3_market_current_fund_rate = publicUtaGetV3MarketCurrentFundRate = Entry('v3/market/current-fund-rate', ['public', 'uta'], 'GET', {'cost': 1}) + public_uta_get_v3_market_history_fund_rate = publicUtaGetV3MarketHistoryFundRate = Entry('v3/market/history-fund-rate', ['public', 'uta'], 'GET', {'cost': 1}) + public_uta_get_v3_market_risk_reserve = publicUtaGetV3MarketRiskReserve = Entry('v3/market/risk-reserve', ['public', 'uta'], 'GET', {'cost': 1}) + public_uta_get_v3_market_discount_rate = publicUtaGetV3MarketDiscountRate = Entry('v3/market/discount-rate', ['public', 'uta'], 'GET', {'cost': 1}) + public_uta_get_v3_market_margin_loans = publicUtaGetV3MarketMarginLoans = Entry('v3/market/margin-loans', ['public', 'uta'], 'GET', {'cost': 1}) + public_uta_get_v3_market_position_tier = publicUtaGetV3MarketPositionTier = Entry('v3/market/position-tier', ['public', 'uta'], 'GET', {'cost': 1}) + public_uta_get_v3_market_oi_limit = publicUtaGetV3MarketOiLimit = Entry('v3/market/oi-limit', ['public', 'uta'], 'GET', {'cost': 2}) + private_spot_get_spot_v1_wallet_deposit_address = privateSpotGetSpotV1WalletDepositAddress = Entry('spot/v1/wallet/deposit-address', ['private', 'spot'], 'GET', {'cost': 4}) + private_spot_get_spot_v1_wallet_withdrawal_list = privateSpotGetSpotV1WalletWithdrawalList = Entry('spot/v1/wallet/withdrawal-list', ['private', 'spot'], 'GET', {'cost': 1}) + private_spot_get_spot_v1_wallet_deposit_list = privateSpotGetSpotV1WalletDepositList = Entry('spot/v1/wallet/deposit-list', ['private', 'spot'], 'GET', {'cost': 1}) + private_spot_get_spot_v1_account_getinfo = privateSpotGetSpotV1AccountGetInfo = Entry('spot/v1/account/getInfo', ['private', 'spot'], 'GET', {'cost': 20}) + private_spot_get_spot_v1_account_assets = privateSpotGetSpotV1AccountAssets = Entry('spot/v1/account/assets', ['private', 'spot'], 'GET', {'cost': 2}) + private_spot_get_spot_v1_account_assets_lite = privateSpotGetSpotV1AccountAssetsLite = Entry('spot/v1/account/assets-lite', ['private', 'spot'], 'GET', {'cost': 2}) + private_spot_get_spot_v1_account_transferrecords = privateSpotGetSpotV1AccountTransferRecords = Entry('spot/v1/account/transferRecords', ['private', 'spot'], 'GET', {'cost': 1}) + private_spot_get_spot_v1_convert_currencies = privateSpotGetSpotV1ConvertCurrencies = Entry('spot/v1/convert/currencies', ['private', 'spot'], 'GET', {'cost': 2}) + private_spot_get_spot_v1_convert_convert_record = privateSpotGetSpotV1ConvertConvertRecord = Entry('spot/v1/convert/convert-record', ['private', 'spot'], 'GET', {'cost': 2}) + private_spot_get_spot_v1_loan_ongoing_orders = privateSpotGetSpotV1LoanOngoingOrders = Entry('spot/v1/loan/ongoing-orders', ['private', 'spot'], 'GET', {'cost': 2}) + private_spot_get_spot_v1_loan_repay_history = privateSpotGetSpotV1LoanRepayHistory = Entry('spot/v1/loan/repay-history', ['private', 'spot'], 'GET', {'cost': 2}) + private_spot_get_spot_v1_loan_revise_history = privateSpotGetSpotV1LoanReviseHistory = Entry('spot/v1/loan/revise-history', ['private', 'spot'], 'GET', {'cost': 2}) + private_spot_get_spot_v1_loan_borrow_history = privateSpotGetSpotV1LoanBorrowHistory = Entry('spot/v1/loan/borrow-history', ['private', 'spot'], 'GET', {'cost': 2}) + private_spot_get_spot_v1_loan_debts = privateSpotGetSpotV1LoanDebts = Entry('spot/v1/loan/debts', ['private', 'spot'], 'GET', {'cost': 2}) + private_spot_get_v2_spot_trade_orderinfo = privateSpotGetV2SpotTradeOrderInfo = Entry('v2/spot/trade/orderInfo', ['private', 'spot'], 'GET', {'cost': 1}) + private_spot_get_v2_spot_trade_unfilled_orders = privateSpotGetV2SpotTradeUnfilledOrders = Entry('v2/spot/trade/unfilled-orders', ['private', 'spot'], 'GET', {'cost': 1}) + private_spot_get_v2_spot_trade_history_orders = privateSpotGetV2SpotTradeHistoryOrders = Entry('v2/spot/trade/history-orders', ['private', 'spot'], 'GET', {'cost': 1}) + private_spot_get_v2_spot_trade_fills = privateSpotGetV2SpotTradeFills = Entry('v2/spot/trade/fills', ['private', 'spot'], 'GET', {'cost': 2}) + private_spot_get_v2_spot_trade_current_plan_order = privateSpotGetV2SpotTradeCurrentPlanOrder = Entry('v2/spot/trade/current-plan-order', ['private', 'spot'], 'GET', {'cost': 1}) + private_spot_get_v2_spot_trade_history_plan_order = privateSpotGetV2SpotTradeHistoryPlanOrder = Entry('v2/spot/trade/history-plan-order', ['private', 'spot'], 'GET', {'cost': 1}) + private_spot_get_v2_spot_account_info = privateSpotGetV2SpotAccountInfo = Entry('v2/spot/account/info', ['private', 'spot'], 'GET', {'cost': 20}) + private_spot_get_v2_spot_account_assets = privateSpotGetV2SpotAccountAssets = Entry('v2/spot/account/assets', ['private', 'spot'], 'GET', {'cost': 2}) + private_spot_get_v2_spot_account_subaccount_assets = privateSpotGetV2SpotAccountSubaccountAssets = Entry('v2/spot/account/subaccount-assets', ['private', 'spot'], 'GET', {'cost': 2}) + private_spot_get_v2_spot_account_bills = privateSpotGetV2SpotAccountBills = Entry('v2/spot/account/bills', ['private', 'spot'], 'GET', {'cost': 2}) + private_spot_get_v2_spot_account_transferrecords = privateSpotGetV2SpotAccountTransferRecords = Entry('v2/spot/account/transferRecords', ['private', 'spot'], 'GET', {'cost': 1}) + private_spot_get_v2_account_funding_assets = privateSpotGetV2AccountFundingAssets = Entry('v2/account/funding-assets', ['private', 'spot'], 'GET', {'cost': 2}) + private_spot_get_v2_account_bot_assets = privateSpotGetV2AccountBotAssets = Entry('v2/account/bot-assets', ['private', 'spot'], 'GET', {'cost': 2}) + private_spot_get_v2_account_all_account_balance = privateSpotGetV2AccountAllAccountBalance = Entry('v2/account/all-account-balance', ['private', 'spot'], 'GET', {'cost': 20}) + private_spot_get_v2_spot_wallet_deposit_address = privateSpotGetV2SpotWalletDepositAddress = Entry('v2/spot/wallet/deposit-address', ['private', 'spot'], 'GET', {'cost': 2}) + private_spot_get_v2_spot_wallet_deposit_records = privateSpotGetV2SpotWalletDepositRecords = Entry('v2/spot/wallet/deposit-records', ['private', 'spot'], 'GET', {'cost': 2}) + private_spot_get_v2_spot_wallet_withdrawal_records = privateSpotGetV2SpotWalletWithdrawalRecords = Entry('v2/spot/wallet/withdrawal-records', ['private', 'spot'], 'GET', {'cost': 2}) + private_spot_post_spot_v1_wallet_transfer = privateSpotPostSpotV1WalletTransfer = Entry('spot/v1/wallet/transfer', ['private', 'spot'], 'POST', {'cost': 4}) + private_spot_post_spot_v1_wallet_transfer_v2 = privateSpotPostSpotV1WalletTransferV2 = Entry('spot/v1/wallet/transfer-v2', ['private', 'spot'], 'POST', {'cost': 4}) + private_spot_post_spot_v1_wallet_subtransfer = privateSpotPostSpotV1WalletSubTransfer = Entry('spot/v1/wallet/subTransfer', ['private', 'spot'], 'POST', {'cost': 10}) + private_spot_post_spot_v1_wallet_withdrawal = privateSpotPostSpotV1WalletWithdrawal = Entry('spot/v1/wallet/withdrawal', ['private', 'spot'], 'POST', {'cost': 4}) + private_spot_post_spot_v1_wallet_withdrawal_v2 = privateSpotPostSpotV1WalletWithdrawalV2 = Entry('spot/v1/wallet/withdrawal-v2', ['private', 'spot'], 'POST', {'cost': 4}) + private_spot_post_spot_v1_wallet_withdrawal_inner = privateSpotPostSpotV1WalletWithdrawalInner = Entry('spot/v1/wallet/withdrawal-inner', ['private', 'spot'], 'POST', {'cost': 4}) + private_spot_post_spot_v1_wallet_withdrawal_inner_v2 = privateSpotPostSpotV1WalletWithdrawalInnerV2 = Entry('spot/v1/wallet/withdrawal-inner-v2', ['private', 'spot'], 'POST', {'cost': 4}) + private_spot_post_spot_v1_account_sub_account_spot_assets = privateSpotPostSpotV1AccountSubAccountSpotAssets = Entry('spot/v1/account/sub-account-spot-assets', ['private', 'spot'], 'POST', {'cost': 200}) + private_spot_post_spot_v1_account_bills = privateSpotPostSpotV1AccountBills = Entry('spot/v1/account/bills', ['private', 'spot'], 'POST', {'cost': 2}) + private_spot_post_spot_v1_trade_orders = privateSpotPostSpotV1TradeOrders = Entry('spot/v1/trade/orders', ['private', 'spot'], 'POST', {'cost': 2}) + private_spot_post_spot_v1_trade_batch_orders = privateSpotPostSpotV1TradeBatchOrders = Entry('spot/v1/trade/batch-orders', ['private', 'spot'], 'POST', {'cost': 4}) + private_spot_post_spot_v1_trade_cancel_order = privateSpotPostSpotV1TradeCancelOrder = Entry('spot/v1/trade/cancel-order', ['private', 'spot'], 'POST', {'cost': 2}) + private_spot_post_spot_v1_trade_cancel_order_v2 = privateSpotPostSpotV1TradeCancelOrderV2 = Entry('spot/v1/trade/cancel-order-v2', ['private', 'spot'], 'POST', {'cost': 2}) + private_spot_post_spot_v1_trade_cancel_symbol_order = privateSpotPostSpotV1TradeCancelSymbolOrder = Entry('spot/v1/trade/cancel-symbol-order', ['private', 'spot'], 'POST', {'cost': 2}) + private_spot_post_spot_v1_trade_cancel_batch_orders = privateSpotPostSpotV1TradeCancelBatchOrders = Entry('spot/v1/trade/cancel-batch-orders', ['private', 'spot'], 'POST', {'cost': 4}) + private_spot_post_spot_v1_trade_cancel_batch_orders_v2 = privateSpotPostSpotV1TradeCancelBatchOrdersV2 = Entry('spot/v1/trade/cancel-batch-orders-v2', ['private', 'spot'], 'POST', {'cost': 4}) + private_spot_post_spot_v1_trade_orderinfo = privateSpotPostSpotV1TradeOrderInfo = Entry('spot/v1/trade/orderInfo', ['private', 'spot'], 'POST', {'cost': 1}) + private_spot_post_spot_v1_trade_open_orders = privateSpotPostSpotV1TradeOpenOrders = Entry('spot/v1/trade/open-orders', ['private', 'spot'], 'POST', {'cost': 1}) + private_spot_post_spot_v1_trade_history = privateSpotPostSpotV1TradeHistory = Entry('spot/v1/trade/history', ['private', 'spot'], 'POST', {'cost': 1}) + private_spot_post_spot_v1_trade_fills = privateSpotPostSpotV1TradeFills = Entry('spot/v1/trade/fills', ['private', 'spot'], 'POST', {'cost': 1}) + private_spot_post_spot_v1_plan_placeplan = privateSpotPostSpotV1PlanPlacePlan = Entry('spot/v1/plan/placePlan', ['private', 'spot'], 'POST', {'cost': 1}) + private_spot_post_spot_v1_plan_modifyplan = privateSpotPostSpotV1PlanModifyPlan = Entry('spot/v1/plan/modifyPlan', ['private', 'spot'], 'POST', {'cost': 1}) + private_spot_post_spot_v1_plan_cancelplan = privateSpotPostSpotV1PlanCancelPlan = Entry('spot/v1/plan/cancelPlan', ['private', 'spot'], 'POST', {'cost': 1}) + private_spot_post_spot_v1_plan_currentplan = privateSpotPostSpotV1PlanCurrentPlan = Entry('spot/v1/plan/currentPlan', ['private', 'spot'], 'POST', {'cost': 1}) + private_spot_post_spot_v1_plan_historyplan = privateSpotPostSpotV1PlanHistoryPlan = Entry('spot/v1/plan/historyPlan', ['private', 'spot'], 'POST', {'cost': 1}) + private_spot_post_spot_v1_plan_batchcancelplan = privateSpotPostSpotV1PlanBatchCancelPlan = Entry('spot/v1/plan/batchCancelPlan', ['private', 'spot'], 'POST', {'cost': 2}) + private_spot_post_spot_v1_convert_quoted_price = privateSpotPostSpotV1ConvertQuotedPrice = Entry('spot/v1/convert/quoted-price', ['private', 'spot'], 'POST', {'cost': 4}) + private_spot_post_spot_v1_convert_trade = privateSpotPostSpotV1ConvertTrade = Entry('spot/v1/convert/trade', ['private', 'spot'], 'POST', {'cost': 4}) + private_spot_post_spot_v1_loan_borrow = privateSpotPostSpotV1LoanBorrow = Entry('spot/v1/loan/borrow', ['private', 'spot'], 'POST', {'cost': 2}) + private_spot_post_spot_v1_loan_repay = privateSpotPostSpotV1LoanRepay = Entry('spot/v1/loan/repay', ['private', 'spot'], 'POST', {'cost': 2}) + private_spot_post_spot_v1_loan_revise_pledge = privateSpotPostSpotV1LoanRevisePledge = Entry('spot/v1/loan/revise-pledge', ['private', 'spot'], 'POST', {'cost': 2}) + private_spot_post_spot_v1_trace_order_ordercurrentlist = privateSpotPostSpotV1TraceOrderOrderCurrentList = Entry('spot/v1/trace/order/orderCurrentList', ['private', 'spot'], 'POST', {'cost': 2}) + private_spot_post_spot_v1_trace_order_orderhistorylist = privateSpotPostSpotV1TraceOrderOrderHistoryList = Entry('spot/v1/trace/order/orderHistoryList', ['private', 'spot'], 'POST', {'cost': 2}) + private_spot_post_spot_v1_trace_order_closetrackingorder = privateSpotPostSpotV1TraceOrderCloseTrackingOrder = Entry('spot/v1/trace/order/closeTrackingOrder', ['private', 'spot'], 'POST', {'cost': 2}) + private_spot_post_spot_v1_trace_order_updatetpsl = privateSpotPostSpotV1TraceOrderUpdateTpsl = Entry('spot/v1/trace/order/updateTpsl', ['private', 'spot'], 'POST', {'cost': 2}) + private_spot_post_spot_v1_trace_order_followerendorder = privateSpotPostSpotV1TraceOrderFollowerEndOrder = Entry('spot/v1/trace/order/followerEndOrder', ['private', 'spot'], 'POST', {'cost': 2}) + private_spot_post_spot_v1_trace_order_spotinfolist = privateSpotPostSpotV1TraceOrderSpotInfoList = Entry('spot/v1/trace/order/spotInfoList', ['private', 'spot'], 'POST', {'cost': 2}) + private_spot_post_spot_v1_trace_config_gettradersettings = privateSpotPostSpotV1TraceConfigGetTraderSettings = Entry('spot/v1/trace/config/getTraderSettings', ['private', 'spot'], 'POST', {'cost': 2}) + private_spot_post_spot_v1_trace_config_getfollowersettings = privateSpotPostSpotV1TraceConfigGetFollowerSettings = Entry('spot/v1/trace/config/getFollowerSettings', ['private', 'spot'], 'POST', {'cost': 2}) + private_spot_post_spot_v1_trace_user_mytraders = privateSpotPostSpotV1TraceUserMyTraders = Entry('spot/v1/trace/user/myTraders', ['private', 'spot'], 'POST', {'cost': 2}) + private_spot_post_spot_v1_trace_config_setfollowerconfig = privateSpotPostSpotV1TraceConfigSetFollowerConfig = Entry('spot/v1/trace/config/setFollowerConfig', ['private', 'spot'], 'POST', {'cost': 2}) + private_spot_post_spot_v1_trace_user_myfollowers = privateSpotPostSpotV1TraceUserMyFollowers = Entry('spot/v1/trace/user/myFollowers', ['private', 'spot'], 'POST', {'cost': 2}) + private_spot_post_spot_v1_trace_config_setproductcode = privateSpotPostSpotV1TraceConfigSetProductCode = Entry('spot/v1/trace/config/setProductCode', ['private', 'spot'], 'POST', {'cost': 2}) + private_spot_post_spot_v1_trace_user_removetrader = privateSpotPostSpotV1TraceUserRemoveTrader = Entry('spot/v1/trace/user/removeTrader', ['private', 'spot'], 'POST', {'cost': 2}) + private_spot_post_spot_v1_trace_getremovablefollower = privateSpotPostSpotV1TraceGetRemovableFollower = Entry('spot/v1/trace/getRemovableFollower', ['private', 'spot'], 'POST', {'cost': 2}) + private_spot_post_spot_v1_trace_user_removefollower = privateSpotPostSpotV1TraceUserRemoveFollower = Entry('spot/v1/trace/user/removeFollower', ['private', 'spot'], 'POST', {'cost': 2}) + private_spot_post_spot_v1_trace_profit_totalprofitinfo = privateSpotPostSpotV1TraceProfitTotalProfitInfo = Entry('spot/v1/trace/profit/totalProfitInfo', ['private', 'spot'], 'POST', {'cost': 2}) + private_spot_post_spot_v1_trace_profit_totalprofitlist = privateSpotPostSpotV1TraceProfitTotalProfitList = Entry('spot/v1/trace/profit/totalProfitList', ['private', 'spot'], 'POST', {'cost': 2}) + private_spot_post_spot_v1_trace_profit_profithislist = privateSpotPostSpotV1TraceProfitProfitHisList = Entry('spot/v1/trace/profit/profitHisList', ['private', 'spot'], 'POST', {'cost': 2}) + private_spot_post_spot_v1_trace_profit_profithisdetaillist = privateSpotPostSpotV1TraceProfitProfitHisDetailList = Entry('spot/v1/trace/profit/profitHisDetailList', ['private', 'spot'], 'POST', {'cost': 2}) + private_spot_post_spot_v1_trace_profit_waitprofitdetaillist = privateSpotPostSpotV1TraceProfitWaitProfitDetailList = Entry('spot/v1/trace/profit/waitProfitDetailList', ['private', 'spot'], 'POST', {'cost': 2}) + private_spot_post_spot_v1_trace_user_gettraderinfo = privateSpotPostSpotV1TraceUserGetTraderInfo = Entry('spot/v1/trace/user/getTraderInfo', ['private', 'spot'], 'POST', {'cost': 2}) + private_spot_post_v2_spot_trade_place_order = privateSpotPostV2SpotTradePlaceOrder = Entry('v2/spot/trade/place-order', ['private', 'spot'], 'POST', {'cost': 2}) + private_spot_post_v2_spot_trade_cancel_order = privateSpotPostV2SpotTradeCancelOrder = Entry('v2/spot/trade/cancel-order', ['private', 'spot'], 'POST', {'cost': 2}) + private_spot_post_v2_spot_trade_batch_orders = privateSpotPostV2SpotTradeBatchOrders = Entry('v2/spot/trade/batch-orders', ['private', 'spot'], 'POST', {'cost': 20}) + private_spot_post_v2_spot_trade_batch_cancel_order = privateSpotPostV2SpotTradeBatchCancelOrder = Entry('v2/spot/trade/batch-cancel-order', ['private', 'spot'], 'POST', {'cost': 2}) + private_spot_post_v2_spot_trade_cancel_symbol_order = privateSpotPostV2SpotTradeCancelSymbolOrder = Entry('v2/spot/trade/cancel-symbol-order', ['private', 'spot'], 'POST', {'cost': 4}) + private_spot_post_v2_spot_trade_place_plan_order = privateSpotPostV2SpotTradePlacePlanOrder = Entry('v2/spot/trade/place-plan-order', ['private', 'spot'], 'POST', {'cost': 1}) + private_spot_post_v2_spot_trade_modify_plan_order = privateSpotPostV2SpotTradeModifyPlanOrder = Entry('v2/spot/trade/modify-plan-order', ['private', 'spot'], 'POST', {'cost': 1}) + private_spot_post_v2_spot_trade_cancel_plan_order = privateSpotPostV2SpotTradeCancelPlanOrder = Entry('v2/spot/trade/cancel-plan-order', ['private', 'spot'], 'POST', {'cost': 1}) + private_spot_post_v2_spot_trade_cancel_replace_order = privateSpotPostV2SpotTradeCancelReplaceOrder = Entry('v2/spot/trade/cancel-replace-order', ['private', 'spot'], 'POST', {'cost': 2}) + private_spot_post_v2_spot_trade_batch_cancel_plan_order = privateSpotPostV2SpotTradeBatchCancelPlanOrder = Entry('v2/spot/trade/batch-cancel-plan-order', ['private', 'spot'], 'POST', {'cost': 2}) + private_spot_post_v2_spot_wallet_transfer = privateSpotPostV2SpotWalletTransfer = Entry('v2/spot/wallet/transfer', ['private', 'spot'], 'POST', {'cost': 2}) + private_spot_post_v2_spot_wallet_subaccount_transfer = privateSpotPostV2SpotWalletSubaccountTransfer = Entry('v2/spot/wallet/subaccount-transfer', ['private', 'spot'], 'POST', {'cost': 2}) + private_spot_post_v2_spot_wallet_withdrawal = privateSpotPostV2SpotWalletWithdrawal = Entry('v2/spot/wallet/withdrawal', ['private', 'spot'], 'POST', {'cost': 2}) + private_spot_post_v2_spot_wallet_cancel_withdrawal = privateSpotPostV2SpotWalletCancelWithdrawal = Entry('v2/spot/wallet/cancel-withdrawal', ['private', 'spot'], 'POST', {'cost': 2}) + private_spot_post_v2_spot_wallet_modify_deposit_account = privateSpotPostV2SpotWalletModifyDepositAccount = Entry('v2/spot/wallet/modify-deposit-account', ['private', 'spot'], 'POST', {'cost': 2}) + private_mix_get_mix_v1_account_account = privateMixGetMixV1AccountAccount = Entry('mix/v1/account/account', ['private', 'mix'], 'GET', {'cost': 2}) + private_mix_get_mix_v1_account_accounts = privateMixGetMixV1AccountAccounts = Entry('mix/v1/account/accounts', ['private', 'mix'], 'GET', {'cost': 2}) + private_mix_get_mix_v1_position_singleposition = privateMixGetMixV1PositionSinglePosition = Entry('mix/v1/position/singlePosition', ['private', 'mix'], 'GET', {'cost': 2}) + private_mix_get_mix_v1_position_singleposition_v2 = privateMixGetMixV1PositionSinglePositionV2 = Entry('mix/v1/position/singlePosition-v2', ['private', 'mix'], 'GET', {'cost': 2}) + private_mix_get_mix_v1_position_allposition = privateMixGetMixV1PositionAllPosition = Entry('mix/v1/position/allPosition', ['private', 'mix'], 'GET', {'cost': 4}) + private_mix_get_mix_v1_position_allposition_v2 = privateMixGetMixV1PositionAllPositionV2 = Entry('mix/v1/position/allPosition-v2', ['private', 'mix'], 'GET', {'cost': 4}) + private_mix_get_mix_v1_position_history_position = privateMixGetMixV1PositionHistoryPosition = Entry('mix/v1/position/history-position', ['private', 'mix'], 'GET', {'cost': 1}) + private_mix_get_mix_v1_account_accountbill = privateMixGetMixV1AccountAccountBill = Entry('mix/v1/account/accountBill', ['private', 'mix'], 'GET', {'cost': 2}) + private_mix_get_mix_v1_account_accountbusinessbill = privateMixGetMixV1AccountAccountBusinessBill = Entry('mix/v1/account/accountBusinessBill', ['private', 'mix'], 'GET', {'cost': 4}) + private_mix_get_mix_v1_order_current = privateMixGetMixV1OrderCurrent = Entry('mix/v1/order/current', ['private', 'mix'], 'GET', {'cost': 1}) + private_mix_get_mix_v1_order_margincoincurrent = privateMixGetMixV1OrderMarginCoinCurrent = Entry('mix/v1/order/marginCoinCurrent', ['private', 'mix'], 'GET', {'cost': 1}) + private_mix_get_mix_v1_order_history = privateMixGetMixV1OrderHistory = Entry('mix/v1/order/history', ['private', 'mix'], 'GET', {'cost': 2}) + private_mix_get_mix_v1_order_historyproducttype = privateMixGetMixV1OrderHistoryProductType = Entry('mix/v1/order/historyProductType', ['private', 'mix'], 'GET', {'cost': 4}) + private_mix_get_mix_v1_order_detail = privateMixGetMixV1OrderDetail = Entry('mix/v1/order/detail', ['private', 'mix'], 'GET', {'cost': 2}) + private_mix_get_mix_v1_order_fills = privateMixGetMixV1OrderFills = Entry('mix/v1/order/fills', ['private', 'mix'], 'GET', {'cost': 2}) + private_mix_get_mix_v1_order_allfills = privateMixGetMixV1OrderAllFills = Entry('mix/v1/order/allFills', ['private', 'mix'], 'GET', {'cost': 2}) + private_mix_get_mix_v1_plan_currentplan = privateMixGetMixV1PlanCurrentPlan = Entry('mix/v1/plan/currentPlan', ['private', 'mix'], 'GET', {'cost': 1}) + private_mix_get_mix_v1_plan_historyplan = privateMixGetMixV1PlanHistoryPlan = Entry('mix/v1/plan/historyPlan', ['private', 'mix'], 'GET', {'cost': 2}) + private_mix_get_mix_v1_trace_currenttrack = privateMixGetMixV1TraceCurrentTrack = Entry('mix/v1/trace/currentTrack', ['private', 'mix'], 'GET', {'cost': 2}) + private_mix_get_mix_v1_trace_followerorder = privateMixGetMixV1TraceFollowerOrder = Entry('mix/v1/trace/followerOrder', ['private', 'mix'], 'GET', {'cost': 2}) + private_mix_get_mix_v1_trace_followerhistoryorders = privateMixGetMixV1TraceFollowerHistoryOrders = Entry('mix/v1/trace/followerHistoryOrders', ['private', 'mix'], 'GET', {'cost': 2}) + private_mix_get_mix_v1_trace_historytrack = privateMixGetMixV1TraceHistoryTrack = Entry('mix/v1/trace/historyTrack', ['private', 'mix'], 'GET', {'cost': 2}) + private_mix_get_mix_v1_trace_summary = privateMixGetMixV1TraceSummary = Entry('mix/v1/trace/summary', ['private', 'mix'], 'GET', {'cost': 1}) + private_mix_get_mix_v1_trace_profitsettletokenidgroup = privateMixGetMixV1TraceProfitSettleTokenIdGroup = Entry('mix/v1/trace/profitSettleTokenIdGroup', ['private', 'mix'], 'GET', {'cost': 1}) + private_mix_get_mix_v1_trace_profitdategrouplist = privateMixGetMixV1TraceProfitDateGroupList = Entry('mix/v1/trace/profitDateGroupList', ['private', 'mix'], 'GET', {'cost': 1}) + private_mix_get_mix_v1_trade_profitdatelist = privateMixGetMixV1TradeProfitDateList = Entry('mix/v1/trade/profitDateList', ['private', 'mix'], 'GET', {'cost': 2}) + private_mix_get_mix_v1_trace_waitprofitdatelist = privateMixGetMixV1TraceWaitProfitDateList = Entry('mix/v1/trace/waitProfitDateList', ['private', 'mix'], 'GET', {'cost': 1}) + private_mix_get_mix_v1_trace_tradersymbols = privateMixGetMixV1TraceTraderSymbols = Entry('mix/v1/trace/traderSymbols', ['private', 'mix'], 'GET', {'cost': 1}) + private_mix_get_mix_v1_trace_traderlist = privateMixGetMixV1TraceTraderList = Entry('mix/v1/trace/traderList', ['private', 'mix'], 'GET', {'cost': 2}) + private_mix_get_mix_v1_trace_traderdetail = privateMixGetMixV1TraceTraderDetail = Entry('mix/v1/trace/traderDetail', ['private', 'mix'], 'GET', {'cost': 2}) + private_mix_get_mix_v1_trace_querytraceconfig = privateMixGetMixV1TraceQueryTraceConfig = Entry('mix/v1/trace/queryTraceConfig', ['private', 'mix'], 'GET', {'cost': 2}) + private_mix_get_v2_mix_account_account = privateMixGetV2MixAccountAccount = Entry('v2/mix/account/account', ['private', 'mix'], 'GET', {'cost': 2}) + private_mix_get_v2_mix_account_accounts = privateMixGetV2MixAccountAccounts = Entry('v2/mix/account/accounts', ['private', 'mix'], 'GET', {'cost': 2}) + private_mix_get_v2_mix_account_sub_account_assets = privateMixGetV2MixAccountSubAccountAssets = Entry('v2/mix/account/sub-account-assets', ['private', 'mix'], 'GET', {'cost': 200}) + private_mix_get_v2_mix_account_open_count = privateMixGetV2MixAccountOpenCount = Entry('v2/mix/account/open-count', ['private', 'mix'], 'GET', {'cost': 2}) + private_mix_get_v2_mix_account_bill = privateMixGetV2MixAccountBill = Entry('v2/mix/account/bill', ['private', 'mix'], 'GET', {'cost': 2}) + private_mix_get_v2_mix_market_query_position_lever = privateMixGetV2MixMarketQueryPositionLever = Entry('v2/mix/market/query-position-lever', ['private', 'mix'], 'GET', {'cost': 2}) + private_mix_get_v2_mix_position_single_position = privateMixGetV2MixPositionSinglePosition = Entry('v2/mix/position/single-position', ['private', 'mix'], 'GET', {'cost': 2}) + private_mix_get_v2_mix_position_all_position = privateMixGetV2MixPositionAllPosition = Entry('v2/mix/position/all-position', ['private', 'mix'], 'GET', {'cost': 4}) + private_mix_get_v2_mix_position_history_position = privateMixGetV2MixPositionHistoryPosition = Entry('v2/mix/position/history-position', ['private', 'mix'], 'GET', {'cost': 1}) + private_mix_get_v2_mix_order_detail = privateMixGetV2MixOrderDetail = Entry('v2/mix/order/detail', ['private', 'mix'], 'GET', {'cost': 2}) + private_mix_get_v2_mix_order_fills = privateMixGetV2MixOrderFills = Entry('v2/mix/order/fills', ['private', 'mix'], 'GET', {'cost': 2}) + private_mix_get_v2_mix_order_fill_history = privateMixGetV2MixOrderFillHistory = Entry('v2/mix/order/fill-history', ['private', 'mix'], 'GET', {'cost': 2}) + private_mix_get_v2_mix_order_orders_pending = privateMixGetV2MixOrderOrdersPending = Entry('v2/mix/order/orders-pending', ['private', 'mix'], 'GET', {'cost': 2}) + private_mix_get_v2_mix_order_orders_history = privateMixGetV2MixOrderOrdersHistory = Entry('v2/mix/order/orders-history', ['private', 'mix'], 'GET', {'cost': 2}) + private_mix_get_v2_mix_order_orders_plan_pending = privateMixGetV2MixOrderOrdersPlanPending = Entry('v2/mix/order/orders-plan-pending', ['private', 'mix'], 'GET', {'cost': 2}) + private_mix_get_v2_mix_order_orders_plan_history = privateMixGetV2MixOrderOrdersPlanHistory = Entry('v2/mix/order/orders-plan-history', ['private', 'mix'], 'GET', {'cost': 2}) + private_mix_get_v2_mix_market_position_long_short = privateMixGetV2MixMarketPositionLongShort = Entry('v2/mix/market/position-long-short', ['private', 'mix'], 'GET', {'cost': 20}) + private_mix_post_mix_v1_account_sub_account_contract_assets = privateMixPostMixV1AccountSubAccountContractAssets = Entry('mix/v1/account/sub-account-contract-assets', ['private', 'mix'], 'POST', {'cost': 200}) + private_mix_post_mix_v1_account_open_count = privateMixPostMixV1AccountOpenCount = Entry('mix/v1/account/open-count', ['private', 'mix'], 'POST', {'cost': 1}) + private_mix_post_mix_v1_account_setleverage = privateMixPostMixV1AccountSetLeverage = Entry('mix/v1/account/setLeverage', ['private', 'mix'], 'POST', {'cost': 4}) + private_mix_post_mix_v1_account_setmargin = privateMixPostMixV1AccountSetMargin = Entry('mix/v1/account/setMargin', ['private', 'mix'], 'POST', {'cost': 4}) + private_mix_post_mix_v1_account_setmarginmode = privateMixPostMixV1AccountSetMarginMode = Entry('mix/v1/account/setMarginMode', ['private', 'mix'], 'POST', {'cost': 4}) + private_mix_post_mix_v1_account_setpositionmode = privateMixPostMixV1AccountSetPositionMode = Entry('mix/v1/account/setPositionMode', ['private', 'mix'], 'POST', {'cost': 4}) + private_mix_post_mix_v1_order_placeorder = privateMixPostMixV1OrderPlaceOrder = Entry('mix/v1/order/placeOrder', ['private', 'mix'], 'POST', {'cost': 2}) + private_mix_post_mix_v1_order_batch_orders = privateMixPostMixV1OrderBatchOrders = Entry('mix/v1/order/batch-orders', ['private', 'mix'], 'POST', {'cost': 2}) + private_mix_post_mix_v1_order_cancel_order = privateMixPostMixV1OrderCancelOrder = Entry('mix/v1/order/cancel-order', ['private', 'mix'], 'POST', {'cost': 2}) + private_mix_post_mix_v1_order_cancel_batch_orders = privateMixPostMixV1OrderCancelBatchOrders = Entry('mix/v1/order/cancel-batch-orders', ['private', 'mix'], 'POST', {'cost': 2}) + private_mix_post_mix_v1_order_modifyorder = privateMixPostMixV1OrderModifyOrder = Entry('mix/v1/order/modifyOrder', ['private', 'mix'], 'POST', {'cost': 2}) + private_mix_post_mix_v1_order_cancel_symbol_orders = privateMixPostMixV1OrderCancelSymbolOrders = Entry('mix/v1/order/cancel-symbol-orders', ['private', 'mix'], 'POST', {'cost': 2}) + private_mix_post_mix_v1_order_cancel_all_orders = privateMixPostMixV1OrderCancelAllOrders = Entry('mix/v1/order/cancel-all-orders', ['private', 'mix'], 'POST', {'cost': 2}) + private_mix_post_mix_v1_order_close_all_positions = privateMixPostMixV1OrderCloseAllPositions = Entry('mix/v1/order/close-all-positions', ['private', 'mix'], 'POST', {'cost': 20}) + private_mix_post_mix_v1_plan_placeplan = privateMixPostMixV1PlanPlacePlan = Entry('mix/v1/plan/placePlan', ['private', 'mix'], 'POST', {'cost': 2}) + private_mix_post_mix_v1_plan_modifyplan = privateMixPostMixV1PlanModifyPlan = Entry('mix/v1/plan/modifyPlan', ['private', 'mix'], 'POST', {'cost': 2}) + private_mix_post_mix_v1_plan_modifyplanpreset = privateMixPostMixV1PlanModifyPlanPreset = Entry('mix/v1/plan/modifyPlanPreset', ['private', 'mix'], 'POST', {'cost': 2}) + private_mix_post_mix_v1_plan_placetpsl = privateMixPostMixV1PlanPlaceTPSL = Entry('mix/v1/plan/placeTPSL', ['private', 'mix'], 'POST', {'cost': 2}) + private_mix_post_mix_v1_plan_placetrailstop = privateMixPostMixV1PlanPlaceTrailStop = Entry('mix/v1/plan/placeTrailStop', ['private', 'mix'], 'POST', {'cost': 2}) + private_mix_post_mix_v1_plan_placepositionstpsl = privateMixPostMixV1PlanPlacePositionsTPSL = Entry('mix/v1/plan/placePositionsTPSL', ['private', 'mix'], 'POST', {'cost': 2}) + private_mix_post_mix_v1_plan_modifytpslplan = privateMixPostMixV1PlanModifyTPSLPlan = Entry('mix/v1/plan/modifyTPSLPlan', ['private', 'mix'], 'POST', {'cost': 2}) + private_mix_post_mix_v1_plan_cancelplan = privateMixPostMixV1PlanCancelPlan = Entry('mix/v1/plan/cancelPlan', ['private', 'mix'], 'POST', {'cost': 2}) + private_mix_post_mix_v1_plan_cancelsymbolplan = privateMixPostMixV1PlanCancelSymbolPlan = Entry('mix/v1/plan/cancelSymbolPlan', ['private', 'mix'], 'POST', {'cost': 2}) + private_mix_post_mix_v1_plan_cancelallplan = privateMixPostMixV1PlanCancelAllPlan = Entry('mix/v1/plan/cancelAllPlan', ['private', 'mix'], 'POST', {'cost': 2}) + private_mix_post_mix_v1_trace_closetrackorder = privateMixPostMixV1TraceCloseTrackOrder = Entry('mix/v1/trace/closeTrackOrder', ['private', 'mix'], 'POST', {'cost': 2}) + private_mix_post_mix_v1_trace_modifytpsl = privateMixPostMixV1TraceModifyTPSL = Entry('mix/v1/trace/modifyTPSL', ['private', 'mix'], 'POST', {'cost': 2}) + private_mix_post_mix_v1_trace_closetrackorderbysymbol = privateMixPostMixV1TraceCloseTrackOrderBySymbol = Entry('mix/v1/trace/closeTrackOrderBySymbol', ['private', 'mix'], 'POST', {'cost': 2}) + private_mix_post_mix_v1_trace_setupcopysymbols = privateMixPostMixV1TraceSetUpCopySymbols = Entry('mix/v1/trace/setUpCopySymbols', ['private', 'mix'], 'POST', {'cost': 2}) + private_mix_post_mix_v1_trace_followersetbatchtraceconfig = privateMixPostMixV1TraceFollowerSetBatchTraceConfig = Entry('mix/v1/trace/followerSetBatchTraceConfig', ['private', 'mix'], 'POST', {'cost': 2}) + private_mix_post_mix_v1_trace_followerclosebytrackingno = privateMixPostMixV1TraceFollowerCloseByTrackingNo = Entry('mix/v1/trace/followerCloseByTrackingNo', ['private', 'mix'], 'POST', {'cost': 2}) + private_mix_post_mix_v1_trace_followerclosebyall = privateMixPostMixV1TraceFollowerCloseByAll = Entry('mix/v1/trace/followerCloseByAll', ['private', 'mix'], 'POST', {'cost': 2}) + private_mix_post_mix_v1_trace_followersettpsl = privateMixPostMixV1TraceFollowerSetTpsl = Entry('mix/v1/trace/followerSetTpsl', ['private', 'mix'], 'POST', {'cost': 2}) + private_mix_post_mix_v1_trace_cancelcopytrader = privateMixPostMixV1TraceCancelCopyTrader = Entry('mix/v1/trace/cancelCopyTrader', ['private', 'mix'], 'POST', {'cost': 4}) + private_mix_post_mix_v1_trace_traderupdateconfig = privateMixPostMixV1TraceTraderUpdateConfig = Entry('mix/v1/trace/traderUpdateConfig', ['private', 'mix'], 'POST', {'cost': 2}) + private_mix_post_mix_v1_trace_mytraderlist = privateMixPostMixV1TraceMyTraderList = Entry('mix/v1/trace/myTraderList', ['private', 'mix'], 'POST', {'cost': 2}) + private_mix_post_mix_v1_trace_myfollowerlist = privateMixPostMixV1TraceMyFollowerList = Entry('mix/v1/trace/myFollowerList', ['private', 'mix'], 'POST', {'cost': 2}) + private_mix_post_mix_v1_trace_removefollower = privateMixPostMixV1TraceRemoveFollower = Entry('mix/v1/trace/removeFollower', ['private', 'mix'], 'POST', {'cost': 2}) + private_mix_post_mix_v1_trace_public_getfollowerconfig = privateMixPostMixV1TracePublicGetFollowerConfig = Entry('mix/v1/trace/public/getFollowerConfig', ['private', 'mix'], 'POST', {'cost': 2}) + private_mix_post_mix_v1_trace_report_order_historylist = privateMixPostMixV1TraceReportOrderHistoryList = Entry('mix/v1/trace/report/order/historyList', ['private', 'mix'], 'POST', {'cost': 2}) + private_mix_post_mix_v1_trace_report_order_currentlist = privateMixPostMixV1TraceReportOrderCurrentList = Entry('mix/v1/trace/report/order/currentList', ['private', 'mix'], 'POST', {'cost': 2}) + private_mix_post_mix_v1_trace_querytradertpslratioconfig = privateMixPostMixV1TraceQueryTraderTpslRatioConfig = Entry('mix/v1/trace/queryTraderTpslRatioConfig', ['private', 'mix'], 'POST', {'cost': 2}) + private_mix_post_mix_v1_trace_traderupdatetpslratioconfig = privateMixPostMixV1TraceTraderUpdateTpslRatioConfig = Entry('mix/v1/trace/traderUpdateTpslRatioConfig', ['private', 'mix'], 'POST', {'cost': 2}) + private_mix_post_v2_mix_account_set_leverage = privateMixPostV2MixAccountSetLeverage = Entry('v2/mix/account/set-leverage', ['private', 'mix'], 'POST', {'cost': 4}) + private_mix_post_v2_mix_account_set_margin = privateMixPostV2MixAccountSetMargin = Entry('v2/mix/account/set-margin', ['private', 'mix'], 'POST', {'cost': 4}) + private_mix_post_v2_mix_account_set_margin_mode = privateMixPostV2MixAccountSetMarginMode = Entry('v2/mix/account/set-margin-mode', ['private', 'mix'], 'POST', {'cost': 4}) + private_mix_post_v2_mix_account_set_position_mode = privateMixPostV2MixAccountSetPositionMode = Entry('v2/mix/account/set-position-mode', ['private', 'mix'], 'POST', {'cost': 4}) + private_mix_post_v2_mix_order_place_order = privateMixPostV2MixOrderPlaceOrder = Entry('v2/mix/order/place-order', ['private', 'mix'], 'POST', {'cost': 2}) + private_mix_post_v2_mix_order_click_backhand = privateMixPostV2MixOrderClickBackhand = Entry('v2/mix/order/click-backhand', ['private', 'mix'], 'POST', {'cost': 20}) + private_mix_post_v2_mix_order_batch_place_order = privateMixPostV2MixOrderBatchPlaceOrder = Entry('v2/mix/order/batch-place-order', ['private', 'mix'], 'POST', {'cost': 20}) + private_mix_post_v2_mix_order_modify_order = privateMixPostV2MixOrderModifyOrder = Entry('v2/mix/order/modify-order', ['private', 'mix'], 'POST', {'cost': 2}) + private_mix_post_v2_mix_order_cancel_order = privateMixPostV2MixOrderCancelOrder = Entry('v2/mix/order/cancel-order', ['private', 'mix'], 'POST', {'cost': 2}) + private_mix_post_v2_mix_order_batch_cancel_orders = privateMixPostV2MixOrderBatchCancelOrders = Entry('v2/mix/order/batch-cancel-orders', ['private', 'mix'], 'POST', {'cost': 2}) + private_mix_post_v2_mix_order_close_positions = privateMixPostV2MixOrderClosePositions = Entry('v2/mix/order/close-positions', ['private', 'mix'], 'POST', {'cost': 20}) + private_mix_post_v2_mix_order_place_tpsl_order = privateMixPostV2MixOrderPlaceTpslOrder = Entry('v2/mix/order/place-tpsl-order', ['private', 'mix'], 'POST', {'cost': 2}) + private_mix_post_v2_mix_order_place_plan_order = privateMixPostV2MixOrderPlacePlanOrder = Entry('v2/mix/order/place-plan-order', ['private', 'mix'], 'POST', {'cost': 2}) + private_mix_post_v2_mix_order_modify_tpsl_order = privateMixPostV2MixOrderModifyTpslOrder = Entry('v2/mix/order/modify-tpsl-order', ['private', 'mix'], 'POST', {'cost': 2}) + private_mix_post_v2_mix_order_modify_plan_order = privateMixPostV2MixOrderModifyPlanOrder = Entry('v2/mix/order/modify-plan-order', ['private', 'mix'], 'POST', {'cost': 2}) + private_mix_post_v2_mix_order_cancel_plan_order = privateMixPostV2MixOrderCancelPlanOrder = Entry('v2/mix/order/cancel-plan-order', ['private', 'mix'], 'POST', {'cost': 2}) + private_user_get_user_v1_fee_query = privateUserGetUserV1FeeQuery = Entry('user/v1/fee/query', ['private', 'user'], 'GET', {'cost': 2}) + private_user_get_user_v1_sub_virtual_list = privateUserGetUserV1SubVirtualList = Entry('user/v1/sub/virtual-list', ['private', 'user'], 'GET', {'cost': 2}) + private_user_get_user_v1_sub_virtual_api_list = privateUserGetUserV1SubVirtualApiList = Entry('user/v1/sub/virtual-api-list', ['private', 'user'], 'GET', {'cost': 2}) + private_user_get_user_v1_tax_spot_record = privateUserGetUserV1TaxSpotRecord = Entry('user/v1/tax/spot-record', ['private', 'user'], 'GET', {'cost': 1}) + private_user_get_user_v1_tax_future_record = privateUserGetUserV1TaxFutureRecord = Entry('user/v1/tax/future-record', ['private', 'user'], 'GET', {'cost': 1}) + private_user_get_user_v1_tax_margin_record = privateUserGetUserV1TaxMarginRecord = Entry('user/v1/tax/margin-record', ['private', 'user'], 'GET', {'cost': 1}) + private_user_get_user_v1_tax_p2p_record = privateUserGetUserV1TaxP2pRecord = Entry('user/v1/tax/p2p-record', ['private', 'user'], 'GET', {'cost': 1}) + private_user_get_v2_user_virtual_subaccount_list = privateUserGetV2UserVirtualSubaccountList = Entry('v2/user/virtual-subaccount-list', ['private', 'user'], 'GET', {'cost': 2}) + private_user_get_v2_user_virtual_subaccount_apikey_list = privateUserGetV2UserVirtualSubaccountApikeyList = Entry('v2/user/virtual-subaccount-apikey-list', ['private', 'user'], 'GET', {'cost': 2}) + private_user_post_user_v1_sub_virtual_create = privateUserPostUserV1SubVirtualCreate = Entry('user/v1/sub/virtual-create', ['private', 'user'], 'POST', {'cost': 4}) + private_user_post_user_v1_sub_virtual_modify = privateUserPostUserV1SubVirtualModify = Entry('user/v1/sub/virtual-modify', ['private', 'user'], 'POST', {'cost': 4}) + private_user_post_user_v1_sub_virtual_api_batch_create = privateUserPostUserV1SubVirtualApiBatchCreate = Entry('user/v1/sub/virtual-api-batch-create', ['private', 'user'], 'POST', {'cost': 20}) + private_user_post_user_v1_sub_virtual_api_create = privateUserPostUserV1SubVirtualApiCreate = Entry('user/v1/sub/virtual-api-create', ['private', 'user'], 'POST', {'cost': 4}) + private_user_post_user_v1_sub_virtual_api_modify = privateUserPostUserV1SubVirtualApiModify = Entry('user/v1/sub/virtual-api-modify', ['private', 'user'], 'POST', {'cost': 4}) + private_user_post_v2_user_create_virtual_subaccount = privateUserPostV2UserCreateVirtualSubaccount = Entry('v2/user/create-virtual-subaccount', ['private', 'user'], 'POST', {'cost': 4}) + private_user_post_v2_user_modify_virtual_subaccount = privateUserPostV2UserModifyVirtualSubaccount = Entry('v2/user/modify-virtual-subaccount', ['private', 'user'], 'POST', {'cost': 4}) + private_user_post_v2_user_batch_create_subaccount_and_apikey = privateUserPostV2UserBatchCreateSubaccountAndApikey = Entry('v2/user/batch-create-subaccount-and-apikey', ['private', 'user'], 'POST', {'cost': 20}) + private_user_post_v2_user_create_virtual_subaccount_apikey = privateUserPostV2UserCreateVirtualSubaccountApikey = Entry('v2/user/create-virtual-subaccount-apikey', ['private', 'user'], 'POST', {'cost': 4}) + private_user_post_v2_user_modify_virtual_subaccount_apikey = privateUserPostV2UserModifyVirtualSubaccountApikey = Entry('v2/user/modify-virtual-subaccount-apikey', ['private', 'user'], 'POST', {'cost': 4}) + private_p2p_get_p2p_v1_merchant_merchantlist = privateP2pGetP2pV1MerchantMerchantList = Entry('p2p/v1/merchant/merchantList', ['private', 'p2p'], 'GET', {'cost': 2}) + private_p2p_get_p2p_v1_merchant_merchantinfo = privateP2pGetP2pV1MerchantMerchantInfo = Entry('p2p/v1/merchant/merchantInfo', ['private', 'p2p'], 'GET', {'cost': 2}) + private_p2p_get_p2p_v1_merchant_advlist = privateP2pGetP2pV1MerchantAdvList = Entry('p2p/v1/merchant/advList', ['private', 'p2p'], 'GET', {'cost': 2}) + private_p2p_get_p2p_v1_merchant_orderlist = privateP2pGetP2pV1MerchantOrderList = Entry('p2p/v1/merchant/orderList', ['private', 'p2p'], 'GET', {'cost': 2}) + private_p2p_get_v2_p2p_merchantlist = privateP2pGetV2P2pMerchantList = Entry('v2/p2p/merchantList', ['private', 'p2p'], 'GET', {'cost': 2}) + private_p2p_get_v2_p2p_merchantinfo = privateP2pGetV2P2pMerchantInfo = Entry('v2/p2p/merchantInfo', ['private', 'p2p'], 'GET', {'cost': 2}) + private_p2p_get_v2_p2p_orderlist = privateP2pGetV2P2pOrderList = Entry('v2/p2p/orderList', ['private', 'p2p'], 'GET', {'cost': 2}) + private_p2p_get_v2_p2p_advlist = privateP2pGetV2P2pAdvList = Entry('v2/p2p/advList', ['private', 'p2p'], 'GET', {'cost': 2}) + private_broker_get_broker_v1_account_info = privateBrokerGetBrokerV1AccountInfo = Entry('broker/v1/account/info', ['private', 'broker'], 'GET', {'cost': 2}) + private_broker_get_broker_v1_account_sub_list = privateBrokerGetBrokerV1AccountSubList = Entry('broker/v1/account/sub-list', ['private', 'broker'], 'GET', {'cost': 20}) + private_broker_get_broker_v1_account_sub_email = privateBrokerGetBrokerV1AccountSubEmail = Entry('broker/v1/account/sub-email', ['private', 'broker'], 'GET', {'cost': 20}) + private_broker_get_broker_v1_account_sub_spot_assets = privateBrokerGetBrokerV1AccountSubSpotAssets = Entry('broker/v1/account/sub-spot-assets', ['private', 'broker'], 'GET', {'cost': 2}) + private_broker_get_broker_v1_account_sub_future_assets = privateBrokerGetBrokerV1AccountSubFutureAssets = Entry('broker/v1/account/sub-future-assets', ['private', 'broker'], 'GET', {'cost': 2}) + private_broker_get_broker_v1_account_subaccount_transfer = privateBrokerGetBrokerV1AccountSubaccountTransfer = Entry('broker/v1/account/subaccount-transfer', ['private', 'broker'], 'GET', {'cost': 1}) + private_broker_get_broker_v1_account_subaccount_deposit = privateBrokerGetBrokerV1AccountSubaccountDeposit = Entry('broker/v1/account/subaccount-deposit', ['private', 'broker'], 'GET', {'cost': 1}) + private_broker_get_broker_v1_account_subaccount_withdrawal = privateBrokerGetBrokerV1AccountSubaccountWithdrawal = Entry('broker/v1/account/subaccount-withdrawal', ['private', 'broker'], 'GET', {'cost': 1}) + private_broker_get_broker_v1_account_sub_api_list = privateBrokerGetBrokerV1AccountSubApiList = Entry('broker/v1/account/sub-api-list', ['private', 'broker'], 'GET', {'cost': 2}) + private_broker_get_v2_broker_account_info = privateBrokerGetV2BrokerAccountInfo = Entry('v2/broker/account/info', ['private', 'broker'], 'GET', {'cost': 2}) + private_broker_get_v2_broker_account_subaccount_list = privateBrokerGetV2BrokerAccountSubaccountList = Entry('v2/broker/account/subaccount-list', ['private', 'broker'], 'GET', {'cost': 20}) + private_broker_get_v2_broker_account_subaccount_email = privateBrokerGetV2BrokerAccountSubaccountEmail = Entry('v2/broker/account/subaccount-email', ['private', 'broker'], 'GET', {'cost': 2}) + private_broker_get_v2_broker_account_subaccount_spot_assets = privateBrokerGetV2BrokerAccountSubaccountSpotAssets = Entry('v2/broker/account/subaccount-spot-assets', ['private', 'broker'], 'GET', {'cost': 2}) + private_broker_get_v2_broker_account_subaccount_future_assets = privateBrokerGetV2BrokerAccountSubaccountFutureAssets = Entry('v2/broker/account/subaccount-future-assets', ['private', 'broker'], 'GET', {'cost': 2}) + private_broker_get_v2_broker_manage_subaccount_apikey_list = privateBrokerGetV2BrokerManageSubaccountApikeyList = Entry('v2/broker/manage/subaccount-apikey-list', ['private', 'broker'], 'GET', {'cost': 2}) + private_broker_post_broker_v1_account_sub_create = privateBrokerPostBrokerV1AccountSubCreate = Entry('broker/v1/account/sub-create', ['private', 'broker'], 'POST', {'cost': 20}) + private_broker_post_broker_v1_account_sub_modify = privateBrokerPostBrokerV1AccountSubModify = Entry('broker/v1/account/sub-modify', ['private', 'broker'], 'POST', {'cost': 20}) + private_broker_post_broker_v1_account_sub_modify_email = privateBrokerPostBrokerV1AccountSubModifyEmail = Entry('broker/v1/account/sub-modify-email', ['private', 'broker'], 'POST', {'cost': 20}) + private_broker_post_broker_v1_account_sub_address = privateBrokerPostBrokerV1AccountSubAddress = Entry('broker/v1/account/sub-address', ['private', 'broker'], 'POST', {'cost': 2}) + private_broker_post_broker_v1_account_sub_withdrawal = privateBrokerPostBrokerV1AccountSubWithdrawal = Entry('broker/v1/account/sub-withdrawal', ['private', 'broker'], 'POST', {'cost': 2}) + private_broker_post_broker_v1_account_sub_auto_transfer = privateBrokerPostBrokerV1AccountSubAutoTransfer = Entry('broker/v1/account/sub-auto-transfer', ['private', 'broker'], 'POST', {'cost': 4}) + private_broker_post_broker_v1_account_sub_api_create = privateBrokerPostBrokerV1AccountSubApiCreate = Entry('broker/v1/account/sub-api-create', ['private', 'broker'], 'POST', {'cost': 2}) + private_broker_post_broker_v1_account_sub_api_modify = privateBrokerPostBrokerV1AccountSubApiModify = Entry('broker/v1/account/sub-api-modify', ['private', 'broker'], 'POST', {'cost': 2}) + private_broker_post_v2_broker_account_modify_subaccount_email = privateBrokerPostV2BrokerAccountModifySubaccountEmail = Entry('v2/broker/account/modify-subaccount-email', ['private', 'broker'], 'POST', {'cost': 2}) + private_broker_post_v2_broker_account_create_subaccount = privateBrokerPostV2BrokerAccountCreateSubaccount = Entry('v2/broker/account/create-subaccount', ['private', 'broker'], 'POST', {'cost': 20}) + private_broker_post_v2_broker_account_modify_subaccount = privateBrokerPostV2BrokerAccountModifySubaccount = Entry('v2/broker/account/modify-subaccount', ['private', 'broker'], 'POST', {'cost': 20}) + private_broker_post_v2_broker_account_subaccount_address = privateBrokerPostV2BrokerAccountSubaccountAddress = Entry('v2/broker/account/subaccount-address', ['private', 'broker'], 'POST', {'cost': 2}) + private_broker_post_v2_broker_account_subaccount_withdrawal = privateBrokerPostV2BrokerAccountSubaccountWithdrawal = Entry('v2/broker/account/subaccount-withdrawal', ['private', 'broker'], 'POST', {'cost': 2}) + private_broker_post_v2_broker_account_set_subaccount_autotransfer = privateBrokerPostV2BrokerAccountSetSubaccountAutotransfer = Entry('v2/broker/account/set-subaccount-autotransfer', ['private', 'broker'], 'POST', {'cost': 2}) + private_broker_post_v2_broker_manage_create_subaccount_apikey = privateBrokerPostV2BrokerManageCreateSubaccountApikey = Entry('v2/broker/manage/create-subaccount-apikey', ['private', 'broker'], 'POST', {'cost': 2}) + private_broker_post_v2_broker_manage_modify_subaccount_apikey = privateBrokerPostV2BrokerManageModifySubaccountApikey = Entry('v2/broker/manage/modify-subaccount-apikey', ['private', 'broker'], 'POST', {'cost': 2}) + private_margin_get_margin_v1_cross_account_riskrate = privateMarginGetMarginV1CrossAccountRiskRate = Entry('margin/v1/cross/account/riskRate', ['private', 'margin'], 'GET', {'cost': 2}) + private_margin_get_margin_v1_cross_account_maxtransferoutamount = privateMarginGetMarginV1CrossAccountMaxTransferOutAmount = Entry('margin/v1/cross/account/maxTransferOutAmount', ['private', 'margin'], 'GET', {'cost': 2}) + private_margin_get_margin_v1_isolated_account_maxtransferoutamount = privateMarginGetMarginV1IsolatedAccountMaxTransferOutAmount = Entry('margin/v1/isolated/account/maxTransferOutAmount', ['private', 'margin'], 'GET', {'cost': 2}) + private_margin_get_margin_v1_isolated_order_openorders = privateMarginGetMarginV1IsolatedOrderOpenOrders = Entry('margin/v1/isolated/order/openOrders', ['private', 'margin'], 'GET', {'cost': 2}) + private_margin_get_margin_v1_isolated_order_history = privateMarginGetMarginV1IsolatedOrderHistory = Entry('margin/v1/isolated/order/history', ['private', 'margin'], 'GET', {'cost': 2}) + private_margin_get_margin_v1_isolated_order_fills = privateMarginGetMarginV1IsolatedOrderFills = Entry('margin/v1/isolated/order/fills', ['private', 'margin'], 'GET', {'cost': 2}) + private_margin_get_margin_v1_isolated_loan_list = privateMarginGetMarginV1IsolatedLoanList = Entry('margin/v1/isolated/loan/list', ['private', 'margin'], 'GET', {'cost': 2}) + private_margin_get_margin_v1_isolated_repay_list = privateMarginGetMarginV1IsolatedRepayList = Entry('margin/v1/isolated/repay/list', ['private', 'margin'], 'GET', {'cost': 2}) + private_margin_get_margin_v1_isolated_interest_list = privateMarginGetMarginV1IsolatedInterestList = Entry('margin/v1/isolated/interest/list', ['private', 'margin'], 'GET', {'cost': 2}) + private_margin_get_margin_v1_isolated_liquidation_list = privateMarginGetMarginV1IsolatedLiquidationList = Entry('margin/v1/isolated/liquidation/list', ['private', 'margin'], 'GET', {'cost': 2}) + private_margin_get_margin_v1_isolated_fin_list = privateMarginGetMarginV1IsolatedFinList = Entry('margin/v1/isolated/fin/list', ['private', 'margin'], 'GET', {'cost': 2}) + private_margin_get_margin_v1_cross_order_openorders = privateMarginGetMarginV1CrossOrderOpenOrders = Entry('margin/v1/cross/order/openOrders', ['private', 'margin'], 'GET', {'cost': 2}) + private_margin_get_margin_v1_cross_order_history = privateMarginGetMarginV1CrossOrderHistory = Entry('margin/v1/cross/order/history', ['private', 'margin'], 'GET', {'cost': 2}) + private_margin_get_margin_v1_cross_order_fills = privateMarginGetMarginV1CrossOrderFills = Entry('margin/v1/cross/order/fills', ['private', 'margin'], 'GET', {'cost': 2}) + private_margin_get_margin_v1_cross_loan_list = privateMarginGetMarginV1CrossLoanList = Entry('margin/v1/cross/loan/list', ['private', 'margin'], 'GET', {'cost': 2}) + private_margin_get_margin_v1_cross_repay_list = privateMarginGetMarginV1CrossRepayList = Entry('margin/v1/cross/repay/list', ['private', 'margin'], 'GET', {'cost': 2}) + private_margin_get_margin_v1_cross_interest_list = privateMarginGetMarginV1CrossInterestList = Entry('margin/v1/cross/interest/list', ['private', 'margin'], 'GET', {'cost': 2}) + private_margin_get_margin_v1_cross_liquidation_list = privateMarginGetMarginV1CrossLiquidationList = Entry('margin/v1/cross/liquidation/list', ['private', 'margin'], 'GET', {'cost': 2}) + private_margin_get_margin_v1_cross_fin_list = privateMarginGetMarginV1CrossFinList = Entry('margin/v1/cross/fin/list', ['private', 'margin'], 'GET', {'cost': 2}) + private_margin_get_margin_v1_cross_account_assets = privateMarginGetMarginV1CrossAccountAssets = Entry('margin/v1/cross/account/assets', ['private', 'margin'], 'GET', {'cost': 2}) + private_margin_get_margin_v1_isolated_account_assets = privateMarginGetMarginV1IsolatedAccountAssets = Entry('margin/v1/isolated/account/assets', ['private', 'margin'], 'GET', {'cost': 2}) + private_margin_get_v2_margin_crossed_borrow_history = privateMarginGetV2MarginCrossedBorrowHistory = Entry('v2/margin/crossed/borrow-history', ['private', 'margin'], 'GET', {'cost': 2}) + private_margin_get_v2_margin_crossed_repay_history = privateMarginGetV2MarginCrossedRepayHistory = Entry('v2/margin/crossed/repay-history', ['private', 'margin'], 'GET', {'cost': 2}) + private_margin_get_v2_margin_crossed_interest_history = privateMarginGetV2MarginCrossedInterestHistory = Entry('v2/margin/crossed/interest-history', ['private', 'margin'], 'GET', {'cost': 2}) + private_margin_get_v2_margin_crossed_liquidation_history = privateMarginGetV2MarginCrossedLiquidationHistory = Entry('v2/margin/crossed/liquidation-history', ['private', 'margin'], 'GET', {'cost': 2}) + private_margin_get_v2_margin_crossed_financial_records = privateMarginGetV2MarginCrossedFinancialRecords = Entry('v2/margin/crossed/financial-records', ['private', 'margin'], 'GET', {'cost': 2}) + private_margin_get_v2_margin_crossed_account_assets = privateMarginGetV2MarginCrossedAccountAssets = Entry('v2/margin/crossed/account/assets', ['private', 'margin'], 'GET', {'cost': 2}) + private_margin_get_v2_margin_crossed_account_risk_rate = privateMarginGetV2MarginCrossedAccountRiskRate = Entry('v2/margin/crossed/account/risk-rate', ['private', 'margin'], 'GET', {'cost': 2}) + private_margin_get_v2_margin_crossed_account_max_borrowable_amount = privateMarginGetV2MarginCrossedAccountMaxBorrowableAmount = Entry('v2/margin/crossed/account/max-borrowable-amount', ['private', 'margin'], 'GET', {'cost': 2}) + private_margin_get_v2_margin_crossed_account_max_transfer_out_amount = privateMarginGetV2MarginCrossedAccountMaxTransferOutAmount = Entry('v2/margin/crossed/account/max-transfer-out-amount', ['private', 'margin'], 'GET', {'cost': 2}) + private_margin_get_v2_margin_crossed_interest_rate_and_limit = privateMarginGetV2MarginCrossedInterestRateAndLimit = Entry('v2/margin/crossed/interest-rate-and-limit', ['private', 'margin'], 'GET', {'cost': 2}) + private_margin_get_v2_margin_crossed_tier_data = privateMarginGetV2MarginCrossedTierData = Entry('v2/margin/crossed/tier-data', ['private', 'margin'], 'GET', {'cost': 2}) + private_margin_get_v2_margin_crossed_open_orders = privateMarginGetV2MarginCrossedOpenOrders = Entry('v2/margin/crossed/open-orders', ['private', 'margin'], 'GET', {'cost': 2}) + private_margin_get_v2_margin_crossed_history_orders = privateMarginGetV2MarginCrossedHistoryOrders = Entry('v2/margin/crossed/history-orders', ['private', 'margin'], 'GET', {'cost': 2}) + private_margin_get_v2_margin_crossed_fills = privateMarginGetV2MarginCrossedFills = Entry('v2/margin/crossed/fills', ['private', 'margin'], 'GET', {'cost': 2}) + private_margin_get_v2_margin_isolated_borrow_history = privateMarginGetV2MarginIsolatedBorrowHistory = Entry('v2/margin/isolated/borrow-history', ['private', 'margin'], 'GET', {'cost': 2}) + private_margin_get_v2_margin_isolated_repay_history = privateMarginGetV2MarginIsolatedRepayHistory = Entry('v2/margin/isolated/repay-history', ['private', 'margin'], 'GET', {'cost': 2}) + private_margin_get_v2_margin_isolated_interest_history = privateMarginGetV2MarginIsolatedInterestHistory = Entry('v2/margin/isolated/interest-history', ['private', 'margin'], 'GET', {'cost': 2}) + private_margin_get_v2_margin_isolated_liquidation_history = privateMarginGetV2MarginIsolatedLiquidationHistory = Entry('v2/margin/isolated/liquidation-history', ['private', 'margin'], 'GET', {'cost': 2}) + private_margin_get_v2_margin_isolated_financial_records = privateMarginGetV2MarginIsolatedFinancialRecords = Entry('v2/margin/isolated/financial-records', ['private', 'margin'], 'GET', {'cost': 2}) + private_margin_get_v2_margin_isolated_account_assets = privateMarginGetV2MarginIsolatedAccountAssets = Entry('v2/margin/isolated/account/assets', ['private', 'margin'], 'GET', {'cost': 2}) + private_margin_get_v2_margin_isolated_account_risk_rate = privateMarginGetV2MarginIsolatedAccountRiskRate = Entry('v2/margin/isolated/account/risk-rate', ['private', 'margin'], 'GET', {'cost': 2}) + private_margin_get_v2_margin_isolated_account_max_borrowable_amount = privateMarginGetV2MarginIsolatedAccountMaxBorrowableAmount = Entry('v2/margin/isolated/account/max-borrowable-amount', ['private', 'margin'], 'GET', {'cost': 2}) + private_margin_get_v2_margin_isolated_account_max_transfer_out_amount = privateMarginGetV2MarginIsolatedAccountMaxTransferOutAmount = Entry('v2/margin/isolated/account/max-transfer-out-amount', ['private', 'margin'], 'GET', {'cost': 2}) + private_margin_get_v2_margin_isolated_interest_rate_and_limit = privateMarginGetV2MarginIsolatedInterestRateAndLimit = Entry('v2/margin/isolated/interest-rate-and-limit', ['private', 'margin'], 'GET', {'cost': 2}) + private_margin_get_v2_margin_isolated_tier_data = privateMarginGetV2MarginIsolatedTierData = Entry('v2/margin/isolated/tier-data', ['private', 'margin'], 'GET', {'cost': 2}) + private_margin_get_v2_margin_isolated_open_orders = privateMarginGetV2MarginIsolatedOpenOrders = Entry('v2/margin/isolated/open-orders', ['private', 'margin'], 'GET', {'cost': 2}) + private_margin_get_v2_margin_isolated_history_orders = privateMarginGetV2MarginIsolatedHistoryOrders = Entry('v2/margin/isolated/history-orders', ['private', 'margin'], 'GET', {'cost': 2}) + private_margin_get_v2_margin_isolated_fills = privateMarginGetV2MarginIsolatedFills = Entry('v2/margin/isolated/fills', ['private', 'margin'], 'GET', {'cost': 2}) + private_margin_post_margin_v1_cross_account_borrow = privateMarginPostMarginV1CrossAccountBorrow = Entry('margin/v1/cross/account/borrow', ['private', 'margin'], 'POST', {'cost': 2}) + private_margin_post_margin_v1_isolated_account_borrow = privateMarginPostMarginV1IsolatedAccountBorrow = Entry('margin/v1/isolated/account/borrow', ['private', 'margin'], 'POST', {'cost': 2}) + private_margin_post_margin_v1_cross_account_repay = privateMarginPostMarginV1CrossAccountRepay = Entry('margin/v1/cross/account/repay', ['private', 'margin'], 'POST', {'cost': 2}) + private_margin_post_margin_v1_isolated_account_repay = privateMarginPostMarginV1IsolatedAccountRepay = Entry('margin/v1/isolated/account/repay', ['private', 'margin'], 'POST', {'cost': 2}) + private_margin_post_margin_v1_isolated_account_riskrate = privateMarginPostMarginV1IsolatedAccountRiskRate = Entry('margin/v1/isolated/account/riskRate', ['private', 'margin'], 'POST', {'cost': 2}) + private_margin_post_margin_v1_cross_account_maxborrowableamount = privateMarginPostMarginV1CrossAccountMaxBorrowableAmount = Entry('margin/v1/cross/account/maxBorrowableAmount', ['private', 'margin'], 'POST', {'cost': 2}) + private_margin_post_margin_v1_isolated_account_maxborrowableamount = privateMarginPostMarginV1IsolatedAccountMaxBorrowableAmount = Entry('margin/v1/isolated/account/maxBorrowableAmount', ['private', 'margin'], 'POST', {'cost': 2}) + private_margin_post_margin_v1_isolated_account_flashrepay = privateMarginPostMarginV1IsolatedAccountFlashRepay = Entry('margin/v1/isolated/account/flashRepay', ['private', 'margin'], 'POST', {'cost': 2}) + private_margin_post_margin_v1_isolated_account_queryflashrepaystatus = privateMarginPostMarginV1IsolatedAccountQueryFlashRepayStatus = Entry('margin/v1/isolated/account/queryFlashRepayStatus', ['private', 'margin'], 'POST', {'cost': 2}) + private_margin_post_margin_v1_cross_account_flashrepay = privateMarginPostMarginV1CrossAccountFlashRepay = Entry('margin/v1/cross/account/flashRepay', ['private', 'margin'], 'POST', {'cost': 2}) + private_margin_post_margin_v1_cross_account_queryflashrepaystatus = privateMarginPostMarginV1CrossAccountQueryFlashRepayStatus = Entry('margin/v1/cross/account/queryFlashRepayStatus', ['private', 'margin'], 'POST', {'cost': 2}) + private_margin_post_margin_v1_isolated_order_placeorder = privateMarginPostMarginV1IsolatedOrderPlaceOrder = Entry('margin/v1/isolated/order/placeOrder', ['private', 'margin'], 'POST', {'cost': 4}) + private_margin_post_margin_v1_isolated_order_batchplaceorder = privateMarginPostMarginV1IsolatedOrderBatchPlaceOrder = Entry('margin/v1/isolated/order/batchPlaceOrder', ['private', 'margin'], 'POST', {'cost': 4}) + private_margin_post_margin_v1_isolated_order_cancelorder = privateMarginPostMarginV1IsolatedOrderCancelOrder = Entry('margin/v1/isolated/order/cancelOrder', ['private', 'margin'], 'POST', {'cost': 2}) + private_margin_post_margin_v1_isolated_order_batchcancelorder = privateMarginPostMarginV1IsolatedOrderBatchCancelOrder = Entry('margin/v1/isolated/order/batchCancelOrder', ['private', 'margin'], 'POST', {'cost': 2}) + private_margin_post_margin_v1_cross_order_placeorder = privateMarginPostMarginV1CrossOrderPlaceOrder = Entry('margin/v1/cross/order/placeOrder', ['private', 'margin'], 'POST', {'cost': 2}) + private_margin_post_margin_v1_cross_order_batchplaceorder = privateMarginPostMarginV1CrossOrderBatchPlaceOrder = Entry('margin/v1/cross/order/batchPlaceOrder', ['private', 'margin'], 'POST', {'cost': 2}) + private_margin_post_margin_v1_cross_order_cancelorder = privateMarginPostMarginV1CrossOrderCancelOrder = Entry('margin/v1/cross/order/cancelOrder', ['private', 'margin'], 'POST', {'cost': 2}) + private_margin_post_margin_v1_cross_order_batchcancelorder = privateMarginPostMarginV1CrossOrderBatchCancelOrder = Entry('margin/v1/cross/order/batchCancelOrder', ['private', 'margin'], 'POST', {'cost': 2}) + private_margin_post_v2_margin_crossed_account_borrow = privateMarginPostV2MarginCrossedAccountBorrow = Entry('v2/margin/crossed/account/borrow', ['private', 'margin'], 'POST', {'cost': 2}) + private_margin_post_v2_margin_crossed_account_repay = privateMarginPostV2MarginCrossedAccountRepay = Entry('v2/margin/crossed/account/repay', ['private', 'margin'], 'POST', {'cost': 2}) + private_margin_post_v2_margin_crossed_account_flash_repay = privateMarginPostV2MarginCrossedAccountFlashRepay = Entry('v2/margin/crossed/account/flash-repay', ['private', 'margin'], 'POST', {'cost': 2}) + private_margin_post_v2_margin_crossed_account_query_flash_repay_status = privateMarginPostV2MarginCrossedAccountQueryFlashRepayStatus = Entry('v2/margin/crossed/account/query-flash-repay-status', ['private', 'margin'], 'POST', {'cost': 2}) + private_margin_post_v2_margin_crossed_place_order = privateMarginPostV2MarginCrossedPlaceOrder = Entry('v2/margin/crossed/place-order', ['private', 'margin'], 'POST', {'cost': 2}) + private_margin_post_v2_margin_crossed_batch_place_order = privateMarginPostV2MarginCrossedBatchPlaceOrder = Entry('v2/margin/crossed/batch-place-order', ['private', 'margin'], 'POST', {'cost': 2}) + private_margin_post_v2_margin_crossed_cancel_order = privateMarginPostV2MarginCrossedCancelOrder = Entry('v2/margin/crossed/cancel-order', ['private', 'margin'], 'POST', {'cost': 2}) + private_margin_post_v2_margin_crossed_batch_cancel_order = privateMarginPostV2MarginCrossedBatchCancelOrder = Entry('v2/margin/crossed/batch-cancel-order', ['private', 'margin'], 'POST', {'cost': 2}) + private_margin_post_v2_margin_isolated_account_borrow = privateMarginPostV2MarginIsolatedAccountBorrow = Entry('v2/margin/isolated/account/borrow', ['private', 'margin'], 'POST', {'cost': 2}) + private_margin_post_v2_margin_isolated_account_repay = privateMarginPostV2MarginIsolatedAccountRepay = Entry('v2/margin/isolated/account/repay', ['private', 'margin'], 'POST', {'cost': 2}) + private_margin_post_v2_margin_isolated_account_flash_repay = privateMarginPostV2MarginIsolatedAccountFlashRepay = Entry('v2/margin/isolated/account/flash-repay', ['private', 'margin'], 'POST', {'cost': 2}) + private_margin_post_v2_margin_isolated_account_query_flash_repay_status = privateMarginPostV2MarginIsolatedAccountQueryFlashRepayStatus = Entry('v2/margin/isolated/account/query-flash-repay-status', ['private', 'margin'], 'POST', {'cost': 2}) + private_margin_post_v2_margin_isolated_place_order = privateMarginPostV2MarginIsolatedPlaceOrder = Entry('v2/margin/isolated/place-order', ['private', 'margin'], 'POST', {'cost': 2}) + private_margin_post_v2_margin_isolated_batch_place_order = privateMarginPostV2MarginIsolatedBatchPlaceOrder = Entry('v2/margin/isolated/batch-place-order', ['private', 'margin'], 'POST', {'cost': 2}) + private_margin_post_v2_margin_isolated_cancel_order = privateMarginPostV2MarginIsolatedCancelOrder = Entry('v2/margin/isolated/cancel-order', ['private', 'margin'], 'POST', {'cost': 2}) + private_margin_post_v2_margin_isolated_batch_cancel_order = privateMarginPostV2MarginIsolatedBatchCancelOrder = Entry('v2/margin/isolated/batch-cancel-order', ['private', 'margin'], 'POST', {'cost': 2}) + private_copy_get_v2_copy_mix_trader_order_current_track = privateCopyGetV2CopyMixTraderOrderCurrentTrack = Entry('v2/copy/mix-trader/order-current-track', ['private', 'copy'], 'GET', {'cost': 2}) + private_copy_get_v2_copy_mix_trader_order_history_track = privateCopyGetV2CopyMixTraderOrderHistoryTrack = Entry('v2/copy/mix-trader/order-history-track', ['private', 'copy'], 'GET', {'cost': 2}) + private_copy_get_v2_copy_mix_trader_order_total_detail = privateCopyGetV2CopyMixTraderOrderTotalDetail = Entry('v2/copy/mix-trader/order-total-detail', ['private', 'copy'], 'GET', {'cost': 2}) + private_copy_get_v2_copy_mix_trader_profit_history_summarys = privateCopyGetV2CopyMixTraderProfitHistorySummarys = Entry('v2/copy/mix-trader/profit-history-summarys', ['private', 'copy'], 'GET', {'cost': 1}) + private_copy_get_v2_copy_mix_trader_profit_history_details = privateCopyGetV2CopyMixTraderProfitHistoryDetails = Entry('v2/copy/mix-trader/profit-history-details', ['private', 'copy'], 'GET', {'cost': 1}) + private_copy_get_v2_copy_mix_trader_profit_details = privateCopyGetV2CopyMixTraderProfitDetails = Entry('v2/copy/mix-trader/profit-details', ['private', 'copy'], 'GET', {'cost': 1}) + private_copy_get_v2_copy_mix_trader_profits_group_coin_date = privateCopyGetV2CopyMixTraderProfitsGroupCoinDate = Entry('v2/copy/mix-trader/profits-group-coin-date', ['private', 'copy'], 'GET', {'cost': 1}) + private_copy_get_v2_copy_mix_trader_config_query_symbols = privateCopyGetV2CopyMixTraderConfigQuerySymbols = Entry('v2/copy/mix-trader/config-query-symbols', ['private', 'copy'], 'GET', {'cost': 1}) + private_copy_get_v2_copy_mix_trader_config_query_followers = privateCopyGetV2CopyMixTraderConfigQueryFollowers = Entry('v2/copy/mix-trader/config-query-followers', ['private', 'copy'], 'GET', {'cost': 2}) + private_copy_get_v2_copy_mix_follower_query_current_orders = privateCopyGetV2CopyMixFollowerQueryCurrentOrders = Entry('v2/copy/mix-follower/query-current-orders', ['private', 'copy'], 'GET', {'cost': 2}) + private_copy_get_v2_copy_mix_follower_query_history_orders = privateCopyGetV2CopyMixFollowerQueryHistoryOrders = Entry('v2/copy/mix-follower/query-history-orders', ['private', 'copy'], 'GET', {'cost': 1}) + private_copy_get_v2_copy_mix_follower_query_settings = privateCopyGetV2CopyMixFollowerQuerySettings = Entry('v2/copy/mix-follower/query-settings', ['private', 'copy'], 'GET', {'cost': 2}) + private_copy_get_v2_copy_mix_follower_query_traders = privateCopyGetV2CopyMixFollowerQueryTraders = Entry('v2/copy/mix-follower/query-traders', ['private', 'copy'], 'GET', {'cost': 2}) + private_copy_get_v2_copy_mix_follower_query_quantity_limit = privateCopyGetV2CopyMixFollowerQueryQuantityLimit = Entry('v2/copy/mix-follower/query-quantity-limit', ['private', 'copy'], 'GET', {'cost': 2}) + private_copy_get_v2_copy_mix_broker_query_traders = privateCopyGetV2CopyMixBrokerQueryTraders = Entry('v2/copy/mix-broker/query-traders', ['private', 'copy'], 'GET', {'cost': 2}) + private_copy_get_v2_copy_mix_broker_query_history_traces = privateCopyGetV2CopyMixBrokerQueryHistoryTraces = Entry('v2/copy/mix-broker/query-history-traces', ['private', 'copy'], 'GET', {'cost': 2}) + private_copy_get_v2_copy_mix_broker_query_current_traces = privateCopyGetV2CopyMixBrokerQueryCurrentTraces = Entry('v2/copy/mix-broker/query-current-traces', ['private', 'copy'], 'GET', {'cost': 2}) + private_copy_get_v2_copy_spot_trader_profit_summarys = privateCopyGetV2CopySpotTraderProfitSummarys = Entry('v2/copy/spot-trader/profit-summarys', ['private', 'copy'], 'GET', {'cost': 2}) + private_copy_get_v2_copy_spot_trader_profit_history_details = privateCopyGetV2CopySpotTraderProfitHistoryDetails = Entry('v2/copy/spot-trader/profit-history-details', ['private', 'copy'], 'GET', {'cost': 2}) + private_copy_get_v2_copy_spot_trader_profit_details = privateCopyGetV2CopySpotTraderProfitDetails = Entry('v2/copy/spot-trader/profit-details', ['private', 'copy'], 'GET', {'cost': 2}) + private_copy_get_v2_copy_spot_trader_order_total_detail = privateCopyGetV2CopySpotTraderOrderTotalDetail = Entry('v2/copy/spot-trader/order-total-detail', ['private', 'copy'], 'GET', {'cost': 2}) + private_copy_get_v2_copy_spot_trader_order_history_track = privateCopyGetV2CopySpotTraderOrderHistoryTrack = Entry('v2/copy/spot-trader/order-history-track', ['private', 'copy'], 'GET', {'cost': 2}) + private_copy_get_v2_copy_spot_trader_order_current_track = privateCopyGetV2CopySpotTraderOrderCurrentTrack = Entry('v2/copy/spot-trader/order-current-track', ['private', 'copy'], 'GET', {'cost': 2}) + private_copy_get_v2_copy_spot_trader_config_query_settings = privateCopyGetV2CopySpotTraderConfigQuerySettings = Entry('v2/copy/spot-trader/config-query-settings', ['private', 'copy'], 'GET', {'cost': 2}) + private_copy_get_v2_copy_spot_trader_config_query_followers = privateCopyGetV2CopySpotTraderConfigQueryFollowers = Entry('v2/copy/spot-trader/config-query-followers', ['private', 'copy'], 'GET', {'cost': 2}) + private_copy_get_v2_copy_spot_follower_query_traders = privateCopyGetV2CopySpotFollowerQueryTraders = Entry('v2/copy/spot-follower/query-traders', ['private', 'copy'], 'GET', {'cost': 2}) + private_copy_get_v2_copy_spot_follower_query_trader_symbols = privateCopyGetV2CopySpotFollowerQueryTraderSymbols = Entry('v2/copy/spot-follower/query-trader-symbols', ['private', 'copy'], 'GET', {'cost': 2}) + private_copy_get_v2_copy_spot_follower_query_settings = privateCopyGetV2CopySpotFollowerQuerySettings = Entry('v2/copy/spot-follower/query-settings', ['private', 'copy'], 'GET', {'cost': 2}) + private_copy_get_v2_copy_spot_follower_query_history_orders = privateCopyGetV2CopySpotFollowerQueryHistoryOrders = Entry('v2/copy/spot-follower/query-history-orders', ['private', 'copy'], 'GET', {'cost': 2}) + private_copy_get_v2_copy_spot_follower_query_current_orders = privateCopyGetV2CopySpotFollowerQueryCurrentOrders = Entry('v2/copy/spot-follower/query-current-orders', ['private', 'copy'], 'GET', {'cost': 2}) + private_copy_post_v2_copy_mix_trader_order_modify_tpsl = privateCopyPostV2CopyMixTraderOrderModifyTpsl = Entry('v2/copy/mix-trader/order-modify-tpsl', ['private', 'copy'], 'POST', {'cost': 2}) + private_copy_post_v2_copy_mix_trader_order_close_positions = privateCopyPostV2CopyMixTraderOrderClosePositions = Entry('v2/copy/mix-trader/order-close-positions', ['private', 'copy'], 'POST', {'cost': 2}) + private_copy_post_v2_copy_mix_trader_config_setting_symbols = privateCopyPostV2CopyMixTraderConfigSettingSymbols = Entry('v2/copy/mix-trader/config-setting-symbols', ['private', 'copy'], 'POST', {'cost': 2}) + private_copy_post_v2_copy_mix_trader_config_setting_base = privateCopyPostV2CopyMixTraderConfigSettingBase = Entry('v2/copy/mix-trader/config-setting-base', ['private', 'copy'], 'POST', {'cost': 2}) + private_copy_post_v2_copy_mix_trader_config_remove_follower = privateCopyPostV2CopyMixTraderConfigRemoveFollower = Entry('v2/copy/mix-trader/config-remove-follower', ['private', 'copy'], 'POST', {'cost': 2}) + private_copy_post_v2_copy_mix_follower_setting_tpsl = privateCopyPostV2CopyMixFollowerSettingTpsl = Entry('v2/copy/mix-follower/setting-tpsl', ['private', 'copy'], 'POST', {'cost': 1}) + private_copy_post_v2_copy_mix_follower_settings = privateCopyPostV2CopyMixFollowerSettings = Entry('v2/copy/mix-follower/settings', ['private', 'copy'], 'POST', {'cost': 2}) + private_copy_post_v2_copy_mix_follower_close_positions = privateCopyPostV2CopyMixFollowerClosePositions = Entry('v2/copy/mix-follower/close-positions', ['private', 'copy'], 'POST', {'cost': 2}) + private_copy_post_v2_copy_mix_follower_cancel_trader = privateCopyPostV2CopyMixFollowerCancelTrader = Entry('v2/copy/mix-follower/cancel-trader', ['private', 'copy'], 'POST', {'cost': 4}) + private_copy_post_v2_copy_spot_trader_order_modify_tpsl = privateCopyPostV2CopySpotTraderOrderModifyTpsl = Entry('v2/copy/spot-trader/order-modify-tpsl', ['private', 'copy'], 'POST', {'cost': 2}) + private_copy_post_v2_copy_spot_trader_order_close_tracking = privateCopyPostV2CopySpotTraderOrderCloseTracking = Entry('v2/copy/spot-trader/order-close-tracking', ['private', 'copy'], 'POST', {'cost': 2}) + private_copy_post_v2_copy_spot_trader_config_setting_symbols = privateCopyPostV2CopySpotTraderConfigSettingSymbols = Entry('v2/copy/spot-trader/config-setting-symbols', ['private', 'copy'], 'POST', {'cost': 2}) + private_copy_post_v2_copy_spot_trader_config_remove_follower = privateCopyPostV2CopySpotTraderConfigRemoveFollower = Entry('v2/copy/spot-trader/config-remove-follower', ['private', 'copy'], 'POST', {'cost': 2}) + private_copy_post_v2_copy_spot_follower_stop_order = privateCopyPostV2CopySpotFollowerStopOrder = Entry('v2/copy/spot-follower/stop-order', ['private', 'copy'], 'POST', {'cost': 2}) + private_copy_post_v2_copy_spot_follower_settings = privateCopyPostV2CopySpotFollowerSettings = Entry('v2/copy/spot-follower/settings', ['private', 'copy'], 'POST', {'cost': 2}) + private_copy_post_v2_copy_spot_follower_setting_tpsl = privateCopyPostV2CopySpotFollowerSettingTpsl = Entry('v2/copy/spot-follower/setting-tpsl', ['private', 'copy'], 'POST', {'cost': 2}) + private_copy_post_v2_copy_spot_follower_order_close_tracking = privateCopyPostV2CopySpotFollowerOrderCloseTracking = Entry('v2/copy/spot-follower/order-close-tracking', ['private', 'copy'], 'POST', {'cost': 2}) + private_copy_post_v2_copy_spot_follower_cancel_trader = privateCopyPostV2CopySpotFollowerCancelTrader = Entry('v2/copy/spot-follower/cancel-trader', ['private', 'copy'], 'POST', {'cost': 2}) + private_tax_get_v2_tax_spot_record = privateTaxGetV2TaxSpotRecord = Entry('v2/tax/spot-record', ['private', 'tax'], 'GET', {'cost': 20}) + private_tax_get_v2_tax_future_record = privateTaxGetV2TaxFutureRecord = Entry('v2/tax/future-record', ['private', 'tax'], 'GET', {'cost': 20}) + private_tax_get_v2_tax_margin_record = privateTaxGetV2TaxMarginRecord = Entry('v2/tax/margin-record', ['private', 'tax'], 'GET', {'cost': 20}) + private_tax_get_v2_tax_p2p_record = privateTaxGetV2TaxP2pRecord = Entry('v2/tax/p2p-record', ['private', 'tax'], 'GET', {'cost': 20}) + private_convert_get_v2_convert_currencies = privateConvertGetV2ConvertCurrencies = Entry('v2/convert/currencies', ['private', 'convert'], 'GET', {'cost': 2}) + private_convert_get_v2_convert_quoted_price = privateConvertGetV2ConvertQuotedPrice = Entry('v2/convert/quoted-price', ['private', 'convert'], 'GET', {'cost': 2}) + private_convert_get_v2_convert_convert_record = privateConvertGetV2ConvertConvertRecord = Entry('v2/convert/convert-record', ['private', 'convert'], 'GET', {'cost': 2}) + private_convert_get_v2_convert_bgb_convert_coin_list = privateConvertGetV2ConvertBgbConvertCoinList = Entry('v2/convert/bgb-convert-coin-list', ['private', 'convert'], 'GET', {'cost': 2}) + private_convert_get_v2_convert_bgb_convert_records = privateConvertGetV2ConvertBgbConvertRecords = Entry('v2/convert/bgb-convert-records', ['private', 'convert'], 'GET', {'cost': 2}) + private_convert_post_v2_convert_trade = privateConvertPostV2ConvertTrade = Entry('v2/convert/trade', ['private', 'convert'], 'POST', {'cost': 2}) + private_convert_post_v2_convert_bgb_convert = privateConvertPostV2ConvertBgbConvert = Entry('v2/convert/bgb-convert', ['private', 'convert'], 'POST', {'cost': 2}) + private_earn_get_v2_earn_savings_product = privateEarnGetV2EarnSavingsProduct = Entry('v2/earn/savings/product', ['private', 'earn'], 'GET', {'cost': 2}) + private_earn_get_v2_earn_savings_account = privateEarnGetV2EarnSavingsAccount = Entry('v2/earn/savings/account', ['private', 'earn'], 'GET', {'cost': 2}) + private_earn_get_v2_earn_savings_assets = privateEarnGetV2EarnSavingsAssets = Entry('v2/earn/savings/assets', ['private', 'earn'], 'GET', {'cost': 2}) + private_earn_get_v2_earn_savings_records = privateEarnGetV2EarnSavingsRecords = Entry('v2/earn/savings/records', ['private', 'earn'], 'GET', {'cost': 2}) + private_earn_get_v2_earn_savings_subscribe_info = privateEarnGetV2EarnSavingsSubscribeInfo = Entry('v2/earn/savings/subscribe-info', ['private', 'earn'], 'GET', {'cost': 2}) + private_earn_get_v2_earn_savings_subscribe_result = privateEarnGetV2EarnSavingsSubscribeResult = Entry('v2/earn/savings/subscribe-result', ['private', 'earn'], 'GET', {'cost': 2}) + private_earn_get_v2_earn_savings_redeem_result = privateEarnGetV2EarnSavingsRedeemResult = Entry('v2/earn/savings/redeem-result', ['private', 'earn'], 'GET', {'cost': 2}) + private_earn_get_v2_earn_sharkfin_product = privateEarnGetV2EarnSharkfinProduct = Entry('v2/earn/sharkfin/product', ['private', 'earn'], 'GET', {'cost': 2}) + private_earn_get_v2_earn_sharkfin_account = privateEarnGetV2EarnSharkfinAccount = Entry('v2/earn/sharkfin/account', ['private', 'earn'], 'GET', {'cost': 2}) + private_earn_get_v2_earn_sharkfin_assets = privateEarnGetV2EarnSharkfinAssets = Entry('v2/earn/sharkfin/assets', ['private', 'earn'], 'GET', {'cost': 2}) + private_earn_get_v2_earn_sharkfin_records = privateEarnGetV2EarnSharkfinRecords = Entry('v2/earn/sharkfin/records', ['private', 'earn'], 'GET', {'cost': 2}) + private_earn_get_v2_earn_sharkfin_subscribe_info = privateEarnGetV2EarnSharkfinSubscribeInfo = Entry('v2/earn/sharkfin/subscribe-info', ['private', 'earn'], 'GET', {'cost': 2}) + private_earn_get_v2_earn_sharkfin_subscribe_result = privateEarnGetV2EarnSharkfinSubscribeResult = Entry('v2/earn/sharkfin/subscribe-result', ['private', 'earn'], 'GET', {'cost': 4}) + private_earn_get_v2_earn_loan_ongoing_orders = privateEarnGetV2EarnLoanOngoingOrders = Entry('v2/earn/loan/ongoing-orders', ['private', 'earn'], 'GET', {'cost': 2}) + private_earn_get_v2_earn_loan_repay_history = privateEarnGetV2EarnLoanRepayHistory = Entry('v2/earn/loan/repay-history', ['private', 'earn'], 'GET', {'cost': 2}) + private_earn_get_v2_earn_loan_revise_history = privateEarnGetV2EarnLoanReviseHistory = Entry('v2/earn/loan/revise-history', ['private', 'earn'], 'GET', {'cost': 2}) + private_earn_get_v2_earn_loan_borrow_history = privateEarnGetV2EarnLoanBorrowHistory = Entry('v2/earn/loan/borrow-history', ['private', 'earn'], 'GET', {'cost': 2}) + private_earn_get_v2_earn_loan_debts = privateEarnGetV2EarnLoanDebts = Entry('v2/earn/loan/debts', ['private', 'earn'], 'GET', {'cost': 2}) + private_earn_get_v2_earn_loan_reduces = privateEarnGetV2EarnLoanReduces = Entry('v2/earn/loan/reduces', ['private', 'earn'], 'GET', {'cost': 2}) + private_earn_get_v2_earn_account_assets = privateEarnGetV2EarnAccountAssets = Entry('v2/earn/account/assets', ['private', 'earn'], 'GET', {'cost': 2}) + private_earn_post_v2_earn_savings_subscribe = privateEarnPostV2EarnSavingsSubscribe = Entry('v2/earn/savings/subscribe', ['private', 'earn'], 'POST', {'cost': 2}) + private_earn_post_v2_earn_savings_redeem = privateEarnPostV2EarnSavingsRedeem = Entry('v2/earn/savings/redeem', ['private', 'earn'], 'POST', {'cost': 2}) + private_earn_post_v2_earn_sharkfin_subscribe = privateEarnPostV2EarnSharkfinSubscribe = Entry('v2/earn/sharkfin/subscribe', ['private', 'earn'], 'POST', {'cost': 2}) + private_earn_post_v2_earn_loan_borrow = privateEarnPostV2EarnLoanBorrow = Entry('v2/earn/loan/borrow', ['private', 'earn'], 'POST', {'cost': 2}) + private_earn_post_v2_earn_loan_repay = privateEarnPostV2EarnLoanRepay = Entry('v2/earn/loan/repay', ['private', 'earn'], 'POST', {'cost': 2}) + private_earn_post_v2_earn_loan_revise_pledge = privateEarnPostV2EarnLoanRevisePledge = Entry('v2/earn/loan/revise-pledge', ['private', 'earn'], 'POST', {'cost': 2}) + private_common_get_v2_common_trade_rate = privateCommonGetV2CommonTradeRate = Entry('v2/common/trade-rate', ['private', 'common'], 'GET', {'cost': 2}) + private_uta_get_v3_account_assets = privateUtaGetV3AccountAssets = Entry('v3/account/assets', ['private', 'uta'], 'GET', {'cost': 1}) + private_uta_get_v3_account_settings = privateUtaGetV3AccountSettings = Entry('v3/account/settings', ['private', 'uta'], 'GET', {'cost': 1}) + private_uta_get_v3_account_deposit_records = privateUtaGetV3AccountDepositRecords = Entry('v3/account/deposit-records', ['private', 'uta'], 'GET', {'cost': 2}) + private_uta_get_v3_account_financial_records = privateUtaGetV3AccountFinancialRecords = Entry('v3/account/financial-records', ['private', 'uta'], 'GET', {'cost': 1}) + private_uta_get_v3_account_repayable_coins = privateUtaGetV3AccountRepayableCoins = Entry('v3/account/repayable-coins', ['private', 'uta'], 'GET', {'cost': 2}) + private_uta_get_v3_account_payment_coins = privateUtaGetV3AccountPaymentCoins = Entry('v3/account/payment-coins', ['private', 'uta'], 'GET', {'cost': 2}) + private_uta_get_v3_account_convert_records = privateUtaGetV3AccountConvertRecords = Entry('v3/account/convert-records', ['private', 'uta'], 'GET', {'cost': 1}) + private_uta_get_v3_account_transferable_coins = privateUtaGetV3AccountTransferableCoins = Entry('v3/account/transferable-coins', ['private', 'uta'], 'GET', {'cost': 2}) + private_uta_get_v3_account_sub_transfer_record = privateUtaGetV3AccountSubTransferRecord = Entry('v3/account/sub-transfer-record', ['private', 'uta'], 'GET', {'cost': 4}) + private_uta_get_v3_ins_loan_transfered = privateUtaGetV3InsLoanTransfered = Entry('v3/ins-loan/transfered', ['private', 'uta'], 'GET', {'cost': 6.6667}) + private_uta_get_v3_ins_loan_symbols = privateUtaGetV3InsLoanSymbols = Entry('v3/ins-loan/symbols', ['private', 'uta'], 'GET', {'cost': 6.6667}) + private_uta_get_v3_ins_loan_risk_unit = privateUtaGetV3InsLoanRiskUnit = Entry('v3/ins-loan/risk-unit', ['private', 'uta'], 'GET', {'cost': 6.6667}) + private_uta_get_v3_ins_loan_repaid_history = privateUtaGetV3InsLoanRepaidHistory = Entry('v3/ins-loan/repaid-history', ['private', 'uta'], 'GET', {'cost': 6.6667}) + private_uta_get_v3_ins_loan_product_infos = privateUtaGetV3InsLoanProductInfos = Entry('v3/ins-loan/product-infos', ['private', 'uta'], 'GET', {'cost': 6.6667}) + private_uta_get_v3_ins_loan_loan_order = privateUtaGetV3InsLoanLoanOrder = Entry('v3/ins-loan/loan-order', ['private', 'uta'], 'GET', {'cost': 6.6667}) + private_uta_get_v3_ins_loan_ltv_convert = privateUtaGetV3InsLoanLtvConvert = Entry('v3/ins-loan/ltv-convert', ['private', 'uta'], 'GET', {'cost': 6.6667}) + private_uta_get_v3_ins_loan_ensure_coins_convert = privateUtaGetV3InsLoanEnsureCoinsConvert = Entry('v3/ins-loan/ensure-coins-convert', ['private', 'uta'], 'GET', {'cost': 6.6667}) + private_uta_get_v3_position_current_position = privateUtaGetV3PositionCurrentPosition = Entry('v3/position/current-position', ['private', 'uta'], 'GET', {'cost': 1}) + private_uta_get_v3_position_history_position = privateUtaGetV3PositionHistoryPosition = Entry('v3/position/history-position', ['private', 'uta'], 'GET', {'cost': 1}) + private_uta_get_v3_trade_order_info = privateUtaGetV3TradeOrderInfo = Entry('v3/trade/order-info', ['private', 'uta'], 'GET', {'cost': 1}) + private_uta_get_v3_trade_unfilled_orders = privateUtaGetV3TradeUnfilledOrders = Entry('v3/trade/unfilled-orders', ['private', 'uta'], 'GET', {'cost': 1}) + private_uta_get_v3_trade_unfilled_strategy_orders = privateUtaGetV3TradeUnfilledStrategyOrders = Entry('v3/trade/unfilled-strategy-orders', ['private', 'uta'], 'GET', {'cost': 1}) + private_uta_get_v3_trade_history_orders = privateUtaGetV3TradeHistoryOrders = Entry('v3/trade/history-orders', ['private', 'uta'], 'GET', {'cost': 1}) + private_uta_get_v3_trade_history_strategy_orders = privateUtaGetV3TradeHistoryStrategyOrders = Entry('v3/trade/history-strategy-orders', ['private', 'uta'], 'GET', {'cost': 1}) + private_uta_get_v3_trade_fills = privateUtaGetV3TradeFills = Entry('v3/trade/fills', ['private', 'uta'], 'GET', {'cost': 1}) + private_uta_get_v3_user_sub_list = privateUtaGetV3UserSubList = Entry('v3/user/sub-list', ['private', 'uta'], 'GET', {'cost': 2}) + private_uta_get_v3_user_sub_api_list = privateUtaGetV3UserSubApiList = Entry('v3/user/sub-api-list', ['private', 'uta'], 'GET', {'cost': 2}) + private_uta_post_v3_account_set_leverage = privateUtaPostV3AccountSetLeverage = Entry('v3/account/set-leverage', ['private', 'uta'], 'POST', {'cost': 2}) + private_uta_post_v3_account_set_hold_mode = privateUtaPostV3AccountSetHoldMode = Entry('v3/account/set-hold-mode', ['private', 'uta'], 'POST', {'cost': 2}) + private_uta_post_v3_account_repay = privateUtaPostV3AccountRepay = Entry('v3/account/repay', ['private', 'uta'], 'POST', {'cost': 4}) + private_uta_post_v3_account_transfer = privateUtaPostV3AccountTransfer = Entry('v3/account/transfer', ['private', 'uta'], 'POST', {'cost': 4}) + private_uta_post_v3_account_sub_transfer = privateUtaPostV3AccountSubTransfer = Entry('v3/account/sub-transfer', ['private', 'uta'], 'POST', {'cost': 4}) + private_uta_post_v3_account_max_open_available = privateUtaPostV3AccountMaxOpenAvailable = Entry('v3/account/max-open-available', ['private', 'uta'], 'POST', {'cost': 4}) + private_uta_post_v3_ins_loan_bind_uid = privateUtaPostV3InsLoanBindUid = Entry('v3/ins-loan/bind-uid', ['private', 'uta'], 'POST', {'cost': 6.6667}) + private_uta_post_v3_trade_place_order = privateUtaPostV3TradePlaceOrder = Entry('v3/trade/place-order', ['private', 'uta'], 'POST', {'cost': 2}) + private_uta_post_v3_trade_place_strategy_order = privateUtaPostV3TradePlaceStrategyOrder = Entry('v3/trade/place-strategy-order', ['private', 'uta'], 'POST', {'cost': 2}) + private_uta_post_v3_trade_modify_order = privateUtaPostV3TradeModifyOrder = Entry('v3/trade/modify-order', ['private', 'uta'], 'POST', {'cost': 2}) + private_uta_post_v3_trade_modify_strategy_order = privateUtaPostV3TradeModifyStrategyOrder = Entry('v3/trade/modify-strategy-order', ['private', 'uta'], 'POST', {'cost': 2}) + private_uta_post_v3_trade_cancel_order = privateUtaPostV3TradeCancelOrder = Entry('v3/trade/cancel-order', ['private', 'uta'], 'POST', {'cost': 2}) + private_uta_post_v3_trade_cancel_strategy_order = privateUtaPostV3TradeCancelStrategyOrder = Entry('v3/trade/cancel-strategy-order', ['private', 'uta'], 'POST', {'cost': 2}) + private_uta_post_v3_trade_place_batch = privateUtaPostV3TradePlaceBatch = Entry('v3/trade/place-batch', ['private', 'uta'], 'POST', {'cost': 4}) + private_uta_post_v3_trade_batch_modify_order = privateUtaPostV3TradeBatchModifyOrder = Entry('v3/trade/batch-modify-order', ['private', 'uta'], 'POST', {'cost': 2}) + private_uta_post_v3_trade_cancel_batch = privateUtaPostV3TradeCancelBatch = Entry('v3/trade/cancel-batch', ['private', 'uta'], 'POST', {'cost': 4}) + private_uta_post_v3_trade_cancel_symbol_order = privateUtaPostV3TradeCancelSymbolOrder = Entry('v3/trade/cancel-symbol-order', ['private', 'uta'], 'POST', {'cost': 4}) + private_uta_post_v3_trade_close_positions = privateUtaPostV3TradeClosePositions = Entry('v3/trade/close-positions', ['private', 'uta'], 'POST', {'cost': 4}) + private_uta_post_v3_user_create_sub = privateUtaPostV3UserCreateSub = Entry('v3/user/create-sub', ['private', 'uta'], 'POST', {'cost': 2}) + private_uta_post_v3_user_freeze_sub = privateUtaPostV3UserFreezeSub = Entry('v3/user/freeze-sub', ['private', 'uta'], 'POST', {'cost': 2}) + private_uta_post_v3_user_create_sub_api = privateUtaPostV3UserCreateSubApi = Entry('v3/user/create-sub-api', ['private', 'uta'], 'POST', {'cost': 2}) + private_uta_post_v3_user_update_sub_api = privateUtaPostV3UserUpdateSubApi = Entry('v3/user/update-sub-api', ['private', 'uta'], 'POST', {'cost': 2}) + private_uta_post_v3_user_delete_sub_api = privateUtaPostV3UserDeleteSubApi = Entry('v3/user/delete-sub-api', ['private', 'uta'], 'POST', {'cost': 2}) diff --git a/ccxt/abstract/bithumb.py b/ccxt/abstract/bithumb.py new file mode 100644 index 0000000..6a9df64 --- /dev/null +++ b/ccxt/abstract/bithumb.py @@ -0,0 +1,32 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_ticker_all_quoteid = publicGetTickerALLQuoteId = Entry('ticker/ALL_{quoteId}', 'public', 'GET', {}) + public_get_ticker_baseid_quoteid = publicGetTickerBaseIdQuoteId = Entry('ticker/{baseId}_{quoteId}', 'public', 'GET', {}) + public_get_orderbook_all_quoteid = publicGetOrderbookALLQuoteId = Entry('orderbook/ALL_{quoteId}', 'public', 'GET', {}) + public_get_orderbook_baseid_quoteid = publicGetOrderbookBaseIdQuoteId = Entry('orderbook/{baseId}_{quoteId}', 'public', 'GET', {}) + public_get_transaction_history_baseid_quoteid = publicGetTransactionHistoryBaseIdQuoteId = Entry('transaction_history/{baseId}_{quoteId}', 'public', 'GET', {}) + public_get_network_info = publicGetNetworkInfo = Entry('network-info', 'public', 'GET', {}) + public_get_assetsstatus_multichain_all = publicGetAssetsstatusMultichainALL = Entry('assetsstatus/multichain/ALL', 'public', 'GET', {}) + public_get_assetsstatus_multichain_currency = publicGetAssetsstatusMultichainCurrency = Entry('assetsstatus/multichain/{currency}', 'public', 'GET', {}) + public_get_withdraw_minimum_all = publicGetWithdrawMinimumALL = Entry('withdraw/minimum/ALL', 'public', 'GET', {}) + public_get_withdraw_minimum_currency = publicGetWithdrawMinimumCurrency = Entry('withdraw/minimum/{currency}', 'public', 'GET', {}) + public_get_assetsstatus_all = publicGetAssetsstatusALL = Entry('assetsstatus/ALL', 'public', 'GET', {}) + public_get_assetsstatus_baseid = publicGetAssetsstatusBaseId = Entry('assetsstatus/{baseId}', 'public', 'GET', {}) + public_get_candlestick_baseid_quoteid_interval = publicGetCandlestickBaseIdQuoteIdInterval = Entry('candlestick/{baseId}_{quoteId}/{interval}', 'public', 'GET', {}) + private_post_info_account = privatePostInfoAccount = Entry('info/account', 'private', 'POST', {}) + private_post_info_balance = privatePostInfoBalance = Entry('info/balance', 'private', 'POST', {}) + private_post_info_wallet_address = privatePostInfoWalletAddress = Entry('info/wallet_address', 'private', 'POST', {}) + private_post_info_ticker = privatePostInfoTicker = Entry('info/ticker', 'private', 'POST', {}) + private_post_info_orders = privatePostInfoOrders = Entry('info/orders', 'private', 'POST', {}) + private_post_info_user_transactions = privatePostInfoUserTransactions = Entry('info/user_transactions', 'private', 'POST', {}) + private_post_info_order_detail = privatePostInfoOrderDetail = Entry('info/order_detail', 'private', 'POST', {}) + private_post_trade_place = privatePostTradePlace = Entry('trade/place', 'private', 'POST', {}) + private_post_trade_cancel = privatePostTradeCancel = Entry('trade/cancel', 'private', 'POST', {}) + private_post_trade_btc_withdrawal = privatePostTradeBtcWithdrawal = Entry('trade/btc_withdrawal', 'private', 'POST', {}) + private_post_trade_krw_deposit = privatePostTradeKrwDeposit = Entry('trade/krw_deposit', 'private', 'POST', {}) + private_post_trade_krw_withdrawal = privatePostTradeKrwWithdrawal = Entry('trade/krw_withdrawal', 'private', 'POST', {}) + private_post_trade_market_buy = privatePostTradeMarketBuy = Entry('trade/market_buy', 'private', 'POST', {}) + private_post_trade_market_sell = privatePostTradeMarketSell = Entry('trade/market_sell', 'private', 'POST', {}) + private_post_trade_stop_limit = privatePostTradeStopLimit = Entry('trade/stop_limit', 'private', 'POST', {}) diff --git a/ccxt/abstract/bitmart.py b/ccxt/abstract/bitmart.py new file mode 100644 index 0000000..5e0727b --- /dev/null +++ b/ccxt/abstract/bitmart.py @@ -0,0 +1,117 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_system_time = publicGetSystemTime = Entry('system/time', 'public', 'GET', {'cost': 3}) + public_get_system_service = publicGetSystemService = Entry('system/service', 'public', 'GET', {'cost': 3}) + public_get_spot_v1_currencies = publicGetSpotV1Currencies = Entry('spot/v1/currencies', 'public', 'GET', {'cost': 7.5}) + public_get_spot_v1_symbols = publicGetSpotV1Symbols = Entry('spot/v1/symbols', 'public', 'GET', {'cost': 7.5}) + public_get_spot_v1_symbols_details = publicGetSpotV1SymbolsDetails = Entry('spot/v1/symbols/details', 'public', 'GET', {'cost': 5}) + public_get_spot_quotation_v3_tickers = publicGetSpotQuotationV3Tickers = Entry('spot/quotation/v3/tickers', 'public', 'GET', {'cost': 6}) + public_get_spot_quotation_v3_ticker = publicGetSpotQuotationV3Ticker = Entry('spot/quotation/v3/ticker', 'public', 'GET', {'cost': 4}) + public_get_spot_quotation_v3_lite_klines = publicGetSpotQuotationV3LiteKlines = Entry('spot/quotation/v3/lite-klines', 'public', 'GET', {'cost': 5}) + public_get_spot_quotation_v3_klines = publicGetSpotQuotationV3Klines = Entry('spot/quotation/v3/klines', 'public', 'GET', {'cost': 7}) + public_get_spot_quotation_v3_books = publicGetSpotQuotationV3Books = Entry('spot/quotation/v3/books', 'public', 'GET', {'cost': 4}) + public_get_spot_quotation_v3_trades = publicGetSpotQuotationV3Trades = Entry('spot/quotation/v3/trades', 'public', 'GET', {'cost': 4}) + public_get_spot_v1_ticker = publicGetSpotV1Ticker = Entry('spot/v1/ticker', 'public', 'GET', {'cost': 5}) + public_get_spot_v2_ticker = publicGetSpotV2Ticker = Entry('spot/v2/ticker', 'public', 'GET', {'cost': 30}) + public_get_spot_v1_ticker_detail = publicGetSpotV1TickerDetail = Entry('spot/v1/ticker_detail', 'public', 'GET', {'cost': 5}) + public_get_spot_v1_steps = publicGetSpotV1Steps = Entry('spot/v1/steps', 'public', 'GET', {'cost': 30}) + public_get_spot_v1_symbols_kline = publicGetSpotV1SymbolsKline = Entry('spot/v1/symbols/kline', 'public', 'GET', {'cost': 6}) + public_get_spot_v1_symbols_book = publicGetSpotV1SymbolsBook = Entry('spot/v1/symbols/book', 'public', 'GET', {'cost': 5}) + public_get_spot_v1_symbols_trades = publicGetSpotV1SymbolsTrades = Entry('spot/v1/symbols/trades', 'public', 'GET', {'cost': 5}) + public_get_contract_v1_tickers = publicGetContractV1Tickers = Entry('contract/v1/tickers', 'public', 'GET', {'cost': 15}) + public_get_contract_public_details = publicGetContractPublicDetails = Entry('contract/public/details', 'public', 'GET', {'cost': 5}) + public_get_contract_public_depth = publicGetContractPublicDepth = Entry('contract/public/depth', 'public', 'GET', {'cost': 5}) + public_get_contract_public_open_interest = publicGetContractPublicOpenInterest = Entry('contract/public/open-interest', 'public', 'GET', {'cost': 30}) + public_get_contract_public_funding_rate = publicGetContractPublicFundingRate = Entry('contract/public/funding-rate', 'public', 'GET', {'cost': 30}) + public_get_contract_public_funding_rate_history = publicGetContractPublicFundingRateHistory = Entry('contract/public/funding-rate-history', 'public', 'GET', {'cost': 30}) + public_get_contract_public_kline = publicGetContractPublicKline = Entry('contract/public/kline', 'public', 'GET', {'cost': 6}) + public_get_account_v1_currencies = publicGetAccountV1Currencies = Entry('account/v1/currencies', 'public', 'GET', {'cost': 30}) + public_get_contract_public_markprice_kline = publicGetContractPublicMarkpriceKline = Entry('contract/public/markprice-kline', 'public', 'GET', {'cost': 5}) + private_get_account_sub_account_v1_transfer_list = privateGetAccountSubAccountV1TransferList = Entry('account/sub-account/v1/transfer-list', 'private', 'GET', {'cost': 7.5}) + private_get_account_sub_account_v1_transfer_history = privateGetAccountSubAccountV1TransferHistory = Entry('account/sub-account/v1/transfer-history', 'private', 'GET', {'cost': 7.5}) + private_get_account_sub_account_main_v1_wallet = privateGetAccountSubAccountMainV1Wallet = Entry('account/sub-account/main/v1/wallet', 'private', 'GET', {'cost': 5}) + private_get_account_sub_account_main_v1_subaccount_list = privateGetAccountSubAccountMainV1SubaccountList = Entry('account/sub-account/main/v1/subaccount-list', 'private', 'GET', {'cost': 7.5}) + private_get_account_contract_sub_account_main_v1_wallet = privateGetAccountContractSubAccountMainV1Wallet = Entry('account/contract/sub-account/main/v1/wallet', 'private', 'GET', {'cost': 5}) + private_get_account_contract_sub_account_main_v1_transfer_list = privateGetAccountContractSubAccountMainV1TransferList = Entry('account/contract/sub-account/main/v1/transfer-list', 'private', 'GET', {'cost': 7.5}) + private_get_account_contract_sub_account_v1_transfer_history = privateGetAccountContractSubAccountV1TransferHistory = Entry('account/contract/sub-account/v1/transfer-history', 'private', 'GET', {'cost': 7.5}) + private_get_account_v1_wallet = privateGetAccountV1Wallet = Entry('account/v1/wallet', 'private', 'GET', {'cost': 5}) + private_get_account_v1_currencies = privateGetAccountV1Currencies = Entry('account/v1/currencies', 'private', 'GET', {'cost': 30}) + private_get_spot_v1_wallet = privateGetSpotV1Wallet = Entry('spot/v1/wallet', 'private', 'GET', {'cost': 5}) + private_get_account_v1_deposit_address = privateGetAccountV1DepositAddress = Entry('account/v1/deposit/address', 'private', 'GET', {'cost': 30}) + private_get_account_v1_withdraw_charge = privateGetAccountV1WithdrawCharge = Entry('account/v1/withdraw/charge', 'private', 'GET', {'cost': 32}) + private_get_account_v2_deposit_withdraw_history = privateGetAccountV2DepositWithdrawHistory = Entry('account/v2/deposit-withdraw/history', 'private', 'GET', {'cost': 7.5}) + private_get_account_v1_deposit_withdraw_detail = privateGetAccountV1DepositWithdrawDetail = Entry('account/v1/deposit-withdraw/detail', 'private', 'GET', {'cost': 7.5}) + private_get_account_v1_withdraw_address_list = privateGetAccountV1WithdrawAddressList = Entry('account/v1/withdraw/address/list', 'private', 'GET', {'cost': 30}) + private_get_spot_v1_order_detail = privateGetSpotV1OrderDetail = Entry('spot/v1/order_detail', 'private', 'GET', {'cost': 1}) + private_get_spot_v2_orders = privateGetSpotV2Orders = Entry('spot/v2/orders', 'private', 'GET', {'cost': 5}) + private_get_spot_v1_trades = privateGetSpotV1Trades = Entry('spot/v1/trades', 'private', 'GET', {'cost': 5}) + private_get_spot_v2_trades = privateGetSpotV2Trades = Entry('spot/v2/trades', 'private', 'GET', {'cost': 4}) + private_get_spot_v3_orders = privateGetSpotV3Orders = Entry('spot/v3/orders', 'private', 'GET', {'cost': 5}) + private_get_spot_v2_order_detail = privateGetSpotV2OrderDetail = Entry('spot/v2/order_detail', 'private', 'GET', {'cost': 1}) + private_get_spot_v1_margin_isolated_borrow_record = privateGetSpotV1MarginIsolatedBorrowRecord = Entry('spot/v1/margin/isolated/borrow_record', 'private', 'GET', {'cost': 1}) + private_get_spot_v1_margin_isolated_repay_record = privateGetSpotV1MarginIsolatedRepayRecord = Entry('spot/v1/margin/isolated/repay_record', 'private', 'GET', {'cost': 1}) + private_get_spot_v1_margin_isolated_pairs = privateGetSpotV1MarginIsolatedPairs = Entry('spot/v1/margin/isolated/pairs', 'private', 'GET', {'cost': 30}) + private_get_spot_v1_margin_isolated_account = privateGetSpotV1MarginIsolatedAccount = Entry('spot/v1/margin/isolated/account', 'private', 'GET', {'cost': 5}) + private_get_spot_v1_trade_fee = privateGetSpotV1TradeFee = Entry('spot/v1/trade_fee', 'private', 'GET', {'cost': 30}) + private_get_spot_v1_user_fee = privateGetSpotV1UserFee = Entry('spot/v1/user_fee', 'private', 'GET', {'cost': 30}) + private_get_spot_v1_broker_rebate = privateGetSpotV1BrokerRebate = Entry('spot/v1/broker/rebate', 'private', 'GET', {'cost': 1}) + private_get_contract_private_assets_detail = privateGetContractPrivateAssetsDetail = Entry('contract/private/assets-detail', 'private', 'GET', {'cost': 5}) + private_get_contract_private_order = privateGetContractPrivateOrder = Entry('contract/private/order', 'private', 'GET', {'cost': 1.2}) + private_get_contract_private_order_history = privateGetContractPrivateOrderHistory = Entry('contract/private/order-history', 'private', 'GET', {'cost': 10}) + private_get_contract_private_position = privateGetContractPrivatePosition = Entry('contract/private/position', 'private', 'GET', {'cost': 10}) + private_get_contract_private_position_v2 = privateGetContractPrivatePositionV2 = Entry('contract/private/position-v2', 'private', 'GET', {'cost': 10}) + private_get_contract_private_get_open_orders = privateGetContractPrivateGetOpenOrders = Entry('contract/private/get-open-orders', 'private', 'GET', {'cost': 1.2}) + private_get_contract_private_current_plan_order = privateGetContractPrivateCurrentPlanOrder = Entry('contract/private/current-plan-order', 'private', 'GET', {'cost': 1.2}) + private_get_contract_private_trades = privateGetContractPrivateTrades = Entry('contract/private/trades', 'private', 'GET', {'cost': 10}) + private_get_contract_private_position_risk = privateGetContractPrivatePositionRisk = Entry('contract/private/position-risk', 'private', 'GET', {'cost': 10}) + private_get_contract_private_affilate_rebate_list = privateGetContractPrivateAffilateRebateList = Entry('contract/private/affilate/rebate-list', 'private', 'GET', {'cost': 10}) + private_get_contract_private_affilate_trade_list = privateGetContractPrivateAffilateTradeList = Entry('contract/private/affilate/trade-list', 'private', 'GET', {'cost': 10}) + private_get_contract_private_transaction_history = privateGetContractPrivateTransactionHistory = Entry('contract/private/transaction-history', 'private', 'GET', {'cost': 10}) + private_get_contract_private_get_position_mode = privateGetContractPrivateGetPositionMode = Entry('contract/private/get-position-mode', 'private', 'GET', {'cost': 1}) + private_post_account_sub_account_main_v1_sub_to_main = privatePostAccountSubAccountMainV1SubToMain = Entry('account/sub-account/main/v1/sub-to-main', 'private', 'POST', {'cost': 30}) + private_post_account_sub_account_sub_v1_sub_to_main = privatePostAccountSubAccountSubV1SubToMain = Entry('account/sub-account/sub/v1/sub-to-main', 'private', 'POST', {'cost': 30}) + private_post_account_sub_account_main_v1_main_to_sub = privatePostAccountSubAccountMainV1MainToSub = Entry('account/sub-account/main/v1/main-to-sub', 'private', 'POST', {'cost': 30}) + private_post_account_sub_account_sub_v1_sub_to_sub = privatePostAccountSubAccountSubV1SubToSub = Entry('account/sub-account/sub/v1/sub-to-sub', 'private', 'POST', {'cost': 30}) + private_post_account_sub_account_main_v1_sub_to_sub = privatePostAccountSubAccountMainV1SubToSub = Entry('account/sub-account/main/v1/sub-to-sub', 'private', 'POST', {'cost': 30}) + private_post_account_contract_sub_account_main_v1_sub_to_main = privatePostAccountContractSubAccountMainV1SubToMain = Entry('account/contract/sub-account/main/v1/sub-to-main', 'private', 'POST', {'cost': 7.5}) + private_post_account_contract_sub_account_main_v1_main_to_sub = privatePostAccountContractSubAccountMainV1MainToSub = Entry('account/contract/sub-account/main/v1/main-to-sub', 'private', 'POST', {'cost': 7.5}) + private_post_account_contract_sub_account_sub_v1_sub_to_main = privatePostAccountContractSubAccountSubV1SubToMain = Entry('account/contract/sub-account/sub/v1/sub-to-main', 'private', 'POST', {'cost': 7.5}) + private_post_account_v1_withdraw_apply = privatePostAccountV1WithdrawApply = Entry('account/v1/withdraw/apply', 'private', 'POST', {'cost': 7.5}) + private_post_spot_v1_submit_order = privatePostSpotV1SubmitOrder = Entry('spot/v1/submit_order', 'private', 'POST', {'cost': 1}) + private_post_spot_v1_batch_orders = privatePostSpotV1BatchOrders = Entry('spot/v1/batch_orders', 'private', 'POST', {'cost': 1}) + private_post_spot_v2_cancel_order = privatePostSpotV2CancelOrder = Entry('spot/v2/cancel_order', 'private', 'POST', {'cost': 1}) + private_post_spot_v1_cancel_orders = privatePostSpotV1CancelOrders = Entry('spot/v1/cancel_orders', 'private', 'POST', {'cost': 15}) + private_post_spot_v4_query_order = privatePostSpotV4QueryOrder = Entry('spot/v4/query/order', 'private', 'POST', {'cost': 1}) + private_post_spot_v4_query_client_order = privatePostSpotV4QueryClientOrder = Entry('spot/v4/query/client-order', 'private', 'POST', {'cost': 1}) + private_post_spot_v4_query_open_orders = privatePostSpotV4QueryOpenOrders = Entry('spot/v4/query/open-orders', 'private', 'POST', {'cost': 5}) + private_post_spot_v4_query_history_orders = privatePostSpotV4QueryHistoryOrders = Entry('spot/v4/query/history-orders', 'private', 'POST', {'cost': 5}) + private_post_spot_v4_query_trades = privatePostSpotV4QueryTrades = Entry('spot/v4/query/trades', 'private', 'POST', {'cost': 5}) + private_post_spot_v4_query_order_trades = privatePostSpotV4QueryOrderTrades = Entry('spot/v4/query/order-trades', 'private', 'POST', {'cost': 5}) + private_post_spot_v4_cancel_orders = privatePostSpotV4CancelOrders = Entry('spot/v4/cancel_orders', 'private', 'POST', {'cost': 3}) + private_post_spot_v4_cancel_all = privatePostSpotV4CancelAll = Entry('spot/v4/cancel_all', 'private', 'POST', {'cost': 90}) + private_post_spot_v4_batch_orders = privatePostSpotV4BatchOrders = Entry('spot/v4/batch_orders', 'private', 'POST', {'cost': 3}) + private_post_spot_v3_cancel_order = privatePostSpotV3CancelOrder = Entry('spot/v3/cancel_order', 'private', 'POST', {'cost': 1}) + private_post_spot_v2_batch_orders = privatePostSpotV2BatchOrders = Entry('spot/v2/batch_orders', 'private', 'POST', {'cost': 1}) + private_post_spot_v2_submit_order = privatePostSpotV2SubmitOrder = Entry('spot/v2/submit_order', 'private', 'POST', {'cost': 1}) + private_post_spot_v1_margin_submit_order = privatePostSpotV1MarginSubmitOrder = Entry('spot/v1/margin/submit_order', 'private', 'POST', {'cost': 1.5}) + private_post_spot_v1_margin_isolated_borrow = privatePostSpotV1MarginIsolatedBorrow = Entry('spot/v1/margin/isolated/borrow', 'private', 'POST', {'cost': 30}) + private_post_spot_v1_margin_isolated_repay = privatePostSpotV1MarginIsolatedRepay = Entry('spot/v1/margin/isolated/repay', 'private', 'POST', {'cost': 30}) + private_post_spot_v1_margin_isolated_transfer = privatePostSpotV1MarginIsolatedTransfer = Entry('spot/v1/margin/isolated/transfer', 'private', 'POST', {'cost': 30}) + private_post_account_v1_transfer_contract_list = privatePostAccountV1TransferContractList = Entry('account/v1/transfer-contract-list', 'private', 'POST', {'cost': 60}) + private_post_account_v1_transfer_contract = privatePostAccountV1TransferContract = Entry('account/v1/transfer-contract', 'private', 'POST', {'cost': 60}) + private_post_contract_private_submit_order = privatePostContractPrivateSubmitOrder = Entry('contract/private/submit-order', 'private', 'POST', {'cost': 2.5}) + private_post_contract_private_cancel_order = privatePostContractPrivateCancelOrder = Entry('contract/private/cancel-order', 'private', 'POST', {'cost': 1.5}) + private_post_contract_private_cancel_orders = privatePostContractPrivateCancelOrders = Entry('contract/private/cancel-orders', 'private', 'POST', {'cost': 30}) + private_post_contract_private_submit_plan_order = privatePostContractPrivateSubmitPlanOrder = Entry('contract/private/submit-plan-order', 'private', 'POST', {'cost': 2.5}) + private_post_contract_private_cancel_plan_order = privatePostContractPrivateCancelPlanOrder = Entry('contract/private/cancel-plan-order', 'private', 'POST', {'cost': 1.5}) + private_post_contract_private_submit_leverage = privatePostContractPrivateSubmitLeverage = Entry('contract/private/submit-leverage', 'private', 'POST', {'cost': 2.5}) + private_post_contract_private_submit_tp_sl_order = privatePostContractPrivateSubmitTpSlOrder = Entry('contract/private/submit-tp-sl-order', 'private', 'POST', {'cost': 2.5}) + private_post_contract_private_modify_plan_order = privatePostContractPrivateModifyPlanOrder = Entry('contract/private/modify-plan-order', 'private', 'POST', {'cost': 2.5}) + private_post_contract_private_modify_preset_plan_order = privatePostContractPrivateModifyPresetPlanOrder = Entry('contract/private/modify-preset-plan-order', 'private', 'POST', {'cost': 2.5}) + private_post_contract_private_modify_limit_order = privatePostContractPrivateModifyLimitOrder = Entry('contract/private/modify-limit-order', 'private', 'POST', {'cost': 2.5}) + private_post_contract_private_modify_tp_sl_order = privatePostContractPrivateModifyTpSlOrder = Entry('contract/private/modify-tp-sl-order', 'private', 'POST', {'cost': 2.5}) + private_post_contract_private_submit_trail_order = privatePostContractPrivateSubmitTrailOrder = Entry('contract/private/submit-trail-order', 'private', 'POST', {'cost': 2.5}) + private_post_contract_private_cancel_trail_order = privatePostContractPrivateCancelTrailOrder = Entry('contract/private/cancel-trail-order', 'private', 'POST', {'cost': 1.5}) + private_post_contract_private_set_position_mode = privatePostContractPrivateSetPositionMode = Entry('contract/private/set-position-mode', 'private', 'POST', {'cost': 1}) diff --git a/ccxt/abstract/bitmex.py b/ccxt/abstract/bitmex.py new file mode 100644 index 0000000..8f4d772 --- /dev/null +++ b/ccxt/abstract/bitmex.py @@ -0,0 +1,97 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_announcement = publicGetAnnouncement = Entry('announcement', 'public', 'GET', {'cost': 5}) + public_get_announcement_urgent = publicGetAnnouncementUrgent = Entry('announcement/urgent', 'public', 'GET', {'cost': 5}) + public_get_chat = publicGetChat = Entry('chat', 'public', 'GET', {'cost': 5}) + public_get_chat_channels = publicGetChatChannels = Entry('chat/channels', 'public', 'GET', {'cost': 5}) + public_get_chat_connected = publicGetChatConnected = Entry('chat/connected', 'public', 'GET', {'cost': 5}) + public_get_chat_pinned = publicGetChatPinned = Entry('chat/pinned', 'public', 'GET', {'cost': 5}) + public_get_funding = publicGetFunding = Entry('funding', 'public', 'GET', {'cost': 5}) + public_get_guild = publicGetGuild = Entry('guild', 'public', 'GET', {'cost': 5}) + public_get_instrument = publicGetInstrument = Entry('instrument', 'public', 'GET', {'cost': 5}) + public_get_instrument_active = publicGetInstrumentActive = Entry('instrument/active', 'public', 'GET', {'cost': 5}) + public_get_instrument_activeandindices = publicGetInstrumentActiveAndIndices = Entry('instrument/activeAndIndices', 'public', 'GET', {'cost': 5}) + public_get_instrument_activeintervals = publicGetInstrumentActiveIntervals = Entry('instrument/activeIntervals', 'public', 'GET', {'cost': 5}) + public_get_instrument_compositeindex = publicGetInstrumentCompositeIndex = Entry('instrument/compositeIndex', 'public', 'GET', {'cost': 5}) + public_get_instrument_indices = publicGetInstrumentIndices = Entry('instrument/indices', 'public', 'GET', {'cost': 5}) + public_get_instrument_usdvolume = publicGetInstrumentUsdVolume = Entry('instrument/usdVolume', 'public', 'GET', {'cost': 5}) + public_get_insurance = publicGetInsurance = Entry('insurance', 'public', 'GET', {'cost': 5}) + public_get_leaderboard = publicGetLeaderboard = Entry('leaderboard', 'public', 'GET', {'cost': 5}) + public_get_liquidation = publicGetLiquidation = Entry('liquidation', 'public', 'GET', {'cost': 5}) + public_get_orderbook_l2 = publicGetOrderBookL2 = Entry('orderBook/L2', 'public', 'GET', {'cost': 5}) + public_get_porl_nonce = publicGetPorlNonce = Entry('porl/nonce', 'public', 'GET', {'cost': 5}) + public_get_quote = publicGetQuote = Entry('quote', 'public', 'GET', {'cost': 5}) + public_get_quote_bucketed = publicGetQuoteBucketed = Entry('quote/bucketed', 'public', 'GET', {'cost': 5}) + public_get_schema = publicGetSchema = Entry('schema', 'public', 'GET', {'cost': 5}) + public_get_schema_websockethelp = publicGetSchemaWebsocketHelp = Entry('schema/websocketHelp', 'public', 'GET', {'cost': 5}) + public_get_settlement = publicGetSettlement = Entry('settlement', 'public', 'GET', {'cost': 5}) + public_get_stats = publicGetStats = Entry('stats', 'public', 'GET', {'cost': 5}) + public_get_stats_history = publicGetStatsHistory = Entry('stats/history', 'public', 'GET', {'cost': 5}) + public_get_stats_historyusd = publicGetStatsHistoryUSD = Entry('stats/historyUSD', 'public', 'GET', {'cost': 5}) + public_get_trade = publicGetTrade = Entry('trade', 'public', 'GET', {'cost': 5}) + public_get_trade_bucketed = publicGetTradeBucketed = Entry('trade/bucketed', 'public', 'GET', {'cost': 5}) + public_get_wallet_assets = publicGetWalletAssets = Entry('wallet/assets', 'public', 'GET', {'cost': 5}) + public_get_wallet_networks = publicGetWalletNetworks = Entry('wallet/networks', 'public', 'GET', {'cost': 5}) + private_get_address = privateGetAddress = Entry('address', 'private', 'GET', {'cost': 5}) + private_get_apikey = privateGetApiKey = Entry('apiKey', 'private', 'GET', {'cost': 5}) + private_get_execution = privateGetExecution = Entry('execution', 'private', 'GET', {'cost': 5}) + private_get_execution_tradehistory = privateGetExecutionTradeHistory = Entry('execution/tradeHistory', 'private', 'GET', {'cost': 5}) + private_get_globalnotification = privateGetGlobalNotification = Entry('globalNotification', 'private', 'GET', {'cost': 5}) + private_get_leaderboard_name = privateGetLeaderboardName = Entry('leaderboard/name', 'private', 'GET', {'cost': 5}) + private_get_order = privateGetOrder = Entry('order', 'private', 'GET', {'cost': 5}) + private_get_porl_snapshots = privateGetPorlSnapshots = Entry('porl/snapshots', 'private', 'GET', {'cost': 5}) + private_get_position = privateGetPosition = Entry('position', 'private', 'GET', {'cost': 5}) + private_get_user = privateGetUser = Entry('user', 'private', 'GET', {'cost': 5}) + private_get_user_affiliatestatus = privateGetUserAffiliateStatus = Entry('user/affiliateStatus', 'private', 'GET', {'cost': 5}) + private_get_user_checkreferralcode = privateGetUserCheckReferralCode = Entry('user/checkReferralCode', 'private', 'GET', {'cost': 5}) + private_get_user_commission = privateGetUserCommission = Entry('user/commission', 'private', 'GET', {'cost': 5}) + private_get_user_csa = privateGetUserCsa = Entry('user/csa', 'private', 'GET', {'cost': 5}) + private_get_user_depositaddress = privateGetUserDepositAddress = Entry('user/depositAddress', 'private', 'GET', {'cost': 5}) + private_get_user_executionhistory = privateGetUserExecutionHistory = Entry('user/executionHistory', 'private', 'GET', {'cost': 5}) + private_get_user_getwallettransferaccounts = privateGetUserGetWalletTransferAccounts = Entry('user/getWalletTransferAccounts', 'private', 'GET', {'cost': 5}) + private_get_user_margin = privateGetUserMargin = Entry('user/margin', 'private', 'GET', {'cost': 5}) + private_get_user_quotefillratio = privateGetUserQuoteFillRatio = Entry('user/quoteFillRatio', 'private', 'GET', {'cost': 5}) + private_get_user_quotevalueratio = privateGetUserQuoteValueRatio = Entry('user/quoteValueRatio', 'private', 'GET', {'cost': 5}) + private_get_user_staking = privateGetUserStaking = Entry('user/staking', 'private', 'GET', {'cost': 5}) + private_get_user_staking_instruments = privateGetUserStakingInstruments = Entry('user/staking/instruments', 'private', 'GET', {'cost': 5}) + private_get_user_staking_tiers = privateGetUserStakingTiers = Entry('user/staking/tiers', 'private', 'GET', {'cost': 5}) + private_get_user_tradingvolume = privateGetUserTradingVolume = Entry('user/tradingVolume', 'private', 'GET', {'cost': 5}) + private_get_user_unstakingrequests = privateGetUserUnstakingRequests = Entry('user/unstakingRequests', 'private', 'GET', {'cost': 5}) + private_get_user_wallet = privateGetUserWallet = Entry('user/wallet', 'private', 'GET', {'cost': 5}) + private_get_user_wallethistory = privateGetUserWalletHistory = Entry('user/walletHistory', 'private', 'GET', {'cost': 5}) + private_get_user_walletsummary = privateGetUserWalletSummary = Entry('user/walletSummary', 'private', 'GET', {'cost': 5}) + private_get_useraffiliates = privateGetUserAffiliates = Entry('userAffiliates', 'private', 'GET', {'cost': 5}) + private_get_userevent = privateGetUserEvent = Entry('userEvent', 'private', 'GET', {'cost': 5}) + private_post_address = privatePostAddress = Entry('address', 'private', 'POST', {'cost': 5}) + private_post_chat = privatePostChat = Entry('chat', 'private', 'POST', {'cost': 5}) + private_post_guild = privatePostGuild = Entry('guild', 'private', 'POST', {'cost': 5}) + private_post_guild_archive = privatePostGuildArchive = Entry('guild/archive', 'private', 'POST', {'cost': 5}) + private_post_guild_join = privatePostGuildJoin = Entry('guild/join', 'private', 'POST', {'cost': 5}) + private_post_guild_kick = privatePostGuildKick = Entry('guild/kick', 'private', 'POST', {'cost': 5}) + private_post_guild_leave = privatePostGuildLeave = Entry('guild/leave', 'private', 'POST', {'cost': 5}) + private_post_guild_sharestrades = privatePostGuildSharesTrades = Entry('guild/sharesTrades', 'private', 'POST', {'cost': 5}) + private_post_order = privatePostOrder = Entry('order', 'private', 'POST', {'cost': 1}) + private_post_order_cancelallafter = privatePostOrderCancelAllAfter = Entry('order/cancelAllAfter', 'private', 'POST', {'cost': 5}) + private_post_order_closeposition = privatePostOrderClosePosition = Entry('order/closePosition', 'private', 'POST', {'cost': 5}) + private_post_position_isolate = privatePostPositionIsolate = Entry('position/isolate', 'private', 'POST', {'cost': 1}) + private_post_position_leverage = privatePostPositionLeverage = Entry('position/leverage', 'private', 'POST', {'cost': 1}) + private_post_position_risklimit = privatePostPositionRiskLimit = Entry('position/riskLimit', 'private', 'POST', {'cost': 5}) + private_post_position_transfermargin = privatePostPositionTransferMargin = Entry('position/transferMargin', 'private', 'POST', {'cost': 1}) + private_post_user_addsubaccount = privatePostUserAddSubaccount = Entry('user/addSubaccount', 'private', 'POST', {'cost': 5}) + private_post_user_cancelwithdrawal = privatePostUserCancelWithdrawal = Entry('user/cancelWithdrawal', 'private', 'POST', {'cost': 5}) + private_post_user_communicationtoken = privatePostUserCommunicationToken = Entry('user/communicationToken', 'private', 'POST', {'cost': 5}) + private_post_user_confirmemail = privatePostUserConfirmEmail = Entry('user/confirmEmail', 'private', 'POST', {'cost': 5}) + private_post_user_confirmwithdrawal = privatePostUserConfirmWithdrawal = Entry('user/confirmWithdrawal', 'private', 'POST', {'cost': 5}) + private_post_user_logout = privatePostUserLogout = Entry('user/logout', 'private', 'POST', {'cost': 5}) + private_post_user_preferences = privatePostUserPreferences = Entry('user/preferences', 'private', 'POST', {'cost': 5}) + private_post_user_requestwithdrawal = privatePostUserRequestWithdrawal = Entry('user/requestWithdrawal', 'private', 'POST', {'cost': 5}) + private_post_user_unstakingrequests = privatePostUserUnstakingRequests = Entry('user/unstakingRequests', 'private', 'POST', {'cost': 5}) + private_post_user_updatesubaccount = privatePostUserUpdateSubaccount = Entry('user/updateSubaccount', 'private', 'POST', {'cost': 5}) + private_post_user_wallettransfer = privatePostUserWalletTransfer = Entry('user/walletTransfer', 'private', 'POST', {'cost': 5}) + private_put_guild = privatePutGuild = Entry('guild', 'private', 'PUT', {'cost': 5}) + private_put_order = privatePutOrder = Entry('order', 'private', 'PUT', {'cost': 1}) + private_delete_order = privateDeleteOrder = Entry('order', 'private', 'DELETE', {'cost': 1}) + private_delete_order_all = privateDeleteOrderAll = Entry('order/all', 'private', 'DELETE', {'cost': 1}) + private_delete_user_unstakingrequests = privateDeleteUserUnstakingRequests = Entry('user/unstakingRequests', 'private', 'DELETE', {'cost': 5}) diff --git a/ccxt/abstract/bitopro.py b/ccxt/abstract/bitopro.py new file mode 100644 index 0000000..d0774cb --- /dev/null +++ b/ccxt/abstract/bitopro.py @@ -0,0 +1,30 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_order_book_pair = publicGetOrderBookPair = Entry('order-book/{pair}', 'public', 'GET', {'cost': 1}) + public_get_tickers = publicGetTickers = Entry('tickers', 'public', 'GET', {'cost': 1}) + public_get_tickers_pair = publicGetTickersPair = Entry('tickers/{pair}', 'public', 'GET', {'cost': 1}) + public_get_trades_pair = publicGetTradesPair = Entry('trades/{pair}', 'public', 'GET', {'cost': 1}) + public_get_provisioning_currencies = publicGetProvisioningCurrencies = Entry('provisioning/currencies', 'public', 'GET', {'cost': 1}) + public_get_provisioning_trading_pairs = publicGetProvisioningTradingPairs = Entry('provisioning/trading-pairs', 'public', 'GET', {'cost': 1}) + public_get_provisioning_limitations_and_fees = publicGetProvisioningLimitationsAndFees = Entry('provisioning/limitations-and-fees', 'public', 'GET', {'cost': 1}) + public_get_trading_history_pair = publicGetTradingHistoryPair = Entry('trading-history/{pair}', 'public', 'GET', {'cost': 1}) + public_get_price_otc_currency = publicGetPriceOtcCurrency = Entry('price/otc/{currency}', 'public', 'GET', {'cost': 1}) + private_get_accounts_balance = privateGetAccountsBalance = Entry('accounts/balance', 'private', 'GET', {'cost': 1}) + private_get_orders_history = privateGetOrdersHistory = Entry('orders/history', 'private', 'GET', {'cost': 1}) + private_get_orders_all_pair = privateGetOrdersAllPair = Entry('orders/all/{pair}', 'private', 'GET', {'cost': 1}) + private_get_orders_trades_pair = privateGetOrdersTradesPair = Entry('orders/trades/{pair}', 'private', 'GET', {'cost': 1}) + private_get_orders_pair_orderid = privateGetOrdersPairOrderId = Entry('orders/{pair}/{orderId}', 'private', 'GET', {'cost': 1}) + private_get_wallet_withdraw_currency_serial = privateGetWalletWithdrawCurrencySerial = Entry('wallet/withdraw/{currency}/{serial}', 'private', 'GET', {'cost': 1}) + private_get_wallet_withdraw_currency_id_id = privateGetWalletWithdrawCurrencyIdId = Entry('wallet/withdraw/{currency}/id/{id}', 'private', 'GET', {'cost': 1}) + private_get_wallet_deposithistory_currency = privateGetWalletDepositHistoryCurrency = Entry('wallet/depositHistory/{currency}', 'private', 'GET', {'cost': 1}) + private_get_wallet_withdrawhistory_currency = privateGetWalletWithdrawHistoryCurrency = Entry('wallet/withdrawHistory/{currency}', 'private', 'GET', {'cost': 1}) + private_get_orders_open = privateGetOrdersOpen = Entry('orders/open', 'private', 'GET', {'cost': 1}) + private_post_orders_pair = privatePostOrdersPair = Entry('orders/{pair}', 'private', 'POST', {'cost': 0.5}) + private_post_orders_batch = privatePostOrdersBatch = Entry('orders/batch', 'private', 'POST', {'cost': 6.666666666666667}) + private_post_wallet_withdraw_currency = privatePostWalletWithdrawCurrency = Entry('wallet/withdraw/{currency}', 'private', 'POST', {'cost': 10}) + private_put_orders = privatePutOrders = Entry('orders', 'private', 'PUT', {'cost': 5}) + private_delete_orders_pair_id = privateDeleteOrdersPairId = Entry('orders/{pair}/{id}', 'private', 'DELETE', {'cost': 0.6666666666666666}) + private_delete_orders_all = privateDeleteOrdersAll = Entry('orders/all', 'private', 'DELETE', {'cost': 5}) + private_delete_orders_pair = privateDeleteOrdersPair = Entry('orders/{pair}', 'private', 'DELETE', {'cost': 5}) diff --git a/ccxt/abstract/bitrue.py b/ccxt/abstract/bitrue.py new file mode 100644 index 0000000..72ff488 --- /dev/null +++ b/ccxt/abstract/bitrue.py @@ -0,0 +1,72 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + spot_kline_public_get_public_json = spotKlinePublicGetPublicJson = Entry('public.json', ['spot', 'kline', 'public'], 'GET', {'cost': 0.24}) + spot_kline_public_get_public_currency_json = spotKlinePublicGetPublicCurrencyJson = Entry('public{currency}.json', ['spot', 'kline', 'public'], 'GET', {'cost': 0.24}) + spot_v1_public_get_ping = spotV1PublicGetPing = Entry('ping', ['spot', 'v1', 'public'], 'GET', {'cost': 0.24}) + spot_v1_public_get_time = spotV1PublicGetTime = Entry('time', ['spot', 'v1', 'public'], 'GET', {'cost': 0.24}) + spot_v1_public_get_exchangeinfo = spotV1PublicGetExchangeInfo = Entry('exchangeInfo', ['spot', 'v1', 'public'], 'GET', {'cost': 0.24}) + spot_v1_public_get_depth = spotV1PublicGetDepth = Entry('depth', ['spot', 'v1', 'public'], 'GET', {'cost': 1, 'byLimit': [[100, 0.24], [500, 1.2], [1000, 2.4]]}) + spot_v1_public_get_trades = spotV1PublicGetTrades = Entry('trades', ['spot', 'v1', 'public'], 'GET', {'cost': 0.24}) + spot_v1_public_get_historicaltrades = spotV1PublicGetHistoricalTrades = Entry('historicalTrades', ['spot', 'v1', 'public'], 'GET', {'cost': 1.2}) + spot_v1_public_get_aggtrades = spotV1PublicGetAggTrades = Entry('aggTrades', ['spot', 'v1', 'public'], 'GET', {'cost': 0.24}) + spot_v1_public_get_ticker_24hr = spotV1PublicGetTicker24hr = Entry('ticker/24hr', ['spot', 'v1', 'public'], 'GET', {'cost': 0.24, 'noSymbol': 9.6}) + spot_v1_public_get_ticker_price = spotV1PublicGetTickerPrice = Entry('ticker/price', ['spot', 'v1', 'public'], 'GET', {'cost': 0.24}) + spot_v1_public_get_ticker_bookticker = spotV1PublicGetTickerBookTicker = Entry('ticker/bookTicker', ['spot', 'v1', 'public'], 'GET', {'cost': 0.24}) + spot_v1_public_get_market_kline = spotV1PublicGetMarketKline = Entry('market/kline', ['spot', 'v1', 'public'], 'GET', {'cost': 0.24}) + spot_v1_private_get_order = spotV1PrivateGetOrder = Entry('order', ['spot', 'v1', 'private'], 'GET', {'cost': 5}) + spot_v1_private_get_openorders = spotV1PrivateGetOpenOrders = Entry('openOrders', ['spot', 'v1', 'private'], 'GET', {'cost': 5}) + spot_v1_private_get_allorders = spotV1PrivateGetAllOrders = Entry('allOrders', ['spot', 'v1', 'private'], 'GET', {'cost': 25}) + spot_v1_private_get_account = spotV1PrivateGetAccount = Entry('account', ['spot', 'v1', 'private'], 'GET', {'cost': 25}) + spot_v1_private_get_mytrades = spotV1PrivateGetMyTrades = Entry('myTrades', ['spot', 'v1', 'private'], 'GET', {'cost': 25}) + spot_v1_private_get_etf_net_value_symbol = spotV1PrivateGetEtfNetValueSymbol = Entry('etf/net-value/{symbol}', ['spot', 'v1', 'private'], 'GET', {'cost': 0.24}) + spot_v1_private_get_withdraw_history = spotV1PrivateGetWithdrawHistory = Entry('withdraw/history', ['spot', 'v1', 'private'], 'GET', {'cost': 120}) + spot_v1_private_get_deposit_history = spotV1PrivateGetDepositHistory = Entry('deposit/history', ['spot', 'v1', 'private'], 'GET', {'cost': 120}) + spot_v1_private_post_order = spotV1PrivatePostOrder = Entry('order', ['spot', 'v1', 'private'], 'POST', {'cost': 5}) + spot_v1_private_post_withdraw_commit = spotV1PrivatePostWithdrawCommit = Entry('withdraw/commit', ['spot', 'v1', 'private'], 'POST', {'cost': 120}) + spot_v1_private_delete_order = spotV1PrivateDeleteOrder = Entry('order', ['spot', 'v1', 'private'], 'DELETE', {'cost': 5}) + spot_v2_private_get_mytrades = spotV2PrivateGetMyTrades = Entry('myTrades', ['spot', 'v2', 'private'], 'GET', {'cost': 1.2}) + fapi_v1_public_get_ping = fapiV1PublicGetPing = Entry('ping', ['fapi', 'v1', 'public'], 'GET', {'cost': 0.24}) + fapi_v1_public_get_time = fapiV1PublicGetTime = Entry('time', ['fapi', 'v1', 'public'], 'GET', {'cost': 0.24}) + fapi_v1_public_get_contracts = fapiV1PublicGetContracts = Entry('contracts', ['fapi', 'v1', 'public'], 'GET', {'cost': 0.24}) + fapi_v1_public_get_depth = fapiV1PublicGetDepth = Entry('depth', ['fapi', 'v1', 'public'], 'GET', {'cost': 0.24}) + fapi_v1_public_get_ticker = fapiV1PublicGetTicker = Entry('ticker', ['fapi', 'v1', 'public'], 'GET', {'cost': 0.24}) + fapi_v1_public_get_klines = fapiV1PublicGetKlines = Entry('klines', ['fapi', 'v1', 'public'], 'GET', {'cost': 0.24}) + fapi_v2_private_get_mytrades = fapiV2PrivateGetMyTrades = Entry('myTrades', ['fapi', 'v2', 'private'], 'GET', {'cost': 5}) + fapi_v2_private_get_openorders = fapiV2PrivateGetOpenOrders = Entry('openOrders', ['fapi', 'v2', 'private'], 'GET', {'cost': 5}) + fapi_v2_private_get_order = fapiV2PrivateGetOrder = Entry('order', ['fapi', 'v2', 'private'], 'GET', {'cost': 5}) + fapi_v2_private_get_account = fapiV2PrivateGetAccount = Entry('account', ['fapi', 'v2', 'private'], 'GET', {'cost': 5}) + fapi_v2_private_get_leveragebracket = fapiV2PrivateGetLeverageBracket = Entry('leverageBracket', ['fapi', 'v2', 'private'], 'GET', {'cost': 5}) + fapi_v2_private_get_commissionrate = fapiV2PrivateGetCommissionRate = Entry('commissionRate', ['fapi', 'v2', 'private'], 'GET', {'cost': 5}) + fapi_v2_private_get_futures_transfer_history = fapiV2PrivateGetFuturesTransferHistory = Entry('futures_transfer_history', ['fapi', 'v2', 'private'], 'GET', {'cost': 5}) + fapi_v2_private_get_forceordershistory = fapiV2PrivateGetForceOrdersHistory = Entry('forceOrdersHistory', ['fapi', 'v2', 'private'], 'GET', {'cost': 5}) + fapi_v2_private_post_positionmargin = fapiV2PrivatePostPositionMargin = Entry('positionMargin', ['fapi', 'v2', 'private'], 'POST', {'cost': 5}) + fapi_v2_private_post_level_edit = fapiV2PrivatePostLevelEdit = Entry('level_edit', ['fapi', 'v2', 'private'], 'POST', {'cost': 5}) + fapi_v2_private_post_cancel = fapiV2PrivatePostCancel = Entry('cancel', ['fapi', 'v2', 'private'], 'POST', {'cost': 5}) + fapi_v2_private_post_order = fapiV2PrivatePostOrder = Entry('order', ['fapi', 'v2', 'private'], 'POST', {'cost': 25}) + fapi_v2_private_post_allopenorders = fapiV2PrivatePostAllOpenOrders = Entry('allOpenOrders', ['fapi', 'v2', 'private'], 'POST', {'cost': 5}) + fapi_v2_private_post_futures_transfer = fapiV2PrivatePostFuturesTransfer = Entry('futures_transfer', ['fapi', 'v2', 'private'], 'POST', {'cost': 5}) + dapi_v1_public_get_ping = dapiV1PublicGetPing = Entry('ping', ['dapi', 'v1', 'public'], 'GET', {'cost': 0.24}) + dapi_v1_public_get_time = dapiV1PublicGetTime = Entry('time', ['dapi', 'v1', 'public'], 'GET', {'cost': 0.24}) + dapi_v1_public_get_contracts = dapiV1PublicGetContracts = Entry('contracts', ['dapi', 'v1', 'public'], 'GET', {'cost': 0.24}) + dapi_v1_public_get_depth = dapiV1PublicGetDepth = Entry('depth', ['dapi', 'v1', 'public'], 'GET', {'cost': 0.24}) + dapi_v1_public_get_ticker = dapiV1PublicGetTicker = Entry('ticker', ['dapi', 'v1', 'public'], 'GET', {'cost': 0.24}) + dapi_v1_public_get_klines = dapiV1PublicGetKlines = Entry('klines', ['dapi', 'v1', 'public'], 'GET', {'cost': 0.24}) + dapi_v2_private_get_mytrades = dapiV2PrivateGetMyTrades = Entry('myTrades', ['dapi', 'v2', 'private'], 'GET', {'cost': 5}) + dapi_v2_private_get_openorders = dapiV2PrivateGetOpenOrders = Entry('openOrders', ['dapi', 'v2', 'private'], 'GET', {'cost': 5}) + dapi_v2_private_get_order = dapiV2PrivateGetOrder = Entry('order', ['dapi', 'v2', 'private'], 'GET', {'cost': 5}) + dapi_v2_private_get_account = dapiV2PrivateGetAccount = Entry('account', ['dapi', 'v2', 'private'], 'GET', {'cost': 5}) + dapi_v2_private_get_leveragebracket = dapiV2PrivateGetLeverageBracket = Entry('leverageBracket', ['dapi', 'v2', 'private'], 'GET', {'cost': 5}) + dapi_v2_private_get_commissionrate = dapiV2PrivateGetCommissionRate = Entry('commissionRate', ['dapi', 'v2', 'private'], 'GET', {'cost': 5}) + dapi_v2_private_get_futures_transfer_history = dapiV2PrivateGetFuturesTransferHistory = Entry('futures_transfer_history', ['dapi', 'v2', 'private'], 'GET', {'cost': 5}) + dapi_v2_private_get_forceordershistory = dapiV2PrivateGetForceOrdersHistory = Entry('forceOrdersHistory', ['dapi', 'v2', 'private'], 'GET', {'cost': 5}) + dapi_v2_private_post_positionmargin = dapiV2PrivatePostPositionMargin = Entry('positionMargin', ['dapi', 'v2', 'private'], 'POST', {'cost': 5}) + dapi_v2_private_post_level_edit = dapiV2PrivatePostLevelEdit = Entry('level_edit', ['dapi', 'v2', 'private'], 'POST', {'cost': 5}) + dapi_v2_private_post_cancel = dapiV2PrivatePostCancel = Entry('cancel', ['dapi', 'v2', 'private'], 'POST', {'cost': 5}) + dapi_v2_private_post_order = dapiV2PrivatePostOrder = Entry('order', ['dapi', 'v2', 'private'], 'POST', {'cost': 5}) + dapi_v2_private_post_allopenorders = dapiV2PrivatePostAllOpenOrders = Entry('allOpenOrders', ['dapi', 'v2', 'private'], 'POST', {'cost': 5}) + dapi_v2_private_post_futures_transfer = dapiV2PrivatePostFuturesTransfer = Entry('futures_transfer', ['dapi', 'v2', 'private'], 'POST', {'cost': 5}) + open_v1_private_post_poseidon_api_v1_listenkey = openV1PrivatePostPoseidonApiV1ListenKey = Entry('poseidon/api/v1/listenKey', ['open', 'v1', 'private'], 'POST', {'cost': 1}) + open_v1_private_put_poseidon_api_v1_listenkey_listenkey = openV1PrivatePutPoseidonApiV1ListenKeyListenKey = Entry('poseidon/api/v1/listenKey/{listenKey}', ['open', 'v1', 'private'], 'PUT', {'cost': 1}) + open_v1_private_delete_poseidon_api_v1_listenkey_listenkey = openV1PrivateDeletePoseidonApiV1ListenKeyListenKey = Entry('poseidon/api/v1/listenKey/{listenKey}', ['open', 'v1', 'private'], 'DELETE', {'cost': 1}) diff --git a/ccxt/abstract/bitso.py b/ccxt/abstract/bitso.py new file mode 100644 index 0000000..eb195f6 --- /dev/null +++ b/ccxt/abstract/bitso.py @@ -0,0 +1,43 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_available_books = publicGetAvailableBooks = Entry('available_books', 'public', 'GET', {}) + public_get_ticker = publicGetTicker = Entry('ticker', 'public', 'GET', {}) + public_get_order_book = publicGetOrderBook = Entry('order_book', 'public', 'GET', {}) + public_get_trades = publicGetTrades = Entry('trades', 'public', 'GET', {}) + public_get_ohlc = publicGetOhlc = Entry('ohlc', 'public', 'GET', {}) + private_get_account_status = privateGetAccountStatus = Entry('account_status', 'private', 'GET', {}) + private_get_balance = privateGetBalance = Entry('balance', 'private', 'GET', {}) + private_get_fees = privateGetFees = Entry('fees', 'private', 'GET', {}) + private_get_fundings = privateGetFundings = Entry('fundings', 'private', 'GET', {}) + private_get_fundings_fid = privateGetFundingsFid = Entry('fundings/{fid}', 'private', 'GET', {}) + private_get_funding_destination = privateGetFundingDestination = Entry('funding_destination', 'private', 'GET', {}) + private_get_kyc_documents = privateGetKycDocuments = Entry('kyc_documents', 'private', 'GET', {}) + private_get_ledger = privateGetLedger = Entry('ledger', 'private', 'GET', {}) + private_get_ledger_trades = privateGetLedgerTrades = Entry('ledger/trades', 'private', 'GET', {}) + private_get_ledger_fees = privateGetLedgerFees = Entry('ledger/fees', 'private', 'GET', {}) + private_get_ledger_fundings = privateGetLedgerFundings = Entry('ledger/fundings', 'private', 'GET', {}) + private_get_ledger_withdrawals = privateGetLedgerWithdrawals = Entry('ledger/withdrawals', 'private', 'GET', {}) + private_get_mx_bank_codes = privateGetMxBankCodes = Entry('mx_bank_codes', 'private', 'GET', {}) + private_get_open_orders = privateGetOpenOrders = Entry('open_orders', 'private', 'GET', {}) + private_get_order_trades_oid = privateGetOrderTradesOid = Entry('order_trades/{oid}', 'private', 'GET', {}) + private_get_orders_oid = privateGetOrdersOid = Entry('orders/{oid}', 'private', 'GET', {}) + private_get_user_trades = privateGetUserTrades = Entry('user_trades', 'private', 'GET', {}) + private_get_user_trades_tid = privateGetUserTradesTid = Entry('user_trades/{tid}', 'private', 'GET', {}) + private_get_withdrawals = privateGetWithdrawals = Entry('withdrawals/', 'private', 'GET', {}) + private_get_withdrawals_wid = privateGetWithdrawalsWid = Entry('withdrawals/{wid}', 'private', 'GET', {}) + private_post_bitcoin_withdrawal = privatePostBitcoinWithdrawal = Entry('bitcoin_withdrawal', 'private', 'POST', {}) + private_post_debit_card_withdrawal = privatePostDebitCardWithdrawal = Entry('debit_card_withdrawal', 'private', 'POST', {}) + private_post_ether_withdrawal = privatePostEtherWithdrawal = Entry('ether_withdrawal', 'private', 'POST', {}) + private_post_orders = privatePostOrders = Entry('orders', 'private', 'POST', {}) + private_post_phone_number = privatePostPhoneNumber = Entry('phone_number', 'private', 'POST', {}) + private_post_phone_verification = privatePostPhoneVerification = Entry('phone_verification', 'private', 'POST', {}) + private_post_phone_withdrawal = privatePostPhoneWithdrawal = Entry('phone_withdrawal', 'private', 'POST', {}) + private_post_spei_withdrawal = privatePostSpeiWithdrawal = Entry('spei_withdrawal', 'private', 'POST', {}) + private_post_ripple_withdrawal = privatePostRippleWithdrawal = Entry('ripple_withdrawal', 'private', 'POST', {}) + private_post_bcash_withdrawal = privatePostBcashWithdrawal = Entry('bcash_withdrawal', 'private', 'POST', {}) + private_post_litecoin_withdrawal = privatePostLitecoinWithdrawal = Entry('litecoin_withdrawal', 'private', 'POST', {}) + private_delete_orders = privateDeleteOrders = Entry('orders', 'private', 'DELETE', {}) + private_delete_orders_oid = privateDeleteOrdersOid = Entry('orders/{oid}', 'private', 'DELETE', {}) + private_delete_orders_all = privateDeleteOrdersAll = Entry('orders/all', 'private', 'DELETE', {}) diff --git a/ccxt/abstract/bitstamp.py b/ccxt/abstract/bitstamp.py new file mode 100644 index 0000000..d1e5022 --- /dev/null +++ b/ccxt/abstract/bitstamp.py @@ -0,0 +1,259 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_ohlc_pair = publicGetOhlcPair = Entry('ohlc/{pair}/', 'public', 'GET', {'cost': 1}) + public_get_order_book_pair = publicGetOrderBookPair = Entry('order_book/{pair}/', 'public', 'GET', {'cost': 1}) + public_get_ticker = publicGetTicker = Entry('ticker/', 'public', 'GET', {'cost': 1}) + public_get_ticker_hour_pair = publicGetTickerHourPair = Entry('ticker_hour/{pair}/', 'public', 'GET', {'cost': 1}) + public_get_ticker_pair = publicGetTickerPair = Entry('ticker/{pair}/', 'public', 'GET', {'cost': 1}) + public_get_transactions_pair = publicGetTransactionsPair = Entry('transactions/{pair}/', 'public', 'GET', {'cost': 1}) + public_get_trading_pairs_info = publicGetTradingPairsInfo = Entry('trading-pairs-info/', 'public', 'GET', {'cost': 1}) + public_get_currencies = publicGetCurrencies = Entry('currencies/', 'public', 'GET', {'cost': 1}) + public_get_eur_usd = publicGetEurUsd = Entry('eur_usd/', 'public', 'GET', {'cost': 1}) + public_get_travel_rule_vasps = publicGetTravelRuleVasps = Entry('travel_rule/vasps/', 'public', 'GET', {'cost': 1}) + private_get_travel_rule_contacts = privateGetTravelRuleContacts = Entry('travel_rule/contacts/', 'private', 'GET', {'cost': 1}) + private_get_contacts_contact_uuid = privateGetContactsContactUuid = Entry('contacts/{contact_uuid}/', 'private', 'GET', {'cost': 1}) + private_get_earn_subscriptions = privateGetEarnSubscriptions = Entry('earn/subscriptions/', 'private', 'GET', {'cost': 1}) + private_get_earn_transactions = privateGetEarnTransactions = Entry('earn/transactions/', 'private', 'GET', {'cost': 1}) + private_post_account_balances = privatePostAccountBalances = Entry('account_balances/', 'private', 'POST', {'cost': 1}) + private_post_account_balances_currency = privatePostAccountBalancesCurrency = Entry('account_balances/{currency}/', 'private', 'POST', {'cost': 1}) + private_post_balance = privatePostBalance = Entry('balance/', 'private', 'POST', {'cost': 1}) + private_post_balance_pair = privatePostBalancePair = Entry('balance/{pair}/', 'private', 'POST', {'cost': 1}) + private_post_bch_withdrawal = privatePostBchWithdrawal = Entry('bch_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_bch_address = privatePostBchAddress = Entry('bch_address/', 'private', 'POST', {'cost': 1}) + private_post_user_transactions = privatePostUserTransactions = Entry('user_transactions/', 'private', 'POST', {'cost': 1}) + private_post_user_transactions_pair = privatePostUserTransactionsPair = Entry('user_transactions/{pair}/', 'private', 'POST', {'cost': 1}) + private_post_crypto_transactions = privatePostCryptoTransactions = Entry('crypto-transactions/', 'private', 'POST', {'cost': 1}) + private_post_open_order = privatePostOpenOrder = Entry('open_order', 'private', 'POST', {'cost': 1}) + private_post_open_orders_all = privatePostOpenOrdersAll = Entry('open_orders/all/', 'private', 'POST', {'cost': 1}) + private_post_open_orders_pair = privatePostOpenOrdersPair = Entry('open_orders/{pair}/', 'private', 'POST', {'cost': 1}) + private_post_order_status = privatePostOrderStatus = Entry('order_status/', 'private', 'POST', {'cost': 1}) + private_post_cancel_order = privatePostCancelOrder = Entry('cancel_order/', 'private', 'POST', {'cost': 1}) + private_post_cancel_all_orders = privatePostCancelAllOrders = Entry('cancel_all_orders/', 'private', 'POST', {'cost': 1}) + private_post_cancel_all_orders_pair = privatePostCancelAllOrdersPair = Entry('cancel_all_orders/{pair}/', 'private', 'POST', {'cost': 1}) + private_post_buy_pair = privatePostBuyPair = Entry('buy/{pair}/', 'private', 'POST', {'cost': 1}) + private_post_buy_market_pair = privatePostBuyMarketPair = Entry('buy/market/{pair}/', 'private', 'POST', {'cost': 1}) + private_post_buy_instant_pair = privatePostBuyInstantPair = Entry('buy/instant/{pair}/', 'private', 'POST', {'cost': 1}) + private_post_sell_pair = privatePostSellPair = Entry('sell/{pair}/', 'private', 'POST', {'cost': 1}) + private_post_sell_market_pair = privatePostSellMarketPair = Entry('sell/market/{pair}/', 'private', 'POST', {'cost': 1}) + private_post_sell_instant_pair = privatePostSellInstantPair = Entry('sell/instant/{pair}/', 'private', 'POST', {'cost': 1}) + private_post_transfer_to_main = privatePostTransferToMain = Entry('transfer-to-main/', 'private', 'POST', {'cost': 1}) + private_post_transfer_from_main = privatePostTransferFromMain = Entry('transfer-from-main/', 'private', 'POST', {'cost': 1}) + private_post_my_trading_pairs = privatePostMyTradingPairs = Entry('my_trading_pairs/', 'private', 'POST', {'cost': 1}) + private_post_fees_trading = privatePostFeesTrading = Entry('fees/trading/', 'private', 'POST', {'cost': 1}) + private_post_fees_trading_market_symbol = privatePostFeesTradingMarketSymbol = Entry('fees/trading/{market_symbol}', 'private', 'POST', {'cost': 1}) + private_post_fees_withdrawal = privatePostFeesWithdrawal = Entry('fees/withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_fees_withdrawal_currency = privatePostFeesWithdrawalCurrency = Entry('fees/withdrawal/{currency}/', 'private', 'POST', {'cost': 1}) + private_post_withdrawal_requests = privatePostWithdrawalRequests = Entry('withdrawal-requests/', 'private', 'POST', {'cost': 1}) + private_post_withdrawal_open = privatePostWithdrawalOpen = Entry('withdrawal/open/', 'private', 'POST', {'cost': 1}) + private_post_withdrawal_status = privatePostWithdrawalStatus = Entry('withdrawal/status/', 'private', 'POST', {'cost': 1}) + private_post_withdrawal_cancel = privatePostWithdrawalCancel = Entry('withdrawal/cancel/', 'private', 'POST', {'cost': 1}) + private_post_liquidation_address_new = privatePostLiquidationAddressNew = Entry('liquidation_address/new/', 'private', 'POST', {'cost': 1}) + private_post_liquidation_address_info = privatePostLiquidationAddressInfo = Entry('liquidation_address/info/', 'private', 'POST', {'cost': 1}) + private_post_btc_unconfirmed = privatePostBtcUnconfirmed = Entry('btc_unconfirmed/', 'private', 'POST', {'cost': 1}) + private_post_websockets_token = privatePostWebsocketsToken = Entry('websockets_token/', 'private', 'POST', {'cost': 1}) + private_post_btc_withdrawal = privatePostBtcWithdrawal = Entry('btc_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_btc_address = privatePostBtcAddress = Entry('btc_address/', 'private', 'POST', {'cost': 1}) + private_post_ripple_withdrawal = privatePostRippleWithdrawal = Entry('ripple_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_ripple_address = privatePostRippleAddress = Entry('ripple_address/', 'private', 'POST', {'cost': 1}) + private_post_ltc_withdrawal = privatePostLtcWithdrawal = Entry('ltc_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_ltc_address = privatePostLtcAddress = Entry('ltc_address/', 'private', 'POST', {'cost': 1}) + private_post_eth_withdrawal = privatePostEthWithdrawal = Entry('eth_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_eth_address = privatePostEthAddress = Entry('eth_address/', 'private', 'POST', {'cost': 1}) + private_post_xrp_withdrawal = privatePostXrpWithdrawal = Entry('xrp_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_xrp_address = privatePostXrpAddress = Entry('xrp_address/', 'private', 'POST', {'cost': 1}) + private_post_xlm_withdrawal = privatePostXlmWithdrawal = Entry('xlm_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_xlm_address = privatePostXlmAddress = Entry('xlm_address/', 'private', 'POST', {'cost': 1}) + private_post_pax_withdrawal = privatePostPaxWithdrawal = Entry('pax_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_pax_address = privatePostPaxAddress = Entry('pax_address/', 'private', 'POST', {'cost': 1}) + private_post_link_withdrawal = privatePostLinkWithdrawal = Entry('link_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_link_address = privatePostLinkAddress = Entry('link_address/', 'private', 'POST', {'cost': 1}) + private_post_usdc_withdrawal = privatePostUsdcWithdrawal = Entry('usdc_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_usdc_address = privatePostUsdcAddress = Entry('usdc_address/', 'private', 'POST', {'cost': 1}) + private_post_omg_withdrawal = privatePostOmgWithdrawal = Entry('omg_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_omg_address = privatePostOmgAddress = Entry('omg_address/', 'private', 'POST', {'cost': 1}) + private_post_dai_withdrawal = privatePostDaiWithdrawal = Entry('dai_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_dai_address = privatePostDaiAddress = Entry('dai_address/', 'private', 'POST', {'cost': 1}) + private_post_knc_withdrawal = privatePostKncWithdrawal = Entry('knc_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_knc_address = privatePostKncAddress = Entry('knc_address/', 'private', 'POST', {'cost': 1}) + private_post_mkr_withdrawal = privatePostMkrWithdrawal = Entry('mkr_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_mkr_address = privatePostMkrAddress = Entry('mkr_address/', 'private', 'POST', {'cost': 1}) + private_post_zrx_withdrawal = privatePostZrxWithdrawal = Entry('zrx_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_zrx_address = privatePostZrxAddress = Entry('zrx_address/', 'private', 'POST', {'cost': 1}) + private_post_gusd_withdrawal = privatePostGusdWithdrawal = Entry('gusd_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_gusd_address = privatePostGusdAddress = Entry('gusd_address/', 'private', 'POST', {'cost': 1}) + private_post_aave_withdrawal = privatePostAaveWithdrawal = Entry('aave_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_aave_address = privatePostAaveAddress = Entry('aave_address/', 'private', 'POST', {'cost': 1}) + private_post_bat_withdrawal = privatePostBatWithdrawal = Entry('bat_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_bat_address = privatePostBatAddress = Entry('bat_address/', 'private', 'POST', {'cost': 1}) + private_post_uma_withdrawal = privatePostUmaWithdrawal = Entry('uma_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_uma_address = privatePostUmaAddress = Entry('uma_address/', 'private', 'POST', {'cost': 1}) + private_post_snx_withdrawal = privatePostSnxWithdrawal = Entry('snx_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_snx_address = privatePostSnxAddress = Entry('snx_address/', 'private', 'POST', {'cost': 1}) + private_post_uni_withdrawal = privatePostUniWithdrawal = Entry('uni_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_uni_address = privatePostUniAddress = Entry('uni_address/', 'private', 'POST', {'cost': 1}) + private_post_yfi_withdrawal = privatePostYfiWithdrawal = Entry('yfi_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_yfi_address = privatePostYfiAddress = Entry('yfi_address/', 'private', 'POST', {'cost': 1}) + private_post_audio_withdrawal = privatePostAudioWithdrawal = Entry('audio_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_audio_address = privatePostAudioAddress = Entry('audio_address/', 'private', 'POST', {'cost': 1}) + private_post_crv_withdrawal = privatePostCrvWithdrawal = Entry('crv_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_crv_address = privatePostCrvAddress = Entry('crv_address/', 'private', 'POST', {'cost': 1}) + private_post_algo_withdrawal = privatePostAlgoWithdrawal = Entry('algo_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_algo_address = privatePostAlgoAddress = Entry('algo_address/', 'private', 'POST', {'cost': 1}) + private_post_comp_withdrawal = privatePostCompWithdrawal = Entry('comp_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_comp_address = privatePostCompAddress = Entry('comp_address/', 'private', 'POST', {'cost': 1}) + private_post_grt_withdrawal = privatePostGrtWithdrawal = Entry('grt_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_grt_address = privatePostGrtAddress = Entry('grt_address/', 'private', 'POST', {'cost': 1}) + private_post_usdt_withdrawal = privatePostUsdtWithdrawal = Entry('usdt_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_usdt_address = privatePostUsdtAddress = Entry('usdt_address/', 'private', 'POST', {'cost': 1}) + private_post_eurt_withdrawal = privatePostEurtWithdrawal = Entry('eurt_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_eurt_address = privatePostEurtAddress = Entry('eurt_address/', 'private', 'POST', {'cost': 1}) + private_post_matic_withdrawal = privatePostMaticWithdrawal = Entry('matic_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_matic_address = privatePostMaticAddress = Entry('matic_address/', 'private', 'POST', {'cost': 1}) + private_post_sushi_withdrawal = privatePostSushiWithdrawal = Entry('sushi_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_sushi_address = privatePostSushiAddress = Entry('sushi_address/', 'private', 'POST', {'cost': 1}) + private_post_chz_withdrawal = privatePostChzWithdrawal = Entry('chz_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_chz_address = privatePostChzAddress = Entry('chz_address/', 'private', 'POST', {'cost': 1}) + private_post_enj_withdrawal = privatePostEnjWithdrawal = Entry('enj_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_enj_address = privatePostEnjAddress = Entry('enj_address/', 'private', 'POST', {'cost': 1}) + private_post_alpha_withdrawal = privatePostAlphaWithdrawal = Entry('alpha_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_alpha_address = privatePostAlphaAddress = Entry('alpha_address/', 'private', 'POST', {'cost': 1}) + private_post_ftt_withdrawal = privatePostFttWithdrawal = Entry('ftt_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_ftt_address = privatePostFttAddress = Entry('ftt_address/', 'private', 'POST', {'cost': 1}) + private_post_storj_withdrawal = privatePostStorjWithdrawal = Entry('storj_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_storj_address = privatePostStorjAddress = Entry('storj_address/', 'private', 'POST', {'cost': 1}) + private_post_axs_withdrawal = privatePostAxsWithdrawal = Entry('axs_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_axs_address = privatePostAxsAddress = Entry('axs_address/', 'private', 'POST', {'cost': 1}) + private_post_sand_withdrawal = privatePostSandWithdrawal = Entry('sand_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_sand_address = privatePostSandAddress = Entry('sand_address/', 'private', 'POST', {'cost': 1}) + private_post_hbar_withdrawal = privatePostHbarWithdrawal = Entry('hbar_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_hbar_address = privatePostHbarAddress = Entry('hbar_address/', 'private', 'POST', {'cost': 1}) + private_post_rgt_withdrawal = privatePostRgtWithdrawal = Entry('rgt_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_rgt_address = privatePostRgtAddress = Entry('rgt_address/', 'private', 'POST', {'cost': 1}) + private_post_fet_withdrawal = privatePostFetWithdrawal = Entry('fet_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_fet_address = privatePostFetAddress = Entry('fet_address/', 'private', 'POST', {'cost': 1}) + private_post_skl_withdrawal = privatePostSklWithdrawal = Entry('skl_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_skl_address = privatePostSklAddress = Entry('skl_address/', 'private', 'POST', {'cost': 1}) + private_post_cel_withdrawal = privatePostCelWithdrawal = Entry('cel_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_cel_address = privatePostCelAddress = Entry('cel_address/', 'private', 'POST', {'cost': 1}) + private_post_sxp_withdrawal = privatePostSxpWithdrawal = Entry('sxp_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_sxp_address = privatePostSxpAddress = Entry('sxp_address/', 'private', 'POST', {'cost': 1}) + private_post_ada_withdrawal = privatePostAdaWithdrawal = Entry('ada_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_ada_address = privatePostAdaAddress = Entry('ada_address/', 'private', 'POST', {'cost': 1}) + private_post_slp_withdrawal = privatePostSlpWithdrawal = Entry('slp_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_slp_address = privatePostSlpAddress = Entry('slp_address/', 'private', 'POST', {'cost': 1}) + private_post_ftm_withdrawal = privatePostFtmWithdrawal = Entry('ftm_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_ftm_address = privatePostFtmAddress = Entry('ftm_address/', 'private', 'POST', {'cost': 1}) + private_post_perp_withdrawal = privatePostPerpWithdrawal = Entry('perp_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_perp_address = privatePostPerpAddress = Entry('perp_address/', 'private', 'POST', {'cost': 1}) + private_post_dydx_withdrawal = privatePostDydxWithdrawal = Entry('dydx_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_dydx_address = privatePostDydxAddress = Entry('dydx_address/', 'private', 'POST', {'cost': 1}) + private_post_gala_withdrawal = privatePostGalaWithdrawal = Entry('gala_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_gala_address = privatePostGalaAddress = Entry('gala_address/', 'private', 'POST', {'cost': 1}) + private_post_shib_withdrawal = privatePostShibWithdrawal = Entry('shib_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_shib_address = privatePostShibAddress = Entry('shib_address/', 'private', 'POST', {'cost': 1}) + private_post_amp_withdrawal = privatePostAmpWithdrawal = Entry('amp_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_amp_address = privatePostAmpAddress = Entry('amp_address/', 'private', 'POST', {'cost': 1}) + private_post_sgb_withdrawal = privatePostSgbWithdrawal = Entry('sgb_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_sgb_address = privatePostSgbAddress = Entry('sgb_address/', 'private', 'POST', {'cost': 1}) + private_post_avax_withdrawal = privatePostAvaxWithdrawal = Entry('avax_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_avax_address = privatePostAvaxAddress = Entry('avax_address/', 'private', 'POST', {'cost': 1}) + private_post_wbtc_withdrawal = privatePostWbtcWithdrawal = Entry('wbtc_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_wbtc_address = privatePostWbtcAddress = Entry('wbtc_address/', 'private', 'POST', {'cost': 1}) + private_post_ctsi_withdrawal = privatePostCtsiWithdrawal = Entry('ctsi_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_ctsi_address = privatePostCtsiAddress = Entry('ctsi_address/', 'private', 'POST', {'cost': 1}) + private_post_cvx_withdrawal = privatePostCvxWithdrawal = Entry('cvx_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_cvx_address = privatePostCvxAddress = Entry('cvx_address/', 'private', 'POST', {'cost': 1}) + private_post_imx_withdrawal = privatePostImxWithdrawal = Entry('imx_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_imx_address = privatePostImxAddress = Entry('imx_address/', 'private', 'POST', {'cost': 1}) + private_post_nexo_withdrawal = privatePostNexoWithdrawal = Entry('nexo_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_nexo_address = privatePostNexoAddress = Entry('nexo_address/', 'private', 'POST', {'cost': 1}) + private_post_ust_withdrawal = privatePostUstWithdrawal = Entry('ust_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_ust_address = privatePostUstAddress = Entry('ust_address/', 'private', 'POST', {'cost': 1}) + private_post_ant_withdrawal = privatePostAntWithdrawal = Entry('ant_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_ant_address = privatePostAntAddress = Entry('ant_address/', 'private', 'POST', {'cost': 1}) + private_post_gods_withdrawal = privatePostGodsWithdrawal = Entry('gods_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_gods_address = privatePostGodsAddress = Entry('gods_address/', 'private', 'POST', {'cost': 1}) + private_post_rad_withdrawal = privatePostRadWithdrawal = Entry('rad_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_rad_address = privatePostRadAddress = Entry('rad_address/', 'private', 'POST', {'cost': 1}) + private_post_band_withdrawal = privatePostBandWithdrawal = Entry('band_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_band_address = privatePostBandAddress = Entry('band_address/', 'private', 'POST', {'cost': 1}) + private_post_inj_withdrawal = privatePostInjWithdrawal = Entry('inj_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_inj_address = privatePostInjAddress = Entry('inj_address/', 'private', 'POST', {'cost': 1}) + private_post_rly_withdrawal = privatePostRlyWithdrawal = Entry('rly_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_rly_address = privatePostRlyAddress = Entry('rly_address/', 'private', 'POST', {'cost': 1}) + private_post_rndr_withdrawal = privatePostRndrWithdrawal = Entry('rndr_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_rndr_address = privatePostRndrAddress = Entry('rndr_address/', 'private', 'POST', {'cost': 1}) + private_post_vega_withdrawal = privatePostVegaWithdrawal = Entry('vega_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_vega_address = privatePostVegaAddress = Entry('vega_address/', 'private', 'POST', {'cost': 1}) + private_post_1inch_withdrawal = privatePost1inchWithdrawal = Entry('1inch_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_1inch_address = privatePost1inchAddress = Entry('1inch_address/', 'private', 'POST', {'cost': 1}) + private_post_ens_withdrawal = privatePostEnsWithdrawal = Entry('ens_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_ens_address = privatePostEnsAddress = Entry('ens_address/', 'private', 'POST', {'cost': 1}) + private_post_mana_withdrawal = privatePostManaWithdrawal = Entry('mana_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_mana_address = privatePostManaAddress = Entry('mana_address/', 'private', 'POST', {'cost': 1}) + private_post_lrc_withdrawal = privatePostLrcWithdrawal = Entry('lrc_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_lrc_address = privatePostLrcAddress = Entry('lrc_address/', 'private', 'POST', {'cost': 1}) + private_post_ape_withdrawal = privatePostApeWithdrawal = Entry('ape_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_ape_address = privatePostApeAddress = Entry('ape_address/', 'private', 'POST', {'cost': 1}) + private_post_mpl_withdrawal = privatePostMplWithdrawal = Entry('mpl_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_mpl_address = privatePostMplAddress = Entry('mpl_address/', 'private', 'POST', {'cost': 1}) + private_post_euroc_withdrawal = privatePostEurocWithdrawal = Entry('euroc_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_euroc_address = privatePostEurocAddress = Entry('euroc_address/', 'private', 'POST', {'cost': 1}) + private_post_sol_withdrawal = privatePostSolWithdrawal = Entry('sol_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_sol_address = privatePostSolAddress = Entry('sol_address/', 'private', 'POST', {'cost': 1}) + private_post_dot_withdrawal = privatePostDotWithdrawal = Entry('dot_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_dot_address = privatePostDotAddress = Entry('dot_address/', 'private', 'POST', {'cost': 1}) + private_post_near_withdrawal = privatePostNearWithdrawal = Entry('near_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_near_address = privatePostNearAddress = Entry('near_address/', 'private', 'POST', {'cost': 1}) + private_post_doge_withdrawal = privatePostDogeWithdrawal = Entry('doge_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_doge_address = privatePostDogeAddress = Entry('doge_address/', 'private', 'POST', {'cost': 1}) + private_post_flr_withdrawal = privatePostFlrWithdrawal = Entry('flr_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_flr_address = privatePostFlrAddress = Entry('flr_address/', 'private', 'POST', {'cost': 1}) + private_post_dgld_withdrawal = privatePostDgldWithdrawal = Entry('dgld_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_dgld_address = privatePostDgldAddress = Entry('dgld_address/', 'private', 'POST', {'cost': 1}) + private_post_ldo_withdrawal = privatePostLdoWithdrawal = Entry('ldo_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_ldo_address = privatePostLdoAddress = Entry('ldo_address/', 'private', 'POST', {'cost': 1}) + private_post_travel_rule_contacts = privatePostTravelRuleContacts = Entry('travel_rule/contacts/', 'private', 'POST', {'cost': 1}) + private_post_earn_subscribe = privatePostEarnSubscribe = Entry('earn/subscribe/', 'private', 'POST', {'cost': 1}) + private_post_earn_subscriptions_setting = privatePostEarnSubscriptionsSetting = Entry('earn/subscriptions/setting/', 'private', 'POST', {'cost': 1}) + private_post_earn_unsubscribe = privatePostEarnUnsubscribe = Entry('earn/unsubscribe', 'private', 'POST', {'cost': 1}) + private_post_wecan_withdrawal = privatePostWecanWithdrawal = Entry('wecan_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_wecan_address = privatePostWecanAddress = Entry('wecan_address/', 'private', 'POST', {'cost': 1}) + private_post_trac_withdrawal = privatePostTracWithdrawal = Entry('trac_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_trac_address = privatePostTracAddress = Entry('trac_address/', 'private', 'POST', {'cost': 1}) + private_post_eurcv_withdrawal = privatePostEurcvWithdrawal = Entry('eurcv_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_eurcv_address = privatePostEurcvAddress = Entry('eurcv_address/', 'private', 'POST', {'cost': 1}) + private_post_pyusd_withdrawal = privatePostPyusdWithdrawal = Entry('pyusd_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_pyusd_address = privatePostPyusdAddress = Entry('pyusd_address/', 'private', 'POST', {'cost': 1}) + private_post_lmwr_withdrawal = privatePostLmwrWithdrawal = Entry('lmwr_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_lmwr_address = privatePostLmwrAddress = Entry('lmwr_address/', 'private', 'POST', {'cost': 1}) + private_post_pepe_withdrawal = privatePostPepeWithdrawal = Entry('pepe_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_pepe_address = privatePostPepeAddress = Entry('pepe_address/', 'private', 'POST', {'cost': 1}) + private_post_blur_withdrawal = privatePostBlurWithdrawal = Entry('blur_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_blur_address = privatePostBlurAddress = Entry('blur_address/', 'private', 'POST', {'cost': 1}) + private_post_vext_withdrawal = privatePostVextWithdrawal = Entry('vext_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_vext_address = privatePostVextAddress = Entry('vext_address/', 'private', 'POST', {'cost': 1}) + private_post_cspr_withdrawal = privatePostCsprWithdrawal = Entry('cspr_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_cspr_address = privatePostCsprAddress = Entry('cspr_address/', 'private', 'POST', {'cost': 1}) + private_post_vchf_withdrawal = privatePostVchfWithdrawal = Entry('vchf_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_vchf_address = privatePostVchfAddress = Entry('vchf_address/', 'private', 'POST', {'cost': 1}) + private_post_veur_withdrawal = privatePostVeurWithdrawal = Entry('veur_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_veur_address = privatePostVeurAddress = Entry('veur_address/', 'private', 'POST', {'cost': 1}) + private_post_truf_withdrawal = privatePostTrufWithdrawal = Entry('truf_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_truf_address = privatePostTrufAddress = Entry('truf_address/', 'private', 'POST', {'cost': 1}) + private_post_wif_withdrawal = privatePostWifWithdrawal = Entry('wif_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_wif_address = privatePostWifAddress = Entry('wif_address/', 'private', 'POST', {'cost': 1}) + private_post_smt_withdrawal = privatePostSmtWithdrawal = Entry('smt_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_smt_address = privatePostSmtAddress = Entry('smt_address/', 'private', 'POST', {'cost': 1}) + private_post_sui_withdrawal = privatePostSuiWithdrawal = Entry('sui_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_sui_address = privatePostSuiAddress = Entry('sui_address/', 'private', 'POST', {'cost': 1}) + private_post_jup_withdrawal = privatePostJupWithdrawal = Entry('jup_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_jup_address = privatePostJupAddress = Entry('jup_address/', 'private', 'POST', {'cost': 1}) + private_post_ondo_withdrawal = privatePostOndoWithdrawal = Entry('ondo_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_ondo_address = privatePostOndoAddress = Entry('ondo_address/', 'private', 'POST', {'cost': 1}) + private_post_boba_withdrawal = privatePostBobaWithdrawal = Entry('boba_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_boba_address = privatePostBobaAddress = Entry('boba_address/', 'private', 'POST', {'cost': 1}) + private_post_pyth_withdrawal = privatePostPythWithdrawal = Entry('pyth_withdrawal/', 'private', 'POST', {'cost': 1}) + private_post_pyth_address = privatePostPythAddress = Entry('pyth_address/', 'private', 'POST', {'cost': 1}) diff --git a/ccxt/abstract/bitteam.py b/ccxt/abstract/bitteam.py new file mode 100644 index 0000000..7af8efc --- /dev/null +++ b/ccxt/abstract/bitteam.py @@ -0,0 +1,29 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + history_get_api_tw_history_pairname_resolution = historyGetApiTwHistoryPairNameResolution = Entry('api/tw/history/{pairName}/{resolution}', 'history', 'GET', {'cost': 1}) + public_get_trade_api_asset = publicGetTradeApiAsset = Entry('trade/api/asset', 'public', 'GET', {'cost': 1}) + public_get_trade_api_currencies = publicGetTradeApiCurrencies = Entry('trade/api/currencies', 'public', 'GET', {'cost': 1}) + public_get_trade_api_orderbooks_symbol = publicGetTradeApiOrderbooksSymbol = Entry('trade/api/orderbooks/{symbol}', 'public', 'GET', {'cost': 1}) + public_get_trade_api_orders = publicGetTradeApiOrders = Entry('trade/api/orders', 'public', 'GET', {'cost': 1}) + public_get_trade_api_pair_name = publicGetTradeApiPairName = Entry('trade/api/pair/{name}', 'public', 'GET', {'cost': 1}) + public_get_trade_api_pairs = publicGetTradeApiPairs = Entry('trade/api/pairs', 'public', 'GET', {'cost': 1}) + public_get_trade_api_pairs_precisions = publicGetTradeApiPairsPrecisions = Entry('trade/api/pairs/precisions', 'public', 'GET', {'cost': 1}) + public_get_trade_api_rates = publicGetTradeApiRates = Entry('trade/api/rates', 'public', 'GET', {'cost': 1}) + public_get_trade_api_trade_id = publicGetTradeApiTradeId = Entry('trade/api/trade/{id}', 'public', 'GET', {'cost': 1}) + public_get_trade_api_trades = publicGetTradeApiTrades = Entry('trade/api/trades', 'public', 'GET', {'cost': 1}) + public_get_trade_api_ccxt_pairs = publicGetTradeApiCcxtPairs = Entry('trade/api/ccxt/pairs', 'public', 'GET', {'cost': 1}) + public_get_trade_api_cmc_assets = publicGetTradeApiCmcAssets = Entry('trade/api/cmc/assets', 'public', 'GET', {'cost': 1}) + public_get_trade_api_cmc_orderbook_pair = publicGetTradeApiCmcOrderbookPair = Entry('trade/api/cmc/orderbook/{pair}', 'public', 'GET', {'cost': 1}) + public_get_trade_api_cmc_summary = publicGetTradeApiCmcSummary = Entry('trade/api/cmc/summary', 'public', 'GET', {'cost': 1}) + public_get_trade_api_cmc_ticker = publicGetTradeApiCmcTicker = Entry('trade/api/cmc/ticker', 'public', 'GET', {'cost': 1}) + public_get_trade_api_cmc_trades_pair = publicGetTradeApiCmcTradesPair = Entry('trade/api/cmc/trades/{pair}', 'public', 'GET', {'cost': 1}) + private_get_trade_api_ccxt_balance = privateGetTradeApiCcxtBalance = Entry('trade/api/ccxt/balance', 'private', 'GET', {'cost': 1}) + private_get_trade_api_ccxt_order_id = privateGetTradeApiCcxtOrderId = Entry('trade/api/ccxt/order/{id}', 'private', 'GET', {'cost': 1}) + private_get_trade_api_ccxt_ordersofuser = privateGetTradeApiCcxtOrdersOfUser = Entry('trade/api/ccxt/ordersOfUser', 'private', 'GET', {'cost': 1}) + private_get_trade_api_ccxt_tradesofuser = privateGetTradeApiCcxtTradesOfUser = Entry('trade/api/ccxt/tradesOfUser', 'private', 'GET', {'cost': 1}) + private_get_trade_api_transactionsofuser = privateGetTradeApiTransactionsOfUser = Entry('trade/api/transactionsOfUser', 'private', 'GET', {'cost': 1}) + private_post_trade_api_ccxt_cancel_all_order = privatePostTradeApiCcxtCancelAllOrder = Entry('trade/api/ccxt/cancel-all-order', 'private', 'POST', {'cost': 1}) + private_post_trade_api_ccxt_cancelorder = privatePostTradeApiCcxtCancelorder = Entry('trade/api/ccxt/cancelorder', 'private', 'POST', {'cost': 1}) + private_post_trade_api_ccxt_ordercreate = privatePostTradeApiCcxtOrdercreate = Entry('trade/api/ccxt/ordercreate', 'private', 'POST', {'cost': 1}) diff --git a/ccxt/abstract/bittrade.py b/ccxt/abstract/bittrade.py new file mode 100644 index 0000000..bd4a3d0 --- /dev/null +++ b/ccxt/abstract/bittrade.py @@ -0,0 +1,114 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + v2public_get_reference_currencies = v2PublicGetReferenceCurrencies = Entry('reference/currencies', 'v2Public', 'GET', {'cost': 1}) + v2public_get_market_status = v2PublicGetMarketStatus = Entry('market-status', 'v2Public', 'GET', {'cost': 1}) + v2private_get_account_ledger = v2PrivateGetAccountLedger = Entry('account/ledger', 'v2Private', 'GET', {'cost': 1}) + v2private_get_account_withdraw_quota = v2PrivateGetAccountWithdrawQuota = Entry('account/withdraw/quota', 'v2Private', 'GET', {'cost': 1}) + v2private_get_account_withdraw_address = v2PrivateGetAccountWithdrawAddress = Entry('account/withdraw/address', 'v2Private', 'GET', {'cost': 1}) + v2private_get_account_deposit_address = v2PrivateGetAccountDepositAddress = Entry('account/deposit/address', 'v2Private', 'GET', {'cost': 1}) + v2private_get_account_repayment = v2PrivateGetAccountRepayment = Entry('account/repayment', 'v2Private', 'GET', {'cost': 5}) + v2private_get_reference_transact_fee_rate = v2PrivateGetReferenceTransactFeeRate = Entry('reference/transact-fee-rate', 'v2Private', 'GET', {'cost': 1}) + v2private_get_account_asset_valuation = v2PrivateGetAccountAssetValuation = Entry('account/asset-valuation', 'v2Private', 'GET', {'cost': 0.2}) + v2private_get_point_account = v2PrivateGetPointAccount = Entry('point/account', 'v2Private', 'GET', {'cost': 5}) + v2private_get_sub_user_user_list = v2PrivateGetSubUserUserList = Entry('sub-user/user-list', 'v2Private', 'GET', {'cost': 1}) + v2private_get_sub_user_user_state = v2PrivateGetSubUserUserState = Entry('sub-user/user-state', 'v2Private', 'GET', {'cost': 1}) + v2private_get_sub_user_account_list = v2PrivateGetSubUserAccountList = Entry('sub-user/account-list', 'v2Private', 'GET', {'cost': 1}) + v2private_get_sub_user_deposit_address = v2PrivateGetSubUserDepositAddress = Entry('sub-user/deposit-address', 'v2Private', 'GET', {'cost': 1}) + v2private_get_sub_user_query_deposit = v2PrivateGetSubUserQueryDeposit = Entry('sub-user/query-deposit', 'v2Private', 'GET', {'cost': 1}) + v2private_get_user_api_key = v2PrivateGetUserApiKey = Entry('user/api-key', 'v2Private', 'GET', {'cost': 1}) + v2private_get_user_uid = v2PrivateGetUserUid = Entry('user/uid', 'v2Private', 'GET', {'cost': 1}) + v2private_get_algo_orders_opening = v2PrivateGetAlgoOrdersOpening = Entry('algo-orders/opening', 'v2Private', 'GET', {'cost': 1}) + v2private_get_algo_orders_history = v2PrivateGetAlgoOrdersHistory = Entry('algo-orders/history', 'v2Private', 'GET', {'cost': 1}) + v2private_get_algo_orders_specific = v2PrivateGetAlgoOrdersSpecific = Entry('algo-orders/specific', 'v2Private', 'GET', {'cost': 1}) + v2private_get_c2c_offers = v2PrivateGetC2cOffers = Entry('c2c/offers', 'v2Private', 'GET', {'cost': 1}) + v2private_get_c2c_offer = v2PrivateGetC2cOffer = Entry('c2c/offer', 'v2Private', 'GET', {'cost': 1}) + v2private_get_c2c_transactions = v2PrivateGetC2cTransactions = Entry('c2c/transactions', 'v2Private', 'GET', {'cost': 1}) + v2private_get_c2c_repayment = v2PrivateGetC2cRepayment = Entry('c2c/repayment', 'v2Private', 'GET', {'cost': 1}) + v2private_get_c2c_account = v2PrivateGetC2cAccount = Entry('c2c/account', 'v2Private', 'GET', {'cost': 1}) + v2private_get_etp_reference = v2PrivateGetEtpReference = Entry('etp/reference', 'v2Private', 'GET', {'cost': 1}) + v2private_get_etp_transactions = v2PrivateGetEtpTransactions = Entry('etp/transactions', 'v2Private', 'GET', {'cost': 5}) + v2private_get_etp_transaction = v2PrivateGetEtpTransaction = Entry('etp/transaction', 'v2Private', 'GET', {'cost': 5}) + v2private_get_etp_rebalance = v2PrivateGetEtpRebalance = Entry('etp/rebalance', 'v2Private', 'GET', {'cost': 1}) + v2private_get_etp_limit = v2PrivateGetEtpLimit = Entry('etp/limit', 'v2Private', 'GET', {'cost': 1}) + v2private_post_account_transfer = v2PrivatePostAccountTransfer = Entry('account/transfer', 'v2Private', 'POST', {'cost': 1}) + v2private_post_account_repayment = v2PrivatePostAccountRepayment = Entry('account/repayment', 'v2Private', 'POST', {'cost': 5}) + v2private_post_point_transfer = v2PrivatePostPointTransfer = Entry('point/transfer', 'v2Private', 'POST', {'cost': 5}) + v2private_post_sub_user_management = v2PrivatePostSubUserManagement = Entry('sub-user/management', 'v2Private', 'POST', {'cost': 1}) + v2private_post_sub_user_creation = v2PrivatePostSubUserCreation = Entry('sub-user/creation', 'v2Private', 'POST', {'cost': 1}) + v2private_post_sub_user_tradable_market = v2PrivatePostSubUserTradableMarket = Entry('sub-user/tradable-market', 'v2Private', 'POST', {'cost': 1}) + v2private_post_sub_user_transferability = v2PrivatePostSubUserTransferability = Entry('sub-user/transferability', 'v2Private', 'POST', {'cost': 1}) + v2private_post_sub_user_api_key_generation = v2PrivatePostSubUserApiKeyGeneration = Entry('sub-user/api-key-generation', 'v2Private', 'POST', {'cost': 1}) + v2private_post_sub_user_api_key_modification = v2PrivatePostSubUserApiKeyModification = Entry('sub-user/api-key-modification', 'v2Private', 'POST', {'cost': 1}) + v2private_post_sub_user_api_key_deletion = v2PrivatePostSubUserApiKeyDeletion = Entry('sub-user/api-key-deletion', 'v2Private', 'POST', {'cost': 1}) + v2private_post_sub_user_deduct_mode = v2PrivatePostSubUserDeductMode = Entry('sub-user/deduct-mode', 'v2Private', 'POST', {'cost': 1}) + v2private_post_algo_orders = v2PrivatePostAlgoOrders = Entry('algo-orders', 'v2Private', 'POST', {'cost': 1}) + v2private_post_algo_orders_cancel_all_after = v2PrivatePostAlgoOrdersCancelAllAfter = Entry('algo-orders/cancel-all-after', 'v2Private', 'POST', {'cost': 1}) + v2private_post_algo_orders_cancellation = v2PrivatePostAlgoOrdersCancellation = Entry('algo-orders/cancellation', 'v2Private', 'POST', {'cost': 1}) + v2private_post_c2c_offer = v2PrivatePostC2cOffer = Entry('c2c/offer', 'v2Private', 'POST', {'cost': 1}) + v2private_post_c2c_cancellation = v2PrivatePostC2cCancellation = Entry('c2c/cancellation', 'v2Private', 'POST', {'cost': 1}) + v2private_post_c2c_cancel_all = v2PrivatePostC2cCancelAll = Entry('c2c/cancel-all', 'v2Private', 'POST', {'cost': 1}) + v2private_post_c2c_repayment = v2PrivatePostC2cRepayment = Entry('c2c/repayment', 'v2Private', 'POST', {'cost': 1}) + v2private_post_c2c_transfer = v2PrivatePostC2cTransfer = Entry('c2c/transfer', 'v2Private', 'POST', {'cost': 1}) + v2private_post_etp_creation = v2PrivatePostEtpCreation = Entry('etp/creation', 'v2Private', 'POST', {'cost': 5}) + v2private_post_etp_redemption = v2PrivatePostEtpRedemption = Entry('etp/redemption', 'v2Private', 'POST', {'cost': 5}) + v2private_post_etp_transactid_cancel = v2PrivatePostEtpTransactIdCancel = Entry('etp/{transactId}/cancel', 'v2Private', 'POST', {'cost': 10}) + v2private_post_etp_batch_cancel = v2PrivatePostEtpBatchCancel = Entry('etp/batch-cancel', 'v2Private', 'POST', {'cost': 50}) + market_get_history_kline = marketGetHistoryKline = Entry('history/kline', 'market', 'GET', {'cost': 1}) + market_get_detail_merged = marketGetDetailMerged = Entry('detail/merged', 'market', 'GET', {'cost': 1}) + market_get_depth = marketGetDepth = Entry('depth', 'market', 'GET', {'cost': 1}) + market_get_trade = marketGetTrade = Entry('trade', 'market', 'GET', {'cost': 1}) + market_get_history_trade = marketGetHistoryTrade = Entry('history/trade', 'market', 'GET', {'cost': 1}) + market_get_detail = marketGetDetail = Entry('detail', 'market', 'GET', {'cost': 1}) + market_get_tickers = marketGetTickers = Entry('tickers', 'market', 'GET', {'cost': 1}) + market_get_etp = marketGetEtp = Entry('etp', 'market', 'GET', {'cost': 1}) + public_get_common_symbols = publicGetCommonSymbols = Entry('common/symbols', 'public', 'GET', {'cost': 1}) + public_get_common_currencys = publicGetCommonCurrencys = Entry('common/currencys', 'public', 'GET', {'cost': 1}) + public_get_common_timestamp = publicGetCommonTimestamp = Entry('common/timestamp', 'public', 'GET', {'cost': 1}) + public_get_common_exchange = publicGetCommonExchange = Entry('common/exchange', 'public', 'GET', {'cost': 1}) + public_get_settings_currencys = publicGetSettingsCurrencys = Entry('settings/currencys', 'public', 'GET', {'cost': 1}) + private_get_account_accounts = privateGetAccountAccounts = Entry('account/accounts', 'private', 'GET', {'cost': 0.2}) + private_get_account_accounts_id_balance = privateGetAccountAccountsIdBalance = Entry('account/accounts/{id}/balance', 'private', 'GET', {'cost': 0.2}) + private_get_account_accounts_sub_uid = privateGetAccountAccountsSubUid = Entry('account/accounts/{sub-uid}', 'private', 'GET', {'cost': 1}) + private_get_account_history = privateGetAccountHistory = Entry('account/history', 'private', 'GET', {'cost': 4}) + private_get_cross_margin_loan_info = privateGetCrossMarginLoanInfo = Entry('cross-margin/loan-info', 'private', 'GET', {'cost': 1}) + private_get_margin_loan_info = privateGetMarginLoanInfo = Entry('margin/loan-info', 'private', 'GET', {'cost': 1}) + private_get_fee_fee_rate_get = privateGetFeeFeeRateGet = Entry('fee/fee-rate/get', 'private', 'GET', {'cost': 1}) + private_get_order_openorders = privateGetOrderOpenOrders = Entry('order/openOrders', 'private', 'GET', {'cost': 0.4}) + private_get_order_orders = privateGetOrderOrders = Entry('order/orders', 'private', 'GET', {'cost': 0.4}) + private_get_order_orders_id = privateGetOrderOrdersId = Entry('order/orders/{id}', 'private', 'GET', {'cost': 0.4}) + private_get_order_orders_id_matchresults = privateGetOrderOrdersIdMatchresults = Entry('order/orders/{id}/matchresults', 'private', 'GET', {'cost': 0.4}) + private_get_order_orders_getclientorder = privateGetOrderOrdersGetClientOrder = Entry('order/orders/getClientOrder', 'private', 'GET', {'cost': 0.4}) + private_get_order_history = privateGetOrderHistory = Entry('order/history', 'private', 'GET', {'cost': 1}) + private_get_order_matchresults = privateGetOrderMatchresults = Entry('order/matchresults', 'private', 'GET', {'cost': 1}) + private_get_query_deposit_withdraw = privateGetQueryDepositWithdraw = Entry('query/deposit-withdraw', 'private', 'GET', {'cost': 1}) + private_get_margin_loan_orders = privateGetMarginLoanOrders = Entry('margin/loan-orders', 'private', 'GET', {'cost': 0.2}) + private_get_margin_accounts_balance = privateGetMarginAccountsBalance = Entry('margin/accounts/balance', 'private', 'GET', {'cost': 0.2}) + private_get_cross_margin_loan_orders = privateGetCrossMarginLoanOrders = Entry('cross-margin/loan-orders', 'private', 'GET', {'cost': 1}) + private_get_cross_margin_accounts_balance = privateGetCrossMarginAccountsBalance = Entry('cross-margin/accounts/balance', 'private', 'GET', {'cost': 1}) + private_get_points_actions = privateGetPointsActions = Entry('points/actions', 'private', 'GET', {'cost': 1}) + private_get_points_orders = privateGetPointsOrders = Entry('points/orders', 'private', 'GET', {'cost': 1}) + private_get_subuser_aggregate_balance = privateGetSubuserAggregateBalance = Entry('subuser/aggregate-balance', 'private', 'GET', {'cost': 10}) + private_get_stable_coin_exchange_rate = privateGetStableCoinExchangeRate = Entry('stable-coin/exchange_rate', 'private', 'GET', {'cost': 1}) + private_get_stable_coin_quote = privateGetStableCoinQuote = Entry('stable-coin/quote', 'private', 'GET', {'cost': 1}) + private_post_account_transfer = privatePostAccountTransfer = Entry('account/transfer', 'private', 'POST', {'cost': 1}) + private_post_futures_transfer = privatePostFuturesTransfer = Entry('futures/transfer', 'private', 'POST', {'cost': 1}) + private_post_order_batch_orders = privatePostOrderBatchOrders = Entry('order/batch-orders', 'private', 'POST', {'cost': 0.4}) + private_post_order_orders_place = privatePostOrderOrdersPlace = Entry('order/orders/place', 'private', 'POST', {'cost': 0.2}) + private_post_order_orders_submitcancelclientorder = privatePostOrderOrdersSubmitCancelClientOrder = Entry('order/orders/submitCancelClientOrder', 'private', 'POST', {'cost': 0.2}) + private_post_order_orders_batchcancelopenorders = privatePostOrderOrdersBatchCancelOpenOrders = Entry('order/orders/batchCancelOpenOrders', 'private', 'POST', {'cost': 0.4}) + private_post_order_orders_id_submitcancel = privatePostOrderOrdersIdSubmitcancel = Entry('order/orders/{id}/submitcancel', 'private', 'POST', {'cost': 0.2}) + private_post_order_orders_batchcancel = privatePostOrderOrdersBatchcancel = Entry('order/orders/batchcancel', 'private', 'POST', {'cost': 0.4}) + private_post_dw_withdraw_api_create = privatePostDwWithdrawApiCreate = Entry('dw/withdraw/api/create', 'private', 'POST', {'cost': 1}) + private_post_dw_withdraw_virtual_id_cancel = privatePostDwWithdrawVirtualIdCancel = Entry('dw/withdraw-virtual/{id}/cancel', 'private', 'POST', {'cost': 1}) + private_post_dw_transfer_in_margin = privatePostDwTransferInMargin = Entry('dw/transfer-in/margin', 'private', 'POST', {'cost': 10}) + private_post_dw_transfer_out_margin = privatePostDwTransferOutMargin = Entry('dw/transfer-out/margin', 'private', 'POST', {'cost': 10}) + private_post_margin_orders = privatePostMarginOrders = Entry('margin/orders', 'private', 'POST', {'cost': 10}) + private_post_margin_orders_id_repay = privatePostMarginOrdersIdRepay = Entry('margin/orders/{id}/repay', 'private', 'POST', {'cost': 10}) + private_post_cross_margin_transfer_in = privatePostCrossMarginTransferIn = Entry('cross-margin/transfer-in', 'private', 'POST', {'cost': 1}) + private_post_cross_margin_transfer_out = privatePostCrossMarginTransferOut = Entry('cross-margin/transfer-out', 'private', 'POST', {'cost': 1}) + private_post_cross_margin_orders = privatePostCrossMarginOrders = Entry('cross-margin/orders', 'private', 'POST', {'cost': 1}) + private_post_cross_margin_orders_id_repay = privatePostCrossMarginOrdersIdRepay = Entry('cross-margin/orders/{id}/repay', 'private', 'POST', {'cost': 1}) + private_post_stable_coin_exchange = privatePostStableCoinExchange = Entry('stable-coin/exchange', 'private', 'POST', {'cost': 1}) + private_post_subuser_transfer = privatePostSubuserTransfer = Entry('subuser/transfer', 'private', 'POST', {'cost': 10}) diff --git a/ccxt/abstract/bitvavo.py b/ccxt/abstract/bitvavo.py new file mode 100644 index 0000000..827d2ea --- /dev/null +++ b/ccxt/abstract/bitvavo.py @@ -0,0 +1,27 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_time = publicGetTime = Entry('time', 'public', 'GET', {'cost': 1}) + public_get_markets = publicGetMarkets = Entry('markets', 'public', 'GET', {'cost': 1}) + public_get_assets = publicGetAssets = Entry('assets', 'public', 'GET', {'cost': 1}) + public_get_market_book = publicGetMarketBook = Entry('{market}/book', 'public', 'GET', {'cost': 1}) + public_get_market_trades = publicGetMarketTrades = Entry('{market}/trades', 'public', 'GET', {'cost': 5}) + public_get_market_candles = publicGetMarketCandles = Entry('{market}/candles', 'public', 'GET', {'cost': 1}) + public_get_ticker_price = publicGetTickerPrice = Entry('ticker/price', 'public', 'GET', {'cost': 1}) + public_get_ticker_book = publicGetTickerBook = Entry('ticker/book', 'public', 'GET', {'cost': 1}) + public_get_ticker_24h = publicGetTicker24h = Entry('ticker/24h', 'public', 'GET', {'cost': 1, 'noMarket': 25}) + private_get_account = privateGetAccount = Entry('account', 'private', 'GET', {'cost': 1}) + private_get_order = privateGetOrder = Entry('order', 'private', 'GET', {'cost': 1}) + private_get_orders = privateGetOrders = Entry('orders', 'private', 'GET', {'cost': 5}) + private_get_ordersopen = privateGetOrdersOpen = Entry('ordersOpen', 'private', 'GET', {'cost': 1, 'noMarket': 25}) + private_get_trades = privateGetTrades = Entry('trades', 'private', 'GET', {'cost': 5}) + private_get_balance = privateGetBalance = Entry('balance', 'private', 'GET', {'cost': 5}) + private_get_deposit = privateGetDeposit = Entry('deposit', 'private', 'GET', {'cost': 1}) + private_get_deposithistory = privateGetDepositHistory = Entry('depositHistory', 'private', 'GET', {'cost': 5}) + private_get_withdrawalhistory = privateGetWithdrawalHistory = Entry('withdrawalHistory', 'private', 'GET', {'cost': 5}) + private_post_order = privatePostOrder = Entry('order', 'private', 'POST', {'cost': 1}) + private_post_withdrawal = privatePostWithdrawal = Entry('withdrawal', 'private', 'POST', {'cost': 1}) + private_put_order = privatePutOrder = Entry('order', 'private', 'PUT', {'cost': 1}) + private_delete_order = privateDeleteOrder = Entry('order', 'private', 'DELETE', {'cost': 1}) + private_delete_orders = privateDeleteOrders = Entry('orders', 'private', 'DELETE', {'cost': 1}) diff --git a/ccxt/abstract/blockchaincom.py b/ccxt/abstract/blockchaincom.py new file mode 100644 index 0000000..295ce2a --- /dev/null +++ b/ccxt/abstract/blockchaincom.py @@ -0,0 +1,28 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_tickers = publicGetTickers = Entry('tickers', 'public', 'GET', {'cost': 1}) + public_get_tickers_symbol = publicGetTickersSymbol = Entry('tickers/{symbol}', 'public', 'GET', {'cost': 1}) + public_get_symbols = publicGetSymbols = Entry('symbols', 'public', 'GET', {'cost': 1}) + public_get_symbols_symbol = publicGetSymbolsSymbol = Entry('symbols/{symbol}', 'public', 'GET', {'cost': 1}) + public_get_l2_symbol = publicGetL2Symbol = Entry('l2/{symbol}', 'public', 'GET', {'cost': 1}) + public_get_l3_symbol = publicGetL3Symbol = Entry('l3/{symbol}', 'public', 'GET', {'cost': 1}) + private_get_fees = privateGetFees = Entry('fees', 'private', 'GET', {'cost': 1}) + private_get_orders = privateGetOrders = Entry('orders', 'private', 'GET', {'cost': 1}) + private_get_orders_orderid = privateGetOrdersOrderId = Entry('orders/{orderId}', 'private', 'GET', {'cost': 1}) + private_get_trades = privateGetTrades = Entry('trades', 'private', 'GET', {'cost': 1}) + private_get_fills = privateGetFills = Entry('fills', 'private', 'GET', {'cost': 1}) + private_get_deposits = privateGetDeposits = Entry('deposits', 'private', 'GET', {'cost': 1}) + private_get_deposits_depositid = privateGetDepositsDepositId = Entry('deposits/{depositId}', 'private', 'GET', {'cost': 1}) + private_get_accounts = privateGetAccounts = Entry('accounts', 'private', 'GET', {'cost': 1}) + private_get_accounts_account_currency = privateGetAccountsAccountCurrency = Entry('accounts/{account}/{currency}', 'private', 'GET', {'cost': 1}) + private_get_whitelist = privateGetWhitelist = Entry('whitelist', 'private', 'GET', {'cost': 1}) + private_get_whitelist_currency = privateGetWhitelistCurrency = Entry('whitelist/{currency}', 'private', 'GET', {'cost': 1}) + private_get_withdrawals = privateGetWithdrawals = Entry('withdrawals', 'private', 'GET', {'cost': 1}) + private_get_withdrawals_withdrawalid = privateGetWithdrawalsWithdrawalId = Entry('withdrawals/{withdrawalId}', 'private', 'GET', {'cost': 1}) + private_post_orders = privatePostOrders = Entry('orders', 'private', 'POST', {'cost': 1}) + private_post_deposits_currency = privatePostDepositsCurrency = Entry('deposits/{currency}', 'private', 'POST', {'cost': 1}) + private_post_withdrawals = privatePostWithdrawals = Entry('withdrawals', 'private', 'POST', {'cost': 1}) + private_delete_orders = privateDeleteOrders = Entry('orders', 'private', 'DELETE', {'cost': 1}) + private_delete_orders_orderid = privateDeleteOrdersOrderId = Entry('orders/{orderId}', 'private', 'DELETE', {'cost': 1}) diff --git a/ccxt/abstract/blofin.py b/ccxt/abstract/blofin.py new file mode 100644 index 0000000..e531018 --- /dev/null +++ b/ccxt/abstract/blofin.py @@ -0,0 +1,67 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_market_instruments = publicGetMarketInstruments = Entry('market/instruments', 'public', 'GET', {'cost': 1}) + public_get_market_tickers = publicGetMarketTickers = Entry('market/tickers', 'public', 'GET', {'cost': 1}) + public_get_market_books = publicGetMarketBooks = Entry('market/books', 'public', 'GET', {'cost': 1}) + public_get_market_trades = publicGetMarketTrades = Entry('market/trades', 'public', 'GET', {'cost': 1}) + public_get_market_candles = publicGetMarketCandles = Entry('market/candles', 'public', 'GET', {'cost': 1}) + public_get_market_mark_price = publicGetMarketMarkPrice = Entry('market/mark-price', 'public', 'GET', {'cost': 1}) + public_get_market_funding_rate = publicGetMarketFundingRate = Entry('market/funding-rate', 'public', 'GET', {'cost': 1}) + public_get_market_funding_rate_history = publicGetMarketFundingRateHistory = Entry('market/funding-rate-history', 'public', 'GET', {'cost': 1}) + private_get_asset_balances = privateGetAssetBalances = Entry('asset/balances', 'private', 'GET', {'cost': 1}) + private_get_trade_orders_pending = privateGetTradeOrdersPending = Entry('trade/orders-pending', 'private', 'GET', {'cost': 1}) + private_get_trade_fills_history = privateGetTradeFillsHistory = Entry('trade/fills-history', 'private', 'GET', {'cost': 1}) + private_get_asset_deposit_history = privateGetAssetDepositHistory = Entry('asset/deposit-history', 'private', 'GET', {'cost': 1}) + private_get_asset_withdrawal_history = privateGetAssetWithdrawalHistory = Entry('asset/withdrawal-history', 'private', 'GET', {'cost': 1}) + private_get_asset_bills = privateGetAssetBills = Entry('asset/bills', 'private', 'GET', {'cost': 1}) + private_get_account_balance = privateGetAccountBalance = Entry('account/balance', 'private', 'GET', {'cost': 1}) + private_get_account_positions = privateGetAccountPositions = Entry('account/positions', 'private', 'GET', {'cost': 1}) + private_get_account_leverage_info = privateGetAccountLeverageInfo = Entry('account/leverage-info', 'private', 'GET', {'cost': 1}) + private_get_account_margin_mode = privateGetAccountMarginMode = Entry('account/margin-mode', 'private', 'GET', {'cost': 1}) + private_get_account_position_mode = privateGetAccountPositionMode = Entry('account/position-mode', 'private', 'GET', {'cost': 1}) + private_get_account_batch_leverage_info = privateGetAccountBatchLeverageInfo = Entry('account/batch-leverage-info', 'private', 'GET', {'cost': 1}) + private_get_trade_orders_tpsl_pending = privateGetTradeOrdersTpslPending = Entry('trade/orders-tpsl-pending', 'private', 'GET', {'cost': 1}) + private_get_trade_orders_algo_pending = privateGetTradeOrdersAlgoPending = Entry('trade/orders-algo-pending', 'private', 'GET', {'cost': 1}) + private_get_trade_orders_history = privateGetTradeOrdersHistory = Entry('trade/orders-history', 'private', 'GET', {'cost': 1}) + private_get_trade_orders_tpsl_history = privateGetTradeOrdersTpslHistory = Entry('trade/orders-tpsl-history', 'private', 'GET', {'cost': 1}) + private_get_trade_orders_algo_history = privateGetTradeOrdersAlgoHistory = Entry('trade/orders-algo-history', 'private', 'GET', {'cost': 1}) + private_get_trade_order_price_range = privateGetTradeOrderPriceRange = Entry('trade/order/price-range', 'private', 'GET', {'cost': 1}) + private_get_user_query_apikey = privateGetUserQueryApikey = Entry('user/query-apikey', 'private', 'GET', {'cost': 1}) + private_get_affiliate_basic = privateGetAffiliateBasic = Entry('affiliate/basic', 'private', 'GET', {'cost': 1}) + private_get_copytrading_instruments = privateGetCopytradingInstruments = Entry('copytrading/instruments', 'private', 'GET', {'cost': 1}) + private_get_copytrading_account_balance = privateGetCopytradingAccountBalance = Entry('copytrading/account/balance', 'private', 'GET', {'cost': 1}) + private_get_copytrading_account_positions_by_order = privateGetCopytradingAccountPositionsByOrder = Entry('copytrading/account/positions-by-order', 'private', 'GET', {'cost': 1}) + private_get_copytrading_account_positions_details_by_order = privateGetCopytradingAccountPositionsDetailsByOrder = Entry('copytrading/account/positions-details-by-order', 'private', 'GET', {'cost': 1}) + private_get_copytrading_account_positions_by_contract = privateGetCopytradingAccountPositionsByContract = Entry('copytrading/account/positions-by-contract', 'private', 'GET', {'cost': 1}) + private_get_copytrading_account_position_mode = privateGetCopytradingAccountPositionMode = Entry('copytrading/account/position-mode', 'private', 'GET', {'cost': 1}) + private_get_copytrading_account_leverage_info = privateGetCopytradingAccountLeverageInfo = Entry('copytrading/account/leverage-info', 'private', 'GET', {'cost': 1}) + private_get_copytrading_trade_orders_pending = privateGetCopytradingTradeOrdersPending = Entry('copytrading/trade/orders-pending', 'private', 'GET', {'cost': 1}) + private_get_copytrading_trade_pending_tpsl_by_contract = privateGetCopytradingTradePendingTpslByContract = Entry('copytrading/trade/pending-tpsl-by-contract', 'private', 'GET', {'cost': 1}) + private_get_copytrading_trade_position_history_by_order = privateGetCopytradingTradePositionHistoryByOrder = Entry('copytrading/trade/position-history-by-order', 'private', 'GET', {'cost': 1}) + private_get_copytrading_trade_orders_history = privateGetCopytradingTradeOrdersHistory = Entry('copytrading/trade/orders-history', 'private', 'GET', {'cost': 1}) + private_get_copytrading_trade_pending_tpsl_by_order = privateGetCopytradingTradePendingTpslByOrder = Entry('copytrading/trade/pending-tpsl-by-order', 'private', 'GET', {'cost': 1}) + private_post_account_set_margin_mode = privatePostAccountSetMarginMode = Entry('account/set-margin-mode', 'private', 'POST', {'cost': 1}) + private_post_account_set_position_mode = privatePostAccountSetPositionMode = Entry('account/set-position-mode', 'private', 'POST', {'cost': 1}) + private_post_trade_order = privatePostTradeOrder = Entry('trade/order', 'private', 'POST', {'cost': 1}) + private_post_trade_order_algo = privatePostTradeOrderAlgo = Entry('trade/order-algo', 'private', 'POST', {'cost': 1}) + private_post_trade_cancel_order = privatePostTradeCancelOrder = Entry('trade/cancel-order', 'private', 'POST', {'cost': 1}) + private_post_trade_cancel_algo = privatePostTradeCancelAlgo = Entry('trade/cancel-algo', 'private', 'POST', {'cost': 1}) + private_post_account_set_leverage = privatePostAccountSetLeverage = Entry('account/set-leverage', 'private', 'POST', {'cost': 1}) + private_post_trade_batch_orders = privatePostTradeBatchOrders = Entry('trade/batch-orders', 'private', 'POST', {'cost': 1}) + private_post_trade_order_tpsl = privatePostTradeOrderTpsl = Entry('trade/order-tpsl', 'private', 'POST', {'cost': 1}) + private_post_trade_cancel_batch_orders = privatePostTradeCancelBatchOrders = Entry('trade/cancel-batch-orders', 'private', 'POST', {'cost': 1}) + private_post_trade_cancel_tpsl = privatePostTradeCancelTpsl = Entry('trade/cancel-tpsl', 'private', 'POST', {'cost': 1}) + private_post_trade_close_position = privatePostTradeClosePosition = Entry('trade/close-position', 'private', 'POST', {'cost': 1}) + private_post_asset_transfer = privatePostAssetTransfer = Entry('asset/transfer', 'private', 'POST', {'cost': 1}) + private_post_copytrading_account_set_position_mode = privatePostCopytradingAccountSetPositionMode = Entry('copytrading/account/set-position-mode', 'private', 'POST', {'cost': 1}) + private_post_copytrading_account_set_leverage = privatePostCopytradingAccountSetLeverage = Entry('copytrading/account/set-leverage', 'private', 'POST', {'cost': 1}) + private_post_copytrading_trade_place_order = privatePostCopytradingTradePlaceOrder = Entry('copytrading/trade/place-order', 'private', 'POST', {'cost': 1}) + private_post_copytrading_trade_cancel_order = privatePostCopytradingTradeCancelOrder = Entry('copytrading/trade/cancel-order', 'private', 'POST', {'cost': 1}) + private_post_copytrading_trade_place_tpsl_by_contract = privatePostCopytradingTradePlaceTpslByContract = Entry('copytrading/trade/place-tpsl-by-contract', 'private', 'POST', {'cost': 1}) + private_post_copytrading_trade_cancel_tpsl_by_contract = privatePostCopytradingTradeCancelTpslByContract = Entry('copytrading/trade/cancel-tpsl-by-contract', 'private', 'POST', {'cost': 1}) + private_post_copytrading_trade_place_tpsl_by_order = privatePostCopytradingTradePlaceTpslByOrder = Entry('copytrading/trade/place-tpsl-by-order', 'private', 'POST', {'cost': 1}) + private_post_copytrading_trade_cancel_tpsl_by_order = privatePostCopytradingTradeCancelTpslByOrder = Entry('copytrading/trade/cancel-tpsl-by-order', 'private', 'POST', {'cost': 1}) + private_post_copytrading_trade_close_position_by_order = privatePostCopytradingTradeClosePositionByOrder = Entry('copytrading/trade/close-position-by-order', 'private', 'POST', {'cost': 1}) + private_post_copytrading_trade_close_position_by_contract = privatePostCopytradingTradeClosePositionByContract = Entry('copytrading/trade/close-position-by-contract', 'private', 'POST', {'cost': 1}) diff --git a/ccxt/abstract/btcalpha.py b/ccxt/abstract/btcalpha.py new file mode 100644 index 0000000..72fcf42 --- /dev/null +++ b/ccxt/abstract/btcalpha.py @@ -0,0 +1,18 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_currencies = publicGetCurrencies = Entry('currencies/', 'public', 'GET', {}) + public_get_pairs = publicGetPairs = Entry('pairs/', 'public', 'GET', {}) + public_get_orderbook_pair_name = publicGetOrderbookPairName = Entry('orderbook/{pair_name}', 'public', 'GET', {}) + public_get_exchanges = publicGetExchanges = Entry('exchanges/', 'public', 'GET', {}) + public_get_charts_pair_type_chart = publicGetChartsPairTypeChart = Entry('charts/{pair}/{type}/chart/', 'public', 'GET', {}) + public_get_ticker = publicGetTicker = Entry('ticker/', 'public', 'GET', {}) + private_get_wallets = privateGetWallets = Entry('wallets/', 'private', 'GET', {}) + private_get_orders_own = privateGetOrdersOwn = Entry('orders/own/', 'private', 'GET', {}) + private_get_order_id = privateGetOrderId = Entry('order/{id}/', 'private', 'GET', {}) + private_get_exchanges_own = privateGetExchangesOwn = Entry('exchanges/own/', 'private', 'GET', {}) + private_get_deposits = privateGetDeposits = Entry('deposits/', 'private', 'GET', {}) + private_get_withdraws = privateGetWithdraws = Entry('withdraws/', 'private', 'GET', {}) + private_post_order = privatePostOrder = Entry('order/', 'private', 'POST', {}) + private_post_order_cancel = privatePostOrderCancel = Entry('order-cancel/', 'private', 'POST', {}) diff --git a/ccxt/abstract/btcbox.py b/ccxt/abstract/btcbox.py new file mode 100644 index 0000000..b8a7ba9 --- /dev/null +++ b/ccxt/abstract/btcbox.py @@ -0,0 +1,15 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_depth = publicGetDepth = Entry('depth', 'public', 'GET', {}) + public_get_orders = publicGetOrders = Entry('orders', 'public', 'GET', {}) + public_get_ticker = publicGetTicker = Entry('ticker', 'public', 'GET', {}) + public_get_tickers = publicGetTickers = Entry('tickers', 'public', 'GET', {}) + private_post_balance = privatePostBalance = Entry('balance', 'private', 'POST', {}) + private_post_trade_add = privatePostTradeAdd = Entry('trade_add', 'private', 'POST', {}) + private_post_trade_cancel = privatePostTradeCancel = Entry('trade_cancel', 'private', 'POST', {}) + private_post_trade_list = privatePostTradeList = Entry('trade_list', 'private', 'POST', {}) + private_post_trade_view = privatePostTradeView = Entry('trade_view', 'private', 'POST', {}) + private_post_wallet = privatePostWallet = Entry('wallet', 'private', 'POST', {}) + webapi_get_ajax_coin_coininfo = webApiGetAjaxCoinCoinInfo = Entry('ajax/coin/coinInfo', 'webApi', 'GET', {}) diff --git a/ccxt/abstract/btcmarkets.py b/ccxt/abstract/btcmarkets.py new file mode 100644 index 0000000..649f964 --- /dev/null +++ b/ccxt/abstract/btcmarkets.py @@ -0,0 +1,39 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_markets = publicGetMarkets = Entry('markets', 'public', 'GET', {}) + public_get_markets_marketid_ticker = publicGetMarketsMarketIdTicker = Entry('markets/{marketId}/ticker', 'public', 'GET', {}) + public_get_markets_marketid_trades = publicGetMarketsMarketIdTrades = Entry('markets/{marketId}/trades', 'public', 'GET', {}) + public_get_markets_marketid_orderbook = publicGetMarketsMarketIdOrderbook = Entry('markets/{marketId}/orderbook', 'public', 'GET', {}) + public_get_markets_marketid_candles = publicGetMarketsMarketIdCandles = Entry('markets/{marketId}/candles', 'public', 'GET', {}) + public_get_markets_tickers = publicGetMarketsTickers = Entry('markets/tickers', 'public', 'GET', {}) + public_get_markets_orderbooks = publicGetMarketsOrderbooks = Entry('markets/orderbooks', 'public', 'GET', {}) + public_get_time = publicGetTime = Entry('time', 'public', 'GET', {}) + private_get_orders = privateGetOrders = Entry('orders', 'private', 'GET', {}) + private_get_orders_id = privateGetOrdersId = Entry('orders/{id}', 'private', 'GET', {}) + private_get_batchorders_ids = privateGetBatchordersIds = Entry('batchorders/{ids}', 'private', 'GET', {}) + private_get_trades = privateGetTrades = Entry('trades', 'private', 'GET', {}) + private_get_trades_id = privateGetTradesId = Entry('trades/{id}', 'private', 'GET', {}) + private_get_withdrawals = privateGetWithdrawals = Entry('withdrawals', 'private', 'GET', {}) + private_get_withdrawals_id = privateGetWithdrawalsId = Entry('withdrawals/{id}', 'private', 'GET', {}) + private_get_deposits = privateGetDeposits = Entry('deposits', 'private', 'GET', {}) + private_get_deposits_id = privateGetDepositsId = Entry('deposits/{id}', 'private', 'GET', {}) + private_get_transfers = privateGetTransfers = Entry('transfers', 'private', 'GET', {}) + private_get_transfers_id = privateGetTransfersId = Entry('transfers/{id}', 'private', 'GET', {}) + private_get_addresses = privateGetAddresses = Entry('addresses', 'private', 'GET', {}) + private_get_withdrawal_fees = privateGetWithdrawalFees = Entry('withdrawal-fees', 'private', 'GET', {}) + private_get_assets = privateGetAssets = Entry('assets', 'private', 'GET', {}) + private_get_accounts_me_trading_fees = privateGetAccountsMeTradingFees = Entry('accounts/me/trading-fees', 'private', 'GET', {}) + private_get_accounts_me_withdrawal_limits = privateGetAccountsMeWithdrawalLimits = Entry('accounts/me/withdrawal-limits', 'private', 'GET', {}) + private_get_accounts_me_balances = privateGetAccountsMeBalances = Entry('accounts/me/balances', 'private', 'GET', {}) + private_get_accounts_me_transactions = privateGetAccountsMeTransactions = Entry('accounts/me/transactions', 'private', 'GET', {}) + private_get_reports_id = privateGetReportsId = Entry('reports/{id}', 'private', 'GET', {}) + private_post_orders = privatePostOrders = Entry('orders', 'private', 'POST', {}) + private_post_batchorders = privatePostBatchorders = Entry('batchorders', 'private', 'POST', {}) + private_post_withdrawals = privatePostWithdrawals = Entry('withdrawals', 'private', 'POST', {}) + private_post_reports = privatePostReports = Entry('reports', 'private', 'POST', {}) + private_delete_orders = privateDeleteOrders = Entry('orders', 'private', 'DELETE', {}) + private_delete_orders_id = privateDeleteOrdersId = Entry('orders/{id}', 'private', 'DELETE', {}) + private_delete_batchorders_ids = privateDeleteBatchordersIds = Entry('batchorders/{ids}', 'private', 'DELETE', {}) + private_put_orders_id = privatePutOrdersId = Entry('orders/{id}', 'private', 'PUT', {}) diff --git a/ccxt/abstract/btcturk.py b/ccxt/abstract/btcturk.py new file mode 100644 index 0000000..d1dcabd --- /dev/null +++ b/ccxt/abstract/btcturk.py @@ -0,0 +1,20 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_orderbook = publicGetOrderbook = Entry('orderbook', 'public', 'GET', {'cost': 1}) + public_get_ticker = publicGetTicker = Entry('ticker', 'public', 'GET', {'cost': 0.1}) + public_get_trades = publicGetTrades = Entry('trades', 'public', 'GET', {'cost': 1}) + public_get_ohlc = publicGetOhlc = Entry('ohlc', 'public', 'GET', {'cost': 1}) + public_get_server_exchangeinfo = publicGetServerExchangeinfo = Entry('server/exchangeinfo', 'public', 'GET', {'cost': 1}) + private_get_users_balances = privateGetUsersBalances = Entry('users/balances', 'private', 'GET', {'cost': 1}) + private_get_openorders = privateGetOpenOrders = Entry('openOrders', 'private', 'GET', {'cost': 1}) + private_get_allorders = privateGetAllOrders = Entry('allOrders', 'private', 'GET', {'cost': 1}) + private_get_users_transactions_trade = privateGetUsersTransactionsTrade = Entry('users/transactions/trade', 'private', 'GET', {'cost': 1}) + private_post_users_transactions_crypto = privatePostUsersTransactionsCrypto = Entry('users/transactions/crypto', 'private', 'POST', {'cost': 1}) + private_post_users_transactions_fiat = privatePostUsersTransactionsFiat = Entry('users/transactions/fiat', 'private', 'POST', {'cost': 1}) + private_post_order = privatePostOrder = Entry('order', 'private', 'POST', {'cost': 1}) + private_post_cancelorder = privatePostCancelOrder = Entry('cancelOrder', 'private', 'POST', {'cost': 1}) + private_delete_order = privateDeleteOrder = Entry('order', 'private', 'DELETE', {'cost': 1}) + graph_get_ohlcs = graphGetOhlcs = Entry('ohlcs', 'graph', 'GET', {'cost': 1}) + graph_get_klines_history = graphGetKlinesHistory = Entry('klines/history', 'graph', 'GET', {'cost': 1}) diff --git a/ccxt/abstract/bybit.py b/ccxt/abstract/bybit.py new file mode 100644 index 0000000..6f736e1 --- /dev/null +++ b/ccxt/abstract/bybit.py @@ -0,0 +1,325 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_spot_v3_public_symbols = publicGetSpotV3PublicSymbols = Entry('spot/v3/public/symbols', 'public', 'GET', {'cost': 1}) + public_get_spot_v3_public_quote_depth = publicGetSpotV3PublicQuoteDepth = Entry('spot/v3/public/quote/depth', 'public', 'GET', {'cost': 1}) + public_get_spot_v3_public_quote_depth_merged = publicGetSpotV3PublicQuoteDepthMerged = Entry('spot/v3/public/quote/depth/merged', 'public', 'GET', {'cost': 1}) + public_get_spot_v3_public_quote_trades = publicGetSpotV3PublicQuoteTrades = Entry('spot/v3/public/quote/trades', 'public', 'GET', {'cost': 1}) + public_get_spot_v3_public_quote_kline = publicGetSpotV3PublicQuoteKline = Entry('spot/v3/public/quote/kline', 'public', 'GET', {'cost': 1}) + public_get_spot_v3_public_quote_ticker_24hr = publicGetSpotV3PublicQuoteTicker24hr = Entry('spot/v3/public/quote/ticker/24hr', 'public', 'GET', {'cost': 1}) + public_get_spot_v3_public_quote_ticker_price = publicGetSpotV3PublicQuoteTickerPrice = Entry('spot/v3/public/quote/ticker/price', 'public', 'GET', {'cost': 1}) + public_get_spot_v3_public_quote_ticker_bookticker = publicGetSpotV3PublicQuoteTickerBookTicker = Entry('spot/v3/public/quote/ticker/bookTicker', 'public', 'GET', {'cost': 1}) + public_get_spot_v3_public_server_time = publicGetSpotV3PublicServerTime = Entry('spot/v3/public/server-time', 'public', 'GET', {'cost': 1}) + public_get_spot_v3_public_infos = publicGetSpotV3PublicInfos = Entry('spot/v3/public/infos', 'public', 'GET', {'cost': 1}) + public_get_spot_v3_public_margin_product_infos = publicGetSpotV3PublicMarginProductInfos = Entry('spot/v3/public/margin-product-infos', 'public', 'GET', {'cost': 1}) + public_get_spot_v3_public_margin_ensure_tokens = publicGetSpotV3PublicMarginEnsureTokens = Entry('spot/v3/public/margin-ensure-tokens', 'public', 'GET', {'cost': 1}) + public_get_v3_public_time = publicGetV3PublicTime = Entry('v3/public/time', 'public', 'GET', {'cost': 1}) + public_get_contract_v3_public_copytrading_symbol_list = publicGetContractV3PublicCopytradingSymbolList = Entry('contract/v3/public/copytrading/symbol/list', 'public', 'GET', {'cost': 1}) + public_get_derivatives_v3_public_order_book_l2 = publicGetDerivativesV3PublicOrderBookL2 = Entry('derivatives/v3/public/order-book/L2', 'public', 'GET', {'cost': 1}) + public_get_derivatives_v3_public_kline = publicGetDerivativesV3PublicKline = Entry('derivatives/v3/public/kline', 'public', 'GET', {'cost': 1}) + public_get_derivatives_v3_public_tickers = publicGetDerivativesV3PublicTickers = Entry('derivatives/v3/public/tickers', 'public', 'GET', {'cost': 1}) + public_get_derivatives_v3_public_instruments_info = publicGetDerivativesV3PublicInstrumentsInfo = Entry('derivatives/v3/public/instruments-info', 'public', 'GET', {'cost': 1}) + public_get_derivatives_v3_public_mark_price_kline = publicGetDerivativesV3PublicMarkPriceKline = Entry('derivatives/v3/public/mark-price-kline', 'public', 'GET', {'cost': 1}) + public_get_derivatives_v3_public_index_price_kline = publicGetDerivativesV3PublicIndexPriceKline = Entry('derivatives/v3/public/index-price-kline', 'public', 'GET', {'cost': 1}) + public_get_derivatives_v3_public_funding_history_funding_rate = publicGetDerivativesV3PublicFundingHistoryFundingRate = Entry('derivatives/v3/public/funding/history-funding-rate', 'public', 'GET', {'cost': 1}) + public_get_derivatives_v3_public_risk_limit_list = publicGetDerivativesV3PublicRiskLimitList = Entry('derivatives/v3/public/risk-limit/list', 'public', 'GET', {'cost': 1}) + public_get_derivatives_v3_public_delivery_price = publicGetDerivativesV3PublicDeliveryPrice = Entry('derivatives/v3/public/delivery-price', 'public', 'GET', {'cost': 1}) + public_get_derivatives_v3_public_recent_trade = publicGetDerivativesV3PublicRecentTrade = Entry('derivatives/v3/public/recent-trade', 'public', 'GET', {'cost': 1}) + public_get_derivatives_v3_public_open_interest = publicGetDerivativesV3PublicOpenInterest = Entry('derivatives/v3/public/open-interest', 'public', 'GET', {'cost': 1}) + public_get_derivatives_v3_public_insurance = publicGetDerivativesV3PublicInsurance = Entry('derivatives/v3/public/insurance', 'public', 'GET', {'cost': 1}) + public_get_v5_announcements_index = publicGetV5AnnouncementsIndex = Entry('v5/announcements/index', 'public', 'GET', {'cost': 5}) + public_get_v5_market_time = publicGetV5MarketTime = Entry('v5/market/time', 'public', 'GET', {'cost': 5}) + public_get_v5_market_kline = publicGetV5MarketKline = Entry('v5/market/kline', 'public', 'GET', {'cost': 5}) + public_get_v5_market_mark_price_kline = publicGetV5MarketMarkPriceKline = Entry('v5/market/mark-price-kline', 'public', 'GET', {'cost': 5}) + public_get_v5_market_index_price_kline = publicGetV5MarketIndexPriceKline = Entry('v5/market/index-price-kline', 'public', 'GET', {'cost': 5}) + public_get_v5_market_premium_index_price_kline = publicGetV5MarketPremiumIndexPriceKline = Entry('v5/market/premium-index-price-kline', 'public', 'GET', {'cost': 5}) + public_get_v5_market_instruments_info = publicGetV5MarketInstrumentsInfo = Entry('v5/market/instruments-info', 'public', 'GET', {'cost': 5}) + public_get_v5_market_orderbook = publicGetV5MarketOrderbook = Entry('v5/market/orderbook', 'public', 'GET', {'cost': 5}) + public_get_v5_market_tickers = publicGetV5MarketTickers = Entry('v5/market/tickers', 'public', 'GET', {'cost': 5}) + public_get_v5_market_funding_history = publicGetV5MarketFundingHistory = Entry('v5/market/funding/history', 'public', 'GET', {'cost': 5}) + public_get_v5_market_recent_trade = publicGetV5MarketRecentTrade = Entry('v5/market/recent-trade', 'public', 'GET', {'cost': 5}) + public_get_v5_market_open_interest = publicGetV5MarketOpenInterest = Entry('v5/market/open-interest', 'public', 'GET', {'cost': 5}) + public_get_v5_market_historical_volatility = publicGetV5MarketHistoricalVolatility = Entry('v5/market/historical-volatility', 'public', 'GET', {'cost': 5}) + public_get_v5_market_insurance = publicGetV5MarketInsurance = Entry('v5/market/insurance', 'public', 'GET', {'cost': 5}) + public_get_v5_market_risk_limit = publicGetV5MarketRiskLimit = Entry('v5/market/risk-limit', 'public', 'GET', {'cost': 5}) + public_get_v5_market_delivery_price = publicGetV5MarketDeliveryPrice = Entry('v5/market/delivery-price', 'public', 'GET', {'cost': 5}) + public_get_v5_market_account_ratio = publicGetV5MarketAccountRatio = Entry('v5/market/account-ratio', 'public', 'GET', {'cost': 5}) + public_get_v5_spot_lever_token_info = publicGetV5SpotLeverTokenInfo = Entry('v5/spot-lever-token/info', 'public', 'GET', {'cost': 5}) + public_get_v5_spot_lever_token_reference = publicGetV5SpotLeverTokenReference = Entry('v5/spot-lever-token/reference', 'public', 'GET', {'cost': 5}) + public_get_v5_spot_margin_trade_data = publicGetV5SpotMarginTradeData = Entry('v5/spot-margin-trade/data', 'public', 'GET', {'cost': 5}) + public_get_v5_spot_margin_trade_collateral = publicGetV5SpotMarginTradeCollateral = Entry('v5/spot-margin-trade/collateral', 'public', 'GET', {'cost': 5}) + public_get_v5_spot_cross_margin_trade_data = publicGetV5SpotCrossMarginTradeData = Entry('v5/spot-cross-margin-trade/data', 'public', 'GET', {'cost': 5}) + public_get_v5_spot_cross_margin_trade_pledge_token = publicGetV5SpotCrossMarginTradePledgeToken = Entry('v5/spot-cross-margin-trade/pledge-token', 'public', 'GET', {'cost': 5}) + public_get_v5_spot_cross_margin_trade_borrow_token = publicGetV5SpotCrossMarginTradeBorrowToken = Entry('v5/spot-cross-margin-trade/borrow-token', 'public', 'GET', {'cost': 5}) + public_get_v5_crypto_loan_collateral_data = publicGetV5CryptoLoanCollateralData = Entry('v5/crypto-loan/collateral-data', 'public', 'GET', {'cost': 5}) + public_get_v5_crypto_loan_loanable_data = publicGetV5CryptoLoanLoanableData = Entry('v5/crypto-loan/loanable-data', 'public', 'GET', {'cost': 5}) + public_get_v5_ins_loan_product_infos = publicGetV5InsLoanProductInfos = Entry('v5/ins-loan/product-infos', 'public', 'GET', {'cost': 5}) + public_get_v5_ins_loan_ensure_tokens_convert = publicGetV5InsLoanEnsureTokensConvert = Entry('v5/ins-loan/ensure-tokens-convert', 'public', 'GET', {'cost': 5}) + public_get_v5_earn_product = publicGetV5EarnProduct = Entry('v5/earn/product', 'public', 'GET', {'cost': 5}) + private_get_v5_market_instruments_info = privateGetV5MarketInstrumentsInfo = Entry('v5/market/instruments-info', 'private', 'GET', {'cost': 5}) + private_get_v2_private_wallet_fund_records = privateGetV2PrivateWalletFundRecords = Entry('v2/private/wallet/fund/records', 'private', 'GET', {'cost': 25}) + private_get_spot_v3_private_order = privateGetSpotV3PrivateOrder = Entry('spot/v3/private/order', 'private', 'GET', {'cost': 2.5}) + private_get_spot_v3_private_open_orders = privateGetSpotV3PrivateOpenOrders = Entry('spot/v3/private/open-orders', 'private', 'GET', {'cost': 2.5}) + private_get_spot_v3_private_history_orders = privateGetSpotV3PrivateHistoryOrders = Entry('spot/v3/private/history-orders', 'private', 'GET', {'cost': 2.5}) + private_get_spot_v3_private_my_trades = privateGetSpotV3PrivateMyTrades = Entry('spot/v3/private/my-trades', 'private', 'GET', {'cost': 2.5}) + private_get_spot_v3_private_account = privateGetSpotV3PrivateAccount = Entry('spot/v3/private/account', 'private', 'GET', {'cost': 2.5}) + private_get_spot_v3_private_reference = privateGetSpotV3PrivateReference = Entry('spot/v3/private/reference', 'private', 'GET', {'cost': 2.5}) + private_get_spot_v3_private_record = privateGetSpotV3PrivateRecord = Entry('spot/v3/private/record', 'private', 'GET', {'cost': 2.5}) + private_get_spot_v3_private_cross_margin_orders = privateGetSpotV3PrivateCrossMarginOrders = Entry('spot/v3/private/cross-margin-orders', 'private', 'GET', {'cost': 10}) + private_get_spot_v3_private_cross_margin_account = privateGetSpotV3PrivateCrossMarginAccount = Entry('spot/v3/private/cross-margin-account', 'private', 'GET', {'cost': 10}) + private_get_spot_v3_private_cross_margin_loan_info = privateGetSpotV3PrivateCrossMarginLoanInfo = Entry('spot/v3/private/cross-margin-loan-info', 'private', 'GET', {'cost': 10}) + private_get_spot_v3_private_cross_margin_repay_history = privateGetSpotV3PrivateCrossMarginRepayHistory = Entry('spot/v3/private/cross-margin-repay-history', 'private', 'GET', {'cost': 10}) + private_get_spot_v3_private_margin_loan_infos = privateGetSpotV3PrivateMarginLoanInfos = Entry('spot/v3/private/margin-loan-infos', 'private', 'GET', {'cost': 10}) + private_get_spot_v3_private_margin_repaid_infos = privateGetSpotV3PrivateMarginRepaidInfos = Entry('spot/v3/private/margin-repaid-infos', 'private', 'GET', {'cost': 10}) + private_get_spot_v3_private_margin_ltv = privateGetSpotV3PrivateMarginLtv = Entry('spot/v3/private/margin-ltv', 'private', 'GET', {'cost': 10}) + private_get_asset_v3_private_transfer_inter_transfer_list_query = privateGetAssetV3PrivateTransferInterTransferListQuery = Entry('asset/v3/private/transfer/inter-transfer/list/query', 'private', 'GET', {'cost': 50}) + private_get_asset_v3_private_transfer_sub_member_list_query = privateGetAssetV3PrivateTransferSubMemberListQuery = Entry('asset/v3/private/transfer/sub-member/list/query', 'private', 'GET', {'cost': 50}) + private_get_asset_v3_private_transfer_sub_member_transfer_list_query = privateGetAssetV3PrivateTransferSubMemberTransferListQuery = Entry('asset/v3/private/transfer/sub-member-transfer/list/query', 'private', 'GET', {'cost': 50}) + private_get_asset_v3_private_transfer_universal_transfer_list_query = privateGetAssetV3PrivateTransferUniversalTransferListQuery = Entry('asset/v3/private/transfer/universal-transfer/list/query', 'private', 'GET', {'cost': 25}) + private_get_asset_v3_private_coin_info_query = privateGetAssetV3PrivateCoinInfoQuery = Entry('asset/v3/private/coin-info/query', 'private', 'GET', {'cost': 25}) + private_get_asset_v3_private_deposit_address_query = privateGetAssetV3PrivateDepositAddressQuery = Entry('asset/v3/private/deposit/address/query', 'private', 'GET', {'cost': 10}) + private_get_contract_v3_private_copytrading_order_list = privateGetContractV3PrivateCopytradingOrderList = Entry('contract/v3/private/copytrading/order/list', 'private', 'GET', {'cost': 30}) + private_get_contract_v3_private_copytrading_position_list = privateGetContractV3PrivateCopytradingPositionList = Entry('contract/v3/private/copytrading/position/list', 'private', 'GET', {'cost': 40}) + private_get_contract_v3_private_copytrading_wallet_balance = privateGetContractV3PrivateCopytradingWalletBalance = Entry('contract/v3/private/copytrading/wallet/balance', 'private', 'GET', {'cost': 25}) + private_get_contract_v3_private_position_limit_info = privateGetContractV3PrivatePositionLimitInfo = Entry('contract/v3/private/position/limit-info', 'private', 'GET', {'cost': 25}) + private_get_contract_v3_private_order_unfilled_orders = privateGetContractV3PrivateOrderUnfilledOrders = Entry('contract/v3/private/order/unfilled-orders', 'private', 'GET', {'cost': 1}) + private_get_contract_v3_private_order_list = privateGetContractV3PrivateOrderList = Entry('contract/v3/private/order/list', 'private', 'GET', {'cost': 1}) + private_get_contract_v3_private_position_list = privateGetContractV3PrivatePositionList = Entry('contract/v3/private/position/list', 'private', 'GET', {'cost': 1}) + private_get_contract_v3_private_execution_list = privateGetContractV3PrivateExecutionList = Entry('contract/v3/private/execution/list', 'private', 'GET', {'cost': 1}) + private_get_contract_v3_private_position_closed_pnl = privateGetContractV3PrivatePositionClosedPnl = Entry('contract/v3/private/position/closed-pnl', 'private', 'GET', {'cost': 1}) + private_get_contract_v3_private_account_wallet_balance = privateGetContractV3PrivateAccountWalletBalance = Entry('contract/v3/private/account/wallet/balance', 'private', 'GET', {'cost': 1}) + private_get_contract_v3_private_account_fee_rate = privateGetContractV3PrivateAccountFeeRate = Entry('contract/v3/private/account/fee-rate', 'private', 'GET', {'cost': 1}) + private_get_contract_v3_private_account_wallet_fund_records = privateGetContractV3PrivateAccountWalletFundRecords = Entry('contract/v3/private/account/wallet/fund-records', 'private', 'GET', {'cost': 1}) + private_get_unified_v3_private_order_unfilled_orders = privateGetUnifiedV3PrivateOrderUnfilledOrders = Entry('unified/v3/private/order/unfilled-orders', 'private', 'GET', {'cost': 1}) + private_get_unified_v3_private_order_list = privateGetUnifiedV3PrivateOrderList = Entry('unified/v3/private/order/list', 'private', 'GET', {'cost': 1}) + private_get_unified_v3_private_position_list = privateGetUnifiedV3PrivatePositionList = Entry('unified/v3/private/position/list', 'private', 'GET', {'cost': 1}) + private_get_unified_v3_private_execution_list = privateGetUnifiedV3PrivateExecutionList = Entry('unified/v3/private/execution/list', 'private', 'GET', {'cost': 1}) + private_get_unified_v3_private_delivery_record = privateGetUnifiedV3PrivateDeliveryRecord = Entry('unified/v3/private/delivery-record', 'private', 'GET', {'cost': 1}) + private_get_unified_v3_private_settlement_record = privateGetUnifiedV3PrivateSettlementRecord = Entry('unified/v3/private/settlement-record', 'private', 'GET', {'cost': 1}) + private_get_unified_v3_private_account_wallet_balance = privateGetUnifiedV3PrivateAccountWalletBalance = Entry('unified/v3/private/account/wallet/balance', 'private', 'GET', {'cost': 1}) + private_get_unified_v3_private_account_transaction_log = privateGetUnifiedV3PrivateAccountTransactionLog = Entry('unified/v3/private/account/transaction-log', 'private', 'GET', {'cost': 1}) + private_get_unified_v3_private_account_borrow_history = privateGetUnifiedV3PrivateAccountBorrowHistory = Entry('unified/v3/private/account/borrow-history', 'private', 'GET', {'cost': 1}) + private_get_unified_v3_private_account_borrow_rate = privateGetUnifiedV3PrivateAccountBorrowRate = Entry('unified/v3/private/account/borrow-rate', 'private', 'GET', {'cost': 1}) + private_get_unified_v3_private_account_info = privateGetUnifiedV3PrivateAccountInfo = Entry('unified/v3/private/account/info', 'private', 'GET', {'cost': 1}) + private_get_user_v3_private_frozen_sub_member = privateGetUserV3PrivateFrozenSubMember = Entry('user/v3/private/frozen-sub-member', 'private', 'GET', {'cost': 10}) + private_get_user_v3_private_query_sub_members = privateGetUserV3PrivateQuerySubMembers = Entry('user/v3/private/query-sub-members', 'private', 'GET', {'cost': 5}) + private_get_user_v3_private_query_api = privateGetUserV3PrivateQueryApi = Entry('user/v3/private/query-api', 'private', 'GET', {'cost': 5}) + private_get_user_v3_private_get_member_type = privateGetUserV3PrivateGetMemberType = Entry('user/v3/private/get-member-type', 'private', 'GET', {'cost': 1}) + private_get_asset_v3_private_transfer_transfer_coin_list_query = privateGetAssetV3PrivateTransferTransferCoinListQuery = Entry('asset/v3/private/transfer/transfer-coin/list/query', 'private', 'GET', {'cost': 50}) + private_get_asset_v3_private_transfer_account_coin_balance_query = privateGetAssetV3PrivateTransferAccountCoinBalanceQuery = Entry('asset/v3/private/transfer/account-coin/balance/query', 'private', 'GET', {'cost': 50}) + private_get_asset_v3_private_transfer_account_coins_balance_query = privateGetAssetV3PrivateTransferAccountCoinsBalanceQuery = Entry('asset/v3/private/transfer/account-coins/balance/query', 'private', 'GET', {'cost': 25}) + private_get_asset_v3_private_transfer_asset_info_query = privateGetAssetV3PrivateTransferAssetInfoQuery = Entry('asset/v3/private/transfer/asset-info/query', 'private', 'GET', {'cost': 50}) + private_get_asset_v3_public_deposit_allowed_deposit_list_query = privateGetAssetV3PublicDepositAllowedDepositListQuery = Entry('asset/v3/public/deposit/allowed-deposit-list/query', 'private', 'GET', {'cost': 0.17}) + private_get_asset_v3_private_deposit_record_query = privateGetAssetV3PrivateDepositRecordQuery = Entry('asset/v3/private/deposit/record/query', 'private', 'GET', {'cost': 10}) + private_get_asset_v3_private_withdraw_record_query = privateGetAssetV3PrivateWithdrawRecordQuery = Entry('asset/v3/private/withdraw/record/query', 'private', 'GET', {'cost': 10}) + private_get_v5_order_realtime = privateGetV5OrderRealtime = Entry('v5/order/realtime', 'private', 'GET', {'cost': 5}) + private_get_v5_order_history = privateGetV5OrderHistory = Entry('v5/order/history', 'private', 'GET', {'cost': 5}) + private_get_v5_order_spot_borrow_check = privateGetV5OrderSpotBorrowCheck = Entry('v5/order/spot-borrow-check', 'private', 'GET', {'cost': 1}) + private_get_v5_position_list = privateGetV5PositionList = Entry('v5/position/list', 'private', 'GET', {'cost': 5}) + private_get_v5_execution_list = privateGetV5ExecutionList = Entry('v5/execution/list', 'private', 'GET', {'cost': 5}) + private_get_v5_position_closed_pnl = privateGetV5PositionClosedPnl = Entry('v5/position/closed-pnl', 'private', 'GET', {'cost': 5}) + private_get_v5_position_move_history = privateGetV5PositionMoveHistory = Entry('v5/position/move-history', 'private', 'GET', {'cost': 5}) + private_get_v5_pre_upgrade_order_history = privateGetV5PreUpgradeOrderHistory = Entry('v5/pre-upgrade/order/history', 'private', 'GET', {'cost': 5}) + private_get_v5_pre_upgrade_execution_list = privateGetV5PreUpgradeExecutionList = Entry('v5/pre-upgrade/execution/list', 'private', 'GET', {'cost': 5}) + private_get_v5_pre_upgrade_position_closed_pnl = privateGetV5PreUpgradePositionClosedPnl = Entry('v5/pre-upgrade/position/closed-pnl', 'private', 'GET', {'cost': 5}) + private_get_v5_pre_upgrade_account_transaction_log = privateGetV5PreUpgradeAccountTransactionLog = Entry('v5/pre-upgrade/account/transaction-log', 'private', 'GET', {'cost': 5}) + private_get_v5_pre_upgrade_asset_delivery_record = privateGetV5PreUpgradeAssetDeliveryRecord = Entry('v5/pre-upgrade/asset/delivery-record', 'private', 'GET', {'cost': 5}) + private_get_v5_pre_upgrade_asset_settlement_record = privateGetV5PreUpgradeAssetSettlementRecord = Entry('v5/pre-upgrade/asset/settlement-record', 'private', 'GET', {'cost': 5}) + private_get_v5_account_wallet_balance = privateGetV5AccountWalletBalance = Entry('v5/account/wallet-balance', 'private', 'GET', {'cost': 1}) + private_get_v5_account_borrow_history = privateGetV5AccountBorrowHistory = Entry('v5/account/borrow-history', 'private', 'GET', {'cost': 1}) + private_get_v5_account_instruments_info = privateGetV5AccountInstrumentsInfo = Entry('v5/account/instruments-info', 'private', 'GET', {'cost': 1}) + private_get_v5_account_collateral_info = privateGetV5AccountCollateralInfo = Entry('v5/account/collateral-info', 'private', 'GET', {'cost': 1}) + private_get_v5_asset_coin_greeks = privateGetV5AssetCoinGreeks = Entry('v5/asset/coin-greeks', 'private', 'GET', {'cost': 1}) + private_get_v5_account_fee_rate = privateGetV5AccountFeeRate = Entry('v5/account/fee-rate', 'private', 'GET', {'cost': 10}) + private_get_v5_account_info = privateGetV5AccountInfo = Entry('v5/account/info', 'private', 'GET', {'cost': 5}) + private_get_v5_account_transaction_log = privateGetV5AccountTransactionLog = Entry('v5/account/transaction-log', 'private', 'GET', {'cost': 1}) + private_get_v5_account_contract_transaction_log = privateGetV5AccountContractTransactionLog = Entry('v5/account/contract-transaction-log', 'private', 'GET', {'cost': 1}) + private_get_v5_account_smp_group = privateGetV5AccountSmpGroup = Entry('v5/account/smp-group', 'private', 'GET', {'cost': 1}) + private_get_v5_account_mmp_state = privateGetV5AccountMmpState = Entry('v5/account/mmp-state', 'private', 'GET', {'cost': 5}) + private_get_v5_account_withdrawal = privateGetV5AccountWithdrawal = Entry('v5/account/withdrawal', 'private', 'GET', {'cost': 5}) + private_get_v5_asset_exchange_query_coin_list = privateGetV5AssetExchangeQueryCoinList = Entry('v5/asset/exchange/query-coin-list', 'private', 'GET', {'cost': 0.5}) + private_get_v5_asset_exchange_convert_result_query = privateGetV5AssetExchangeConvertResultQuery = Entry('v5/asset/exchange/convert-result-query', 'private', 'GET', {'cost': 0.5}) + private_get_v5_asset_exchange_query_convert_history = privateGetV5AssetExchangeQueryConvertHistory = Entry('v5/asset/exchange/query-convert-history', 'private', 'GET', {'cost': 0.5}) + private_get_v5_asset_exchange_order_record = privateGetV5AssetExchangeOrderRecord = Entry('v5/asset/exchange/order-record', 'private', 'GET', {'cost': 5}) + private_get_v5_asset_delivery_record = privateGetV5AssetDeliveryRecord = Entry('v5/asset/delivery-record', 'private', 'GET', {'cost': 5}) + private_get_v5_asset_settlement_record = privateGetV5AssetSettlementRecord = Entry('v5/asset/settlement-record', 'private', 'GET', {'cost': 5}) + private_get_v5_asset_transfer_query_asset_info = privateGetV5AssetTransferQueryAssetInfo = Entry('v5/asset/transfer/query-asset-info', 'private', 'GET', {'cost': 50}) + private_get_v5_asset_transfer_query_account_coins_balance = privateGetV5AssetTransferQueryAccountCoinsBalance = Entry('v5/asset/transfer/query-account-coins-balance', 'private', 'GET', {'cost': 25}) + private_get_v5_asset_transfer_query_account_coin_balance = privateGetV5AssetTransferQueryAccountCoinBalance = Entry('v5/asset/transfer/query-account-coin-balance', 'private', 'GET', {'cost': 50}) + private_get_v5_asset_transfer_query_transfer_coin_list = privateGetV5AssetTransferQueryTransferCoinList = Entry('v5/asset/transfer/query-transfer-coin-list', 'private', 'GET', {'cost': 50}) + private_get_v5_asset_transfer_query_inter_transfer_list = privateGetV5AssetTransferQueryInterTransferList = Entry('v5/asset/transfer/query-inter-transfer-list', 'private', 'GET', {'cost': 50}) + private_get_v5_asset_transfer_query_sub_member_list = privateGetV5AssetTransferQuerySubMemberList = Entry('v5/asset/transfer/query-sub-member-list', 'private', 'GET', {'cost': 50}) + private_get_v5_asset_transfer_query_universal_transfer_list = privateGetV5AssetTransferQueryUniversalTransferList = Entry('v5/asset/transfer/query-universal-transfer-list', 'private', 'GET', {'cost': 25}) + private_get_v5_asset_deposit_query_allowed_list = privateGetV5AssetDepositQueryAllowedList = Entry('v5/asset/deposit/query-allowed-list', 'private', 'GET', {'cost': 5}) + private_get_v5_asset_deposit_query_record = privateGetV5AssetDepositQueryRecord = Entry('v5/asset/deposit/query-record', 'private', 'GET', {'cost': 10}) + private_get_v5_asset_deposit_query_sub_member_record = privateGetV5AssetDepositQuerySubMemberRecord = Entry('v5/asset/deposit/query-sub-member-record', 'private', 'GET', {'cost': 10}) + private_get_v5_asset_deposit_query_internal_record = privateGetV5AssetDepositQueryInternalRecord = Entry('v5/asset/deposit/query-internal-record', 'private', 'GET', {'cost': 5}) + private_get_v5_asset_deposit_query_address = privateGetV5AssetDepositQueryAddress = Entry('v5/asset/deposit/query-address', 'private', 'GET', {'cost': 10}) + private_get_v5_asset_deposit_query_sub_member_address = privateGetV5AssetDepositQuerySubMemberAddress = Entry('v5/asset/deposit/query-sub-member-address', 'private', 'GET', {'cost': 10}) + private_get_v5_asset_coin_query_info = privateGetV5AssetCoinQueryInfo = Entry('v5/asset/coin/query-info', 'private', 'GET', {'cost': 28}) + private_get_v5_asset_withdraw_query_address = privateGetV5AssetWithdrawQueryAddress = Entry('v5/asset/withdraw/query-address', 'private', 'GET', {'cost': 10}) + private_get_v5_asset_withdraw_query_record = privateGetV5AssetWithdrawQueryRecord = Entry('v5/asset/withdraw/query-record', 'private', 'GET', {'cost': 10}) + private_get_v5_asset_withdraw_withdrawable_amount = privateGetV5AssetWithdrawWithdrawableAmount = Entry('v5/asset/withdraw/withdrawable-amount', 'private', 'GET', {'cost': 5}) + private_get_v5_asset_withdraw_vasp_list = privateGetV5AssetWithdrawVaspList = Entry('v5/asset/withdraw/vasp/list', 'private', 'GET', {'cost': 5}) + private_get_v5_user_query_sub_members = privateGetV5UserQuerySubMembers = Entry('v5/user/query-sub-members', 'private', 'GET', {'cost': 5}) + private_get_v5_user_query_api = privateGetV5UserQueryApi = Entry('v5/user/query-api', 'private', 'GET', {'cost': 5}) + private_get_v5_user_sub_apikeys = privateGetV5UserSubApikeys = Entry('v5/user/sub-apikeys', 'private', 'GET', {'cost': 5}) + private_get_v5_user_get_member_type = privateGetV5UserGetMemberType = Entry('v5/user/get-member-type', 'private', 'GET', {'cost': 5}) + private_get_v5_user_aff_customer_info = privateGetV5UserAffCustomerInfo = Entry('v5/user/aff-customer-info', 'private', 'GET', {'cost': 5}) + private_get_v5_user_del_submember = privateGetV5UserDelSubmember = Entry('v5/user/del-submember', 'private', 'GET', {'cost': 5}) + private_get_v5_user_submembers = privateGetV5UserSubmembers = Entry('v5/user/submembers', 'private', 'GET', {'cost': 5}) + private_get_v5_affiliate_aff_user_list = privateGetV5AffiliateAffUserList = Entry('v5/affiliate/aff-user-list', 'private', 'GET', {'cost': 5}) + private_get_v5_spot_lever_token_order_record = privateGetV5SpotLeverTokenOrderRecord = Entry('v5/spot-lever-token/order-record', 'private', 'GET', {'cost': 1}) + private_get_v5_spot_margin_trade_interest_rate_history = privateGetV5SpotMarginTradeInterestRateHistory = Entry('v5/spot-margin-trade/interest-rate-history', 'private', 'GET', {'cost': 5}) + private_get_v5_spot_margin_trade_state = privateGetV5SpotMarginTradeState = Entry('v5/spot-margin-trade/state', 'private', 'GET', {'cost': 5}) + private_get_v5_spot_margin_trade_max_borrowable = privateGetV5SpotMarginTradeMaxBorrowable = Entry('v5/spot-margin-trade/max-borrowable', 'private', 'GET', {'cost': 5}) + private_get_v5_spot_margin_trade_position_tiers = privateGetV5SpotMarginTradePositionTiers = Entry('v5/spot-margin-trade/position-tiers', 'private', 'GET', {'cost': 5}) + private_get_v5_spot_margin_trade_coinstate = privateGetV5SpotMarginTradeCoinstate = Entry('v5/spot-margin-trade/coinstate', 'private', 'GET', {'cost': 5}) + private_get_v5_spot_margin_trade_repayment_available_amount = privateGetV5SpotMarginTradeRepaymentAvailableAmount = Entry('v5/spot-margin-trade/repayment-available-amount', 'private', 'GET', {'cost': 5}) + private_get_v5_spot_cross_margin_trade_loan_info = privateGetV5SpotCrossMarginTradeLoanInfo = Entry('v5/spot-cross-margin-trade/loan-info', 'private', 'GET', {'cost': 1}) + private_get_v5_spot_cross_margin_trade_account = privateGetV5SpotCrossMarginTradeAccount = Entry('v5/spot-cross-margin-trade/account', 'private', 'GET', {'cost': 1}) + private_get_v5_spot_cross_margin_trade_orders = privateGetV5SpotCrossMarginTradeOrders = Entry('v5/spot-cross-margin-trade/orders', 'private', 'GET', {'cost': 1}) + private_get_v5_spot_cross_margin_trade_repay_history = privateGetV5SpotCrossMarginTradeRepayHistory = Entry('v5/spot-cross-margin-trade/repay-history', 'private', 'GET', {'cost': 1}) + private_get_v5_crypto_loan_borrowable_collateralisable_number = privateGetV5CryptoLoanBorrowableCollateralisableNumber = Entry('v5/crypto-loan/borrowable-collateralisable-number', 'private', 'GET', {'cost': 5}) + private_get_v5_crypto_loan_ongoing_orders = privateGetV5CryptoLoanOngoingOrders = Entry('v5/crypto-loan/ongoing-orders', 'private', 'GET', {'cost': 5}) + private_get_v5_crypto_loan_repayment_history = privateGetV5CryptoLoanRepaymentHistory = Entry('v5/crypto-loan/repayment-history', 'private', 'GET', {'cost': 5}) + private_get_v5_crypto_loan_borrow_history = privateGetV5CryptoLoanBorrowHistory = Entry('v5/crypto-loan/borrow-history', 'private', 'GET', {'cost': 5}) + private_get_v5_crypto_loan_max_collateral_amount = privateGetV5CryptoLoanMaxCollateralAmount = Entry('v5/crypto-loan/max-collateral-amount', 'private', 'GET', {'cost': 5}) + private_get_v5_crypto_loan_adjustment_history = privateGetV5CryptoLoanAdjustmentHistory = Entry('v5/crypto-loan/adjustment-history', 'private', 'GET', {'cost': 5}) + private_get_v5_ins_loan_product_infos = privateGetV5InsLoanProductInfos = Entry('v5/ins-loan/product-infos', 'private', 'GET', {'cost': 5}) + private_get_v5_ins_loan_ensure_tokens_convert = privateGetV5InsLoanEnsureTokensConvert = Entry('v5/ins-loan/ensure-tokens-convert', 'private', 'GET', {'cost': 5}) + private_get_v5_ins_loan_loan_order = privateGetV5InsLoanLoanOrder = Entry('v5/ins-loan/loan-order', 'private', 'GET', {'cost': 5}) + private_get_v5_ins_loan_repaid_history = privateGetV5InsLoanRepaidHistory = Entry('v5/ins-loan/repaid-history', 'private', 'GET', {'cost': 5}) + private_get_v5_ins_loan_ltv_convert = privateGetV5InsLoanLtvConvert = Entry('v5/ins-loan/ltv-convert', 'private', 'GET', {'cost': 5}) + private_get_v5_lending_info = privateGetV5LendingInfo = Entry('v5/lending/info', 'private', 'GET', {'cost': 5}) + private_get_v5_lending_history_order = privateGetV5LendingHistoryOrder = Entry('v5/lending/history-order', 'private', 'GET', {'cost': 5}) + private_get_v5_lending_account = privateGetV5LendingAccount = Entry('v5/lending/account', 'private', 'GET', {'cost': 5}) + private_get_v5_broker_earning_record = privateGetV5BrokerEarningRecord = Entry('v5/broker/earning-record', 'private', 'GET', {'cost': 5}) + private_get_v5_broker_earnings_info = privateGetV5BrokerEarningsInfo = Entry('v5/broker/earnings-info', 'private', 'GET', {'cost': 5}) + private_get_v5_broker_account_info = privateGetV5BrokerAccountInfo = Entry('v5/broker/account-info', 'private', 'GET', {'cost': 5}) + private_get_v5_broker_asset_query_sub_member_deposit_record = privateGetV5BrokerAssetQuerySubMemberDepositRecord = Entry('v5/broker/asset/query-sub-member-deposit-record', 'private', 'GET', {'cost': 10}) + private_get_v5_earn_product = privateGetV5EarnProduct = Entry('v5/earn/product', 'private', 'GET', {'cost': 5}) + private_get_v5_earn_order = privateGetV5EarnOrder = Entry('v5/earn/order', 'private', 'GET', {'cost': 5}) + private_get_v5_earn_position = privateGetV5EarnPosition = Entry('v5/earn/position', 'private', 'GET', {'cost': 5}) + private_get_v5_earn_yield = privateGetV5EarnYield = Entry('v5/earn/yield', 'private', 'GET', {'cost': 5}) + private_get_v5_earn_hourly_yield = privateGetV5EarnHourlyYield = Entry('v5/earn/hourly-yield', 'private', 'GET', {'cost': 5}) + private_post_spot_v3_private_order = privatePostSpotV3PrivateOrder = Entry('spot/v3/private/order', 'private', 'POST', {'cost': 2.5}) + private_post_spot_v3_private_cancel_order = privatePostSpotV3PrivateCancelOrder = Entry('spot/v3/private/cancel-order', 'private', 'POST', {'cost': 2.5}) + private_post_spot_v3_private_cancel_orders = privatePostSpotV3PrivateCancelOrders = Entry('spot/v3/private/cancel-orders', 'private', 'POST', {'cost': 2.5}) + private_post_spot_v3_private_cancel_orders_by_ids = privatePostSpotV3PrivateCancelOrdersByIds = Entry('spot/v3/private/cancel-orders-by-ids', 'private', 'POST', {'cost': 2.5}) + private_post_spot_v3_private_purchase = privatePostSpotV3PrivatePurchase = Entry('spot/v3/private/purchase', 'private', 'POST', {'cost': 2.5}) + private_post_spot_v3_private_redeem = privatePostSpotV3PrivateRedeem = Entry('spot/v3/private/redeem', 'private', 'POST', {'cost': 2.5}) + private_post_spot_v3_private_cross_margin_loan = privatePostSpotV3PrivateCrossMarginLoan = Entry('spot/v3/private/cross-margin-loan', 'private', 'POST', {'cost': 10}) + private_post_spot_v3_private_cross_margin_repay = privatePostSpotV3PrivateCrossMarginRepay = Entry('spot/v3/private/cross-margin-repay', 'private', 'POST', {'cost': 10}) + private_post_asset_v3_private_transfer_inter_transfer = privatePostAssetV3PrivateTransferInterTransfer = Entry('asset/v3/private/transfer/inter-transfer', 'private', 'POST', {'cost': 150}) + private_post_asset_v3_private_withdraw_create = privatePostAssetV3PrivateWithdrawCreate = Entry('asset/v3/private/withdraw/create', 'private', 'POST', {'cost': 300}) + private_post_asset_v3_private_withdraw_cancel = privatePostAssetV3PrivateWithdrawCancel = Entry('asset/v3/private/withdraw/cancel', 'private', 'POST', {'cost': 50}) + private_post_asset_v3_private_transfer_sub_member_transfer = privatePostAssetV3PrivateTransferSubMemberTransfer = Entry('asset/v3/private/transfer/sub-member-transfer', 'private', 'POST', {'cost': 150}) + private_post_asset_v3_private_transfer_transfer_sub_member_save = privatePostAssetV3PrivateTransferTransferSubMemberSave = Entry('asset/v3/private/transfer/transfer-sub-member-save', 'private', 'POST', {'cost': 150}) + private_post_asset_v3_private_transfer_universal_transfer = privatePostAssetV3PrivateTransferUniversalTransfer = Entry('asset/v3/private/transfer/universal-transfer', 'private', 'POST', {'cost': 10}) + private_post_user_v3_private_create_sub_member = privatePostUserV3PrivateCreateSubMember = Entry('user/v3/private/create-sub-member', 'private', 'POST', {'cost': 10}) + private_post_user_v3_private_create_sub_api = privatePostUserV3PrivateCreateSubApi = Entry('user/v3/private/create-sub-api', 'private', 'POST', {'cost': 10}) + private_post_user_v3_private_update_api = privatePostUserV3PrivateUpdateApi = Entry('user/v3/private/update-api', 'private', 'POST', {'cost': 10}) + private_post_user_v3_private_delete_api = privatePostUserV3PrivateDeleteApi = Entry('user/v3/private/delete-api', 'private', 'POST', {'cost': 10}) + private_post_user_v3_private_update_sub_api = privatePostUserV3PrivateUpdateSubApi = Entry('user/v3/private/update-sub-api', 'private', 'POST', {'cost': 10}) + private_post_user_v3_private_delete_sub_api = privatePostUserV3PrivateDeleteSubApi = Entry('user/v3/private/delete-sub-api', 'private', 'POST', {'cost': 10}) + private_post_contract_v3_private_copytrading_order_create = privatePostContractV3PrivateCopytradingOrderCreate = Entry('contract/v3/private/copytrading/order/create', 'private', 'POST', {'cost': 30}) + private_post_contract_v3_private_copytrading_order_cancel = privatePostContractV3PrivateCopytradingOrderCancel = Entry('contract/v3/private/copytrading/order/cancel', 'private', 'POST', {'cost': 30}) + private_post_contract_v3_private_copytrading_order_close = privatePostContractV3PrivateCopytradingOrderClose = Entry('contract/v3/private/copytrading/order/close', 'private', 'POST', {'cost': 30}) + private_post_contract_v3_private_copytrading_position_close = privatePostContractV3PrivateCopytradingPositionClose = Entry('contract/v3/private/copytrading/position/close', 'private', 'POST', {'cost': 40}) + private_post_contract_v3_private_copytrading_position_set_leverage = privatePostContractV3PrivateCopytradingPositionSetLeverage = Entry('contract/v3/private/copytrading/position/set-leverage', 'private', 'POST', {'cost': 40}) + private_post_contract_v3_private_copytrading_wallet_transfer = privatePostContractV3PrivateCopytradingWalletTransfer = Entry('contract/v3/private/copytrading/wallet/transfer', 'private', 'POST', {'cost': 25}) + private_post_contract_v3_private_copytrading_order_trading_stop = privatePostContractV3PrivateCopytradingOrderTradingStop = Entry('contract/v3/private/copytrading/order/trading-stop', 'private', 'POST', {'cost': 2.5}) + private_post_contract_v3_private_order_create = privatePostContractV3PrivateOrderCreate = Entry('contract/v3/private/order/create', 'private', 'POST', {'cost': 1}) + private_post_contract_v3_private_order_cancel = privatePostContractV3PrivateOrderCancel = Entry('contract/v3/private/order/cancel', 'private', 'POST', {'cost': 1}) + private_post_contract_v3_private_order_cancel_all = privatePostContractV3PrivateOrderCancelAll = Entry('contract/v3/private/order/cancel-all', 'private', 'POST', {'cost': 1}) + private_post_contract_v3_private_order_replace = privatePostContractV3PrivateOrderReplace = Entry('contract/v3/private/order/replace', 'private', 'POST', {'cost': 1}) + private_post_contract_v3_private_position_set_auto_add_margin = privatePostContractV3PrivatePositionSetAutoAddMargin = Entry('contract/v3/private/position/set-auto-add-margin', 'private', 'POST', {'cost': 1}) + private_post_contract_v3_private_position_switch_isolated = privatePostContractV3PrivatePositionSwitchIsolated = Entry('contract/v3/private/position/switch-isolated', 'private', 'POST', {'cost': 1}) + private_post_contract_v3_private_position_switch_mode = privatePostContractV3PrivatePositionSwitchMode = Entry('contract/v3/private/position/switch-mode', 'private', 'POST', {'cost': 1}) + private_post_contract_v3_private_position_switch_tpsl_mode = privatePostContractV3PrivatePositionSwitchTpslMode = Entry('contract/v3/private/position/switch-tpsl-mode', 'private', 'POST', {'cost': 1}) + private_post_contract_v3_private_position_set_leverage = privatePostContractV3PrivatePositionSetLeverage = Entry('contract/v3/private/position/set-leverage', 'private', 'POST', {'cost': 1}) + private_post_contract_v3_private_position_trading_stop = privatePostContractV3PrivatePositionTradingStop = Entry('contract/v3/private/position/trading-stop', 'private', 'POST', {'cost': 1}) + private_post_contract_v3_private_position_set_risk_limit = privatePostContractV3PrivatePositionSetRiskLimit = Entry('contract/v3/private/position/set-risk-limit', 'private', 'POST', {'cost': 1}) + private_post_contract_v3_private_account_setmarginmode = privatePostContractV3PrivateAccountSetMarginMode = Entry('contract/v3/private/account/setMarginMode', 'private', 'POST', {'cost': 1}) + private_post_unified_v3_private_order_create = privatePostUnifiedV3PrivateOrderCreate = Entry('unified/v3/private/order/create', 'private', 'POST', {'cost': 30}) + private_post_unified_v3_private_order_replace = privatePostUnifiedV3PrivateOrderReplace = Entry('unified/v3/private/order/replace', 'private', 'POST', {'cost': 30}) + private_post_unified_v3_private_order_cancel = privatePostUnifiedV3PrivateOrderCancel = Entry('unified/v3/private/order/cancel', 'private', 'POST', {'cost': 30}) + private_post_unified_v3_private_order_create_batch = privatePostUnifiedV3PrivateOrderCreateBatch = Entry('unified/v3/private/order/create-batch', 'private', 'POST', {'cost': 30}) + private_post_unified_v3_private_order_replace_batch = privatePostUnifiedV3PrivateOrderReplaceBatch = Entry('unified/v3/private/order/replace-batch', 'private', 'POST', {'cost': 30}) + private_post_unified_v3_private_order_cancel_batch = privatePostUnifiedV3PrivateOrderCancelBatch = Entry('unified/v3/private/order/cancel-batch', 'private', 'POST', {'cost': 30}) + private_post_unified_v3_private_order_cancel_all = privatePostUnifiedV3PrivateOrderCancelAll = Entry('unified/v3/private/order/cancel-all', 'private', 'POST', {'cost': 30}) + private_post_unified_v3_private_position_set_leverage = privatePostUnifiedV3PrivatePositionSetLeverage = Entry('unified/v3/private/position/set-leverage', 'private', 'POST', {'cost': 2.5}) + private_post_unified_v3_private_position_tpsl_switch_mode = privatePostUnifiedV3PrivatePositionTpslSwitchMode = Entry('unified/v3/private/position/tpsl/switch-mode', 'private', 'POST', {'cost': 2.5}) + private_post_unified_v3_private_position_set_risk_limit = privatePostUnifiedV3PrivatePositionSetRiskLimit = Entry('unified/v3/private/position/set-risk-limit', 'private', 'POST', {'cost': 2.5}) + private_post_unified_v3_private_position_trading_stop = privatePostUnifiedV3PrivatePositionTradingStop = Entry('unified/v3/private/position/trading-stop', 'private', 'POST', {'cost': 2.5}) + private_post_unified_v3_private_account_upgrade_unified_account = privatePostUnifiedV3PrivateAccountUpgradeUnifiedAccount = Entry('unified/v3/private/account/upgrade-unified-account', 'private', 'POST', {'cost': 2.5}) + private_post_unified_v3_private_account_setmarginmode = privatePostUnifiedV3PrivateAccountSetMarginMode = Entry('unified/v3/private/account/setMarginMode', 'private', 'POST', {'cost': 2.5}) + private_post_fht_compliance_tax_v3_private_registertime = privatePostFhtComplianceTaxV3PrivateRegistertime = Entry('fht/compliance/tax/v3/private/registertime', 'private', 'POST', {'cost': 50}) + private_post_fht_compliance_tax_v3_private_create = privatePostFhtComplianceTaxV3PrivateCreate = Entry('fht/compliance/tax/v3/private/create', 'private', 'POST', {'cost': 50}) + private_post_fht_compliance_tax_v3_private_status = privatePostFhtComplianceTaxV3PrivateStatus = Entry('fht/compliance/tax/v3/private/status', 'private', 'POST', {'cost': 50}) + private_post_fht_compliance_tax_v3_private_url = privatePostFhtComplianceTaxV3PrivateUrl = Entry('fht/compliance/tax/v3/private/url', 'private', 'POST', {'cost': 50}) + private_post_v5_order_create = privatePostV5OrderCreate = Entry('v5/order/create', 'private', 'POST', {'cost': 2.5}) + private_post_v5_order_amend = privatePostV5OrderAmend = Entry('v5/order/amend', 'private', 'POST', {'cost': 5}) + private_post_v5_order_cancel = privatePostV5OrderCancel = Entry('v5/order/cancel', 'private', 'POST', {'cost': 2.5}) + private_post_v5_order_cancel_all = privatePostV5OrderCancelAll = Entry('v5/order/cancel-all', 'private', 'POST', {'cost': 50}) + private_post_v5_order_create_batch = privatePostV5OrderCreateBatch = Entry('v5/order/create-batch', 'private', 'POST', {'cost': 5}) + private_post_v5_order_amend_batch = privatePostV5OrderAmendBatch = Entry('v5/order/amend-batch', 'private', 'POST', {'cost': 5}) + private_post_v5_order_cancel_batch = privatePostV5OrderCancelBatch = Entry('v5/order/cancel-batch', 'private', 'POST', {'cost': 5}) + private_post_v5_order_disconnected_cancel_all = privatePostV5OrderDisconnectedCancelAll = Entry('v5/order/disconnected-cancel-all', 'private', 'POST', {'cost': 5}) + private_post_v5_position_set_leverage = privatePostV5PositionSetLeverage = Entry('v5/position/set-leverage', 'private', 'POST', {'cost': 5}) + private_post_v5_position_switch_isolated = privatePostV5PositionSwitchIsolated = Entry('v5/position/switch-isolated', 'private', 'POST', {'cost': 5}) + private_post_v5_position_set_tpsl_mode = privatePostV5PositionSetTpslMode = Entry('v5/position/set-tpsl-mode', 'private', 'POST', {'cost': 5}) + private_post_v5_position_switch_mode = privatePostV5PositionSwitchMode = Entry('v5/position/switch-mode', 'private', 'POST', {'cost': 5}) + private_post_v5_position_set_risk_limit = privatePostV5PositionSetRiskLimit = Entry('v5/position/set-risk-limit', 'private', 'POST', {'cost': 5}) + private_post_v5_position_trading_stop = privatePostV5PositionTradingStop = Entry('v5/position/trading-stop', 'private', 'POST', {'cost': 5}) + private_post_v5_position_set_auto_add_margin = privatePostV5PositionSetAutoAddMargin = Entry('v5/position/set-auto-add-margin', 'private', 'POST', {'cost': 5}) + private_post_v5_position_add_margin = privatePostV5PositionAddMargin = Entry('v5/position/add-margin', 'private', 'POST', {'cost': 5}) + private_post_v5_position_move_positions = privatePostV5PositionMovePositions = Entry('v5/position/move-positions', 'private', 'POST', {'cost': 5}) + private_post_v5_position_confirm_pending_mmr = privatePostV5PositionConfirmPendingMmr = Entry('v5/position/confirm-pending-mmr', 'private', 'POST', {'cost': 5}) + private_post_v5_account_upgrade_to_uta = privatePostV5AccountUpgradeToUta = Entry('v5/account/upgrade-to-uta', 'private', 'POST', {'cost': 5}) + private_post_v5_account_quick_repayment = privatePostV5AccountQuickRepayment = Entry('v5/account/quick-repayment', 'private', 'POST', {'cost': 5}) + private_post_v5_account_set_margin_mode = privatePostV5AccountSetMarginMode = Entry('v5/account/set-margin-mode', 'private', 'POST', {'cost': 5}) + private_post_v5_account_set_hedging_mode = privatePostV5AccountSetHedgingMode = Entry('v5/account/set-hedging-mode', 'private', 'POST', {'cost': 5}) + private_post_v5_account_mmp_modify = privatePostV5AccountMmpModify = Entry('v5/account/mmp-modify', 'private', 'POST', {'cost': 5}) + private_post_v5_account_mmp_reset = privatePostV5AccountMmpReset = Entry('v5/account/mmp-reset', 'private', 'POST', {'cost': 5}) + private_post_v5_account_borrow = privatePostV5AccountBorrow = Entry('v5/account/borrow', 'private', 'POST', {'cost': 5}) + private_post_v5_account_repay = privatePostV5AccountRepay = Entry('v5/account/repay', 'private', 'POST', {'cost': 5}) + private_post_v5_account_no_convert_repay = privatePostV5AccountNoConvertRepay = Entry('v5/account/no-convert-repay', 'private', 'POST', {'cost': 5}) + private_post_v5_asset_exchange_quote_apply = privatePostV5AssetExchangeQuoteApply = Entry('v5/asset/exchange/quote-apply', 'private', 'POST', {'cost': 1}) + private_post_v5_asset_exchange_convert_execute = privatePostV5AssetExchangeConvertExecute = Entry('v5/asset/exchange/convert-execute', 'private', 'POST', {'cost': 1}) + private_post_v5_asset_transfer_inter_transfer = privatePostV5AssetTransferInterTransfer = Entry('v5/asset/transfer/inter-transfer', 'private', 'POST', {'cost': 50}) + private_post_v5_asset_transfer_save_transfer_sub_member = privatePostV5AssetTransferSaveTransferSubMember = Entry('v5/asset/transfer/save-transfer-sub-member', 'private', 'POST', {'cost': 150}) + private_post_v5_asset_transfer_universal_transfer = privatePostV5AssetTransferUniversalTransfer = Entry('v5/asset/transfer/universal-transfer', 'private', 'POST', {'cost': 10}) + private_post_v5_asset_deposit_deposit_to_account = privatePostV5AssetDepositDepositToAccount = Entry('v5/asset/deposit/deposit-to-account', 'private', 'POST', {'cost': 5}) + private_post_v5_asset_withdraw_create = privatePostV5AssetWithdrawCreate = Entry('v5/asset/withdraw/create', 'private', 'POST', {'cost': 50}) + private_post_v5_asset_withdraw_cancel = privatePostV5AssetWithdrawCancel = Entry('v5/asset/withdraw/cancel', 'private', 'POST', {'cost': 50}) + private_post_v5_user_create_sub_member = privatePostV5UserCreateSubMember = Entry('v5/user/create-sub-member', 'private', 'POST', {'cost': 10}) + private_post_v5_user_create_sub_api = privatePostV5UserCreateSubApi = Entry('v5/user/create-sub-api', 'private', 'POST', {'cost': 10}) + private_post_v5_user_frozen_sub_member = privatePostV5UserFrozenSubMember = Entry('v5/user/frozen-sub-member', 'private', 'POST', {'cost': 10}) + private_post_v5_user_update_api = privatePostV5UserUpdateApi = Entry('v5/user/update-api', 'private', 'POST', {'cost': 10}) + private_post_v5_user_update_sub_api = privatePostV5UserUpdateSubApi = Entry('v5/user/update-sub-api', 'private', 'POST', {'cost': 10}) + private_post_v5_user_delete_api = privatePostV5UserDeleteApi = Entry('v5/user/delete-api', 'private', 'POST', {'cost': 10}) + private_post_v5_user_delete_sub_api = privatePostV5UserDeleteSubApi = Entry('v5/user/delete-sub-api', 'private', 'POST', {'cost': 10}) + private_post_v5_spot_lever_token_purchase = privatePostV5SpotLeverTokenPurchase = Entry('v5/spot-lever-token/purchase', 'private', 'POST', {'cost': 2.5}) + private_post_v5_spot_lever_token_redeem = privatePostV5SpotLeverTokenRedeem = Entry('v5/spot-lever-token/redeem', 'private', 'POST', {'cost': 2.5}) + private_post_v5_spot_margin_trade_switch_mode = privatePostV5SpotMarginTradeSwitchMode = Entry('v5/spot-margin-trade/switch-mode', 'private', 'POST', {'cost': 5}) + private_post_v5_spot_margin_trade_set_leverage = privatePostV5SpotMarginTradeSetLeverage = Entry('v5/spot-margin-trade/set-leverage', 'private', 'POST', {'cost': 5}) + private_post_v5_spot_cross_margin_trade_loan = privatePostV5SpotCrossMarginTradeLoan = Entry('v5/spot-cross-margin-trade/loan', 'private', 'POST', {'cost': 2.5}) + private_post_v5_spot_cross_margin_trade_repay = privatePostV5SpotCrossMarginTradeRepay = Entry('v5/spot-cross-margin-trade/repay', 'private', 'POST', {'cost': 2.5}) + private_post_v5_spot_cross_margin_trade_switch = privatePostV5SpotCrossMarginTradeSwitch = Entry('v5/spot-cross-margin-trade/switch', 'private', 'POST', {'cost': 2.5}) + private_post_v5_crypto_loan_borrow = privatePostV5CryptoLoanBorrow = Entry('v5/crypto-loan/borrow', 'private', 'POST', {'cost': 5}) + private_post_v5_crypto_loan_repay = privatePostV5CryptoLoanRepay = Entry('v5/crypto-loan/repay', 'private', 'POST', {'cost': 5}) + private_post_v5_crypto_loan_adjust_ltv = privatePostV5CryptoLoanAdjustLtv = Entry('v5/crypto-loan/adjust-ltv', 'private', 'POST', {'cost': 5}) + private_post_v5_ins_loan_association_uid = privatePostV5InsLoanAssociationUid = Entry('v5/ins-loan/association-uid', 'private', 'POST', {'cost': 5}) + private_post_v5_lending_purchase = privatePostV5LendingPurchase = Entry('v5/lending/purchase', 'private', 'POST', {'cost': 5}) + private_post_v5_lending_redeem = privatePostV5LendingRedeem = Entry('v5/lending/redeem', 'private', 'POST', {'cost': 5}) + private_post_v5_lending_redeem_cancel = privatePostV5LendingRedeemCancel = Entry('v5/lending/redeem-cancel', 'private', 'POST', {'cost': 5}) + private_post_v5_account_set_collateral_switch = privatePostV5AccountSetCollateralSwitch = Entry('v5/account/set-collateral-switch', 'private', 'POST', {'cost': 5}) + private_post_v5_account_set_collateral_switch_batch = privatePostV5AccountSetCollateralSwitchBatch = Entry('v5/account/set-collateral-switch-batch', 'private', 'POST', {'cost': 5}) + private_post_v5_account_demo_apply_money = privatePostV5AccountDemoApplyMoney = Entry('v5/account/demo-apply-money', 'private', 'POST', {'cost': 5}) + private_post_v5_broker_award_info = privatePostV5BrokerAwardInfo = Entry('v5/broker/award/info', 'private', 'POST', {'cost': 5}) + private_post_v5_broker_award_distribute_award = privatePostV5BrokerAwardDistributeAward = Entry('v5/broker/award/distribute-award', 'private', 'POST', {'cost': 5}) + private_post_v5_broker_award_distribution_record = privatePostV5BrokerAwardDistributionRecord = Entry('v5/broker/award/distribution-record', 'private', 'POST', {'cost': 5}) + private_post_v5_earn_place_order = privatePostV5EarnPlaceOrder = Entry('v5/earn/place-order', 'private', 'POST', {'cost': 5}) diff --git a/ccxt/abstract/cex.py b/ccxt/abstract/cex.py new file mode 100644 index 0000000..21ad837 --- /dev/null +++ b/ccxt/abstract/cex.py @@ -0,0 +1,32 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_post_get_server_time = publicPostGetServerTime = Entry('get_server_time', 'public', 'POST', {'cost': 1}) + public_post_get_pairs_info = publicPostGetPairsInfo = Entry('get_pairs_info', 'public', 'POST', {'cost': 1}) + public_post_get_currencies_info = publicPostGetCurrenciesInfo = Entry('get_currencies_info', 'public', 'POST', {'cost': 1}) + public_post_get_processing_info = publicPostGetProcessingInfo = Entry('get_processing_info', 'public', 'POST', {'cost': 10}) + public_post_get_ticker = publicPostGetTicker = Entry('get_ticker', 'public', 'POST', {'cost': 1}) + public_post_get_trade_history = publicPostGetTradeHistory = Entry('get_trade_history', 'public', 'POST', {'cost': 1}) + public_post_get_order_book = publicPostGetOrderBook = Entry('get_order_book', 'public', 'POST', {'cost': 1}) + public_post_get_candles = publicPostGetCandles = Entry('get_candles', 'public', 'POST', {'cost': 1}) + private_post_get_my_current_fee = privatePostGetMyCurrentFee = Entry('get_my_current_fee', 'private', 'POST', {'cost': 5}) + private_post_get_fee_strategy = privatePostGetFeeStrategy = Entry('get_fee_strategy', 'private', 'POST', {'cost': 1}) + private_post_get_my_volume = privatePostGetMyVolume = Entry('get_my_volume', 'private', 'POST', {'cost': 5}) + private_post_do_create_account = privatePostDoCreateAccount = Entry('do_create_account', 'private', 'POST', {'cost': 1}) + private_post_get_my_account_status_v3 = privatePostGetMyAccountStatusV3 = Entry('get_my_account_status_v3', 'private', 'POST', {'cost': 5}) + private_post_get_my_wallet_balance = privatePostGetMyWalletBalance = Entry('get_my_wallet_balance', 'private', 'POST', {'cost': 5}) + private_post_get_my_orders = privatePostGetMyOrders = Entry('get_my_orders', 'private', 'POST', {'cost': 5}) + private_post_do_my_new_order = privatePostDoMyNewOrder = Entry('do_my_new_order', 'private', 'POST', {'cost': 1}) + private_post_do_cancel_my_order = privatePostDoCancelMyOrder = Entry('do_cancel_my_order', 'private', 'POST', {'cost': 1}) + private_post_do_cancel_all_orders = privatePostDoCancelAllOrders = Entry('do_cancel_all_orders', 'private', 'POST', {'cost': 5}) + private_post_get_order_book = privatePostGetOrderBook = Entry('get_order_book', 'private', 'POST', {'cost': 1}) + private_post_get_candles = privatePostGetCandles = Entry('get_candles', 'private', 'POST', {'cost': 1}) + private_post_get_trade_history = privatePostGetTradeHistory = Entry('get_trade_history', 'private', 'POST', {'cost': 1}) + private_post_get_my_transaction_history = privatePostGetMyTransactionHistory = Entry('get_my_transaction_history', 'private', 'POST', {'cost': 1}) + private_post_get_my_funding_history = privatePostGetMyFundingHistory = Entry('get_my_funding_history', 'private', 'POST', {'cost': 5}) + private_post_do_my_internal_transfer = privatePostDoMyInternalTransfer = Entry('do_my_internal_transfer', 'private', 'POST', {'cost': 1}) + private_post_get_processing_info = privatePostGetProcessingInfo = Entry('get_processing_info', 'private', 'POST', {'cost': 10}) + private_post_get_deposit_address = privatePostGetDepositAddress = Entry('get_deposit_address', 'private', 'POST', {'cost': 5}) + private_post_do_deposit_funds_from_wallet = privatePostDoDepositFundsFromWallet = Entry('do_deposit_funds_from_wallet', 'private', 'POST', {'cost': 1}) + private_post_do_withdrawal_funds_to_wallet = privatePostDoWithdrawalFundsToWallet = Entry('do_withdrawal_funds_to_wallet', 'private', 'POST', {'cost': 1}) diff --git a/ccxt/abstract/coinbase.py b/ccxt/abstract/coinbase.py new file mode 100644 index 0000000..be387dd --- /dev/null +++ b/ccxt/abstract/coinbase.py @@ -0,0 +1,95 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + v2_public_get_currencies = v2PublicGetCurrencies = Entry('currencies', ['v2', 'public'], 'GET', {'cost': 10.6}) + v2_public_get_currencies_crypto = v2PublicGetCurrenciesCrypto = Entry('currencies/crypto', ['v2', 'public'], 'GET', {'cost': 10.6}) + v2_public_get_time = v2PublicGetTime = Entry('time', ['v2', 'public'], 'GET', {'cost': 10.6}) + v2_public_get_exchange_rates = v2PublicGetExchangeRates = Entry('exchange-rates', ['v2', 'public'], 'GET', {'cost': 10.6}) + v2_public_get_users_user_id = v2PublicGetUsersUserId = Entry('users/{user_id}', ['v2', 'public'], 'GET', {'cost': 10.6}) + v2_public_get_prices_symbol_buy = v2PublicGetPricesSymbolBuy = Entry('prices/{symbol}/buy', ['v2', 'public'], 'GET', {'cost': 10.6}) + v2_public_get_prices_symbol_sell = v2PublicGetPricesSymbolSell = Entry('prices/{symbol}/sell', ['v2', 'public'], 'GET', {'cost': 10.6}) + v2_public_get_prices_symbol_spot = v2PublicGetPricesSymbolSpot = Entry('prices/{symbol}/spot', ['v2', 'public'], 'GET', {'cost': 10.6}) + v2_private_get_accounts = v2PrivateGetAccounts = Entry('accounts', ['v2', 'private'], 'GET', {'cost': 10.6}) + v2_private_get_accounts_account_id = v2PrivateGetAccountsAccountId = Entry('accounts/{account_id}', ['v2', 'private'], 'GET', {'cost': 10.6}) + v2_private_get_accounts_account_id_addresses = v2PrivateGetAccountsAccountIdAddresses = Entry('accounts/{account_id}/addresses', ['v2', 'private'], 'GET', {'cost': 10.6}) + v2_private_get_accounts_account_id_addresses_address_id = v2PrivateGetAccountsAccountIdAddressesAddressId = Entry('accounts/{account_id}/addresses/{address_id}', ['v2', 'private'], 'GET', {'cost': 10.6}) + v2_private_get_accounts_account_id_addresses_address_id_transactions = v2PrivateGetAccountsAccountIdAddressesAddressIdTransactions = Entry('accounts/{account_id}/addresses/{address_id}/transactions', ['v2', 'private'], 'GET', {'cost': 10.6}) + v2_private_get_accounts_account_id_transactions = v2PrivateGetAccountsAccountIdTransactions = Entry('accounts/{account_id}/transactions', ['v2', 'private'], 'GET', {'cost': 10.6}) + v2_private_get_accounts_account_id_transactions_transaction_id = v2PrivateGetAccountsAccountIdTransactionsTransactionId = Entry('accounts/{account_id}/transactions/{transaction_id}', ['v2', 'private'], 'GET', {'cost': 10.6}) + v2_private_get_accounts_account_id_buys = v2PrivateGetAccountsAccountIdBuys = Entry('accounts/{account_id}/buys', ['v2', 'private'], 'GET', {'cost': 10.6}) + v2_private_get_accounts_account_id_buys_buy_id = v2PrivateGetAccountsAccountIdBuysBuyId = Entry('accounts/{account_id}/buys/{buy_id}', ['v2', 'private'], 'GET', {'cost': 10.6}) + v2_private_get_accounts_account_id_sells = v2PrivateGetAccountsAccountIdSells = Entry('accounts/{account_id}/sells', ['v2', 'private'], 'GET', {'cost': 10.6}) + v2_private_get_accounts_account_id_sells_sell_id = v2PrivateGetAccountsAccountIdSellsSellId = Entry('accounts/{account_id}/sells/{sell_id}', ['v2', 'private'], 'GET', {'cost': 10.6}) + v2_private_get_accounts_account_id_deposits = v2PrivateGetAccountsAccountIdDeposits = Entry('accounts/{account_id}/deposits', ['v2', 'private'], 'GET', {'cost': 10.6}) + v2_private_get_accounts_account_id_deposits_deposit_id = v2PrivateGetAccountsAccountIdDepositsDepositId = Entry('accounts/{account_id}/deposits/{deposit_id}', ['v2', 'private'], 'GET', {'cost': 10.6}) + v2_private_get_accounts_account_id_withdrawals = v2PrivateGetAccountsAccountIdWithdrawals = Entry('accounts/{account_id}/withdrawals', ['v2', 'private'], 'GET', {'cost': 10.6}) + v2_private_get_accounts_account_id_withdrawals_withdrawal_id = v2PrivateGetAccountsAccountIdWithdrawalsWithdrawalId = Entry('accounts/{account_id}/withdrawals/{withdrawal_id}', ['v2', 'private'], 'GET', {'cost': 10.6}) + v2_private_get_payment_methods = v2PrivateGetPaymentMethods = Entry('payment-methods', ['v2', 'private'], 'GET', {'cost': 10.6}) + v2_private_get_payment_methods_payment_method_id = v2PrivateGetPaymentMethodsPaymentMethodId = Entry('payment-methods/{payment_method_id}', ['v2', 'private'], 'GET', {'cost': 10.6}) + v2_private_get_user = v2PrivateGetUser = Entry('user', ['v2', 'private'], 'GET', {'cost': 10.6}) + v2_private_get_user_auth = v2PrivateGetUserAuth = Entry('user/auth', ['v2', 'private'], 'GET', {'cost': 10.6}) + v2_private_post_accounts = v2PrivatePostAccounts = Entry('accounts', ['v2', 'private'], 'POST', {'cost': 10.6}) + v2_private_post_accounts_account_id_primary = v2PrivatePostAccountsAccountIdPrimary = Entry('accounts/{account_id}/primary', ['v2', 'private'], 'POST', {'cost': 10.6}) + v2_private_post_accounts_account_id_addresses = v2PrivatePostAccountsAccountIdAddresses = Entry('accounts/{account_id}/addresses', ['v2', 'private'], 'POST', {'cost': 10.6}) + v2_private_post_accounts_account_id_transactions = v2PrivatePostAccountsAccountIdTransactions = Entry('accounts/{account_id}/transactions', ['v2', 'private'], 'POST', {'cost': 10.6}) + v2_private_post_accounts_account_id_transactions_transaction_id_complete = v2PrivatePostAccountsAccountIdTransactionsTransactionIdComplete = Entry('accounts/{account_id}/transactions/{transaction_id}/complete', ['v2', 'private'], 'POST', {'cost': 10.6}) + v2_private_post_accounts_account_id_transactions_transaction_id_resend = v2PrivatePostAccountsAccountIdTransactionsTransactionIdResend = Entry('accounts/{account_id}/transactions/{transaction_id}/resend', ['v2', 'private'], 'POST', {'cost': 10.6}) + v2_private_post_accounts_account_id_buys = v2PrivatePostAccountsAccountIdBuys = Entry('accounts/{account_id}/buys', ['v2', 'private'], 'POST', {'cost': 10.6}) + v2_private_post_accounts_account_id_buys_buy_id_commit = v2PrivatePostAccountsAccountIdBuysBuyIdCommit = Entry('accounts/{account_id}/buys/{buy_id}/commit', ['v2', 'private'], 'POST', {'cost': 10.6}) + v2_private_post_accounts_account_id_sells = v2PrivatePostAccountsAccountIdSells = Entry('accounts/{account_id}/sells', ['v2', 'private'], 'POST', {'cost': 10.6}) + v2_private_post_accounts_account_id_sells_sell_id_commit = v2PrivatePostAccountsAccountIdSellsSellIdCommit = Entry('accounts/{account_id}/sells/{sell_id}/commit', ['v2', 'private'], 'POST', {'cost': 10.6}) + v2_private_post_accounts_account_id_deposits = v2PrivatePostAccountsAccountIdDeposits = Entry('accounts/{account_id}/deposits', ['v2', 'private'], 'POST', {'cost': 10.6}) + v2_private_post_accounts_account_id_deposits_deposit_id_commit = v2PrivatePostAccountsAccountIdDepositsDepositIdCommit = Entry('accounts/{account_id}/deposits/{deposit_id}/commit', ['v2', 'private'], 'POST', {'cost': 10.6}) + v2_private_post_accounts_account_id_withdrawals = v2PrivatePostAccountsAccountIdWithdrawals = Entry('accounts/{account_id}/withdrawals', ['v2', 'private'], 'POST', {'cost': 10.6}) + v2_private_post_accounts_account_id_withdrawals_withdrawal_id_commit = v2PrivatePostAccountsAccountIdWithdrawalsWithdrawalIdCommit = Entry('accounts/{account_id}/withdrawals/{withdrawal_id}/commit', ['v2', 'private'], 'POST', {'cost': 10.6}) + v2_private_put_accounts_account_id = v2PrivatePutAccountsAccountId = Entry('accounts/{account_id}', ['v2', 'private'], 'PUT', {'cost': 10.6}) + v2_private_put_user = v2PrivatePutUser = Entry('user', ['v2', 'private'], 'PUT', {'cost': 10.6}) + v2_private_delete_accounts_id = v2PrivateDeleteAccountsId = Entry('accounts/{id}', ['v2', 'private'], 'DELETE', {'cost': 10.6}) + v2_private_delete_accounts_account_id_transactions_transaction_id = v2PrivateDeleteAccountsAccountIdTransactionsTransactionId = Entry('accounts/{account_id}/transactions/{transaction_id}', ['v2', 'private'], 'DELETE', {'cost': 10.6}) + v3_public_get_brokerage_time = v3PublicGetBrokerageTime = Entry('brokerage/time', ['v3', 'public'], 'GET', {'cost': 3}) + v3_public_get_brokerage_market_product_book = v3PublicGetBrokerageMarketProductBook = Entry('brokerage/market/product_book', ['v3', 'public'], 'GET', {'cost': 3}) + v3_public_get_brokerage_market_products = v3PublicGetBrokerageMarketProducts = Entry('brokerage/market/products', ['v3', 'public'], 'GET', {'cost': 3}) + v3_public_get_brokerage_market_products_product_id = v3PublicGetBrokerageMarketProductsProductId = Entry('brokerage/market/products/{product_id}', ['v3', 'public'], 'GET', {'cost': 3}) + v3_public_get_brokerage_market_products_product_id_candles = v3PublicGetBrokerageMarketProductsProductIdCandles = Entry('brokerage/market/products/{product_id}/candles', ['v3', 'public'], 'GET', {'cost': 3}) + v3_public_get_brokerage_market_products_product_id_ticker = v3PublicGetBrokerageMarketProductsProductIdTicker = Entry('brokerage/market/products/{product_id}/ticker', ['v3', 'public'], 'GET', {'cost': 3}) + v3_private_get_brokerage_accounts = v3PrivateGetBrokerageAccounts = Entry('brokerage/accounts', ['v3', 'private'], 'GET', {'cost': 1}) + v3_private_get_brokerage_accounts_account_uuid = v3PrivateGetBrokerageAccountsAccountUuid = Entry('brokerage/accounts/{account_uuid}', ['v3', 'private'], 'GET', {'cost': 1}) + v3_private_get_brokerage_orders_historical_batch = v3PrivateGetBrokerageOrdersHistoricalBatch = Entry('brokerage/orders/historical/batch', ['v3', 'private'], 'GET', {'cost': 1}) + v3_private_get_brokerage_orders_historical_fills = v3PrivateGetBrokerageOrdersHistoricalFills = Entry('brokerage/orders/historical/fills', ['v3', 'private'], 'GET', {'cost': 1}) + v3_private_get_brokerage_orders_historical_order_id = v3PrivateGetBrokerageOrdersHistoricalOrderId = Entry('brokerage/orders/historical/{order_id}', ['v3', 'private'], 'GET', {'cost': 1}) + v3_private_get_brokerage_products = v3PrivateGetBrokerageProducts = Entry('brokerage/products', ['v3', 'private'], 'GET', {'cost': 3}) + v3_private_get_brokerage_products_product_id = v3PrivateGetBrokerageProductsProductId = Entry('brokerage/products/{product_id}', ['v3', 'private'], 'GET', {'cost': 3}) + v3_private_get_brokerage_products_product_id_candles = v3PrivateGetBrokerageProductsProductIdCandles = Entry('brokerage/products/{product_id}/candles', ['v3', 'private'], 'GET', {'cost': 3}) + v3_private_get_brokerage_products_product_id_ticker = v3PrivateGetBrokerageProductsProductIdTicker = Entry('brokerage/products/{product_id}/ticker', ['v3', 'private'], 'GET', {'cost': 3}) + v3_private_get_brokerage_best_bid_ask = v3PrivateGetBrokerageBestBidAsk = Entry('brokerage/best_bid_ask', ['v3', 'private'], 'GET', {'cost': 3}) + v3_private_get_brokerage_product_book = v3PrivateGetBrokerageProductBook = Entry('brokerage/product_book', ['v3', 'private'], 'GET', {'cost': 3}) + v3_private_get_brokerage_transaction_summary = v3PrivateGetBrokerageTransactionSummary = Entry('brokerage/transaction_summary', ['v3', 'private'], 'GET', {'cost': 3}) + v3_private_get_brokerage_portfolios = v3PrivateGetBrokeragePortfolios = Entry('brokerage/portfolios', ['v3', 'private'], 'GET', {'cost': 1}) + v3_private_get_brokerage_portfolios_portfolio_uuid = v3PrivateGetBrokeragePortfoliosPortfolioUuid = Entry('brokerage/portfolios/{portfolio_uuid}', ['v3', 'private'], 'GET', {'cost': 1}) + v3_private_get_brokerage_convert_trade_trade_id = v3PrivateGetBrokerageConvertTradeTradeId = Entry('brokerage/convert/trade/{trade_id}', ['v3', 'private'], 'GET', {'cost': 1}) + v3_private_get_brokerage_cfm_balance_summary = v3PrivateGetBrokerageCfmBalanceSummary = Entry('brokerage/cfm/balance_summary', ['v3', 'private'], 'GET', {'cost': 1}) + v3_private_get_brokerage_cfm_positions = v3PrivateGetBrokerageCfmPositions = Entry('brokerage/cfm/positions', ['v3', 'private'], 'GET', {'cost': 1}) + v3_private_get_brokerage_cfm_positions_product_id = v3PrivateGetBrokerageCfmPositionsProductId = Entry('brokerage/cfm/positions/{product_id}', ['v3', 'private'], 'GET', {'cost': 1}) + v3_private_get_brokerage_cfm_sweeps = v3PrivateGetBrokerageCfmSweeps = Entry('brokerage/cfm/sweeps', ['v3', 'private'], 'GET', {'cost': 1}) + v3_private_get_brokerage_intx_portfolio_portfolio_uuid = v3PrivateGetBrokerageIntxPortfolioPortfolioUuid = Entry('brokerage/intx/portfolio/{portfolio_uuid}', ['v3', 'private'], 'GET', {'cost': 1}) + v3_private_get_brokerage_intx_positions_portfolio_uuid = v3PrivateGetBrokerageIntxPositionsPortfolioUuid = Entry('brokerage/intx/positions/{portfolio_uuid}', ['v3', 'private'], 'GET', {'cost': 1}) + v3_private_get_brokerage_intx_positions_portfolio_uuid_symbol = v3PrivateGetBrokerageIntxPositionsPortfolioUuidSymbol = Entry('brokerage/intx/positions/{portfolio_uuid}/{symbol}', ['v3', 'private'], 'GET', {'cost': 1}) + v3_private_get_brokerage_payment_methods = v3PrivateGetBrokeragePaymentMethods = Entry('brokerage/payment_methods', ['v3', 'private'], 'GET', {'cost': 1}) + v3_private_get_brokerage_payment_methods_payment_method_id = v3PrivateGetBrokeragePaymentMethodsPaymentMethodId = Entry('brokerage/payment_methods/{payment_method_id}', ['v3', 'private'], 'GET', {'cost': 1}) + v3_private_get_brokerage_key_permissions = v3PrivateGetBrokerageKeyPermissions = Entry('brokerage/key_permissions', ['v3', 'private'], 'GET', {'cost': 1}) + v3_private_post_brokerage_orders = v3PrivatePostBrokerageOrders = Entry('brokerage/orders', ['v3', 'private'], 'POST', {'cost': 1}) + v3_private_post_brokerage_orders_batch_cancel = v3PrivatePostBrokerageOrdersBatchCancel = Entry('brokerage/orders/batch_cancel', ['v3', 'private'], 'POST', {'cost': 1}) + v3_private_post_brokerage_orders_edit = v3PrivatePostBrokerageOrdersEdit = Entry('brokerage/orders/edit', ['v3', 'private'], 'POST', {'cost': 1}) + v3_private_post_brokerage_orders_edit_preview = v3PrivatePostBrokerageOrdersEditPreview = Entry('brokerage/orders/edit_preview', ['v3', 'private'], 'POST', {'cost': 1}) + v3_private_post_brokerage_orders_preview = v3PrivatePostBrokerageOrdersPreview = Entry('brokerage/orders/preview', ['v3', 'private'], 'POST', {'cost': 1}) + v3_private_post_brokerage_portfolios = v3PrivatePostBrokeragePortfolios = Entry('brokerage/portfolios', ['v3', 'private'], 'POST', {'cost': 1}) + v3_private_post_brokerage_portfolios_move_funds = v3PrivatePostBrokeragePortfoliosMoveFunds = Entry('brokerage/portfolios/move_funds', ['v3', 'private'], 'POST', {'cost': 1}) + v3_private_post_brokerage_convert_quote = v3PrivatePostBrokerageConvertQuote = Entry('brokerage/convert/quote', ['v3', 'private'], 'POST', {'cost': 1}) + v3_private_post_brokerage_convert_trade_trade_id = v3PrivatePostBrokerageConvertTradeTradeId = Entry('brokerage/convert/trade/{trade_id}', ['v3', 'private'], 'POST', {'cost': 1}) + v3_private_post_brokerage_cfm_sweeps_schedule = v3PrivatePostBrokerageCfmSweepsSchedule = Entry('brokerage/cfm/sweeps/schedule', ['v3', 'private'], 'POST', {'cost': 1}) + v3_private_post_brokerage_intx_allocate = v3PrivatePostBrokerageIntxAllocate = Entry('brokerage/intx/allocate', ['v3', 'private'], 'POST', {'cost': 1}) + v3_private_post_brokerage_orders_close_position = v3PrivatePostBrokerageOrdersClosePosition = Entry('brokerage/orders/close_position', ['v3', 'private'], 'POST', {'cost': 1}) + v3_private_put_brokerage_portfolios_portfolio_uuid = v3PrivatePutBrokeragePortfoliosPortfolioUuid = Entry('brokerage/portfolios/{portfolio_uuid}', ['v3', 'private'], 'PUT', {'cost': 1}) + v3_private_delete_brokerage_portfolios_portfolio_uuid = v3PrivateDeleteBrokeragePortfoliosPortfolioUuid = Entry('brokerage/portfolios/{portfolio_uuid}', ['v3', 'private'], 'DELETE', {'cost': 1}) + v3_private_delete_brokerage_cfm_sweeps = v3PrivateDeleteBrokerageCfmSweeps = Entry('brokerage/cfm/sweeps', ['v3', 'private'], 'DELETE', {'cost': 1}) diff --git a/ccxt/abstract/coinbaseadvanced.py b/ccxt/abstract/coinbaseadvanced.py new file mode 100644 index 0000000..be387dd --- /dev/null +++ b/ccxt/abstract/coinbaseadvanced.py @@ -0,0 +1,95 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + v2_public_get_currencies = v2PublicGetCurrencies = Entry('currencies', ['v2', 'public'], 'GET', {'cost': 10.6}) + v2_public_get_currencies_crypto = v2PublicGetCurrenciesCrypto = Entry('currencies/crypto', ['v2', 'public'], 'GET', {'cost': 10.6}) + v2_public_get_time = v2PublicGetTime = Entry('time', ['v2', 'public'], 'GET', {'cost': 10.6}) + v2_public_get_exchange_rates = v2PublicGetExchangeRates = Entry('exchange-rates', ['v2', 'public'], 'GET', {'cost': 10.6}) + v2_public_get_users_user_id = v2PublicGetUsersUserId = Entry('users/{user_id}', ['v2', 'public'], 'GET', {'cost': 10.6}) + v2_public_get_prices_symbol_buy = v2PublicGetPricesSymbolBuy = Entry('prices/{symbol}/buy', ['v2', 'public'], 'GET', {'cost': 10.6}) + v2_public_get_prices_symbol_sell = v2PublicGetPricesSymbolSell = Entry('prices/{symbol}/sell', ['v2', 'public'], 'GET', {'cost': 10.6}) + v2_public_get_prices_symbol_spot = v2PublicGetPricesSymbolSpot = Entry('prices/{symbol}/spot', ['v2', 'public'], 'GET', {'cost': 10.6}) + v2_private_get_accounts = v2PrivateGetAccounts = Entry('accounts', ['v2', 'private'], 'GET', {'cost': 10.6}) + v2_private_get_accounts_account_id = v2PrivateGetAccountsAccountId = Entry('accounts/{account_id}', ['v2', 'private'], 'GET', {'cost': 10.6}) + v2_private_get_accounts_account_id_addresses = v2PrivateGetAccountsAccountIdAddresses = Entry('accounts/{account_id}/addresses', ['v2', 'private'], 'GET', {'cost': 10.6}) + v2_private_get_accounts_account_id_addresses_address_id = v2PrivateGetAccountsAccountIdAddressesAddressId = Entry('accounts/{account_id}/addresses/{address_id}', ['v2', 'private'], 'GET', {'cost': 10.6}) + v2_private_get_accounts_account_id_addresses_address_id_transactions = v2PrivateGetAccountsAccountIdAddressesAddressIdTransactions = Entry('accounts/{account_id}/addresses/{address_id}/transactions', ['v2', 'private'], 'GET', {'cost': 10.6}) + v2_private_get_accounts_account_id_transactions = v2PrivateGetAccountsAccountIdTransactions = Entry('accounts/{account_id}/transactions', ['v2', 'private'], 'GET', {'cost': 10.6}) + v2_private_get_accounts_account_id_transactions_transaction_id = v2PrivateGetAccountsAccountIdTransactionsTransactionId = Entry('accounts/{account_id}/transactions/{transaction_id}', ['v2', 'private'], 'GET', {'cost': 10.6}) + v2_private_get_accounts_account_id_buys = v2PrivateGetAccountsAccountIdBuys = Entry('accounts/{account_id}/buys', ['v2', 'private'], 'GET', {'cost': 10.6}) + v2_private_get_accounts_account_id_buys_buy_id = v2PrivateGetAccountsAccountIdBuysBuyId = Entry('accounts/{account_id}/buys/{buy_id}', ['v2', 'private'], 'GET', {'cost': 10.6}) + v2_private_get_accounts_account_id_sells = v2PrivateGetAccountsAccountIdSells = Entry('accounts/{account_id}/sells', ['v2', 'private'], 'GET', {'cost': 10.6}) + v2_private_get_accounts_account_id_sells_sell_id = v2PrivateGetAccountsAccountIdSellsSellId = Entry('accounts/{account_id}/sells/{sell_id}', ['v2', 'private'], 'GET', {'cost': 10.6}) + v2_private_get_accounts_account_id_deposits = v2PrivateGetAccountsAccountIdDeposits = Entry('accounts/{account_id}/deposits', ['v2', 'private'], 'GET', {'cost': 10.6}) + v2_private_get_accounts_account_id_deposits_deposit_id = v2PrivateGetAccountsAccountIdDepositsDepositId = Entry('accounts/{account_id}/deposits/{deposit_id}', ['v2', 'private'], 'GET', {'cost': 10.6}) + v2_private_get_accounts_account_id_withdrawals = v2PrivateGetAccountsAccountIdWithdrawals = Entry('accounts/{account_id}/withdrawals', ['v2', 'private'], 'GET', {'cost': 10.6}) + v2_private_get_accounts_account_id_withdrawals_withdrawal_id = v2PrivateGetAccountsAccountIdWithdrawalsWithdrawalId = Entry('accounts/{account_id}/withdrawals/{withdrawal_id}', ['v2', 'private'], 'GET', {'cost': 10.6}) + v2_private_get_payment_methods = v2PrivateGetPaymentMethods = Entry('payment-methods', ['v2', 'private'], 'GET', {'cost': 10.6}) + v2_private_get_payment_methods_payment_method_id = v2PrivateGetPaymentMethodsPaymentMethodId = Entry('payment-methods/{payment_method_id}', ['v2', 'private'], 'GET', {'cost': 10.6}) + v2_private_get_user = v2PrivateGetUser = Entry('user', ['v2', 'private'], 'GET', {'cost': 10.6}) + v2_private_get_user_auth = v2PrivateGetUserAuth = Entry('user/auth', ['v2', 'private'], 'GET', {'cost': 10.6}) + v2_private_post_accounts = v2PrivatePostAccounts = Entry('accounts', ['v2', 'private'], 'POST', {'cost': 10.6}) + v2_private_post_accounts_account_id_primary = v2PrivatePostAccountsAccountIdPrimary = Entry('accounts/{account_id}/primary', ['v2', 'private'], 'POST', {'cost': 10.6}) + v2_private_post_accounts_account_id_addresses = v2PrivatePostAccountsAccountIdAddresses = Entry('accounts/{account_id}/addresses', ['v2', 'private'], 'POST', {'cost': 10.6}) + v2_private_post_accounts_account_id_transactions = v2PrivatePostAccountsAccountIdTransactions = Entry('accounts/{account_id}/transactions', ['v2', 'private'], 'POST', {'cost': 10.6}) + v2_private_post_accounts_account_id_transactions_transaction_id_complete = v2PrivatePostAccountsAccountIdTransactionsTransactionIdComplete = Entry('accounts/{account_id}/transactions/{transaction_id}/complete', ['v2', 'private'], 'POST', {'cost': 10.6}) + v2_private_post_accounts_account_id_transactions_transaction_id_resend = v2PrivatePostAccountsAccountIdTransactionsTransactionIdResend = Entry('accounts/{account_id}/transactions/{transaction_id}/resend', ['v2', 'private'], 'POST', {'cost': 10.6}) + v2_private_post_accounts_account_id_buys = v2PrivatePostAccountsAccountIdBuys = Entry('accounts/{account_id}/buys', ['v2', 'private'], 'POST', {'cost': 10.6}) + v2_private_post_accounts_account_id_buys_buy_id_commit = v2PrivatePostAccountsAccountIdBuysBuyIdCommit = Entry('accounts/{account_id}/buys/{buy_id}/commit', ['v2', 'private'], 'POST', {'cost': 10.6}) + v2_private_post_accounts_account_id_sells = v2PrivatePostAccountsAccountIdSells = Entry('accounts/{account_id}/sells', ['v2', 'private'], 'POST', {'cost': 10.6}) + v2_private_post_accounts_account_id_sells_sell_id_commit = v2PrivatePostAccountsAccountIdSellsSellIdCommit = Entry('accounts/{account_id}/sells/{sell_id}/commit', ['v2', 'private'], 'POST', {'cost': 10.6}) + v2_private_post_accounts_account_id_deposits = v2PrivatePostAccountsAccountIdDeposits = Entry('accounts/{account_id}/deposits', ['v2', 'private'], 'POST', {'cost': 10.6}) + v2_private_post_accounts_account_id_deposits_deposit_id_commit = v2PrivatePostAccountsAccountIdDepositsDepositIdCommit = Entry('accounts/{account_id}/deposits/{deposit_id}/commit', ['v2', 'private'], 'POST', {'cost': 10.6}) + v2_private_post_accounts_account_id_withdrawals = v2PrivatePostAccountsAccountIdWithdrawals = Entry('accounts/{account_id}/withdrawals', ['v2', 'private'], 'POST', {'cost': 10.6}) + v2_private_post_accounts_account_id_withdrawals_withdrawal_id_commit = v2PrivatePostAccountsAccountIdWithdrawalsWithdrawalIdCommit = Entry('accounts/{account_id}/withdrawals/{withdrawal_id}/commit', ['v2', 'private'], 'POST', {'cost': 10.6}) + v2_private_put_accounts_account_id = v2PrivatePutAccountsAccountId = Entry('accounts/{account_id}', ['v2', 'private'], 'PUT', {'cost': 10.6}) + v2_private_put_user = v2PrivatePutUser = Entry('user', ['v2', 'private'], 'PUT', {'cost': 10.6}) + v2_private_delete_accounts_id = v2PrivateDeleteAccountsId = Entry('accounts/{id}', ['v2', 'private'], 'DELETE', {'cost': 10.6}) + v2_private_delete_accounts_account_id_transactions_transaction_id = v2PrivateDeleteAccountsAccountIdTransactionsTransactionId = Entry('accounts/{account_id}/transactions/{transaction_id}', ['v2', 'private'], 'DELETE', {'cost': 10.6}) + v3_public_get_brokerage_time = v3PublicGetBrokerageTime = Entry('brokerage/time', ['v3', 'public'], 'GET', {'cost': 3}) + v3_public_get_brokerage_market_product_book = v3PublicGetBrokerageMarketProductBook = Entry('brokerage/market/product_book', ['v3', 'public'], 'GET', {'cost': 3}) + v3_public_get_brokerage_market_products = v3PublicGetBrokerageMarketProducts = Entry('brokerage/market/products', ['v3', 'public'], 'GET', {'cost': 3}) + v3_public_get_brokerage_market_products_product_id = v3PublicGetBrokerageMarketProductsProductId = Entry('brokerage/market/products/{product_id}', ['v3', 'public'], 'GET', {'cost': 3}) + v3_public_get_brokerage_market_products_product_id_candles = v3PublicGetBrokerageMarketProductsProductIdCandles = Entry('brokerage/market/products/{product_id}/candles', ['v3', 'public'], 'GET', {'cost': 3}) + v3_public_get_brokerage_market_products_product_id_ticker = v3PublicGetBrokerageMarketProductsProductIdTicker = Entry('brokerage/market/products/{product_id}/ticker', ['v3', 'public'], 'GET', {'cost': 3}) + v3_private_get_brokerage_accounts = v3PrivateGetBrokerageAccounts = Entry('brokerage/accounts', ['v3', 'private'], 'GET', {'cost': 1}) + v3_private_get_brokerage_accounts_account_uuid = v3PrivateGetBrokerageAccountsAccountUuid = Entry('brokerage/accounts/{account_uuid}', ['v3', 'private'], 'GET', {'cost': 1}) + v3_private_get_brokerage_orders_historical_batch = v3PrivateGetBrokerageOrdersHistoricalBatch = Entry('brokerage/orders/historical/batch', ['v3', 'private'], 'GET', {'cost': 1}) + v3_private_get_brokerage_orders_historical_fills = v3PrivateGetBrokerageOrdersHistoricalFills = Entry('brokerage/orders/historical/fills', ['v3', 'private'], 'GET', {'cost': 1}) + v3_private_get_brokerage_orders_historical_order_id = v3PrivateGetBrokerageOrdersHistoricalOrderId = Entry('brokerage/orders/historical/{order_id}', ['v3', 'private'], 'GET', {'cost': 1}) + v3_private_get_brokerage_products = v3PrivateGetBrokerageProducts = Entry('brokerage/products', ['v3', 'private'], 'GET', {'cost': 3}) + v3_private_get_brokerage_products_product_id = v3PrivateGetBrokerageProductsProductId = Entry('brokerage/products/{product_id}', ['v3', 'private'], 'GET', {'cost': 3}) + v3_private_get_brokerage_products_product_id_candles = v3PrivateGetBrokerageProductsProductIdCandles = Entry('brokerage/products/{product_id}/candles', ['v3', 'private'], 'GET', {'cost': 3}) + v3_private_get_brokerage_products_product_id_ticker = v3PrivateGetBrokerageProductsProductIdTicker = Entry('brokerage/products/{product_id}/ticker', ['v3', 'private'], 'GET', {'cost': 3}) + v3_private_get_brokerage_best_bid_ask = v3PrivateGetBrokerageBestBidAsk = Entry('brokerage/best_bid_ask', ['v3', 'private'], 'GET', {'cost': 3}) + v3_private_get_brokerage_product_book = v3PrivateGetBrokerageProductBook = Entry('brokerage/product_book', ['v3', 'private'], 'GET', {'cost': 3}) + v3_private_get_brokerage_transaction_summary = v3PrivateGetBrokerageTransactionSummary = Entry('brokerage/transaction_summary', ['v3', 'private'], 'GET', {'cost': 3}) + v3_private_get_brokerage_portfolios = v3PrivateGetBrokeragePortfolios = Entry('brokerage/portfolios', ['v3', 'private'], 'GET', {'cost': 1}) + v3_private_get_brokerage_portfolios_portfolio_uuid = v3PrivateGetBrokeragePortfoliosPortfolioUuid = Entry('brokerage/portfolios/{portfolio_uuid}', ['v3', 'private'], 'GET', {'cost': 1}) + v3_private_get_brokerage_convert_trade_trade_id = v3PrivateGetBrokerageConvertTradeTradeId = Entry('brokerage/convert/trade/{trade_id}', ['v3', 'private'], 'GET', {'cost': 1}) + v3_private_get_brokerage_cfm_balance_summary = v3PrivateGetBrokerageCfmBalanceSummary = Entry('brokerage/cfm/balance_summary', ['v3', 'private'], 'GET', {'cost': 1}) + v3_private_get_brokerage_cfm_positions = v3PrivateGetBrokerageCfmPositions = Entry('brokerage/cfm/positions', ['v3', 'private'], 'GET', {'cost': 1}) + v3_private_get_brokerage_cfm_positions_product_id = v3PrivateGetBrokerageCfmPositionsProductId = Entry('brokerage/cfm/positions/{product_id}', ['v3', 'private'], 'GET', {'cost': 1}) + v3_private_get_brokerage_cfm_sweeps = v3PrivateGetBrokerageCfmSweeps = Entry('brokerage/cfm/sweeps', ['v3', 'private'], 'GET', {'cost': 1}) + v3_private_get_brokerage_intx_portfolio_portfolio_uuid = v3PrivateGetBrokerageIntxPortfolioPortfolioUuid = Entry('brokerage/intx/portfolio/{portfolio_uuid}', ['v3', 'private'], 'GET', {'cost': 1}) + v3_private_get_brokerage_intx_positions_portfolio_uuid = v3PrivateGetBrokerageIntxPositionsPortfolioUuid = Entry('brokerage/intx/positions/{portfolio_uuid}', ['v3', 'private'], 'GET', {'cost': 1}) + v3_private_get_brokerage_intx_positions_portfolio_uuid_symbol = v3PrivateGetBrokerageIntxPositionsPortfolioUuidSymbol = Entry('brokerage/intx/positions/{portfolio_uuid}/{symbol}', ['v3', 'private'], 'GET', {'cost': 1}) + v3_private_get_brokerage_payment_methods = v3PrivateGetBrokeragePaymentMethods = Entry('brokerage/payment_methods', ['v3', 'private'], 'GET', {'cost': 1}) + v3_private_get_brokerage_payment_methods_payment_method_id = v3PrivateGetBrokeragePaymentMethodsPaymentMethodId = Entry('brokerage/payment_methods/{payment_method_id}', ['v3', 'private'], 'GET', {'cost': 1}) + v3_private_get_brokerage_key_permissions = v3PrivateGetBrokerageKeyPermissions = Entry('brokerage/key_permissions', ['v3', 'private'], 'GET', {'cost': 1}) + v3_private_post_brokerage_orders = v3PrivatePostBrokerageOrders = Entry('brokerage/orders', ['v3', 'private'], 'POST', {'cost': 1}) + v3_private_post_brokerage_orders_batch_cancel = v3PrivatePostBrokerageOrdersBatchCancel = Entry('brokerage/orders/batch_cancel', ['v3', 'private'], 'POST', {'cost': 1}) + v3_private_post_brokerage_orders_edit = v3PrivatePostBrokerageOrdersEdit = Entry('brokerage/orders/edit', ['v3', 'private'], 'POST', {'cost': 1}) + v3_private_post_brokerage_orders_edit_preview = v3PrivatePostBrokerageOrdersEditPreview = Entry('brokerage/orders/edit_preview', ['v3', 'private'], 'POST', {'cost': 1}) + v3_private_post_brokerage_orders_preview = v3PrivatePostBrokerageOrdersPreview = Entry('brokerage/orders/preview', ['v3', 'private'], 'POST', {'cost': 1}) + v3_private_post_brokerage_portfolios = v3PrivatePostBrokeragePortfolios = Entry('brokerage/portfolios', ['v3', 'private'], 'POST', {'cost': 1}) + v3_private_post_brokerage_portfolios_move_funds = v3PrivatePostBrokeragePortfoliosMoveFunds = Entry('brokerage/portfolios/move_funds', ['v3', 'private'], 'POST', {'cost': 1}) + v3_private_post_brokerage_convert_quote = v3PrivatePostBrokerageConvertQuote = Entry('brokerage/convert/quote', ['v3', 'private'], 'POST', {'cost': 1}) + v3_private_post_brokerage_convert_trade_trade_id = v3PrivatePostBrokerageConvertTradeTradeId = Entry('brokerage/convert/trade/{trade_id}', ['v3', 'private'], 'POST', {'cost': 1}) + v3_private_post_brokerage_cfm_sweeps_schedule = v3PrivatePostBrokerageCfmSweepsSchedule = Entry('brokerage/cfm/sweeps/schedule', ['v3', 'private'], 'POST', {'cost': 1}) + v3_private_post_brokerage_intx_allocate = v3PrivatePostBrokerageIntxAllocate = Entry('brokerage/intx/allocate', ['v3', 'private'], 'POST', {'cost': 1}) + v3_private_post_brokerage_orders_close_position = v3PrivatePostBrokerageOrdersClosePosition = Entry('brokerage/orders/close_position', ['v3', 'private'], 'POST', {'cost': 1}) + v3_private_put_brokerage_portfolios_portfolio_uuid = v3PrivatePutBrokeragePortfoliosPortfolioUuid = Entry('brokerage/portfolios/{portfolio_uuid}', ['v3', 'private'], 'PUT', {'cost': 1}) + v3_private_delete_brokerage_portfolios_portfolio_uuid = v3PrivateDeleteBrokeragePortfoliosPortfolioUuid = Entry('brokerage/portfolios/{portfolio_uuid}', ['v3', 'private'], 'DELETE', {'cost': 1}) + v3_private_delete_brokerage_cfm_sweeps = v3PrivateDeleteBrokerageCfmSweeps = Entry('brokerage/cfm/sweeps', ['v3', 'private'], 'DELETE', {'cost': 1}) diff --git a/ccxt/abstract/coinbaseexchange.py b/ccxt/abstract/coinbaseexchange.py new file mode 100644 index 0000000..8fc3bd5 --- /dev/null +++ b/ccxt/abstract/coinbaseexchange.py @@ -0,0 +1,83 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_currencies = publicGetCurrencies = Entry('currencies', 'public', 'GET', {}) + public_get_products = publicGetProducts = Entry('products', 'public', 'GET', {}) + public_get_products_id = publicGetProductsId = Entry('products/{id}', 'public', 'GET', {}) + public_get_products_id_book = publicGetProductsIdBook = Entry('products/{id}/book', 'public', 'GET', {}) + public_get_products_id_candles = publicGetProductsIdCandles = Entry('products/{id}/candles', 'public', 'GET', {}) + public_get_products_id_stats = publicGetProductsIdStats = Entry('products/{id}/stats', 'public', 'GET', {}) + public_get_products_id_ticker = publicGetProductsIdTicker = Entry('products/{id}/ticker', 'public', 'GET', {}) + public_get_products_id_trades = publicGetProductsIdTrades = Entry('products/{id}/trades', 'public', 'GET', {}) + public_get_time = publicGetTime = Entry('time', 'public', 'GET', {}) + public_get_products_spark_lines = publicGetProductsSparkLines = Entry('products/spark-lines', 'public', 'GET', {}) + public_get_products_volume_summary = publicGetProductsVolumeSummary = Entry('products/volume-summary', 'public', 'GET', {}) + private_get_address_book = privateGetAddressBook = Entry('address-book', 'private', 'GET', {}) + private_get_accounts = privateGetAccounts = Entry('accounts', 'private', 'GET', {}) + private_get_accounts_id = privateGetAccountsId = Entry('accounts/{id}', 'private', 'GET', {}) + private_get_accounts_id_holds = privateGetAccountsIdHolds = Entry('accounts/{id}/holds', 'private', 'GET', {}) + private_get_accounts_id_ledger = privateGetAccountsIdLedger = Entry('accounts/{id}/ledger', 'private', 'GET', {}) + private_get_accounts_id_transfers = privateGetAccountsIdTransfers = Entry('accounts/{id}/transfers', 'private', 'GET', {}) + private_get_coinbase_accounts = privateGetCoinbaseAccounts = Entry('coinbase-accounts', 'private', 'GET', {}) + private_get_fills = privateGetFills = Entry('fills', 'private', 'GET', {}) + private_get_funding = privateGetFunding = Entry('funding', 'private', 'GET', {}) + private_get_fees = privateGetFees = Entry('fees', 'private', 'GET', {}) + private_get_margin_profile_information = privateGetMarginProfileInformation = Entry('margin/profile_information', 'private', 'GET', {}) + private_get_margin_buying_power = privateGetMarginBuyingPower = Entry('margin/buying_power', 'private', 'GET', {}) + private_get_margin_withdrawal_power = privateGetMarginWithdrawalPower = Entry('margin/withdrawal_power', 'private', 'GET', {}) + private_get_margin_withdrawal_power_all = privateGetMarginWithdrawalPowerAll = Entry('margin/withdrawal_power_all', 'private', 'GET', {}) + private_get_margin_exit_plan = privateGetMarginExitPlan = Entry('margin/exit_plan', 'private', 'GET', {}) + private_get_margin_liquidation_history = privateGetMarginLiquidationHistory = Entry('margin/liquidation_history', 'private', 'GET', {}) + private_get_margin_position_refresh_amounts = privateGetMarginPositionRefreshAmounts = Entry('margin/position_refresh_amounts', 'private', 'GET', {}) + private_get_margin_status = privateGetMarginStatus = Entry('margin/status', 'private', 'GET', {}) + private_get_oracle = privateGetOracle = Entry('oracle', 'private', 'GET', {}) + private_get_orders = privateGetOrders = Entry('orders', 'private', 'GET', {}) + private_get_orders_id = privateGetOrdersId = Entry('orders/{id}', 'private', 'GET', {}) + private_get_orders_client_client_oid = privateGetOrdersClientClientOid = Entry('orders/client:{client_oid}', 'private', 'GET', {}) + private_get_otc_orders = privateGetOtcOrders = Entry('otc/orders', 'private', 'GET', {}) + private_get_payment_methods = privateGetPaymentMethods = Entry('payment-methods', 'private', 'GET', {}) + private_get_position = privateGetPosition = Entry('position', 'private', 'GET', {}) + private_get_profiles = privateGetProfiles = Entry('profiles', 'private', 'GET', {}) + private_get_profiles_id = privateGetProfilesId = Entry('profiles/{id}', 'private', 'GET', {}) + private_get_reports_report_id = privateGetReportsReportId = Entry('reports/{report_id}', 'private', 'GET', {}) + private_get_transfers = privateGetTransfers = Entry('transfers', 'private', 'GET', {}) + private_get_transfers_transfer_id = privateGetTransfersTransferId = Entry('transfers/{transfer_id}', 'private', 'GET', {}) + private_get_users_self_exchange_limits = privateGetUsersSelfExchangeLimits = Entry('users/self/exchange-limits', 'private', 'GET', {}) + private_get_users_self_hold_balances = privateGetUsersSelfHoldBalances = Entry('users/self/hold-balances', 'private', 'GET', {}) + private_get_users_self_trailing_volume = privateGetUsersSelfTrailingVolume = Entry('users/self/trailing-volume', 'private', 'GET', {}) + private_get_withdrawals_fee_estimate = privateGetWithdrawalsFeeEstimate = Entry('withdrawals/fee-estimate', 'private', 'GET', {}) + private_get_conversions_conversion_id = privateGetConversionsConversionId = Entry('conversions/{conversion_id}', 'private', 'GET', {}) + private_get_conversions = privateGetConversions = Entry('conversions', 'private', 'GET', {}) + private_get_conversions_fees = privateGetConversionsFees = Entry('conversions/fees', 'private', 'GET', {}) + private_get_loans_lending_overview = privateGetLoansLendingOverview = Entry('loans/lending-overview', 'private', 'GET', {}) + private_get_loans_lending_overview_xm = privateGetLoansLendingOverviewXm = Entry('loans/lending-overview-xm', 'private', 'GET', {}) + private_get_loans_loan_preview = privateGetLoansLoanPreview = Entry('loans/loan-preview', 'private', 'GET', {}) + private_get_loans_loan_preview_xm = privateGetLoansLoanPreviewXm = Entry('loans/loan-preview-xm', 'private', 'GET', {}) + private_get_loans_repayment_preview = privateGetLoansRepaymentPreview = Entry('loans/repayment-preview', 'private', 'GET', {}) + private_get_loans_repayment_preview_xm = privateGetLoansRepaymentPreviewXm = Entry('loans/repayment-preview-xm', 'private', 'GET', {}) + private_get_loans_interest_loan_id = privateGetLoansInterestLoanId = Entry('loans/interest/{loan_id}', 'private', 'GET', {}) + private_get_loans_interest_history_loan_id = privateGetLoansInterestHistoryLoanId = Entry('loans/interest/history/{loan_id}', 'private', 'GET', {}) + private_get_loans_interest = privateGetLoansInterest = Entry('loans/interest', 'private', 'GET', {}) + private_get_loans_assets = privateGetLoansAssets = Entry('loans/assets', 'private', 'GET', {}) + private_get_loans = privateGetLoans = Entry('loans', 'private', 'GET', {}) + private_post_conversions = privatePostConversions = Entry('conversions', 'private', 'POST', {}) + private_post_deposits_coinbase_account = privatePostDepositsCoinbaseAccount = Entry('deposits/coinbase-account', 'private', 'POST', {}) + private_post_deposits_payment_method = privatePostDepositsPaymentMethod = Entry('deposits/payment-method', 'private', 'POST', {}) + private_post_coinbase_accounts_id_addresses = privatePostCoinbaseAccountsIdAddresses = Entry('coinbase-accounts/{id}/addresses', 'private', 'POST', {}) + private_post_funding_repay = privatePostFundingRepay = Entry('funding/repay', 'private', 'POST', {}) + private_post_orders = privatePostOrders = Entry('orders', 'private', 'POST', {}) + private_post_position_close = privatePostPositionClose = Entry('position/close', 'private', 'POST', {}) + private_post_profiles_margin_transfer = privatePostProfilesMarginTransfer = Entry('profiles/margin-transfer', 'private', 'POST', {}) + private_post_profiles_transfer = privatePostProfilesTransfer = Entry('profiles/transfer', 'private', 'POST', {}) + private_post_reports = privatePostReports = Entry('reports', 'private', 'POST', {}) + private_post_withdrawals_coinbase = privatePostWithdrawalsCoinbase = Entry('withdrawals/coinbase', 'private', 'POST', {}) + private_post_withdrawals_coinbase_account = privatePostWithdrawalsCoinbaseAccount = Entry('withdrawals/coinbase-account', 'private', 'POST', {}) + private_post_withdrawals_crypto = privatePostWithdrawalsCrypto = Entry('withdrawals/crypto', 'private', 'POST', {}) + private_post_withdrawals_payment_method = privatePostWithdrawalsPaymentMethod = Entry('withdrawals/payment-method', 'private', 'POST', {}) + private_post_loans_open = privatePostLoansOpen = Entry('loans/open', 'private', 'POST', {}) + private_post_loans_repay_interest = privatePostLoansRepayInterest = Entry('loans/repay-interest', 'private', 'POST', {}) + private_post_loans_repay_principal = privatePostLoansRepayPrincipal = Entry('loans/repay-principal', 'private', 'POST', {}) + private_delete_orders = privateDeleteOrders = Entry('orders', 'private', 'DELETE', {}) + private_delete_orders_client_client_oid = privateDeleteOrdersClientClientOid = Entry('orders/client:{client_oid}', 'private', 'DELETE', {}) + private_delete_orders_id = privateDeleteOrdersId = Entry('orders/{id}', 'private', 'DELETE', {}) diff --git a/ccxt/abstract/coinbaseinternational.py b/ccxt/abstract/coinbaseinternational.py new file mode 100644 index 0000000..a0db910 --- /dev/null +++ b/ccxt/abstract/coinbaseinternational.py @@ -0,0 +1,39 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + v1_public_get_assets = v1PublicGetAssets = Entry('assets', ['v1', 'public'], 'GET', {}) + v1_public_get_assets_assets = v1PublicGetAssetsAssets = Entry('assets/{assets}', ['v1', 'public'], 'GET', {}) + v1_public_get_assets_asset_networks = v1PublicGetAssetsAssetNetworks = Entry('assets/{asset}/networks', ['v1', 'public'], 'GET', {}) + v1_public_get_instruments = v1PublicGetInstruments = Entry('instruments', ['v1', 'public'], 'GET', {}) + v1_public_get_instruments_instrument = v1PublicGetInstrumentsInstrument = Entry('instruments/{instrument}', ['v1', 'public'], 'GET', {}) + v1_public_get_instruments_instrument_quote = v1PublicGetInstrumentsInstrumentQuote = Entry('instruments/{instrument}/quote', ['v1', 'public'], 'GET', {}) + v1_public_get_instruments_instrument_funding = v1PublicGetInstrumentsInstrumentFunding = Entry('instruments/{instrument}/funding', ['v1', 'public'], 'GET', {}) + v1_public_get_instruments_instrument_candles = v1PublicGetInstrumentsInstrumentCandles = Entry('instruments/{instrument}/candles', ['v1', 'public'], 'GET', {}) + v1_private_get_orders = v1PrivateGetOrders = Entry('orders', ['v1', 'private'], 'GET', {}) + v1_private_get_orders_id = v1PrivateGetOrdersId = Entry('orders/{id}', ['v1', 'private'], 'GET', {}) + v1_private_get_portfolios = v1PrivateGetPortfolios = Entry('portfolios', ['v1', 'private'], 'GET', {}) + v1_private_get_portfolios_portfolio = v1PrivateGetPortfoliosPortfolio = Entry('portfolios/{portfolio}', ['v1', 'private'], 'GET', {}) + v1_private_get_portfolios_portfolio_detail = v1PrivateGetPortfoliosPortfolioDetail = Entry('portfolios/{portfolio}/detail', ['v1', 'private'], 'GET', {}) + v1_private_get_portfolios_portfolio_summary = v1PrivateGetPortfoliosPortfolioSummary = Entry('portfolios/{portfolio}/summary', ['v1', 'private'], 'GET', {}) + v1_private_get_portfolios_portfolio_balances = v1PrivateGetPortfoliosPortfolioBalances = Entry('portfolios/{portfolio}/balances', ['v1', 'private'], 'GET', {}) + v1_private_get_portfolios_portfolio_balances_asset = v1PrivateGetPortfoliosPortfolioBalancesAsset = Entry('portfolios/{portfolio}/balances/{asset}', ['v1', 'private'], 'GET', {}) + v1_private_get_portfolios_portfolio_positions = v1PrivateGetPortfoliosPortfolioPositions = Entry('portfolios/{portfolio}/positions', ['v1', 'private'], 'GET', {}) + v1_private_get_portfolios_portfolio_positions_instrument = v1PrivateGetPortfoliosPortfolioPositionsInstrument = Entry('portfolios/{portfolio}/positions/{instrument}', ['v1', 'private'], 'GET', {}) + v1_private_get_portfolios_fills = v1PrivateGetPortfoliosFills = Entry('portfolios/fills', ['v1', 'private'], 'GET', {}) + v1_private_get_portfolios_portfolio_fills = v1PrivateGetPortfoliosPortfolioFills = Entry('portfolios/{portfolio}/fills', ['v1', 'private'], 'GET', {}) + v1_private_get_transfers = v1PrivateGetTransfers = Entry('transfers', ['v1', 'private'], 'GET', {}) + v1_private_get_transfers_transfer_uuid = v1PrivateGetTransfersTransferUuid = Entry('transfers/{transfer_uuid}', ['v1', 'private'], 'GET', {}) + v1_private_post_orders = v1PrivatePostOrders = Entry('orders', ['v1', 'private'], 'POST', {}) + v1_private_post_portfolios = v1PrivatePostPortfolios = Entry('portfolios', ['v1', 'private'], 'POST', {}) + v1_private_post_portfolios_margin = v1PrivatePostPortfoliosMargin = Entry('portfolios/margin', ['v1', 'private'], 'POST', {}) + v1_private_post_portfolios_transfer = v1PrivatePostPortfoliosTransfer = Entry('portfolios/transfer', ['v1', 'private'], 'POST', {}) + v1_private_post_transfers_withdraw = v1PrivatePostTransfersWithdraw = Entry('transfers/withdraw', ['v1', 'private'], 'POST', {}) + v1_private_post_transfers_address = v1PrivatePostTransfersAddress = Entry('transfers/address', ['v1', 'private'], 'POST', {}) + v1_private_post_transfers_create_counterparty_id = v1PrivatePostTransfersCreateCounterpartyId = Entry('transfers/create-counterparty-id', ['v1', 'private'], 'POST', {}) + v1_private_post_transfers_validate_counterparty_id = v1PrivatePostTransfersValidateCounterpartyId = Entry('transfers/validate-counterparty-id', ['v1', 'private'], 'POST', {}) + v1_private_post_transfers_withdraw_counterparty = v1PrivatePostTransfersWithdrawCounterparty = Entry('transfers/withdraw/counterparty', ['v1', 'private'], 'POST', {}) + v1_private_put_orders_id = v1PrivatePutOrdersId = Entry('orders/{id}', ['v1', 'private'], 'PUT', {}) + v1_private_put_portfolios_portfolio = v1PrivatePutPortfoliosPortfolio = Entry('portfolios/{portfolio}', ['v1', 'private'], 'PUT', {}) + v1_private_delete_orders = v1PrivateDeleteOrders = Entry('orders', ['v1', 'private'], 'DELETE', {}) + v1_private_delete_orders_id = v1PrivateDeleteOrdersId = Entry('orders/{id}', ['v1', 'private'], 'DELETE', {}) diff --git a/ccxt/abstract/coincatch.py b/ccxt/abstract/coincatch.py new file mode 100644 index 0000000..ae3e40b --- /dev/null +++ b/ccxt/abstract/coincatch.py @@ -0,0 +1,94 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_api_spot_v1_public_time = publicGetApiSpotV1PublicTime = Entry('api/spot/v1/public/time', 'public', 'GET', {'cost': 1}) + public_get_api_spot_v1_public_currencies = publicGetApiSpotV1PublicCurrencies = Entry('api/spot/v1/public/currencies', 'public', 'GET', {'cost': 6.666666666666667}) + public_get_api_spot_v1_market_ticker = publicGetApiSpotV1MarketTicker = Entry('api/spot/v1/market/ticker', 'public', 'GET', {'cost': 1}) + public_get_api_spot_v1_market_tickers = publicGetApiSpotV1MarketTickers = Entry('api/spot/v1/market/tickers', 'public', 'GET', {'cost': 1}) + public_get_api_spot_v1_market_fills = publicGetApiSpotV1MarketFills = Entry('api/spot/v1/market/fills', 'public', 'GET', {'cost': 2}) + public_get_api_spot_v1_market_fills_history = publicGetApiSpotV1MarketFillsHistory = Entry('api/spot/v1/market/fills-history', 'public', 'GET', {'cost': 2}) + public_get_api_spot_v1_market_candles = publicGetApiSpotV1MarketCandles = Entry('api/spot/v1/market/candles', 'public', 'GET', {'cost': 1}) + public_get_api_spot_v1_market_history_candles = publicGetApiSpotV1MarketHistoryCandles = Entry('api/spot/v1/market/history-candles', 'public', 'GET', {'cost': 1}) + public_get_api_spot_v1_market_depth = publicGetApiSpotV1MarketDepth = Entry('api/spot/v1/market/depth', 'public', 'GET', {'cost': 1}) + public_get_api_spot_v1_market_merge_depth = publicGetApiSpotV1MarketMergeDepth = Entry('api/spot/v1/market/merge-depth', 'public', 'GET', {'cost': 1}) + public_get_api_mix_v1_market_contracts = publicGetApiMixV1MarketContracts = Entry('api/mix/v1/market/contracts', 'public', 'GET', {'cost': 1}) + public_get_api_mix_v1_market_merge_depth = publicGetApiMixV1MarketMergeDepth = Entry('api/mix/v1/market/merge-depth', 'public', 'GET', {'cost': 1}) + public_get_api_mix_v1_market_depth = publicGetApiMixV1MarketDepth = Entry('api/mix/v1/market/depth', 'public', 'GET', {'cost': 1}) + public_get_api_mix_v1_market_ticker = publicGetApiMixV1MarketTicker = Entry('api/mix/v1/market/ticker', 'public', 'GET', {'cost': 1}) + public_get_api_mix_v1_market_tickers = publicGetApiMixV1MarketTickers = Entry('api/mix/v1/market/tickers', 'public', 'GET', {'cost': 1}) + public_get_api_mix_v1_market_fills = publicGetApiMixV1MarketFills = Entry('api/mix/v1/market/fills', 'public', 'GET', {'cost': 1}) + public_get_api_mix_v1_market_fills_history = publicGetApiMixV1MarketFillsHistory = Entry('api/mix/v1/market/fills-history', 'public', 'GET', {'cost': 1}) + public_get_api_mix_v1_market_candles = publicGetApiMixV1MarketCandles = Entry('api/mix/v1/market/candles', 'public', 'GET', {'cost': 1}) + public_get_pi_mix_v1_market_index = publicGetPiMixV1MarketIndex = Entry('pi/mix/v1/market/index', 'public', 'GET', {'cost': 1}) + public_get_api_mix_v1_market_funding_time = publicGetApiMixV1MarketFundingTime = Entry('api/mix/v1/market/funding-time', 'public', 'GET', {'cost': 1}) + public_get_api_mix_v1_market_history_fundrate = publicGetApiMixV1MarketHistoryFundRate = Entry('api/mix/v1/market/history-fundRate', 'public', 'GET', {'cost': 1}) + public_get_api_mix_v1_market_current_fundrate = publicGetApiMixV1MarketCurrentFundRate = Entry('api/mix/v1/market/current-fundRate', 'public', 'GET', {'cost': 1}) + public_get_api_mix_v1_market_open_interest = publicGetApiMixV1MarketOpenInterest = Entry('api/mix/v1/market/open-interest', 'public', 'GET', {'cost': 1}) + public_get_api_mix_v1_market_mark_price = publicGetApiMixV1MarketMarkPrice = Entry('api/mix/v1/market/mark-price', 'public', 'GET', {'cost': 1}) + public_get_api_mix_v1_market_symbol_leverage = publicGetApiMixV1MarketSymbolLeverage = Entry('api/mix/v1/market/symbol-leverage', 'public', 'GET', {'cost': 1}) + public_get_api_mix_v1_market_querypositionlever = publicGetApiMixV1MarketQueryPositionLever = Entry('api/mix/v1/market/queryPositionLever', 'public', 'GET', {'cost': 1}) + private_get_api_spot_v1_wallet_deposit_address = privateGetApiSpotV1WalletDepositAddress = Entry('api/spot/v1/wallet/deposit-address', 'private', 'GET', {'cost': 4}) + private_get_pi_spot_v1_wallet_withdrawal_list = privateGetPiSpotV1WalletWithdrawalList = Entry('pi/spot/v1/wallet/withdrawal-list', 'private', 'GET', {'cost': 1}) + private_get_api_spot_v1_wallet_withdrawal_list_v2 = privateGetApiSpotV1WalletWithdrawalListV2 = Entry('api/spot/v1/wallet/withdrawal-list-v2', 'private', 'GET', {'cost': 1}) + private_get_api_spot_v1_wallet_deposit_list = privateGetApiSpotV1WalletDepositList = Entry('api/spot/v1/wallet/deposit-list', 'private', 'GET', {'cost': 1}) + private_get_api_spot_v1_account_getinfo = privateGetApiSpotV1AccountGetInfo = Entry('api/spot/v1/account/getInfo', 'private', 'GET', {'cost': 1}) + private_get_api_spot_v1_account_assets = privateGetApiSpotV1AccountAssets = Entry('api/spot/v1/account/assets', 'private', 'GET', {'cost': 2}) + private_get_api_spot_v1_account_transferrecords = privateGetApiSpotV1AccountTransferRecords = Entry('api/spot/v1/account/transferRecords', 'private', 'GET', {'cost': 1}) + private_get_api_mix_v1_account_account = privateGetApiMixV1AccountAccount = Entry('api/mix/v1/account/account', 'private', 'GET', {'cost': 2}) + private_get_api_mix_v1_account_accounts = privateGetApiMixV1AccountAccounts = Entry('api/mix/v1/account/accounts', 'private', 'GET', {'cost': 2}) + private_get_api_mix_v1_position_singleposition_v2 = privateGetApiMixV1PositionSinglePositionV2 = Entry('api/mix/v1/position/singlePosition-v2', 'private', 'GET', {'cost': 2}) + private_get_api_mix_v1_position_allposition_v2 = privateGetApiMixV1PositionAllPositionV2 = Entry('api/mix/v1/position/allPosition-v2', 'private', 'GET', {'cost': 4}) + private_get_api_mix_v1_account_accountbill = privateGetApiMixV1AccountAccountBill = Entry('api/mix/v1/account/accountBill', 'private', 'GET', {'cost': 2}) + private_get_api_mix_v1_account_accountbusinessbill = privateGetApiMixV1AccountAccountBusinessBill = Entry('api/mix/v1/account/accountBusinessBill', 'private', 'GET', {'cost': 4}) + private_get_api_mix_v1_order_current = privateGetApiMixV1OrderCurrent = Entry('api/mix/v1/order/current', 'private', 'GET', {'cost': 1}) + private_get_api_mix_v1_order_margincoincurrent = privateGetApiMixV1OrderMarginCoinCurrent = Entry('api/mix/v1/order/marginCoinCurrent', 'private', 'GET', {'cost': 1}) + private_get_api_mix_v1_order_history = privateGetApiMixV1OrderHistory = Entry('api/mix/v1/order/history', 'private', 'GET', {'cost': 2}) + private_get_api_mix_v1_order_historyproducttype = privateGetApiMixV1OrderHistoryProductType = Entry('api/mix/v1/order/historyProductType', 'private', 'GET', {'cost': 4}) + private_get_api_mix_v1_order_detail = privateGetApiMixV1OrderDetail = Entry('api/mix/v1/order/detail', 'private', 'GET', {'cost': 2}) + private_get_api_mix_v1_order_fills = privateGetApiMixV1OrderFills = Entry('api/mix/v1/order/fills', 'private', 'GET', {'cost': 2}) + private_get_api_mix_v1_order_allfills = privateGetApiMixV1OrderAllFills = Entry('api/mix/v1/order/allFills', 'private', 'GET', {'cost': 2}) + private_get_api_mix_v1_plan_currentplan = privateGetApiMixV1PlanCurrentPlan = Entry('api/mix/v1/plan/currentPlan', 'private', 'GET', {'cost': 1}) + private_get_api_mix_v1_plan_historyplan = privateGetApiMixV1PlanHistoryPlan = Entry('api/mix/v1/plan/historyPlan', 'private', 'GET', {'cost': 2}) + private_post_api_spot_v1_wallet_transfer_v2 = privatePostApiSpotV1WalletTransferV2 = Entry('api/spot/v1/wallet/transfer-v2', 'private', 'POST', {'cost': 4}) + private_post_api_spot_v1_wallet_withdrawal_v2 = privatePostApiSpotV1WalletWithdrawalV2 = Entry('api/spot/v1/wallet/withdrawal-v2', 'private', 'POST', {'cost': 4}) + private_post_api_spot_v1_wallet_withdrawal_inner_v2 = privatePostApiSpotV1WalletWithdrawalInnerV2 = Entry('api/spot/v1/wallet/withdrawal-inner-v2', 'private', 'POST', {'cost': 1}) + private_post_api_spot_v1_account_bills = privatePostApiSpotV1AccountBills = Entry('api/spot/v1/account/bills', 'private', 'POST', {'cost': 2}) + private_post_api_spot_v1_trade_orders = privatePostApiSpotV1TradeOrders = Entry('api/spot/v1/trade/orders', 'private', 'POST', {'cost': 2}) + private_post_api_spot_v1_trade_batch_orders = privatePostApiSpotV1TradeBatchOrders = Entry('api/spot/v1/trade/batch-orders', 'private', 'POST', {'cost': 4, 'step': 10}) + private_post_api_spot_v1_trade_cancel_order = privatePostApiSpotV1TradeCancelOrder = Entry('api/spot/v1/trade/cancel-order', 'private', 'POST', {'cost': 1}) + private_post_api_spot_v1_trade_cancel_order_v2 = privatePostApiSpotV1TradeCancelOrderV2 = Entry('api/spot/v1/trade/cancel-order-v2', 'private', 'POST', {'cost': 2}) + private_post_api_spot_v1_trade_cancel_symbol_order = privatePostApiSpotV1TradeCancelSymbolOrder = Entry('api/spot/v1/trade/cancel-symbol-order', 'private', 'POST', {'cost': 2}) + private_post_api_spot_v1_trade_cancel_batch_orders = privatePostApiSpotV1TradeCancelBatchOrders = Entry('api/spot/v1/trade/cancel-batch-orders', 'private', 'POST', {'cost': 1}) + private_post_api_spot_v1_trade_cancel_batch_orders_v2 = privatePostApiSpotV1TradeCancelBatchOrdersV2 = Entry('api/spot/v1/trade/cancel-batch-orders-v2', 'private', 'POST', {'cost': 1}) + private_post_api_spot_v1_trade_orderinfo = privatePostApiSpotV1TradeOrderInfo = Entry('api/spot/v1/trade/orderInfo', 'private', 'POST', {'cost': 1}) + private_post_api_spot_v1_trade_open_orders = privatePostApiSpotV1TradeOpenOrders = Entry('api/spot/v1/trade/open-orders', 'private', 'POST', {'cost': 1}) + private_post_api_spot_v1_trade_history = privatePostApiSpotV1TradeHistory = Entry('api/spot/v1/trade/history', 'private', 'POST', {'cost': 1}) + private_post_api_spot_v1_trade_fills = privatePostApiSpotV1TradeFills = Entry('api/spot/v1/trade/fills', 'private', 'POST', {'cost': 1}) + private_post_api_spot_v1_plan_placeplan = privatePostApiSpotV1PlanPlacePlan = Entry('api/spot/v1/plan/placePlan', 'private', 'POST', {'cost': 1}) + private_post_api_spot_v1_plan_modifyplan = privatePostApiSpotV1PlanModifyPlan = Entry('api/spot/v1/plan/modifyPlan', 'private', 'POST', {'cost': 1}) + private_post_api_spot_v1_plan_cancelplan = privatePostApiSpotV1PlanCancelPlan = Entry('api/spot/v1/plan/cancelPlan', 'private', 'POST', {'cost': 1}) + private_post_api_spot_v1_plan_currentplan = privatePostApiSpotV1PlanCurrentPlan = Entry('api/spot/v1/plan/currentPlan', 'private', 'POST', {'cost': 1}) + private_post_api_spot_v1_plan_historyplan = privatePostApiSpotV1PlanHistoryPlan = Entry('api/spot/v1/plan/historyPlan', 'private', 'POST', {'cost': 1}) + private_post_api_spot_v1_plan_batchcancelplan = privatePostApiSpotV1PlanBatchCancelPlan = Entry('api/spot/v1/plan/batchCancelPlan', 'private', 'POST', {'cost': 2}) + private_post_api_mix_v1_account_open_count = privatePostApiMixV1AccountOpenCount = Entry('api/mix/v1/account/open-count', 'private', 'POST', {'cost': 1}) + private_post_api_mix_v1_account_setleverage = privatePostApiMixV1AccountSetLeverage = Entry('api/mix/v1/account/setLeverage', 'private', 'POST', {'cost': 4}) + private_post_api_mix_v1_account_setmargin = privatePostApiMixV1AccountSetMargin = Entry('api/mix/v1/account/setMargin', 'private', 'POST', {'cost': 4}) + private_post_api_mix_v1_account_setmarginmode = privatePostApiMixV1AccountSetMarginMode = Entry('api/mix/v1/account/setMarginMode', 'private', 'POST', {'cost': 4}) + private_post_api_mix_v1_account_setpositionmode = privatePostApiMixV1AccountSetPositionMode = Entry('api/mix/v1/account/setPositionMode', 'private', 'POST', {'cost': 4}) + private_post_api_mix_v1_order_placeorder = privatePostApiMixV1OrderPlaceOrder = Entry('api/mix/v1/order/placeOrder', 'private', 'POST', {'cost': 2}) + private_post_api_mix_v1_order_batch_orders = privatePostApiMixV1OrderBatchOrders = Entry('api/mix/v1/order/batch-orders', 'private', 'POST', {'cost': 4, 'step': 10}) + private_post_api_mix_v1_order_cancel_order = privatePostApiMixV1OrderCancelOrder = Entry('api/mix/v1/order/cancel-order', 'private', 'POST', {'cost': 2}) + private_post_api_mix_v1_order_cancel_batch_orders = privatePostApiMixV1OrderCancelBatchOrders = Entry('api/mix/v1/order/cancel-batch-orders', 'private', 'POST', {'cost': 2}) + private_post_api_mix_v1_order_cancel_symbol_orders = privatePostApiMixV1OrderCancelSymbolOrders = Entry('api/mix/v1/order/cancel-symbol-orders', 'private', 'POST', {'cost': 2}) + private_post_api_mix_v1_order_cancel_all_orders = privatePostApiMixV1OrderCancelAllOrders = Entry('api/mix/v1/order/cancel-all-orders', 'private', 'POST', {'cost': 2}) + private_post_api_mix_v1_plan_placeplan = privatePostApiMixV1PlanPlacePlan = Entry('api/mix/v1/plan/placePlan', 'private', 'POST', {'cost': 2}) + private_post_api_mix_v1_plan_modifyplan = privatePostApiMixV1PlanModifyPlan = Entry('api/mix/v1/plan/modifyPlan', 'private', 'POST', {'cost': 2}) + private_post_api_mix_v1_plan_modifyplanpreset = privatePostApiMixV1PlanModifyPlanPreset = Entry('api/mix/v1/plan/modifyPlanPreset', 'private', 'POST', {'cost': 2}) + private_post_api_mix_v1_plan_placetpsl = privatePostApiMixV1PlanPlaceTPSL = Entry('api/mix/v1/plan/placeTPSL', 'private', 'POST', {'cost': 2}) + private_post_api_mix_v1_plan_placetrailstop = privatePostApiMixV1PlanPlaceTrailStop = Entry('api/mix/v1/plan/placeTrailStop', 'private', 'POST', {'cost': 2}) + private_post_api_mix_v1_plan_placepositionstpsl = privatePostApiMixV1PlanPlacePositionsTPSL = Entry('api/mix/v1/plan/placePositionsTPSL', 'private', 'POST', {'cost': 2}) + private_post_api_mix_v1_plan_modifytpslplan = privatePostApiMixV1PlanModifyTPSLPlan = Entry('api/mix/v1/plan/modifyTPSLPlan', 'private', 'POST', {'cost': 2}) + private_post_api_mix_v1_plan_cancelplan = privatePostApiMixV1PlanCancelPlan = Entry('api/mix/v1/plan/cancelPlan', 'private', 'POST', {'cost': 2}) + private_post_api_mix_v1_plan_cancelsymbolplan = privatePostApiMixV1PlanCancelSymbolPlan = Entry('api/mix/v1/plan/cancelSymbolPlan', 'private', 'POST', {'cost': 2}) + private_post_api_mix_v1_plan_cancelallplan = privatePostApiMixV1PlanCancelAllPlan = Entry('api/mix/v1/plan/cancelAllPlan', 'private', 'POST', {'cost': 2}) diff --git a/ccxt/abstract/coincheck.py b/ccxt/abstract/coincheck.py new file mode 100644 index 0000000..68e52a9 --- /dev/null +++ b/ccxt/abstract/coincheck.py @@ -0,0 +1,33 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_exchange_orders_rate = publicGetExchangeOrdersRate = Entry('exchange/orders/rate', 'public', 'GET', {}) + public_get_order_books = publicGetOrderBooks = Entry('order_books', 'public', 'GET', {}) + public_get_rate_pair = publicGetRatePair = Entry('rate/{pair}', 'public', 'GET', {}) + public_get_ticker = publicGetTicker = Entry('ticker', 'public', 'GET', {}) + public_get_trades = publicGetTrades = Entry('trades', 'public', 'GET', {}) + private_get_accounts = privateGetAccounts = Entry('accounts', 'private', 'GET', {}) + private_get_accounts_balance = privateGetAccountsBalance = Entry('accounts/balance', 'private', 'GET', {}) + private_get_accounts_leverage_balance = privateGetAccountsLeverageBalance = Entry('accounts/leverage_balance', 'private', 'GET', {}) + private_get_bank_accounts = privateGetBankAccounts = Entry('bank_accounts', 'private', 'GET', {}) + private_get_deposit_money = privateGetDepositMoney = Entry('deposit_money', 'private', 'GET', {}) + private_get_exchange_orders_opens = privateGetExchangeOrdersOpens = Entry('exchange/orders/opens', 'private', 'GET', {}) + private_get_exchange_orders_transactions = privateGetExchangeOrdersTransactions = Entry('exchange/orders/transactions', 'private', 'GET', {}) + private_get_exchange_orders_transactions_pagination = privateGetExchangeOrdersTransactionsPagination = Entry('exchange/orders/transactions_pagination', 'private', 'GET', {}) + private_get_exchange_leverage_positions = privateGetExchangeLeveragePositions = Entry('exchange/leverage/positions', 'private', 'GET', {}) + private_get_lending_borrows_matches = privateGetLendingBorrowsMatches = Entry('lending/borrows/matches', 'private', 'GET', {}) + private_get_send_money = privateGetSendMoney = Entry('send_money', 'private', 'GET', {}) + private_get_withdraws = privateGetWithdraws = Entry('withdraws', 'private', 'GET', {}) + private_post_bank_accounts = privatePostBankAccounts = Entry('bank_accounts', 'private', 'POST', {}) + private_post_deposit_money_id_fast = privatePostDepositMoneyIdFast = Entry('deposit_money/{id}/fast', 'private', 'POST', {}) + private_post_exchange_orders = privatePostExchangeOrders = Entry('exchange/orders', 'private', 'POST', {}) + private_post_exchange_transfers_to_leverage = privatePostExchangeTransfersToLeverage = Entry('exchange/transfers/to_leverage', 'private', 'POST', {}) + private_post_exchange_transfers_from_leverage = privatePostExchangeTransfersFromLeverage = Entry('exchange/transfers/from_leverage', 'private', 'POST', {}) + private_post_lending_borrows = privatePostLendingBorrows = Entry('lending/borrows', 'private', 'POST', {}) + private_post_lending_borrows_id_repay = privatePostLendingBorrowsIdRepay = Entry('lending/borrows/{id}/repay', 'private', 'POST', {}) + private_post_send_money = privatePostSendMoney = Entry('send_money', 'private', 'POST', {}) + private_post_withdraws = privatePostWithdraws = Entry('withdraws', 'private', 'POST', {}) + private_delete_bank_accounts_id = privateDeleteBankAccountsId = Entry('bank_accounts/{id}', 'private', 'DELETE', {}) + private_delete_exchange_orders_id = privateDeleteExchangeOrdersId = Entry('exchange/orders/{id}', 'private', 'DELETE', {}) + private_delete_withdraws_id = privateDeleteWithdrawsId = Entry('withdraws/{id}', 'private', 'DELETE', {}) diff --git a/ccxt/abstract/coinex.py b/ccxt/abstract/coinex.py new file mode 100644 index 0000000..29e7d4e --- /dev/null +++ b/ccxt/abstract/coinex.py @@ -0,0 +1,237 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + v1_public_get_amm_market = v1PublicGetAmmMarket = Entry('amm/market', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_common_currency_rate = v1PublicGetCommonCurrencyRate = Entry('common/currency/rate', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_common_asset_config = v1PublicGetCommonAssetConfig = Entry('common/asset/config', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_common_maintain_info = v1PublicGetCommonMaintainInfo = Entry('common/maintain/info', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_common_temp_maintain_info = v1PublicGetCommonTempMaintainInfo = Entry('common/temp-maintain/info', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_margin_market = v1PublicGetMarginMarket = Entry('margin/market', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_market_info = v1PublicGetMarketInfo = Entry('market/info', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_market_list = v1PublicGetMarketList = Entry('market/list', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_market_ticker = v1PublicGetMarketTicker = Entry('market/ticker', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_market_ticker_all = v1PublicGetMarketTickerAll = Entry('market/ticker/all', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_market_depth = v1PublicGetMarketDepth = Entry('market/depth', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_market_deals = v1PublicGetMarketDeals = Entry('market/deals', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_market_kline = v1PublicGetMarketKline = Entry('market/kline', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_market_detail = v1PublicGetMarketDetail = Entry('market/detail', ['v1', 'public'], 'GET', {'cost': 1}) + v1_private_get_account_amm_balance = v1PrivateGetAccountAmmBalance = Entry('account/amm/balance', ['v1', 'private'], 'GET', {'cost': 40}) + v1_private_get_account_investment_balance = v1PrivateGetAccountInvestmentBalance = Entry('account/investment/balance', ['v1', 'private'], 'GET', {'cost': 40}) + v1_private_get_account_balance_history = v1PrivateGetAccountBalanceHistory = Entry('account/balance/history', ['v1', 'private'], 'GET', {'cost': 40}) + v1_private_get_account_market_fee = v1PrivateGetAccountMarketFee = Entry('account/market/fee', ['v1', 'private'], 'GET', {'cost': 40}) + v1_private_get_balance_coin_deposit = v1PrivateGetBalanceCoinDeposit = Entry('balance/coin/deposit', ['v1', 'private'], 'GET', {'cost': 40}) + v1_private_get_balance_coin_withdraw = v1PrivateGetBalanceCoinWithdraw = Entry('balance/coin/withdraw', ['v1', 'private'], 'GET', {'cost': 40}) + v1_private_get_balance_info = v1PrivateGetBalanceInfo = Entry('balance/info', ['v1', 'private'], 'GET', {'cost': 40}) + v1_private_get_balance_deposit_address_coin_type = v1PrivateGetBalanceDepositAddressCoinType = Entry('balance/deposit/address/{coin_type}', ['v1', 'private'], 'GET', {'cost': 40}) + v1_private_get_contract_transfer_history = v1PrivateGetContractTransferHistory = Entry('contract/transfer/history', ['v1', 'private'], 'GET', {'cost': 40}) + v1_private_get_credit_info = v1PrivateGetCreditInfo = Entry('credit/info', ['v1', 'private'], 'GET', {'cost': 40}) + v1_private_get_credit_balance = v1PrivateGetCreditBalance = Entry('credit/balance', ['v1', 'private'], 'GET', {'cost': 40}) + v1_private_get_investment_transfer_history = v1PrivateGetInvestmentTransferHistory = Entry('investment/transfer/history', ['v1', 'private'], 'GET', {'cost': 40}) + v1_private_get_margin_account = v1PrivateGetMarginAccount = Entry('margin/account', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_margin_config = v1PrivateGetMarginConfig = Entry('margin/config', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_margin_loan_history = v1PrivateGetMarginLoanHistory = Entry('margin/loan/history', ['v1', 'private'], 'GET', {'cost': 40}) + v1_private_get_margin_transfer_history = v1PrivateGetMarginTransferHistory = Entry('margin/transfer/history', ['v1', 'private'], 'GET', {'cost': 40}) + v1_private_get_order_deals = v1PrivateGetOrderDeals = Entry('order/deals', ['v1', 'private'], 'GET', {'cost': 40}) + v1_private_get_order_finished = v1PrivateGetOrderFinished = Entry('order/finished', ['v1', 'private'], 'GET', {'cost': 40}) + v1_private_get_order_pending = v1PrivateGetOrderPending = Entry('order/pending', ['v1', 'private'], 'GET', {'cost': 8}) + v1_private_get_order_status = v1PrivateGetOrderStatus = Entry('order/status', ['v1', 'private'], 'GET', {'cost': 8}) + v1_private_get_order_status_batch = v1PrivateGetOrderStatusBatch = Entry('order/status/batch', ['v1', 'private'], 'GET', {'cost': 8}) + v1_private_get_order_user_deals = v1PrivateGetOrderUserDeals = Entry('order/user/deals', ['v1', 'private'], 'GET', {'cost': 40}) + v1_private_get_order_stop_finished = v1PrivateGetOrderStopFinished = Entry('order/stop/finished', ['v1', 'private'], 'GET', {'cost': 40}) + v1_private_get_order_stop_pending = v1PrivateGetOrderStopPending = Entry('order/stop/pending', ['v1', 'private'], 'GET', {'cost': 8}) + v1_private_get_order_user_trade_fee = v1PrivateGetOrderUserTradeFee = Entry('order/user/trade/fee', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_order_market_trade_info = v1PrivateGetOrderMarketTradeInfo = Entry('order/market/trade/info', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_sub_account_balance = v1PrivateGetSubAccountBalance = Entry('sub_account/balance', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_sub_account_transfer_history = v1PrivateGetSubAccountTransferHistory = Entry('sub_account/transfer/history', ['v1', 'private'], 'GET', {'cost': 40}) + v1_private_get_sub_account_auth_api = v1PrivateGetSubAccountAuthApi = Entry('sub_account/auth/api', ['v1', 'private'], 'GET', {'cost': 40}) + v1_private_get_sub_account_auth_api_user_auth_id = v1PrivateGetSubAccountAuthApiUserAuthId = Entry('sub_account/auth/api/{user_auth_id}', ['v1', 'private'], 'GET', {'cost': 40}) + v1_private_post_balance_coin_withdraw = v1PrivatePostBalanceCoinWithdraw = Entry('balance/coin/withdraw', ['v1', 'private'], 'POST', {'cost': 40}) + v1_private_post_contract_balance_transfer = v1PrivatePostContractBalanceTransfer = Entry('contract/balance/transfer', ['v1', 'private'], 'POST', {'cost': 40}) + v1_private_post_margin_flat = v1PrivatePostMarginFlat = Entry('margin/flat', ['v1', 'private'], 'POST', {'cost': 40}) + v1_private_post_margin_loan = v1PrivatePostMarginLoan = Entry('margin/loan', ['v1', 'private'], 'POST', {'cost': 40}) + v1_private_post_margin_transfer = v1PrivatePostMarginTransfer = Entry('margin/transfer', ['v1', 'private'], 'POST', {'cost': 40}) + v1_private_post_order_limit_batch = v1PrivatePostOrderLimitBatch = Entry('order/limit/batch', ['v1', 'private'], 'POST', {'cost': 40}) + v1_private_post_order_ioc = v1PrivatePostOrderIoc = Entry('order/ioc', ['v1', 'private'], 'POST', {'cost': 13.334}) + v1_private_post_order_limit = v1PrivatePostOrderLimit = Entry('order/limit', ['v1', 'private'], 'POST', {'cost': 13.334}) + v1_private_post_order_market = v1PrivatePostOrderMarket = Entry('order/market', ['v1', 'private'], 'POST', {'cost': 13.334}) + v1_private_post_order_modify = v1PrivatePostOrderModify = Entry('order/modify', ['v1', 'private'], 'POST', {'cost': 13.334}) + v1_private_post_order_stop_limit = v1PrivatePostOrderStopLimit = Entry('order/stop/limit', ['v1', 'private'], 'POST', {'cost': 13.334}) + v1_private_post_order_stop_market = v1PrivatePostOrderStopMarket = Entry('order/stop/market', ['v1', 'private'], 'POST', {'cost': 13.334}) + v1_private_post_order_stop_modify = v1PrivatePostOrderStopModify = Entry('order/stop/modify', ['v1', 'private'], 'POST', {'cost': 13.334}) + v1_private_post_sub_account_transfer = v1PrivatePostSubAccountTransfer = Entry('sub_account/transfer', ['v1', 'private'], 'POST', {'cost': 40}) + v1_private_post_sub_account_register = v1PrivatePostSubAccountRegister = Entry('sub_account/register', ['v1', 'private'], 'POST', {'cost': 1}) + v1_private_post_sub_account_unfrozen = v1PrivatePostSubAccountUnfrozen = Entry('sub_account/unfrozen', ['v1', 'private'], 'POST', {'cost': 40}) + v1_private_post_sub_account_frozen = v1PrivatePostSubAccountFrozen = Entry('sub_account/frozen', ['v1', 'private'], 'POST', {'cost': 40}) + v1_private_post_sub_account_auth_api = v1PrivatePostSubAccountAuthApi = Entry('sub_account/auth/api', ['v1', 'private'], 'POST', {'cost': 40}) + v1_private_put_balance_deposit_address_coin_type = v1PrivatePutBalanceDepositAddressCoinType = Entry('balance/deposit/address/{coin_type}', ['v1', 'private'], 'PUT', {'cost': 40}) + v1_private_put_sub_account_unfrozen = v1PrivatePutSubAccountUnfrozen = Entry('sub_account/unfrozen', ['v1', 'private'], 'PUT', {'cost': 40}) + v1_private_put_sub_account_frozen = v1PrivatePutSubAccountFrozen = Entry('sub_account/frozen', ['v1', 'private'], 'PUT', {'cost': 40}) + v1_private_put_sub_account_auth_api_user_auth_id = v1PrivatePutSubAccountAuthApiUserAuthId = Entry('sub_account/auth/api/{user_auth_id}', ['v1', 'private'], 'PUT', {'cost': 40}) + v1_private_put_v1_account_settings = v1PrivatePutV1AccountSettings = Entry('v1/account/settings', ['v1', 'private'], 'PUT', {'cost': 40}) + v1_private_delete_balance_coin_withdraw = v1PrivateDeleteBalanceCoinWithdraw = Entry('balance/coin/withdraw', ['v1', 'private'], 'DELETE', {'cost': 40}) + v1_private_delete_order_pending_batch = v1PrivateDeleteOrderPendingBatch = Entry('order/pending/batch', ['v1', 'private'], 'DELETE', {'cost': 40}) + v1_private_delete_order_pending = v1PrivateDeleteOrderPending = Entry('order/pending', ['v1', 'private'], 'DELETE', {'cost': 13.334}) + v1_private_delete_order_stop_pending = v1PrivateDeleteOrderStopPending = Entry('order/stop/pending', ['v1', 'private'], 'DELETE', {'cost': 40}) + v1_private_delete_order_stop_pending_id = v1PrivateDeleteOrderStopPendingId = Entry('order/stop/pending/{id}', ['v1', 'private'], 'DELETE', {'cost': 13.334}) + v1_private_delete_order_pending_by_client_id = v1PrivateDeleteOrderPendingByClientId = Entry('order/pending/by_client_id', ['v1', 'private'], 'DELETE', {'cost': 40}) + v1_private_delete_order_stop_pending_by_client_id = v1PrivateDeleteOrderStopPendingByClientId = Entry('order/stop/pending/by_client_id', ['v1', 'private'], 'DELETE', {'cost': 40}) + v1_private_delete_sub_account_auth_api_user_auth_id = v1PrivateDeleteSubAccountAuthApiUserAuthId = Entry('sub_account/auth/api/{user_auth_id}', ['v1', 'private'], 'DELETE', {'cost': 40}) + v1_private_delete_sub_account_authorize_id = v1PrivateDeleteSubAccountAuthorizeId = Entry('sub_account/authorize/{id}', ['v1', 'private'], 'DELETE', {'cost': 40}) + v1_perpetualpublic_get_ping = v1PerpetualPublicGetPing = Entry('ping', ['v1', 'perpetualPublic'], 'GET', {'cost': 1}) + v1_perpetualpublic_get_time = v1PerpetualPublicGetTime = Entry('time', ['v1', 'perpetualPublic'], 'GET', {'cost': 1}) + v1_perpetualpublic_get_market_list = v1PerpetualPublicGetMarketList = Entry('market/list', ['v1', 'perpetualPublic'], 'GET', {'cost': 1}) + v1_perpetualpublic_get_market_limit_config = v1PerpetualPublicGetMarketLimitConfig = Entry('market/limit_config', ['v1', 'perpetualPublic'], 'GET', {'cost': 1}) + v1_perpetualpublic_get_market_ticker = v1PerpetualPublicGetMarketTicker = Entry('market/ticker', ['v1', 'perpetualPublic'], 'GET', {'cost': 1}) + v1_perpetualpublic_get_market_ticker_all = v1PerpetualPublicGetMarketTickerAll = Entry('market/ticker/all', ['v1', 'perpetualPublic'], 'GET', {'cost': 1}) + v1_perpetualpublic_get_market_depth = v1PerpetualPublicGetMarketDepth = Entry('market/depth', ['v1', 'perpetualPublic'], 'GET', {'cost': 1}) + v1_perpetualpublic_get_market_deals = v1PerpetualPublicGetMarketDeals = Entry('market/deals', ['v1', 'perpetualPublic'], 'GET', {'cost': 1}) + v1_perpetualpublic_get_market_funding_history = v1PerpetualPublicGetMarketFundingHistory = Entry('market/funding_history', ['v1', 'perpetualPublic'], 'GET', {'cost': 1}) + v1_perpetualpublic_get_market_kline = v1PerpetualPublicGetMarketKline = Entry('market/kline', ['v1', 'perpetualPublic'], 'GET', {'cost': 1}) + v1_perpetualprivate_get_market_user_deals = v1PerpetualPrivateGetMarketUserDeals = Entry('market/user_deals', ['v1', 'perpetualPrivate'], 'GET', {'cost': 1}) + v1_perpetualprivate_get_asset_query = v1PerpetualPrivateGetAssetQuery = Entry('asset/query', ['v1', 'perpetualPrivate'], 'GET', {'cost': 40}) + v1_perpetualprivate_get_order_pending = v1PerpetualPrivateGetOrderPending = Entry('order/pending', ['v1', 'perpetualPrivate'], 'GET', {'cost': 8}) + v1_perpetualprivate_get_order_finished = v1PerpetualPrivateGetOrderFinished = Entry('order/finished', ['v1', 'perpetualPrivate'], 'GET', {'cost': 40}) + v1_perpetualprivate_get_order_stop_finished = v1PerpetualPrivateGetOrderStopFinished = Entry('order/stop_finished', ['v1', 'perpetualPrivate'], 'GET', {'cost': 40}) + v1_perpetualprivate_get_order_stop_pending = v1PerpetualPrivateGetOrderStopPending = Entry('order/stop_pending', ['v1', 'perpetualPrivate'], 'GET', {'cost': 8}) + v1_perpetualprivate_get_order_status = v1PerpetualPrivateGetOrderStatus = Entry('order/status', ['v1', 'perpetualPrivate'], 'GET', {'cost': 8}) + v1_perpetualprivate_get_order_stop_status = v1PerpetualPrivateGetOrderStopStatus = Entry('order/stop_status', ['v1', 'perpetualPrivate'], 'GET', {'cost': 8}) + v1_perpetualprivate_get_position_finished = v1PerpetualPrivateGetPositionFinished = Entry('position/finished', ['v1', 'perpetualPrivate'], 'GET', {'cost': 40}) + v1_perpetualprivate_get_position_pending = v1PerpetualPrivateGetPositionPending = Entry('position/pending', ['v1', 'perpetualPrivate'], 'GET', {'cost': 40}) + v1_perpetualprivate_get_position_funding = v1PerpetualPrivateGetPositionFunding = Entry('position/funding', ['v1', 'perpetualPrivate'], 'GET', {'cost': 40}) + v1_perpetualprivate_get_position_adl_history = v1PerpetualPrivateGetPositionAdlHistory = Entry('position/adl_history', ['v1', 'perpetualPrivate'], 'GET', {'cost': 40}) + v1_perpetualprivate_get_market_preference = v1PerpetualPrivateGetMarketPreference = Entry('market/preference', ['v1', 'perpetualPrivate'], 'GET', {'cost': 40}) + v1_perpetualprivate_get_position_margin_history = v1PerpetualPrivateGetPositionMarginHistory = Entry('position/margin_history', ['v1', 'perpetualPrivate'], 'GET', {'cost': 40}) + v1_perpetualprivate_get_position_settle_history = v1PerpetualPrivateGetPositionSettleHistory = Entry('position/settle_history', ['v1', 'perpetualPrivate'], 'GET', {'cost': 40}) + v1_perpetualprivate_post_market_adjust_leverage = v1PerpetualPrivatePostMarketAdjustLeverage = Entry('market/adjust_leverage', ['v1', 'perpetualPrivate'], 'POST', {'cost': 1}) + v1_perpetualprivate_post_market_position_expect = v1PerpetualPrivatePostMarketPositionExpect = Entry('market/position_expect', ['v1', 'perpetualPrivate'], 'POST', {'cost': 1}) + v1_perpetualprivate_post_order_put_limit = v1PerpetualPrivatePostOrderPutLimit = Entry('order/put_limit', ['v1', 'perpetualPrivate'], 'POST', {'cost': 20}) + v1_perpetualprivate_post_order_put_market = v1PerpetualPrivatePostOrderPutMarket = Entry('order/put_market', ['v1', 'perpetualPrivate'], 'POST', {'cost': 20}) + v1_perpetualprivate_post_order_put_stop_limit = v1PerpetualPrivatePostOrderPutStopLimit = Entry('order/put_stop_limit', ['v1', 'perpetualPrivate'], 'POST', {'cost': 20}) + v1_perpetualprivate_post_order_put_stop_market = v1PerpetualPrivatePostOrderPutStopMarket = Entry('order/put_stop_market', ['v1', 'perpetualPrivate'], 'POST', {'cost': 20}) + v1_perpetualprivate_post_order_modify = v1PerpetualPrivatePostOrderModify = Entry('order/modify', ['v1', 'perpetualPrivate'], 'POST', {'cost': 20}) + v1_perpetualprivate_post_order_modify_stop = v1PerpetualPrivatePostOrderModifyStop = Entry('order/modify_stop', ['v1', 'perpetualPrivate'], 'POST', {'cost': 20}) + v1_perpetualprivate_post_order_cancel = v1PerpetualPrivatePostOrderCancel = Entry('order/cancel', ['v1', 'perpetualPrivate'], 'POST', {'cost': 20}) + v1_perpetualprivate_post_order_cancel_all = v1PerpetualPrivatePostOrderCancelAll = Entry('order/cancel_all', ['v1', 'perpetualPrivate'], 'POST', {'cost': 40}) + v1_perpetualprivate_post_order_cancel_batch = v1PerpetualPrivatePostOrderCancelBatch = Entry('order/cancel_batch', ['v1', 'perpetualPrivate'], 'POST', {'cost': 40}) + v1_perpetualprivate_post_order_cancel_stop = v1PerpetualPrivatePostOrderCancelStop = Entry('order/cancel_stop', ['v1', 'perpetualPrivate'], 'POST', {'cost': 20}) + v1_perpetualprivate_post_order_cancel_stop_all = v1PerpetualPrivatePostOrderCancelStopAll = Entry('order/cancel_stop_all', ['v1', 'perpetualPrivate'], 'POST', {'cost': 40}) + v1_perpetualprivate_post_order_close_limit = v1PerpetualPrivatePostOrderCloseLimit = Entry('order/close_limit', ['v1', 'perpetualPrivate'], 'POST', {'cost': 20}) + v1_perpetualprivate_post_order_close_market = v1PerpetualPrivatePostOrderCloseMarket = Entry('order/close_market', ['v1', 'perpetualPrivate'], 'POST', {'cost': 20}) + v1_perpetualprivate_post_position_adjust_margin = v1PerpetualPrivatePostPositionAdjustMargin = Entry('position/adjust_margin', ['v1', 'perpetualPrivate'], 'POST', {'cost': 20}) + v1_perpetualprivate_post_position_stop_loss = v1PerpetualPrivatePostPositionStopLoss = Entry('position/stop_loss', ['v1', 'perpetualPrivate'], 'POST', {'cost': 20}) + v1_perpetualprivate_post_position_take_profit = v1PerpetualPrivatePostPositionTakeProfit = Entry('position/take_profit', ['v1', 'perpetualPrivate'], 'POST', {'cost': 20}) + v1_perpetualprivate_post_position_market_close = v1PerpetualPrivatePostPositionMarketClose = Entry('position/market_close', ['v1', 'perpetualPrivate'], 'POST', {'cost': 20}) + v1_perpetualprivate_post_order_cancel_by_client_id = v1PerpetualPrivatePostOrderCancelByClientId = Entry('order/cancel/by_client_id', ['v1', 'perpetualPrivate'], 'POST', {'cost': 20}) + v1_perpetualprivate_post_order_cancel_stop_by_client_id = v1PerpetualPrivatePostOrderCancelStopByClientId = Entry('order/cancel_stop/by_client_id', ['v1', 'perpetualPrivate'], 'POST', {'cost': 20}) + v1_perpetualprivate_post_market_preference = v1PerpetualPrivatePostMarketPreference = Entry('market/preference', ['v1', 'perpetualPrivate'], 'POST', {'cost': 20}) + v2_public_get_maintain_info = v2PublicGetMaintainInfo = Entry('maintain/info', ['v2', 'public'], 'GET', {'cost': 1}) + v2_public_get_ping = v2PublicGetPing = Entry('ping', ['v2', 'public'], 'GET', {'cost': 1}) + v2_public_get_time = v2PublicGetTime = Entry('time', ['v2', 'public'], 'GET', {'cost': 1}) + v2_public_get_spot_market = v2PublicGetSpotMarket = Entry('spot/market', ['v2', 'public'], 'GET', {'cost': 1}) + v2_public_get_spot_ticker = v2PublicGetSpotTicker = Entry('spot/ticker', ['v2', 'public'], 'GET', {'cost': 1}) + v2_public_get_spot_depth = v2PublicGetSpotDepth = Entry('spot/depth', ['v2', 'public'], 'GET', {'cost': 1}) + v2_public_get_spot_deals = v2PublicGetSpotDeals = Entry('spot/deals', ['v2', 'public'], 'GET', {'cost': 1}) + v2_public_get_spot_kline = v2PublicGetSpotKline = Entry('spot/kline', ['v2', 'public'], 'GET', {'cost': 1}) + v2_public_get_spot_index = v2PublicGetSpotIndex = Entry('spot/index', ['v2', 'public'], 'GET', {'cost': 1}) + v2_public_get_futures_market = v2PublicGetFuturesMarket = Entry('futures/market', ['v2', 'public'], 'GET', {'cost': 1}) + v2_public_get_futures_ticker = v2PublicGetFuturesTicker = Entry('futures/ticker', ['v2', 'public'], 'GET', {'cost': 1}) + v2_public_get_futures_depth = v2PublicGetFuturesDepth = Entry('futures/depth', ['v2', 'public'], 'GET', {'cost': 1}) + v2_public_get_futures_deals = v2PublicGetFuturesDeals = Entry('futures/deals', ['v2', 'public'], 'GET', {'cost': 1}) + v2_public_get_futures_kline = v2PublicGetFuturesKline = Entry('futures/kline', ['v2', 'public'], 'GET', {'cost': 1}) + v2_public_get_futures_index = v2PublicGetFuturesIndex = Entry('futures/index', ['v2', 'public'], 'GET', {'cost': 1}) + v2_public_get_futures_funding_rate = v2PublicGetFuturesFundingRate = Entry('futures/funding-rate', ['v2', 'public'], 'GET', {'cost': 1}) + v2_public_get_futures_funding_rate_history = v2PublicGetFuturesFundingRateHistory = Entry('futures/funding-rate-history', ['v2', 'public'], 'GET', {'cost': 1}) + v2_public_get_futures_position_level = v2PublicGetFuturesPositionLevel = Entry('futures/position-level', ['v2', 'public'], 'GET', {'cost': 1}) + v2_public_get_futures_liquidation_history = v2PublicGetFuturesLiquidationHistory = Entry('futures/liquidation-history', ['v2', 'public'], 'GET', {'cost': 1}) + v2_public_get_futures_basis_history = v2PublicGetFuturesBasisHistory = Entry('futures/basis-history', ['v2', 'public'], 'GET', {'cost': 1}) + v2_public_get_assets_deposit_withdraw_config = v2PublicGetAssetsDepositWithdrawConfig = Entry('assets/deposit-withdraw-config', ['v2', 'public'], 'GET', {'cost': 1}) + v2_public_get_assets_all_deposit_withdraw_config = v2PublicGetAssetsAllDepositWithdrawConfig = Entry('assets/all-deposit-withdraw-config', ['v2', 'public'], 'GET', {'cost': 1}) + v2_private_get_account_subs = v2PrivateGetAccountSubs = Entry('account/subs', ['v2', 'private'], 'GET', {'cost': 1}) + v2_private_get_account_subs_api_detail = v2PrivateGetAccountSubsApiDetail = Entry('account/subs/api-detail', ['v2', 'private'], 'GET', {'cost': 40}) + v2_private_get_account_subs_info = v2PrivateGetAccountSubsInfo = Entry('account/subs/info', ['v2', 'private'], 'GET', {'cost': 1}) + v2_private_get_account_subs_api = v2PrivateGetAccountSubsApi = Entry('account/subs/api', ['v2', 'private'], 'GET', {'cost': 40}) + v2_private_get_account_subs_transfer_history = v2PrivateGetAccountSubsTransferHistory = Entry('account/subs/transfer-history', ['v2', 'private'], 'GET', {'cost': 40}) + v2_private_get_account_subs_spot_balance = v2PrivateGetAccountSubsSpotBalance = Entry('account/subs/spot-balance', ['v2', 'private'], 'GET', {'cost': 1}) + v2_private_get_account_trade_fee_rate = v2PrivateGetAccountTradeFeeRate = Entry('account/trade-fee-rate', ['v2', 'private'], 'GET', {'cost': 40}) + v2_private_get_assets_spot_balance = v2PrivateGetAssetsSpotBalance = Entry('assets/spot/balance', ['v2', 'private'], 'GET', {'cost': 40}) + v2_private_get_assets_futures_balance = v2PrivateGetAssetsFuturesBalance = Entry('assets/futures/balance', ['v2', 'private'], 'GET', {'cost': 40}) + v2_private_get_assets_margin_balance = v2PrivateGetAssetsMarginBalance = Entry('assets/margin/balance', ['v2', 'private'], 'GET', {'cost': 1}) + v2_private_get_assets_financial_balance = v2PrivateGetAssetsFinancialBalance = Entry('assets/financial/balance', ['v2', 'private'], 'GET', {'cost': 40}) + v2_private_get_assets_amm_liquidity = v2PrivateGetAssetsAmmLiquidity = Entry('assets/amm/liquidity', ['v2', 'private'], 'GET', {'cost': 40}) + v2_private_get_assets_credit_info = v2PrivateGetAssetsCreditInfo = Entry('assets/credit/info', ['v2', 'private'], 'GET', {'cost': 40}) + v2_private_get_assets_margin_borrow_history = v2PrivateGetAssetsMarginBorrowHistory = Entry('assets/margin/borrow-history', ['v2', 'private'], 'GET', {'cost': 40}) + v2_private_get_assets_margin_interest_limit = v2PrivateGetAssetsMarginInterestLimit = Entry('assets/margin/interest-limit', ['v2', 'private'], 'GET', {'cost': 1}) + v2_private_get_assets_deposit_address = v2PrivateGetAssetsDepositAddress = Entry('assets/deposit-address', ['v2', 'private'], 'GET', {'cost': 40}) + v2_private_get_assets_deposit_history = v2PrivateGetAssetsDepositHistory = Entry('assets/deposit-history', ['v2', 'private'], 'GET', {'cost': 40}) + v2_private_get_assets_withdraw = v2PrivateGetAssetsWithdraw = Entry('assets/withdraw', ['v2', 'private'], 'GET', {'cost': 40}) + v2_private_get_assets_transfer_history = v2PrivateGetAssetsTransferHistory = Entry('assets/transfer-history', ['v2', 'private'], 'GET', {'cost': 40}) + v2_private_get_spot_order_status = v2PrivateGetSpotOrderStatus = Entry('spot/order-status', ['v2', 'private'], 'GET', {'cost': 8}) + v2_private_get_spot_batch_order_status = v2PrivateGetSpotBatchOrderStatus = Entry('spot/batch-order-status', ['v2', 'private'], 'GET', {'cost': 8}) + v2_private_get_spot_pending_order = v2PrivateGetSpotPendingOrder = Entry('spot/pending-order', ['v2', 'private'], 'GET', {'cost': 8}) + v2_private_get_spot_finished_order = v2PrivateGetSpotFinishedOrder = Entry('spot/finished-order', ['v2', 'private'], 'GET', {'cost': 40}) + v2_private_get_spot_pending_stop_order = v2PrivateGetSpotPendingStopOrder = Entry('spot/pending-stop-order', ['v2', 'private'], 'GET', {'cost': 8}) + v2_private_get_spot_finished_stop_order = v2PrivateGetSpotFinishedStopOrder = Entry('spot/finished-stop-order', ['v2', 'private'], 'GET', {'cost': 40}) + v2_private_get_spot_user_deals = v2PrivateGetSpotUserDeals = Entry('spot/user-deals', ['v2', 'private'], 'GET', {'cost': 40}) + v2_private_get_spot_order_deals = v2PrivateGetSpotOrderDeals = Entry('spot/order-deals', ['v2', 'private'], 'GET', {'cost': 40}) + v2_private_get_futures_order_status = v2PrivateGetFuturesOrderStatus = Entry('futures/order-status', ['v2', 'private'], 'GET', {'cost': 8}) + v2_private_get_futures_batch_order_status = v2PrivateGetFuturesBatchOrderStatus = Entry('futures/batch-order-status', ['v2', 'private'], 'GET', {'cost': 1}) + v2_private_get_futures_pending_order = v2PrivateGetFuturesPendingOrder = Entry('futures/pending-order', ['v2', 'private'], 'GET', {'cost': 8}) + v2_private_get_futures_finished_order = v2PrivateGetFuturesFinishedOrder = Entry('futures/finished-order', ['v2', 'private'], 'GET', {'cost': 40}) + v2_private_get_futures_pending_stop_order = v2PrivateGetFuturesPendingStopOrder = Entry('futures/pending-stop-order', ['v2', 'private'], 'GET', {'cost': 8}) + v2_private_get_futures_finished_stop_order = v2PrivateGetFuturesFinishedStopOrder = Entry('futures/finished-stop-order', ['v2', 'private'], 'GET', {'cost': 40}) + v2_private_get_futures_user_deals = v2PrivateGetFuturesUserDeals = Entry('futures/user-deals', ['v2', 'private'], 'GET', {'cost': 1}) + v2_private_get_futures_order_deals = v2PrivateGetFuturesOrderDeals = Entry('futures/order-deals', ['v2', 'private'], 'GET', {'cost': 1}) + v2_private_get_futures_pending_position = v2PrivateGetFuturesPendingPosition = Entry('futures/pending-position', ['v2', 'private'], 'GET', {'cost': 40}) + v2_private_get_futures_finished_position = v2PrivateGetFuturesFinishedPosition = Entry('futures/finished-position', ['v2', 'private'], 'GET', {'cost': 1}) + v2_private_get_futures_position_margin_history = v2PrivateGetFuturesPositionMarginHistory = Entry('futures/position-margin-history', ['v2', 'private'], 'GET', {'cost': 1}) + v2_private_get_futures_position_funding_history = v2PrivateGetFuturesPositionFundingHistory = Entry('futures/position-funding-history', ['v2', 'private'], 'GET', {'cost': 40}) + v2_private_get_futures_position_adl_history = v2PrivateGetFuturesPositionAdlHistory = Entry('futures/position-adl-history', ['v2', 'private'], 'GET', {'cost': 1}) + v2_private_get_futures_position_settle_history = v2PrivateGetFuturesPositionSettleHistory = Entry('futures/position-settle-history', ['v2', 'private'], 'GET', {'cost': 1}) + v2_private_post_account_subs = v2PrivatePostAccountSubs = Entry('account/subs', ['v2', 'private'], 'POST', {'cost': 40}) + v2_private_post_account_subs_frozen = v2PrivatePostAccountSubsFrozen = Entry('account/subs/frozen', ['v2', 'private'], 'POST', {'cost': 40}) + v2_private_post_account_subs_unfrozen = v2PrivatePostAccountSubsUnfrozen = Entry('account/subs/unfrozen', ['v2', 'private'], 'POST', {'cost': 40}) + v2_private_post_account_subs_api = v2PrivatePostAccountSubsApi = Entry('account/subs/api', ['v2', 'private'], 'POST', {'cost': 40}) + v2_private_post_account_subs_edit_api = v2PrivatePostAccountSubsEditApi = Entry('account/subs/edit-api', ['v2', 'private'], 'POST', {'cost': 40}) + v2_private_post_account_subs_delete_api = v2PrivatePostAccountSubsDeleteApi = Entry('account/subs/delete-api', ['v2', 'private'], 'POST', {'cost': 40}) + v2_private_post_account_subs_transfer = v2PrivatePostAccountSubsTransfer = Entry('account/subs/transfer', ['v2', 'private'], 'POST', {'cost': 40}) + v2_private_post_account_settings = v2PrivatePostAccountSettings = Entry('account/settings', ['v2', 'private'], 'POST', {'cost': 40}) + v2_private_post_assets_margin_borrow = v2PrivatePostAssetsMarginBorrow = Entry('assets/margin/borrow', ['v2', 'private'], 'POST', {'cost': 40}) + v2_private_post_assets_margin_repay = v2PrivatePostAssetsMarginRepay = Entry('assets/margin/repay', ['v2', 'private'], 'POST', {'cost': 40}) + v2_private_post_assets_renewal_deposit_address = v2PrivatePostAssetsRenewalDepositAddress = Entry('assets/renewal-deposit-address', ['v2', 'private'], 'POST', {'cost': 40}) + v2_private_post_assets_withdraw = v2PrivatePostAssetsWithdraw = Entry('assets/withdraw', ['v2', 'private'], 'POST', {'cost': 40}) + v2_private_post_assets_cancel_withdraw = v2PrivatePostAssetsCancelWithdraw = Entry('assets/cancel-withdraw', ['v2', 'private'], 'POST', {'cost': 40}) + v2_private_post_assets_transfer = v2PrivatePostAssetsTransfer = Entry('assets/transfer', ['v2', 'private'], 'POST', {'cost': 40}) + v2_private_post_assets_amm_add_liquidity = v2PrivatePostAssetsAmmAddLiquidity = Entry('assets/amm/add-liquidity', ['v2', 'private'], 'POST', {'cost': 1}) + v2_private_post_assets_amm_remove_liquidity = v2PrivatePostAssetsAmmRemoveLiquidity = Entry('assets/amm/remove-liquidity', ['v2', 'private'], 'POST', {'cost': 1}) + v2_private_post_spot_order = v2PrivatePostSpotOrder = Entry('spot/order', ['v2', 'private'], 'POST', {'cost': 13.334}) + v2_private_post_spot_stop_order = v2PrivatePostSpotStopOrder = Entry('spot/stop-order', ['v2', 'private'], 'POST', {'cost': 13.334}) + v2_private_post_spot_batch_order = v2PrivatePostSpotBatchOrder = Entry('spot/batch-order', ['v2', 'private'], 'POST', {'cost': 40}) + v2_private_post_spot_batch_stop_order = v2PrivatePostSpotBatchStopOrder = Entry('spot/batch-stop-order', ['v2', 'private'], 'POST', {'cost': 1}) + v2_private_post_spot_modify_order = v2PrivatePostSpotModifyOrder = Entry('spot/modify-order', ['v2', 'private'], 'POST', {'cost': 13.334}) + v2_private_post_spot_modify_stop_order = v2PrivatePostSpotModifyStopOrder = Entry('spot/modify-stop-order', ['v2', 'private'], 'POST', {'cost': 13.334}) + v2_private_post_spot_cancel_all_order = v2PrivatePostSpotCancelAllOrder = Entry('spot/cancel-all-order', ['v2', 'private'], 'POST', {'cost': 1}) + v2_private_post_spot_cancel_order = v2PrivatePostSpotCancelOrder = Entry('spot/cancel-order', ['v2', 'private'], 'POST', {'cost': 6.667}) + v2_private_post_spot_cancel_stop_order = v2PrivatePostSpotCancelStopOrder = Entry('spot/cancel-stop-order', ['v2', 'private'], 'POST', {'cost': 6.667}) + v2_private_post_spot_cancel_batch_order = v2PrivatePostSpotCancelBatchOrder = Entry('spot/cancel-batch-order', ['v2', 'private'], 'POST', {'cost': 10}) + v2_private_post_spot_cancel_batch_stop_order = v2PrivatePostSpotCancelBatchStopOrder = Entry('spot/cancel-batch-stop-order', ['v2', 'private'], 'POST', {'cost': 10}) + v2_private_post_spot_cancel_order_by_client_id = v2PrivatePostSpotCancelOrderByClientId = Entry('spot/cancel-order-by-client-id', ['v2', 'private'], 'POST', {'cost': 1}) + v2_private_post_spot_cancel_stop_order_by_client_id = v2PrivatePostSpotCancelStopOrderByClientId = Entry('spot/cancel-stop-order-by-client-id', ['v2', 'private'], 'POST', {'cost': 1}) + v2_private_post_futures_order = v2PrivatePostFuturesOrder = Entry('futures/order', ['v2', 'private'], 'POST', {'cost': 20}) + v2_private_post_futures_stop_order = v2PrivatePostFuturesStopOrder = Entry('futures/stop-order', ['v2', 'private'], 'POST', {'cost': 20}) + v2_private_post_futures_batch_order = v2PrivatePostFuturesBatchOrder = Entry('futures/batch-order', ['v2', 'private'], 'POST', {'cost': 1}) + v2_private_post_futures_batch_stop_order = v2PrivatePostFuturesBatchStopOrder = Entry('futures/batch-stop-order', ['v2', 'private'], 'POST', {'cost': 1}) + v2_private_post_futures_modify_order = v2PrivatePostFuturesModifyOrder = Entry('futures/modify-order', ['v2', 'private'], 'POST', {'cost': 20}) + v2_private_post_futures_modify_stop_order = v2PrivatePostFuturesModifyStopOrder = Entry('futures/modify-stop-order', ['v2', 'private'], 'POST', {'cost': 20}) + v2_private_post_futures_cancel_all_order = v2PrivatePostFuturesCancelAllOrder = Entry('futures/cancel-all-order', ['v2', 'private'], 'POST', {'cost': 1}) + v2_private_post_futures_cancel_order = v2PrivatePostFuturesCancelOrder = Entry('futures/cancel-order', ['v2', 'private'], 'POST', {'cost': 10}) + v2_private_post_futures_cancel_stop_order = v2PrivatePostFuturesCancelStopOrder = Entry('futures/cancel-stop-order', ['v2', 'private'], 'POST', {'cost': 10}) + v2_private_post_futures_cancel_batch_order = v2PrivatePostFuturesCancelBatchOrder = Entry('futures/cancel-batch-order', ['v2', 'private'], 'POST', {'cost': 20}) + v2_private_post_futures_cancel_batch_stop_order = v2PrivatePostFuturesCancelBatchStopOrder = Entry('futures/cancel-batch-stop-order', ['v2', 'private'], 'POST', {'cost': 20}) + v2_private_post_futures_cancel_order_by_client_id = v2PrivatePostFuturesCancelOrderByClientId = Entry('futures/cancel-order-by-client-id', ['v2', 'private'], 'POST', {'cost': 1}) + v2_private_post_futures_cancel_stop_order_by_client_id = v2PrivatePostFuturesCancelStopOrderByClientId = Entry('futures/cancel-stop-order-by-client-id', ['v2', 'private'], 'POST', {'cost': 1}) + v2_private_post_futures_close_position = v2PrivatePostFuturesClosePosition = Entry('futures/close-position', ['v2', 'private'], 'POST', {'cost': 20}) + v2_private_post_futures_adjust_position_margin = v2PrivatePostFuturesAdjustPositionMargin = Entry('futures/adjust-position-margin', ['v2', 'private'], 'POST', {'cost': 20}) + v2_private_post_futures_adjust_position_leverage = v2PrivatePostFuturesAdjustPositionLeverage = Entry('futures/adjust-position-leverage', ['v2', 'private'], 'POST', {'cost': 20}) + v2_private_post_futures_set_position_stop_loss = v2PrivatePostFuturesSetPositionStopLoss = Entry('futures/set-position-stop-loss', ['v2', 'private'], 'POST', {'cost': 20}) + v2_private_post_futures_set_position_take_profit = v2PrivatePostFuturesSetPositionTakeProfit = Entry('futures/set-position-take-profit', ['v2', 'private'], 'POST', {'cost': 20}) diff --git a/ccxt/abstract/coinmate.py b/ccxt/abstract/coinmate.py new file mode 100644 index 0000000..913f89a --- /dev/null +++ b/ccxt/abstract/coinmate.py @@ -0,0 +1,62 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_orderbook = publicGetOrderBook = Entry('orderBook', 'public', 'GET', {}) + public_get_ticker = publicGetTicker = Entry('ticker', 'public', 'GET', {}) + public_get_tickerall = publicGetTickerAll = Entry('tickerAll', 'public', 'GET', {}) + public_get_products = publicGetProducts = Entry('products', 'public', 'GET', {}) + public_get_transactions = publicGetTransactions = Entry('transactions', 'public', 'GET', {}) + public_get_tradingpairs = publicGetTradingPairs = Entry('tradingPairs', 'public', 'GET', {}) + private_post_balances = privatePostBalances = Entry('balances', 'private', 'POST', {}) + private_post_bitcoincashwithdrawal = privatePostBitcoinCashWithdrawal = Entry('bitcoinCashWithdrawal', 'private', 'POST', {}) + private_post_bitcoincashdepositaddresses = privatePostBitcoinCashDepositAddresses = Entry('bitcoinCashDepositAddresses', 'private', 'POST', {}) + private_post_bitcoindepositaddresses = privatePostBitcoinDepositAddresses = Entry('bitcoinDepositAddresses', 'private', 'POST', {}) + private_post_bitcoinwithdrawal = privatePostBitcoinWithdrawal = Entry('bitcoinWithdrawal', 'private', 'POST', {}) + private_post_bitcoinwithdrawalfees = privatePostBitcoinWithdrawalFees = Entry('bitcoinWithdrawalFees', 'private', 'POST', {}) + private_post_buyinstant = privatePostBuyInstant = Entry('buyInstant', 'private', 'POST', {}) + private_post_buylimit = privatePostBuyLimit = Entry('buyLimit', 'private', 'POST', {}) + private_post_cancelorder = privatePostCancelOrder = Entry('cancelOrder', 'private', 'POST', {}) + private_post_cancelorderwithinfo = privatePostCancelOrderWithInfo = Entry('cancelOrderWithInfo', 'private', 'POST', {}) + private_post_createvoucher = privatePostCreateVoucher = Entry('createVoucher', 'private', 'POST', {}) + private_post_dashdepositaddresses = privatePostDashDepositAddresses = Entry('dashDepositAddresses', 'private', 'POST', {}) + private_post_dashwithdrawal = privatePostDashWithdrawal = Entry('dashWithdrawal', 'private', 'POST', {}) + private_post_ethereumwithdrawal = privatePostEthereumWithdrawal = Entry('ethereumWithdrawal', 'private', 'POST', {}) + private_post_ethereumdepositaddresses = privatePostEthereumDepositAddresses = Entry('ethereumDepositAddresses', 'private', 'POST', {}) + private_post_litecoinwithdrawal = privatePostLitecoinWithdrawal = Entry('litecoinWithdrawal', 'private', 'POST', {}) + private_post_litecoindepositaddresses = privatePostLitecoinDepositAddresses = Entry('litecoinDepositAddresses', 'private', 'POST', {}) + private_post_openorders = privatePostOpenOrders = Entry('openOrders', 'private', 'POST', {}) + private_post_order = privatePostOrder = Entry('order', 'private', 'POST', {}) + private_post_orderhistory = privatePostOrderHistory = Entry('orderHistory', 'private', 'POST', {}) + private_post_orderbyid = privatePostOrderById = Entry('orderById', 'private', 'POST', {}) + private_post_pusherauth = privatePostPusherAuth = Entry('pusherAuth', 'private', 'POST', {}) + private_post_redeemvoucher = privatePostRedeemVoucher = Entry('redeemVoucher', 'private', 'POST', {}) + private_post_replacebybuylimit = privatePostReplaceByBuyLimit = Entry('replaceByBuyLimit', 'private', 'POST', {}) + private_post_replacebybuyinstant = privatePostReplaceByBuyInstant = Entry('replaceByBuyInstant', 'private', 'POST', {}) + private_post_replacebyselllimit = privatePostReplaceBySellLimit = Entry('replaceBySellLimit', 'private', 'POST', {}) + private_post_replacebysellinstant = privatePostReplaceBySellInstant = Entry('replaceBySellInstant', 'private', 'POST', {}) + private_post_rippledepositaddresses = privatePostRippleDepositAddresses = Entry('rippleDepositAddresses', 'private', 'POST', {}) + private_post_ripplewithdrawal = privatePostRippleWithdrawal = Entry('rippleWithdrawal', 'private', 'POST', {}) + private_post_sellinstant = privatePostSellInstant = Entry('sellInstant', 'private', 'POST', {}) + private_post_selllimit = privatePostSellLimit = Entry('sellLimit', 'private', 'POST', {}) + private_post_transactionhistory = privatePostTransactionHistory = Entry('transactionHistory', 'private', 'POST', {}) + private_post_traderfees = privatePostTraderFees = Entry('traderFees', 'private', 'POST', {}) + private_post_tradehistory = privatePostTradeHistory = Entry('tradeHistory', 'private', 'POST', {}) + private_post_transfer = privatePostTransfer = Entry('transfer', 'private', 'POST', {}) + private_post_transferhistory = privatePostTransferHistory = Entry('transferHistory', 'private', 'POST', {}) + private_post_unconfirmedbitcoindeposits = privatePostUnconfirmedBitcoinDeposits = Entry('unconfirmedBitcoinDeposits', 'private', 'POST', {}) + private_post_unconfirmedbitcoincashdeposits = privatePostUnconfirmedBitcoinCashDeposits = Entry('unconfirmedBitcoinCashDeposits', 'private', 'POST', {}) + private_post_unconfirmeddashdeposits = privatePostUnconfirmedDashDeposits = Entry('unconfirmedDashDeposits', 'private', 'POST', {}) + private_post_unconfirmedethereumdeposits = privatePostUnconfirmedEthereumDeposits = Entry('unconfirmedEthereumDeposits', 'private', 'POST', {}) + private_post_unconfirmedlitecoindeposits = privatePostUnconfirmedLitecoinDeposits = Entry('unconfirmedLitecoinDeposits', 'private', 'POST', {}) + private_post_unconfirmedrippledeposits = privatePostUnconfirmedRippleDeposits = Entry('unconfirmedRippleDeposits', 'private', 'POST', {}) + private_post_cancelallopenorders = privatePostCancelAllOpenOrders = Entry('cancelAllOpenOrders', 'private', 'POST', {}) + private_post_withdrawvirtualcurrency = privatePostWithdrawVirtualCurrency = Entry('withdrawVirtualCurrency', 'private', 'POST', {}) + private_post_virtualcurrencydepositaddresses = privatePostVirtualCurrencyDepositAddresses = Entry('virtualCurrencyDepositAddresses', 'private', 'POST', {}) + private_post_unconfirmedvirtualcurrencydeposits = privatePostUnconfirmedVirtualCurrencyDeposits = Entry('unconfirmedVirtualCurrencyDeposits', 'private', 'POST', {}) + private_post_adawithdrawal = privatePostAdaWithdrawal = Entry('adaWithdrawal', 'private', 'POST', {}) + private_post_adadepositaddresses = privatePostAdaDepositAddresses = Entry('adaDepositAddresses', 'private', 'POST', {}) + private_post_unconfirmedadadeposits = privatePostUnconfirmedAdaDeposits = Entry('unconfirmedAdaDeposits', 'private', 'POST', {}) + private_post_solwithdrawal = privatePostSolWithdrawal = Entry('solWithdrawal', 'private', 'POST', {}) + private_post_soldepositaddresses = privatePostSolDepositAddresses = Entry('solDepositAddresses', 'private', 'POST', {}) + private_post_unconfirmedsoldeposits = privatePostUnconfirmedSolDeposits = Entry('unconfirmedSolDeposits', 'private', 'POST', {}) diff --git a/ccxt/abstract/coinmetro.py b/ccxt/abstract/coinmetro.py new file mode 100644 index 0000000..8c705bd --- /dev/null +++ b/ccxt/abstract/coinmetro.py @@ -0,0 +1,34 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_demo_temp = publicGetDemoTemp = Entry('demo/temp', 'public', 'GET', {'cost': 1}) + public_get_exchange_candles_pair_timeframe_from_to = publicGetExchangeCandlesPairTimeframeFromTo = Entry('exchange/candles/{pair}/{timeframe}/{from}/{to}', 'public', 'GET', {'cost': 3}) + public_get_exchange_prices = publicGetExchangePrices = Entry('exchange/prices', 'public', 'GET', {'cost': 1}) + public_get_exchange_ticks_pair_from = publicGetExchangeTicksPairFrom = Entry('exchange/ticks/{pair}/{from}', 'public', 'GET', {'cost': 3}) + public_get_assets = publicGetAssets = Entry('assets', 'public', 'GET', {'cost': 1}) + public_get_markets = publicGetMarkets = Entry('markets', 'public', 'GET', {'cost': 1}) + public_get_exchange_book_pair = publicGetExchangeBookPair = Entry('exchange/book/{pair}', 'public', 'GET', {'cost': 3}) + public_get_exchange_bookupdates_pair_from = publicGetExchangeBookUpdatesPairFrom = Entry('exchange/bookUpdates/{pair}/{from}', 'public', 'GET', {'cost': 1}) + private_get_users_balances = privateGetUsersBalances = Entry('users/balances', 'private', 'GET', {'cost': 1}) + private_get_users_wallets = privateGetUsersWallets = Entry('users/wallets', 'private', 'GET', {'cost': 1}) + private_get_users_wallets_history_since = privateGetUsersWalletsHistorySince = Entry('users/wallets/history/{since}', 'private', 'GET', {'cost': 1.67}) + private_get_exchange_orders_status_orderid = privateGetExchangeOrdersStatusOrderID = Entry('exchange/orders/status/{orderID}', 'private', 'GET', {'cost': 1}) + private_get_exchange_orders_active = privateGetExchangeOrdersActive = Entry('exchange/orders/active', 'private', 'GET', {'cost': 1}) + private_get_exchange_orders_history_since = privateGetExchangeOrdersHistorySince = Entry('exchange/orders/history/{since}', 'private', 'GET', {'cost': 1.67}) + private_get_exchange_fills_since = privateGetExchangeFillsSince = Entry('exchange/fills/{since}', 'private', 'GET', {'cost': 1.67}) + private_get_exchange_margin = privateGetExchangeMargin = Entry('exchange/margin', 'private', 'GET', {'cost': 1}) + private_post_jwt = privatePostJwt = Entry('jwt', 'private', 'POST', {'cost': 1}) + private_post_jwtdevice = privatePostJwtDevice = Entry('jwtDevice', 'private', 'POST', {'cost': 1}) + private_post_devices = privatePostDevices = Entry('devices', 'private', 'POST', {'cost': 1}) + private_post_jwt_read_only = privatePostJwtReadOnly = Entry('jwt-read-only', 'private', 'POST', {'cost': 1}) + private_post_exchange_orders_create = privatePostExchangeOrdersCreate = Entry('exchange/orders/create', 'private', 'POST', {'cost': 1}) + private_post_exchange_orders_modify_orderid = privatePostExchangeOrdersModifyOrderID = Entry('exchange/orders/modify/{orderID}', 'private', 'POST', {'cost': 1}) + private_post_exchange_swap = privatePostExchangeSwap = Entry('exchange/swap', 'private', 'POST', {'cost': 1}) + private_post_exchange_swap_confirm_swapid = privatePostExchangeSwapConfirmSwapId = Entry('exchange/swap/confirm/{swapId}', 'private', 'POST', {'cost': 1}) + private_post_exchange_orders_close_orderid = privatePostExchangeOrdersCloseOrderID = Entry('exchange/orders/close/{orderID}', 'private', 'POST', {'cost': 1}) + private_post_exchange_orders_hedge = privatePostExchangeOrdersHedge = Entry('exchange/orders/hedge', 'private', 'POST', {'cost': 1}) + private_put_jwt = privatePutJwt = Entry('jwt', 'private', 'PUT', {'cost': 1}) + private_put_exchange_orders_cancel_orderid = privatePutExchangeOrdersCancelOrderID = Entry('exchange/orders/cancel/{orderID}', 'private', 'PUT', {'cost': 1}) + private_put_users_margin_collateral = privatePutUsersMarginCollateral = Entry('users/margin/collateral', 'private', 'PUT', {'cost': 1}) + private_put_users_margin_primary_currency = privatePutUsersMarginPrimaryCurrency = Entry('users/margin/primary/{currency}', 'private', 'PUT', {'cost': 1}) diff --git a/ccxt/abstract/coinone.py b/ccxt/abstract/coinone.py new file mode 100644 index 0000000..018115b --- /dev/null +++ b/ccxt/abstract/coinone.py @@ -0,0 +1,67 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_orderbook = publicGetOrderbook = Entry('orderbook', 'public', 'GET', {}) + public_get_ticker = publicGetTicker = Entry('ticker', 'public', 'GET', {}) + public_get_ticker_utc = publicGetTickerUtc = Entry('ticker_utc', 'public', 'GET', {}) + public_get_trades = publicGetTrades = Entry('trades', 'public', 'GET', {}) + v2public_get_range_units = v2PublicGetRangeUnits = Entry('range_units', 'v2Public', 'GET', {}) + v2public_get_markets_quote_currency = v2PublicGetMarketsQuoteCurrency = Entry('markets/{quote_currency}', 'v2Public', 'GET', {}) + v2public_get_markets_quote_currency_target_currency = v2PublicGetMarketsQuoteCurrencyTargetCurrency = Entry('markets/{quote_currency}/{target_currency}', 'v2Public', 'GET', {}) + v2public_get_orderbook_quote_currency_target_currency = v2PublicGetOrderbookQuoteCurrencyTargetCurrency = Entry('orderbook/{quote_currency}/{target_currency}', 'v2Public', 'GET', {}) + v2public_get_trades_quote_currency_target_currency = v2PublicGetTradesQuoteCurrencyTargetCurrency = Entry('trades/{quote_currency}/{target_currency}', 'v2Public', 'GET', {}) + v2public_get_ticker_new_quote_currency = v2PublicGetTickerNewQuoteCurrency = Entry('ticker_new/{quote_currency}', 'v2Public', 'GET', {}) + v2public_get_ticker_new_quote_currency_target_currency = v2PublicGetTickerNewQuoteCurrencyTargetCurrency = Entry('ticker_new/{quote_currency}/{target_currency}', 'v2Public', 'GET', {}) + v2public_get_ticker_utc_new_quote_currency = v2PublicGetTickerUtcNewQuoteCurrency = Entry('ticker_utc_new/{quote_currency}', 'v2Public', 'GET', {}) + v2public_get_ticker_utc_new_quote_currency_target_currency = v2PublicGetTickerUtcNewQuoteCurrencyTargetCurrency = Entry('ticker_utc_new/{quote_currency}/{target_currency}', 'v2Public', 'GET', {}) + v2public_get_currencies = v2PublicGetCurrencies = Entry('currencies', 'v2Public', 'GET', {}) + v2public_get_currencies_currency = v2PublicGetCurrenciesCurrency = Entry('currencies/{currency}', 'v2Public', 'GET', {}) + v2public_get_chart_quote_currency_target_currency = v2PublicGetChartQuoteCurrencyTargetCurrency = Entry('chart/{quote_currency}/{target_currency}', 'v2Public', 'GET', {}) + private_post_account_deposit_address = privatePostAccountDepositAddress = Entry('account/deposit_address', 'private', 'POST', {}) + private_post_account_btc_deposit_address = privatePostAccountBtcDepositAddress = Entry('account/btc_deposit_address', 'private', 'POST', {}) + private_post_account_balance = privatePostAccountBalance = Entry('account/balance', 'private', 'POST', {}) + private_post_account_daily_balance = privatePostAccountDailyBalance = Entry('account/daily_balance', 'private', 'POST', {}) + private_post_account_user_info = privatePostAccountUserInfo = Entry('account/user_info', 'private', 'POST', {}) + private_post_account_virtual_account = privatePostAccountVirtualAccount = Entry('account/virtual_account', 'private', 'POST', {}) + private_post_order_cancel_all = privatePostOrderCancelAll = Entry('order/cancel_all', 'private', 'POST', {}) + private_post_order_cancel = privatePostOrderCancel = Entry('order/cancel', 'private', 'POST', {}) + private_post_order_limit_buy = privatePostOrderLimitBuy = Entry('order/limit_buy', 'private', 'POST', {}) + private_post_order_limit_sell = privatePostOrderLimitSell = Entry('order/limit_sell', 'private', 'POST', {}) + private_post_order_complete_orders = privatePostOrderCompleteOrders = Entry('order/complete_orders', 'private', 'POST', {}) + private_post_order_limit_orders = privatePostOrderLimitOrders = Entry('order/limit_orders', 'private', 'POST', {}) + private_post_order_order_info = privatePostOrderOrderInfo = Entry('order/order_info', 'private', 'POST', {}) + private_post_transaction_auth_number = privatePostTransactionAuthNumber = Entry('transaction/auth_number', 'private', 'POST', {}) + private_post_transaction_history = privatePostTransactionHistory = Entry('transaction/history', 'private', 'POST', {}) + private_post_transaction_krw_history = privatePostTransactionKrwHistory = Entry('transaction/krw/history', 'private', 'POST', {}) + private_post_transaction_btc = privatePostTransactionBtc = Entry('transaction/btc', 'private', 'POST', {}) + private_post_transaction_coin = privatePostTransactionCoin = Entry('transaction/coin', 'private', 'POST', {}) + v2private_post_account_balance = v2PrivatePostAccountBalance = Entry('account/balance', 'v2Private', 'POST', {}) + v2private_post_account_deposit_address = v2PrivatePostAccountDepositAddress = Entry('account/deposit_address', 'v2Private', 'POST', {}) + v2private_post_account_user_info = v2PrivatePostAccountUserInfo = Entry('account/user_info', 'v2Private', 'POST', {}) + v2private_post_account_virtual_account = v2PrivatePostAccountVirtualAccount = Entry('account/virtual_account', 'v2Private', 'POST', {}) + v2private_post_order_cancel = v2PrivatePostOrderCancel = Entry('order/cancel', 'v2Private', 'POST', {}) + v2private_post_order_limit_buy = v2PrivatePostOrderLimitBuy = Entry('order/limit_buy', 'v2Private', 'POST', {}) + v2private_post_order_limit_sell = v2PrivatePostOrderLimitSell = Entry('order/limit_sell', 'v2Private', 'POST', {}) + v2private_post_order_limit_orders = v2PrivatePostOrderLimitOrders = Entry('order/limit_orders', 'v2Private', 'POST', {}) + v2private_post_order_complete_orders = v2PrivatePostOrderCompleteOrders = Entry('order/complete_orders', 'v2Private', 'POST', {}) + v2private_post_order_query_order = v2PrivatePostOrderQueryOrder = Entry('order/query_order', 'v2Private', 'POST', {}) + v2private_post_transaction_auth_number = v2PrivatePostTransactionAuthNumber = Entry('transaction/auth_number', 'v2Private', 'POST', {}) + v2private_post_transaction_btc = v2PrivatePostTransactionBtc = Entry('transaction/btc', 'v2Private', 'POST', {}) + v2private_post_transaction_history = v2PrivatePostTransactionHistory = Entry('transaction/history', 'v2Private', 'POST', {}) + v2private_post_transaction_krw_history = v2PrivatePostTransactionKrwHistory = Entry('transaction/krw/history', 'v2Private', 'POST', {}) + v2_1private_post_account_balance_all = v2_1PrivatePostAccountBalanceAll = Entry('account/balance/all', 'v2_1Private', 'POST', {}) + v2_1private_post_account_balance = v2_1PrivatePostAccountBalance = Entry('account/balance', 'v2_1Private', 'POST', {}) + v2_1private_post_account_trade_fee = v2_1PrivatePostAccountTradeFee = Entry('account/trade_fee', 'v2_1Private', 'POST', {}) + v2_1private_post_account_trade_fee_quote_currency_target_currency = v2_1PrivatePostAccountTradeFeeQuoteCurrencyTargetCurrency = Entry('account/trade_fee/{quote_currency}/{target_currency}', 'v2_1Private', 'POST', {}) + v2_1private_post_order_limit = v2_1PrivatePostOrderLimit = Entry('order/limit', 'v2_1Private', 'POST', {}) + v2_1private_post_order_cancel = v2_1PrivatePostOrderCancel = Entry('order/cancel', 'v2_1Private', 'POST', {}) + v2_1private_post_order_cancel_all = v2_1PrivatePostOrderCancelAll = Entry('order/cancel/all', 'v2_1Private', 'POST', {}) + v2_1private_post_order_open_orders = v2_1PrivatePostOrderOpenOrders = Entry('order/open_orders', 'v2_1Private', 'POST', {}) + v2_1private_post_order_open_orders_all = v2_1PrivatePostOrderOpenOrdersAll = Entry('order/open_orders/all', 'v2_1Private', 'POST', {}) + v2_1private_post_order_complete_orders = v2_1PrivatePostOrderCompleteOrders = Entry('order/complete_orders', 'v2_1Private', 'POST', {}) + v2_1private_post_order_complete_orders_all = v2_1PrivatePostOrderCompleteOrdersAll = Entry('order/complete_orders/all', 'v2_1Private', 'POST', {}) + v2_1private_post_order_info = v2_1PrivatePostOrderInfo = Entry('order/info', 'v2_1Private', 'POST', {}) + v2_1private_post_transaction_krw_history = v2_1PrivatePostTransactionKrwHistory = Entry('transaction/krw/history', 'v2_1Private', 'POST', {}) + v2_1private_post_transaction_coin_history = v2_1PrivatePostTransactionCoinHistory = Entry('transaction/coin/history', 'v2_1Private', 'POST', {}) + v2_1private_post_transaction_coin_withdrawal_limit = v2_1PrivatePostTransactionCoinWithdrawalLimit = Entry('transaction/coin/withdrawal/limit', 'v2_1Private', 'POST', {}) diff --git a/ccxt/abstract/coinsph.py b/ccxt/abstract/coinsph.py new file mode 100644 index 0000000..91e079a --- /dev/null +++ b/ccxt/abstract/coinsph.py @@ -0,0 +1,54 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_openapi_v1_ping = publicGetOpenapiV1Ping = Entry('openapi/v1/ping', 'public', 'GET', {'cost': 1}) + public_get_openapi_v1_time = publicGetOpenapiV1Time = Entry('openapi/v1/time', 'public', 'GET', {'cost': 1}) + public_get_openapi_quote_v1_ticker_24hr = publicGetOpenapiQuoteV1Ticker24hr = Entry('openapi/quote/v1/ticker/24hr', 'public', 'GET', {'cost': 1, 'noSymbolAndNoSymbols': 40, 'byNumberOfSymbols': [[101, 40], [21, 20], [0, 1]]}) + public_get_openapi_quote_v1_ticker_price = publicGetOpenapiQuoteV1TickerPrice = Entry('openapi/quote/v1/ticker/price', 'public', 'GET', {'cost': 1, 'noSymbol': 2}) + public_get_openapi_quote_v1_ticker_bookticker = publicGetOpenapiQuoteV1TickerBookTicker = Entry('openapi/quote/v1/ticker/bookTicker', 'public', 'GET', {'cost': 1, 'noSymbol': 2}) + public_get_openapi_v1_exchangeinfo = publicGetOpenapiV1ExchangeInfo = Entry('openapi/v1/exchangeInfo', 'public', 'GET', {'cost': 10}) + public_get_openapi_quote_v1_depth = publicGetOpenapiQuoteV1Depth = Entry('openapi/quote/v1/depth', 'public', 'GET', {'cost': 1, 'byLimit': [[101, 5], [0, 1]]}) + public_get_openapi_quote_v1_klines = publicGetOpenapiQuoteV1Klines = Entry('openapi/quote/v1/klines', 'public', 'GET', {'cost': 1}) + public_get_openapi_quote_v1_trades = publicGetOpenapiQuoteV1Trades = Entry('openapi/quote/v1/trades', 'public', 'GET', {'cost': 1}) + public_get_openapi_v1_pairs = publicGetOpenapiV1Pairs = Entry('openapi/v1/pairs', 'public', 'GET', {'cost': 1}) + public_get_openapi_quote_v1_avgprice = publicGetOpenapiQuoteV1AvgPrice = Entry('openapi/quote/v1/avgPrice', 'public', 'GET', {'cost': 1}) + private_get_openapi_wallet_v1_config_getall = privateGetOpenapiWalletV1ConfigGetall = Entry('openapi/wallet/v1/config/getall', 'private', 'GET', {'cost': 10}) + private_get_openapi_wallet_v1_deposit_address = privateGetOpenapiWalletV1DepositAddress = Entry('openapi/wallet/v1/deposit/address', 'private', 'GET', {'cost': 10}) + private_get_openapi_wallet_v1_deposit_history = privateGetOpenapiWalletV1DepositHistory = Entry('openapi/wallet/v1/deposit/history', 'private', 'GET', {'cost': 1}) + private_get_openapi_wallet_v1_withdraw_history = privateGetOpenapiWalletV1WithdrawHistory = Entry('openapi/wallet/v1/withdraw/history', 'private', 'GET', {'cost': 1}) + private_get_openapi_v1_account = privateGetOpenapiV1Account = Entry('openapi/v1/account', 'private', 'GET', {'cost': 10}) + private_get_openapi_v1_openorders = privateGetOpenapiV1OpenOrders = Entry('openapi/v1/openOrders', 'private', 'GET', {'cost': 3, 'noSymbol': 40}) + private_get_openapi_v1_asset_tradefee = privateGetOpenapiV1AssetTradeFee = Entry('openapi/v1/asset/tradeFee', 'private', 'GET', {'cost': 1}) + private_get_openapi_v1_order = privateGetOpenapiV1Order = Entry('openapi/v1/order', 'private', 'GET', {'cost': 2}) + private_get_openapi_v1_historyorders = privateGetOpenapiV1HistoryOrders = Entry('openapi/v1/historyOrders', 'private', 'GET', {'cost': 10, 'noSymbol': 40}) + private_get_openapi_v1_mytrades = privateGetOpenapiV1MyTrades = Entry('openapi/v1/myTrades', 'private', 'GET', {'cost': 10}) + private_get_openapi_v1_capital_deposit_history = privateGetOpenapiV1CapitalDepositHistory = Entry('openapi/v1/capital/deposit/history', 'private', 'GET', {'cost': 1}) + private_get_openapi_v1_capital_withdraw_history = privateGetOpenapiV1CapitalWithdrawHistory = Entry('openapi/v1/capital/withdraw/history', 'private', 'GET', {'cost': 1}) + private_get_openapi_v3_payment_request_get_payment_request = privateGetOpenapiV3PaymentRequestGetPaymentRequest = Entry('openapi/v3/payment-request/get-payment-request', 'private', 'GET', {'cost': 1}) + private_get_merchant_api_v1_get_invoices = privateGetMerchantApiV1GetInvoices = Entry('merchant-api/v1/get-invoices', 'private', 'GET', {'cost': 1}) + private_get_openapi_account_v3_crypto_accounts = privateGetOpenapiAccountV3CryptoAccounts = Entry('openapi/account/v3/crypto-accounts', 'private', 'GET', {'cost': 1}) + private_get_openapi_transfer_v3_transfers_id = privateGetOpenapiTransferV3TransfersId = Entry('openapi/transfer/v3/transfers/{id}', 'private', 'GET', {'cost': 1}) + private_post_openapi_wallet_v1_withdraw_apply = privatePostOpenapiWalletV1WithdrawApply = Entry('openapi/wallet/v1/withdraw/apply', 'private', 'POST', {'cost': 600}) + private_post_openapi_v1_order_test = privatePostOpenapiV1OrderTest = Entry('openapi/v1/order/test', 'private', 'POST', {'cost': 1}) + private_post_openapi_v1_order = privatePostOpenapiV1Order = Entry('openapi/v1/order', 'private', 'POST', {'cost': 1}) + private_post_openapi_v1_capital_withdraw_apply = privatePostOpenapiV1CapitalWithdrawApply = Entry('openapi/v1/capital/withdraw/apply', 'private', 'POST', {'cost': 1}) + private_post_openapi_v1_capital_deposit_apply = privatePostOpenapiV1CapitalDepositApply = Entry('openapi/v1/capital/deposit/apply', 'private', 'POST', {'cost': 1}) + private_post_openapi_v3_payment_request_payment_requests = privatePostOpenapiV3PaymentRequestPaymentRequests = Entry('openapi/v3/payment-request/payment-requests', 'private', 'POST', {'cost': 1}) + private_post_openapi_v3_payment_request_delete_payment_request = privatePostOpenapiV3PaymentRequestDeletePaymentRequest = Entry('openapi/v3/payment-request/delete-payment-request', 'private', 'POST', {'cost': 1}) + private_post_openapi_v3_payment_request_payment_request_reminder = privatePostOpenapiV3PaymentRequestPaymentRequestReminder = Entry('openapi/v3/payment-request/payment-request-reminder', 'private', 'POST', {'cost': 1}) + private_post_openapi_v1_userdatastream = privatePostOpenapiV1UserDataStream = Entry('openapi/v1/userDataStream', 'private', 'POST', {'cost': 1}) + private_post_merchant_api_v1_invoices = privatePostMerchantApiV1Invoices = Entry('merchant-api/v1/invoices', 'private', 'POST', {'cost': 1}) + private_post_merchant_api_v1_invoices_cancel = privatePostMerchantApiV1InvoicesCancel = Entry('merchant-api/v1/invoices-cancel', 'private', 'POST', {'cost': 1}) + private_post_openapi_convert_v1_get_supported_trading_pairs = privatePostOpenapiConvertV1GetSupportedTradingPairs = Entry('openapi/convert/v1/get-supported-trading-pairs', 'private', 'POST', {'cost': 1}) + private_post_openapi_convert_v1_get_quote = privatePostOpenapiConvertV1GetQuote = Entry('openapi/convert/v1/get-quote', 'private', 'POST', {'cost': 1}) + private_post_openapi_convert_v1_accpet_quote = privatePostOpenapiConvertV1AccpetQuote = Entry('openapi/convert/v1/accpet-quote', 'private', 'POST', {'cost': 1}) + private_post_openapi_fiat_v1_support_channel = privatePostOpenapiFiatV1SupportChannel = Entry('openapi/fiat/v1/support-channel', 'private', 'POST', {'cost': 1}) + private_post_openapi_fiat_v1_cash_out = privatePostOpenapiFiatV1CashOut = Entry('openapi/fiat/v1/cash-out', 'private', 'POST', {'cost': 1}) + private_post_openapi_fiat_v1_history = privatePostOpenapiFiatV1History = Entry('openapi/fiat/v1/history', 'private', 'POST', {'cost': 1}) + private_post_openapi_migration_v4_sellorder = privatePostOpenapiMigrationV4Sellorder = Entry('openapi/migration/v4/sellorder', 'private', 'POST', {'cost': 1}) + private_post_openapi_migration_v4_validate_field = privatePostOpenapiMigrationV4ValidateField = Entry('openapi/migration/v4/validate-field', 'private', 'POST', {'cost': 1}) + private_post_openapi_transfer_v3_transfers = privatePostOpenapiTransferV3Transfers = Entry('openapi/transfer/v3/transfers', 'private', 'POST', {'cost': 1}) + private_delete_openapi_v1_order = privateDeleteOpenapiV1Order = Entry('openapi/v1/order', 'private', 'DELETE', {'cost': 1}) + private_delete_openapi_v1_openorders = privateDeleteOpenapiV1OpenOrders = Entry('openapi/v1/openOrders', 'private', 'DELETE', {'cost': 1}) + private_delete_openapi_v1_userdatastream = privateDeleteOpenapiV1UserDataStream = Entry('openapi/v1/userDataStream', 'private', 'DELETE', {'cost': 1}) diff --git a/ccxt/abstract/coinspot.py b/ccxt/abstract/coinspot.py new file mode 100644 index 0000000..bffc35d --- /dev/null +++ b/ccxt/abstract/coinspot.py @@ -0,0 +1,28 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_latest = publicGetLatest = Entry('latest', 'public', 'GET', {}) + private_post_orders = privatePostOrders = Entry('orders', 'private', 'POST', {}) + private_post_orders_history = privatePostOrdersHistory = Entry('orders/history', 'private', 'POST', {}) + private_post_my_coin_deposit = privatePostMyCoinDeposit = Entry('my/coin/deposit', 'private', 'POST', {}) + private_post_my_coin_send = privatePostMyCoinSend = Entry('my/coin/send', 'private', 'POST', {}) + private_post_quote_buy = privatePostQuoteBuy = Entry('quote/buy', 'private', 'POST', {}) + private_post_quote_sell = privatePostQuoteSell = Entry('quote/sell', 'private', 'POST', {}) + private_post_my_balances = privatePostMyBalances = Entry('my/balances', 'private', 'POST', {}) + private_post_my_orders = privatePostMyOrders = Entry('my/orders', 'private', 'POST', {}) + private_post_my_buy = privatePostMyBuy = Entry('my/buy', 'private', 'POST', {}) + private_post_my_sell = privatePostMySell = Entry('my/sell', 'private', 'POST', {}) + private_post_my_buy_cancel = privatePostMyBuyCancel = Entry('my/buy/cancel', 'private', 'POST', {}) + private_post_my_sell_cancel = privatePostMySellCancel = Entry('my/sell/cancel', 'private', 'POST', {}) + private_post_ro_my_balances = privatePostRoMyBalances = Entry('ro/my/balances', 'private', 'POST', {}) + private_post_ro_my_balances_cointype = privatePostRoMyBalancesCointype = Entry('ro/my/balances/{cointype}', 'private', 'POST', {}) + private_post_ro_my_deposits = privatePostRoMyDeposits = Entry('ro/my/deposits', 'private', 'POST', {}) + private_post_ro_my_withdrawals = privatePostRoMyWithdrawals = Entry('ro/my/withdrawals', 'private', 'POST', {}) + private_post_ro_my_transactions = privatePostRoMyTransactions = Entry('ro/my/transactions', 'private', 'POST', {}) + private_post_ro_my_transactions_cointype = privatePostRoMyTransactionsCointype = Entry('ro/my/transactions/{cointype}', 'private', 'POST', {}) + private_post_ro_my_transactions_open = privatePostRoMyTransactionsOpen = Entry('ro/my/transactions/open', 'private', 'POST', {}) + private_post_ro_my_transactions_cointype_open = privatePostRoMyTransactionsCointypeOpen = Entry('ro/my/transactions/{cointype}/open', 'private', 'POST', {}) + private_post_ro_my_sendreceive = privatePostRoMySendreceive = Entry('ro/my/sendreceive', 'private', 'POST', {}) + private_post_ro_my_affiliatepayments = privatePostRoMyAffiliatepayments = Entry('ro/my/affiliatepayments', 'private', 'POST', {}) + private_post_ro_my_referralpayments = privatePostRoMyReferralpayments = Entry('ro/my/referralpayments', 'private', 'POST', {}) diff --git a/ccxt/abstract/cryptocom.py b/ccxt/abstract/cryptocom.py new file mode 100644 index 0000000..2383deb --- /dev/null +++ b/ccxt/abstract/cryptocom.py @@ -0,0 +1,123 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + base_public_get_v1_public_get_announcements = basePublicGetV1PublicGetAnnouncements = Entry('v1/public/get-announcements', ['base', 'public'], 'GET', {'cost': 1}) + v1_public_get_public_auth = v1PublicGetPublicAuth = Entry('public/auth', ['v1', 'public'], 'GET', {'cost': 3.3333333333333335}) + v1_public_get_public_get_instruments = v1PublicGetPublicGetInstruments = Entry('public/get-instruments', ['v1', 'public'], 'GET', {'cost': 3.3333333333333335}) + v1_public_get_public_get_book = v1PublicGetPublicGetBook = Entry('public/get-book', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_public_get_candlestick = v1PublicGetPublicGetCandlestick = Entry('public/get-candlestick', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_public_get_trades = v1PublicGetPublicGetTrades = Entry('public/get-trades', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_public_get_tickers = v1PublicGetPublicGetTickers = Entry('public/get-tickers', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_public_get_valuations = v1PublicGetPublicGetValuations = Entry('public/get-valuations', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_public_get_expired_settlement_price = v1PublicGetPublicGetExpiredSettlementPrice = Entry('public/get-expired-settlement-price', ['v1', 'public'], 'GET', {'cost': 3.3333333333333335}) + v1_public_get_public_get_insurance = v1PublicGetPublicGetInsurance = Entry('public/get-insurance', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_public_get_risk_parameters = v1PublicGetPublicGetRiskParameters = Entry('public/get-risk-parameters', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_post_public_staking_get_conversion_rate = v1PublicPostPublicStakingGetConversionRate = Entry('public/staking/get-conversion-rate', ['v1', 'public'], 'POST', {'cost': 2}) + v1_private_post_private_set_cancel_on_disconnect = v1PrivatePostPrivateSetCancelOnDisconnect = Entry('private/set-cancel-on-disconnect', ['v1', 'private'], 'POST', {'cost': 3.3333333333333335}) + v1_private_post_private_get_cancel_on_disconnect = v1PrivatePostPrivateGetCancelOnDisconnect = Entry('private/get-cancel-on-disconnect', ['v1', 'private'], 'POST', {'cost': 3.3333333333333335}) + v1_private_post_private_user_balance = v1PrivatePostPrivateUserBalance = Entry('private/user-balance', ['v1', 'private'], 'POST', {'cost': 3.3333333333333335}) + v1_private_post_private_user_balance_history = v1PrivatePostPrivateUserBalanceHistory = Entry('private/user-balance-history', ['v1', 'private'], 'POST', {'cost': 3.3333333333333335}) + v1_private_post_private_get_positions = v1PrivatePostPrivateGetPositions = Entry('private/get-positions', ['v1', 'private'], 'POST', {'cost': 3.3333333333333335}) + v1_private_post_private_create_order = v1PrivatePostPrivateCreateOrder = Entry('private/create-order', ['v1', 'private'], 'POST', {'cost': 0.6666666666666666}) + v1_private_post_private_amend_order = v1PrivatePostPrivateAmendOrder = Entry('private/amend-order', ['v1', 'private'], 'POST', {'cost': 1.3333333333333333}) + v1_private_post_private_create_order_list = v1PrivatePostPrivateCreateOrderList = Entry('private/create-order-list', ['v1', 'private'], 'POST', {'cost': 3.3333333333333335}) + v1_private_post_private_cancel_order = v1PrivatePostPrivateCancelOrder = Entry('private/cancel-order', ['v1', 'private'], 'POST', {'cost': 0.6666666666666666}) + v1_private_post_private_cancel_order_list = v1PrivatePostPrivateCancelOrderList = Entry('private/cancel-order-list', ['v1', 'private'], 'POST', {'cost': 3.3333333333333335}) + v1_private_post_private_cancel_all_orders = v1PrivatePostPrivateCancelAllOrders = Entry('private/cancel-all-orders', ['v1', 'private'], 'POST', {'cost': 0.6666666666666666}) + v1_private_post_private_close_position = v1PrivatePostPrivateClosePosition = Entry('private/close-position', ['v1', 'private'], 'POST', {'cost': 3.3333333333333335}) + v1_private_post_private_get_order_history = v1PrivatePostPrivateGetOrderHistory = Entry('private/get-order-history', ['v1', 'private'], 'POST', {'cost': 100}) + v1_private_post_private_get_open_orders = v1PrivatePostPrivateGetOpenOrders = Entry('private/get-open-orders', ['v1', 'private'], 'POST', {'cost': 3.3333333333333335}) + v1_private_post_private_get_order_detail = v1PrivatePostPrivateGetOrderDetail = Entry('private/get-order-detail', ['v1', 'private'], 'POST', {'cost': 0.3333333333333333}) + v1_private_post_private_get_trades = v1PrivatePostPrivateGetTrades = Entry('private/get-trades', ['v1', 'private'], 'POST', {'cost': 100}) + v1_private_post_private_change_account_leverage = v1PrivatePostPrivateChangeAccountLeverage = Entry('private/change-account-leverage', ['v1', 'private'], 'POST', {'cost': 3.3333333333333335}) + v1_private_post_private_get_transactions = v1PrivatePostPrivateGetTransactions = Entry('private/get-transactions', ['v1', 'private'], 'POST', {'cost': 3.3333333333333335}) + v1_private_post_private_create_subaccount_transfer = v1PrivatePostPrivateCreateSubaccountTransfer = Entry('private/create-subaccount-transfer', ['v1', 'private'], 'POST', {'cost': 3.3333333333333335}) + v1_private_post_private_get_subaccount_balances = v1PrivatePostPrivateGetSubaccountBalances = Entry('private/get-subaccount-balances', ['v1', 'private'], 'POST', {'cost': 3.3333333333333335}) + v1_private_post_private_get_order_list = v1PrivatePostPrivateGetOrderList = Entry('private/get-order-list', ['v1', 'private'], 'POST', {'cost': 3.3333333333333335}) + v1_private_post_private_create_withdrawal = v1PrivatePostPrivateCreateWithdrawal = Entry('private/create-withdrawal', ['v1', 'private'], 'POST', {'cost': 3.3333333333333335}) + v1_private_post_private_get_currency_networks = v1PrivatePostPrivateGetCurrencyNetworks = Entry('private/get-currency-networks', ['v1', 'private'], 'POST', {'cost': 3.3333333333333335}) + v1_private_post_private_get_deposit_address = v1PrivatePostPrivateGetDepositAddress = Entry('private/get-deposit-address', ['v1', 'private'], 'POST', {'cost': 3.3333333333333335}) + v1_private_post_private_get_accounts = v1PrivatePostPrivateGetAccounts = Entry('private/get-accounts', ['v1', 'private'], 'POST', {'cost': 3.3333333333333335}) + v1_private_post_private_get_withdrawal_history = v1PrivatePostPrivateGetWithdrawalHistory = Entry('private/get-withdrawal-history', ['v1', 'private'], 'POST', {'cost': 3.3333333333333335}) + v1_private_post_private_get_deposit_history = v1PrivatePostPrivateGetDepositHistory = Entry('private/get-deposit-history', ['v1', 'private'], 'POST', {'cost': 3.3333333333333335}) + v1_private_post_private_get_fee_rate = v1PrivatePostPrivateGetFeeRate = Entry('private/get-fee-rate', ['v1', 'private'], 'POST', {'cost': 2}) + v1_private_post_private_get_instrument_fee_rate = v1PrivatePostPrivateGetInstrumentFeeRate = Entry('private/get-instrument-fee-rate', ['v1', 'private'], 'POST', {'cost': 2}) + v1_private_post_private_staking_stake = v1PrivatePostPrivateStakingStake = Entry('private/staking/stake', ['v1', 'private'], 'POST', {'cost': 2}) + v1_private_post_private_staking_unstake = v1PrivatePostPrivateStakingUnstake = Entry('private/staking/unstake', ['v1', 'private'], 'POST', {'cost': 2}) + v1_private_post_private_staking_get_staking_position = v1PrivatePostPrivateStakingGetStakingPosition = Entry('private/staking/get-staking-position', ['v1', 'private'], 'POST', {'cost': 2}) + v1_private_post_private_staking_get_staking_instruments = v1PrivatePostPrivateStakingGetStakingInstruments = Entry('private/staking/get-staking-instruments', ['v1', 'private'], 'POST', {'cost': 2}) + v1_private_post_private_staking_get_open_stake = v1PrivatePostPrivateStakingGetOpenStake = Entry('private/staking/get-open-stake', ['v1', 'private'], 'POST', {'cost': 2}) + v1_private_post_private_staking_get_stake_history = v1PrivatePostPrivateStakingGetStakeHistory = Entry('private/staking/get-stake-history', ['v1', 'private'], 'POST', {'cost': 2}) + v1_private_post_private_staking_get_reward_history = v1PrivatePostPrivateStakingGetRewardHistory = Entry('private/staking/get-reward-history', ['v1', 'private'], 'POST', {'cost': 2}) + v1_private_post_private_staking_convert = v1PrivatePostPrivateStakingConvert = Entry('private/staking/convert', ['v1', 'private'], 'POST', {'cost': 2}) + v1_private_post_private_staking_get_open_convert = v1PrivatePostPrivateStakingGetOpenConvert = Entry('private/staking/get-open-convert', ['v1', 'private'], 'POST', {'cost': 2}) + v1_private_post_private_staking_get_convert_history = v1PrivatePostPrivateStakingGetConvertHistory = Entry('private/staking/get-convert-history', ['v1', 'private'], 'POST', {'cost': 2}) + v2_public_get_public_auth = v2PublicGetPublicAuth = Entry('public/auth', ['v2', 'public'], 'GET', {'cost': 1}) + v2_public_get_public_get_instruments = v2PublicGetPublicGetInstruments = Entry('public/get-instruments', ['v2', 'public'], 'GET', {'cost': 1}) + v2_public_get_public_get_book = v2PublicGetPublicGetBook = Entry('public/get-book', ['v2', 'public'], 'GET', {'cost': 1}) + v2_public_get_public_get_candlestick = v2PublicGetPublicGetCandlestick = Entry('public/get-candlestick', ['v2', 'public'], 'GET', {'cost': 1}) + v2_public_get_public_get_ticker = v2PublicGetPublicGetTicker = Entry('public/get-ticker', ['v2', 'public'], 'GET', {'cost': 1}) + v2_public_get_public_get_trades = v2PublicGetPublicGetTrades = Entry('public/get-trades', ['v2', 'public'], 'GET', {'cost': 1}) + v2_public_get_public_margin_get_transfer_currencies = v2PublicGetPublicMarginGetTransferCurrencies = Entry('public/margin/get-transfer-currencies', ['v2', 'public'], 'GET', {'cost': 1}) + v2_public_get_public_margin_get_load_currenices = v2PublicGetPublicMarginGetLoadCurrenices = Entry('public/margin/get-load-currenices', ['v2', 'public'], 'GET', {'cost': 1}) + v2_public_get_public_respond_heartbeat = v2PublicGetPublicRespondHeartbeat = Entry('public/respond-heartbeat', ['v2', 'public'], 'GET', {'cost': 1}) + v2_private_post_private_set_cancel_on_disconnect = v2PrivatePostPrivateSetCancelOnDisconnect = Entry('private/set-cancel-on-disconnect', ['v2', 'private'], 'POST', {'cost': 3.3333333333333335}) + v2_private_post_private_get_cancel_on_disconnect = v2PrivatePostPrivateGetCancelOnDisconnect = Entry('private/get-cancel-on-disconnect', ['v2', 'private'], 'POST', {'cost': 3.3333333333333335}) + v2_private_post_private_create_withdrawal = v2PrivatePostPrivateCreateWithdrawal = Entry('private/create-withdrawal', ['v2', 'private'], 'POST', {'cost': 3.3333333333333335}) + v2_private_post_private_get_withdrawal_history = v2PrivatePostPrivateGetWithdrawalHistory = Entry('private/get-withdrawal-history', ['v2', 'private'], 'POST', {'cost': 3.3333333333333335}) + v2_private_post_private_get_currency_networks = v2PrivatePostPrivateGetCurrencyNetworks = Entry('private/get-currency-networks', ['v2', 'private'], 'POST', {'cost': 3.3333333333333335}) + v2_private_post_private_get_deposit_history = v2PrivatePostPrivateGetDepositHistory = Entry('private/get-deposit-history', ['v2', 'private'], 'POST', {'cost': 3.3333333333333335}) + v2_private_post_private_get_deposit_address = v2PrivatePostPrivateGetDepositAddress = Entry('private/get-deposit-address', ['v2', 'private'], 'POST', {'cost': 3.3333333333333335}) + v2_private_post_private_export_create_export_request = v2PrivatePostPrivateExportCreateExportRequest = Entry('private/export/create-export-request', ['v2', 'private'], 'POST', {'cost': 3.3333333333333335}) + v2_private_post_private_export_get_export_requests = v2PrivatePostPrivateExportGetExportRequests = Entry('private/export/get-export-requests', ['v2', 'private'], 'POST', {'cost': 3.3333333333333335}) + v2_private_post_private_export_download_export_output = v2PrivatePostPrivateExportDownloadExportOutput = Entry('private/export/download-export-output', ['v2', 'private'], 'POST', {'cost': 3.3333333333333335}) + v2_private_post_private_get_account_summary = v2PrivatePostPrivateGetAccountSummary = Entry('private/get-account-summary', ['v2', 'private'], 'POST', {'cost': 3.3333333333333335}) + v2_private_post_private_create_order = v2PrivatePostPrivateCreateOrder = Entry('private/create-order', ['v2', 'private'], 'POST', {'cost': 0.6666666666666666}) + v2_private_post_private_cancel_order = v2PrivatePostPrivateCancelOrder = Entry('private/cancel-order', ['v2', 'private'], 'POST', {'cost': 0.6666666666666666}) + v2_private_post_private_cancel_all_orders = v2PrivatePostPrivateCancelAllOrders = Entry('private/cancel-all-orders', ['v2', 'private'], 'POST', {'cost': 0.6666666666666666}) + v2_private_post_private_create_order_list = v2PrivatePostPrivateCreateOrderList = Entry('private/create-order-list', ['v2', 'private'], 'POST', {'cost': 3.3333333333333335}) + v2_private_post_private_get_order_history = v2PrivatePostPrivateGetOrderHistory = Entry('private/get-order-history', ['v2', 'private'], 'POST', {'cost': 3.3333333333333335}) + v2_private_post_private_get_open_orders = v2PrivatePostPrivateGetOpenOrders = Entry('private/get-open-orders', ['v2', 'private'], 'POST', {'cost': 3.3333333333333335}) + v2_private_post_private_get_order_detail = v2PrivatePostPrivateGetOrderDetail = Entry('private/get-order-detail', ['v2', 'private'], 'POST', {'cost': 0.3333333333333333}) + v2_private_post_private_get_trades = v2PrivatePostPrivateGetTrades = Entry('private/get-trades', ['v2', 'private'], 'POST', {'cost': 100}) + v2_private_post_private_get_accounts = v2PrivatePostPrivateGetAccounts = Entry('private/get-accounts', ['v2', 'private'], 'POST', {'cost': 3.3333333333333335}) + v2_private_post_private_get_subaccount_balances = v2PrivatePostPrivateGetSubaccountBalances = Entry('private/get-subaccount-balances', ['v2', 'private'], 'POST', {'cost': 3.3333333333333335}) + v2_private_post_private_create_subaccount_transfer = v2PrivatePostPrivateCreateSubaccountTransfer = Entry('private/create-subaccount-transfer', ['v2', 'private'], 'POST', {'cost': 3.3333333333333335}) + v2_private_post_private_otc_get_otc_user = v2PrivatePostPrivateOtcGetOtcUser = Entry('private/otc/get-otc-user', ['v2', 'private'], 'POST', {'cost': 3.3333333333333335}) + v2_private_post_private_otc_get_instruments = v2PrivatePostPrivateOtcGetInstruments = Entry('private/otc/get-instruments', ['v2', 'private'], 'POST', {'cost': 3.3333333333333335}) + v2_private_post_private_otc_request_quote = v2PrivatePostPrivateOtcRequestQuote = Entry('private/otc/request-quote', ['v2', 'private'], 'POST', {'cost': 100}) + v2_private_post_private_otc_accept_quote = v2PrivatePostPrivateOtcAcceptQuote = Entry('private/otc/accept-quote', ['v2', 'private'], 'POST', {'cost': 100}) + v2_private_post_private_otc_get_quote_history = v2PrivatePostPrivateOtcGetQuoteHistory = Entry('private/otc/get-quote-history', ['v2', 'private'], 'POST', {'cost': 3.3333333333333335}) + v2_private_post_private_otc_get_trade_history = v2PrivatePostPrivateOtcGetTradeHistory = Entry('private/otc/get-trade-history', ['v2', 'private'], 'POST', {'cost': 3.3333333333333335}) + v2_private_post_private_otc_create_order = v2PrivatePostPrivateOtcCreateOrder = Entry('private/otc/create-order', ['v2', 'private'], 'POST', {'cost': 3.3333333333333335}) + derivatives_public_get_public_auth = derivativesPublicGetPublicAuth = Entry('public/auth', ['derivatives', 'public'], 'GET', {'cost': 3.3333333333333335}) + derivatives_public_get_public_get_instruments = derivativesPublicGetPublicGetInstruments = Entry('public/get-instruments', ['derivatives', 'public'], 'GET', {'cost': 3.3333333333333335}) + derivatives_public_get_public_get_book = derivativesPublicGetPublicGetBook = Entry('public/get-book', ['derivatives', 'public'], 'GET', {'cost': 1}) + derivatives_public_get_public_get_candlestick = derivativesPublicGetPublicGetCandlestick = Entry('public/get-candlestick', ['derivatives', 'public'], 'GET', {'cost': 1}) + derivatives_public_get_public_get_trades = derivativesPublicGetPublicGetTrades = Entry('public/get-trades', ['derivatives', 'public'], 'GET', {'cost': 1}) + derivatives_public_get_public_get_tickers = derivativesPublicGetPublicGetTickers = Entry('public/get-tickers', ['derivatives', 'public'], 'GET', {'cost': 1}) + derivatives_public_get_public_get_valuations = derivativesPublicGetPublicGetValuations = Entry('public/get-valuations', ['derivatives', 'public'], 'GET', {'cost': 1}) + derivatives_public_get_public_get_expired_settlement_price = derivativesPublicGetPublicGetExpiredSettlementPrice = Entry('public/get-expired-settlement-price', ['derivatives', 'public'], 'GET', {'cost': 3.3333333333333335}) + derivatives_public_get_public_get_insurance = derivativesPublicGetPublicGetInsurance = Entry('public/get-insurance', ['derivatives', 'public'], 'GET', {'cost': 1}) + derivatives_private_post_private_set_cancel_on_disconnect = derivativesPrivatePostPrivateSetCancelOnDisconnect = Entry('private/set-cancel-on-disconnect', ['derivatives', 'private'], 'POST', {'cost': 3.3333333333333335}) + derivatives_private_post_private_get_cancel_on_disconnect = derivativesPrivatePostPrivateGetCancelOnDisconnect = Entry('private/get-cancel-on-disconnect', ['derivatives', 'private'], 'POST', {'cost': 3.3333333333333335}) + derivatives_private_post_private_user_balance = derivativesPrivatePostPrivateUserBalance = Entry('private/user-balance', ['derivatives', 'private'], 'POST', {'cost': 3.3333333333333335}) + derivatives_private_post_private_user_balance_history = derivativesPrivatePostPrivateUserBalanceHistory = Entry('private/user-balance-history', ['derivatives', 'private'], 'POST', {'cost': 3.3333333333333335}) + derivatives_private_post_private_get_positions = derivativesPrivatePostPrivateGetPositions = Entry('private/get-positions', ['derivatives', 'private'], 'POST', {'cost': 3.3333333333333335}) + derivatives_private_post_private_create_order = derivativesPrivatePostPrivateCreateOrder = Entry('private/create-order', ['derivatives', 'private'], 'POST', {'cost': 0.6666666666666666}) + derivatives_private_post_private_create_order_list = derivativesPrivatePostPrivateCreateOrderList = Entry('private/create-order-list', ['derivatives', 'private'], 'POST', {'cost': 3.3333333333333335}) + derivatives_private_post_private_cancel_order = derivativesPrivatePostPrivateCancelOrder = Entry('private/cancel-order', ['derivatives', 'private'], 'POST', {'cost': 0.6666666666666666}) + derivatives_private_post_private_cancel_order_list = derivativesPrivatePostPrivateCancelOrderList = Entry('private/cancel-order-list', ['derivatives', 'private'], 'POST', {'cost': 3.3333333333333335}) + derivatives_private_post_private_cancel_all_orders = derivativesPrivatePostPrivateCancelAllOrders = Entry('private/cancel-all-orders', ['derivatives', 'private'], 'POST', {'cost': 0.6666666666666666}) + derivatives_private_post_private_close_position = derivativesPrivatePostPrivateClosePosition = Entry('private/close-position', ['derivatives', 'private'], 'POST', {'cost': 3.3333333333333335}) + derivatives_private_post_private_convert_collateral = derivativesPrivatePostPrivateConvertCollateral = Entry('private/convert-collateral', ['derivatives', 'private'], 'POST', {'cost': 3.3333333333333335}) + derivatives_private_post_private_get_order_history = derivativesPrivatePostPrivateGetOrderHistory = Entry('private/get-order-history', ['derivatives', 'private'], 'POST', {'cost': 100}) + derivatives_private_post_private_get_open_orders = derivativesPrivatePostPrivateGetOpenOrders = Entry('private/get-open-orders', ['derivatives', 'private'], 'POST', {'cost': 3.3333333333333335}) + derivatives_private_post_private_get_order_detail = derivativesPrivatePostPrivateGetOrderDetail = Entry('private/get-order-detail', ['derivatives', 'private'], 'POST', {'cost': 0.3333333333333333}) + derivatives_private_post_private_get_trades = derivativesPrivatePostPrivateGetTrades = Entry('private/get-trades', ['derivatives', 'private'], 'POST', {'cost': 100}) + derivatives_private_post_private_change_account_leverage = derivativesPrivatePostPrivateChangeAccountLeverage = Entry('private/change-account-leverage', ['derivatives', 'private'], 'POST', {'cost': 3.3333333333333335}) + derivatives_private_post_private_get_transactions = derivativesPrivatePostPrivateGetTransactions = Entry('private/get-transactions', ['derivatives', 'private'], 'POST', {'cost': 3.3333333333333335}) + derivatives_private_post_private_create_subaccount_transfer = derivativesPrivatePostPrivateCreateSubaccountTransfer = Entry('private/create-subaccount-transfer', ['derivatives', 'private'], 'POST', {'cost': 3.3333333333333335}) + derivatives_private_post_private_get_subaccount_balances = derivativesPrivatePostPrivateGetSubaccountBalances = Entry('private/get-subaccount-balances', ['derivatives', 'private'], 'POST', {'cost': 3.3333333333333335}) + derivatives_private_post_private_get_order_list = derivativesPrivatePostPrivateGetOrderList = Entry('private/get-order-list', ['derivatives', 'private'], 'POST', {'cost': 3.3333333333333335}) diff --git a/ccxt/abstract/cryptomus.py b/ccxt/abstract/cryptomus.py new file mode 100644 index 0000000..320ebf0 --- /dev/null +++ b/ccxt/abstract/cryptomus.py @@ -0,0 +1,20 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_v2_user_api_exchange_markets = publicGetV2UserApiExchangeMarkets = Entry('v2/user-api/exchange/markets', 'public', 'GET', {'cost': 1}) + public_get_v2_user_api_exchange_market_price = publicGetV2UserApiExchangeMarketPrice = Entry('v2/user-api/exchange/market/price', 'public', 'GET', {'cost': 1}) + public_get_v1_exchange_market_assets = publicGetV1ExchangeMarketAssets = Entry('v1/exchange/market/assets', 'public', 'GET', {'cost': 1}) + public_get_v1_exchange_market_order_book_currencypair = publicGetV1ExchangeMarketOrderBookCurrencyPair = Entry('v1/exchange/market/order-book/{currencyPair}', 'public', 'GET', {'cost': 1}) + public_get_v1_exchange_market_tickers = publicGetV1ExchangeMarketTickers = Entry('v1/exchange/market/tickers', 'public', 'GET', {'cost': 1}) + public_get_v1_exchange_market_trades_currencypair = publicGetV1ExchangeMarketTradesCurrencyPair = Entry('v1/exchange/market/trades/{currencyPair}', 'public', 'GET', {'cost': 1}) + private_get_v2_user_api_exchange_orders = privateGetV2UserApiExchangeOrders = Entry('v2/user-api/exchange/orders', 'private', 'GET', {'cost': 1}) + private_get_v2_user_api_exchange_orders_history = privateGetV2UserApiExchangeOrdersHistory = Entry('v2/user-api/exchange/orders/history', 'private', 'GET', {'cost': 1}) + private_get_v2_user_api_exchange_account_balance = privateGetV2UserApiExchangeAccountBalance = Entry('v2/user-api/exchange/account/balance', 'private', 'GET', {'cost': 1}) + private_get_v2_user_api_exchange_account_tariffs = privateGetV2UserApiExchangeAccountTariffs = Entry('v2/user-api/exchange/account/tariffs', 'private', 'GET', {'cost': 1}) + private_get_v2_user_api_payment_services = privateGetV2UserApiPaymentServices = Entry('v2/user-api/payment/services', 'private', 'GET', {'cost': 1}) + private_get_v2_user_api_payout_services = privateGetV2UserApiPayoutServices = Entry('v2/user-api/payout/services', 'private', 'GET', {'cost': 1}) + private_get_v2_user_api_transaction_list = privateGetV2UserApiTransactionList = Entry('v2/user-api/transaction/list', 'private', 'GET', {'cost': 1}) + private_post_v2_user_api_exchange_orders = privatePostV2UserApiExchangeOrders = Entry('v2/user-api/exchange/orders', 'private', 'POST', {'cost': 1}) + private_post_v2_user_api_exchange_orders_market = privatePostV2UserApiExchangeOrdersMarket = Entry('v2/user-api/exchange/orders/market', 'private', 'POST', {'cost': 1}) + private_delete_v2_user_api_exchange_orders_orderid = privateDeleteV2UserApiExchangeOrdersOrderId = Entry('v2/user-api/exchange/orders/{orderId}', 'private', 'DELETE', {'cost': 1}) diff --git a/ccxt/abstract/deepcoin.py b/ccxt/abstract/deepcoin.py new file mode 100644 index 0000000..539ffc9 --- /dev/null +++ b/ccxt/abstract/deepcoin.py @@ -0,0 +1,57 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_deepcoin_market_books = publicGetDeepcoinMarketBooks = Entry('deepcoin/market/books', 'public', 'GET', {'cost': 1}) + public_get_deepcoin_market_candles = publicGetDeepcoinMarketCandles = Entry('deepcoin/market/candles', 'public', 'GET', {'cost': 1}) + public_get_deepcoin_market_instruments = publicGetDeepcoinMarketInstruments = Entry('deepcoin/market/instruments', 'public', 'GET', {'cost': 1}) + public_get_deepcoin_market_tickers = publicGetDeepcoinMarketTickers = Entry('deepcoin/market/tickers', 'public', 'GET', {'cost': 1}) + public_get_deepcoin_market_index_candles = publicGetDeepcoinMarketIndexCandles = Entry('deepcoin/market/index-candles', 'public', 'GET', {'cost': 1}) + public_get_deepcoin_market_trades = publicGetDeepcoinMarketTrades = Entry('deepcoin/market/trades', 'public', 'GET', {'cost': 1}) + public_get_deepcoin_market_mark_price_candles = publicGetDeepcoinMarketMarkPriceCandles = Entry('deepcoin/market/mark-price-candles', 'public', 'GET', {'cost': 1}) + public_get_deepcoin_market_step_margin = publicGetDeepcoinMarketStepMargin = Entry('deepcoin/market/step-margin', 'public', 'GET', {'cost': 5}) + private_get_deepcoin_account_balances = privateGetDeepcoinAccountBalances = Entry('deepcoin/account/balances', 'private', 'GET', {'cost': 5}) + private_get_deepcoin_account_bills = privateGetDeepcoinAccountBills = Entry('deepcoin/account/bills', 'private', 'GET', {'cost': 5}) + private_get_deepcoin_account_positions = privateGetDeepcoinAccountPositions = Entry('deepcoin/account/positions', 'private', 'GET', {'cost': 5}) + private_get_deepcoin_trade_fills = privateGetDeepcoinTradeFills = Entry('deepcoin/trade/fills', 'private', 'GET', {'cost': 5}) + private_get_deepcoin_trade_orderbyid = privateGetDeepcoinTradeOrderByID = Entry('deepcoin/trade/orderByID', 'private', 'GET', {'cost': 5}) + private_get_deepcoin_trade_finishorderbyid = privateGetDeepcoinTradeFinishOrderByID = Entry('deepcoin/trade/finishOrderByID', 'private', 'GET', {'cost': 5}) + private_get_deepcoin_trade_orders_history = privateGetDeepcoinTradeOrdersHistory = Entry('deepcoin/trade/orders-history', 'private', 'GET', {'cost': 5}) + private_get_deepcoin_trade_v2_orders_pending = privateGetDeepcoinTradeV2OrdersPending = Entry('deepcoin/trade/v2/orders-pending', 'private', 'GET', {'cost': 5}) + private_get_deepcoin_trade_funding_rate = privateGetDeepcoinTradeFundingRate = Entry('deepcoin/trade/funding-rate', 'private', 'GET', {'cost': 5}) + private_get_deepcoin_trade_fund_rate_current_funding_rate = privateGetDeepcoinTradeFundRateCurrentFundingRate = Entry('deepcoin/trade/fund-rate/current-funding-rate', 'private', 'GET', {'cost': 5}) + private_get_deepcoin_trade_fund_rate_history = privateGetDeepcoinTradeFundRateHistory = Entry('deepcoin/trade/fund-rate/history', 'private', 'GET', {'cost': 5}) + private_get_deepcoin_trade_trigger_orders_pending = privateGetDeepcoinTradeTriggerOrdersPending = Entry('deepcoin/trade/trigger-orders-pending', 'private', 'GET', {'cost': 5}) + private_get_deepcoin_trade_trigger_orders_history = privateGetDeepcoinTradeTriggerOrdersHistory = Entry('deepcoin/trade/trigger-orders-history', 'private', 'GET', {'cost': 5}) + private_get_deepcoin_copytrading_support_contracts = privateGetDeepcoinCopytradingSupportContracts = Entry('deepcoin/copytrading/support-contracts', 'private', 'GET', {'cost': 5}) + private_get_deepcoin_copytrading_leader_position = privateGetDeepcoinCopytradingLeaderPosition = Entry('deepcoin/copytrading/leader-position', 'private', 'GET', {'cost': 5}) + private_get_deepcoin_copytrading_estimate_profit = privateGetDeepcoinCopytradingEstimateProfit = Entry('deepcoin/copytrading/estimate-profit', 'private', 'GET', {'cost': 5}) + private_get_deepcoin_copytrading_history_profit = privateGetDeepcoinCopytradingHistoryProfit = Entry('deepcoin/copytrading/history-profit', 'private', 'GET', {'cost': 5}) + private_get_deepcoin_copytrading_follower_rank = privateGetDeepcoinCopytradingFollowerRank = Entry('deepcoin/copytrading/follower-rank', 'private', 'GET', {'cost': 5}) + private_get_deepcoin_internal_transfer_support = privateGetDeepcoinInternalTransferSupport = Entry('deepcoin/internal-transfer/support', 'private', 'GET', {'cost': 5}) + private_get_deepcoin_internal_transfer_history_order = privateGetDeepcoinInternalTransferHistoryOrder = Entry('deepcoin/internal-transfer/history-order', 'private', 'GET', {'cost': 5}) + private_get_deepcoin_rebate_config = privateGetDeepcoinRebateConfig = Entry('deepcoin/rebate/config', 'private', 'GET', {'cost': 5}) + private_get_deepcoin_agents_users = privateGetDeepcoinAgentsUsers = Entry('deepcoin/agents/users', 'private', 'GET', {'cost': 5}) + private_get_deepcoin_agents_users_rebate_list = privateGetDeepcoinAgentsUsersRebateList = Entry('deepcoin/agents/users/rebate-list', 'private', 'GET', {'cost': 5}) + private_get_deepcoin_agents_users_rebates = privateGetDeepcoinAgentsUsersRebates = Entry('deepcoin/agents/users/rebates', 'private', 'GET', {'cost': 5}) + private_get_deepcoin_asset_deposit_list = privateGetDeepcoinAssetDepositList = Entry('deepcoin/asset/deposit-list', 'private', 'GET', {'cost': 5}) + private_get_deepcoin_asset_withdraw_list = privateGetDeepcoinAssetWithdrawList = Entry('deepcoin/asset/withdraw-list', 'private', 'GET', {'cost': 5}) + private_get_deepcoin_asset_recharge_chain_list = privateGetDeepcoinAssetRechargeChainList = Entry('deepcoin/asset/recharge-chain-list', 'private', 'GET', {'cost': 5}) + private_get_deepcoin_listenkey_acquire = privateGetDeepcoinListenkeyAcquire = Entry('deepcoin/listenkey/acquire', 'private', 'GET', {'cost': 5}) + private_get_deepcoin_listenkey_extend = privateGetDeepcoinListenkeyExtend = Entry('deepcoin/listenkey/extend', 'private', 'GET', {'cost': 5}) + private_post_deepcoin_account_set_leverage = privatePostDeepcoinAccountSetLeverage = Entry('deepcoin/account/set-leverage', 'private', 'POST', {'cost': 5}) + private_post_deepcoin_trade_order = privatePostDeepcoinTradeOrder = Entry('deepcoin/trade/order', 'private', 'POST', {'cost': 5}) + private_post_deepcoin_trade_replace_order = privatePostDeepcoinTradeReplaceOrder = Entry('deepcoin/trade/replace-order', 'private', 'POST', {'cost': 5}) + private_post_deepcoin_trade_cancel_order = privatePostDeepcoinTradeCancelOrder = Entry('deepcoin/trade/cancel-order', 'private', 'POST', {'cost': 5}) + private_post_deepcoin_trade_batch_cancel_order = privatePostDeepcoinTradeBatchCancelOrder = Entry('deepcoin/trade/batch-cancel-order', 'private', 'POST', {'cost': 5}) + private_post_deepcoin_trade_cancel_trigger_order = privatePostDeepcoinTradeCancelTriggerOrder = Entry('deepcoin/trade/cancel-trigger-order', 'private', 'POST', {'cost': 0.16666666666666666}) + private_post_deepcoin_trade_swap_cancel_all = privatePostDeepcoinTradeSwapCancelAll = Entry('deepcoin/trade/swap/cancel-all', 'private', 'POST', {'cost': 5}) + private_post_deepcoin_trade_trigger_order = privatePostDeepcoinTradeTriggerOrder = Entry('deepcoin/trade/trigger-order', 'private', 'POST', {'cost': 5}) + private_post_deepcoin_trade_batch_close_position = privatePostDeepcoinTradeBatchClosePosition = Entry('deepcoin/trade/batch-close-position', 'private', 'POST', {'cost': 5}) + private_post_deepcoin_trade_replace_order_sltp = privatePostDeepcoinTradeReplaceOrderSltp = Entry('deepcoin/trade/replace-order-sltp', 'private', 'POST', {'cost': 5}) + private_post_deepcoin_trade_close_position_by_ids = privatePostDeepcoinTradeClosePositionByIds = Entry('deepcoin/trade/close-position-by-ids', 'private', 'POST', {'cost': 5}) + private_post_deepcoin_copytrading_leader_settings = privatePostDeepcoinCopytradingLeaderSettings = Entry('deepcoin/copytrading/leader-settings', 'private', 'POST', {'cost': 5}) + private_post_deepcoin_copytrading_set_contracts = privatePostDeepcoinCopytradingSetContracts = Entry('deepcoin/copytrading/set-contracts', 'private', 'POST', {'cost': 5}) + private_post_deepcoin_internal_transfer = privatePostDeepcoinInternalTransfer = Entry('deepcoin/internal-transfer', 'private', 'POST', {'cost': 5}) + private_post_deepcoin_rebate_config = privatePostDeepcoinRebateConfig = Entry('deepcoin/rebate/config', 'private', 'POST', {'cost': 5}) + private_post_deepcoin_asset_transfer = privatePostDeepcoinAssetTransfer = Entry('deepcoin/asset/transfer', 'private', 'POST', {'cost': 5}) diff --git a/ccxt/abstract/defx.py b/ccxt/abstract/defx.py new file mode 100644 index 0000000..5325c15 --- /dev/null +++ b/ccxt/abstract/defx.py @@ -0,0 +1,69 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + v1_public_get_healthcheck_ping = v1PublicGetHealthcheckPing = Entry('healthcheck/ping', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_symbols_symbol_ohlc = v1PublicGetSymbolsSymbolOhlc = Entry('symbols/{symbol}/ohlc', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_symbols_symbol_trades = v1PublicGetSymbolsSymbolTrades = Entry('symbols/{symbol}/trades', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_symbols_symbol_prices = v1PublicGetSymbolsSymbolPrices = Entry('symbols/{symbol}/prices', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_symbols_symbol_ticker_24hr = v1PublicGetSymbolsSymbolTicker24hr = Entry('symbols/{symbol}/ticker/24hr', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_symbols_symbol_depth_level_slab = v1PublicGetSymbolsSymbolDepthLevelSlab = Entry('symbols/{symbol}/depth/{level}/{slab}', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_ticker_24hragg = v1PublicGetTicker24HrAgg = Entry('ticker/24HrAgg', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_c_markets = v1PublicGetCMarkets = Entry('c/markets', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_c_markets_metadata = v1PublicGetCMarketsMetadata = Entry('c/markets/metadata', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_analytics_market_stats_newusers = v1PublicGetAnalyticsMarketStatsNewUsers = Entry('analytics/market/stats/newUsers', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_analytics_market_stats_tvl = v1PublicGetAnalyticsMarketStatsTvl = Entry('analytics/market/stats/tvl', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_analytics_market_stats_volumebyinstrument = v1PublicGetAnalyticsMarketStatsVolumeByInstrument = Entry('analytics/market/stats/volumeByInstrument', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_analytics_market_stats_liquidation = v1PublicGetAnalyticsMarketStatsLiquidation = Entry('analytics/market/stats/liquidation', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_analytics_market_stats_totalvolume = v1PublicGetAnalyticsMarketStatsTotalVolume = Entry('analytics/market/stats/totalVolume', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_analytics_market_stats_openinterest = v1PublicGetAnalyticsMarketStatsOpenInterest = Entry('analytics/market/stats/openInterest', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_analytics_market_stats_totaltrades = v1PublicGetAnalyticsMarketStatsTotalTrades = Entry('analytics/market/stats/totalTrades', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_analytics_market_stats_basis = v1PublicGetAnalyticsMarketStatsBasis = Entry('analytics/market/stats/basis', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_analytics_market_stats_insurancefund = v1PublicGetAnalyticsMarketStatsInsuranceFund = Entry('analytics/market/stats/insuranceFund', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_analytics_market_stats_longandshortratio = v1PublicGetAnalyticsMarketStatsLongAndShortRatio = Entry('analytics/market/stats/longAndShortRatio', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_analytics_market_stats_fundingrate = v1PublicGetAnalyticsMarketStatsFundingRate = Entry('analytics/market/stats/fundingRate', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_analytics_market_overview = v1PublicGetAnalyticsMarketOverview = Entry('analytics/market/overview', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_explorer_search = v1PublicGetExplorerSearch = Entry('explorer/search', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_explorer_transactions = v1PublicGetExplorerTransactions = Entry('explorer/transactions', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_explorer_blocks = v1PublicGetExplorerBlocks = Entry('explorer/blocks', ['v1', 'public'], 'GET', {'cost': 1}) + v1_private_get_api_order_orderid = v1PrivateGetApiOrderOrderId = Entry('api/order/{orderId}', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_api_orders = v1PrivateGetApiOrders = Entry('api/orders', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_api_orders_oco_parentorderid = v1PrivateGetApiOrdersOcoParentOrderId = Entry('api/orders/oco/{parentOrderId}', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_api_trades = v1PrivateGetApiTrades = Entry('api/trades', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_api_position_active = v1PrivateGetApiPositionActive = Entry('api/position/active', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_api_users_metadata_leverage = v1PrivateGetApiUsersMetadataLeverage = Entry('api/users/metadata/leverage', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_api_users_metadata_feemultiplier = v1PrivateGetApiUsersMetadataFeeMultiplier = Entry('api/users/metadata/feeMultiplier', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_api_users_metadata_slippage = v1PrivateGetApiUsersMetadataSlippage = Entry('api/users/metadata/slippage', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_api_users_referral = v1PrivateGetApiUsersReferral = Entry('api/users/referral', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_api_users_apikeys = v1PrivateGetApiUsersApikeys = Entry('api/users/apikeys', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_connection_signature_message_evm = v1PrivateGetConnectionSignatureMessageEvm = Entry('connection-signature-message/evm', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_api_users_profile_wallets = v1PrivateGetApiUsersProfileWallets = Entry('api/users/profile/wallets', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_api_notifications = v1PrivateGetApiNotifications = Entry('api/notifications', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_api_wallet_balance = v1PrivateGetApiWalletBalance = Entry('api/wallet/balance', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_api_wallet_transactions = v1PrivateGetApiWalletTransactions = Entry('api/wallet/transactions', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_api_analytics_user_overview = v1PrivateGetApiAnalyticsUserOverview = Entry('api/analytics/user/overview', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_api_analytics_user_pnl = v1PrivateGetApiAnalyticsUserPnl = Entry('api/analytics/user/pnl', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_api_analytics_points_overview = v1PrivateGetApiAnalyticsPointsOverview = Entry('api/analytics/points/overview', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_api_analytics_points_history = v1PrivateGetApiAnalyticsPointsHistory = Entry('api/analytics/points/history', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_post_api_order = v1PrivatePostApiOrder = Entry('api/order', ['v1', 'private'], 'POST', {'cost': 1}) + v1_private_post_api_position_oco = v1PrivatePostApiPositionOco = Entry('api/position/oco', ['v1', 'private'], 'POST', {'cost': 1}) + v1_private_post_api_users_socket_listenkeys = v1PrivatePostApiUsersSocketListenKeys = Entry('api/users/socket/listenKeys', ['v1', 'private'], 'POST', {'cost': 1}) + v1_private_post_api_users_metadata_leverage = v1PrivatePostApiUsersMetadataLeverage = Entry('api/users/metadata/leverage', ['v1', 'private'], 'POST', {'cost': 1}) + v1_private_post_api_users_metadata_feemultiplier = v1PrivatePostApiUsersMetadataFeeMultiplier = Entry('api/users/metadata/feeMultiplier', ['v1', 'private'], 'POST', {'cost': 1}) + v1_private_post_api_users_metadata_slippage = v1PrivatePostApiUsersMetadataSlippage = Entry('api/users/metadata/slippage', ['v1', 'private'], 'POST', {'cost': 1}) + v1_private_post_api_users_referral_recordreferralsignup = v1PrivatePostApiUsersReferralRecordReferralSignup = Entry('api/users/referral/recordReferralSignup', ['v1', 'private'], 'POST', {'cost': 1}) + v1_private_post_api_users_apikeys = v1PrivatePostApiUsersApikeys = Entry('api/users/apikeys', ['v1', 'private'], 'POST', {'cost': 1}) + v1_private_post_api_users_profile_wallets = v1PrivatePostApiUsersProfileWallets = Entry('api/users/profile/wallets', ['v1', 'private'], 'POST', {'cost': 1}) + v1_private_post_api_transfers_withdrawal = v1PrivatePostApiTransfersWithdrawal = Entry('api/transfers/withdrawal', ['v1', 'private'], 'POST', {'cost': 1}) + v1_private_post_api_transfers_bridge_withdrawal = v1PrivatePostApiTransfersBridgeWithdrawal = Entry('api/transfers/bridge/withdrawal', ['v1', 'private'], 'POST', {'cost': 1}) + v1_private_put_api_position_updatepositionmargin = v1PrivatePutApiPositionUpdatePositionMargin = Entry('api/position/updatePositionMargin', ['v1', 'private'], 'PUT', {'cost': 1}) + v1_private_put_api_users_socket_listenkeys_listenkey = v1PrivatePutApiUsersSocketListenKeysListenKey = Entry('api/users/socket/listenKeys/{listenKey}', ['v1', 'private'], 'PUT', {'cost': 1}) + v1_private_put_api_users_apikeys_accesskey_status = v1PrivatePutApiUsersApikeysAccessKeyStatus = Entry('api/users/apikeys/{accessKey}/status', ['v1', 'private'], 'PUT', {'cost': 1}) + v1_private_put_api_users_referral = v1PrivatePutApiUsersReferral = Entry('api/users/referral', ['v1', 'private'], 'PUT', {'cost': 1}) + v1_private_patch_api_users_apikeys_accesskey = v1PrivatePatchApiUsersApikeysAccessKey = Entry('api/users/apikeys/{accessKey}', ['v1', 'private'], 'PATCH', {'cost': 1}) + v1_private_delete_api_orders_allopen = v1PrivateDeleteApiOrdersAllOpen = Entry('api/orders/allOpen', ['v1', 'private'], 'DELETE', {'cost': 1}) + v1_private_delete_api_order_orderid = v1PrivateDeleteApiOrderOrderId = Entry('api/order/{orderId}', ['v1', 'private'], 'DELETE', {'cost': 1}) + v1_private_delete_api_position_positionid = v1PrivateDeleteApiPositionPositionId = Entry('api/position/{positionId}', ['v1', 'private'], 'DELETE', {'cost': 1}) + v1_private_delete_api_position_all = v1PrivateDeleteApiPositionAll = Entry('api/position/all', ['v1', 'private'], 'DELETE', {'cost': 1}) + v1_private_delete_api_users_socket_listenkeys_listenkey = v1PrivateDeleteApiUsersSocketListenKeysListenKey = Entry('api/users/socket/listenKeys/{listenKey}', ['v1', 'private'], 'DELETE', {'cost': 1}) + v1_private_delete_api_users_apikeys_accesskey = v1PrivateDeleteApiUsersApikeysAccessKey = Entry('api/users/apikeys/{accessKey}', ['v1', 'private'], 'DELETE', {'cost': 1}) diff --git a/ccxt/abstract/delta.py b/ccxt/abstract/delta.py new file mode 100644 index 0000000..ce5bd0d --- /dev/null +++ b/ccxt/abstract/delta.py @@ -0,0 +1,54 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_assets = publicGetAssets = Entry('assets', 'public', 'GET', {}) + public_get_indices = publicGetIndices = Entry('indices', 'public', 'GET', {}) + public_get_products = publicGetProducts = Entry('products', 'public', 'GET', {}) + public_get_products_symbol = publicGetProductsSymbol = Entry('products/{symbol}', 'public', 'GET', {}) + public_get_tickers = publicGetTickers = Entry('tickers', 'public', 'GET', {}) + public_get_tickers_symbol = publicGetTickersSymbol = Entry('tickers/{symbol}', 'public', 'GET', {}) + public_get_l2orderbook_symbol = publicGetL2orderbookSymbol = Entry('l2orderbook/{symbol}', 'public', 'GET', {}) + public_get_trades_symbol = publicGetTradesSymbol = Entry('trades/{symbol}', 'public', 'GET', {}) + public_get_stats = publicGetStats = Entry('stats', 'public', 'GET', {}) + public_get_history_candles = publicGetHistoryCandles = Entry('history/candles', 'public', 'GET', {}) + public_get_history_sparklines = publicGetHistorySparklines = Entry('history/sparklines', 'public', 'GET', {}) + public_get_settings = publicGetSettings = Entry('settings', 'public', 'GET', {}) + private_get_orders = privateGetOrders = Entry('orders', 'private', 'GET', {}) + private_get_orders_order_id = privateGetOrdersOrderId = Entry('orders/{order_id}', 'private', 'GET', {}) + private_get_orders_client_order_id_client_oid = privateGetOrdersClientOrderIdClientOid = Entry('orders/client_order_id/{client_oid}', 'private', 'GET', {}) + private_get_products_product_id_orders_leverage = privateGetProductsProductIdOrdersLeverage = Entry('products/{product_id}/orders/leverage', 'private', 'GET', {}) + private_get_positions_margined = privateGetPositionsMargined = Entry('positions/margined', 'private', 'GET', {}) + private_get_positions = privateGetPositions = Entry('positions', 'private', 'GET', {}) + private_get_orders_history = privateGetOrdersHistory = Entry('orders/history', 'private', 'GET', {}) + private_get_fills = privateGetFills = Entry('fills', 'private', 'GET', {}) + private_get_fills_history_download_csv = privateGetFillsHistoryDownloadCsv = Entry('fills/history/download/csv', 'private', 'GET', {}) + private_get_wallet_balances = privateGetWalletBalances = Entry('wallet/balances', 'private', 'GET', {}) + private_get_wallet_transactions = privateGetWalletTransactions = Entry('wallet/transactions', 'private', 'GET', {}) + private_get_wallet_transactions_download = privateGetWalletTransactionsDownload = Entry('wallet/transactions/download', 'private', 'GET', {}) + private_get_wallets_sub_accounts_transfer_history = privateGetWalletsSubAccountsTransferHistory = Entry('wallets/sub_accounts_transfer_history', 'private', 'GET', {}) + private_get_users_trading_preferences = privateGetUsersTradingPreferences = Entry('users/trading_preferences', 'private', 'GET', {}) + private_get_sub_accounts = privateGetSubAccounts = Entry('sub_accounts', 'private', 'GET', {}) + private_get_profile = privateGetProfile = Entry('profile', 'private', 'GET', {}) + private_get_heartbeat = privateGetHeartbeat = Entry('heartbeat', 'private', 'GET', {}) + private_get_deposits_address = privateGetDepositsAddress = Entry('deposits/address', 'private', 'GET', {}) + private_post_orders = privatePostOrders = Entry('orders', 'private', 'POST', {}) + private_post_orders_bracket = privatePostOrdersBracket = Entry('orders/bracket', 'private', 'POST', {}) + private_post_orders_batch = privatePostOrdersBatch = Entry('orders/batch', 'private', 'POST', {}) + private_post_products_product_id_orders_leverage = privatePostProductsProductIdOrdersLeverage = Entry('products/{product_id}/orders/leverage', 'private', 'POST', {}) + private_post_positions_change_margin = privatePostPositionsChangeMargin = Entry('positions/change_margin', 'private', 'POST', {}) + private_post_positions_close_all = privatePostPositionsCloseAll = Entry('positions/close_all', 'private', 'POST', {}) + private_post_wallets_sub_account_balance_transfer = privatePostWalletsSubAccountBalanceTransfer = Entry('wallets/sub_account_balance_transfer', 'private', 'POST', {}) + private_post_heartbeat_create = privatePostHeartbeatCreate = Entry('heartbeat/create', 'private', 'POST', {}) + private_post_heartbeat = privatePostHeartbeat = Entry('heartbeat', 'private', 'POST', {}) + private_post_orders_cancel_after = privatePostOrdersCancelAfter = Entry('orders/cancel_after', 'private', 'POST', {}) + private_post_orders_leverage = privatePostOrdersLeverage = Entry('orders/leverage', 'private', 'POST', {}) + private_put_orders = privatePutOrders = Entry('orders', 'private', 'PUT', {}) + private_put_orders_bracket = privatePutOrdersBracket = Entry('orders/bracket', 'private', 'PUT', {}) + private_put_orders_batch = privatePutOrdersBatch = Entry('orders/batch', 'private', 'PUT', {}) + private_put_positions_auto_topup = privatePutPositionsAutoTopup = Entry('positions/auto_topup', 'private', 'PUT', {}) + private_put_users_update_mmp = privatePutUsersUpdateMmp = Entry('users/update_mmp', 'private', 'PUT', {}) + private_put_users_reset_mmp = privatePutUsersResetMmp = Entry('users/reset_mmp', 'private', 'PUT', {}) + private_delete_orders = privateDeleteOrders = Entry('orders', 'private', 'DELETE', {}) + private_delete_orders_all = privateDeleteOrdersAll = Entry('orders/all', 'private', 'DELETE', {}) + private_delete_orders_batch = privateDeleteOrdersBatch = Entry('orders/batch', 'private', 'DELETE', {}) diff --git a/ccxt/abstract/deribit.py b/ccxt/abstract/deribit.py new file mode 100644 index 0000000..a167c9a --- /dev/null +++ b/ccxt/abstract/deribit.py @@ -0,0 +1,126 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_auth = publicGetAuth = Entry('auth', 'public', 'GET', {'cost': 1}) + public_get_exchange_token = publicGetExchangeToken = Entry('exchange_token', 'public', 'GET', {'cost': 1}) + public_get_fork_token = publicGetForkToken = Entry('fork_token', 'public', 'GET', {'cost': 1}) + public_get_set_heartbeat = publicGetSetHeartbeat = Entry('set_heartbeat', 'public', 'GET', {'cost': 1}) + public_get_disable_heartbeat = publicGetDisableHeartbeat = Entry('disable_heartbeat', 'public', 'GET', {'cost': 1}) + public_get_get_time = publicGetGetTime = Entry('get_time', 'public', 'GET', {'cost': 1}) + public_get_hello = publicGetHello = Entry('hello', 'public', 'GET', {'cost': 1}) + public_get_status = publicGetStatus = Entry('status', 'public', 'GET', {'cost': 1}) + public_get_test = publicGetTest = Entry('test', 'public', 'GET', {'cost': 1}) + public_get_subscribe = publicGetSubscribe = Entry('subscribe', 'public', 'GET', {'cost': 1}) + public_get_unsubscribe = publicGetUnsubscribe = Entry('unsubscribe', 'public', 'GET', {'cost': 1}) + public_get_unsubscribe_all = publicGetUnsubscribeAll = Entry('unsubscribe_all', 'public', 'GET', {'cost': 1}) + public_get_get_announcements = publicGetGetAnnouncements = Entry('get_announcements', 'public', 'GET', {'cost': 1}) + public_get_get_book_summary_by_currency = publicGetGetBookSummaryByCurrency = Entry('get_book_summary_by_currency', 'public', 'GET', {'cost': 1}) + public_get_get_book_summary_by_instrument = publicGetGetBookSummaryByInstrument = Entry('get_book_summary_by_instrument', 'public', 'GET', {'cost': 1}) + public_get_get_contract_size = publicGetGetContractSize = Entry('get_contract_size', 'public', 'GET', {'cost': 1}) + public_get_get_currencies = publicGetGetCurrencies = Entry('get_currencies', 'public', 'GET', {'cost': 1}) + public_get_get_delivery_prices = publicGetGetDeliveryPrices = Entry('get_delivery_prices', 'public', 'GET', {'cost': 1}) + public_get_get_funding_chart_data = publicGetGetFundingChartData = Entry('get_funding_chart_data', 'public', 'GET', {'cost': 1}) + public_get_get_funding_rate_history = publicGetGetFundingRateHistory = Entry('get_funding_rate_history', 'public', 'GET', {'cost': 1}) + public_get_get_funding_rate_value = publicGetGetFundingRateValue = Entry('get_funding_rate_value', 'public', 'GET', {'cost': 1}) + public_get_get_historical_volatility = publicGetGetHistoricalVolatility = Entry('get_historical_volatility', 'public', 'GET', {'cost': 1}) + public_get_get_index = publicGetGetIndex = Entry('get_index', 'public', 'GET', {'cost': 1}) + public_get_get_index_price = publicGetGetIndexPrice = Entry('get_index_price', 'public', 'GET', {'cost': 1}) + public_get_get_index_price_names = publicGetGetIndexPriceNames = Entry('get_index_price_names', 'public', 'GET', {'cost': 1}) + public_get_get_instrument = publicGetGetInstrument = Entry('get_instrument', 'public', 'GET', {'cost': 1}) + public_get_get_instruments = publicGetGetInstruments = Entry('get_instruments', 'public', 'GET', {'cost': 1}) + public_get_get_last_settlements_by_currency = publicGetGetLastSettlementsByCurrency = Entry('get_last_settlements_by_currency', 'public', 'GET', {'cost': 1}) + public_get_get_last_settlements_by_instrument = publicGetGetLastSettlementsByInstrument = Entry('get_last_settlements_by_instrument', 'public', 'GET', {'cost': 1}) + public_get_get_last_trades_by_currency = publicGetGetLastTradesByCurrency = Entry('get_last_trades_by_currency', 'public', 'GET', {'cost': 1}) + public_get_get_last_trades_by_currency_and_time = publicGetGetLastTradesByCurrencyAndTime = Entry('get_last_trades_by_currency_and_time', 'public', 'GET', {'cost': 1}) + public_get_get_last_trades_by_instrument = publicGetGetLastTradesByInstrument = Entry('get_last_trades_by_instrument', 'public', 'GET', {'cost': 1}) + public_get_get_last_trades_by_instrument_and_time = publicGetGetLastTradesByInstrumentAndTime = Entry('get_last_trades_by_instrument_and_time', 'public', 'GET', {'cost': 1}) + public_get_get_mark_price_history = publicGetGetMarkPriceHistory = Entry('get_mark_price_history', 'public', 'GET', {'cost': 1}) + public_get_get_order_book = publicGetGetOrderBook = Entry('get_order_book', 'public', 'GET', {'cost': 1}) + public_get_get_trade_volumes = publicGetGetTradeVolumes = Entry('get_trade_volumes', 'public', 'GET', {'cost': 1}) + public_get_get_tradingview_chart_data = publicGetGetTradingviewChartData = Entry('get_tradingview_chart_data', 'public', 'GET', {'cost': 1}) + public_get_get_volatility_index_data = publicGetGetVolatilityIndexData = Entry('get_volatility_index_data', 'public', 'GET', {'cost': 1}) + public_get_ticker = publicGetTicker = Entry('ticker', 'public', 'GET', {'cost': 1}) + private_get_logout = privateGetLogout = Entry('logout', 'private', 'GET', {'cost': 1}) + private_get_enable_cancel_on_disconnect = privateGetEnableCancelOnDisconnect = Entry('enable_cancel_on_disconnect', 'private', 'GET', {'cost': 1}) + private_get_disable_cancel_on_disconnect = privateGetDisableCancelOnDisconnect = Entry('disable_cancel_on_disconnect', 'private', 'GET', {'cost': 1}) + private_get_get_cancel_on_disconnect = privateGetGetCancelOnDisconnect = Entry('get_cancel_on_disconnect', 'private', 'GET', {'cost': 1}) + private_get_subscribe = privateGetSubscribe = Entry('subscribe', 'private', 'GET', {'cost': 1}) + private_get_unsubscribe = privateGetUnsubscribe = Entry('unsubscribe', 'private', 'GET', {'cost': 1}) + private_get_unsubscribe_all = privateGetUnsubscribeAll = Entry('unsubscribe_all', 'private', 'GET', {'cost': 1}) + private_get_change_api_key_name = privateGetChangeApiKeyName = Entry('change_api_key_name', 'private', 'GET', {'cost': 1}) + private_get_change_scope_in_api_key = privateGetChangeScopeInApiKey = Entry('change_scope_in_api_key', 'private', 'GET', {'cost': 1}) + private_get_change_subaccount_name = privateGetChangeSubaccountName = Entry('change_subaccount_name', 'private', 'GET', {'cost': 1}) + private_get_create_api_key = privateGetCreateApiKey = Entry('create_api_key', 'private', 'GET', {'cost': 1}) + private_get_create_subaccount = privateGetCreateSubaccount = Entry('create_subaccount', 'private', 'GET', {'cost': 1}) + private_get_disable_api_key = privateGetDisableApiKey = Entry('disable_api_key', 'private', 'GET', {'cost': 1}) + private_get_disable_tfa_for_subaccount = privateGetDisableTfaForSubaccount = Entry('disable_tfa_for_subaccount', 'private', 'GET', {'cost': 1}) + private_get_enable_affiliate_program = privateGetEnableAffiliateProgram = Entry('enable_affiliate_program', 'private', 'GET', {'cost': 1}) + private_get_enable_api_key = privateGetEnableApiKey = Entry('enable_api_key', 'private', 'GET', {'cost': 1}) + private_get_get_access_log = privateGetGetAccessLog = Entry('get_access_log', 'private', 'GET', {'cost': 1}) + private_get_get_account_summary = privateGetGetAccountSummary = Entry('get_account_summary', 'private', 'GET', {'cost': 1}) + private_get_get_account_summaries = privateGetGetAccountSummaries = Entry('get_account_summaries', 'private', 'GET', {'cost': 1}) + private_get_get_affiliate_program_info = privateGetGetAffiliateProgramInfo = Entry('get_affiliate_program_info', 'private', 'GET', {'cost': 1}) + private_get_get_email_language = privateGetGetEmailLanguage = Entry('get_email_language', 'private', 'GET', {'cost': 1}) + private_get_get_new_announcements = privateGetGetNewAnnouncements = Entry('get_new_announcements', 'private', 'GET', {'cost': 1}) + private_get_get_portfolio_margins = privateGetGetPortfolioMargins = Entry('get_portfolio_margins', 'private', 'GET', {'cost': 1}) + private_get_get_position = privateGetGetPosition = Entry('get_position', 'private', 'GET', {'cost': 1}) + private_get_get_positions = privateGetGetPositions = Entry('get_positions', 'private', 'GET', {'cost': 1}) + private_get_get_subaccounts = privateGetGetSubaccounts = Entry('get_subaccounts', 'private', 'GET', {'cost': 1}) + private_get_get_subaccounts_details = privateGetGetSubaccountsDetails = Entry('get_subaccounts_details', 'private', 'GET', {'cost': 1}) + private_get_get_transaction_log = privateGetGetTransactionLog = Entry('get_transaction_log', 'private', 'GET', {'cost': 1}) + private_get_list_api_keys = privateGetListApiKeys = Entry('list_api_keys', 'private', 'GET', {'cost': 1}) + private_get_remove_api_key = privateGetRemoveApiKey = Entry('remove_api_key', 'private', 'GET', {'cost': 1}) + private_get_remove_subaccount = privateGetRemoveSubaccount = Entry('remove_subaccount', 'private', 'GET', {'cost': 1}) + private_get_reset_api_key = privateGetResetApiKey = Entry('reset_api_key', 'private', 'GET', {'cost': 1}) + private_get_set_announcement_as_read = privateGetSetAnnouncementAsRead = Entry('set_announcement_as_read', 'private', 'GET', {'cost': 1}) + private_get_set_api_key_as_default = privateGetSetApiKeyAsDefault = Entry('set_api_key_as_default', 'private', 'GET', {'cost': 1}) + private_get_set_email_for_subaccount = privateGetSetEmailForSubaccount = Entry('set_email_for_subaccount', 'private', 'GET', {'cost': 1}) + private_get_set_email_language = privateGetSetEmailLanguage = Entry('set_email_language', 'private', 'GET', {'cost': 1}) + private_get_set_password_for_subaccount = privateGetSetPasswordForSubaccount = Entry('set_password_for_subaccount', 'private', 'GET', {'cost': 1}) + private_get_toggle_notifications_from_subaccount = privateGetToggleNotificationsFromSubaccount = Entry('toggle_notifications_from_subaccount', 'private', 'GET', {'cost': 1}) + private_get_toggle_subaccount_login = privateGetToggleSubaccountLogin = Entry('toggle_subaccount_login', 'private', 'GET', {'cost': 1}) + private_get_execute_block_trade = privateGetExecuteBlockTrade = Entry('execute_block_trade', 'private', 'GET', {'cost': 4}) + private_get_get_block_trade = privateGetGetBlockTrade = Entry('get_block_trade', 'private', 'GET', {'cost': 1}) + private_get_get_last_block_trades_by_currency = privateGetGetLastBlockTradesByCurrency = Entry('get_last_block_trades_by_currency', 'private', 'GET', {'cost': 1}) + private_get_invalidate_block_trade_signature = privateGetInvalidateBlockTradeSignature = Entry('invalidate_block_trade_signature', 'private', 'GET', {'cost': 1}) + private_get_verify_block_trade = privateGetVerifyBlockTrade = Entry('verify_block_trade', 'private', 'GET', {'cost': 4}) + private_get_buy = privateGetBuy = Entry('buy', 'private', 'GET', {'cost': 4}) + private_get_sell = privateGetSell = Entry('sell', 'private', 'GET', {'cost': 4}) + private_get_edit = privateGetEdit = Entry('edit', 'private', 'GET', {'cost': 4}) + private_get_edit_by_label = privateGetEditByLabel = Entry('edit_by_label', 'private', 'GET', {'cost': 4}) + private_get_cancel = privateGetCancel = Entry('cancel', 'private', 'GET', {'cost': 4}) + private_get_cancel_all = privateGetCancelAll = Entry('cancel_all', 'private', 'GET', {'cost': 4}) + private_get_cancel_all_by_currency = privateGetCancelAllByCurrency = Entry('cancel_all_by_currency', 'private', 'GET', {'cost': 4}) + private_get_cancel_all_by_instrument = privateGetCancelAllByInstrument = Entry('cancel_all_by_instrument', 'private', 'GET', {'cost': 4}) + private_get_cancel_by_label = privateGetCancelByLabel = Entry('cancel_by_label', 'private', 'GET', {'cost': 4}) + private_get_close_position = privateGetClosePosition = Entry('close_position', 'private', 'GET', {'cost': 4}) + private_get_get_margins = privateGetGetMargins = Entry('get_margins', 'private', 'GET', {'cost': 1}) + private_get_get_mmp_config = privateGetGetMmpConfig = Entry('get_mmp_config', 'private', 'GET', {'cost': 1}) + private_get_get_open_orders_by_currency = privateGetGetOpenOrdersByCurrency = Entry('get_open_orders_by_currency', 'private', 'GET', {'cost': 1}) + private_get_get_open_orders_by_instrument = privateGetGetOpenOrdersByInstrument = Entry('get_open_orders_by_instrument', 'private', 'GET', {'cost': 1}) + private_get_get_order_history_by_currency = privateGetGetOrderHistoryByCurrency = Entry('get_order_history_by_currency', 'private', 'GET', {'cost': 1}) + private_get_get_order_history_by_instrument = privateGetGetOrderHistoryByInstrument = Entry('get_order_history_by_instrument', 'private', 'GET', {'cost': 1}) + private_get_get_order_margin_by_ids = privateGetGetOrderMarginByIds = Entry('get_order_margin_by_ids', 'private', 'GET', {'cost': 1}) + private_get_get_order_state = privateGetGetOrderState = Entry('get_order_state', 'private', 'GET', {'cost': 1}) + private_get_get_stop_order_history = privateGetGetStopOrderHistory = Entry('get_stop_order_history', 'private', 'GET', {'cost': 1}) + private_get_get_trigger_order_history = privateGetGetTriggerOrderHistory = Entry('get_trigger_order_history', 'private', 'GET', {'cost': 1}) + private_get_get_user_trades_by_currency = privateGetGetUserTradesByCurrency = Entry('get_user_trades_by_currency', 'private', 'GET', {'cost': 1}) + private_get_get_user_trades_by_currency_and_time = privateGetGetUserTradesByCurrencyAndTime = Entry('get_user_trades_by_currency_and_time', 'private', 'GET', {'cost': 1}) + private_get_get_user_trades_by_instrument = privateGetGetUserTradesByInstrument = Entry('get_user_trades_by_instrument', 'private', 'GET', {'cost': 1}) + private_get_get_user_trades_by_instrument_and_time = privateGetGetUserTradesByInstrumentAndTime = Entry('get_user_trades_by_instrument_and_time', 'private', 'GET', {'cost': 1}) + private_get_get_user_trades_by_order = privateGetGetUserTradesByOrder = Entry('get_user_trades_by_order', 'private', 'GET', {'cost': 1}) + private_get_reset_mmp = privateGetResetMmp = Entry('reset_mmp', 'private', 'GET', {'cost': 1}) + private_get_set_mmp_config = privateGetSetMmpConfig = Entry('set_mmp_config', 'private', 'GET', {'cost': 1}) + private_get_get_settlement_history_by_instrument = privateGetGetSettlementHistoryByInstrument = Entry('get_settlement_history_by_instrument', 'private', 'GET', {'cost': 1}) + private_get_get_settlement_history_by_currency = privateGetGetSettlementHistoryByCurrency = Entry('get_settlement_history_by_currency', 'private', 'GET', {'cost': 1}) + private_get_cancel_transfer_by_id = privateGetCancelTransferById = Entry('cancel_transfer_by_id', 'private', 'GET', {'cost': 1}) + private_get_cancel_withdrawal = privateGetCancelWithdrawal = Entry('cancel_withdrawal', 'private', 'GET', {'cost': 1}) + private_get_create_deposit_address = privateGetCreateDepositAddress = Entry('create_deposit_address', 'private', 'GET', {'cost': 1}) + private_get_get_current_deposit_address = privateGetGetCurrentDepositAddress = Entry('get_current_deposit_address', 'private', 'GET', {'cost': 1}) + private_get_get_deposits = privateGetGetDeposits = Entry('get_deposits', 'private', 'GET', {'cost': 1}) + private_get_get_transfers = privateGetGetTransfers = Entry('get_transfers', 'private', 'GET', {'cost': 1}) + private_get_get_withdrawals = privateGetGetWithdrawals = Entry('get_withdrawals', 'private', 'GET', {'cost': 1}) + private_get_submit_transfer_to_subaccount = privateGetSubmitTransferToSubaccount = Entry('submit_transfer_to_subaccount', 'private', 'GET', {'cost': 1}) + private_get_submit_transfer_to_user = privateGetSubmitTransferToUser = Entry('submit_transfer_to_user', 'private', 'GET', {'cost': 1}) + private_get_withdraw = privateGetWithdraw = Entry('withdraw', 'private', 'GET', {'cost': 1}) diff --git a/ccxt/abstract/derive.py b/ccxt/abstract/derive.py new file mode 100644 index 0000000..46f3415 --- /dev/null +++ b/ccxt/abstract/derive.py @@ -0,0 +1,117 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_get_all_currencies = publicGetGetAllCurrencies = Entry('get_all_currencies', 'public', 'GET', {}) + public_post_build_register_session_key_tx = publicPostBuildRegisterSessionKeyTx = Entry('build_register_session_key_tx', 'public', 'POST', {}) + public_post_register_session_key = publicPostRegisterSessionKey = Entry('register_session_key', 'public', 'POST', {}) + public_post_deregister_session_key = publicPostDeregisterSessionKey = Entry('deregister_session_key', 'public', 'POST', {}) + public_post_login = publicPostLogin = Entry('login', 'public', 'POST', {}) + public_post_statistics = publicPostStatistics = Entry('statistics', 'public', 'POST', {}) + public_post_get_all_currencies = publicPostGetAllCurrencies = Entry('get_all_currencies', 'public', 'POST', {}) + public_post_get_currency = publicPostGetCurrency = Entry('get_currency', 'public', 'POST', {}) + public_post_get_instrument = publicPostGetInstrument = Entry('get_instrument', 'public', 'POST', {}) + public_post_get_all_instruments = publicPostGetAllInstruments = Entry('get_all_instruments', 'public', 'POST', {}) + public_post_get_instruments = publicPostGetInstruments = Entry('get_instruments', 'public', 'POST', {}) + public_post_get_ticker = publicPostGetTicker = Entry('get_ticker', 'public', 'POST', {}) + public_post_get_latest_signed_feeds = publicPostGetLatestSignedFeeds = Entry('get_latest_signed_feeds', 'public', 'POST', {}) + public_post_get_option_settlement_prices = publicPostGetOptionSettlementPrices = Entry('get_option_settlement_prices', 'public', 'POST', {}) + public_post_get_spot_feed_history = publicPostGetSpotFeedHistory = Entry('get_spot_feed_history', 'public', 'POST', {}) + public_post_get_spot_feed_history_candles = publicPostGetSpotFeedHistoryCandles = Entry('get_spot_feed_history_candles', 'public', 'POST', {}) + public_post_get_funding_rate_history = publicPostGetFundingRateHistory = Entry('get_funding_rate_history', 'public', 'POST', {}) + public_post_get_trade_history = publicPostGetTradeHistory = Entry('get_trade_history', 'public', 'POST', {}) + public_post_get_option_settlement_history = publicPostGetOptionSettlementHistory = Entry('get_option_settlement_history', 'public', 'POST', {}) + public_post_get_liquidation_history = publicPostGetLiquidationHistory = Entry('get_liquidation_history', 'public', 'POST', {}) + public_post_get_interest_rate_history = publicPostGetInterestRateHistory = Entry('get_interest_rate_history', 'public', 'POST', {}) + public_post_get_transaction = publicPostGetTransaction = Entry('get_transaction', 'public', 'POST', {}) + public_post_get_margin = publicPostGetMargin = Entry('get_margin', 'public', 'POST', {}) + public_post_margin_watch = publicPostMarginWatch = Entry('margin_watch', 'public', 'POST', {}) + public_post_validate_invite_code = publicPostValidateInviteCode = Entry('validate_invite_code', 'public', 'POST', {}) + public_post_get_points = publicPostGetPoints = Entry('get_points', 'public', 'POST', {}) + public_post_get_all_points = publicPostGetAllPoints = Entry('get_all_points', 'public', 'POST', {}) + public_post_get_points_leaderboard = publicPostGetPointsLeaderboard = Entry('get_points_leaderboard', 'public', 'POST', {}) + public_post_get_descendant_tree = publicPostGetDescendantTree = Entry('get_descendant_tree', 'public', 'POST', {}) + public_post_get_tree_roots = publicPostGetTreeRoots = Entry('get_tree_roots', 'public', 'POST', {}) + public_post_get_swell_percent_points = publicPostGetSwellPercentPoints = Entry('get_swell_percent_points', 'public', 'POST', {}) + public_post_get_vault_assets = publicPostGetVaultAssets = Entry('get_vault_assets', 'public', 'POST', {}) + public_post_get_etherfi_effective_balances = publicPostGetEtherfiEffectiveBalances = Entry('get_etherfi_effective_balances', 'public', 'POST', {}) + public_post_get_kelp_effective_balances = publicPostGetKelpEffectiveBalances = Entry('get_kelp_effective_balances', 'public', 'POST', {}) + public_post_get_bridge_balances = publicPostGetBridgeBalances = Entry('get_bridge_balances', 'public', 'POST', {}) + public_post_get_ethena_participants = publicPostGetEthenaParticipants = Entry('get_ethena_participants', 'public', 'POST', {}) + public_post_get_vault_share = publicPostGetVaultShare = Entry('get_vault_share', 'public', 'POST', {}) + public_post_get_vault_statistics = publicPostGetVaultStatistics = Entry('get_vault_statistics', 'public', 'POST', {}) + public_post_get_vault_balances = publicPostGetVaultBalances = Entry('get_vault_balances', 'public', 'POST', {}) + public_post_estimate_integrator_points = publicPostEstimateIntegratorPoints = Entry('estimate_integrator_points', 'public', 'POST', {}) + public_post_create_subaccount_debug = publicPostCreateSubaccountDebug = Entry('create_subaccount_debug', 'public', 'POST', {}) + public_post_deposit_debug = publicPostDepositDebug = Entry('deposit_debug', 'public', 'POST', {}) + public_post_withdraw_debug = publicPostWithdrawDebug = Entry('withdraw_debug', 'public', 'POST', {}) + public_post_send_quote_debug = publicPostSendQuoteDebug = Entry('send_quote_debug', 'public', 'POST', {}) + public_post_execute_quote_debug = publicPostExecuteQuoteDebug = Entry('execute_quote_debug', 'public', 'POST', {}) + public_post_get_invite_code = publicPostGetInviteCode = Entry('get_invite_code', 'public', 'POST', {}) + public_post_register_invite = publicPostRegisterInvite = Entry('register_invite', 'public', 'POST', {}) + public_post_get_time = publicPostGetTime = Entry('get_time', 'public', 'POST', {}) + public_post_get_live_incidents = publicPostGetLiveIncidents = Entry('get_live_incidents', 'public', 'POST', {}) + public_post_get_maker_programs = publicPostGetMakerPrograms = Entry('get_maker_programs', 'public', 'POST', {}) + public_post_get_maker_program_scores = publicPostGetMakerProgramScores = Entry('get_maker_program_scores', 'public', 'POST', {}) + private_post_get_account = privatePostGetAccount = Entry('get_account', 'private', 'POST', {}) + private_post_create_subaccount = privatePostCreateSubaccount = Entry('create_subaccount', 'private', 'POST', {}) + private_post_get_subaccount = privatePostGetSubaccount = Entry('get_subaccount', 'private', 'POST', {}) + private_post_get_subaccounts = privatePostGetSubaccounts = Entry('get_subaccounts', 'private', 'POST', {}) + private_post_get_all_portfolios = privatePostGetAllPortfolios = Entry('get_all_portfolios', 'private', 'POST', {}) + private_post_change_subaccount_label = privatePostChangeSubaccountLabel = Entry('change_subaccount_label', 'private', 'POST', {}) + private_post_get_notificationsv = privatePostGetNotificationsv = Entry('get_notificationsv', 'private', 'POST', {}) + private_post_update_notifications = privatePostUpdateNotifications = Entry('update_notifications', 'private', 'POST', {}) + private_post_deposit = privatePostDeposit = Entry('deposit', 'private', 'POST', {}) + private_post_withdraw = privatePostWithdraw = Entry('withdraw', 'private', 'POST', {}) + private_post_transfer_erc20 = privatePostTransferErc20 = Entry('transfer_erc20', 'private', 'POST', {}) + private_post_transfer_position = privatePostTransferPosition = Entry('transfer_position', 'private', 'POST', {}) + private_post_transfer_positions = privatePostTransferPositions = Entry('transfer_positions', 'private', 'POST', {}) + private_post_order = privatePostOrder = Entry('order', 'private', 'POST', {}) + private_post_replace = privatePostReplace = Entry('replace', 'private', 'POST', {}) + private_post_order_debug = privatePostOrderDebug = Entry('order_debug', 'private', 'POST', {}) + private_post_get_order = privatePostGetOrder = Entry('get_order', 'private', 'POST', {}) + private_post_get_orders = privatePostGetOrders = Entry('get_orders', 'private', 'POST', {}) + private_post_get_open_orders = privatePostGetOpenOrders = Entry('get_open_orders', 'private', 'POST', {}) + private_post_cancel = privatePostCancel = Entry('cancel', 'private', 'POST', {}) + private_post_cancel_by_label = privatePostCancelByLabel = Entry('cancel_by_label', 'private', 'POST', {}) + private_post_cancel_by_nonce = privatePostCancelByNonce = Entry('cancel_by_nonce', 'private', 'POST', {}) + private_post_cancel_by_instrument = privatePostCancelByInstrument = Entry('cancel_by_instrument', 'private', 'POST', {}) + private_post_cancel_all = privatePostCancelAll = Entry('cancel_all', 'private', 'POST', {}) + private_post_cancel_trigger_order = privatePostCancelTriggerOrder = Entry('cancel_trigger_order', 'private', 'POST', {}) + private_post_get_order_history = privatePostGetOrderHistory = Entry('get_order_history', 'private', 'POST', {}) + private_post_get_trade_history = privatePostGetTradeHistory = Entry('get_trade_history', 'private', 'POST', {}) + private_post_get_deposit_history = privatePostGetDepositHistory = Entry('get_deposit_history', 'private', 'POST', {}) + private_post_get_withdrawal_history = privatePostGetWithdrawalHistory = Entry('get_withdrawal_history', 'private', 'POST', {}) + private_post_send_rfq = privatePostSendRfq = Entry('send_rfq', 'private', 'POST', {}) + private_post_cancel_rfq = privatePostCancelRfq = Entry('cancel_rfq', 'private', 'POST', {}) + private_post_cancel_batch_rfqs = privatePostCancelBatchRfqs = Entry('cancel_batch_rfqs', 'private', 'POST', {}) + private_post_get_rfqs = privatePostGetRfqs = Entry('get_rfqs', 'private', 'POST', {}) + private_post_poll_rfqs = privatePostPollRfqs = Entry('poll_rfqs', 'private', 'POST', {}) + private_post_send_quote = privatePostSendQuote = Entry('send_quote', 'private', 'POST', {}) + private_post_cancel_quote = privatePostCancelQuote = Entry('cancel_quote', 'private', 'POST', {}) + private_post_cancel_batch_quotes = privatePostCancelBatchQuotes = Entry('cancel_batch_quotes', 'private', 'POST', {}) + private_post_get_quotes = privatePostGetQuotes = Entry('get_quotes', 'private', 'POST', {}) + private_post_poll_quotes = privatePostPollQuotes = Entry('poll_quotes', 'private', 'POST', {}) + private_post_execute_quote = privatePostExecuteQuote = Entry('execute_quote', 'private', 'POST', {}) + private_post_rfq_get_best_quote = privatePostRfqGetBestQuote = Entry('rfq_get_best_quote', 'private', 'POST', {}) + private_post_get_margin = privatePostGetMargin = Entry('get_margin', 'private', 'POST', {}) + private_post_get_collaterals = privatePostGetCollaterals = Entry('get_collaterals', 'private', 'POST', {}) + private_post_get_positions = privatePostGetPositions = Entry('get_positions', 'private', 'POST', {}) + private_post_get_option_settlement_history = privatePostGetOptionSettlementHistory = Entry('get_option_settlement_history', 'private', 'POST', {}) + private_post_get_subaccount_value_history = privatePostGetSubaccountValueHistory = Entry('get_subaccount_value_history', 'private', 'POST', {}) + private_post_expired_and_cancelled_history = privatePostExpiredAndCancelledHistory = Entry('expired_and_cancelled_history', 'private', 'POST', {}) + private_post_get_funding_history = privatePostGetFundingHistory = Entry('get_funding_history', 'private', 'POST', {}) + private_post_get_interest_history = privatePostGetInterestHistory = Entry('get_interest_history', 'private', 'POST', {}) + private_post_get_erc20_transfer_history = privatePostGetErc20TransferHistory = Entry('get_erc20_transfer_history', 'private', 'POST', {}) + private_post_get_liquidation_history = privatePostGetLiquidationHistory = Entry('get_liquidation_history', 'private', 'POST', {}) + private_post_liquidate = privatePostLiquidate = Entry('liquidate', 'private', 'POST', {}) + private_post_get_liquidator_history = privatePostGetLiquidatorHistory = Entry('get_liquidator_history', 'private', 'POST', {}) + private_post_session_keys = privatePostSessionKeys = Entry('session_keys', 'private', 'POST', {}) + private_post_edit_session_key = privatePostEditSessionKey = Entry('edit_session_key', 'private', 'POST', {}) + private_post_register_scoped_session_key = privatePostRegisterScopedSessionKey = Entry('register_scoped_session_key', 'private', 'POST', {}) + private_post_get_mmp_config = privatePostGetMmpConfig = Entry('get_mmp_config', 'private', 'POST', {}) + private_post_set_mmp_config = privatePostSetMmpConfig = Entry('set_mmp_config', 'private', 'POST', {}) + private_post_reset_mmp = privatePostResetMmp = Entry('reset_mmp', 'private', 'POST', {}) + private_post_set_cancel_on_disconnect = privatePostSetCancelOnDisconnect = Entry('set_cancel_on_disconnect', 'private', 'POST', {}) + private_post_get_invite_code = privatePostGetInviteCode = Entry('get_invite_code', 'private', 'POST', {}) + private_post_register_invite = privatePostRegisterInvite = Entry('register_invite', 'private', 'POST', {}) diff --git a/ccxt/abstract/digifinex.py b/ccxt/abstract/digifinex.py new file mode 100644 index 0000000..25be140 --- /dev/null +++ b/ccxt/abstract/digifinex.py @@ -0,0 +1,92 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_spot_get_market_symbols = publicSpotGetMarketSymbols = Entry('{market}/symbols', ['public', 'spot'], 'GET', {}) + public_spot_get_kline = publicSpotGetKline = Entry('kline', ['public', 'spot'], 'GET', {}) + public_spot_get_margin_currencies = publicSpotGetMarginCurrencies = Entry('margin/currencies', ['public', 'spot'], 'GET', {}) + public_spot_get_margin_symbols = publicSpotGetMarginSymbols = Entry('margin/symbols', ['public', 'spot'], 'GET', {}) + public_spot_get_markets = publicSpotGetMarkets = Entry('markets', ['public', 'spot'], 'GET', {}) + public_spot_get_order_book = publicSpotGetOrderBook = Entry('order_book', ['public', 'spot'], 'GET', {}) + public_spot_get_ping = publicSpotGetPing = Entry('ping', ['public', 'spot'], 'GET', {}) + public_spot_get_spot_symbols = publicSpotGetSpotSymbols = Entry('spot/symbols', ['public', 'spot'], 'GET', {}) + public_spot_get_time = publicSpotGetTime = Entry('time', ['public', 'spot'], 'GET', {}) + public_spot_get_trades = publicSpotGetTrades = Entry('trades', ['public', 'spot'], 'GET', {}) + public_spot_get_trades_symbols = publicSpotGetTradesSymbols = Entry('trades/symbols', ['public', 'spot'], 'GET', {}) + public_spot_get_ticker = publicSpotGetTicker = Entry('ticker', ['public', 'spot'], 'GET', {}) + public_spot_get_currencies = publicSpotGetCurrencies = Entry('currencies', ['public', 'spot'], 'GET', {}) + public_swap_get_public_api_weight = publicSwapGetPublicApiWeight = Entry('public/api_weight', ['public', 'swap'], 'GET', {}) + public_swap_get_public_candles = publicSwapGetPublicCandles = Entry('public/candles', ['public', 'swap'], 'GET', {}) + public_swap_get_public_candles_history = publicSwapGetPublicCandlesHistory = Entry('public/candles_history', ['public', 'swap'], 'GET', {}) + public_swap_get_public_depth = publicSwapGetPublicDepth = Entry('public/depth', ['public', 'swap'], 'GET', {}) + public_swap_get_public_funding_rate = publicSwapGetPublicFundingRate = Entry('public/funding_rate', ['public', 'swap'], 'GET', {}) + public_swap_get_public_funding_rate_history = publicSwapGetPublicFundingRateHistory = Entry('public/funding_rate_history', ['public', 'swap'], 'GET', {}) + public_swap_get_public_instrument = publicSwapGetPublicInstrument = Entry('public/instrument', ['public', 'swap'], 'GET', {}) + public_swap_get_public_instruments = publicSwapGetPublicInstruments = Entry('public/instruments', ['public', 'swap'], 'GET', {}) + public_swap_get_public_ticker = publicSwapGetPublicTicker = Entry('public/ticker', ['public', 'swap'], 'GET', {}) + public_swap_get_public_tickers = publicSwapGetPublicTickers = Entry('public/tickers', ['public', 'swap'], 'GET', {}) + public_swap_get_public_time = publicSwapGetPublicTime = Entry('public/time', ['public', 'swap'], 'GET', {}) + public_swap_get_public_trades = publicSwapGetPublicTrades = Entry('public/trades', ['public', 'swap'], 'GET', {}) + private_spot_get_market_financelog = privateSpotGetMarketFinancelog = Entry('{market}/financelog', ['private', 'spot'], 'GET', {}) + private_spot_get_market_mytrades = privateSpotGetMarketMytrades = Entry('{market}/mytrades', ['private', 'spot'], 'GET', {}) + private_spot_get_market_order = privateSpotGetMarketOrder = Entry('{market}/order', ['private', 'spot'], 'GET', {}) + private_spot_get_market_order_detail = privateSpotGetMarketOrderDetail = Entry('{market}/order/detail', ['private', 'spot'], 'GET', {}) + private_spot_get_market_order_current = privateSpotGetMarketOrderCurrent = Entry('{market}/order/current', ['private', 'spot'], 'GET', {}) + private_spot_get_market_order_history = privateSpotGetMarketOrderHistory = Entry('{market}/order/history', ['private', 'spot'], 'GET', {}) + private_spot_get_margin_assets = privateSpotGetMarginAssets = Entry('margin/assets', ['private', 'spot'], 'GET', {}) + private_spot_get_margin_financelog = privateSpotGetMarginFinancelog = Entry('margin/financelog', ['private', 'spot'], 'GET', {}) + private_spot_get_margin_mytrades = privateSpotGetMarginMytrades = Entry('margin/mytrades', ['private', 'spot'], 'GET', {}) + private_spot_get_margin_order = privateSpotGetMarginOrder = Entry('margin/order', ['private', 'spot'], 'GET', {}) + private_spot_get_margin_order_current = privateSpotGetMarginOrderCurrent = Entry('margin/order/current', ['private', 'spot'], 'GET', {}) + private_spot_get_margin_order_history = privateSpotGetMarginOrderHistory = Entry('margin/order/history', ['private', 'spot'], 'GET', {}) + private_spot_get_margin_positions = privateSpotGetMarginPositions = Entry('margin/positions', ['private', 'spot'], 'GET', {}) + private_spot_get_otc_financelog = privateSpotGetOtcFinancelog = Entry('otc/financelog', ['private', 'spot'], 'GET', {}) + private_spot_get_spot_assets = privateSpotGetSpotAssets = Entry('spot/assets', ['private', 'spot'], 'GET', {}) + private_spot_get_spot_financelog = privateSpotGetSpotFinancelog = Entry('spot/financelog', ['private', 'spot'], 'GET', {}) + private_spot_get_spot_mytrades = privateSpotGetSpotMytrades = Entry('spot/mytrades', ['private', 'spot'], 'GET', {}) + private_spot_get_spot_order = privateSpotGetSpotOrder = Entry('spot/order', ['private', 'spot'], 'GET', {}) + private_spot_get_spot_order_current = privateSpotGetSpotOrderCurrent = Entry('spot/order/current', ['private', 'spot'], 'GET', {}) + private_spot_get_spot_order_history = privateSpotGetSpotOrderHistory = Entry('spot/order/history', ['private', 'spot'], 'GET', {}) + private_spot_get_deposit_address = privateSpotGetDepositAddress = Entry('deposit/address', ['private', 'spot'], 'GET', {}) + private_spot_get_deposit_history = privateSpotGetDepositHistory = Entry('deposit/history', ['private', 'spot'], 'GET', {}) + private_spot_get_withdraw_history = privateSpotGetWithdrawHistory = Entry('withdraw/history', ['private', 'spot'], 'GET', {}) + private_spot_post_market_order_cancel = privateSpotPostMarketOrderCancel = Entry('{market}/order/cancel', ['private', 'spot'], 'POST', {}) + private_spot_post_market_order_new = privateSpotPostMarketOrderNew = Entry('{market}/order/new', ['private', 'spot'], 'POST', {}) + private_spot_post_market_order_batch_new = privateSpotPostMarketOrderBatchNew = Entry('{market}/order/batch_new', ['private', 'spot'], 'POST', {}) + private_spot_post_margin_order_cancel = privateSpotPostMarginOrderCancel = Entry('margin/order/cancel', ['private', 'spot'], 'POST', {}) + private_spot_post_margin_order_new = privateSpotPostMarginOrderNew = Entry('margin/order/new', ['private', 'spot'], 'POST', {}) + private_spot_post_margin_position_close = privateSpotPostMarginPositionClose = Entry('margin/position/close', ['private', 'spot'], 'POST', {}) + private_spot_post_spot_order_cancel = privateSpotPostSpotOrderCancel = Entry('spot/order/cancel', ['private', 'spot'], 'POST', {}) + private_spot_post_spot_order_new = privateSpotPostSpotOrderNew = Entry('spot/order/new', ['private', 'spot'], 'POST', {}) + private_spot_post_transfer = privateSpotPostTransfer = Entry('transfer', ['private', 'spot'], 'POST', {}) + private_spot_post_withdraw_new = privateSpotPostWithdrawNew = Entry('withdraw/new', ['private', 'spot'], 'POST', {}) + private_spot_post_withdraw_cancel = privateSpotPostWithdrawCancel = Entry('withdraw/cancel', ['private', 'spot'], 'POST', {}) + private_swap_get_account_balance = privateSwapGetAccountBalance = Entry('account/balance', ['private', 'swap'], 'GET', {}) + private_swap_get_account_positions = privateSwapGetAccountPositions = Entry('account/positions', ['private', 'swap'], 'GET', {}) + private_swap_get_account_finance_record = privateSwapGetAccountFinanceRecord = Entry('account/finance_record', ['private', 'swap'], 'GET', {}) + private_swap_get_account_trading_fee_rate = privateSwapGetAccountTradingFeeRate = Entry('account/trading_fee_rate', ['private', 'swap'], 'GET', {}) + private_swap_get_account_transfer_record = privateSwapGetAccountTransferRecord = Entry('account/transfer_record', ['private', 'swap'], 'GET', {}) + private_swap_get_account_funding_fee = privateSwapGetAccountFundingFee = Entry('account/funding_fee', ['private', 'swap'], 'GET', {}) + private_swap_get_trade_history_orders = privateSwapGetTradeHistoryOrders = Entry('trade/history_orders', ['private', 'swap'], 'GET', {}) + private_swap_get_trade_history_trades = privateSwapGetTradeHistoryTrades = Entry('trade/history_trades', ['private', 'swap'], 'GET', {}) + private_swap_get_trade_open_orders = privateSwapGetTradeOpenOrders = Entry('trade/open_orders', ['private', 'swap'], 'GET', {}) + private_swap_get_trade_order_info = privateSwapGetTradeOrderInfo = Entry('trade/order_info', ['private', 'swap'], 'GET', {}) + private_swap_post_account_transfer = privateSwapPostAccountTransfer = Entry('account/transfer', ['private', 'swap'], 'POST', {}) + private_swap_post_account_leverage = privateSwapPostAccountLeverage = Entry('account/leverage', ['private', 'swap'], 'POST', {}) + private_swap_post_account_position_mode = privateSwapPostAccountPositionMode = Entry('account/position_mode', ['private', 'swap'], 'POST', {}) + private_swap_post_account_position_margin = privateSwapPostAccountPositionMargin = Entry('account/position_margin', ['private', 'swap'], 'POST', {}) + private_swap_post_trade_batch_cancel_order = privateSwapPostTradeBatchCancelOrder = Entry('trade/batch_cancel_order', ['private', 'swap'], 'POST', {}) + private_swap_post_trade_batch_order = privateSwapPostTradeBatchOrder = Entry('trade/batch_order', ['private', 'swap'], 'POST', {}) + private_swap_post_trade_cancel_order = privateSwapPostTradeCancelOrder = Entry('trade/cancel_order', ['private', 'swap'], 'POST', {}) + private_swap_post_trade_order_place = privateSwapPostTradeOrderPlace = Entry('trade/order_place', ['private', 'swap'], 'POST', {}) + private_swap_post_follow_sponsor_order = privateSwapPostFollowSponsorOrder = Entry('follow/sponsor_order', ['private', 'swap'], 'POST', {}) + private_swap_post_follow_close_order = privateSwapPostFollowCloseOrder = Entry('follow/close_order', ['private', 'swap'], 'POST', {}) + private_swap_post_follow_cancel_order = privateSwapPostFollowCancelOrder = Entry('follow/cancel_order', ['private', 'swap'], 'POST', {}) + private_swap_post_follow_user_center_current = privateSwapPostFollowUserCenterCurrent = Entry('follow/user_center_current', ['private', 'swap'], 'POST', {}) + private_swap_post_follow_user_center_history = privateSwapPostFollowUserCenterHistory = Entry('follow/user_center_history', ['private', 'swap'], 'POST', {}) + private_swap_post_follow_expert_current_open_order = privateSwapPostFollowExpertCurrentOpenOrder = Entry('follow/expert_current_open_order', ['private', 'swap'], 'POST', {}) + private_swap_post_follow_add_algo = privateSwapPostFollowAddAlgo = Entry('follow/add_algo', ['private', 'swap'], 'POST', {}) + private_swap_post_follow_cancel_algo = privateSwapPostFollowCancelAlgo = Entry('follow/cancel_algo', ['private', 'swap'], 'POST', {}) + private_swap_post_follow_account_available = privateSwapPostFollowAccountAvailable = Entry('follow/account_available', ['private', 'swap'], 'POST', {}) + private_swap_post_follow_plan_task = privateSwapPostFollowPlanTask = Entry('follow/plan_task', ['private', 'swap'], 'POST', {}) + private_swap_post_follow_instrument_list = privateSwapPostFollowInstrumentList = Entry('follow/instrument_list', ['private', 'swap'], 'POST', {}) diff --git a/ccxt/abstract/exmo.py b/ccxt/abstract/exmo.py new file mode 100644 index 0000000..8bed6bd --- /dev/null +++ b/ccxt/abstract/exmo.py @@ -0,0 +1,55 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + web_get_ctrl_feesandlimits = webGetCtrlFeesAndLimits = Entry('ctrl/feesAndLimits', 'web', 'GET', {}) + web_get_en_docs_fees = webGetEnDocsFees = Entry('en/docs/fees', 'web', 'GET', {}) + public_get_currency = publicGetCurrency = Entry('currency', 'public', 'GET', {}) + public_get_currency_list_extended = publicGetCurrencyListExtended = Entry('currency/list/extended', 'public', 'GET', {}) + public_get_order_book = publicGetOrderBook = Entry('order_book', 'public', 'GET', {}) + public_get_pair_settings = publicGetPairSettings = Entry('pair_settings', 'public', 'GET', {}) + public_get_ticker = publicGetTicker = Entry('ticker', 'public', 'GET', {}) + public_get_trades = publicGetTrades = Entry('trades', 'public', 'GET', {}) + public_get_candles_history = publicGetCandlesHistory = Entry('candles_history', 'public', 'GET', {}) + public_get_required_amount = publicGetRequiredAmount = Entry('required_amount', 'public', 'GET', {}) + public_get_payments_providers_crypto_list = publicGetPaymentsProvidersCryptoList = Entry('payments/providers/crypto/list', 'public', 'GET', {}) + private_post_user_info = privatePostUserInfo = Entry('user_info', 'private', 'POST', {}) + private_post_order_create = privatePostOrderCreate = Entry('order_create', 'private', 'POST', {}) + private_post_order_cancel = privatePostOrderCancel = Entry('order_cancel', 'private', 'POST', {}) + private_post_stop_market_order_create = privatePostStopMarketOrderCreate = Entry('stop_market_order_create', 'private', 'POST', {}) + private_post_stop_market_order_cancel = privatePostStopMarketOrderCancel = Entry('stop_market_order_cancel', 'private', 'POST', {}) + private_post_user_open_orders = privatePostUserOpenOrders = Entry('user_open_orders', 'private', 'POST', {}) + private_post_user_trades = privatePostUserTrades = Entry('user_trades', 'private', 'POST', {}) + private_post_user_cancelled_orders = privatePostUserCancelledOrders = Entry('user_cancelled_orders', 'private', 'POST', {}) + private_post_order_trades = privatePostOrderTrades = Entry('order_trades', 'private', 'POST', {}) + private_post_deposit_address = privatePostDepositAddress = Entry('deposit_address', 'private', 'POST', {}) + private_post_withdraw_crypt = privatePostWithdrawCrypt = Entry('withdraw_crypt', 'private', 'POST', {}) + private_post_withdraw_get_txid = privatePostWithdrawGetTxid = Entry('withdraw_get_txid', 'private', 'POST', {}) + private_post_excode_create = privatePostExcodeCreate = Entry('excode_create', 'private', 'POST', {}) + private_post_excode_load = privatePostExcodeLoad = Entry('excode_load', 'private', 'POST', {}) + private_post_code_check = privatePostCodeCheck = Entry('code_check', 'private', 'POST', {}) + private_post_wallet_history = privatePostWalletHistory = Entry('wallet_history', 'private', 'POST', {}) + private_post_wallet_operations = privatePostWalletOperations = Entry('wallet_operations', 'private', 'POST', {}) + private_post_margin_user_order_create = privatePostMarginUserOrderCreate = Entry('margin/user/order/create', 'private', 'POST', {}) + private_post_margin_user_order_update = privatePostMarginUserOrderUpdate = Entry('margin/user/order/update', 'private', 'POST', {}) + private_post_margin_user_order_cancel = privatePostMarginUserOrderCancel = Entry('margin/user/order/cancel', 'private', 'POST', {}) + private_post_margin_user_position_close = privatePostMarginUserPositionClose = Entry('margin/user/position/close', 'private', 'POST', {}) + private_post_margin_user_position_margin_add = privatePostMarginUserPositionMarginAdd = Entry('margin/user/position/margin_add', 'private', 'POST', {}) + private_post_margin_user_position_margin_remove = privatePostMarginUserPositionMarginRemove = Entry('margin/user/position/margin_remove', 'private', 'POST', {}) + private_post_margin_currency_list = privatePostMarginCurrencyList = Entry('margin/currency/list', 'private', 'POST', {}) + private_post_margin_pair_list = privatePostMarginPairList = Entry('margin/pair/list', 'private', 'POST', {}) + private_post_margin_settings = privatePostMarginSettings = Entry('margin/settings', 'private', 'POST', {}) + private_post_margin_funding_list = privatePostMarginFundingList = Entry('margin/funding/list', 'private', 'POST', {}) + private_post_margin_user_info = privatePostMarginUserInfo = Entry('margin/user/info', 'private', 'POST', {}) + private_post_margin_user_order_list = privatePostMarginUserOrderList = Entry('margin/user/order/list', 'private', 'POST', {}) + private_post_margin_user_order_history = privatePostMarginUserOrderHistory = Entry('margin/user/order/history', 'private', 'POST', {}) + private_post_margin_user_order_trades = privatePostMarginUserOrderTrades = Entry('margin/user/order/trades', 'private', 'POST', {}) + private_post_margin_user_order_max_quantity = privatePostMarginUserOrderMaxQuantity = Entry('margin/user/order/max_quantity', 'private', 'POST', {}) + private_post_margin_user_position_list = privatePostMarginUserPositionList = Entry('margin/user/position/list', 'private', 'POST', {}) + private_post_margin_user_position_margin_remove_info = privatePostMarginUserPositionMarginRemoveInfo = Entry('margin/user/position/margin_remove_info', 'private', 'POST', {}) + private_post_margin_user_position_margin_add_info = privatePostMarginUserPositionMarginAddInfo = Entry('margin/user/position/margin_add_info', 'private', 'POST', {}) + private_post_margin_user_wallet_list = privatePostMarginUserWalletList = Entry('margin/user/wallet/list', 'private', 'POST', {}) + private_post_margin_user_wallet_history = privatePostMarginUserWalletHistory = Entry('margin/user/wallet/history', 'private', 'POST', {}) + private_post_margin_user_trade_list = privatePostMarginUserTradeList = Entry('margin/user/trade/list', 'private', 'POST', {}) + private_post_margin_trades = privatePostMarginTrades = Entry('margin/trades', 'private', 'POST', {}) + private_post_margin_liquidation_feed = privatePostMarginLiquidationFeed = Entry('margin/liquidation/feed', 'private', 'POST', {}) diff --git a/ccxt/abstract/fmfwio.py b/ccxt/abstract/fmfwio.py new file mode 100644 index 0000000..9507bfd --- /dev/null +++ b/ccxt/abstract/fmfwio.py @@ -0,0 +1,115 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_public_currency = publicGetPublicCurrency = Entry('public/currency', 'public', 'GET', {'cost': 10}) + public_get_public_currency_currency = publicGetPublicCurrencyCurrency = Entry('public/currency/{currency}', 'public', 'GET', {'cost': 10}) + public_get_public_symbol = publicGetPublicSymbol = Entry('public/symbol', 'public', 'GET', {'cost': 10}) + public_get_public_symbol_symbol = publicGetPublicSymbolSymbol = Entry('public/symbol/{symbol}', 'public', 'GET', {'cost': 10}) + public_get_public_ticker = publicGetPublicTicker = Entry('public/ticker', 'public', 'GET', {'cost': 10}) + public_get_public_ticker_symbol = publicGetPublicTickerSymbol = Entry('public/ticker/{symbol}', 'public', 'GET', {'cost': 10}) + public_get_public_price_rate = publicGetPublicPriceRate = Entry('public/price/rate', 'public', 'GET', {'cost': 10}) + public_get_public_price_history = publicGetPublicPriceHistory = Entry('public/price/history', 'public', 'GET', {'cost': 10}) + public_get_public_price_ticker = publicGetPublicPriceTicker = Entry('public/price/ticker', 'public', 'GET', {'cost': 10}) + public_get_public_price_ticker_symbol = publicGetPublicPriceTickerSymbol = Entry('public/price/ticker/{symbol}', 'public', 'GET', {'cost': 10}) + public_get_public_trades = publicGetPublicTrades = Entry('public/trades', 'public', 'GET', {'cost': 10}) + public_get_public_trades_symbol = publicGetPublicTradesSymbol = Entry('public/trades/{symbol}', 'public', 'GET', {'cost': 10}) + public_get_public_orderbook = publicGetPublicOrderbook = Entry('public/orderbook', 'public', 'GET', {'cost': 10}) + public_get_public_orderbook_symbol = publicGetPublicOrderbookSymbol = Entry('public/orderbook/{symbol}', 'public', 'GET', {'cost': 10}) + public_get_public_candles = publicGetPublicCandles = Entry('public/candles', 'public', 'GET', {'cost': 10}) + public_get_public_candles_symbol = publicGetPublicCandlesSymbol = Entry('public/candles/{symbol}', 'public', 'GET', {'cost': 10}) + public_get_public_converted_candles = publicGetPublicConvertedCandles = Entry('public/converted/candles', 'public', 'GET', {'cost': 10}) + public_get_public_converted_candles_symbol = publicGetPublicConvertedCandlesSymbol = Entry('public/converted/candles/{symbol}', 'public', 'GET', {'cost': 10}) + public_get_public_futures_info = publicGetPublicFuturesInfo = Entry('public/futures/info', 'public', 'GET', {'cost': 10}) + public_get_public_futures_info_symbol = publicGetPublicFuturesInfoSymbol = Entry('public/futures/info/{symbol}', 'public', 'GET', {'cost': 10}) + public_get_public_futures_history_funding = publicGetPublicFuturesHistoryFunding = Entry('public/futures/history/funding', 'public', 'GET', {'cost': 10}) + public_get_public_futures_history_funding_symbol = publicGetPublicFuturesHistoryFundingSymbol = Entry('public/futures/history/funding/{symbol}', 'public', 'GET', {'cost': 10}) + public_get_public_futures_candles_index_price = publicGetPublicFuturesCandlesIndexPrice = Entry('public/futures/candles/index_price', 'public', 'GET', {'cost': 10}) + public_get_public_futures_candles_index_price_symbol = publicGetPublicFuturesCandlesIndexPriceSymbol = Entry('public/futures/candles/index_price/{symbol}', 'public', 'GET', {'cost': 10}) + public_get_public_futures_candles_mark_price = publicGetPublicFuturesCandlesMarkPrice = Entry('public/futures/candles/mark_price', 'public', 'GET', {'cost': 10}) + public_get_public_futures_candles_mark_price_symbol = publicGetPublicFuturesCandlesMarkPriceSymbol = Entry('public/futures/candles/mark_price/{symbol}', 'public', 'GET', {'cost': 10}) + public_get_public_futures_candles_premium_index = publicGetPublicFuturesCandlesPremiumIndex = Entry('public/futures/candles/premium_index', 'public', 'GET', {'cost': 10}) + public_get_public_futures_candles_premium_index_symbol = publicGetPublicFuturesCandlesPremiumIndexSymbol = Entry('public/futures/candles/premium_index/{symbol}', 'public', 'GET', {'cost': 10}) + public_get_public_futures_candles_open_interest = publicGetPublicFuturesCandlesOpenInterest = Entry('public/futures/candles/open_interest', 'public', 'GET', {'cost': 10}) + public_get_public_futures_candles_open_interest_symbol = publicGetPublicFuturesCandlesOpenInterestSymbol = Entry('public/futures/candles/open_interest/{symbol}', 'public', 'GET', {'cost': 10}) + private_get_spot_balance = privateGetSpotBalance = Entry('spot/balance', 'private', 'GET', {'cost': 15}) + private_get_spot_balance_currency = privateGetSpotBalanceCurrency = Entry('spot/balance/{currency}', 'private', 'GET', {'cost': 15}) + private_get_spot_order = privateGetSpotOrder = Entry('spot/order', 'private', 'GET', {'cost': 1}) + private_get_spot_order_client_order_id = privateGetSpotOrderClientOrderId = Entry('spot/order/{client_order_id}', 'private', 'GET', {'cost': 1}) + private_get_spot_fee = privateGetSpotFee = Entry('spot/fee', 'private', 'GET', {'cost': 15}) + private_get_spot_fee_symbol = privateGetSpotFeeSymbol = Entry('spot/fee/{symbol}', 'private', 'GET', {'cost': 15}) + private_get_spot_history_order = privateGetSpotHistoryOrder = Entry('spot/history/order', 'private', 'GET', {'cost': 15}) + private_get_spot_history_trade = privateGetSpotHistoryTrade = Entry('spot/history/trade', 'private', 'GET', {'cost': 15}) + private_get_margin_account = privateGetMarginAccount = Entry('margin/account', 'private', 'GET', {'cost': 1}) + private_get_margin_account_isolated_symbol = privateGetMarginAccountIsolatedSymbol = Entry('margin/account/isolated/{symbol}', 'private', 'GET', {'cost': 1}) + private_get_margin_account_cross_currency = privateGetMarginAccountCrossCurrency = Entry('margin/account/cross/{currency}', 'private', 'GET', {'cost': 1}) + private_get_margin_order = privateGetMarginOrder = Entry('margin/order', 'private', 'GET', {'cost': 1}) + private_get_margin_order_client_order_id = privateGetMarginOrderClientOrderId = Entry('margin/order/{client_order_id}', 'private', 'GET', {'cost': 1}) + private_get_margin_config = privateGetMarginConfig = Entry('margin/config', 'private', 'GET', {'cost': 15}) + private_get_margin_history_order = privateGetMarginHistoryOrder = Entry('margin/history/order', 'private', 'GET', {'cost': 15}) + private_get_margin_history_trade = privateGetMarginHistoryTrade = Entry('margin/history/trade', 'private', 'GET', {'cost': 15}) + private_get_margin_history_positions = privateGetMarginHistoryPositions = Entry('margin/history/positions', 'private', 'GET', {'cost': 15}) + private_get_margin_history_clearing = privateGetMarginHistoryClearing = Entry('margin/history/clearing', 'private', 'GET', {'cost': 15}) + private_get_futures_balance = privateGetFuturesBalance = Entry('futures/balance', 'private', 'GET', {'cost': 15}) + private_get_futures_balance_currency = privateGetFuturesBalanceCurrency = Entry('futures/balance/{currency}', 'private', 'GET', {'cost': 15}) + private_get_futures_account = privateGetFuturesAccount = Entry('futures/account', 'private', 'GET', {'cost': 1}) + private_get_futures_account_isolated_symbol = privateGetFuturesAccountIsolatedSymbol = Entry('futures/account/isolated/{symbol}', 'private', 'GET', {'cost': 1}) + private_get_futures_order = privateGetFuturesOrder = Entry('futures/order', 'private', 'GET', {'cost': 1}) + private_get_futures_order_client_order_id = privateGetFuturesOrderClientOrderId = Entry('futures/order/{client_order_id}', 'private', 'GET', {'cost': 1}) + private_get_futures_config = privateGetFuturesConfig = Entry('futures/config', 'private', 'GET', {'cost': 15}) + private_get_futures_fee = privateGetFuturesFee = Entry('futures/fee', 'private', 'GET', {'cost': 15}) + private_get_futures_fee_symbol = privateGetFuturesFeeSymbol = Entry('futures/fee/{symbol}', 'private', 'GET', {'cost': 15}) + private_get_futures_history_order = privateGetFuturesHistoryOrder = Entry('futures/history/order', 'private', 'GET', {'cost': 15}) + private_get_futures_history_trade = privateGetFuturesHistoryTrade = Entry('futures/history/trade', 'private', 'GET', {'cost': 15}) + private_get_futures_history_positions = privateGetFuturesHistoryPositions = Entry('futures/history/positions', 'private', 'GET', {'cost': 15}) + private_get_futures_history_clearing = privateGetFuturesHistoryClearing = Entry('futures/history/clearing', 'private', 'GET', {'cost': 15}) + private_get_wallet_balance = privateGetWalletBalance = Entry('wallet/balance', 'private', 'GET', {'cost': 30}) + private_get_wallet_balance_currency = privateGetWalletBalanceCurrency = Entry('wallet/balance/{currency}', 'private', 'GET', {'cost': 30}) + private_get_wallet_crypto_address = privateGetWalletCryptoAddress = Entry('wallet/crypto/address', 'private', 'GET', {'cost': 30}) + private_get_wallet_crypto_address_recent_deposit = privateGetWalletCryptoAddressRecentDeposit = Entry('wallet/crypto/address/recent-deposit', 'private', 'GET', {'cost': 30}) + private_get_wallet_crypto_address_recent_withdraw = privateGetWalletCryptoAddressRecentWithdraw = Entry('wallet/crypto/address/recent-withdraw', 'private', 'GET', {'cost': 30}) + private_get_wallet_crypto_address_check_mine = privateGetWalletCryptoAddressCheckMine = Entry('wallet/crypto/address/check-mine', 'private', 'GET', {'cost': 30}) + private_get_wallet_transactions = privateGetWalletTransactions = Entry('wallet/transactions', 'private', 'GET', {'cost': 30}) + private_get_wallet_transactions_tx_id = privateGetWalletTransactionsTxId = Entry('wallet/transactions/{tx_id}', 'private', 'GET', {'cost': 30}) + private_get_wallet_crypto_fee_estimate = privateGetWalletCryptoFeeEstimate = Entry('wallet/crypto/fee/estimate', 'private', 'GET', {'cost': 30}) + private_get_wallet_airdrops = privateGetWalletAirdrops = Entry('wallet/airdrops', 'private', 'GET', {'cost': 30}) + private_get_wallet_amount_locks = privateGetWalletAmountLocks = Entry('wallet/amount-locks', 'private', 'GET', {'cost': 30}) + private_get_sub_account = privateGetSubAccount = Entry('sub-account', 'private', 'GET', {'cost': 15}) + private_get_sub_account_acl = privateGetSubAccountAcl = Entry('sub-account/acl', 'private', 'GET', {'cost': 15}) + private_get_sub_account_balance_subaccid = privateGetSubAccountBalanceSubAccID = Entry('sub-account/balance/{subAccID}', 'private', 'GET', {'cost': 15}) + private_get_sub_account_crypto_address_subaccid_currency = privateGetSubAccountCryptoAddressSubAccIDCurrency = Entry('sub-account/crypto/address/{subAccID}/{currency}', 'private', 'GET', {'cost': 15}) + private_post_spot_order = privatePostSpotOrder = Entry('spot/order', 'private', 'POST', {'cost': 1}) + private_post_spot_order_list = privatePostSpotOrderList = Entry('spot/order/list', 'private', 'POST', {'cost': 1}) + private_post_margin_order = privatePostMarginOrder = Entry('margin/order', 'private', 'POST', {'cost': 1}) + private_post_margin_order_list = privatePostMarginOrderList = Entry('margin/order/list', 'private', 'POST', {'cost': 1}) + private_post_futures_order = privatePostFuturesOrder = Entry('futures/order', 'private', 'POST', {'cost': 1}) + private_post_futures_order_list = privatePostFuturesOrderList = Entry('futures/order/list', 'private', 'POST', {'cost': 1}) + private_post_wallet_crypto_address = privatePostWalletCryptoAddress = Entry('wallet/crypto/address', 'private', 'POST', {'cost': 30}) + private_post_wallet_crypto_withdraw = privatePostWalletCryptoWithdraw = Entry('wallet/crypto/withdraw', 'private', 'POST', {'cost': 30}) + private_post_wallet_convert = privatePostWalletConvert = Entry('wallet/convert', 'private', 'POST', {'cost': 30}) + private_post_wallet_transfer = privatePostWalletTransfer = Entry('wallet/transfer', 'private', 'POST', {'cost': 30}) + private_post_wallet_internal_withdraw = privatePostWalletInternalWithdraw = Entry('wallet/internal/withdraw', 'private', 'POST', {'cost': 30}) + private_post_wallet_crypto_check_offchain_available = privatePostWalletCryptoCheckOffchainAvailable = Entry('wallet/crypto/check-offchain-available', 'private', 'POST', {'cost': 30}) + private_post_wallet_crypto_fees_estimate = privatePostWalletCryptoFeesEstimate = Entry('wallet/crypto/fees/estimate', 'private', 'POST', {'cost': 30}) + private_post_wallet_airdrops_id_claim = privatePostWalletAirdropsIdClaim = Entry('wallet/airdrops/{id}/claim', 'private', 'POST', {'cost': 30}) + private_post_sub_account_freeze = privatePostSubAccountFreeze = Entry('sub-account/freeze', 'private', 'POST', {'cost': 15}) + private_post_sub_account_activate = privatePostSubAccountActivate = Entry('sub-account/activate', 'private', 'POST', {'cost': 15}) + private_post_sub_account_transfer = privatePostSubAccountTransfer = Entry('sub-account/transfer', 'private', 'POST', {'cost': 15}) + private_post_sub_account_acl = privatePostSubAccountAcl = Entry('sub-account/acl', 'private', 'POST', {'cost': 15}) + private_patch_spot_order_client_order_id = privatePatchSpotOrderClientOrderId = Entry('spot/order/{client_order_id}', 'private', 'PATCH', {'cost': 1}) + private_patch_margin_order_client_order_id = privatePatchMarginOrderClientOrderId = Entry('margin/order/{client_order_id}', 'private', 'PATCH', {'cost': 1}) + private_patch_futures_order_client_order_id = privatePatchFuturesOrderClientOrderId = Entry('futures/order/{client_order_id}', 'private', 'PATCH', {'cost': 1}) + private_delete_spot_order = privateDeleteSpotOrder = Entry('spot/order', 'private', 'DELETE', {'cost': 1}) + private_delete_spot_order_client_order_id = privateDeleteSpotOrderClientOrderId = Entry('spot/order/{client_order_id}', 'private', 'DELETE', {'cost': 1}) + private_delete_margin_position = privateDeleteMarginPosition = Entry('margin/position', 'private', 'DELETE', {'cost': 1}) + private_delete_margin_position_isolated_symbol = privateDeleteMarginPositionIsolatedSymbol = Entry('margin/position/isolated/{symbol}', 'private', 'DELETE', {'cost': 1}) + private_delete_margin_order = privateDeleteMarginOrder = Entry('margin/order', 'private', 'DELETE', {'cost': 1}) + private_delete_margin_order_client_order_id = privateDeleteMarginOrderClientOrderId = Entry('margin/order/{client_order_id}', 'private', 'DELETE', {'cost': 1}) + private_delete_futures_position = privateDeleteFuturesPosition = Entry('futures/position', 'private', 'DELETE', {'cost': 1}) + private_delete_futures_position_margin_mode_symbol = privateDeleteFuturesPositionMarginModeSymbol = Entry('futures/position/{margin_mode}/{symbol}', 'private', 'DELETE', {'cost': 1}) + private_delete_futures_order = privateDeleteFuturesOrder = Entry('futures/order', 'private', 'DELETE', {'cost': 1}) + private_delete_futures_order_client_order_id = privateDeleteFuturesOrderClientOrderId = Entry('futures/order/{client_order_id}', 'private', 'DELETE', {'cost': 1}) + private_delete_wallet_crypto_withdraw_id = privateDeleteWalletCryptoWithdrawId = Entry('wallet/crypto/withdraw/{id}', 'private', 'DELETE', {'cost': 30}) + private_put_margin_account_isolated_symbol = privatePutMarginAccountIsolatedSymbol = Entry('margin/account/isolated/{symbol}', 'private', 'PUT', {'cost': 1}) + private_put_futures_account_isolated_symbol = privatePutFuturesAccountIsolatedSymbol = Entry('futures/account/isolated/{symbol}', 'private', 'PUT', {'cost': 1}) + private_put_wallet_crypto_withdraw_id = privatePutWalletCryptoWithdrawId = Entry('wallet/crypto/withdraw/{id}', 'private', 'PUT', {'cost': 30}) diff --git a/ccxt/abstract/foxbit.py b/ccxt/abstract/foxbit.py new file mode 100644 index 0000000..672c714 --- /dev/null +++ b/ccxt/abstract/foxbit.py @@ -0,0 +1,26 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + v3_public_get_currencies = v3PublicGetCurrencies = Entry('currencies', ['v3', 'public'], 'GET', {'cost': 5}) + v3_public_get_markets = v3PublicGetMarkets = Entry('markets', ['v3', 'public'], 'GET', {'cost': 5}) + v3_public_get_markets_ticker_24hr = v3PublicGetMarketsTicker24hr = Entry('markets/ticker/24hr', ['v3', 'public'], 'GET', {'cost': 60}) + v3_public_get_markets_market_orderbook = v3PublicGetMarketsMarketOrderbook = Entry('markets/{market}/orderbook', ['v3', 'public'], 'GET', {'cost': 6}) + v3_public_get_markets_market_candlesticks = v3PublicGetMarketsMarketCandlesticks = Entry('markets/{market}/candlesticks', ['v3', 'public'], 'GET', {'cost': 12}) + v3_public_get_markets_market_trades_history = v3PublicGetMarketsMarketTradesHistory = Entry('markets/{market}/trades/history', ['v3', 'public'], 'GET', {'cost': 12}) + v3_public_get_markets_market_ticker_24hr = v3PublicGetMarketsMarketTicker24hr = Entry('markets/{market}/ticker/24hr', ['v3', 'public'], 'GET', {'cost': 15}) + v3_private_get_accounts = v3PrivateGetAccounts = Entry('accounts', ['v3', 'private'], 'GET', {'cost': 2}) + v3_private_get_accounts_symbol_transactions = v3PrivateGetAccountsSymbolTransactions = Entry('accounts/{symbol}/transactions', ['v3', 'private'], 'GET', {'cost': 60}) + v3_private_get_orders = v3PrivateGetOrders = Entry('orders', ['v3', 'private'], 'GET', {'cost': 2}) + v3_private_get_orders_by_order_id_id = v3PrivateGetOrdersByOrderIdId = Entry('orders/by-order-id/{id}', ['v3', 'private'], 'GET', {'cost': 2}) + v3_private_get_trades = v3PrivateGetTrades = Entry('trades', ['v3', 'private'], 'GET', {'cost': 6}) + v3_private_get_deposits_address = v3PrivateGetDepositsAddress = Entry('deposits/address', ['v3', 'private'], 'GET', {'cost': 10}) + v3_private_get_deposits = v3PrivateGetDeposits = Entry('deposits', ['v3', 'private'], 'GET', {'cost': 10}) + v3_private_get_withdrawals = v3PrivateGetWithdrawals = Entry('withdrawals', ['v3', 'private'], 'GET', {'cost': 10}) + v3_private_get_me_fees_trading = v3PrivateGetMeFeesTrading = Entry('me/fees/trading', ['v3', 'private'], 'GET', {'cost': 60}) + v3_private_post_orders = v3PrivatePostOrders = Entry('orders', ['v3', 'private'], 'POST', {'cost': 2}) + v3_private_post_orders_batch = v3PrivatePostOrdersBatch = Entry('orders/batch', ['v3', 'private'], 'POST', {'cost': 7.5}) + v3_private_post_orders_cancel_replace = v3PrivatePostOrdersCancelReplace = Entry('orders/cancel-replace', ['v3', 'private'], 'POST', {'cost': 3}) + v3_private_post_withdrawals = v3PrivatePostWithdrawals = Entry('withdrawals', ['v3', 'private'], 'POST', {'cost': 10}) + v3_private_put_orders_cancel = v3PrivatePutOrdersCancel = Entry('orders/cancel', ['v3', 'private'], 'PUT', {'cost': 2}) + status_public_get_status = statusPublicGetStatus = Entry('status', ['status', 'public'], 'GET', {'cost': 30}) diff --git a/ccxt/abstract/gate.py b/ccxt/abstract/gate.py new file mode 100644 index 0000000..1103c2a --- /dev/null +++ b/ccxt/abstract/gate.py @@ -0,0 +1,328 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_wallet_get_currency_chains = publicWalletGetCurrencyChains = Entry('currency_chains', ['public', 'wallet'], 'GET', {'cost': 1}) + public_unified_get_currencies = publicUnifiedGetCurrencies = Entry('currencies', ['public', 'unified'], 'GET', {'cost': 1}) + public_unified_get_history_loan_rate = publicUnifiedGetHistoryLoanRate = Entry('history_loan_rate', ['public', 'unified'], 'GET', {'cost': 1}) + public_spot_get_currencies = publicSpotGetCurrencies = Entry('currencies', ['public', 'spot'], 'GET', {'cost': 1}) + public_spot_get_currencies_currency = publicSpotGetCurrenciesCurrency = Entry('currencies/{currency}', ['public', 'spot'], 'GET', {'cost': 1}) + public_spot_get_currency_pairs = publicSpotGetCurrencyPairs = Entry('currency_pairs', ['public', 'spot'], 'GET', {'cost': 1}) + public_spot_get_currency_pairs_currency_pair = publicSpotGetCurrencyPairsCurrencyPair = Entry('currency_pairs/{currency_pair}', ['public', 'spot'], 'GET', {'cost': 1}) + public_spot_get_tickers = publicSpotGetTickers = Entry('tickers', ['public', 'spot'], 'GET', {'cost': 1}) + public_spot_get_order_book = publicSpotGetOrderBook = Entry('order_book', ['public', 'spot'], 'GET', {'cost': 1}) + public_spot_get_trades = publicSpotGetTrades = Entry('trades', ['public', 'spot'], 'GET', {'cost': 1}) + public_spot_get_candlesticks = publicSpotGetCandlesticks = Entry('candlesticks', ['public', 'spot'], 'GET', {'cost': 1}) + public_spot_get_time = publicSpotGetTime = Entry('time', ['public', 'spot'], 'GET', {'cost': 1}) + public_spot_get_insurance_history = publicSpotGetInsuranceHistory = Entry('insurance_history', ['public', 'spot'], 'GET', {'cost': 1}) + public_margin_get_uni_currency_pairs = publicMarginGetUniCurrencyPairs = Entry('uni/currency_pairs', ['public', 'margin'], 'GET', {'cost': 1}) + public_margin_get_uni_currency_pairs_currency_pair = publicMarginGetUniCurrencyPairsCurrencyPair = Entry('uni/currency_pairs/{currency_pair}', ['public', 'margin'], 'GET', {'cost': 1}) + public_margin_get_loan_margin_tiers = publicMarginGetLoanMarginTiers = Entry('loan_margin_tiers', ['public', 'margin'], 'GET', {'cost': 1}) + public_margin_get_currency_pairs = publicMarginGetCurrencyPairs = Entry('currency_pairs', ['public', 'margin'], 'GET', {'cost': 1}) + public_margin_get_currency_pairs_currency_pair = publicMarginGetCurrencyPairsCurrencyPair = Entry('currency_pairs/{currency_pair}', ['public', 'margin'], 'GET', {'cost': 1}) + public_margin_get_funding_book = publicMarginGetFundingBook = Entry('funding_book', ['public', 'margin'], 'GET', {'cost': 1}) + public_margin_get_cross_currencies = publicMarginGetCrossCurrencies = Entry('cross/currencies', ['public', 'margin'], 'GET', {'cost': 1}) + public_margin_get_cross_currencies_currency = publicMarginGetCrossCurrenciesCurrency = Entry('cross/currencies/{currency}', ['public', 'margin'], 'GET', {'cost': 1}) + public_flash_swap_get_currency_pairs = publicFlash_swapGetCurrencyPairs = Entry('currency_pairs', ['public', 'flash_swap'], 'GET', {'cost': 1}) + public_flash_swap_get_currencies = publicFlash_swapGetCurrencies = Entry('currencies', ['public', 'flash_swap'], 'GET', {'cost': 1}) + public_futures_get_settle_contracts = publicFuturesGetSettleContracts = Entry('{settle}/contracts', ['public', 'futures'], 'GET', {'cost': 1}) + public_futures_get_settle_contracts_contract = publicFuturesGetSettleContractsContract = Entry('{settle}/contracts/{contract}', ['public', 'futures'], 'GET', {'cost': 1}) + public_futures_get_settle_order_book = publicFuturesGetSettleOrderBook = Entry('{settle}/order_book', ['public', 'futures'], 'GET', {'cost': 1}) + public_futures_get_settle_trades = publicFuturesGetSettleTrades = Entry('{settle}/trades', ['public', 'futures'], 'GET', {'cost': 1}) + public_futures_get_settle_candlesticks = publicFuturesGetSettleCandlesticks = Entry('{settle}/candlesticks', ['public', 'futures'], 'GET', {'cost': 1}) + public_futures_get_settle_premium_index = publicFuturesGetSettlePremiumIndex = Entry('{settle}/premium_index', ['public', 'futures'], 'GET', {'cost': 1}) + public_futures_get_settle_tickers = publicFuturesGetSettleTickers = Entry('{settle}/tickers', ['public', 'futures'], 'GET', {'cost': 1}) + public_futures_get_settle_funding_rate = publicFuturesGetSettleFundingRate = Entry('{settle}/funding_rate', ['public', 'futures'], 'GET', {'cost': 1}) + public_futures_get_settle_insurance = publicFuturesGetSettleInsurance = Entry('{settle}/insurance', ['public', 'futures'], 'GET', {'cost': 1}) + public_futures_get_settle_contract_stats = publicFuturesGetSettleContractStats = Entry('{settle}/contract_stats', ['public', 'futures'], 'GET', {'cost': 1}) + public_futures_get_settle_index_constituents_index = publicFuturesGetSettleIndexConstituentsIndex = Entry('{settle}/index_constituents/{index}', ['public', 'futures'], 'GET', {'cost': 1}) + public_futures_get_settle_liq_orders = publicFuturesGetSettleLiqOrders = Entry('{settle}/liq_orders', ['public', 'futures'], 'GET', {'cost': 1}) + public_futures_get_settle_risk_limit_tiers = publicFuturesGetSettleRiskLimitTiers = Entry('{settle}/risk_limit_tiers', ['public', 'futures'], 'GET', {'cost': 1}) + public_delivery_get_settle_contracts = publicDeliveryGetSettleContracts = Entry('{settle}/contracts', ['public', 'delivery'], 'GET', {'cost': 1}) + public_delivery_get_settle_contracts_contract = publicDeliveryGetSettleContractsContract = Entry('{settle}/contracts/{contract}', ['public', 'delivery'], 'GET', {'cost': 1}) + public_delivery_get_settle_order_book = publicDeliveryGetSettleOrderBook = Entry('{settle}/order_book', ['public', 'delivery'], 'GET', {'cost': 1}) + public_delivery_get_settle_trades = publicDeliveryGetSettleTrades = Entry('{settle}/trades', ['public', 'delivery'], 'GET', {'cost': 1}) + public_delivery_get_settle_candlesticks = publicDeliveryGetSettleCandlesticks = Entry('{settle}/candlesticks', ['public', 'delivery'], 'GET', {'cost': 1}) + public_delivery_get_settle_tickers = publicDeliveryGetSettleTickers = Entry('{settle}/tickers', ['public', 'delivery'], 'GET', {'cost': 1}) + public_delivery_get_settle_insurance = publicDeliveryGetSettleInsurance = Entry('{settle}/insurance', ['public', 'delivery'], 'GET', {'cost': 1}) + public_delivery_get_settle_risk_limit_tiers = publicDeliveryGetSettleRiskLimitTiers = Entry('{settle}/risk_limit_tiers', ['public', 'delivery'], 'GET', {'cost': 1}) + public_options_get_underlyings = publicOptionsGetUnderlyings = Entry('underlyings', ['public', 'options'], 'GET', {'cost': 1}) + public_options_get_expirations = publicOptionsGetExpirations = Entry('expirations', ['public', 'options'], 'GET', {'cost': 1}) + public_options_get_contracts = publicOptionsGetContracts = Entry('contracts', ['public', 'options'], 'GET', {'cost': 1}) + public_options_get_contracts_contract = publicOptionsGetContractsContract = Entry('contracts/{contract}', ['public', 'options'], 'GET', {'cost': 1}) + public_options_get_settlements = publicOptionsGetSettlements = Entry('settlements', ['public', 'options'], 'GET', {'cost': 1}) + public_options_get_settlements_contract = publicOptionsGetSettlementsContract = Entry('settlements/{contract}', ['public', 'options'], 'GET', {'cost': 1}) + public_options_get_order_book = publicOptionsGetOrderBook = Entry('order_book', ['public', 'options'], 'GET', {'cost': 1}) + public_options_get_tickers = publicOptionsGetTickers = Entry('tickers', ['public', 'options'], 'GET', {'cost': 1}) + public_options_get_underlying_tickers_underlying = publicOptionsGetUnderlyingTickersUnderlying = Entry('underlying/tickers/{underlying}', ['public', 'options'], 'GET', {'cost': 1}) + public_options_get_candlesticks = publicOptionsGetCandlesticks = Entry('candlesticks', ['public', 'options'], 'GET', {'cost': 1}) + public_options_get_underlying_candlesticks = publicOptionsGetUnderlyingCandlesticks = Entry('underlying/candlesticks', ['public', 'options'], 'GET', {'cost': 1}) + public_options_get_trades = publicOptionsGetTrades = Entry('trades', ['public', 'options'], 'GET', {'cost': 1}) + public_earn_get_uni_currencies = publicEarnGetUniCurrencies = Entry('uni/currencies', ['public', 'earn'], 'GET', {'cost': 1}) + public_earn_get_uni_currencies_currency = publicEarnGetUniCurrenciesCurrency = Entry('uni/currencies/{currency}', ['public', 'earn'], 'GET', {'cost': 1}) + public_earn_get_dual_investment_plan = publicEarnGetDualInvestmentPlan = Entry('dual/investment_plan', ['public', 'earn'], 'GET', {'cost': 1}) + public_earn_get_structured_products = publicEarnGetStructuredProducts = Entry('structured/products', ['public', 'earn'], 'GET', {'cost': 1}) + public_loan_get_collateral_currencies = publicLoanGetCollateralCurrencies = Entry('collateral/currencies', ['public', 'loan'], 'GET', {'cost': 1}) + public_loan_get_multi_collateral_currencies = publicLoanGetMultiCollateralCurrencies = Entry('multi_collateral/currencies', ['public', 'loan'], 'GET', {'cost': 1}) + public_loan_get_multi_collateral_ltv = publicLoanGetMultiCollateralLtv = Entry('multi_collateral/ltv', ['public', 'loan'], 'GET', {'cost': 1}) + public_loan_get_multi_collateral_fixed_rate = publicLoanGetMultiCollateralFixedRate = Entry('multi_collateral/fixed_rate', ['public', 'loan'], 'GET', {'cost': 1}) + public_loan_get_multi_collateral_current_rate = publicLoanGetMultiCollateralCurrentRate = Entry('multi_collateral/current_rate', ['public', 'loan'], 'GET', {'cost': 1}) + private_withdrawals_post_withdrawals = privateWithdrawalsPostWithdrawals = Entry('withdrawals', ['private', 'withdrawals'], 'POST', {'cost': 20}) + private_withdrawals_post_push = privateWithdrawalsPostPush = Entry('push', ['private', 'withdrawals'], 'POST', {'cost': 1}) + private_withdrawals_delete_withdrawals_withdrawal_id = privateWithdrawalsDeleteWithdrawalsWithdrawalId = Entry('withdrawals/{withdrawal_id}', ['private', 'withdrawals'], 'DELETE', {'cost': 1}) + private_wallet_get_deposit_address = privateWalletGetDepositAddress = Entry('deposit_address', ['private', 'wallet'], 'GET', {'cost': 1}) + private_wallet_get_withdrawals = privateWalletGetWithdrawals = Entry('withdrawals', ['private', 'wallet'], 'GET', {'cost': 1}) + private_wallet_get_deposits = privateWalletGetDeposits = Entry('deposits', ['private', 'wallet'], 'GET', {'cost': 1}) + private_wallet_get_sub_account_transfers = privateWalletGetSubAccountTransfers = Entry('sub_account_transfers', ['private', 'wallet'], 'GET', {'cost': 1}) + private_wallet_get_order_status = privateWalletGetOrderStatus = Entry('order_status', ['private', 'wallet'], 'GET', {'cost': 1}) + private_wallet_get_withdraw_status = privateWalletGetWithdrawStatus = Entry('withdraw_status', ['private', 'wallet'], 'GET', {'cost': 1}) + private_wallet_get_sub_account_balances = privateWalletGetSubAccountBalances = Entry('sub_account_balances', ['private', 'wallet'], 'GET', {'cost': 2.5}) + private_wallet_get_sub_account_margin_balances = privateWalletGetSubAccountMarginBalances = Entry('sub_account_margin_balances', ['private', 'wallet'], 'GET', {'cost': 2.5}) + private_wallet_get_sub_account_futures_balances = privateWalletGetSubAccountFuturesBalances = Entry('sub_account_futures_balances', ['private', 'wallet'], 'GET', {'cost': 2.5}) + private_wallet_get_sub_account_cross_margin_balances = privateWalletGetSubAccountCrossMarginBalances = Entry('sub_account_cross_margin_balances', ['private', 'wallet'], 'GET', {'cost': 2.5}) + private_wallet_get_saved_address = privateWalletGetSavedAddress = Entry('saved_address', ['private', 'wallet'], 'GET', {'cost': 1}) + private_wallet_get_fee = privateWalletGetFee = Entry('fee', ['private', 'wallet'], 'GET', {'cost': 1}) + private_wallet_get_total_balance = privateWalletGetTotalBalance = Entry('total_balance', ['private', 'wallet'], 'GET', {'cost': 2.5}) + private_wallet_get_small_balance = privateWalletGetSmallBalance = Entry('small_balance', ['private', 'wallet'], 'GET', {'cost': 1}) + private_wallet_get_small_balance_history = privateWalletGetSmallBalanceHistory = Entry('small_balance_history', ['private', 'wallet'], 'GET', {'cost': 1}) + private_wallet_get_push = privateWalletGetPush = Entry('push', ['private', 'wallet'], 'GET', {'cost': 1}) + private_wallet_post_transfers = privateWalletPostTransfers = Entry('transfers', ['private', 'wallet'], 'POST', {'cost': 2.5}) + private_wallet_post_sub_account_transfers = privateWalletPostSubAccountTransfers = Entry('sub_account_transfers', ['private', 'wallet'], 'POST', {'cost': 2.5}) + private_wallet_post_sub_account_to_sub_account = privateWalletPostSubAccountToSubAccount = Entry('sub_account_to_sub_account', ['private', 'wallet'], 'POST', {'cost': 2.5}) + private_wallet_post_small_balance = privateWalletPostSmallBalance = Entry('small_balance', ['private', 'wallet'], 'POST', {'cost': 1}) + private_subaccounts_get_sub_accounts = privateSubAccountsGetSubAccounts = Entry('sub_accounts', ['private', 'subAccounts'], 'GET', {'cost': 2.5}) + private_subaccounts_get_sub_accounts_user_id = privateSubAccountsGetSubAccountsUserId = Entry('sub_accounts/{user_id}', ['private', 'subAccounts'], 'GET', {'cost': 2.5}) + private_subaccounts_get_sub_accounts_user_id_keys = privateSubAccountsGetSubAccountsUserIdKeys = Entry('sub_accounts/{user_id}/keys', ['private', 'subAccounts'], 'GET', {'cost': 2.5}) + private_subaccounts_get_sub_accounts_user_id_keys_key = privateSubAccountsGetSubAccountsUserIdKeysKey = Entry('sub_accounts/{user_id}/keys/{key}', ['private', 'subAccounts'], 'GET', {'cost': 2.5}) + private_subaccounts_post_sub_accounts = privateSubAccountsPostSubAccounts = Entry('sub_accounts', ['private', 'subAccounts'], 'POST', {'cost': 2.5}) + private_subaccounts_post_sub_accounts_user_id_keys = privateSubAccountsPostSubAccountsUserIdKeys = Entry('sub_accounts/{user_id}/keys', ['private', 'subAccounts'], 'POST', {'cost': 2.5}) + private_subaccounts_post_sub_accounts_user_id_lock = privateSubAccountsPostSubAccountsUserIdLock = Entry('sub_accounts/{user_id}/lock', ['private', 'subAccounts'], 'POST', {'cost': 2.5}) + private_subaccounts_post_sub_accounts_user_id_unlock = privateSubAccountsPostSubAccountsUserIdUnlock = Entry('sub_accounts/{user_id}/unlock', ['private', 'subAccounts'], 'POST', {'cost': 2.5}) + private_subaccounts_put_sub_accounts_user_id_keys_key = privateSubAccountsPutSubAccountsUserIdKeysKey = Entry('sub_accounts/{user_id}/keys/{key}', ['private', 'subAccounts'], 'PUT', {'cost': 2.5}) + private_subaccounts_delete_sub_accounts_user_id_keys_key = privateSubAccountsDeleteSubAccountsUserIdKeysKey = Entry('sub_accounts/{user_id}/keys/{key}', ['private', 'subAccounts'], 'DELETE', {'cost': 2.5}) + private_unified_get_accounts = privateUnifiedGetAccounts = Entry('accounts', ['private', 'unified'], 'GET', {'cost': 1.3333333333333333}) + private_unified_get_borrowable = privateUnifiedGetBorrowable = Entry('borrowable', ['private', 'unified'], 'GET', {'cost': 1.3333333333333333}) + private_unified_get_transferable = privateUnifiedGetTransferable = Entry('transferable', ['private', 'unified'], 'GET', {'cost': 1.3333333333333333}) + private_unified_get_transferables = privateUnifiedGetTransferables = Entry('transferables', ['private', 'unified'], 'GET', {'cost': 1.3333333333333333}) + private_unified_get_batch_borrowable = privateUnifiedGetBatchBorrowable = Entry('batch_borrowable', ['private', 'unified'], 'GET', {'cost': 1.3333333333333333}) + private_unified_get_loans = privateUnifiedGetLoans = Entry('loans', ['private', 'unified'], 'GET', {'cost': 1.3333333333333333}) + private_unified_get_loan_records = privateUnifiedGetLoanRecords = Entry('loan_records', ['private', 'unified'], 'GET', {'cost': 1.3333333333333333}) + private_unified_get_interest_records = privateUnifiedGetInterestRecords = Entry('interest_records', ['private', 'unified'], 'GET', {'cost': 1.3333333333333333}) + private_unified_get_risk_units = privateUnifiedGetRiskUnits = Entry('risk_units', ['private', 'unified'], 'GET', {'cost': 1.3333333333333333}) + private_unified_get_unified_mode = privateUnifiedGetUnifiedMode = Entry('unified_mode', ['private', 'unified'], 'GET', {'cost': 1.3333333333333333}) + private_unified_get_estimate_rate = privateUnifiedGetEstimateRate = Entry('estimate_rate', ['private', 'unified'], 'GET', {'cost': 1.3333333333333333}) + private_unified_get_currency_discount_tiers = privateUnifiedGetCurrencyDiscountTiers = Entry('currency_discount_tiers', ['private', 'unified'], 'GET', {'cost': 1.3333333333333333}) + private_unified_get_loan_margin_tiers = privateUnifiedGetLoanMarginTiers = Entry('loan_margin_tiers', ['private', 'unified'], 'GET', {'cost': 1.3333333333333333}) + private_unified_get_leverage_user_currency_config = privateUnifiedGetLeverageUserCurrencyConfig = Entry('leverage/user_currency_config', ['private', 'unified'], 'GET', {'cost': 1.3333333333333333}) + private_unified_get_leverage_user_currency_setting = privateUnifiedGetLeverageUserCurrencySetting = Entry('leverage/user_currency_setting', ['private', 'unified'], 'GET', {'cost': 1.3333333333333333}) + private_unified_get_account_mode = privateUnifiedGetAccountMode = Entry('account_mode', ['private', 'unified'], 'GET', {'cost': 1.3333333333333333}) + private_unified_post_loans = privateUnifiedPostLoans = Entry('loans', ['private', 'unified'], 'POST', {'cost': 13.333333333333334}) + private_unified_post_portfolio_calculator = privateUnifiedPostPortfolioCalculator = Entry('portfolio_calculator', ['private', 'unified'], 'POST', {'cost': 1.3333333333333333}) + private_unified_post_leverage_user_currency_setting = privateUnifiedPostLeverageUserCurrencySetting = Entry('leverage/user_currency_setting', ['private', 'unified'], 'POST', {'cost': 1.3333333333333333}) + private_unified_post_collateral_currencies = privateUnifiedPostCollateralCurrencies = Entry('collateral_currencies', ['private', 'unified'], 'POST', {'cost': 1.3333333333333333}) + private_unified_post_account_mode = privateUnifiedPostAccountMode = Entry('account_mode', ['private', 'unified'], 'POST', {'cost': 1.3333333333333333}) + private_unified_put_unified_mode = privateUnifiedPutUnifiedMode = Entry('unified_mode', ['private', 'unified'], 'PUT', {'cost': 1.3333333333333333}) + private_spot_get_fee = privateSpotGetFee = Entry('fee', ['private', 'spot'], 'GET', {'cost': 1}) + private_spot_get_batch_fee = privateSpotGetBatchFee = Entry('batch_fee', ['private', 'spot'], 'GET', {'cost': 1}) + private_spot_get_accounts = privateSpotGetAccounts = Entry('accounts', ['private', 'spot'], 'GET', {'cost': 1}) + private_spot_get_account_book = privateSpotGetAccountBook = Entry('account_book', ['private', 'spot'], 'GET', {'cost': 1}) + private_spot_get_open_orders = privateSpotGetOpenOrders = Entry('open_orders', ['private', 'spot'], 'GET', {'cost': 1}) + private_spot_get_orders = privateSpotGetOrders = Entry('orders', ['private', 'spot'], 'GET', {'cost': 1}) + private_spot_get_orders_order_id = privateSpotGetOrdersOrderId = Entry('orders/{order_id}', ['private', 'spot'], 'GET', {'cost': 1}) + private_spot_get_my_trades = privateSpotGetMyTrades = Entry('my_trades', ['private', 'spot'], 'GET', {'cost': 1}) + private_spot_get_price_orders = privateSpotGetPriceOrders = Entry('price_orders', ['private', 'spot'], 'GET', {'cost': 1}) + private_spot_get_price_orders_order_id = privateSpotGetPriceOrdersOrderId = Entry('price_orders/{order_id}', ['private', 'spot'], 'GET', {'cost': 1}) + private_spot_post_batch_orders = privateSpotPostBatchOrders = Entry('batch_orders', ['private', 'spot'], 'POST', {'cost': 0.4}) + private_spot_post_cross_liquidate_orders = privateSpotPostCrossLiquidateOrders = Entry('cross_liquidate_orders', ['private', 'spot'], 'POST', {'cost': 1}) + private_spot_post_orders = privateSpotPostOrders = Entry('orders', ['private', 'spot'], 'POST', {'cost': 0.4}) + private_spot_post_cancel_batch_orders = privateSpotPostCancelBatchOrders = Entry('cancel_batch_orders', ['private', 'spot'], 'POST', {'cost': 0.26666666666666666}) + private_spot_post_countdown_cancel_all = privateSpotPostCountdownCancelAll = Entry('countdown_cancel_all', ['private', 'spot'], 'POST', {'cost': 0.26666666666666666}) + private_spot_post_amend_batch_orders = privateSpotPostAmendBatchOrders = Entry('amend_batch_orders', ['private', 'spot'], 'POST', {'cost': 0.4}) + private_spot_post_price_orders = privateSpotPostPriceOrders = Entry('price_orders', ['private', 'spot'], 'POST', {'cost': 0.4}) + private_spot_delete_orders = privateSpotDeleteOrders = Entry('orders', ['private', 'spot'], 'DELETE', {'cost': 0.26666666666666666}) + private_spot_delete_orders_order_id = privateSpotDeleteOrdersOrderId = Entry('orders/{order_id}', ['private', 'spot'], 'DELETE', {'cost': 0.26666666666666666}) + private_spot_delete_price_orders = privateSpotDeletePriceOrders = Entry('price_orders', ['private', 'spot'], 'DELETE', {'cost': 0.26666666666666666}) + private_spot_delete_price_orders_order_id = privateSpotDeletePriceOrdersOrderId = Entry('price_orders/{order_id}', ['private', 'spot'], 'DELETE', {'cost': 0.26666666666666666}) + private_spot_patch_orders_order_id = privateSpotPatchOrdersOrderId = Entry('orders/{order_id}', ['private', 'spot'], 'PATCH', {'cost': 0.4}) + private_margin_get_accounts = privateMarginGetAccounts = Entry('accounts', ['private', 'margin'], 'GET', {'cost': 1.3333333333333333}) + private_margin_get_account_book = privateMarginGetAccountBook = Entry('account_book', ['private', 'margin'], 'GET', {'cost': 1.3333333333333333}) + private_margin_get_funding_accounts = privateMarginGetFundingAccounts = Entry('funding_accounts', ['private', 'margin'], 'GET', {'cost': 1.3333333333333333}) + private_margin_get_auto_repay = privateMarginGetAutoRepay = Entry('auto_repay', ['private', 'margin'], 'GET', {'cost': 1.3333333333333333}) + private_margin_get_transferable = privateMarginGetTransferable = Entry('transferable', ['private', 'margin'], 'GET', {'cost': 1.3333333333333333}) + private_margin_get_uni_estimate_rate = privateMarginGetUniEstimateRate = Entry('uni/estimate_rate', ['private', 'margin'], 'GET', {'cost': 1.3333333333333333}) + private_margin_get_uni_loans = privateMarginGetUniLoans = Entry('uni/loans', ['private', 'margin'], 'GET', {'cost': 1.3333333333333333}) + private_margin_get_uni_loan_records = privateMarginGetUniLoanRecords = Entry('uni/loan_records', ['private', 'margin'], 'GET', {'cost': 1.3333333333333333}) + private_margin_get_uni_interest_records = privateMarginGetUniInterestRecords = Entry('uni/interest_records', ['private', 'margin'], 'GET', {'cost': 1.3333333333333333}) + private_margin_get_uni_borrowable = privateMarginGetUniBorrowable = Entry('uni/borrowable', ['private', 'margin'], 'GET', {'cost': 1.3333333333333333}) + private_margin_get_user_loan_margin_tiers = privateMarginGetUserLoanMarginTiers = Entry('user/loan_margin_tiers', ['private', 'margin'], 'GET', {'cost': 1.3333333333333333}) + private_margin_get_user_account = privateMarginGetUserAccount = Entry('user/account', ['private', 'margin'], 'GET', {'cost': 1.3333333333333333}) + private_margin_get_loans = privateMarginGetLoans = Entry('loans', ['private', 'margin'], 'GET', {'cost': 1.3333333333333333}) + private_margin_get_loans_loan_id = privateMarginGetLoansLoanId = Entry('loans/{loan_id}', ['private', 'margin'], 'GET', {'cost': 1.3333333333333333}) + private_margin_get_loans_loan_id_repayment = privateMarginGetLoansLoanIdRepayment = Entry('loans/{loan_id}/repayment', ['private', 'margin'], 'GET', {'cost': 1.3333333333333333}) + private_margin_get_loan_records = privateMarginGetLoanRecords = Entry('loan_records', ['private', 'margin'], 'GET', {'cost': 1.3333333333333333}) + private_margin_get_loan_records_loan_record_id = privateMarginGetLoanRecordsLoanRecordId = Entry('loan_records/{loan_record_id}', ['private', 'margin'], 'GET', {'cost': 1.3333333333333333}) + private_margin_get_borrowable = privateMarginGetBorrowable = Entry('borrowable', ['private', 'margin'], 'GET', {'cost': 1.3333333333333333}) + private_margin_get_cross_accounts = privateMarginGetCrossAccounts = Entry('cross/accounts', ['private', 'margin'], 'GET', {'cost': 1.3333333333333333}) + private_margin_get_cross_account_book = privateMarginGetCrossAccountBook = Entry('cross/account_book', ['private', 'margin'], 'GET', {'cost': 1.3333333333333333}) + private_margin_get_cross_loans = privateMarginGetCrossLoans = Entry('cross/loans', ['private', 'margin'], 'GET', {'cost': 1.3333333333333333}) + private_margin_get_cross_loans_loan_id = privateMarginGetCrossLoansLoanId = Entry('cross/loans/{loan_id}', ['private', 'margin'], 'GET', {'cost': 1.3333333333333333}) + private_margin_get_cross_repayments = privateMarginGetCrossRepayments = Entry('cross/repayments', ['private', 'margin'], 'GET', {'cost': 1.3333333333333333}) + private_margin_get_cross_interest_records = privateMarginGetCrossInterestRecords = Entry('cross/interest_records', ['private', 'margin'], 'GET', {'cost': 1.3333333333333333}) + private_margin_get_cross_transferable = privateMarginGetCrossTransferable = Entry('cross/transferable', ['private', 'margin'], 'GET', {'cost': 1.3333333333333333}) + private_margin_get_cross_estimate_rate = privateMarginGetCrossEstimateRate = Entry('cross/estimate_rate', ['private', 'margin'], 'GET', {'cost': 1.3333333333333333}) + private_margin_get_cross_borrowable = privateMarginGetCrossBorrowable = Entry('cross/borrowable', ['private', 'margin'], 'GET', {'cost': 1.3333333333333333}) + private_margin_post_auto_repay = privateMarginPostAutoRepay = Entry('auto_repay', ['private', 'margin'], 'POST', {'cost': 1.3333333333333333}) + private_margin_post_uni_loans = privateMarginPostUniLoans = Entry('uni/loans', ['private', 'margin'], 'POST', {'cost': 1.3333333333333333}) + private_margin_post_leverage_user_market_setting = privateMarginPostLeverageUserMarketSetting = Entry('leverage/user_market_setting', ['private', 'margin'], 'POST', {'cost': 1.3333333333333333}) + private_margin_post_loans = privateMarginPostLoans = Entry('loans', ['private', 'margin'], 'POST', {'cost': 1.3333333333333333}) + private_margin_post_merged_loans = privateMarginPostMergedLoans = Entry('merged_loans', ['private', 'margin'], 'POST', {'cost': 1.3333333333333333}) + private_margin_post_loans_loan_id_repayment = privateMarginPostLoansLoanIdRepayment = Entry('loans/{loan_id}/repayment', ['private', 'margin'], 'POST', {'cost': 1.3333333333333333}) + private_margin_post_cross_loans = privateMarginPostCrossLoans = Entry('cross/loans', ['private', 'margin'], 'POST', {'cost': 1.3333333333333333}) + private_margin_post_cross_repayments = privateMarginPostCrossRepayments = Entry('cross/repayments', ['private', 'margin'], 'POST', {'cost': 1.3333333333333333}) + private_margin_patch_loans_loan_id = privateMarginPatchLoansLoanId = Entry('loans/{loan_id}', ['private', 'margin'], 'PATCH', {'cost': 1.3333333333333333}) + private_margin_patch_loan_records_loan_record_id = privateMarginPatchLoanRecordsLoanRecordId = Entry('loan_records/{loan_record_id}', ['private', 'margin'], 'PATCH', {'cost': 1.3333333333333333}) + private_margin_delete_loans_loan_id = privateMarginDeleteLoansLoanId = Entry('loans/{loan_id}', ['private', 'margin'], 'DELETE', {'cost': 1.3333333333333333}) + private_flash_swap_get_orders = privateFlash_swapGetOrders = Entry('orders', ['private', 'flash_swap'], 'GET', {'cost': 1}) + private_flash_swap_get_orders_order_id = privateFlash_swapGetOrdersOrderId = Entry('orders/{order_id}', ['private', 'flash_swap'], 'GET', {'cost': 1}) + private_flash_swap_post_orders = privateFlash_swapPostOrders = Entry('orders', ['private', 'flash_swap'], 'POST', {'cost': 1}) + private_flash_swap_post_orders_preview = privateFlash_swapPostOrdersPreview = Entry('orders/preview', ['private', 'flash_swap'], 'POST', {'cost': 1}) + private_futures_get_settle_accounts = privateFuturesGetSettleAccounts = Entry('{settle}/accounts', ['private', 'futures'], 'GET', {'cost': 1}) + private_futures_get_settle_account_book = privateFuturesGetSettleAccountBook = Entry('{settle}/account_book', ['private', 'futures'], 'GET', {'cost': 1}) + private_futures_get_settle_positions = privateFuturesGetSettlePositions = Entry('{settle}/positions', ['private', 'futures'], 'GET', {'cost': 1}) + private_futures_get_settle_positions_contract = privateFuturesGetSettlePositionsContract = Entry('{settle}/positions/{contract}', ['private', 'futures'], 'GET', {'cost': 1}) + private_futures_get_settle_dual_comp_positions_contract = privateFuturesGetSettleDualCompPositionsContract = Entry('{settle}/dual_comp/positions/{contract}', ['private', 'futures'], 'GET', {'cost': 1}) + private_futures_get_settle_orders = privateFuturesGetSettleOrders = Entry('{settle}/orders', ['private', 'futures'], 'GET', {'cost': 1}) + private_futures_get_settle_orders_timerange = privateFuturesGetSettleOrdersTimerange = Entry('{settle}/orders_timerange', ['private', 'futures'], 'GET', {'cost': 1}) + private_futures_get_settle_orders_order_id = privateFuturesGetSettleOrdersOrderId = Entry('{settle}/orders/{order_id}', ['private', 'futures'], 'GET', {'cost': 1}) + private_futures_get_settle_my_trades = privateFuturesGetSettleMyTrades = Entry('{settle}/my_trades', ['private', 'futures'], 'GET', {'cost': 1}) + private_futures_get_settle_my_trades_timerange = privateFuturesGetSettleMyTradesTimerange = Entry('{settle}/my_trades_timerange', ['private', 'futures'], 'GET', {'cost': 1}) + private_futures_get_settle_position_close = privateFuturesGetSettlePositionClose = Entry('{settle}/position_close', ['private', 'futures'], 'GET', {'cost': 1}) + private_futures_get_settle_liquidates = privateFuturesGetSettleLiquidates = Entry('{settle}/liquidates', ['private', 'futures'], 'GET', {'cost': 1}) + private_futures_get_settle_auto_deleverages = privateFuturesGetSettleAutoDeleverages = Entry('{settle}/auto_deleverages', ['private', 'futures'], 'GET', {'cost': 1}) + private_futures_get_settle_fee = privateFuturesGetSettleFee = Entry('{settle}/fee', ['private', 'futures'], 'GET', {'cost': 1}) + private_futures_get_settle_risk_limit_table = privateFuturesGetSettleRiskLimitTable = Entry('{settle}/risk_limit_table', ['private', 'futures'], 'GET', {'cost': 1}) + private_futures_get_settle_price_orders = privateFuturesGetSettlePriceOrders = Entry('{settle}/price_orders', ['private', 'futures'], 'GET', {'cost': 1}) + private_futures_get_settle_price_orders_order_id = privateFuturesGetSettlePriceOrdersOrderId = Entry('{settle}/price_orders/{order_id}', ['private', 'futures'], 'GET', {'cost': 1}) + private_futures_post_settle_positions_contract_margin = privateFuturesPostSettlePositionsContractMargin = Entry('{settle}/positions/{contract}/margin', ['private', 'futures'], 'POST', {'cost': 1}) + private_futures_post_settle_positions_contract_leverage = privateFuturesPostSettlePositionsContractLeverage = Entry('{settle}/positions/{contract}/leverage', ['private', 'futures'], 'POST', {'cost': 1}) + private_futures_post_settle_positions_contract_risk_limit = privateFuturesPostSettlePositionsContractRiskLimit = Entry('{settle}/positions/{contract}/risk_limit', ['private', 'futures'], 'POST', {'cost': 1}) + private_futures_post_settle_positions_cross_mode = privateFuturesPostSettlePositionsCrossMode = Entry('{settle}/positions/cross_mode', ['private', 'futures'], 'POST', {'cost': 1}) + private_futures_post_settle_dual_comp_positions_cross_mode = privateFuturesPostSettleDualCompPositionsCrossMode = Entry('{settle}/dual_comp/positions/cross_mode', ['private', 'futures'], 'POST', {'cost': 1}) + private_futures_post_settle_dual_mode = privateFuturesPostSettleDualMode = Entry('{settle}/dual_mode', ['private', 'futures'], 'POST', {'cost': 1}) + private_futures_post_settle_dual_comp_positions_contract_margin = privateFuturesPostSettleDualCompPositionsContractMargin = Entry('{settle}/dual_comp/positions/{contract}/margin', ['private', 'futures'], 'POST', {'cost': 1}) + private_futures_post_settle_dual_comp_positions_contract_leverage = privateFuturesPostSettleDualCompPositionsContractLeverage = Entry('{settle}/dual_comp/positions/{contract}/leverage', ['private', 'futures'], 'POST', {'cost': 1}) + private_futures_post_settle_dual_comp_positions_contract_risk_limit = privateFuturesPostSettleDualCompPositionsContractRiskLimit = Entry('{settle}/dual_comp/positions/{contract}/risk_limit', ['private', 'futures'], 'POST', {'cost': 1}) + private_futures_post_settle_orders = privateFuturesPostSettleOrders = Entry('{settle}/orders', ['private', 'futures'], 'POST', {'cost': 0.4}) + private_futures_post_settle_batch_orders = privateFuturesPostSettleBatchOrders = Entry('{settle}/batch_orders', ['private', 'futures'], 'POST', {'cost': 0.4}) + private_futures_post_settle_countdown_cancel_all = privateFuturesPostSettleCountdownCancelAll = Entry('{settle}/countdown_cancel_all', ['private', 'futures'], 'POST', {'cost': 0.4}) + private_futures_post_settle_batch_cancel_orders = privateFuturesPostSettleBatchCancelOrders = Entry('{settle}/batch_cancel_orders', ['private', 'futures'], 'POST', {'cost': 0.4}) + private_futures_post_settle_batch_amend_orders = privateFuturesPostSettleBatchAmendOrders = Entry('{settle}/batch_amend_orders', ['private', 'futures'], 'POST', {'cost': 0.4}) + private_futures_post_settle_bbo_orders = privateFuturesPostSettleBboOrders = Entry('{settle}/bbo_orders', ['private', 'futures'], 'POST', {'cost': 0.4}) + private_futures_post_settle_price_orders = privateFuturesPostSettlePriceOrders = Entry('{settle}/price_orders', ['private', 'futures'], 'POST', {'cost': 0.4}) + private_futures_put_settle_orders_order_id = privateFuturesPutSettleOrdersOrderId = Entry('{settle}/orders/{order_id}', ['private', 'futures'], 'PUT', {'cost': 1}) + private_futures_delete_settle_orders = privateFuturesDeleteSettleOrders = Entry('{settle}/orders', ['private', 'futures'], 'DELETE', {'cost': 0.26666666666666666}) + private_futures_delete_settle_orders_order_id = privateFuturesDeleteSettleOrdersOrderId = Entry('{settle}/orders/{order_id}', ['private', 'futures'], 'DELETE', {'cost': 0.26666666666666666}) + private_futures_delete_settle_price_orders = privateFuturesDeleteSettlePriceOrders = Entry('{settle}/price_orders', ['private', 'futures'], 'DELETE', {'cost': 0.26666666666666666}) + private_futures_delete_settle_price_orders_order_id = privateFuturesDeleteSettlePriceOrdersOrderId = Entry('{settle}/price_orders/{order_id}', ['private', 'futures'], 'DELETE', {'cost': 0.26666666666666666}) + private_delivery_get_settle_accounts = privateDeliveryGetSettleAccounts = Entry('{settle}/accounts', ['private', 'delivery'], 'GET', {'cost': 1.3333333333333333}) + private_delivery_get_settle_account_book = privateDeliveryGetSettleAccountBook = Entry('{settle}/account_book', ['private', 'delivery'], 'GET', {'cost': 1.3333333333333333}) + private_delivery_get_settle_positions = privateDeliveryGetSettlePositions = Entry('{settle}/positions', ['private', 'delivery'], 'GET', {'cost': 1.3333333333333333}) + private_delivery_get_settle_positions_contract = privateDeliveryGetSettlePositionsContract = Entry('{settle}/positions/{contract}', ['private', 'delivery'], 'GET', {'cost': 1.3333333333333333}) + private_delivery_get_settle_orders = privateDeliveryGetSettleOrders = Entry('{settle}/orders', ['private', 'delivery'], 'GET', {'cost': 1.3333333333333333}) + private_delivery_get_settle_orders_order_id = privateDeliveryGetSettleOrdersOrderId = Entry('{settle}/orders/{order_id}', ['private', 'delivery'], 'GET', {'cost': 1.3333333333333333}) + private_delivery_get_settle_my_trades = privateDeliveryGetSettleMyTrades = Entry('{settle}/my_trades', ['private', 'delivery'], 'GET', {'cost': 1.3333333333333333}) + private_delivery_get_settle_position_close = privateDeliveryGetSettlePositionClose = Entry('{settle}/position_close', ['private', 'delivery'], 'GET', {'cost': 1.3333333333333333}) + private_delivery_get_settle_liquidates = privateDeliveryGetSettleLiquidates = Entry('{settle}/liquidates', ['private', 'delivery'], 'GET', {'cost': 1.3333333333333333}) + private_delivery_get_settle_settlements = privateDeliveryGetSettleSettlements = Entry('{settle}/settlements', ['private', 'delivery'], 'GET', {'cost': 1.3333333333333333}) + private_delivery_get_settle_price_orders = privateDeliveryGetSettlePriceOrders = Entry('{settle}/price_orders', ['private', 'delivery'], 'GET', {'cost': 1.3333333333333333}) + private_delivery_get_settle_price_orders_order_id = privateDeliveryGetSettlePriceOrdersOrderId = Entry('{settle}/price_orders/{order_id}', ['private', 'delivery'], 'GET', {'cost': 1.3333333333333333}) + private_delivery_post_settle_positions_contract_margin = privateDeliveryPostSettlePositionsContractMargin = Entry('{settle}/positions/{contract}/margin', ['private', 'delivery'], 'POST', {'cost': 1.3333333333333333}) + private_delivery_post_settle_positions_contract_leverage = privateDeliveryPostSettlePositionsContractLeverage = Entry('{settle}/positions/{contract}/leverage', ['private', 'delivery'], 'POST', {'cost': 1.3333333333333333}) + private_delivery_post_settle_positions_contract_risk_limit = privateDeliveryPostSettlePositionsContractRiskLimit = Entry('{settle}/positions/{contract}/risk_limit', ['private', 'delivery'], 'POST', {'cost': 1.3333333333333333}) + private_delivery_post_settle_orders = privateDeliveryPostSettleOrders = Entry('{settle}/orders', ['private', 'delivery'], 'POST', {'cost': 1.3333333333333333}) + private_delivery_post_settle_price_orders = privateDeliveryPostSettlePriceOrders = Entry('{settle}/price_orders', ['private', 'delivery'], 'POST', {'cost': 1.3333333333333333}) + private_delivery_delete_settle_orders = privateDeliveryDeleteSettleOrders = Entry('{settle}/orders', ['private', 'delivery'], 'DELETE', {'cost': 1.3333333333333333}) + private_delivery_delete_settle_orders_order_id = privateDeliveryDeleteSettleOrdersOrderId = Entry('{settle}/orders/{order_id}', ['private', 'delivery'], 'DELETE', {'cost': 1.3333333333333333}) + private_delivery_delete_settle_price_orders = privateDeliveryDeleteSettlePriceOrders = Entry('{settle}/price_orders', ['private', 'delivery'], 'DELETE', {'cost': 1.3333333333333333}) + private_delivery_delete_settle_price_orders_order_id = privateDeliveryDeleteSettlePriceOrdersOrderId = Entry('{settle}/price_orders/{order_id}', ['private', 'delivery'], 'DELETE', {'cost': 1.3333333333333333}) + private_options_get_my_settlements = privateOptionsGetMySettlements = Entry('my_settlements', ['private', 'options'], 'GET', {'cost': 1.3333333333333333}) + private_options_get_accounts = privateOptionsGetAccounts = Entry('accounts', ['private', 'options'], 'GET', {'cost': 1.3333333333333333}) + private_options_get_account_book = privateOptionsGetAccountBook = Entry('account_book', ['private', 'options'], 'GET', {'cost': 1.3333333333333333}) + private_options_get_positions = privateOptionsGetPositions = Entry('positions', ['private', 'options'], 'GET', {'cost': 1.3333333333333333}) + private_options_get_positions_contract = privateOptionsGetPositionsContract = Entry('positions/{contract}', ['private', 'options'], 'GET', {'cost': 1.3333333333333333}) + private_options_get_position_close = privateOptionsGetPositionClose = Entry('position_close', ['private', 'options'], 'GET', {'cost': 1.3333333333333333}) + private_options_get_orders = privateOptionsGetOrders = Entry('orders', ['private', 'options'], 'GET', {'cost': 1.3333333333333333}) + private_options_get_orders_order_id = privateOptionsGetOrdersOrderId = Entry('orders/{order_id}', ['private', 'options'], 'GET', {'cost': 1.3333333333333333}) + private_options_get_my_trades = privateOptionsGetMyTrades = Entry('my_trades', ['private', 'options'], 'GET', {'cost': 1.3333333333333333}) + private_options_get_mmp = privateOptionsGetMmp = Entry('mmp', ['private', 'options'], 'GET', {'cost': 1.3333333333333333}) + private_options_post_orders = privateOptionsPostOrders = Entry('orders', ['private', 'options'], 'POST', {'cost': 1.3333333333333333}) + private_options_post_countdown_cancel_all = privateOptionsPostCountdownCancelAll = Entry('countdown_cancel_all', ['private', 'options'], 'POST', {'cost': 1.3333333333333333}) + private_options_post_mmp = privateOptionsPostMmp = Entry('mmp', ['private', 'options'], 'POST', {'cost': 1.3333333333333333}) + private_options_post_mmp_reset = privateOptionsPostMmpReset = Entry('mmp/reset', ['private', 'options'], 'POST', {'cost': 1.3333333333333333}) + private_options_delete_orders = privateOptionsDeleteOrders = Entry('orders', ['private', 'options'], 'DELETE', {'cost': 1.3333333333333333}) + private_options_delete_orders_order_id = privateOptionsDeleteOrdersOrderId = Entry('orders/{order_id}', ['private', 'options'], 'DELETE', {'cost': 1.3333333333333333}) + private_earn_get_uni_lends = privateEarnGetUniLends = Entry('uni/lends', ['private', 'earn'], 'GET', {'cost': 1.3333333333333333}) + private_earn_get_uni_lend_records = privateEarnGetUniLendRecords = Entry('uni/lend_records', ['private', 'earn'], 'GET', {'cost': 1.3333333333333333}) + private_earn_get_uni_interests_currency = privateEarnGetUniInterestsCurrency = Entry('uni/interests/{currency}', ['private', 'earn'], 'GET', {'cost': 1.3333333333333333}) + private_earn_get_uni_interest_records = privateEarnGetUniInterestRecords = Entry('uni/interest_records', ['private', 'earn'], 'GET', {'cost': 1.3333333333333333}) + private_earn_get_uni_interest_status_currency = privateEarnGetUniInterestStatusCurrency = Entry('uni/interest_status/{currency}', ['private', 'earn'], 'GET', {'cost': 1.3333333333333333}) + private_earn_get_uni_chart = privateEarnGetUniChart = Entry('uni/chart', ['private', 'earn'], 'GET', {'cost': 1.3333333333333333}) + private_earn_get_uni_rate = privateEarnGetUniRate = Entry('uni/rate', ['private', 'earn'], 'GET', {'cost': 1.3333333333333333}) + private_earn_get_staking_eth2_rate_records = privateEarnGetStakingEth2RateRecords = Entry('staking/eth2/rate_records', ['private', 'earn'], 'GET', {'cost': 1.3333333333333333}) + private_earn_get_dual_orders = privateEarnGetDualOrders = Entry('dual/orders', ['private', 'earn'], 'GET', {'cost': 1.3333333333333333}) + private_earn_get_structured_orders = privateEarnGetStructuredOrders = Entry('structured/orders', ['private', 'earn'], 'GET', {'cost': 1.3333333333333333}) + private_earn_get_staking_coins = privateEarnGetStakingCoins = Entry('staking/coins', ['private', 'earn'], 'GET', {'cost': 1.3333333333333333}) + private_earn_get_staking_order_list = privateEarnGetStakingOrderList = Entry('staking/order_list', ['private', 'earn'], 'GET', {'cost': 1.3333333333333333}) + private_earn_get_staking_award_list = privateEarnGetStakingAwardList = Entry('staking/award_list', ['private', 'earn'], 'GET', {'cost': 1.3333333333333333}) + private_earn_get_staking_assets = privateEarnGetStakingAssets = Entry('staking/assets', ['private', 'earn'], 'GET', {'cost': 1.3333333333333333}) + private_earn_get_uni_currencies = privateEarnGetUniCurrencies = Entry('uni/currencies', ['private', 'earn'], 'GET', {'cost': 1.3333333333333333}) + private_earn_get_uni_currencies_currency = privateEarnGetUniCurrenciesCurrency = Entry('uni/currencies/{currency}', ['private', 'earn'], 'GET', {'cost': 1.3333333333333333}) + private_earn_post_uni_lends = privateEarnPostUniLends = Entry('uni/lends', ['private', 'earn'], 'POST', {'cost': 1.3333333333333333}) + private_earn_post_staking_eth2_swap = privateEarnPostStakingEth2Swap = Entry('staking/eth2/swap', ['private', 'earn'], 'POST', {'cost': 1.3333333333333333}) + private_earn_post_dual_orders = privateEarnPostDualOrders = Entry('dual/orders', ['private', 'earn'], 'POST', {'cost': 1.3333333333333333}) + private_earn_post_structured_orders = privateEarnPostStructuredOrders = Entry('structured/orders', ['private', 'earn'], 'POST', {'cost': 1.3333333333333333}) + private_earn_post_staking_swap = privateEarnPostStakingSwap = Entry('staking/swap', ['private', 'earn'], 'POST', {'cost': 1.3333333333333333}) + private_earn_put_uni_interest_reinvest = privateEarnPutUniInterestReinvest = Entry('uni/interest_reinvest', ['private', 'earn'], 'PUT', {'cost': 1.3333333333333333}) + private_earn_patch_uni_lends = privateEarnPatchUniLends = Entry('uni/lends', ['private', 'earn'], 'PATCH', {'cost': 1.3333333333333333}) + private_loan_get_collateral_orders = privateLoanGetCollateralOrders = Entry('collateral/orders', ['private', 'loan'], 'GET', {'cost': 1.3333333333333333}) + private_loan_get_collateral_orders_order_id = privateLoanGetCollateralOrdersOrderId = Entry('collateral/orders/{order_id}', ['private', 'loan'], 'GET', {'cost': 1.3333333333333333}) + private_loan_get_collateral_repay_records = privateLoanGetCollateralRepayRecords = Entry('collateral/repay_records', ['private', 'loan'], 'GET', {'cost': 1.3333333333333333}) + private_loan_get_collateral_collaterals = privateLoanGetCollateralCollaterals = Entry('collateral/collaterals', ['private', 'loan'], 'GET', {'cost': 1.3333333333333333}) + private_loan_get_collateral_total_amount = privateLoanGetCollateralTotalAmount = Entry('collateral/total_amount', ['private', 'loan'], 'GET', {'cost': 1.3333333333333333}) + private_loan_get_collateral_ltv = privateLoanGetCollateralLtv = Entry('collateral/ltv', ['private', 'loan'], 'GET', {'cost': 1.3333333333333333}) + private_loan_get_multi_collateral_orders = privateLoanGetMultiCollateralOrders = Entry('multi_collateral/orders', ['private', 'loan'], 'GET', {'cost': 1.3333333333333333}) + private_loan_get_multi_collateral_orders_order_id = privateLoanGetMultiCollateralOrdersOrderId = Entry('multi_collateral/orders/{order_id}', ['private', 'loan'], 'GET', {'cost': 1.3333333333333333}) + private_loan_get_multi_collateral_repay = privateLoanGetMultiCollateralRepay = Entry('multi_collateral/repay', ['private', 'loan'], 'GET', {'cost': 1.3333333333333333}) + private_loan_get_multi_collateral_mortgage = privateLoanGetMultiCollateralMortgage = Entry('multi_collateral/mortgage', ['private', 'loan'], 'GET', {'cost': 1.3333333333333333}) + private_loan_get_multi_collateral_currency_quota = privateLoanGetMultiCollateralCurrencyQuota = Entry('multi_collateral/currency_quota', ['private', 'loan'], 'GET', {'cost': 1.3333333333333333}) + private_loan_get_collateral_currencies = privateLoanGetCollateralCurrencies = Entry('collateral/currencies', ['private', 'loan'], 'GET', {'cost': 1.3333333333333333}) + private_loan_get_multi_collateral_currencies = privateLoanGetMultiCollateralCurrencies = Entry('multi_collateral/currencies', ['private', 'loan'], 'GET', {'cost': 1.3333333333333333}) + private_loan_get_multi_collateral_ltv = privateLoanGetMultiCollateralLtv = Entry('multi_collateral/ltv', ['private', 'loan'], 'GET', {'cost': 1.3333333333333333}) + private_loan_get_multi_collateral_fixed_rate = privateLoanGetMultiCollateralFixedRate = Entry('multi_collateral/fixed_rate', ['private', 'loan'], 'GET', {'cost': 1.3333333333333333}) + private_loan_get_multi_collateral_current_rate = privateLoanGetMultiCollateralCurrentRate = Entry('multi_collateral/current_rate', ['private', 'loan'], 'GET', {'cost': 1.3333333333333333}) + private_loan_post_collateral_orders = privateLoanPostCollateralOrders = Entry('collateral/orders', ['private', 'loan'], 'POST', {'cost': 1.3333333333333333}) + private_loan_post_collateral_repay = privateLoanPostCollateralRepay = Entry('collateral/repay', ['private', 'loan'], 'POST', {'cost': 1.3333333333333333}) + private_loan_post_collateral_collaterals = privateLoanPostCollateralCollaterals = Entry('collateral/collaterals', ['private', 'loan'], 'POST', {'cost': 1.3333333333333333}) + private_loan_post_multi_collateral_orders = privateLoanPostMultiCollateralOrders = Entry('multi_collateral/orders', ['private', 'loan'], 'POST', {'cost': 1.3333333333333333}) + private_loan_post_multi_collateral_repay = privateLoanPostMultiCollateralRepay = Entry('multi_collateral/repay', ['private', 'loan'], 'POST', {'cost': 1.3333333333333333}) + private_loan_post_multi_collateral_mortgage = privateLoanPostMultiCollateralMortgage = Entry('multi_collateral/mortgage', ['private', 'loan'], 'POST', {'cost': 1.3333333333333333}) + private_account_get_detail = privateAccountGetDetail = Entry('detail', ['private', 'account'], 'GET', {'cost': 1.3333333333333333}) + private_account_get_main_keys = privateAccountGetMainKeys = Entry('main_keys', ['private', 'account'], 'GET', {'cost': 1.3333333333333333}) + private_account_get_rate_limit = privateAccountGetRateLimit = Entry('rate_limit', ['private', 'account'], 'GET', {'cost': 1.3333333333333333}) + private_account_get_stp_groups = privateAccountGetStpGroups = Entry('stp_groups', ['private', 'account'], 'GET', {'cost': 1.3333333333333333}) + private_account_get_stp_groups_stp_id_users = privateAccountGetStpGroupsStpIdUsers = Entry('stp_groups/{stp_id}/users', ['private', 'account'], 'GET', {'cost': 1.3333333333333333}) + private_account_get_stp_groups_debit_fee = privateAccountGetStpGroupsDebitFee = Entry('stp_groups/debit_fee', ['private', 'account'], 'GET', {'cost': 1.3333333333333333}) + private_account_get_debit_fee = privateAccountGetDebitFee = Entry('debit_fee', ['private', 'account'], 'GET', {'cost': 1.3333333333333333}) + private_account_post_stp_groups = privateAccountPostStpGroups = Entry('stp_groups', ['private', 'account'], 'POST', {'cost': 1.3333333333333333}) + private_account_post_stp_groups_stp_id_users = privateAccountPostStpGroupsStpIdUsers = Entry('stp_groups/{stp_id}/users', ['private', 'account'], 'POST', {'cost': 1.3333333333333333}) + private_account_post_debit_fee = privateAccountPostDebitFee = Entry('debit_fee', ['private', 'account'], 'POST', {'cost': 1.3333333333333333}) + private_account_delete_stp_groups_stp_id_users = privateAccountDeleteStpGroupsStpIdUsers = Entry('stp_groups/{stp_id}/users', ['private', 'account'], 'DELETE', {'cost': 1.3333333333333333}) + private_rebate_get_agency_transaction_history = privateRebateGetAgencyTransactionHistory = Entry('agency/transaction_history', ['private', 'rebate'], 'GET', {'cost': 1.3333333333333333}) + private_rebate_get_agency_commission_history = privateRebateGetAgencyCommissionHistory = Entry('agency/commission_history', ['private', 'rebate'], 'GET', {'cost': 1.3333333333333333}) + private_rebate_get_partner_transaction_history = privateRebateGetPartnerTransactionHistory = Entry('partner/transaction_history', ['private', 'rebate'], 'GET', {'cost': 1.3333333333333333}) + private_rebate_get_partner_commission_history = privateRebateGetPartnerCommissionHistory = Entry('partner/commission_history', ['private', 'rebate'], 'GET', {'cost': 1.3333333333333333}) + private_rebate_get_partner_sub_list = privateRebateGetPartnerSubList = Entry('partner/sub_list', ['private', 'rebate'], 'GET', {'cost': 1.3333333333333333}) + private_rebate_get_broker_commission_history = privateRebateGetBrokerCommissionHistory = Entry('broker/commission_history', ['private', 'rebate'], 'GET', {'cost': 1.3333333333333333}) + private_rebate_get_broker_transaction_history = privateRebateGetBrokerTransactionHistory = Entry('broker/transaction_history', ['private', 'rebate'], 'GET', {'cost': 1.3333333333333333}) + private_rebate_get_user_info = privateRebateGetUserInfo = Entry('user/info', ['private', 'rebate'], 'GET', {'cost': 1.3333333333333333}) + private_rebate_get_user_sub_relation = privateRebateGetUserSubRelation = Entry('user/sub_relation', ['private', 'rebate'], 'GET', {'cost': 1.3333333333333333}) diff --git a/ccxt/abstract/gateio.py b/ccxt/abstract/gateio.py new file mode 100644 index 0000000..1103c2a --- /dev/null +++ b/ccxt/abstract/gateio.py @@ -0,0 +1,328 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_wallet_get_currency_chains = publicWalletGetCurrencyChains = Entry('currency_chains', ['public', 'wallet'], 'GET', {'cost': 1}) + public_unified_get_currencies = publicUnifiedGetCurrencies = Entry('currencies', ['public', 'unified'], 'GET', {'cost': 1}) + public_unified_get_history_loan_rate = publicUnifiedGetHistoryLoanRate = Entry('history_loan_rate', ['public', 'unified'], 'GET', {'cost': 1}) + public_spot_get_currencies = publicSpotGetCurrencies = Entry('currencies', ['public', 'spot'], 'GET', {'cost': 1}) + public_spot_get_currencies_currency = publicSpotGetCurrenciesCurrency = Entry('currencies/{currency}', ['public', 'spot'], 'GET', {'cost': 1}) + public_spot_get_currency_pairs = publicSpotGetCurrencyPairs = Entry('currency_pairs', ['public', 'spot'], 'GET', {'cost': 1}) + public_spot_get_currency_pairs_currency_pair = publicSpotGetCurrencyPairsCurrencyPair = Entry('currency_pairs/{currency_pair}', ['public', 'spot'], 'GET', {'cost': 1}) + public_spot_get_tickers = publicSpotGetTickers = Entry('tickers', ['public', 'spot'], 'GET', {'cost': 1}) + public_spot_get_order_book = publicSpotGetOrderBook = Entry('order_book', ['public', 'spot'], 'GET', {'cost': 1}) + public_spot_get_trades = publicSpotGetTrades = Entry('trades', ['public', 'spot'], 'GET', {'cost': 1}) + public_spot_get_candlesticks = publicSpotGetCandlesticks = Entry('candlesticks', ['public', 'spot'], 'GET', {'cost': 1}) + public_spot_get_time = publicSpotGetTime = Entry('time', ['public', 'spot'], 'GET', {'cost': 1}) + public_spot_get_insurance_history = publicSpotGetInsuranceHistory = Entry('insurance_history', ['public', 'spot'], 'GET', {'cost': 1}) + public_margin_get_uni_currency_pairs = publicMarginGetUniCurrencyPairs = Entry('uni/currency_pairs', ['public', 'margin'], 'GET', {'cost': 1}) + public_margin_get_uni_currency_pairs_currency_pair = publicMarginGetUniCurrencyPairsCurrencyPair = Entry('uni/currency_pairs/{currency_pair}', ['public', 'margin'], 'GET', {'cost': 1}) + public_margin_get_loan_margin_tiers = publicMarginGetLoanMarginTiers = Entry('loan_margin_tiers', ['public', 'margin'], 'GET', {'cost': 1}) + public_margin_get_currency_pairs = publicMarginGetCurrencyPairs = Entry('currency_pairs', ['public', 'margin'], 'GET', {'cost': 1}) + public_margin_get_currency_pairs_currency_pair = publicMarginGetCurrencyPairsCurrencyPair = Entry('currency_pairs/{currency_pair}', ['public', 'margin'], 'GET', {'cost': 1}) + public_margin_get_funding_book = publicMarginGetFundingBook = Entry('funding_book', ['public', 'margin'], 'GET', {'cost': 1}) + public_margin_get_cross_currencies = publicMarginGetCrossCurrencies = Entry('cross/currencies', ['public', 'margin'], 'GET', {'cost': 1}) + public_margin_get_cross_currencies_currency = publicMarginGetCrossCurrenciesCurrency = Entry('cross/currencies/{currency}', ['public', 'margin'], 'GET', {'cost': 1}) + public_flash_swap_get_currency_pairs = publicFlash_swapGetCurrencyPairs = Entry('currency_pairs', ['public', 'flash_swap'], 'GET', {'cost': 1}) + public_flash_swap_get_currencies = publicFlash_swapGetCurrencies = Entry('currencies', ['public', 'flash_swap'], 'GET', {'cost': 1}) + public_futures_get_settle_contracts = publicFuturesGetSettleContracts = Entry('{settle}/contracts', ['public', 'futures'], 'GET', {'cost': 1}) + public_futures_get_settle_contracts_contract = publicFuturesGetSettleContractsContract = Entry('{settle}/contracts/{contract}', ['public', 'futures'], 'GET', {'cost': 1}) + public_futures_get_settle_order_book = publicFuturesGetSettleOrderBook = Entry('{settle}/order_book', ['public', 'futures'], 'GET', {'cost': 1}) + public_futures_get_settle_trades = publicFuturesGetSettleTrades = Entry('{settle}/trades', ['public', 'futures'], 'GET', {'cost': 1}) + public_futures_get_settle_candlesticks = publicFuturesGetSettleCandlesticks = Entry('{settle}/candlesticks', ['public', 'futures'], 'GET', {'cost': 1}) + public_futures_get_settle_premium_index = publicFuturesGetSettlePremiumIndex = Entry('{settle}/premium_index', ['public', 'futures'], 'GET', {'cost': 1}) + public_futures_get_settle_tickers = publicFuturesGetSettleTickers = Entry('{settle}/tickers', ['public', 'futures'], 'GET', {'cost': 1}) + public_futures_get_settle_funding_rate = publicFuturesGetSettleFundingRate = Entry('{settle}/funding_rate', ['public', 'futures'], 'GET', {'cost': 1}) + public_futures_get_settle_insurance = publicFuturesGetSettleInsurance = Entry('{settle}/insurance', ['public', 'futures'], 'GET', {'cost': 1}) + public_futures_get_settle_contract_stats = publicFuturesGetSettleContractStats = Entry('{settle}/contract_stats', ['public', 'futures'], 'GET', {'cost': 1}) + public_futures_get_settle_index_constituents_index = publicFuturesGetSettleIndexConstituentsIndex = Entry('{settle}/index_constituents/{index}', ['public', 'futures'], 'GET', {'cost': 1}) + public_futures_get_settle_liq_orders = publicFuturesGetSettleLiqOrders = Entry('{settle}/liq_orders', ['public', 'futures'], 'GET', {'cost': 1}) + public_futures_get_settle_risk_limit_tiers = publicFuturesGetSettleRiskLimitTiers = Entry('{settle}/risk_limit_tiers', ['public', 'futures'], 'GET', {'cost': 1}) + public_delivery_get_settle_contracts = publicDeliveryGetSettleContracts = Entry('{settle}/contracts', ['public', 'delivery'], 'GET', {'cost': 1}) + public_delivery_get_settle_contracts_contract = publicDeliveryGetSettleContractsContract = Entry('{settle}/contracts/{contract}', ['public', 'delivery'], 'GET', {'cost': 1}) + public_delivery_get_settle_order_book = publicDeliveryGetSettleOrderBook = Entry('{settle}/order_book', ['public', 'delivery'], 'GET', {'cost': 1}) + public_delivery_get_settle_trades = publicDeliveryGetSettleTrades = Entry('{settle}/trades', ['public', 'delivery'], 'GET', {'cost': 1}) + public_delivery_get_settle_candlesticks = publicDeliveryGetSettleCandlesticks = Entry('{settle}/candlesticks', ['public', 'delivery'], 'GET', {'cost': 1}) + public_delivery_get_settle_tickers = publicDeliveryGetSettleTickers = Entry('{settle}/tickers', ['public', 'delivery'], 'GET', {'cost': 1}) + public_delivery_get_settle_insurance = publicDeliveryGetSettleInsurance = Entry('{settle}/insurance', ['public', 'delivery'], 'GET', {'cost': 1}) + public_delivery_get_settle_risk_limit_tiers = publicDeliveryGetSettleRiskLimitTiers = Entry('{settle}/risk_limit_tiers', ['public', 'delivery'], 'GET', {'cost': 1}) + public_options_get_underlyings = publicOptionsGetUnderlyings = Entry('underlyings', ['public', 'options'], 'GET', {'cost': 1}) + public_options_get_expirations = publicOptionsGetExpirations = Entry('expirations', ['public', 'options'], 'GET', {'cost': 1}) + public_options_get_contracts = publicOptionsGetContracts = Entry('contracts', ['public', 'options'], 'GET', {'cost': 1}) + public_options_get_contracts_contract = publicOptionsGetContractsContract = Entry('contracts/{contract}', ['public', 'options'], 'GET', {'cost': 1}) + public_options_get_settlements = publicOptionsGetSettlements = Entry('settlements', ['public', 'options'], 'GET', {'cost': 1}) + public_options_get_settlements_contract = publicOptionsGetSettlementsContract = Entry('settlements/{contract}', ['public', 'options'], 'GET', {'cost': 1}) + public_options_get_order_book = publicOptionsGetOrderBook = Entry('order_book', ['public', 'options'], 'GET', {'cost': 1}) + public_options_get_tickers = publicOptionsGetTickers = Entry('tickers', ['public', 'options'], 'GET', {'cost': 1}) + public_options_get_underlying_tickers_underlying = publicOptionsGetUnderlyingTickersUnderlying = Entry('underlying/tickers/{underlying}', ['public', 'options'], 'GET', {'cost': 1}) + public_options_get_candlesticks = publicOptionsGetCandlesticks = Entry('candlesticks', ['public', 'options'], 'GET', {'cost': 1}) + public_options_get_underlying_candlesticks = publicOptionsGetUnderlyingCandlesticks = Entry('underlying/candlesticks', ['public', 'options'], 'GET', {'cost': 1}) + public_options_get_trades = publicOptionsGetTrades = Entry('trades', ['public', 'options'], 'GET', {'cost': 1}) + public_earn_get_uni_currencies = publicEarnGetUniCurrencies = Entry('uni/currencies', ['public', 'earn'], 'GET', {'cost': 1}) + public_earn_get_uni_currencies_currency = publicEarnGetUniCurrenciesCurrency = Entry('uni/currencies/{currency}', ['public', 'earn'], 'GET', {'cost': 1}) + public_earn_get_dual_investment_plan = publicEarnGetDualInvestmentPlan = Entry('dual/investment_plan', ['public', 'earn'], 'GET', {'cost': 1}) + public_earn_get_structured_products = publicEarnGetStructuredProducts = Entry('structured/products', ['public', 'earn'], 'GET', {'cost': 1}) + public_loan_get_collateral_currencies = publicLoanGetCollateralCurrencies = Entry('collateral/currencies', ['public', 'loan'], 'GET', {'cost': 1}) + public_loan_get_multi_collateral_currencies = publicLoanGetMultiCollateralCurrencies = Entry('multi_collateral/currencies', ['public', 'loan'], 'GET', {'cost': 1}) + public_loan_get_multi_collateral_ltv = publicLoanGetMultiCollateralLtv = Entry('multi_collateral/ltv', ['public', 'loan'], 'GET', {'cost': 1}) + public_loan_get_multi_collateral_fixed_rate = publicLoanGetMultiCollateralFixedRate = Entry('multi_collateral/fixed_rate', ['public', 'loan'], 'GET', {'cost': 1}) + public_loan_get_multi_collateral_current_rate = publicLoanGetMultiCollateralCurrentRate = Entry('multi_collateral/current_rate', ['public', 'loan'], 'GET', {'cost': 1}) + private_withdrawals_post_withdrawals = privateWithdrawalsPostWithdrawals = Entry('withdrawals', ['private', 'withdrawals'], 'POST', {'cost': 20}) + private_withdrawals_post_push = privateWithdrawalsPostPush = Entry('push', ['private', 'withdrawals'], 'POST', {'cost': 1}) + private_withdrawals_delete_withdrawals_withdrawal_id = privateWithdrawalsDeleteWithdrawalsWithdrawalId = Entry('withdrawals/{withdrawal_id}', ['private', 'withdrawals'], 'DELETE', {'cost': 1}) + private_wallet_get_deposit_address = privateWalletGetDepositAddress = Entry('deposit_address', ['private', 'wallet'], 'GET', {'cost': 1}) + private_wallet_get_withdrawals = privateWalletGetWithdrawals = Entry('withdrawals', ['private', 'wallet'], 'GET', {'cost': 1}) + private_wallet_get_deposits = privateWalletGetDeposits = Entry('deposits', ['private', 'wallet'], 'GET', {'cost': 1}) + private_wallet_get_sub_account_transfers = privateWalletGetSubAccountTransfers = Entry('sub_account_transfers', ['private', 'wallet'], 'GET', {'cost': 1}) + private_wallet_get_order_status = privateWalletGetOrderStatus = Entry('order_status', ['private', 'wallet'], 'GET', {'cost': 1}) + private_wallet_get_withdraw_status = privateWalletGetWithdrawStatus = Entry('withdraw_status', ['private', 'wallet'], 'GET', {'cost': 1}) + private_wallet_get_sub_account_balances = privateWalletGetSubAccountBalances = Entry('sub_account_balances', ['private', 'wallet'], 'GET', {'cost': 2.5}) + private_wallet_get_sub_account_margin_balances = privateWalletGetSubAccountMarginBalances = Entry('sub_account_margin_balances', ['private', 'wallet'], 'GET', {'cost': 2.5}) + private_wallet_get_sub_account_futures_balances = privateWalletGetSubAccountFuturesBalances = Entry('sub_account_futures_balances', ['private', 'wallet'], 'GET', {'cost': 2.5}) + private_wallet_get_sub_account_cross_margin_balances = privateWalletGetSubAccountCrossMarginBalances = Entry('sub_account_cross_margin_balances', ['private', 'wallet'], 'GET', {'cost': 2.5}) + private_wallet_get_saved_address = privateWalletGetSavedAddress = Entry('saved_address', ['private', 'wallet'], 'GET', {'cost': 1}) + private_wallet_get_fee = privateWalletGetFee = Entry('fee', ['private', 'wallet'], 'GET', {'cost': 1}) + private_wallet_get_total_balance = privateWalletGetTotalBalance = Entry('total_balance', ['private', 'wallet'], 'GET', {'cost': 2.5}) + private_wallet_get_small_balance = privateWalletGetSmallBalance = Entry('small_balance', ['private', 'wallet'], 'GET', {'cost': 1}) + private_wallet_get_small_balance_history = privateWalletGetSmallBalanceHistory = Entry('small_balance_history', ['private', 'wallet'], 'GET', {'cost': 1}) + private_wallet_get_push = privateWalletGetPush = Entry('push', ['private', 'wallet'], 'GET', {'cost': 1}) + private_wallet_post_transfers = privateWalletPostTransfers = Entry('transfers', ['private', 'wallet'], 'POST', {'cost': 2.5}) + private_wallet_post_sub_account_transfers = privateWalletPostSubAccountTransfers = Entry('sub_account_transfers', ['private', 'wallet'], 'POST', {'cost': 2.5}) + private_wallet_post_sub_account_to_sub_account = privateWalletPostSubAccountToSubAccount = Entry('sub_account_to_sub_account', ['private', 'wallet'], 'POST', {'cost': 2.5}) + private_wallet_post_small_balance = privateWalletPostSmallBalance = Entry('small_balance', ['private', 'wallet'], 'POST', {'cost': 1}) + private_subaccounts_get_sub_accounts = privateSubAccountsGetSubAccounts = Entry('sub_accounts', ['private', 'subAccounts'], 'GET', {'cost': 2.5}) + private_subaccounts_get_sub_accounts_user_id = privateSubAccountsGetSubAccountsUserId = Entry('sub_accounts/{user_id}', ['private', 'subAccounts'], 'GET', {'cost': 2.5}) + private_subaccounts_get_sub_accounts_user_id_keys = privateSubAccountsGetSubAccountsUserIdKeys = Entry('sub_accounts/{user_id}/keys', ['private', 'subAccounts'], 'GET', {'cost': 2.5}) + private_subaccounts_get_sub_accounts_user_id_keys_key = privateSubAccountsGetSubAccountsUserIdKeysKey = Entry('sub_accounts/{user_id}/keys/{key}', ['private', 'subAccounts'], 'GET', {'cost': 2.5}) + private_subaccounts_post_sub_accounts = privateSubAccountsPostSubAccounts = Entry('sub_accounts', ['private', 'subAccounts'], 'POST', {'cost': 2.5}) + private_subaccounts_post_sub_accounts_user_id_keys = privateSubAccountsPostSubAccountsUserIdKeys = Entry('sub_accounts/{user_id}/keys', ['private', 'subAccounts'], 'POST', {'cost': 2.5}) + private_subaccounts_post_sub_accounts_user_id_lock = privateSubAccountsPostSubAccountsUserIdLock = Entry('sub_accounts/{user_id}/lock', ['private', 'subAccounts'], 'POST', {'cost': 2.5}) + private_subaccounts_post_sub_accounts_user_id_unlock = privateSubAccountsPostSubAccountsUserIdUnlock = Entry('sub_accounts/{user_id}/unlock', ['private', 'subAccounts'], 'POST', {'cost': 2.5}) + private_subaccounts_put_sub_accounts_user_id_keys_key = privateSubAccountsPutSubAccountsUserIdKeysKey = Entry('sub_accounts/{user_id}/keys/{key}', ['private', 'subAccounts'], 'PUT', {'cost': 2.5}) + private_subaccounts_delete_sub_accounts_user_id_keys_key = privateSubAccountsDeleteSubAccountsUserIdKeysKey = Entry('sub_accounts/{user_id}/keys/{key}', ['private', 'subAccounts'], 'DELETE', {'cost': 2.5}) + private_unified_get_accounts = privateUnifiedGetAccounts = Entry('accounts', ['private', 'unified'], 'GET', {'cost': 1.3333333333333333}) + private_unified_get_borrowable = privateUnifiedGetBorrowable = Entry('borrowable', ['private', 'unified'], 'GET', {'cost': 1.3333333333333333}) + private_unified_get_transferable = privateUnifiedGetTransferable = Entry('transferable', ['private', 'unified'], 'GET', {'cost': 1.3333333333333333}) + private_unified_get_transferables = privateUnifiedGetTransferables = Entry('transferables', ['private', 'unified'], 'GET', {'cost': 1.3333333333333333}) + private_unified_get_batch_borrowable = privateUnifiedGetBatchBorrowable = Entry('batch_borrowable', ['private', 'unified'], 'GET', {'cost': 1.3333333333333333}) + private_unified_get_loans = privateUnifiedGetLoans = Entry('loans', ['private', 'unified'], 'GET', {'cost': 1.3333333333333333}) + private_unified_get_loan_records = privateUnifiedGetLoanRecords = Entry('loan_records', ['private', 'unified'], 'GET', {'cost': 1.3333333333333333}) + private_unified_get_interest_records = privateUnifiedGetInterestRecords = Entry('interest_records', ['private', 'unified'], 'GET', {'cost': 1.3333333333333333}) + private_unified_get_risk_units = privateUnifiedGetRiskUnits = Entry('risk_units', ['private', 'unified'], 'GET', {'cost': 1.3333333333333333}) + private_unified_get_unified_mode = privateUnifiedGetUnifiedMode = Entry('unified_mode', ['private', 'unified'], 'GET', {'cost': 1.3333333333333333}) + private_unified_get_estimate_rate = privateUnifiedGetEstimateRate = Entry('estimate_rate', ['private', 'unified'], 'GET', {'cost': 1.3333333333333333}) + private_unified_get_currency_discount_tiers = privateUnifiedGetCurrencyDiscountTiers = Entry('currency_discount_tiers', ['private', 'unified'], 'GET', {'cost': 1.3333333333333333}) + private_unified_get_loan_margin_tiers = privateUnifiedGetLoanMarginTiers = Entry('loan_margin_tiers', ['private', 'unified'], 'GET', {'cost': 1.3333333333333333}) + private_unified_get_leverage_user_currency_config = privateUnifiedGetLeverageUserCurrencyConfig = Entry('leverage/user_currency_config', ['private', 'unified'], 'GET', {'cost': 1.3333333333333333}) + private_unified_get_leverage_user_currency_setting = privateUnifiedGetLeverageUserCurrencySetting = Entry('leverage/user_currency_setting', ['private', 'unified'], 'GET', {'cost': 1.3333333333333333}) + private_unified_get_account_mode = privateUnifiedGetAccountMode = Entry('account_mode', ['private', 'unified'], 'GET', {'cost': 1.3333333333333333}) + private_unified_post_loans = privateUnifiedPostLoans = Entry('loans', ['private', 'unified'], 'POST', {'cost': 13.333333333333334}) + private_unified_post_portfolio_calculator = privateUnifiedPostPortfolioCalculator = Entry('portfolio_calculator', ['private', 'unified'], 'POST', {'cost': 1.3333333333333333}) + private_unified_post_leverage_user_currency_setting = privateUnifiedPostLeverageUserCurrencySetting = Entry('leverage/user_currency_setting', ['private', 'unified'], 'POST', {'cost': 1.3333333333333333}) + private_unified_post_collateral_currencies = privateUnifiedPostCollateralCurrencies = Entry('collateral_currencies', ['private', 'unified'], 'POST', {'cost': 1.3333333333333333}) + private_unified_post_account_mode = privateUnifiedPostAccountMode = Entry('account_mode', ['private', 'unified'], 'POST', {'cost': 1.3333333333333333}) + private_unified_put_unified_mode = privateUnifiedPutUnifiedMode = Entry('unified_mode', ['private', 'unified'], 'PUT', {'cost': 1.3333333333333333}) + private_spot_get_fee = privateSpotGetFee = Entry('fee', ['private', 'spot'], 'GET', {'cost': 1}) + private_spot_get_batch_fee = privateSpotGetBatchFee = Entry('batch_fee', ['private', 'spot'], 'GET', {'cost': 1}) + private_spot_get_accounts = privateSpotGetAccounts = Entry('accounts', ['private', 'spot'], 'GET', {'cost': 1}) + private_spot_get_account_book = privateSpotGetAccountBook = Entry('account_book', ['private', 'spot'], 'GET', {'cost': 1}) + private_spot_get_open_orders = privateSpotGetOpenOrders = Entry('open_orders', ['private', 'spot'], 'GET', {'cost': 1}) + private_spot_get_orders = privateSpotGetOrders = Entry('orders', ['private', 'spot'], 'GET', {'cost': 1}) + private_spot_get_orders_order_id = privateSpotGetOrdersOrderId = Entry('orders/{order_id}', ['private', 'spot'], 'GET', {'cost': 1}) + private_spot_get_my_trades = privateSpotGetMyTrades = Entry('my_trades', ['private', 'spot'], 'GET', {'cost': 1}) + private_spot_get_price_orders = privateSpotGetPriceOrders = Entry('price_orders', ['private', 'spot'], 'GET', {'cost': 1}) + private_spot_get_price_orders_order_id = privateSpotGetPriceOrdersOrderId = Entry('price_orders/{order_id}', ['private', 'spot'], 'GET', {'cost': 1}) + private_spot_post_batch_orders = privateSpotPostBatchOrders = Entry('batch_orders', ['private', 'spot'], 'POST', {'cost': 0.4}) + private_spot_post_cross_liquidate_orders = privateSpotPostCrossLiquidateOrders = Entry('cross_liquidate_orders', ['private', 'spot'], 'POST', {'cost': 1}) + private_spot_post_orders = privateSpotPostOrders = Entry('orders', ['private', 'spot'], 'POST', {'cost': 0.4}) + private_spot_post_cancel_batch_orders = privateSpotPostCancelBatchOrders = Entry('cancel_batch_orders', ['private', 'spot'], 'POST', {'cost': 0.26666666666666666}) + private_spot_post_countdown_cancel_all = privateSpotPostCountdownCancelAll = Entry('countdown_cancel_all', ['private', 'spot'], 'POST', {'cost': 0.26666666666666666}) + private_spot_post_amend_batch_orders = privateSpotPostAmendBatchOrders = Entry('amend_batch_orders', ['private', 'spot'], 'POST', {'cost': 0.4}) + private_spot_post_price_orders = privateSpotPostPriceOrders = Entry('price_orders', ['private', 'spot'], 'POST', {'cost': 0.4}) + private_spot_delete_orders = privateSpotDeleteOrders = Entry('orders', ['private', 'spot'], 'DELETE', {'cost': 0.26666666666666666}) + private_spot_delete_orders_order_id = privateSpotDeleteOrdersOrderId = Entry('orders/{order_id}', ['private', 'spot'], 'DELETE', {'cost': 0.26666666666666666}) + private_spot_delete_price_orders = privateSpotDeletePriceOrders = Entry('price_orders', ['private', 'spot'], 'DELETE', {'cost': 0.26666666666666666}) + private_spot_delete_price_orders_order_id = privateSpotDeletePriceOrdersOrderId = Entry('price_orders/{order_id}', ['private', 'spot'], 'DELETE', {'cost': 0.26666666666666666}) + private_spot_patch_orders_order_id = privateSpotPatchOrdersOrderId = Entry('orders/{order_id}', ['private', 'spot'], 'PATCH', {'cost': 0.4}) + private_margin_get_accounts = privateMarginGetAccounts = Entry('accounts', ['private', 'margin'], 'GET', {'cost': 1.3333333333333333}) + private_margin_get_account_book = privateMarginGetAccountBook = Entry('account_book', ['private', 'margin'], 'GET', {'cost': 1.3333333333333333}) + private_margin_get_funding_accounts = privateMarginGetFundingAccounts = Entry('funding_accounts', ['private', 'margin'], 'GET', {'cost': 1.3333333333333333}) + private_margin_get_auto_repay = privateMarginGetAutoRepay = Entry('auto_repay', ['private', 'margin'], 'GET', {'cost': 1.3333333333333333}) + private_margin_get_transferable = privateMarginGetTransferable = Entry('transferable', ['private', 'margin'], 'GET', {'cost': 1.3333333333333333}) + private_margin_get_uni_estimate_rate = privateMarginGetUniEstimateRate = Entry('uni/estimate_rate', ['private', 'margin'], 'GET', {'cost': 1.3333333333333333}) + private_margin_get_uni_loans = privateMarginGetUniLoans = Entry('uni/loans', ['private', 'margin'], 'GET', {'cost': 1.3333333333333333}) + private_margin_get_uni_loan_records = privateMarginGetUniLoanRecords = Entry('uni/loan_records', ['private', 'margin'], 'GET', {'cost': 1.3333333333333333}) + private_margin_get_uni_interest_records = privateMarginGetUniInterestRecords = Entry('uni/interest_records', ['private', 'margin'], 'GET', {'cost': 1.3333333333333333}) + private_margin_get_uni_borrowable = privateMarginGetUniBorrowable = Entry('uni/borrowable', ['private', 'margin'], 'GET', {'cost': 1.3333333333333333}) + private_margin_get_user_loan_margin_tiers = privateMarginGetUserLoanMarginTiers = Entry('user/loan_margin_tiers', ['private', 'margin'], 'GET', {'cost': 1.3333333333333333}) + private_margin_get_user_account = privateMarginGetUserAccount = Entry('user/account', ['private', 'margin'], 'GET', {'cost': 1.3333333333333333}) + private_margin_get_loans = privateMarginGetLoans = Entry('loans', ['private', 'margin'], 'GET', {'cost': 1.3333333333333333}) + private_margin_get_loans_loan_id = privateMarginGetLoansLoanId = Entry('loans/{loan_id}', ['private', 'margin'], 'GET', {'cost': 1.3333333333333333}) + private_margin_get_loans_loan_id_repayment = privateMarginGetLoansLoanIdRepayment = Entry('loans/{loan_id}/repayment', ['private', 'margin'], 'GET', {'cost': 1.3333333333333333}) + private_margin_get_loan_records = privateMarginGetLoanRecords = Entry('loan_records', ['private', 'margin'], 'GET', {'cost': 1.3333333333333333}) + private_margin_get_loan_records_loan_record_id = privateMarginGetLoanRecordsLoanRecordId = Entry('loan_records/{loan_record_id}', ['private', 'margin'], 'GET', {'cost': 1.3333333333333333}) + private_margin_get_borrowable = privateMarginGetBorrowable = Entry('borrowable', ['private', 'margin'], 'GET', {'cost': 1.3333333333333333}) + private_margin_get_cross_accounts = privateMarginGetCrossAccounts = Entry('cross/accounts', ['private', 'margin'], 'GET', {'cost': 1.3333333333333333}) + private_margin_get_cross_account_book = privateMarginGetCrossAccountBook = Entry('cross/account_book', ['private', 'margin'], 'GET', {'cost': 1.3333333333333333}) + private_margin_get_cross_loans = privateMarginGetCrossLoans = Entry('cross/loans', ['private', 'margin'], 'GET', {'cost': 1.3333333333333333}) + private_margin_get_cross_loans_loan_id = privateMarginGetCrossLoansLoanId = Entry('cross/loans/{loan_id}', ['private', 'margin'], 'GET', {'cost': 1.3333333333333333}) + private_margin_get_cross_repayments = privateMarginGetCrossRepayments = Entry('cross/repayments', ['private', 'margin'], 'GET', {'cost': 1.3333333333333333}) + private_margin_get_cross_interest_records = privateMarginGetCrossInterestRecords = Entry('cross/interest_records', ['private', 'margin'], 'GET', {'cost': 1.3333333333333333}) + private_margin_get_cross_transferable = privateMarginGetCrossTransferable = Entry('cross/transferable', ['private', 'margin'], 'GET', {'cost': 1.3333333333333333}) + private_margin_get_cross_estimate_rate = privateMarginGetCrossEstimateRate = Entry('cross/estimate_rate', ['private', 'margin'], 'GET', {'cost': 1.3333333333333333}) + private_margin_get_cross_borrowable = privateMarginGetCrossBorrowable = Entry('cross/borrowable', ['private', 'margin'], 'GET', {'cost': 1.3333333333333333}) + private_margin_post_auto_repay = privateMarginPostAutoRepay = Entry('auto_repay', ['private', 'margin'], 'POST', {'cost': 1.3333333333333333}) + private_margin_post_uni_loans = privateMarginPostUniLoans = Entry('uni/loans', ['private', 'margin'], 'POST', {'cost': 1.3333333333333333}) + private_margin_post_leverage_user_market_setting = privateMarginPostLeverageUserMarketSetting = Entry('leverage/user_market_setting', ['private', 'margin'], 'POST', {'cost': 1.3333333333333333}) + private_margin_post_loans = privateMarginPostLoans = Entry('loans', ['private', 'margin'], 'POST', {'cost': 1.3333333333333333}) + private_margin_post_merged_loans = privateMarginPostMergedLoans = Entry('merged_loans', ['private', 'margin'], 'POST', {'cost': 1.3333333333333333}) + private_margin_post_loans_loan_id_repayment = privateMarginPostLoansLoanIdRepayment = Entry('loans/{loan_id}/repayment', ['private', 'margin'], 'POST', {'cost': 1.3333333333333333}) + private_margin_post_cross_loans = privateMarginPostCrossLoans = Entry('cross/loans', ['private', 'margin'], 'POST', {'cost': 1.3333333333333333}) + private_margin_post_cross_repayments = privateMarginPostCrossRepayments = Entry('cross/repayments', ['private', 'margin'], 'POST', {'cost': 1.3333333333333333}) + private_margin_patch_loans_loan_id = privateMarginPatchLoansLoanId = Entry('loans/{loan_id}', ['private', 'margin'], 'PATCH', {'cost': 1.3333333333333333}) + private_margin_patch_loan_records_loan_record_id = privateMarginPatchLoanRecordsLoanRecordId = Entry('loan_records/{loan_record_id}', ['private', 'margin'], 'PATCH', {'cost': 1.3333333333333333}) + private_margin_delete_loans_loan_id = privateMarginDeleteLoansLoanId = Entry('loans/{loan_id}', ['private', 'margin'], 'DELETE', {'cost': 1.3333333333333333}) + private_flash_swap_get_orders = privateFlash_swapGetOrders = Entry('orders', ['private', 'flash_swap'], 'GET', {'cost': 1}) + private_flash_swap_get_orders_order_id = privateFlash_swapGetOrdersOrderId = Entry('orders/{order_id}', ['private', 'flash_swap'], 'GET', {'cost': 1}) + private_flash_swap_post_orders = privateFlash_swapPostOrders = Entry('orders', ['private', 'flash_swap'], 'POST', {'cost': 1}) + private_flash_swap_post_orders_preview = privateFlash_swapPostOrdersPreview = Entry('orders/preview', ['private', 'flash_swap'], 'POST', {'cost': 1}) + private_futures_get_settle_accounts = privateFuturesGetSettleAccounts = Entry('{settle}/accounts', ['private', 'futures'], 'GET', {'cost': 1}) + private_futures_get_settle_account_book = privateFuturesGetSettleAccountBook = Entry('{settle}/account_book', ['private', 'futures'], 'GET', {'cost': 1}) + private_futures_get_settle_positions = privateFuturesGetSettlePositions = Entry('{settle}/positions', ['private', 'futures'], 'GET', {'cost': 1}) + private_futures_get_settle_positions_contract = privateFuturesGetSettlePositionsContract = Entry('{settle}/positions/{contract}', ['private', 'futures'], 'GET', {'cost': 1}) + private_futures_get_settle_dual_comp_positions_contract = privateFuturesGetSettleDualCompPositionsContract = Entry('{settle}/dual_comp/positions/{contract}', ['private', 'futures'], 'GET', {'cost': 1}) + private_futures_get_settle_orders = privateFuturesGetSettleOrders = Entry('{settle}/orders', ['private', 'futures'], 'GET', {'cost': 1}) + private_futures_get_settle_orders_timerange = privateFuturesGetSettleOrdersTimerange = Entry('{settle}/orders_timerange', ['private', 'futures'], 'GET', {'cost': 1}) + private_futures_get_settle_orders_order_id = privateFuturesGetSettleOrdersOrderId = Entry('{settle}/orders/{order_id}', ['private', 'futures'], 'GET', {'cost': 1}) + private_futures_get_settle_my_trades = privateFuturesGetSettleMyTrades = Entry('{settle}/my_trades', ['private', 'futures'], 'GET', {'cost': 1}) + private_futures_get_settle_my_trades_timerange = privateFuturesGetSettleMyTradesTimerange = Entry('{settle}/my_trades_timerange', ['private', 'futures'], 'GET', {'cost': 1}) + private_futures_get_settle_position_close = privateFuturesGetSettlePositionClose = Entry('{settle}/position_close', ['private', 'futures'], 'GET', {'cost': 1}) + private_futures_get_settle_liquidates = privateFuturesGetSettleLiquidates = Entry('{settle}/liquidates', ['private', 'futures'], 'GET', {'cost': 1}) + private_futures_get_settle_auto_deleverages = privateFuturesGetSettleAutoDeleverages = Entry('{settle}/auto_deleverages', ['private', 'futures'], 'GET', {'cost': 1}) + private_futures_get_settle_fee = privateFuturesGetSettleFee = Entry('{settle}/fee', ['private', 'futures'], 'GET', {'cost': 1}) + private_futures_get_settle_risk_limit_table = privateFuturesGetSettleRiskLimitTable = Entry('{settle}/risk_limit_table', ['private', 'futures'], 'GET', {'cost': 1}) + private_futures_get_settle_price_orders = privateFuturesGetSettlePriceOrders = Entry('{settle}/price_orders', ['private', 'futures'], 'GET', {'cost': 1}) + private_futures_get_settle_price_orders_order_id = privateFuturesGetSettlePriceOrdersOrderId = Entry('{settle}/price_orders/{order_id}', ['private', 'futures'], 'GET', {'cost': 1}) + private_futures_post_settle_positions_contract_margin = privateFuturesPostSettlePositionsContractMargin = Entry('{settle}/positions/{contract}/margin', ['private', 'futures'], 'POST', {'cost': 1}) + private_futures_post_settle_positions_contract_leverage = privateFuturesPostSettlePositionsContractLeverage = Entry('{settle}/positions/{contract}/leverage', ['private', 'futures'], 'POST', {'cost': 1}) + private_futures_post_settle_positions_contract_risk_limit = privateFuturesPostSettlePositionsContractRiskLimit = Entry('{settle}/positions/{contract}/risk_limit', ['private', 'futures'], 'POST', {'cost': 1}) + private_futures_post_settle_positions_cross_mode = privateFuturesPostSettlePositionsCrossMode = Entry('{settle}/positions/cross_mode', ['private', 'futures'], 'POST', {'cost': 1}) + private_futures_post_settle_dual_comp_positions_cross_mode = privateFuturesPostSettleDualCompPositionsCrossMode = Entry('{settle}/dual_comp/positions/cross_mode', ['private', 'futures'], 'POST', {'cost': 1}) + private_futures_post_settle_dual_mode = privateFuturesPostSettleDualMode = Entry('{settle}/dual_mode', ['private', 'futures'], 'POST', {'cost': 1}) + private_futures_post_settle_dual_comp_positions_contract_margin = privateFuturesPostSettleDualCompPositionsContractMargin = Entry('{settle}/dual_comp/positions/{contract}/margin', ['private', 'futures'], 'POST', {'cost': 1}) + private_futures_post_settle_dual_comp_positions_contract_leverage = privateFuturesPostSettleDualCompPositionsContractLeverage = Entry('{settle}/dual_comp/positions/{contract}/leverage', ['private', 'futures'], 'POST', {'cost': 1}) + private_futures_post_settle_dual_comp_positions_contract_risk_limit = privateFuturesPostSettleDualCompPositionsContractRiskLimit = Entry('{settle}/dual_comp/positions/{contract}/risk_limit', ['private', 'futures'], 'POST', {'cost': 1}) + private_futures_post_settle_orders = privateFuturesPostSettleOrders = Entry('{settle}/orders', ['private', 'futures'], 'POST', {'cost': 0.4}) + private_futures_post_settle_batch_orders = privateFuturesPostSettleBatchOrders = Entry('{settle}/batch_orders', ['private', 'futures'], 'POST', {'cost': 0.4}) + private_futures_post_settle_countdown_cancel_all = privateFuturesPostSettleCountdownCancelAll = Entry('{settle}/countdown_cancel_all', ['private', 'futures'], 'POST', {'cost': 0.4}) + private_futures_post_settle_batch_cancel_orders = privateFuturesPostSettleBatchCancelOrders = Entry('{settle}/batch_cancel_orders', ['private', 'futures'], 'POST', {'cost': 0.4}) + private_futures_post_settle_batch_amend_orders = privateFuturesPostSettleBatchAmendOrders = Entry('{settle}/batch_amend_orders', ['private', 'futures'], 'POST', {'cost': 0.4}) + private_futures_post_settle_bbo_orders = privateFuturesPostSettleBboOrders = Entry('{settle}/bbo_orders', ['private', 'futures'], 'POST', {'cost': 0.4}) + private_futures_post_settle_price_orders = privateFuturesPostSettlePriceOrders = Entry('{settle}/price_orders', ['private', 'futures'], 'POST', {'cost': 0.4}) + private_futures_put_settle_orders_order_id = privateFuturesPutSettleOrdersOrderId = Entry('{settle}/orders/{order_id}', ['private', 'futures'], 'PUT', {'cost': 1}) + private_futures_delete_settle_orders = privateFuturesDeleteSettleOrders = Entry('{settle}/orders', ['private', 'futures'], 'DELETE', {'cost': 0.26666666666666666}) + private_futures_delete_settle_orders_order_id = privateFuturesDeleteSettleOrdersOrderId = Entry('{settle}/orders/{order_id}', ['private', 'futures'], 'DELETE', {'cost': 0.26666666666666666}) + private_futures_delete_settle_price_orders = privateFuturesDeleteSettlePriceOrders = Entry('{settle}/price_orders', ['private', 'futures'], 'DELETE', {'cost': 0.26666666666666666}) + private_futures_delete_settle_price_orders_order_id = privateFuturesDeleteSettlePriceOrdersOrderId = Entry('{settle}/price_orders/{order_id}', ['private', 'futures'], 'DELETE', {'cost': 0.26666666666666666}) + private_delivery_get_settle_accounts = privateDeliveryGetSettleAccounts = Entry('{settle}/accounts', ['private', 'delivery'], 'GET', {'cost': 1.3333333333333333}) + private_delivery_get_settle_account_book = privateDeliveryGetSettleAccountBook = Entry('{settle}/account_book', ['private', 'delivery'], 'GET', {'cost': 1.3333333333333333}) + private_delivery_get_settle_positions = privateDeliveryGetSettlePositions = Entry('{settle}/positions', ['private', 'delivery'], 'GET', {'cost': 1.3333333333333333}) + private_delivery_get_settle_positions_contract = privateDeliveryGetSettlePositionsContract = Entry('{settle}/positions/{contract}', ['private', 'delivery'], 'GET', {'cost': 1.3333333333333333}) + private_delivery_get_settle_orders = privateDeliveryGetSettleOrders = Entry('{settle}/orders', ['private', 'delivery'], 'GET', {'cost': 1.3333333333333333}) + private_delivery_get_settle_orders_order_id = privateDeliveryGetSettleOrdersOrderId = Entry('{settle}/orders/{order_id}', ['private', 'delivery'], 'GET', {'cost': 1.3333333333333333}) + private_delivery_get_settle_my_trades = privateDeliveryGetSettleMyTrades = Entry('{settle}/my_trades', ['private', 'delivery'], 'GET', {'cost': 1.3333333333333333}) + private_delivery_get_settle_position_close = privateDeliveryGetSettlePositionClose = Entry('{settle}/position_close', ['private', 'delivery'], 'GET', {'cost': 1.3333333333333333}) + private_delivery_get_settle_liquidates = privateDeliveryGetSettleLiquidates = Entry('{settle}/liquidates', ['private', 'delivery'], 'GET', {'cost': 1.3333333333333333}) + private_delivery_get_settle_settlements = privateDeliveryGetSettleSettlements = Entry('{settle}/settlements', ['private', 'delivery'], 'GET', {'cost': 1.3333333333333333}) + private_delivery_get_settle_price_orders = privateDeliveryGetSettlePriceOrders = Entry('{settle}/price_orders', ['private', 'delivery'], 'GET', {'cost': 1.3333333333333333}) + private_delivery_get_settle_price_orders_order_id = privateDeliveryGetSettlePriceOrdersOrderId = Entry('{settle}/price_orders/{order_id}', ['private', 'delivery'], 'GET', {'cost': 1.3333333333333333}) + private_delivery_post_settle_positions_contract_margin = privateDeliveryPostSettlePositionsContractMargin = Entry('{settle}/positions/{contract}/margin', ['private', 'delivery'], 'POST', {'cost': 1.3333333333333333}) + private_delivery_post_settle_positions_contract_leverage = privateDeliveryPostSettlePositionsContractLeverage = Entry('{settle}/positions/{contract}/leverage', ['private', 'delivery'], 'POST', {'cost': 1.3333333333333333}) + private_delivery_post_settle_positions_contract_risk_limit = privateDeliveryPostSettlePositionsContractRiskLimit = Entry('{settle}/positions/{contract}/risk_limit', ['private', 'delivery'], 'POST', {'cost': 1.3333333333333333}) + private_delivery_post_settle_orders = privateDeliveryPostSettleOrders = Entry('{settle}/orders', ['private', 'delivery'], 'POST', {'cost': 1.3333333333333333}) + private_delivery_post_settle_price_orders = privateDeliveryPostSettlePriceOrders = Entry('{settle}/price_orders', ['private', 'delivery'], 'POST', {'cost': 1.3333333333333333}) + private_delivery_delete_settle_orders = privateDeliveryDeleteSettleOrders = Entry('{settle}/orders', ['private', 'delivery'], 'DELETE', {'cost': 1.3333333333333333}) + private_delivery_delete_settle_orders_order_id = privateDeliveryDeleteSettleOrdersOrderId = Entry('{settle}/orders/{order_id}', ['private', 'delivery'], 'DELETE', {'cost': 1.3333333333333333}) + private_delivery_delete_settle_price_orders = privateDeliveryDeleteSettlePriceOrders = Entry('{settle}/price_orders', ['private', 'delivery'], 'DELETE', {'cost': 1.3333333333333333}) + private_delivery_delete_settle_price_orders_order_id = privateDeliveryDeleteSettlePriceOrdersOrderId = Entry('{settle}/price_orders/{order_id}', ['private', 'delivery'], 'DELETE', {'cost': 1.3333333333333333}) + private_options_get_my_settlements = privateOptionsGetMySettlements = Entry('my_settlements', ['private', 'options'], 'GET', {'cost': 1.3333333333333333}) + private_options_get_accounts = privateOptionsGetAccounts = Entry('accounts', ['private', 'options'], 'GET', {'cost': 1.3333333333333333}) + private_options_get_account_book = privateOptionsGetAccountBook = Entry('account_book', ['private', 'options'], 'GET', {'cost': 1.3333333333333333}) + private_options_get_positions = privateOptionsGetPositions = Entry('positions', ['private', 'options'], 'GET', {'cost': 1.3333333333333333}) + private_options_get_positions_contract = privateOptionsGetPositionsContract = Entry('positions/{contract}', ['private', 'options'], 'GET', {'cost': 1.3333333333333333}) + private_options_get_position_close = privateOptionsGetPositionClose = Entry('position_close', ['private', 'options'], 'GET', {'cost': 1.3333333333333333}) + private_options_get_orders = privateOptionsGetOrders = Entry('orders', ['private', 'options'], 'GET', {'cost': 1.3333333333333333}) + private_options_get_orders_order_id = privateOptionsGetOrdersOrderId = Entry('orders/{order_id}', ['private', 'options'], 'GET', {'cost': 1.3333333333333333}) + private_options_get_my_trades = privateOptionsGetMyTrades = Entry('my_trades', ['private', 'options'], 'GET', {'cost': 1.3333333333333333}) + private_options_get_mmp = privateOptionsGetMmp = Entry('mmp', ['private', 'options'], 'GET', {'cost': 1.3333333333333333}) + private_options_post_orders = privateOptionsPostOrders = Entry('orders', ['private', 'options'], 'POST', {'cost': 1.3333333333333333}) + private_options_post_countdown_cancel_all = privateOptionsPostCountdownCancelAll = Entry('countdown_cancel_all', ['private', 'options'], 'POST', {'cost': 1.3333333333333333}) + private_options_post_mmp = privateOptionsPostMmp = Entry('mmp', ['private', 'options'], 'POST', {'cost': 1.3333333333333333}) + private_options_post_mmp_reset = privateOptionsPostMmpReset = Entry('mmp/reset', ['private', 'options'], 'POST', {'cost': 1.3333333333333333}) + private_options_delete_orders = privateOptionsDeleteOrders = Entry('orders', ['private', 'options'], 'DELETE', {'cost': 1.3333333333333333}) + private_options_delete_orders_order_id = privateOptionsDeleteOrdersOrderId = Entry('orders/{order_id}', ['private', 'options'], 'DELETE', {'cost': 1.3333333333333333}) + private_earn_get_uni_lends = privateEarnGetUniLends = Entry('uni/lends', ['private', 'earn'], 'GET', {'cost': 1.3333333333333333}) + private_earn_get_uni_lend_records = privateEarnGetUniLendRecords = Entry('uni/lend_records', ['private', 'earn'], 'GET', {'cost': 1.3333333333333333}) + private_earn_get_uni_interests_currency = privateEarnGetUniInterestsCurrency = Entry('uni/interests/{currency}', ['private', 'earn'], 'GET', {'cost': 1.3333333333333333}) + private_earn_get_uni_interest_records = privateEarnGetUniInterestRecords = Entry('uni/interest_records', ['private', 'earn'], 'GET', {'cost': 1.3333333333333333}) + private_earn_get_uni_interest_status_currency = privateEarnGetUniInterestStatusCurrency = Entry('uni/interest_status/{currency}', ['private', 'earn'], 'GET', {'cost': 1.3333333333333333}) + private_earn_get_uni_chart = privateEarnGetUniChart = Entry('uni/chart', ['private', 'earn'], 'GET', {'cost': 1.3333333333333333}) + private_earn_get_uni_rate = privateEarnGetUniRate = Entry('uni/rate', ['private', 'earn'], 'GET', {'cost': 1.3333333333333333}) + private_earn_get_staking_eth2_rate_records = privateEarnGetStakingEth2RateRecords = Entry('staking/eth2/rate_records', ['private', 'earn'], 'GET', {'cost': 1.3333333333333333}) + private_earn_get_dual_orders = privateEarnGetDualOrders = Entry('dual/orders', ['private', 'earn'], 'GET', {'cost': 1.3333333333333333}) + private_earn_get_structured_orders = privateEarnGetStructuredOrders = Entry('structured/orders', ['private', 'earn'], 'GET', {'cost': 1.3333333333333333}) + private_earn_get_staking_coins = privateEarnGetStakingCoins = Entry('staking/coins', ['private', 'earn'], 'GET', {'cost': 1.3333333333333333}) + private_earn_get_staking_order_list = privateEarnGetStakingOrderList = Entry('staking/order_list', ['private', 'earn'], 'GET', {'cost': 1.3333333333333333}) + private_earn_get_staking_award_list = privateEarnGetStakingAwardList = Entry('staking/award_list', ['private', 'earn'], 'GET', {'cost': 1.3333333333333333}) + private_earn_get_staking_assets = privateEarnGetStakingAssets = Entry('staking/assets', ['private', 'earn'], 'GET', {'cost': 1.3333333333333333}) + private_earn_get_uni_currencies = privateEarnGetUniCurrencies = Entry('uni/currencies', ['private', 'earn'], 'GET', {'cost': 1.3333333333333333}) + private_earn_get_uni_currencies_currency = privateEarnGetUniCurrenciesCurrency = Entry('uni/currencies/{currency}', ['private', 'earn'], 'GET', {'cost': 1.3333333333333333}) + private_earn_post_uni_lends = privateEarnPostUniLends = Entry('uni/lends', ['private', 'earn'], 'POST', {'cost': 1.3333333333333333}) + private_earn_post_staking_eth2_swap = privateEarnPostStakingEth2Swap = Entry('staking/eth2/swap', ['private', 'earn'], 'POST', {'cost': 1.3333333333333333}) + private_earn_post_dual_orders = privateEarnPostDualOrders = Entry('dual/orders', ['private', 'earn'], 'POST', {'cost': 1.3333333333333333}) + private_earn_post_structured_orders = privateEarnPostStructuredOrders = Entry('structured/orders', ['private', 'earn'], 'POST', {'cost': 1.3333333333333333}) + private_earn_post_staking_swap = privateEarnPostStakingSwap = Entry('staking/swap', ['private', 'earn'], 'POST', {'cost': 1.3333333333333333}) + private_earn_put_uni_interest_reinvest = privateEarnPutUniInterestReinvest = Entry('uni/interest_reinvest', ['private', 'earn'], 'PUT', {'cost': 1.3333333333333333}) + private_earn_patch_uni_lends = privateEarnPatchUniLends = Entry('uni/lends', ['private', 'earn'], 'PATCH', {'cost': 1.3333333333333333}) + private_loan_get_collateral_orders = privateLoanGetCollateralOrders = Entry('collateral/orders', ['private', 'loan'], 'GET', {'cost': 1.3333333333333333}) + private_loan_get_collateral_orders_order_id = privateLoanGetCollateralOrdersOrderId = Entry('collateral/orders/{order_id}', ['private', 'loan'], 'GET', {'cost': 1.3333333333333333}) + private_loan_get_collateral_repay_records = privateLoanGetCollateralRepayRecords = Entry('collateral/repay_records', ['private', 'loan'], 'GET', {'cost': 1.3333333333333333}) + private_loan_get_collateral_collaterals = privateLoanGetCollateralCollaterals = Entry('collateral/collaterals', ['private', 'loan'], 'GET', {'cost': 1.3333333333333333}) + private_loan_get_collateral_total_amount = privateLoanGetCollateralTotalAmount = Entry('collateral/total_amount', ['private', 'loan'], 'GET', {'cost': 1.3333333333333333}) + private_loan_get_collateral_ltv = privateLoanGetCollateralLtv = Entry('collateral/ltv', ['private', 'loan'], 'GET', {'cost': 1.3333333333333333}) + private_loan_get_multi_collateral_orders = privateLoanGetMultiCollateralOrders = Entry('multi_collateral/orders', ['private', 'loan'], 'GET', {'cost': 1.3333333333333333}) + private_loan_get_multi_collateral_orders_order_id = privateLoanGetMultiCollateralOrdersOrderId = Entry('multi_collateral/orders/{order_id}', ['private', 'loan'], 'GET', {'cost': 1.3333333333333333}) + private_loan_get_multi_collateral_repay = privateLoanGetMultiCollateralRepay = Entry('multi_collateral/repay', ['private', 'loan'], 'GET', {'cost': 1.3333333333333333}) + private_loan_get_multi_collateral_mortgage = privateLoanGetMultiCollateralMortgage = Entry('multi_collateral/mortgage', ['private', 'loan'], 'GET', {'cost': 1.3333333333333333}) + private_loan_get_multi_collateral_currency_quota = privateLoanGetMultiCollateralCurrencyQuota = Entry('multi_collateral/currency_quota', ['private', 'loan'], 'GET', {'cost': 1.3333333333333333}) + private_loan_get_collateral_currencies = privateLoanGetCollateralCurrencies = Entry('collateral/currencies', ['private', 'loan'], 'GET', {'cost': 1.3333333333333333}) + private_loan_get_multi_collateral_currencies = privateLoanGetMultiCollateralCurrencies = Entry('multi_collateral/currencies', ['private', 'loan'], 'GET', {'cost': 1.3333333333333333}) + private_loan_get_multi_collateral_ltv = privateLoanGetMultiCollateralLtv = Entry('multi_collateral/ltv', ['private', 'loan'], 'GET', {'cost': 1.3333333333333333}) + private_loan_get_multi_collateral_fixed_rate = privateLoanGetMultiCollateralFixedRate = Entry('multi_collateral/fixed_rate', ['private', 'loan'], 'GET', {'cost': 1.3333333333333333}) + private_loan_get_multi_collateral_current_rate = privateLoanGetMultiCollateralCurrentRate = Entry('multi_collateral/current_rate', ['private', 'loan'], 'GET', {'cost': 1.3333333333333333}) + private_loan_post_collateral_orders = privateLoanPostCollateralOrders = Entry('collateral/orders', ['private', 'loan'], 'POST', {'cost': 1.3333333333333333}) + private_loan_post_collateral_repay = privateLoanPostCollateralRepay = Entry('collateral/repay', ['private', 'loan'], 'POST', {'cost': 1.3333333333333333}) + private_loan_post_collateral_collaterals = privateLoanPostCollateralCollaterals = Entry('collateral/collaterals', ['private', 'loan'], 'POST', {'cost': 1.3333333333333333}) + private_loan_post_multi_collateral_orders = privateLoanPostMultiCollateralOrders = Entry('multi_collateral/orders', ['private', 'loan'], 'POST', {'cost': 1.3333333333333333}) + private_loan_post_multi_collateral_repay = privateLoanPostMultiCollateralRepay = Entry('multi_collateral/repay', ['private', 'loan'], 'POST', {'cost': 1.3333333333333333}) + private_loan_post_multi_collateral_mortgage = privateLoanPostMultiCollateralMortgage = Entry('multi_collateral/mortgage', ['private', 'loan'], 'POST', {'cost': 1.3333333333333333}) + private_account_get_detail = privateAccountGetDetail = Entry('detail', ['private', 'account'], 'GET', {'cost': 1.3333333333333333}) + private_account_get_main_keys = privateAccountGetMainKeys = Entry('main_keys', ['private', 'account'], 'GET', {'cost': 1.3333333333333333}) + private_account_get_rate_limit = privateAccountGetRateLimit = Entry('rate_limit', ['private', 'account'], 'GET', {'cost': 1.3333333333333333}) + private_account_get_stp_groups = privateAccountGetStpGroups = Entry('stp_groups', ['private', 'account'], 'GET', {'cost': 1.3333333333333333}) + private_account_get_stp_groups_stp_id_users = privateAccountGetStpGroupsStpIdUsers = Entry('stp_groups/{stp_id}/users', ['private', 'account'], 'GET', {'cost': 1.3333333333333333}) + private_account_get_stp_groups_debit_fee = privateAccountGetStpGroupsDebitFee = Entry('stp_groups/debit_fee', ['private', 'account'], 'GET', {'cost': 1.3333333333333333}) + private_account_get_debit_fee = privateAccountGetDebitFee = Entry('debit_fee', ['private', 'account'], 'GET', {'cost': 1.3333333333333333}) + private_account_post_stp_groups = privateAccountPostStpGroups = Entry('stp_groups', ['private', 'account'], 'POST', {'cost': 1.3333333333333333}) + private_account_post_stp_groups_stp_id_users = privateAccountPostStpGroupsStpIdUsers = Entry('stp_groups/{stp_id}/users', ['private', 'account'], 'POST', {'cost': 1.3333333333333333}) + private_account_post_debit_fee = privateAccountPostDebitFee = Entry('debit_fee', ['private', 'account'], 'POST', {'cost': 1.3333333333333333}) + private_account_delete_stp_groups_stp_id_users = privateAccountDeleteStpGroupsStpIdUsers = Entry('stp_groups/{stp_id}/users', ['private', 'account'], 'DELETE', {'cost': 1.3333333333333333}) + private_rebate_get_agency_transaction_history = privateRebateGetAgencyTransactionHistory = Entry('agency/transaction_history', ['private', 'rebate'], 'GET', {'cost': 1.3333333333333333}) + private_rebate_get_agency_commission_history = privateRebateGetAgencyCommissionHistory = Entry('agency/commission_history', ['private', 'rebate'], 'GET', {'cost': 1.3333333333333333}) + private_rebate_get_partner_transaction_history = privateRebateGetPartnerTransactionHistory = Entry('partner/transaction_history', ['private', 'rebate'], 'GET', {'cost': 1.3333333333333333}) + private_rebate_get_partner_commission_history = privateRebateGetPartnerCommissionHistory = Entry('partner/commission_history', ['private', 'rebate'], 'GET', {'cost': 1.3333333333333333}) + private_rebate_get_partner_sub_list = privateRebateGetPartnerSubList = Entry('partner/sub_list', ['private', 'rebate'], 'GET', {'cost': 1.3333333333333333}) + private_rebate_get_broker_commission_history = privateRebateGetBrokerCommissionHistory = Entry('broker/commission_history', ['private', 'rebate'], 'GET', {'cost': 1.3333333333333333}) + private_rebate_get_broker_transaction_history = privateRebateGetBrokerTransactionHistory = Entry('broker/transaction_history', ['private', 'rebate'], 'GET', {'cost': 1.3333333333333333}) + private_rebate_get_user_info = privateRebateGetUserInfo = Entry('user/info', ['private', 'rebate'], 'GET', {'cost': 1.3333333333333333}) + private_rebate_get_user_sub_relation = privateRebateGetUserSubRelation = Entry('user/sub_relation', ['private', 'rebate'], 'GET', {'cost': 1.3333333333333333}) diff --git a/ccxt/abstract/gemini.py b/ccxt/abstract/gemini.py new file mode 100644 index 0000000..6c0b43d --- /dev/null +++ b/ccxt/abstract/gemini.py @@ -0,0 +1,59 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + webexchange_get = webExchangeGet = Entry('', 'webExchange', 'GET', {}) + web_get_rest_api = webGetRestApi = Entry('rest-api', 'web', 'GET', {}) + public_get_v1_symbols = publicGetV1Symbols = Entry('v1/symbols', 'public', 'GET', {'cost': 5}) + public_get_v1_symbols_details_symbol = publicGetV1SymbolsDetailsSymbol = Entry('v1/symbols/details/{symbol}', 'public', 'GET', {'cost': 5}) + public_get_v1_staking_rates = publicGetV1StakingRates = Entry('v1/staking/rates', 'public', 'GET', {'cost': 5}) + public_get_v1_pubticker_symbol = publicGetV1PubtickerSymbol = Entry('v1/pubticker/{symbol}', 'public', 'GET', {'cost': 5}) + public_get_v2_ticker_symbol = publicGetV2TickerSymbol = Entry('v2/ticker/{symbol}', 'public', 'GET', {'cost': 5}) + public_get_v2_candles_symbol_timeframe = publicGetV2CandlesSymbolTimeframe = Entry('v2/candles/{symbol}/{timeframe}', 'public', 'GET', {'cost': 5}) + public_get_v1_trades_symbol = publicGetV1TradesSymbol = Entry('v1/trades/{symbol}', 'public', 'GET', {'cost': 5}) + public_get_v1_auction_symbol = publicGetV1AuctionSymbol = Entry('v1/auction/{symbol}', 'public', 'GET', {'cost': 5}) + public_get_v1_auction_symbol_history = publicGetV1AuctionSymbolHistory = Entry('v1/auction/{symbol}/history', 'public', 'GET', {'cost': 5}) + public_get_v1_pricefeed = publicGetV1Pricefeed = Entry('v1/pricefeed', 'public', 'GET', {'cost': 5}) + public_get_v1_book_symbol = publicGetV1BookSymbol = Entry('v1/book/{symbol}', 'public', 'GET', {'cost': 5}) + public_get_v1_earn_rates = publicGetV1EarnRates = Entry('v1/earn/rates', 'public', 'GET', {'cost': 5}) + private_post_v1_staking_unstake = privatePostV1StakingUnstake = Entry('v1/staking/unstake', 'private', 'POST', {'cost': 1}) + private_post_v1_staking_stake = privatePostV1StakingStake = Entry('v1/staking/stake', 'private', 'POST', {'cost': 1}) + private_post_v1_staking_rewards = privatePostV1StakingRewards = Entry('v1/staking/rewards', 'private', 'POST', {'cost': 1}) + private_post_v1_staking_history = privatePostV1StakingHistory = Entry('v1/staking/history', 'private', 'POST', {'cost': 1}) + private_post_v1_order_new = privatePostV1OrderNew = Entry('v1/order/new', 'private', 'POST', {'cost': 1}) + private_post_v1_order_cancel = privatePostV1OrderCancel = Entry('v1/order/cancel', 'private', 'POST', {'cost': 1}) + private_post_v1_wrap_symbol = privatePostV1WrapSymbol = Entry('v1/wrap/{symbol}', 'private', 'POST', {'cost': 1}) + private_post_v1_order_cancel_session = privatePostV1OrderCancelSession = Entry('v1/order/cancel/session', 'private', 'POST', {'cost': 1}) + private_post_v1_order_cancel_all = privatePostV1OrderCancelAll = Entry('v1/order/cancel/all', 'private', 'POST', {'cost': 1}) + private_post_v1_order_status = privatePostV1OrderStatus = Entry('v1/order/status', 'private', 'POST', {'cost': 1}) + private_post_v1_orders = privatePostV1Orders = Entry('v1/orders', 'private', 'POST', {'cost': 1}) + private_post_v1_mytrades = privatePostV1Mytrades = Entry('v1/mytrades', 'private', 'POST', {'cost': 1}) + private_post_v1_notionalvolume = privatePostV1Notionalvolume = Entry('v1/notionalvolume', 'private', 'POST', {'cost': 1}) + private_post_v1_tradevolume = privatePostV1Tradevolume = Entry('v1/tradevolume', 'private', 'POST', {'cost': 1}) + private_post_v1_clearing_new = privatePostV1ClearingNew = Entry('v1/clearing/new', 'private', 'POST', {'cost': 1}) + private_post_v1_clearing_status = privatePostV1ClearingStatus = Entry('v1/clearing/status', 'private', 'POST', {'cost': 1}) + private_post_v1_clearing_cancel = privatePostV1ClearingCancel = Entry('v1/clearing/cancel', 'private', 'POST', {'cost': 1}) + private_post_v1_clearing_confirm = privatePostV1ClearingConfirm = Entry('v1/clearing/confirm', 'private', 'POST', {'cost': 1}) + private_post_v1_balances = privatePostV1Balances = Entry('v1/balances', 'private', 'POST', {'cost': 1}) + private_post_v1_balances_staking = privatePostV1BalancesStaking = Entry('v1/balances/staking', 'private', 'POST', {'cost': 1}) + private_post_v1_notionalbalances_currency = privatePostV1NotionalbalancesCurrency = Entry('v1/notionalbalances/{currency}', 'private', 'POST', {'cost': 1}) + private_post_v1_transfers = privatePostV1Transfers = Entry('v1/transfers', 'private', 'POST', {'cost': 1}) + private_post_v1_addresses_network = privatePostV1AddressesNetwork = Entry('v1/addresses/{network}', 'private', 'POST', {'cost': 1}) + private_post_v1_deposit_network_newaddress = privatePostV1DepositNetworkNewAddress = Entry('v1/deposit/{network}/newAddress', 'private', 'POST', {'cost': 1}) + private_post_v1_deposit_currency_newaddress = privatePostV1DepositCurrencyNewAddress = Entry('v1/deposit/{currency}/newAddress', 'private', 'POST', {'cost': 1}) + private_post_v1_withdraw_currency = privatePostV1WithdrawCurrency = Entry('v1/withdraw/{currency}', 'private', 'POST', {'cost': 1}) + private_post_v1_account_transfer_currency = privatePostV1AccountTransferCurrency = Entry('v1/account/transfer/{currency}', 'private', 'POST', {'cost': 1}) + private_post_v1_payments_addbank = privatePostV1PaymentsAddbank = Entry('v1/payments/addbank', 'private', 'POST', {'cost': 1}) + private_post_v1_payments_methods = privatePostV1PaymentsMethods = Entry('v1/payments/methods', 'private', 'POST', {'cost': 1}) + private_post_v1_payments_sen_withdraw = privatePostV1PaymentsSenWithdraw = Entry('v1/payments/sen/withdraw', 'private', 'POST', {'cost': 1}) + private_post_v1_balances_earn = privatePostV1BalancesEarn = Entry('v1/balances/earn', 'private', 'POST', {'cost': 1}) + private_post_v1_earn_interest = privatePostV1EarnInterest = Entry('v1/earn/interest', 'private', 'POST', {'cost': 1}) + private_post_v1_earn_history = privatePostV1EarnHistory = Entry('v1/earn/history', 'private', 'POST', {'cost': 1}) + private_post_v1_approvedaddresses_network_request = privatePostV1ApprovedAddressesNetworkRequest = Entry('v1/approvedAddresses/{network}/request', 'private', 'POST', {'cost': 1}) + private_post_v1_approvedaddresses_account_network = privatePostV1ApprovedAddressesAccountNetwork = Entry('v1/approvedAddresses/account/{network}', 'private', 'POST', {'cost': 1}) + private_post_v1_approvedaddresses_network_remove = privatePostV1ApprovedAddressesNetworkRemove = Entry('v1/approvedAddresses/{network}/remove', 'private', 'POST', {'cost': 1}) + private_post_v1_account = privatePostV1Account = Entry('v1/account', 'private', 'POST', {'cost': 1}) + private_post_v1_account_create = privatePostV1AccountCreate = Entry('v1/account/create', 'private', 'POST', {'cost': 1}) + private_post_v1_account_list = privatePostV1AccountList = Entry('v1/account/list', 'private', 'POST', {'cost': 1}) + private_post_v1_heartbeat = privatePostV1Heartbeat = Entry('v1/heartbeat', 'private', 'POST', {'cost': 1}) + private_post_v1_roles = privatePostV1Roles = Entry('v1/roles', 'private', 'POST', {'cost': 1}) diff --git a/ccxt/abstract/hashkey.py b/ccxt/abstract/hashkey.py new file mode 100644 index 0000000..40bd2b3 --- /dev/null +++ b/ccxt/abstract/hashkey.py @@ -0,0 +1,67 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_api_v1_exchangeinfo = publicGetApiV1ExchangeInfo = Entry('api/v1/exchangeInfo', 'public', 'GET', {'cost': 5}) + public_get_quote_v1_depth = publicGetQuoteV1Depth = Entry('quote/v1/depth', 'public', 'GET', {'cost': 1}) + public_get_quote_v1_trades = publicGetQuoteV1Trades = Entry('quote/v1/trades', 'public', 'GET', {'cost': 1}) + public_get_quote_v1_klines = publicGetQuoteV1Klines = Entry('quote/v1/klines', 'public', 'GET', {'cost': 1}) + public_get_quote_v1_ticker_24hr = publicGetQuoteV1Ticker24hr = Entry('quote/v1/ticker/24hr', 'public', 'GET', {'cost': 1}) + public_get_quote_v1_ticker_price = publicGetQuoteV1TickerPrice = Entry('quote/v1/ticker/price', 'public', 'GET', {'cost': 1}) + public_get_quote_v1_ticker_bookticker = publicGetQuoteV1TickerBookTicker = Entry('quote/v1/ticker/bookTicker', 'public', 'GET', {'cost': 1}) + public_get_quote_v1_depth_merged = publicGetQuoteV1DepthMerged = Entry('quote/v1/depth/merged', 'public', 'GET', {'cost': 1}) + public_get_quote_v1_markprice = publicGetQuoteV1MarkPrice = Entry('quote/v1/markPrice', 'public', 'GET', {'cost': 1}) + public_get_quote_v1_index = publicGetQuoteV1Index = Entry('quote/v1/index', 'public', 'GET', {'cost': 1}) + public_get_api_v1_futures_fundingrate = publicGetApiV1FuturesFundingRate = Entry('api/v1/futures/fundingRate', 'public', 'GET', {'cost': 1}) + public_get_api_v1_futures_historyfundingrate = publicGetApiV1FuturesHistoryFundingRate = Entry('api/v1/futures/historyFundingRate', 'public', 'GET', {'cost': 1}) + public_get_api_v1_ping = publicGetApiV1Ping = Entry('api/v1/ping', 'public', 'GET', {'cost': 1}) + public_get_api_v1_time = publicGetApiV1Time = Entry('api/v1/time', 'public', 'GET', {'cost': 1}) + private_get_api_v1_spot_order = privateGetApiV1SpotOrder = Entry('api/v1/spot/order', 'private', 'GET', {'cost': 1}) + private_get_api_v1_spot_openorders = privateGetApiV1SpotOpenOrders = Entry('api/v1/spot/openOrders', 'private', 'GET', {'cost': 1}) + private_get_api_v1_spot_tradeorders = privateGetApiV1SpotTradeOrders = Entry('api/v1/spot/tradeOrders', 'private', 'GET', {'cost': 5}) + private_get_api_v1_futures_leverage = privateGetApiV1FuturesLeverage = Entry('api/v1/futures/leverage', 'private', 'GET', {'cost': 1}) + private_get_api_v1_futures_order = privateGetApiV1FuturesOrder = Entry('api/v1/futures/order', 'private', 'GET', {'cost': 1}) + private_get_api_v1_futures_openorders = privateGetApiV1FuturesOpenOrders = Entry('api/v1/futures/openOrders', 'private', 'GET', {'cost': 1}) + private_get_api_v1_futures_usertrades = privateGetApiV1FuturesUserTrades = Entry('api/v1/futures/userTrades', 'private', 'GET', {'cost': 1}) + private_get_api_v1_futures_positions = privateGetApiV1FuturesPositions = Entry('api/v1/futures/positions', 'private', 'GET', {'cost': 1}) + private_get_api_v1_futures_historyorders = privateGetApiV1FuturesHistoryOrders = Entry('api/v1/futures/historyOrders', 'private', 'GET', {'cost': 1}) + private_get_api_v1_futures_balance = privateGetApiV1FuturesBalance = Entry('api/v1/futures/balance', 'private', 'GET', {'cost': 1}) + private_get_api_v1_futures_liquidationassignstatus = privateGetApiV1FuturesLiquidationAssignStatus = Entry('api/v1/futures/liquidationAssignStatus', 'private', 'GET', {'cost': 1}) + private_get_api_v1_futures_risklimit = privateGetApiV1FuturesRiskLimit = Entry('api/v1/futures/riskLimit', 'private', 'GET', {'cost': 1}) + private_get_api_v1_futures_commissionrate = privateGetApiV1FuturesCommissionRate = Entry('api/v1/futures/commissionRate', 'private', 'GET', {'cost': 1}) + private_get_api_v1_futures_getbestorder = privateGetApiV1FuturesGetBestOrder = Entry('api/v1/futures/getBestOrder', 'private', 'GET', {'cost': 1}) + private_get_api_v1_account_vipinfo = privateGetApiV1AccountVipInfo = Entry('api/v1/account/vipInfo', 'private', 'GET', {'cost': 1}) + private_get_api_v1_account = privateGetApiV1Account = Entry('api/v1/account', 'private', 'GET', {'cost': 1}) + private_get_api_v1_account_trades = privateGetApiV1AccountTrades = Entry('api/v1/account/trades', 'private', 'GET', {'cost': 5}) + private_get_api_v1_account_type = privateGetApiV1AccountType = Entry('api/v1/account/type', 'private', 'GET', {'cost': 5}) + private_get_api_v1_account_checkapikey = privateGetApiV1AccountCheckApiKey = Entry('api/v1/account/checkApiKey', 'private', 'GET', {'cost': 1}) + private_get_api_v1_account_balanceflow = privateGetApiV1AccountBalanceFlow = Entry('api/v1/account/balanceFlow', 'private', 'GET', {'cost': 5}) + private_get_api_v1_spot_subaccount_openorders = privateGetApiV1SpotSubAccountOpenOrders = Entry('api/v1/spot/subAccount/openOrders', 'private', 'GET', {'cost': 1}) + private_get_api_v1_spot_subaccount_tradeorders = privateGetApiV1SpotSubAccountTradeOrders = Entry('api/v1/spot/subAccount/tradeOrders', 'private', 'GET', {'cost': 1}) + private_get_api_v1_subaccount_trades = privateGetApiV1SubAccountTrades = Entry('api/v1/subAccount/trades', 'private', 'GET', {'cost': 1}) + private_get_api_v1_futures_subaccount_openorders = privateGetApiV1FuturesSubAccountOpenOrders = Entry('api/v1/futures/subAccount/openOrders', 'private', 'GET', {'cost': 1}) + private_get_api_v1_futures_subaccount_historyorders = privateGetApiV1FuturesSubAccountHistoryOrders = Entry('api/v1/futures/subAccount/historyOrders', 'private', 'GET', {'cost': 1}) + private_get_api_v1_futures_subaccount_usertrades = privateGetApiV1FuturesSubAccountUserTrades = Entry('api/v1/futures/subAccount/userTrades', 'private', 'GET', {'cost': 1}) + private_get_api_v1_account_deposit_address = privateGetApiV1AccountDepositAddress = Entry('api/v1/account/deposit/address', 'private', 'GET', {'cost': 1}) + private_get_api_v1_account_depositorders = privateGetApiV1AccountDepositOrders = Entry('api/v1/account/depositOrders', 'private', 'GET', {'cost': 1}) + private_get_api_v1_account_withdraworders = privateGetApiV1AccountWithdrawOrders = Entry('api/v1/account/withdrawOrders', 'private', 'GET', {'cost': 1}) + private_post_api_v1_userdatastream = privatePostApiV1UserDataStream = Entry('api/v1/userDataStream', 'private', 'POST', {'cost': 1}) + private_post_api_v1_spot_ordertest = privatePostApiV1SpotOrderTest = Entry('api/v1/spot/orderTest', 'private', 'POST', {'cost': 1}) + private_post_api_v1_spot_order = privatePostApiV1SpotOrder = Entry('api/v1/spot/order', 'private', 'POST', {'cost': 1}) + private_post_api_v1_1_spot_order = privatePostApiV11SpotOrder = Entry('api/v1.1/spot/order', 'private', 'POST', {'cost': 1}) + private_post_api_v1_spot_batchorders = privatePostApiV1SpotBatchOrders = Entry('api/v1/spot/batchOrders', 'private', 'POST', {'cost': 5}) + private_post_api_v1_futures_leverage = privatePostApiV1FuturesLeverage = Entry('api/v1/futures/leverage', 'private', 'POST', {'cost': 1}) + private_post_api_v1_futures_order = privatePostApiV1FuturesOrder = Entry('api/v1/futures/order', 'private', 'POST', {'cost': 1}) + private_post_api_v1_futures_position_trading_stop = privatePostApiV1FuturesPositionTradingStop = Entry('api/v1/futures/position/trading-stop', 'private', 'POST', {'cost': 3}) + private_post_api_v1_futures_batchorders = privatePostApiV1FuturesBatchOrders = Entry('api/v1/futures/batchOrders', 'private', 'POST', {'cost': 5}) + private_post_api_v1_account_assettransfer = privatePostApiV1AccountAssetTransfer = Entry('api/v1/account/assetTransfer', 'private', 'POST', {'cost': 1}) + private_post_api_v1_account_authaddress = privatePostApiV1AccountAuthAddress = Entry('api/v1/account/authAddress', 'private', 'POST', {'cost': 1}) + private_post_api_v1_account_withdraw = privatePostApiV1AccountWithdraw = Entry('api/v1/account/withdraw', 'private', 'POST', {'cost': 1}) + private_put_api_v1_userdatastream = privatePutApiV1UserDataStream = Entry('api/v1/userDataStream', 'private', 'PUT', {'cost': 1}) + private_delete_api_v1_spot_order = privateDeleteApiV1SpotOrder = Entry('api/v1/spot/order', 'private', 'DELETE', {'cost': 1}) + private_delete_api_v1_spot_openorders = privateDeleteApiV1SpotOpenOrders = Entry('api/v1/spot/openOrders', 'private', 'DELETE', {'cost': 5}) + private_delete_api_v1_spot_cancelorderbyids = privateDeleteApiV1SpotCancelOrderByIds = Entry('api/v1/spot/cancelOrderByIds', 'private', 'DELETE', {'cost': 5}) + private_delete_api_v1_futures_order = privateDeleteApiV1FuturesOrder = Entry('api/v1/futures/order', 'private', 'DELETE', {'cost': 1}) + private_delete_api_v1_futures_batchorders = privateDeleteApiV1FuturesBatchOrders = Entry('api/v1/futures/batchOrders', 'private', 'DELETE', {'cost': 1}) + private_delete_api_v1_futures_cancelorderbyids = privateDeleteApiV1FuturesCancelOrderByIds = Entry('api/v1/futures/cancelOrderByIds', 'private', 'DELETE', {'cost': 1}) + private_delete_api_v1_userdatastream = privateDeleteApiV1UserDataStream = Entry('api/v1/userDataStream', 'private', 'DELETE', {'cost': 1}) diff --git a/ccxt/abstract/hibachi.py b/ccxt/abstract/hibachi.py new file mode 100644 index 0000000..4351b62 --- /dev/null +++ b/ccxt/abstract/hibachi.py @@ -0,0 +1,26 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_market_exchange_info = publicGetMarketExchangeInfo = Entry('market/exchange-info', 'public', 'GET', {'cost': 1}) + public_get_market_data_trades = publicGetMarketDataTrades = Entry('market/data/trades', 'public', 'GET', {'cost': 1}) + public_get_market_data_prices = publicGetMarketDataPrices = Entry('market/data/prices', 'public', 'GET', {'cost': 1}) + public_get_market_data_stats = publicGetMarketDataStats = Entry('market/data/stats', 'public', 'GET', {'cost': 1}) + public_get_market_data_klines = publicGetMarketDataKlines = Entry('market/data/klines', 'public', 'GET', {'cost': 1}) + public_get_market_data_orderbook = publicGetMarketDataOrderbook = Entry('market/data/orderbook', 'public', 'GET', {'cost': 1}) + public_get_market_data_open_interest = publicGetMarketDataOpenInterest = Entry('market/data/open-interest', 'public', 'GET', {'cost': 1}) + public_get_market_data_funding_rates = publicGetMarketDataFundingRates = Entry('market/data/funding-rates', 'public', 'GET', {'cost': 1}) + public_get_exchange_utc_timestamp = publicGetExchangeUtcTimestamp = Entry('exchange/utc-timestamp', 'public', 'GET', {'cost': 1}) + private_get_capital_deposit_info = privateGetCapitalDepositInfo = Entry('capital/deposit-info', 'private', 'GET', {'cost': 1}) + private_get_capital_history = privateGetCapitalHistory = Entry('capital/history', 'private', 'GET', {'cost': 1}) + private_get_trade_account_trading_history = privateGetTradeAccountTradingHistory = Entry('trade/account/trading_history', 'private', 'GET', {'cost': 1}) + private_get_trade_account_info = privateGetTradeAccountInfo = Entry('trade/account/info', 'private', 'GET', {'cost': 1}) + private_get_trade_order = privateGetTradeOrder = Entry('trade/order', 'private', 'GET', {'cost': 1}) + private_get_trade_account_trades = privateGetTradeAccountTrades = Entry('trade/account/trades', 'private', 'GET', {'cost': 1}) + private_get_trade_orders = privateGetTradeOrders = Entry('trade/orders', 'private', 'GET', {'cost': 1}) + private_put_trade_order = privatePutTradeOrder = Entry('trade/order', 'private', 'PUT', {'cost': 1}) + private_delete_trade_order = privateDeleteTradeOrder = Entry('trade/order', 'private', 'DELETE', {'cost': 1}) + private_delete_trade_orders = privateDeleteTradeOrders = Entry('trade/orders', 'private', 'DELETE', {'cost': 1}) + private_post_trade_order = privatePostTradeOrder = Entry('trade/order', 'private', 'POST', {'cost': 1}) + private_post_trade_orders = privatePostTradeOrders = Entry('trade/orders', 'private', 'POST', {'cost': 1}) + private_post_capital_withdraw = privatePostCapitalWithdraw = Entry('capital/withdraw', 'private', 'POST', {'cost': 1}) diff --git a/ccxt/abstract/hitbtc.py b/ccxt/abstract/hitbtc.py new file mode 100644 index 0000000..9507bfd --- /dev/null +++ b/ccxt/abstract/hitbtc.py @@ -0,0 +1,115 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_public_currency = publicGetPublicCurrency = Entry('public/currency', 'public', 'GET', {'cost': 10}) + public_get_public_currency_currency = publicGetPublicCurrencyCurrency = Entry('public/currency/{currency}', 'public', 'GET', {'cost': 10}) + public_get_public_symbol = publicGetPublicSymbol = Entry('public/symbol', 'public', 'GET', {'cost': 10}) + public_get_public_symbol_symbol = publicGetPublicSymbolSymbol = Entry('public/symbol/{symbol}', 'public', 'GET', {'cost': 10}) + public_get_public_ticker = publicGetPublicTicker = Entry('public/ticker', 'public', 'GET', {'cost': 10}) + public_get_public_ticker_symbol = publicGetPublicTickerSymbol = Entry('public/ticker/{symbol}', 'public', 'GET', {'cost': 10}) + public_get_public_price_rate = publicGetPublicPriceRate = Entry('public/price/rate', 'public', 'GET', {'cost': 10}) + public_get_public_price_history = publicGetPublicPriceHistory = Entry('public/price/history', 'public', 'GET', {'cost': 10}) + public_get_public_price_ticker = publicGetPublicPriceTicker = Entry('public/price/ticker', 'public', 'GET', {'cost': 10}) + public_get_public_price_ticker_symbol = publicGetPublicPriceTickerSymbol = Entry('public/price/ticker/{symbol}', 'public', 'GET', {'cost': 10}) + public_get_public_trades = publicGetPublicTrades = Entry('public/trades', 'public', 'GET', {'cost': 10}) + public_get_public_trades_symbol = publicGetPublicTradesSymbol = Entry('public/trades/{symbol}', 'public', 'GET', {'cost': 10}) + public_get_public_orderbook = publicGetPublicOrderbook = Entry('public/orderbook', 'public', 'GET', {'cost': 10}) + public_get_public_orderbook_symbol = publicGetPublicOrderbookSymbol = Entry('public/orderbook/{symbol}', 'public', 'GET', {'cost': 10}) + public_get_public_candles = publicGetPublicCandles = Entry('public/candles', 'public', 'GET', {'cost': 10}) + public_get_public_candles_symbol = publicGetPublicCandlesSymbol = Entry('public/candles/{symbol}', 'public', 'GET', {'cost': 10}) + public_get_public_converted_candles = publicGetPublicConvertedCandles = Entry('public/converted/candles', 'public', 'GET', {'cost': 10}) + public_get_public_converted_candles_symbol = publicGetPublicConvertedCandlesSymbol = Entry('public/converted/candles/{symbol}', 'public', 'GET', {'cost': 10}) + public_get_public_futures_info = publicGetPublicFuturesInfo = Entry('public/futures/info', 'public', 'GET', {'cost': 10}) + public_get_public_futures_info_symbol = publicGetPublicFuturesInfoSymbol = Entry('public/futures/info/{symbol}', 'public', 'GET', {'cost': 10}) + public_get_public_futures_history_funding = publicGetPublicFuturesHistoryFunding = Entry('public/futures/history/funding', 'public', 'GET', {'cost': 10}) + public_get_public_futures_history_funding_symbol = publicGetPublicFuturesHistoryFundingSymbol = Entry('public/futures/history/funding/{symbol}', 'public', 'GET', {'cost': 10}) + public_get_public_futures_candles_index_price = publicGetPublicFuturesCandlesIndexPrice = Entry('public/futures/candles/index_price', 'public', 'GET', {'cost': 10}) + public_get_public_futures_candles_index_price_symbol = publicGetPublicFuturesCandlesIndexPriceSymbol = Entry('public/futures/candles/index_price/{symbol}', 'public', 'GET', {'cost': 10}) + public_get_public_futures_candles_mark_price = publicGetPublicFuturesCandlesMarkPrice = Entry('public/futures/candles/mark_price', 'public', 'GET', {'cost': 10}) + public_get_public_futures_candles_mark_price_symbol = publicGetPublicFuturesCandlesMarkPriceSymbol = Entry('public/futures/candles/mark_price/{symbol}', 'public', 'GET', {'cost': 10}) + public_get_public_futures_candles_premium_index = publicGetPublicFuturesCandlesPremiumIndex = Entry('public/futures/candles/premium_index', 'public', 'GET', {'cost': 10}) + public_get_public_futures_candles_premium_index_symbol = publicGetPublicFuturesCandlesPremiumIndexSymbol = Entry('public/futures/candles/premium_index/{symbol}', 'public', 'GET', {'cost': 10}) + public_get_public_futures_candles_open_interest = publicGetPublicFuturesCandlesOpenInterest = Entry('public/futures/candles/open_interest', 'public', 'GET', {'cost': 10}) + public_get_public_futures_candles_open_interest_symbol = publicGetPublicFuturesCandlesOpenInterestSymbol = Entry('public/futures/candles/open_interest/{symbol}', 'public', 'GET', {'cost': 10}) + private_get_spot_balance = privateGetSpotBalance = Entry('spot/balance', 'private', 'GET', {'cost': 15}) + private_get_spot_balance_currency = privateGetSpotBalanceCurrency = Entry('spot/balance/{currency}', 'private', 'GET', {'cost': 15}) + private_get_spot_order = privateGetSpotOrder = Entry('spot/order', 'private', 'GET', {'cost': 1}) + private_get_spot_order_client_order_id = privateGetSpotOrderClientOrderId = Entry('spot/order/{client_order_id}', 'private', 'GET', {'cost': 1}) + private_get_spot_fee = privateGetSpotFee = Entry('spot/fee', 'private', 'GET', {'cost': 15}) + private_get_spot_fee_symbol = privateGetSpotFeeSymbol = Entry('spot/fee/{symbol}', 'private', 'GET', {'cost': 15}) + private_get_spot_history_order = privateGetSpotHistoryOrder = Entry('spot/history/order', 'private', 'GET', {'cost': 15}) + private_get_spot_history_trade = privateGetSpotHistoryTrade = Entry('spot/history/trade', 'private', 'GET', {'cost': 15}) + private_get_margin_account = privateGetMarginAccount = Entry('margin/account', 'private', 'GET', {'cost': 1}) + private_get_margin_account_isolated_symbol = privateGetMarginAccountIsolatedSymbol = Entry('margin/account/isolated/{symbol}', 'private', 'GET', {'cost': 1}) + private_get_margin_account_cross_currency = privateGetMarginAccountCrossCurrency = Entry('margin/account/cross/{currency}', 'private', 'GET', {'cost': 1}) + private_get_margin_order = privateGetMarginOrder = Entry('margin/order', 'private', 'GET', {'cost': 1}) + private_get_margin_order_client_order_id = privateGetMarginOrderClientOrderId = Entry('margin/order/{client_order_id}', 'private', 'GET', {'cost': 1}) + private_get_margin_config = privateGetMarginConfig = Entry('margin/config', 'private', 'GET', {'cost': 15}) + private_get_margin_history_order = privateGetMarginHistoryOrder = Entry('margin/history/order', 'private', 'GET', {'cost': 15}) + private_get_margin_history_trade = privateGetMarginHistoryTrade = Entry('margin/history/trade', 'private', 'GET', {'cost': 15}) + private_get_margin_history_positions = privateGetMarginHistoryPositions = Entry('margin/history/positions', 'private', 'GET', {'cost': 15}) + private_get_margin_history_clearing = privateGetMarginHistoryClearing = Entry('margin/history/clearing', 'private', 'GET', {'cost': 15}) + private_get_futures_balance = privateGetFuturesBalance = Entry('futures/balance', 'private', 'GET', {'cost': 15}) + private_get_futures_balance_currency = privateGetFuturesBalanceCurrency = Entry('futures/balance/{currency}', 'private', 'GET', {'cost': 15}) + private_get_futures_account = privateGetFuturesAccount = Entry('futures/account', 'private', 'GET', {'cost': 1}) + private_get_futures_account_isolated_symbol = privateGetFuturesAccountIsolatedSymbol = Entry('futures/account/isolated/{symbol}', 'private', 'GET', {'cost': 1}) + private_get_futures_order = privateGetFuturesOrder = Entry('futures/order', 'private', 'GET', {'cost': 1}) + private_get_futures_order_client_order_id = privateGetFuturesOrderClientOrderId = Entry('futures/order/{client_order_id}', 'private', 'GET', {'cost': 1}) + private_get_futures_config = privateGetFuturesConfig = Entry('futures/config', 'private', 'GET', {'cost': 15}) + private_get_futures_fee = privateGetFuturesFee = Entry('futures/fee', 'private', 'GET', {'cost': 15}) + private_get_futures_fee_symbol = privateGetFuturesFeeSymbol = Entry('futures/fee/{symbol}', 'private', 'GET', {'cost': 15}) + private_get_futures_history_order = privateGetFuturesHistoryOrder = Entry('futures/history/order', 'private', 'GET', {'cost': 15}) + private_get_futures_history_trade = privateGetFuturesHistoryTrade = Entry('futures/history/trade', 'private', 'GET', {'cost': 15}) + private_get_futures_history_positions = privateGetFuturesHistoryPositions = Entry('futures/history/positions', 'private', 'GET', {'cost': 15}) + private_get_futures_history_clearing = privateGetFuturesHistoryClearing = Entry('futures/history/clearing', 'private', 'GET', {'cost': 15}) + private_get_wallet_balance = privateGetWalletBalance = Entry('wallet/balance', 'private', 'GET', {'cost': 30}) + private_get_wallet_balance_currency = privateGetWalletBalanceCurrency = Entry('wallet/balance/{currency}', 'private', 'GET', {'cost': 30}) + private_get_wallet_crypto_address = privateGetWalletCryptoAddress = Entry('wallet/crypto/address', 'private', 'GET', {'cost': 30}) + private_get_wallet_crypto_address_recent_deposit = privateGetWalletCryptoAddressRecentDeposit = Entry('wallet/crypto/address/recent-deposit', 'private', 'GET', {'cost': 30}) + private_get_wallet_crypto_address_recent_withdraw = privateGetWalletCryptoAddressRecentWithdraw = Entry('wallet/crypto/address/recent-withdraw', 'private', 'GET', {'cost': 30}) + private_get_wallet_crypto_address_check_mine = privateGetWalletCryptoAddressCheckMine = Entry('wallet/crypto/address/check-mine', 'private', 'GET', {'cost': 30}) + private_get_wallet_transactions = privateGetWalletTransactions = Entry('wallet/transactions', 'private', 'GET', {'cost': 30}) + private_get_wallet_transactions_tx_id = privateGetWalletTransactionsTxId = Entry('wallet/transactions/{tx_id}', 'private', 'GET', {'cost': 30}) + private_get_wallet_crypto_fee_estimate = privateGetWalletCryptoFeeEstimate = Entry('wallet/crypto/fee/estimate', 'private', 'GET', {'cost': 30}) + private_get_wallet_airdrops = privateGetWalletAirdrops = Entry('wallet/airdrops', 'private', 'GET', {'cost': 30}) + private_get_wallet_amount_locks = privateGetWalletAmountLocks = Entry('wallet/amount-locks', 'private', 'GET', {'cost': 30}) + private_get_sub_account = privateGetSubAccount = Entry('sub-account', 'private', 'GET', {'cost': 15}) + private_get_sub_account_acl = privateGetSubAccountAcl = Entry('sub-account/acl', 'private', 'GET', {'cost': 15}) + private_get_sub_account_balance_subaccid = privateGetSubAccountBalanceSubAccID = Entry('sub-account/balance/{subAccID}', 'private', 'GET', {'cost': 15}) + private_get_sub_account_crypto_address_subaccid_currency = privateGetSubAccountCryptoAddressSubAccIDCurrency = Entry('sub-account/crypto/address/{subAccID}/{currency}', 'private', 'GET', {'cost': 15}) + private_post_spot_order = privatePostSpotOrder = Entry('spot/order', 'private', 'POST', {'cost': 1}) + private_post_spot_order_list = privatePostSpotOrderList = Entry('spot/order/list', 'private', 'POST', {'cost': 1}) + private_post_margin_order = privatePostMarginOrder = Entry('margin/order', 'private', 'POST', {'cost': 1}) + private_post_margin_order_list = privatePostMarginOrderList = Entry('margin/order/list', 'private', 'POST', {'cost': 1}) + private_post_futures_order = privatePostFuturesOrder = Entry('futures/order', 'private', 'POST', {'cost': 1}) + private_post_futures_order_list = privatePostFuturesOrderList = Entry('futures/order/list', 'private', 'POST', {'cost': 1}) + private_post_wallet_crypto_address = privatePostWalletCryptoAddress = Entry('wallet/crypto/address', 'private', 'POST', {'cost': 30}) + private_post_wallet_crypto_withdraw = privatePostWalletCryptoWithdraw = Entry('wallet/crypto/withdraw', 'private', 'POST', {'cost': 30}) + private_post_wallet_convert = privatePostWalletConvert = Entry('wallet/convert', 'private', 'POST', {'cost': 30}) + private_post_wallet_transfer = privatePostWalletTransfer = Entry('wallet/transfer', 'private', 'POST', {'cost': 30}) + private_post_wallet_internal_withdraw = privatePostWalletInternalWithdraw = Entry('wallet/internal/withdraw', 'private', 'POST', {'cost': 30}) + private_post_wallet_crypto_check_offchain_available = privatePostWalletCryptoCheckOffchainAvailable = Entry('wallet/crypto/check-offchain-available', 'private', 'POST', {'cost': 30}) + private_post_wallet_crypto_fees_estimate = privatePostWalletCryptoFeesEstimate = Entry('wallet/crypto/fees/estimate', 'private', 'POST', {'cost': 30}) + private_post_wallet_airdrops_id_claim = privatePostWalletAirdropsIdClaim = Entry('wallet/airdrops/{id}/claim', 'private', 'POST', {'cost': 30}) + private_post_sub_account_freeze = privatePostSubAccountFreeze = Entry('sub-account/freeze', 'private', 'POST', {'cost': 15}) + private_post_sub_account_activate = privatePostSubAccountActivate = Entry('sub-account/activate', 'private', 'POST', {'cost': 15}) + private_post_sub_account_transfer = privatePostSubAccountTransfer = Entry('sub-account/transfer', 'private', 'POST', {'cost': 15}) + private_post_sub_account_acl = privatePostSubAccountAcl = Entry('sub-account/acl', 'private', 'POST', {'cost': 15}) + private_patch_spot_order_client_order_id = privatePatchSpotOrderClientOrderId = Entry('spot/order/{client_order_id}', 'private', 'PATCH', {'cost': 1}) + private_patch_margin_order_client_order_id = privatePatchMarginOrderClientOrderId = Entry('margin/order/{client_order_id}', 'private', 'PATCH', {'cost': 1}) + private_patch_futures_order_client_order_id = privatePatchFuturesOrderClientOrderId = Entry('futures/order/{client_order_id}', 'private', 'PATCH', {'cost': 1}) + private_delete_spot_order = privateDeleteSpotOrder = Entry('spot/order', 'private', 'DELETE', {'cost': 1}) + private_delete_spot_order_client_order_id = privateDeleteSpotOrderClientOrderId = Entry('spot/order/{client_order_id}', 'private', 'DELETE', {'cost': 1}) + private_delete_margin_position = privateDeleteMarginPosition = Entry('margin/position', 'private', 'DELETE', {'cost': 1}) + private_delete_margin_position_isolated_symbol = privateDeleteMarginPositionIsolatedSymbol = Entry('margin/position/isolated/{symbol}', 'private', 'DELETE', {'cost': 1}) + private_delete_margin_order = privateDeleteMarginOrder = Entry('margin/order', 'private', 'DELETE', {'cost': 1}) + private_delete_margin_order_client_order_id = privateDeleteMarginOrderClientOrderId = Entry('margin/order/{client_order_id}', 'private', 'DELETE', {'cost': 1}) + private_delete_futures_position = privateDeleteFuturesPosition = Entry('futures/position', 'private', 'DELETE', {'cost': 1}) + private_delete_futures_position_margin_mode_symbol = privateDeleteFuturesPositionMarginModeSymbol = Entry('futures/position/{margin_mode}/{symbol}', 'private', 'DELETE', {'cost': 1}) + private_delete_futures_order = privateDeleteFuturesOrder = Entry('futures/order', 'private', 'DELETE', {'cost': 1}) + private_delete_futures_order_client_order_id = privateDeleteFuturesOrderClientOrderId = Entry('futures/order/{client_order_id}', 'private', 'DELETE', {'cost': 1}) + private_delete_wallet_crypto_withdraw_id = privateDeleteWalletCryptoWithdrawId = Entry('wallet/crypto/withdraw/{id}', 'private', 'DELETE', {'cost': 30}) + private_put_margin_account_isolated_symbol = privatePutMarginAccountIsolatedSymbol = Entry('margin/account/isolated/{symbol}', 'private', 'PUT', {'cost': 1}) + private_put_futures_account_isolated_symbol = privatePutFuturesAccountIsolatedSymbol = Entry('futures/account/isolated/{symbol}', 'private', 'PUT', {'cost': 1}) + private_put_wallet_crypto_withdraw_id = privatePutWalletCryptoWithdrawId = Entry('wallet/crypto/withdraw/{id}', 'private', 'PUT', {'cost': 30}) diff --git a/ccxt/abstract/hollaex.py b/ccxt/abstract/hollaex.py new file mode 100644 index 0000000..ff44f86 --- /dev/null +++ b/ccxt/abstract/hollaex.py @@ -0,0 +1,33 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_health = publicGetHealth = Entry('health', 'public', 'GET', {'cost': 1}) + public_get_constants = publicGetConstants = Entry('constants', 'public', 'GET', {'cost': 1}) + public_get_kit = publicGetKit = Entry('kit', 'public', 'GET', {'cost': 1}) + public_get_tiers = publicGetTiers = Entry('tiers', 'public', 'GET', {'cost': 1}) + public_get_ticker = publicGetTicker = Entry('ticker', 'public', 'GET', {'cost': 1}) + public_get_tickers = publicGetTickers = Entry('tickers', 'public', 'GET', {'cost': 1}) + public_get_orderbook = publicGetOrderbook = Entry('orderbook', 'public', 'GET', {'cost': 1}) + public_get_orderbooks = publicGetOrderbooks = Entry('orderbooks', 'public', 'GET', {'cost': 1}) + public_get_trades = publicGetTrades = Entry('trades', 'public', 'GET', {'cost': 1}) + public_get_chart = publicGetChart = Entry('chart', 'public', 'GET', {'cost': 1}) + public_get_charts = publicGetCharts = Entry('charts', 'public', 'GET', {'cost': 1}) + public_get_minicharts = publicGetMinicharts = Entry('minicharts', 'public', 'GET', {'cost': 1}) + public_get_oracle_prices = publicGetOraclePrices = Entry('oracle/prices', 'public', 'GET', {'cost': 1}) + public_get_quick_trade = publicGetQuickTrade = Entry('quick-trade', 'public', 'GET', {'cost': 1}) + public_get_udf_config = publicGetUdfConfig = Entry('udf/config', 'public', 'GET', {'cost': 1}) + public_get_udf_history = publicGetUdfHistory = Entry('udf/history', 'public', 'GET', {'cost': 1}) + public_get_udf_symbols = publicGetUdfSymbols = Entry('udf/symbols', 'public', 'GET', {'cost': 1}) + private_get_user = privateGetUser = Entry('user', 'private', 'GET', {'cost': 1}) + private_get_user_balance = privateGetUserBalance = Entry('user/balance', 'private', 'GET', {'cost': 1}) + private_get_user_deposits = privateGetUserDeposits = Entry('user/deposits', 'private', 'GET', {'cost': 1}) + private_get_user_withdrawals = privateGetUserWithdrawals = Entry('user/withdrawals', 'private', 'GET', {'cost': 1}) + private_get_user_withdrawal_fee = privateGetUserWithdrawalFee = Entry('user/withdrawal/fee', 'private', 'GET', {'cost': 1}) + private_get_user_trades = privateGetUserTrades = Entry('user/trades', 'private', 'GET', {'cost': 1}) + private_get_orders = privateGetOrders = Entry('orders', 'private', 'GET', {'cost': 1}) + private_get_order = privateGetOrder = Entry('order', 'private', 'GET', {'cost': 1}) + private_post_user_withdrawal = privatePostUserWithdrawal = Entry('user/withdrawal', 'private', 'POST', {'cost': 1}) + private_post_order = privatePostOrder = Entry('order', 'private', 'POST', {'cost': 1}) + private_delete_order_all = privateDeleteOrderAll = Entry('order/all', 'private', 'DELETE', {'cost': 1}) + private_delete_order = privateDeleteOrder = Entry('order', 'private', 'DELETE', {'cost': 1}) diff --git a/ccxt/abstract/htx.py b/ccxt/abstract/htx.py new file mode 100644 index 0000000..884b800 --- /dev/null +++ b/ccxt/abstract/htx.py @@ -0,0 +1,548 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + v2public_get_reference_currencies = v2PublicGetReferenceCurrencies = Entry('reference/currencies', 'v2Public', 'GET', {'cost': 1}) + v2public_get_market_status = v2PublicGetMarketStatus = Entry('market-status', 'v2Public', 'GET', {'cost': 1}) + v2private_get_account_ledger = v2PrivateGetAccountLedger = Entry('account/ledger', 'v2Private', 'GET', {'cost': 1}) + v2private_get_account_withdraw_quota = v2PrivateGetAccountWithdrawQuota = Entry('account/withdraw/quota', 'v2Private', 'GET', {'cost': 1}) + v2private_get_account_withdraw_address = v2PrivateGetAccountWithdrawAddress = Entry('account/withdraw/address', 'v2Private', 'GET', {'cost': 1}) + v2private_get_account_deposit_address = v2PrivateGetAccountDepositAddress = Entry('account/deposit/address', 'v2Private', 'GET', {'cost': 1}) + v2private_get_account_repayment = v2PrivateGetAccountRepayment = Entry('account/repayment', 'v2Private', 'GET', {'cost': 5}) + v2private_get_reference_transact_fee_rate = v2PrivateGetReferenceTransactFeeRate = Entry('reference/transact-fee-rate', 'v2Private', 'GET', {'cost': 1}) + v2private_get_account_asset_valuation = v2PrivateGetAccountAssetValuation = Entry('account/asset-valuation', 'v2Private', 'GET', {'cost': 0.2}) + v2private_get_point_account = v2PrivateGetPointAccount = Entry('point/account', 'v2Private', 'GET', {'cost': 5}) + v2private_get_sub_user_user_list = v2PrivateGetSubUserUserList = Entry('sub-user/user-list', 'v2Private', 'GET', {'cost': 1}) + v2private_get_sub_user_user_state = v2PrivateGetSubUserUserState = Entry('sub-user/user-state', 'v2Private', 'GET', {'cost': 1}) + v2private_get_sub_user_account_list = v2PrivateGetSubUserAccountList = Entry('sub-user/account-list', 'v2Private', 'GET', {'cost': 1}) + v2private_get_sub_user_deposit_address = v2PrivateGetSubUserDepositAddress = Entry('sub-user/deposit-address', 'v2Private', 'GET', {'cost': 1}) + v2private_get_sub_user_query_deposit = v2PrivateGetSubUserQueryDeposit = Entry('sub-user/query-deposit', 'v2Private', 'GET', {'cost': 1}) + v2private_get_user_api_key = v2PrivateGetUserApiKey = Entry('user/api-key', 'v2Private', 'GET', {'cost': 1}) + v2private_get_user_uid = v2PrivateGetUserUid = Entry('user/uid', 'v2Private', 'GET', {'cost': 1}) + v2private_get_algo_orders_opening = v2PrivateGetAlgoOrdersOpening = Entry('algo-orders/opening', 'v2Private', 'GET', {'cost': 1}) + v2private_get_algo_orders_history = v2PrivateGetAlgoOrdersHistory = Entry('algo-orders/history', 'v2Private', 'GET', {'cost': 1}) + v2private_get_algo_orders_specific = v2PrivateGetAlgoOrdersSpecific = Entry('algo-orders/specific', 'v2Private', 'GET', {'cost': 1}) + v2private_get_c2c_offers = v2PrivateGetC2cOffers = Entry('c2c/offers', 'v2Private', 'GET', {'cost': 1}) + v2private_get_c2c_offer = v2PrivateGetC2cOffer = Entry('c2c/offer', 'v2Private', 'GET', {'cost': 1}) + v2private_get_c2c_transactions = v2PrivateGetC2cTransactions = Entry('c2c/transactions', 'v2Private', 'GET', {'cost': 1}) + v2private_get_c2c_repayment = v2PrivateGetC2cRepayment = Entry('c2c/repayment', 'v2Private', 'GET', {'cost': 1}) + v2private_get_c2c_account = v2PrivateGetC2cAccount = Entry('c2c/account', 'v2Private', 'GET', {'cost': 1}) + v2private_get_etp_reference = v2PrivateGetEtpReference = Entry('etp/reference', 'v2Private', 'GET', {'cost': 1}) + v2private_get_etp_transactions = v2PrivateGetEtpTransactions = Entry('etp/transactions', 'v2Private', 'GET', {'cost': 5}) + v2private_get_etp_transaction = v2PrivateGetEtpTransaction = Entry('etp/transaction', 'v2Private', 'GET', {'cost': 5}) + v2private_get_etp_rebalance = v2PrivateGetEtpRebalance = Entry('etp/rebalance', 'v2Private', 'GET', {'cost': 1}) + v2private_get_etp_limit = v2PrivateGetEtpLimit = Entry('etp/limit', 'v2Private', 'GET', {'cost': 1}) + v2private_post_account_transfer = v2PrivatePostAccountTransfer = Entry('account/transfer', 'v2Private', 'POST', {'cost': 1}) + v2private_post_account_repayment = v2PrivatePostAccountRepayment = Entry('account/repayment', 'v2Private', 'POST', {'cost': 5}) + v2private_post_point_transfer = v2PrivatePostPointTransfer = Entry('point/transfer', 'v2Private', 'POST', {'cost': 5}) + v2private_post_sub_user_management = v2PrivatePostSubUserManagement = Entry('sub-user/management', 'v2Private', 'POST', {'cost': 1}) + v2private_post_sub_user_creation = v2PrivatePostSubUserCreation = Entry('sub-user/creation', 'v2Private', 'POST', {'cost': 1}) + v2private_post_sub_user_tradable_market = v2PrivatePostSubUserTradableMarket = Entry('sub-user/tradable-market', 'v2Private', 'POST', {'cost': 1}) + v2private_post_sub_user_transferability = v2PrivatePostSubUserTransferability = Entry('sub-user/transferability', 'v2Private', 'POST', {'cost': 1}) + v2private_post_sub_user_api_key_generation = v2PrivatePostSubUserApiKeyGeneration = Entry('sub-user/api-key-generation', 'v2Private', 'POST', {'cost': 1}) + v2private_post_sub_user_api_key_modification = v2PrivatePostSubUserApiKeyModification = Entry('sub-user/api-key-modification', 'v2Private', 'POST', {'cost': 1}) + v2private_post_sub_user_api_key_deletion = v2PrivatePostSubUserApiKeyDeletion = Entry('sub-user/api-key-deletion', 'v2Private', 'POST', {'cost': 1}) + v2private_post_sub_user_deduct_mode = v2PrivatePostSubUserDeductMode = Entry('sub-user/deduct-mode', 'v2Private', 'POST', {'cost': 1}) + v2private_post_algo_orders = v2PrivatePostAlgoOrders = Entry('algo-orders', 'v2Private', 'POST', {'cost': 1}) + v2private_post_algo_orders_cancel_all_after = v2PrivatePostAlgoOrdersCancelAllAfter = Entry('algo-orders/cancel-all-after', 'v2Private', 'POST', {'cost': 1}) + v2private_post_algo_orders_cancellation = v2PrivatePostAlgoOrdersCancellation = Entry('algo-orders/cancellation', 'v2Private', 'POST', {'cost': 1}) + v2private_post_c2c_offer = v2PrivatePostC2cOffer = Entry('c2c/offer', 'v2Private', 'POST', {'cost': 1}) + v2private_post_c2c_cancellation = v2PrivatePostC2cCancellation = Entry('c2c/cancellation', 'v2Private', 'POST', {'cost': 1}) + v2private_post_c2c_cancel_all = v2PrivatePostC2cCancelAll = Entry('c2c/cancel-all', 'v2Private', 'POST', {'cost': 1}) + v2private_post_c2c_repayment = v2PrivatePostC2cRepayment = Entry('c2c/repayment', 'v2Private', 'POST', {'cost': 1}) + v2private_post_c2c_transfer = v2PrivatePostC2cTransfer = Entry('c2c/transfer', 'v2Private', 'POST', {'cost': 1}) + v2private_post_etp_creation = v2PrivatePostEtpCreation = Entry('etp/creation', 'v2Private', 'POST', {'cost': 5}) + v2private_post_etp_redemption = v2PrivatePostEtpRedemption = Entry('etp/redemption', 'v2Private', 'POST', {'cost': 5}) + v2private_post_etp_transactid_cancel = v2PrivatePostEtpTransactIdCancel = Entry('etp/{transactId}/cancel', 'v2Private', 'POST', {'cost': 10}) + v2private_post_etp_batch_cancel = v2PrivatePostEtpBatchCancel = Entry('etp/batch-cancel', 'v2Private', 'POST', {'cost': 50}) + public_get_common_symbols = publicGetCommonSymbols = Entry('common/symbols', 'public', 'GET', {'cost': 1}) + public_get_common_currencys = publicGetCommonCurrencys = Entry('common/currencys', 'public', 'GET', {'cost': 1}) + public_get_common_timestamp = publicGetCommonTimestamp = Entry('common/timestamp', 'public', 'GET', {'cost': 1}) + public_get_common_exchange = publicGetCommonExchange = Entry('common/exchange', 'public', 'GET', {'cost': 1}) + public_get_settings_currencys = publicGetSettingsCurrencys = Entry('settings/currencys', 'public', 'GET', {'cost': 1}) + private_get_account_accounts = privateGetAccountAccounts = Entry('account/accounts', 'private', 'GET', {'cost': 0.2}) + private_get_account_accounts_id_balance = privateGetAccountAccountsIdBalance = Entry('account/accounts/{id}/balance', 'private', 'GET', {'cost': 0.2}) + private_get_account_accounts_sub_uid = privateGetAccountAccountsSubUid = Entry('account/accounts/{sub-uid}', 'private', 'GET', {'cost': 1}) + private_get_account_history = privateGetAccountHistory = Entry('account/history', 'private', 'GET', {'cost': 4}) + private_get_cross_margin_loan_info = privateGetCrossMarginLoanInfo = Entry('cross-margin/loan-info', 'private', 'GET', {'cost': 1}) + private_get_margin_loan_info = privateGetMarginLoanInfo = Entry('margin/loan-info', 'private', 'GET', {'cost': 1}) + private_get_fee_fee_rate_get = privateGetFeeFeeRateGet = Entry('fee/fee-rate/get', 'private', 'GET', {'cost': 1}) + private_get_order_openorders = privateGetOrderOpenOrders = Entry('order/openOrders', 'private', 'GET', {'cost': 0.4}) + private_get_order_orders = privateGetOrderOrders = Entry('order/orders', 'private', 'GET', {'cost': 0.4}) + private_get_order_orders_id = privateGetOrderOrdersId = Entry('order/orders/{id}', 'private', 'GET', {'cost': 0.4}) + private_get_order_orders_id_matchresults = privateGetOrderOrdersIdMatchresults = Entry('order/orders/{id}/matchresults', 'private', 'GET', {'cost': 0.4}) + private_get_order_orders_getclientorder = privateGetOrderOrdersGetClientOrder = Entry('order/orders/getClientOrder', 'private', 'GET', {'cost': 0.4}) + private_get_order_history = privateGetOrderHistory = Entry('order/history', 'private', 'GET', {'cost': 1}) + private_get_order_matchresults = privateGetOrderMatchresults = Entry('order/matchresults', 'private', 'GET', {'cost': 1}) + private_get_query_deposit_withdraw = privateGetQueryDepositWithdraw = Entry('query/deposit-withdraw', 'private', 'GET', {'cost': 1}) + private_get_margin_loan_orders = privateGetMarginLoanOrders = Entry('margin/loan-orders', 'private', 'GET', {'cost': 0.2}) + private_get_margin_accounts_balance = privateGetMarginAccountsBalance = Entry('margin/accounts/balance', 'private', 'GET', {'cost': 0.2}) + private_get_cross_margin_loan_orders = privateGetCrossMarginLoanOrders = Entry('cross-margin/loan-orders', 'private', 'GET', {'cost': 1}) + private_get_cross_margin_accounts_balance = privateGetCrossMarginAccountsBalance = Entry('cross-margin/accounts/balance', 'private', 'GET', {'cost': 1}) + private_get_points_actions = privateGetPointsActions = Entry('points/actions', 'private', 'GET', {'cost': 1}) + private_get_points_orders = privateGetPointsOrders = Entry('points/orders', 'private', 'GET', {'cost': 1}) + private_get_subuser_aggregate_balance = privateGetSubuserAggregateBalance = Entry('subuser/aggregate-balance', 'private', 'GET', {'cost': 10}) + private_get_stable_coin_exchange_rate = privateGetStableCoinExchangeRate = Entry('stable-coin/exchange_rate', 'private', 'GET', {'cost': 1}) + private_get_stable_coin_quote = privateGetStableCoinQuote = Entry('stable-coin/quote', 'private', 'GET', {'cost': 1}) + private_post_account_transfer = privatePostAccountTransfer = Entry('account/transfer', 'private', 'POST', {'cost': 1}) + private_post_futures_transfer = privatePostFuturesTransfer = Entry('futures/transfer', 'private', 'POST', {'cost': 1}) + private_post_order_batch_orders = privatePostOrderBatchOrders = Entry('order/batch-orders', 'private', 'POST', {'cost': 0.4}) + private_post_order_orders_place = privatePostOrderOrdersPlace = Entry('order/orders/place', 'private', 'POST', {'cost': 0.2}) + private_post_order_orders_submitcancelclientorder = privatePostOrderOrdersSubmitCancelClientOrder = Entry('order/orders/submitCancelClientOrder', 'private', 'POST', {'cost': 0.2}) + private_post_order_orders_batchcancelopenorders = privatePostOrderOrdersBatchCancelOpenOrders = Entry('order/orders/batchCancelOpenOrders', 'private', 'POST', {'cost': 0.4}) + private_post_order_orders_id_submitcancel = privatePostOrderOrdersIdSubmitcancel = Entry('order/orders/{id}/submitcancel', 'private', 'POST', {'cost': 0.2}) + private_post_order_orders_batchcancel = privatePostOrderOrdersBatchcancel = Entry('order/orders/batchcancel', 'private', 'POST', {'cost': 0.4}) + private_post_dw_withdraw_api_create = privatePostDwWithdrawApiCreate = Entry('dw/withdraw/api/create', 'private', 'POST', {'cost': 1}) + private_post_dw_withdraw_virtual_id_cancel = privatePostDwWithdrawVirtualIdCancel = Entry('dw/withdraw-virtual/{id}/cancel', 'private', 'POST', {'cost': 1}) + private_post_dw_transfer_in_margin = privatePostDwTransferInMargin = Entry('dw/transfer-in/margin', 'private', 'POST', {'cost': 10}) + private_post_dw_transfer_out_margin = privatePostDwTransferOutMargin = Entry('dw/transfer-out/margin', 'private', 'POST', {'cost': 10}) + private_post_margin_orders = privatePostMarginOrders = Entry('margin/orders', 'private', 'POST', {'cost': 10}) + private_post_margin_orders_id_repay = privatePostMarginOrdersIdRepay = Entry('margin/orders/{id}/repay', 'private', 'POST', {'cost': 10}) + private_post_cross_margin_transfer_in = privatePostCrossMarginTransferIn = Entry('cross-margin/transfer-in', 'private', 'POST', {'cost': 1}) + private_post_cross_margin_transfer_out = privatePostCrossMarginTransferOut = Entry('cross-margin/transfer-out', 'private', 'POST', {'cost': 1}) + private_post_cross_margin_orders = privatePostCrossMarginOrders = Entry('cross-margin/orders', 'private', 'POST', {'cost': 1}) + private_post_cross_margin_orders_id_repay = privatePostCrossMarginOrdersIdRepay = Entry('cross-margin/orders/{id}/repay', 'private', 'POST', {'cost': 1}) + private_post_stable_coin_exchange = privatePostStableCoinExchange = Entry('stable-coin/exchange', 'private', 'POST', {'cost': 1}) + private_post_subuser_transfer = privatePostSubuserTransfer = Entry('subuser/transfer', 'private', 'POST', {'cost': 10}) + status_public_spot_get_api_v2_summary_json = statusPublicSpotGetApiV2SummaryJson = Entry('api/v2/summary.json', ['status', 'public', 'spot'], 'GET', {'cost': 1}) + status_public_future_inverse_get_api_v2_summary_json = statusPublicFutureInverseGetApiV2SummaryJson = Entry('api/v2/summary.json', ['status', 'public', 'future', 'inverse'], 'GET', {'cost': 1}) + status_public_future_linear_get_api_v2_summary_json = statusPublicFutureLinearGetApiV2SummaryJson = Entry('api/v2/summary.json', ['status', 'public', 'future', 'linear'], 'GET', {'cost': 1}) + status_public_swap_inverse_get_api_v2_summary_json = statusPublicSwapInverseGetApiV2SummaryJson = Entry('api/v2/summary.json', ['status', 'public', 'swap', 'inverse'], 'GET', {'cost': 1}) + status_public_swap_linear_get_api_v2_summary_json = statusPublicSwapLinearGetApiV2SummaryJson = Entry('api/v2/summary.json', ['status', 'public', 'swap', 'linear'], 'GET', {'cost': 1}) + spot_public_get_v2_market_status = spotPublicGetV2MarketStatus = Entry('v2/market-status', ['spot', 'public'], 'GET', {'cost': 1}) + spot_public_get_v1_common_symbols = spotPublicGetV1CommonSymbols = Entry('v1/common/symbols', ['spot', 'public'], 'GET', {'cost': 1}) + spot_public_get_v1_common_currencys = spotPublicGetV1CommonCurrencys = Entry('v1/common/currencys', ['spot', 'public'], 'GET', {'cost': 1}) + spot_public_get_v2_settings_common_currencies = spotPublicGetV2SettingsCommonCurrencies = Entry('v2/settings/common/currencies', ['spot', 'public'], 'GET', {'cost': 1}) + spot_public_get_v2_reference_currencies = spotPublicGetV2ReferenceCurrencies = Entry('v2/reference/currencies', ['spot', 'public'], 'GET', {'cost': 1}) + spot_public_get_v1_common_timestamp = spotPublicGetV1CommonTimestamp = Entry('v1/common/timestamp', ['spot', 'public'], 'GET', {'cost': 1}) + spot_public_get_v1_common_exchange = spotPublicGetV1CommonExchange = Entry('v1/common/exchange', ['spot', 'public'], 'GET', {'cost': 1}) + spot_public_get_v1_settings_common_chains = spotPublicGetV1SettingsCommonChains = Entry('v1/settings/common/chains', ['spot', 'public'], 'GET', {'cost': 1}) + spot_public_get_v1_settings_common_currencys = spotPublicGetV1SettingsCommonCurrencys = Entry('v1/settings/common/currencys', ['spot', 'public'], 'GET', {'cost': 1}) + spot_public_get_v1_settings_common_symbols = spotPublicGetV1SettingsCommonSymbols = Entry('v1/settings/common/symbols', ['spot', 'public'], 'GET', {'cost': 1}) + spot_public_get_v2_settings_common_symbols = spotPublicGetV2SettingsCommonSymbols = Entry('v2/settings/common/symbols', ['spot', 'public'], 'GET', {'cost': 1}) + spot_public_get_v1_settings_common_market_symbols = spotPublicGetV1SettingsCommonMarketSymbols = Entry('v1/settings/common/market-symbols', ['spot', 'public'], 'GET', {'cost': 1}) + spot_public_get_market_history_candles = spotPublicGetMarketHistoryCandles = Entry('market/history/candles', ['spot', 'public'], 'GET', {'cost': 1}) + spot_public_get_market_history_kline = spotPublicGetMarketHistoryKline = Entry('market/history/kline', ['spot', 'public'], 'GET', {'cost': 1}) + spot_public_get_market_detail_merged = spotPublicGetMarketDetailMerged = Entry('market/detail/merged', ['spot', 'public'], 'GET', {'cost': 1}) + spot_public_get_market_tickers = spotPublicGetMarketTickers = Entry('market/tickers', ['spot', 'public'], 'GET', {'cost': 1}) + spot_public_get_market_detail = spotPublicGetMarketDetail = Entry('market/detail', ['spot', 'public'], 'GET', {'cost': 1}) + spot_public_get_market_depth = spotPublicGetMarketDepth = Entry('market/depth', ['spot', 'public'], 'GET', {'cost': 1}) + spot_public_get_market_trade = spotPublicGetMarketTrade = Entry('market/trade', ['spot', 'public'], 'GET', {'cost': 1}) + spot_public_get_market_history_trade = spotPublicGetMarketHistoryTrade = Entry('market/history/trade', ['spot', 'public'], 'GET', {'cost': 1}) + spot_public_get_market_etp = spotPublicGetMarketEtp = Entry('market/etp', ['spot', 'public'], 'GET', {'cost': 1}) + spot_public_get_v2_etp_reference = spotPublicGetV2EtpReference = Entry('v2/etp/reference', ['spot', 'public'], 'GET', {'cost': 1}) + spot_public_get_v2_etp_rebalance = spotPublicGetV2EtpRebalance = Entry('v2/etp/rebalance', ['spot', 'public'], 'GET', {'cost': 1}) + spot_private_get_v1_account_accounts = spotPrivateGetV1AccountAccounts = Entry('v1/account/accounts', ['spot', 'private'], 'GET', {'cost': 0.2}) + spot_private_get_v1_account_accounts_account_id_balance = spotPrivateGetV1AccountAccountsAccountIdBalance = Entry('v1/account/accounts/{account-id}/balance', ['spot', 'private'], 'GET', {'cost': 0.2}) + spot_private_get_v2_account_valuation = spotPrivateGetV2AccountValuation = Entry('v2/account/valuation', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_v2_account_asset_valuation = spotPrivateGetV2AccountAssetValuation = Entry('v2/account/asset-valuation', ['spot', 'private'], 'GET', {'cost': 0.2}) + spot_private_get_v1_account_history = spotPrivateGetV1AccountHistory = Entry('v1/account/history', ['spot', 'private'], 'GET', {'cost': 4}) + spot_private_get_v2_account_ledger = spotPrivateGetV2AccountLedger = Entry('v2/account/ledger', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_v2_point_account = spotPrivateGetV2PointAccount = Entry('v2/point/account', ['spot', 'private'], 'GET', {'cost': 5}) + spot_private_get_v2_account_deposit_address = spotPrivateGetV2AccountDepositAddress = Entry('v2/account/deposit/address', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_v2_account_withdraw_quota = spotPrivateGetV2AccountWithdrawQuota = Entry('v2/account/withdraw/quota', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_v2_account_withdraw_address = spotPrivateGetV2AccountWithdrawAddress = Entry('v2/account/withdraw/address', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_v2_reference_currencies = spotPrivateGetV2ReferenceCurrencies = Entry('v2/reference/currencies', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_v1_query_deposit_withdraw = spotPrivateGetV1QueryDepositWithdraw = Entry('v1/query/deposit-withdraw', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_v1_query_withdraw_client_order_id = spotPrivateGetV1QueryWithdrawClientOrderId = Entry('v1/query/withdraw/client-order-id', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_v2_user_api_key = spotPrivateGetV2UserApiKey = Entry('v2/user/api-key', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_v2_user_uid = spotPrivateGetV2UserUid = Entry('v2/user/uid', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_v2_sub_user_user_list = spotPrivateGetV2SubUserUserList = Entry('v2/sub-user/user-list', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_v2_sub_user_user_state = spotPrivateGetV2SubUserUserState = Entry('v2/sub-user/user-state', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_v2_sub_user_account_list = spotPrivateGetV2SubUserAccountList = Entry('v2/sub-user/account-list', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_v2_sub_user_deposit_address = spotPrivateGetV2SubUserDepositAddress = Entry('v2/sub-user/deposit-address', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_v2_sub_user_query_deposit = spotPrivateGetV2SubUserQueryDeposit = Entry('v2/sub-user/query-deposit', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_v1_subuser_aggregate_balance = spotPrivateGetV1SubuserAggregateBalance = Entry('v1/subuser/aggregate-balance', ['spot', 'private'], 'GET', {'cost': 10}) + spot_private_get_v1_account_accounts_sub_uid = spotPrivateGetV1AccountAccountsSubUid = Entry('v1/account/accounts/{sub-uid}', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_v1_order_openorders = spotPrivateGetV1OrderOpenOrders = Entry('v1/order/openOrders', ['spot', 'private'], 'GET', {'cost': 0.4}) + spot_private_get_v1_order_orders_order_id = spotPrivateGetV1OrderOrdersOrderId = Entry('v1/order/orders/{order-id}', ['spot', 'private'], 'GET', {'cost': 0.4}) + spot_private_get_v1_order_orders_getclientorder = spotPrivateGetV1OrderOrdersGetClientOrder = Entry('v1/order/orders/getClientOrder', ['spot', 'private'], 'GET', {'cost': 0.4}) + spot_private_get_v1_order_orders_order_id_matchresult = spotPrivateGetV1OrderOrdersOrderIdMatchresult = Entry('v1/order/orders/{order-id}/matchresult', ['spot', 'private'], 'GET', {'cost': 0.4}) + spot_private_get_v1_order_orders_order_id_matchresults = spotPrivateGetV1OrderOrdersOrderIdMatchresults = Entry('v1/order/orders/{order-id}/matchresults', ['spot', 'private'], 'GET', {'cost': 0.4}) + spot_private_get_v1_order_orders = spotPrivateGetV1OrderOrders = Entry('v1/order/orders', ['spot', 'private'], 'GET', {'cost': 0.4}) + spot_private_get_v1_order_history = spotPrivateGetV1OrderHistory = Entry('v1/order/history', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_v1_order_matchresults = spotPrivateGetV1OrderMatchresults = Entry('v1/order/matchresults', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_v2_reference_transact_fee_rate = spotPrivateGetV2ReferenceTransactFeeRate = Entry('v2/reference/transact-fee-rate', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_v2_algo_orders_opening = spotPrivateGetV2AlgoOrdersOpening = Entry('v2/algo-orders/opening', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_v2_algo_orders_history = spotPrivateGetV2AlgoOrdersHistory = Entry('v2/algo-orders/history', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_v2_algo_orders_specific = spotPrivateGetV2AlgoOrdersSpecific = Entry('v2/algo-orders/specific', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_v1_margin_loan_info = spotPrivateGetV1MarginLoanInfo = Entry('v1/margin/loan-info', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_v1_margin_loan_orders = spotPrivateGetV1MarginLoanOrders = Entry('v1/margin/loan-orders', ['spot', 'private'], 'GET', {'cost': 0.2}) + spot_private_get_v1_margin_accounts_balance = spotPrivateGetV1MarginAccountsBalance = Entry('v1/margin/accounts/balance', ['spot', 'private'], 'GET', {'cost': 0.2}) + spot_private_get_v1_cross_margin_loan_info = spotPrivateGetV1CrossMarginLoanInfo = Entry('v1/cross-margin/loan-info', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_v1_cross_margin_loan_orders = spotPrivateGetV1CrossMarginLoanOrders = Entry('v1/cross-margin/loan-orders', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_v1_cross_margin_accounts_balance = spotPrivateGetV1CrossMarginAccountsBalance = Entry('v1/cross-margin/accounts/balance', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_v2_account_repayment = spotPrivateGetV2AccountRepayment = Entry('v2/account/repayment', ['spot', 'private'], 'GET', {'cost': 5}) + spot_private_get_v1_stable_coin_quote = spotPrivateGetV1StableCoinQuote = Entry('v1/stable-coin/quote', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_v1_stable_coin_exchange_rate = spotPrivateGetV1StableCoinExchangeRate = Entry('v1/stable_coin/exchange_rate', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_v2_etp_transactions = spotPrivateGetV2EtpTransactions = Entry('v2/etp/transactions', ['spot', 'private'], 'GET', {'cost': 5}) + spot_private_get_v2_etp_transaction = spotPrivateGetV2EtpTransaction = Entry('v2/etp/transaction', ['spot', 'private'], 'GET', {'cost': 5}) + spot_private_get_v2_etp_limit = spotPrivateGetV2EtpLimit = Entry('v2/etp/limit', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_post_v1_account_transfer = spotPrivatePostV1AccountTransfer = Entry('v1/account/transfer', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_post_v1_futures_transfer = spotPrivatePostV1FuturesTransfer = Entry('v1/futures/transfer', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_post_v2_point_transfer = spotPrivatePostV2PointTransfer = Entry('v2/point/transfer', ['spot', 'private'], 'POST', {'cost': 5}) + spot_private_post_v2_account_transfer = spotPrivatePostV2AccountTransfer = Entry('v2/account/transfer', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_post_v1_dw_withdraw_api_create = spotPrivatePostV1DwWithdrawApiCreate = Entry('v1/dw/withdraw/api/create', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_post_v1_dw_withdraw_virtual_withdraw_id_cancel = spotPrivatePostV1DwWithdrawVirtualWithdrawIdCancel = Entry('v1/dw/withdraw-virtual/{withdraw-id}/cancel', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_post_v2_sub_user_deduct_mode = spotPrivatePostV2SubUserDeductMode = Entry('v2/sub-user/deduct-mode', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_post_v2_sub_user_creation = spotPrivatePostV2SubUserCreation = Entry('v2/sub-user/creation', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_post_v2_sub_user_management = spotPrivatePostV2SubUserManagement = Entry('v2/sub-user/management', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_post_v2_sub_user_tradable_market = spotPrivatePostV2SubUserTradableMarket = Entry('v2/sub-user/tradable-market', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_post_v2_sub_user_transferability = spotPrivatePostV2SubUserTransferability = Entry('v2/sub-user/transferability', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_post_v2_sub_user_api_key_generation = spotPrivatePostV2SubUserApiKeyGeneration = Entry('v2/sub-user/api-key-generation', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_post_v2_sub_user_api_key_modification = spotPrivatePostV2SubUserApiKeyModification = Entry('v2/sub-user/api-key-modification', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_post_v2_sub_user_api_key_deletion = spotPrivatePostV2SubUserApiKeyDeletion = Entry('v2/sub-user/api-key-deletion', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_post_v1_subuser_transfer = spotPrivatePostV1SubuserTransfer = Entry('v1/subuser/transfer', ['spot', 'private'], 'POST', {'cost': 10}) + spot_private_post_v1_trust_user_active_credit = spotPrivatePostV1TrustUserActiveCredit = Entry('v1/trust/user/active/credit', ['spot', 'private'], 'POST', {'cost': 10}) + spot_private_post_v1_order_orders_place = spotPrivatePostV1OrderOrdersPlace = Entry('v1/order/orders/place', ['spot', 'private'], 'POST', {'cost': 0.2}) + spot_private_post_v1_order_batch_orders = spotPrivatePostV1OrderBatchOrders = Entry('v1/order/batch-orders', ['spot', 'private'], 'POST', {'cost': 0.4}) + spot_private_post_v1_order_auto_place = spotPrivatePostV1OrderAutoPlace = Entry('v1/order/auto/place', ['spot', 'private'], 'POST', {'cost': 0.2}) + spot_private_post_v1_order_orders_order_id_submitcancel = spotPrivatePostV1OrderOrdersOrderIdSubmitcancel = Entry('v1/order/orders/{order-id}/submitcancel', ['spot', 'private'], 'POST', {'cost': 0.2}) + spot_private_post_v1_order_orders_submitcancelclientorder = spotPrivatePostV1OrderOrdersSubmitCancelClientOrder = Entry('v1/order/orders/submitCancelClientOrder', ['spot', 'private'], 'POST', {'cost': 0.2}) + spot_private_post_v1_order_orders_batchcancelopenorders = spotPrivatePostV1OrderOrdersBatchCancelOpenOrders = Entry('v1/order/orders/batchCancelOpenOrders', ['spot', 'private'], 'POST', {'cost': 0.4}) + spot_private_post_v1_order_orders_batchcancel = spotPrivatePostV1OrderOrdersBatchcancel = Entry('v1/order/orders/batchcancel', ['spot', 'private'], 'POST', {'cost': 0.4}) + spot_private_post_v2_algo_orders_cancel_all_after = spotPrivatePostV2AlgoOrdersCancelAllAfter = Entry('v2/algo-orders/cancel-all-after', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_post_v2_algo_orders = spotPrivatePostV2AlgoOrders = Entry('v2/algo-orders', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_post_v2_algo_orders_cancellation = spotPrivatePostV2AlgoOrdersCancellation = Entry('v2/algo-orders/cancellation', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_post_v2_account_repayment = spotPrivatePostV2AccountRepayment = Entry('v2/account/repayment', ['spot', 'private'], 'POST', {'cost': 5}) + spot_private_post_v1_dw_transfer_in_margin = spotPrivatePostV1DwTransferInMargin = Entry('v1/dw/transfer-in/margin', ['spot', 'private'], 'POST', {'cost': 10}) + spot_private_post_v1_dw_transfer_out_margin = spotPrivatePostV1DwTransferOutMargin = Entry('v1/dw/transfer-out/margin', ['spot', 'private'], 'POST', {'cost': 10}) + spot_private_post_v1_margin_orders = spotPrivatePostV1MarginOrders = Entry('v1/margin/orders', ['spot', 'private'], 'POST', {'cost': 10}) + spot_private_post_v1_margin_orders_order_id_repay = spotPrivatePostV1MarginOrdersOrderIdRepay = Entry('v1/margin/orders/{order-id}/repay', ['spot', 'private'], 'POST', {'cost': 10}) + spot_private_post_v1_cross_margin_transfer_in = spotPrivatePostV1CrossMarginTransferIn = Entry('v1/cross-margin/transfer-in', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_post_v1_cross_margin_transfer_out = spotPrivatePostV1CrossMarginTransferOut = Entry('v1/cross-margin/transfer-out', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_post_v1_cross_margin_orders = spotPrivatePostV1CrossMarginOrders = Entry('v1/cross-margin/orders', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_post_v1_cross_margin_orders_order_id_repay = spotPrivatePostV1CrossMarginOrdersOrderIdRepay = Entry('v1/cross-margin/orders/{order-id}/repay', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_post_v1_stable_coin_exchange = spotPrivatePostV1StableCoinExchange = Entry('v1/stable-coin/exchange', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_post_v2_etp_creation = spotPrivatePostV2EtpCreation = Entry('v2/etp/creation', ['spot', 'private'], 'POST', {'cost': 5}) + spot_private_post_v2_etp_redemption = spotPrivatePostV2EtpRedemption = Entry('v2/etp/redemption', ['spot', 'private'], 'POST', {'cost': 5}) + spot_private_post_v2_etp_transactid_cancel = spotPrivatePostV2EtpTransactIdCancel = Entry('v2/etp/{transactId}/cancel', ['spot', 'private'], 'POST', {'cost': 10}) + spot_private_post_v2_etp_batch_cancel = spotPrivatePostV2EtpBatchCancel = Entry('v2/etp/batch-cancel', ['spot', 'private'], 'POST', {'cost': 50}) + contract_public_get_api_v1_timestamp = contractPublicGetApiV1Timestamp = Entry('api/v1/timestamp', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_heartbeat = contractPublicGetHeartbeat = Entry('heartbeat/', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_api_v1_contract_contract_info = contractPublicGetApiV1ContractContractInfo = Entry('api/v1/contract_contract_info', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_api_v1_contract_index = contractPublicGetApiV1ContractIndex = Entry('api/v1/contract_index', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_api_v1_contract_query_elements = contractPublicGetApiV1ContractQueryElements = Entry('api/v1/contract_query_elements', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_api_v1_contract_price_limit = contractPublicGetApiV1ContractPriceLimit = Entry('api/v1/contract_price_limit', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_api_v1_contract_open_interest = contractPublicGetApiV1ContractOpenInterest = Entry('api/v1/contract_open_interest', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_api_v1_contract_delivery_price = contractPublicGetApiV1ContractDeliveryPrice = Entry('api/v1/contract_delivery_price', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_market_depth = contractPublicGetMarketDepth = Entry('market/depth', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_market_bbo = contractPublicGetMarketBbo = Entry('market/bbo', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_market_history_kline = contractPublicGetMarketHistoryKline = Entry('market/history/kline', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_index_market_history_mark_price_kline = contractPublicGetIndexMarketHistoryMarkPriceKline = Entry('index/market/history/mark_price_kline', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_market_detail_merged = contractPublicGetMarketDetailMerged = Entry('market/detail/merged', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_market_detail_batch_merged = contractPublicGetMarketDetailBatchMerged = Entry('market/detail/batch_merged', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_v2_market_detail_batch_merged = contractPublicGetV2MarketDetailBatchMerged = Entry('v2/market/detail/batch_merged', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_market_trade = contractPublicGetMarketTrade = Entry('market/trade', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_market_history_trade = contractPublicGetMarketHistoryTrade = Entry('market/history/trade', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_api_v1_contract_risk_info = contractPublicGetApiV1ContractRiskInfo = Entry('api/v1/contract_risk_info', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_api_v1_contract_insurance_fund = contractPublicGetApiV1ContractInsuranceFund = Entry('api/v1/contract_insurance_fund', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_api_v1_contract_adjustfactor = contractPublicGetApiV1ContractAdjustfactor = Entry('api/v1/contract_adjustfactor', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_api_v1_contract_his_open_interest = contractPublicGetApiV1ContractHisOpenInterest = Entry('api/v1/contract_his_open_interest', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_api_v1_contract_ladder_margin = contractPublicGetApiV1ContractLadderMargin = Entry('api/v1/contract_ladder_margin', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_api_v1_contract_api_state = contractPublicGetApiV1ContractApiState = Entry('api/v1/contract_api_state', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_api_v1_contract_elite_account_ratio = contractPublicGetApiV1ContractEliteAccountRatio = Entry('api/v1/contract_elite_account_ratio', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_api_v1_contract_elite_position_ratio = contractPublicGetApiV1ContractElitePositionRatio = Entry('api/v1/contract_elite_position_ratio', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_api_v1_contract_liquidation_orders = contractPublicGetApiV1ContractLiquidationOrders = Entry('api/v1/contract_liquidation_orders', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_api_v1_contract_settlement_records = contractPublicGetApiV1ContractSettlementRecords = Entry('api/v1/contract_settlement_records', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_index_market_history_index = contractPublicGetIndexMarketHistoryIndex = Entry('index/market/history/index', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_index_market_history_basis = contractPublicGetIndexMarketHistoryBasis = Entry('index/market/history/basis', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_api_v1_contract_estimated_settlement_price = contractPublicGetApiV1ContractEstimatedSettlementPrice = Entry('api/v1/contract_estimated_settlement_price', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_api_v3_contract_liquidation_orders = contractPublicGetApiV3ContractLiquidationOrders = Entry('api/v3/contract_liquidation_orders', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_swap_api_v1_swap_contract_info = contractPublicGetSwapApiV1SwapContractInfo = Entry('swap-api/v1/swap_contract_info', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_swap_api_v1_swap_index = contractPublicGetSwapApiV1SwapIndex = Entry('swap-api/v1/swap_index', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_swap_api_v1_swap_query_elements = contractPublicGetSwapApiV1SwapQueryElements = Entry('swap-api/v1/swap_query_elements', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_swap_api_v1_swap_price_limit = contractPublicGetSwapApiV1SwapPriceLimit = Entry('swap-api/v1/swap_price_limit', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_swap_api_v1_swap_open_interest = contractPublicGetSwapApiV1SwapOpenInterest = Entry('swap-api/v1/swap_open_interest', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_swap_ex_market_depth = contractPublicGetSwapExMarketDepth = Entry('swap-ex/market/depth', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_swap_ex_market_bbo = contractPublicGetSwapExMarketBbo = Entry('swap-ex/market/bbo', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_swap_ex_market_history_kline = contractPublicGetSwapExMarketHistoryKline = Entry('swap-ex/market/history/kline', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_index_market_history_swap_mark_price_kline = contractPublicGetIndexMarketHistorySwapMarkPriceKline = Entry('index/market/history/swap_mark_price_kline', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_swap_ex_market_detail_merged = contractPublicGetSwapExMarketDetailMerged = Entry('swap-ex/market/detail/merged', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_v2_swap_ex_market_detail_batch_merged = contractPublicGetV2SwapExMarketDetailBatchMerged = Entry('v2/swap-ex/market/detail/batch_merged', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_index_market_history_swap_premium_index_kline = contractPublicGetIndexMarketHistorySwapPremiumIndexKline = Entry('index/market/history/swap_premium_index_kline', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_swap_ex_market_detail_batch_merged = contractPublicGetSwapExMarketDetailBatchMerged = Entry('swap-ex/market/detail/batch_merged', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_swap_ex_market_trade = contractPublicGetSwapExMarketTrade = Entry('swap-ex/market/trade', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_swap_ex_market_history_trade = contractPublicGetSwapExMarketHistoryTrade = Entry('swap-ex/market/history/trade', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_swap_api_v1_swap_risk_info = contractPublicGetSwapApiV1SwapRiskInfo = Entry('swap-api/v1/swap_risk_info', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_swap_api_v1_swap_insurance_fund = contractPublicGetSwapApiV1SwapInsuranceFund = Entry('swap-api/v1/swap_insurance_fund', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_swap_api_v1_swap_adjustfactor = contractPublicGetSwapApiV1SwapAdjustfactor = Entry('swap-api/v1/swap_adjustfactor', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_swap_api_v1_swap_his_open_interest = contractPublicGetSwapApiV1SwapHisOpenInterest = Entry('swap-api/v1/swap_his_open_interest', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_swap_api_v1_swap_ladder_margin = contractPublicGetSwapApiV1SwapLadderMargin = Entry('swap-api/v1/swap_ladder_margin', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_swap_api_v1_swap_api_state = contractPublicGetSwapApiV1SwapApiState = Entry('swap-api/v1/swap_api_state', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_swap_api_v1_swap_elite_account_ratio = contractPublicGetSwapApiV1SwapEliteAccountRatio = Entry('swap-api/v1/swap_elite_account_ratio', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_swap_api_v1_swap_elite_position_ratio = contractPublicGetSwapApiV1SwapElitePositionRatio = Entry('swap-api/v1/swap_elite_position_ratio', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_swap_api_v1_swap_estimated_settlement_price = contractPublicGetSwapApiV1SwapEstimatedSettlementPrice = Entry('swap-api/v1/swap_estimated_settlement_price', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_swap_api_v1_swap_liquidation_orders = contractPublicGetSwapApiV1SwapLiquidationOrders = Entry('swap-api/v1/swap_liquidation_orders', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_swap_api_v1_swap_settlement_records = contractPublicGetSwapApiV1SwapSettlementRecords = Entry('swap-api/v1/swap_settlement_records', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_swap_api_v1_swap_funding_rate = contractPublicGetSwapApiV1SwapFundingRate = Entry('swap-api/v1/swap_funding_rate', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_swap_api_v1_swap_batch_funding_rate = contractPublicGetSwapApiV1SwapBatchFundingRate = Entry('swap-api/v1/swap_batch_funding_rate', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_swap_api_v1_swap_historical_funding_rate = contractPublicGetSwapApiV1SwapHistoricalFundingRate = Entry('swap-api/v1/swap_historical_funding_rate', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_swap_api_v3_swap_liquidation_orders = contractPublicGetSwapApiV3SwapLiquidationOrders = Entry('swap-api/v3/swap_liquidation_orders', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_index_market_history_swap_estimated_rate_kline = contractPublicGetIndexMarketHistorySwapEstimatedRateKline = Entry('index/market/history/swap_estimated_rate_kline', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_index_market_history_swap_basis = contractPublicGetIndexMarketHistorySwapBasis = Entry('index/market/history/swap_basis', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_linear_swap_api_v1_swap_contract_info = contractPublicGetLinearSwapApiV1SwapContractInfo = Entry('linear-swap-api/v1/swap_contract_info', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_linear_swap_api_v1_swap_index = contractPublicGetLinearSwapApiV1SwapIndex = Entry('linear-swap-api/v1/swap_index', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_linear_swap_api_v1_swap_query_elements = contractPublicGetLinearSwapApiV1SwapQueryElements = Entry('linear-swap-api/v1/swap_query_elements', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_linear_swap_api_v1_swap_price_limit = contractPublicGetLinearSwapApiV1SwapPriceLimit = Entry('linear-swap-api/v1/swap_price_limit', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_linear_swap_api_v1_swap_open_interest = contractPublicGetLinearSwapApiV1SwapOpenInterest = Entry('linear-swap-api/v1/swap_open_interest', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_linear_swap_ex_market_depth = contractPublicGetLinearSwapExMarketDepth = Entry('linear-swap-ex/market/depth', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_linear_swap_ex_market_bbo = contractPublicGetLinearSwapExMarketBbo = Entry('linear-swap-ex/market/bbo', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_linear_swap_ex_market_history_kline = contractPublicGetLinearSwapExMarketHistoryKline = Entry('linear-swap-ex/market/history/kline', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_index_market_history_linear_swap_mark_price_kline = contractPublicGetIndexMarketHistoryLinearSwapMarkPriceKline = Entry('index/market/history/linear_swap_mark_price_kline', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_linear_swap_ex_market_detail_merged = contractPublicGetLinearSwapExMarketDetailMerged = Entry('linear-swap-ex/market/detail/merged', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_linear_swap_ex_market_detail_batch_merged = contractPublicGetLinearSwapExMarketDetailBatchMerged = Entry('linear-swap-ex/market/detail/batch_merged', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_v2_linear_swap_ex_market_detail_batch_merged = contractPublicGetV2LinearSwapExMarketDetailBatchMerged = Entry('v2/linear-swap-ex/market/detail/batch_merged', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_linear_swap_ex_market_trade = contractPublicGetLinearSwapExMarketTrade = Entry('linear-swap-ex/market/trade', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_linear_swap_ex_market_history_trade = contractPublicGetLinearSwapExMarketHistoryTrade = Entry('linear-swap-ex/market/history/trade', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_linear_swap_api_v1_swap_risk_info = contractPublicGetLinearSwapApiV1SwapRiskInfo = Entry('linear-swap-api/v1/swap_risk_info', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_swap_api_v1_linear_swap_api_v1_swap_insurance_fund = contractPublicGetSwapApiV1LinearSwapApiV1SwapInsuranceFund = Entry('swap-api/v1/linear-swap-api/v1/swap_insurance_fund', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_linear_swap_api_v1_swap_adjustfactor = contractPublicGetLinearSwapApiV1SwapAdjustfactor = Entry('linear-swap-api/v1/swap_adjustfactor', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_linear_swap_api_v1_swap_cross_adjustfactor = contractPublicGetLinearSwapApiV1SwapCrossAdjustfactor = Entry('linear-swap-api/v1/swap_cross_adjustfactor', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_linear_swap_api_v1_swap_his_open_interest = contractPublicGetLinearSwapApiV1SwapHisOpenInterest = Entry('linear-swap-api/v1/swap_his_open_interest', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_linear_swap_api_v1_swap_ladder_margin = contractPublicGetLinearSwapApiV1SwapLadderMargin = Entry('linear-swap-api/v1/swap_ladder_margin', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_linear_swap_api_v1_swap_cross_ladder_margin = contractPublicGetLinearSwapApiV1SwapCrossLadderMargin = Entry('linear-swap-api/v1/swap_cross_ladder_margin', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_linear_swap_api_v1_swap_api_state = contractPublicGetLinearSwapApiV1SwapApiState = Entry('linear-swap-api/v1/swap_api_state', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_linear_swap_api_v1_swap_cross_transfer_state = contractPublicGetLinearSwapApiV1SwapCrossTransferState = Entry('linear-swap-api/v1/swap_cross_transfer_state', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_linear_swap_api_v1_swap_cross_trade_state = contractPublicGetLinearSwapApiV1SwapCrossTradeState = Entry('linear-swap-api/v1/swap_cross_trade_state', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_linear_swap_api_v1_swap_elite_account_ratio = contractPublicGetLinearSwapApiV1SwapEliteAccountRatio = Entry('linear-swap-api/v1/swap_elite_account_ratio', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_linear_swap_api_v1_swap_elite_position_ratio = contractPublicGetLinearSwapApiV1SwapElitePositionRatio = Entry('linear-swap-api/v1/swap_elite_position_ratio', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_linear_swap_api_v1_swap_liquidation_orders = contractPublicGetLinearSwapApiV1SwapLiquidationOrders = Entry('linear-swap-api/v1/swap_liquidation_orders', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_linear_swap_api_v1_swap_settlement_records = contractPublicGetLinearSwapApiV1SwapSettlementRecords = Entry('linear-swap-api/v1/swap_settlement_records', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_linear_swap_api_v1_swap_funding_rate = contractPublicGetLinearSwapApiV1SwapFundingRate = Entry('linear-swap-api/v1/swap_funding_rate', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_linear_swap_api_v1_swap_batch_funding_rate = contractPublicGetLinearSwapApiV1SwapBatchFundingRate = Entry('linear-swap-api/v1/swap_batch_funding_rate', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_linear_swap_api_v1_swap_historical_funding_rate = contractPublicGetLinearSwapApiV1SwapHistoricalFundingRate = Entry('linear-swap-api/v1/swap_historical_funding_rate', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_linear_swap_api_v3_swap_liquidation_orders = contractPublicGetLinearSwapApiV3SwapLiquidationOrders = Entry('linear-swap-api/v3/swap_liquidation_orders', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_index_market_history_linear_swap_premium_index_kline = contractPublicGetIndexMarketHistoryLinearSwapPremiumIndexKline = Entry('index/market/history/linear_swap_premium_index_kline', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_index_market_history_linear_swap_estimated_rate_kline = contractPublicGetIndexMarketHistoryLinearSwapEstimatedRateKline = Entry('index/market/history/linear_swap_estimated_rate_kline', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_index_market_history_linear_swap_basis = contractPublicGetIndexMarketHistoryLinearSwapBasis = Entry('index/market/history/linear_swap_basis', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_linear_swap_api_v1_swap_estimated_settlement_price = contractPublicGetLinearSwapApiV1SwapEstimatedSettlementPrice = Entry('linear-swap-api/v1/swap_estimated_settlement_price', ['contract', 'public'], 'GET', {'cost': 1}) + contract_private_get_api_v1_contract_sub_auth_list = contractPrivateGetApiV1ContractSubAuthList = Entry('api/v1/contract_sub_auth_list', ['contract', 'private'], 'GET', {'cost': 1}) + contract_private_get_api_v1_contract_api_trading_status = contractPrivateGetApiV1ContractApiTradingStatus = Entry('api/v1/contract_api_trading_status', ['contract', 'private'], 'GET', {'cost': 1}) + contract_private_get_swap_api_v1_swap_sub_auth_list = contractPrivateGetSwapApiV1SwapSubAuthList = Entry('swap-api/v1/swap_sub_auth_list', ['contract', 'private'], 'GET', {'cost': 1}) + contract_private_get_swap_api_v1_swap_api_trading_status = contractPrivateGetSwapApiV1SwapApiTradingStatus = Entry('swap-api/v1/swap_api_trading_status', ['contract', 'private'], 'GET', {'cost': 1}) + contract_private_get_linear_swap_api_v1_swap_sub_auth_list = contractPrivateGetLinearSwapApiV1SwapSubAuthList = Entry('linear-swap-api/v1/swap_sub_auth_list', ['contract', 'private'], 'GET', {'cost': 1}) + contract_private_get_linear_swap_api_v1_swap_api_trading_status = contractPrivateGetLinearSwapApiV1SwapApiTradingStatus = Entry('linear-swap-api/v1/swap_api_trading_status', ['contract', 'private'], 'GET', {'cost': 1}) + contract_private_get_linear_swap_api_v1_swap_cross_position_side = contractPrivateGetLinearSwapApiV1SwapCrossPositionSide = Entry('linear-swap-api/v1/swap_cross_position_side', ['contract', 'private'], 'GET', {'cost': 1}) + contract_private_get_linear_swap_api_v1_swap_position_side = contractPrivateGetLinearSwapApiV1SwapPositionSide = Entry('linear-swap-api/v1/swap_position_side', ['contract', 'private'], 'GET', {'cost': 1}) + contract_private_get_linear_swap_api_v3_unified_account_info = contractPrivateGetLinearSwapApiV3UnifiedAccountInfo = Entry('linear-swap-api/v3/unified_account_info', ['contract', 'private'], 'GET', {'cost': 1}) + contract_private_get_linear_swap_api_v3_fix_position_margin_change_record = contractPrivateGetLinearSwapApiV3FixPositionMarginChangeRecord = Entry('linear-swap-api/v3/fix_position_margin_change_record', ['contract', 'private'], 'GET', {'cost': 1}) + contract_private_get_linear_swap_api_v3_swap_unified_account_type = contractPrivateGetLinearSwapApiV3SwapUnifiedAccountType = Entry('linear-swap-api/v3/swap_unified_account_type', ['contract', 'private'], 'GET', {'cost': 1}) + contract_private_get_linear_swap_api_v3_linear_swap_overview_account_info = contractPrivateGetLinearSwapApiV3LinearSwapOverviewAccountInfo = Entry('linear-swap-api/v3/linear_swap_overview_account_info', ['contract', 'private'], 'GET', {'cost': 1}) + contract_private_post_api_v1_contract_balance_valuation = contractPrivatePostApiV1ContractBalanceValuation = Entry('api/v1/contract_balance_valuation', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_account_info = contractPrivatePostApiV1ContractAccountInfo = Entry('api/v1/contract_account_info', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_position_info = contractPrivatePostApiV1ContractPositionInfo = Entry('api/v1/contract_position_info', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_sub_auth = contractPrivatePostApiV1ContractSubAuth = Entry('api/v1/contract_sub_auth', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_sub_account_list = contractPrivatePostApiV1ContractSubAccountList = Entry('api/v1/contract_sub_account_list', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_sub_account_info_list = contractPrivatePostApiV1ContractSubAccountInfoList = Entry('api/v1/contract_sub_account_info_list', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_sub_account_info = contractPrivatePostApiV1ContractSubAccountInfo = Entry('api/v1/contract_sub_account_info', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_sub_position_info = contractPrivatePostApiV1ContractSubPositionInfo = Entry('api/v1/contract_sub_position_info', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_financial_record = contractPrivatePostApiV1ContractFinancialRecord = Entry('api/v1/contract_financial_record', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_financial_record_exact = contractPrivatePostApiV1ContractFinancialRecordExact = Entry('api/v1/contract_financial_record_exact', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_user_settlement_records = contractPrivatePostApiV1ContractUserSettlementRecords = Entry('api/v1/contract_user_settlement_records', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_order_limit = contractPrivatePostApiV1ContractOrderLimit = Entry('api/v1/contract_order_limit', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_fee = contractPrivatePostApiV1ContractFee = Entry('api/v1/contract_fee', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_transfer_limit = contractPrivatePostApiV1ContractTransferLimit = Entry('api/v1/contract_transfer_limit', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_position_limit = contractPrivatePostApiV1ContractPositionLimit = Entry('api/v1/contract_position_limit', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_account_position_info = contractPrivatePostApiV1ContractAccountPositionInfo = Entry('api/v1/contract_account_position_info', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_master_sub_transfer = contractPrivatePostApiV1ContractMasterSubTransfer = Entry('api/v1/contract_master_sub_transfer', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_master_sub_transfer_record = contractPrivatePostApiV1ContractMasterSubTransferRecord = Entry('api/v1/contract_master_sub_transfer_record', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_available_level_rate = contractPrivatePostApiV1ContractAvailableLevelRate = Entry('api/v1/contract_available_level_rate', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v3_contract_financial_record = contractPrivatePostApiV3ContractFinancialRecord = Entry('api/v3/contract_financial_record', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v3_contract_financial_record_exact = contractPrivatePostApiV3ContractFinancialRecordExact = Entry('api/v3/contract_financial_record_exact', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_cancel_after = contractPrivatePostApiV1ContractCancelAfter = Entry('api/v1/contract-cancel-after', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_order = contractPrivatePostApiV1ContractOrder = Entry('api/v1/contract_order', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_batchorder = contractPrivatePostApiV1ContractBatchorder = Entry('api/v1/contract_batchorder', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_cancel = contractPrivatePostApiV1ContractCancel = Entry('api/v1/contract_cancel', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_cancelall = contractPrivatePostApiV1ContractCancelall = Entry('api/v1/contract_cancelall', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_switch_lever_rate = contractPrivatePostApiV1ContractSwitchLeverRate = Entry('api/v1/contract_switch_lever_rate', ['contract', 'private'], 'POST', {'cost': 30}) + contract_private_post_api_v1_lightning_close_position = contractPrivatePostApiV1LightningClosePosition = Entry('api/v1/lightning_close_position', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_order_info = contractPrivatePostApiV1ContractOrderInfo = Entry('api/v1/contract_order_info', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_order_detail = contractPrivatePostApiV1ContractOrderDetail = Entry('api/v1/contract_order_detail', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_openorders = contractPrivatePostApiV1ContractOpenorders = Entry('api/v1/contract_openorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_hisorders = contractPrivatePostApiV1ContractHisorders = Entry('api/v1/contract_hisorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_hisorders_exact = contractPrivatePostApiV1ContractHisordersExact = Entry('api/v1/contract_hisorders_exact', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_matchresults = contractPrivatePostApiV1ContractMatchresults = Entry('api/v1/contract_matchresults', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_matchresults_exact = contractPrivatePostApiV1ContractMatchresultsExact = Entry('api/v1/contract_matchresults_exact', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v3_contract_hisorders = contractPrivatePostApiV3ContractHisorders = Entry('api/v3/contract_hisorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v3_contract_hisorders_exact = contractPrivatePostApiV3ContractHisordersExact = Entry('api/v3/contract_hisorders_exact', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v3_contract_matchresults = contractPrivatePostApiV3ContractMatchresults = Entry('api/v3/contract_matchresults', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v3_contract_matchresults_exact = contractPrivatePostApiV3ContractMatchresultsExact = Entry('api/v3/contract_matchresults_exact', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_trigger_order = contractPrivatePostApiV1ContractTriggerOrder = Entry('api/v1/contract_trigger_order', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_trigger_cancel = contractPrivatePostApiV1ContractTriggerCancel = Entry('api/v1/contract_trigger_cancel', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_trigger_cancelall = contractPrivatePostApiV1ContractTriggerCancelall = Entry('api/v1/contract_trigger_cancelall', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_trigger_openorders = contractPrivatePostApiV1ContractTriggerOpenorders = Entry('api/v1/contract_trigger_openorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_trigger_hisorders = contractPrivatePostApiV1ContractTriggerHisorders = Entry('api/v1/contract_trigger_hisorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_tpsl_order = contractPrivatePostApiV1ContractTpslOrder = Entry('api/v1/contract_tpsl_order', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_tpsl_cancel = contractPrivatePostApiV1ContractTpslCancel = Entry('api/v1/contract_tpsl_cancel', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_tpsl_cancelall = contractPrivatePostApiV1ContractTpslCancelall = Entry('api/v1/contract_tpsl_cancelall', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_tpsl_openorders = contractPrivatePostApiV1ContractTpslOpenorders = Entry('api/v1/contract_tpsl_openorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_tpsl_hisorders = contractPrivatePostApiV1ContractTpslHisorders = Entry('api/v1/contract_tpsl_hisorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_relation_tpsl_order = contractPrivatePostApiV1ContractRelationTpslOrder = Entry('api/v1/contract_relation_tpsl_order', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_track_order = contractPrivatePostApiV1ContractTrackOrder = Entry('api/v1/contract_track_order', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_track_cancel = contractPrivatePostApiV1ContractTrackCancel = Entry('api/v1/contract_track_cancel', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_track_cancelall = contractPrivatePostApiV1ContractTrackCancelall = Entry('api/v1/contract_track_cancelall', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_track_openorders = contractPrivatePostApiV1ContractTrackOpenorders = Entry('api/v1/contract_track_openorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_track_hisorders = contractPrivatePostApiV1ContractTrackHisorders = Entry('api/v1/contract_track_hisorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_balance_valuation = contractPrivatePostSwapApiV1SwapBalanceValuation = Entry('swap-api/v1/swap_balance_valuation', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_account_info = contractPrivatePostSwapApiV1SwapAccountInfo = Entry('swap-api/v1/swap_account_info', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_position_info = contractPrivatePostSwapApiV1SwapPositionInfo = Entry('swap-api/v1/swap_position_info', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_account_position_info = contractPrivatePostSwapApiV1SwapAccountPositionInfo = Entry('swap-api/v1/swap_account_position_info', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_sub_auth = contractPrivatePostSwapApiV1SwapSubAuth = Entry('swap-api/v1/swap_sub_auth', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_sub_account_list = contractPrivatePostSwapApiV1SwapSubAccountList = Entry('swap-api/v1/swap_sub_account_list', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_sub_account_info_list = contractPrivatePostSwapApiV1SwapSubAccountInfoList = Entry('swap-api/v1/swap_sub_account_info_list', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_sub_account_info = contractPrivatePostSwapApiV1SwapSubAccountInfo = Entry('swap-api/v1/swap_sub_account_info', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_sub_position_info = contractPrivatePostSwapApiV1SwapSubPositionInfo = Entry('swap-api/v1/swap_sub_position_info', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_financial_record = contractPrivatePostSwapApiV1SwapFinancialRecord = Entry('swap-api/v1/swap_financial_record', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_financial_record_exact = contractPrivatePostSwapApiV1SwapFinancialRecordExact = Entry('swap-api/v1/swap_financial_record_exact', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_user_settlement_records = contractPrivatePostSwapApiV1SwapUserSettlementRecords = Entry('swap-api/v1/swap_user_settlement_records', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_available_level_rate = contractPrivatePostSwapApiV1SwapAvailableLevelRate = Entry('swap-api/v1/swap_available_level_rate', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_order_limit = contractPrivatePostSwapApiV1SwapOrderLimit = Entry('swap-api/v1/swap_order_limit', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_fee = contractPrivatePostSwapApiV1SwapFee = Entry('swap-api/v1/swap_fee', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_transfer_limit = contractPrivatePostSwapApiV1SwapTransferLimit = Entry('swap-api/v1/swap_transfer_limit', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_position_limit = contractPrivatePostSwapApiV1SwapPositionLimit = Entry('swap-api/v1/swap_position_limit', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_master_sub_transfer = contractPrivatePostSwapApiV1SwapMasterSubTransfer = Entry('swap-api/v1/swap_master_sub_transfer', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_master_sub_transfer_record = contractPrivatePostSwapApiV1SwapMasterSubTransferRecord = Entry('swap-api/v1/swap_master_sub_transfer_record', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v3_swap_financial_record = contractPrivatePostSwapApiV3SwapFinancialRecord = Entry('swap-api/v3/swap_financial_record', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v3_swap_financial_record_exact = contractPrivatePostSwapApiV3SwapFinancialRecordExact = Entry('swap-api/v3/swap_financial_record_exact', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_cancel_after = contractPrivatePostSwapApiV1SwapCancelAfter = Entry('swap-api/v1/swap-cancel-after', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_order = contractPrivatePostSwapApiV1SwapOrder = Entry('swap-api/v1/swap_order', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_batchorder = contractPrivatePostSwapApiV1SwapBatchorder = Entry('swap-api/v1/swap_batchorder', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_cancel = contractPrivatePostSwapApiV1SwapCancel = Entry('swap-api/v1/swap_cancel', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_cancelall = contractPrivatePostSwapApiV1SwapCancelall = Entry('swap-api/v1/swap_cancelall', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_lightning_close_position = contractPrivatePostSwapApiV1SwapLightningClosePosition = Entry('swap-api/v1/swap_lightning_close_position', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_switch_lever_rate = contractPrivatePostSwapApiV1SwapSwitchLeverRate = Entry('swap-api/v1/swap_switch_lever_rate', ['contract', 'private'], 'POST', {'cost': 30}) + contract_private_post_swap_api_v1_swap_order_info = contractPrivatePostSwapApiV1SwapOrderInfo = Entry('swap-api/v1/swap_order_info', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_order_detail = contractPrivatePostSwapApiV1SwapOrderDetail = Entry('swap-api/v1/swap_order_detail', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_openorders = contractPrivatePostSwapApiV1SwapOpenorders = Entry('swap-api/v1/swap_openorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_hisorders = contractPrivatePostSwapApiV1SwapHisorders = Entry('swap-api/v1/swap_hisorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_hisorders_exact = contractPrivatePostSwapApiV1SwapHisordersExact = Entry('swap-api/v1/swap_hisorders_exact', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_matchresults = contractPrivatePostSwapApiV1SwapMatchresults = Entry('swap-api/v1/swap_matchresults', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_matchresults_exact = contractPrivatePostSwapApiV1SwapMatchresultsExact = Entry('swap-api/v1/swap_matchresults_exact', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v3_swap_matchresults = contractPrivatePostSwapApiV3SwapMatchresults = Entry('swap-api/v3/swap_matchresults', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v3_swap_matchresults_exact = contractPrivatePostSwapApiV3SwapMatchresultsExact = Entry('swap-api/v3/swap_matchresults_exact', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v3_swap_hisorders = contractPrivatePostSwapApiV3SwapHisorders = Entry('swap-api/v3/swap_hisorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v3_swap_hisorders_exact = contractPrivatePostSwapApiV3SwapHisordersExact = Entry('swap-api/v3/swap_hisorders_exact', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_trigger_order = contractPrivatePostSwapApiV1SwapTriggerOrder = Entry('swap-api/v1/swap_trigger_order', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_trigger_cancel = contractPrivatePostSwapApiV1SwapTriggerCancel = Entry('swap-api/v1/swap_trigger_cancel', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_trigger_cancelall = contractPrivatePostSwapApiV1SwapTriggerCancelall = Entry('swap-api/v1/swap_trigger_cancelall', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_trigger_openorders = contractPrivatePostSwapApiV1SwapTriggerOpenorders = Entry('swap-api/v1/swap_trigger_openorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_trigger_hisorders = contractPrivatePostSwapApiV1SwapTriggerHisorders = Entry('swap-api/v1/swap_trigger_hisorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_tpsl_order = contractPrivatePostSwapApiV1SwapTpslOrder = Entry('swap-api/v1/swap_tpsl_order', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_tpsl_cancel = contractPrivatePostSwapApiV1SwapTpslCancel = Entry('swap-api/v1/swap_tpsl_cancel', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_tpsl_cancelall = contractPrivatePostSwapApiV1SwapTpslCancelall = Entry('swap-api/v1/swap_tpsl_cancelall', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_tpsl_openorders = contractPrivatePostSwapApiV1SwapTpslOpenorders = Entry('swap-api/v1/swap_tpsl_openorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_tpsl_hisorders = contractPrivatePostSwapApiV1SwapTpslHisorders = Entry('swap-api/v1/swap_tpsl_hisorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_relation_tpsl_order = contractPrivatePostSwapApiV1SwapRelationTpslOrder = Entry('swap-api/v1/swap_relation_tpsl_order', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_track_order = contractPrivatePostSwapApiV1SwapTrackOrder = Entry('swap-api/v1/swap_track_order', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_track_cancel = contractPrivatePostSwapApiV1SwapTrackCancel = Entry('swap-api/v1/swap_track_cancel', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_track_cancelall = contractPrivatePostSwapApiV1SwapTrackCancelall = Entry('swap-api/v1/swap_track_cancelall', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_track_openorders = contractPrivatePostSwapApiV1SwapTrackOpenorders = Entry('swap-api/v1/swap_track_openorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_track_hisorders = contractPrivatePostSwapApiV1SwapTrackHisorders = Entry('swap-api/v1/swap_track_hisorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_lever_position_limit = contractPrivatePostLinearSwapApiV1SwapLeverPositionLimit = Entry('linear-swap-api/v1/swap_lever_position_limit', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_lever_position_limit = contractPrivatePostLinearSwapApiV1SwapCrossLeverPositionLimit = Entry('linear-swap-api/v1/swap_cross_lever_position_limit', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_balance_valuation = contractPrivatePostLinearSwapApiV1SwapBalanceValuation = Entry('linear-swap-api/v1/swap_balance_valuation', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_account_info = contractPrivatePostLinearSwapApiV1SwapAccountInfo = Entry('linear-swap-api/v1/swap_account_info', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_account_info = contractPrivatePostLinearSwapApiV1SwapCrossAccountInfo = Entry('linear-swap-api/v1/swap_cross_account_info', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_position_info = contractPrivatePostLinearSwapApiV1SwapPositionInfo = Entry('linear-swap-api/v1/swap_position_info', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_position_info = contractPrivatePostLinearSwapApiV1SwapCrossPositionInfo = Entry('linear-swap-api/v1/swap_cross_position_info', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_account_position_info = contractPrivatePostLinearSwapApiV1SwapAccountPositionInfo = Entry('linear-swap-api/v1/swap_account_position_info', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_account_position_info = contractPrivatePostLinearSwapApiV1SwapCrossAccountPositionInfo = Entry('linear-swap-api/v1/swap_cross_account_position_info', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_sub_auth = contractPrivatePostLinearSwapApiV1SwapSubAuth = Entry('linear-swap-api/v1/swap_sub_auth', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_sub_account_list = contractPrivatePostLinearSwapApiV1SwapSubAccountList = Entry('linear-swap-api/v1/swap_sub_account_list', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_sub_account_list = contractPrivatePostLinearSwapApiV1SwapCrossSubAccountList = Entry('linear-swap-api/v1/swap_cross_sub_account_list', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_sub_account_info_list = contractPrivatePostLinearSwapApiV1SwapSubAccountInfoList = Entry('linear-swap-api/v1/swap_sub_account_info_list', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_sub_account_info_list = contractPrivatePostLinearSwapApiV1SwapCrossSubAccountInfoList = Entry('linear-swap-api/v1/swap_cross_sub_account_info_list', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_sub_account_info = contractPrivatePostLinearSwapApiV1SwapSubAccountInfo = Entry('linear-swap-api/v1/swap_sub_account_info', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_sub_account_info = contractPrivatePostLinearSwapApiV1SwapCrossSubAccountInfo = Entry('linear-swap-api/v1/swap_cross_sub_account_info', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_sub_position_info = contractPrivatePostLinearSwapApiV1SwapSubPositionInfo = Entry('linear-swap-api/v1/swap_sub_position_info', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_sub_position_info = contractPrivatePostLinearSwapApiV1SwapCrossSubPositionInfo = Entry('linear-swap-api/v1/swap_cross_sub_position_info', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_financial_record = contractPrivatePostLinearSwapApiV1SwapFinancialRecord = Entry('linear-swap-api/v1/swap_financial_record', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_financial_record_exact = contractPrivatePostLinearSwapApiV1SwapFinancialRecordExact = Entry('linear-swap-api/v1/swap_financial_record_exact', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_user_settlement_records = contractPrivatePostLinearSwapApiV1SwapUserSettlementRecords = Entry('linear-swap-api/v1/swap_user_settlement_records', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_user_settlement_records = contractPrivatePostLinearSwapApiV1SwapCrossUserSettlementRecords = Entry('linear-swap-api/v1/swap_cross_user_settlement_records', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_available_level_rate = contractPrivatePostLinearSwapApiV1SwapAvailableLevelRate = Entry('linear-swap-api/v1/swap_available_level_rate', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_available_level_rate = contractPrivatePostLinearSwapApiV1SwapCrossAvailableLevelRate = Entry('linear-swap-api/v1/swap_cross_available_level_rate', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_order_limit = contractPrivatePostLinearSwapApiV1SwapOrderLimit = Entry('linear-swap-api/v1/swap_order_limit', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_fee = contractPrivatePostLinearSwapApiV1SwapFee = Entry('linear-swap-api/v1/swap_fee', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_transfer_limit = contractPrivatePostLinearSwapApiV1SwapTransferLimit = Entry('linear-swap-api/v1/swap_transfer_limit', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_transfer_limit = contractPrivatePostLinearSwapApiV1SwapCrossTransferLimit = Entry('linear-swap-api/v1/swap_cross_transfer_limit', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_position_limit = contractPrivatePostLinearSwapApiV1SwapPositionLimit = Entry('linear-swap-api/v1/swap_position_limit', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_position_limit = contractPrivatePostLinearSwapApiV1SwapCrossPositionLimit = Entry('linear-swap-api/v1/swap_cross_position_limit', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_master_sub_transfer = contractPrivatePostLinearSwapApiV1SwapMasterSubTransfer = Entry('linear-swap-api/v1/swap_master_sub_transfer', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_master_sub_transfer_record = contractPrivatePostLinearSwapApiV1SwapMasterSubTransferRecord = Entry('linear-swap-api/v1/swap_master_sub_transfer_record', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_transfer_inner = contractPrivatePostLinearSwapApiV1SwapTransferInner = Entry('linear-swap-api/v1/swap_transfer_inner', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v3_swap_financial_record = contractPrivatePostLinearSwapApiV3SwapFinancialRecord = Entry('linear-swap-api/v3/swap_financial_record', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v3_swap_financial_record_exact = contractPrivatePostLinearSwapApiV3SwapFinancialRecordExact = Entry('linear-swap-api/v3/swap_financial_record_exact', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_order = contractPrivatePostLinearSwapApiV1SwapOrder = Entry('linear-swap-api/v1/swap_order', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_order = contractPrivatePostLinearSwapApiV1SwapCrossOrder = Entry('linear-swap-api/v1/swap_cross_order', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_batchorder = contractPrivatePostLinearSwapApiV1SwapBatchorder = Entry('linear-swap-api/v1/swap_batchorder', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_batchorder = contractPrivatePostLinearSwapApiV1SwapCrossBatchorder = Entry('linear-swap-api/v1/swap_cross_batchorder', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cancel = contractPrivatePostLinearSwapApiV1SwapCancel = Entry('linear-swap-api/v1/swap_cancel', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_cancel = contractPrivatePostLinearSwapApiV1SwapCrossCancel = Entry('linear-swap-api/v1/swap_cross_cancel', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cancelall = contractPrivatePostLinearSwapApiV1SwapCancelall = Entry('linear-swap-api/v1/swap_cancelall', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_cancelall = contractPrivatePostLinearSwapApiV1SwapCrossCancelall = Entry('linear-swap-api/v1/swap_cross_cancelall', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_switch_lever_rate = contractPrivatePostLinearSwapApiV1SwapSwitchLeverRate = Entry('linear-swap-api/v1/swap_switch_lever_rate', ['contract', 'private'], 'POST', {'cost': 30}) + contract_private_post_linear_swap_api_v1_swap_cross_switch_lever_rate = contractPrivatePostLinearSwapApiV1SwapCrossSwitchLeverRate = Entry('linear-swap-api/v1/swap_cross_switch_lever_rate', ['contract', 'private'], 'POST', {'cost': 30}) + contract_private_post_linear_swap_api_v1_swap_lightning_close_position = contractPrivatePostLinearSwapApiV1SwapLightningClosePosition = Entry('linear-swap-api/v1/swap_lightning_close_position', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_lightning_close_position = contractPrivatePostLinearSwapApiV1SwapCrossLightningClosePosition = Entry('linear-swap-api/v1/swap_cross_lightning_close_position', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_order_info = contractPrivatePostLinearSwapApiV1SwapOrderInfo = Entry('linear-swap-api/v1/swap_order_info', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_order_info = contractPrivatePostLinearSwapApiV1SwapCrossOrderInfo = Entry('linear-swap-api/v1/swap_cross_order_info', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_order_detail = contractPrivatePostLinearSwapApiV1SwapOrderDetail = Entry('linear-swap-api/v1/swap_order_detail', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_order_detail = contractPrivatePostLinearSwapApiV1SwapCrossOrderDetail = Entry('linear-swap-api/v1/swap_cross_order_detail', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_openorders = contractPrivatePostLinearSwapApiV1SwapOpenorders = Entry('linear-swap-api/v1/swap_openorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_openorders = contractPrivatePostLinearSwapApiV1SwapCrossOpenorders = Entry('linear-swap-api/v1/swap_cross_openorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_hisorders = contractPrivatePostLinearSwapApiV1SwapHisorders = Entry('linear-swap-api/v1/swap_hisorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_hisorders = contractPrivatePostLinearSwapApiV1SwapCrossHisorders = Entry('linear-swap-api/v1/swap_cross_hisorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_hisorders_exact = contractPrivatePostLinearSwapApiV1SwapHisordersExact = Entry('linear-swap-api/v1/swap_hisorders_exact', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_hisorders_exact = contractPrivatePostLinearSwapApiV1SwapCrossHisordersExact = Entry('linear-swap-api/v1/swap_cross_hisorders_exact', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_matchresults = contractPrivatePostLinearSwapApiV1SwapMatchresults = Entry('linear-swap-api/v1/swap_matchresults', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_matchresults = contractPrivatePostLinearSwapApiV1SwapCrossMatchresults = Entry('linear-swap-api/v1/swap_cross_matchresults', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_matchresults_exact = contractPrivatePostLinearSwapApiV1SwapMatchresultsExact = Entry('linear-swap-api/v1/swap_matchresults_exact', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_matchresults_exact = contractPrivatePostLinearSwapApiV1SwapCrossMatchresultsExact = Entry('linear-swap-api/v1/swap_cross_matchresults_exact', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_linear_cancel_after = contractPrivatePostLinearSwapApiV1LinearCancelAfter = Entry('linear-swap-api/v1/linear-cancel-after', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_switch_position_mode = contractPrivatePostLinearSwapApiV1SwapSwitchPositionMode = Entry('linear-swap-api/v1/swap_switch_position_mode', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_switch_position_mode = contractPrivatePostLinearSwapApiV1SwapCrossSwitchPositionMode = Entry('linear-swap-api/v1/swap_cross_switch_position_mode', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v3_swap_matchresults = contractPrivatePostLinearSwapApiV3SwapMatchresults = Entry('linear-swap-api/v3/swap_matchresults', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v3_swap_cross_matchresults = contractPrivatePostLinearSwapApiV3SwapCrossMatchresults = Entry('linear-swap-api/v3/swap_cross_matchresults', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v3_swap_matchresults_exact = contractPrivatePostLinearSwapApiV3SwapMatchresultsExact = Entry('linear-swap-api/v3/swap_matchresults_exact', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v3_swap_cross_matchresults_exact = contractPrivatePostLinearSwapApiV3SwapCrossMatchresultsExact = Entry('linear-swap-api/v3/swap_cross_matchresults_exact', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v3_swap_hisorders = contractPrivatePostLinearSwapApiV3SwapHisorders = Entry('linear-swap-api/v3/swap_hisorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v3_swap_cross_hisorders = contractPrivatePostLinearSwapApiV3SwapCrossHisorders = Entry('linear-swap-api/v3/swap_cross_hisorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v3_swap_hisorders_exact = contractPrivatePostLinearSwapApiV3SwapHisordersExact = Entry('linear-swap-api/v3/swap_hisorders_exact', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v3_swap_cross_hisorders_exact = contractPrivatePostLinearSwapApiV3SwapCrossHisordersExact = Entry('linear-swap-api/v3/swap_cross_hisorders_exact', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v3_fix_position_margin_change = contractPrivatePostLinearSwapApiV3FixPositionMarginChange = Entry('linear-swap-api/v3/fix_position_margin_change', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v3_swap_switch_account_type = contractPrivatePostLinearSwapApiV3SwapSwitchAccountType = Entry('linear-swap-api/v3/swap_switch_account_type', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v3_linear_swap_fee_switch = contractPrivatePostLinearSwapApiV3LinearSwapFeeSwitch = Entry('linear-swap-api/v3/linear_swap_fee_switch', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_trigger_order = contractPrivatePostLinearSwapApiV1SwapTriggerOrder = Entry('linear-swap-api/v1/swap_trigger_order', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_trigger_order = contractPrivatePostLinearSwapApiV1SwapCrossTriggerOrder = Entry('linear-swap-api/v1/swap_cross_trigger_order', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_trigger_cancel = contractPrivatePostLinearSwapApiV1SwapTriggerCancel = Entry('linear-swap-api/v1/swap_trigger_cancel', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_trigger_cancel = contractPrivatePostLinearSwapApiV1SwapCrossTriggerCancel = Entry('linear-swap-api/v1/swap_cross_trigger_cancel', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_trigger_cancelall = contractPrivatePostLinearSwapApiV1SwapTriggerCancelall = Entry('linear-swap-api/v1/swap_trigger_cancelall', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_trigger_cancelall = contractPrivatePostLinearSwapApiV1SwapCrossTriggerCancelall = Entry('linear-swap-api/v1/swap_cross_trigger_cancelall', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_trigger_openorders = contractPrivatePostLinearSwapApiV1SwapTriggerOpenorders = Entry('linear-swap-api/v1/swap_trigger_openorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_trigger_openorders = contractPrivatePostLinearSwapApiV1SwapCrossTriggerOpenorders = Entry('linear-swap-api/v1/swap_cross_trigger_openorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_trigger_hisorders = contractPrivatePostLinearSwapApiV1SwapTriggerHisorders = Entry('linear-swap-api/v1/swap_trigger_hisorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_trigger_hisorders = contractPrivatePostLinearSwapApiV1SwapCrossTriggerHisorders = Entry('linear-swap-api/v1/swap_cross_trigger_hisorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_tpsl_order = contractPrivatePostLinearSwapApiV1SwapTpslOrder = Entry('linear-swap-api/v1/swap_tpsl_order', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_tpsl_order = contractPrivatePostLinearSwapApiV1SwapCrossTpslOrder = Entry('linear-swap-api/v1/swap_cross_tpsl_order', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_tpsl_cancel = contractPrivatePostLinearSwapApiV1SwapTpslCancel = Entry('linear-swap-api/v1/swap_tpsl_cancel', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_tpsl_cancel = contractPrivatePostLinearSwapApiV1SwapCrossTpslCancel = Entry('linear-swap-api/v1/swap_cross_tpsl_cancel', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_tpsl_cancelall = contractPrivatePostLinearSwapApiV1SwapTpslCancelall = Entry('linear-swap-api/v1/swap_tpsl_cancelall', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_tpsl_cancelall = contractPrivatePostLinearSwapApiV1SwapCrossTpslCancelall = Entry('linear-swap-api/v1/swap_cross_tpsl_cancelall', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_tpsl_openorders = contractPrivatePostLinearSwapApiV1SwapTpslOpenorders = Entry('linear-swap-api/v1/swap_tpsl_openorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_tpsl_openorders = contractPrivatePostLinearSwapApiV1SwapCrossTpslOpenorders = Entry('linear-swap-api/v1/swap_cross_tpsl_openorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_tpsl_hisorders = contractPrivatePostLinearSwapApiV1SwapTpslHisorders = Entry('linear-swap-api/v1/swap_tpsl_hisorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_tpsl_hisorders = contractPrivatePostLinearSwapApiV1SwapCrossTpslHisorders = Entry('linear-swap-api/v1/swap_cross_tpsl_hisorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_relation_tpsl_order = contractPrivatePostLinearSwapApiV1SwapRelationTpslOrder = Entry('linear-swap-api/v1/swap_relation_tpsl_order', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_relation_tpsl_order = contractPrivatePostLinearSwapApiV1SwapCrossRelationTpslOrder = Entry('linear-swap-api/v1/swap_cross_relation_tpsl_order', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_track_order = contractPrivatePostLinearSwapApiV1SwapTrackOrder = Entry('linear-swap-api/v1/swap_track_order', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_track_order = contractPrivatePostLinearSwapApiV1SwapCrossTrackOrder = Entry('linear-swap-api/v1/swap_cross_track_order', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_track_cancel = contractPrivatePostLinearSwapApiV1SwapTrackCancel = Entry('linear-swap-api/v1/swap_track_cancel', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_track_cancel = contractPrivatePostLinearSwapApiV1SwapCrossTrackCancel = Entry('linear-swap-api/v1/swap_cross_track_cancel', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_track_cancelall = contractPrivatePostLinearSwapApiV1SwapTrackCancelall = Entry('linear-swap-api/v1/swap_track_cancelall', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_track_cancelall = contractPrivatePostLinearSwapApiV1SwapCrossTrackCancelall = Entry('linear-swap-api/v1/swap_cross_track_cancelall', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_track_openorders = contractPrivatePostLinearSwapApiV1SwapTrackOpenorders = Entry('linear-swap-api/v1/swap_track_openorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_track_openorders = contractPrivatePostLinearSwapApiV1SwapCrossTrackOpenorders = Entry('linear-swap-api/v1/swap_cross_track_openorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_track_hisorders = contractPrivatePostLinearSwapApiV1SwapTrackHisorders = Entry('linear-swap-api/v1/swap_track_hisorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_track_hisorders = contractPrivatePostLinearSwapApiV1SwapCrossTrackHisorders = Entry('linear-swap-api/v1/swap_cross_track_hisorders', ['contract', 'private'], 'POST', {'cost': 1}) diff --git a/ccxt/abstract/huobi.py b/ccxt/abstract/huobi.py new file mode 100644 index 0000000..884b800 --- /dev/null +++ b/ccxt/abstract/huobi.py @@ -0,0 +1,548 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + v2public_get_reference_currencies = v2PublicGetReferenceCurrencies = Entry('reference/currencies', 'v2Public', 'GET', {'cost': 1}) + v2public_get_market_status = v2PublicGetMarketStatus = Entry('market-status', 'v2Public', 'GET', {'cost': 1}) + v2private_get_account_ledger = v2PrivateGetAccountLedger = Entry('account/ledger', 'v2Private', 'GET', {'cost': 1}) + v2private_get_account_withdraw_quota = v2PrivateGetAccountWithdrawQuota = Entry('account/withdraw/quota', 'v2Private', 'GET', {'cost': 1}) + v2private_get_account_withdraw_address = v2PrivateGetAccountWithdrawAddress = Entry('account/withdraw/address', 'v2Private', 'GET', {'cost': 1}) + v2private_get_account_deposit_address = v2PrivateGetAccountDepositAddress = Entry('account/deposit/address', 'v2Private', 'GET', {'cost': 1}) + v2private_get_account_repayment = v2PrivateGetAccountRepayment = Entry('account/repayment', 'v2Private', 'GET', {'cost': 5}) + v2private_get_reference_transact_fee_rate = v2PrivateGetReferenceTransactFeeRate = Entry('reference/transact-fee-rate', 'v2Private', 'GET', {'cost': 1}) + v2private_get_account_asset_valuation = v2PrivateGetAccountAssetValuation = Entry('account/asset-valuation', 'v2Private', 'GET', {'cost': 0.2}) + v2private_get_point_account = v2PrivateGetPointAccount = Entry('point/account', 'v2Private', 'GET', {'cost': 5}) + v2private_get_sub_user_user_list = v2PrivateGetSubUserUserList = Entry('sub-user/user-list', 'v2Private', 'GET', {'cost': 1}) + v2private_get_sub_user_user_state = v2PrivateGetSubUserUserState = Entry('sub-user/user-state', 'v2Private', 'GET', {'cost': 1}) + v2private_get_sub_user_account_list = v2PrivateGetSubUserAccountList = Entry('sub-user/account-list', 'v2Private', 'GET', {'cost': 1}) + v2private_get_sub_user_deposit_address = v2PrivateGetSubUserDepositAddress = Entry('sub-user/deposit-address', 'v2Private', 'GET', {'cost': 1}) + v2private_get_sub_user_query_deposit = v2PrivateGetSubUserQueryDeposit = Entry('sub-user/query-deposit', 'v2Private', 'GET', {'cost': 1}) + v2private_get_user_api_key = v2PrivateGetUserApiKey = Entry('user/api-key', 'v2Private', 'GET', {'cost': 1}) + v2private_get_user_uid = v2PrivateGetUserUid = Entry('user/uid', 'v2Private', 'GET', {'cost': 1}) + v2private_get_algo_orders_opening = v2PrivateGetAlgoOrdersOpening = Entry('algo-orders/opening', 'v2Private', 'GET', {'cost': 1}) + v2private_get_algo_orders_history = v2PrivateGetAlgoOrdersHistory = Entry('algo-orders/history', 'v2Private', 'GET', {'cost': 1}) + v2private_get_algo_orders_specific = v2PrivateGetAlgoOrdersSpecific = Entry('algo-orders/specific', 'v2Private', 'GET', {'cost': 1}) + v2private_get_c2c_offers = v2PrivateGetC2cOffers = Entry('c2c/offers', 'v2Private', 'GET', {'cost': 1}) + v2private_get_c2c_offer = v2PrivateGetC2cOffer = Entry('c2c/offer', 'v2Private', 'GET', {'cost': 1}) + v2private_get_c2c_transactions = v2PrivateGetC2cTransactions = Entry('c2c/transactions', 'v2Private', 'GET', {'cost': 1}) + v2private_get_c2c_repayment = v2PrivateGetC2cRepayment = Entry('c2c/repayment', 'v2Private', 'GET', {'cost': 1}) + v2private_get_c2c_account = v2PrivateGetC2cAccount = Entry('c2c/account', 'v2Private', 'GET', {'cost': 1}) + v2private_get_etp_reference = v2PrivateGetEtpReference = Entry('etp/reference', 'v2Private', 'GET', {'cost': 1}) + v2private_get_etp_transactions = v2PrivateGetEtpTransactions = Entry('etp/transactions', 'v2Private', 'GET', {'cost': 5}) + v2private_get_etp_transaction = v2PrivateGetEtpTransaction = Entry('etp/transaction', 'v2Private', 'GET', {'cost': 5}) + v2private_get_etp_rebalance = v2PrivateGetEtpRebalance = Entry('etp/rebalance', 'v2Private', 'GET', {'cost': 1}) + v2private_get_etp_limit = v2PrivateGetEtpLimit = Entry('etp/limit', 'v2Private', 'GET', {'cost': 1}) + v2private_post_account_transfer = v2PrivatePostAccountTransfer = Entry('account/transfer', 'v2Private', 'POST', {'cost': 1}) + v2private_post_account_repayment = v2PrivatePostAccountRepayment = Entry('account/repayment', 'v2Private', 'POST', {'cost': 5}) + v2private_post_point_transfer = v2PrivatePostPointTransfer = Entry('point/transfer', 'v2Private', 'POST', {'cost': 5}) + v2private_post_sub_user_management = v2PrivatePostSubUserManagement = Entry('sub-user/management', 'v2Private', 'POST', {'cost': 1}) + v2private_post_sub_user_creation = v2PrivatePostSubUserCreation = Entry('sub-user/creation', 'v2Private', 'POST', {'cost': 1}) + v2private_post_sub_user_tradable_market = v2PrivatePostSubUserTradableMarket = Entry('sub-user/tradable-market', 'v2Private', 'POST', {'cost': 1}) + v2private_post_sub_user_transferability = v2PrivatePostSubUserTransferability = Entry('sub-user/transferability', 'v2Private', 'POST', {'cost': 1}) + v2private_post_sub_user_api_key_generation = v2PrivatePostSubUserApiKeyGeneration = Entry('sub-user/api-key-generation', 'v2Private', 'POST', {'cost': 1}) + v2private_post_sub_user_api_key_modification = v2PrivatePostSubUserApiKeyModification = Entry('sub-user/api-key-modification', 'v2Private', 'POST', {'cost': 1}) + v2private_post_sub_user_api_key_deletion = v2PrivatePostSubUserApiKeyDeletion = Entry('sub-user/api-key-deletion', 'v2Private', 'POST', {'cost': 1}) + v2private_post_sub_user_deduct_mode = v2PrivatePostSubUserDeductMode = Entry('sub-user/deduct-mode', 'v2Private', 'POST', {'cost': 1}) + v2private_post_algo_orders = v2PrivatePostAlgoOrders = Entry('algo-orders', 'v2Private', 'POST', {'cost': 1}) + v2private_post_algo_orders_cancel_all_after = v2PrivatePostAlgoOrdersCancelAllAfter = Entry('algo-orders/cancel-all-after', 'v2Private', 'POST', {'cost': 1}) + v2private_post_algo_orders_cancellation = v2PrivatePostAlgoOrdersCancellation = Entry('algo-orders/cancellation', 'v2Private', 'POST', {'cost': 1}) + v2private_post_c2c_offer = v2PrivatePostC2cOffer = Entry('c2c/offer', 'v2Private', 'POST', {'cost': 1}) + v2private_post_c2c_cancellation = v2PrivatePostC2cCancellation = Entry('c2c/cancellation', 'v2Private', 'POST', {'cost': 1}) + v2private_post_c2c_cancel_all = v2PrivatePostC2cCancelAll = Entry('c2c/cancel-all', 'v2Private', 'POST', {'cost': 1}) + v2private_post_c2c_repayment = v2PrivatePostC2cRepayment = Entry('c2c/repayment', 'v2Private', 'POST', {'cost': 1}) + v2private_post_c2c_transfer = v2PrivatePostC2cTransfer = Entry('c2c/transfer', 'v2Private', 'POST', {'cost': 1}) + v2private_post_etp_creation = v2PrivatePostEtpCreation = Entry('etp/creation', 'v2Private', 'POST', {'cost': 5}) + v2private_post_etp_redemption = v2PrivatePostEtpRedemption = Entry('etp/redemption', 'v2Private', 'POST', {'cost': 5}) + v2private_post_etp_transactid_cancel = v2PrivatePostEtpTransactIdCancel = Entry('etp/{transactId}/cancel', 'v2Private', 'POST', {'cost': 10}) + v2private_post_etp_batch_cancel = v2PrivatePostEtpBatchCancel = Entry('etp/batch-cancel', 'v2Private', 'POST', {'cost': 50}) + public_get_common_symbols = publicGetCommonSymbols = Entry('common/symbols', 'public', 'GET', {'cost': 1}) + public_get_common_currencys = publicGetCommonCurrencys = Entry('common/currencys', 'public', 'GET', {'cost': 1}) + public_get_common_timestamp = publicGetCommonTimestamp = Entry('common/timestamp', 'public', 'GET', {'cost': 1}) + public_get_common_exchange = publicGetCommonExchange = Entry('common/exchange', 'public', 'GET', {'cost': 1}) + public_get_settings_currencys = publicGetSettingsCurrencys = Entry('settings/currencys', 'public', 'GET', {'cost': 1}) + private_get_account_accounts = privateGetAccountAccounts = Entry('account/accounts', 'private', 'GET', {'cost': 0.2}) + private_get_account_accounts_id_balance = privateGetAccountAccountsIdBalance = Entry('account/accounts/{id}/balance', 'private', 'GET', {'cost': 0.2}) + private_get_account_accounts_sub_uid = privateGetAccountAccountsSubUid = Entry('account/accounts/{sub-uid}', 'private', 'GET', {'cost': 1}) + private_get_account_history = privateGetAccountHistory = Entry('account/history', 'private', 'GET', {'cost': 4}) + private_get_cross_margin_loan_info = privateGetCrossMarginLoanInfo = Entry('cross-margin/loan-info', 'private', 'GET', {'cost': 1}) + private_get_margin_loan_info = privateGetMarginLoanInfo = Entry('margin/loan-info', 'private', 'GET', {'cost': 1}) + private_get_fee_fee_rate_get = privateGetFeeFeeRateGet = Entry('fee/fee-rate/get', 'private', 'GET', {'cost': 1}) + private_get_order_openorders = privateGetOrderOpenOrders = Entry('order/openOrders', 'private', 'GET', {'cost': 0.4}) + private_get_order_orders = privateGetOrderOrders = Entry('order/orders', 'private', 'GET', {'cost': 0.4}) + private_get_order_orders_id = privateGetOrderOrdersId = Entry('order/orders/{id}', 'private', 'GET', {'cost': 0.4}) + private_get_order_orders_id_matchresults = privateGetOrderOrdersIdMatchresults = Entry('order/orders/{id}/matchresults', 'private', 'GET', {'cost': 0.4}) + private_get_order_orders_getclientorder = privateGetOrderOrdersGetClientOrder = Entry('order/orders/getClientOrder', 'private', 'GET', {'cost': 0.4}) + private_get_order_history = privateGetOrderHistory = Entry('order/history', 'private', 'GET', {'cost': 1}) + private_get_order_matchresults = privateGetOrderMatchresults = Entry('order/matchresults', 'private', 'GET', {'cost': 1}) + private_get_query_deposit_withdraw = privateGetQueryDepositWithdraw = Entry('query/deposit-withdraw', 'private', 'GET', {'cost': 1}) + private_get_margin_loan_orders = privateGetMarginLoanOrders = Entry('margin/loan-orders', 'private', 'GET', {'cost': 0.2}) + private_get_margin_accounts_balance = privateGetMarginAccountsBalance = Entry('margin/accounts/balance', 'private', 'GET', {'cost': 0.2}) + private_get_cross_margin_loan_orders = privateGetCrossMarginLoanOrders = Entry('cross-margin/loan-orders', 'private', 'GET', {'cost': 1}) + private_get_cross_margin_accounts_balance = privateGetCrossMarginAccountsBalance = Entry('cross-margin/accounts/balance', 'private', 'GET', {'cost': 1}) + private_get_points_actions = privateGetPointsActions = Entry('points/actions', 'private', 'GET', {'cost': 1}) + private_get_points_orders = privateGetPointsOrders = Entry('points/orders', 'private', 'GET', {'cost': 1}) + private_get_subuser_aggregate_balance = privateGetSubuserAggregateBalance = Entry('subuser/aggregate-balance', 'private', 'GET', {'cost': 10}) + private_get_stable_coin_exchange_rate = privateGetStableCoinExchangeRate = Entry('stable-coin/exchange_rate', 'private', 'GET', {'cost': 1}) + private_get_stable_coin_quote = privateGetStableCoinQuote = Entry('stable-coin/quote', 'private', 'GET', {'cost': 1}) + private_post_account_transfer = privatePostAccountTransfer = Entry('account/transfer', 'private', 'POST', {'cost': 1}) + private_post_futures_transfer = privatePostFuturesTransfer = Entry('futures/transfer', 'private', 'POST', {'cost': 1}) + private_post_order_batch_orders = privatePostOrderBatchOrders = Entry('order/batch-orders', 'private', 'POST', {'cost': 0.4}) + private_post_order_orders_place = privatePostOrderOrdersPlace = Entry('order/orders/place', 'private', 'POST', {'cost': 0.2}) + private_post_order_orders_submitcancelclientorder = privatePostOrderOrdersSubmitCancelClientOrder = Entry('order/orders/submitCancelClientOrder', 'private', 'POST', {'cost': 0.2}) + private_post_order_orders_batchcancelopenorders = privatePostOrderOrdersBatchCancelOpenOrders = Entry('order/orders/batchCancelOpenOrders', 'private', 'POST', {'cost': 0.4}) + private_post_order_orders_id_submitcancel = privatePostOrderOrdersIdSubmitcancel = Entry('order/orders/{id}/submitcancel', 'private', 'POST', {'cost': 0.2}) + private_post_order_orders_batchcancel = privatePostOrderOrdersBatchcancel = Entry('order/orders/batchcancel', 'private', 'POST', {'cost': 0.4}) + private_post_dw_withdraw_api_create = privatePostDwWithdrawApiCreate = Entry('dw/withdraw/api/create', 'private', 'POST', {'cost': 1}) + private_post_dw_withdraw_virtual_id_cancel = privatePostDwWithdrawVirtualIdCancel = Entry('dw/withdraw-virtual/{id}/cancel', 'private', 'POST', {'cost': 1}) + private_post_dw_transfer_in_margin = privatePostDwTransferInMargin = Entry('dw/transfer-in/margin', 'private', 'POST', {'cost': 10}) + private_post_dw_transfer_out_margin = privatePostDwTransferOutMargin = Entry('dw/transfer-out/margin', 'private', 'POST', {'cost': 10}) + private_post_margin_orders = privatePostMarginOrders = Entry('margin/orders', 'private', 'POST', {'cost': 10}) + private_post_margin_orders_id_repay = privatePostMarginOrdersIdRepay = Entry('margin/orders/{id}/repay', 'private', 'POST', {'cost': 10}) + private_post_cross_margin_transfer_in = privatePostCrossMarginTransferIn = Entry('cross-margin/transfer-in', 'private', 'POST', {'cost': 1}) + private_post_cross_margin_transfer_out = privatePostCrossMarginTransferOut = Entry('cross-margin/transfer-out', 'private', 'POST', {'cost': 1}) + private_post_cross_margin_orders = privatePostCrossMarginOrders = Entry('cross-margin/orders', 'private', 'POST', {'cost': 1}) + private_post_cross_margin_orders_id_repay = privatePostCrossMarginOrdersIdRepay = Entry('cross-margin/orders/{id}/repay', 'private', 'POST', {'cost': 1}) + private_post_stable_coin_exchange = privatePostStableCoinExchange = Entry('stable-coin/exchange', 'private', 'POST', {'cost': 1}) + private_post_subuser_transfer = privatePostSubuserTransfer = Entry('subuser/transfer', 'private', 'POST', {'cost': 10}) + status_public_spot_get_api_v2_summary_json = statusPublicSpotGetApiV2SummaryJson = Entry('api/v2/summary.json', ['status', 'public', 'spot'], 'GET', {'cost': 1}) + status_public_future_inverse_get_api_v2_summary_json = statusPublicFutureInverseGetApiV2SummaryJson = Entry('api/v2/summary.json', ['status', 'public', 'future', 'inverse'], 'GET', {'cost': 1}) + status_public_future_linear_get_api_v2_summary_json = statusPublicFutureLinearGetApiV2SummaryJson = Entry('api/v2/summary.json', ['status', 'public', 'future', 'linear'], 'GET', {'cost': 1}) + status_public_swap_inverse_get_api_v2_summary_json = statusPublicSwapInverseGetApiV2SummaryJson = Entry('api/v2/summary.json', ['status', 'public', 'swap', 'inverse'], 'GET', {'cost': 1}) + status_public_swap_linear_get_api_v2_summary_json = statusPublicSwapLinearGetApiV2SummaryJson = Entry('api/v2/summary.json', ['status', 'public', 'swap', 'linear'], 'GET', {'cost': 1}) + spot_public_get_v2_market_status = spotPublicGetV2MarketStatus = Entry('v2/market-status', ['spot', 'public'], 'GET', {'cost': 1}) + spot_public_get_v1_common_symbols = spotPublicGetV1CommonSymbols = Entry('v1/common/symbols', ['spot', 'public'], 'GET', {'cost': 1}) + spot_public_get_v1_common_currencys = spotPublicGetV1CommonCurrencys = Entry('v1/common/currencys', ['spot', 'public'], 'GET', {'cost': 1}) + spot_public_get_v2_settings_common_currencies = spotPublicGetV2SettingsCommonCurrencies = Entry('v2/settings/common/currencies', ['spot', 'public'], 'GET', {'cost': 1}) + spot_public_get_v2_reference_currencies = spotPublicGetV2ReferenceCurrencies = Entry('v2/reference/currencies', ['spot', 'public'], 'GET', {'cost': 1}) + spot_public_get_v1_common_timestamp = spotPublicGetV1CommonTimestamp = Entry('v1/common/timestamp', ['spot', 'public'], 'GET', {'cost': 1}) + spot_public_get_v1_common_exchange = spotPublicGetV1CommonExchange = Entry('v1/common/exchange', ['spot', 'public'], 'GET', {'cost': 1}) + spot_public_get_v1_settings_common_chains = spotPublicGetV1SettingsCommonChains = Entry('v1/settings/common/chains', ['spot', 'public'], 'GET', {'cost': 1}) + spot_public_get_v1_settings_common_currencys = spotPublicGetV1SettingsCommonCurrencys = Entry('v1/settings/common/currencys', ['spot', 'public'], 'GET', {'cost': 1}) + spot_public_get_v1_settings_common_symbols = spotPublicGetV1SettingsCommonSymbols = Entry('v1/settings/common/symbols', ['spot', 'public'], 'GET', {'cost': 1}) + spot_public_get_v2_settings_common_symbols = spotPublicGetV2SettingsCommonSymbols = Entry('v2/settings/common/symbols', ['spot', 'public'], 'GET', {'cost': 1}) + spot_public_get_v1_settings_common_market_symbols = spotPublicGetV1SettingsCommonMarketSymbols = Entry('v1/settings/common/market-symbols', ['spot', 'public'], 'GET', {'cost': 1}) + spot_public_get_market_history_candles = spotPublicGetMarketHistoryCandles = Entry('market/history/candles', ['spot', 'public'], 'GET', {'cost': 1}) + spot_public_get_market_history_kline = spotPublicGetMarketHistoryKline = Entry('market/history/kline', ['spot', 'public'], 'GET', {'cost': 1}) + spot_public_get_market_detail_merged = spotPublicGetMarketDetailMerged = Entry('market/detail/merged', ['spot', 'public'], 'GET', {'cost': 1}) + spot_public_get_market_tickers = spotPublicGetMarketTickers = Entry('market/tickers', ['spot', 'public'], 'GET', {'cost': 1}) + spot_public_get_market_detail = spotPublicGetMarketDetail = Entry('market/detail', ['spot', 'public'], 'GET', {'cost': 1}) + spot_public_get_market_depth = spotPublicGetMarketDepth = Entry('market/depth', ['spot', 'public'], 'GET', {'cost': 1}) + spot_public_get_market_trade = spotPublicGetMarketTrade = Entry('market/trade', ['spot', 'public'], 'GET', {'cost': 1}) + spot_public_get_market_history_trade = spotPublicGetMarketHistoryTrade = Entry('market/history/trade', ['spot', 'public'], 'GET', {'cost': 1}) + spot_public_get_market_etp = spotPublicGetMarketEtp = Entry('market/etp', ['spot', 'public'], 'GET', {'cost': 1}) + spot_public_get_v2_etp_reference = spotPublicGetV2EtpReference = Entry('v2/etp/reference', ['spot', 'public'], 'GET', {'cost': 1}) + spot_public_get_v2_etp_rebalance = spotPublicGetV2EtpRebalance = Entry('v2/etp/rebalance', ['spot', 'public'], 'GET', {'cost': 1}) + spot_private_get_v1_account_accounts = spotPrivateGetV1AccountAccounts = Entry('v1/account/accounts', ['spot', 'private'], 'GET', {'cost': 0.2}) + spot_private_get_v1_account_accounts_account_id_balance = spotPrivateGetV1AccountAccountsAccountIdBalance = Entry('v1/account/accounts/{account-id}/balance', ['spot', 'private'], 'GET', {'cost': 0.2}) + spot_private_get_v2_account_valuation = spotPrivateGetV2AccountValuation = Entry('v2/account/valuation', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_v2_account_asset_valuation = spotPrivateGetV2AccountAssetValuation = Entry('v2/account/asset-valuation', ['spot', 'private'], 'GET', {'cost': 0.2}) + spot_private_get_v1_account_history = spotPrivateGetV1AccountHistory = Entry('v1/account/history', ['spot', 'private'], 'GET', {'cost': 4}) + spot_private_get_v2_account_ledger = spotPrivateGetV2AccountLedger = Entry('v2/account/ledger', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_v2_point_account = spotPrivateGetV2PointAccount = Entry('v2/point/account', ['spot', 'private'], 'GET', {'cost': 5}) + spot_private_get_v2_account_deposit_address = spotPrivateGetV2AccountDepositAddress = Entry('v2/account/deposit/address', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_v2_account_withdraw_quota = spotPrivateGetV2AccountWithdrawQuota = Entry('v2/account/withdraw/quota', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_v2_account_withdraw_address = spotPrivateGetV2AccountWithdrawAddress = Entry('v2/account/withdraw/address', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_v2_reference_currencies = spotPrivateGetV2ReferenceCurrencies = Entry('v2/reference/currencies', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_v1_query_deposit_withdraw = spotPrivateGetV1QueryDepositWithdraw = Entry('v1/query/deposit-withdraw', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_v1_query_withdraw_client_order_id = spotPrivateGetV1QueryWithdrawClientOrderId = Entry('v1/query/withdraw/client-order-id', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_v2_user_api_key = spotPrivateGetV2UserApiKey = Entry('v2/user/api-key', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_v2_user_uid = spotPrivateGetV2UserUid = Entry('v2/user/uid', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_v2_sub_user_user_list = spotPrivateGetV2SubUserUserList = Entry('v2/sub-user/user-list', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_v2_sub_user_user_state = spotPrivateGetV2SubUserUserState = Entry('v2/sub-user/user-state', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_v2_sub_user_account_list = spotPrivateGetV2SubUserAccountList = Entry('v2/sub-user/account-list', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_v2_sub_user_deposit_address = spotPrivateGetV2SubUserDepositAddress = Entry('v2/sub-user/deposit-address', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_v2_sub_user_query_deposit = spotPrivateGetV2SubUserQueryDeposit = Entry('v2/sub-user/query-deposit', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_v1_subuser_aggregate_balance = spotPrivateGetV1SubuserAggregateBalance = Entry('v1/subuser/aggregate-balance', ['spot', 'private'], 'GET', {'cost': 10}) + spot_private_get_v1_account_accounts_sub_uid = spotPrivateGetV1AccountAccountsSubUid = Entry('v1/account/accounts/{sub-uid}', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_v1_order_openorders = spotPrivateGetV1OrderOpenOrders = Entry('v1/order/openOrders', ['spot', 'private'], 'GET', {'cost': 0.4}) + spot_private_get_v1_order_orders_order_id = spotPrivateGetV1OrderOrdersOrderId = Entry('v1/order/orders/{order-id}', ['spot', 'private'], 'GET', {'cost': 0.4}) + spot_private_get_v1_order_orders_getclientorder = spotPrivateGetV1OrderOrdersGetClientOrder = Entry('v1/order/orders/getClientOrder', ['spot', 'private'], 'GET', {'cost': 0.4}) + spot_private_get_v1_order_orders_order_id_matchresult = spotPrivateGetV1OrderOrdersOrderIdMatchresult = Entry('v1/order/orders/{order-id}/matchresult', ['spot', 'private'], 'GET', {'cost': 0.4}) + spot_private_get_v1_order_orders_order_id_matchresults = spotPrivateGetV1OrderOrdersOrderIdMatchresults = Entry('v1/order/orders/{order-id}/matchresults', ['spot', 'private'], 'GET', {'cost': 0.4}) + spot_private_get_v1_order_orders = spotPrivateGetV1OrderOrders = Entry('v1/order/orders', ['spot', 'private'], 'GET', {'cost': 0.4}) + spot_private_get_v1_order_history = spotPrivateGetV1OrderHistory = Entry('v1/order/history', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_v1_order_matchresults = spotPrivateGetV1OrderMatchresults = Entry('v1/order/matchresults', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_v2_reference_transact_fee_rate = spotPrivateGetV2ReferenceTransactFeeRate = Entry('v2/reference/transact-fee-rate', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_v2_algo_orders_opening = spotPrivateGetV2AlgoOrdersOpening = Entry('v2/algo-orders/opening', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_v2_algo_orders_history = spotPrivateGetV2AlgoOrdersHistory = Entry('v2/algo-orders/history', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_v2_algo_orders_specific = spotPrivateGetV2AlgoOrdersSpecific = Entry('v2/algo-orders/specific', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_v1_margin_loan_info = spotPrivateGetV1MarginLoanInfo = Entry('v1/margin/loan-info', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_v1_margin_loan_orders = spotPrivateGetV1MarginLoanOrders = Entry('v1/margin/loan-orders', ['spot', 'private'], 'GET', {'cost': 0.2}) + spot_private_get_v1_margin_accounts_balance = spotPrivateGetV1MarginAccountsBalance = Entry('v1/margin/accounts/balance', ['spot', 'private'], 'GET', {'cost': 0.2}) + spot_private_get_v1_cross_margin_loan_info = spotPrivateGetV1CrossMarginLoanInfo = Entry('v1/cross-margin/loan-info', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_v1_cross_margin_loan_orders = spotPrivateGetV1CrossMarginLoanOrders = Entry('v1/cross-margin/loan-orders', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_v1_cross_margin_accounts_balance = spotPrivateGetV1CrossMarginAccountsBalance = Entry('v1/cross-margin/accounts/balance', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_v2_account_repayment = spotPrivateGetV2AccountRepayment = Entry('v2/account/repayment', ['spot', 'private'], 'GET', {'cost': 5}) + spot_private_get_v1_stable_coin_quote = spotPrivateGetV1StableCoinQuote = Entry('v1/stable-coin/quote', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_v1_stable_coin_exchange_rate = spotPrivateGetV1StableCoinExchangeRate = Entry('v1/stable_coin/exchange_rate', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_v2_etp_transactions = spotPrivateGetV2EtpTransactions = Entry('v2/etp/transactions', ['spot', 'private'], 'GET', {'cost': 5}) + spot_private_get_v2_etp_transaction = spotPrivateGetV2EtpTransaction = Entry('v2/etp/transaction', ['spot', 'private'], 'GET', {'cost': 5}) + spot_private_get_v2_etp_limit = spotPrivateGetV2EtpLimit = Entry('v2/etp/limit', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_post_v1_account_transfer = spotPrivatePostV1AccountTransfer = Entry('v1/account/transfer', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_post_v1_futures_transfer = spotPrivatePostV1FuturesTransfer = Entry('v1/futures/transfer', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_post_v2_point_transfer = spotPrivatePostV2PointTransfer = Entry('v2/point/transfer', ['spot', 'private'], 'POST', {'cost': 5}) + spot_private_post_v2_account_transfer = spotPrivatePostV2AccountTransfer = Entry('v2/account/transfer', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_post_v1_dw_withdraw_api_create = spotPrivatePostV1DwWithdrawApiCreate = Entry('v1/dw/withdraw/api/create', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_post_v1_dw_withdraw_virtual_withdraw_id_cancel = spotPrivatePostV1DwWithdrawVirtualWithdrawIdCancel = Entry('v1/dw/withdraw-virtual/{withdraw-id}/cancel', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_post_v2_sub_user_deduct_mode = spotPrivatePostV2SubUserDeductMode = Entry('v2/sub-user/deduct-mode', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_post_v2_sub_user_creation = spotPrivatePostV2SubUserCreation = Entry('v2/sub-user/creation', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_post_v2_sub_user_management = spotPrivatePostV2SubUserManagement = Entry('v2/sub-user/management', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_post_v2_sub_user_tradable_market = spotPrivatePostV2SubUserTradableMarket = Entry('v2/sub-user/tradable-market', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_post_v2_sub_user_transferability = spotPrivatePostV2SubUserTransferability = Entry('v2/sub-user/transferability', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_post_v2_sub_user_api_key_generation = spotPrivatePostV2SubUserApiKeyGeneration = Entry('v2/sub-user/api-key-generation', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_post_v2_sub_user_api_key_modification = spotPrivatePostV2SubUserApiKeyModification = Entry('v2/sub-user/api-key-modification', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_post_v2_sub_user_api_key_deletion = spotPrivatePostV2SubUserApiKeyDeletion = Entry('v2/sub-user/api-key-deletion', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_post_v1_subuser_transfer = spotPrivatePostV1SubuserTransfer = Entry('v1/subuser/transfer', ['spot', 'private'], 'POST', {'cost': 10}) + spot_private_post_v1_trust_user_active_credit = spotPrivatePostV1TrustUserActiveCredit = Entry('v1/trust/user/active/credit', ['spot', 'private'], 'POST', {'cost': 10}) + spot_private_post_v1_order_orders_place = spotPrivatePostV1OrderOrdersPlace = Entry('v1/order/orders/place', ['spot', 'private'], 'POST', {'cost': 0.2}) + spot_private_post_v1_order_batch_orders = spotPrivatePostV1OrderBatchOrders = Entry('v1/order/batch-orders', ['spot', 'private'], 'POST', {'cost': 0.4}) + spot_private_post_v1_order_auto_place = spotPrivatePostV1OrderAutoPlace = Entry('v1/order/auto/place', ['spot', 'private'], 'POST', {'cost': 0.2}) + spot_private_post_v1_order_orders_order_id_submitcancel = spotPrivatePostV1OrderOrdersOrderIdSubmitcancel = Entry('v1/order/orders/{order-id}/submitcancel', ['spot', 'private'], 'POST', {'cost': 0.2}) + spot_private_post_v1_order_orders_submitcancelclientorder = spotPrivatePostV1OrderOrdersSubmitCancelClientOrder = Entry('v1/order/orders/submitCancelClientOrder', ['spot', 'private'], 'POST', {'cost': 0.2}) + spot_private_post_v1_order_orders_batchcancelopenorders = spotPrivatePostV1OrderOrdersBatchCancelOpenOrders = Entry('v1/order/orders/batchCancelOpenOrders', ['spot', 'private'], 'POST', {'cost': 0.4}) + spot_private_post_v1_order_orders_batchcancel = spotPrivatePostV1OrderOrdersBatchcancel = Entry('v1/order/orders/batchcancel', ['spot', 'private'], 'POST', {'cost': 0.4}) + spot_private_post_v2_algo_orders_cancel_all_after = spotPrivatePostV2AlgoOrdersCancelAllAfter = Entry('v2/algo-orders/cancel-all-after', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_post_v2_algo_orders = spotPrivatePostV2AlgoOrders = Entry('v2/algo-orders', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_post_v2_algo_orders_cancellation = spotPrivatePostV2AlgoOrdersCancellation = Entry('v2/algo-orders/cancellation', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_post_v2_account_repayment = spotPrivatePostV2AccountRepayment = Entry('v2/account/repayment', ['spot', 'private'], 'POST', {'cost': 5}) + spot_private_post_v1_dw_transfer_in_margin = spotPrivatePostV1DwTransferInMargin = Entry('v1/dw/transfer-in/margin', ['spot', 'private'], 'POST', {'cost': 10}) + spot_private_post_v1_dw_transfer_out_margin = spotPrivatePostV1DwTransferOutMargin = Entry('v1/dw/transfer-out/margin', ['spot', 'private'], 'POST', {'cost': 10}) + spot_private_post_v1_margin_orders = spotPrivatePostV1MarginOrders = Entry('v1/margin/orders', ['spot', 'private'], 'POST', {'cost': 10}) + spot_private_post_v1_margin_orders_order_id_repay = spotPrivatePostV1MarginOrdersOrderIdRepay = Entry('v1/margin/orders/{order-id}/repay', ['spot', 'private'], 'POST', {'cost': 10}) + spot_private_post_v1_cross_margin_transfer_in = spotPrivatePostV1CrossMarginTransferIn = Entry('v1/cross-margin/transfer-in', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_post_v1_cross_margin_transfer_out = spotPrivatePostV1CrossMarginTransferOut = Entry('v1/cross-margin/transfer-out', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_post_v1_cross_margin_orders = spotPrivatePostV1CrossMarginOrders = Entry('v1/cross-margin/orders', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_post_v1_cross_margin_orders_order_id_repay = spotPrivatePostV1CrossMarginOrdersOrderIdRepay = Entry('v1/cross-margin/orders/{order-id}/repay', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_post_v1_stable_coin_exchange = spotPrivatePostV1StableCoinExchange = Entry('v1/stable-coin/exchange', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_post_v2_etp_creation = spotPrivatePostV2EtpCreation = Entry('v2/etp/creation', ['spot', 'private'], 'POST', {'cost': 5}) + spot_private_post_v2_etp_redemption = spotPrivatePostV2EtpRedemption = Entry('v2/etp/redemption', ['spot', 'private'], 'POST', {'cost': 5}) + spot_private_post_v2_etp_transactid_cancel = spotPrivatePostV2EtpTransactIdCancel = Entry('v2/etp/{transactId}/cancel', ['spot', 'private'], 'POST', {'cost': 10}) + spot_private_post_v2_etp_batch_cancel = spotPrivatePostV2EtpBatchCancel = Entry('v2/etp/batch-cancel', ['spot', 'private'], 'POST', {'cost': 50}) + contract_public_get_api_v1_timestamp = contractPublicGetApiV1Timestamp = Entry('api/v1/timestamp', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_heartbeat = contractPublicGetHeartbeat = Entry('heartbeat/', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_api_v1_contract_contract_info = contractPublicGetApiV1ContractContractInfo = Entry('api/v1/contract_contract_info', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_api_v1_contract_index = contractPublicGetApiV1ContractIndex = Entry('api/v1/contract_index', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_api_v1_contract_query_elements = contractPublicGetApiV1ContractQueryElements = Entry('api/v1/contract_query_elements', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_api_v1_contract_price_limit = contractPublicGetApiV1ContractPriceLimit = Entry('api/v1/contract_price_limit', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_api_v1_contract_open_interest = contractPublicGetApiV1ContractOpenInterest = Entry('api/v1/contract_open_interest', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_api_v1_contract_delivery_price = contractPublicGetApiV1ContractDeliveryPrice = Entry('api/v1/contract_delivery_price', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_market_depth = contractPublicGetMarketDepth = Entry('market/depth', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_market_bbo = contractPublicGetMarketBbo = Entry('market/bbo', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_market_history_kline = contractPublicGetMarketHistoryKline = Entry('market/history/kline', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_index_market_history_mark_price_kline = contractPublicGetIndexMarketHistoryMarkPriceKline = Entry('index/market/history/mark_price_kline', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_market_detail_merged = contractPublicGetMarketDetailMerged = Entry('market/detail/merged', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_market_detail_batch_merged = contractPublicGetMarketDetailBatchMerged = Entry('market/detail/batch_merged', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_v2_market_detail_batch_merged = contractPublicGetV2MarketDetailBatchMerged = Entry('v2/market/detail/batch_merged', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_market_trade = contractPublicGetMarketTrade = Entry('market/trade', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_market_history_trade = contractPublicGetMarketHistoryTrade = Entry('market/history/trade', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_api_v1_contract_risk_info = contractPublicGetApiV1ContractRiskInfo = Entry('api/v1/contract_risk_info', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_api_v1_contract_insurance_fund = contractPublicGetApiV1ContractInsuranceFund = Entry('api/v1/contract_insurance_fund', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_api_v1_contract_adjustfactor = contractPublicGetApiV1ContractAdjustfactor = Entry('api/v1/contract_adjustfactor', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_api_v1_contract_his_open_interest = contractPublicGetApiV1ContractHisOpenInterest = Entry('api/v1/contract_his_open_interest', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_api_v1_contract_ladder_margin = contractPublicGetApiV1ContractLadderMargin = Entry('api/v1/contract_ladder_margin', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_api_v1_contract_api_state = contractPublicGetApiV1ContractApiState = Entry('api/v1/contract_api_state', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_api_v1_contract_elite_account_ratio = contractPublicGetApiV1ContractEliteAccountRatio = Entry('api/v1/contract_elite_account_ratio', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_api_v1_contract_elite_position_ratio = contractPublicGetApiV1ContractElitePositionRatio = Entry('api/v1/contract_elite_position_ratio', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_api_v1_contract_liquidation_orders = contractPublicGetApiV1ContractLiquidationOrders = Entry('api/v1/contract_liquidation_orders', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_api_v1_contract_settlement_records = contractPublicGetApiV1ContractSettlementRecords = Entry('api/v1/contract_settlement_records', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_index_market_history_index = contractPublicGetIndexMarketHistoryIndex = Entry('index/market/history/index', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_index_market_history_basis = contractPublicGetIndexMarketHistoryBasis = Entry('index/market/history/basis', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_api_v1_contract_estimated_settlement_price = contractPublicGetApiV1ContractEstimatedSettlementPrice = Entry('api/v1/contract_estimated_settlement_price', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_api_v3_contract_liquidation_orders = contractPublicGetApiV3ContractLiquidationOrders = Entry('api/v3/contract_liquidation_orders', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_swap_api_v1_swap_contract_info = contractPublicGetSwapApiV1SwapContractInfo = Entry('swap-api/v1/swap_contract_info', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_swap_api_v1_swap_index = contractPublicGetSwapApiV1SwapIndex = Entry('swap-api/v1/swap_index', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_swap_api_v1_swap_query_elements = contractPublicGetSwapApiV1SwapQueryElements = Entry('swap-api/v1/swap_query_elements', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_swap_api_v1_swap_price_limit = contractPublicGetSwapApiV1SwapPriceLimit = Entry('swap-api/v1/swap_price_limit', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_swap_api_v1_swap_open_interest = contractPublicGetSwapApiV1SwapOpenInterest = Entry('swap-api/v1/swap_open_interest', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_swap_ex_market_depth = contractPublicGetSwapExMarketDepth = Entry('swap-ex/market/depth', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_swap_ex_market_bbo = contractPublicGetSwapExMarketBbo = Entry('swap-ex/market/bbo', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_swap_ex_market_history_kline = contractPublicGetSwapExMarketHistoryKline = Entry('swap-ex/market/history/kline', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_index_market_history_swap_mark_price_kline = contractPublicGetIndexMarketHistorySwapMarkPriceKline = Entry('index/market/history/swap_mark_price_kline', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_swap_ex_market_detail_merged = contractPublicGetSwapExMarketDetailMerged = Entry('swap-ex/market/detail/merged', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_v2_swap_ex_market_detail_batch_merged = contractPublicGetV2SwapExMarketDetailBatchMerged = Entry('v2/swap-ex/market/detail/batch_merged', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_index_market_history_swap_premium_index_kline = contractPublicGetIndexMarketHistorySwapPremiumIndexKline = Entry('index/market/history/swap_premium_index_kline', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_swap_ex_market_detail_batch_merged = contractPublicGetSwapExMarketDetailBatchMerged = Entry('swap-ex/market/detail/batch_merged', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_swap_ex_market_trade = contractPublicGetSwapExMarketTrade = Entry('swap-ex/market/trade', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_swap_ex_market_history_trade = contractPublicGetSwapExMarketHistoryTrade = Entry('swap-ex/market/history/trade', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_swap_api_v1_swap_risk_info = contractPublicGetSwapApiV1SwapRiskInfo = Entry('swap-api/v1/swap_risk_info', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_swap_api_v1_swap_insurance_fund = contractPublicGetSwapApiV1SwapInsuranceFund = Entry('swap-api/v1/swap_insurance_fund', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_swap_api_v1_swap_adjustfactor = contractPublicGetSwapApiV1SwapAdjustfactor = Entry('swap-api/v1/swap_adjustfactor', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_swap_api_v1_swap_his_open_interest = contractPublicGetSwapApiV1SwapHisOpenInterest = Entry('swap-api/v1/swap_his_open_interest', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_swap_api_v1_swap_ladder_margin = contractPublicGetSwapApiV1SwapLadderMargin = Entry('swap-api/v1/swap_ladder_margin', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_swap_api_v1_swap_api_state = contractPublicGetSwapApiV1SwapApiState = Entry('swap-api/v1/swap_api_state', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_swap_api_v1_swap_elite_account_ratio = contractPublicGetSwapApiV1SwapEliteAccountRatio = Entry('swap-api/v1/swap_elite_account_ratio', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_swap_api_v1_swap_elite_position_ratio = contractPublicGetSwapApiV1SwapElitePositionRatio = Entry('swap-api/v1/swap_elite_position_ratio', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_swap_api_v1_swap_estimated_settlement_price = contractPublicGetSwapApiV1SwapEstimatedSettlementPrice = Entry('swap-api/v1/swap_estimated_settlement_price', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_swap_api_v1_swap_liquidation_orders = contractPublicGetSwapApiV1SwapLiquidationOrders = Entry('swap-api/v1/swap_liquidation_orders', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_swap_api_v1_swap_settlement_records = contractPublicGetSwapApiV1SwapSettlementRecords = Entry('swap-api/v1/swap_settlement_records', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_swap_api_v1_swap_funding_rate = contractPublicGetSwapApiV1SwapFundingRate = Entry('swap-api/v1/swap_funding_rate', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_swap_api_v1_swap_batch_funding_rate = contractPublicGetSwapApiV1SwapBatchFundingRate = Entry('swap-api/v1/swap_batch_funding_rate', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_swap_api_v1_swap_historical_funding_rate = contractPublicGetSwapApiV1SwapHistoricalFundingRate = Entry('swap-api/v1/swap_historical_funding_rate', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_swap_api_v3_swap_liquidation_orders = contractPublicGetSwapApiV3SwapLiquidationOrders = Entry('swap-api/v3/swap_liquidation_orders', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_index_market_history_swap_estimated_rate_kline = contractPublicGetIndexMarketHistorySwapEstimatedRateKline = Entry('index/market/history/swap_estimated_rate_kline', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_index_market_history_swap_basis = contractPublicGetIndexMarketHistorySwapBasis = Entry('index/market/history/swap_basis', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_linear_swap_api_v1_swap_contract_info = contractPublicGetLinearSwapApiV1SwapContractInfo = Entry('linear-swap-api/v1/swap_contract_info', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_linear_swap_api_v1_swap_index = contractPublicGetLinearSwapApiV1SwapIndex = Entry('linear-swap-api/v1/swap_index', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_linear_swap_api_v1_swap_query_elements = contractPublicGetLinearSwapApiV1SwapQueryElements = Entry('linear-swap-api/v1/swap_query_elements', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_linear_swap_api_v1_swap_price_limit = contractPublicGetLinearSwapApiV1SwapPriceLimit = Entry('linear-swap-api/v1/swap_price_limit', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_linear_swap_api_v1_swap_open_interest = contractPublicGetLinearSwapApiV1SwapOpenInterest = Entry('linear-swap-api/v1/swap_open_interest', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_linear_swap_ex_market_depth = contractPublicGetLinearSwapExMarketDepth = Entry('linear-swap-ex/market/depth', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_linear_swap_ex_market_bbo = contractPublicGetLinearSwapExMarketBbo = Entry('linear-swap-ex/market/bbo', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_linear_swap_ex_market_history_kline = contractPublicGetLinearSwapExMarketHistoryKline = Entry('linear-swap-ex/market/history/kline', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_index_market_history_linear_swap_mark_price_kline = contractPublicGetIndexMarketHistoryLinearSwapMarkPriceKline = Entry('index/market/history/linear_swap_mark_price_kline', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_linear_swap_ex_market_detail_merged = contractPublicGetLinearSwapExMarketDetailMerged = Entry('linear-swap-ex/market/detail/merged', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_linear_swap_ex_market_detail_batch_merged = contractPublicGetLinearSwapExMarketDetailBatchMerged = Entry('linear-swap-ex/market/detail/batch_merged', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_v2_linear_swap_ex_market_detail_batch_merged = contractPublicGetV2LinearSwapExMarketDetailBatchMerged = Entry('v2/linear-swap-ex/market/detail/batch_merged', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_linear_swap_ex_market_trade = contractPublicGetLinearSwapExMarketTrade = Entry('linear-swap-ex/market/trade', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_linear_swap_ex_market_history_trade = contractPublicGetLinearSwapExMarketHistoryTrade = Entry('linear-swap-ex/market/history/trade', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_linear_swap_api_v1_swap_risk_info = contractPublicGetLinearSwapApiV1SwapRiskInfo = Entry('linear-swap-api/v1/swap_risk_info', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_swap_api_v1_linear_swap_api_v1_swap_insurance_fund = contractPublicGetSwapApiV1LinearSwapApiV1SwapInsuranceFund = Entry('swap-api/v1/linear-swap-api/v1/swap_insurance_fund', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_linear_swap_api_v1_swap_adjustfactor = contractPublicGetLinearSwapApiV1SwapAdjustfactor = Entry('linear-swap-api/v1/swap_adjustfactor', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_linear_swap_api_v1_swap_cross_adjustfactor = contractPublicGetLinearSwapApiV1SwapCrossAdjustfactor = Entry('linear-swap-api/v1/swap_cross_adjustfactor', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_linear_swap_api_v1_swap_his_open_interest = contractPublicGetLinearSwapApiV1SwapHisOpenInterest = Entry('linear-swap-api/v1/swap_his_open_interest', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_linear_swap_api_v1_swap_ladder_margin = contractPublicGetLinearSwapApiV1SwapLadderMargin = Entry('linear-swap-api/v1/swap_ladder_margin', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_linear_swap_api_v1_swap_cross_ladder_margin = contractPublicGetLinearSwapApiV1SwapCrossLadderMargin = Entry('linear-swap-api/v1/swap_cross_ladder_margin', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_linear_swap_api_v1_swap_api_state = contractPublicGetLinearSwapApiV1SwapApiState = Entry('linear-swap-api/v1/swap_api_state', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_linear_swap_api_v1_swap_cross_transfer_state = contractPublicGetLinearSwapApiV1SwapCrossTransferState = Entry('linear-swap-api/v1/swap_cross_transfer_state', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_linear_swap_api_v1_swap_cross_trade_state = contractPublicGetLinearSwapApiV1SwapCrossTradeState = Entry('linear-swap-api/v1/swap_cross_trade_state', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_linear_swap_api_v1_swap_elite_account_ratio = contractPublicGetLinearSwapApiV1SwapEliteAccountRatio = Entry('linear-swap-api/v1/swap_elite_account_ratio', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_linear_swap_api_v1_swap_elite_position_ratio = contractPublicGetLinearSwapApiV1SwapElitePositionRatio = Entry('linear-swap-api/v1/swap_elite_position_ratio', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_linear_swap_api_v1_swap_liquidation_orders = contractPublicGetLinearSwapApiV1SwapLiquidationOrders = Entry('linear-swap-api/v1/swap_liquidation_orders', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_linear_swap_api_v1_swap_settlement_records = contractPublicGetLinearSwapApiV1SwapSettlementRecords = Entry('linear-swap-api/v1/swap_settlement_records', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_linear_swap_api_v1_swap_funding_rate = contractPublicGetLinearSwapApiV1SwapFundingRate = Entry('linear-swap-api/v1/swap_funding_rate', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_linear_swap_api_v1_swap_batch_funding_rate = contractPublicGetLinearSwapApiV1SwapBatchFundingRate = Entry('linear-swap-api/v1/swap_batch_funding_rate', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_linear_swap_api_v1_swap_historical_funding_rate = contractPublicGetLinearSwapApiV1SwapHistoricalFundingRate = Entry('linear-swap-api/v1/swap_historical_funding_rate', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_linear_swap_api_v3_swap_liquidation_orders = contractPublicGetLinearSwapApiV3SwapLiquidationOrders = Entry('linear-swap-api/v3/swap_liquidation_orders', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_index_market_history_linear_swap_premium_index_kline = contractPublicGetIndexMarketHistoryLinearSwapPremiumIndexKline = Entry('index/market/history/linear_swap_premium_index_kline', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_index_market_history_linear_swap_estimated_rate_kline = contractPublicGetIndexMarketHistoryLinearSwapEstimatedRateKline = Entry('index/market/history/linear_swap_estimated_rate_kline', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_index_market_history_linear_swap_basis = contractPublicGetIndexMarketHistoryLinearSwapBasis = Entry('index/market/history/linear_swap_basis', ['contract', 'public'], 'GET', {'cost': 1}) + contract_public_get_linear_swap_api_v1_swap_estimated_settlement_price = contractPublicGetLinearSwapApiV1SwapEstimatedSettlementPrice = Entry('linear-swap-api/v1/swap_estimated_settlement_price', ['contract', 'public'], 'GET', {'cost': 1}) + contract_private_get_api_v1_contract_sub_auth_list = contractPrivateGetApiV1ContractSubAuthList = Entry('api/v1/contract_sub_auth_list', ['contract', 'private'], 'GET', {'cost': 1}) + contract_private_get_api_v1_contract_api_trading_status = contractPrivateGetApiV1ContractApiTradingStatus = Entry('api/v1/contract_api_trading_status', ['contract', 'private'], 'GET', {'cost': 1}) + contract_private_get_swap_api_v1_swap_sub_auth_list = contractPrivateGetSwapApiV1SwapSubAuthList = Entry('swap-api/v1/swap_sub_auth_list', ['contract', 'private'], 'GET', {'cost': 1}) + contract_private_get_swap_api_v1_swap_api_trading_status = contractPrivateGetSwapApiV1SwapApiTradingStatus = Entry('swap-api/v1/swap_api_trading_status', ['contract', 'private'], 'GET', {'cost': 1}) + contract_private_get_linear_swap_api_v1_swap_sub_auth_list = contractPrivateGetLinearSwapApiV1SwapSubAuthList = Entry('linear-swap-api/v1/swap_sub_auth_list', ['contract', 'private'], 'GET', {'cost': 1}) + contract_private_get_linear_swap_api_v1_swap_api_trading_status = contractPrivateGetLinearSwapApiV1SwapApiTradingStatus = Entry('linear-swap-api/v1/swap_api_trading_status', ['contract', 'private'], 'GET', {'cost': 1}) + contract_private_get_linear_swap_api_v1_swap_cross_position_side = contractPrivateGetLinearSwapApiV1SwapCrossPositionSide = Entry('linear-swap-api/v1/swap_cross_position_side', ['contract', 'private'], 'GET', {'cost': 1}) + contract_private_get_linear_swap_api_v1_swap_position_side = contractPrivateGetLinearSwapApiV1SwapPositionSide = Entry('linear-swap-api/v1/swap_position_side', ['contract', 'private'], 'GET', {'cost': 1}) + contract_private_get_linear_swap_api_v3_unified_account_info = contractPrivateGetLinearSwapApiV3UnifiedAccountInfo = Entry('linear-swap-api/v3/unified_account_info', ['contract', 'private'], 'GET', {'cost': 1}) + contract_private_get_linear_swap_api_v3_fix_position_margin_change_record = contractPrivateGetLinearSwapApiV3FixPositionMarginChangeRecord = Entry('linear-swap-api/v3/fix_position_margin_change_record', ['contract', 'private'], 'GET', {'cost': 1}) + contract_private_get_linear_swap_api_v3_swap_unified_account_type = contractPrivateGetLinearSwapApiV3SwapUnifiedAccountType = Entry('linear-swap-api/v3/swap_unified_account_type', ['contract', 'private'], 'GET', {'cost': 1}) + contract_private_get_linear_swap_api_v3_linear_swap_overview_account_info = contractPrivateGetLinearSwapApiV3LinearSwapOverviewAccountInfo = Entry('linear-swap-api/v3/linear_swap_overview_account_info', ['contract', 'private'], 'GET', {'cost': 1}) + contract_private_post_api_v1_contract_balance_valuation = contractPrivatePostApiV1ContractBalanceValuation = Entry('api/v1/contract_balance_valuation', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_account_info = contractPrivatePostApiV1ContractAccountInfo = Entry('api/v1/contract_account_info', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_position_info = contractPrivatePostApiV1ContractPositionInfo = Entry('api/v1/contract_position_info', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_sub_auth = contractPrivatePostApiV1ContractSubAuth = Entry('api/v1/contract_sub_auth', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_sub_account_list = contractPrivatePostApiV1ContractSubAccountList = Entry('api/v1/contract_sub_account_list', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_sub_account_info_list = contractPrivatePostApiV1ContractSubAccountInfoList = Entry('api/v1/contract_sub_account_info_list', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_sub_account_info = contractPrivatePostApiV1ContractSubAccountInfo = Entry('api/v1/contract_sub_account_info', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_sub_position_info = contractPrivatePostApiV1ContractSubPositionInfo = Entry('api/v1/contract_sub_position_info', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_financial_record = contractPrivatePostApiV1ContractFinancialRecord = Entry('api/v1/contract_financial_record', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_financial_record_exact = contractPrivatePostApiV1ContractFinancialRecordExact = Entry('api/v1/contract_financial_record_exact', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_user_settlement_records = contractPrivatePostApiV1ContractUserSettlementRecords = Entry('api/v1/contract_user_settlement_records', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_order_limit = contractPrivatePostApiV1ContractOrderLimit = Entry('api/v1/contract_order_limit', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_fee = contractPrivatePostApiV1ContractFee = Entry('api/v1/contract_fee', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_transfer_limit = contractPrivatePostApiV1ContractTransferLimit = Entry('api/v1/contract_transfer_limit', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_position_limit = contractPrivatePostApiV1ContractPositionLimit = Entry('api/v1/contract_position_limit', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_account_position_info = contractPrivatePostApiV1ContractAccountPositionInfo = Entry('api/v1/contract_account_position_info', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_master_sub_transfer = contractPrivatePostApiV1ContractMasterSubTransfer = Entry('api/v1/contract_master_sub_transfer', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_master_sub_transfer_record = contractPrivatePostApiV1ContractMasterSubTransferRecord = Entry('api/v1/contract_master_sub_transfer_record', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_available_level_rate = contractPrivatePostApiV1ContractAvailableLevelRate = Entry('api/v1/contract_available_level_rate', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v3_contract_financial_record = contractPrivatePostApiV3ContractFinancialRecord = Entry('api/v3/contract_financial_record', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v3_contract_financial_record_exact = contractPrivatePostApiV3ContractFinancialRecordExact = Entry('api/v3/contract_financial_record_exact', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_cancel_after = contractPrivatePostApiV1ContractCancelAfter = Entry('api/v1/contract-cancel-after', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_order = contractPrivatePostApiV1ContractOrder = Entry('api/v1/contract_order', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_batchorder = contractPrivatePostApiV1ContractBatchorder = Entry('api/v1/contract_batchorder', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_cancel = contractPrivatePostApiV1ContractCancel = Entry('api/v1/contract_cancel', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_cancelall = contractPrivatePostApiV1ContractCancelall = Entry('api/v1/contract_cancelall', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_switch_lever_rate = contractPrivatePostApiV1ContractSwitchLeverRate = Entry('api/v1/contract_switch_lever_rate', ['contract', 'private'], 'POST', {'cost': 30}) + contract_private_post_api_v1_lightning_close_position = contractPrivatePostApiV1LightningClosePosition = Entry('api/v1/lightning_close_position', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_order_info = contractPrivatePostApiV1ContractOrderInfo = Entry('api/v1/contract_order_info', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_order_detail = contractPrivatePostApiV1ContractOrderDetail = Entry('api/v1/contract_order_detail', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_openorders = contractPrivatePostApiV1ContractOpenorders = Entry('api/v1/contract_openorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_hisorders = contractPrivatePostApiV1ContractHisorders = Entry('api/v1/contract_hisorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_hisorders_exact = contractPrivatePostApiV1ContractHisordersExact = Entry('api/v1/contract_hisorders_exact', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_matchresults = contractPrivatePostApiV1ContractMatchresults = Entry('api/v1/contract_matchresults', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_matchresults_exact = contractPrivatePostApiV1ContractMatchresultsExact = Entry('api/v1/contract_matchresults_exact', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v3_contract_hisorders = contractPrivatePostApiV3ContractHisorders = Entry('api/v3/contract_hisorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v3_contract_hisorders_exact = contractPrivatePostApiV3ContractHisordersExact = Entry('api/v3/contract_hisorders_exact', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v3_contract_matchresults = contractPrivatePostApiV3ContractMatchresults = Entry('api/v3/contract_matchresults', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v3_contract_matchresults_exact = contractPrivatePostApiV3ContractMatchresultsExact = Entry('api/v3/contract_matchresults_exact', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_trigger_order = contractPrivatePostApiV1ContractTriggerOrder = Entry('api/v1/contract_trigger_order', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_trigger_cancel = contractPrivatePostApiV1ContractTriggerCancel = Entry('api/v1/contract_trigger_cancel', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_trigger_cancelall = contractPrivatePostApiV1ContractTriggerCancelall = Entry('api/v1/contract_trigger_cancelall', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_trigger_openorders = contractPrivatePostApiV1ContractTriggerOpenorders = Entry('api/v1/contract_trigger_openorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_trigger_hisorders = contractPrivatePostApiV1ContractTriggerHisorders = Entry('api/v1/contract_trigger_hisorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_tpsl_order = contractPrivatePostApiV1ContractTpslOrder = Entry('api/v1/contract_tpsl_order', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_tpsl_cancel = contractPrivatePostApiV1ContractTpslCancel = Entry('api/v1/contract_tpsl_cancel', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_tpsl_cancelall = contractPrivatePostApiV1ContractTpslCancelall = Entry('api/v1/contract_tpsl_cancelall', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_tpsl_openorders = contractPrivatePostApiV1ContractTpslOpenorders = Entry('api/v1/contract_tpsl_openorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_tpsl_hisorders = contractPrivatePostApiV1ContractTpslHisorders = Entry('api/v1/contract_tpsl_hisorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_relation_tpsl_order = contractPrivatePostApiV1ContractRelationTpslOrder = Entry('api/v1/contract_relation_tpsl_order', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_track_order = contractPrivatePostApiV1ContractTrackOrder = Entry('api/v1/contract_track_order', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_track_cancel = contractPrivatePostApiV1ContractTrackCancel = Entry('api/v1/contract_track_cancel', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_track_cancelall = contractPrivatePostApiV1ContractTrackCancelall = Entry('api/v1/contract_track_cancelall', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_track_openorders = contractPrivatePostApiV1ContractTrackOpenorders = Entry('api/v1/contract_track_openorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_api_v1_contract_track_hisorders = contractPrivatePostApiV1ContractTrackHisorders = Entry('api/v1/contract_track_hisorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_balance_valuation = contractPrivatePostSwapApiV1SwapBalanceValuation = Entry('swap-api/v1/swap_balance_valuation', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_account_info = contractPrivatePostSwapApiV1SwapAccountInfo = Entry('swap-api/v1/swap_account_info', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_position_info = contractPrivatePostSwapApiV1SwapPositionInfo = Entry('swap-api/v1/swap_position_info', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_account_position_info = contractPrivatePostSwapApiV1SwapAccountPositionInfo = Entry('swap-api/v1/swap_account_position_info', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_sub_auth = contractPrivatePostSwapApiV1SwapSubAuth = Entry('swap-api/v1/swap_sub_auth', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_sub_account_list = contractPrivatePostSwapApiV1SwapSubAccountList = Entry('swap-api/v1/swap_sub_account_list', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_sub_account_info_list = contractPrivatePostSwapApiV1SwapSubAccountInfoList = Entry('swap-api/v1/swap_sub_account_info_list', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_sub_account_info = contractPrivatePostSwapApiV1SwapSubAccountInfo = Entry('swap-api/v1/swap_sub_account_info', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_sub_position_info = contractPrivatePostSwapApiV1SwapSubPositionInfo = Entry('swap-api/v1/swap_sub_position_info', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_financial_record = contractPrivatePostSwapApiV1SwapFinancialRecord = Entry('swap-api/v1/swap_financial_record', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_financial_record_exact = contractPrivatePostSwapApiV1SwapFinancialRecordExact = Entry('swap-api/v1/swap_financial_record_exact', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_user_settlement_records = contractPrivatePostSwapApiV1SwapUserSettlementRecords = Entry('swap-api/v1/swap_user_settlement_records', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_available_level_rate = contractPrivatePostSwapApiV1SwapAvailableLevelRate = Entry('swap-api/v1/swap_available_level_rate', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_order_limit = contractPrivatePostSwapApiV1SwapOrderLimit = Entry('swap-api/v1/swap_order_limit', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_fee = contractPrivatePostSwapApiV1SwapFee = Entry('swap-api/v1/swap_fee', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_transfer_limit = contractPrivatePostSwapApiV1SwapTransferLimit = Entry('swap-api/v1/swap_transfer_limit', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_position_limit = contractPrivatePostSwapApiV1SwapPositionLimit = Entry('swap-api/v1/swap_position_limit', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_master_sub_transfer = contractPrivatePostSwapApiV1SwapMasterSubTransfer = Entry('swap-api/v1/swap_master_sub_transfer', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_master_sub_transfer_record = contractPrivatePostSwapApiV1SwapMasterSubTransferRecord = Entry('swap-api/v1/swap_master_sub_transfer_record', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v3_swap_financial_record = contractPrivatePostSwapApiV3SwapFinancialRecord = Entry('swap-api/v3/swap_financial_record', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v3_swap_financial_record_exact = contractPrivatePostSwapApiV3SwapFinancialRecordExact = Entry('swap-api/v3/swap_financial_record_exact', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_cancel_after = contractPrivatePostSwapApiV1SwapCancelAfter = Entry('swap-api/v1/swap-cancel-after', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_order = contractPrivatePostSwapApiV1SwapOrder = Entry('swap-api/v1/swap_order', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_batchorder = contractPrivatePostSwapApiV1SwapBatchorder = Entry('swap-api/v1/swap_batchorder', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_cancel = contractPrivatePostSwapApiV1SwapCancel = Entry('swap-api/v1/swap_cancel', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_cancelall = contractPrivatePostSwapApiV1SwapCancelall = Entry('swap-api/v1/swap_cancelall', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_lightning_close_position = contractPrivatePostSwapApiV1SwapLightningClosePosition = Entry('swap-api/v1/swap_lightning_close_position', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_switch_lever_rate = contractPrivatePostSwapApiV1SwapSwitchLeverRate = Entry('swap-api/v1/swap_switch_lever_rate', ['contract', 'private'], 'POST', {'cost': 30}) + contract_private_post_swap_api_v1_swap_order_info = contractPrivatePostSwapApiV1SwapOrderInfo = Entry('swap-api/v1/swap_order_info', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_order_detail = contractPrivatePostSwapApiV1SwapOrderDetail = Entry('swap-api/v1/swap_order_detail', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_openorders = contractPrivatePostSwapApiV1SwapOpenorders = Entry('swap-api/v1/swap_openorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_hisorders = contractPrivatePostSwapApiV1SwapHisorders = Entry('swap-api/v1/swap_hisorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_hisorders_exact = contractPrivatePostSwapApiV1SwapHisordersExact = Entry('swap-api/v1/swap_hisorders_exact', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_matchresults = contractPrivatePostSwapApiV1SwapMatchresults = Entry('swap-api/v1/swap_matchresults', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_matchresults_exact = contractPrivatePostSwapApiV1SwapMatchresultsExact = Entry('swap-api/v1/swap_matchresults_exact', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v3_swap_matchresults = contractPrivatePostSwapApiV3SwapMatchresults = Entry('swap-api/v3/swap_matchresults', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v3_swap_matchresults_exact = contractPrivatePostSwapApiV3SwapMatchresultsExact = Entry('swap-api/v3/swap_matchresults_exact', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v3_swap_hisorders = contractPrivatePostSwapApiV3SwapHisorders = Entry('swap-api/v3/swap_hisorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v3_swap_hisorders_exact = contractPrivatePostSwapApiV3SwapHisordersExact = Entry('swap-api/v3/swap_hisorders_exact', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_trigger_order = contractPrivatePostSwapApiV1SwapTriggerOrder = Entry('swap-api/v1/swap_trigger_order', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_trigger_cancel = contractPrivatePostSwapApiV1SwapTriggerCancel = Entry('swap-api/v1/swap_trigger_cancel', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_trigger_cancelall = contractPrivatePostSwapApiV1SwapTriggerCancelall = Entry('swap-api/v1/swap_trigger_cancelall', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_trigger_openorders = contractPrivatePostSwapApiV1SwapTriggerOpenorders = Entry('swap-api/v1/swap_trigger_openorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_trigger_hisorders = contractPrivatePostSwapApiV1SwapTriggerHisorders = Entry('swap-api/v1/swap_trigger_hisorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_tpsl_order = contractPrivatePostSwapApiV1SwapTpslOrder = Entry('swap-api/v1/swap_tpsl_order', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_tpsl_cancel = contractPrivatePostSwapApiV1SwapTpslCancel = Entry('swap-api/v1/swap_tpsl_cancel', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_tpsl_cancelall = contractPrivatePostSwapApiV1SwapTpslCancelall = Entry('swap-api/v1/swap_tpsl_cancelall', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_tpsl_openorders = contractPrivatePostSwapApiV1SwapTpslOpenorders = Entry('swap-api/v1/swap_tpsl_openorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_tpsl_hisorders = contractPrivatePostSwapApiV1SwapTpslHisorders = Entry('swap-api/v1/swap_tpsl_hisorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_relation_tpsl_order = contractPrivatePostSwapApiV1SwapRelationTpslOrder = Entry('swap-api/v1/swap_relation_tpsl_order', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_track_order = contractPrivatePostSwapApiV1SwapTrackOrder = Entry('swap-api/v1/swap_track_order', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_track_cancel = contractPrivatePostSwapApiV1SwapTrackCancel = Entry('swap-api/v1/swap_track_cancel', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_track_cancelall = contractPrivatePostSwapApiV1SwapTrackCancelall = Entry('swap-api/v1/swap_track_cancelall', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_track_openorders = contractPrivatePostSwapApiV1SwapTrackOpenorders = Entry('swap-api/v1/swap_track_openorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_swap_api_v1_swap_track_hisorders = contractPrivatePostSwapApiV1SwapTrackHisorders = Entry('swap-api/v1/swap_track_hisorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_lever_position_limit = contractPrivatePostLinearSwapApiV1SwapLeverPositionLimit = Entry('linear-swap-api/v1/swap_lever_position_limit', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_lever_position_limit = contractPrivatePostLinearSwapApiV1SwapCrossLeverPositionLimit = Entry('linear-swap-api/v1/swap_cross_lever_position_limit', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_balance_valuation = contractPrivatePostLinearSwapApiV1SwapBalanceValuation = Entry('linear-swap-api/v1/swap_balance_valuation', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_account_info = contractPrivatePostLinearSwapApiV1SwapAccountInfo = Entry('linear-swap-api/v1/swap_account_info', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_account_info = contractPrivatePostLinearSwapApiV1SwapCrossAccountInfo = Entry('linear-swap-api/v1/swap_cross_account_info', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_position_info = contractPrivatePostLinearSwapApiV1SwapPositionInfo = Entry('linear-swap-api/v1/swap_position_info', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_position_info = contractPrivatePostLinearSwapApiV1SwapCrossPositionInfo = Entry('linear-swap-api/v1/swap_cross_position_info', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_account_position_info = contractPrivatePostLinearSwapApiV1SwapAccountPositionInfo = Entry('linear-swap-api/v1/swap_account_position_info', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_account_position_info = contractPrivatePostLinearSwapApiV1SwapCrossAccountPositionInfo = Entry('linear-swap-api/v1/swap_cross_account_position_info', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_sub_auth = contractPrivatePostLinearSwapApiV1SwapSubAuth = Entry('linear-swap-api/v1/swap_sub_auth', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_sub_account_list = contractPrivatePostLinearSwapApiV1SwapSubAccountList = Entry('linear-swap-api/v1/swap_sub_account_list', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_sub_account_list = contractPrivatePostLinearSwapApiV1SwapCrossSubAccountList = Entry('linear-swap-api/v1/swap_cross_sub_account_list', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_sub_account_info_list = contractPrivatePostLinearSwapApiV1SwapSubAccountInfoList = Entry('linear-swap-api/v1/swap_sub_account_info_list', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_sub_account_info_list = contractPrivatePostLinearSwapApiV1SwapCrossSubAccountInfoList = Entry('linear-swap-api/v1/swap_cross_sub_account_info_list', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_sub_account_info = contractPrivatePostLinearSwapApiV1SwapSubAccountInfo = Entry('linear-swap-api/v1/swap_sub_account_info', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_sub_account_info = contractPrivatePostLinearSwapApiV1SwapCrossSubAccountInfo = Entry('linear-swap-api/v1/swap_cross_sub_account_info', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_sub_position_info = contractPrivatePostLinearSwapApiV1SwapSubPositionInfo = Entry('linear-swap-api/v1/swap_sub_position_info', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_sub_position_info = contractPrivatePostLinearSwapApiV1SwapCrossSubPositionInfo = Entry('linear-swap-api/v1/swap_cross_sub_position_info', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_financial_record = contractPrivatePostLinearSwapApiV1SwapFinancialRecord = Entry('linear-swap-api/v1/swap_financial_record', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_financial_record_exact = contractPrivatePostLinearSwapApiV1SwapFinancialRecordExact = Entry('linear-swap-api/v1/swap_financial_record_exact', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_user_settlement_records = contractPrivatePostLinearSwapApiV1SwapUserSettlementRecords = Entry('linear-swap-api/v1/swap_user_settlement_records', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_user_settlement_records = contractPrivatePostLinearSwapApiV1SwapCrossUserSettlementRecords = Entry('linear-swap-api/v1/swap_cross_user_settlement_records', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_available_level_rate = contractPrivatePostLinearSwapApiV1SwapAvailableLevelRate = Entry('linear-swap-api/v1/swap_available_level_rate', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_available_level_rate = contractPrivatePostLinearSwapApiV1SwapCrossAvailableLevelRate = Entry('linear-swap-api/v1/swap_cross_available_level_rate', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_order_limit = contractPrivatePostLinearSwapApiV1SwapOrderLimit = Entry('linear-swap-api/v1/swap_order_limit', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_fee = contractPrivatePostLinearSwapApiV1SwapFee = Entry('linear-swap-api/v1/swap_fee', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_transfer_limit = contractPrivatePostLinearSwapApiV1SwapTransferLimit = Entry('linear-swap-api/v1/swap_transfer_limit', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_transfer_limit = contractPrivatePostLinearSwapApiV1SwapCrossTransferLimit = Entry('linear-swap-api/v1/swap_cross_transfer_limit', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_position_limit = contractPrivatePostLinearSwapApiV1SwapPositionLimit = Entry('linear-swap-api/v1/swap_position_limit', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_position_limit = contractPrivatePostLinearSwapApiV1SwapCrossPositionLimit = Entry('linear-swap-api/v1/swap_cross_position_limit', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_master_sub_transfer = contractPrivatePostLinearSwapApiV1SwapMasterSubTransfer = Entry('linear-swap-api/v1/swap_master_sub_transfer', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_master_sub_transfer_record = contractPrivatePostLinearSwapApiV1SwapMasterSubTransferRecord = Entry('linear-swap-api/v1/swap_master_sub_transfer_record', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_transfer_inner = contractPrivatePostLinearSwapApiV1SwapTransferInner = Entry('linear-swap-api/v1/swap_transfer_inner', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v3_swap_financial_record = contractPrivatePostLinearSwapApiV3SwapFinancialRecord = Entry('linear-swap-api/v3/swap_financial_record', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v3_swap_financial_record_exact = contractPrivatePostLinearSwapApiV3SwapFinancialRecordExact = Entry('linear-swap-api/v3/swap_financial_record_exact', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_order = contractPrivatePostLinearSwapApiV1SwapOrder = Entry('linear-swap-api/v1/swap_order', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_order = contractPrivatePostLinearSwapApiV1SwapCrossOrder = Entry('linear-swap-api/v1/swap_cross_order', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_batchorder = contractPrivatePostLinearSwapApiV1SwapBatchorder = Entry('linear-swap-api/v1/swap_batchorder', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_batchorder = contractPrivatePostLinearSwapApiV1SwapCrossBatchorder = Entry('linear-swap-api/v1/swap_cross_batchorder', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cancel = contractPrivatePostLinearSwapApiV1SwapCancel = Entry('linear-swap-api/v1/swap_cancel', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_cancel = contractPrivatePostLinearSwapApiV1SwapCrossCancel = Entry('linear-swap-api/v1/swap_cross_cancel', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cancelall = contractPrivatePostLinearSwapApiV1SwapCancelall = Entry('linear-swap-api/v1/swap_cancelall', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_cancelall = contractPrivatePostLinearSwapApiV1SwapCrossCancelall = Entry('linear-swap-api/v1/swap_cross_cancelall', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_switch_lever_rate = contractPrivatePostLinearSwapApiV1SwapSwitchLeverRate = Entry('linear-swap-api/v1/swap_switch_lever_rate', ['contract', 'private'], 'POST', {'cost': 30}) + contract_private_post_linear_swap_api_v1_swap_cross_switch_lever_rate = contractPrivatePostLinearSwapApiV1SwapCrossSwitchLeverRate = Entry('linear-swap-api/v1/swap_cross_switch_lever_rate', ['contract', 'private'], 'POST', {'cost': 30}) + contract_private_post_linear_swap_api_v1_swap_lightning_close_position = contractPrivatePostLinearSwapApiV1SwapLightningClosePosition = Entry('linear-swap-api/v1/swap_lightning_close_position', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_lightning_close_position = contractPrivatePostLinearSwapApiV1SwapCrossLightningClosePosition = Entry('linear-swap-api/v1/swap_cross_lightning_close_position', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_order_info = contractPrivatePostLinearSwapApiV1SwapOrderInfo = Entry('linear-swap-api/v1/swap_order_info', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_order_info = contractPrivatePostLinearSwapApiV1SwapCrossOrderInfo = Entry('linear-swap-api/v1/swap_cross_order_info', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_order_detail = contractPrivatePostLinearSwapApiV1SwapOrderDetail = Entry('linear-swap-api/v1/swap_order_detail', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_order_detail = contractPrivatePostLinearSwapApiV1SwapCrossOrderDetail = Entry('linear-swap-api/v1/swap_cross_order_detail', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_openorders = contractPrivatePostLinearSwapApiV1SwapOpenorders = Entry('linear-swap-api/v1/swap_openorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_openorders = contractPrivatePostLinearSwapApiV1SwapCrossOpenorders = Entry('linear-swap-api/v1/swap_cross_openorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_hisorders = contractPrivatePostLinearSwapApiV1SwapHisorders = Entry('linear-swap-api/v1/swap_hisorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_hisorders = contractPrivatePostLinearSwapApiV1SwapCrossHisorders = Entry('linear-swap-api/v1/swap_cross_hisorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_hisorders_exact = contractPrivatePostLinearSwapApiV1SwapHisordersExact = Entry('linear-swap-api/v1/swap_hisorders_exact', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_hisorders_exact = contractPrivatePostLinearSwapApiV1SwapCrossHisordersExact = Entry('linear-swap-api/v1/swap_cross_hisorders_exact', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_matchresults = contractPrivatePostLinearSwapApiV1SwapMatchresults = Entry('linear-swap-api/v1/swap_matchresults', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_matchresults = contractPrivatePostLinearSwapApiV1SwapCrossMatchresults = Entry('linear-swap-api/v1/swap_cross_matchresults', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_matchresults_exact = contractPrivatePostLinearSwapApiV1SwapMatchresultsExact = Entry('linear-swap-api/v1/swap_matchresults_exact', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_matchresults_exact = contractPrivatePostLinearSwapApiV1SwapCrossMatchresultsExact = Entry('linear-swap-api/v1/swap_cross_matchresults_exact', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_linear_cancel_after = contractPrivatePostLinearSwapApiV1LinearCancelAfter = Entry('linear-swap-api/v1/linear-cancel-after', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_switch_position_mode = contractPrivatePostLinearSwapApiV1SwapSwitchPositionMode = Entry('linear-swap-api/v1/swap_switch_position_mode', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_switch_position_mode = contractPrivatePostLinearSwapApiV1SwapCrossSwitchPositionMode = Entry('linear-swap-api/v1/swap_cross_switch_position_mode', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v3_swap_matchresults = contractPrivatePostLinearSwapApiV3SwapMatchresults = Entry('linear-swap-api/v3/swap_matchresults', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v3_swap_cross_matchresults = contractPrivatePostLinearSwapApiV3SwapCrossMatchresults = Entry('linear-swap-api/v3/swap_cross_matchresults', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v3_swap_matchresults_exact = contractPrivatePostLinearSwapApiV3SwapMatchresultsExact = Entry('linear-swap-api/v3/swap_matchresults_exact', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v3_swap_cross_matchresults_exact = contractPrivatePostLinearSwapApiV3SwapCrossMatchresultsExact = Entry('linear-swap-api/v3/swap_cross_matchresults_exact', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v3_swap_hisorders = contractPrivatePostLinearSwapApiV3SwapHisorders = Entry('linear-swap-api/v3/swap_hisorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v3_swap_cross_hisorders = contractPrivatePostLinearSwapApiV3SwapCrossHisorders = Entry('linear-swap-api/v3/swap_cross_hisorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v3_swap_hisorders_exact = contractPrivatePostLinearSwapApiV3SwapHisordersExact = Entry('linear-swap-api/v3/swap_hisorders_exact', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v3_swap_cross_hisorders_exact = contractPrivatePostLinearSwapApiV3SwapCrossHisordersExact = Entry('linear-swap-api/v3/swap_cross_hisorders_exact', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v3_fix_position_margin_change = contractPrivatePostLinearSwapApiV3FixPositionMarginChange = Entry('linear-swap-api/v3/fix_position_margin_change', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v3_swap_switch_account_type = contractPrivatePostLinearSwapApiV3SwapSwitchAccountType = Entry('linear-swap-api/v3/swap_switch_account_type', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v3_linear_swap_fee_switch = contractPrivatePostLinearSwapApiV3LinearSwapFeeSwitch = Entry('linear-swap-api/v3/linear_swap_fee_switch', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_trigger_order = contractPrivatePostLinearSwapApiV1SwapTriggerOrder = Entry('linear-swap-api/v1/swap_trigger_order', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_trigger_order = contractPrivatePostLinearSwapApiV1SwapCrossTriggerOrder = Entry('linear-swap-api/v1/swap_cross_trigger_order', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_trigger_cancel = contractPrivatePostLinearSwapApiV1SwapTriggerCancel = Entry('linear-swap-api/v1/swap_trigger_cancel', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_trigger_cancel = contractPrivatePostLinearSwapApiV1SwapCrossTriggerCancel = Entry('linear-swap-api/v1/swap_cross_trigger_cancel', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_trigger_cancelall = contractPrivatePostLinearSwapApiV1SwapTriggerCancelall = Entry('linear-swap-api/v1/swap_trigger_cancelall', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_trigger_cancelall = contractPrivatePostLinearSwapApiV1SwapCrossTriggerCancelall = Entry('linear-swap-api/v1/swap_cross_trigger_cancelall', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_trigger_openorders = contractPrivatePostLinearSwapApiV1SwapTriggerOpenorders = Entry('linear-swap-api/v1/swap_trigger_openorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_trigger_openorders = contractPrivatePostLinearSwapApiV1SwapCrossTriggerOpenorders = Entry('linear-swap-api/v1/swap_cross_trigger_openorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_trigger_hisorders = contractPrivatePostLinearSwapApiV1SwapTriggerHisorders = Entry('linear-swap-api/v1/swap_trigger_hisorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_trigger_hisorders = contractPrivatePostLinearSwapApiV1SwapCrossTriggerHisorders = Entry('linear-swap-api/v1/swap_cross_trigger_hisorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_tpsl_order = contractPrivatePostLinearSwapApiV1SwapTpslOrder = Entry('linear-swap-api/v1/swap_tpsl_order', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_tpsl_order = contractPrivatePostLinearSwapApiV1SwapCrossTpslOrder = Entry('linear-swap-api/v1/swap_cross_tpsl_order', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_tpsl_cancel = contractPrivatePostLinearSwapApiV1SwapTpslCancel = Entry('linear-swap-api/v1/swap_tpsl_cancel', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_tpsl_cancel = contractPrivatePostLinearSwapApiV1SwapCrossTpslCancel = Entry('linear-swap-api/v1/swap_cross_tpsl_cancel', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_tpsl_cancelall = contractPrivatePostLinearSwapApiV1SwapTpslCancelall = Entry('linear-swap-api/v1/swap_tpsl_cancelall', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_tpsl_cancelall = contractPrivatePostLinearSwapApiV1SwapCrossTpslCancelall = Entry('linear-swap-api/v1/swap_cross_tpsl_cancelall', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_tpsl_openorders = contractPrivatePostLinearSwapApiV1SwapTpslOpenorders = Entry('linear-swap-api/v1/swap_tpsl_openorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_tpsl_openorders = contractPrivatePostLinearSwapApiV1SwapCrossTpslOpenorders = Entry('linear-swap-api/v1/swap_cross_tpsl_openorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_tpsl_hisorders = contractPrivatePostLinearSwapApiV1SwapTpslHisorders = Entry('linear-swap-api/v1/swap_tpsl_hisorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_tpsl_hisorders = contractPrivatePostLinearSwapApiV1SwapCrossTpslHisorders = Entry('linear-swap-api/v1/swap_cross_tpsl_hisorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_relation_tpsl_order = contractPrivatePostLinearSwapApiV1SwapRelationTpslOrder = Entry('linear-swap-api/v1/swap_relation_tpsl_order', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_relation_tpsl_order = contractPrivatePostLinearSwapApiV1SwapCrossRelationTpslOrder = Entry('linear-swap-api/v1/swap_cross_relation_tpsl_order', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_track_order = contractPrivatePostLinearSwapApiV1SwapTrackOrder = Entry('linear-swap-api/v1/swap_track_order', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_track_order = contractPrivatePostLinearSwapApiV1SwapCrossTrackOrder = Entry('linear-swap-api/v1/swap_cross_track_order', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_track_cancel = contractPrivatePostLinearSwapApiV1SwapTrackCancel = Entry('linear-swap-api/v1/swap_track_cancel', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_track_cancel = contractPrivatePostLinearSwapApiV1SwapCrossTrackCancel = Entry('linear-swap-api/v1/swap_cross_track_cancel', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_track_cancelall = contractPrivatePostLinearSwapApiV1SwapTrackCancelall = Entry('linear-swap-api/v1/swap_track_cancelall', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_track_cancelall = contractPrivatePostLinearSwapApiV1SwapCrossTrackCancelall = Entry('linear-swap-api/v1/swap_cross_track_cancelall', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_track_openorders = contractPrivatePostLinearSwapApiV1SwapTrackOpenorders = Entry('linear-swap-api/v1/swap_track_openorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_track_openorders = contractPrivatePostLinearSwapApiV1SwapCrossTrackOpenorders = Entry('linear-swap-api/v1/swap_cross_track_openorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_track_hisorders = contractPrivatePostLinearSwapApiV1SwapTrackHisorders = Entry('linear-swap-api/v1/swap_track_hisorders', ['contract', 'private'], 'POST', {'cost': 1}) + contract_private_post_linear_swap_api_v1_swap_cross_track_hisorders = contractPrivatePostLinearSwapApiV1SwapCrossTrackHisorders = Entry('linear-swap-api/v1/swap_cross_track_hisorders', ['contract', 'private'], 'POST', {'cost': 1}) diff --git a/ccxt/abstract/hyperliquid.py b/ccxt/abstract/hyperliquid.py new file mode 100644 index 0000000..943f5e9 --- /dev/null +++ b/ccxt/abstract/hyperliquid.py @@ -0,0 +1,6 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_post_info = publicPostInfo = Entry('info', 'public', 'POST', {'cost': 20, 'byType': {'l2Book': 2, 'allMids': 2, 'clearinghouseState': 2, 'orderStatus': 2, 'spotClearinghouseState': 2, 'exchangeStatus': 2, 'candleSnapshot': 4}}) + private_post_exchange = privatePostExchange = Entry('exchange', 'private', 'POST', {'cost': 1}) diff --git a/ccxt/abstract/independentreserve.py b/ccxt/abstract/independentreserve.py new file mode 100644 index 0000000..f0eb8c3 --- /dev/null +++ b/ccxt/abstract/independentreserve.py @@ -0,0 +1,43 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_getvalidprimarycurrencycodes = publicGetGetValidPrimaryCurrencyCodes = Entry('GetValidPrimaryCurrencyCodes', 'public', 'GET', {}) + public_get_getvalidsecondarycurrencycodes = publicGetGetValidSecondaryCurrencyCodes = Entry('GetValidSecondaryCurrencyCodes', 'public', 'GET', {}) + public_get_getvalidlimitordertypes = publicGetGetValidLimitOrderTypes = Entry('GetValidLimitOrderTypes', 'public', 'GET', {}) + public_get_getvalidmarketordertypes = publicGetGetValidMarketOrderTypes = Entry('GetValidMarketOrderTypes', 'public', 'GET', {}) + public_get_getvalidordertypes = publicGetGetValidOrderTypes = Entry('GetValidOrderTypes', 'public', 'GET', {}) + public_get_getvalidtransactiontypes = publicGetGetValidTransactionTypes = Entry('GetValidTransactionTypes', 'public', 'GET', {}) + public_get_getmarketsummary = publicGetGetMarketSummary = Entry('GetMarketSummary', 'public', 'GET', {}) + public_get_getorderbook = publicGetGetOrderBook = Entry('GetOrderBook', 'public', 'GET', {}) + public_get_getallorders = publicGetGetAllOrders = Entry('GetAllOrders', 'public', 'GET', {}) + public_get_gettradehistorysummary = publicGetGetTradeHistorySummary = Entry('GetTradeHistorySummary', 'public', 'GET', {}) + public_get_getrecenttrades = publicGetGetRecentTrades = Entry('GetRecentTrades', 'public', 'GET', {}) + public_get_getfxrates = publicGetGetFxRates = Entry('GetFxRates', 'public', 'GET', {}) + public_get_getorderminimumvolumes = publicGetGetOrderMinimumVolumes = Entry('GetOrderMinimumVolumes', 'public', 'GET', {}) + public_get_getcryptowithdrawalfees = publicGetGetCryptoWithdrawalFees = Entry('GetCryptoWithdrawalFees', 'public', 'GET', {}) + public_get_getcryptowithdrawalfees2 = publicGetGetCryptoWithdrawalFees2 = Entry('GetCryptoWithdrawalFees2', 'public', 'GET', {}) + public_get_getnetworks = publicGetGetNetworks = Entry('GetNetworks', 'public', 'GET', {}) + public_get_getprimarycurrencyconfig2 = publicGetGetPrimaryCurrencyConfig2 = Entry('GetPrimaryCurrencyConfig2', 'public', 'GET', {}) + private_post_getopenorders = privatePostGetOpenOrders = Entry('GetOpenOrders', 'private', 'POST', {}) + private_post_getclosedorders = privatePostGetClosedOrders = Entry('GetClosedOrders', 'private', 'POST', {}) + private_post_getclosedfilledorders = privatePostGetClosedFilledOrders = Entry('GetClosedFilledOrders', 'private', 'POST', {}) + private_post_getorderdetails = privatePostGetOrderDetails = Entry('GetOrderDetails', 'private', 'POST', {}) + private_post_getaccounts = privatePostGetAccounts = Entry('GetAccounts', 'private', 'POST', {}) + private_post_gettransactions = privatePostGetTransactions = Entry('GetTransactions', 'private', 'POST', {}) + private_post_getfiatbankaccounts = privatePostGetFiatBankAccounts = Entry('GetFiatBankAccounts', 'private', 'POST', {}) + private_post_getdigitalcurrencydepositaddress = privatePostGetDigitalCurrencyDepositAddress = Entry('GetDigitalCurrencyDepositAddress', 'private', 'POST', {}) + private_post_getdigitalcurrencydepositaddress2 = privatePostGetDigitalCurrencyDepositAddress2 = Entry('GetDigitalCurrencyDepositAddress2', 'private', 'POST', {}) + private_post_getdigitalcurrencydepositaddresses = privatePostGetDigitalCurrencyDepositAddresses = Entry('GetDigitalCurrencyDepositAddresses', 'private', 'POST', {}) + private_post_getdigitalcurrencydepositaddresses2 = privatePostGetDigitalCurrencyDepositAddresses2 = Entry('GetDigitalCurrencyDepositAddresses2', 'private', 'POST', {}) + private_post_gettrades = privatePostGetTrades = Entry('GetTrades', 'private', 'POST', {}) + private_post_getbrokeragefees = privatePostGetBrokerageFees = Entry('GetBrokerageFees', 'private', 'POST', {}) + private_post_getdigitalcurrencywithdrawal = privatePostGetDigitalCurrencyWithdrawal = Entry('GetDigitalCurrencyWithdrawal', 'private', 'POST', {}) + private_post_placelimitorder = privatePostPlaceLimitOrder = Entry('PlaceLimitOrder', 'private', 'POST', {}) + private_post_placemarketorder = privatePostPlaceMarketOrder = Entry('PlaceMarketOrder', 'private', 'POST', {}) + private_post_cancelorder = privatePostCancelOrder = Entry('CancelOrder', 'private', 'POST', {}) + private_post_synchdigitalcurrencydepositaddresswithblockchain = privatePostSynchDigitalCurrencyDepositAddressWithBlockchain = Entry('SynchDigitalCurrencyDepositAddressWithBlockchain', 'private', 'POST', {}) + private_post_requestfiatwithdrawal = privatePostRequestFiatWithdrawal = Entry('RequestFiatWithdrawal', 'private', 'POST', {}) + private_post_withdrawfiatcurrency = privatePostWithdrawFiatCurrency = Entry('WithdrawFiatCurrency', 'private', 'POST', {}) + private_post_withdrawdigitalcurrency = privatePostWithdrawDigitalCurrency = Entry('WithdrawDigitalCurrency', 'private', 'POST', {}) + private_post_withdrawcrypto = privatePostWithdrawCrypto = Entry('WithdrawCrypto', 'private', 'POST', {}) diff --git a/ccxt/abstract/indodax.py b/ccxt/abstract/indodax.py new file mode 100644 index 0000000..8cbb7dd --- /dev/null +++ b/ccxt/abstract/indodax.py @@ -0,0 +1,26 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_api_server_time = publicGetApiServerTime = Entry('api/server_time', 'public', 'GET', {'cost': 5}) + public_get_api_pairs = publicGetApiPairs = Entry('api/pairs', 'public', 'GET', {'cost': 5}) + public_get_api_price_increments = publicGetApiPriceIncrements = Entry('api/price_increments', 'public', 'GET', {'cost': 5}) + public_get_api_summaries = publicGetApiSummaries = Entry('api/summaries', 'public', 'GET', {'cost': 5}) + public_get_api_ticker_pair = publicGetApiTickerPair = Entry('api/ticker/{pair}', 'public', 'GET', {'cost': 5}) + public_get_api_ticker_all = publicGetApiTickerAll = Entry('api/ticker_all', 'public', 'GET', {'cost': 5}) + public_get_api_trades_pair = publicGetApiTradesPair = Entry('api/trades/{pair}', 'public', 'GET', {'cost': 5}) + public_get_api_depth_pair = publicGetApiDepthPair = Entry('api/depth/{pair}', 'public', 'GET', {'cost': 5}) + public_get_tradingview_history_v2 = publicGetTradingviewHistoryV2 = Entry('tradingview/history_v2', 'public', 'GET', {'cost': 5}) + private_post_getinfo = privatePostGetInfo = Entry('getInfo', 'private', 'POST', {'cost': 4}) + private_post_transhistory = privatePostTransHistory = Entry('transHistory', 'private', 'POST', {'cost': 4}) + private_post_trade = privatePostTrade = Entry('trade', 'private', 'POST', {'cost': 1}) + private_post_tradehistory = privatePostTradeHistory = Entry('tradeHistory', 'private', 'POST', {'cost': 4}) + private_post_openorders = privatePostOpenOrders = Entry('openOrders', 'private', 'POST', {'cost': 4}) + private_post_orderhistory = privatePostOrderHistory = Entry('orderHistory', 'private', 'POST', {'cost': 4}) + private_post_getorder = privatePostGetOrder = Entry('getOrder', 'private', 'POST', {'cost': 4}) + private_post_cancelorder = privatePostCancelOrder = Entry('cancelOrder', 'private', 'POST', {'cost': 4}) + private_post_withdrawfee = privatePostWithdrawFee = Entry('withdrawFee', 'private', 'POST', {'cost': 4}) + private_post_withdrawcoin = privatePostWithdrawCoin = Entry('withdrawCoin', 'private', 'POST', {'cost': 4}) + private_post_listdownline = privatePostListDownline = Entry('listDownline', 'private', 'POST', {'cost': 4}) + private_post_checkdownline = privatePostCheckDownline = Entry('checkDownline', 'private', 'POST', {'cost': 4}) + private_post_createvoucher = privatePostCreateVoucher = Entry('createVoucher', 'private', 'POST', {'cost': 4}) diff --git a/ccxt/abstract/kraken.py b/ccxt/abstract/kraken.py new file mode 100644 index 0000000..8dcc79f --- /dev/null +++ b/ccxt/abstract/kraken.py @@ -0,0 +1,58 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + zendesk_get_360000292886 = zendeskGet360000292886 = Entry('360000292886', 'zendesk', 'GET', {}) + zendesk_get_201893608 = zendeskGet201893608 = Entry('201893608', 'zendesk', 'GET', {}) + public_get_assets = publicGetAssets = Entry('Assets', 'public', 'GET', {'cost': 1}) + public_get_assetpairs = publicGetAssetPairs = Entry('AssetPairs', 'public', 'GET', {'cost': 1}) + public_get_depth = publicGetDepth = Entry('Depth', 'public', 'GET', {'cost': 1.2}) + public_get_ohlc = publicGetOHLC = Entry('OHLC', 'public', 'GET', {'cost': 1.2}) + public_get_spread = publicGetSpread = Entry('Spread', 'public', 'GET', {'cost': 1}) + public_get_systemstatus = publicGetSystemStatus = Entry('SystemStatus', 'public', 'GET', {'cost': 1}) + public_get_ticker = publicGetTicker = Entry('Ticker', 'public', 'GET', {'cost': 1}) + public_get_time = publicGetTime = Entry('Time', 'public', 'GET', {'cost': 1}) + public_get_trades = publicGetTrades = Entry('Trades', 'public', 'GET', {'cost': 1.2}) + private_post_addorder = privatePostAddOrder = Entry('AddOrder', 'private', 'POST', {'cost': 0}) + private_post_addorderbatch = privatePostAddOrderBatch = Entry('AddOrderBatch', 'private', 'POST', {'cost': 0}) + private_post_addexport = privatePostAddExport = Entry('AddExport', 'private', 'POST', {'cost': 3}) + private_post_amendorder = privatePostAmendOrder = Entry('AmendOrder', 'private', 'POST', {'cost': 0}) + private_post_balance = privatePostBalance = Entry('Balance', 'private', 'POST', {'cost': 3}) + private_post_cancelall = privatePostCancelAll = Entry('CancelAll', 'private', 'POST', {'cost': 3}) + private_post_cancelallordersafter = privatePostCancelAllOrdersAfter = Entry('CancelAllOrdersAfter', 'private', 'POST', {'cost': 3}) + private_post_cancelorder = privatePostCancelOrder = Entry('CancelOrder', 'private', 'POST', {'cost': 0}) + private_post_cancelorderbatch = privatePostCancelOrderBatch = Entry('CancelOrderBatch', 'private', 'POST', {'cost': 0}) + private_post_closedorders = privatePostClosedOrders = Entry('ClosedOrders', 'private', 'POST', {'cost': 3}) + private_post_depositaddresses = privatePostDepositAddresses = Entry('DepositAddresses', 'private', 'POST', {'cost': 3}) + private_post_depositmethods = privatePostDepositMethods = Entry('DepositMethods', 'private', 'POST', {'cost': 3}) + private_post_depositstatus = privatePostDepositStatus = Entry('DepositStatus', 'private', 'POST', {'cost': 3}) + private_post_editorder = privatePostEditOrder = Entry('EditOrder', 'private', 'POST', {'cost': 0}) + private_post_exportstatus = privatePostExportStatus = Entry('ExportStatus', 'private', 'POST', {'cost': 3}) + private_post_getwebsocketstoken = privatePostGetWebSocketsToken = Entry('GetWebSocketsToken', 'private', 'POST', {'cost': 3}) + private_post_ledgers = privatePostLedgers = Entry('Ledgers', 'private', 'POST', {'cost': 6}) + private_post_openorders = privatePostOpenOrders = Entry('OpenOrders', 'private', 'POST', {'cost': 3}) + private_post_openpositions = privatePostOpenPositions = Entry('OpenPositions', 'private', 'POST', {'cost': 3}) + private_post_queryledgers = privatePostQueryLedgers = Entry('QueryLedgers', 'private', 'POST', {'cost': 3}) + private_post_queryorders = privatePostQueryOrders = Entry('QueryOrders', 'private', 'POST', {'cost': 3}) + private_post_querytrades = privatePostQueryTrades = Entry('QueryTrades', 'private', 'POST', {'cost': 3}) + private_post_retrieveexport = privatePostRetrieveExport = Entry('RetrieveExport', 'private', 'POST', {'cost': 3}) + private_post_removeexport = privatePostRemoveExport = Entry('RemoveExport', 'private', 'POST', {'cost': 3}) + private_post_balanceex = privatePostBalanceEx = Entry('BalanceEx', 'private', 'POST', {'cost': 3}) + private_post_tradebalance = privatePostTradeBalance = Entry('TradeBalance', 'private', 'POST', {'cost': 3}) + private_post_tradeshistory = privatePostTradesHistory = Entry('TradesHistory', 'private', 'POST', {'cost': 6}) + private_post_tradevolume = privatePostTradeVolume = Entry('TradeVolume', 'private', 'POST', {'cost': 3}) + private_post_withdraw = privatePostWithdraw = Entry('Withdraw', 'private', 'POST', {'cost': 3}) + private_post_withdrawcancel = privatePostWithdrawCancel = Entry('WithdrawCancel', 'private', 'POST', {'cost': 3}) + private_post_withdrawinfo = privatePostWithdrawInfo = Entry('WithdrawInfo', 'private', 'POST', {'cost': 3}) + private_post_withdrawmethods = privatePostWithdrawMethods = Entry('WithdrawMethods', 'private', 'POST', {'cost': 3}) + private_post_withdrawaddresses = privatePostWithdrawAddresses = Entry('WithdrawAddresses', 'private', 'POST', {'cost': 3}) + private_post_withdrawstatus = privatePostWithdrawStatus = Entry('WithdrawStatus', 'private', 'POST', {'cost': 3}) + private_post_wallettransfer = privatePostWalletTransfer = Entry('WalletTransfer', 'private', 'POST', {'cost': 3}) + private_post_createsubaccount = privatePostCreateSubaccount = Entry('CreateSubaccount', 'private', 'POST', {'cost': 3}) + private_post_accounttransfer = privatePostAccountTransfer = Entry('AccountTransfer', 'private', 'POST', {'cost': 3}) + private_post_earn_allocate = privatePostEarnAllocate = Entry('Earn/Allocate', 'private', 'POST', {'cost': 3}) + private_post_earn_deallocate = privatePostEarnDeallocate = Entry('Earn/Deallocate', 'private', 'POST', {'cost': 3}) + private_post_earn_allocatestatus = privatePostEarnAllocateStatus = Entry('Earn/AllocateStatus', 'private', 'POST', {'cost': 3}) + private_post_earn_deallocatestatus = privatePostEarnDeallocateStatus = Entry('Earn/DeallocateStatus', 'private', 'POST', {'cost': 3}) + private_post_earn_strategies = privatePostEarnStrategies = Entry('Earn/Strategies', 'private', 'POST', {'cost': 3}) + private_post_earn_allocations = privatePostEarnAllocations = Entry('Earn/Allocations', 'private', 'POST', {'cost': 3}) diff --git a/ccxt/abstract/krakenfutures.py b/ccxt/abstract/krakenfutures.py new file mode 100644 index 0000000..74f7835 --- /dev/null +++ b/ccxt/abstract/krakenfutures.py @@ -0,0 +1,42 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_feeschedules = publicGetFeeschedules = Entry('feeschedules', 'public', 'GET', {}) + public_get_instruments = publicGetInstruments = Entry('instruments', 'public', 'GET', {}) + public_get_orderbook = publicGetOrderbook = Entry('orderbook', 'public', 'GET', {}) + public_get_tickers = publicGetTickers = Entry('tickers', 'public', 'GET', {}) + public_get_history = publicGetHistory = Entry('history', 'public', 'GET', {}) + public_get_historicalfundingrates = publicGetHistoricalfundingrates = Entry('historicalfundingrates', 'public', 'GET', {}) + private_get_feeschedules_volumes = privateGetFeeschedulesVolumes = Entry('feeschedules/volumes', 'private', 'GET', {}) + private_get_openpositions = privateGetOpenpositions = Entry('openpositions', 'private', 'GET', {}) + private_get_notifications = privateGetNotifications = Entry('notifications', 'private', 'GET', {}) + private_get_accounts = privateGetAccounts = Entry('accounts', 'private', 'GET', {}) + private_get_openorders = privateGetOpenorders = Entry('openorders', 'private', 'GET', {}) + private_get_recentorders = privateGetRecentorders = Entry('recentorders', 'private', 'GET', {}) + private_get_fills = privateGetFills = Entry('fills', 'private', 'GET', {}) + private_get_transfers = privateGetTransfers = Entry('transfers', 'private', 'GET', {}) + private_get_leveragepreferences = privateGetLeveragepreferences = Entry('leveragepreferences', 'private', 'GET', {}) + private_get_pnlpreferences = privateGetPnlpreferences = Entry('pnlpreferences', 'private', 'GET', {}) + private_get_assignmentprogram_current = privateGetAssignmentprogramCurrent = Entry('assignmentprogram/current', 'private', 'GET', {}) + private_get_assignmentprogram_history = privateGetAssignmentprogramHistory = Entry('assignmentprogram/history', 'private', 'GET', {}) + private_post_sendorder = privatePostSendorder = Entry('sendorder', 'private', 'POST', {}) + private_post_editorder = privatePostEditorder = Entry('editorder', 'private', 'POST', {}) + private_post_cancelorder = privatePostCancelorder = Entry('cancelorder', 'private', 'POST', {}) + private_post_transfer = privatePostTransfer = Entry('transfer', 'private', 'POST', {}) + private_post_batchorder = privatePostBatchorder = Entry('batchorder', 'private', 'POST', {}) + private_post_cancelallorders = privatePostCancelallorders = Entry('cancelallorders', 'private', 'POST', {}) + private_post_cancelallordersafter = privatePostCancelallordersafter = Entry('cancelallordersafter', 'private', 'POST', {}) + private_post_withdrawal = privatePostWithdrawal = Entry('withdrawal', 'private', 'POST', {}) + private_post_assignmentprogram_add = privatePostAssignmentprogramAdd = Entry('assignmentprogram/add', 'private', 'POST', {}) + private_post_assignmentprogram_delete = privatePostAssignmentprogramDelete = Entry('assignmentprogram/delete', 'private', 'POST', {}) + private_put_leveragepreferences = privatePutLeveragepreferences = Entry('leveragepreferences', 'private', 'PUT', {}) + private_put_pnlpreferences = privatePutPnlpreferences = Entry('pnlpreferences', 'private', 'PUT', {}) + charts_get_price_type_symbol_interval = chartsGetPriceTypeSymbolInterval = Entry('{price_type}/{symbol}/{interval}', 'charts', 'GET', {}) + history_get_orders = historyGetOrders = Entry('orders', 'history', 'GET', {}) + history_get_executions = historyGetExecutions = Entry('executions', 'history', 'GET', {}) + history_get_triggers = historyGetTriggers = Entry('triggers', 'history', 'GET', {}) + history_get_accountlogcsv = historyGetAccountlogcsv = Entry('accountlogcsv', 'history', 'GET', {}) + history_get_account_log = historyGetAccountLog = Entry('account-log', 'history', 'GET', {}) + history_get_market_symbol_orders = historyGetMarketSymbolOrders = Entry('market/{symbol}/orders', 'history', 'GET', {}) + history_get_market_symbol_executions = historyGetMarketSymbolExecutions = Entry('market/{symbol}/executions', 'history', 'GET', {}) diff --git a/ccxt/abstract/kucoin.py b/ccxt/abstract/kucoin.py new file mode 100644 index 0000000..6f93d51 --- /dev/null +++ b/ccxt/abstract/kucoin.py @@ -0,0 +1,254 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_currencies = publicGetCurrencies = Entry('currencies', 'public', 'GET', {'cost': 4.5}) + public_get_currencies_currency = publicGetCurrenciesCurrency = Entry('currencies/{currency}', 'public', 'GET', {'cost': 4.5}) + public_get_symbols = publicGetSymbols = Entry('symbols', 'public', 'GET', {'cost': 6}) + public_get_market_orderbook_level1 = publicGetMarketOrderbookLevel1 = Entry('market/orderbook/level1', 'public', 'GET', {'cost': 3}) + public_get_market_alltickers = publicGetMarketAllTickers = Entry('market/allTickers', 'public', 'GET', {'cost': 22.5}) + public_get_market_stats = publicGetMarketStats = Entry('market/stats', 'public', 'GET', {'cost': 22.5}) + public_get_markets = publicGetMarkets = Entry('markets', 'public', 'GET', {'cost': 4.5}) + public_get_market_orderbook_level_level_limit = publicGetMarketOrderbookLevelLevelLimit = Entry('market/orderbook/level{level}_{limit}', 'public', 'GET', {'cost': 6}) + public_get_market_orderbook_level2_20 = publicGetMarketOrderbookLevel220 = Entry('market/orderbook/level2_20', 'public', 'GET', {'cost': 3}) + public_get_market_orderbook_level2_100 = publicGetMarketOrderbookLevel2100 = Entry('market/orderbook/level2_100', 'public', 'GET', {'cost': 6}) + public_get_market_histories = publicGetMarketHistories = Entry('market/histories', 'public', 'GET', {'cost': 4.5}) + public_get_market_candles = publicGetMarketCandles = Entry('market/candles', 'public', 'GET', {'cost': 4.5}) + public_get_prices = publicGetPrices = Entry('prices', 'public', 'GET', {'cost': 4.5}) + public_get_timestamp = publicGetTimestamp = Entry('timestamp', 'public', 'GET', {'cost': 4.5}) + public_get_status = publicGetStatus = Entry('status', 'public', 'GET', {'cost': 4.5}) + public_get_mark_price_symbol_current = publicGetMarkPriceSymbolCurrent = Entry('mark-price/{symbol}/current', 'public', 'GET', {'cost': 3}) + public_get_mark_price_all_symbols = publicGetMarkPriceAllSymbols = Entry('mark-price/all-symbols', 'public', 'GET', {'cost': 3}) + public_get_margin_config = publicGetMarginConfig = Entry('margin/config', 'public', 'GET', {'cost': 25}) + public_get_announcements = publicGetAnnouncements = Entry('announcements', 'public', 'GET', {'cost': 20}) + public_get_margin_collateralratio = publicGetMarginCollateralRatio = Entry('margin/collateralRatio', 'public', 'GET', {'cost': 10}) + public_post_bullet_public = publicPostBulletPublic = Entry('bullet-public', 'public', 'POST', {'cost': 15}) + private_get_user_info = privateGetUserInfo = Entry('user-info', 'private', 'GET', {'cost': 30}) + private_get_accounts = privateGetAccounts = Entry('accounts', 'private', 'GET', {'cost': 7.5}) + private_get_accounts_accountid = privateGetAccountsAccountId = Entry('accounts/{accountId}', 'private', 'GET', {'cost': 7.5}) + private_get_accounts_ledgers = privateGetAccountsLedgers = Entry('accounts/ledgers', 'private', 'GET', {'cost': 3}) + private_get_hf_accounts_ledgers = privateGetHfAccountsLedgers = Entry('hf/accounts/ledgers', 'private', 'GET', {'cost': 2}) + private_get_hf_margin_account_ledgers = privateGetHfMarginAccountLedgers = Entry('hf/margin/account/ledgers', 'private', 'GET', {'cost': 2}) + private_get_transaction_history = privateGetTransactionHistory = Entry('transaction-history', 'private', 'GET', {'cost': 3}) + private_get_sub_user = privateGetSubUser = Entry('sub/user', 'private', 'GET', {'cost': 30}) + private_get_sub_accounts_subuserid = privateGetSubAccountsSubUserId = Entry('sub-accounts/{subUserId}', 'private', 'GET', {'cost': 22.5}) + private_get_sub_accounts = privateGetSubAccounts = Entry('sub-accounts', 'private', 'GET', {'cost': 30}) + private_get_sub_api_key = privateGetSubApiKey = Entry('sub/api-key', 'private', 'GET', {'cost': 30}) + private_get_margin_account = privateGetMarginAccount = Entry('margin/account', 'private', 'GET', {'cost': 40}) + private_get_margin_accounts = privateGetMarginAccounts = Entry('margin/accounts', 'private', 'GET', {'cost': 15}) + private_get_isolated_accounts = privateGetIsolatedAccounts = Entry('isolated/accounts', 'private', 'GET', {'cost': 15}) + private_get_deposit_addresses = privateGetDepositAddresses = Entry('deposit-addresses', 'private', 'GET', {'cost': 7.5}) + private_get_deposits = privateGetDeposits = Entry('deposits', 'private', 'GET', {'cost': 7.5}) + private_get_hist_deposits = privateGetHistDeposits = Entry('hist-deposits', 'private', 'GET', {'cost': 7.5}) + private_get_withdrawals = privateGetWithdrawals = Entry('withdrawals', 'private', 'GET', {'cost': 30}) + private_get_hist_withdrawals = privateGetHistWithdrawals = Entry('hist-withdrawals', 'private', 'GET', {'cost': 30}) + private_get_withdrawals_quotas = privateGetWithdrawalsQuotas = Entry('withdrawals/quotas', 'private', 'GET', {'cost': 30}) + private_get_accounts_transferable = privateGetAccountsTransferable = Entry('accounts/transferable', 'private', 'GET', {'cost': 30}) + private_get_transfer_list = privateGetTransferList = Entry('transfer-list', 'private', 'GET', {'cost': 30}) + private_get_base_fee = privateGetBaseFee = Entry('base-fee', 'private', 'GET', {'cost': 3}) + private_get_trade_fees = privateGetTradeFees = Entry('trade-fees', 'private', 'GET', {'cost': 3}) + private_get_market_orderbook_level_level = privateGetMarketOrderbookLevelLevel = Entry('market/orderbook/level{level}', 'private', 'GET', {'cost': 3}) + private_get_market_orderbook_level2 = privateGetMarketOrderbookLevel2 = Entry('market/orderbook/level2', 'private', 'GET', {'cost': 3}) + private_get_market_orderbook_level3 = privateGetMarketOrderbookLevel3 = Entry('market/orderbook/level3', 'private', 'GET', {'cost': 3}) + private_get_hf_accounts_opened = privateGetHfAccountsOpened = Entry('hf/accounts/opened', 'private', 'GET', {'cost': 2}) + private_get_hf_orders_active = privateGetHfOrdersActive = Entry('hf/orders/active', 'private', 'GET', {'cost': 2}) + private_get_hf_orders_active_symbols = privateGetHfOrdersActiveSymbols = Entry('hf/orders/active/symbols', 'private', 'GET', {'cost': 2}) + private_get_hf_margin_order_active_symbols = privateGetHfMarginOrderActiveSymbols = Entry('hf/margin/order/active/symbols', 'private', 'GET', {'cost': 2}) + private_get_hf_orders_done = privateGetHfOrdersDone = Entry('hf/orders/done', 'private', 'GET', {'cost': 2}) + private_get_hf_orders_orderid = privateGetHfOrdersOrderId = Entry('hf/orders/{orderId}', 'private', 'GET', {'cost': 2}) + private_get_hf_orders_client_order_clientoid = privateGetHfOrdersClientOrderClientOid = Entry('hf/orders/client-order/{clientOid}', 'private', 'GET', {'cost': 2}) + private_get_hf_orders_dead_cancel_all_query = privateGetHfOrdersDeadCancelAllQuery = Entry('hf/orders/dead-cancel-all/query', 'private', 'GET', {'cost': 2}) + private_get_hf_fills = privateGetHfFills = Entry('hf/fills', 'private', 'GET', {'cost': 2}) + private_get_orders = privateGetOrders = Entry('orders', 'private', 'GET', {'cost': 2}) + private_get_limit_orders = privateGetLimitOrders = Entry('limit/orders', 'private', 'GET', {'cost': 3}) + private_get_orders_orderid = privateGetOrdersOrderId = Entry('orders/{orderId}', 'private', 'GET', {'cost': 2}) + private_get_order_client_order_clientoid = privateGetOrderClientOrderClientOid = Entry('order/client-order/{clientOid}', 'private', 'GET', {'cost': 3}) + private_get_fills = privateGetFills = Entry('fills', 'private', 'GET', {'cost': 10}) + private_get_limit_fills = privateGetLimitFills = Entry('limit/fills', 'private', 'GET', {'cost': 20}) + private_get_stop_order = privateGetStopOrder = Entry('stop-order', 'private', 'GET', {'cost': 8}) + private_get_stop_order_orderid = privateGetStopOrderOrderId = Entry('stop-order/{orderId}', 'private', 'GET', {'cost': 3}) + private_get_stop_order_queryorderbyclientoid = privateGetStopOrderQueryOrderByClientOid = Entry('stop-order/queryOrderByClientOid', 'private', 'GET', {'cost': 3}) + private_get_oco_order_orderid = privateGetOcoOrderOrderId = Entry('oco/order/{orderId}', 'private', 'GET', {'cost': 2}) + private_get_oco_order_details_orderid = privateGetOcoOrderDetailsOrderId = Entry('oco/order/details/{orderId}', 'private', 'GET', {'cost': 2}) + private_get_oco_client_order_clientoid = privateGetOcoClientOrderClientOid = Entry('oco/client-order/{clientOid}', 'private', 'GET', {'cost': 2}) + private_get_oco_orders = privateGetOcoOrders = Entry('oco/orders', 'private', 'GET', {'cost': 2}) + private_get_hf_margin_orders_active = privateGetHfMarginOrdersActive = Entry('hf/margin/orders/active', 'private', 'GET', {'cost': 4}) + private_get_hf_margin_orders_done = privateGetHfMarginOrdersDone = Entry('hf/margin/orders/done', 'private', 'GET', {'cost': 10}) + private_get_hf_margin_orders_orderid = privateGetHfMarginOrdersOrderId = Entry('hf/margin/orders/{orderId}', 'private', 'GET', {'cost': 4}) + private_get_hf_margin_orders_client_order_clientoid = privateGetHfMarginOrdersClientOrderClientOid = Entry('hf/margin/orders/client-order/{clientOid}', 'private', 'GET', {'cost': 5}) + private_get_hf_margin_fills = privateGetHfMarginFills = Entry('hf/margin/fills', 'private', 'GET', {'cost': 5}) + private_get_etf_info = privateGetEtfInfo = Entry('etf/info', 'private', 'GET', {'cost': 25}) + private_get_margin_currencies = privateGetMarginCurrencies = Entry('margin/currencies', 'private', 'GET', {'cost': 20}) + private_get_risk_limit_strategy = privateGetRiskLimitStrategy = Entry('risk/limit/strategy', 'private', 'GET', {'cost': 20}) + private_get_isolated_symbols = privateGetIsolatedSymbols = Entry('isolated/symbols', 'private', 'GET', {'cost': 20}) + private_get_margin_symbols = privateGetMarginSymbols = Entry('margin/symbols', 'private', 'GET', {'cost': 5}) + private_get_isolated_account_symbol = privateGetIsolatedAccountSymbol = Entry('isolated/account/{symbol}', 'private', 'GET', {'cost': 50}) + private_get_margin_borrow = privateGetMarginBorrow = Entry('margin/borrow', 'private', 'GET', {'cost': 15}) + private_get_margin_repay = privateGetMarginRepay = Entry('margin/repay', 'private', 'GET', {'cost': 15}) + private_get_margin_interest = privateGetMarginInterest = Entry('margin/interest', 'private', 'GET', {'cost': 20}) + private_get_project_list = privateGetProjectList = Entry('project/list', 'private', 'GET', {'cost': 10}) + private_get_project_marketinterestrate = privateGetProjectMarketInterestRate = Entry('project/marketInterestRate', 'private', 'GET', {'cost': 7.5}) + private_get_redeem_orders = privateGetRedeemOrders = Entry('redeem/orders', 'private', 'GET', {'cost': 10}) + private_get_purchase_orders = privateGetPurchaseOrders = Entry('purchase/orders', 'private', 'GET', {'cost': 10}) + private_get_broker_api_rebase_download = privateGetBrokerApiRebaseDownload = Entry('broker/api/rebase/download', 'private', 'GET', {'cost': 3}) + private_get_broker_querymycommission = privateGetBrokerQueryMyCommission = Entry('broker/queryMyCommission', 'private', 'GET', {'cost': 3}) + private_get_broker_queryuser = privateGetBrokerQueryUser = Entry('broker/queryUser', 'private', 'GET', {'cost': 3}) + private_get_broker_querydetailbyuid = privateGetBrokerQueryDetailByUid = Entry('broker/queryDetailByUid', 'private', 'GET', {'cost': 3}) + private_get_migrate_user_account_status = privateGetMigrateUserAccountStatus = Entry('migrate/user/account/status', 'private', 'GET', {'cost': 3}) + private_get_affiliate_inviter_statistics = privateGetAffiliateInviterStatistics = Entry('affiliate/inviter/statistics', 'private', 'GET', {'cost': 30}) + private_post_sub_user_created = privatePostSubUserCreated = Entry('sub/user/created', 'private', 'POST', {'cost': 22.5}) + private_post_sub_api_key = privatePostSubApiKey = Entry('sub/api-key', 'private', 'POST', {'cost': 30}) + private_post_sub_api_key_update = privatePostSubApiKeyUpdate = Entry('sub/api-key/update', 'private', 'POST', {'cost': 45}) + private_post_deposit_addresses = privatePostDepositAddresses = Entry('deposit-addresses', 'private', 'POST', {'cost': 30}) + private_post_withdrawals = privatePostWithdrawals = Entry('withdrawals', 'private', 'POST', {'cost': 7.5}) + private_post_accounts_universal_transfer = privatePostAccountsUniversalTransfer = Entry('accounts/universal-transfer', 'private', 'POST', {'cost': 6}) + private_post_accounts_sub_transfer = privatePostAccountsSubTransfer = Entry('accounts/sub-transfer', 'private', 'POST', {'cost': 45}) + private_post_accounts_inner_transfer = privatePostAccountsInnerTransfer = Entry('accounts/inner-transfer', 'private', 'POST', {'cost': 15}) + private_post_transfer_out = privatePostTransferOut = Entry('transfer-out', 'private', 'POST', {'cost': 30}) + private_post_transfer_in = privatePostTransferIn = Entry('transfer-in', 'private', 'POST', {'cost': 30}) + private_post_hf_orders = privatePostHfOrders = Entry('hf/orders', 'private', 'POST', {'cost': 1}) + private_post_hf_orders_test = privatePostHfOrdersTest = Entry('hf/orders/test', 'private', 'POST', {'cost': 1}) + private_post_hf_orders_sync = privatePostHfOrdersSync = Entry('hf/orders/sync', 'private', 'POST', {'cost': 1}) + private_post_hf_orders_multi = privatePostHfOrdersMulti = Entry('hf/orders/multi', 'private', 'POST', {'cost': 1}) + private_post_hf_orders_multi_sync = privatePostHfOrdersMultiSync = Entry('hf/orders/multi/sync', 'private', 'POST', {'cost': 1}) + private_post_hf_orders_alter = privatePostHfOrdersAlter = Entry('hf/orders/alter', 'private', 'POST', {'cost': 3}) + private_post_hf_orders_dead_cancel_all = privatePostHfOrdersDeadCancelAll = Entry('hf/orders/dead-cancel-all', 'private', 'POST', {'cost': 2}) + private_post_orders = privatePostOrders = Entry('orders', 'private', 'POST', {'cost': 2}) + private_post_orders_test = privatePostOrdersTest = Entry('orders/test', 'private', 'POST', {'cost': 2}) + private_post_orders_multi = privatePostOrdersMulti = Entry('orders/multi', 'private', 'POST', {'cost': 3}) + private_post_stop_order = privatePostStopOrder = Entry('stop-order', 'private', 'POST', {'cost': 2}) + private_post_oco_order = privatePostOcoOrder = Entry('oco/order', 'private', 'POST', {'cost': 2}) + private_post_hf_margin_order = privatePostHfMarginOrder = Entry('hf/margin/order', 'private', 'POST', {'cost': 5}) + private_post_hf_margin_order_test = privatePostHfMarginOrderTest = Entry('hf/margin/order/test', 'private', 'POST', {'cost': 5}) + private_post_margin_order = privatePostMarginOrder = Entry('margin/order', 'private', 'POST', {'cost': 5}) + private_post_margin_order_test = privatePostMarginOrderTest = Entry('margin/order/test', 'private', 'POST', {'cost': 5}) + private_post_margin_borrow = privatePostMarginBorrow = Entry('margin/borrow', 'private', 'POST', {'cost': 15}) + private_post_margin_repay = privatePostMarginRepay = Entry('margin/repay', 'private', 'POST', {'cost': 10}) + private_post_purchase = privatePostPurchase = Entry('purchase', 'private', 'POST', {'cost': 15}) + private_post_redeem = privatePostRedeem = Entry('redeem', 'private', 'POST', {'cost': 15}) + private_post_lend_purchase_update = privatePostLendPurchaseUpdate = Entry('lend/purchase/update', 'private', 'POST', {'cost': 10}) + private_post_bullet_private = privatePostBulletPrivate = Entry('bullet-private', 'private', 'POST', {'cost': 10}) + private_post_position_update_user_leverage = privatePostPositionUpdateUserLeverage = Entry('position/update-user-leverage', 'private', 'POST', {'cost': 5}) + private_post_deposit_address_create = privatePostDepositAddressCreate = Entry('deposit-address/create', 'private', 'POST', {'cost': 20}) + private_delete_sub_api_key = privateDeleteSubApiKey = Entry('sub/api-key', 'private', 'DELETE', {'cost': 45}) + private_delete_withdrawals_withdrawalid = privateDeleteWithdrawalsWithdrawalId = Entry('withdrawals/{withdrawalId}', 'private', 'DELETE', {'cost': 30}) + private_delete_hf_orders_orderid = privateDeleteHfOrdersOrderId = Entry('hf/orders/{orderId}', 'private', 'DELETE', {'cost': 1}) + private_delete_hf_orders_sync_orderid = privateDeleteHfOrdersSyncOrderId = Entry('hf/orders/sync/{orderId}', 'private', 'DELETE', {'cost': 1}) + private_delete_hf_orders_client_order_clientoid = privateDeleteHfOrdersClientOrderClientOid = Entry('hf/orders/client-order/{clientOid}', 'private', 'DELETE', {'cost': 1}) + private_delete_hf_orders_sync_client_order_clientoid = privateDeleteHfOrdersSyncClientOrderClientOid = Entry('hf/orders/sync/client-order/{clientOid}', 'private', 'DELETE', {'cost': 1}) + private_delete_hf_orders_cancel_orderid = privateDeleteHfOrdersCancelOrderId = Entry('hf/orders/cancel/{orderId}', 'private', 'DELETE', {'cost': 2}) + private_delete_hf_orders = privateDeleteHfOrders = Entry('hf/orders', 'private', 'DELETE', {'cost': 2}) + private_delete_hf_orders_cancelall = privateDeleteHfOrdersCancelAll = Entry('hf/orders/cancelAll', 'private', 'DELETE', {'cost': 30}) + private_delete_orders_orderid = privateDeleteOrdersOrderId = Entry('orders/{orderId}', 'private', 'DELETE', {'cost': 3}) + private_delete_order_client_order_clientoid = privateDeleteOrderClientOrderClientOid = Entry('order/client-order/{clientOid}', 'private', 'DELETE', {'cost': 5}) + private_delete_orders = privateDeleteOrders = Entry('orders', 'private', 'DELETE', {'cost': 20}) + private_delete_stop_order_orderid = privateDeleteStopOrderOrderId = Entry('stop-order/{orderId}', 'private', 'DELETE', {'cost': 3}) + private_delete_stop_order_cancelorderbyclientoid = privateDeleteStopOrderCancelOrderByClientOid = Entry('stop-order/cancelOrderByClientOid', 'private', 'DELETE', {'cost': 5}) + private_delete_stop_order_cancel = privateDeleteStopOrderCancel = Entry('stop-order/cancel', 'private', 'DELETE', {'cost': 3}) + private_delete_oco_order_orderid = privateDeleteOcoOrderOrderId = Entry('oco/order/{orderId}', 'private', 'DELETE', {'cost': 3}) + private_delete_oco_client_order_clientoid = privateDeleteOcoClientOrderClientOid = Entry('oco/client-order/{clientOid}', 'private', 'DELETE', {'cost': 3}) + private_delete_oco_orders = privateDeleteOcoOrders = Entry('oco/orders', 'private', 'DELETE', {'cost': 3}) + private_delete_hf_margin_orders_orderid = privateDeleteHfMarginOrdersOrderId = Entry('hf/margin/orders/{orderId}', 'private', 'DELETE', {'cost': 5}) + private_delete_hf_margin_orders_client_order_clientoid = privateDeleteHfMarginOrdersClientOrderClientOid = Entry('hf/margin/orders/client-order/{clientOid}', 'private', 'DELETE', {'cost': 5}) + private_delete_hf_margin_orders = privateDeleteHfMarginOrders = Entry('hf/margin/orders', 'private', 'DELETE', {'cost': 10}) + futurespublic_get_contracts_active = futuresPublicGetContractsActive = Entry('contracts/active', 'futuresPublic', 'GET', {'cost': 4.5}) + futurespublic_get_contracts_symbol = futuresPublicGetContractsSymbol = Entry('contracts/{symbol}', 'futuresPublic', 'GET', {'cost': 4.5}) + futurespublic_get_ticker = futuresPublicGetTicker = Entry('ticker', 'futuresPublic', 'GET', {'cost': 3}) + futurespublic_get_level2_snapshot = futuresPublicGetLevel2Snapshot = Entry('level2/snapshot', 'futuresPublic', 'GET', {'cost': 4.5}) + futurespublic_get_level2_depth20 = futuresPublicGetLevel2Depth20 = Entry('level2/depth20', 'futuresPublic', 'GET', {'cost': 7.5}) + futurespublic_get_level2_depth100 = futuresPublicGetLevel2Depth100 = Entry('level2/depth100', 'futuresPublic', 'GET', {'cost': 15}) + futurespublic_get_trade_history = futuresPublicGetTradeHistory = Entry('trade/history', 'futuresPublic', 'GET', {'cost': 7.5}) + futurespublic_get_kline_query = futuresPublicGetKlineQuery = Entry('kline/query', 'futuresPublic', 'GET', {'cost': 4.5}) + futurespublic_get_interest_query = futuresPublicGetInterestQuery = Entry('interest/query', 'futuresPublic', 'GET', {'cost': 7.5}) + futurespublic_get_index_query = futuresPublicGetIndexQuery = Entry('index/query', 'futuresPublic', 'GET', {'cost': 3}) + futurespublic_get_mark_price_symbol_current = futuresPublicGetMarkPriceSymbolCurrent = Entry('mark-price/{symbol}/current', 'futuresPublic', 'GET', {'cost': 4.5}) + futurespublic_get_premium_query = futuresPublicGetPremiumQuery = Entry('premium/query', 'futuresPublic', 'GET', {'cost': 4.5}) + futurespublic_get_trade_statistics = futuresPublicGetTradeStatistics = Entry('trade-statistics', 'futuresPublic', 'GET', {'cost': 4.5}) + futurespublic_get_funding_rate_symbol_current = futuresPublicGetFundingRateSymbolCurrent = Entry('funding-rate/{symbol}/current', 'futuresPublic', 'GET', {'cost': 3}) + futurespublic_get_contract_funding_rates = futuresPublicGetContractFundingRates = Entry('contract/funding-rates', 'futuresPublic', 'GET', {'cost': 7.5}) + futurespublic_get_timestamp = futuresPublicGetTimestamp = Entry('timestamp', 'futuresPublic', 'GET', {'cost': 3}) + futurespublic_get_status = futuresPublicGetStatus = Entry('status', 'futuresPublic', 'GET', {'cost': 6}) + futurespublic_get_level2_message_query = futuresPublicGetLevel2MessageQuery = Entry('level2/message/query', 'futuresPublic', 'GET', {'cost': 1.3953}) + futurespublic_post_bullet_public = futuresPublicPostBulletPublic = Entry('bullet-public', 'futuresPublic', 'POST', {'cost': 15}) + futuresprivate_get_transaction_history = futuresPrivateGetTransactionHistory = Entry('transaction-history', 'futuresPrivate', 'GET', {'cost': 3}) + futuresprivate_get_account_overview = futuresPrivateGetAccountOverview = Entry('account-overview', 'futuresPrivate', 'GET', {'cost': 7.5}) + futuresprivate_get_account_overview_all = futuresPrivateGetAccountOverviewAll = Entry('account-overview-all', 'futuresPrivate', 'GET', {'cost': 9}) + futuresprivate_get_transfer_list = futuresPrivateGetTransferList = Entry('transfer-list', 'futuresPrivate', 'GET', {'cost': 30}) + futuresprivate_get_orders = futuresPrivateGetOrders = Entry('orders', 'futuresPrivate', 'GET', {'cost': 3}) + futuresprivate_get_stoporders = futuresPrivateGetStopOrders = Entry('stopOrders', 'futuresPrivate', 'GET', {'cost': 9}) + futuresprivate_get_recentdoneorders = futuresPrivateGetRecentDoneOrders = Entry('recentDoneOrders', 'futuresPrivate', 'GET', {'cost': 7.5}) + futuresprivate_get_orders_orderid = futuresPrivateGetOrdersOrderId = Entry('orders/{orderId}', 'futuresPrivate', 'GET', {'cost': 7.5}) + futuresprivate_get_orders_byclientoid = futuresPrivateGetOrdersByClientOid = Entry('orders/byClientOid', 'futuresPrivate', 'GET', {'cost': 7.5}) + futuresprivate_get_fills = futuresPrivateGetFills = Entry('fills', 'futuresPrivate', 'GET', {'cost': 7.5}) + futuresprivate_get_recentfills = futuresPrivateGetRecentFills = Entry('recentFills', 'futuresPrivate', 'GET', {'cost': 4.5}) + futuresprivate_get_openorderstatistics = futuresPrivateGetOpenOrderStatistics = Entry('openOrderStatistics', 'futuresPrivate', 'GET', {'cost': 15}) + futuresprivate_get_position = futuresPrivateGetPosition = Entry('position', 'futuresPrivate', 'GET', {'cost': 3}) + futuresprivate_get_positions = futuresPrivateGetPositions = Entry('positions', 'futuresPrivate', 'GET', {'cost': 3}) + futuresprivate_get_margin_maxwithdrawmargin = futuresPrivateGetMarginMaxWithdrawMargin = Entry('margin/maxWithdrawMargin', 'futuresPrivate', 'GET', {'cost': 15}) + futuresprivate_get_contracts_risk_limit_symbol = futuresPrivateGetContractsRiskLimitSymbol = Entry('contracts/risk-limit/{symbol}', 'futuresPrivate', 'GET', {'cost': 7.5}) + futuresprivate_get_funding_history = futuresPrivateGetFundingHistory = Entry('funding-history', 'futuresPrivate', 'GET', {'cost': 7.5}) + futuresprivate_get_copy_trade_futures_get_max_open_size = futuresPrivateGetCopyTradeFuturesGetMaxOpenSize = Entry('copy-trade/futures/get-max-open-size', 'futuresPrivate', 'GET', {'cost': 6}) + futuresprivate_get_copy_trade_futures_position_margin_max_withdraw_margin = futuresPrivateGetCopyTradeFuturesPositionMarginMaxWithdrawMargin = Entry('copy-trade/futures/position/margin/max-withdraw-margin', 'futuresPrivate', 'GET', {'cost': 15}) + futuresprivate_post_transfer_out = futuresPrivatePostTransferOut = Entry('transfer-out', 'futuresPrivate', 'POST', {'cost': 30}) + futuresprivate_post_transfer_in = futuresPrivatePostTransferIn = Entry('transfer-in', 'futuresPrivate', 'POST', {'cost': 30}) + futuresprivate_post_orders = futuresPrivatePostOrders = Entry('orders', 'futuresPrivate', 'POST', {'cost': 3}) + futuresprivate_post_orders_test = futuresPrivatePostOrdersTest = Entry('orders/test', 'futuresPrivate', 'POST', {'cost': 3}) + futuresprivate_post_orders_multi = futuresPrivatePostOrdersMulti = Entry('orders/multi', 'futuresPrivate', 'POST', {'cost': 4.5}) + futuresprivate_post_position_margin_auto_deposit_status = futuresPrivatePostPositionMarginAutoDepositStatus = Entry('position/margin/auto-deposit-status', 'futuresPrivate', 'POST', {'cost': 6}) + futuresprivate_post_margin_withdrawmargin = futuresPrivatePostMarginWithdrawMargin = Entry('margin/withdrawMargin', 'futuresPrivate', 'POST', {'cost': 15}) + futuresprivate_post_position_margin_deposit_margin = futuresPrivatePostPositionMarginDepositMargin = Entry('position/margin/deposit-margin', 'futuresPrivate', 'POST', {'cost': 6}) + futuresprivate_post_position_risk_limit_level_change = futuresPrivatePostPositionRiskLimitLevelChange = Entry('position/risk-limit-level/change', 'futuresPrivate', 'POST', {'cost': 6}) + futuresprivate_post_copy_trade_futures_orders = futuresPrivatePostCopyTradeFuturesOrders = Entry('copy-trade/futures/orders', 'futuresPrivate', 'POST', {'cost': 3}) + futuresprivate_post_copy_trade_futures_orders_test = futuresPrivatePostCopyTradeFuturesOrdersTest = Entry('copy-trade/futures/orders/test', 'futuresPrivate', 'POST', {'cost': 3}) + futuresprivate_post_copy_trade_futures_st_orders = futuresPrivatePostCopyTradeFuturesStOrders = Entry('copy-trade/futures/st-orders', 'futuresPrivate', 'POST', {'cost': 3}) + futuresprivate_post_copy_trade_futures_position_margin_deposit_margin = futuresPrivatePostCopyTradeFuturesPositionMarginDepositMargin = Entry('copy-trade/futures/position/margin/deposit-margin', 'futuresPrivate', 'POST', {'cost': 6}) + futuresprivate_post_copy_trade_futures_position_margin_withdraw_margin = futuresPrivatePostCopyTradeFuturesPositionMarginWithdrawMargin = Entry('copy-trade/futures/position/margin/withdraw-margin', 'futuresPrivate', 'POST', {'cost': 15}) + futuresprivate_post_copy_trade_futures_position_risk_limit_level_change = futuresPrivatePostCopyTradeFuturesPositionRiskLimitLevelChange = Entry('copy-trade/futures/position/risk-limit-level/change', 'futuresPrivate', 'POST', {'cost': 3}) + futuresprivate_post_copy_trade_futures_position_margin_auto_deposit_status = futuresPrivatePostCopyTradeFuturesPositionMarginAutoDepositStatus = Entry('copy-trade/futures/position/margin/auto-deposit-status', 'futuresPrivate', 'POST', {'cost': 6}) + futuresprivate_post_copy_trade_futures_position_changemarginmode = futuresPrivatePostCopyTradeFuturesPositionChangeMarginMode = Entry('copy-trade/futures/position/changeMarginMode', 'futuresPrivate', 'POST', {'cost': 3}) + futuresprivate_post_copy_trade_futures_position_changecrossuserleverage = futuresPrivatePostCopyTradeFuturesPositionChangeCrossUserLeverage = Entry('copy-trade/futures/position/changeCrossUserLeverage', 'futuresPrivate', 'POST', {'cost': 3}) + futuresprivate_post_copy_trade_getcrossmodemarginrequirement = futuresPrivatePostCopyTradeGetCrossModeMarginRequirement = Entry('copy-trade/getCrossModeMarginRequirement', 'futuresPrivate', 'POST', {'cost': 4.5}) + futuresprivate_post_copy_trade_position_switchpositionmode = futuresPrivatePostCopyTradePositionSwitchPositionMode = Entry('copy-trade/position/switchPositionMode', 'futuresPrivate', 'POST', {'cost': 3}) + futuresprivate_post_bullet_private = futuresPrivatePostBulletPrivate = Entry('bullet-private', 'futuresPrivate', 'POST', {'cost': 15}) + futuresprivate_delete_orders_orderid = futuresPrivateDeleteOrdersOrderId = Entry('orders/{orderId}', 'futuresPrivate', 'DELETE', {'cost': 1.5}) + futuresprivate_delete_orders_client_order_clientoid = futuresPrivateDeleteOrdersClientOrderClientOid = Entry('orders/client-order/{clientOid}', 'futuresPrivate', 'DELETE', {'cost': 1.5}) + futuresprivate_delete_orders = futuresPrivateDeleteOrders = Entry('orders', 'futuresPrivate', 'DELETE', {'cost': 45}) + futuresprivate_delete_stoporders = futuresPrivateDeleteStopOrders = Entry('stopOrders', 'futuresPrivate', 'DELETE', {'cost': 22.5}) + futuresprivate_delete_copy_trade_futures_orders = futuresPrivateDeleteCopyTradeFuturesOrders = Entry('copy-trade/futures/orders', 'futuresPrivate', 'DELETE', {'cost': 1.5}) + futuresprivate_delete_copy_trade_futures_orders_client_order = futuresPrivateDeleteCopyTradeFuturesOrdersClientOrder = Entry('copy-trade/futures/orders/client-order', 'futuresPrivate', 'DELETE', {'cost': 1.5}) + webexchange_get_currency_currency_chain_info = webExchangeGetCurrencyCurrencyChainInfo = Entry('currency/currency/chain-info', 'webExchange', 'GET', {'cost': 1}) + broker_get_broker_nd_info = brokerGetBrokerNdInfo = Entry('broker/nd/info', 'broker', 'GET', {'cost': 2}) + broker_get_broker_nd_account = brokerGetBrokerNdAccount = Entry('broker/nd/account', 'broker', 'GET', {'cost': 2}) + broker_get_broker_nd_account_apikey = brokerGetBrokerNdAccountApikey = Entry('broker/nd/account/apikey', 'broker', 'GET', {'cost': 2}) + broker_get_broker_nd_rebase_download = brokerGetBrokerNdRebaseDownload = Entry('broker/nd/rebase/download', 'broker', 'GET', {'cost': 3}) + broker_get_asset_ndbroker_deposit_list = brokerGetAssetNdbrokerDepositList = Entry('asset/ndbroker/deposit/list', 'broker', 'GET', {'cost': 1}) + broker_get_broker_nd_transfer_detail = brokerGetBrokerNdTransferDetail = Entry('broker/nd/transfer/detail', 'broker', 'GET', {'cost': 1}) + broker_get_broker_nd_deposit_detail = brokerGetBrokerNdDepositDetail = Entry('broker/nd/deposit/detail', 'broker', 'GET', {'cost': 1}) + broker_get_broker_nd_withdraw_detail = brokerGetBrokerNdWithdrawDetail = Entry('broker/nd/withdraw/detail', 'broker', 'GET', {'cost': 1}) + broker_post_broker_nd_transfer = brokerPostBrokerNdTransfer = Entry('broker/nd/transfer', 'broker', 'POST', {'cost': 1}) + broker_post_broker_nd_account = brokerPostBrokerNdAccount = Entry('broker/nd/account', 'broker', 'POST', {'cost': 3}) + broker_post_broker_nd_account_apikey = brokerPostBrokerNdAccountApikey = Entry('broker/nd/account/apikey', 'broker', 'POST', {'cost': 3}) + broker_post_broker_nd_account_update_apikey = brokerPostBrokerNdAccountUpdateApikey = Entry('broker/nd/account/update-apikey', 'broker', 'POST', {'cost': 3}) + broker_delete_broker_nd_account_apikey = brokerDeleteBrokerNdAccountApikey = Entry('broker/nd/account/apikey', 'broker', 'DELETE', {'cost': 3}) + earn_get_otc_loan_loan = earnGetOtcLoanLoan = Entry('otc-loan/loan', 'earn', 'GET', {'cost': 1}) + earn_get_otc_loan_accounts = earnGetOtcLoanAccounts = Entry('otc-loan/accounts', 'earn', 'GET', {'cost': 1}) + earn_get_earn_redeem_preview = earnGetEarnRedeemPreview = Entry('earn/redeem-preview', 'earn', 'GET', {'cost': 7.5}) + earn_get_earn_saving_products = earnGetEarnSavingProducts = Entry('earn/saving/products', 'earn', 'GET', {'cost': 7.5}) + earn_get_earn_hold_assets = earnGetEarnHoldAssets = Entry('earn/hold-assets', 'earn', 'GET', {'cost': 7.5}) + earn_get_earn_promotion_products = earnGetEarnPromotionProducts = Entry('earn/promotion/products', 'earn', 'GET', {'cost': 7.5}) + earn_get_earn_kcs_staking_products = earnGetEarnKcsStakingProducts = Entry('earn/kcs-staking/products', 'earn', 'GET', {'cost': 7.5}) + earn_get_earn_staking_products = earnGetEarnStakingProducts = Entry('earn/staking/products', 'earn', 'GET', {'cost': 7.5}) + earn_get_earn_eth_staking_products = earnGetEarnEthStakingProducts = Entry('earn/eth-staking/products', 'earn', 'GET', {'cost': 7.5}) + earn_post_earn_orders = earnPostEarnOrders = Entry('earn/orders', 'earn', 'POST', {'cost': 7.5}) + earn_delete_earn_orders = earnDeleteEarnOrders = Entry('earn/orders', 'earn', 'DELETE', {'cost': 7.5}) + uta_get_market_announcement = utaGetMarketAnnouncement = Entry('market/announcement', 'uta', 'GET', {'cost': 20}) + uta_get_market_currency = utaGetMarketCurrency = Entry('market/currency', 'uta', 'GET', {'cost': 3}) + uta_get_market_instrument = utaGetMarketInstrument = Entry('market/instrument', 'uta', 'GET', {'cost': 4}) + uta_get_market_ticker = utaGetMarketTicker = Entry('market/ticker', 'uta', 'GET', {'cost': 15}) + uta_get_market_orderbook = utaGetMarketOrderbook = Entry('market/orderbook', 'uta', 'GET', {'cost': 3}) + uta_get_market_trade = utaGetMarketTrade = Entry('market/trade', 'uta', 'GET', {'cost': 3}) + uta_get_market_kline = utaGetMarketKline = Entry('market/kline', 'uta', 'GET', {'cost': 3}) + uta_get_market_funding_rate = utaGetMarketFundingRate = Entry('market/funding-rate', 'uta', 'GET', {'cost': 2}) + uta_get_market_funding_rate_history = utaGetMarketFundingRateHistory = Entry('market/funding-rate-history', 'uta', 'GET', {'cost': 5}) + uta_get_market_cross_config = utaGetMarketCrossConfig = Entry('market/cross-config', 'uta', 'GET', {'cost': 25}) + uta_get_server_status = utaGetServerStatus = Entry('server/status', 'uta', 'GET', {'cost': 3}) diff --git a/ccxt/abstract/kucoinfutures.py b/ccxt/abstract/kucoinfutures.py new file mode 100644 index 0000000..fd58dba --- /dev/null +++ b/ccxt/abstract/kucoinfutures.py @@ -0,0 +1,282 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_currencies = publicGetCurrencies = Entry('currencies', 'public', 'GET', {'cost': 4.5}) + public_get_currencies_currency = publicGetCurrenciesCurrency = Entry('currencies/{currency}', 'public', 'GET', {'cost': 4.5}) + public_get_symbols = publicGetSymbols = Entry('symbols', 'public', 'GET', {'cost': 6}) + public_get_market_orderbook_level1 = publicGetMarketOrderbookLevel1 = Entry('market/orderbook/level1', 'public', 'GET', {'cost': 3}) + public_get_market_alltickers = publicGetMarketAllTickers = Entry('market/allTickers', 'public', 'GET', {'cost': 22.5}) + public_get_market_stats = publicGetMarketStats = Entry('market/stats', 'public', 'GET', {'cost': 22.5}) + public_get_markets = publicGetMarkets = Entry('markets', 'public', 'GET', {'cost': 4.5}) + public_get_market_orderbook_level_level_limit = publicGetMarketOrderbookLevelLevelLimit = Entry('market/orderbook/level{level}_{limit}', 'public', 'GET', {'cost': 6}) + public_get_market_orderbook_level2_20 = publicGetMarketOrderbookLevel220 = Entry('market/orderbook/level2_20', 'public', 'GET', {'cost': 3}) + public_get_market_orderbook_level2_100 = publicGetMarketOrderbookLevel2100 = Entry('market/orderbook/level2_100', 'public', 'GET', {'cost': 6}) + public_get_market_histories = publicGetMarketHistories = Entry('market/histories', 'public', 'GET', {'cost': 4.5}) + public_get_market_candles = publicGetMarketCandles = Entry('market/candles', 'public', 'GET', {'cost': 4.5}) + public_get_prices = publicGetPrices = Entry('prices', 'public', 'GET', {'cost': 4.5}) + public_get_timestamp = publicGetTimestamp = Entry('timestamp', 'public', 'GET', {'cost': 4.5}) + public_get_status = publicGetStatus = Entry('status', 'public', 'GET', {'cost': 4.5}) + public_get_mark_price_symbol_current = publicGetMarkPriceSymbolCurrent = Entry('mark-price/{symbol}/current', 'public', 'GET', {'cost': 3}) + public_get_mark_price_all_symbols = publicGetMarkPriceAllSymbols = Entry('mark-price/all-symbols', 'public', 'GET', {'cost': 3}) + public_get_margin_config = publicGetMarginConfig = Entry('margin/config', 'public', 'GET', {'cost': 25}) + public_get_announcements = publicGetAnnouncements = Entry('announcements', 'public', 'GET', {'cost': 20}) + public_get_margin_collateralratio = publicGetMarginCollateralRatio = Entry('margin/collateralRatio', 'public', 'GET', {'cost': 10}) + public_post_bullet_public = publicPostBulletPublic = Entry('bullet-public', 'public', 'POST', {'cost': 15}) + private_get_user_info = privateGetUserInfo = Entry('user-info', 'private', 'GET', {'cost': 30}) + private_get_accounts = privateGetAccounts = Entry('accounts', 'private', 'GET', {'cost': 7.5}) + private_get_accounts_accountid = privateGetAccountsAccountId = Entry('accounts/{accountId}', 'private', 'GET', {'cost': 7.5}) + private_get_accounts_ledgers = privateGetAccountsLedgers = Entry('accounts/ledgers', 'private', 'GET', {'cost': 3}) + private_get_hf_accounts_ledgers = privateGetHfAccountsLedgers = Entry('hf/accounts/ledgers', 'private', 'GET', {'cost': 2}) + private_get_hf_margin_account_ledgers = privateGetHfMarginAccountLedgers = Entry('hf/margin/account/ledgers', 'private', 'GET', {'cost': 2}) + private_get_transaction_history = privateGetTransactionHistory = Entry('transaction-history', 'private', 'GET', {'cost': 3}) + private_get_sub_user = privateGetSubUser = Entry('sub/user', 'private', 'GET', {'cost': 30}) + private_get_sub_accounts_subuserid = privateGetSubAccountsSubUserId = Entry('sub-accounts/{subUserId}', 'private', 'GET', {'cost': 22.5}) + private_get_sub_accounts = privateGetSubAccounts = Entry('sub-accounts', 'private', 'GET', {'cost': 30}) + private_get_sub_api_key = privateGetSubApiKey = Entry('sub/api-key', 'private', 'GET', {'cost': 30}) + private_get_margin_account = privateGetMarginAccount = Entry('margin/account', 'private', 'GET', {'cost': 40}) + private_get_margin_accounts = privateGetMarginAccounts = Entry('margin/accounts', 'private', 'GET', {'cost': 15}) + private_get_isolated_accounts = privateGetIsolatedAccounts = Entry('isolated/accounts', 'private', 'GET', {'cost': 15}) + private_get_deposit_addresses = privateGetDepositAddresses = Entry('deposit-addresses', 'private', 'GET', {'cost': 7.5}) + private_get_deposits = privateGetDeposits = Entry('deposits', 'private', 'GET', {'cost': 7.5}) + private_get_hist_deposits = privateGetHistDeposits = Entry('hist-deposits', 'private', 'GET', {'cost': 7.5}) + private_get_withdrawals = privateGetWithdrawals = Entry('withdrawals', 'private', 'GET', {'cost': 30}) + private_get_hist_withdrawals = privateGetHistWithdrawals = Entry('hist-withdrawals', 'private', 'GET', {'cost': 30}) + private_get_withdrawals_quotas = privateGetWithdrawalsQuotas = Entry('withdrawals/quotas', 'private', 'GET', {'cost': 30}) + private_get_accounts_transferable = privateGetAccountsTransferable = Entry('accounts/transferable', 'private', 'GET', {'cost': 30}) + private_get_transfer_list = privateGetTransferList = Entry('transfer-list', 'private', 'GET', {'cost': 30}) + private_get_base_fee = privateGetBaseFee = Entry('base-fee', 'private', 'GET', {'cost': 3}) + private_get_trade_fees = privateGetTradeFees = Entry('trade-fees', 'private', 'GET', {'cost': 3}) + private_get_market_orderbook_level_level = privateGetMarketOrderbookLevelLevel = Entry('market/orderbook/level{level}', 'private', 'GET', {'cost': 3}) + private_get_market_orderbook_level2 = privateGetMarketOrderbookLevel2 = Entry('market/orderbook/level2', 'private', 'GET', {'cost': 3}) + private_get_market_orderbook_level3 = privateGetMarketOrderbookLevel3 = Entry('market/orderbook/level3', 'private', 'GET', {'cost': 3}) + private_get_hf_accounts_opened = privateGetHfAccountsOpened = Entry('hf/accounts/opened', 'private', 'GET', {'cost': 2}) + private_get_hf_orders_active = privateGetHfOrdersActive = Entry('hf/orders/active', 'private', 'GET', {'cost': 2}) + private_get_hf_orders_active_symbols = privateGetHfOrdersActiveSymbols = Entry('hf/orders/active/symbols', 'private', 'GET', {'cost': 2}) + private_get_hf_margin_order_active_symbols = privateGetHfMarginOrderActiveSymbols = Entry('hf/margin/order/active/symbols', 'private', 'GET', {'cost': 2}) + private_get_hf_orders_done = privateGetHfOrdersDone = Entry('hf/orders/done', 'private', 'GET', {'cost': 2}) + private_get_hf_orders_orderid = privateGetHfOrdersOrderId = Entry('hf/orders/{orderId}', 'private', 'GET', {'cost': 2}) + private_get_hf_orders_client_order_clientoid = privateGetHfOrdersClientOrderClientOid = Entry('hf/orders/client-order/{clientOid}', 'private', 'GET', {'cost': 2}) + private_get_hf_orders_dead_cancel_all_query = privateGetHfOrdersDeadCancelAllQuery = Entry('hf/orders/dead-cancel-all/query', 'private', 'GET', {'cost': 2}) + private_get_hf_fills = privateGetHfFills = Entry('hf/fills', 'private', 'GET', {'cost': 2}) + private_get_orders = privateGetOrders = Entry('orders', 'private', 'GET', {'cost': 2}) + private_get_limit_orders = privateGetLimitOrders = Entry('limit/orders', 'private', 'GET', {'cost': 3}) + private_get_orders_orderid = privateGetOrdersOrderId = Entry('orders/{orderId}', 'private', 'GET', {'cost': 2}) + private_get_order_client_order_clientoid = privateGetOrderClientOrderClientOid = Entry('order/client-order/{clientOid}', 'private', 'GET', {'cost': 3}) + private_get_fills = privateGetFills = Entry('fills', 'private', 'GET', {'cost': 10}) + private_get_limit_fills = privateGetLimitFills = Entry('limit/fills', 'private', 'GET', {'cost': 20}) + private_get_stop_order = privateGetStopOrder = Entry('stop-order', 'private', 'GET', {'cost': 8}) + private_get_stop_order_orderid = privateGetStopOrderOrderId = Entry('stop-order/{orderId}', 'private', 'GET', {'cost': 3}) + private_get_stop_order_queryorderbyclientoid = privateGetStopOrderQueryOrderByClientOid = Entry('stop-order/queryOrderByClientOid', 'private', 'GET', {'cost': 3}) + private_get_oco_order_orderid = privateGetOcoOrderOrderId = Entry('oco/order/{orderId}', 'private', 'GET', {'cost': 2}) + private_get_oco_order_details_orderid = privateGetOcoOrderDetailsOrderId = Entry('oco/order/details/{orderId}', 'private', 'GET', {'cost': 2}) + private_get_oco_client_order_clientoid = privateGetOcoClientOrderClientOid = Entry('oco/client-order/{clientOid}', 'private', 'GET', {'cost': 2}) + private_get_oco_orders = privateGetOcoOrders = Entry('oco/orders', 'private', 'GET', {'cost': 2}) + private_get_hf_margin_orders_active = privateGetHfMarginOrdersActive = Entry('hf/margin/orders/active', 'private', 'GET', {'cost': 4}) + private_get_hf_margin_orders_done = privateGetHfMarginOrdersDone = Entry('hf/margin/orders/done', 'private', 'GET', {'cost': 10}) + private_get_hf_margin_orders_orderid = privateGetHfMarginOrdersOrderId = Entry('hf/margin/orders/{orderId}', 'private', 'GET', {'cost': 4}) + private_get_hf_margin_orders_client_order_clientoid = privateGetHfMarginOrdersClientOrderClientOid = Entry('hf/margin/orders/client-order/{clientOid}', 'private', 'GET', {'cost': 5}) + private_get_hf_margin_fills = privateGetHfMarginFills = Entry('hf/margin/fills', 'private', 'GET', {'cost': 5}) + private_get_etf_info = privateGetEtfInfo = Entry('etf/info', 'private', 'GET', {'cost': 25}) + private_get_margin_currencies = privateGetMarginCurrencies = Entry('margin/currencies', 'private', 'GET', {'cost': 20}) + private_get_risk_limit_strategy = privateGetRiskLimitStrategy = Entry('risk/limit/strategy', 'private', 'GET', {'cost': 20}) + private_get_isolated_symbols = privateGetIsolatedSymbols = Entry('isolated/symbols', 'private', 'GET', {'cost': 20}) + private_get_margin_symbols = privateGetMarginSymbols = Entry('margin/symbols', 'private', 'GET', {'cost': 5}) + private_get_isolated_account_symbol = privateGetIsolatedAccountSymbol = Entry('isolated/account/{symbol}', 'private', 'GET', {'cost': 50}) + private_get_margin_borrow = privateGetMarginBorrow = Entry('margin/borrow', 'private', 'GET', {'cost': 15}) + private_get_margin_repay = privateGetMarginRepay = Entry('margin/repay', 'private', 'GET', {'cost': 15}) + private_get_margin_interest = privateGetMarginInterest = Entry('margin/interest', 'private', 'GET', {'cost': 20}) + private_get_project_list = privateGetProjectList = Entry('project/list', 'private', 'GET', {'cost': 10}) + private_get_project_marketinterestrate = privateGetProjectMarketInterestRate = Entry('project/marketInterestRate', 'private', 'GET', {'cost': 7.5}) + private_get_redeem_orders = privateGetRedeemOrders = Entry('redeem/orders', 'private', 'GET', {'cost': 10}) + private_get_purchase_orders = privateGetPurchaseOrders = Entry('purchase/orders', 'private', 'GET', {'cost': 10}) + private_get_broker_api_rebase_download = privateGetBrokerApiRebaseDownload = Entry('broker/api/rebase/download', 'private', 'GET', {'cost': 3}) + private_get_broker_querymycommission = privateGetBrokerQueryMyCommission = Entry('broker/queryMyCommission', 'private', 'GET', {'cost': 3}) + private_get_broker_queryuser = privateGetBrokerQueryUser = Entry('broker/queryUser', 'private', 'GET', {'cost': 3}) + private_get_broker_querydetailbyuid = privateGetBrokerQueryDetailByUid = Entry('broker/queryDetailByUid', 'private', 'GET', {'cost': 3}) + private_get_migrate_user_account_status = privateGetMigrateUserAccountStatus = Entry('migrate/user/account/status', 'private', 'GET', {'cost': 3}) + private_get_affiliate_inviter_statistics = privateGetAffiliateInviterStatistics = Entry('affiliate/inviter/statistics', 'private', 'GET', {'cost': 30}) + private_post_sub_user_created = privatePostSubUserCreated = Entry('sub/user/created', 'private', 'POST', {'cost': 22.5}) + private_post_sub_api_key = privatePostSubApiKey = Entry('sub/api-key', 'private', 'POST', {'cost': 30}) + private_post_sub_api_key_update = privatePostSubApiKeyUpdate = Entry('sub/api-key/update', 'private', 'POST', {'cost': 45}) + private_post_deposit_addresses = privatePostDepositAddresses = Entry('deposit-addresses', 'private', 'POST', {'cost': 30}) + private_post_withdrawals = privatePostWithdrawals = Entry('withdrawals', 'private', 'POST', {'cost': 7.5}) + private_post_accounts_universal_transfer = privatePostAccountsUniversalTransfer = Entry('accounts/universal-transfer', 'private', 'POST', {'cost': 6}) + private_post_accounts_sub_transfer = privatePostAccountsSubTransfer = Entry('accounts/sub-transfer', 'private', 'POST', {'cost': 45}) + private_post_accounts_inner_transfer = privatePostAccountsInnerTransfer = Entry('accounts/inner-transfer', 'private', 'POST', {'cost': 15}) + private_post_transfer_out = privatePostTransferOut = Entry('transfer-out', 'private', 'POST', {'cost': 30}) + private_post_transfer_in = privatePostTransferIn = Entry('transfer-in', 'private', 'POST', {'cost': 30}) + private_post_hf_orders = privatePostHfOrders = Entry('hf/orders', 'private', 'POST', {'cost': 1}) + private_post_hf_orders_test = privatePostHfOrdersTest = Entry('hf/orders/test', 'private', 'POST', {'cost': 1}) + private_post_hf_orders_sync = privatePostHfOrdersSync = Entry('hf/orders/sync', 'private', 'POST', {'cost': 1}) + private_post_hf_orders_multi = privatePostHfOrdersMulti = Entry('hf/orders/multi', 'private', 'POST', {'cost': 1}) + private_post_hf_orders_multi_sync = privatePostHfOrdersMultiSync = Entry('hf/orders/multi/sync', 'private', 'POST', {'cost': 1}) + private_post_hf_orders_alter = privatePostHfOrdersAlter = Entry('hf/orders/alter', 'private', 'POST', {'cost': 3}) + private_post_hf_orders_dead_cancel_all = privatePostHfOrdersDeadCancelAll = Entry('hf/orders/dead-cancel-all', 'private', 'POST', {'cost': 2}) + private_post_orders = privatePostOrders = Entry('orders', 'private', 'POST', {'cost': 2}) + private_post_orders_test = privatePostOrdersTest = Entry('orders/test', 'private', 'POST', {'cost': 2}) + private_post_orders_multi = privatePostOrdersMulti = Entry('orders/multi', 'private', 'POST', {'cost': 3}) + private_post_stop_order = privatePostStopOrder = Entry('stop-order', 'private', 'POST', {'cost': 2}) + private_post_oco_order = privatePostOcoOrder = Entry('oco/order', 'private', 'POST', {'cost': 2}) + private_post_hf_margin_order = privatePostHfMarginOrder = Entry('hf/margin/order', 'private', 'POST', {'cost': 5}) + private_post_hf_margin_order_test = privatePostHfMarginOrderTest = Entry('hf/margin/order/test', 'private', 'POST', {'cost': 5}) + private_post_margin_order = privatePostMarginOrder = Entry('margin/order', 'private', 'POST', {'cost': 5}) + private_post_margin_order_test = privatePostMarginOrderTest = Entry('margin/order/test', 'private', 'POST', {'cost': 5}) + private_post_margin_borrow = privatePostMarginBorrow = Entry('margin/borrow', 'private', 'POST', {'cost': 15}) + private_post_margin_repay = privatePostMarginRepay = Entry('margin/repay', 'private', 'POST', {'cost': 10}) + private_post_purchase = privatePostPurchase = Entry('purchase', 'private', 'POST', {'cost': 15}) + private_post_redeem = privatePostRedeem = Entry('redeem', 'private', 'POST', {'cost': 15}) + private_post_lend_purchase_update = privatePostLendPurchaseUpdate = Entry('lend/purchase/update', 'private', 'POST', {'cost': 10}) + private_post_bullet_private = privatePostBulletPrivate = Entry('bullet-private', 'private', 'POST', {'cost': 10}) + private_post_position_update_user_leverage = privatePostPositionUpdateUserLeverage = Entry('position/update-user-leverage', 'private', 'POST', {'cost': 5}) + private_post_deposit_address_create = privatePostDepositAddressCreate = Entry('deposit-address/create', 'private', 'POST', {'cost': 20}) + private_delete_sub_api_key = privateDeleteSubApiKey = Entry('sub/api-key', 'private', 'DELETE', {'cost': 45}) + private_delete_withdrawals_withdrawalid = privateDeleteWithdrawalsWithdrawalId = Entry('withdrawals/{withdrawalId}', 'private', 'DELETE', {'cost': 30}) + private_delete_hf_orders_orderid = privateDeleteHfOrdersOrderId = Entry('hf/orders/{orderId}', 'private', 'DELETE', {'cost': 1}) + private_delete_hf_orders_sync_orderid = privateDeleteHfOrdersSyncOrderId = Entry('hf/orders/sync/{orderId}', 'private', 'DELETE', {'cost': 1}) + private_delete_hf_orders_client_order_clientoid = privateDeleteHfOrdersClientOrderClientOid = Entry('hf/orders/client-order/{clientOid}', 'private', 'DELETE', {'cost': 1}) + private_delete_hf_orders_sync_client_order_clientoid = privateDeleteHfOrdersSyncClientOrderClientOid = Entry('hf/orders/sync/client-order/{clientOid}', 'private', 'DELETE', {'cost': 1}) + private_delete_hf_orders_cancel_orderid = privateDeleteHfOrdersCancelOrderId = Entry('hf/orders/cancel/{orderId}', 'private', 'DELETE', {'cost': 2}) + private_delete_hf_orders = privateDeleteHfOrders = Entry('hf/orders', 'private', 'DELETE', {'cost': 2}) + private_delete_hf_orders_cancelall = privateDeleteHfOrdersCancelAll = Entry('hf/orders/cancelAll', 'private', 'DELETE', {'cost': 30}) + private_delete_orders_orderid = privateDeleteOrdersOrderId = Entry('orders/{orderId}', 'private', 'DELETE', {'cost': 3}) + private_delete_order_client_order_clientoid = privateDeleteOrderClientOrderClientOid = Entry('order/client-order/{clientOid}', 'private', 'DELETE', {'cost': 5}) + private_delete_orders = privateDeleteOrders = Entry('orders', 'private', 'DELETE', {'cost': 20}) + private_delete_stop_order_orderid = privateDeleteStopOrderOrderId = Entry('stop-order/{orderId}', 'private', 'DELETE', {'cost': 3}) + private_delete_stop_order_cancelorderbyclientoid = privateDeleteStopOrderCancelOrderByClientOid = Entry('stop-order/cancelOrderByClientOid', 'private', 'DELETE', {'cost': 5}) + private_delete_stop_order_cancel = privateDeleteStopOrderCancel = Entry('stop-order/cancel', 'private', 'DELETE', {'cost': 3}) + private_delete_oco_order_orderid = privateDeleteOcoOrderOrderId = Entry('oco/order/{orderId}', 'private', 'DELETE', {'cost': 3}) + private_delete_oco_client_order_clientoid = privateDeleteOcoClientOrderClientOid = Entry('oco/client-order/{clientOid}', 'private', 'DELETE', {'cost': 3}) + private_delete_oco_orders = privateDeleteOcoOrders = Entry('oco/orders', 'private', 'DELETE', {'cost': 3}) + private_delete_hf_margin_orders_orderid = privateDeleteHfMarginOrdersOrderId = Entry('hf/margin/orders/{orderId}', 'private', 'DELETE', {'cost': 5}) + private_delete_hf_margin_orders_client_order_clientoid = privateDeleteHfMarginOrdersClientOrderClientOid = Entry('hf/margin/orders/client-order/{clientOid}', 'private', 'DELETE', {'cost': 5}) + private_delete_hf_margin_orders = privateDeleteHfMarginOrders = Entry('hf/margin/orders', 'private', 'DELETE', {'cost': 10}) + futurespublic_get_contracts_active = futuresPublicGetContractsActive = Entry('contracts/active', 'futuresPublic', 'GET', {'cost': 1}) + futurespublic_get_contracts_symbol = futuresPublicGetContractsSymbol = Entry('contracts/{symbol}', 'futuresPublic', 'GET', {'cost': 1}) + futurespublic_get_ticker = futuresPublicGetTicker = Entry('ticker', 'futuresPublic', 'GET', {'cost': 1}) + futurespublic_get_level2_snapshot = futuresPublicGetLevel2Snapshot = Entry('level2/snapshot', 'futuresPublic', 'GET', {'cost': 1.33}) + futurespublic_get_level2_depth20 = futuresPublicGetLevel2Depth20 = Entry('level2/depth20', 'futuresPublic', 'GET', {'cost': 7.5}) + futurespublic_get_level2_depth100 = futuresPublicGetLevel2Depth100 = Entry('level2/depth100', 'futuresPublic', 'GET', {'cost': 15}) + futurespublic_get_trade_history = futuresPublicGetTradeHistory = Entry('trade/history', 'futuresPublic', 'GET', {'cost': 1}) + futurespublic_get_kline_query = futuresPublicGetKlineQuery = Entry('kline/query', 'futuresPublic', 'GET', {'cost': 1}) + futurespublic_get_interest_query = futuresPublicGetInterestQuery = Entry('interest/query', 'futuresPublic', 'GET', {'cost': 1}) + futurespublic_get_index_query = futuresPublicGetIndexQuery = Entry('index/query', 'futuresPublic', 'GET', {'cost': 1}) + futurespublic_get_mark_price_symbol_current = futuresPublicGetMarkPriceSymbolCurrent = Entry('mark-price/{symbol}/current', 'futuresPublic', 'GET', {'cost': 1}) + futurespublic_get_premium_query = futuresPublicGetPremiumQuery = Entry('premium/query', 'futuresPublic', 'GET', {'cost': 1}) + futurespublic_get_trade_statistics = futuresPublicGetTradeStatistics = Entry('trade-statistics', 'futuresPublic', 'GET', {'cost': 4.5}) + futurespublic_get_funding_rate_symbol_current = futuresPublicGetFundingRateSymbolCurrent = Entry('funding-rate/{symbol}/current', 'futuresPublic', 'GET', {'cost': 1}) + futurespublic_get_contract_funding_rates = futuresPublicGetContractFundingRates = Entry('contract/funding-rates', 'futuresPublic', 'GET', {'cost': 7.5}) + futurespublic_get_timestamp = futuresPublicGetTimestamp = Entry('timestamp', 'futuresPublic', 'GET', {'cost': 1}) + futurespublic_get_status = futuresPublicGetStatus = Entry('status', 'futuresPublic', 'GET', {'cost': 1}) + futurespublic_get_level2_message_query = futuresPublicGetLevel2MessageQuery = Entry('level2/message/query', 'futuresPublic', 'GET', {'cost': 1}) + futurespublic_get_contracts_risk_limit_symbol = futuresPublicGetContractsRiskLimitSymbol = Entry('contracts/risk-limit/{symbol}', 'futuresPublic', 'GET', {'cost': 1}) + futurespublic_get_alltickers = futuresPublicGetAllTickers = Entry('allTickers', 'futuresPublic', 'GET', {'cost': 1}) + futurespublic_get_level2_depth_limit = futuresPublicGetLevel2DepthLimit = Entry('level2/depth{limit}', 'futuresPublic', 'GET', {'cost': 1}) + futurespublic_get_level3_message_query = futuresPublicGetLevel3MessageQuery = Entry('level3/message/query', 'futuresPublic', 'GET', {'cost': 1}) + futurespublic_get_level3_snapshot = futuresPublicGetLevel3Snapshot = Entry('level3/snapshot', 'futuresPublic', 'GET', {'cost': 1}) + futurespublic_post_bullet_public = futuresPublicPostBulletPublic = Entry('bullet-public', 'futuresPublic', 'POST', {'cost': 1}) + futuresprivate_get_transaction_history = futuresPrivateGetTransactionHistory = Entry('transaction-history', 'futuresPrivate', 'GET', {'cost': 4.44}) + futuresprivate_get_account_overview = futuresPrivateGetAccountOverview = Entry('account-overview', 'futuresPrivate', 'GET', {'cost': 1.33}) + futuresprivate_get_account_overview_all = futuresPrivateGetAccountOverviewAll = Entry('account-overview-all', 'futuresPrivate', 'GET', {'cost': 9}) + futuresprivate_get_transfer_list = futuresPrivateGetTransferList = Entry('transfer-list', 'futuresPrivate', 'GET', {'cost': 1}) + futuresprivate_get_orders = futuresPrivateGetOrders = Entry('orders', 'futuresPrivate', 'GET', {'cost': 1.33}) + futuresprivate_get_stoporders = futuresPrivateGetStopOrders = Entry('stopOrders', 'futuresPrivate', 'GET', {'cost': 1}) + futuresprivate_get_recentdoneorders = futuresPrivateGetRecentDoneOrders = Entry('recentDoneOrders', 'futuresPrivate', 'GET', {'cost': 1}) + futuresprivate_get_orders_orderid = futuresPrivateGetOrdersOrderId = Entry('orders/{orderId}', 'futuresPrivate', 'GET', {'cost': 1}) + futuresprivate_get_orders_byclientoid = futuresPrivateGetOrdersByClientOid = Entry('orders/byClientOid', 'futuresPrivate', 'GET', {'cost': 1}) + futuresprivate_get_fills = futuresPrivateGetFills = Entry('fills', 'futuresPrivate', 'GET', {'cost': 4.44}) + futuresprivate_get_recentfills = futuresPrivateGetRecentFills = Entry('recentFills', 'futuresPrivate', 'GET', {'cost': 4.44}) + futuresprivate_get_openorderstatistics = futuresPrivateGetOpenOrderStatistics = Entry('openOrderStatistics', 'futuresPrivate', 'GET', {'cost': 1}) + futuresprivate_get_position = futuresPrivateGetPosition = Entry('position', 'futuresPrivate', 'GET', {'cost': 1}) + futuresprivate_get_positions = futuresPrivateGetPositions = Entry('positions', 'futuresPrivate', 'GET', {'cost': 4.44}) + futuresprivate_get_margin_maxwithdrawmargin = futuresPrivateGetMarginMaxWithdrawMargin = Entry('margin/maxWithdrawMargin', 'futuresPrivate', 'GET', {'cost': 15}) + futuresprivate_get_contracts_risk_limit_symbol = futuresPrivateGetContractsRiskLimitSymbol = Entry('contracts/risk-limit/{symbol}', 'futuresPrivate', 'GET', {'cost': 7.5}) + futuresprivate_get_funding_history = futuresPrivateGetFundingHistory = Entry('funding-history', 'futuresPrivate', 'GET', {'cost': 4.44}) + futuresprivate_get_copy_trade_futures_get_max_open_size = futuresPrivateGetCopyTradeFuturesGetMaxOpenSize = Entry('copy-trade/futures/get-max-open-size', 'futuresPrivate', 'GET', {'cost': 6}) + futuresprivate_get_copy_trade_futures_position_margin_max_withdraw_margin = futuresPrivateGetCopyTradeFuturesPositionMarginMaxWithdrawMargin = Entry('copy-trade/futures/position/margin/max-withdraw-margin', 'futuresPrivate', 'GET', {'cost': 15}) + futuresprivate_get_deposit_address = futuresPrivateGetDepositAddress = Entry('deposit-address', 'futuresPrivate', 'GET', {'cost': 1}) + futuresprivate_get_deposit_list = futuresPrivateGetDepositList = Entry('deposit-list', 'futuresPrivate', 'GET', {'cost': 1}) + futuresprivate_get_withdrawals_quotas = futuresPrivateGetWithdrawalsQuotas = Entry('withdrawals/quotas', 'futuresPrivate', 'GET', {'cost': 1}) + futuresprivate_get_withdrawal_list = futuresPrivateGetWithdrawalList = Entry('withdrawal-list', 'futuresPrivate', 'GET', {'cost': 1}) + futuresprivate_get_sub_api_key = futuresPrivateGetSubApiKey = Entry('sub/api-key', 'futuresPrivate', 'GET', {'cost': 1}) + futuresprivate_get_trade_statistics = futuresPrivateGetTradeStatistics = Entry('trade-statistics', 'futuresPrivate', 'GET', {'cost': 1}) + futuresprivate_get_trade_fees = futuresPrivateGetTradeFees = Entry('trade-fees', 'futuresPrivate', 'GET', {'cost': 1}) + futuresprivate_get_history_positions = futuresPrivateGetHistoryPositions = Entry('history-positions', 'futuresPrivate', 'GET', {'cost': 1}) + futuresprivate_get_getmaxopensize = futuresPrivateGetGetMaxOpenSize = Entry('getMaxOpenSize', 'futuresPrivate', 'GET', {'cost': 1}) + futuresprivate_get_getcrossuserleverage = futuresPrivateGetGetCrossUserLeverage = Entry('getCrossUserLeverage', 'futuresPrivate', 'GET', {'cost': 1}) + futuresprivate_get_position_getmarginmode = futuresPrivateGetPositionGetMarginMode = Entry('position/getMarginMode', 'futuresPrivate', 'GET', {'cost': 1}) + futuresprivate_post_transfer_out = futuresPrivatePostTransferOut = Entry('transfer-out', 'futuresPrivate', 'POST', {'cost': 1}) + futuresprivate_post_transfer_in = futuresPrivatePostTransferIn = Entry('transfer-in', 'futuresPrivate', 'POST', {'cost': 1}) + futuresprivate_post_orders = futuresPrivatePostOrders = Entry('orders', 'futuresPrivate', 'POST', {'cost': 1.33}) + futuresprivate_post_orders_test = futuresPrivatePostOrdersTest = Entry('orders/test', 'futuresPrivate', 'POST', {'cost': 1.33}) + futuresprivate_post_orders_multi = futuresPrivatePostOrdersMulti = Entry('orders/multi', 'futuresPrivate', 'POST', {'cost': 4.5}) + futuresprivate_post_position_margin_auto_deposit_status = futuresPrivatePostPositionMarginAutoDepositStatus = Entry('position/margin/auto-deposit-status', 'futuresPrivate', 'POST', {'cost': 1}) + futuresprivate_post_margin_withdrawmargin = futuresPrivatePostMarginWithdrawMargin = Entry('margin/withdrawMargin', 'futuresPrivate', 'POST', {'cost': 15}) + futuresprivate_post_position_margin_deposit_margin = futuresPrivatePostPositionMarginDepositMargin = Entry('position/margin/deposit-margin', 'futuresPrivate', 'POST', {'cost': 1}) + futuresprivate_post_position_risk_limit_level_change = futuresPrivatePostPositionRiskLimitLevelChange = Entry('position/risk-limit-level/change', 'futuresPrivate', 'POST', {'cost': 1}) + futuresprivate_post_copy_trade_futures_orders = futuresPrivatePostCopyTradeFuturesOrders = Entry('copy-trade/futures/orders', 'futuresPrivate', 'POST', {'cost': 3}) + futuresprivate_post_copy_trade_futures_orders_test = futuresPrivatePostCopyTradeFuturesOrdersTest = Entry('copy-trade/futures/orders/test', 'futuresPrivate', 'POST', {'cost': 3}) + futuresprivate_post_copy_trade_futures_st_orders = futuresPrivatePostCopyTradeFuturesStOrders = Entry('copy-trade/futures/st-orders', 'futuresPrivate', 'POST', {'cost': 3}) + futuresprivate_post_copy_trade_futures_position_margin_deposit_margin = futuresPrivatePostCopyTradeFuturesPositionMarginDepositMargin = Entry('copy-trade/futures/position/margin/deposit-margin', 'futuresPrivate', 'POST', {'cost': 6}) + futuresprivate_post_copy_trade_futures_position_margin_withdraw_margin = futuresPrivatePostCopyTradeFuturesPositionMarginWithdrawMargin = Entry('copy-trade/futures/position/margin/withdraw-margin', 'futuresPrivate', 'POST', {'cost': 15}) + futuresprivate_post_copy_trade_futures_position_risk_limit_level_change = futuresPrivatePostCopyTradeFuturesPositionRiskLimitLevelChange = Entry('copy-trade/futures/position/risk-limit-level/change', 'futuresPrivate', 'POST', {'cost': 3}) + futuresprivate_post_copy_trade_futures_position_margin_auto_deposit_status = futuresPrivatePostCopyTradeFuturesPositionMarginAutoDepositStatus = Entry('copy-trade/futures/position/margin/auto-deposit-status', 'futuresPrivate', 'POST', {'cost': 6}) + futuresprivate_post_copy_trade_futures_position_changemarginmode = futuresPrivatePostCopyTradeFuturesPositionChangeMarginMode = Entry('copy-trade/futures/position/changeMarginMode', 'futuresPrivate', 'POST', {'cost': 3}) + futuresprivate_post_copy_trade_futures_position_changecrossuserleverage = futuresPrivatePostCopyTradeFuturesPositionChangeCrossUserLeverage = Entry('copy-trade/futures/position/changeCrossUserLeverage', 'futuresPrivate', 'POST', {'cost': 3}) + futuresprivate_post_copy_trade_getcrossmodemarginrequirement = futuresPrivatePostCopyTradeGetCrossModeMarginRequirement = Entry('copy-trade/getCrossModeMarginRequirement', 'futuresPrivate', 'POST', {'cost': 4.5}) + futuresprivate_post_copy_trade_position_switchpositionmode = futuresPrivatePostCopyTradePositionSwitchPositionMode = Entry('copy-trade/position/switchPositionMode', 'futuresPrivate', 'POST', {'cost': 3}) + futuresprivate_post_bullet_private = futuresPrivatePostBulletPrivate = Entry('bullet-private', 'futuresPrivate', 'POST', {'cost': 1}) + futuresprivate_post_withdrawals = futuresPrivatePostWithdrawals = Entry('withdrawals', 'futuresPrivate', 'POST', {'cost': 1}) + futuresprivate_post_st_orders = futuresPrivatePostStOrders = Entry('st-orders', 'futuresPrivate', 'POST', {'cost': 1.33}) + futuresprivate_post_sub_api_key = futuresPrivatePostSubApiKey = Entry('sub/api-key', 'futuresPrivate', 'POST', {'cost': 1}) + futuresprivate_post_sub_api_key_update = futuresPrivatePostSubApiKeyUpdate = Entry('sub/api-key/update', 'futuresPrivate', 'POST', {'cost': 1}) + futuresprivate_post_changecrossuserleverage = futuresPrivatePostChangeCrossUserLeverage = Entry('changeCrossUserLeverage', 'futuresPrivate', 'POST', {'cost': 1}) + futuresprivate_post_position_changemarginmode = futuresPrivatePostPositionChangeMarginMode = Entry('position/changeMarginMode', 'futuresPrivate', 'POST', {'cost': 1}) + futuresprivate_post_position_switchpositionmode = futuresPrivatePostPositionSwitchPositionMode = Entry('position/switchPositionMode', 'futuresPrivate', 'POST', {'cost': 1}) + futuresprivate_delete_orders_orderid = futuresPrivateDeleteOrdersOrderId = Entry('orders/{orderId}', 'futuresPrivate', 'DELETE', {'cost': 1}) + futuresprivate_delete_orders_client_order_clientoid = futuresPrivateDeleteOrdersClientOrderClientOid = Entry('orders/client-order/{clientOid}', 'futuresPrivate', 'DELETE', {'cost': 1}) + futuresprivate_delete_orders = futuresPrivateDeleteOrders = Entry('orders', 'futuresPrivate', 'DELETE', {'cost': 4.44}) + futuresprivate_delete_stoporders = futuresPrivateDeleteStopOrders = Entry('stopOrders', 'futuresPrivate', 'DELETE', {'cost': 1}) + futuresprivate_delete_copy_trade_futures_orders = futuresPrivateDeleteCopyTradeFuturesOrders = Entry('copy-trade/futures/orders', 'futuresPrivate', 'DELETE', {'cost': 1.5}) + futuresprivate_delete_copy_trade_futures_orders_client_order = futuresPrivateDeleteCopyTradeFuturesOrdersClientOrder = Entry('copy-trade/futures/orders/client-order', 'futuresPrivate', 'DELETE', {'cost': 1.5}) + futuresprivate_delete_withdrawals_withdrawalid = futuresPrivateDeleteWithdrawalsWithdrawalId = Entry('withdrawals/{withdrawalId}', 'futuresPrivate', 'DELETE', {'cost': 1}) + futuresprivate_delete_cancel_transfer_out = futuresPrivateDeleteCancelTransferOut = Entry('cancel/transfer-out', 'futuresPrivate', 'DELETE', {'cost': 1}) + futuresprivate_delete_sub_api_key = futuresPrivateDeleteSubApiKey = Entry('sub/api-key', 'futuresPrivate', 'DELETE', {'cost': 1}) + futuresprivate_delete_orders_multi_cancel = futuresPrivateDeleteOrdersMultiCancel = Entry('orders/multi-cancel', 'futuresPrivate', 'DELETE', {'cost': 20}) + webexchange_get_currency_currency_chain_info = webExchangeGetCurrencyCurrencyChainInfo = Entry('currency/currency/chain-info', 'webExchange', 'GET', {'cost': 1}) + webexchange_get_contract_symbol_funding_rates = webExchangeGetContractSymbolFundingRates = Entry('contract/{symbol}/funding-rates', 'webExchange', 'GET', {'cost': 1}) + broker_get_broker_nd_info = brokerGetBrokerNdInfo = Entry('broker/nd/info', 'broker', 'GET', {'cost': 2}) + broker_get_broker_nd_account = brokerGetBrokerNdAccount = Entry('broker/nd/account', 'broker', 'GET', {'cost': 2}) + broker_get_broker_nd_account_apikey = brokerGetBrokerNdAccountApikey = Entry('broker/nd/account/apikey', 'broker', 'GET', {'cost': 2}) + broker_get_broker_nd_rebase_download = brokerGetBrokerNdRebaseDownload = Entry('broker/nd/rebase/download', 'broker', 'GET', {'cost': 3}) + broker_get_asset_ndbroker_deposit_list = brokerGetAssetNdbrokerDepositList = Entry('asset/ndbroker/deposit/list', 'broker', 'GET', {'cost': 1}) + broker_get_broker_nd_transfer_detail = brokerGetBrokerNdTransferDetail = Entry('broker/nd/transfer/detail', 'broker', 'GET', {'cost': 1}) + broker_get_broker_nd_deposit_detail = brokerGetBrokerNdDepositDetail = Entry('broker/nd/deposit/detail', 'broker', 'GET', {'cost': 1}) + broker_get_broker_nd_withdraw_detail = brokerGetBrokerNdWithdrawDetail = Entry('broker/nd/withdraw/detail', 'broker', 'GET', {'cost': 1}) + broker_post_broker_nd_transfer = brokerPostBrokerNdTransfer = Entry('broker/nd/transfer', 'broker', 'POST', {'cost': 1}) + broker_post_broker_nd_account = brokerPostBrokerNdAccount = Entry('broker/nd/account', 'broker', 'POST', {'cost': 3}) + broker_post_broker_nd_account_apikey = brokerPostBrokerNdAccountApikey = Entry('broker/nd/account/apikey', 'broker', 'POST', {'cost': 3}) + broker_post_broker_nd_account_update_apikey = brokerPostBrokerNdAccountUpdateApikey = Entry('broker/nd/account/update-apikey', 'broker', 'POST', {'cost': 3}) + broker_delete_broker_nd_account_apikey = brokerDeleteBrokerNdAccountApikey = Entry('broker/nd/account/apikey', 'broker', 'DELETE', {'cost': 3}) + earn_get_otc_loan_loan = earnGetOtcLoanLoan = Entry('otc-loan/loan', 'earn', 'GET', {'cost': 1}) + earn_get_otc_loan_accounts = earnGetOtcLoanAccounts = Entry('otc-loan/accounts', 'earn', 'GET', {'cost': 1}) + earn_get_earn_redeem_preview = earnGetEarnRedeemPreview = Entry('earn/redeem-preview', 'earn', 'GET', {'cost': 7.5}) + earn_get_earn_saving_products = earnGetEarnSavingProducts = Entry('earn/saving/products', 'earn', 'GET', {'cost': 7.5}) + earn_get_earn_hold_assets = earnGetEarnHoldAssets = Entry('earn/hold-assets', 'earn', 'GET', {'cost': 7.5}) + earn_get_earn_promotion_products = earnGetEarnPromotionProducts = Entry('earn/promotion/products', 'earn', 'GET', {'cost': 7.5}) + earn_get_earn_kcs_staking_products = earnGetEarnKcsStakingProducts = Entry('earn/kcs-staking/products', 'earn', 'GET', {'cost': 7.5}) + earn_get_earn_staking_products = earnGetEarnStakingProducts = Entry('earn/staking/products', 'earn', 'GET', {'cost': 7.5}) + earn_get_earn_eth_staking_products = earnGetEarnEthStakingProducts = Entry('earn/eth-staking/products', 'earn', 'GET', {'cost': 7.5}) + earn_post_earn_orders = earnPostEarnOrders = Entry('earn/orders', 'earn', 'POST', {'cost': 7.5}) + earn_delete_earn_orders = earnDeleteEarnOrders = Entry('earn/orders', 'earn', 'DELETE', {'cost': 7.5}) + uta_get_market_announcement = utaGetMarketAnnouncement = Entry('market/announcement', 'uta', 'GET', {'cost': 20}) + uta_get_market_currency = utaGetMarketCurrency = Entry('market/currency', 'uta', 'GET', {'cost': 3}) + uta_get_market_instrument = utaGetMarketInstrument = Entry('market/instrument', 'uta', 'GET', {'cost': 4}) + uta_get_market_ticker = utaGetMarketTicker = Entry('market/ticker', 'uta', 'GET', {'cost': 15}) + uta_get_market_orderbook = utaGetMarketOrderbook = Entry('market/orderbook', 'uta', 'GET', {'cost': 3}) + uta_get_market_trade = utaGetMarketTrade = Entry('market/trade', 'uta', 'GET', {'cost': 3}) + uta_get_market_kline = utaGetMarketKline = Entry('market/kline', 'uta', 'GET', {'cost': 3}) + uta_get_market_funding_rate = utaGetMarketFundingRate = Entry('market/funding-rate', 'uta', 'GET', {'cost': 2}) + uta_get_market_funding_rate_history = utaGetMarketFundingRateHistory = Entry('market/funding-rate-history', 'uta', 'GET', {'cost': 5}) + uta_get_market_cross_config = utaGetMarketCrossConfig = Entry('market/cross-config', 'uta', 'GET', {'cost': 25}) + uta_get_server_status = utaGetServerStatus = Entry('server/status', 'uta', 'GET', {'cost': 3}) diff --git a/ccxt/abstract/latoken.py b/ccxt/abstract/latoken.py new file mode 100644 index 0000000..c984661 --- /dev/null +++ b/ccxt/abstract/latoken.py @@ -0,0 +1,56 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_book_currency_quote = publicGetBookCurrencyQuote = Entry('book/{currency}/{quote}', 'public', 'GET', {'cost': 1}) + public_get_chart_week = publicGetChartWeek = Entry('chart/week', 'public', 'GET', {'cost': 1}) + public_get_chart_week_currency_quote = publicGetChartWeekCurrencyQuote = Entry('chart/week/{currency}/{quote}', 'public', 'GET', {'cost': 1}) + public_get_currency = publicGetCurrency = Entry('currency', 'public', 'GET', {'cost': 1}) + public_get_currency_available = publicGetCurrencyAvailable = Entry('currency/available', 'public', 'GET', {'cost': 1}) + public_get_currency_quotes = publicGetCurrencyQuotes = Entry('currency/quotes', 'public', 'GET', {'cost': 1}) + public_get_currency_currency = publicGetCurrencyCurrency = Entry('currency/{currency}', 'public', 'GET', {'cost': 1}) + public_get_pair = publicGetPair = Entry('pair', 'public', 'GET', {'cost': 1}) + public_get_pair_available = publicGetPairAvailable = Entry('pair/available', 'public', 'GET', {'cost': 1}) + public_get_ticker = publicGetTicker = Entry('ticker', 'public', 'GET', {'cost': 1}) + public_get_ticker_base_quote = publicGetTickerBaseQuote = Entry('ticker/{base}/{quote}', 'public', 'GET', {'cost': 1}) + public_get_time = publicGetTime = Entry('time', 'public', 'GET', {'cost': 1}) + public_get_trade_history_currency_quote = publicGetTradeHistoryCurrencyQuote = Entry('trade/history/{currency}/{quote}', 'public', 'GET', {'cost': 1}) + public_get_trade_fee_currency_quote = publicGetTradeFeeCurrencyQuote = Entry('trade/fee/{currency}/{quote}', 'public', 'GET', {'cost': 1}) + public_get_trade_feelevels = publicGetTradeFeeLevels = Entry('trade/feeLevels', 'public', 'GET', {'cost': 1}) + public_get_transaction_bindings = publicGetTransactionBindings = Entry('transaction/bindings', 'public', 'GET', {'cost': 1}) + private_get_auth_account = privateGetAuthAccount = Entry('auth/account', 'private', 'GET', {'cost': 1}) + private_get_auth_account_currency_currency_type = privateGetAuthAccountCurrencyCurrencyType = Entry('auth/account/currency/{currency}/{type}', 'private', 'GET', {'cost': 1}) + private_get_auth_order = privateGetAuthOrder = Entry('auth/order', 'private', 'GET', {'cost': 1}) + private_get_auth_order_getorder_id = privateGetAuthOrderGetOrderId = Entry('auth/order/getOrder/{id}', 'private', 'GET', {'cost': 1}) + private_get_auth_order_pair_currency_quote = privateGetAuthOrderPairCurrencyQuote = Entry('auth/order/pair/{currency}/{quote}', 'private', 'GET', {'cost': 1}) + private_get_auth_order_pair_currency_quote_active = privateGetAuthOrderPairCurrencyQuoteActive = Entry('auth/order/pair/{currency}/{quote}/active', 'private', 'GET', {'cost': 1}) + private_get_auth_stoporder = privateGetAuthStopOrder = Entry('auth/stopOrder', 'private', 'GET', {'cost': 1}) + private_get_auth_stoporder_getorder_id = privateGetAuthStopOrderGetOrderId = Entry('auth/stopOrder/getOrder/{id}', 'private', 'GET', {'cost': 1}) + private_get_auth_stoporder_pair_currency_quote = privateGetAuthStopOrderPairCurrencyQuote = Entry('auth/stopOrder/pair/{currency}/{quote}', 'private', 'GET', {'cost': 1}) + private_get_auth_stoporder_pair_currency_quote_active = privateGetAuthStopOrderPairCurrencyQuoteActive = Entry('auth/stopOrder/pair/{currency}/{quote}/active', 'private', 'GET', {'cost': 1}) + private_get_auth_trade = privateGetAuthTrade = Entry('auth/trade', 'private', 'GET', {'cost': 1}) + private_get_auth_trade_pair_currency_quote = privateGetAuthTradePairCurrencyQuote = Entry('auth/trade/pair/{currency}/{quote}', 'private', 'GET', {'cost': 1}) + private_get_auth_trade_fee_currency_quote = privateGetAuthTradeFeeCurrencyQuote = Entry('auth/trade/fee/{currency}/{quote}', 'private', 'GET', {'cost': 1}) + private_get_auth_transaction = privateGetAuthTransaction = Entry('auth/transaction', 'private', 'GET', {'cost': 1}) + private_get_auth_transaction_bindings = privateGetAuthTransactionBindings = Entry('auth/transaction/bindings', 'private', 'GET', {'cost': 1}) + private_get_auth_transaction_bindings_currency = privateGetAuthTransactionBindingsCurrency = Entry('auth/transaction/bindings/{currency}', 'private', 'GET', {'cost': 1}) + private_get_auth_transaction_id = privateGetAuthTransactionId = Entry('auth/transaction/{id}', 'private', 'GET', {'cost': 1}) + private_get_auth_transfer = privateGetAuthTransfer = Entry('auth/transfer', 'private', 'GET', {'cost': 1}) + private_post_auth_order_cancel = privatePostAuthOrderCancel = Entry('auth/order/cancel', 'private', 'POST', {'cost': 1}) + private_post_auth_order_cancelall = privatePostAuthOrderCancelAll = Entry('auth/order/cancelAll', 'private', 'POST', {'cost': 1}) + private_post_auth_order_cancelall_currency_quote = privatePostAuthOrderCancelAllCurrencyQuote = Entry('auth/order/cancelAll/{currency}/{quote}', 'private', 'POST', {'cost': 1}) + private_post_auth_order_place = privatePostAuthOrderPlace = Entry('auth/order/place', 'private', 'POST', {'cost': 1}) + private_post_auth_spot_deposit = privatePostAuthSpotDeposit = Entry('auth/spot/deposit', 'private', 'POST', {'cost': 1}) + private_post_auth_spot_withdraw = privatePostAuthSpotWithdraw = Entry('auth/spot/withdraw', 'private', 'POST', {'cost': 1}) + private_post_auth_stoporder_cancel = privatePostAuthStopOrderCancel = Entry('auth/stopOrder/cancel', 'private', 'POST', {'cost': 1}) + private_post_auth_stoporder_cancelall = privatePostAuthStopOrderCancelAll = Entry('auth/stopOrder/cancelAll', 'private', 'POST', {'cost': 1}) + private_post_auth_stoporder_cancelall_currency_quote = privatePostAuthStopOrderCancelAllCurrencyQuote = Entry('auth/stopOrder/cancelAll/{currency}/{quote}', 'private', 'POST', {'cost': 1}) + private_post_auth_stoporder_place = privatePostAuthStopOrderPlace = Entry('auth/stopOrder/place', 'private', 'POST', {'cost': 1}) + private_post_auth_transaction_depositaddress = privatePostAuthTransactionDepositAddress = Entry('auth/transaction/depositAddress', 'private', 'POST', {'cost': 1}) + private_post_auth_transaction_withdraw = privatePostAuthTransactionWithdraw = Entry('auth/transaction/withdraw', 'private', 'POST', {'cost': 1}) + private_post_auth_transaction_withdraw_cancel = privatePostAuthTransactionWithdrawCancel = Entry('auth/transaction/withdraw/cancel', 'private', 'POST', {'cost': 1}) + private_post_auth_transaction_withdraw_confirm = privatePostAuthTransactionWithdrawConfirm = Entry('auth/transaction/withdraw/confirm', 'private', 'POST', {'cost': 1}) + private_post_auth_transaction_withdraw_resendcode = privatePostAuthTransactionWithdrawResendCode = Entry('auth/transaction/withdraw/resendCode', 'private', 'POST', {'cost': 1}) + private_post_auth_transfer_email = privatePostAuthTransferEmail = Entry('auth/transfer/email', 'private', 'POST', {'cost': 1}) + private_post_auth_transfer_id = privatePostAuthTransferId = Entry('auth/transfer/id', 'private', 'POST', {'cost': 1}) + private_post_auth_transfer_phone = privatePostAuthTransferPhone = Entry('auth/transfer/phone', 'private', 'POST', {'cost': 1}) diff --git a/ccxt/abstract/lbank.py b/ccxt/abstract/lbank.py new file mode 100644 index 0000000..b83e7f2 --- /dev/null +++ b/ccxt/abstract/lbank.py @@ -0,0 +1,62 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + spot_public_get_currencypairs = spotPublicGetCurrencyPairs = Entry('currencyPairs', ['spot', 'public'], 'GET', {'cost': 2.5}) + spot_public_get_accuracy = spotPublicGetAccuracy = Entry('accuracy', ['spot', 'public'], 'GET', {'cost': 2.5}) + spot_public_get_usdtocny = spotPublicGetUsdToCny = Entry('usdToCny', ['spot', 'public'], 'GET', {'cost': 2.5}) + spot_public_get_assetconfigs = spotPublicGetAssetConfigs = Entry('assetConfigs', ['spot', 'public'], 'GET', {'cost': 2.5}) + spot_public_get_withdrawconfigs = spotPublicGetWithdrawConfigs = Entry('withdrawConfigs', ['spot', 'public'], 'GET', {'cost': 3.75}) + spot_public_get_timestamp = spotPublicGetTimestamp = Entry('timestamp', ['spot', 'public'], 'GET', {'cost': 2.5}) + spot_public_get_ticker_24hr = spotPublicGetTicker24hr = Entry('ticker/24hr', ['spot', 'public'], 'GET', {'cost': 2.5}) + spot_public_get_ticker = spotPublicGetTicker = Entry('ticker', ['spot', 'public'], 'GET', {'cost': 2.5}) + spot_public_get_depth = spotPublicGetDepth = Entry('depth', ['spot', 'public'], 'GET', {'cost': 2.5}) + spot_public_get_incrdepth = spotPublicGetIncrDepth = Entry('incrDepth', ['spot', 'public'], 'GET', {'cost': 2.5}) + spot_public_get_trades = spotPublicGetTrades = Entry('trades', ['spot', 'public'], 'GET', {'cost': 2.5}) + spot_public_get_kline = spotPublicGetKline = Entry('kline', ['spot', 'public'], 'GET', {'cost': 2.5}) + spot_public_get_supplement_system_ping = spotPublicGetSupplementSystemPing = Entry('supplement/system_ping', ['spot', 'public'], 'GET', {'cost': 2.5}) + spot_public_get_supplement_incrdepth = spotPublicGetSupplementIncrDepth = Entry('supplement/incrDepth', ['spot', 'public'], 'GET', {'cost': 2.5}) + spot_public_get_supplement_trades = spotPublicGetSupplementTrades = Entry('supplement/trades', ['spot', 'public'], 'GET', {'cost': 2.5}) + spot_public_get_supplement_ticker_price = spotPublicGetSupplementTickerPrice = Entry('supplement/ticker/price', ['spot', 'public'], 'GET', {'cost': 2.5}) + spot_public_get_supplement_ticker_bookticker = spotPublicGetSupplementTickerBookTicker = Entry('supplement/ticker/bookTicker', ['spot', 'public'], 'GET', {'cost': 2.5}) + spot_public_post_supplement_system_status = spotPublicPostSupplementSystemStatus = Entry('supplement/system_status', ['spot', 'public'], 'POST', {'cost': 2.5}) + spot_private_post_user_info = spotPrivatePostUserInfo = Entry('user_info', ['spot', 'private'], 'POST', {'cost': 2.5}) + spot_private_post_subscribe_get_key = spotPrivatePostSubscribeGetKey = Entry('subscribe/get_key', ['spot', 'private'], 'POST', {'cost': 2.5}) + spot_private_post_subscribe_refresh_key = spotPrivatePostSubscribeRefreshKey = Entry('subscribe/refresh_key', ['spot', 'private'], 'POST', {'cost': 2.5}) + spot_private_post_subscribe_destroy_key = spotPrivatePostSubscribeDestroyKey = Entry('subscribe/destroy_key', ['spot', 'private'], 'POST', {'cost': 2.5}) + spot_private_post_get_deposit_address = spotPrivatePostGetDepositAddress = Entry('get_deposit_address', ['spot', 'private'], 'POST', {'cost': 2.5}) + spot_private_post_deposit_history = spotPrivatePostDepositHistory = Entry('deposit_history', ['spot', 'private'], 'POST', {'cost': 2.5}) + spot_private_post_create_order = spotPrivatePostCreateOrder = Entry('create_order', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_post_batch_create_order = spotPrivatePostBatchCreateOrder = Entry('batch_create_order', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_post_cancel_order = spotPrivatePostCancelOrder = Entry('cancel_order', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_post_cancel_clientorders = spotPrivatePostCancelClientOrders = Entry('cancel_clientOrders', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_post_orders_info = spotPrivatePostOrdersInfo = Entry('orders_info', ['spot', 'private'], 'POST', {'cost': 2.5}) + spot_private_post_orders_info_history = spotPrivatePostOrdersInfoHistory = Entry('orders_info_history', ['spot', 'private'], 'POST', {'cost': 2.5}) + spot_private_post_order_transaction_detail = spotPrivatePostOrderTransactionDetail = Entry('order_transaction_detail', ['spot', 'private'], 'POST', {'cost': 2.5}) + spot_private_post_transaction_history = spotPrivatePostTransactionHistory = Entry('transaction_history', ['spot', 'private'], 'POST', {'cost': 2.5}) + spot_private_post_orders_info_no_deal = spotPrivatePostOrdersInfoNoDeal = Entry('orders_info_no_deal', ['spot', 'private'], 'POST', {'cost': 2.5}) + spot_private_post_withdraw = spotPrivatePostWithdraw = Entry('withdraw', ['spot', 'private'], 'POST', {'cost': 2.5}) + spot_private_post_withdrawcancel = spotPrivatePostWithdrawCancel = Entry('withdrawCancel', ['spot', 'private'], 'POST', {'cost': 2.5}) + spot_private_post_withdraws = spotPrivatePostWithdraws = Entry('withdraws', ['spot', 'private'], 'POST', {'cost': 2.5}) + spot_private_post_supplement_user_info = spotPrivatePostSupplementUserInfo = Entry('supplement/user_info', ['spot', 'private'], 'POST', {'cost': 2.5}) + spot_private_post_supplement_withdraw = spotPrivatePostSupplementWithdraw = Entry('supplement/withdraw', ['spot', 'private'], 'POST', {'cost': 2.5}) + spot_private_post_supplement_deposit_history = spotPrivatePostSupplementDepositHistory = Entry('supplement/deposit_history', ['spot', 'private'], 'POST', {'cost': 2.5}) + spot_private_post_supplement_withdraws = spotPrivatePostSupplementWithdraws = Entry('supplement/withdraws', ['spot', 'private'], 'POST', {'cost': 2.5}) + spot_private_post_supplement_get_deposit_address = spotPrivatePostSupplementGetDepositAddress = Entry('supplement/get_deposit_address', ['spot', 'private'], 'POST', {'cost': 2.5}) + spot_private_post_supplement_asset_detail = spotPrivatePostSupplementAssetDetail = Entry('supplement/asset_detail', ['spot', 'private'], 'POST', {'cost': 2.5}) + spot_private_post_supplement_customer_trade_fee = spotPrivatePostSupplementCustomerTradeFee = Entry('supplement/customer_trade_fee', ['spot', 'private'], 'POST', {'cost': 2.5}) + spot_private_post_supplement_api_restrictions = spotPrivatePostSupplementApiRestrictions = Entry('supplement/api_Restrictions', ['spot', 'private'], 'POST', {'cost': 2.5}) + spot_private_post_supplement_system_ping = spotPrivatePostSupplementSystemPing = Entry('supplement/system_ping', ['spot', 'private'], 'POST', {'cost': 2.5}) + spot_private_post_supplement_create_order_test = spotPrivatePostSupplementCreateOrderTest = Entry('supplement/create_order_test', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_post_supplement_create_order = spotPrivatePostSupplementCreateOrder = Entry('supplement/create_order', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_post_supplement_cancel_order = spotPrivatePostSupplementCancelOrder = Entry('supplement/cancel_order', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_post_supplement_cancel_order_by_symbol = spotPrivatePostSupplementCancelOrderBySymbol = Entry('supplement/cancel_order_by_symbol', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_post_supplement_orders_info = spotPrivatePostSupplementOrdersInfo = Entry('supplement/orders_info', ['spot', 'private'], 'POST', {'cost': 2.5}) + spot_private_post_supplement_orders_info_no_deal = spotPrivatePostSupplementOrdersInfoNoDeal = Entry('supplement/orders_info_no_deal', ['spot', 'private'], 'POST', {'cost': 2.5}) + spot_private_post_supplement_orders_info_history = spotPrivatePostSupplementOrdersInfoHistory = Entry('supplement/orders_info_history', ['spot', 'private'], 'POST', {'cost': 2.5}) + spot_private_post_supplement_user_info_account = spotPrivatePostSupplementUserInfoAccount = Entry('supplement/user_info_account', ['spot', 'private'], 'POST', {'cost': 2.5}) + spot_private_post_supplement_transaction_history = spotPrivatePostSupplementTransactionHistory = Entry('supplement/transaction_history', ['spot', 'private'], 'POST', {'cost': 2.5}) + contract_public_get_cfd_openapi_v1_pub_gettime = contractPublicGetCfdOpenApiV1PubGetTime = Entry('cfd/openApi/v1/pub/getTime', ['contract', 'public'], 'GET', {'cost': 2.5}) + contract_public_get_cfd_openapi_v1_pub_instrument = contractPublicGetCfdOpenApiV1PubInstrument = Entry('cfd/openApi/v1/pub/instrument', ['contract', 'public'], 'GET', {'cost': 2.5}) + contract_public_get_cfd_openapi_v1_pub_marketdata = contractPublicGetCfdOpenApiV1PubMarketData = Entry('cfd/openApi/v1/pub/marketData', ['contract', 'public'], 'GET', {'cost': 2.5}) + contract_public_get_cfd_openapi_v1_pub_marketorder = contractPublicGetCfdOpenApiV1PubMarketOrder = Entry('cfd/openApi/v1/pub/marketOrder', ['contract', 'public'], 'GET', {'cost': 2.5}) diff --git a/ccxt/abstract/luno.py b/ccxt/abstract/luno.py new file mode 100644 index 0000000..6427724 --- /dev/null +++ b/ccxt/abstract/luno.py @@ -0,0 +1,38 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + exchange_get_markets = exchangeGetMarkets = Entry('markets', 'exchange', 'GET', {'cost': 1}) + exchangeprivate_get_candles = exchangePrivateGetCandles = Entry('candles', 'exchangePrivate', 'GET', {'cost': 1}) + public_get_orderbook = publicGetOrderbook = Entry('orderbook', 'public', 'GET', {'cost': 1}) + public_get_orderbook_top = publicGetOrderbookTop = Entry('orderbook_top', 'public', 'GET', {'cost': 1}) + public_get_ticker = publicGetTicker = Entry('ticker', 'public', 'GET', {'cost': 1}) + public_get_tickers = publicGetTickers = Entry('tickers', 'public', 'GET', {'cost': 1}) + public_get_trades = publicGetTrades = Entry('trades', 'public', 'GET', {'cost': 1}) + private_get_accounts_id_pending = privateGetAccountsIdPending = Entry('accounts/{id}/pending', 'private', 'GET', {'cost': 1}) + private_get_accounts_id_transactions = privateGetAccountsIdTransactions = Entry('accounts/{id}/transactions', 'private', 'GET', {'cost': 1}) + private_get_balance = privateGetBalance = Entry('balance', 'private', 'GET', {'cost': 1}) + private_get_beneficiaries = privateGetBeneficiaries = Entry('beneficiaries', 'private', 'GET', {'cost': 1}) + private_get_send_networks = privateGetSendNetworks = Entry('send/networks', 'private', 'GET', {'cost': 1}) + private_get_fee_info = privateGetFeeInfo = Entry('fee_info', 'private', 'GET', {'cost': 1}) + private_get_funding_address = privateGetFundingAddress = Entry('funding_address', 'private', 'GET', {'cost': 1}) + private_get_listorders = privateGetListorders = Entry('listorders', 'private', 'GET', {'cost': 1}) + private_get_listtrades = privateGetListtrades = Entry('listtrades', 'private', 'GET', {'cost': 1}) + private_get_send_fee = privateGetSendFee = Entry('send_fee', 'private', 'GET', {'cost': 1}) + private_get_orders_id = privateGetOrdersId = Entry('orders/{id}', 'private', 'GET', {'cost': 1}) + private_get_withdrawals = privateGetWithdrawals = Entry('withdrawals', 'private', 'GET', {'cost': 1}) + private_get_withdrawals_id = privateGetWithdrawalsId = Entry('withdrawals/{id}', 'private', 'GET', {'cost': 1}) + private_get_transfers = privateGetTransfers = Entry('transfers', 'private', 'GET', {'cost': 1}) + private_post_accounts = privatePostAccounts = Entry('accounts', 'private', 'POST', {'cost': 1}) + private_post_address_validate = privatePostAddressValidate = Entry('address/validate', 'private', 'POST', {'cost': 1}) + private_post_postorder = privatePostPostorder = Entry('postorder', 'private', 'POST', {'cost': 1}) + private_post_marketorder = privatePostMarketorder = Entry('marketorder', 'private', 'POST', {'cost': 1}) + private_post_stoporder = privatePostStoporder = Entry('stoporder', 'private', 'POST', {'cost': 1}) + private_post_funding_address = privatePostFundingAddress = Entry('funding_address', 'private', 'POST', {'cost': 1}) + private_post_withdrawals = privatePostWithdrawals = Entry('withdrawals', 'private', 'POST', {'cost': 1}) + private_post_send = privatePostSend = Entry('send', 'private', 'POST', {'cost': 1}) + private_post_oauth2_grant = privatePostOauth2Grant = Entry('oauth2/grant', 'private', 'POST', {'cost': 1}) + private_post_beneficiaries = privatePostBeneficiaries = Entry('beneficiaries', 'private', 'POST', {'cost': 1}) + private_put_accounts_id_name = privatePutAccountsIdName = Entry('accounts/{id}/name', 'private', 'PUT', {'cost': 1}) + private_delete_withdrawals_id = privateDeleteWithdrawalsId = Entry('withdrawals/{id}', 'private', 'DELETE', {'cost': 1}) + private_delete_beneficiaries_id = privateDeleteBeneficiariesId = Entry('beneficiaries/{id}', 'private', 'DELETE', {'cost': 1}) diff --git a/ccxt/abstract/mercado.py b/ccxt/abstract/mercado.py new file mode 100644 index 0000000..c4c7207 --- /dev/null +++ b/ccxt/abstract/mercado.py @@ -0,0 +1,25 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_coins = publicGetCoins = Entry('coins', 'public', 'GET', {}) + public_get_coin_orderbook = publicGetCoinOrderbook = Entry('{coin}/orderbook/', 'public', 'GET', {}) + public_get_coin_ticker = publicGetCoinTicker = Entry('{coin}/ticker/', 'public', 'GET', {}) + public_get_coin_trades = publicGetCoinTrades = Entry('{coin}/trades/', 'public', 'GET', {}) + public_get_coin_trades_from = publicGetCoinTradesFrom = Entry('{coin}/trades/{from}/', 'public', 'GET', {}) + public_get_coin_trades_from_to = publicGetCoinTradesFromTo = Entry('{coin}/trades/{from}/{to}', 'public', 'GET', {}) + public_get_coin_day_summary_year_month_day = publicGetCoinDaySummaryYearMonthDay = Entry('{coin}/day-summary/{year}/{month}/{day}/', 'public', 'GET', {}) + private_post_cancel_order = privatePostCancelOrder = Entry('cancel_order', 'private', 'POST', {}) + private_post_get_account_info = privatePostGetAccountInfo = Entry('get_account_info', 'private', 'POST', {}) + private_post_get_order = privatePostGetOrder = Entry('get_order', 'private', 'POST', {}) + private_post_get_withdrawal = privatePostGetWithdrawal = Entry('get_withdrawal', 'private', 'POST', {}) + private_post_list_system_messages = privatePostListSystemMessages = Entry('list_system_messages', 'private', 'POST', {}) + private_post_list_orders = privatePostListOrders = Entry('list_orders', 'private', 'POST', {}) + private_post_list_orderbook = privatePostListOrderbook = Entry('list_orderbook', 'private', 'POST', {}) + private_post_place_buy_order = privatePostPlaceBuyOrder = Entry('place_buy_order', 'private', 'POST', {}) + private_post_place_sell_order = privatePostPlaceSellOrder = Entry('place_sell_order', 'private', 'POST', {}) + private_post_place_market_buy_order = privatePostPlaceMarketBuyOrder = Entry('place_market_buy_order', 'private', 'POST', {}) + private_post_place_market_sell_order = privatePostPlaceMarketSellOrder = Entry('place_market_sell_order', 'private', 'POST', {}) + private_post_withdraw_coin = privatePostWithdrawCoin = Entry('withdraw_coin', 'private', 'POST', {}) + v4public_get_coin_candle = v4PublicGetCoinCandle = Entry('{coin}/candle/', 'v4Public', 'GET', {}) + v4publicnet_get_candles = v4PublicNetGetCandles = Entry('candles', 'v4PublicNet', 'GET', {}) diff --git a/ccxt/abstract/mexc.py b/ccxt/abstract/mexc.py new file mode 100644 index 0000000..8d27964 --- /dev/null +++ b/ccxt/abstract/mexc.py @@ -0,0 +1,181 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + spot_public_get_ping = spotPublicGetPing = Entry('ping', ['spot', 'public'], 'GET', {'cost': 1}) + spot_public_get_time = spotPublicGetTime = Entry('time', ['spot', 'public'], 'GET', {'cost': 1}) + spot_public_get_exchangeinfo = spotPublicGetExchangeInfo = Entry('exchangeInfo', ['spot', 'public'], 'GET', {'cost': 10}) + spot_public_get_depth = spotPublicGetDepth = Entry('depth', ['spot', 'public'], 'GET', {'cost': 1}) + spot_public_get_trades = spotPublicGetTrades = Entry('trades', ['spot', 'public'], 'GET', {'cost': 5}) + spot_public_get_historicaltrades = spotPublicGetHistoricalTrades = Entry('historicalTrades', ['spot', 'public'], 'GET', {'cost': 1}) + spot_public_get_aggtrades = spotPublicGetAggTrades = Entry('aggTrades', ['spot', 'public'], 'GET', {'cost': 1}) + spot_public_get_klines = spotPublicGetKlines = Entry('klines', ['spot', 'public'], 'GET', {'cost': 1}) + spot_public_get_avgprice = spotPublicGetAvgPrice = Entry('avgPrice', ['spot', 'public'], 'GET', {'cost': 1}) + spot_public_get_ticker_24hr = spotPublicGetTicker24hr = Entry('ticker/24hr', ['spot', 'public'], 'GET', {'cost': 1}) + spot_public_get_ticker_price = spotPublicGetTickerPrice = Entry('ticker/price', ['spot', 'public'], 'GET', {'cost': 1}) + spot_public_get_ticker_bookticker = spotPublicGetTickerBookTicker = Entry('ticker/bookTicker', ['spot', 'public'], 'GET', {'cost': 1}) + spot_public_get_etf_info = spotPublicGetEtfInfo = Entry('etf/info', ['spot', 'public'], 'GET', {'cost': 1}) + spot_private_get_order = spotPrivateGetOrder = Entry('order', ['spot', 'private'], 'GET', {'cost': 2}) + spot_private_get_openorders = spotPrivateGetOpenOrders = Entry('openOrders', ['spot', 'private'], 'GET', {'cost': 3}) + spot_private_get_allorders = spotPrivateGetAllOrders = Entry('allOrders', ['spot', 'private'], 'GET', {'cost': 10}) + spot_private_get_account = spotPrivateGetAccount = Entry('account', ['spot', 'private'], 'GET', {'cost': 10}) + spot_private_get_mytrades = spotPrivateGetMyTrades = Entry('myTrades', ['spot', 'private'], 'GET', {'cost': 10}) + spot_private_get_tradefee = spotPrivateGetTradeFee = Entry('tradeFee', ['spot', 'private'], 'GET', {'cost': 10}) + spot_private_get_sub_account_list = spotPrivateGetSubAccountList = Entry('sub-account/list', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_sub_account_apikey = spotPrivateGetSubAccountApiKey = Entry('sub-account/apiKey', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_capital_config_getall = spotPrivateGetCapitalConfigGetall = Entry('capital/config/getall', ['spot', 'private'], 'GET', {'cost': 10}) + spot_private_get_capital_deposit_hisrec = spotPrivateGetCapitalDepositHisrec = Entry('capital/deposit/hisrec', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_capital_withdraw_history = spotPrivateGetCapitalWithdrawHistory = Entry('capital/withdraw/history', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_capital_withdraw_address = spotPrivateGetCapitalWithdrawAddress = Entry('capital/withdraw/address', ['spot', 'private'], 'GET', {'cost': 10}) + spot_private_get_capital_deposit_address = spotPrivateGetCapitalDepositAddress = Entry('capital/deposit/address', ['spot', 'private'], 'GET', {'cost': 10}) + spot_private_get_capital_transfer = spotPrivateGetCapitalTransfer = Entry('capital/transfer', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_capital_transfer_tranid = spotPrivateGetCapitalTransferTranId = Entry('capital/transfer/tranId', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_capital_transfer_internal = spotPrivateGetCapitalTransferInternal = Entry('capital/transfer/internal', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_capital_sub_account_universaltransfer = spotPrivateGetCapitalSubAccountUniversalTransfer = Entry('capital/sub-account/universalTransfer', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_capital_convert = spotPrivateGetCapitalConvert = Entry('capital/convert', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_capital_convert_list = spotPrivateGetCapitalConvertList = Entry('capital/convert/list', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_margin_loan = spotPrivateGetMarginLoan = Entry('margin/loan', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_margin_allorders = spotPrivateGetMarginAllOrders = Entry('margin/allOrders', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_margin_mytrades = spotPrivateGetMarginMyTrades = Entry('margin/myTrades', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_margin_openorders = spotPrivateGetMarginOpenOrders = Entry('margin/openOrders', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_margin_maxtransferable = spotPrivateGetMarginMaxTransferable = Entry('margin/maxTransferable', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_margin_priceindex = spotPrivateGetMarginPriceIndex = Entry('margin/priceIndex', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_margin_order = spotPrivateGetMarginOrder = Entry('margin/order', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_margin_isolated_account = spotPrivateGetMarginIsolatedAccount = Entry('margin/isolated/account', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_margin_maxborrowable = spotPrivateGetMarginMaxBorrowable = Entry('margin/maxBorrowable', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_margin_repay = spotPrivateGetMarginRepay = Entry('margin/repay', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_margin_isolated_pair = spotPrivateGetMarginIsolatedPair = Entry('margin/isolated/pair', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_margin_forceliquidationrec = spotPrivateGetMarginForceLiquidationRec = Entry('margin/forceLiquidationRec', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_margin_isolatedmargindata = spotPrivateGetMarginIsolatedMarginData = Entry('margin/isolatedMarginData', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_margin_isolatedmargintier = spotPrivateGetMarginIsolatedMarginTier = Entry('margin/isolatedMarginTier', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_rebate_taxquery = spotPrivateGetRebateTaxQuery = Entry('rebate/taxQuery', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_rebate_detail = spotPrivateGetRebateDetail = Entry('rebate/detail', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_rebate_detail_kickback = spotPrivateGetRebateDetailKickback = Entry('rebate/detail/kickback', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_rebate_refercode = spotPrivateGetRebateReferCode = Entry('rebate/referCode', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_rebate_affiliate_commission = spotPrivateGetRebateAffiliateCommission = Entry('rebate/affiliate/commission', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_rebate_affiliate_withdraw = spotPrivateGetRebateAffiliateWithdraw = Entry('rebate/affiliate/withdraw', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_rebate_affiliate_commission_detail = spotPrivateGetRebateAffiliateCommissionDetail = Entry('rebate/affiliate/commission/detail', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_mxdeduct_enable = spotPrivateGetMxDeductEnable = Entry('mxDeduct/enable', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_userdatastream = spotPrivateGetUserDataStream = Entry('userDataStream', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_selfsymbols = spotPrivateGetSelfSymbols = Entry('selfSymbols', ['spot', 'private'], 'GET', {'cost': 1}) + spot_private_get_asset_internal_transfer_record = spotPrivateGetAssetInternalTransferRecord = Entry('asset/internal/transfer/record', ['spot', 'private'], 'GET', {'cost': 10}) + spot_private_post_order = spotPrivatePostOrder = Entry('order', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_post_order_test = spotPrivatePostOrderTest = Entry('order/test', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_post_sub_account_virtualsubaccount = spotPrivatePostSubAccountVirtualSubAccount = Entry('sub-account/virtualSubAccount', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_post_sub_account_apikey = spotPrivatePostSubAccountApiKey = Entry('sub-account/apiKey', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_post_sub_account_futures = spotPrivatePostSubAccountFutures = Entry('sub-account/futures', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_post_sub_account_margin = spotPrivatePostSubAccountMargin = Entry('sub-account/margin', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_post_batchorders = spotPrivatePostBatchOrders = Entry('batchOrders', ['spot', 'private'], 'POST', {'cost': 10}) + spot_private_post_capital_withdraw_apply = spotPrivatePostCapitalWithdrawApply = Entry('capital/withdraw/apply', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_post_capital_withdraw = spotPrivatePostCapitalWithdraw = Entry('capital/withdraw', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_post_capital_transfer = spotPrivatePostCapitalTransfer = Entry('capital/transfer', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_post_capital_transfer_internal = spotPrivatePostCapitalTransferInternal = Entry('capital/transfer/internal', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_post_capital_deposit_address = spotPrivatePostCapitalDepositAddress = Entry('capital/deposit/address', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_post_capital_sub_account_universaltransfer = spotPrivatePostCapitalSubAccountUniversalTransfer = Entry('capital/sub-account/universalTransfer', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_post_capital_convert = spotPrivatePostCapitalConvert = Entry('capital/convert', ['spot', 'private'], 'POST', {'cost': 10}) + spot_private_post_mxdeduct_enable = spotPrivatePostMxDeductEnable = Entry('mxDeduct/enable', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_post_userdatastream = spotPrivatePostUserDataStream = Entry('userDataStream', ['spot', 'private'], 'POST', {'cost': 1}) + spot_private_put_userdatastream = spotPrivatePutUserDataStream = Entry('userDataStream', ['spot', 'private'], 'PUT', {'cost': 1}) + spot_private_delete_order = spotPrivateDeleteOrder = Entry('order', ['spot', 'private'], 'DELETE', {'cost': 1}) + spot_private_delete_openorders = spotPrivateDeleteOpenOrders = Entry('openOrders', ['spot', 'private'], 'DELETE', {'cost': 1}) + spot_private_delete_sub_account_apikey = spotPrivateDeleteSubAccountApiKey = Entry('sub-account/apiKey', ['spot', 'private'], 'DELETE', {'cost': 1}) + spot_private_delete_margin_order = spotPrivateDeleteMarginOrder = Entry('margin/order', ['spot', 'private'], 'DELETE', {'cost': 1}) + spot_private_delete_margin_openorders = spotPrivateDeleteMarginOpenOrders = Entry('margin/openOrders', ['spot', 'private'], 'DELETE', {'cost': 1}) + spot_private_delete_userdatastream = spotPrivateDeleteUserDataStream = Entry('userDataStream', ['spot', 'private'], 'DELETE', {'cost': 1}) + spot_private_delete_capital_withdraw = spotPrivateDeleteCapitalWithdraw = Entry('capital/withdraw', ['spot', 'private'], 'DELETE', {'cost': 1}) + contract_public_get_ping = contractPublicGetPing = Entry('ping', ['contract', 'public'], 'GET', {'cost': 2}) + contract_public_get_detail = contractPublicGetDetail = Entry('detail', ['contract', 'public'], 'GET', {'cost': 100}) + contract_public_get_support_currencies = contractPublicGetSupportCurrencies = Entry('support_currencies', ['contract', 'public'], 'GET', {'cost': 2}) + contract_public_get_depth_symbol = contractPublicGetDepthSymbol = Entry('depth/{symbol}', ['contract', 'public'], 'GET', {'cost': 2}) + contract_public_get_depth_commits_symbol_limit = contractPublicGetDepthCommitsSymbolLimit = Entry('depth_commits/{symbol}/{limit}', ['contract', 'public'], 'GET', {'cost': 2}) + contract_public_get_index_price_symbol = contractPublicGetIndexPriceSymbol = Entry('index_price/{symbol}', ['contract', 'public'], 'GET', {'cost': 2}) + contract_public_get_fair_price_symbol = contractPublicGetFairPriceSymbol = Entry('fair_price/{symbol}', ['contract', 'public'], 'GET', {'cost': 2}) + contract_public_get_funding_rate_symbol = contractPublicGetFundingRateSymbol = Entry('funding_rate/{symbol}', ['contract', 'public'], 'GET', {'cost': 2}) + contract_public_get_kline_symbol = contractPublicGetKlineSymbol = Entry('kline/{symbol}', ['contract', 'public'], 'GET', {'cost': 2}) + contract_public_get_kline_index_price_symbol = contractPublicGetKlineIndexPriceSymbol = Entry('kline/index_price/{symbol}', ['contract', 'public'], 'GET', {'cost': 2}) + contract_public_get_kline_fair_price_symbol = contractPublicGetKlineFairPriceSymbol = Entry('kline/fair_price/{symbol}', ['contract', 'public'], 'GET', {'cost': 2}) + contract_public_get_deals_symbol = contractPublicGetDealsSymbol = Entry('deals/{symbol}', ['contract', 'public'], 'GET', {'cost': 2}) + contract_public_get_ticker = contractPublicGetTicker = Entry('ticker', ['contract', 'public'], 'GET', {'cost': 2}) + contract_public_get_risk_reverse = contractPublicGetRiskReverse = Entry('risk_reverse', ['contract', 'public'], 'GET', {'cost': 2}) + contract_public_get_risk_reverse_history = contractPublicGetRiskReverseHistory = Entry('risk_reverse/history', ['contract', 'public'], 'GET', {'cost': 2}) + contract_public_get_funding_rate_history = contractPublicGetFundingRateHistory = Entry('funding_rate/history', ['contract', 'public'], 'GET', {'cost': 2}) + contract_private_get_account_assets = contractPrivateGetAccountAssets = Entry('account/assets', ['contract', 'private'], 'GET', {'cost': 2}) + contract_private_get_account_asset_currency = contractPrivateGetAccountAssetCurrency = Entry('account/asset/{currency}', ['contract', 'private'], 'GET', {'cost': 2}) + contract_private_get_account_transfer_record = contractPrivateGetAccountTransferRecord = Entry('account/transfer_record', ['contract', 'private'], 'GET', {'cost': 2}) + contract_private_get_position_list_history_positions = contractPrivateGetPositionListHistoryPositions = Entry('position/list/history_positions', ['contract', 'private'], 'GET', {'cost': 2}) + contract_private_get_position_open_positions = contractPrivateGetPositionOpenPositions = Entry('position/open_positions', ['contract', 'private'], 'GET', {'cost': 2}) + contract_private_get_position_funding_records = contractPrivateGetPositionFundingRecords = Entry('position/funding_records', ['contract', 'private'], 'GET', {'cost': 2}) + contract_private_get_position_position_mode = contractPrivateGetPositionPositionMode = Entry('position/position_mode', ['contract', 'private'], 'GET', {'cost': 2}) + contract_private_get_order_list_open_orders_symbol = contractPrivateGetOrderListOpenOrdersSymbol = Entry('order/list/open_orders/{symbol}', ['contract', 'private'], 'GET', {'cost': 2}) + contract_private_get_order_list_history_orders = contractPrivateGetOrderListHistoryOrders = Entry('order/list/history_orders', ['contract', 'private'], 'GET', {'cost': 2}) + contract_private_get_order_external_symbol_external_oid = contractPrivateGetOrderExternalSymbolExternalOid = Entry('order/external/{symbol}/{external_oid}', ['contract', 'private'], 'GET', {'cost': 2}) + contract_private_get_order_get_order_id = contractPrivateGetOrderGetOrderId = Entry('order/get/{order_id}', ['contract', 'private'], 'GET', {'cost': 2}) + contract_private_get_order_batch_query = contractPrivateGetOrderBatchQuery = Entry('order/batch_query', ['contract', 'private'], 'GET', {'cost': 8}) + contract_private_get_order_deal_details_order_id = contractPrivateGetOrderDealDetailsOrderId = Entry('order/deal_details/{order_id}', ['contract', 'private'], 'GET', {'cost': 2}) + contract_private_get_order_list_order_deals = contractPrivateGetOrderListOrderDeals = Entry('order/list/order_deals', ['contract', 'private'], 'GET', {'cost': 2}) + contract_private_get_planorder_list_orders = contractPrivateGetPlanorderListOrders = Entry('planorder/list/orders', ['contract', 'private'], 'GET', {'cost': 2}) + contract_private_get_stoporder_list_orders = contractPrivateGetStoporderListOrders = Entry('stoporder/list/orders', ['contract', 'private'], 'GET', {'cost': 2}) + contract_private_get_stoporder_order_details_stop_order_id = contractPrivateGetStoporderOrderDetailsStopOrderId = Entry('stoporder/order_details/{stop_order_id}', ['contract', 'private'], 'GET', {'cost': 2}) + contract_private_get_account_risk_limit = contractPrivateGetAccountRiskLimit = Entry('account/risk_limit', ['contract', 'private'], 'GET', {'cost': 2}) + contract_private_get_account_tiered_fee_rate = contractPrivateGetAccountTieredFeeRate = Entry('account/tiered_fee_rate', ['contract', 'private'], 'GET', {'cost': 2}) + contract_private_get_position_leverage = contractPrivateGetPositionLeverage = Entry('position/leverage', ['contract', 'private'], 'GET', {'cost': 2}) + contract_private_post_position_change_margin = contractPrivatePostPositionChangeMargin = Entry('position/change_margin', ['contract', 'private'], 'POST', {'cost': 2}) + contract_private_post_position_change_leverage = contractPrivatePostPositionChangeLeverage = Entry('position/change_leverage', ['contract', 'private'], 'POST', {'cost': 2}) + contract_private_post_position_change_position_mode = contractPrivatePostPositionChangePositionMode = Entry('position/change_position_mode', ['contract', 'private'], 'POST', {'cost': 2}) + contract_private_post_order_submit = contractPrivatePostOrderSubmit = Entry('order/submit', ['contract', 'private'], 'POST', {'cost': 2}) + contract_private_post_order_submit_batch = contractPrivatePostOrderSubmitBatch = Entry('order/submit_batch', ['contract', 'private'], 'POST', {'cost': 40}) + contract_private_post_order_cancel = contractPrivatePostOrderCancel = Entry('order/cancel', ['contract', 'private'], 'POST', {'cost': 2}) + contract_private_post_order_cancel_with_external = contractPrivatePostOrderCancelWithExternal = Entry('order/cancel_with_external', ['contract', 'private'], 'POST', {'cost': 2}) + contract_private_post_order_cancel_all = contractPrivatePostOrderCancelAll = Entry('order/cancel_all', ['contract', 'private'], 'POST', {'cost': 2}) + contract_private_post_account_change_risk_level = contractPrivatePostAccountChangeRiskLevel = Entry('account/change_risk_level', ['contract', 'private'], 'POST', {'cost': 2}) + contract_private_post_planorder_place = contractPrivatePostPlanorderPlace = Entry('planorder/place', ['contract', 'private'], 'POST', {'cost': 2}) + contract_private_post_planorder_cancel = contractPrivatePostPlanorderCancel = Entry('planorder/cancel', ['contract', 'private'], 'POST', {'cost': 2}) + contract_private_post_planorder_cancel_all = contractPrivatePostPlanorderCancelAll = Entry('planorder/cancel_all', ['contract', 'private'], 'POST', {'cost': 2}) + contract_private_post_stoporder_cancel = contractPrivatePostStoporderCancel = Entry('stoporder/cancel', ['contract', 'private'], 'POST', {'cost': 2}) + contract_private_post_stoporder_cancel_all = contractPrivatePostStoporderCancelAll = Entry('stoporder/cancel_all', ['contract', 'private'], 'POST', {'cost': 2}) + contract_private_post_stoporder_change_price = contractPrivatePostStoporderChangePrice = Entry('stoporder/change_price', ['contract', 'private'], 'POST', {'cost': 2}) + contract_private_post_stoporder_change_plan_price = contractPrivatePostStoporderChangePlanPrice = Entry('stoporder/change_plan_price', ['contract', 'private'], 'POST', {'cost': 2}) + spot2_public_get_market_symbols = spot2PublicGetMarketSymbols = Entry('market/symbols', ['spot2', 'public'], 'GET', {'cost': 1}) + spot2_public_get_market_coin_list = spot2PublicGetMarketCoinList = Entry('market/coin/list', ['spot2', 'public'], 'GET', {'cost': 2}) + spot2_public_get_common_timestamp = spot2PublicGetCommonTimestamp = Entry('common/timestamp', ['spot2', 'public'], 'GET', {'cost': 1}) + spot2_public_get_common_ping = spot2PublicGetCommonPing = Entry('common/ping', ['spot2', 'public'], 'GET', {'cost': 2}) + spot2_public_get_market_ticker = spot2PublicGetMarketTicker = Entry('market/ticker', ['spot2', 'public'], 'GET', {'cost': 1}) + spot2_public_get_market_depth = spot2PublicGetMarketDepth = Entry('market/depth', ['spot2', 'public'], 'GET', {'cost': 1}) + spot2_public_get_market_deals = spot2PublicGetMarketDeals = Entry('market/deals', ['spot2', 'public'], 'GET', {'cost': 1}) + spot2_public_get_market_kline = spot2PublicGetMarketKline = Entry('market/kline', ['spot2', 'public'], 'GET', {'cost': 1}) + spot2_public_get_market_api_default_symbols = spot2PublicGetMarketApiDefaultSymbols = Entry('market/api_default_symbols', ['spot2', 'public'], 'GET', {'cost': 2}) + spot2_private_get_account_info = spot2PrivateGetAccountInfo = Entry('account/info', ['spot2', 'private'], 'GET', {'cost': 1}) + spot2_private_get_order_open_orders = spot2PrivateGetOrderOpenOrders = Entry('order/open_orders', ['spot2', 'private'], 'GET', {'cost': 1}) + spot2_private_get_order_list = spot2PrivateGetOrderList = Entry('order/list', ['spot2', 'private'], 'GET', {'cost': 1}) + spot2_private_get_order_query = spot2PrivateGetOrderQuery = Entry('order/query', ['spot2', 'private'], 'GET', {'cost': 1}) + spot2_private_get_order_deals = spot2PrivateGetOrderDeals = Entry('order/deals', ['spot2', 'private'], 'GET', {'cost': 1}) + spot2_private_get_order_deal_detail = spot2PrivateGetOrderDealDetail = Entry('order/deal_detail', ['spot2', 'private'], 'GET', {'cost': 1}) + spot2_private_get_asset_deposit_address_list = spot2PrivateGetAssetDepositAddressList = Entry('asset/deposit/address/list', ['spot2', 'private'], 'GET', {'cost': 2}) + spot2_private_get_asset_deposit_list = spot2PrivateGetAssetDepositList = Entry('asset/deposit/list', ['spot2', 'private'], 'GET', {'cost': 2}) + spot2_private_get_asset_address_list = spot2PrivateGetAssetAddressList = Entry('asset/address/list', ['spot2', 'private'], 'GET', {'cost': 2}) + spot2_private_get_asset_withdraw_list = spot2PrivateGetAssetWithdrawList = Entry('asset/withdraw/list', ['spot2', 'private'], 'GET', {'cost': 2}) + spot2_private_get_asset_internal_transfer_record = spot2PrivateGetAssetInternalTransferRecord = Entry('asset/internal/transfer/record', ['spot2', 'private'], 'GET', {'cost': 10}) + spot2_private_get_account_balance = spot2PrivateGetAccountBalance = Entry('account/balance', ['spot2', 'private'], 'GET', {'cost': 10}) + spot2_private_get_asset_internal_transfer_info = spot2PrivateGetAssetInternalTransferInfo = Entry('asset/internal/transfer/info', ['spot2', 'private'], 'GET', {'cost': 10}) + spot2_private_get_market_api_symbols = spot2PrivateGetMarketApiSymbols = Entry('market/api_symbols', ['spot2', 'private'], 'GET', {'cost': 2}) + spot2_private_post_order_place = spot2PrivatePostOrderPlace = Entry('order/place', ['spot2', 'private'], 'POST', {'cost': 1}) + spot2_private_post_order_place_batch = spot2PrivatePostOrderPlaceBatch = Entry('order/place_batch', ['spot2', 'private'], 'POST', {'cost': 1}) + spot2_private_post_order_advanced_place_batch = spot2PrivatePostOrderAdvancedPlaceBatch = Entry('order/advanced/place_batch', ['spot2', 'private'], 'POST', {'cost': 1}) + spot2_private_post_asset_withdraw = spot2PrivatePostAssetWithdraw = Entry('asset/withdraw', ['spot2', 'private'], 'POST', {'cost': 2}) + spot2_private_post_asset_internal_transfer = spot2PrivatePostAssetInternalTransfer = Entry('asset/internal/transfer', ['spot2', 'private'], 'POST', {'cost': 10}) + spot2_private_delete_order_cancel = spot2PrivateDeleteOrderCancel = Entry('order/cancel', ['spot2', 'private'], 'DELETE', {'cost': 1}) + spot2_private_delete_order_cancel_by_symbol = spot2PrivateDeleteOrderCancelBySymbol = Entry('order/cancel_by_symbol', ['spot2', 'private'], 'DELETE', {'cost': 1}) + spot2_private_delete_asset_withdraw = spot2PrivateDeleteAssetWithdraw = Entry('asset/withdraw', ['spot2', 'private'], 'DELETE', {'cost': 2}) + broker_private_get_sub_account_universaltransfer = brokerPrivateGetSubAccountUniversalTransfer = Entry('sub-account/universalTransfer', ['broker', 'private'], 'GET', {'cost': 1}) + broker_private_get_sub_account_list = brokerPrivateGetSubAccountList = Entry('sub-account/list', ['broker', 'private'], 'GET', {'cost': 1}) + broker_private_get_sub_account_apikey = brokerPrivateGetSubAccountApiKey = Entry('sub-account/apiKey', ['broker', 'private'], 'GET', {'cost': 1}) + broker_private_get_capital_deposit_subaddress = brokerPrivateGetCapitalDepositSubAddress = Entry('capital/deposit/subAddress', ['broker', 'private'], 'GET', {'cost': 1}) + broker_private_get_capital_deposit_subhisrec = brokerPrivateGetCapitalDepositSubHisrec = Entry('capital/deposit/subHisrec', ['broker', 'private'], 'GET', {'cost': 1}) + broker_private_get_capital_deposit_subhisrec_getall = brokerPrivateGetCapitalDepositSubHisrecGetall = Entry('capital/deposit/subHisrec/getall', ['broker', 'private'], 'GET', {'cost': 1}) + broker_private_post_sub_account_virtualsubaccount = brokerPrivatePostSubAccountVirtualSubAccount = Entry('sub-account/virtualSubAccount', ['broker', 'private'], 'POST', {'cost': 1}) + broker_private_post_sub_account_apikey = brokerPrivatePostSubAccountApiKey = Entry('sub-account/apiKey', ['broker', 'private'], 'POST', {'cost': 1}) + broker_private_post_capital_deposit_subaddress = brokerPrivatePostCapitalDepositSubAddress = Entry('capital/deposit/subAddress', ['broker', 'private'], 'POST', {'cost': 1}) + broker_private_post_capital_withdraw_apply = brokerPrivatePostCapitalWithdrawApply = Entry('capital/withdraw/apply', ['broker', 'private'], 'POST', {'cost': 1}) + broker_private_post_sub_account_universaltransfer = brokerPrivatePostSubAccountUniversalTransfer = Entry('sub-account/universalTransfer', ['broker', 'private'], 'POST', {'cost': 1}) + broker_private_post_sub_account_futures = brokerPrivatePostSubAccountFutures = Entry('sub-account/futures', ['broker', 'private'], 'POST', {'cost': 1}) + broker_private_delete_sub_account_apikey = brokerPrivateDeleteSubAccountApiKey = Entry('sub-account/apiKey', ['broker', 'private'], 'DELETE', {'cost': 1}) diff --git a/ccxt/abstract/modetrade.py b/ccxt/abstract/modetrade.py new file mode 100644 index 0000000..ff60ce5 --- /dev/null +++ b/ccxt/abstract/modetrade.py @@ -0,0 +1,119 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + v1_public_get_public_volume_stats = v1PublicGetPublicVolumeStats = Entry('public/volume/stats', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_public_broker_name = v1PublicGetPublicBrokerName = Entry('public/broker/name', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_public_chain_info_broker_id = v1PublicGetPublicChainInfoBrokerId = Entry('public/chain_info/{broker_id}', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_public_system_info = v1PublicGetPublicSystemInfo = Entry('public/system_info', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_public_vault_balance = v1PublicGetPublicVaultBalance = Entry('public/vault_balance', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_public_insurancefund = v1PublicGetPublicInsurancefund = Entry('public/insurancefund', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_public_chain_info = v1PublicGetPublicChainInfo = Entry('public/chain_info', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_faucet_usdc = v1PublicGetFaucetUsdc = Entry('faucet/usdc', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_public_account = v1PublicGetPublicAccount = Entry('public/account', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_get_account = v1PublicGetGetAccount = Entry('get_account', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_registration_nonce = v1PublicGetRegistrationNonce = Entry('registration_nonce', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_get_orderly_key = v1PublicGetGetOrderlyKey = Entry('get_orderly_key', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_public_liquidation = v1PublicGetPublicLiquidation = Entry('public/liquidation', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_public_liquidated_positions = v1PublicGetPublicLiquidatedPositions = Entry('public/liquidated_positions', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_public_config = v1PublicGetPublicConfig = Entry('public/config', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_public_campaign_ranking = v1PublicGetPublicCampaignRanking = Entry('public/campaign/ranking', ['v1', 'public'], 'GET', {'cost': 10}) + v1_public_get_public_campaign_stats = v1PublicGetPublicCampaignStats = Entry('public/campaign/stats', ['v1', 'public'], 'GET', {'cost': 10}) + v1_public_get_public_campaign_user = v1PublicGetPublicCampaignUser = Entry('public/campaign/user', ['v1', 'public'], 'GET', {'cost': 10}) + v1_public_get_public_campaign_stats_details = v1PublicGetPublicCampaignStatsDetails = Entry('public/campaign/stats/details', ['v1', 'public'], 'GET', {'cost': 10}) + v1_public_get_public_campaigns = v1PublicGetPublicCampaigns = Entry('public/campaigns', ['v1', 'public'], 'GET', {'cost': 10}) + v1_public_get_public_points_leaderboard = v1PublicGetPublicPointsLeaderboard = Entry('public/points/leaderboard', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_client_points = v1PublicGetClientPoints = Entry('client/points', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_public_points_epoch = v1PublicGetPublicPointsEpoch = Entry('public/points/epoch', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_public_points_epoch_dates = v1PublicGetPublicPointsEpochDates = Entry('public/points/epoch_dates', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_public_referral_check_ref_code = v1PublicGetPublicReferralCheckRefCode = Entry('public/referral/check_ref_code', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_public_referral_verify_ref_code = v1PublicGetPublicReferralVerifyRefCode = Entry('public/referral/verify_ref_code', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_referral_admin_info = v1PublicGetReferralAdminInfo = Entry('referral/admin_info', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_referral_info = v1PublicGetReferralInfo = Entry('referral/info', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_referral_referee_info = v1PublicGetReferralRefereeInfo = Entry('referral/referee_info', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_referral_referee_rebate_summary = v1PublicGetReferralRefereeRebateSummary = Entry('referral/referee_rebate_summary', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_referral_referee_history = v1PublicGetReferralRefereeHistory = Entry('referral/referee_history', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_referral_referral_history = v1PublicGetReferralReferralHistory = Entry('referral/referral_history', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_referral_rebate_summary = v1PublicGetReferralRebateSummary = Entry('referral/rebate_summary', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_client_distribution_history = v1PublicGetClientDistributionHistory = Entry('client/distribution_history', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_tv_config = v1PublicGetTvConfig = Entry('tv/config', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_tv_history = v1PublicGetTvHistory = Entry('tv/history', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_tv_symbol_info = v1PublicGetTvSymbolInfo = Entry('tv/symbol_info', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_public_funding_rate_history = v1PublicGetPublicFundingRateHistory = Entry('public/funding_rate_history', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_public_funding_rate_symbol = v1PublicGetPublicFundingRateSymbol = Entry('public/funding_rate/{symbol}', ['v1', 'public'], 'GET', {'cost': 0.33}) + v1_public_get_public_funding_rates = v1PublicGetPublicFundingRates = Entry('public/funding_rates', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_public_info = v1PublicGetPublicInfo = Entry('public/info', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_public_info_symbol = v1PublicGetPublicInfoSymbol = Entry('public/info/{symbol}', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_public_market_trades = v1PublicGetPublicMarketTrades = Entry('public/market_trades', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_public_token = v1PublicGetPublicToken = Entry('public/token', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_public_futures = v1PublicGetPublicFutures = Entry('public/futures', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_public_futures_symbol = v1PublicGetPublicFuturesSymbol = Entry('public/futures/{symbol}', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_post_register_account = v1PublicPostRegisterAccount = Entry('register_account', ['v1', 'public'], 'POST', {'cost': 1}) + v1_private_get_client_key_info = v1PrivateGetClientKeyInfo = Entry('client/key_info', ['v1', 'private'], 'GET', {'cost': 6}) + v1_private_get_client_orderly_key_ip_restriction = v1PrivateGetClientOrderlyKeyIpRestriction = Entry('client/orderly_key_ip_restriction', ['v1', 'private'], 'GET', {'cost': 6}) + v1_private_get_order_oid = v1PrivateGetOrderOid = Entry('order/{oid}', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_client_order_client_order_id = v1PrivateGetClientOrderClientOrderId = Entry('client/order/{client_order_id}', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_algo_order_oid = v1PrivateGetAlgoOrderOid = Entry('algo/order/{oid}', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_algo_client_order_client_order_id = v1PrivateGetAlgoClientOrderClientOrderId = Entry('algo/client/order/{client_order_id}', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_orders = v1PrivateGetOrders = Entry('orders', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_algo_orders = v1PrivateGetAlgoOrders = Entry('algo/orders', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_trade_tid = v1PrivateGetTradeTid = Entry('trade/{tid}', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_trades = v1PrivateGetTrades = Entry('trades', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_order_oid_trades = v1PrivateGetOrderOidTrades = Entry('order/{oid}/trades', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_client_liquidator_liquidations = v1PrivateGetClientLiquidatorLiquidations = Entry('client/liquidator_liquidations', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_liquidations = v1PrivateGetLiquidations = Entry('liquidations', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_asset_history = v1PrivateGetAssetHistory = Entry('asset/history', ['v1', 'private'], 'GET', {'cost': 60}) + v1_private_get_client_holding = v1PrivateGetClientHolding = Entry('client/holding', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_withdraw_nonce = v1PrivateGetWithdrawNonce = Entry('withdraw_nonce', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_settle_nonce = v1PrivateGetSettleNonce = Entry('settle_nonce', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_pnl_settlement_history = v1PrivateGetPnlSettlementHistory = Entry('pnl_settlement/history', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_volume_user_daily = v1PrivateGetVolumeUserDaily = Entry('volume/user/daily', ['v1', 'private'], 'GET', {'cost': 60}) + v1_private_get_volume_user_stats = v1PrivateGetVolumeUserStats = Entry('volume/user/stats', ['v1', 'private'], 'GET', {'cost': 60}) + v1_private_get_client_statistics = v1PrivateGetClientStatistics = Entry('client/statistics', ['v1', 'private'], 'GET', {'cost': 60}) + v1_private_get_client_info = v1PrivateGetClientInfo = Entry('client/info', ['v1', 'private'], 'GET', {'cost': 60}) + v1_private_get_client_statistics_daily = v1PrivateGetClientStatisticsDaily = Entry('client/statistics/daily', ['v1', 'private'], 'GET', {'cost': 60}) + v1_private_get_positions = v1PrivateGetPositions = Entry('positions', ['v1', 'private'], 'GET', {'cost': 3.33}) + v1_private_get_position_symbol = v1PrivateGetPositionSymbol = Entry('position/{symbol}', ['v1', 'private'], 'GET', {'cost': 3.33}) + v1_private_get_funding_fee_history = v1PrivateGetFundingFeeHistory = Entry('funding_fee/history', ['v1', 'private'], 'GET', {'cost': 30}) + v1_private_get_notification_inbox_notifications = v1PrivateGetNotificationInboxNotifications = Entry('notification/inbox/notifications', ['v1', 'private'], 'GET', {'cost': 60}) + v1_private_get_notification_inbox_unread = v1PrivateGetNotificationInboxUnread = Entry('notification/inbox/unread', ['v1', 'private'], 'GET', {'cost': 60}) + v1_private_get_volume_broker_daily = v1PrivateGetVolumeBrokerDaily = Entry('volume/broker/daily', ['v1', 'private'], 'GET', {'cost': 60}) + v1_private_get_broker_fee_rate_default = v1PrivateGetBrokerFeeRateDefault = Entry('broker/fee_rate/default', ['v1', 'private'], 'GET', {'cost': 10}) + v1_private_get_broker_user_info = v1PrivateGetBrokerUserInfo = Entry('broker/user_info', ['v1', 'private'], 'GET', {'cost': 10}) + v1_private_get_orderbook_symbol = v1PrivateGetOrderbookSymbol = Entry('orderbook/{symbol}', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_kline = v1PrivateGetKline = Entry('kline', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_post_orderly_key = v1PrivatePostOrderlyKey = Entry('orderly_key', ['v1', 'private'], 'POST', {'cost': 1}) + v1_private_post_client_set_orderly_key_ip_restriction = v1PrivatePostClientSetOrderlyKeyIpRestriction = Entry('client/set_orderly_key_ip_restriction', ['v1', 'private'], 'POST', {'cost': 6}) + v1_private_post_client_reset_orderly_key_ip_restriction = v1PrivatePostClientResetOrderlyKeyIpRestriction = Entry('client/reset_orderly_key_ip_restriction', ['v1', 'private'], 'POST', {'cost': 6}) + v1_private_post_order = v1PrivatePostOrder = Entry('order', ['v1', 'private'], 'POST', {'cost': 1}) + v1_private_post_batch_order = v1PrivatePostBatchOrder = Entry('batch-order', ['v1', 'private'], 'POST', {'cost': 10}) + v1_private_post_algo_order = v1PrivatePostAlgoOrder = Entry('algo/order', ['v1', 'private'], 'POST', {'cost': 1}) + v1_private_post_liquidation = v1PrivatePostLiquidation = Entry('liquidation', ['v1', 'private'], 'POST', {'cost': 1}) + v1_private_post_claim_insurance_fund = v1PrivatePostClaimInsuranceFund = Entry('claim_insurance_fund', ['v1', 'private'], 'POST', {'cost': 1}) + v1_private_post_withdraw_request = v1PrivatePostWithdrawRequest = Entry('withdraw_request', ['v1', 'private'], 'POST', {'cost': 1}) + v1_private_post_settle_pnl = v1PrivatePostSettlePnl = Entry('settle_pnl', ['v1', 'private'], 'POST', {'cost': 1}) + v1_private_post_notification_inbox_mark_read = v1PrivatePostNotificationInboxMarkRead = Entry('notification/inbox/mark_read', ['v1', 'private'], 'POST', {'cost': 60}) + v1_private_post_notification_inbox_mark_read_all = v1PrivatePostNotificationInboxMarkReadAll = Entry('notification/inbox/mark_read_all', ['v1', 'private'], 'POST', {'cost': 60}) + v1_private_post_client_leverage = v1PrivatePostClientLeverage = Entry('client/leverage', ['v1', 'private'], 'POST', {'cost': 120}) + v1_private_post_client_maintenance_config = v1PrivatePostClientMaintenanceConfig = Entry('client/maintenance_config', ['v1', 'private'], 'POST', {'cost': 60}) + v1_private_post_delegate_signer = v1PrivatePostDelegateSigner = Entry('delegate_signer', ['v1', 'private'], 'POST', {'cost': 10}) + v1_private_post_delegate_orderly_key = v1PrivatePostDelegateOrderlyKey = Entry('delegate_orderly_key', ['v1', 'private'], 'POST', {'cost': 10}) + v1_private_post_delegate_settle_pnl = v1PrivatePostDelegateSettlePnl = Entry('delegate_settle_pnl', ['v1', 'private'], 'POST', {'cost': 10}) + v1_private_post_delegate_withdraw_request = v1PrivatePostDelegateWithdrawRequest = Entry('delegate_withdraw_request', ['v1', 'private'], 'POST', {'cost': 10}) + v1_private_post_broker_fee_rate_set = v1PrivatePostBrokerFeeRateSet = Entry('broker/fee_rate/set', ['v1', 'private'], 'POST', {'cost': 10}) + v1_private_post_broker_fee_rate_set_default = v1PrivatePostBrokerFeeRateSetDefault = Entry('broker/fee_rate/set_default', ['v1', 'private'], 'POST', {'cost': 10}) + v1_private_post_broker_fee_rate_default = v1PrivatePostBrokerFeeRateDefault = Entry('broker/fee_rate/default', ['v1', 'private'], 'POST', {'cost': 10}) + v1_private_post_referral_create = v1PrivatePostReferralCreate = Entry('referral/create', ['v1', 'private'], 'POST', {'cost': 10}) + v1_private_post_referral_update = v1PrivatePostReferralUpdate = Entry('referral/update', ['v1', 'private'], 'POST', {'cost': 10}) + v1_private_post_referral_bind = v1PrivatePostReferralBind = Entry('referral/bind', ['v1', 'private'], 'POST', {'cost': 10}) + v1_private_post_referral_edit_split = v1PrivatePostReferralEditSplit = Entry('referral/edit_split', ['v1', 'private'], 'POST', {'cost': 10}) + v1_private_put_order = v1PrivatePutOrder = Entry('order', ['v1', 'private'], 'PUT', {'cost': 1}) + v1_private_put_algo_order = v1PrivatePutAlgoOrder = Entry('algo/order', ['v1', 'private'], 'PUT', {'cost': 1}) + v1_private_delete_order = v1PrivateDeleteOrder = Entry('order', ['v1', 'private'], 'DELETE', {'cost': 1}) + v1_private_delete_algo_order = v1PrivateDeleteAlgoOrder = Entry('algo/order', ['v1', 'private'], 'DELETE', {'cost': 1}) + v1_private_delete_client_order = v1PrivateDeleteClientOrder = Entry('client/order', ['v1', 'private'], 'DELETE', {'cost': 1}) + v1_private_delete_algo_client_order = v1PrivateDeleteAlgoClientOrder = Entry('algo/client/order', ['v1', 'private'], 'DELETE', {'cost': 1}) + v1_private_delete_algo_orders = v1PrivateDeleteAlgoOrders = Entry('algo/orders', ['v1', 'private'], 'DELETE', {'cost': 1}) + v1_private_delete_orders = v1PrivateDeleteOrders = Entry('orders', ['v1', 'private'], 'DELETE', {'cost': 1}) + v1_private_delete_batch_order = v1PrivateDeleteBatchOrder = Entry('batch-order', ['v1', 'private'], 'DELETE', {'cost': 1}) + v1_private_delete_client_batch_order = v1PrivateDeleteClientBatchOrder = Entry('client/batch-order', ['v1', 'private'], 'DELETE', {'cost': 1}) diff --git a/ccxt/abstract/mt5.py b/ccxt/abstract/mt5.py new file mode 100644 index 0000000..87ed9d3 --- /dev/null +++ b/ccxt/abstract/mt5.py @@ -0,0 +1,25 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_ping = publicGetPing = Entry('ping', 'public', 'GET', {'cost': 1}) + + private_get_connect = privateGetConnect = Entry('Connect', 'private', 'GET', {'cost': 1}) + private_get_checkconnect = privateGetCheckConnect = Entry('CheckConnect', 'private', 'GET', {'cost': 1}) + private_get_disconnect = privateGetDisconnect = Entry('Disconnect', 'private', 'GET', {'cost': 1}) + private_get_symbols = privateGetSymbols = Entry('Symbols', 'private', 'GET', {'cost': 1}) + private_get_getquote = privateGetGetQuote = Entry('GetQuote', 'private', 'GET', {'cost': 1}) + private_get_getquotemany = privateGetGetQuoteMany = Entry('GetQuoteMany', 'private', 'GET', {'cost': 1}) + private_get_servertimezone = privateGetServerTimezone = Entry('ServerTimezone', 'private', 'GET', {'cost': 1}) + private_get_accountsummary = privateGetAccountSummary = Entry('AccountSummary', 'private', 'GET', {'cost': 1}) + private_get_accountdetails = privateGetAccountDetails = Entry('AccountDetails', 'private', 'GET', {'cost': 1}) + private_get_symbolList = privateGetSymbolList = Entry('SymbolList', 'private', 'GET', {'cost': 1}) + private_get_marketwatchmany = privateGetMarketWatchMany = Entry('MarketWatchMany', 'private', 'GET', {'cost': 1}) + private_get_openedorders = privateGetOpenedOrders = Entry('OpenedOrders', 'private', 'GET', {'cost': 1}) + private_get_closedorders = privateGetClosedOrders = Entry('ClosedOrders', 'private', 'GET', {'cost': 1}) + private_get_openedorder = privateGetOpenedOrder = Entry('OpenedOrder', 'private', 'GET', {'cost': 1}) + private_get_orderhistory = privateGetOrderHistory = Entry('OrderHistory', 'private', 'GET', {'cost': 1}) + private_get_pricehistory = privateGetPriceHistory = Entry('PriceHistory', 'private', 'GET', {'cost': 1}) + private_get_ordersend = privateGetOrderSend = Entry('OrderSend', 'private', 'GET', {'cost': 1}) + private_get_ordermodify = privateGetOrderModify = Entry('OrderModify', 'private', 'GET', {'cost': 1}) + private_get_orderclose = privateGetOrderClose = Entry('OrderClose', 'private', 'GET', {'cost': 1}) \ No newline at end of file diff --git a/ccxt/abstract/myokx.py b/ccxt/abstract/myokx.py new file mode 100644 index 0000000..ac9e2a1 --- /dev/null +++ b/ccxt/abstract/myokx.py @@ -0,0 +1,351 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_market_books_full = publicGetMarketBooksFull = Entry('market/books-full', 'public', 'GET', {'cost': 2}) + public_get_market_tickers = publicGetMarketTickers = Entry('market/tickers', 'public', 'GET', {'cost': 1}) + public_get_market_ticker = publicGetMarketTicker = Entry('market/ticker', 'public', 'GET', {'cost': 1}) + public_get_market_index_tickers = publicGetMarketIndexTickers = Entry('market/index-tickers', 'public', 'GET', {'cost': 1}) + public_get_market_books = publicGetMarketBooks = Entry('market/books', 'public', 'GET', {'cost': 0.5}) + public_get_market_books_lite = publicGetMarketBooksLite = Entry('market/books-lite', 'public', 'GET', {'cost': 1.6666666666666667}) + public_get_market_candles = publicGetMarketCandles = Entry('market/candles', 'public', 'GET', {'cost': 0.5}) + public_get_market_history_candles = publicGetMarketHistoryCandles = Entry('market/history-candles', 'public', 'GET', {'cost': 1}) + public_get_market_index_candles = publicGetMarketIndexCandles = Entry('market/index-candles', 'public', 'GET', {'cost': 1}) + public_get_market_history_index_candles = publicGetMarketHistoryIndexCandles = Entry('market/history-index-candles', 'public', 'GET', {'cost': 2}) + public_get_market_mark_price_candles = publicGetMarketMarkPriceCandles = Entry('market/mark-price-candles', 'public', 'GET', {'cost': 1}) + public_get_market_history_mark_price_candles = publicGetMarketHistoryMarkPriceCandles = Entry('market/history-mark-price-candles', 'public', 'GET', {'cost': 2}) + public_get_market_trades = publicGetMarketTrades = Entry('market/trades', 'public', 'GET', {'cost': 0.2}) + public_get_market_history_trades = publicGetMarketHistoryTrades = Entry('market/history-trades', 'public', 'GET', {'cost': 2}) + public_get_market_option_instrument_family_trades = publicGetMarketOptionInstrumentFamilyTrades = Entry('market/option/instrument-family-trades', 'public', 'GET', {'cost': 1}) + public_get_market_platform_24_volume = publicGetMarketPlatform24Volume = Entry('market/platform-24-volume', 'public', 'GET', {'cost': 10}) + public_get_market_open_oracle = publicGetMarketOpenOracle = Entry('market/open-oracle', 'public', 'GET', {'cost': 50}) + public_get_market_exchange_rate = publicGetMarketExchangeRate = Entry('market/exchange-rate', 'public', 'GET', {'cost': 20}) + public_get_market_index_components = publicGetMarketIndexComponents = Entry('market/index-components', 'public', 'GET', {'cost': 1}) + public_get_public_market_data_history = publicGetPublicMarketDataHistory = Entry('public/market-data-history', 'public', 'GET', {'cost': 4}) + public_get_public_economic_calendar = publicGetPublicEconomicCalendar = Entry('public/economic-calendar', 'public', 'GET', {'cost': 50}) + public_get_market_block_tickers = publicGetMarketBlockTickers = Entry('market/block-tickers', 'public', 'GET', {'cost': 1}) + public_get_market_block_ticker = publicGetMarketBlockTicker = Entry('market/block-ticker', 'public', 'GET', {'cost': 1}) + public_get_public_block_trades = publicGetPublicBlockTrades = Entry('public/block-trades', 'public', 'GET', {'cost': 1}) + public_get_public_instruments = publicGetPublicInstruments = Entry('public/instruments', 'public', 'GET', {'cost': 1}) + public_get_public_delivery_exercise_history = publicGetPublicDeliveryExerciseHistory = Entry('public/delivery-exercise-history', 'public', 'GET', {'cost': 0.5}) + public_get_public_open_interest = publicGetPublicOpenInterest = Entry('public/open-interest', 'public', 'GET', {'cost': 1}) + public_get_public_funding_rate = publicGetPublicFundingRate = Entry('public/funding-rate', 'public', 'GET', {'cost': 1}) + public_get_public_funding_rate_history = publicGetPublicFundingRateHistory = Entry('public/funding-rate-history', 'public', 'GET', {'cost': 1}) + public_get_public_price_limit = publicGetPublicPriceLimit = Entry('public/price-limit', 'public', 'GET', {'cost': 1}) + public_get_public_opt_summary = publicGetPublicOptSummary = Entry('public/opt-summary', 'public', 'GET', {'cost': 1}) + public_get_public_estimated_price = publicGetPublicEstimatedPrice = Entry('public/estimated-price', 'public', 'GET', {'cost': 2}) + public_get_public_discount_rate_interest_free_quota = publicGetPublicDiscountRateInterestFreeQuota = Entry('public/discount-rate-interest-free-quota', 'public', 'GET', {'cost': 10}) + public_get_public_time = publicGetPublicTime = Entry('public/time', 'public', 'GET', {'cost': 2}) + public_get_public_mark_price = publicGetPublicMarkPrice = Entry('public/mark-price', 'public', 'GET', {'cost': 2}) + public_get_public_position_tiers = publicGetPublicPositionTiers = Entry('public/position-tiers', 'public', 'GET', {'cost': 2}) + public_get_public_interest_rate_loan_quota = publicGetPublicInterestRateLoanQuota = Entry('public/interest-rate-loan-quota', 'public', 'GET', {'cost': 10}) + public_get_public_vip_interest_rate_loan_quota = publicGetPublicVipInterestRateLoanQuota = Entry('public/vip-interest-rate-loan-quota', 'public', 'GET', {'cost': 10}) + public_get_public_underlying = publicGetPublicUnderlying = Entry('public/underlying', 'public', 'GET', {'cost': 1}) + public_get_public_insurance_fund = publicGetPublicInsuranceFund = Entry('public/insurance-fund', 'public', 'GET', {'cost': 2}) + public_get_public_convert_contract_coin = publicGetPublicConvertContractCoin = Entry('public/convert-contract-coin', 'public', 'GET', {'cost': 2}) + public_get_public_option_trades = publicGetPublicOptionTrades = Entry('public/option-trades', 'public', 'GET', {'cost': 1}) + public_get_public_instrument_tick_bands = publicGetPublicInstrumentTickBands = Entry('public/instrument-tick-bands', 'public', 'GET', {'cost': 4}) + public_get_rubik_stat_trading_data_support_coin = publicGetRubikStatTradingDataSupportCoin = Entry('rubik/stat/trading-data/support-coin', 'public', 'GET', {'cost': 4}) + public_get_rubik_stat_taker_volume = publicGetRubikStatTakerVolume = Entry('rubik/stat/taker-volume', 'public', 'GET', {'cost': 4}) + public_get_rubik_stat_margin_loan_ratio = publicGetRubikStatMarginLoanRatio = Entry('rubik/stat/margin/loan-ratio', 'public', 'GET', {'cost': 4}) + public_get_rubik_stat_contracts_long_short_account_ratio = publicGetRubikStatContractsLongShortAccountRatio = Entry('rubik/stat/contracts/long-short-account-ratio', 'public', 'GET', {'cost': 4}) + public_get_rubik_stat_contracts_long_short_account_ratio_contract = publicGetRubikStatContractsLongShortAccountRatioContract = Entry('rubik/stat/contracts/long-short-account-ratio-contract', 'public', 'GET', {'cost': 4}) + public_get_rubik_stat_contracts_open_interest_volume = publicGetRubikStatContractsOpenInterestVolume = Entry('rubik/stat/contracts/open-interest-volume', 'public', 'GET', {'cost': 4}) + public_get_rubik_stat_option_open_interest_volume = publicGetRubikStatOptionOpenInterestVolume = Entry('rubik/stat/option/open-interest-volume', 'public', 'GET', {'cost': 4}) + public_get_rubik_stat_option_open_interest_volume_ratio = publicGetRubikStatOptionOpenInterestVolumeRatio = Entry('rubik/stat/option/open-interest-volume-ratio', 'public', 'GET', {'cost': 4}) + public_get_rubik_stat_option_open_interest_volume_expiry = publicGetRubikStatOptionOpenInterestVolumeExpiry = Entry('rubik/stat/option/open-interest-volume-expiry', 'public', 'GET', {'cost': 4}) + public_get_rubik_stat_option_open_interest_volume_strike = publicGetRubikStatOptionOpenInterestVolumeStrike = Entry('rubik/stat/option/open-interest-volume-strike', 'public', 'GET', {'cost': 4}) + public_get_rubik_stat_option_taker_block_volume = publicGetRubikStatOptionTakerBlockVolume = Entry('rubik/stat/option/taker-block-volume', 'public', 'GET', {'cost': 4}) + public_get_system_status = publicGetSystemStatus = Entry('system/status', 'public', 'GET', {'cost': 50}) + public_get_sprd_spreads = publicGetSprdSpreads = Entry('sprd/spreads', 'public', 'GET', {'cost': 1}) + public_get_sprd_books = publicGetSprdBooks = Entry('sprd/books', 'public', 'GET', {'cost': 0.5}) + public_get_sprd_ticker = publicGetSprdTicker = Entry('sprd/ticker', 'public', 'GET', {'cost': 1}) + public_get_sprd_public_trades = publicGetSprdPublicTrades = Entry('sprd/public-trades', 'public', 'GET', {'cost': 0.2}) + public_get_market_sprd_ticker = publicGetMarketSprdTicker = Entry('market/sprd-ticker', 'public', 'GET', {'cost': 2}) + public_get_market_sprd_candles = publicGetMarketSprdCandles = Entry('market/sprd-candles', 'public', 'GET', {'cost': 2}) + public_get_market_sprd_history_candles = publicGetMarketSprdHistoryCandles = Entry('market/sprd-history-candles', 'public', 'GET', {'cost': 2}) + public_get_tradingbot_grid_ai_param = publicGetTradingBotGridAiParam = Entry('tradingBot/grid/ai-param', 'public', 'GET', {'cost': 1}) + public_get_tradingbot_grid_min_investment = publicGetTradingBotGridMinInvestment = Entry('tradingBot/grid/min-investment', 'public', 'GET', {'cost': 1}) + public_get_tradingbot_public_rsi_back_testing = publicGetTradingBotPublicRsiBackTesting = Entry('tradingBot/public/rsi-back-testing', 'public', 'GET', {'cost': 1}) + public_get_asset_exchange_list = publicGetAssetExchangeList = Entry('asset/exchange-list', 'public', 'GET', {'cost': 1.6666666666666667}) + public_get_finance_staking_defi_eth_apy_history = publicGetFinanceStakingDefiEthApyHistory = Entry('finance/staking-defi/eth/apy-history', 'public', 'GET', {'cost': 1.6666666666666667}) + public_get_finance_staking_defi_sol_apy_history = publicGetFinanceStakingDefiSolApyHistory = Entry('finance/staking-defi/sol/apy-history', 'public', 'GET', {'cost': 1.6666666666666667}) + public_get_finance_savings_lending_rate_summary = publicGetFinanceSavingsLendingRateSummary = Entry('finance/savings/lending-rate-summary', 'public', 'GET', {'cost': 1.6666666666666667}) + public_get_finance_savings_lending_rate_history = publicGetFinanceSavingsLendingRateHistory = Entry('finance/savings/lending-rate-history', 'public', 'GET', {'cost': 1.6666666666666667}) + public_get_finance_fixed_loan_lending_offers = publicGetFinanceFixedLoanLendingOffers = Entry('finance/fixed-loan/lending-offers', 'public', 'GET', {'cost': 3.3333333333333335}) + public_get_finance_fixed_loan_lending_apy_history = publicGetFinanceFixedLoanLendingApyHistory = Entry('finance/fixed-loan/lending-apy-history', 'public', 'GET', {'cost': 3.3333333333333335}) + public_get_finance_fixed_loan_pending_lending_volume = publicGetFinanceFixedLoanPendingLendingVolume = Entry('finance/fixed-loan/pending-lending-volume', 'public', 'GET', {'cost': 3.3333333333333335}) + public_get_finance_sfp_dcd_products = publicGetFinanceSfpDcdProducts = Entry('finance/sfp/dcd/products', 'public', 'GET', {'cost': 0.6666666666666666}) + public_get_copytrading_public_lead_traders = publicGetCopytradingPublicLeadTraders = Entry('copytrading/public-lead-traders', 'public', 'GET', {'cost': 4}) + public_get_copytrading_public_weekly_pnl = publicGetCopytradingPublicWeeklyPnl = Entry('copytrading/public-weekly-pnl', 'public', 'GET', {'cost': 4}) + public_get_copytrading_public_stats = publicGetCopytradingPublicStats = Entry('copytrading/public-stats', 'public', 'GET', {'cost': 4}) + public_get_copytrading_public_preference_currency = publicGetCopytradingPublicPreferenceCurrency = Entry('copytrading/public-preference-currency', 'public', 'GET', {'cost': 4}) + public_get_copytrading_public_current_subpositions = publicGetCopytradingPublicCurrentSubpositions = Entry('copytrading/public-current-subpositions', 'public', 'GET', {'cost': 4}) + public_get_copytrading_public_subpositions_history = publicGetCopytradingPublicSubpositionsHistory = Entry('copytrading/public-subpositions-history', 'public', 'GET', {'cost': 4}) + public_get_support_announcements_types = publicGetSupportAnnouncementsTypes = Entry('support/announcements-types', 'public', 'GET', {'cost': 20}) + private_get_rfq_counterparties = privateGetRfqCounterparties = Entry('rfq/counterparties', 'private', 'GET', {'cost': 4}) + private_get_rfq_maker_instrument_settings = privateGetRfqMakerInstrumentSettings = Entry('rfq/maker-instrument-settings', 'private', 'GET', {'cost': 4}) + private_get_rfq_mmp_config = privateGetRfqMmpConfig = Entry('rfq/mmp-config', 'private', 'GET', {'cost': 4}) + private_get_rfq_rfqs = privateGetRfqRfqs = Entry('rfq/rfqs', 'private', 'GET', {'cost': 10}) + private_get_rfq_quotes = privateGetRfqQuotes = Entry('rfq/quotes', 'private', 'GET', {'cost': 10}) + private_get_rfq_trades = privateGetRfqTrades = Entry('rfq/trades', 'private', 'GET', {'cost': 4}) + private_get_rfq_public_trades = privateGetRfqPublicTrades = Entry('rfq/public-trades', 'private', 'GET', {'cost': 4}) + private_get_sprd_order = privateGetSprdOrder = Entry('sprd/order', 'private', 'GET', {'cost': 0.3333333333333333}) + private_get_sprd_orders_pending = privateGetSprdOrdersPending = Entry('sprd/orders-pending', 'private', 'GET', {'cost': 0.3333333333333333}) + private_get_sprd_orders_history = privateGetSprdOrdersHistory = Entry('sprd/orders-history', 'private', 'GET', {'cost': 0.5}) + private_get_sprd_orders_history_archive = privateGetSprdOrdersHistoryArchive = Entry('sprd/orders-history-archive', 'private', 'GET', {'cost': 0.5}) + private_get_sprd_trades = privateGetSprdTrades = Entry('sprd/trades', 'private', 'GET', {'cost': 0.3333333333333333}) + private_get_trade_order = privateGetTradeOrder = Entry('trade/order', 'private', 'GET', {'cost': 0.3333333333333333}) + private_get_trade_orders_pending = privateGetTradeOrdersPending = Entry('trade/orders-pending', 'private', 'GET', {'cost': 0.3333333333333333}) + private_get_trade_orders_history = privateGetTradeOrdersHistory = Entry('trade/orders-history', 'private', 'GET', {'cost': 0.5}) + private_get_trade_orders_history_archive = privateGetTradeOrdersHistoryArchive = Entry('trade/orders-history-archive', 'private', 'GET', {'cost': 1}) + private_get_trade_fills = privateGetTradeFills = Entry('trade/fills', 'private', 'GET', {'cost': 0.3333333333333333}) + private_get_trade_fills_history = privateGetTradeFillsHistory = Entry('trade/fills-history', 'private', 'GET', {'cost': 2.2}) + private_get_trade_fills_archive = privateGetTradeFillsArchive = Entry('trade/fills-archive', 'private', 'GET', {'cost': 2}) + private_get_trade_order_algo = privateGetTradeOrderAlgo = Entry('trade/order-algo', 'private', 'GET', {'cost': 1}) + private_get_trade_orders_algo_pending = privateGetTradeOrdersAlgoPending = Entry('trade/orders-algo-pending', 'private', 'GET', {'cost': 1}) + private_get_trade_orders_algo_history = privateGetTradeOrdersAlgoHistory = Entry('trade/orders-algo-history', 'private', 'GET', {'cost': 1}) + private_get_trade_easy_convert_currency_list = privateGetTradeEasyConvertCurrencyList = Entry('trade/easy-convert-currency-list', 'private', 'GET', {'cost': 20}) + private_get_trade_easy_convert_history = privateGetTradeEasyConvertHistory = Entry('trade/easy-convert-history', 'private', 'GET', {'cost': 20}) + private_get_trade_one_click_repay_currency_list = privateGetTradeOneClickRepayCurrencyList = Entry('trade/one-click-repay-currency-list', 'private', 'GET', {'cost': 20}) + private_get_trade_one_click_repay_currency_list_v2 = privateGetTradeOneClickRepayCurrencyListV2 = Entry('trade/one-click-repay-currency-list-v2', 'private', 'GET', {'cost': 20}) + private_get_trade_one_click_repay_history = privateGetTradeOneClickRepayHistory = Entry('trade/one-click-repay-history', 'private', 'GET', {'cost': 20}) + private_get_trade_one_click_repay_history_v2 = privateGetTradeOneClickRepayHistoryV2 = Entry('trade/one-click-repay-history-v2', 'private', 'GET', {'cost': 20}) + private_get_trade_account_rate_limit = privateGetTradeAccountRateLimit = Entry('trade/account-rate-limit', 'private', 'GET', {'cost': 1}) + private_get_asset_currencies = privateGetAssetCurrencies = Entry('asset/currencies', 'private', 'GET', {'cost': 1.6666666666666667}) + private_get_asset_balances = privateGetAssetBalances = Entry('asset/balances', 'private', 'GET', {'cost': 1.6666666666666667}) + private_get_asset_non_tradable_assets = privateGetAssetNonTradableAssets = Entry('asset/non-tradable-assets', 'private', 'GET', {'cost': 1.6666666666666667}) + private_get_asset_asset_valuation = privateGetAssetAssetValuation = Entry('asset/asset-valuation', 'private', 'GET', {'cost': 10}) + private_get_asset_transfer_state = privateGetAssetTransferState = Entry('asset/transfer-state', 'private', 'GET', {'cost': 10}) + private_get_asset_bills = privateGetAssetBills = Entry('asset/bills', 'private', 'GET', {'cost': 1.6666666666666667}) + private_get_asset_deposit_lightning = privateGetAssetDepositLightning = Entry('asset/deposit-lightning', 'private', 'GET', {'cost': 5}) + private_get_asset_deposit_address = privateGetAssetDepositAddress = Entry('asset/deposit-address', 'private', 'GET', {'cost': 1.6666666666666667}) + private_get_asset_deposit_history = privateGetAssetDepositHistory = Entry('asset/deposit-history', 'private', 'GET', {'cost': 1.6666666666666667}) + private_get_asset_withdrawal_history = privateGetAssetWithdrawalHistory = Entry('asset/withdrawal-history', 'private', 'GET', {'cost': 1.6666666666666667}) + private_get_asset_deposit_withdraw_status = privateGetAssetDepositWithdrawStatus = Entry('asset/deposit-withdraw-status', 'private', 'GET', {'cost': 20}) + private_get_asset_convert_currencies = privateGetAssetConvertCurrencies = Entry('asset/convert/currencies', 'private', 'GET', {'cost': 1.6666666666666667}) + private_get_asset_convert_currency_pair = privateGetAssetConvertCurrencyPair = Entry('asset/convert/currency-pair', 'private', 'GET', {'cost': 1.6666666666666667}) + private_get_asset_convert_history = privateGetAssetConvertHistory = Entry('asset/convert/history', 'private', 'GET', {'cost': 1.6666666666666667}) + private_get_asset_monthly_statement = privateGetAssetMonthlyStatement = Entry('asset/monthly-statement', 'private', 'GET', {'cost': 2}) + private_get_account_instruments = privateGetAccountInstruments = Entry('account/instruments', 'private', 'GET', {'cost': 1}) + private_get_account_balance = privateGetAccountBalance = Entry('account/balance', 'private', 'GET', {'cost': 2}) + private_get_account_positions = privateGetAccountPositions = Entry('account/positions', 'private', 'GET', {'cost': 2}) + private_get_account_positions_history = privateGetAccountPositionsHistory = Entry('account/positions-history', 'private', 'GET', {'cost': 100}) + private_get_account_account_position_risk = privateGetAccountAccountPositionRisk = Entry('account/account-position-risk', 'private', 'GET', {'cost': 2}) + private_get_account_bills = privateGetAccountBills = Entry('account/bills', 'private', 'GET', {'cost': 1.6666666666666667}) + private_get_account_bills_archive = privateGetAccountBillsArchive = Entry('account/bills-archive', 'private', 'GET', {'cost': 1.6666666666666667}) + private_get_account_bills_history_archive = privateGetAccountBillsHistoryArchive = Entry('account/bills-history-archive', 'private', 'GET', {'cost': 2}) + private_get_account_config = privateGetAccountConfig = Entry('account/config', 'private', 'GET', {'cost': 4}) + private_get_account_max_size = privateGetAccountMaxSize = Entry('account/max-size', 'private', 'GET', {'cost': 1}) + private_get_account_max_avail_size = privateGetAccountMaxAvailSize = Entry('account/max-avail-size', 'private', 'GET', {'cost': 1}) + private_get_account_leverage_info = privateGetAccountLeverageInfo = Entry('account/leverage-info', 'private', 'GET', {'cost': 1}) + private_get_account_adjust_leverage_info = privateGetAccountAdjustLeverageInfo = Entry('account/adjust-leverage-info', 'private', 'GET', {'cost': 4}) + private_get_account_max_loan = privateGetAccountMaxLoan = Entry('account/max-loan', 'private', 'GET', {'cost': 1}) + private_get_account_trade_fee = privateGetAccountTradeFee = Entry('account/trade-fee', 'private', 'GET', {'cost': 4}) + private_get_account_interest_accrued = privateGetAccountInterestAccrued = Entry('account/interest-accrued', 'private', 'GET', {'cost': 4}) + private_get_account_interest_rate = privateGetAccountInterestRate = Entry('account/interest-rate', 'private', 'GET', {'cost': 4}) + private_get_account_max_withdrawal = privateGetAccountMaxWithdrawal = Entry('account/max-withdrawal', 'private', 'GET', {'cost': 1}) + private_get_account_risk_state = privateGetAccountRiskState = Entry('account/risk-state', 'private', 'GET', {'cost': 2}) + private_get_account_quick_margin_borrow_repay_history = privateGetAccountQuickMarginBorrowRepayHistory = Entry('account/quick-margin-borrow-repay-history', 'private', 'GET', {'cost': 4}) + private_get_account_borrow_repay_history = privateGetAccountBorrowRepayHistory = Entry('account/borrow-repay-history', 'private', 'GET', {'cost': 4}) + private_get_account_vip_interest_accrued = privateGetAccountVipInterestAccrued = Entry('account/vip-interest-accrued', 'private', 'GET', {'cost': 4}) + private_get_account_vip_interest_deducted = privateGetAccountVipInterestDeducted = Entry('account/vip-interest-deducted', 'private', 'GET', {'cost': 4}) + private_get_account_vip_loan_order_list = privateGetAccountVipLoanOrderList = Entry('account/vip-loan-order-list', 'private', 'GET', {'cost': 4}) + private_get_account_vip_loan_order_detail = privateGetAccountVipLoanOrderDetail = Entry('account/vip-loan-order-detail', 'private', 'GET', {'cost': 4}) + private_get_account_interest_limits = privateGetAccountInterestLimits = Entry('account/interest-limits', 'private', 'GET', {'cost': 4}) + private_get_account_greeks = privateGetAccountGreeks = Entry('account/greeks', 'private', 'GET', {'cost': 2}) + private_get_account_position_tiers = privateGetAccountPositionTiers = Entry('account/position-tiers', 'private', 'GET', {'cost': 2}) + private_get_account_mmp_config = privateGetAccountMmpConfig = Entry('account/mmp-config', 'private', 'GET', {'cost': 4}) + private_get_account_fixed_loan_borrowing_limit = privateGetAccountFixedLoanBorrowingLimit = Entry('account/fixed-loan/borrowing-limit', 'private', 'GET', {'cost': 4}) + private_get_account_fixed_loan_borrowing_quote = privateGetAccountFixedLoanBorrowingQuote = Entry('account/fixed-loan/borrowing-quote', 'private', 'GET', {'cost': 5}) + private_get_account_fixed_loan_borrowing_orders_list = privateGetAccountFixedLoanBorrowingOrdersList = Entry('account/fixed-loan/borrowing-orders-list', 'private', 'GET', {'cost': 5}) + private_get_account_spot_manual_borrow_repay = privateGetAccountSpotManualBorrowRepay = Entry('account/spot-manual-borrow-repay', 'private', 'GET', {'cost': 30}) + private_get_account_set_auto_repay = privateGetAccountSetAutoRepay = Entry('account/set-auto-repay', 'private', 'GET', {'cost': 4}) + private_get_account_spot_borrow_repay_history = privateGetAccountSpotBorrowRepayHistory = Entry('account/spot-borrow-repay-history', 'private', 'GET', {'cost': 4}) + private_get_account_move_positions_history = privateGetAccountMovePositionsHistory = Entry('account/move-positions-history', 'private', 'GET', {'cost': 10}) + private_get_users_subaccount_list = privateGetUsersSubaccountList = Entry('users/subaccount/list', 'private', 'GET', {'cost': 10}) + private_get_account_subaccount_balances = privateGetAccountSubaccountBalances = Entry('account/subaccount/balances', 'private', 'GET', {'cost': 3.3333333333333335}) + private_get_asset_subaccount_balances = privateGetAssetSubaccountBalances = Entry('asset/subaccount/balances', 'private', 'GET', {'cost': 3.3333333333333335}) + private_get_account_subaccount_max_withdrawal = privateGetAccountSubaccountMaxWithdrawal = Entry('account/subaccount/max-withdrawal', 'private', 'GET', {'cost': 1}) + private_get_asset_subaccount_bills = privateGetAssetSubaccountBills = Entry('asset/subaccount/bills', 'private', 'GET', {'cost': 1.6666666666666667}) + private_get_asset_subaccount_managed_subaccount_bills = privateGetAssetSubaccountManagedSubaccountBills = Entry('asset/subaccount/managed-subaccount-bills', 'private', 'GET', {'cost': 1.6666666666666667}) + private_get_users_entrust_subaccount_list = privateGetUsersEntrustSubaccountList = Entry('users/entrust-subaccount-list', 'private', 'GET', {'cost': 10}) + private_get_account_subaccount_interest_limits = privateGetAccountSubaccountInterestLimits = Entry('account/subaccount/interest-limits', 'private', 'GET', {'cost': 4}) + private_get_users_subaccount_apikey = privateGetUsersSubaccountApikey = Entry('users/subaccount/apikey', 'private', 'GET', {'cost': 10}) + private_get_tradingbot_grid_orders_algo_pending = privateGetTradingBotGridOrdersAlgoPending = Entry('tradingBot/grid/orders-algo-pending', 'private', 'GET', {'cost': 1}) + private_get_tradingbot_grid_orders_algo_history = privateGetTradingBotGridOrdersAlgoHistory = Entry('tradingBot/grid/orders-algo-history', 'private', 'GET', {'cost': 1}) + private_get_tradingbot_grid_orders_algo_details = privateGetTradingBotGridOrdersAlgoDetails = Entry('tradingBot/grid/orders-algo-details', 'private', 'GET', {'cost': 1}) + private_get_tradingbot_grid_sub_orders = privateGetTradingBotGridSubOrders = Entry('tradingBot/grid/sub-orders', 'private', 'GET', {'cost': 1}) + private_get_tradingbot_grid_positions = privateGetTradingBotGridPositions = Entry('tradingBot/grid/positions', 'private', 'GET', {'cost': 1}) + private_get_tradingbot_grid_ai_param = privateGetTradingBotGridAiParam = Entry('tradingBot/grid/ai-param', 'private', 'GET', {'cost': 1}) + private_get_tradingbot_signal_signals = privateGetTradingBotSignalSignals = Entry('tradingBot/signal/signals', 'private', 'GET', {'cost': 1}) + private_get_tradingbot_signal_orders_algo_details = privateGetTradingBotSignalOrdersAlgoDetails = Entry('tradingBot/signal/orders-algo-details', 'private', 'GET', {'cost': 1}) + private_get_tradingbot_signal_orders_algo_history = privateGetTradingBotSignalOrdersAlgoHistory = Entry('tradingBot/signal/orders-algo-history', 'private', 'GET', {'cost': 1}) + private_get_tradingbot_signal_positions = privateGetTradingBotSignalPositions = Entry('tradingBot/signal/positions', 'private', 'GET', {'cost': 1}) + private_get_tradingbot_signal_positions_history = privateGetTradingBotSignalPositionsHistory = Entry('tradingBot/signal/positions-history', 'private', 'GET', {'cost': 1}) + private_get_tradingbot_signal_sub_orders = privateGetTradingBotSignalSubOrders = Entry('tradingBot/signal/sub-orders', 'private', 'GET', {'cost': 1}) + private_get_tradingbot_signal_event_history = privateGetTradingBotSignalEventHistory = Entry('tradingBot/signal/event-history', 'private', 'GET', {'cost': 1}) + private_get_tradingbot_recurring_orders_algo_pending = privateGetTradingBotRecurringOrdersAlgoPending = Entry('tradingBot/recurring/orders-algo-pending', 'private', 'GET', {'cost': 1}) + private_get_tradingbot_recurring_orders_algo_history = privateGetTradingBotRecurringOrdersAlgoHistory = Entry('tradingBot/recurring/orders-algo-history', 'private', 'GET', {'cost': 1}) + private_get_tradingbot_recurring_orders_algo_details = privateGetTradingBotRecurringOrdersAlgoDetails = Entry('tradingBot/recurring/orders-algo-details', 'private', 'GET', {'cost': 1}) + private_get_tradingbot_recurring_sub_orders = privateGetTradingBotRecurringSubOrders = Entry('tradingBot/recurring/sub-orders', 'private', 'GET', {'cost': 1}) + private_get_finance_savings_balance = privateGetFinanceSavingsBalance = Entry('finance/savings/balance', 'private', 'GET', {'cost': 1.6666666666666667}) + private_get_finance_savings_lending_history = privateGetFinanceSavingsLendingHistory = Entry('finance/savings/lending-history', 'private', 'GET', {'cost': 1.6666666666666667}) + private_get_finance_staking_defi_offers = privateGetFinanceStakingDefiOffers = Entry('finance/staking-defi/offers', 'private', 'GET', {'cost': 3.3333333333333335}) + private_get_finance_staking_defi_orders_active = privateGetFinanceStakingDefiOrdersActive = Entry('finance/staking-defi/orders-active', 'private', 'GET', {'cost': 3.3333333333333335}) + private_get_finance_staking_defi_orders_history = privateGetFinanceStakingDefiOrdersHistory = Entry('finance/staking-defi/orders-history', 'private', 'GET', {'cost': 3.3333333333333335}) + private_get_finance_staking_defi_eth_balance = privateGetFinanceStakingDefiEthBalance = Entry('finance/staking-defi/eth/balance', 'private', 'GET', {'cost': 1.6666666666666667}) + private_get_finance_staking_defi_eth_purchase_redeem_history = privateGetFinanceStakingDefiEthPurchaseRedeemHistory = Entry('finance/staking-defi/eth/purchase-redeem-history', 'private', 'GET', {'cost': 1.6666666666666667}) + private_get_finance_staking_defi_eth_product_info = privateGetFinanceStakingDefiEthProductInfo = Entry('finance/staking-defi/eth/product-info', 'private', 'GET', {'cost': 3}) + private_get_finance_staking_defi_sol_balance = privateGetFinanceStakingDefiSolBalance = Entry('finance/staking-defi/sol/balance', 'private', 'GET', {'cost': 1.6666666666666667}) + private_get_finance_staking_defi_sol_purchase_redeem_history = privateGetFinanceStakingDefiSolPurchaseRedeemHistory = Entry('finance/staking-defi/sol/purchase-redeem-history', 'private', 'GET', {'cost': 1.6666666666666667}) + private_get_copytrading_current_subpositions = privateGetCopytradingCurrentSubpositions = Entry('copytrading/current-subpositions', 'private', 'GET', {'cost': 1}) + private_get_copytrading_subpositions_history = privateGetCopytradingSubpositionsHistory = Entry('copytrading/subpositions-history', 'private', 'GET', {'cost': 1}) + private_get_copytrading_instruments = privateGetCopytradingInstruments = Entry('copytrading/instruments', 'private', 'GET', {'cost': 4}) + private_get_copytrading_profit_sharing_details = privateGetCopytradingProfitSharingDetails = Entry('copytrading/profit-sharing-details', 'private', 'GET', {'cost': 4}) + private_get_copytrading_total_profit_sharing = privateGetCopytradingTotalProfitSharing = Entry('copytrading/total-profit-sharing', 'private', 'GET', {'cost': 4}) + private_get_copytrading_unrealized_profit_sharing_details = privateGetCopytradingUnrealizedProfitSharingDetails = Entry('copytrading/unrealized-profit-sharing-details', 'private', 'GET', {'cost': 4}) + private_get_copytrading_copy_settings = privateGetCopytradingCopySettings = Entry('copytrading/copy-settings', 'private', 'GET', {'cost': 4}) + private_get_copytrading_batch_leverage_info = privateGetCopytradingBatchLeverageInfo = Entry('copytrading/batch-leverage-info', 'private', 'GET', {'cost': 4}) + private_get_copytrading_current_lead_traders = privateGetCopytradingCurrentLeadTraders = Entry('copytrading/current-lead-traders', 'private', 'GET', {'cost': 4}) + private_get_copytrading_lead_traders_history = privateGetCopytradingLeadTradersHistory = Entry('copytrading/lead-traders-history', 'private', 'GET', {'cost': 4}) + private_get_broker_nd_info = privateGetBrokerNdInfo = Entry('broker/nd/info', 'private', 'GET', {'cost': 10}) + private_get_broker_nd_subaccount_info = privateGetBrokerNdSubaccountInfo = Entry('broker/nd/subaccount-info', 'private', 'GET', {'cost': 10}) + private_get_broker_nd_subaccount_apikey = privateGetBrokerNdSubaccountApikey = Entry('broker/nd/subaccount/apikey', 'private', 'GET', {'cost': 10}) + private_get_asset_broker_nd_subaccount_deposit_address = privateGetAssetBrokerNdSubaccountDepositAddress = Entry('asset/broker/nd/subaccount-deposit-address', 'private', 'GET', {'cost': 1.6666666666666667}) + private_get_asset_broker_nd_subaccount_deposit_history = privateGetAssetBrokerNdSubaccountDepositHistory = Entry('asset/broker/nd/subaccount-deposit-history', 'private', 'GET', {'cost': 4}) + private_get_asset_broker_nd_subaccount_withdrawal_history = privateGetAssetBrokerNdSubaccountWithdrawalHistory = Entry('asset/broker/nd/subaccount-withdrawal-history', 'private', 'GET', {'cost': 4}) + private_get_broker_nd_rebate_daily = privateGetBrokerNdRebateDaily = Entry('broker/nd/rebate-daily', 'private', 'GET', {'cost': 100}) + private_get_broker_nd_rebate_per_orders = privateGetBrokerNdRebatePerOrders = Entry('broker/nd/rebate-per-orders', 'private', 'GET', {'cost': 300}) + private_get_finance_sfp_dcd_order = privateGetFinanceSfpDcdOrder = Entry('finance/sfp/dcd/order', 'private', 'GET', {'cost': 2}) + private_get_finance_sfp_dcd_orders = privateGetFinanceSfpDcdOrders = Entry('finance/sfp/dcd/orders', 'private', 'GET', {'cost': 2}) + private_get_broker_fd_rebate_per_orders = privateGetBrokerFdRebatePerOrders = Entry('broker/fd/rebate-per-orders', 'private', 'GET', {'cost': 300}) + private_get_broker_fd_if_rebate = privateGetBrokerFdIfRebate = Entry('broker/fd/if-rebate', 'private', 'GET', {'cost': 5}) + private_get_affiliate_invitee_detail = privateGetAffiliateInviteeDetail = Entry('affiliate/invitee/detail', 'private', 'GET', {'cost': 1}) + private_get_users_partner_if_rebate = privateGetUsersPartnerIfRebate = Entry('users/partner/if-rebate', 'private', 'GET', {'cost': 1}) + private_get_support_announcements = privateGetSupportAnnouncements = Entry('support/announcements', 'private', 'GET', {'cost': 4}) + private_post_rfq_create_rfq = privatePostRfqCreateRfq = Entry('rfq/create-rfq', 'private', 'POST', {'cost': 4}) + private_post_rfq_cancel_rfq = privatePostRfqCancelRfq = Entry('rfq/cancel-rfq', 'private', 'POST', {'cost': 4}) + private_post_rfq_cancel_batch_rfqs = privatePostRfqCancelBatchRfqs = Entry('rfq/cancel-batch-rfqs', 'private', 'POST', {'cost': 10}) + private_post_rfq_cancel_all_rfqs = privatePostRfqCancelAllRfqs = Entry('rfq/cancel-all-rfqs', 'private', 'POST', {'cost': 10}) + private_post_rfq_execute_quote = privatePostRfqExecuteQuote = Entry('rfq/execute-quote', 'private', 'POST', {'cost': 15}) + private_post_rfq_maker_instrument_settings = privatePostRfqMakerInstrumentSettings = Entry('rfq/maker-instrument-settings', 'private', 'POST', {'cost': 4}) + private_post_rfq_mmp_reset = privatePostRfqMmpReset = Entry('rfq/mmp-reset', 'private', 'POST', {'cost': 4}) + private_post_rfq_mmp_config = privatePostRfqMmpConfig = Entry('rfq/mmp-config', 'private', 'POST', {'cost': 100}) + private_post_rfq_create_quote = privatePostRfqCreateQuote = Entry('rfq/create-quote', 'private', 'POST', {'cost': 0.4}) + private_post_rfq_cancel_quote = privatePostRfqCancelQuote = Entry('rfq/cancel-quote', 'private', 'POST', {'cost': 0.4}) + private_post_rfq_cancel_batch_quotes = privatePostRfqCancelBatchQuotes = Entry('rfq/cancel-batch-quotes', 'private', 'POST', {'cost': 10}) + private_post_rfq_cancel_all_quotes = privatePostRfqCancelAllQuotes = Entry('rfq/cancel-all-quotes', 'private', 'POST', {'cost': 10}) + private_post_sprd_order = privatePostSprdOrder = Entry('sprd/order', 'private', 'POST', {'cost': 1}) + private_post_sprd_cancel_order = privatePostSprdCancelOrder = Entry('sprd/cancel-order', 'private', 'POST', {'cost': 1}) + private_post_sprd_mass_cancel = privatePostSprdMassCancel = Entry('sprd/mass-cancel', 'private', 'POST', {'cost': 1}) + private_post_sprd_amend_order = privatePostSprdAmendOrder = Entry('sprd/amend-order', 'private', 'POST', {'cost': 1}) + private_post_sprd_cancel_all_after = privatePostSprdCancelAllAfter = Entry('sprd/cancel-all-after', 'private', 'POST', {'cost': 10}) + private_post_trade_order = privatePostTradeOrder = Entry('trade/order', 'private', 'POST', {'cost': 0.3333333333333333}) + private_post_trade_batch_orders = privatePostTradeBatchOrders = Entry('trade/batch-orders', 'private', 'POST', {'cost': 0.06666666666666667}) + private_post_trade_cancel_order = privatePostTradeCancelOrder = Entry('trade/cancel-order', 'private', 'POST', {'cost': 0.3333333333333333}) + private_post_trade_cancel_batch_orders = privatePostTradeCancelBatchOrders = Entry('trade/cancel-batch-orders', 'private', 'POST', {'cost': 0.06666666666666667}) + private_post_trade_amend_order = privatePostTradeAmendOrder = Entry('trade/amend-order', 'private', 'POST', {'cost': 0.3333333333333333}) + private_post_trade_amend_batch_orders = privatePostTradeAmendBatchOrders = Entry('trade/amend-batch-orders', 'private', 'POST', {'cost': 0.006666666666666667}) + private_post_trade_close_position = privatePostTradeClosePosition = Entry('trade/close-position', 'private', 'POST', {'cost': 1}) + private_post_trade_fills_archive = privatePostTradeFillsArchive = Entry('trade/fills-archive', 'private', 'POST', {'cost': 172800}) + private_post_trade_order_algo = privatePostTradeOrderAlgo = Entry('trade/order-algo', 'private', 'POST', {'cost': 1}) + private_post_trade_cancel_algos = privatePostTradeCancelAlgos = Entry('trade/cancel-algos', 'private', 'POST', {'cost': 1}) + private_post_trade_amend_algos = privatePostTradeAmendAlgos = Entry('trade/amend-algos', 'private', 'POST', {'cost': 1}) + private_post_trade_cancel_advance_algos = privatePostTradeCancelAdvanceAlgos = Entry('trade/cancel-advance-algos', 'private', 'POST', {'cost': 1}) + private_post_trade_easy_convert = privatePostTradeEasyConvert = Entry('trade/easy-convert', 'private', 'POST', {'cost': 20}) + private_post_trade_one_click_repay = privatePostTradeOneClickRepay = Entry('trade/one-click-repay', 'private', 'POST', {'cost': 20}) + private_post_trade_one_click_repay_v2 = privatePostTradeOneClickRepayV2 = Entry('trade/one-click-repay-v2', 'private', 'POST', {'cost': 20}) + private_post_trade_mass_cancel = privatePostTradeMassCancel = Entry('trade/mass-cancel', 'private', 'POST', {'cost': 4}) + private_post_trade_cancel_all_after = privatePostTradeCancelAllAfter = Entry('trade/cancel-all-after', 'private', 'POST', {'cost': 10}) + private_post_asset_transfer = privatePostAssetTransfer = Entry('asset/transfer', 'private', 'POST', {'cost': 10}) + private_post_asset_withdrawal = privatePostAssetWithdrawal = Entry('asset/withdrawal', 'private', 'POST', {'cost': 1.6666666666666667}) + private_post_asset_withdrawal_lightning = privatePostAssetWithdrawalLightning = Entry('asset/withdrawal-lightning', 'private', 'POST', {'cost': 5}) + private_post_asset_cancel_withdrawal = privatePostAssetCancelWithdrawal = Entry('asset/cancel-withdrawal', 'private', 'POST', {'cost': 1.6666666666666667}) + private_post_asset_convert_dust_assets = privatePostAssetConvertDustAssets = Entry('asset/convert-dust-assets', 'private', 'POST', {'cost': 10}) + private_post_asset_convert_estimate_quote = privatePostAssetConvertEstimateQuote = Entry('asset/convert/estimate-quote', 'private', 'POST', {'cost': 1}) + private_post_asset_convert_trade = privatePostAssetConvertTrade = Entry('asset/convert/trade', 'private', 'POST', {'cost': 1}) + private_post_asset_monthly_statement = privatePostAssetMonthlyStatement = Entry('asset/monthly-statement', 'private', 'POST', {'cost': 1}) + private_post_account_set_position_mode = privatePostAccountSetPositionMode = Entry('account/set-position-mode', 'private', 'POST', {'cost': 4}) + private_post_account_set_leverage = privatePostAccountSetLeverage = Entry('account/set-leverage', 'private', 'POST', {'cost': 1}) + private_post_account_position_margin_balance = privatePostAccountPositionMarginBalance = Entry('account/position/margin-balance', 'private', 'POST', {'cost': 1}) + private_post_account_set_greeks = privatePostAccountSetGreeks = Entry('account/set-greeks', 'private', 'POST', {'cost': 4}) + private_post_account_set_isolated_mode = privatePostAccountSetIsolatedMode = Entry('account/set-isolated-mode', 'private', 'POST', {'cost': 4}) + private_post_account_quick_margin_borrow_repay = privatePostAccountQuickMarginBorrowRepay = Entry('account/quick-margin-borrow-repay', 'private', 'POST', {'cost': 4}) + private_post_account_borrow_repay = privatePostAccountBorrowRepay = Entry('account/borrow-repay', 'private', 'POST', {'cost': 1.6666666666666667}) + private_post_account_simulated_margin = privatePostAccountSimulatedMargin = Entry('account/simulated_margin', 'private', 'POST', {'cost': 10}) + private_post_account_position_builder = privatePostAccountPositionBuilder = Entry('account/position-builder', 'private', 'POST', {'cost': 10}) + private_post_account_set_riskoffset_type = privatePostAccountSetRiskOffsetType = Entry('account/set-riskOffset-type', 'private', 'POST', {'cost': 2}) + private_post_account_activate_option = privatePostAccountActivateOption = Entry('account/activate-option', 'private', 'POST', {'cost': 4}) + private_post_account_set_auto_loan = privatePostAccountSetAutoLoan = Entry('account/set-auto-loan', 'private', 'POST', {'cost': 4}) + private_post_account_set_account_level = privatePostAccountSetAccountLevel = Entry('account/set-account-level', 'private', 'POST', {'cost': 4}) + private_post_account_mmp_reset = privatePostAccountMmpReset = Entry('account/mmp-reset', 'private', 'POST', {'cost': 4}) + private_post_account_mmp_config = privatePostAccountMmpConfig = Entry('account/mmp-config', 'private', 'POST', {'cost': 100}) + private_post_account_fixed_loan_borrowing_order = privatePostAccountFixedLoanBorrowingOrder = Entry('account/fixed-loan/borrowing-order', 'private', 'POST', {'cost': 5}) + private_post_account_fixed_loan_amend_borrowing_order = privatePostAccountFixedLoanAmendBorrowingOrder = Entry('account/fixed-loan/amend-borrowing-order', 'private', 'POST', {'cost': 5}) + private_post_account_fixed_loan_manual_reborrow = privatePostAccountFixedLoanManualReborrow = Entry('account/fixed-loan/manual-reborrow', 'private', 'POST', {'cost': 5}) + private_post_account_fixed_loan_repay_borrowing_order = privatePostAccountFixedLoanRepayBorrowingOrder = Entry('account/fixed-loan/repay-borrowing-order', 'private', 'POST', {'cost': 5}) + private_post_account_bills_history_archive = privatePostAccountBillsHistoryArchive = Entry('account/bills-history-archive', 'private', 'POST', {'cost': 72000}) + private_post_account_move_positions = privatePostAccountMovePositions = Entry('account/move-positions', 'private', 'POST', {'cost': 10}) + private_post_account_set_settle_currency = privatePostAccountSetSettleCurrency = Entry('account/set-settle-currency', 'private', 'POST', {'cost': 1}) + private_post_users_subaccount_modify_apikey = privatePostUsersSubaccountModifyApikey = Entry('users/subaccount/modify-apikey', 'private', 'POST', {'cost': 10}) + private_post_asset_subaccount_transfer = privatePostAssetSubaccountTransfer = Entry('asset/subaccount/transfer', 'private', 'POST', {'cost': 10}) + private_post_users_subaccount_set_transfer_out = privatePostUsersSubaccountSetTransferOut = Entry('users/subaccount/set-transfer-out', 'private', 'POST', {'cost': 10}) + private_post_account_subaccount_set_loan_allocation = privatePostAccountSubaccountSetLoanAllocation = Entry('account/subaccount/set-loan-allocation', 'private', 'POST', {'cost': 4}) + private_post_users_subaccount_create_subaccount = privatePostUsersSubaccountCreateSubaccount = Entry('users/subaccount/create-subaccount', 'private', 'POST', {'cost': 10}) + private_post_users_subaccount_subaccount_apikey = privatePostUsersSubaccountSubaccountApikey = Entry('users/subaccount/subaccount-apikey', 'private', 'POST', {'cost': 10}) + private_post_users_subaccount_delete_apikey = privatePostUsersSubaccountDeleteApikey = Entry('users/subaccount/delete-apikey', 'private', 'POST', {'cost': 10}) + private_post_tradingbot_grid_order_algo = privatePostTradingBotGridOrderAlgo = Entry('tradingBot/grid/order-algo', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_grid_amend_order_algo = privatePostTradingBotGridAmendOrderAlgo = Entry('tradingBot/grid/amend-order-algo', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_grid_stop_order_algo = privatePostTradingBotGridStopOrderAlgo = Entry('tradingBot/grid/stop-order-algo', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_grid_close_position = privatePostTradingBotGridClosePosition = Entry('tradingBot/grid/close-position', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_grid_cancel_close_order = privatePostTradingBotGridCancelCloseOrder = Entry('tradingBot/grid/cancel-close-order', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_grid_order_instant_trigger = privatePostTradingBotGridOrderInstantTrigger = Entry('tradingBot/grid/order-instant-trigger', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_grid_withdraw_income = privatePostTradingBotGridWithdrawIncome = Entry('tradingBot/grid/withdraw-income', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_grid_compute_margin_balance = privatePostTradingBotGridComputeMarginBalance = Entry('tradingBot/grid/compute-margin-balance', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_grid_margin_balance = privatePostTradingBotGridMarginBalance = Entry('tradingBot/grid/margin-balance', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_grid_min_investment = privatePostTradingBotGridMinInvestment = Entry('tradingBot/grid/min-investment', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_grid_adjust_investment = privatePostTradingBotGridAdjustInvestment = Entry('tradingBot/grid/adjust-investment', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_signal_create_signal = privatePostTradingBotSignalCreateSignal = Entry('tradingBot/signal/create-signal', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_signal_order_algo = privatePostTradingBotSignalOrderAlgo = Entry('tradingBot/signal/order-algo', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_signal_stop_order_algo = privatePostTradingBotSignalStopOrderAlgo = Entry('tradingBot/signal/stop-order-algo', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_signal_margin_balance = privatePostTradingBotSignalMarginBalance = Entry('tradingBot/signal/margin-balance', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_signal_amendtpsl = privatePostTradingBotSignalAmendTPSL = Entry('tradingBot/signal/amendTPSL', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_signal_set_instruments = privatePostTradingBotSignalSetInstruments = Entry('tradingBot/signal/set-instruments', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_signal_close_position = privatePostTradingBotSignalClosePosition = Entry('tradingBot/signal/close-position', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_signal_sub_order = privatePostTradingBotSignalSubOrder = Entry('tradingBot/signal/sub-order', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_signal_cancel_sub_order = privatePostTradingBotSignalCancelSubOrder = Entry('tradingBot/signal/cancel-sub-order', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_recurring_order_algo = privatePostTradingBotRecurringOrderAlgo = Entry('tradingBot/recurring/order-algo', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_recurring_amend_order_algo = privatePostTradingBotRecurringAmendOrderAlgo = Entry('tradingBot/recurring/amend-order-algo', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_recurring_stop_order_algo = privatePostTradingBotRecurringStopOrderAlgo = Entry('tradingBot/recurring/stop-order-algo', 'private', 'POST', {'cost': 1}) + private_post_finance_savings_purchase_redempt = privatePostFinanceSavingsPurchaseRedempt = Entry('finance/savings/purchase-redempt', 'private', 'POST', {'cost': 1.6666666666666667}) + private_post_finance_savings_set_lending_rate = privatePostFinanceSavingsSetLendingRate = Entry('finance/savings/set-lending-rate', 'private', 'POST', {'cost': 1.6666666666666667}) + private_post_finance_staking_defi_purchase = privatePostFinanceStakingDefiPurchase = Entry('finance/staking-defi/purchase', 'private', 'POST', {'cost': 3}) + private_post_finance_staking_defi_redeem = privatePostFinanceStakingDefiRedeem = Entry('finance/staking-defi/redeem', 'private', 'POST', {'cost': 3}) + private_post_finance_staking_defi_cancel = privatePostFinanceStakingDefiCancel = Entry('finance/staking-defi/cancel', 'private', 'POST', {'cost': 3}) + private_post_finance_staking_defi_eth_purchase = privatePostFinanceStakingDefiEthPurchase = Entry('finance/staking-defi/eth/purchase', 'private', 'POST', {'cost': 5}) + private_post_finance_staking_defi_eth_redeem = privatePostFinanceStakingDefiEthRedeem = Entry('finance/staking-defi/eth/redeem', 'private', 'POST', {'cost': 5}) + private_post_finance_staking_defi_sol_purchase = privatePostFinanceStakingDefiSolPurchase = Entry('finance/staking-defi/sol/purchase', 'private', 'POST', {'cost': 5}) + private_post_finance_staking_defi_sol_redeem = privatePostFinanceStakingDefiSolRedeem = Entry('finance/staking-defi/sol/redeem', 'private', 'POST', {'cost': 5}) + private_post_copytrading_algo_order = privatePostCopytradingAlgoOrder = Entry('copytrading/algo-order', 'private', 'POST', {'cost': 1}) + private_post_copytrading_close_subposition = privatePostCopytradingCloseSubposition = Entry('copytrading/close-subposition', 'private', 'POST', {'cost': 1}) + private_post_copytrading_set_instruments = privatePostCopytradingSetInstruments = Entry('copytrading/set-instruments', 'private', 'POST', {'cost': 4}) + private_post_copytrading_first_copy_settings = privatePostCopytradingFirstCopySettings = Entry('copytrading/first-copy-settings', 'private', 'POST', {'cost': 4}) + private_post_copytrading_amend_copy_settings = privatePostCopytradingAmendCopySettings = Entry('copytrading/amend-copy-settings', 'private', 'POST', {'cost': 4}) + private_post_copytrading_stop_copy_trading = privatePostCopytradingStopCopyTrading = Entry('copytrading/stop-copy-trading', 'private', 'POST', {'cost': 4}) + private_post_copytrading_batch_set_leverage = privatePostCopytradingBatchSetLeverage = Entry('copytrading/batch-set-leverage', 'private', 'POST', {'cost': 4}) + private_post_broker_nd_create_subaccount = privatePostBrokerNdCreateSubaccount = Entry('broker/nd/create-subaccount', 'private', 'POST', {'cost': 0.25}) + private_post_broker_nd_delete_subaccount = privatePostBrokerNdDeleteSubaccount = Entry('broker/nd/delete-subaccount', 'private', 'POST', {'cost': 1}) + private_post_broker_nd_subaccount_apikey = privatePostBrokerNdSubaccountApikey = Entry('broker/nd/subaccount/apikey', 'private', 'POST', {'cost': 0.25}) + private_post_broker_nd_subaccount_modify_apikey = privatePostBrokerNdSubaccountModifyApikey = Entry('broker/nd/subaccount/modify-apikey', 'private', 'POST', {'cost': 1}) + private_post_broker_nd_subaccount_delete_apikey = privatePostBrokerNdSubaccountDeleteApikey = Entry('broker/nd/subaccount/delete-apikey', 'private', 'POST', {'cost': 1}) + private_post_broker_nd_set_subaccount_level = privatePostBrokerNdSetSubaccountLevel = Entry('broker/nd/set-subaccount-level', 'private', 'POST', {'cost': 4}) + private_post_broker_nd_set_subaccount_fee_rate = privatePostBrokerNdSetSubaccountFeeRate = Entry('broker/nd/set-subaccount-fee-rate', 'private', 'POST', {'cost': 4}) + private_post_broker_nd_set_subaccount_assets = privatePostBrokerNdSetSubaccountAssets = Entry('broker/nd/set-subaccount-assets', 'private', 'POST', {'cost': 0.25}) + private_post_asset_broker_nd_subaccount_deposit_address = privatePostAssetBrokerNdSubaccountDepositAddress = Entry('asset/broker/nd/subaccount-deposit-address', 'private', 'POST', {'cost': 1}) + private_post_asset_broker_nd_modify_subaccount_deposit_address = privatePostAssetBrokerNdModifySubaccountDepositAddress = Entry('asset/broker/nd/modify-subaccount-deposit-address', 'private', 'POST', {'cost': 1.6666666666666667}) + private_post_broker_nd_rebate_per_orders = privatePostBrokerNdRebatePerOrders = Entry('broker/nd/rebate-per-orders', 'private', 'POST', {'cost': 36000}) + private_post_finance_sfp_dcd_quote = privatePostFinanceSfpDcdQuote = Entry('finance/sfp/dcd/quote', 'private', 'POST', {'cost': 10}) + private_post_finance_sfp_dcd_order = privatePostFinanceSfpDcdOrder = Entry('finance/sfp/dcd/order', 'private', 'POST', {'cost': 10}) + private_post_broker_nd_report_subaccount_ip = privatePostBrokerNdReportSubaccountIp = Entry('broker/nd/report-subaccount-ip', 'private', 'POST', {'cost': 0.25}) + private_post_broker_fd_rebate_per_orders = privatePostBrokerFdRebatePerOrders = Entry('broker/fd/rebate-per-orders', 'private', 'POST', {'cost': 36000}) diff --git a/ccxt/abstract/ndax.py b/ccxt/abstract/ndax.py new file mode 100644 index 0000000..348d27f --- /dev/null +++ b/ccxt/abstract/ndax.py @@ -0,0 +1,97 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_activate2fa = publicGetActivate2FA = Entry('Activate2FA', 'public', 'GET', {'cost': 1}) + public_get_authenticate2fa = publicGetAuthenticate2FA = Entry('Authenticate2FA', 'public', 'GET', {'cost': 1}) + public_get_authenticateuser = publicGetAuthenticateUser = Entry('AuthenticateUser', 'public', 'GET', {'cost': 1}) + public_get_getl2snapshot = publicGetGetL2Snapshot = Entry('GetL2Snapshot', 'public', 'GET', {'cost': 1}) + public_get_getlevel1 = publicGetGetLevel1 = Entry('GetLevel1', 'public', 'GET', {'cost': 1}) + public_get_getvalidate2farequiredendpoints = publicGetGetValidate2FARequiredEndpoints = Entry('GetValidate2FARequiredEndpoints', 'public', 'GET', {'cost': 1}) + public_get_logout = publicGetLogOut = Entry('LogOut', 'public', 'GET', {'cost': 1}) + public_get_gettickerhistory = publicGetGetTickerHistory = Entry('GetTickerHistory', 'public', 'GET', {'cost': 1}) + public_get_getproduct = publicGetGetProduct = Entry('GetProduct', 'public', 'GET', {'cost': 1}) + public_get_getproducts = publicGetGetProducts = Entry('GetProducts', 'public', 'GET', {'cost': 1}) + public_get_getinstrument = publicGetGetInstrument = Entry('GetInstrument', 'public', 'GET', {'cost': 1}) + public_get_getinstruments = publicGetGetInstruments = Entry('GetInstruments', 'public', 'GET', {'cost': 1}) + public_get_ping = publicGetPing = Entry('Ping', 'public', 'GET', {'cost': 1}) + public_get_trades = publicGetTrades = Entry('trades', 'public', 'GET', {'cost': 1}) + public_get_getlasttrades = publicGetGetLastTrades = Entry('GetLastTrades', 'public', 'GET', {'cost': 1}) + public_get_subscribelevel1 = publicGetSubscribeLevel1 = Entry('SubscribeLevel1', 'public', 'GET', {'cost': 1}) + public_get_subscribelevel2 = publicGetSubscribeLevel2 = Entry('SubscribeLevel2', 'public', 'GET', {'cost': 1}) + public_get_subscribeticker = publicGetSubscribeTicker = Entry('SubscribeTicker', 'public', 'GET', {'cost': 1}) + public_get_subscribetrades = publicGetSubscribeTrades = Entry('SubscribeTrades', 'public', 'GET', {'cost': 1}) + public_get_subscribeblocktrades = publicGetSubscribeBlockTrades = Entry('SubscribeBlockTrades', 'public', 'GET', {'cost': 1}) + public_get_unsubscribeblocktrades = publicGetUnsubscribeBlockTrades = Entry('UnsubscribeBlockTrades', 'public', 'GET', {'cost': 1}) + public_get_unsubscribelevel1 = publicGetUnsubscribeLevel1 = Entry('UnsubscribeLevel1', 'public', 'GET', {'cost': 1}) + public_get_unsubscribelevel2 = publicGetUnsubscribeLevel2 = Entry('UnsubscribeLevel2', 'public', 'GET', {'cost': 1}) + public_get_unsubscribeticker = publicGetUnsubscribeTicker = Entry('UnsubscribeTicker', 'public', 'GET', {'cost': 1}) + public_get_unsubscribetrades = publicGetUnsubscribeTrades = Entry('UnsubscribeTrades', 'public', 'GET', {'cost': 1}) + public_get_authenticate = publicGetAuthenticate = Entry('Authenticate', 'public', 'GET', {'cost': 1}) + private_get_getuseraccountinfos = privateGetGetUserAccountInfos = Entry('GetUserAccountInfos', 'private', 'GET', {'cost': 1}) + private_get_getuseraccounts = privateGetGetUserAccounts = Entry('GetUserAccounts', 'private', 'GET', {'cost': 1}) + private_get_getuseraffiliatecount = privateGetGetUserAffiliateCount = Entry('GetUserAffiliateCount', 'private', 'GET', {'cost': 1}) + private_get_getuseraffiliatetag = privateGetGetUserAffiliateTag = Entry('GetUserAffiliateTag', 'private', 'GET', {'cost': 1}) + private_get_getuserconfig = privateGetGetUserConfig = Entry('GetUserConfig', 'private', 'GET', {'cost': 1}) + private_get_getallunredacteduserconfigsforuser = privateGetGetAllUnredactedUserConfigsForUser = Entry('GetAllUnredactedUserConfigsForUser', 'private', 'GET', {'cost': 1}) + private_get_getunredacteduserconfigbykey = privateGetGetUnredactedUserConfigByKey = Entry('GetUnredactedUserConfigByKey', 'private', 'GET', {'cost': 1}) + private_get_getuserdevices = privateGetGetUserDevices = Entry('GetUserDevices', 'private', 'GET', {'cost': 1}) + private_get_getuserreporttickets = privateGetGetUserReportTickets = Entry('GetUserReportTickets', 'private', 'GET', {'cost': 1}) + private_get_getuserreportwriterresultrecords = privateGetGetUserReportWriterResultRecords = Entry('GetUserReportWriterResultRecords', 'private', 'GET', {'cost': 1}) + private_get_getaccountinfo = privateGetGetAccountInfo = Entry('GetAccountInfo', 'private', 'GET', {'cost': 1}) + private_get_getaccountpositions = privateGetGetAccountPositions = Entry('GetAccountPositions', 'private', 'GET', {'cost': 1}) + private_get_getallaccountconfigs = privateGetGetAllAccountConfigs = Entry('GetAllAccountConfigs', 'private', 'GET', {'cost': 1}) + private_get_gettreasuryproductsforaccount = privateGetGetTreasuryProductsForAccount = Entry('GetTreasuryProductsForAccount', 'private', 'GET', {'cost': 1}) + private_get_getaccounttrades = privateGetGetAccountTrades = Entry('GetAccountTrades', 'private', 'GET', {'cost': 1}) + private_get_getaccounttransactions = privateGetGetAccountTransactions = Entry('GetAccountTransactions', 'private', 'GET', {'cost': 1}) + private_get_getopentradereports = privateGetGetOpenTradeReports = Entry('GetOpenTradeReports', 'private', 'GET', {'cost': 1}) + private_get_getallopentradereports = privateGetGetAllOpenTradeReports = Entry('GetAllOpenTradeReports', 'private', 'GET', {'cost': 1}) + private_get_gettradeshistory = privateGetGetTradesHistory = Entry('GetTradesHistory', 'private', 'GET', {'cost': 1}) + private_get_getopenorders = privateGetGetOpenOrders = Entry('GetOpenOrders', 'private', 'GET', {'cost': 1}) + private_get_getopenquotes = privateGetGetOpenQuotes = Entry('GetOpenQuotes', 'private', 'GET', {'cost': 1}) + private_get_getorderfee = privateGetGetOrderFee = Entry('GetOrderFee', 'private', 'GET', {'cost': 1}) + private_get_getorderhistory = privateGetGetOrderHistory = Entry('GetOrderHistory', 'private', 'GET', {'cost': 1}) + private_get_getordershistory = privateGetGetOrdersHistory = Entry('GetOrdersHistory', 'private', 'GET', {'cost': 1}) + private_get_getorderstatus = privateGetGetOrderStatus = Entry('GetOrderStatus', 'private', 'GET', {'cost': 1}) + private_get_getomsfeetiers = privateGetGetOmsFeeTiers = Entry('GetOmsFeeTiers', 'private', 'GET', {'cost': 1}) + private_get_getaccountdeposittransactions = privateGetGetAccountDepositTransactions = Entry('GetAccountDepositTransactions', 'private', 'GET', {'cost': 1}) + private_get_getaccountwithdrawtransactions = privateGetGetAccountWithdrawTransactions = Entry('GetAccountWithdrawTransactions', 'private', 'GET', {'cost': 1}) + private_get_getalldepositrequestinfotemplates = privateGetGetAllDepositRequestInfoTemplates = Entry('GetAllDepositRequestInfoTemplates', 'private', 'GET', {'cost': 1}) + private_get_getdepositinfo = privateGetGetDepositInfo = Entry('GetDepositInfo', 'private', 'GET', {'cost': 1}) + private_get_getdepositrequestinfotemplate = privateGetGetDepositRequestInfoTemplate = Entry('GetDepositRequestInfoTemplate', 'private', 'GET', {'cost': 1}) + private_get_getdeposits = privateGetGetDeposits = Entry('GetDeposits', 'private', 'GET', {'cost': 1}) + private_get_getdepositticket = privateGetGetDepositTicket = Entry('GetDepositTicket', 'private', 'GET', {'cost': 1}) + private_get_getdeposittickets = privateGetGetDepositTickets = Entry('GetDepositTickets', 'private', 'GET', {'cost': 1}) + private_get_getomswithdrawfees = privateGetGetOMSWithdrawFees = Entry('GetOMSWithdrawFees', 'private', 'GET', {'cost': 1}) + private_get_getwithdrawfee = privateGetGetWithdrawFee = Entry('GetWithdrawFee', 'private', 'GET', {'cost': 1}) + private_get_getwithdraws = privateGetGetWithdraws = Entry('GetWithdraws', 'private', 'GET', {'cost': 1}) + private_get_getwithdrawtemplate = privateGetGetWithdrawTemplate = Entry('GetWithdrawTemplate', 'private', 'GET', {'cost': 1}) + private_get_getwithdrawtemplatetypes = privateGetGetWithdrawTemplateTypes = Entry('GetWithdrawTemplateTypes', 'private', 'GET', {'cost': 1}) + private_get_getwithdrawticket = privateGetGetWithdrawTicket = Entry('GetWithdrawTicket', 'private', 'GET', {'cost': 1}) + private_get_getwithdrawtickets = privateGetGetWithdrawTickets = Entry('GetWithdrawTickets', 'private', 'GET', {'cost': 1}) + private_post_adduseraffiliatetag = privatePostAddUserAffiliateTag = Entry('AddUserAffiliateTag', 'private', 'POST', {'cost': 1}) + private_post_canceluserreport = privatePostCancelUserReport = Entry('CancelUserReport', 'private', 'POST', {'cost': 1}) + private_post_registernewdevice = privatePostRegisterNewDevice = Entry('RegisterNewDevice', 'private', 'POST', {'cost': 1}) + private_post_subscribeaccountevents = privatePostSubscribeAccountEvents = Entry('SubscribeAccountEvents', 'private', 'POST', {'cost': 1}) + private_post_updateuseraffiliatetag = privatePostUpdateUserAffiliateTag = Entry('UpdateUserAffiliateTag', 'private', 'POST', {'cost': 1}) + private_post_generatetradeactivityreport = privatePostGenerateTradeActivityReport = Entry('GenerateTradeActivityReport', 'private', 'POST', {'cost': 1}) + private_post_generatetransactionactivityreport = privatePostGenerateTransactionActivityReport = Entry('GenerateTransactionActivityReport', 'private', 'POST', {'cost': 1}) + private_post_generatetreasuryactivityreport = privatePostGenerateTreasuryActivityReport = Entry('GenerateTreasuryActivityReport', 'private', 'POST', {'cost': 1}) + private_post_scheduletradeactivityreport = privatePostScheduleTradeActivityReport = Entry('ScheduleTradeActivityReport', 'private', 'POST', {'cost': 1}) + private_post_scheduletransactionactivityreport = privatePostScheduleTransactionActivityReport = Entry('ScheduleTransactionActivityReport', 'private', 'POST', {'cost': 1}) + private_post_scheduletreasuryactivityreport = privatePostScheduleTreasuryActivityReport = Entry('ScheduleTreasuryActivityReport', 'private', 'POST', {'cost': 1}) + private_post_cancelallorders = privatePostCancelAllOrders = Entry('CancelAllOrders', 'private', 'POST', {'cost': 1}) + private_post_cancelorder = privatePostCancelOrder = Entry('CancelOrder', 'private', 'POST', {'cost': 1}) + private_post_cancelquote = privatePostCancelQuote = Entry('CancelQuote', 'private', 'POST', {'cost': 1}) + private_post_cancelreplaceorder = privatePostCancelReplaceOrder = Entry('CancelReplaceOrder', 'private', 'POST', {'cost': 1}) + private_post_createquote = privatePostCreateQuote = Entry('CreateQuote', 'private', 'POST', {'cost': 1}) + private_post_modifyorder = privatePostModifyOrder = Entry('ModifyOrder', 'private', 'POST', {'cost': 1}) + private_post_sendorder = privatePostSendOrder = Entry('SendOrder', 'private', 'POST', {'cost': 1}) + private_post_submitblocktrade = privatePostSubmitBlockTrade = Entry('SubmitBlockTrade', 'private', 'POST', {'cost': 1}) + private_post_updatequote = privatePostUpdateQuote = Entry('UpdateQuote', 'private', 'POST', {'cost': 1}) + private_post_cancelwithdraw = privatePostCancelWithdraw = Entry('CancelWithdraw', 'private', 'POST', {'cost': 1}) + private_post_createdepositticket = privatePostCreateDepositTicket = Entry('CreateDepositTicket', 'private', 'POST', {'cost': 1}) + private_post_createwithdrawticket = privatePostCreateWithdrawTicket = Entry('CreateWithdrawTicket', 'private', 'POST', {'cost': 1}) + private_post_submitdepositticketcomment = privatePostSubmitDepositTicketComment = Entry('SubmitDepositTicketComment', 'private', 'POST', {'cost': 1}) + private_post_submitwithdrawticketcomment = privatePostSubmitWithdrawTicketComment = Entry('SubmitWithdrawTicketComment', 'private', 'POST', {'cost': 1}) + private_post_getorderhistorybyorderid = privatePostGetOrderHistoryByOrderId = Entry('GetOrderHistoryByOrderId', 'private', 'POST', {'cost': 1}) diff --git a/ccxt/abstract/novadax.py b/ccxt/abstract/novadax.py new file mode 100644 index 0000000..e06cc91 --- /dev/null +++ b/ccxt/abstract/novadax.py @@ -0,0 +1,29 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_common_symbol = publicGetCommonSymbol = Entry('common/symbol', 'public', 'GET', {'cost': 1}) + public_get_common_symbols = publicGetCommonSymbols = Entry('common/symbols', 'public', 'GET', {'cost': 1}) + public_get_common_timestamp = publicGetCommonTimestamp = Entry('common/timestamp', 'public', 'GET', {'cost': 1}) + public_get_market_tickers = publicGetMarketTickers = Entry('market/tickers', 'public', 'GET', {'cost': 5}) + public_get_market_ticker = publicGetMarketTicker = Entry('market/ticker', 'public', 'GET', {'cost': 1}) + public_get_market_depth = publicGetMarketDepth = Entry('market/depth', 'public', 'GET', {'cost': 1}) + public_get_market_trades = publicGetMarketTrades = Entry('market/trades', 'public', 'GET', {'cost': 5}) + public_get_market_kline_history = publicGetMarketKlineHistory = Entry('market/kline/history', 'public', 'GET', {'cost': 5}) + private_get_orders_get = privateGetOrdersGet = Entry('orders/get', 'private', 'GET', {'cost': 1}) + private_get_orders_list = privateGetOrdersList = Entry('orders/list', 'private', 'GET', {'cost': 10}) + private_get_orders_fill = privateGetOrdersFill = Entry('orders/fill', 'private', 'GET', {'cost': 3}) + private_get_orders_fills = privateGetOrdersFills = Entry('orders/fills', 'private', 'GET', {'cost': 10}) + private_get_account_getbalance = privateGetAccountGetBalance = Entry('account/getBalance', 'private', 'GET', {'cost': 1}) + private_get_account_subs = privateGetAccountSubs = Entry('account/subs', 'private', 'GET', {'cost': 1}) + private_get_account_subs_balance = privateGetAccountSubsBalance = Entry('account/subs/balance', 'private', 'GET', {'cost': 1}) + private_get_account_subs_transfer_record = privateGetAccountSubsTransferRecord = Entry('account/subs/transfer/record', 'private', 'GET', {'cost': 10}) + private_get_wallet_query_deposit_withdraw = privateGetWalletQueryDepositWithdraw = Entry('wallet/query/deposit-withdraw', 'private', 'GET', {'cost': 3}) + private_post_orders_create = privatePostOrdersCreate = Entry('orders/create', 'private', 'POST', {'cost': 5}) + private_post_orders_batch_create = privatePostOrdersBatchCreate = Entry('orders/batch-create', 'private', 'POST', {'cost': 50}) + private_post_orders_cancel = privatePostOrdersCancel = Entry('orders/cancel', 'private', 'POST', {'cost': 1}) + private_post_orders_batch_cancel = privatePostOrdersBatchCancel = Entry('orders/batch-cancel', 'private', 'POST', {'cost': 10}) + private_post_orders_cancel_by_symbol = privatePostOrdersCancelBySymbol = Entry('orders/cancel-by-symbol', 'private', 'POST', {'cost': 10}) + private_post_account_subs_transfer = privatePostAccountSubsTransfer = Entry('account/subs/transfer', 'private', 'POST', {'cost': 5}) + private_post_wallet_withdraw_coin = privatePostWalletWithdrawCoin = Entry('wallet/withdraw/coin', 'private', 'POST', {'cost': 3}) + private_post_account_withdraw_coin = privatePostAccountWithdrawCoin = Entry('account/withdraw/coin', 'private', 'POST', {'cost': 3}) diff --git a/ccxt/abstract/oceanex.py b/ccxt/abstract/oceanex.py new file mode 100644 index 0000000..99ccc77 --- /dev/null +++ b/ccxt/abstract/oceanex.py @@ -0,0 +1,27 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_markets = publicGetMarkets = Entry('markets', 'public', 'GET', {}) + public_get_tickers_pair = publicGetTickersPair = Entry('tickers/{pair}', 'public', 'GET', {}) + public_get_tickers_multi = publicGetTickersMulti = Entry('tickers_multi', 'public', 'GET', {}) + public_get_order_book = publicGetOrderBook = Entry('order_book', 'public', 'GET', {}) + public_get_order_book_multi = publicGetOrderBookMulti = Entry('order_book/multi', 'public', 'GET', {}) + public_get_fees_trading = publicGetFeesTrading = Entry('fees/trading', 'public', 'GET', {}) + public_get_trades = publicGetTrades = Entry('trades', 'public', 'GET', {}) + public_get_timestamp = publicGetTimestamp = Entry('timestamp', 'public', 'GET', {}) + public_post_k = publicPostK = Entry('k', 'public', 'POST', {}) + private_get_key = privateGetKey = Entry('key', 'private', 'GET', {}) + private_get_members_me = privateGetMembersMe = Entry('members/me', 'private', 'GET', {}) + private_get_orders = privateGetOrders = Entry('orders', 'private', 'GET', {}) + private_get_orders_filter = privateGetOrdersFilter = Entry('orders/filter', 'private', 'GET', {}) + private_post_orders = privatePostOrders = Entry('orders', 'private', 'POST', {}) + private_post_orders_multi = privatePostOrdersMulti = Entry('orders/multi', 'private', 'POST', {}) + private_post_order_delete = privatePostOrderDelete = Entry('order/delete', 'private', 'POST', {}) + private_post_order_delete_multi = privatePostOrderDeleteMulti = Entry('order/delete/multi', 'private', 'POST', {}) + private_post_orders_clear = privatePostOrdersClear = Entry('orders/clear', 'private', 'POST', {}) + private_post_withdraws_special_new = privatePostWithdrawsSpecialNew = Entry('/withdraws/special/new', 'private', 'POST', {}) + private_post_deposit_address = privatePostDepositAddress = Entry('/deposit_address', 'private', 'POST', {}) + private_post_deposit_addresses = privatePostDepositAddresses = Entry('/deposit_addresses', 'private', 'POST', {}) + private_post_deposit_history = privatePostDepositHistory = Entry('/deposit_history', 'private', 'POST', {}) + private_post_withdraw_history = privatePostWithdrawHistory = Entry('/withdraw_history', 'private', 'POST', {}) diff --git a/ccxt/abstract/okx.py b/ccxt/abstract/okx.py new file mode 100644 index 0000000..ac9e2a1 --- /dev/null +++ b/ccxt/abstract/okx.py @@ -0,0 +1,351 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_market_books_full = publicGetMarketBooksFull = Entry('market/books-full', 'public', 'GET', {'cost': 2}) + public_get_market_tickers = publicGetMarketTickers = Entry('market/tickers', 'public', 'GET', {'cost': 1}) + public_get_market_ticker = publicGetMarketTicker = Entry('market/ticker', 'public', 'GET', {'cost': 1}) + public_get_market_index_tickers = publicGetMarketIndexTickers = Entry('market/index-tickers', 'public', 'GET', {'cost': 1}) + public_get_market_books = publicGetMarketBooks = Entry('market/books', 'public', 'GET', {'cost': 0.5}) + public_get_market_books_lite = publicGetMarketBooksLite = Entry('market/books-lite', 'public', 'GET', {'cost': 1.6666666666666667}) + public_get_market_candles = publicGetMarketCandles = Entry('market/candles', 'public', 'GET', {'cost': 0.5}) + public_get_market_history_candles = publicGetMarketHistoryCandles = Entry('market/history-candles', 'public', 'GET', {'cost': 1}) + public_get_market_index_candles = publicGetMarketIndexCandles = Entry('market/index-candles', 'public', 'GET', {'cost': 1}) + public_get_market_history_index_candles = publicGetMarketHistoryIndexCandles = Entry('market/history-index-candles', 'public', 'GET', {'cost': 2}) + public_get_market_mark_price_candles = publicGetMarketMarkPriceCandles = Entry('market/mark-price-candles', 'public', 'GET', {'cost': 1}) + public_get_market_history_mark_price_candles = publicGetMarketHistoryMarkPriceCandles = Entry('market/history-mark-price-candles', 'public', 'GET', {'cost': 2}) + public_get_market_trades = publicGetMarketTrades = Entry('market/trades', 'public', 'GET', {'cost': 0.2}) + public_get_market_history_trades = publicGetMarketHistoryTrades = Entry('market/history-trades', 'public', 'GET', {'cost': 2}) + public_get_market_option_instrument_family_trades = publicGetMarketOptionInstrumentFamilyTrades = Entry('market/option/instrument-family-trades', 'public', 'GET', {'cost': 1}) + public_get_market_platform_24_volume = publicGetMarketPlatform24Volume = Entry('market/platform-24-volume', 'public', 'GET', {'cost': 10}) + public_get_market_open_oracle = publicGetMarketOpenOracle = Entry('market/open-oracle', 'public', 'GET', {'cost': 50}) + public_get_market_exchange_rate = publicGetMarketExchangeRate = Entry('market/exchange-rate', 'public', 'GET', {'cost': 20}) + public_get_market_index_components = publicGetMarketIndexComponents = Entry('market/index-components', 'public', 'GET', {'cost': 1}) + public_get_public_market_data_history = publicGetPublicMarketDataHistory = Entry('public/market-data-history', 'public', 'GET', {'cost': 4}) + public_get_public_economic_calendar = publicGetPublicEconomicCalendar = Entry('public/economic-calendar', 'public', 'GET', {'cost': 50}) + public_get_market_block_tickers = publicGetMarketBlockTickers = Entry('market/block-tickers', 'public', 'GET', {'cost': 1}) + public_get_market_block_ticker = publicGetMarketBlockTicker = Entry('market/block-ticker', 'public', 'GET', {'cost': 1}) + public_get_public_block_trades = publicGetPublicBlockTrades = Entry('public/block-trades', 'public', 'GET', {'cost': 1}) + public_get_public_instruments = publicGetPublicInstruments = Entry('public/instruments', 'public', 'GET', {'cost': 1}) + public_get_public_delivery_exercise_history = publicGetPublicDeliveryExerciseHistory = Entry('public/delivery-exercise-history', 'public', 'GET', {'cost': 0.5}) + public_get_public_open_interest = publicGetPublicOpenInterest = Entry('public/open-interest', 'public', 'GET', {'cost': 1}) + public_get_public_funding_rate = publicGetPublicFundingRate = Entry('public/funding-rate', 'public', 'GET', {'cost': 1}) + public_get_public_funding_rate_history = publicGetPublicFundingRateHistory = Entry('public/funding-rate-history', 'public', 'GET', {'cost': 1}) + public_get_public_price_limit = publicGetPublicPriceLimit = Entry('public/price-limit', 'public', 'GET', {'cost': 1}) + public_get_public_opt_summary = publicGetPublicOptSummary = Entry('public/opt-summary', 'public', 'GET', {'cost': 1}) + public_get_public_estimated_price = publicGetPublicEstimatedPrice = Entry('public/estimated-price', 'public', 'GET', {'cost': 2}) + public_get_public_discount_rate_interest_free_quota = publicGetPublicDiscountRateInterestFreeQuota = Entry('public/discount-rate-interest-free-quota', 'public', 'GET', {'cost': 10}) + public_get_public_time = publicGetPublicTime = Entry('public/time', 'public', 'GET', {'cost': 2}) + public_get_public_mark_price = publicGetPublicMarkPrice = Entry('public/mark-price', 'public', 'GET', {'cost': 2}) + public_get_public_position_tiers = publicGetPublicPositionTiers = Entry('public/position-tiers', 'public', 'GET', {'cost': 2}) + public_get_public_interest_rate_loan_quota = publicGetPublicInterestRateLoanQuota = Entry('public/interest-rate-loan-quota', 'public', 'GET', {'cost': 10}) + public_get_public_vip_interest_rate_loan_quota = publicGetPublicVipInterestRateLoanQuota = Entry('public/vip-interest-rate-loan-quota', 'public', 'GET', {'cost': 10}) + public_get_public_underlying = publicGetPublicUnderlying = Entry('public/underlying', 'public', 'GET', {'cost': 1}) + public_get_public_insurance_fund = publicGetPublicInsuranceFund = Entry('public/insurance-fund', 'public', 'GET', {'cost': 2}) + public_get_public_convert_contract_coin = publicGetPublicConvertContractCoin = Entry('public/convert-contract-coin', 'public', 'GET', {'cost': 2}) + public_get_public_option_trades = publicGetPublicOptionTrades = Entry('public/option-trades', 'public', 'GET', {'cost': 1}) + public_get_public_instrument_tick_bands = publicGetPublicInstrumentTickBands = Entry('public/instrument-tick-bands', 'public', 'GET', {'cost': 4}) + public_get_rubik_stat_trading_data_support_coin = publicGetRubikStatTradingDataSupportCoin = Entry('rubik/stat/trading-data/support-coin', 'public', 'GET', {'cost': 4}) + public_get_rubik_stat_taker_volume = publicGetRubikStatTakerVolume = Entry('rubik/stat/taker-volume', 'public', 'GET', {'cost': 4}) + public_get_rubik_stat_margin_loan_ratio = publicGetRubikStatMarginLoanRatio = Entry('rubik/stat/margin/loan-ratio', 'public', 'GET', {'cost': 4}) + public_get_rubik_stat_contracts_long_short_account_ratio = publicGetRubikStatContractsLongShortAccountRatio = Entry('rubik/stat/contracts/long-short-account-ratio', 'public', 'GET', {'cost': 4}) + public_get_rubik_stat_contracts_long_short_account_ratio_contract = publicGetRubikStatContractsLongShortAccountRatioContract = Entry('rubik/stat/contracts/long-short-account-ratio-contract', 'public', 'GET', {'cost': 4}) + public_get_rubik_stat_contracts_open_interest_volume = publicGetRubikStatContractsOpenInterestVolume = Entry('rubik/stat/contracts/open-interest-volume', 'public', 'GET', {'cost': 4}) + public_get_rubik_stat_option_open_interest_volume = publicGetRubikStatOptionOpenInterestVolume = Entry('rubik/stat/option/open-interest-volume', 'public', 'GET', {'cost': 4}) + public_get_rubik_stat_option_open_interest_volume_ratio = publicGetRubikStatOptionOpenInterestVolumeRatio = Entry('rubik/stat/option/open-interest-volume-ratio', 'public', 'GET', {'cost': 4}) + public_get_rubik_stat_option_open_interest_volume_expiry = publicGetRubikStatOptionOpenInterestVolumeExpiry = Entry('rubik/stat/option/open-interest-volume-expiry', 'public', 'GET', {'cost': 4}) + public_get_rubik_stat_option_open_interest_volume_strike = publicGetRubikStatOptionOpenInterestVolumeStrike = Entry('rubik/stat/option/open-interest-volume-strike', 'public', 'GET', {'cost': 4}) + public_get_rubik_stat_option_taker_block_volume = publicGetRubikStatOptionTakerBlockVolume = Entry('rubik/stat/option/taker-block-volume', 'public', 'GET', {'cost': 4}) + public_get_system_status = publicGetSystemStatus = Entry('system/status', 'public', 'GET', {'cost': 50}) + public_get_sprd_spreads = publicGetSprdSpreads = Entry('sprd/spreads', 'public', 'GET', {'cost': 1}) + public_get_sprd_books = publicGetSprdBooks = Entry('sprd/books', 'public', 'GET', {'cost': 0.5}) + public_get_sprd_ticker = publicGetSprdTicker = Entry('sprd/ticker', 'public', 'GET', {'cost': 1}) + public_get_sprd_public_trades = publicGetSprdPublicTrades = Entry('sprd/public-trades', 'public', 'GET', {'cost': 0.2}) + public_get_market_sprd_ticker = publicGetMarketSprdTicker = Entry('market/sprd-ticker', 'public', 'GET', {'cost': 2}) + public_get_market_sprd_candles = publicGetMarketSprdCandles = Entry('market/sprd-candles', 'public', 'GET', {'cost': 2}) + public_get_market_sprd_history_candles = publicGetMarketSprdHistoryCandles = Entry('market/sprd-history-candles', 'public', 'GET', {'cost': 2}) + public_get_tradingbot_grid_ai_param = publicGetTradingBotGridAiParam = Entry('tradingBot/grid/ai-param', 'public', 'GET', {'cost': 1}) + public_get_tradingbot_grid_min_investment = publicGetTradingBotGridMinInvestment = Entry('tradingBot/grid/min-investment', 'public', 'GET', {'cost': 1}) + public_get_tradingbot_public_rsi_back_testing = publicGetTradingBotPublicRsiBackTesting = Entry('tradingBot/public/rsi-back-testing', 'public', 'GET', {'cost': 1}) + public_get_asset_exchange_list = publicGetAssetExchangeList = Entry('asset/exchange-list', 'public', 'GET', {'cost': 1.6666666666666667}) + public_get_finance_staking_defi_eth_apy_history = publicGetFinanceStakingDefiEthApyHistory = Entry('finance/staking-defi/eth/apy-history', 'public', 'GET', {'cost': 1.6666666666666667}) + public_get_finance_staking_defi_sol_apy_history = publicGetFinanceStakingDefiSolApyHistory = Entry('finance/staking-defi/sol/apy-history', 'public', 'GET', {'cost': 1.6666666666666667}) + public_get_finance_savings_lending_rate_summary = publicGetFinanceSavingsLendingRateSummary = Entry('finance/savings/lending-rate-summary', 'public', 'GET', {'cost': 1.6666666666666667}) + public_get_finance_savings_lending_rate_history = publicGetFinanceSavingsLendingRateHistory = Entry('finance/savings/lending-rate-history', 'public', 'GET', {'cost': 1.6666666666666667}) + public_get_finance_fixed_loan_lending_offers = publicGetFinanceFixedLoanLendingOffers = Entry('finance/fixed-loan/lending-offers', 'public', 'GET', {'cost': 3.3333333333333335}) + public_get_finance_fixed_loan_lending_apy_history = publicGetFinanceFixedLoanLendingApyHistory = Entry('finance/fixed-loan/lending-apy-history', 'public', 'GET', {'cost': 3.3333333333333335}) + public_get_finance_fixed_loan_pending_lending_volume = publicGetFinanceFixedLoanPendingLendingVolume = Entry('finance/fixed-loan/pending-lending-volume', 'public', 'GET', {'cost': 3.3333333333333335}) + public_get_finance_sfp_dcd_products = publicGetFinanceSfpDcdProducts = Entry('finance/sfp/dcd/products', 'public', 'GET', {'cost': 0.6666666666666666}) + public_get_copytrading_public_lead_traders = publicGetCopytradingPublicLeadTraders = Entry('copytrading/public-lead-traders', 'public', 'GET', {'cost': 4}) + public_get_copytrading_public_weekly_pnl = publicGetCopytradingPublicWeeklyPnl = Entry('copytrading/public-weekly-pnl', 'public', 'GET', {'cost': 4}) + public_get_copytrading_public_stats = publicGetCopytradingPublicStats = Entry('copytrading/public-stats', 'public', 'GET', {'cost': 4}) + public_get_copytrading_public_preference_currency = publicGetCopytradingPublicPreferenceCurrency = Entry('copytrading/public-preference-currency', 'public', 'GET', {'cost': 4}) + public_get_copytrading_public_current_subpositions = publicGetCopytradingPublicCurrentSubpositions = Entry('copytrading/public-current-subpositions', 'public', 'GET', {'cost': 4}) + public_get_copytrading_public_subpositions_history = publicGetCopytradingPublicSubpositionsHistory = Entry('copytrading/public-subpositions-history', 'public', 'GET', {'cost': 4}) + public_get_support_announcements_types = publicGetSupportAnnouncementsTypes = Entry('support/announcements-types', 'public', 'GET', {'cost': 20}) + private_get_rfq_counterparties = privateGetRfqCounterparties = Entry('rfq/counterparties', 'private', 'GET', {'cost': 4}) + private_get_rfq_maker_instrument_settings = privateGetRfqMakerInstrumentSettings = Entry('rfq/maker-instrument-settings', 'private', 'GET', {'cost': 4}) + private_get_rfq_mmp_config = privateGetRfqMmpConfig = Entry('rfq/mmp-config', 'private', 'GET', {'cost': 4}) + private_get_rfq_rfqs = privateGetRfqRfqs = Entry('rfq/rfqs', 'private', 'GET', {'cost': 10}) + private_get_rfq_quotes = privateGetRfqQuotes = Entry('rfq/quotes', 'private', 'GET', {'cost': 10}) + private_get_rfq_trades = privateGetRfqTrades = Entry('rfq/trades', 'private', 'GET', {'cost': 4}) + private_get_rfq_public_trades = privateGetRfqPublicTrades = Entry('rfq/public-trades', 'private', 'GET', {'cost': 4}) + private_get_sprd_order = privateGetSprdOrder = Entry('sprd/order', 'private', 'GET', {'cost': 0.3333333333333333}) + private_get_sprd_orders_pending = privateGetSprdOrdersPending = Entry('sprd/orders-pending', 'private', 'GET', {'cost': 0.3333333333333333}) + private_get_sprd_orders_history = privateGetSprdOrdersHistory = Entry('sprd/orders-history', 'private', 'GET', {'cost': 0.5}) + private_get_sprd_orders_history_archive = privateGetSprdOrdersHistoryArchive = Entry('sprd/orders-history-archive', 'private', 'GET', {'cost': 0.5}) + private_get_sprd_trades = privateGetSprdTrades = Entry('sprd/trades', 'private', 'GET', {'cost': 0.3333333333333333}) + private_get_trade_order = privateGetTradeOrder = Entry('trade/order', 'private', 'GET', {'cost': 0.3333333333333333}) + private_get_trade_orders_pending = privateGetTradeOrdersPending = Entry('trade/orders-pending', 'private', 'GET', {'cost': 0.3333333333333333}) + private_get_trade_orders_history = privateGetTradeOrdersHistory = Entry('trade/orders-history', 'private', 'GET', {'cost': 0.5}) + private_get_trade_orders_history_archive = privateGetTradeOrdersHistoryArchive = Entry('trade/orders-history-archive', 'private', 'GET', {'cost': 1}) + private_get_trade_fills = privateGetTradeFills = Entry('trade/fills', 'private', 'GET', {'cost': 0.3333333333333333}) + private_get_trade_fills_history = privateGetTradeFillsHistory = Entry('trade/fills-history', 'private', 'GET', {'cost': 2.2}) + private_get_trade_fills_archive = privateGetTradeFillsArchive = Entry('trade/fills-archive', 'private', 'GET', {'cost': 2}) + private_get_trade_order_algo = privateGetTradeOrderAlgo = Entry('trade/order-algo', 'private', 'GET', {'cost': 1}) + private_get_trade_orders_algo_pending = privateGetTradeOrdersAlgoPending = Entry('trade/orders-algo-pending', 'private', 'GET', {'cost': 1}) + private_get_trade_orders_algo_history = privateGetTradeOrdersAlgoHistory = Entry('trade/orders-algo-history', 'private', 'GET', {'cost': 1}) + private_get_trade_easy_convert_currency_list = privateGetTradeEasyConvertCurrencyList = Entry('trade/easy-convert-currency-list', 'private', 'GET', {'cost': 20}) + private_get_trade_easy_convert_history = privateGetTradeEasyConvertHistory = Entry('trade/easy-convert-history', 'private', 'GET', {'cost': 20}) + private_get_trade_one_click_repay_currency_list = privateGetTradeOneClickRepayCurrencyList = Entry('trade/one-click-repay-currency-list', 'private', 'GET', {'cost': 20}) + private_get_trade_one_click_repay_currency_list_v2 = privateGetTradeOneClickRepayCurrencyListV2 = Entry('trade/one-click-repay-currency-list-v2', 'private', 'GET', {'cost': 20}) + private_get_trade_one_click_repay_history = privateGetTradeOneClickRepayHistory = Entry('trade/one-click-repay-history', 'private', 'GET', {'cost': 20}) + private_get_trade_one_click_repay_history_v2 = privateGetTradeOneClickRepayHistoryV2 = Entry('trade/one-click-repay-history-v2', 'private', 'GET', {'cost': 20}) + private_get_trade_account_rate_limit = privateGetTradeAccountRateLimit = Entry('trade/account-rate-limit', 'private', 'GET', {'cost': 1}) + private_get_asset_currencies = privateGetAssetCurrencies = Entry('asset/currencies', 'private', 'GET', {'cost': 1.6666666666666667}) + private_get_asset_balances = privateGetAssetBalances = Entry('asset/balances', 'private', 'GET', {'cost': 1.6666666666666667}) + private_get_asset_non_tradable_assets = privateGetAssetNonTradableAssets = Entry('asset/non-tradable-assets', 'private', 'GET', {'cost': 1.6666666666666667}) + private_get_asset_asset_valuation = privateGetAssetAssetValuation = Entry('asset/asset-valuation', 'private', 'GET', {'cost': 10}) + private_get_asset_transfer_state = privateGetAssetTransferState = Entry('asset/transfer-state', 'private', 'GET', {'cost': 10}) + private_get_asset_bills = privateGetAssetBills = Entry('asset/bills', 'private', 'GET', {'cost': 1.6666666666666667}) + private_get_asset_deposit_lightning = privateGetAssetDepositLightning = Entry('asset/deposit-lightning', 'private', 'GET', {'cost': 5}) + private_get_asset_deposit_address = privateGetAssetDepositAddress = Entry('asset/deposit-address', 'private', 'GET', {'cost': 1.6666666666666667}) + private_get_asset_deposit_history = privateGetAssetDepositHistory = Entry('asset/deposit-history', 'private', 'GET', {'cost': 1.6666666666666667}) + private_get_asset_withdrawal_history = privateGetAssetWithdrawalHistory = Entry('asset/withdrawal-history', 'private', 'GET', {'cost': 1.6666666666666667}) + private_get_asset_deposit_withdraw_status = privateGetAssetDepositWithdrawStatus = Entry('asset/deposit-withdraw-status', 'private', 'GET', {'cost': 20}) + private_get_asset_convert_currencies = privateGetAssetConvertCurrencies = Entry('asset/convert/currencies', 'private', 'GET', {'cost': 1.6666666666666667}) + private_get_asset_convert_currency_pair = privateGetAssetConvertCurrencyPair = Entry('asset/convert/currency-pair', 'private', 'GET', {'cost': 1.6666666666666667}) + private_get_asset_convert_history = privateGetAssetConvertHistory = Entry('asset/convert/history', 'private', 'GET', {'cost': 1.6666666666666667}) + private_get_asset_monthly_statement = privateGetAssetMonthlyStatement = Entry('asset/monthly-statement', 'private', 'GET', {'cost': 2}) + private_get_account_instruments = privateGetAccountInstruments = Entry('account/instruments', 'private', 'GET', {'cost': 1}) + private_get_account_balance = privateGetAccountBalance = Entry('account/balance', 'private', 'GET', {'cost': 2}) + private_get_account_positions = privateGetAccountPositions = Entry('account/positions', 'private', 'GET', {'cost': 2}) + private_get_account_positions_history = privateGetAccountPositionsHistory = Entry('account/positions-history', 'private', 'GET', {'cost': 100}) + private_get_account_account_position_risk = privateGetAccountAccountPositionRisk = Entry('account/account-position-risk', 'private', 'GET', {'cost': 2}) + private_get_account_bills = privateGetAccountBills = Entry('account/bills', 'private', 'GET', {'cost': 1.6666666666666667}) + private_get_account_bills_archive = privateGetAccountBillsArchive = Entry('account/bills-archive', 'private', 'GET', {'cost': 1.6666666666666667}) + private_get_account_bills_history_archive = privateGetAccountBillsHistoryArchive = Entry('account/bills-history-archive', 'private', 'GET', {'cost': 2}) + private_get_account_config = privateGetAccountConfig = Entry('account/config', 'private', 'GET', {'cost': 4}) + private_get_account_max_size = privateGetAccountMaxSize = Entry('account/max-size', 'private', 'GET', {'cost': 1}) + private_get_account_max_avail_size = privateGetAccountMaxAvailSize = Entry('account/max-avail-size', 'private', 'GET', {'cost': 1}) + private_get_account_leverage_info = privateGetAccountLeverageInfo = Entry('account/leverage-info', 'private', 'GET', {'cost': 1}) + private_get_account_adjust_leverage_info = privateGetAccountAdjustLeverageInfo = Entry('account/adjust-leverage-info', 'private', 'GET', {'cost': 4}) + private_get_account_max_loan = privateGetAccountMaxLoan = Entry('account/max-loan', 'private', 'GET', {'cost': 1}) + private_get_account_trade_fee = privateGetAccountTradeFee = Entry('account/trade-fee', 'private', 'GET', {'cost': 4}) + private_get_account_interest_accrued = privateGetAccountInterestAccrued = Entry('account/interest-accrued', 'private', 'GET', {'cost': 4}) + private_get_account_interest_rate = privateGetAccountInterestRate = Entry('account/interest-rate', 'private', 'GET', {'cost': 4}) + private_get_account_max_withdrawal = privateGetAccountMaxWithdrawal = Entry('account/max-withdrawal', 'private', 'GET', {'cost': 1}) + private_get_account_risk_state = privateGetAccountRiskState = Entry('account/risk-state', 'private', 'GET', {'cost': 2}) + private_get_account_quick_margin_borrow_repay_history = privateGetAccountQuickMarginBorrowRepayHistory = Entry('account/quick-margin-borrow-repay-history', 'private', 'GET', {'cost': 4}) + private_get_account_borrow_repay_history = privateGetAccountBorrowRepayHistory = Entry('account/borrow-repay-history', 'private', 'GET', {'cost': 4}) + private_get_account_vip_interest_accrued = privateGetAccountVipInterestAccrued = Entry('account/vip-interest-accrued', 'private', 'GET', {'cost': 4}) + private_get_account_vip_interest_deducted = privateGetAccountVipInterestDeducted = Entry('account/vip-interest-deducted', 'private', 'GET', {'cost': 4}) + private_get_account_vip_loan_order_list = privateGetAccountVipLoanOrderList = Entry('account/vip-loan-order-list', 'private', 'GET', {'cost': 4}) + private_get_account_vip_loan_order_detail = privateGetAccountVipLoanOrderDetail = Entry('account/vip-loan-order-detail', 'private', 'GET', {'cost': 4}) + private_get_account_interest_limits = privateGetAccountInterestLimits = Entry('account/interest-limits', 'private', 'GET', {'cost': 4}) + private_get_account_greeks = privateGetAccountGreeks = Entry('account/greeks', 'private', 'GET', {'cost': 2}) + private_get_account_position_tiers = privateGetAccountPositionTiers = Entry('account/position-tiers', 'private', 'GET', {'cost': 2}) + private_get_account_mmp_config = privateGetAccountMmpConfig = Entry('account/mmp-config', 'private', 'GET', {'cost': 4}) + private_get_account_fixed_loan_borrowing_limit = privateGetAccountFixedLoanBorrowingLimit = Entry('account/fixed-loan/borrowing-limit', 'private', 'GET', {'cost': 4}) + private_get_account_fixed_loan_borrowing_quote = privateGetAccountFixedLoanBorrowingQuote = Entry('account/fixed-loan/borrowing-quote', 'private', 'GET', {'cost': 5}) + private_get_account_fixed_loan_borrowing_orders_list = privateGetAccountFixedLoanBorrowingOrdersList = Entry('account/fixed-loan/borrowing-orders-list', 'private', 'GET', {'cost': 5}) + private_get_account_spot_manual_borrow_repay = privateGetAccountSpotManualBorrowRepay = Entry('account/spot-manual-borrow-repay', 'private', 'GET', {'cost': 30}) + private_get_account_set_auto_repay = privateGetAccountSetAutoRepay = Entry('account/set-auto-repay', 'private', 'GET', {'cost': 4}) + private_get_account_spot_borrow_repay_history = privateGetAccountSpotBorrowRepayHistory = Entry('account/spot-borrow-repay-history', 'private', 'GET', {'cost': 4}) + private_get_account_move_positions_history = privateGetAccountMovePositionsHistory = Entry('account/move-positions-history', 'private', 'GET', {'cost': 10}) + private_get_users_subaccount_list = privateGetUsersSubaccountList = Entry('users/subaccount/list', 'private', 'GET', {'cost': 10}) + private_get_account_subaccount_balances = privateGetAccountSubaccountBalances = Entry('account/subaccount/balances', 'private', 'GET', {'cost': 3.3333333333333335}) + private_get_asset_subaccount_balances = privateGetAssetSubaccountBalances = Entry('asset/subaccount/balances', 'private', 'GET', {'cost': 3.3333333333333335}) + private_get_account_subaccount_max_withdrawal = privateGetAccountSubaccountMaxWithdrawal = Entry('account/subaccount/max-withdrawal', 'private', 'GET', {'cost': 1}) + private_get_asset_subaccount_bills = privateGetAssetSubaccountBills = Entry('asset/subaccount/bills', 'private', 'GET', {'cost': 1.6666666666666667}) + private_get_asset_subaccount_managed_subaccount_bills = privateGetAssetSubaccountManagedSubaccountBills = Entry('asset/subaccount/managed-subaccount-bills', 'private', 'GET', {'cost': 1.6666666666666667}) + private_get_users_entrust_subaccount_list = privateGetUsersEntrustSubaccountList = Entry('users/entrust-subaccount-list', 'private', 'GET', {'cost': 10}) + private_get_account_subaccount_interest_limits = privateGetAccountSubaccountInterestLimits = Entry('account/subaccount/interest-limits', 'private', 'GET', {'cost': 4}) + private_get_users_subaccount_apikey = privateGetUsersSubaccountApikey = Entry('users/subaccount/apikey', 'private', 'GET', {'cost': 10}) + private_get_tradingbot_grid_orders_algo_pending = privateGetTradingBotGridOrdersAlgoPending = Entry('tradingBot/grid/orders-algo-pending', 'private', 'GET', {'cost': 1}) + private_get_tradingbot_grid_orders_algo_history = privateGetTradingBotGridOrdersAlgoHistory = Entry('tradingBot/grid/orders-algo-history', 'private', 'GET', {'cost': 1}) + private_get_tradingbot_grid_orders_algo_details = privateGetTradingBotGridOrdersAlgoDetails = Entry('tradingBot/grid/orders-algo-details', 'private', 'GET', {'cost': 1}) + private_get_tradingbot_grid_sub_orders = privateGetTradingBotGridSubOrders = Entry('tradingBot/grid/sub-orders', 'private', 'GET', {'cost': 1}) + private_get_tradingbot_grid_positions = privateGetTradingBotGridPositions = Entry('tradingBot/grid/positions', 'private', 'GET', {'cost': 1}) + private_get_tradingbot_grid_ai_param = privateGetTradingBotGridAiParam = Entry('tradingBot/grid/ai-param', 'private', 'GET', {'cost': 1}) + private_get_tradingbot_signal_signals = privateGetTradingBotSignalSignals = Entry('tradingBot/signal/signals', 'private', 'GET', {'cost': 1}) + private_get_tradingbot_signal_orders_algo_details = privateGetTradingBotSignalOrdersAlgoDetails = Entry('tradingBot/signal/orders-algo-details', 'private', 'GET', {'cost': 1}) + private_get_tradingbot_signal_orders_algo_history = privateGetTradingBotSignalOrdersAlgoHistory = Entry('tradingBot/signal/orders-algo-history', 'private', 'GET', {'cost': 1}) + private_get_tradingbot_signal_positions = privateGetTradingBotSignalPositions = Entry('tradingBot/signal/positions', 'private', 'GET', {'cost': 1}) + private_get_tradingbot_signal_positions_history = privateGetTradingBotSignalPositionsHistory = Entry('tradingBot/signal/positions-history', 'private', 'GET', {'cost': 1}) + private_get_tradingbot_signal_sub_orders = privateGetTradingBotSignalSubOrders = Entry('tradingBot/signal/sub-orders', 'private', 'GET', {'cost': 1}) + private_get_tradingbot_signal_event_history = privateGetTradingBotSignalEventHistory = Entry('tradingBot/signal/event-history', 'private', 'GET', {'cost': 1}) + private_get_tradingbot_recurring_orders_algo_pending = privateGetTradingBotRecurringOrdersAlgoPending = Entry('tradingBot/recurring/orders-algo-pending', 'private', 'GET', {'cost': 1}) + private_get_tradingbot_recurring_orders_algo_history = privateGetTradingBotRecurringOrdersAlgoHistory = Entry('tradingBot/recurring/orders-algo-history', 'private', 'GET', {'cost': 1}) + private_get_tradingbot_recurring_orders_algo_details = privateGetTradingBotRecurringOrdersAlgoDetails = Entry('tradingBot/recurring/orders-algo-details', 'private', 'GET', {'cost': 1}) + private_get_tradingbot_recurring_sub_orders = privateGetTradingBotRecurringSubOrders = Entry('tradingBot/recurring/sub-orders', 'private', 'GET', {'cost': 1}) + private_get_finance_savings_balance = privateGetFinanceSavingsBalance = Entry('finance/savings/balance', 'private', 'GET', {'cost': 1.6666666666666667}) + private_get_finance_savings_lending_history = privateGetFinanceSavingsLendingHistory = Entry('finance/savings/lending-history', 'private', 'GET', {'cost': 1.6666666666666667}) + private_get_finance_staking_defi_offers = privateGetFinanceStakingDefiOffers = Entry('finance/staking-defi/offers', 'private', 'GET', {'cost': 3.3333333333333335}) + private_get_finance_staking_defi_orders_active = privateGetFinanceStakingDefiOrdersActive = Entry('finance/staking-defi/orders-active', 'private', 'GET', {'cost': 3.3333333333333335}) + private_get_finance_staking_defi_orders_history = privateGetFinanceStakingDefiOrdersHistory = Entry('finance/staking-defi/orders-history', 'private', 'GET', {'cost': 3.3333333333333335}) + private_get_finance_staking_defi_eth_balance = privateGetFinanceStakingDefiEthBalance = Entry('finance/staking-defi/eth/balance', 'private', 'GET', {'cost': 1.6666666666666667}) + private_get_finance_staking_defi_eth_purchase_redeem_history = privateGetFinanceStakingDefiEthPurchaseRedeemHistory = Entry('finance/staking-defi/eth/purchase-redeem-history', 'private', 'GET', {'cost': 1.6666666666666667}) + private_get_finance_staking_defi_eth_product_info = privateGetFinanceStakingDefiEthProductInfo = Entry('finance/staking-defi/eth/product-info', 'private', 'GET', {'cost': 3}) + private_get_finance_staking_defi_sol_balance = privateGetFinanceStakingDefiSolBalance = Entry('finance/staking-defi/sol/balance', 'private', 'GET', {'cost': 1.6666666666666667}) + private_get_finance_staking_defi_sol_purchase_redeem_history = privateGetFinanceStakingDefiSolPurchaseRedeemHistory = Entry('finance/staking-defi/sol/purchase-redeem-history', 'private', 'GET', {'cost': 1.6666666666666667}) + private_get_copytrading_current_subpositions = privateGetCopytradingCurrentSubpositions = Entry('copytrading/current-subpositions', 'private', 'GET', {'cost': 1}) + private_get_copytrading_subpositions_history = privateGetCopytradingSubpositionsHistory = Entry('copytrading/subpositions-history', 'private', 'GET', {'cost': 1}) + private_get_copytrading_instruments = privateGetCopytradingInstruments = Entry('copytrading/instruments', 'private', 'GET', {'cost': 4}) + private_get_copytrading_profit_sharing_details = privateGetCopytradingProfitSharingDetails = Entry('copytrading/profit-sharing-details', 'private', 'GET', {'cost': 4}) + private_get_copytrading_total_profit_sharing = privateGetCopytradingTotalProfitSharing = Entry('copytrading/total-profit-sharing', 'private', 'GET', {'cost': 4}) + private_get_copytrading_unrealized_profit_sharing_details = privateGetCopytradingUnrealizedProfitSharingDetails = Entry('copytrading/unrealized-profit-sharing-details', 'private', 'GET', {'cost': 4}) + private_get_copytrading_copy_settings = privateGetCopytradingCopySettings = Entry('copytrading/copy-settings', 'private', 'GET', {'cost': 4}) + private_get_copytrading_batch_leverage_info = privateGetCopytradingBatchLeverageInfo = Entry('copytrading/batch-leverage-info', 'private', 'GET', {'cost': 4}) + private_get_copytrading_current_lead_traders = privateGetCopytradingCurrentLeadTraders = Entry('copytrading/current-lead-traders', 'private', 'GET', {'cost': 4}) + private_get_copytrading_lead_traders_history = privateGetCopytradingLeadTradersHistory = Entry('copytrading/lead-traders-history', 'private', 'GET', {'cost': 4}) + private_get_broker_nd_info = privateGetBrokerNdInfo = Entry('broker/nd/info', 'private', 'GET', {'cost': 10}) + private_get_broker_nd_subaccount_info = privateGetBrokerNdSubaccountInfo = Entry('broker/nd/subaccount-info', 'private', 'GET', {'cost': 10}) + private_get_broker_nd_subaccount_apikey = privateGetBrokerNdSubaccountApikey = Entry('broker/nd/subaccount/apikey', 'private', 'GET', {'cost': 10}) + private_get_asset_broker_nd_subaccount_deposit_address = privateGetAssetBrokerNdSubaccountDepositAddress = Entry('asset/broker/nd/subaccount-deposit-address', 'private', 'GET', {'cost': 1.6666666666666667}) + private_get_asset_broker_nd_subaccount_deposit_history = privateGetAssetBrokerNdSubaccountDepositHistory = Entry('asset/broker/nd/subaccount-deposit-history', 'private', 'GET', {'cost': 4}) + private_get_asset_broker_nd_subaccount_withdrawal_history = privateGetAssetBrokerNdSubaccountWithdrawalHistory = Entry('asset/broker/nd/subaccount-withdrawal-history', 'private', 'GET', {'cost': 4}) + private_get_broker_nd_rebate_daily = privateGetBrokerNdRebateDaily = Entry('broker/nd/rebate-daily', 'private', 'GET', {'cost': 100}) + private_get_broker_nd_rebate_per_orders = privateGetBrokerNdRebatePerOrders = Entry('broker/nd/rebate-per-orders', 'private', 'GET', {'cost': 300}) + private_get_finance_sfp_dcd_order = privateGetFinanceSfpDcdOrder = Entry('finance/sfp/dcd/order', 'private', 'GET', {'cost': 2}) + private_get_finance_sfp_dcd_orders = privateGetFinanceSfpDcdOrders = Entry('finance/sfp/dcd/orders', 'private', 'GET', {'cost': 2}) + private_get_broker_fd_rebate_per_orders = privateGetBrokerFdRebatePerOrders = Entry('broker/fd/rebate-per-orders', 'private', 'GET', {'cost': 300}) + private_get_broker_fd_if_rebate = privateGetBrokerFdIfRebate = Entry('broker/fd/if-rebate', 'private', 'GET', {'cost': 5}) + private_get_affiliate_invitee_detail = privateGetAffiliateInviteeDetail = Entry('affiliate/invitee/detail', 'private', 'GET', {'cost': 1}) + private_get_users_partner_if_rebate = privateGetUsersPartnerIfRebate = Entry('users/partner/if-rebate', 'private', 'GET', {'cost': 1}) + private_get_support_announcements = privateGetSupportAnnouncements = Entry('support/announcements', 'private', 'GET', {'cost': 4}) + private_post_rfq_create_rfq = privatePostRfqCreateRfq = Entry('rfq/create-rfq', 'private', 'POST', {'cost': 4}) + private_post_rfq_cancel_rfq = privatePostRfqCancelRfq = Entry('rfq/cancel-rfq', 'private', 'POST', {'cost': 4}) + private_post_rfq_cancel_batch_rfqs = privatePostRfqCancelBatchRfqs = Entry('rfq/cancel-batch-rfqs', 'private', 'POST', {'cost': 10}) + private_post_rfq_cancel_all_rfqs = privatePostRfqCancelAllRfqs = Entry('rfq/cancel-all-rfqs', 'private', 'POST', {'cost': 10}) + private_post_rfq_execute_quote = privatePostRfqExecuteQuote = Entry('rfq/execute-quote', 'private', 'POST', {'cost': 15}) + private_post_rfq_maker_instrument_settings = privatePostRfqMakerInstrumentSettings = Entry('rfq/maker-instrument-settings', 'private', 'POST', {'cost': 4}) + private_post_rfq_mmp_reset = privatePostRfqMmpReset = Entry('rfq/mmp-reset', 'private', 'POST', {'cost': 4}) + private_post_rfq_mmp_config = privatePostRfqMmpConfig = Entry('rfq/mmp-config', 'private', 'POST', {'cost': 100}) + private_post_rfq_create_quote = privatePostRfqCreateQuote = Entry('rfq/create-quote', 'private', 'POST', {'cost': 0.4}) + private_post_rfq_cancel_quote = privatePostRfqCancelQuote = Entry('rfq/cancel-quote', 'private', 'POST', {'cost': 0.4}) + private_post_rfq_cancel_batch_quotes = privatePostRfqCancelBatchQuotes = Entry('rfq/cancel-batch-quotes', 'private', 'POST', {'cost': 10}) + private_post_rfq_cancel_all_quotes = privatePostRfqCancelAllQuotes = Entry('rfq/cancel-all-quotes', 'private', 'POST', {'cost': 10}) + private_post_sprd_order = privatePostSprdOrder = Entry('sprd/order', 'private', 'POST', {'cost': 1}) + private_post_sprd_cancel_order = privatePostSprdCancelOrder = Entry('sprd/cancel-order', 'private', 'POST', {'cost': 1}) + private_post_sprd_mass_cancel = privatePostSprdMassCancel = Entry('sprd/mass-cancel', 'private', 'POST', {'cost': 1}) + private_post_sprd_amend_order = privatePostSprdAmendOrder = Entry('sprd/amend-order', 'private', 'POST', {'cost': 1}) + private_post_sprd_cancel_all_after = privatePostSprdCancelAllAfter = Entry('sprd/cancel-all-after', 'private', 'POST', {'cost': 10}) + private_post_trade_order = privatePostTradeOrder = Entry('trade/order', 'private', 'POST', {'cost': 0.3333333333333333}) + private_post_trade_batch_orders = privatePostTradeBatchOrders = Entry('trade/batch-orders', 'private', 'POST', {'cost': 0.06666666666666667}) + private_post_trade_cancel_order = privatePostTradeCancelOrder = Entry('trade/cancel-order', 'private', 'POST', {'cost': 0.3333333333333333}) + private_post_trade_cancel_batch_orders = privatePostTradeCancelBatchOrders = Entry('trade/cancel-batch-orders', 'private', 'POST', {'cost': 0.06666666666666667}) + private_post_trade_amend_order = privatePostTradeAmendOrder = Entry('trade/amend-order', 'private', 'POST', {'cost': 0.3333333333333333}) + private_post_trade_amend_batch_orders = privatePostTradeAmendBatchOrders = Entry('trade/amend-batch-orders', 'private', 'POST', {'cost': 0.006666666666666667}) + private_post_trade_close_position = privatePostTradeClosePosition = Entry('trade/close-position', 'private', 'POST', {'cost': 1}) + private_post_trade_fills_archive = privatePostTradeFillsArchive = Entry('trade/fills-archive', 'private', 'POST', {'cost': 172800}) + private_post_trade_order_algo = privatePostTradeOrderAlgo = Entry('trade/order-algo', 'private', 'POST', {'cost': 1}) + private_post_trade_cancel_algos = privatePostTradeCancelAlgos = Entry('trade/cancel-algos', 'private', 'POST', {'cost': 1}) + private_post_trade_amend_algos = privatePostTradeAmendAlgos = Entry('trade/amend-algos', 'private', 'POST', {'cost': 1}) + private_post_trade_cancel_advance_algos = privatePostTradeCancelAdvanceAlgos = Entry('trade/cancel-advance-algos', 'private', 'POST', {'cost': 1}) + private_post_trade_easy_convert = privatePostTradeEasyConvert = Entry('trade/easy-convert', 'private', 'POST', {'cost': 20}) + private_post_trade_one_click_repay = privatePostTradeOneClickRepay = Entry('trade/one-click-repay', 'private', 'POST', {'cost': 20}) + private_post_trade_one_click_repay_v2 = privatePostTradeOneClickRepayV2 = Entry('trade/one-click-repay-v2', 'private', 'POST', {'cost': 20}) + private_post_trade_mass_cancel = privatePostTradeMassCancel = Entry('trade/mass-cancel', 'private', 'POST', {'cost': 4}) + private_post_trade_cancel_all_after = privatePostTradeCancelAllAfter = Entry('trade/cancel-all-after', 'private', 'POST', {'cost': 10}) + private_post_asset_transfer = privatePostAssetTransfer = Entry('asset/transfer', 'private', 'POST', {'cost': 10}) + private_post_asset_withdrawal = privatePostAssetWithdrawal = Entry('asset/withdrawal', 'private', 'POST', {'cost': 1.6666666666666667}) + private_post_asset_withdrawal_lightning = privatePostAssetWithdrawalLightning = Entry('asset/withdrawal-lightning', 'private', 'POST', {'cost': 5}) + private_post_asset_cancel_withdrawal = privatePostAssetCancelWithdrawal = Entry('asset/cancel-withdrawal', 'private', 'POST', {'cost': 1.6666666666666667}) + private_post_asset_convert_dust_assets = privatePostAssetConvertDustAssets = Entry('asset/convert-dust-assets', 'private', 'POST', {'cost': 10}) + private_post_asset_convert_estimate_quote = privatePostAssetConvertEstimateQuote = Entry('asset/convert/estimate-quote', 'private', 'POST', {'cost': 1}) + private_post_asset_convert_trade = privatePostAssetConvertTrade = Entry('asset/convert/trade', 'private', 'POST', {'cost': 1}) + private_post_asset_monthly_statement = privatePostAssetMonthlyStatement = Entry('asset/monthly-statement', 'private', 'POST', {'cost': 1}) + private_post_account_set_position_mode = privatePostAccountSetPositionMode = Entry('account/set-position-mode', 'private', 'POST', {'cost': 4}) + private_post_account_set_leverage = privatePostAccountSetLeverage = Entry('account/set-leverage', 'private', 'POST', {'cost': 1}) + private_post_account_position_margin_balance = privatePostAccountPositionMarginBalance = Entry('account/position/margin-balance', 'private', 'POST', {'cost': 1}) + private_post_account_set_greeks = privatePostAccountSetGreeks = Entry('account/set-greeks', 'private', 'POST', {'cost': 4}) + private_post_account_set_isolated_mode = privatePostAccountSetIsolatedMode = Entry('account/set-isolated-mode', 'private', 'POST', {'cost': 4}) + private_post_account_quick_margin_borrow_repay = privatePostAccountQuickMarginBorrowRepay = Entry('account/quick-margin-borrow-repay', 'private', 'POST', {'cost': 4}) + private_post_account_borrow_repay = privatePostAccountBorrowRepay = Entry('account/borrow-repay', 'private', 'POST', {'cost': 1.6666666666666667}) + private_post_account_simulated_margin = privatePostAccountSimulatedMargin = Entry('account/simulated_margin', 'private', 'POST', {'cost': 10}) + private_post_account_position_builder = privatePostAccountPositionBuilder = Entry('account/position-builder', 'private', 'POST', {'cost': 10}) + private_post_account_set_riskoffset_type = privatePostAccountSetRiskOffsetType = Entry('account/set-riskOffset-type', 'private', 'POST', {'cost': 2}) + private_post_account_activate_option = privatePostAccountActivateOption = Entry('account/activate-option', 'private', 'POST', {'cost': 4}) + private_post_account_set_auto_loan = privatePostAccountSetAutoLoan = Entry('account/set-auto-loan', 'private', 'POST', {'cost': 4}) + private_post_account_set_account_level = privatePostAccountSetAccountLevel = Entry('account/set-account-level', 'private', 'POST', {'cost': 4}) + private_post_account_mmp_reset = privatePostAccountMmpReset = Entry('account/mmp-reset', 'private', 'POST', {'cost': 4}) + private_post_account_mmp_config = privatePostAccountMmpConfig = Entry('account/mmp-config', 'private', 'POST', {'cost': 100}) + private_post_account_fixed_loan_borrowing_order = privatePostAccountFixedLoanBorrowingOrder = Entry('account/fixed-loan/borrowing-order', 'private', 'POST', {'cost': 5}) + private_post_account_fixed_loan_amend_borrowing_order = privatePostAccountFixedLoanAmendBorrowingOrder = Entry('account/fixed-loan/amend-borrowing-order', 'private', 'POST', {'cost': 5}) + private_post_account_fixed_loan_manual_reborrow = privatePostAccountFixedLoanManualReborrow = Entry('account/fixed-loan/manual-reborrow', 'private', 'POST', {'cost': 5}) + private_post_account_fixed_loan_repay_borrowing_order = privatePostAccountFixedLoanRepayBorrowingOrder = Entry('account/fixed-loan/repay-borrowing-order', 'private', 'POST', {'cost': 5}) + private_post_account_bills_history_archive = privatePostAccountBillsHistoryArchive = Entry('account/bills-history-archive', 'private', 'POST', {'cost': 72000}) + private_post_account_move_positions = privatePostAccountMovePositions = Entry('account/move-positions', 'private', 'POST', {'cost': 10}) + private_post_account_set_settle_currency = privatePostAccountSetSettleCurrency = Entry('account/set-settle-currency', 'private', 'POST', {'cost': 1}) + private_post_users_subaccount_modify_apikey = privatePostUsersSubaccountModifyApikey = Entry('users/subaccount/modify-apikey', 'private', 'POST', {'cost': 10}) + private_post_asset_subaccount_transfer = privatePostAssetSubaccountTransfer = Entry('asset/subaccount/transfer', 'private', 'POST', {'cost': 10}) + private_post_users_subaccount_set_transfer_out = privatePostUsersSubaccountSetTransferOut = Entry('users/subaccount/set-transfer-out', 'private', 'POST', {'cost': 10}) + private_post_account_subaccount_set_loan_allocation = privatePostAccountSubaccountSetLoanAllocation = Entry('account/subaccount/set-loan-allocation', 'private', 'POST', {'cost': 4}) + private_post_users_subaccount_create_subaccount = privatePostUsersSubaccountCreateSubaccount = Entry('users/subaccount/create-subaccount', 'private', 'POST', {'cost': 10}) + private_post_users_subaccount_subaccount_apikey = privatePostUsersSubaccountSubaccountApikey = Entry('users/subaccount/subaccount-apikey', 'private', 'POST', {'cost': 10}) + private_post_users_subaccount_delete_apikey = privatePostUsersSubaccountDeleteApikey = Entry('users/subaccount/delete-apikey', 'private', 'POST', {'cost': 10}) + private_post_tradingbot_grid_order_algo = privatePostTradingBotGridOrderAlgo = Entry('tradingBot/grid/order-algo', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_grid_amend_order_algo = privatePostTradingBotGridAmendOrderAlgo = Entry('tradingBot/grid/amend-order-algo', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_grid_stop_order_algo = privatePostTradingBotGridStopOrderAlgo = Entry('tradingBot/grid/stop-order-algo', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_grid_close_position = privatePostTradingBotGridClosePosition = Entry('tradingBot/grid/close-position', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_grid_cancel_close_order = privatePostTradingBotGridCancelCloseOrder = Entry('tradingBot/grid/cancel-close-order', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_grid_order_instant_trigger = privatePostTradingBotGridOrderInstantTrigger = Entry('tradingBot/grid/order-instant-trigger', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_grid_withdraw_income = privatePostTradingBotGridWithdrawIncome = Entry('tradingBot/grid/withdraw-income', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_grid_compute_margin_balance = privatePostTradingBotGridComputeMarginBalance = Entry('tradingBot/grid/compute-margin-balance', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_grid_margin_balance = privatePostTradingBotGridMarginBalance = Entry('tradingBot/grid/margin-balance', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_grid_min_investment = privatePostTradingBotGridMinInvestment = Entry('tradingBot/grid/min-investment', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_grid_adjust_investment = privatePostTradingBotGridAdjustInvestment = Entry('tradingBot/grid/adjust-investment', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_signal_create_signal = privatePostTradingBotSignalCreateSignal = Entry('tradingBot/signal/create-signal', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_signal_order_algo = privatePostTradingBotSignalOrderAlgo = Entry('tradingBot/signal/order-algo', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_signal_stop_order_algo = privatePostTradingBotSignalStopOrderAlgo = Entry('tradingBot/signal/stop-order-algo', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_signal_margin_balance = privatePostTradingBotSignalMarginBalance = Entry('tradingBot/signal/margin-balance', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_signal_amendtpsl = privatePostTradingBotSignalAmendTPSL = Entry('tradingBot/signal/amendTPSL', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_signal_set_instruments = privatePostTradingBotSignalSetInstruments = Entry('tradingBot/signal/set-instruments', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_signal_close_position = privatePostTradingBotSignalClosePosition = Entry('tradingBot/signal/close-position', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_signal_sub_order = privatePostTradingBotSignalSubOrder = Entry('tradingBot/signal/sub-order', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_signal_cancel_sub_order = privatePostTradingBotSignalCancelSubOrder = Entry('tradingBot/signal/cancel-sub-order', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_recurring_order_algo = privatePostTradingBotRecurringOrderAlgo = Entry('tradingBot/recurring/order-algo', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_recurring_amend_order_algo = privatePostTradingBotRecurringAmendOrderAlgo = Entry('tradingBot/recurring/amend-order-algo', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_recurring_stop_order_algo = privatePostTradingBotRecurringStopOrderAlgo = Entry('tradingBot/recurring/stop-order-algo', 'private', 'POST', {'cost': 1}) + private_post_finance_savings_purchase_redempt = privatePostFinanceSavingsPurchaseRedempt = Entry('finance/savings/purchase-redempt', 'private', 'POST', {'cost': 1.6666666666666667}) + private_post_finance_savings_set_lending_rate = privatePostFinanceSavingsSetLendingRate = Entry('finance/savings/set-lending-rate', 'private', 'POST', {'cost': 1.6666666666666667}) + private_post_finance_staking_defi_purchase = privatePostFinanceStakingDefiPurchase = Entry('finance/staking-defi/purchase', 'private', 'POST', {'cost': 3}) + private_post_finance_staking_defi_redeem = privatePostFinanceStakingDefiRedeem = Entry('finance/staking-defi/redeem', 'private', 'POST', {'cost': 3}) + private_post_finance_staking_defi_cancel = privatePostFinanceStakingDefiCancel = Entry('finance/staking-defi/cancel', 'private', 'POST', {'cost': 3}) + private_post_finance_staking_defi_eth_purchase = privatePostFinanceStakingDefiEthPurchase = Entry('finance/staking-defi/eth/purchase', 'private', 'POST', {'cost': 5}) + private_post_finance_staking_defi_eth_redeem = privatePostFinanceStakingDefiEthRedeem = Entry('finance/staking-defi/eth/redeem', 'private', 'POST', {'cost': 5}) + private_post_finance_staking_defi_sol_purchase = privatePostFinanceStakingDefiSolPurchase = Entry('finance/staking-defi/sol/purchase', 'private', 'POST', {'cost': 5}) + private_post_finance_staking_defi_sol_redeem = privatePostFinanceStakingDefiSolRedeem = Entry('finance/staking-defi/sol/redeem', 'private', 'POST', {'cost': 5}) + private_post_copytrading_algo_order = privatePostCopytradingAlgoOrder = Entry('copytrading/algo-order', 'private', 'POST', {'cost': 1}) + private_post_copytrading_close_subposition = privatePostCopytradingCloseSubposition = Entry('copytrading/close-subposition', 'private', 'POST', {'cost': 1}) + private_post_copytrading_set_instruments = privatePostCopytradingSetInstruments = Entry('copytrading/set-instruments', 'private', 'POST', {'cost': 4}) + private_post_copytrading_first_copy_settings = privatePostCopytradingFirstCopySettings = Entry('copytrading/first-copy-settings', 'private', 'POST', {'cost': 4}) + private_post_copytrading_amend_copy_settings = privatePostCopytradingAmendCopySettings = Entry('copytrading/amend-copy-settings', 'private', 'POST', {'cost': 4}) + private_post_copytrading_stop_copy_trading = privatePostCopytradingStopCopyTrading = Entry('copytrading/stop-copy-trading', 'private', 'POST', {'cost': 4}) + private_post_copytrading_batch_set_leverage = privatePostCopytradingBatchSetLeverage = Entry('copytrading/batch-set-leverage', 'private', 'POST', {'cost': 4}) + private_post_broker_nd_create_subaccount = privatePostBrokerNdCreateSubaccount = Entry('broker/nd/create-subaccount', 'private', 'POST', {'cost': 0.25}) + private_post_broker_nd_delete_subaccount = privatePostBrokerNdDeleteSubaccount = Entry('broker/nd/delete-subaccount', 'private', 'POST', {'cost': 1}) + private_post_broker_nd_subaccount_apikey = privatePostBrokerNdSubaccountApikey = Entry('broker/nd/subaccount/apikey', 'private', 'POST', {'cost': 0.25}) + private_post_broker_nd_subaccount_modify_apikey = privatePostBrokerNdSubaccountModifyApikey = Entry('broker/nd/subaccount/modify-apikey', 'private', 'POST', {'cost': 1}) + private_post_broker_nd_subaccount_delete_apikey = privatePostBrokerNdSubaccountDeleteApikey = Entry('broker/nd/subaccount/delete-apikey', 'private', 'POST', {'cost': 1}) + private_post_broker_nd_set_subaccount_level = privatePostBrokerNdSetSubaccountLevel = Entry('broker/nd/set-subaccount-level', 'private', 'POST', {'cost': 4}) + private_post_broker_nd_set_subaccount_fee_rate = privatePostBrokerNdSetSubaccountFeeRate = Entry('broker/nd/set-subaccount-fee-rate', 'private', 'POST', {'cost': 4}) + private_post_broker_nd_set_subaccount_assets = privatePostBrokerNdSetSubaccountAssets = Entry('broker/nd/set-subaccount-assets', 'private', 'POST', {'cost': 0.25}) + private_post_asset_broker_nd_subaccount_deposit_address = privatePostAssetBrokerNdSubaccountDepositAddress = Entry('asset/broker/nd/subaccount-deposit-address', 'private', 'POST', {'cost': 1}) + private_post_asset_broker_nd_modify_subaccount_deposit_address = privatePostAssetBrokerNdModifySubaccountDepositAddress = Entry('asset/broker/nd/modify-subaccount-deposit-address', 'private', 'POST', {'cost': 1.6666666666666667}) + private_post_broker_nd_rebate_per_orders = privatePostBrokerNdRebatePerOrders = Entry('broker/nd/rebate-per-orders', 'private', 'POST', {'cost': 36000}) + private_post_finance_sfp_dcd_quote = privatePostFinanceSfpDcdQuote = Entry('finance/sfp/dcd/quote', 'private', 'POST', {'cost': 10}) + private_post_finance_sfp_dcd_order = privatePostFinanceSfpDcdOrder = Entry('finance/sfp/dcd/order', 'private', 'POST', {'cost': 10}) + private_post_broker_nd_report_subaccount_ip = privatePostBrokerNdReportSubaccountIp = Entry('broker/nd/report-subaccount-ip', 'private', 'POST', {'cost': 0.25}) + private_post_broker_fd_rebate_per_orders = privatePostBrokerFdRebatePerOrders = Entry('broker/fd/rebate-per-orders', 'private', 'POST', {'cost': 36000}) diff --git a/ccxt/abstract/okxus.py b/ccxt/abstract/okxus.py new file mode 100644 index 0000000..ac9e2a1 --- /dev/null +++ b/ccxt/abstract/okxus.py @@ -0,0 +1,351 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_market_books_full = publicGetMarketBooksFull = Entry('market/books-full', 'public', 'GET', {'cost': 2}) + public_get_market_tickers = publicGetMarketTickers = Entry('market/tickers', 'public', 'GET', {'cost': 1}) + public_get_market_ticker = publicGetMarketTicker = Entry('market/ticker', 'public', 'GET', {'cost': 1}) + public_get_market_index_tickers = publicGetMarketIndexTickers = Entry('market/index-tickers', 'public', 'GET', {'cost': 1}) + public_get_market_books = publicGetMarketBooks = Entry('market/books', 'public', 'GET', {'cost': 0.5}) + public_get_market_books_lite = publicGetMarketBooksLite = Entry('market/books-lite', 'public', 'GET', {'cost': 1.6666666666666667}) + public_get_market_candles = publicGetMarketCandles = Entry('market/candles', 'public', 'GET', {'cost': 0.5}) + public_get_market_history_candles = publicGetMarketHistoryCandles = Entry('market/history-candles', 'public', 'GET', {'cost': 1}) + public_get_market_index_candles = publicGetMarketIndexCandles = Entry('market/index-candles', 'public', 'GET', {'cost': 1}) + public_get_market_history_index_candles = publicGetMarketHistoryIndexCandles = Entry('market/history-index-candles', 'public', 'GET', {'cost': 2}) + public_get_market_mark_price_candles = publicGetMarketMarkPriceCandles = Entry('market/mark-price-candles', 'public', 'GET', {'cost': 1}) + public_get_market_history_mark_price_candles = publicGetMarketHistoryMarkPriceCandles = Entry('market/history-mark-price-candles', 'public', 'GET', {'cost': 2}) + public_get_market_trades = publicGetMarketTrades = Entry('market/trades', 'public', 'GET', {'cost': 0.2}) + public_get_market_history_trades = publicGetMarketHistoryTrades = Entry('market/history-trades', 'public', 'GET', {'cost': 2}) + public_get_market_option_instrument_family_trades = publicGetMarketOptionInstrumentFamilyTrades = Entry('market/option/instrument-family-trades', 'public', 'GET', {'cost': 1}) + public_get_market_platform_24_volume = publicGetMarketPlatform24Volume = Entry('market/platform-24-volume', 'public', 'GET', {'cost': 10}) + public_get_market_open_oracle = publicGetMarketOpenOracle = Entry('market/open-oracle', 'public', 'GET', {'cost': 50}) + public_get_market_exchange_rate = publicGetMarketExchangeRate = Entry('market/exchange-rate', 'public', 'GET', {'cost': 20}) + public_get_market_index_components = publicGetMarketIndexComponents = Entry('market/index-components', 'public', 'GET', {'cost': 1}) + public_get_public_market_data_history = publicGetPublicMarketDataHistory = Entry('public/market-data-history', 'public', 'GET', {'cost': 4}) + public_get_public_economic_calendar = publicGetPublicEconomicCalendar = Entry('public/economic-calendar', 'public', 'GET', {'cost': 50}) + public_get_market_block_tickers = publicGetMarketBlockTickers = Entry('market/block-tickers', 'public', 'GET', {'cost': 1}) + public_get_market_block_ticker = publicGetMarketBlockTicker = Entry('market/block-ticker', 'public', 'GET', {'cost': 1}) + public_get_public_block_trades = publicGetPublicBlockTrades = Entry('public/block-trades', 'public', 'GET', {'cost': 1}) + public_get_public_instruments = publicGetPublicInstruments = Entry('public/instruments', 'public', 'GET', {'cost': 1}) + public_get_public_delivery_exercise_history = publicGetPublicDeliveryExerciseHistory = Entry('public/delivery-exercise-history', 'public', 'GET', {'cost': 0.5}) + public_get_public_open_interest = publicGetPublicOpenInterest = Entry('public/open-interest', 'public', 'GET', {'cost': 1}) + public_get_public_funding_rate = publicGetPublicFundingRate = Entry('public/funding-rate', 'public', 'GET', {'cost': 1}) + public_get_public_funding_rate_history = publicGetPublicFundingRateHistory = Entry('public/funding-rate-history', 'public', 'GET', {'cost': 1}) + public_get_public_price_limit = publicGetPublicPriceLimit = Entry('public/price-limit', 'public', 'GET', {'cost': 1}) + public_get_public_opt_summary = publicGetPublicOptSummary = Entry('public/opt-summary', 'public', 'GET', {'cost': 1}) + public_get_public_estimated_price = publicGetPublicEstimatedPrice = Entry('public/estimated-price', 'public', 'GET', {'cost': 2}) + public_get_public_discount_rate_interest_free_quota = publicGetPublicDiscountRateInterestFreeQuota = Entry('public/discount-rate-interest-free-quota', 'public', 'GET', {'cost': 10}) + public_get_public_time = publicGetPublicTime = Entry('public/time', 'public', 'GET', {'cost': 2}) + public_get_public_mark_price = publicGetPublicMarkPrice = Entry('public/mark-price', 'public', 'GET', {'cost': 2}) + public_get_public_position_tiers = publicGetPublicPositionTiers = Entry('public/position-tiers', 'public', 'GET', {'cost': 2}) + public_get_public_interest_rate_loan_quota = publicGetPublicInterestRateLoanQuota = Entry('public/interest-rate-loan-quota', 'public', 'GET', {'cost': 10}) + public_get_public_vip_interest_rate_loan_quota = publicGetPublicVipInterestRateLoanQuota = Entry('public/vip-interest-rate-loan-quota', 'public', 'GET', {'cost': 10}) + public_get_public_underlying = publicGetPublicUnderlying = Entry('public/underlying', 'public', 'GET', {'cost': 1}) + public_get_public_insurance_fund = publicGetPublicInsuranceFund = Entry('public/insurance-fund', 'public', 'GET', {'cost': 2}) + public_get_public_convert_contract_coin = publicGetPublicConvertContractCoin = Entry('public/convert-contract-coin', 'public', 'GET', {'cost': 2}) + public_get_public_option_trades = publicGetPublicOptionTrades = Entry('public/option-trades', 'public', 'GET', {'cost': 1}) + public_get_public_instrument_tick_bands = publicGetPublicInstrumentTickBands = Entry('public/instrument-tick-bands', 'public', 'GET', {'cost': 4}) + public_get_rubik_stat_trading_data_support_coin = publicGetRubikStatTradingDataSupportCoin = Entry('rubik/stat/trading-data/support-coin', 'public', 'GET', {'cost': 4}) + public_get_rubik_stat_taker_volume = publicGetRubikStatTakerVolume = Entry('rubik/stat/taker-volume', 'public', 'GET', {'cost': 4}) + public_get_rubik_stat_margin_loan_ratio = publicGetRubikStatMarginLoanRatio = Entry('rubik/stat/margin/loan-ratio', 'public', 'GET', {'cost': 4}) + public_get_rubik_stat_contracts_long_short_account_ratio = publicGetRubikStatContractsLongShortAccountRatio = Entry('rubik/stat/contracts/long-short-account-ratio', 'public', 'GET', {'cost': 4}) + public_get_rubik_stat_contracts_long_short_account_ratio_contract = publicGetRubikStatContractsLongShortAccountRatioContract = Entry('rubik/stat/contracts/long-short-account-ratio-contract', 'public', 'GET', {'cost': 4}) + public_get_rubik_stat_contracts_open_interest_volume = publicGetRubikStatContractsOpenInterestVolume = Entry('rubik/stat/contracts/open-interest-volume', 'public', 'GET', {'cost': 4}) + public_get_rubik_stat_option_open_interest_volume = publicGetRubikStatOptionOpenInterestVolume = Entry('rubik/stat/option/open-interest-volume', 'public', 'GET', {'cost': 4}) + public_get_rubik_stat_option_open_interest_volume_ratio = publicGetRubikStatOptionOpenInterestVolumeRatio = Entry('rubik/stat/option/open-interest-volume-ratio', 'public', 'GET', {'cost': 4}) + public_get_rubik_stat_option_open_interest_volume_expiry = publicGetRubikStatOptionOpenInterestVolumeExpiry = Entry('rubik/stat/option/open-interest-volume-expiry', 'public', 'GET', {'cost': 4}) + public_get_rubik_stat_option_open_interest_volume_strike = publicGetRubikStatOptionOpenInterestVolumeStrike = Entry('rubik/stat/option/open-interest-volume-strike', 'public', 'GET', {'cost': 4}) + public_get_rubik_stat_option_taker_block_volume = publicGetRubikStatOptionTakerBlockVolume = Entry('rubik/stat/option/taker-block-volume', 'public', 'GET', {'cost': 4}) + public_get_system_status = publicGetSystemStatus = Entry('system/status', 'public', 'GET', {'cost': 50}) + public_get_sprd_spreads = publicGetSprdSpreads = Entry('sprd/spreads', 'public', 'GET', {'cost': 1}) + public_get_sprd_books = publicGetSprdBooks = Entry('sprd/books', 'public', 'GET', {'cost': 0.5}) + public_get_sprd_ticker = publicGetSprdTicker = Entry('sprd/ticker', 'public', 'GET', {'cost': 1}) + public_get_sprd_public_trades = publicGetSprdPublicTrades = Entry('sprd/public-trades', 'public', 'GET', {'cost': 0.2}) + public_get_market_sprd_ticker = publicGetMarketSprdTicker = Entry('market/sprd-ticker', 'public', 'GET', {'cost': 2}) + public_get_market_sprd_candles = publicGetMarketSprdCandles = Entry('market/sprd-candles', 'public', 'GET', {'cost': 2}) + public_get_market_sprd_history_candles = publicGetMarketSprdHistoryCandles = Entry('market/sprd-history-candles', 'public', 'GET', {'cost': 2}) + public_get_tradingbot_grid_ai_param = publicGetTradingBotGridAiParam = Entry('tradingBot/grid/ai-param', 'public', 'GET', {'cost': 1}) + public_get_tradingbot_grid_min_investment = publicGetTradingBotGridMinInvestment = Entry('tradingBot/grid/min-investment', 'public', 'GET', {'cost': 1}) + public_get_tradingbot_public_rsi_back_testing = publicGetTradingBotPublicRsiBackTesting = Entry('tradingBot/public/rsi-back-testing', 'public', 'GET', {'cost': 1}) + public_get_asset_exchange_list = publicGetAssetExchangeList = Entry('asset/exchange-list', 'public', 'GET', {'cost': 1.6666666666666667}) + public_get_finance_staking_defi_eth_apy_history = publicGetFinanceStakingDefiEthApyHistory = Entry('finance/staking-defi/eth/apy-history', 'public', 'GET', {'cost': 1.6666666666666667}) + public_get_finance_staking_defi_sol_apy_history = publicGetFinanceStakingDefiSolApyHistory = Entry('finance/staking-defi/sol/apy-history', 'public', 'GET', {'cost': 1.6666666666666667}) + public_get_finance_savings_lending_rate_summary = publicGetFinanceSavingsLendingRateSummary = Entry('finance/savings/lending-rate-summary', 'public', 'GET', {'cost': 1.6666666666666667}) + public_get_finance_savings_lending_rate_history = publicGetFinanceSavingsLendingRateHistory = Entry('finance/savings/lending-rate-history', 'public', 'GET', {'cost': 1.6666666666666667}) + public_get_finance_fixed_loan_lending_offers = publicGetFinanceFixedLoanLendingOffers = Entry('finance/fixed-loan/lending-offers', 'public', 'GET', {'cost': 3.3333333333333335}) + public_get_finance_fixed_loan_lending_apy_history = publicGetFinanceFixedLoanLendingApyHistory = Entry('finance/fixed-loan/lending-apy-history', 'public', 'GET', {'cost': 3.3333333333333335}) + public_get_finance_fixed_loan_pending_lending_volume = publicGetFinanceFixedLoanPendingLendingVolume = Entry('finance/fixed-loan/pending-lending-volume', 'public', 'GET', {'cost': 3.3333333333333335}) + public_get_finance_sfp_dcd_products = publicGetFinanceSfpDcdProducts = Entry('finance/sfp/dcd/products', 'public', 'GET', {'cost': 0.6666666666666666}) + public_get_copytrading_public_lead_traders = publicGetCopytradingPublicLeadTraders = Entry('copytrading/public-lead-traders', 'public', 'GET', {'cost': 4}) + public_get_copytrading_public_weekly_pnl = publicGetCopytradingPublicWeeklyPnl = Entry('copytrading/public-weekly-pnl', 'public', 'GET', {'cost': 4}) + public_get_copytrading_public_stats = publicGetCopytradingPublicStats = Entry('copytrading/public-stats', 'public', 'GET', {'cost': 4}) + public_get_copytrading_public_preference_currency = publicGetCopytradingPublicPreferenceCurrency = Entry('copytrading/public-preference-currency', 'public', 'GET', {'cost': 4}) + public_get_copytrading_public_current_subpositions = publicGetCopytradingPublicCurrentSubpositions = Entry('copytrading/public-current-subpositions', 'public', 'GET', {'cost': 4}) + public_get_copytrading_public_subpositions_history = publicGetCopytradingPublicSubpositionsHistory = Entry('copytrading/public-subpositions-history', 'public', 'GET', {'cost': 4}) + public_get_support_announcements_types = publicGetSupportAnnouncementsTypes = Entry('support/announcements-types', 'public', 'GET', {'cost': 20}) + private_get_rfq_counterparties = privateGetRfqCounterparties = Entry('rfq/counterparties', 'private', 'GET', {'cost': 4}) + private_get_rfq_maker_instrument_settings = privateGetRfqMakerInstrumentSettings = Entry('rfq/maker-instrument-settings', 'private', 'GET', {'cost': 4}) + private_get_rfq_mmp_config = privateGetRfqMmpConfig = Entry('rfq/mmp-config', 'private', 'GET', {'cost': 4}) + private_get_rfq_rfqs = privateGetRfqRfqs = Entry('rfq/rfqs', 'private', 'GET', {'cost': 10}) + private_get_rfq_quotes = privateGetRfqQuotes = Entry('rfq/quotes', 'private', 'GET', {'cost': 10}) + private_get_rfq_trades = privateGetRfqTrades = Entry('rfq/trades', 'private', 'GET', {'cost': 4}) + private_get_rfq_public_trades = privateGetRfqPublicTrades = Entry('rfq/public-trades', 'private', 'GET', {'cost': 4}) + private_get_sprd_order = privateGetSprdOrder = Entry('sprd/order', 'private', 'GET', {'cost': 0.3333333333333333}) + private_get_sprd_orders_pending = privateGetSprdOrdersPending = Entry('sprd/orders-pending', 'private', 'GET', {'cost': 0.3333333333333333}) + private_get_sprd_orders_history = privateGetSprdOrdersHistory = Entry('sprd/orders-history', 'private', 'GET', {'cost': 0.5}) + private_get_sprd_orders_history_archive = privateGetSprdOrdersHistoryArchive = Entry('sprd/orders-history-archive', 'private', 'GET', {'cost': 0.5}) + private_get_sprd_trades = privateGetSprdTrades = Entry('sprd/trades', 'private', 'GET', {'cost': 0.3333333333333333}) + private_get_trade_order = privateGetTradeOrder = Entry('trade/order', 'private', 'GET', {'cost': 0.3333333333333333}) + private_get_trade_orders_pending = privateGetTradeOrdersPending = Entry('trade/orders-pending', 'private', 'GET', {'cost': 0.3333333333333333}) + private_get_trade_orders_history = privateGetTradeOrdersHistory = Entry('trade/orders-history', 'private', 'GET', {'cost': 0.5}) + private_get_trade_orders_history_archive = privateGetTradeOrdersHistoryArchive = Entry('trade/orders-history-archive', 'private', 'GET', {'cost': 1}) + private_get_trade_fills = privateGetTradeFills = Entry('trade/fills', 'private', 'GET', {'cost': 0.3333333333333333}) + private_get_trade_fills_history = privateGetTradeFillsHistory = Entry('trade/fills-history', 'private', 'GET', {'cost': 2.2}) + private_get_trade_fills_archive = privateGetTradeFillsArchive = Entry('trade/fills-archive', 'private', 'GET', {'cost': 2}) + private_get_trade_order_algo = privateGetTradeOrderAlgo = Entry('trade/order-algo', 'private', 'GET', {'cost': 1}) + private_get_trade_orders_algo_pending = privateGetTradeOrdersAlgoPending = Entry('trade/orders-algo-pending', 'private', 'GET', {'cost': 1}) + private_get_trade_orders_algo_history = privateGetTradeOrdersAlgoHistory = Entry('trade/orders-algo-history', 'private', 'GET', {'cost': 1}) + private_get_trade_easy_convert_currency_list = privateGetTradeEasyConvertCurrencyList = Entry('trade/easy-convert-currency-list', 'private', 'GET', {'cost': 20}) + private_get_trade_easy_convert_history = privateGetTradeEasyConvertHistory = Entry('trade/easy-convert-history', 'private', 'GET', {'cost': 20}) + private_get_trade_one_click_repay_currency_list = privateGetTradeOneClickRepayCurrencyList = Entry('trade/one-click-repay-currency-list', 'private', 'GET', {'cost': 20}) + private_get_trade_one_click_repay_currency_list_v2 = privateGetTradeOneClickRepayCurrencyListV2 = Entry('trade/one-click-repay-currency-list-v2', 'private', 'GET', {'cost': 20}) + private_get_trade_one_click_repay_history = privateGetTradeOneClickRepayHistory = Entry('trade/one-click-repay-history', 'private', 'GET', {'cost': 20}) + private_get_trade_one_click_repay_history_v2 = privateGetTradeOneClickRepayHistoryV2 = Entry('trade/one-click-repay-history-v2', 'private', 'GET', {'cost': 20}) + private_get_trade_account_rate_limit = privateGetTradeAccountRateLimit = Entry('trade/account-rate-limit', 'private', 'GET', {'cost': 1}) + private_get_asset_currencies = privateGetAssetCurrencies = Entry('asset/currencies', 'private', 'GET', {'cost': 1.6666666666666667}) + private_get_asset_balances = privateGetAssetBalances = Entry('asset/balances', 'private', 'GET', {'cost': 1.6666666666666667}) + private_get_asset_non_tradable_assets = privateGetAssetNonTradableAssets = Entry('asset/non-tradable-assets', 'private', 'GET', {'cost': 1.6666666666666667}) + private_get_asset_asset_valuation = privateGetAssetAssetValuation = Entry('asset/asset-valuation', 'private', 'GET', {'cost': 10}) + private_get_asset_transfer_state = privateGetAssetTransferState = Entry('asset/transfer-state', 'private', 'GET', {'cost': 10}) + private_get_asset_bills = privateGetAssetBills = Entry('asset/bills', 'private', 'GET', {'cost': 1.6666666666666667}) + private_get_asset_deposit_lightning = privateGetAssetDepositLightning = Entry('asset/deposit-lightning', 'private', 'GET', {'cost': 5}) + private_get_asset_deposit_address = privateGetAssetDepositAddress = Entry('asset/deposit-address', 'private', 'GET', {'cost': 1.6666666666666667}) + private_get_asset_deposit_history = privateGetAssetDepositHistory = Entry('asset/deposit-history', 'private', 'GET', {'cost': 1.6666666666666667}) + private_get_asset_withdrawal_history = privateGetAssetWithdrawalHistory = Entry('asset/withdrawal-history', 'private', 'GET', {'cost': 1.6666666666666667}) + private_get_asset_deposit_withdraw_status = privateGetAssetDepositWithdrawStatus = Entry('asset/deposit-withdraw-status', 'private', 'GET', {'cost': 20}) + private_get_asset_convert_currencies = privateGetAssetConvertCurrencies = Entry('asset/convert/currencies', 'private', 'GET', {'cost': 1.6666666666666667}) + private_get_asset_convert_currency_pair = privateGetAssetConvertCurrencyPair = Entry('asset/convert/currency-pair', 'private', 'GET', {'cost': 1.6666666666666667}) + private_get_asset_convert_history = privateGetAssetConvertHistory = Entry('asset/convert/history', 'private', 'GET', {'cost': 1.6666666666666667}) + private_get_asset_monthly_statement = privateGetAssetMonthlyStatement = Entry('asset/monthly-statement', 'private', 'GET', {'cost': 2}) + private_get_account_instruments = privateGetAccountInstruments = Entry('account/instruments', 'private', 'GET', {'cost': 1}) + private_get_account_balance = privateGetAccountBalance = Entry('account/balance', 'private', 'GET', {'cost': 2}) + private_get_account_positions = privateGetAccountPositions = Entry('account/positions', 'private', 'GET', {'cost': 2}) + private_get_account_positions_history = privateGetAccountPositionsHistory = Entry('account/positions-history', 'private', 'GET', {'cost': 100}) + private_get_account_account_position_risk = privateGetAccountAccountPositionRisk = Entry('account/account-position-risk', 'private', 'GET', {'cost': 2}) + private_get_account_bills = privateGetAccountBills = Entry('account/bills', 'private', 'GET', {'cost': 1.6666666666666667}) + private_get_account_bills_archive = privateGetAccountBillsArchive = Entry('account/bills-archive', 'private', 'GET', {'cost': 1.6666666666666667}) + private_get_account_bills_history_archive = privateGetAccountBillsHistoryArchive = Entry('account/bills-history-archive', 'private', 'GET', {'cost': 2}) + private_get_account_config = privateGetAccountConfig = Entry('account/config', 'private', 'GET', {'cost': 4}) + private_get_account_max_size = privateGetAccountMaxSize = Entry('account/max-size', 'private', 'GET', {'cost': 1}) + private_get_account_max_avail_size = privateGetAccountMaxAvailSize = Entry('account/max-avail-size', 'private', 'GET', {'cost': 1}) + private_get_account_leverage_info = privateGetAccountLeverageInfo = Entry('account/leverage-info', 'private', 'GET', {'cost': 1}) + private_get_account_adjust_leverage_info = privateGetAccountAdjustLeverageInfo = Entry('account/adjust-leverage-info', 'private', 'GET', {'cost': 4}) + private_get_account_max_loan = privateGetAccountMaxLoan = Entry('account/max-loan', 'private', 'GET', {'cost': 1}) + private_get_account_trade_fee = privateGetAccountTradeFee = Entry('account/trade-fee', 'private', 'GET', {'cost': 4}) + private_get_account_interest_accrued = privateGetAccountInterestAccrued = Entry('account/interest-accrued', 'private', 'GET', {'cost': 4}) + private_get_account_interest_rate = privateGetAccountInterestRate = Entry('account/interest-rate', 'private', 'GET', {'cost': 4}) + private_get_account_max_withdrawal = privateGetAccountMaxWithdrawal = Entry('account/max-withdrawal', 'private', 'GET', {'cost': 1}) + private_get_account_risk_state = privateGetAccountRiskState = Entry('account/risk-state', 'private', 'GET', {'cost': 2}) + private_get_account_quick_margin_borrow_repay_history = privateGetAccountQuickMarginBorrowRepayHistory = Entry('account/quick-margin-borrow-repay-history', 'private', 'GET', {'cost': 4}) + private_get_account_borrow_repay_history = privateGetAccountBorrowRepayHistory = Entry('account/borrow-repay-history', 'private', 'GET', {'cost': 4}) + private_get_account_vip_interest_accrued = privateGetAccountVipInterestAccrued = Entry('account/vip-interest-accrued', 'private', 'GET', {'cost': 4}) + private_get_account_vip_interest_deducted = privateGetAccountVipInterestDeducted = Entry('account/vip-interest-deducted', 'private', 'GET', {'cost': 4}) + private_get_account_vip_loan_order_list = privateGetAccountVipLoanOrderList = Entry('account/vip-loan-order-list', 'private', 'GET', {'cost': 4}) + private_get_account_vip_loan_order_detail = privateGetAccountVipLoanOrderDetail = Entry('account/vip-loan-order-detail', 'private', 'GET', {'cost': 4}) + private_get_account_interest_limits = privateGetAccountInterestLimits = Entry('account/interest-limits', 'private', 'GET', {'cost': 4}) + private_get_account_greeks = privateGetAccountGreeks = Entry('account/greeks', 'private', 'GET', {'cost': 2}) + private_get_account_position_tiers = privateGetAccountPositionTiers = Entry('account/position-tiers', 'private', 'GET', {'cost': 2}) + private_get_account_mmp_config = privateGetAccountMmpConfig = Entry('account/mmp-config', 'private', 'GET', {'cost': 4}) + private_get_account_fixed_loan_borrowing_limit = privateGetAccountFixedLoanBorrowingLimit = Entry('account/fixed-loan/borrowing-limit', 'private', 'GET', {'cost': 4}) + private_get_account_fixed_loan_borrowing_quote = privateGetAccountFixedLoanBorrowingQuote = Entry('account/fixed-loan/borrowing-quote', 'private', 'GET', {'cost': 5}) + private_get_account_fixed_loan_borrowing_orders_list = privateGetAccountFixedLoanBorrowingOrdersList = Entry('account/fixed-loan/borrowing-orders-list', 'private', 'GET', {'cost': 5}) + private_get_account_spot_manual_borrow_repay = privateGetAccountSpotManualBorrowRepay = Entry('account/spot-manual-borrow-repay', 'private', 'GET', {'cost': 30}) + private_get_account_set_auto_repay = privateGetAccountSetAutoRepay = Entry('account/set-auto-repay', 'private', 'GET', {'cost': 4}) + private_get_account_spot_borrow_repay_history = privateGetAccountSpotBorrowRepayHistory = Entry('account/spot-borrow-repay-history', 'private', 'GET', {'cost': 4}) + private_get_account_move_positions_history = privateGetAccountMovePositionsHistory = Entry('account/move-positions-history', 'private', 'GET', {'cost': 10}) + private_get_users_subaccount_list = privateGetUsersSubaccountList = Entry('users/subaccount/list', 'private', 'GET', {'cost': 10}) + private_get_account_subaccount_balances = privateGetAccountSubaccountBalances = Entry('account/subaccount/balances', 'private', 'GET', {'cost': 3.3333333333333335}) + private_get_asset_subaccount_balances = privateGetAssetSubaccountBalances = Entry('asset/subaccount/balances', 'private', 'GET', {'cost': 3.3333333333333335}) + private_get_account_subaccount_max_withdrawal = privateGetAccountSubaccountMaxWithdrawal = Entry('account/subaccount/max-withdrawal', 'private', 'GET', {'cost': 1}) + private_get_asset_subaccount_bills = privateGetAssetSubaccountBills = Entry('asset/subaccount/bills', 'private', 'GET', {'cost': 1.6666666666666667}) + private_get_asset_subaccount_managed_subaccount_bills = privateGetAssetSubaccountManagedSubaccountBills = Entry('asset/subaccount/managed-subaccount-bills', 'private', 'GET', {'cost': 1.6666666666666667}) + private_get_users_entrust_subaccount_list = privateGetUsersEntrustSubaccountList = Entry('users/entrust-subaccount-list', 'private', 'GET', {'cost': 10}) + private_get_account_subaccount_interest_limits = privateGetAccountSubaccountInterestLimits = Entry('account/subaccount/interest-limits', 'private', 'GET', {'cost': 4}) + private_get_users_subaccount_apikey = privateGetUsersSubaccountApikey = Entry('users/subaccount/apikey', 'private', 'GET', {'cost': 10}) + private_get_tradingbot_grid_orders_algo_pending = privateGetTradingBotGridOrdersAlgoPending = Entry('tradingBot/grid/orders-algo-pending', 'private', 'GET', {'cost': 1}) + private_get_tradingbot_grid_orders_algo_history = privateGetTradingBotGridOrdersAlgoHistory = Entry('tradingBot/grid/orders-algo-history', 'private', 'GET', {'cost': 1}) + private_get_tradingbot_grid_orders_algo_details = privateGetTradingBotGridOrdersAlgoDetails = Entry('tradingBot/grid/orders-algo-details', 'private', 'GET', {'cost': 1}) + private_get_tradingbot_grid_sub_orders = privateGetTradingBotGridSubOrders = Entry('tradingBot/grid/sub-orders', 'private', 'GET', {'cost': 1}) + private_get_tradingbot_grid_positions = privateGetTradingBotGridPositions = Entry('tradingBot/grid/positions', 'private', 'GET', {'cost': 1}) + private_get_tradingbot_grid_ai_param = privateGetTradingBotGridAiParam = Entry('tradingBot/grid/ai-param', 'private', 'GET', {'cost': 1}) + private_get_tradingbot_signal_signals = privateGetTradingBotSignalSignals = Entry('tradingBot/signal/signals', 'private', 'GET', {'cost': 1}) + private_get_tradingbot_signal_orders_algo_details = privateGetTradingBotSignalOrdersAlgoDetails = Entry('tradingBot/signal/orders-algo-details', 'private', 'GET', {'cost': 1}) + private_get_tradingbot_signal_orders_algo_history = privateGetTradingBotSignalOrdersAlgoHistory = Entry('tradingBot/signal/orders-algo-history', 'private', 'GET', {'cost': 1}) + private_get_tradingbot_signal_positions = privateGetTradingBotSignalPositions = Entry('tradingBot/signal/positions', 'private', 'GET', {'cost': 1}) + private_get_tradingbot_signal_positions_history = privateGetTradingBotSignalPositionsHistory = Entry('tradingBot/signal/positions-history', 'private', 'GET', {'cost': 1}) + private_get_tradingbot_signal_sub_orders = privateGetTradingBotSignalSubOrders = Entry('tradingBot/signal/sub-orders', 'private', 'GET', {'cost': 1}) + private_get_tradingbot_signal_event_history = privateGetTradingBotSignalEventHistory = Entry('tradingBot/signal/event-history', 'private', 'GET', {'cost': 1}) + private_get_tradingbot_recurring_orders_algo_pending = privateGetTradingBotRecurringOrdersAlgoPending = Entry('tradingBot/recurring/orders-algo-pending', 'private', 'GET', {'cost': 1}) + private_get_tradingbot_recurring_orders_algo_history = privateGetTradingBotRecurringOrdersAlgoHistory = Entry('tradingBot/recurring/orders-algo-history', 'private', 'GET', {'cost': 1}) + private_get_tradingbot_recurring_orders_algo_details = privateGetTradingBotRecurringOrdersAlgoDetails = Entry('tradingBot/recurring/orders-algo-details', 'private', 'GET', {'cost': 1}) + private_get_tradingbot_recurring_sub_orders = privateGetTradingBotRecurringSubOrders = Entry('tradingBot/recurring/sub-orders', 'private', 'GET', {'cost': 1}) + private_get_finance_savings_balance = privateGetFinanceSavingsBalance = Entry('finance/savings/balance', 'private', 'GET', {'cost': 1.6666666666666667}) + private_get_finance_savings_lending_history = privateGetFinanceSavingsLendingHistory = Entry('finance/savings/lending-history', 'private', 'GET', {'cost': 1.6666666666666667}) + private_get_finance_staking_defi_offers = privateGetFinanceStakingDefiOffers = Entry('finance/staking-defi/offers', 'private', 'GET', {'cost': 3.3333333333333335}) + private_get_finance_staking_defi_orders_active = privateGetFinanceStakingDefiOrdersActive = Entry('finance/staking-defi/orders-active', 'private', 'GET', {'cost': 3.3333333333333335}) + private_get_finance_staking_defi_orders_history = privateGetFinanceStakingDefiOrdersHistory = Entry('finance/staking-defi/orders-history', 'private', 'GET', {'cost': 3.3333333333333335}) + private_get_finance_staking_defi_eth_balance = privateGetFinanceStakingDefiEthBalance = Entry('finance/staking-defi/eth/balance', 'private', 'GET', {'cost': 1.6666666666666667}) + private_get_finance_staking_defi_eth_purchase_redeem_history = privateGetFinanceStakingDefiEthPurchaseRedeemHistory = Entry('finance/staking-defi/eth/purchase-redeem-history', 'private', 'GET', {'cost': 1.6666666666666667}) + private_get_finance_staking_defi_eth_product_info = privateGetFinanceStakingDefiEthProductInfo = Entry('finance/staking-defi/eth/product-info', 'private', 'GET', {'cost': 3}) + private_get_finance_staking_defi_sol_balance = privateGetFinanceStakingDefiSolBalance = Entry('finance/staking-defi/sol/balance', 'private', 'GET', {'cost': 1.6666666666666667}) + private_get_finance_staking_defi_sol_purchase_redeem_history = privateGetFinanceStakingDefiSolPurchaseRedeemHistory = Entry('finance/staking-defi/sol/purchase-redeem-history', 'private', 'GET', {'cost': 1.6666666666666667}) + private_get_copytrading_current_subpositions = privateGetCopytradingCurrentSubpositions = Entry('copytrading/current-subpositions', 'private', 'GET', {'cost': 1}) + private_get_copytrading_subpositions_history = privateGetCopytradingSubpositionsHistory = Entry('copytrading/subpositions-history', 'private', 'GET', {'cost': 1}) + private_get_copytrading_instruments = privateGetCopytradingInstruments = Entry('copytrading/instruments', 'private', 'GET', {'cost': 4}) + private_get_copytrading_profit_sharing_details = privateGetCopytradingProfitSharingDetails = Entry('copytrading/profit-sharing-details', 'private', 'GET', {'cost': 4}) + private_get_copytrading_total_profit_sharing = privateGetCopytradingTotalProfitSharing = Entry('copytrading/total-profit-sharing', 'private', 'GET', {'cost': 4}) + private_get_copytrading_unrealized_profit_sharing_details = privateGetCopytradingUnrealizedProfitSharingDetails = Entry('copytrading/unrealized-profit-sharing-details', 'private', 'GET', {'cost': 4}) + private_get_copytrading_copy_settings = privateGetCopytradingCopySettings = Entry('copytrading/copy-settings', 'private', 'GET', {'cost': 4}) + private_get_copytrading_batch_leverage_info = privateGetCopytradingBatchLeverageInfo = Entry('copytrading/batch-leverage-info', 'private', 'GET', {'cost': 4}) + private_get_copytrading_current_lead_traders = privateGetCopytradingCurrentLeadTraders = Entry('copytrading/current-lead-traders', 'private', 'GET', {'cost': 4}) + private_get_copytrading_lead_traders_history = privateGetCopytradingLeadTradersHistory = Entry('copytrading/lead-traders-history', 'private', 'GET', {'cost': 4}) + private_get_broker_nd_info = privateGetBrokerNdInfo = Entry('broker/nd/info', 'private', 'GET', {'cost': 10}) + private_get_broker_nd_subaccount_info = privateGetBrokerNdSubaccountInfo = Entry('broker/nd/subaccount-info', 'private', 'GET', {'cost': 10}) + private_get_broker_nd_subaccount_apikey = privateGetBrokerNdSubaccountApikey = Entry('broker/nd/subaccount/apikey', 'private', 'GET', {'cost': 10}) + private_get_asset_broker_nd_subaccount_deposit_address = privateGetAssetBrokerNdSubaccountDepositAddress = Entry('asset/broker/nd/subaccount-deposit-address', 'private', 'GET', {'cost': 1.6666666666666667}) + private_get_asset_broker_nd_subaccount_deposit_history = privateGetAssetBrokerNdSubaccountDepositHistory = Entry('asset/broker/nd/subaccount-deposit-history', 'private', 'GET', {'cost': 4}) + private_get_asset_broker_nd_subaccount_withdrawal_history = privateGetAssetBrokerNdSubaccountWithdrawalHistory = Entry('asset/broker/nd/subaccount-withdrawal-history', 'private', 'GET', {'cost': 4}) + private_get_broker_nd_rebate_daily = privateGetBrokerNdRebateDaily = Entry('broker/nd/rebate-daily', 'private', 'GET', {'cost': 100}) + private_get_broker_nd_rebate_per_orders = privateGetBrokerNdRebatePerOrders = Entry('broker/nd/rebate-per-orders', 'private', 'GET', {'cost': 300}) + private_get_finance_sfp_dcd_order = privateGetFinanceSfpDcdOrder = Entry('finance/sfp/dcd/order', 'private', 'GET', {'cost': 2}) + private_get_finance_sfp_dcd_orders = privateGetFinanceSfpDcdOrders = Entry('finance/sfp/dcd/orders', 'private', 'GET', {'cost': 2}) + private_get_broker_fd_rebate_per_orders = privateGetBrokerFdRebatePerOrders = Entry('broker/fd/rebate-per-orders', 'private', 'GET', {'cost': 300}) + private_get_broker_fd_if_rebate = privateGetBrokerFdIfRebate = Entry('broker/fd/if-rebate', 'private', 'GET', {'cost': 5}) + private_get_affiliate_invitee_detail = privateGetAffiliateInviteeDetail = Entry('affiliate/invitee/detail', 'private', 'GET', {'cost': 1}) + private_get_users_partner_if_rebate = privateGetUsersPartnerIfRebate = Entry('users/partner/if-rebate', 'private', 'GET', {'cost': 1}) + private_get_support_announcements = privateGetSupportAnnouncements = Entry('support/announcements', 'private', 'GET', {'cost': 4}) + private_post_rfq_create_rfq = privatePostRfqCreateRfq = Entry('rfq/create-rfq', 'private', 'POST', {'cost': 4}) + private_post_rfq_cancel_rfq = privatePostRfqCancelRfq = Entry('rfq/cancel-rfq', 'private', 'POST', {'cost': 4}) + private_post_rfq_cancel_batch_rfqs = privatePostRfqCancelBatchRfqs = Entry('rfq/cancel-batch-rfqs', 'private', 'POST', {'cost': 10}) + private_post_rfq_cancel_all_rfqs = privatePostRfqCancelAllRfqs = Entry('rfq/cancel-all-rfqs', 'private', 'POST', {'cost': 10}) + private_post_rfq_execute_quote = privatePostRfqExecuteQuote = Entry('rfq/execute-quote', 'private', 'POST', {'cost': 15}) + private_post_rfq_maker_instrument_settings = privatePostRfqMakerInstrumentSettings = Entry('rfq/maker-instrument-settings', 'private', 'POST', {'cost': 4}) + private_post_rfq_mmp_reset = privatePostRfqMmpReset = Entry('rfq/mmp-reset', 'private', 'POST', {'cost': 4}) + private_post_rfq_mmp_config = privatePostRfqMmpConfig = Entry('rfq/mmp-config', 'private', 'POST', {'cost': 100}) + private_post_rfq_create_quote = privatePostRfqCreateQuote = Entry('rfq/create-quote', 'private', 'POST', {'cost': 0.4}) + private_post_rfq_cancel_quote = privatePostRfqCancelQuote = Entry('rfq/cancel-quote', 'private', 'POST', {'cost': 0.4}) + private_post_rfq_cancel_batch_quotes = privatePostRfqCancelBatchQuotes = Entry('rfq/cancel-batch-quotes', 'private', 'POST', {'cost': 10}) + private_post_rfq_cancel_all_quotes = privatePostRfqCancelAllQuotes = Entry('rfq/cancel-all-quotes', 'private', 'POST', {'cost': 10}) + private_post_sprd_order = privatePostSprdOrder = Entry('sprd/order', 'private', 'POST', {'cost': 1}) + private_post_sprd_cancel_order = privatePostSprdCancelOrder = Entry('sprd/cancel-order', 'private', 'POST', {'cost': 1}) + private_post_sprd_mass_cancel = privatePostSprdMassCancel = Entry('sprd/mass-cancel', 'private', 'POST', {'cost': 1}) + private_post_sprd_amend_order = privatePostSprdAmendOrder = Entry('sprd/amend-order', 'private', 'POST', {'cost': 1}) + private_post_sprd_cancel_all_after = privatePostSprdCancelAllAfter = Entry('sprd/cancel-all-after', 'private', 'POST', {'cost': 10}) + private_post_trade_order = privatePostTradeOrder = Entry('trade/order', 'private', 'POST', {'cost': 0.3333333333333333}) + private_post_trade_batch_orders = privatePostTradeBatchOrders = Entry('trade/batch-orders', 'private', 'POST', {'cost': 0.06666666666666667}) + private_post_trade_cancel_order = privatePostTradeCancelOrder = Entry('trade/cancel-order', 'private', 'POST', {'cost': 0.3333333333333333}) + private_post_trade_cancel_batch_orders = privatePostTradeCancelBatchOrders = Entry('trade/cancel-batch-orders', 'private', 'POST', {'cost': 0.06666666666666667}) + private_post_trade_amend_order = privatePostTradeAmendOrder = Entry('trade/amend-order', 'private', 'POST', {'cost': 0.3333333333333333}) + private_post_trade_amend_batch_orders = privatePostTradeAmendBatchOrders = Entry('trade/amend-batch-orders', 'private', 'POST', {'cost': 0.006666666666666667}) + private_post_trade_close_position = privatePostTradeClosePosition = Entry('trade/close-position', 'private', 'POST', {'cost': 1}) + private_post_trade_fills_archive = privatePostTradeFillsArchive = Entry('trade/fills-archive', 'private', 'POST', {'cost': 172800}) + private_post_trade_order_algo = privatePostTradeOrderAlgo = Entry('trade/order-algo', 'private', 'POST', {'cost': 1}) + private_post_trade_cancel_algos = privatePostTradeCancelAlgos = Entry('trade/cancel-algos', 'private', 'POST', {'cost': 1}) + private_post_trade_amend_algos = privatePostTradeAmendAlgos = Entry('trade/amend-algos', 'private', 'POST', {'cost': 1}) + private_post_trade_cancel_advance_algos = privatePostTradeCancelAdvanceAlgos = Entry('trade/cancel-advance-algos', 'private', 'POST', {'cost': 1}) + private_post_trade_easy_convert = privatePostTradeEasyConvert = Entry('trade/easy-convert', 'private', 'POST', {'cost': 20}) + private_post_trade_one_click_repay = privatePostTradeOneClickRepay = Entry('trade/one-click-repay', 'private', 'POST', {'cost': 20}) + private_post_trade_one_click_repay_v2 = privatePostTradeOneClickRepayV2 = Entry('trade/one-click-repay-v2', 'private', 'POST', {'cost': 20}) + private_post_trade_mass_cancel = privatePostTradeMassCancel = Entry('trade/mass-cancel', 'private', 'POST', {'cost': 4}) + private_post_trade_cancel_all_after = privatePostTradeCancelAllAfter = Entry('trade/cancel-all-after', 'private', 'POST', {'cost': 10}) + private_post_asset_transfer = privatePostAssetTransfer = Entry('asset/transfer', 'private', 'POST', {'cost': 10}) + private_post_asset_withdrawal = privatePostAssetWithdrawal = Entry('asset/withdrawal', 'private', 'POST', {'cost': 1.6666666666666667}) + private_post_asset_withdrawal_lightning = privatePostAssetWithdrawalLightning = Entry('asset/withdrawal-lightning', 'private', 'POST', {'cost': 5}) + private_post_asset_cancel_withdrawal = privatePostAssetCancelWithdrawal = Entry('asset/cancel-withdrawal', 'private', 'POST', {'cost': 1.6666666666666667}) + private_post_asset_convert_dust_assets = privatePostAssetConvertDustAssets = Entry('asset/convert-dust-assets', 'private', 'POST', {'cost': 10}) + private_post_asset_convert_estimate_quote = privatePostAssetConvertEstimateQuote = Entry('asset/convert/estimate-quote', 'private', 'POST', {'cost': 1}) + private_post_asset_convert_trade = privatePostAssetConvertTrade = Entry('asset/convert/trade', 'private', 'POST', {'cost': 1}) + private_post_asset_monthly_statement = privatePostAssetMonthlyStatement = Entry('asset/monthly-statement', 'private', 'POST', {'cost': 1}) + private_post_account_set_position_mode = privatePostAccountSetPositionMode = Entry('account/set-position-mode', 'private', 'POST', {'cost': 4}) + private_post_account_set_leverage = privatePostAccountSetLeverage = Entry('account/set-leverage', 'private', 'POST', {'cost': 1}) + private_post_account_position_margin_balance = privatePostAccountPositionMarginBalance = Entry('account/position/margin-balance', 'private', 'POST', {'cost': 1}) + private_post_account_set_greeks = privatePostAccountSetGreeks = Entry('account/set-greeks', 'private', 'POST', {'cost': 4}) + private_post_account_set_isolated_mode = privatePostAccountSetIsolatedMode = Entry('account/set-isolated-mode', 'private', 'POST', {'cost': 4}) + private_post_account_quick_margin_borrow_repay = privatePostAccountQuickMarginBorrowRepay = Entry('account/quick-margin-borrow-repay', 'private', 'POST', {'cost': 4}) + private_post_account_borrow_repay = privatePostAccountBorrowRepay = Entry('account/borrow-repay', 'private', 'POST', {'cost': 1.6666666666666667}) + private_post_account_simulated_margin = privatePostAccountSimulatedMargin = Entry('account/simulated_margin', 'private', 'POST', {'cost': 10}) + private_post_account_position_builder = privatePostAccountPositionBuilder = Entry('account/position-builder', 'private', 'POST', {'cost': 10}) + private_post_account_set_riskoffset_type = privatePostAccountSetRiskOffsetType = Entry('account/set-riskOffset-type', 'private', 'POST', {'cost': 2}) + private_post_account_activate_option = privatePostAccountActivateOption = Entry('account/activate-option', 'private', 'POST', {'cost': 4}) + private_post_account_set_auto_loan = privatePostAccountSetAutoLoan = Entry('account/set-auto-loan', 'private', 'POST', {'cost': 4}) + private_post_account_set_account_level = privatePostAccountSetAccountLevel = Entry('account/set-account-level', 'private', 'POST', {'cost': 4}) + private_post_account_mmp_reset = privatePostAccountMmpReset = Entry('account/mmp-reset', 'private', 'POST', {'cost': 4}) + private_post_account_mmp_config = privatePostAccountMmpConfig = Entry('account/mmp-config', 'private', 'POST', {'cost': 100}) + private_post_account_fixed_loan_borrowing_order = privatePostAccountFixedLoanBorrowingOrder = Entry('account/fixed-loan/borrowing-order', 'private', 'POST', {'cost': 5}) + private_post_account_fixed_loan_amend_borrowing_order = privatePostAccountFixedLoanAmendBorrowingOrder = Entry('account/fixed-loan/amend-borrowing-order', 'private', 'POST', {'cost': 5}) + private_post_account_fixed_loan_manual_reborrow = privatePostAccountFixedLoanManualReborrow = Entry('account/fixed-loan/manual-reborrow', 'private', 'POST', {'cost': 5}) + private_post_account_fixed_loan_repay_borrowing_order = privatePostAccountFixedLoanRepayBorrowingOrder = Entry('account/fixed-loan/repay-borrowing-order', 'private', 'POST', {'cost': 5}) + private_post_account_bills_history_archive = privatePostAccountBillsHistoryArchive = Entry('account/bills-history-archive', 'private', 'POST', {'cost': 72000}) + private_post_account_move_positions = privatePostAccountMovePositions = Entry('account/move-positions', 'private', 'POST', {'cost': 10}) + private_post_account_set_settle_currency = privatePostAccountSetSettleCurrency = Entry('account/set-settle-currency', 'private', 'POST', {'cost': 1}) + private_post_users_subaccount_modify_apikey = privatePostUsersSubaccountModifyApikey = Entry('users/subaccount/modify-apikey', 'private', 'POST', {'cost': 10}) + private_post_asset_subaccount_transfer = privatePostAssetSubaccountTransfer = Entry('asset/subaccount/transfer', 'private', 'POST', {'cost': 10}) + private_post_users_subaccount_set_transfer_out = privatePostUsersSubaccountSetTransferOut = Entry('users/subaccount/set-transfer-out', 'private', 'POST', {'cost': 10}) + private_post_account_subaccount_set_loan_allocation = privatePostAccountSubaccountSetLoanAllocation = Entry('account/subaccount/set-loan-allocation', 'private', 'POST', {'cost': 4}) + private_post_users_subaccount_create_subaccount = privatePostUsersSubaccountCreateSubaccount = Entry('users/subaccount/create-subaccount', 'private', 'POST', {'cost': 10}) + private_post_users_subaccount_subaccount_apikey = privatePostUsersSubaccountSubaccountApikey = Entry('users/subaccount/subaccount-apikey', 'private', 'POST', {'cost': 10}) + private_post_users_subaccount_delete_apikey = privatePostUsersSubaccountDeleteApikey = Entry('users/subaccount/delete-apikey', 'private', 'POST', {'cost': 10}) + private_post_tradingbot_grid_order_algo = privatePostTradingBotGridOrderAlgo = Entry('tradingBot/grid/order-algo', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_grid_amend_order_algo = privatePostTradingBotGridAmendOrderAlgo = Entry('tradingBot/grid/amend-order-algo', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_grid_stop_order_algo = privatePostTradingBotGridStopOrderAlgo = Entry('tradingBot/grid/stop-order-algo', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_grid_close_position = privatePostTradingBotGridClosePosition = Entry('tradingBot/grid/close-position', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_grid_cancel_close_order = privatePostTradingBotGridCancelCloseOrder = Entry('tradingBot/grid/cancel-close-order', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_grid_order_instant_trigger = privatePostTradingBotGridOrderInstantTrigger = Entry('tradingBot/grid/order-instant-trigger', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_grid_withdraw_income = privatePostTradingBotGridWithdrawIncome = Entry('tradingBot/grid/withdraw-income', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_grid_compute_margin_balance = privatePostTradingBotGridComputeMarginBalance = Entry('tradingBot/grid/compute-margin-balance', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_grid_margin_balance = privatePostTradingBotGridMarginBalance = Entry('tradingBot/grid/margin-balance', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_grid_min_investment = privatePostTradingBotGridMinInvestment = Entry('tradingBot/grid/min-investment', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_grid_adjust_investment = privatePostTradingBotGridAdjustInvestment = Entry('tradingBot/grid/adjust-investment', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_signal_create_signal = privatePostTradingBotSignalCreateSignal = Entry('tradingBot/signal/create-signal', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_signal_order_algo = privatePostTradingBotSignalOrderAlgo = Entry('tradingBot/signal/order-algo', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_signal_stop_order_algo = privatePostTradingBotSignalStopOrderAlgo = Entry('tradingBot/signal/stop-order-algo', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_signal_margin_balance = privatePostTradingBotSignalMarginBalance = Entry('tradingBot/signal/margin-balance', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_signal_amendtpsl = privatePostTradingBotSignalAmendTPSL = Entry('tradingBot/signal/amendTPSL', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_signal_set_instruments = privatePostTradingBotSignalSetInstruments = Entry('tradingBot/signal/set-instruments', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_signal_close_position = privatePostTradingBotSignalClosePosition = Entry('tradingBot/signal/close-position', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_signal_sub_order = privatePostTradingBotSignalSubOrder = Entry('tradingBot/signal/sub-order', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_signal_cancel_sub_order = privatePostTradingBotSignalCancelSubOrder = Entry('tradingBot/signal/cancel-sub-order', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_recurring_order_algo = privatePostTradingBotRecurringOrderAlgo = Entry('tradingBot/recurring/order-algo', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_recurring_amend_order_algo = privatePostTradingBotRecurringAmendOrderAlgo = Entry('tradingBot/recurring/amend-order-algo', 'private', 'POST', {'cost': 1}) + private_post_tradingbot_recurring_stop_order_algo = privatePostTradingBotRecurringStopOrderAlgo = Entry('tradingBot/recurring/stop-order-algo', 'private', 'POST', {'cost': 1}) + private_post_finance_savings_purchase_redempt = privatePostFinanceSavingsPurchaseRedempt = Entry('finance/savings/purchase-redempt', 'private', 'POST', {'cost': 1.6666666666666667}) + private_post_finance_savings_set_lending_rate = privatePostFinanceSavingsSetLendingRate = Entry('finance/savings/set-lending-rate', 'private', 'POST', {'cost': 1.6666666666666667}) + private_post_finance_staking_defi_purchase = privatePostFinanceStakingDefiPurchase = Entry('finance/staking-defi/purchase', 'private', 'POST', {'cost': 3}) + private_post_finance_staking_defi_redeem = privatePostFinanceStakingDefiRedeem = Entry('finance/staking-defi/redeem', 'private', 'POST', {'cost': 3}) + private_post_finance_staking_defi_cancel = privatePostFinanceStakingDefiCancel = Entry('finance/staking-defi/cancel', 'private', 'POST', {'cost': 3}) + private_post_finance_staking_defi_eth_purchase = privatePostFinanceStakingDefiEthPurchase = Entry('finance/staking-defi/eth/purchase', 'private', 'POST', {'cost': 5}) + private_post_finance_staking_defi_eth_redeem = privatePostFinanceStakingDefiEthRedeem = Entry('finance/staking-defi/eth/redeem', 'private', 'POST', {'cost': 5}) + private_post_finance_staking_defi_sol_purchase = privatePostFinanceStakingDefiSolPurchase = Entry('finance/staking-defi/sol/purchase', 'private', 'POST', {'cost': 5}) + private_post_finance_staking_defi_sol_redeem = privatePostFinanceStakingDefiSolRedeem = Entry('finance/staking-defi/sol/redeem', 'private', 'POST', {'cost': 5}) + private_post_copytrading_algo_order = privatePostCopytradingAlgoOrder = Entry('copytrading/algo-order', 'private', 'POST', {'cost': 1}) + private_post_copytrading_close_subposition = privatePostCopytradingCloseSubposition = Entry('copytrading/close-subposition', 'private', 'POST', {'cost': 1}) + private_post_copytrading_set_instruments = privatePostCopytradingSetInstruments = Entry('copytrading/set-instruments', 'private', 'POST', {'cost': 4}) + private_post_copytrading_first_copy_settings = privatePostCopytradingFirstCopySettings = Entry('copytrading/first-copy-settings', 'private', 'POST', {'cost': 4}) + private_post_copytrading_amend_copy_settings = privatePostCopytradingAmendCopySettings = Entry('copytrading/amend-copy-settings', 'private', 'POST', {'cost': 4}) + private_post_copytrading_stop_copy_trading = privatePostCopytradingStopCopyTrading = Entry('copytrading/stop-copy-trading', 'private', 'POST', {'cost': 4}) + private_post_copytrading_batch_set_leverage = privatePostCopytradingBatchSetLeverage = Entry('copytrading/batch-set-leverage', 'private', 'POST', {'cost': 4}) + private_post_broker_nd_create_subaccount = privatePostBrokerNdCreateSubaccount = Entry('broker/nd/create-subaccount', 'private', 'POST', {'cost': 0.25}) + private_post_broker_nd_delete_subaccount = privatePostBrokerNdDeleteSubaccount = Entry('broker/nd/delete-subaccount', 'private', 'POST', {'cost': 1}) + private_post_broker_nd_subaccount_apikey = privatePostBrokerNdSubaccountApikey = Entry('broker/nd/subaccount/apikey', 'private', 'POST', {'cost': 0.25}) + private_post_broker_nd_subaccount_modify_apikey = privatePostBrokerNdSubaccountModifyApikey = Entry('broker/nd/subaccount/modify-apikey', 'private', 'POST', {'cost': 1}) + private_post_broker_nd_subaccount_delete_apikey = privatePostBrokerNdSubaccountDeleteApikey = Entry('broker/nd/subaccount/delete-apikey', 'private', 'POST', {'cost': 1}) + private_post_broker_nd_set_subaccount_level = privatePostBrokerNdSetSubaccountLevel = Entry('broker/nd/set-subaccount-level', 'private', 'POST', {'cost': 4}) + private_post_broker_nd_set_subaccount_fee_rate = privatePostBrokerNdSetSubaccountFeeRate = Entry('broker/nd/set-subaccount-fee-rate', 'private', 'POST', {'cost': 4}) + private_post_broker_nd_set_subaccount_assets = privatePostBrokerNdSetSubaccountAssets = Entry('broker/nd/set-subaccount-assets', 'private', 'POST', {'cost': 0.25}) + private_post_asset_broker_nd_subaccount_deposit_address = privatePostAssetBrokerNdSubaccountDepositAddress = Entry('asset/broker/nd/subaccount-deposit-address', 'private', 'POST', {'cost': 1}) + private_post_asset_broker_nd_modify_subaccount_deposit_address = privatePostAssetBrokerNdModifySubaccountDepositAddress = Entry('asset/broker/nd/modify-subaccount-deposit-address', 'private', 'POST', {'cost': 1.6666666666666667}) + private_post_broker_nd_rebate_per_orders = privatePostBrokerNdRebatePerOrders = Entry('broker/nd/rebate-per-orders', 'private', 'POST', {'cost': 36000}) + private_post_finance_sfp_dcd_quote = privatePostFinanceSfpDcdQuote = Entry('finance/sfp/dcd/quote', 'private', 'POST', {'cost': 10}) + private_post_finance_sfp_dcd_order = privatePostFinanceSfpDcdOrder = Entry('finance/sfp/dcd/order', 'private', 'POST', {'cost': 10}) + private_post_broker_nd_report_subaccount_ip = privatePostBrokerNdReportSubaccountIp = Entry('broker/nd/report-subaccount-ip', 'private', 'POST', {'cost': 0.25}) + private_post_broker_fd_rebate_per_orders = privatePostBrokerFdRebatePerOrders = Entry('broker/fd/rebate-per-orders', 'private', 'POST', {'cost': 36000}) diff --git a/ccxt/abstract/onetrading.py b/ccxt/abstract/onetrading.py new file mode 100644 index 0000000..d33cbf2 --- /dev/null +++ b/ccxt/abstract/onetrading.py @@ -0,0 +1,23 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_currencies = publicGetCurrencies = Entry('currencies', 'public', 'GET', {}) + public_get_candlesticks_instrument_code = publicGetCandlesticksInstrumentCode = Entry('candlesticks/{instrument_code}', 'public', 'GET', {}) + public_get_fees = publicGetFees = Entry('fees', 'public', 'GET', {}) + public_get_instruments = publicGetInstruments = Entry('instruments', 'public', 'GET', {}) + public_get_order_book_instrument_code = publicGetOrderBookInstrumentCode = Entry('order-book/{instrument_code}', 'public', 'GET', {}) + public_get_market_ticker = publicGetMarketTicker = Entry('market-ticker', 'public', 'GET', {}) + public_get_market_ticker_instrument_code = publicGetMarketTickerInstrumentCode = Entry('market-ticker/{instrument_code}', 'public', 'GET', {}) + public_get_time = publicGetTime = Entry('time', 'public', 'GET', {}) + private_get_account_balances = privateGetAccountBalances = Entry('account/balances', 'private', 'GET', {}) + private_get_account_fees = privateGetAccountFees = Entry('account/fees', 'private', 'GET', {}) + private_get_account_orders = privateGetAccountOrders = Entry('account/orders', 'private', 'GET', {}) + private_get_account_orders_order_id = privateGetAccountOrdersOrderId = Entry('account/orders/{order_id}', 'private', 'GET', {}) + private_get_account_orders_order_id_trades = privateGetAccountOrdersOrderIdTrades = Entry('account/orders/{order_id}/trades', 'private', 'GET', {}) + private_get_account_trades = privateGetAccountTrades = Entry('account/trades', 'private', 'GET', {}) + private_get_account_trades_trade_id = privateGetAccountTradesTradeId = Entry('account/trades/{trade_id}', 'private', 'GET', {}) + private_post_account_orders = privatePostAccountOrders = Entry('account/orders', 'private', 'POST', {}) + private_delete_account_orders = privateDeleteAccountOrders = Entry('account/orders', 'private', 'DELETE', {}) + private_delete_account_orders_order_id = privateDeleteAccountOrdersOrderId = Entry('account/orders/{order_id}', 'private', 'DELETE', {}) + private_delete_account_orders_client_client_id = privateDeleteAccountOrdersClientClientId = Entry('account/orders/client/{client_id}', 'private', 'DELETE', {}) diff --git a/ccxt/abstract/oxfun.py b/ccxt/abstract/oxfun.py new file mode 100644 index 0000000..9395e17 --- /dev/null +++ b/ccxt/abstract/oxfun.py @@ -0,0 +1,34 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_v3_markets = publicGetV3Markets = Entry('v3/markets', 'public', 'GET', {'cost': 1}) + public_get_v3_assets = publicGetV3Assets = Entry('v3/assets', 'public', 'GET', {'cost': 1}) + public_get_v3_tickers = publicGetV3Tickers = Entry('v3/tickers', 'public', 'GET', {'cost': 1}) + public_get_v3_funding_estimates = publicGetV3FundingEstimates = Entry('v3/funding/estimates', 'public', 'GET', {'cost': 1}) + public_get_v3_candles = publicGetV3Candles = Entry('v3/candles', 'public', 'GET', {'cost': 1}) + public_get_v3_depth = publicGetV3Depth = Entry('v3/depth', 'public', 'GET', {'cost': 1}) + public_get_v3_markets_operational = publicGetV3MarketsOperational = Entry('v3/markets/operational', 'public', 'GET', {'cost': 1}) + public_get_v3_exchange_trades = publicGetV3ExchangeTrades = Entry('v3/exchange-trades', 'public', 'GET', {'cost': 1}) + public_get_v3_funding_rates = publicGetV3FundingRates = Entry('v3/funding/rates', 'public', 'GET', {'cost': 1}) + public_get_v3_leverage_tiers = publicGetV3LeverageTiers = Entry('v3/leverage/tiers', 'public', 'GET', {'cost': 1}) + private_get_v3_account = privateGetV3Account = Entry('v3/account', 'private', 'GET', {'cost': 1}) + private_get_v3_account_names = privateGetV3AccountNames = Entry('v3/account/names', 'private', 'GET', {'cost': 1}) + private_get_v3_wallet = privateGetV3Wallet = Entry('v3/wallet', 'private', 'GET', {'cost': 1}) + private_get_v3_transfer = privateGetV3Transfer = Entry('v3/transfer', 'private', 'GET', {'cost': 1}) + private_get_v3_balances = privateGetV3Balances = Entry('v3/balances', 'private', 'GET', {'cost': 1}) + private_get_v3_positions = privateGetV3Positions = Entry('v3/positions', 'private', 'GET', {'cost': 1}) + private_get_v3_funding = privateGetV3Funding = Entry('v3/funding', 'private', 'GET', {'cost': 1}) + private_get_v3_deposit_addresses = privateGetV3DepositAddresses = Entry('v3/deposit-addresses', 'private', 'GET', {'cost': 1}) + private_get_v3_deposit = privateGetV3Deposit = Entry('v3/deposit', 'private', 'GET', {'cost': 1}) + private_get_v3_withdrawal_addresses = privateGetV3WithdrawalAddresses = Entry('v3/withdrawal-addresses', 'private', 'GET', {'cost': 1}) + private_get_v3_withdrawal = privateGetV3Withdrawal = Entry('v3/withdrawal', 'private', 'GET', {'cost': 1}) + private_get_v3_withdrawal_fees = privateGetV3WithdrawalFees = Entry('v3/withdrawal-fees', 'private', 'GET', {'cost': 1}) + private_get_v3_orders_status = privateGetV3OrdersStatus = Entry('v3/orders/status', 'private', 'GET', {'cost': 1}) + private_get_v3_orders_working = privateGetV3OrdersWorking = Entry('v3/orders/working', 'private', 'GET', {'cost': 1}) + private_get_v3_trades = privateGetV3Trades = Entry('v3/trades', 'private', 'GET', {'cost': 1}) + private_post_v3_transfer = privatePostV3Transfer = Entry('v3/transfer', 'private', 'POST', {'cost': 1}) + private_post_v3_withdrawal = privatePostV3Withdrawal = Entry('v3/withdrawal', 'private', 'POST', {'cost': 1}) + private_post_v3_orders_place = privatePostV3OrdersPlace = Entry('v3/orders/place', 'private', 'POST', {'cost': 1}) + private_delete_v3_orders_cancel = privateDeleteV3OrdersCancel = Entry('v3/orders/cancel', 'private', 'DELETE', {'cost': 1}) + private_delete_v3_orders_cancel_all = privateDeleteV3OrdersCancelAll = Entry('v3/orders/cancel-all', 'private', 'DELETE', {'cost': 1}) diff --git a/ccxt/abstract/p2b.py b/ccxt/abstract/p2b.py new file mode 100644 index 0000000..c1bb7ef --- /dev/null +++ b/ccxt/abstract/p2b.py @@ -0,0 +1,22 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_markets = publicGetMarkets = Entry('markets', 'public', 'GET', {'cost': 1}) + public_get_market = publicGetMarket = Entry('market', 'public', 'GET', {'cost': 1}) + public_get_tickers = publicGetTickers = Entry('tickers', 'public', 'GET', {'cost': 1}) + public_get_ticker = publicGetTicker = Entry('ticker', 'public', 'GET', {'cost': 1}) + public_get_book = publicGetBook = Entry('book', 'public', 'GET', {'cost': 1}) + public_get_history = publicGetHistory = Entry('history', 'public', 'GET', {'cost': 1}) + public_get_depth_result = publicGetDepthResult = Entry('depth/result', 'public', 'GET', {'cost': 1}) + public_get_market_kline = publicGetMarketKline = Entry('market/kline', 'public', 'GET', {'cost': 1}) + private_post_account_balances = privatePostAccountBalances = Entry('account/balances', 'private', 'POST', {'cost': 1}) + private_post_account_balance = privatePostAccountBalance = Entry('account/balance', 'private', 'POST', {'cost': 1}) + private_post_order_new = privatePostOrderNew = Entry('order/new', 'private', 'POST', {'cost': 1}) + private_post_order_cancel = privatePostOrderCancel = Entry('order/cancel', 'private', 'POST', {'cost': 1}) + private_post_orders = privatePostOrders = Entry('orders', 'private', 'POST', {'cost': 1}) + private_post_account_market_order_history = privatePostAccountMarketOrderHistory = Entry('account/market_order_history', 'private', 'POST', {'cost': 1}) + private_post_account_market_deal_history = privatePostAccountMarketDealHistory = Entry('account/market_deal_history', 'private', 'POST', {'cost': 1}) + private_post_account_order = privatePostAccountOrder = Entry('account/order', 'private', 'POST', {'cost': 1}) + private_post_account_order_history = privatePostAccountOrderHistory = Entry('account/order_history', 'private', 'POST', {'cost': 1}) + private_post_account_executed_history = privatePostAccountExecutedHistory = Entry('account/executed_history', 'private', 'POST', {'cost': 1}) diff --git a/ccxt/abstract/paradex.py b/ccxt/abstract/paradex.py new file mode 100644 index 0000000..a0a2f24 --- /dev/null +++ b/ccxt/abstract/paradex.py @@ -0,0 +1,63 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_bbo_market = publicGetBboMarket = Entry('bbo/{market}', 'public', 'GET', {'cost': 1}) + public_get_funding_data = publicGetFundingData = Entry('funding/data', 'public', 'GET', {'cost': 1}) + public_get_markets = publicGetMarkets = Entry('markets', 'public', 'GET', {'cost': 1}) + public_get_markets_klines = publicGetMarketsKlines = Entry('markets/klines', 'public', 'GET', {'cost': 1}) + public_get_markets_summary = publicGetMarketsSummary = Entry('markets/summary', 'public', 'GET', {'cost': 1}) + public_get_orderbook_market = publicGetOrderbookMarket = Entry('orderbook/{market}', 'public', 'GET', {'cost': 1}) + public_get_insurance = publicGetInsurance = Entry('insurance', 'public', 'GET', {'cost': 1}) + public_get_referrals_config = publicGetReferralsConfig = Entry('referrals/config', 'public', 'GET', {'cost': 1}) + public_get_system_config = publicGetSystemConfig = Entry('system/config', 'public', 'GET', {'cost': 1}) + public_get_system_state = publicGetSystemState = Entry('system/state', 'public', 'GET', {'cost': 1}) + public_get_system_time = publicGetSystemTime = Entry('system/time', 'public', 'GET', {'cost': 1}) + public_get_trades = publicGetTrades = Entry('trades', 'public', 'GET', {'cost': 1}) + public_get_vaults = publicGetVaults = Entry('vaults', 'public', 'GET', {'cost': 1}) + public_get_vaults_balance = publicGetVaultsBalance = Entry('vaults/balance', 'public', 'GET', {'cost': 1}) + public_get_vaults_config = publicGetVaultsConfig = Entry('vaults/config', 'public', 'GET', {'cost': 1}) + public_get_vaults_history = publicGetVaultsHistory = Entry('vaults/history', 'public', 'GET', {'cost': 1}) + public_get_vaults_positions = publicGetVaultsPositions = Entry('vaults/positions', 'public', 'GET', {'cost': 1}) + public_get_vaults_summary = publicGetVaultsSummary = Entry('vaults/summary', 'public', 'GET', {'cost': 1}) + public_get_vaults_transfers = publicGetVaultsTransfers = Entry('vaults/transfers', 'public', 'GET', {'cost': 1}) + private_get_account = privateGetAccount = Entry('account', 'private', 'GET', {'cost': 1}) + private_get_account_info = privateGetAccountInfo = Entry('account/info', 'private', 'GET', {'cost': 1}) + private_get_account_history = privateGetAccountHistory = Entry('account/history', 'private', 'GET', {'cost': 1}) + private_get_account_margin = privateGetAccountMargin = Entry('account/margin', 'private', 'GET', {'cost': 1}) + private_get_account_profile = privateGetAccountProfile = Entry('account/profile', 'private', 'GET', {'cost': 1}) + private_get_account_subaccounts = privateGetAccountSubaccounts = Entry('account/subaccounts', 'private', 'GET', {'cost': 1}) + private_get_balance = privateGetBalance = Entry('balance', 'private', 'GET', {'cost': 1}) + private_get_fills = privateGetFills = Entry('fills', 'private', 'GET', {'cost': 1}) + private_get_funding_payments = privateGetFundingPayments = Entry('funding/payments', 'private', 'GET', {'cost': 1}) + private_get_positions = privateGetPositions = Entry('positions', 'private', 'GET', {'cost': 1}) + private_get_tradebusts = privateGetTradebusts = Entry('tradebusts', 'private', 'GET', {'cost': 1}) + private_get_transactions = privateGetTransactions = Entry('transactions', 'private', 'GET', {'cost': 1}) + private_get_liquidations = privateGetLiquidations = Entry('liquidations', 'private', 'GET', {'cost': 1}) + private_get_orders = privateGetOrders = Entry('orders', 'private', 'GET', {'cost': 1}) + private_get_orders_history = privateGetOrdersHistory = Entry('orders-history', 'private', 'GET', {'cost': 1}) + private_get_orders_by_client_id_client_id = privateGetOrdersByClientIdClientId = Entry('orders/by_client_id/{client_id}', 'private', 'GET', {'cost': 1}) + private_get_orders_order_id = privateGetOrdersOrderId = Entry('orders/{order_id}', 'private', 'GET', {'cost': 1}) + private_get_points_data_market_program = privateGetPointsDataMarketProgram = Entry('points_data/{market}/{program}', 'private', 'GET', {'cost': 1}) + private_get_referrals_qr_code = privateGetReferralsQrCode = Entry('referrals/qr-code', 'private', 'GET', {'cost': 1}) + private_get_referrals_summary = privateGetReferralsSummary = Entry('referrals/summary', 'private', 'GET', {'cost': 1}) + private_get_transfers = privateGetTransfers = Entry('transfers', 'private', 'GET', {'cost': 1}) + private_get_algo_orders = privateGetAlgoOrders = Entry('algo/orders', 'private', 'GET', {'cost': 1}) + private_get_algo_orders_history = privateGetAlgoOrdersHistory = Entry('algo/orders-history', 'private', 'GET', {'cost': 1}) + private_get_algo_orders_algo_id = privateGetAlgoOrdersAlgoId = Entry('algo/orders/{algo_id}', 'private', 'GET', {'cost': 1}) + private_get_vaults_account_summary = privateGetVaultsAccountSummary = Entry('vaults/account-summary', 'private', 'GET', {'cost': 1}) + private_post_account_margin_market = privatePostAccountMarginMarket = Entry('account/margin/{market}', 'private', 'POST', {'cost': 1}) + private_post_account_profile_max_slippage = privatePostAccountProfileMaxSlippage = Entry('account/profile/max_slippage', 'private', 'POST', {'cost': 1}) + private_post_account_profile_referral_code = privatePostAccountProfileReferralCode = Entry('account/profile/referral_code', 'private', 'POST', {'cost': 1}) + private_post_account_profile_username = privatePostAccountProfileUsername = Entry('account/profile/username', 'private', 'POST', {'cost': 1}) + private_post_auth = privatePostAuth = Entry('auth', 'private', 'POST', {'cost': 1}) + private_post_onboarding = privatePostOnboarding = Entry('onboarding', 'private', 'POST', {'cost': 1}) + private_post_orders = privatePostOrders = Entry('orders', 'private', 'POST', {'cost': 1}) + private_post_orders_batch = privatePostOrdersBatch = Entry('orders/batch', 'private', 'POST', {'cost': 1}) + private_post_algo_orders = privatePostAlgoOrders = Entry('algo/orders', 'private', 'POST', {'cost': 1}) + private_post_vaults = privatePostVaults = Entry('vaults', 'private', 'POST', {'cost': 1}) + private_put_orders_order_id = privatePutOrdersOrderId = Entry('orders/{order_id}', 'private', 'PUT', {'cost': 1}) + private_delete_orders = privateDeleteOrders = Entry('orders', 'private', 'DELETE', {'cost': 1}) + private_delete_orders_by_client_id_client_id = privateDeleteOrdersByClientIdClientId = Entry('orders/by_client_id/{client_id}', 'private', 'DELETE', {'cost': 1}) + private_delete_orders_order_id = privateDeleteOrdersOrderId = Entry('orders/{order_id}', 'private', 'DELETE', {'cost': 1}) + private_delete_algo_orders_algo_id = privateDeleteAlgoOrdersAlgoId = Entry('algo/orders/{algo_id}', 'private', 'DELETE', {'cost': 1}) diff --git a/ccxt/abstract/paymium.py b/ccxt/abstract/paymium.py new file mode 100644 index 0000000..5aaae30 --- /dev/null +++ b/ccxt/abstract/paymium.py @@ -0,0 +1,28 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_countries = publicGetCountries = Entry('countries', 'public', 'GET', {}) + public_get_currencies = publicGetCurrencies = Entry('currencies', 'public', 'GET', {}) + public_get_data_currency_ticker = publicGetDataCurrencyTicker = Entry('data/{currency}/ticker', 'public', 'GET', {}) + public_get_data_currency_trades = publicGetDataCurrencyTrades = Entry('data/{currency}/trades', 'public', 'GET', {}) + public_get_data_currency_depth = publicGetDataCurrencyDepth = Entry('data/{currency}/depth', 'public', 'GET', {}) + public_get_bitcoin_charts_id_trades = publicGetBitcoinChartsIdTrades = Entry('bitcoin_charts/{id}/trades', 'public', 'GET', {}) + public_get_bitcoin_charts_id_depth = publicGetBitcoinChartsIdDepth = Entry('bitcoin_charts/{id}/depth', 'public', 'GET', {}) + private_get_user = privateGetUser = Entry('user', 'private', 'GET', {}) + private_get_user_addresses = privateGetUserAddresses = Entry('user/addresses', 'private', 'GET', {}) + private_get_user_addresses_address = privateGetUserAddressesAddress = Entry('user/addresses/{address}', 'private', 'GET', {}) + private_get_user_orders = privateGetUserOrders = Entry('user/orders', 'private', 'GET', {}) + private_get_user_orders_uuid = privateGetUserOrdersUuid = Entry('user/orders/{uuid}', 'private', 'GET', {}) + private_get_user_price_alerts = privateGetUserPriceAlerts = Entry('user/price_alerts', 'private', 'GET', {}) + private_get_merchant_get_payment_uuid = privateGetMerchantGetPaymentUuid = Entry('merchant/get_payment/{uuid}', 'private', 'GET', {}) + private_post_user_addresses = privatePostUserAddresses = Entry('user/addresses', 'private', 'POST', {}) + private_post_user_orders = privatePostUserOrders = Entry('user/orders', 'private', 'POST', {}) + private_post_user_withdrawals = privatePostUserWithdrawals = Entry('user/withdrawals', 'private', 'POST', {}) + private_post_user_email_transfers = privatePostUserEmailTransfers = Entry('user/email_transfers', 'private', 'POST', {}) + private_post_user_payment_requests = privatePostUserPaymentRequests = Entry('user/payment_requests', 'private', 'POST', {}) + private_post_user_price_alerts = privatePostUserPriceAlerts = Entry('user/price_alerts', 'private', 'POST', {}) + private_post_merchant_create_payment = privatePostMerchantCreatePayment = Entry('merchant/create_payment', 'private', 'POST', {}) + private_delete_user_orders_uuid = privateDeleteUserOrdersUuid = Entry('user/orders/{uuid}', 'private', 'DELETE', {}) + private_delete_user_orders_uuid_cancel = privateDeleteUserOrdersUuidCancel = Entry('user/orders/{uuid}/cancel', 'private', 'DELETE', {}) + private_delete_user_price_alerts_id = privateDeleteUserPriceAlertsId = Entry('user/price_alerts/{id}', 'private', 'DELETE', {}) diff --git a/ccxt/abstract/phemex.py b/ccxt/abstract/phemex.py new file mode 100644 index 0000000..a98a308 --- /dev/null +++ b/ccxt/abstract/phemex.py @@ -0,0 +1,118 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_cfg_v2_products = publicGetCfgV2Products = Entry('cfg/v2/products', 'public', 'GET', {'cost': 5}) + public_get_cfg_fundingrates = publicGetCfgFundingRates = Entry('cfg/fundingRates', 'public', 'GET', {'cost': 5}) + public_get_products = publicGetProducts = Entry('products', 'public', 'GET', {'cost': 5}) + public_get_nomics_trades = publicGetNomicsTrades = Entry('nomics/trades', 'public', 'GET', {'cost': 5}) + public_get_md_kline = publicGetMdKline = Entry('md/kline', 'public', 'GET', {'cost': 5}) + public_get_md_v2_kline_list = publicGetMdV2KlineList = Entry('md/v2/kline/list', 'public', 'GET', {'cost': 5}) + public_get_md_v2_kline = publicGetMdV2Kline = Entry('md/v2/kline', 'public', 'GET', {'cost': 5}) + public_get_md_v2_kline_last = publicGetMdV2KlineLast = Entry('md/v2/kline/last', 'public', 'GET', {'cost': 5}) + public_get_md_orderbook = publicGetMdOrderbook = Entry('md/orderbook', 'public', 'GET', {'cost': 5}) + public_get_md_trade = publicGetMdTrade = Entry('md/trade', 'public', 'GET', {'cost': 5}) + public_get_md_spot_ticker_24hr = publicGetMdSpotTicker24hr = Entry('md/spot/ticker/24hr', 'public', 'GET', {'cost': 5}) + public_get_exchange_public_cfg_chain_settings = publicGetExchangePublicCfgChainSettings = Entry('exchange/public/cfg/chain-settings', 'public', 'GET', {'cost': 5}) + v1_get_md_fullbook = v1GetMdFullbook = Entry('md/fullbook', 'v1', 'GET', {'cost': 5}) + v1_get_md_orderbook = v1GetMdOrderbook = Entry('md/orderbook', 'v1', 'GET', {'cost': 5}) + v1_get_md_trade = v1GetMdTrade = Entry('md/trade', 'v1', 'GET', {'cost': 5}) + v1_get_md_ticker_24hr = v1GetMdTicker24hr = Entry('md/ticker/24hr', 'v1', 'GET', {'cost': 5}) + v1_get_md_ticker_24hr_all = v1GetMdTicker24hrAll = Entry('md/ticker/24hr/all', 'v1', 'GET', {'cost': 5}) + v1_get_md_spot_ticker_24hr = v1GetMdSpotTicker24hr = Entry('md/spot/ticker/24hr', 'v1', 'GET', {'cost': 5}) + v1_get_md_spot_ticker_24hr_all = v1GetMdSpotTicker24hrAll = Entry('md/spot/ticker/24hr/all', 'v1', 'GET', {'cost': 5}) + v1_get_exchange_public_products = v1GetExchangePublicProducts = Entry('exchange/public/products', 'v1', 'GET', {'cost': 5}) + v1_get_api_data_public_data_funding_rate_history = v1GetApiDataPublicDataFundingRateHistory = Entry('api-data/public/data/funding-rate-history', 'v1', 'GET', {'cost': 5}) + v2_get_public_products = v2GetPublicProducts = Entry('public/products', 'v2', 'GET', {'cost': 5}) + v2_get_public_products_plus = v2GetPublicProductsPlus = Entry('public/products-plus', 'v2', 'GET', {'cost': 5}) + v2_get_md_v2_orderbook = v2GetMdV2Orderbook = Entry('md/v2/orderbook', 'v2', 'GET', {'cost': 5}) + v2_get_md_v2_trade = v2GetMdV2Trade = Entry('md/v2/trade', 'v2', 'GET', {'cost': 5}) + v2_get_md_v2_ticker_24hr = v2GetMdV2Ticker24hr = Entry('md/v2/ticker/24hr', 'v2', 'GET', {'cost': 5}) + v2_get_md_v2_ticker_24hr_all = v2GetMdV2Ticker24hrAll = Entry('md/v2/ticker/24hr/all', 'v2', 'GET', {'cost': 5}) + v2_get_api_data_public_data_funding_rate_history = v2GetApiDataPublicDataFundingRateHistory = Entry('api-data/public/data/funding-rate-history', 'v2', 'GET', {'cost': 5}) + private_get_spot_orders_active = privateGetSpotOrdersActive = Entry('spot/orders/active', 'private', 'GET', {'cost': 1}) + private_get_spot_orders = privateGetSpotOrders = Entry('spot/orders', 'private', 'GET', {'cost': 1}) + private_get_spot_wallets = privateGetSpotWallets = Entry('spot/wallets', 'private', 'GET', {'cost': 5}) + private_get_exchange_spot_order = privateGetExchangeSpotOrder = Entry('exchange/spot/order', 'private', 'GET', {'cost': 5}) + private_get_exchange_spot_order_trades = privateGetExchangeSpotOrderTrades = Entry('exchange/spot/order/trades', 'private', 'GET', {'cost': 5}) + private_get_exchange_order_v2_orderlist = privateGetExchangeOrderV2OrderList = Entry('exchange/order/v2/orderList', 'private', 'GET', {'cost': 5}) + private_get_exchange_order_v2_tradinglist = privateGetExchangeOrderV2TradingList = Entry('exchange/order/v2/tradingList', 'private', 'GET', {'cost': 5}) + private_get_accounts_accountpositions = privateGetAccountsAccountPositions = Entry('accounts/accountPositions', 'private', 'GET', {'cost': 1}) + private_get_g_accounts_accountpositions = privateGetGAccountsAccountPositions = Entry('g-accounts/accountPositions', 'private', 'GET', {'cost': 1}) + private_get_g_accounts_positions = privateGetGAccountsPositions = Entry('g-accounts/positions', 'private', 'GET', {'cost': 25}) + private_get_g_accounts_risk_unit = privateGetGAccountsRiskUnit = Entry('g-accounts/risk-unit', 'private', 'GET', {'cost': 1}) + private_get_api_data_futures_funding_fees = privateGetApiDataFuturesFundingFees = Entry('api-data/futures/funding-fees', 'private', 'GET', {'cost': 5}) + private_get_api_data_g_futures_funding_fees = privateGetApiDataGFuturesFundingFees = Entry('api-data/g-futures/funding-fees', 'private', 'GET', {'cost': 5}) + private_get_api_data_futures_orders = privateGetApiDataFuturesOrders = Entry('api-data/futures/orders', 'private', 'GET', {'cost': 5}) + private_get_api_data_g_futures_orders = privateGetApiDataGFuturesOrders = Entry('api-data/g-futures/orders', 'private', 'GET', {'cost': 5}) + private_get_api_data_futures_orders_by_order_id = privateGetApiDataFuturesOrdersByOrderId = Entry('api-data/futures/orders/by-order-id', 'private', 'GET', {'cost': 5}) + private_get_api_data_g_futures_orders_by_order_id = privateGetApiDataGFuturesOrdersByOrderId = Entry('api-data/g-futures/orders/by-order-id', 'private', 'GET', {'cost': 5}) + private_get_api_data_futures_trades = privateGetApiDataFuturesTrades = Entry('api-data/futures/trades', 'private', 'GET', {'cost': 5}) + private_get_api_data_g_futures_trades = privateGetApiDataGFuturesTrades = Entry('api-data/g-futures/trades', 'private', 'GET', {'cost': 5}) + private_get_api_data_futures_trading_fees = privateGetApiDataFuturesTradingFees = Entry('api-data/futures/trading-fees', 'private', 'GET', {'cost': 5}) + private_get_api_data_g_futures_trading_fees = privateGetApiDataGFuturesTradingFees = Entry('api-data/g-futures/trading-fees', 'private', 'GET', {'cost': 5}) + private_get_api_data_futures_v2_tradeaccountdetail = privateGetApiDataFuturesV2TradeAccountDetail = Entry('api-data/futures/v2/tradeAccountDetail', 'private', 'GET', {'cost': 5}) + private_get_g_orders_activelist = privateGetGOrdersActiveList = Entry('g-orders/activeList', 'private', 'GET', {'cost': 1}) + private_get_orders_activelist = privateGetOrdersActiveList = Entry('orders/activeList', 'private', 'GET', {'cost': 1}) + private_get_exchange_order_list = privateGetExchangeOrderList = Entry('exchange/order/list', 'private', 'GET', {'cost': 5}) + private_get_exchange_order = privateGetExchangeOrder = Entry('exchange/order', 'private', 'GET', {'cost': 5}) + private_get_exchange_order_trade = privateGetExchangeOrderTrade = Entry('exchange/order/trade', 'private', 'GET', {'cost': 5}) + private_get_phemex_user_users_children = privateGetPhemexUserUsersChildren = Entry('phemex-user/users/children', 'private', 'GET', {'cost': 5}) + private_get_phemex_user_wallets_v2_depositaddress = privateGetPhemexUserWalletsV2DepositAddress = Entry('phemex-user/wallets/v2/depositAddress', 'private', 'GET', {'cost': 5}) + private_get_phemex_user_wallets_tradeaccountdetail = privateGetPhemexUserWalletsTradeAccountDetail = Entry('phemex-user/wallets/tradeAccountDetail', 'private', 'GET', {'cost': 5}) + private_get_phemex_deposit_wallets_api_depositaddress = privateGetPhemexDepositWalletsApiDepositAddress = Entry('phemex-deposit/wallets/api/depositAddress', 'private', 'GET', {'cost': 5}) + private_get_phemex_deposit_wallets_api_deposithist = privateGetPhemexDepositWalletsApiDepositHist = Entry('phemex-deposit/wallets/api/depositHist', 'private', 'GET', {'cost': 5}) + private_get_phemex_deposit_wallets_api_chaincfg = privateGetPhemexDepositWalletsApiChainCfg = Entry('phemex-deposit/wallets/api/chainCfg', 'private', 'GET', {'cost': 5}) + private_get_phemex_withdraw_wallets_api_withdrawhist = privateGetPhemexWithdrawWalletsApiWithdrawHist = Entry('phemex-withdraw/wallets/api/withdrawHist', 'private', 'GET', {'cost': 5}) + private_get_phemex_withdraw_wallets_api_asset_info = privateGetPhemexWithdrawWalletsApiAssetInfo = Entry('phemex-withdraw/wallets/api/asset/info', 'private', 'GET', {'cost': 5}) + private_get_phemex_user_order_closedpositionlist = privateGetPhemexUserOrderClosedPositionList = Entry('phemex-user/order/closedPositionList', 'private', 'GET', {'cost': 5}) + private_get_exchange_margins_transfer = privateGetExchangeMarginsTransfer = Entry('exchange/margins/transfer', 'private', 'GET', {'cost': 5}) + private_get_exchange_wallets_confirm_withdraw = privateGetExchangeWalletsConfirmWithdraw = Entry('exchange/wallets/confirm/withdraw', 'private', 'GET', {'cost': 5}) + private_get_exchange_wallets_withdrawlist = privateGetExchangeWalletsWithdrawList = Entry('exchange/wallets/withdrawList', 'private', 'GET', {'cost': 5}) + private_get_exchange_wallets_depositlist = privateGetExchangeWalletsDepositList = Entry('exchange/wallets/depositList', 'private', 'GET', {'cost': 5}) + private_get_exchange_wallets_v2_depositaddress = privateGetExchangeWalletsV2DepositAddress = Entry('exchange/wallets/v2/depositAddress', 'private', 'GET', {'cost': 5}) + private_get_api_data_spots_funds = privateGetApiDataSpotsFunds = Entry('api-data/spots/funds', 'private', 'GET', {'cost': 5}) + private_get_api_data_spots_orders = privateGetApiDataSpotsOrders = Entry('api-data/spots/orders', 'private', 'GET', {'cost': 5}) + private_get_api_data_spots_orders_by_order_id = privateGetApiDataSpotsOrdersByOrderId = Entry('api-data/spots/orders/by-order-id', 'private', 'GET', {'cost': 5}) + private_get_api_data_spots_pnls = privateGetApiDataSpotsPnls = Entry('api-data/spots/pnls', 'private', 'GET', {'cost': 5}) + private_get_api_data_spots_trades = privateGetApiDataSpotsTrades = Entry('api-data/spots/trades', 'private', 'GET', {'cost': 5}) + private_get_api_data_spots_trades_by_order_id = privateGetApiDataSpotsTradesByOrderId = Entry('api-data/spots/trades/by-order-id', 'private', 'GET', {'cost': 5}) + private_get_assets_convert = privateGetAssetsConvert = Entry('assets/convert', 'private', 'GET', {'cost': 5}) + private_get_assets_transfer = privateGetAssetsTransfer = Entry('assets/transfer', 'private', 'GET', {'cost': 5}) + private_get_assets_spots_sub_accounts_transfer = privateGetAssetsSpotsSubAccountsTransfer = Entry('assets/spots/sub-accounts/transfer', 'private', 'GET', {'cost': 5}) + private_get_assets_futures_sub_accounts_transfer = privateGetAssetsFuturesSubAccountsTransfer = Entry('assets/futures/sub-accounts/transfer', 'private', 'GET', {'cost': 5}) + private_get_assets_quote = privateGetAssetsQuote = Entry('assets/quote', 'private', 'GET', {'cost': 5}) + private_post_spot_orders = privatePostSpotOrders = Entry('spot/orders', 'private', 'POST', {'cost': 1}) + private_post_orders = privatePostOrders = Entry('orders', 'private', 'POST', {'cost': 1}) + private_post_g_orders = privatePostGOrders = Entry('g-orders', 'private', 'POST', {'cost': 1}) + private_post_positions_assign = privatePostPositionsAssign = Entry('positions/assign', 'private', 'POST', {'cost': 5}) + private_post_exchange_wallets_transferout = privatePostExchangeWalletsTransferOut = Entry('exchange/wallets/transferOut', 'private', 'POST', {'cost': 5}) + private_post_exchange_wallets_transferin = privatePostExchangeWalletsTransferIn = Entry('exchange/wallets/transferIn', 'private', 'POST', {'cost': 5}) + private_post_exchange_margins = privatePostExchangeMargins = Entry('exchange/margins', 'private', 'POST', {'cost': 5}) + private_post_exchange_wallets_createwithdraw = privatePostExchangeWalletsCreateWithdraw = Entry('exchange/wallets/createWithdraw', 'private', 'POST', {'cost': 5}) + private_post_exchange_wallets_cancelwithdraw = privatePostExchangeWalletsCancelWithdraw = Entry('exchange/wallets/cancelWithdraw', 'private', 'POST', {'cost': 5}) + private_post_exchange_wallets_createwithdrawaddress = privatePostExchangeWalletsCreateWithdrawAddress = Entry('exchange/wallets/createWithdrawAddress', 'private', 'POST', {'cost': 5}) + private_post_assets_transfer = privatePostAssetsTransfer = Entry('assets/transfer', 'private', 'POST', {'cost': 5}) + private_post_assets_spots_sub_accounts_transfer = privatePostAssetsSpotsSubAccountsTransfer = Entry('assets/spots/sub-accounts/transfer', 'private', 'POST', {'cost': 5}) + private_post_assets_futures_sub_accounts_transfer = privatePostAssetsFuturesSubAccountsTransfer = Entry('assets/futures/sub-accounts/transfer', 'private', 'POST', {'cost': 5}) + private_post_assets_universal_transfer = privatePostAssetsUniversalTransfer = Entry('assets/universal-transfer', 'private', 'POST', {'cost': 5}) + private_post_assets_convert = privatePostAssetsConvert = Entry('assets/convert', 'private', 'POST', {'cost': 5}) + private_post_phemex_withdraw_wallets_api_createwithdraw = privatePostPhemexWithdrawWalletsApiCreateWithdraw = Entry('phemex-withdraw/wallets/api/createWithdraw', 'private', 'POST', {'cost': 5}) + private_post_phemex_withdraw_wallets_api_cancelwithdraw = privatePostPhemexWithdrawWalletsApiCancelWithdraw = Entry('phemex-withdraw/wallets/api/cancelWithdraw', 'private', 'POST', {'cost': 5}) + private_put_spot_orders_create = privatePutSpotOrdersCreate = Entry('spot/orders/create', 'private', 'PUT', {'cost': 1}) + private_put_spot_orders = privatePutSpotOrders = Entry('spot/orders', 'private', 'PUT', {'cost': 1}) + private_put_orders_replace = privatePutOrdersReplace = Entry('orders/replace', 'private', 'PUT', {'cost': 1}) + private_put_g_orders_replace = privatePutGOrdersReplace = Entry('g-orders/replace', 'private', 'PUT', {'cost': 1}) + private_put_g_orders_create = privatePutGOrdersCreate = Entry('g-orders/create', 'private', 'PUT', {'cost': 1}) + private_put_positions_leverage = privatePutPositionsLeverage = Entry('positions/leverage', 'private', 'PUT', {'cost': 5}) + private_put_g_positions_leverage = privatePutGPositionsLeverage = Entry('g-positions/leverage', 'private', 'PUT', {'cost': 5}) + private_put_g_positions_switch_pos_mode_sync = privatePutGPositionsSwitchPosModeSync = Entry('g-positions/switch-pos-mode-sync', 'private', 'PUT', {'cost': 5}) + private_put_positions_risklimit = privatePutPositionsRiskLimit = Entry('positions/riskLimit', 'private', 'PUT', {'cost': 5}) + private_delete_spot_orders = privateDeleteSpotOrders = Entry('spot/orders', 'private', 'DELETE', {'cost': 2}) + private_delete_spot_orders_all = privateDeleteSpotOrdersAll = Entry('spot/orders/all', 'private', 'DELETE', {'cost': 2}) + private_delete_orders_cancel = privateDeleteOrdersCancel = Entry('orders/cancel', 'private', 'DELETE', {'cost': 1}) + private_delete_orders = privateDeleteOrders = Entry('orders', 'private', 'DELETE', {'cost': 1}) + private_delete_orders_all = privateDeleteOrdersAll = Entry('orders/all', 'private', 'DELETE', {'cost': 3}) + private_delete_g_orders_cancel = privateDeleteGOrdersCancel = Entry('g-orders/cancel', 'private', 'DELETE', {'cost': 1}) + private_delete_g_orders = privateDeleteGOrders = Entry('g-orders', 'private', 'DELETE', {'cost': 1}) + private_delete_g_orders_all = privateDeleteGOrdersAll = Entry('g-orders/all', 'private', 'DELETE', {'cost': 3}) diff --git a/ccxt/abstract/poloniex.py b/ccxt/abstract/poloniex.py new file mode 100644 index 0000000..4934dae --- /dev/null +++ b/ccxt/abstract/poloniex.py @@ -0,0 +1,105 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_markets = publicGetMarkets = Entry('markets', 'public', 'GET', {'cost': 20}) + public_get_markets_symbol = publicGetMarketsSymbol = Entry('markets/{symbol}', 'public', 'GET', {'cost': 1}) + public_get_currencies = publicGetCurrencies = Entry('currencies', 'public', 'GET', {'cost': 20}) + public_get_currencies_currency = publicGetCurrenciesCurrency = Entry('currencies/{currency}', 'public', 'GET', {'cost': 20}) + public_get_v2_currencies = publicGetV2Currencies = Entry('v2/currencies', 'public', 'GET', {'cost': 20}) + public_get_v2_currencies_currency = publicGetV2CurrenciesCurrency = Entry('v2/currencies/{currency}', 'public', 'GET', {'cost': 20}) + public_get_timestamp = publicGetTimestamp = Entry('timestamp', 'public', 'GET', {'cost': 1}) + public_get_markets_price = publicGetMarketsPrice = Entry('markets/price', 'public', 'GET', {'cost': 1}) + public_get_markets_symbol_price = publicGetMarketsSymbolPrice = Entry('markets/{symbol}/price', 'public', 'GET', {'cost': 1}) + public_get_markets_markprice = publicGetMarketsMarkPrice = Entry('markets/markPrice', 'public', 'GET', {'cost': 1}) + public_get_markets_symbol_markprice = publicGetMarketsSymbolMarkPrice = Entry('markets/{symbol}/markPrice', 'public', 'GET', {'cost': 1}) + public_get_markets_symbol_markpricecomponents = publicGetMarketsSymbolMarkPriceComponents = Entry('markets/{symbol}/markPriceComponents', 'public', 'GET', {'cost': 1}) + public_get_markets_symbol_orderbook = publicGetMarketsSymbolOrderBook = Entry('markets/{symbol}/orderBook', 'public', 'GET', {'cost': 1}) + public_get_markets_symbol_candles = publicGetMarketsSymbolCandles = Entry('markets/{symbol}/candles', 'public', 'GET', {'cost': 1}) + public_get_markets_symbol_trades = publicGetMarketsSymbolTrades = Entry('markets/{symbol}/trades', 'public', 'GET', {'cost': 20}) + public_get_markets_ticker24h = publicGetMarketsTicker24h = Entry('markets/ticker24h', 'public', 'GET', {'cost': 20}) + public_get_markets_symbol_ticker24h = publicGetMarketsSymbolTicker24h = Entry('markets/{symbol}/ticker24h', 'public', 'GET', {'cost': 20}) + public_get_markets_collateralinfo = publicGetMarketsCollateralInfo = Entry('markets/collateralInfo', 'public', 'GET', {'cost': 1}) + public_get_markets_currency_collateralinfo = publicGetMarketsCurrencyCollateralInfo = Entry('markets/{currency}/collateralInfo', 'public', 'GET', {'cost': 1}) + public_get_markets_borrowratesinfo = publicGetMarketsBorrowRatesInfo = Entry('markets/borrowRatesInfo', 'public', 'GET', {'cost': 1}) + private_get_accounts = privateGetAccounts = Entry('accounts', 'private', 'GET', {'cost': 4}) + private_get_accounts_balances = privateGetAccountsBalances = Entry('accounts/balances', 'private', 'GET', {'cost': 4}) + private_get_accounts_id_balances = privateGetAccountsIdBalances = Entry('accounts/{id}/balances', 'private', 'GET', {'cost': 4}) + private_get_accounts_activity = privateGetAccountsActivity = Entry('accounts/activity', 'private', 'GET', {'cost': 20}) + private_get_accounts_transfer = privateGetAccountsTransfer = Entry('accounts/transfer', 'private', 'GET', {'cost': 20}) + private_get_accounts_transfer_id = privateGetAccountsTransferId = Entry('accounts/transfer/{id}', 'private', 'GET', {'cost': 4}) + private_get_feeinfo = privateGetFeeinfo = Entry('feeinfo', 'private', 'GET', {'cost': 20}) + private_get_accounts_interest_history = privateGetAccountsInterestHistory = Entry('accounts/interest/history', 'private', 'GET', {'cost': 1}) + private_get_subaccounts = privateGetSubaccounts = Entry('subaccounts', 'private', 'GET', {'cost': 4}) + private_get_subaccounts_balances = privateGetSubaccountsBalances = Entry('subaccounts/balances', 'private', 'GET', {'cost': 20}) + private_get_subaccounts_id_balances = privateGetSubaccountsIdBalances = Entry('subaccounts/{id}/balances', 'private', 'GET', {'cost': 4}) + private_get_subaccounts_transfer = privateGetSubaccountsTransfer = Entry('subaccounts/transfer', 'private', 'GET', {'cost': 20}) + private_get_subaccounts_transfer_id = privateGetSubaccountsTransferId = Entry('subaccounts/transfer/{id}', 'private', 'GET', {'cost': 4}) + private_get_wallets_addresses = privateGetWalletsAddresses = Entry('wallets/addresses', 'private', 'GET', {'cost': 20}) + private_get_wallets_addresses_currency = privateGetWalletsAddressesCurrency = Entry('wallets/addresses/{currency}', 'private', 'GET', {'cost': 20}) + private_get_wallets_activity = privateGetWalletsActivity = Entry('wallets/activity', 'private', 'GET', {'cost': 20}) + private_get_margin_accountmargin = privateGetMarginAccountMargin = Entry('margin/accountMargin', 'private', 'GET', {'cost': 4}) + private_get_margin_borrowstatus = privateGetMarginBorrowStatus = Entry('margin/borrowStatus', 'private', 'GET', {'cost': 4}) + private_get_margin_maxsize = privateGetMarginMaxSize = Entry('margin/maxSize', 'private', 'GET', {'cost': 4}) + private_get_orders = privateGetOrders = Entry('orders', 'private', 'GET', {'cost': 20}) + private_get_orders_id = privateGetOrdersId = Entry('orders/{id}', 'private', 'GET', {'cost': 4}) + private_get_orders_killswitchstatus = privateGetOrdersKillSwitchStatus = Entry('orders/killSwitchStatus', 'private', 'GET', {'cost': 4}) + private_get_smartorders = privateGetSmartorders = Entry('smartorders', 'private', 'GET', {'cost': 20}) + private_get_smartorders_id = privateGetSmartordersId = Entry('smartorders/{id}', 'private', 'GET', {'cost': 4}) + private_get_orders_history = privateGetOrdersHistory = Entry('orders/history', 'private', 'GET', {'cost': 20}) + private_get_smartorders_history = privateGetSmartordersHistory = Entry('smartorders/history', 'private', 'GET', {'cost': 20}) + private_get_trades = privateGetTrades = Entry('trades', 'private', 'GET', {'cost': 20}) + private_get_orders_id_trades = privateGetOrdersIdTrades = Entry('orders/{id}/trades', 'private', 'GET', {'cost': 4}) + private_post_accounts_transfer = privatePostAccountsTransfer = Entry('accounts/transfer', 'private', 'POST', {'cost': 4}) + private_post_subaccounts_transfer = privatePostSubaccountsTransfer = Entry('subaccounts/transfer', 'private', 'POST', {'cost': 20}) + private_post_wallets_address = privatePostWalletsAddress = Entry('wallets/address', 'private', 'POST', {'cost': 20}) + private_post_wallets_withdraw = privatePostWalletsWithdraw = Entry('wallets/withdraw', 'private', 'POST', {'cost': 20}) + private_post_v2_wallets_withdraw = privatePostV2WalletsWithdraw = Entry('v2/wallets/withdraw', 'private', 'POST', {'cost': 20}) + private_post_orders = privatePostOrders = Entry('orders', 'private', 'POST', {'cost': 4}) + private_post_orders_batch = privatePostOrdersBatch = Entry('orders/batch', 'private', 'POST', {'cost': 20}) + private_post_orders_killswitch = privatePostOrdersKillSwitch = Entry('orders/killSwitch', 'private', 'POST', {'cost': 4}) + private_post_smartorders = privatePostSmartorders = Entry('smartorders', 'private', 'POST', {'cost': 4}) + private_delete_orders_id = privateDeleteOrdersId = Entry('orders/{id}', 'private', 'DELETE', {'cost': 4}) + private_delete_orders_cancelbyids = privateDeleteOrdersCancelByIds = Entry('orders/cancelByIds', 'private', 'DELETE', {'cost': 20}) + private_delete_orders = privateDeleteOrders = Entry('orders', 'private', 'DELETE', {'cost': 20}) + private_delete_smartorders_id = privateDeleteSmartordersId = Entry('smartorders/{id}', 'private', 'DELETE', {'cost': 4}) + private_delete_smartorders_cancelbyids = privateDeleteSmartordersCancelByIds = Entry('smartorders/cancelByIds', 'private', 'DELETE', {'cost': 20}) + private_delete_smartorders = privateDeleteSmartorders = Entry('smartorders', 'private', 'DELETE', {'cost': 20}) + private_put_orders_id = privatePutOrdersId = Entry('orders/{id}', 'private', 'PUT', {'cost': 20}) + private_put_smartorders_id = privatePutSmartordersId = Entry('smartorders/{id}', 'private', 'PUT', {'cost': 20}) + swappublic_get_v3_market_allinstruments = swapPublicGetV3MarketAllInstruments = Entry('v3/market/allInstruments', 'swapPublic', 'GET', {'cost': 0.6666666666666666}) + swappublic_get_v3_market_instruments = swapPublicGetV3MarketInstruments = Entry('v3/market/instruments', 'swapPublic', 'GET', {'cost': 0.6666666666666666}) + swappublic_get_v3_market_orderbook = swapPublicGetV3MarketOrderBook = Entry('v3/market/orderBook', 'swapPublic', 'GET', {'cost': 0.6666666666666666}) + swappublic_get_v3_market_candles = swapPublicGetV3MarketCandles = Entry('v3/market/candles', 'swapPublic', 'GET', {'cost': 10}) + swappublic_get_v3_market_indexpricecandlesticks = swapPublicGetV3MarketIndexPriceCandlesticks = Entry('v3/market/indexPriceCandlesticks', 'swapPublic', 'GET', {'cost': 10}) + swappublic_get_v3_market_premiumindexcandlesticks = swapPublicGetV3MarketPremiumIndexCandlesticks = Entry('v3/market/premiumIndexCandlesticks', 'swapPublic', 'GET', {'cost': 10}) + swappublic_get_v3_market_markpricecandlesticks = swapPublicGetV3MarketMarkPriceCandlesticks = Entry('v3/market/markPriceCandlesticks', 'swapPublic', 'GET', {'cost': 10}) + swappublic_get_v3_market_trades = swapPublicGetV3MarketTrades = Entry('v3/market/trades', 'swapPublic', 'GET', {'cost': 0.6666666666666666}) + swappublic_get_v3_market_liquidationorder = swapPublicGetV3MarketLiquidationOrder = Entry('v3/market/liquidationOrder', 'swapPublic', 'GET', {'cost': 0.6666666666666666}) + swappublic_get_v3_market_tickers = swapPublicGetV3MarketTickers = Entry('v3/market/tickers', 'swapPublic', 'GET', {'cost': 0.6666666666666666}) + swappublic_get_v3_market_markprice = swapPublicGetV3MarketMarkPrice = Entry('v3/market/markPrice', 'swapPublic', 'GET', {'cost': 0.6666666666666666}) + swappublic_get_v3_market_indexprice = swapPublicGetV3MarketIndexPrice = Entry('v3/market/indexPrice', 'swapPublic', 'GET', {'cost': 0.6666666666666666}) + swappublic_get_v3_market_indexpricecomponents = swapPublicGetV3MarketIndexPriceComponents = Entry('v3/market/indexPriceComponents', 'swapPublic', 'GET', {'cost': 0.6666666666666666}) + swappublic_get_v3_market_fundingrate = swapPublicGetV3MarketFundingRate = Entry('v3/market/fundingRate', 'swapPublic', 'GET', {'cost': 0.6666666666666666}) + swappublic_get_v3_market_openinterest = swapPublicGetV3MarketOpenInterest = Entry('v3/market/openInterest', 'swapPublic', 'GET', {'cost': 0.6666666666666666}) + swappublic_get_v3_market_insurance = swapPublicGetV3MarketInsurance = Entry('v3/market/insurance', 'swapPublic', 'GET', {'cost': 0.6666666666666666}) + swappublic_get_v3_market_risklimit = swapPublicGetV3MarketRiskLimit = Entry('v3/market/riskLimit', 'swapPublic', 'GET', {'cost': 0.6666666666666666}) + swapprivate_get_v3_account_balance = swapPrivateGetV3AccountBalance = Entry('v3/account/balance', 'swapPrivate', 'GET', {'cost': 4}) + swapprivate_get_v3_account_bills = swapPrivateGetV3AccountBills = Entry('v3/account/bills', 'swapPrivate', 'GET', {'cost': 20}) + swapprivate_get_v3_trade_order_opens = swapPrivateGetV3TradeOrderOpens = Entry('v3/trade/order/opens', 'swapPrivate', 'GET', {'cost': 20}) + swapprivate_get_v3_trade_order_trades = swapPrivateGetV3TradeOrderTrades = Entry('v3/trade/order/trades', 'swapPrivate', 'GET', {'cost': 20}) + swapprivate_get_v3_trade_order_history = swapPrivateGetV3TradeOrderHistory = Entry('v3/trade/order/history', 'swapPrivate', 'GET', {'cost': 20}) + swapprivate_get_v3_trade_position_opens = swapPrivateGetV3TradePositionOpens = Entry('v3/trade/position/opens', 'swapPrivate', 'GET', {'cost': 20}) + swapprivate_get_v3_trade_position_history = swapPrivateGetV3TradePositionHistory = Entry('v3/trade/position/history', 'swapPrivate', 'GET', {'cost': 20}) + swapprivate_get_v3_position_leverages = swapPrivateGetV3PositionLeverages = Entry('v3/position/leverages', 'swapPrivate', 'GET', {'cost': 20}) + swapprivate_get_v3_position_mode = swapPrivateGetV3PositionMode = Entry('v3/position/mode', 'swapPrivate', 'GET', {'cost': 20}) + swapprivate_post_v3_trade_order = swapPrivatePostV3TradeOrder = Entry('v3/trade/order', 'swapPrivate', 'POST', {'cost': 4}) + swapprivate_post_v3_trade_orders = swapPrivatePostV3TradeOrders = Entry('v3/trade/orders', 'swapPrivate', 'POST', {'cost': 40}) + swapprivate_post_v3_trade_position = swapPrivatePostV3TradePosition = Entry('v3/trade/position', 'swapPrivate', 'POST', {'cost': 20}) + swapprivate_post_v3_trade_positionall = swapPrivatePostV3TradePositionAll = Entry('v3/trade/positionAll', 'swapPrivate', 'POST', {'cost': 100}) + swapprivate_post_v3_position_leverage = swapPrivatePostV3PositionLeverage = Entry('v3/position/leverage', 'swapPrivate', 'POST', {'cost': 20}) + swapprivate_post_v3_position_mode = swapPrivatePostV3PositionMode = Entry('v3/position/mode', 'swapPrivate', 'POST', {'cost': 20}) + swapprivate_post_v3_trade_position_margin = swapPrivatePostV3TradePositionMargin = Entry('v3/trade/position/margin', 'swapPrivate', 'POST', {'cost': 20}) + swapprivate_delete_v3_trade_order = swapPrivateDeleteV3TradeOrder = Entry('v3/trade/order', 'swapPrivate', 'DELETE', {'cost': 2}) + swapprivate_delete_v3_trade_batchorders = swapPrivateDeleteV3TradeBatchOrders = Entry('v3/trade/batchOrders', 'swapPrivate', 'DELETE', {'cost': 20}) + swapprivate_delete_v3_trade_allorders = swapPrivateDeleteV3TradeAllOrders = Entry('v3/trade/allOrders', 'swapPrivate', 'DELETE', {'cost': 20}) diff --git a/ccxt/abstract/probit.py b/ccxt/abstract/probit.py new file mode 100644 index 0000000..97430f0 --- /dev/null +++ b/ccxt/abstract/probit.py @@ -0,0 +1,23 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_market = publicGetMarket = Entry('market', 'public', 'GET', {'cost': 1}) + public_get_currency = publicGetCurrency = Entry('currency', 'public', 'GET', {'cost': 1}) + public_get_currency_with_platform = publicGetCurrencyWithPlatform = Entry('currency_with_platform', 'public', 'GET', {'cost': 1}) + public_get_time = publicGetTime = Entry('time', 'public', 'GET', {'cost': 1}) + public_get_ticker = publicGetTicker = Entry('ticker', 'public', 'GET', {'cost': 1}) + public_get_order_book = publicGetOrderBook = Entry('order_book', 'public', 'GET', {'cost': 1}) + public_get_trade = publicGetTrade = Entry('trade', 'public', 'GET', {'cost': 1}) + public_get_candle = publicGetCandle = Entry('candle', 'public', 'GET', {'cost': 1}) + private_post_new_order = privatePostNewOrder = Entry('new_order', 'private', 'POST', {'cost': 2}) + private_post_cancel_order = privatePostCancelOrder = Entry('cancel_order', 'private', 'POST', {'cost': 1}) + private_post_withdrawal = privatePostWithdrawal = Entry('withdrawal', 'private', 'POST', {'cost': 2}) + private_get_balance = privateGetBalance = Entry('balance', 'private', 'GET', {'cost': 1}) + private_get_order = privateGetOrder = Entry('order', 'private', 'GET', {'cost': 1}) + private_get_open_order = privateGetOpenOrder = Entry('open_order', 'private', 'GET', {'cost': 1}) + private_get_order_history = privateGetOrderHistory = Entry('order_history', 'private', 'GET', {'cost': 1}) + private_get_trade_history = privateGetTradeHistory = Entry('trade_history', 'private', 'GET', {'cost': 1}) + private_get_deposit_address = privateGetDepositAddress = Entry('deposit_address', 'private', 'GET', {'cost': 1}) + private_get_transfer_payment = privateGetTransferPayment = Entry('transfer/payment', 'private', 'GET', {'cost': 1}) + accounts_post_token = accountsPostToken = Entry('token', 'accounts', 'POST', {'cost': 1}) diff --git a/ccxt/abstract/timex.py b/ccxt/abstract/timex.py new file mode 100644 index 0000000..1a44eac --- /dev/null +++ b/ccxt/abstract/timex.py @@ -0,0 +1,62 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + addressbook_get_me = addressbookGetMe = Entry('me', 'addressbook', 'GET', {}) + addressbook_post = addressbookPost = Entry('', 'addressbook', 'POST', {}) + addressbook_post_id_id = addressbookPostIdId = Entry('id/{id}', 'addressbook', 'POST', {}) + addressbook_post_id_id_remove = addressbookPostIdIdRemove = Entry('id/{id}/remove', 'addressbook', 'POST', {}) + custody_get_credentials = custodyGetCredentials = Entry('credentials', 'custody', 'GET', {}) + custody_get_credentials_h_hash = custodyGetCredentialsHHash = Entry('credentials/h/{hash}', 'custody', 'GET', {}) + custody_get_credentials_k_key = custodyGetCredentialsKKey = Entry('credentials/k/{key}', 'custody', 'GET', {}) + custody_get_credentials_me = custodyGetCredentialsMe = Entry('credentials/me', 'custody', 'GET', {}) + custody_get_credentials_me_address = custodyGetCredentialsMeAddress = Entry('credentials/me/address', 'custody', 'GET', {}) + custody_get_deposit_addresses = custodyGetDepositAddresses = Entry('deposit-addresses', 'custody', 'GET', {}) + custody_get_deposit_addresses_h_hash = custodyGetDepositAddressesHHash = Entry('deposit-addresses/h/{hash}', 'custody', 'GET', {}) + history_get_orders = historyGetOrders = Entry('orders', 'history', 'GET', {}) + history_get_orders_details = historyGetOrdersDetails = Entry('orders/details', 'history', 'GET', {}) + history_get_orders_export_csv = historyGetOrdersExportCsv = Entry('orders/export/csv', 'history', 'GET', {}) + history_get_trades = historyGetTrades = Entry('trades', 'history', 'GET', {}) + history_get_trades_export_csv = historyGetTradesExportCsv = Entry('trades/export/csv', 'history', 'GET', {}) + currencies_get_a_address = currenciesGetAAddress = Entry('a/{address}', 'currencies', 'GET', {}) + currencies_get_i_id = currenciesGetIId = Entry('i/{id}', 'currencies', 'GET', {}) + currencies_get_s_symbol = currenciesGetSSymbol = Entry('s/{symbol}', 'currencies', 'GET', {}) + currencies_post_perform = currenciesPostPerform = Entry('perform', 'currencies', 'POST', {}) + currencies_post_prepare = currenciesPostPrepare = Entry('prepare', 'currencies', 'POST', {}) + currencies_post_remove_perform = currenciesPostRemovePerform = Entry('remove/perform', 'currencies', 'POST', {}) + currencies_post_s_symbol_remove_prepare = currenciesPostSSymbolRemovePrepare = Entry('s/{symbol}/remove/prepare', 'currencies', 'POST', {}) + currencies_post_s_symbol_update_perform = currenciesPostSSymbolUpdatePerform = Entry('s/{symbol}/update/perform', 'currencies', 'POST', {}) + currencies_post_s_symbol_update_prepare = currenciesPostSSymbolUpdatePrepare = Entry('s/{symbol}/update/prepare', 'currencies', 'POST', {}) + manager_get_deposits = managerGetDeposits = Entry('deposits', 'manager', 'GET', {}) + manager_get_transfers = managerGetTransfers = Entry('transfers', 'manager', 'GET', {}) + manager_get_withdrawals = managerGetWithdrawals = Entry('withdrawals', 'manager', 'GET', {}) + markets_get_i_id = marketsGetIId = Entry('i/{id}', 'markets', 'GET', {}) + markets_get_s_symbol = marketsGetSSymbol = Entry('s/{symbol}', 'markets', 'GET', {}) + markets_post_perform = marketsPostPerform = Entry('perform', 'markets', 'POST', {}) + markets_post_prepare = marketsPostPrepare = Entry('prepare', 'markets', 'POST', {}) + markets_post_remove_perform = marketsPostRemovePerform = Entry('remove/perform', 'markets', 'POST', {}) + markets_post_s_symbol_remove_prepare = marketsPostSSymbolRemovePrepare = Entry('s/{symbol}/remove/prepare', 'markets', 'POST', {}) + markets_post_s_symbol_update_perform = marketsPostSSymbolUpdatePerform = Entry('s/{symbol}/update/perform', 'markets', 'POST', {}) + markets_post_s_symbol_update_prepare = marketsPostSSymbolUpdatePrepare = Entry('s/{symbol}/update/prepare', 'markets', 'POST', {}) + public_get_candles = publicGetCandles = Entry('candles', 'public', 'GET', {}) + public_get_currencies = publicGetCurrencies = Entry('currencies', 'public', 'GET', {}) + public_get_markets = publicGetMarkets = Entry('markets', 'public', 'GET', {}) + public_get_orderbook = publicGetOrderbook = Entry('orderbook', 'public', 'GET', {}) + public_get_orderbook_raw = publicGetOrderbookRaw = Entry('orderbook/raw', 'public', 'GET', {}) + public_get_orderbook_v2 = publicGetOrderbookV2 = Entry('orderbook/v2', 'public', 'GET', {}) + public_get_tickers = publicGetTickers = Entry('tickers', 'public', 'GET', {}) + public_get_trades = publicGetTrades = Entry('trades', 'public', 'GET', {}) + statistics_get_address = statisticsGetAddress = Entry('address', 'statistics', 'GET', {}) + trading_get_balances = tradingGetBalances = Entry('balances', 'trading', 'GET', {}) + trading_get_fees = tradingGetFees = Entry('fees', 'trading', 'GET', {}) + trading_get_orders = tradingGetOrders = Entry('orders', 'trading', 'GET', {}) + trading_post_orders = tradingPostOrders = Entry('orders', 'trading', 'POST', {}) + trading_post_orders_json = tradingPostOrdersJson = Entry('orders/json', 'trading', 'POST', {}) + trading_put_orders = tradingPutOrders = Entry('orders', 'trading', 'PUT', {}) + trading_put_orders_json = tradingPutOrdersJson = Entry('orders/json', 'trading', 'PUT', {}) + trading_delete_orders = tradingDeleteOrders = Entry('orders', 'trading', 'DELETE', {}) + trading_delete_orders_json = tradingDeleteOrdersJson = Entry('orders/json', 'trading', 'DELETE', {}) + tradingview_get_config = tradingviewGetConfig = Entry('config', 'tradingview', 'GET', {}) + tradingview_get_history = tradingviewGetHistory = Entry('history', 'tradingview', 'GET', {}) + tradingview_get_symbol_info = tradingviewGetSymbolInfo = Entry('symbol_info', 'tradingview', 'GET', {}) + tradingview_get_time = tradingviewGetTime = Entry('time', 'tradingview', 'GET', {}) diff --git a/ccxt/abstract/tokocrypto.py b/ccxt/abstract/tokocrypto.py new file mode 100644 index 0000000..081eee0 --- /dev/null +++ b/ccxt/abstract/tokocrypto.py @@ -0,0 +1,37 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + binance_get_ping = binanceGetPing = Entry('ping', 'binance', 'GET', {'cost': 1}) + binance_get_time = binanceGetTime = Entry('time', 'binance', 'GET', {'cost': 1}) + binance_get_depth = binanceGetDepth = Entry('depth', 'binance', 'GET', {'cost': 1, 'byLimit': [[100, 1], [500, 5], [1000, 10], [5000, 50]]}) + binance_get_trades = binanceGetTrades = Entry('trades', 'binance', 'GET', {'cost': 1}) + binance_get_aggtrades = binanceGetAggTrades = Entry('aggTrades', 'binance', 'GET', {'cost': 1}) + binance_get_historicaltrades = binanceGetHistoricalTrades = Entry('historicalTrades', 'binance', 'GET', {'cost': 5}) + binance_get_klines = binanceGetKlines = Entry('klines', 'binance', 'GET', {'cost': 1}) + binance_get_ticker_24hr = binanceGetTicker24hr = Entry('ticker/24hr', 'binance', 'GET', {'cost': 1, 'noSymbol': 40}) + binance_get_ticker_price = binanceGetTickerPrice = Entry('ticker/price', 'binance', 'GET', {'cost': 1, 'noSymbol': 2}) + binance_get_ticker_bookticker = binanceGetTickerBookTicker = Entry('ticker/bookTicker', 'binance', 'GET', {'cost': 1, 'noSymbol': 2}) + binance_get_exchangeinfo = binanceGetExchangeInfo = Entry('exchangeInfo', 'binance', 'GET', {'cost': 10}) + binance_put_userdatastream = binancePutUserDataStream = Entry('userDataStream', 'binance', 'PUT', {'cost': 1}) + binance_post_userdatastream = binancePostUserDataStream = Entry('userDataStream', 'binance', 'POST', {'cost': 1}) + binance_delete_userdatastream = binanceDeleteUserDataStream = Entry('userDataStream', 'binance', 'DELETE', {'cost': 1}) + public_get_open_v1_common_time = publicGetOpenV1CommonTime = Entry('open/v1/common/time', 'public', 'GET', {'cost': 1}) + public_get_open_v1_common_symbols = publicGetOpenV1CommonSymbols = Entry('open/v1/common/symbols', 'public', 'GET', {'cost': 1}) + public_get_open_v1_market_depth = publicGetOpenV1MarketDepth = Entry('open/v1/market/depth', 'public', 'GET', {'cost': 1}) + public_get_open_v1_market_trades = publicGetOpenV1MarketTrades = Entry('open/v1/market/trades', 'public', 'GET', {'cost': 1}) + public_get_open_v1_market_agg_trades = publicGetOpenV1MarketAggTrades = Entry('open/v1/market/agg-trades', 'public', 'GET', {'cost': 1}) + public_get_open_v1_market_klines = publicGetOpenV1MarketKlines = Entry('open/v1/market/klines', 'public', 'GET', {'cost': 1}) + private_get_open_v1_orders_detail = privateGetOpenV1OrdersDetail = Entry('open/v1/orders/detail', 'private', 'GET', {'cost': 1}) + private_get_open_v1_orders = privateGetOpenV1Orders = Entry('open/v1/orders', 'private', 'GET', {'cost': 1}) + private_get_open_v1_account_spot = privateGetOpenV1AccountSpot = Entry('open/v1/account/spot', 'private', 'GET', {'cost': 1}) + private_get_open_v1_account_spot_asset = privateGetOpenV1AccountSpotAsset = Entry('open/v1/account/spot/asset', 'private', 'GET', {'cost': 1}) + private_get_open_v1_orders_trades = privateGetOpenV1OrdersTrades = Entry('open/v1/orders/trades', 'private', 'GET', {'cost': 1}) + private_get_open_v1_withdraws = privateGetOpenV1Withdraws = Entry('open/v1/withdraws', 'private', 'GET', {'cost': 1}) + private_get_open_v1_deposits = privateGetOpenV1Deposits = Entry('open/v1/deposits', 'private', 'GET', {'cost': 1}) + private_get_open_v1_deposits_address = privateGetOpenV1DepositsAddress = Entry('open/v1/deposits/address', 'private', 'GET', {'cost': 1}) + private_post_open_v1_orders = privatePostOpenV1Orders = Entry('open/v1/orders', 'private', 'POST', {'cost': 1}) + private_post_open_v1_orders_cancel = privatePostOpenV1OrdersCancel = Entry('open/v1/orders/cancel', 'private', 'POST', {'cost': 1}) + private_post_open_v1_orders_oco = privatePostOpenV1OrdersOco = Entry('open/v1/orders/oco', 'private', 'POST', {'cost': 1}) + private_post_open_v1_withdraws = privatePostOpenV1Withdraws = Entry('open/v1/withdraws', 'private', 'POST', {'cost': 1}) + private_post_open_v1_user_data_stream = privatePostOpenV1UserDataStream = Entry('open/v1/user-data-stream', 'private', 'POST', {'cost': 1}) diff --git a/ccxt/abstract/toobit.py b/ccxt/abstract/toobit.py new file mode 100644 index 0000000..7714a46 --- /dev/null +++ b/ccxt/abstract/toobit.py @@ -0,0 +1,63 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + common_get_api_v1_time = commonGetApiV1Time = Entry('api/v1/time', 'common', 'GET', {'cost': 1}) + common_get_api_v1_ping = commonGetApiV1Ping = Entry('api/v1/ping', 'common', 'GET', {'cost': 1}) + common_get_api_v1_exchangeinfo = commonGetApiV1ExchangeInfo = Entry('api/v1/exchangeInfo', 'common', 'GET', {'cost': 1}) + common_get_quote_v1_depth = commonGetQuoteV1Depth = Entry('quote/v1/depth', 'common', 'GET', {'cost': 1}) + common_get_quote_v1_depth_merged = commonGetQuoteV1DepthMerged = Entry('quote/v1/depth/merged', 'common', 'GET', {'cost': 1}) + common_get_quote_v1_trades = commonGetQuoteV1Trades = Entry('quote/v1/trades', 'common', 'GET', {'cost': 1}) + common_get_quote_v1_klines = commonGetQuoteV1Klines = Entry('quote/v1/klines', 'common', 'GET', {'cost': 1}) + common_get_quote_v1_index_klines = commonGetQuoteV1IndexKlines = Entry('quote/v1/index/klines', 'common', 'GET', {'cost': 1}) + common_get_quote_v1_markprice_klines = commonGetQuoteV1MarkPriceKlines = Entry('quote/v1/markPrice/klines', 'common', 'GET', {'cost': 1}) + common_get_quote_v1_markprice = commonGetQuoteV1MarkPrice = Entry('quote/v1/markPrice', 'common', 'GET', {'cost': 1}) + common_get_quote_v1_index = commonGetQuoteV1Index = Entry('quote/v1/index', 'common', 'GET', {'cost': 1}) + common_get_quote_v1_ticker_24hr = commonGetQuoteV1Ticker24hr = Entry('quote/v1/ticker/24hr', 'common', 'GET', {'cost': 40}) + common_get_quote_v1_contract_ticker_24hr = commonGetQuoteV1ContractTicker24hr = Entry('quote/v1/contract/ticker/24hr', 'common', 'GET', {'cost': 40}) + common_get_quote_v1_ticker_price = commonGetQuoteV1TickerPrice = Entry('quote/v1/ticker/price', 'common', 'GET', {'cost': 1}) + common_get_quote_v1_ticker_bookticker = commonGetQuoteV1TickerBookTicker = Entry('quote/v1/ticker/bookTicker', 'common', 'GET', {'cost': 1}) + common_get_api_v1_futures_fundingrate = commonGetApiV1FuturesFundingRate = Entry('api/v1/futures/fundingRate', 'common', 'GET', {'cost': 1}) + common_get_api_v1_futures_historyfundingrate = commonGetApiV1FuturesHistoryFundingRate = Entry('api/v1/futures/historyFundingRate', 'common', 'GET', {'cost': 1}) + private_get_api_v1_account = privateGetApiV1Account = Entry('api/v1/account', 'private', 'GET', {'cost': 5}) + private_get_api_v1_account_checkapikey = privateGetApiV1AccountCheckApiKey = Entry('api/v1/account/checkApiKey', 'private', 'GET', {'cost': 1}) + private_get_api_v1_spot_order = privateGetApiV1SpotOrder = Entry('api/v1/spot/order', 'private', 'GET', {'cost': 1.67}) + private_get_api_v1_spot_openorders = privateGetApiV1SpotOpenOrders = Entry('api/v1/spot/openOrders', 'private', 'GET', {'cost': 1.67}) + private_get_api_v1_futures_openorders = privateGetApiV1FuturesOpenOrders = Entry('api/v1/futures/openOrders', 'private', 'GET', {'cost': 1.67}) + private_get_api_v1_spot_tradeorders = privateGetApiV1SpotTradeOrders = Entry('api/v1/spot/tradeOrders', 'private', 'GET', {'cost': 8.35}) + private_get_api_v1_futures_historyorders = privateGetApiV1FuturesHistoryOrders = Entry('api/v1/futures/historyOrders', 'private', 'GET', {'cost': 8.35}) + private_get_api_v1_account_trades = privateGetApiV1AccountTrades = Entry('api/v1/account/trades', 'private', 'GET', {'cost': 8.35}) + private_get_api_v1_account_balanceflow = privateGetApiV1AccountBalanceFlow = Entry('api/v1/account/balanceFlow', 'private', 'GET', {'cost': 5}) + private_get_api_v1_account_depositorders = privateGetApiV1AccountDepositOrders = Entry('api/v1/account/depositOrders', 'private', 'GET', {'cost': 5}) + private_get_api_v1_account_withdraworders = privateGetApiV1AccountWithdrawOrders = Entry('api/v1/account/withdrawOrders', 'private', 'GET', {'cost': 5}) + private_get_api_v1_account_deposit_address = privateGetApiV1AccountDepositAddress = Entry('api/v1/account/deposit/address', 'private', 'GET', {'cost': 1}) + private_get_api_v1_subaccount = privateGetApiV1SubAccount = Entry('api/v1/subAccount', 'private', 'GET', {'cost': 5}) + private_get_api_v1_futures_accountleverage = privateGetApiV1FuturesAccountLeverage = Entry('api/v1/futures/accountLeverage', 'private', 'GET', {'cost': 1}) + private_get_api_v1_futures_order = privateGetApiV1FuturesOrder = Entry('api/v1/futures/order', 'private', 'GET', {'cost': 1.67}) + private_get_api_v1_futures_positions = privateGetApiV1FuturesPositions = Entry('api/v1/futures/positions', 'private', 'GET', {'cost': 8.35}) + private_get_api_v1_futures_balance = privateGetApiV1FuturesBalance = Entry('api/v1/futures/balance', 'private', 'GET', {'cost': 5}) + private_get_api_v1_futures_usertrades = privateGetApiV1FuturesUserTrades = Entry('api/v1/futures/userTrades', 'private', 'GET', {'cost': 8.35}) + private_get_api_v1_futures_balanceflow = privateGetApiV1FuturesBalanceFlow = Entry('api/v1/futures/balanceFlow', 'private', 'GET', {'cost': 5}) + private_get_api_v1_futures_commissionrate = privateGetApiV1FuturesCommissionRate = Entry('api/v1/futures/commissionRate', 'private', 'GET', {'cost': 5}) + private_get_api_v1_futures_todaypnl = privateGetApiV1FuturesTodayPnl = Entry('api/v1/futures/todayPnl', 'private', 'GET', {'cost': 5}) + private_post_api_v1_spot_ordertest = privatePostApiV1SpotOrderTest = Entry('api/v1/spot/orderTest', 'private', 'POST', {'cost': 1.67}) + private_post_api_v1_spot_order = privatePostApiV1SpotOrder = Entry('api/v1/spot/order', 'private', 'POST', {'cost': 1.67}) + private_post_api_v1_futures_order = privatePostApiV1FuturesOrder = Entry('api/v1/futures/order', 'private', 'POST', {'cost': 1.67}) + private_post_api_v1_spot_batchorders = privatePostApiV1SpotBatchOrders = Entry('api/v1/spot/batchOrders', 'private', 'POST', {'cost': 3.34}) + private_post_api_v1_subaccount_transfer = privatePostApiV1SubAccountTransfer = Entry('api/v1/subAccount/transfer', 'private', 'POST', {'cost': 1}) + private_post_api_v1_account_withdraw = privatePostApiV1AccountWithdraw = Entry('api/v1/account/withdraw', 'private', 'POST', {'cost': 1}) + private_post_api_v1_futures_margintype = privatePostApiV1FuturesMarginType = Entry('api/v1/futures/marginType', 'private', 'POST', {'cost': 1}) + private_post_api_v1_futures_leverage = privatePostApiV1FuturesLeverage = Entry('api/v1/futures/leverage', 'private', 'POST', {'cost': 1}) + private_post_api_v1_futures_batchorders = privatePostApiV1FuturesBatchOrders = Entry('api/v1/futures/batchOrders', 'private', 'POST', {'cost': 3.34}) + private_post_api_v1_futures_position_trading_stop = privatePostApiV1FuturesPositionTradingStop = Entry('api/v1/futures/position/trading-stop', 'private', 'POST', {'cost': 5.01}) + private_post_api_v1_futures_positionmargin = privatePostApiV1FuturesPositionMargin = Entry('api/v1/futures/positionMargin', 'private', 'POST', {'cost': 1}) + private_post_api_v1_userdatastream = privatePostApiV1UserDataStream = Entry('api/v1/userDataStream', 'private', 'POST', {'cost': 1}) + private_post_api_v1_listenkey = privatePostApiV1ListenKey = Entry('api/v1/listenKey', 'private', 'POST', {'cost': 1}) + private_delete_api_v1_spot_order = privateDeleteApiV1SpotOrder = Entry('api/v1/spot/order', 'private', 'DELETE', {'cost': 1.67}) + private_delete_api_v1_futures_order = privateDeleteApiV1FuturesOrder = Entry('api/v1/futures/order', 'private', 'DELETE', {'cost': 1.67}) + private_delete_api_v1_spot_openorders = privateDeleteApiV1SpotOpenOrders = Entry('api/v1/spot/openOrders', 'private', 'DELETE', {'cost': 8.35}) + private_delete_api_v1_futures_batchorders = privateDeleteApiV1FuturesBatchOrders = Entry('api/v1/futures/batchOrders', 'private', 'DELETE', {'cost': 8.35}) + private_delete_api_v1_spot_cancelorderbyids = privateDeleteApiV1SpotCancelOrderByIds = Entry('api/v1/spot/cancelOrderByIds', 'private', 'DELETE', {'cost': 8.35}) + private_delete_api_v1_futures_cancelorderbyids = privateDeleteApiV1FuturesCancelOrderByIds = Entry('api/v1/futures/cancelOrderByIds', 'private', 'DELETE', {'cost': 8.35}) + private_delete_api_v1_listenkey = privateDeleteApiV1ListenKey = Entry('api/v1/listenKey', 'private', 'DELETE', {'cost': 1}) + private_put_api_v1_listenkey = privatePutApiV1ListenKey = Entry('api/v1/listenKey', 'private', 'PUT', {'cost': 1}) diff --git a/ccxt/abstract/upbit.py b/ccxt/abstract/upbit.py new file mode 100644 index 0000000..5ee3070 --- /dev/null +++ b/ccxt/abstract/upbit.py @@ -0,0 +1,56 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_market_all = publicGetMarketAll = Entry('market/all', 'public', 'GET', {'cost': 2}) + public_get_candles_timeframe = publicGetCandlesTimeframe = Entry('candles/{timeframe}', 'public', 'GET', {'cost': 2}) + public_get_candles_timeframe_unit = publicGetCandlesTimeframeUnit = Entry('candles/{timeframe}/{unit}', 'public', 'GET', {'cost': 2}) + public_get_candles_seconds = publicGetCandlesSeconds = Entry('candles/seconds', 'public', 'GET', {'cost': 2}) + public_get_candles_minutes_unit = publicGetCandlesMinutesUnit = Entry('candles/minutes/{unit}', 'public', 'GET', {'cost': 2}) + public_get_candles_minutes_1 = publicGetCandlesMinutes1 = Entry('candles/minutes/1', 'public', 'GET', {'cost': 2}) + public_get_candles_minutes_3 = publicGetCandlesMinutes3 = Entry('candles/minutes/3', 'public', 'GET', {'cost': 2}) + public_get_candles_minutes_5 = publicGetCandlesMinutes5 = Entry('candles/minutes/5', 'public', 'GET', {'cost': 2}) + public_get_candles_minutes_10 = publicGetCandlesMinutes10 = Entry('candles/minutes/10', 'public', 'GET', {'cost': 2}) + public_get_candles_minutes_15 = publicGetCandlesMinutes15 = Entry('candles/minutes/15', 'public', 'GET', {'cost': 2}) + public_get_candles_minutes_30 = publicGetCandlesMinutes30 = Entry('candles/minutes/30', 'public', 'GET', {'cost': 2}) + public_get_candles_minutes_60 = publicGetCandlesMinutes60 = Entry('candles/minutes/60', 'public', 'GET', {'cost': 2}) + public_get_candles_minutes_240 = publicGetCandlesMinutes240 = Entry('candles/minutes/240', 'public', 'GET', {'cost': 2}) + public_get_candles_days = publicGetCandlesDays = Entry('candles/days', 'public', 'GET', {'cost': 2}) + public_get_candles_weeks = publicGetCandlesWeeks = Entry('candles/weeks', 'public', 'GET', {'cost': 2}) + public_get_candles_months = publicGetCandlesMonths = Entry('candles/months', 'public', 'GET', {'cost': 2}) + public_get_candles_years = publicGetCandlesYears = Entry('candles/years', 'public', 'GET', {'cost': 2}) + public_get_trades_ticks = publicGetTradesTicks = Entry('trades/ticks', 'public', 'GET', {'cost': 2}) + public_get_ticker = publicGetTicker = Entry('ticker', 'public', 'GET', {'cost': 2}) + public_get_ticker_all = publicGetTickerAll = Entry('ticker/all', 'public', 'GET', {'cost': 2}) + public_get_orderbook = publicGetOrderbook = Entry('orderbook', 'public', 'GET', {'cost': 2}) + public_get_orderbook_instruments = publicGetOrderbookInstruments = Entry('orderbook/instruments', 'public', 'GET', {'cost': 2}) + public_get_orderbook_supported_levels = publicGetOrderbookSupportedLevels = Entry('orderbook/supported_levels', 'public', 'GET', {'cost': 2}) + private_get_accounts = privateGetAccounts = Entry('accounts', 'private', 'GET', {'cost': 0.67}) + private_get_orders_chance = privateGetOrdersChance = Entry('orders/chance', 'private', 'GET', {'cost': 0.67}) + private_get_order = privateGetOrder = Entry('order', 'private', 'GET', {'cost': 0.67}) + private_get_orders_closed = privateGetOrdersClosed = Entry('orders/closed', 'private', 'GET', {'cost': 0.67}) + private_get_orders_open = privateGetOrdersOpen = Entry('orders/open', 'private', 'GET', {'cost': 0.67}) + private_get_orders_uuids = privateGetOrdersUuids = Entry('orders/uuids', 'private', 'GET', {'cost': 0.67}) + private_get_withdraws = privateGetWithdraws = Entry('withdraws', 'private', 'GET', {'cost': 0.67}) + private_get_withdraw = privateGetWithdraw = Entry('withdraw', 'private', 'GET', {'cost': 0.67}) + private_get_withdraws_chance = privateGetWithdrawsChance = Entry('withdraws/chance', 'private', 'GET', {'cost': 0.67}) + private_get_withdraws_coin_addresses = privateGetWithdrawsCoinAddresses = Entry('withdraws/coin_addresses', 'private', 'GET', {'cost': 0.67}) + private_get_deposits = privateGetDeposits = Entry('deposits', 'private', 'GET', {'cost': 0.67}) + private_get_deposits_chance_coin = privateGetDepositsChanceCoin = Entry('deposits/chance/coin', 'private', 'GET', {'cost': 0.67}) + private_get_deposit = privateGetDeposit = Entry('deposit', 'private', 'GET', {'cost': 0.67}) + private_get_deposits_coin_addresses = privateGetDepositsCoinAddresses = Entry('deposits/coin_addresses', 'private', 'GET', {'cost': 0.67}) + private_get_deposits_coin_address = privateGetDepositsCoinAddress = Entry('deposits/coin_address', 'private', 'GET', {'cost': 0.67}) + private_get_travel_rule_vasps = privateGetTravelRuleVasps = Entry('travel_rule/vasps', 'private', 'GET', {'cost': 0.67}) + private_get_status_wallet = privateGetStatusWallet = Entry('status/wallet', 'private', 'GET', {'cost': 0.67}) + private_get_api_keys = privateGetApiKeys = Entry('api_keys', 'private', 'GET', {'cost': 0.67}) + private_post_orders = privatePostOrders = Entry('orders', 'private', 'POST', {'cost': 2.5}) + private_post_orders_cancel_and_new = privatePostOrdersCancelAndNew = Entry('orders/cancel_and_new', 'private', 'POST', {'cost': 2.5}) + private_post_withdraws_coin = privatePostWithdrawsCoin = Entry('withdraws/coin', 'private', 'POST', {'cost': 0.67}) + private_post_withdraws_krw = privatePostWithdrawsKrw = Entry('withdraws/krw', 'private', 'POST', {'cost': 0.67}) + private_post_deposits_krw = privatePostDepositsKrw = Entry('deposits/krw', 'private', 'POST', {'cost': 0.67}) + private_post_deposits_generate_coin_address = privatePostDepositsGenerateCoinAddress = Entry('deposits/generate_coin_address', 'private', 'POST', {'cost': 0.67}) + private_post_travel_rule_deposit_uuid = privatePostTravelRuleDepositUuid = Entry('travel_rule/deposit/uuid', 'private', 'POST', {'cost': 0.67}) + private_post_travel_rule_deposit_txid = privatePostTravelRuleDepositTxid = Entry('travel_rule/deposit/txid', 'private', 'POST', {'cost': 0.67}) + private_delete_order = privateDeleteOrder = Entry('order', 'private', 'DELETE', {'cost': 0.67}) + private_delete_orders_open = privateDeleteOrdersOpen = Entry('orders/open', 'private', 'DELETE', {'cost': 40}) + private_delete_orders_uuids = privateDeleteOrdersUuids = Entry('orders/uuids', 'private', 'DELETE', {'cost': 0.67}) diff --git a/ccxt/abstract/wavesexchange.py b/ccxt/abstract/wavesexchange.py new file mode 100644 index 0000000..aaca96c --- /dev/null +++ b/ccxt/abstract/wavesexchange.py @@ -0,0 +1,154 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + matcher_get_matcher = matcherGetMatcher = Entry('matcher', 'matcher', 'GET', {}) + matcher_get_matcher_settings = matcherGetMatcherSettings = Entry('matcher/settings', 'matcher', 'GET', {}) + matcher_get_matcher_settings_rates = matcherGetMatcherSettingsRates = Entry('matcher/settings/rates', 'matcher', 'GET', {}) + matcher_get_matcher_balance_reserved_publickey = matcherGetMatcherBalanceReservedPublicKey = Entry('matcher/balance/reserved/{publicKey}', 'matcher', 'GET', {}) + matcher_get_matcher_debug_allsnashotoffsets = matcherGetMatcherDebugAllSnashotOffsets = Entry('matcher/debug/allSnashotOffsets', 'matcher', 'GET', {}) + matcher_get_matcher_debug_currentoffset = matcherGetMatcherDebugCurrentOffset = Entry('matcher/debug/currentOffset', 'matcher', 'GET', {}) + matcher_get_matcher_debug_lastoffset = matcherGetMatcherDebugLastOffset = Entry('matcher/debug/lastOffset', 'matcher', 'GET', {}) + matcher_get_matcher_debug_oldestsnapshotoffset = matcherGetMatcherDebugOldestSnapshotOffset = Entry('matcher/debug/oldestSnapshotOffset', 'matcher', 'GET', {}) + matcher_get_matcher_debug_config = matcherGetMatcherDebugConfig = Entry('matcher/debug/config', 'matcher', 'GET', {}) + matcher_get_matcher_debug_address_address = matcherGetMatcherDebugAddressAddress = Entry('matcher/debug/address/{address}', 'matcher', 'GET', {}) + matcher_get_matcher_debug_status = matcherGetMatcherDebugStatus = Entry('matcher/debug/status', 'matcher', 'GET', {}) + matcher_get_matcher_debug_address_address_check = matcherGetMatcherDebugAddressAddressCheck = Entry('matcher/debug/address/{address}/check', 'matcher', 'GET', {}) + matcher_get_matcher_orderbook = matcherGetMatcherOrderbook = Entry('matcher/orderbook', 'matcher', 'GET', {}) + matcher_get_matcher_orderbook_baseid_quoteid = matcherGetMatcherOrderbookBaseIdQuoteId = Entry('matcher/orderbook/{baseId}/{quoteId}', 'matcher', 'GET', {}) + matcher_get_matcher_orderbook_baseid_quoteid_publickey_publickey = matcherGetMatcherOrderbookBaseIdQuoteIdPublicKeyPublicKey = Entry('matcher/orderbook/{baseId}/{quoteId}/publicKey/{publicKey}', 'matcher', 'GET', {}) + matcher_get_matcher_orderbook_baseid_quoteid_orderid = matcherGetMatcherOrderbookBaseIdQuoteIdOrderId = Entry('matcher/orderbook/{baseId}/{quoteId}/{orderId}', 'matcher', 'GET', {}) + matcher_get_matcher_orderbook_baseid_quoteid_info = matcherGetMatcherOrderbookBaseIdQuoteIdInfo = Entry('matcher/orderbook/{baseId}/{quoteId}/info', 'matcher', 'GET', {}) + matcher_get_matcher_orderbook_baseid_quoteid_status = matcherGetMatcherOrderbookBaseIdQuoteIdStatus = Entry('matcher/orderbook/{baseId}/{quoteId}/status', 'matcher', 'GET', {}) + matcher_get_matcher_orderbook_baseid_quoteid_tradablebalance_address = matcherGetMatcherOrderbookBaseIdQuoteIdTradableBalanceAddress = Entry('matcher/orderbook/{baseId}/{quoteId}/tradableBalance/{address}', 'matcher', 'GET', {}) + matcher_get_matcher_orderbook_publickey = matcherGetMatcherOrderbookPublicKey = Entry('matcher/orderbook/{publicKey}', 'matcher', 'GET', {}) + matcher_get_matcher_orderbook_publickey_orderid = matcherGetMatcherOrderbookPublicKeyOrderId = Entry('matcher/orderbook/{publicKey}/{orderId}', 'matcher', 'GET', {}) + matcher_get_matcher_orders_address = matcherGetMatcherOrdersAddress = Entry('matcher/orders/{address}', 'matcher', 'GET', {}) + matcher_get_matcher_orders_address_orderid = matcherGetMatcherOrdersAddressOrderId = Entry('matcher/orders/{address}/{orderId}', 'matcher', 'GET', {}) + matcher_get_matcher_transactions_orderid = matcherGetMatcherTransactionsOrderId = Entry('matcher/transactions/{orderId}', 'matcher', 'GET', {}) + matcher_get_api_v1_orderbook_baseid_quoteid = matcherGetApiV1OrderbookBaseIdQuoteId = Entry('api/v1/orderbook/{baseId}/{quoteId}', 'matcher', 'GET', {}) + matcher_post_matcher_orderbook = matcherPostMatcherOrderbook = Entry('matcher/orderbook', 'matcher', 'POST', {}) + matcher_post_matcher_orderbook_market = matcherPostMatcherOrderbookMarket = Entry('matcher/orderbook/market', 'matcher', 'POST', {}) + matcher_post_matcher_orderbook_cancel = matcherPostMatcherOrderbookCancel = Entry('matcher/orderbook/cancel', 'matcher', 'POST', {}) + matcher_post_matcher_orderbook_baseid_quoteid_cancel = matcherPostMatcherOrderbookBaseIdQuoteIdCancel = Entry('matcher/orderbook/{baseId}/{quoteId}/cancel', 'matcher', 'POST', {}) + matcher_post_matcher_orderbook_baseid_quoteid_calculatefee = matcherPostMatcherOrderbookBaseIdQuoteIdCalculateFee = Entry('matcher/orderbook/{baseId}/{quoteId}/calculateFee', 'matcher', 'POST', {}) + matcher_post_matcher_orderbook_baseid_quoteid_delete = matcherPostMatcherOrderbookBaseIdQuoteIdDelete = Entry('matcher/orderbook/{baseId}/{quoteId}/delete', 'matcher', 'POST', {}) + matcher_post_matcher_orderbook_baseid_quoteid_cancelall = matcherPostMatcherOrderbookBaseIdQuoteIdCancelAll = Entry('matcher/orderbook/{baseId}/{quoteId}/cancelAll', 'matcher', 'POST', {}) + matcher_post_matcher_debug_savesnapshots = matcherPostMatcherDebugSaveSnapshots = Entry('matcher/debug/saveSnapshots', 'matcher', 'POST', {}) + matcher_post_matcher_orders_address_cancel = matcherPostMatcherOrdersAddressCancel = Entry('matcher/orders/{address}/cancel', 'matcher', 'POST', {}) + matcher_post_matcher_orders_cancel_orderid = matcherPostMatcherOrdersCancelOrderId = Entry('matcher/orders/cancel/{orderId}', 'matcher', 'POST', {}) + matcher_post_matcher_orders_serialize = matcherPostMatcherOrdersSerialize = Entry('matcher/orders/serialize', 'matcher', 'POST', {}) + matcher_delete_matcher_orderbook_baseid_quoteid = matcherDeleteMatcherOrderbookBaseIdQuoteId = Entry('matcher/orderbook/{baseId}/{quoteId}', 'matcher', 'DELETE', {}) + matcher_delete_matcher_settings_rates_assetid = matcherDeleteMatcherSettingsRatesAssetId = Entry('matcher/settings/rates/{assetId}', 'matcher', 'DELETE', {}) + matcher_put_matcher_settings_rates_assetid = matcherPutMatcherSettingsRatesAssetId = Entry('matcher/settings/rates/{assetId}', 'matcher', 'PUT', {}) + node_get_addresses = nodeGetAddresses = Entry('addresses', 'node', 'GET', {}) + node_get_addresses_balance_address = nodeGetAddressesBalanceAddress = Entry('addresses/balance/{address}', 'node', 'GET', {}) + node_get_addresses_balance_address_confirmations = nodeGetAddressesBalanceAddressConfirmations = Entry('addresses/balance/{address}/{confirmations}', 'node', 'GET', {}) + node_get_addresses_balance_details_address = nodeGetAddressesBalanceDetailsAddress = Entry('addresses/balance/details/{address}', 'node', 'GET', {}) + node_get_addresses_data_address = nodeGetAddressesDataAddress = Entry('addresses/data/{address}', 'node', 'GET', {}) + node_get_addresses_data_address_key = nodeGetAddressesDataAddressKey = Entry('addresses/data/{address}/{key}', 'node', 'GET', {}) + node_get_addresses_effectivebalance_address = nodeGetAddressesEffectiveBalanceAddress = Entry('addresses/effectiveBalance/{address}', 'node', 'GET', {}) + node_get_addresses_effectivebalance_address_confirmations = nodeGetAddressesEffectiveBalanceAddressConfirmations = Entry('addresses/effectiveBalance/{address}/{confirmations}', 'node', 'GET', {}) + node_get_addresses_publickey_publickey = nodeGetAddressesPublicKeyPublicKey = Entry('addresses/publicKey/{publicKey}', 'node', 'GET', {}) + node_get_addresses_scriptinfo_address = nodeGetAddressesScriptInfoAddress = Entry('addresses/scriptInfo/{address}', 'node', 'GET', {}) + node_get_addresses_scriptinfo_address_meta = nodeGetAddressesScriptInfoAddressMeta = Entry('addresses/scriptInfo/{address}/meta', 'node', 'GET', {}) + node_get_addresses_seed_address = nodeGetAddressesSeedAddress = Entry('addresses/seed/{address}', 'node', 'GET', {}) + node_get_addresses_seq_from_to = nodeGetAddressesSeqFromTo = Entry('addresses/seq/{from}/{to}', 'node', 'GET', {}) + node_get_addresses_validate_address = nodeGetAddressesValidateAddress = Entry('addresses/validate/{address}', 'node', 'GET', {}) + node_get_alias_by_address_address = nodeGetAliasByAddressAddress = Entry('alias/by-address/{address}', 'node', 'GET', {}) + node_get_alias_by_alias_alias = nodeGetAliasByAliasAlias = Entry('alias/by-alias/{alias}', 'node', 'GET', {}) + node_get_assets_assetid_distribution_height_limit = nodeGetAssetsAssetIdDistributionHeightLimit = Entry('assets/{assetId}/distribution/{height}/{limit}', 'node', 'GET', {}) + node_get_assets_balance_address = nodeGetAssetsBalanceAddress = Entry('assets/balance/{address}', 'node', 'GET', {}) + node_get_assets_balance_address_assetid = nodeGetAssetsBalanceAddressAssetId = Entry('assets/balance/{address}/{assetId}', 'node', 'GET', {}) + node_get_assets_details_assetid = nodeGetAssetsDetailsAssetId = Entry('assets/details/{assetId}', 'node', 'GET', {}) + node_get_assets_nft_address_limit_limit = nodeGetAssetsNftAddressLimitLimit = Entry('assets/nft/{address}/limit/{limit}', 'node', 'GET', {}) + node_get_blockchain_rewards = nodeGetBlockchainRewards = Entry('blockchain/rewards', 'node', 'GET', {}) + node_get_blockchain_rewards_height = nodeGetBlockchainRewardsHeight = Entry('blockchain/rewards/height', 'node', 'GET', {}) + node_get_blocks_address_address_from_to = nodeGetBlocksAddressAddressFromTo = Entry('blocks/address/{address}/{from}/{to}/', 'node', 'GET', {}) + node_get_blocks_at_height = nodeGetBlocksAtHeight = Entry('blocks/at/{height}', 'node', 'GET', {}) + node_get_blocks_delay_signature_blocknum = nodeGetBlocksDelaySignatureBlockNum = Entry('blocks/delay/{signature}/{blockNum}', 'node', 'GET', {}) + node_get_blocks_first = nodeGetBlocksFirst = Entry('blocks/first', 'node', 'GET', {}) + node_get_blocks_headers_last = nodeGetBlocksHeadersLast = Entry('blocks/headers/last', 'node', 'GET', {}) + node_get_blocks_headers_seq_from_to = nodeGetBlocksHeadersSeqFromTo = Entry('blocks/headers/seq/{from}/{to}', 'node', 'GET', {}) + node_get_blocks_height = nodeGetBlocksHeight = Entry('blocks/height', 'node', 'GET', {}) + node_get_blocks_height_signature = nodeGetBlocksHeightSignature = Entry('blocks/height/{signature}', 'node', 'GET', {}) + node_get_blocks_last = nodeGetBlocksLast = Entry('blocks/last', 'node', 'GET', {}) + node_get_blocks_seq_from_to = nodeGetBlocksSeqFromTo = Entry('blocks/seq/{from}/{to}', 'node', 'GET', {}) + node_get_blocks_signature_signature = nodeGetBlocksSignatureSignature = Entry('blocks/signature/{signature}', 'node', 'GET', {}) + node_get_consensus_algo = nodeGetConsensusAlgo = Entry('consensus/algo', 'node', 'GET', {}) + node_get_consensus_basetarget = nodeGetConsensusBasetarget = Entry('consensus/basetarget', 'node', 'GET', {}) + node_get_consensus_basetarget_blockid = nodeGetConsensusBasetargetBlockId = Entry('consensus/basetarget/{blockId}', 'node', 'GET', {}) + node_get_consensus_generatingbalance_address = nodeGetConsensusGeneratingbalanceAddress = Entry('consensus/{generatingbalance}/address', 'node', 'GET', {}) + node_get_consensus_generationsignature = nodeGetConsensusGenerationsignature = Entry('consensus/generationsignature', 'node', 'GET', {}) + node_get_consensus_generationsignature_blockid = nodeGetConsensusGenerationsignatureBlockId = Entry('consensus/generationsignature/{blockId}', 'node', 'GET', {}) + node_get_debug_balances_history_address = nodeGetDebugBalancesHistoryAddress = Entry('debug/balances/history/{address}', 'node', 'GET', {}) + node_get_debug_blocks_howmany = nodeGetDebugBlocksHowMany = Entry('debug/blocks/{howMany}', 'node', 'GET', {}) + node_get_debug_configinfo = nodeGetDebugConfigInfo = Entry('debug/configInfo', 'node', 'GET', {}) + node_get_debug_historyinfo = nodeGetDebugHistoryInfo = Entry('debug/historyInfo', 'node', 'GET', {}) + node_get_debug_info = nodeGetDebugInfo = Entry('debug/info', 'node', 'GET', {}) + node_get_debug_minerinfo = nodeGetDebugMinerInfo = Entry('debug/minerInfo', 'node', 'GET', {}) + node_get_debug_portfolios_address = nodeGetDebugPortfoliosAddress = Entry('debug/portfolios/{address}', 'node', 'GET', {}) + node_get_debug_state = nodeGetDebugState = Entry('debug/state', 'node', 'GET', {}) + node_get_debug_statechanges_address_address = nodeGetDebugStateChangesAddressAddress = Entry('debug/stateChanges/address/{address}', 'node', 'GET', {}) + node_get_debug_statechanges_info_id = nodeGetDebugStateChangesInfoId = Entry('debug/stateChanges/info/{id}', 'node', 'GET', {}) + node_get_debug_statewaves_height = nodeGetDebugStateWavesHeight = Entry('debug/stateWaves/{height}', 'node', 'GET', {}) + node_get_leasing_active_address = nodeGetLeasingActiveAddress = Entry('leasing/active/{address}', 'node', 'GET', {}) + node_get_node_state = nodeGetNodeState = Entry('node/state', 'node', 'GET', {}) + node_get_node_version = nodeGetNodeVersion = Entry('node/version', 'node', 'GET', {}) + node_get_peers_all = nodeGetPeersAll = Entry('peers/all', 'node', 'GET', {}) + node_get_peers_blacklisted = nodeGetPeersBlacklisted = Entry('peers/blacklisted', 'node', 'GET', {}) + node_get_peers_connected = nodeGetPeersConnected = Entry('peers/connected', 'node', 'GET', {}) + node_get_peers_suspended = nodeGetPeersSuspended = Entry('peers/suspended', 'node', 'GET', {}) + node_get_transactions_address_address_limit_limit = nodeGetTransactionsAddressAddressLimitLimit = Entry('transactions/address/{address}/limit/{limit}', 'node', 'GET', {}) + node_get_transactions_info_id = nodeGetTransactionsInfoId = Entry('transactions/info/{id}', 'node', 'GET', {}) + node_get_transactions_status = nodeGetTransactionsStatus = Entry('transactions/status', 'node', 'GET', {}) + node_get_transactions_unconfirmed = nodeGetTransactionsUnconfirmed = Entry('transactions/unconfirmed', 'node', 'GET', {}) + node_get_transactions_unconfirmed_info_id = nodeGetTransactionsUnconfirmedInfoId = Entry('transactions/unconfirmed/info/{id}', 'node', 'GET', {}) + node_get_transactions_unconfirmed_size = nodeGetTransactionsUnconfirmedSize = Entry('transactions/unconfirmed/size', 'node', 'GET', {}) + node_get_utils_seed = nodeGetUtilsSeed = Entry('utils/seed', 'node', 'GET', {}) + node_get_utils_seed_length = nodeGetUtilsSeedLength = Entry('utils/seed/{length}', 'node', 'GET', {}) + node_get_utils_time = nodeGetUtilsTime = Entry('utils/time', 'node', 'GET', {}) + node_get_wallet_seed = nodeGetWalletSeed = Entry('wallet/seed', 'node', 'GET', {}) + node_post_addresses = nodePostAddresses = Entry('addresses', 'node', 'POST', {}) + node_post_addresses_data_address = nodePostAddressesDataAddress = Entry('addresses/data/{address}', 'node', 'POST', {}) + node_post_addresses_sign_address = nodePostAddressesSignAddress = Entry('addresses/sign/{address}', 'node', 'POST', {}) + node_post_addresses_signtext_address = nodePostAddressesSignTextAddress = Entry('addresses/signText/{address}', 'node', 'POST', {}) + node_post_addresses_verify_address = nodePostAddressesVerifyAddress = Entry('addresses/verify/{address}', 'node', 'POST', {}) + node_post_addresses_verifytext_address = nodePostAddressesVerifyTextAddress = Entry('addresses/verifyText/{address}', 'node', 'POST', {}) + node_post_debug_blacklist = nodePostDebugBlacklist = Entry('debug/blacklist', 'node', 'POST', {}) + node_post_debug_print = nodePostDebugPrint = Entry('debug/print', 'node', 'POST', {}) + node_post_debug_rollback = nodePostDebugRollback = Entry('debug/rollback', 'node', 'POST', {}) + node_post_debug_validate = nodePostDebugValidate = Entry('debug/validate', 'node', 'POST', {}) + node_post_node_stop = nodePostNodeStop = Entry('node/stop', 'node', 'POST', {}) + node_post_peers_clearblacklist = nodePostPeersClearblacklist = Entry('peers/clearblacklist', 'node', 'POST', {}) + node_post_peers_connect = nodePostPeersConnect = Entry('peers/connect', 'node', 'POST', {}) + node_post_transactions_broadcast = nodePostTransactionsBroadcast = Entry('transactions/broadcast', 'node', 'POST', {}) + node_post_transactions_calculatefee = nodePostTransactionsCalculateFee = Entry('transactions/calculateFee', 'node', 'POST', {}) + node_post_tranasctions_sign = nodePostTranasctionsSign = Entry('tranasctions/sign', 'node', 'POST', {}) + node_post_transactions_sign_signeraddress = nodePostTransactionsSignSignerAddress = Entry('transactions/sign/{signerAddress}', 'node', 'POST', {}) + node_post_tranasctions_status = nodePostTranasctionsStatus = Entry('tranasctions/status', 'node', 'POST', {}) + node_post_utils_hash_fast = nodePostUtilsHashFast = Entry('utils/hash/fast', 'node', 'POST', {}) + node_post_utils_hash_secure = nodePostUtilsHashSecure = Entry('utils/hash/secure', 'node', 'POST', {}) + node_post_utils_script_compilecode = nodePostUtilsScriptCompileCode = Entry('utils/script/compileCode', 'node', 'POST', {}) + node_post_utils_script_compilewithimports = nodePostUtilsScriptCompileWithImports = Entry('utils/script/compileWithImports', 'node', 'POST', {}) + node_post_utils_script_decompile = nodePostUtilsScriptDecompile = Entry('utils/script/decompile', 'node', 'POST', {}) + node_post_utils_script_estimate = nodePostUtilsScriptEstimate = Entry('utils/script/estimate', 'node', 'POST', {}) + node_post_utils_sign_privatekey = nodePostUtilsSignPrivateKey = Entry('utils/sign/{privateKey}', 'node', 'POST', {}) + node_post_utils_transactionsserialize = nodePostUtilsTransactionsSerialize = Entry('utils/transactionsSerialize', 'node', 'POST', {}) + node_delete_addresses_address = nodeDeleteAddressesAddress = Entry('addresses/{address}', 'node', 'DELETE', {}) + node_delete_debug_rollback_to_signature = nodeDeleteDebugRollbackToSignature = Entry('debug/rollback-to/{signature}', 'node', 'DELETE', {}) + public_get_assets = publicGetAssets = Entry('assets', 'public', 'GET', {}) + public_get_pairs = publicGetPairs = Entry('pairs', 'public', 'GET', {}) + public_get_candles_baseid_quoteid = publicGetCandlesBaseIdQuoteId = Entry('candles/{baseId}/{quoteId}', 'public', 'GET', {}) + public_get_transactions_exchange = publicGetTransactionsExchange = Entry('transactions/exchange', 'public', 'GET', {}) + private_get_deposit_addresses_currency = privateGetDepositAddressesCurrency = Entry('deposit/addresses/{currency}', 'private', 'GET', {}) + private_get_deposit_addresses_currency_platform = privateGetDepositAddressesCurrencyPlatform = Entry('deposit/addresses/{currency}/{platform}', 'private', 'GET', {}) + private_get_platforms = privateGetPlatforms = Entry('platforms', 'private', 'GET', {}) + private_get_deposit_currencies = privateGetDepositCurrencies = Entry('deposit/currencies', 'private', 'GET', {}) + private_get_withdraw_currencies = privateGetWithdrawCurrencies = Entry('withdraw/currencies', 'private', 'GET', {}) + private_get_withdraw_addresses_currency_address = privateGetWithdrawAddressesCurrencyAddress = Entry('withdraw/addresses/{currency}/{address}', 'private', 'GET', {}) + private_post_oauth2_token = privatePostOauth2Token = Entry('oauth2/token', 'private', 'POST', {}) + forward_get_matcher_orders_address = forwardGetMatcherOrdersAddress = Entry('matcher/orders/{address}', 'forward', 'GET', {}) + forward_get_matcher_orders_address_orderid = forwardGetMatcherOrdersAddressOrderId = Entry('matcher/orders/{address}/{orderId}', 'forward', 'GET', {}) + forward_post_matcher_orders_wavesaddress_cancel = forwardPostMatcherOrdersWavesAddressCancel = Entry('matcher/orders/{wavesAddress}/cancel', 'forward', 'POST', {}) + market_get_tickers = marketGetTickers = Entry('tickers', 'market', 'GET', {}) diff --git a/ccxt/abstract/whitebit.py b/ccxt/abstract/whitebit.py new file mode 100644 index 0000000..9988689 --- /dev/null +++ b/ccxt/abstract/whitebit.py @@ -0,0 +1,114 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + web_get_v1_healthcheck = webGetV1Healthcheck = Entry('v1/healthcheck', 'web', 'GET', {}) + v1_public_get_markets = v1PublicGetMarkets = Entry('markets', ['v1', 'public'], 'GET', {}) + v1_public_get_tickers = v1PublicGetTickers = Entry('tickers', ['v1', 'public'], 'GET', {}) + v1_public_get_ticker = v1PublicGetTicker = Entry('ticker', ['v1', 'public'], 'GET', {}) + v1_public_get_symbols = v1PublicGetSymbols = Entry('symbols', ['v1', 'public'], 'GET', {}) + v1_public_get_depth_result = v1PublicGetDepthResult = Entry('depth/result', ['v1', 'public'], 'GET', {}) + v1_public_get_history = v1PublicGetHistory = Entry('history', ['v1', 'public'], 'GET', {}) + v1_public_get_kline = v1PublicGetKline = Entry('kline', ['v1', 'public'], 'GET', {}) + v1_private_post_account_balance = v1PrivatePostAccountBalance = Entry('account/balance', ['v1', 'private'], 'POST', {}) + v1_private_post_order_new = v1PrivatePostOrderNew = Entry('order/new', ['v1', 'private'], 'POST', {}) + v1_private_post_order_cancel = v1PrivatePostOrderCancel = Entry('order/cancel', ['v1', 'private'], 'POST', {}) + v1_private_post_orders = v1PrivatePostOrders = Entry('orders', ['v1', 'private'], 'POST', {}) + v1_private_post_account_order_history = v1PrivatePostAccountOrderHistory = Entry('account/order_history', ['v1', 'private'], 'POST', {}) + v1_private_post_account_executed_history = v1PrivatePostAccountExecutedHistory = Entry('account/executed_history', ['v1', 'private'], 'POST', {}) + v1_private_post_account_executed_history_all = v1PrivatePostAccountExecutedHistoryAll = Entry('account/executed_history/all', ['v1', 'private'], 'POST', {}) + v1_private_post_account_order = v1PrivatePostAccountOrder = Entry('account/order', ['v1', 'private'], 'POST', {}) + v2_public_get_markets = v2PublicGetMarkets = Entry('markets', ['v2', 'public'], 'GET', {}) + v2_public_get_ticker = v2PublicGetTicker = Entry('ticker', ['v2', 'public'], 'GET', {}) + v2_public_get_assets = v2PublicGetAssets = Entry('assets', ['v2', 'public'], 'GET', {}) + v2_public_get_fee = v2PublicGetFee = Entry('fee', ['v2', 'public'], 'GET', {}) + v2_public_get_depth_market = v2PublicGetDepthMarket = Entry('depth/{market}', ['v2', 'public'], 'GET', {}) + v2_public_get_trades_market = v2PublicGetTradesMarket = Entry('trades/{market}', ['v2', 'public'], 'GET', {}) + v4_public_get_assets = v4PublicGetAssets = Entry('assets', ['v4', 'public'], 'GET', {}) + v4_public_get_collateral_markets = v4PublicGetCollateralMarkets = Entry('collateral/markets', ['v4', 'public'], 'GET', {}) + v4_public_get_fee = v4PublicGetFee = Entry('fee', ['v4', 'public'], 'GET', {}) + v4_public_get_orderbook_depth_market = v4PublicGetOrderbookDepthMarket = Entry('orderbook/depth/{market}', ['v4', 'public'], 'GET', {}) + v4_public_get_orderbook_market = v4PublicGetOrderbookMarket = Entry('orderbook/{market}', ['v4', 'public'], 'GET', {}) + v4_public_get_ticker = v4PublicGetTicker = Entry('ticker', ['v4', 'public'], 'GET', {}) + v4_public_get_trades_market = v4PublicGetTradesMarket = Entry('trades/{market}', ['v4', 'public'], 'GET', {}) + v4_public_get_time = v4PublicGetTime = Entry('time', ['v4', 'public'], 'GET', {}) + v4_public_get_ping = v4PublicGetPing = Entry('ping', ['v4', 'public'], 'GET', {}) + v4_public_get_markets = v4PublicGetMarkets = Entry('markets', ['v4', 'public'], 'GET', {}) + v4_public_get_futures = v4PublicGetFutures = Entry('futures', ['v4', 'public'], 'GET', {}) + v4_public_get_platform_status = v4PublicGetPlatformStatus = Entry('platform/status', ['v4', 'public'], 'GET', {}) + v4_public_get_mining_pool = v4PublicGetMiningPool = Entry('mining-pool', ['v4', 'public'], 'GET', {}) + v4_private_post_collateral_account_balance = v4PrivatePostCollateralAccountBalance = Entry('collateral-account/balance', ['v4', 'private'], 'POST', {}) + v4_private_post_collateral_account_balance_summary = v4PrivatePostCollateralAccountBalanceSummary = Entry('collateral-account/balance-summary', ['v4', 'private'], 'POST', {}) + v4_private_post_collateral_account_positions_history = v4PrivatePostCollateralAccountPositionsHistory = Entry('collateral-account/positions/history', ['v4', 'private'], 'POST', {}) + v4_private_post_collateral_account_leverage = v4PrivatePostCollateralAccountLeverage = Entry('collateral-account/leverage', ['v4', 'private'], 'POST', {}) + v4_private_post_collateral_account_positions_open = v4PrivatePostCollateralAccountPositionsOpen = Entry('collateral-account/positions/open', ['v4', 'private'], 'POST', {}) + v4_private_post_collateral_account_summary = v4PrivatePostCollateralAccountSummary = Entry('collateral-account/summary', ['v4', 'private'], 'POST', {}) + v4_private_post_collateral_account_funding_history = v4PrivatePostCollateralAccountFundingHistory = Entry('collateral-account/funding-history', ['v4', 'private'], 'POST', {}) + v4_private_post_main_account_address = v4PrivatePostMainAccountAddress = Entry('main-account/address', ['v4', 'private'], 'POST', {}) + v4_private_post_main_account_balance = v4PrivatePostMainAccountBalance = Entry('main-account/balance', ['v4', 'private'], 'POST', {}) + v4_private_post_main_account_create_new_address = v4PrivatePostMainAccountCreateNewAddress = Entry('main-account/create-new-address', ['v4', 'private'], 'POST', {}) + v4_private_post_main_account_codes = v4PrivatePostMainAccountCodes = Entry('main-account/codes', ['v4', 'private'], 'POST', {}) + v4_private_post_main_account_codes_apply = v4PrivatePostMainAccountCodesApply = Entry('main-account/codes/apply', ['v4', 'private'], 'POST', {}) + v4_private_post_main_account_codes_my = v4PrivatePostMainAccountCodesMy = Entry('main-account/codes/my', ['v4', 'private'], 'POST', {}) + v4_private_post_main_account_codes_history = v4PrivatePostMainAccountCodesHistory = Entry('main-account/codes/history', ['v4', 'private'], 'POST', {}) + v4_private_post_main_account_fiat_deposit_url = v4PrivatePostMainAccountFiatDepositUrl = Entry('main-account/fiat-deposit-url', ['v4', 'private'], 'POST', {}) + v4_private_post_main_account_history = v4PrivatePostMainAccountHistory = Entry('main-account/history', ['v4', 'private'], 'POST', {}) + v4_private_post_main_account_withdraw = v4PrivatePostMainAccountWithdraw = Entry('main-account/withdraw', ['v4', 'private'], 'POST', {}) + v4_private_post_main_account_withdraw_pay = v4PrivatePostMainAccountWithdrawPay = Entry('main-account/withdraw-pay', ['v4', 'private'], 'POST', {}) + v4_private_post_main_account_transfer = v4PrivatePostMainAccountTransfer = Entry('main-account/transfer', ['v4', 'private'], 'POST', {}) + v4_private_post_main_account_smart_plans = v4PrivatePostMainAccountSmartPlans = Entry('main-account/smart/plans', ['v4', 'private'], 'POST', {}) + v4_private_post_main_account_smart_investment = v4PrivatePostMainAccountSmartInvestment = Entry('main-account/smart/investment', ['v4', 'private'], 'POST', {}) + v4_private_post_main_account_smart_investment_close = v4PrivatePostMainAccountSmartInvestmentClose = Entry('main-account/smart/investment/close', ['v4', 'private'], 'POST', {}) + v4_private_post_main_account_smart_investments = v4PrivatePostMainAccountSmartInvestments = Entry('main-account/smart/investments', ['v4', 'private'], 'POST', {}) + v4_private_post_main_account_fee = v4PrivatePostMainAccountFee = Entry('main-account/fee', ['v4', 'private'], 'POST', {}) + v4_private_post_main_account_smart_interest_payment_history = v4PrivatePostMainAccountSmartInterestPaymentHistory = Entry('main-account/smart/interest-payment-history', ['v4', 'private'], 'POST', {}) + v4_private_post_trade_account_balance = v4PrivatePostTradeAccountBalance = Entry('trade-account/balance', ['v4', 'private'], 'POST', {}) + v4_private_post_trade_account_executed_history = v4PrivatePostTradeAccountExecutedHistory = Entry('trade-account/executed-history', ['v4', 'private'], 'POST', {}) + v4_private_post_trade_account_order_history = v4PrivatePostTradeAccountOrderHistory = Entry('trade-account/order/history', ['v4', 'private'], 'POST', {}) + v4_private_post_trade_account_order = v4PrivatePostTradeAccountOrder = Entry('trade-account/order', ['v4', 'private'], 'POST', {}) + v4_private_post_order_collateral_limit = v4PrivatePostOrderCollateralLimit = Entry('order/collateral/limit', ['v4', 'private'], 'POST', {}) + v4_private_post_order_collateral_market = v4PrivatePostOrderCollateralMarket = Entry('order/collateral/market', ['v4', 'private'], 'POST', {}) + v4_private_post_order_collateral_stop_limit = v4PrivatePostOrderCollateralStopLimit = Entry('order/collateral/stop-limit', ['v4', 'private'], 'POST', {}) + v4_private_post_order_collateral_trigger_market = v4PrivatePostOrderCollateralTriggerMarket = Entry('order/collateral/trigger-market', ['v4', 'private'], 'POST', {}) + v4_private_post_order_collateral_bulk = v4PrivatePostOrderCollateralBulk = Entry('order/collateral/bulk', ['v4', 'private'], 'POST', {}) + v4_private_post_order_new = v4PrivatePostOrderNew = Entry('order/new', ['v4', 'private'], 'POST', {}) + v4_private_post_order_market = v4PrivatePostOrderMarket = Entry('order/market', ['v4', 'private'], 'POST', {}) + v4_private_post_order_stock_market = v4PrivatePostOrderStockMarket = Entry('order/stock_market', ['v4', 'private'], 'POST', {}) + v4_private_post_order_stop_limit = v4PrivatePostOrderStopLimit = Entry('order/stop_limit', ['v4', 'private'], 'POST', {}) + v4_private_post_order_stop_market = v4PrivatePostOrderStopMarket = Entry('order/stop_market', ['v4', 'private'], 'POST', {}) + v4_private_post_order_cancel = v4PrivatePostOrderCancel = Entry('order/cancel', ['v4', 'private'], 'POST', {}) + v4_private_post_order_cancel_all = v4PrivatePostOrderCancelAll = Entry('order/cancel/all', ['v4', 'private'], 'POST', {}) + v4_private_post_order_kill_switch = v4PrivatePostOrderKillSwitch = Entry('order/kill-switch', ['v4', 'private'], 'POST', {}) + v4_private_post_order_kill_switch_status = v4PrivatePostOrderKillSwitchStatus = Entry('order/kill-switch/status', ['v4', 'private'], 'POST', {}) + v4_private_post_order_bulk = v4PrivatePostOrderBulk = Entry('order/bulk', ['v4', 'private'], 'POST', {}) + v4_private_post_order_modify = v4PrivatePostOrderModify = Entry('order/modify', ['v4', 'private'], 'POST', {}) + v4_private_post_order_conditional_cancel = v4PrivatePostOrderConditionalCancel = Entry('order/conditional-cancel', ['v4', 'private'], 'POST', {}) + v4_private_post_orders = v4PrivatePostOrders = Entry('orders', ['v4', 'private'], 'POST', {}) + v4_private_post_oco_orders = v4PrivatePostOcoOrders = Entry('oco-orders', ['v4', 'private'], 'POST', {}) + v4_private_post_order_collateral_oco = v4PrivatePostOrderCollateralOco = Entry('order/collateral/oco', ['v4', 'private'], 'POST', {}) + v4_private_post_order_oco_cancel = v4PrivatePostOrderOcoCancel = Entry('order/oco-cancel', ['v4', 'private'], 'POST', {}) + v4_private_post_order_oto_cancel = v4PrivatePostOrderOtoCancel = Entry('order/oto-cancel', ['v4', 'private'], 'POST', {}) + v4_private_post_profile_websocket_token = v4PrivatePostProfileWebsocketToken = Entry('profile/websocket_token', ['v4', 'private'], 'POST', {}) + v4_private_post_convert_estimate = v4PrivatePostConvertEstimate = Entry('convert/estimate', ['v4', 'private'], 'POST', {}) + v4_private_post_convert_confirm = v4PrivatePostConvertConfirm = Entry('convert/confirm', ['v4', 'private'], 'POST', {}) + v4_private_post_convert_history = v4PrivatePostConvertHistory = Entry('convert/history', ['v4', 'private'], 'POST', {}) + v4_private_post_sub_account_create = v4PrivatePostSubAccountCreate = Entry('sub-account/create', ['v4', 'private'], 'POST', {}) + v4_private_post_sub_account_delete = v4PrivatePostSubAccountDelete = Entry('sub-account/delete', ['v4', 'private'], 'POST', {}) + v4_private_post_sub_account_edit = v4PrivatePostSubAccountEdit = Entry('sub-account/edit', ['v4', 'private'], 'POST', {}) + v4_private_post_sub_account_list = v4PrivatePostSubAccountList = Entry('sub-account/list', ['v4', 'private'], 'POST', {}) + v4_private_post_sub_account_transfer = v4PrivatePostSubAccountTransfer = Entry('sub-account/transfer', ['v4', 'private'], 'POST', {}) + v4_private_post_sub_account_block = v4PrivatePostSubAccountBlock = Entry('sub-account/block', ['v4', 'private'], 'POST', {}) + v4_private_post_sub_account_unblock = v4PrivatePostSubAccountUnblock = Entry('sub-account/unblock', ['v4', 'private'], 'POST', {}) + v4_private_post_sub_account_balances = v4PrivatePostSubAccountBalances = Entry('sub-account/balances', ['v4', 'private'], 'POST', {}) + v4_private_post_sub_account_transfer_history = v4PrivatePostSubAccountTransferHistory = Entry('sub-account/transfer/history', ['v4', 'private'], 'POST', {}) + v4_private_post_sub_account_api_key_create = v4PrivatePostSubAccountApiKeyCreate = Entry('sub-account/api-key/create', ['v4', 'private'], 'POST', {}) + v4_private_post_sub_account_api_key_edit = v4PrivatePostSubAccountApiKeyEdit = Entry('sub-account/api-key/edit', ['v4', 'private'], 'POST', {}) + v4_private_post_sub_account_api_key_delete = v4PrivatePostSubAccountApiKeyDelete = Entry('sub-account/api-key/delete', ['v4', 'private'], 'POST', {}) + v4_private_post_sub_account_api_key_list = v4PrivatePostSubAccountApiKeyList = Entry('sub-account/api-key/list', ['v4', 'private'], 'POST', {}) + v4_private_post_sub_account_api_key_reset = v4PrivatePostSubAccountApiKeyReset = Entry('sub-account/api-key/reset', ['v4', 'private'], 'POST', {}) + v4_private_post_sub_account_api_key_ip_address_list = v4PrivatePostSubAccountApiKeyIpAddressList = Entry('sub-account/api-key/ip-address/list', ['v4', 'private'], 'POST', {}) + v4_private_post_sub_account_api_key_ip_address_create = v4PrivatePostSubAccountApiKeyIpAddressCreate = Entry('sub-account/api-key/ip-address/create', ['v4', 'private'], 'POST', {}) + v4_private_post_sub_account_api_key_ip_address_delete = v4PrivatePostSubAccountApiKeyIpAddressDelete = Entry('sub-account/api-key/ip-address/delete', ['v4', 'private'], 'POST', {}) + v4_private_post_mining_rewards = v4PrivatePostMiningRewards = Entry('mining/rewards', ['v4', 'private'], 'POST', {}) + v4_private_post_market_fee = v4PrivatePostMarketFee = Entry('market/fee', ['v4', 'private'], 'POST', {}) + v4_private_post_conditional_orders = v4PrivatePostConditionalOrders = Entry('conditional-orders', ['v4', 'private'], 'POST', {}) diff --git a/ccxt/abstract/woo.py b/ccxt/abstract/woo.py new file mode 100644 index 0000000..555d624 --- /dev/null +++ b/ccxt/abstract/woo.py @@ -0,0 +1,138 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + v1_pub_get_hist_kline = v1PubGetHistKline = Entry('hist/kline', ['v1', 'pub'], 'GET', {'cost': 10}) + v1_pub_get_hist_trades = v1PubGetHistTrades = Entry('hist/trades', ['v1', 'pub'], 'GET', {'cost': 10}) + v1_public_get_info = v1PublicGetInfo = Entry('info', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_info_symbol = v1PublicGetInfoSymbol = Entry('info/{symbol}', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_system_info = v1PublicGetSystemInfo = Entry('system_info', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_market_trades = v1PublicGetMarketTrades = Entry('market_trades', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_token = v1PublicGetToken = Entry('token', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_token_network = v1PublicGetTokenNetwork = Entry('token_network', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_funding_rates = v1PublicGetFundingRates = Entry('funding_rates', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_funding_rate_symbol = v1PublicGetFundingRateSymbol = Entry('funding_rate/{symbol}', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_funding_rate_history = v1PublicGetFundingRateHistory = Entry('funding_rate_history', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_futures = v1PublicGetFutures = Entry('futures', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_futures_symbol = v1PublicGetFuturesSymbol = Entry('futures/{symbol}', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_orderbook_symbol = v1PublicGetOrderbookSymbol = Entry('orderbook/{symbol}', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_kline = v1PublicGetKline = Entry('kline', ['v1', 'public'], 'GET', {'cost': 1}) + v1_private_get_client_token = v1PrivateGetClientToken = Entry('client/token', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_order_oid = v1PrivateGetOrderOid = Entry('order/{oid}', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_client_order_client_order_id = v1PrivateGetClientOrderClientOrderId = Entry('client/order/{client_order_id}', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_orders = v1PrivateGetOrders = Entry('orders', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_client_trade_tid = v1PrivateGetClientTradeTid = Entry('client/trade/{tid}', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_order_oid_trades = v1PrivateGetOrderOidTrades = Entry('order/{oid}/trades', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_client_trades = v1PrivateGetClientTrades = Entry('client/trades', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_client_hist_trades = v1PrivateGetClientHistTrades = Entry('client/hist_trades', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_staking_yield_history = v1PrivateGetStakingYieldHistory = Entry('staking/yield_history', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_client_holding = v1PrivateGetClientHolding = Entry('client/holding', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_asset_deposit = v1PrivateGetAssetDeposit = Entry('asset/deposit', ['v1', 'private'], 'GET', {'cost': 10}) + v1_private_get_asset_history = v1PrivateGetAssetHistory = Entry('asset/history', ['v1', 'private'], 'GET', {'cost': 60}) + v1_private_get_sub_account_all = v1PrivateGetSubAccountAll = Entry('sub_account/all', ['v1', 'private'], 'GET', {'cost': 60}) + v1_private_get_sub_account_assets = v1PrivateGetSubAccountAssets = Entry('sub_account/assets', ['v1', 'private'], 'GET', {'cost': 60}) + v1_private_get_sub_account_asset_detail = v1PrivateGetSubAccountAssetDetail = Entry('sub_account/asset_detail', ['v1', 'private'], 'GET', {'cost': 60}) + v1_private_get_sub_account_ip_restriction = v1PrivateGetSubAccountIpRestriction = Entry('sub_account/ip_restriction', ['v1', 'private'], 'GET', {'cost': 10}) + v1_private_get_asset_main_sub_transfer_history = v1PrivateGetAssetMainSubTransferHistory = Entry('asset/main_sub_transfer_history', ['v1', 'private'], 'GET', {'cost': 30}) + v1_private_get_token_interest = v1PrivateGetTokenInterest = Entry('token_interest', ['v1', 'private'], 'GET', {'cost': 60}) + v1_private_get_token_interest_token = v1PrivateGetTokenInterestToken = Entry('token_interest/{token}', ['v1', 'private'], 'GET', {'cost': 60}) + v1_private_get_interest_history = v1PrivateGetInterestHistory = Entry('interest/history', ['v1', 'private'], 'GET', {'cost': 60}) + v1_private_get_interest_repay = v1PrivateGetInterestRepay = Entry('interest/repay', ['v1', 'private'], 'GET', {'cost': 60}) + v1_private_get_funding_fee_history = v1PrivateGetFundingFeeHistory = Entry('funding_fee/history', ['v1', 'private'], 'GET', {'cost': 30}) + v1_private_get_positions = v1PrivateGetPositions = Entry('positions', ['v1', 'private'], 'GET', {'cost': 3.33}) + v1_private_get_position_symbol = v1PrivateGetPositionSymbol = Entry('position/{symbol}', ['v1', 'private'], 'GET', {'cost': 3.33}) + v1_private_get_client_transaction_history = v1PrivateGetClientTransactionHistory = Entry('client/transaction_history', ['v1', 'private'], 'GET', {'cost': 60}) + v1_private_get_client_futures_leverage = v1PrivateGetClientFuturesLeverage = Entry('client/futures_leverage', ['v1', 'private'], 'GET', {'cost': 60}) + v1_private_post_order = v1PrivatePostOrder = Entry('order', ['v1', 'private'], 'POST', {'cost': 1}) + v1_private_post_order_cancel_all_after = v1PrivatePostOrderCancelAllAfter = Entry('order/cancel_all_after', ['v1', 'private'], 'POST', {'cost': 1}) + v1_private_post_asset_ltv = v1PrivatePostAssetLtv = Entry('asset/ltv', ['v1', 'private'], 'POST', {'cost': 30}) + v1_private_post_asset_internal_withdraw = v1PrivatePostAssetInternalWithdraw = Entry('asset/internal_withdraw', ['v1', 'private'], 'POST', {'cost': 30}) + v1_private_post_interest_repay = v1PrivatePostInterestRepay = Entry('interest/repay', ['v1', 'private'], 'POST', {'cost': 60}) + v1_private_post_client_account_mode = v1PrivatePostClientAccountMode = Entry('client/account_mode', ['v1', 'private'], 'POST', {'cost': 120}) + v1_private_post_client_position_mode = v1PrivatePostClientPositionMode = Entry('client/position_mode', ['v1', 'private'], 'POST', {'cost': 5}) + v1_private_post_client_leverage = v1PrivatePostClientLeverage = Entry('client/leverage', ['v1', 'private'], 'POST', {'cost': 120}) + v1_private_post_client_futures_leverage = v1PrivatePostClientFuturesLeverage = Entry('client/futures_leverage', ['v1', 'private'], 'POST', {'cost': 30}) + v1_private_post_client_isolated_margin = v1PrivatePostClientIsolatedMargin = Entry('client/isolated_margin', ['v1', 'private'], 'POST', {'cost': 30}) + v1_private_delete_order = v1PrivateDeleteOrder = Entry('order', ['v1', 'private'], 'DELETE', {'cost': 1}) + v1_private_delete_client_order = v1PrivateDeleteClientOrder = Entry('client/order', ['v1', 'private'], 'DELETE', {'cost': 1}) + v1_private_delete_orders = v1PrivateDeleteOrders = Entry('orders', ['v1', 'private'], 'DELETE', {'cost': 1}) + v1_private_delete_asset_withdraw = v1PrivateDeleteAssetWithdraw = Entry('asset/withdraw', ['v1', 'private'], 'DELETE', {'cost': 120}) + v2_private_get_client_holding = v2PrivateGetClientHolding = Entry('client/holding', ['v2', 'private'], 'GET', {'cost': 1}) + v3_public_get_systeminfo = v3PublicGetSystemInfo = Entry('systemInfo', ['v3', 'public'], 'GET', {'cost': 1}) + v3_public_get_instruments = v3PublicGetInstruments = Entry('instruments', ['v3', 'public'], 'GET', {'cost': 1}) + v3_public_get_token = v3PublicGetToken = Entry('token', ['v3', 'public'], 'GET', {'cost': 1}) + v3_public_get_tokennetwork = v3PublicGetTokenNetwork = Entry('tokenNetwork', ['v3', 'public'], 'GET', {'cost': 1}) + v3_public_get_tokeninfo = v3PublicGetTokenInfo = Entry('tokenInfo', ['v3', 'public'], 'GET', {'cost': 1}) + v3_public_get_markettrades = v3PublicGetMarketTrades = Entry('marketTrades', ['v3', 'public'], 'GET', {'cost': 1}) + v3_public_get_markettradeshistory = v3PublicGetMarketTradesHistory = Entry('marketTradesHistory', ['v3', 'public'], 'GET', {'cost': 1}) + v3_public_get_orderbook = v3PublicGetOrderbook = Entry('orderbook', ['v3', 'public'], 'GET', {'cost': 1}) + v3_public_get_kline = v3PublicGetKline = Entry('kline', ['v3', 'public'], 'GET', {'cost': 1}) + v3_public_get_klinehistory = v3PublicGetKlineHistory = Entry('klineHistory', ['v3', 'public'], 'GET', {'cost': 1}) + v3_public_get_futures = v3PublicGetFutures = Entry('futures', ['v3', 'public'], 'GET', {'cost': 1}) + v3_public_get_fundingrate = v3PublicGetFundingRate = Entry('fundingRate', ['v3', 'public'], 'GET', {'cost': 1}) + v3_public_get_fundingratehistory = v3PublicGetFundingRateHistory = Entry('fundingRateHistory', ['v3', 'public'], 'GET', {'cost': 1}) + v3_public_get_insurancefund = v3PublicGetInsuranceFund = Entry('insuranceFund', ['v3', 'public'], 'GET', {'cost': 1}) + v3_private_get_trade_order = v3PrivateGetTradeOrder = Entry('trade/order', ['v3', 'private'], 'GET', {'cost': 2}) + v3_private_get_trade_orders = v3PrivateGetTradeOrders = Entry('trade/orders', ['v3', 'private'], 'GET', {'cost': 1}) + v3_private_get_trade_algoorder = v3PrivateGetTradeAlgoOrder = Entry('trade/algoOrder', ['v3', 'private'], 'GET', {'cost': 1}) + v3_private_get_trade_algoorders = v3PrivateGetTradeAlgoOrders = Entry('trade/algoOrders', ['v3', 'private'], 'GET', {'cost': 1}) + v3_private_get_trade_transaction = v3PrivateGetTradeTransaction = Entry('trade/transaction', ['v3', 'private'], 'GET', {'cost': 1}) + v3_private_get_trade_transactionhistory = v3PrivateGetTradeTransactionHistory = Entry('trade/transactionHistory', ['v3', 'private'], 'GET', {'cost': 5}) + v3_private_get_trade_tradingfee = v3PrivateGetTradeTradingFee = Entry('trade/tradingFee', ['v3', 'private'], 'GET', {'cost': 5}) + v3_private_get_account_info = v3PrivateGetAccountInfo = Entry('account/info', ['v3', 'private'], 'GET', {'cost': 60}) + v3_private_get_account_tokenconfig = v3PrivateGetAccountTokenConfig = Entry('account/tokenConfig', ['v3', 'private'], 'GET', {'cost': 1}) + v3_private_get_account_symbolconfig = v3PrivateGetAccountSymbolConfig = Entry('account/symbolConfig', ['v3', 'private'], 'GET', {'cost': 1}) + v3_private_get_account_subaccounts_all = v3PrivateGetAccountSubAccountsAll = Entry('account/subAccounts/all', ['v3', 'private'], 'GET', {'cost': 60}) + v3_private_get_account_referral_summary = v3PrivateGetAccountReferralSummary = Entry('account/referral/summary', ['v3', 'private'], 'GET', {'cost': 60}) + v3_private_get_account_referral_rewardhistory = v3PrivateGetAccountReferralRewardHistory = Entry('account/referral/rewardHistory', ['v3', 'private'], 'GET', {'cost': 60}) + v3_private_get_account_credentials = v3PrivateGetAccountCredentials = Entry('account/credentials', ['v3', 'private'], 'GET', {'cost': 60}) + v3_private_get_asset_balances = v3PrivateGetAssetBalances = Entry('asset/balances', ['v3', 'private'], 'GET', {'cost': 1}) + v3_private_get_asset_token_history = v3PrivateGetAssetTokenHistory = Entry('asset/token/history', ['v3', 'private'], 'GET', {'cost': 60}) + v3_private_get_asset_transfer_history = v3PrivateGetAssetTransferHistory = Entry('asset/transfer/history', ['v3', 'private'], 'GET', {'cost': 30}) + v3_private_get_asset_wallet_history = v3PrivateGetAssetWalletHistory = Entry('asset/wallet/history', ['v3', 'private'], 'GET', {'cost': 60}) + v3_private_get_asset_wallet_deposit = v3PrivateGetAssetWalletDeposit = Entry('asset/wallet/deposit', ['v3', 'private'], 'GET', {'cost': 60}) + v3_private_get_asset_staking_yieldhistory = v3PrivateGetAssetStakingYieldHistory = Entry('asset/staking/yieldHistory', ['v3', 'private'], 'GET', {'cost': 60}) + v3_private_get_futures_positions = v3PrivateGetFuturesPositions = Entry('futures/positions', ['v3', 'private'], 'GET', {'cost': 3.33}) + v3_private_get_futures_leverage = v3PrivateGetFuturesLeverage = Entry('futures/leverage', ['v3', 'private'], 'GET', {'cost': 60}) + v3_private_get_futures_defaultmarginmode = v3PrivateGetFuturesDefaultMarginMode = Entry('futures/defaultMarginMode', ['v3', 'private'], 'GET', {'cost': 60}) + v3_private_get_futures_fundingfee_history = v3PrivateGetFuturesFundingFeeHistory = Entry('futures/fundingFee/history', ['v3', 'private'], 'GET', {'cost': 30}) + v3_private_get_spotmargin_interestrate = v3PrivateGetSpotMarginInterestRate = Entry('spotMargin/interestRate', ['v3', 'private'], 'GET', {'cost': 60}) + v3_private_get_spotmargin_interesthistory = v3PrivateGetSpotMarginInterestHistory = Entry('spotMargin/interestHistory', ['v3', 'private'], 'GET', {'cost': 60}) + v3_private_get_spotmargin_maxmargin = v3PrivateGetSpotMarginMaxMargin = Entry('spotMargin/maxMargin', ['v3', 'private'], 'GET', {'cost': 60}) + v3_private_get_algo_order_oid = v3PrivateGetAlgoOrderOid = Entry('algo/order/{oid}', ['v3', 'private'], 'GET', {'cost': 1}) + v3_private_get_algo_orders = v3PrivateGetAlgoOrders = Entry('algo/orders', ['v3', 'private'], 'GET', {'cost': 1}) + v3_private_get_positions = v3PrivateGetPositions = Entry('positions', ['v3', 'private'], 'GET', {'cost': 3.33}) + v3_private_get_buypower = v3PrivateGetBuypower = Entry('buypower', ['v3', 'private'], 'GET', {'cost': 1}) + v3_private_get_convert_exchangeinfo = v3PrivateGetConvertExchangeInfo = Entry('convert/exchangeInfo', ['v3', 'private'], 'GET', {'cost': 1}) + v3_private_get_convert_assetinfo = v3PrivateGetConvertAssetInfo = Entry('convert/assetInfo', ['v3', 'private'], 'GET', {'cost': 1}) + v3_private_get_convert_rfq = v3PrivateGetConvertRfq = Entry('convert/rfq', ['v3', 'private'], 'GET', {'cost': 60}) + v3_private_get_convert_trade = v3PrivateGetConvertTrade = Entry('convert/trade', ['v3', 'private'], 'GET', {'cost': 1}) + v3_private_get_convert_trades = v3PrivateGetConvertTrades = Entry('convert/trades', ['v3', 'private'], 'GET', {'cost': 1}) + v3_private_post_trade_order = v3PrivatePostTradeOrder = Entry('trade/order', ['v3', 'private'], 'POST', {'cost': 2}) + v3_private_post_trade_algoorder = v3PrivatePostTradeAlgoOrder = Entry('trade/algoOrder', ['v3', 'private'], 'POST', {'cost': 5}) + v3_private_post_trade_cancelallafter = v3PrivatePostTradeCancelAllAfter = Entry('trade/cancelAllAfter', ['v3', 'private'], 'POST', {'cost': 1}) + v3_private_post_account_tradingmode = v3PrivatePostAccountTradingMode = Entry('account/tradingMode', ['v3', 'private'], 'POST', {'cost': 120}) + v3_private_post_account_listenkey = v3PrivatePostAccountListenKey = Entry('account/listenKey', ['v3', 'private'], 'POST', {'cost': 20}) + v3_private_post_asset_transfer = v3PrivatePostAssetTransfer = Entry('asset/transfer', ['v3', 'private'], 'POST', {'cost': 30}) + v3_private_post_asset_wallet_withdraw = v3PrivatePostAssetWalletWithdraw = Entry('asset/wallet/withdraw', ['v3', 'private'], 'POST', {'cost': 60}) + v3_private_post_spotmargin_leverage = v3PrivatePostSpotMarginLeverage = Entry('spotMargin/leverage', ['v3', 'private'], 'POST', {'cost': 120}) + v3_private_post_spotmargin_interestrepay = v3PrivatePostSpotMarginInterestRepay = Entry('spotMargin/interestRepay', ['v3', 'private'], 'POST', {'cost': 60}) + v3_private_post_algo_order = v3PrivatePostAlgoOrder = Entry('algo/order', ['v3', 'private'], 'POST', {'cost': 5}) + v3_private_post_convert_rft = v3PrivatePostConvertRft = Entry('convert/rft', ['v3', 'private'], 'POST', {'cost': 60}) + v3_private_put_trade_order = v3PrivatePutTradeOrder = Entry('trade/order', ['v3', 'private'], 'PUT', {'cost': 2}) + v3_private_put_trade_algoorder = v3PrivatePutTradeAlgoOrder = Entry('trade/algoOrder', ['v3', 'private'], 'PUT', {'cost': 2}) + v3_private_put_futures_leverage = v3PrivatePutFuturesLeverage = Entry('futures/leverage', ['v3', 'private'], 'PUT', {'cost': 60}) + v3_private_put_futures_positionmode = v3PrivatePutFuturesPositionMode = Entry('futures/positionMode', ['v3', 'private'], 'PUT', {'cost': 120}) + v3_private_put_order_oid = v3PrivatePutOrderOid = Entry('order/{oid}', ['v3', 'private'], 'PUT', {'cost': 2}) + v3_private_put_order_client_client_order_id = v3PrivatePutOrderClientClientOrderId = Entry('order/client/{client_order_id}', ['v3', 'private'], 'PUT', {'cost': 2}) + v3_private_put_algo_order_oid = v3PrivatePutAlgoOrderOid = Entry('algo/order/{oid}', ['v3', 'private'], 'PUT', {'cost': 2}) + v3_private_put_algo_order_client_client_order_id = v3PrivatePutAlgoOrderClientClientOrderId = Entry('algo/order/client/{client_order_id}', ['v3', 'private'], 'PUT', {'cost': 2}) + v3_private_delete_trade_order = v3PrivateDeleteTradeOrder = Entry('trade/order', ['v3', 'private'], 'DELETE', {'cost': 1}) + v3_private_delete_trade_orders = v3PrivateDeleteTradeOrders = Entry('trade/orders', ['v3', 'private'], 'DELETE', {'cost': 1}) + v3_private_delete_trade_algoorder = v3PrivateDeleteTradeAlgoOrder = Entry('trade/algoOrder', ['v3', 'private'], 'DELETE', {'cost': 1}) + v3_private_delete_trade_algoorders = v3PrivateDeleteTradeAlgoOrders = Entry('trade/algoOrders', ['v3', 'private'], 'DELETE', {'cost': 1}) + v3_private_delete_trade_allorders = v3PrivateDeleteTradeAllOrders = Entry('trade/allOrders', ['v3', 'private'], 'DELETE', {'cost': 1}) + v3_private_delete_algo_order_order_id = v3PrivateDeleteAlgoOrderOrderId = Entry('algo/order/{order_id}', ['v3', 'private'], 'DELETE', {'cost': 1}) + v3_private_delete_algo_orders_pending = v3PrivateDeleteAlgoOrdersPending = Entry('algo/orders/pending', ['v3', 'private'], 'DELETE', {'cost': 1}) + v3_private_delete_algo_orders_pending_symbol = v3PrivateDeleteAlgoOrdersPendingSymbol = Entry('algo/orders/pending/{symbol}', ['v3', 'private'], 'DELETE', {'cost': 1}) + v3_private_delete_orders_pending = v3PrivateDeleteOrdersPending = Entry('orders/pending', ['v3', 'private'], 'DELETE', {'cost': 1}) diff --git a/ccxt/abstract/woofipro.py b/ccxt/abstract/woofipro.py new file mode 100644 index 0000000..ff60ce5 --- /dev/null +++ b/ccxt/abstract/woofipro.py @@ -0,0 +1,119 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + v1_public_get_public_volume_stats = v1PublicGetPublicVolumeStats = Entry('public/volume/stats', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_public_broker_name = v1PublicGetPublicBrokerName = Entry('public/broker/name', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_public_chain_info_broker_id = v1PublicGetPublicChainInfoBrokerId = Entry('public/chain_info/{broker_id}', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_public_system_info = v1PublicGetPublicSystemInfo = Entry('public/system_info', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_public_vault_balance = v1PublicGetPublicVaultBalance = Entry('public/vault_balance', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_public_insurancefund = v1PublicGetPublicInsurancefund = Entry('public/insurancefund', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_public_chain_info = v1PublicGetPublicChainInfo = Entry('public/chain_info', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_faucet_usdc = v1PublicGetFaucetUsdc = Entry('faucet/usdc', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_public_account = v1PublicGetPublicAccount = Entry('public/account', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_get_account = v1PublicGetGetAccount = Entry('get_account', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_registration_nonce = v1PublicGetRegistrationNonce = Entry('registration_nonce', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_get_orderly_key = v1PublicGetGetOrderlyKey = Entry('get_orderly_key', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_public_liquidation = v1PublicGetPublicLiquidation = Entry('public/liquidation', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_public_liquidated_positions = v1PublicGetPublicLiquidatedPositions = Entry('public/liquidated_positions', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_public_config = v1PublicGetPublicConfig = Entry('public/config', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_public_campaign_ranking = v1PublicGetPublicCampaignRanking = Entry('public/campaign/ranking', ['v1', 'public'], 'GET', {'cost': 10}) + v1_public_get_public_campaign_stats = v1PublicGetPublicCampaignStats = Entry('public/campaign/stats', ['v1', 'public'], 'GET', {'cost': 10}) + v1_public_get_public_campaign_user = v1PublicGetPublicCampaignUser = Entry('public/campaign/user', ['v1', 'public'], 'GET', {'cost': 10}) + v1_public_get_public_campaign_stats_details = v1PublicGetPublicCampaignStatsDetails = Entry('public/campaign/stats/details', ['v1', 'public'], 'GET', {'cost': 10}) + v1_public_get_public_campaigns = v1PublicGetPublicCampaigns = Entry('public/campaigns', ['v1', 'public'], 'GET', {'cost': 10}) + v1_public_get_public_points_leaderboard = v1PublicGetPublicPointsLeaderboard = Entry('public/points/leaderboard', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_client_points = v1PublicGetClientPoints = Entry('client/points', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_public_points_epoch = v1PublicGetPublicPointsEpoch = Entry('public/points/epoch', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_public_points_epoch_dates = v1PublicGetPublicPointsEpochDates = Entry('public/points/epoch_dates', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_public_referral_check_ref_code = v1PublicGetPublicReferralCheckRefCode = Entry('public/referral/check_ref_code', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_public_referral_verify_ref_code = v1PublicGetPublicReferralVerifyRefCode = Entry('public/referral/verify_ref_code', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_referral_admin_info = v1PublicGetReferralAdminInfo = Entry('referral/admin_info', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_referral_info = v1PublicGetReferralInfo = Entry('referral/info', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_referral_referee_info = v1PublicGetReferralRefereeInfo = Entry('referral/referee_info', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_referral_referee_rebate_summary = v1PublicGetReferralRefereeRebateSummary = Entry('referral/referee_rebate_summary', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_referral_referee_history = v1PublicGetReferralRefereeHistory = Entry('referral/referee_history', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_referral_referral_history = v1PublicGetReferralReferralHistory = Entry('referral/referral_history', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_referral_rebate_summary = v1PublicGetReferralRebateSummary = Entry('referral/rebate_summary', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_client_distribution_history = v1PublicGetClientDistributionHistory = Entry('client/distribution_history', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_tv_config = v1PublicGetTvConfig = Entry('tv/config', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_tv_history = v1PublicGetTvHistory = Entry('tv/history', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_tv_symbol_info = v1PublicGetTvSymbolInfo = Entry('tv/symbol_info', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_public_funding_rate_history = v1PublicGetPublicFundingRateHistory = Entry('public/funding_rate_history', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_public_funding_rate_symbol = v1PublicGetPublicFundingRateSymbol = Entry('public/funding_rate/{symbol}', ['v1', 'public'], 'GET', {'cost': 0.33}) + v1_public_get_public_funding_rates = v1PublicGetPublicFundingRates = Entry('public/funding_rates', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_public_info = v1PublicGetPublicInfo = Entry('public/info', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_public_info_symbol = v1PublicGetPublicInfoSymbol = Entry('public/info/{symbol}', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_public_market_trades = v1PublicGetPublicMarketTrades = Entry('public/market_trades', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_public_token = v1PublicGetPublicToken = Entry('public/token', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_public_futures = v1PublicGetPublicFutures = Entry('public/futures', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_get_public_futures_symbol = v1PublicGetPublicFuturesSymbol = Entry('public/futures/{symbol}', ['v1', 'public'], 'GET', {'cost': 1}) + v1_public_post_register_account = v1PublicPostRegisterAccount = Entry('register_account', ['v1', 'public'], 'POST', {'cost': 1}) + v1_private_get_client_key_info = v1PrivateGetClientKeyInfo = Entry('client/key_info', ['v1', 'private'], 'GET', {'cost': 6}) + v1_private_get_client_orderly_key_ip_restriction = v1PrivateGetClientOrderlyKeyIpRestriction = Entry('client/orderly_key_ip_restriction', ['v1', 'private'], 'GET', {'cost': 6}) + v1_private_get_order_oid = v1PrivateGetOrderOid = Entry('order/{oid}', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_client_order_client_order_id = v1PrivateGetClientOrderClientOrderId = Entry('client/order/{client_order_id}', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_algo_order_oid = v1PrivateGetAlgoOrderOid = Entry('algo/order/{oid}', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_algo_client_order_client_order_id = v1PrivateGetAlgoClientOrderClientOrderId = Entry('algo/client/order/{client_order_id}', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_orders = v1PrivateGetOrders = Entry('orders', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_algo_orders = v1PrivateGetAlgoOrders = Entry('algo/orders', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_trade_tid = v1PrivateGetTradeTid = Entry('trade/{tid}', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_trades = v1PrivateGetTrades = Entry('trades', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_order_oid_trades = v1PrivateGetOrderOidTrades = Entry('order/{oid}/trades', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_client_liquidator_liquidations = v1PrivateGetClientLiquidatorLiquidations = Entry('client/liquidator_liquidations', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_liquidations = v1PrivateGetLiquidations = Entry('liquidations', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_asset_history = v1PrivateGetAssetHistory = Entry('asset/history', ['v1', 'private'], 'GET', {'cost': 60}) + v1_private_get_client_holding = v1PrivateGetClientHolding = Entry('client/holding', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_withdraw_nonce = v1PrivateGetWithdrawNonce = Entry('withdraw_nonce', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_settle_nonce = v1PrivateGetSettleNonce = Entry('settle_nonce', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_pnl_settlement_history = v1PrivateGetPnlSettlementHistory = Entry('pnl_settlement/history', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_volume_user_daily = v1PrivateGetVolumeUserDaily = Entry('volume/user/daily', ['v1', 'private'], 'GET', {'cost': 60}) + v1_private_get_volume_user_stats = v1PrivateGetVolumeUserStats = Entry('volume/user/stats', ['v1', 'private'], 'GET', {'cost': 60}) + v1_private_get_client_statistics = v1PrivateGetClientStatistics = Entry('client/statistics', ['v1', 'private'], 'GET', {'cost': 60}) + v1_private_get_client_info = v1PrivateGetClientInfo = Entry('client/info', ['v1', 'private'], 'GET', {'cost': 60}) + v1_private_get_client_statistics_daily = v1PrivateGetClientStatisticsDaily = Entry('client/statistics/daily', ['v1', 'private'], 'GET', {'cost': 60}) + v1_private_get_positions = v1PrivateGetPositions = Entry('positions', ['v1', 'private'], 'GET', {'cost': 3.33}) + v1_private_get_position_symbol = v1PrivateGetPositionSymbol = Entry('position/{symbol}', ['v1', 'private'], 'GET', {'cost': 3.33}) + v1_private_get_funding_fee_history = v1PrivateGetFundingFeeHistory = Entry('funding_fee/history', ['v1', 'private'], 'GET', {'cost': 30}) + v1_private_get_notification_inbox_notifications = v1PrivateGetNotificationInboxNotifications = Entry('notification/inbox/notifications', ['v1', 'private'], 'GET', {'cost': 60}) + v1_private_get_notification_inbox_unread = v1PrivateGetNotificationInboxUnread = Entry('notification/inbox/unread', ['v1', 'private'], 'GET', {'cost': 60}) + v1_private_get_volume_broker_daily = v1PrivateGetVolumeBrokerDaily = Entry('volume/broker/daily', ['v1', 'private'], 'GET', {'cost': 60}) + v1_private_get_broker_fee_rate_default = v1PrivateGetBrokerFeeRateDefault = Entry('broker/fee_rate/default', ['v1', 'private'], 'GET', {'cost': 10}) + v1_private_get_broker_user_info = v1PrivateGetBrokerUserInfo = Entry('broker/user_info', ['v1', 'private'], 'GET', {'cost': 10}) + v1_private_get_orderbook_symbol = v1PrivateGetOrderbookSymbol = Entry('orderbook/{symbol}', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_get_kline = v1PrivateGetKline = Entry('kline', ['v1', 'private'], 'GET', {'cost': 1}) + v1_private_post_orderly_key = v1PrivatePostOrderlyKey = Entry('orderly_key', ['v1', 'private'], 'POST', {'cost': 1}) + v1_private_post_client_set_orderly_key_ip_restriction = v1PrivatePostClientSetOrderlyKeyIpRestriction = Entry('client/set_orderly_key_ip_restriction', ['v1', 'private'], 'POST', {'cost': 6}) + v1_private_post_client_reset_orderly_key_ip_restriction = v1PrivatePostClientResetOrderlyKeyIpRestriction = Entry('client/reset_orderly_key_ip_restriction', ['v1', 'private'], 'POST', {'cost': 6}) + v1_private_post_order = v1PrivatePostOrder = Entry('order', ['v1', 'private'], 'POST', {'cost': 1}) + v1_private_post_batch_order = v1PrivatePostBatchOrder = Entry('batch-order', ['v1', 'private'], 'POST', {'cost': 10}) + v1_private_post_algo_order = v1PrivatePostAlgoOrder = Entry('algo/order', ['v1', 'private'], 'POST', {'cost': 1}) + v1_private_post_liquidation = v1PrivatePostLiquidation = Entry('liquidation', ['v1', 'private'], 'POST', {'cost': 1}) + v1_private_post_claim_insurance_fund = v1PrivatePostClaimInsuranceFund = Entry('claim_insurance_fund', ['v1', 'private'], 'POST', {'cost': 1}) + v1_private_post_withdraw_request = v1PrivatePostWithdrawRequest = Entry('withdraw_request', ['v1', 'private'], 'POST', {'cost': 1}) + v1_private_post_settle_pnl = v1PrivatePostSettlePnl = Entry('settle_pnl', ['v1', 'private'], 'POST', {'cost': 1}) + v1_private_post_notification_inbox_mark_read = v1PrivatePostNotificationInboxMarkRead = Entry('notification/inbox/mark_read', ['v1', 'private'], 'POST', {'cost': 60}) + v1_private_post_notification_inbox_mark_read_all = v1PrivatePostNotificationInboxMarkReadAll = Entry('notification/inbox/mark_read_all', ['v1', 'private'], 'POST', {'cost': 60}) + v1_private_post_client_leverage = v1PrivatePostClientLeverage = Entry('client/leverage', ['v1', 'private'], 'POST', {'cost': 120}) + v1_private_post_client_maintenance_config = v1PrivatePostClientMaintenanceConfig = Entry('client/maintenance_config', ['v1', 'private'], 'POST', {'cost': 60}) + v1_private_post_delegate_signer = v1PrivatePostDelegateSigner = Entry('delegate_signer', ['v1', 'private'], 'POST', {'cost': 10}) + v1_private_post_delegate_orderly_key = v1PrivatePostDelegateOrderlyKey = Entry('delegate_orderly_key', ['v1', 'private'], 'POST', {'cost': 10}) + v1_private_post_delegate_settle_pnl = v1PrivatePostDelegateSettlePnl = Entry('delegate_settle_pnl', ['v1', 'private'], 'POST', {'cost': 10}) + v1_private_post_delegate_withdraw_request = v1PrivatePostDelegateWithdrawRequest = Entry('delegate_withdraw_request', ['v1', 'private'], 'POST', {'cost': 10}) + v1_private_post_broker_fee_rate_set = v1PrivatePostBrokerFeeRateSet = Entry('broker/fee_rate/set', ['v1', 'private'], 'POST', {'cost': 10}) + v1_private_post_broker_fee_rate_set_default = v1PrivatePostBrokerFeeRateSetDefault = Entry('broker/fee_rate/set_default', ['v1', 'private'], 'POST', {'cost': 10}) + v1_private_post_broker_fee_rate_default = v1PrivatePostBrokerFeeRateDefault = Entry('broker/fee_rate/default', ['v1', 'private'], 'POST', {'cost': 10}) + v1_private_post_referral_create = v1PrivatePostReferralCreate = Entry('referral/create', ['v1', 'private'], 'POST', {'cost': 10}) + v1_private_post_referral_update = v1PrivatePostReferralUpdate = Entry('referral/update', ['v1', 'private'], 'POST', {'cost': 10}) + v1_private_post_referral_bind = v1PrivatePostReferralBind = Entry('referral/bind', ['v1', 'private'], 'POST', {'cost': 10}) + v1_private_post_referral_edit_split = v1PrivatePostReferralEditSplit = Entry('referral/edit_split', ['v1', 'private'], 'POST', {'cost': 10}) + v1_private_put_order = v1PrivatePutOrder = Entry('order', ['v1', 'private'], 'PUT', {'cost': 1}) + v1_private_put_algo_order = v1PrivatePutAlgoOrder = Entry('algo/order', ['v1', 'private'], 'PUT', {'cost': 1}) + v1_private_delete_order = v1PrivateDeleteOrder = Entry('order', ['v1', 'private'], 'DELETE', {'cost': 1}) + v1_private_delete_algo_order = v1PrivateDeleteAlgoOrder = Entry('algo/order', ['v1', 'private'], 'DELETE', {'cost': 1}) + v1_private_delete_client_order = v1PrivateDeleteClientOrder = Entry('client/order', ['v1', 'private'], 'DELETE', {'cost': 1}) + v1_private_delete_algo_client_order = v1PrivateDeleteAlgoClientOrder = Entry('algo/client/order', ['v1', 'private'], 'DELETE', {'cost': 1}) + v1_private_delete_algo_orders = v1PrivateDeleteAlgoOrders = Entry('algo/orders', ['v1', 'private'], 'DELETE', {'cost': 1}) + v1_private_delete_orders = v1PrivateDeleteOrders = Entry('orders', ['v1', 'private'], 'DELETE', {'cost': 1}) + v1_private_delete_batch_order = v1PrivateDeleteBatchOrder = Entry('batch-order', ['v1', 'private'], 'DELETE', {'cost': 1}) + v1_private_delete_client_batch_order = v1PrivateDeleteClientBatchOrder = Entry('client/batch-order', ['v1', 'private'], 'DELETE', {'cost': 1}) diff --git a/ccxt/abstract/xt.py b/ccxt/abstract/xt.py new file mode 100644 index 0000000..27ae6d3 --- /dev/null +++ b/ccxt/abstract/xt.py @@ -0,0 +1,157 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_spot_get_currencies = publicSpotGetCurrencies = Entry('currencies', ['public', 'spot'], 'GET', {'cost': 1}) + public_spot_get_depth = publicSpotGetDepth = Entry('depth', ['public', 'spot'], 'GET', {'cost': 10}) + public_spot_get_kline = publicSpotGetKline = Entry('kline', ['public', 'spot'], 'GET', {'cost': 1}) + public_spot_get_symbol = publicSpotGetSymbol = Entry('symbol', ['public', 'spot'], 'GET', {'cost': 1}) + public_spot_get_ticker = publicSpotGetTicker = Entry('ticker', ['public', 'spot'], 'GET', {'cost': 1}) + public_spot_get_ticker_book = publicSpotGetTickerBook = Entry('ticker/book', ['public', 'spot'], 'GET', {'cost': 1}) + public_spot_get_ticker_price = publicSpotGetTickerPrice = Entry('ticker/price', ['public', 'spot'], 'GET', {'cost': 1}) + public_spot_get_ticker_24h = publicSpotGetTicker24h = Entry('ticker/24h', ['public', 'spot'], 'GET', {'cost': 1}) + public_spot_get_time = publicSpotGetTime = Entry('time', ['public', 'spot'], 'GET', {'cost': 1}) + public_spot_get_trade_history = publicSpotGetTradeHistory = Entry('trade/history', ['public', 'spot'], 'GET', {'cost': 1}) + public_spot_get_trade_recent = publicSpotGetTradeRecent = Entry('trade/recent', ['public', 'spot'], 'GET', {'cost': 1}) + public_spot_get_wallet_support_currency = publicSpotGetWalletSupportCurrency = Entry('wallet/support/currency', ['public', 'spot'], 'GET', {'cost': 1}) + public_linear_get_future_market_v1_public_contract_risk_balance = publicLinearGetFutureMarketV1PublicContractRiskBalance = Entry('future/market/v1/public/contract/risk-balance', ['public', 'linear'], 'GET', {'cost': 1}) + public_linear_get_future_market_v1_public_contract_open_interest = publicLinearGetFutureMarketV1PublicContractOpenInterest = Entry('future/market/v1/public/contract/open-interest', ['public', 'linear'], 'GET', {'cost': 1}) + public_linear_get_future_market_v1_public_leverage_bracket_detail = publicLinearGetFutureMarketV1PublicLeverageBracketDetail = Entry('future/market/v1/public/leverage/bracket/detail', ['public', 'linear'], 'GET', {'cost': 1}) + public_linear_get_future_market_v1_public_leverage_bracket_list = publicLinearGetFutureMarketV1PublicLeverageBracketList = Entry('future/market/v1/public/leverage/bracket/list', ['public', 'linear'], 'GET', {'cost': 1}) + public_linear_get_future_market_v1_public_q_agg_ticker = publicLinearGetFutureMarketV1PublicQAggTicker = Entry('future/market/v1/public/q/agg-ticker', ['public', 'linear'], 'GET', {'cost': 1}) + public_linear_get_future_market_v1_public_q_agg_tickers = publicLinearGetFutureMarketV1PublicQAggTickers = Entry('future/market/v1/public/q/agg-tickers', ['public', 'linear'], 'GET', {'cost': 1}) + public_linear_get_future_market_v1_public_q_deal = publicLinearGetFutureMarketV1PublicQDeal = Entry('future/market/v1/public/q/deal', ['public', 'linear'], 'GET', {'cost': 1}) + public_linear_get_future_market_v1_public_q_depth = publicLinearGetFutureMarketV1PublicQDepth = Entry('future/market/v1/public/q/depth', ['public', 'linear'], 'GET', {'cost': 1}) + public_linear_get_future_market_v1_public_q_funding_rate = publicLinearGetFutureMarketV1PublicQFundingRate = Entry('future/market/v1/public/q/funding-rate', ['public', 'linear'], 'GET', {'cost': 1}) + public_linear_get_future_market_v1_public_q_funding_rate_record = publicLinearGetFutureMarketV1PublicQFundingRateRecord = Entry('future/market/v1/public/q/funding-rate-record', ['public', 'linear'], 'GET', {'cost': 1}) + public_linear_get_future_market_v1_public_q_index_price = publicLinearGetFutureMarketV1PublicQIndexPrice = Entry('future/market/v1/public/q/index-price', ['public', 'linear'], 'GET', {'cost': 1}) + public_linear_get_future_market_v1_public_q_kline = publicLinearGetFutureMarketV1PublicQKline = Entry('future/market/v1/public/q/kline', ['public', 'linear'], 'GET', {'cost': 1}) + public_linear_get_future_market_v1_public_q_mark_price = publicLinearGetFutureMarketV1PublicQMarkPrice = Entry('future/market/v1/public/q/mark-price', ['public', 'linear'], 'GET', {'cost': 1}) + public_linear_get_future_market_v1_public_q_symbol_index_price = publicLinearGetFutureMarketV1PublicQSymbolIndexPrice = Entry('future/market/v1/public/q/symbol-index-price', ['public', 'linear'], 'GET', {'cost': 1}) + public_linear_get_future_market_v1_public_q_symbol_mark_price = publicLinearGetFutureMarketV1PublicQSymbolMarkPrice = Entry('future/market/v1/public/q/symbol-mark-price', ['public', 'linear'], 'GET', {'cost': 1}) + public_linear_get_future_market_v1_public_q_ticker = publicLinearGetFutureMarketV1PublicQTicker = Entry('future/market/v1/public/q/ticker', ['public', 'linear'], 'GET', {'cost': 1}) + public_linear_get_future_market_v1_public_q_tickers = publicLinearGetFutureMarketV1PublicQTickers = Entry('future/market/v1/public/q/tickers', ['public', 'linear'], 'GET', {'cost': 1}) + public_linear_get_future_market_v1_public_symbol_coins = publicLinearGetFutureMarketV1PublicSymbolCoins = Entry('future/market/v1/public/symbol/coins', ['public', 'linear'], 'GET', {'cost': 3.33}) + public_linear_get_future_market_v1_public_symbol_detail = publicLinearGetFutureMarketV1PublicSymbolDetail = Entry('future/market/v1/public/symbol/detail', ['public', 'linear'], 'GET', {'cost': 3.33}) + public_linear_get_future_market_v1_public_symbol_list = publicLinearGetFutureMarketV1PublicSymbolList = Entry('future/market/v1/public/symbol/list', ['public', 'linear'], 'GET', {'cost': 1}) + public_inverse_get_future_market_v1_public_contract_risk_balance = publicInverseGetFutureMarketV1PublicContractRiskBalance = Entry('future/market/v1/public/contract/risk-balance', ['public', 'inverse'], 'GET', {'cost': 1}) + public_inverse_get_future_market_v1_public_contract_open_interest = publicInverseGetFutureMarketV1PublicContractOpenInterest = Entry('future/market/v1/public/contract/open-interest', ['public', 'inverse'], 'GET', {'cost': 1}) + public_inverse_get_future_market_v1_public_leverage_bracket_detail = publicInverseGetFutureMarketV1PublicLeverageBracketDetail = Entry('future/market/v1/public/leverage/bracket/detail', ['public', 'inverse'], 'GET', {'cost': 1}) + public_inverse_get_future_market_v1_public_leverage_bracket_list = publicInverseGetFutureMarketV1PublicLeverageBracketList = Entry('future/market/v1/public/leverage/bracket/list', ['public', 'inverse'], 'GET', {'cost': 1}) + public_inverse_get_future_market_v1_public_q_agg_ticker = publicInverseGetFutureMarketV1PublicQAggTicker = Entry('future/market/v1/public/q/agg-ticker', ['public', 'inverse'], 'GET', {'cost': 1}) + public_inverse_get_future_market_v1_public_q_agg_tickers = publicInverseGetFutureMarketV1PublicQAggTickers = Entry('future/market/v1/public/q/agg-tickers', ['public', 'inverse'], 'GET', {'cost': 1}) + public_inverse_get_future_market_v1_public_q_deal = publicInverseGetFutureMarketV1PublicQDeal = Entry('future/market/v1/public/q/deal', ['public', 'inverse'], 'GET', {'cost': 1}) + public_inverse_get_future_market_v1_public_q_depth = publicInverseGetFutureMarketV1PublicQDepth = Entry('future/market/v1/public/q/depth', ['public', 'inverse'], 'GET', {'cost': 1}) + public_inverse_get_future_market_v1_public_q_funding_rate = publicInverseGetFutureMarketV1PublicQFundingRate = Entry('future/market/v1/public/q/funding-rate', ['public', 'inverse'], 'GET', {'cost': 1}) + public_inverse_get_future_market_v1_public_q_funding_rate_record = publicInverseGetFutureMarketV1PublicQFundingRateRecord = Entry('future/market/v1/public/q/funding-rate-record', ['public', 'inverse'], 'GET', {'cost': 1}) + public_inverse_get_future_market_v1_public_q_index_price = publicInverseGetFutureMarketV1PublicQIndexPrice = Entry('future/market/v1/public/q/index-price', ['public', 'inverse'], 'GET', {'cost': 1}) + public_inverse_get_future_market_v1_public_q_kline = publicInverseGetFutureMarketV1PublicQKline = Entry('future/market/v1/public/q/kline', ['public', 'inverse'], 'GET', {'cost': 1}) + public_inverse_get_future_market_v1_public_q_mark_price = publicInverseGetFutureMarketV1PublicQMarkPrice = Entry('future/market/v1/public/q/mark-price', ['public', 'inverse'], 'GET', {'cost': 1}) + public_inverse_get_future_market_v1_public_q_symbol_index_price = publicInverseGetFutureMarketV1PublicQSymbolIndexPrice = Entry('future/market/v1/public/q/symbol-index-price', ['public', 'inverse'], 'GET', {'cost': 1}) + public_inverse_get_future_market_v1_public_q_symbol_mark_price = publicInverseGetFutureMarketV1PublicQSymbolMarkPrice = Entry('future/market/v1/public/q/symbol-mark-price', ['public', 'inverse'], 'GET', {'cost': 1}) + public_inverse_get_future_market_v1_public_q_ticker = publicInverseGetFutureMarketV1PublicQTicker = Entry('future/market/v1/public/q/ticker', ['public', 'inverse'], 'GET', {'cost': 1}) + public_inverse_get_future_market_v1_public_q_tickers = publicInverseGetFutureMarketV1PublicQTickers = Entry('future/market/v1/public/q/tickers', ['public', 'inverse'], 'GET', {'cost': 1}) + public_inverse_get_future_market_v1_public_symbol_coins = publicInverseGetFutureMarketV1PublicSymbolCoins = Entry('future/market/v1/public/symbol/coins', ['public', 'inverse'], 'GET', {'cost': 3.33}) + public_inverse_get_future_market_v1_public_symbol_detail = publicInverseGetFutureMarketV1PublicSymbolDetail = Entry('future/market/v1/public/symbol/detail', ['public', 'inverse'], 'GET', {'cost': 3.33}) + public_inverse_get_future_market_v1_public_symbol_list = publicInverseGetFutureMarketV1PublicSymbolList = Entry('future/market/v1/public/symbol/list', ['public', 'inverse'], 'GET', {'cost': 1}) + private_spot_get_balance = privateSpotGetBalance = Entry('balance', ['private', 'spot'], 'GET', {'cost': 1}) + private_spot_get_balances = privateSpotGetBalances = Entry('balances', ['private', 'spot'], 'GET', {'cost': 1}) + private_spot_get_batch_order = privateSpotGetBatchOrder = Entry('batch-order', ['private', 'spot'], 'GET', {'cost': 1}) + private_spot_get_deposit_address = privateSpotGetDepositAddress = Entry('deposit/address', ['private', 'spot'], 'GET', {'cost': 1}) + private_spot_get_deposit_history = privateSpotGetDepositHistory = Entry('deposit/history', ['private', 'spot'], 'GET', {'cost': 1}) + private_spot_get_history_order = privateSpotGetHistoryOrder = Entry('history-order', ['private', 'spot'], 'GET', {'cost': 1}) + private_spot_get_open_order = privateSpotGetOpenOrder = Entry('open-order', ['private', 'spot'], 'GET', {'cost': 1}) + private_spot_get_order = privateSpotGetOrder = Entry('order', ['private', 'spot'], 'GET', {'cost': 1}) + private_spot_get_order_orderid = privateSpotGetOrderOrderId = Entry('order/{orderId}', ['private', 'spot'], 'GET', {'cost': 1}) + private_spot_get_trade = privateSpotGetTrade = Entry('trade', ['private', 'spot'], 'GET', {'cost': 1}) + private_spot_get_withdraw_history = privateSpotGetWithdrawHistory = Entry('withdraw/history', ['private', 'spot'], 'GET', {'cost': 1}) + private_spot_post_order = privateSpotPostOrder = Entry('order', ['private', 'spot'], 'POST', {'cost': 0.2}) + private_spot_post_withdraw = privateSpotPostWithdraw = Entry('withdraw', ['private', 'spot'], 'POST', {'cost': 10}) + private_spot_post_balance_transfer = privateSpotPostBalanceTransfer = Entry('balance/transfer', ['private', 'spot'], 'POST', {'cost': 1}) + private_spot_post_balance_account_transfer = privateSpotPostBalanceAccountTransfer = Entry('balance/account/transfer', ['private', 'spot'], 'POST', {'cost': 1}) + private_spot_post_ws_token = privateSpotPostWsToken = Entry('ws-token', ['private', 'spot'], 'POST', {'cost': 1}) + private_spot_delete_batch_order = privateSpotDeleteBatchOrder = Entry('batch-order', ['private', 'spot'], 'DELETE', {'cost': 1}) + private_spot_delete_open_order = privateSpotDeleteOpenOrder = Entry('open-order', ['private', 'spot'], 'DELETE', {'cost': 1}) + private_spot_delete_order_orderid = privateSpotDeleteOrderOrderId = Entry('order/{orderId}', ['private', 'spot'], 'DELETE', {'cost': 1}) + private_spot_put_order_orderid = privateSpotPutOrderOrderId = Entry('order/{orderId}', ['private', 'spot'], 'PUT', {'cost': 1}) + private_linear_get_future_trade_v1_entrust_plan_detail = privateLinearGetFutureTradeV1EntrustPlanDetail = Entry('future/trade/v1/entrust/plan-detail', ['private', 'linear'], 'GET', {'cost': 1}) + private_linear_get_future_trade_v1_entrust_plan_list = privateLinearGetFutureTradeV1EntrustPlanList = Entry('future/trade/v1/entrust/plan-list', ['private', 'linear'], 'GET', {'cost': 1}) + private_linear_get_future_trade_v1_entrust_plan_list_history = privateLinearGetFutureTradeV1EntrustPlanListHistory = Entry('future/trade/v1/entrust/plan-list-history', ['private', 'linear'], 'GET', {'cost': 1}) + private_linear_get_future_trade_v1_entrust_profit_detail = privateLinearGetFutureTradeV1EntrustProfitDetail = Entry('future/trade/v1/entrust/profit-detail', ['private', 'linear'], 'GET', {'cost': 1}) + private_linear_get_future_trade_v1_entrust_profit_list = privateLinearGetFutureTradeV1EntrustProfitList = Entry('future/trade/v1/entrust/profit-list', ['private', 'linear'], 'GET', {'cost': 1}) + private_linear_get_future_trade_v1_order_detail = privateLinearGetFutureTradeV1OrderDetail = Entry('future/trade/v1/order/detail', ['private', 'linear'], 'GET', {'cost': 1}) + private_linear_get_future_trade_v1_order_list = privateLinearGetFutureTradeV1OrderList = Entry('future/trade/v1/order/list', ['private', 'linear'], 'GET', {'cost': 1}) + private_linear_get_future_trade_v1_order_list_history = privateLinearGetFutureTradeV1OrderListHistory = Entry('future/trade/v1/order/list-history', ['private', 'linear'], 'GET', {'cost': 1}) + private_linear_get_future_trade_v1_order_trade_list = privateLinearGetFutureTradeV1OrderTradeList = Entry('future/trade/v1/order/trade-list', ['private', 'linear'], 'GET', {'cost': 1}) + private_linear_get_future_user_v1_account_info = privateLinearGetFutureUserV1AccountInfo = Entry('future/user/v1/account/info', ['private', 'linear'], 'GET', {'cost': 1}) + private_linear_get_future_user_v1_balance_bills = privateLinearGetFutureUserV1BalanceBills = Entry('future/user/v1/balance/bills', ['private', 'linear'], 'GET', {'cost': 1}) + private_linear_get_future_user_v1_balance_detail = privateLinearGetFutureUserV1BalanceDetail = Entry('future/user/v1/balance/detail', ['private', 'linear'], 'GET', {'cost': 1}) + private_linear_get_future_user_v1_balance_funding_rate_list = privateLinearGetFutureUserV1BalanceFundingRateList = Entry('future/user/v1/balance/funding-rate-list', ['private', 'linear'], 'GET', {'cost': 1}) + private_linear_get_future_user_v1_balance_list = privateLinearGetFutureUserV1BalanceList = Entry('future/user/v1/balance/list', ['private', 'linear'], 'GET', {'cost': 1}) + private_linear_get_future_user_v1_position_adl = privateLinearGetFutureUserV1PositionAdl = Entry('future/user/v1/position/adl', ['private', 'linear'], 'GET', {'cost': 1}) + private_linear_get_future_user_v1_position_list = privateLinearGetFutureUserV1PositionList = Entry('future/user/v1/position/list', ['private', 'linear'], 'GET', {'cost': 1}) + private_linear_get_future_user_v1_user_collection_list = privateLinearGetFutureUserV1UserCollectionList = Entry('future/user/v1/user/collection/list', ['private', 'linear'], 'GET', {'cost': 1}) + private_linear_get_future_user_v1_user_listen_key = privateLinearGetFutureUserV1UserListenKey = Entry('future/user/v1/user/listen-key', ['private', 'linear'], 'GET', {'cost': 1}) + private_linear_post_future_trade_v1_entrust_cancel_all_plan = privateLinearPostFutureTradeV1EntrustCancelAllPlan = Entry('future/trade/v1/entrust/cancel-all-plan', ['private', 'linear'], 'POST', {'cost': 1}) + private_linear_post_future_trade_v1_entrust_cancel_all_profit_stop = privateLinearPostFutureTradeV1EntrustCancelAllProfitStop = Entry('future/trade/v1/entrust/cancel-all-profit-stop', ['private', 'linear'], 'POST', {'cost': 1}) + private_linear_post_future_trade_v1_entrust_cancel_plan = privateLinearPostFutureTradeV1EntrustCancelPlan = Entry('future/trade/v1/entrust/cancel-plan', ['private', 'linear'], 'POST', {'cost': 1}) + private_linear_post_future_trade_v1_entrust_cancel_profit_stop = privateLinearPostFutureTradeV1EntrustCancelProfitStop = Entry('future/trade/v1/entrust/cancel-profit-stop', ['private', 'linear'], 'POST', {'cost': 1}) + private_linear_post_future_trade_v1_entrust_create_plan = privateLinearPostFutureTradeV1EntrustCreatePlan = Entry('future/trade/v1/entrust/create-plan', ['private', 'linear'], 'POST', {'cost': 1}) + private_linear_post_future_trade_v1_entrust_create_profit = privateLinearPostFutureTradeV1EntrustCreateProfit = Entry('future/trade/v1/entrust/create-profit', ['private', 'linear'], 'POST', {'cost': 1}) + private_linear_post_future_trade_v1_entrust_update_profit_stop = privateLinearPostFutureTradeV1EntrustUpdateProfitStop = Entry('future/trade/v1/entrust/update-profit-stop', ['private', 'linear'], 'POST', {'cost': 1}) + private_linear_post_future_trade_v1_order_cancel = privateLinearPostFutureTradeV1OrderCancel = Entry('future/trade/v1/order/cancel', ['private', 'linear'], 'POST', {'cost': 1}) + private_linear_post_future_trade_v1_order_cancel_all = privateLinearPostFutureTradeV1OrderCancelAll = Entry('future/trade/v1/order/cancel-all', ['private', 'linear'], 'POST', {'cost': 1}) + private_linear_post_future_trade_v1_order_create = privateLinearPostFutureTradeV1OrderCreate = Entry('future/trade/v1/order/create', ['private', 'linear'], 'POST', {'cost': 1}) + private_linear_post_future_trade_v1_order_create_batch = privateLinearPostFutureTradeV1OrderCreateBatch = Entry('future/trade/v1/order/create-batch', ['private', 'linear'], 'POST', {'cost': 1}) + private_linear_post_future_trade_v1_order_update = privateLinearPostFutureTradeV1OrderUpdate = Entry('future/trade/v1/order/update', ['private', 'linear'], 'POST', {'cost': 1}) + private_linear_post_future_user_v1_account_open = privateLinearPostFutureUserV1AccountOpen = Entry('future/user/v1/account/open', ['private', 'linear'], 'POST', {'cost': 1}) + private_linear_post_future_user_v1_position_adjust_leverage = privateLinearPostFutureUserV1PositionAdjustLeverage = Entry('future/user/v1/position/adjust-leverage', ['private', 'linear'], 'POST', {'cost': 1}) + private_linear_post_future_user_v1_position_auto_margin = privateLinearPostFutureUserV1PositionAutoMargin = Entry('future/user/v1/position/auto-margin', ['private', 'linear'], 'POST', {'cost': 1}) + private_linear_post_future_user_v1_position_close_all = privateLinearPostFutureUserV1PositionCloseAll = Entry('future/user/v1/position/close-all', ['private', 'linear'], 'POST', {'cost': 1}) + private_linear_post_future_user_v1_position_margin = privateLinearPostFutureUserV1PositionMargin = Entry('future/user/v1/position/margin', ['private', 'linear'], 'POST', {'cost': 1}) + private_linear_post_future_user_v1_user_collection_add = privateLinearPostFutureUserV1UserCollectionAdd = Entry('future/user/v1/user/collection/add', ['private', 'linear'], 'POST', {'cost': 1}) + private_linear_post_future_user_v1_user_collection_cancel = privateLinearPostFutureUserV1UserCollectionCancel = Entry('future/user/v1/user/collection/cancel', ['private', 'linear'], 'POST', {'cost': 1}) + private_linear_post_future_user_v1_position_change_type = privateLinearPostFutureUserV1PositionChangeType = Entry('future/user/v1/position/change-type', ['private', 'linear'], 'POST', {'cost': 1}) + private_inverse_get_future_trade_v1_entrust_plan_detail = privateInverseGetFutureTradeV1EntrustPlanDetail = Entry('future/trade/v1/entrust/plan-detail', ['private', 'inverse'], 'GET', {'cost': 1}) + private_inverse_get_future_trade_v1_entrust_plan_list = privateInverseGetFutureTradeV1EntrustPlanList = Entry('future/trade/v1/entrust/plan-list', ['private', 'inverse'], 'GET', {'cost': 1}) + private_inverse_get_future_trade_v1_entrust_plan_list_history = privateInverseGetFutureTradeV1EntrustPlanListHistory = Entry('future/trade/v1/entrust/plan-list-history', ['private', 'inverse'], 'GET', {'cost': 1}) + private_inverse_get_future_trade_v1_entrust_profit_detail = privateInverseGetFutureTradeV1EntrustProfitDetail = Entry('future/trade/v1/entrust/profit-detail', ['private', 'inverse'], 'GET', {'cost': 1}) + private_inverse_get_future_trade_v1_entrust_profit_list = privateInverseGetFutureTradeV1EntrustProfitList = Entry('future/trade/v1/entrust/profit-list', ['private', 'inverse'], 'GET', {'cost': 1}) + private_inverse_get_future_trade_v1_order_detail = privateInverseGetFutureTradeV1OrderDetail = Entry('future/trade/v1/order/detail', ['private', 'inverse'], 'GET', {'cost': 1}) + private_inverse_get_future_trade_v1_order_list = privateInverseGetFutureTradeV1OrderList = Entry('future/trade/v1/order/list', ['private', 'inverse'], 'GET', {'cost': 1}) + private_inverse_get_future_trade_v1_order_list_history = privateInverseGetFutureTradeV1OrderListHistory = Entry('future/trade/v1/order/list-history', ['private', 'inverse'], 'GET', {'cost': 1}) + private_inverse_get_future_trade_v1_order_trade_list = privateInverseGetFutureTradeV1OrderTradeList = Entry('future/trade/v1/order/trade-list', ['private', 'inverse'], 'GET', {'cost': 1}) + private_inverse_get_future_user_v1_account_info = privateInverseGetFutureUserV1AccountInfo = Entry('future/user/v1/account/info', ['private', 'inverse'], 'GET', {'cost': 1}) + private_inverse_get_future_user_v1_balance_bills = privateInverseGetFutureUserV1BalanceBills = Entry('future/user/v1/balance/bills', ['private', 'inverse'], 'GET', {'cost': 1}) + private_inverse_get_future_user_v1_balance_detail = privateInverseGetFutureUserV1BalanceDetail = Entry('future/user/v1/balance/detail', ['private', 'inverse'], 'GET', {'cost': 1}) + private_inverse_get_future_user_v1_balance_funding_rate_list = privateInverseGetFutureUserV1BalanceFundingRateList = Entry('future/user/v1/balance/funding-rate-list', ['private', 'inverse'], 'GET', {'cost': 1}) + private_inverse_get_future_user_v1_balance_list = privateInverseGetFutureUserV1BalanceList = Entry('future/user/v1/balance/list', ['private', 'inverse'], 'GET', {'cost': 1}) + private_inverse_get_future_user_v1_position_adl = privateInverseGetFutureUserV1PositionAdl = Entry('future/user/v1/position/adl', ['private', 'inverse'], 'GET', {'cost': 1}) + private_inverse_get_future_user_v1_position_list = privateInverseGetFutureUserV1PositionList = Entry('future/user/v1/position/list', ['private', 'inverse'], 'GET', {'cost': 1}) + private_inverse_get_future_user_v1_user_collection_list = privateInverseGetFutureUserV1UserCollectionList = Entry('future/user/v1/user/collection/list', ['private', 'inverse'], 'GET', {'cost': 1}) + private_inverse_get_future_user_v1_user_listen_key = privateInverseGetFutureUserV1UserListenKey = Entry('future/user/v1/user/listen-key', ['private', 'inverse'], 'GET', {'cost': 1}) + private_inverse_post_future_trade_v1_entrust_cancel_all_plan = privateInversePostFutureTradeV1EntrustCancelAllPlan = Entry('future/trade/v1/entrust/cancel-all-plan', ['private', 'inverse'], 'POST', {'cost': 1}) + private_inverse_post_future_trade_v1_entrust_cancel_all_profit_stop = privateInversePostFutureTradeV1EntrustCancelAllProfitStop = Entry('future/trade/v1/entrust/cancel-all-profit-stop', ['private', 'inverse'], 'POST', {'cost': 1}) + private_inverse_post_future_trade_v1_entrust_cancel_plan = privateInversePostFutureTradeV1EntrustCancelPlan = Entry('future/trade/v1/entrust/cancel-plan', ['private', 'inverse'], 'POST', {'cost': 1}) + private_inverse_post_future_trade_v1_entrust_cancel_profit_stop = privateInversePostFutureTradeV1EntrustCancelProfitStop = Entry('future/trade/v1/entrust/cancel-profit-stop', ['private', 'inverse'], 'POST', {'cost': 1}) + private_inverse_post_future_trade_v1_entrust_create_plan = privateInversePostFutureTradeV1EntrustCreatePlan = Entry('future/trade/v1/entrust/create-plan', ['private', 'inverse'], 'POST', {'cost': 1}) + private_inverse_post_future_trade_v1_entrust_create_profit = privateInversePostFutureTradeV1EntrustCreateProfit = Entry('future/trade/v1/entrust/create-profit', ['private', 'inverse'], 'POST', {'cost': 1}) + private_inverse_post_future_trade_v1_entrust_update_profit_stop = privateInversePostFutureTradeV1EntrustUpdateProfitStop = Entry('future/trade/v1/entrust/update-profit-stop', ['private', 'inverse'], 'POST', {'cost': 1}) + private_inverse_post_future_trade_v1_order_cancel = privateInversePostFutureTradeV1OrderCancel = Entry('future/trade/v1/order/cancel', ['private', 'inverse'], 'POST', {'cost': 1}) + private_inverse_post_future_trade_v1_order_cancel_all = privateInversePostFutureTradeV1OrderCancelAll = Entry('future/trade/v1/order/cancel-all', ['private', 'inverse'], 'POST', {'cost': 1}) + private_inverse_post_future_trade_v1_order_create = privateInversePostFutureTradeV1OrderCreate = Entry('future/trade/v1/order/create', ['private', 'inverse'], 'POST', {'cost': 1}) + private_inverse_post_future_trade_v1_order_create_batch = privateInversePostFutureTradeV1OrderCreateBatch = Entry('future/trade/v1/order/create-batch', ['private', 'inverse'], 'POST', {'cost': 1}) + private_inverse_post_future_trade_v1_order_update = privateInversePostFutureTradeV1OrderUpdate = Entry('future/trade/v1/order/update', ['private', 'inverse'], 'POST', {'cost': 1}) + private_inverse_post_future_user_v1_account_open = privateInversePostFutureUserV1AccountOpen = Entry('future/user/v1/account/open', ['private', 'inverse'], 'POST', {'cost': 1}) + private_inverse_post_future_user_v1_position_adjust_leverage = privateInversePostFutureUserV1PositionAdjustLeverage = Entry('future/user/v1/position/adjust-leverage', ['private', 'inverse'], 'POST', {'cost': 1}) + private_inverse_post_future_user_v1_position_auto_margin = privateInversePostFutureUserV1PositionAutoMargin = Entry('future/user/v1/position/auto-margin', ['private', 'inverse'], 'POST', {'cost': 1}) + private_inverse_post_future_user_v1_position_close_all = privateInversePostFutureUserV1PositionCloseAll = Entry('future/user/v1/position/close-all', ['private', 'inverse'], 'POST', {'cost': 1}) + private_inverse_post_future_user_v1_position_margin = privateInversePostFutureUserV1PositionMargin = Entry('future/user/v1/position/margin', ['private', 'inverse'], 'POST', {'cost': 1}) + private_inverse_post_future_user_v1_user_collection_add = privateInversePostFutureUserV1UserCollectionAdd = Entry('future/user/v1/user/collection/add', ['private', 'inverse'], 'POST', {'cost': 1}) + private_inverse_post_future_user_v1_user_collection_cancel = privateInversePostFutureUserV1UserCollectionCancel = Entry('future/user/v1/user/collection/cancel', ['private', 'inverse'], 'POST', {'cost': 1}) + private_user_get_user_account = privateUserGetUserAccount = Entry('user/account', ['private', 'user'], 'GET', {'cost': 1}) + private_user_get_user_account_api_key = privateUserGetUserAccountApiKey = Entry('user/account/api-key', ['private', 'user'], 'GET', {'cost': 1}) + private_user_post_user_account = privateUserPostUserAccount = Entry('user/account', ['private', 'user'], 'POST', {'cost': 1}) + private_user_post_user_account_api_key = privateUserPostUserAccountApiKey = Entry('user/account/api-key', ['private', 'user'], 'POST', {'cost': 1}) + private_user_put_user_account_api_key = privateUserPutUserAccountApiKey = Entry('user/account/api-key', ['private', 'user'], 'PUT', {'cost': 1}) + private_user_delete_user_account_apikeyid = privateUserDeleteUserAccountApiKeyId = Entry('user/account/{apiKeyId}', ['private', 'user'], 'DELETE', {'cost': 1}) diff --git a/ccxt/abstract/yobit.py b/ccxt/abstract/yobit.py new file mode 100644 index 0000000..3b8b5b7 --- /dev/null +++ b/ccxt/abstract/yobit.py @@ -0,0 +1,16 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_depth_pair = publicGetDepthPair = Entry('depth/{pair}', 'public', 'GET', {'cost': 1}) + public_get_info = publicGetInfo = Entry('info', 'public', 'GET', {'cost': 1}) + public_get_ticker_pair = publicGetTickerPair = Entry('ticker/{pair}', 'public', 'GET', {'cost': 1}) + public_get_trades_pair = publicGetTradesPair = Entry('trades/{pair}', 'public', 'GET', {'cost': 1}) + private_post_activeorders = privatePostActiveOrders = Entry('ActiveOrders', 'private', 'POST', {'cost': 1}) + private_post_cancelorder = privatePostCancelOrder = Entry('CancelOrder', 'private', 'POST', {'cost': 1}) + private_post_getdepositaddress = privatePostGetDepositAddress = Entry('GetDepositAddress', 'private', 'POST', {'cost': 1}) + private_post_getinfo = privatePostGetInfo = Entry('getInfo', 'private', 'POST', {'cost': 1}) + private_post_orderinfo = privatePostOrderInfo = Entry('OrderInfo', 'private', 'POST', {'cost': 1}) + private_post_trade = privatePostTrade = Entry('Trade', 'private', 'POST', {'cost': 1}) + private_post_tradehistory = privatePostTradeHistory = Entry('TradeHistory', 'private', 'POST', {'cost': 1}) + private_post_withdrawcoinstoaddress = privatePostWithdrawCoinsToAddress = Entry('WithdrawCoinsToAddress', 'private', 'POST', {'cost': 1}) diff --git a/ccxt/abstract/zaif.py b/ccxt/abstract/zaif.py new file mode 100644 index 0000000..59c1160 --- /dev/null +++ b/ccxt/abstract/zaif.py @@ -0,0 +1,38 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_depth_pair = publicGetDepthPair = Entry('depth/{pair}', 'public', 'GET', {'cost': 1}) + public_get_currencies_pair = publicGetCurrenciesPair = Entry('currencies/{pair}', 'public', 'GET', {'cost': 1}) + public_get_currencies_all = publicGetCurrenciesAll = Entry('currencies/all', 'public', 'GET', {'cost': 1}) + public_get_currency_pairs_pair = publicGetCurrencyPairsPair = Entry('currency_pairs/{pair}', 'public', 'GET', {'cost': 1}) + public_get_currency_pairs_all = publicGetCurrencyPairsAll = Entry('currency_pairs/all', 'public', 'GET', {'cost': 1}) + public_get_last_price_pair = publicGetLastPricePair = Entry('last_price/{pair}', 'public', 'GET', {'cost': 1}) + public_get_ticker_pair = publicGetTickerPair = Entry('ticker/{pair}', 'public', 'GET', {'cost': 1}) + public_get_trades_pair = publicGetTradesPair = Entry('trades/{pair}', 'public', 'GET', {'cost': 1}) + private_post_active_orders = privatePostActiveOrders = Entry('active_orders', 'private', 'POST', {'cost': 5}) + private_post_cancel_order = privatePostCancelOrder = Entry('cancel_order', 'private', 'POST', {'cost': 5}) + private_post_deposit_history = privatePostDepositHistory = Entry('deposit_history', 'private', 'POST', {'cost': 5}) + private_post_get_id_info = privatePostGetIdInfo = Entry('get_id_info', 'private', 'POST', {'cost': 5}) + private_post_get_info = privatePostGetInfo = Entry('get_info', 'private', 'POST', {'cost': 10}) + private_post_get_info2 = privatePostGetInfo2 = Entry('get_info2', 'private', 'POST', {'cost': 5}) + private_post_get_personal_info = privatePostGetPersonalInfo = Entry('get_personal_info', 'private', 'POST', {'cost': 5}) + private_post_trade = privatePostTrade = Entry('trade', 'private', 'POST', {'cost': 5}) + private_post_trade_history = privatePostTradeHistory = Entry('trade_history', 'private', 'POST', {'cost': 50}) + private_post_withdraw = privatePostWithdraw = Entry('withdraw', 'private', 'POST', {'cost': 5}) + private_post_withdraw_history = privatePostWithdrawHistory = Entry('withdraw_history', 'private', 'POST', {'cost': 5}) + ecapi_post_createinvoice = ecapiPostCreateInvoice = Entry('createInvoice', 'ecapi', 'POST', {'cost': 1}) + ecapi_post_getinvoice = ecapiPostGetInvoice = Entry('getInvoice', 'ecapi', 'POST', {'cost': 1}) + ecapi_post_getinvoiceidsbyordernumber = ecapiPostGetInvoiceIdsByOrderNumber = Entry('getInvoiceIdsByOrderNumber', 'ecapi', 'POST', {'cost': 1}) + ecapi_post_cancelinvoice = ecapiPostCancelInvoice = Entry('cancelInvoice', 'ecapi', 'POST', {'cost': 1}) + tlapi_post_get_positions = tlapiPostGetPositions = Entry('get_positions', 'tlapi', 'POST', {'cost': 66}) + tlapi_post_position_history = tlapiPostPositionHistory = Entry('position_history', 'tlapi', 'POST', {'cost': 66}) + tlapi_post_active_positions = tlapiPostActivePositions = Entry('active_positions', 'tlapi', 'POST', {'cost': 5}) + tlapi_post_create_position = tlapiPostCreatePosition = Entry('create_position', 'tlapi', 'POST', {'cost': 33}) + tlapi_post_change_position = tlapiPostChangePosition = Entry('change_position', 'tlapi', 'POST', {'cost': 33}) + tlapi_post_cancel_position = tlapiPostCancelPosition = Entry('cancel_position', 'tlapi', 'POST', {'cost': 33}) + fapi_get_groups_group_id = fapiGetGroupsGroupId = Entry('groups/{group_id}', 'fapi', 'GET', {'cost': 1}) + fapi_get_last_price_group_id_pair = fapiGetLastPriceGroupIdPair = Entry('last_price/{group_id}/{pair}', 'fapi', 'GET', {'cost': 1}) + fapi_get_ticker_group_id_pair = fapiGetTickerGroupIdPair = Entry('ticker/{group_id}/{pair}', 'fapi', 'GET', {'cost': 1}) + fapi_get_trades_group_id_pair = fapiGetTradesGroupIdPair = Entry('trades/{group_id}/{pair}', 'fapi', 'GET', {'cost': 1}) + fapi_get_depth_group_id_pair = fapiGetDepthGroupIdPair = Entry('depth/{group_id}/{pair}', 'fapi', 'GET', {'cost': 1}) diff --git a/ccxt/abstract/zonda.py b/ccxt/abstract/zonda.py new file mode 100644 index 0000000..ea7bf3b --- /dev/null +++ b/ccxt/abstract/zonda.py @@ -0,0 +1,53 @@ +from ccxt.base.types import Entry + + +class ImplicitAPI: + public_get_id_all = publicGetIdAll = Entry('{id}/all', 'public', 'GET', {}) + public_get_id_market = publicGetIdMarket = Entry('{id}/market', 'public', 'GET', {}) + public_get_id_orderbook = publicGetIdOrderbook = Entry('{id}/orderbook', 'public', 'GET', {}) + public_get_id_ticker = publicGetIdTicker = Entry('{id}/ticker', 'public', 'GET', {}) + public_get_id_trades = publicGetIdTrades = Entry('{id}/trades', 'public', 'GET', {}) + private_post_info = privatePostInfo = Entry('info', 'private', 'POST', {}) + private_post_trade = privatePostTrade = Entry('trade', 'private', 'POST', {}) + private_post_cancel = privatePostCancel = Entry('cancel', 'private', 'POST', {}) + private_post_orderbook = privatePostOrderbook = Entry('orderbook', 'private', 'POST', {}) + private_post_orders = privatePostOrders = Entry('orders', 'private', 'POST', {}) + private_post_transfer = privatePostTransfer = Entry('transfer', 'private', 'POST', {}) + private_post_withdraw = privatePostWithdraw = Entry('withdraw', 'private', 'POST', {}) + private_post_history = privatePostHistory = Entry('history', 'private', 'POST', {}) + private_post_transactions = privatePostTransactions = Entry('transactions', 'private', 'POST', {}) + v1_01public_get_trading_ticker = v1_01PublicGetTradingTicker = Entry('trading/ticker', 'v1_01Public', 'GET', {}) + v1_01public_get_trading_ticker_symbol = v1_01PublicGetTradingTickerSymbol = Entry('trading/ticker/{symbol}', 'v1_01Public', 'GET', {}) + v1_01public_get_trading_stats = v1_01PublicGetTradingStats = Entry('trading/stats', 'v1_01Public', 'GET', {}) + v1_01public_get_trading_stats_symbol = v1_01PublicGetTradingStatsSymbol = Entry('trading/stats/{symbol}', 'v1_01Public', 'GET', {}) + v1_01public_get_trading_orderbook_symbol = v1_01PublicGetTradingOrderbookSymbol = Entry('trading/orderbook/{symbol}', 'v1_01Public', 'GET', {}) + v1_01public_get_trading_transactions_symbol = v1_01PublicGetTradingTransactionsSymbol = Entry('trading/transactions/{symbol}', 'v1_01Public', 'GET', {}) + v1_01public_get_trading_candle_history_symbol_resolution = v1_01PublicGetTradingCandleHistorySymbolResolution = Entry('trading/candle/history/{symbol}/{resolution}', 'v1_01Public', 'GET', {}) + v1_01private_get_api_payments_deposits_crypto_addresses = v1_01PrivateGetApiPaymentsDepositsCryptoAddresses = Entry('api_payments/deposits/crypto/addresses', 'v1_01Private', 'GET', {}) + v1_01private_get_payments_withdrawal_detailid = v1_01PrivateGetPaymentsWithdrawalDetailId = Entry('payments/withdrawal/{detailId}', 'v1_01Private', 'GET', {}) + v1_01private_get_payments_deposit_detailid = v1_01PrivateGetPaymentsDepositDetailId = Entry('payments/deposit/{detailId}', 'v1_01Private', 'GET', {}) + v1_01private_get_trading_offer = v1_01PrivateGetTradingOffer = Entry('trading/offer', 'v1_01Private', 'GET', {}) + v1_01private_get_trading_stop_offer = v1_01PrivateGetTradingStopOffer = Entry('trading/stop/offer', 'v1_01Private', 'GET', {}) + v1_01private_get_trading_config_symbol = v1_01PrivateGetTradingConfigSymbol = Entry('trading/config/{symbol}', 'v1_01Private', 'GET', {}) + v1_01private_get_trading_history_transactions = v1_01PrivateGetTradingHistoryTransactions = Entry('trading/history/transactions', 'v1_01Private', 'GET', {}) + v1_01private_get_balances_bitbay_history = v1_01PrivateGetBalancesBITBAYHistory = Entry('balances/BITBAY/history', 'v1_01Private', 'GET', {}) + v1_01private_get_balances_bitbay_balance = v1_01PrivateGetBalancesBITBAYBalance = Entry('balances/BITBAY/balance', 'v1_01Private', 'GET', {}) + v1_01private_get_fiat_cantor_rate_baseid_quoteid = v1_01PrivateGetFiatCantorRateBaseIdQuoteId = Entry('fiat_cantor/rate/{baseId}/{quoteId}', 'v1_01Private', 'GET', {}) + v1_01private_get_fiat_cantor_history = v1_01PrivateGetFiatCantorHistory = Entry('fiat_cantor/history', 'v1_01Private', 'GET', {}) + v1_01private_get_client_payments_v2_customer_crypto_currency_channels_deposit = v1_01PrivateGetClientPaymentsV2CustomerCryptoCurrencyChannelsDeposit = Entry('client_payments/v2/customer/crypto/{currency}/channels/deposit', 'v1_01Private', 'GET', {}) + v1_01private_get_client_payments_v2_customer_crypto_currency_channels_withdrawal = v1_01PrivateGetClientPaymentsV2CustomerCryptoCurrencyChannelsWithdrawal = Entry('client_payments/v2/customer/crypto/{currency}/channels/withdrawal', 'v1_01Private', 'GET', {}) + v1_01private_get_client_payments_v2_customer_crypto_deposit_fee = v1_01PrivateGetClientPaymentsV2CustomerCryptoDepositFee = Entry('client_payments/v2/customer/crypto/deposit/fee', 'v1_01Private', 'GET', {}) + v1_01private_get_client_payments_v2_customer_crypto_withdrawal_fee = v1_01PrivateGetClientPaymentsV2CustomerCryptoWithdrawalFee = Entry('client_payments/v2/customer/crypto/withdrawal/fee', 'v1_01Private', 'GET', {}) + v1_01private_post_trading_offer_symbol = v1_01PrivatePostTradingOfferSymbol = Entry('trading/offer/{symbol}', 'v1_01Private', 'POST', {}) + v1_01private_post_trading_stop_offer_symbol = v1_01PrivatePostTradingStopOfferSymbol = Entry('trading/stop/offer/{symbol}', 'v1_01Private', 'POST', {}) + v1_01private_post_trading_config_symbol = v1_01PrivatePostTradingConfigSymbol = Entry('trading/config/{symbol}', 'v1_01Private', 'POST', {}) + v1_01private_post_balances_bitbay_balance = v1_01PrivatePostBalancesBITBAYBalance = Entry('balances/BITBAY/balance', 'v1_01Private', 'POST', {}) + v1_01private_post_balances_bitbay_balance_transfer_source_destination = v1_01PrivatePostBalancesBITBAYBalanceTransferSourceDestination = Entry('balances/BITBAY/balance/transfer/{source}/{destination}', 'v1_01Private', 'POST', {}) + v1_01private_post_fiat_cantor_exchange = v1_01PrivatePostFiatCantorExchange = Entry('fiat_cantor/exchange', 'v1_01Private', 'POST', {}) + v1_01private_post_api_payments_withdrawals_crypto = v1_01PrivatePostApiPaymentsWithdrawalsCrypto = Entry('api_payments/withdrawals/crypto', 'v1_01Private', 'POST', {}) + v1_01private_post_api_payments_withdrawals_fiat = v1_01PrivatePostApiPaymentsWithdrawalsFiat = Entry('api_payments/withdrawals/fiat', 'v1_01Private', 'POST', {}) + v1_01private_post_client_payments_v2_customer_crypto_deposit = v1_01PrivatePostClientPaymentsV2CustomerCryptoDeposit = Entry('client_payments/v2/customer/crypto/deposit', 'v1_01Private', 'POST', {}) + v1_01private_post_client_payments_v2_customer_crypto_withdrawal = v1_01PrivatePostClientPaymentsV2CustomerCryptoWithdrawal = Entry('client_payments/v2/customer/crypto/withdrawal', 'v1_01Private', 'POST', {}) + v1_01private_delete_trading_offer_symbol_id_side_price = v1_01PrivateDeleteTradingOfferSymbolIdSidePrice = Entry('trading/offer/{symbol}/{id}/{side}/{price}', 'v1_01Private', 'DELETE', {}) + v1_01private_delete_trading_stop_offer_symbol_id_side_price = v1_01PrivateDeleteTradingStopOfferSymbolIdSidePrice = Entry('trading/stop/offer/{symbol}/{id}/{side}/{price}', 'v1_01Private', 'DELETE', {}) + v1_01private_put_balances_bitbay_balance_id = v1_01PrivatePutBalancesBITBAYBalanceId = Entry('balances/BITBAY/balance/{id}', 'v1_01Private', 'PUT', {}) diff --git a/ccxt/alpaca.py b/ccxt/alpaca.py new file mode 100644 index 0000000..547c87f --- /dev/null +++ b/ccxt/alpaca.py @@ -0,0 +1,1860 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.alpaca import ImplicitAPI +from ccxt.base.types import Any, Balances, Currency, DepositAddress, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import NotSupported +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class alpaca(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(alpaca, self).describe(), { + 'id': 'alpaca', + 'name': 'Alpaca', + 'countries': ['US'], + # 3 req/s for free + # 150 req/s for subscribers: https://alpaca.markets/data + # for brokers: https://alpaca.markets/docs/api-references/broker-api/#authentication-and-rate-limit + 'rateLimit': 333, + 'hostname': 'alpaca.markets', + 'pro': True, + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/e9476df8-a450-4c3e-ab9a-1a7794219e1b', + 'www': 'https://alpaca.markets', + 'api': { + 'broker': 'https://broker-api.{hostname}', + 'trader': 'https://api.{hostname}', + 'market': 'https://data.{hostname}', + }, + 'test': { + 'broker': 'https://broker-api.sandbox.{hostname}', + 'trader': 'https://paper-api.{hostname}', + 'market': 'https://data.{hostname}', + }, + 'doc': 'https://alpaca.markets/docs/', + 'fees': 'https://docs.alpaca.markets/docs/crypto-fees', + }, + 'has': { + 'CORS': False, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createMarketBuyOrder': True, + 'createMarketBuyOrderWithCost': True, + 'createMarketOrderWithCost': True, + 'createOrder': True, + 'createOrderWithTakeProfitAndStopLoss': False, + 'createOrderWithTakeProfitAndStopLossWs': False, + 'createReduceOnlyOrder': False, + 'createStopOrder': True, + 'createTriggerOrder': True, + 'editOrder': True, + 'fetchBalance': True, + 'fetchBidsAsks': False, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': False, + 'fetchDepositAddress': True, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchDepositsWithdrawals': True, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchL1OrderBook': True, + 'fetchL2OrderBook': False, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrder': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchStatus': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': False, + 'fetchTransactionFees': False, + 'fetchTransactions': False, + 'fetchTransfers': False, + 'fetchVolatilityHistory': False, + 'fetchWithdrawals': True, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'sandbox': True, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'transfer': False, + 'withdraw': True, + }, + 'api': { + 'broker': { + }, + 'trader': { + 'private': { + 'get': [ + 'v2/account', + 'v2/orders', + 'v2/orders/{order_id}', + 'v2/positions', + 'v2/positions/{symbol_or_asset_id}', + 'v2/account/portfolio/history', + 'v2/watchlists', + 'v2/watchlists/{watchlist_id}', + 'v2/watchlists:by_name', + 'v2/account/configurations', + 'v2/account/activities', + 'v2/account/activities/{activity_type}', + 'v2/calendar', + 'v2/clock', + 'v2/assets', + 'v2/assets/{symbol_or_asset_id}', + 'v2/corporate_actions/announcements/{id}', + 'v2/corporate_actions/announcements', + 'v2/wallets', + 'v2/wallets/transfers', + ], + 'post': [ + 'v2/orders', + 'v2/watchlists', + 'v2/watchlists/{watchlist_id}', + 'v2/watchlists:by_name', + 'v2/wallets/transfers', + ], + 'put': [ + 'v2/orders/{order_id}', + 'v2/watchlists/{watchlist_id}', + 'v2/watchlists:by_name', + ], + 'patch': [ + 'v2/orders/{order_id}', + 'v2/account/configurations', + ], + 'delete': [ + 'v2/orders', + 'v2/orders/{order_id}', + 'v2/positions', + 'v2/positions/{symbol_or_asset_id}', + 'v2/watchlists/{watchlist_id}', + 'v2/watchlists:by_name', + 'v2/watchlists/{watchlist_id}/{symbol}', + ], + }, + }, + 'market': { + 'public': { + 'get': [ + 'v1beta3/crypto/{loc}/bars', + 'v1beta3/crypto/{loc}/latest/bars', + 'v1beta3/crypto/{loc}/latest/orderbooks', + 'v1beta3/crypto/{loc}/latest/quotes', + 'v1beta3/crypto/{loc}/latest/trades', + 'v1beta3/crypto/{loc}/quotes', + 'v1beta3/crypto/{loc}/snapshots', + 'v1beta3/crypto/{loc}/trades', + ], + }, + 'private': { + 'get': [ + 'v1beta1/corporate-actions', + 'v1beta1/forex/latest/rates', + 'v1beta1/forex/rates', + 'v1beta1/logos/{symbol}', + 'v1beta1/news', + 'v1beta1/screener/stocks/most-actives', + 'v1beta1/screener/{market_type}/movers', + 'v2/stocks/auctions', + 'v2/stocks/bars', + 'v2/stocks/bars/latest', + 'v2/stocks/meta/conditions/{ticktype}', + 'v2/stocks/meta/exchanges', + 'v2/stocks/quotes', + 'v2/stocks/quotes/latest', + 'v2/stocks/snapshots', + 'v2/stocks/trades', + 'v2/stocks/trades/latest', + 'v2/stocks/{symbol}/auctions', + 'v2/stocks/{symbol}/bars', + 'v2/stocks/{symbol}/bars/latest', + 'v2/stocks/{symbol}/quotes', + 'v2/stocks/{symbol}/quotes/latest', + 'v2/stocks/{symbol}/snapshot', + 'v2/stocks/{symbol}/trades', + 'v2/stocks/{symbol}/trades/latest', + ], + }, + }, + }, + 'timeframes': { + '1m': '1min', + '3m': '3min', + '5m': '5min', + '15m': '15min', + '30m': '30min', + '1h': '1H', + '2h': '2H', + '4h': '4H', + '6h': '6H', + '8h': '8H', + '12h': '12H', + '1d': '1D', + '3d': '3D', + '1w': '1W', + '1M': '1M', + }, + 'precisionMode': TICK_SIZE, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + }, + 'fees': { + 'trading': { + 'tierBased': True, + 'percentage': True, + 'maker': self.parse_number('0.0015'), + 'taker': self.parse_number('0.0025'), + 'tiers': { + 'taker': [ + [self.parse_number('0'), self.parse_number('0.0025')], + [self.parse_number('100000'), self.parse_number('0.0022')], + [self.parse_number('500000'), self.parse_number('0.0020')], + [self.parse_number('1000000'), self.parse_number('0.0018')], + [self.parse_number('10000000'), self.parse_number('0.0015')], + [self.parse_number('25000000'), self.parse_number('0.0013')], + [self.parse_number('50000000'), self.parse_number('0.0012')], + [self.parse_number('100000000'), self.parse_number('0.001')], + ], + 'maker': [ + [self.parse_number('0'), self.parse_number('0.0015')], + [self.parse_number('100000'), self.parse_number('0.0012')], + [self.parse_number('500000'), self.parse_number('0.001')], + [self.parse_number('1000000'), self.parse_number('0.0008')], + [self.parse_number('10000000'), self.parse_number('0.0005')], + [self.parse_number('25000000'), self.parse_number('0.0002')], + [self.parse_number('50000000'), self.parse_number('0.0002')], + [self.parse_number('100000000'), self.parse_number('0.00')], + ], + }, + }, + }, + 'headers': { + 'APCA-PARTNER-ID': 'ccxt', + }, + 'options': { + 'defaultExchange': 'CBSE', + 'exchanges': [ + 'CBSE', # Coinbase + 'FTX', # FTXUS + 'GNSS', # Genesis + 'ERSX', # ErisX + ], + 'defaultTimeInForce': 'gtc', # fok, gtc, ioc + 'clientOrderId': 'ccxt_{id}', + }, + 'features': { + 'spot': { + 'sandbox': True, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': { + 'triggerPriceType': { + 'last': True, + 'mark': True, + 'index': True, + }, + 'price': True, + }, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': True, # todo: implementation + 'leverage': False, + 'marketBuyRequiresPrice': False, + 'marketBuyByCost': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 100000, + 'untilDays': 100000, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 500, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': 500, + 'daysBack': 100000, + 'untilDays': 100000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 500, + 'daysBack': 100000, + 'daysBackCanceled': None, + 'untilDays': 100000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 1000, + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'exceptions': { + 'exact': { + 'forbidden.': PermissionDenied, # {"message": "forbidden."} + '40410000': InvalidOrder, # {"code": 40410000, "message": "order is not found."} + '40010001': BadRequest, # {"code":40010001,"message":"invalid order type for crypto order"} + '40110000': PermissionDenied, # {"code": 40110000, "message": "request is not authorized"} + '40310000': InsufficientFunds, # {"available":"0","balance":"0","code":40310000,"message":"insufficient balance for USDT(requested: 221.63, available: 0)","symbol":"USDT"} + '42910000': RateLimitExceeded, # {"code":42910000,"message":"rate limit exceeded"} + }, + 'broad': { + 'Invalid format for parameter': BadRequest, # {"message":"Invalid format for parameter start: error parsing '0' or 2006-01-02 time: parsing time \"0\" as \"2006-01-02\": cannot parse \"0\" as \"2006\""} + 'Invalid symbol': BadSymbol, # {"message":"Invalid symbol(s): BTC/USDdsda does not match ^[A-Z]+/[A-Z]+$"} + }, + }, + }) + + def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = self.traderPrivateGetV2Clock(params) + # + # { + # timestamp: '2023-11-22T08:07:57.654738097-05:00', + # is_open: False, + # next_open: '2023-11-22T09:30:00-05:00', + # next_close: '2023-11-22T16:00:00-05:00' + # } + # + timestamp = self.safe_string(response, 'timestamp') + localTime = timestamp[0:23] + jetlagStrStart = len(timestamp) - 6 + jetlagStrEnd = len(timestamp) - 3 + jetlag = timestamp[jetlagStrStart:jetlagStrEnd] + iso = self.parse8601(localTime) - self.parse_to_numeric(jetlag) * 3600 * 1000 + return iso + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for alpaca + + https://docs.alpaca.markets/reference/get-v2-assets + + :param dict [params]: extra parameters specific to the exchange api endpoint + :returns dict[]: an array of objects representing market data + """ + request: dict = { + 'asset_class': 'crypto', + 'status': 'active', + } + assets = self.traderPrivateGetV2Assets(self.extend(request, params)) + # + # [ + # { + # "id": "c150e086-1e75-44e6-9c2c-093bb1e93139", + # "class": "crypto", + # "exchange": "CRYPTO", + # "symbol": "BTC/USDT", + # "name": "Bitcoin / USD Tether", + # "status": "active", + # "tradable": True, + # "marginable": False, + # "maintenance_margin_requirement": 100, + # "shortable": False, + # "easy_to_borrow": False, + # "fractionable": True, + # "attributes": [], + # "min_order_size": "0.000026873", + # "min_trade_increment": "0.000000001", + # "price_increment": "1" + # } + # ] + # + return self.parse_markets(assets) + + def parse_market(self, asset) -> Market: + # + # { + # "id": "c150e086-1e75-44e6-9c2c-093bb1e93139", + # "class": "crypto", + # "exchange": "CRYPTO", + # "symbol": "BTC/USDT", + # "name": "Bitcoin / USD Tether", + # "status": "active", + # "tradable": True, + # "marginable": False, + # "maintenance_margin_requirement": 101, + # "shortable": False, + # "easy_to_borrow": False, + # "fractionable": True, + # "attributes": [], + # "min_order_size": "0.000026873", + # "min_trade_increment": "0.000000001", + # "price_increment": "1" + # } + # + marketId = self.safe_string(asset, 'symbol') + parts = marketId.split('/') + assetClass = self.safe_string(asset, 'class') + baseId = self.safe_string(parts, 0) + quoteId = self.safe_string(parts, 1) + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + # Us equity markets do not include quote in symbol. + # We can safely coerce us_equity quote to USD + if quote is None and assetClass == 'us_equity': + quote = 'USD' + symbol = base + '/' + quote + status = self.safe_string(asset, 'status') + active = (status == 'active') + minAmount = self.safe_number(asset, 'min_order_size') + amount = self.safe_number(asset, 'min_trade_increment') + price = self.safe_number(asset, 'price_increment') + return { + 'id': marketId, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': None, + 'swap': False, + 'future': False, + 'option': False, + 'active': active, + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': amount, + 'price': price, + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': minAmount, + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + 'info': asset, + } + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://docs.alpaca.markets/reference/cryptotrades + https://docs.alpaca.markets/reference/cryptolatesttrades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.loc]: crypto location, default: us + :param str [params.method]: method, default: marketPublicGetV1beta3CryptoLocTrades + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + marketId = market['id'] + loc = self.safe_string(params, 'loc', 'us') + method = self.safe_string(params, 'method', 'marketPublicGetV1beta3CryptoLocTrades') + request: dict = { + 'symbols': marketId, + 'loc': loc, + } + params = self.omit(params, ['loc', 'method']) + symbolTrades = None + if method == 'marketPublicGetV1beta3CryptoLocTrades': + if since is not None: + request['start'] = self.iso8601(since) + if limit is not None: + request['limit'] = limit + response = self.marketPublicGetV1beta3CryptoLocTrades(self.extend(request, params)) + # + # { + # "next_page_token": null, + # "trades": { + # "BTC/USD": [ + # { + # "i": 36440704, + # "p": 22625, + # "s": 0.0001, + # "t": "2022-07-21T11:47:31.073391Z", + # "tks": "B" + # } + # ] + # } + # } + # + trades = self.safe_dict(response, 'trades', {}) + symbolTrades = self.safe_list(trades, marketId, []) + elif method == 'marketPublicGetV1beta3CryptoLocLatestTrades': + response = self.marketPublicGetV1beta3CryptoLocLatestTrades(self.extend(request, params)) + # + # { + # "trades": { + # "BTC/USD": { + # "i": 36440704, + # "p": 22625, + # "s": 0.0001, + # "t": "2022-07-21T11:47:31.073391Z", + # "tks": "B" + # } + # } + # } + # + trades = self.safe_dict(response, 'trades', {}) + symbolTrades = self.safe_dict(trades, marketId, {}) + symbolTrades = [symbolTrades] + else: + raise NotSupported(self.id + ' fetchTrades() does not support ' + method + ', marketPublicGetV1beta3CryptoLocTrades and marketPublicGetV1beta3CryptoLocLatestTrades are supported') + return self.parse_trades(symbolTrades, market, since, limit) + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://docs.alpaca.markets/reference/cryptolatestorderbooks + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.loc]: crypto location, default: us + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + id = market['id'] + loc = self.safe_string(params, 'loc', 'us') + request: dict = { + 'symbols': id, + 'loc': loc, + } + response = self.marketPublicGetV1beta3CryptoLocLatestOrderbooks(self.extend(request, params)) + # + # { + # "orderbooks":{ + # "BTC/USD":{ + # "a":[ + # { + # "p":22208, + # "s":0.0051 + # }, + # { + # "p":22209, + # "s":0.1123 + # }, + # { + # "p":22210, + # "s":0.2465 + # } + # ], + # "b":[ + # { + # "p":22203, + # "s":0.395 + # }, + # { + # "p":22202, + # "s":0.2465 + # }, + # { + # "p":22201, + # "s":0.6455 + # } + # ], + # "t":"2022-07-19T13:41:55.13210112Z" + # } + # } + # } + # + orderbooks = self.safe_dict(response, 'orderbooks', {}) + rawOrderbook = self.safe_dict(orderbooks, id, {}) + timestamp = self.parse8601(self.safe_string(rawOrderbook, 't')) + return self.parse_order_book(rawOrderbook, market['symbol'], timestamp, 'b', 'a', 'p', 's') + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://docs.alpaca.markets/reference/cryptobars + https://docs.alpaca.markets/reference/cryptolatestbars + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the alpha api endpoint + :param str [params.loc]: crypto location, default: us + :param str [params.method]: method, default: marketPublicGetV1beta3CryptoLocBars + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + market = self.market(symbol) + marketId = market['id'] + loc = self.safe_string(params, 'loc', 'us') + method = self.safe_string(params, 'method', 'marketPublicGetV1beta3CryptoLocBars') + request: dict = { + 'symbols': marketId, + 'loc': loc, + } + params = self.omit(params, ['loc', 'method']) + ohlcvs = None + if method == 'marketPublicGetV1beta3CryptoLocBars': + if limit is not None: + request['limit'] = limit + if since is not None: + request['start'] = self.yyyymmdd(since) + request['timeframe'] = self.safe_string(self.timeframes, timeframe, timeframe) + response = self.marketPublicGetV1beta3CryptoLocBars(self.extend(request, params)) + # + # { + # "bars": { + # "BTC/USD": [ + # { + # "c": 22887, + # "h": 22888, + # "l": 22873, + # "n": 11, + # "o": 22883, + # "t": "2022-07-21T05:00:00Z", + # "v": 1.1138, + # "vw": 22883.0155324116 + # }, + # { + # "c": 22895, + # "h": 22895, + # "l": 22884, + # "n": 6, + # "o": 22884, + # "t": "2022-07-21T05:01:00Z", + # "v": 0.001, + # "vw": 22889.5 + # } + # ] + # }, + # "next_page_token": "QlRDL1VTRHxNfDIwMjItMDctMjFUMDU6MDE6MDAuMDAwMDAwMDAwWg==" + # } + # + bars = self.safe_dict(response, 'bars', {}) + ohlcvs = self.safe_list(bars, marketId, []) + elif method == 'marketPublicGetV1beta3CryptoLocLatestBars': + response = self.marketPublicGetV1beta3CryptoLocLatestBars(self.extend(request, params)) + # + # { + # "bars": { + # "BTC/USD": { + # "c": 22887, + # "h": 22888, + # "l": 22873, + # "n": 11, + # "o": 22883, + # "t": "2022-07-21T05:00:00Z", + # "v": 1.1138, + # "vw": 22883.0155324116 + # } + # } + # } + # + bars = self.safe_dict(response, 'bars', {}) + ohlcvs = self.safe_dict(bars, marketId, {}) + ohlcvs = [ohlcvs] + else: + raise NotSupported(self.id + ' fetchOHLCV() does not support ' + method + ', marketPublicGetV1beta3CryptoLocBars and marketPublicGetV1beta3CryptoLocLatestBars are supported') + return self.parse_ohlcvs(ohlcvs, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # { + # "c":22895, + # "h":22895, + # "l":22884, + # "n":6, + # "o":22884, + # "t":"2022-07-21T05:01:00Z", + # "v":0.001, + # "vw":22889.5 + # } + # + datetime = self.safe_string(ohlcv, 't') + timestamp = self.parse8601(datetime) + return [ + timestamp, # timestamp + self.safe_number(ohlcv, 'o'), # open + self.safe_number(ohlcv, 'h'), # high + self.safe_number(ohlcv, 'l'), # low + self.safe_number(ohlcv, 'c'), # close + self.safe_number(ohlcv, 'v'), # volume + ] + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://docs.alpaca.markets/reference/cryptosnapshots-1 + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.loc]: crypto location, default: us + :returns dict: a `ticker structure ` + """ + self.load_markets() + symbol = self.symbol(symbol) + tickers = self.fetch_tickers([symbol], params) + return self.safe_dict(tickers, symbol) + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://docs.alpaca.markets/reference/cryptosnapshots-1 + + :param str[] symbols: unified symbols of the markets to fetch tickers for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.loc]: crypto location, default: us + :returns dict: a dictionary of `ticker structures ` + """ + if symbols is None: + raise ArgumentsRequired(self.id + ' fetchTickers() requires a symbols argument') + self.load_markets() + symbols = self.market_symbols(symbols) + loc = self.safe_string(params, 'loc', 'us') + ids = self.market_ids(symbols) + request = { + 'symbols': ','.join(ids), + 'loc': loc, + } + params = self.omit(params, 'loc') + response = self.marketPublicGetV1beta3CryptoLocSnapshots(self.extend(request, params)) + # + # { + # "snapshots": { + # "BTC/USD": { + # "dailyBar": { + # "c": 69403.554, + # "h": 69609.6515, + # "l": 69013.26, + # "n": 9, + # "o": 69536.7, + # "t": "2024-11-01T05:00:00Z", + # "v": 0.210809181, + # "vw": 69327.655393908 + # }, + # "latestQuote": { + # "ap": 69424.19, + # "as": 0.68149, + # "bp": 69366.086, + # "bs": 0.68312, + # "t": "2024-11-01T08:31:41.880246926Z" + # }, + # "latestTrade": { + # "i": 5272941104897543146, + # "p": 69416.9, + # "s": 0.014017324, + # "t": "2024-11-01T08:14:28.245088803Z", + # "tks": "B" + # }, + # "minuteBar": { + # "c": 69403.554, + # "h": 69403.554, + # "l": 69399.125, + # "n": 0, + # "o": 69399.125, + # "t": "2024-11-01T08:30:00Z", + # "v": 0, + # "vw": 0 + # }, + # "prevDailyBar": { + # "c": 69515.1415, + # "h": 72668.837, + # "l": 68796.85, + # "n": 129, + # "o": 72258.9, + # "t": "2024-10-31T05:00:00Z", + # "v": 2.217683307, + # "vw": 70782.6811608144 + # } + # }, + # } + # } + # + results = [] + snapshots = self.safe_dict(response, 'snapshots', {}) + marketIds = list(snapshots.keys()) + for i in range(0, len(marketIds)): + marketId = marketIds[i] + market = self.safe_market(marketId) + entry = self.safe_dict(snapshots, marketId) + dailyBar = self.safe_dict(entry, 'dailyBar', {}) + prevDailyBar = self.safe_dict(entry, 'prevDailyBar', {}) + latestQuote = self.safe_dict(entry, 'latestQuote', {}) + latestTrade = self.safe_dict(entry, 'latestTrade', {}) + datetime = self.safe_string(latestQuote, 't') + ticker = self.safe_ticker({ + 'info': entry, + 'symbol': market['symbol'], + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + 'high': self.safe_string(dailyBar, 'h'), + 'low': self.safe_string(dailyBar, 'l'), + 'bid': self.safe_string(latestQuote, 'bp'), + 'bidVolume': self.safe_string(latestQuote, 'bs'), + 'ask': self.safe_string(latestQuote, 'ap'), + 'askVolume': self.safe_string(latestQuote, 'as'), + 'vwap': self.safe_string(dailyBar, 'vw'), + 'open': self.safe_string(dailyBar, 'o'), + 'close': self.safe_string(dailyBar, 'c'), + 'last': self.safe_string(latestTrade, 'p'), + 'previousClose': self.safe_string(prevDailyBar, 'c'), + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': self.safe_string(dailyBar, 'v'), + 'quoteVolume': self.safe_string(dailyBar, 'n'), + }, market) + results.append(ticker) + return self.filter_by_array(results, 'symbol', symbols) + + def generate_client_order_id(self, params): + clientOrderIdprefix = self.safe_string(self.options, 'clientOrderId') + uuid = self.uuid() + parts = uuid.split('-') + random_id = ''.join(parts) + defaultClientId = self.implode_params(clientOrderIdprefix, {'id': random_id}) + clientOrderId = self.safe_string(params, 'clientOrderId', defaultClientId) + return clientOrderId + + def create_market_order_with_cost(self, symbol: str, side: OrderSide, cost: float, params={}): + """ + create a market order by providing the symbol, side and cost + + https://docs.alpaca.markets/reference/postorder + + :param str symbol: unified symbol of the market to create an order in + :param str side: 'buy' or 'sell' + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + req = { + 'cost': cost, + } + return self.create_order(symbol, 'market', side, 0, None, self.extend(req, params)) + + def create_market_buy_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market buy order by providing the symbol and cost + + https://docs.alpaca.markets/reference/postorder + + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + req = { + 'cost': cost, + } + return self.create_order(symbol, 'market', 'buy', 0, None, self.extend(req, params)) + + def create_market_sell_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market sell order by providing the symbol and cost + + https://docs.alpaca.markets/reference/postorder + + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + req = { + 'cost': cost, + } + return self.create_order(symbol, 'market', 'sell', cost, None, self.extend(req, params)) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://docs.alpaca.markets/reference/postorder + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market', 'limit' or 'stop_limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: The price at which a trigger order is triggered at + :param float [params.cost]: *market orders only* the cost of the order in units of the quote currency + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + id = market['id'] + request: dict = { + 'symbol': id, + 'side': side, + 'type': type, # market, limit, stop_limit + } + triggerPrice = self.safe_string_n(params, ['triggerPrice', 'stop_price']) + if triggerPrice is not None: + newType: str + if type.find('limit') >= 0: + newType = 'stop_limit' + else: + raise NotSupported(self.id + ' createOrder() does not support stop orders for ' + type + ' orders, only stop_limit orders are supported') + request['stop_price'] = self.price_to_precision(symbol, triggerPrice) + request['type'] = newType + if type.find('limit') >= 0: + request['limit_price'] = self.price_to_precision(symbol, price) + cost = self.safe_string(params, 'cost') + if cost is not None: + params = self.omit(params, 'cost') + request['notional'] = self.cost_to_precision(symbol, cost) + else: + request['qty'] = self.amount_to_precision(symbol, amount) + defaultTIF = self.safe_string(self.options, 'defaultTimeInForce') + request['time_in_force'] = self.safe_string(params, 'timeInForce', defaultTIF) + params = self.omit(params, ['timeInForce', 'triggerPrice']) + request['client_order_id'] = self.generate_client_order_id(params) + params = self.omit(params, ['clientOrderId']) + order = self.traderPrivatePostV2Orders(self.extend(request, params)) + # + # { + # "id": "61e69015-8549-4bfd-b9c3-01e75843f47d", + # "client_order_id": "eb9e2aaa-f71a-4f51-b5b4-52a6c565dad4", + # "created_at": "2021-03-16T18:38:01.942282Z", + # "updated_at": "2021-03-16T18:38:01.942282Z", + # "submitted_at": "2021-03-16T18:38:01.937734Z", + # "filled_at": null, + # "expired_at": null, + # "canceled_at": null, + # "failed_at": null, + # "replaced_at": null, + # "replaced_by": null, + # "replaces": null, + # "asset_id": "b0b6dd9d-8b9b-48a9-ba46-b9d54906e415", + # "symbol": "AAPL", + # "asset_class": "us_equity", + # "notional": "500", + # "qty": null, + # "filled_qty": "0", + # "filled_avg_price": null, + # "order_class": "", + # "order_type": "market", + # "type": "market", + # "side": "buy", + # "time_in_force": "day", + # "limit_price": null, + # "stop_price": null, + # "status": "accepted", + # "extended_hours": False, + # "legs": null, + # "trail_percent": null, + # "trail_price": null, + # "hwm": null + # } + # + return self.parse_order(order, market) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://docs.alpaca.markets/reference/deleteorderbyorderid + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + request: dict = { + 'order_id': id, + } + response = self.traderPrivateDeleteV2OrdersOrderId(self.extend(request, params)) + # + # { + # "code": 40410000, + # "message": "order is not found." + # } + # + return self.parse_order(response) + + def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders in a market + + https://docs.alpaca.markets/reference/deleteallorders + + :param str symbol: alpaca cancelAllOrders cannot setting symbol, it will cancel all open orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + self.load_markets() + response = self.traderPrivateDeleteV2Orders(params) + if isinstance(response, list): + return self.parse_orders(response, None) + else: + return [ + self.safe_order({ + 'info': response, + }), + ] + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://docs.alpaca.markets/reference/getorderbyorderid + + :param str id: the order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + request: dict = { + 'order_id': id, + } + order = self.traderPrivateGetV2OrdersOrderId(self.extend(request, params)) + marketId = self.safe_string(order, 'symbol') + market = self.safe_market(marketId) + return self.parse_order(order, market) + + def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://docs.alpaca.markets/reference/getallorders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch orders for + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + request: dict = { + 'status': 'all', + } + market = None + if symbol is not None: + market = self.market(symbol) + request['symbols'] = market['id'] + until = self.safe_integer(params, 'until') + if until is not None: + params = self.omit(params, 'until') + request['endTime'] = self.iso8601(until) + if since is not None: + request['after'] = self.iso8601(since) + if limit is not None: + request['limit'] = limit + response = self.traderPrivateGetV2Orders(self.extend(request, params)) + # + # [ + # { + # "id": "cbaf12d7-69b8-49c0-a31b-b46af35c755c", + # "client_order_id": "ccxt_b36156ae6fd44d098ac9c179bab33efd", + # "created_at": "2023-11-17T04:21:42.234579Z", + # "updated_at": "2023-11-17T04:22:34.442765Z", + # "submitted_at": "2023-11-17T04:21:42.233357Z", + # "filled_at": null, + # "expired_at": null, + # "canceled_at": "2023-11-17T04:22:34.399019Z", + # "failed_at": null, + # "replaced_at": null, + # "replaced_by": null, + # "replaces": null, + # "asset_id": "77c6f47f-0939-4b23-b41e-47b4469c4bc8", + # "symbol": "LTC/USDT", + # "asset_class": "crypto", + # "notional": null, + # "qty": "0.001", + # "filled_qty": "0", + # "filled_avg_price": null, + # "order_class": "", + # "order_type": "limit", + # "type": "limit", + # "side": "sell", + # "time_in_force": "gtc", + # "limit_price": "1000", + # "stop_price": null, + # "status": "canceled", + # "extended_hours": False, + # "legs": null, + # "trail_percent": null, + # "trail_price": null, + # "hwm": null, + # "subtag": null, + # "source": "access_key" + # } + # ] + # + return self.parse_orders(response, market, since, limit) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://docs.alpaca.markets/reference/getallorders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch orders for + :returns Order[]: a list of `order structures ` + """ + request: dict = { + 'status': 'open', + } + return self.fetch_orders(symbol, since, limit, self.extend(request, params)) + + def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://docs.alpaca.markets/reference/getallorders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch orders for + :returns Order[]: a list of `order structures ` + """ + request: dict = { + 'status': 'closed', + } + return self.fetch_orders(symbol, since, limit, self.extend(request, params)) + + def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + """ + edit a trade order + + https://docs.alpaca.markets/reference/patchorderbyorderid-1 + + :param str id: order id + :param str [symbol]: unified symbol of the market to create an order in + :param str [type]: 'market', 'limit' or 'stop_limit' + :param str [side]: 'buy' or 'sell' + :param float [amount]: how much of the currency you want to trade in units of the base currency + :param float [price]: the price for the order, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.triggerPrice]: the price to trigger a stop order + :param str [params.timeInForce]: for crypto trading either 'gtc' or 'ioc' can be used + :param str [params.clientOrderId]: a unique identifier for the order, automatically generated if not sent + :returns dict: an `order structure ` + """ + self.load_markets() + request: dict = { + 'order_id': id, + } + market = None + if symbol is not None: + market = self.market(symbol) + if amount is not None: + request['qty'] = self.amount_to_precision(symbol, amount) + triggerPrice = self.safe_string_n(params, ['triggerPrice', 'stop_price']) + if triggerPrice is not None: + request['stop_price'] = self.price_to_precision(symbol, triggerPrice) + params = self.omit(params, 'triggerPrice') + if price is not None: + request['limit_price'] = self.price_to_precision(symbol, price) + timeInForce = None + timeInForce, params = self.handle_option_and_params_2(params, 'editOrder', 'timeInForce', 'defaultTimeInForce') + if timeInForce is not None: + request['time_in_force'] = timeInForce + request['client_order_id'] = self.generate_client_order_id(params) + params = self.omit(params, ['clientOrderId']) + response = self.traderPrivatePatchV2OrdersOrderId(self.extend(request, params)) + return self.parse_order(response, market) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # { + # "id":"6ecfcc34-4bed-4b53-83ba-c564aa832a81", + # "client_order_id":"ccxt_1c6ceab0b5e84727b2f1c0394ba17560", + # "created_at":"2022-06-14T13:59:30.224037068Z", + # "updated_at":"2022-06-14T13:59:30.224037068Z", + # "submitted_at":"2022-06-14T13:59:30.221856828Z", + # "filled_at":null, + # "expired_at":null, + # "canceled_at":null, + # "failed_at":null, + # "replaced_at":null, + # "replaced_by":null, + # "replaces":null, + # "asset_id":"64bbff51-59d6-4b3c-9351-13ad85e3c752", + # "symbol":"BTCUSD", + # "asset_class":"crypto", + # "notional":null, + # "qty":"0.01", + # "filled_qty":"0", + # "filled_avg_price":null, + # "order_class":"", + # "order_type":"limit", + # "type":"limit", + # "side":"buy", + # "time_in_force":"day", + # "limit_price":"14000", + # "stop_price":null, + # "status":"accepted", + # "extended_hours":false, + # "legs":null, + # "trail_percent":null, + # "trail_price":null, + # "hwm":null, + # "commission":"0.42", + # "source":null + # } + # + marketId = self.safe_string(order, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + alpacaStatus = self.safe_string(order, 'status') + status = self.parse_order_status(alpacaStatus) + feeValue = self.safe_string(order, 'commission') + fee = None + if feeValue is not None: + fee = { + 'cost': feeValue, + 'currency': 'USD', + } + orderType = self.safe_string(order, 'order_type') + if orderType is not None: + if orderType.find('limit') >= 0: + # might be limit or stop-limit + orderType = 'limit' + datetime = self.safe_string(order, 'submitted_at') + timestamp = self.parse8601(datetime) + return self.safe_order({ + 'id': self.safe_string(order, 'id'), + 'clientOrderId': self.safe_string(order, 'client_order_id'), + 'timestamp': timestamp, + 'datetime': datetime, + 'lastTradeTimeStamp': None, + 'status': status, + 'symbol': symbol, + 'type': orderType, + 'timeInForce': self.parse_time_in_force(self.safe_string(order, 'time_in_force')), + 'postOnly': None, + 'side': self.safe_string(order, 'side'), + 'price': self.safe_number(order, 'limit_price'), + 'triggerPrice': self.safe_number(order, 'stop_price'), + 'cost': None, + 'average': self.safe_number(order, 'filled_avg_price'), + 'amount': self.safe_number(order, 'qty'), + 'filled': self.safe_number(order, 'filled_qty'), + 'remaining': None, + 'trades': None, + 'fee': fee, + 'info': order, + }, market) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'pending_new': 'open', + 'accepted': 'open', + 'new': 'open', + 'partially_filled': 'open', + 'activated': 'open', + 'filled': 'closed', + } + return self.safe_string(statuses, status, status) + + def parse_time_in_force(self, timeInForce: Str): + timeInForces: dict = { + 'day': 'Day', + } + return self.safe_string(timeInForces, timeInForce, timeInForce) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://docs.alpaca.markets/reference/getaccountactivitiesbyactivitytype-1 + + :param str [symbol]: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trade structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch trades for + :param str [params.page_token]: page_token - used for paging + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = None + request: dict = { + 'activity_type': 'FILL', + } + if symbol is not None: + market = self.market(symbol) + until = self.safe_integer(params, 'until') + if until is not None: + params = self.omit(params, 'until') + request['until'] = self.iso8601(until) + if since is not None: + request['after'] = self.iso8601(since) + if limit is not None: + request['page_size'] = limit + request, params = self.handle_until_option('until', request, params) + response = self.traderPrivateGetV2AccountActivitiesActivityType(self.extend(request, params)) + # + # [ + # { + # "id": "20221228071929579::ca2aafd0-1270-4b56-b0a9-85423b4a07c8", + # "activity_type": "FILL", + # "transaction_time": "2022-12-28T12:19:29.579352Z", + # "type": "fill", + # "price": "67.31", + # "qty": "0.07", + # "side": "sell", + # "symbol": "LTC/USD", + # "leaves_qty": "0", + # "order_id": "82eebcf7-6e66-4b7e-93f8-be0df0e4f12e", + # "cum_qty": "0.07", + # "order_status": "filled", + # "swap_rate": "1" + # }, + # ] + # + return self.parse_trades(response, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades + # + # { + # "t":"2022-06-14T05:00:00.027869Z", + # "x":"CBSE", + # "p":"21942.15", + # "s":"0.0001", + # "tks":"S", + # "i":"355681339" + # } + # + # fetchMyTrades + # + # { + # "id": "20221228071929579::ca2aafd0-1270-4b56-b0a9-85423b4a07c8", + # "activity_type": "FILL", + # "transaction_time": "2022-12-28T12:19:29.579352Z", + # "type": "fill", + # "price": "67.31", + # "qty": "0.07", + # "side": "sell", + # "symbol": "LTC/USD", + # "leaves_qty": "0", + # "order_id": "82eebcf7-6e66-4b7e-93f8-be0df0e4f12e", + # "cum_qty": "0.07", + # "order_status": "filled", + # "swap_rate": "1" + # }, + # + marketId = self.safe_string_2(trade, 'S', 'symbol') + symbol = self.safe_symbol(marketId, market) + datetime = self.safe_string_2(trade, 't', 'transaction_time') + timestamp = self.parse8601(datetime) + alpacaSide = self.safe_string(trade, 'tks') + side = self.safe_string(trade, 'side') + if alpacaSide == 'B': + side = 'buy' + elif alpacaSide == 'S': + side = 'sell' + priceString = self.safe_string_2(trade, 'p', 'price') + amountString = self.safe_string_2(trade, 's', 'qty') + return self.safe_trade({ + 'info': trade, + 'id': self.safe_string_2(trade, 'i', 'id'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'order': self.safe_string(trade, 'order_id'), + 'type': None, + 'side': side, + 'takerOrMaker': 'taker', + 'price': priceString, + 'amount': amountString, + 'cost': None, + 'fee': None, + }, market) + + def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://docs.alpaca.markets/reference/listcryptofundingwallets + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'asset': currency['id'], + } + response = self.traderPrivateGetV2Wallets(self.extend(request, params)) + # + # { + # "asset_id": "4fa30c85-77b7-4cbc-92dd-7b7513640aad", + # "address": "bc1q2fpskfnwem3uq9z8660e4z6pfv7aqfamysk75r", + # "created_at": "2024-11-03T07:30:05.609976344Z" + # } + # + return self.parse_deposit_address(response, currency) + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + # + # { + # "asset_id": "4fa30c85-77b7-4cbc-92dd-7b7513640aad", + # "address": "bc1q2fpskfnwem3uq9z8660e4z6pfv7aqfamysk75r", + # "created_at": "2024-11-03T07:30:05.609976344Z" + # } + # + parsedCurrency = None + if currency is not None: + parsedCurrency = currency['id'] + return { + 'info': depositAddress, + 'currency': parsedCurrency, + 'network': None, + 'address': self.safe_string(depositAddress, 'address'), + 'tag': None, + } + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://docs.alpaca.markets/reference/createcryptotransferforaccount + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: a memo for the transaction + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.check_address(address) + self.load_markets() + currency = self.currency(code) + if tag: + address = address + ':' + tag + request: dict = { + 'asset': currency['id'], + 'address': address, + 'amount': self.number_to_string(amount), + } + response = self.traderPrivatePostV2WalletsTransfers(self.extend(request, params)) + # + # { + # "id": "e27b70a6-5610-40d7-8468-a516a284b776", + # "tx_hash": null, + # "direction": "OUTGOING", + # "amount": "20", + # "usd_value": "19.99856", + # "chain": "ETH", + # "asset": "USDT", + # "from_address": "0x123930E4dCA196E070d39B60c644C8Aae02f23", + # "to_address": "0x1232c0925196e4dcf05945f67f690153190fbaab", + # "status": "PROCESSING", + # "created_at": "2024-11-07T02:39:01.775495Z", + # "network_fee": "4", + # "fees": "0.1" + # } + # + return self.parse_transaction(response, currency) + + def fetch_transactions_helper(self, type, code, since, limit, params): + self.load_markets() + currency = None + if code is not None: + currency = self.currency(code) + response = self.traderPrivateGetV2WalletsTransfers(params) + # + # { + # "id": "e27b70a6-5610-40d7-8468-a516a284b776", + # "tx_hash": null, + # "direction": "OUTGOING", + # "amount": "20", + # "usd_value": "19.99856", + # "chain": "ETH", + # "asset": "USDT", + # "from_address": "0x123930E4dCA196E070d39B60c644C8Aae02f23", + # "to_address": "0x1232c0925196e4dcf05945f67f690153190fbaab", + # "status": "PROCESSING", + # "created_at": "2024-11-07T02:39:01.775495Z", + # "network_fee": "4", + # "fees": "0.1" + # } + # + results = [] + for i in range(0, len(response)): + entry = response[i] + direction = self.safe_string(entry, 'direction') + if direction == type: + results.append(entry) + elif type == 'BOTH': + results.append(entry) + return self.parse_transactions(results, currency, since, limit, params) + + def fetch_deposits_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch history of deposits and withdrawals + + https://docs.alpaca.markets/reference/listcryptofundingtransfers + + :param str [code]: unified currency code for the currency of the deposit/withdrawals, default is None + :param int [since]: timestamp in ms of the earliest deposit/withdrawal, default is None + :param int [limit]: max number of deposit/withdrawals to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `transaction structure ` + """ + return self.fetch_transactions_helper('BOTH', code, since, limit, params) + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://docs.alpaca.markets/reference/listcryptofundingtransfers + + :param str [code]: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposit structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + return self.fetch_transactions_helper('INCOMING', code, since, limit, params) + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://docs.alpaca.markets/reference/listcryptofundingtransfers + + :param str [code]: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawal structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + return self.fetch_transactions_helper('OUTGOING', code, since, limit, params) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # { + # "id": "e27b70a6-5610-40d7-8468-a516a284b776", + # "tx_hash": null, + # "direction": "OUTGOING", + # "amount": "20", + # "usd_value": "19.99856", + # "chain": "ETH", + # "asset": "USDT", + # "from_address": "0x123930E4dCA196E070d39B60c644C8Aae02f23", + # "to_address": "0x1232c0925196e4dcf05945f67f690153190fbaab", + # "status": "PROCESSING", + # "created_at": "2024-11-07T02:39:01.775495Z", + # "network_fee": "4", + # "fees": "0.1" + # } + # + datetime = self.safe_string(transaction, 'created_at') + currencyId = self.safe_string(transaction, 'asset') + code = self.safe_currency_code(currencyId, currency) + fees = self.safe_string(transaction, 'fees') + networkFee = self.safe_string(transaction, 'network_fee') + totalFee = Precise.string_add(fees, networkFee) + fee = { + 'cost': self.parse_number(totalFee), + 'currency': code, + } + return { + 'info': transaction, + 'id': self.safe_string(transaction, 'id'), + 'txid': self.safe_string(transaction, 'tx_hash'), + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + 'network': self.safe_string(transaction, 'chain'), + 'address': self.safe_string(transaction, 'to_address'), + 'addressTo': self.safe_string(transaction, 'to_address'), + 'addressFrom': self.safe_string(transaction, 'from_address'), + 'tag': None, + 'tagTo': None, + 'tagFrom': None, + 'type': self.parse_transaction_type(self.safe_string(transaction, 'direction')), + 'amount': self.safe_number(transaction, 'amount'), + 'currency': code, + 'status': self.parse_transaction_status(self.safe_string(transaction, 'status')), + 'updated': None, + 'fee': fee, + 'comment': None, + 'internal': None, + } + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'PROCESSING': 'pending', + 'FAILED': 'failed', + 'COMPLETE': 'ok', + } + return self.safe_string(statuses, status, status) + + def parse_transaction_type(self, type): + types: dict = { + 'INCOMING': 'deposit', + 'OUTGOING': 'withdrawal', + } + return self.safe_string(types, type, type) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://docs.alpaca.markets/reference/getaccount-1 + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + response = self.traderPrivateGetV2Account(params) + # + # { + # "id": "43a01bde-4eb1-64fssc26adb5", + # "admin_configurations": { + # "allow_instant_ach": True, + # "max_margin_multiplier": "4" + # }, + # "user_configurations": { + # "fractional_trading": True, + # "max_margin_multiplier": "4" + # }, + # "account_number": "744873727", + # "status": "ACTIVE", + # "crypto_status": "ACTIVE", + # "currency": "USD", + # "buying_power": "5.92", + # "regt_buying_power": "5.92", + # "daytrading_buying_power": "0", + # "effective_buying_power": "5.92", + # "non_marginable_buying_power": "5.92", + # "bod_dtbp": "0", + # "cash": "5.92", + # "accrued_fees": "0", + # "portfolio_value": "48.6", + # "pattern_day_trader": False, + # "trading_blocked": False, + # "transfers_blocked": False, + # "account_blocked": False, + # "created_at": "2022-06-13T14:59:18.318096Z", + # "trade_suspended_by_user": False, + # "multiplier": "1", + # "shorting_enabled": False, + # "equity": "48.6", + # "last_equity": "48.8014266", + # "long_market_value": "42.68", + # "short_market_value": "0", + # "position_market_value": "42.68", + # "initial_margin": "0", + # "maintenance_margin": "0", + # "last_maintenance_margin": "0", + # "sma": "5.92", + # "daytrade_count": 0, + # "balance_asof": "2024-12-10", + # "crypto_tier": 1, + # "intraday_adjustments": "0", + # "pending_reg_taf_fees": "0" + # } + # + return self.parse_balance(response) + + def parse_balance(self, response) -> Balances: + result: dict = {'info': response} + account = self.account() + currencyId = self.safe_string(response, 'currency') + code = self.safe_currency_code(currencyId) + account['free'] = self.safe_string(response, 'cash') + account['total'] = self.safe_string(response, 'equity') + result[code] = account + return self.safe_balance(result) + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + endpoint = '/' + self.implode_params(path, params) + url = self.implode_hostname(self.urls['api'][api[0]]) + headers = headers if (headers is not None) else {} + if api[1] == 'private': + self.check_required_credentials() + headers['APCA-API-KEY-ID'] = self.apiKey + headers['APCA-API-SECRET-KEY'] = self.secret + query = self.omit(params, self.extract_params(path)) + if query: + if (method == 'GET') or (method == 'DELETE'): + endpoint += '?' + self.urlencode(query) + else: + body = self.json(query) + headers['Content-Type'] = 'application/json' + url = url + endpoint + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None # default error handler + # { + # "code": 40110000, + # "message": "request is not authorized" + # } + feedback = self.id + ' ' + body + errorCode = self.safe_string(response, 'code') + if code is not None: + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + message = self.safe_value(response, 'message', None) + if message is not None: + self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + raise ExchangeError(feedback) + return None diff --git a/ccxt/apex.py b/ccxt/apex.py new file mode 100644 index 0000000..46deba4 --- /dev/null +++ b/ccxt/apex.py @@ -0,0 +1,1898 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.apex import ImplicitAPI +import hashlib +import math +from ccxt.base.types import Account, Any, Balances, Currencies, Currency, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, Trade, MarketInterface, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.decimal_to_precision import TRUNCATE +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class apex(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(apex, self).describe(), { + 'id': 'apex', + 'name': 'Apex', + 'countries': [], + 'version': 'v3', + 'rateLimit': 20, # 600 requests per minute, 10 request per second + 'certified': False, + 'pro': True, + 'dex': True, + 'has': { + 'CORS': None, + 'spot': False, + 'margin': False, + 'swap': True, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelAllOrders': True, + 'cancelAllOrdersAfter': False, + 'cancelOrder': True, + 'cancelOrders': False, + 'cancelOrdersForSymbols': False, + 'closeAllPositions': False, + 'closePosition': False, + 'createMarketBuyOrderWithCost': False, + 'createMarketOrderWithCost': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createOrders': False, + 'createPostOnlyOrder': True, + 'createReduceOnlyOrder': True, + 'createStopOrder': True, + 'createTriggerOrder': True, + 'editOrder': False, + 'fetchAccounts': True, + 'fetchAllGreeks': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchCanceledAndClosedOrders': False, + 'fetchCanceledOrders': False, + 'fetchClosedOrders': False, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': False, + 'fetchDepositAddresses': False, + 'fetchDeposits': False, + 'fetchDepositWithdrawFee': False, + 'fetchDepositWithdrawFees': False, + 'fetchFundingHistory': True, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': True, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchLedger': False, + 'fetchLeverage': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchMarginMode': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMyLiquidations': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': True, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchOrderTrades': True, + 'fetchPosition': False, + 'fetchPositionMode': False, + 'fetchPositions': True, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': False, + 'fetchTransfer': True, + 'fetchTransfers': True, + 'fetchVolatilityHistory': False, + 'fetchWithdrawal': False, + 'fetchWithdrawals': False, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'sandbox': True, + 'setLeverage': True, + 'setMarginMode': False, + 'setPositionMode': False, + 'transfer': False, + 'withdraw': False, + }, + 'timeframes': { + '1m': '1', + '5m': '5', + '15m': '15', + '30m': '30', + '1h': '60', + '2h': '120', + '4h': '240', + '6h': '360', + '12h': '720', + '1d': 'D', + '1w': 'W', + '1M': 'M', + }, + 'hostname': 'omni.apex.exchange', + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/fef8f2f7-4265-46aa-965e-33a91881cb00', + 'api': { + 'public': 'https://{hostname}/api', + 'private': 'https://{hostname}/api', + }, + 'test': { + 'public': 'https://testnet.omni.apex.exchange/api', + 'private': 'https://testnet.omni.apex.exchange/api', + }, + 'www': 'https://apex.exchange/', + 'doc': 'https://api-docs.pro.apex.exchange', + 'fees': 'https://apex-pro.gitbook.io/apex-pro/apex-omni-live-now/trading-perpetual-contracts/trading-fees', + 'referral': 'https://omni.apex.exchange/trade', + }, + 'api': { + 'public': { + 'get': { + 'v3/symbols': 1, + 'v3/history-funding': 1, + 'v3/ticker': 1, + 'v3/klines': 1, + 'v3/trades': 1, + 'v3/depth': 1, + 'v3/time': 1, + 'v3/data/all-ticker-info': 1, + }, + }, + 'private': { + 'get': { + 'v3/account': 1, + 'v3/account-balance': 1, + 'v3/fills': 1, + 'v3/order-fills': 1, + 'v3/order': 1, + 'v3/history-orders': 1, + 'v3/order-by-client-order-id': 1, + 'v3/funding': 1, + 'v3/historical-pnl': 1, + 'v3/open-orders': 1, + 'v3/transfers': 1, + 'v3/transfer': 1, + }, + 'post': { + 'v3/delete-open-orders': 1, + 'v3/delete-client-order-id': 1, + 'v3/delete-order': 1, + 'v3/order': 1, + 'v3/set-initial-margin-rate': 1, + 'v3/transfer-out': 1, + 'v3/contract-transfer-out': 1, + }, + }, + }, + 'httpExceptions': { + '403': RateLimitExceeded, # Forbidden -- You request too many times + }, + 'exceptions': { + # Uncodumented explanation of error strings: + # - oc_diff: order cost needed to place self order + # - new_oc: total order cost of open orders including the order you are trying to open + # - ob: order balance - the total cost of current open orders + # - ab: available balance + 'exact': { + '20006': 'apikey sign error', # apikey sign error + '20016': 'request para error', # apikey sign error + '10001': BadRequest, + }, + 'broad': { + 'ORDER_PRICE_MUST_GREETER_ZERO': InvalidOrder, + 'ORDER_POSSIBLE_LEAD_TO_ACCOUNT_LIQUIDATED': InvalidOrder, + 'ORDER_WITH_THIS_PRICE_CANNOT_REDUCE_POSITION_ONLY': InvalidOrder, + }, + }, + 'fees': { + 'swap': { + 'taker': self.parse_number('0.0005'), + 'maker': self.parse_number('0.0002'), + }, + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + 'walletAddress': False, + 'privateKey': False, + 'password': True, + }, + 'precisionMode': TICK_SIZE, + 'commonCurrencies': {}, + 'options': { + 'defaultType': 'swap', + 'defaultSlippage': 0.05, + 'brokerId': '6956', + }, + 'features': { + 'default': { + 'sandbox': True, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': True, + }, + 'hedged': False, + 'selfTradePrevention': False, + 'trailing': True, # todo unify + 'leverage': False, + 'marketBuyByCost': False, + 'marketBuyRequiresPrice': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 500, + 'daysBack': 100000, + 'untilDays': 100000, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 100000, + 'untilDays': 100000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchClosedOrders': None, + 'fetchOHLCV': { + 'limit': 200, + }, + }, + 'swap': { + 'linear': { + 'extends': 'default', + }, + 'inverse': None, + }, + }, + }) + + def fetch_time(self, params={}): + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://api-docs.pro.apex.exchange/#publicapi-v3-for-omni-get-system-time-v3 + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = self.publicGetV3Time(params) + data = self.safe_dict(response, 'data', {}) + # + # { + # "data": { + # "time": 1738837534454 + # } + # } + return self.safe_integer(data, 'time') + + def parse_balance(self, response) -> Balances: + # + # { + # "totalEquityValue": "100.000000", + # "availableBalance": "100.000000", + # "initialMargin": "100.000000", + # "maintenanceMargin": "100.000000", + # "symbolToOraclePrice": { + # "BTC-USDC": { + # "oraclePrice": "20000", + # "createdTime": 124566 + # } + # } + # } + # + timestamp = self.milliseconds() + result: dict = { + 'info': response, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + code = 'USDT' + account = self.account() + account['free'] = self.safe_string(response, 'availableBalance') + account['total'] = self.safe_string(response, 'totalEquityValue') + result[code] = account + return self.safe_balance(result) + + def fetch_balance(self, params={}) -> Balances: + """ + query for account info + + https://api-docs.pro.apex.exchange/#privateapi-v3-for-omni-get-retrieve-user-account-balance + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + response = self.privateGetV3AccountBalance(params) + data = self.safe_dict(response, 'data', {}) + return self.parse_balance(data) + + def parse_account(self, account: dict) -> Account: + accountId = self.safe_string(account, 'id', '0') + return { + 'id': accountId, + 'type': None, + 'code': None, + 'info': account, + } + + def fetch_account(self, params={}) -> Account: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://api-docs.pro.apex.exchange/#privateapi-v3-for-omni-get-retrieve-user-account-data + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + response = self.privateGetV3Account(params) + data = self.safe_dict(response, 'data', {}) + return self.parse_account(data) + + def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://api-docs.pro.apex.exchange/#publicapi-v3-for-omni-get-all-config-data-v3 + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = self.publicGetV3Symbols(params) + data = self.safe_dict(response, 'data', {}) + spotConfig = self.safe_dict(data, 'spotConfig', {}) + multiChain = self.safe_dict(spotConfig, 'multiChain', {}) + # "spotConfig": { + # "assets": [ + # { + # "tokenId": "141", + # "token": "USDT", + # "displayName": "Tether USD Coin", + # "decimals": 18, + # "showStep": "0.01", + # "iconUrl": "https://static-pro.apex.exchange/chains/chain_tokens/Ethereum/Ethereum_USDT.svg", + # "l2WithdrawFee": "0", + # "enableCollateral": True, + # "enableCrossCollateral": False, + # "crossCollateralDiscountRate": null, + # "isGray": False + # } + # ], + # "multiChain": { + # "chains": [ + # { + # "chain": "Arbitrum One", + # "chainId": "9", + # "chainType": "0", + # "l1ChainId": "42161", + # "chainIconUrl": "https://static-pro.apex.exchange/chains/chain_logos/Arbitrum.svg", + # "contractAddress": "0x3169844a120c0f517b4eb4a750c08d8518c8466a", + # "swapContractAddress": "0x9e07b6Aef1bbD9E513fc2Eb8873e311E80B4f855", + # "stopDeposit": False, + # "feeLess": False, + # "gasLess": False, + # "gasToken": "ETH", + # "dynamicFee": True, + # "gasTokenDecimals": 18, + # "feeGasLimit": 300000, + # "blockTimeSeconds": 2, + # "rpcUrl": "https://arb.pro.apex.exchange", + # "minSwapUsdtAmount": "", + # "maxSwapUsdtAmount": "", + # "webRpcUrl": "https://arb.pro.apex.exchange", + # "webTxUrl": "https://arbiscan.io/tx/", + # "backupRpcUrl": "https://arb-mainnet.g.alchemy.com/v2/rGlYUbRHtUav5mfeThCPtsV9GLPt2Xq5", + # "txConfirm": 20, + # "withdrawGasFeeLess": False, + # "tokens": [ + # { + # "decimals": 6, + # "iconUrl": "https://static-pro.apex.exchange/chains/chain_tokens/Arbitrum/Arbitrum_USDT.svg", + # "token": "USDT", + # "tokenAddress": "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", + # "pullOff": False, + # "withdrawEnable": True, + # "slippage": "", + # "isDefaultToken": False, + # "displayToken": "USDT", + # "needResetApproval": True, + # "minFee": "2", + # "maxFee": "40", + # "feeRate": "0.0001", + # "maxWithdraw": "", + # "minDeposit": "", + # "minWithdraw": "", + # "maxFastWithdrawAmount": "40000", + # "minFastWithdrawAmount": "1", + # "isGray": False + # }, + # { + # "decimals": 6, + # "iconUrl": "https://static-pro.apex.exchange/chains/chain_tokens/Arbitrum/Arbitrum_USDC.svg", + # "token": "USDC", + # "tokenAddress": "0xaf88d065e77c8cc2239327c5edb3a432268e5831", + # "pullOff": False, + # "withdrawEnable": True, + # "slippage": "", + # "isDefaultToken": False, + # "displayToken": "USDC", + # "needResetApproval": True, + # "minFee": "2", + # "maxFee": "20", + # "feeRate": "0.0001", + # "maxWithdraw": "", + # "minDeposit": "", + # "minWithdraw": "", + # "maxFastWithdrawAmount": "1", + # "minFastWithdrawAmount": "1", + # "isGray": False + # } + # ] + # } + # ] + # } + rows = self.safe_list(spotConfig, 'assets', []) + chains = self.safe_list(multiChain, 'chains', []) + result: dict = {} + for i in range(0, len(rows)): + currency = rows[i] + currencyId = self.safe_string(currency, 'token') + code = self.safe_currency_code(currencyId) + name = self.safe_string(currency, 'displayName') + networks: dict = {} + for j in range(0, len(chains)): + chain = chains[j] + tokens = self.safe_list(chain, 'tokens', []) + for f in range(0, len(tokens)): + token = tokens[f] + tokenName = self.safe_string(token, 'token') + if tokenName == currencyId: + networkId = self.safe_string(chain, 'chainId') + networkCode = self.network_id_to_code(networkId) + networks[networkCode] = { + 'info': chain, + 'id': networkId, + 'network': networkCode, + 'active': None, + 'deposit': not self.safe_bool(chain, 'depositDisable'), + 'withdraw': self.safe_bool(token, 'withdrawEnable'), + 'fee': self.safe_number(token, 'minFee'), + 'precision': self.parse_number(self.parse_precision(self.safe_string(token, 'decimals'))), + 'limits': { + 'withdraw': { + 'min': self.safe_number(token, 'minWithdraw'), + 'max': None, + }, + 'deposit': { + 'min': self.safe_number(chain, 'minDeposit'), + 'max': None, + }, + }, + } + networkKeys = list(networks.keys()) + networksLength = len(networkKeys) + emptyChains = networksLength == 0 # non-functional coins + valueForEmpty = False if emptyChains else None + result[code] = self.safe_currency_structure({ + 'info': currency, + 'code': code, + 'id': currencyId, + 'type': 'crypto', + 'name': name, + 'active': None, + 'deposit': valueForEmpty, + 'withdraw': valueForEmpty, + 'fee': None, + 'precision': None, + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + 'deposit': { + 'min': None, + 'max': None, + }, + }, + 'networks': networks, + }) + return result + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for apex + + https://api-docs.pro.apex.exchange/#publicapi-v3-for-omni-get-all-config-data-v3 + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = self.publicGetV3Symbols(params) + data = self.safe_dict(response, 'data', {}) + contractConfig = self.safe_dict(data, 'contractConfig', {}) + perpetualContract = self.safe_list(contractConfig, 'perpetualContract', []) + # { + # "perpetualContract":[ + # { + # "baselinePositionValue": "50000.0000", + # "crossId": 30002, + # "crossSymbolId": 10, + # "crossSymbolName": "BTCUSDT", + # "digitMerge": "0.1,0.2,0.4,1,2", + # "displayMaxLeverage": "100", + # "displayMinLeverage": "1", + # "enableDisplay": True, + # "enableOpenPosition": True, + # "enableTrade": True, + # "fundingImpactMarginNotional": "6", + # "fundingInterestRate": "0.0003", + # "incrementalInitialMarginRate": "0.00250", + # "incrementalMaintenanceMarginRate": "0.00100", + # "incrementalPositionValue": "50000.0000", + # "initialMarginRate": "0.01", + # "maintenanceMarginRate": "0.005", + # "maxOrderSize": "50", + # "maxPositionSize": "100", + # "minOrderSize": "0.0010", + # "maxMarketPriceRange": "0.025", + # "settleAssetId": "USDT", + # "baseTokenId": "BTC", + # "stepSize": "0.001", + # "symbol": "BTC-USDT", + # "symbolDisplayName": "BTCUSDT", + # "tickSize": "0.1", + # "maxMaintenanceMarginRate": "0.5000", + # "maxPositionValue": "5000000.0000", + # "tagIconUrl": "https://static-pro.apex.exchange/icon/LABLE_HOT.svg", + # "tag": "HOT", + # "riskTip": False, + # "defaultInitialMarginRate": "0.05", + # "klineStartTime": 0, + # "maxMarketSizeBuffer": "0.98", + # "enableFundingSettlement": True, + # "indexPriceDecimals": 2, + # "indexPriceVarRate": "0.001", + # "openPositionOiLimitRate": "0.05", + # "fundingMaxRate": "0.000234", + # "fundingMinRate": "-0.000234", + # "fundingMaxValue": "", + # "enableFundingMxValue": True, + # "l2PairId": "50001", + # "settleTimeStamp": 0, + # "isPrelaunch": False, + # "riskLimitConfig": {}, + # "category": "L1" + # } + # ] + # } + return self.parse_markets(perpetualContract) + + def parse_market(self, market: dict) -> Market: + id = self.safe_string(market, 'symbol') + id2 = self.safe_string(market, 'crossSymbolName') + quoteId = self.safe_string(market, 'l2PairId') + baseId = self.safe_string(market, 'baseTokenId') + quote = self.safe_string(market, 'settleAssetId') + base = self.safe_currency_code(baseId) + settleId = self.safe_string(market, 'settleAssetId') + settle = self.safe_currency_code(settleId) + symbol = baseId + '/' + quote + ':' + settle + expiry = 0 + takerFee = self.parse_number('0.0002') + makerFee = self.parse_number('0.0005') + return self.safe_market_structure({ + 'id': id, + 'id2': id2, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': 'swap', + 'spot': False, + 'margin': None, + 'swap': True, + 'future': False, + 'option': False, + 'active': self.safe_bool(market, 'enableTrade'), + 'contract': True, + 'linear': True, + 'inverse': False, + 'taker': takerFee, + 'maker': makerFee, + 'contractSize': self.safe_number(market, 'minOrderSize'), + 'expiry': None if (expiry == 0) else expiry, + 'expiryDatetime': None if (expiry == 0) else self.iso8601(expiry), + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.safe_number(market, 'stepSize'), + 'price': self.safe_number(market, 'tickSize'), + }, + 'limits': { + 'leverage': { + 'min': self.safe_number(market, 'displayMinLeverage'), + 'max': self.safe_number(market, 'displayMaxLeverage'), + }, + 'amount': { + 'min': self.safe_number(market, 'minOrderSize'), + 'max': self.safe_number(market, 'maxOrderSize'), + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + 'info': market, + }) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "symbol": "BTCUSDT", + # "price24hPcnt": "0.450141", + # "lastPrice": "43511.50", + # "highPrice24h": "43513.50", + # "lowPrice24h": "29996.00", + # "markPrice": "43513.50", + # "indexPrice": "40828.94", + # "openInterest": "2036854775808", + # "turnover24h": "5626085.23749999", + # "volume24h": "169.317", + # "fundingRate": "0", + # "predictedFundingRate": "0", + # "nextFundingTime": "10:00:00", + # "tradeCount": 100 + # } + # + timestamp = self.milliseconds() + marketId = self.safe_string(ticker, 'symbol') + market = self.safe_market(marketId, market) + symbol = self.safe_symbol(marketId, market) + last = self.safe_string(ticker, 'lastPrice') + percentage = self.safe_string(ticker, 'price24hPcnt') + quoteVolume = self.safe_string(ticker, 'turnover24h') + baseVolume = self.safe_string(ticker, 'volume24h') + high = self.safe_string(ticker, 'highPrice24h') + low = self.safe_string(ticker, 'lowPrice24h') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': high, + 'low': low, + 'bid': None, + 'bidVolume': None, + 'ask': None, + 'askVolume': None, + 'vwap': None, + 'open': None, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': percentage, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'markPrice': self.safe_string(ticker, 'markPrice'), + 'indexPrice': self.safe_string(ticker, 'indexPrice'), + 'info': ticker, + }, market) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://api-docs.pro.apex.exchange/#publicapi-v3-for-omni-get-ticker-data-v3 + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id2'], + } + response = self.publicGetV3Ticker(self.extend(request, params)) + tickers = self.safe_list(response, 'data', []) + rawTicker = self.safe_dict(tickers, 0, {}) + return self.parse_ticker(rawTicker, market) + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://api-docs.pro.apex.exchange/#publicapi-v3-for-omni-get-ticker-data-v3 + + :param str symbols: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + response = self.publicGetV3DataAllTickerInfo(params) + tickers = self.safe_list(response, 'data', []) + return self.parse_tickers(tickers, symbols) + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://api-docs.pro.apex.exchange/#publicapi-v3-for-omni-get-candlestick-chart-data-v3 + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest candle to fetch + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'interval': self.safe_string(self.timeframes, timeframe, timeframe), + 'symbol': market['id2'], + } + if limit is None: + limit = 200 # default is 200 when requested with `since` + request['limit'] = limit # max 200, default 200 + request, params = self.handle_until_option('end', request, params, 0.001) + if since is not None: + request['start'] = int(math.floor(since / 1000)) + response = self.publicGetV3Klines(self.extend(request, params)) + data = self.safe_dict(response, 'data', {}) + OHLCVs = self.safe_list(data, market['id2'], []) + return self.parse_ohlcvs(OHLCVs, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # { + # "start": 1647511440000, + # "symbol": "BTC-USD", + # "interval": "1", + # "low": "40000", + # "high": "45000", + # "open": "45000", + # "close": "40000", + # "volume": "1.002", + # "turnover": "3" + # } {"s":"BTCUSDT","i":"1","t":1741265880000,"c":"90235","h":"90235","l":"90156","o":"90156","v":"0.052","tr":"4690.4466"} + # + return [ + self.safe_integer_n(ohlcv, ['start', 't']), + self.safe_number_n(ohlcv, ['open', 'o']), + self.safe_number_n(ohlcv, ['high', 'h']), + self.safe_number_n(ohlcv, ['low', 'l']), + self.safe_number_n(ohlcv, ['close', 'c']), + self.safe_number_n(ohlcv, ['volume', 'v']), + ] + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://api-docs.pro.apex.exchange/#publicapi-v3-for-omni-get-market-depth-v3 + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id2'], + } + if limit is None: + limit = 100 # default is 200 when requested with `since` + request['limit'] = limit # max 100, default 100 + response = self.publicGetV3Depth(self.extend(request, params)) + # + # { + # "a": [ + # [ + # "96576.3", + # "0.399" + # ], + # [ + # "96577.6", + # "0.106" + # ] + # ], + # "b": [ + # [ + # "96565.2", + # "0.131" + # ], + # [ + # "96565.1", + # "0.038" + # ] + # ], + # "s": "BTCUSDT", + # "u": 18665465 + # } + # + data = self.safe_dict(response, 'data', {}) + timestamp = self.milliseconds() + orderbook = self.parse_order_book(data, market['symbol'], timestamp, 'b', 'a') + orderbook['nonce'] = self.safe_integer(data, 'u') + return orderbook + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://api-docs.pro.apex.exchange/#publicapi-v3-for-omni-get-newest-trading-data-v3 + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch trades for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id2'], + } + if limit is None: + limit = 500 # default is 50 + request['limit'] = limit + response = self.publicGetV3Trades(self.extend(request, params)) + # + # [ + # { + # "i": "993f7f85-9215-5723-9078-2186ae140847", + # "p": "96534.3", + # "S": "Sell", + # "v": "0.261", + # "s": "BTCUSDT", + # "T": 1739118072710 + # }, + # { + # "i": "c947c9cf-8c18-5784-89c3-91bdf86ddde8", + # "p": "96513.5", + # "S": "Sell", + # "v": "0.042", + # "s": "BTCUSDT", + # "T": 1739118075944 + # } + # ] + # + trades = self.safe_list(response, 'data', []) + return self.parse_trades(trades, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # [ + # { + # "i": "993f7f85-9215-5723-9078-2186ae140847", + # "p": "96534.3", + # "S": "Sell", + # "v": "0.261", + # "s": "BTCUSDT", + # "T": 1739118072710 + # } + # ] + # + marketId = self.safe_string_n(trade, ['s', 'symbol']) + market = self.safe_market(marketId, market) + id = self.safe_string_n(trade, ['i', 'id']) + timestamp = self.safe_integer_n(trade, ['t', 'T', 'createdAt']) + priceString = self.safe_string_n(trade, ['p', 'price']) + amountString = self.safe_string_n(trade, ['v', 'size']) + side = self.safe_string_lower_n(trade, ['S', 'side']) + type = self.safe_string_n(trade, ['type']) + fee = self.safe_string_n(trade, ['fee']) + return self.safe_trade({ + 'info': trade, + 'id': id, + 'order': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'type': type, + 'takerOrMaker': None, + 'side': side, + 'price': priceString, + 'amount': amountString, + 'cost': None, + 'fee': fee, + }, market) + + def fetch_open_interest(self, symbol: str, params={}): + """ + retrieves the open interest of a contract trading pair + + https://api-docs.pro.apex.exchange/#publicapi-v3-for-omni-get-ticker-data-v3 + + :param str symbol: unified CCXT market symbol + :param dict [params]: exchange specific parameters + :returns dict} an open interest structure{@link https://docs.ccxt.com/#/?id=open-interest-structure: + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id2'], + } + response = self.publicGetV3Ticker(self.extend(request, params)) + tickers = self.safe_list(response, 'data', []) + rawTicker = self.safe_dict(tickers, 0, {}) + return self.parse_open_interest(rawTicker, market) + + def parse_open_interest(self, interest, market: Market = None): + # + # { + # "symbol": "BTCUSDT", + # "price24hPcnt": "0.450141", + # "lastPrice": "43511.50", + # "highPrice24h": "43513.50", + # "lowPrice24h": "29996.00", + # "markPrice": "43513.50", + # "indexPrice": "40828.94", + # "openInterest": "2036854775808", + # "turnover24h": "5626085.23749999", + # "volume24h": "169.317", + # "fundingRate": "0", + # "predictedFundingRate": "0", + # "nextFundingTime": "10:00:00", + # "tradeCount": 100 + # } + # + timestamp = self.milliseconds() + marketId = self.safe_string(interest, 'symbol') + market = self.safe_market(marketId, market) + symbol = self.safe_symbol(marketId, market) + return self.safe_open_interest({ + 'symbol': symbol, + 'openInterestAmount': self.safe_string(interest, 'openInterest'), + 'openInterestValue': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'info': interest, + }, market) + + def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical funding rate prices + + https://api-docs.pro.apex.exchange/#publicapi-v3-for-omni-get-funding-rate-history-v3 + + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: timestamp in ms of the earliest funding rate to fetch + :param int [limit]: the maximum amount of `funding rate structures ` to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest funding rate + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `funding rate structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchFundingRateHistory() requires a symbol argument') + self.load_markets() + request: dict = {} + market = self.market(symbol) + request['symbol'] = market['id'] + if since is not None: + request['beginTimeInclusive'] = since + if limit is not None: + request['limit'] = limit + page = self.safe_integer(params, 'page') + if page is not None: + request['page'] = page + endTimeExclusive = self.safe_integer_n(params, ['endTime', 'endTimeExclusive', 'until']) + if endTimeExclusive is not None: + request['endTimeExclusive'] = endTimeExclusive + response = self.publicGetV3HistoryFunding(self.extend(request, params)) + # + # { + # "historyFunds": [ + # { + # "symbol": "BTC-USD", + # "rate": "0.0000125000", + # "price": "31297.5000008009374142", + # "fundingTime": 12315555, + # "fundingTimestamp": 12315555 + # } + # ], + # "totalSize": 11 + # } + # + rates = [] + data = self.safe_dict(response, 'data', {}) + resultList = self.safe_list(data, 'historyFunds', []) + for i in range(0, len(resultList)): + entry = resultList[i] + timestamp = self.safe_integer(entry, 'fundingTimestamp') + marketId = self.safe_string(entry, 'symbol') + rates.append({ + 'info': entry, + 'symbol': self.safe_symbol(marketId, market), + 'fundingRate': self.safe_number(entry, 'rate'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + sorted = self.sort_by(rates, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, symbol, since, limit) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # { + # "id": "1234", + # "clientId": "1234", + # "accountId": "12345", + # "symbol": "BTC-USD", + # "side": "SELL", + # "price": "18000", + # "limitFee": "100", + # "fee": "100", + # "triggerPrice": "1.2", + # "trailingPercent": "0.12", + # "size": "100", + # "remainingSize": "100", + # "type": "LIMIT", + # "createdAt": 1647502440973, + # "updatedTime": 1647502440973, + # "expiresAt": 1647502440973, + # "status": "PENDING", + # "timeInForce": "GOOD_TIL_CANCEL", + # "postOnly": False, + # "reduceOnly": False, + # "stopPnl": False, + # "latestMatchFillPrice": "reason", + # "cumMatchFillSize": "0.1", + # "cumMatchFillValue": "1000", + # "cumMatchFillFee": "1", + # "cumSuccessFillSize": "0.1", + # "cumSuccessFillValue": "1000", + # "cumSuccessFillFee": "1", + # "triggerPriceType": "INDEX", + # "isOpenTpslOrder": True, + # "isSetOpenTp": True, + # "isSetOpenSl": False, + # "openTpParam": { + # "side": "SELL", + # "price": "18000", + # "limitFee": "100", + # "clientOrderId": "111100", + # "triggerPrice": "1.2", + # "trailingPercent": "0.12", + # "size": "100" + # }, + # "openSlParam": { + # "side": "SELL", + # "price": "18000", + # "limitFee": "100", + # "clientOrderId": "111100", + # "triggerPrice": "1.2", + # "trailingPercent": "0.12", + # "size": "100" + # } + # } + # + timestamp = self.safe_integer(order, 'createdAt') + orderId = self.safe_string(order, 'id') + clientOrderId = self.safe_string(order, 'clientId') + marketId = self.safe_string(order, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + price = self.safe_string(order, 'price') + amount = self.safe_string(order, 'size') + orderType = self.safe_string(order, 'type') + status = self.safe_string(order, 'status') + side = self.safe_string_lower(order, 'side') + # average = self.omit_zero(self.safe_string(order, 'avg_fill_price')) + remaining = self.omit_zero(self.safe_string(order, 'remainingSize')) + lastUpdateTimestamp = self.safe_integer(order, 'updatedTime') + return self.safe_order({ + 'id': orderId, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'lastUpdateTimestamp': lastUpdateTimestamp, + 'status': self.parse_order_status(status), + 'symbol': symbol, + 'type': self.parse_order_type(orderType), + 'timeInForce': self.parse_time_in_force(self.safe_string(order, 'timeInForce')), + 'postOnly': self.safe_bool(order, 'postOnly'), + 'reduceOnly': self.safe_bool(order, 'reduceOnly'), + 'side': side, + 'price': price, + 'triggerPrice': self.safe_string(order, 'triggerPrice'), + 'takeProfitPrice': None, + 'stopLossPrice': None, + 'average': None, + 'amount': amount, + 'filled': None, + 'remaining': remaining, + 'cost': None, + 'trades': None, + 'fee': { + 'cost': self.safe_string(order, 'fee'), + 'currency': market['settleId'], + }, + 'info': order, + }, market) + + def parse_time_in_force(self, timeInForce: Str): + timeInForces: dict = { + 'GOOD_TIL_CANCEL': 'GOOD_TIL_CANCEL', + 'FILL_OR_KILL': 'FILL_OR_KILL', + 'IMMEDIATE_OR_CANCEL': 'IMMEDIATE_OR_CANCEL', + 'POST_ONLY': 'POST_ONLY', + } + return self.safe_string(timeInForces, timeInForce, None) + + def parse_order_status(self, status: Str): + if status is not None: + statuses: dict = { + 'PENDING': 'open', + 'OPEN': 'open', + 'FILLED': 'filled', + 'CANCELING': 'canceled', + 'CANCELED': 'canceled', + 'UNTRIGGERED': 'open', + } + return self.safe_string(statuses, status, status) + return status + + def parse_order_type(self, type: Str): + types: dict = { + 'LIMIT': 'limit', + 'MARKET': 'market', + 'STOP_LIMIT': 'limit', + 'STOP_MARKET': 'market', + 'TAKE_PROFIT_LIMIT': 'limit', + 'TAKE_PROFIT_MARKET': 'market', + } + return self.safe_string(types, type, type) + + def safe_market(self, marketId: Str = None, market: Market = None, delimiter: Str = None, marketType: Str = None) -> MarketInterface: + if market is None and marketId is not None: + if marketId in self.markets: + market = self.markets[marketId] + elif marketId in self.markets_by_id: + market = self.markets_by_id[marketId] + else: + newMarketId = self.add_hyphen_before_usdt(marketId) + if newMarketId in self.markets_by_id: + markets = self.markets_by_id[newMarketId] + numMarkets = len(markets) + if numMarkets > 0: + if self.markets_by_id[newMarketId][0]['id2'] == marketId: + market = self.markets_by_id[newMarketId][0] + return super(apex, self).safe_market(marketId, market, delimiter, marketType) + + def generate_random_client_id_omni(self, _accountId: str): + accountId = _accountId or str(self.rand_number(12)) + return 'apexomni-' + accountId + '-' + str(self.milliseconds()) + '-' + str(self.rand_number(6)) + + def add_hyphen_before_usdt(self, symbol: str): + uppercaseSymbol = symbol.upper() + index = uppercaseSymbol.find('USDT') + symbolChar = self.safe_string(symbol, index - 1) + if index > 0 and symbolChar != '-': + return symbol[0:index] + '-' + symbol[index:] + return symbol + + def get_seeds(self): + seeds = self.safe_string(self.options, 'seeds') + if seeds is None: + raise ArgumentsRequired(self.id + ' the "seeds" key is required in the options to access private endpoints. You can find it in API Management > Omni Key, and then set it.options["seeds"] = XXXX') + return seeds + + def get_account_id(self): + accountId = self.safe_string(self.options, 'accountId', '0') + if accountId == '0': + accountData = self.fetch_account() + self.options['accountId'] = self.safe_string(accountData, 'id', '0') + return self.options['accountId'] + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://api-docs.pro.apex.exchange/#privateapi-v3-for-omni-post-creating-orders + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fullfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: The price a trigger order is triggered at + :param float [params.stopLossPrice]: The price a stop loss order is triggered at + :param float [params.takeProfitPrice]: The price a take profit order is triggered at + :param str [params.timeInForce]: "GTC", "IOC", or "POST_ONLY" + :param bool [params.postOnly]: True or False + :param bool [params.reduceOnly]: Ensures that the executed order does not flip the opened position. + :param str [params.clientOrderId]: a unique id for the order + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + orderType = type.upper() + orderSide = side.upper() + orderSize = self.amount_to_precision(symbol, amount) + orderPrice = '0' + if price is not None: + orderPrice = self.price_to_precision(symbol, price) + fees = self.safe_dict(self.fees, 'swap', {}) + taker = self.safe_string(fees, 'taker', '0.0005') + maker = self.safe_string(fees, 'maker', '0.0002') + limitFee = self.decimal_to_precision(Precise.string_add(Precise.string_mul(Precise.string_mul(orderPrice, orderSize), taker), self.number_to_string(market['precision']['price'])), TRUNCATE, market['precision']['price'], self.precisionMode, self.paddingMode) + timeNow = self.milliseconds() + triggerPrice = self.safe_string(params, 'triggerPrice') + stopLossPrice = self.safe_string(params, 'stopLossPrice') + takeProfitPrice = self.safe_string(params, 'takeProfitPrice') + if stopLossPrice is not None: + orderType = 'STOP_MARKET' if (orderType == 'MARKET') else 'STOP_LIMIT' + triggerPrice = stopLossPrice + elif takeProfitPrice is not None: + orderType = 'TAKE_PROFIT_MARKET' if (orderType == 'MARKET') else 'TAKE_PROFIT_LIMIT' + triggerPrice = takeProfitPrice + isMarket = orderType == 'MARKET' + if isMarket and (price is None): + raise ArgumentsRequired(self.id + ' createOrder() requires a price argument for market orders') + timeInForce = self.safe_string_upper(params, 'timeInForce') + postOnly = self.is_post_only(isMarket, None, params) + if timeInForce is None: + timeInForce = 'GOOD_TIL_CANCEL' + if not isMarket: + if postOnly: + timeInForce = 'POST_ONLY' + elif timeInForce == 'ioc': + timeInForce = 'IMMEDIATE_OR_CANCEL' + params = self.omit(params, 'timeInForce') + params = self.omit(params, 'postOnly') + clientOrderId = self.safe_string_n(params, ['clientId', 'clientOrderId', 'client_order_id']) + accountId = self.get_account_id() + if clientOrderId is None: + clientOrderId = self.generate_random_client_id_omni(accountId) + params = self.omit(params, ['clientId', 'clientOrderId', 'client_order_id', 'stopLossPrice', 'takeProfitPrice', 'triggerPrice']) + orderToSign = { + 'accountId': accountId, + 'slotId': clientOrderId, + 'nonce': clientOrderId, + 'pairId': market['quoteId'], + 'size': orderSize, + 'price': orderPrice, + 'direction': orderSide, + 'makerFeeRate': maker, + 'takerFeeRate': taker, + } + if triggerPrice is not None: + orderToSign['triggerPrice'] = self.price_to_precision(symbol, triggerPrice) + signature = self.get_zk_contract_signature_obj(self.remove0x_prefix(self.get_seeds()), orderToSign) + request: dict = { + 'symbol': market['id'], + 'side': orderSide, + 'type': orderType, # LIMIT/MARKET/STOP_LIMIT/STOP_MARKET + 'size': orderSize, + 'price': orderPrice, + 'limitFee': limitFee, + 'expiration': int(math.floor(timeNow / 1000 + 30 * 24 * 60 * 60)), + 'timeInForce': timeInForce, + 'clientId': clientOrderId, + 'brokerId': self.safe_string(self.options, 'brokerId', '6956'), + } + if triggerPrice is not None: + request['triggerPrice'] = self.price_to_precision(symbol, triggerPrice) + request['signature'] = signature + response = self.privatePostV3Order(self.extend(request, params)) + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data, market) + + def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account to transfer from + :param str toAccount: account to transfer to + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.transferId]: UUID, which is unique across the platform + :returns dict: a `transfer structure ` + """ + self.load_markets() + configResponse = self.publicGetV3Symbols(params) + configData = self.safe_dict(configResponse, 'data', {}) + contractConfig = self.safe_dict(configData, 'contractConfig', {}) + contractAssets = self.safe_list(contractConfig, 'assets', []) + spotConfig = self.safe_dict(configData, 'spotConfig', {}) + spotAssets = self.safe_list(spotConfig, 'assets', []) + globalConfig = self.safe_dict(spotConfig, 'global', {}) + receiverAddress = self.safe_string(globalConfig, 'contractAssetPoolEthAddress', '') + receiverZkAccountId = self.safe_string(globalConfig, 'contractAssetPoolZkAccountId', '') + receiverSubAccountId = self.safe_string(globalConfig, 'contractAssetPoolSubAccount', '') + receiverAccountId = self.safe_string(globalConfig, 'contractAssetPoolAccountId', '') + accountResponse = self.privateGetV3Account(params) + accountData = self.safe_dict(accountResponse, 'data', {}) + spotAccount = self.safe_dict(accountData, 'spotAccount', {}) + zkAccountId = self.safe_string(spotAccount, 'zkAccountId', '') + subAccountId = self.safe_string(spotAccount, 'defaultSubAccountId', '0') + subAccounts = self.safe_list(spotAccount, 'subAccounts', []) + nonce = '0' + if len(subAccounts) > 0: + nonce = self.safe_string(subAccounts[0], 'nonce', '0') + ethAddress = self.safe_string(accountData, 'ethereumAddress', '') + accountId = self.safe_string(accountData, 'id', '') + currency = {} + assets = [] + if fromAccount is not None and fromAccount.lower() == 'contract': + assets = contractAssets + else: + assets = spotAssets + for i in range(0, len(assets)): + if self.safe_string(assets[i], 'token', '') == code: + currency = assets[i] + tokenId = self.safe_string(currency, 'tokenId', '') + amountNumber = self.parse_to_int(amount * (math.pow(10, self.safe_number(currency, 'decimals', 0)))) + timestampSeconds = self.parse_to_int(self.milliseconds() / 1000) + clientOrderId = self.safe_string_n(params, ['clientId', 'clientOrderId', 'client_order_id']) + if clientOrderId is None: + clientOrderId = self.generate_random_client_id_omni(self.safe_string(self.options, 'accountId')) + params = self.omit(params, ['clientId', 'clientOrderId', 'client_order_id']) + if fromAccount is not None and fromAccount.lower() == 'contract': + formattedUint32 = '4294967295' + zkSignAccountId = Precise.string_mod(accountId, formattedUint32) + expireTime = timestampSeconds + 3600 * 24 * 28 + orderToSign = { + 'zkAccountId': zkSignAccountId, + 'receiverAddress': ethAddress, + 'subAccountId': subAccountId, + 'receiverSubAccountId': subAccountId, + 'tokenId': tokenId, + 'amount': str(amountNumber), + 'fee': '0', + 'nonce': clientOrderId, + 'timestampSeconds': expireTime, + 'isContract': True, + } + signature = self.get_zk_transfer_signature_obj(self.remove0x_prefix(self.get_seeds()), orderToSign) + request: dict = { + 'amount': amount, + 'expireTime': expireTime, + 'clientWithdrawId': clientOrderId, + 'signature': signature, + 'token': code, + 'ethAddress': ethAddress, + } + response = self.privatePostV3ContractTransferOut(self.extend(request, params)) + data = self.safe_dict(response, 'data', {}) + currentTime = self.milliseconds() + return self.extend(self.parse_transfer(data, self.currency(code)), { + 'timestamp': currentTime, + 'datetime': self.iso8601(currentTime), + 'amount': self.parse_number(amount), + 'fromAccount': 'contract', + 'toAccount': 'spot', + }) + else: + orderToSign = { + 'zkAccountId': zkAccountId, + 'receiverAddress': receiverAddress, + 'subAccountId': subAccountId, + 'receiverSubAccountId': receiverSubAccountId, + 'tokenId': tokenId, + 'amount': str(amountNumber), + 'fee': '0', + 'nonce': nonce, + 'timestampSeconds': timestampSeconds, + } + signature = self.get_zk_transfer_signature_obj(self.remove0x_prefix(self.get_seeds()), orderToSign) + request: dict = { + 'amount': str(amount), + 'timestamp': timestampSeconds, + 'clientTransferId': clientOrderId, + 'signature': signature, + 'zkAccountId': zkAccountId, + 'subAccountId': subAccountId, + 'fee': '0', + 'token': code, + 'tokenId': tokenId, + 'receiverAccountId': receiverAccountId, + 'receiverZkAccountId': receiverZkAccountId, + 'receiverSubAccountId': receiverSubAccountId, + 'receiverAddress': receiverAddress, + 'nonce': nonce, + } + response = self.privatePostV3TransferOut(self.extend(request, params)) + data = self.safe_dict(response, 'data', {}) + currentTime = self.milliseconds() + return self.extend(self.parse_transfer(data, self.currency(code)), { + 'timestamp': currentTime, + 'datetime': self.iso8601(currentTime), + 'amount': self.parse_number(amount), + 'fromAccount': 'spot', + 'toAccount': 'contract', + }) + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + currencyId = self.safe_string(transfer, 'coin') + timestamp = self.safe_integer(transfer, 'timestamp') + fromAccount = self.safe_string(transfer, 'fromAccount') + toAccount = self.safe_string(transfer, 'toAccount') + return { + 'info': transfer, + 'id': self.safe_string_n(transfer, ['transferId', 'id']), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'currency': self.safe_currency_code(currencyId, currency), + 'amount': self.safe_number(transfer, 'amount'), + 'fromAccount': fromAccount, + 'toAccount': toAccount, + 'status': self.safe_string(transfer, 'status'), + } + + def cancel_all_orders(self, symbol: Str = None, params={}) -> List[Order]: + """ + cancel all open orders in a market + + https://api-docs.pro.apex.exchange/#privateapi-v3-for-omni-post-cancel-all-open-orders + + :param str symbol: unified market symbol of the market to cancel orders in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + self.load_markets() + market = None + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + response = self.privatePostV3DeleteOpenOrders(self.extend(request, params)) + data = self.safe_dict(response, 'data', {}) + return [self.parse_order(data, market)] + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://api-docs.pro.apex.exchange/#privateapi-v3-for-omni-post-cancel-order + + :param str id: order id + @param symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + request: dict = {} + clientOrderId = self.safe_string_n(params, ['clientId', 'clientOrderId', 'client_order_id']) + response = None + if clientOrderId is not None: + request['id'] = clientOrderId + params = self.omit(params, ['clientId', 'clientOrderId', 'client_order_id']) + response = self.privatePostV3DeleteClientOrderId(self.extend(request, params)) + else: + request['id'] = id + response = self.privatePostV3DeleteOrder(self.extend(request, params)) + data = self.safe_dict(response, 'data', {}) + return self.safe_order(data) + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://api-docs.pro.apex.exchange/#privateapi-v3-for-omni-get-order-id + https://api-docs.pro.apex.exchange/#privateapi-v3-for-omni-get-order-by-clientorderid + + :param str id: the order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.clientOrderId]: a unique id for the order + :returns dict: An `order structure ` + """ + self.load_markets() + request: dict = {} + clientOrderId = self.safe_string_n(params, ['clientId', 'clientOrderId', 'client_order_id']) + response = None + if clientOrderId is not None: + request['id'] = clientOrderId + params = self.omit(params, ['clientId', 'clientOrderId', 'client_order_id']) + response = self.privateGetV3OrderByClientOrderId(self.extend(request, params)) + else: + request['id'] = id + response = self.privateGetV3Order(self.extend(request, params)) + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://api-docs.pro.apex.exchange/#privateapi-v3-for-omni-get-open-orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + response = self.privateGetV3OpenOrders(params) + orders = self.safe_list(response, 'data', []) + return self.parse_orders(orders, None, since, limit) + + def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user *classic accounts only* + + https://api-docs.pro.apex.exchange/#privateapi-v3-for-omni-get-all-order-history + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve, default 100 + :param dict [params]: extra parameters specific to the exchange API endpoint + :param dict [params.until]: end time, ms + :param boolean [params.status]: "PENDING", "OPEN", "FILLED", "CANCELED", "EXPIRED", "UNTRIGGERED" + :param boolean [params.side]: BUY or SELL + :param str [params.type]: "LIMIT", "MARKET","STOP_LIMIT", "STOP_MARKET", "TAKE_PROFIT_LIMIT","TAKE_PROFIT_MARKET" + :param str [params.orderType]: "ACTIVE","CONDITION","HISTORY" + :param boolean [params.page]: Page numbers start from 0 + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if since is not None: + request['beginTimeInclusive'] = since + if limit is not None: + request['limit'] = limit + endTimeExclusive = self.safe_integer_n(params, ['endTime', 'endTimeExclusive', 'until']) + if endTimeExclusive is not None: + request['endTimeExclusive'] = endTimeExclusive + params = self.omit(params, ['endTime', 'endTimeExclusive', 'until']) + response = self.privateGetV3HistoryOrders(self.extend(request, params)) + data = self.safe_dict(response, 'data', {}) + orders = self.safe_list(data, 'orders', []) + return self.parse_orders(orders, market, since, limit) + + def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all the trades made from a single order + + https://api-docs.pro.apex.exchange/#privateapi-v3-for-omni-get-trade-history + + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + self.load_markets() + request: dict = {} + clientOrderId = self.safe_string_2(params, 'clientOrderId', 'clientId') + if clientOrderId is not None: + request['clientOrderId'] = clientOrderId + else: + request['orderId'] = id + params = self.omit(params, ['clientOrderId', 'clientId']) + response = self.privateGetV3OrderFills(self.extend(request, params)) + data = self.safe_dict(response, 'data', {}) + orders = self.safe_list(data, 'orders', []) + return self.parse_trades(orders, None, since, limit) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches information on multiple orders made by the user *classic accounts only* + + https://api-docs.pro.apex.exchange/#privateapi-v3-for-omni-get-trade-history + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve, default 100 + :param dict [params]: extra parameters specific to the exchange API endpoint + :param dict [params.until]: end time + :param boolean [params.side]: BUY or SELL + :param str [params.orderType]: "LIMIT", "MARKET","STOP_LIMIT", "STOP_MARKET", "TAKE_PROFIT_LIMIT","TAKE_PROFIT_MARKET" + :param boolean [params.page]: Page numbers start from 0 + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if since is not None: + request['beginTimeInclusive'] = since + if limit is not None: + request['limit'] = limit + endTimeExclusive = self.safe_integer_n(params, ['endTime', 'endTimeExclusive', 'until']) + if endTimeExclusive is not None: + request['endTimeExclusive'] = endTimeExclusive + params = self.omit(params, ['endTime', 'endTimeExclusive', 'until']) + response = self.privateGetV3Fills(self.extend(request, params)) + data = self.safe_dict(response, 'data', {}) + orders = self.safe_list(data, 'orders', []) + return self.parse_trades(orders, market, since, limit) + + def fetch_funding_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches information on multiple orders made by the user *classic accounts only* + + https://api-docs.pro.apex.exchange/#privateapi-v3-for-omni-get-funding-rate + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve, default 100 + :param dict [params]: extra parameters specific to the exchange API endpoint + :param dict [params.until]: end time, ms + :param boolean [params.side]: BUY or SELL + :param boolean [params.page]: Page numbers start from 0 + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if since is not None: + request['beginTimeInclusive'] = since + if limit is not None: + request['limit'] = limit + endTimeExclusive = self.safe_integer_n(params, ['endTime', 'endTimeExclusive', 'until']) + if endTimeExclusive is not None: + params = self.omit(params, ['endTime', 'endTimeExclusive', 'until']) + request['endTimeExclusive'] = endTimeExclusive + response = self.privateGetV3Funding(self.extend(request, params)) + data = self.safe_dict(response, 'data', {}) + fundingValues = self.safe_list(data, 'fundingValues', []) + return self.parse_incomes(fundingValues, market, since, limit) + + def parse_income(self, income, market: Market = None): + # + # { + # "id": "1234", + # "symbol": "BTC-USDT", + # "fundingValue": "10000", + # "rate": "0.0000125000", + # "positionSize": "500", + # "price": "90", + # "side": "LONG", + # "status": "SUCCESS", + # "fundingTime": 1647502440973, + # "transactionId": "1234556" + # } + # + marketId = self.safe_string(income, 'symbol') + market = self.safe_market(marketId, market, None, 'contract') + code = 'USDT' + timestamp = self.safe_integer(income, 'fundingTime') + return { + 'info': income, + 'symbol': self.safe_symbol(marketId, market), + 'code': code, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': self.safe_string(income, 'id'), + 'amount': self.safe_number(income, 'fundingValue'), + 'rate': self.safe_number(income, 'rate'), + } + + def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://api-docs.pro.apex.exchange/#privateapi-v3-for-omni-post-sets-the-initial-margin-rate-of-a-contract + + :param float leverage: the rate of leverage + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setLeverage() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + leverageString = self.number_to_string(leverage) + initialMarginRate = Precise.string_div('1', leverageString, 4) + request: dict = { + 'symbol': market['id'], + 'initialMarginRate': initialMarginRate, + } + response = self.privatePostV3SetInitialMarginRate(self.extend(request, params)) + data = self.safe_dict(response, 'data', {}) + return data + + def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://api-docs.pro.apex.exchange/#privateapi-v3-for-omni-get-retrieve-user-account-data + + :param str[] [symbols]: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + self.load_markets() + response = self.privateGetV3Account(params) + data = self.safe_dict(response, 'data', {}) + positions = self.safe_list(data, 'positions', []) + return self.parse_positions(positions, symbols) + + def parse_position(self, position: dict, market: Market = None): + # + # { + # "symbol": "BTC-USDT", + # "status": "", + # "side": "LONG", + # "size": "0.000", + # "entryPrice": "0.00", + # "exitPrice": "", + # "createdAt": 1690366452416, + # "updatedTime": 1690366452416, + # "fee": "0.000000", + # "fundingFee": "0.000000", + # "lightNumbers": "", + # "customInitialMarginRate": "0" + # } + marketId = self.safe_string(position, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + side = self.safe_string_lower(position, 'side') + quantity = self.safe_string(position, 'size') + timestamp = self.safe_integer(position, 'updatedTime') + leverage = 20 + customInitialMarginRate = self.safe_string_n(position, ['customInitialMarginRate', 'customImr'], '0') + if self.precision_from_string(customInitialMarginRate) != 0: + leverage = self.parse_to_int(Precise.string_div('1', customInitialMarginRate, 4)) + return self.safe_position({ + 'info': position, + 'id': self.safe_string(position, 'id'), + 'symbol': symbol, + 'entryPrice': self.safe_string(position, 'entryPrice'), + 'markPrice': None, + 'notional': None, + 'collateral': None, + 'unrealizedPnl': None, + 'side': side, + 'contracts': self.parse_number(quantity), + 'contractSize': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'hedged': None, + 'maintenanceMargin': None, + 'maintenanceMarginPercentage': None, + 'initialMargin': None, + 'initialMarginPercentage': None, + 'leverage': leverage, + 'liquidationPrice': None, + 'marginRatio': None, + 'marginMode': None, + 'percentage': None, + }) + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = self.implode_hostname(self.urls['api'][api]) + '/' + path + headers = { + 'User-Agent': 'apex-CCXT', + 'Accept': 'application/json', + 'Content-Type': 'application/x-www-form-urlencoded', + } + signPath = '/api/' + path + signBody = body + if method.upper() != 'POST': + if params: + signPath += '?' + self.rawencode(params) + url += '?' + self.rawencode(params) + else: + sortedQuery = self.keysort(params) + signBody = self.rawencode(sortedQuery) + if api == 'private': + self.check_required_credentials() + timestamp = str(self.milliseconds()) + messageString = timestamp + method.upper() + signPath + if signBody is not None: + messageString = messageString + signBody + signature = self.hmac(self.encode(messageString), self.encode(self.string_to_base64(self.secret)), hashlib.sha256, 'base64') + headers['APEX-SIGNATURE'] = signature + headers['APEX-API-KEY'] = self.apiKey + headers['APEX-TIMESTAMP'] = timestamp + headers['APEX-PASSPHRASE'] = self.password + return {'url': url, 'method': method, 'body': signBody, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + # + # {"code":3,"msg":"Order price must be greater than 0. Order price is 0.","key":"ORDER_PRICE_MUST_GREETER_ZERO","detail":{"price":"0"}} + # {"code":400,"msg":"strconv.ParseInt: parsing \"dsfdfsd\": invalid syntax","timeCost":5320995} + # + if response is None: + return None + errorCode = self.safe_integer(response, 'code') + if errorCode is not None and errorCode != 0: + feedback = self.id + ' ' + body + message = self.safe_string_2(response, 'key', 'msg') + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + status = str(code) + self.throw_exactly_matched_exception(self.exceptions['exact'], status, feedback) + raise ExchangeError(feedback) + return None diff --git a/ccxt/arkham.py b/ccxt/arkham.py new file mode 100644 index 0000000..e2f6bb8 --- /dev/null +++ b/ccxt/arkham.py @@ -0,0 +1,2407 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.arkham import ImplicitAPI +import hashlib +from ccxt.base.types import Account, Any, Balances, Currencies, Currency, DepositAddress, Int, Leverage, LeverageTier, LeverageTiers, Market, Num, Order, OrderBook, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, Trade, TradingFees, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import OperationRejected +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidAddress +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import OperationFailed +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class arkham(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(arkham, self).describe(), { + 'id': 'arkham', + 'name': 'ARKHAM', + 'countries': ['US'], + 'version': 'v1', + 'rateLimit': 20 / 3, # 150 req/s + 'certified': False, + 'pro': True, + 'has': { + 'CORS': False, + 'spot': True, + 'margin': False, + 'swap': True, + 'future': False, + 'option': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'createDepositAddress': True, + 'createOrder': True, + 'fetchAccounts': True, + 'fetchAllGreeks': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': False, + 'fetchDepositAddressesByNetwork': True, + 'fetchDeposits': True, + 'fetchFundingHistory': True, + 'fetchGreeks': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchLeverage': True, + 'fetchLeverageTiers': True, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchPositions': True, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFees': True, + 'fetchVolatilityHistory': False, + 'fetchWithdrawals': True, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'sandbox': False, + 'setLeverage': True, + 'withdraw': True, + }, + 'timeframes': { + # enums are wrong in DOCS, these string values need to be in request + '1m': '1m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1h', + '6h': '6h', + '1d': '24h', + }, + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/5cefdcfb-2c10-445b-835c-fa21317bf5ac', + 'api': { + 'v1': 'https://arkm.com/api', + }, + 'www': 'https://arkm.com/', + 'referral': { + 'url': 'https://arkm.com/register?ref=ccxt', + 'discount': 0, + }, + 'doc': [ + 'https://arkm.com/limits-api', + 'https://info.arkm.com/api-platform', + ], + 'fees': 'https://arkm.com/fees', + }, + 'api': { + 'v1': { + 'public': { + 'get': { + 'alerts': 1, + 'announcements': 1, + 'assets': 1, + 'book': 1, + 'candles': 1, + 'chains': 1, + 'contracts': 1, + 'index-price': 1, + 'index-prices': 1, + 'margin-schedules': 1, + 'marketcapchart': 1, + 'marketcaps': 1, + 'pair': 1, + 'pairs': 1, + 'server-time': 1, + 'ticker': 1, + 'tickers': 1, + 'trades': 1, + }, + }, + 'private': { + # for orders: spot 20/s, todo: perp 40/s + 'get': { + 'user': 7.5, + 'orders': 7.5, + 'orders/by-client-order-id': 7.5, + 'orders/history': 7.5, + 'orders/history/by-client-order-id': 7.5, + 'orders/history_offset': 7.5, + 'orders/{id}': 7.5, + 'trades': 7.5, + 'trades/history': 7.5, + 'trades/time': 7.5, + 'trigger-orders': 7.5, + 'account/airdrops': 7.5, + 'account/balance-updates': 7.5, + 'account/balances': 7.5, + 'account/balances/ll': 7.5, + 'account/balances/history': 7.5, + 'account/balances/commissions': 7.5, + 'account/deposit/addresses': 7.5, + 'account/deposits': 7.5, + 'account/fees': 7.5, + 'account/funding-rate-payments': 7.5, + 'account/leverage': 7.5, + 'account/lsp-assignments': 7.5, + 'account/margin': 7.5, + 'account/margin/all': 7.5, + 'account/notifications': 7.5, + 'account/position-updates': 7.5, + 'account/positions': 7.5, + 'account/realized-pnl': 7.5, + 'account/rebates': 7.5, + 'account/referral-links': 7.5, + 'account/sessions': 7.5, + 'account/settings': 7.5, + 'account/settings/price-alert': 7.5, + 'account/transfers': 7.5, + 'account/unsubscribe': 7.5, + 'account/watchlist': 7.5, + 'account/withdrawal/addresses': 7.5, + 'account/withdrawal/addresses/{id}': 7.5, + 'account/withdrawals': 7.5, + 'subaccounts': 7.5, + 'airdrop': 7.5, + 'airdrop/claim': 7.5, + 'affiliate-dashboard/commission-earned': 7.5, + 'affiliate-dashboard/min-arkm-last-30d': 7.5, + 'affiliate-dashboard/points': 7.5, + 'affiliate-dashboard/points-season-1': 7.5, + 'affiliate-dashboard/points-season-2': 7.5, + 'affiliate-dashboard/realized-pnl': 7.5, + 'affiliate-dashboard/rebate-balance': 7.5, + 'affiliate-dashboard/referral-count': 7.5, + 'affiliate-dashboard/referrals-season-1': 7.5, + 'affiliate-dashboard/referrals-season-2': 7.5, + 'affiliate-dashboard/trading-volume-stats': 7.5, + 'affiliate-dashboard/volume-season-1': 7.5, + 'affiliate-dashboard/volume-season-2': 7.5, + 'affiliate-dashboard/api-key': 7.5, + 'competitions/opt-in-status': 7.5, + 'rewards/info': 7.5, + 'rewards/vouchers': 7.5, + }, + 'post': { + 'orders/new': 7.5, + 'trigger-orders/new': 7.5, + 'orders/cancel': 7.5, + 'trigger-orders/cancel': 7.5, + 'orders/cancel/all': 7.5, + 'trigger-orders/cancel/all': 7.5, + 'orders/new/simple': 7.5, + 'account/deposit/addresses/new': 7.5, + 'account/leverage': 7.5, + 'account/notifications/read': 7.5, + 'account/referral-links': 7.5, + 'account/sessions/delete': 7.5, + 'account/sessions/terminate-all': 7.5, + 'account/settings/update': 7.5, + 'account/watchlist/add': 7.5, + 'account/watchlist/remove': 7.5, + 'account/withdraw': 7.5, + 'account/withdrawal/addresses/confirm': 7.5, + 'subaccounts': 7.5, + 'subaccounts/transfer': 7.5, + 'subaccounts/perp-transfer': 7.5, + 'subaccounts/update-settings': 7.5, + 'airdrop': 7.5, + 'api-key/create': 7.5, + 'authenticate': 7.5, + 'competitions/opt-in': 7.5, + 'rewards/vouchers/claim': 7.5, + }, + 'put': { + 'account/referral-links/{id}/slug': 7.5, + 'account/settings/price-alert': 7.5, + 'account/withdrawal/addresses/{id}': 7.5, + 'subaccounts': 7.5, + 'api-key/update/{id}': 7.5, + }, + 'delete': { + 'account/settings/price-alert': 7.5, + 'account/withdrawal/addresses/{id}': 7.5, + 'subaccounts/{subaccountId}': 7.5, + 'api-key/{id}': 7.5, + }, + }, + }, + }, + 'options': { + 'networks': { + 'ETH': 'ETH', + 'ERC20': 'ETH', + 'BTC': 'BTC', + 'SOL': 'SOL', + 'TON': 'TON', + 'DOGE': 'DOGE', + 'SUI': 'SUI', + 'XRP': 'XRP', + 'OP': 'OP', + 'AVAXC': 'AVAX', + 'ARBONE': 'ARB', + }, + 'networksById': { + 'ETH': 'ERC20', + 'ERC20': 'ERC20', + }, + 'requestExpiration': 5000, # 5 seconds + 'timeframeDurations': { + '1m': 60000000, + '5m': 300000000, + '15m': 900000000, + '30m': 1800000000, + '1h': 3600000000, + '6h': 21600000000, + '1d': 86400000000, + }, + }, + 'features': { + 'default': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerPriceType': { + 'mark': True, + 'index': True, + 'last': True, + }, + 'triggerDirection': True, + 'stopLossPrice': True, + 'takeProfitPrice': True, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'selfTradePrevention': False, + 'trailing': False, + 'iceberg': False, + 'leverage': False, + 'marketBuyByCost': False, + 'marketBuyRequiresPrice': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, + 'daysBack': None, + 'untilDays': 1, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': True, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': None, + 'daysBackCanceled': None, + 'untilDays': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 365, + }, + }, + 'spot': { + 'extends': 'default', + }, + 'swap': { + 'linear': { + 'extends': 'default', + }, + 'inverse': None, + }, + 'future': { + 'linear': { + 'extends': 'default', + }, + 'inverse': None, + }, + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'exact': { + # 1XXXX General Errors + # These errors can occur for a variety of reasons and may be returned by the API or Websocket on any endpoint. + '10000': OperationFailed, + '10001': BadRequest, + '10002': AuthenticationError, + '10003': BadSymbol, + '10004': ArgumentsRequired, + '10005': RateLimitExceeded, + '10006': PermissionDenied, + '10007': PermissionDenied, + '10008': RateLimitExceeded, + '10009': PermissionDenied, + '10010': PermissionDenied, + '10011': AuthenticationError, + '10012': PermissionDenied, + '10013': PermissionDenied, + '10014': AuthenticationError, + '10015': PermissionDenied, + '10016': PermissionDenied, + '10017': PermissionDenied, + '10018': AuthenticationError, + '10019': AuthenticationError, + '10020': PermissionDenied, + '10021': PermissionDenied, + '10022': ExchangeError, + '10023': BadRequest, + '10024': ExchangeError, + '10025': BadRequest, + # #2XXXX General Websocket Errors + '20001': BadRequest, + '20002': ArgumentsRequired, + '20003': BadRequest, + '20004': ArgumentsRequired, + '20005': BadRequest, + # #3XXXX Trading Errors + '30001': InvalidOrder, + '30002': InvalidOrder, + '30003': InvalidOrder, + '30004': InvalidOrder, + '30005': InvalidOrder, + '30006': InvalidOrder, + '30007': BadSymbol, + '30008': OperationRejected, + '30009': OperationRejected, + '30010': InsufficientFunds, + '30011': BadSymbol, + '30012': OperationRejected, + '30013': OperationRejected, + '30014': InvalidOrder, + '30015': OrderNotFound, + '30016': InvalidOrder, + '30017': InvalidOrder, + '30018': InvalidOrder, + '30019': OperationRejected, + '30020': InvalidOrder, + '30021': InvalidOrder, + '30022': InvalidOrder, + '30023': InvalidOrder, + '30024': InvalidOrder, + '30025': BadRequest, + '30026': PermissionDenied, + '30027': PermissionDenied, + '30028': OrderNotFound, + # #4XXXX Funding Errors + '40001': OperationRejected, + '40002': BadRequest, + '40003': InvalidAddress, + '40004': OperationRejected, + '40005': BadRequest, + '40006': PermissionDenied, + '40007': OperationRejected, + '40008': OperationRejected, + '40009': OperationRejected, + '40010': BadRequest, + '40011': OperationRejected, + '40012': BadRequest, + '40013': BadRequest, + # #9XXXX Other Errors + '90001': BadRequest, + '90002': BadRequest, + '90003': OperationRejected, + '90004': BadRequest, + '90005': BadRequest, + '90006': RateLimitExceeded, + '90007': AuthenticationError, + '90008': RateLimitExceeded, + '90009': PermissionDenied, + '90010': BadRequest, + '90011': RateLimitExceeded, + }, + 'broad': { + 'less than min withdrawal ': OperationRejected, # {"message":"amount 1 less than min withdrawal 5"} + }, + }, + }) + + def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://arkm.com/docs#get/public/assets + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = self.v1PublicGetAssets(params) + # + # [ + # { + # "symbol": "USDT", + # "name": "Tether", + # "imageUrl": "https://static.arkhamintelligence.com/tokens/tether.png", + # "stablecoin": True, + # "featuredPair": "BTC_USDT", + # "chains": [ + # { + # "symbol": "ETH", + # "assetSymbol": "ETH", + # "name": "Ethereum", + # "type": "1", + # "confirmations": "6", + # "blockTime": "12000000" + # } + # ], + # "status": "listed", + # "minDeposit": "5", + # "minWithdrawal": "5", + # "withdrawalFee": "2" + # }, + # ... + # + result: dict = {} + for i in range(0, len(response)): + currency = response[i] + id = self.safe_string(currency, 'symbol') + code = self.safe_currency_code(id) + networks: dict = {} + chains = self.safe_list(currency, 'chains', []) + for j in range(0, len(chains)): + chain = chains[j] + networkId = self.safe_string(chain, 'symbol') + network = self.network_id_to_code(networkId) + networks[network] = { + 'info': chain, + 'id': networkId, + 'network': network, + 'title': self.safe_string(chain, 'name'), + 'active': None, + 'deposit': None, + 'withdraw': None, + 'fee': None, + 'precision': None, + 'limits': { + 'withdraw': { + 'min': None, + 'max': None, + }, + }, + } + result[code] = self.safe_currency_structure({ + 'info': currency, + 'id': id, + 'code': code, + 'name': self.safe_string(currency, 'name'), + 'active': self.safe_string(currency, 'status') == 'listed', + 'deposit': None, + 'withdraw': None, + 'fee': self.safe_number(currency, 'withdrawalFee'), + 'precision': None, + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': self.safe_number(currency, 'minWithdrawal'), + 'max': None, + }, + 'deposit': { + 'min': self.safe_number(currency, 'minDeposit'), + 'max': None, + }, + }, + 'type': 'crypto', + 'networks': networks, + }) + return result + + def fetch_markets(self, params={}) -> List[Market]: + """ + + https://arkm.com/docs#get/public/pairs + + retrieves data on all markets for arkm + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = self.v1PublicGetPairs(params) + # + # [ + # { + # "symbol": "BTC_USDT", + # "baseSymbol": "BTC", + # "baseImageUrl": "https://static.arkhamintelligence.com/tokens/bitcoin.png", + # "baseIsStablecoin": False, + # "baseName": "Bitcoin", + # "quoteSymbol": "USDT", + # "quoteImageUrl": "https://static.arkhamintelligence.com/tokens/tether.png", + # "quoteIsStablecoin": True, + # "quoteName": "Tether", + # "minTickPrice": "0.01", + # "minLotSize": "0.00001", + # "minSize": "0.00001", + # "maxSize": "9000", + # "minPrice": "0.01", + # "maxPrice": "1000000", + # "minNotional": "5", + # "maxPriceScalarUp": "1.8", + # "maxPriceScalarDown": "0.2", + # "pairType": "spot", # atm, always 'spot' value + # "maxLeverage": "0", + # "status": "listed" + # }, + # { + # "symbol": "BTC_USDT_PERP", + # "baseSymbol": "BTC.P", + # "baseImageUrl": "https://static.arkhamintelligence.com/tokens/bitcoin.png", + # "baseIsStablecoin": False, + # "baseName": "Bitcoin Perpetual", + # "quoteSymbol": "USDT", + # "quoteImageUrl": "https://static.arkhamintelligence.com/tokens/tether.png", + # "quoteIsStablecoin": True, + # "quoteName": "Tether", + # "minTickPrice": "0.01", + # "minLotSize": "0.00001", + # "minSize": "0.00001", + # "maxSize": "9000", + # "minPrice": "0.01", + # "maxPrice": "1000000", + # "minNotional": "5", + # "maxPriceScalarUp": "1.5", + # "maxPriceScalarDown": "0.5", + # "pairType": "perpetual", + # "marginSchedule": "C", + # "maxLeverage": "25", + # "status": "listed" + # }, + # ... + # + result = [] + for i in range(0, len(response)): + market = response[i] + id = self.safe_string(market, 'symbol') + baseId = self.safe_string(market, 'baseSymbol') + quoteId = self.safe_string(market, 'quoteSymbol') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + marketType: Str = None + symbol: Str = None + pairType = self.safe_string(market, 'pairType') + isSpot = pairType == 'spot' + isPerpetual = pairType == 'perpetual' + settle = None + settleId = None + if isSpot: + marketType = 'spot' + symbol = base + '/' + quote + elif isPerpetual: + marketType = 'swap' + base = base.replace('.P', '') + settle = quote + settleId = quoteId + symbol = base + '/' + quote + ':' + settle + result.append({ + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': marketType, + 'spot': isSpot, + 'margin': None, + 'swap': isPerpetual, + 'future': False, + 'option': False, + 'active': self.safe_string(market, 'status') == 'listed', + 'contract': isPerpetual, + 'linear': True if isPerpetual else None, + 'inverse': False if isPerpetual else None, + 'contractSize': None if isSpot else 1, # seems 1 per fetchTrades + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'price': self.safe_number(market, 'minTickPrice'), + 'amount': self.safe_number(market, 'minLotSize'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(market, 'minSize'), + 'max': self.safe_number(market, 'maxSize'), + }, + 'price': { + 'min': self.safe_number(market, 'minPrice'), + 'max': self.safe_number(market, 'maxPrice'), + }, + 'cost': { + 'min': self.safe_number(market, 'minNotional'), + 'max': None, + }, + }, + 'created': None, + 'info': market, + }) + return result + + def fetch_time(self, params={}): + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://arkm.com/docs#get/public/server-time + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = self.v1PublicGetServerTime(params) + # + # { + # "serverTime": "1753465832770820" + # } + # + return self.safe_integer_product(response, 'serverTime', 0.001) + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://arkm.com/docs#get/public/book + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the number of order book entries to return, max 50 + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['limit'] = limit + response = self.v1PublicGetBook(self.extend(request, params)) + # + # { + # "symbol": "BTC_USDT", + # "group": "0.01", + # "asks": [ + # { + # "price": "122900.43", + # "size": "0.0243" + # }, + # { + # "price": "121885.53", + # "size": "0.00116" + # }, + # ... + # ], + # "bids": [ + # { + # "price": "20400", + # "size": "0.00316" + # }, + # { + # "price": "30000", + # "size": "0.00116" + # }, + # ... + # ], + # "lastTime": "1753419275604353" + # } + # + timestamp = self.safe_integer_product(response, 'lastTime', 0.001) + marketId = self.safe_string(response, 'symbol') + return self.parse_order_book(response, self.safe_symbol(marketId, market), timestamp, 'bids', 'asks', 'price', 'size') + + def fetch_ohlcv(self, symbol: str, timeframe='1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://arkm.com/docs#get/public/candles + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms for the ending date filter, default is the current time + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + maxLimit = 365 + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate', False) + if paginate: + return self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params, maxLimit) + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'duration': self.safe_string(self.timeframes, timeframe, timeframe), + } + durationMs = self.parse_timeframe(timeframe) * 1000 + until = self.safe_integer(params, 'until') + params = self.omit(params, ['until']) + selectedLimit = min(limit, maxLimit) if (limit is not None) else maxLimit + if since is not None: + request['start'] = since + request['end'] = self.sum(since, selectedLimit * durationMs) + else: + now = self.milliseconds() + request['end'] = until if (until is not None) else now + request['start'] = request['end'] - selectedLimit * durationMs + # exchange needs microseconds + request['start'] = request['start'] * 1000 + request['end'] = request['end'] * 1000 + response = self.v1PublicGetCandles(self.extend(request, params)) + # + # [ + # { + # "symbol": "BTC_USDT_PERP", + # "time": "1753464720000000", + # "duration": "60000000", + # "open": "116051.35", + # "high": "116060.27", + # "low": "116051.35", + # "close": "116060.27", + # "volume": "0.0257", + # "quoteVolume": "2982.6724054" + # }, + # ... + # ] + # + return self.parse_ohlcvs(response, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # { + # "symbol": "BTC_USDT_PERP", + # "time": "1753464720000000", + # "duration": "60000000", + # "open": "116051.35", + # "high": "116060.27", + # "low": "116051.35", + # "close": "116060.27", + # "volume": "0.0257", + # "quoteVolume": "2982.6724054" + # } + # + return [ + self.safe_integer_product(ohlcv, 'time', 0.001), + self.safe_number(ohlcv, 'open'), + self.safe_number(ohlcv, 'high'), + self.safe_number(ohlcv, 'low'), + self.safe_number(ohlcv, 'close'), + self.safe_number(ohlcv, 'volume'), + ] + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + response = self.v1PublicGetTickers(params) + # + # [ + # { + # "symbol": "BTC_USDT_PERP", + # "baseSymbol": "BTC.P", + # "quoteSymbol": "USDT", + # "indexCurrency": "USDT", + # "price": "118806.89", + # "price24hAgo": "118212.29", + # "high24h": "119468.05", + # "low24h": "117104.44", + # "volume24h": "180.99438", + # "quoteVolume24h": "21430157.5928827", + # "markPrice": "118814.71", + # "indexPrice": "118804.222610343", + # "fundingRate": "0.000007", + # "nextFundingRate": "0.000006", + # "nextFundingTime": "1753390800000000", + # "productType": "perpetual", + # "openInterest": "2.55847", + # "usdVolume24h": "21430157.5928827", + # "openInterestUSD": "303963.8638583" + # }, + # ... + # + return self.parse_tickers(response, symbols) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = self.v1PublicGetTicker(self.extend(request, params)) + # + # { + # "symbol": "BTC_USDT_PERP", + # "baseSymbol": "BTC.P", + # "quoteSymbol": "USDT", + # "indexCurrency": "USDT", + # "price": "118806.89", + # "price24hAgo": "118212.29", + # "high24h": "119468.05", + # "low24h": "117104.44", + # "volume24h": "180.99438", + # "quoteVolume24h": "21430157.5928827", + # "markPrice": "118814.71", + # "indexPrice": "118804.222610343", + # "fundingRate": "0.000007", + # "nextFundingRate": "0.000006", + # "nextFundingTime": "1753390800000000", + # "productType": "perpetual", + # "openInterest": "2.55847", + # "usdVolume24h": "21430157.5928827", + # "openInterestUSD": "303963.8638583" + # } + # + return self.parse_ticker(response, market) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + marketId = self.safe_string(ticker, 'symbol') + market = self.safe_market(marketId, market) + return self.safe_ticker({ + 'info': ticker, + 'symbol': self.safe_symbol(marketId, market), + 'high': self.safe_number(ticker, 'high24h'), + 'low': self.safe_number(ticker, 'low24h'), + 'bid': self.safe_number(ticker, 'bid'), + 'last': self.safe_number(ticker, 'price'), + 'open': self.safe_number(ticker, 'price24hAgo'), + 'change': self.safe_number(ticker, 'priceChange'), + 'percentage': self.safe_number(ticker, 'priceChangePercent'), + 'baseVolume': self.safe_number(ticker, 'volume24h'), + 'quoteVolume': self.safe_number(ticker, 'usdVolume24h'), + 'markPrice': self.safe_number(ticker, 'markPrice'), + 'indexPrice': self.safe_number(ticker, 'indexPrice'), + 'vwap': None, + 'average': None, + 'previousClose': None, + 'askVolume': None, + 'bidVolume': None, + }) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://arkm.com/docs#get/public/trades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.loc]: crypto location, default: us + :param str [params.method]: method, default: marketPublicGetV1beta3CryptoLocTrades + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + marketId = market['id'] + request: dict = { + 'symbol': marketId, + } + if limit is not None: + request['limit'] = limit + response = self.v1PublicGetTrades(self.extend(request, params)) + # + # [ + # { + # "symbol": "BTC_USDT_PERP", + # "revisionId": "1130514101", + # "size": "0.01668", + # "price": "116309.57", + # "takerSide": "sell", + # "time": "1753439710374047" + # }, + # ... + # ] + # + return self.parse_trades(response, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades + # + # { + # "symbol": "BTC_USDT_PERP", + # "revisionId": "1130514101", + # "size": "0.01668", + # "price": "116309.57", + # "takerSide": "sell", + # "time": "1753439710374047" + # } + # + # fetchMyTrades + # + # { + # "symbol": "SOL_USDT", + # "revisionId": "891839406", + # "size": "0.042", + # "price": "185.06", + # "takerSide": "sell", + # "time": "1753773952039342", + # "orderId": "3717304929194", + # "userSide": "sell", + # "quoteFee": "0.00777252", + # "arkmFee": "0", + # "clientOrderId": "" + # } + # + marketId = self.safe_string(trade, 'symbol') + market = self.safe_market(marketId, market) + timestamp = self.safe_integer_product(trade, 'time', 0.001) + quoteFee = self.safe_number(trade, 'quoteFee') + arkmFee = self.safe_number(trade, 'arkmFee') + fee = None + if quoteFee is not None: + fee = { + 'cost': quoteFee, + 'currency': market['quote'], + } + elif arkmFee is not None: + fee = { + 'cost': arkmFee, + 'currency': 'ARKM', + } + return self.safe_trade({ + 'info': trade, + 'id': self.safe_string(trade, 'revisionId'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'type': None, + 'side': self.safe_string_2(trade, 'userSide', 'takerSide'), # priority to userSide + 'takerOrMaker': None, + 'price': self.safe_string(trade, 'price'), + 'amount': self.safe_string(trade, 'size'), + 'cost': None, + 'fee': fee, + 'order': self.safe_string(trade, 'orderId'), + }, market) + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://arkm.com/docs#get/orders/by-client-order-id + + :param str id: the order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.clientOrderId]: a unique id for the order + :returns dict: An `order structure ` + """ + request: dict = { + 'id': int(id), + } + response = self.v1PrivateGetOrdersId(self.extend(request, params)) + # + # { + # "orderId": "3690478767430", + # "userId": "2959123", + # "subaccountId": "0", + # "symbol": "SOL_USDT", + # "time": "1753696843913970", + # "side": "sell", + # "type": "limitGtc", + # "size": "0.066", + # "price": "293.2", + # "postOnly": False, + # "reduceOnly": False, + # "executedSize": "0", + # "status": "booked", + # "avgPrice": "0", + # "executedNotional": "0", + # "creditFeePaid": "0", + # "marginBonusFeePaid": "0", + # "quoteFeePaid": "0", + # "arkmFeePaid": "0", + # "revisionId": "887956326", + # "lastTime": "1753696843914830", + # "clientOrderId": "", + # "lastSize": "0", + # "lastPrice": "0", + # "lastCreditFee": "0", + # "lastMarginBonusFee": "0", + # "lastQuoteFee": "0", + # "lastArkmFee": "0" + # } + # + return self.parse_order(response) + + def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://arkm.com/docs#get/orders/history + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch orders for + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if limit is not None: + request['limit'] = limit # note, API does not work for self param + response = self.v1PrivateGetOrdersHistory(self.extend(request, params)) + # + # [ + # { + # "orderId": "3690478767430", + # "userId": "2959123", + # "subaccountId": "0", + # "symbol": "SOL_USDT", + # "time": "1753696843913970", + # "side": "sell", + # "type": "limitGtc", + # "size": "0.066", + # "price": "293.2", + # "postOnly": False, + # "reduceOnly": False, + # "executedSize": "0", + # "status": "closed", + # "avgPrice": "0", + # "executedNotional": "0", + # "creditFeePaid": "0", + # "marginBonusFeePaid": "0", + # "quoteFeePaid": "0", + # "arkmFeePaid": "0", + # "revisionId": "888084076", + # "lastTime": "1753701350088305", + # "clientOrderId": "", + # "lastSize": "0", + # "lastPrice": "0", + # "lastCreditFee": "0", + # "lastMarginBonusFee": "0", + # "lastQuoteFee": "0", + # "lastArkmFee": "0" + # } + # ] + # + return self.parse_orders(response, market, since, limit) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://arkm.com/docs#get/orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch orders for + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + isTriggerOrder = self.safe_bool(params, 'trigger') + params = self.omit(params, 'trigger') + response = None + if isTriggerOrder: + response = self.v1PrivateGetTriggerOrders(self.extend({}, params)) + # + # [ + # { + # "subaccountId": "0", + # "symbol": "SOL_USDT", + # "side": "sell", + # "type": "market", + # "size": "0.045", + # "price": "99.9", + # "postOnly": False, + # "reduceOnly": False, + # "time": "1753768103780063", + # "triggerOrderId": "3715847222127", + # "triggerType": "stopLoss", + # "triggerPriceType": "last", + # "triggerPrice": "111", + # "clientOrderId": "", + # "status": "staged" + # }, + # ] + # + else: + response = self.v1PrivateGetOrders(self.extend({}, params)) + # + # [ + # { + # "orderId": "3690478767430", + # "userId": "2959123", + # "subaccountId": "0", + # "symbol": "SOL_USDT", + # "time": "1753696843913970", + # "side": "sell", + # "type": "limitGtc", + # "size": "0.066", + # "price": "293.2", + # "postOnly": False, + # "reduceOnly": False, + # "executedSize": "0", + # "status": "booked", + # "avgPrice": "0", + # "executedNotional": "0", + # "creditFeePaid": "0", + # "marginBonusFeePaid": "0", + # "quoteFeePaid": "0", + # "arkmFeePaid": "0", + # "revisionId": "887956326", + # "lastTime": "1753696843914830", + # "clientOrderId": "", + # "lastSize": "0", + # "lastPrice": "0", + # "lastCreditFee": "0", + # "lastMarginBonusFee": "0", + # "lastQuoteFee": "0", + # "lastArkmFee": "0" + # } + # ] + # + return self.parse_orders(response, market, since, limit) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://arkm.com/docs#post/orders/cancel + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + isTriggerOrder = self.safe_bool(params, 'trigger') + params = self.omit(params, 'trigger') + response = None + request: dict = {} + clientOrderId = self.safe_integer(params, 'clientOrderId') + if clientOrderId is not None: + params = self.omit(params, 'clientOrderId') + request['clientOrderId'] = clientOrderId + else: + if isTriggerOrder: + request['triggerOrderId'] = int(id) + else: + request['orderId'] = int(id) + if isTriggerOrder: + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument for trigger orders') + market = self.market(symbol) + request['symbol'] = market['id'] + response = self.v1PrivatePostTriggerOrdersCancel(self.extend(request, params)) + else: + response = self.v1PrivatePostOrdersCancel(self.extend(request, params)) + # + # {"orderId":3691703758327} + # + return self.parse_order(response) + + def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders in a market + + https://arkm.com/docs#post/orders/cancel/all + + :param str symbol: cancel alls open orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + if symbol is not None: + raise BadRequest(self.id + ' cancelAllOrders() does not support a symbol argument, use cancelOrder() or fetchOpenOrders() instead') + isTriggerOrder = self.safe_bool(params, 'trigger') + params = self.omit(params, 'trigger') + response = None + if isTriggerOrder: + response = self.v1PrivatePostTriggerOrdersCancelAll(params) + else: + response = self.v1PrivatePostOrdersCancelAll(params) + # + # [] returns an empty array, even when successfully cancels orders + # + return self.parse_orders(response, None) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order on the exchange + + https://arkm.com/docs#post/orders/new + + :param str symbol: unified CCXT market symbol + :param str type: "limit" or "market" + :param str side: "buy" or "sell" + :param float amount: the amount of currency to trade + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.timeInForce]: "GTC", "IOC", "FOK", or "PO" + :param float [params.triggerPrice]: price for a trigger(conditional) order + :param float [params.stopLossPrice]: price for a stoploss order + :param float [params.takeProfitPrice]: price for a takeprofit order + :param str [params.triggerDirection]: the direction for trigger orders, 'ascending' or 'descending' + :param str [params.triggerPriceType]: mark, index or last + :param bool [params.postOnly]: True or False whether the order is post-only + :param bool [params.reduceOnly]: True or False whether the order is reduce-only + :returns: `An order structure ` + """ + self.load_markets() + market = self.market(symbol) + isTriggerOrder = self.safe_number_n(params, ['triggerPrice', 'stopLossPrice', 'takeProfitPrice']) is not None + request = self.create_order_request(symbol, type, side, amount, price, params) + response = None + if isTriggerOrder: + response = self.v1PrivatePostTriggerOrdersNew(request) + # + # { + # "triggerOrderId": "3716436645573", + # "symbol": "SOL_USDT_PERP", + # "side": "buy", + # "type": "limitGtc", + # "size": "0.05", + # "price": "150" + # } + # + else: + response = self.v1PrivatePostOrdersNew(request) + # + # { + # "orderId": "3694872060678", + # "clientOrderId": "test123", + # "symbol": "SOL_USDT", + # "subaccountId": "0", + # "side": "buy", + # "type": "limitGtc", + # "size": "0.05", + # "price": "170", + # "time": "1753710501474043" + # } + # + return self.parse_order(response, market) + + def create_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + market = self.market(symbol) + symbol = market['symbol'] + request: dict = { + 'symbol': market['id'], + 'side': side, + 'size': self.amount_to_precision(symbol, amount), + } + isBuy = (side == 'buy') + stopLossPrice = self.safe_number(params, 'stopLossPrice') + takeProfitPrice = self.safe_number(params, 'takeProfitPrice') + triggerPriceAny = self.safe_string_n(params, ['triggerPrice', 'stopLossPrice', 'takeProfitPrice']) + if triggerPriceAny is not None: + request['triggerPrice'] = self.price_to_precision(symbol, triggerPriceAny) + if stopLossPrice is not None: + request['triggerType'] = 'stopLoss' if isBuy else 'takeProfit' + elif takeProfitPrice is not None: + request['triggerType'] = 'takeProfit' if isBuy else 'stopLoss' + else: + triggerDirection = self.safe_string(params, 'triggerDirection') + if triggerDirection is None: + raise ArgumentsRequired(self.id + ' createOrder() requires a triggerDirection parameter when triggerPrice is specified, must be "ascending" or "descending"') + if triggerDirection is not None: + if triggerDirection == 'ascending': + request['triggerType'] = 'stopLoss' if isBuy else 'takeProfit' + elif triggerDirection == 'descending': + request['triggerType'] = 'takeProfit' if isBuy else 'stopLoss' + # mandatory triggerPriceType + if self.safe_string(params, 'triggerPriceType') is None: + request['triggerPriceType'] = 'last' # default + isMarketOrder = (type == 'market') + isLimitOrder = (type == 'limit') + isLimitExchangeSpecific = self.in_array(type, ['limitGtc', 'limitIoc', 'limitFok']) + postOnly = self.is_post_only(isMarketOrder, False, params) + timeInForce = self.safe_string(params, 'timeInForce') + params = self.omit(params, ['postOnly', 'timeInForce', 'triggerPrice', 'stopLossPrice', 'takeProfitPrice', 'triggerDirection']) + if postOnly: + request['postOnly'] = True + if isLimitOrder or isLimitExchangeSpecific: + request['price'] = self.price_to_precision(symbol, price) + # + if timeInForce == 'IOC': + request['type'] = 'limitIoc' + elif timeInForce == 'FOK': + request['type'] = 'limitFok' + else: + request['type'] = 'limitGtc' + elif isMarketOrder: + request['type'] = 'market' + # we don't need to manually handle `reduceOnly`, `clientOrderId`, `triggerPriceType` here-specific keyname & values matches + return self.extend(request, params) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder + # + # { + # "orderId": "3694872060678", + # "clientOrderId": "test123", + # "symbol": "SOL_USDT", + # "subaccountId": "0", + # "side": "buy", + # "type": "limitGtc", + # "size": "0.05", + # "price": "170", + # "time": "1753710501474043" + # } + # + # fetchOrder, fetchOpenOrders, fetchClosedOrders + # + # { + # "orderId": "3690478767430", + # "userId": "2959123", + # "subaccountId": "0", + # "symbol": "SOL_USDT", + # "time": "1753696843913970", + # "side": "sell", + # "type": "limitGtc", + # "size": "0.066", + # "price": "293.2", + # "postOnly": False, + # "reduceOnly": False, + # "executedSize": "0", + # "status": "booked", + # "avgPrice": "0", + # "executedNotional": "0", + # "creditFeePaid": "0", + # "marginBonusFeePaid": "0", + # "quoteFeePaid": "0", + # "arkmFeePaid": "0", + # "revisionId": "887956326", + # "lastTime": "1753696843914830", + # "clientOrderId": "", + # "lastSize": "0", + # "lastPrice": "0", + # "lastCreditFee": "0", + # "lastMarginBonusFee": "0", + # "lastQuoteFee": "0", + # "lastArkmFee": "0" + # } + # + # trigger-orders: createOrder + # + # { + # "triggerOrderId": "3716436645573", + # "symbol": "SOL_USDT_PERP", + # "side": "buy", + # "type": "limitGtc", + # "size": "0.05", + # "price": "150" + # } + # + # trigger-orders: fetchOpenOrders + # + # { + # "subaccountId": "0", + # "symbol": "SOL_USDT", + # "side": "sell", + # "type": "market", + # "size": "0.045", + # "price": "99.9", + # "postOnly": False, + # "reduceOnly": False, + # "time": "1753768103780063", + # "triggerOrderId": "3715847222127", + # "triggerType": "stopLoss", + # "triggerPriceType": "last", + # "triggerPrice": "111", + # "clientOrderId": "", + # "status": "staged" + # } + # + marketId = self.safe_string(order, 'symbol') + market = self.safe_market(marketId, market) + isPostOnly = self.safe_bool(order, 'postOnly') + typeRaw = self.safe_string(order, 'type') + orderType = 'limit' if isPostOnly else self.parse_order_type(typeRaw) + timeInForce = 'PO' if isPostOnly else self.parse_time_in_force(typeRaw) + quoteFeePaid = self.safe_string(order, 'quoteFeePaid') + arkmFeePaid = self.safe_string(order, 'arkmFeePaid') + fees = [] + if quoteFeePaid is not None: + fees.append({ + 'cost': quoteFeePaid, + 'currency': self.safe_string(market, 'quote'), + }) + if arkmFeePaid is not None: + fees.append({ + 'cost': arkmFeePaid, + 'currency': 'ARKM', + }) + timestamp = self.safe_integer_product(order, 'time', 0.001) + return self.safe_order({ + 'id': self.safe_string_2(order, 'orderId', 'triggerOrderId'), + 'clientOrderId': self.safe_string(order, 'clientOrderId'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimeStamp': None, + 'lastUpdateTimestamp': self.safe_integer_product(order, 'lastTime', 0.001), + 'status': self.parse_order_status(self.safe_string(order, 'status')), + 'symbol': market['symbol'], + 'type': orderType, + 'timeInForce': timeInForce, + 'postOnly': None, + 'side': self.safe_string(order, 'side'), + 'price': self.safe_number(order, 'price'), + 'triggerPrice': None, + 'cost': self.safe_number(order, 'executedNotional'), + 'average': self.safe_number_omit_zero(order, 'avgPrice'), + 'amount': self.safe_number(order, 'size'), + 'filled': self.safe_number(order, ''), + 'remaining': None, + 'trades': None, + 'fees': fees, + 'reduceOnly': self.safe_bool(order, 'reduceOnly'), + 'info': order, + }, market) + + def parse_order_type(self, type: Str) -> Str: + types: dict = { + 'limitGtc': 'limit', + 'limitIoc': 'limit', + 'limitFok': 'limit', + 'market': 'market', + } + return self.safe_string_upper(types, type, type) + + def parse_time_in_force(self, type: Str) -> Str: + types: dict = { + 'limitGtc': 'GTC', + 'limitIoc': 'IOC', + 'limitFok': 'FOK', + 'market': 'IOC', + } + return self.safe_string_upper(types, type, type) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'new': 'pending', + 'staged': 'open', + 'booked': 'open', + 'taker': 'closed', + 'maker': 'closed', + 'cancelled': 'canceled', + 'closed': 'closed', + } + return self.safe_string(statuses, status, status) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://arkm.com/docs#get/trades/time + + :param str [symbol]: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trade structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch trades for + :param str [params.page_token]: page_token - used for paging + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + request: dict = {} + if limit is not None: + request['limit'] = limit + # exchange needs to obtain some `from & to` values, otherwise it does not return any result + defaultRange = 24 * 60 * 60 * 1000 # default to last 24 hours + if since is not None: + request['from'] = since * 1000 # convert ms to microseconds + else: + request['from'] = (self.milliseconds() - defaultRange) * 1000 # default to last 24 hours + until = self.safe_integer(params, 'until') + if until is not None: + params = self.omit(params, 'until') + request['to'] = until * 1000 # convert ms to microseconds + else: + request['to'] = self.sum(request['from'], defaultRange * 1000) + request, params = self.handle_until_option('until', request, params) + response = self.v1PrivateGetTradesTime(self.extend(request, params)) + # + # [ + # { + # "symbol": "SOL_USDT", + # "revisionId": "891839406", + # "size": "0.042", + # "price": "185.06", + # "takerSide": "sell", + # "time": "1753773952039342", + # "orderId": "3717304929194", + # "userSide": "sell", + # "quoteFee": "0.00777252", + # "arkmFee": "0", + # "clientOrderId": "" + # }, + # ... + # + return self.parse_trades(response, None, since, limit) + + def fetch_accounts(self, params={}) -> List[Account]: + """ + fetch all the accounts associated with a profile + + https://arkm.com/docs#get/user + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `account structures ` indexed by the account type + """ + self.load_markets() + request: dict = {} + accountId = None + accountId, params = self.handle_option_and_params(params, 'fetchAccounts', 'accountId') + if accountId is not None: + request['subAccountId'] = accountId + response = self.v1PrivateGetUser(self.extend(request, params)) + # + # { + # "id": "2959123", + # "email": "xyz@gmail.com", + # "username": "t.123", + # "requireMFA": True, + # "kycVerifiedAt": "1753434515850673", + # "pmm": False, + # "dmm": False, + # "becameVipAt": "0", + # "subaccounts": [ + # { + # "id": "0", + # "name": "Primary", + # "pinned": True, + # "isLsp": False, + # "futuresEnabled": True, + # "payFeesInArkm": False, + # "lspSettings": [] + # } + # ], + # "settings": { + # "autogenDepositAddresses": False, + # "hideBalances": False, + # "confirmBeforePlaceOrder": False, + # "tickerTapeScroll": True, + # "updatesFlash": True, + # "notifyOrderFills": False, + # "notifyAnnouncements": False, + # "notifyMarginUsage": False, + # "marginUsageThreshold": "0.5", + # "notifyWithdrawals": True, + # "notifyDeposits": True, + # "notifySendEmail": True, + # "notifyRebates": True, + # "notifyCommissions": True, + # "allowSequenceEmails": True, + # "language": "en" + # }, + # "airdropKycAt": null + # } + # + subAccounts = self.safe_list(response, 'subaccounts', []) + return self.parse_accounts(subAccounts, params) + + def parse_account(self, account): + # + # { + # "id": "0", + # "name": "Primary", + # "pinned": True, + # "isLsp": False, + # "futuresEnabled": True, + # "payFeesInArkm": False, + # "lspSettings": [] + # } + # + return { + 'id': self.safe_string(account, 'id'), + 'type': None, + 'code': None, + 'info': account, + } + + def fetch_balance(self, params={}) -> Balances: + """ + query for account info + + https://arkm.com/docs#get/account/balances + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + response = self.v1PrivateGetAccountBalances(params) + # + # [ + # { + # "subaccountId": "0", + # "symbol": "USDT", + # "balance": "19.66494694", + # "free": "19.66494694", + # "priceUSDT": "1", + # "balanceUSDT": "19.66494694", + # "freeUSDT": "19.66494694", + # "lastUpdateReason": "orderFill", + # "lastUpdateTime": "1753773952039342", + # "lastUpdateId": "248507437", + # "lastUpdateAmount": "7.77252" + # }, + # { + # "subaccountId": "0", + # "symbol": "SOL", + # "balance": "0", + # "free": "0", + # "priceUSDT": "186.025584673", + # "balanceUSDT": "0", + # "freeUSDT": "0", + # "lastUpdateReason": "orderFill", + # "lastUpdateTime": "1753773952039342", + # "lastUpdateId": "248507435", + # "lastUpdateAmount": "-0.042" + # } + # ] + # + return self.parse_balance(response) + + def parse_balance(self, response) -> Balances: + timestamp = self.safe_integer_product(response, 'lastUpdateTime', 0.001) + result: dict = { + 'info': response, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + for i in range(0, len(response)): + balance = response[i] + symbol = self.safe_string(balance, 'symbol') + code = self.safe_currency_code(symbol) + account = self.account() + account['total'] = self.safe_string(balance, 'balance') + account['free'] = self.safe_string(balance, 'free') + result[code] = account + return self.safe_balance(result) + + def create_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + create a currency deposit address + + https://arkm.com/docs#post/account/deposit/addresses/new + + :param str code: unified currency code of the currency for the deposit address + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + self.load_markets() + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + if networkCode is None: + raise ArgumentsRequired(self.id + ' createDepositAddress() requires a "network" param') + request: dict = { + 'chain': networkCode, + } + response = self.v1PrivatePostAccountDepositAddressesNew(self.extend(request, params)) + # + # { + # "addresses": "12NauJ26TUT9aYkpId7YdePJJDRMGbAsEMVoTVUvBErV" + # } + # + address = self.safe_string(response, 'addresses') + return self.parse_deposit_address(address, self.currency(code)) + + def fetch_deposit_addresses_by_network(self, code: str, params={}) -> List[DepositAddress]: + """ + fetch the deposit addresses for a currency associated with self account + + https://arkm.com/docs#get/account/deposit/addresses + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary `address structures `, indexed by the network + """ + self.load_markets() + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + if networkCode is None: + raise ArgumentsRequired(self.id + ' fetchDepositAddressesByNetwork() requires a "network" param') + request: dict = { + 'chain': self.network_code_to_id(networkCode), + } + response = self.v1PrivateGetAccountDepositAddresses(self.extend(request, params)) + # + # { + # "addresses": [ + # "12NauJ26TUT9aYkpId7YdePJJDRMGbAsEMVoTVUvBErV" + # ] + # } + # + data = self.safe_list(response, 'addresses') + parsed = self.parse_deposit_addresses(data, None, False, {'network': networkCode}) + return self.index_by(parsed, 'network') + + def parse_deposit_address(self, entry, currency: Currency = None) -> DepositAddress: + # + # "12NauJ26TUT9aYkpId7YdePJJDRMGbAsEMVoTVUvBErV" + # + return { + 'info': entry, + 'currency': self.safe_string(currency, 'code'), + 'network': None, + 'address': entry, + 'tag': None, + } + + def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://arkm.com/docs#get/account/deposit/addresses + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + self.load_markets() + currency = self.currency(code) + networkCodeAndParams = self.handle_network_code_and_params(params) + networkCode = networkCodeAndParams[0] + indexedAddresses = self.fetch_deposit_addresses_by_network(code, params) + selectedNetworkCode = self.select_network_code_from_unified_networks(currency['code'], networkCode, indexedAddresses) + address = self.safe_dict(indexedAddresses, selectedNetworkCode) + if address is None: + raise InvalidAddress(self.id + ' fetchDepositAddress() could not find a deposit address for ' + code) + return address + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://arkm.com/docs#get/account/deposits + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + request: dict = {} + if limit is not None: + request['limit'] = limit + response = self.v1PrivateGetAccountDeposits(self.extend(request, params)) + # + # [ + # { + # "id": "238644", + # "symbol": "SOL", + # "amount": "0.104", + # "time": "1753436404000000", + # "confirmed": True, + # "transactionHash": "1DRxbbyePTsMuB82SDf2fG5gLXH5iYnY8TQDstDPLULpLtjMJtF1ug1T4Mf8B6DSb8fp2sb5YtdbyqieZ2tkE1Ve", + # "chain": "Solana", + # "depositAddress": "12NauJ26TUT9aYkpId7YdePJJDRMGbAsEMVoTVUvBErV", + # "price": "180.322010164" + # } + # ] + # + currency = None + if code is not None: + currency = self.currency(code) + return self.parse_transactions(response, currency, since, limit) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # { + # "id": "238644", + # "symbol": "SOL", + # "amount": "0.104", + # "time": "1753436404000000", + # "confirmed": True, + # "transactionHash": "1DRxbbyePTsMuB82SDf2fG5gLXH5iYnY8TQDstDPLULpLtjMJtF1ug1T4Mf8B6DSb8fp2sb5YtdbyqieZ2tkE1Ve", + # "chain": "Solana", + # "depositAddress": "12NauJ26TUT9aYkpId7YdePJJDRMGbAsEMVoTVUvBErV", + # "price": "180.322010164" + # } + # + address = self.safe_string(transaction, 'depositAddress') + timestamp = self.safe_integer_product(transaction, 'time', 0.001) + confirmd = self.safe_bool(transaction, 'confirmed') + status = None + if confirmd: + status = 'ok' + currencyId = self.safe_string(transaction, 'symbol') + code = self.safe_currency_code(currencyId, currency) + return { + 'info': transaction, + 'id': self.safe_string(transaction, 'id'), + 'txid': self.safe_string(transaction, 'transactionHash'), + 'type': None, + 'currency': code, + 'network': self.network_id_to_code(self.safe_string(transaction, 'chain')), + 'amount': self.safe_number(transaction, 'amount'), + 'status': status, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'address': address, + 'addressFrom': None, + 'addressTo': address, + 'tag': None, + 'tagFrom': None, + 'tagTo': None, + 'updated': None, + 'comment': None, + 'fee': None, + 'internal': False, + } + + def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://arkm.com/docs#get/account/fees + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.subType]: "linear" or "inverse" + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + self.load_markets() + response = self.v1PrivateGetAccountFees(params) + # + # { + # "perpMakerFee": "1.23", + # "perpTakerFee": "1.23", + # "spotMakerFee": "1.23", + # "spotTakerFee": "1.23" + # } + # + symbols = list(self.markets.keys()) + result: dict = {} + spotMaker = self.safe_number(response, 'spotMakerFee') + spotTaker = self.safe_number(response, 'spotTakerFee') + perpMaker = self.safe_number(response, 'perpMakerFee') + perpTaker = self.safe_number(response, 'perpTakerFee') + for i in range(0, len(symbols)): + symbol = symbols[i] + market = self.markets[symbol] + result[symbol] = { + 'info': response, + 'symbol': symbol, + } + if market['spot']: + result[symbol]['maker'] = spotMaker + result[symbol]['taker'] = spotTaker + elif market['swap'] or market['future']: + result[symbol]['maker'] = perpMaker + result[symbol]['taker'] = perpTaker + return result + + def fetch_funding_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch the history of funding payments paid and received on self account + + https://arkm.com/docs#get/account/funding-rate-payments + + :param str [symbol]: unified market symbol + :param int [since]: the earliest time in ms to fetch funding history for + :param int [limit]: the maximum number of funding history structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict: a `funding history structure ` + """ + self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + request: dict = {} + if limit is not None: + request['limit'] = limit + response = self.v1PrivateGetAccountFundingRatePayments(self.extend(request, params)) + # + # [ + # { + # "amount": "20.1", + # "assetSymbol": "BTC", + # "indexPrice": "1.23", + # "pairSymbol": "BTC_USDT", + # "time": 1704067200000000, + # "id": 1, + # "subaccountId": 1, + # "userId": 1 + # }, + # ... + # ] + # + return self.parse_incomes(response, market, since, limit) + + def parse_income(self, income, market: Market = None): + # + # { + # "amount": "20.1", + # "assetSymbol": "BTC", + # "indexPrice": "1.23", + # "pairSymbol": "BTC_USDT", + # "time": 1704067200000000, + # "id": 1, + # "subaccountId": 1, + # "userId": 1 + # } + # + marketId = self.safe_string(income, 'pairSymbol') + currencyId = self.safe_string(income, 'assetSymbol') + timestamp = self.safe_integer_product(income, 'time', 0.001) + return { + 'info': income, + 'symbol': self.safe_symbol(marketId, market), + 'code': self.safe_currency_code(currencyId), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': self.safe_string(income, 'id'), + 'amount': self.safe_number(income, 'amount'), + } + + def fetch_leverage(self, symbol: str, params={}) -> Leverage: + """ + fetch the set leverage for a market + + https://arkm.com/docs#get/account/leverage + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `leverage structure ` + """ + self.load_markets() + market = self.market(symbol) + marketId = self.safe_string(market, 'id') + request: dict = { + 'symbol': marketId, + } + response = self.v1PrivateGetAccountLeverage(self.extend(request, params)) + # + # might be empty if not changed from default value(which is 1x) + # + # [ + # { + # "symbol": "BTC_USDT_PERP", + # "leverage": "7" + # }, + # { + # "symbol": "ETH_USDT_PERP", + # "leverage": "5" + # } + # ] + # + indexed = self.index_by(response, 'symbol') + data = self.safe_dict(indexed, marketId, {}) + return self.parse_leverage(data, market) + + def parse_leverage(self, leverage: dict, market: Market = None) -> Leverage: + # + # { + # "symbol": "ETH_USDT_PERP", + # "leverage": "5" + # } + # + marketId = self.safe_string(leverage, 'symbol') + leverageNum = self.safe_number(leverage, 'leverage') # default leverage is 1 typically + return { + 'info': leverage, + 'symbol': self.safe_symbol(marketId, market), + 'marginMode': None, + 'longLeverage': leverageNum, + 'shortLeverage': leverageNum, + } + + def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://arkm.com/docs#post/account/leverage + + :param float leverage: the rate of leverage + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setLeverage() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + leverageString = self.number_to_string(leverage) + marketId = self.safe_string(market, 'id') + request: dict = { + 'symbol': marketId, + 'leverage': leverageString, + } + response = self.v1PrivatePostAccountLeverage(self.extend(request, params)) + # + # response is just empty string + # + return self.parse_leverage(response, market) + + def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://arkm.com/docs#get/account/positions + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.standard]: whether to fetch standard contract positions + :returns dict[]: a list of `position structures ` + """ + self.load_markets() + symbols = self.market_symbols(symbols) + response = self.v1PrivateGetAccountPositions(params) + # + # [ + # { + # "subaccountId": "0", + # "symbol": "SOL_USDT_PERP", + # "base": "0.037", + # "quote": "-6.44614", + # "openBuySize": "0", + # "openSellSize": "0", + # "openBuyNotional": "0", + # "openSellNotional": "0", + # "lastUpdateReason": "orderFill", + # "lastUpdateTime": "1753903829389966", + # "lastUpdateId": "250434684", + # "lastUpdateBaseDelta": "0.037", + # "lastUpdateQuoteDelta": "-6.44614", + # "breakEvenPrice": "174.22", + # "markPrice": "174.33", + # "value": "6.45021", + # "pnl": "0.00407", + # "initialMargin": "0.645021", + # "maintenanceMargin": "0.3870126", + # "averageEntryPrice": "174.22" + # } + # ] + # + return self.parse_positions(response, symbols) + + def parse_position(self, position: dict, market: Market = None): + # + # { + # "subaccountId": "0", + # "symbol": "SOL_USDT_PERP", + # "base": "0.037", # negative for short position + # "quote": "-6.44614", # negative for long position + # "openBuySize": "0", + # "openSellSize": "0", + # "openBuyNotional": "0", + # "openSellNotional": "0", + # "lastUpdateReason": "orderFill", + # "lastUpdateTime": "1753903829389966", + # "lastUpdateId": "250434684", + # "lastUpdateBaseDelta": "0.037", + # "lastUpdateQuoteDelta": "-6.44614", + # "breakEvenPrice": "174.22", + # "markPrice": "174.33", + # "value": "6.45021", + # "pnl": "0.00407", + # "initialMargin": "0.645021", + # "maintenanceMargin": "0.3870126", + # "averageEntryPrice": "174.22" + # } + # + base = self.safe_string(position, 'base') + baseAbs = Precise.string_abs(base) + isLong = Precise.string_ge(base, '0') + side = 'long' if isLong else 'short' + marketId = self.safe_string(position, 'symbol') + notional = self.safe_string(position, 'value') + return self.safe_position({ + 'info': position, + 'id': None, + 'symbol': self.safe_symbol(marketId, market), + 'notional': self.parse_number(Precise.string_abs(notional)), + 'marginMode': None, + 'liquidationPrice': None, + 'entryPrice': self.safe_number(position, 'averageEntryPrice'), + 'unrealizedPnl': self.safe_number(position, 'pnl'), + 'realizedPnl': None, + 'percentage': None, + 'contracts': self.parse_number(baseAbs), + 'contractSize': None, + 'markPrice': self.safe_number(position, 'markPrice'), + 'lastPrice': None, + 'side': side, + 'hedged': None, + 'timestamp': None, + 'datetime': None, + 'lastUpdateTimestamp': self.safe_integer(position, 'lastUpdateTime'), + 'maintenanceMargin': self.safe_number(position, 'maintenanceMargin'), + 'maintenanceMarginPercentage': None, + 'collateral': None, + 'initialMargin': self.safe_number(position, 'initialMargin'), + 'initialMarginPercentage': None, + 'leverage': None, + 'marginRatio': None, + 'stopLossPrice': None, + 'takeProfitPrice': None, + }) + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://arkm.com/docs#post/account/withdraw + https://arkm.com/docs#get/account/withdrawal/addresses + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.load_markets() + withdrawalAddresses = self.v1PrivateGetAccountWithdrawalAddresses() + # + # [ + # { + # "id": "12345", + # "chain": "ETH", + # "address": "0x743f79D65EA07AA222F4a83c10dee4210A920a6e", + # "label": "my_binance", + # "createdAt": "1753905200074355", + # "updatedAt": "1753905213464278", + # "confirmed": True + # } + # ] + # + currency = self.currency(code) + request: dict = { + 'symbol': currency['id'], + 'amount': self.currency_to_precision(code, amount), + 'subaccountId': self.safe_integer(params, 'subAccountId', 0), + } + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + if networkCode is None: + raise ArgumentsRequired(self.id + ' withdraw() requires a "network" param') + indexedList = self.group_by(withdrawalAddresses, 'address') + if not (address in indexedList): + raise InvalidAddress(self.id + ' withdraw() requires an address that has been previously added to the whitelisted addresses') + withdrawalObjects = indexedList[address] + foundWithdrawalObject = None + for i in range(0, len(withdrawalObjects)): + withdrawalObject = withdrawalObjects[i] + if withdrawalObject['chain'] == networkCode: + foundWithdrawalObject = withdrawalObject + break + if foundWithdrawalObject is None: + raise InvalidAddress(self.id + ' withdraw() can not find whitelisted withdrawal address for ' + address + ' with network ' + networkCode) + request['addressId'] = self.safe_integer(foundWithdrawalObject, 'id') + response = self.v1PrivatePostAccountWithdraw(self.extend(request, params)) + # + # response is a weird string like: + # + # "1234709779980\\n" + # + responseString = response.replace('\n', '') + data = {'id': responseString} + return self.parse_transaction(data, currency) + + def fetch_leverage_tiers(self, symbols: Strings = None, params={}) -> LeverageTiers: + """ + retrieve information on the maximum leverage, and maintenance margin for trades of varying trade sizes + + https://arkm.com/docs#get/public/margin-schedules + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `leverage tiers structures `, indexed by market symbols + """ + self.load_markets() + if symbols is None: + raise ArgumentsRequired(self.id + ' fetchLeverageTiers() requires a symbols argument') + symbols = self.market_symbols(symbols) + response = self.v1PublicGetMarginSchedules(params) + # + # [ + # { + # "name": "A", + # "bands": [ + # { + # "positionLimit": "1000000", + # "leverageRate": "50", + # "marginRate": "0.02", + # "rebate": "0" + # }, + # { + # "positionLimit": "2000000", + # "leverageRate": "25", + # "marginRate": "0.04", + # "rebate": "20000" + # }, + # { + # "positionLimit": "5000000", + # "leverageRate": "20", + # "marginRate": "0.05", + # "rebate": "40000" + # } + # ] + # }, + # { + # "name": "B", + # ... + # + return self.parse_leverage_tiers(response, symbols) + + def parse_leverage_tiers(self, response: Any, symbols: List[str] = None, marketIdKey=None) -> LeverageTiers: + # overloaded method + indexed = self.index_by(response, 'name') + symbols = self.market_symbols(symbols) + tiers = {} + for i in range(0, len(symbols)): + symbol = symbols[i] + market = self.market(symbol) + marginSchedule = self.safe_string(market['info'], 'marginSchedule') + if marginSchedule is None: + raise BadSymbol(self.id + ' fetchLeverageTiers() could not find marginSchedule for ' + symbol) + selectedDict = self.safe_dict(indexed, marginSchedule, {}) + bands = self.safe_list(selectedDict, 'bands', []) + tiers[symbol] = self.parse_market_leverage_tiers(bands, market) + return tiers + + def parse_market_leverage_tiers(self, info, market: Market = None) -> List[LeverageTier]: + tiers = [] + brackets = info + minNotional = 0 + for i in range(0, len(brackets)): + tier = brackets[i] + marketId = self.safe_string(info, 'market') + market = self.safe_market(marketId, market, None, 'swap') + maxNotional = self.safe_number(tier, 'positionLimit') + tiers.append({ + 'tier': self.sum(i, 1), + 'symbol': self.safe_symbol(marketId, market, None, 'swap'), + 'currency': market['base'] if market['linear'] else market['quote'], + 'minNotional': minNotional, + 'maxNotional': maxNotional, + 'maintenanceMarginRate': self.safe_number(tier, 'marginRate'), + 'maxLeverage': self.safe_integer(tier, 'leverageRate'), + 'info': tier, + }) + minNotional = maxNotional + return tiers + + def find_timeframe_by_duration(self, duration: Int) -> str: + # self method is used to find the timeframe by duration in seconds + timeframes = self.safe_dict(self.options, 'timeframeDurations', {}) + keys = list(timeframes.keys()) + for i in range(0, len(keys)): + timeframe = keys[i] + durationInMicroseconds = self.safe_integer(timeframes, timeframe) + if durationInMicroseconds == duration: + return timeframe + return None + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + type = self.safe_string(api, 0) + access = self.safe_string(api, 1) + accessPart = access + '/' if (access == 'public') else '' + query = self.omit(params, self.extract_params(path)) + path = self.implode_params(path, params) + url = self.urls['api'][type] + '/' + accessPart + path + queryString = '' + if method == 'GET': + if query: + queryString = self.urlencode(query) + url += '?' + queryString + if access == 'private': + self.check_required_credentials() + expires = (self.milliseconds() + self.safe_integer(self.options, 'requestExpiration', 5000)) * 1000 # need macroseconds + if method == 'POST': + body = self.json(params) + if queryString != '': + path = path + '?' + queryString + bodyStr = body if (body is not None) else '' + payload = self.apiKey + str(expires) + method.upper() + '/' + path + bodyStr + decodedSecret = self.base64_to_binary(self.secret) + signature = self.hmac(self.encode(payload), decodedSecret, hashlib.sha256, 'base64') + headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Arkham-Api-Key': self.apiKey, + 'Arkham-Expires': str(expires), + 'Arkham-Signature': signature, + 'Arkham-Broker-Id': '1001', + } + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + # + # error example: + # + # { + # "id": "30005", + # "name": "InvalidNotional", + # "message": "order validation failed: invalid notional: notional 0.25 is less than min notional 1" + # } + # + message = self.safe_string(response, 'message') + if message is not None: + errorCode = self.safe_string(response, 'id') + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + raise ExchangeError(self.id + ' ' + body) + return None diff --git a/ccxt/ascendex.py b/ccxt/ascendex.py new file mode 100644 index 0000000..07f0d48 --- /dev/null +++ b/ccxt/ascendex.py @@ -0,0 +1,3526 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.ascendex import ImplicitAPI +import hashlib +from ccxt.base.types import Account, Any, Balances, Currencies, Currency, DepositAddress, Int, Leverage, Leverages, LeverageTier, LeverageTiers, MarginMode, MarginModes, MarginModification, Market, Num, Order, OrderBook, OrderRequest, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, FundingRates, Trade, TradingFees, Transaction, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import AccountSuspended +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import NotSupported +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class ascendex(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(ascendex, self).describe(), { + 'id': 'ascendex', + 'name': 'AscendEX', + 'countries': ['SG'], # Singapore + # 8 requests per minute = 0.13333 per second => rateLimit = 750 + # testing 400 works + 'rateLimit': 400, + 'certified': False, + 'pro': True, + # new metainfo interface + 'has': { + 'CORS': None, + 'spot': True, + 'margin': True, + 'swap': True, + 'future': False, + 'option': False, + 'addMargin': True, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'createOrder': True, + 'createOrders': True, + 'createPostOnlyOrder': True, + 'createReduceOnlyOrder': True, + 'createStopLimitOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'fetchAccounts': True, + 'fetchAllGreeks': False, + 'fetchBalance': True, + 'fetchClosedOrders': True, + 'fetchCurrencies': True, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchDepositsWithdrawals': True, + 'fetchDepositWithdrawFee': 'emulated', + 'fetchDepositWithdrawFees': True, + 'fetchFundingHistory': True, + 'fetchFundingRate': 'emulated', + 'fetchFundingRateHistory': False, + 'fetchFundingRates': True, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchLeverage': 'emulated', + 'fetchLeverages': True, + 'fetchLeverageTiers': True, + 'fetchMarginMode': 'emulated', + 'fetchMarginModes': True, + 'fetchMarketLeverageTiers': 'emulated', + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMySettlementHistory': False, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': False, + 'fetchPosition': False, + 'fetchPositionMode': False, + 'fetchPositions': True, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': True, + 'fetchTransactionFee': False, + 'fetchTransactionFees': False, + 'fetchTransactions': 'emulated', + 'fetchTransfer': False, + 'fetchTransfers': False, + 'fetchVolatilityHistory': False, + 'fetchWithdrawal': False, + 'fetchWithdrawals': True, + 'reduceMargin': True, + 'sandbox': True, + 'setLeverage': True, + 'setMarginMode': True, + 'setPositionMode': False, + 'transfer': True, + }, + 'timeframes': { + '1m': '1', + '5m': '5', + '15m': '15', + '30m': '30', + '1h': '60', + '2h': '120', + '4h': '240', + '6h': '360', + '12h': '720', + '1d': '1d', + '1w': '1w', + '1M': '1m', + }, + 'version': 'v2', + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/55bab6b9-d4ca-42a8-a0e6-fac81ae557f1', + 'api': { + 'rest': 'https://ascendex.com', + }, + 'test': { + 'rest': 'https://api-test.ascendex-sandbox.com', + }, + 'www': 'https://ascendex.com', + 'doc': [ + 'https://ascendex.github.io/ascendex-pro-api/#ascendex-pro-api-documentation', + ], + 'fees': 'https://ascendex.com/en/feerate/transactionfee-traderate', + 'referral': { + 'url': 'https://ascendex.com/en-us/register?inviteCode=EL6BXBQM', + 'discount': 0.25, + }, + }, + 'api': { + 'v1': { + 'public': { + 'get': { + 'assets': 1, + 'products': 1, + 'ticker': 1, + 'barhist/info': 1, + 'barhist': 1, + 'depth': 1, + 'trades': 1, + 'cash/assets': 1, # not documented + 'cash/products': 1, # not documented + 'margin/assets': 1, # not documented + 'margin/products': 1, # not documented + 'futures/collateral': 1, + 'futures/contracts': 1, + 'futures/ref-px': 1, + 'futures/market-data': 1, + 'futures/funding-rates': 1, + 'risk-limit-info': 1, + 'exchange-info': 1, + }, + }, + 'private': { + 'get': { + 'info': 1, + 'wallet/transactions': 1, + 'wallet/deposit/address': 1, + 'data/balance/snapshot': 1, + 'data/balance/history': 1, + }, + 'accountCategory': { + 'get': { + 'balance': 1, + 'order/open': 1, + 'order/status': 1, + 'order/hist/current': 1, + 'risk': 1, + }, + 'post': { + 'order': 1, + 'order/batch': 1, + }, + 'delete': { + 'order': 1, + 'order/all': 1, + 'order/batch': 1, + }, + }, + 'accountGroup': { + 'get': { + 'cash/balance': 1, + 'margin/balance': 1, + 'margin/risk': 1, + 'futures/collateral-balance': 1, + 'futures/position': 1, + 'futures/risk': 1, + 'futures/funding-payments': 1, + 'order/hist': 1, + 'spot/fee': 1, + }, + 'post': { + 'transfer': 1, + 'futures/transfer/deposit': 1, + 'futures/transfer/withdraw': 1, + }, + }, + }, + }, + 'v2': { + 'public': { + 'get': { + 'assets': 1, + 'futures/contract': 1, + 'futures/collateral': 1, + 'futures/pricing-data': 1, + 'futures/ticker': 1, + 'risk-limit-info': 1, + }, + }, + 'private': { + 'data': { + 'get': { + 'order/hist': 1, + }, + }, + 'get': { + 'account/info': 1, + }, + 'accountGroup': { + 'get': { + 'order/hist': 1, + 'futures/position': 1, + 'futures/free-margin': 1, + 'futures/order/hist/current': 1, + 'futures/funding-payments': 1, + 'futures/order/open': 1, + 'futures/order/status': 1, + }, + 'post': { + 'futures/isolated-position-margin': 1, + 'futures/margin-type': 1, + 'futures/leverage': 1, + 'futures/transfer/deposit': 1, + 'futures/transfer/withdraw': 1, + 'futures/order': 1, + 'futures/order/batch': 1, + 'futures/order/open': 1, + 'subuser/subuser-transfer': 1, + 'subuser/subuser-transfer-hist': 1, + }, + 'delete': { + 'futures/order': 1, + 'futures/order/batch': 1, + 'futures/order/all': 1, + }, + }, + }, + }, + }, + 'fees': { + 'trading': { + 'feeSide': 'get', + 'tierBased': True, + 'percentage': True, + 'taker': self.parse_number('0.002'), + 'maker': self.parse_number('0.002'), + }, + }, + 'precisionMode': TICK_SIZE, + 'options': { + 'account-category': 'cash', # 'cash', 'margin', 'futures' # obsolete + 'account-group': None, + 'fetchClosedOrders': { + 'method': 'v2PrivateDataGetOrderHist', # 'v1PrivateAccountCategoryGetOrderHistCurrent' + }, + 'defaultType': 'spot', # 'spot', 'margin', 'swap' + 'accountsByType': { + 'spot': 'cash', + 'swap': 'futures', + 'margin': 'margin', + }, + 'transfer': { + 'fillResponseFromRequest': True, + }, + 'networks': { + 'BSC': 'BEP20 ' + '(BSC)', + 'ARB': 'arbitrum', + 'SOL': 'Solana', + 'AVAX': 'avalanche C chain', + 'OMNI': 'Omni', + # 'TRC': 'TRC20', + 'TRC20': 'TRC20', + 'ERC20': 'ERC20', + 'GO20': 'GO20', + 'BEP2': 'BEP2', + 'BTC': 'Bitcoin', + 'BCH': 'Bitcoin ABC', + 'LTC': 'Litecoin', + 'MATIC': 'Matic Network', + 'AKT': 'Akash', + }, + }, + 'features': { + 'default': { + 'sandbox': True, + 'createOrder': { + 'marginMode': True, + 'triggerPrice': True, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, # todo with triggerprice + 'takeProfitPrice': False, # todo with triggerprice + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyRequiresPrice': False, + 'marketBuyByCost': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': { + 'max': 10, + }, + 'fetchMyTrades': None, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'marketType': True, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'marketType': True, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': None, + 'fetchOHLCV': { + 'limit': 500, + }, + }, + 'spot': { + 'extends': 'default', + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 100000, + 'daysBackCanceled': 1, + 'untilDays': 100000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + }, + 'forDerivatives': { + 'extends': 'default', + 'createOrder': { + # todo: implementation + 'attachedStopLossTakeProfit': { + 'triggerPriceType': { + 'last': True, + 'mark': False, + 'index': False, + }, + 'price': False, + }, + }, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': None, + 'daysBackCanceled': None, + 'untilDays': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + }, + 'swap': { + 'linear': { + 'extends': 'forDerivatives', + }, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'exceptions': { + 'exact': { + # not documented + '1900': BadRequest, # {"code":1900,"message":"Invalid Http Request Input"} + '2100': AuthenticationError, # {"code":2100,"message":"ApiKeyFailure"} + '5002': BadSymbol, # {"code":5002,"message":"Invalid Symbol"} + '6001': BadSymbol, # {"code":6001,"message":"Trading is disabled on symbol."} + '6010': InsufficientFunds, # {'code': 6010, 'message': 'Not enough balance.'} + '60060': InvalidOrder, # {'code': 60060, 'message': 'The order is already filled or canceled.'} + '600503': InvalidOrder, # {"code":600503,"message":"Notional is too small."} + # documented + '100001': BadRequest, # INVALID_HTTP_INPUT Http request is invalid + '100002': BadRequest, # DATA_NOT_AVAILABLE Some required data is missing + '100003': BadRequest, # KEY_CONFLICT The same key exists already + '100004': BadRequest, # INVALID_REQUEST_DATA The HTTP request contains invalid field or argument + '100005': BadRequest, # INVALID_WS_REQUEST_DATA Websocket request contains invalid field or argument + '100006': BadRequest, # INVALID_ARGUMENT The arugment is invalid + '100007': BadRequest, # ENCRYPTION_ERROR Something wrong with data encryption + '100008': BadSymbol, # SYMBOL_ERROR Symbol does not exist or not valid for the request + '100009': AuthenticationError, # AUTHORIZATION_NEEDED Authorization is require for the API access or request + '100010': BadRequest, # INVALID_OPERATION The action is invalid or not allowed for the account + '100011': BadRequest, # INVALID_TIMESTAMP Not a valid timestamp + '100012': BadRequest, # INVALID_STR_FORMAT str format does not + '100013': BadRequest, # INVALID_NUM_FORMAT Invalid number input + '100101': ExchangeError, # UNKNOWN_ERROR Some unknown error + '150001': BadRequest, # INVALID_JSON_FORMAT Require a valid json object + '200001': AuthenticationError, # AUTHENTICATION_FAILED Authorization failed + '200002': ExchangeError, # TOO_MANY_ATTEMPTS Tried and failed too many times + '200003': ExchangeError, # ACCOUNT_NOT_FOUND Account not exist + '200004': ExchangeError, # ACCOUNT_NOT_SETUP Account not setup properly + '200005': ExchangeError, # ACCOUNT_ALREADY_EXIST Account already exist + '200006': ExchangeError, # ACCOUNT_ERROR Some error related with error + '200007': ExchangeError, # CODE_NOT_FOUND + '200008': ExchangeError, # CODE_EXPIRED Code expired + '200009': ExchangeError, # CODE_MISMATCH Code does not match + '200010': AuthenticationError, # PASSWORD_ERROR Wrong assword + '200011': ExchangeError, # CODE_GEN_FAILED Do not generate required code promptly + '200012': ExchangeError, # FAKE_COKE_VERIFY + '200013': ExchangeError, # SECURITY_ALERT Provide security alert message + '200014': PermissionDenied, # RESTRICTED_ACCOUNT Account is restricted for certain activity, such, or withdraw. + '200015': PermissionDenied, # PERMISSION_DENIED No enough permission for the operation + '300001': InvalidOrder, # INVALID_PRICE Order price is invalid + '300002': InvalidOrder, # INVALID_QTY Order size is invalid + '300003': InvalidOrder, # INVALID_SIDE Order side is invalid + '300004': InvalidOrder, # INVALID_NOTIONAL Notional is too small or too large + '300005': InvalidOrder, # INVALID_TYPE Order typs is invalid + '300006': InvalidOrder, # INVALID_ORDER_ID Order id is invalid + '300007': InvalidOrder, # INVALID_TIME_IN_FORCE Time In Force in order request is invalid + '300008': InvalidOrder, # INVALID_ORDER_PARAMETER Some order parameter is invalid + '300009': InvalidOrder, # TRADING_VIOLATION Trading violation on account or asset + '300011': InsufficientFunds, # INVALID_BALANCE No enough account or asset balance for the trading + '300012': BadSymbol, # INVALID_PRODUCT Not a valid product supported by exchange + '300013': InvalidOrder, # INVALID_BATCH_ORDER Some or all orders are invalid in batch order request + '300014': InvalidOrder, # {"code":300014,"message":"Order price doesn't conform to the required tick size: 0.1","reason":"TICK_SIZE_VIOLATION"} + '300020': InvalidOrder, # TRADING_RESTRICTED There is some trading restriction on account or asset + '300021': AccountSuspended, # {"code":300021,"message":"Trading disabled for self account.","reason":"TRADING_DISABLED"} + '300031': InvalidOrder, # NO_MARKET_PRICE No market price for market type order trading + '310001': InsufficientFunds, # INVALID_MARGIN_BALANCE No enough margin balance + '310002': InvalidOrder, # INVALID_MARGIN_ACCOUNT Not a valid account for margin trading + '310003': InvalidOrder, # MARGIN_TOO_RISKY Leverage is too high + '310004': BadSymbol, # INVALID_MARGIN_ASSET This asset does not support margin trading + '310005': InvalidOrder, # INVALID_REFERENCE_PRICE There is no valid reference price + '510001': ExchangeError, # SERVER_ERROR Something wrong with server. + '900001': ExchangeError, # HUMAN_CHALLENGE Human change do not pass + }, + 'broad': {}, + }, + 'commonCurrencies': { + 'XBT': 'XBT', # self is not BTC ! just another token + 'BOND': 'BONDED', + 'BTCBEAR': 'BEAR', + 'BTCBULL': 'BULL', + 'BYN': 'BeyondFi', + 'PLN': 'Pollen', + }, + }) + + def get_account(self, params={}): + # get current or provided bitmax sub-account + account = self.safe_value(params, 'account', self.options['account']) + lowercaseAccount = account.lower() + return self.capitalize(lowercaseAccount) + + def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = self.v2PublicGetAssets(params) + # + # { + # "code": "0", + # "data": [ + # { + # "assetCode": "USDT", + # "assetName": "Tether", + # "precisionScale": 9, + # "nativeScale": 4, + # "blockChain": [ + # { + # "chainName": "Solana", + # "withdrawFee": "2.0", + # "allowDeposit": True, + # "allowWithdraw": True, + # "minDepositAmt": "0.01", + # "minWithdrawal": "4.0", + # "numConfirmations": 1 + # }, + # ... + # ] + # }, + # ] + # } + # + data = self.safe_list(response, 'data', []) + result: dict = {} + for i in range(0, len(data)): + currency = data[i] + id = self.safe_string(currency, 'assetCode') + code = self.safe_currency_code(id) + chains = self.safe_list(currency, 'blockChain', []) + precision = self.parse_number(self.parse_precision(self.safe_string(currency, 'nativeScale'))) + networks = {} + for j in range(0, len(chains)): + networkEtnry = chains[j] + networkId = self.safe_string(networkEtnry, 'chainName') + networkCode = self.network_code_to_id(networkId) + networks[networkCode] = { + 'fee': self.safe_number(networkEtnry, 'withdrawFee'), + 'active': None, + 'withdraw': self.safe_bool(networkEtnry, 'allowWithdraw'), + 'deposit': self.safe_bool(networkEtnry, 'allowDeposit'), + 'precision': precision, + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': self.safe_number(networkEtnry, 'minWithdrawal'), + 'max': None, + }, + 'deposit': { + 'min': self.safe_number(networkEtnry, 'minDepositAmt'), + 'max': None, + }, + }, + } + # todo type: if chainsLength == 0 and (assetName.endswith(' Staking') or assetName.find(' Reward ') >= 0 or assetName.find('Slot Auction') >= 0 or assetName.find(' Freeze Asset') >= 0): + result[code] = self.safe_currency_structure({ + 'id': id, + 'code': code, + 'info': currency, + 'type': None, + 'margin': None, + 'name': self.safe_string(currency, 'assetName'), + 'active': None, + 'deposit': None, + 'withdraw': None, + 'fee': None, + 'precision': precision, + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': self.safe_number(currency, 'minWithdrawalAmt'), + 'max': None, + }, + }, + 'networks': networks, + }) + return result + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for ascendex + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + spotPromise = self.fetch_spot_markets(params) + contractPromise = self.fetch_contract_markets(params) + spotMarkets, contractMarkets = [spotPromise, contractPromise] + return self.array_concat(spotMarkets, contractMarkets) + + def fetch_spot_markets(self, params={}) -> List[Market]: + productsPromise = self.v1PublicGetProducts(params) + # + # { + # "code": 0, + # "data": [ + # { + # "symbol": "LBA/BTC", + # "baseAsset": "LBA", + # "quoteAsset": "BTC", + # "status": "Normal", + # "minNotional": "0.000625", + # "maxNotional": "6.25", + # "marginTradable": False, + # "commissionType": "Quote", + # "commissionReserveRate": "0.001", + # "tickSize": "0.000000001", + # "lotSize": "1" + # }, + # ] + # } + # + cashPromise = self.v1PublicGetCashProducts(params) + # + # { + # "code": 0, + # "data": [ + # { + # "symbol": "QTUM/BTC", + # "displayName": "QTUM/BTC", + # "domain": "BTC", + # "tradingStartTime": 1569506400000, + # "collapseDecimals": "0.0001,0.000001,0.00000001", + # "minQty": "0.000000001", + # "maxQty": "1000000000", + # "minNotional": "0.000625", + # "maxNotional": "12.5", + # "statusCode": "Normal", + # "statusMessage": "", + # "tickSize": "0.00000001", + # "useTick": False, + # "lotSize": "0.1", + # "useLot": False, + # "commissionType": "Quote", + # "commissionReserveRate": "0.001", + # "qtyScale": 1, + # "priceScale": 8, + # "notionalScale": 4 + # } + # ] + # } + # + products, cash = [productsPromise, cashPromise] + productsData = self.safe_list(products, 'data', []) + productsById = self.index_by(productsData, 'symbol') + cashData = self.safe_list(cash, 'data', []) + cashAndPerpetualsById = self.index_by(cashData, 'symbol') + dataById = self.deep_extend(productsById, cashAndPerpetualsById) + ids = list(dataById.keys()) + result = [] + for i in range(0, len(ids)): + id = ids[i] + if id.find('-PERP') >= 0: + continue # skip perpetuals, endpoint returns them + market = dataById[id] + status = self.safe_string(market, 'status') + domain = self.safe_string(market, 'domain') + active = False + if ((status == 'Normal') or (status == 'InternalTrading')) and (domain != 'LeveragedETF'): + active = True + minQty = self.safe_number(market, 'minQty') + maxQty = self.safe_number(market, 'maxQty') + minPrice = self.safe_number(market, 'tickSize') + maxPrice: Num = None + underlying = self.safe_string_2(market, 'underlying', 'symbol') + parts = underlying.split('/') + baseId = self.safe_string(parts, 0) + quoteId = self.safe_string(parts, 1) + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + fee = self.safe_number(market, 'commissionReserveRate') + marginTradable = self.safe_bool(market, 'marginTradable', False) + result.append({ + 'id': id, + 'symbol': base + '/' + quote, + 'base': base, + 'baseId': baseId, + 'quote': quote, + 'quoteId': quoteId, + 'settle': None, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': marginTradable, + 'swap': False, + 'future': False, + 'option': False, + 'active': active, + 'contract': False, + 'linear': None, + 'inverse': None, + 'taker': fee, + 'maker': fee, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.safe_number(market, 'lotSize'), + 'price': self.safe_number(market, 'tickSize'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': minQty, + 'max': maxQty, + }, + 'price': { + 'min': minPrice, + 'max': maxPrice, + }, + 'cost': { + 'min': self.safe_number(market, 'minNotional'), + 'max': self.safe_number(market, 'maxNotional'), + }, + }, + 'created': self.safe_integer(market, 'tradingStartTime'), + 'info': market, + }) + return result + + def fetch_contract_markets(self, params={}) -> List[Market]: + contracts = self.v2PublicGetFuturesContract(params) + # + # { + # "code": 0, + # "data": [ + # { + # "symbol": "BTC-PERP", + # "status": "Normal", + # "displayName": "BTCUSDT", + # "settlementAsset": "USDT", + # "underlying": "BTC/USDT", + # "tradingStartTime": 1579701600000, + # "priceFilter": { + # "minPrice": "0.1", + # "maxPrice": "1000000", + # "tickSize": "0.1" + # }, + # "lotSizeFilter": { + # "minQty": "0.0001", + # "maxQty": "1000000000", + # "lotSize": "0.0001" + # }, + # "commissionType": "Quote", + # "commissionReserveRate": "0.001", + # "marketOrderPriceMarkup": "0.03", + # "marginRequirements": [ + # { + # "positionNotionalLowerBound": "0", + # "positionNotionalUpperBound": "50000", + # "initialMarginRate": "0.01", + # "maintenanceMarginRate": "0.006" + # }, + # ... + # ] + # } + # ] + # } + # + data = self.safe_list(contracts, 'data', []) + result = [] + for i in range(0, len(data)): + market = data[i] + id = self.safe_string(market, 'symbol') + underlying = self.safe_string(market, 'underlying') + parts = underlying.split('/') + baseId = self.safe_string(parts, 0) + base = self.safe_currency_code(baseId) + quoteId = self.safe_string(parts, 1) + quote = self.safe_currency_code(quoteId) + settleId = self.safe_string(market, 'settlementAsset') + settle = self.safe_currency_code(settleId) + linear = settle == quote + inverse = settle == base + symbol = base + '/' + quote + ':' + settle + priceFilter = self.safe_dict(market, 'priceFilter') + lotSizeFilter = self.safe_dict(market, 'lotSizeFilter') + fee = self.safe_number(market, 'commissionReserveRate') + result.append({ + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': 'swap', + 'spot': False, + 'margin': None, + 'swap': True, + 'future': False, + 'option': False, + 'active': self.safe_string(market, 'status') == 'Normal', + 'contract': True, + 'linear': linear, + 'inverse': inverse, + 'taker': fee, + 'maker': fee, + 'contractSize': self.parse_number('1'), + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.safe_number(lotSizeFilter, 'lotSize'), + 'price': self.safe_number(priceFilter, 'tickSize'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(lotSizeFilter, 'minQty'), + 'max': self.safe_number(lotSizeFilter, 'maxQty'), + }, + 'price': { + 'min': self.safe_number(priceFilter, 'minPrice'), + 'max': self.safe_number(priceFilter, 'maxPrice'), + }, + 'cost': { + 'min': self.safe_number(market, 'minNotional'), + 'max': self.safe_number(market, 'maxNotional'), + }, + }, + 'created': self.safe_integer(market, 'tradingStartTime'), + 'info': market, + }) + return result + + def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the ascendex server + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the ascendex server + """ + request: dict = { + 'requestTime': self.milliseconds(), + } + response = self.v1PublicGetExchangeInfo(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "requestTimeEcho": 1656560463601, + # "requestReceiveAt": 1656560464331, + # "latency": 730 + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.safe_integer(data, 'requestReceiveAt') + + def fetch_accounts(self, params={}) -> List[Account]: + """ + fetch all the accounts associated with a profile + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `account structures ` indexed by the account type + """ + accountGroup = self.safe_string(self.options, 'account-group') + response = None + if accountGroup is None: + response = self.v1PrivateGetInfo(params) + # + # { + # "code":0, + # "data":{ + # "email":"igor.kroitor@gmail.com", + # "accountGroup":8, + # "viewPermission":true, + # "tradePermission":true, + # "transferPermission":true, + # "cashAccount":["cshrHKLZCjlZ2ejqkmvIHHtPmLYqdnda"], + # "marginAccount":["martXoh1v1N3EMQC5FDtSj5VHso8aI2Z"], + # "futuresAccount":["futc9r7UmFJAyBY2rE3beA2JFxav2XFF"], + # "userUID":"U6491137460" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + accountGroup = self.safe_string(data, 'accountGroup') + self.options['account-group'] = accountGroup + return [ + { + 'id': accountGroup, + 'type': None, + 'code': None, + 'info': response, + }, + ] + + def parse_balance(self, response) -> Balances: + result: dict = { + 'info': response, + 'timestamp': None, + 'datetime': None, + } + balances = self.safe_list(response, 'data', []) + for i in range(0, len(balances)): + balance = balances[i] + code = self.safe_currency_code(self.safe_string(balance, 'asset')) + account = self.account() + account['free'] = self.safe_string(balance, 'availableBalance') + account['total'] = self.safe_string(balance, 'totalBalance') + result[code] = account + return self.safe_balance(result) + + def parse_margin_balance(self, response): + result: dict = { + 'info': response, + 'timestamp': None, + 'datetime': None, + } + balances = self.safe_list(response, 'data', []) + for i in range(0, len(balances)): + balance = balances[i] + code = self.safe_currency_code(self.safe_string(balance, 'asset')) + account = self.account() + account['free'] = self.safe_string(balance, 'availableBalance') + account['total'] = self.safe_string(balance, 'totalBalance') + debt = self.safe_string(balance, 'borrowed') + interest = self.safe_string(balance, 'interest') + account['debt'] = Precise.string_add(debt, interest) + result[code] = account + return self.safe_balance(result) + + def parse_swap_balance(self, response): + result: dict = { + 'info': response, + 'timestamp': None, + 'datetime': None, + } + data = self.safe_dict(response, 'data', {}) + collaterals = self.safe_list(data, 'collaterals', []) + for i in range(0, len(collaterals)): + balance = collaterals[i] + code = self.safe_currency_code(self.safe_string(balance, 'asset')) + account = self.account() + account['total'] = self.safe_string(balance, 'balance') + result[code] = account + return self.safe_balance(result) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://ascendex.github.io/ascendex-pro-api/#cash-account-balance + https://ascendex.github.io/ascendex-pro-api/#margin-account-balance + https://ascendex.github.io/ascendex-futures-pro-api-v2/#position + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: wallet type, 'spot', 'margin', or 'swap' + :param str [params.marginMode]: 'cross' or None, for spot margin trading, value of 'isolated' is invalid + :returns dict: a `balance structure ` + """ + self.load_markets() + self.load_accounts() + marketType = None + marginMode = None + marketType, params = self.handle_market_type_and_params('fetchBalance', None, params) + marginMode, params = self.handle_margin_mode_and_params('fetchBalance', params) + isMargin = self.safe_bool(params, 'margin', False) + isCross = marginMode == 'cross' + marketType = 'margin' if (isMargin or isCross) else marketType + params = self.omit(params, 'margin') + accountsByType = self.safe_dict(self.options, 'accountsByType', {}) + accountCategory = self.safe_string(accountsByType, marketType, 'cash') + account = self.safe_dict(self.accounts, 0, {}) + accountGroup = self.safe_string(account, 'id') + request: dict = { + 'account-group': accountGroup, + } + if (marginMode == 'isolated') and (marketType != 'swap'): + raise BadRequest(self.id + ' does not supported isolated margin trading') + if (accountCategory == 'cash') or (accountCategory == 'margin'): + request['account-category'] = accountCategory + response = None + if (marketType == 'spot') or (marketType == 'margin'): + response = self.v1PrivateAccountCategoryGetBalance(self.extend(request, params)) + elif marketType == 'swap': + response = self.v2PrivateAccountGroupGetFuturesPosition(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchBalance() is not currently supported for ' + marketType + ' markets') + # + # cash + # + # { + # "code": 0, + # "data": [ + # { + # "asset": "BCHSV", + # "totalBalance": "64.298000048", + # "availableBalance": "64.298000048", + # }, + # ] + # } + # + # margin + # + # { + # "code": 0, + # "data": [ + # { + # "asset": "BCHSV", + # "totalBalance": "64.298000048", + # "availableBalance": "64.298000048", + # "borrowed": "0", + # "interest": "0", + # }, + # ] + # } + # + # swap + # + # { + # "code": 0, + # "data": { + # "accountId": "fut2ODPhGiY71Pl4vtXnOZ00ssgD7QGn", + # "ac": "FUTURES", + # "collaterals": [ + # {"asset":"ADA","balance":"0.355803","referencePrice":"1.05095","discountFactor":"0.9"}, + # {"asset":"USDT","balance":"0.000014519","referencePrice":"1","discountFactor":"1"} + # ], + # }j + # } + # + if marketType == 'swap': + return self.parse_swap_balance(response) + elif marketType == 'margin': + return self.parse_margin_balance(response) + else: + return self.parse_balance(response) + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = self.v1PublicGetDepth(self.extend(request, params)) + # + # { + # "code":0, + # "data":{ + # "m":"depth-snapshot", + # "symbol":"BTC-PERP", + # "data":{ + # "ts":1590223998202, + # "seqnum":115444921, + # "asks":[ + # ["9207.5","18.2383"], + # ["9207.75","18.8235"], + # ["9208","10.7873"], + # ], + # "bids":[ + # ["9207.25","0.4009"], + # ["9207","0.003"], + # ["9206.5","0.003"], + # ] + # } + # } + # } + # + data = self.safe_dict(response, 'data', {}) + orderbook = self.safe_dict(data, 'data', {}) + timestamp = self.safe_integer(orderbook, 'ts') + result = self.parse_order_book(orderbook, symbol, timestamp) + result['nonce'] = self.safe_integer(orderbook, 'seqnum') + return result + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "symbol":"QTUM/BTC", + # "open":"0.00016537", + # "close":"0.00019077", + # "high":"0.000192", + # "low":"0.00016537", + # "volume":"846.6", + # "ask":["0.00018698","26.2"], + # "bid":["0.00018408","503.7"], + # "type":"spot" + # } + # + timestamp = None + marketId = self.safe_string(ticker, 'symbol') + type = self.safe_string(ticker, 'type') + delimiter = '/' if (type == 'spot') else None + symbol = self.safe_symbol(marketId, market, delimiter) + close = self.safe_string(ticker, 'close') + bid = self.safe_list(ticker, 'bid', []) + ask = self.safe_list(ticker, 'ask', []) + open = self.safe_string(ticker, 'open') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': None, + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': self.safe_string(bid, 0), + 'bidVolume': self.safe_string(bid, 1), + 'ask': self.safe_string(ask, 0), + 'askVolume': self.safe_string(ask, 1), + 'vwap': None, + 'open': open, + 'close': close, + 'last': close, + 'previousClose': None, # previous day close + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': self.safe_string(ticker, 'volume'), + 'quoteVolume': None, + 'info': ticker, + }, market) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = self.v1PublicGetTicker(self.extend(request, params)) + # + # { + # "code":0, + # "data":{ + # "symbol":"BTC-PERP", # or "BTC/USDT" + # "open":"9073", + # "close":"9185.75", + # "high":"9185.75", + # "low":"9185.75", + # "volume":"576.8334", + # "ask":["9185.75","15.5863"], + # "bid":["9185.5","0.003"], + # "type":"derivatives", # or "spot" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_ticker(data, market) + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://ascendex.github.io/ascendex-pro-api/#ticker + https://ascendex.github.io/ascendex-futures-pro-api-v2/#ticker + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + request: dict = {} + market = None + if symbols is not None: + symbol = self.safe_string(symbols, 0) + market = self.market(symbol) + marketIds = self.market_ids(symbols) + request['symbol'] = ','.join(marketIds) + type = None + type, params = self.handle_market_type_and_params('fetchTickers', market, params) + response = None + if type == 'spot': + response = self.v1PublicGetTicker(self.extend(request, params)) + else: + response = self.v2PublicGetFuturesTicker(self.extend(request, params)) + # + # { + # "code":0, + # "data": { + # "symbol":"QTUM/BTC", + # "open":"0.00016537", + # "close":"0.00019077", + # "high":"0.000192", + # "low":"0.00016537", + # "volume":"846.6", + # "ask":["0.00018698","26.2"], + # "bid":["0.00018408","503.7"], + # "type":"spot" + # } + # } + # + data = self.safe_list(response, 'data', []) + if not isinstance(data, list): + return self.parse_tickers([data], symbols) + return self.parse_tickers(data, symbols) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # { + # "m":"bar", + # "s":"BTC/USDT", + # "data":{ + # "i":"1", + # "ts":1590228000000, + # "o":"9139.59", + # "c":"9131.94", + # "h":"9139.99", + # "l":"9121.71", + # "v":"25.20648" + # } + # } + # + data = self.safe_dict(ohlcv, 'data', {}) + return [ + self.safe_integer(data, 'ts'), + self.safe_number(data, 'o'), + self.safe_number(data, 'h'), + self.safe_number(data, 'l'), + self.safe_number(data, 'c'), + self.safe_number(data, 'v'), + ] + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest candle to fetch + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'interval': self.safe_string(self.timeframes, timeframe, timeframe), + } + # if since and limit are not specified + # the exchange will return just 1 last candle by default + duration = self.parse_timeframe(timeframe) + options = self.safe_dict(self.options, 'fetchOHLCV', {}) + defaultLimit = self.safe_integer(options, 'limit', 500) + until = self.safe_integer(params, 'until') + if since is not None: + request['from'] = since + if limit is None: + limit = defaultLimit + else: + limit = min(limit, defaultLimit) + toWithLimit = self.sum(since, limit * duration * 1000, 1) + if until is not None: + request['to'] = min(toWithLimit, until + 1) + else: + request['to'] = toWithLimit + elif until is not None: + request['to'] = until + 1 + if limit is None: + limit = defaultLimit + else: + limit = min(limit, defaultLimit) + request['from'] = until - (limit * duration * 1000) + elif limit is not None: + request['n'] = limit # max 500 + params = self.omit(params, 'until') + response = self.v1PublicGetBarhist(self.extend(request, params)) + # + # { + # "code":0, + # "data":[ + # { + # "m":"bar", + # "s":"BTC/USDT", + # "data":{ + # "i":"1", + # "ts":1590228000000, + # "o":"9139.59", + # "c":"9131.94", + # "h":"9139.99", + # "l":"9121.71", + # "v":"25.20648" + # } + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_ohlcvs(data, market, timeframe, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # public fetchTrades + # + # { + # "p":"9128.5", # price + # "q":"0.0030", # quantity + # "ts":1590229002385, # timestamp + # "bm":false, # if True, the buyer is the market maker, we only use self field to "define the side" of a public trade + # "seqnum":180143985289898554 + # } + # + timestamp = self.safe_integer(trade, 'ts') + priceString = self.safe_string_2(trade, 'price', 'p') + amountString = self.safe_string(trade, 'q') + buyerIsMaker = self.safe_bool(trade, 'bm', False) + side = 'sell' if buyerIsMaker else 'buy' + market = self.safe_market(None, market) + return self.safe_trade({ + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'id': None, + 'order': None, + 'type': None, + 'takerOrMaker': None, + 'side': side, + 'price': priceString, + 'amount': amountString, + 'cost': None, + 'fee': None, + }, market) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://ascendex.github.io/ascendex-pro-api/#market-trades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['n'] = limit # max 100 + response = self.v1PublicGetTrades(self.extend(request, params)) + # + # { + # "code":0, + # "data":{ + # "m":"trades", + # "symbol":"BTC-PERP", + # "data":[ + # {"p":"9128.5","q":"0.0030","ts":1590229002385,"bm":false,"seqnum":180143985289898554}, + # {"p":"9129","q":"0.0030","ts":1590229002642,"bm":false,"seqnum":180143985289898587}, + # {"p":"9129.5","q":"0.0030","ts":1590229021306,"bm":false,"seqnum":180143985289899043} + # ] + # } + # } + # + records = self.safe_dict(response, 'data', {}) + trades = self.safe_list(records, 'data', []) + return self.parse_trades(trades, market, since, limit) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'PendingNew': 'open', + 'New': 'open', + 'PartiallyFilled': 'open', + 'Filled': 'closed', + 'Canceled': 'canceled', + 'Rejected': 'rejected', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder + # + # { + # "id": "16e607e2b83a8bXHbAwwoqDo55c166fa", + # "orderId": "16e85b4d9b9a8bXHbAwwoqDoc3d66830", + # "orderType": "Market", + # "symbol": "BTC/USDT", + # "timestamp": 1573576916201 + # } + # + # & linear(fetchClosedOrders) + # + # { + # "ac": "FUTURES", + # "accountId": "fut2ODPhGiY71Pl4vtXnOZ00ssgD7QGn", + # "time": 1640819389454, + # "orderId": "a17e0874ecbdU0711043490bbtcpDU5X", + # "seqNum": -1, + # "orderType": "Limit", + # "execInst": "NULL_VAL", # NULL_VAL, ReduceOnly , ... + # "side": "Buy", + # "symbol": "BTC-PERP", + # "price": "30000", + # "orderQty": "0.002", + # "stopPrice": "0", + # "stopBy": "ref-px", + # "status": "Ack", + # "lastExecTime": 1640819389454, + # "lastQty": "0", + # "lastPx": "0", + # "avgFilledPx": "0", + # "cumFilledQty": "0", + # "fee": "0", + # "cumFee": "0", + # "feeAsset": "", + # "errorCode": "", + # "posStopLossPrice": "0", + # "posStopLossTrigger": "market", + # "posTakeProfitPrice": "0", + # "posTakeProfitTrigger": "market", + # "liquidityInd": "n" + # } + # + # fetchOrder, fetchOpenOrders, fetchClosedOrders + # + # { + # "symbol": "BTC/USDT", + # "price": "8131.22", + # "orderQty": "0.00082", + # "orderType": "Market", + # "avgPx": "7392.02", + # "cumFee": "0.005152238", + # "cumFilledQty": "0.00082", + # "errorCode": "", + # "feeAsset": "USDT", + # "lastExecTime": 1575953151764, + # "orderId": "a16eee20b6750866943712zWEDdAjt3", + # "seqNum": 2623469, + # "side": "Buy", + # "status": "Filled", + # "stopPrice": "", + # "execInst": "NULL_VAL" # "Post"(for postOnly orders), "reduceOnly"(for reduceOnly orders) + # } + # + # { + # "orderId": "a173ad938fc3U22666567717788c3b66", # orderId + # "seqNum": 18777366360, # sequence number + # "accountId": "cshwSjbpPjSwHmxPdz2CPQVU9mnbzPpt", # accountId + # "symbol": "BTC/USDT", # symbol + # "orderType": "Limit", # order type(Limit/Market/StopMarket/StopLimit) + # "side": "Sell", # order side(Buy/Sell) + # "price": "11346.77", # order price + # "stopPrice": "0", # stop price(0 by default) + # "orderQty": "0.01", # order quantity(in base asset) + # "status": "Canceled", # order status(Filled/Canceled/Rejected) + # "createTime": 1596344995793, # order creation time + # "lastExecTime": 1596344996053, # last execution time + # "avgFillPrice": "11346.77", # average filled price + # "fillQty": "0.01", # filled quantity(in base asset) + # "fee": "-0.004992579", # cummulative fee. if negative, self value is the commission charged; if possitive, self value is the rebate received. + # "feeAsset": "USDT" # fee asset + # } + # + # { + # "ac": "FUTURES", + # "accountId": "testabcdefg", + # "avgPx": "0", + # "cumFee": "0", + # "cumQty": "0", + # "errorCode": "NULL_VAL", + # "execInst": "NULL_VAL", + # "feeAsset": "USDT", + # "lastExecTime": 1584072844085, + # "orderId": "r170d21956dd5450276356bbtcpKa74", + # "orderQty": "1.1499", + # "orderType": "Limit", + # "price": "4000", + # "sendingTime": 1584072841033, + # "seqNum": 24105338, + # "side": "Buy", + # "status": "Canceled", + # "stopPrice": "", + # "symbol": "BTC-PERP" + # }, + # + status = self.parse_order_status(self.safe_string(order, 'status')) + marketId = self.safe_string(order, 'symbol') + symbol = self.safe_symbol(marketId, market, '/') + timestamp = self.safe_integer_n(order, ['timestamp', 'sendingTime', 'time']) + lastTradeTimestamp = self.safe_integer(order, 'lastExecTime') + if timestamp is None: + timestamp = lastTradeTimestamp + price = self.safe_string(order, 'price') + amount = self.safe_string(order, 'orderQty') + average = self.safe_string_2(order, 'avgPx', 'avgFilledPx') + filled = self.safe_string_n(order, ['cumFilledQty', 'cumQty', 'fillQty']) + id = self.safe_string(order, 'orderId') + clientOrderId = self.safe_string(order, 'id') + if clientOrderId is not None: + if len(clientOrderId) < 1: + clientOrderId = None + rawTypeLower = self.safe_string_lower(order, 'orderType') + type = rawTypeLower + if rawTypeLower is not None: + if rawTypeLower == 'stoplimit': + type = 'limit' + if rawTypeLower == 'stopmarket': + type = 'market' + side = self.safe_string_lower(order, 'side') + feeCost = self.safe_number_2(order, 'cumFee', 'fee') + fee = None + if feeCost is not None: + feeCurrencyId = self.safe_string(order, 'feeAsset') + feeCurrencyCode = self.safe_currency_code(feeCurrencyId) + fee = { + 'cost': feeCost, + 'currency': feeCurrencyCode, + } + triggerPrice = self.omit_zero(self.safe_string(order, 'stopPrice')) + reduceOnly = None + execInst = self.safe_string_lower(order, 'execInst') + if execInst == 'reduceonly': + reduceOnly = True + postOnly = None + if execInst == 'post': + postOnly = True + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': lastTradeTimestamp, + 'symbol': symbol, + 'type': type, + 'timeInForce': None, + 'postOnly': postOnly, + 'reduceOnly': reduceOnly, + 'side': side, + 'price': price, + 'triggerPrice': triggerPrice, + 'amount': amount, + 'cost': None, + 'average': average, + 'filled': filled, + 'remaining': None, + 'status': status, + 'fee': fee, + 'trades': None, + }, market) + + def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + self.load_markets() + self.load_accounts() + account = self.safe_dict(self.accounts, 0, {}) + accountGroup = self.safe_string(account, 'id') + request: dict = { + 'account-group': accountGroup, + } + response = self.v1PrivateAccountGroupGetSpotFee(self.extend(request, params)) + # + # { + # "code": "0", + # "data": { + # "domain": "spot", + # "userUID": "U1479576457", + # "vipLevel": "0", + # "fees": [ + # {symbol: 'HT/USDT', fee: {taker: '0.001', maker: "0.001"}}, + # {symbol: 'LAMB/BTC', fee: {taker: '0.002', maker: "0.002"}}, + # {symbol: 'STOS/USDT', fee: {taker: '0.002', maker: "0.002"}}, + # ... + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + fees = self.safe_list(data, 'fees', []) + result: dict = {} + for i in range(0, len(fees)): + fee = fees[i] + marketId = self.safe_string(fee, 'symbol') + symbol = self.safe_symbol(marketId, None, '/') + takerMaker = self.safe_dict(fee, 'fee', {}) + result[symbol] = { + 'info': fee, + 'symbol': symbol, + 'maker': self.safe_number(takerMaker, 'maker'), + 'taker': self.safe_number(takerMaker, 'taker'), + 'percentage': None, + 'tierBased': None, + } + return result + + def create_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + @ignore + helper function to build request + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.timeInForce]: "GTC", "IOC", "FOK", or "PO" + :param bool [params.postOnly]: True or False + :param float [params.triggerPrice]: the price at which a trigger order is triggered at + :returns dict: request to be sent to the exchange + """ + market = self.market(symbol) + marginMode = None + marketType = None + marginMode, params = self.handle_margin_mode_and_params('createOrderRequest', params) + marketType, params = self.handle_market_type_and_params('createOrderRequest', market, params) + accountsByType = self.safe_dict(self.options, 'accountsByType', {}) + accountCategory = self.safe_string(accountsByType, marketType, 'cash') + if marginMode is not None: + accountCategory = 'margin' + account = self.safe_dict(self.accounts, 0, {}) + accountGroup = self.safe_string(account, 'id') + clientOrderId = self.safe_string_2(params, 'clientOrderId', 'id') + request: dict = { + 'account-group': accountGroup, + 'account-category': accountCategory, + 'symbol': market['id'], + 'time': self.milliseconds(), + 'orderQty': self.amount_to_precision(symbol, amount), + 'orderType': type, # limit, market, stop_market, stop_limit + 'side': side, # buy or sell, + # 'execInst': # Post for postOnly, ReduceOnly for reduceOnly + # 'respInst': 'ACK', # ACK, 'ACCEPT, DONE + } + isMarketOrder = ((type == 'market') or (type == 'stop_market')) + isLimitOrder = ((type == 'limit') or (type == 'stop_limit')) + timeInForce = self.safe_string(params, 'timeInForce') + postOnly = self.is_post_only(isMarketOrder, False, params) + reduceOnly = self.safe_bool(params, 'reduceOnly', False) + triggerPrice = self.safe_string_2(params, 'triggerPrice', 'stopPrice') + if isLimitOrder: + request['orderPrice'] = self.price_to_precision(symbol, price) + if timeInForce == 'IOC': + request['timeInForce'] = 'IOC' + if timeInForce == 'FOK': + request['timeInForce'] = 'FOK' + if postOnly: + request['postOnly'] = True + if triggerPrice is not None: + request['stopPrice'] = self.price_to_precision(symbol, triggerPrice) + if isLimitOrder: + request['orderType'] = 'stop_limit' + elif isMarketOrder: + request['orderType'] = 'stop_market' + if clientOrderId is not None: + request['id'] = clientOrderId + if market['spot']: + if accountCategory is not None: + request['category'] = accountCategory + else: + request['account-category'] = accountCategory + if reduceOnly: + request['execInst'] = 'ReduceOnly' + if postOnly: + request['execInst'] = 'Post' + params = self.omit(params, ['reduceOnly', 'triggerPrice']) + return self.extend(request, params) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order on the exchange + + https://ascendex.github.io/ascendex-pro-api/#place-order + https://ascendex.github.io/ascendex-futures-pro-api-v2/#new-order + + :param str symbol: unified CCXT market symbol + :param str type: "limit" or "market" + :param str side: "buy" or "sell" + :param float amount: the amount of currency to trade + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.timeInForce]: "GTC", "IOC", "FOK", or "PO" + :param bool [params.postOnly]: True or False + :param float [params.triggerPrice]: the price at which a trigger order is triggered at + :param dict [params.takeProfit]: *takeProfit object in params* containing the triggerPrice that the attached take profit order will be triggered(perpetual swap markets only) + :param float [params.takeProfit.triggerPrice]: *swap only* take profit trigger price + :param dict [params.stopLoss]: *stopLoss object in params* containing the triggerPrice that the attached stop loss order will be triggered(perpetual swap markets only) + :param float [params.stopLoss.triggerPrice]: *swap only* stop loss trigger price + :returns: `An order structure ` + """ + self.load_markets() + self.load_accounts() + market = self.market(symbol) + request = self.create_order_request(symbol, type, side, amount, price, params) + response = None + if market['swap']: + response = self.v2PrivateAccountGroupPostFuturesOrder(request) + else: + response = self.v1PrivateAccountCategoryPostOrder(request) + # + # spot + # + # { + # "code":0, + # "data": { + # "accountId":"cshwT8RKojkT1HoaA5UdeimR2SrmHG2I", + # "ac":"CASH", + # "action":"place-order", + # "status":"Ack", + # "info": { + # "symbol":"TRX/USDT", + # "orderType":"StopLimit", + # "timestamp":1654290662172, + # "id":"", + # "orderId":"a1812b6840ddU8191168955av0k6Eyhj" + # } + # } + # } + # + # swap + # + # { + # "code":0, + # "data": { + # "meta": { + # "id":"", + # "action":"place-order", + # "respInst":"ACK" + # }, + # "order": { + # "ac":"FUTURES", + # "accountId":"futwT8RKojkT1HoaA5UdeimR2SrmHG2I", + # "time":1654290969965, + # "orderId":"a1812b6cf322U8191168955oJamfTh7b", + # "seqNum":-1, + # "orderType":"StopLimit", + # "execInst":"NULL_VAL", + # "side":"Buy", + # "symbol":"TRX-PERP", + # "price":"0.083", + # "orderQty":"1", + # "stopPrice":"0.082", + # "stopBy":"ref-px", + # "status":"Ack", + # "lastExecTime":1654290969965, + # "lastQty":"0", + # "lastPx":"0", + # "avgFilledPx":"0", + # "cumFilledQty":"0", + # "fee":"0", + # "cumFee":"0", + # "feeAsset":"", + # "errorCode":"", + # "posStopLossPrice":"0", + # "posStopLossTrigger":"market", + # "posTakeProfitPrice":"0", + # "posTakeProfitTrigger":"market", + # "liquidityInd":"n" + # } + # } + # } + # + data = self.safe_dict(response, 'data', {}) + order = self.safe_dict_2(data, 'order', 'info', {}) + return self.parse_order(order, market) + + def create_orders(self, orders: List[OrderRequest], params={}): + """ + create a list of trade orders + + https://ascendex.github.io/ascendex-pro-api/#place-batch-orders + https://ascendex.github.io/ascendex-futures-pro-api-v2/#place-batch-orders + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.timeInForce]: "GTC", "IOC", "FOK", or "PO" + :param bool [params.postOnly]: True or False + :param float [params.triggerPrice]: the price at which a trigger order is triggered at + :returns dict: an `order structure ` + """ + self.load_markets() + self.load_accounts() + ordersRequests = [] + symbol = None + marginMode = None + for i in range(0, len(orders)): + rawOrder = orders[i] + marketId = self.safe_string(rawOrder, 'symbol') + if symbol is None: + symbol = marketId + else: + if symbol != marketId: + raise BadRequest(self.id + ' createOrders() requires all orders to have the same symbol') + type = self.safe_string(rawOrder, 'type') + side = self.safe_string(rawOrder, 'side') + amount = self.safe_number(rawOrder, 'amount') + price = self.safe_number(rawOrder, 'price') + orderParams = self.safe_dict(rawOrder, 'params', {}) + marginResult = self.handle_margin_mode_and_params('createOrders', orderParams) + currentMarginMode = marginResult[0] + if currentMarginMode is not None: + if marginMode is None: + marginMode = currentMarginMode + else: + if marginMode != currentMarginMode: + raise BadRequest(self.id + ' createOrders() requires all orders to have the same margin mode(isolated or cross)') + orderRequest = self.create_order_request(marketId, type, side, amount, price, orderParams) + ordersRequests.append(orderRequest) + market = self.market(symbol) + accountsByType = self.safe_dict(self.options, 'accountsByType', {}) + accountCategory = self.safe_string(accountsByType, market['type'], 'cash') + if marginMode is not None: + accountCategory = 'margin' + account = self.safe_dict(self.accounts, 0, {}) + accountGroup = self.safe_string(account, 'id') + request: dict = {} + response = None + if market['swap']: + raise NotSupported(self.id + ' createOrders() is not currently supported for swap markets on ascendex') + # request['account-group'] = accountGroup + # request['category'] = accountCategory + # request['orders'] = ordersRequests + # response = self.v2PrivateAccountGroupPostFuturesOrderBatch(request) + else: + request['account-group'] = accountGroup + request['account-category'] = accountCategory + request['orders'] = ordersRequests + response = self.v1PrivateAccountCategoryPostOrderBatch(request) + # + # spot + # + # { + # "code": 0, + # "data": { + # "accountId": "cshdAKBO43TKIh2kJtq7FVVb42KIePyS", + # "ac": "CASH", + # "action": "batch-place-order", + # "status": "Ack", + # "info": [ + # { + # "symbol": "BTC/USDT", + # "orderType": "Limit", + # "timestamp": 1699326589344, + # "id": "", + # "orderId": "a18ba7c1f6efU0711043490p3HvjjN5x" + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + info = self.safe_list(data, 'info', []) + return self.parse_orders(info, market) + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://ascendex.github.io/ascendex-pro-api/#query-order + https://ascendex.github.io/ascendex-futures-pro-api-v2/#query-order-by-id + + :param str id: the order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + self.load_accounts() + market = None + if symbol is not None: + market = self.market(symbol) + type, query = self.handle_market_type_and_params('fetchOrder', market, params) + accountsByType = self.safe_dict(self.options, 'accountsByType', {}) + accountCategory = self.safe_string(accountsByType, type, 'cash') + account = self.safe_dict(self.accounts, 0, {}) + accountGroup = self.safe_string(account, 'id') + request: dict = { + 'account-group': accountGroup, + 'account-category': accountCategory, + 'orderId': id, + } + response = None + if (type == 'spot') or (type == 'margin'): + response = self.v1PrivateAccountCategoryGetOrderStatus(self.extend(request, query)) + elif type == 'swap': + request['account-category'] = accountCategory + response = self.v2PrivateAccountGroupGetFuturesOrderStatus(self.extend(request, query)) + else: + raise NotSupported(self.id + ' fetchOrder() is not currently supported for ' + type + ' markets') + # + # AccountCategoryGetOrderStatus + # + # { + # "code": 0, + # "accountCategory": "CASH", + # "accountId": "cshQtyfq8XLAA9kcf19h8bXHbAwwoqDo", + # "data": [ + # { + # "symbol": "BTC/USDT", + # "price": "8131.22", + # "orderQty": "0.00082", + # "orderType": "Market", + # "avgPx": "7392.02", + # "cumFee": "0.005152238", + # "cumFilledQty": "0.00082", + # "errorCode": "", + # "feeAsset": "USDT", + # "lastExecTime": 1575953151764, + # "orderId": "a16eee20b6750866943712zWEDdAjt3", + # "seqNum": 2623469, + # "side": "Buy", + # "status": "Filled", + # "stopPrice": "", + # "execInst": "NULL_VAL" + # } + # ] + # } + # + # AccountGroupGetFuturesOrderStatus + # + # { + # "code": 0, + # "accountId": "fut2ODPhGiY71Pl4vtXnOZ00ssgD7QGn", + # "ac": "FUTURES", + # "data": { + # "ac": "FUTURES", + # "accountId": "fut2ODPhGiY71Pl4vtXnOZ00ssgD7QGn", + # "time": 1640247020217, + # "orderId": "r17de65747aeU0711043490bbtcp0cmt", + # "seqNum": 28796162908, + # "orderType": "Limit", + # "execInst": "NULL_VAL", + # "side": "Buy", + # "symbol": "BTC-PERP", + # "price": "30000", + # "orderQty": "0.0021", + # "stopPrice": "0", + # "stopBy": "market", + # "status": "New", + # "lastExecTime": 1640247020232, + # "lastQty": "0", + # "lastPx": "0", + # "avgFilledPx": "0", + # "cumFilledQty": "0", + # "fee": "0", + # "cumFee": "0", + # "feeAsset": "USDT", + # "errorCode": "", + # "posStopLossPrice": "0", + # "posStopLossTrigger": "market", + # "posTakeProfitPrice": "0", + # "posTakeProfitTrigger": "market", + # "liquidityInd": "n" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data, market) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://ascendex.github.io/ascendex-pro-api/#list-open-orders + https://ascendex.github.io/ascendex-futures-pro-api-v2/#list-open-orders + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + self.load_accounts() + market = None + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + account = self.safe_dict(self.accounts, 0, {}) + accountGroup = self.safe_string(account, 'id') + type, query = self.handle_market_type_and_params('fetchOpenOrders', market, params) + accountsByType = self.safe_dict(self.options, 'accountsByType', {}) + accountCategory = self.safe_string(accountsByType, type, 'cash') + request: dict = { + 'account-group': accountGroup, + 'account-category': accountCategory, + } + response = None + if (type == 'spot') or (type == 'margin'): + response = self.v1PrivateAccountCategoryGetOrderOpen(self.extend(request, query)) + elif type == 'swap': + request['account-category'] = accountCategory + response = self.v2PrivateAccountGroupGetFuturesOrderOpen(self.extend(request, query)) + else: + raise NotSupported(self.id + ' fetchOpenOrders() is not currently supported for ' + type + ' markets') + # + # AccountCategoryGetOrderOpen + # + # { + # "ac": "CASH", + # "accountId": "cshQtyfq8XLAA9kcf19h8bXHbAwwoqDo", + # "code": 0, + # "data": [ + # { + # "avgPx": "0", # Average filled price of the order + # "cumFee": "0", # cumulative fee paid for self order + # "cumFilledQty": "0", # cumulative filled quantity + # "errorCode": "", # error code; could be empty + # "feeAsset": "USDT", # fee asset + # "lastExecTime": 1576019723550, # The last execution time of the order + # "orderId": "s16ef21882ea0866943712034f36d83", # server provided orderId + # "orderQty": "0.0083", # order quantity + # "orderType": "Limit", # order type + # "price": "7105", # order price + # "seqNum": 8193258, # sequence number + # "side": "Buy", # order side + # "status": "New", # order status on matching engine + # "stopPrice": "", # only available for stop market and stop limit orders; otherwise empty + # "symbol": "BTC/USDT", + # "execInst": "NULL_VAL" # execution instruction + # }, + # ] + # } + # + # AccountGroupGetFuturesOrderOpen + # + # { + # "code": 0, + # "data": [ + # { + # "ac": "FUTURES", + # "accountId": "fut2ODPhGiY71Pl4vtXnOZ00ssgD7QGn", + # "time": 1640247020217, + # "orderId": "r17de65747aeU0711043490bbtcp0cmt", + # "seqNum": 28796162908, + # "orderType": "Limit", + # "execInst": "NULL_VAL", + # "side": "Buy", + # "symbol": "BTC-PERP", + # "price": "30000", + # "orderQty": "0.0021", + # "stopPrice": "0", + # "stopBy": "market", + # "status": "New", + # "lastExecTime": 1640247020232, + # "lastQty": "0", + # "lastPx": "0", + # "avgFilledPx": "0", + # "cumFilledQty": "0", + # "fee": "0", + # "cumFee": "0", + # "feeAsset": "USDT", + # "errorCode": "", + # "posStopLossPrice": "0", + # "posStopLossTrigger": "market", + # "posTakeProfitPrice": "0", + # "posTakeProfitTrigger": "market", + # "liquidityInd": "n" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + if accountCategory == 'futures': + return self.parse_orders(data, market, since, limit) + # a workaround for https://github.com/ccxt/ccxt/issues/7187 + orders = [] + for i in range(0, len(data)): + order = self.parse_order(data[i], market) + orders.append(order) + return self.filter_by_symbol_since_limit(orders, symbol, since, limit) + + def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://ascendex.github.io/ascendex-pro-api/#list-history-orders-v2 + https://ascendex.github.io/ascendex-futures-pro-api-v2/#list-current-history-orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch orders for + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + self.load_accounts() + account = self.safe_dict(self.accounts, 0, {}) + accountGroup = self.safe_string(account, 'id') + request: dict = { + # 'category': accountCategory, + # 'symbol': market['id'], + # 'orderType': 'market', # optional, string + # 'side': 'buy', # or 'sell', optional, case insensitive. + # 'status': 'Filled', # "Filled", "Canceled", or "Rejected" + # 'startTime': exchange.milliseconds(), + # 'endTime': exchange.milliseconds(), + # 'page': 1, + # 'pageSize': 100, + } + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + type, query = self.handle_market_type_and_params('fetchClosedOrders', market, params) + options = self.safe_dict(self.options, 'fetchClosedOrders', {}) + defaultMethod = self.safe_string(options, 'method', 'v2PrivateDataGetOrderHist') + method = self.get_supported_mapping(type, { + 'spot': defaultMethod, + 'margin': defaultMethod, + 'swap': 'v2PrivateAccountGroupGetFuturesOrderHistCurrent', + }) + if since is not None: + request['startTime'] = since + until = self.safe_string(params, 'until') + if until is not None: + request['endTime'] = until + accountsByType = self.safe_dict(self.options, 'accountsByType', {}) + accountCategory = self.safe_string(accountsByType, type, 'cash') # margin, futures + response = None + if method == 'v1PrivateAccountCategoryGetOrderHistCurrent': + request['account-group'] = accountGroup + request['account-category'] = accountCategory + if limit is not None: + request['limit'] = limit + response = self.v1PrivateAccountCategoryGetOrderHistCurrent(self.extend(request, query)) + elif method == 'v2PrivateDataGetOrderHist': + request['account'] = accountCategory + if limit is not None: + request['limit'] = limit + response = self.v2PrivateDataGetOrderHist(self.extend(request, query)) + elif method == 'v2PrivateAccountGroupGetFuturesOrderHistCurrent': + request['account-group'] = accountGroup + request['account-category'] = accountCategory + if limit is not None: + request['pageSize'] = limit + response = self.v2PrivateAccountGroupGetFuturesOrderHistCurrent(self.extend(request, query)) + else: + raise NotSupported(self.id + ' fetchClosedOrders() is not currently supported for ' + type + ' markets') + # + # accountCategoryGetOrderHistCurrent + # + # { + # "code":0, + # "accountId":"cshrHKLZCjlZ2ejqkmvIHHtPmLYqdnda", + # "ac":"CASH", + # "data":[ + # { + # "seqNum":15561826728, + # "orderId":"a17294d305c0U6491137460bethu7kw9", + # "symbol":"ETH/USDT", + # "orderType":"Limit", + # "lastExecTime":1591635618200, + # "price":"200", + # "orderQty":"0.1", + # "side":"Buy", + # "status":"Canceled", + # "avgPx":"0", + # "cumFilledQty":"0", + # "stopPrice":"", + # "errorCode":"", + # "cumFee":"0", + # "feeAsset":"USDT", + # "execInst":"NULL_VAL" + # } + # ] + # } + # + # { + # "code": 0, + # "data": [ + # { + # "orderId" : "a173ad938fc3U22666567717788c3b66", # orderId + # "seqNum" : 18777366360, # sequence number + # "accountId" : "cshwSjbpPjSwHmxPdz2CPQVU9mnbzPpt", # accountId + # "symbol" : "BTC/USDT", # symbol + # "orderType" : "Limit", # order type(Limit/Market/StopMarket/StopLimit) + # "side" : "Sell", # order side(Buy/Sell) + # "price" : "11346.77", # order price + # "stopPrice" : "0", # stop price(0 by default) + # "orderQty" : "0.01", # order quantity(in base asset) + # "status" : "Canceled", # order status(Filled/Canceled/Rejected) + # "createTime" : 1596344995793, # order creation time + # "lastExecTime": 1596344996053, # last execution time + # "avgFillPrice": "11346.77", # average filled price + # "fillQty" : "0.01", # filled quantity(in base asset) + # "fee" : "-0.004992579", # cummulative fee. if negative, self value is the commission charged; if possitive, self value is the rebate received. + # "feeAsset" : "USDT" # fee asset + # } + # ] + # } + # + # accountGroupGetFuturesOrderHistCurrent + # + # { + # "code": 0, + # "data": [ + # { + # "ac": "FUTURES", + # "accountId": "fut2ODPhGiY71Pl4vtXnOZ00ssgD7QGn", + # "time": 1640245777002, + # "orderId": "r17de6444fa6U0711043490bbtcpJ2lI", + # "seqNum": 28796124902, + # "orderType": "Limit", + # "execInst": "NULL_VAL", + # "side": "Buy", + # "symbol": "BTC-PERP", + # "price": "30000", + # "orderQty": "0.0021", + # "stopPrice": "0", + # "stopBy": "market", + # "status": "Canceled", + # "lastExecTime": 1640246574886, + # "lastQty": "0", + # "lastPx": "0", + # "avgFilledPx": "0", + # "cumFilledQty": "0", + # "fee": "0", + # "cumFee": "0", + # "feeAsset": "USDT", + # "errorCode": "", + # "posStopLossPrice": "0", + # "posStopLossTrigger": "market", + # "posTakeProfitPrice": "0", + # "posTakeProfitTrigger": "market", + # "liquidityInd": "n" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + if not isinstance(data, list): + data = self.safe_list(data, 'data', []) + return self.parse_orders(data, market, since, limit) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://ascendex.github.io/ascendex-pro-api/#cancel-order + https://ascendex.github.io/ascendex-futures-pro-api-v2/#cancel-order + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + self.load_markets() + self.load_accounts() + market = self.market(symbol) + type, query = self.handle_market_type_and_params('cancelOrder', market, params) + accountsByType = self.safe_dict(self.options, 'accountsByType', {}) + accountCategory = self.safe_string(accountsByType, type, 'cash') + account = self.safe_dict(self.accounts, 0, {}) + accountGroup = self.safe_string(account, 'id') + request: dict = { + 'account-group': accountGroup, + 'account-category': accountCategory, + 'symbol': market['id'], + 'time': self.milliseconds(), + 'id': 'foobar', + } + clientOrderId = self.safe_string_2(params, 'clientOrderId', 'id') + if clientOrderId is None: + request['orderId'] = id + else: + request['id'] = clientOrderId + params = self.omit(params, ['clientOrderId', 'id']) + response = None + if (type == 'spot') or (type == 'margin'): + response = self.v1PrivateAccountCategoryDeleteOrder(self.extend(request, query)) + elif type == 'swap': + request['account-category'] = accountCategory + response = self.v2PrivateAccountGroupDeleteFuturesOrder(self.extend(request, query)) + else: + raise NotSupported(self.id + ' cancelOrder() is not currently supported for ' + type + ' markets') + # + # AccountCategoryDeleteOrder + # + # { + # "code": 0, + # "data": { + # "accountId": "cshQtyfq8XLAA9kcf19h8bXHbAwwoqDo", + # "ac": "CASH", + # "action": "cancel-order", + # "status": "Ack", + # "info": { + # "id": "wv8QGquoeamhssvQBeHOHGQCGlcBjj23", + # "orderId": "16e6198afb4s8bXHbAwwoqDo2ebc19dc", + # "orderType": "", # could be empty + # "symbol": "ETH/USDT", + # "timestamp": 1573594877822 + # } + # } + # } + # + # AccountGroupDeleteFuturesOrder + # + # { + # "code": 0, + # "data": { + # "meta": { + # "id": "foobar", + # "action": "cancel-order", + # "respInst": "ACK" + # }, + # "order": { + # "ac": "FUTURES", + # "accountId": "fut2ODPhGiY71Pl4vtXnOZ00ssgD7QGn", + # "time": 1640244480476, + # "orderId": "r17de63086f4U0711043490bbtcpPUF4", + # "seqNum": 28795959269, + # "orderType": "Limit", + # "execInst": "NULL_VAL", + # "side": "Buy", + # "symbol": "BTC-PERP", + # "price": "30000", + # "orderQty": "0.0021", + # "stopPrice": "0", + # "stopBy": "market", + # "status": "New", + # "lastExecTime": 1640244480491, + # "lastQty": "0", + # "lastPx": "0", + # "avgFilledPx": "0", + # "cumFilledQty": "0", + # "fee": "0", + # "cumFee": "0", + # "feeAsset": "BTCPC", + # "errorCode": "", + # "posStopLossPrice": "0", + # "posStopLossTrigger": "market", + # "posTakeProfitPrice": "0", + # "posTakeProfitTrigger": "market", + # "liquidityInd": "n" + # } + # } + # } + # + data = self.safe_dict(response, 'data', {}) + order = self.safe_dict_2(data, 'order', 'info', {}) + return self.parse_order(order, market) + + def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders + + https://ascendex.github.io/ascendex-pro-api/#cancel-all-orders + https://ascendex.github.io/ascendex-futures-pro-api-v2/#cancel-all-open-orders + + :param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list with a single `order structure ` with the response assigned to the info property + """ + self.load_markets() + self.load_accounts() + market = None + if symbol is not None: + market = self.market(symbol) + type, query = self.handle_market_type_and_params('cancelAllOrders', market, params) + accountsByType = self.safe_dict(self.options, 'accountsByType', {}) + accountCategory = self.safe_string(accountsByType, type, 'cash') + account = self.safe_dict(self.accounts, 0, {}) + accountGroup = self.safe_string(account, 'id') + request: dict = { + 'account-group': accountGroup, + 'account-category': accountCategory, + 'time': self.milliseconds(), + } + if symbol is not None: + request['symbol'] = market['id'] + response = None + if (type == 'spot') or (type == 'margin'): + response = self.v1PrivateAccountCategoryDeleteOrderAll(self.extend(request, query)) + elif type == 'swap': + request['account-category'] = accountCategory + response = self.v2PrivateAccountGroupDeleteFuturesOrderAll(self.extend(request, query)) + else: + raise NotSupported(self.id + ' cancelAllOrders() is not currently supported for ' + type + ' markets') + # + # AccountCategoryDeleteOrderAll + # + # { + # "code": 0, + # "data": { + # "ac": "CASH", + # "accountId": "cshQtyfq8XLAA9kcf19h8bXHbAwwoqDo", + # "action": "cancel-all", + # "info": { + # "id": "2bmYvi7lyTrneMzpcJcf2D7Pe9V1P9wy", + # "orderId": "", + # "orderType": "NULL_VAL", + # "symbol": "", + # "timestamp": 1574118495462 + # }, + # "status": "Ack" + # } + # } + # + # AccountGroupDeleteFuturesOrderAll + # + # { + # "code": 0, + # "data": { + # "ac": "FUTURES", + # "accountId": "fut2ODPhGiY71Pl4vtXnOZ00ssgD7QGn", + # "action": "cancel-all", + # "info": { + # "symbol":"BTC-PERP" + # } + # } + # } + # + return [self.safe_order({ + 'info': response, + })] + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + # + # { + # "address": "0xe7c70b4e73b6b450ee46c3b5c0f5fb127ca55722", + # "destTag": "", + # "tagType": "", + # "tagId": "", + # "chainName": "ERC20", + # "numConfirmations": 20, + # "withdrawalFee": 1, + # "nativeScale": 4, + # "tips": [] + # } + # + address = self.safe_string(depositAddress, 'address') + tagId = self.safe_string(depositAddress, 'tagId') + tag = self.safe_string(depositAddress, tagId) + self.check_address(address) + code = None if (currency is None) else currency['code'] + chainName = self.safe_string(depositAddress, 'blockchain') + network = self.network_id_to_code(chainName, code) + return { + 'info': depositAddress, + 'currency': code, + 'network': network, + 'address': address, + 'tag': tag, + } + + def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://ascendex.github.io/ascendex-pro-api/#query-deposit-addresses + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.network]: unified network code for deposit chain + :returns dict: an `address structure ` + """ + self.load_markets() + currency = self.currency(code) + networkCode = self.safe_string_2(params, 'network', 'chainName') + networkId = self.network_code_to_id(networkCode) + params = self.omit(params, ['chainName']) + request: dict = { + 'asset': currency['id'], + 'blockchain': networkId, + } + response = self.v1PrivateGetWalletDepositAddress(self.extend(request, params)) + # + # { + # "code":0, + # "data":{ + # "asset":"USDT", + # "assetName":"Tether", + # "address":[ + # { + # "address":"1N22odLHXnLPCjC8kwBJPTayarr9RtPod6", + # "destTag":"", + # "tagType":"", + # "tagId":"", + # "chainName":"Omni", + # "numConfirmations":3, + # "withdrawalFee":4.7, + # "nativeScale":4, + # "tips":[] + # }, + # { + # "address":"0xe7c70b4e73b6b450ee46c3b5c0f5fb127ca55722", + # "destTag":"", + # "tagType":"", + # "tagId":"", + # "chainName":"ERC20", + # "numConfirmations":20, + # "withdrawalFee":1.0, + # "nativeScale":4, + # "tips":[] + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + addresses = self.safe_list(data, 'address', []) + numAddresses = len(addresses) + address = None + if numAddresses > 1: + addressesByChainName = self.index_by(addresses, 'chainName') + if networkId is None: + chainNames = list(addressesByChainName.keys()) + chains = ', '.join(chainNames) + raise ArgumentsRequired(self.id + ' fetchDepositAddress() returned more than one address, a chainName parameter is required, one of ' + chains) + address = self.safe_dict(addressesByChainName, networkId, {}) + else: + # first address + address = self.safe_dict(addresses, 0, {}) + result = self.parse_deposit_address(address, currency) + return self.extend(result, { + 'info': response, + }) + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + request: dict = { + 'txType': 'deposit', + } + return self.fetch_transactions(code, since, limit, self.extend(request, params)) + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + request: dict = { + 'txType': 'withdrawal', + } + return self.fetch_transactions(code, since, limit, self.extend(request, params)) + + def fetch_deposits_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch history of deposits and withdrawals + :param str [code]: unified currency code for the currency of the deposit/withdrawals, default is None + :param int [since]: timestamp in ms of the earliest deposit/withdrawal, default is None + :param int [limit]: max number of deposit/withdrawals to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `transaction structure ` + """ + self.load_markets() + request: dict = { + # 'asset': currency['id'], + # 'page': 1, + # 'pageSize': 20, + # 'startTs': self.milliseconds(), + # 'endTs': self.milliseconds(), + # 'txType': undefned, # deposit, withdrawal + } + currency = None + if code is not None: + currency = self.currency(code) + request['asset'] = currency['id'] + if since is not None: + request['startTs'] = since + if limit is not None: + request['pageSize'] = limit + response = self.v1PrivateGetWalletTransactions(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "data": [ + # { + # "requestId": "wuzd1Ojsqtz4bCA3UXwtUnnJDmU8PiyB", + # "time": 1591606166000, + # "asset": "USDT", + # "transactionType": "deposit", + # "amount": "25", + # "commission": "0", + # "networkTransactionId": "0xbc4eabdce92f14dbcc01d799a5f8ca1f02f4a3a804b6350ea202be4d3c738fce", + # "status": "pending", + # "numConfirmed": 8, + # "numConfirmations": 20, + # "destAddress": {address: "0xe7c70b4e73b6b450ee46c3b5c0f5fb127ca55722"} + # } + # ], + # "page": 1, + # "pageSize": 20, + # "hasNext": False + # } + # } + # + data = self.safe_dict(response, 'data', {}) + transactions = self.safe_list(data, 'data', []) + return self.parse_transactions(transactions, currency, since, limit) + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'reviewing': 'pending', + 'pending': 'pending', + 'confirmed': 'ok', + 'rejected': 'rejected', + } + return self.safe_string(statuses, status, status) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # { + # "requestId": "wuzd1Ojsqtz4bCA3UXwtUnnJDmU8PiyB", + # "time": 1591606166000, + # "asset": "USDT", + # "transactionType": "deposit", + # "amount": "25", + # "commission": "0", + # "networkTransactionId": "0xbc4eabdce92f14dbcc01d799a5f8ca1f02f4a3a804b6350ea202be4d3c738fce", + # "status": "pending", + # "numConfirmed": 8, + # "numConfirmations": 20, + # "destAddress": { + # "address": "0xe7c70b4e73b6b450ee46c3b5c0f5fb127ca55722", + # "destTag": "..." # for currencies that have it + # } + # } + # + destAddress = self.safe_dict(transaction, 'destAddress', {}) + address = self.safe_string(destAddress, 'address') + tag = self.safe_string(destAddress, 'destTag') + timestamp = self.safe_integer(transaction, 'time') + currencyId = self.safe_string(transaction, 'asset') + amountString = self.safe_string(transaction, 'amount') + feeCostString = self.safe_string(transaction, 'commission') + amountString = Precise.string_sub(amountString, feeCostString) + code = self.safe_currency_code(currencyId, currency) + return { + 'info': transaction, + 'id': self.safe_string(transaction, 'requestId'), + 'txid': self.safe_string(transaction, 'networkTransactionId'), + 'type': self.safe_string(transaction, 'transactionType'), + 'currency': code, + 'network': None, + 'amount': self.parse_number(amountString), + 'status': self.parse_transaction_status(self.safe_string(transaction, 'status')), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'address': address, + 'addressFrom': None, + 'addressTo': address, + 'tag': tag, + 'tagFrom': None, + 'tagTo': tag, + 'updated': None, + 'comment': None, + 'fee': { + 'currency': code, + 'cost': self.parse_number(feeCostString), + 'rate': None, + }, + 'internal': False, + } + + def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + self.load_markets() + self.load_accounts() + account = self.safe_dict(self.accounts, 0, {}) + accountGroup = self.safe_string(account, 'id') + request: dict = { + 'account-group': accountGroup, + } + response = self.v2PrivateAccountGroupGetFuturesPosition(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "accountId": "fut2ODPhGiY71Pl4vtXnOZ00ssgD7QGn", + # "ac": "FUTURES", + # "collaterals": [ + # { + # "asset": "USDT", + # "balance": "44.570287262", + # "referencePrice": "1", + # "discountFactor": "1" + # } + # ], + # "contracts": [ + # { + # "symbol": "BTC-PERP", + # "side": "LONG", + # "position": "0.0001", + # "referenceCost": "-3.12277254", + # "unrealizedPnl": "-0.001700233", + # "realizedPnl": "0", + # "avgOpenPrice": "31209", + # "marginType": "isolated", + # "isolatedMargin": "1.654972977", + # "leverage": "2", + # "takeProfitPrice": "0", + # "takeProfitTrigger": "market", + # "stopLossPrice": "0", + # "stopLossTrigger": "market", + # "buyOpenOrderNotional": "0", + # "sellOpenOrderNotional": "0", + # "markPrice": "31210.723063672", + # "indexPrice": "31223.148857925" + # }, + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + position = self.safe_list(data, 'contracts', []) + result = [] + for i in range(0, len(position)): + result.append(self.parse_position(position[i])) + symbols = self.market_symbols(symbols) + return self.filter_by_array_positions(result, 'symbol', symbols, False) + + def parse_position(self, position: dict, market: Market = None): + # + # { + # "symbol": "BTC-PERP", + # "side": "LONG", + # "position": "0.0001", + # "referenceCost": "-3.12277254", + # "unrealizedPnl": "-0.001700233", + # "realizedPnl": "0", + # "avgOpenPrice": "31209", + # "marginType": "isolated", + # "isolatedMargin": "1.654972977", + # "leverage": "2", + # "takeProfitPrice": "0", + # "takeProfitTrigger": "market", + # "stopLossPrice": "0", + # "stopLossTrigger": "market", + # "buyOpenOrderNotional": "0", + # "sellOpenOrderNotional": "0", + # "markPrice": "31210.723063672", + # "indexPrice": "31223.148857925" + # }, + # + marketId = self.safe_string(position, 'symbol') + market = self.safe_market(marketId, market) + notional = self.safe_string(position, 'buyOpenOrderNotional') + if Precise.string_eq(notional, '0'): + notional = self.safe_string(position, 'sellOpenOrderNotional') + marginType = self.safe_string(position, 'marginType') + marginMode = 'cross' if (marginType == 'crossed') else 'isolated' + collateral = None + if marginMode == 'isolated': + collateral = self.safe_string(position, 'isolatedMargin') + return self.safe_position({ + 'info': position, + 'id': None, + 'symbol': market['symbol'], + 'notional': self.parse_number(notional), + 'marginMode': marginMode, + 'liquidationPrice': None, + 'entryPrice': self.safe_number(position, 'avgOpenPrice'), + 'unrealizedPnl': self.safe_number(position, 'unrealizedPnl'), + 'percentage': None, + 'contracts': self.safe_number(position, 'position'), + 'contractSize': self.safe_number(market, 'contractSize'), + 'markPrice': self.safe_number(position, 'markPrice'), + 'lastPrice': None, + 'side': self.safe_string_lower(position, 'side'), + 'hedged': None, + 'timestamp': None, + 'datetime': None, + 'lastUpdateTimestamp': None, + 'maintenanceMargin': None, + 'maintenanceMarginPercentage': None, + 'collateral': collateral, + 'initialMargin': None, + 'initialMarginPercentage': None, + 'leverage': self.safe_integer(position, 'leverage'), + 'marginRatio': None, + 'stopLossPrice': self.safe_number(position, 'stopLossPrice'), + 'takeProfitPrice': self.safe_number(position, 'takeProfitPrice'), + }) + + def parse_funding_rate(self, contract, market: Market = None) -> FundingRate: + # + # { + # "time": 1640061364830, + # "symbol": "EOS-PERP", + # "markPrice": "3.353854865", + # "indexPrice": "3.3542", + # "openInterest": "14242", + # "fundingRate": "-0.000073026", + # "nextFundingTime": 1640073600000 + # } + # + marketId = self.safe_string(contract, 'symbol') + symbol = self.safe_symbol(marketId, market) + currentTime = self.safe_integer(contract, 'time') + nextFundingRate = self.safe_number(contract, 'fundingRate') + nextFundingRateTimestamp = self.safe_integer(contract, 'nextFundingTime') + return { + 'info': contract, + 'symbol': symbol, + 'markPrice': self.safe_number(contract, 'markPrice'), + 'indexPrice': self.safe_number(contract, 'indexPrice'), + 'interestRate': self.parse_number('0'), + 'estimatedSettlePrice': None, + 'timestamp': currentTime, + 'datetime': self.iso8601(currentTime), + 'previousFundingRate': None, + 'nextFundingRate': None, + 'previousFundingTimestamp': None, + 'nextFundingTimestamp': None, + 'previousFundingDatetime': None, + 'nextFundingDatetime': None, + 'fundingRate': nextFundingRate, + 'fundingTimestamp': nextFundingRateTimestamp, + 'fundingDatetime': self.iso8601(nextFundingRateTimestamp), + 'interval': None, + } + + def fetch_funding_rates(self, symbols: Strings = None, params={}) -> FundingRates: + """ + fetch the funding rate for multiple markets + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `funding rates structures `, indexe by market symbols + """ + self.load_markets() + symbols = self.market_symbols(symbols) + response = self.v2PublicGetFuturesPricingData(params) + # + # { + # "code": 0, + # "data": { + # "contracts": [ + # { + # "time": 1640061364830, + # "symbol": "EOS-PERP", + # "markPrice": "3.353854865", + # "indexPrice": "3.3542", + # "openInterest": "14242", + # "fundingRate": "-0.000073026", + # "nextFundingTime": 1640073600000 + # }, + # ], + # "collaterals": [ + # { + # "asset": "USDTR", + # "referencePrice": "1" + # }, + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + contracts = self.safe_list(data, 'contracts', []) + return self.parse_funding_rates(contracts, symbols) + + def modify_margin_helper(self, symbol: str, amount, type, params={}) -> MarginModification: + self.load_markets() + self.load_accounts() + market = self.market(symbol) + account = self.safe_dict(self.accounts, 0, {}) + accountGroup = self.safe_string(account, 'id') + amount = self.amount_to_precision(symbol, amount) + request: dict = { + 'account-group': accountGroup, + 'symbol': market['id'], + 'amount': amount, # positive value for adding margin, negative for reducing + } + response = self.v2PrivateAccountGroupPostFuturesIsolatedPositionMargin(self.extend(request, params)) + # + # Can only change margin for perpetual futures isolated margin positions + # + # { + # "code": 0 + # } + # + if type == 'reduce': + amount = Precise.string_abs(amount) + return self.extend(self.parse_margin_modification(response, market), { + 'amount': self.parse_number(amount), + 'type': type, + }) + + def parse_margin_modification(self, data: dict, market: Market = None) -> MarginModification: + # + # addMargin/reduceMargin + # + # { + # "code": 0 + # } + # + errorCode = self.safe_string(data, 'code') + status = 'ok' if (errorCode == '0') else 'failed' + return { + 'info': data, + 'symbol': market['symbol'], + 'type': None, + 'marginMode': 'isolated', + 'amount': None, + 'total': None, + 'code': market['quote'], + 'status': status, + 'timestamp': None, + 'datetime': None, + } + + def reduce_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + remove margin from a position + :param str symbol: unified market symbol + :param float amount: the amount of margin to remove + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin structure ` + """ + return self.modify_margin_helper(symbol, -amount, 'reduce', params) + + def add_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + add margin + :param str symbol: unified market symbol + :param float amount: amount of margin to add + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin structure ` + """ + return self.modify_margin_helper(symbol, amount, 'add', params) + + def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://ascendex.github.io/ascendex-futures-pro-api-v2/#change-contract-leverage + + :param float leverage: the rate of leverage + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setLeverage() requires a symbol argument') + if (leverage < 1) or (leverage > 100): + raise BadRequest(self.id + ' leverage should be between 1 and 100') + self.load_markets() + self.load_accounts() + market = self.market(symbol) + if not market['swap']: + raise BadSymbol(self.id + ' setLeverage() supports swap contracts only') + account = self.safe_dict(self.accounts, 0, {}) + accountGroup = self.safe_string(account, 'id') + request: dict = { + 'account-group': accountGroup, + 'symbol': market['id'], + 'leverage': leverage, + } + return self.v2PrivateAccountGroupPostFuturesLeverage(self.extend(request, params)) + + def set_margin_mode(self, marginMode: str, symbol: Str = None, params={}): + """ + set margin mode to 'cross' or 'isolated' + + https://ascendex.github.io/ascendex-futures-pro-api-v2/#change-margin-type + + :param str marginMode: 'cross' or 'isolated' + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setMarginMode() requires a symbol argument') + marginMode = marginMode.lower() + if marginMode == 'cross': + marginMode = 'crossed' + if marginMode != 'isolated' and marginMode != 'crossed': + raise BadRequest(self.id + ' setMarginMode() marginMode argument should be isolated or cross') + self.load_markets() + self.load_accounts() + market = self.market(symbol) + account = self.safe_dict(self.accounts, 0, {}) + accountGroup = self.safe_string(account, 'id') + request: dict = { + 'account-group': accountGroup, + 'symbol': market['id'], + 'marginType': marginMode, + } + if not market['swap']: + raise BadSymbol(self.id + ' setMarginMode() supports swap contracts only') + return self.v2PrivateAccountGroupPostFuturesMarginType(self.extend(request, params)) + + def fetch_leverage_tiers(self, symbols: Strings = None, params={}) -> LeverageTiers: + """ + retrieve information on the maximum leverage, and maintenance margin for trades of varying trade sizes + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `leverage tiers structures `, indexed by market symbols + """ + self.load_markets() + response = self.v2PublicGetFuturesContract(params) + # + # { + # "code":0, + # "data":[ + # { + # "symbol":"BTC-PERP", + # "status":"Normal", + # "displayName":"BTCUSDT", + # "settlementAsset":"USDT", + # "underlying":"BTC/USDT", + # "tradingStartTime":1579701600000, + # "priceFilter":{"minPrice":"1","maxPrice":"1000000","tickSize":"1"}, + # "lotSizeFilter":{"minQty":"0.0001","maxQty":"1000000000","lotSize":"0.0001"}, + # "commissionType":"Quote", + # "commissionReserveRate":"0.001", + # "marketOrderPriceMarkup":"0.03", + # "marginRequirements":[ + # {"positionNotionalLowerBound":"0","positionNotionalUpperBound":"50000","initialMarginRate":"0.01","maintenanceMarginRate":"0.006"}, + # {"positionNotionalLowerBound":"50000","positionNotionalUpperBound":"200000","initialMarginRate":"0.02","maintenanceMarginRate":"0.012"}, + # {"positionNotionalLowerBound":"200000","positionNotionalUpperBound":"2000000","initialMarginRate":"0.04","maintenanceMarginRate":"0.024"}, + # {"positionNotionalLowerBound":"2000000","positionNotionalUpperBound":"20000000","initialMarginRate":"0.1","maintenanceMarginRate":"0.06"}, + # {"positionNotionalLowerBound":"20000000","positionNotionalUpperBound":"40000000","initialMarginRate":"0.2","maintenanceMarginRate":"0.12"}, + # {"positionNotionalLowerBound":"40000000","positionNotionalUpperBound":"1000000000","initialMarginRate":"0.333333","maintenanceMarginRate":"0.2"} + # ] + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + symbols = self.market_symbols(symbols) + return self.parse_leverage_tiers(data, symbols, 'symbol') + + def parse_market_leverage_tiers(self, info, market: Market = None) -> List[LeverageTier]: + """ + :param dict info: Exchange market response for 1 market + :param dict market: CCXT market + """ + # + # { + # "symbol":"BTC-PERP", + # "status":"Normal", + # "displayName":"BTCUSDT", + # "settlementAsset":"USDT", + # "underlying":"BTC/USDT", + # "tradingStartTime":1579701600000, + # "priceFilter":{"minPrice":"1","maxPrice":"1000000","tickSize":"1"}, + # "lotSizeFilter":{"minQty":"0.0001","maxQty":"1000000000","lotSize":"0.0001"}, + # "commissionType":"Quote", + # "commissionReserveRate":"0.001", + # "marketOrderPriceMarkup":"0.03", + # "marginRequirements":[ + # {"positionNotionalLowerBound":"0","positionNotionalUpperBound":"50000","initialMarginRate":"0.01","maintenanceMarginRate":"0.006"}, + # {"positionNotionalLowerBound":"50000","positionNotionalUpperBound":"200000","initialMarginRate":"0.02","maintenanceMarginRate":"0.012"}, + # {"positionNotionalLowerBound":"200000","positionNotionalUpperBound":"2000000","initialMarginRate":"0.04","maintenanceMarginRate":"0.024"}, + # {"positionNotionalLowerBound":"2000000","positionNotionalUpperBound":"20000000","initialMarginRate":"0.1","maintenanceMarginRate":"0.06"}, + # {"positionNotionalLowerBound":"20000000","positionNotionalUpperBound":"40000000","initialMarginRate":"0.2","maintenanceMarginRate":"0.12"}, + # {"positionNotionalLowerBound":"40000000","positionNotionalUpperBound":"1000000000","initialMarginRate":"0.333333","maintenanceMarginRate":"0.2"} + # ] + # } + # + marginRequirements = self.safe_list(info, 'marginRequirements', []) + marketId = self.safe_string(info, 'symbol') + market = self.safe_market(marketId, market) + tiers = [] + for i in range(0, len(marginRequirements)): + tier = marginRequirements[i] + initialMarginRate = self.safe_string(tier, 'initialMarginRate') + tiers.append({ + 'tier': self.sum(i, 1), + 'symbol': self.safe_symbol(marketId, market, None, 'contract'), + 'currency': market['quote'], + 'minNotional': self.safe_number(tier, 'positionNotionalLowerBound'), + 'maxNotional': self.safe_number(tier, 'positionNotionalUpperBound'), + 'maintenanceMarginRate': self.safe_number(tier, 'maintenanceMarginRate'), + 'maxLeverage': self.parse_number(Precise.string_div('1', initialMarginRate)), + 'info': tier, + }) + return tiers + + def parse_deposit_withdraw_fee(self, fee, currency: Currency = None): + # + # { + # "assetCode": "USDT", + # "assetName": "Tether", + # "precisionScale": 9, + # "nativeScale": 4, + # "blockChain": [ + # { + # "chainName": "Omni", + # "withdrawFee": "30.0", + # "allowDeposit": True, + # "allowWithdraw": True, + # "minDepositAmt": "0.0", + # "minWithdrawal": "50.0", + # "numConfirmations": 3 + # }, + # ] + # } + # + blockChains = self.safe_list(fee, 'blockChain', []) + blockChainsLength = len(blockChains) + result: dict = { + 'info': fee, + 'withdraw': { + 'fee': None, + 'percentage': None, + }, + 'deposit': { + 'fee': None, + 'percentage': None, + }, + 'networks': {}, + } + for i in range(0, blockChainsLength): + blockChain = blockChains[i] + networkId = self.safe_string(blockChain, 'chainName') + currencyCode = self.safe_string(currency, 'code') + networkCode = self.network_id_to_code(networkId, currencyCode) + result['networks'][networkCode] = { + 'deposit': {'fee': None, 'percentage': None}, + 'withdraw': {'fee': self.safe_number(blockChain, 'withdrawFee'), 'percentage': False}, + } + if blockChainsLength == 1: + result['withdraw']['fee'] = self.safe_number(blockChain, 'withdrawFee') + result['withdraw']['percentage'] = False + return result + + def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}): + """ + fetch deposit and withdraw fees + + https://ascendex.github.io/ascendex-pro-api/#list-all-assets + + :param str[]|None codes: list of unified currency codes + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `fee structures ` + """ + self.load_markets() + response = self.v2PublicGetAssets(params) + data = self.safe_list(response, 'data') + return self.parse_deposit_withdraw_fees(data, codes, 'assetCode') + + def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + :param str code: unified currency codeåå + :param float amount: amount to transfer + :param str fromAccount: account to transfer from + :param str toAccount: account to transfer to + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transfer structure ` + """ + self.load_markets() + self.load_accounts() + account = self.safe_dict(self.accounts, 0, {}) + accountGroup = self.safe_string(account, 'id') + currency = self.currency(code) + accountsByType = self.safe_dict(self.options, 'accountsByType', {}) + fromId = self.safe_string(accountsByType, fromAccount, fromAccount) + toId = self.safe_string(accountsByType, toAccount, toAccount) + if fromId != 'cash' and toId != 'cash': + raise ExchangeError(self.id + ' transfer() only supports direct balance transfer between spot and swap, spot and margin') + request: dict = { + 'account-group': accountGroup, + 'amount': self.currency_to_precision(code, amount), + 'asset': currency['id'], + 'fromAccount': fromId, + 'toAccount': toId, + } + response = self.v1PrivateAccountGroupPostTransfer(self.extend(request, params)) + # + # {"code": "0"} + # + transferOptions = self.safe_dict(self.options, 'transfer', {}) + fillResponseFromRequest = self.safe_bool(transferOptions, 'fillResponseFromRequest', True) + transfer = self.parse_transfer(response, currency) + if fillResponseFromRequest: + transfer['fromAccount'] = fromAccount + transfer['toAccount'] = toAccount + transfer['amount'] = amount + transfer['currency'] = code + return transfer + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + # + # {"code": "0"} + # + status = self.safe_string(transfer, 'code') + currencyCode = self.safe_currency_code(None, currency) + return { + 'info': transfer, + 'id': None, + 'timestamp': None, + 'datetime': None, + 'currency': currencyCode, + 'amount': None, + 'fromAccount': None, + 'toAccount': None, + 'status': self.parse_transfer_status(status), + } + + def parse_transfer_status(self, status: Str) -> Str: + if status == '0': + return 'ok' + return 'failed' + + def fetch_funding_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch the history of funding payments paid and received on self account + + https://ascendex.github.io/ascendex-futures-pro-api-v2/#funding-payment-history + + :param str [symbol]: unified market symbol + :param int [since]: the earliest time in ms to fetch funding history for + :param int [limit]: the maximum number of funding history structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict: a `funding history structure ` + """ + self.load_markets() + self.load_accounts() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchFundingHistory', 'paginate') + if paginate: + return self.fetch_paginated_call_incremental('fetchFundingHistory', symbol, since, limit, params, 'page', 25) + account = self.safe_dict(self.accounts, 0, {}) + accountGroup = self.safe_string(account, 'id') + request: dict = { + 'account-group': accountGroup, + } + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if limit is not None: + request['pageSize'] = limit + response = self.v2PrivateAccountGroupGetFuturesFundingPayments(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "data": [ + # { + # "timestamp": 1640476800000, + # "symbol": "BTC-PERP", + # "paymentInUSDT": "-0.013991178", + # "fundingRate": "0.000173497" + # }, + # ], + # "page": 1, + # "pageSize": 3, + # "hasNext": True + # } + # } + # + data = self.safe_dict(response, 'data', {}) + rows = self.safe_list(data, 'data', []) + return self.parse_incomes(rows, market, since, limit) + + def parse_income(self, income, market: Market = None): + # + # { + # "timestamp": 1640476800000, + # "symbol": "BTC-PERP", + # "paymentInUSDT": "-0.013991178", + # "fundingRate": "0.000173497" + # } + # + marketId = self.safe_string(income, 'symbol') + timestamp = self.safe_integer(income, 'timestamp') + return { + 'info': income, + 'symbol': self.safe_symbol(marketId, market, '-', 'swap'), + 'code': 'USDT', + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': None, + 'amount': self.safe_number(income, 'paymentInUSDT'), + } + + def fetch_margin_modes(self, symbols: Strings = None, params={}) -> MarginModes: + """ + fetches the set margin mode of the user + + https://ascendex.github.io/ascendex-futures-pro-api-v2/#position + + :param str[] [symbols]: a list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `margin mode structures ` + """ + self.load_markets() + self.load_accounts() + account = self.safe_dict(self.accounts, 0, {}) + accountGroup = self.safe_string(account, 'id') + request: dict = { + 'account-group': accountGroup, + } + response = self.v2PrivateAccountGroupGetFuturesPosition(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "accountId": "fut2ODPhGiY71Pl4vtXnOZ00ssgD7QGn", + # "ac": "FUTURES", + # "collaterals": [ + # { + # "asset": "USDT", + # "balance": "44.570287262", + # "referencePrice": "1", + # "discountFactor": "1" + # } + # ], + # "contracts": [ + # { + # "symbol": "BTC-PERP", + # "side": "LONG", + # "position": "0.0001", + # "referenceCost": "-3.12277254", + # "unrealizedPnl": "-0.001700233", + # "realizedPnl": "0", + # "avgOpenPrice": "31209", + # "marginType": "isolated", + # "isolatedMargin": "1.654972977", + # "leverage": "2", + # "takeProfitPrice": "0", + # "takeProfitTrigger": "market", + # "stopLossPrice": "0", + # "stopLossTrigger": "market", + # "buyOpenOrderNotional": "0", + # "sellOpenOrderNotional": "0", + # "markPrice": "31210.723063672", + # "indexPrice": "31223.148857925" + # }, + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + marginModes = self.safe_list(data, 'contracts', []) + return self.parse_margin_modes(marginModes, symbols, 'symbol') + + def parse_margin_mode(self, marginMode: dict, market=None) -> MarginMode: + marketId = self.safe_string(marginMode, 'symbol') + marginType = self.safe_string(marginMode, 'marginType') + margin = 'cross' if (marginType == 'crossed') else 'isolated' + return { + 'info': marginMode, + 'symbol': self.safe_symbol(marketId, market), + 'marginMode': margin, + } + + def fetch_leverages(self, symbols: Strings = None, params={}) -> Leverages: + """ + fetch the set leverage for all contract markets + + https://ascendex.github.io/ascendex-futures-pro-api-v2/#position + + :param str[] [symbols]: a list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `leverage structures ` + """ + self.load_markets() + self.load_accounts() + account = self.safe_dict(self.accounts, 0, {}) + accountGroup = self.safe_string(account, 'id') + request: dict = { + 'account-group': accountGroup, + } + response = self.v2PrivateAccountGroupGetFuturesPosition(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "accountId": "fut2ODPhGiY71Pl4vtXnOZ00ssgD7QGn", + # "ac": "FUTURES", + # "collaterals": [ + # { + # "asset": "USDT", + # "balance": "44.570287262", + # "referencePrice": "1", + # "discountFactor": "1" + # } + # ], + # "contracts": [ + # { + # "symbol": "BTC-PERP", + # "side": "LONG", + # "position": "0.0001", + # "referenceCost": "-3.12277254", + # "unrealizedPnl": "-0.001700233", + # "realizedPnl": "0", + # "avgOpenPrice": "31209", + # "marginType": "isolated", + # "isolatedMargin": "1.654972977", + # "leverage": "2", + # "takeProfitPrice": "0", + # "takeProfitTrigger": "market", + # "stopLossPrice": "0", + # "stopLossTrigger": "market", + # "buyOpenOrderNotional": "0", + # "sellOpenOrderNotional": "0", + # "markPrice": "31210.723063672", + # "indexPrice": "31223.148857925" + # }, + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + leverages = self.safe_list(data, 'contracts', []) + return self.parse_leverages(leverages, symbols, 'symbol') + + def parse_leverage(self, leverage: dict, market: Market = None) -> Leverage: + marketId = self.safe_string(leverage, 'symbol') + leverageValue = self.safe_integer(leverage, 'leverage') + marginType = self.safe_string(leverage, 'marginType') + marginMode = 'cross' if (marginType == 'crossed') else 'isolated' + return { + 'info': leverage, + 'symbol': self.safe_symbol(marketId, market), + 'marginMode': marginMode, + 'longLeverage': leverageValue, + 'shortLeverage': leverageValue, + } + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + version = api[0] + access = api[1] + type = self.safe_string(api, 2) + url = '' + accountCategory = (type == 'accountCategory') + if accountCategory or (type == 'accountGroup'): + url += self.implode_params('/{account-group}', params) + params = self.omit(params, 'account-group') + request = self.implode_params(path, params) + url += '/api/pro/' + if version == 'v2': + if type == 'data': + request = 'data/' + version + '/' + request + else: + request = version + '/' + request + else: + url += version + '/' + if accountCategory: + url += self.implode_params('{account-category}/', params) + params = self.omit(params, 'account-category') + url += request + if (version == 'v1') and (request == 'cash/balance') or (request == 'margin/balance'): + request = 'balance' + if (version == 'v1') and (request == 'spot/fee'): + request = 'fee' + if request.find('subuser') >= 0: + parts = request.split('/') + request = parts[2] + params = self.omit(params, self.extract_params(path)) + if access == 'public': + if params: + url += '?' + self.urlencode(params) + else: + self.check_required_credentials() + timestamp = str(self.milliseconds()) + payload = timestamp + '+' + request + hmac = self.hmac(self.encode(payload), self.encode(self.secret), hashlib.sha256, 'base64') + headers = { + 'x-auth-key': self.apiKey, + 'x-auth-timestamp': timestamp, + 'x-auth-signature': hmac, + } + if method == 'GET': + if params: + url += '?' + self.urlencode(params) + else: + headers['Content-Type'] = 'application/json' + body = self.json(params) + url = self.urls['api']['rest'] + url + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None # fallback to default error handler + # + # {"code": 6010, "message": "Not enough balance."} + # {"code": 60060, "message": "The order is already filled or canceled."} + # {"code":2100,"message":"ApiKeyFailure"} + # {"code":300001,"message":"Price is too low from market price.","reason":"INVALID_PRICE","accountId":"cshrHKLZCjlZ2ejqkmvIHHtPmLYqdnda","ac":"CASH","action":"place-order","status":"Err","info":{"symbol":"BTC/USDT"}} + # + code = self.safe_string(response, 'code') + message = self.safe_string(response, 'message') + error = (code is not None) and (code != '0') + if error or (message is not None): + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], code, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + raise ExchangeError(feedback) # unknown message + return None diff --git a/ccxt/async_support/__init__.py b/ccxt/async_support/__init__.py new file mode 100644 index 0000000..caa4fb5 --- /dev/null +++ b/ccxt/async_support/__init__.py @@ -0,0 +1,293 @@ +# -*- coding: utf-8 -*- + +"""CCXT: CryptoCurrency eXchange Trading Library (Async)""" + +# ----------------------------------------------------------------------------- + +__version__ = '4.5.18' + +# ----------------------------------------------------------------------------- + +from ccxt.async_support.base.exchange import Exchange # noqa: F401 + +from ccxt.base.decimal_to_precision import decimal_to_precision # noqa: F401 +from ccxt.base.decimal_to_precision import TRUNCATE # noqa: F401 +from ccxt.base.decimal_to_precision import ROUND # noqa: F401 +from ccxt.base.decimal_to_precision import TICK_SIZE # noqa: F401 +from ccxt.base.decimal_to_precision import DECIMAL_PLACES # noqa: F401 +from ccxt.base.decimal_to_precision import SIGNIFICANT_DIGITS # noqa: F401 +from ccxt.base.decimal_to_precision import NO_PADDING # noqa: F401 +from ccxt.base.decimal_to_precision import PAD_WITH_ZERO # noqa: F401 + +from ccxt.base import errors # noqa: F401 +from ccxt.base.errors import BaseError # noqa: F401 +from ccxt.base.errors import ExchangeError # noqa: F401 +from ccxt.base.errors import AuthenticationError # noqa: F401 +from ccxt.base.errors import PermissionDenied # noqa: F401 +from ccxt.base.errors import AccountNotEnabled # noqa: F401 +from ccxt.base.errors import AccountSuspended # noqa: F401 +from ccxt.base.errors import ArgumentsRequired # noqa: F401 +from ccxt.base.errors import BadRequest # noqa: F401 +from ccxt.base.errors import BadSymbol # noqa: F401 +from ccxt.base.errors import OperationRejected # noqa: F401 +from ccxt.base.errors import NoChange # noqa: F401 +from ccxt.base.errors import MarginModeAlreadySet # noqa: F401 +from ccxt.base.errors import MarketClosed # noqa: F401 +from ccxt.base.errors import ManualInteractionNeeded # noqa: F401 +from ccxt.base.errors import RestrictedLocation # noqa: F401 +from ccxt.base.errors import InsufficientFunds # noqa: F401 +from ccxt.base.errors import InvalidAddress # noqa: F401 +from ccxt.base.errors import AddressPending # noqa: F401 +from ccxt.base.errors import InvalidOrder # noqa: F401 +from ccxt.base.errors import OrderNotFound # noqa: F401 +from ccxt.base.errors import OrderNotCached # noqa: F401 +from ccxt.base.errors import OrderImmediatelyFillable # noqa: F401 +from ccxt.base.errors import OrderNotFillable # noqa: F401 +from ccxt.base.errors import DuplicateOrderId # noqa: F401 +from ccxt.base.errors import ContractUnavailable # noqa: F401 +from ccxt.base.errors import NotSupported # noqa: F401 +from ccxt.base.errors import InvalidProxySettings # noqa: F401 +from ccxt.base.errors import ExchangeClosedByUser # noqa: F401 +from ccxt.base.errors import OperationFailed # noqa: F401 +from ccxt.base.errors import NetworkError # noqa: F401 +from ccxt.base.errors import DDoSProtection # noqa: F401 +from ccxt.base.errors import RateLimitExceeded # noqa: F401 +from ccxt.base.errors import ExchangeNotAvailable # noqa: F401 +from ccxt.base.errors import OnMaintenance # noqa: F401 +from ccxt.base.errors import InvalidNonce # noqa: F401 +from ccxt.base.errors import ChecksumError # noqa: F401 +from ccxt.base.errors import RequestTimeout # noqa: F401 +from ccxt.base.errors import BadResponse # noqa: F401 +from ccxt.base.errors import NullResponse # noqa: F401 +from ccxt.base.errors import CancelPending # noqa: F401 +from ccxt.base.errors import UnsubscribeError # noqa: F401 +from ccxt.base.errors import error_hierarchy # noqa: F401 + + +from ccxt.async_support.alpaca import alpaca # noqa: F401 +from ccxt.async_support.apex import apex # noqa: F401 +from ccxt.async_support.arkham import arkham # noqa: F401 +from ccxt.async_support.ascendex import ascendex # noqa: F401 +from ccxt.async_support.backpack import backpack # noqa: F401 +from ccxt.async_support.bequant import bequant # noqa: F401 +from ccxt.async_support.bigone import bigone # noqa: F401 +from ccxt.async_support.binance import binance # noqa: F401 +from ccxt.async_support.binancecoinm import binancecoinm # noqa: F401 +from ccxt.async_support.binanceus import binanceus # noqa: F401 +from ccxt.async_support.binanceusdm import binanceusdm # noqa: F401 +from ccxt.async_support.bingx import bingx # noqa: F401 +from ccxt.async_support.bit2c import bit2c # noqa: F401 +from ccxt.async_support.bitbank import bitbank # noqa: F401 +from ccxt.async_support.bitbns import bitbns # noqa: F401 +from ccxt.async_support.bitfinex import bitfinex # noqa: F401 +from ccxt.async_support.bitflyer import bitflyer # noqa: F401 +from ccxt.async_support.bitget import bitget # noqa: F401 +from ccxt.async_support.bithumb import bithumb # noqa: F401 +from ccxt.async_support.bitmart import bitmart # noqa: F401 +from ccxt.async_support.bitmex import bitmex # noqa: F401 +from ccxt.async_support.bitopro import bitopro # noqa: F401 +from ccxt.async_support.bitrue import bitrue # noqa: F401 +from ccxt.async_support.bitso import bitso # noqa: F401 +from ccxt.async_support.bitstamp import bitstamp # noqa: F401 +from ccxt.async_support.bitteam import bitteam # noqa: F401 +from ccxt.async_support.bittrade import bittrade # noqa: F401 +from ccxt.async_support.bitvavo import bitvavo # noqa: F401 +from ccxt.async_support.blockchaincom import blockchaincom # noqa: F401 +from ccxt.async_support.blofin import blofin # noqa: F401 +from ccxt.async_support.btcalpha import btcalpha # noqa: F401 +from ccxt.async_support.btcbox import btcbox # noqa: F401 +from ccxt.async_support.btcmarkets import btcmarkets # noqa: F401 +from ccxt.async_support.btcturk import btcturk # noqa: F401 +from ccxt.async_support.bybit import bybit # noqa: F401 +from ccxt.async_support.cex import cex # noqa: F401 +from ccxt.async_support.coinbase import coinbase # noqa: F401 +from ccxt.async_support.coinbaseadvanced import coinbaseadvanced # noqa: F401 +from ccxt.async_support.coinbaseexchange import coinbaseexchange # noqa: F401 +from ccxt.async_support.coinbaseinternational import coinbaseinternational # noqa: F401 +from ccxt.async_support.coincatch import coincatch # noqa: F401 +from ccxt.async_support.coincheck import coincheck # noqa: F401 +from ccxt.async_support.coinex import coinex # noqa: F401 +from ccxt.async_support.coinmate import coinmate # noqa: F401 +from ccxt.async_support.coinmetro import coinmetro # noqa: F401 +from ccxt.async_support.coinone import coinone # noqa: F401 +from ccxt.async_support.coinsph import coinsph # noqa: F401 +from ccxt.async_support.coinspot import coinspot # noqa: F401 +from ccxt.async_support.cryptocom import cryptocom # noqa: F401 +from ccxt.async_support.cryptomus import cryptomus # noqa: F401 +from ccxt.async_support.deepcoin import deepcoin # noqa: F401 +from ccxt.async_support.defx import defx # noqa: F401 +from ccxt.async_support.delta import delta # noqa: F401 +from ccxt.async_support.deribit import deribit # noqa: F401 +from ccxt.async_support.derive import derive # noqa: F401 +from ccxt.async_support.digifinex import digifinex # noqa: F401 +from ccxt.async_support.exmo import exmo # noqa: F401 +from ccxt.async_support.fmfwio import fmfwio # noqa: F401 +from ccxt.async_support.foxbit import foxbit # noqa: F401 +from ccxt.async_support.gate import gate # noqa: F401 +from ccxt.async_support.gateio import gateio # noqa: F401 +from ccxt.async_support.gemini import gemini # noqa: F401 +from ccxt.async_support.hashkey import hashkey # noqa: F401 +from ccxt.async_support.hibachi import hibachi # noqa: F401 +from ccxt.async_support.hitbtc import hitbtc # noqa: F401 +from ccxt.async_support.hollaex import hollaex # noqa: F401 +from ccxt.async_support.htx import htx # noqa: F401 +from ccxt.async_support.huobi import huobi # noqa: F401 +from ccxt.async_support.hyperliquid import hyperliquid # noqa: F401 +from ccxt.async_support.independentreserve import independentreserve # noqa: F401 +from ccxt.async_support.indodax import indodax # noqa: F401 +from ccxt.async_support.kraken import kraken # noqa: F401 +from ccxt.async_support.krakenfutures import krakenfutures # noqa: F401 +from ccxt.async_support.kucoin import kucoin # noqa: F401 +from ccxt.async_support.kucoinfutures import kucoinfutures # noqa: F401 +from ccxt.async_support.latoken import latoken # noqa: F401 +from ccxt.async_support.lbank import lbank # noqa: F401 +from ccxt.async_support.luno import luno # noqa: F401 +from ccxt.async_support.mercado import mercado # noqa: F401 +from ccxt.async_support.mexc import mexc # noqa: F401 +from ccxt.async_support.modetrade import modetrade # noqa: F401 +from ccxt.async_support.myokx import myokx # noqa: F401 +from ccxt.async_support.ndax import ndax # noqa: F401 +from ccxt.async_support.novadax import novadax # noqa: F401 +from ccxt.async_support.oceanex import oceanex # noqa: F401 +from ccxt.async_support.okx import okx # noqa: F401 +from ccxt.async_support.okxus import okxus # noqa: F401 +from ccxt.async_support.onetrading import onetrading # noqa: F401 +from ccxt.async_support.oxfun import oxfun # noqa: F401 +from ccxt.async_support.p2b import p2b # noqa: F401 +from ccxt.async_support.paradex import paradex # noqa: F401 +from ccxt.async_support.paymium import paymium # noqa: F401 +from ccxt.async_support.phemex import phemex # noqa: F401 +from ccxt.async_support.poloniex import poloniex # noqa: F401 +from ccxt.async_support.probit import probit # noqa: F401 +from ccxt.async_support.timex import timex # noqa: F401 +from ccxt.async_support.tokocrypto import tokocrypto # noqa: F401 +from ccxt.async_support.toobit import toobit # noqa: F401 +from ccxt.async_support.upbit import upbit # noqa: F401 +from ccxt.async_support.wavesexchange import wavesexchange # noqa: F401 +from ccxt.async_support.whitebit import whitebit # noqa: F401 +from ccxt.async_support.woo import woo # noqa: F401 +from ccxt.async_support.woofipro import woofipro # noqa: F401 +from ccxt.async_support.xt import xt # noqa: F401 +from ccxt.async_support.yobit import yobit # noqa: F401 +from ccxt.async_support.zaif import zaif # noqa: F401 +from ccxt.async_support.zonda import zonda # noqa: F401 +from ccxt.async_support.mt5 import mt5 # noqa: F401 + +exchanges = [ + 'alpaca', + 'apex', + 'arkham', + 'ascendex', + 'backpack', + 'bequant', + 'bigone', + 'binance', + 'binancecoinm', + 'binanceus', + 'binanceusdm', + 'bingx', + 'bit2c', + 'bitbank', + 'bitbns', + 'bitfinex', + 'bitflyer', + 'bitget', + 'bithumb', + 'bitmart', + 'bitmex', + 'bitopro', + 'bitrue', + 'bitso', + 'bitstamp', + 'bitteam', + 'bittrade', + 'bitvavo', + 'blockchaincom', + 'blofin', + 'btcalpha', + 'btcbox', + 'btcmarkets', + 'btcturk', + 'bybit', + 'cex', + 'coinbase', + 'coinbaseadvanced', + 'coinbaseexchange', + 'coinbaseinternational', + 'coincatch', + 'coincheck', + 'coinex', + 'coinmate', + 'coinmetro', + 'coinone', + 'coinsph', + 'coinspot', + 'cryptocom', + 'cryptomus', + 'deepcoin', + 'defx', + 'delta', + 'deribit', + 'derive', + 'digifinex', + 'exmo', + 'fmfwio', + 'foxbit', + 'gate', + 'gateio', + 'gemini', + 'hashkey', + 'hibachi', + 'hitbtc', + 'hollaex', + 'htx', + 'huobi', + 'hyperliquid', + 'independentreserve', + 'indodax', + 'kraken', + 'krakenfutures', + 'kucoin', + 'kucoinfutures', + 'latoken', + 'lbank', + 'luno', + 'mercado', + 'mexc', + 'modetrade', + 'myokx', + 'ndax', + 'novadax', + 'oceanex', + 'okx', + 'okxus', + 'onetrading', + 'oxfun', + 'p2b', + 'paradex', + 'paymium', + 'phemex', + 'poloniex', + 'probit', + 'timex', + 'tokocrypto', + 'toobit', + 'upbit', + 'wavesexchange', + 'whitebit', + 'woo', + 'woofipro', + 'xt', + 'yobit', + 'zaif', + 'zonda', + 'mt5', +] + +base = [ + 'Exchange', + 'exchanges', + 'decimal_to_precision', +] + +__all__ = base + errors.__all__ + exchanges diff --git a/ccxt/async_support/__pycache__/__init__.cpython-311.pyc b/ccxt/async_support/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..aa8c3d4 Binary files /dev/null and b/ccxt/async_support/__pycache__/__init__.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/alpaca.cpython-311.pyc b/ccxt/async_support/__pycache__/alpaca.cpython-311.pyc new file mode 100644 index 0000000..f526f5d Binary files /dev/null and b/ccxt/async_support/__pycache__/alpaca.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/apex.cpython-311.pyc b/ccxt/async_support/__pycache__/apex.cpython-311.pyc new file mode 100644 index 0000000..946fd60 Binary files /dev/null and b/ccxt/async_support/__pycache__/apex.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/arkham.cpython-311.pyc b/ccxt/async_support/__pycache__/arkham.cpython-311.pyc new file mode 100644 index 0000000..2e5c86c Binary files /dev/null and b/ccxt/async_support/__pycache__/arkham.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/ascendex.cpython-311.pyc b/ccxt/async_support/__pycache__/ascendex.cpython-311.pyc new file mode 100644 index 0000000..bdb34c2 Binary files /dev/null and b/ccxt/async_support/__pycache__/ascendex.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/backpack.cpython-311.pyc b/ccxt/async_support/__pycache__/backpack.cpython-311.pyc new file mode 100644 index 0000000..dfcc054 Binary files /dev/null and b/ccxt/async_support/__pycache__/backpack.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/bequant.cpython-311.pyc b/ccxt/async_support/__pycache__/bequant.cpython-311.pyc new file mode 100644 index 0000000..b2ef541 Binary files /dev/null and b/ccxt/async_support/__pycache__/bequant.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/bigone.cpython-311.pyc b/ccxt/async_support/__pycache__/bigone.cpython-311.pyc new file mode 100644 index 0000000..98f52b1 Binary files /dev/null and b/ccxt/async_support/__pycache__/bigone.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/binance.cpython-311.pyc b/ccxt/async_support/__pycache__/binance.cpython-311.pyc new file mode 100644 index 0000000..53689b5 Binary files /dev/null and b/ccxt/async_support/__pycache__/binance.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/binancecoinm.cpython-311.pyc b/ccxt/async_support/__pycache__/binancecoinm.cpython-311.pyc new file mode 100644 index 0000000..af1710a Binary files /dev/null and b/ccxt/async_support/__pycache__/binancecoinm.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/binanceus.cpython-311.pyc b/ccxt/async_support/__pycache__/binanceus.cpython-311.pyc new file mode 100644 index 0000000..d69e46a Binary files /dev/null and b/ccxt/async_support/__pycache__/binanceus.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/binanceusdm.cpython-311.pyc b/ccxt/async_support/__pycache__/binanceusdm.cpython-311.pyc new file mode 100644 index 0000000..2861add Binary files /dev/null and b/ccxt/async_support/__pycache__/binanceusdm.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/bingx.cpython-311.pyc b/ccxt/async_support/__pycache__/bingx.cpython-311.pyc new file mode 100644 index 0000000..9fe547b Binary files /dev/null and b/ccxt/async_support/__pycache__/bingx.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/bit2c.cpython-311.pyc b/ccxt/async_support/__pycache__/bit2c.cpython-311.pyc new file mode 100644 index 0000000..cd16e77 Binary files /dev/null and b/ccxt/async_support/__pycache__/bit2c.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/bitbank.cpython-311.pyc b/ccxt/async_support/__pycache__/bitbank.cpython-311.pyc new file mode 100644 index 0000000..62f46cf Binary files /dev/null and b/ccxt/async_support/__pycache__/bitbank.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/bitbns.cpython-311.pyc b/ccxt/async_support/__pycache__/bitbns.cpython-311.pyc new file mode 100644 index 0000000..80c6fd3 Binary files /dev/null and b/ccxt/async_support/__pycache__/bitbns.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/bitfinex.cpython-311.pyc b/ccxt/async_support/__pycache__/bitfinex.cpython-311.pyc new file mode 100644 index 0000000..c4fa72c Binary files /dev/null and b/ccxt/async_support/__pycache__/bitfinex.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/bitflyer.cpython-311.pyc b/ccxt/async_support/__pycache__/bitflyer.cpython-311.pyc new file mode 100644 index 0000000..0924379 Binary files /dev/null and b/ccxt/async_support/__pycache__/bitflyer.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/bitget.cpython-311.pyc b/ccxt/async_support/__pycache__/bitget.cpython-311.pyc new file mode 100644 index 0000000..4066701 Binary files /dev/null and b/ccxt/async_support/__pycache__/bitget.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/bithumb.cpython-311.pyc b/ccxt/async_support/__pycache__/bithumb.cpython-311.pyc new file mode 100644 index 0000000..2f77955 Binary files /dev/null and b/ccxt/async_support/__pycache__/bithumb.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/bitmart.cpython-311.pyc b/ccxt/async_support/__pycache__/bitmart.cpython-311.pyc new file mode 100644 index 0000000..bec6f2f Binary files /dev/null and b/ccxt/async_support/__pycache__/bitmart.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/bitmex.cpython-311.pyc b/ccxt/async_support/__pycache__/bitmex.cpython-311.pyc new file mode 100644 index 0000000..9039b03 Binary files /dev/null and b/ccxt/async_support/__pycache__/bitmex.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/bitopro.cpython-311.pyc b/ccxt/async_support/__pycache__/bitopro.cpython-311.pyc new file mode 100644 index 0000000..2d43c03 Binary files /dev/null and b/ccxt/async_support/__pycache__/bitopro.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/bitrue.cpython-311.pyc b/ccxt/async_support/__pycache__/bitrue.cpython-311.pyc new file mode 100644 index 0000000..b3e7251 Binary files /dev/null and b/ccxt/async_support/__pycache__/bitrue.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/bitso.cpython-311.pyc b/ccxt/async_support/__pycache__/bitso.cpython-311.pyc new file mode 100644 index 0000000..eb12e6f Binary files /dev/null and b/ccxt/async_support/__pycache__/bitso.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/bitstamp.cpython-311.pyc b/ccxt/async_support/__pycache__/bitstamp.cpython-311.pyc new file mode 100644 index 0000000..d0f265c Binary files /dev/null and b/ccxt/async_support/__pycache__/bitstamp.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/bitteam.cpython-311.pyc b/ccxt/async_support/__pycache__/bitteam.cpython-311.pyc new file mode 100644 index 0000000..9903aee Binary files /dev/null and b/ccxt/async_support/__pycache__/bitteam.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/bittrade.cpython-311.pyc b/ccxt/async_support/__pycache__/bittrade.cpython-311.pyc new file mode 100644 index 0000000..d018267 Binary files /dev/null and b/ccxt/async_support/__pycache__/bittrade.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/bitvavo.cpython-311.pyc b/ccxt/async_support/__pycache__/bitvavo.cpython-311.pyc new file mode 100644 index 0000000..a08d81a Binary files /dev/null and b/ccxt/async_support/__pycache__/bitvavo.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/blockchaincom.cpython-311.pyc b/ccxt/async_support/__pycache__/blockchaincom.cpython-311.pyc new file mode 100644 index 0000000..1709060 Binary files /dev/null and b/ccxt/async_support/__pycache__/blockchaincom.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/blofin.cpython-311.pyc b/ccxt/async_support/__pycache__/blofin.cpython-311.pyc new file mode 100644 index 0000000..16f6825 Binary files /dev/null and b/ccxt/async_support/__pycache__/blofin.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/btcalpha.cpython-311.pyc b/ccxt/async_support/__pycache__/btcalpha.cpython-311.pyc new file mode 100644 index 0000000..3081a75 Binary files /dev/null and b/ccxt/async_support/__pycache__/btcalpha.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/btcbox.cpython-311.pyc b/ccxt/async_support/__pycache__/btcbox.cpython-311.pyc new file mode 100644 index 0000000..1dc68cb Binary files /dev/null and b/ccxt/async_support/__pycache__/btcbox.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/btcmarkets.cpython-311.pyc b/ccxt/async_support/__pycache__/btcmarkets.cpython-311.pyc new file mode 100644 index 0000000..a09c89a Binary files /dev/null and b/ccxt/async_support/__pycache__/btcmarkets.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/btcturk.cpython-311.pyc b/ccxt/async_support/__pycache__/btcturk.cpython-311.pyc new file mode 100644 index 0000000..d8ba683 Binary files /dev/null and b/ccxt/async_support/__pycache__/btcturk.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/bybit.cpython-311.pyc b/ccxt/async_support/__pycache__/bybit.cpython-311.pyc new file mode 100644 index 0000000..ecd6d81 Binary files /dev/null and b/ccxt/async_support/__pycache__/bybit.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/cex.cpython-311.pyc b/ccxt/async_support/__pycache__/cex.cpython-311.pyc new file mode 100644 index 0000000..b2859ce Binary files /dev/null and b/ccxt/async_support/__pycache__/cex.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/coinbase.cpython-311.pyc b/ccxt/async_support/__pycache__/coinbase.cpython-311.pyc new file mode 100644 index 0000000..4674ab3 Binary files /dev/null and b/ccxt/async_support/__pycache__/coinbase.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/coinbaseadvanced.cpython-311.pyc b/ccxt/async_support/__pycache__/coinbaseadvanced.cpython-311.pyc new file mode 100644 index 0000000..762a621 Binary files /dev/null and b/ccxt/async_support/__pycache__/coinbaseadvanced.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/coinbaseexchange.cpython-311.pyc b/ccxt/async_support/__pycache__/coinbaseexchange.cpython-311.pyc new file mode 100644 index 0000000..82f5208 Binary files /dev/null and b/ccxt/async_support/__pycache__/coinbaseexchange.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/coinbaseinternational.cpython-311.pyc b/ccxt/async_support/__pycache__/coinbaseinternational.cpython-311.pyc new file mode 100644 index 0000000..6943671 Binary files /dev/null and b/ccxt/async_support/__pycache__/coinbaseinternational.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/coincatch.cpython-311.pyc b/ccxt/async_support/__pycache__/coincatch.cpython-311.pyc new file mode 100644 index 0000000..056306f Binary files /dev/null and b/ccxt/async_support/__pycache__/coincatch.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/coincheck.cpython-311.pyc b/ccxt/async_support/__pycache__/coincheck.cpython-311.pyc new file mode 100644 index 0000000..2f933a1 Binary files /dev/null and b/ccxt/async_support/__pycache__/coincheck.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/coinex.cpython-311.pyc b/ccxt/async_support/__pycache__/coinex.cpython-311.pyc new file mode 100644 index 0000000..9b6adf7 Binary files /dev/null and b/ccxt/async_support/__pycache__/coinex.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/coinmate.cpython-311.pyc b/ccxt/async_support/__pycache__/coinmate.cpython-311.pyc new file mode 100644 index 0000000..d531933 Binary files /dev/null and b/ccxt/async_support/__pycache__/coinmate.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/coinmetro.cpython-311.pyc b/ccxt/async_support/__pycache__/coinmetro.cpython-311.pyc new file mode 100644 index 0000000..80a117f Binary files /dev/null and b/ccxt/async_support/__pycache__/coinmetro.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/coinone.cpython-311.pyc b/ccxt/async_support/__pycache__/coinone.cpython-311.pyc new file mode 100644 index 0000000..27a8bfa Binary files /dev/null and b/ccxt/async_support/__pycache__/coinone.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/coinsph.cpython-311.pyc b/ccxt/async_support/__pycache__/coinsph.cpython-311.pyc new file mode 100644 index 0000000..23c13f0 Binary files /dev/null and b/ccxt/async_support/__pycache__/coinsph.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/coinspot.cpython-311.pyc b/ccxt/async_support/__pycache__/coinspot.cpython-311.pyc new file mode 100644 index 0000000..c4e360c Binary files /dev/null and b/ccxt/async_support/__pycache__/coinspot.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/cryptocom.cpython-311.pyc b/ccxt/async_support/__pycache__/cryptocom.cpython-311.pyc new file mode 100644 index 0000000..133443a Binary files /dev/null and b/ccxt/async_support/__pycache__/cryptocom.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/cryptomus.cpython-311.pyc b/ccxt/async_support/__pycache__/cryptomus.cpython-311.pyc new file mode 100644 index 0000000..6d54a99 Binary files /dev/null and b/ccxt/async_support/__pycache__/cryptomus.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/deepcoin.cpython-311.pyc b/ccxt/async_support/__pycache__/deepcoin.cpython-311.pyc new file mode 100644 index 0000000..e5f6c69 Binary files /dev/null and b/ccxt/async_support/__pycache__/deepcoin.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/defx.cpython-311.pyc b/ccxt/async_support/__pycache__/defx.cpython-311.pyc new file mode 100644 index 0000000..0100778 Binary files /dev/null and b/ccxt/async_support/__pycache__/defx.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/delta.cpython-311.pyc b/ccxt/async_support/__pycache__/delta.cpython-311.pyc new file mode 100644 index 0000000..bb1ffcf Binary files /dev/null and b/ccxt/async_support/__pycache__/delta.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/deribit.cpython-311.pyc b/ccxt/async_support/__pycache__/deribit.cpython-311.pyc new file mode 100644 index 0000000..19dcfa2 Binary files /dev/null and b/ccxt/async_support/__pycache__/deribit.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/derive.cpython-311.pyc b/ccxt/async_support/__pycache__/derive.cpython-311.pyc new file mode 100644 index 0000000..70d42d7 Binary files /dev/null and b/ccxt/async_support/__pycache__/derive.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/digifinex.cpython-311.pyc b/ccxt/async_support/__pycache__/digifinex.cpython-311.pyc new file mode 100644 index 0000000..c89ba24 Binary files /dev/null and b/ccxt/async_support/__pycache__/digifinex.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/exmo.cpython-311.pyc b/ccxt/async_support/__pycache__/exmo.cpython-311.pyc new file mode 100644 index 0000000..3d8c4f2 Binary files /dev/null and b/ccxt/async_support/__pycache__/exmo.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/fmfwio.cpython-311.pyc b/ccxt/async_support/__pycache__/fmfwio.cpython-311.pyc new file mode 100644 index 0000000..6cdbe5b Binary files /dev/null and b/ccxt/async_support/__pycache__/fmfwio.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/foxbit.cpython-311.pyc b/ccxt/async_support/__pycache__/foxbit.cpython-311.pyc new file mode 100644 index 0000000..cfdf8fd Binary files /dev/null and b/ccxt/async_support/__pycache__/foxbit.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/gate.cpython-311.pyc b/ccxt/async_support/__pycache__/gate.cpython-311.pyc new file mode 100644 index 0000000..6898b19 Binary files /dev/null and b/ccxt/async_support/__pycache__/gate.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/gateio.cpython-311.pyc b/ccxt/async_support/__pycache__/gateio.cpython-311.pyc new file mode 100644 index 0000000..176253b Binary files /dev/null and b/ccxt/async_support/__pycache__/gateio.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/gemini.cpython-311.pyc b/ccxt/async_support/__pycache__/gemini.cpython-311.pyc new file mode 100644 index 0000000..cc946e9 Binary files /dev/null and b/ccxt/async_support/__pycache__/gemini.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/hashkey.cpython-311.pyc b/ccxt/async_support/__pycache__/hashkey.cpython-311.pyc new file mode 100644 index 0000000..5874963 Binary files /dev/null and b/ccxt/async_support/__pycache__/hashkey.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/hibachi.cpython-311.pyc b/ccxt/async_support/__pycache__/hibachi.cpython-311.pyc new file mode 100644 index 0000000..687791c Binary files /dev/null and b/ccxt/async_support/__pycache__/hibachi.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/hitbtc.cpython-311.pyc b/ccxt/async_support/__pycache__/hitbtc.cpython-311.pyc new file mode 100644 index 0000000..8224b52 Binary files /dev/null and b/ccxt/async_support/__pycache__/hitbtc.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/hollaex.cpython-311.pyc b/ccxt/async_support/__pycache__/hollaex.cpython-311.pyc new file mode 100644 index 0000000..6553bed Binary files /dev/null and b/ccxt/async_support/__pycache__/hollaex.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/htx.cpython-311.pyc b/ccxt/async_support/__pycache__/htx.cpython-311.pyc new file mode 100644 index 0000000..2efca3f Binary files /dev/null and b/ccxt/async_support/__pycache__/htx.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/huobi.cpython-311.pyc b/ccxt/async_support/__pycache__/huobi.cpython-311.pyc new file mode 100644 index 0000000..3c09027 Binary files /dev/null and b/ccxt/async_support/__pycache__/huobi.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/hyperliquid.cpython-311.pyc b/ccxt/async_support/__pycache__/hyperliquid.cpython-311.pyc new file mode 100644 index 0000000..edf04a1 Binary files /dev/null and b/ccxt/async_support/__pycache__/hyperliquid.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/independentreserve.cpython-311.pyc b/ccxt/async_support/__pycache__/independentreserve.cpython-311.pyc new file mode 100644 index 0000000..95727a4 Binary files /dev/null and b/ccxt/async_support/__pycache__/independentreserve.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/indodax.cpython-311.pyc b/ccxt/async_support/__pycache__/indodax.cpython-311.pyc new file mode 100644 index 0000000..0889338 Binary files /dev/null and b/ccxt/async_support/__pycache__/indodax.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/kraken.cpython-311.pyc b/ccxt/async_support/__pycache__/kraken.cpython-311.pyc new file mode 100644 index 0000000..38a36e5 Binary files /dev/null and b/ccxt/async_support/__pycache__/kraken.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/krakenfutures.cpython-311.pyc b/ccxt/async_support/__pycache__/krakenfutures.cpython-311.pyc new file mode 100644 index 0000000..a74081e Binary files /dev/null and b/ccxt/async_support/__pycache__/krakenfutures.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/kucoin.cpython-311.pyc b/ccxt/async_support/__pycache__/kucoin.cpython-311.pyc new file mode 100644 index 0000000..89623f8 Binary files /dev/null and b/ccxt/async_support/__pycache__/kucoin.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/kucoinfutures.cpython-311.pyc b/ccxt/async_support/__pycache__/kucoinfutures.cpython-311.pyc new file mode 100644 index 0000000..17ee1f0 Binary files /dev/null and b/ccxt/async_support/__pycache__/kucoinfutures.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/latoken.cpython-311.pyc b/ccxt/async_support/__pycache__/latoken.cpython-311.pyc new file mode 100644 index 0000000..12c6b6b Binary files /dev/null and b/ccxt/async_support/__pycache__/latoken.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/lbank.cpython-311.pyc b/ccxt/async_support/__pycache__/lbank.cpython-311.pyc new file mode 100644 index 0000000..9f087b4 Binary files /dev/null and b/ccxt/async_support/__pycache__/lbank.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/luno.cpython-311.pyc b/ccxt/async_support/__pycache__/luno.cpython-311.pyc new file mode 100644 index 0000000..6f73843 Binary files /dev/null and b/ccxt/async_support/__pycache__/luno.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/mercado.cpython-311.pyc b/ccxt/async_support/__pycache__/mercado.cpython-311.pyc new file mode 100644 index 0000000..35178e7 Binary files /dev/null and b/ccxt/async_support/__pycache__/mercado.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/mexc.cpython-311.pyc b/ccxt/async_support/__pycache__/mexc.cpython-311.pyc new file mode 100644 index 0000000..21ee0b3 Binary files /dev/null and b/ccxt/async_support/__pycache__/mexc.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/modetrade.cpython-311.pyc b/ccxt/async_support/__pycache__/modetrade.cpython-311.pyc new file mode 100644 index 0000000..545d1b0 Binary files /dev/null and b/ccxt/async_support/__pycache__/modetrade.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/mt5.cpython-311.pyc b/ccxt/async_support/__pycache__/mt5.cpython-311.pyc new file mode 100644 index 0000000..15d20c8 Binary files /dev/null and b/ccxt/async_support/__pycache__/mt5.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/myokx.cpython-311.pyc b/ccxt/async_support/__pycache__/myokx.cpython-311.pyc new file mode 100644 index 0000000..bc47543 Binary files /dev/null and b/ccxt/async_support/__pycache__/myokx.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/ndax.cpython-311.pyc b/ccxt/async_support/__pycache__/ndax.cpython-311.pyc new file mode 100644 index 0000000..129db01 Binary files /dev/null and b/ccxt/async_support/__pycache__/ndax.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/novadax.cpython-311.pyc b/ccxt/async_support/__pycache__/novadax.cpython-311.pyc new file mode 100644 index 0000000..d151d0d Binary files /dev/null and b/ccxt/async_support/__pycache__/novadax.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/oceanex.cpython-311.pyc b/ccxt/async_support/__pycache__/oceanex.cpython-311.pyc new file mode 100644 index 0000000..d55bb5b Binary files /dev/null and b/ccxt/async_support/__pycache__/oceanex.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/okx.cpython-311.pyc b/ccxt/async_support/__pycache__/okx.cpython-311.pyc new file mode 100644 index 0000000..287bfdf Binary files /dev/null and b/ccxt/async_support/__pycache__/okx.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/okxus.cpython-311.pyc b/ccxt/async_support/__pycache__/okxus.cpython-311.pyc new file mode 100644 index 0000000..eacc111 Binary files /dev/null and b/ccxt/async_support/__pycache__/okxus.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/onetrading.cpython-311.pyc b/ccxt/async_support/__pycache__/onetrading.cpython-311.pyc new file mode 100644 index 0000000..f00c24e Binary files /dev/null and b/ccxt/async_support/__pycache__/onetrading.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/oxfun.cpython-311.pyc b/ccxt/async_support/__pycache__/oxfun.cpython-311.pyc new file mode 100644 index 0000000..118243f Binary files /dev/null and b/ccxt/async_support/__pycache__/oxfun.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/p2b.cpython-311.pyc b/ccxt/async_support/__pycache__/p2b.cpython-311.pyc new file mode 100644 index 0000000..cd3e5bd Binary files /dev/null and b/ccxt/async_support/__pycache__/p2b.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/paradex.cpython-311.pyc b/ccxt/async_support/__pycache__/paradex.cpython-311.pyc new file mode 100644 index 0000000..ce3ff0a Binary files /dev/null and b/ccxt/async_support/__pycache__/paradex.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/paymium.cpython-311.pyc b/ccxt/async_support/__pycache__/paymium.cpython-311.pyc new file mode 100644 index 0000000..b148e3f Binary files /dev/null and b/ccxt/async_support/__pycache__/paymium.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/phemex.cpython-311.pyc b/ccxt/async_support/__pycache__/phemex.cpython-311.pyc new file mode 100644 index 0000000..dcf6afb Binary files /dev/null and b/ccxt/async_support/__pycache__/phemex.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/poloniex.cpython-311.pyc b/ccxt/async_support/__pycache__/poloniex.cpython-311.pyc new file mode 100644 index 0000000..aa4f81e Binary files /dev/null and b/ccxt/async_support/__pycache__/poloniex.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/probit.cpython-311.pyc b/ccxt/async_support/__pycache__/probit.cpython-311.pyc new file mode 100644 index 0000000..f5688d0 Binary files /dev/null and b/ccxt/async_support/__pycache__/probit.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/timex.cpython-311.pyc b/ccxt/async_support/__pycache__/timex.cpython-311.pyc new file mode 100644 index 0000000..d93b051 Binary files /dev/null and b/ccxt/async_support/__pycache__/timex.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/tokocrypto.cpython-311.pyc b/ccxt/async_support/__pycache__/tokocrypto.cpython-311.pyc new file mode 100644 index 0000000..538d785 Binary files /dev/null and b/ccxt/async_support/__pycache__/tokocrypto.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/toobit.cpython-311.pyc b/ccxt/async_support/__pycache__/toobit.cpython-311.pyc new file mode 100644 index 0000000..eabd722 Binary files /dev/null and b/ccxt/async_support/__pycache__/toobit.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/upbit.cpython-311.pyc b/ccxt/async_support/__pycache__/upbit.cpython-311.pyc new file mode 100644 index 0000000..66319db Binary files /dev/null and b/ccxt/async_support/__pycache__/upbit.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/wavesexchange.cpython-311.pyc b/ccxt/async_support/__pycache__/wavesexchange.cpython-311.pyc new file mode 100644 index 0000000..1429346 Binary files /dev/null and b/ccxt/async_support/__pycache__/wavesexchange.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/whitebit.cpython-311.pyc b/ccxt/async_support/__pycache__/whitebit.cpython-311.pyc new file mode 100644 index 0000000..f1f7bf7 Binary files /dev/null and b/ccxt/async_support/__pycache__/whitebit.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/woo.cpython-311.pyc b/ccxt/async_support/__pycache__/woo.cpython-311.pyc new file mode 100644 index 0000000..8f605cd Binary files /dev/null and b/ccxt/async_support/__pycache__/woo.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/woofipro.cpython-311.pyc b/ccxt/async_support/__pycache__/woofipro.cpython-311.pyc new file mode 100644 index 0000000..36f475a Binary files /dev/null and b/ccxt/async_support/__pycache__/woofipro.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/xt.cpython-311.pyc b/ccxt/async_support/__pycache__/xt.cpython-311.pyc new file mode 100644 index 0000000..3567a34 Binary files /dev/null and b/ccxt/async_support/__pycache__/xt.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/yobit.cpython-311.pyc b/ccxt/async_support/__pycache__/yobit.cpython-311.pyc new file mode 100644 index 0000000..f73f24c Binary files /dev/null and b/ccxt/async_support/__pycache__/yobit.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/zaif.cpython-311.pyc b/ccxt/async_support/__pycache__/zaif.cpython-311.pyc new file mode 100644 index 0000000..a4c967f Binary files /dev/null and b/ccxt/async_support/__pycache__/zaif.cpython-311.pyc differ diff --git a/ccxt/async_support/__pycache__/zonda.cpython-311.pyc b/ccxt/async_support/__pycache__/zonda.cpython-311.pyc new file mode 100644 index 0000000..dc6718a Binary files /dev/null and b/ccxt/async_support/__pycache__/zonda.cpython-311.pyc differ diff --git a/ccxt/async_support/alpaca.py b/ccxt/async_support/alpaca.py new file mode 100644 index 0000000..d26cc06 --- /dev/null +++ b/ccxt/async_support/alpaca.py @@ -0,0 +1,1860 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.alpaca import ImplicitAPI +from ccxt.base.types import Any, Balances, Currency, DepositAddress, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import NotSupported +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class alpaca(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(alpaca, self).describe(), { + 'id': 'alpaca', + 'name': 'Alpaca', + 'countries': ['US'], + # 3 req/s for free + # 150 req/s for subscribers: https://alpaca.markets/data + # for brokers: https://alpaca.markets/docs/api-references/broker-api/#authentication-and-rate-limit + 'rateLimit': 333, + 'hostname': 'alpaca.markets', + 'pro': True, + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/e9476df8-a450-4c3e-ab9a-1a7794219e1b', + 'www': 'https://alpaca.markets', + 'api': { + 'broker': 'https://broker-api.{hostname}', + 'trader': 'https://api.{hostname}', + 'market': 'https://data.{hostname}', + }, + 'test': { + 'broker': 'https://broker-api.sandbox.{hostname}', + 'trader': 'https://paper-api.{hostname}', + 'market': 'https://data.{hostname}', + }, + 'doc': 'https://alpaca.markets/docs/', + 'fees': 'https://docs.alpaca.markets/docs/crypto-fees', + }, + 'has': { + 'CORS': False, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createMarketBuyOrder': True, + 'createMarketBuyOrderWithCost': True, + 'createMarketOrderWithCost': True, + 'createOrder': True, + 'createOrderWithTakeProfitAndStopLoss': False, + 'createOrderWithTakeProfitAndStopLossWs': False, + 'createReduceOnlyOrder': False, + 'createStopOrder': True, + 'createTriggerOrder': True, + 'editOrder': True, + 'fetchBalance': True, + 'fetchBidsAsks': False, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': False, + 'fetchDepositAddress': True, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchDepositsWithdrawals': True, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchL1OrderBook': True, + 'fetchL2OrderBook': False, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrder': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchStatus': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': False, + 'fetchTransactionFees': False, + 'fetchTransactions': False, + 'fetchTransfers': False, + 'fetchVolatilityHistory': False, + 'fetchWithdrawals': True, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'sandbox': True, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'transfer': False, + 'withdraw': True, + }, + 'api': { + 'broker': { + }, + 'trader': { + 'private': { + 'get': [ + 'v2/account', + 'v2/orders', + 'v2/orders/{order_id}', + 'v2/positions', + 'v2/positions/{symbol_or_asset_id}', + 'v2/account/portfolio/history', + 'v2/watchlists', + 'v2/watchlists/{watchlist_id}', + 'v2/watchlists:by_name', + 'v2/account/configurations', + 'v2/account/activities', + 'v2/account/activities/{activity_type}', + 'v2/calendar', + 'v2/clock', + 'v2/assets', + 'v2/assets/{symbol_or_asset_id}', + 'v2/corporate_actions/announcements/{id}', + 'v2/corporate_actions/announcements', + 'v2/wallets', + 'v2/wallets/transfers', + ], + 'post': [ + 'v2/orders', + 'v2/watchlists', + 'v2/watchlists/{watchlist_id}', + 'v2/watchlists:by_name', + 'v2/wallets/transfers', + ], + 'put': [ + 'v2/orders/{order_id}', + 'v2/watchlists/{watchlist_id}', + 'v2/watchlists:by_name', + ], + 'patch': [ + 'v2/orders/{order_id}', + 'v2/account/configurations', + ], + 'delete': [ + 'v2/orders', + 'v2/orders/{order_id}', + 'v2/positions', + 'v2/positions/{symbol_or_asset_id}', + 'v2/watchlists/{watchlist_id}', + 'v2/watchlists:by_name', + 'v2/watchlists/{watchlist_id}/{symbol}', + ], + }, + }, + 'market': { + 'public': { + 'get': [ + 'v1beta3/crypto/{loc}/bars', + 'v1beta3/crypto/{loc}/latest/bars', + 'v1beta3/crypto/{loc}/latest/orderbooks', + 'v1beta3/crypto/{loc}/latest/quotes', + 'v1beta3/crypto/{loc}/latest/trades', + 'v1beta3/crypto/{loc}/quotes', + 'v1beta3/crypto/{loc}/snapshots', + 'v1beta3/crypto/{loc}/trades', + ], + }, + 'private': { + 'get': [ + 'v1beta1/corporate-actions', + 'v1beta1/forex/latest/rates', + 'v1beta1/forex/rates', + 'v1beta1/logos/{symbol}', + 'v1beta1/news', + 'v1beta1/screener/stocks/most-actives', + 'v1beta1/screener/{market_type}/movers', + 'v2/stocks/auctions', + 'v2/stocks/bars', + 'v2/stocks/bars/latest', + 'v2/stocks/meta/conditions/{ticktype}', + 'v2/stocks/meta/exchanges', + 'v2/stocks/quotes', + 'v2/stocks/quotes/latest', + 'v2/stocks/snapshots', + 'v2/stocks/trades', + 'v2/stocks/trades/latest', + 'v2/stocks/{symbol}/auctions', + 'v2/stocks/{symbol}/bars', + 'v2/stocks/{symbol}/bars/latest', + 'v2/stocks/{symbol}/quotes', + 'v2/stocks/{symbol}/quotes/latest', + 'v2/stocks/{symbol}/snapshot', + 'v2/stocks/{symbol}/trades', + 'v2/stocks/{symbol}/trades/latest', + ], + }, + }, + }, + 'timeframes': { + '1m': '1min', + '3m': '3min', + '5m': '5min', + '15m': '15min', + '30m': '30min', + '1h': '1H', + '2h': '2H', + '4h': '4H', + '6h': '6H', + '8h': '8H', + '12h': '12H', + '1d': '1D', + '3d': '3D', + '1w': '1W', + '1M': '1M', + }, + 'precisionMode': TICK_SIZE, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + }, + 'fees': { + 'trading': { + 'tierBased': True, + 'percentage': True, + 'maker': self.parse_number('0.0015'), + 'taker': self.parse_number('0.0025'), + 'tiers': { + 'taker': [ + [self.parse_number('0'), self.parse_number('0.0025')], + [self.parse_number('100000'), self.parse_number('0.0022')], + [self.parse_number('500000'), self.parse_number('0.0020')], + [self.parse_number('1000000'), self.parse_number('0.0018')], + [self.parse_number('10000000'), self.parse_number('0.0015')], + [self.parse_number('25000000'), self.parse_number('0.0013')], + [self.parse_number('50000000'), self.parse_number('0.0012')], + [self.parse_number('100000000'), self.parse_number('0.001')], + ], + 'maker': [ + [self.parse_number('0'), self.parse_number('0.0015')], + [self.parse_number('100000'), self.parse_number('0.0012')], + [self.parse_number('500000'), self.parse_number('0.001')], + [self.parse_number('1000000'), self.parse_number('0.0008')], + [self.parse_number('10000000'), self.parse_number('0.0005')], + [self.parse_number('25000000'), self.parse_number('0.0002')], + [self.parse_number('50000000'), self.parse_number('0.0002')], + [self.parse_number('100000000'), self.parse_number('0.00')], + ], + }, + }, + }, + 'headers': { + 'APCA-PARTNER-ID': 'ccxt', + }, + 'options': { + 'defaultExchange': 'CBSE', + 'exchanges': [ + 'CBSE', # Coinbase + 'FTX', # FTXUS + 'GNSS', # Genesis + 'ERSX', # ErisX + ], + 'defaultTimeInForce': 'gtc', # fok, gtc, ioc + 'clientOrderId': 'ccxt_{id}', + }, + 'features': { + 'spot': { + 'sandbox': True, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': { + 'triggerPriceType': { + 'last': True, + 'mark': True, + 'index': True, + }, + 'price': True, + }, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': True, # todo: implementation + 'leverage': False, + 'marketBuyRequiresPrice': False, + 'marketBuyByCost': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 100000, + 'untilDays': 100000, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 500, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': 500, + 'daysBack': 100000, + 'untilDays': 100000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 500, + 'daysBack': 100000, + 'daysBackCanceled': None, + 'untilDays': 100000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 1000, + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'exceptions': { + 'exact': { + 'forbidden.': PermissionDenied, # {"message": "forbidden."} + '40410000': InvalidOrder, # {"code": 40410000, "message": "order is not found."} + '40010001': BadRequest, # {"code":40010001,"message":"invalid order type for crypto order"} + '40110000': PermissionDenied, # {"code": 40110000, "message": "request is not authorized"} + '40310000': InsufficientFunds, # {"available":"0","balance":"0","code":40310000,"message":"insufficient balance for USDT(requested: 221.63, available: 0)","symbol":"USDT"} + '42910000': RateLimitExceeded, # {"code":42910000,"message":"rate limit exceeded"} + }, + 'broad': { + 'Invalid format for parameter': BadRequest, # {"message":"Invalid format for parameter start: error parsing '0' or 2006-01-02 time: parsing time \"0\" as \"2006-01-02\": cannot parse \"0\" as \"2006\""} + 'Invalid symbol': BadSymbol, # {"message":"Invalid symbol(s): BTC/USDdsda does not match ^[A-Z]+/[A-Z]+$"} + }, + }, + }) + + async def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = await self.traderPrivateGetV2Clock(params) + # + # { + # timestamp: '2023-11-22T08:07:57.654738097-05:00', + # is_open: False, + # next_open: '2023-11-22T09:30:00-05:00', + # next_close: '2023-11-22T16:00:00-05:00' + # } + # + timestamp = self.safe_string(response, 'timestamp') + localTime = timestamp[0:23] + jetlagStrStart = len(timestamp) - 6 + jetlagStrEnd = len(timestamp) - 3 + jetlag = timestamp[jetlagStrStart:jetlagStrEnd] + iso = self.parse8601(localTime) - self.parse_to_numeric(jetlag) * 3600 * 1000 + return iso + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for alpaca + + https://docs.alpaca.markets/reference/get-v2-assets + + :param dict [params]: extra parameters specific to the exchange api endpoint + :returns dict[]: an array of objects representing market data + """ + request: dict = { + 'asset_class': 'crypto', + 'status': 'active', + } + assets = await self.traderPrivateGetV2Assets(self.extend(request, params)) + # + # [ + # { + # "id": "c150e086-1e75-44e6-9c2c-093bb1e93139", + # "class": "crypto", + # "exchange": "CRYPTO", + # "symbol": "BTC/USDT", + # "name": "Bitcoin / USD Tether", + # "status": "active", + # "tradable": True, + # "marginable": False, + # "maintenance_margin_requirement": 100, + # "shortable": False, + # "easy_to_borrow": False, + # "fractionable": True, + # "attributes": [], + # "min_order_size": "0.000026873", + # "min_trade_increment": "0.000000001", + # "price_increment": "1" + # } + # ] + # + return self.parse_markets(assets) + + def parse_market(self, asset) -> Market: + # + # { + # "id": "c150e086-1e75-44e6-9c2c-093bb1e93139", + # "class": "crypto", + # "exchange": "CRYPTO", + # "symbol": "BTC/USDT", + # "name": "Bitcoin / USD Tether", + # "status": "active", + # "tradable": True, + # "marginable": False, + # "maintenance_margin_requirement": 101, + # "shortable": False, + # "easy_to_borrow": False, + # "fractionable": True, + # "attributes": [], + # "min_order_size": "0.000026873", + # "min_trade_increment": "0.000000001", + # "price_increment": "1" + # } + # + marketId = self.safe_string(asset, 'symbol') + parts = marketId.split('/') + assetClass = self.safe_string(asset, 'class') + baseId = self.safe_string(parts, 0) + quoteId = self.safe_string(parts, 1) + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + # Us equity markets do not include quote in symbol. + # We can safely coerce us_equity quote to USD + if quote is None and assetClass == 'us_equity': + quote = 'USD' + symbol = base + '/' + quote + status = self.safe_string(asset, 'status') + active = (status == 'active') + minAmount = self.safe_number(asset, 'min_order_size') + amount = self.safe_number(asset, 'min_trade_increment') + price = self.safe_number(asset, 'price_increment') + return { + 'id': marketId, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': None, + 'swap': False, + 'future': False, + 'option': False, + 'active': active, + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': amount, + 'price': price, + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': minAmount, + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + 'info': asset, + } + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://docs.alpaca.markets/reference/cryptotrades + https://docs.alpaca.markets/reference/cryptolatesttrades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.loc]: crypto location, default: us + :param str [params.method]: method, default: marketPublicGetV1beta3CryptoLocTrades + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + marketId = market['id'] + loc = self.safe_string(params, 'loc', 'us') + method = self.safe_string(params, 'method', 'marketPublicGetV1beta3CryptoLocTrades') + request: dict = { + 'symbols': marketId, + 'loc': loc, + } + params = self.omit(params, ['loc', 'method']) + symbolTrades = None + if method == 'marketPublicGetV1beta3CryptoLocTrades': + if since is not None: + request['start'] = self.iso8601(since) + if limit is not None: + request['limit'] = limit + response = await self.marketPublicGetV1beta3CryptoLocTrades(self.extend(request, params)) + # + # { + # "next_page_token": null, + # "trades": { + # "BTC/USD": [ + # { + # "i": 36440704, + # "p": 22625, + # "s": 0.0001, + # "t": "2022-07-21T11:47:31.073391Z", + # "tks": "B" + # } + # ] + # } + # } + # + trades = self.safe_dict(response, 'trades', {}) + symbolTrades = self.safe_list(trades, marketId, []) + elif method == 'marketPublicGetV1beta3CryptoLocLatestTrades': + response = await self.marketPublicGetV1beta3CryptoLocLatestTrades(self.extend(request, params)) + # + # { + # "trades": { + # "BTC/USD": { + # "i": 36440704, + # "p": 22625, + # "s": 0.0001, + # "t": "2022-07-21T11:47:31.073391Z", + # "tks": "B" + # } + # } + # } + # + trades = self.safe_dict(response, 'trades', {}) + symbolTrades = self.safe_dict(trades, marketId, {}) + symbolTrades = [symbolTrades] + else: + raise NotSupported(self.id + ' fetchTrades() does not support ' + method + ', marketPublicGetV1beta3CryptoLocTrades and marketPublicGetV1beta3CryptoLocLatestTrades are supported') + return self.parse_trades(symbolTrades, market, since, limit) + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://docs.alpaca.markets/reference/cryptolatestorderbooks + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.loc]: crypto location, default: us + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + id = market['id'] + loc = self.safe_string(params, 'loc', 'us') + request: dict = { + 'symbols': id, + 'loc': loc, + } + response = await self.marketPublicGetV1beta3CryptoLocLatestOrderbooks(self.extend(request, params)) + # + # { + # "orderbooks":{ + # "BTC/USD":{ + # "a":[ + # { + # "p":22208, + # "s":0.0051 + # }, + # { + # "p":22209, + # "s":0.1123 + # }, + # { + # "p":22210, + # "s":0.2465 + # } + # ], + # "b":[ + # { + # "p":22203, + # "s":0.395 + # }, + # { + # "p":22202, + # "s":0.2465 + # }, + # { + # "p":22201, + # "s":0.6455 + # } + # ], + # "t":"2022-07-19T13:41:55.13210112Z" + # } + # } + # } + # + orderbooks = self.safe_dict(response, 'orderbooks', {}) + rawOrderbook = self.safe_dict(orderbooks, id, {}) + timestamp = self.parse8601(self.safe_string(rawOrderbook, 't')) + return self.parse_order_book(rawOrderbook, market['symbol'], timestamp, 'b', 'a', 'p', 's') + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://docs.alpaca.markets/reference/cryptobars + https://docs.alpaca.markets/reference/cryptolatestbars + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the alpha api endpoint + :param str [params.loc]: crypto location, default: us + :param str [params.method]: method, default: marketPublicGetV1beta3CryptoLocBars + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + marketId = market['id'] + loc = self.safe_string(params, 'loc', 'us') + method = self.safe_string(params, 'method', 'marketPublicGetV1beta3CryptoLocBars') + request: dict = { + 'symbols': marketId, + 'loc': loc, + } + params = self.omit(params, ['loc', 'method']) + ohlcvs = None + if method == 'marketPublicGetV1beta3CryptoLocBars': + if limit is not None: + request['limit'] = limit + if since is not None: + request['start'] = self.yyyymmdd(since) + request['timeframe'] = self.safe_string(self.timeframes, timeframe, timeframe) + response = await self.marketPublicGetV1beta3CryptoLocBars(self.extend(request, params)) + # + # { + # "bars": { + # "BTC/USD": [ + # { + # "c": 22887, + # "h": 22888, + # "l": 22873, + # "n": 11, + # "o": 22883, + # "t": "2022-07-21T05:00:00Z", + # "v": 1.1138, + # "vw": 22883.0155324116 + # }, + # { + # "c": 22895, + # "h": 22895, + # "l": 22884, + # "n": 6, + # "o": 22884, + # "t": "2022-07-21T05:01:00Z", + # "v": 0.001, + # "vw": 22889.5 + # } + # ] + # }, + # "next_page_token": "QlRDL1VTRHxNfDIwMjItMDctMjFUMDU6MDE6MDAuMDAwMDAwMDAwWg==" + # } + # + bars = self.safe_dict(response, 'bars', {}) + ohlcvs = self.safe_list(bars, marketId, []) + elif method == 'marketPublicGetV1beta3CryptoLocLatestBars': + response = await self.marketPublicGetV1beta3CryptoLocLatestBars(self.extend(request, params)) + # + # { + # "bars": { + # "BTC/USD": { + # "c": 22887, + # "h": 22888, + # "l": 22873, + # "n": 11, + # "o": 22883, + # "t": "2022-07-21T05:00:00Z", + # "v": 1.1138, + # "vw": 22883.0155324116 + # } + # } + # } + # + bars = self.safe_dict(response, 'bars', {}) + ohlcvs = self.safe_dict(bars, marketId, {}) + ohlcvs = [ohlcvs] + else: + raise NotSupported(self.id + ' fetchOHLCV() does not support ' + method + ', marketPublicGetV1beta3CryptoLocBars and marketPublicGetV1beta3CryptoLocLatestBars are supported') + return self.parse_ohlcvs(ohlcvs, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # { + # "c":22895, + # "h":22895, + # "l":22884, + # "n":6, + # "o":22884, + # "t":"2022-07-21T05:01:00Z", + # "v":0.001, + # "vw":22889.5 + # } + # + datetime = self.safe_string(ohlcv, 't') + timestamp = self.parse8601(datetime) + return [ + timestamp, # timestamp + self.safe_number(ohlcv, 'o'), # open + self.safe_number(ohlcv, 'h'), # high + self.safe_number(ohlcv, 'l'), # low + self.safe_number(ohlcv, 'c'), # close + self.safe_number(ohlcv, 'v'), # volume + ] + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://docs.alpaca.markets/reference/cryptosnapshots-1 + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.loc]: crypto location, default: us + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbol = self.symbol(symbol) + tickers = await self.fetch_tickers([symbol], params) + return self.safe_dict(tickers, symbol) + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://docs.alpaca.markets/reference/cryptosnapshots-1 + + :param str[] symbols: unified symbols of the markets to fetch tickers for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.loc]: crypto location, default: us + :returns dict: a dictionary of `ticker structures ` + """ + if symbols is None: + raise ArgumentsRequired(self.id + ' fetchTickers() requires a symbols argument') + await self.load_markets() + symbols = self.market_symbols(symbols) + loc = self.safe_string(params, 'loc', 'us') + ids = self.market_ids(symbols) + request = { + 'symbols': ','.join(ids), + 'loc': loc, + } + params = self.omit(params, 'loc') + response = await self.marketPublicGetV1beta3CryptoLocSnapshots(self.extend(request, params)) + # + # { + # "snapshots": { + # "BTC/USD": { + # "dailyBar": { + # "c": 69403.554, + # "h": 69609.6515, + # "l": 69013.26, + # "n": 9, + # "o": 69536.7, + # "t": "2024-11-01T05:00:00Z", + # "v": 0.210809181, + # "vw": 69327.655393908 + # }, + # "latestQuote": { + # "ap": 69424.19, + # "as": 0.68149, + # "bp": 69366.086, + # "bs": 0.68312, + # "t": "2024-11-01T08:31:41.880246926Z" + # }, + # "latestTrade": { + # "i": 5272941104897543146, + # "p": 69416.9, + # "s": 0.014017324, + # "t": "2024-11-01T08:14:28.245088803Z", + # "tks": "B" + # }, + # "minuteBar": { + # "c": 69403.554, + # "h": 69403.554, + # "l": 69399.125, + # "n": 0, + # "o": 69399.125, + # "t": "2024-11-01T08:30:00Z", + # "v": 0, + # "vw": 0 + # }, + # "prevDailyBar": { + # "c": 69515.1415, + # "h": 72668.837, + # "l": 68796.85, + # "n": 129, + # "o": 72258.9, + # "t": "2024-10-31T05:00:00Z", + # "v": 2.217683307, + # "vw": 70782.6811608144 + # } + # }, + # } + # } + # + results = [] + snapshots = self.safe_dict(response, 'snapshots', {}) + marketIds = list(snapshots.keys()) + for i in range(0, len(marketIds)): + marketId = marketIds[i] + market = self.safe_market(marketId) + entry = self.safe_dict(snapshots, marketId) + dailyBar = self.safe_dict(entry, 'dailyBar', {}) + prevDailyBar = self.safe_dict(entry, 'prevDailyBar', {}) + latestQuote = self.safe_dict(entry, 'latestQuote', {}) + latestTrade = self.safe_dict(entry, 'latestTrade', {}) + datetime = self.safe_string(latestQuote, 't') + ticker = self.safe_ticker({ + 'info': entry, + 'symbol': market['symbol'], + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + 'high': self.safe_string(dailyBar, 'h'), + 'low': self.safe_string(dailyBar, 'l'), + 'bid': self.safe_string(latestQuote, 'bp'), + 'bidVolume': self.safe_string(latestQuote, 'bs'), + 'ask': self.safe_string(latestQuote, 'ap'), + 'askVolume': self.safe_string(latestQuote, 'as'), + 'vwap': self.safe_string(dailyBar, 'vw'), + 'open': self.safe_string(dailyBar, 'o'), + 'close': self.safe_string(dailyBar, 'c'), + 'last': self.safe_string(latestTrade, 'p'), + 'previousClose': self.safe_string(prevDailyBar, 'c'), + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': self.safe_string(dailyBar, 'v'), + 'quoteVolume': self.safe_string(dailyBar, 'n'), + }, market) + results.append(ticker) + return self.filter_by_array(results, 'symbol', symbols) + + def generate_client_order_id(self, params): + clientOrderIdprefix = self.safe_string(self.options, 'clientOrderId') + uuid = self.uuid() + parts = uuid.split('-') + random_id = ''.join(parts) + defaultClientId = self.implode_params(clientOrderIdprefix, {'id': random_id}) + clientOrderId = self.safe_string(params, 'clientOrderId', defaultClientId) + return clientOrderId + + async def create_market_order_with_cost(self, symbol: str, side: OrderSide, cost: float, params={}): + """ + create a market order by providing the symbol, side and cost + + https://docs.alpaca.markets/reference/postorder + + :param str symbol: unified symbol of the market to create an order in + :param str side: 'buy' or 'sell' + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + req = { + 'cost': cost, + } + return await self.create_order(symbol, 'market', side, 0, None, self.extend(req, params)) + + async def create_market_buy_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market buy order by providing the symbol and cost + + https://docs.alpaca.markets/reference/postorder + + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + req = { + 'cost': cost, + } + return await self.create_order(symbol, 'market', 'buy', 0, None, self.extend(req, params)) + + async def create_market_sell_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market sell order by providing the symbol and cost + + https://docs.alpaca.markets/reference/postorder + + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + req = { + 'cost': cost, + } + return await self.create_order(symbol, 'market', 'sell', cost, None, self.extend(req, params)) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://docs.alpaca.markets/reference/postorder + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market', 'limit' or 'stop_limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: The price at which a trigger order is triggered at + :param float [params.cost]: *market orders only* the cost of the order in units of the quote currency + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + id = market['id'] + request: dict = { + 'symbol': id, + 'side': side, + 'type': type, # market, limit, stop_limit + } + triggerPrice = self.safe_string_n(params, ['triggerPrice', 'stop_price']) + if triggerPrice is not None: + newType: str + if type.find('limit') >= 0: + newType = 'stop_limit' + else: + raise NotSupported(self.id + ' createOrder() does not support stop orders for ' + type + ' orders, only stop_limit orders are supported') + request['stop_price'] = self.price_to_precision(symbol, triggerPrice) + request['type'] = newType + if type.find('limit') >= 0: + request['limit_price'] = self.price_to_precision(symbol, price) + cost = self.safe_string(params, 'cost') + if cost is not None: + params = self.omit(params, 'cost') + request['notional'] = self.cost_to_precision(symbol, cost) + else: + request['qty'] = self.amount_to_precision(symbol, amount) + defaultTIF = self.safe_string(self.options, 'defaultTimeInForce') + request['time_in_force'] = self.safe_string(params, 'timeInForce', defaultTIF) + params = self.omit(params, ['timeInForce', 'triggerPrice']) + request['client_order_id'] = self.generate_client_order_id(params) + params = self.omit(params, ['clientOrderId']) + order = await self.traderPrivatePostV2Orders(self.extend(request, params)) + # + # { + # "id": "61e69015-8549-4bfd-b9c3-01e75843f47d", + # "client_order_id": "eb9e2aaa-f71a-4f51-b5b4-52a6c565dad4", + # "created_at": "2021-03-16T18:38:01.942282Z", + # "updated_at": "2021-03-16T18:38:01.942282Z", + # "submitted_at": "2021-03-16T18:38:01.937734Z", + # "filled_at": null, + # "expired_at": null, + # "canceled_at": null, + # "failed_at": null, + # "replaced_at": null, + # "replaced_by": null, + # "replaces": null, + # "asset_id": "b0b6dd9d-8b9b-48a9-ba46-b9d54906e415", + # "symbol": "AAPL", + # "asset_class": "us_equity", + # "notional": "500", + # "qty": null, + # "filled_qty": "0", + # "filled_avg_price": null, + # "order_class": "", + # "order_type": "market", + # "type": "market", + # "side": "buy", + # "time_in_force": "day", + # "limit_price": null, + # "stop_price": null, + # "status": "accepted", + # "extended_hours": False, + # "legs": null, + # "trail_percent": null, + # "trail_price": null, + # "hwm": null + # } + # + return self.parse_order(order, market) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://docs.alpaca.markets/reference/deleteorderbyorderid + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + request: dict = { + 'order_id': id, + } + response = await self.traderPrivateDeleteV2OrdersOrderId(self.extend(request, params)) + # + # { + # "code": 40410000, + # "message": "order is not found." + # } + # + return self.parse_order(response) + + async def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders in a market + + https://docs.alpaca.markets/reference/deleteallorders + + :param str symbol: alpaca cancelAllOrders cannot setting symbol, it will cancel all open orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + response = await self.traderPrivateDeleteV2Orders(params) + if isinstance(response, list): + return self.parse_orders(response, None) + else: + return [ + self.safe_order({ + 'info': response, + }), + ] + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://docs.alpaca.markets/reference/getorderbyorderid + + :param str id: the order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + request: dict = { + 'order_id': id, + } + order = await self.traderPrivateGetV2OrdersOrderId(self.extend(request, params)) + marketId = self.safe_string(order, 'symbol') + market = self.safe_market(marketId) + return self.parse_order(order, market) + + async def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://docs.alpaca.markets/reference/getallorders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch orders for + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + request: dict = { + 'status': 'all', + } + market = None + if symbol is not None: + market = self.market(symbol) + request['symbols'] = market['id'] + until = self.safe_integer(params, 'until') + if until is not None: + params = self.omit(params, 'until') + request['endTime'] = self.iso8601(until) + if since is not None: + request['after'] = self.iso8601(since) + if limit is not None: + request['limit'] = limit + response = await self.traderPrivateGetV2Orders(self.extend(request, params)) + # + # [ + # { + # "id": "cbaf12d7-69b8-49c0-a31b-b46af35c755c", + # "client_order_id": "ccxt_b36156ae6fd44d098ac9c179bab33efd", + # "created_at": "2023-11-17T04:21:42.234579Z", + # "updated_at": "2023-11-17T04:22:34.442765Z", + # "submitted_at": "2023-11-17T04:21:42.233357Z", + # "filled_at": null, + # "expired_at": null, + # "canceled_at": "2023-11-17T04:22:34.399019Z", + # "failed_at": null, + # "replaced_at": null, + # "replaced_by": null, + # "replaces": null, + # "asset_id": "77c6f47f-0939-4b23-b41e-47b4469c4bc8", + # "symbol": "LTC/USDT", + # "asset_class": "crypto", + # "notional": null, + # "qty": "0.001", + # "filled_qty": "0", + # "filled_avg_price": null, + # "order_class": "", + # "order_type": "limit", + # "type": "limit", + # "side": "sell", + # "time_in_force": "gtc", + # "limit_price": "1000", + # "stop_price": null, + # "status": "canceled", + # "extended_hours": False, + # "legs": null, + # "trail_percent": null, + # "trail_price": null, + # "hwm": null, + # "subtag": null, + # "source": "access_key" + # } + # ] + # + return self.parse_orders(response, market, since, limit) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://docs.alpaca.markets/reference/getallorders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch orders for + :returns Order[]: a list of `order structures ` + """ + request: dict = { + 'status': 'open', + } + return await self.fetch_orders(symbol, since, limit, self.extend(request, params)) + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://docs.alpaca.markets/reference/getallorders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch orders for + :returns Order[]: a list of `order structures ` + """ + request: dict = { + 'status': 'closed', + } + return await self.fetch_orders(symbol, since, limit, self.extend(request, params)) + + async def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + """ + edit a trade order + + https://docs.alpaca.markets/reference/patchorderbyorderid-1 + + :param str id: order id + :param str [symbol]: unified symbol of the market to create an order in + :param str [type]: 'market', 'limit' or 'stop_limit' + :param str [side]: 'buy' or 'sell' + :param float [amount]: how much of the currency you want to trade in units of the base currency + :param float [price]: the price for the order, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.triggerPrice]: the price to trigger a stop order + :param str [params.timeInForce]: for crypto trading either 'gtc' or 'ioc' can be used + :param str [params.clientOrderId]: a unique identifier for the order, automatically generated if not sent + :returns dict: an `order structure ` + """ + await self.load_markets() + request: dict = { + 'order_id': id, + } + market = None + if symbol is not None: + market = self.market(symbol) + if amount is not None: + request['qty'] = self.amount_to_precision(symbol, amount) + triggerPrice = self.safe_string_n(params, ['triggerPrice', 'stop_price']) + if triggerPrice is not None: + request['stop_price'] = self.price_to_precision(symbol, triggerPrice) + params = self.omit(params, 'triggerPrice') + if price is not None: + request['limit_price'] = self.price_to_precision(symbol, price) + timeInForce = None + timeInForce, params = self.handle_option_and_params_2(params, 'editOrder', 'timeInForce', 'defaultTimeInForce') + if timeInForce is not None: + request['time_in_force'] = timeInForce + request['client_order_id'] = self.generate_client_order_id(params) + params = self.omit(params, ['clientOrderId']) + response = await self.traderPrivatePatchV2OrdersOrderId(self.extend(request, params)) + return self.parse_order(response, market) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # { + # "id":"6ecfcc34-4bed-4b53-83ba-c564aa832a81", + # "client_order_id":"ccxt_1c6ceab0b5e84727b2f1c0394ba17560", + # "created_at":"2022-06-14T13:59:30.224037068Z", + # "updated_at":"2022-06-14T13:59:30.224037068Z", + # "submitted_at":"2022-06-14T13:59:30.221856828Z", + # "filled_at":null, + # "expired_at":null, + # "canceled_at":null, + # "failed_at":null, + # "replaced_at":null, + # "replaced_by":null, + # "replaces":null, + # "asset_id":"64bbff51-59d6-4b3c-9351-13ad85e3c752", + # "symbol":"BTCUSD", + # "asset_class":"crypto", + # "notional":null, + # "qty":"0.01", + # "filled_qty":"0", + # "filled_avg_price":null, + # "order_class":"", + # "order_type":"limit", + # "type":"limit", + # "side":"buy", + # "time_in_force":"day", + # "limit_price":"14000", + # "stop_price":null, + # "status":"accepted", + # "extended_hours":false, + # "legs":null, + # "trail_percent":null, + # "trail_price":null, + # "hwm":null, + # "commission":"0.42", + # "source":null + # } + # + marketId = self.safe_string(order, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + alpacaStatus = self.safe_string(order, 'status') + status = self.parse_order_status(alpacaStatus) + feeValue = self.safe_string(order, 'commission') + fee = None + if feeValue is not None: + fee = { + 'cost': feeValue, + 'currency': 'USD', + } + orderType = self.safe_string(order, 'order_type') + if orderType is not None: + if orderType.find('limit') >= 0: + # might be limit or stop-limit + orderType = 'limit' + datetime = self.safe_string(order, 'submitted_at') + timestamp = self.parse8601(datetime) + return self.safe_order({ + 'id': self.safe_string(order, 'id'), + 'clientOrderId': self.safe_string(order, 'client_order_id'), + 'timestamp': timestamp, + 'datetime': datetime, + 'lastTradeTimeStamp': None, + 'status': status, + 'symbol': symbol, + 'type': orderType, + 'timeInForce': self.parse_time_in_force(self.safe_string(order, 'time_in_force')), + 'postOnly': None, + 'side': self.safe_string(order, 'side'), + 'price': self.safe_number(order, 'limit_price'), + 'triggerPrice': self.safe_number(order, 'stop_price'), + 'cost': None, + 'average': self.safe_number(order, 'filled_avg_price'), + 'amount': self.safe_number(order, 'qty'), + 'filled': self.safe_number(order, 'filled_qty'), + 'remaining': None, + 'trades': None, + 'fee': fee, + 'info': order, + }, market) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'pending_new': 'open', + 'accepted': 'open', + 'new': 'open', + 'partially_filled': 'open', + 'activated': 'open', + 'filled': 'closed', + } + return self.safe_string(statuses, status, status) + + def parse_time_in_force(self, timeInForce: Str): + timeInForces: dict = { + 'day': 'Day', + } + return self.safe_string(timeInForces, timeInForce, timeInForce) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://docs.alpaca.markets/reference/getaccountactivitiesbyactivitytype-1 + + :param str [symbol]: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trade structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch trades for + :param str [params.page_token]: page_token - used for paging + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = None + request: dict = { + 'activity_type': 'FILL', + } + if symbol is not None: + market = self.market(symbol) + until = self.safe_integer(params, 'until') + if until is not None: + params = self.omit(params, 'until') + request['until'] = self.iso8601(until) + if since is not None: + request['after'] = self.iso8601(since) + if limit is not None: + request['page_size'] = limit + request, params = self.handle_until_option('until', request, params) + response = await self.traderPrivateGetV2AccountActivitiesActivityType(self.extend(request, params)) + # + # [ + # { + # "id": "20221228071929579::ca2aafd0-1270-4b56-b0a9-85423b4a07c8", + # "activity_type": "FILL", + # "transaction_time": "2022-12-28T12:19:29.579352Z", + # "type": "fill", + # "price": "67.31", + # "qty": "0.07", + # "side": "sell", + # "symbol": "LTC/USD", + # "leaves_qty": "0", + # "order_id": "82eebcf7-6e66-4b7e-93f8-be0df0e4f12e", + # "cum_qty": "0.07", + # "order_status": "filled", + # "swap_rate": "1" + # }, + # ] + # + return self.parse_trades(response, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades + # + # { + # "t":"2022-06-14T05:00:00.027869Z", + # "x":"CBSE", + # "p":"21942.15", + # "s":"0.0001", + # "tks":"S", + # "i":"355681339" + # } + # + # fetchMyTrades + # + # { + # "id": "20221228071929579::ca2aafd0-1270-4b56-b0a9-85423b4a07c8", + # "activity_type": "FILL", + # "transaction_time": "2022-12-28T12:19:29.579352Z", + # "type": "fill", + # "price": "67.31", + # "qty": "0.07", + # "side": "sell", + # "symbol": "LTC/USD", + # "leaves_qty": "0", + # "order_id": "82eebcf7-6e66-4b7e-93f8-be0df0e4f12e", + # "cum_qty": "0.07", + # "order_status": "filled", + # "swap_rate": "1" + # }, + # + marketId = self.safe_string_2(trade, 'S', 'symbol') + symbol = self.safe_symbol(marketId, market) + datetime = self.safe_string_2(trade, 't', 'transaction_time') + timestamp = self.parse8601(datetime) + alpacaSide = self.safe_string(trade, 'tks') + side = self.safe_string(trade, 'side') + if alpacaSide == 'B': + side = 'buy' + elif alpacaSide == 'S': + side = 'sell' + priceString = self.safe_string_2(trade, 'p', 'price') + amountString = self.safe_string_2(trade, 's', 'qty') + return self.safe_trade({ + 'info': trade, + 'id': self.safe_string_2(trade, 'i', 'id'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'order': self.safe_string(trade, 'order_id'), + 'type': None, + 'side': side, + 'takerOrMaker': 'taker', + 'price': priceString, + 'amount': amountString, + 'cost': None, + 'fee': None, + }, market) + + async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://docs.alpaca.markets/reference/listcryptofundingwallets + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'asset': currency['id'], + } + response = await self.traderPrivateGetV2Wallets(self.extend(request, params)) + # + # { + # "asset_id": "4fa30c85-77b7-4cbc-92dd-7b7513640aad", + # "address": "bc1q2fpskfnwem3uq9z8660e4z6pfv7aqfamysk75r", + # "created_at": "2024-11-03T07:30:05.609976344Z" + # } + # + return self.parse_deposit_address(response, currency) + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + # + # { + # "asset_id": "4fa30c85-77b7-4cbc-92dd-7b7513640aad", + # "address": "bc1q2fpskfnwem3uq9z8660e4z6pfv7aqfamysk75r", + # "created_at": "2024-11-03T07:30:05.609976344Z" + # } + # + parsedCurrency = None + if currency is not None: + parsedCurrency = currency['id'] + return { + 'info': depositAddress, + 'currency': parsedCurrency, + 'network': None, + 'address': self.safe_string(depositAddress, 'address'), + 'tag': None, + } + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://docs.alpaca.markets/reference/createcryptotransferforaccount + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: a memo for the transaction + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.check_address(address) + await self.load_markets() + currency = self.currency(code) + if tag: + address = address + ':' + tag + request: dict = { + 'asset': currency['id'], + 'address': address, + 'amount': self.number_to_string(amount), + } + response = await self.traderPrivatePostV2WalletsTransfers(self.extend(request, params)) + # + # { + # "id": "e27b70a6-5610-40d7-8468-a516a284b776", + # "tx_hash": null, + # "direction": "OUTGOING", + # "amount": "20", + # "usd_value": "19.99856", + # "chain": "ETH", + # "asset": "USDT", + # "from_address": "0x123930E4dCA196E070d39B60c644C8Aae02f23", + # "to_address": "0x1232c0925196e4dcf05945f67f690153190fbaab", + # "status": "PROCESSING", + # "created_at": "2024-11-07T02:39:01.775495Z", + # "network_fee": "4", + # "fees": "0.1" + # } + # + return self.parse_transaction(response, currency) + + async def fetch_transactions_helper(self, type, code, since, limit, params): + await self.load_markets() + currency = None + if code is not None: + currency = self.currency(code) + response = await self.traderPrivateGetV2WalletsTransfers(params) + # + # { + # "id": "e27b70a6-5610-40d7-8468-a516a284b776", + # "tx_hash": null, + # "direction": "OUTGOING", + # "amount": "20", + # "usd_value": "19.99856", + # "chain": "ETH", + # "asset": "USDT", + # "from_address": "0x123930E4dCA196E070d39B60c644C8Aae02f23", + # "to_address": "0x1232c0925196e4dcf05945f67f690153190fbaab", + # "status": "PROCESSING", + # "created_at": "2024-11-07T02:39:01.775495Z", + # "network_fee": "4", + # "fees": "0.1" + # } + # + results = [] + for i in range(0, len(response)): + entry = response[i] + direction = self.safe_string(entry, 'direction') + if direction == type: + results.append(entry) + elif type == 'BOTH': + results.append(entry) + return self.parse_transactions(results, currency, since, limit, params) + + async def fetch_deposits_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch history of deposits and withdrawals + + https://docs.alpaca.markets/reference/listcryptofundingtransfers + + :param str [code]: unified currency code for the currency of the deposit/withdrawals, default is None + :param int [since]: timestamp in ms of the earliest deposit/withdrawal, default is None + :param int [limit]: max number of deposit/withdrawals to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `transaction structure ` + """ + return await self.fetch_transactions_helper('BOTH', code, since, limit, params) + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://docs.alpaca.markets/reference/listcryptofundingtransfers + + :param str [code]: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposit structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + return await self.fetch_transactions_helper('INCOMING', code, since, limit, params) + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://docs.alpaca.markets/reference/listcryptofundingtransfers + + :param str [code]: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawal structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + return await self.fetch_transactions_helper('OUTGOING', code, since, limit, params) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # { + # "id": "e27b70a6-5610-40d7-8468-a516a284b776", + # "tx_hash": null, + # "direction": "OUTGOING", + # "amount": "20", + # "usd_value": "19.99856", + # "chain": "ETH", + # "asset": "USDT", + # "from_address": "0x123930E4dCA196E070d39B60c644C8Aae02f23", + # "to_address": "0x1232c0925196e4dcf05945f67f690153190fbaab", + # "status": "PROCESSING", + # "created_at": "2024-11-07T02:39:01.775495Z", + # "network_fee": "4", + # "fees": "0.1" + # } + # + datetime = self.safe_string(transaction, 'created_at') + currencyId = self.safe_string(transaction, 'asset') + code = self.safe_currency_code(currencyId, currency) + fees = self.safe_string(transaction, 'fees') + networkFee = self.safe_string(transaction, 'network_fee') + totalFee = Precise.string_add(fees, networkFee) + fee = { + 'cost': self.parse_number(totalFee), + 'currency': code, + } + return { + 'info': transaction, + 'id': self.safe_string(transaction, 'id'), + 'txid': self.safe_string(transaction, 'tx_hash'), + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + 'network': self.safe_string(transaction, 'chain'), + 'address': self.safe_string(transaction, 'to_address'), + 'addressTo': self.safe_string(transaction, 'to_address'), + 'addressFrom': self.safe_string(transaction, 'from_address'), + 'tag': None, + 'tagTo': None, + 'tagFrom': None, + 'type': self.parse_transaction_type(self.safe_string(transaction, 'direction')), + 'amount': self.safe_number(transaction, 'amount'), + 'currency': code, + 'status': self.parse_transaction_status(self.safe_string(transaction, 'status')), + 'updated': None, + 'fee': fee, + 'comment': None, + 'internal': None, + } + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'PROCESSING': 'pending', + 'FAILED': 'failed', + 'COMPLETE': 'ok', + } + return self.safe_string(statuses, status, status) + + def parse_transaction_type(self, type): + types: dict = { + 'INCOMING': 'deposit', + 'OUTGOING': 'withdrawal', + } + return self.safe_string(types, type, type) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://docs.alpaca.markets/reference/getaccount-1 + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + response = await self.traderPrivateGetV2Account(params) + # + # { + # "id": "43a01bde-4eb1-64fssc26adb5", + # "admin_configurations": { + # "allow_instant_ach": True, + # "max_margin_multiplier": "4" + # }, + # "user_configurations": { + # "fractional_trading": True, + # "max_margin_multiplier": "4" + # }, + # "account_number": "744873727", + # "status": "ACTIVE", + # "crypto_status": "ACTIVE", + # "currency": "USD", + # "buying_power": "5.92", + # "regt_buying_power": "5.92", + # "daytrading_buying_power": "0", + # "effective_buying_power": "5.92", + # "non_marginable_buying_power": "5.92", + # "bod_dtbp": "0", + # "cash": "5.92", + # "accrued_fees": "0", + # "portfolio_value": "48.6", + # "pattern_day_trader": False, + # "trading_blocked": False, + # "transfers_blocked": False, + # "account_blocked": False, + # "created_at": "2022-06-13T14:59:18.318096Z", + # "trade_suspended_by_user": False, + # "multiplier": "1", + # "shorting_enabled": False, + # "equity": "48.6", + # "last_equity": "48.8014266", + # "long_market_value": "42.68", + # "short_market_value": "0", + # "position_market_value": "42.68", + # "initial_margin": "0", + # "maintenance_margin": "0", + # "last_maintenance_margin": "0", + # "sma": "5.92", + # "daytrade_count": 0, + # "balance_asof": "2024-12-10", + # "crypto_tier": 1, + # "intraday_adjustments": "0", + # "pending_reg_taf_fees": "0" + # } + # + return self.parse_balance(response) + + def parse_balance(self, response) -> Balances: + result: dict = {'info': response} + account = self.account() + currencyId = self.safe_string(response, 'currency') + code = self.safe_currency_code(currencyId) + account['free'] = self.safe_string(response, 'cash') + account['total'] = self.safe_string(response, 'equity') + result[code] = account + return self.safe_balance(result) + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + endpoint = '/' + self.implode_params(path, params) + url = self.implode_hostname(self.urls['api'][api[0]]) + headers = headers if (headers is not None) else {} + if api[1] == 'private': + self.check_required_credentials() + headers['APCA-API-KEY-ID'] = self.apiKey + headers['APCA-API-SECRET-KEY'] = self.secret + query = self.omit(params, self.extract_params(path)) + if query: + if (method == 'GET') or (method == 'DELETE'): + endpoint += '?' + self.urlencode(query) + else: + body = self.json(query) + headers['Content-Type'] = 'application/json' + url = url + endpoint + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None # default error handler + # { + # "code": 40110000, + # "message": "request is not authorized" + # } + feedback = self.id + ' ' + body + errorCode = self.safe_string(response, 'code') + if code is not None: + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + message = self.safe_value(response, 'message', None) + if message is not None: + self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + raise ExchangeError(feedback) + return None diff --git a/ccxt/async_support/apex.py b/ccxt/async_support/apex.py new file mode 100644 index 0000000..025b429 --- /dev/null +++ b/ccxt/async_support/apex.py @@ -0,0 +1,1898 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.apex import ImplicitAPI +import hashlib +import math +from ccxt.base.types import Account, Any, Balances, Currencies, Currency, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, Trade, MarketInterface, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.decimal_to_precision import TRUNCATE +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class apex(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(apex, self).describe(), { + 'id': 'apex', + 'name': 'Apex', + 'countries': [], + 'version': 'v3', + 'rateLimit': 20, # 600 requests per minute, 10 request per second + 'certified': False, + 'pro': True, + 'dex': True, + 'has': { + 'CORS': None, + 'spot': False, + 'margin': False, + 'swap': True, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelAllOrders': True, + 'cancelAllOrdersAfter': False, + 'cancelOrder': True, + 'cancelOrders': False, + 'cancelOrdersForSymbols': False, + 'closeAllPositions': False, + 'closePosition': False, + 'createMarketBuyOrderWithCost': False, + 'createMarketOrderWithCost': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createOrders': False, + 'createPostOnlyOrder': True, + 'createReduceOnlyOrder': True, + 'createStopOrder': True, + 'createTriggerOrder': True, + 'editOrder': False, + 'fetchAccounts': True, + 'fetchAllGreeks': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchCanceledAndClosedOrders': False, + 'fetchCanceledOrders': False, + 'fetchClosedOrders': False, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': False, + 'fetchDepositAddresses': False, + 'fetchDeposits': False, + 'fetchDepositWithdrawFee': False, + 'fetchDepositWithdrawFees': False, + 'fetchFundingHistory': True, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': True, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchLedger': False, + 'fetchLeverage': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchMarginMode': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMyLiquidations': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': True, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchOrderTrades': True, + 'fetchPosition': False, + 'fetchPositionMode': False, + 'fetchPositions': True, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': False, + 'fetchTransfer': True, + 'fetchTransfers': True, + 'fetchVolatilityHistory': False, + 'fetchWithdrawal': False, + 'fetchWithdrawals': False, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'sandbox': True, + 'setLeverage': True, + 'setMarginMode': False, + 'setPositionMode': False, + 'transfer': False, + 'withdraw': False, + }, + 'timeframes': { + '1m': '1', + '5m': '5', + '15m': '15', + '30m': '30', + '1h': '60', + '2h': '120', + '4h': '240', + '6h': '360', + '12h': '720', + '1d': 'D', + '1w': 'W', + '1M': 'M', + }, + 'hostname': 'omni.apex.exchange', + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/fef8f2f7-4265-46aa-965e-33a91881cb00', + 'api': { + 'public': 'https://{hostname}/api', + 'private': 'https://{hostname}/api', + }, + 'test': { + 'public': 'https://testnet.omni.apex.exchange/api', + 'private': 'https://testnet.omni.apex.exchange/api', + }, + 'www': 'https://apex.exchange/', + 'doc': 'https://api-docs.pro.apex.exchange', + 'fees': 'https://apex-pro.gitbook.io/apex-pro/apex-omni-live-now/trading-perpetual-contracts/trading-fees', + 'referral': 'https://omni.apex.exchange/trade', + }, + 'api': { + 'public': { + 'get': { + 'v3/symbols': 1, + 'v3/history-funding': 1, + 'v3/ticker': 1, + 'v3/klines': 1, + 'v3/trades': 1, + 'v3/depth': 1, + 'v3/time': 1, + 'v3/data/all-ticker-info': 1, + }, + }, + 'private': { + 'get': { + 'v3/account': 1, + 'v3/account-balance': 1, + 'v3/fills': 1, + 'v3/order-fills': 1, + 'v3/order': 1, + 'v3/history-orders': 1, + 'v3/order-by-client-order-id': 1, + 'v3/funding': 1, + 'v3/historical-pnl': 1, + 'v3/open-orders': 1, + 'v3/transfers': 1, + 'v3/transfer': 1, + }, + 'post': { + 'v3/delete-open-orders': 1, + 'v3/delete-client-order-id': 1, + 'v3/delete-order': 1, + 'v3/order': 1, + 'v3/set-initial-margin-rate': 1, + 'v3/transfer-out': 1, + 'v3/contract-transfer-out': 1, + }, + }, + }, + 'httpExceptions': { + '403': RateLimitExceeded, # Forbidden -- You request too many times + }, + 'exceptions': { + # Uncodumented explanation of error strings: + # - oc_diff: order cost needed to place self order + # - new_oc: total order cost of open orders including the order you are trying to open + # - ob: order balance - the total cost of current open orders + # - ab: available balance + 'exact': { + '20006': 'apikey sign error', # apikey sign error + '20016': 'request para error', # apikey sign error + '10001': BadRequest, + }, + 'broad': { + 'ORDER_PRICE_MUST_GREETER_ZERO': InvalidOrder, + 'ORDER_POSSIBLE_LEAD_TO_ACCOUNT_LIQUIDATED': InvalidOrder, + 'ORDER_WITH_THIS_PRICE_CANNOT_REDUCE_POSITION_ONLY': InvalidOrder, + }, + }, + 'fees': { + 'swap': { + 'taker': self.parse_number('0.0005'), + 'maker': self.parse_number('0.0002'), + }, + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + 'walletAddress': False, + 'privateKey': False, + 'password': True, + }, + 'precisionMode': TICK_SIZE, + 'commonCurrencies': {}, + 'options': { + 'defaultType': 'swap', + 'defaultSlippage': 0.05, + 'brokerId': '6956', + }, + 'features': { + 'default': { + 'sandbox': True, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': True, + }, + 'hedged': False, + 'selfTradePrevention': False, + 'trailing': True, # todo unify + 'leverage': False, + 'marketBuyByCost': False, + 'marketBuyRequiresPrice': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 500, + 'daysBack': 100000, + 'untilDays': 100000, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 100000, + 'untilDays': 100000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchClosedOrders': None, + 'fetchOHLCV': { + 'limit': 200, + }, + }, + 'swap': { + 'linear': { + 'extends': 'default', + }, + 'inverse': None, + }, + }, + }) + + async def fetch_time(self, params={}): + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://api-docs.pro.apex.exchange/#publicapi-v3-for-omni-get-system-time-v3 + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = await self.publicGetV3Time(params) + data = self.safe_dict(response, 'data', {}) + # + # { + # "data": { + # "time": 1738837534454 + # } + # } + return self.safe_integer(data, 'time') + + def parse_balance(self, response) -> Balances: + # + # { + # "totalEquityValue": "100.000000", + # "availableBalance": "100.000000", + # "initialMargin": "100.000000", + # "maintenanceMargin": "100.000000", + # "symbolToOraclePrice": { + # "BTC-USDC": { + # "oraclePrice": "20000", + # "createdTime": 124566 + # } + # } + # } + # + timestamp = self.milliseconds() + result: dict = { + 'info': response, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + code = 'USDT' + account = self.account() + account['free'] = self.safe_string(response, 'availableBalance') + account['total'] = self.safe_string(response, 'totalEquityValue') + result[code] = account + return self.safe_balance(result) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for account info + + https://api-docs.pro.apex.exchange/#privateapi-v3-for-omni-get-retrieve-user-account-balance + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + response = await self.privateGetV3AccountBalance(params) + data = self.safe_dict(response, 'data', {}) + return self.parse_balance(data) + + def parse_account(self, account: dict) -> Account: + accountId = self.safe_string(account, 'id', '0') + return { + 'id': accountId, + 'type': None, + 'code': None, + 'info': account, + } + + async def fetch_account(self, params={}) -> Account: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://api-docs.pro.apex.exchange/#privateapi-v3-for-omni-get-retrieve-user-account-data + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + response = await self.privateGetV3Account(params) + data = self.safe_dict(response, 'data', {}) + return self.parse_account(data) + + async def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://api-docs.pro.apex.exchange/#publicapi-v3-for-omni-get-all-config-data-v3 + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = await self.publicGetV3Symbols(params) + data = self.safe_dict(response, 'data', {}) + spotConfig = self.safe_dict(data, 'spotConfig', {}) + multiChain = self.safe_dict(spotConfig, 'multiChain', {}) + # "spotConfig": { + # "assets": [ + # { + # "tokenId": "141", + # "token": "USDT", + # "displayName": "Tether USD Coin", + # "decimals": 18, + # "showStep": "0.01", + # "iconUrl": "https://static-pro.apex.exchange/chains/chain_tokens/Ethereum/Ethereum_USDT.svg", + # "l2WithdrawFee": "0", + # "enableCollateral": True, + # "enableCrossCollateral": False, + # "crossCollateralDiscountRate": null, + # "isGray": False + # } + # ], + # "multiChain": { + # "chains": [ + # { + # "chain": "Arbitrum One", + # "chainId": "9", + # "chainType": "0", + # "l1ChainId": "42161", + # "chainIconUrl": "https://static-pro.apex.exchange/chains/chain_logos/Arbitrum.svg", + # "contractAddress": "0x3169844a120c0f517b4eb4a750c08d8518c8466a", + # "swapContractAddress": "0x9e07b6Aef1bbD9E513fc2Eb8873e311E80B4f855", + # "stopDeposit": False, + # "feeLess": False, + # "gasLess": False, + # "gasToken": "ETH", + # "dynamicFee": True, + # "gasTokenDecimals": 18, + # "feeGasLimit": 300000, + # "blockTimeSeconds": 2, + # "rpcUrl": "https://arb.pro.apex.exchange", + # "minSwapUsdtAmount": "", + # "maxSwapUsdtAmount": "", + # "webRpcUrl": "https://arb.pro.apex.exchange", + # "webTxUrl": "https://arbiscan.io/tx/", + # "backupRpcUrl": "https://arb-mainnet.g.alchemy.com/v2/rGlYUbRHtUav5mfeThCPtsV9GLPt2Xq5", + # "txConfirm": 20, + # "withdrawGasFeeLess": False, + # "tokens": [ + # { + # "decimals": 6, + # "iconUrl": "https://static-pro.apex.exchange/chains/chain_tokens/Arbitrum/Arbitrum_USDT.svg", + # "token": "USDT", + # "tokenAddress": "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", + # "pullOff": False, + # "withdrawEnable": True, + # "slippage": "", + # "isDefaultToken": False, + # "displayToken": "USDT", + # "needResetApproval": True, + # "minFee": "2", + # "maxFee": "40", + # "feeRate": "0.0001", + # "maxWithdraw": "", + # "minDeposit": "", + # "minWithdraw": "", + # "maxFastWithdrawAmount": "40000", + # "minFastWithdrawAmount": "1", + # "isGray": False + # }, + # { + # "decimals": 6, + # "iconUrl": "https://static-pro.apex.exchange/chains/chain_tokens/Arbitrum/Arbitrum_USDC.svg", + # "token": "USDC", + # "tokenAddress": "0xaf88d065e77c8cc2239327c5edb3a432268e5831", + # "pullOff": False, + # "withdrawEnable": True, + # "slippage": "", + # "isDefaultToken": False, + # "displayToken": "USDC", + # "needResetApproval": True, + # "minFee": "2", + # "maxFee": "20", + # "feeRate": "0.0001", + # "maxWithdraw": "", + # "minDeposit": "", + # "minWithdraw": "", + # "maxFastWithdrawAmount": "1", + # "minFastWithdrawAmount": "1", + # "isGray": False + # } + # ] + # } + # ] + # } + rows = self.safe_list(spotConfig, 'assets', []) + chains = self.safe_list(multiChain, 'chains', []) + result: dict = {} + for i in range(0, len(rows)): + currency = rows[i] + currencyId = self.safe_string(currency, 'token') + code = self.safe_currency_code(currencyId) + name = self.safe_string(currency, 'displayName') + networks: dict = {} + for j in range(0, len(chains)): + chain = chains[j] + tokens = self.safe_list(chain, 'tokens', []) + for f in range(0, len(tokens)): + token = tokens[f] + tokenName = self.safe_string(token, 'token') + if tokenName == currencyId: + networkId = self.safe_string(chain, 'chainId') + networkCode = self.network_id_to_code(networkId) + networks[networkCode] = { + 'info': chain, + 'id': networkId, + 'network': networkCode, + 'active': None, + 'deposit': not self.safe_bool(chain, 'depositDisable'), + 'withdraw': self.safe_bool(token, 'withdrawEnable'), + 'fee': self.safe_number(token, 'minFee'), + 'precision': self.parse_number(self.parse_precision(self.safe_string(token, 'decimals'))), + 'limits': { + 'withdraw': { + 'min': self.safe_number(token, 'minWithdraw'), + 'max': None, + }, + 'deposit': { + 'min': self.safe_number(chain, 'minDeposit'), + 'max': None, + }, + }, + } + networkKeys = list(networks.keys()) + networksLength = len(networkKeys) + emptyChains = networksLength == 0 # non-functional coins + valueForEmpty = False if emptyChains else None + result[code] = self.safe_currency_structure({ + 'info': currency, + 'code': code, + 'id': currencyId, + 'type': 'crypto', + 'name': name, + 'active': None, + 'deposit': valueForEmpty, + 'withdraw': valueForEmpty, + 'fee': None, + 'precision': None, + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + 'deposit': { + 'min': None, + 'max': None, + }, + }, + 'networks': networks, + }) + return result + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for apex + + https://api-docs.pro.apex.exchange/#publicapi-v3-for-omni-get-all-config-data-v3 + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = await self.publicGetV3Symbols(params) + data = self.safe_dict(response, 'data', {}) + contractConfig = self.safe_dict(data, 'contractConfig', {}) + perpetualContract = self.safe_list(contractConfig, 'perpetualContract', []) + # { + # "perpetualContract":[ + # { + # "baselinePositionValue": "50000.0000", + # "crossId": 30002, + # "crossSymbolId": 10, + # "crossSymbolName": "BTCUSDT", + # "digitMerge": "0.1,0.2,0.4,1,2", + # "displayMaxLeverage": "100", + # "displayMinLeverage": "1", + # "enableDisplay": True, + # "enableOpenPosition": True, + # "enableTrade": True, + # "fundingImpactMarginNotional": "6", + # "fundingInterestRate": "0.0003", + # "incrementalInitialMarginRate": "0.00250", + # "incrementalMaintenanceMarginRate": "0.00100", + # "incrementalPositionValue": "50000.0000", + # "initialMarginRate": "0.01", + # "maintenanceMarginRate": "0.005", + # "maxOrderSize": "50", + # "maxPositionSize": "100", + # "minOrderSize": "0.0010", + # "maxMarketPriceRange": "0.025", + # "settleAssetId": "USDT", + # "baseTokenId": "BTC", + # "stepSize": "0.001", + # "symbol": "BTC-USDT", + # "symbolDisplayName": "BTCUSDT", + # "tickSize": "0.1", + # "maxMaintenanceMarginRate": "0.5000", + # "maxPositionValue": "5000000.0000", + # "tagIconUrl": "https://static-pro.apex.exchange/icon/LABLE_HOT.svg", + # "tag": "HOT", + # "riskTip": False, + # "defaultInitialMarginRate": "0.05", + # "klineStartTime": 0, + # "maxMarketSizeBuffer": "0.98", + # "enableFundingSettlement": True, + # "indexPriceDecimals": 2, + # "indexPriceVarRate": "0.001", + # "openPositionOiLimitRate": "0.05", + # "fundingMaxRate": "0.000234", + # "fundingMinRate": "-0.000234", + # "fundingMaxValue": "", + # "enableFundingMxValue": True, + # "l2PairId": "50001", + # "settleTimeStamp": 0, + # "isPrelaunch": False, + # "riskLimitConfig": {}, + # "category": "L1" + # } + # ] + # } + return self.parse_markets(perpetualContract) + + def parse_market(self, market: dict) -> Market: + id = self.safe_string(market, 'symbol') + id2 = self.safe_string(market, 'crossSymbolName') + quoteId = self.safe_string(market, 'l2PairId') + baseId = self.safe_string(market, 'baseTokenId') + quote = self.safe_string(market, 'settleAssetId') + base = self.safe_currency_code(baseId) + settleId = self.safe_string(market, 'settleAssetId') + settle = self.safe_currency_code(settleId) + symbol = baseId + '/' + quote + ':' + settle + expiry = 0 + takerFee = self.parse_number('0.0002') + makerFee = self.parse_number('0.0005') + return self.safe_market_structure({ + 'id': id, + 'id2': id2, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': 'swap', + 'spot': False, + 'margin': None, + 'swap': True, + 'future': False, + 'option': False, + 'active': self.safe_bool(market, 'enableTrade'), + 'contract': True, + 'linear': True, + 'inverse': False, + 'taker': takerFee, + 'maker': makerFee, + 'contractSize': self.safe_number(market, 'minOrderSize'), + 'expiry': None if (expiry == 0) else expiry, + 'expiryDatetime': None if (expiry == 0) else self.iso8601(expiry), + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.safe_number(market, 'stepSize'), + 'price': self.safe_number(market, 'tickSize'), + }, + 'limits': { + 'leverage': { + 'min': self.safe_number(market, 'displayMinLeverage'), + 'max': self.safe_number(market, 'displayMaxLeverage'), + }, + 'amount': { + 'min': self.safe_number(market, 'minOrderSize'), + 'max': self.safe_number(market, 'maxOrderSize'), + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + 'info': market, + }) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "symbol": "BTCUSDT", + # "price24hPcnt": "0.450141", + # "lastPrice": "43511.50", + # "highPrice24h": "43513.50", + # "lowPrice24h": "29996.00", + # "markPrice": "43513.50", + # "indexPrice": "40828.94", + # "openInterest": "2036854775808", + # "turnover24h": "5626085.23749999", + # "volume24h": "169.317", + # "fundingRate": "0", + # "predictedFundingRate": "0", + # "nextFundingTime": "10:00:00", + # "tradeCount": 100 + # } + # + timestamp = self.milliseconds() + marketId = self.safe_string(ticker, 'symbol') + market = self.safe_market(marketId, market) + symbol = self.safe_symbol(marketId, market) + last = self.safe_string(ticker, 'lastPrice') + percentage = self.safe_string(ticker, 'price24hPcnt') + quoteVolume = self.safe_string(ticker, 'turnover24h') + baseVolume = self.safe_string(ticker, 'volume24h') + high = self.safe_string(ticker, 'highPrice24h') + low = self.safe_string(ticker, 'lowPrice24h') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': high, + 'low': low, + 'bid': None, + 'bidVolume': None, + 'ask': None, + 'askVolume': None, + 'vwap': None, + 'open': None, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': percentage, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'markPrice': self.safe_string(ticker, 'markPrice'), + 'indexPrice': self.safe_string(ticker, 'indexPrice'), + 'info': ticker, + }, market) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://api-docs.pro.apex.exchange/#publicapi-v3-for-omni-get-ticker-data-v3 + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id2'], + } + response = await self.publicGetV3Ticker(self.extend(request, params)) + tickers = self.safe_list(response, 'data', []) + rawTicker = self.safe_dict(tickers, 0, {}) + return self.parse_ticker(rawTicker, market) + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://api-docs.pro.apex.exchange/#publicapi-v3-for-omni-get-ticker-data-v3 + + :param str symbols: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + response = await self.publicGetV3DataAllTickerInfo(params) + tickers = self.safe_list(response, 'data', []) + return self.parse_tickers(tickers, symbols) + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://api-docs.pro.apex.exchange/#publicapi-v3-for-omni-get-candlestick-chart-data-v3 + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest candle to fetch + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'interval': self.safe_string(self.timeframes, timeframe, timeframe), + 'symbol': market['id2'], + } + if limit is None: + limit = 200 # default is 200 when requested with `since` + request['limit'] = limit # max 200, default 200 + request, params = self.handle_until_option('end', request, params, 0.001) + if since is not None: + request['start'] = int(math.floor(since / 1000)) + response = await self.publicGetV3Klines(self.extend(request, params)) + data = self.safe_dict(response, 'data', {}) + OHLCVs = self.safe_list(data, market['id2'], []) + return self.parse_ohlcvs(OHLCVs, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # { + # "start": 1647511440000, + # "symbol": "BTC-USD", + # "interval": "1", + # "low": "40000", + # "high": "45000", + # "open": "45000", + # "close": "40000", + # "volume": "1.002", + # "turnover": "3" + # } {"s":"BTCUSDT","i":"1","t":1741265880000,"c":"90235","h":"90235","l":"90156","o":"90156","v":"0.052","tr":"4690.4466"} + # + return [ + self.safe_integer_n(ohlcv, ['start', 't']), + self.safe_number_n(ohlcv, ['open', 'o']), + self.safe_number_n(ohlcv, ['high', 'h']), + self.safe_number_n(ohlcv, ['low', 'l']), + self.safe_number_n(ohlcv, ['close', 'c']), + self.safe_number_n(ohlcv, ['volume', 'v']), + ] + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://api-docs.pro.apex.exchange/#publicapi-v3-for-omni-get-market-depth-v3 + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id2'], + } + if limit is None: + limit = 100 # default is 200 when requested with `since` + request['limit'] = limit # max 100, default 100 + response = await self.publicGetV3Depth(self.extend(request, params)) + # + # { + # "a": [ + # [ + # "96576.3", + # "0.399" + # ], + # [ + # "96577.6", + # "0.106" + # ] + # ], + # "b": [ + # [ + # "96565.2", + # "0.131" + # ], + # [ + # "96565.1", + # "0.038" + # ] + # ], + # "s": "BTCUSDT", + # "u": 18665465 + # } + # + data = self.safe_dict(response, 'data', {}) + timestamp = self.milliseconds() + orderbook = self.parse_order_book(data, market['symbol'], timestamp, 'b', 'a') + orderbook['nonce'] = self.safe_integer(data, 'u') + return orderbook + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://api-docs.pro.apex.exchange/#publicapi-v3-for-omni-get-newest-trading-data-v3 + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch trades for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id2'], + } + if limit is None: + limit = 500 # default is 50 + request['limit'] = limit + response = await self.publicGetV3Trades(self.extend(request, params)) + # + # [ + # { + # "i": "993f7f85-9215-5723-9078-2186ae140847", + # "p": "96534.3", + # "S": "Sell", + # "v": "0.261", + # "s": "BTCUSDT", + # "T": 1739118072710 + # }, + # { + # "i": "c947c9cf-8c18-5784-89c3-91bdf86ddde8", + # "p": "96513.5", + # "S": "Sell", + # "v": "0.042", + # "s": "BTCUSDT", + # "T": 1739118075944 + # } + # ] + # + trades = self.safe_list(response, 'data', []) + return self.parse_trades(trades, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # [ + # { + # "i": "993f7f85-9215-5723-9078-2186ae140847", + # "p": "96534.3", + # "S": "Sell", + # "v": "0.261", + # "s": "BTCUSDT", + # "T": 1739118072710 + # } + # ] + # + marketId = self.safe_string_n(trade, ['s', 'symbol']) + market = self.safe_market(marketId, market) + id = self.safe_string_n(trade, ['i', 'id']) + timestamp = self.safe_integer_n(trade, ['t', 'T', 'createdAt']) + priceString = self.safe_string_n(trade, ['p', 'price']) + amountString = self.safe_string_n(trade, ['v', 'size']) + side = self.safe_string_lower_n(trade, ['S', 'side']) + type = self.safe_string_n(trade, ['type']) + fee = self.safe_string_n(trade, ['fee']) + return self.safe_trade({ + 'info': trade, + 'id': id, + 'order': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'type': type, + 'takerOrMaker': None, + 'side': side, + 'price': priceString, + 'amount': amountString, + 'cost': None, + 'fee': fee, + }, market) + + async def fetch_open_interest(self, symbol: str, params={}): + """ + retrieves the open interest of a contract trading pair + + https://api-docs.pro.apex.exchange/#publicapi-v3-for-omni-get-ticker-data-v3 + + :param str symbol: unified CCXT market symbol + :param dict [params]: exchange specific parameters + :returns dict} an open interest structure{@link https://docs.ccxt.com/#/?id=open-interest-structure: + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id2'], + } + response = await self.publicGetV3Ticker(self.extend(request, params)) + tickers = self.safe_list(response, 'data', []) + rawTicker = self.safe_dict(tickers, 0, {}) + return self.parse_open_interest(rawTicker, market) + + def parse_open_interest(self, interest, market: Market = None): + # + # { + # "symbol": "BTCUSDT", + # "price24hPcnt": "0.450141", + # "lastPrice": "43511.50", + # "highPrice24h": "43513.50", + # "lowPrice24h": "29996.00", + # "markPrice": "43513.50", + # "indexPrice": "40828.94", + # "openInterest": "2036854775808", + # "turnover24h": "5626085.23749999", + # "volume24h": "169.317", + # "fundingRate": "0", + # "predictedFundingRate": "0", + # "nextFundingTime": "10:00:00", + # "tradeCount": 100 + # } + # + timestamp = self.milliseconds() + marketId = self.safe_string(interest, 'symbol') + market = self.safe_market(marketId, market) + symbol = self.safe_symbol(marketId, market) + return self.safe_open_interest({ + 'symbol': symbol, + 'openInterestAmount': self.safe_string(interest, 'openInterest'), + 'openInterestValue': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'info': interest, + }, market) + + async def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical funding rate prices + + https://api-docs.pro.apex.exchange/#publicapi-v3-for-omni-get-funding-rate-history-v3 + + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: timestamp in ms of the earliest funding rate to fetch + :param int [limit]: the maximum amount of `funding rate structures ` to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest funding rate + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `funding rate structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchFundingRateHistory() requires a symbol argument') + await self.load_markets() + request: dict = {} + market = self.market(symbol) + request['symbol'] = market['id'] + if since is not None: + request['beginTimeInclusive'] = since + if limit is not None: + request['limit'] = limit + page = self.safe_integer(params, 'page') + if page is not None: + request['page'] = page + endTimeExclusive = self.safe_integer_n(params, ['endTime', 'endTimeExclusive', 'until']) + if endTimeExclusive is not None: + request['endTimeExclusive'] = endTimeExclusive + response = await self.publicGetV3HistoryFunding(self.extend(request, params)) + # + # { + # "historyFunds": [ + # { + # "symbol": "BTC-USD", + # "rate": "0.0000125000", + # "price": "31297.5000008009374142", + # "fundingTime": 12315555, + # "fundingTimestamp": 12315555 + # } + # ], + # "totalSize": 11 + # } + # + rates = [] + data = self.safe_dict(response, 'data', {}) + resultList = self.safe_list(data, 'historyFunds', []) + for i in range(0, len(resultList)): + entry = resultList[i] + timestamp = self.safe_integer(entry, 'fundingTimestamp') + marketId = self.safe_string(entry, 'symbol') + rates.append({ + 'info': entry, + 'symbol': self.safe_symbol(marketId, market), + 'fundingRate': self.safe_number(entry, 'rate'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + sorted = self.sort_by(rates, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, symbol, since, limit) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # { + # "id": "1234", + # "clientId": "1234", + # "accountId": "12345", + # "symbol": "BTC-USD", + # "side": "SELL", + # "price": "18000", + # "limitFee": "100", + # "fee": "100", + # "triggerPrice": "1.2", + # "trailingPercent": "0.12", + # "size": "100", + # "remainingSize": "100", + # "type": "LIMIT", + # "createdAt": 1647502440973, + # "updatedTime": 1647502440973, + # "expiresAt": 1647502440973, + # "status": "PENDING", + # "timeInForce": "GOOD_TIL_CANCEL", + # "postOnly": False, + # "reduceOnly": False, + # "stopPnl": False, + # "latestMatchFillPrice": "reason", + # "cumMatchFillSize": "0.1", + # "cumMatchFillValue": "1000", + # "cumMatchFillFee": "1", + # "cumSuccessFillSize": "0.1", + # "cumSuccessFillValue": "1000", + # "cumSuccessFillFee": "1", + # "triggerPriceType": "INDEX", + # "isOpenTpslOrder": True, + # "isSetOpenTp": True, + # "isSetOpenSl": False, + # "openTpParam": { + # "side": "SELL", + # "price": "18000", + # "limitFee": "100", + # "clientOrderId": "111100", + # "triggerPrice": "1.2", + # "trailingPercent": "0.12", + # "size": "100" + # }, + # "openSlParam": { + # "side": "SELL", + # "price": "18000", + # "limitFee": "100", + # "clientOrderId": "111100", + # "triggerPrice": "1.2", + # "trailingPercent": "0.12", + # "size": "100" + # } + # } + # + timestamp = self.safe_integer(order, 'createdAt') + orderId = self.safe_string(order, 'id') + clientOrderId = self.safe_string(order, 'clientId') + marketId = self.safe_string(order, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + price = self.safe_string(order, 'price') + amount = self.safe_string(order, 'size') + orderType = self.safe_string(order, 'type') + status = self.safe_string(order, 'status') + side = self.safe_string_lower(order, 'side') + # average = self.omit_zero(self.safe_string(order, 'avg_fill_price')) + remaining = self.omit_zero(self.safe_string(order, 'remainingSize')) + lastUpdateTimestamp = self.safe_integer(order, 'updatedTime') + return self.safe_order({ + 'id': orderId, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'lastUpdateTimestamp': lastUpdateTimestamp, + 'status': self.parse_order_status(status), + 'symbol': symbol, + 'type': self.parse_order_type(orderType), + 'timeInForce': self.parse_time_in_force(self.safe_string(order, 'timeInForce')), + 'postOnly': self.safe_bool(order, 'postOnly'), + 'reduceOnly': self.safe_bool(order, 'reduceOnly'), + 'side': side, + 'price': price, + 'triggerPrice': self.safe_string(order, 'triggerPrice'), + 'takeProfitPrice': None, + 'stopLossPrice': None, + 'average': None, + 'amount': amount, + 'filled': None, + 'remaining': remaining, + 'cost': None, + 'trades': None, + 'fee': { + 'cost': self.safe_string(order, 'fee'), + 'currency': market['settleId'], + }, + 'info': order, + }, market) + + def parse_time_in_force(self, timeInForce: Str): + timeInForces: dict = { + 'GOOD_TIL_CANCEL': 'GOOD_TIL_CANCEL', + 'FILL_OR_KILL': 'FILL_OR_KILL', + 'IMMEDIATE_OR_CANCEL': 'IMMEDIATE_OR_CANCEL', + 'POST_ONLY': 'POST_ONLY', + } + return self.safe_string(timeInForces, timeInForce, None) + + def parse_order_status(self, status: Str): + if status is not None: + statuses: dict = { + 'PENDING': 'open', + 'OPEN': 'open', + 'FILLED': 'filled', + 'CANCELING': 'canceled', + 'CANCELED': 'canceled', + 'UNTRIGGERED': 'open', + } + return self.safe_string(statuses, status, status) + return status + + def parse_order_type(self, type: Str): + types: dict = { + 'LIMIT': 'limit', + 'MARKET': 'market', + 'STOP_LIMIT': 'limit', + 'STOP_MARKET': 'market', + 'TAKE_PROFIT_LIMIT': 'limit', + 'TAKE_PROFIT_MARKET': 'market', + } + return self.safe_string(types, type, type) + + def safe_market(self, marketId: Str = None, market: Market = None, delimiter: Str = None, marketType: Str = None) -> MarketInterface: + if market is None and marketId is not None: + if marketId in self.markets: + market = self.markets[marketId] + elif marketId in self.markets_by_id: + market = self.markets_by_id[marketId] + else: + newMarketId = self.add_hyphen_before_usdt(marketId) + if newMarketId in self.markets_by_id: + markets = self.markets_by_id[newMarketId] + numMarkets = len(markets) + if numMarkets > 0: + if self.markets_by_id[newMarketId][0]['id2'] == marketId: + market = self.markets_by_id[newMarketId][0] + return super(apex, self).safe_market(marketId, market, delimiter, marketType) + + def generate_random_client_id_omni(self, _accountId: str): + accountId = _accountId or str(self.rand_number(12)) + return 'apexomni-' + accountId + '-' + str(self.milliseconds()) + '-' + str(self.rand_number(6)) + + def add_hyphen_before_usdt(self, symbol: str): + uppercaseSymbol = symbol.upper() + index = uppercaseSymbol.find('USDT') + symbolChar = self.safe_string(symbol, index - 1) + if index > 0 and symbolChar != '-': + return symbol[0:index] + '-' + symbol[index:] + return symbol + + def get_seeds(self): + seeds = self.safe_string(self.options, 'seeds') + if seeds is None: + raise ArgumentsRequired(self.id + ' the "seeds" key is required in the options to access private endpoints. You can find it in API Management > Omni Key, and then set it.options["seeds"] = XXXX') + return seeds + + async def get_account_id(self): + accountId = self.safe_string(self.options, 'accountId', '0') + if accountId == '0': + accountData = await self.fetch_account() + self.options['accountId'] = self.safe_string(accountData, 'id', '0') + return self.options['accountId'] + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://api-docs.pro.apex.exchange/#privateapi-v3-for-omni-post-creating-orders + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fullfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: The price a trigger order is triggered at + :param float [params.stopLossPrice]: The price a stop loss order is triggered at + :param float [params.takeProfitPrice]: The price a take profit order is triggered at + :param str [params.timeInForce]: "GTC", "IOC", or "POST_ONLY" + :param bool [params.postOnly]: True or False + :param bool [params.reduceOnly]: Ensures that the executed order does not flip the opened position. + :param str [params.clientOrderId]: a unique id for the order + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + orderType = type.upper() + orderSide = side.upper() + orderSize = self.amount_to_precision(symbol, amount) + orderPrice = '0' + if price is not None: + orderPrice = self.price_to_precision(symbol, price) + fees = self.safe_dict(self.fees, 'swap', {}) + taker = self.safe_string(fees, 'taker', '0.0005') + maker = self.safe_string(fees, 'maker', '0.0002') + limitFee = self.decimal_to_precision(Precise.string_add(Precise.string_mul(Precise.string_mul(orderPrice, orderSize), taker), self.number_to_string(market['precision']['price'])), TRUNCATE, market['precision']['price'], self.precisionMode, self.paddingMode) + timeNow = self.milliseconds() + triggerPrice = self.safe_string(params, 'triggerPrice') + stopLossPrice = self.safe_string(params, 'stopLossPrice') + takeProfitPrice = self.safe_string(params, 'takeProfitPrice') + if stopLossPrice is not None: + orderType = 'STOP_MARKET' if (orderType == 'MARKET') else 'STOP_LIMIT' + triggerPrice = stopLossPrice + elif takeProfitPrice is not None: + orderType = 'TAKE_PROFIT_MARKET' if (orderType == 'MARKET') else 'TAKE_PROFIT_LIMIT' + triggerPrice = takeProfitPrice + isMarket = orderType == 'MARKET' + if isMarket and (price is None): + raise ArgumentsRequired(self.id + ' createOrder() requires a price argument for market orders') + timeInForce = self.safe_string_upper(params, 'timeInForce') + postOnly = self.is_post_only(isMarket, None, params) + if timeInForce is None: + timeInForce = 'GOOD_TIL_CANCEL' + if not isMarket: + if postOnly: + timeInForce = 'POST_ONLY' + elif timeInForce == 'ioc': + timeInForce = 'IMMEDIATE_OR_CANCEL' + params = self.omit(params, 'timeInForce') + params = self.omit(params, 'postOnly') + clientOrderId = self.safe_string_n(params, ['clientId', 'clientOrderId', 'client_order_id']) + accountId = await self.get_account_id() + if clientOrderId is None: + clientOrderId = self.generate_random_client_id_omni(accountId) + params = self.omit(params, ['clientId', 'clientOrderId', 'client_order_id', 'stopLossPrice', 'takeProfitPrice', 'triggerPrice']) + orderToSign = { + 'accountId': accountId, + 'slotId': clientOrderId, + 'nonce': clientOrderId, + 'pairId': market['quoteId'], + 'size': orderSize, + 'price': orderPrice, + 'direction': orderSide, + 'makerFeeRate': maker, + 'takerFeeRate': taker, + } + if triggerPrice is not None: + orderToSign['triggerPrice'] = self.price_to_precision(symbol, triggerPrice) + signature = await self.get_zk_contract_signature_obj(self.remove0x_prefix(self.get_seeds()), orderToSign) + request: dict = { + 'symbol': market['id'], + 'side': orderSide, + 'type': orderType, # LIMIT/MARKET/STOP_LIMIT/STOP_MARKET + 'size': orderSize, + 'price': orderPrice, + 'limitFee': limitFee, + 'expiration': int(math.floor(timeNow / 1000 + 30 * 24 * 60 * 60)), + 'timeInForce': timeInForce, + 'clientId': clientOrderId, + 'brokerId': self.safe_string(self.options, 'brokerId', '6956'), + } + if triggerPrice is not None: + request['triggerPrice'] = self.price_to_precision(symbol, triggerPrice) + request['signature'] = signature + response = await self.privatePostV3Order(self.extend(request, params)) + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data, market) + + async def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account to transfer from + :param str toAccount: account to transfer to + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.transferId]: UUID, which is unique across the platform + :returns dict: a `transfer structure ` + """ + await self.load_markets() + configResponse = await self.publicGetV3Symbols(params) + configData = self.safe_dict(configResponse, 'data', {}) + contractConfig = self.safe_dict(configData, 'contractConfig', {}) + contractAssets = self.safe_list(contractConfig, 'assets', []) + spotConfig = self.safe_dict(configData, 'spotConfig', {}) + spotAssets = self.safe_list(spotConfig, 'assets', []) + globalConfig = self.safe_dict(spotConfig, 'global', {}) + receiverAddress = self.safe_string(globalConfig, 'contractAssetPoolEthAddress', '') + receiverZkAccountId = self.safe_string(globalConfig, 'contractAssetPoolZkAccountId', '') + receiverSubAccountId = self.safe_string(globalConfig, 'contractAssetPoolSubAccount', '') + receiverAccountId = self.safe_string(globalConfig, 'contractAssetPoolAccountId', '') + accountResponse = await self.privateGetV3Account(params) + accountData = self.safe_dict(accountResponse, 'data', {}) + spotAccount = self.safe_dict(accountData, 'spotAccount', {}) + zkAccountId = self.safe_string(spotAccount, 'zkAccountId', '') + subAccountId = self.safe_string(spotAccount, 'defaultSubAccountId', '0') + subAccounts = self.safe_list(spotAccount, 'subAccounts', []) + nonce = '0' + if len(subAccounts) > 0: + nonce = self.safe_string(subAccounts[0], 'nonce', '0') + ethAddress = self.safe_string(accountData, 'ethereumAddress', '') + accountId = self.safe_string(accountData, 'id', '') + currency = {} + assets = [] + if fromAccount is not None and fromAccount.lower() == 'contract': + assets = contractAssets + else: + assets = spotAssets + for i in range(0, len(assets)): + if self.safe_string(assets[i], 'token', '') == code: + currency = assets[i] + tokenId = self.safe_string(currency, 'tokenId', '') + amountNumber = self.parse_to_int(amount * (math.pow(10, self.safe_number(currency, 'decimals', 0)))) + timestampSeconds = self.parse_to_int(self.milliseconds() / 1000) + clientOrderId = self.safe_string_n(params, ['clientId', 'clientOrderId', 'client_order_id']) + if clientOrderId is None: + clientOrderId = self.generate_random_client_id_omni(self.safe_string(self.options, 'accountId')) + params = self.omit(params, ['clientId', 'clientOrderId', 'client_order_id']) + if fromAccount is not None and fromAccount.lower() == 'contract': + formattedUint32 = '4294967295' + zkSignAccountId = Precise.string_mod(accountId, formattedUint32) + expireTime = timestampSeconds + 3600 * 24 * 28 + orderToSign = { + 'zkAccountId': zkSignAccountId, + 'receiverAddress': ethAddress, + 'subAccountId': subAccountId, + 'receiverSubAccountId': subAccountId, + 'tokenId': tokenId, + 'amount': str(amountNumber), + 'fee': '0', + 'nonce': clientOrderId, + 'timestampSeconds': expireTime, + 'isContract': True, + } + signature = await self.get_zk_transfer_signature_obj(self.remove0x_prefix(self.get_seeds()), orderToSign) + request: dict = { + 'amount': amount, + 'expireTime': expireTime, + 'clientWithdrawId': clientOrderId, + 'signature': signature, + 'token': code, + 'ethAddress': ethAddress, + } + response = await self.privatePostV3ContractTransferOut(self.extend(request, params)) + data = self.safe_dict(response, 'data', {}) + currentTime = self.milliseconds() + return self.extend(self.parse_transfer(data, self.currency(code)), { + 'timestamp': currentTime, + 'datetime': self.iso8601(currentTime), + 'amount': self.parse_number(amount), + 'fromAccount': 'contract', + 'toAccount': 'spot', + }) + else: + orderToSign = { + 'zkAccountId': zkAccountId, + 'receiverAddress': receiverAddress, + 'subAccountId': subAccountId, + 'receiverSubAccountId': receiverSubAccountId, + 'tokenId': tokenId, + 'amount': str(amountNumber), + 'fee': '0', + 'nonce': nonce, + 'timestampSeconds': timestampSeconds, + } + signature = await self.get_zk_transfer_signature_obj(self.remove0x_prefix(self.get_seeds()), orderToSign) + request: dict = { + 'amount': str(amount), + 'timestamp': timestampSeconds, + 'clientTransferId': clientOrderId, + 'signature': signature, + 'zkAccountId': zkAccountId, + 'subAccountId': subAccountId, + 'fee': '0', + 'token': code, + 'tokenId': tokenId, + 'receiverAccountId': receiverAccountId, + 'receiverZkAccountId': receiverZkAccountId, + 'receiverSubAccountId': receiverSubAccountId, + 'receiverAddress': receiverAddress, + 'nonce': nonce, + } + response = await self.privatePostV3TransferOut(self.extend(request, params)) + data = self.safe_dict(response, 'data', {}) + currentTime = self.milliseconds() + return self.extend(self.parse_transfer(data, self.currency(code)), { + 'timestamp': currentTime, + 'datetime': self.iso8601(currentTime), + 'amount': self.parse_number(amount), + 'fromAccount': 'spot', + 'toAccount': 'contract', + }) + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + currencyId = self.safe_string(transfer, 'coin') + timestamp = self.safe_integer(transfer, 'timestamp') + fromAccount = self.safe_string(transfer, 'fromAccount') + toAccount = self.safe_string(transfer, 'toAccount') + return { + 'info': transfer, + 'id': self.safe_string_n(transfer, ['transferId', 'id']), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'currency': self.safe_currency_code(currencyId, currency), + 'amount': self.safe_number(transfer, 'amount'), + 'fromAccount': fromAccount, + 'toAccount': toAccount, + 'status': self.safe_string(transfer, 'status'), + } + + async def cancel_all_orders(self, symbol: Str = None, params={}) -> List[Order]: + """ + cancel all open orders in a market + + https://api-docs.pro.apex.exchange/#privateapi-v3-for-omni-post-cancel-all-open-orders + + :param str symbol: unified market symbol of the market to cancel orders in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + market = None + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + response = await self.privatePostV3DeleteOpenOrders(self.extend(request, params)) + data = self.safe_dict(response, 'data', {}) + return [self.parse_order(data, market)] + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://api-docs.pro.apex.exchange/#privateapi-v3-for-omni-post-cancel-order + + :param str id: order id + @param symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + request: dict = {} + clientOrderId = self.safe_string_n(params, ['clientId', 'clientOrderId', 'client_order_id']) + response = None + if clientOrderId is not None: + request['id'] = clientOrderId + params = self.omit(params, ['clientId', 'clientOrderId', 'client_order_id']) + response = await self.privatePostV3DeleteClientOrderId(self.extend(request, params)) + else: + request['id'] = id + response = await self.privatePostV3DeleteOrder(self.extend(request, params)) + data = self.safe_dict(response, 'data', {}) + return self.safe_order(data) + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://api-docs.pro.apex.exchange/#privateapi-v3-for-omni-get-order-id + https://api-docs.pro.apex.exchange/#privateapi-v3-for-omni-get-order-by-clientorderid + + :param str id: the order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.clientOrderId]: a unique id for the order + :returns dict: An `order structure ` + """ + await self.load_markets() + request: dict = {} + clientOrderId = self.safe_string_n(params, ['clientId', 'clientOrderId', 'client_order_id']) + response = None + if clientOrderId is not None: + request['id'] = clientOrderId + params = self.omit(params, ['clientId', 'clientOrderId', 'client_order_id']) + response = await self.privateGetV3OrderByClientOrderId(self.extend(request, params)) + else: + request['id'] = id + response = await self.privateGetV3Order(self.extend(request, params)) + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://api-docs.pro.apex.exchange/#privateapi-v3-for-omni-get-open-orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + response = await self.privateGetV3OpenOrders(params) + orders = self.safe_list(response, 'data', []) + return self.parse_orders(orders, None, since, limit) + + async def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user *classic accounts only* + + https://api-docs.pro.apex.exchange/#privateapi-v3-for-omni-get-all-order-history + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve, default 100 + :param dict [params]: extra parameters specific to the exchange API endpoint + :param dict [params.until]: end time, ms + :param boolean [params.status]: "PENDING", "OPEN", "FILLED", "CANCELED", "EXPIRED", "UNTRIGGERED" + :param boolean [params.side]: BUY or SELL + :param str [params.type]: "LIMIT", "MARKET","STOP_LIMIT", "STOP_MARKET", "TAKE_PROFIT_LIMIT","TAKE_PROFIT_MARKET" + :param str [params.orderType]: "ACTIVE","CONDITION","HISTORY" + :param boolean [params.page]: Page numbers start from 0 + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if since is not None: + request['beginTimeInclusive'] = since + if limit is not None: + request['limit'] = limit + endTimeExclusive = self.safe_integer_n(params, ['endTime', 'endTimeExclusive', 'until']) + if endTimeExclusive is not None: + request['endTimeExclusive'] = endTimeExclusive + params = self.omit(params, ['endTime', 'endTimeExclusive', 'until']) + response = await self.privateGetV3HistoryOrders(self.extend(request, params)) + data = self.safe_dict(response, 'data', {}) + orders = self.safe_list(data, 'orders', []) + return self.parse_orders(orders, market, since, limit) + + async def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all the trades made from a single order + + https://api-docs.pro.apex.exchange/#privateapi-v3-for-omni-get-trade-history + + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + request: dict = {} + clientOrderId = self.safe_string_2(params, 'clientOrderId', 'clientId') + if clientOrderId is not None: + request['clientOrderId'] = clientOrderId + else: + request['orderId'] = id + params = self.omit(params, ['clientOrderId', 'clientId']) + response = await self.privateGetV3OrderFills(self.extend(request, params)) + data = self.safe_dict(response, 'data', {}) + orders = self.safe_list(data, 'orders', []) + return self.parse_trades(orders, None, since, limit) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches information on multiple orders made by the user *classic accounts only* + + https://api-docs.pro.apex.exchange/#privateapi-v3-for-omni-get-trade-history + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve, default 100 + :param dict [params]: extra parameters specific to the exchange API endpoint + :param dict [params.until]: end time + :param boolean [params.side]: BUY or SELL + :param str [params.orderType]: "LIMIT", "MARKET","STOP_LIMIT", "STOP_MARKET", "TAKE_PROFIT_LIMIT","TAKE_PROFIT_MARKET" + :param boolean [params.page]: Page numbers start from 0 + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if since is not None: + request['beginTimeInclusive'] = since + if limit is not None: + request['limit'] = limit + endTimeExclusive = self.safe_integer_n(params, ['endTime', 'endTimeExclusive', 'until']) + if endTimeExclusive is not None: + request['endTimeExclusive'] = endTimeExclusive + params = self.omit(params, ['endTime', 'endTimeExclusive', 'until']) + response = await self.privateGetV3Fills(self.extend(request, params)) + data = self.safe_dict(response, 'data', {}) + orders = self.safe_list(data, 'orders', []) + return self.parse_trades(orders, market, since, limit) + + async def fetch_funding_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches information on multiple orders made by the user *classic accounts only* + + https://api-docs.pro.apex.exchange/#privateapi-v3-for-omni-get-funding-rate + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve, default 100 + :param dict [params]: extra parameters specific to the exchange API endpoint + :param dict [params.until]: end time, ms + :param boolean [params.side]: BUY or SELL + :param boolean [params.page]: Page numbers start from 0 + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if since is not None: + request['beginTimeInclusive'] = since + if limit is not None: + request['limit'] = limit + endTimeExclusive = self.safe_integer_n(params, ['endTime', 'endTimeExclusive', 'until']) + if endTimeExclusive is not None: + params = self.omit(params, ['endTime', 'endTimeExclusive', 'until']) + request['endTimeExclusive'] = endTimeExclusive + response = await self.privateGetV3Funding(self.extend(request, params)) + data = self.safe_dict(response, 'data', {}) + fundingValues = self.safe_list(data, 'fundingValues', []) + return self.parse_incomes(fundingValues, market, since, limit) + + def parse_income(self, income, market: Market = None): + # + # { + # "id": "1234", + # "symbol": "BTC-USDT", + # "fundingValue": "10000", + # "rate": "0.0000125000", + # "positionSize": "500", + # "price": "90", + # "side": "LONG", + # "status": "SUCCESS", + # "fundingTime": 1647502440973, + # "transactionId": "1234556" + # } + # + marketId = self.safe_string(income, 'symbol') + market = self.safe_market(marketId, market, None, 'contract') + code = 'USDT' + timestamp = self.safe_integer(income, 'fundingTime') + return { + 'info': income, + 'symbol': self.safe_symbol(marketId, market), + 'code': code, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': self.safe_string(income, 'id'), + 'amount': self.safe_number(income, 'fundingValue'), + 'rate': self.safe_number(income, 'rate'), + } + + async def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://api-docs.pro.apex.exchange/#privateapi-v3-for-omni-post-sets-the-initial-margin-rate-of-a-contract + + :param float leverage: the rate of leverage + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setLeverage() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + leverageString = self.number_to_string(leverage) + initialMarginRate = Precise.string_div('1', leverageString, 4) + request: dict = { + 'symbol': market['id'], + 'initialMarginRate': initialMarginRate, + } + response = await self.privatePostV3SetInitialMarginRate(self.extend(request, params)) + data = self.safe_dict(response, 'data', {}) + return data + + async def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://api-docs.pro.apex.exchange/#privateapi-v3-for-omni-get-retrieve-user-account-data + + :param str[] [symbols]: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + await self.load_markets() + response = await self.privateGetV3Account(params) + data = self.safe_dict(response, 'data', {}) + positions = self.safe_list(data, 'positions', []) + return self.parse_positions(positions, symbols) + + def parse_position(self, position: dict, market: Market = None): + # + # { + # "symbol": "BTC-USDT", + # "status": "", + # "side": "LONG", + # "size": "0.000", + # "entryPrice": "0.00", + # "exitPrice": "", + # "createdAt": 1690366452416, + # "updatedTime": 1690366452416, + # "fee": "0.000000", + # "fundingFee": "0.000000", + # "lightNumbers": "", + # "customInitialMarginRate": "0" + # } + marketId = self.safe_string(position, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + side = self.safe_string_lower(position, 'side') + quantity = self.safe_string(position, 'size') + timestamp = self.safe_integer(position, 'updatedTime') + leverage = 20 + customInitialMarginRate = self.safe_string_n(position, ['customInitialMarginRate', 'customImr'], '0') + if self.precision_from_string(customInitialMarginRate) != 0: + leverage = self.parse_to_int(Precise.string_div('1', customInitialMarginRate, 4)) + return self.safe_position({ + 'info': position, + 'id': self.safe_string(position, 'id'), + 'symbol': symbol, + 'entryPrice': self.safe_string(position, 'entryPrice'), + 'markPrice': None, + 'notional': None, + 'collateral': None, + 'unrealizedPnl': None, + 'side': side, + 'contracts': self.parse_number(quantity), + 'contractSize': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'hedged': None, + 'maintenanceMargin': None, + 'maintenanceMarginPercentage': None, + 'initialMargin': None, + 'initialMarginPercentage': None, + 'leverage': leverage, + 'liquidationPrice': None, + 'marginRatio': None, + 'marginMode': None, + 'percentage': None, + }) + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = self.implode_hostname(self.urls['api'][api]) + '/' + path + headers = { + 'User-Agent': 'apex-CCXT', + 'Accept': 'application/json', + 'Content-Type': 'application/x-www-form-urlencoded', + } + signPath = '/api/' + path + signBody = body + if method.upper() != 'POST': + if params: + signPath += '?' + self.rawencode(params) + url += '?' + self.rawencode(params) + else: + sortedQuery = self.keysort(params) + signBody = self.rawencode(sortedQuery) + if api == 'private': + self.check_required_credentials() + timestamp = str(self.milliseconds()) + messageString = timestamp + method.upper() + signPath + if signBody is not None: + messageString = messageString + signBody + signature = self.hmac(self.encode(messageString), self.encode(self.string_to_base64(self.secret)), hashlib.sha256, 'base64') + headers['APEX-SIGNATURE'] = signature + headers['APEX-API-KEY'] = self.apiKey + headers['APEX-TIMESTAMP'] = timestamp + headers['APEX-PASSPHRASE'] = self.password + return {'url': url, 'method': method, 'body': signBody, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + # + # {"code":3,"msg":"Order price must be greater than 0. Order price is 0.","key":"ORDER_PRICE_MUST_GREETER_ZERO","detail":{"price":"0"}} + # {"code":400,"msg":"strconv.ParseInt: parsing \"dsfdfsd\": invalid syntax","timeCost":5320995} + # + if response is None: + return None + errorCode = self.safe_integer(response, 'code') + if errorCode is not None and errorCode != 0: + feedback = self.id + ' ' + body + message = self.safe_string_2(response, 'key', 'msg') + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + status = str(code) + self.throw_exactly_matched_exception(self.exceptions['exact'], status, feedback) + raise ExchangeError(feedback) + return None diff --git a/ccxt/async_support/arkham.py b/ccxt/async_support/arkham.py new file mode 100644 index 0000000..cf6d9cd --- /dev/null +++ b/ccxt/async_support/arkham.py @@ -0,0 +1,2407 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.arkham import ImplicitAPI +import hashlib +from ccxt.base.types import Account, Any, Balances, Currencies, Currency, DepositAddress, Int, Leverage, LeverageTier, LeverageTiers, Market, Num, Order, OrderBook, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, Trade, TradingFees, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import OperationRejected +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidAddress +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import OperationFailed +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class arkham(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(arkham, self).describe(), { + 'id': 'arkham', + 'name': 'ARKHAM', + 'countries': ['US'], + 'version': 'v1', + 'rateLimit': 20 / 3, # 150 req/s + 'certified': False, + 'pro': True, + 'has': { + 'CORS': False, + 'spot': True, + 'margin': False, + 'swap': True, + 'future': False, + 'option': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'createDepositAddress': True, + 'createOrder': True, + 'fetchAccounts': True, + 'fetchAllGreeks': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': False, + 'fetchDepositAddressesByNetwork': True, + 'fetchDeposits': True, + 'fetchFundingHistory': True, + 'fetchGreeks': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchLeverage': True, + 'fetchLeverageTiers': True, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchPositions': True, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFees': True, + 'fetchVolatilityHistory': False, + 'fetchWithdrawals': True, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'sandbox': False, + 'setLeverage': True, + 'withdraw': True, + }, + 'timeframes': { + # enums are wrong in DOCS, these string values need to be in request + '1m': '1m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1h', + '6h': '6h', + '1d': '24h', + }, + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/5cefdcfb-2c10-445b-835c-fa21317bf5ac', + 'api': { + 'v1': 'https://arkm.com/api', + }, + 'www': 'https://arkm.com/', + 'referral': { + 'url': 'https://arkm.com/register?ref=ccxt', + 'discount': 0, + }, + 'doc': [ + 'https://arkm.com/limits-api', + 'https://info.arkm.com/api-platform', + ], + 'fees': 'https://arkm.com/fees', + }, + 'api': { + 'v1': { + 'public': { + 'get': { + 'alerts': 1, + 'announcements': 1, + 'assets': 1, + 'book': 1, + 'candles': 1, + 'chains': 1, + 'contracts': 1, + 'index-price': 1, + 'index-prices': 1, + 'margin-schedules': 1, + 'marketcapchart': 1, + 'marketcaps': 1, + 'pair': 1, + 'pairs': 1, + 'server-time': 1, + 'ticker': 1, + 'tickers': 1, + 'trades': 1, + }, + }, + 'private': { + # for orders: spot 20/s, todo: perp 40/s + 'get': { + 'user': 7.5, + 'orders': 7.5, + 'orders/by-client-order-id': 7.5, + 'orders/history': 7.5, + 'orders/history/by-client-order-id': 7.5, + 'orders/history_offset': 7.5, + 'orders/{id}': 7.5, + 'trades': 7.5, + 'trades/history': 7.5, + 'trades/time': 7.5, + 'trigger-orders': 7.5, + 'account/airdrops': 7.5, + 'account/balance-updates': 7.5, + 'account/balances': 7.5, + 'account/balances/ll': 7.5, + 'account/balances/history': 7.5, + 'account/balances/commissions': 7.5, + 'account/deposit/addresses': 7.5, + 'account/deposits': 7.5, + 'account/fees': 7.5, + 'account/funding-rate-payments': 7.5, + 'account/leverage': 7.5, + 'account/lsp-assignments': 7.5, + 'account/margin': 7.5, + 'account/margin/all': 7.5, + 'account/notifications': 7.5, + 'account/position-updates': 7.5, + 'account/positions': 7.5, + 'account/realized-pnl': 7.5, + 'account/rebates': 7.5, + 'account/referral-links': 7.5, + 'account/sessions': 7.5, + 'account/settings': 7.5, + 'account/settings/price-alert': 7.5, + 'account/transfers': 7.5, + 'account/unsubscribe': 7.5, + 'account/watchlist': 7.5, + 'account/withdrawal/addresses': 7.5, + 'account/withdrawal/addresses/{id}': 7.5, + 'account/withdrawals': 7.5, + 'subaccounts': 7.5, + 'airdrop': 7.5, + 'airdrop/claim': 7.5, + 'affiliate-dashboard/commission-earned': 7.5, + 'affiliate-dashboard/min-arkm-last-30d': 7.5, + 'affiliate-dashboard/points': 7.5, + 'affiliate-dashboard/points-season-1': 7.5, + 'affiliate-dashboard/points-season-2': 7.5, + 'affiliate-dashboard/realized-pnl': 7.5, + 'affiliate-dashboard/rebate-balance': 7.5, + 'affiliate-dashboard/referral-count': 7.5, + 'affiliate-dashboard/referrals-season-1': 7.5, + 'affiliate-dashboard/referrals-season-2': 7.5, + 'affiliate-dashboard/trading-volume-stats': 7.5, + 'affiliate-dashboard/volume-season-1': 7.5, + 'affiliate-dashboard/volume-season-2': 7.5, + 'affiliate-dashboard/api-key': 7.5, + 'competitions/opt-in-status': 7.5, + 'rewards/info': 7.5, + 'rewards/vouchers': 7.5, + }, + 'post': { + 'orders/new': 7.5, + 'trigger-orders/new': 7.5, + 'orders/cancel': 7.5, + 'trigger-orders/cancel': 7.5, + 'orders/cancel/all': 7.5, + 'trigger-orders/cancel/all': 7.5, + 'orders/new/simple': 7.5, + 'account/deposit/addresses/new': 7.5, + 'account/leverage': 7.5, + 'account/notifications/read': 7.5, + 'account/referral-links': 7.5, + 'account/sessions/delete': 7.5, + 'account/sessions/terminate-all': 7.5, + 'account/settings/update': 7.5, + 'account/watchlist/add': 7.5, + 'account/watchlist/remove': 7.5, + 'account/withdraw': 7.5, + 'account/withdrawal/addresses/confirm': 7.5, + 'subaccounts': 7.5, + 'subaccounts/transfer': 7.5, + 'subaccounts/perp-transfer': 7.5, + 'subaccounts/update-settings': 7.5, + 'airdrop': 7.5, + 'api-key/create': 7.5, + 'authenticate': 7.5, + 'competitions/opt-in': 7.5, + 'rewards/vouchers/claim': 7.5, + }, + 'put': { + 'account/referral-links/{id}/slug': 7.5, + 'account/settings/price-alert': 7.5, + 'account/withdrawal/addresses/{id}': 7.5, + 'subaccounts': 7.5, + 'api-key/update/{id}': 7.5, + }, + 'delete': { + 'account/settings/price-alert': 7.5, + 'account/withdrawal/addresses/{id}': 7.5, + 'subaccounts/{subaccountId}': 7.5, + 'api-key/{id}': 7.5, + }, + }, + }, + }, + 'options': { + 'networks': { + 'ETH': 'ETH', + 'ERC20': 'ETH', + 'BTC': 'BTC', + 'SOL': 'SOL', + 'TON': 'TON', + 'DOGE': 'DOGE', + 'SUI': 'SUI', + 'XRP': 'XRP', + 'OP': 'OP', + 'AVAXC': 'AVAX', + 'ARBONE': 'ARB', + }, + 'networksById': { + 'ETH': 'ERC20', + 'ERC20': 'ERC20', + }, + 'requestExpiration': 5000, # 5 seconds + 'timeframeDurations': { + '1m': 60000000, + '5m': 300000000, + '15m': 900000000, + '30m': 1800000000, + '1h': 3600000000, + '6h': 21600000000, + '1d': 86400000000, + }, + }, + 'features': { + 'default': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerPriceType': { + 'mark': True, + 'index': True, + 'last': True, + }, + 'triggerDirection': True, + 'stopLossPrice': True, + 'takeProfitPrice': True, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'selfTradePrevention': False, + 'trailing': False, + 'iceberg': False, + 'leverage': False, + 'marketBuyByCost': False, + 'marketBuyRequiresPrice': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, + 'daysBack': None, + 'untilDays': 1, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': True, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': None, + 'daysBackCanceled': None, + 'untilDays': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 365, + }, + }, + 'spot': { + 'extends': 'default', + }, + 'swap': { + 'linear': { + 'extends': 'default', + }, + 'inverse': None, + }, + 'future': { + 'linear': { + 'extends': 'default', + }, + 'inverse': None, + }, + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'exact': { + # 1XXXX General Errors + # These errors can occur for a variety of reasons and may be returned by the API or Websocket on any endpoint. + '10000': OperationFailed, + '10001': BadRequest, + '10002': AuthenticationError, + '10003': BadSymbol, + '10004': ArgumentsRequired, + '10005': RateLimitExceeded, + '10006': PermissionDenied, + '10007': PermissionDenied, + '10008': RateLimitExceeded, + '10009': PermissionDenied, + '10010': PermissionDenied, + '10011': AuthenticationError, + '10012': PermissionDenied, + '10013': PermissionDenied, + '10014': AuthenticationError, + '10015': PermissionDenied, + '10016': PermissionDenied, + '10017': PermissionDenied, + '10018': AuthenticationError, + '10019': AuthenticationError, + '10020': PermissionDenied, + '10021': PermissionDenied, + '10022': ExchangeError, + '10023': BadRequest, + '10024': ExchangeError, + '10025': BadRequest, + # #2XXXX General Websocket Errors + '20001': BadRequest, + '20002': ArgumentsRequired, + '20003': BadRequest, + '20004': ArgumentsRequired, + '20005': BadRequest, + # #3XXXX Trading Errors + '30001': InvalidOrder, + '30002': InvalidOrder, + '30003': InvalidOrder, + '30004': InvalidOrder, + '30005': InvalidOrder, + '30006': InvalidOrder, + '30007': BadSymbol, + '30008': OperationRejected, + '30009': OperationRejected, + '30010': InsufficientFunds, + '30011': BadSymbol, + '30012': OperationRejected, + '30013': OperationRejected, + '30014': InvalidOrder, + '30015': OrderNotFound, + '30016': InvalidOrder, + '30017': InvalidOrder, + '30018': InvalidOrder, + '30019': OperationRejected, + '30020': InvalidOrder, + '30021': InvalidOrder, + '30022': InvalidOrder, + '30023': InvalidOrder, + '30024': InvalidOrder, + '30025': BadRequest, + '30026': PermissionDenied, + '30027': PermissionDenied, + '30028': OrderNotFound, + # #4XXXX Funding Errors + '40001': OperationRejected, + '40002': BadRequest, + '40003': InvalidAddress, + '40004': OperationRejected, + '40005': BadRequest, + '40006': PermissionDenied, + '40007': OperationRejected, + '40008': OperationRejected, + '40009': OperationRejected, + '40010': BadRequest, + '40011': OperationRejected, + '40012': BadRequest, + '40013': BadRequest, + # #9XXXX Other Errors + '90001': BadRequest, + '90002': BadRequest, + '90003': OperationRejected, + '90004': BadRequest, + '90005': BadRequest, + '90006': RateLimitExceeded, + '90007': AuthenticationError, + '90008': RateLimitExceeded, + '90009': PermissionDenied, + '90010': BadRequest, + '90011': RateLimitExceeded, + }, + 'broad': { + 'less than min withdrawal ': OperationRejected, # {"message":"amount 1 less than min withdrawal 5"} + }, + }, + }) + + async def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://arkm.com/docs#get/public/assets + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = await self.v1PublicGetAssets(params) + # + # [ + # { + # "symbol": "USDT", + # "name": "Tether", + # "imageUrl": "https://static.arkhamintelligence.com/tokens/tether.png", + # "stablecoin": True, + # "featuredPair": "BTC_USDT", + # "chains": [ + # { + # "symbol": "ETH", + # "assetSymbol": "ETH", + # "name": "Ethereum", + # "type": "1", + # "confirmations": "6", + # "blockTime": "12000000" + # } + # ], + # "status": "listed", + # "minDeposit": "5", + # "minWithdrawal": "5", + # "withdrawalFee": "2" + # }, + # ... + # + result: dict = {} + for i in range(0, len(response)): + currency = response[i] + id = self.safe_string(currency, 'symbol') + code = self.safe_currency_code(id) + networks: dict = {} + chains = self.safe_list(currency, 'chains', []) + for j in range(0, len(chains)): + chain = chains[j] + networkId = self.safe_string(chain, 'symbol') + network = self.network_id_to_code(networkId) + networks[network] = { + 'info': chain, + 'id': networkId, + 'network': network, + 'title': self.safe_string(chain, 'name'), + 'active': None, + 'deposit': None, + 'withdraw': None, + 'fee': None, + 'precision': None, + 'limits': { + 'withdraw': { + 'min': None, + 'max': None, + }, + }, + } + result[code] = self.safe_currency_structure({ + 'info': currency, + 'id': id, + 'code': code, + 'name': self.safe_string(currency, 'name'), + 'active': self.safe_string(currency, 'status') == 'listed', + 'deposit': None, + 'withdraw': None, + 'fee': self.safe_number(currency, 'withdrawalFee'), + 'precision': None, + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': self.safe_number(currency, 'minWithdrawal'), + 'max': None, + }, + 'deposit': { + 'min': self.safe_number(currency, 'minDeposit'), + 'max': None, + }, + }, + 'type': 'crypto', + 'networks': networks, + }) + return result + + async def fetch_markets(self, params={}) -> List[Market]: + """ + + https://arkm.com/docs#get/public/pairs + + retrieves data on all markets for arkm + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = await self.v1PublicGetPairs(params) + # + # [ + # { + # "symbol": "BTC_USDT", + # "baseSymbol": "BTC", + # "baseImageUrl": "https://static.arkhamintelligence.com/tokens/bitcoin.png", + # "baseIsStablecoin": False, + # "baseName": "Bitcoin", + # "quoteSymbol": "USDT", + # "quoteImageUrl": "https://static.arkhamintelligence.com/tokens/tether.png", + # "quoteIsStablecoin": True, + # "quoteName": "Tether", + # "minTickPrice": "0.01", + # "minLotSize": "0.00001", + # "minSize": "0.00001", + # "maxSize": "9000", + # "minPrice": "0.01", + # "maxPrice": "1000000", + # "minNotional": "5", + # "maxPriceScalarUp": "1.8", + # "maxPriceScalarDown": "0.2", + # "pairType": "spot", # atm, always 'spot' value + # "maxLeverage": "0", + # "status": "listed" + # }, + # { + # "symbol": "BTC_USDT_PERP", + # "baseSymbol": "BTC.P", + # "baseImageUrl": "https://static.arkhamintelligence.com/tokens/bitcoin.png", + # "baseIsStablecoin": False, + # "baseName": "Bitcoin Perpetual", + # "quoteSymbol": "USDT", + # "quoteImageUrl": "https://static.arkhamintelligence.com/tokens/tether.png", + # "quoteIsStablecoin": True, + # "quoteName": "Tether", + # "minTickPrice": "0.01", + # "minLotSize": "0.00001", + # "minSize": "0.00001", + # "maxSize": "9000", + # "minPrice": "0.01", + # "maxPrice": "1000000", + # "minNotional": "5", + # "maxPriceScalarUp": "1.5", + # "maxPriceScalarDown": "0.5", + # "pairType": "perpetual", + # "marginSchedule": "C", + # "maxLeverage": "25", + # "status": "listed" + # }, + # ... + # + result = [] + for i in range(0, len(response)): + market = response[i] + id = self.safe_string(market, 'symbol') + baseId = self.safe_string(market, 'baseSymbol') + quoteId = self.safe_string(market, 'quoteSymbol') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + marketType: Str = None + symbol: Str = None + pairType = self.safe_string(market, 'pairType') + isSpot = pairType == 'spot' + isPerpetual = pairType == 'perpetual' + settle = None + settleId = None + if isSpot: + marketType = 'spot' + symbol = base + '/' + quote + elif isPerpetual: + marketType = 'swap' + base = base.replace('.P', '') + settle = quote + settleId = quoteId + symbol = base + '/' + quote + ':' + settle + result.append({ + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': marketType, + 'spot': isSpot, + 'margin': None, + 'swap': isPerpetual, + 'future': False, + 'option': False, + 'active': self.safe_string(market, 'status') == 'listed', + 'contract': isPerpetual, + 'linear': True if isPerpetual else None, + 'inverse': False if isPerpetual else None, + 'contractSize': None if isSpot else 1, # seems 1 per fetchTrades + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'price': self.safe_number(market, 'minTickPrice'), + 'amount': self.safe_number(market, 'minLotSize'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(market, 'minSize'), + 'max': self.safe_number(market, 'maxSize'), + }, + 'price': { + 'min': self.safe_number(market, 'minPrice'), + 'max': self.safe_number(market, 'maxPrice'), + }, + 'cost': { + 'min': self.safe_number(market, 'minNotional'), + 'max': None, + }, + }, + 'created': None, + 'info': market, + }) + return result + + async def fetch_time(self, params={}): + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://arkm.com/docs#get/public/server-time + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = await self.v1PublicGetServerTime(params) + # + # { + # "serverTime": "1753465832770820" + # } + # + return self.safe_integer_product(response, 'serverTime', 0.001) + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://arkm.com/docs#get/public/book + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the number of order book entries to return, max 50 + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['limit'] = limit + response = await self.v1PublicGetBook(self.extend(request, params)) + # + # { + # "symbol": "BTC_USDT", + # "group": "0.01", + # "asks": [ + # { + # "price": "122900.43", + # "size": "0.0243" + # }, + # { + # "price": "121885.53", + # "size": "0.00116" + # }, + # ... + # ], + # "bids": [ + # { + # "price": "20400", + # "size": "0.00316" + # }, + # { + # "price": "30000", + # "size": "0.00116" + # }, + # ... + # ], + # "lastTime": "1753419275604353" + # } + # + timestamp = self.safe_integer_product(response, 'lastTime', 0.001) + marketId = self.safe_string(response, 'symbol') + return self.parse_order_book(response, self.safe_symbol(marketId, market), timestamp, 'bids', 'asks', 'price', 'size') + + async def fetch_ohlcv(self, symbol: str, timeframe='1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://arkm.com/docs#get/public/candles + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms for the ending date filter, default is the current time + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + maxLimit = 365 + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate', False) + if paginate: + return await self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params, maxLimit) + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'duration': self.safe_string(self.timeframes, timeframe, timeframe), + } + durationMs = self.parse_timeframe(timeframe) * 1000 + until = self.safe_integer(params, 'until') + params = self.omit(params, ['until']) + selectedLimit = min(limit, maxLimit) if (limit is not None) else maxLimit + if since is not None: + request['start'] = since + request['end'] = self.sum(since, selectedLimit * durationMs) + else: + now = self.milliseconds() + request['end'] = until if (until is not None) else now + request['start'] = request['end'] - selectedLimit * durationMs + # exchange needs microseconds + request['start'] = request['start'] * 1000 + request['end'] = request['end'] * 1000 + response = await self.v1PublicGetCandles(self.extend(request, params)) + # + # [ + # { + # "symbol": "BTC_USDT_PERP", + # "time": "1753464720000000", + # "duration": "60000000", + # "open": "116051.35", + # "high": "116060.27", + # "low": "116051.35", + # "close": "116060.27", + # "volume": "0.0257", + # "quoteVolume": "2982.6724054" + # }, + # ... + # ] + # + return self.parse_ohlcvs(response, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # { + # "symbol": "BTC_USDT_PERP", + # "time": "1753464720000000", + # "duration": "60000000", + # "open": "116051.35", + # "high": "116060.27", + # "low": "116051.35", + # "close": "116060.27", + # "volume": "0.0257", + # "quoteVolume": "2982.6724054" + # } + # + return [ + self.safe_integer_product(ohlcv, 'time', 0.001), + self.safe_number(ohlcv, 'open'), + self.safe_number(ohlcv, 'high'), + self.safe_number(ohlcv, 'low'), + self.safe_number(ohlcv, 'close'), + self.safe_number(ohlcv, 'volume'), + ] + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + response = await self.v1PublicGetTickers(params) + # + # [ + # { + # "symbol": "BTC_USDT_PERP", + # "baseSymbol": "BTC.P", + # "quoteSymbol": "USDT", + # "indexCurrency": "USDT", + # "price": "118806.89", + # "price24hAgo": "118212.29", + # "high24h": "119468.05", + # "low24h": "117104.44", + # "volume24h": "180.99438", + # "quoteVolume24h": "21430157.5928827", + # "markPrice": "118814.71", + # "indexPrice": "118804.222610343", + # "fundingRate": "0.000007", + # "nextFundingRate": "0.000006", + # "nextFundingTime": "1753390800000000", + # "productType": "perpetual", + # "openInterest": "2.55847", + # "usdVolume24h": "21430157.5928827", + # "openInterestUSD": "303963.8638583" + # }, + # ... + # + return self.parse_tickers(response, symbols) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = await self.v1PublicGetTicker(self.extend(request, params)) + # + # { + # "symbol": "BTC_USDT_PERP", + # "baseSymbol": "BTC.P", + # "quoteSymbol": "USDT", + # "indexCurrency": "USDT", + # "price": "118806.89", + # "price24hAgo": "118212.29", + # "high24h": "119468.05", + # "low24h": "117104.44", + # "volume24h": "180.99438", + # "quoteVolume24h": "21430157.5928827", + # "markPrice": "118814.71", + # "indexPrice": "118804.222610343", + # "fundingRate": "0.000007", + # "nextFundingRate": "0.000006", + # "nextFundingTime": "1753390800000000", + # "productType": "perpetual", + # "openInterest": "2.55847", + # "usdVolume24h": "21430157.5928827", + # "openInterestUSD": "303963.8638583" + # } + # + return self.parse_ticker(response, market) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + marketId = self.safe_string(ticker, 'symbol') + market = self.safe_market(marketId, market) + return self.safe_ticker({ + 'info': ticker, + 'symbol': self.safe_symbol(marketId, market), + 'high': self.safe_number(ticker, 'high24h'), + 'low': self.safe_number(ticker, 'low24h'), + 'bid': self.safe_number(ticker, 'bid'), + 'last': self.safe_number(ticker, 'price'), + 'open': self.safe_number(ticker, 'price24hAgo'), + 'change': self.safe_number(ticker, 'priceChange'), + 'percentage': self.safe_number(ticker, 'priceChangePercent'), + 'baseVolume': self.safe_number(ticker, 'volume24h'), + 'quoteVolume': self.safe_number(ticker, 'usdVolume24h'), + 'markPrice': self.safe_number(ticker, 'markPrice'), + 'indexPrice': self.safe_number(ticker, 'indexPrice'), + 'vwap': None, + 'average': None, + 'previousClose': None, + 'askVolume': None, + 'bidVolume': None, + }) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://arkm.com/docs#get/public/trades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.loc]: crypto location, default: us + :param str [params.method]: method, default: marketPublicGetV1beta3CryptoLocTrades + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + marketId = market['id'] + request: dict = { + 'symbol': marketId, + } + if limit is not None: + request['limit'] = limit + response = await self.v1PublicGetTrades(self.extend(request, params)) + # + # [ + # { + # "symbol": "BTC_USDT_PERP", + # "revisionId": "1130514101", + # "size": "0.01668", + # "price": "116309.57", + # "takerSide": "sell", + # "time": "1753439710374047" + # }, + # ... + # ] + # + return self.parse_trades(response, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades + # + # { + # "symbol": "BTC_USDT_PERP", + # "revisionId": "1130514101", + # "size": "0.01668", + # "price": "116309.57", + # "takerSide": "sell", + # "time": "1753439710374047" + # } + # + # fetchMyTrades + # + # { + # "symbol": "SOL_USDT", + # "revisionId": "891839406", + # "size": "0.042", + # "price": "185.06", + # "takerSide": "sell", + # "time": "1753773952039342", + # "orderId": "3717304929194", + # "userSide": "sell", + # "quoteFee": "0.00777252", + # "arkmFee": "0", + # "clientOrderId": "" + # } + # + marketId = self.safe_string(trade, 'symbol') + market = self.safe_market(marketId, market) + timestamp = self.safe_integer_product(trade, 'time', 0.001) + quoteFee = self.safe_number(trade, 'quoteFee') + arkmFee = self.safe_number(trade, 'arkmFee') + fee = None + if quoteFee is not None: + fee = { + 'cost': quoteFee, + 'currency': market['quote'], + } + elif arkmFee is not None: + fee = { + 'cost': arkmFee, + 'currency': 'ARKM', + } + return self.safe_trade({ + 'info': trade, + 'id': self.safe_string(trade, 'revisionId'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'type': None, + 'side': self.safe_string_2(trade, 'userSide', 'takerSide'), # priority to userSide + 'takerOrMaker': None, + 'price': self.safe_string(trade, 'price'), + 'amount': self.safe_string(trade, 'size'), + 'cost': None, + 'fee': fee, + 'order': self.safe_string(trade, 'orderId'), + }, market) + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://arkm.com/docs#get/orders/by-client-order-id + + :param str id: the order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.clientOrderId]: a unique id for the order + :returns dict: An `order structure ` + """ + request: dict = { + 'id': int(id), + } + response = await self.v1PrivateGetOrdersId(self.extend(request, params)) + # + # { + # "orderId": "3690478767430", + # "userId": "2959123", + # "subaccountId": "0", + # "symbol": "SOL_USDT", + # "time": "1753696843913970", + # "side": "sell", + # "type": "limitGtc", + # "size": "0.066", + # "price": "293.2", + # "postOnly": False, + # "reduceOnly": False, + # "executedSize": "0", + # "status": "booked", + # "avgPrice": "0", + # "executedNotional": "0", + # "creditFeePaid": "0", + # "marginBonusFeePaid": "0", + # "quoteFeePaid": "0", + # "arkmFeePaid": "0", + # "revisionId": "887956326", + # "lastTime": "1753696843914830", + # "clientOrderId": "", + # "lastSize": "0", + # "lastPrice": "0", + # "lastCreditFee": "0", + # "lastMarginBonusFee": "0", + # "lastQuoteFee": "0", + # "lastArkmFee": "0" + # } + # + return self.parse_order(response) + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://arkm.com/docs#get/orders/history + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch orders for + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if limit is not None: + request['limit'] = limit # note, API does not work for self param + response = await self.v1PrivateGetOrdersHistory(self.extend(request, params)) + # + # [ + # { + # "orderId": "3690478767430", + # "userId": "2959123", + # "subaccountId": "0", + # "symbol": "SOL_USDT", + # "time": "1753696843913970", + # "side": "sell", + # "type": "limitGtc", + # "size": "0.066", + # "price": "293.2", + # "postOnly": False, + # "reduceOnly": False, + # "executedSize": "0", + # "status": "closed", + # "avgPrice": "0", + # "executedNotional": "0", + # "creditFeePaid": "0", + # "marginBonusFeePaid": "0", + # "quoteFeePaid": "0", + # "arkmFeePaid": "0", + # "revisionId": "888084076", + # "lastTime": "1753701350088305", + # "clientOrderId": "", + # "lastSize": "0", + # "lastPrice": "0", + # "lastCreditFee": "0", + # "lastMarginBonusFee": "0", + # "lastQuoteFee": "0", + # "lastArkmFee": "0" + # } + # ] + # + return self.parse_orders(response, market, since, limit) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://arkm.com/docs#get/orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch orders for + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + isTriggerOrder = self.safe_bool(params, 'trigger') + params = self.omit(params, 'trigger') + response = None + if isTriggerOrder: + response = await self.v1PrivateGetTriggerOrders(self.extend({}, params)) + # + # [ + # { + # "subaccountId": "0", + # "symbol": "SOL_USDT", + # "side": "sell", + # "type": "market", + # "size": "0.045", + # "price": "99.9", + # "postOnly": False, + # "reduceOnly": False, + # "time": "1753768103780063", + # "triggerOrderId": "3715847222127", + # "triggerType": "stopLoss", + # "triggerPriceType": "last", + # "triggerPrice": "111", + # "clientOrderId": "", + # "status": "staged" + # }, + # ] + # + else: + response = await self.v1PrivateGetOrders(self.extend({}, params)) + # + # [ + # { + # "orderId": "3690478767430", + # "userId": "2959123", + # "subaccountId": "0", + # "symbol": "SOL_USDT", + # "time": "1753696843913970", + # "side": "sell", + # "type": "limitGtc", + # "size": "0.066", + # "price": "293.2", + # "postOnly": False, + # "reduceOnly": False, + # "executedSize": "0", + # "status": "booked", + # "avgPrice": "0", + # "executedNotional": "0", + # "creditFeePaid": "0", + # "marginBonusFeePaid": "0", + # "quoteFeePaid": "0", + # "arkmFeePaid": "0", + # "revisionId": "887956326", + # "lastTime": "1753696843914830", + # "clientOrderId": "", + # "lastSize": "0", + # "lastPrice": "0", + # "lastCreditFee": "0", + # "lastMarginBonusFee": "0", + # "lastQuoteFee": "0", + # "lastArkmFee": "0" + # } + # ] + # + return self.parse_orders(response, market, since, limit) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://arkm.com/docs#post/orders/cancel + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + isTriggerOrder = self.safe_bool(params, 'trigger') + params = self.omit(params, 'trigger') + response = None + request: dict = {} + clientOrderId = self.safe_integer(params, 'clientOrderId') + if clientOrderId is not None: + params = self.omit(params, 'clientOrderId') + request['clientOrderId'] = clientOrderId + else: + if isTriggerOrder: + request['triggerOrderId'] = int(id) + else: + request['orderId'] = int(id) + if isTriggerOrder: + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument for trigger orders') + market = self.market(symbol) + request['symbol'] = market['id'] + response = await self.v1PrivatePostTriggerOrdersCancel(self.extend(request, params)) + else: + response = await self.v1PrivatePostOrdersCancel(self.extend(request, params)) + # + # {"orderId":3691703758327} + # + return self.parse_order(response) + + async def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders in a market + + https://arkm.com/docs#post/orders/cancel/all + + :param str symbol: cancel alls open orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + if symbol is not None: + raise BadRequest(self.id + ' cancelAllOrders() does not support a symbol argument, use cancelOrder() or fetchOpenOrders() instead') + isTriggerOrder = self.safe_bool(params, 'trigger') + params = self.omit(params, 'trigger') + response = None + if isTriggerOrder: + response = await self.v1PrivatePostTriggerOrdersCancelAll(params) + else: + response = await self.v1PrivatePostOrdersCancelAll(params) + # + # [] returns an empty array, even when successfully cancels orders + # + return self.parse_orders(response, None) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order on the exchange + + https://arkm.com/docs#post/orders/new + + :param str symbol: unified CCXT market symbol + :param str type: "limit" or "market" + :param str side: "buy" or "sell" + :param float amount: the amount of currency to trade + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.timeInForce]: "GTC", "IOC", "FOK", or "PO" + :param float [params.triggerPrice]: price for a trigger(conditional) order + :param float [params.stopLossPrice]: price for a stoploss order + :param float [params.takeProfitPrice]: price for a takeprofit order + :param str [params.triggerDirection]: the direction for trigger orders, 'ascending' or 'descending' + :param str [params.triggerPriceType]: mark, index or last + :param bool [params.postOnly]: True or False whether the order is post-only + :param bool [params.reduceOnly]: True or False whether the order is reduce-only + :returns: `An order structure ` + """ + await self.load_markets() + market = self.market(symbol) + isTriggerOrder = self.safe_number_n(params, ['triggerPrice', 'stopLossPrice', 'takeProfitPrice']) is not None + request = self.create_order_request(symbol, type, side, amount, price, params) + response = None + if isTriggerOrder: + response = await self.v1PrivatePostTriggerOrdersNew(request) + # + # { + # "triggerOrderId": "3716436645573", + # "symbol": "SOL_USDT_PERP", + # "side": "buy", + # "type": "limitGtc", + # "size": "0.05", + # "price": "150" + # } + # + else: + response = await self.v1PrivatePostOrdersNew(request) + # + # { + # "orderId": "3694872060678", + # "clientOrderId": "test123", + # "symbol": "SOL_USDT", + # "subaccountId": "0", + # "side": "buy", + # "type": "limitGtc", + # "size": "0.05", + # "price": "170", + # "time": "1753710501474043" + # } + # + return self.parse_order(response, market) + + def create_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + market = self.market(symbol) + symbol = market['symbol'] + request: dict = { + 'symbol': market['id'], + 'side': side, + 'size': self.amount_to_precision(symbol, amount), + } + isBuy = (side == 'buy') + stopLossPrice = self.safe_number(params, 'stopLossPrice') + takeProfitPrice = self.safe_number(params, 'takeProfitPrice') + triggerPriceAny = self.safe_string_n(params, ['triggerPrice', 'stopLossPrice', 'takeProfitPrice']) + if triggerPriceAny is not None: + request['triggerPrice'] = self.price_to_precision(symbol, triggerPriceAny) + if stopLossPrice is not None: + request['triggerType'] = 'stopLoss' if isBuy else 'takeProfit' + elif takeProfitPrice is not None: + request['triggerType'] = 'takeProfit' if isBuy else 'stopLoss' + else: + triggerDirection = self.safe_string(params, 'triggerDirection') + if triggerDirection is None: + raise ArgumentsRequired(self.id + ' createOrder() requires a triggerDirection parameter when triggerPrice is specified, must be "ascending" or "descending"') + if triggerDirection is not None: + if triggerDirection == 'ascending': + request['triggerType'] = 'stopLoss' if isBuy else 'takeProfit' + elif triggerDirection == 'descending': + request['triggerType'] = 'takeProfit' if isBuy else 'stopLoss' + # mandatory triggerPriceType + if self.safe_string(params, 'triggerPriceType') is None: + request['triggerPriceType'] = 'last' # default + isMarketOrder = (type == 'market') + isLimitOrder = (type == 'limit') + isLimitExchangeSpecific = self.in_array(type, ['limitGtc', 'limitIoc', 'limitFok']) + postOnly = self.is_post_only(isMarketOrder, False, params) + timeInForce = self.safe_string(params, 'timeInForce') + params = self.omit(params, ['postOnly', 'timeInForce', 'triggerPrice', 'stopLossPrice', 'takeProfitPrice', 'triggerDirection']) + if postOnly: + request['postOnly'] = True + if isLimitOrder or isLimitExchangeSpecific: + request['price'] = self.price_to_precision(symbol, price) + # + if timeInForce == 'IOC': + request['type'] = 'limitIoc' + elif timeInForce == 'FOK': + request['type'] = 'limitFok' + else: + request['type'] = 'limitGtc' + elif isMarketOrder: + request['type'] = 'market' + # we don't need to manually handle `reduceOnly`, `clientOrderId`, `triggerPriceType` here-specific keyname & values matches + return self.extend(request, params) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder + # + # { + # "orderId": "3694872060678", + # "clientOrderId": "test123", + # "symbol": "SOL_USDT", + # "subaccountId": "0", + # "side": "buy", + # "type": "limitGtc", + # "size": "0.05", + # "price": "170", + # "time": "1753710501474043" + # } + # + # fetchOrder, fetchOpenOrders, fetchClosedOrders + # + # { + # "orderId": "3690478767430", + # "userId": "2959123", + # "subaccountId": "0", + # "symbol": "SOL_USDT", + # "time": "1753696843913970", + # "side": "sell", + # "type": "limitGtc", + # "size": "0.066", + # "price": "293.2", + # "postOnly": False, + # "reduceOnly": False, + # "executedSize": "0", + # "status": "booked", + # "avgPrice": "0", + # "executedNotional": "0", + # "creditFeePaid": "0", + # "marginBonusFeePaid": "0", + # "quoteFeePaid": "0", + # "arkmFeePaid": "0", + # "revisionId": "887956326", + # "lastTime": "1753696843914830", + # "clientOrderId": "", + # "lastSize": "0", + # "lastPrice": "0", + # "lastCreditFee": "0", + # "lastMarginBonusFee": "0", + # "lastQuoteFee": "0", + # "lastArkmFee": "0" + # } + # + # trigger-orders: createOrder + # + # { + # "triggerOrderId": "3716436645573", + # "symbol": "SOL_USDT_PERP", + # "side": "buy", + # "type": "limitGtc", + # "size": "0.05", + # "price": "150" + # } + # + # trigger-orders: fetchOpenOrders + # + # { + # "subaccountId": "0", + # "symbol": "SOL_USDT", + # "side": "sell", + # "type": "market", + # "size": "0.045", + # "price": "99.9", + # "postOnly": False, + # "reduceOnly": False, + # "time": "1753768103780063", + # "triggerOrderId": "3715847222127", + # "triggerType": "stopLoss", + # "triggerPriceType": "last", + # "triggerPrice": "111", + # "clientOrderId": "", + # "status": "staged" + # } + # + marketId = self.safe_string(order, 'symbol') + market = self.safe_market(marketId, market) + isPostOnly = self.safe_bool(order, 'postOnly') + typeRaw = self.safe_string(order, 'type') + orderType = 'limit' if isPostOnly else self.parse_order_type(typeRaw) + timeInForce = 'PO' if isPostOnly else self.parse_time_in_force(typeRaw) + quoteFeePaid = self.safe_string(order, 'quoteFeePaid') + arkmFeePaid = self.safe_string(order, 'arkmFeePaid') + fees = [] + if quoteFeePaid is not None: + fees.append({ + 'cost': quoteFeePaid, + 'currency': self.safe_string(market, 'quote'), + }) + if arkmFeePaid is not None: + fees.append({ + 'cost': arkmFeePaid, + 'currency': 'ARKM', + }) + timestamp = self.safe_integer_product(order, 'time', 0.001) + return self.safe_order({ + 'id': self.safe_string_2(order, 'orderId', 'triggerOrderId'), + 'clientOrderId': self.safe_string(order, 'clientOrderId'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimeStamp': None, + 'lastUpdateTimestamp': self.safe_integer_product(order, 'lastTime', 0.001), + 'status': self.parse_order_status(self.safe_string(order, 'status')), + 'symbol': market['symbol'], + 'type': orderType, + 'timeInForce': timeInForce, + 'postOnly': None, + 'side': self.safe_string(order, 'side'), + 'price': self.safe_number(order, 'price'), + 'triggerPrice': None, + 'cost': self.safe_number(order, 'executedNotional'), + 'average': self.safe_number_omit_zero(order, 'avgPrice'), + 'amount': self.safe_number(order, 'size'), + 'filled': self.safe_number(order, ''), + 'remaining': None, + 'trades': None, + 'fees': fees, + 'reduceOnly': self.safe_bool(order, 'reduceOnly'), + 'info': order, + }, market) + + def parse_order_type(self, type: Str) -> Str: + types: dict = { + 'limitGtc': 'limit', + 'limitIoc': 'limit', + 'limitFok': 'limit', + 'market': 'market', + } + return self.safe_string_upper(types, type, type) + + def parse_time_in_force(self, type: Str) -> Str: + types: dict = { + 'limitGtc': 'GTC', + 'limitIoc': 'IOC', + 'limitFok': 'FOK', + 'market': 'IOC', + } + return self.safe_string_upper(types, type, type) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'new': 'pending', + 'staged': 'open', + 'booked': 'open', + 'taker': 'closed', + 'maker': 'closed', + 'cancelled': 'canceled', + 'closed': 'closed', + } + return self.safe_string(statuses, status, status) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://arkm.com/docs#get/trades/time + + :param str [symbol]: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trade structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch trades for + :param str [params.page_token]: page_token - used for paging + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + request: dict = {} + if limit is not None: + request['limit'] = limit + # exchange needs to obtain some `from & to` values, otherwise it does not return any result + defaultRange = 24 * 60 * 60 * 1000 # default to last 24 hours + if since is not None: + request['from'] = since * 1000 # convert ms to microseconds + else: + request['from'] = (self.milliseconds() - defaultRange) * 1000 # default to last 24 hours + until = self.safe_integer(params, 'until') + if until is not None: + params = self.omit(params, 'until') + request['to'] = until * 1000 # convert ms to microseconds + else: + request['to'] = self.sum(request['from'], defaultRange * 1000) + request, params = self.handle_until_option('until', request, params) + response = await self.v1PrivateGetTradesTime(self.extend(request, params)) + # + # [ + # { + # "symbol": "SOL_USDT", + # "revisionId": "891839406", + # "size": "0.042", + # "price": "185.06", + # "takerSide": "sell", + # "time": "1753773952039342", + # "orderId": "3717304929194", + # "userSide": "sell", + # "quoteFee": "0.00777252", + # "arkmFee": "0", + # "clientOrderId": "" + # }, + # ... + # + return self.parse_trades(response, None, since, limit) + + async def fetch_accounts(self, params={}) -> List[Account]: + """ + fetch all the accounts associated with a profile + + https://arkm.com/docs#get/user + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `account structures ` indexed by the account type + """ + await self.load_markets() + request: dict = {} + accountId = None + accountId, params = self.handle_option_and_params(params, 'fetchAccounts', 'accountId') + if accountId is not None: + request['subAccountId'] = accountId + response = await self.v1PrivateGetUser(self.extend(request, params)) + # + # { + # "id": "2959123", + # "email": "xyz@gmail.com", + # "username": "t.123", + # "requireMFA": True, + # "kycVerifiedAt": "1753434515850673", + # "pmm": False, + # "dmm": False, + # "becameVipAt": "0", + # "subaccounts": [ + # { + # "id": "0", + # "name": "Primary", + # "pinned": True, + # "isLsp": False, + # "futuresEnabled": True, + # "payFeesInArkm": False, + # "lspSettings": [] + # } + # ], + # "settings": { + # "autogenDepositAddresses": False, + # "hideBalances": False, + # "confirmBeforePlaceOrder": False, + # "tickerTapeScroll": True, + # "updatesFlash": True, + # "notifyOrderFills": False, + # "notifyAnnouncements": False, + # "notifyMarginUsage": False, + # "marginUsageThreshold": "0.5", + # "notifyWithdrawals": True, + # "notifyDeposits": True, + # "notifySendEmail": True, + # "notifyRebates": True, + # "notifyCommissions": True, + # "allowSequenceEmails": True, + # "language": "en" + # }, + # "airdropKycAt": null + # } + # + subAccounts = self.safe_list(response, 'subaccounts', []) + return self.parse_accounts(subAccounts, params) + + def parse_account(self, account): + # + # { + # "id": "0", + # "name": "Primary", + # "pinned": True, + # "isLsp": False, + # "futuresEnabled": True, + # "payFeesInArkm": False, + # "lspSettings": [] + # } + # + return { + 'id': self.safe_string(account, 'id'), + 'type': None, + 'code': None, + 'info': account, + } + + async def fetch_balance(self, params={}) -> Balances: + """ + query for account info + + https://arkm.com/docs#get/account/balances + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + response = await self.v1PrivateGetAccountBalances(params) + # + # [ + # { + # "subaccountId": "0", + # "symbol": "USDT", + # "balance": "19.66494694", + # "free": "19.66494694", + # "priceUSDT": "1", + # "balanceUSDT": "19.66494694", + # "freeUSDT": "19.66494694", + # "lastUpdateReason": "orderFill", + # "lastUpdateTime": "1753773952039342", + # "lastUpdateId": "248507437", + # "lastUpdateAmount": "7.77252" + # }, + # { + # "subaccountId": "0", + # "symbol": "SOL", + # "balance": "0", + # "free": "0", + # "priceUSDT": "186.025584673", + # "balanceUSDT": "0", + # "freeUSDT": "0", + # "lastUpdateReason": "orderFill", + # "lastUpdateTime": "1753773952039342", + # "lastUpdateId": "248507435", + # "lastUpdateAmount": "-0.042" + # } + # ] + # + return self.parse_balance(response) + + def parse_balance(self, response) -> Balances: + timestamp = self.safe_integer_product(response, 'lastUpdateTime', 0.001) + result: dict = { + 'info': response, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + for i in range(0, len(response)): + balance = response[i] + symbol = self.safe_string(balance, 'symbol') + code = self.safe_currency_code(symbol) + account = self.account() + account['total'] = self.safe_string(balance, 'balance') + account['free'] = self.safe_string(balance, 'free') + result[code] = account + return self.safe_balance(result) + + async def create_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + create a currency deposit address + + https://arkm.com/docs#post/account/deposit/addresses/new + + :param str code: unified currency code of the currency for the deposit address + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + await self.load_markets() + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + if networkCode is None: + raise ArgumentsRequired(self.id + ' createDepositAddress() requires a "network" param') + request: dict = { + 'chain': networkCode, + } + response = await self.v1PrivatePostAccountDepositAddressesNew(self.extend(request, params)) + # + # { + # "addresses": "12NauJ26TUT9aYkpId7YdePJJDRMGbAsEMVoTVUvBErV" + # } + # + address = self.safe_string(response, 'addresses') + return self.parse_deposit_address(address, self.currency(code)) + + async def fetch_deposit_addresses_by_network(self, code: str, params={}) -> List[DepositAddress]: + """ + fetch the deposit addresses for a currency associated with self account + + https://arkm.com/docs#get/account/deposit/addresses + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary `address structures `, indexed by the network + """ + await self.load_markets() + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + if networkCode is None: + raise ArgumentsRequired(self.id + ' fetchDepositAddressesByNetwork() requires a "network" param') + request: dict = { + 'chain': self.network_code_to_id(networkCode), + } + response = await self.v1PrivateGetAccountDepositAddresses(self.extend(request, params)) + # + # { + # "addresses": [ + # "12NauJ26TUT9aYkpId7YdePJJDRMGbAsEMVoTVUvBErV" + # ] + # } + # + data = self.safe_list(response, 'addresses') + parsed = self.parse_deposit_addresses(data, None, False, {'network': networkCode}) + return self.index_by(parsed, 'network') + + def parse_deposit_address(self, entry, currency: Currency = None) -> DepositAddress: + # + # "12NauJ26TUT9aYkpId7YdePJJDRMGbAsEMVoTVUvBErV" + # + return { + 'info': entry, + 'currency': self.safe_string(currency, 'code'), + 'network': None, + 'address': entry, + 'tag': None, + } + + async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://arkm.com/docs#get/account/deposit/addresses + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + await self.load_markets() + currency = self.currency(code) + networkCodeAndParams = self.handle_network_code_and_params(params) + networkCode = networkCodeAndParams[0] + indexedAddresses = await self.fetch_deposit_addresses_by_network(code, params) + selectedNetworkCode = self.select_network_code_from_unified_networks(currency['code'], networkCode, indexedAddresses) + address = self.safe_dict(indexedAddresses, selectedNetworkCode) + if address is None: + raise InvalidAddress(self.id + ' fetchDepositAddress() could not find a deposit address for ' + code) + return address + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://arkm.com/docs#get/account/deposits + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + request: dict = {} + if limit is not None: + request['limit'] = limit + response = await self.v1PrivateGetAccountDeposits(self.extend(request, params)) + # + # [ + # { + # "id": "238644", + # "symbol": "SOL", + # "amount": "0.104", + # "time": "1753436404000000", + # "confirmed": True, + # "transactionHash": "1DRxbbyePTsMuB82SDf2fG5gLXH5iYnY8TQDstDPLULpLtjMJtF1ug1T4Mf8B6DSb8fp2sb5YtdbyqieZ2tkE1Ve", + # "chain": "Solana", + # "depositAddress": "12NauJ26TUT9aYkpId7YdePJJDRMGbAsEMVoTVUvBErV", + # "price": "180.322010164" + # } + # ] + # + currency = None + if code is not None: + currency = self.currency(code) + return self.parse_transactions(response, currency, since, limit) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # { + # "id": "238644", + # "symbol": "SOL", + # "amount": "0.104", + # "time": "1753436404000000", + # "confirmed": True, + # "transactionHash": "1DRxbbyePTsMuB82SDf2fG5gLXH5iYnY8TQDstDPLULpLtjMJtF1ug1T4Mf8B6DSb8fp2sb5YtdbyqieZ2tkE1Ve", + # "chain": "Solana", + # "depositAddress": "12NauJ26TUT9aYkpId7YdePJJDRMGbAsEMVoTVUvBErV", + # "price": "180.322010164" + # } + # + address = self.safe_string(transaction, 'depositAddress') + timestamp = self.safe_integer_product(transaction, 'time', 0.001) + confirmd = self.safe_bool(transaction, 'confirmed') + status = None + if confirmd: + status = 'ok' + currencyId = self.safe_string(transaction, 'symbol') + code = self.safe_currency_code(currencyId, currency) + return { + 'info': transaction, + 'id': self.safe_string(transaction, 'id'), + 'txid': self.safe_string(transaction, 'transactionHash'), + 'type': None, + 'currency': code, + 'network': self.network_id_to_code(self.safe_string(transaction, 'chain')), + 'amount': self.safe_number(transaction, 'amount'), + 'status': status, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'address': address, + 'addressFrom': None, + 'addressTo': address, + 'tag': None, + 'tagFrom': None, + 'tagTo': None, + 'updated': None, + 'comment': None, + 'fee': None, + 'internal': False, + } + + async def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://arkm.com/docs#get/account/fees + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.subType]: "linear" or "inverse" + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + await self.load_markets() + response = await self.v1PrivateGetAccountFees(params) + # + # { + # "perpMakerFee": "1.23", + # "perpTakerFee": "1.23", + # "spotMakerFee": "1.23", + # "spotTakerFee": "1.23" + # } + # + symbols = list(self.markets.keys()) + result: dict = {} + spotMaker = self.safe_number(response, 'spotMakerFee') + spotTaker = self.safe_number(response, 'spotTakerFee') + perpMaker = self.safe_number(response, 'perpMakerFee') + perpTaker = self.safe_number(response, 'perpTakerFee') + for i in range(0, len(symbols)): + symbol = symbols[i] + market = self.markets[symbol] + result[symbol] = { + 'info': response, + 'symbol': symbol, + } + if market['spot']: + result[symbol]['maker'] = spotMaker + result[symbol]['taker'] = spotTaker + elif market['swap'] or market['future']: + result[symbol]['maker'] = perpMaker + result[symbol]['taker'] = perpTaker + return result + + async def fetch_funding_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch the history of funding payments paid and received on self account + + https://arkm.com/docs#get/account/funding-rate-payments + + :param str [symbol]: unified market symbol + :param int [since]: the earliest time in ms to fetch funding history for + :param int [limit]: the maximum number of funding history structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict: a `funding history structure ` + """ + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + request: dict = {} + if limit is not None: + request['limit'] = limit + response = await self.v1PrivateGetAccountFundingRatePayments(self.extend(request, params)) + # + # [ + # { + # "amount": "20.1", + # "assetSymbol": "BTC", + # "indexPrice": "1.23", + # "pairSymbol": "BTC_USDT", + # "time": 1704067200000000, + # "id": 1, + # "subaccountId": 1, + # "userId": 1 + # }, + # ... + # ] + # + return self.parse_incomes(response, market, since, limit) + + def parse_income(self, income, market: Market = None): + # + # { + # "amount": "20.1", + # "assetSymbol": "BTC", + # "indexPrice": "1.23", + # "pairSymbol": "BTC_USDT", + # "time": 1704067200000000, + # "id": 1, + # "subaccountId": 1, + # "userId": 1 + # } + # + marketId = self.safe_string(income, 'pairSymbol') + currencyId = self.safe_string(income, 'assetSymbol') + timestamp = self.safe_integer_product(income, 'time', 0.001) + return { + 'info': income, + 'symbol': self.safe_symbol(marketId, market), + 'code': self.safe_currency_code(currencyId), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': self.safe_string(income, 'id'), + 'amount': self.safe_number(income, 'amount'), + } + + async def fetch_leverage(self, symbol: str, params={}) -> Leverage: + """ + fetch the set leverage for a market + + https://arkm.com/docs#get/account/leverage + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `leverage structure ` + """ + await self.load_markets() + market = self.market(symbol) + marketId = self.safe_string(market, 'id') + request: dict = { + 'symbol': marketId, + } + response = await self.v1PrivateGetAccountLeverage(self.extend(request, params)) + # + # might be empty if not changed from default value(which is 1x) + # + # [ + # { + # "symbol": "BTC_USDT_PERP", + # "leverage": "7" + # }, + # { + # "symbol": "ETH_USDT_PERP", + # "leverage": "5" + # } + # ] + # + indexed = self.index_by(response, 'symbol') + data = self.safe_dict(indexed, marketId, {}) + return self.parse_leverage(data, market) + + def parse_leverage(self, leverage: dict, market: Market = None) -> Leverage: + # + # { + # "symbol": "ETH_USDT_PERP", + # "leverage": "5" + # } + # + marketId = self.safe_string(leverage, 'symbol') + leverageNum = self.safe_number(leverage, 'leverage') # default leverage is 1 typically + return { + 'info': leverage, + 'symbol': self.safe_symbol(marketId, market), + 'marginMode': None, + 'longLeverage': leverageNum, + 'shortLeverage': leverageNum, + } + + async def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://arkm.com/docs#post/account/leverage + + :param float leverage: the rate of leverage + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setLeverage() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + leverageString = self.number_to_string(leverage) + marketId = self.safe_string(market, 'id') + request: dict = { + 'symbol': marketId, + 'leverage': leverageString, + } + response = await self.v1PrivatePostAccountLeverage(self.extend(request, params)) + # + # response is just empty string + # + return self.parse_leverage(response, market) + + async def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://arkm.com/docs#get/account/positions + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.standard]: whether to fetch standard contract positions + :returns dict[]: a list of `position structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + response = await self.v1PrivateGetAccountPositions(params) + # + # [ + # { + # "subaccountId": "0", + # "symbol": "SOL_USDT_PERP", + # "base": "0.037", + # "quote": "-6.44614", + # "openBuySize": "0", + # "openSellSize": "0", + # "openBuyNotional": "0", + # "openSellNotional": "0", + # "lastUpdateReason": "orderFill", + # "lastUpdateTime": "1753903829389966", + # "lastUpdateId": "250434684", + # "lastUpdateBaseDelta": "0.037", + # "lastUpdateQuoteDelta": "-6.44614", + # "breakEvenPrice": "174.22", + # "markPrice": "174.33", + # "value": "6.45021", + # "pnl": "0.00407", + # "initialMargin": "0.645021", + # "maintenanceMargin": "0.3870126", + # "averageEntryPrice": "174.22" + # } + # ] + # + return self.parse_positions(response, symbols) + + def parse_position(self, position: dict, market: Market = None): + # + # { + # "subaccountId": "0", + # "symbol": "SOL_USDT_PERP", + # "base": "0.037", # negative for short position + # "quote": "-6.44614", # negative for long position + # "openBuySize": "0", + # "openSellSize": "0", + # "openBuyNotional": "0", + # "openSellNotional": "0", + # "lastUpdateReason": "orderFill", + # "lastUpdateTime": "1753903829389966", + # "lastUpdateId": "250434684", + # "lastUpdateBaseDelta": "0.037", + # "lastUpdateQuoteDelta": "-6.44614", + # "breakEvenPrice": "174.22", + # "markPrice": "174.33", + # "value": "6.45021", + # "pnl": "0.00407", + # "initialMargin": "0.645021", + # "maintenanceMargin": "0.3870126", + # "averageEntryPrice": "174.22" + # } + # + base = self.safe_string(position, 'base') + baseAbs = Precise.string_abs(base) + isLong = Precise.string_ge(base, '0') + side = 'long' if isLong else 'short' + marketId = self.safe_string(position, 'symbol') + notional = self.safe_string(position, 'value') + return self.safe_position({ + 'info': position, + 'id': None, + 'symbol': self.safe_symbol(marketId, market), + 'notional': self.parse_number(Precise.string_abs(notional)), + 'marginMode': None, + 'liquidationPrice': None, + 'entryPrice': self.safe_number(position, 'averageEntryPrice'), + 'unrealizedPnl': self.safe_number(position, 'pnl'), + 'realizedPnl': None, + 'percentage': None, + 'contracts': self.parse_number(baseAbs), + 'contractSize': None, + 'markPrice': self.safe_number(position, 'markPrice'), + 'lastPrice': None, + 'side': side, + 'hedged': None, + 'timestamp': None, + 'datetime': None, + 'lastUpdateTimestamp': self.safe_integer(position, 'lastUpdateTime'), + 'maintenanceMargin': self.safe_number(position, 'maintenanceMargin'), + 'maintenanceMarginPercentage': None, + 'collateral': None, + 'initialMargin': self.safe_number(position, 'initialMargin'), + 'initialMarginPercentage': None, + 'leverage': None, + 'marginRatio': None, + 'stopLossPrice': None, + 'takeProfitPrice': None, + }) + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://arkm.com/docs#post/account/withdraw + https://arkm.com/docs#get/account/withdrawal/addresses + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + await self.load_markets() + withdrawalAddresses = await self.v1PrivateGetAccountWithdrawalAddresses() + # + # [ + # { + # "id": "12345", + # "chain": "ETH", + # "address": "0x743f79D65EA07AA222F4a83c10dee4210A920a6e", + # "label": "my_binance", + # "createdAt": "1753905200074355", + # "updatedAt": "1753905213464278", + # "confirmed": True + # } + # ] + # + currency = self.currency(code) + request: dict = { + 'symbol': currency['id'], + 'amount': self.currency_to_precision(code, amount), + 'subaccountId': self.safe_integer(params, 'subAccountId', 0), + } + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + if networkCode is None: + raise ArgumentsRequired(self.id + ' withdraw() requires a "network" param') + indexedList = self.group_by(withdrawalAddresses, 'address') + if not (address in indexedList): + raise InvalidAddress(self.id + ' withdraw() requires an address that has been previously added to the whitelisted addresses') + withdrawalObjects = indexedList[address] + foundWithdrawalObject = None + for i in range(0, len(withdrawalObjects)): + withdrawalObject = withdrawalObjects[i] + if withdrawalObject['chain'] == networkCode: + foundWithdrawalObject = withdrawalObject + break + if foundWithdrawalObject is None: + raise InvalidAddress(self.id + ' withdraw() can not find whitelisted withdrawal address for ' + address + ' with network ' + networkCode) + request['addressId'] = self.safe_integer(foundWithdrawalObject, 'id') + response = await self.v1PrivatePostAccountWithdraw(self.extend(request, params)) + # + # response is a weird string like: + # + # "1234709779980\\n" + # + responseString = response.replace('\n', '') + data = {'id': responseString} + return self.parse_transaction(data, currency) + + async def fetch_leverage_tiers(self, symbols: Strings = None, params={}) -> LeverageTiers: + """ + retrieve information on the maximum leverage, and maintenance margin for trades of varying trade sizes + + https://arkm.com/docs#get/public/margin-schedules + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `leverage tiers structures `, indexed by market symbols + """ + await self.load_markets() + if symbols is None: + raise ArgumentsRequired(self.id + ' fetchLeverageTiers() requires a symbols argument') + symbols = self.market_symbols(symbols) + response = await self.v1PublicGetMarginSchedules(params) + # + # [ + # { + # "name": "A", + # "bands": [ + # { + # "positionLimit": "1000000", + # "leverageRate": "50", + # "marginRate": "0.02", + # "rebate": "0" + # }, + # { + # "positionLimit": "2000000", + # "leverageRate": "25", + # "marginRate": "0.04", + # "rebate": "20000" + # }, + # { + # "positionLimit": "5000000", + # "leverageRate": "20", + # "marginRate": "0.05", + # "rebate": "40000" + # } + # ] + # }, + # { + # "name": "B", + # ... + # + return self.parse_leverage_tiers(response, symbols) + + def parse_leverage_tiers(self, response: Any, symbols: List[str] = None, marketIdKey=None) -> LeverageTiers: + # overloaded method + indexed = self.index_by(response, 'name') + symbols = self.market_symbols(symbols) + tiers = {} + for i in range(0, len(symbols)): + symbol = symbols[i] + market = self.market(symbol) + marginSchedule = self.safe_string(market['info'], 'marginSchedule') + if marginSchedule is None: + raise BadSymbol(self.id + ' fetchLeverageTiers() could not find marginSchedule for ' + symbol) + selectedDict = self.safe_dict(indexed, marginSchedule, {}) + bands = self.safe_list(selectedDict, 'bands', []) + tiers[symbol] = self.parse_market_leverage_tiers(bands, market) + return tiers + + def parse_market_leverage_tiers(self, info, market: Market = None) -> List[LeverageTier]: + tiers = [] + brackets = info + minNotional = 0 + for i in range(0, len(brackets)): + tier = brackets[i] + marketId = self.safe_string(info, 'market') + market = self.safe_market(marketId, market, None, 'swap') + maxNotional = self.safe_number(tier, 'positionLimit') + tiers.append({ + 'tier': self.sum(i, 1), + 'symbol': self.safe_symbol(marketId, market, None, 'swap'), + 'currency': market['base'] if market['linear'] else market['quote'], + 'minNotional': minNotional, + 'maxNotional': maxNotional, + 'maintenanceMarginRate': self.safe_number(tier, 'marginRate'), + 'maxLeverage': self.safe_integer(tier, 'leverageRate'), + 'info': tier, + }) + minNotional = maxNotional + return tiers + + def find_timeframe_by_duration(self, duration: Int) -> str: + # self method is used to find the timeframe by duration in seconds + timeframes = self.safe_dict(self.options, 'timeframeDurations', {}) + keys = list(timeframes.keys()) + for i in range(0, len(keys)): + timeframe = keys[i] + durationInMicroseconds = self.safe_integer(timeframes, timeframe) + if durationInMicroseconds == duration: + return timeframe + return None + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + type = self.safe_string(api, 0) + access = self.safe_string(api, 1) + accessPart = access + '/' if (access == 'public') else '' + query = self.omit(params, self.extract_params(path)) + path = self.implode_params(path, params) + url = self.urls['api'][type] + '/' + accessPart + path + queryString = '' + if method == 'GET': + if query: + queryString = self.urlencode(query) + url += '?' + queryString + if access == 'private': + self.check_required_credentials() + expires = (self.milliseconds() + self.safe_integer(self.options, 'requestExpiration', 5000)) * 1000 # need macroseconds + if method == 'POST': + body = self.json(params) + if queryString != '': + path = path + '?' + queryString + bodyStr = body if (body is not None) else '' + payload = self.apiKey + str(expires) + method.upper() + '/' + path + bodyStr + decodedSecret = self.base64_to_binary(self.secret) + signature = self.hmac(self.encode(payload), decodedSecret, hashlib.sha256, 'base64') + headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Arkham-Api-Key': self.apiKey, + 'Arkham-Expires': str(expires), + 'Arkham-Signature': signature, + 'Arkham-Broker-Id': '1001', + } + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + # + # error example: + # + # { + # "id": "30005", + # "name": "InvalidNotional", + # "message": "order validation failed: invalid notional: notional 0.25 is less than min notional 1" + # } + # + message = self.safe_string(response, 'message') + if message is not None: + errorCode = self.safe_string(response, 'id') + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + raise ExchangeError(self.id + ' ' + body) + return None diff --git a/ccxt/async_support/ascendex.py b/ccxt/async_support/ascendex.py new file mode 100644 index 0000000..184ba83 --- /dev/null +++ b/ccxt/async_support/ascendex.py @@ -0,0 +1,3527 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.ascendex import ImplicitAPI +import asyncio +import hashlib +from ccxt.base.types import Account, Any, Balances, Currencies, Currency, DepositAddress, Int, Leverage, Leverages, LeverageTier, LeverageTiers, MarginMode, MarginModes, MarginModification, Market, Num, Order, OrderBook, OrderRequest, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, FundingRates, Trade, TradingFees, Transaction, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import AccountSuspended +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import NotSupported +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class ascendex(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(ascendex, self).describe(), { + 'id': 'ascendex', + 'name': 'AscendEX', + 'countries': ['SG'], # Singapore + # 8 requests per minute = 0.13333 per second => rateLimit = 750 + # testing 400 works + 'rateLimit': 400, + 'certified': False, + 'pro': True, + # new metainfo interface + 'has': { + 'CORS': None, + 'spot': True, + 'margin': True, + 'swap': True, + 'future': False, + 'option': False, + 'addMargin': True, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'createOrder': True, + 'createOrders': True, + 'createPostOnlyOrder': True, + 'createReduceOnlyOrder': True, + 'createStopLimitOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'fetchAccounts': True, + 'fetchAllGreeks': False, + 'fetchBalance': True, + 'fetchClosedOrders': True, + 'fetchCurrencies': True, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchDepositsWithdrawals': True, + 'fetchDepositWithdrawFee': 'emulated', + 'fetchDepositWithdrawFees': True, + 'fetchFundingHistory': True, + 'fetchFundingRate': 'emulated', + 'fetchFundingRateHistory': False, + 'fetchFundingRates': True, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchLeverage': 'emulated', + 'fetchLeverages': True, + 'fetchLeverageTiers': True, + 'fetchMarginMode': 'emulated', + 'fetchMarginModes': True, + 'fetchMarketLeverageTiers': 'emulated', + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMySettlementHistory': False, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': False, + 'fetchPosition': False, + 'fetchPositionMode': False, + 'fetchPositions': True, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': True, + 'fetchTransactionFee': False, + 'fetchTransactionFees': False, + 'fetchTransactions': 'emulated', + 'fetchTransfer': False, + 'fetchTransfers': False, + 'fetchVolatilityHistory': False, + 'fetchWithdrawal': False, + 'fetchWithdrawals': True, + 'reduceMargin': True, + 'sandbox': True, + 'setLeverage': True, + 'setMarginMode': True, + 'setPositionMode': False, + 'transfer': True, + }, + 'timeframes': { + '1m': '1', + '5m': '5', + '15m': '15', + '30m': '30', + '1h': '60', + '2h': '120', + '4h': '240', + '6h': '360', + '12h': '720', + '1d': '1d', + '1w': '1w', + '1M': '1m', + }, + 'version': 'v2', + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/55bab6b9-d4ca-42a8-a0e6-fac81ae557f1', + 'api': { + 'rest': 'https://ascendex.com', + }, + 'test': { + 'rest': 'https://api-test.ascendex-sandbox.com', + }, + 'www': 'https://ascendex.com', + 'doc': [ + 'https://ascendex.github.io/ascendex-pro-api/#ascendex-pro-api-documentation', + ], + 'fees': 'https://ascendex.com/en/feerate/transactionfee-traderate', + 'referral': { + 'url': 'https://ascendex.com/en-us/register?inviteCode=EL6BXBQM', + 'discount': 0.25, + }, + }, + 'api': { + 'v1': { + 'public': { + 'get': { + 'assets': 1, + 'products': 1, + 'ticker': 1, + 'barhist/info': 1, + 'barhist': 1, + 'depth': 1, + 'trades': 1, + 'cash/assets': 1, # not documented + 'cash/products': 1, # not documented + 'margin/assets': 1, # not documented + 'margin/products': 1, # not documented + 'futures/collateral': 1, + 'futures/contracts': 1, + 'futures/ref-px': 1, + 'futures/market-data': 1, + 'futures/funding-rates': 1, + 'risk-limit-info': 1, + 'exchange-info': 1, + }, + }, + 'private': { + 'get': { + 'info': 1, + 'wallet/transactions': 1, + 'wallet/deposit/address': 1, + 'data/balance/snapshot': 1, + 'data/balance/history': 1, + }, + 'accountCategory': { + 'get': { + 'balance': 1, + 'order/open': 1, + 'order/status': 1, + 'order/hist/current': 1, + 'risk': 1, + }, + 'post': { + 'order': 1, + 'order/batch': 1, + }, + 'delete': { + 'order': 1, + 'order/all': 1, + 'order/batch': 1, + }, + }, + 'accountGroup': { + 'get': { + 'cash/balance': 1, + 'margin/balance': 1, + 'margin/risk': 1, + 'futures/collateral-balance': 1, + 'futures/position': 1, + 'futures/risk': 1, + 'futures/funding-payments': 1, + 'order/hist': 1, + 'spot/fee': 1, + }, + 'post': { + 'transfer': 1, + 'futures/transfer/deposit': 1, + 'futures/transfer/withdraw': 1, + }, + }, + }, + }, + 'v2': { + 'public': { + 'get': { + 'assets': 1, + 'futures/contract': 1, + 'futures/collateral': 1, + 'futures/pricing-data': 1, + 'futures/ticker': 1, + 'risk-limit-info': 1, + }, + }, + 'private': { + 'data': { + 'get': { + 'order/hist': 1, + }, + }, + 'get': { + 'account/info': 1, + }, + 'accountGroup': { + 'get': { + 'order/hist': 1, + 'futures/position': 1, + 'futures/free-margin': 1, + 'futures/order/hist/current': 1, + 'futures/funding-payments': 1, + 'futures/order/open': 1, + 'futures/order/status': 1, + }, + 'post': { + 'futures/isolated-position-margin': 1, + 'futures/margin-type': 1, + 'futures/leverage': 1, + 'futures/transfer/deposit': 1, + 'futures/transfer/withdraw': 1, + 'futures/order': 1, + 'futures/order/batch': 1, + 'futures/order/open': 1, + 'subuser/subuser-transfer': 1, + 'subuser/subuser-transfer-hist': 1, + }, + 'delete': { + 'futures/order': 1, + 'futures/order/batch': 1, + 'futures/order/all': 1, + }, + }, + }, + }, + }, + 'fees': { + 'trading': { + 'feeSide': 'get', + 'tierBased': True, + 'percentage': True, + 'taker': self.parse_number('0.002'), + 'maker': self.parse_number('0.002'), + }, + }, + 'precisionMode': TICK_SIZE, + 'options': { + 'account-category': 'cash', # 'cash', 'margin', 'futures' # obsolete + 'account-group': None, + 'fetchClosedOrders': { + 'method': 'v2PrivateDataGetOrderHist', # 'v1PrivateAccountCategoryGetOrderHistCurrent' + }, + 'defaultType': 'spot', # 'spot', 'margin', 'swap' + 'accountsByType': { + 'spot': 'cash', + 'swap': 'futures', + 'margin': 'margin', + }, + 'transfer': { + 'fillResponseFromRequest': True, + }, + 'networks': { + 'BSC': 'BEP20 ' + '(BSC)', + 'ARB': 'arbitrum', + 'SOL': 'Solana', + 'AVAX': 'avalanche C chain', + 'OMNI': 'Omni', + # 'TRC': 'TRC20', + 'TRC20': 'TRC20', + 'ERC20': 'ERC20', + 'GO20': 'GO20', + 'BEP2': 'BEP2', + 'BTC': 'Bitcoin', + 'BCH': 'Bitcoin ABC', + 'LTC': 'Litecoin', + 'MATIC': 'Matic Network', + 'AKT': 'Akash', + }, + }, + 'features': { + 'default': { + 'sandbox': True, + 'createOrder': { + 'marginMode': True, + 'triggerPrice': True, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, # todo with triggerprice + 'takeProfitPrice': False, # todo with triggerprice + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyRequiresPrice': False, + 'marketBuyByCost': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': { + 'max': 10, + }, + 'fetchMyTrades': None, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'marketType': True, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'marketType': True, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': None, + 'fetchOHLCV': { + 'limit': 500, + }, + }, + 'spot': { + 'extends': 'default', + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 100000, + 'daysBackCanceled': 1, + 'untilDays': 100000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + }, + 'forDerivatives': { + 'extends': 'default', + 'createOrder': { + # todo: implementation + 'attachedStopLossTakeProfit': { + 'triggerPriceType': { + 'last': True, + 'mark': False, + 'index': False, + }, + 'price': False, + }, + }, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': None, + 'daysBackCanceled': None, + 'untilDays': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + }, + 'swap': { + 'linear': { + 'extends': 'forDerivatives', + }, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'exceptions': { + 'exact': { + # not documented + '1900': BadRequest, # {"code":1900,"message":"Invalid Http Request Input"} + '2100': AuthenticationError, # {"code":2100,"message":"ApiKeyFailure"} + '5002': BadSymbol, # {"code":5002,"message":"Invalid Symbol"} + '6001': BadSymbol, # {"code":6001,"message":"Trading is disabled on symbol."} + '6010': InsufficientFunds, # {'code': 6010, 'message': 'Not enough balance.'} + '60060': InvalidOrder, # {'code': 60060, 'message': 'The order is already filled or canceled.'} + '600503': InvalidOrder, # {"code":600503,"message":"Notional is too small."} + # documented + '100001': BadRequest, # INVALID_HTTP_INPUT Http request is invalid + '100002': BadRequest, # DATA_NOT_AVAILABLE Some required data is missing + '100003': BadRequest, # KEY_CONFLICT The same key exists already + '100004': BadRequest, # INVALID_REQUEST_DATA The HTTP request contains invalid field or argument + '100005': BadRequest, # INVALID_WS_REQUEST_DATA Websocket request contains invalid field or argument + '100006': BadRequest, # INVALID_ARGUMENT The arugment is invalid + '100007': BadRequest, # ENCRYPTION_ERROR Something wrong with data encryption + '100008': BadSymbol, # SYMBOL_ERROR Symbol does not exist or not valid for the request + '100009': AuthenticationError, # AUTHORIZATION_NEEDED Authorization is require for the API access or request + '100010': BadRequest, # INVALID_OPERATION The action is invalid or not allowed for the account + '100011': BadRequest, # INVALID_TIMESTAMP Not a valid timestamp + '100012': BadRequest, # INVALID_STR_FORMAT str format does not + '100013': BadRequest, # INVALID_NUM_FORMAT Invalid number input + '100101': ExchangeError, # UNKNOWN_ERROR Some unknown error + '150001': BadRequest, # INVALID_JSON_FORMAT Require a valid json object + '200001': AuthenticationError, # AUTHENTICATION_FAILED Authorization failed + '200002': ExchangeError, # TOO_MANY_ATTEMPTS Tried and failed too many times + '200003': ExchangeError, # ACCOUNT_NOT_FOUND Account not exist + '200004': ExchangeError, # ACCOUNT_NOT_SETUP Account not setup properly + '200005': ExchangeError, # ACCOUNT_ALREADY_EXIST Account already exist + '200006': ExchangeError, # ACCOUNT_ERROR Some error related with error + '200007': ExchangeError, # CODE_NOT_FOUND + '200008': ExchangeError, # CODE_EXPIRED Code expired + '200009': ExchangeError, # CODE_MISMATCH Code does not match + '200010': AuthenticationError, # PASSWORD_ERROR Wrong assword + '200011': ExchangeError, # CODE_GEN_FAILED Do not generate required code promptly + '200012': ExchangeError, # FAKE_COKE_VERIFY + '200013': ExchangeError, # SECURITY_ALERT Provide security alert message + '200014': PermissionDenied, # RESTRICTED_ACCOUNT Account is restricted for certain activity, such, or withdraw. + '200015': PermissionDenied, # PERMISSION_DENIED No enough permission for the operation + '300001': InvalidOrder, # INVALID_PRICE Order price is invalid + '300002': InvalidOrder, # INVALID_QTY Order size is invalid + '300003': InvalidOrder, # INVALID_SIDE Order side is invalid + '300004': InvalidOrder, # INVALID_NOTIONAL Notional is too small or too large + '300005': InvalidOrder, # INVALID_TYPE Order typs is invalid + '300006': InvalidOrder, # INVALID_ORDER_ID Order id is invalid + '300007': InvalidOrder, # INVALID_TIME_IN_FORCE Time In Force in order request is invalid + '300008': InvalidOrder, # INVALID_ORDER_PARAMETER Some order parameter is invalid + '300009': InvalidOrder, # TRADING_VIOLATION Trading violation on account or asset + '300011': InsufficientFunds, # INVALID_BALANCE No enough account or asset balance for the trading + '300012': BadSymbol, # INVALID_PRODUCT Not a valid product supported by exchange + '300013': InvalidOrder, # INVALID_BATCH_ORDER Some or all orders are invalid in batch order request + '300014': InvalidOrder, # {"code":300014,"message":"Order price doesn't conform to the required tick size: 0.1","reason":"TICK_SIZE_VIOLATION"} + '300020': InvalidOrder, # TRADING_RESTRICTED There is some trading restriction on account or asset + '300021': AccountSuspended, # {"code":300021,"message":"Trading disabled for self account.","reason":"TRADING_DISABLED"} + '300031': InvalidOrder, # NO_MARKET_PRICE No market price for market type order trading + '310001': InsufficientFunds, # INVALID_MARGIN_BALANCE No enough margin balance + '310002': InvalidOrder, # INVALID_MARGIN_ACCOUNT Not a valid account for margin trading + '310003': InvalidOrder, # MARGIN_TOO_RISKY Leverage is too high + '310004': BadSymbol, # INVALID_MARGIN_ASSET This asset does not support margin trading + '310005': InvalidOrder, # INVALID_REFERENCE_PRICE There is no valid reference price + '510001': ExchangeError, # SERVER_ERROR Something wrong with server. + '900001': ExchangeError, # HUMAN_CHALLENGE Human change do not pass + }, + 'broad': {}, + }, + 'commonCurrencies': { + 'XBT': 'XBT', # self is not BTC ! just another token + 'BOND': 'BONDED', + 'BTCBEAR': 'BEAR', + 'BTCBULL': 'BULL', + 'BYN': 'BeyondFi', + 'PLN': 'Pollen', + }, + }) + + def get_account(self, params={}): + # get current or provided bitmax sub-account + account = self.safe_value(params, 'account', self.options['account']) + lowercaseAccount = account.lower() + return self.capitalize(lowercaseAccount) + + async def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = await self.v2PublicGetAssets(params) + # + # { + # "code": "0", + # "data": [ + # { + # "assetCode": "USDT", + # "assetName": "Tether", + # "precisionScale": 9, + # "nativeScale": 4, + # "blockChain": [ + # { + # "chainName": "Solana", + # "withdrawFee": "2.0", + # "allowDeposit": True, + # "allowWithdraw": True, + # "minDepositAmt": "0.01", + # "minWithdrawal": "4.0", + # "numConfirmations": 1 + # }, + # ... + # ] + # }, + # ] + # } + # + data = self.safe_list(response, 'data', []) + result: dict = {} + for i in range(0, len(data)): + currency = data[i] + id = self.safe_string(currency, 'assetCode') + code = self.safe_currency_code(id) + chains = self.safe_list(currency, 'blockChain', []) + precision = self.parse_number(self.parse_precision(self.safe_string(currency, 'nativeScale'))) + networks = {} + for j in range(0, len(chains)): + networkEtnry = chains[j] + networkId = self.safe_string(networkEtnry, 'chainName') + networkCode = self.network_code_to_id(networkId) + networks[networkCode] = { + 'fee': self.safe_number(networkEtnry, 'withdrawFee'), + 'active': None, + 'withdraw': self.safe_bool(networkEtnry, 'allowWithdraw'), + 'deposit': self.safe_bool(networkEtnry, 'allowDeposit'), + 'precision': precision, + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': self.safe_number(networkEtnry, 'minWithdrawal'), + 'max': None, + }, + 'deposit': { + 'min': self.safe_number(networkEtnry, 'minDepositAmt'), + 'max': None, + }, + }, + } + # todo type: if chainsLength == 0 and (assetName.endswith(' Staking') or assetName.find(' Reward ') >= 0 or assetName.find('Slot Auction') >= 0 or assetName.find(' Freeze Asset') >= 0): + result[code] = self.safe_currency_structure({ + 'id': id, + 'code': code, + 'info': currency, + 'type': None, + 'margin': None, + 'name': self.safe_string(currency, 'assetName'), + 'active': None, + 'deposit': None, + 'withdraw': None, + 'fee': None, + 'precision': precision, + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': self.safe_number(currency, 'minWithdrawalAmt'), + 'max': None, + }, + }, + 'networks': networks, + }) + return result + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for ascendex + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + spotPromise = self.fetch_spot_markets(params) + contractPromise = self.fetch_contract_markets(params) + spotMarkets, contractMarkets = await asyncio.gather(*[spotPromise, contractPromise]) + return self.array_concat(spotMarkets, contractMarkets) + + async def fetch_spot_markets(self, params={}) -> List[Market]: + productsPromise = self.v1PublicGetProducts(params) + # + # { + # "code": 0, + # "data": [ + # { + # "symbol": "LBA/BTC", + # "baseAsset": "LBA", + # "quoteAsset": "BTC", + # "status": "Normal", + # "minNotional": "0.000625", + # "maxNotional": "6.25", + # "marginTradable": False, + # "commissionType": "Quote", + # "commissionReserveRate": "0.001", + # "tickSize": "0.000000001", + # "lotSize": "1" + # }, + # ] + # } + # + cashPromise = self.v1PublicGetCashProducts(params) + # + # { + # "code": 0, + # "data": [ + # { + # "symbol": "QTUM/BTC", + # "displayName": "QTUM/BTC", + # "domain": "BTC", + # "tradingStartTime": 1569506400000, + # "collapseDecimals": "0.0001,0.000001,0.00000001", + # "minQty": "0.000000001", + # "maxQty": "1000000000", + # "minNotional": "0.000625", + # "maxNotional": "12.5", + # "statusCode": "Normal", + # "statusMessage": "", + # "tickSize": "0.00000001", + # "useTick": False, + # "lotSize": "0.1", + # "useLot": False, + # "commissionType": "Quote", + # "commissionReserveRate": "0.001", + # "qtyScale": 1, + # "priceScale": 8, + # "notionalScale": 4 + # } + # ] + # } + # + products, cash = await asyncio.gather(*[productsPromise, cashPromise]) + productsData = self.safe_list(products, 'data', []) + productsById = self.index_by(productsData, 'symbol') + cashData = self.safe_list(cash, 'data', []) + cashAndPerpetualsById = self.index_by(cashData, 'symbol') + dataById = self.deep_extend(productsById, cashAndPerpetualsById) + ids = list(dataById.keys()) + result = [] + for i in range(0, len(ids)): + id = ids[i] + if id.find('-PERP') >= 0: + continue # skip perpetuals, endpoint returns them + market = dataById[id] + status = self.safe_string(market, 'status') + domain = self.safe_string(market, 'domain') + active = False + if ((status == 'Normal') or (status == 'InternalTrading')) and (domain != 'LeveragedETF'): + active = True + minQty = self.safe_number(market, 'minQty') + maxQty = self.safe_number(market, 'maxQty') + minPrice = self.safe_number(market, 'tickSize') + maxPrice: Num = None + underlying = self.safe_string_2(market, 'underlying', 'symbol') + parts = underlying.split('/') + baseId = self.safe_string(parts, 0) + quoteId = self.safe_string(parts, 1) + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + fee = self.safe_number(market, 'commissionReserveRate') + marginTradable = self.safe_bool(market, 'marginTradable', False) + result.append({ + 'id': id, + 'symbol': base + '/' + quote, + 'base': base, + 'baseId': baseId, + 'quote': quote, + 'quoteId': quoteId, + 'settle': None, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': marginTradable, + 'swap': False, + 'future': False, + 'option': False, + 'active': active, + 'contract': False, + 'linear': None, + 'inverse': None, + 'taker': fee, + 'maker': fee, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.safe_number(market, 'lotSize'), + 'price': self.safe_number(market, 'tickSize'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': minQty, + 'max': maxQty, + }, + 'price': { + 'min': minPrice, + 'max': maxPrice, + }, + 'cost': { + 'min': self.safe_number(market, 'minNotional'), + 'max': self.safe_number(market, 'maxNotional'), + }, + }, + 'created': self.safe_integer(market, 'tradingStartTime'), + 'info': market, + }) + return result + + async def fetch_contract_markets(self, params={}) -> List[Market]: + contracts = await self.v2PublicGetFuturesContract(params) + # + # { + # "code": 0, + # "data": [ + # { + # "symbol": "BTC-PERP", + # "status": "Normal", + # "displayName": "BTCUSDT", + # "settlementAsset": "USDT", + # "underlying": "BTC/USDT", + # "tradingStartTime": 1579701600000, + # "priceFilter": { + # "minPrice": "0.1", + # "maxPrice": "1000000", + # "tickSize": "0.1" + # }, + # "lotSizeFilter": { + # "minQty": "0.0001", + # "maxQty": "1000000000", + # "lotSize": "0.0001" + # }, + # "commissionType": "Quote", + # "commissionReserveRate": "0.001", + # "marketOrderPriceMarkup": "0.03", + # "marginRequirements": [ + # { + # "positionNotionalLowerBound": "0", + # "positionNotionalUpperBound": "50000", + # "initialMarginRate": "0.01", + # "maintenanceMarginRate": "0.006" + # }, + # ... + # ] + # } + # ] + # } + # + data = self.safe_list(contracts, 'data', []) + result = [] + for i in range(0, len(data)): + market = data[i] + id = self.safe_string(market, 'symbol') + underlying = self.safe_string(market, 'underlying') + parts = underlying.split('/') + baseId = self.safe_string(parts, 0) + base = self.safe_currency_code(baseId) + quoteId = self.safe_string(parts, 1) + quote = self.safe_currency_code(quoteId) + settleId = self.safe_string(market, 'settlementAsset') + settle = self.safe_currency_code(settleId) + linear = settle == quote + inverse = settle == base + symbol = base + '/' + quote + ':' + settle + priceFilter = self.safe_dict(market, 'priceFilter') + lotSizeFilter = self.safe_dict(market, 'lotSizeFilter') + fee = self.safe_number(market, 'commissionReserveRate') + result.append({ + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': 'swap', + 'spot': False, + 'margin': None, + 'swap': True, + 'future': False, + 'option': False, + 'active': self.safe_string(market, 'status') == 'Normal', + 'contract': True, + 'linear': linear, + 'inverse': inverse, + 'taker': fee, + 'maker': fee, + 'contractSize': self.parse_number('1'), + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.safe_number(lotSizeFilter, 'lotSize'), + 'price': self.safe_number(priceFilter, 'tickSize'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(lotSizeFilter, 'minQty'), + 'max': self.safe_number(lotSizeFilter, 'maxQty'), + }, + 'price': { + 'min': self.safe_number(priceFilter, 'minPrice'), + 'max': self.safe_number(priceFilter, 'maxPrice'), + }, + 'cost': { + 'min': self.safe_number(market, 'minNotional'), + 'max': self.safe_number(market, 'maxNotional'), + }, + }, + 'created': self.safe_integer(market, 'tradingStartTime'), + 'info': market, + }) + return result + + async def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the ascendex server + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the ascendex server + """ + request: dict = { + 'requestTime': self.milliseconds(), + } + response = await self.v1PublicGetExchangeInfo(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "requestTimeEcho": 1656560463601, + # "requestReceiveAt": 1656560464331, + # "latency": 730 + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.safe_integer(data, 'requestReceiveAt') + + async def fetch_accounts(self, params={}) -> List[Account]: + """ + fetch all the accounts associated with a profile + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `account structures ` indexed by the account type + """ + accountGroup = self.safe_string(self.options, 'account-group') + response = None + if accountGroup is None: + response = await self.v1PrivateGetInfo(params) + # + # { + # "code":0, + # "data":{ + # "email":"igor.kroitor@gmail.com", + # "accountGroup":8, + # "viewPermission":true, + # "tradePermission":true, + # "transferPermission":true, + # "cashAccount":["cshrHKLZCjlZ2ejqkmvIHHtPmLYqdnda"], + # "marginAccount":["martXoh1v1N3EMQC5FDtSj5VHso8aI2Z"], + # "futuresAccount":["futc9r7UmFJAyBY2rE3beA2JFxav2XFF"], + # "userUID":"U6491137460" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + accountGroup = self.safe_string(data, 'accountGroup') + self.options['account-group'] = accountGroup + return [ + { + 'id': accountGroup, + 'type': None, + 'code': None, + 'info': response, + }, + ] + + def parse_balance(self, response) -> Balances: + result: dict = { + 'info': response, + 'timestamp': None, + 'datetime': None, + } + balances = self.safe_list(response, 'data', []) + for i in range(0, len(balances)): + balance = balances[i] + code = self.safe_currency_code(self.safe_string(balance, 'asset')) + account = self.account() + account['free'] = self.safe_string(balance, 'availableBalance') + account['total'] = self.safe_string(balance, 'totalBalance') + result[code] = account + return self.safe_balance(result) + + def parse_margin_balance(self, response): + result: dict = { + 'info': response, + 'timestamp': None, + 'datetime': None, + } + balances = self.safe_list(response, 'data', []) + for i in range(0, len(balances)): + balance = balances[i] + code = self.safe_currency_code(self.safe_string(balance, 'asset')) + account = self.account() + account['free'] = self.safe_string(balance, 'availableBalance') + account['total'] = self.safe_string(balance, 'totalBalance') + debt = self.safe_string(balance, 'borrowed') + interest = self.safe_string(balance, 'interest') + account['debt'] = Precise.string_add(debt, interest) + result[code] = account + return self.safe_balance(result) + + def parse_swap_balance(self, response): + result: dict = { + 'info': response, + 'timestamp': None, + 'datetime': None, + } + data = self.safe_dict(response, 'data', {}) + collaterals = self.safe_list(data, 'collaterals', []) + for i in range(0, len(collaterals)): + balance = collaterals[i] + code = self.safe_currency_code(self.safe_string(balance, 'asset')) + account = self.account() + account['total'] = self.safe_string(balance, 'balance') + result[code] = account + return self.safe_balance(result) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://ascendex.github.io/ascendex-pro-api/#cash-account-balance + https://ascendex.github.io/ascendex-pro-api/#margin-account-balance + https://ascendex.github.io/ascendex-futures-pro-api-v2/#position + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: wallet type, 'spot', 'margin', or 'swap' + :param str [params.marginMode]: 'cross' or None, for spot margin trading, value of 'isolated' is invalid + :returns dict: a `balance structure ` + """ + await self.load_markets() + await self.load_accounts() + marketType = None + marginMode = None + marketType, params = self.handle_market_type_and_params('fetchBalance', None, params) + marginMode, params = self.handle_margin_mode_and_params('fetchBalance', params) + isMargin = self.safe_bool(params, 'margin', False) + isCross = marginMode == 'cross' + marketType = 'margin' if (isMargin or isCross) else marketType + params = self.omit(params, 'margin') + accountsByType = self.safe_dict(self.options, 'accountsByType', {}) + accountCategory = self.safe_string(accountsByType, marketType, 'cash') + account = self.safe_dict(self.accounts, 0, {}) + accountGroup = self.safe_string(account, 'id') + request: dict = { + 'account-group': accountGroup, + } + if (marginMode == 'isolated') and (marketType != 'swap'): + raise BadRequest(self.id + ' does not supported isolated margin trading') + if (accountCategory == 'cash') or (accountCategory == 'margin'): + request['account-category'] = accountCategory + response = None + if (marketType == 'spot') or (marketType == 'margin'): + response = await self.v1PrivateAccountCategoryGetBalance(self.extend(request, params)) + elif marketType == 'swap': + response = await self.v2PrivateAccountGroupGetFuturesPosition(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchBalance() is not currently supported for ' + marketType + ' markets') + # + # cash + # + # { + # "code": 0, + # "data": [ + # { + # "asset": "BCHSV", + # "totalBalance": "64.298000048", + # "availableBalance": "64.298000048", + # }, + # ] + # } + # + # margin + # + # { + # "code": 0, + # "data": [ + # { + # "asset": "BCHSV", + # "totalBalance": "64.298000048", + # "availableBalance": "64.298000048", + # "borrowed": "0", + # "interest": "0", + # }, + # ] + # } + # + # swap + # + # { + # "code": 0, + # "data": { + # "accountId": "fut2ODPhGiY71Pl4vtXnOZ00ssgD7QGn", + # "ac": "FUTURES", + # "collaterals": [ + # {"asset":"ADA","balance":"0.355803","referencePrice":"1.05095","discountFactor":"0.9"}, + # {"asset":"USDT","balance":"0.000014519","referencePrice":"1","discountFactor":"1"} + # ], + # }j + # } + # + if marketType == 'swap': + return self.parse_swap_balance(response) + elif marketType == 'margin': + return self.parse_margin_balance(response) + else: + return self.parse_balance(response) + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = await self.v1PublicGetDepth(self.extend(request, params)) + # + # { + # "code":0, + # "data":{ + # "m":"depth-snapshot", + # "symbol":"BTC-PERP", + # "data":{ + # "ts":1590223998202, + # "seqnum":115444921, + # "asks":[ + # ["9207.5","18.2383"], + # ["9207.75","18.8235"], + # ["9208","10.7873"], + # ], + # "bids":[ + # ["9207.25","0.4009"], + # ["9207","0.003"], + # ["9206.5","0.003"], + # ] + # } + # } + # } + # + data = self.safe_dict(response, 'data', {}) + orderbook = self.safe_dict(data, 'data', {}) + timestamp = self.safe_integer(orderbook, 'ts') + result = self.parse_order_book(orderbook, symbol, timestamp) + result['nonce'] = self.safe_integer(orderbook, 'seqnum') + return result + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "symbol":"QTUM/BTC", + # "open":"0.00016537", + # "close":"0.00019077", + # "high":"0.000192", + # "low":"0.00016537", + # "volume":"846.6", + # "ask":["0.00018698","26.2"], + # "bid":["0.00018408","503.7"], + # "type":"spot" + # } + # + timestamp = None + marketId = self.safe_string(ticker, 'symbol') + type = self.safe_string(ticker, 'type') + delimiter = '/' if (type == 'spot') else None + symbol = self.safe_symbol(marketId, market, delimiter) + close = self.safe_string(ticker, 'close') + bid = self.safe_list(ticker, 'bid', []) + ask = self.safe_list(ticker, 'ask', []) + open = self.safe_string(ticker, 'open') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': None, + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': self.safe_string(bid, 0), + 'bidVolume': self.safe_string(bid, 1), + 'ask': self.safe_string(ask, 0), + 'askVolume': self.safe_string(ask, 1), + 'vwap': None, + 'open': open, + 'close': close, + 'last': close, + 'previousClose': None, # previous day close + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': self.safe_string(ticker, 'volume'), + 'quoteVolume': None, + 'info': ticker, + }, market) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = await self.v1PublicGetTicker(self.extend(request, params)) + # + # { + # "code":0, + # "data":{ + # "symbol":"BTC-PERP", # or "BTC/USDT" + # "open":"9073", + # "close":"9185.75", + # "high":"9185.75", + # "low":"9185.75", + # "volume":"576.8334", + # "ask":["9185.75","15.5863"], + # "bid":["9185.5","0.003"], + # "type":"derivatives", # or "spot" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_ticker(data, market) + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://ascendex.github.io/ascendex-pro-api/#ticker + https://ascendex.github.io/ascendex-futures-pro-api-v2/#ticker + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + request: dict = {} + market = None + if symbols is not None: + symbol = self.safe_string(symbols, 0) + market = self.market(symbol) + marketIds = self.market_ids(symbols) + request['symbol'] = ','.join(marketIds) + type = None + type, params = self.handle_market_type_and_params('fetchTickers', market, params) + response = None + if type == 'spot': + response = await self.v1PublicGetTicker(self.extend(request, params)) + else: + response = await self.v2PublicGetFuturesTicker(self.extend(request, params)) + # + # { + # "code":0, + # "data": { + # "symbol":"QTUM/BTC", + # "open":"0.00016537", + # "close":"0.00019077", + # "high":"0.000192", + # "low":"0.00016537", + # "volume":"846.6", + # "ask":["0.00018698","26.2"], + # "bid":["0.00018408","503.7"], + # "type":"spot" + # } + # } + # + data = self.safe_list(response, 'data', []) + if not isinstance(data, list): + return self.parse_tickers([data], symbols) + return self.parse_tickers(data, symbols) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # { + # "m":"bar", + # "s":"BTC/USDT", + # "data":{ + # "i":"1", + # "ts":1590228000000, + # "o":"9139.59", + # "c":"9131.94", + # "h":"9139.99", + # "l":"9121.71", + # "v":"25.20648" + # } + # } + # + data = self.safe_dict(ohlcv, 'data', {}) + return [ + self.safe_integer(data, 'ts'), + self.safe_number(data, 'o'), + self.safe_number(data, 'h'), + self.safe_number(data, 'l'), + self.safe_number(data, 'c'), + self.safe_number(data, 'v'), + ] + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest candle to fetch + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'interval': self.safe_string(self.timeframes, timeframe, timeframe), + } + # if since and limit are not specified + # the exchange will return just 1 last candle by default + duration = self.parse_timeframe(timeframe) + options = self.safe_dict(self.options, 'fetchOHLCV', {}) + defaultLimit = self.safe_integer(options, 'limit', 500) + until = self.safe_integer(params, 'until') + if since is not None: + request['from'] = since + if limit is None: + limit = defaultLimit + else: + limit = min(limit, defaultLimit) + toWithLimit = self.sum(since, limit * duration * 1000, 1) + if until is not None: + request['to'] = min(toWithLimit, until + 1) + else: + request['to'] = toWithLimit + elif until is not None: + request['to'] = until + 1 + if limit is None: + limit = defaultLimit + else: + limit = min(limit, defaultLimit) + request['from'] = until - (limit * duration * 1000) + elif limit is not None: + request['n'] = limit # max 500 + params = self.omit(params, 'until') + response = await self.v1PublicGetBarhist(self.extend(request, params)) + # + # { + # "code":0, + # "data":[ + # { + # "m":"bar", + # "s":"BTC/USDT", + # "data":{ + # "i":"1", + # "ts":1590228000000, + # "o":"9139.59", + # "c":"9131.94", + # "h":"9139.99", + # "l":"9121.71", + # "v":"25.20648" + # } + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_ohlcvs(data, market, timeframe, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # public fetchTrades + # + # { + # "p":"9128.5", # price + # "q":"0.0030", # quantity + # "ts":1590229002385, # timestamp + # "bm":false, # if True, the buyer is the market maker, we only use self field to "define the side" of a public trade + # "seqnum":180143985289898554 + # } + # + timestamp = self.safe_integer(trade, 'ts') + priceString = self.safe_string_2(trade, 'price', 'p') + amountString = self.safe_string(trade, 'q') + buyerIsMaker = self.safe_bool(trade, 'bm', False) + side = 'sell' if buyerIsMaker else 'buy' + market = self.safe_market(None, market) + return self.safe_trade({ + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'id': None, + 'order': None, + 'type': None, + 'takerOrMaker': None, + 'side': side, + 'price': priceString, + 'amount': amountString, + 'cost': None, + 'fee': None, + }, market) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://ascendex.github.io/ascendex-pro-api/#market-trades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['n'] = limit # max 100 + response = await self.v1PublicGetTrades(self.extend(request, params)) + # + # { + # "code":0, + # "data":{ + # "m":"trades", + # "symbol":"BTC-PERP", + # "data":[ + # {"p":"9128.5","q":"0.0030","ts":1590229002385,"bm":false,"seqnum":180143985289898554}, + # {"p":"9129","q":"0.0030","ts":1590229002642,"bm":false,"seqnum":180143985289898587}, + # {"p":"9129.5","q":"0.0030","ts":1590229021306,"bm":false,"seqnum":180143985289899043} + # ] + # } + # } + # + records = self.safe_dict(response, 'data', {}) + trades = self.safe_list(records, 'data', []) + return self.parse_trades(trades, market, since, limit) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'PendingNew': 'open', + 'New': 'open', + 'PartiallyFilled': 'open', + 'Filled': 'closed', + 'Canceled': 'canceled', + 'Rejected': 'rejected', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder + # + # { + # "id": "16e607e2b83a8bXHbAwwoqDo55c166fa", + # "orderId": "16e85b4d9b9a8bXHbAwwoqDoc3d66830", + # "orderType": "Market", + # "symbol": "BTC/USDT", + # "timestamp": 1573576916201 + # } + # + # & linear(fetchClosedOrders) + # + # { + # "ac": "FUTURES", + # "accountId": "fut2ODPhGiY71Pl4vtXnOZ00ssgD7QGn", + # "time": 1640819389454, + # "orderId": "a17e0874ecbdU0711043490bbtcpDU5X", + # "seqNum": -1, + # "orderType": "Limit", + # "execInst": "NULL_VAL", # NULL_VAL, ReduceOnly , ... + # "side": "Buy", + # "symbol": "BTC-PERP", + # "price": "30000", + # "orderQty": "0.002", + # "stopPrice": "0", + # "stopBy": "ref-px", + # "status": "Ack", + # "lastExecTime": 1640819389454, + # "lastQty": "0", + # "lastPx": "0", + # "avgFilledPx": "0", + # "cumFilledQty": "0", + # "fee": "0", + # "cumFee": "0", + # "feeAsset": "", + # "errorCode": "", + # "posStopLossPrice": "0", + # "posStopLossTrigger": "market", + # "posTakeProfitPrice": "0", + # "posTakeProfitTrigger": "market", + # "liquidityInd": "n" + # } + # + # fetchOrder, fetchOpenOrders, fetchClosedOrders + # + # { + # "symbol": "BTC/USDT", + # "price": "8131.22", + # "orderQty": "0.00082", + # "orderType": "Market", + # "avgPx": "7392.02", + # "cumFee": "0.005152238", + # "cumFilledQty": "0.00082", + # "errorCode": "", + # "feeAsset": "USDT", + # "lastExecTime": 1575953151764, + # "orderId": "a16eee20b6750866943712zWEDdAjt3", + # "seqNum": 2623469, + # "side": "Buy", + # "status": "Filled", + # "stopPrice": "", + # "execInst": "NULL_VAL" # "Post"(for postOnly orders), "reduceOnly"(for reduceOnly orders) + # } + # + # { + # "orderId": "a173ad938fc3U22666567717788c3b66", # orderId + # "seqNum": 18777366360, # sequence number + # "accountId": "cshwSjbpPjSwHmxPdz2CPQVU9mnbzPpt", # accountId + # "symbol": "BTC/USDT", # symbol + # "orderType": "Limit", # order type(Limit/Market/StopMarket/StopLimit) + # "side": "Sell", # order side(Buy/Sell) + # "price": "11346.77", # order price + # "stopPrice": "0", # stop price(0 by default) + # "orderQty": "0.01", # order quantity(in base asset) + # "status": "Canceled", # order status(Filled/Canceled/Rejected) + # "createTime": 1596344995793, # order creation time + # "lastExecTime": 1596344996053, # last execution time + # "avgFillPrice": "11346.77", # average filled price + # "fillQty": "0.01", # filled quantity(in base asset) + # "fee": "-0.004992579", # cummulative fee. if negative, self value is the commission charged; if possitive, self value is the rebate received. + # "feeAsset": "USDT" # fee asset + # } + # + # { + # "ac": "FUTURES", + # "accountId": "testabcdefg", + # "avgPx": "0", + # "cumFee": "0", + # "cumQty": "0", + # "errorCode": "NULL_VAL", + # "execInst": "NULL_VAL", + # "feeAsset": "USDT", + # "lastExecTime": 1584072844085, + # "orderId": "r170d21956dd5450276356bbtcpKa74", + # "orderQty": "1.1499", + # "orderType": "Limit", + # "price": "4000", + # "sendingTime": 1584072841033, + # "seqNum": 24105338, + # "side": "Buy", + # "status": "Canceled", + # "stopPrice": "", + # "symbol": "BTC-PERP" + # }, + # + status = self.parse_order_status(self.safe_string(order, 'status')) + marketId = self.safe_string(order, 'symbol') + symbol = self.safe_symbol(marketId, market, '/') + timestamp = self.safe_integer_n(order, ['timestamp', 'sendingTime', 'time']) + lastTradeTimestamp = self.safe_integer(order, 'lastExecTime') + if timestamp is None: + timestamp = lastTradeTimestamp + price = self.safe_string(order, 'price') + amount = self.safe_string(order, 'orderQty') + average = self.safe_string_2(order, 'avgPx', 'avgFilledPx') + filled = self.safe_string_n(order, ['cumFilledQty', 'cumQty', 'fillQty']) + id = self.safe_string(order, 'orderId') + clientOrderId = self.safe_string(order, 'id') + if clientOrderId is not None: + if len(clientOrderId) < 1: + clientOrderId = None + rawTypeLower = self.safe_string_lower(order, 'orderType') + type = rawTypeLower + if rawTypeLower is not None: + if rawTypeLower == 'stoplimit': + type = 'limit' + if rawTypeLower == 'stopmarket': + type = 'market' + side = self.safe_string_lower(order, 'side') + feeCost = self.safe_number_2(order, 'cumFee', 'fee') + fee = None + if feeCost is not None: + feeCurrencyId = self.safe_string(order, 'feeAsset') + feeCurrencyCode = self.safe_currency_code(feeCurrencyId) + fee = { + 'cost': feeCost, + 'currency': feeCurrencyCode, + } + triggerPrice = self.omit_zero(self.safe_string(order, 'stopPrice')) + reduceOnly = None + execInst = self.safe_string_lower(order, 'execInst') + if execInst == 'reduceonly': + reduceOnly = True + postOnly = None + if execInst == 'post': + postOnly = True + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': lastTradeTimestamp, + 'symbol': symbol, + 'type': type, + 'timeInForce': None, + 'postOnly': postOnly, + 'reduceOnly': reduceOnly, + 'side': side, + 'price': price, + 'triggerPrice': triggerPrice, + 'amount': amount, + 'cost': None, + 'average': average, + 'filled': filled, + 'remaining': None, + 'status': status, + 'fee': fee, + 'trades': None, + }, market) + + async def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + await self.load_markets() + await self.load_accounts() + account = self.safe_dict(self.accounts, 0, {}) + accountGroup = self.safe_string(account, 'id') + request: dict = { + 'account-group': accountGroup, + } + response = await self.v1PrivateAccountGroupGetSpotFee(self.extend(request, params)) + # + # { + # "code": "0", + # "data": { + # "domain": "spot", + # "userUID": "U1479576457", + # "vipLevel": "0", + # "fees": [ + # {symbol: 'HT/USDT', fee: {taker: '0.001', maker: "0.001"}}, + # {symbol: 'LAMB/BTC', fee: {taker: '0.002', maker: "0.002"}}, + # {symbol: 'STOS/USDT', fee: {taker: '0.002', maker: "0.002"}}, + # ... + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + fees = self.safe_list(data, 'fees', []) + result: dict = {} + for i in range(0, len(fees)): + fee = fees[i] + marketId = self.safe_string(fee, 'symbol') + symbol = self.safe_symbol(marketId, None, '/') + takerMaker = self.safe_dict(fee, 'fee', {}) + result[symbol] = { + 'info': fee, + 'symbol': symbol, + 'maker': self.safe_number(takerMaker, 'maker'), + 'taker': self.safe_number(takerMaker, 'taker'), + 'percentage': None, + 'tierBased': None, + } + return result + + def create_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + @ignore + helper function to build request + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.timeInForce]: "GTC", "IOC", "FOK", or "PO" + :param bool [params.postOnly]: True or False + :param float [params.triggerPrice]: the price at which a trigger order is triggered at + :returns dict: request to be sent to the exchange + """ + market = self.market(symbol) + marginMode = None + marketType = None + marginMode, params = self.handle_margin_mode_and_params('createOrderRequest', params) + marketType, params = self.handle_market_type_and_params('createOrderRequest', market, params) + accountsByType = self.safe_dict(self.options, 'accountsByType', {}) + accountCategory = self.safe_string(accountsByType, marketType, 'cash') + if marginMode is not None: + accountCategory = 'margin' + account = self.safe_dict(self.accounts, 0, {}) + accountGroup = self.safe_string(account, 'id') + clientOrderId = self.safe_string_2(params, 'clientOrderId', 'id') + request: dict = { + 'account-group': accountGroup, + 'account-category': accountCategory, + 'symbol': market['id'], + 'time': self.milliseconds(), + 'orderQty': self.amount_to_precision(symbol, amount), + 'orderType': type, # limit, market, stop_market, stop_limit + 'side': side, # buy or sell, + # 'execInst': # Post for postOnly, ReduceOnly for reduceOnly + # 'respInst': 'ACK', # ACK, 'ACCEPT, DONE + } + isMarketOrder = ((type == 'market') or (type == 'stop_market')) + isLimitOrder = ((type == 'limit') or (type == 'stop_limit')) + timeInForce = self.safe_string(params, 'timeInForce') + postOnly = self.is_post_only(isMarketOrder, False, params) + reduceOnly = self.safe_bool(params, 'reduceOnly', False) + triggerPrice = self.safe_string_2(params, 'triggerPrice', 'stopPrice') + if isLimitOrder: + request['orderPrice'] = self.price_to_precision(symbol, price) + if timeInForce == 'IOC': + request['timeInForce'] = 'IOC' + if timeInForce == 'FOK': + request['timeInForce'] = 'FOK' + if postOnly: + request['postOnly'] = True + if triggerPrice is not None: + request['stopPrice'] = self.price_to_precision(symbol, triggerPrice) + if isLimitOrder: + request['orderType'] = 'stop_limit' + elif isMarketOrder: + request['orderType'] = 'stop_market' + if clientOrderId is not None: + request['id'] = clientOrderId + if market['spot']: + if accountCategory is not None: + request['category'] = accountCategory + else: + request['account-category'] = accountCategory + if reduceOnly: + request['execInst'] = 'ReduceOnly' + if postOnly: + request['execInst'] = 'Post' + params = self.omit(params, ['reduceOnly', 'triggerPrice']) + return self.extend(request, params) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order on the exchange + + https://ascendex.github.io/ascendex-pro-api/#place-order + https://ascendex.github.io/ascendex-futures-pro-api-v2/#new-order + + :param str symbol: unified CCXT market symbol + :param str type: "limit" or "market" + :param str side: "buy" or "sell" + :param float amount: the amount of currency to trade + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.timeInForce]: "GTC", "IOC", "FOK", or "PO" + :param bool [params.postOnly]: True or False + :param float [params.triggerPrice]: the price at which a trigger order is triggered at + :param dict [params.takeProfit]: *takeProfit object in params* containing the triggerPrice that the attached take profit order will be triggered(perpetual swap markets only) + :param float [params.takeProfit.triggerPrice]: *swap only* take profit trigger price + :param dict [params.stopLoss]: *stopLoss object in params* containing the triggerPrice that the attached stop loss order will be triggered(perpetual swap markets only) + :param float [params.stopLoss.triggerPrice]: *swap only* stop loss trigger price + :returns: `An order structure ` + """ + await self.load_markets() + await self.load_accounts() + market = self.market(symbol) + request = self.create_order_request(symbol, type, side, amount, price, params) + response = None + if market['swap']: + response = await self.v2PrivateAccountGroupPostFuturesOrder(request) + else: + response = await self.v1PrivateAccountCategoryPostOrder(request) + # + # spot + # + # { + # "code":0, + # "data": { + # "accountId":"cshwT8RKojkT1HoaA5UdeimR2SrmHG2I", + # "ac":"CASH", + # "action":"place-order", + # "status":"Ack", + # "info": { + # "symbol":"TRX/USDT", + # "orderType":"StopLimit", + # "timestamp":1654290662172, + # "id":"", + # "orderId":"a1812b6840ddU8191168955av0k6Eyhj" + # } + # } + # } + # + # swap + # + # { + # "code":0, + # "data": { + # "meta": { + # "id":"", + # "action":"place-order", + # "respInst":"ACK" + # }, + # "order": { + # "ac":"FUTURES", + # "accountId":"futwT8RKojkT1HoaA5UdeimR2SrmHG2I", + # "time":1654290969965, + # "orderId":"a1812b6cf322U8191168955oJamfTh7b", + # "seqNum":-1, + # "orderType":"StopLimit", + # "execInst":"NULL_VAL", + # "side":"Buy", + # "symbol":"TRX-PERP", + # "price":"0.083", + # "orderQty":"1", + # "stopPrice":"0.082", + # "stopBy":"ref-px", + # "status":"Ack", + # "lastExecTime":1654290969965, + # "lastQty":"0", + # "lastPx":"0", + # "avgFilledPx":"0", + # "cumFilledQty":"0", + # "fee":"0", + # "cumFee":"0", + # "feeAsset":"", + # "errorCode":"", + # "posStopLossPrice":"0", + # "posStopLossTrigger":"market", + # "posTakeProfitPrice":"0", + # "posTakeProfitTrigger":"market", + # "liquidityInd":"n" + # } + # } + # } + # + data = self.safe_dict(response, 'data', {}) + order = self.safe_dict_2(data, 'order', 'info', {}) + return self.parse_order(order, market) + + async def create_orders(self, orders: List[OrderRequest], params={}): + """ + create a list of trade orders + + https://ascendex.github.io/ascendex-pro-api/#place-batch-orders + https://ascendex.github.io/ascendex-futures-pro-api-v2/#place-batch-orders + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.timeInForce]: "GTC", "IOC", "FOK", or "PO" + :param bool [params.postOnly]: True or False + :param float [params.triggerPrice]: the price at which a trigger order is triggered at + :returns dict: an `order structure ` + """ + await self.load_markets() + await self.load_accounts() + ordersRequests = [] + symbol = None + marginMode = None + for i in range(0, len(orders)): + rawOrder = orders[i] + marketId = self.safe_string(rawOrder, 'symbol') + if symbol is None: + symbol = marketId + else: + if symbol != marketId: + raise BadRequest(self.id + ' createOrders() requires all orders to have the same symbol') + type = self.safe_string(rawOrder, 'type') + side = self.safe_string(rawOrder, 'side') + amount = self.safe_number(rawOrder, 'amount') + price = self.safe_number(rawOrder, 'price') + orderParams = self.safe_dict(rawOrder, 'params', {}) + marginResult = self.handle_margin_mode_and_params('createOrders', orderParams) + currentMarginMode = marginResult[0] + if currentMarginMode is not None: + if marginMode is None: + marginMode = currentMarginMode + else: + if marginMode != currentMarginMode: + raise BadRequest(self.id + ' createOrders() requires all orders to have the same margin mode(isolated or cross)') + orderRequest = self.create_order_request(marketId, type, side, amount, price, orderParams) + ordersRequests.append(orderRequest) + market = self.market(symbol) + accountsByType = self.safe_dict(self.options, 'accountsByType', {}) + accountCategory = self.safe_string(accountsByType, market['type'], 'cash') + if marginMode is not None: + accountCategory = 'margin' + account = self.safe_dict(self.accounts, 0, {}) + accountGroup = self.safe_string(account, 'id') + request: dict = {} + response = None + if market['swap']: + raise NotSupported(self.id + ' createOrders() is not currently supported for swap markets on ascendex') + # request['account-group'] = accountGroup + # request['category'] = accountCategory + # request['orders'] = ordersRequests + # response = await self.v2PrivateAccountGroupPostFuturesOrderBatch(request) + else: + request['account-group'] = accountGroup + request['account-category'] = accountCategory + request['orders'] = ordersRequests + response = await self.v1PrivateAccountCategoryPostOrderBatch(request) + # + # spot + # + # { + # "code": 0, + # "data": { + # "accountId": "cshdAKBO43TKIh2kJtq7FVVb42KIePyS", + # "ac": "CASH", + # "action": "batch-place-order", + # "status": "Ack", + # "info": [ + # { + # "symbol": "BTC/USDT", + # "orderType": "Limit", + # "timestamp": 1699326589344, + # "id": "", + # "orderId": "a18ba7c1f6efU0711043490p3HvjjN5x" + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + info = self.safe_list(data, 'info', []) + return self.parse_orders(info, market) + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://ascendex.github.io/ascendex-pro-api/#query-order + https://ascendex.github.io/ascendex-futures-pro-api-v2/#query-order-by-id + + :param str id: the order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + await self.load_accounts() + market = None + if symbol is not None: + market = self.market(symbol) + type, query = self.handle_market_type_and_params('fetchOrder', market, params) + accountsByType = self.safe_dict(self.options, 'accountsByType', {}) + accountCategory = self.safe_string(accountsByType, type, 'cash') + account = self.safe_dict(self.accounts, 0, {}) + accountGroup = self.safe_string(account, 'id') + request: dict = { + 'account-group': accountGroup, + 'account-category': accountCategory, + 'orderId': id, + } + response = None + if (type == 'spot') or (type == 'margin'): + response = await self.v1PrivateAccountCategoryGetOrderStatus(self.extend(request, query)) + elif type == 'swap': + request['account-category'] = accountCategory + response = await self.v2PrivateAccountGroupGetFuturesOrderStatus(self.extend(request, query)) + else: + raise NotSupported(self.id + ' fetchOrder() is not currently supported for ' + type + ' markets') + # + # AccountCategoryGetOrderStatus + # + # { + # "code": 0, + # "accountCategory": "CASH", + # "accountId": "cshQtyfq8XLAA9kcf19h8bXHbAwwoqDo", + # "data": [ + # { + # "symbol": "BTC/USDT", + # "price": "8131.22", + # "orderQty": "0.00082", + # "orderType": "Market", + # "avgPx": "7392.02", + # "cumFee": "0.005152238", + # "cumFilledQty": "0.00082", + # "errorCode": "", + # "feeAsset": "USDT", + # "lastExecTime": 1575953151764, + # "orderId": "a16eee20b6750866943712zWEDdAjt3", + # "seqNum": 2623469, + # "side": "Buy", + # "status": "Filled", + # "stopPrice": "", + # "execInst": "NULL_VAL" + # } + # ] + # } + # + # AccountGroupGetFuturesOrderStatus + # + # { + # "code": 0, + # "accountId": "fut2ODPhGiY71Pl4vtXnOZ00ssgD7QGn", + # "ac": "FUTURES", + # "data": { + # "ac": "FUTURES", + # "accountId": "fut2ODPhGiY71Pl4vtXnOZ00ssgD7QGn", + # "time": 1640247020217, + # "orderId": "r17de65747aeU0711043490bbtcp0cmt", + # "seqNum": 28796162908, + # "orderType": "Limit", + # "execInst": "NULL_VAL", + # "side": "Buy", + # "symbol": "BTC-PERP", + # "price": "30000", + # "orderQty": "0.0021", + # "stopPrice": "0", + # "stopBy": "market", + # "status": "New", + # "lastExecTime": 1640247020232, + # "lastQty": "0", + # "lastPx": "0", + # "avgFilledPx": "0", + # "cumFilledQty": "0", + # "fee": "0", + # "cumFee": "0", + # "feeAsset": "USDT", + # "errorCode": "", + # "posStopLossPrice": "0", + # "posStopLossTrigger": "market", + # "posTakeProfitPrice": "0", + # "posTakeProfitTrigger": "market", + # "liquidityInd": "n" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data, market) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://ascendex.github.io/ascendex-pro-api/#list-open-orders + https://ascendex.github.io/ascendex-futures-pro-api-v2/#list-open-orders + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + await self.load_accounts() + market = None + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + account = self.safe_dict(self.accounts, 0, {}) + accountGroup = self.safe_string(account, 'id') + type, query = self.handle_market_type_and_params('fetchOpenOrders', market, params) + accountsByType = self.safe_dict(self.options, 'accountsByType', {}) + accountCategory = self.safe_string(accountsByType, type, 'cash') + request: dict = { + 'account-group': accountGroup, + 'account-category': accountCategory, + } + response = None + if (type == 'spot') or (type == 'margin'): + response = await self.v1PrivateAccountCategoryGetOrderOpen(self.extend(request, query)) + elif type == 'swap': + request['account-category'] = accountCategory + response = await self.v2PrivateAccountGroupGetFuturesOrderOpen(self.extend(request, query)) + else: + raise NotSupported(self.id + ' fetchOpenOrders() is not currently supported for ' + type + ' markets') + # + # AccountCategoryGetOrderOpen + # + # { + # "ac": "CASH", + # "accountId": "cshQtyfq8XLAA9kcf19h8bXHbAwwoqDo", + # "code": 0, + # "data": [ + # { + # "avgPx": "0", # Average filled price of the order + # "cumFee": "0", # cumulative fee paid for self order + # "cumFilledQty": "0", # cumulative filled quantity + # "errorCode": "", # error code; could be empty + # "feeAsset": "USDT", # fee asset + # "lastExecTime": 1576019723550, # The last execution time of the order + # "orderId": "s16ef21882ea0866943712034f36d83", # server provided orderId + # "orderQty": "0.0083", # order quantity + # "orderType": "Limit", # order type + # "price": "7105", # order price + # "seqNum": 8193258, # sequence number + # "side": "Buy", # order side + # "status": "New", # order status on matching engine + # "stopPrice": "", # only available for stop market and stop limit orders; otherwise empty + # "symbol": "BTC/USDT", + # "execInst": "NULL_VAL" # execution instruction + # }, + # ] + # } + # + # AccountGroupGetFuturesOrderOpen + # + # { + # "code": 0, + # "data": [ + # { + # "ac": "FUTURES", + # "accountId": "fut2ODPhGiY71Pl4vtXnOZ00ssgD7QGn", + # "time": 1640247020217, + # "orderId": "r17de65747aeU0711043490bbtcp0cmt", + # "seqNum": 28796162908, + # "orderType": "Limit", + # "execInst": "NULL_VAL", + # "side": "Buy", + # "symbol": "BTC-PERP", + # "price": "30000", + # "orderQty": "0.0021", + # "stopPrice": "0", + # "stopBy": "market", + # "status": "New", + # "lastExecTime": 1640247020232, + # "lastQty": "0", + # "lastPx": "0", + # "avgFilledPx": "0", + # "cumFilledQty": "0", + # "fee": "0", + # "cumFee": "0", + # "feeAsset": "USDT", + # "errorCode": "", + # "posStopLossPrice": "0", + # "posStopLossTrigger": "market", + # "posTakeProfitPrice": "0", + # "posTakeProfitTrigger": "market", + # "liquidityInd": "n" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + if accountCategory == 'futures': + return self.parse_orders(data, market, since, limit) + # a workaround for https://github.com/ccxt/ccxt/issues/7187 + orders = [] + for i in range(0, len(data)): + order = self.parse_order(data[i], market) + orders.append(order) + return self.filter_by_symbol_since_limit(orders, symbol, since, limit) + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://ascendex.github.io/ascendex-pro-api/#list-history-orders-v2 + https://ascendex.github.io/ascendex-futures-pro-api-v2/#list-current-history-orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch orders for + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + await self.load_accounts() + account = self.safe_dict(self.accounts, 0, {}) + accountGroup = self.safe_string(account, 'id') + request: dict = { + # 'category': accountCategory, + # 'symbol': market['id'], + # 'orderType': 'market', # optional, string + # 'side': 'buy', # or 'sell', optional, case insensitive. + # 'status': 'Filled', # "Filled", "Canceled", or "Rejected" + # 'startTime': exchange.milliseconds(), + # 'endTime': exchange.milliseconds(), + # 'page': 1, + # 'pageSize': 100, + } + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + type, query = self.handle_market_type_and_params('fetchClosedOrders', market, params) + options = self.safe_dict(self.options, 'fetchClosedOrders', {}) + defaultMethod = self.safe_string(options, 'method', 'v2PrivateDataGetOrderHist') + method = self.get_supported_mapping(type, { + 'spot': defaultMethod, + 'margin': defaultMethod, + 'swap': 'v2PrivateAccountGroupGetFuturesOrderHistCurrent', + }) + if since is not None: + request['startTime'] = since + until = self.safe_string(params, 'until') + if until is not None: + request['endTime'] = until + accountsByType = self.safe_dict(self.options, 'accountsByType', {}) + accountCategory = self.safe_string(accountsByType, type, 'cash') # margin, futures + response = None + if method == 'v1PrivateAccountCategoryGetOrderHistCurrent': + request['account-group'] = accountGroup + request['account-category'] = accountCategory + if limit is not None: + request['limit'] = limit + response = await self.v1PrivateAccountCategoryGetOrderHistCurrent(self.extend(request, query)) + elif method == 'v2PrivateDataGetOrderHist': + request['account'] = accountCategory + if limit is not None: + request['limit'] = limit + response = await self.v2PrivateDataGetOrderHist(self.extend(request, query)) + elif method == 'v2PrivateAccountGroupGetFuturesOrderHistCurrent': + request['account-group'] = accountGroup + request['account-category'] = accountCategory + if limit is not None: + request['pageSize'] = limit + response = await self.v2PrivateAccountGroupGetFuturesOrderHistCurrent(self.extend(request, query)) + else: + raise NotSupported(self.id + ' fetchClosedOrders() is not currently supported for ' + type + ' markets') + # + # accountCategoryGetOrderHistCurrent + # + # { + # "code":0, + # "accountId":"cshrHKLZCjlZ2ejqkmvIHHtPmLYqdnda", + # "ac":"CASH", + # "data":[ + # { + # "seqNum":15561826728, + # "orderId":"a17294d305c0U6491137460bethu7kw9", + # "symbol":"ETH/USDT", + # "orderType":"Limit", + # "lastExecTime":1591635618200, + # "price":"200", + # "orderQty":"0.1", + # "side":"Buy", + # "status":"Canceled", + # "avgPx":"0", + # "cumFilledQty":"0", + # "stopPrice":"", + # "errorCode":"", + # "cumFee":"0", + # "feeAsset":"USDT", + # "execInst":"NULL_VAL" + # } + # ] + # } + # + # { + # "code": 0, + # "data": [ + # { + # "orderId" : "a173ad938fc3U22666567717788c3b66", # orderId + # "seqNum" : 18777366360, # sequence number + # "accountId" : "cshwSjbpPjSwHmxPdz2CPQVU9mnbzPpt", # accountId + # "symbol" : "BTC/USDT", # symbol + # "orderType" : "Limit", # order type(Limit/Market/StopMarket/StopLimit) + # "side" : "Sell", # order side(Buy/Sell) + # "price" : "11346.77", # order price + # "stopPrice" : "0", # stop price(0 by default) + # "orderQty" : "0.01", # order quantity(in base asset) + # "status" : "Canceled", # order status(Filled/Canceled/Rejected) + # "createTime" : 1596344995793, # order creation time + # "lastExecTime": 1596344996053, # last execution time + # "avgFillPrice": "11346.77", # average filled price + # "fillQty" : "0.01", # filled quantity(in base asset) + # "fee" : "-0.004992579", # cummulative fee. if negative, self value is the commission charged; if possitive, self value is the rebate received. + # "feeAsset" : "USDT" # fee asset + # } + # ] + # } + # + # accountGroupGetFuturesOrderHistCurrent + # + # { + # "code": 0, + # "data": [ + # { + # "ac": "FUTURES", + # "accountId": "fut2ODPhGiY71Pl4vtXnOZ00ssgD7QGn", + # "time": 1640245777002, + # "orderId": "r17de6444fa6U0711043490bbtcpJ2lI", + # "seqNum": 28796124902, + # "orderType": "Limit", + # "execInst": "NULL_VAL", + # "side": "Buy", + # "symbol": "BTC-PERP", + # "price": "30000", + # "orderQty": "0.0021", + # "stopPrice": "0", + # "stopBy": "market", + # "status": "Canceled", + # "lastExecTime": 1640246574886, + # "lastQty": "0", + # "lastPx": "0", + # "avgFilledPx": "0", + # "cumFilledQty": "0", + # "fee": "0", + # "cumFee": "0", + # "feeAsset": "USDT", + # "errorCode": "", + # "posStopLossPrice": "0", + # "posStopLossTrigger": "market", + # "posTakeProfitPrice": "0", + # "posTakeProfitTrigger": "market", + # "liquidityInd": "n" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + if not isinstance(data, list): + data = self.safe_list(data, 'data', []) + return self.parse_orders(data, market, since, limit) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://ascendex.github.io/ascendex-pro-api/#cancel-order + https://ascendex.github.io/ascendex-futures-pro-api-v2/#cancel-order + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + await self.load_markets() + await self.load_accounts() + market = self.market(symbol) + type, query = self.handle_market_type_and_params('cancelOrder', market, params) + accountsByType = self.safe_dict(self.options, 'accountsByType', {}) + accountCategory = self.safe_string(accountsByType, type, 'cash') + account = self.safe_dict(self.accounts, 0, {}) + accountGroup = self.safe_string(account, 'id') + request: dict = { + 'account-group': accountGroup, + 'account-category': accountCategory, + 'symbol': market['id'], + 'time': self.milliseconds(), + 'id': 'foobar', + } + clientOrderId = self.safe_string_2(params, 'clientOrderId', 'id') + if clientOrderId is None: + request['orderId'] = id + else: + request['id'] = clientOrderId + params = self.omit(params, ['clientOrderId', 'id']) + response = None + if (type == 'spot') or (type == 'margin'): + response = await self.v1PrivateAccountCategoryDeleteOrder(self.extend(request, query)) + elif type == 'swap': + request['account-category'] = accountCategory + response = await self.v2PrivateAccountGroupDeleteFuturesOrder(self.extend(request, query)) + else: + raise NotSupported(self.id + ' cancelOrder() is not currently supported for ' + type + ' markets') + # + # AccountCategoryDeleteOrder + # + # { + # "code": 0, + # "data": { + # "accountId": "cshQtyfq8XLAA9kcf19h8bXHbAwwoqDo", + # "ac": "CASH", + # "action": "cancel-order", + # "status": "Ack", + # "info": { + # "id": "wv8QGquoeamhssvQBeHOHGQCGlcBjj23", + # "orderId": "16e6198afb4s8bXHbAwwoqDo2ebc19dc", + # "orderType": "", # could be empty + # "symbol": "ETH/USDT", + # "timestamp": 1573594877822 + # } + # } + # } + # + # AccountGroupDeleteFuturesOrder + # + # { + # "code": 0, + # "data": { + # "meta": { + # "id": "foobar", + # "action": "cancel-order", + # "respInst": "ACK" + # }, + # "order": { + # "ac": "FUTURES", + # "accountId": "fut2ODPhGiY71Pl4vtXnOZ00ssgD7QGn", + # "time": 1640244480476, + # "orderId": "r17de63086f4U0711043490bbtcpPUF4", + # "seqNum": 28795959269, + # "orderType": "Limit", + # "execInst": "NULL_VAL", + # "side": "Buy", + # "symbol": "BTC-PERP", + # "price": "30000", + # "orderQty": "0.0021", + # "stopPrice": "0", + # "stopBy": "market", + # "status": "New", + # "lastExecTime": 1640244480491, + # "lastQty": "0", + # "lastPx": "0", + # "avgFilledPx": "0", + # "cumFilledQty": "0", + # "fee": "0", + # "cumFee": "0", + # "feeAsset": "BTCPC", + # "errorCode": "", + # "posStopLossPrice": "0", + # "posStopLossTrigger": "market", + # "posTakeProfitPrice": "0", + # "posTakeProfitTrigger": "market", + # "liquidityInd": "n" + # } + # } + # } + # + data = self.safe_dict(response, 'data', {}) + order = self.safe_dict_2(data, 'order', 'info', {}) + return self.parse_order(order, market) + + async def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders + + https://ascendex.github.io/ascendex-pro-api/#cancel-all-orders + https://ascendex.github.io/ascendex-futures-pro-api-v2/#cancel-all-open-orders + + :param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list with a single `order structure ` with the response assigned to the info property + """ + await self.load_markets() + await self.load_accounts() + market = None + if symbol is not None: + market = self.market(symbol) + type, query = self.handle_market_type_and_params('cancelAllOrders', market, params) + accountsByType = self.safe_dict(self.options, 'accountsByType', {}) + accountCategory = self.safe_string(accountsByType, type, 'cash') + account = self.safe_dict(self.accounts, 0, {}) + accountGroup = self.safe_string(account, 'id') + request: dict = { + 'account-group': accountGroup, + 'account-category': accountCategory, + 'time': self.milliseconds(), + } + if symbol is not None: + request['symbol'] = market['id'] + response = None + if (type == 'spot') or (type == 'margin'): + response = await self.v1PrivateAccountCategoryDeleteOrderAll(self.extend(request, query)) + elif type == 'swap': + request['account-category'] = accountCategory + response = await self.v2PrivateAccountGroupDeleteFuturesOrderAll(self.extend(request, query)) + else: + raise NotSupported(self.id + ' cancelAllOrders() is not currently supported for ' + type + ' markets') + # + # AccountCategoryDeleteOrderAll + # + # { + # "code": 0, + # "data": { + # "ac": "CASH", + # "accountId": "cshQtyfq8XLAA9kcf19h8bXHbAwwoqDo", + # "action": "cancel-all", + # "info": { + # "id": "2bmYvi7lyTrneMzpcJcf2D7Pe9V1P9wy", + # "orderId": "", + # "orderType": "NULL_VAL", + # "symbol": "", + # "timestamp": 1574118495462 + # }, + # "status": "Ack" + # } + # } + # + # AccountGroupDeleteFuturesOrderAll + # + # { + # "code": 0, + # "data": { + # "ac": "FUTURES", + # "accountId": "fut2ODPhGiY71Pl4vtXnOZ00ssgD7QGn", + # "action": "cancel-all", + # "info": { + # "symbol":"BTC-PERP" + # } + # } + # } + # + return [self.safe_order({ + 'info': response, + })] + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + # + # { + # "address": "0xe7c70b4e73b6b450ee46c3b5c0f5fb127ca55722", + # "destTag": "", + # "tagType": "", + # "tagId": "", + # "chainName": "ERC20", + # "numConfirmations": 20, + # "withdrawalFee": 1, + # "nativeScale": 4, + # "tips": [] + # } + # + address = self.safe_string(depositAddress, 'address') + tagId = self.safe_string(depositAddress, 'tagId') + tag = self.safe_string(depositAddress, tagId) + self.check_address(address) + code = None if (currency is None) else currency['code'] + chainName = self.safe_string(depositAddress, 'blockchain') + network = self.network_id_to_code(chainName, code) + return { + 'info': depositAddress, + 'currency': code, + 'network': network, + 'address': address, + 'tag': tag, + } + + async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://ascendex.github.io/ascendex-pro-api/#query-deposit-addresses + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.network]: unified network code for deposit chain + :returns dict: an `address structure ` + """ + await self.load_markets() + currency = self.currency(code) + networkCode = self.safe_string_2(params, 'network', 'chainName') + networkId = self.network_code_to_id(networkCode) + params = self.omit(params, ['chainName']) + request: dict = { + 'asset': currency['id'], + 'blockchain': networkId, + } + response = await self.v1PrivateGetWalletDepositAddress(self.extend(request, params)) + # + # { + # "code":0, + # "data":{ + # "asset":"USDT", + # "assetName":"Tether", + # "address":[ + # { + # "address":"1N22odLHXnLPCjC8kwBJPTayarr9RtPod6", + # "destTag":"", + # "tagType":"", + # "tagId":"", + # "chainName":"Omni", + # "numConfirmations":3, + # "withdrawalFee":4.7, + # "nativeScale":4, + # "tips":[] + # }, + # { + # "address":"0xe7c70b4e73b6b450ee46c3b5c0f5fb127ca55722", + # "destTag":"", + # "tagType":"", + # "tagId":"", + # "chainName":"ERC20", + # "numConfirmations":20, + # "withdrawalFee":1.0, + # "nativeScale":4, + # "tips":[] + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + addresses = self.safe_list(data, 'address', []) + numAddresses = len(addresses) + address = None + if numAddresses > 1: + addressesByChainName = self.index_by(addresses, 'chainName') + if networkId is None: + chainNames = list(addressesByChainName.keys()) + chains = ', '.join(chainNames) + raise ArgumentsRequired(self.id + ' fetchDepositAddress() returned more than one address, a chainName parameter is required, one of ' + chains) + address = self.safe_dict(addressesByChainName, networkId, {}) + else: + # first address + address = self.safe_dict(addresses, 0, {}) + result = self.parse_deposit_address(address, currency) + return self.extend(result, { + 'info': response, + }) + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + request: dict = { + 'txType': 'deposit', + } + return await self.fetch_transactions(code, since, limit, self.extend(request, params)) + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + request: dict = { + 'txType': 'withdrawal', + } + return await self.fetch_transactions(code, since, limit, self.extend(request, params)) + + async def fetch_deposits_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch history of deposits and withdrawals + :param str [code]: unified currency code for the currency of the deposit/withdrawals, default is None + :param int [since]: timestamp in ms of the earliest deposit/withdrawal, default is None + :param int [limit]: max number of deposit/withdrawals to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `transaction structure ` + """ + await self.load_markets() + request: dict = { + # 'asset': currency['id'], + # 'page': 1, + # 'pageSize': 20, + # 'startTs': self.milliseconds(), + # 'endTs': self.milliseconds(), + # 'txType': undefned, # deposit, withdrawal + } + currency = None + if code is not None: + currency = self.currency(code) + request['asset'] = currency['id'] + if since is not None: + request['startTs'] = since + if limit is not None: + request['pageSize'] = limit + response = await self.v1PrivateGetWalletTransactions(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "data": [ + # { + # "requestId": "wuzd1Ojsqtz4bCA3UXwtUnnJDmU8PiyB", + # "time": 1591606166000, + # "asset": "USDT", + # "transactionType": "deposit", + # "amount": "25", + # "commission": "0", + # "networkTransactionId": "0xbc4eabdce92f14dbcc01d799a5f8ca1f02f4a3a804b6350ea202be4d3c738fce", + # "status": "pending", + # "numConfirmed": 8, + # "numConfirmations": 20, + # "destAddress": {address: "0xe7c70b4e73b6b450ee46c3b5c0f5fb127ca55722"} + # } + # ], + # "page": 1, + # "pageSize": 20, + # "hasNext": False + # } + # } + # + data = self.safe_dict(response, 'data', {}) + transactions = self.safe_list(data, 'data', []) + return self.parse_transactions(transactions, currency, since, limit) + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'reviewing': 'pending', + 'pending': 'pending', + 'confirmed': 'ok', + 'rejected': 'rejected', + } + return self.safe_string(statuses, status, status) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # { + # "requestId": "wuzd1Ojsqtz4bCA3UXwtUnnJDmU8PiyB", + # "time": 1591606166000, + # "asset": "USDT", + # "transactionType": "deposit", + # "amount": "25", + # "commission": "0", + # "networkTransactionId": "0xbc4eabdce92f14dbcc01d799a5f8ca1f02f4a3a804b6350ea202be4d3c738fce", + # "status": "pending", + # "numConfirmed": 8, + # "numConfirmations": 20, + # "destAddress": { + # "address": "0xe7c70b4e73b6b450ee46c3b5c0f5fb127ca55722", + # "destTag": "..." # for currencies that have it + # } + # } + # + destAddress = self.safe_dict(transaction, 'destAddress', {}) + address = self.safe_string(destAddress, 'address') + tag = self.safe_string(destAddress, 'destTag') + timestamp = self.safe_integer(transaction, 'time') + currencyId = self.safe_string(transaction, 'asset') + amountString = self.safe_string(transaction, 'amount') + feeCostString = self.safe_string(transaction, 'commission') + amountString = Precise.string_sub(amountString, feeCostString) + code = self.safe_currency_code(currencyId, currency) + return { + 'info': transaction, + 'id': self.safe_string(transaction, 'requestId'), + 'txid': self.safe_string(transaction, 'networkTransactionId'), + 'type': self.safe_string(transaction, 'transactionType'), + 'currency': code, + 'network': None, + 'amount': self.parse_number(amountString), + 'status': self.parse_transaction_status(self.safe_string(transaction, 'status')), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'address': address, + 'addressFrom': None, + 'addressTo': address, + 'tag': tag, + 'tagFrom': None, + 'tagTo': tag, + 'updated': None, + 'comment': None, + 'fee': { + 'currency': code, + 'cost': self.parse_number(feeCostString), + 'rate': None, + }, + 'internal': False, + } + + async def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + await self.load_markets() + await self.load_accounts() + account = self.safe_dict(self.accounts, 0, {}) + accountGroup = self.safe_string(account, 'id') + request: dict = { + 'account-group': accountGroup, + } + response = await self.v2PrivateAccountGroupGetFuturesPosition(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "accountId": "fut2ODPhGiY71Pl4vtXnOZ00ssgD7QGn", + # "ac": "FUTURES", + # "collaterals": [ + # { + # "asset": "USDT", + # "balance": "44.570287262", + # "referencePrice": "1", + # "discountFactor": "1" + # } + # ], + # "contracts": [ + # { + # "symbol": "BTC-PERP", + # "side": "LONG", + # "position": "0.0001", + # "referenceCost": "-3.12277254", + # "unrealizedPnl": "-0.001700233", + # "realizedPnl": "0", + # "avgOpenPrice": "31209", + # "marginType": "isolated", + # "isolatedMargin": "1.654972977", + # "leverage": "2", + # "takeProfitPrice": "0", + # "takeProfitTrigger": "market", + # "stopLossPrice": "0", + # "stopLossTrigger": "market", + # "buyOpenOrderNotional": "0", + # "sellOpenOrderNotional": "0", + # "markPrice": "31210.723063672", + # "indexPrice": "31223.148857925" + # }, + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + position = self.safe_list(data, 'contracts', []) + result = [] + for i in range(0, len(position)): + result.append(self.parse_position(position[i])) + symbols = self.market_symbols(symbols) + return self.filter_by_array_positions(result, 'symbol', symbols, False) + + def parse_position(self, position: dict, market: Market = None): + # + # { + # "symbol": "BTC-PERP", + # "side": "LONG", + # "position": "0.0001", + # "referenceCost": "-3.12277254", + # "unrealizedPnl": "-0.001700233", + # "realizedPnl": "0", + # "avgOpenPrice": "31209", + # "marginType": "isolated", + # "isolatedMargin": "1.654972977", + # "leverage": "2", + # "takeProfitPrice": "0", + # "takeProfitTrigger": "market", + # "stopLossPrice": "0", + # "stopLossTrigger": "market", + # "buyOpenOrderNotional": "0", + # "sellOpenOrderNotional": "0", + # "markPrice": "31210.723063672", + # "indexPrice": "31223.148857925" + # }, + # + marketId = self.safe_string(position, 'symbol') + market = self.safe_market(marketId, market) + notional = self.safe_string(position, 'buyOpenOrderNotional') + if Precise.string_eq(notional, '0'): + notional = self.safe_string(position, 'sellOpenOrderNotional') + marginType = self.safe_string(position, 'marginType') + marginMode = 'cross' if (marginType == 'crossed') else 'isolated' + collateral = None + if marginMode == 'isolated': + collateral = self.safe_string(position, 'isolatedMargin') + return self.safe_position({ + 'info': position, + 'id': None, + 'symbol': market['symbol'], + 'notional': self.parse_number(notional), + 'marginMode': marginMode, + 'liquidationPrice': None, + 'entryPrice': self.safe_number(position, 'avgOpenPrice'), + 'unrealizedPnl': self.safe_number(position, 'unrealizedPnl'), + 'percentage': None, + 'contracts': self.safe_number(position, 'position'), + 'contractSize': self.safe_number(market, 'contractSize'), + 'markPrice': self.safe_number(position, 'markPrice'), + 'lastPrice': None, + 'side': self.safe_string_lower(position, 'side'), + 'hedged': None, + 'timestamp': None, + 'datetime': None, + 'lastUpdateTimestamp': None, + 'maintenanceMargin': None, + 'maintenanceMarginPercentage': None, + 'collateral': collateral, + 'initialMargin': None, + 'initialMarginPercentage': None, + 'leverage': self.safe_integer(position, 'leverage'), + 'marginRatio': None, + 'stopLossPrice': self.safe_number(position, 'stopLossPrice'), + 'takeProfitPrice': self.safe_number(position, 'takeProfitPrice'), + }) + + def parse_funding_rate(self, contract, market: Market = None) -> FundingRate: + # + # { + # "time": 1640061364830, + # "symbol": "EOS-PERP", + # "markPrice": "3.353854865", + # "indexPrice": "3.3542", + # "openInterest": "14242", + # "fundingRate": "-0.000073026", + # "nextFundingTime": 1640073600000 + # } + # + marketId = self.safe_string(contract, 'symbol') + symbol = self.safe_symbol(marketId, market) + currentTime = self.safe_integer(contract, 'time') + nextFundingRate = self.safe_number(contract, 'fundingRate') + nextFundingRateTimestamp = self.safe_integer(contract, 'nextFundingTime') + return { + 'info': contract, + 'symbol': symbol, + 'markPrice': self.safe_number(contract, 'markPrice'), + 'indexPrice': self.safe_number(contract, 'indexPrice'), + 'interestRate': self.parse_number('0'), + 'estimatedSettlePrice': None, + 'timestamp': currentTime, + 'datetime': self.iso8601(currentTime), + 'previousFundingRate': None, + 'nextFundingRate': None, + 'previousFundingTimestamp': None, + 'nextFundingTimestamp': None, + 'previousFundingDatetime': None, + 'nextFundingDatetime': None, + 'fundingRate': nextFundingRate, + 'fundingTimestamp': nextFundingRateTimestamp, + 'fundingDatetime': self.iso8601(nextFundingRateTimestamp), + 'interval': None, + } + + async def fetch_funding_rates(self, symbols: Strings = None, params={}) -> FundingRates: + """ + fetch the funding rate for multiple markets + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `funding rates structures `, indexe by market symbols + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + response = await self.v2PublicGetFuturesPricingData(params) + # + # { + # "code": 0, + # "data": { + # "contracts": [ + # { + # "time": 1640061364830, + # "symbol": "EOS-PERP", + # "markPrice": "3.353854865", + # "indexPrice": "3.3542", + # "openInterest": "14242", + # "fundingRate": "-0.000073026", + # "nextFundingTime": 1640073600000 + # }, + # ], + # "collaterals": [ + # { + # "asset": "USDTR", + # "referencePrice": "1" + # }, + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + contracts = self.safe_list(data, 'contracts', []) + return self.parse_funding_rates(contracts, symbols) + + async def modify_margin_helper(self, symbol: str, amount, type, params={}) -> MarginModification: + await self.load_markets() + await self.load_accounts() + market = self.market(symbol) + account = self.safe_dict(self.accounts, 0, {}) + accountGroup = self.safe_string(account, 'id') + amount = self.amount_to_precision(symbol, amount) + request: dict = { + 'account-group': accountGroup, + 'symbol': market['id'], + 'amount': amount, # positive value for adding margin, negative for reducing + } + response = await self.v2PrivateAccountGroupPostFuturesIsolatedPositionMargin(self.extend(request, params)) + # + # Can only change margin for perpetual futures isolated margin positions + # + # { + # "code": 0 + # } + # + if type == 'reduce': + amount = Precise.string_abs(amount) + return self.extend(self.parse_margin_modification(response, market), { + 'amount': self.parse_number(amount), + 'type': type, + }) + + def parse_margin_modification(self, data: dict, market: Market = None) -> MarginModification: + # + # addMargin/reduceMargin + # + # { + # "code": 0 + # } + # + errorCode = self.safe_string(data, 'code') + status = 'ok' if (errorCode == '0') else 'failed' + return { + 'info': data, + 'symbol': market['symbol'], + 'type': None, + 'marginMode': 'isolated', + 'amount': None, + 'total': None, + 'code': market['quote'], + 'status': status, + 'timestamp': None, + 'datetime': None, + } + + async def reduce_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + remove margin from a position + :param str symbol: unified market symbol + :param float amount: the amount of margin to remove + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin structure ` + """ + return await self.modify_margin_helper(symbol, -amount, 'reduce', params) + + async def add_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + add margin + :param str symbol: unified market symbol + :param float amount: amount of margin to add + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin structure ` + """ + return await self.modify_margin_helper(symbol, amount, 'add', params) + + async def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://ascendex.github.io/ascendex-futures-pro-api-v2/#change-contract-leverage + + :param float leverage: the rate of leverage + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setLeverage() requires a symbol argument') + if (leverage < 1) or (leverage > 100): + raise BadRequest(self.id + ' leverage should be between 1 and 100') + await self.load_markets() + await self.load_accounts() + market = self.market(symbol) + if not market['swap']: + raise BadSymbol(self.id + ' setLeverage() supports swap contracts only') + account = self.safe_dict(self.accounts, 0, {}) + accountGroup = self.safe_string(account, 'id') + request: dict = { + 'account-group': accountGroup, + 'symbol': market['id'], + 'leverage': leverage, + } + return await self.v2PrivateAccountGroupPostFuturesLeverage(self.extend(request, params)) + + async def set_margin_mode(self, marginMode: str, symbol: Str = None, params={}): + """ + set margin mode to 'cross' or 'isolated' + + https://ascendex.github.io/ascendex-futures-pro-api-v2/#change-margin-type + + :param str marginMode: 'cross' or 'isolated' + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setMarginMode() requires a symbol argument') + marginMode = marginMode.lower() + if marginMode == 'cross': + marginMode = 'crossed' + if marginMode != 'isolated' and marginMode != 'crossed': + raise BadRequest(self.id + ' setMarginMode() marginMode argument should be isolated or cross') + await self.load_markets() + await self.load_accounts() + market = self.market(symbol) + account = self.safe_dict(self.accounts, 0, {}) + accountGroup = self.safe_string(account, 'id') + request: dict = { + 'account-group': accountGroup, + 'symbol': market['id'], + 'marginType': marginMode, + } + if not market['swap']: + raise BadSymbol(self.id + ' setMarginMode() supports swap contracts only') + return await self.v2PrivateAccountGroupPostFuturesMarginType(self.extend(request, params)) + + async def fetch_leverage_tiers(self, symbols: Strings = None, params={}) -> LeverageTiers: + """ + retrieve information on the maximum leverage, and maintenance margin for trades of varying trade sizes + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `leverage tiers structures `, indexed by market symbols + """ + await self.load_markets() + response = await self.v2PublicGetFuturesContract(params) + # + # { + # "code":0, + # "data":[ + # { + # "symbol":"BTC-PERP", + # "status":"Normal", + # "displayName":"BTCUSDT", + # "settlementAsset":"USDT", + # "underlying":"BTC/USDT", + # "tradingStartTime":1579701600000, + # "priceFilter":{"minPrice":"1","maxPrice":"1000000","tickSize":"1"}, + # "lotSizeFilter":{"minQty":"0.0001","maxQty":"1000000000","lotSize":"0.0001"}, + # "commissionType":"Quote", + # "commissionReserveRate":"0.001", + # "marketOrderPriceMarkup":"0.03", + # "marginRequirements":[ + # {"positionNotionalLowerBound":"0","positionNotionalUpperBound":"50000","initialMarginRate":"0.01","maintenanceMarginRate":"0.006"}, + # {"positionNotionalLowerBound":"50000","positionNotionalUpperBound":"200000","initialMarginRate":"0.02","maintenanceMarginRate":"0.012"}, + # {"positionNotionalLowerBound":"200000","positionNotionalUpperBound":"2000000","initialMarginRate":"0.04","maintenanceMarginRate":"0.024"}, + # {"positionNotionalLowerBound":"2000000","positionNotionalUpperBound":"20000000","initialMarginRate":"0.1","maintenanceMarginRate":"0.06"}, + # {"positionNotionalLowerBound":"20000000","positionNotionalUpperBound":"40000000","initialMarginRate":"0.2","maintenanceMarginRate":"0.12"}, + # {"positionNotionalLowerBound":"40000000","positionNotionalUpperBound":"1000000000","initialMarginRate":"0.333333","maintenanceMarginRate":"0.2"} + # ] + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + symbols = self.market_symbols(symbols) + return self.parse_leverage_tiers(data, symbols, 'symbol') + + def parse_market_leverage_tiers(self, info, market: Market = None) -> List[LeverageTier]: + """ + :param dict info: Exchange market response for 1 market + :param dict market: CCXT market + """ + # + # { + # "symbol":"BTC-PERP", + # "status":"Normal", + # "displayName":"BTCUSDT", + # "settlementAsset":"USDT", + # "underlying":"BTC/USDT", + # "tradingStartTime":1579701600000, + # "priceFilter":{"minPrice":"1","maxPrice":"1000000","tickSize":"1"}, + # "lotSizeFilter":{"minQty":"0.0001","maxQty":"1000000000","lotSize":"0.0001"}, + # "commissionType":"Quote", + # "commissionReserveRate":"0.001", + # "marketOrderPriceMarkup":"0.03", + # "marginRequirements":[ + # {"positionNotionalLowerBound":"0","positionNotionalUpperBound":"50000","initialMarginRate":"0.01","maintenanceMarginRate":"0.006"}, + # {"positionNotionalLowerBound":"50000","positionNotionalUpperBound":"200000","initialMarginRate":"0.02","maintenanceMarginRate":"0.012"}, + # {"positionNotionalLowerBound":"200000","positionNotionalUpperBound":"2000000","initialMarginRate":"0.04","maintenanceMarginRate":"0.024"}, + # {"positionNotionalLowerBound":"2000000","positionNotionalUpperBound":"20000000","initialMarginRate":"0.1","maintenanceMarginRate":"0.06"}, + # {"positionNotionalLowerBound":"20000000","positionNotionalUpperBound":"40000000","initialMarginRate":"0.2","maintenanceMarginRate":"0.12"}, + # {"positionNotionalLowerBound":"40000000","positionNotionalUpperBound":"1000000000","initialMarginRate":"0.333333","maintenanceMarginRate":"0.2"} + # ] + # } + # + marginRequirements = self.safe_list(info, 'marginRequirements', []) + marketId = self.safe_string(info, 'symbol') + market = self.safe_market(marketId, market) + tiers = [] + for i in range(0, len(marginRequirements)): + tier = marginRequirements[i] + initialMarginRate = self.safe_string(tier, 'initialMarginRate') + tiers.append({ + 'tier': self.sum(i, 1), + 'symbol': self.safe_symbol(marketId, market, None, 'contract'), + 'currency': market['quote'], + 'minNotional': self.safe_number(tier, 'positionNotionalLowerBound'), + 'maxNotional': self.safe_number(tier, 'positionNotionalUpperBound'), + 'maintenanceMarginRate': self.safe_number(tier, 'maintenanceMarginRate'), + 'maxLeverage': self.parse_number(Precise.string_div('1', initialMarginRate)), + 'info': tier, + }) + return tiers + + def parse_deposit_withdraw_fee(self, fee, currency: Currency = None): + # + # { + # "assetCode": "USDT", + # "assetName": "Tether", + # "precisionScale": 9, + # "nativeScale": 4, + # "blockChain": [ + # { + # "chainName": "Omni", + # "withdrawFee": "30.0", + # "allowDeposit": True, + # "allowWithdraw": True, + # "minDepositAmt": "0.0", + # "minWithdrawal": "50.0", + # "numConfirmations": 3 + # }, + # ] + # } + # + blockChains = self.safe_list(fee, 'blockChain', []) + blockChainsLength = len(blockChains) + result: dict = { + 'info': fee, + 'withdraw': { + 'fee': None, + 'percentage': None, + }, + 'deposit': { + 'fee': None, + 'percentage': None, + }, + 'networks': {}, + } + for i in range(0, blockChainsLength): + blockChain = blockChains[i] + networkId = self.safe_string(blockChain, 'chainName') + currencyCode = self.safe_string(currency, 'code') + networkCode = self.network_id_to_code(networkId, currencyCode) + result['networks'][networkCode] = { + 'deposit': {'fee': None, 'percentage': None}, + 'withdraw': {'fee': self.safe_number(blockChain, 'withdrawFee'), 'percentage': False}, + } + if blockChainsLength == 1: + result['withdraw']['fee'] = self.safe_number(blockChain, 'withdrawFee') + result['withdraw']['percentage'] = False + return result + + async def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}): + """ + fetch deposit and withdraw fees + + https://ascendex.github.io/ascendex-pro-api/#list-all-assets + + :param str[]|None codes: list of unified currency codes + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `fee structures ` + """ + await self.load_markets() + response = await self.v2PublicGetAssets(params) + data = self.safe_list(response, 'data') + return self.parse_deposit_withdraw_fees(data, codes, 'assetCode') + + async def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + :param str code: unified currency codeåå + :param float amount: amount to transfer + :param str fromAccount: account to transfer from + :param str toAccount: account to transfer to + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transfer structure ` + """ + await self.load_markets() + await self.load_accounts() + account = self.safe_dict(self.accounts, 0, {}) + accountGroup = self.safe_string(account, 'id') + currency = self.currency(code) + accountsByType = self.safe_dict(self.options, 'accountsByType', {}) + fromId = self.safe_string(accountsByType, fromAccount, fromAccount) + toId = self.safe_string(accountsByType, toAccount, toAccount) + if fromId != 'cash' and toId != 'cash': + raise ExchangeError(self.id + ' transfer() only supports direct balance transfer between spot and swap, spot and margin') + request: dict = { + 'account-group': accountGroup, + 'amount': self.currency_to_precision(code, amount), + 'asset': currency['id'], + 'fromAccount': fromId, + 'toAccount': toId, + } + response = await self.v1PrivateAccountGroupPostTransfer(self.extend(request, params)) + # + # {"code": "0"} + # + transferOptions = self.safe_dict(self.options, 'transfer', {}) + fillResponseFromRequest = self.safe_bool(transferOptions, 'fillResponseFromRequest', True) + transfer = self.parse_transfer(response, currency) + if fillResponseFromRequest: + transfer['fromAccount'] = fromAccount + transfer['toAccount'] = toAccount + transfer['amount'] = amount + transfer['currency'] = code + return transfer + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + # + # {"code": "0"} + # + status = self.safe_string(transfer, 'code') + currencyCode = self.safe_currency_code(None, currency) + return { + 'info': transfer, + 'id': None, + 'timestamp': None, + 'datetime': None, + 'currency': currencyCode, + 'amount': None, + 'fromAccount': None, + 'toAccount': None, + 'status': self.parse_transfer_status(status), + } + + def parse_transfer_status(self, status: Str) -> Str: + if status == '0': + return 'ok' + return 'failed' + + async def fetch_funding_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch the history of funding payments paid and received on self account + + https://ascendex.github.io/ascendex-futures-pro-api-v2/#funding-payment-history + + :param str [symbol]: unified market symbol + :param int [since]: the earliest time in ms to fetch funding history for + :param int [limit]: the maximum number of funding history structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict: a `funding history structure ` + """ + await self.load_markets() + await self.load_accounts() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchFundingHistory', 'paginate') + if paginate: + return await self.fetch_paginated_call_incremental('fetchFundingHistory', symbol, since, limit, params, 'page', 25) + account = self.safe_dict(self.accounts, 0, {}) + accountGroup = self.safe_string(account, 'id') + request: dict = { + 'account-group': accountGroup, + } + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if limit is not None: + request['pageSize'] = limit + response = await self.v2PrivateAccountGroupGetFuturesFundingPayments(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "data": [ + # { + # "timestamp": 1640476800000, + # "symbol": "BTC-PERP", + # "paymentInUSDT": "-0.013991178", + # "fundingRate": "0.000173497" + # }, + # ], + # "page": 1, + # "pageSize": 3, + # "hasNext": True + # } + # } + # + data = self.safe_dict(response, 'data', {}) + rows = self.safe_list(data, 'data', []) + return self.parse_incomes(rows, market, since, limit) + + def parse_income(self, income, market: Market = None): + # + # { + # "timestamp": 1640476800000, + # "symbol": "BTC-PERP", + # "paymentInUSDT": "-0.013991178", + # "fundingRate": "0.000173497" + # } + # + marketId = self.safe_string(income, 'symbol') + timestamp = self.safe_integer(income, 'timestamp') + return { + 'info': income, + 'symbol': self.safe_symbol(marketId, market, '-', 'swap'), + 'code': 'USDT', + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': None, + 'amount': self.safe_number(income, 'paymentInUSDT'), + } + + async def fetch_margin_modes(self, symbols: Strings = None, params={}) -> MarginModes: + """ + fetches the set margin mode of the user + + https://ascendex.github.io/ascendex-futures-pro-api-v2/#position + + :param str[] [symbols]: a list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `margin mode structures ` + """ + await self.load_markets() + await self.load_accounts() + account = self.safe_dict(self.accounts, 0, {}) + accountGroup = self.safe_string(account, 'id') + request: dict = { + 'account-group': accountGroup, + } + response = await self.v2PrivateAccountGroupGetFuturesPosition(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "accountId": "fut2ODPhGiY71Pl4vtXnOZ00ssgD7QGn", + # "ac": "FUTURES", + # "collaterals": [ + # { + # "asset": "USDT", + # "balance": "44.570287262", + # "referencePrice": "1", + # "discountFactor": "1" + # } + # ], + # "contracts": [ + # { + # "symbol": "BTC-PERP", + # "side": "LONG", + # "position": "0.0001", + # "referenceCost": "-3.12277254", + # "unrealizedPnl": "-0.001700233", + # "realizedPnl": "0", + # "avgOpenPrice": "31209", + # "marginType": "isolated", + # "isolatedMargin": "1.654972977", + # "leverage": "2", + # "takeProfitPrice": "0", + # "takeProfitTrigger": "market", + # "stopLossPrice": "0", + # "stopLossTrigger": "market", + # "buyOpenOrderNotional": "0", + # "sellOpenOrderNotional": "0", + # "markPrice": "31210.723063672", + # "indexPrice": "31223.148857925" + # }, + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + marginModes = self.safe_list(data, 'contracts', []) + return self.parse_margin_modes(marginModes, symbols, 'symbol') + + def parse_margin_mode(self, marginMode: dict, market=None) -> MarginMode: + marketId = self.safe_string(marginMode, 'symbol') + marginType = self.safe_string(marginMode, 'marginType') + margin = 'cross' if (marginType == 'crossed') else 'isolated' + return { + 'info': marginMode, + 'symbol': self.safe_symbol(marketId, market), + 'marginMode': margin, + } + + async def fetch_leverages(self, symbols: Strings = None, params={}) -> Leverages: + """ + fetch the set leverage for all contract markets + + https://ascendex.github.io/ascendex-futures-pro-api-v2/#position + + :param str[] [symbols]: a list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `leverage structures ` + """ + await self.load_markets() + await self.load_accounts() + account = self.safe_dict(self.accounts, 0, {}) + accountGroup = self.safe_string(account, 'id') + request: dict = { + 'account-group': accountGroup, + } + response = await self.v2PrivateAccountGroupGetFuturesPosition(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "accountId": "fut2ODPhGiY71Pl4vtXnOZ00ssgD7QGn", + # "ac": "FUTURES", + # "collaterals": [ + # { + # "asset": "USDT", + # "balance": "44.570287262", + # "referencePrice": "1", + # "discountFactor": "1" + # } + # ], + # "contracts": [ + # { + # "symbol": "BTC-PERP", + # "side": "LONG", + # "position": "0.0001", + # "referenceCost": "-3.12277254", + # "unrealizedPnl": "-0.001700233", + # "realizedPnl": "0", + # "avgOpenPrice": "31209", + # "marginType": "isolated", + # "isolatedMargin": "1.654972977", + # "leverage": "2", + # "takeProfitPrice": "0", + # "takeProfitTrigger": "market", + # "stopLossPrice": "0", + # "stopLossTrigger": "market", + # "buyOpenOrderNotional": "0", + # "sellOpenOrderNotional": "0", + # "markPrice": "31210.723063672", + # "indexPrice": "31223.148857925" + # }, + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + leverages = self.safe_list(data, 'contracts', []) + return self.parse_leverages(leverages, symbols, 'symbol') + + def parse_leverage(self, leverage: dict, market: Market = None) -> Leverage: + marketId = self.safe_string(leverage, 'symbol') + leverageValue = self.safe_integer(leverage, 'leverage') + marginType = self.safe_string(leverage, 'marginType') + marginMode = 'cross' if (marginType == 'crossed') else 'isolated' + return { + 'info': leverage, + 'symbol': self.safe_symbol(marketId, market), + 'marginMode': marginMode, + 'longLeverage': leverageValue, + 'shortLeverage': leverageValue, + } + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + version = api[0] + access = api[1] + type = self.safe_string(api, 2) + url = '' + accountCategory = (type == 'accountCategory') + if accountCategory or (type == 'accountGroup'): + url += self.implode_params('/{account-group}', params) + params = self.omit(params, 'account-group') + request = self.implode_params(path, params) + url += '/api/pro/' + if version == 'v2': + if type == 'data': + request = 'data/' + version + '/' + request + else: + request = version + '/' + request + else: + url += version + '/' + if accountCategory: + url += self.implode_params('{account-category}/', params) + params = self.omit(params, 'account-category') + url += request + if (version == 'v1') and (request == 'cash/balance') or (request == 'margin/balance'): + request = 'balance' + if (version == 'v1') and (request == 'spot/fee'): + request = 'fee' + if request.find('subuser') >= 0: + parts = request.split('/') + request = parts[2] + params = self.omit(params, self.extract_params(path)) + if access == 'public': + if params: + url += '?' + self.urlencode(params) + else: + self.check_required_credentials() + timestamp = str(self.milliseconds()) + payload = timestamp + '+' + request + hmac = self.hmac(self.encode(payload), self.encode(self.secret), hashlib.sha256, 'base64') + headers = { + 'x-auth-key': self.apiKey, + 'x-auth-timestamp': timestamp, + 'x-auth-signature': hmac, + } + if method == 'GET': + if params: + url += '?' + self.urlencode(params) + else: + headers['Content-Type'] = 'application/json' + body = self.json(params) + url = self.urls['api']['rest'] + url + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None # fallback to default error handler + # + # {"code": 6010, "message": "Not enough balance."} + # {"code": 60060, "message": "The order is already filled or canceled."} + # {"code":2100,"message":"ApiKeyFailure"} + # {"code":300001,"message":"Price is too low from market price.","reason":"INVALID_PRICE","accountId":"cshrHKLZCjlZ2ejqkmvIHHtPmLYqdnda","ac":"CASH","action":"place-order","status":"Err","info":{"symbol":"BTC/USDT"}} + # + code = self.safe_string(response, 'code') + message = self.safe_string(response, 'message') + error = (code is not None) and (code != '0') + if error or (message is not None): + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], code, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + raise ExchangeError(feedback) # unknown message + return None diff --git a/ccxt/async_support/backpack.py b/ccxt/async_support/backpack.py new file mode 100644 index 0000000..6364bf0 --- /dev/null +++ b/ccxt/async_support/backpack.py @@ -0,0 +1,2230 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.backpack import ImplicitAPI +from ccxt.base.types import Any, Balances, Bool, Currencies, Currency, DepositAddress, Int, Market, MarketType, Num, Order, OrderBook, OrderRequest, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, Trade, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import OperationRejected +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OperationFailed +from ccxt.base.errors import NetworkError +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.errors import RequestTimeout +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class backpack(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(backpack, self).describe(), { + 'id': 'backpack', + 'name': 'Backpack', + 'countries': ['JP'], # Japan + 'rateLimit': 50, # 20 times per second + 'version': 'v1', + 'certified': False, + 'pro': True, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': True, + 'swap': True, + 'future': False, + 'option': False, + 'addMargin': False, + 'cancelAllOrders': True, + 'cancelAllOrdersAfter': False, + 'cancelOrder': True, + 'cancelOrders': False, + 'cancelWithdraw': False, + 'closePosition': False, + 'createConvertTrade': False, # todo + 'createDepositAddress': False, + 'createLimitBuyOrder': True, + 'createLimitOrder': True, + 'createLimitSellOrder': True, + 'createMarketBuyOrder': True, + 'createMarketBuyOrderWithCost': True, + 'createMarketOrder': True, + 'createMarketOrderWithCost': True, + 'createMarketSellOrder': True, + 'createMarketSellOrderWithCost': True, + 'createOrder': True, + 'createOrders': True, + 'createOrderWithTakeProfitAndStopLoss': True, + 'createPostOnlyOrder': True, + 'createReduceOnlyOrder': True, + 'createStopLossOrder': False, + 'createTakeProfitOrder': False, + 'createTrailingAmountOrder': False, + 'createTrailingPercentOrder': False, + 'createTriggerOrder': True, + 'fetchAccounts': False, + 'fetchAllGreeks': False, + 'fetchBalance': True, + 'fetchCanceledAndClosedOrders': False, + 'fetchCanceledOrders': False, + 'fetchClosedOrder': False, + 'fetchClosedOrders': False, + 'fetchConvertCurrencies': False, + 'fetchConvertQuote': False, + 'fetchConvertTrade': False, + 'fetchConvertTradeHistory': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': True, + 'fetchDeposits': True, + 'fetchDepositsWithdrawals': False, + 'fetchDepositWithdrawFees': False, + 'fetchFundingHistory': True, + 'fetchFundingRate': True, + 'fetchFundingRateHistory': True, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': True, + 'fetchLedger': False, + 'fetchLeverage': False, + 'fetchLeverageTiers': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': True, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': True, + 'fetchOpenInterestHistory': True, + 'fetchOpenOrder': True, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': False, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchOrderTrades': False, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': True, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchStatus': True, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': False, + 'fetchTransactions': False, + 'fetchTransfers': False, + 'fetchVolatilityHistory': False, + 'fetchWithdrawals': True, + 'reduceMargin': False, + 'sandbox': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'transfer': False, + 'withdraw': True, + }, + 'timeframes': { + '1m': '1m', + '3m': '3m', + '5m': '5m', + '15': '15m', + '30': '30m', + '1h': '1h', + '2h': '2h', + '4h': '4h', + '6h': '6h', + '8h': '8h', + '12h': '12h', + '1d': '1d', + '3d': '3d', + '1w': '1w', + '1M': '1month', + }, + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/cc04c278-679f-4554-9f72-930dd632b80f', + 'api': { + 'public': 'https://api.backpack.exchange', + 'private': 'https://api.backpack.exchange', + }, + 'www': 'https://backpack.exchange/', + 'doc': 'https://docs.backpack.exchange/', + 'referral': 'https://backpack.exchange/join/ccxt', + }, + 'api': { + 'public': { + 'get': { + 'api/v1/assets': 1, # done + 'api/v1/collateral': 1, # not used + 'api/v1/borrowLend/markets': 1, + 'api/v1/borrowLend/markets/history': 1, + 'api/v1/markets': 1, # done + 'api/v1/market': 1, # not used + 'api/v1/ticker': 1, # done + 'api/v1/tickers': 1, # done + 'api/v1/depth': 1, # done + 'api/v1/klines': 1, # done + 'api/v1/markPrices': 1, # done + 'api/v1/openInterest': 1, # done + 'api/v1/fundingRates': 1, # done + 'api/v1/status': 1, # done + 'api/v1/ping': 1, # todo check if it is needed for ws + 'api/v1/time': 1, # done + 'api/v1/wallets': 1, # not used + 'api/v1/trades': 1, # done + 'api/v1/trades/history': 1, # done + }, + }, + 'private': { + 'get': { + 'api/v1/account': 1, # todo fetchTradingFee + 'api/v1/account/limits/borrow': 1, # not used + 'api/v1/account/limits/order': 1, # not used + 'api/v1/account/limits/withdrawal': 1, # not used + 'api/v1/borrowLend/positions': 1, # todo fetchBorrowInterest + 'api/v1/capital': 1, # done + 'api/v1/capital/collateral': 1, # not used + 'wapi/v1/capital/deposits': 1, # done + 'wapi/v1/capital/deposit/address': 1, # done + 'wapi/v1/capital/withdrawals': 1, # todo complete after withdrawal + 'api/v1/position': 1, # done but todo check if all is right + 'wapi/v1/history/borrowLend': 1, # not used + 'wapi/v1/history/interest': 1, # not used + 'wapi/v1/history/borrowLend/positions': 1, # not used + 'wapi/v1/history/dust': 1, # not used + 'wapi/v1/history/fills': 1, # done + 'wapi/v1/history/funding': 1, # done + 'wapi/v1/history/orders': 1, # done + 'wapi/v1/history/rfq': 1, + 'wapi/v1/history/quote': 1, + 'wapi/v1/history/settlement': 1, + 'wapi/v1/history/strategies': 1, + 'api/v1/order': 1, # done + 'api/v1/orders': 1, # done + }, + 'post': { + 'api/v1/account/convertDust': 1, + 'api/v1/borrowLend': 1, # todo borrowCrossMargin + 'wapi/v1/capital/withdrawals': 1, # todo complete after withdrawal + 'api/v1/order': 1, # done + 'api/v1/orders': 1, # done + 'api/v1/rfq': 1, + 'api/v1/rfq/accept': 1, + 'api/v1/rfq/refresh': 1, + 'api/v1/rfq/cancel': 1, + 'api/v1/rfq/quote': 1, + }, + 'delete': { + 'api/v1/order': 1, # done + 'api/v1/orders': 1, # done + }, + 'patch': { + 'api/v1/account': 1, + }, + }, + }, + 'features': { + 'default': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': True, + 'takeProfitPrice': True, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'GTC': True, + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': True, + 'selfTradePrevention': { + 'EXPIRE_MAKER': True, + 'EXPIRE_TAKER': True, + 'EXPIRE_BOTH': True, + 'NONE': False, + }, + 'iceberg': False, + }, + 'createOrders': { + 'max': 20, + }, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': None, + 'untilDays': None, + 'symbolRequired': False, + }, + 'fetchOrder': None, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 1000, + 'trigger': True, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': None, + 'untilDays': None, + 'trigger': True, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchClosedOrders': None, + 'fetchOHLCV': { + 'paginate': False, + 'limit': 1000, + }, + }, + 'spot': { + 'extends': 'default', + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + }, + 'precisionMode': TICK_SIZE, + 'options': { + 'instructions': { + 'api/v1/account': { + 'GET': 'accountQuery', + 'PATCH': 'accountUpdate', + }, + 'api/v1/capital': { + 'GET': 'balanceQuery', + }, + 'api/v1/account/limits/borrow': { + 'GET': 'maxBorrowQuantity', + }, + 'api/v1/account/limits/order': { + 'GET': 'maxOrderQuantity', + }, + 'api/v1/account/limits/withdrawal': { + 'GET': 'maxWithdrawalQuantity', + }, + 'api/v1/borrowLend/positions': { + 'GET': 'borrowLendPositionQuery', + }, + 'api/v1/borrowLend': { + 'POST': 'borrowLendExecute', + }, + 'wapi/v1/history/borrowLend/positions': { + 'GET': 'borrowPositionHistoryQueryAll', + }, + 'wapi/v1/history/borrowLend': { + 'GET': 'borrowHistoryQueryAll', + }, + 'wapi/v1/history/dust': { + 'GET': 'dustHistoryQueryAll', + }, + 'api/v1/capital/collateral': { + 'GET': 'collateralQuery', + }, + 'wapi/v1/capital/deposit/address': { + 'GET': 'depositAddressQuery', + }, + 'wapi/v1/capital/deposits': { + 'GET': 'depositQueryAll', + }, + 'wapi/v1/history/fills': { + 'GET': 'fillHistoryQueryAll', + }, + 'wapi/v1/history/funding': { + 'GET': 'fundingHistoryQueryAll', + }, + 'wapi/v1/history/interest': { + 'GET': 'interestHistoryQueryAll', + }, + 'api/v1/order': { + 'GET': 'orderQuery', + 'POST': 'orderExecute', + 'DELETE': 'orderCancel', + }, + 'api/v1/orders': { + 'GET': 'orderQueryAll', + 'POST': 'orderExecute', + 'DELETE': 'orderCancelAll', + }, + 'wapi/v1/history/orders': { + 'GET': 'orderHistoryQueryAll', + }, + 'wapi/v1/history/pnl': { + 'GET': 'pnlHistoryQueryAll', + }, + 'wapi/v1/history/rfq': { + 'GET': 'rfqHistoryQueryAll', + }, + 'wapi/v1/history/quote': { + 'GET': 'quoteHistoryQueryAll', + }, + 'wapi/v1/history/settlement': { + 'GET': 'settlementHistoryQueryAll', + }, + 'api/v1/position': { + 'GET': 'positionQuery', + }, + 'api/v1/rfq/quote': { + 'POST': 'quoteSubmit', + }, + 'wapi/v1/history/strategies': { + 'GET': 'strategyHistoryQueryAll', + }, + 'wapi/v1/capital/withdrawals': { + 'GET': 'withdrawalQueryAll', + 'POST': 'withdraw', + }, + }, + 'recvWindow': 5000, # default is 5000, max is 60000 + 'brokerId': '', + 'currencyIdsListForParseMarket': None, + 'broker': '', + 'timeDifference': 0, # the difference between system clock and the exchange server clock in milliseconds + 'adjustForTimeDifference': False, # controls the adjustment logic upon instantiation + 'networks': { + 'APT': 'Aptos', + 'ARB': 'Arbitrum', + 'AVAX': 'Avalanche', + 'BASE': 'Base', + 'BERA': 'Berachain', + 'BTC': 'Bitcoin', + 'BCH': 'BitcoinCash', + 'BSC': 'Bsc', + 'ADA': 'Cardano', + 'DOGE': 'Dogecoin', + 'ECLIPSE': 'Eclipse', + 'EQUALSMONEY': 'EqualsMoney', + 'ERC20': 'Ethereum', + 'HYP': 'Hyperliquid', + 'LTC': 'Litecoin', + 'OPTIMISM': 'Optimism', + 'MATIC': 'Polygon', + 'SEI': 'Sei', + 'SUI': 'Sui', + 'SOL': 'Solana', + 'STORY': 'Story', + 'TRC20': 'Tron', + 'XRP': 'XRP', + }, + 'networksById': { + 'aptos': 'APT', + 'arbitrum': 'ARB', + 'avalanche': 'AVAX', + 'base': 'BASE', + 'berachain': 'BERA', + 'bitcoin': 'BTC', + 'bitcoincash': 'BCH', + 'bsc': 'BSC', + 'cardano': 'ADA', + 'dogecoin': 'DOGE', + 'eclipse': 'ECLIPSE', + 'equalsmoney': 'EQUALSMONEY', + 'ethereum': 'ERC20', + 'hyperliquid': 'HYP', + 'litecoin': 'LTC', + 'optimism': 'OPTIMISM', + 'polygon': 'MATIC', + 'sei': 'SEI', + 'sui': 'SUI', + 'solana': 'SOL', + 'story': 'STORY', + 'tron': 'TRC20', + 'xrp': 'XRP', + }, + }, + 'commonCurrencies': {}, + 'exceptions': { + 'exact': { + 'INVALID_CLIENT_REQUEST': BadRequest, + 'INVALID_ORDER': InvalidOrder, + 'ACCOUNT_LIQUIDATING': BadRequest, + 'BORROW_LIMIT': BadRequest, + 'BORROW_REQUIRES_LEND_REDEEM': BadRequest, + 'FORBIDDEN': OperationRejected, + 'INSUFFICIENT_FUNDS': InsufficientFunds, + 'INSUFFICIENT_MARGIN': InsufficientFunds, + 'INSUFFICIENT_SUPPLY': InsufficientFunds, + 'INVALID_ASSET': BadRequest, + 'INVALID_MARKET': BadSymbol, + 'INVALID_PRICE': BadRequest, + 'INVALID_POSITION_ID': BadRequest, + 'INVALID_QUANTITY': BadRequest, + 'INVALID_RANGE': BadRequest, + 'INVALID_SIGNATURE': AuthenticationError, + 'INVALID_SOURCE': BadRequest, + 'INVALID_SYMBOL': BadSymbol, + 'INVALID_TWO_FACTOR_CODE': BadRequest, + 'LEND_LIMIT': BadRequest, + 'LEND_REQUIRES_BORROW_REPAY': BadRequest, + 'MAINTENANCE': ExchangeError, + 'MAX_LEVERAGE_REACHED': InsufficientFunds, + 'NOT_IMPLEMENTED': OperationFailed, + 'ORDER_LIMIT': OperationRejected, + 'POSITION_LIMIT': OperationRejected, + 'PRECONDITION_FAILED': OperationFailed, + 'RESOURCE_NOT_FOUND': ExchangeNotAvailable, + 'SERVER_ERROR': NetworkError, + 'TIMEOUT': RequestTimeout, + 'TOO_MANY_REQUESTS': RateLimitExceeded, + 'TRADING_PAUSED': ExchangeNotAvailable, + 'UNAUTHORIZED': AuthenticationError, + }, + # Bad Request parse request payload error: failed to parse "MarketSymbol": Invalid market symbol(occurred while parsing "OrderExecutePayload") + # failed to parse parameter `interval`: failed to parse "KlineInterval": Expect a valid enumeration value. + 'broad': {}, + }, + }) + + async def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://docs.backpack.exchange/#tag/Assets + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = await self.publicGetApiV1Assets(params) + # + # [ + # { + # "coingeckoId": "jito-governance-token", + # "displayName": "Jito", + # "symbol": "JTO", + # "tokens": [ + # { + # "blockchain": "Solana", + # "contractAddress": "jtojtomepa8beP8AuQc6eXt5FriJwfFMwQx2v2f9mCL", + # "depositEnabled": True, + # "displayName": "Jito", + # "maximumWithdrawal": null, + # "minimumDeposit": "0.29", + # "minimumWithdrawal": "0.58", + # "withdrawEnabled": True, + # "withdrawalFee": "0.29" + # } + # ] + # } + # ... + # ] + # + result: dict = {} + for i in range(0, len(response)): + currecy = response[i] + currencyId = self.safe_string(currecy, 'symbol') + code = self.safe_currency_code(currencyId) + networks = self.safe_list(currecy, 'tokens', []) + parsedNetworks: dict = {} + for j in range(0, len(networks)): + network = networks[j] + networkId = self.safe_string(network, 'blockchain') + networkIdLowerCase = self.safe_string_lower(network, 'blockchain') + networkCode = self.network_id_to_code(networkIdLowerCase) + parsedNetworks[networkCode] = { + 'id': networkId, + 'network': networkCode, + 'limits': { + 'withdraw': { + 'min': self.safe_number(network, 'minimumWithdrawal'), + 'max': self.parse_number(self.omit_zero(self.safe_string(network, 'maximumWithdrawal'))), + }, + 'deposit': { + 'min': self.safe_number(network, 'minimumDeposit'), + 'max': None, + }, + }, + 'active': None, + 'deposit': self.safe_bool(network, 'depositEnabled'), + 'withdraw': self.safe_bool(network, 'withdrawEnabled'), + 'fee': self.safe_number(network, 'withdrawalFee'), + 'precision': None, + 'info': network, + } + active = None + deposit = None + withdraw = None + if self.is_empty(parsedNetworks): # if networks are not provided + active = False + deposit = False + withdraw = False + result[code] = self.safe_currency_structure({ + 'id': currencyId, + 'code': code, + 'precision': None, + 'type': 'crypto', # todo check if it is always crypto + 'name': self.safe_string(currecy, 'displayName'), + 'active': active, + 'deposit': deposit, + 'withdraw': withdraw, + 'fee': None, + 'limits': { + 'deposit': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + }, + 'networks': parsedNetworks, + 'info': currecy, + }) + return result + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for bitbank + + https://docs.backpack.exchange/#tag/Markets/operation/get_markets + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + if self.options['adjustForTimeDifference']: + await self.load_time_difference() + response = await self.publicGetApiV1Markets(params) + return self.parse_markets(response) + + def parse_market(self, market: dict) -> Market: + # + # [ + # { + # "baseSymbol": "SOL", + # "createdAt": "2025-01-21T06:34:54.691858", + # "filters": { + # "price": { + # "borrowmarketFeeMaxMultiplier": null, + # "borrowmarketFeeMinMultiplier": null, + # "maxImpactMultiplier": "1.03", + # "maxMultiplier": "1.25", + # "maxPrice": null, + # "meanMarkPriceBand": { + # "maxMultiplier": "1.15", + # "minMultiplier": "0.9" + # }, + # "meanPremiumBand": null, + # "minImpactMultiplier": "0.97", + # "minMultiplier": "0.75", + # "minPrice": "0.01", + # "tickSize": "0.01" + # }, + # "quantity": { + # "maxQuantity": null, + # "minQuantity": "0.01", + # "stepSize": "0.01" + # } + # }, + # "fundingInterval": 28800000, + # "fundingRateLowerBound": null, + # "fundingRateUpperBound": null, + # "imfFunction": null, + # "marketType": "SPOT", + # "mmfFunction": null, + # "openInterestLimit": "0", + # "orderBookState": "Open", + # "quoteSymbol": "USDC", + # "symbol": "SOL_USDC" + # }, + # { + # "baseSymbol": "SOL", + # "createdAt": "2025-01-21T06:34:54.691858", + # "filters": { + # "price": { + # "borrowEntryFeeMaxMultiplier": null, + # "borrowEntryFeeMinMultiplier": null, + # "maxImpactMultiplier": "1.03", + # "maxMultiplier": "1.25", + # "maxPrice": "1000", + # "meanMarkPriceBand": { + # "maxMultiplier": "1.1", + # "minMultiplier": "0.9" + # }, + # "meanPremiumBand": { + # "tolerancePct": "0.05" + # }, + # "minImpactMultiplier": "0.97", + # "minMultiplier": "0.75", + # "minPrice": "0.01", + # "tickSize": "0.01" + # }, + # "quantity": { + # "maxQuantity": null, + # "minQuantity": "0.01", + # "stepSize": "0.01" + # } + # }, + # "fundingInterval": "28800000", + # "fundingRateLowerBound": "-100", + # "fundingRateUpperBound": "100", + # "imfFunction": { + # "base": "0.02", + # "factor": "0.0001275", + # "type": "sqrt" + # }, + # "marketType": "PERP", + # "mmfFunction": { + # "base": "0.0125", + # "factor": "0.0000765", + # "type": "sqrt" + # }, + # "openInterestLimit": "4000000", + # "orderBookState": "Open", + # "quoteSymbol": "USDC", + # "symbol": "SOL_USDC_PERP" + # } + # ] + # + id = self.safe_string(market, 'symbol') + baseId = self.safe_string(market, 'baseSymbol') + quoteId = self.safe_string(market, 'quoteSymbol') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + symbol = base + '/' + quote + filters = self.safe_dict(market, 'filters', {}) + priceFilter = self.safe_dict(filters, 'price', {}) + maxPrice = self.safe_number(priceFilter, 'maxPrice') + minPrice = self.safe_number(priceFilter, 'minPrice') + pricePrecision = self.safe_number(priceFilter, 'tickSize') + quantityFilter = self.safe_dict(filters, 'quantity', {}) + maxQuantity = self.safe_number(quantityFilter, 'maxQuantity') + minQuantity = self.safe_number(quantityFilter, 'minQuantity') + amountPrecision = self.safe_number(quantityFilter, 'stepSize') + type: MarketType + typeOfMarket = self.parse_market_type(self.safe_string(market, 'marketType')) + linear: Bool = None + inverse: Bool = None + settle: Str = None + settleId: Str = None + contractSize: Num = None + if typeOfMarket == 'spot': + type = 'spot' + elif typeOfMarket == 'swap': + type = 'swap' + linear = True + inverse = False + settleId = self.safe_string(market, 'quoteSymbol') + settle = self.safe_currency_code(settleId) + symbol += ':' + settle + contractSize = 1 + orderBookState = self.safe_string(market, 'orderBookState') + return self.safe_market_structure({ + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': type, + 'spot': type == 'spot', + 'margin': type == 'spot', # todo check if margin is supported for all markets + 'swap': type == 'swap', + 'future': False, + 'option': False, + 'active': orderBookState == 'Open', + 'contract': type != 'spot', + 'linear': linear, + 'inverse': inverse, + 'taker': None, # todo check commission + 'maker': None, # todo check commission + 'contractSize': contractSize, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': amountPrecision, + 'price': pricePrecision, + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': minQuantity, + 'max': maxQuantity, + }, + 'price': { + 'min': minPrice, + 'max': maxPrice, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': self.parse8601(self.safe_string(market, 'createdAt')), + 'info': market, + }) + + def parse_market_type(self, type): + types = { + 'SPOT': 'spot', + 'PERP': 'swap', + # current types are described in the docs, but the exchange returns only 'SPOT' and 'PERP' + # 'IPERP': 'swap', + # 'DATED': 'swap', + # 'PREDICTION': 'swap', + # 'RFQ': 'swap', + } + return self.safe_string(types, type, type) + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + + https://docs.backpack.exchange/#tag/Markets/operation/get_tickers + + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + request: dict = {} + response = await self.publicGetApiV1Tickers(self.extend(request, params)) + tickers = self.parse_tickers(response) + return self.filter_by_array_tickers(tickers, 'symbol', symbols) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://docs.backpack.exchange/#tag/Markets/operation/get_ticker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = await self.publicGetApiV1Ticker(self.extend(request, params)) + return self.parse_ticker(response, market) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # fetchTicker/fetchTickers + # + # { + # "firstPrice": "327.38", + # "high": "337.99", + # "lastPrice": "317.14", + # "low": "300.01", + # "priceChange": "-10.24", + # "priceChangePercent": "-0.031279", + # "quoteVolume": "21584.32278", + # "symbol": "AAVE_USDC", + # "trades": "245", + # "volume": "65.823" + # }, ... + # + marketId = self.safe_string(ticker, 'symbol') + market = self.safe_market(marketId, market) + symbol = self.safe_symbol(marketId, market) + open = self.safe_string(ticker, 'firstPrice') + last = self.safe_string(ticker, 'lastPrice') + high = self.safe_string(ticker, 'high') + low = self.safe_string(ticker, 'low') + baseVolume = self.safe_string(ticker, 'volume') + quoteVolume = self.safe_string(ticker, 'quoteVolume') + percentage = self.safe_string(ticker, 'priceChangePercent') + change = self.safe_string(ticker, 'priceChange') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': None, + 'datetime': None, + 'high': high, + 'low': low, + 'bid': None, + 'bidVolume': None, + 'ask': None, + 'askVolume': None, + 'vwap': None, + 'open': open, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': change, + 'percentage': percentage, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'markPrice': None, + 'indexPrice': None, + 'info': ticker, + }, market) + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://docs.backpack.exchange/#tag/Markets/operation/get_depth + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return(default 100, max 200) + :param dict [params]: extra parameters specific to the bitteam api endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = await self.publicGetApiV1Depth(self.extend(request, params)) + # + # { + # "asks": [ + # ["118318.3","0.00633"], + # ["118567.2","0.08450"] + # ], + # "bids": [ + # ["1.0","0.38647"], + # ["12.9","1.00000"] + # ], + # "lastUpdateId":"1504999670", + # "timestamp":1753102447307501 + # } + # + microseconds = self.safe_integer(response, 'timestamp') + timestamp = self.parse_to_int(microseconds / 1000) + orderbook = self.parse_order_book(response, symbol, timestamp) + orderbook['nonce'] = self.safe_integer(response, 'lastUpdateId') + return orderbook + + async def fetch_ohlcv(self, symbol: str, timeframe='1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://docs.backpack.exchange/#tag/Markets/operation/get_klines + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in seconds of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch(default 100) + :param dict [params]: extra parameters specific to the bitteam api endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + interval = self.safe_string(self.timeframes, timeframe, timeframe) + request: dict = { + 'symbol': market['id'], + 'interval': interval, + } + until: Int = None + until, params = self.handle_option_and_params(params, 'fetchOHLCV', 'until') + if until is not None: + request['endTime'] = self.parse_to_int(until / 1000) # convert milliseconds to seconds + defaultLimit = 100 + if since is None: + if limit is None: + limit = defaultLimit + duration = self.parse_timeframe(timeframe) + endTime = self.parse_to_int(until / 1000) if until else self.seconds() + startTime = endTime - (limit * duration) + request['startTime'] = startTime + else: + request['startTime'] = self.parse_to_int(since / 1000) # convert milliseconds to seconds + price = self.safe_string(params, 'price') + if price is not None: + request['priceType'] = self.capitalize(price) + params = self.omit(params, 'price') + response = await self.publicGetApiV1Klines(self.extend(request, params)) + return self.parse_ohlcvs(response, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # [ + # { + # "close": "118294.6", + # "end": "2025-07-19 13:12:00", + # "high": "118297.6", + # "low": "118237.5", + # "open": "118238", + # "quoteVolume": "4106.558156", + # "start": "2025-07-19 13:09:00", + # "trades": "12", + # "volume": "0.03473" + # }, + # ... + # ] + # + return [ + self.parse8601(self.safe_string(ohlcv, 'start')), + self.safe_number(ohlcv, 'open'), + self.safe_number(ohlcv, 'high'), + self.safe_number(ohlcv, 'low'), + self.safe_number(ohlcv, 'close'), + self.safe_number(ohlcv, 'volume'), + ] + + async def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate + + https://docs.backpack.exchange/#tag/Markets/operation/get_mark_prices + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + await self.load_markets() + market = self.market(symbol) + if market['spot']: + raise BadRequest(self.id + ' fetchFundingRate() symbol does not support market ' + symbol) + request: dict = { + 'symbol': market['id'], + } + response = await self.publicGetApiV1MarkPrices(self.extend(request, params)) + data = self.safe_dict(response, 0, {}) + return self.parse_funding_rate(data, market) + + def parse_funding_rate(self, contract, market: Market = None) -> FundingRate: + # + # { + # "fundingRate": "0.0001", + # "indexPrice": "118333.18643195", + # "markPrice": "118343.51853741", + # "nextFundingTimestamp": 1753113600000, + # "symbol": "BTC_USDC_PERP" + # } + # + marketId = self.safe_string(contract, 'symbol') + market = self.safe_market(marketId, market) + symbol = self.safe_symbol(marketId, market) + nextFundingTimestamp = self.safe_integer(contract, 'nextFundingTimestamp') + return { + 'info': contract, + 'symbol': symbol, + 'markPrice': self.safe_number(contract, 'markPrice'), + 'indexPrice': self.safe_number(contract, 'indexPrice'), + 'interestRate': None, + 'estimatedSettlePrice': None, + 'timestamp': None, + 'datetime': None, + 'fundingRate': self.safe_number(contract, 'fundingRate'), + 'fundingTimestamp': None, + 'fundingDatetime': None, + 'nextFundingRate': None, + 'nextFundingTimestamp': nextFundingTimestamp, + 'nextFundingDatetime': self.iso8601(nextFundingTimestamp), + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': '1h', + } + + async def fetch_open_interest(self, symbol: str, params={}): + """ + Retrieves the open interest of a derivative trading pair + + https://docs.backpack.exchange/#tag/Markets/operation/get_open_interest + + :param str symbol: Unified CCXT market symbol + :param dict [params]: exchange specific parameters + :returns dict} an open interest structure{@link https://docs.ccxt.com/#/?id=interest-history-structure: + """ + await self.load_markets() + market = self.market(symbol) + if market['spot']: + raise BadRequest(self.id + ' fetchOpenInterest() symbol does not support market ' + symbol) + request: dict = { + 'symbol': market['id'], + } + response = await self.publicGetApiV1OpenInterest(self.extend(request, params)) + interest = self.safe_dict(response, 0, {}) + return self.parse_open_interest(interest, market) + + def parse_open_interest(self, interest, market: Market = None): + # + # [ + # { + # "openInterest": "1273.85214", + # "symbol": "BTC_USDC_PERP", + # "timestamp":1753105735301 + # } + # ] + # + timestamp = self.safe_integer(interest, 'timestamp') + openInterest = self.safe_number(interest, 'openInterest') + return self.safe_open_interest({ + 'symbol': market['symbol'], + 'openInterestAmount': None, + 'openInterestValue': openInterest, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'info': interest, + }, market) + + async def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical funding rate prices + + https://docs.backpack.exchange/#tag/Markets/operation/get_funding_interval_rates + + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: timestamp in ms of the earliest funding rate to fetch + :param int [limit]: the maximum amount of funding rate structures + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `funding rate structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchFundingRateHistory() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['limit'] = min(limit, 1000) # api maximum 1000 + response = await self.publicGetApiV1FundingRates(self.extend(request, params)) + # + # [ + # { + # "fundingRate": "0.0001", + # "intervalEndTimestamp": "2025-07-22T00:00:00", + # "symbol": "BTC_USDC_PERP" + # } + # ] + # + rates = [] + for i in range(0, len(response)): + rate = response[i] + datetime = self.safe_string(rate, 'intervalEndTimestamp') + timestamp = self.parse8601(datetime) + rates.append({ + 'info': rate, + 'symbol': market['symbol'], + 'fundingRate': self.safe_number(rate, 'fundingRate'), + 'timestamp': timestamp, + 'datetime': datetime, + }) + sorted = self.sort_by(rates, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, market['symbol'], since, limit) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://docs.backpack.exchange/#tag/Trades/operation/get_recent_trades + https://docs.backpack.exchange/#tag/Trades/operation/get_historical_trades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.offset]: the number of trades to skip, default is 0 + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['limit'] = min(limit, 1000) # api maximum 1000 + response = None + offset = self.safe_integer(params, 'offset') + if offset is not None: + response = await self.publicGetApiV1TradesHistory(self.extend(request, params)) + else: + response = await self.publicGetApiV1Trades(self.extend(request, params)) + return self.parse_trades(response, market, since, limit) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://docs.backpack.exchange/#tag/History/operation/get_fills + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve(default 100, max 1000) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch trades for + :param str [params.fillType]: 'User'(default) 'BookLiquidation' or 'Adl' or 'Backstop' or 'Liquidation' or 'AllLiquidation' or 'CollateralConversion' or 'CollateralConversionAndSpotLiquidation' + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if since is not None: + request['from'] = since + if limit is not None: + request['limit'] = limit + until = self.safe_integer(params, 'until') + if until is not None: + params = self.omit(params, ['until']) + request['to'] = until + fillType = self.safe_string(params, 'fillType') + if fillType is None: + request['fillType'] = 'User' # default + response = await self.privateGetWapiV1HistoryFills(self.extend(request, params)) + return self.parse_trades(response, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades + # { + # "id": 8721564, + # "isBuyerMaker": False, + # "price": "117427.6", + # "quantity": "0.00016", + # "quoteQuantity": "18.788416", + # "timestamp": 1753123916818 + # } + # + # fetchMyTrades + # { + # "clientId": null, + # "fee": "0.004974", + # "feeSymbol": "USDC", + # "isMaker": False, + # "orderId": "4238907375", + # "price": "3826.15", + # "quantity": "0.0026", + # "side": "Bid", + # "symbol": "ETH_USDC_PERP", + # "systemOrderType": null, + # "timestamp": "2025-07-27T17:39:00.092", + # "tradeId": 9748827 + # } + # + id = self.safe_string_2(trade, 'id', 'tradeId') + marketId = self.safe_string(trade, 'symbol') + market = self.safe_market(marketId, market) + price = self.safe_string(trade, 'price') + amount = self.safe_string(trade, 'quantity') + isMaker = self.safe_bool(trade, 'isMaker') + takerOrMaker = 'maker' if isMaker else 'taker' + orderId = self.safe_string(trade, 'orderId') + side = self.parse_order_side(self.safe_string(trade, 'side')) + fee = None + feeAmount = self.safe_string(trade, 'fee') + timestamp = self.safe_integer(trade, 'timestamp') + if feeAmount is not None: + # if fetchMyTrades + datetime = self.safe_string(trade, 'timestamp') + timestamp = self.parse8601(datetime) + feeSymbol = self.safe_currency_code(self.safe_string(trade, 'feeSymbol')) + if feeAmount is not None: + fee = { + 'cost': feeAmount, + 'currency': feeSymbol, + 'rate': None, + } + return self.safe_trade({ + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'id': id, + 'order': orderId, + 'type': None, + 'side': side, + 'takerOrMaker': takerOrMaker, + 'price': price, + 'amount': amount, + 'cost': None, + 'fee': fee, + }, market) + + async def fetch_status(self, params={}): + """ + the latest known information on the availability of the exchange API + + https://docs.backpack.exchange/#tag/System/operation/get_status + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `status structure ` + """ + response = await self.publicGetApiV1Status(params) + # + # { + # "message":null, + # "status":"Ok" + # } + # + status = self.safe_string(response, 'status') + return { + 'status': status.lower(), + 'updated': None, + 'eta': None, + 'url': None, + 'info': response, + } + + async def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://developer-pro.bitmart.com/en/spot/#get-system-time + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = await self.publicGetApiV1Time(params) + # + # 1753131712992 + # + return self.safe_integer(response, 0, self.milliseconds()) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://docs.backpack.exchange/#tag/Capital/operation/get_balances + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + response = await self.privateGetApiV1Capital(params) + return self.parse_balance(response) + + def parse_balance(self, response) -> Balances: + # + # { + # "USDC": { + # "available": "120", + # "locked": "0", + # "staked": "0" + # } + # } + # + balanceKeys = list(response.keys()) + result: dict = {} + for i in range(0, len(balanceKeys)): + id = balanceKeys[i] + code = self.safe_currency_code(id) + balance = response[id] + account = self.account() + locked = self.safe_string(balance, 'locked') + staked = self.safe_string(balance, 'staked') + used = Precise.string_add(locked, staked) + account['free'] = self.safe_string(balance, 'available') + account['used'] = used + result[code] = account + return self.safe_balance(result) + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://docs.backpack.exchange/#tag/Capital/operation/get_deposits + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch entries for + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + request: dict = { + } + currency: Currency = None + if code is not None: + currency = self.currency(code) + if since is not None: + request['from'] = since + if limit is not None: + request['limit'] = limit # default 100, max 1000 + until: Int = None + until, params = self.handle_option_and_params(params, 'fetchDeposits', 'until') + if until is not None: + request['endTime'] = until + response = await self.privateGetWapiV1CapitalDeposits(self.extend(request, params)) + return self.parse_transactions(response, currency, since, limit) + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://docs.backpack.exchange/#tag/Capital/operation/get_withdrawals + + :param str code: unified currency code of the currency transferred + :param int [since]: the earliest time in ms to fetch transfers for(default 24 hours ago) + :param int [limit]: the maximum number of transfer structures to retrieve(default 50, max 200) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch transfers for(default time now) + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + request: dict = {} + currency: Currency = None + if code is not None: + currency = self.currency(code) + if since is not None: + request['from'] = since + if limit is not None: + request['limit'] = limit + until: Int = None + until, params = self.handle_option_and_params(params, 'fetchWithdrawals', 'until') + if until is not None: + request['to'] = until + response = await self.privateGetWapiV1CapitalWithdrawals(self.extend(request, params)) + return self.parse_transactions(response, currency, since, limit) + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://docs.backpack.exchange/#tag/Capital/operation/request_withdrawal + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str params['network']: the network to withdraw on(mandatory) + :returns dict: a `transaction structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'symbol': currency['id'], + 'quantity': self.number_to_string(amount), + 'address': address, + } + if tag is not None: + request['clientId'] = tag # memo or tag + networkCode, query = self.handle_network_code_and_params(params) + networkId = self.network_code_to_id(networkCode) + if networkId is None: + raise BadRequest(self.id + ' withdraw() requires a network parameter') + request['blockchain'] = networkId + response = await self.privatePostWapiV1CapitalWithdrawals(self.extend(request, query)) + return self.parse_transaction(response, currency) + + def parse_transaction(self, transaction, currency: Currency = None) -> Transaction: + # + # fetchDeposits + # [ + # { + # "createdAt": "2025-07-23T13:55:54.267", + # "fiatAmount": null, + # "fiatCurrency": null, + # "fromAddress": "0x2e3ab3e88a7dbdc763aadf5b28c18fb085af420a", + # "id": 6695353, + # "institutionBic": null, + # "platformMemo": null, + # "quantity": "120", + # "source": "ethereum", + # "status": "confirmed", + # "symbol": "USDC", + # "toAddress": "0xfBe7CbfCde93c8a4204a4be6B56732Eb32690170", + # "transactionHash": "0x58edaac415398d617b34c6673fffcaf0024990d5700565030119db5cbf3765d1" + # } + # ] + # + # withdraw + # { + # "accountIdentifier": null, + # "bankIdentifier": null, + # "bankName": null, + # "blockchain": "Ethereum", + # "clientId": null, + # "createdAt": "2025-08-13T19:27:13.817", + # "fee": "3", + # "fiatFee": null, + # "fiatState": null, + # "fiatSymbol": null, + # "id": 5479929, + # "identifier": null, + # "isInternal": False, + # "providerId": null, + # "quantity": "10", + # "status": "pending", + # "subaccountId": null, + # "symbol": "USDC", + # "toAddress": "0x0ad42b8e602c2d3d475ae52d678cf63d84ab2749", + # "transactionHash": null, + # "triggerAt": null + # } + # + # fetchWithdrawals + # [ + # { + # "accountIdentifier": null, + # "bankIdentifier": null, + # "bankName": null, + # "blockchain": "Ethereum", + # "clientId": null, + # "createdAt": "2025-08-13T19:27:13.817", + # "fee": "3", + # "fiatFee": null, + # "fiatState": null, + # "fiatSymbol": null, + # "id": 5479929, + # "identifier": null, + # "isInternal": False, + # "providerId": null, + # "quantity": "10", + # "status": "confirmed", + # "subaccountId": null, + # "symbol": "USDC", + # "toAddress": "0x0ad42b8e602c2d3d475ae52d678cf63d84ab2749", + # "transactionHash": "0x658b6d082af4afa0d3cf85caf344ff7c19d980117726bf193b00d8850f8746a1", + # "triggerAt": null + # } + # ] + # + status = self.parse_transaction_status(self.safe_string(transaction, 'status')) + id = self.safe_string(transaction, 'id') + txid = self.safe_string(transaction, 'transactionHash') + coin = self.safe_string(transaction, 'symbol') + code = self.safe_currency_code(coin, currency) + timestamp = self.parse8601(self.safe_string(transaction, 'createdAt')) + amount = self.safe_number(transaction, 'quantity') + networkId = self.safe_string_lower_2(transaction, 'source', 'blockchain') + network = self.network_id_to_code(networkId) + addressTo = self.safe_string(transaction, 'toAddress') + addressFrom = self.safe_string(transaction, 'fromAddress') + tag = self.safe_string(transaction, 'platformMemo') + feeCost = self.safe_number(transaction, 'fee') + internal = self.safe_bool(transaction, 'isInternal', False) + fee = None + if feeCost is not None: + fee = { + 'cost': feeCost, + 'currency': code, + } + return { + 'info': transaction, + 'id': id, + 'txid': txid, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': network, + 'address': None, + 'addressTo': addressTo, + 'addressFrom': addressFrom, + 'tag': tag, + 'tagTo': None, + 'tagFrom': None, + 'type': None, + 'amount': amount, + 'currency': code, + 'status': status, + 'updated': None, + 'internal': internal, + 'comment': None, + 'fee': fee, + } + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'cancelled': 'cancelled', + 'confirmed': 'ok', + 'declined': 'declined', + 'expired': 'expired', + 'initiated': 'initiated', + 'pending': 'pending', + 'refunded': 'refunded', + 'information required': 'pending', + } + return self.safe_string(statuses, status, status) + + async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://docs.backpack.exchange/#tag/Capital/operation/get_deposit_address + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.networkCode]: the network to fetch the deposit address(mandatory) + :returns dict: an `address structure ` + """ + await self.load_markets() + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + if networkCode is None: + raise ArgumentsRequired(self.id + ' fetchDepositAddress() requires a network parameter, see https://docs.ccxt.com/#/?id=network-codes') + currency = self.currency(code) + request: dict = { + 'blockchain': self.network_code_to_id(networkCode), + } + response = await self.privateGetWapiV1CapitalDepositAddress(self.extend(request, params)) + return self.parse_deposit_address(response, currency) + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + # + # { + # "address": "0xfBe7CbfCde93c8a4204a4be6B56732Eb32690170" + # } + # + address = self.safe_string(depositAddress, 'address') + currencyId = self.safe_string(depositAddress, 'currency') + currency = self.safe_currency(currencyId, currency) + return { + 'info': depositAddress, + 'currency': currency['code'], + 'network': None, # network is not returned by the API + 'address': address, + 'tag': None, + } + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}) -> Order: + """ + create a trade order + + https://docs.backpack.exchange/#tag/Order/operation/execute_order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fullfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.cost]: *market orders only* the cost of the order in units of the quote currency(could be used instead of amount) + :param int [params.clientOrderId]: a unique id for the order + :param boolean [params.postOnly]: True to place a post only order + :param str [params.timeInForce]: 'GTC', 'IOC', 'FOK' or 'PO' + :param bool [params.reduceOnly]: *contract only* Indicates if self order is to reduce the size of a position + :param str [params.selfTradePrevention]: one of EXPIRE_MAKER, EXPIRE_TAKER or EXPIRE_BOTH + :param bool [params.autoLend]: *spot margin only* if True then the order can lend + :param bool [params.autoLendRedeem]: *spot margin only* if True then the order can redeem a lend if required + :param bool [params.autoBorrow]: *spot margin only* if True then the order can borrow + :param bool [params.autoBorrowRepay]: *spot margin only* if True then the order can repay a borrow + :param float [params.triggerPrice]: the price that a trigger order is triggered at + :param dict [params.takeProfit]: *swap markets only - takeProfit object in params* containing the triggerPrice at which the attached take profit order will be triggered + :param float [params.takeProfit.triggerPrice]: take profit trigger price + :param float [params.takeProfit.price]: take profit order price(if not provided the order will be a market order) + :param dict [params.stopLoss]: *swap markets only - stopLoss object in params* containing the triggerPrice at which the attached stop loss order will be triggered + :param float [params.stopLoss.triggerPrice]: stop loss trigger price + :param float [params.stopLoss.price]: stop loss order price(if not provided the order will be a market order) + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + orderRequest = self.create_order_request(symbol, type, side, amount, price, params) + response = await self.privatePostApiV1Order(orderRequest) + return self.parse_order(response, market) + + async def create_orders(self, orders: List[OrderRequest], params={}): + """ + create a list of trade orders + + https://docs.backpack.exchange/#tag/Order/operation/execute_order_batch + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + ordersRequests = [] + for i in range(0, len(orders)): + rawOrder = orders[i] + marketId = self.safe_string(rawOrder, 'symbol') + type = self.safe_string(rawOrder, 'type') + side = self.safe_string(rawOrder, 'side') + amount = self.safe_number(rawOrder, 'amount') + price = self.safe_number(rawOrder, 'price') + orderParams = self.safe_dict(rawOrder, 'params', {}) + extendedParams = self.extend(orderParams, params) # the request does not accept extra params since it's a list, so we're extending each order with the common params + orderRequest = self.create_order_request(marketId, type, side, amount, price, extendedParams) + ordersRequests.append(orderRequest) + response = await self.privatePostApiV1Orders(ordersRequests) + return self.parse_orders(response) + + def create_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'side': self.encode_order_side(side), + 'orderType': self.capitalize(type), + } + triggerPrice = self.safe_string(params, 'triggerPrice') + isTriggerOrder = triggerPrice is not None + quantityKey = 'triggerQuantity' if isTriggerOrder else 'quantity' + # handle basic limit/market order types + if type == 'limit': + request['price'] = self.price_to_precision(symbol, price) + request[quantityKey] = self.amount_to_precision(symbol, amount) + elif type == 'market': + cost = self.safe_string_2(params, 'cost', 'quoteQuantity') + if cost is not None: + request['quoteQuantity'] = self.cost_to_precision(symbol, cost) + params = self.omit(params, ['cost', 'quoteQuantity']) + else: + request[quantityKey] = self.amount_to_precision(symbol, amount) + # trigger orders + if isTriggerOrder: + request['triggerPrice'] = self.price_to_precision(symbol, triggerPrice) + params = self.omit(params, 'triggerPrice') + clientOrderId = self.safe_integer(params, 'clientOrderId') # the exchange requires uint + if clientOrderId is not None: + request['clientId'] = clientOrderId + params = self.omit(params, 'clientOrderId') + postOnly = False + postOnly, params = self.handle_post_only(type == 'market', False, params) + if postOnly: + params['postOnly'] = True + takeProfit = self.safe_dict(params, 'takeProfit') + if takeProfit is not None: + takeProfitTriggerPrice = self.safe_string(takeProfit, 'triggerPrice') + if takeProfitTriggerPrice is not None: + request['takeProfitTriggerPrice'] = self.price_to_precision(symbol, takeProfitTriggerPrice) + takeProfitPrice = self.safe_string(takeProfit, 'price') + if takeProfitPrice is not None: + request['takeProfitLimitPrice'] = self.price_to_precision(symbol, takeProfitPrice) + params = self.omit(params, 'takeProfit') + stopLoss = self.safe_dict(params, 'stopLoss') + if stopLoss is not None: + stopLossTriggerPrice = self.safe_string(stopLoss, 'triggerPrice') + if stopLossTriggerPrice is not None: + request['stopLossTriggerPrice'] = self.price_to_precision(symbol, stopLossTriggerPrice) + stopLossPrice = self.safe_string(stopLoss, 'price') + if stopLossPrice is not None: + request['stopLossLimitPrice'] = self.price_to_precision(symbol, stopLossPrice) + params = self.omit(params, 'stopLoss') + selfTradePrevention = None + selfTradePrevention, params = self.handle_option_and_params(params, 'createOrder', 'selfTradePrevention') + if selfTradePrevention is not None: + if selfTradePrevention == 'EXPIRE_MAKER': + request['selfTradePrevention'] = 'RejectMaker' + elif selfTradePrevention == 'EXPIRE_TAKER': + request['selfTradePrevention'] = 'RejectTaker' + elif selfTradePrevention == 'EXPIRE_BOTH': + request['selfTradePrevention'] = 'RejectBoth' + return self.extend(request, params) + + def encode_order_side(self, side): + sides: dict = { + 'buy': 'Bid', + 'sell': 'Ask', + } + return self.safe_string(sides, side, side) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all unfilled currently open orders + + https://docs.backpack.exchange/#tag/Order/operation/get_open_orders + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + response = await self.privateGetApiV1Orders(self.extend(request, params)) + return self.parse_orders(response, market, since, limit) + + async def fetch_open_order(self, id: str, symbol: Str = None, params={}): + """ + fetch an open order by it's id + + https://docs.backpack.exchange/#tag/Order/operation/get_order + + :param str id: order id + :param str symbol: not used by hollaex fetchOpenOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOpenOrder() requires a symbol argument') + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'orderId': id, + } + response = await self.privateGetApiV1Order(self.extend(request, params)) + return self.parse_order(response) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://docs.backpack.exchange/#tag/Order/operation/cancel_order + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + market = self.market(symbol) + request: dict = { + 'orderId': id, + 'symbol': market['id'], + } + response = await self.privateDeleteApiV1Order(self.extend(request, params)) + return self.parse_order(response) + + async def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders + + https://docs.backpack.exchange/#tag/Order/operation/cancel_open_orders + + :param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = await self.privateDeleteApiV1Orders(self.extend(request, params)) + return self.parse_orders(response, market) + + async def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://docs.backpack.exchange/#tag/History/operation/get_order_history + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of orde structures to retrieve(default 100, max 1000) + :param dict [params]: extra parameters specific to the bitteam api endpoint + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if limit is not None: + request['limit'] = limit + response = await self.privateGetWapiV1HistoryOrders(self.extend(request, params)) + return self.parse_orders(response, market, since, limit) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # { + # "clientId": null, + # "createdAt": 1753624283415, + # "executedQuantity": "0.001", + # "executedQuoteQuantity": "3.81428", + # "id": "4227701917", + # "orderType": "Market", + # "quantity": "0.001", + # "quoteQuantity": "3.82", + # "reduceOnly": null, + # "relatedOrderId": null, + # "selfTradePrevention": "RejectTaker", + # "side": "Bid", + # "status": "Filled", + # "stopLossLimitPrice": null, + # "stopLossTriggerBy": null, + # "stopLossTriggerPrice": null, + # "strategyId": null, + # "symbol": "ETH_USDC", + # "takeProfitLimitPrice": null, + # "takeProfitTriggerBy": null, + # "takeProfitTriggerPrice": null, + # "timeInForce": "GTC", + # "triggerBy": null, + # "triggerPrice": null, + # "triggerQuantity": null, + # "triggeredAt": null + # } + # + # fetchOpenOrders + # { + # "clientId": 123456789, + # "createdAt": 1753626206762, + # "executedQuantity": "0", + # "executedQuoteQuantity": "0", + # "id": "4228978331", + # "orderType": "Limit", + # "postOnly": True, + # "price": "3000", + # "quantity": "0.001", + # "reduceOnly": null, + # "relatedOrderId": null, + # "selfTradePrevention": "RejectTaker", + # "side": "Bid", + # "status": "New", + # "stopLossLimitPrice": null, + # "stopLossTriggerBy": null, + # "stopLossTriggerPrice": null, + # "strategyId": null, + # "symbol": "ETH_USDC", + # "takeProfitLimitPrice": null, + # "takeProfitTriggerBy": null, + # "takeProfitTriggerPrice": null, + # "timeInForce": "GTC", + # "triggerBy": null, + # "triggerPrice": null, + # "triggerQuantity": null, + # "triggeredAt": null + # } + # + # fetchOrders + # { + # "clientId": null, + # "createdAt": "2025-07-27T18:05:40.897", + # "executedQuantity": "0", + # "executedQuoteQuantity": "0", + # "expiryReason": null, + # "id": "4239996998", + # "orderType": "Limit", + # "postOnly": False, + # "price": "4500", + # "quantity": null, + # "quoteQuantity": null, + # "selfTradePrevention": "RejectTaker", + # "side": "Ask", + # "status": "Cancelled", + # "stopLossLimitPrice": null, + # "stopLossTriggerBy": null, + # "stopLossTriggerPrice": null, + # "strategyId": null, + # "symbol": "ETH_USDC", + # "systemOrderType": null, + # "takeProfitLimitPrice": null, + # "takeProfitTriggerBy": null, + # "takeProfitTriggerPrice": null, + # "timeInForce": "GTC", + # "triggerBy": null, + # "triggerPrice": "4300", + # "triggerQuantity": "0.001" + # } + # + timestamp = self.safe_integer(order, 'createdAt') + timestamp2 = self.parse8601(self.safe_string(order, 'createdAt')) + if timestamp2 is not None: + timestamp = timestamp2 + id = self.safe_string(order, 'id') + clientOrderId = self.safe_string(order, 'clientId') + symbol = self.safe_symbol(self.safe_string(order, 'symbol'), market) + type = self.safe_string_lower(order, 'orderType') + timeInForce = self.safe_string(order, 'timeInForce') + side = self.parse_order_side(self.safe_string(order, 'side')) + amount = self.safe_string_2(order, 'quantity', 'triggerQuantity') + price = self.safe_string(order, 'price') + cost = self.safe_string(order, 'executedQuoteQuantity') + status = self.parse_order_status(self.safe_string(order, 'status')) + triggerPrice = self.safe_string(order, 'triggerPrice') + filled = self.safe_string(order, 'executedQuantity') + reduceOnly = self.safe_bool(order, 'reduceOnly') + postOnly = self.safe_bool(order, 'postOnly') + stopLossPrice = self.safe_string_2(order, 'stopLossLimitPrice', 'stopLossTriggerPrice') + takeProfitPrice = self.safe_string_2(order, 'takeProfitLimitPrice', 'takeProfitTriggerPrice') + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'symbol': symbol, + 'type': type, + 'timeInForce': timeInForce, + 'postOnly': postOnly, + 'reduceOnly': reduceOnly, + 'side': side, + 'price': price, + 'triggerPrice': triggerPrice, + 'stopLossPrice': stopLossPrice, + 'takeProfitPrice': takeProfitPrice, + 'amount': amount, + 'cost': cost, + 'average': None, + 'filled': filled, + 'remaining': None, + 'status': status, + 'fee': None, + 'trades': None, + }, market) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'New': 'open', + 'Filled': 'closed', + 'Cancelled': 'canceled', + 'Expired': 'canceled', + 'PartiallyFilled': 'open', + 'TriggerPending': 'open', + 'TriggerFailed': 'rejected', + } + return self.safe_string(statuses, status, status) + + def parse_order_side(self, side: Str): + sides: dict = { + 'Bid': 'buy', + 'Ask': 'sell', + } + return self.safe_string(sides, side, side) + + async def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://docs.backpack.exchange/#tag/Futures/operation/get_positions + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + await self.load_markets() + response = await self.privateGetApiV1Position(params) + positions = self.parse_positions(response) + if self.is_empty(symbols): + return positions + symbols = self.market_symbols(symbols) + return self.filter_by_array_positions(positions, 'symbol', symbols, False) + + def parse_position(self, position: dict, market: Market = None): + # + # fetchPositions + # { + # "breakEvenPrice": "3831.3630555555555555555555556", + # "cumulativeFundingPayment": "-0.009218", + # "cumulativeInterest": "0", + # "entryPrice": "3826.8888888888888888888888889", + # "estLiquidationPrice": "0", + # "imf": "0.02", + # "imfFunction": { + # "base": "0.02", + # "factor": "0.0000935", + # "type": "sqrt" + # }, + # "markPrice": "3787.46813304", + # "mmf": "0.0125", + # "mmfFunction": { + # "base": "0.0125", + # "factor": "0.0000561", + # "type": "sqrt" + # }, + # "netCost": "13.7768", + # "netExposureNotional": "13.634885278944", + # "netExposureQuantity": "0.0036", + # "netQuantity": "0.0036", + # "pnlRealized": "0", + # "pnlUnrealized": "-0.141914", + # "positionId": "4238420454", + # "subaccountId": null, + # "symbol": "ETH_USDC_PERP", + # "userId":1813870 + # } + # + # + id = self.safe_string(position, 'positionId') + marketId = self.safe_string(position, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + entryPrice = self.safe_string(position, 'entryPrice') + markPrice = self.safe_string(position, 'markPrice') + netCost = self.safe_string(position, 'netCost') + hedged = False + side = 'long' + if Precise.string_lt(netCost, '0'): + side = 'short' + if netCost is None: + hedged = None + side = None + unrealizedPnl = self.safe_string(position, 'pnlUnrealized') + realizedPnl = self.safe_string(position, 'pnlRealized') + liquidationPrice = self.safe_string(position, 'estLiquidationPrice') + return self.safe_position({ + 'info': position, + 'id': id, + 'symbol': symbol, + 'timestamp': self.parse8601(self.safe_string(position, 'timestamp')), + 'datetime': self.iso8601(self.parse8601(self.safe_string(position, 'timestamp'))), + 'lastUpdateTimestamp': None, + 'hedged': hedged, + 'side': side, + 'contracts': self.safe_string(position, 'netExposureQuantity'), + 'contractSize': None, + 'entryPrice': entryPrice, + 'markPrice': markPrice, + 'lastPrice': None, + 'notional': Precise.string_abs(netCost), + 'leverage': None, + 'collateral': None, + 'initialMargin': None, + 'initialMarginPercentage': self.safe_string(position, 'imf'), + 'maintenanceMargin': None, + 'maintenanceMarginPercentage': self.safe_string(position, 'mmf'), + 'realizedPnl': realizedPnl, + 'unrealizedPnl': unrealizedPnl, + 'liquidationPrice': liquidationPrice, + 'marginMode': None, + 'marginRatio': None, + 'percentage': None, + 'stopLossPrice': None, + 'takeProfitPrice': None, + }) + + async def fetch_funding_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches the history of funding payments + + https://docs.backpack.exchange/#tag/History/operation/get_funding_payments + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch(default 24 hours ago) + :param int [limit]: the maximum amount of trades to fetch(default 200, max 500) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest trade to fetch(default now) + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if limit is not None: + request['limit'] = limit + response = await self.privateGetWapiV1HistoryFunding(self.extend(request, params)) + return self.parse_incomes(response, market, since, limit) + + def parse_income(self, income, market: Market = None): + # + # { + # "fundingRate": "0.0001", + # "intervalEndTimestamp": "2025-08-01T16:00:00", + # "quantity": "-0.001301", + # "subaccountId": 0, + # "symbol": "ETH_USDC_PERP", + # "userId": 1813870 + # } + # + marketId = self.safe_string(income, 'symbol') + symbol = self.safe_symbol(marketId, market) + amount = self.safe_number(income, 'quantity') + id = self.safe_string(income, 'userId') + timestamp = self.parse8601(self.safe_string(income, 'intervalEndTimestamp')) + rate = self.safe_number(income, 'fundingRate') + return { + 'info': income, + 'symbol': symbol, + 'code': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': id, + 'amount': amount, + 'rate': rate, + } + + def nonce(self): + return self.milliseconds() - self.options['timeDifference'] + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + endpoint = '/' + path + url = self.urls['api'][api] + sortedParams = params if isinstance(params, list) else self.keysort(params) + if api == 'private': + self.check_required_credentials() + ts = str(self.nonce()) + recvWindow = self.safe_string_2(self.options, 'recvWindow', 'X-Window', '5000') + optionInstructions = self.safe_dict(self.options, 'instructions', {}) + optionPathInstructions = self.safe_dict(optionInstructions, path, {}) + instruction = self.safe_string(optionPathInstructions, method, '') + payload = '' + if (path == 'api/v1/orders') and (method == 'POST'): # for createOrders + payload = self.generate_batch_payload(sortedParams, ts, recvWindow, instruction) + else: + queryString = self.urlencode(sortedParams) + if len(queryString) > 0: + queryString += '&' + payload = 'instruction=' + instruction + '&' + queryString + 'timestamp=' + ts + '&window=' + recvWindow + secretBytes = self.base64_to_binary(self.secret) + seed = self.array_slice(secretBytes, 0, 32) + signature = self.eddsa(self.encode(payload), seed, 'ed25519') + headers = { + 'X-Timestamp': ts, + 'X-Window': recvWindow, + 'X-API-Key': self.apiKey, + 'X-Signature': signature, + 'X-Broker-Id': '1400', + } + if method != 'GET': + body = self.json(sortedParams) + headers['Content-Type'] = 'application/json' + if method == 'GET': + query = self.urlencode(sortedParams) + if len(query) != 0: + endpoint += '?' + query + url += endpoint + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def generate_batch_payload(self, params, ts, recvWindow, instruction): + payload = '' + for i in range(0, len(params)): + order = self.safe_dict(params, i, {}) + sortedOrder = self.keysort(order) + orderQuery = self.urlencode(sortedOrder) + payload += 'instruction=' + instruction + '&' + orderQuery + '&' + if i == (len(params) - 1): + payload += 'timestamp=' + ts + '&window=' + recvWindow + return payload + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None # fallback to default error handler + # + # {"code":"INVALID_ORDER","message":"Invalid order"} + # {"code":"INVALID_CLIENT_REQUEST","message":"Must specify both `triggerPrice` and `triggerQuantity` or neither"} + # + errorCode = self.safe_string(response, 'code') + message = self.safe_string(response, 'message') + if errorCode is not None: + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + raise ExchangeError(feedback) # unknown message + return None diff --git a/ccxt/async_support/base/__init__.py b/ccxt/async_support/base/__init__.py new file mode 100644 index 0000000..7ff6eae --- /dev/null +++ b/ccxt/async_support/base/__init__.py @@ -0,0 +1 @@ +# this file is a stub so that files inside of ccxt/rest are loaded diff --git a/ccxt/async_support/base/__pycache__/__init__.cpython-311.pyc b/ccxt/async_support/base/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..53dc86b Binary files /dev/null and b/ccxt/async_support/base/__pycache__/__init__.cpython-311.pyc differ diff --git a/ccxt/async_support/base/__pycache__/exchange.cpython-311.pyc b/ccxt/async_support/base/__pycache__/exchange.cpython-311.pyc new file mode 100644 index 0000000..2298a68 Binary files /dev/null and b/ccxt/async_support/base/__pycache__/exchange.cpython-311.pyc differ diff --git a/ccxt/async_support/base/__pycache__/throttler.cpython-311.pyc b/ccxt/async_support/base/__pycache__/throttler.cpython-311.pyc new file mode 100644 index 0000000..b0126a7 Binary files /dev/null and b/ccxt/async_support/base/__pycache__/throttler.cpython-311.pyc differ diff --git a/ccxt/async_support/base/exchange.py b/ccxt/async_support/base/exchange.py new file mode 100644 index 0000000..e92cd77 --- /dev/null +++ b/ccxt/async_support/base/exchange.py @@ -0,0 +1,2308 @@ +# -*- coding: utf-8 -*- + +# ----------------------------------------------------------------------------- + +__version__ = '4.5.18' + +# ----------------------------------------------------------------------------- + +import asyncio +import concurrent.futures +import socket +import certifi +import aiohttp +import ssl +import sys +import yarl +import math +from typing import Any, List +from ccxt.base.types import Int, Str, Num, Strings + +# ----------------------------------------------------------------------------- + +from ccxt.async_support.base.throttler import Throttler + +# ----------------------------------------------------------------------------- + +from ccxt.base.errors import BaseError, BadSymbol, BadRequest, BadResponse, ExchangeError, ExchangeNotAvailable, RequestTimeout, NotSupported, NullResponse, InvalidAddress, RateLimitExceeded, OperationFailed +from ccxt.base.types import ConstructorArgs, OrderType, OrderSide, OrderRequest, CancellationRequest, Order + +# ----------------------------------------------------------------------------- + +from ccxt.base.exchange import Exchange as BaseExchange, ArgumentsRequired + +# ----------------------------------------------------------------------------- + +from ccxt.async_support.base.ws.functions import inflate, inflate64, gunzip +from ccxt.async_support.base.ws.client import Client +from ccxt.async_support.base.ws.future import Future +from ccxt.async_support.base.ws.order_book import OrderBook, IndexedOrderBook, CountedOrderBook + + +# ----------------------------------------------------------------------------- + +try: + from aiohttp_socks import ProxyConnector as SocksProxyConnector +except ImportError: + SocksProxyConnector = None + +# ----------------------------------------------------------------------------- + +__all__ = [ + 'BaseExchange', + 'Exchange', +] + +# ----------------------------------------------------------------------------- +# --- PROTO BUF IMPORTS +try: + from ccxt.protobuf.mexc import PushDataV3ApiWrapper_pb2 + from google.protobuf.json_format import MessageToDict +except ImportError: + PushDataV3ApiWrapper_pb2 = None + MessageToDict = None + +# ----------------------------------------------------------------------------- + + +class Exchange(BaseExchange): + synchronous = False + streaming = { + 'maxPingPongMisses': 2, + 'keepAlive': 30000 + } + ping = None + newUpdates = True + clients = {} + timeout_on_exit = 250 # needed for: https://github.com/ccxt/ccxt/pull/23470 + + def __init__(self, config: ConstructorArgs = {}): + if 'asyncio_loop' in config: + self.asyncio_loop = config['asyncio_loop'] + self.aiohttp_trust_env = config.get('aiohttp_trust_env', self.aiohttp_trust_env) + self.verify = config.get('verify', self.verify) + self.own_session = 'session' not in config + self.cafile = config.get('cafile', certifi.where()) + self.throttler = None + super(Exchange, self).__init__(config) + self.markets_loading = None + self.reloading_markets = False + + def get_event_loop(self): + return self.asyncio_loop + + def init_throttler(self, cost=None): + self.throttler = Throttler(self.tokenBucket, self.asyncio_loop) + + async def throttle(self, cost=None): + return await self.throttler(cost) + + def get_session(self): + return self.session + + def __del__(self): + if self.session is not None or self.socks_proxy_sessions is not None: + self.logger.warning(self.id + " requires to release all resources with an explicit call to the .close() coroutine. If you are using the exchange instance with async coroutines, add `await exchange.close()` to your code into a place when you're done with the exchange and don't need the exchange instance anymore (at the end of your async coroutine).") + + if sys.version_info >= (3, 5): + async def __aenter__(self): + self.open() + return self + + async def __aexit__(self, exc_type, exc, tb): + await self.close() + + def open(self): + if self.asyncio_loop is None: + if sys.version_info >= (3, 7): + self.asyncio_loop = asyncio.get_running_loop() + else: + self.asyncio_loop = asyncio.get_event_loop() + self.throttler.loop = self.asyncio_loop + + if self.ssl_context is None: + # Create our SSL context object with our CA cert file + self.ssl_context = ssl.create_default_context(cafile=self.cafile) if self.verify else self.verify + if (self.ssl_context and self.safe_bool(self.options, 'include_OS_certificates', False)): + os_default_paths = ssl.get_default_verify_paths() + if os_default_paths.cafile and os_default_paths.cafile != self.cafile: + self.ssl_context.load_verify_locations(cafile=os_default_paths.cafile) + + if self.own_session and self.session is None: + # Pass this SSL context to aiohttp and create a TCPConnector + self.tcp_connector = aiohttp.TCPConnector(ssl=self.ssl_context, loop=self.asyncio_loop, enable_cleanup_closed=True) + self.session = aiohttp.ClientSession(loop=self.asyncio_loop, connector=self.tcp_connector, trust_env=self.aiohttp_trust_env) + + async def close(self): + await self.ws_close() + if self.session is not None: + if self.own_session: + await self.session.close() + self.session = None + await self.close_connector() + await self.close_proxy_sessions() + await self.sleep(self.timeout_on_exit) + + async def close_connector(self): + if self.tcp_connector is not None: + await self.tcp_connector.close() + self.tcp_connector = None + if self.aiohttp_socks_connector is not None: + await self.aiohttp_socks_connector.close() + self.aiohttp_socks_connector = None + + async def close_proxy_sessions(self): + if self.socks_proxy_sessions is not None: + for url in self.socks_proxy_sessions: + await self.socks_proxy_sessions[url].close() + self.socks_proxy_sessions = None + + async def fetch(self, url, method='GET', headers=None, body=None): + """Perform a HTTP request and return decoded JSON data""" + + # ##### PROXY & HEADERS ##### + request_headers = self.prepare_request_headers(headers) + self.last_request_headers = request_headers + # proxy-url + proxyUrl = self.check_proxy_url_settings(url, method, headers, body) + if proxyUrl is not None: + request_headers.update({'Origin': self.origin}) + url = proxyUrl + self.url_encoder_for_proxy_url(url) + # proxy agents + final_proxy = None # set default + proxy_session = None + httpProxy, httpsProxy, socksProxy = self.check_proxy_settings(url, method, headers, body) + if httpProxy: + final_proxy = httpProxy + elif httpsProxy: + final_proxy = httpsProxy + elif socksProxy: + if SocksProxyConnector is None: + raise NotSupported(self.id + ' - to use SOCKS proxy with ccxt, you need "aiohttp_socks" module that can be installed by "pip install aiohttp_socks"') + # override session + if (self.socks_proxy_sessions is None): + self.socks_proxy_sessions = {} + if (socksProxy not in self.socks_proxy_sessions): + # Create our SSL context object with our CA cert file + self.open() # ensure `asyncio_loop` is set + proxy_session = self.get_socks_proxy_session(socksProxy) + # add aiohttp_proxy for python as exclusion + elif self.aiohttp_proxy: + final_proxy = self.aiohttp_proxy + + proxyAgentSet = final_proxy is not None or socksProxy is not None + self.checkConflictingProxies(proxyAgentSet, proxyUrl) + + # avoid old proxies mixing + if (self.aiohttp_proxy is not None) and (proxyUrl is not None or httpProxy is not None or httpsProxy is not None or socksProxy is not None): + raise NotSupported(self.id + ' you have set multiple proxies, please use one or another') + + # log + if self.verbose: + self.log("\nfetch Request:", self.id, method, url, "RequestHeaders:", request_headers, "RequestBody:", body) + self.logger.debug("%s %s, Request: %s %s", method, url, headers, body) + # end of proxies & headers + + request_body = body + encoded_body = body.encode() if body else None + self.open() + final_session = proxy_session if proxy_session is not None else self.session + session_method = getattr(final_session, method.lower()) + + http_response = None + http_status_code = None + http_status_text = None + json_response = None + try: + async with session_method(yarl.URL(url, encoded=True), + data=encoded_body, + headers=request_headers, + timeout=(self.timeout / 1000), + proxy=final_proxy) as response: + http_response = await response.text(errors='replace') + # CIMultiDictProxy + raw_headers = response.headers + headers = {} + for header in raw_headers: + if header in headers: + headers[header] = headers[header] + ', ' + raw_headers[header] + else: + headers[header] = raw_headers[header] + http_status_code = response.status + http_status_text = response.reason + http_response = self.on_rest_response(http_status_code, http_status_text, url, method, headers, http_response, request_headers, request_body) + json_response = self.parse_json(http_response) + if self.enableLastHttpResponse: + self.last_http_response = http_response + if self.enableLastResponseHeaders: + self.last_response_headers = headers + if self.enableLastJsonResponse: + self.last_json_response = json_response + if self.verbose: + self.log("\nfetch Response:", self.id, method, url, http_status_code, "ResponseHeaders:", headers, "ResponseBody:", http_response) + if json_response and not isinstance(json_response, list) and self.returnResponseHeaders: + json_response['responseHeaders'] = headers + self.logger.debug("%s %s, Response: %s %s %s", method, url, http_status_code, headers, http_response) + + except socket.gaierror as e: + details = ' '.join([self.id, method, url]) + raise ExchangeNotAvailable(details) from e + + except (concurrent.futures.TimeoutError, asyncio.TimeoutError) as e: + details = ' '.join([self.id, method, url]) + raise RequestTimeout(details) from e + + except aiohttp.ClientConnectionError as e: + details = ' '.join([self.id, method, url]) + raise ExchangeNotAvailable(details) from e + + except aiohttp.ClientError as e: # base exception class + details = ' '.join([self.id, method, url]) + raise ExchangeError(details) from e + + self.handle_errors(http_status_code, http_status_text, url, method, headers, http_response, json_response, request_headers, request_body) + self.handle_http_status_code(http_status_code, http_status_text, url, method, http_response) + if json_response is not None: + return json_response + if self.is_text_response(headers): + return http_response + if http_response == '' or http_response is None: + return http_response + return response.content + + def get_socks_proxy_session(self, socksProxy): + if (self.socks_proxy_sessions is None): + self.socks_proxy_sessions = {} + if (socksProxy not in self.socks_proxy_sessions): + reverse_dns = socksProxy.startswith('socks5h://') + socks_proxy_selected = socksProxy if not reverse_dns else socksProxy.replace('socks5h://', 'socks5://') + self.aiohttp_socks_connector = SocksProxyConnector.from_url( + socks_proxy_selected, + # extra args copied from self.open() + ssl=self.ssl_context, + loop=self.asyncio_loop, + enable_cleanup_closed=True, + rdns=reverse_dns if reverse_dns else None + ) + self.socks_proxy_sessions[socksProxy] = aiohttp.ClientSession(loop=self.asyncio_loop, connector=self.aiohttp_socks_connector, trust_env=self.aiohttp_trust_env) + return self.socks_proxy_sessions[socksProxy] + + async def load_markets_helper(self, reload=False, params={}): + if not reload: + if self.markets: + if not self.markets_by_id: + return self.set_markets(self.markets) + return self.markets + currencies = None + if self.has['fetchCurrencies'] is True: + currencies = await self.fetch_currencies() + self.options['cachedCurrencies'] = currencies + markets = await self.fetch_markets(params) + if 'cachedCurrencies' in self.options: + del self.options['cachedCurrencies'] + return self.set_markets(markets, currencies) + + + async def load_markets(self, reload=False, params={}): + """ + Loads and prepares the markets for trading. + + Args: + reload (bool): If True, the markets will be reloaded from the exchange. + params (dict): Additional exchange-specific parameters for the request. + + Returns: + dict: A dictionary of markets. + + Raises: + Exception: If the markets cannot be loaded or prepared. + + Notes: + This method is asynchronous. + It ensures that the markets are only loaded once, even if called multiple times. + If the markets are already loaded and `reload` is False or not provided, it returns the existing markets. + If a reload is in progress, it waits for completion before returning. + If an error occurs during loading or preparation, an exception is raised. + """ + if (reload and not self.reloading_markets) or not self.markets_loading: + self.reloading_markets = True + coroutine = self.load_markets_helper(reload, params) + # coroutines can only be awaited once so we wrap it in a task + self.markets_loading = asyncio.ensure_future(coroutine) + try: + result = await self.markets_loading + except asyncio.CancelledError as e: # CancelledError is a base exception so we need to catch it explicitly + self.reloading_markets = False + self.markets_loading = None + raise e + except Exception as e: + self.reloading_markets = False + self.markets_loading = None + raise e + self.reloading_markets = False + return result + + async def load_fees(self, reload=False): + if not reload: + if self.loaded_fees != Exchange.loaded_fees: + return self.loaded_fees + self.loaded_fees = self.deep_extend(self.loaded_fees, await self.fetch_fees()) + return self.loaded_fees + + async def fetch_markets(self, params={}): + # markets are returned as a list + # currencies are returned as a dict + # this is for historical reasons + # and may be changed for consistency later + return self.to_array(self.markets) + + async def fetch_currencies(self, params={}): + # markets are returned as a list + # currencies are returned as a dict + # this is for historical reasons + # and may be changed for consistency later + return self.currencies + + async def fetchOHLCVC(self, symbol, timeframe='1m', since=None, limit=None, params={}): + return await self.fetch_ohlcvc(symbol, timeframe, since, limit, params) + + async def fetch_full_tickers(self, symbols=None, params={}): + return await self.fetch_tickers(symbols, params) + + async def sleep(self, milliseconds): + return await asyncio.sleep(milliseconds / 1000) + + async def spawn_async(self, method, *args): + try: + await method(*args) + except Exception: + # todo: handle spawned errors + pass + + def spawn(self, method, *args): + def callback(asyncio_future): + exception = asyncio_future.exception() + if exception is None: + future.resolve(asyncio_future.result()) + else: + future.reject(exception) + future = Future() + task = self.asyncio_loop.create_task(method(*args)) + task.add_done_callback(callback) + return future + + # ----------------------------------------------------------------------- + # WS/PRO code + + @staticmethod + def inflate(data): + return inflate(data) + + @staticmethod + def inflate64(data): + return inflate64(data) + + @staticmethod + def gunzip(data): + return gunzip(data) + + def order_book(self, snapshot={}, depth=None): + return OrderBook(snapshot, depth) + + def indexed_order_book(self, snapshot={}, depth=None): + return IndexedOrderBook(snapshot, depth) + + def counted_order_book(self, snapshot={}, depth=None): + return CountedOrderBook(snapshot, depth) + + def client(self, url): + self.clients = self.clients or {} + if url not in self.clients: + on_message = self.handle_message + on_error = self.on_error + on_close = self.on_close + on_connected = self.on_connected + # decide client type here: aiohttp ws / websockets / signalr / socketio + ws_options = self.safe_value(self.options, 'ws', {}) + options = self.extend(self.streaming, { + 'log': getattr(self, 'log'), + 'ping': getattr(self, 'ping', None), + 'verbose': self.verbose, + 'throttle': Throttler(self.tokenBucket, self.asyncio_loop), + 'asyncio_loop': self.asyncio_loop, + 'decompressBinary': self.safe_bool(self.options, 'decompressBinary', True), + }, ws_options) + # we use aiohttp instead of fastClient now because of this + # https://github.com/ccxt/ccxt/pull/25995 + self.clients[url] = Client(url, on_message, on_error, on_close, on_connected, options) + # set http/s proxy (socks proxy should be set in other place) + httpProxy, httpsProxy, socksProxy = self.check_ws_proxy_settings() + if (httpProxy or httpsProxy): + self.clients[url].proxy = httpProxy if httpProxy else httpsProxy + return self.clients[url] + + def delay(self, timeout, method, *args): + return self.asyncio_loop.call_later(timeout / 1000, self.spawn, method, *args) + + def handle_message(self, client, message): + always = True + if always: + raise NotSupported(self.id + '.handle_message() not implemented yet') + return {} + + def watch_multiple(self, url, message_hashes, message=None, subscribe_hashes=None, subscription=None): + # base exchange self.open starts the aiohttp Session in an async context + self.open() + backoff_delay = 0 + client = self.client(url) + + future = Future.race([client.future(message_hash) for message_hash in message_hashes]) + + missing_subscriptions = [] + if subscribe_hashes is not None: + for subscribe_hash in subscribe_hashes: + if subscribe_hash not in client.subscriptions: + missing_subscriptions.append(subscribe_hash) + client.subscriptions[subscribe_hash] = subscription or True + + connected = client.connected if client.connected.done() \ + else asyncio.ensure_future(client.connect(self.session, backoff_delay)) + + def after(fut): + # todo: decouple signing from subscriptions + options = self.safe_value(self.options, 'ws') + cost = self.safe_value(options, 'cost', 1) + if message: + async def send_message(): + if self.enableRateLimit: + await client.throttle(cost) + try: + await client.send(message) + except ConnectionError as e: + client.on_error(e) + except Exception as e: + client.on_error(e) + asyncio.ensure_future(send_message()) + + if missing_subscriptions: + connected.add_done_callback(after) + + return future + + def watch(self, url, message_hash, message=None, subscribe_hash=None, subscription=None): + # base exchange self.open starts the aiohttp Session in an async context + self.open() + backoff_delay = 0 + client = self.client(url) + if subscribe_hash is None and message_hash in client.futures: + return client.futures[message_hash] + future = client.future(message_hash) + + subscribed = client.subscriptions.get(subscribe_hash) + + if not subscribed: + client.subscriptions[subscribe_hash] = subscription or True + + selected_session = self.session + # http/s proxy is being set in other places + httpProxy, httpsProxy, socksProxy = self.check_ws_proxy_settings() + if (socksProxy): + selected_session = self.get_socks_proxy_session(socksProxy) + connected = client.connected if client.connected.done() \ + else asyncio.ensure_future(client.connect(selected_session, backoff_delay)) + + def after(fut): + # todo: decouple signing from subscriptions + options = self.safe_value(self.options, 'ws') + cost = self.safe_value(options, 'cost', 1) + if message: + async def send_message(): + if self.enableRateLimit: + await client.throttle(cost) + try: + await client.send(message) + except ConnectionError as e: + client.on_error(e) + except Exception as e: + client.on_error(e) + asyncio.ensure_future(send_message()) + + if not subscribed: + connected.add_done_callback(after) + + return future + + def on_connected(self, client, message=None): + # for user hooks + # print('Connected to', client.url) + pass + + def on_error(self, client, error): + if client.url in self.clients and self.clients[client.url].error: + del self.clients[client.url] + + def on_close(self, client, error): + if client.error: + # connection closed by the user or due to an error + pass + else: + # server disconnected a working connection + if client.url in self.clients: + del self.clients[client.url] + + async def ws_close(self): + if self.clients: + await asyncio.wait([asyncio.create_task(client.close()) for client in self.clients.values()], return_when=asyncio.ALL_COMPLETED) + for url in self.clients.copy(): + del self.clients[url] + + async def load_order_book(self, client, messageHash, symbol, limit=None, params={}): + if symbol not in self.orderbooks: + client.reject(ExchangeError(self.id + ' loadOrderBook() orderbook is not initiated'), messageHash) + return + try: + maxRetries = self.handle_option('watchOrderBook', 'maxRetries', 3) + tries = 0 + stored = self.orderbooks[symbol] + while tries < maxRetries: + cache = stored.cache + order_book = await self.fetch_order_book(symbol, limit, params) + index = self.get_cache_index(order_book, cache) + if index >= 0: + stored.reset(order_book) + self.handle_deltas(stored, cache[index:]) + cache.clear() + client.resolve(stored, messageHash) + return + tries += 1 + client.reject(ExchangeError(self.id + ' nonce is behind cache after ' + str(maxRetries) + ' tries.'), messageHash) + del self.clients[client.url] + except BaseError as e: + client.reject(e, messageHash) + await self.load_order_book(client, messageHash, symbol, limit, params) + + def format_scientific_notation_ftx(self, n): + if n == 0: + return '0e-00' + return format(n, 'g') + + def decode_proto_msg(self, data): + if not MessageToDict: + raise NotSupported(self.id + ' requires protobuf to decode messages, please install it with `pip install "protobuf==5.29.5"`') + message = PushDataV3ApiWrapper_pb2.PushDataV3ApiWrapper() + message.ParseFromString(data) + dict_msg = MessageToDict(message) + # { + # "channel":"spot@public.kline.v3.api.pb@BTCUSDT@Min1", + # "symbol":"BTCUSDT", + # "symbolId":"2fb942154ef44a4ab2ef98c8afb6a4a7", + # "createTime":"1754735110559", + # "publicSpotKline":{ + # "interval":"Min1", + # "windowStart":"1754735100", + # "openingPrice":"117792.45", + # "closingPrice":"117805.32", + # "highestPrice":"117814.63", + # "lowestPrice":"117792.45", + # "volume":"0.13425465", + # "amount":"15815.77", + # "windowEnd":"1754735160" + # } + # } + return dict_msg + + # ######################################################################## + # ######################################################################## + # ######################################################################## + # ######################################################################## + # ######## ######## ######## + # ######## ######## ######## + # ######## ######## ######## + # ######## ######## ######## + # ######## ######################## ######################## + # ######## ######################## ######################## + # ######## ######################## ######################## + # ######## ######################## ######################## + # ######## ######## ######## + # ######## ######## ######## + # ######## ######## ######## + # ######## ######## ######## + # ######################################################################## + # ######################################################################## + # ######################################################################## + # ######################################################################## + # ######## ######## ######## ######## + # ######## ######## ######## ######## + # ######## ######## ######## ######## + # ######## ######## ######## ######## + # ################ ######################## ################ + # ################ ######################## ################ + # ################ ######################## ################ + # ################ ######################## ################ + # ######## ######## ################ ################ + # ######## ######## ################ ################ + # ######## ######## ################ ################ + # ######## ######## ################ ################ + # ######################################################################## + # ######################################################################## + # ######################################################################## + # ######################################################################## + + # METHODS BELOW THIS LINE ARE TRANSPILED FROM TYPESCRIPT + + async def fetch_accounts(self, params={}): + raise NotSupported(self.id + ' fetchAccounts() is not supported yet') + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' fetchTrades() is not supported yet') + + async def fetch_trades_ws(self, symbol: str, since: Int = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' fetchTradesWs() is not supported yet') + + async def watch_liquidations(self, symbol: str, since: Int = None, limit: Int = None, params={}): + if self.has['watchLiquidationsForSymbols']: + return await self.watch_liquidations_for_symbols([symbol], since, limit, params) + raise NotSupported(self.id + ' watchLiquidations() is not supported yet') + + async def watch_liquidations_for_symbols(self, symbols: List[str], since: Int = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' watchLiquidationsForSymbols() is not supported yet') + + async def watch_my_liquidations(self, symbol: str, since: Int = None, limit: Int = None, params={}): + if self.has['watchMyLiquidationsForSymbols']: + return self.watch_my_liquidations_for_symbols([symbol], since, limit, params) + raise NotSupported(self.id + ' watchMyLiquidations() is not supported yet') + + async def watch_my_liquidations_for_symbols(self, symbols: List[str], since: Int = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' watchMyLiquidationsForSymbols() is not supported yet') + + async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' watchTrades() is not supported yet') + + async def un_watch_orders(self, symbol: Str = None, params={}): + raise NotSupported(self.id + ' unWatchOrders() is not supported yet') + + async def un_watch_trades(self, symbol: str, params={}): + raise NotSupported(self.id + ' unWatchTrades() is not supported yet') + + async def watch_trades_for_symbols(self, symbols: List[str], since: Int = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' watchTradesForSymbols() is not supported yet') + + async def un_watch_trades_for_symbols(self, symbols: List[str], params={}): + raise NotSupported(self.id + ' unWatchTradesForSymbols() is not supported yet') + + async def watch_my_trades_for_symbols(self, symbols: List[str], since: Int = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' watchMyTradesForSymbols() is not supported yet') + + async def watch_orders_for_symbols(self, symbols: List[str], since: Int = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' watchOrdersForSymbols() is not supported yet') + + async def watch_ohlcv_for_symbols(self, symbolsAndTimeframes: List[List[str]], since: Int = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' watchOHLCVForSymbols() is not supported yet') + + async def un_watch_ohlcv_for_symbols(self, symbolsAndTimeframes: List[List[str]], params={}): + raise NotSupported(self.id + ' unWatchOHLCVForSymbols() is not supported yet') + + async def watch_order_book_for_symbols(self, symbols: List[str], limit: Int = None, params={}): + raise NotSupported(self.id + ' watchOrderBookForSymbols() is not supported yet') + + async def un_watch_order_book_for_symbols(self, symbols: List[str], params={}): + raise NotSupported(self.id + ' unWatchOrderBookForSymbols() is not supported yet') + + async def un_watch_positions(self, symbols: Strings = None, params={}): + raise NotSupported(self.id + ' unWatchPositions() is not supported yet') + + async def un_watch_ticker(self, symbol: str, params={}): + raise NotSupported(self.id + ' unWatchTicker() is not supported yet') + + async def un_watch_mark_price(self, symbol: str, params={}): + raise NotSupported(self.id + ' unWatchMarkPrice() is not supported yet') + + async def un_watch_mark_prices(self, symbols: Strings = None, params={}): + raise NotSupported(self.id + ' unWatchMarkPrices() is not supported yet') + + async def fetch_deposit_addresses(self, codes: Strings = None, params={}): + raise NotSupported(self.id + ' fetchDepositAddresses() is not supported yet') + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}): + raise NotSupported(self.id + ' fetchOrderBook() is not supported yet') + + async def fetch_order_book_ws(self, symbol: str, limit: Int = None, params={}): + raise NotSupported(self.id + ' fetchOrderBookWs() is not supported yet') + + async def fetch_margin_mode(self, symbol: str, params={}): + if self.has['fetchMarginModes']: + marginModes = await self.fetch_margin_modes([symbol], params) + return self.safe_dict(marginModes, symbol) + else: + raise NotSupported(self.id + ' fetchMarginMode() is not supported yet') + + async def fetch_margin_modes(self, symbols: Strings = None, params={}): + raise NotSupported(self.id + ' fetchMarginModes() is not supported yet') + + async def fetch_rest_order_book_safe(self, symbol, limit=None, params={}): + fetchSnapshotMaxRetries = self.handle_option('watchOrderBook', 'maxRetries', 3) + for i in range(0, fetchSnapshotMaxRetries): + try: + orderBook = await self.fetch_order_book(symbol, limit, params) + return orderBook + except Exception as e: + if (i + 1) == fetchSnapshotMaxRetries: + raise e + return None + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}): + raise NotSupported(self.id + ' watchOrderBook() is not supported yet') + + async def un_watch_order_book(self, symbol: str, params={}): + raise NotSupported(self.id + ' unWatchOrderBook() is not supported yet') + + async def fetch_time(self, params={}): + raise NotSupported(self.id + ' fetchTime() is not supported yet') + + async def fetch_trading_limits(self, symbols: Strings = None, params={}): + raise NotSupported(self.id + ' fetchTradingLimits() is not supported yet') + + async def fetch_cross_borrow_rates(self, params={}): + raise NotSupported(self.id + ' fetchCrossBorrowRates() is not supported yet') + + async def fetch_isolated_borrow_rates(self, params={}): + raise NotSupported(self.id + ' fetchIsolatedBorrowRates() is not supported yet') + + async def fetch_leverage_tiers(self, symbols: Strings = None, params={}): + raise NotSupported(self.id + ' fetchLeverageTiers() is not supported yet') + + async def fetch_funding_rates(self, symbols: Strings = None, params={}): + raise NotSupported(self.id + ' fetchFundingRates() is not supported yet') + + async def fetch_funding_intervals(self, symbols: Strings = None, params={}): + raise NotSupported(self.id + ' fetchFundingIntervals() is not supported yet') + + async def watch_funding_rate(self, symbol: str, params={}): + raise NotSupported(self.id + ' watchFundingRate() is not supported yet') + + async def watch_funding_rates(self, symbols: List[str], params={}): + raise NotSupported(self.id + ' watchFundingRates() is not supported yet') + + async def watch_funding_rates_for_symbols(self, symbols: List[str], params={}): + return await self.watch_funding_rates(symbols, params) + + async def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}): + raise NotSupported(self.id + ' transfer() is not supported yet') + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}): + raise NotSupported(self.id + ' withdraw() is not supported yet') + + async def create_deposit_address(self, code: str, params={}): + raise NotSupported(self.id + ' createDepositAddress() is not supported yet') + + async def set_leverage(self, leverage: int, symbol: Str = None, params={}): + raise NotSupported(self.id + ' setLeverage() is not supported yet') + + async def fetch_leverage(self, symbol: str, params={}): + if self.has['fetchLeverages']: + leverages = await self.fetch_leverages([symbol], params) + return self.safe_dict(leverages, symbol) + else: + raise NotSupported(self.id + ' fetchLeverage() is not supported yet') + + async def fetch_leverages(self, symbols: Strings = None, params={}): + raise NotSupported(self.id + ' fetchLeverages() is not supported yet') + + async def set_position_mode(self, hedged: bool, symbol: Str = None, params={}): + raise NotSupported(self.id + ' setPositionMode() is not supported yet') + + async def add_margin(self, symbol: str, amount: float, params={}): + raise NotSupported(self.id + ' addMargin() is not supported yet') + + async def reduce_margin(self, symbol: str, amount: float, params={}): + raise NotSupported(self.id + ' reduceMargin() is not supported yet') + + async def set_margin(self, symbol: str, amount: float, params={}): + raise NotSupported(self.id + ' setMargin() is not supported yet') + + async def fetch_long_short_ratio(self, symbol: str, timeframe: Str = None, params={}): + raise NotSupported(self.id + ' fetchLongShortRatio() is not supported yet') + + async def fetch_long_short_ratio_history(self, symbol: Str = None, timeframe: Str = None, since: Int = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' fetchLongShortRatioHistory() is not supported yet') + + async def fetch_margin_adjustment_history(self, symbol: Str = None, type: Str = None, since: Num = None, limit: Num = None, params={}): + """ + fetches the history of margin added or reduced from contract isolated positions + :param str [symbol]: unified market symbol + :param str [type]: "add" or "reduce" + :param int [since]: timestamp in ms of the earliest change to fetch + :param int [limit]: the maximum amount of changes to fetch + :param dict params: extra parameters specific to the exchange api endpoint + :returns dict[]: a list of `margin structures ` + """ + raise NotSupported(self.id + ' fetchMarginAdjustmentHistory() is not supported yet') + + async def set_margin_mode(self, marginMode: str, symbol: Str = None, params={}): + raise NotSupported(self.id + ' setMarginMode() is not supported yet') + + async def fetch_deposit_addresses_by_network(self, code: str, params={}): + raise NotSupported(self.id + ' fetchDepositAddressesByNetwork() is not supported yet') + + async def fetch_open_interest_history(self, symbol: str, timeframe: str = '1h', since: Int = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' fetchOpenInterestHistory() is not supported yet') + + async def fetch_open_interest(self, symbol: str, params={}): + raise NotSupported(self.id + ' fetchOpenInterest() is not supported yet') + + async def fetch_open_interests(self, symbols: Strings = None, params={}): + raise NotSupported(self.id + ' fetchOpenInterests() is not supported yet') + + async def sign_in(self, params={}): + raise NotSupported(self.id + ' signIn() is not supported yet') + + async def fetch_payment_methods(self, params={}): + raise NotSupported(self.id + ' fetchPaymentMethods() is not supported yet') + + async def fetch_borrow_rate(self, code: str, amount: float, params={}): + raise NotSupported(self.id + ' fetchBorrowRate is deprecated, please use fetchCrossBorrowRate or fetchIsolatedBorrowRate instead') + + async def repay_cross_margin(self, code: str, amount: float, params={}): + raise NotSupported(self.id + ' repayCrossMargin is not support yet') + + async def repay_isolated_margin(self, symbol: str, code: str, amount: float, params={}): + raise NotSupported(self.id + ' repayIsolatedMargin is not support yet') + + async def borrow_cross_margin(self, code: str, amount: float, params={}): + raise NotSupported(self.id + ' borrowCrossMargin is not support yet') + + async def borrow_isolated_margin(self, symbol: str, code: str, amount: float, params={}): + raise NotSupported(self.id + ' borrowIsolatedMargin is not support yet') + + async def borrow_margin(self, code: str, amount: float, symbol: Str = None, params={}): + raise NotSupported(self.id + ' borrowMargin is deprecated, please use borrowCrossMargin or borrowIsolatedMargin instead') + + async def repay_margin(self, code: str, amount: float, symbol: Str = None, params={}): + raise NotSupported(self.id + ' repayMargin is deprecated, please use repayCrossMargin or repayIsolatedMargin instead') + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}): + message = '' + if self.has['fetchTrades']: + message = '. If you want to build OHLCV candles from trade executions data, visit https://github.com/ccxt/ccxt/tree/master/examples/ and see "build-ohlcv-bars" file' + raise NotSupported(self.id + ' fetchOHLCV() is not supported yet' + message) + + async def fetch_ohlcv_ws(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}): + message = '' + if self.has['fetchTradesWs']: + message = '. If you want to build OHLCV candles from trade executions data, visit https://github.com/ccxt/ccxt/tree/master/examples/ and see "build-ohlcv-bars" file' + raise NotSupported(self.id + ' fetchOHLCVWs() is not supported yet. Try using fetchOHLCV instead.' + message) + + async def watch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' watchOHLCV() is not supported yet') + + async def fetch_web_endpoint(self, method, endpointMethod, returnAsJson, startRegex=None, endRegex=None): + errorMessage = '' + options = self.safe_value(self.options, method, {}) + muteOnFailure = self.safe_bool(options, 'webApiMuteFailure', True) + try: + # if it was not explicitly disabled, then don't fetch + if self.safe_bool(options, 'webApiEnable', True) is not True: + return None + maxRetries = self.safe_value(options, 'webApiRetries', 10) + response = None + retry = 0 + shouldBreak = False + while(retry < maxRetries): + try: + response = await getattr(self, endpointMethod)({}) + shouldBreak = True + break + except Exception as e: + retry = retry + 1 + if retry == maxRetries: + raise e + if shouldBreak: + break # self is needed because of GO + content = response + if startRegex is not None: + splitted_by_start = content.split(startRegex) + content = splitted_by_start[1] # we need second part after start + if endRegex is not None: + splitted_by_end = content.split(endRegex) + content = splitted_by_end[0] # we need first part after start + if returnAsJson and (isinstance(content, str)): + jsoned = self.parse_json(content.strip()) # content should be trimmed before json parsing + if jsoned: + return jsoned # if parsing was not successfull, exception should be thrown + else: + raise BadResponse('could not parse the response into json') + else: + return content + except Exception as e: + errorMessage = self.id + ' ' + method + '() failed to fetch correct data from website. Probably webpage markup has been changed, breaking the page custom parser.' + if muteOnFailure: + return None + else: + raise BadResponse(errorMessage) + + async def fetch_l2_order_book(self, symbol: str, limit: Int = None, params={}): + orderbook = await self.fetch_order_book(symbol, limit, params) + return self.extend(orderbook, { + 'asks': self.sort_by(self.aggregate(orderbook['asks']), 0), + 'bids': self.sort_by(self.aggregate(orderbook['bids']), 0, True), + }) + + async def load_trading_limits(self, symbols: Strings = None, reload=False, params={}): + if self.has['fetchTradingLimits']: + if reload or not ('limitsLoaded' in self.options): + response = await self.fetch_trading_limits(symbols) + for i in range(0, len(symbols)): + symbol = symbols[i] + self.markets[symbol] = self.deep_extend(self.markets[symbol], response[symbol]) + self.options['limitsLoaded'] = self.milliseconds() + return self.markets + + async def fetch2(self, path, api: Any = 'public', method='GET', params={}, headers: Any = None, body: Any = None, config={}): + if self.enableRateLimit: + cost = self.calculate_rate_limiter_cost(api, method, path, params, config) + await self.throttle(cost) + retries = None + retries, params = self.handle_option_and_params(params, path, 'maxRetriesOnFailure', 0) + retryDelay = None + retryDelay, params = self.handle_option_and_params(params, path, 'maxRetriesOnFailureDelay', 0) + self.lastRestRequestTimestamp = self.milliseconds() + request = self.sign(path, api, method, params, headers, body) + self.last_request_headers = request['headers'] + self.last_request_body = request['body'] + self.last_request_url = request['url'] + for i in range(0, retries + 1): + try: + return await self.fetch(request['url'], request['method'], request['headers'], request['body']) + except Exception as e: + if isinstance(e, OperationFailed): + if i < retries: + if self.verbose: + self.log('Request failed with the error: ' + str(e) + ', retrying ' + (i + str(1)) + ' of ' + str(retries) + '...') + if (retryDelay is not None) and (retryDelay != 0): + await self.sleep(retryDelay) + else: + raise e + else: + raise e + return None # self line is never reached, but exists for c# value return requirement + + async def request(self, path, api: Any = 'public', method='GET', params={}, headers: Any = None, body: Any = None, config={}): + return await self.fetch2(path, api, method, params, headers, body, config) + + async def load_accounts(self, reload=False, params={}): + if reload: + self.accounts = await self.fetch_accounts(params) + else: + if self.accounts: + return self.accounts + else: + self.accounts = await self.fetch_accounts(params) + self.accountsById = self.index_by(self.accounts, 'id') + return self.accounts + + async def edit_limit_buy_order(self, id: str, symbol: str, amount: float, price: Num = None, params={}): + return await self.edit_limit_order(id, symbol, 'buy', amount, price, params) + + async def edit_limit_sell_order(self, id: str, symbol: str, amount: float, price: Num = None, params={}): + return await self.edit_limit_order(id, symbol, 'sell', amount, price, params) + + async def edit_limit_order(self, id: str, symbol: str, side: OrderSide, amount: float, price: Num = None, params={}): + return await self.edit_order(id, symbol, 'limit', side, amount, price, params) + + async def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + await self.cancel_order(id, symbol) + return await self.create_order(symbol, type, side, amount, price, params) + + async def edit_order_with_client_order_id(self, clientOrderId: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + return await self.edit_order('', symbol, type, side, amount, price, self.extend({'clientOrderId': clientOrderId}, params)) + + async def edit_order_ws(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + await self.cancel_order_ws(id, symbol) + return await self.create_order_ws(symbol, type, side, amount, price, params) + + async def fetch_position(self, symbol: str, params={}): + raise NotSupported(self.id + ' fetchPosition() is not supported yet') + + async def fetch_position_ws(self, symbol: str, params={}): + raise NotSupported(self.id + ' fetchPositionWs() is not supported yet') + + async def watch_position(self, symbol: Str = None, params={}): + raise NotSupported(self.id + ' watchPosition() is not supported yet') + + async def watch_positions(self, symbols: Strings = None, since: Int = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' watchPositions() is not supported yet') + + async def watch_position_for_symbols(self, symbols: Strings = None, since: Int = None, limit: Int = None, params={}): + return await self.watch_positions(symbols, since, limit, params) + + async def fetch_positions_for_symbol(self, symbol: str, params={}): + """ + fetches all open positions for specific symbol, unlike fetchPositions(which is designed to work with multiple symbols) so self method might be preffered for one-market position, because of less rate-limit consumption and speed + :param str symbol: unified market symbol + :param dict params: extra parameters specific to the endpoint + :returns dict[]: a list of `position structure ` with maximum 3 items - possible one position for "one-way" mode, and possible two positions(long & short) for "two-way"(a.k.a. hedge) mode + """ + raise NotSupported(self.id + ' fetchPositionsForSymbol() is not supported yet') + + async def fetch_positions_for_symbol_ws(self, symbol: str, params={}): + """ + fetches all open positions for specific symbol, unlike fetchPositions(which is designed to work with multiple symbols) so self method might be preffered for one-market position, because of less rate-limit consumption and speed + :param str symbol: unified market symbol + :param dict params: extra parameters specific to the endpoint + :returns dict[]: a list of `position structure ` with maximum 3 items - possible one position for "one-way" mode, and possible two positions(long & short) for "two-way"(a.k.a. hedge) mode + """ + raise NotSupported(self.id + ' fetchPositionsForSymbol() is not supported yet') + + async def fetch_positions(self, symbols: Strings = None, params={}): + raise NotSupported(self.id + ' fetchPositions() is not supported yet') + + async def fetch_positions_ws(self, symbols: Strings = None, params={}): + raise NotSupported(self.id + ' fetchPositions() is not supported yet') + + async def fetch_positions_risk(self, symbols: Strings = None, params={}): + raise NotSupported(self.id + ' fetchPositionsRisk() is not supported yet') + + async def fetch_bids_asks(self, symbols: Strings = None, params={}): + raise NotSupported(self.id + ' fetchBidsAsks() is not supported yet') + + async def fetch_borrow_interest(self, code: Str = None, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' fetchBorrowInterest() is not supported yet') + + async def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' fetchLedger() is not supported yet') + + async def fetch_ledger_entry(self, id: str, code: Str = None, params={}): + raise NotSupported(self.id + ' fetchLedgerEntry() is not supported yet') + + async def fetch_balance(self, params={}): + raise NotSupported(self.id + ' fetchBalance() is not supported yet') + + async def fetch_balance_ws(self, params={}): + raise NotSupported(self.id + ' fetchBalanceWs() is not supported yet') + + async def watch_balance(self, params={}): + raise NotSupported(self.id + ' watchBalance() is not supported yet') + + async def fetch_partial_balance(self, part, params={}): + balance = await self.fetch_balance(params) + return balance[part] + + async def fetch_free_balance(self, params={}): + return await self.fetch_partial_balance('free', params) + + async def fetch_used_balance(self, params={}): + return await self.fetch_partial_balance('used', params) + + async def fetch_total_balance(self, params={}): + return await self.fetch_partial_balance('total', params) + + async def fetch_status(self, params={}): + raise NotSupported(self.id + ' fetchStatus() is not supported yet') + + async def fetch_transaction_fee(self, code: str, params={}): + if not self.has['fetchTransactionFees']: + raise NotSupported(self.id + ' fetchTransactionFee() is not supported yet') + return await self.fetch_transaction_fees([code], params) + + async def fetch_transaction_fees(self, codes: Strings = None, params={}): + raise NotSupported(self.id + ' fetchTransactionFees() is not supported yet') + + async def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}): + raise NotSupported(self.id + ' fetchDepositWithdrawFees() is not supported yet') + + async def fetch_deposit_withdraw_fee(self, code: str, params={}): + if not self.has['fetchDepositWithdrawFees']: + raise NotSupported(self.id + ' fetchDepositWithdrawFee() is not supported yet') + fees = await self.fetch_deposit_withdraw_fees([code], params) + return self.safe_value(fees, code) + + async def fetch_cross_borrow_rate(self, code: str, params={}): + await self.load_markets() + if not self.has['fetchBorrowRates']: + raise NotSupported(self.id + ' fetchCrossBorrowRate() is not supported yet') + borrowRates = await self.fetch_cross_borrow_rates(params) + rate = self.safe_value(borrowRates, code) + if rate is None: + raise ExchangeError(self.id + ' fetchCrossBorrowRate() could not find the borrow rate for currency code ' + code) + return rate + + async def fetch_isolated_borrow_rate(self, symbol: str, params={}): + await self.load_markets() + if not self.has['fetchBorrowRates']: + raise NotSupported(self.id + ' fetchIsolatedBorrowRate() is not supported yet') + borrowRates = await self.fetch_isolated_borrow_rates(params) + rate = self.safe_dict(borrowRates, symbol) + if rate is None: + raise ExchangeError(self.id + ' fetchIsolatedBorrowRate() could not find the borrow rate for market symbol ' + symbol) + return rate + + async def fetch_ticker(self, symbol: str, params={}): + if self.has['fetchTickers']: + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + tickers = await self.fetch_tickers([symbol], params) + ticker = self.safe_dict(tickers, symbol) + if ticker is None: + raise NullResponse(self.id + ' fetchTickers() could not find a ticker for ' + symbol) + else: + return ticker + else: + raise NotSupported(self.id + ' fetchTicker() is not supported yet') + + async def fetch_mark_price(self, symbol: str, params={}): + if self.has['fetchMarkPrices']: + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + tickers = await self.fetch_mark_prices([symbol], params) + ticker = self.safe_dict(tickers, symbol) + if ticker is None: + raise NullResponse(self.id + ' fetchMarkPrices() could not find a ticker for ' + symbol) + else: + return ticker + else: + raise NotSupported(self.id + ' fetchMarkPrices() is not supported yet') + + async def fetch_ticker_ws(self, symbol: str, params={}): + if self.has['fetchTickersWs']: + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + tickers = await self.fetch_tickers_ws([symbol], params) + ticker = self.safe_dict(tickers, symbol) + if ticker is None: + raise NullResponse(self.id + ' fetchTickerWs() could not find a ticker for ' + symbol) + else: + return ticker + else: + raise NotSupported(self.id + ' fetchTickerWs() is not supported yet') + + async def watch_ticker(self, symbol: str, params={}): + raise NotSupported(self.id + ' watchTicker() is not supported yet') + + async def fetch_tickers(self, symbols: Strings = None, params={}): + raise NotSupported(self.id + ' fetchTickers() is not supported yet') + + async def fetch_mark_prices(self, symbols: Strings = None, params={}): + raise NotSupported(self.id + ' fetchMarkPrices() is not supported yet') + + async def fetch_tickers_ws(self, symbols: Strings = None, params={}): + raise NotSupported(self.id + ' fetchTickers() is not supported yet') + + async def fetch_order_books(self, symbols: Strings = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' fetchOrderBooks() is not supported yet') + + async def watch_bids_asks(self, symbols: Strings = None, params={}): + raise NotSupported(self.id + ' watchBidsAsks() is not supported yet') + + async def watch_tickers(self, symbols: Strings = None, params={}): + raise NotSupported(self.id + ' watchTickers() is not supported yet') + + async def un_watch_tickers(self, symbols: Strings = None, params={}): + raise NotSupported(self.id + ' unWatchTickers() is not supported yet') + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + raise NotSupported(self.id + ' fetchOrder() is not supported yet') + + async def fetch_order_with_client_order_id(self, clientOrderId: str, symbol: Str = None, params={}): + """ + create a market order by providing the symbol, side and cost + :param str clientOrderId: client order Id + :param str symbol: unified symbol of the market to create an order in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + extendedParams = self.extend(params, {'clientOrderId': clientOrderId}) + return await self.fetch_order('', symbol, extendedParams) + + async def fetch_order_ws(self, id: str, symbol: Str = None, params={}): + raise NotSupported(self.id + ' fetchOrderWs() is not supported yet') + + async def fetch_order_status(self, id: str, symbol: Str = None, params={}): + # TODO: TypeScript: change method signature by replacing + # Promise with Promise. + order = await self.fetch_order(id, symbol, params) + return order['status'] + + async def fetch_unified_order(self, order, params={}): + return await self.fetch_order(self.safe_string(order, 'id'), self.safe_string(order, 'symbol'), params) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + raise NotSupported(self.id + ' createOrder() is not supported yet') + + async def create_convert_trade(self, id: str, fromCode: str, toCode: str, amount: Num = None, params={}): + raise NotSupported(self.id + ' createConvertTrade() is not supported yet') + + async def fetch_convert_trade(self, id: str, code: Str = None, params={}): + raise NotSupported(self.id + ' fetchConvertTrade() is not supported yet') + + async def fetch_convert_trade_history(self, code: Str = None, since: Int = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' fetchConvertTradeHistory() is not supported yet') + + async def fetch_position_mode(self, symbol: Str = None, params={}): + raise NotSupported(self.id + ' fetchPositionMode() is not supported yet') + + async def create_trailing_amount_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, trailingAmount: Num = None, trailingTriggerPrice: Num = None, params={}): + """ + create a trailing order by providing the symbol, type, side, amount, price and trailingAmount + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency, or number of contracts + :param float [price]: the price for the order to be filled at, in units of the quote currency, ignored in market orders + :param float trailingAmount: the quote amount to trail away from the current market price + :param float [trailingTriggerPrice]: the price to activate a trailing order, default uses the price argument + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + if trailingAmount is None: + raise ArgumentsRequired(self.id + ' createTrailingAmountOrder() requires a trailingAmount argument') + params['trailingAmount'] = trailingAmount + if trailingTriggerPrice is not None: + params['trailingTriggerPrice'] = trailingTriggerPrice + if self.has['createTrailingAmountOrder']: + return await self.create_order(symbol, type, side, amount, price, params) + raise NotSupported(self.id + ' createTrailingAmountOrder() is not supported yet') + + async def create_trailing_amount_order_ws(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, trailingAmount: Num = None, trailingTriggerPrice: Num = None, params={}): + """ + create a trailing order by providing the symbol, type, side, amount, price and trailingAmount + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency, or number of contracts + :param float [price]: the price for the order to be filled at, in units of the quote currency, ignored in market orders + :param float trailingAmount: the quote amount to trail away from the current market price + :param float [trailingTriggerPrice]: the price to activate a trailing order, default uses the price argument + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + if trailingAmount is None: + raise ArgumentsRequired(self.id + ' createTrailingAmountOrderWs() requires a trailingAmount argument') + params['trailingAmount'] = trailingAmount + if trailingTriggerPrice is not None: + params['trailingTriggerPrice'] = trailingTriggerPrice + if self.has['createTrailingAmountOrderWs']: + return await self.create_order_ws(symbol, type, side, amount, price, params) + raise NotSupported(self.id + ' createTrailingAmountOrderWs() is not supported yet') + + async def create_trailing_percent_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, trailingPercent: Num = None, trailingTriggerPrice: Num = None, params={}): + """ + create a trailing order by providing the symbol, type, side, amount, price and trailingPercent + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency, or number of contracts + :param float [price]: the price for the order to be filled at, in units of the quote currency, ignored in market orders + :param float trailingPercent: the percent to trail away from the current market price + :param float [trailingTriggerPrice]: the price to activate a trailing order, default uses the price argument + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + if trailingPercent is None: + raise ArgumentsRequired(self.id + ' createTrailingPercentOrder() requires a trailingPercent argument') + params['trailingPercent'] = trailingPercent + if trailingTriggerPrice is not None: + params['trailingTriggerPrice'] = trailingTriggerPrice + if self.has['createTrailingPercentOrder']: + return await self.create_order(symbol, type, side, amount, price, params) + raise NotSupported(self.id + ' createTrailingPercentOrder() is not supported yet') + + async def create_trailing_percent_order_ws(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, trailingPercent: Num = None, trailingTriggerPrice: Num = None, params={}): + """ + create a trailing order by providing the symbol, type, side, amount, price and trailingPercent + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency, or number of contracts + :param float [price]: the price for the order to be filled at, in units of the quote currency, ignored in market orders + :param float trailingPercent: the percent to trail away from the current market price + :param float [trailingTriggerPrice]: the price to activate a trailing order, default uses the price argument + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + if trailingPercent is None: + raise ArgumentsRequired(self.id + ' createTrailingPercentOrderWs() requires a trailingPercent argument') + params['trailingPercent'] = trailingPercent + if trailingTriggerPrice is not None: + params['trailingTriggerPrice'] = trailingTriggerPrice + if self.has['createTrailingPercentOrderWs']: + return await self.create_order_ws(symbol, type, side, amount, price, params) + raise NotSupported(self.id + ' createTrailingPercentOrderWs() is not supported yet') + + async def create_market_order_with_cost(self, symbol: str, side: OrderSide, cost: float, params={}): + """ + create a market order by providing the symbol, side and cost + :param str symbol: unified symbol of the market to create an order in + :param str side: 'buy' or 'sell' + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + if self.has['createMarketOrderWithCost'] or (self.has['createMarketBuyOrderWithCost'] and self.has['createMarketSellOrderWithCost']): + return await self.create_order(symbol, 'market', side, cost, 1, params) + raise NotSupported(self.id + ' createMarketOrderWithCost() is not supported yet') + + async def create_market_buy_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market buy order by providing the symbol and cost + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + if self.options['createMarketBuyOrderRequiresPrice'] or self.has['createMarketBuyOrderWithCost']: + return await self.create_order(symbol, 'market', 'buy', cost, 1, params) + raise NotSupported(self.id + ' createMarketBuyOrderWithCost() is not supported yet') + + async def create_market_sell_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market sell order by providing the symbol and cost + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + if self.options['createMarketSellOrderRequiresPrice'] or self.has['createMarketSellOrderWithCost']: + return await self.create_order(symbol, 'market', 'sell', cost, 1, params) + raise NotSupported(self.id + ' createMarketSellOrderWithCost() is not supported yet') + + async def create_market_order_with_cost_ws(self, symbol: str, side: OrderSide, cost: float, params={}): + """ + create a market order by providing the symbol, side and cost + :param str symbol: unified symbol of the market to create an order in + :param str side: 'buy' or 'sell' + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + if self.has['createMarketOrderWithCostWs'] or (self.has['createMarketBuyOrderWithCostWs'] and self.has['createMarketSellOrderWithCostWs']): + return await self.create_order_ws(symbol, 'market', side, cost, 1, params) + raise NotSupported(self.id + ' createMarketOrderWithCostWs() is not supported yet') + + async def create_trigger_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, triggerPrice: Num = None, params={}): + """ + create a trigger stop order(type 1) + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency or the number of contracts + :param float [price]: the price to fulfill the order, in units of the quote currency, ignored in market orders + :param float triggerPrice: the price to trigger the stop order, in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + if triggerPrice is None: + raise ArgumentsRequired(self.id + ' createTriggerOrder() requires a triggerPrice argument') + params['triggerPrice'] = triggerPrice + if self.has['createTriggerOrder']: + return await self.create_order(symbol, type, side, amount, price, params) + raise NotSupported(self.id + ' createTriggerOrder() is not supported yet') + + async def create_trigger_order_ws(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, triggerPrice: Num = None, params={}): + """ + create a trigger stop order(type 1) + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency or the number of contracts + :param float [price]: the price to fulfill the order, in units of the quote currency, ignored in market orders + :param float triggerPrice: the price to trigger the stop order, in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + if triggerPrice is None: + raise ArgumentsRequired(self.id + ' createTriggerOrderWs() requires a triggerPrice argument') + params['triggerPrice'] = triggerPrice + if self.has['createTriggerOrderWs']: + return await self.create_order_ws(symbol, type, side, amount, price, params) + raise NotSupported(self.id + ' createTriggerOrderWs() is not supported yet') + + async def create_stop_loss_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, stopLossPrice: Num = None, params={}): + """ + create a trigger stop loss order(type 2) + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency or the number of contracts + :param float [price]: the price to fulfill the order, in units of the quote currency, ignored in market orders + :param float stopLossPrice: the price to trigger the stop loss order, in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + if stopLossPrice is None: + raise ArgumentsRequired(self.id + ' createStopLossOrder() requires a stopLossPrice argument') + params['stopLossPrice'] = stopLossPrice + if self.has['createStopLossOrder']: + return await self.create_order(symbol, type, side, amount, price, params) + raise NotSupported(self.id + ' createStopLossOrder() is not supported yet') + + async def create_stop_loss_order_ws(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, stopLossPrice: Num = None, params={}): + """ + create a trigger stop loss order(type 2) + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency or the number of contracts + :param float [price]: the price to fulfill the order, in units of the quote currency, ignored in market orders + :param float stopLossPrice: the price to trigger the stop loss order, in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + if stopLossPrice is None: + raise ArgumentsRequired(self.id + ' createStopLossOrderWs() requires a stopLossPrice argument') + params['stopLossPrice'] = stopLossPrice + if self.has['createStopLossOrderWs']: + return await self.create_order_ws(symbol, type, side, amount, price, params) + raise NotSupported(self.id + ' createStopLossOrderWs() is not supported yet') + + async def create_take_profit_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, takeProfitPrice: Num = None, params={}): + """ + create a trigger take profit order(type 2) + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency or the number of contracts + :param float [price]: the price to fulfill the order, in units of the quote currency, ignored in market orders + :param float takeProfitPrice: the price to trigger the take profit order, in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + if takeProfitPrice is None: + raise ArgumentsRequired(self.id + ' createTakeProfitOrder() requires a takeProfitPrice argument') + params['takeProfitPrice'] = takeProfitPrice + if self.has['createTakeProfitOrder']: + return await self.create_order(symbol, type, side, amount, price, params) + raise NotSupported(self.id + ' createTakeProfitOrder() is not supported yet') + + async def create_take_profit_order_ws(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, takeProfitPrice: Num = None, params={}): + """ + create a trigger take profit order(type 2) + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency or the number of contracts + :param float [price]: the price to fulfill the order, in units of the quote currency, ignored in market orders + :param float takeProfitPrice: the price to trigger the take profit order, in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + if takeProfitPrice is None: + raise ArgumentsRequired(self.id + ' createTakeProfitOrderWs() requires a takeProfitPrice argument') + params['takeProfitPrice'] = takeProfitPrice + if self.has['createTakeProfitOrderWs']: + return await self.create_order_ws(symbol, type, side, amount, price, params) + raise NotSupported(self.id + ' createTakeProfitOrderWs() is not supported yet') + + async def create_order_with_take_profit_and_stop_loss(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, takeProfit: Num = None, stopLoss: Num = None, params={}): + """ + create an order with a stop loss or take profit attached(type 3) + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency or the number of contracts + :param float [price]: the price to fulfill the order, in units of the quote currency, ignored in market orders + :param float [takeProfit]: the take profit price, in units of the quote currency + :param float [stopLoss]: the stop loss price, in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.takeProfitType]: *not available on all exchanges* 'limit' or 'market' + :param str [params.stopLossType]: *not available on all exchanges* 'limit' or 'market' + :param str [params.takeProfitPriceType]: *not available on all exchanges* 'last', 'mark' or 'index' + :param str [params.stopLossPriceType]: *not available on all exchanges* 'last', 'mark' or 'index' + :param float [params.takeProfitLimitPrice]: *not available on all exchanges* limit price for a limit take profit order + :param float [params.stopLossLimitPrice]: *not available on all exchanges* stop loss for a limit stop loss order + :param float [params.takeProfitAmount]: *not available on all exchanges* the amount for a take profit + :param float [params.stopLossAmount]: *not available on all exchanges* the amount for a stop loss + :returns dict: an `order structure ` + """ + params = self.set_take_profit_and_stop_loss_params(symbol, type, side, amount, price, takeProfit, stopLoss, params) + if self.has['createOrderWithTakeProfitAndStopLoss']: + return await self.create_order(symbol, type, side, amount, price, params) + raise NotSupported(self.id + ' createOrderWithTakeProfitAndStopLoss() is not supported yet') + + async def create_order_with_take_profit_and_stop_loss_ws(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, takeProfit: Num = None, stopLoss: Num = None, params={}): + """ + create an order with a stop loss or take profit attached(type 3) + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency or the number of contracts + :param float [price]: the price to fulfill the order, in units of the quote currency, ignored in market orders + :param float [takeProfit]: the take profit price, in units of the quote currency + :param float [stopLoss]: the stop loss price, in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.takeProfitType]: *not available on all exchanges* 'limit' or 'market' + :param str [params.stopLossType]: *not available on all exchanges* 'limit' or 'market' + :param str [params.takeProfitPriceType]: *not available on all exchanges* 'last', 'mark' or 'index' + :param str [params.stopLossPriceType]: *not available on all exchanges* 'last', 'mark' or 'index' + :param float [params.takeProfitLimitPrice]: *not available on all exchanges* limit price for a limit take profit order + :param float [params.stopLossLimitPrice]: *not available on all exchanges* stop loss for a limit stop loss order + :param float [params.takeProfitAmount]: *not available on all exchanges* the amount for a take profit + :param float [params.stopLossAmount]: *not available on all exchanges* the amount for a stop loss + :returns dict: an `order structure ` + """ + params = self.set_take_profit_and_stop_loss_params(symbol, type, side, amount, price, takeProfit, stopLoss, params) + if self.has['createOrderWithTakeProfitAndStopLossWs']: + return await self.create_order_ws(symbol, type, side, amount, price, params) + raise NotSupported(self.id + ' createOrderWithTakeProfitAndStopLossWs() is not supported yet') + + async def create_orders(self, orders: List[OrderRequest], params={}): + raise NotSupported(self.id + ' createOrders() is not supported yet') + + async def edit_orders(self, orders: List[OrderRequest], params={}): + raise NotSupported(self.id + ' editOrders() is not supported yet') + + async def create_order_ws(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + raise NotSupported(self.id + ' createOrderWs() is not supported yet') + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + raise NotSupported(self.id + ' cancelOrder() is not supported yet') + + async def cancel_order_with_client_order_id(self, clientOrderId: str, symbol: Str = None, params={}): + """ + create a market order by providing the symbol, side and cost + :param str clientOrderId: client order Id + :param str symbol: unified symbol of the market to create an order in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + extendedParams = self.extend(params, {'clientOrderId': clientOrderId}) + return await self.cancel_order('', symbol, extendedParams) + + async def cancel_order_ws(self, id: str, symbol: Str = None, params={}): + raise NotSupported(self.id + ' cancelOrderWs() is not supported yet') + + async def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): + raise NotSupported(self.id + ' cancelOrders() is not supported yet') + + async def cancel_orders_with_client_order_ids(self, clientOrderIds: List[str], symbol: Str = None, params={}): + """ + create a market order by providing the symbol, side and cost + :param str[] clientOrderIds: client order Ids + :param str symbol: unified symbol of the market to create an order in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + extendedParams = self.extend(params, {'clientOrderIds': clientOrderIds}) + return await self.cancel_orders([], symbol, extendedParams) + + async def cancel_orders_ws(self, ids: List[str], symbol: Str = None, params={}): + raise NotSupported(self.id + ' cancelOrdersWs() is not supported yet') + + async def cancel_all_orders(self, symbol: Str = None, params={}): + raise NotSupported(self.id + ' cancelAllOrders() is not supported yet') + + async def cancel_all_orders_after(self, timeout: Int, params={}): + raise NotSupported(self.id + ' cancelAllOrdersAfter() is not supported yet') + + async def cancel_orders_for_symbols(self, orders: List[CancellationRequest], params={}): + raise NotSupported(self.id + ' cancelOrdersForSymbols() is not supported yet') + + async def cancel_all_orders_ws(self, symbol: Str = None, params={}): + raise NotSupported(self.id + ' cancelAllOrdersWs() is not supported yet') + + async def cancel_unified_order(self, order: Order, params={}): + return self.cancel_order(self.safe_string(order, 'id'), self.safe_string(order, 'symbol'), params) + + async def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + if self.has['fetchOpenOrders'] and self.has['fetchClosedOrders']: + raise NotSupported(self.id + ' fetchOrders() is not supported yet, consider using fetchOpenOrders() and fetchClosedOrders() instead') + raise NotSupported(self.id + ' fetchOrders() is not supported yet') + + async def fetch_orders_ws(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' fetchOrdersWs() is not supported yet') + + async def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' fetchOrderTrades() is not supported yet') + + async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' watchOrders() is not supported yet') + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + if self.has['fetchOrders']: + orders = await self.fetch_orders(symbol, since, limit, params) + return self.filter_by(orders, 'status', 'open') + raise NotSupported(self.id + ' fetchOpenOrders() is not supported yet') + + async def fetch_open_orders_ws(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + if self.has['fetchOrdersWs']: + orders = await self.fetch_orders_ws(symbol, since, limit, params) + return self.filter_by(orders, 'status', 'open') + raise NotSupported(self.id + ' fetchOpenOrdersWs() is not supported yet') + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + if self.has['fetchOrders']: + orders = await self.fetch_orders(symbol, since, limit, params) + return self.filter_by(orders, 'status', 'closed') + raise NotSupported(self.id + ' fetchClosedOrders() is not supported yet') + + async def fetch_canceled_and_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' fetchCanceledAndClosedOrders() is not supported yet') + + async def fetch_closed_orders_ws(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + if self.has['fetchOrdersWs']: + orders = await self.fetch_orders_ws(symbol, since, limit, params) + return self.filter_by(orders, 'status', 'closed') + raise NotSupported(self.id + ' fetchClosedOrdersWs() is not supported yet') + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' fetchMyTrades() is not supported yet') + + async def fetch_my_liquidations(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' fetchMyLiquidations() is not supported yet') + + async def fetch_liquidations(self, symbol: str, since: Int = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' fetchLiquidations() is not supported yet') + + async def fetch_my_trades_ws(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' fetchMyTradesWs() is not supported yet') + + async def watch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' watchMyTrades() is not supported yet') + + async def fetch_greeks(self, symbol: str, params={}): + raise NotSupported(self.id + ' fetchGreeks() is not supported yet') + + async def fetch_all_greeks(self, symbols: Strings = None, params={}): + raise NotSupported(self.id + ' fetchAllGreeks() is not supported yet') + + async def fetch_option_chain(self, code: str, params={}): + raise NotSupported(self.id + ' fetchOptionChain() is not supported yet') + + async def fetch_option(self, symbol: str, params={}): + raise NotSupported(self.id + ' fetchOption() is not supported yet') + + async def fetch_convert_quote(self, fromCode: str, toCode: str, amount: Num = None, params={}): + raise NotSupported(self.id + ' fetchConvertQuote() is not supported yet') + + async def fetch_deposits_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch history of deposits and withdrawals + :param str [code]: unified currency code for the currency of the deposit/withdrawals, default is None + :param int [since]: timestamp in ms of the earliest deposit/withdrawal, default is None + :param int [limit]: max number of deposit/withdrawals to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `transaction structures ` + """ + raise NotSupported(self.id + ' fetchDepositsWithdrawals() is not supported yet') + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' fetchDeposits() is not supported yet') + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' fetchWithdrawals() is not supported yet') + + async def fetch_deposits_ws(self, code: Str = None, since: Int = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' fetchDepositsWs() is not supported yet') + + async def fetch_withdrawals_ws(self, code: Str = None, since: Int = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' fetchWithdrawalsWs() is not supported yet') + + async def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' fetchFundingRateHistory() is not supported yet') + + async def fetch_funding_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' fetchFundingHistory() is not supported yet') + + async def close_position(self, symbol: str, side: OrderSide = None, params={}): + raise NotSupported(self.id + ' closePosition() is not supported yet') + + async def close_all_positions(self, params={}): + raise NotSupported(self.id + ' closeAllPositions() is not supported yet') + + async def fetch_l3_order_book(self, symbol: str, limit: Int = None, params={}): + raise BadRequest(self.id + ' fetchL3OrderBook() is not supported yet') + + async def fetch_deposit_address(self, code: str, params={}): + if self.has['fetchDepositAddresses']: + depositAddresses = await self.fetch_deposit_addresses([code], params) + depositAddress = self.safe_value(depositAddresses, code) + if depositAddress is None: + raise InvalidAddress(self.id + ' fetchDepositAddress() could not find a deposit address for ' + code + ', make sure you have created a corresponding deposit address in your wallet on the exchange website') + else: + return depositAddress + elif self.has['fetchDepositAddressesByNetwork']: + network = self.safe_string(params, 'network') + params = self.omit(params, 'network') + addressStructures = await self.fetch_deposit_addresses_by_network(code, params) + if network is not None: + return self.safe_dict(addressStructures, network) + else: + keys = list(addressStructures.keys()) + key = self.safe_string(keys, 0) + return self.safe_dict(addressStructures, key) + else: + raise NotSupported(self.id + ' fetchDepositAddress() is not supported yet') + + async def create_limit_order(self, symbol: str, side: OrderSide, amount: float, price: float, params={}): + return await self.create_order(symbol, 'limit', side, amount, price, params) + + async def create_limit_order_ws(self, symbol: str, side: OrderSide, amount: float, price: float, params={}): + return await self.create_order_ws(symbol, 'limit', side, amount, price, params) + + async def create_market_order(self, symbol: str, side: OrderSide, amount: float, price: Num = None, params={}): + return await self.create_order(symbol, 'market', side, amount, price, params) + + async def create_market_order_ws(self, symbol: str, side: OrderSide, amount: float, price: Num = None, params={}): + return await self.create_order_ws(symbol, 'market', side, amount, price, params) + + async def create_limit_buy_order(self, symbol: str, amount: float, price: float, params={}): + return await self.create_order(symbol, 'limit', 'buy', amount, price, params) + + async def create_limit_buy_order_ws(self, symbol: str, amount: float, price: float, params={}): + return await self.create_order_ws(symbol, 'limit', 'buy', amount, price, params) + + async def create_limit_sell_order(self, symbol: str, amount: float, price: float, params={}): + return await self.create_order(symbol, 'limit', 'sell', amount, price, params) + + async def create_limit_sell_order_ws(self, symbol: str, amount: float, price: float, params={}): + return await self.create_order_ws(symbol, 'limit', 'sell', amount, price, params) + + async def create_market_buy_order(self, symbol: str, amount: float, params={}): + return await self.create_order(symbol, 'market', 'buy', amount, None, params) + + async def create_market_buy_order_ws(self, symbol: str, amount: float, params={}): + return await self.create_order_ws(symbol, 'market', 'buy', amount, None, params) + + async def create_market_sell_order(self, symbol: str, amount: float, params={}): + return await self.create_order(symbol, 'market', 'sell', amount, None, params) + + async def create_market_sell_order_ws(self, symbol: str, amount: float, params={}): + return await self.create_order_ws(symbol, 'market', 'sell', amount, None, params) + + async def load_time_difference(self, params={}): + serverTime = await self.fetch_time(params) + after = self.milliseconds() + self.options['timeDifference'] = after - serverTime + return self.options['timeDifference'] + + async def fetch_market_leverage_tiers(self, symbol: str, params={}): + if self.has['fetchLeverageTiers']: + market = self.market(symbol) + if not market['contract']: + raise BadSymbol(self.id + ' fetchMarketLeverageTiers() supports contract markets only') + tiers = await self.fetch_leverage_tiers([symbol]) + return self.safe_value(tiers, symbol) + else: + raise NotSupported(self.id + ' fetchMarketLeverageTiers() is not supported yet') + + async def create_post_only_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + if not self.has['createPostOnlyOrder']: + raise NotSupported(self.id + ' createPostOnlyOrder() is not supported yet') + query = self.extend(params, {'postOnly': True}) + return await self.create_order(symbol, type, side, amount, price, query) + + async def create_post_only_order_ws(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + if not self.has['createPostOnlyOrderWs']: + raise NotSupported(self.id + ' createPostOnlyOrderWs() is not supported yet') + query = self.extend(params, {'postOnly': True}) + return await self.create_order_ws(symbol, type, side, amount, price, query) + + async def create_reduce_only_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + if not self.has['createReduceOnlyOrder']: + raise NotSupported(self.id + ' createReduceOnlyOrder() is not supported yet') + query = self.extend(params, {'reduceOnly': True}) + return await self.create_order(symbol, type, side, amount, price, query) + + async def create_reduce_only_order_ws(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + if not self.has['createReduceOnlyOrderWs']: + raise NotSupported(self.id + ' createReduceOnlyOrderWs() is not supported yet') + query = self.extend(params, {'reduceOnly': True}) + return await self.create_order_ws(symbol, type, side, amount, price, query) + + async def create_stop_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, triggerPrice: Num = None, params={}): + if not self.has['createStopOrder']: + raise NotSupported(self.id + ' createStopOrder() is not supported yet') + if triggerPrice is None: + raise ArgumentsRequired(self.id + ' create_stop_order() requires a stopPrice argument') + query = self.extend(params, {'stopPrice': triggerPrice}) + return await self.create_order(symbol, type, side, amount, price, query) + + async def create_stop_order_ws(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, triggerPrice: Num = None, params={}): + if not self.has['createStopOrderWs']: + raise NotSupported(self.id + ' createStopOrderWs() is not supported yet') + if triggerPrice is None: + raise ArgumentsRequired(self.id + ' createStopOrderWs() requires a stopPrice argument') + query = self.extend(params, {'stopPrice': triggerPrice}) + return await self.create_order_ws(symbol, type, side, amount, price, query) + + async def create_stop_limit_order(self, symbol: str, side: OrderSide, amount: float, price: float, triggerPrice: float, params={}): + if not self.has['createStopLimitOrder']: + raise NotSupported(self.id + ' createStopLimitOrder() is not supported yet') + query = self.extend(params, {'stopPrice': triggerPrice}) + return await self.create_order(symbol, 'limit', side, amount, price, query) + + async def create_stop_limit_order_ws(self, symbol: str, side: OrderSide, amount: float, price: float, triggerPrice: float, params={}): + if not self.has['createStopLimitOrderWs']: + raise NotSupported(self.id + ' createStopLimitOrderWs() is not supported yet') + query = self.extend(params, {'stopPrice': triggerPrice}) + return await self.create_order_ws(symbol, 'limit', side, amount, price, query) + + async def create_stop_market_order(self, symbol: str, side: OrderSide, amount: float, triggerPrice: float, params={}): + if not self.has['createStopMarketOrder']: + raise NotSupported(self.id + ' createStopMarketOrder() is not supported yet') + query = self.extend(params, {'stopPrice': triggerPrice}) + return await self.create_order(symbol, 'market', side, amount, None, query) + + async def create_stop_market_order_ws(self, symbol: str, side: OrderSide, amount: float, triggerPrice: float, params={}): + if not self.has['createStopMarketOrderWs']: + raise NotSupported(self.id + ' createStopMarketOrderWs() is not supported yet') + query = self.extend(params, {'stopPrice': triggerPrice}) + return await self.create_order_ws(symbol, 'market', side, amount, None, query) + + async def fetch_last_prices(self, symbols: Strings = None, params={}): + raise NotSupported(self.id + ' fetchLastPrices() is not supported yet') + + async def fetch_trading_fees(self, params={}): + raise NotSupported(self.id + ' fetchTradingFees() is not supported yet') + + async def fetch_trading_fees_ws(self, params={}): + raise NotSupported(self.id + ' fetchTradingFeesWs() is not supported yet') + + async def fetch_trading_fee(self, symbol: str, params={}): + if not self.has['fetchTradingFees']: + raise NotSupported(self.id + ' fetchTradingFee() is not supported yet') + fees = await self.fetch_trading_fees(params) + return self.safe_dict(fees, symbol) + + async def fetch_convert_currencies(self, params={}): + raise NotSupported(self.id + ' fetchConvertCurrencies() is not supported yet') + + async def fetch_funding_rate(self, symbol: str, params={}): + if self.has['fetchFundingRates']: + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + if not market['contract']: + raise BadSymbol(self.id + ' fetchFundingRate() supports contract markets only') + rates = await self.fetch_funding_rates([symbol], params) + rate = self.safe_value(rates, symbol) + if rate is None: + raise NullResponse(self.id + ' fetchFundingRate() returned no data for ' + symbol) + else: + return rate + else: + raise NotSupported(self.id + ' fetchFundingRate() is not supported yet') + + async def fetch_funding_interval(self, symbol: str, params={}): + if self.has['fetchFundingIntervals']: + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + if not market['contract']: + raise BadSymbol(self.id + ' fetchFundingInterval() supports contract markets only') + rates = await self.fetch_funding_intervals([symbol], params) + rate = self.safe_value(rates, symbol) + if rate is None: + raise NullResponse(self.id + ' fetchFundingInterval() returned no data for ' + symbol) + else: + return rate + else: + raise NotSupported(self.id + ' fetchFundingInterval() is not supported yet') + + async def fetch_mark_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}): + """ + fetches historical mark price candlestick data containing the open, high, low, and close price of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns float[][]: A list of candles ordered, open, high, low, close, None + """ + if self.has['fetchMarkOHLCV']: + request: dict = { + 'price': 'mark', + } + return await self.fetch_ohlcv(symbol, timeframe, since, limit, self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchMarkOHLCV() is not supported yet') + + async def fetch_index_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}): + """ + fetches historical index price candlestick data containing the open, high, low, and close price of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + @returns {} A list of candles ordered, open, high, low, close, None + """ + if self.has['fetchIndexOHLCV']: + request: dict = { + 'price': 'index', + } + return await self.fetch_ohlcv(symbol, timeframe, since, limit, self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchIndexOHLCV() is not supported yet') + + async def fetch_premium_index_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}): + """ + fetches historical premium index price candlestick data containing the open, high, low, and close price of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns float[][]: A list of candles ordered, open, high, low, close, None + """ + if self.has['fetchPremiumIndexOHLCV']: + request: dict = { + 'price': 'premiumIndex', + } + return await self.fetch_ohlcv(symbol, timeframe, since, limit, self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchPremiumIndexOHLCV() is not supported yet') + + async def fetch_transactions(self, code: Str = None, since: Int = None, limit: Int = None, params={}): + """ + @deprecated + *DEPRECATED* use fetchDepositsWithdrawals instead + :param str code: unified currency code for the currency of the deposit/withdrawals, default is None + :param int [since]: timestamp in ms of the earliest deposit/withdrawal, default is None + :param int [limit]: max number of deposit/withdrawals to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `transaction structures ` + """ + if self.has['fetchDepositsWithdrawals']: + return await self.fetch_deposits_withdrawals(code, since, limit, params) + else: + raise NotSupported(self.id + ' fetchTransactions() is not supported yet') + + async def fetch_paginated_call_dynamic(self, method: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}, maxEntriesPerRequest: Int = None, removeRepeated=True): + maxCalls = None + maxCalls, params = self.handle_option_and_params(params, method, 'paginationCalls', 10) + maxRetries = None + maxRetries, params = self.handle_option_and_params(params, method, 'maxRetries', 3) + paginationDirection = None + paginationDirection, params = self.handle_option_and_params(params, method, 'paginationDirection', 'backward') + paginationTimestamp = None + removeRepeatedOption = removeRepeated + removeRepeatedOption, params = self.handle_option_and_params(params, method, 'removeRepeated', removeRepeated) + calls = 0 + result = [] + errors = 0 + until = self.safe_integer_n(params, ['until', 'untill', 'till']) # do not omit it from params here + maxEntriesPerRequest, params = self.handle_max_entries_per_request_and_params(method, maxEntriesPerRequest, params) + if (paginationDirection == 'forward'): + if since is None: + raise ArgumentsRequired(self.id + ' pagination requires a since argument when paginationDirection set to forward') + paginationTimestamp = since + while((calls < maxCalls)): + calls += 1 + try: + if paginationDirection == 'backward': + # do it backwards, starting from the last + # UNTIL filtering is required in order to work + if paginationTimestamp is not None: + params['until'] = paginationTimestamp - 1 + response = await getattr(self, method)(symbol, None, maxEntriesPerRequest, params) + responseLength = len(response) + if self.verbose: + backwardMessage = 'Dynamic pagination call ' + self.number_to_string(calls) + ' method ' + method + ' response length ' + self.number_to_string(responseLength) + if paginationTimestamp is not None: + backwardMessage += ' timestamp ' + self.number_to_string(paginationTimestamp) + self.log(backwardMessage) + if responseLength == 0: + break + errors = 0 + result = self.array_concat(result, response) + firstElement = self.safe_value(response, 0) + paginationTimestamp = self.safe_integer_2(firstElement, 'timestamp', 0) + if (since is not None) and (paginationTimestamp <= since): + break + else: + # do it forwards, starting from the since + response = await getattr(self, method)(symbol, paginationTimestamp, maxEntriesPerRequest, params) + responseLength = len(response) + if self.verbose: + forwardMessage = 'Dynamic pagination call ' + self.number_to_string(calls) + ' method ' + method + ' response length ' + self.number_to_string(responseLength) + if paginationTimestamp is not None: + forwardMessage += ' timestamp ' + self.number_to_string(paginationTimestamp) + self.log(forwardMessage) + if responseLength == 0: + break + errors = 0 + result = self.array_concat(result, response) + last = self.safe_value(response, responseLength - 1) + paginationTimestamp = self.safe_integer(last, 'timestamp') + 1 + if (until is not None) and (paginationTimestamp >= until): + break + except Exception as e: + errors += 1 + if errors > maxRetries: + raise e + uniqueResults = result + if removeRepeatedOption: + uniqueResults = self.remove_repeated_elements_from_array(result) + key = 0 if (method == 'fetchOHLCV') else 'timestamp' + return self.filter_by_since_limit(uniqueResults, since, limit, key) + + async def safe_deterministic_call(self, method: str, symbol: Str = None, since: Int = None, limit: Int = None, timeframe: Str = None, params={}): + maxRetries = None + maxRetries, params = self.handle_option_and_params(params, method, 'maxRetries', 3) + errors = 0 + while(errors <= maxRetries): + try: + if timeframe and method != 'fetchFundingRateHistory': + return await getattr(self, method)(symbol, timeframe, since, limit, params) + else: + return await getattr(self, method)(symbol, since, limit, params) + except Exception as e: + if isinstance(e, RateLimitExceeded): + raise e # if we are rate limited, we should not retry and fail fast + errors += 1 + if errors > maxRetries: + raise e + return [] + + async def fetch_paginated_call_deterministic(self, method: str, symbol: Str = None, since: Int = None, limit: Int = None, timeframe: Str = None, params={}, maxEntriesPerRequest=None): + maxCalls = None + maxCalls, params = self.handle_option_and_params(params, method, 'paginationCalls', 10) + maxEntriesPerRequest, params = self.handle_max_entries_per_request_and_params(method, maxEntriesPerRequest, params) + current = self.milliseconds() + tasks = [] + time = self.parse_timeframe(timeframe) * 1000 + step = time * maxEntriesPerRequest + currentSince = current - (maxCalls * step) - 1 + if since is not None: + currentSince = max(currentSince, since) + else: + currentSince = max(currentSince, 1241440531000) # avoid timestamps older than 2009 + until = self.safe_integer_2(params, 'until', 'till') # do not omit it here + if until is not None: + requiredCalls = int(math.ceil((until - since)) / step) + if requiredCalls > maxCalls: + raise BadRequest(self.id + ' the number of required calls is greater than the max number of calls allowed, either increase the paginationCalls or decrease the since-until gap. Current paginationCalls limit is ' + str(maxCalls) + ' required calls is ' + str(requiredCalls)) + for i in range(0, maxCalls): + if (until is not None) and (currentSince >= until): + break + if currentSince >= current: + break + tasks.append(self.safe_deterministic_call(method, symbol, currentSince, maxEntriesPerRequest, timeframe, params)) + currentSince = self.sum(currentSince, step) - 1 + results = await asyncio.gather(*tasks) + result = [] + for i in range(0, len(results)): + result = self.array_concat(result, results[i]) + uniqueResults = self.remove_repeated_elements_from_array(result) + key = 0 if (method == 'fetchOHLCV') else 'timestamp' + return self.filter_by_since_limit(uniqueResults, since, limit, key) + + async def fetch_paginated_call_cursor(self, method: str, symbol: Str = None, since=None, limit=None, params={}, cursorReceived=None, cursorSent=None, cursorIncrement=None, maxEntriesPerRequest=None): + maxCalls = None + maxCalls, params = self.handle_option_and_params(params, method, 'paginationCalls', 10) + maxRetries = None + maxRetries, params = self.handle_option_and_params(params, method, 'maxRetries', 3) + maxEntriesPerRequest, params = self.handle_max_entries_per_request_and_params(method, maxEntriesPerRequest, params) + cursorValue = None + i = 0 + errors = 0 + result = [] + timeframe = self.safe_string(params, 'timeframe') + params = self.omit(params, 'timeframe') # reading the timeframe from the method arguments to avoid changing the signature + while(i < maxCalls): + try: + if cursorValue is not None: + if cursorIncrement is not None: + cursorValue = self.parse_to_int(cursorValue) + cursorIncrement + params[cursorSent] = cursorValue + response = None + if method == 'fetchAccounts': + response = await getattr(self, method)(params) + elif method == 'getLeverageTiersPaginated' or method == 'fetchPositions': + response = await getattr(self, method)(symbol, params) + elif method == 'fetchOpenInterestHistory': + response = await getattr(self, method)(symbol, timeframe, since, maxEntriesPerRequest, params) + else: + response = await getattr(self, method)(symbol, since, maxEntriesPerRequest, params) + errors = 0 + responseLength = len(response) + if self.verbose: + cursorString = '' if (cursorValue is None) else cursorValue + iteration = (i + 1) + cursorMessage = 'Cursor pagination call ' + str(iteration) + ' method ' + method + ' response length ' + str(responseLength) + ' cursor ' + cursorString + self.log(cursorMessage) + if responseLength == 0: + break + result = self.array_concat(result, response) + last = self.safe_dict(response, responseLength - 1) + # cursorValue = self.safe_value(last['info'], cursorReceived) + cursorValue = None # search for the cursor + for j in range(0, responseLength): + index = responseLength - j - 1 + entry = self.safe_dict(response, index) + info = self.safe_dict(entry, 'info') + cursor = self.safe_value(info, cursorReceived) + if cursor is not None: + cursorValue = cursor + break + if cursorValue is None: + break + lastTimestamp = self.safe_integer(last, 'timestamp') + if lastTimestamp is not None and lastTimestamp < since: + break + except Exception as e: + errors += 1 + if errors > maxRetries: + raise e + i += 1 + sorted = self.sort_cursor_paginated_result(result) + key = 0 if (method == 'fetchOHLCV') else 'timestamp' + return self.filter_by_since_limit(sorted, since, limit, key) + + async def fetch_paginated_call_incremental(self, method: str, symbol: Str = None, since=None, limit=None, params={}, pageKey=None, maxEntriesPerRequest=None): + maxCalls = None + maxCalls, params = self.handle_option_and_params(params, method, 'paginationCalls', 10) + maxRetries = None + maxRetries, params = self.handle_option_and_params(params, method, 'maxRetries', 3) + maxEntriesPerRequest, params = self.handle_max_entries_per_request_and_params(method, maxEntriesPerRequest, params) + i = 0 + errors = 0 + result = [] + while(i < maxCalls): + try: + params[pageKey] = i + 1 + response = await getattr(self, method)(symbol, since, maxEntriesPerRequest, params) + errors = 0 + responseLength = len(response) + if self.verbose: + iteration = (i + str(1)) + incrementalMessage = 'Incremental pagination call ' + iteration + ' method ' + method + ' response length ' + str(responseLength) + self.log(incrementalMessage) + if responseLength == 0: + break + result = self.array_concat(result, response) + except Exception as e: + errors += 1 + if errors > maxRetries: + raise e + i += 1 + sorted = self.sort_cursor_paginated_result(result) + key = 0 if (method == 'fetchOHLCV') else 'timestamp' + return self.filter_by_since_limit(sorted, since, limit, key) + + async def fetch_position_history(self, symbol: str, since: Int = None, limit: Int = None, params={}): + """ + fetches the history of margin added or reduced from contract isolated positions + :param str [symbol]: unified market symbol + :param int [since]: timestamp in ms of the position + :param int [limit]: the maximum amount of candles to fetch, default=1000 + :param dict params: extra parameters specific to the exchange api endpoint + :returns dict[]: a list of `position structures ` + """ + if self.has['fetchPositionsHistory']: + positions = await self.fetch_positions_history([symbol], since, limit, params) + return positions + else: + raise NotSupported(self.id + ' fetchPositionHistory() is not supported yet') + + async def fetch_positions_history(self, symbols: Strings = None, since: Int = None, limit: Int = None, params={}): + """ + fetches the history of margin added or reduced from contract isolated positions + :param str [symbol]: unified market symbol + :param int [since]: timestamp in ms of the position + :param int [limit]: the maximum amount of candles to fetch, default=1000 + :param dict params: extra parameters specific to the exchange api endpoint + :returns dict[]: a list of `position structures ` + """ + raise NotSupported(self.id + ' fetchPositionsHistory() is not supported yet') + + async def fetch_transfer(self, id: str, code: Str = None, params={}): + """ + fetches a transfer + :param str id: transfer id + :param [str] code: unified currency code + :param dict params: extra parameters specific to the exchange api endpoint + :returns dict: a `transfer structure ` + """ + raise NotSupported(self.id + ' fetchTransfer() is not supported yet') + + async def fetch_transfers(self, code: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches a transfer + :param str id: transfer id + :param int [since]: timestamp in ms of the earliest transfer to fetch + :param int [limit]: the maximum amount of transfers to fetch + :param dict params: extra parameters specific to the exchange api endpoint + :returns dict: a `transfer structure ` + """ + raise NotSupported(self.id + ' fetchTransfers() is not supported yet') + + async def un_watch_ohlcv(self, symbol: str, timeframe: str = '1m', params={}): + """ + watches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + raise NotSupported(self.id + ' unWatchOHLCV() is not supported yet') + + async def watch_mark_price(self, symbol: str, params={}): + """ + watches a mark price for a specific market + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + raise NotSupported(self.id + ' watchMarkPrice() is not supported yet') + + async def watch_mark_prices(self, symbols: Strings = None, params={}): + """ + watches the mark price for all markets + :param str[] symbols: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + raise NotSupported(self.id + ' watchMarkPrices() is not supported yet') + + async def withdraw_ws(self, code: str, amount: float, address: str, tag: Str = None, params={}): + """ + make a withdrawal + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the bitvavo api endpoint + :returns dict: a `transaction structure ` + """ + raise NotSupported(self.id + ' withdrawWs() is not supported yet') + + async def un_watch_my_trades(self, symbol: Str = None, params={}): + """ + unWatches information on multiple trades made by the user + :param str symbol: unified market symbol of the market orders were made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + raise NotSupported(self.id + ' unWatchMyTrades() is not supported yet') + + async def create_orders_ws(self, orders: List[OrderRequest], params={}): + """ + create a list of trade orders + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + raise NotSupported(self.id + ' createOrdersWs() is not supported yet') + + async def fetch_orders_by_status_ws(self, status: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + raise NotSupported(self.id + ' fetchOrdersByStatusWs() is not supported yet') + + async def un_watch_bids_asks(self, symbols: Strings = None, params={}): + """ + unWatches best bid & ask for symbols + :param str[] symbols: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + raise NotSupported(self.id + ' unWatchBidsAsks() is not supported yet') diff --git a/ccxt/async_support/base/throttler.py b/ccxt/async_support/base/throttler.py new file mode 100644 index 0000000..4d15f71 --- /dev/null +++ b/ccxt/async_support/base/throttler.py @@ -0,0 +1,50 @@ +import asyncio +import collections +from time import time + + +class Throttler: + def __init__(self, config, loop=None): + self.loop = loop + self.config = { + 'refillRate': 1.0, + 'delay': 0.001, + 'cost': 1.0, + 'tokens': 0, + 'maxCapacity': 2000, + 'capacity': 1.0, + } + self.config.update(config) + self.queue = collections.deque() + self.running = False + + async def looper(self): + last_timestamp = time() * 1000 + while self.running: + future, cost = self.queue[0] + cost = self.config['cost'] if cost is None else cost + if self.config['tokens'] >= 0: + self.config['tokens'] -= cost + if not future.done(): + future.set_result(None) + self.queue.popleft() + # context switch + await asyncio.sleep(0) + if len(self.queue) == 0: + self.running = False + else: + await asyncio.sleep(self.config['delay']) + now = time() * 1000 + elapsed = now - last_timestamp + last_timestamp = now + self.config['tokens'] = min(self.config['tokens'] + elapsed * self.config['refillRate'], self.config['capacity']) + + def __call__(self, cost=None): + future = asyncio.Future() + if len(self.queue) > self.config['maxCapacity']: + raise RuntimeError('throttle queue is over maxCapacity (' + str(int(self.config['maxCapacity'])) + '), see https://docs.ccxt.com/#/README?id=maximum-requests-capacity') + self.queue.append((future, cost)) + if not self.running: + self.running = True + asyncio.ensure_future(self.looper(), loop=self.loop) + return future diff --git a/ccxt/async_support/base/ws/__init__.py b/ccxt/async_support/base/ws/__init__.py new file mode 100644 index 0000000..a034e77 --- /dev/null +++ b/ccxt/async_support/base/ws/__init__.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- + +from ccxt.base import errors + +# ----------------------------------------------------------------------------- + +from ccxt.base import decimal_to_precision + +from ccxt import BaseError # noqa: F401 +from ccxt import ExchangeError # noqa: F401 +from ccxt import NotSupported # noqa: F401 +from ccxt import AuthenticationError # noqa: F401 +from ccxt import PermissionDenied # noqa: F401 +from ccxt import AccountSuspended # noqa: F401 +from ccxt import InvalidNonce # noqa: F401 +from ccxt import InsufficientFunds # noqa: F401 +from ccxt import InvalidOrder # noqa: F401 +from ccxt import OrderNotFound # noqa: F401 +from ccxt import OrderNotCached # noqa: F401 +from ccxt import DuplicateOrderId # noqa: F401 +from ccxt import CancelPending # noqa: F401 +from ccxt import NetworkError # noqa: F401 +from ccxt import DDoSProtection # noqa: F401 +from ccxt import RateLimitExceeded # noqa: F401 +from ccxt import RequestTimeout # noqa: F401 +from ccxt import ExchangeNotAvailable # noqa: F401 +from ccxt import OnMaintenance # noqa: F401 +from ccxt import InvalidAddress # noqa: F401 +from ccxt import AddressPending # noqa: F401 +from ccxt import ArgumentsRequired # noqa: F401 +from ccxt import BadRequest # noqa: F401 +from ccxt import BadResponse # noqa: F401 +from ccxt import NullResponse # noqa: F401 +from ccxt import OrderImmediatelyFillable # noqa: F401 +from ccxt import OrderNotFillable # noqa: F401 + + +__all__ = decimal_to_precision.__all__ + errors.__all__ # noqa: F405 diff --git a/ccxt/async_support/base/ws/__pycache__/__init__.cpython-311.pyc b/ccxt/async_support/base/ws/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..fa0020d Binary files /dev/null and b/ccxt/async_support/base/ws/__pycache__/__init__.cpython-311.pyc differ diff --git a/ccxt/async_support/base/ws/__pycache__/cache.cpython-311.pyc b/ccxt/async_support/base/ws/__pycache__/cache.cpython-311.pyc new file mode 100644 index 0000000..2725981 Binary files /dev/null and b/ccxt/async_support/base/ws/__pycache__/cache.cpython-311.pyc differ diff --git a/ccxt/async_support/base/ws/__pycache__/client.cpython-311.pyc b/ccxt/async_support/base/ws/__pycache__/client.cpython-311.pyc new file mode 100644 index 0000000..6d0bbab Binary files /dev/null and b/ccxt/async_support/base/ws/__pycache__/client.cpython-311.pyc differ diff --git a/ccxt/async_support/base/ws/__pycache__/functions.cpython-311.pyc b/ccxt/async_support/base/ws/__pycache__/functions.cpython-311.pyc new file mode 100644 index 0000000..171ed2d Binary files /dev/null and b/ccxt/async_support/base/ws/__pycache__/functions.cpython-311.pyc differ diff --git a/ccxt/async_support/base/ws/__pycache__/future.cpython-311.pyc b/ccxt/async_support/base/ws/__pycache__/future.cpython-311.pyc new file mode 100644 index 0000000..09d8508 Binary files /dev/null and b/ccxt/async_support/base/ws/__pycache__/future.cpython-311.pyc differ diff --git a/ccxt/async_support/base/ws/__pycache__/order_book.cpython-311.pyc b/ccxt/async_support/base/ws/__pycache__/order_book.cpython-311.pyc new file mode 100644 index 0000000..bc53188 Binary files /dev/null and b/ccxt/async_support/base/ws/__pycache__/order_book.cpython-311.pyc differ diff --git a/ccxt/async_support/base/ws/__pycache__/order_book_side.cpython-311.pyc b/ccxt/async_support/base/ws/__pycache__/order_book_side.cpython-311.pyc new file mode 100644 index 0000000..428c7ca Binary files /dev/null and b/ccxt/async_support/base/ws/__pycache__/order_book_side.cpython-311.pyc differ diff --git a/ccxt/async_support/base/ws/cache.py b/ccxt/async_support/base/ws/cache.py new file mode 100644 index 0000000..4c1ce4a --- /dev/null +++ b/ccxt/async_support/base/ws/cache.py @@ -0,0 +1,218 @@ +import collections +import logging + +logger = logging.getLogger(__name__) + +class Delegate: + def __init__(self, name, delegated): + self.name = name + self.delegated = delegated + + def __get__(self, instance, owner): + deque = getattr(instance, self.delegated) + return getattr(deque, self.name) + + +class BaseCache(list): + # implicitly called magic methods don't invoke __getattribute__ + # https://docs.python.org/3/reference/datamodel.html#special-method-lookup + # all method lookups obey the descriptor protocol + # this is how the implicit api is defined in ccxt + __iter__ = Delegate('__iter__', '_deque') + __setitem__ = Delegate('__setitem__', '_deque') + __delitem__ = Delegate('__delitem__', '_deque') + __len__ = Delegate('__len__', '_deque') + __contains__ = Delegate('__contains__', '_deque') + __reversed__ = Delegate('__reversed__', '_deque') + clear = Delegate('clear', '_deque') + pop = Delegate('pop', '_deque') + + def __init__(self, max_size=None): + super(BaseCache, self).__init__() + self.max_size = max_size + self._deque = collections.deque([], max_size) + + def __eq__(self, other): + return list(self) == other + + def __repr__(self): + return str(list(self)) + + def __add__(self, other): + return list(self) + other + + def __getitem__(self, item): + # deque doesn't support slicing + deque = super(list, self).__getattribute__('_deque') + if isinstance(item, slice): + start, stop, step = item.indices(len(deque)) + return [deque[i] for i in range(start, stop, step)] + else: + return deque[item] + + # to be overriden + def getLimit(self, symbol, limit): + pass + + # support transpiled snake_case calls + def get_limit(self, symbol, limit): + return self.getLimit(symbol, limit) + + +class ArrayCache(BaseCache): + def __init__(self, max_size=None): + super(ArrayCache, self).__init__(max_size) + self._nested_new_updates_by_symbol = False + self._new_updates_by_symbol = {} + self._clear_updates_by_symbol = {} + self._all_new_updates = 0 + self._clear_all_updates = False + + def getLimit(self, symbol, limit): + if symbol is None: + new_updates_value = self._all_new_updates + self._clear_all_updates = True + else: + new_updates_value = self._new_updates_by_symbol.get(symbol) + if new_updates_value is not None and self._nested_new_updates_by_symbol: + new_updates_value = len(new_updates_value) + self._clear_updates_by_symbol[symbol] = True + + if new_updates_value is None: + return limit + elif limit is not None: + return min(new_updates_value, limit) + else: + return new_updates_value + + def append(self, item): + self._deque.append(item) + if self._clear_all_updates: + self._clear_all_updates = False + self._clear_updates_by_symbol.clear() + self._all_new_updates = 0 + self._new_updates_by_symbol.clear() + if self._clear_updates_by_symbol.get(item['symbol']): + self._clear_updates_by_symbol[item['symbol']] = False + self._new_updates_by_symbol[item['symbol']] = 0 + self._new_updates_by_symbol[item['symbol']] = self._new_updates_by_symbol.get(item['symbol'], 0) + 1 + self._all_new_updates = (self._all_new_updates or 0) + 1 + + +class ArrayCacheByTimestamp(BaseCache): + def __init__(self, max_size=None): + super(ArrayCacheByTimestamp, self).__init__(max_size) + self.hashmap = {} + self._size_tracker = set() + self._new_updates = 0 + self._clear_updates = False + + def getLimit(self, symbol, limit): + self._clear_updates = True + if limit is None: + return self._new_updates + return min(self._new_updates, limit) + + def append(self, item): + if item[0] in self.hashmap: + reference = self.hashmap[item[0]] + if reference != item: + reference[0:len(item)] = item + else: + self.hashmap[item[0]] = item + if len(self._deque) == self._deque.maxlen: + delete_reference = self._deque.popleft() + del self.hashmap[delete_reference[0]] + self._deque.append(item) + if self._clear_updates: + self._clear_updates = False + self._size_tracker.clear() + self._size_tracker.add(item[0]) + self._new_updates = len(self._size_tracker) + + +class ArrayCacheBySymbolById(ArrayCache): + def __init__(self, max_size=None): + super(ArrayCacheBySymbolById, self).__init__(max_size) + self._nested_new_updates_by_symbol = True + self.hashmap = {} + self._index = collections.deque([], max_size) + + def append(self, item): + by_id = self.hashmap.setdefault(item['symbol'], {}) + if item['id'] in by_id: + reference = by_id[item['id']] + if reference != item: + reference.update(item) + item = reference + index = self._index.index(item['id']) + del self._deque[index] + del self._index[index] + else: + by_id[item['id']] = item + if len(self._deque) == self._deque.maxlen: + delete_item = self._deque.popleft() + self._index.popleft() + try: + del self.hashmap[delete_item['symbol']][delete_item['id']] + except Exception as e: + logger.error(f"Error deleting item from hashmap: {delete_item}. Error:{e}") + self._deque.append(item) + self._index.append(item['id']) + if self._clear_all_updates: + self._clear_all_updates = False + self._clear_updates_by_symbol.clear() + self._all_new_updates = 0 + self._new_updates_by_symbol.clear() + if item['symbol'] not in self._new_updates_by_symbol: + self._new_updates_by_symbol[item['symbol']] = set() + if self._clear_updates_by_symbol.get(item['symbol']): + self._clear_updates_by_symbol[item['symbol']] = False + self._new_updates_by_symbol[item['symbol']].clear() + id_set = self._new_updates_by_symbol[item['symbol']] + before_length = len(id_set) + id_set.add(item['id']) + after_length = len(id_set) + self._all_new_updates = (self._all_new_updates or 0) + (after_length - before_length) + + +class ArrayCacheBySymbolBySide(ArrayCache): + def __init__(self, max_size=None): + super(ArrayCacheBySymbolBySide, self).__init__(max_size) + self._nested_new_updates_by_symbol = True + self.hashmap = {} + self._index = collections.deque([], max_size) + + def append(self, item): + by_side = self.hashmap.setdefault(item['symbol'], {}) + if item['side'] in by_side: + reference = by_side[item['side']] + if reference != item: + reference.update(item) + item = reference + index = self._index.index(item['symbol'] + item['side']) + del self._deque[index] + del self._index[index] + else: + by_side[item['side']] = item + if len(self._deque) == self._deque.maxlen: + delete_item = self._deque.popleft() + self._index.popleft() + del self.hashmap[delete_item['symbol']][delete_item['side']] + self._deque.append(item) + self._index.append(item['symbol'] + item['side']) + if self._clear_all_updates: + self._clear_all_updates = False + self._clear_updates_by_symbol.clear() + self._all_new_updates = 0 + self._new_updates_by_symbol.clear() + if item['symbol'] not in self._new_updates_by_symbol: + self._new_updates_by_symbol[item['symbol']] = set() + if self._clear_updates_by_symbol.get(item['symbol']): + self._clear_updates_by_symbol[item['symbol']] = False + self._new_updates_by_symbol[item['symbol']].clear() + side_set = self._new_updates_by_symbol[item['symbol']] + before_length = len(side_set) + side_set.add(item['side']) + after_length = len(side_set) + self._all_new_updates = (self._all_new_updates or 0) + (after_length - before_length) diff --git a/ccxt/async_support/base/ws/client.py b/ccxt/async_support/base/ws/client.py new file mode 100644 index 0000000..b576de4 --- /dev/null +++ b/ccxt/async_support/base/ws/client.py @@ -0,0 +1,346 @@ +# -*- coding: utf-8 -*- + +orjson = None +try: + import orjson as orjson +except ImportError: + pass + +import json + +from asyncio import sleep, ensure_future, wait_for, TimeoutError, BaseEventLoop, Future as asyncioFuture +from .functions import milliseconds, iso8601, deep_extend, is_json_encoded_object +from ccxt import NetworkError, RequestTimeout +from ccxt.async_support.base.ws.future import Future +from ccxt.async_support.base.ws.functions import gunzip, inflate +from typing import Dict + +from aiohttp import WSMsgType + + +class Client(object): + + url = None + ws = None + futures: Dict[str, Future] = {} + options = {} # ws-specific options + subscriptions = {} + rejections = {} + on_message_callback = None + on_error_callback = None + on_close_callback = None + on_connected_callback = None + connectionStarted = None + connectionEstablished = None + isConnected = False + connectionTimeout = 10000 # ms, false to disable + connection = None + error = None # low-level networking exception, if any + connected = None # connection-related Future + keepAlive = 5000 + heartbeat = True + maxPingPongMisses = 2.0 # how many missed pongs to raise a timeout + lastPong = None + ping = None # ping-function if defined + proxy = None + verbose = False # verbose output + gunzip = False + inflate = False + throttle = None + connecting = False + asyncio_loop: BaseEventLoop = None + ping_looper = None + decompressBinary = True # decompress binary messages by default + + def __init__(self, url, on_message_callback, on_error_callback, on_close_callback, on_connected_callback, config={}): + defaults = { + 'url': url, + 'futures': {}, + 'subscriptions': {}, + 'rejections': {}, + 'on_message_callback': on_message_callback, + 'on_error_callback': on_error_callback, + 'on_close_callback': on_close_callback, + 'on_connected_callback': on_connected_callback, + } + settings = {} + settings.update(defaults) + settings.update(config) + for key in settings: + if hasattr(self, key) and isinstance(getattr(self, key), dict): + setattr(self, key, deep_extend(getattr(self, key), settings[key])) + else: + setattr(self, key, settings[key]) + # connection-related Future + self.connected = Future() + + def future(self, message_hash): + if message_hash not in self.futures or self.futures[message_hash].cancelled(): + self.futures[message_hash] = Future() + future = self.futures[message_hash] + if message_hash in self.rejections: + future.reject(self.rejections[message_hash]) + del self.rejections[message_hash] + return future + + def reusable_future(self, message_hash): + return self.future(message_hash) # only used in go + + def reusableFuture(self, message_hash): + return self.future(message_hash) # only used in go + + def resolve(self, result, message_hash): + if self.verbose and message_hash is None: + self.log(iso8601(milliseconds()), 'resolve received None messageHash') + if message_hash in self.futures: + future = self.futures[message_hash] + future.resolve(result) + del self.futures[message_hash] + return result + + def reject(self, result, message_hash=None): + if message_hash is not None: + if message_hash in self.futures: + future = self.futures[message_hash] + future.reject(result) + del self.futures[message_hash] + else: + self.rejections[message_hash] = result + else: + message_hashes = list(self.futures.keys()) + for message_hash in message_hashes: + self.reject(result, message_hash) + return result + + def receive_loop(self): + if self.verbose: + self.log(iso8601(milliseconds()), 'receive loop') + if not self.closed(): + # let's drain the aiohttp buffer to avoid latency + if self.buffer and len(self.buffer) > 1: + size_delta = 0 + while len(self.buffer) > 1: + message, size = self.buffer.popleft() + size_delta += size + self.handle_message(message) + # we must update the size of the last message inside WebSocketDataQueue + # self.receive() calls WebSocketDataQueue.read() that calls WebSocketDataQueue._read_from_buffer() + # which updates the size of the buffer, the _size will overflow and pause the transport + # make sure to set the enviroment variable AIOHTTP_NO_EXTENSIONS=Y to check + # print(self.connection._conn.protocol._payload._size) + self.buffer[0] = (self.buffer[0][0], self.buffer[0][1] + size_delta) + + task = self.asyncio_loop.create_task(self.receive()) + + def after_interrupt(resolved: asyncioFuture): + exception = resolved.exception() + if exception is None: + self.handle_message(resolved.result()) + self.asyncio_loop.call_soon(self.receive_loop) + else: + error = NetworkError(str(exception)) + if self.verbose: + self.log(iso8601(milliseconds()), 'receive_loop', 'Exception', error) + self.reject(error) + + task.add_done_callback(after_interrupt) + else: + # connection got terminated after the connection was made and before the receive loop ran + self.on_close(1006) + + async def open(self, session, backoff_delay=0): + # exponential backoff for consequent connections if necessary + if backoff_delay: + await sleep(backoff_delay) + if self.verbose: + self.log(iso8601(milliseconds()), 'connecting to', self.url, 'with timeout', self.connectionTimeout, 'ms') + self.connectionStarted = milliseconds() + try: + coroutine = self.create_connection(session) + self.connection = await wait_for(coroutine, timeout=int(self.connectionTimeout / 1000)) + self.connecting = False + self.connectionEstablished = milliseconds() + self.isConnected = True + if self.verbose: + self.log(iso8601(milliseconds()), 'connected') + self.connected.resolve(self.url) + self.on_connected_callback(self) + # run both loops forever + self.ping_looper = ensure_future(self.ping_loop(), loop=self.asyncio_loop) + self.asyncio_loop.call_soon(self.receive_loop) + except TimeoutError: + # connection timeout + error = RequestTimeout('Connection timeout') + if self.verbose: + self.log(iso8601(milliseconds()), 'RequestTimeout', error) + self.on_error(error) + except Exception as e: + # connection failed or rejected (ConnectionRefusedError, ClientConnectorError) + error = NetworkError(e) + if self.verbose: + self.log(iso8601(milliseconds()), 'NetworkError', error) + self.on_error(error) + + @property + def buffer(self): + # looks like they exposed it in C + # this means we can bypass it + # https://github.com/aio-libs/aiohttp/blob/master/aiohttp/_websocket/reader_c.pxd#L53C24-L53C31 + # these checks are necessary to protect these errors: AttributeError: 'NoneType' object has no attribute '_buffer' + # upon getting an error message + if self.connection is None: + return None + if self.connection._conn is None: + return None + if self.connection._conn.protocol is None: + return None + if self.connection._conn.protocol._payload is None: + return None + return self.connection._conn.protocol._payload._buffer + + def connect(self, session, backoff_delay=0): + if not self.connection and not self.connecting: + self.connecting = True + ensure_future(self.open(session, backoff_delay), loop=self.asyncio_loop) + return self.connected + + def on_error(self, error): + if self.verbose: + self.log(iso8601(milliseconds()), 'on_error', error) + self.error = error + self.reject(error) + self.on_error_callback(self, error) + if not self.closed(): + ensure_future(self.close(1006), loop=self.asyncio_loop) + + def on_close(self, code): + if self.verbose: + self.log(iso8601(milliseconds()), 'on_close', code) + if not self.error: + self.reject(NetworkError('Connection closed by remote server, closing code ' + str(code))) + self.on_close_callback(self, code) + ensure_future(self.aiohttp_close(), loop=self.asyncio_loop) + + def log(self, *args): + print(*args) + + def closed(self): + return (self.connection is None) or self.connection.closed + + def receive(self): + return self.connection.receive() + + # helper method for binary and text messages + def handle_text_or_binary_message(self, data): + if self.verbose: + self.log(iso8601(milliseconds()), 'message', data) + if isinstance(data, bytes): + if self.decompressBinary: + data = data.decode() + # decoded = json.loads(data) if is_json_encoded_object(data) else data + decode = None + if is_json_encoded_object(data): + if orjson is None: + decode = json.loads(data) + else: + decode = orjson.loads(data) + else: + decode = data + self.on_message_callback(self, decode) + + def handle_message(self, message): + # self.log(iso8601(milliseconds()), message) + if message.type == WSMsgType.TEXT: + self.handle_text_or_binary_message(message.data) + elif message.type == WSMsgType.BINARY: + data = message.data + if self.gunzip: + data = gunzip(data) + elif self.inflate: + data = inflate(data) + self.handle_text_or_binary_message(data) + # autoping is responsible for automatically replying with pong + # to a ping incoming from a server, we have to disable autoping + # with aiohttp's websockets and respond with pong manually + # otherwise aiohttp's websockets client won't trigger WSMsgType.PONG + elif message.type == WSMsgType.PING: + if self.verbose: + self.log(iso8601(milliseconds()), 'ping', message) + ensure_future(self.connection.pong(message.data), loop=self.asyncio_loop) + elif message.type == WSMsgType.PONG: + self.lastPong = milliseconds() + if self.verbose: + self.log(iso8601(milliseconds()), 'pong', message) + pass + elif message.type == WSMsgType.CLOSE: + if self.verbose: + self.log(iso8601(milliseconds()), 'close', self.closed(), message) + self.on_close(message.data) + elif message.type == WSMsgType.ERROR: + if self.verbose: + self.log(iso8601(milliseconds()), 'error', message) + error = NetworkError(str(message)) + self.on_error(error) + + def create_connection(self, session): + # autoping is responsible for automatically replying with pong + # to a ping incoming from a server, we have to disable autoping + # with aiohttp's websockets and respond with pong manually + # otherwise aiohttp's websockets client won't trigger WSMsgType.PONG + # call aenter here to simulate async with otherwise we get the error "await not called with future" + # if connecting to a non-existent endpoint + if (self.proxy): + return session.ws_connect(self.url, autoping=False, autoclose=False, headers=self.options.get('headers'), proxy=self.proxy, max_msg_size=10485760).__aenter__() + return session.ws_connect(self.url, autoping=False, autoclose=False, headers=self.options.get('headers'), max_msg_size=10485760).__aenter__() + + async def send(self, message): + if self.verbose: + self.log(iso8601(milliseconds()), 'sending', message) + send_msg = None + if isinstance(message, str): + send_msg = message + else: + if orjson is None: + send_msg = json.dumps(message, separators=(',', ':')) + else: + send_msg = orjson.dumps(message).decode('utf-8') + if self.closed(): + raise ConnectionError('Cannot Send Message: Connection closed before send') + return await self.connection.send_str(send_msg) + + async def close(self, code=1000): + if self.verbose: + self.log(iso8601(milliseconds()), 'closing', code) + for future in self.futures.values(): + future.cancel() + await self.aiohttp_close() + + async def aiohttp_close(self): + if not self.closed(): + await self.connection.close() + # these will end automatically once self.closed() = True + # so we don't need to cancel them + if self.ping_looper: + self.ping_looper.cancel() + + async def ping_loop(self): + if self.verbose: + self.log(iso8601(milliseconds()), 'ping loop') + while self.keepAlive and not self.closed(): + now = milliseconds() + self.lastPong = now if self.lastPong is None else self.lastPong + if (self.lastPong + self.keepAlive * self.maxPingPongMisses) < now: + self.on_error(RequestTimeout('Connection to ' + self.url + ' timed out due to a ping-pong keepalive missing on time')) + # the following ping-clause is not necessary with aiohttp's built-in ws + # since it has a heartbeat option (see create_connection above) + # however some exchanges require a text-type ping message + # therefore we need this clause anyway + else: + if self.ping: + try: + await self.send(self.ping(self)) + except Exception as e: + self.on_error(e) + else: + await self.connection.ping() + await sleep(self.keepAlive / 1000) diff --git a/ccxt/async_support/base/ws/functions.py b/ccxt/async_support/base/ws/functions.py new file mode 100644 index 0000000..2aa542c --- /dev/null +++ b/ccxt/async_support/base/ws/functions.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- + +from zlib import decompress, MAX_WBITS +from base64 import b64decode +from gzip import GzipFile +from io import BytesIO +import time +import datetime + + +def inflate(data): + return decompress(data, -MAX_WBITS) + + +def inflate64(data): + return inflate(b64decode(data)) + + +def gunzip(data): + return GzipFile('', 'rb', 9, BytesIO(data)).read().decode('utf-8') + + +# Tmp : added methods below to avoid circular imports between exchange.py and aiohttp.py + +def milliseconds(): + return int(time.time() * 1000) + + +def iso8601(timestamp=None): + if timestamp is None: + return timestamp + if not isinstance(timestamp, int): + return None + if int(timestamp) < 0: + return None + try: + utc = datetime.datetime.fromtimestamp(timestamp // 1000, datetime.timezone.utc) + return utc.strftime('%Y-%m-%dT%H:%M:%S.%f')[:-6] + "{:03d}".format(int(timestamp) % 1000) + 'Z' + except (TypeError, OverflowError, OSError): + return None + + +def is_json_encoded_object(input): + return (isinstance(input, str) and + (len(input) >= 2) and + ((input[0] == '{') or (input[0] == '['))) + + +def deep_extend(*args): + result = None + for arg in args: + if isinstance(arg, dict): + if not isinstance(result, dict): + result = {} + for key in arg: + result[key] = deep_extend(result[key] if key in result else None, arg[key]) + else: + result = arg + return result diff --git a/ccxt/async_support/base/ws/future.py b/ccxt/async_support/base/ws/future.py new file mode 100644 index 0000000..b79bf39 --- /dev/null +++ b/ccxt/async_support/base/ws/future.py @@ -0,0 +1,46 @@ +import asyncio + +# Test by running: +# - python python/ccxt/pro/test/base/test_close.py +# - python python/ccxt/pro/test/base/test_future.py +class Future(asyncio.Future): + + def resolve(self, result=None): + if not self.done(): + self.set_result(result) + + def reject(self, error=None): + if not self.done(): + self.set_exception(error) + + @classmethod + def race(cls, futures): + future = Future() + coro = asyncio.wait(futures, return_when=asyncio.FIRST_COMPLETED) + task = asyncio.create_task(coro) + + def callback(done): + complete, _ = done.result() + # check for exceptions + exceptions = [] + cancelled = False + for f in complete: + if f.cancelled(): + cancelled = True + else: + err = f.exception() + if err: + exceptions.append(err) + # if any exceptions return with first exception + if future.cancelled(): + return + if len(exceptions) > 0: + future.set_exception(exceptions[0]) + # else return first result + elif cancelled: + future.cancel() + else: + first_result = list(complete)[0].result() + future.set_result(first_result) + task.add_done_callback(callback) + return future diff --git a/ccxt/async_support/base/ws/order_book.py b/ccxt/async_support/base/ws/order_book.py new file mode 100644 index 0000000..dc9dbfd --- /dev/null +++ b/ccxt/async_support/base/ws/order_book.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- + +from ccxt.async_support.base.ws import order_book_side +from ccxt import Exchange +import sys + + +class OrderBook(dict): + def __init__(self, snapshot={}, depth=None): + self.cache = [] + depth = depth or sys.maxsize + defaults = { + 'bids': [], + 'asks': [], + 'timestamp': None, + 'datetime': None, + 'nonce': None, + 'symbol': None, + } + # do not mutate snapshot + defaults.update(snapshot) + if not isinstance(defaults['asks'], order_book_side.OrderBookSide): + defaults['asks'] = order_book_side.Asks(defaults['asks'], depth) + if not isinstance(defaults['bids'], order_book_side.OrderBookSide): + defaults['bids'] = order_book_side.Bids(defaults['bids'], depth) + defaults['datetime'] = Exchange.iso8601(defaults.get('timestamp')) + # merge to self + super(OrderBook, self).__init__(defaults) + + def limit(self): + self['asks'].limit() + self['bids'].limit() + return self + + def reset(self, snapshot={}): + self['asks']._index.clear() + self['asks'].clear() + for ask in snapshot.get('asks', []): + self['asks'].storeArray(ask) + self['bids']._index.clear() + self['bids'].clear() + for bid in snapshot.get('bids', []): + self['bids'].storeArray(bid) + self['nonce'] = snapshot.get('nonce') + self['timestamp'] = snapshot.get('timestamp') + self['datetime'] = Exchange.iso8601(self['timestamp']) + self['symbol'] = snapshot.get('symbol') + + def update(self, snapshot): + nonce = snapshot.get('nonce') + if nonce is not None and self['nonce'] is not None and nonce < self['nonce']: + return self + self.reset(snapshot) + +# ----------------------------------------------------------------------------- +# overwrites absolute volumes at price levels +# or deletes price levels based on order counts (3rd value in a bidask delta) + + +class CountedOrderBook(OrderBook): + def __init__(self, snapshot={}, depth=None): + copy = Exchange.extend(snapshot, { + 'asks': order_book_side.CountedAsks(snapshot.get('asks', []), depth), + 'bids': order_book_side.CountedBids(snapshot.get('bids', []), depth), + }) + super(CountedOrderBook, self).__init__(copy, depth) + +# ----------------------------------------------------------------------------- +# indexed by order ids (3rd value in a bidask delta) + + +class IndexedOrderBook(OrderBook): + def __init__(self, snapshot={}, depth=None): + copy = Exchange.extend(snapshot, { + 'asks': order_book_side.IndexedAsks(snapshot.get('asks', []), depth), + 'bids': order_book_side.IndexedBids(snapshot.get('bids', []), depth), + }) + super(IndexedOrderBook, self).__init__(copy, depth) diff --git a/ccxt/async_support/base/ws/order_book_side.py b/ccxt/async_support/base/ws/order_book_side.py new file mode 100644 index 0000000..c3bb888 --- /dev/null +++ b/ccxt/async_support/base/ws/order_book_side.py @@ -0,0 +1,174 @@ +# -*- coding: utf-8 -*- + +import sys +import bisect + +"""Author: Carlo Revelli""" +"""Fast bisect bindings""" +"""https://github.com/python/cpython/blob/master/Modules/_bisectmodule.c""" +"""Performs a binary search when inserting keys in sorted order""" + + +class OrderBookSide(list): + side = None # set to True for bids and False for asks + + def __init__(self, deltas=[], depth=None): + super(OrderBookSide, self).__init__() + self._depth = depth or sys.maxsize + self._n = sys.maxsize + # parallel to self + self._index = [] + for delta in deltas: + self.storeArray(list(delta)) + + def store_array(self, delta): + return self.storeArray(delta) + + def storeArray(self, delta): + price = delta[0] + size = delta[1] + index_price = -price if self.side else price + index = bisect.bisect_left(self._index, index_price) + if size: + if index < len(self._index) and self._index[index] == index_price: + self[index][1] = size + else: + self._index.insert(index, index_price) + self.insert(index, delta) + elif index < len(self._index) and self._index[index] == index_price: + del self._index[index] + del self[index] + + def store(self, price, size): + self.storeArray([price, size]) + + def limit(self): + difference = len(self) - self._depth + for _ in range(difference): + self.remove_index(self.pop()) + self._index.pop() + + def remove_index(self, order): + pass + + def __len__(self): + length = super(OrderBookSide, self).__len__() + return min(length, self._n) + + def __getitem__(self, item): + if isinstance(item, slice): + start, stop, step = item.indices(len(self)) + return [self[i] for i in range(start, stop, step)] + else: + return super(OrderBookSide, self).__getitem__(item) + + def __eq__(self, other): + if isinstance(other, list): + return list(self) == other + return super(OrderBookSide, self).__eq__(other) + + def __repr__(self): + return str(list(self)) + +# ----------------------------------------------------------------------------- +# overwrites absolute volumes at price levels +# or deletes price levels based on order counts (3rd value in a bidask delta) +# this class stores vector arrays of values indexed by price + + +class CountedOrderBookSide(OrderBookSide): + def __init__(self, deltas=[], depth=None): + super(CountedOrderBookSide, self).__init__(deltas, depth) + + def storeArray(self, delta): + price = delta[0] + size = delta[1] + count = delta[2] + index_price = -price if self.side else price + index = bisect.bisect_left(self._index, index_price) + if size and count: + if index < len(self._index) and self._index[index] == index_price: + self[index][1] = size + self[index][2] = count + else: + self._index.insert(index, index_price) + self.insert(index, delta) + elif index < len(self._index) and self._index[index] == index_price: + del self._index[index] + del self[index] + + def store(self, price, size, count): + self.storeArray([price, size, count]) + +# ----------------------------------------------------------------------------- +# indexed by order ids (3rd value in a bidask delta) + + +class IndexedOrderBookSide(OrderBookSide): + def __init__(self, deltas=[], depth=None): + self._hashmap = {} + super(IndexedOrderBookSide, self).__init__(deltas, depth) + + def storeArray(self, delta): + price = delta[0] + if price is not None: + index_price = -price if self.side else price + else: + index_price = None + size = delta[1] + order_id = delta[2] + if size: + if order_id in self._hashmap: + old_price = self._hashmap[order_id] + index_price = index_price or old_price + # in case the price is not defined + delta[0] = abs(index_price) + # matches if price is not defined or if price matches + if index_price == old_price: + # just overwrite the old index + index = bisect.bisect_left(self._index, index_price) + while self[index][2] != order_id: + index += 1 + self._index[index] = index_price + self[index] = delta + return + else: + # remove old price level + old_index = bisect.bisect_left(self._index, old_price) + while self[old_index][2] != order_id: + old_index += 1 + del self._index[old_index] + del self[old_index] + # insert new price level + self._hashmap[order_id] = index_price + index = bisect.bisect_left(self._index, index_price) + while index < len (self._index) and self._index[index] == index_price and self[index][2] < order_id: + index += 1 + self._index.insert(index, index_price) + self.insert(index, delta) + elif order_id in self._hashmap: + old_price = self._hashmap[order_id] + index = bisect.bisect_left(self._index, old_price) + while self[index][2] != order_id: + index += 1 + del self._index[index] + del self[index] + del self._hashmap[order_id] + + def remove_index(self, order): + order_id = order[2] + if order_id in self._hashmap: + del self._hashmap[order_id] + + def store(self, price, size, order_id): + self.storeArray([price, size, order_id]) + +# ----------------------------------------------------------------------------- +# a more elegant syntax is possible here, but native inheritance is portable + +class Asks(OrderBookSide): side = False # noqa +class Bids(OrderBookSide): side = True # noqa +class CountedAsks(CountedOrderBookSide): side = False # noqa +class CountedBids(CountedOrderBookSide): side = True # noqa +class IndexedAsks(IndexedOrderBookSide): side = False # noqa +class IndexedBids(IndexedOrderBookSide): side = True # noqa diff --git a/ccxt/async_support/bequant.py b/ccxt/async_support/bequant.py new file mode 100644 index 0000000..5ad20af --- /dev/null +++ b/ccxt/async_support/bequant.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.hitbtc import hitbtc +from ccxt.abstract.bequant import ImplicitAPI +from ccxt.base.types import Any + + +class bequant(hitbtc, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(bequant, self).describe(), { + 'id': 'bequant', + 'name': 'Bequant', + 'pro': True, + 'countries': ['MT'], # Malta + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/0583ef1f-29fe-4b7c-8189-63565a0e2867', + 'api': { + # v3 + 'public': 'https://api.bequant.io/api/3', + 'private': 'https://api.bequant.io/api/3', + }, + 'www': 'https://bequant.io', + 'doc': [ + 'https://api.bequant.io/', + ], + 'fees': [ + 'https://bequant.io/fees-and-limits', + ], + 'referral': 'https://bequant.io/referral/dd104e3bee7634ec', + }, + }) diff --git a/ccxt/async_support/bigone.py b/ccxt/async_support/bigone.py new file mode 100644 index 0000000..5508b8b --- /dev/null +++ b/ccxt/async_support/bigone.py @@ -0,0 +1,2268 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.bigone import ImplicitAPI +import asyncio +from ccxt.base.types import Any, Balances, Currencies, Currency, DepositAddress, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, Transaction, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import NotSupported +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class bigone(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(bigone, self).describe(), { + 'id': 'bigone', + 'name': 'BigONE', + 'countries': ['CN'], + 'version': 'v3', + 'rateLimit': 20, # 500 requests per 10 seconds + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': True, + 'future': None, # has but unimplemented + 'option': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'createMarketBuyOrderWithCost': True, + 'createMarketOrderWithCost': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createPostOnlyOrder': True, + 'createStopLimitOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'fetchAllGreeks': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchFundingHistory': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchMarkets': True, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': False, + 'fetchTransactionFees': False, + 'fetchVolatilityHistory': False, + 'fetchWithdrawals': True, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'transfer': True, + 'withdraw': True, + }, + 'timeframes': { + '1m': 'min1', + '5m': 'min5', + '15m': 'min15', + '30m': 'min30', + '1h': 'hour1', + '3h': 'hour3', + '4h': 'hour4', + '6h': 'hour6', + '12h': 'hour12', + '1d': 'day1', + '1w': 'week1', + '1M': 'month1', + }, + 'hostname': 'big.one', # or 'bigone.com' + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/4e5cfd53-98cc-4b90-92cd-0d7b512653d1', + 'api': { + 'public': 'https://{hostname}/api/v3', + 'private': 'https://{hostname}/api/v3/viewer', + 'contractPublic': 'https://{hostname}/api/contract/v2', + 'contractPrivate': 'https://{hostname}/api/contract/v2', + 'webExchange': 'https://{hostname}/api/', + }, + 'www': 'https://big.one', + 'doc': 'https://open.big.one/docs/api.html', + 'fees': 'https://bigone.zendesk.com/hc/en-us/articles/115001933374-BigONE-Fee-Policy', + 'referral': 'https://b1.run/users/new?code=D3LLBVFT', + }, + 'api': { + 'public': { + 'get': [ + 'ping', + 'asset_pairs', + 'asset_pairs/{asset_pair_name}/depth', + 'asset_pairs/{asset_pair_name}/trades', + 'asset_pairs/{asset_pair_name}/ticker', + 'asset_pairs/{asset_pair_name}/candles', + 'asset_pairs/tickers', + ], + }, + 'private': { + 'get': [ + 'accounts', + 'fund/accounts', + 'assets/{asset_symbol}/address', + 'orders', + 'orders/{id}', + 'orders/multi', + 'trades', + 'withdrawals', + 'deposits', + ], + 'post': [ + 'orders', + 'orders/{id}/cancel', + 'orders/cancel', + 'withdrawals', + 'transfer', + ], + }, + 'contractPublic': { + 'get': [ + 'symbols', + 'instruments', + 'depth@{symbol}/snapshot', + 'instruments/difference', + 'instruments/prices', + ], + }, + 'contractPrivate': { + 'get': [ + 'accounts', + 'orders/{id}', + 'orders', + 'orders/opening', + 'orders/count', + 'orders/opening/count', + 'trades', + 'trades/count', + ], + 'post': [ + 'orders', + 'orders/batch', + ], + 'put': [ + 'positions/{symbol}/margin', + 'positions/{symbol}/risk-limit', + ], + 'delete': [ + 'orders/{id}', + 'orders/batch', + ], + }, + 'webExchange': { + 'get': [ + 'v3/assets', + ], + }, + }, + 'fees': { + 'trading': { + 'maker': self.parse_number('0.001'), + 'taker': self.parse_number('0.001'), + }, + 'funding': { + 'withdraw': {}, + }, + }, + 'options': { + 'createMarketBuyOrderRequiresPrice': True, + 'accountsByType': { + 'spot': 'SPOT', + 'fund': 'FUND', + 'funding': 'FUND', + 'future': 'CONTRACT', + 'swap': 'CONTRACT', + }, + 'transfer': { + 'fillResponseFromRequest': True, + }, + 'exchangeMillisecondsCorrection': -100, + 'fetchCurrencies': { + 'webApiEnable': True, # fetches from WEB + 'webApiRetries': 5, + 'webApiMuteFailure': True, + }, + 'defaultNetwork': 'ERC20', + 'defaultNetworks': { + 'USDT': 'TRC20', + }, + 'networks': { + 'ABBC': 'ABBC', + 'ACA': 'Acala', + 'AE': 'Aeternity', + 'ALGO': 'Algorand', + 'APT': 'Aptos', + 'AR': 'Arweave', + 'ASTR': 'Astar', + 'AVAXC': 'Avax', + 'AVAXX': 'AvaxChain', + 'BEAM': 'Beam', + 'BEP20': 'BinanceSmartChain', + 'BITCI': 'BitciChain', + 'BTC': 'Bitcoin', + 'BCH': 'BitcoinCash', + 'BSV': 'BitcoinSV', + 'CELO': 'Celo', + 'CKKB': 'CKB', + 'ATOM': 'Cosmos', + 'CRC20': 'CRO', + 'DASH': 'Dash', + 'DOGE': 'Dogecoin', + 'XEC': 'ECash', + 'EOS': 'EOS', + 'ETH': 'Ethereum', + 'ETC': 'EthereumClassic', + 'ETHW': 'EthereumPow', + 'FTM': 'Fantom', + 'FIL': 'Filecoin', + 'FSN': 'Fusion', + 'GRIN': 'Grin', + 'ONE': 'Harmony', + 'HRC20': 'Hecochain', + 'HBAR': 'Hedera', + 'HNT': 'Helium', + 'ZEN': 'Horizen', + 'IOST': 'IOST', + 'IRIS': 'IRIS', + 'KLAY': 'Klaytn', + 'KSM': 'Kusama', + 'LTC': 'Litecoin', + 'XMR': 'Monero', + 'GLMR': 'Moonbeam', + 'NEAR': 'Near', + 'NEO': 'Neo', + 'NEON3': 'NeoN3', + 'OASIS': 'Oasis', + 'OKC': 'Okexchain', + 'ONT': 'Ontology', + 'OPTIMISM': 'Optimism', + 'DOT': 'Polkadot', + 'MATIC': 'Polygon', + 'QTUM': 'Qtum', + 'REI': 'REI', + 'XRP': 'Ripple', + 'SGB': 'SGB', + 'SDN': 'Shiden', + 'SOL': 'Solana', + 'XLM': 'Stellar', + 'TERA': 'Tera', + 'XTZ': 'Tezos', + 'TRC20': 'Tron', + 'VET': 'Vechain', + 'VSYS': 'VSystems', + 'WAX': 'WAX', + 'ZEC': 'Zcash', + # todo: uncomment after consensus + # 'BITSHARES_OLD': 'Bitshares', + # 'BITSHARES_NEW': 'NewBitshares', + # 'MOBILECOIN': 'Mobilecoin', + # 'LBRY': 'Lbry', + # 'ZEEPIN': 'Zeepin', + # 'WAYFCOIN': 'Wayfcoin', + # 'UCACOIN': 'Ucacoin', + # 'VANILLACASH': 'Vcash', + # 'LAMDEN': 'Lamden', + # 'GXSHARES': 'Gxshares', + # 'ICP': 'Dfinity', + # 'CLOVER': 'Clover', + # 'CLASSZZ': 'Classzz', + # 'CLASSZZ_V2': 'ClasszzV2', + # 'CHAINX_V2': 'ChainxV2', + # 'BITCOINDIAMON': 'BitcoinDiamond', + # 'BITCOINGOLD': 'BitcoinGold', + # 'BUTTRUSTSYSTEM': 'BitTrustSystem', + # 'BYTOM_V2': 'BytomV2', + # 'LIBONOMY': 'Libonomy', + # 'TERRACLASSIC': 'Terra', + # 'TERRA': 'Terra2', + # 'SUPERBITCOIN': 'SuperBitcoin', + # 'SIACLASSIC': 'Sia', + # 'SIACOIN': 'SiaCore', + # 'PARALLELFINANCE': 'Parallel', + # 'PLCULTIMA': 'Plcu', + # 'PLCULTIMA2': 'Plcu2', + # undetermined: XinFin, YAS, Ycash + }, + }, + 'features': { + 'default': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerPriceType': None, + 'triggerDirection': True, # todo implement + 'stopLossPrice': False, # todo by trigger + 'takeProfitPrice': False, # todo by trigger + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': False, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyRequiresPrice': True, + 'marketBuyByCost': True, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, # todo: implement + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 200, + 'daysBack': None, + 'untilDays': None, + 'symbolRequired': True, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 200, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': 200, + 'daysBack': None, + 'untilDays': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 200, + 'daysBack': None, + 'daysBackCanceled': None, + 'untilDays': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOHLCV': { + 'limit': 500, + }, + }, + 'spot': { + 'extends': 'default', + }, + 'forDerivatives': { + 'extends': 'default', + 'createOrder': { + # todo: implement + 'triggerPriceType': { + 'mark': True, + 'index': True, + 'last': True, + }, + }, + 'fetchOrders': { + 'daysBack': 100000, + 'untilDays': 100000, + }, + 'fetchClosedOrders': { + 'daysBack': 100000, + 'untilDays': 100000, + }, + }, + 'swap': { + 'linear': { + 'extends': 'forDerivatives', + }, + 'inverse': { + 'extends': 'forDerivatives', + }, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'exact': { + '10001': BadRequest, # syntax error + '10005': ExchangeError, # internal error + "Amount's scale must greater than AssetPair's base scale": InvalidOrder, + "Price mulit with amount should larger than AssetPair's min_quote_value": InvalidOrder, + '10007': BadRequest, # parameter error, {"code":10007,"message":"Amount's scale must greater than AssetPair's base scale"} + '10011': ExchangeError, # system error + '10013': BadSymbol, # {"code":10013,"message":"Resource not found"} + '10014': InsufficientFunds, # {"code":10014,"message":"Insufficient funds"} + '10403': PermissionDenied, # permission denied + '10429': RateLimitExceeded, # too many requests + '40004': AuthenticationError, # {"code":40004,"message":"invalid jwt"} + '40103': AuthenticationError, # invalid otp code + '40104': AuthenticationError, # invalid asset pin code + '40301': PermissionDenied, # {"code":40301,"message":"Permission denied withdrawal create"} + '40302': ExchangeError, # already requested + '40601': ExchangeError, # resource is locked + '40602': ExchangeError, # resource is depleted + '40603': InsufficientFunds, # insufficient resource + '40604': InvalidOrder, # {"code":40604,"message":"Price exceed the maximum order price"} + '40605': InvalidOrder, # {"code":40605,"message":"Price less than the minimum order price"} + '40120': InvalidOrder, # Order is in trading + '40121': InvalidOrder, # Order is already cancelled or filled + '60100': BadSymbol, # {"code":60100,"message":"Asset pair is suspended"} + }, + 'broad': { + }, + }, + 'commonCurrencies': { + 'CRE': 'Cybereits', + 'FXT': 'FXTTOKEN', + 'FREE': 'FreeRossDAO', + 'MBN': 'Mobilian Coin', + 'ONE': 'BigONE Token', + }, + }) + + async def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + # we use undocumented link(possible, less informative alternative is : https://big.one/api/uc/v3/assets/accounts) + data = await self.fetch_web_endpoint('fetchCurrencies', 'webExchangeGetV3Assets', True) + if data is None: + return {} + # + # { + # "code": "0", + # "message": "", + # "data": [ + # { + # "uuid": "17082d1c-0195-4fb6-8779-2cdbcb9eeb3c", + # "symbol": "USDT", + # "name": "TetherUS", + # "scale": 12, + # "is_fiat": False, + # "is_transfer_enabled": True, + # "transfer_scale": 12, + # "binding_gateways": [ + # { + # "guid": "07efc37f-d1ec-4bc9-8339-a745256ea2ba", + # "is_deposit_enabled": True, + # "gateway_name": "Ethereum", + # "min_withdrawal_amount": "0.000001", + # "withdrawal_fee": "5.71", + # "is_withdrawal_enabled": True, + # "min_deposit_amount": "0.000001", + # "is_memo_required": False, + # "withdrawal_scale": 6, + # "scale": 12 + # }, + # { + # "guid": "4e387a9a-a480-40a3-b4ae-ed1773c2db5a", + # "is_deposit_enabled": True, + # "gateway_name": "BinanceSmartChain", + # "min_withdrawal_amount": "10", + # "withdrawal_fee": "5", + # "is_withdrawal_enabled": False, + # "min_deposit_amount": "1", + # "is_memo_required": False, + # "withdrawal_scale": 8, + # "scale": 12 + # } + # ] + # }, + # ... + # ], + # } + # + currenciesData = self.safe_list(data, 'data', []) + result: dict = {} + for i in range(0, len(currenciesData)): + currency = currenciesData[i] + id = self.safe_string(currency, 'symbol') + code = self.safe_currency_code(id) + name = self.safe_string(currency, 'name') + networks: dict = {} + chains = self.safe_list(currency, 'binding_gateways', []) + currencyMaxPrecision = self.parse_precision(self.safe_string_2(currency, 'withdrawal_scale', 'scale')) + for j in range(0, len(chains)): + chain = chains[j] + networkId = self.safe_string(chain, 'gateway_name') + networkCode = self.network_id_to_code(networkId) + deposit = self.safe_bool(chain, 'is_deposit_enabled') + withdraw = self.safe_bool(chain, 'is_withdrawal_enabled') + minDepositAmount = self.safe_string(chain, 'min_deposit_amount') + minWithdrawalAmount = self.safe_string(chain, 'min_withdrawal_amount') + withdrawalFee = self.safe_string(chain, 'withdrawal_fee') + precision = self.parse_precision(self.safe_string_2(chain, 'withdrawal_scale', 'scale')) + networks[networkCode] = { + 'id': networkId, + 'network': networkCode, + 'margin': None, + 'deposit': deposit, + 'withdraw': withdraw, + 'active': None, + 'fee': self.parse_number(withdrawalFee), + 'precision': self.parse_number(precision), + 'limits': { + 'deposit': { + 'min': minDepositAmount, + 'max': None, + }, + 'withdraw': { + 'min': minWithdrawalAmount, + 'max': None, + }, + }, + 'info': chain, + } + chainLength = len(chains) + type: Str = None + if self.safe_bool(currency, 'is_fiat'): + type = 'fiat' + elif chainLength == 0: + if self.is_leveraged_currency(id): + type = 'leveraged' + else: + type = 'other' + else: + type = 'crypto' + result[code] = self.safe_currency_structure({ + 'id': id, + 'code': code, + 'info': currency, + 'name': name, + 'type': type, + 'active': None, + 'deposit': None, + 'withdraw': None, + 'fee': None, + 'precision': self.parse_number(currencyMaxPrecision), + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + }, + 'networks': networks, + }) + return result + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for bigone + + https://open.big.one/docs/spot_asset_pair.html + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + promises = [self.publicGetAssetPairs(params), self.contractPublicGetSymbols(params)] + promisesResult = await asyncio.gather(*promises) + response = promisesResult[0] + contractResponse = promisesResult[1] + # + # { + # "code":0, + # "data":[ + # { + # "id":"01e48809-b42f-4a38-96b1-c4c547365db1", + # "name":"PCX-BTC", + # "quote_scale":7, + # "quote_asset":{ + # "id":"0df9c3c3-255a-46d7-ab82-dedae169fba9", + # "symbol":"BTC", + # "name":"Bitcoin", + # }, + # "base_asset":{ + # "id":"405484f7-4b03-4378-a9c1-2bd718ecab51", + # "symbol":"PCX", + # "name":"ChainX", + # }, + # "base_scale":3, + # "min_quote_value":"0.0001", + # "max_quote_value":"35" + # }, + # ] + # } + # + # + # [ + # { + # "baseCurrency": "BTC", + # "multiplier": 1, + # "enable": True, + # "priceStep": 0.5, + # "maxRiskLimit": 1000, + # "pricePrecision": 1, + # "maintenanceMargin": 0.00500, + # "symbol": "BTCUSD", + # "valuePrecision": 4, + # "minRiskLimit": 100, + # "riskLimit": 100, + # "isInverse": True, + # "riskStep": 1, + # "settleCurrency": "BTC", + # "baseName": "Bitcoin", + # "feePrecision": 8, + # "priceMin": 0.5, + # "priceMax": 1E+6, + # "initialMargin": 0.01000, + # "quoteCurrency": "USD" + # }, + # ... + # ] + # + markets = self.safe_list(response, 'data', []) + result = [] + for i in range(0, len(markets)): + market = markets[i] + baseAsset = self.safe_dict(market, 'base_asset', {}) + quoteAsset = self.safe_dict(market, 'quote_asset', {}) + baseId = self.safe_string(baseAsset, 'symbol') + quoteId = self.safe_string(quoteAsset, 'symbol') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + result.append(self.safe_market_structure({ + 'id': self.safe_string(market, 'name'), + 'uuid': self.safe_string(market, 'id'), + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': True, + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(self.parse_precision(self.safe_string(market, 'base_scale'))), + 'price': self.parse_number(self.parse_precision(self.safe_string(market, 'quote_scale'))), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': None, + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': self.safe_number(market, 'min_quote_value'), + 'max': self.safe_number(market, 'max_quote_value'), + }, + }, + 'created': None, + 'info': market, + })) + for i in range(0, len(contractResponse)): + market = contractResponse[i] + baseId = self.safe_string(market, 'baseCurrency') + quoteId = self.safe_string(market, 'quoteCurrency') + settleId = self.safe_string(market, 'settleCurrency') + marketId = self.safe_string(market, 'symbol') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + settle = self.safe_currency_code(settleId) + inverse = self.safe_bool(market, 'isInverse') + result.append(self.safe_market_structure({ + 'id': marketId, + 'symbol': base + '/' + quote + ':' + settle, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': 'swap', + 'spot': False, + 'margin': False, + 'swap': True, + 'future': False, + 'option': False, + 'active': self.safe_bool(market, 'enable'), + 'contract': True, + 'linear': not inverse, + 'inverse': inverse, + 'contractSize': self.safe_number(market, 'multiplier'), + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(self.parse_precision(self.safe_string(market, 'valuePrecision'))), + 'price': self.parse_number(self.parse_precision(self.safe_string(market, 'pricePrecision'))), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': None, + 'max': None, + }, + 'price': { + 'min': self.safe_number(market, 'priceMin'), + 'max': self.safe_number(market, 'priceMax'), + }, + 'cost': { + 'min': self.safe_number(market, 'initialMargin'), + 'max': None, + }, + }, + 'info': market, + })) + return result + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # spot + # + # { + # "asset_pair_name": "ETH-BTC", + # "bid": { + # "price": "0.021593", + # "order_count": 1, + # "quantity": "0.20936" + # }, + # "ask": { + # "price": "0.021613", + # "order_count": 1, + # "quantity": "2.87064" + # }, + # "open": "0.021795", + # "high": "0.021795", + # "low": "0.021471", + # "close": "0.021613", + # "volume": "117078.90431", + # "daily_change": "-0.000182" + # } + # + # contract + # + # { + # "usdtPrice": 1.00031998, + # "symbol": "BTCUSD", + # "btcPrice": 34700.4, + # "ethPrice": 1787.83, + # "nextFundingRate": 0.00010, + # "fundingRate": 0.00010, + # "latestPrice": 34708.5, + # "last24hPriceChange": 0.0321, + # "indexPrice": 34700.4, + # "volume24h": 261319063, + # "turnover24h": 8204.129380685496, + # "nextFundingTime": 1698285600000, + # "markPrice": 34702.4646738, + # "last24hMaxPrice": 35127.5, + # "volume24hInUsd": 0.0, + # "openValue": 32.88054722085945, + # "last24hMinPrice": 33552.0, + # "openInterest": 1141372.0 + # } + # + marketType = 'spot' if ('asset_pair_name' in ticker) else 'swap' + marketId = self.safe_string_2(ticker, 'asset_pair_name', 'symbol') + symbol = self.safe_symbol(marketId, market, '-', marketType) + close = self.safe_string_2(ticker, 'close', 'latestPrice') + bid = self.safe_dict(ticker, 'bid', {}) + ask = self.safe_dict(ticker, 'ask', {}) + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': None, + 'datetime': None, + 'high': self.safe_string_2(ticker, 'high', 'last24hMaxPrice'), + 'low': self.safe_string_2(ticker, 'low', 'last24hMinPrice'), + 'bid': self.safe_string(bid, 'price'), + 'bidVolume': self.safe_string(bid, 'quantity'), + 'ask': self.safe_string(ask, 'price'), + 'askVolume': self.safe_string(ask, 'quantity'), + 'vwap': None, + 'open': self.safe_string(ticker, 'open'), + 'close': close, + 'last': close, + 'previousClose': None, + 'change': self.safe_string_2(ticker, 'daily_change', 'last24hPriceChange'), + 'percentage': None, + 'average': None, + 'baseVolume': self.safe_string_2(ticker, 'volume', 'volume24h'), + 'quoteVolume': self.safe_string(ticker, 'volume24hInUsd'), + 'markPrice': self.safe_string(ticker, 'markPrice'), + 'indexPrice': self.safe_string(ticker, 'indexPrice'), + 'info': ticker, + }, market) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://open.big.one/docs/spot_tickers.html + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + type = None + type, params = self.handle_market_type_and_params('fetchTicker', market, params) + if type == 'spot': + request: dict = { + 'asset_pair_name': market['id'], + } + response = await self.publicGetAssetPairsAssetPairNameTicker(self.extend(request, params)) + # + # { + # "code":0, + # "data":{ + # "asset_pair_name":"ETH-BTC", + # "bid":{"price":"0.021593","order_count":1,"quantity":"0.20936"}, + # "ask":{"price":"0.021613","order_count":1,"quantity":"2.87064"}, + # "open":"0.021795", + # "high":"0.021795", + # "low":"0.021471", + # "close":"0.021613", + # "volume":"117078.90431", + # "daily_change":"-0.000182" + # } + # } + # + ticker = self.safe_dict(response, 'data', {}) + return self.parse_ticker(ticker, market) + else: + tickers = await self.fetch_tickers([symbol], params) + return self.safe_value(tickers, symbol) + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://open.big.one/docs/spot_tickers.html + + :param str[] [symbols]: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + market = None + symbol = self.safe_string(symbols, 0) + if symbol is not None: + market = self.market(symbol) + type = None + type, params = self.handle_market_type_and_params('fetchTickers', market, params) + isSpot = type == 'spot' + request: dict = {} + symbols = self.market_symbols(symbols) + data = None + if isSpot: + if symbols is not None: + ids = self.market_ids(symbols) + request['pair_names'] = ','.join(ids) + response = await self.publicGetAssetPairsTickers(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "asset_pair_name": "PCX-BTC", + # "bid": { + # "price": "0.000234", + # "order_count": 1, + # "quantity": "0.518" + # }, + # "ask": { + # "price": "0.0002348", + # "order_count": 1, + # "quantity": "2.348" + # }, + # "open": "0.0002343", + # "high": "0.0002348", + # "low": "0.0002162", + # "close": "0.0002348", + # "volume": "12887.016", + # "daily_change": "0.0000005" + # }, + # ... + # ] + # } + # + data = self.safe_list(response, 'data', []) + else: + data = await self.contractPublicGetInstruments(params) + # + # [ + # { + # "usdtPrice": 1.00031998, + # "symbol": "BTCUSD", + # "btcPrice": 34700.4, + # "ethPrice": 1787.83, + # "nextFundingRate": 0.00010, + # "fundingRate": 0.00010, + # "latestPrice": 34708.5, + # "last24hPriceChange": 0.0321, + # "indexPrice": 34700.4, + # "volume24h": 261319063, + # "turnover24h": 8204.129380685496, + # "nextFundingTime": 1698285600000, + # "markPrice": 34702.4646738, + # "last24hMaxPrice": 35127.5, + # "volume24hInUsd": 0.0, + # "openValue": 32.88054722085945, + # "last24hMinPrice": 33552.0, + # "openInterest": 1141372.0 + # } + # ... + # ] + # + tickers = self.parse_tickers(data, symbols) + return self.filter_by_array_tickers(tickers, 'symbol', symbols) + + async def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://open.big.one/docs/spot_ping.html + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = await self.publicGetPing(params) + # + # { + # "data": { + # "timestamp": 1527665262168391000 + # } + # } + # + data = self.safe_dict(response, 'data', {}) + timestamp = self.safe_integer(data, 'Timestamp') + return self.parse_to_int(timestamp / 1000000) + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://open.big.one/docs/contract_misc.html#get-orderbook-snapshot + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + response = None + if market['contract']: + request: dict = { + 'symbol': market['id'], + } + response = await self.contractPublicGetDepthSymbolSnapshot(self.extend(request, params)) + # + # { + # bids: { + # '20000': '20', + # ... + # '34552': '64851', + # '34526.5': '59594', + # ... + # '34551.5': '29711' + # }, + # asks: { + # '34557': '34395', + # ... + # '40000': '20', + # '34611.5': '56024', + # ... + # '34578.5': '66367' + # }, + # to: '59737174', + # lastPrice: '34554.5', + # bestPrices: { + # ask: '34557.0', + # bid: '34552.0' + # }, + # from: '0' + # } + # + return self.parse_contract_order_book(response, market['symbol'], limit) + else: + request: dict = { + 'asset_pair_name': market['id'], + } + if limit is not None: + request['limit'] = limit # default 50, max 200 + response = await self.publicGetAssetPairsAssetPairNameDepth(self.extend(request, params)) + # + # { + # "code":0, + # "data": { + # "asset_pair_name": "EOS-BTC", + # "bids": [ + # {"price": "42", "order_count": 4, "quantity": "23.33363711"} + # ], + # "asks": [ + # {"price": "45", "order_count": 2, "quantity": "4193.3283464"} + # ] + # } + # } + # + orderbook = self.safe_dict(response, 'data', {}) + return self.parse_order_book(orderbook, market['symbol'], None, 'bids', 'asks', 'price', 'quantity') + + def parse_contract_bids_asks(self, bidsAsks): + bidsAsksKeys = list(bidsAsks.keys()) + result = [] + for i in range(0, len(bidsAsksKeys)): + price = bidsAsksKeys[i] + amount = bidsAsks[price] + result.append([self.parse_number(price), self.parse_number(amount)]) + return result + + def parse_contract_order_book(self, orderbook: object, symbol: str, limit: Int = None) -> OrderBook: + responseBids = self.safe_value(orderbook, 'bids') + responseAsks = self.safe_value(orderbook, 'asks') + bids = self.parse_contract_bids_asks(responseBids) + asks = self.parse_contract_bids_asks(responseAsks) + return { + 'symbol': symbol, + 'bids': self.filter_by_limit(self.sort_by(bids, 0, True), limit), + 'asks': self.filter_by_limit(self.sort_by(asks, 0), limit), + 'timestamp': None, + 'datetime': None, + 'nonce': None, + } + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(public) + # + # { + # "id": 38199941, + # "price": "3378.67", + # "amount": "0.019812", + # "taker_side": "ASK", + # "created_at": "2019-01-29T06:05:56Z" + # } + # + # fetchMyTrades(private) + # + # { + # "id": 10854280, + # "asset_pair_name": "XIN-USDT", + # "price": "70", + # "amount": "1", + # "taker_side": "ASK", + # "maker_order_id": 58284908, + # "taker_order_id": 58284909, + # "maker_fee": "0.0008", + # "taker_fee": "0.07", + # "side": "SELF_TRADING", + # "inserted_at": "2019-04-16T12:00:01Z" + # }, + # + # { + # "id": 10854263, + # "asset_pair_name": "XIN-USDT", + # "price": "75.7", + # "amount": "12.743149", + # "taker_side": "BID", + # "maker_order_id": null, + # "taker_order_id": 58284888, + # "maker_fee": null, + # "taker_fee": "0.0025486298", + # "side": "BID", + # "inserted_at": "2019-04-15T06:20:57Z" + # } + # + timestamp = self.parse8601(self.safe_string_2(trade, 'created_at', 'inserted_at')) + priceString = self.safe_string(trade, 'price') + amountString = self.safe_string(trade, 'amount') + marketId = self.safe_string(trade, 'asset_pair_name') + market = self.safe_market(marketId, market, '-') + side = self.safe_string(trade, 'side') + takerSide = self.safe_string(trade, 'taker_side') + takerOrMaker: Str = None + if (takerSide is not None) and (side is not None) and (side != 'SELF_TRADING'): + takerOrMaker = 'taker' if (takerSide == side) else 'maker' + if side is None: + # taker side is not related to buy/sell side + # the following code is probably a mistake + side = 'sell' if (takerSide == 'ASK') else 'buy' + else: + if side == 'BID': + side = 'buy' + elif side == 'ASK': + side = 'sell' + makerOrderId = self.safe_string(trade, 'maker_order_id') + takerOrderId = self.safe_string(trade, 'taker_order_id') + orderId: Str = None + if makerOrderId is not None: + orderId = makerOrderId + elif takerOrderId is not None: + orderId = takerOrderId + id = self.safe_string(trade, 'id') + result: dict = { + 'id': id, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'order': orderId, + 'type': 'limit', + 'side': side, + 'takerOrMaker': takerOrMaker, + 'price': priceString, + 'amount': amountString, + 'cost': None, + 'info': trade, + } + makerCurrencyCode: str + takerCurrencyCode: str + if takerOrMaker is not None: + if side == 'buy': + if takerOrMaker == 'maker': + makerCurrencyCode = market['base'] + takerCurrencyCode = market['quote'] + else: + makerCurrencyCode = market['quote'] + takerCurrencyCode = market['base'] + else: + if takerOrMaker == 'maker': + makerCurrencyCode = market['quote'] + takerCurrencyCode = market['base'] + else: + makerCurrencyCode = market['base'] + takerCurrencyCode = market['quote'] + elif side == 'SELF_TRADING': + if takerSide == 'BID': + makerCurrencyCode = market['quote'] + takerCurrencyCode = market['base'] + elif takerSide == 'ASK': + makerCurrencyCode = market['base'] + takerCurrencyCode = market['quote'] + makerFeeCost = self.safe_string(trade, 'maker_fee') + takerFeeCost = self.safe_string(trade, 'taker_fee') + if makerFeeCost is not None: + if takerFeeCost is not None: + result['fees'] = [ + {'cost': makerFeeCost, 'currency': makerCurrencyCode}, + {'cost': takerFeeCost, 'currency': takerCurrencyCode}, + ] + else: + result['fee'] = {'cost': makerFeeCost, 'currency': makerCurrencyCode} + elif takerFeeCost is not None: + result['fee'] = {'cost': takerFeeCost, 'currency': takerCurrencyCode} + else: + result['fee'] = None + return self.safe_trade(result, market) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://open.big.one/docs/spot_asset_pair_trade.html + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + if market['contract']: + raise NotSupported(self.id + ' fetchTrades() can only fetch trades for spot markets') + request: dict = { + 'asset_pair_name': market['id'], + } + response = await self.publicGetAssetPairsAssetPairNameTrades(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "id": 38199941, + # "price": "3378.67", + # "amount": "0.019812", + # "taker_side": "ASK", + # "created_at": "2019-01-29T06:05:56Z" + # }, + # { + # "id": 38199934, + # "price": "3376.14", + # "amount": "0.019384", + # "taker_side": "ASK", + # "created_at": "2019-01-29T06:05:40Z" + # } + # ] + # } + # + trades = self.safe_list(response, 'data', []) + return self.parse_trades(trades, market, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # { + # "close": "0.021562", + # "high": "0.021563", + # "low": "0.02156", + # "open": "0.021563", + # "time": "2019-11-21T07:54:00Z", + # "volume": "59.84376" + # } + # + return [ + self.parse8601(self.safe_string(ohlcv, 'time')), + self.safe_number(ohlcv, 'open'), + self.safe_number(ohlcv, 'high'), + self.safe_number(ohlcv, 'low'), + self.safe_number(ohlcv, 'close'), + self.safe_number(ohlcv, 'volume'), + ] + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://open.big.one/docs/spot_asset_pair_candle.html + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the earliest candle to fetch + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + if market['contract']: + raise NotSupported(self.id + ' fetchOHLCV() can only fetch ohlcvs for spot markets') + until = self.safe_integer(params, 'until') + untilIsDefined = (until is not None) + sinceIsDefined = (since is not None) + if limit is None: + limit = 500 if (sinceIsDefined and untilIsDefined) else 100 # default 100, max 500, if since and limit defined then fetch all the candles between them unless it exceeds the max of 500 + request: dict = { + 'asset_pair_name': market['id'], + 'period': self.safe_string(self.timeframes, timeframe, timeframe), + 'limit': limit, + } + if sinceIsDefined: + # start = self.parse_to_int(since / 1000) + duration = self.parse_timeframe(timeframe) + endByLimit = self.sum(since, limit * duration * 1000) + if untilIsDefined: + request['time'] = self.iso8601(min(endByLimit, until + 1)) + else: + request['time'] = self.iso8601(endByLimit) + elif untilIsDefined: + request['time'] = self.iso8601(until + 1) + params = self.omit(params, 'until') + response = await self.publicGetAssetPairsAssetPairNameCandles(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "close": "0.021656", + # "high": "0.021658", + # "low": "0.021652", + # "open": "0.021652", + # "time": "2019-11-21T09:30:00Z", + # "volume": "53.08664" + # }, + # { + # "close": "0.021652", + # "high": "0.021656", + # "low": "0.021652", + # "open": "0.021656", + # "time": "2019-11-21T09:29:00Z", + # "volume": "88.39861" + # }, + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_ohlcvs(data, market, timeframe, since, limit) + + def parse_balance(self, response) -> Balances: + result: dict = { + 'info': response, + 'timestamp': None, + 'datetime': None, + } + balances = self.safe_list(response, 'data', []) + for i in range(0, len(balances)): + balance = balances[i] + symbol = self.safe_string(balance, 'asset_symbol') + code = self.safe_currency_code(symbol) + account = self.account() + account['total'] = self.safe_string(balance, 'balance') + account['used'] = self.safe_string(balance, 'locked_balance') + result[code] = account + return self.safe_balance(result) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://open.big.one/docs/fund_accounts.html + https://open.big.one/docs/spot_accounts.html + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + type = self.safe_string(params, 'type', '') + params = self.omit(params, 'type') + response = None + if type == 'funding' or type == 'fund': + response = await self.privateGetFundAccounts(params) + else: + response = await self.privateGetAccounts(params) + # + # { + # "code":0, + # "data":[ + # {"asset_symbol":"NKC","balance":"0","locked_balance":"0"}, + # {"asset_symbol":"UBTC","balance":"0","locked_balance":"0"}, + # {"asset_symbol":"READ","balance":"0","locked_balance":"0"}, + # ], + # } + # + return self.parse_balance(response) + + def parse_type(self, type: str): + types: dict = { + 'STOP_LIMIT': 'limit', + 'STOP_MARKET': 'market', + 'LIMIT': 'limit', + 'MARKET': 'market', + } + return self.safe_string(types, type, type) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # { + # "id": "42154072251", + # "asset_pair_name": "SOL-USDT", + # "price": "20", + # "amount": "0.5", + # "filled_amount": "0", + # "avg_deal_price": "0", + # "side": "ASK", + # "state": "PENDING", + # "created_at": "2023-09-13T03:42:00Z", + # "updated_at": "2023-09-13T03:42:00Z", + # "type": "LIMIT", + # "stop_price": "0", + # "immediate_or_cancel": False, + # "post_only": False, + # "client_order_id": '' + # } + # + id = self.safe_string(order, 'id') + marketId = self.safe_string(order, 'asset_pair_name') + symbol = self.safe_symbol(marketId, market, '-') + timestamp = self.parse8601(self.safe_string(order, 'created_at')) + side = self.safe_string(order, 'side') + if side == 'BID': + side = 'buy' + else: + side = 'sell' + triggerPrice = self.safe_string(order, 'stop_price') + if Precise.string_eq(triggerPrice, '0'): + triggerPrice = None + immediateOrCancel = self.safe_bool(order, 'immediate_or_cancel') + timeInForce = None + if immediateOrCancel: + timeInForce = 'IOC' + type = self.parse_type(self.safe_string(order, 'type')) + price = self.safe_string(order, 'price') + amount = None + filled = None + cost = None + if type == 'market' and side == 'buy': + cost = self.safe_string(order, 'filled_amount') + else: + amount = self.safe_string(order, 'amount') + filled = self.safe_string(order, 'filled_amount') + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': self.safe_string(order, 'client_order_id'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': self.parse8601(self.safe_string(order, 'updated_at')), + 'symbol': symbol, + 'type': type, + 'timeInForce': timeInForce, + 'postOnly': self.safe_bool(order, 'post_only'), + 'side': side, + 'price': price, + 'triggerPrice': triggerPrice, + 'amount': amount, + 'cost': cost, + 'average': self.safe_string(order, 'avg_deal_price'), + 'filled': filled, + 'remaining': None, + 'status': self.parse_order_status(self.safe_string(order, 'state')), + 'fee': None, + 'trades': None, + }, market) + + async def create_market_buy_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market buy order by providing the symbol and cost + + https://open.big.one/docs/spot_orders.html#create-order + + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' createMarketBuyOrderWithCost() supports spot orders only') + params['createMarketBuyOrderRequiresPrice'] = False + return await self.create_order(symbol, 'market', 'buy', cost, None, params) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://open.big.one/docs/spot_orders.html#create-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: the price at which a trigger order is triggered at + :param bool [params.postOnly]: if True, the order will only be posted to the order book and not executed immediately + :param str [params.timeInForce]: "GTC", "IOC", or "PO" + :param float [params.cost]: *spot market buy only* the quote quantity that can be used alternative for the amount + + EXCHANGE SPECIFIC PARAMETERS + :param str [params.operator]: *stop order only* GTE or LTE(default) + :param str [params.client_order_id]: must match ^[a-zA-Z0-9-_]{1,36}$ self regex. client_order_id is unique in 24 hours, If created 24 hours later and the order closed, it will be released and can be reused + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + isBuy = (side == 'buy') + requestSide = 'BID' if isBuy else 'ASK' + uppercaseType = type.upper() + isLimit = uppercaseType == 'LIMIT' + exchangeSpecificParam = self.safe_bool(params, 'post_only', False) + postOnly = None + postOnly, params = self.handle_post_only((uppercaseType == 'MARKET'), exchangeSpecificParam, params) + triggerPrice = self.safe_string_n(params, ['triggerPrice', 'stopPrice', 'stop_price']) + request: dict = { + 'asset_pair_name': market['id'], # asset pair name BTC-USDT, required + 'side': requestSide, # order side one of "ASK"/"BID", required + 'amount': self.amount_to_precision(symbol, amount), # order amount, string, required + # "price": self.price_to_precision(symbol, price), # order price, string, required + # "operator": "GTE", # stop orders only, GTE greater than and equal, LTE less than and equal + # "immediate_or_cancel": False, # limit orders only, must be False when post_only is True + # "post_only": False, # limit orders only, must be False when immediate_or_cancel is True + } + if isLimit or (uppercaseType == 'STOP_LIMIT'): + request['price'] = self.price_to_precision(symbol, price) + if isLimit: + timeInForce = self.safe_string(params, 'timeInForce') + if timeInForce == 'IOC': + request['immediate_or_cancel'] = True + if postOnly: + request['post_only'] = True + request['amount'] = self.amount_to_precision(symbol, amount) + else: + if isBuy: + createMarketBuyOrderRequiresPrice = True + createMarketBuyOrderRequiresPrice, params = self.handle_option_and_params(params, 'createOrder', 'createMarketBuyOrderRequiresPrice', True) + cost = self.safe_number(params, 'cost') + params = self.omit(params, 'cost') + if createMarketBuyOrderRequiresPrice: + if (price is None) and (cost is None): + raise InvalidOrder(self.id + ' createOrder() requires the price argument for market buy orders to calculate the total cost to spend(amount * price), alternatively set the createMarketBuyOrderRequiresPrice option or param to False and pass the cost to spend in the amount argument') + else: + amountString = self.number_to_string(amount) + priceString = self.number_to_string(price) + quoteAmount = self.parse_to_numeric(Precise.string_mul(amountString, priceString)) + costRequest = cost if (cost is not None) else quoteAmount + request['amount'] = self.cost_to_precision(symbol, costRequest) + else: + request['amount'] = self.cost_to_precision(symbol, amount) + else: + request['amount'] = self.amount_to_precision(symbol, amount) + if triggerPrice is not None: + request['stop_price'] = self.price_to_precision(symbol, triggerPrice) + request['operator'] = 'GTE' if isBuy else 'LTE' + if isLimit: + uppercaseType = 'STOP_LIMIT' + elif uppercaseType == 'MARKET': + uppercaseType = 'STOP_MARKET' + request['type'] = uppercaseType + clientOrderId = self.safe_string(params, 'clientOrderId') + if clientOrderId is not None: + request['client_order_id'] = clientOrderId + params = self.omit(params, ['stop_price', 'stopPrice', 'triggerPrice', 'timeInForce', 'clientOrderId']) + response = await self.privatePostOrders(self.extend(request, params)) + # + # { + # "id": 10, + # "asset_pair_name": "EOS-BTC", + # "price": "10.00", + # "amount": "10.00", + # "filled_amount": "9.0", + # "avg_deal_price": "12.0", + # "side": "ASK", + # "state": "FILLED", + # "created_at":"2019-01-29T06:05:56Z", + # "updated_at":"2019-01-29T06:05:56Z" + # } + # + order = self.safe_dict(response, 'data') + return self.parse_order(order, market) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://open.big.one/docs/spot_orders.html#cancel-order + + :param str id: order id + :param str symbol: Not used by bigone cancelOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + request: dict = {'id': id} + response = await self.privatePostOrdersIdCancel(self.extend(request, params)) + # { + # "id": 10, + # "asset_pair_name": "EOS-BTC", + # "price": "10.00", + # "amount": "10.00", + # "filled_amount": "9.0", + # "avg_deal_price": "12.0", + # "side": "ASK", + # "state": "CANCELLED", + # "created_at":"2019-01-29T06:05:56Z", + # "updated_at":"2019-01-29T06:05:56Z" + # } + order = self.safe_dict(response, 'data') + return self.parse_order(order) + + async def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders + + https://open.big.one/docs/spot_orders.html#cancel-all-orders + + :param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'asset_pair_name': market['id'], + } + response = await self.privatePostOrdersCancel(self.extend(request, params)) + # + # { + # "code":0, + # "data": { + # "cancelled":[ + # 58272370, + # 58272377 + # ], + # "failed": [] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + cancelled = self.safe_list(data, 'cancelled', []) + failed = self.safe_list(data, 'failed', []) + result = [] + for i in range(0, len(cancelled)): + orderId = cancelled[i] + result.append(self.safe_order({ + 'info': orderId, + 'id': orderId, + 'status': 'canceled', + })) + for i in range(0, len(failed)): + orderId = failed[i] + result.append(self.safe_order({ + 'info': orderId, + 'id': orderId, + 'status': 'failed', + })) + return result + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://open.big.one/docs/spot_orders.html#get-one-order + + :param str id: the order id + :param str symbol: not used by bigone fetchOrder + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + request: dict = {'id': id} + response = await self.privateGetOrdersId(self.extend(request, params)) + order = self.safe_dict(response, 'data', {}) + return self.parse_order(order) + + async def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://open.big.one/docs/spot_orders.html#get-user-orders-in-one-asset-pair + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrders() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'asset_pair_name': market['id'], + # 'page_token': 'dxzef', # request page after self page token + # 'side': 'ASK', # 'ASK' or 'BID', optional + # 'state': 'FILLED', # 'CANCELLED', 'FILLED', 'PENDING' + # 'limit' 20, # default 20, max 200 + } + if limit is not None: + request['limit'] = limit # default 20, max 200 + response = await self.privateGetOrders(self.extend(request, params)) + # + # { + # "code":0, + # "data": [ + # { + # "id": 10, + # "asset_pair_name": "ETH-BTC", + # "price": "10.00", + # "amount": "10.00", + # "filled_amount": "9.0", + # "avg_deal_price": "12.0", + # "side": "ASK", + # "state": "FILLED", + # "created_at":"2019-01-29T06:05:56Z", + # "updated_at":"2019-01-29T06:05:56Z", + # }, + # ], + # "page_token":"dxzef", + # } + # + orders = self.safe_list(response, 'data', []) + return self.parse_orders(orders, market, since, limit) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://open.big.one/docs/spot_trade.html#trades-of-user + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMyTrades() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'asset_pair_name': market['id'], + # 'page_token': 'dxzef', # request page after self page token + } + if limit is not None: + request['limit'] = limit # default 20, max 200 + response = await self.privateGetTrades(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "id": 10854280, + # "asset_pair_name": "XIN-USDT", + # "price": "70", + # "amount": "1", + # "taker_side": "ASK", + # "maker_order_id": 58284908, + # "taker_order_id": 58284909, + # "maker_fee": "0.0008", + # "taker_fee": "0.07", + # "side": "SELF_TRADING", + # "inserted_at": "2019-04-16T12:00:01Z" + # }, + # { + # "id": 10854263, + # "asset_pair_name": "XIN-USDT", + # "price": "75.7", + # "amount": "12.743149", + # "taker_side": "BID", + # "maker_order_id": null, + # "taker_order_id": 58284888, + # "maker_fee": null, + # "taker_fee": "0.0025486298", + # "side": "BID", + # "inserted_at": "2019-04-15T06:20:57Z" + # } + # ], + # "page_token":"dxfv" + # } + # + trades = self.safe_list(response, 'data', []) + return self.parse_trades(trades, market, since, limit) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'PENDING': 'open', + 'FILLED': 'closed', + 'CANCELLED': 'canceled', + } + return self.safe_string(statuses, status) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://open.big.one/docs/spot_orders.html#get-user-orders-in-one-asset-pair + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + request: dict = { + 'state': 'PENDING', + } + return await self.fetch_orders(symbol, since, limit, self.extend(request, params)) + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://open.big.one/docs/spot_orders.html#get-user-orders-in-one-asset-pair + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + request: dict = { + 'state': 'FILLED', + } + return await self.fetch_orders(symbol, since, limit, self.extend(request, params)) + + def nonce(self): + exchangeTimeCorrection = self.safe_integer(self.options, 'exchangeMillisecondsCorrection', 0) * 1000000 + return self.sum(self.microseconds() * 1000, exchangeTimeCorrection) + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + query = self.omit(params, self.extract_params(path)) + baseUrl = self.implode_hostname(self.urls['api'][api]) + url = baseUrl + '/' + self.implode_params(path, params) + headers = {} + if api == 'public' or api == 'webExchange' or api == 'contractPublic': + if query: + url += '?' + self.urlencode(query) + else: + self.check_required_credentials() + nonce = str(self.nonce()) + request: dict = { + 'type': 'OpenAPIV2', + 'sub': self.apiKey, + 'nonce': nonce, + # 'recv_window': '30', # default 30 + } + token = self.jwt(request, self.encode(self.secret), 'sha256') + headers['Authorization'] = 'Bearer ' + token + if method == 'GET': + if query: + url += '?' + self.urlencode(query) + elif method == 'POST': + headers['Content-Type'] = 'application/json' + body = self.json(query) + headers['User-Agent'] = 'ccxt/' + self.id + '-' + self.version + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://open.big.one/docs/spot_deposit.html#get-deposite-address-of-one-asset-of-user + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'asset_symbol': currency['id'], + } + networkCode, paramsOmitted = self.handle_network_code_and_params(params) + response = await self.privateGetAssetsAssetSymbolAddress(self.extend(request, paramsOmitted)) + # + # the actual response format is not the same documented one + # the data key contains an array in the actual response + # + # { + # "code":0, + # "message":"", + # "data":[ + # { + # "id":5521878, + # "chain":"Bitcoin", + # "value":"1GbmyKoikhpiQVZ1C9sbF17mTyvBjeobVe", + # "memo":"" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + dataLength = len(data) + if dataLength < 1: + raise ExchangeError(self.id + ' fetchDepositAddress() returned empty address response') + chainsIndexedById = self.index_by(data, 'chain') + selectedNetworkId = self.select_network_id_from_raw_networks(code, networkCode, chainsIndexedById) + addressObject = self.safe_dict(chainsIndexedById, selectedNetworkId, {}) + address = self.safe_string(addressObject, 'value') + tag = self.safe_string(addressObject, 'memo') + self.check_address(address) + return { + 'info': response, + 'currency': code, + 'network': self.network_id_to_code(selectedNetworkId), + 'address': address, + 'tag': tag, + } + + def parse_transaction_status(self, status: Str): + statuses: dict = { + # what are other statuses here? + 'WITHHOLD': 'ok', # deposits + 'UNCONFIRMED': 'pending', + 'CONFIRMED': 'ok', # withdrawals + 'COMPLETED': 'ok', + 'PENDING': 'pending', + } + return self.safe_string(statuses, status, status) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fetchDeposits + # + # { + # "amount": "25.0", + # "asset_symbol": "BTS" + # "confirms": 100, + # "id": 5, + # "inserted_at": "2018-02-16T11:39:58.000Z", + # "is_internal": False, + # "kind": "default", + # "memo": "", + # "state": "WITHHOLD", + # "txid": "72e03037d144dae3d32b68b5045462b1049a0755", + # "updated_at": "2018-11-09T10:20:09.000Z", + # } + # + # fetchWithdrawals + # + # { + # "amount": "5", + # "asset_symbol": "ETH", + # "completed_at": "2018-03-15T16:13:45.610463Z", + # "customer_id": "10", + # "id": 10, + # "inserted_at": "2018-03-15T16:13:45.610463Z", + # "is_internal": True, + # "note": "2018-03-15T16:13:45.610463Z", + # "state": "CONFIRMED", + # "target_address": "0x4643bb6b393ac20a6175c713175734a72517c63d6f7" + # "txid": "0x4643bb6b393ac20a6175c713175734a72517c63d6f73a3ca90a15356f2e967da0", + # } + # + # withdraw + # + # { + # "id":1077391, + # "customer_id":1082679, + # "amount":"21.9000000000000000", + # "txid":"", + # "is_internal":false, + # "kind":"on_chain", + # "state":"PENDING", + # "inserted_at":"2020-06-03T00:50:57+00:00", + # "updated_at":"2020-06-03T00:50:57+00:00", + # "memo":"", + # "target_address":"rDYtYT3dBeuw376rvHqoZBKW3UmvguoBAf", + # "fee":"0.1000000000000000", + # "asset_symbol":"XRP" + # } + # + currencyId = self.safe_string(transaction, 'asset_symbol') + code = self.safe_currency_code(currencyId) + id = self.safe_string(transaction, 'id') + amount = self.safe_number(transaction, 'amount') + status = self.parse_transaction_status(self.safe_string(transaction, 'state')) + timestamp = self.parse8601(self.safe_string(transaction, 'inserted_at')) + updated = self.parse8601(self.safe_string_2(transaction, 'updated_at', 'completed_at')) + txid = self.safe_string(transaction, 'txid') + address = self.safe_string(transaction, 'target_address') + tag = self.safe_string(transaction, 'memo') + type = 'withdrawal' if ('customer_id' in transaction) else 'deposit' + internal = self.safe_bool(transaction, 'is_internal') + return { + 'info': transaction, + 'id': id, + 'txid': txid, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': None, + 'addressFrom': None, + 'address': None, + 'addressTo': address, + 'tagFrom': None, + 'tag': tag, + 'tagTo': None, + 'type': type, + 'amount': amount, + 'currency': code, + 'status': status, + 'updated': updated, + 'fee': None, + 'comment': None, + 'internal': internal, + } + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://open.big.one/docs/spot_deposit.html#deposit-of-user + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + request: dict = { + # 'page_token': 'dxzef', # request page after self page token + # 'limit': 50, # optional, default 50 + # 'kind': 'string', # optional - air_drop, big_holder_dividend, default, eosc_to_eos, internal, equally_airdrop, referral_mining, one_holder_dividend, single_customer, snapshotted_airdrop, trade_mining + # 'asset_symbol': 'BTC', # optional + } + currency = None + if code is not None: + currency = self.currency(code) + request['asset_symbol'] = currency['id'] + if limit is not None: + request['limit'] = limit # default 50 + response = await self.privateGetDeposits(self.extend(request, params)) + # + # { + # "code": 0, + # "page_token": "NQ==", + # "data": [ + # { + # "id": 5, + # "amount": "25.0", + # "confirms": 100, + # "txid": "72e03037d144dae3d32b68b5045462b1049a0755", + # "is_internal": False, + # "inserted_at": "2018-02-16T11:39:58.000Z", + # "updated_at": "2018-11-09T10:20:09.000Z", + # "kind": "default", + # "memo": "", + # "state": "WITHHOLD", + # "asset_symbol": "BTS" + # } + # ] + # } + # + deposits = self.safe_list(response, 'data', []) + return self.parse_transactions(deposits, currency, since, limit) + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://open.big.one/docs/spot_withdrawal.html#get-withdrawals-of-user + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + request: dict = { + # 'page_token': 'dxzef', # request page after self page token + # 'limit': 50, # optional, default 50 + # 'kind': 'string', # optional - air_drop, big_holder_dividend, default, eosc_to_eos, internal, equally_airdrop, referral_mining, one_holder_dividend, single_customer, snapshotted_airdrop, trade_mining + # 'asset_symbol': 'BTC', # optional + } + currency = None + if code is not None: + currency = self.currency(code) + request['asset_symbol'] = currency['id'] + if limit is not None: + request['limit'] = limit # default 50 + response = await self.privateGetWithdrawals(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "id": 10, + # "customer_id": "10", + # "asset_symbol": "ETH", + # "amount": "5", + # "state": "CONFIRMED", + # "note": "2018-03-15T16:13:45.610463Z", + # "txid": "0x4643bb6b393ac20a6175c713175734a72517c63d6f73a3ca90a15356f2e967da0", + # "completed_at": "2018-03-15T16:13:45.610463Z", + # "inserted_at": "2018-03-15T16:13:45.610463Z", + # "is_internal": True, + # "target_address": "0x4643bb6b393ac20a6175c713175734a72517c63d6f7" + # } + # ], + # "page_token":"dxvf" + # } + # + withdrawals = self.safe_list(response, 'data', []) + return self.parse_transactions(withdrawals, currency, since, limit) + + async def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://open.big.one/docs/spot_transfer.html#transfer-of-user + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: 'SPOT', 'FUND', or 'CONTRACT' + :param str toAccount: 'SPOT', 'FUND', or 'CONTRACT' + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transfer structure ` + """ + await self.load_markets() + currency = self.currency(code) + accountsByType = self.safe_dict(self.options, 'accountsByType', {}) + fromId = self.safe_string(accountsByType, fromAccount, fromAccount) + toId = self.safe_string(accountsByType, toAccount, toAccount) + guid = self.safe_string(params, 'guid', self.uuid()) + request: dict = { + 'symbol': currency['id'], + 'amount': self.currency_to_precision(code, amount), + 'from': fromId, + 'to': toId, + 'guid': guid, + # 'type': type, # NORMAL, MASTER_TO_SUB, SUB_TO_MASTER, SUB_INTERNAL, default is NORMAL + # 'sub_acccunt': '', # when type is NORMAL, it should be empty, and when type is others it is required + } + response = await self.privatePostTransfer(self.extend(request, params)) + # + # { + # "code": 0, + # "data": null + # } + # + transfer = self.parse_transfer(response, currency) + transferOptions = self.safe_dict(self.options, 'transfer', {}) + fillResponseFromRequest = self.safe_bool(transferOptions, 'fillResponseFromRequest', True) + if fillResponseFromRequest: + transfer['fromAccount'] = fromAccount + transfer['toAccount'] = toAccount + transfer['amount'] = amount + transfer['id'] = guid + return transfer + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + # + # { + # "code": 0, + # "data": null + # } + # + code = self.safe_string(transfer, 'code') + return { + 'info': transfer, + 'id': None, + 'timestamp': None, + 'datetime': None, + 'currency': None, + 'amount': None, + 'fromAccount': None, + 'toAccount': None, + 'status': self.parse_transfer_status(code), + } + + def parse_transfer_status(self, status: Str) -> Str: + statuses: dict = { + '0': 'ok', + } + return self.safe_string(statuses, status, 'failed') + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://open.big.one/docs/spot_withdrawal.html#create-withdrawal-of-user + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'symbol': currency['id'], + 'target_address': address, + 'amount': self.currency_to_precision(code, amount), + } + if tag is not None: + request['memo'] = tag + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + if networkCode is not None: + request['gateway_name'] = self.network_code_to_id(networkCode) + # requires write permission on the wallet + response = await self.privatePostWithdrawals(self.extend(request, params)) + # + # { + # "code":0, + # "message":"", + # "data":{ + # "id":1077391, + # "customer_id":1082679, + # "amount":"21.9000000000000000", + # "txid":"", + # "is_internal":false, + # "kind":"on_chain", + # "state":"PENDING", + # "inserted_at":"2020-06-03T00:50:57+00:00", + # "updated_at":"2020-06-03T00:50:57+00:00", + # "memo":"", + # "target_address":"rDYtYT3dBeuw376rvHqoZBKW3UmvguoBAf", + # "fee":"0.1000000000000000", + # "asset_symbol":"XRP" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_transaction(data, currency) + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None # fallback to default error handler + # + # {"code":10013,"message":"Resource not found"} + # {"code":40004,"message":"invalid jwt"} + # + code = self.safe_string(response, 'code') + message = self.safe_string(response, 'message') + if (code != '0') and (code is not None): + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], code, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + raise ExchangeError(feedback) # unknown message + return None diff --git a/ccxt/async_support/binance.py b/ccxt/async_support/binance.py new file mode 100644 index 0000000..8028359 --- /dev/null +++ b/ccxt/async_support/binance.py @@ -0,0 +1,13487 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.binance import ImplicitAPI +import asyncio +import hashlib +import json +from ccxt.base.types import Any, Balances, BorrowInterest, Conversion, CrossBorrowRate, Currencies, Currency, DepositAddress, Greeks, Int, IsolatedBorrowRate, IsolatedBorrowRates, LedgerEntry, Leverage, Leverages, LeverageTier, LeverageTiers, LongShortRatio, MarginMode, MarginModes, MarginModification, Market, Num, Option, Order, OrderBook, OrderRequest, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, FundingRates, Trade, TradingFeeInterface, TradingFees, Transaction, MarketInterface, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import AccountSuspended +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import OperationRejected +from ccxt.base.errors import MarginModeAlreadySet +from ccxt.base.errors import MarketClosed +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import OrderImmediatelyFillable +from ccxt.base.errors import OrderNotFillable +from ccxt.base.errors import NotSupported +from ccxt.base.errors import OperationFailed +from ccxt.base.errors import DDoSProtection +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import OnMaintenance +from ccxt.base.errors import InvalidNonce +from ccxt.base.errors import RequestTimeout +from ccxt.base.errors import BadResponse +from ccxt.base.decimal_to_precision import TRUNCATE +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class binance(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(binance, self).describe(), { + 'id': 'binance', + 'name': 'Binance', + 'countries': [], # Japan + 'rateLimit': 50, + 'certified': True, + 'pro': True, + # new metainfo2 interface + 'has': { + 'CORS': None, + 'spot': True, + 'margin': True, + 'swap': True, + 'future': True, + 'option': True, + 'addMargin': True, + 'borrowCrossMargin': True, + 'borrowIsolatedMargin': True, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'cancelOrders': True, # contract only + 'closeAllPositions': False, + 'closePosition': False, # exchange specific closePosition parameter for binance createOrder is not synonymous with how CCXT uses closePositions + 'createConvertTrade': True, + 'createDepositAddress': False, + 'createLimitBuyOrder': True, + 'createLimitSellOrder': True, + 'createMarketBuyOrder': True, + 'createMarketBuyOrderWithCost': True, + 'createMarketOrderWithCost': True, + 'createMarketSellOrder': True, + 'createMarketSellOrderWithCost': True, + 'createOrder': True, + 'createOrders': True, + 'createOrderWithTakeProfitAndStopLoss': False, + 'createPostOnlyOrder': True, + 'createReduceOnlyOrder': True, + 'createStopLimitOrder': True, + 'createStopLossOrder': True, + 'createStopMarketOrder': False, + 'createStopOrder': True, + 'createTakeProfitOrder': True, + 'createTrailingPercentOrder': True, + 'createTriggerOrder': True, + 'editOrder': True, + 'editOrders': True, + 'fetchAccounts': None, + 'fetchAllGreeks': True, + 'fetchBalance': True, + 'fetchBidsAsks': True, + 'fetchBorrowInterest': True, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': True, + 'fetchCanceledAndClosedOrders': 'emulated', + 'fetchCanceledOrders': 'emulated', + 'fetchClosedOrder': False, + 'fetchClosedOrders': 'emulated', + 'fetchConvertCurrencies': True, + 'fetchConvertQuote': True, + 'fetchConvertTrade': True, + 'fetchConvertTradeHistory': True, + 'fetchCrossBorrowRate': True, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDeposit': False, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchDepositsWithdrawals': False, + 'fetchDepositWithdrawFee': 'emulated', + 'fetchDepositWithdrawFees': True, + 'fetchFundingHistory': True, + 'fetchFundingInterval': 'emulated', + 'fetchFundingIntervals': True, + 'fetchFundingRate': True, + 'fetchFundingRateHistory': True, + 'fetchFundingRates': True, + 'fetchGreeks': True, + 'fetchIndexOHLCV': True, + 'fetchIsolatedBorrowRate': 'emulated', + 'fetchIsolatedBorrowRates': True, + 'fetchL3OrderBook': False, + 'fetchLastPrices': True, + 'fetchLedger': True, + 'fetchLedgerEntry': True, + 'fetchLeverage': 'emulated', + 'fetchLeverages': True, + 'fetchLeverageTiers': True, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': True, + 'fetchMarginAdjustmentHistory': True, + 'fetchMarginMode': True, + 'fetchMarginModes': True, + 'fetchMarketLeverageTiers': 'emulated', + 'fetchMarkets': True, + 'fetchMarkOHLCV': True, + 'fetchMarkPrice': True, + 'fetchMarkPrices': True, + 'fetchMyLiquidations': True, + 'fetchMySettlementHistory': True, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': True, + 'fetchOpenInterestHistory': True, + 'fetchOpenOrder': True, + 'fetchOpenOrders': True, + 'fetchOption': True, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrderBooks': False, + 'fetchOrders': True, + 'fetchOrderTrades': True, + 'fetchPosition': True, + 'fetchPositionHistory': False, + 'fetchPositionMode': True, + 'fetchPositions': True, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': True, + 'fetchPremiumIndexOHLCV': True, + 'fetchSettlementHistory': True, + 'fetchStatus': True, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': True, + 'fetchTradingFees': True, + 'fetchTradingLimits': 'emulated', + 'fetchTransactionFee': 'emulated', + 'fetchTransactionFees': True, + 'fetchTransactions': False, + 'fetchTransfer': False, + 'fetchTransfers': True, + 'fetchUnderlyingAssets': False, + 'fetchVolatilityHistory': False, + 'fetchWithdrawAddresses': False, + 'fetchWithdrawal': False, + 'fetchWithdrawals': True, + 'fetchWithdrawalWhitelist': False, + 'reduceMargin': True, + 'repayCrossMargin': True, + 'repayIsolatedMargin': True, + 'sandbox': True, + 'setLeverage': True, + 'setMargin': False, + 'setMarginMode': True, + 'setPositionMode': True, + 'signIn': False, + 'transfer': True, + 'withdraw': True, + }, + 'timeframes': { + '1s': '1s', # spot only for now + '1m': '1m', + '3m': '3m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1h', + '2h': '2h', + '4h': '4h', + '6h': '6h', + '8h': '8h', + '12h': '12h', + '1d': '1d', + '3d': '3d', + '1w': '1w', + '1M': '1M', + }, + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/e9419b93-ccb0-46aa-9bff-c883f096274b', + 'test': { + 'dapiPublic': 'https://testnet.binancefuture.com/dapi/v1', + 'dapiPrivate': 'https://testnet.binancefuture.com/dapi/v1', + 'dapiPrivateV2': 'https://testnet.binancefuture.com/dapi/v2', + 'fapiPublic': 'https://testnet.binancefuture.com/fapi/v1', + 'fapiPublicV2': 'https://testnet.binancefuture.com/fapi/v2', + 'fapiPublicV3': 'https://testnet.binancefuture.com/fapi/v3', + 'fapiPrivate': 'https://testnet.binancefuture.com/fapi/v1', + 'fapiPrivateV2': 'https://testnet.binancefuture.com/fapi/v2', + 'fapiPrivateV3': 'https://testnet.binancefuture.com/fapi/v3', + 'public': 'https://testnet.binance.vision/api/v3', + 'private': 'https://testnet.binance.vision/api/v3', + 'v1': 'https://testnet.binance.vision/api/v1', + }, + 'demo': { + 'dapiPublic': 'https://demo-dapi.binance.com/dapi/v1', + 'dapiPrivate': 'https://demo-dapi.binance.com/dapi/v1', + 'dapiPrivateV2': 'https://demo-dapi.binance.com/dapi/v2', + 'fapiPublic': 'https://demo-fapi.binance.com/fapi/v1', + 'fapiPublicV2': 'https://demo-fapi.binance.com/fapi/v2', + 'fapiPublicV3': 'https://demo-fapi.binance.com/fapi/v3', + 'fapiPrivate': 'https://demo-fapi.binance.com/fapi/v1', + 'fapiPrivateV2': 'https://demo-fapi.binance.com/fapi/v2', + 'fapiPrivateV3': 'https://demo-fapi.binance.com/fapi/v3', + 'public': 'https://demo-api.binance.com/api/v3', + 'private': 'https://demo-api.binance.com/api/v3', + 'v1': 'https://demo-api.binance.com/api/v1', + }, + 'api': { + 'sapi': 'https://api.binance.com/sapi/v1', + 'sapiV2': 'https://api.binance.com/sapi/v2', + 'sapiV3': 'https://api.binance.com/sapi/v3', + 'sapiV4': 'https://api.binance.com/sapi/v4', + 'dapiPublic': 'https://dapi.binance.com/dapi/v1', + 'dapiPrivate': 'https://dapi.binance.com/dapi/v1', + 'eapiPublic': 'https://eapi.binance.com/eapi/v1', + 'eapiPrivate': 'https://eapi.binance.com/eapi/v1', + 'dapiPrivateV2': 'https://dapi.binance.com/dapi/v2', + 'dapiData': 'https://dapi.binance.com/futures/data', + 'fapiPublic': 'https://fapi.binance.com/fapi/v1', + 'fapiPublicV2': 'https://fapi.binance.com/fapi/v2', + 'fapiPublicV3': 'https://fapi.binance.com/fapi/v3', + 'fapiPrivate': 'https://fapi.binance.com/fapi/v1', + 'fapiPrivateV2': 'https://fapi.binance.com/fapi/v2', + 'fapiPrivateV3': 'https://fapi.binance.com/fapi/v3', + 'fapiData': 'https://fapi.binance.com/futures/data', + 'public': 'https://api.binance.com/api/v3', + 'private': 'https://api.binance.com/api/v3', + 'v1': 'https://api.binance.com/api/v1', + 'papi': 'https://papi.binance.com/papi/v1', + 'papiV2': 'https://papi.binance.com/papi/v2', + }, + 'www': 'https://www.binance.com', + 'referral': { + 'url': 'https://accounts.binance.com/register?ref=CCXTCOM', + 'discount': 0.1, + }, + 'doc': [ + 'https://developers.binance.com/en', + ], + 'api_management': 'https://www.binance.com/en/usercenter/settings/api-management', + 'fees': 'https://www.binance.com/en/fee/schedule', + }, + 'api': { + # the API structure below will need 3-layer apidefs + 'sapi': { + # IP(sapi) request rate limit of 12 000 per minute + # 1 IP(sapi) => cost = 0.1 =>(1000 / (50 * 0.1)) * 60 = 12000 + # 10 IP(sapi) => cost = 1 + # UID(sapi) request rate limit of 180 000 per minute + # 1 UID(sapi) => cost = 0.006667 =>(1000 / (50 * 0.006667)) * 60 = 180000 + 'get': { + # copy trading + 'copyTrading/futures/userStatus': 2, + 'copyTrading/futures/leadSymbol': 2, + 'system/status': 0.1, + # these endpoints require self.apiKey + 'accountSnapshot': 240, # Weight(IP): 2400 => cost = 0.1 * 2400 = 240 + 'account/info': 0.1, + 'margin/asset': 1, # Weight(IP): 10 => cost = 0.1 * 10 = 1 + 'margin/pair': 1, + 'margin/allAssets': 0.1, + 'margin/allPairs': 0.1, + 'margin/priceIndex': 1, + # these endpoints require self.apiKey + self.secret + 'spot/delist-schedule': 10, + 'asset/assetDividend': 1, + 'asset/dribblet': 0.1, + 'asset/transfer': 0.1, + 'asset/assetDetail': 0.1, + 'asset/tradeFee': 0.1, + 'asset/ledger-transfer/cloud-mining/queryByPage': 4.0002, # Weight(UID): 600 => cost = 0.006667 * 600 = 4.0002 + 'asset/convert-transfer/queryByPage': 0.033335, + 'asset/wallet/balance': 6, # Weight(IP): 60 => cost = 0.1 * 60 = 6 + 'asset/custody/transfer-history': 6, # Weight(IP): 60 => cost = 0.1 * 60 = 6 + 'margin/borrow-repay': 1, + 'margin/loan': 1, + 'margin/repay': 1, + 'margin/account': 1, + 'margin/transfer': 0.1, + 'margin/interestHistory': 0.1, + 'margin/forceLiquidationRec': 0.1, + 'margin/order': 1, + 'margin/openOrders': 1, + 'margin/allOrders': 20, # Weight(IP): 200 => cost = 0.1 * 200 = 20 + 'margin/myTrades': 1, + 'margin/maxBorrowable': 5, # Weight(IP): 50 => cost = 0.1 * 50 = 5 + 'margin/maxTransferable': 5, + 'margin/tradeCoeff': 1, + 'margin/isolated/transfer': 0.1, + 'margin/isolated/account': 1, + 'margin/isolated/pair': 1, + 'margin/isolated/allPairs': 1, + 'margin/isolated/accountLimit': 0.1, + 'margin/interestRateHistory': 0.1, + 'margin/orderList': 1, + 'margin/allOrderList': 20, # Weight(IP): 200 => cost = 0.1 * 200 = 20 + 'margin/openOrderList': 1, + 'margin/crossMarginData': {'cost': 0.1, 'noCoin': 0.5}, + 'margin/isolatedMarginData': {'cost': 0.1, 'noCoin': 1}, + 'margin/isolatedMarginTier': 0.1, + 'margin/rateLimit/order': 2, + 'margin/dribblet': 0.1, + 'margin/dust': 20.001, # Weight(UID): 3000 => cost = 0.006667 * 3000 = 20 + 'margin/crossMarginCollateralRatio': 10, + 'margin/exchange-small-liability': 0.6667, + 'margin/exchange-small-liability-history': 0.6667, + 'margin/next-hourly-interest-rate': 0.6667, + 'margin/capital-flow': 10, # Weight(IP): 100 => cost = 0.1 * 100 = 10 + 'margin/delist-schedule': 10, # Weight(IP): 100 => cost = 0.1 * 100 = 10 + 'margin/available-inventory': 0.3334, # Weight(UID): 50 => cost = 0.006667 * 50 = 0.3334 + 'margin/leverageBracket': 0.1, # Weight(IP): 1 => cost = 0.1 * 1 = 0.1 + 'loan/vip/loanable/data': 40, # Weight(IP): 400 => cost = 0.1 * 400 = 40 + 'loan/vip/collateral/data': 40, # Weight(IP): 400 => cost = 0.1 * 400 = 40 + 'loan/vip/request/data': 2.6668, # Weight(UID): 400 => cost = 0.006667 * 400 = 2.6668 + 'loan/vip/request/interestRate': 2.6668, # Weight(UID): 400 => cost = 0.006667 * 400 = 2.6668 + 'loan/income': 40.002, # Weight(UID): 6000 => cost = 0.006667 * 6000 = 40.002 + 'loan/ongoing/orders': 40, # Weight(IP): 400 => cost = 0.1 * 400 = 40 + 'loan/ltv/adjustment/history': 40, # Weight(IP): 400 => cost = 0.1 * 400 = 40 + 'loan/borrow/history': 40, # Weight(IP): 400 => cost = 0.1 * 400 = 40 + 'loan/repay/history': 40, # Weight(IP): 400 => cost = 0.1 * 400 = 40 + 'loan/loanable/data': 40, # Weight(IP): 400 => cost = 0.1 * 400 = 40 + 'loan/collateral/data': 40, # Weight(IP): 400 => cost = 0.1 * 400 = 40 + 'loan/repay/collateral/rate': 600, # Weight(IP): 6000 => cost = 0.1 * 6000 = 600 + 'loan/flexible/ongoing/orders': 30, # TODO: Deprecating at 2024-04-24 03:00(UTC) + 'loan/flexible/borrow/history': 40, # Weight(IP): 400 => cost = 0.1 * 400 = 40, check flexible rate loans order history before 2024-02-27 08:00(UTC) + 'loan/flexible/repay/history': 40, # Weight(IP): 400 => cost = 0.1 * 400 = 40, check flexible rate loans order history before 2024-02-27 08:00(UTC) + 'loan/flexible/ltv/adjustment/history': 40, # Weight(IP): 400 => cost = 0.1 * 400 = 40, check flexible rate loans order history before 2024-02-27 08:00(UTC) + 'loan/vip/ongoing/orders': 40, # Weight(IP): 400 => cost = 0.1 * 400 = 40 + 'loan/vip/repay/history': 40, # Weight(IP): 400 => cost = 0.1 * 400 = 40 + 'loan/vip/collateral/account': 600, # Weight(IP): 6000 => cost = 0.1 * 6000 = 600 + 'fiat/orders': 600.03, # Weight(UID): 90000 => cost = 0.006667 * 90000 = 600.03 + 'fiat/payments': 0.1, + 'futures/transfer': 1, + 'futures/histDataLink': 0.1, # Weight(IP): 1 => cost = 0.1 * 1 = 0.1 + 'rebate/taxQuery': 80.004, # Weight(UID): 12000 => cost = 0.006667 * 12000 = 80.004 + 'capital/config/getall': 1, # get networks for withdrawing USDT ERC20 vs USDT Omni + 'capital/deposit/address': 1, + 'capital/deposit/address/list': 1, + 'capital/deposit/hisrec': 0.1, + 'capital/deposit/subAddress': 0.1, + 'capital/deposit/subHisrec': 0.1, + 'capital/withdraw/history': 2, # Weight(UID): 18000 + (Additional: 10 requests per second => cost = ( 1000 / rateLimit ) / 10 = 2 + 'capital/withdraw/address/list': 10, + 'capital/contract/convertible-coins': 4.0002, # Weight(UID): 600 => cost = 0.006667 * 600 = 4.0002 + 'convert/tradeFlow': 20.001, # Weight(UID): 3000 => cost = 0.006667 * 3000 = 20.001 + 'convert/exchangeInfo': 50, + 'convert/assetInfo': 10, + 'convert/orderStatus': 0.6667, + 'convert/limit/queryOpenOrders': 20.001, # Weight(UID): 3000 => cost = 0.006667 * 3000 = 20.001 + 'account/status': 0.1, + 'account/apiTradingStatus': 0.1, + 'account/apiRestrictions/ipRestriction': 0.1, + 'bnbBurn': 0.1, + 'sub-account/futures/account': 1, + 'sub-account/futures/accountSummary': 0.1, + 'sub-account/futures/positionRisk': 1, + 'sub-account/futures/internalTransfer': 0.1, + 'sub-account/list': 0.1, + 'sub-account/margin/account': 1, + 'sub-account/margin/accountSummary': 1, + 'sub-account/spotSummary': 0.1, + 'sub-account/status': 1, + 'sub-account/sub/transfer/history': 0.1, + 'sub-account/transfer/subUserHistory': 0.1, + 'sub-account/universalTransfer': 0.1, + 'sub-account/apiRestrictions/ipRestriction/thirdPartyList': 1, + 'sub-account/transaction-statistics': 0.40002, # Weight(UID): 60 => cost = 0.006667 * 60 = 0.40002 + 'sub-account/subAccountApi/ipRestriction': 20.001, # Weight(UID): 3000 => cost = 0.006667 * 3000 = 20.001 + 'managed-subaccount/asset': 0.1, + 'managed-subaccount/accountSnapshot': 240, + 'managed-subaccount/queryTransLogForInvestor': 0.1, + 'managed-subaccount/queryTransLogForTradeParent': 0.40002, # Weight(UID): 60 => cost = 0.006667 * 60 = 0.40002 + 'managed-subaccount/fetch-future-asset': 0.40002, # Weight(UID): 60 => cost = 0.006667 * 60 = 0.40002 + 'managed-subaccount/marginAsset': 0.1, + 'managed-subaccount/info': 0.40002, # Weight(UID): 60 => cost = 0.006667 * 60 = 0.40002 + 'managed-subaccount/deposit/address': 0.006667, # Weight(UID): 1 => cost = 0.006667 * 1 = 0.006667 + 'managed-subaccount/query-trans-log': 0.40002, + # lending endpoints + 'lending/daily/product/list': 0.1, + 'lending/daily/userLeftQuota': 0.1, + 'lending/daily/userRedemptionQuota': 0.1, + 'lending/daily/token/position': 0.1, + 'lending/union/account': 0.1, + 'lending/union/purchaseRecord': 0.1, + 'lending/union/redemptionRecord': 0.1, + 'lending/union/interestHistory': 0.1, + 'lending/project/list': 0.1, + 'lending/project/position/list': 0.1, + # eth-staking + 'eth-staking/eth/history/stakingHistory': 15, # Weight(IP): 150 => cost = 0.1 * 150 = 15 + 'eth-staking/eth/history/redemptionHistory': 15, # Weight(IP): 150 => cost = 0.1 * 150 = 15 + 'eth-staking/eth/history/rewardsHistory': 15, # Weight(IP): 150 => cost = 0.1 * 150 = 15 + 'eth-staking/eth/quota': 15, # Weight(IP): 150 => cost = 0.1 * 150 = 15 + 'eth-staking/eth/history/rateHistory': 15, # Weight(IP): 150 => cost = 0.1 * 150 = 15 + 'eth-staking/account': 15, # Weight(IP): 150 => cost = 0.1 * 150 = 15 + 'eth-staking/wbeth/history/wrapHistory': 15, # Weight(IP): 150 => cost = 0.1 * 150 = 15 + 'eth-staking/wbeth/history/unwrapHistory': 15, # Weight(IP): 150 => cost = 0.1 * 150 = 15 + 'eth-staking/eth/history/wbethRewardsHistory': 15, # Weight(IP): 150 => cost = 0.1 * 150 = 15 + 'sol-staking/sol/history/stakingHistory': 15, + 'sol-staking/sol/history/redemptionHistory': 15, + 'sol-staking/sol/history/bnsolRewardsHistory': 15, + 'sol-staking/sol/history/rateHistory': 15, + 'sol-staking/account': 15, + 'sol-staking/sol/quota': 15, + # mining endpoints + 'mining/pub/algoList': 0.1, + 'mining/pub/coinList': 0.1, + 'mining/worker/detail': 0.5, # Weight(IP): 5 => cost = 0.1 * 5 = 0.5 + 'mining/worker/list': 0.5, + 'mining/payment/list': 0.5, + 'mining/statistics/user/status': 0.5, + 'mining/statistics/user/list': 0.5, + 'mining/payment/uid': 0.5, + # liquid swap endpoints + 'bswap/pools': 0.1, + 'bswap/liquidity': {'cost': 0.1, 'noPoolId': 1}, + 'bswap/liquidityOps': 20.001, # Weight(UID): 3000 => cost = 0.006667 * 3000 = 20.001 + 'bswap/quote': 1.00005, # Weight(UID): 150 => cost = 0.006667 * 150 = 1.00005 + 'bswap/swap': 20.001, # Weight(UID): 3000 => cost = 0.006667 * 3000 = 20.001 + 'bswap/poolConfigure': 1.00005, # Weight(UID): 150 => cost = 0.006667 * 150 = 1.00005 + 'bswap/addLiquidityPreview': 1.00005, # Weight(UID): 150 => cost = 0.006667 * 150 = 1.00005 + 'bswap/removeLiquidityPreview': 1.00005, # Weight(UID): 150 => cost = 0.006667 * 150 = 1.00005 + 'bswap/unclaimedRewards': 6.667, # Weight(UID): 1000 => cost = 0.006667 * 1000 = 6.667 + 'bswap/claimedHistory': 6.667, # Weight(UID): 1000 => cost = 0.006667 * 1000 = 6.667 + # leveraged token endpoints + 'blvt/tokenInfo': 0.1, + 'blvt/subscribe/record': 0.1, + 'blvt/redeem/record': 0.1, + 'blvt/userLimit': 0.1, + # broker api TODO(NOT IN DOCS) + 'apiReferral/ifNewUser': 1, + 'apiReferral/customization': 1, + 'apiReferral/userCustomization': 1, + 'apiReferral/rebate/recentRecord': 1, + 'apiReferral/rebate/historicalRecord': 1, + 'apiReferral/kickback/recentRecord': 1, + 'apiReferral/kickback/historicalRecord': 1, + # brokerage API TODO https://binance-docs.github.io/Brokerage-API/General/ does not state ratelimits + 'broker/subAccountApi': 1, + 'broker/subAccount': 1, + 'broker/subAccountApi/commission/futures': 1, + 'broker/subAccountApi/commission/coinFutures': 1, + 'broker/info': 1, + 'broker/transfer': 1, + 'broker/transfer/futures': 1, + 'broker/rebate/recentRecord': 1, + 'broker/rebate/historicalRecord': 1, + 'broker/subAccount/bnbBurn/status': 1, + 'broker/subAccount/depositHist': 1, + 'broker/subAccount/spotSummary': 1, + 'broker/subAccount/marginSummary': 1, + 'broker/subAccount/futuresSummary': 1, + 'broker/rebate/futures/recentRecord': 1, + 'broker/subAccountApi/ipRestriction': 1, + 'broker/universalTransfer': 1, + # v2 not supported yet + # GET /sapi/v2/broker/subAccount/futuresSummary + 'account/apiRestrictions': 0.1, + # c2c / p2p + 'c2c/orderMatch/listUserOrderHistory': 0.1, + # nft endpoints + 'nft/history/transactions': 20.001, # Weight(UID): 3000 => cost = 0.006667 * 3000 = 20.001 + 'nft/history/deposit': 20.001, + 'nft/history/withdraw': 20.001, + 'nft/user/getAsset': 20.001, + 'pay/transactions': 20.001, + 'giftcard/verify': 0.1, + 'giftcard/cryptography/rsa-public-key': 0.1, + 'giftcard/buyCode/token-limit': 0.1, + 'algo/spot/openOrders': 0.1, + 'algo/spot/historicalOrders': 0.1, + 'algo/spot/subOrders': 0.1, + 'algo/futures/openOrders': 0.1, + 'algo/futures/historicalOrders': 0.1, + 'algo/futures/subOrders': 0.1, + 'portfolio/account': 0.1, + 'portfolio/collateralRate': 5, + 'portfolio/pmLoan': 3.3335, + 'portfolio/interest-history': 0.6667, + 'portfolio/asset-index-price': 0.1, + 'portfolio/repay-futures-switch': 3, # Weight(IP): 30 => cost = 0.1 * 30 = 3 + 'portfolio/margin-asset-leverage': 5, # Weight(IP): 50 => cost = 0.1 * 50 = 5 + 'portfolio/balance': 2, + 'portfolio/negative-balance-exchange-record': 2, + 'portfolio/pmloan-history': 5, + 'portfolio/earn-asset-balance': 150, # Weight(IP): 1500 => cost = 0.1 * 1500 = 150 + # staking + 'staking/productList': 0.1, + 'staking/position': 0.1, + 'staking/stakingRecord': 0.1, + 'staking/personalLeftQuota': 0.1, + 'lending/auto-invest/target-asset/list': 0.1, # Weight(IP): 1 => cost = 0.1 * 1 = 0.1 + 'lending/auto-invest/target-asset/roi/list': 0.1, + 'lending/auto-invest/all/asset': 0.1, + 'lending/auto-invest/source-asset/list': 0.1, + 'lending/auto-invest/plan/list': 0.1, + 'lending/auto-invest/plan/id': 0.1, + 'lending/auto-invest/history/list': 0.1, + 'lending/auto-invest/index/info': 0.1, # Weight(IP): 1 => cost = 0.1 * 1 = 0.1 + 'lending/auto-invest/index/user-summary': 0.1, # Weight(IP): 1 => cost = 0.1 * 1 = 0.1 + 'lending/auto-invest/one-off/status': 0.1, # Weight(IP): 1 => cost = 0.1 * 1 = 0.1 + 'lending/auto-invest/redeem/history': 0.1, # Weight(IP): 1 => cost = 0.1 * 1 = 0.1 + 'lending/auto-invest/rebalance/history': 0.1, # Weight(IP): 1 => cost = 0.1 * 1 = 0.1 + # simple earn + 'simple-earn/flexible/list': 15, + 'simple-earn/locked/list': 15, + 'simple-earn/flexible/personalLeftQuota': 15, + 'simple-earn/locked/personalLeftQuota': 15, + 'simple-earn/flexible/subscriptionPreview': 15, + 'simple-earn/locked/subscriptionPreview': 15, + 'simple-earn/flexible/history/rateHistory': 15, + 'simple-earn/flexible/position': 15, + 'simple-earn/locked/position': 15, + 'simple-earn/account': 15, + 'simple-earn/flexible/history/subscriptionRecord': 15, + 'simple-earn/locked/history/subscriptionRecord': 15, + 'simple-earn/flexible/history/redemptionRecord': 15, + 'simple-earn/locked/history/redemptionRecord': 15, + 'simple-earn/flexible/history/rewardsRecord': 15, + 'simple-earn/locked/history/rewardsRecord': 15, + 'simple-earn/flexible/history/collateralRecord': 0.1, + # Convert + 'dci/product/list': 0.1, + 'dci/product/positions': 0.1, + 'dci/product/accounts': 0.1, + }, + 'post': { + 'asset/dust': 0.06667, # Weight(UID): 10 => cost = 0.006667 * 10 = 0.06667 + 'asset/dust-btc': 0.1, + 'asset/transfer': 6.0003, # Weight(UID): 900 => cost = 0.006667 * 900 = 6.0003 + 'asset/get-funding-asset': 0.1, + 'asset/convert-transfer': 0.033335, + 'account/disableFastWithdrawSwitch': 0.1, + 'account/enableFastWithdrawSwitch': 0.1, + # 'account/apiRestrictions/ipRestriction': 1, discontinued + # 'account/apiRestrictions/ipRestriction/ipList': 1, discontinued + 'capital/withdraw/apply': 4.0002, # Weight(UID): 600 => cost = 0.006667 * 600 = 4.0002 + 'capital/contract/convertible-coins': 4.0002, + 'capital/deposit/credit-apply': 0.1, # Weight(IP): 1 => cost = 0.1 * 1 = 0.1 + 'margin/borrow-repay': 20.001, + 'margin/transfer': 4.0002, + 'margin/loan': 20.001, # Weight(UID): 3000 => cost = 0.006667 * 3000 = 20.001 + 'margin/repay': 20.001, + 'margin/order': 0.040002, # Weight(UID): 6 => cost = 0.006667 * 6 = 0.040002 + 'margin/order/oco': 0.040002, + 'margin/dust': 20.001, # Weight(UID): 3000 => cost = 0.006667 * 3000 = 20.001 + 'margin/exchange-small-liability': 20.001, + # 'margin/isolated/create': 1, discontinued + 'margin/isolated/transfer': 4.0002, # Weight(UID): 600 => cost = 0.006667 * 600 = 4.0002 + 'margin/isolated/account': 2.0001, # Weight(UID): 300 => cost = 0.006667 * 300 = 2.0001 + 'margin/max-leverage': 300, # Weight(IP): 3000 => cost = 0.1 * 3000 = 300 + 'bnbBurn': 0.1, + 'sub-account/virtualSubAccount': 0.1, + 'sub-account/margin/transfer': 4.0002, # Weight(UID): 600 => cost = 0.006667 * 600 = 4.0002 + 'sub-account/margin/enable': 0.1, + 'sub-account/futures/enable': 0.1, + 'sub-account/futures/transfer': 0.1, + 'sub-account/futures/internalTransfer': 0.1, + 'sub-account/transfer/subToSub': 0.1, + 'sub-account/transfer/subToMaster': 0.1, + 'sub-account/universalTransfer': 0.1, + 'sub-account/options/enable': 0.1, + 'managed-subaccount/deposit': 0.1, + 'managed-subaccount/withdraw': 0.1, + 'userDataStream': 0.1, + 'userDataStream/isolated': 0.1, + 'futures/transfer': 0.1, + # lending + 'lending/customizedFixed/purchase': 0.1, + 'lending/daily/purchase': 0.1, + 'lending/daily/redeem': 0.1, + # liquid swap endpoints + 'bswap/liquidityAdd': 60, # Weight(UID): 1000 + (Additional: 1 request every 3 seconds = 0.333 requests per second) => cost = ( 1000 / rateLimit ) / 0.333 = 60.0000006 + 'bswap/liquidityRemove': 60, # Weight(UID): 1000 + (Additional: 1 request every three seconds) + 'bswap/swap': 60, # Weight(UID): 1000 + (Additional: 1 request every three seconds) + 'bswap/claimRewards': 6.667, # Weight(UID): 1000 => cost = 0.006667 * 1000 = 6.667 + # leveraged token endpoints + 'blvt/subscribe': 0.1, + 'blvt/redeem': 0.1, + # brokerage API TODO: NO MENTION OF RATELIMITS IN BROKERAGE DOCS + 'apiReferral/customization': 1, + 'apiReferral/userCustomization': 1, + 'apiReferral/rebate/historicalRecord': 1, + 'apiReferral/kickback/historicalRecord': 1, + 'broker/subAccount': 1, + 'broker/subAccount/margin': 1, + 'broker/subAccount/futures': 1, + 'broker/subAccountApi': 1, + 'broker/subAccountApi/permission': 1, + 'broker/subAccountApi/commission': 1, + 'broker/subAccountApi/commission/futures': 1, + 'broker/subAccountApi/commission/coinFutures': 1, + 'broker/transfer': 1, + 'broker/transfer/futures': 1, + 'broker/rebate/historicalRecord': 1, + 'broker/subAccount/bnbBurn/spot': 1, + 'broker/subAccount/bnbBurn/marginInterest': 1, + 'broker/subAccount/blvt': 1, + 'broker/subAccountApi/ipRestriction': 1, + 'broker/subAccountApi/ipRestriction/ipList': 1, + 'broker/universalTransfer': 1, + 'broker/subAccountApi/permission/universalTransfer': 1, + 'broker/subAccountApi/permission/vanillaOptions': 1, + # + 'giftcard/createCode': 0.1, + 'giftcard/redeemCode': 0.1, + 'giftcard/buyCode': 0.1, + 'algo/spot/newOrderTwap': 20.001, + 'algo/futures/newOrderVp': 20.001, + 'algo/futures/newOrderTwap': 20.001, + # staking + 'staking/purchase': 0.1, + 'staking/redeem': 0.1, + 'staking/setAutoStaking': 0.1, + # eth-staking + 'eth-staking/eth/stake': 15, # Weight(IP): 150 => cost = 0.1 * 150 = 15 + 'eth-staking/eth/redeem': 15, # Weight(IP): 150 => cost = 0.1 * 150 = 15 + 'eth-staking/wbeth/wrap': 15, # Weight(IP): 150 => cost = 0.1 * 150 = 15 + 'sol-staking/sol/stake': 15, + 'sol-staking/sol/redeem': 15, + # mining endpoints + 'mining/hash-transfer/config': 0.5, # Weight(IP): 5 => cost = 0.1 * 5 = 0.5 + 'mining/hash-transfer/config/cancel': 0.5, # Weight(IP): 5 => cost = 0.1 * 5 = 0.5 + 'portfolio/repay': 20.001, + 'loan/vip/renew': 40.002, # Weight(UID): 6000 => cost = 0.006667 * 6000 = 40.002 + 'loan/vip/borrow': 40.002, + 'loan/borrow': 40.002, + 'loan/repay': 40.002, + 'loan/adjust/ltv': 40.002, + 'loan/customize/margin_call': 40.002, + 'loan/flexible/repay': 40.002, # TODO: Deprecating at 2024-04-24 03:00(UTC) + 'loan/flexible/adjust/ltv': 40.002, # TODO: Deprecating at 2024-04-24 03:00(UTC) + 'loan/vip/repay': 40.002, + 'convert/getQuote': 1.3334, # Weight(UID): 200 => cost = 0.006667 * 200 = 1.3334 + 'convert/acceptQuote': 3.3335, # Weight(UID): 500 => cost = 0.006667 * 500 = 3.3335 + 'convert/limit/placeOrder': 3.3335, # Weight(UID): 500 => cost = 0.006667 * 500 = 3.3335 + 'convert/limit/cancelOrder': 1.3334, # Weight(UID): 200 => cost = 0.006667 * 200 = 1.3334 + 'portfolio/auto-collection': 150, # Weight(IP): 1500 => cost = 0.1 * 1500 = 150 + 'portfolio/asset-collection': 6, # Weight(IP): 60 => cost = 0.1 * 60 = 6 + 'portfolio/bnb-transfer': 150, # Weight(IP): 1500 => cost = 0.1 * 1500 = 150 + 'portfolio/repay-futures-switch': 150, # Weight(IP): 1500 => cost = 0.1 * 1500 = 150 + 'portfolio/repay-futures-negative-balance': 150, # Weight(IP): 1500 => cost = 0.1 * 1500 = 150 + 'portfolio/mint': 20, + 'portfolio/redeem': 20, + 'portfolio/earn-asset-transfer': 150, # Weight(IP): 1500 => cost = 0.1 * 1500 = 150 + 'lending/auto-invest/plan/add': 0.1, # Weight(IP): 1 => cost = 0.1 * 1 = 0.1 + 'lending/auto-invest/plan/edit': 0.1, # Weight(IP): 1 => cost = 0.1 * 1 = 0.1 + 'lending/auto-invest/plan/edit-status': 0.1, # Weight(IP): 1 => cost = 0.1 * 1 = 0.1 + 'lending/auto-invest/one-off': 0.1, # Weight(IP): 1 => cost = 0.1 * 1 = 0.1 + 'lending/auto-invest/redeem': 0.1, # Weight(IP): 1 => cost = 0.1 * 1 = 0.1 + # simple earn + 'simple-earn/flexible/subscribe': 0.1, + 'simple-earn/locked/subscribe': 0.1, + 'simple-earn/flexible/redeem': 0.1, + 'simple-earn/locked/redeem': 0.1, + 'simple-earn/flexible/setAutoSubscribe': 15, + 'simple-earn/locked/setAutoSubscribe': 15, + 'simple-earn/locked/setRedeemOption': 5, + # convert + 'dci/product/subscribe': 0.1, + 'dci/product/auto_compound/edit': 0.1, + }, + 'put': { + 'userDataStream': 0.1, + 'userDataStream/isolated': 0.1, + }, + 'delete': { + # 'account/apiRestrictions/ipRestriction/ipList': 1, discontinued + 'margin/openOrders': 0.1, + 'margin/order': 0.006667, # Weight(UID): 1 => cost = 0.006667 + 'margin/orderList': 0.006667, + 'margin/isolated/account': 2.0001, # Weight(UID): 300 => cost = 0.006667 * 300 = 2.0001 + 'userDataStream': 0.1, + 'userDataStream/isolated': 0.1, + # brokerage API TODO NO MENTION OF RATELIMIT IN BROKERAGE DOCS + 'broker/subAccountApi': 1, + 'broker/subAccountApi/ipRestriction/ipList': 1, + 'algo/spot/order': 0.1, + 'algo/futures/order': 0.1, + 'sub-account/subAccountApi/ipRestriction/ipList': 20.001, # Weight(UID): 3000 => cost = 0.006667 * 3000 = 20.001 + }, + }, + 'sapiV2': { + 'get': { + 'eth-staking/account': 15, # Weight(IP): 150 => cost = 0.1 * 150 = 15 + 'sub-account/futures/account': 0.1, + 'sub-account/futures/accountSummary': 1, + 'sub-account/futures/positionRisk': 0.1, + 'loan/flexible/ongoing/orders': 30, # Weight(IP): 300 => cost = 0.1 * 300 = 30 + 'loan/flexible/borrow/history': 40, # Weight(IP): 400 => cost = 0.1 * 400 = 40 + 'loan/flexible/repay/history': 40, # Weight(IP): 400 => cost = 0.1 * 400 = 40 + 'loan/flexible/ltv/adjustment/history': 40, # Weight(IP): 400 => cost = 0.1 * 400 = 40 + 'loan/flexible/loanable/data': 40, # Weight(IP): 400 => cost = 0.1 * 400 = 40 + 'loan/flexible/collateral/data': 40, # Weight(IP): 400 => cost = 0.1 * 400 = 40 + 'portfolio/account': 2, + }, + 'post': { + 'eth-staking/eth/stake': 15, # Weight(IP): 150 => cost = 0.1 * 150 = 15 + 'sub-account/subAccountApi/ipRestriction': 20.001, # Weight(UID): 3000 => cost = 0.006667 * 3000 = 20.001 + 'loan/flexible/borrow': 40.002, # Weight(UID): 6000 => cost = 0.006667 * 6000 = 40.002 + 'loan/flexible/repay': 40.002, # Weight(UID): 6000 => cost = 0.006667 * 6000 = 40.002 + 'loan/flexible/adjust/ltv': 40.002, # Weight(UID): 6000 => cost = 0.006667 * 6000 = 40.002 + }, + }, + 'sapiV3': { + 'get': { + 'sub-account/assets': 0.40002, # Weight(UID): 60 => cost = 0.006667 * 60 = 0.40002 + }, + 'post': { + 'asset/getUserAsset': 0.5, + }, + }, + 'sapiV4': { + 'get': { + 'sub-account/assets': 0.40002, # Weight(UID): 60 => cost = 0.006667 * 60 = 0.40002 + }, + }, + 'dapiPublic': { + 'get': { + 'ping': 1, + 'time': 1, + 'exchangeInfo': 1, + 'depth': {'cost': 2, 'byLimit': [[50, 2], [100, 5], [500, 10], [1000, 20]]}, + 'trades': 5, + 'historicalTrades': 20, + 'aggTrades': 20, + 'premiumIndex': 10, + 'fundingRate': 1, + 'klines': {'cost': 1, 'byLimit': [[99, 1], [499, 2], [1000, 5], [10000, 10]]}, + 'continuousKlines': {'cost': 1, 'byLimit': [[99, 1], [499, 2], [1000, 5], [10000, 10]]}, + 'indexPriceKlines': {'cost': 1, 'byLimit': [[99, 1], [499, 2], [1000, 5], [10000, 10]]}, + 'markPriceKlines': {'cost': 1, 'byLimit': [[99, 1], [499, 2], [1000, 5], [10000, 10]]}, + 'premiumIndexKlines': {'cost': 1, 'byLimit': [[99, 1], [499, 2], [1000, 5], [10000, 10]]}, + 'ticker/24hr': {'cost': 1, 'noSymbol': 40}, + 'ticker/price': {'cost': 1, 'noSymbol': 2}, + 'ticker/bookTicker': {'cost': 2, 'noSymbol': 5}, + 'constituents': 2, + 'openInterest': 1, + 'fundingInfo': 1, + }, + }, + 'dapiData': { + 'get': { + 'delivery-price': 1, + 'openInterestHist': 1, + 'topLongShortAccountRatio': 1, + 'topLongShortPositionRatio': 1, + 'globalLongShortAccountRatio': 1, + 'takerBuySellVol': 1, + 'basis': 1, + }, + }, + 'dapiPrivate': { + 'get': { + 'positionSide/dual': 30, + 'orderAmendment': 1, + 'order': 1, + 'openOrder': 1, + 'openOrders': {'cost': 1, 'noSymbol': 5}, + 'allOrders': {'cost': 20, 'noSymbol': 40}, + 'balance': 1, + 'account': 5, + 'positionMargin/history': 1, + 'positionRisk': 1, + 'userTrades': {'cost': 20, 'noSymbol': 40}, + 'income': 20, + 'leverageBracket': 1, + 'forceOrders': {'cost': 20, 'noSymbol': 50}, + 'adlQuantile': 5, + 'commissionRate': 20, + 'income/asyn': 5, + 'income/asyn/id': 5, + 'trade/asyn': 0.5, + 'trade/asyn/id': 0.5, + 'order/asyn': 0.5, + 'order/asyn/id': 0.5, + 'pmExchangeInfo': 0.5, # Weight(IP): 5 => cost = 0.1 * 5 = 0.5 + 'pmAccountInfo': 0.5, # Weight(IP): 5 => cost = 0.1 * 5 = 0.5 + }, + 'post': { + 'positionSide/dual': 1, + 'order': 4, + 'batchOrders': 5, + 'countdownCancelAll': 10, + 'leverage': 1, + 'marginType': 1, + 'positionMargin': 1, + 'listenKey': 1, + }, + 'put': { + 'listenKey': 1, + 'order': 1, + 'batchOrders': 5, + }, + 'delete': { + 'order': 1, + 'allOpenOrders': 1, + 'batchOrders': 5, + 'listenKey': 1, + }, + }, + 'dapiPrivateV2': { + 'get': { + 'leverageBracket': 1, + }, + }, + 'fapiPublic': { + 'get': { + 'ping': 1, + 'time': 1, + 'exchangeInfo': 1, + 'depth': {'cost': 2, 'byLimit': [[50, 2], [100, 5], [500, 10], [1000, 20]]}, + 'trades': 5, + 'historicalTrades': 20, + 'aggTrades': 20, + 'klines': {'cost': 1, 'byLimit': [[99, 1], [499, 2], [1000, 5], [10000, 10]]}, + 'continuousKlines': {'cost': 1, 'byLimit': [[99, 1], [499, 2], [1000, 5], [10000, 10]]}, + 'markPriceKlines': {'cost': 1, 'byLimit': [[99, 1], [499, 2], [1000, 5], [10000, 10]]}, + 'indexPriceKlines': {'cost': 1, 'byLimit': [[99, 1], [499, 2], [1000, 5], [10000, 10]]}, + 'premiumIndexKlines': {'cost': 1, 'byLimit': [[99, 1], [499, 2], [1000, 5], [10000, 10]]}, + 'fundingRate': 1, + 'fundingInfo': 1, + 'premiumIndex': 1, + 'ticker/24hr': {'cost': 1, 'noSymbol': 40}, + 'ticker/price': {'cost': 1, 'noSymbol': 2}, + 'ticker/bookTicker': {'cost': 1, 'noSymbol': 2}, + 'openInterest': 1, + 'indexInfo': 1, + 'assetIndex': {'cost': 1, 'noSymbol': 10}, + 'constituents': 2, + 'apiTradingStatus': {'cost': 1, 'noSymbol': 10}, + 'lvtKlines': 1, + 'convert/exchangeInfo': 4, + 'insuranceBalance': 1, + }, + }, + 'fapiData': { + 'get': { + 'delivery-price': 1, + 'openInterestHist': 1, + 'topLongShortAccountRatio': 1, + 'topLongShortPositionRatio': 1, + 'globalLongShortAccountRatio': 1, + 'takerlongshortRatio': 1, + 'basis': 1, + }, + }, + 'fapiPrivate': { + 'get': { + 'forceOrders': {'cost': 20, 'noSymbol': 50}, + 'allOrders': 5, + 'openOrder': 1, + 'openOrders': {'cost': 1, 'noSymbol': 40}, + 'order': 1, + 'account': 5, + 'balance': 5, + 'leverageBracket': 1, + 'positionMargin/history': 1, + 'positionRisk': 5, + 'positionSide/dual': 30, + 'userTrades': 5, + 'income': 30, + 'commissionRate': 20, + 'rateLimit/order': 1, + 'apiTradingStatus': 1, + 'multiAssetsMargin': 30, + # broker endpoints + 'apiReferral/ifNewUser': 1, + 'apiReferral/customization': 1, + 'apiReferral/userCustomization': 1, + 'apiReferral/traderNum': 1, + 'apiReferral/overview': 1, + 'apiReferral/tradeVol': 1, + 'apiReferral/rebateVol': 1, + 'apiReferral/traderSummary': 1, + 'adlQuantile': 5, + 'pmAccountInfo': 5, + 'orderAmendment': 1, + 'income/asyn': 1000, + 'income/asyn/id': 10, + 'order/asyn': 1000, + 'order/asyn/id': 10, + 'trade/asyn': 1000, + 'trade/asyn/id': 10, + 'feeBurn': 1, + 'symbolConfig': 5, + 'accountConfig': 5, + 'convert/orderStatus': 5, + }, + 'post': { + 'batchOrders': 5, + 'positionSide/dual': 1, + 'positionMargin': 1, + 'marginType': 1, + 'order': 4, + 'leverage': 1, + 'listenKey': 1, + 'countdownCancelAll': 10, + 'multiAssetsMargin': 1, + # broker endpoints + 'apiReferral/customization': 1, + 'apiReferral/userCustomization': 1, + 'feeBurn': 1, + 'convert/getQuote': 200, # 360 requests per hour + 'convert/acceptQuote': 20, + }, + 'put': { + 'listenKey': 1, + 'order': 1, + 'batchOrders': 5, + }, + 'delete': { + 'batchOrders': 1, + 'order': 1, + 'allOpenOrders': 1, + 'listenKey': 1, + }, + }, + 'fapiPublicV2': { + 'get': { + 'ticker/price': 0, + }, + }, + 'fapiPrivateV2': { + 'get': { + 'account': 1, + 'balance': 1, + 'positionRisk': 1, + }, + }, + 'fapiPublicV3': { + 'get': {}, + }, + 'fapiPrivateV3': { + 'get': { + 'account': 1, + 'balance': 1, + 'positionRisk': 1, + }, + }, + 'eapiPublic': { + 'get': { + 'ping': 1, + 'time': 1, + 'exchangeInfo': 1, + 'index': 1, + 'ticker': 5, + 'mark': 5, + 'depth': 1, + 'klines': 1, + 'trades': 5, + 'historicalTrades': 20, + 'exerciseHistory': 3, + 'openInterest': 3, + }, + }, + 'eapiPrivate': { + 'get': { + 'account': 3, + 'position': 5, + 'openOrders': {'cost': 1, 'noSymbol': 40}, + 'historyOrders': 3, + 'userTrades': 5, + 'exerciseRecord': 5, + 'bill': 1, + 'income/asyn': 5, + 'income/asyn/id': 5, + 'marginAccount': 3, + 'mmp': 1, + 'countdownCancelAll': 1, + 'order': 1, + 'block/order/orders': 5, + 'block/order/execute': 5, + 'block/user-trades': 5, + 'blockTrades': 5, + }, + 'post': { + 'order': 1, + 'batchOrders': 5, + 'listenKey': 1, + 'mmpSet': 1, + 'mmpReset': 1, + 'countdownCancelAll': 1, + 'countdownCancelAllHeartBeat': 10, + 'block/order/create': 5, + 'block/order/execute': 5, + }, + 'put': { + 'listenKey': 1, + 'block/order/create': 5, + }, + 'delete': { + 'order': 1, + 'batchOrders': 1, + 'allOpenOrders': 1, + 'allOpenOrdersByUnderlying': 1, + 'listenKey': 1, + 'block/order/create': 5, + }, + }, + 'public': { + # IP(api) request rate limit of 6000 per minute + # 1 IP(api) => cost = 0.2 =>(1000 / (50 * 0.2)) * 60 = 6000 + 'get': { + 'ping': 0.2, # Weight(IP): 1 => cost = 0.2 * 1 = 0.2 + 'time': 0.2, + 'depth': {'cost': 1, 'byLimit': [[100, 1], [500, 5], [1000, 10], [5000, 50]]}, + 'trades': 2, # Weight(IP): 10 => cost = 0.2 * 10 = 2 + 'aggTrades': 0.4, + 'historicalTrades': 2, # Weight(IP): 10 => cost = 0.2 * 10 = 2 + 'klines': 0.4, + 'uiKlines': 0.4, + 'ticker/24hr': {'cost': 0.4, 'noSymbol': 16}, + 'ticker': {'cost': 0.4, 'noSymbol': 16}, + 'ticker/tradingDay': 0.8, + 'ticker/price': {'cost': 0.4, 'noSymbol': 0.8}, + 'ticker/bookTicker': {'cost': 0.4, 'noSymbol': 0.8}, + 'exchangeInfo': 4, # Weight(IP): 20 => cost = 0.2 * 20 = 4 + 'avgPrice': 0.4, + }, + 'put': { + 'userDataStream': 0.4, + }, + 'post': { + 'userDataStream': 0.4, + }, + 'delete': { + 'userDataStream': 0.4, + }, + }, + 'private': { + 'get': { + 'allOrderList': 4, # oco Weight(IP): 20 => cost = 0.2 * 20 = 4 + 'openOrderList': 1.2, # oco Weight(IP): 6 => cost = 0.2 * 6 = 1.2 + 'orderList': 0.8, # oco + 'order': 0.8, + 'openOrders': {'cost': 1.2, 'noSymbol': 16}, + 'allOrders': 4, + 'account': 4, + 'myTrades': 4, + 'rateLimit/order': 8, # Weight(IP): 40 => cost = 0.2 * 40 = 8 + 'myPreventedMatches': 4, # Weight(IP): 20 => cost = 0.2 * 20 = 4 + 'myAllocations': 4, + 'account/commission': 4, + }, + 'post': { + 'order/oco': 0.2, + 'orderList/oco': 0.2, + 'orderList/oto': 0.2, + 'orderList/otoco': 0.2, + 'sor/order': 0.2, + 'sor/order/test': 0.2, + 'order': 0.2, + 'order/cancelReplace': 0.2, + 'order/test': 0.2, + }, + 'delete': { + 'openOrders': 0.2, + 'orderList': 0.2, # oco + 'order': 0.2, + }, + }, + 'papi': { + # IP(papi) request rate limit of 6000 per minute + # 1 IP(papi) => cost = 0.2 =>(1000 / (50 * 0.2)) * 60 = 6000 + # Order(papi) request rate limit of 1200 per minute + # 1 Order(papi) => cost = 1 =>(1000 / (50 * 1)) * 60 = 1200 + 'get': { + 'ping': 0.2, + 'um/order': 1, + 'um/openOrder': 1, + 'um/openOrders': {'cost': 1, 'noSymbol': 40}, + 'um/allOrders': 5, + 'cm/order': 1, + 'cm/openOrder': 1, + 'cm/openOrders': {'cost': 1, 'noSymbol': 40}, + 'cm/allOrders': 20, + 'um/conditional/openOrder': 1, + 'um/conditional/openOrders': {'cost': 1, 'noSymbol': 40}, + 'um/conditional/orderHistory': 1, + 'um/conditional/allOrders': {'cost': 1, 'noSymbol': 40}, + 'cm/conditional/openOrder': 1, + 'cm/conditional/openOrders': {'cost': 1, 'noSymbol': 40}, + 'cm/conditional/orderHistory': 1, + 'cm/conditional/allOrders': 40, + 'margin/order': 10, + 'margin/openOrders': 5, + 'margin/allOrders': 100, + 'margin/orderList': 5, + 'margin/allOrderList': 100, + 'margin/openOrderList': 5, + 'margin/myTrades': 5, + 'balance': 4, + 'account': 4, + 'margin/maxBorrowable': 1, + 'margin/maxWithdraw': 1, + 'um/positionRisk': 1, + 'cm/positionRisk': 0.2, + 'um/positionSide/dual': 6, + 'cm/positionSide/dual': 6, + 'um/userTrades': 5, + 'cm/userTrades': 20, + 'um/leverageBracket': 0.2, + 'cm/leverageBracket': 0.2, + 'margin/forceOrders': 1, + 'um/forceOrders': {'cost': 20, 'noSymbol': 50}, + 'cm/forceOrders': {'cost': 20, 'noSymbol': 50}, + 'um/apiTradingStatus': {'cost': 0.2, 'noSymbol': 2}, + 'um/commissionRate': 4, + 'cm/commissionRate': 4, + 'margin/marginLoan': 2, + 'margin/repayLoan': 2, + 'margin/marginInterestHistory': 0.2, + 'portfolio/interest-history': 10, + 'um/income': 6, + 'cm/income': 6, + 'um/account': 1, + 'cm/account': 1, + 'repay-futures-switch': 6, + 'um/adlQuantile': 5, + 'cm/adlQuantile': 5, + 'um/trade/asyn': 300, + 'um/trade/asyn/id': 2, + 'um/order/asyn': 300, + 'um/order/asyn/id': 2, + 'um/income/asyn': 300, + 'um/income/asyn/id': 2, + 'um/orderAmendment': 1, + 'cm/orderAmendment': 1, + 'um/feeBurn': 30, + 'um/accountConfig': 1, + 'um/symbolConfig': 1, + 'cm/accountConfig': 1, + 'cm/symbolConfig': 1, + 'rateLimit/order': 1, + }, + 'post': { + 'um/order': 1, + 'um/conditional/order': 1, + 'cm/order': 1, + 'cm/conditional/order': 1, + 'margin/order': 1, + 'marginLoan': 100, + 'repayLoan': 100, + 'margin/order/oco': 1, + 'um/leverage': 0.2, + 'cm/leverage': 0.2, + 'um/positionSide/dual': 0.2, + 'cm/positionSide/dual': 0.2, + 'auto-collection': 150, + 'bnb-transfer': 150, + 'repay-futures-switch': 150, + 'repay-futures-negative-balance': 150, + 'listenKey': 0.2, + 'asset-collection': 6, + 'margin/repay-debt': 3000, + 'um/feeBurn': 1, + }, + 'put': { + 'listenKey': 0.2, + 'um/order': 1, + 'cm/order': 1, + }, + 'delete': { + 'um/order': 1, + 'um/conditional/order': 1, + 'um/allOpenOrders': 1, + 'um/conditional/allOpenOrders': 1, + 'cm/order': 1, + 'cm/conditional/order': 1, + 'cm/allOpenOrders': 1, + 'cm/conditional/allOpenOrders': 1, + 'margin/order': 2, + 'margin/allOpenOrders': 5, + 'margin/orderList': 2, + 'listenKey': 0.2, + }, + }, + 'papiV2': { + 'get': { + 'um/account': 1, + }, + }, + }, + 'fees': { + 'trading': { + 'feeSide': 'get', + 'tierBased': False, + 'percentage': True, + 'taker': self.parse_number('0.001'), + 'maker': self.parse_number('0.001'), + }, + 'linear': { + 'trading': { + 'feeSide': 'quote', + 'tierBased': True, + 'percentage': True, + 'taker': self.parse_number('0.000500'), + 'maker': self.parse_number('0.000200'), + 'tiers': { + 'taker': [ + [self.parse_number('0'), self.parse_number('0.000400')], + [self.parse_number('250'), self.parse_number('0.000400')], + [self.parse_number('2500'), self.parse_number('0.000350')], + [self.parse_number('7500'), self.parse_number('0.000320')], + [self.parse_number('22500'), self.parse_number('0.000300')], + [self.parse_number('50000'), self.parse_number('0.000270')], + [self.parse_number('100000'), self.parse_number('0.000250')], + [self.parse_number('200000'), self.parse_number('0.000220')], + [self.parse_number('400000'), self.parse_number('0.000200')], + [self.parse_number('750000'), self.parse_number('0.000170')], + ], + 'maker': [ + [self.parse_number('0'), self.parse_number('0.000200')], + [self.parse_number('250'), self.parse_number('0.000160')], + [self.parse_number('2500'), self.parse_number('0.000140')], + [self.parse_number('7500'), self.parse_number('0.000120')], + [self.parse_number('22500'), self.parse_number('0.000100')], + [self.parse_number('50000'), self.parse_number('0.000080')], + [self.parse_number('100000'), self.parse_number('0.000060')], + [self.parse_number('200000'), self.parse_number('0.000040')], + [self.parse_number('400000'), self.parse_number('0.000020')], + [self.parse_number('750000'), self.parse_number('0')], + ], + }, + }, + }, + 'inverse': { + 'trading': { + 'feeSide': 'base', + 'tierBased': True, + 'percentage': True, + 'taker': self.parse_number('0.000500'), + 'maker': self.parse_number('0.000100'), + 'tiers': { + 'taker': [ + [self.parse_number('0'), self.parse_number('0.000500')], + [self.parse_number('250'), self.parse_number('0.000450')], + [self.parse_number('2500'), self.parse_number('0.000400')], + [self.parse_number('7500'), self.parse_number('0.000300')], + [self.parse_number('22500'), self.parse_number('0.000250')], + [self.parse_number('50000'), self.parse_number('0.000240')], + [self.parse_number('100000'), self.parse_number('0.000240')], + [self.parse_number('200000'), self.parse_number('0.000240')], + [self.parse_number('400000'), self.parse_number('0.000240')], + [self.parse_number('750000'), self.parse_number('0.000240')], + ], + 'maker': [ + [self.parse_number('0'), self.parse_number('0.000100')], + [self.parse_number('250'), self.parse_number('0.000080')], + [self.parse_number('2500'), self.parse_number('0.000050')], + [self.parse_number('7500'), self.parse_number('0.0000030')], + [self.parse_number('22500'), self.parse_number('0')], + [self.parse_number('50000'), self.parse_number('-0.000050')], + [self.parse_number('100000'), self.parse_number('-0.000060')], + [self.parse_number('200000'), self.parse_number('-0.000070')], + [self.parse_number('400000'), self.parse_number('-0.000080')], + [self.parse_number('750000'), self.parse_number('-0.000090')], + ], + }, + }, + }, + 'option': {}, + }, + 'currencies': { + 'BNFCR': self.safe_currency_structure({'id': 'BNFCR', 'code': 'BNFCR', 'precision': self.parse_number('0.001')}), + }, + 'commonCurrencies': { + 'BCC': 'BCC', # kept for backward-compatibility https://github.com/ccxt/ccxt/issues/4848 + 'YOYO': 'YOYOW', + }, + 'precisionMode': TICK_SIZE, + # exchange-specific options + 'options': { + 'sandboxMode': False, + 'fetchMargins': True, + 'fetchMarkets': { + 'types': [ + 'spot', # allows CORS in browsers + 'linear', # allows CORS in browsers + 'inverse', # allows CORS in browsers + # 'option', # does not allow CORS, enable outside of the browser only + ], + }, + 'loadAllOptions': False, + 'fetchCurrencies': True, # self is a private call and it requires API keys + # 'fetchTradesMethod': 'publicGetAggTrades', # publicGetTrades, publicGetHistoricalTrades, eapiPublicGetTrades + # 'repayCrossMarginMethod': 'papiPostRepayLoan', # papiPostMarginRepayDebt + 'defaultTimeInForce': 'GTC', # 'GTC' = Good To Cancel(default), 'IOC' = Immediate Or Cancel + 'defaultType': 'spot', # 'spot', 'future', 'margin', 'delivery', 'option' + 'defaultSubType': None, # 'linear', 'inverse' + 'hasAlreadyAuthenticatedSuccessfully': False, + 'warnOnFetchOpenOrdersWithoutSymbol': True, + 'currencyToPrecisionRoundingMode': TRUNCATE, + # not an error + # https://github.com/ccxt/ccxt/issues/11268 + # https://github.com/ccxt/ccxt/pull/11624 + # POST https://fapi.binance.com/fapi/v1/marginType 400 Bad Request + # binanceusdm + 'throwMarginModeAlreadySet': False, + 'fetchPositions': 'positionRisk', # or 'account' or 'option' + 'recvWindow': 10 * 1000, # 10 sec + 'timeDifference': 0, # the difference between system clock and Binance clock + 'adjustForTimeDifference': False, # controls the adjustment logic upon instantiation + 'newOrderRespType': { + 'market': 'FULL', # 'ACK' for order id, 'RESULT' for full order or 'FULL' for order with fills + 'limit': 'FULL', # we change it from 'ACK' by default to 'FULL'(returns immediately if limit is not hit) + }, + 'quoteOrderQty': True, # whether market orders support amounts in quote currency + 'broker': { + 'spot': 'x-TKT5PX2F', + 'margin': 'x-TKT5PX2F', + 'future': 'x-cvBPrNm9', + 'delivery': 'x-xcKtGhcu', + 'swap': 'x-cvBPrNm9', + 'option': 'x-xcKtGhcu', + 'inverse': 'x-xcKtGhcu', + }, + 'accountsByType': { + 'main': 'MAIN', + 'spot': 'MAIN', + 'funding': 'FUNDING', + 'margin': 'MARGIN', + 'cross': 'MARGIN', + 'future': 'UMFUTURE', # backwards compatibility + 'delivery': 'CMFUTURE', # backwards compatbility + 'linear': 'UMFUTURE', + 'swap': 'UMFUTURE', + 'inverse': 'CMFUTURE', + 'option': 'OPTION', + }, + 'accountsById': { + 'MAIN': 'spot', + 'FUNDING': 'funding', + 'MARGIN': 'margin', + 'UMFUTURE': 'linear', + 'CMFUTURE': 'inverse', + 'OPTION': 'option', + }, + 'networks': { + 'ERC20': 'ETH', + 'TRC20': 'TRX', + 'BEP2': 'BNB', + 'BEP20': 'BSC', + 'OMNI': 'OMNI', + 'EOS': 'EOS', + 'SPL': 'SOL', # temporarily keep support for SPL(old name) + 'SOL': 'SOL', # we shouldn't rename SOL + }, + 'networksById': { + 'SOL': 'SOL', # temporary fix for SPL definition + }, + 'impliedNetworks': { + 'ETH': {'ERC20': 'ETH'}, + 'TRX': {'TRC20': 'TRX'}, + }, + 'legalMoney': { + 'MXN': True, + 'UGX': True, + 'SEK': True, + 'CHF': True, + 'VND': True, + 'AED': True, + 'DKK': True, + 'KZT': True, + 'HUF': True, + 'PEN': True, + 'PHP': True, + 'USD': True, + 'TRY': True, + 'EUR': True, + 'NGN': True, + 'PLN': True, + 'BRL': True, + 'ZAR': True, + 'KES': True, + 'ARS': True, + 'RUB': True, + 'AUD': True, + 'NOK': True, + 'CZK': True, + 'GBP': True, + 'UAH': True, + 'GHS': True, + 'HKD': True, + 'CAD': True, + 'INR': True, + 'JPY': True, + 'NZD': True, + }, + 'legalMoneyCurrenciesById': { + 'BUSD': 'USD', + }, + 'defaultWithdrawPrecision': 0.00000001, + }, + 'features': { + 'spot': { + 'sandbox': True, + 'fetchCurrencies': { + 'private': True, + }, + 'createOrder': { + 'marginMode': True, + 'triggerPrice': True, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': True, + 'takeProfitPrice': True, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': True, + 'leverage': False, + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': { + 'EXPIRE_MAKER': True, + 'EXPIRE_TAKER': True, + 'EXPIRE_BOTH': True, + 'NONE': True, + }, + 'trailing': False, # todo: self is different from standard trailing https://github.com/binance/binance-spot-api-docs/blob/master/faqs/trailing-stop-faq.md + 'icebergAmount': True, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': None, + 'untilDays': 1, # days between start-end + 'symbolRequired': True, + }, + 'fetchOrder': { + 'marginMode': True, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOpenOrders': { + 'marginMode': True, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': True, + 'limit': 1000, + 'daysBack': None, + 'untilDays': 10000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchClosedOrders': { + 'marginMode': True, + 'limit': 1000, + 'daysBack': None, + 'daysBackCanceled': None, + 'untilDays': 10000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOHLCV': { + 'limit': 1000, + }, + }, + 'forDerivatives': { + 'sandbox': True, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerPriceType': { + 'mark': True, + 'last': True, + 'index': False, + }, + 'stopLossPrice': True, + 'takeProfitPrice': True, + 'attachedStopLossTakeProfit': None, # not supported + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': True, + # 'GTX': True, + }, + 'hedged': True, + # exchange-supported features + 'selfTradePrevention': True, + 'trailing': True, + 'iceberg': False, + 'leverage': False, + 'marketBuyRequiresPrice': False, + 'marketBuyByCost': True, + }, + 'createOrders': { + 'max': 5, + }, + 'fetchMyTrades': { + 'marginMode': False, + 'daysBack': None, + 'limit': 1000, + 'untilDays': 7, + 'symbolRequired': True, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOpenOrders': { + 'marginMode': True, + 'limit': 500, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': True, + 'limit': 1000, + 'daysBack': 90, + 'untilDays': 7, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchClosedOrders': { + 'marginMode': True, + 'limit': 1000, + 'daysBack': 90, + 'daysBackCanceled': 3, + 'untilDays': 7, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOHLCV': { + 'limit': 1500, + }, + }, + 'swap': { + 'linear': { + 'extends': 'forDerivatives', + }, + 'inverse': { + 'extends': 'forDerivatives', + }, + }, + 'future': { + 'linear': { + 'extends': 'forDerivatives', + }, + 'inverse': { + 'extends': 'forDerivatives', + }, + }, + }, + 'exceptions': { + 'spot': { + 'exact': { + # + # 1xxx + # + '-1004': OperationFailed, # {"code":-1004,"msg":"Server is busy, please wait and try again"} + '-1008': OperationFailed, # undocumented, but mentioned: This is sent whenever the servers are overloaded with requests. + '-1099': AuthenticationError, # {"code":-1099,"msg":"Not found, authenticated, or authorized"} + '-1108': BadRequest, # undocumented, but mentioned: This error will occur if a value to a parameter being sent was too large, potentially causing overflow + '-1131': BadRequest, # {"code":-1131,"msg":"recvWindow must be less than 60000"} + '-1134': BadRequest, # strategyType was less than 1000000. + '-1135': BadRequest, # undocumented, but mentioned: This error code will occur if a parameter requiring a JSON object is invalid. + '-1145': BadRequest, # cancelRestrictions has to be either ONLY_NEW or ONLY_PARTIALLY_FILLED. + '-1151': BadSymbol, # Symbol is present multiple times in the list. + # + # 2xxx + # + '-2008': AuthenticationError, # undocumented, Invalid Api-Key ID + '-2016': OperationRejected, # {"code":-2016,"msg":"No trading window could be found for the symbol. Try ticker/24hrs instead."} + '-2021': BadResponse, # This code is sent when either the cancellation of the order failed or the new order placement failed but not both. + '-2022': BadResponse, # This code is sent when both the cancellation of the order failed and the new order placement failed. + '-2026': InvalidOrder, # Order was canceled or expired with no executed qty over 90 days ago and has been archived. + # + # 3xxx(these errors are available only for spot atm) + # + '-3000': OperationFailed, # {"code":-3000,"msg":"Internal server error."} + '-3001': AuthenticationError, # {"code":-3001,"msg":"Please enable 2FA first."} + '-3002': BadSymbol, # {"code":-3002,"msg":"We don't have self asset."} + '-3003': BadRequest, # {"code":-3003,"msg":"Margin account does not exist."} + '-3004': OperationRejected, # {"code":-3004,"msg":"Trade not allowed."} + '-3005': BadRequest, # {"code":-3005,"msg":"Transferring out not allowed. Transfer out amount exceeds max amount."} + '-3006': BadRequest, # {"code":-3006,"msg":"Your borrow amount has exceed maximum borrow amount."} + '-3007': OperationFailed, # {"code":-3007,"msg":"You have pending transaction, please try again later.."} + '-3008': BadRequest, # {"code":-3008,"msg":"Borrow not allowed. Your borrow amount has exceed maximum borrow amount."} + '-3009': OperationRejected, # {"code":-3009,"msg":"This asset are not allowed to transfer into margin account currently."} + '-3010': BadRequest, # {"code":-3010,"msg":"Repay not allowed. Repay amount exceeds borrow amount."} + '-3011': BadRequest, # {"code":-3011,"msg":"Your input date is invalid."} + '-3012': OperationRejected, # {"code":-3012,"msg":"Borrow is banned for self asset."} + '-3013': BadRequest, # {"code":-3013,"msg":"Borrow amount less than minimum borrow amount."} + '-3014': AccountSuspended, # {"code":-3014,"msg":"Borrow is banned for self account."} + '-3015': BadRequest, # {"code":-3015,"msg":"Repay amount exceeds borrow amount."} + '-3016': BadRequest, # {"code":-3016,"msg":"Repay amount less than minimum repay amount."} + '-3017': OperationRejected, # {"code":-3017,"msg":"This asset are not allowed to transfer into margin account currently."} + '-3018': AccountSuspended, # {"code":-3018,"msg":"Transferring in has been banned for self account."} + '-3019': AccountSuspended, # {"code":-3019,"msg":"Transferring out has been banned for self account."} + '-3020': BadRequest, # {"code":-3020,"msg":"Transfer out amount exceeds max amount."} + '-3021': BadRequest, # {"code":-3021,"msg":"Margin account are not allowed to trade self trading pair."} + '-3022': AccountSuspended, # {"code":-3022,"msg":"You account's trading is banned."} + '-3023': OperationRejected, # {"code":-3023,"msg":"You can't transfer out/place order under current margin level."} + '-3024': OperationRejected, # {"code":-3024,"msg":"The unpaid debt is too small after self repayment."} + '-3025': BadRequest, # {"code":-3025,"msg":"Your input date is invalid."} + '-3026': BadRequest, # {"code":-3026,"msg":"Your input param is invalid."} + '-3027': BadSymbol, # {"code":-3027,"msg":"Not a valid margin asset."} + '-3028': BadSymbol, # {"code":-3028,"msg":"Not a valid margin pair."} + '-3029': OperationFailed, # {"code":-3029,"msg":"Transfer failed."} + '-3036': AccountSuspended, # {"code":-3036,"msg":"This account is not allowed to repay."} + '-3037': OperationFailed, # {"code":-3037,"msg":"PNL is clearing. Wait a second."} + '-3038': BadRequest, # {"code":-3038,"msg":"Listen key not found."} + '-3041': InsufficientFunds, # {"code":-3041,"msg":"Balance is not enough"} + '-3042': BadRequest, # {"code":-3042,"msg":"PriceIndex not available for self margin pair."} + '-3043': PermissionDenied, # {"code":-3043,"msg":"Transferring in not allowed."} + '-3044': OperationFailed, # {"code":-3044,"msg":"System busy."} + '-3045': OperationRejected, # {"code":-3045,"msg":"The system doesn't have enough asset now."} + '-3999': PermissionDenied, # {"code":-3999,"msg":"This function is only available for invited users."} + # + # 4xxx(different from contract markets) + # + '-4000': ExchangeError, # override commons + '-4001': BadRequest, # {"code":-4001 ,"msg":"Invalid operation."} + '-4002': BadRequest, # {"code":-4002 ,"msg":"Invalid get."} + '-4003': BadRequest, # {"code":-4003 ,"msg":"Your input email is invalid."} + '-4004': AuthenticationError, # {"code":-4004,"msg":"You don't login or auth."} + '-4005': RateLimitExceeded, # {"code":-4005 ,"msg":"Too many new requests."} + '-4006': BadRequest, # {"code":-4006 ,"msg":"Support main account only."} + '-4007': PermissionDenied, # {"code":-4007 ,"msg":"Address validation is not passed."} + '-4008': PermissionDenied, # {"code":-4008 ,"msg":"Address tag validation is not passed."} + '-4009': ExchangeError, # undocumented + '-4010': PermissionDenied, # {"code":-4010 ,"msg":"White list mail has been confirmed."} # [TODO] possible bug: it should probably be "has not been confirmed" + '-4011': BadRequest, # {"code":-4011 ,"msg":"White list mail is invalid."} + '-4012': PermissionDenied, # {"code":-4012 ,"msg":"White list is not opened."} + '-4013': AuthenticationError, # {"code":-4013 ,"msg":"2FA is not opened."} + '-4014': OperationRejected, # {"code":-4014 ,"msg":"Withdraw is not allowed within 2 min login."} + '-4015': PermissionDenied, # {"code":-4015 ,"msg":"Withdraw is limited."} + '-4016': PermissionDenied, # {"code":-4016 ,"msg":"Within 24 hours after password modification, withdrawal is prohibited."} + '-4017': PermissionDenied, # {"code":-4017 ,"msg":"Within 24 hours after the release of 2FA, withdrawal is prohibited."} + '-4018': BadSymbol, # {"code":-4018,"msg":"We don't have self asset."} + '-4019': BadRequest, # {"code":-4019,"msg":"Current asset is not open for withdrawal."} + '-4020': ExchangeError, # override commons + '-4021': BadRequest, # {"code":-4021,"msg":"Asset withdrawal must be an %s multiple of %s."} + '-4022': BadRequest, # {"code":-4022,"msg":"Not less than the minimum pick-up quantity %s."} + '-4023': OperationRejected, # {"code":-4023,"msg":"Within 24 hours, the withdrawal exceeds the maximum amount."} + '-4024': InsufficientFunds, # {"code":-4024,"msg":"You don't have self asset."} + '-4025': InsufficientFunds, # {"code":-4025,"msg":"The number of hold asset is less than zero."} + '-4026': InsufficientFunds, # {"code":-4026,"msg":"You have insufficient balance."} + '-4027': OperationFailed, # {"code":-4027,"msg":"Failed to obtain tranId."} + '-4028': BadRequest, # {"code":-4028,"msg":"The amount of withdrawal must be greater than the Commission."} + '-4029': BadRequest, # {"code":-4029,"msg":"The withdrawal record does not exist."} + '-4030': BadResponse, # {"code":-4030,"msg":"Confirmation of successful asset withdrawal. [TODO] possible bug in docs"} + '-4031': OperationFailed, # {"code":-4031,"msg":"Cancellation failed."} + '-4032': OperationRejected, # {"code":-4032,"msg":"Withdraw verification exception."} + '-4033': BadRequest, # {"code":-4033,"msg":"Illegal address."} + '-4034': OperationRejected, # {"code":-4034,"msg":"The address is suspected of fake."} + '-4035': PermissionDenied, # {"code":-4035,"msg":"This address is not on the whitelist. Please join and try again."} + '-4036': PermissionDenied, # {"code":-4036,"msg":"The new address needs to be withdrawn in {0} hours."} + '-4037': OperationFailed, # {"code":-4037,"msg":"Re-sending Mail failed."} + '-4038': OperationFailed, # {"code":-4038,"msg":"Please try again in 5 minutes."} + '-4039': PermissionDenied, # {"code":-4039,"msg":"The user does not exist."} + '-4040': OperationRejected, # {"code":-4040,"msg":"This address not charged."} + '-4041': OperationFailed, # {"code":-4041,"msg":"Please try again in one minute."} + '-4042': OperationRejected, # {"code":-4042,"msg":"This asset cannot get deposit address again."} + '-4043': OperationRejected, # {"code":-4043,"msg":"More than 100 recharge addresses were used in 24 hours."} + '-4044': PermissionDenied, # {"code":-4044,"msg":"This is a blacklist country."} + '-4045': OperationFailed, # {"code":-4045,"msg":"Failure to acquire assets."} + '-4046': AuthenticationError, # {"code":-4046,"msg":"Agreement not confirmed."} + '-4047': BadRequest, # {"code":-4047,"msg":"Time interval must be within 0-90 days"} + '-4048': ExchangeError, # override commons + '-4049': ExchangeError, # override commons + '-4050': ExchangeError, # override commons + '-4051': ExchangeError, # override commons + '-4052': ExchangeError, # override commons + '-4053': ExchangeError, # override commons + '-4054': ExchangeError, # override commons + '-4055': ExchangeError, # override commons + '-4056': ExchangeError, # override commons + '-4057': ExchangeError, # override commons + '-4058': ExchangeError, # override commons + '-4059': ExchangeError, # override commons + '-4060': OperationFailed, # As your deposit has not reached the required block confirmations, we have temporarily locked {0} asset + '-4061': ExchangeError, # override commons + '-4062': ExchangeError, # override commons + '-4063': ExchangeError, # override commons + '-4064': ExchangeError, # override commons + '-4065': ExchangeError, # override commons + '-4066': ExchangeError, # override commons + '-4067': ExchangeError, # override commons + '-4068': ExchangeError, # override commons + '-4069': ExchangeError, # override commons + '-4070': ExchangeError, # override commons + '-4071': ExchangeError, # override commons + '-4072': ExchangeError, # override commons + '-4073': ExchangeError, # override commons + '-4074': ExchangeError, # override commons + '-4075': ExchangeError, # override commons + '-4076': ExchangeError, # override commons + '-4077': ExchangeError, # override commons + '-4078': ExchangeError, # override commons + '-4079': ExchangeError, # override commons + '-4080': ExchangeError, # override commons + '-4081': ExchangeError, # override commons + '-4082': ExchangeError, # override commons + '-4083': ExchangeError, # override commons + '-4084': ExchangeError, # override commons + '-4085': ExchangeError, # override commons + '-4086': ExchangeError, # override commons + '-4087': ExchangeError, # override commons + '-4088': ExchangeError, # override commons + '-4089': ExchangeError, # override commons + '-4091': ExchangeError, # override commons + '-4092': ExchangeError, # override commons + '-4093': ExchangeError, # override commons + '-4094': ExchangeError, # override commons + '-4095': ExchangeError, # override commons + '-4096': ExchangeError, # override commons + '-4097': ExchangeError, # override commons + '-4098': ExchangeError, # override commons + '-4099': ExchangeError, # override commons + '-4101': ExchangeError, # override commons + '-4102': ExchangeError, # override commons + '-4103': ExchangeError, # override commons + '-4104': ExchangeError, # override commons + '-4105': ExchangeError, # override commons + '-4106': ExchangeError, # override commons + '-4107': ExchangeError, # override commons + '-4108': ExchangeError, # override commons + '-4109': ExchangeError, # override commons + '-4110': ExchangeError, # override commons + '-4112': ExchangeError, # override commons + '-4113': ExchangeError, # override commons + '-4114': ExchangeError, # override commons + '-4115': ExchangeError, # override commons + '-4116': ExchangeError, # override commons + '-4117': ExchangeError, # override commons + '-4118': ExchangeError, # override commons + '-4119': ExchangeError, # override commons + '-4120': ExchangeError, # override commons + '-4121': ExchangeError, # override commons + '-4122': ExchangeError, # override commons + '-4123': ExchangeError, # override commons + '-4124': ExchangeError, # override commons + '-4125': ExchangeError, # override commons + '-4126': ExchangeError, # override commons + '-4127': ExchangeError, # override commons + '-4128': ExchangeError, # override commons + '-4129': ExchangeError, # override commons + '-4130': ExchangeError, # override commons + '-4131': ExchangeError, # override commons + '-4132': ExchangeError, # override commons + '-4133': ExchangeError, # override commons + '-4134': ExchangeError, # override commons + '-4135': ExchangeError, # override commons + '-4136': ExchangeError, # override commons + '-4137': ExchangeError, # override commons + '-4138': ExchangeError, # override commons + '-4139': ExchangeError, # override commons + '-4141': ExchangeError, # override commons + '-4142': ExchangeError, # override commons + '-4143': ExchangeError, # override commons + '-4144': ExchangeError, # override commons + '-4145': ExchangeError, # override commons + '-4146': ExchangeError, # override commons + '-4147': ExchangeError, # override commons + '-4148': ExchangeError, # override commons + '-4149': ExchangeError, # override commons + '-4150': ExchangeError, # override commons + # + # 5xxx + # + '-5001': BadRequest, # Don't allow transfer to micro assets. + '-5002': InsufficientFunds, # You have insufficient balance. + '-5003': InsufficientFunds, # You don't have self asset. + '-5004': OperationRejected, # The residual balances of %s have exceeded 0.001BTC, Please re-choose. + '-5005': OperationRejected, # The residual balances of %s is too low, Please re-choose. + '-5006': OperationRejected, # Only transfer once in 24 hours. + '-5007': BadRequest, # Quantity must be greater than zero. + '-5008': OperationRejected, # Insufficient amount of returnable assets. + '-5009': BadSymbol, # Product does not exist. + '-5010': OperationFailed, # Asset transfer fail. + '-5011': BadRequest, # future account not exists. + '-5012': OperationFailed, # Asset transfer is in pending. + '-5013': InsufficientFunds, # {"code":-5013,"msg":"Asset transfer failed: insufficient balance""} # undocumented + '-5021': BadRequest, # This parent sub have no relation + '-5022': BadRequest, # future account or sub relation not exists. + # + # 6xxx + # + '-6001': BadSymbol, # Daily product not exists. + '-6003': PermissionDenied, # Product not exist or you don't have permission + '-6004': BadRequest, # Product not in purchase status + '-6005': BadRequest, # Smaller than min purchase limit + '-6006': BadRequest, # Redeem amount error + '-6007': OperationRejected, # Not in redeem time + '-6008': OperationRejected, # Product not in redeem status + '-6009': RateLimitExceeded, # Request frequency too high + '-6011': OperationRejected, # Exceeding the maximum num allowed to purchase per user + '-6012': InsufficientFunds, # Balance not enough + '-6013': BadResponse, # Purchasing failed + '-6014': OperationRejected, # Exceed up-limit allowed to purchased + '-6015': BadRequest, # Empty request body + '-6016': BadRequest, # Parameter err + '-6017': PermissionDenied, # Not in whitelist + '-6018': InsufficientFunds, # Asset not enough + '-6019': OperationRejected, # Need confirm + '-6020': BadRequest, # Project not exists + # + # 7xxx + # + '-7001': BadRequest, # Date range is not supported. + '-7002': BadRequest, # Data request type is not supported. + # + # 1xxxx + # + '-10001': OperationFailed, # The system is under maintenance, please try again later. + '-10002': BadRequest, # Invalid input parameters. + '-10005': BadResponse, # No records found. + '-10007': BadRequest, # This coin is not loanable + '-10008': BadRequest, # This coin is not loanable + '-10009': BadRequest, # This coin can not be used. + '-10010': BadRequest, # This coin can not be used. + '-10011': InsufficientFunds, # Insufficient spot assets. + '-10012': BadRequest, # Invalid repayment amount. + '-10013': InsufficientFunds, # Insufficient collateral amount. + '-10015': OperationFailed, # Collateral deduction failed. + '-10016': OperationFailed, # Failed to provide loan. + '-10017': OperationRejected, # {"code":-10017,"msg":"Repay amount should not be larger than liability."} + '-10018': BadRequest, # Invalid repayment amount. + '-10019': BadRequest, # Configuration does not exists. + '-10020': BadRequest, # User ID does not exist. + '-10021': InvalidOrder, # Order does not exist. + '-10022': BadRequest, # Invalid adjustment amount. + '-10023': OperationFailed, # Failed to adjust LTV. + '-10024': BadRequest, # LTV adjustment not supported. + '-10025': OperationFailed, # Repayment failed. + '-10026': BadRequest, # Invalid parameter. + '-10028': BadRequest, # Invalid parameter. + '-10029': OperationRejected, # Loan amount is too small. + '-10030': OperationRejected, # Loan amount is too much. + '-10031': OperationRejected, # Individual loan quota reached. + '-10032': OperationFailed, # Repayment is temporarily unavailable. + '-10034': OperationRejected, # Repay with collateral is not available currently, please try to repay with borrowed coin. + '-10039': OperationRejected, # Repayment amount is too small. + '-10040': OperationRejected, # Repayment amount is too large. + '-10041': OperationFailed, # Due to high demand, there are currently insufficient loanable assets for {0}. Please adjust your borrow amount or try again tomorrow. + '-10042': BadSymbol, # asset %s is not supported + '-10043': OperationRejected, # {0} borrowing is currently not supported. + '-10044': OperationRejected, # Collateral amount has reached the limit. Please reduce your collateral amount or try with other collaterals. + '-10045': OperationRejected, # The loan coin does not support collateral repayment. Please try again later. + '-10046': OperationRejected, # Collateral Adjustment exceeds the maximum limit. Please try again. + '-10047': PermissionDenied, # This coin is currently not supported in your location due to local regulations. + '-11008': OperationRejected, # undocumented: Exceeding the account’s maximum borrowable limit + '-12014': RateLimitExceeded, # More than 1 request in 2 seconds + # BLVT + '-13000': OperationRejected, # Redeption of the token is forbiden now + '-13001': OperationRejected, # Exceeds individual 24h redemption limit of the token + '-13002': OperationRejected, # Exceeds total 24h redemption limit of the token + '-13003': PermissionDenied, # Subscription of the token is forbiden now + '-13004': OperationRejected, # Exceeds individual 24h subscription limit of the token + '-13005': OperationRejected, # Exceeds total 24h subscription limit of the token + '-13006': OperationRejected, # Subscription amount is too small + '-13007': PermissionDenied, # The Agreement is not signed + # 18xxx - BINANCE CODE + '-18002': OperationRejected, # The total amount of codes you created has exceeded the 24-hour limit, please try again after UTC 0 + '-18003': OperationRejected, # Too many codes created in 24 hours, please try again after UTC 0 + '-18004': OperationRejected, # Too many invalid redeem attempts in 24 hours, please try again after UTC 0 + '-18005': PermissionDenied, # Too many invalid verify attempts, please try later + '-18006': OperationRejected, # The amount is too small, please re-enter + '-18007': OperationRejected, # This token is not currently supported, please re-enter + # + # 2xxxx + # + # 21xxx - PORTFOLIO MARGIN(documented in spot docs) + '-21001': BadRequest, # Request ID is not a Portfolio Margin Account. + '-21002': BadRequest, # Portfolio Margin Account doesn't support transfer from margin to futures. + '-21003': BadResponse, # Fail to retrieve margin assets. + '-21004': OperationRejected, # User doesn’t have portfolio margin bankruptcy loan + '-21005': InsufficientFunds, # User’s spot wallet doesn’t have enough BUSD to repay portfolio margin bankruptcy loan + '-21006': OperationFailed, # User had portfolio margin bankruptcy loan repayment in process + '-21007': OperationFailed, # User failed to repay portfolio margin bankruptcy loan since liquidation was in process + # + # misc + # + '-32603': BadRequest, # undocumented, Filter failure: LOT_SIZE & precision + '400002': BadRequest, # undocumented, {“status”: “FAIL”, “code”: “400002”, “errorMessage”: “Signature for self request is not valid.”} + '100001003': AuthenticationError, # undocumented, {"code":100001003,"msg":"Verification failed"} + '200003903': AuthenticationError, # undocumented, {"code":200003903,"msg":"Your identity verification has been rejected. Please complete identity verification again."} + }, + }, + 'linear': { + 'exact': { + # + # 1xxx + # + '-1005': PermissionDenied, # {"code":-1005,"msg":"No such IP has been white listed"} + '-1008': OperationFailed, # -1008 SERVER_BUSY: Server is currently overloaded with other requests. Please try again in a few minutes. + '-1011': PermissionDenied, # {"code":-1011,"msg":"This IP cannot access self route."} + '-1023': BadRequest, # {"code":-1023,"msg":"Start time is greater than end time."} + '-1099': AuthenticationError, # {"code":-1099,"msg":"Not found, authenticated, or authorized"} + '-1109': PermissionDenied, # {"code":-1109,"msg":"Invalid account."} + '-1110': BadRequest, # {"code":-1110,"msg":"Invalid symbolType."} + '-1113': BadRequest, # {"code":-1113,"msg":"Withdrawal amount must be negative."} + '-1122': BadRequest, # INVALID_SYMBOL_STATUS + '-1126': BadSymbol, # ASSET_NOT_SUPPORTED + '-1136': BadRequest, # {"code":-1136,"msg":"Invalid newOrderRespType"} + # + # 2xxx + # + '-2012': OperationFailed, # CANCEL_ALL_FAIL + '-2016': OperationRejected, # {"code":-2016,"msg":"No trading window could be found for the symbol. Try ticker/24hrs instead."} + '-2017': PermissionDenied, # API Keys are locked on self account. + '-2018': InsufficientFunds, # {"code":-2018,"msg":"Balance is insufficient"} + '-2019': InsufficientFunds, # {"code":-2019,"msg":"Margin is insufficient."} + '-2020': OperationFailed, # {"code":-2020,"msg":"Unable to fill."} + '-2021': OrderImmediatelyFillable, # {"code":-2021,"msg":"Order would immediately trigger."} + '-2022': InvalidOrder, # {"code":-2022,"msg":"ReduceOnly Order is rejected."} + '-2023': OperationFailed, # {"code":-2023,"msg":"User in liquidation mode now."} + '-2024': InsufficientFunds, # {"code":-2024,"msg":"Position is not sufficient."} + '-2025': OperationRejected, # {"code":-2025,"msg":"Reach max open order limit."} + '-2026': InvalidOrder, # {"code":-2026,"msg":"This OrderType is not supported when reduceOnly."} + '-2027': OperationRejected, # {"code":-2027,"msg":"Exceeded the maximum allowable position at current leverage."} + '-2028': OperationRejected, # {"code":-2028,"msg":"Leverage is smaller than permitted: insufficient margin balance"} + # + # 4xxx + # + '-4063': BadRequest, # INVALID_OPTIONS_REQUEST_TYPE + '-4064': BadRequest, # INVALID_OPTIONS_TIME_FRAME + '-4065': BadRequest, # INVALID_OPTIONS_AMOUNT + '-4066': BadRequest, # INVALID_OPTIONS_EVENT_TYPE + '-4069': BadRequest, # Position INVALID_OPTIONS_PREMIUM_FEE + '-4070': BadRequest, # Client options id is not valid. + '-4071': BadRequest, # Invalid options direction + '-4072': OperationRejected, # premium fee is not updated, reject order + '-4073': BadRequest, # OPTIONS_PREMIUM_INPUT_LESS_THAN_ZERO + '-4074': OperationRejected, # Order amount is bigger than upper boundary or less than 0, reject order + '-4075': BadRequest, # output premium fee is less than 0, reject order + '-4076': OperationRejected, # original fee is too much higher than last fee + '-4077': OperationRejected, # place order amount has reached to limit, reject order + '-4078': OperationFailed, # options internal error + '-4079': BadRequest, # invalid options id + '-4080': PermissionDenied, # user not found with id: %s + '-4081': BadRequest, # OPTIONS_NOT_FOUND + '-4085': BadRequest, # Invalid notional limit coefficient + '-4087': PermissionDenied, # User can only place reduce only order + '-4088': PermissionDenied, # User can not place order currently + '-4114': BadRequest, # INVALID_CLIENT_TRAN_ID_LEN + '-4115': BadRequest, # DUPLICATED_CLIENT_TRAN_ID + '-4116': InvalidOrder, # DUPLICATED_CLIENT_ORDER_ID + '-4117': OperationRejected, # STOP_ORDER_TRIGGERING + '-4118': OperationRejected, # REDUCE_ONLY_MARGIN_CHECK_FAILED + '-4131': OperationRejected, # The counterparty's best price does not meet the PERCENT_PRICE filter limit + '-4140': BadRequest, # Invalid symbol status for opening position + '-4141': OperationRejected, # Symbol is closed + '-4144': BadSymbol, # Invalid pair + '-4164': InvalidOrder, # {"code":-4164,"msg":"Order's notional must be no smaller than 20(unless you choose reduce only)."}, + '-4136': InvalidOrder, # {"code":-4136,"msg":"Target strategy invalid for orderType TRAILING_STOP_MARKET,closePosition True"} + '-4165': BadRequest, # Invalid time interval + '-4167': BadRequest, # Unable to adjust to Multi-Assets mode with symbols of USDⓈ-M Futures under isolated-margin mode. + '-4168': BadRequest, # Unable to adjust to isolated-margin mode under the Multi-Assets mode. + '-4169': OperationRejected, # Unable to adjust Multi-Assets Mode with insufficient margin balance in USDⓈ-M Futures + '-4170': OperationRejected, # Unable to adjust Multi-Assets Mode with open orders in USDⓈ-M Futures + '-4171': OperationRejected, # Adjusted asset mode is currently set and does not need to be adjusted repeatedly + '-4172': OperationRejected, # Unable to adjust Multi-Assets Mode with a negative wallet balance of margin available asset in USDⓈ-M Futures account. + '-4183': BadRequest, # Price is higher than stop price multiplier cap. + '-4184': BadRequest, # Price is lower than stop price multiplier floor. + '-4192': PermissionDenied, # Trade forbidden due to Cooling-off Period. + '-4202': PermissionDenied, # Intermediate Personal Verification is required for adjusting leverage over 20x + '-4203': PermissionDenied, # More than 20x leverage is available one month after account registration. + '-4205': PermissionDenied, # More than 20x leverage is available %s days after Futures account registration. + '-4206': PermissionDenied, # hasattr(self, Users) country has limited adjust leverage. + '-4208': OperationRejected, # Current symbol leverage cannot exceed 20 when using position limit adjustment service. + '-4209': OperationRejected, # Leverage adjustment failed. Current symbol max leverage limit is %sx + '-4210': BadRequest, # Stop price is higher than price multiplier cap + '-4211': BadRequest, # Stop price is lower than price multiplier floor + '-4400': PermissionDenied, # Futures Trading Quantitative Rules violated, only reduceOnly order is allowed, please try again later. + '-4401': PermissionDenied, # Compliance restricted account permission: can only place reduceOnly order. + '-4402': PermissionDenied, # Dear user, our Terms of Use and compliance with local regulations, self feature is currently not available in your region. + '-4403': PermissionDenied, # Dear user, our Terms of Use and compliance with local regulations, the leverage can only up to %sx in your region + # + # 5xxx + # + '-5021': OrderNotFillable, # Due to the order could not be filled immediately, the FOK order has been rejected. + '-5022': OrderNotFillable, # Due to the order could not be executed, the Post Only order will be rejected. + '-5024': OperationRejected, # Symbol is not in trading status. Order amendment is not permitted. + '-5025': OperationRejected, # Only limit order is supported. + '-5026': OperationRejected, # Exceed maximum modify order limit. + '-5027': OperationRejected, # No need to modify the order. + '-5028': BadRequest, # Timestamp for self request is outside of the ME recvWindow. + '-5037': BadRequest, # Invalid price match + '-5038': BadRequest, # Price match only supports order type: LIMIT, STOP AND TAKE_PROFIT + '-5039': BadRequest, # Invalid self trade prevention mode + '-5040': BadRequest, # The goodTillDate timestamp must be greater than the current time plus 600 seconds and smaller than 253402300799000 + '-5041': OperationFailed, # No depth matches self BBO order + }, + }, + 'inverse': { + 'exact': { + # + # 1xxx + # + '-1005': PermissionDenied, # {"code":-1005,"msg":"No such IP has been white listed"} + '-1011': PermissionDenied, # {"code":-1011,"msg":"This IP cannot access self route."} + '-1023': BadRequest, # {"code":-1023,"msg":"Start time is greater than end time."} + '-1109': AuthenticationError, # {"code":-1109,"msg":"Invalid account."} + '-1110': BadSymbol, # {"code":-1110,"msg":"Invalid symbolType."} + '-1113': BadRequest, # {"code":-1113,"msg":"Withdrawal amount must be negative."} + '-1128': BadRequest, # {"code":-1128,"msg":"Combination of optional parameters invalid."} + '-1136': BadRequest, # {"code":-1136,"msg":"Invalid newOrderRespType"} + # + # 2xxx + # + '-2016': OperationRejected, # {"code":-2016,"msg":"No trading window could be found for the symbol. Try ticker/24hrs instead."} + '-2018': InsufficientFunds, # {"code":-2018,"msg":"Balance is insufficient"} + '-2019': InsufficientFunds, # {"code":-2019,"msg":"Margin is insufficient."} + '-2020': OperationFailed, # {"code":-2020,"msg":"Unable to fill."} + '-2021': OrderImmediatelyFillable, # {"code":-2021,"msg":"Order would immediately trigger."} + '-2022': InvalidOrder, # {"code":-2022,"msg":"ReduceOnly Order is rejected."} + '-2023': OperationFailed, # {"code":-2023,"msg":"User in liquidation mode now."} + '-2024': BadRequest, # {"code":-2024,"msg":"Position is not sufficient."} + '-2025': OperationRejected, # {"code":-2025,"msg":"Reach max open order limit."} + '-2026': InvalidOrder, # {"code":-2026,"msg":"This OrderType is not supported when reduceOnly."} + '-2027': OperationRejected, # {"code":-2027,"msg":"Exceeded the maximum allowable position at current leverage."} + '-2028': OperationRejected, # {"code":-2028,"msg":"Leverage is smaller than permitted: insufficient margin balance"} + # + # 4xxx + # + '-4086': BadRequest, # Invalid price spread threshold. + '-4087': BadSymbol, # Invalid pair + '-4088': BadRequest, # Invalid time interval + '-4089': PermissionDenied, # User can only place reduce only order. + '-4090': PermissionDenied, # User can not place order currently. + '-4110': BadRequest, # clientTranId is not valid + '-4111': BadRequest, # clientTranId is duplicated. + '-4112': OperationRejected, # ReduceOnly Order Failed. Please check your existing position and open orders. + '-4113': OperationRejected, # The counterparty's best price does not meet the PERCENT_PRICE filter limit. + '-4150': OperationRejected, # Leverage reduction is not supported in Isolated Margin Mode with open positions. + '-4151': BadRequest, # Price is higher than stop price multiplier cap. + '-4152': BadRequest, # Price is lower than stop price multiplier floor. + '-4154': BadRequest, # Stop price is higher than price multiplier cap. + '-4155': BadRequest, # Stop price is lower than price multiplier floor + '-4178': BadRequest, # Order's notional must be no smaller than one(unless you choose reduce only) + '-4188': BadRequest, # Timestamp for self request is outside of the ME recvWindow. + '-4192': PermissionDenied, # Trade forbidden due to Cooling-off Period. + '-4194': PermissionDenied, # Intermediate Personal Verification is required for adjusting leverage over 20x. + '-4195': PermissionDenied, # More than 20x leverage is available one month after account registration. + '-4196': BadRequest, # Only limit order is supported. + '-4197': OperationRejected, # No need to modify the order. + '-4198': OperationRejected, # Exceed maximum modify order limit. + '-4199': BadRequest, # Symbol is not in trading status. Order amendment is not permitted. + '-4200': PermissionDenied, # More than 20x leverage is available %s days after Futures account registration. + '-4201': PermissionDenied, # Users in your location/country can only access a maximum leverage of %s + '-4202': OperationRejected, # Current symbol leverage cannot exceed 20 when using position limit adjustment service. + }, + }, + 'option': { + 'exact': { + # + # 1xxx + # + '-1003': ExchangeError, # override common + '-1004': ExchangeError, # override common + '-1006': ExchangeError, # override common + '-1007': ExchangeError, # override common + '-1008': RateLimitExceeded, # TOO_MANY_REQUEST + '-1010': ExchangeError, # override common + '-1013': ExchangeError, # override common + '-1108': ExchangeError, # override common + '-1112': ExchangeError, # override common + '-1114': ExchangeError, # override common + '-1128': BadSymbol, # BAD_CONTRACT + '-1129': BadSymbol, # BAD_CURRENCY + '-1131': BadRequest, # {"code":-1131,"msg":"recvWindow must be less than 60000"} + # + # 2xxx + # + '-2011': ExchangeError, # override common + '-2018': InsufficientFunds, # BALANCE_NOT_SUFFICIENT + '-2027': InsufficientFunds, # OPTION_MARGIN_NOT_SUFFICIENT + # + # 3xxx + # + '-3029': OperationFailed, # {"code":-3029,"msg":"Transfer failed."} + # + # 4xxx + # + # -4001 inherited + # -4002 inherited + # -4003 inherited + # -4004 inherited + # -4005 inherited + '-4006': ExchangeError, # override commons + '-4007': ExchangeError, # override commons + '-4008': ExchangeError, # override commons + '-4009': ExchangeError, # override commons + '-4010': ExchangeError, # override commons + '-4011': ExchangeError, # override commons + '-4012': ExchangeError, # override commons + # -4013 inherited + '-4014': ExchangeError, # override commons + '-4015': ExchangeError, # override commons + '-4016': ExchangeError, # override commons + '-4017': ExchangeError, # override commons + '-4018': ExchangeError, # override commons + '-4019': ExchangeError, # override commons + '-4020': ExchangeError, # override commons + '-4021': ExchangeError, # override commons + '-4022': ExchangeError, # override commons + '-4023': ExchangeError, # override commons + '-4024': ExchangeError, # override commons + '-4025': ExchangeError, # override commons + '-4026': ExchangeError, # override commons + '-4027': ExchangeError, # override commons + '-4028': ExchangeError, # override commons + # -4029 inherited + # -4030 inherited + '-4031': ExchangeError, # override commons + '-4032': ExchangeError, # override commons + '-4033': ExchangeError, # override commons + '-4034': ExchangeError, # override commons + '-4035': ExchangeError, # override commons + '-4036': ExchangeError, # override commons + '-4037': ExchangeError, # override commons + '-4038': ExchangeError, # override commons + '-4039': ExchangeError, # override commons + '-4040': ExchangeError, # override commons + '-4041': ExchangeError, # override commons + '-4042': ExchangeError, # override commons + '-4043': ExchangeError, # override commons + '-4044': ExchangeError, # override commons + '-4045': ExchangeError, # override commons + '-4046': ExchangeError, # override commons + '-4047': ExchangeError, # override commons + '-4048': ExchangeError, # override commons + '-4049': ExchangeError, # override commons + '-4050': ExchangeError, # override commons + '-4051': ExchangeError, # override commons + '-4052': ExchangeError, # override commons + '-4053': ExchangeError, # override commons + '-4054': ExchangeError, # override commons + # -4055 inherited + '-4056': ExchangeError, # override commons + '-4057': ExchangeError, # override commons + '-4058': ExchangeError, # override commons + '-4059': ExchangeError, # override commons + '-4060': ExchangeError, # override commons + '-4061': ExchangeError, # override commons + '-4062': ExchangeError, # override commons + '-4063': ExchangeError, # override commons + '-4064': ExchangeError, # override commons + '-4065': ExchangeError, # override commons + '-4066': ExchangeError, # override commons + '-4067': ExchangeError, # override commons + '-4068': ExchangeError, # override commons + '-4069': ExchangeError, # override commons + '-4070': ExchangeError, # override commons + '-4071': ExchangeError, # override commons + '-4072': ExchangeError, # override commons + '-4073': ExchangeError, # override commons + '-4074': ExchangeError, # override commons + '-4075': ExchangeError, # override commons + '-4076': ExchangeError, # override commons + '-4077': ExchangeError, # override commons + '-4078': ExchangeError, # override commons + '-4079': ExchangeError, # override commons + '-4080': ExchangeError, # override commons + '-4081': ExchangeError, # override commons + '-4082': ExchangeError, # override commons + '-4083': ExchangeError, # override commons + '-4084': ExchangeError, # override commons + '-4085': ExchangeError, # override commons + '-4086': ExchangeError, # override commons + '-4087': ExchangeError, # override commons + '-4088': ExchangeError, # override commons + '-4089': ExchangeError, # override commons + '-4091': ExchangeError, # override commons + '-4092': ExchangeError, # override commons + '-4093': ExchangeError, # override commons + '-4094': ExchangeError, # override commons + '-4095': ExchangeError, # override commons + '-4096': ExchangeError, # override commons + '-4097': ExchangeError, # override commons + '-4098': ExchangeError, # override commons + '-4099': ExchangeError, # override commons + '-4101': ExchangeError, # override commons + '-4102': ExchangeError, # override commons + '-4103': ExchangeError, # override commons + '-4104': ExchangeError, # override commons + '-4105': ExchangeError, # override commons + '-4106': ExchangeError, # override commons + '-4107': ExchangeError, # override commons + '-4108': ExchangeError, # override commons + '-4109': ExchangeError, # override commons + '-4110': ExchangeError, # override commons + '-4112': ExchangeError, # override commons + '-4113': ExchangeError, # override commons + '-4114': ExchangeError, # override commons + '-4115': ExchangeError, # override commons + '-4116': ExchangeError, # override commons + '-4117': ExchangeError, # override commons + '-4118': ExchangeError, # override commons + '-4119': ExchangeError, # override commons + '-4120': ExchangeError, # override commons + '-4121': ExchangeError, # override commons + '-4122': ExchangeError, # override commons + '-4123': ExchangeError, # override commons + '-4124': ExchangeError, # override commons + '-4125': ExchangeError, # override commons + '-4126': ExchangeError, # override commons + '-4127': ExchangeError, # override commons + '-4128': ExchangeError, # override commons + '-4129': ExchangeError, # override commons + '-4130': ExchangeError, # override commons + '-4131': ExchangeError, # override commons + '-4132': ExchangeError, # override commons + '-4133': ExchangeError, # override commons + '-4134': ExchangeError, # override commons + '-4135': ExchangeError, # override commons + '-4136': ExchangeError, # override commons + '-4137': ExchangeError, # override commons + '-4138': ExchangeError, # override commons + '-4139': ExchangeError, # override commons + '-4141': ExchangeError, # override commons + '-4142': ExchangeError, # override commons + '-4143': ExchangeError, # override commons + '-4144': ExchangeError, # override commons + '-4145': ExchangeError, # override commons + '-4146': ExchangeError, # override commons + '-4147': ExchangeError, # override commons + '-4148': ExchangeError, # override commons + '-4149': ExchangeError, # override commons + '-4150': ExchangeError, # override commons + # + # 2xxxx + # + '-20121': ExchangeError, # override commons + '-20124': ExchangeError, # override commons + '-20130': ExchangeError, # override commons + '-20132': ExchangeError, # override commons + '-20194': ExchangeError, # override commons + '-20195': ExchangeError, # override commons + '-20196': ExchangeError, # override commons + '-20198': ExchangeError, # override commons + '-20204': ExchangeError, # override commons + }, + }, + 'portfolioMargin': { + 'exact': { + # + # 10xx General Server or Network Issues + # + '-1000': OperationFailed, # An unknown error occured while processing the request. + '-1001': ExchangeError, # Internal error; unable to process your request. Please try again. + '-1002': PermissionDenied, # You are not authorized to execute self request. + '-1003': RateLimitExceeded, # Too many requests use the websocket for live updates to avoid polling the API. + '-1004': BadRequest, # This IP is already on the white list. + '-1005': PermissionDenied, # No such IP has been white listed. + '-1006': BadResponse, # An unexpected response was received from the message bus. Execution status unknown. + '-1007': BadResponse, # Timeout waiting for response from backend server. Send status unknown, execution status unknown. + '-1008': OperationFailed, # WS Spot server is currently overloaded with other requests. Please try again in a few minutes. + '-1010': ExchangeError, # ERROR_MSG_RECEIVED + '-1011': PermissionDenied, # This IP cannot access self route. + '-1013': ExchangeError, # INVALID_MESSAGE. + '-1014': InvalidOrder, # Unsupported order combination. + '-1015': InvalidOrder, # Too many new orders. + '-1016': NotSupported, # This service is no longer available. + '-1020': NotSupported, # This operation is not supported. + '-1021': BadRequest, # Timestamp for self request is outside of the recvWindow 1000ms ahead of the servers time. + '-1022': BadRequest, # Signature for self request is not valid. + '-1023': BadRequest, # Start time is greater than end time + '-1099': OperationFailed, # WS not found authenticated or authorized + # + # 11xx Request Issues + # + '-1100': BadRequest, # Illegal characters found in a parameter. + '-1101': BadRequest, # Too many parameters sent for self endpoint. + '-1102': BadRequest, # A mandatory parameter was not sent, was empty/null, or malformed. + '-1103': BadRequest, # An unknown parameter was sent. + '-1104': BadRequest, # Not all sent parameters were read. + '-1105': BadRequest, # A parameter was empty. + '-1106': BadRequest, # A parameter was sent when not required. + '-1108': BadRequest, # Invalid asset. + '-1109': BadRequest, # Invalid account. + '-1110': BadSymbol, # Invalid symbolType. + '-1111': BadRequest, # Precision is over the maximum defined for self asset. + '-1112': BadRequest, # No orders on book for symbol. + '-1113': BadRequest, # Withdrawal amount must be negative. + '-1114': BadRequest, # TimeInForce parameter sent when not required. + '-1115': BadRequest, # Invalid timeInForce. + '-1116': BadRequest, # Invalid orderType. + '-1117': BadRequest, # Invalid side. + '-1118': BadRequest, # New client order ID was empty. + '-1119': BadRequest, # Original client order ID was empty. + '-1120': BadRequest, # Invalid interval. + '-1121': BadSymbol, # Invalid symbol. + '-1125': BadRequest, # This listenKey does not exist. + '-1127': BadRequest, # Lookup interval is too big. + '-1128': BadRequest, # Combination of optional parameters invalid. + '-1130': BadRequest, # Invalid data sent for a parameter. + '-1131': BadRequest, # WS recvWindow must be less than 60000 + '-1134': BadRequest, # WS strategyType was less than 1000000. + '-1136': BadRequest, # Invalid newOrderRespType. + '-1145': BadRequest, # WS cancelRestrictions has to be either ONLY_NEW or ONLY_PARTIALLY_FILLED. + '-1151': BadRequest, # WS Symbol is present multiple times in the list. + # + # 20xx Processing Issues + # + '-2010': InvalidOrder, # NEW_ORDER_REJECTED + '-2011': OperationRejected, # CANCEL_REJECTED + '-2013': OrderNotFound, # Order does not exist. + '-2014': OperationRejected, # API-key format invalid. + '-2015': OperationRejected, # Invalid API-key, IP, or permissions for action. + '-2016': OperationFailed, # No trading window could be found for the symbol. Try ticker/24hrs instead. + '-2018': OperationFailed, # Balance is insufficient. + '-2019': OperationFailed, # Margin is insufficient. + '-2020': OrderNotFillable, # Unable to fill. + '-2021': OrderImmediatelyFillable, # Order would immediately trigger. + '-2022': InvalidOrder, # ReduceOnly Order is rejected. + '-2023': OperationFailed, # User in liquidation mode now. + '-2024': OperationRejected, # Position is not sufficient. + '-2025': OperationRejected, # Reach max open order limit. + '-2026': InvalidOrder, # This OrderType is not supported when reduceOnly. + '-2027': OperationRejected, # Exceeded the maximum allowable position at current leverage. + '-2028': OperationRejected, # Leverage is smaller than permitted: insufficient margin balance. + # + # 4xxx Filters and other issues + # + '-4000': BadRequest, # Invalid order status. + '-4001': BadRequest, # Price less than 0. + '-4002': BadRequest, # Price greater than max price. + '-4003': BadRequest, # Quantity less than zero. + '-4004': BadRequest, # Quantity less than min quantity. + '-4005': BadRequest, # Quantity greater than max quantity. + '-4006': BadRequest, # Stop price less than zero. + '-4007': BadRequest, # Stop price greater than max price. + '-4008': BadRequest, # Tick size less than zero. + '-4009': BadRequest, # Max price less than min price. + '-4010': BadRequest, # Max qty less than min qty. + '-4011': BadRequest, # Step size less than zero. + '-4012': BadRequest, # Max mum orders less than zero. + '-4013': BadRequest, # Price less than min price. + '-4014': BadRequest, # Price not increased by tick size. + '-4015': BadRequest, # Client order id is not valid. + '-4016': BadRequest, # Price is higher than mark price multiplier cap. + '-4017': BadRequest, # Multiplier up less than zero. + '-4018': BadRequest, # Multiplier down less than zero. + '-4019': BadRequest, # Composite scale too large. + '-4020': BadRequest, # Target strategy invalid for orderType '%s',reduceOnly '%b'. + '-4021': BadRequest, # Invalid depth limit. + '-4022': BadRequest, # market status sent is not valid. + '-4023': BadRequest, # Qty not increased by step size. + '-4024': BadRequest, # Price is lower than mark price multiplier floor. + '-4025': BadRequest, # Multiplier decimal less than zero. + '-4026': BadRequest, # Commission invalid. + '-4027': BadRequest, # Invalid account type. + '-4028': BadRequest, # Invalid leverage + '-4029': BadRequest, # Tick size precision is invalid. + '-4030': BadRequest, # Step size precision is invalid. + '-4031': BadRequest, # Invalid parameter working type + '-4032': BadRequest, # Exceed maximum cancel order size. + '-4033': BadRequest, # Insurance account not found. + '-4044': BadRequest, # Balance Type is invalid. + '-4045': BadRequest, # Reach max stop order limit. + '-4046': BadRequest, # No need to change margin type. + '-4047': BadRequest, # Margin type cannot be changed if there exists open orders. + '-4048': BadRequest, # Margin type cannot be changed if there exists position. + '-4049': BadRequest, # Add margin only support for isolated position. + '-4050': BadRequest, # Cross balance insufficient. + '-4051': BadRequest, # Isolated balance insufficient. + '-4052': BadRequest, # No need to change auto add margin. + '-4053': BadRequest, # Auto add margin only support for isolated position. + '-4054': BadRequest, # Cannot add position margin: position is 0. + '-4055': BadRequest, # Amount must be positive. + '-4056': PermissionDenied, # Invalid api key type. + '-4057': PermissionDenied, # Invalid api public key + '-4058': BadRequest, # maxPrice and priceDecimal too large,please check. + '-4059': BadRequest, # No need to change position side. + '-4060': BadRequest, # Invalid position side. + '-4061': InvalidOrder, # Order's position side does not match user's setting. + '-4062': BadRequest, # Invalid or improper reduceOnly value. + '-4063': BadRequest, # Invalid options request type + '-4064': BadRequest, # Invalid options time frame + '-4065': BadRequest, # Invalid options amount + '-4066': BadRequest, # Invalid options event type + '-4067': BadRequest, # Position side cannot be changed if there exists open orders. + '-4068': BadRequest, # Position side cannot be changed if there exists position. + '-4069': BadRequest, # Invalid options premium fee + '-4070': BadRequest, # Client options id is not valid. + '-4071': BadRequest, # Invalid options direction + '-4072': OperationRejected, # premium fee is not updated, reject order + '-4073': BadRequest, # input premium fee is less than 0, reject order + '-4074': BadRequest, # Order amount is bigger than upper boundary or less than 0, reject order + '-4075': BadRequest, # output premium fee is less than 0, reject order + '-4076': OperationRejected, # original fee is too much higher than last fee + '-4077': OperationRejected, # place order amount has reached to limit, reject order + '-4078': OperationFailed, # options internal error + '-4079': BadRequest, # invalid options id + '-4080': PermissionDenied, # user not found + '-4081': BadRequest, # options not found + '-4082': BadRequest, # Invalid number of batch place orders. + '-4083': BadRequest, # Fail to place batch orders. + '-4084': NotSupported, # Method is not allowed currently. Upcoming soon. + '-4085': BadRequest, # Invalid notional limit coefficient + '-4086': BadRequest, # Invalid price spread threshold + '-4087': PermissionDenied, # User can only place reduce only order + '-4088': PermissionDenied, # User can not place order currently + '-4104': BadRequest, # Invalid contract type + '-4114': BadRequest, # clientTranId is not valid + '-4115': BadRequest, # clientTranId is duplicated + '-4118': OperationRejected, # ReduceOnly Order Failed. Please check your existing position and open orders + '-4131': OperationRejected, # The counterparty's best price does not meet the PERCENT_PRICE filter limit + '-4135': BadRequest, # Invalid activation price + '-4137': BadRequest, # Quantity must be zero with closePosition equals True + '-4138': BadRequest, # Reduce only must be True with closePosition equals True + '-4139': BadRequest, # Order type can not be market if it's unable to cancel + '-4140': OrderImmediatelyFillable, # Invalid symbol status for opening position + '-4141': BadRequest, # Symbol is closed + '-4142': OrderImmediatelyFillable, # REJECT: take profit or stop order will be triggered immediately + '-4144': BadSymbol, # Invalid pair + '-4161': OperationRejected, # Leverage reduction is not supported in Isolated Margin Mode with open positions + '-4164': InvalidOrder, # Order's notional must be no smaller than 5.0(unless you choose reduce only) + '-4165': BadRequest, # Invalid time interval + '-4183': InvalidOrder, # Price is higher than stop price multiplier cap. + '-4184': InvalidOrder, # Price is lower than stop price multiplier floor. + '-4408': InvalidOrder, # This symbol is in reduce only mode due to regulation requirements. Please upgrade to Binance Credits Trading Mode. + # + # 5xxx Order Execution Issues + # + '-5021': OrderNotFillable, # Due to the order could not be filled immediately, the FOK order has been rejected. + '-5022': OrderNotFillable, # Due to the order could not be executed, the Post Only order will be rejected. + '-5028': OperationFailed, # The requested timestamp is outside the recvWindow of the matching engine + '-5041': RateLimitExceeded, # Time out for too many requests from self account queueing at the same time. + }, + }, + 'exact': { + # error codes to cover ALL market types(however, specific market type might have override) + # + # 1xxx + # + '-1000': OperationFailed, # {"code":-1000,"msg":"An unknown error occured while processing the request."} + '-1001': OperationFailed, # {"code":-1001,"msg":"'Internal error; unable to process your request. Please try again.'"} + '-1002': AuthenticationError, # {"code":-1002,"msg":"'You are not authorized to execute self request.'"} + '-1003': RateLimitExceeded, # {"code":-1003,"msg":"Too much request weight used, current limit is 1200 request weight per 1 MINUTE. Please use the websocket for live updates to avoid polling the API."} + '-1004': OperationRejected, # DUPLICATE_IP : This IP is already on the white list + '-1006': OperationFailed, # {"code":-1006,"msg":"An unexpected response was received from the message bus. Execution status unknown."} + '-1007': RequestTimeout, # {"code":-1007,"msg":"Timeout waiting for response from backend server. Send status unknown; execution status unknown."} + '-1010': OperationFailed, # {"code":-1010,"msg":"ERROR_MSG_RECEIVED."} + '-1013': BadRequest, # INVALID_MESSAGE + '-1014': InvalidOrder, # {"code":-1014,"msg":"Unsupported order combination."} + '-1015': RateLimitExceeded, # {"code":-1015,"msg":"'Too many new orders; current limit is %s orders per %s.'"} + '-1016': BadRequest, # {"code":-1016,"msg":"'This service is no longer available.',"} + '-1020': BadRequest, # {"code":-1020,"msg":"'This operation is not supported.'"} + '-1021': InvalidNonce, # {"code":-1021,"msg":"'your time is ahead of server'"} + '-1022': AuthenticationError, # {"code":-1022,"msg":"Signature for self request is not valid."} + '-1100': BadRequest, # {"code":-1100,"msg":"createOrder(symbol, 1, asdf) -> 'Illegal characters found in parameter 'price'"} + '-1101': BadRequest, # {"code":-1101,"msg":"Too many parameters; expected %s and received %s."} + '-1102': BadRequest, # {"code":-1102,"msg":"Param %s or %s must be sent, but both were empty"} + '-1103': BadRequest, # {"code":-1103,"msg":"An unknown parameter was sent."} + '-1104': BadRequest, # {"code":-1104,"msg":"Not all sent parameters were read, read 8 parameters but was sent 9"} + '-1105': BadRequest, # {"code":-1105,"msg":"Parameter %s was empty."} + '-1106': BadRequest, # {"code":-1106,"msg":"Parameter %s sent when not required."} + '-1108': BadSymbol, # {"code":-1108,"msg":"Invalid asset."} + '-1111': BadRequest, # {"code":-1111,"msg":"Precision is over the maximum defined for self asset."} + '-1112': OperationFailed, # {"code":-1112,"msg":"No orders on book for symbol."} + '-1114': BadRequest, # {"code":-1114,"msg":"TimeInForce parameter sent when not required."} + '-1115': BadRequest, # {"code":-1115,"msg":"Invalid timeInForce."} + '-1116': BadRequest, # {"code":-1116,"msg":"Invalid orderType."} + '-1117': BadRequest, # {"code":-1117,"msg":"Invalid side."} + '-1118': BadRequest, # {"code":-1118,"msg":"New client order ID was empty."} + '-1119': BadRequest, # {"code":-1119,"msg":"Original client order ID was empty."} + '-1120': BadRequest, # {"code":-1120,"msg":"Invalid interval."} + '-1121': BadSymbol, # {"code":-1121,"msg":"Invalid symbol."} + '-1125': AuthenticationError, # {"code":-1125,"msg":"This listenKey does not exist."} + '-1127': BadRequest, # {"code":-1127,"msg":"More than %s hours between startTime and endTime."} + '-1128': BadRequest, # {"code":-1128,"msg":"Combination of optional parameters invalid."} + '-1130': BadRequest, # {"code":-1130,"msg":"Data sent for paramter %s is not valid."} + # + # 2xxx + # + '-2010': InvalidOrder, # NEW_ORDER_REJECTED + '-2011': OrderNotFound, # {"code":-2011,"msg":"cancelOrder(1, 'BTC/USDT') -> 'UNKNOWN_ORDER'"} + '-2013': OrderNotFound, # {"code":-2013,"msg":"fetchOrder(1, 'BTC/USDT') -> 'Order does not exist'"} + '-2014': AuthenticationError, # {"code":-2014,"msg":"API-key format invalid."} + '-2015': AuthenticationError, # {"code":-2015,"msg":"Invalid API-key, IP, or permissions for action."} + # + # 4xxx(common for linear, inverse, pm) + # + '-4000': InvalidOrder, # INVALID_ORDER_STATUS + '-4001': BadRequest, # PRICE_LESS_THAN_ZERO + '-4002': BadRequest, # PRICE_GREATER_THAN_MAX_PRICE + '-4003': BadRequest, # QTY_LESS_THAN_ZERO + '-4004': BadRequest, # QTY_LESS_THAN_MIN_QTY + '-4005': BadRequest, # QTY_GREATER_THAN_MAX_QTY + '-4006': BadRequest, # STOP_PRICE_LESS_THAN_ZERO + '-4007': BadRequest, # STOP_PRICE_GREATER_THAN_MAX_PRICE + '-4008': BadRequest, # TICK SIZE LESS THAN ZERO + '-4009': BadRequest, # MAX_PRICE_LESS_THAN_MIN_PRICE + '-4010': BadRequest, # MAX_QTY_LESS_THAN_MIN_QTY + '-4011': BadRequest, # STEP_SIZE_LESS_THAN_ZERO + '-4012': BadRequest, # MAX_NUM_ORDERS_LESS_THAN_ZERO + '-4013': BadRequest, # PRICE_LESS_THAN_MIN_PRICE + '-4014': BadRequest, # PRICE NOT INCREASED BY TICK SIZE + '-4015': BadRequest, # Client order id is not valid + '-4016': BadRequest, # Price is higher than mark price multiplier cap. + '-4017': BadRequest, # MULTIPLIER_UP_LESS_THAN_ZERO + '-4018': BadRequest, # MULTIPLIER_DOWN_LESS_THAN_ZERO + '-4019': OperationRejected, # COMPOSITE_SCALE_OVERFLOW + '-4020': BadRequest, # TARGET_STRATEGY_INVALID + '-4021': BadRequest, # INVALID_DEPTH_LIMIT + '-4022': BadRequest, # WRONG_MARKET_STATUS + '-4023': BadRequest, # QTY_NOT_INCREASED_BY_STEP_SIZE + '-4024': BadRequest, # PRICE_LOWER_THAN_MULTIPLIER_DOWN + '-4025': BadRequest, # MULTIPLIER_DECIMAL_LESS_THAN_ZERO + '-4026': BadRequest, # COMMISSION_INVALID + '-4027': BadRequest, # INVALID_ACCOUNT_TYPE + '-4028': BadRequest, # INVALID_LEVERAGE + '-4029': BadRequest, # INVALID_TICK SIZE_PRECISION + '-4030': BadRequest, # INVALID_STEP_SIZE_PRECISION + '-4031': BadRequest, # INVALID_WORKING_TYPE + '-4032': OperationRejected, # EXCEED_MAX_CANCEL_ORDER_SIZE(or Invalid parameter working type: %s) + '-4033': BadRequest, # INSURANCE_ACCOUNT_NOT_FOUND + '-4044': BadRequest, # INVALID_BALANCE_TYPE + '-4045': OperationRejected, # MAX_STOP_ORDER_EXCEEDED + '-4046': OperationRejected, # NO_NEED_TO_CHANGE_MARGIN_TYPE + '-4047': OperationRejected, # Margin type cannot be changed if there exists open orders. + '-4048': OperationRejected, # Margin type cannot be changed if there exists position. + '-4049': BadRequest, # Add margin only support for isolated position. + '-4050': InsufficientFunds, # Cross balance insufficient + '-4051': InsufficientFunds, # Isolated balance insufficient. + '-4052': OperationRejected, # No need to change auto add margin. + '-4053': BadRequest, # Auto add margin only support for isolated position. + '-4054': OperationRejected, # Cannot add position margin: position is 0. + '-4055': BadRequest, # Amount must be positive. + '-4056': AuthenticationError, # INVALID_API_KEY_TYPE + '-4057': AuthenticationError, # INVALID_RSA_PUBLIC_KEY: Invalid api public key + '-4058': BadRequest, # MAX_PRICE_TOO_LARGE + '-4059': OperationRejected, # NO_NEED_TO_CHANGE_POSITION_SIDE + '-4060': BadRequest, # INVALID_POSITION_SIDE + '-4061': OperationRejected, # POSITION_SIDE_NOT_MATCH: Order's position side does not match user's setting. + '-4062': BadRequest, # REDUCE_ONLY_CONFLICT: Invalid or improper reduceOnly value. + '-4067': OperationRejected, # Position side cannot be changed if there exists open orders. + '-4068': OperationRejected, # Position side cannot be changed if there exists position. + '-4082': BadRequest, # Invalid number of batch place orders. + '-4083': OperationRejected, # PLACE_BATCH_ORDERS_FAIL : Fail to place batch orders. + '-4084': BadRequest, # UPCOMING_METHOD : Method is not allowed currently. Upcoming soon. + '-4086': BadRequest, # Invalid price spread threshold. + '-4104': BadRequest, # INVALID_CONTRACT_TYPE + '-4135': BadRequest, # Invalid activation price + '-4137': BadRequest, # Quantity must be zero with closePosition equals True + '-4138': BadRequest, # Reduce only must be True with closePosition equals True + '-4139': BadRequest, # Order type can not be market if it's unable to cancel + '-4142': OrderImmediatelyFillable, # REJECT: take profit or stop order will be triggered immediately + # + # 2xxxx + # + # 20xxx - spot & futures algo(TBD for OPTIONS & PORTFOLIO MARGIN) + '-20121': BadSymbol, # Invalid symbol. + '-20124': BadRequest, # Invalid algo id or it has been completed. + '-20130': BadRequest, # Invalid data sent for a parameter + '-20132': BadRequest, # The client algo id is duplicated + '-20194': BadRequest, # Duration is too short to execute all required quantity. + '-20195': BadRequest, # The total size is too small. + '-20196': BadRequest, # The total size is too large. + '-20198': OperationRejected, # Reach the max open orders allowed. + '-20204': BadRequest, # The notional of USD is less or more than the limit. + # + # strings + # + 'System is under maintenance.': OnMaintenance, # {"code":1,"msg":"System is under maintenance."} + 'System abnormality': OperationFailed, # {"code":-1000,"msg":"System abnormality"} + 'You are not authorized to execute self request.': PermissionDenied, # {"msg":"You are not authorized to execute self request."} + 'API key does not exist': AuthenticationError, + 'Order would trigger immediately.': OrderImmediatelyFillable, + 'Stop price would trigger immediately.': OrderImmediatelyFillable, # {"code":-2010,"msg":"Stop price would trigger immediately."} + 'Order would immediately match and take.': OrderImmediatelyFillable, # {"code":-2010,"msg":"Order would immediately match and take."} + 'Account has insufficient balance for requested action.': InsufficientFunds, + 'Rest API trading is not enabled.': PermissionDenied, + 'This account may not place or cancel orders.': PermissionDenied, + "You don't have permission.": PermissionDenied, # {"msg":"You don't have permission.","success":false} + 'Market is closed.': MarketClosed, # {"code":-1013,"msg":"Market is closed."} + 'Too many requests. Please try again later.': RateLimitExceeded, # {"msg":"Too many requests. Please try again later.","success":false} + 'This action is disabled on self account.': AccountSuspended, # {"code":-2011,"msg":"This action is disabled on self account."} + 'Limit orders require GTC for self phase.': BadRequest, + 'This order type is not hasattr(self, possible) trading phase.': BadRequest, + 'This type of sub-account exceeds the maximum number limit': OperationRejected, # {"code":-9000,"msg":"This type of sub-account exceeds the maximum number limit"} + 'This symbol is restricted for self account.': PermissionDenied, + 'This symbol is not permitted for self account.': PermissionDenied, # {"code":-2010,"msg":"This symbol is not permitted for self account."} + }, + 'broad': { + 'has no operation privilege': PermissionDenied, + 'MAX_POSITION': BadRequest, # {"code":-2010,"msg":"Filter failure: MAX_POSITION"} + }, + }, + }) + + def is_inverse(self, type: str, subType: Str = None) -> bool: + if subType is None: + return(type == 'delivery') + else: + return subType == 'inverse' + + def is_linear(self, type: str, subType: Str = None) -> bool: + if subType is None: + return(type == 'future') or (type == 'swap') + else: + return subType == 'linear' + + def set_sandbox_mode(self, enable: bool): + super(binance, self).set_sandbox_mode(enable) + self.options['sandboxMode'] = enable + + def create_expired_option_market(self, symbol: str): + # support expired option contracts + settle = 'USDT' + optionParts = symbol.split('-') + symbolBase = symbol.split('/') + base = None + if symbol.find('/') > -1: + base = self.safe_string(symbolBase, 0) + else: + base = self.safe_string(optionParts, 0) + expiry = self.safe_string(optionParts, 1) + strike = self.safe_integer(optionParts, 2) + strikeAsString = self.safe_string(optionParts, 2) + optionType = self.safe_string(optionParts, 3) + datetime = self.convert_expire_date(expiry) + timestamp = self.parse8601(datetime) + return { + 'id': base + '-' + expiry + '-' + strikeAsString + '-' + optionType, + 'symbol': base + '/' + settle + ':' + settle + '-' + expiry + '-' + strikeAsString + '-' + optionType, + 'base': base, + 'quote': settle, + 'baseId': base, + 'quoteId': settle, + 'active': None, + 'type': 'option', + 'linear': None, + 'inverse': None, + 'spot': False, + 'swap': False, + 'future': False, + 'option': True, + 'margin': False, + 'contract': True, + 'contractSize': None, + 'expiry': timestamp, + 'expiryDatetime': datetime, + 'optionType': 'call' if (optionType == 'C') else 'put', + 'strike': strike, + 'settle': settle, + 'settleId': settle, + 'precision': { + 'amount': None, + 'price': None, + }, + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'info': None, + } + + def market(self, symbol: str) -> MarketInterface: + if self.markets is None: + raise ExchangeError(self.id + ' markets not loaded') + # defaultType has legacy support on binance + defaultType = self.safe_string(self.options, 'defaultType') + defaultSubType = self.safe_string(self.options, 'defaultSubType') + isLegacyLinear = defaultType == 'future' + isLegacyInverse = defaultType == 'delivery' + isLegacy = isLegacyLinear or isLegacyInverse + if isinstance(symbol, str): + if symbol in self.markets: + market = self.markets[symbol] + # begin diff + if isLegacy and market['spot']: + settle = market['quote'] if isLegacyLinear else market['base'] + futuresSymbol = symbol + ':' + settle + if futuresSymbol in self.markets: + return self.markets[futuresSymbol] + else: + return market + # end diff + elif symbol in self.markets_by_id: + markets = self.markets_by_id[symbol] + # begin diff + if isLegacyLinear: + defaultType = 'linear' + elif isLegacyInverse: + defaultType = 'inverse' + elif defaultType is None: + defaultType = defaultSubType + # end diff + for i in range(0, len(markets)): + market = markets[i] + if market[defaultType]: + return market + return markets[0] + elif (symbol.find('/') > -1) and (symbol.find(':') < 0): + if (defaultType is not None) and (defaultType != 'spot'): + # support legacy symbols + base, quote = symbol.split('/') + settle = base if (quote == 'USD') else quote + futuresSymbol = symbol + ':' + settle + if futuresSymbol in self.markets: + return self.markets[futuresSymbol] + elif (symbol.find('-C') > -1) or (symbol.find('-P') > -1): # both exchange-id and unified symbols are supported self way regardless of the defaultType + return self.create_expired_option_market(symbol) + raise BadSymbol(self.id + ' does not have market symbol ' + symbol) + + def safe_market(self, marketId: Str = None, market: Market = None, delimiter: Str = None, marketType: Str = None) -> MarketInterface: + isOption = (marketId is not None) and ((marketId.find('-C') > -1) or (marketId.find('-P') > -1)) + if isOption and not (marketId in self.markets_by_id): + # handle expired option contracts + return self.create_expired_option_market(marketId) + return super(binance, self).safe_market(marketId, market, delimiter, marketType) + + def cost_to_precision(self, symbol, cost): + return self.decimal_to_precision(cost, TRUNCATE, self.markets[symbol]['precision']['quote'], self.precisionMode, self.paddingMode) + + def nonce(self): + return self.milliseconds() - self.options['timeDifference'] + + def enable_demo_trading(self, enable: bool): + """ + enables or disables demo trading mode + + https://www.binance.com/en/support/faq/detail/9be58f73e5e14338809e3b705b9687dd + https://demo.binance.com/en/my/settings/api-management + + :param boolean [enable]: True if demo trading should be enabled, False otherwise + """ + if self.isSandboxModeEnabled: + raise NotSupported(self.id + ' demo trading is not supported in the sandbox environment. Please check https://www.binance.com/en/support/faq/detail/9be58f73e5e14338809e3b705b9687dd to see the differences') + if enable: + self.urls['apiBackupDemoTrading'] = self.urls['api'] + self.urls['api'] = self.urls['demo'] + elif 'apiBackupDemoTrading' in self.urls: + self.urls['api'] = self.urls['apiBackupDemoTrading'] + newUrls = self.omit(self.urls, 'apiBackupDemoTrading') + self.urls = newUrls + self.options['enableDemoTrading'] = enable + + async def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://developers.binance.com/docs/binance-spot-api-docs/rest-api/general-endpoints#check-server-time # spot + https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/Check-Server-Time # swap + https://developers.binance.com/docs/derivatives/coin-margined-futures/market-data/rest-api/Check-Server-time # future + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.subType]: "linear" or "inverse" + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + defaultType = self.safe_string_2(self.options, 'fetchTime', 'defaultType', 'spot') + type = self.safe_string(params, 'type', defaultType) + query = self.omit(params, 'type') + subType = None + subType, params = self.handle_sub_type_and_params('fetchTime', None, params) + response = None + if self.is_linear(type, subType): + response = await self.fapiPublicGetTime(query) + elif self.is_inverse(type, subType): + response = await self.dapiPublicGetTime(query) + else: + response = await self.publicGetTime(query) + return self.safe_integer(response, 'serverTime') + + async def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://developers.binance.com/docs/wallet/capital/all-coins-info + https://developers.binance.com/docs/margin_trading/market-data/Get-All-Margin-Assets + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + fetchCurrenciesEnabled = self.safe_bool(self.options, 'fetchCurrencies') + if not fetchCurrenciesEnabled: + return {} + # self endpoint requires authentication + # while fetchCurrencies is a public API method by design + # therefore we check the keys here + # and fallback to generating the currencies from the markets + if not self.check_required_credentials(False): + return {} + # sandbox/testnet does not support sapi endpoints + apiBackup = self.safe_value(self.urls, 'apiBackup') + if apiBackup is not None: + return {} + # demotrading does not support sapi endpoints + if self.safe_bool(self.options, 'enableDemoTrading', False): + return {} + promises = [self.sapiGetCapitalConfigGetall(params)] + fetchMargins = self.safe_bool(self.options, 'fetchMargins', False) + if fetchMargins: + promises.append(self.sapiGetMarginAllPairs(params)) + results = await asyncio.gather(*promises) + responseCurrencies = results[0] + marginablesById = None + if fetchMargins: + responseMarginables = results[1] + marginablesById = self.index_by(responseMarginables, 'assetName') + result: dict = {} + for i in range(0, len(responseCurrencies)): + # + # { + # "coin": "LINK", + # "depositAllEnable": True, + # "withdrawAllEnable": True, + # "name": "ChainLink", + # "free": "0", + # "locked": "0", + # "freeze": "0", + # "withdrawing": "0", + # "ipoing": "0", + # "ipoable": "0", + # "storage": "0", + # "isLegalMoney": False, + # "trading": True, + # "networkList": [ + # { + # "network": "BSC", + # "coin": "LINK", + # "withdrawIntegerMultiple": "0.00000001", + # "isDefault": False, + # "depositEnable": True, + # "withdrawEnable": True, + # "depositDesc": "", + # "withdrawDesc": "", + # "specialTips": "", + # "specialWithdrawTips": "The network you have selected is BSC. Please ensure that the withdrawal address supports the Binance Smart Chain network. You will lose your assets if the chosen platform does not support retrievals.", + # "name": "BNB Smart Chain(BEP20)", + # "resetAddressStatus": False, + # "addressRegex": "^(0x)[0-9A-Fa-f]{40}$", + # "addressRule": "", + # "memoRegex": "", + # "withdrawFee": "0.012", + # "withdrawMin": "0.024", + # "withdrawMax": "9999999999.99999999", + # "minConfirm": "15", + # "unLockConfirm": "0", + # "sameAddress": False, + # "estimatedArrivalTime": "5", + # "busy": False, + # "country": "AE,BINANCE_BAHRAIN_BSC" + # }, + # { + # "network": "BNB", + # "coin": "LINK", + # "withdrawIntegerMultiple": "0.00000001", + # "isDefault": False, + # "depositEnable": True, + # "withdrawEnable": True, + # "depositDesc": "", + # "withdrawDesc": "", + # "specialTips": "Both a MEMO and an Address are required to successfully deposit your LINK BEP2 tokens to Binance.", + # "specialWithdrawTips": "", + # "name": "BNB Beacon Chain(BEP2)", + # "resetAddressStatus": False, + # "addressRegex": "^(bnb1)[0-9a-z]{38}$", + # "addressRule": "", + # "memoRegex": "^[0-9A-Za-z\\-_]{1,120}$", + # "withdrawFee": "0.003", + # "withdrawMin": "0.01", + # "withdrawMax": "10000000000", + # "minConfirm": "1", + # "unLockConfirm": "0", + # "sameAddress": True, + # "estimatedArrivalTime": "5", + # "busy": False, + # "country": "AE,BINANCE_BAHRAIN_BSC" + # }, + # { + # "network": "ETH", + # "coin": "LINK", + # "withdrawIntegerMultiple": "0.00000001", + # "isDefault": True, + # "depositEnable": True, + # "withdrawEnable": True, + # "depositDesc": "", + # "withdrawDesc": "", + # "name": "Ethereum(ERC20)", + # "resetAddressStatus": False, + # "addressRegex": "^(0x)[0-9A-Fa-f]{40}$", + # "addressRule": "", + # "memoRegex": "", + # "withdrawFee": "0.55", + # "withdrawMin": "1.1", + # "withdrawMax": "10000000000", + # "minConfirm": "12", + # "unLockConfirm": "0", + # "sameAddress": False, + # "estimatedArrivalTime": "5", + # "busy": False, + # "country": "AE,BINANCE_BAHRAIN_BSC" + # } + # ] + # } + # + entry = responseCurrencies[i] + id = self.safe_string(entry, 'coin') + name = self.safe_string(entry, 'name') + code = self.safe_currency_code(id) + isFiat = self.safe_bool(entry, 'isLegalMoney') + minPrecision = None + isWithdrawEnabled = True + isDepositEnabled = True + networkList = self.safe_list(entry, 'networkList', []) + fees: dict = {} + fee = None + networks: dict = {} + for j in range(0, len(networkList)): + networkItem = networkList[j] + network = self.safe_string(networkItem, 'network') + networkCode = self.network_id_to_code(network) + isETF = (network == 'ETF') # e.g. BTCUP, ETHDOWN + # name = self.safe_string(networkItem, 'name') + withdrawFee = self.safe_number(networkItem, 'withdrawFee') + depositEnable = self.safe_bool(networkItem, 'depositEnable') + withdrawEnable = self.safe_bool(networkItem, 'withdrawEnable') + isDepositEnabled = isDepositEnabled or depositEnable + isWithdrawEnabled = isWithdrawEnabled or withdrawEnable + fees[network] = withdrawFee + isDefault = self.safe_bool(networkItem, 'isDefault') + if isDefault or (fee is None): + fee = withdrawFee + # todo: default networks in "setMarkets" overload + # if isDefault: + # self.options['defaultNetworkCodesForCurrencies'][code] = networkCode + # } + precisionTick = self.safe_string(networkItem, 'withdrawIntegerMultiple') + withdrawPrecision = precisionTick + # avoid zero values, which are mostly from fiat or leveraged tokens or some abandoned coins : https://github.com/ccxt/ccxt/pull/14902#issuecomment-1271636731 + if not Precise.string_eq(precisionTick, '0'): + minPrecision = precisionTick if (minPrecision is None) else Precise.string_min(minPrecision, precisionTick) + else: + if not isFiat and not isETF: + # non-fiat and non-ETF currency, there are many cases when precision is set to zero(probably bug, we've reported to binance already) + # in such cases, we can set default precision of 8(which is in UI for such coins) + withdrawPrecision = self.omit_zero(self.safe_string(networkItem, 'withdrawInternalMin')) + if withdrawPrecision is None: + withdrawPrecision = self.safe_string(self.options, 'defaultWithdrawPrecision') + networks[networkCode] = { + 'info': networkItem, + 'id': network, + 'network': networkCode, + 'active': depositEnable and withdrawEnable, + 'deposit': depositEnable, + 'withdraw': withdrawEnable, + 'fee': withdrawFee, + 'precision': self.parse_number(withdrawPrecision), + 'limits': { + 'withdraw': { + 'min': self.safe_number(networkItem, 'withdrawMin'), + 'max': self.safe_number(networkItem, 'withdrawMax'), + }, + 'deposit': { + 'min': self.safe_number(networkItem, 'depositDust'), + 'max': None, + }, + }, + } + trading = self.safe_bool(entry, 'trading') + active = (isWithdrawEnabled and isDepositEnabled and trading) + marginEntry = self.safe_dict(marginablesById, id, {}) + # + # { + # assetName: "BTC", + # assetFullName: "Bitcoin", + # isBorrowable: True, + # isMortgageable: True, + # userMinBorrow: "0", + # userMinRepay: "0", + # } + # + result[code] = { + 'id': id, + 'name': name, + 'code': code, + 'type': 'fiat' if isFiat else 'crypto', + 'precision': self.parse_number(minPrecision), + 'info': entry, + 'active': active, + 'deposit': isDepositEnabled, + 'withdraw': isWithdrawEnabled, + 'networks': networks, + 'fee': fee, + 'fees': fees, + 'limits': self.limits, + 'margin': self.safe_bool(marginEntry, 'isBorrowable'), + } + return result + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for binance + + https://developers.binance.com/docs/binance-spot-api-docs/rest-api/general-endpoints#exchange-information # spot + https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/Exchange-Information # swap + https://developers.binance.com/docs/derivatives/coin-margined-futures/market-data/rest-api/Exchange-Information # future + https://developers.binance.com/docs/derivatives/option/market-data/Exchange-Information # option + https://developers.binance.com/docs/margin_trading/market-data/Get-All-Cross-Margin-Pairs # cross margin + https://developers.binance.com/docs/margin_trading/market-data/Get-All-Isolated-Margin-Symbol # isolated margin + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + promisesRaw = [] + rawFetchMarkets = None + defaultTypes = ['spot', 'linear', 'inverse'] + fetchMarketsOptions = self.safe_dict(self.options, 'fetchMarkets') + if fetchMarketsOptions is not None: + rawFetchMarkets = self.safe_list(fetchMarketsOptions, 'types', defaultTypes) + else: + # for backward-compatibility + rawFetchMarkets = self.safe_list(self.options, 'fetchMarkets', defaultTypes) + # handle loadAllOptions option + loadAllOptions = self.safe_bool(self.options, 'loadAllOptions', False) + if loadAllOptions: + if not self.in_array('option', rawFetchMarkets): + rawFetchMarkets.append('option') + sandboxMode = self.safe_bool(self.options, 'sandboxMode', False) + demoMode = self.safe_bool(self.options, 'enableDemoTrading', False) + isDemoEnv = demoMode or sandboxMode + fetchMarkets = [] + for i in range(0, len(rawFetchMarkets)): + type = rawFetchMarkets[i] + if type == 'option' and isDemoEnv: + continue + fetchMarkets.append(type) + fetchMargins = self.safe_bool(self.options, 'fetchMargins', False) + for i in range(0, len(fetchMarkets)): + marketType = fetchMarkets[i] + if marketType == 'spot': + promisesRaw.append(self.publicGetExchangeInfo(params)) + if fetchMargins and self.check_required_credentials(False) and not isDemoEnv: + promisesRaw.append(self.sapiGetMarginAllPairs(params)) + promisesRaw.append(self.sapiGetMarginIsolatedAllPairs(params)) + elif marketType == 'linear': + promisesRaw.append(self.fapiPublicGetExchangeInfo(params)) + elif marketType == 'inverse': + promisesRaw.append(self.dapiPublicGetExchangeInfo(params)) + elif marketType == 'option': + promisesRaw.append(self.eapiPublicGetExchangeInfo(params)) + else: + raise ExchangeError(self.id + ' fetchMarkets() self.options fetchMarkets "' + marketType + '" is not a supported market type') + results = await asyncio.gather(*promisesRaw) + markets = [] + self.options['crossMarginPairsData'] = [] + self.options['isolatedMarginPairsData'] = [] + for i in range(0, len(results)): + res = self.safe_value(results, i) + if fetchMargins and isinstance(res, list): + keysList = list(self.index_by(res, 'symbol').keys()) + length = len(self.options['crossMarginPairsData']) + # first one is the cross-margin promise + if length == 0: + self.options['crossMarginPairsData'] = keysList + else: + self.options['isolatedMarginPairsData'] = keysList + else: + resultMarkets = self.safe_list_2(res, 'symbols', 'optionSymbols', []) + markets = self.array_concat(markets, resultMarkets) + # + # spot / margin + # + # { + # "timezone":"UTC", + # "serverTime":1575416692969, + # "rateLimits":[ + # {"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":1200}, + # {"rateLimitType":"ORDERS","interval":"SECOND","intervalNum":10,"limit":100}, + # {"rateLimitType":"ORDERS","interval":"DAY","intervalNum":1,"limit":200000} + # ], + # "exchangeFilters":[], + # "symbols":[ + # { + # "symbol":"ETHBTC", + # "status":"TRADING", + # "baseAsset":"ETH", + # "baseAssetPrecision":8, + # "quoteAsset":"BTC", + # "quotePrecision":8, + # "baseCommissionPrecision":8, + # "quoteCommissionPrecision":8, + # "orderTypes":["LIMIT","LIMIT_MAKER","MARKET","STOP_LOSS_LIMIT","TAKE_PROFIT_LIMIT"], + # "icebergAllowed":true, + # "ocoAllowed":true, + # "quoteOrderQtyMarketAllowed":true, + # "allowTrailingStop":false, + # "isSpotTradingAllowed":true, + # "isMarginTradingAllowed":true, + # "filters":[ + # {"filterType":"PRICE_FILTER","minPrice":"0.00000100","maxPrice":"100000.00000000","tickSize":"0.00000100"}, + # {"filterType":"PERCENT_PRICE","multiplierUp":"5","multiplierDown":"0.2","avgPriceMins":5}, + # {"filterType":"LOT_SIZE","minQty":"0.00100000","maxQty":"100000.00000000","stepSize":"0.00100000"}, + # {"filterType":"MIN_NOTIONAL","minNotional":"0.00010000","applyToMarket":true,"avgPriceMins":5}, + # {"filterType":"ICEBERG_PARTS","limit":10}, + # {"filterType":"MARKET_LOT_SIZE","minQty":"0.00000000","maxQty":"63100.00000000","stepSize":"0.00000000"}, + # {"filterType":"MAX_NUM_ORDERS","maxNumOrders":200}, + # {"filterType":"MAX_NUM_ALGO_ORDERS","maxNumAlgoOrders":5} + # ], + # "permissions":["SPOT","MARGIN"]} + # }, + # ], + # } + # + # cross & isolated pairs response: + # + # [ + # { + # symbol: "BTCUSDT", + # base: "BTC", + # quote: "USDT", + # isMarginTrade: True, + # isBuyAllowed: True, + # isSellAllowed: True, + # id: "376870555451677893", # doesn't exist in isolated + # }, + # ] + # + # futures/usdt-margined(fapi) + # + # { + # "timezone":"UTC", + # "serverTime":1575417244353, + # "rateLimits":[ + # {"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":1200}, + # {"rateLimitType":"ORDERS","interval":"MINUTE","intervalNum":1,"limit":1200} + # ], + # "exchangeFilters":[], + # "symbols":[ + # { + # "symbol":"BTCUSDT", + # "status":"TRADING", + # "maintMarginPercent":"2.5000", + # "requiredMarginPercent":"5.0000", + # "baseAsset":"BTC", + # "quoteAsset":"USDT", + # "pricePrecision":2, + # "quantityPrecision":3, + # "baseAssetPrecision":8, + # "quotePrecision":8, + # "filters":[ + # {"minPrice":"0.01","maxPrice":"100000","filterType":"PRICE_FILTER","tickSize":"0.01"}, + # {"stepSize":"0.001","filterType":"LOT_SIZE","maxQty":"1000","minQty":"0.001"}, + # {"stepSize":"0.001","filterType":"MARKET_LOT_SIZE","maxQty":"1000","minQty":"0.001"}, + # {"limit":200,"filterType":"MAX_NUM_ORDERS"}, + # {"multiplierDown":"0.8500","multiplierUp":"1.1500","multiplierDecimal":"4","filterType":"PERCENT_PRICE"} + # ], + # "orderTypes":["LIMIT","MARKET","STOP"], + # "timeInForce":["GTC","IOC","FOK","GTX"] + # } + # ] + # } + # + # delivery/coin-margined(dapi) + # + # { + # "timezone": "UTC", + # "serverTime": 1597667052958, + # "rateLimits": [ + # {"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":6000}, + # {"rateLimitType":"ORDERS","interval":"MINUTE","intervalNum":1,"limit":6000} + # ], + # "exchangeFilters": [], + # "symbols": [ + # { + # "symbol": "BTCUSD_200925", + # "pair": "BTCUSD", + # "contractType": "CURRENT_QUARTER", + # "deliveryDate": 1601020800000, + # "onboardDate": 1590739200000, + # "contractStatus": "TRADING", + # "contractSize": 100, + # "marginAsset": "BTC", + # "maintMarginPercent": "2.5000", + # "requiredMarginPercent": "5.0000", + # "baseAsset": "BTC", + # "quoteAsset": "USD", + # "pricePrecision": 1, + # "quantityPrecision": 0, + # "baseAssetPrecision": 8, + # "quotePrecision": 8, + # "equalQtyPrecision": 4, + # "filters": [ + # {"minPrice":"0.1","maxPrice":"100000","filterType":"PRICE_FILTER","tickSize":"0.1"}, + # {"stepSize":"1","filterType":"LOT_SIZE","maxQty":"100000","minQty":"1"}, + # {"stepSize":"0","filterType":"MARKET_LOT_SIZE","maxQty":"100000","minQty":"1"}, + # {"limit":200,"filterType":"MAX_NUM_ORDERS"}, + # {"multiplierDown":"0.9500","multiplierUp":"1.0500","multiplierDecimal":"4","filterType":"PERCENT_PRICE"} + # ], + # "orderTypes": ["LIMIT","MARKET","STOP","STOP_MARKET","TAKE_PROFIT","TAKE_PROFIT_MARKET","TRAILING_STOP_MARKET"], + # "timeInForce": ["GTC","IOC","FOK","GTX"] + # }, + # { + # "symbol": "BTCUSD_PERP", + # "pair": "BTCUSD", + # "contractType": "PERPETUAL", + # "deliveryDate": 4133404800000, + # "onboardDate": 1596006000000, + # "contractStatus": "TRADING", + # "contractSize": 100, + # "marginAsset": "BTC", + # "maintMarginPercent": "2.5000", + # "requiredMarginPercent": "5.0000", + # "baseAsset": "BTC", + # "quoteAsset": "USD", + # "pricePrecision": 1, + # "quantityPrecision": 0, + # "baseAssetPrecision": 8, + # "quotePrecision": 8, + # "equalQtyPrecision": 4, + # "filters": [ + # {"minPrice":"0.1","maxPrice":"100000","filterType":"PRICE_FILTER","tickSize":"0.1"}, + # {"stepSize":"1","filterType":"LOT_SIZE","maxQty":"100000","minQty":"1"}, + # {"stepSize":"1","filterType":"MARKET_LOT_SIZE","maxQty":"100000","minQty":"1"}, + # {"limit":200,"filterType":"MAX_NUM_ORDERS"}, + # {"multiplierDown":"0.8500","multiplierUp":"1.1500","multiplierDecimal":"4","filterType":"PERCENT_PRICE"} + # ], + # "orderTypes": ["LIMIT","MARKET","STOP","STOP_MARKET","TAKE_PROFIT","TAKE_PROFIT_MARKET","TRAILING_STOP_MARKET"], + # "timeInForce": ["GTC","IOC","FOK","GTX"] + # } + # ] + # } + # + # options(eapi) + # + # { + # "timezone": "UTC", + # "serverTime": 1675912490405, + # "optionContracts": [ + # { + # "id": 1, + # "baseAsset": "SOL", + # "quoteAsset": "USDT", + # "underlying": "SOLUSDT", + # "settleAsset": "USDT" + # }, + # ... + # ], + # "optionAssets": [ + # {"id":1,"name":"USDT"} + # ], + # "optionSymbols": [ + # { + # "contractId": 3, + # "expiryDate": 1677225600000, + # "filters": [ + # {"filterType":"PRICE_FILTER","minPrice":"724.6","maxPrice":"919.2","tickSize":"0.1"}, + # {"filterType":"LOT_SIZE","minQty":"0.01","maxQty":"1001","stepSize":"0.01"} + # ], + # "id": 2474, + # "symbol": "ETH-230224-800-C", + # "side": "CALL", + # "strikePrice": "800.00000000", + # "underlying": "ETHUSDT", + # "unit": 1, + # "makerFeeRate": "0.00020000", + # "takerFeeRate": "0.00020000", + # "minQty": "0.01", + # "maxQty": "1000", + # "initialMargin": "0.15000000", + # "maintenanceMargin": "0.07500000", + # "minInitialMargin": "0.10000000", + # "minMaintenanceMargin": "0.05000000", + # "priceScale": 1, + # "quantityScale": 2, + # "quoteAsset": "USDT" + # }, + # ... + # ], + # "rateLimits": [ + # {"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":400}, + # {"rateLimitType":"ORDERS","interval":"MINUTE","intervalNum":1,"limit":100}, + # {"rateLimitType":"ORDERS","interval":"SECOND","intervalNum":10,"limit":30} + # ] + # } + # + if self.options['adjustForTimeDifference']: + await self.load_time_difference() + result = [] + for i in range(0, len(markets)): + result.append(self.parse_market(markets[i])) + return result + + def parse_market(self, market: dict) -> Market: + swap = False + future = False + option = False + underlying = self.safe_string(market, 'underlying') + id = self.safe_string(market, 'symbol') + optionParts = id.split('-') + optionBase = self.safe_string(optionParts, 0) + lowercaseId = self.safe_string_lower(market, 'symbol') + baseId = self.safe_string(market, 'baseAsset', optionBase) + quoteId = self.safe_string(market, 'quoteAsset') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + contractType = self.safe_string(market, 'contractType') + contract = ('contractType' in market) + expiry = self.safe_integer_2(market, 'deliveryDate', 'expiryDate') + settleId = self.safe_string(market, 'marginAsset') + if (contractType == 'PERPETUAL') or (expiry == 4133404800000): # some swap markets do not have contract type, eg: BTCST + expiry = None + swap = True + elif underlying is not None: + contract = True + option = True + settleId = 'USDT' if (settleId is None) else settleId + elif expiry is not None: + future = True + settle = self.safe_currency_code(settleId) + spot = not contract + filters = self.safe_list(market, 'filters', []) + filtersByType = self.index_by(filters, 'filterType') + status = self.safe_string_2(market, 'status', 'contractStatus') + contractSize = None + fees = self.fees + linear = None + inverse = None + symbol = base + '/' + quote + strike = None + if contract: + if swap: + symbol = symbol + ':' + settle + elif future: + symbol = symbol + ':' + settle + '-' + self.yymmdd(expiry) + elif option: + strike = self.number_to_string(self.parse_to_numeric(self.safe_string(market, 'strikePrice'))) + symbol = symbol + ':' + settle + '-' + self.yymmdd(expiry) + '-' + strike + '-' + self.safe_string(optionParts, 3) + contractSize = self.safe_number_2(market, 'contractSize', 'unit', self.parse_number('1')) + linear = settle == quote + inverse = settle == base + feesType = 'linear' if linear else 'inverse' + fees = self.safe_dict(self.fees, feesType, {}) + active = (status == 'TRADING') + if spot: + permissions = self.safe_list(market, 'permissions', []) + for j in range(0, len(permissions)): + if permissions[j] == 'TRD_GRP_003': + active = False + break + isMarginTradingAllowed = self.safe_bool(market, 'isMarginTradingAllowed', False) + marginModes = None + if spot: + hasCrossMargin = self.in_array(id, self.options['crossMarginPairsData']) + hasIsolatedMargin = self.in_array(id, self.options['isolatedMarginPairsData']) + marginModes = { + 'cross': hasCrossMargin, + 'isolated': hasIsolatedMargin, + } + elif linear or inverse: + marginModes = { + 'cross': True, + 'isolated': True, + } + unifiedType = None + if spot: + unifiedType = 'spot' + elif swap: + unifiedType = 'swap' + elif future: + unifiedType = 'future' + elif option: + unifiedType = 'option' + active = None + parsedStrike = None + if strike is not None: + parsedStrike = self.parse_to_numeric(strike) + entry = { + 'id': id, + 'lowercaseId': lowercaseId, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': unifiedType, + 'spot': spot, + 'margin': spot and isMarginTradingAllowed, + 'marginModes': marginModes, + 'swap': swap, + 'future': future, + 'option': option, + 'active': active, + 'contract': contract, + 'linear': linear, + 'inverse': inverse, + 'taker': fees['trading']['taker'], + 'maker': fees['trading']['maker'], + 'contractSize': contractSize, + 'expiry': expiry, + 'expiryDatetime': self.iso8601(expiry), + 'strike': parsedStrike, + 'optionType': self.safe_string_lower(market, 'side'), + 'precision': { + 'amount': self.parse_number(self.parse_precision(self.safe_string_2(market, 'quantityPrecision', 'quantityScale'))), + 'price': self.parse_number(self.parse_precision(self.safe_string_2(market, 'pricePrecision', 'priceScale'))), + 'base': self.parse_number(self.parse_precision(self.safe_string(market, 'baseAssetPrecision'))), + 'quote': self.parse_number(self.parse_precision(self.safe_string(market, 'quotePrecision'))), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(market, 'minQty'), + 'max': self.safe_number(market, 'maxQty'), + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'info': market, + 'created': self.safe_integer(market, 'onboardDate'), # present in inverse & linear apis + } + if 'PRICE_FILTER' in filtersByType: + filter = self.safe_dict(filtersByType, 'PRICE_FILTER', {}) + # PRICE_FILTER reports zero values for maxPrice + # since they updated filter types in November 2018 + # https://github.com/ccxt/ccxt/issues/4286 + # therefore limits['price']['max'] doesn't have any meaningful value except None + entry['limits']['price'] = { + 'min': self.safe_number(filter, 'minPrice'), + 'max': self.safe_number(filter, 'maxPrice'), + } + entry['precision']['price'] = self.safe_number(filter, 'tickSize') + if 'LOT_SIZE' in filtersByType: + filter = self.safe_dict(filtersByType, 'LOT_SIZE', {}) + entry['precision']['amount'] = self.safe_number(filter, 'stepSize') + entry['limits']['amount'] = { + 'min': self.safe_number(filter, 'minQty'), + 'max': self.safe_number(filter, 'maxQty'), + } + if 'MARKET_LOT_SIZE' in filtersByType: + filter = self.safe_dict(filtersByType, 'MARKET_LOT_SIZE', {}) + entry['limits']['market'] = { + 'min': self.safe_number(filter, 'minQty'), + 'max': self.safe_number(filter, 'maxQty'), + } + if ('MIN_NOTIONAL' in filtersByType) or ('NOTIONAL' in filtersByType): # notional added in 12/04/23 to spot testnet + filter = self.safe_dict_2(filtersByType, 'MIN_NOTIONAL', 'NOTIONAL', {}) + entry['limits']['cost']['min'] = self.safe_number_2(filter, 'minNotional', 'notional') + entry['limits']['cost']['max'] = self.safe_number(filter, 'maxNotional') + return entry + + def parse_balance_helper(self, entry): + account = self.account() + account['used'] = self.safe_string(entry, 'locked') + account['free'] = self.safe_string(entry, 'free') + interest = self.safe_string(entry, 'interest') + debt = self.safe_string(entry, 'borrowed') + account['debt'] = Precise.string_add(debt, interest) + return account + + def parse_balance_custom(self, response, type=None, marginMode=None, isPortfolioMargin=False) -> Balances: + result = { + 'info': response, + } + timestamp = None + isolated = marginMode == 'isolated' + cross = (type == 'margin') or (marginMode == 'cross') + if isPortfolioMargin: + for i in range(0, len(response)): + entry = response[i] + account = self.account() + currencyId = self.safe_string(entry, 'asset') + code = self.safe_currency_code(currencyId) + if type == 'linear': + account['free'] = self.safe_string(entry, 'umWalletBalance') + account['used'] = self.safe_string(entry, 'umUnrealizedPNL') + elif type == 'inverse': + account['free'] = self.safe_string(entry, 'cmWalletBalance') + account['used'] = self.safe_string(entry, 'cmUnrealizedPNL') + elif cross: + borrowed = self.safe_string(entry, 'crossMarginBorrowed') + interest = self.safe_string(entry, 'crossMarginInterest') + account['debt'] = Precise.string_add(borrowed, interest) + account['free'] = self.safe_string(entry, 'crossMarginFree') + account['used'] = self.safe_string(entry, 'crossMarginLocked') + account['total'] = self.safe_string(entry, 'crossMarginAsset') + else: + usedLinear = self.safe_string(entry, 'umUnrealizedPNL') + usedInverse = self.safe_string(entry, 'cmUnrealizedPNL') + totalUsed = Precise.string_add(usedLinear, usedInverse) + totalWalletBalance = self.safe_string(entry, 'totalWalletBalance') + account['total'] = Precise.string_add(totalUsed, totalWalletBalance) + result[code] = account + elif not isolated and ((type == 'spot') or cross): + timestamp = self.safe_integer(response, 'updateTime') + balances = self.safe_list_2(response, 'balances', 'userAssets', []) + for i in range(0, len(balances)): + balance = balances[i] + currencyId = self.safe_string(balance, 'asset') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(balance, 'free') + account['used'] = self.safe_string(balance, 'locked') + if cross: + debt = self.safe_string(balance, 'borrowed') + interest = self.safe_string(balance, 'interest') + account['debt'] = Precise.string_add(debt, interest) + result[code] = account + elif isolated: + assets = self.safe_list(response, 'assets') + for i in range(0, len(assets)): + asset = assets[i] + marketId = self.safe_string(asset, 'symbol') + symbol = self.safe_symbol(marketId, None, None, 'spot') + base = self.safe_dict(asset, 'baseAsset', {}) + quote = self.safe_dict(asset, 'quoteAsset', {}) + baseCode = self.safe_currency_code(self.safe_string(base, 'asset')) + quoteCode = self.safe_currency_code(self.safe_string(quote, 'asset')) + subResult: dict = {} + subResult[baseCode] = self.parse_balance_helper(base) + subResult[quoteCode] = self.parse_balance_helper(quote) + result[symbol] = self.safe_balance(subResult) + elif type == 'savings': + positionAmountVos = self.safe_list(response, 'positionAmountVos', []) + for i in range(0, len(positionAmountVos)): + entry = positionAmountVos[i] + currencyId = self.safe_string(entry, 'asset') + code = self.safe_currency_code(currencyId) + account = self.account() + usedAndTotal = self.safe_string(entry, 'amount') + account['total'] = usedAndTotal + account['used'] = usedAndTotal + result[code] = account + elif type == 'funding': + for i in range(0, len(response)): + entry = response[i] + account = self.account() + currencyId = self.safe_string(entry, 'asset') + code = self.safe_currency_code(currencyId) + account['free'] = self.safe_string(entry, 'free') + frozen = self.safe_string(entry, 'freeze') + withdrawing = self.safe_string(entry, 'withdrawing') + locked = self.safe_string(entry, 'locked') + account['used'] = Precise.string_add(frozen, Precise.string_add(locked, withdrawing)) + result[code] = account + else: + balances = response + if not isinstance(response, list): + balances = self.safe_list(response, 'assets', []) + for i in range(0, len(balances)): + balance = balances[i] + currencyId = self.safe_string(balance, 'asset') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(balance, 'availableBalance') + account['used'] = self.safe_string(balance, 'initialMargin') + account['total'] = self.safe_string_2(balance, 'marginBalance', 'balance') + result[code] = account + result['timestamp'] = timestamp + result['datetime'] = self.iso8601(timestamp) + return result if isolated else self.safe_balance(result) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://developers.binance.com/docs/binance-spot-api-docs/rest-api/account-endpoints#account-information-user_data # spot + https://developers.binance.com/docs/margin_trading/account/Query-Cross-Margin-Account-Details # cross margin + https://developers.binance.com/docs/margin_trading/account/Query-Isolated-Margin-Account-Info # isolated margin + https://developers.binance.com/docs/wallet/asset/funding-wallet # funding + https://developers.binance.com/docs/derivatives/usds-margined-futures/account/rest-api/Futures-Account-Balance-V2 # swap + https://developers.binance.com/docs/derivatives/coin-margined-futures/account/rest-api/Futures-Account-Balance # future + https://developers.binance.com/docs/derivatives/option/account/Option-Account-Information # option + https://developers.binance.com/docs/derivatives/portfolio-margin/account/Account-Balance # portfolio margin + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: 'future', 'delivery', 'savings', 'funding', or 'spot' or 'papi' + :param str [params.marginMode]: 'cross' or 'isolated', for margin trading, uses self.options.defaultMarginMode if not passed, defaults to None/None/None + :param str[]|None [params.symbols]: unified market symbols, only used in isolated margin mode + :param boolean [params.portfolioMargin]: set to True if you would like to fetch the balance for a portfolio margin account + :param str [params.subType]: 'linear' or 'inverse' + :returns dict: a `balance structure ` + """ + await self.load_markets() + defaultType = self.safe_string_2(self.options, 'fetchBalance', 'defaultType', 'spot') + type = self.safe_string(params, 'type', defaultType) + subType = None + subType, params = self.handle_sub_type_and_params('fetchBalance', None, params) + isPortfolioMargin = None + isPortfolioMargin, params = self.handle_option_and_params_2(params, 'fetchBalance', 'papi', 'portfolioMargin', False) + marginMode = None + query = None + marginMode, query = self.handle_margin_mode_and_params('fetchBalance', params) + query = self.omit(query, 'type') + response = None + request: dict = {} + if isPortfolioMargin or (type == 'papi'): + if self.is_linear(type, subType): + type = 'linear' + elif self.is_inverse(type, subType): + type = 'inverse' + isPortfolioMargin = True + response = await self.papiGetBalance(self.extend(request, query)) + elif self.is_linear(type, subType): + type = 'linear' + useV2 = None + useV2, params = self.handle_option_and_params(params, 'fetchBalance', 'useV2', False) + params = self.extend(request, query) + if not useV2: + response = await self.fapiPrivateV3GetAccount(params) + else: + response = await self.fapiPrivateV2GetAccount(params) + elif self.is_inverse(type, subType): + type = 'inverse' + response = await self.dapiPrivateGetAccount(self.extend(request, query)) + elif marginMode == 'isolated': + paramSymbols = self.safe_list(params, 'symbols') + query = self.omit(query, 'symbols') + if paramSymbols is not None: + symbols = '' + if isinstance(paramSymbols, list): + symbols = self.market_id(paramSymbols[0]) + for i in range(1, len(paramSymbols)): + symbol = paramSymbols[i] + id = self.market_id(symbol) + symbols += ',' + id + else: + symbols = paramSymbols + request['symbols'] = symbols + response = await self.sapiGetMarginIsolatedAccount(self.extend(request, query)) + elif (type == 'margin') or (marginMode == 'cross'): + response = await self.sapiGetMarginAccount(self.extend(request, query)) + elif type == 'savings': + response = await self.sapiGetLendingUnionAccount(self.extend(request, query)) + elif type == 'funding': + response = await self.sapiPostAssetGetFundingAsset(self.extend(request, query)) + else: + response = await self.privateGetAccount(self.extend(request, query)) + # + # spot + # + # { + # "makerCommission": 10, + # "takerCommission": 10, + # "buyerCommission": 0, + # "sellerCommission": 0, + # "canTrade": True, + # "canWithdraw": True, + # "canDeposit": True, + # "updateTime": 1575357359602, + # "accountType": "MARGIN", + # "balances": [ + # {asset: "BTC", free: "0.00219821", locked: "0.00000000" }, + # ] + # } + # + # margin(cross) + # + # { + # "borrowEnabled":true, + # "marginLevel":"999.00000000", + # "totalAssetOfBtc":"0.00000000", + # "totalLiabilityOfBtc":"0.00000000", + # "totalNetAssetOfBtc":"0.00000000", + # "tradeEnabled":true, + # "transferEnabled":true, + # "userAssets":[ + # {"asset":"MATIC","borrowed":"0.00000000","free":"0.00000000","interest":"0.00000000","locked":"0.00000000","netAsset":"0.00000000"}, + # {"asset":"VET","borrowed":"0.00000000","free":"0.00000000","interest":"0.00000000","locked":"0.00000000","netAsset":"0.00000000"}, + # {"asset":"USDT","borrowed":"0.00000000","free":"0.00000000","interest":"0.00000000","locked":"0.00000000","netAsset":"0.00000000"} + # ], + # } + # + # margin(isolated) + # + # { + # "info": { + # "assets": [ + # { + # "baseAsset": { + # "asset": "1INCH", + # "borrowEnabled": True, + # "borrowed": "0", + # "free": "0", + # "interest": "0", + # "locked": "0", + # "netAsset": "0", + # "netAssetOfBtc": "0", + # "repayEnabled": True, + # "totalAsset": "0" + # }, + # "quoteAsset": { + # "asset": "USDT", + # "borrowEnabled": True, + # "borrowed": "0", + # "free": "11", + # "interest": "0", + # "locked": "0", + # "netAsset": "11", + # "netAssetOfBtc": "0.00054615", + # "repayEnabled": True, + # "totalAsset": "11" + # }, + # "symbol": "1INCHUSDT", + # "isolatedCreated": True, + # "marginLevel": "999", + # "marginLevelStatus": "EXCESSIVE", + # "marginRatio": "5", + # "indexPrice": "0.59184331", + # "liquidatePrice": "0", + # "liquidateRate": "0", + # "tradeEnabled": True, + # "enabled": True + # }, + # ] + # } + # } + # + # futures(fapi) + # + # fapiPrivateV3GetAccount + # + # { + # "feeTier":0, + # "canTrade":true, + # "canDeposit":true, + # "canWithdraw":true, + # "updateTime":0, + # "totalInitialMargin":"0.00000000", + # "totalMaintMargin":"0.00000000", + # "totalWalletBalance":"0.00000000", + # "totalUnrealizedProfit":"0.00000000", + # "totalMarginBalance":"0.00000000", + # "totalPositionInitialMargin":"0.00000000", + # "totalOpenOrderInitialMargin":"0.00000000", + # "totalCrossWalletBalance":"0.00000000", + # "totalCrossUnPnl":"0.00000000", + # "availableBalance":"0.00000000", + # "maxWithdrawAmount":"0.00000000", + # "assets":[ + # { + # "asset":"BNB", + # "walletBalance":"0.01000000", + # "unrealizedProfit":"0.00000000", + # "marginBalance":"0.01000000", + # "maintMargin":"0.00000000", + # "initialMargin":"0.00000000", + # "positionInitialMargin":"0.00000000", + # "openOrderInitialMargin":"0.00000000", + # "maxWithdrawAmount":"0.01000000", + # "crossWalletBalance":"0.01000000", + # "crossUnPnl":"0.00000000", + # "availableBalance":"0.01000000" + # } + # ], + # "positions":[ + # { + # "symbol":"BTCUSDT", + # "initialMargin":"0", + # "maintMargin":"0", + # "unrealizedProfit":"0.00000000", + # "positionInitialMargin":"0", + # "openOrderInitialMargin":"0", + # "leverage":"21", + # "isolated":false, + # "entryPrice":"0.00000", + # "maxNotional":"5000000", + # "positionSide":"BOTH" + # }, + # ] + # } + # + # fapiPrivateV2GetBalance + # + # [ + # { + # "accountAlias":"FzFzXquXXqoC", + # "asset":"BNB", + # "balance":"0.01000000", + # "crossWalletBalance":"0.01000000", + # "crossUnPnl":"0.00000000", + # "availableBalance":"0.01000000", + # "maxWithdrawAmount":"0.01000000" + # } + # ] + # + # binance pay + # + # [ + # { + # "asset": "BUSD", + # "free": "1129.83", + # "locked": "0", + # "freeze": "0", + # "withdrawing": "0" + # } + # ] + # + # portfolio margin + # + # [ + # { + # "asset": "USDT", + # "totalWalletBalance": "66.9923261", + # "crossMarginAsset": "35.9697141", + # "crossMarginBorrowed": "0.0", + # "crossMarginFree": "35.9697141", + # "crossMarginInterest": "0.0", + # "crossMarginLocked": "0.0", + # "umWalletBalance": "31.022612", + # "umUnrealizedPNL": "0.0", + # "cmWalletBalance": "0.0", + # "cmUnrealizedPNL": "0.0", + # "updateTime": 0, + # "negativeBalance": "0.0" + # }, + # ] + # + return self.parse_balance_custom(response, type, marginMode, isPortfolioMargin) + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://developers.binance.com/docs/binance-spot-api-docs/rest-api/market-data-endpoints#order-book # spot + https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/Order-Book # swap + https://developers.binance.com/docs/derivatives/coin-margined-futures/market-data/rest-api/Order-Book # future + https://developers.binance.com/docs/derivatives/option/market-data/Order-Book # option + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['limit'] = limit # default 100, max 5000, see https://github.com/binance/binance-spot-api-docs/blob/master/rest-api.md#order-book + response = None + if market['option']: + response = await self.eapiPublicGetDepth(self.extend(request, params)) + elif market['linear']: + response = await self.fapiPublicGetDepth(self.extend(request, params)) + elif market['inverse']: + response = await self.dapiPublicGetDepth(self.extend(request, params)) + else: + response = await self.publicGetDepth(self.extend(request, params)) + # + # future + # + # { + # "lastUpdateId":333598053905, + # "E":1618631511986, + # "T":1618631511964, + # "bids":[ + # ["2493.56","20.189"], + # ["2493.54","1.000"], + # ["2493.51","0.005"] + # ], + # "asks":[ + # ["2493.57","0.877"], + # ["2493.62","0.063"], + # ["2493.71","12.054"], + # ] + # } + # + # options(eapi) + # + # { + # "bids": [ + # ["108.7","16.08"], + # ["106","21.29"], + # ["82.4","0.02"] + # ], + # "asks": [ + # ["111.4","19.52"], + # ["119.9","17.6"], + # ["141.2","31"] + # ], + # "T": 1676771382078, + # "u": 1015939 + # } + # + timestamp = self.safe_integer(response, 'T') + orderbook = self.parse_order_book(response, symbol, timestamp) + orderbook['nonce'] = self.safe_integer_2(response, 'lastUpdateId', 'u') + return orderbook + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # markPrices + # + # { + # "symbol": "BTCUSDT", + # "markPrice": "11793.63104561", # mark price + # "indexPrice": "11781.80495970", # index price + # "estimatedSettlePrice": "11781.16138815", # Estimated Settle Price, only useful in the last hour before the settlement starts + # "lastFundingRate": "0.00038246", # This is the lastest estimated funding rate + # "nextFundingTime": 1597392000000, + # "interestRate": "0.00010000", + # "time": 1597370495002 + # } + # + # spot - ticker + # + # { + # "symbol": "BTCUSDT", + # "priceChange": "-188.18000000", + # "priceChangePercent": "-0.159", + # "weightedAvgPrice": "118356.64734074", + # "lastPrice": "118449.03000000", + # "prevClosePrice": "118637.22000000", # field absent in rolling ticker + # "lastQty": "0.00731000", # field absent in rolling ticker + # "bidPrice": "118449.02000000", # field absent in rolling ticker + # "bidQty": "7.15931000", # field absent in rolling ticker + # "askPrice": "118449.03000000", # field absent in rolling ticker + # "askQty": "0.09592000", # field absent in rolling ticker + # "openPrice": "118637.21000000", + # "highPrice": "119273.36000000", + # "lowPrice": "117427.50000000", + # "volume": "14741.41491000", + # "quoteVolume": "1744744445.80640740", + # "openTime": "1753701474013", + # "closeTime": "1753787874013", + # "firstId": "5116031635", + # "lastId": "5117964946", + # "count": "1933312" + # } + # + # usdm tickers + # + # { + # "symbol": "SUSDT", + # "priceChange": "-0.0229000", + # "priceChangePercent": "-6.777", + # "weightedAvgPrice": "0.3210035", + # "lastPrice": "0.3150000", + # "lastQty": "16", + # "openPrice": "0.3379000", + # "highPrice": "0.3411000", + # "lowPrice": "0.3071000", + # "volume": "120588225", + # "quoteVolume": "38709237.2289000", + # "openTime": "1753701720000", + # "closeTime": "1753788172414", + # "firstId": "72234973", + # "lastId": "72423677", + # "count": "188700" + # } + # + # coinm + # + # { + # "baseVolume": "214549.95171161", + # "closeTime": "1621965286847", + # "count": "1283779", + # "firstId": "152560106", + # "highPrice": "39938.3", + # "lastId": "153843955", + # "lastPrice": "37993.4", + # "lastQty": "1", + # "lowPrice": "36457.2", + # "openPrice": "37783.4", + # "openTime": "1621878840000", + # "pair": "BTCUSD", + # "priceChange": "210.0", + # "priceChangePercent": "0.556", + # "symbol": "BTCUSD_PERP", + # "volume": "81990451", + # "weightedAvgPrice": "38215.08713747" + # } + # + # eapi: fetchTicker, fetchTickers + # + # { + # "symbol": "ETH-230510-1825-C", + # "priceChange": "-5.1", + # "priceChangePercent": "-0.1854", + # "lastPrice": "22.4", + # "lastQty": "0", + # "open": "27.5", + # "high": "34.1", + # "low": "22.4", + # "volume": "6.83", + # "amount": "201.44", + # "bidPrice": "21.9", + # "askPrice": "22.4", + # "openTime": 1683614771898, + # "closeTime": 1683695017784, + # "firstTradeId": 12, + # "tradeCount": 22, + # "strikePrice": "1825", + # "exercisePrice": "1845.95341176" + # } + # + # spot bidsAsks + # + # { + # "symbol":"ETHBTC", + # "bidPrice":"0.07466800", + # "bidQty":"5.31990000", + # "askPrice":"0.07466900", + # "askQty":"10.93540000" + # } + # + # usdm bidsAsks + # + # { + # "symbol":"BTCUSDT", + # "bidPrice":"21321.90", + # "bidQty":"33.592", + # "askPrice":"21322.00", + # "askQty":"1.427", + # "time":"1673899207538" + # } + # + # coinm bidsAsks + # + # { + # "symbol":"BTCUSD_PERP", + # "pair":"BTCUSD", + # "bidPrice":"21301.2", + # "bidQty":"188", + # "askPrice":"21301.3", + # "askQty":"10302", + # "time":"1673899278514" + # } + # + timestamp = self.safe_integer_2(ticker, 'closeTime', 'time') + marketType = None + if ('time' in ticker): + marketType = 'contract' + if marketType is None: + marketType = 'spot' if ('bidQty' in ticker) else 'contract' + marketId = self.safe_string(ticker, 'symbol') + symbol = self.safe_symbol(marketId, market, None, marketType) + last = self.safe_string(ticker, 'lastPrice') + wAvg = self.safe_string(ticker, 'weightedAvgPrice') + isCoinm = ('baseVolume' in ticker) + baseVolume = None + quoteVolume = None + if isCoinm: + baseVolume = self.safe_string(ticker, 'baseVolume') + # 'volume' field in inverse markets is not quoteVolume, but traded amount(per contracts) + quoteVolume = Precise.string_mul(baseVolume, wAvg) + else: + baseVolume = self.safe_string(ticker, 'volume') + quoteVolume = self.safe_string_2(ticker, 'quoteVolume', 'amount') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string_2(ticker, 'highPrice', 'high'), + 'low': self.safe_string_2(ticker, 'lowPrice', 'low'), + 'bid': self.safe_string(ticker, 'bidPrice'), + 'bidVolume': self.safe_string(ticker, 'bidQty'), + 'ask': self.safe_string(ticker, 'askPrice'), + 'askVolume': self.safe_string(ticker, 'askQty'), + 'vwap': wAvg, + 'open': self.safe_string_2(ticker, 'openPrice', 'open'), + 'close': last, + 'last': last, + 'previousClose': self.safe_string(ticker, 'prevClosePrice'), # previous day close + 'change': self.safe_string(ticker, 'priceChange'), + 'percentage': self.safe_string(ticker, 'priceChangePercent'), + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'markPrice': self.safe_string(ticker, 'markPrice'), + 'indexPrice': self.safe_string(ticker, 'indexPrice'), + 'info': ticker, + }, market) + + async def fetch_status(self, params={}): + """ + the latest known information on the availability of the exchange API + + https://developers.binance.com/docs/wallet/others/system-status + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `status structure ` + """ + response = await self.sapiGetSystemStatus(params) + # + # { + # "status": 0, # 0: normal,1:system maintenance + # "msg": "normal" # "normal", "system_maintenance" + # } + # + statusRaw = self.safe_string(response, 'status') + return { + 'status': self.safe_string({'0': 'ok', '1': 'maintenance'}, statusRaw, statusRaw), + 'updated': None, + 'eta': None, + 'url': None, + 'info': response, + } + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://developers.binance.com/docs/binance-spot-api-docs/rest-api/market-data-endpoints#24hr-ticker-price-change-statistics # spot + https://developers.binance.com/docs/binance-spot-api-docs/rest-api/market-data-endpoints#rolling-window-price-change-statistics # spot + https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/24hr-Ticker-Price-Change-Statistics # swap + https://developers.binance.com/docs/derivatives/coin-margined-futures/market-data/rest-api/24hr-Ticker-Price-Change-Statistics # future + https://developers.binance.com/docs/derivatives/option/market-data/24hr-Ticker-Price-Change-Statistics # option + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.rolling]:(spot only) default False, if True, uses the rolling 24 hour ticker endpoint /api/v3/ticker + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = None + if market['option']: + response = await self.eapiPublicGetTicker(self.extend(request, params)) + elif market['linear']: + response = await self.fapiPublicGetTicker24hr(self.extend(request, params)) + elif market['inverse']: + response = await self.dapiPublicGetTicker24hr(self.extend(request, params)) + else: + rolling = self.safe_bool(params, 'rolling', False) + params = self.omit(params, 'rolling') + if rolling: + response = await self.publicGetTicker(self.extend(request, params)) + else: + response = await self.publicGetTicker24hr(self.extend(request, params)) + if isinstance(response, list): + firstTicker = self.safe_dict(response, 0, {}) + return self.parse_ticker(firstTicker, market) + return self.parse_ticker(response, market) + + async def fetch_bids_asks(self, symbols: Strings = None, params={}): + """ + fetches the bid and ask price and volume for multiple markets + + https://developers.binance.com/docs/binance-spot-api-docs/rest-api/market-data-endpoints#symbol-order-book-ticker # spot + https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/Symbol-Order-Book-Ticker # swap + https://developers.binance.com/docs/derivatives/coin-margined-futures/market-data/rest-api/Symbol-Order-Book-Ticker # future + + :param str[]|None symbols: unified symbols of the markets to fetch the bids and asks for, all markets are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.subType]: "linear" or "inverse" + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, True, True, True) + market = self.get_market_from_symbols(symbols) + type = None + type, params = self.handle_market_type_and_params('fetchBidsAsks', market, params) + subType = None + subType, params = self.handle_sub_type_and_params('fetchBidsAsks', market, params) + response = None + if self.is_linear(type, subType): + response = await self.fapiPublicGetTickerBookTicker(params) + elif self.is_inverse(type, subType): + response = await self.dapiPublicGetTickerBookTicker(params) + elif type == 'spot': + request: dict = {} + if symbols is not None: + request['symbols'] = self.json(self.market_ids(symbols)) + response = await self.publicGetTickerBookTicker(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchBidsAsks() does not support ' + type + ' markets yet') + return self.parse_tickers(response, symbols) + + async def fetch_last_prices(self, symbols: Strings = None, params={}): + """ + fetches the last price for multiple markets + + https://developers.binance.com/docs/binance-spot-api-docs/rest-api/market-data-endpoints#symbol-price-ticker # spot + https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/Symbol-Price-Ticker # swap + https://developers.binance.com/docs/derivatives/coin-margined-futures/market-data/rest-api/Symbol-Price-Ticker # future + + :param str[]|None symbols: unified symbols of the markets to fetch the last prices + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.subType]: "linear" or "inverse" + :returns dict: a dictionary of lastprices structures + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, True, True, True) + market = self.get_market_from_symbols(symbols) + type = None + type, params = self.handle_market_type_and_params('fetchLastPrices', market, params) + subType = None + subType, params = self.handle_sub_type_and_params('fetchLastPrices', market, params) + response = None + if self.is_linear(type, subType): + response = await self.fapiPublicV2GetTickerPrice(params) + # + # [ + # { + # "symbol": "LTCBTC", + # "price": "4.00000200" + # "time": 1589437530011 + # }, + # ... + # ] + # + elif self.is_inverse(type, subType): + response = await self.dapiPublicGetTickerPrice(params) + # + # [ + # { + # "symbol": "BTCUSD_200626", + # "ps": "9647.8", + # "price": "9647.8", + # "time": 1591257246176 + # } + # ] + # + elif type == 'spot': + response = await self.publicGetTickerPrice(params) + # + # [ + # { + # "symbol": "LTCBTC", + # "price": "4.00000200" + # }, + # ... + # ] + # + else: + raise NotSupported(self.id + ' fetchLastPrices() does not support ' + type + ' markets yet') + return self.parse_last_prices(response, symbols) + + def parse_last_price(self, entry, market: Market = None): + # + # spot + # + # { + # "symbol": "LTCBTC", + # "price": "4.00000200" + # } + # + # usdm(swap/future) + # + # { + # "symbol": "BTCUSDT", + # "price": "6000.01", + # "time": 1589437530011 # Transaction time + # } + # + # + # coinm(swap/future) + # + # { + # "symbol": "BTCUSD_200626", # symbol("BTCUSD_200626", "BTCUSD_PERP", etc..) + # "ps": "BTCUSD", # pair + # "price": "9647.8", + # "time": 1591257246176 + # } + # + timestamp = self.safe_integer(entry, 'time') + type = 'spot' if (timestamp is None) else 'swap' + marketId = self.safe_string(entry, 'symbol') + market = self.safe_market(marketId, market, None, type) + return { + 'symbol': market['symbol'], + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'price': self.safe_number_omit_zero(entry, 'price'), + 'side': None, + 'info': entry, + } + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://developers.binance.com/docs/binance-spot-api-docs/rest-api/market-data-endpoints#24hr-ticker-price-change-statistics # spot + https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/24hr-Ticker-Price-Change-Statistics # swap + https://developers.binance.com/docs/derivatives/coin-margined-futures/market-data/rest-api/24hr-Ticker-Price-Change-Statistics # future + https://developers.binance.com/docs/derivatives/option/market-data/24hr-Ticker-Price-Change-Statistics # option + + :param str[] [symbols]: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.subType]: "linear" or "inverse" + :param str [params.type]: 'spot', 'option', use params["subType"] for swap and future markets + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, True, True, True) + market = self.get_market_from_symbols(symbols) + type = None + type, params = self.handle_market_type_and_params('fetchTickers', market, params) + subType = None + subType, params = self.handle_sub_type_and_params('fetchTickers', market, params) + response = None + if self.is_linear(type, subType): + response = await self.fapiPublicGetTicker24hr(params) + elif self.is_inverse(type, subType): + response = await self.dapiPublicGetTicker24hr(params) + elif type == 'spot': + rolling = self.safe_bool(params, 'rolling', False) + params = self.omit(params, 'rolling') + if rolling: + symbols = self.market_symbols(symbols) + request: dict = { + 'symbols': self.json(self.market_ids(symbols)), + } + response = await self.publicGetTicker(self.extend(request, params)) + # parseTicker is not able to handle marketType for spot-rolling ticker fields, so we need custom parsing + return self.parse_tickers_for_rolling(response, symbols) + else: + request: dict = {} + if symbols is not None: + request['symbols'] = self.json(self.market_ids(symbols)) + response = await self.publicGetTicker24hr(self.extend(request, params)) + elif type == 'option': + response = await self.eapiPublicGetTicker(params) + else: + raise NotSupported(self.id + ' fetchTickers() does not support ' + type + ' markets yet') + return self.parse_tickers(response, symbols) + + def parse_tickers_for_rolling(self, response, symbols): + results = [] + for i in range(0, len(response)): + marketId = self.safe_string(response[i], 'symbol') + tickerMarket = self.safe_market(marketId, None, None, 'spot') + parsedTicker = self.parse_ticker(response[i]) + parsedTicker['symbol'] = tickerMarket['symbol'] + results.append(parsedTicker) + return self.filter_by_array(results, 'symbol', symbols) + + async def fetch_mark_price(self, symbol: str, params={}) -> Ticker: + """ + fetches mark price for the market + + https://developers.binance.com/docs/derivatives/coin-margined-futures/market-data/rest-api/Index-Price-and-Mark-Price + https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/Mark-Price + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.subType]: "linear" or "inverse" + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + market = self.market(symbol) + type = None + type, params = self.handle_market_type_and_params('fetchMarkPrice', market, params, 'swap') + subType = None + subType, params = self.handle_sub_type_and_params('fetchMarkPrice', market, params, 'linear') + request = { + 'symbol': market['id'], + } + response = None + if self.is_linear(type, subType): + response = await self.fapiPublicGetPremiumIndex(self.extend(request, params)) + elif self.is_inverse(type, subType): + response = await self.dapiPublicGetPremiumIndex(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchMarkPrice() does not support ' + type + ' markets yet') + if isinstance(response, list): + return self.parse_ticker(self.safe_dict(response, 0, {}), market) + return self.parse_ticker(response, market) + + async def fetch_mark_prices(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches mark prices for multiple markets + + https://developers.binance.com/docs/derivatives/coin-margined-futures/market-data/rest-api/Index-Price-and-Mark-Price + https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/Mark-Price + + :param str[] [symbols]: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.subType]: "linear" or "inverse" + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, True, True, True) + market = self.get_market_from_symbols(symbols) + type = None + type, params = self.handle_market_type_and_params('fetchMarkPrices', market, params, 'swap') + subType = None + subType, params = self.handle_sub_type_and_params('fetchMarkPrices', market, params, 'linear') + response = None + if self.is_linear(type, subType): + response = await self.fapiPublicGetPremiumIndex(params) + elif self.is_inverse(type, subType): + response = await self.dapiPublicGetPremiumIndex(params) + else: + raise NotSupported(self.id + ' fetchMarkPrices() does not support ' + type + ' markets yet') + return self.parse_tickers(response, symbols) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # when api method = publicGetKlines or fapiPublicGetKlines or dapiPublicGetKlines + # [ + # 1591478520000, # open time + # "0.02501300", # open + # "0.02501800", # high + # "0.02500000", # low + # "0.02500000", # close + # "22.19000000", # volume + # 1591478579999, # close time + # "0.55490906", # quote asset volume, base asset volume for dapi + # 40, # number of trades + # "10.92900000", # taker buy base asset volume + # "0.27336462", # taker buy quote asset volume + # "0" # ignore + # ] + # + # when api method = fapiPublicGetMarkPriceKlines or fapiPublicGetIndexPriceKlines + # [ + # [ + # 1591256460000, # Open time + # "9653.29201333", # Open + # "9654.56401333", # High + # "9653.07367333", # Low + # "9653.07367333", # Close(or latest price) + # "0", # Ignore + # 1591256519999, # Close time + # "0", # Ignore + # 60, # Number of bisic data + # "0", # Ignore + # "0", # Ignore + # "0" # Ignore + # ] + # ] + # + # options + # + # { + # "open": "32.2", + # "high": "32.2", + # "low": "32.2", + # "close": "32.2", + # "volume": "0", + # "interval": "5m", + # "tradeCount": 0, + # "takerVolume": "0", + # "takerAmount": "0", + # "amount": "0", + # "openTime": 1677096900000, + # "closeTime": 1677097200000 + # } + # + inverse = self.safe_bool(market, 'inverse') + volumeIndex = 7 if inverse else 5 + return [ + self.safe_integer_2(ohlcv, 0, 'openTime'), + self.safe_number_2(ohlcv, 1, 'open'), + self.safe_number_2(ohlcv, 2, 'high'), + self.safe_number_2(ohlcv, 3, 'low'), + self.safe_number_2(ohlcv, 4, 'close'), + self.safe_number_2(ohlcv, volumeIndex, 'volume'), + ] + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://developers.binance.com/docs/binance-spot-api-docs/rest-api/market-data-endpoints#klinecandlestick-data + https://developers.binance.com/docs/derivatives/option/market-data/Kline-Candlestick-Data + https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/Kline-Candlestick-Data + https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/Index-Price-Kline-Candlestick-Data + https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/Mark-Price-Kline-Candlestick-Data + https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/Premium-Index-Kline-Data + https://developers.binance.com/docs/derivatives/coin-margined-futures/market-data/rest-api/Kline-Candlestick-Data + https://developers.binance.com/docs/derivatives/coin-margined-futures/market-data/rest-api/Index-Price-Kline-Candlestick-Data + https://developers.binance.com/docs/derivatives/coin-margined-futures/market-data/rest-api/Mark-Price-Kline-Candlestick-Data + https://developers.binance.com/docs/derivatives/coin-margined-futures/market-data/rest-api/Premium-Index-Kline-Data + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.price]: "mark" or "index" for mark price and index price candles + :param int [params.until]: timestamp in ms of the latest candle to fetch + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate', False) + if paginate: + return await self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params, 1000) + market = self.market(symbol) + # binance docs say that the default limit 500, max 1500 for futures, max 1000 for spot markets + # the reality is that the time range wider than 500 candles won't work right + defaultLimit = 500 + maxLimit = 1500 + price = self.safe_string(params, 'price') + until = self.safe_integer(params, 'until') + params = self.omit(params, ['price', 'until']) + if since is not None and until is not None and limit is None: + limit = maxLimit + limit = defaultLimit if (limit is None) else min(limit, maxLimit) + request: dict = { + 'interval': self.safe_string(self.timeframes, timeframe, timeframe), + 'limit': limit, + } + marketId = market['id'] + if price == 'index': + parts = marketId.split('_') + pair = self.safe_string(parts, 0) + request['pair'] = pair # Index price takes self argument instead of symbol + else: + request['symbol'] = marketId + # duration = self.parse_timeframe(timeframe) + if since is not None: + request['startTime'] = since + # + # It didn't work before without the endTime + # https://github.com/ccxt/ccxt/issues/8454 + # + if market['inverse']: + if since > 0: + duration = self.parse_timeframe(timeframe) + endTime = self.sum(since, limit * duration * 1000 - 1) + now = self.milliseconds() + request['endTime'] = min(now, endTime) + if until is not None: + request['endTime'] = until + response = None + if market['option']: + response = await self.eapiPublicGetKlines(self.extend(request, params)) + elif price == 'mark': + if market['inverse']: + response = await self.dapiPublicGetMarkPriceKlines(self.extend(request, params)) + else: + response = await self.fapiPublicGetMarkPriceKlines(self.extend(request, params)) + elif price == 'index': + if market['inverse']: + response = await self.dapiPublicGetIndexPriceKlines(self.extend(request, params)) + else: + response = await self.fapiPublicGetIndexPriceKlines(self.extend(request, params)) + elif price == 'premiumIndex': + if market['inverse']: + response = await self.dapiPublicGetPremiumIndexKlines(self.extend(request, params)) + else: + response = await self.fapiPublicGetPremiumIndexKlines(self.extend(request, params)) + elif market['linear']: + response = await self.fapiPublicGetKlines(self.extend(request, params)) + elif market['inverse']: + response = await self.dapiPublicGetKlines(self.extend(request, params)) + else: + response = await self.publicGetKlines(self.extend(request, params)) + # + # [ + # [1591478520000,"0.02501300","0.02501800","0.02500000","0.02500000","22.19000000",1591478579999,"0.55490906",40,"10.92900000","0.27336462","0"], + # [1591478580000,"0.02499600","0.02500900","0.02499400","0.02500300","21.34700000",1591478639999,"0.53370468",24,"7.53800000","0.18850725","0"], + # [1591478640000,"0.02500800","0.02501100","0.02500300","0.02500800","154.14200000",1591478699999,"3.85405839",97,"5.32300000","0.13312641","0"], + # ] + # + # options(eapi) + # + # [ + # { + # "open": "32.2", + # "high": "32.2", + # "low": "32.2", + # "close": "32.2", + # "volume": "0", + # "interval": "5m", + # "tradeCount": 0, + # "takerVolume": "0", + # "takerAmount": "0", + # "amount": "0", + # "openTime": 1677096900000, + # "closeTime": 1677097200000 + # } + # ] + # + candles = self.parse_ohlcvs(response, market, timeframe, since, limit) + return candles + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + if 'isDustTrade' in trade: + return self.parse_dust_trade(trade, market) + # + # aggregate trades + # https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md#compressedaggregate-trades-list + # + # { + # "a": 26129, # Aggregate tradeId + # "p": "0.01633102", # Price + # "q": "4.70443515", # Quantity + # "f": 27781, # First tradeId + # "l": 27781, # Last tradeId + # "T": 1498793709153, # Timestamp + # "m": True, # Was the buyer the maker? + # "M": True # Was the trade the best price match? + # } + # + # REST: aggregate trades for swap & future(both linear and inverse) + # + # { + # "a": "269772814", + # "p": "25864.1", + # "q": "3", + # "f": "662149354", + # "l": "662149355", + # "T": "1694209776022", + # "m": False, + # } + # + # recent public trades and old public trades + # https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md#recent-trades-list + # https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md#old-trade-lookup-market_data + # + # { + # "id": 28457, + # "price": "4.00000100", + # "qty": "12.00000000", + # "time": 1499865549590, + # "isBuyerMaker": True, + # "isBestMatch": True + # } + # + # private trades + # https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md#account-trade-list-user_data + # + # { + # "symbol": "BNBBTC", + # "id": 28457, + # "orderId": 100234, + # "price": "4.00000100", + # "qty": "12.00000000", + # "commission": "10.10000000", + # "commissionAsset": "BNB", + # "time": 1499865549590, + # "isBuyer": True, + # "isMaker": False, + # "isBestMatch": True + # } + # + # futures trades + # + # { + # "accountId": 20, + # "buyer": False, + # "commission": "-0.07819010", + # "commissionAsset": "USDT", + # "counterPartyId": 653, + # "id": 698759, + # "maker": False, + # "orderId": 25851813, + # "price": "7819.01", + # "qty": "0.002", + # "quoteQty": "0.01563", + # "realizedPnl": "-0.91539999", + # "side": "SELL", + # "symbol": "BTCUSDT", + # "time": 1569514978020 + # } + # { + # "symbol": "BTCUSDT", + # "id": 477128891, + # "orderId": 13809777875, + # "side": "SELL", + # "price": "38479.55", + # "qty": "0.001", + # "realizedPnl": "-0.00009534", + # "marginAsset": "USDT", + # "quoteQty": "38.47955", + # "commission": "-0.00076959", + # "commissionAsset": "USDT", + # "time": 1612733566708, + # "positionSide": "BOTH", + # "maker": True, + # "buyer": False + # } + # + # {respType: FULL} + # + # { + # "price": "4000.00000000", + # "qty": "1.00000000", + # "commission": "4.00000000", + # "commissionAsset": "USDT", + # "tradeId": "1234", + # } + # + # options: fetchMyTrades + # + # { + # "id": 1125899906844226012, + # "tradeId": 73, + # "orderId": 4638761100843040768, + # "symbol": "ETH-230211-1500-C", + # "price": "18.70000000", + # "quantity": "-0.57000000", + # "fee": "0.17305890", + # "realizedProfit": "-3.53400000", + # "side": "SELL", + # "type": "LIMIT", + # "volatility": "0.30000000", + # "liquidity": "MAKER", + # "time": 1676085216845, + # "priceScale": 1, + # "quantityScale": 2, + # "optionSide": "CALL", + # "quoteAsset": "USDT" + # } + # + # options: fetchTrades + # + # { + # "id": 1, + # "symbol": "ETH-230216-1500-C", + # "price": "35.5", + # "qty": "0.03", + # "quoteQty": "1.065", + # "side": 1, + # "time": 1676366446072 + # } + # + # fetchMyTrades: linear portfolio margin + # + # { + # "symbol": "BTCUSDT", + # "id": 4575108247, + # "orderId": 261942655610, + # "side": "SELL", + # "price": "47263.40", + # "qty": "0.010", + # "realizedPnl": "27.38400000", + # "marginAsset": "USDT", + # "quoteQty": "472.63", + # "commission": "0.18905360", + # "commissionAsset": "USDT", + # "time": 1707530039409, + # "buyer": False, + # "maker": False, + # "positionSide": "LONG" + # } + # + # fetchMyTrades: inverse portfolio margin + # + # { + # "symbol": "ETHUSD_PERP", + # "id": 701907838, + # "orderId": 71548909034, + # "pair": "ETHUSD", + # "side": "SELL", + # "price": "2498.15", + # "qty": "1", + # "realizedPnl": "0.00012517", + # "marginAsset": "ETH", + # "baseQty": "0.00400296", + # "commission": "0.00000160", + # "commissionAsset": "ETH", + # "time": 1707530317519, + # "positionSide": "LONG", + # "buyer": False, + # "maker": False + # } + # + # fetchMyTrades: spot margin portfolio margin + # + # { + # "symbol": "ADAUSDT", + # "id": 470227543, + # "orderId": 4421170947, + # "price": "0.53880000", + # "qty": "10.00000000", + # "quoteQty": "5.38800000", + # "commission": "0.00538800", + # "commissionAsset": "USDT", + # "time": 1707545780522, + # "isBuyer": False, + # "isMaker": False, + # "isBestMatch": True + # } + # + timestamp = self.safe_integer_2(trade, 'T', 'time') + amount = self.safe_string_2(trade, 'q', 'qty') + amount = self.safe_string(trade, 'quantity', amount) + marketId = self.safe_string(trade, 'symbol') + isSpotTrade = ('isIsolated' in trade) or ('M' in trade) or ('orderListId' in trade) or ('isMaker' in trade) + marketType = 'spot' if isSpotTrade else 'contract' + market = self.safe_market(marketId, market, None, marketType) + symbol = market['symbol'] + side = None + buyerMaker = self.safe_bool_2(trade, 'm', 'isBuyerMaker') + takerOrMaker = None + if buyerMaker is not None: + side = 'sell' if buyerMaker else 'buy' # self is reversed intentionally + elif 'side' in trade: + side = self.safe_string_lower(trade, 'side') + else: + if 'isBuyer' in trade: + side = 'buy' if trade['isBuyer'] else 'sell' # self is a True side + fee = None + if 'commission' in trade: + fee = { + 'cost': self.safe_string(trade, 'commission'), + 'currency': self.safe_currency_code(self.safe_string(trade, 'commissionAsset')), + } + if 'isMaker' in trade: + takerOrMaker = 'maker' if trade['isMaker'] else 'taker' + if 'maker' in trade: + takerOrMaker = 'maker' if trade['maker'] else 'taker' + if ('optionSide' in trade) or market['option']: + settle = self.safe_currency_code(self.safe_string(trade, 'quoteAsset', 'USDT')) + takerOrMaker = self.safe_string_lower(trade, 'liquidity') + if 'fee' in trade: + fee = { + 'cost': self.safe_string(trade, 'fee'), + 'currency': settle, + } + if (side != 'buy') and (side != 'sell'): + side = 'buy' if (side == '1') else 'sell' + if 'optionSide' in trade: + if side != 'buy': + amount = Precise.string_mul('-1', amount) + return self.safe_trade({ + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'id': self.safe_string_n(trade, ['t', 'a', 'tradeId', 'id']), + 'order': self.safe_string(trade, 'orderId'), + 'type': self.safe_string_lower(trade, 'type'), + 'side': side, + 'takerOrMaker': takerOrMaker, + 'price': self.safe_string_2(trade, 'p', 'price'), + 'amount': amount, + 'cost': self.safe_string_2(trade, 'quoteQty', 'baseQty'), + 'fee': fee, + }, market) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + Default fetchTradesMethod + + https://developers.binance.com/docs/binance-spot-api-docs/rest-api/market-data-endpoints#compressedaggregate-trades-list # publicGetAggTrades(spot) + https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/Compressed-Aggregate-Trades-List # fapiPublicGetAggTrades(swap) + https://developers.binance.com/docs/derivatives/coin-margined-futures/market-data/rest-api/Compressed-Aggregate-Trades-List # dapiPublicGetAggTrades(future) + https://developers.binance.com/docs/derivatives/option/market-data/Recent-Trades-List # eapiPublicGetTrades(option) + + Other fetchTradesMethod + + https://developers.binance.com/docs/binance-spot-api-docs/rest-api/market-data-endpoints#recent-trades-list # publicGetTrades(spot) + https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/Recent-Trades-List # fapiPublicGetTrades(swap) + https://developers.binance.com/docs/derivatives/coin-margined-futures/market-data/rest-api/Recent-Trades-List # dapiPublicGetTrades(future) + https://developers.binance.com/docs/binance-spot-api-docs/rest-api/market-data-endpoints#old-trade-lookup # publicGetHistoricalTrades(spot) + https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/Old-Trades-Lookup # fapiPublicGetHistoricalTrades(swap) + https://developers.binance.com/docs/derivatives/coin-margined-futures/market-data/rest-api/Old-Trades-Lookup # dapiPublicGetHistoricalTrades(future) + https://developers.binance.com/docs/derivatives/option/market-data/Old-Trades-Lookup # eapiPublicGetHistoricalTrades(option) + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: only used when fetchTradesMethod is 'publicGetAggTrades', 'fapiPublicGetAggTrades', or 'dapiPublicGetAggTrades' + :param int [limit]: default 500, max 1000 + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: only used when fetchTradesMethod is 'publicGetAggTrades', 'fapiPublicGetAggTrades', or 'dapiPublicGetAggTrades' + :param int [params.fetchTradesMethod]: 'publicGetAggTrades'(spot default), 'fapiPublicGetAggTrades'(swap default), 'dapiPublicGetAggTrades'(future default), 'eapiPublicGetTrades'(option default), 'publicGetTrades', 'fapiPublicGetTrades', 'dapiPublicGetTrades', 'publicGetHistoricalTrades', 'fapiPublicGetHistoricalTrades', 'dapiPublicGetHistoricalTrades', 'eapiPublicGetHistoricalTrades' + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + + EXCHANGE SPECIFIC PARAMETERS + :param int [params.fromId]: trade id to fetch from, default gets most recent trades, not used when fetchTradesMethod is 'publicGetTrades', 'fapiPublicGetTrades', 'dapiPublicGetTrades', or 'eapiPublicGetTrades' + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchTrades', 'paginate') + if paginate: + return await self.fetch_paginated_call_dynamic('fetchTrades', symbol, since, limit, params) + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + # 'fromId': 123, # ID to get aggregate trades from INCLUSIVE. + # 'startTime': 456, # Timestamp in ms to get aggregate trades from INCLUSIVE. + # 'endTime': 789, # Timestamp in ms to get aggregate trades until INCLUSIVE. + # 'limit': 500, # default = 500, maximum = 1000 + } + if not market['option']: + if since is not None: + request['startTime'] = since + # https://github.com/ccxt/ccxt/issues/6400 + # https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md#compressedaggregate-trades-list + request['endTime'] = self.sum(since, 3600000) + until = self.safe_integer(params, 'until') + if until is not None: + request['endTime'] = until + method = self.safe_string(self.options, 'fetchTradesMethod') + method = self.safe_string_2(params, 'fetchTradesMethod', 'method', method) + if limit is not None: + isFutureOrSwap = (market['swap'] or market['future']) + isHistoricalEndpoint = (method is not None) and (method.find('GetHistoricalTrades') >= 0) + maxLimitForContractHistorical = 500 if isHistoricalEndpoint else 1000 + request['limit'] = min(limit, maxLimitForContractHistorical) if isFutureOrSwap else limit # default = 500, maximum = 1000 + params = self.omit(params, ['until', 'fetchTradesMethod']) + response = None + if market['option'] or method == 'eapiPublicGetTrades': + response = await self.eapiPublicGetTrades(self.extend(request, params)) + elif market['linear'] or method == 'fapiPublicGetAggTrades': + response = await self.fapiPublicGetAggTrades(self.extend(request, params)) + elif market['inverse'] or method == 'dapiPublicGetAggTrades': + response = await self.dapiPublicGetAggTrades(self.extend(request, params)) + else: + response = await self.publicGetAggTrades(self.extend(request, params)) + # + # Caveats: + # - default limit(500) applies only if no other parameters set, trades up + # to the maximum limit may be returned to satisfy other parameters + # - if both limit and time window is set and time window contains more + # trades than the limit then the last trades from the window are returned + # - "tradeId" accepted and returned by self method is "aggregate" trade id + # which is different from actual trade id + # - setting both fromId and time window results in error + # + # aggregate trades + # + # [ + # { + # "a": 26129, # Aggregate tradeId + # "p": "0.01633102", # Price + # "q": "4.70443515", # Quantity + # "f": 27781, # First tradeId + # "l": 27781, # Last tradeId + # "T": 1498793709153, # Timestamp + # "m": True, # Was the buyer the maker? + # "M": True # Was the trade the best price match? + # } + # ] + # + # inverse(swap & future) + # + # [ + # { + # "a": "269772814", + # "p": "25864.1", + # "q": "3", + # "f": "662149354", + # "l": "662149355", + # "T": "1694209776022", + # "m": False, + # }, + # ] + # + # recent public trades and historical public trades + # + # [ + # { + # "id": 28457, + # "price": "4.00000100", + # "qty": "12.00000000", + # "time": 1499865549590, + # "isBuyerMaker": True, + # "isBestMatch": True + # } + # ] + # + # options(eapi) + # + # [ + # { + # "id": 1, + # "symbol": "ETH-230216-1500-C", + # "price": "35.5", + # "qty": "0.03", + # "quoteQty": "1.065", + # "side": 1, + # "time": 1676366446072 + # }, + # ] + # + return self.parse_trades(response, market, since, limit) + + async def edit_spot_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + @ignore + edit a trade order + + https://developers.binance.com/docs/binance-spot-api-docs/rest-api/trading-endpoints#cancel-an-existing-order-and-send-a-new-order-trade + + :param str id: cancel order id + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' or 'STOP_LOSS' or 'STOP_LOSS_LIMIT' or 'TAKE_PROFIT' or 'TAKE_PROFIT_LIMIT' or 'STOP' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated', for spot margin trading + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' editSpotOrder() does not support ' + market['type'] + ' orders') + payload = self.edit_spot_order_request(id, symbol, type, side, amount, price, params) + response = await self.privatePostOrderCancelReplace(payload) + # + # spot + # + # { + # "cancelResult": "SUCCESS", + # "newOrderResult": "SUCCESS", + # "cancelResponse": { + # "symbol": "BTCUSDT", + # "origClientOrderId": "web_3f6286480b194b079870ac75fb6978b7", + # "orderId": 16383156620, + # "orderListId": -1, + # "clientOrderId": "Azt6foVTTgHPNhqBf41TTt", + # "price": "14000.00000000", + # "origQty": "0.00110000", + # "executedQty": "0.00000000", + # "cummulativeQuoteQty": "0.00000000", + # "status": "CANCELED", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "side": "BUY" + # }, + # "newOrderResponse": { + # "symbol": "BTCUSDT", + # "orderId": 16383176297, + # "orderListId": -1, + # "clientOrderId": "x-TKT5PX2F22ecb58eb9074fb1be018c", + # "transactTime": 1670891847932, + # "price": "13500.00000000", + # "origQty": "0.00085000", + # "executedQty": "0.00000000", + # "cummulativeQuoteQty": "0.00000000", + # "status": "NEW", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "side": "BUY", + # "fills": [] + # } + # } + # + data = self.safe_dict(response, 'newOrderResponse') + return self.parse_order(data, market) + + def edit_spot_order_request(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + @ignore + helper function to build request for editSpotOrder + :param str id: order id to be edited + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' or 'STOP_LOSS' or 'STOP_LOSS_LIMIT' or 'TAKE_PROFIT' or 'TAKE_PROFIT_LIMIT' or 'STOP' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict params: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated', for spot margin trading + :returns dict: request to be sent to the exchange + """ + market = self.market(symbol) + clientOrderId = self.safe_string_n(params, ['newClientOrderId', 'clientOrderId', 'origClientOrderId']) + request: dict = { + 'symbol': market['id'], + 'side': side.upper(), + } + initialUppercaseType = type.upper() + uppercaseType = initialUppercaseType + postOnly = self.is_post_only(initialUppercaseType == 'MARKET', initialUppercaseType == 'LIMIT_MAKER', params) + if postOnly: + uppercaseType = 'LIMIT_MAKER' + triggerPrice = self.safe_number_2(params, 'stopPrice', 'triggerPrice') + if triggerPrice is not None: + if uppercaseType == 'MARKET': + uppercaseType = 'STOP_LOSS' + elif uppercaseType == 'LIMIT': + uppercaseType = 'STOP_LOSS_LIMIT' + request['type'] = uppercaseType + validOrderTypes = self.safe_list(market['info'], 'orderTypes') + if not self.in_array(uppercaseType, validOrderTypes): + if initialUppercaseType != uppercaseType: + raise InvalidOrder(self.id + ' triggerPrice parameter is not allowed for ' + symbol + ' ' + type + ' orders') + else: + raise InvalidOrder(self.id + ' ' + type + ' is not a valid order type for the ' + symbol + ' market') + if clientOrderId is None: + broker = self.safe_dict(self.options, 'broker') + if broker is not None: + brokerId = self.safe_string(broker, 'spot') + if brokerId is not None: + request['newClientOrderId'] = brokerId + self.uuid22() + else: + request['newClientOrderId'] = clientOrderId + request['newOrderRespType'] = self.safe_value(self.options['newOrderRespType'], type, 'RESULT') # 'ACK' for order id, 'RESULT' for full order or 'FULL' for order with fills + timeInForceIsRequired = False + priceIsRequired = False + triggerPriceIsRequired = False + quantityIsRequired = False + if uppercaseType == 'MARKET': + quoteOrderQty = self.safe_bool(self.options, 'quoteOrderQty', True) + if quoteOrderQty: + quoteOrderQtyNew = self.safe_value_2(params, 'quoteOrderQty', 'cost') + precision = market['precision']['price'] + if quoteOrderQtyNew is not None: + request['quoteOrderQty'] = self.decimal_to_precision(quoteOrderQtyNew, TRUNCATE, precision, self.precisionMode) + elif price is not None: + amountString = self.number_to_string(amount) + priceString = self.number_to_string(price) + quoteOrderQuantity = Precise.string_mul(amountString, priceString) + request['quoteOrderQty'] = self.decimal_to_precision(quoteOrderQuantity, TRUNCATE, precision, self.precisionMode) + else: + quantityIsRequired = True + else: + quantityIsRequired = True + elif uppercaseType == 'LIMIT': + priceIsRequired = True + timeInForceIsRequired = True + quantityIsRequired = True + elif (uppercaseType == 'STOP_LOSS') or (uppercaseType == 'TAKE_PROFIT'): + triggerPriceIsRequired = True + quantityIsRequired = True + elif (uppercaseType == 'STOP_LOSS_LIMIT') or (uppercaseType == 'TAKE_PROFIT_LIMIT'): + quantityIsRequired = True + triggerPriceIsRequired = True + priceIsRequired = True + timeInForceIsRequired = True + elif uppercaseType == 'LIMIT_MAKER': + priceIsRequired = True + quantityIsRequired = True + if quantityIsRequired: + request['quantity'] = self.amount_to_precision(symbol, amount) + if priceIsRequired: + if price is None: + raise InvalidOrder(self.id + ' editOrder() requires a price argument for a ' + type + ' order') + request['price'] = self.price_to_precision(symbol, price) + if timeInForceIsRequired and (self.safe_string(params, 'timeInForce') is None): + request['timeInForce'] = self.options['defaultTimeInForce'] # 'GTC' = Good To Cancel(default), 'IOC' = Immediate Or Cancel + if triggerPriceIsRequired: + if triggerPrice is None: + raise InvalidOrder(self.id + ' editOrder() requires a triggerPrice extra param for a ' + type + ' order') + else: + request['stopPrice'] = self.price_to_precision(symbol, triggerPrice) + request['cancelReplaceMode'] = 'STOP_ON_FAILURE' # If the cancel request fails, the new order placement will not be attempted. + cancelId = self.safe_string_2(params, 'cancelNewClientOrderId', 'cancelOrigClientOrderId') + if cancelId is None: + request['cancelOrderId'] = id # user can provide either cancelOrderId, cancelOrigClientOrderId or cancelOrigClientOrderId + # remove timeInForce from params because PO is only used by self.is_post_only and it's not a valid value for Binance + if self.safe_string(params, 'timeInForce') == 'PO': + params = self.omit(params, ['timeInForce']) + params = self.omit(params, ['quoteOrderQty', 'cost', 'stopPrice', 'newClientOrderId', 'clientOrderId', 'postOnly']) + return self.extend(request, params) + + def edit_contract_order_request(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + market = self.market(symbol) + if not market['contract']: + raise NotSupported(self.id + ' editContractOrder() does not support ' + market['type'] + ' orders') + request: dict = { + 'symbol': market['id'], + 'side': side.upper(), + 'orderId': id, + 'quantity': self.amount_to_precision(symbol, amount), + } + clientOrderId = self.safe_string_n(params, ['newClientOrderId', 'clientOrderId', 'origClientOrderId']) + if price is not None: + request['price'] = self.price_to_precision(symbol, price) + if clientOrderId is not None: + request['origClientOrderId'] = clientOrderId + params = self.omit(params, ['clientOrderId', 'newClientOrderId']) + return request + + async def edit_contract_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + edit a trade order + + https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Modify-Order + https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/rest-api/Modify-Order + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Modify-UM-Order + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Modify-CM-Order + + :param str id: cancel order id + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.portfolioMargin]: set to True if you would like to edit an order in a portfolio margin account + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + isPortfolioMargin = None + isPortfolioMargin, params = self.handle_option_and_params_2(params, 'editContractOrder', 'papi', 'portfolioMargin', False) + if market['linear'] or isPortfolioMargin: + if (price is None) and not ('priceMatch' in params): + raise ArgumentsRequired(self.id + ' editOrder() requires a price argument for portfolio margin and linear orders') + request = self.edit_contract_order_request(id, symbol, type, side, amount, price, params) + response = None + if market['linear']: + if isPortfolioMargin: + response = await self.papiPutUmOrder(self.extend(request, params)) + else: + response = await self.fapiPrivatePutOrder(self.extend(request, params)) + elif market['inverse']: + if isPortfolioMargin: + response = await self.papiPutCmOrder(self.extend(request, params)) + else: + response = await self.dapiPrivatePutOrder(self.extend(request, params)) + # + # swap and future + # + # { + # "orderId": 151007482392, + # "symbol": "BTCUSDT", + # "status": "NEW", + # "clientOrderId": "web_pCCGp9AIHjziKLlpGpXI", + # "price": "25000", + # "avgPrice": "0.00000", + # "origQty": "0.001", + # "executedQty": "0", + # "cumQty": "0", + # "cumQuote": "0", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "reduceOnly": False, + # "closePosition": False, + # "side": "BUY", + # "positionSide": "BOTH", + # "stopPrice": "0", + # "workingType": "CONTRACT_PRICE", + # "priceProtect": False, + # "origType": "LIMIT", + # "updateTime": 1684300587845 + # } + # + return self.parse_order(response, market) + + async def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + """ + edit a trade order + + https://developers.binance.com/docs/binance-spot-api-docs/rest-api/trading-endpoints#cancel-an-existing-order-and-send-a-new-order-trade + https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Modify-Order + https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/rest-api/Modify-Order + + :param str id: cancel order id + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + if market['option']: + raise NotSupported(self.id + ' editOrder() does not support ' + market['type'] + ' orders') + if market['spot']: + return await self.edit_spot_order(id, symbol, type, side, amount, price, params) + else: + return await self.edit_contract_order(id, symbol, type, side, amount, price, params) + + async def edit_orders(self, orders: List[OrderRequest], params={}): + """ + edit a list of trade orders + + https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Modify-Multiple-Orders + https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/rest-api/Modify-Multiple-Orders + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + ordersRequests = [] + orderSymbols = [] + for i in range(0, len(orders)): + rawOrder = orders[i] + marketId = self.safe_string(rawOrder, 'symbol') + orderSymbols.append(marketId) + id = self.safe_string(rawOrder, 'id') + type = self.safe_string(rawOrder, 'type') + side = self.safe_string(rawOrder, 'side') + amount = self.safe_value(rawOrder, 'amount') + price = self.safe_value(rawOrder, 'price') + orderParams = self.safe_dict(rawOrder, 'params', {}) + isPortfolioMargin = None + isPortfolioMargin, orderParams = self.handle_option_and_params_2(orderParams, 'editOrders', 'papi', 'portfolioMargin', False) + if isPortfolioMargin: + raise NotSupported(self.id + ' editOrders() does not support portfolio margin orders') + orderRequest = self.edit_contract_order_request(id, marketId, type, side, amount, price, orderParams) + ordersRequests.append(orderRequest) + orderSymbols = self.market_symbols(orderSymbols, None, False, True, True) + market = self.market(orderSymbols[0]) + if market['spot'] or market['option']: + raise NotSupported(self.id + ' editOrders() does not support ' + market['type'] + ' orders') + response = None + request: dict = { + 'batchOrders': ordersRequests, + } + request = self.extend(request, params) + if market['linear']: + response = await self.fapiPrivatePutBatchOrders(request) + elif market['inverse']: + response = await self.dapiPrivatePutBatchOrders(request) + # + # [ + # { + # "code": -4005, + # "msg": "Quantity greater than max quantity." + # }, + # { + # "orderId": 650640530, + # "symbol": "LTCUSDT", + # "status": "NEW", + # "clientOrderId": "x-xcKtGhcu32184eb13585491289bbaf", + # "price": "54.00", + # "avgPrice": "0.00", + # "origQty": "0.100", + # "executedQty": "0.000", + # "cumQty": "0.000", + # "cumQuote": "0.00000", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "reduceOnly": False, + # "closePosition": False, + # "side": "BUY", + # "positionSide": "BOTH", + # "stopPrice": "0.00", + # "workingType": "CONTRACT_PRICE", + # "priceProtect": False, + # "origType": "LIMIT", + # "priceMatch": "NONE", + # "selfTradePreventionMode": "NONE", + # "goodTillDate": 0, + # "updateTime": 1698073926929 + # } + # ] + # + return self.parse_orders(response) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'NEW': 'open', + 'PARTIALLY_FILLED': 'open', + 'ACCEPTED': 'open', + 'FILLED': 'closed', + 'CANCELED': 'canceled', + 'CANCELLED': 'canceled', + 'PENDING_CANCEL': 'canceling', # currently unused + 'REJECTED': 'rejected', + 'EXPIRED': 'expired', + 'EXPIRED_IN_MATCH': 'expired', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # spot + # + # { + # "symbol": "LTCBTC", + # "orderId": 1, + # "clientOrderId": "myOrder1", + # "price": "0.1", + # "origQty": "1.0", + # "executedQty": "0.0", + # "cummulativeQuoteQty": "0.0", + # "status": "NEW", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "side": "BUY", + # "stopPrice": "0.0", + # "icebergQty": "0.0", + # "time": 1499827319559, + # "updateTime": 1499827319559, + # "isWorking": True + # } + # + # spot: editOrder + # + # { + # "symbol": "BTCUSDT", + # "orderId": 16383176297, + # "orderListId": -1, + # "clientOrderId": "x-TKT5PX2F22ecb58eb9074fb1be018c", + # "transactTime": 1670891847932, + # "price": "13500.00000000", + # "origQty": "0.00085000", + # "executedQty": "0.00000000", + # "cummulativeQuoteQty": "0.00000000", + # "status": "NEW", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "side": "BUY", + # "fills": [] + # } + # + # swap and future: editOrder + # + # { + # "orderId": 151007482392, + # "symbol": "BTCUSDT", + # "status": "NEW", + # "clientOrderId": "web_pCCGp9AIHjziKLlpGpXI", + # "price": "25000", + # "avgPrice": "0.00000", + # "origQty": "0.001", + # "executedQty": "0", + # "cumQty": "0", + # "cumQuote": "0", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "reduceOnly": False, + # "closePosition": False, + # "side": "BUY", + # "positionSide": "BOTH", + # "stopPrice": "0", + # "workingType": "CONTRACT_PRICE", + # "priceProtect": False, + # "origType": "LIMIT", + # "updateTime": 1684300587845 + # } + # + # futures + # + # { + # "symbol": "BTCUSDT", + # "orderId": 1, + # "clientOrderId": "myOrder1", + # "price": "0.1", + # "origQty": "1.0", + # "executedQty": "1.0", + # "cumQuote": "10.0", + # "status": "NEW", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "side": "BUY", + # "stopPrice": "0.0", + # "updateTime": 1499827319559 + # } + # + # createOrder with {"newOrderRespType": "FULL"} + # + # { + # "symbol": "BTCUSDT", + # "orderId": 5403233939, + # "orderListId": -1, + # "clientOrderId": "x-TKT5PX2F5e669e75b6c14f69a2c43e", + # "transactTime": 1617151923742, + # "price": "0.00000000", + # "origQty": "0.00050000", + # "executedQty": "0.00050000", + # "cummulativeQuoteQty": "29.47081500", + # "status": "FILLED", + # "timeInForce": "GTC", + # "type": "MARKET", + # "side": "BUY", + # "fills": [ + # { + # "price": "58941.63000000", + # "qty": "0.00050000", + # "commission": "0.00007050", + # "commissionAsset": "BNB", + # "tradeId": 737466631 + # } + # ] + # } + # + # delivery + # + # { + # "orderId": "18742727411", + # "symbol": "ETHUSD_PERP", + # "pair": "ETHUSD", + # "status": "FILLED", + # "clientOrderId": "x-xcKtGhcu3e2d1503fdd543b3b02419", + # "price": "0", + # "avgPrice": "4522.14", + # "origQty": "1", + # "executedQty": "1", + # "cumBase": "0.00221134", + # "timeInForce": "GTC", + # "type": "MARKET", + # "reduceOnly": False, + # "closePosition": False, + # "side": "SELL", + # "positionSide": "BOTH", + # "stopPrice": "0", + # "workingType": "CONTRACT_PRICE", + # "priceProtect": False, + # "origType": "MARKET", + # "time": "1636061952660", + # "updateTime": "1636061952660" + # } + # + # option: createOrder, fetchOrder, fetchOpenOrders, fetchOrders + # + # { + # "orderId": 4728833085436977152, + # "symbol": "ETH-230211-1500-C", + # "price": "10.0", + # "quantity": "1.00", + # "executedQty": "0.00", + # "fee": "0", + # "side": "BUY", + # "type": "LIMIT", + # "timeInForce": "GTC", + # "reduceOnly": False, + # "postOnly": False, + # "createTime": 1676083034462, + # "updateTime": 1676083034462, + # "status": "ACCEPTED", + # "avgPrice": "0", + # "source": "API", + # "clientOrderId": "", + # "priceScale": 1, + # "quantityScale": 2, + # "optionSide": "CALL", + # "quoteAsset": "USDT", + # "lastTrade": {"id":"69","time":"1676084430567","price":"24.9","qty":"1.00"}, + # "mmp": False + # } + # + # cancelOrders/createOrders + # + # { + # "code": -4005, + # "msg": "Quantity greater than max quantity." + # } + # + # createOrder, fetchOpenOrders, fetchOrder, cancelOrder, fetchOrders: portfolio margin linear swap and future + # + # { + # "symbol": "BTCUSDT", + # "side": "BUY", + # "executedQty": "0.000", + # "orderId": 258649539704, + # "goodTillDate": 0, + # "avgPrice": "0", + # "origQty": "0.010", + # "clientOrderId": "x-xcKtGhcu02573c6f15e544e990057b", + # "positionSide": "BOTH", + # "cumQty": "0.000", + # "updateTime": 1707110415436, + # "type": "LIMIT", + # "reduceOnly": False, + # "price": "35000.00", + # "cumQuote": "0.00000", + # "selfTradePreventionMode": "NONE", + # "timeInForce": "GTC", + # "status": "NEW" + # } + # + # createOrder, fetchOpenOrders, fetchOrder, cancelOrder, fetchOrders: portfolio margin inverse swap and future + # + # { + # "symbol": "ETHUSD_PERP", + # "side": "BUY", + # "cumBase": "0", + # "executedQty": "0", + # "orderId": 71275227732, + # "avgPrice": "0.00", + # "origQty": "1", + # "clientOrderId": "x-xcKtGhcuca5af3acfb5044198c5398", + # "positionSide": "BOTH", + # "cumQty": "0", + # "updateTime": 1707110994334, + # "type": "LIMIT", + # "pair": "ETHUSD", + # "reduceOnly": False, + # "price": "2000", + # "timeInForce": "GTC", + # "status": "NEW" + # } + # + # createOrder, fetchOpenOrders, fetchOpenOrder: portfolio margin linear swap and future conditional + # + # { + # "newClientStrategyId": "x-xcKtGhcu27f109953d6e4dc0974006", + # "strategyId": 3645916, + # "strategyStatus": "NEW", + # "strategyType": "STOP", + # "origQty": "0.010", + # "price": "35000.00", + # "reduceOnly": False, + # "side": "BUY", + # "positionSide": "BOTH", + # "stopPrice": "45000.00", + # "symbol": "BTCUSDT", + # "timeInForce": "GTC", + # "bookTime": 1707112625879, + # "updateTime": 1707112625879, + # "workingType": "CONTRACT_PRICE", + # "priceProtect": False, + # "goodTillDate": 0, + # "selfTradePreventionMode": "NONE" + # } + # + # createOrder, fetchOpenOrders: portfolio margin inverse swap and future conditional + # + # { + # "newClientStrategyId": "x-xcKtGhcuc6b86f053bb34933850739", + # "strategyId": 1423462, + # "strategyStatus": "NEW", + # "strategyType": "STOP", + # "origQty": "1", + # "price": "2000", + # "reduceOnly": False, + # "side": "BUY", + # "positionSide": "BOTH", + # "stopPrice": "3000", + # "symbol": "ETHUSD_PERP", + # "timeInForce": "GTC", + # "bookTime": 1707113098840, + # "updateTime": 1707113098840, + # "workingType": "CONTRACT_PRICE", + # "priceProtect": False + # } + # + # createOrder, cancelAllOrders, cancelOrder: portfolio margin spot margin + # + # { + # "clientOrderId": "x-TKT5PX2Fe9ef29d8346440f0b28b86", + # "cummulativeQuoteQty": "0.00000000", + # "executedQty": "0.00000000", + # "fills": [], + # "orderId": 24684460474, + # "origQty": "0.00100000", + # "price": "35000.00000000", + # "selfTradePreventionMode": "EXPIRE_MAKER", + # "side": "BUY", + # "status": "NEW", + # "symbol": "BTCUSDT", + # "timeInForce": "GTC", + # "transactTime": 1707113538870, + # "type": "LIMIT" + # } + # + # fetchOpenOrders, fetchOrder, fetchOrders: portfolio margin spot margin + # + # { + # "symbol": "BTCUSDT", + # "orderId": 24700763749, + # "clientOrderId": "x-TKT5PX2F6f724c2a4af6425f98c7b6", + # "price": "35000.00000000", + # "origQty": "0.00100000", + # "executedQty": "0.00000000", + # "cummulativeQuoteQty": "0.00000000", + # "status": "NEW", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "side": "BUY", + # "stopPrice": "0.00000000", + # "icebergQty": "0.00000000", + # "time": 1707199187679, + # "updateTime": 1707199187679, + # "isWorking": True, + # "accountId": 200180970, + # "selfTradePreventionMode": "EXPIRE_MAKER", + # "preventedMatchId": null, + # "preventedQuantity": null + # } + # + # cancelOrder: portfolio margin linear and inverse swap conditional + # + # { + # "strategyId": 3733211, + # "newClientStrategyId": "x-xcKtGhcuaf166172ed504cd1bc0396", + # "strategyType": "STOP", + # "strategyStatus": "CANCELED", + # "origQty": "0.010", + # "price": "35000.00", + # "reduceOnly": False, + # "side": "BUY", + # "positionSide": "BOTH", + # "stopPrice": "50000.00", # ignored with trailing orders + # "symbol": "BTCUSDT", + # "timeInForce": "GTC", + # "activatePrice": null, # only return with trailing orders + # "priceRate": null, # only return with trailing orders + # "bookTime": 1707270098774, + # "updateTime": 1707270119261, + # "workingType": "CONTRACT_PRICE", + # "priceProtect": False, + # "goodTillDate": 0, + # "selfTradePreventionMode": "NONE" + # } + # + # fetchOrders: portfolio margin linear and inverse swap conditional + # + # { + # "newClientStrategyId": "x-xcKtGhcuaf166172ed504cd1bc0396", + # "strategyId": 3733211, + # "strategyStatus": "CANCELLED", + # "strategyType": "STOP", + # "origQty": "0.010", + # "price": "35000", + # "orderId": 0, + # "reduceOnly": False, + # "side": "BUY", + # "positionSide": "BOTH", + # "stopPrice": "50000", + # "symbol": "BTCUSDT", + # "type": "LIMIT", + # "bookTime": 1707270098774, + # "updateTime": 1707270119261, + # "timeInForce": "GTC", + # "triggerTime": 0, + # "workingType": "CONTRACT_PRICE", + # "priceProtect": False, + # "goodTillDate": 0, + # "selfTradePreventionMode": "NONE" + # } + # + # fetchOpenOrder: linear swap + # + # { + # "orderId": 3697213934, + # "symbol": "BTCUSDT", + # "status": "NEW", + # "clientOrderId": "x-xcKtGhcufb20c5a7761a4aa09aa156", + # "price": "33000.00", + # "avgPrice": "0.00000", + # "origQty": "0.010", + # "executedQty": "0.000", + # "cumQuote": "0.00000", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "reduceOnly": False, + # "closePosition": False, + # "side": "BUY", + # "positionSide": "BOTH", + # "stopPrice": "0.00", + # "workingType": "CONTRACT_PRICE", + # "priceProtect": False, + # "origType": "LIMIT", + # "priceMatch": "NONE", + # "selfTradePreventionMode": "NONE", + # "goodTillDate": 0, + # "time": 1707892893502, + # "updateTime": 1707892893515 + # } + # + # fetchOpenOrder: inverse swap + # + # { + # "orderId": 597368542, + # "symbol": "BTCUSD_PERP", + # "pair": "BTCUSD", + # "status": "NEW", + # "clientOrderId": "x-xcKtGhcubbde7ba93b1a4ab881eff3", + # "price": "35000", + # "avgPrice": "0", + # "origQty": "1", + # "executedQty": "0", + # "cumBase": "0", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "reduceOnly": False, + # "closePosition": False, + # "side": "BUY", + # "positionSide": "BOTH", + # "stopPrice": "0", + # "workingType": "CONTRACT_PRICE", + # "priceProtect": False, + # "origType": "LIMIT", + # "time": 1707893453199, + # "updateTime": 1707893453199 + # } + # + # fetchOpenOrder: linear portfolio margin + # + # { + # "orderId": 264895013409, + # "symbol": "BTCUSDT", + # "status": "NEW", + # "clientOrderId": "x-xcKtGhcu6278f1adbdf14f74ab432e", + # "price": "35000", + # "avgPrice": "0", + # "origQty": "0.010", + # "executedQty": "0", + # "cumQuote": "0", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "reduceOnly": False, + # "side": "BUY", + # "positionSide": "LONG", + # "origType": "LIMIT", + # "time": 1707893839364, + # "updateTime": 1707893839364, + # "goodTillDate": 0, + # "selfTradePreventionMode": "NONE" + # } + # + # fetchOpenOrder: inverse portfolio margin + # + # { + # "orderId": 71790316950, + # "symbol": "ETHUSD_PERP", + # "pair": "ETHUSD", + # "status": "NEW", + # "clientOrderId": "x-xcKtGhcuec11030474204ab08ba2c2", + # "price": "2500", + # "avgPrice": "0", + # "origQty": "1", + # "executedQty": "0", + # "cumBase": "0", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "reduceOnly": False, + # "side": "BUY", + # "positionSide": "LONG", + # "origType": "LIMIT", + # "time": 1707894181694, + # "updateTime": 1707894181694 + # } + # + # fetchOpenOrder: inverse portfolio margin conditional + # + # { + # "newClientStrategyId": "x-xcKtGhcu2da9c765294b433994ffce", + # "strategyId": 1423501, + # "strategyStatus": "NEW", + # "strategyType": "STOP", + # "origQty": "1", + # "price": "2500", + # "reduceOnly": False, + # "side": "BUY", + # "positionSide": "LONG", + # "stopPrice": "4000", + # "symbol": "ETHUSD_PERP", + # "bookTime": 1707894782679, + # "updateTime": 1707894782679, + # "timeInForce": "GTC", + # "workingType": "CONTRACT_PRICE", + # "priceProtect": False + # } + # + code = self.safe_string(order, 'code') + if code is not None: + # cancelOrders/createOrders might have a partial success + return self.safe_order({'info': order, 'status': 'rejected'}, market) + status = self.parse_order_status(self.safe_string_2(order, 'status', 'strategyStatus')) + marketId = self.safe_string(order, 'symbol') + isContract = ('positionSide' in order) or ('cumQuote' in order) + marketType = 'contract' if isContract else 'spot' + symbol = self.safe_symbol(marketId, market, None, marketType) + filled = self.safe_string(order, 'executedQty', '0') + timestamp = self.safe_integer_n(order, ['time', 'createTime', 'workingTime', 'transactTime', 'updateTime']) # order of the keys matters here + lastTradeTimestamp = None + if ('transactTime' in order) or ('updateTime' in order): + timestampValue = self.safe_integer_2(order, 'updateTime', 'transactTime') + if status == 'open': + if Precise.string_gt(filled, '0'): + lastTradeTimestamp = timestampValue + elif status == 'closed': + lastTradeTimestamp = timestampValue + lastUpdateTimestamp = self.safe_integer_2(order, 'transactTime', 'updateTime') + average = self.safe_string(order, 'avgPrice') + price = self.safe_string(order, 'price') + amount = self.safe_string_2(order, 'origQty', 'quantity') + # - Spot/Margin market: cummulativeQuoteQty + # - Futures market: cumQuote. + # Note self is not the actual cost, since Binance futures uses leverage to calculate margins. + cost = self.safe_string_2(order, 'cummulativeQuoteQty', 'cumQuote') + cost = self.safe_string(order, 'cumBase', cost) + type = self.safe_string_lower(order, 'type') + side = self.safe_string_lower(order, 'side') + fills = self.safe_list(order, 'fills', []) + timeInForce = self.safe_string(order, 'timeInForce') + if timeInForce == 'GTX': + # GTX means "Good Till Crossing" and is an equivalent way of saying Post Only + timeInForce = 'PO' + postOnly = (type == 'limit_maker') or (timeInForce == 'PO') + if type == 'limit_maker': + type = 'limit' + stopPriceString = self.safe_string(order, 'stopPrice') + triggerPrice = self.parse_number(self.omit_zero(stopPriceString)) + feeCost = self.safe_number(order, 'fee') + fee = None + if feeCost is not None: + fee = { + 'currency': self.safe_string(order, 'quoteAsset'), + 'cost': feeCost, + 'rate': None, + } + return self.safe_order({ + 'info': order, + 'id': self.safe_string_2(order, 'strategyId', 'orderId'), + 'clientOrderId': self.safe_string_2(order, 'clientOrderId', 'newClientStrategyId'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': lastTradeTimestamp, + 'lastUpdateTimestamp': lastUpdateTimestamp, + 'symbol': symbol, + 'type': type, + 'timeInForce': timeInForce, + 'postOnly': postOnly, + 'reduceOnly': self.safe_bool(order, 'reduceOnly'), + 'side': side, + 'price': price, + 'triggerPrice': triggerPrice, + 'amount': amount, + 'cost': cost, + 'average': average, + 'filled': filled, + 'remaining': None, + 'status': status, + 'fee': fee, + 'trades': fills, + }, market) + + async def create_orders(self, orders: List[OrderRequest], params={}): + """ + *contract only* create a list of trade orders + + https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/rest-api/Place-Multiple-Orders + https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Place-Multiple-Orders + https://developers.binance.com/docs/derivatives/option/trade/Place-Multiple-Orders + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + ordersRequests = [] + orderSymbols = [] + for i in range(0, len(orders)): + rawOrder = orders[i] + marketId = self.safe_string(rawOrder, 'symbol') + orderSymbols.append(marketId) + type = self.safe_string(rawOrder, 'type') + side = self.safe_string(rawOrder, 'side') + amount = self.safe_value(rawOrder, 'amount') + price = self.safe_value(rawOrder, 'price') + orderParams = self.safe_dict(rawOrder, 'params', {}) + orderRequest = self.create_order_request(marketId, type, side, amount, price, orderParams) + ordersRequests.append(orderRequest) + orderSymbols = self.market_symbols(orderSymbols, None, False, True, True) + market = self.market(orderSymbols[0]) + if market['spot']: + raise NotSupported(self.id + ' createOrders() does not support ' + market['type'] + ' orders') + response = None + request: dict = { + 'batchOrders': ordersRequests, + } + request = self.extend(request, params) + if market['linear']: + response = await self.fapiPrivatePostBatchOrders(request) + elif market['option']: + response = await self.eapiPrivatePostBatchOrders(request) + else: + response = await self.dapiPrivatePostBatchOrders(request) + # + # [ + # { + # "code": -4005, + # "msg": "Quantity greater than max quantity." + # }, + # { + # "orderId": 650640530, + # "symbol": "LTCUSDT", + # "status": "NEW", + # "clientOrderId": "x-xcKtGhcu32184eb13585491289bbaf", + # "price": "54.00", + # "avgPrice": "0.00", + # "origQty": "0.100", + # "executedQty": "0.000", + # "cumQty": "0.000", + # "cumQuote": "0.00000", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "reduceOnly": False, + # "closePosition": False, + # "side": "BUY", + # "positionSide": "BOTH", + # "stopPrice": "0.00", + # "workingType": "CONTRACT_PRICE", + # "priceProtect": False, + # "origType": "LIMIT", + # "priceMatch": "NONE", + # "selfTradePreventionMode": "NONE", + # "goodTillDate": 0, + # "updateTime": 1698073926929 + # } + # ] + # + return self.parse_orders(response) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://developers.binance.com/docs/binance-spot-api-docs/rest-api/trading-endpoints#new-order-trade + https://developers.binance.com/docs/binance-spot-api-docs/testnet/rest-api/trading-endpoints#test-new-order-trade + https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/New-Order + https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/rest-api + https://developers.binance.com/docs/derivatives/option/trade/New-Order + https://developers.binance.com/docs/binance-spot-api-docs/rest-api/trading-endpoints#sor + https://developers.binance.com/docs/binance-spot-api-docs/testnet/rest-api/trading-endpoints#sor + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/New-UM-Order + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/New-CM-Order + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/New-Margin-Order + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/New-UM-Conditional-Order + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/New-CM-Conditional-Order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' or 'STOP_LOSS' or 'STOP_LOSS_LIMIT' or 'TAKE_PROFIT' or 'TAKE_PROFIT_LIMIT' or 'STOP' + :param str side: 'buy' or 'sell' + :param float amount: how much of you want to trade in units of the base currency + :param float [price]: the price that the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.reduceOnly]: for swap and future reduceOnly is a string 'true' or 'false' that cant be sent with close position set to True or in hedge mode. For spot margin and option reduceOnly is a boolean. + :param str [params.marginMode]: 'cross' or 'isolated', for spot margin trading + :param boolean [params.sor]: *spot only* whether to use SOR(Smart Order Routing) or not, default is False + :param boolean [params.test]: *spot only* whether to use the test endpoint or not, default is False + :param float [params.trailingPercent]: the percent to trail away from the current market price + :param float [params.trailingTriggerPrice]: the price to trigger a trailing order, default uses the price argument + :param float [params.triggerPrice]: the price that a trigger order is triggered at + :param float [params.stopLossPrice]: the price that a stop loss order is triggered at + :param float [params.takeProfitPrice]: the price that a take profit order is triggered at + :param boolean [params.portfolioMargin]: set to True if you would like to create an order in a portfolio margin account + :param str [params.selfTradePrevention]: set unified value for stp, one of NONE, EXPIRE_MAKER, EXPIRE_TAKER or EXPIRE_BOTH + :param float [params.icebergAmount]: set iceberg amount for limit orders + :param str [params.stopLossOrTakeProfit]: 'stopLoss' or 'takeProfit', required for spot trailing orders + :param str [params.positionSide]: *swap and portfolio margin only* "BOTH" for one-way mode, "LONG" for buy side of hedged mode, "SHORT" for sell side of hedged mode + :param bool [params.hedged]: *swap and portfolio margin only* True for hedged mode, False for one way mode, default is False + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + # don't handle/omit params here, omitting happens inside createOrderRequest + marketType = self.safe_string(params, 'type', market['type']) + marginMode = self.safe_string(params, 'marginMode') + porfolioOptionsValue = self.safe_bool_2(self.options, 'papi', 'portfolioMargin', False) + isPortfolioMargin = self.safe_bool_2(params, 'papi', 'portfolioMargin', porfolioOptionsValue) + triggerPrice = self.safe_string_2(params, 'triggerPrice', 'stopPrice') + stopLossPrice = self.safe_string(params, 'stopLossPrice') + takeProfitPrice = self.safe_string(params, 'takeProfitPrice') + trailingPercent = self.safe_string_2(params, 'trailingPercent', 'callbackRate') + isTrailingPercentOrder = trailingPercent is not None + isStopLoss = stopLossPrice is not None + isTakeProfit = takeProfitPrice is not None + isConditional = (triggerPrice is not None) or isTrailingPercentOrder or isStopLoss or isTakeProfit + sor = self.safe_bool_2(params, 'sor', 'SOR', False) + test = self.safe_bool(params, 'test', False) + params = self.omit(params, ['sor', 'SOR', 'test']) + # if isPortfolioMargin: + # params['portfolioMargin'] = isPortfolioMargin + # } + request = self.create_order_request(symbol, type, side, amount, price, params) + response = None + if market['option']: + response = await self.eapiPrivatePostOrder(request) + elif sor: + if test: + response = await self.privatePostSorOrderTest(request) + else: + response = await self.privatePostSorOrder(request) + elif market['linear']: + if isPortfolioMargin: + if isConditional: + response = await self.papiPostUmConditionalOrder(request) + else: + response = await self.papiPostUmOrder(request) + else: + response = await self.fapiPrivatePostOrder(request) + elif market['inverse']: + if isPortfolioMargin: + if isConditional: + response = await self.papiPostCmConditionalOrder(request) + else: + response = await self.papiPostCmOrder(request) + else: + response = await self.dapiPrivatePostOrder(request) + elif marketType == 'margin' or marginMode is not None or isPortfolioMargin: + if isPortfolioMargin: + response = await self.papiPostMarginOrder(request) + else: + response = await self.sapiPostMarginOrder(request) + else: + if test: + response = await self.privatePostOrderTest(request) + else: + response = await self.privatePostOrder(request) + return self.parse_order(response, market) + + def create_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + @ignore + helper function to build the request + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency + :param float [price]: the price that the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: request to be sent to the exchange + """ + market = self.market(symbol) + marketType = self.safe_string(params, 'type', market['type']) + clientOrderId = self.safe_string_2(params, 'newClientOrderId', 'clientOrderId') + initialUppercaseType = type.upper() + isMarketOrder = initialUppercaseType == 'MARKET' + isLimitOrder = initialUppercaseType == 'LIMIT' + request: dict = { + 'symbol': market['id'], + 'side': side.upper(), + } + isPortfolioMargin = None + isPortfolioMargin, params = self.handle_option_and_params_2(params, 'createOrder', 'papi', 'portfolioMargin', False) + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('createOrder', params) + reduceOnly = self.safe_bool(params, 'reduceOnly', False) + if reduceOnly: + if marketType == 'margin' or (not market['contract'] and (marginMode is not None)): + params = self.omit(params, 'reduceOnly') + request['sideEffectType'] = 'AUTO_REPAY' + triggerPrice = self.safe_string_2(params, 'triggerPrice', 'stopPrice') + stopLossPrice = self.safe_string(params, 'stopLossPrice', triggerPrice) # fallback to stopLoss + takeProfitPrice = self.safe_string(params, 'takeProfitPrice') + trailingDelta = self.safe_string(params, 'trailingDelta') + trailingTriggerPrice = self.safe_string_2(params, 'trailingTriggerPrice', 'activationPrice') + trailingPercent = self.safe_string_n(params, ['trailingPercent', 'callbackRate', 'trailingDelta']) + priceMatch = self.safe_string(params, 'priceMatch') + isTrailingPercentOrder = trailingPercent is not None + isStopLoss = stopLossPrice is not None or trailingDelta is not None + isTakeProfit = takeProfitPrice is not None + isTriggerOrder = triggerPrice is not None + isConditional = isTriggerOrder or isTrailingPercentOrder or isStopLoss or isTakeProfit + isPortfolioMarginConditional = (isPortfolioMargin and isConditional) + isPriceMatch = priceMatch is not None + priceRequiredForTrailing = True + uppercaseType = type.upper() + stopPrice = None + if isTrailingPercentOrder: + if market['swap']: + uppercaseType = 'TRAILING_STOP_MARKET' + request['callbackRate'] = trailingPercent + if trailingTriggerPrice is not None: + request['activationPrice'] = self.price_to_precision(symbol, trailingTriggerPrice) + else: + if (uppercaseType != 'STOP_LOSS') and (uppercaseType != 'TAKE_PROFIT') and (uppercaseType != 'STOP_LOSS_LIMIT') and (uppercaseType != 'TAKE_PROFIT_LIMIT'): + stopLossOrTakeProfit = self.safe_string(params, 'stopLossOrTakeProfit') + params = self.omit(params, 'stopLossOrTakeProfit') + if (stopLossOrTakeProfit != 'stopLoss') and (stopLossOrTakeProfit != 'takeProfit'): + raise InvalidOrder(self.id + symbol + ' trailingPercent orders require a stopLossOrTakeProfit parameter of either stopLoss or takeProfit') + if isMarketOrder: + if stopLossOrTakeProfit == 'stopLoss': + uppercaseType = 'STOP_LOSS' + elif stopLossOrTakeProfit == 'takeProfit': + uppercaseType = 'TAKE_PROFIT' + else: + if stopLossOrTakeProfit == 'stopLoss': + uppercaseType = 'STOP_LOSS_LIMIT' + elif stopLossOrTakeProfit == 'takeProfit': + uppercaseType = 'TAKE_PROFIT_LIMIT' + if (uppercaseType == 'STOP_LOSS') or (uppercaseType == 'TAKE_PROFIT'): + priceRequiredForTrailing = False + if trailingTriggerPrice is not None: + stopPrice = self.price_to_precision(symbol, trailingTriggerPrice) + trailingPercentConverted = Precise.string_mul(trailingPercent, '100') + request['trailingDelta'] = trailingPercentConverted + elif isStopLoss: + stopPrice = stopLossPrice + if isMarketOrder: + # spot STOP_LOSS market orders are not a valid order type + uppercaseType = 'STOP_MARKET' if market['contract'] else 'STOP_LOSS' + elif isLimitOrder: + uppercaseType = 'STOP' if market['contract'] else 'STOP_LOSS_LIMIT' + elif isTakeProfit: + stopPrice = takeProfitPrice + if isMarketOrder: + # spot TAKE_PROFIT market orders are not a valid order type + uppercaseType = 'TAKE_PROFIT_MARKET' if market['contract'] else 'TAKE_PROFIT' + elif isLimitOrder: + uppercaseType = 'TAKE_PROFIT' if market['contract'] else 'TAKE_PROFIT_LIMIT' + if market['option']: + if type == 'market': + raise InvalidOrder(self.id + ' ' + type + ' is not a valid order type for the ' + symbol + ' market') + else: + validOrderTypes = self.safe_list(market['info'], 'orderTypes') + if not self.in_array(uppercaseType, validOrderTypes): + if initialUppercaseType != uppercaseType: + raise InvalidOrder(self.id + ' triggerPrice parameter is not allowed for ' + symbol + ' ' + type + ' orders') + else: + raise InvalidOrder(self.id + ' ' + type + ' is not a valid order type for the ' + symbol + ' market') + clientOrderIdRequest = 'newClientStrategyId' if isPortfolioMarginConditional else 'newClientOrderId' + if clientOrderId is None: + broker = self.safe_dict(self.options, 'broker', {}) + defaultId = 'x-xcKtGhcu' if (market['contract']) else 'x-TKT5PX2F' + idMarketType = 'spot' + if market['contract']: + idMarketType = 'swap' if (market['swap'] and market['linear']) else 'inverse' + brokerId = self.safe_string(broker, idMarketType, defaultId) + request[clientOrderIdRequest] = brokerId + self.uuid22() + else: + request[clientOrderIdRequest] = clientOrderId + postOnly = None + if not isPortfolioMargin: + postOnly = self.is_post_only(isMarketOrder, initialUppercaseType == 'LIMIT_MAKER', params) + if market['spot'] or marketType == 'margin': + # only supported for spot/margin api(all margin markets are spot markets) + if postOnly: + uppercaseType = 'LIMIT_MAKER' + if marginMode == 'isolated': + request['isIsolated'] = True + else: + postOnly = self.is_post_only(isMarketOrder, initialUppercaseType == 'LIMIT_MAKER', params) + if postOnly: + if not market['contract']: + uppercaseType = 'LIMIT_MAKER' + else: + request['timeInForce'] = 'GTX' + # handle newOrderRespType response type + if ((marketType == 'spot') or (marketType == 'margin')) and not isPortfolioMargin: + request['newOrderRespType'] = self.safe_string(self.options['newOrderRespType'], type, 'FULL') # 'ACK' for order id, 'RESULT' for full order or 'FULL' for order with fills + else: + # swap, futures and options + request['newOrderRespType'] = 'RESULT' # "ACK", "RESULT", default "ACK" + typeRequest = 'strategyType' if isPortfolioMarginConditional else 'type' + request[typeRequest] = uppercaseType + # additional required fields depending on the order type + closePosition = self.safe_bool(params, 'closePosition', False) + timeInForceIsRequired = False + priceIsRequired = False + triggerPriceIsRequired = False + quantityIsRequired = False + # + # spot/margin + # + # LIMIT timeInForce, quantity, price + # MARKET quantity or quoteOrderQty + # STOP_LOSS quantity, stopPrice + # STOP_LOSS_LIMIT timeInForce, quantity, price, stopPrice + # TAKE_PROFIT quantity, stopPrice + # TAKE_PROFIT_LIMIT timeInForce, quantity, price, stopPrice + # LIMIT_MAKER quantity, price + # + # futures + # + # LIMIT timeInForce, quantity, price + # MARKET quantity + # STOP/TAKE_PROFIT quantity, price, stopPrice + # STOP_MARKET stopPrice + # TAKE_PROFIT_MARKET stopPrice + # TRAILING_STOP_MARKET callbackRate + # + if uppercaseType == 'MARKET': + if market['spot']: + quoteOrderQty = self.safe_bool(self.options, 'quoteOrderQty', True) + if quoteOrderQty: + quoteOrderQtyNew = self.safe_string_2(params, 'quoteOrderQty', 'cost') + precision = market['precision']['price'] + if quoteOrderQtyNew is not None: + request['quoteOrderQty'] = self.decimal_to_precision(quoteOrderQtyNew, TRUNCATE, precision, self.precisionMode) + elif price is not None: + amountString = self.number_to_string(amount) + priceString = self.number_to_string(price) + quoteOrderQuantity = Precise.string_mul(amountString, priceString) + request['quoteOrderQty'] = self.decimal_to_precision(quoteOrderQuantity, TRUNCATE, precision, self.precisionMode) + else: + quantityIsRequired = True + else: + quantityIsRequired = True + else: + quantityIsRequired = True + elif uppercaseType == 'LIMIT': + priceIsRequired = True + timeInForceIsRequired = True + quantityIsRequired = True + elif (uppercaseType == 'STOP_LOSS') or (uppercaseType == 'TAKE_PROFIT'): + triggerPriceIsRequired = True + quantityIsRequired = True + if (market['linear'] or market['inverse']) and priceRequiredForTrailing: + priceIsRequired = True + elif (uppercaseType == 'STOP_LOSS_LIMIT') or (uppercaseType == 'TAKE_PROFIT_LIMIT'): + quantityIsRequired = True + triggerPriceIsRequired = True + priceIsRequired = True + timeInForceIsRequired = True + elif uppercaseType == 'LIMIT_MAKER': + priceIsRequired = True + quantityIsRequired = True + elif uppercaseType == 'STOP': + quantityIsRequired = True + triggerPriceIsRequired = True + priceIsRequired = True + elif (uppercaseType == 'STOP_MARKET') or (uppercaseType == 'TAKE_PROFIT_MARKET'): + if not closePosition: + quantityIsRequired = True + triggerPriceIsRequired = True + elif uppercaseType == 'TRAILING_STOP_MARKET': + if not closePosition: + quantityIsRequired = True + if trailingPercent is None: + raise InvalidOrder(self.id + ' createOrder() requires a trailingPercent param for a ' + type + ' order') + if quantityIsRequired: + marketAmountPrecision = self.safe_string(market['precision'], 'amount') + isPrecisionAvailable = (marketAmountPrecision is not None) + if isPrecisionAvailable: + request['quantity'] = self.amount_to_precision(symbol, amount) + else: + request['quantity'] = self.parse_to_numeric(amount) # some options don't have the precision available + if priceIsRequired and not isPriceMatch: + if price is None: + raise InvalidOrder(self.id + ' createOrder() requires a price argument for a ' + type + ' order') + pricePrecision = self.safe_string(market['precision'], 'price') + isPricePrecisionAvailable = (pricePrecision is not None) + if isPricePrecisionAvailable: + request['price'] = self.price_to_precision(symbol, price) + else: + request['price'] = self.parse_to_numeric(price) # some options don't have the precision available + if triggerPriceIsRequired: + if market['contract']: + if stopPrice is None: + raise InvalidOrder(self.id + ' createOrder() requires a triggerPrice extra param for a ' + type + ' order') + else: + # check for delta price + if trailingDelta is None and stopPrice is None and trailingPercent is None: + raise InvalidOrder(self.id + ' createOrder() requires a triggerPrice, trailingDelta or trailingPercent param for a ' + type + ' order') + if stopPrice is not None: + request['stopPrice'] = self.price_to_precision(symbol, stopPrice) + if timeInForceIsRequired and (self.safe_string(params, 'timeInForce') is None) and (self.safe_string(request, 'timeInForce') is None): + request['timeInForce'] = self.safe_string(self.options, 'defaultTimeInForce') # 'GTC' = Good To Cancel(default), 'IOC' = Immediate Or Cancel + if not isPortfolioMargin and market['contract'] and postOnly: + request['timeInForce'] = 'GTX' + # remove timeInForce from params because PO is only used by self.is_post_only and it's not a valid value for Binance + if self.safe_string(params, 'timeInForce') == 'PO': + params = self.omit(params, 'timeInForce') + hedged = self.safe_bool(params, 'hedged', False) + if not market['spot'] and not market['option'] and hedged: + if reduceOnly: + params = self.omit(params, 'reduceOnly') + side = 'sell' if (side == 'buy') else 'buy' + request['positionSide'] = 'LONG' if (side == 'buy') else 'SHORT' + # unified stp + selfTradePrevention = None + selfTradePrevention, params = self.handle_option_and_params(params, 'createOrder', 'selfTradePrevention') + if selfTradePrevention is not None: + if market['spot']: + request['selfTradePreventionMode'] = selfTradePrevention.upper() # binance enums exactly match the unified ccxt enums(but needs uppercase) + # unified iceberg + icebergAmount = self.safe_number(params, 'icebergAmount') + if icebergAmount is not None: + if market['spot']: + request['icebergQty'] = self.amount_to_precision(symbol, icebergAmount) + requestParams = self.omit(params, ['type', 'newClientOrderId', 'clientOrderId', 'postOnly', 'stopLossPrice', 'takeProfitPrice', 'stopPrice', 'triggerPrice', 'trailingTriggerPrice', 'trailingPercent', 'quoteOrderQty', 'cost', 'test', 'hedged', 'icebergAmount']) + return self.extend(request, requestParams) + + async def create_market_order_with_cost(self, symbol: str, side: OrderSide, cost: float, params={}): + """ + create a market order by providing the symbol, side and cost + + https://developers.binance.com/docs/binance-spot-api-docs/rest-api/trading-endpoints#new-order-trade + + :param str symbol: unified symbol of the market to create an order in + :param str side: 'buy' or 'sell' + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' createMarketOrderWithCost() supports spot orders only') + req = { + 'cost': cost, + } + return await self.create_order(symbol, 'market', side, cost, None, self.extend(req, params)) + + async def create_market_buy_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market buy order by providing the symbol and cost + + https://developers.binance.com/docs/binance-spot-api-docs/rest-api/trading-endpoints#new-order-trade + + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' createMarketBuyOrderWithCost() supports spot orders only') + req = { + 'cost': cost, + } + return await self.create_order(symbol, 'market', 'buy', cost, None, self.extend(req, params)) + + async def create_market_sell_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market sell order by providing the symbol and cost + + https://developers.binance.com/docs/binance-spot-api-docs/rest-api/trading-endpoints#new-order-trade + + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' createMarketSellOrderWithCost() supports spot orders only') + params['quoteOrderQty'] = cost + return await self.create_order(symbol, 'market', 'sell', cost, None, params) + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://developers.binance.com/docs/binance-spot-api-docs/rest-api/trading-endpoints#query-order-user_data + https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Query-Order + https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/rest-api/Query-Order + https://developers.binance.com/docs/derivatives/option/trade/Query-Single-Order + https://developers.binance.com/docs/margin_trading/trade/Query-Margin-Account-Order + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Query-UM-Order + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Query-CM-Order + + :param str id: the order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated', for spot margin trading + :param boolean [params.portfolioMargin]: set to True if you would like to fetch an order in a portfolio margin account + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrder() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + defaultType = self.safe_string_2(self.options, 'fetchOrder', 'defaultType', 'spot') + type = self.safe_string(params, 'type', defaultType) + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchOrder', params) + isPortfolioMargin = None + isPortfolioMargin, params = self.handle_option_and_params_2(params, 'fetchOrder', 'papi', 'portfolioMargin', False) + request: dict = { + 'symbol': market['id'], + } + clientOrderId = self.safe_string_2(params, 'origClientOrderId', 'clientOrderId') + if clientOrderId is not None: + if market['option']: + request['clientOrderId'] = clientOrderId + else: + request['origClientOrderId'] = clientOrderId + else: + request['orderId'] = id + params = self.omit(params, ['type', 'clientOrderId', 'origClientOrderId']) + response = None + if market['option']: + response = await self.eapiPrivateGetOrder(self.extend(request, params)) + elif market['linear']: + if isPortfolioMargin: + response = await self.papiGetUmOrder(self.extend(request, params)) + else: + response = await self.fapiPrivateGetOrder(self.extend(request, params)) + elif market['inverse']: + if isPortfolioMargin: + response = await self.papiGetCmOrder(self.extend(request, params)) + else: + response = await self.dapiPrivateGetOrder(self.extend(request, params)) + elif (type == 'margin') or (marginMode is not None) or isPortfolioMargin: + if isPortfolioMargin: + response = await self.papiGetMarginOrder(self.extend(request, params)) + else: + if marginMode == 'isolated': + request['isIsolated'] = True + response = await self.sapiGetMarginOrder(self.extend(request, params)) + else: + response = await self.privateGetOrder(self.extend(request, params)) + return self.parse_order(response, market) + + async def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://developers.binance.com/docs/binance-spot-api-docs/rest-api/trading-endpoints#all-orders-user_data + https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/All-Orders + https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/rest-api/All-Orders + https://developers.binance.com/docs/derivatives/option/trade/Query-Option-Order-History + https://developers.binance.com/docs/margin_trading/trade/Query-Margin-Account-All-Orders + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Query-All-UM-Orders + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Query-All-CM-Orders + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Query-All-UM-Conditional-Orders + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Query-All-CM-Conditional-Orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated', for spot margin trading + :param int [params.until]: the latest time in ms to fetch orders for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param boolean [params.portfolioMargin]: set to True if you would like to fetch orders in a portfolio margin account + :param boolean [params.trigger]: set to True if you would like to fetch portfolio margin account trigger or conditional orders + :returns Order[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrders() requires a symbol argument') + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOrders', 'paginate') + if paginate: + return await self.fetch_paginated_call_dynamic('fetchOrders', symbol, since, limit, params) + market = self.market(symbol) + defaultType = self.safe_string_2(self.options, 'fetchOrders', 'defaultType', market['type']) + type = self.safe_string(params, 'type', defaultType) + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchOrders', params) + isPortfolioMargin = None + isPortfolioMargin, params = self.handle_option_and_params_2(params, 'fetchOrders', 'papi', 'portfolioMargin', False) + isConditional = self.safe_bool_n(params, ['stop', 'trigger', 'conditional']) + params = self.omit(params, ['stop', 'trigger', 'conditional', 'type']) + request: dict = { + 'symbol': market['id'], + } + request, params = self.handle_until_option('endTime', request, params) + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + response = None + if market['option']: + response = await self.eapiPrivateGetHistoryOrders(self.extend(request, params)) + elif market['linear']: + if isPortfolioMargin: + if isConditional: + response = await self.papiGetUmConditionalAllOrders(self.extend(request, params)) + else: + response = await self.papiGetUmAllOrders(self.extend(request, params)) + else: + response = await self.fapiPrivateGetAllOrders(self.extend(request, params)) + elif market['inverse']: + if isPortfolioMargin: + if isConditional: + response = await self.papiGetCmConditionalAllOrders(self.extend(request, params)) + else: + response = await self.papiGetCmAllOrders(self.extend(request, params)) + else: + response = await self.dapiPrivateGetAllOrders(self.extend(request, params)) + else: + if isPortfolioMargin: + response = await self.papiGetMarginAllOrders(self.extend(request, params)) + elif type == 'margin' or marginMode is not None: + if marginMode == 'isolated': + request['isIsolated'] = True + response = await self.sapiGetMarginAllOrders(self.extend(request, params)) + else: + response = await self.privateGetAllOrders(self.extend(request, params)) + # + # spot + # + # [ + # { + # "symbol": "LTCBTC", + # "orderId": 1, + # "clientOrderId": "myOrder1", + # "price": "0.1", + # "origQty": "1.0", + # "executedQty": "0.0", + # "cummulativeQuoteQty": "0.0", + # "status": "NEW", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "side": "BUY", + # "stopPrice": "0.0", + # "icebergQty": "0.0", + # "time": 1499827319559, + # "updateTime": 1499827319559, + # "isWorking": True + # } + # ] + # + # futures + # + # [ + # { + # "symbol": "BTCUSDT", + # "orderId": 1, + # "clientOrderId": "myOrder1", + # "price": "0.1", + # "origQty": "1.0", + # "executedQty": "1.0", + # "cumQuote": "10.0", + # "status": "NEW", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "side": "BUY", + # "stopPrice": "0.0", + # "updateTime": 1499827319559 + # } + # ] + # + # options + # + # [ + # { + # "orderId": 4728833085436977152, + # "symbol": "ETH-230211-1500-C", + # "price": "10.0", + # "quantity": "1.00", + # "executedQty": "0.00", + # "fee": "0", + # "side": "BUY", + # "type": "LIMIT", + # "timeInForce": "GTC", + # "reduceOnly": False, + # "postOnly": False, + # "createTime": 1676083034462, + # "updateTime": 1676083034462, + # "status": "ACCEPTED", + # "avgPrice": "0", + # "source": "API", + # "clientOrderId": "", + # "priceScale": 1, + # "quantityScale": 2, + # "optionSide": "CALL", + # "quoteAsset": "USDT", + # "lastTrade": {"id":"69","time":"1676084430567","price":"24.9","qty":"1.00"}, + # "mmp": False + # } + # ] + # + # inverse portfolio margin + # + # [ + # { + # "orderId": 71328442983, + # "symbol": "ETHUSD_PERP", + # "pair": "ETHUSD", + # "status": "CANCELED", + # "clientOrderId": "x-xcKtGhcu4b3e3d8515dd4dc5ba9ccc", + # "price": "2000", + # "avgPrice": "0.00", + # "origQty": "1", + # "executedQty": "0", + # "cumBase": "0", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "reduceOnly": False, + # "side": "BUY", + # "origType": "LIMIT", + # "time": 1707197843046, + # "updateTime": 1707197941373, + # "positionSide": "BOTH" + # }, + # ] + # + # linear portfolio margin + # + # [ + # { + # "orderId": 259235347005, + # "symbol": "BTCUSDT", + # "status": "CANCELED", + # "clientOrderId": "x-xcKtGhcu402881c9103f42bdb4183b", + # "price": "35000", + # "avgPrice": "0.00000", + # "origQty": "0.010", + # "executedQty": "0", + # "cumQuote": "0", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "reduceOnly": False, + # "side": "BUY", + # "origType": "LIMIT", + # "time": 1707194702167, + # "updateTime": 1707197804748, + # "positionSide": "BOTH", + # "selfTradePreventionMode": "NONE", + # "goodTillDate": 0 + # }, + # ] + # + # conditional portfolio margin + # + # [ + # { + # "newClientStrategyId": "x-xcKtGhcuaf166172ed504cd1bc0396", + # "strategyId": 3733211, + # "strategyStatus": "CANCELLED", + # "strategyType": "STOP", + # "origQty": "0.010", + # "price": "35000", + # "orderId": 0, + # "reduceOnly": False, + # "side": "BUY", + # "positionSide": "BOTH", + # "stopPrice": "50000", + # "symbol": "BTCUSDT", + # "type": "LIMIT", + # "bookTime": 1707270098774, + # "updateTime": 1707270119261, + # "timeInForce": "GTC", + # "triggerTime": 0, + # "workingType": "CONTRACT_PRICE", + # "priceProtect": False, + # "goodTillDate": 0, + # "selfTradePreventionMode": "NONE" + # }, + # ] + # + # spot margin portfolio margin + # + # [ + # { + # "symbol": "BTCUSDT", + # "orderId": 24684460474, + # "clientOrderId": "x-TKT5PX2Fe9ef29d8346440f0b28b86", + # "price": "35000.00000000", + # "origQty": "0.00100000", + # "executedQty": "0.00000000", + # "cummulativeQuoteQty": "0.00000000", + # "status": "CANCELED", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "side": "BUY", + # "stopPrice": "0.00000000", + # "icebergQty": "0.00000000", + # "time": 1707113538870, + # "updateTime": 1707113797688, + # "isWorking": True, + # "accountId": 200180970, + # "selfTradePreventionMode": "EXPIRE_MAKER", + # "preventedMatchId": null, + # "preventedQuantity": null + # }, + # ] + # + return self.parse_orders(response, market, since, limit) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://developers.binance.com/docs/binance-spot-api-docs/rest-api/trading-endpoints#current-open-orders-user_data + https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Current-All-Open-Orders + https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/rest-api/Current-All-Open-Orders + https://developers.binance.com/docs/derivatives/option/trade/Query-Current-Open-Option-Orders + https://developers.binance.com/docs/margin_trading/trade/Query-Margin-Account-Open-Orders + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Query-All-Current-UM-Open-Orders + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Query-All-Current-UM-Open-Conditional-Orders + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Query-All-Current-CM-Open-Orders + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Query-All-Current-CM-Open-Conditional-Orders + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated', for spot margin trading + :param boolean [params.portfolioMargin]: set to True if you would like to fetch open orders in the portfolio margin account + :param boolean [params.trigger]: set to True if you would like to fetch portfolio margin account conditional orders + :param str [params.subType]: "linear" or "inverse" + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + market = None + type = None + request: dict = {} + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchOpenOrders', params) + isPortfolioMargin = None + isPortfolioMargin, params = self.handle_option_and_params_2(params, 'fetchOpenOrders', 'papi', 'portfolioMargin', False) + isConditional = self.safe_bool_n(params, ['stop', 'trigger', 'conditional']) + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + defaultType = self.safe_string_2(self.options, 'fetchOpenOrders', 'defaultType', 'spot') + marketType = market['type'] if ('type' in market) else defaultType + type = self.safe_string(params, 'type', marketType) + elif self.options['warnOnFetchOpenOrdersWithoutSymbol']: + raise ExchangeError(self.id + ' fetchOpenOrders() WARNING: fetching open orders without specifying a symbol has stricter rate limits(10 times more for spot, 40 times more for other markets) compared to requesting with symbol argument. To acknowledge self warning, set ' + self.id + '.options["warnOnFetchOpenOrdersWithoutSymbol"] = False to suppress self warning message.') + else: + defaultType = self.safe_string_2(self.options, 'fetchOpenOrders', 'defaultType', 'spot') + type = self.safe_string(params, 'type', defaultType) + subType = None + subType, params = self.handle_sub_type_and_params('fetchOpenOrders', market, params) + params = self.omit(params, ['type', 'stop', 'trigger', 'conditional']) + response = None + if type == 'option': + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + response = await self.eapiPrivateGetOpenOrders(self.extend(request, params)) + elif self.is_linear(type, subType): + if isPortfolioMargin: + if isConditional: + response = await self.papiGetUmConditionalOpenOrders(self.extend(request, params)) + else: + response = await self.papiGetUmOpenOrders(self.extend(request, params)) + else: + response = await self.fapiPrivateGetOpenOrders(self.extend(request, params)) + elif self.is_inverse(type, subType): + if isPortfolioMargin: + if isConditional: + response = await self.papiGetCmConditionalOpenOrders(self.extend(request, params)) + else: + response = await self.papiGetCmOpenOrders(self.extend(request, params)) + else: + response = await self.dapiPrivateGetOpenOrders(self.extend(request, params)) + elif type == 'margin' or marginMode is not None or isPortfolioMargin: + if isPortfolioMargin: + response = await self.papiGetMarginOpenOrders(self.extend(request, params)) + else: + if marginMode == 'isolated': + request['isIsolated'] = True + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOpenOrders() requires a symbol argument for isolated markets') + response = await self.sapiGetMarginOpenOrders(self.extend(request, params)) + else: + response = await self.privateGetOpenOrders(self.extend(request, params)) + return self.parse_orders(response, market, since, limit) + + async def fetch_open_order(self, id: str, symbol: Str = None, params={}): + """ + fetch an open order by the id + + https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Query-Current-Open-Order + https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/rest-api/Query-Current-Open-Order + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Query-Current-UM-Open-Order + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Query-Current-UM-Open-Conditional-Order + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Query-Current-CM-Open-Order + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Query-Current-CM-Open-Conditional-Order + + :param str id: order id + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.trigger]: set to True if you would like to fetch portfolio margin account stop or conditional orders + :param boolean [params.portfolioMargin]: set to True if you would like to fetch for a portfolio margin account + :returns dict: an `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOpenOrder() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + isPortfolioMargin = None + isPortfolioMargin, params = self.handle_option_and_params_2(params, 'fetchOpenOrder', 'papi', 'portfolioMargin', False) + isConditional = self.safe_bool_n(params, ['stop', 'trigger', 'conditional']) + params = self.omit(params, ['stop', 'trigger', 'conditional']) + isPortfolioMarginConditional = (isPortfolioMargin and isConditional) + orderIdRequest = 'strategyId' if isPortfolioMarginConditional else 'orderId' + request[orderIdRequest] = id + response = None + if market['linear']: + if isPortfolioMargin: + if isConditional: + response = await self.papiGetUmConditionalOpenOrder(self.extend(request, params)) + else: + response = await self.papiGetUmOpenOrder(self.extend(request, params)) + else: + response = await self.fapiPrivateGetOpenOrder(self.extend(request, params)) + elif market['inverse']: + if isPortfolioMargin: + if isConditional: + response = await self.papiGetCmConditionalOpenOrder(self.extend(request, params)) + else: + response = await self.papiGetCmOpenOrder(self.extend(request, params)) + else: + response = await self.dapiPrivateGetOpenOrder(self.extend(request, params)) + else: + if market['option']: + raise NotSupported(self.id + ' fetchOpenOrder() does not support option markets') + elif market['spot']: + raise NotSupported(self.id + ' fetchOpenOrder() does not support spot markets') + # + # linear swap + # + # { + # "orderId": 3697213934, + # "symbol": "BTCUSDT", + # "status": "NEW", + # "clientOrderId": "x-xcKtGhcufb20c5a7761a4aa09aa156", + # "price": "33000.00", + # "avgPrice": "0.00000", + # "origQty": "0.010", + # "executedQty": "0.000", + # "cumQuote": "0.00000", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "reduceOnly": False, + # "closePosition": False, + # "side": "BUY", + # "positionSide": "BOTH", + # "stopPrice": "0.00", + # "workingType": "CONTRACT_PRICE", + # "priceProtect": False, + # "origType": "LIMIT", + # "priceMatch": "NONE", + # "selfTradePreventionMode": "NONE", + # "goodTillDate": 0, + # "time": 1707892893502, + # "updateTime": 1707892893515 + # } + # + # inverse swap + # + # { + # "orderId": 597368542, + # "symbol": "BTCUSD_PERP", + # "pair": "BTCUSD", + # "status": "NEW", + # "clientOrderId": "x-xcKtGhcubbde7ba93b1a4ab881eff3", + # "price": "35000", + # "avgPrice": "0", + # "origQty": "1", + # "executedQty": "0", + # "cumBase": "0", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "reduceOnly": False, + # "closePosition": False, + # "side": "BUY", + # "positionSide": "BOTH", + # "stopPrice": "0", + # "workingType": "CONTRACT_PRICE", + # "priceProtect": False, + # "origType": "LIMIT", + # "time": 1707893453199, + # "updateTime": 1707893453199 + # } + # + # linear portfolio margin + # + # { + # "orderId": 264895013409, + # "symbol": "BTCUSDT", + # "status": "NEW", + # "clientOrderId": "x-xcKtGhcu6278f1adbdf14f74ab432e", + # "price": "35000", + # "avgPrice": "0", + # "origQty": "0.010", + # "executedQty": "0", + # "cumQuote": "0", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "reduceOnly": False, + # "side": "BUY", + # "positionSide": "LONG", + # "origType": "LIMIT", + # "time": 1707893839364, + # "updateTime": 1707893839364, + # "goodTillDate": 0, + # "selfTradePreventionMode": "NONE" + # } + # + # inverse portfolio margin + # + # { + # "orderId": 71790316950, + # "symbol": "ETHUSD_PERP", + # "pair": "ETHUSD", + # "status": "NEW", + # "clientOrderId": "x-xcKtGhcuec11030474204ab08ba2c2", + # "price": "2500", + # "avgPrice": "0", + # "origQty": "1", + # "executedQty": "0", + # "cumBase": "0", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "reduceOnly": False, + # "side": "BUY", + # "positionSide": "LONG", + # "origType": "LIMIT", + # "time": 1707894181694, + # "updateTime": 1707894181694 + # } + # + # linear portfolio margin conditional + # + # { + # "newClientStrategyId": "x-xcKtGhcu2205fde44418483ca21874", + # "strategyId": 4084339, + # "strategyStatus": "NEW", + # "strategyType": "STOP", + # "origQty": "0.010", + # "price": "35000", + # "reduceOnly": False, + # "side": "BUY", + # "positionSide": "LONG", + # "stopPrice": "60000", + # "symbol": "BTCUSDT", + # "bookTime": 1707894490094, + # "updateTime": 1707894490094, + # "timeInForce": "GTC", + # "workingType": "CONTRACT_PRICE", + # "priceProtect": False, + # "goodTillDate": 0, + # "selfTradePreventionMode": "NONE" + # } + # + # inverse portfolio margin conditional + # + # { + # "newClientStrategyId": "x-xcKtGhcu2da9c765294b433994ffce", + # "strategyId": 1423501, + # "strategyStatus": "NEW", + # "strategyType": "STOP", + # "origQty": "1", + # "price": "2500", + # "reduceOnly": False, + # "side": "BUY", + # "positionSide": "LONG", + # "stopPrice": "4000", + # "symbol": "ETHUSD_PERP", + # "bookTime": 1707894782679, + # "updateTime": 1707894782679, + # "timeInForce": "GTC", + # "workingType": "CONTRACT_PRICE", + # "priceProtect": False + # } + # + return self.parse_order(response, market) + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://developers.binance.com/docs/binance-spot-api-docs/rest-api/trading-endpoints#all-orders-user_data + https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/All-Orders + https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/rest-api/All-Orders + https://developers.binance.com/docs/derivatives/option/trade/Query-Option-Order-History + https://developers.binance.com/docs/margin_trading/trade/Query-Margin-Account-All-Orders + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Query-All-UM-Orders + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Query-All-CM-Orders + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Query-All-UM-Conditional-Orders + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Query-All-CM-Conditional-Orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param boolean [params.portfolioMargin]: set to True if you would like to fetch orders in a portfolio margin account + :param boolean [params.trigger]: set to True if you would like to fetch portfolio margin account trigger or conditional orders + :returns Order[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchClosedOrders() requires a symbol argument') + orders = await self.fetch_orders(symbol, since, None, params) + filteredOrders = self.filter_by(orders, 'status', 'closed') + return self.filter_by_since_limit(filteredOrders, since, limit) + + async def fetch_canceled_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches information on multiple canceled orders made by the user + + https://developers.binance.com/docs/binance-spot-api-docs/rest-api/trading-endpoints#all-orders-user_data + https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/All-Orders + https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/rest-api/All-Orders + https://developers.binance.com/docs/derivatives/option/trade/Query-Option-Order-History + https://developers.binance.com/docs/margin_trading/trade/Query-Margin-Account-All-Orders + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Query-All-UM-Orders + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Query-All-CM-Orders + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Query-All-UM-Conditional-Orders + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Query-All-CM-Conditional-Orders + + :param str symbol: unified market symbol of the market the orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param boolean [params.portfolioMargin]: set to True if you would like to fetch orders in a portfolio margin account + :param boolean [params.trigger]: set to True if you would like to fetch portfolio margin account trigger or conditional orders + :returns dict[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchCanceledOrders() requires a symbol argument') + orders = await self.fetch_orders(symbol, since, None, params) + filteredOrders = self.filter_by(orders, 'status', 'canceled') + return self.filter_by_since_limit(filteredOrders, since, limit) + + async def fetch_canceled_and_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple canceled orders made by the user + + https://developers.binance.com/docs/binance-spot-api-docs/rest-api/trading-endpoints#all-orders-user_data + https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/All-Orders + https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/rest-api/All-Orders + https://developers.binance.com/docs/derivatives/option/trade/Query-Option-Order-History + https://developers.binance.com/docs/margin_trading/trade/Query-Margin-Account-All-Orders + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Query-All-UM-Orders + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Query-All-CM-Orders + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Query-All-UM-Conditional-Orders + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Query-All-CM-Conditional-Orders + + :param str symbol: unified market symbol of the market the orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param boolean [params.portfolioMargin]: set to True if you would like to fetch orders in a portfolio margin account + :param boolean [params.trigger]: set to True if you would like to fetch portfolio margin account trigger or conditional orders + :returns dict[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchCanceledAndClosedOrders() requires a symbol argument') + orders = await self.fetch_orders(symbol, since, None, params) + canceledOrders = self.filter_by(orders, 'status', 'canceled') + closedOrders = self.filter_by(orders, 'status', 'closed') + filteredOrders = self.array_concat(canceledOrders, closedOrders) + sortedOrders = self.sort_by(filteredOrders, 'timestamp') + return self.filter_by_since_limit(sortedOrders, since, limit) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://developers.binance.com/docs/binance-spot-api-docs/rest-api/trading-endpoints#cancel-order-trade + https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Cancel-Order + https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/rest-api/Cancel-Order + https://developers.binance.com/docs/derivatives/option/trade/Cancel-Option-Order + https://developers.binance.com/docs/margin_trading/trade/Margin-Account-Cancel-Order + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Cancel-UM-Order + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Cancel-CM-Order + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Cancel-UM-Conditional-Order + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Cancel-CM-Conditional-Order + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Cancel-Margin-Account-Order + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.portfolioMargin]: set to True if you would like to cancel an order in a portfolio margin account + :param boolean [params.trigger]: set to True if you would like to cancel a portfolio margin account conditional order + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + defaultType = self.safe_string_2(self.options, 'cancelOrder', 'defaultType', 'spot') + type = self.safe_string(params, 'type', defaultType) + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('cancelOrder', params) + isPortfolioMargin = None + isPortfolioMargin, params = self.handle_option_and_params_2(params, 'cancelOrder', 'papi', 'portfolioMargin', False) + isConditional = self.safe_bool_n(params, ['stop', 'trigger', 'conditional']) + request: dict = { + 'symbol': market['id'], + } + clientOrderId = self.safe_string_n(params, ['origClientOrderId', 'clientOrderId', 'newClientStrategyId']) + if clientOrderId is not None: + if market['option']: + request['clientOrderId'] = clientOrderId + else: + if isPortfolioMargin and isConditional: + request['newClientStrategyId'] = clientOrderId + else: + request['origClientOrderId'] = clientOrderId + else: + if isPortfolioMargin and isConditional: + request['strategyId'] = id + else: + request['orderId'] = id + params = self.omit(params, ['type', 'origClientOrderId', 'clientOrderId', 'newClientStrategyId', 'stop', 'trigger', 'conditional']) + response = None + if market['option']: + response = await self.eapiPrivateDeleteOrder(self.extend(request, params)) + elif market['linear']: + if isPortfolioMargin: + if isConditional: + response = await self.papiDeleteUmConditionalOrder(self.extend(request, params)) + else: + response = await self.papiDeleteUmOrder(self.extend(request, params)) + else: + response = await self.fapiPrivateDeleteOrder(self.extend(request, params)) + elif market['inverse']: + if isPortfolioMargin: + if isConditional: + response = await self.papiDeleteCmConditionalOrder(self.extend(request, params)) + else: + response = await self.papiDeleteCmOrder(self.extend(request, params)) + else: + response = await self.dapiPrivateDeleteOrder(self.extend(request, params)) + elif (type == 'margin') or (marginMode is not None) or isPortfolioMargin: + if isPortfolioMargin: + response = await self.papiDeleteMarginOrder(self.extend(request, params)) + else: + if marginMode == 'isolated': + request['isIsolated'] = True + response = await self.sapiDeleteMarginOrder(self.extend(request, params)) + else: + response = await self.privateDeleteOrder(self.extend(request, params)) + return self.parse_order(response, market) + + async def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders in a market + + https://developers.binance.com/docs/binance-spot-api-docs/rest-api/trading-endpoints#cancel-all-open-orders-on-a-symbol-trade + https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Cancel-All-Open-Orders + https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/rest-api/Cancel-All-Open-Orders + https://developers.binance.com/docs/derivatives/option/trade/Cancel-all-Option-orders-on-specific-symbol + https://developers.binance.com/docs/margin_trading/trade/Margin-Account-Cancel-All-Open-Orders + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Cancel-All-UM-Open-Orders + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Cancel-All-UM-Open-Conditional-Orders + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Cancel-All-CM-Open-Orders + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Cancel-All-CM-Open-Conditional-Orders + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Cancel-Margin-Account-All-Open-Orders-on-a-Symbol + + :param str symbol: unified market symbol of the market to cancel orders in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated', for spot margin trading + :param boolean [params.portfolioMargin]: set to True if you would like to cancel orders in a portfolio margin account + :param boolean [params.trigger]: set to True if you would like to cancel portfolio margin account conditional orders + :returns dict[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelAllOrders() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + isPortfolioMargin = None + isPortfolioMargin, params = self.handle_option_and_params_2(params, 'cancelAllOrders', 'papi', 'portfolioMargin', False) + isConditional = self.safe_bool_n(params, ['stop', 'trigger', 'conditional']) + type = self.safe_string(params, 'type', market['type']) + params = self.omit(params, ['type', 'stop', 'trigger', 'conditional']) + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('cancelAllOrders', params) + response = None + if market['option']: + response = await self.eapiPrivateDeleteAllOpenOrders(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "success" + # } + # + elif market['linear']: + if isPortfolioMargin: + if isConditional: + response = await self.papiDeleteUmConditionalAllOpenOrders(self.extend(request, params)) + # + # { + # "code": "200", + # "msg": "The operation of cancel all conditional open order is done." + # } + # + else: + response = await self.papiDeleteUmAllOpenOrders(self.extend(request, params)) + # + # { + # "code": 200, + # "msg": "The operation of cancel all open order is done." + # } + # + else: + response = await self.fapiPrivateDeleteAllOpenOrders(self.extend(request, params)) + # + # { + # "code": 200, + # "msg": "The operation of cancel all open order is done." + # } + # + elif market['inverse']: + if isPortfolioMargin: + if isConditional: + response = await self.papiDeleteCmConditionalAllOpenOrders(self.extend(request, params)) + # + # { + # "code": "200", + # "msg": "The operation of cancel all conditional open order is done." + # } + # + else: + response = await self.papiDeleteCmAllOpenOrders(self.extend(request, params)) + # + # { + # "code": 200, + # "msg": "The operation of cancel all open order is done." + # } + # + else: + response = await self.dapiPrivateDeleteAllOpenOrders(self.extend(request, params)) + # + # { + # "code": 200, + # "msg": "The operation of cancel all open order is done." + # } + # + elif (type == 'margin') or (marginMode is not None) or isPortfolioMargin: + if isPortfolioMargin: + response = await self.papiDeleteMarginAllOpenOrders(self.extend(request, params)) + else: + if marginMode == 'isolated': + request['isIsolated'] = True + response = await self.sapiDeleteMarginOpenOrders(self.extend(request, params)) + # + # [ + # { + # "symbol": "BTCUSDT", + # "isIsolated": True, # if isolated margin + # "origClientOrderId": "E6APeyTJvkMvLMYMqu1KQ4", + # "orderId": 11, + # "orderListId": -1, + # "clientOrderId": "pXLV6Hz6mprAcVYpVMTGgx", + # "price": "0.089853", + # "origQty": "0.178622", + # "executedQty": "0.000000", + # "cummulativeQuoteQty": "0.000000", + # "status": "CANCELED", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "side": "BUY", + # "selfTradePreventionMode": "NONE" + # }, + # ... + # ] + # + else: + response = await self.privateDeleteOpenOrders(self.extend(request, params)) + # + # [ + # { + # "symbol": "ADAUSDT", + # "origClientOrderId": "x-TKT5PX2F662cde7a90114475b86e21", + # "orderId": 3935107, + # "orderListId": -1, + # "clientOrderId": "bqM2w1oTlugfRAjnTIFBE8", + # "transactTime": 1720589016657, + # "price": "0.35000000", + # "origQty": "30.00000000", + # "executedQty": "0.00000000", + # "cummulativeQuoteQty": "0.00000000", + # "status": "CANCELED", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "side": "BUY", + # "selfTradePreventionMode": "EXPIRE_MAKER" + # } + # ] + # + if isinstance(response, list): + return self.parse_orders(response, market) + else: + return [ + self.safe_order({ + 'info': response, + }), + ] + + async def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): + """ + cancel multiple orders + + https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Cancel-Multiple-Orders + https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/rest-api/Cancel-Multiple-Orders + + :param str[] ids: order ids + :param str [symbol]: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str[] [params.clientOrderIds]: alternative to ids, array of client order ids + + EXCHANGE SPECIFIC PARAMETERS + :param str[] [params.origClientOrderIdList]: max length 10 e.g. ["my_id_1","my_id_2"], encode the double quotes. No space after comma + :param int[] [params.recvWindow]: + :returns dict: an list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrders() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + if not market['contract']: + raise BadRequest(self.id + ' cancelOrders is only supported for swap markets.') + request: dict = { + 'symbol': market['id'], + # 'orderidlist': ids, + } + origClientOrderIdList = self.safe_list_2(params, 'origClientOrderIdList', 'clientOrderIds') + if origClientOrderIdList is not None: + params = self.omit(params, ['clientOrderIds']) + request['origClientOrderIdList'] = origClientOrderIdList + else: + request['orderidlist'] = ids + response = None + if market['linear']: + response = await self.fapiPrivateDeleteBatchOrders(self.extend(request, params)) + elif market['inverse']: + response = await self.dapiPrivateDeleteBatchOrders(self.extend(request, params)) + # + # [ + # { + # "clientOrderId": "myOrder1", + # "cumQty": "0", + # "cumQuote": "0", + # "executedQty": "0", + # "orderId": 283194212, + # "origQty": "11", + # "origType": "TRAILING_STOP_MARKET", + # "price": "0", + # "reduceOnly": False, + # "side": "BUY", + # "positionSide": "SHORT", + # "status": "CANCELED", + # "stopPrice": "9300", # please ignore when order type is TRAILING_STOP_MARKET + # "closePosition": False, # if Close-All + # "symbol": "BTCUSDT", + # "timeInForce": "GTC", + # "type": "TRAILING_STOP_MARKET", + # "activatePrice": "9020", # activation price, only return with TRAILING_STOP_MARKET order + # "priceRate": "0.3", # callback rate, only return with TRAILING_STOP_MARKET order + # "updateTime": 1571110484038, + # "workingType": "CONTRACT_PRICE", + # "priceProtect": False, # if conditional order trigger is protected + # "priceMatch": "NONE", # price match mode + # "selfTradePreventionMode": "NONE", # self trading preventation mode + # "goodTillDate": 0 # order pre-set auot cancel time for TIF GTD order + # }, + # { + # "code": -2011, + # "msg": "Unknown order sent." + # } + # ] + # + return self.parse_orders(response, market) + + async def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all the trades made from a single order + + https://developers.binance.com/docs/binance-spot-api-docs/rest-api/account-endpoints#account-trade-list-user_data + https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Account-Trade-List + https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/rest-api/Account-Trade-List + https://developers.binance.com/docs/margin_trading/trade/Query-Margin-Account-Trade-List + + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrderTrades() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + type = self.safe_string(params, 'type', market['type']) + params = self.omit(params, 'type') + if type != 'spot': + raise NotSupported(self.id + ' fetchOrderTrades() supports spot markets only') + request: dict = { + 'orderId': id, + } + return await self.fetch_my_trades(symbol, since, limit, self.extend(request, params)) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://developers.binance.com/docs/binance-spot-api-docs/rest-api/account-endpoints#account-trade-list-user_data + https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Account-Trade-List + https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/rest-api/Account-Trade-List + https://developers.binance.com/docs/margin_trading/trade/Query-Margin-Account-Trade-List + https://developers.binance.com/docs/derivatives/option/trade/Account-Trade-List + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/UM-Account-Trade-List + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/CM-Account-Trade-List + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param int [params.until]: the latest time in ms to fetch entries for + :param boolean [params.portfolioMargin]: set to True if you would like to fetch trades for a portfolio margin account + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchMyTrades', 'paginate') + if paginate: + return await self.fetch_paginated_call_dynamic('fetchMyTrades', symbol, since, limit, params) + request: dict = {} + market = None + type = None + marginMode = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + type, params = self.handle_market_type_and_params('fetchMyTrades', market, params) + endTime = self.safe_integer_2(params, 'until', 'endTime') + if since is not None: + startTime = since + request['startTime'] = startTime + # If startTime and endTime are both not sent, then the last 7 days' data will be returned. + # The time between startTime and endTime cannot be longer than 7 days. + # The parameter fromId cannot be sent with startTime or endTime. + currentTimestamp = self.milliseconds() + oneWeek = 7 * 24 * 60 * 60 * 1000 + if (currentTimestamp - startTime) >= oneWeek: + if (endTime is None) and market['linear']: + endTime = self.sum(startTime, oneWeek) + endTime = min(endTime, currentTimestamp) + if endTime is not None: + request['endTime'] = endTime + params = self.omit(params, ['endTime', 'until']) + if limit is not None: + if (type == 'option') or market['contract']: + limit = min(limit, 1000) # above 1000, returns error + request['limit'] = limit + response = None + if type == 'option': + response = await self.eapiPrivateGetUserTrades(self.extend(request, params)) + else: + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMyTrades() requires a symbol argument') + marginMode, params = self.handle_margin_mode_and_params('fetchMyTrades', params) + isPortfolioMargin = None + isPortfolioMargin, params = self.handle_option_and_params_2(params, 'fetchMyTrades', 'papi', 'portfolioMargin', False) + if type == 'spot' or type == 'margin': + if isPortfolioMargin: + response = await self.papiGetMarginMyTrades(self.extend(request, params)) + elif (type == 'margin') or (marginMode is not None): + if marginMode == 'isolated': + request['isIsolated'] = True + response = await self.sapiGetMarginMyTrades(self.extend(request, params)) + else: + response = await self.privateGetMyTrades(self.extend(request, params)) + elif market['linear']: + if isPortfolioMargin: + response = await self.papiGetUmUserTrades(self.extend(request, params)) + else: + response = await self.fapiPrivateGetUserTrades(self.extend(request, params)) + elif market['inverse']: + if isPortfolioMargin: + response = await self.papiGetCmUserTrades(self.extend(request, params)) + else: + response = await self.dapiPrivateGetUserTrades(self.extend(request, params)) + # + # spot trade + # + # [ + # { + # "symbol": "BNBBTC", + # "id": 28457, + # "orderId": 100234, + # "price": "4.00000100", + # "qty": "12.00000000", + # "commission": "10.10000000", + # "commissionAsset": "BNB", + # "time": 1499865549590, + # "isBuyer": True, + # "isMaker": False, + # "isBestMatch": True, + # } + # ] + # + # futures trade + # + # [ + # { + # "accountId": 20, + # "buyer": False, + # "commission": "-0.07819010", + # "commissionAsset": "USDT", + # "counterPartyId": 653, + # "id": 698759, + # "maker": False, + # "orderId": 25851813, + # "price": "7819.01", + # "qty": "0.002", + # "quoteQty": "0.01563", + # "realizedPnl": "-0.91539999", + # "side": "SELL", + # "symbol": "BTCUSDT", + # "time": 1569514978020 + # } + # ] + # + # options(eapi) + # + # [ + # { + # "id": 1125899906844226012, + # "tradeId": 73, + # "orderId": 4638761100843040768, + # "symbol": "ETH-230211-1500-C", + # "price": "18.70000000", + # "quantity": "-0.57000000", + # "fee": "0.17305890", + # "realizedProfit": "-3.53400000", + # "side": "SELL", + # "type": "LIMIT", + # "volatility": "0.30000000", + # "liquidity": "MAKER", + # "time": 1676085216845, + # "priceScale": 1, + # "quantityScale": 2, + # "optionSide": "CALL", + # "quoteAsset": "USDT" + # } + # ] + # + # linear portfolio margin + # + # [ + # { + # "symbol": "BTCUSDT", + # "id": 4575108247, + # "orderId": 261942655610, + # "side": "SELL", + # "price": "47263.40", + # "qty": "0.010", + # "realizedPnl": "27.38400000", + # "marginAsset": "USDT", + # "quoteQty": "472.63", + # "commission": "0.18905360", + # "commissionAsset": "USDT", + # "time": 1707530039409, + # "buyer": False, + # "maker": False, + # "positionSide": "LONG" + # } + # ] + # + # inverse portfolio margin + # + # [ + # { + # "symbol": "ETHUSD_PERP", + # "id": 701907838, + # "orderId": 71548909034, + # "pair": "ETHUSD", + # "side": "SELL", + # "price": "2498.15", + # "qty": "1", + # "realizedPnl": "0.00012517", + # "marginAsset": "ETH", + # "baseQty": "0.00400296", + # "commission": "0.00000160", + # "commissionAsset": "ETH", + # "time": 1707530317519, + # "positionSide": "LONG", + # "buyer": False, + # "maker": False + # } + # ] + # + # spot margin portfolio margin + # + # [ + # { + # "symbol": "ADAUSDT", + # "id": 470227543, + # "orderId": 4421170947, + # "price": "0.53880000", + # "qty": "10.00000000", + # "quoteQty": "5.38800000", + # "commission": "0.00538800", + # "commissionAsset": "USDT", + # "time": 1707545780522, + # "isBuyer": False, + # "isMaker": False, + # "isBestMatch": True + # } + # ] + # + return self.parse_trades(response, market, since, limit) + + async def fetch_my_dust_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all dust trades made by the user + + https://developers.binance.com/docs/wallet/asset/dust-log + + :param str symbol: not used by binance fetchMyDustTrades() + :param int [since]: the earliest time in ms to fetch my dust trades for + :param int [limit]: the maximum number of dust trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: 'spot' or 'margin', default spot + :returns dict[]: a list of `trade structures ` + """ + # + # Binance provides an opportunity to trade insignificant(i.e. non-tradable and non-withdrawable) + # token leftovers(of any asset) into `BNB` coin which in turn can be used to pay trading fees with it. + # The corresponding trades history is called the `Dust Log` and can be requested via the following end-point: + # https://github.com/binance-exchange/binance-official-api-docs/blob/master/wapi-api.md#dustlog-user_data + # + await self.load_markets() + request: dict = {} + if since is not None: + request['startTime'] = since + request['endTime'] = self.sum(since, 7776000000) + accountType = self.safe_string_upper(params, 'type') + params = self.omit(params, 'type') + if accountType is not None: + request['accountType'] = accountType + response = await self.sapiGetAssetDribblet(self.extend(request, params)) + # { + # "total": "4", + # "userAssetDribblets": [ + # { + # "operateTime": "1627575731000", + # "totalServiceChargeAmount": "0.00001453", + # "totalTransferedAmount": "0.00072693", + # "transId": "70899815863", + # "userAssetDribbletDetails": [ + # { + # "fromAsset": "LTC", + # "amount": "0.000006", + # "transferedAmount": "0.00000267", + # "serviceChargeAmount": "0.00000005", + # "operateTime": "1627575731000", + # "transId": "70899815863" + # }, + # { + # "fromAsset": "GBP", + # "amount": "0.15949157", + # "transferedAmount": "0.00072426", + # "serviceChargeAmount": "0.00001448", + # "operateTime": "1627575731000", + # "transId": "70899815863" + # } + # ] + # }, + # ] + # } + results = self.safe_list(response, 'userAssetDribblets', []) + rows = self.safe_integer(response, 'total', 0) + data = [] + for i in range(0, rows): + logs = self.safe_list(results[i], 'userAssetDribbletDetails', []) + for j in range(0, len(logs)): + logs[j]['isDustTrade'] = True + data.append(logs[j]) + trades = self.parse_trades(data, None, since, limit) + return self.filter_by_since_limit(trades, since, limit) + + def parse_dust_trade(self, trade, market: Market = None): + # + # { + # "fromAsset": "USDT", + # "amount": "0.009669", + # "transferedAmount": "0.00002992", + # "serviceChargeAmount": "0.00000059", + # "operateTime": "1628076010000", + # "transId": "71416578712", + # "isDustTrade": True + # } + # + orderId = self.safe_string(trade, 'transId') + timestamp = self.safe_integer(trade, 'operateTime') + currencyId = self.safe_string(trade, 'fromAsset') + tradedCurrency = self.safe_currency_code(currencyId) + bnb = self.currency('BNB') + earnedCurrency = bnb['code'] + applicantSymbol = earnedCurrency + '/' + tradedCurrency + tradedCurrencyIsQuote = False + if applicantSymbol in self.markets: + tradedCurrencyIsQuote = True + feeCostString = self.safe_string(trade, 'serviceChargeAmount') + fee = { + 'currency': earnedCurrency, + 'cost': self.parse_number(feeCostString), + } + symbol = None + amountString = None + costString = None + side = None + if tradedCurrencyIsQuote: + symbol = applicantSymbol + amountString = self.safe_string(trade, 'transferedAmount') + costString = self.safe_string(trade, 'amount') + side = 'buy' + else: + symbol = tradedCurrency + '/' + earnedCurrency + amountString = self.safe_string(trade, 'amount') + costString = self.safe_string(trade, 'transferedAmount') + side = 'sell' + priceString = None + if costString is not None: + if amountString: + priceString = Precise.string_div(costString, amountString) + id = None + amount = self.parse_number(amountString) + price = self.parse_number(priceString) + cost = self.parse_number(costString) + type = None + takerOrMaker = None + return { + 'id': id, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'order': orderId, + 'type': type, + 'takerOrMaker': takerOrMaker, + 'side': side, + 'amount': amount, + 'price': price, + 'cost': cost, + 'fee': fee, + 'info': trade, + } + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://developers.binance.com/docs/wallet/capital/deposite-history + https://developers.binance.com/docs/fiat/rest-api/Get-Fiat-Deposit-Withdraw-History + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.fiat]: if True, only fiat deposits will be returned + :param int [params.until]: the latest time in ms to fetch entries for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchDeposits', 'paginate') + if paginate: + return await self.fetch_paginated_call_dynamic('fetchDeposits', code, since, limit, params) + currency = None + response = None + request: dict = {} + legalMoney = self.safe_dict(self.options, 'legalMoney', {}) + fiatOnly = self.safe_bool(params, 'fiat', False) + params = self.omit(params, 'fiatOnly') + until = self.safe_integer(params, 'until') + params = self.omit(params, 'until') + if fiatOnly or (code in legalMoney): + if code is not None: + currency = self.currency(code) + request['transactionType'] = 0 + if since is not None: + request['beginTime'] = since + if until is not None: + request['endTime'] = until + raw = await self.sapiGetFiatOrders(self.extend(request, params)) + response = self.safe_list(raw, 'data', []) + # { + # "code": "000000", + # "message": "success", + # "data": [ + # { + # "orderNo": "25ced37075c1470ba8939d0df2316e23", + # "fiatCurrency": "EUR", + # "indicatedAmount": "15.00", + # "amount": "15.00", + # "totalFee": "0.00", + # "method": "card", + # "status": "Failed", + # "createTime": 1627501026000, + # "updateTime": 1627501027000 + # } + # ], + # "total": 1, + # "success": True + # } + else: + if code is not None: + currency = self.currency(code) + request['coin'] = currency['id'] + if since is not None: + request['startTime'] = since + # max 3 months range https://github.com/ccxt/ccxt/issues/6495 + endTime = self.sum(since, 7776000000) + if until is not None: + endTime = min(endTime, until) + request['endTime'] = endTime + if limit is not None: + request['limit'] = limit + response = await self.sapiGetCapitalDepositHisrec(self.extend(request, params)) + # [ + # { + # "amount": "0.01844487", + # "coin": "BCH", + # "network": "BCH", + # "status": 1, + # "address": "1NYxAJhW2281HK1KtJeaENBqHeygA88FzR", + # "addressTag": "", + # "txId": "bafc5902504d6504a00b7d0306a41154cbf1d1b767ab70f3bc226327362588af", + # "insertTime": 1610784980000, + # "transferType": 0, + # "confirmTimes": "2/2" + # }, + # { + # "amount": "4500", + # "coin": "USDT", + # "network": "BSC", + # "status": 1, + # "address": "0xc9c923c87347ca0f3451d6d308ce84f691b9f501", + # "addressTag": "", + # "txId": "Internal transfer 51376627901", + # "insertTime": 1618394381000, + # "transferType": 1, + # "confirmTimes": "1/15" + # } + # ] + for i in range(0, len(response)): + response[i]['type'] = 'deposit' + return self.parse_transactions(response, currency, since, limit) + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://developers.binance.com/docs/wallet/capital/withdraw-history + https://developers.binance.com/docs/fiat/rest-api/Get-Fiat-Deposit-Withdraw-History + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.fiat]: if True, only fiat withdrawals will be returned + :param int [params.until]: the latest time in ms to fetch withdrawals for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchWithdrawals', 'paginate') + if paginate: + return await self.fetch_paginated_call_dynamic('fetchWithdrawals', code, since, limit, params) + legalMoney = self.safe_dict(self.options, 'legalMoney', {}) + fiatOnly = self.safe_bool(params, 'fiat', False) + params = self.omit(params, 'fiatOnly') + request: dict = {} + until = self.safe_integer(params, 'until') + if until is not None: + params = self.omit(params, 'until') + request['endTime'] = until + response = None + currency = None + if fiatOnly or (code in legalMoney): + if code is not None: + currency = self.currency(code) + request['transactionType'] = 1 + if since is not None: + request['beginTime'] = since + raw = await self.sapiGetFiatOrders(self.extend(request, params)) + response = self.safe_list(raw, 'data', []) + # { + # "code": "000000", + # "message": "success", + # "data": [ + # { + # "orderNo": "CJW706452266115170304", + # "fiatCurrency": "GBP", + # "indicatedAmount": "10001.50", + # "amount": "100.00", + # "totalFee": "1.50", + # "method": "bank transfer", + # "status": "Successful", + # "createTime": 1620037745000, + # "updateTime": 1620038480000 + # }, + # { + # "orderNo": "CJW706287492781891584", + # "fiatCurrency": "GBP", + # "indicatedAmount": "10001.50", + # "amount": "100.00", + # "totalFee": "1.50", + # "method": "bank transfer", + # "status": "Successful", + # "createTime": 1619998460000, + # "updateTime": 1619998823000 + # } + # ], + # "total": 39, + # "success": True + # } + else: + if code is not None: + currency = self.currency(code) + request['coin'] = currency['id'] + if since is not None: + request['startTime'] = since + # max 3 months range https://github.com/ccxt/ccxt/issues/6495 + request['endTime'] = self.sum(since, 7776000000) + if limit is not None: + request['limit'] = limit + response = await self.sapiGetCapitalWithdrawHistory(self.extend(request, params)) + # [ + # { + # "id": "69e53ad305124b96b43668ceab158a18", + # "amount": "28.75", + # "transactionFee": "0.25", + # "coin": "XRP", + # "status": 6, + # "address": "r3T75fuLjX51mmfb5Sk1kMNuhBgBPJsjza", + # "addressTag": "101286922", + # "txId": "19A5B24ED0B697E4F0E9CD09FCB007170A605BC93C9280B9E6379C5E6EF0F65A", + # "applyTime": "2021-04-15 12:09:16", + # "network": "XRP", + # "transferType": 0 + # }, + # { + # "id": "9a67628b16ba4988ae20d329333f16bc", + # "amount": "20", + # "transactionFee": "20", + # "coin": "USDT", + # "status": 6, + # "address": "0x0AB991497116f7F5532a4c2f4f7B1784488628e1", + # "txId": "0x77fbf2cf2c85b552f0fd31fd2e56dc95c08adae031d96f3717d8b17e1aea3e46", + # "applyTime": "2021-04-15 12:06:53", + # "network": "ETH", + # "transferType": 0 + # }, + # { + # "id": "a7cdc0afbfa44a48bd225c9ece958fe2", + # "amount": "51", + # "transactionFee": "1", + # "coin": "USDT", + # "status": 6, + # "address": "TYDmtuWL8bsyjvcauUTerpfYyVhFtBjqyo", + # "txId": "168a75112bce6ceb4823c66726ad47620ad332e69fe92d9cb8ceb76023f9a028", + # "applyTime": "2021-04-13 12:46:59", + # "network": "TRX", + # "transferType": 0 + # } + # ] + for i in range(0, len(response)): + response[i]['type'] = 'withdrawal' + return self.parse_transactions(response, currency, since, limit) + + def parse_transaction_status_by_type(self, status, type=None): + if type is None: + return status + statusesByType: dict = { + 'deposit': { + '0': 'pending', + '1': 'ok', + '6': 'ok', + # Fiat + # Processing, Failed, Successful, Finished, Refunding, Refunded, Refund Failed, Order Partial credit Stopped + 'Processing': 'pending', + 'Failed': 'failed', + 'Successful': 'ok', + 'Refunding': 'canceled', + 'Refunded': 'canceled', + 'Refund Failed': 'failed', + }, + 'withdrawal': { + '0': 'pending', # Email Sent + '1': 'canceled', # Cancelled(different from 1 = ok in deposits) + '2': 'pending', # Awaiting Approval + '3': 'failed', # Rejected + '4': 'pending', # Processing + '5': 'failed', # Failure + '6': 'ok', # Completed + # Fiat + # Processing, Failed, Successful, Finished, Refunding, Refunded, Refund Failed, Order Partial credit Stopped + 'Processing': 'pending', + 'Failed': 'failed', + 'Successful': 'ok', + 'Refunding': 'canceled', + 'Refunded': 'canceled', + 'Refund Failed': 'failed', + }, + } + statuses = self.safe_dict(statusesByType, type, {}) + return self.safe_string(statuses, status, status) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fetchDeposits + # + # { + # "amount": "4500", + # "coin": "USDT", + # "network": "BSC", + # "status": 1, + # "address": "0xc9c923c87347ca0f3451d6d308ce84f691b9f501", + # "addressTag": "", + # "txId": "Internal transfer 51376627901", + # "insertTime": 1618394381000, + # "transferType": 1, + # "confirmTimes": "1/15" + # } + # + # fetchWithdrawals + # + # { + # "id": "69e53ad305124b96b43668ceab158a18", + # "amount": "28.75", + # "transactionFee": "0.25", + # "coin": "XRP", + # "status": 6, + # "address": "r3T75fuLjX51mmfb5Sk1kMNuhBgBPJsjza", + # "addressTag": "101286922", + # "txId": "19A5B24ED0B697E4F0E9CD09FCB007170A605BC93C9280B9E6379C5E6EF0F65A", + # "applyTime": "2021-04-15 12:09:16", + # "network": "XRP", + # "transferType": 0 + # } + # + # fiat transaction + # withdraw + # { + # "orderNo": "CJW684897551397171200", + # "fiatCurrency": "GBP", + # "indicatedAmount": "29.99", + # "amount": "28.49", + # "totalFee": "1.50", + # "method": "bank transfer", + # "status": "Successful", + # "createTime": 1614898701000, + # "updateTime": 1614898820000 + # } + # + # deposit + # { + # "orderNo": "25ced37075c1470ba8939d0df2316e23", + # "fiatCurrency": "EUR", + # "transactionType": 0, + # "indicatedAmount": "15.00", + # "amount": "15.00", + # "totalFee": "0.00", + # "method": "card", + # "status": "Failed", + # "createTime": "1627501026000", + # "updateTime": "1627501027000" + # } + # + # withdraw + # + # {id: "9a67628b16ba4988ae20d329333f16bc"} + # + id = self.safe_string_2(transaction, 'id', 'orderNo') + address = self.safe_string(transaction, 'address') + tag = self.safe_string(transaction, 'addressTag') # set but unused + if tag is not None: + if len(tag) < 1: + tag = None + txid = self.safe_string(transaction, 'txId') + if (txid is not None) and (txid.find('Internal transfer ') >= 0): + txid = txid[18:] + currencyId = self.safe_string_2(transaction, 'coin', 'fiatCurrency') + code = self.safe_currency_code(currencyId, currency) + timestamp = None + timestamp = self.safe_integer_2(transaction, 'insertTime', 'createTime') + if timestamp is None: + timestamp = self.parse8601(self.safe_string(transaction, 'applyTime')) + updated = self.safe_integer_2(transaction, 'successTime', 'updateTime') + type = self.safe_string(transaction, 'type') + if type is None: + txType = self.safe_string(transaction, 'transactionType') + if txType is not None: + type = 'deposit' if (txType == '0') else 'withdrawal' + legalMoneyCurrenciesById = self.safe_dict(self.options, 'legalMoneyCurrenciesById') + code = self.safe_string(legalMoneyCurrenciesById, code, code) + status = self.parse_transaction_status_by_type(self.safe_string(transaction, 'status'), type) + amount = self.safe_number(transaction, 'amount') + feeCost = self.safe_number_2(transaction, 'transactionFee', 'totalFee') + fee = None + if feeCost is not None: + fee = {'currency': code, 'cost': feeCost} + internalInteger = self.safe_integer(transaction, 'transferType') + internal = None + if internalInteger is not None: + internal = True if (internalInteger != 0) else False + network = self.safe_string(transaction, 'network') + return { + 'info': transaction, + 'id': id, + 'txid': txid, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': network, + 'address': address, + 'addressTo': address, + 'addressFrom': None, + 'tag': tag, + 'tagTo': tag, + 'tagFrom': None, + 'type': type, + 'amount': amount, + 'currency': code, + 'status': status, + 'updated': updated, + 'internal': internal, + 'comment': None, + 'fee': fee, + } + + def parse_transfer_status(self, status: Str) -> Str: + statuses: dict = { + 'CONFIRMED': 'ok', + } + return self.safe_string(statuses, status, status) + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + # + # transfer + # + # { + # "tranId":13526853623 + # } + # + # fetchTransfers + # + # { + # "timestamp": 1614640878000, + # "asset": "USDT", + # "amount": "25", + # "type": "MAIN_UMFUTURE", + # "status": "CONFIRMED", + # "tranId": 43000126248 + # } + # + # { + # "orderType": "C2C", # Enum:PAY(C2B Merchant Acquiring Payment), PAY_REFUND(C2B Merchant Acquiring Payment,refund), C2C(C2C Transfer Payment),CRYPTO_BOX(Crypto box), CRYPTO_BOX_RF(Crypto Box, refund), C2C_HOLDING(Transfer to new Binance user), C2C_HOLDING_RF(Transfer to new Binance user,refund), PAYOUT(B2C Disbursement Payment), REMITTANCE(Send cash) + # "transactionId": "M_P_71505104267788288", + # "transactionTime": 1610090460133, #trade timestamp + # "amount": "23.72469206", #order amount(up to 8 decimal places), positive is income, negative is expenditure + # "currency": "BNB", + # "walletType": 1, #main wallet type, 1 for funding wallet, 2 for spot wallet, 3 for fiat wallet, 4 or 6 for card payment, 5 for earn wallet + # "walletTypes": [1,2], #array format,there are multiple values when using combination payment + # "fundsDetail": [ # details + # { + # "currency": "USDT", #asset + # "amount": "1.2", + # "walletAssetCost":[ #details of asset cost per wallet + # {"1":"0.6"}, + # {"2":"0.6"} + # ] + # }, + # { + # "currency": "ETH", + # "amount": "0.0001", + # "walletAssetCost":[ + # {"1":"0.00005"}, + # {"2":"0.00005"} + # ] + # } + # ], + # "payerInfo":{ + # "name":"Jack", #nickname or merchant name + # "type":"USER", #account type,USER for personal,MERCHANT for merchant + # "binanceId":"12345678", #binance uid + # "accountId":"67736251" #binance pay id + # }, + # "receiverInfo":{ + # "name":"Alan", #nickname or merchant name + # "type":"MERCHANT", #account type,USER for personal,MERCHANT for merchant + # "email":"alan@binance.com", #email + # "binanceId":"34355667", #binance uid + # "accountId":"21326891", #binance pay id + # "countryCode":"1", #International area code + # "phoneNumber":"8057651210", + # "mobileCode":"US", #country code + # "extend":[ #extension field + # "institutionName": "", + # "cardNumber": "", + # "digitalWalletId": "" + # ] + # } + # } + id = self.safe_string_2(transfer, 'tranId', 'transactionId') + currencyId = self.safe_string_2(transfer, 'asset', 'currency') + code = self.safe_currency_code(currencyId, currency) + amount = self.safe_number(transfer, 'amount') + type = self.safe_string(transfer, 'type') + fromAccount = None + toAccount = None + accountsById = self.safe_dict(self.options, 'accountsById', {}) + if type is not None: + parts = type.split('_') + fromAccount = self.safe_value(parts, 0) + toAccount = self.safe_value(parts, 1) + fromAccount = self.safe_string(accountsById, fromAccount, fromAccount) + toAccount = self.safe_string(accountsById, toAccount, toAccount) + walletType = self.safe_integer(transfer, 'walletType') + if walletType is not None: + payer = self.safe_dict(transfer, 'payerInfo', {}) + receiver = self.safe_dict(transfer, 'receiverInfo', {}) + fromAccount = self.safe_string(payer, 'accountId') + toAccount = self.safe_string(receiver, 'accountId') + timestamp = self.safe_integer_2(transfer, 'timestamp', 'transactionTime') + status = self.parse_transfer_status(self.safe_string(transfer, 'status')) + return { + 'info': transfer, + 'id': id, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'currency': code, + 'amount': amount, + 'fromAccount': fromAccount, + 'toAccount': toAccount, + 'status': status, + } + + def parse_income(self, income, market: Market = None): + # + # { + # "symbol": "ETHUSDT", + # "incomeType": "FUNDING_FEE", + # "income": "0.00134317", + # "asset": "USDT", + # "time": "1621584000000", + # "info": "FUNDING_FEE", + # "tranId": "4480321991774044580", + # "tradeId": "" + # } + # + marketId = self.safe_string(income, 'symbol') + currencyId = self.safe_string(income, 'asset') + timestamp = self.safe_integer(income, 'time') + return { + 'info': income, + 'symbol': self.safe_symbol(marketId, market, None, 'swap'), + 'code': self.safe_currency_code(currencyId), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': self.safe_string(income, 'tranId'), + 'amount': self.safe_number(income, 'income'), + } + + async def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://developers.binance.com/docs/wallet/asset/user-universal-transfer + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account to transfer from + :param str toAccount: account to transfer to + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: exchange specific transfer type + :param str [params.symbol]: the unified symbol, required for isolated margin transfers + :returns dict: a `transfer structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'asset': currency['id'], + 'amount': self.currency_to_precision(code, amount), + } + request['type'] = self.safe_string(params, 'type') + params = self.omit(params, 'type') + if request['type'] is None: + symbol = self.safe_string(params, 'symbol') + market = None + if symbol is not None: + market = self.market(symbol) + params = self.omit(params, 'symbol') + fromId = self.convert_type_to_account(fromAccount).upper() + toId = self.convert_type_to_account(toAccount).upper() + isolatedSymbol = None + if market is not None: + isolatedSymbol = market['id'] + if fromId == 'ISOLATED': + if symbol is None: + raise ArgumentsRequired(self.id + ' transfer() requires params["symbol"] when fromAccount is ' + fromAccount) + if toId == 'ISOLATED': + if symbol is None: + raise ArgumentsRequired(self.id + ' transfer() requires params["symbol"] when toAccount is ' + toAccount) + accountsById = self.safe_dict(self.options, 'accountsById', {}) + fromIsolated = not (fromId in accountsById) + toIsolated = not (toId in accountsById) + if fromIsolated and (market is None): + isolatedSymbol = fromId # allow user provide symbol from/to account + if toIsolated and (market is None): + isolatedSymbol = toId + if fromIsolated or toIsolated: # Isolated margin transfer + fromFuture = fromId == 'UMFUTURE' or fromId == 'CMFUTURE' + toFuture = toId == 'UMFUTURE' or toId == 'CMFUTURE' + fromSpot = fromId == 'MAIN' + toSpot = toId == 'MAIN' + funding = fromId == 'FUNDING' or toId == 'FUNDING' + option = fromId == 'OPTION' or toId == 'OPTION' + prohibitedWithIsolated = fromFuture or toFuture or funding or option + if (fromIsolated or toIsolated) and prohibitedWithIsolated: + raise BadRequest(self.id + ' transfer() does not allow transfers between ' + fromAccount + ' and ' + toAccount) + elif toSpot and fromIsolated: + fromId = 'ISOLATED_MARGIN' + request['fromSymbol'] = isolatedSymbol + elif fromSpot and toIsolated: + toId = 'ISOLATED_MARGIN' + request['toSymbol'] = isolatedSymbol + else: + if fromIsolated and toIsolated: + request['fromSymbol'] = fromId + request['toSymbol'] = toId + fromId = 'ISOLATEDMARGIN' + toId = 'ISOLATEDMARGIN' + else: + if fromIsolated: + request['fromSymbol'] = isolatedSymbol + fromId = 'ISOLATEDMARGIN' + if toIsolated: + request['toSymbol'] = isolatedSymbol + toId = 'ISOLATEDMARGIN' + request['type'] = fromId + '_' + toId + else: + request['type'] = fromId + '_' + toId + response = await self.sapiPostAssetTransfer(self.extend(request, params)) + # + # { + # "tranId":13526853623 + # } + # + return self.parse_transfer(response, currency) + + async def fetch_transfers(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[TransferEntry]: + """ + fetch a history of internal transfers made on an account + + https://developers.binance.com/docs/wallet/asset/query-user-universal-transfer + + :param str code: unified currency code of the currency transferred + :param int [since]: the earliest time in ms to fetch transfers for + :param int [limit]: the maximum number of transfers structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch transfers for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param boolean [params.internal]: default False, when True will fetch pay trade history + :returns dict[]: a list of `transfer structures ` + """ + await self.load_markets() + internal = self.safe_bool(params, 'internal') + params = self.omit(params, 'internal') + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchTransfers', 'paginate') + if paginate and not internal: + return await self.fetch_paginated_call_dynamic('fetchTransfers', code, since, limit, params) + currency = None + if code is not None: + currency = self.currency(code) + request: dict = {} + limitKey = 'limit' + if not internal: + defaultType = self.safe_string_2(self.options, 'fetchTransfers', 'defaultType', 'spot') + fromAccount = self.safe_string(params, 'fromAccount', defaultType) + defaultTo = 'spot' if (fromAccount == 'future') else 'future' + toAccount = self.safe_string(params, 'toAccount', defaultTo) + type = self.safe_string(params, 'type') + accountsByType = self.safe_dict(self.options, 'accountsByType', {}) + fromId = self.safe_string(accountsByType, fromAccount) + toId = self.safe_string(accountsByType, toAccount) + if type is None: + if fromId is None: + keys = list(accountsByType.keys()) + raise ExchangeError(self.id + ' fromAccount parameter must be one of ' + ', '.join(keys)) + if toId is None: + keys = list(accountsByType.keys()) + raise ExchangeError(self.id + ' toAccount parameter must be one of ' + ', '.join(keys)) + type = fromId + '_' + toId + request['type'] = type + limitKey = 'size' + if limit is not None: + request[limitKey] = limit + if since is not None: + request['startTime'] = since + until = self.safe_integer(params, 'until') + if until is not None: + params = self.omit(params, 'until') + request['endTime'] = until + response = None + if internal: + response = await self.sapiGetPayTransactions(self.extend(request, params)) + # + # { + # "code": "000000", + # "message": "success", + # "data": [ + # { + # "orderType": "C2C", # Enum:PAY(C2B Merchant Acquiring Payment), PAY_REFUND(C2B Merchant Acquiring Payment,refund), C2C(C2C Transfer Payment),CRYPTO_BOX(Crypto box), CRYPTO_BOX_RF(Crypto Box, refund), C2C_HOLDING(Transfer to new Binance user), C2C_HOLDING_RF(Transfer to new Binance user,refund), PAYOUT(B2C Disbursement Payment), REMITTANCE(Send cash) + # "transactionId": "M_P_71505104267788288", + # "transactionTime": 1610090460133, #trade timestamp + # "amount": "23.72469206", #order amount(up to 8 decimal places), positive is income, negative is expenditure + # "currency": "BNB", + # "walletType": 1, #main wallet type, 1 for funding wallet, 2 for spot wallet, 3 for fiat wallet, 4 or 6 for card payment, 5 for earn wallet + # "walletTypes": [1,2], #array format,there are multiple values when using combination payment + # "fundsDetail": [ # details + # { + # "currency": "USDT", #asset + # "amount": "1.2", + # "walletAssetCost":[ #details of asset cost per wallet + # {"1":"0.6"}, + # {"2":"0.6"} + # ] + # }, + # { + # "currency": "ETH", + # "amount": "0.0001", + # "walletAssetCost":[ + # {"1":"0.00005"}, + # {"2":"0.00005"} + # ] + # } + # ], + # "payerInfo":{ + # "name":"Jack", #nickname or merchant name + # "type":"USER", #account type,USER for personal,MERCHANT for merchant + # "binanceId":"12345678", #binance uid + # "accountId":"67736251" #binance pay id + # }, + # "receiverInfo":{ + # "name":"Alan", #nickname or merchant name + # "type":"MERCHANT", #account type,USER for personal,MERCHANT for merchant + # "email":"alan@binance.com", #email + # "binanceId":"34355667", #binance uid + # "accountId":"21326891", #binance pay id + # "countryCode":"1", #International area code + # "phoneNumber":"8057651210", + # "mobileCode":"US", #country code + # "extend":[ #extension field + # "institutionName": "", + # "cardNumber": "", + # "digitalWalletId": "" + # ] + # } + # } + # ], + # "success": True + # } + # + else: + response = await self.sapiGetAssetTransfer(self.extend(request, params)) + # + # { + # "total": 3, + # "rows": [ + # { + # "timestamp": 1614640878000, + # "asset": "USDT", + # "amount": "25", + # "type": "MAIN_UMFUTURE", + # "status": "CONFIRMED", + # "tranId": 43000126248 + # }, + # ] + # } + # + rows = self.safe_list_2(response, 'rows', 'data', []) + return self.parse_transfers(rows, currency, since, limit) + + async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://developers.binance.com/docs/wallet/capital/deposite-address + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.network]: network for fetch deposit address + :returns dict: an `address structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'coin': currency['id'], + # 'network': 'ETH', # 'BSC', 'XMR', you can get network and isDefault in networkList in the response of sapiGetCapitalConfigDetail + } + networks = self.safe_dict(self.options, 'networks', {}) + network = self.safe_string_upper(params, 'network') # self line allows the user to specify either ERC20 or ETH + network = self.safe_string(networks, network, network) # handle ERC20>ETH alias + if network is not None: + request['network'] = network + params = self.omit(params, 'network') + # has support for the 'network' parameter + response = await self.sapiGetCapitalDepositAddress(self.extend(request, params)) + # + # { + # "currency": "XRP", + # "address": "rEb8TK3gBgk5auZkwc6sHnwrGVJH8DuaLh", + # "tag": "108618262", + # "info": { + # "coin": "XRP", + # "address": "rEb8TK3gBgk5auZkwc6sHnwrGVJH8DuaLh", + # "tag": "108618262", + # "url": "https://bithomp.com/explorer/rEb8TK3gBgk5auZkwc6sHnwrGVJH8DuaLh" + # } + # } + # + return self.parse_deposit_address(response, currency) + + def parse_deposit_address(self, response, currency: Currency = None) -> DepositAddress: + # + # { + # "coin": "XRP", + # "address": "rEb8TK3gBgk5auZkwc6sHnwrGVJH8DuaLh", + # "tag": "108618262", + # "url": "https://bithomp.com/explorer/rEb8TK3gBgk5auZkwc6sHnwrGVJH8DuaLh" + # } + # + url = self.safe_string(response, 'url') + address = self.safe_string(response, 'address') + currencyId = self.safe_string(response, 'currency') + code = self.safe_currency_code(currencyId, currency) + # deposit-address endpoint provides only network url(not network ID/CODE) + # so we should map the url to network(their data is inside currencies) + networkCode = self.get_network_code_by_network_url(code, url) + tag = self.safe_string(response, 'tag', '') + if len(tag) == 0: + tag = None + self.check_address(address) + return { + 'info': response, + 'currency': code, + 'network': networkCode, + 'address': address, + 'tag': tag, + } + + async def fetch_transaction_fees(self, codes: Strings = None, params={}): + """ + @deprecated + please use fetchDepositWithdrawFees instead + + https://developers.binance.com/docs/wallet/capital/all-coins-info + + :param str[]|None codes: not used by binance fetchTransactionFees() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `fee structures ` + """ + await self.load_markets() + response = await self.sapiGetCapitalConfigGetall(params) + # + # [ + # { + # "coin": "BAT", + # "depositAllEnable": True, + # "withdrawAllEnable": True, + # "name": "Basic Attention Token", + # "free": "0", + # "locked": "0", + # "freeze": "0", + # "withdrawing": "0", + # "ipoing": "0", + # "ipoable": "0", + # "storage": "0", + # "isLegalMoney": False, + # "trading": True, + # "networkList": [ + # { + # "network": "BNB", + # "coin": "BAT", + # "withdrawIntegerMultiple": "0.00000001", + # "isDefault": False, + # "depositEnable": True, + # "withdrawEnable": True, + # "depositDesc": '', + # "withdrawDesc": '', + # "specialTips": "The name of self asset is Basic Attention Token(BAT). Both a MEMO and an Address are required to successfully deposit your BEP2 tokens to Binance.", + # "name": "BEP2", + # "resetAddressStatus": False, + # "addressRegex": "^(bnb1)[0-9a-z]{38}$", + # "memoRegex": "^[0-9A-Za-z\\-_]{1,120}$", + # "withdrawFee": "0.27", + # "withdrawMin": "0.54", + # "withdrawMax": "10000000000", + # "minConfirm": "1", + # "unLockConfirm": "0" + # }, + # { + # "network": "BSC", + # "coin": "BAT", + # "withdrawIntegerMultiple": "0.00000001", + # "isDefault": False, + # "depositEnable": True, + # "withdrawEnable": True, + # "depositDesc": '', + # "withdrawDesc": '', + # "specialTips": "The name of self asset is Basic Attention Token. Please ensure you are depositing Basic Attention Token(BAT) tokens under the contract address ending in 9766e.", + # "name": "BEP20(BSC)", + # "resetAddressStatus": False, + # "addressRegex": "^(0x)[0-9A-Fa-f]{40}$", + # "memoRegex": '', + # "withdrawFee": "0.27", + # "withdrawMin": "0.54", + # "withdrawMax": "10000000000", + # "minConfirm": "15", + # "unLockConfirm": "0" + # }, + # { + # "network": "ETH", + # "coin": "BAT", + # "withdrawIntegerMultiple": "0.00000001", + # "isDefault": True, + # "depositEnable": True, + # "withdrawEnable": True, + # "depositDesc": '', + # "withdrawDesc": '', + # "specialTips": "The name of self asset is Basic Attention Token. Please ensure you are depositing Basic Attention Token(BAT) tokens under the contract address ending in 887ef.", + # "name": "ERC20", + # "resetAddressStatus": False, + # "addressRegex": "^(0x)[0-9A-Fa-f]{40}$", + # "memoRegex": '', + # "withdrawFee": "27", + # "withdrawMin": "54", + # "withdrawMax": "10000000000", + # "minConfirm": "12", + # "unLockConfirm": "0" + # } + # ] + # } + # ] + # + withdrawFees: dict = {} + for i in range(0, len(response)): + entry = response[i] + currencyId = self.safe_string(entry, 'coin') + code = self.safe_currency_code(currencyId) + networkList = self.safe_list(entry, 'networkList', []) + withdrawFees[code] = {} + for j in range(0, len(networkList)): + networkEntry = networkList[j] + networkId = self.safe_string(networkEntry, 'network') + networkCode = self.safe_currency_code(networkId) + fee = self.safe_number(networkEntry, 'withdrawFee') + withdrawFees[code][networkCode] = fee + return { + 'withdraw': withdrawFees, + 'deposit': {}, + 'info': response, + } + + async def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}): + """ + fetch deposit and withdraw fees + + https://developers.binance.com/docs/wallet/capital/all-coins-info + + :param str[]|None codes: not used by binance fetchDepositWithdrawFees() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `fee structures ` + """ + await self.load_markets() + response = await self.sapiGetCapitalConfigGetall(params) + # + # [ + # { + # "coin": "BAT", + # "depositAllEnable": True, + # "withdrawAllEnable": True, + # "name": "Basic Attention Token", + # "free": "0", + # "locked": "0", + # "freeze": "0", + # "withdrawing": "0", + # "ipoing": "0", + # "ipoable": "0", + # "storage": "0", + # "isLegalMoney": False, + # "trading": True, + # "networkList": [ + # { + # "network": "BNB", + # "coin": "BAT", + # "withdrawIntegerMultiple": "0.00000001", + # "isDefault": False, + # "depositEnable": True, + # "withdrawEnable": True, + # "depositDesc": '', + # "withdrawDesc": '', + # "specialTips": "The name of self asset is Basic Attention Token(BAT). Both a MEMO and an Address are required to successfully deposit your BEP2 tokens to Binance.", + # "name": "BEP2", + # "resetAddressStatus": False, + # "addressRegex": "^(bnb1)[0-9a-z]{38}$", + # "memoRegex": "^[0-9A-Za-z\\-_]{1,120}$", + # "withdrawFee": "0.27", + # "withdrawMin": "0.54", + # "withdrawMax": "10000000000", + # "minConfirm": "1", + # "unLockConfirm": "0" + # }, + # ... + # ] + # } + # ] + # + return self.parse_deposit_withdraw_fees(response, codes, 'coin') + + def parse_deposit_withdraw_fee(self, fee, currency: Currency = None): + # + # { + # "coin": "BAT", + # "depositAllEnable": True, + # "withdrawAllEnable": True, + # "name": "Basic Attention Token", + # "free": "0", + # "locked": "0", + # "freeze": "0", + # "withdrawing": "0", + # "ipoing": "0", + # "ipoable": "0", + # "storage": "0", + # "isLegalMoney": False, + # "trading": True, + # "networkList": [ + # { + # "network": "BNB", + # "coin": "BAT", + # "withdrawIntegerMultiple": "0.00000001", + # "isDefault": False, + # "depositEnable": True, + # "withdrawEnable": True, + # "depositDesc": '', + # "withdrawDesc": '', + # "specialTips": "The name of self asset is Basic Attention Token(BAT). Both a MEMO and an Address are required to successfully deposit your BEP2 tokens to Binance.", + # "name": "BEP2", + # "resetAddressStatus": False, + # "addressRegex": "^(bnb1)[0-9a-z]{38}$", + # "memoRegex": "^[0-9A-Za-z\\-_]{1,120}$", + # "withdrawFee": "0.27", + # "withdrawMin": "0.54", + # "withdrawMax": "10000000000", + # "minConfirm": "1", + # "unLockConfirm": "0" + # }, + # ... + # ] + # } + # + networkList = self.safe_list(fee, 'networkList', []) + result = self.deposit_withdraw_fee(fee) + for j in range(0, len(networkList)): + networkEntry = networkList[j] + networkId = self.safe_string(networkEntry, 'network') + networkCode = self.network_id_to_code(networkId) + withdrawFee = self.safe_number(networkEntry, 'withdrawFee') + isDefault = self.safe_bool(networkEntry, 'isDefault') + if isDefault is True: + result['withdraw'] = { + 'fee': withdrawFee, + 'percentage': None, + } + result['networks'][networkCode] = { + 'withdraw': { + 'fee': withdrawFee, + 'percentage': None, + }, + 'deposit': { + 'fee': None, + 'percentage': None, + }, + } + return result + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://developers.binance.com/docs/wallet/capital/withdraw + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.check_address(address) + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'coin': currency['id'], + 'address': address, + # issue sapiGetCapitalConfigGetall() to get networks for withdrawing USDT ERC20 vs USDT Omni + # 'network': 'ETH', # 'BTC', 'TRX', etc, optional + } + if tag is not None: + request['addressTag'] = tag + networks = self.safe_dict(self.options, 'networks', {}) + network = self.safe_string_upper(params, 'network') # self line allows the user to specify either ERC20 or ETH + network = self.safe_string(networks, network, network) # handle ERC20>ETH alias + if network is not None: + request['network'] = network + params = self.omit(params, 'network') + request['amount'] = self.currency_to_precision(code, amount, network) + response = await self.sapiPostCapitalWithdrawApply(self.extend(request, params)) + # {id: '9a67628b16ba4988ae20d329333f16bc'} + return self.parse_transaction(response, currency) + + def parse_trading_fee(self, fee: dict, market: Market = None) -> TradingFeeInterface: + # + # spot + # [ + # { + # "symbol": "BTCUSDT", + # "makerCommission": "0.001", + # "takerCommission": "0.001" + # } + # ] + # + # swap + # { + # "symbol": "BTCUSD_PERP", + # "makerCommissionRate": "0.00015", # 0.015% + # "takerCommissionRate": "0.00040" # 0.040% + # } + # + marketId = self.safe_string(fee, 'symbol') + symbol = self.safe_symbol(marketId, market, None, 'spot') + return { + 'info': fee, + 'symbol': symbol, + 'maker': self.safe_number_2(fee, 'makerCommission', 'makerCommissionRate'), + 'taker': self.safe_number_2(fee, 'takerCommission', 'takerCommissionRate'), + 'percentage': None, + 'tierBased': None, + } + + async def fetch_trading_fee(self, symbol: str, params={}) -> TradingFeeInterface: + """ + fetch the trading fees for a market + + https://developers.binance.com/docs/wallet/asset/trade-fee + https://developers.binance.com/docs/derivatives/usds-margined-futures/account/rest-api/User-Commission-Rate + https://developers.binance.com/docs/derivatives/coin-margined-futures/account/rest-api/User-Commission-Rate + https://developers.binance.com/docs/derivatives/portfolio-margin/account/Get-User-Commission-Rate-for-UM + https://developers.binance.com/docs/derivatives/portfolio-margin/account/Get-User-Commission-Rate-for-CM + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.portfolioMargin]: set to True if you would like to fetch trading fees in a portfolio margin account + :param str [params.subType]: "linear" or "inverse" + :returns dict: a `fee structure ` + """ + await self.load_markets() + market = self.market(symbol) + type = market['type'] + subType = None + subType, params = self.handle_sub_type_and_params('fetchTradingFee', market, params) + isPortfolioMargin = None + isPortfolioMargin, params = self.handle_option_and_params_2(params, 'fetchTradingFee', 'papi', 'portfolioMargin', False) + isLinear = self.is_linear(type, subType) + isInverse = self.is_inverse(type, subType) + request: dict = { + 'symbol': market['id'], + } + response = None + if isLinear: + if isPortfolioMargin: + response = await self.papiGetUmCommissionRate(self.extend(request, params)) + else: + response = await self.fapiPrivateGetCommissionRate(self.extend(request, params)) + elif isInverse: + if isPortfolioMargin: + response = await self.papiGetCmCommissionRate(self.extend(request, params)) + else: + response = await self.dapiPrivateGetCommissionRate(self.extend(request, params)) + else: + response = await self.sapiGetAssetTradeFee(self.extend(request, params)) + # + # spot + # + # [ + # { + # "symbol": "BTCUSDT", + # "makerCommission": "0.001", + # "takerCommission": "0.001" + # } + # ] + # + # swap + # + # { + # "symbol": "BTCUSD_PERP", + # "makerCommissionRate": "0.00015", # 0.015% + # "takerCommissionRate": "0.00040" # 0.040% + # } + # + data = response + if isinstance(data, list): + data = self.safe_dict(data, 0, {}) + return self.parse_trading_fee(data, market) + + async def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://developers.binance.com/docs/wallet/asset/trade-fee + https://developers.binance.com/docs/derivatives/usds-margined-futures/account/rest-api/Account-Information-V2 + https://developers.binance.com/docs/derivatives/coin-margined-futures/account/rest-api/Account-Information + https://developers.binance.com/docs/derivatives/usds-margined-futures/account/rest-api/Account-Config + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.subType]: "linear" or "inverse" + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + await self.load_markets() + type = None + type, params = self.handle_market_type_and_params('fetchTradingFees', None, params) + subType = None + subType, params = self.handle_sub_type_and_params('fetchTradingFees', None, params, 'linear') + isSpotOrMargin = (type == 'spot') or (type == 'margin') + isLinear = self.is_linear(type, subType) + isInverse = self.is_inverse(type, subType) + response = None + if isSpotOrMargin: + response = await self.sapiGetAssetTradeFee(params) + elif isLinear: + response = await self.fapiPrivateGetAccountConfig(params) + elif isInverse: + response = await self.dapiPrivateGetAccount(params) + # + # sapi / spot + # + # [ + # { + # "symbol": "ZRXBNB", + # "makerCommission": "0.001", + # "takerCommission": "0.001" + # }, + # { + # "symbol": "ZRXBTC", + # "makerCommission": "0.001", + # "takerCommission": "0.001" + # }, + # ] + # + # fapi / future / linear + # + # { + # "feeTier": 0, # account commisssion tier + # "canTrade": True, # if can trade + # "canDeposit": True, # if can transfer in asset + # "canWithdraw": True, # if can transfer out asset + # "updateTime": 0, + # "totalInitialMargin": "0.00000000", # total initial margin required with current mark price(useless with isolated positions), only for USDT asset + # "totalMaintMargin": "0.00000000", # total maintenance margin required, only for USDT asset + # "totalWalletBalance": "23.72469206", # total wallet balance, only for USDT asset + # "totalUnrealizedProfit": "0.00000000", # total unrealized profit, only for USDT asset + # "totalMarginBalance": "23.72469206", # total margin balance, only for USDT asset + # "totalPositionInitialMargin": "0.00000000", # initial margin required for positions with current mark price, only for USDT asset + # "totalOpenOrderInitialMargin": "0.00000000", # initial margin required for open orders with current mark price, only for USDT asset + # "totalCrossWalletBalance": "23.72469206", # crossed wallet balance, only for USDT asset + # "totalCrossUnPnl": "0.00000000", # unrealized profit of crossed positions, only for USDT asset + # "availableBalance": "23.72469206", # available balance, only for USDT asset + # "maxWithdrawAmount": "23.72469206" # maximum amount for transfer out, only for USDT asset + # ... + # } + # + # dapi / delivery / inverse + # + # { + # "canDeposit": True, + # "canTrade": True, + # "canWithdraw": True, + # "feeTier": 2, + # "updateTime": 0 + # } + # + if isSpotOrMargin: + # + # [ + # { + # "symbol": "ZRXBNB", + # "makerCommission": "0.001", + # "takerCommission": "0.001" + # }, + # { + # "symbol": "ZRXBTC", + # "makerCommission": "0.001", + # "takerCommission": "0.001" + # }, + # ] + # + result: dict = {} + for i in range(0, len(response)): + fee = self.parse_trading_fee(response[i]) + symbol = fee['symbol'] + result[symbol] = fee + return result + elif isLinear: + # + # { + # "feeTier": 0, # account commisssion tier + # "canTrade": True, # if can trade + # "canDeposit": True, # if can transfer in asset + # "canWithdraw": True, # if can transfer out asset + # "updateTime": 0, + # "totalInitialMargin": "0.00000000", # total initial margin required with current mark price(useless with isolated positions), only for USDT asset + # "totalMaintMargin": "0.00000000", # total maintenance margin required, only for USDT asset + # "totalWalletBalance": "23.72469206", # total wallet balance, only for USDT asset + # "totalUnrealizedProfit": "0.00000000", # total unrealized profit, only for USDT asset + # "totalMarginBalance": "23.72469206", # total margin balance, only for USDT asset + # "totalPositionInitialMargin": "0.00000000", # initial margin required for positions with current mark price, only for USDT asset + # "totalOpenOrderInitialMargin": "0.00000000", # initial margin required for open orders with current mark price, only for USDT asset + # "totalCrossWalletBalance": "23.72469206", # crossed wallet balance, only for USDT asset + # "totalCrossUnPnl": "0.00000000", # unrealized profit of crossed positions, only for USDT asset + # "availableBalance": "23.72469206", # available balance, only for USDT asset + # "maxWithdrawAmount": "23.72469206" # maximum amount for transfer out, only for USDT asset + # ... + # } + # + symbols = list(self.markets.keys()) + result: dict = {} + feeTier = self.safe_integer(response, 'feeTier') + feeTiers = self.fees['linear']['trading']['tiers'] + maker = feeTiers['maker'][feeTier][1] + taker = feeTiers['taker'][feeTier][1] + for i in range(0, len(symbols)): + symbol = symbols[i] + market = self.markets[symbol] + if market['linear']: + result[symbol] = { + 'info': { + 'feeTier': feeTier, + }, + 'symbol': symbol, + 'maker': maker, + 'taker': taker, + } + return result + elif isInverse: + # + # { + # "canDeposit": True, + # "canTrade": True, + # "canWithdraw": True, + # "feeTier": 2, + # "updateTime": 0 + # } + # + symbols = list(self.markets.keys()) + result: dict = {} + feeTier = self.safe_integer(response, 'feeTier') + feeTiers = self.fees['inverse']['trading']['tiers'] + maker = feeTiers['maker'][feeTier][1] + taker = feeTiers['taker'][feeTier][1] + for i in range(0, len(symbols)): + symbol = symbols[i] + market = self.markets[symbol] + if market['inverse']: + result[symbol] = { + 'info': { + 'feeTier': feeTier, + }, + 'symbol': symbol, + 'maker': maker, + 'taker': taker, + } + return result + return None + + async def futures_transfer(self, code: str, amount, type, params={}): + """ + @ignore + transfer between futures account + + https://developers.binance.com/docs/derivatives/usds-margined-futures/account/rest-api/New-Future-Account-Transfer + + :param str code: unified currency code + :param float amount: the amount to transfer + :param str type: 1 - transfer from spot account to USDT-Ⓜ futures account, 2 - transfer from USDT-Ⓜ futures account to spot account, 3 - transfer from spot account to COIN-Ⓜ futures account, 4 - transfer from COIN-Ⓜ futures account to spot account + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float params.recvWindow: + :returns dict: a `transfer structure ` + """ + if (type < 1) or (type > 4): + raise ArgumentsRequired(self.id + ' type must be between 1 and 4') + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'asset': currency['id'], + 'amount': amount, + 'type': type, + } + response = await self.sapiPostFuturesTransfer(self.extend(request, params)) + # + # { + # "tranId": 100000001 + # } + # + return self.parse_transfer(response, currency) + + async def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate + + https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/Mark-Price + https://developers.binance.com/docs/derivatives/coin-margined-futures/market-data/rest-api/Index-Price-and-Mark-Price + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = None + if market['linear']: + response = await self.fapiPublicGetPremiumIndex(self.extend(request, params)) + elif market['inverse']: + response = await self.dapiPublicGetPremiumIndex(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchFundingRate() supports linear and inverse contracts only') + if market['inverse']: + response = response[0] + # + # { + # "symbol": "BTCUSDT", + # "markPrice": "45802.81129892", + # "indexPrice": "45745.47701915", + # "estimatedSettlePrice": "45133.91753671", + # "lastFundingRate": "0.00063521", + # "interestRate": "0.00010000", + # "nextFundingTime": "1621267200000", + # "time": "1621252344001" + # } + # + return self.parse_funding_rate(response, market) + + async def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical funding rate prices + + https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/Get-Funding-Rate-History + https://developers.binance.com/docs/derivatives/coin-margined-futures/market-data/rest-api/Get-Funding-Rate-History-of-Perpetual-Futures + + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: timestamp in ms of the earliest funding rate to fetch + :param int [limit]: the maximum amount of `funding rate structures ` to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest funding rate + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param str [params.subType]: "linear" or "inverse" + :returns dict[]: a list of `funding rate structures ` + """ + await self.load_markets() + request: dict = {} + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchFundingRateHistory', 'paginate') + if paginate: + return await self.fetch_paginated_call_deterministic('fetchFundingRateHistory', symbol, since, limit, '8h', params) + defaultType = self.safe_string_2(self.options, 'fetchFundingRateHistory', 'defaultType', 'future') + type = self.safe_string(params, 'type', defaultType) + market = None + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + request['symbol'] = market['id'] + subType = None + subType, params = self.handle_sub_type_and_params('fetchFundingRateHistory', market, params, 'linear') + params = self.omit(params, 'type') + if since is not None: + request['startTime'] = since + until = self.safe_integer(params, 'until') # unified in milliseconds + endTime = self.safe_integer(params, 'endTime', until) # exchange-specific in milliseconds + params = self.omit(params, ['endTime', 'until']) + if endTime is not None: + request['endTime'] = endTime + if limit is not None: + request['limit'] = limit + response = None + if self.is_linear(type, subType): + response = await self.fapiPublicGetFundingRate(self.extend(request, params)) + elif self.is_inverse(type, subType): + response = await self.dapiPublicGetFundingRate(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchFundingRateHistory() is not supported for ' + type + ' markets') + # + # { + # "symbol": "BTCUSDT", + # "fundingRate": "0.00063521", + # "fundingTime": "1621267200000", + # } + # + return self.parse_funding_rate_histories(response, market, since, limit) + + def parse_funding_rate_history(self, contract, market: Market = None): + # + # { + # "symbol": "BTCUSDT", + # "fundingRate": "0.00063521", + # "fundingTime": "1621267200000", + # } + # + timestamp = self.safe_integer(contract, 'fundingTime') + return { + 'info': contract, + 'symbol': self.safe_symbol(self.safe_string(contract, 'symbol'), None, None, 'swap'), + 'fundingRate': self.safe_number(contract, 'fundingRate'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + + async def fetch_funding_rates(self, symbols: Strings = None, params={}) -> FundingRates: + """ + fetch the funding rate for multiple markets + + https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/Mark-Price + https://developers.binance.com/docs/derivatives/coin-margined-futures/market-data/rest-api/Index-Price-and-Mark-Price + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.subType]: "linear" or "inverse" + :returns dict[]: a list of `funding rate structures `, indexed by market symbols + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + defaultType = self.safe_string_2(self.options, 'fetchFundingRates', 'defaultType', 'future') + type = self.safe_string(params, 'type', defaultType) + subType = None + subType, params = self.handle_sub_type_and_params('fetchFundingRates', None, params, 'linear') + query = self.omit(params, 'type') + response = None + if self.is_linear(type, subType): + response = await self.fapiPublicGetPremiumIndex(query) + elif self.is_inverse(type, subType): + response = await self.dapiPublicGetPremiumIndex(query) + else: + raise NotSupported(self.id + ' fetchFundingRates() supports linear and inverse contracts only') + return self.parse_funding_rates(response, symbols) + + def parse_funding_rate(self, contract, market: Market = None) -> FundingRate: + # ensure it matches with https://www.binance.com/en/futures/funding-history/0 + # + # fetchFundingRate, fetchFundingRates + # + # { + # "symbol": "BTCUSDT", + # "markPrice": "45802.81129892", + # "indexPrice": "45745.47701915", + # "estimatedSettlePrice": "45133.91753671", + # "lastFundingRate": "0.00063521", + # "interestRate": "0.00010000", + # "nextFundingTime": "1621267200000", + # "time": "1621252344001" + # } + # + # fetchFundingInterval, fetchFundingIntervals + # + # { + # "symbol": "BLZUSDT", + # "adjustedFundingRateCap": "0.03000000", + # "adjustedFundingRateFloor": "-0.03000000", + # "fundingIntervalHours": 4, + # "disclaimer": False + # } + # + timestamp = self.safe_integer(contract, 'time') + marketId = self.safe_string(contract, 'symbol') + symbol = self.safe_symbol(marketId, market, None, 'contract') + markPrice = self.safe_number(contract, 'markPrice') + indexPrice = self.safe_number(contract, 'indexPrice') + interestRate = self.safe_number(contract, 'interestRate') + estimatedSettlePrice = self.safe_number(contract, 'estimatedSettlePrice') + fundingRate = self.safe_number(contract, 'lastFundingRate') + fundingTime = self.safe_integer(contract, 'nextFundingTime') + interval = self.safe_string(contract, 'fundingIntervalHours') + intervalString = None + if interval is not None: + intervalString = interval + 'h' + return { + 'info': contract, + 'symbol': symbol, + 'markPrice': markPrice, + 'indexPrice': indexPrice, + 'interestRate': interestRate, + 'estimatedSettlePrice': estimatedSettlePrice, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'fundingRate': fundingRate, + 'fundingTimestamp': fundingTime, + 'fundingDatetime': self.iso8601(fundingTime), + 'nextFundingRate': None, + 'nextFundingTimestamp': None, + 'nextFundingDatetime': None, + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': intervalString, + } + + def parse_account_positions(self, account, filterClosed=False): + positions = self.safe_list(account, 'positions') + assets = self.safe_list(account, 'assets', []) + balances: dict = {} + for i in range(0, len(assets)): + entry = assets[i] + currencyId = self.safe_string(entry, 'asset') + code = self.safe_currency_code(currencyId) + crossWalletBalance = self.safe_string(entry, 'crossWalletBalance') + crossUnPnl = self.safe_string(entry, 'crossUnPnl') + balances[code] = { + 'crossMargin': Precise.string_add(crossWalletBalance, crossUnPnl), + 'crossWalletBalance': crossWalletBalance, + } + result = [] + for i in range(0, len(positions)): + position = positions[i] + marketId = self.safe_string(position, 'symbol') + market = self.safe_market(marketId, None, None, 'contract') + code = market['quote'] if market['linear'] else market['base'] + maintenanceMargin = self.safe_string(position, 'maintMargin') + # check for maintenance margin so empty positions are not returned + isPositionOpen = (maintenanceMargin != '0') and (maintenanceMargin != '0.00000000') + if not filterClosed or isPositionOpen: + # sometimes not all the codes are correctly returned... + if code in balances: + parsed = self.parse_account_position(self.extend(position, { + 'crossMargin': balances[code]['crossMargin'], + 'crossWalletBalance': balances[code]['crossWalletBalance'], + }), market) + result.append(parsed) + return result + + def parse_account_position(self, position, market: Market = None): + # + # usdm + # + # v3(similar for cross & isolated) + # + # { + # "symbol": "WLDUSDT", + # "positionSide": "BOTH", + # "positionAmt": "-849", + # "unrealizedProfit": "11.17920750", + # "notional": "-1992.46079250", + # "isolatedMargin": "0", + # "isolatedWallet": "0", + # "initialMargin": "99.62303962", + # "maintMargin": "11.95476475", + # "updateTime": "1721995760449" + # "leverage": "50", # in v2 + # "entryPrice": "2.34", # in v2 + # "positionInitialMargin": "118.82116614", # in v2 + # "openOrderInitialMargin": "0", # in v2 + # "isolated": False, # in v2 + # "breakEvenPrice": "2.3395788", # in v2 + # "maxNotional": "25000", # in v2 + # "bidNotional": "0", # in v2 + # "askNotional": "0" # in v2 + # } + # + # coinm + # + # { + # "symbol": "BTCUSD_210625", + # "initialMargin": "0.00024393", + # "maintMargin": "0.00002439", + # "unrealizedProfit": "-0.00000163", + # "positionInitialMargin": "0.00024393", + # "openOrderInitialMargin": "0", + # "leverage": "10", + # "isolated": False, + # "positionSide": "BOTH", + # "entryPrice": "41021.20000069", + # "maxQty": "100", + # "notionalValue": "0.00243939", + # "isolatedWallet": "0", + # "crossMargin": "0.314" + # "crossWalletBalance": "34", + # } + # + # linear portfolio margin + # + # { + # "symbol": "CTSIUSDT", + # "initialMargin": "0", + # "maintMargin": "0", + # "unrealizedProfit": "0.00000000", + # "positionInitialMargin": "0", + # "openOrderInitialMargin": "0", + # "leverage": "20", + # "entryPrice": "0.0", + # "maxNotional": "25000", + # "bidNotional": "0", + # "askNotional": "0", + # "positionSide": "SHORT", + # "positionAmt": "0", + # "updateTime": 0, + # "notional": "0", + # "breakEvenPrice": "0.0" + # } + # + # inverse portoflio margin + # + # { + # "symbol": "TRXUSD_PERP", + # "initialMargin": "0", + # "maintMargin": "0", + # "unrealizedProfit": "0.00000000", + # "positionInitialMargin": "0", + # "openOrderInitialMargin": "0", + # "leverage": "20", + # "entryPrice": "0.00000000", + # "positionSide": "SHORT", + # "positionAmt": "0", + # "maxQty": "5000000", + # "updateTime": 0, + # "notionalValue": "0", + # "breakEvenPrice": "0.00000000" + # } + # + marketId = self.safe_string(position, 'symbol') + market = self.safe_market(marketId, market, None, 'contract') + symbol = self.safe_string(market, 'symbol') + leverageString = self.safe_string(position, 'leverage') + leverage = int(leverageString) if (leverageString is not None) else None + initialMarginString = self.safe_string(position, 'initialMargin') + initialMargin = self.parse_number(initialMarginString) + initialMarginPercentageString = None + if leverageString is not None: + initialMarginPercentageString = Precise.string_div('1', leverageString, 8) + rational = self.is_round_number(1000 % leverage) + if not rational: + initialMarginPercentageString = Precise.string_div(Precise.string_add(initialMarginPercentageString, '1e-8'), '1', 8) + # to notionalValue + usdm = ('notional' in position) + maintenanceMarginString = self.safe_string(position, 'maintMargin') + maintenanceMargin = self.parse_number(maintenanceMarginString) + entryPriceString = self.safe_string(position, 'entryPrice') + entryPrice = self.parse_number(entryPriceString) + notionalString = self.safe_string_2(position, 'notional', 'notionalValue') + notionalStringAbs = Precise.string_abs(notionalString) + notional = self.parse_number(notionalStringAbs) + contractsString = self.safe_string(position, 'positionAmt') + contractsStringAbs = Precise.string_abs(contractsString) + if contractsString is None: + entryNotional = Precise.string_mul(Precise.string_mul(leverageString, initialMarginString), entryPriceString) + contractSizeNew = self.safe_string(market, 'contractSize') + contractsString = Precise.string_div(entryNotional, contractSizeNew) + contractsStringAbs = Precise.string_div(Precise.string_add(contractsString, '0.5'), '1', 0) + contracts = self.parse_number(contractsStringAbs) + leverageBrackets = self.safe_dict(self.options, 'leverageBrackets', {}) + leverageBracket = self.safe_list(leverageBrackets, symbol, []) + maintenanceMarginPercentageString = None + for i in range(0, len(leverageBracket)): + bracket = leverageBracket[i] + if Precise.string_lt(notionalStringAbs, bracket[0]): + break + maintenanceMarginPercentageString = bracket[1] + maintenanceMarginPercentage = self.parse_number(maintenanceMarginPercentageString) + unrealizedPnlString = self.safe_string(position, 'unrealizedProfit') + unrealizedPnl = self.parse_number(unrealizedPnlString) + timestamp = self.safe_integer(position, 'updateTime') + if timestamp == 0: + timestamp = None + isolated = self.safe_bool(position, 'isolated') + if isolated is None: + isolatedMarginRaw = self.safe_string(position, 'isolatedMargin') + isolated = not Precise.string_eq(isolatedMarginRaw, '0') + marginMode = None + collateralString = None + walletBalance = None + if isolated: + marginMode = 'isolated' + walletBalance = self.safe_string(position, 'isolatedWallet') + collateralString = Precise.string_add(walletBalance, unrealizedPnlString) + else: + marginMode = 'cross' + walletBalance = self.safe_string(position, 'crossWalletBalance') + collateralString = self.safe_string(position, 'crossMargin') + collateral = self.parse_number(collateralString) + marginRatio = None + side = None + percentage = None + liquidationPriceStringRaw = None + liquidationPrice = None + contractSize = self.safe_value(market, 'contractSize') + contractSizeString = self.number_to_string(contractSize) + if Precise.string_equals(notionalString, '0'): + entryPrice = None + else: + side = 'short' if Precise.string_lt(notionalString, '0') else 'long' + marginRatio = self.parse_number(Precise.string_div(Precise.string_add(Precise.string_div(maintenanceMarginString, collateralString), '5e-5'), '1', 4)) + percentage = self.parse_number(Precise.string_mul(Precise.string_div(unrealizedPnlString, initialMarginString, 4), '100')) + if usdm: + # calculate liquidation price + # + # liquidationPrice = (walletBalance / (contracts * (±1 + mmp))) + (±entryPrice / (±1 + mmp)) + # + # mmp = maintenanceMarginPercentage + # where ± is negative for long and positive for short + # TODO: calculate liquidation price for coinm contracts + onePlusMaintenanceMarginPercentageString = None + entryPriceSignString = entryPriceString + if side == 'short': + onePlusMaintenanceMarginPercentageString = Precise.string_add('1', maintenanceMarginPercentageString) + else: + onePlusMaintenanceMarginPercentageString = Precise.string_add('-1', maintenanceMarginPercentageString) + entryPriceSignString = Precise.string_mul('-1', entryPriceSignString) + leftSide = Precise.string_div(walletBalance, Precise.string_mul(contractsStringAbs, onePlusMaintenanceMarginPercentageString)) + rightSide = Precise.string_div(entryPriceSignString, onePlusMaintenanceMarginPercentageString) + liquidationPriceStringRaw = Precise.string_add(leftSide, rightSide) + else: + # calculate liquidation price + # + # liquidationPrice = (contracts * contractSize(±1 - mmp)) / (±1/entryPrice * contracts * contractSize - walletBalance) + # + onePlusMaintenanceMarginPercentageString = None + entryPriceSignString = entryPriceString + if side == 'short': + onePlusMaintenanceMarginPercentageString = Precise.string_sub('1', maintenanceMarginPercentageString) + else: + onePlusMaintenanceMarginPercentageString = Precise.string_sub('-1', maintenanceMarginPercentageString) + entryPriceSignString = Precise.string_mul('-1', entryPriceSignString) + size = Precise.string_mul(contractsStringAbs, contractSizeString) + leftSide = Precise.string_mul(size, onePlusMaintenanceMarginPercentageString) + rightSide = Precise.string_sub(Precise.string_mul(Precise.string_div('1', entryPriceSignString), size), walletBalance) + liquidationPriceStringRaw = Precise.string_div(leftSide, rightSide) + pricePrecision = self.precision_from_string(self.safe_string(market['precision'], 'price')) + pricePrecisionPlusOne = pricePrecision + 1 + pricePrecisionPlusOneString = str(pricePrecisionPlusOne) + # round half up + rounder = Precise('5e-' + pricePrecisionPlusOneString) + rounderString = str(rounder) + liquidationPriceRoundedString = Precise.string_add(rounderString, liquidationPriceStringRaw) + truncatedLiquidationPrice = Precise.string_div(liquidationPriceRoundedString, '1', pricePrecision) + if truncatedLiquidationPrice[0] == '-': + # user cannot be liquidated + # since he has more collateral than the size of the position + truncatedLiquidationPrice = None + liquidationPrice = self.parse_number(truncatedLiquidationPrice) + positionSide = self.safe_string(position, 'positionSide') + hedged = positionSide != 'BOTH' + return { + 'info': position, + 'id': None, + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'initialMargin': initialMargin, + 'initialMarginPercentage': self.parse_number(initialMarginPercentageString), + 'maintenanceMargin': maintenanceMargin, + 'maintenanceMarginPercentage': maintenanceMarginPercentage, + 'entryPrice': entryPrice, + 'notional': notional, + 'leverage': self.parse_number(leverageString), + 'unrealizedPnl': unrealizedPnl, + 'contracts': contracts, + 'contractSize': contractSize, + 'marginRatio': marginRatio, + 'liquidationPrice': liquidationPrice, + 'markPrice': None, + 'collateral': collateral, + 'marginMode': marginMode, + 'side': side, + 'hedged': hedged, + 'percentage': percentage, + } + + def parse_position_risk(self, position, market: Market = None): + # + # usdm + # + # { + # symbol: "WLDUSDT", + # positionSide: "BOTH", + # positionAmt: "5", + # entryPrice: "2.3483", + # breakEvenPrice: "2.349356735", + # markPrice: "2.39560000", + # unRealizedProfit: "0.23650000", + # liquidationPrice: "0", + # isolatedMargin: "0", + # notional: "11.97800000", + # isolatedWallet: "0", + # updateTime: "1722062678998", + # initialMargin: "2.39560000", # not in v2 + # maintMargin: "0.07186800", # not in v2 + # positionInitialMargin: "2.39560000", # not in v2 + # openOrderInitialMargin: "0", # not in v2 + # adl: "2", # not in v2 + # bidNotional: "0", # not in v2 + # askNotional: "0", # not in v2 + # marginAsset: "USDT", # not in v2 + # # the below fields are only in v2 + # leverage: "5", + # maxNotionalValue: "6000000", + # marginType: "cross", + # isAutoAddMargin: "false", + # isolated: False, + # adlQuantile: "2", + # + # coinm + # + # { + # "symbol": "BTCUSD_PERP", + # "positionAmt": "2", + # "entryPrice": "37643.10000021", + # "markPrice": "38103.05510455", + # "unRealizedProfit": "0.00006413", + # "liquidationPrice": "25119.97445760", + # "leverage": "2", + # "maxQty": "1500", + # "marginType": "isolated", + # "isolatedMargin": "0.00274471", + # "isAutoAddMargin": "false", + # "positionSide": "BOTH", + # "notionalValue": "0.00524892", + # "isolatedWallet": "0.00268058" + # } + # + # inverse portfolio margin + # + # { + # "symbol": "ETHUSD_PERP", + # "positionAmt": "1", + # "entryPrice": "2422.400000007", + # "markPrice": "2424.51267823", + # "unRealizedProfit": "0.0000036", + # "liquidationPrice": "293.57678898", + # "leverage": "100", + # "positionSide": "LONG", + # "updateTime": 1707371941861, + # "maxQty": "15", + # "notionalValue": "0.00412454", + # "breakEvenPrice": "2423.368960034" + # } + # + # linear portfolio margin + # + # { + # "symbol": "BTCUSDT", + # "positionAmt": "0.01", + # "entryPrice": "44525.0", + # "markPrice": "45464.1735922", + # "unRealizedProfit": "9.39173592", + # "liquidationPrice": "38007.16308568", + # "leverage": "100", + # "positionSide": "LONG", + # "updateTime": 1707371879042, + # "maxNotionalValue": "500000.0", + # "notional": "454.64173592", + # "breakEvenPrice": "44542.81" + # } + # + marketId = self.safe_string(position, 'symbol') + market = self.safe_market(marketId, market, None, 'contract') + symbol = self.safe_string(market, 'symbol') + isolatedMarginString = self.safe_string(position, 'isolatedMargin') + leverageBrackets = self.safe_dict(self.options, 'leverageBrackets', {}) + leverageBracket = self.safe_list(leverageBrackets, symbol, []) + notionalString = self.safe_string_2(position, 'notional', 'notionalValue') + notionalStringAbs = Precise.string_abs(notionalString) + maintenanceMarginPercentageString = None + for i in range(0, len(leverageBracket)): + bracket = leverageBracket[i] + if Precise.string_lt(notionalStringAbs, bracket[0]): + break + maintenanceMarginPercentageString = bracket[1] + notional = self.parse_number(notionalStringAbs) + contractsAbs = Precise.string_abs(self.safe_string(position, 'positionAmt')) + contracts = self.parse_number(contractsAbs) + unrealizedPnlString = self.safe_string(position, 'unRealizedProfit') + unrealizedPnl = self.parse_number(unrealizedPnlString) + liquidationPriceString = self.omit_zero(self.safe_string(position, 'liquidationPrice')) + liquidationPrice = self.parse_number(liquidationPriceString) + collateralString = None + marginMode = self.safe_string(position, 'marginType') + if marginMode is None and isolatedMarginString is not None: + marginMode = 'cross' if Precise.string_eq(isolatedMarginString, '0') else 'isolated' + side = None + if Precise.string_gt(notionalString, '0'): + side = 'long' + elif Precise.string_lt(notionalString, '0'): + side = 'short' + entryPriceString = self.safe_string(position, 'entryPrice') + entryPrice = self.parse_number(entryPriceString) + contractSize = self.safe_value(market, 'contractSize') + contractSizeString = self.number_to_string(contractSize) + # to notionalValue + linear = ('notional' in position) + if marginMode == 'cross': + # calculate collateral + precision = self.safe_dict(market, 'precision', {}) + basePrecisionValue = self.safe_string(precision, 'base') + quotePrecisionValue = self.safe_string_2(precision, 'quote', 'price') + precisionIsUndefined = (basePrecisionValue is None) and (quotePrecisionValue is None) + if not precisionIsUndefined: + if linear: + # walletBalance = (liquidationPrice * (±1 + mmp) ± entryPrice) * contracts + onePlusMaintenanceMarginPercentageString = None + entryPriceSignString = entryPriceString + if side == 'short': + onePlusMaintenanceMarginPercentageString = Precise.string_add('1', maintenanceMarginPercentageString) + entryPriceSignString = Precise.string_mul('-1', entryPriceSignString) + else: + onePlusMaintenanceMarginPercentageString = Precise.string_add('-1', maintenanceMarginPercentageString) + inner = Precise.string_mul(liquidationPriceString, onePlusMaintenanceMarginPercentageString) + leftSide = Precise.string_add(inner, entryPriceSignString) + quotePrecision = self.precision_from_string(self.safe_string_2(precision, 'quote', 'price')) + if quotePrecision is not None: + collateralString = Precise.string_div(Precise.string_mul(leftSide, contractsAbs), '1', quotePrecision) + else: + # walletBalance = (contracts * contractSize) * (±1/entryPrice - (±1 - mmp) / liquidationPrice) + onePlusMaintenanceMarginPercentageString = None + entryPriceSignString = entryPriceString + if side == 'short': + onePlusMaintenanceMarginPercentageString = Precise.string_sub('1', maintenanceMarginPercentageString) + else: + onePlusMaintenanceMarginPercentageString = Precise.string_sub('-1', maintenanceMarginPercentageString) + entryPriceSignString = Precise.string_mul('-1', entryPriceSignString) + leftSide = Precise.string_mul(contractsAbs, contractSizeString) + rightSide = Precise.string_sub(Precise.string_div('1', entryPriceSignString), Precise.string_div(onePlusMaintenanceMarginPercentageString, liquidationPriceString)) + basePrecision = self.precision_from_string(self.safe_string(precision, 'base')) + if basePrecision is not None: + collateralString = Precise.string_div(Precise.string_mul(leftSide, rightSide), '1', basePrecision) + else: + collateralString = self.safe_string(position, 'isolatedMargin') + collateralString = '0' if (collateralString is None) else collateralString + collateral = self.parse_number(collateralString) + markPrice = self.parse_number(self.omit_zero(self.safe_string(position, 'markPrice'))) + timestamp = self.safe_integer(position, 'updateTime') + if timestamp == 0: + timestamp = None + maintenanceMarginPercentage = self.parse_number(maintenanceMarginPercentageString) + maintenanceMarginString = Precise.string_mul(maintenanceMarginPercentageString, notionalStringAbs) + if maintenanceMarginString is None: + # for a while, self new value was a backup to the existing calculations, but in future we might prioritize self + maintenanceMarginString = self.safe_string(position, 'maintMargin') + maintenanceMargin = self.parse_number(maintenanceMarginString) + initialMarginString = None + initialMarginPercentageString = None + leverageString = self.safe_string(position, 'leverage') + if leverageString is not None: + leverage = int(leverageString) + rational = self.is_round_number(1000 % leverage) + initialMarginPercentageString = Precise.string_div('1', leverageString, 8) + if not rational: + initialMarginPercentageString = Precise.string_add(initialMarginPercentageString, '1e-8') + unrounded = Precise.string_mul(notionalStringAbs, initialMarginPercentageString) + initialMarginString = Precise.string_div(unrounded, '1', 8) + else: + initialMarginString = self.safe_string(position, 'initialMargin') + unrounded = Precise.string_mul(initialMarginString, '1') + initialMarginPercentageString = Precise.string_div(unrounded, notionalStringAbs, 8) + marginRatio = None + percentage = None + if not Precise.string_equals(collateralString, '0'): + marginRatio = self.parse_number(Precise.string_div(Precise.string_add(Precise.string_div(maintenanceMarginString, collateralString), '5e-5'), '1', 4)) + percentage = self.parse_number(Precise.string_mul(Precise.string_div(unrealizedPnlString, initialMarginString, 4), '100')) + positionSide = self.safe_string(position, 'positionSide') + hedged = positionSide != 'BOTH' + return { + 'info': position, + 'id': None, + 'symbol': symbol, + 'contracts': contracts, + 'contractSize': contractSize, + 'unrealizedPnl': unrealizedPnl, + 'leverage': self.parse_number(leverageString), + 'liquidationPrice': liquidationPrice, + 'collateral': collateral, + 'notional': notional, + 'markPrice': markPrice, + 'entryPrice': entryPrice, + 'timestamp': timestamp, + 'initialMargin': self.parse_number(initialMarginString), + 'initialMarginPercentage': self.parse_number(initialMarginPercentageString), + 'maintenanceMargin': maintenanceMargin, + 'maintenanceMarginPercentage': maintenanceMarginPercentage, + 'marginRatio': marginRatio, + 'datetime': self.iso8601(timestamp), + 'marginMode': marginMode, + 'marginType': marginMode, # deprecated + 'side': side, + 'hedged': hedged, + 'percentage': percentage, + 'stopLossPrice': None, + 'takeProfitPrice': None, + } + + async def load_leverage_brackets(self, reload=False, params={}): + await self.load_markets() + # by default cache the leverage bracket + # it contains useful stuff like the maintenance margin and initial margin for positions + leverageBrackets = self.safe_dict(self.options, 'leverageBrackets') + if (leverageBrackets is None) or (reload): + defaultType = self.safe_string(self.options, 'defaultType', 'future') + type = self.safe_string(params, 'type', defaultType) + query = self.omit(params, 'type') + subType = None + subType, params = self.handle_sub_type_and_params('loadLeverageBrackets', None, params, 'linear') + isPortfolioMargin = None + isPortfolioMargin, params = self.handle_option_and_params_2(params, 'loadLeverageBrackets', 'papi', 'portfolioMargin', False) + response = None + if self.is_linear(type, subType): + if isPortfolioMargin: + response = await self.papiGetUmLeverageBracket(query) + else: + response = await self.fapiPrivateGetLeverageBracket(query) + elif self.is_inverse(type, subType): + if isPortfolioMargin: + response = await self.papiGetCmLeverageBracket(query) + else: + response = await self.dapiPrivateV2GetLeverageBracket(query) + else: + raise NotSupported(self.id + ' loadLeverageBrackets() supports linear and inverse contracts only') + self.options['leverageBrackets'] = self.create_safe_dictionary() + for i in range(0, len(response)): + entry = response[i] + marketId = self.safe_string(entry, 'symbol') + symbol = self.safe_symbol(marketId, None, None, 'contract') + brackets = self.safe_list(entry, 'brackets', []) + result = [] + for j in range(0, len(brackets)): + bracket = brackets[j] + floorValue = self.safe_string_2(bracket, 'notionalFloor', 'qtyFloor') + maintenanceMarginPercentage = self.safe_string(bracket, 'maintMarginRatio') + result.append([floorValue, maintenanceMarginPercentage]) + self.options['leverageBrackets'][symbol] = result + return self.options['leverageBrackets'] + + async def fetch_leverage_tiers(self, symbols: Strings = None, params={}) -> LeverageTiers: + """ + retrieve information on the maximum leverage, and maintenance margin for trades of varying trade sizes + + https://developers.binance.com/docs/derivatives/usds-margined-futures/account/rest-api/Notional-and-Leverage-Brackets + https://developers.binance.com/docs/derivatives/coin-margined-futures/account/rest-api/Notional-Bracket-for-Pair + https://developers.binance.com/docs/derivatives/portfolio-margin/account/UM-Notional-and-Leverage-Brackets + https://developers.binance.com/docs/derivatives/portfolio-margin/account/CM-Notional-and-Leverage-Brackets + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.portfolioMargin]: set to True if you would like to fetch the leverage tiers for a portfolio margin account + :param str [params.subType]: "linear" or "inverse" + :returns dict: a dictionary of `leverage tiers structures `, indexed by market symbols + """ + await self.load_markets() + type = None + type, params = self.handle_market_type_and_params('fetchLeverageTiers', None, params) + subType = None + subType, params = self.handle_sub_type_and_params('fetchLeverageTiers', None, params, 'linear') + isPortfolioMargin = None + isPortfolioMargin, params = self.handle_option_and_params_2(params, 'fetchLeverageTiers', 'papi', 'portfolioMargin', False) + response = None + if self.is_linear(type, subType): + if isPortfolioMargin: + response = await self.papiGetUmLeverageBracket(params) + else: + response = await self.fapiPrivateGetLeverageBracket(params) + elif self.is_inverse(type, subType): + if isPortfolioMargin: + response = await self.papiGetCmLeverageBracket(params) + else: + response = await self.dapiPrivateV2GetLeverageBracket(params) + else: + raise NotSupported(self.id + ' fetchLeverageTiers() supports linear and inverse contracts only') + # + # usdm + # + # [ + # { + # "symbol": "SUSHIUSDT", + # "brackets": [ + # { + # "bracket": 1, + # "initialLeverage": 50, + # "notionalCap": 50000, + # "notionalFloor": 0, + # "maintMarginRatio": 0.01, + # "cum": 0.0 + # }, + # ... + # ] + # } + # ] + # + # coinm + # + # [ + # { + # "symbol":"XRPUSD_210326", + # "brackets":[ + # { + # "bracket":1, + # "initialLeverage":20, + # "qtyCap":500000, + # "qtyFloor":0, + # "maintMarginRatio":0.0185, + # "cum":0.0 + # } + # ] + # } + # ] + # + return self.parse_leverage_tiers(response, symbols, 'symbol') + + def parse_market_leverage_tiers(self, info, market: Market = None) -> List[LeverageTier]: + """ + @ignore + :param dict info: Exchange response for 1 market + :param dict market: CCXT market + """ + # + # { + # "symbol": "SUSHIUSDT", + # "brackets": [ + # { + # "bracket": 1, + # "initialLeverage": 50, + # "notionalCap": 50000, + # "notionalFloor": 0, + # "maintMarginRatio": 0.01, + # "cum": 0.0 + # }, + # ... + # ] + # } + # + marketId = self.safe_string(info, 'symbol') + market = self.safe_market(marketId, market, None, 'contract') + brackets = self.safe_list(info, 'brackets', []) + tiers = [] + for j in range(0, len(brackets)): + bracket = brackets[j] + tiers.append({ + 'tier': self.safe_number(bracket, 'bracket'), + 'symbol': self.safe_symbol(marketId, market), + 'currency': market['quote'], + 'minNotional': self.safe_number_2(bracket, 'notionalFloor', 'qtyFloor'), + 'maxNotional': self.safe_number_2(bracket, 'notionalCap', 'qtyCap'), + 'maintenanceMarginRate': self.safe_number(bracket, 'maintMarginRatio'), + 'maxLeverage': self.safe_number(bracket, 'initialLeverage'), + 'info': bracket, + }) + return tiers + + async def fetch_position(self, symbol: str, params={}): + """ + fetch data on an open position + + https://developers.binance.com/docs/derivatives/option/trade/Option-Position-Information + + :param str symbol: unified market symbol of the market the position is held in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `position structure ` + """ + await self.load_markets() + market = self.market(symbol) + if not market['option']: + raise NotSupported(self.id + ' fetchPosition() supports option markets only') + request: dict = { + 'symbol': market['id'], + } + response = await self.eapiPrivateGetPosition(self.extend(request, params)) + # + # [ + # { + # "entryPrice": "27.70000000", + # "symbol": "ETH-230426-1850-C", + # "side": "LONG", + # "quantity": "0.50000000", + # "reducibleQty": "0.50000000", + # "markValue": "10.250000000", + # "ror": "-0.2599", + # "unrealizedPNL": "-3.600000000", + # "markPrice": "20.5", + # "strikePrice": "1850.00000000", + # "positionCost": "13.85000000", + # "expiryDate": 1682496000000, + # "priceScale": 1, + # "quantityScale": 2, + # "optionSide": "CALL", + # "quoteAsset": "USDT", + # "time": 1682492427106 + # } + # ] + # + return self.parse_position(response[0], market) + + async def fetch_option_positions(self, symbols: Strings = None, params={}): + """ + fetch data on open options positions + + https://developers.binance.com/docs/derivatives/option/trade/Option-Position-Information + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + request: dict = {} + market = None + if symbols is not None: + symbol = None + if isinstance(symbols, list): + symbolsLength = len(symbols) + if symbolsLength > 1: + raise BadRequest(self.id + ' fetchPositions() symbols argument cannot contain more than 1 symbol') + symbol = symbols[0] + else: + symbol = symbols + market = self.market(symbol) + request['symbol'] = market['id'] + response = await self.eapiPrivateGetPosition(self.extend(request, params)) + # + # [ + # { + # "entryPrice": "27.70000000", + # "symbol": "ETH-230426-1850-C", + # "side": "LONG", + # "quantity": "0.50000000", + # "reducibleQty": "0.50000000", + # "markValue": "10.250000000", + # "ror": "-0.2599", + # "unrealizedPNL": "-3.600000000", + # "markPrice": "20.5", + # "strikePrice": "1850.00000000", + # "positionCost": "13.85000000", + # "expiryDate": 1682496000000, + # "priceScale": 1, + # "quantityScale": 2, + # "optionSide": "CALL", + # "quoteAsset": "USDT", + # "time": 1682492427106 + # } + # ] + # + result = [] + for i in range(0, len(response)): + result.append(self.parse_position(response[i], market)) + return self.filter_by_array_positions(result, 'symbol', symbols, False) + + def parse_position(self, position: dict, market: Market = None): + # + # { + # "entryPrice": "27.70000000", + # "symbol": "ETH-230426-1850-C", + # "side": "LONG", + # "quantity": "0.50000000", + # "reducibleQty": "0.50000000", + # "markValue": "10.250000000", + # "ror": "-0.2599", + # "unrealizedPNL": "-3.600000000", + # "markPrice": "20.5", + # "strikePrice": "1850.00000000", + # "positionCost": "13.85000000", + # "expiryDate": 1682496000000, + # "priceScale": 1, + # "quantityScale": 2, + # "optionSide": "CALL", + # "quoteAsset": "USDT", + # "time": 1682492427106 + # } + # + marketId = self.safe_string(position, 'symbol') + market = self.safe_market(marketId, market, None, 'swap') + symbol = market['symbol'] + side = self.safe_string_lower(position, 'side') + quantity = self.safe_string(position, 'quantity') + if side != 'long': + quantity = Precise.string_mul('-1', quantity) + timestamp = self.safe_integer(position, 'time') + return self.safe_position({ + 'info': position, + 'id': None, + 'symbol': symbol, + 'entryPrice': self.safe_number(position, 'entryPrice'), + 'markPrice': self.safe_number(position, 'markPrice'), + 'notional': self.safe_number(position, 'markValue'), + 'collateral': self.safe_number(position, 'positionCost'), + 'unrealizedPnl': self.safe_number(position, 'unrealizedPNL'), + 'side': side, + 'contracts': self.parse_number(quantity), + 'contractSize': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'hedged': None, + 'maintenanceMargin': None, + 'maintenanceMarginPercentage': None, + 'initialMargin': None, + 'initialMarginPercentage': None, + 'leverage': None, + 'liquidationPrice': None, + 'marginRatio': None, + 'marginMode': None, + 'percentage': None, + }) + + async def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://developers.binance.com/docs/derivatives/usds-margined-futures/account/rest-api/Account-Information-V2 + https://developers.binance.com/docs/derivatives/coin-margined-futures/account/rest-api/Account-Information + https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Position-Information-V2 + https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/rest-api/Position-Information + https://developers.binance.com/docs/derivatives/option/trade/Option-Position-Information + + :param str[] [symbols]: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param dict [params.params]: extra parameters specific to the exchange API endpoint + :param str [params.method]: method name to call, "positionRisk", "account" or "option", default is "positionRisk" + :param bool [params.useV2]: set to True if you want to use the obsolete endpoint, where some more additional fields were provided + :returns dict[]: a list of `position structure ` + """ + defaultMethod = None + defaultMethod, params = self.handle_option_and_params(params, 'fetchPositions', 'method') + if defaultMethod is None: + options = self.safe_dict(self.options, 'fetchPositions') + if options is None: + defaultMethod = self.safe_string(self.options, 'fetchPositions', 'positionRisk') + else: + defaultMethod = 'positionRisk' + if defaultMethod == 'positionRisk': + return await self.fetch_positions_risk(symbols, params) + elif defaultMethod == 'account': + return await self.fetch_account_positions(symbols, params) + elif defaultMethod == 'option': + return await self.fetch_option_positions(symbols, params) + else: + raise NotSupported(self.id + '.options["fetchPositions"]["method"] or params["method"] = "' + defaultMethod + '" is invalid, please choose between "account", "positionRisk" and "option"') + + async def fetch_account_positions(self, symbols: Strings = None, params={}): + """ + @ignore + fetch account positions + + https://developers.binance.com/docs/derivatives/usds-margined-futures/account/rest-api/Account-Information-V2 + https://developers.binance.com/docs/derivatives/coin-margined-futures/account/rest-api/Account-Information + https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Position-Information-V2 + https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/rest-api/Position-Information + https://developers.binance.com/docs/derivatives/usds-margined-futures/account/rest-api/Account-Information-V3 + + :param str[] [symbols]: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.portfolioMargin]: set to True if you would like to fetch positions in a portfolio margin account + :param str [params.subType]: "linear" or "inverse" + :param boolean [params.filterClosed]: set to True if you would like to filter out closed positions, default is False + :param boolean [params.useV2]: set to True if you want to use obsolete endpoint, where some more additional fields were provided + :returns dict: data on account positions + """ + if symbols is not None: + if not isinstance(symbols, list): + raise ArgumentsRequired(self.id + ' fetchPositions() requires an array argument for symbols') + await self.load_markets() + await self.load_leverage_brackets(False, params) + defaultType = self.safe_string(self.options, 'defaultType', 'future') + type = self.safe_string(params, 'type', defaultType) + params = self.omit(params, 'type') + subType = None + subType, params = self.handle_sub_type_and_params('fetchAccountPositions', None, params, 'linear') + isPortfolioMargin = None + isPortfolioMargin, params = self.handle_option_and_params_2(params, 'fetchAccountPositions', 'papi', 'portfolioMargin', False) + response = None + if self.is_linear(type, subType): + if isPortfolioMargin: + response = await self.papiV2GetUmAccount(params) + else: + useV2 = None + useV2, params = self.handle_option_and_params(params, 'fetchAccountPositions', 'useV2', False) + if not useV2: + response = await self.fapiPrivateV3GetAccount(params) + else: + response = await self.fapiPrivateV2GetAccount(params) + # + # { + # "totalInitialMargin": "99.62112386", + # "totalMaintMargin": "11.95453485", + # "totalWalletBalance": "99.84331553", + # "totalUnrealizedProfit": "11.17675690", + # "totalMarginBalance": "111.02007243", + # "totalPositionInitialMargin": "99.62112386", + # "totalOpenOrderInitialMargin": "0.00000000", + # "totalCrossWalletBalance": "99.84331553", + # "totalCrossUnPnl": "11.17675690", + # "availableBalance": "11.39894857", + # "maxWithdrawAmount": "11.39894857", + # "feeTier": "0", # in v2 + # "canTrade": True, # in v2 + # "canDeposit": True, # in v2 + # "canWithdraw": True, # in v2 + # "feeBurn": True, # in v2 + # "tradeGroupId": "-1",// in v2 + # "updateTime": "0", # in v2 + # "multiAssetsMargin": True # in v2 + # "assets": [ + # { + # "asset": "USDT", + # "walletBalance": "72.72317863", + # "unrealizedProfit": "11.17920750", + # "marginBalance": "83.90238613", + # "maintMargin": "11.95476475", + # "initialMargin": "99.62303962", + # "positionInitialMargin": "99.62303962", + # "openOrderInitialMargin": "0.00000000", + # "crossWalletBalance": "72.72317863", + # "crossUnPnl": "11.17920750", + # "availableBalance": "11.39916777", + # "maxWithdrawAmount": "11.39916777", + # "updateTime": "1721995605338", + # "marginAvailable": True # in v2 + # }, + # ... and some few supported settle currencies: USDC, BTC, ETH, BNB .. + # ], + # "positions": [ + # { + # "symbol": "WLDUSDT", + # "positionSide": "BOTH", + # "positionAmt": "-849", + # "unrealizedProfit": "11.17920750", + # "isolatedMargin": "0", + # "isolatedWallet": "0", + # "notional": "-1992.46079250", + # "initialMargin": "99.62303962", + # "maintMargin": "11.95476475", + # "updateTime": "1721995760449" + # "leverage": "50", # in v2 + # "entryPrice": "2.34", # in v2 + # "positionInitialMargin": "118.82116614", # in v2 + # "openOrderInitialMargin": "0", # in v2 + # "isolated": False, # in v2 + # "breakEvenPrice": "2.3395788", # in v2 + # "maxNotional": "25000", # in v2 + # "bidNotional": "0", # in v2 + # "askNotional": "0" # in v2 + # }, + # ... + # ] + # } + # + elif self.is_inverse(type, subType): + if isPortfolioMargin: + response = await self.papiGetCmAccount(params) + else: + response = await self.dapiPrivateGetAccount(params) + else: + raise NotSupported(self.id + ' fetchPositions() supports linear and inverse contracts only') + filterClosed = None + filterClosed, params = self.handle_option_and_params(params, 'fetchAccountPositions', 'filterClosed', False) + result = self.parse_account_positions(response, filterClosed) + symbols = self.market_symbols(symbols) + return self.filter_by_array_positions(result, 'symbol', symbols, False) + + async def fetch_positions_risk(self, symbols: Strings = None, params={}): + """ + @ignore + fetch positions risk + + https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Position-Information-V2 + https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/rest-api/Position-Information + https://developers.binance.com/docs/derivatives/portfolio-margin/account/Query-UM-Position-Information + https://developers.binance.com/docs/derivatives/portfolio-margin/account/Query-CM-Position-Information + https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Position-Information-V3 + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.portfolioMargin]: set to True if you would like to fetch positions for a portfolio margin account + :param str [params.subType]: "linear" or "inverse" + :param bool [params.useV2]: set to True if you want to use the obsolete endpoint, where some more additional fields were provided + :returns dict: data on the positions risk + """ + if symbols is not None: + if not isinstance(symbols, list): + raise ArgumentsRequired(self.id + ' fetchPositionsRisk() requires an array argument for symbols') + await self.load_markets() + await self.load_leverage_brackets(False, params) + request: dict = {} + defaultType = 'future' + defaultType = self.safe_string(self.options, 'defaultType', defaultType) + type = self.safe_string(params, 'type', defaultType) + subType = None + subType, params = self.handle_sub_type_and_params('fetchPositionsRisk', None, params, 'linear') + isPortfolioMargin = None + isPortfolioMargin, params = self.handle_option_and_params_2(params, 'fetchPositionsRisk', 'papi', 'portfolioMargin', False) + params = self.omit(params, 'type') + response = None + if self.is_linear(type, subType): + if isPortfolioMargin: + response = await self.papiGetUmPositionRisk(self.extend(request, params)) + else: + useV2 = None + useV2, params = self.handle_option_and_params(params, 'fetchPositionsRisk', 'useV2', False) + params = self.extend(request, params) + if not useV2: + response = await self.fapiPrivateV3GetPositionRisk(params) + else: + response = await self.fapiPrivateV2GetPositionRisk(params) + # + # [ + # { + # symbol: "WLDUSDT", + # positionSide: "BOTH", + # positionAmt: "5", + # entryPrice: "2.3483", + # breakEvenPrice: "2.349356735", + # markPrice: "2.39560000", + # unRealizedProfit: "0.23650000", + # liquidationPrice: "0", + # isolatedMargin: "0", + # notional: "11.97800000", + # isolatedWallet: "0", + # updateTime: "1722062678998", + # initialMargin: "2.39560000", # added in v3 + # maintMargin: "0.07186800", # added in v3 + # positionInitialMargin: "2.39560000", # added in v3 + # openOrderInitialMargin: "0", # added in v3 + # adl: "2", # added in v3 + # bidNotional: "0", # added in v3 + # askNotional: "0", # added in v3 + # marginAsset: "USDT", # added in v3 + # }, + # ] + # + elif self.is_inverse(type, subType): + if isPortfolioMargin: + response = await self.papiGetCmPositionRisk(self.extend(request, params)) + else: + response = await self.dapiPrivateGetPositionRisk(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchPositionsRisk() supports linear and inverse contracts only') + # ### Response examples ### + # + # For One-way position mode: + # + # [ + # { + # "symbol": "BTCUSDT", + # "positionSide": "BOTH", + # "positionAmt": "0.000", + # "entryPrice": "0.00000", + # "markPrice": "6679.50671178", + # "unRealizedProfit": "0.00000000", + # "liquidationPrice": "0", + # "isolatedMargin": "0.00000000", + # "marginType": "isolated", + # "isAutoAddMargin": "false", + # "leverage": "10", + # "maxNotionalValue": "20000000", + # "updateTime": 0 + # } + # ] + # + # For Hedge position mode: + # + # [ + # { + # "entryPrice": "6563.66500", + # "marginType": "isolated", + # "isAutoAddMargin": "false", + # "isolatedMargin": "15517.54150468", + # "leverage": "10", + # "liquidationPrice": "5930.78", + # "markPrice": "6679.50671178", + # "maxNotionalValue": "20000000", + # "positionSide": "LONG", + # "positionAmt": "20.000", # negative value for 'SHORT' + # "symbol": "BTCUSDT", + # "unRealizedProfit": "2316.83423560" + # "updateTime": 1625474304765 + # }, + # .. second dict is similar, but with `positionSide: SHORT` + # ] + # + # inverse portfolio margin: + # + # [ + # { + # "symbol": "ETHUSD_PERP", + # "positionAmt": "1", + # "entryPrice": "2422.400000007", + # "markPrice": "2424.51267823", + # "unRealizedProfit": "0.0000036", + # "liquidationPrice": "293.57678898", + # "leverage": "100", + # "positionSide": "LONG", + # "updateTime": 1707371941861, + # "maxQty": "15", + # "notionalValue": "0.00412454", + # "breakEvenPrice": "2423.368960034" + # } + # ] + # + # linear portfolio margin: + # + # [ + # { + # "symbol": "BTCUSDT", + # "positionAmt": "0.01", + # "entryPrice": "44525.0", + # "markPrice": "45464.1735922", + # "unRealizedProfit": "9.39173592", + # "liquidationPrice": "38007.16308568", + # "leverage": "100", + # "positionSide": "LONG", + # "updateTime": 1707371879042, + # "maxNotionalValue": "500000.0", + # "notional": "454.64173592", + # "breakEvenPrice": "44542.81" + # } + # ] + # + result = [] + for i in range(0, len(response)): + rawPosition = response[i] + entryPriceString = self.safe_string(rawPosition, 'entryPrice') + if Precise.string_gt(entryPriceString, '0'): + result.append(self.parse_position_risk(response[i])) + symbols = self.market_symbols(symbols) + return self.filter_by_array_positions(result, 'symbol', symbols, False) + + async def fetch_funding_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch the history of funding payments paid and received on self account + + https://developers.binance.com/docs/derivatives/usds-margined-futures/account/rest-api/Get-Income-History + https://developers.binance.com/docs/derivatives/coin-margined-futures/account/rest-api/Get-Income-History + https://developers.binance.com/docs/derivatives/portfolio-margin/account/Get-UM-Income-History + https://developers.binance.com/docs/derivatives/portfolio-margin/account/Get-CM-Income-History + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch funding history for + :param int [limit]: the maximum number of funding history structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest funding history entry + :param boolean [params.portfolioMargin]: set to True if you would like to fetch the funding history for a portfolio margin account + :param str [params.subType]: "linear" or "inverse" + :returns dict: a `funding history structure ` + """ + await self.load_markets() + market = None + request: dict = { + 'incomeType': 'FUNDING_FEE', # "TRANSFER","WELCOME_BONUS", "REALIZED_PNL","FUNDING_FEE", "COMMISSION" and "INSURANCE_CLEAR" + } + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if not market['swap']: + raise NotSupported(self.id + ' fetchFundingHistory() supports swap contracts only') + subType = None + subType, params = self.handle_sub_type_and_params('fetchFundingHistory', market, params, 'linear') + isPortfolioMargin = None + isPortfolioMargin, params = self.handle_option_and_params_2(params, 'fetchFundingHistory', 'papi', 'portfolioMargin', False) + request, params = self.handle_until_option('endTime', request, params) + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + defaultType = self.safe_string_2(self.options, 'fetchFundingHistory', 'defaultType', 'future') + type = self.safe_string(params, 'type', defaultType) + params = self.omit(params, 'type') + response = None + if self.is_linear(type, subType): + if isPortfolioMargin: + response = await self.papiGetUmIncome(self.extend(request, params)) + else: + response = await self.fapiPrivateGetIncome(self.extend(request, params)) + elif self.is_inverse(type, subType): + if isPortfolioMargin: + response = await self.papiGetCmIncome(self.extend(request, params)) + else: + response = await self.dapiPrivateGetIncome(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchFundingHistory() supports linear and inverse contracts only') + return self.parse_incomes(response, market, since, limit) + + async def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Change-Initial-Leverage + https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/rest-api/Change-Initial-Leverage + https://developers.binance.com/docs/derivatives/portfolio-margin/account/Change-UM-Initial-Leverage + https://developers.binance.com/docs/derivatives/portfolio-margin/account/Change-CM-Initial-Leverage + + :param float leverage: the rate of leverage + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.portfolioMargin]: set to True if you would like to set the leverage for a trading pair in a portfolio margin account + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setLeverage() requires a symbol argument') + # WARNING: THIS WILL INCREASE LIQUIDATION PRICE FOR OPEN ISOLATED LONG POSITIONS + # AND DECREASE LIQUIDATION PRICE FOR OPEN ISOLATED SHORT POSITIONS + if (leverage < 1) or (leverage > 125): + raise BadRequest(self.id + ' leverage should be between 1 and 125') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'leverage': leverage, + } + isPortfolioMargin = None + isPortfolioMargin, params = self.handle_option_and_params_2(params, 'setLeverage', 'papi', 'portfolioMargin', False) + response = None + if market['linear']: + if isPortfolioMargin: + response = await self.papiPostUmLeverage(self.extend(request, params)) + else: + response = await self.fapiPrivatePostLeverage(self.extend(request, params)) + elif market['inverse']: + if isPortfolioMargin: + response = await self.papiPostCmLeverage(self.extend(request, params)) + else: + response = await self.dapiPrivatePostLeverage(self.extend(request, params)) + else: + raise NotSupported(self.id + ' setLeverage() supports linear and inverse contracts only') + return response + + async def set_margin_mode(self, marginMode: str, symbol: Str = None, params={}): + """ + set margin mode to 'cross' or 'isolated' + + https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Change-Margin-Type + https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/rest-api/Change-Margin-Type + + :param str marginMode: 'cross' or 'isolated' + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setMarginMode() requires a symbol argument') + # + # {"code": -4048 , "msg": "Margin type cannot be changed if there exists position."} + # + # or + # + # {"code": 200, "msg": "success"} + # + marginMode = marginMode.upper() + if marginMode == 'CROSS': + marginMode = 'CROSSED' + if (marginMode != 'ISOLATED') and (marginMode != 'CROSSED'): + raise BadRequest(self.id + ' marginMode must be either isolated or cross') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'marginType': marginMode, + } + response = None + try: + if market['linear']: + response = await self.fapiPrivatePostMarginType(self.extend(request, params)) + elif market['inverse']: + response = await self.dapiPrivatePostMarginType(self.extend(request, params)) + else: + raise NotSupported(self.id + ' setMarginMode() supports linear and inverse contracts only') + except Exception as e: + # not an error + # https://github.com/ccxt/ccxt/issues/11268 + # https://github.com/ccxt/ccxt/pull/11624 + # POST https://fapi.binance.com/fapi/v1/marginType 400 Bad Request + # binanceusdm + if isinstance(e, MarginModeAlreadySet): + throwMarginModeAlreadySet = self.safe_bool(self.options, 'throwMarginModeAlreadySet', False) + if throwMarginModeAlreadySet: + raise e + else: + response = {'code': -4046, 'msg': 'No need to change margin type.'} + else: + raise e + return response + + async def set_position_mode(self, hedged: bool, symbol: Str = None, params={}): + """ + set hedged to True or False for a market + + https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Change-Position-Mode + https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/rest-api/Change-Position-Mode + https://developers.binance.com/docs/derivatives/portfolio-margin/account/Get-UM-Current-Position-Mode + https://developers.binance.com/docs/derivatives/portfolio-margin/account/Get-CM-Current-Position-Mode + + :param bool hedged: set to True to use dualSidePosition + :param str symbol: not used by binance setPositionMode() + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.portfolioMargin]: set to True if you would like to set the position mode for a portfolio margin account + :param str [params.subType]: "linear" or "inverse" + :returns dict: response from the exchange + """ + market = None + if symbol is not None: + market = self.market(symbol) + type = None + type, params = self.handle_market_type_and_params('setPositionMode', market, params) + subType = None + subType, params = self.handle_sub_type_and_params('setPositionMode', market, params) + isPortfolioMargin = None + isPortfolioMargin, params = self.handle_option_and_params_2(params, 'setPositionMode', 'papi', 'portfolioMargin', False) + dualSidePosition = None + if hedged: + dualSidePosition = 'true' + else: + dualSidePosition = 'false' + request: dict = { + 'dualSidePosition': dualSidePosition, + } + response = None + if self.is_inverse(type, subType): + if isPortfolioMargin: + response = await self.papiPostCmPositionSideDual(self.extend(request, params)) + else: + response = await self.dapiPrivatePostPositionSideDual(self.extend(request, params)) + elif self.is_linear(type, subType): + if isPortfolioMargin: + response = await self.papiPostUmPositionSideDual(self.extend(request, params)) + else: + response = await self.fapiPrivatePostPositionSideDual(self.extend(request, params)) + else: + raise BadRequest(self.id + ' setPositionMode() supports linear and inverse contracts only') + # + # { + # "code": 200, + # "msg": "success" + # } + # + return response + + async def fetch_leverages(self, symbols: Strings = None, params={}) -> Leverages: + """ + fetch the set leverage for all markets + + https://developers.binance.com/docs/derivatives/usds-margined-futures/account/rest-api/Account-Information-V2 + https://developers.binance.com/docs/derivatives/coin-margined-futures/account/rest-api/Account-Information + https://developers.binance.com/docs/derivatives/portfolio-margin/account/Get-UM-Account-Detail + https://developers.binance.com/docs/derivatives/portfolio-margin/account/Get-CM-Account-Detail + https://developers.binance.com/docs/derivatives/usds-margined-futures/account/rest-api/Symbol-Config + + :param str[] [symbols]: a list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.subType]: "linear" or "inverse" + :returns dict: a list of `leverage structures ` + """ + await self.load_markets() + await self.load_leverage_brackets(False, params) + type = None + type, params = self.handle_market_type_and_params('fetchLeverages', None, params) + subType = None + subType, params = self.handle_sub_type_and_params('fetchLeverages', None, params, 'linear') + isPortfolioMargin = None + isPortfolioMargin, params = self.handle_option_and_params_2(params, 'fetchLeverages', 'papi', 'portfolioMargin', False) + response = None + if self.is_linear(type, subType): + if isPortfolioMargin: + response = await self.papiGetUmAccount(params) + else: + response = await self.fapiPrivateGetSymbolConfig(params) + elif self.is_inverse(type, subType): + if isPortfolioMargin: + response = await self.papiGetCmAccount(params) + else: + response = await self.dapiPrivateGetAccount(params) + else: + raise NotSupported(self.id + ' fetchLeverages() supports linear and inverse contracts only') + leverages = self.safe_list(response, 'positions', []) + if isinstance(response, list): + leverages = response + return self.parse_leverages(leverages, symbols, 'symbol') + + def parse_leverage(self, leverage: dict, market: Market = None) -> Leverage: + marketId = self.safe_string(leverage, 'symbol') + marginModeRaw = self.safe_bool(leverage, 'isolated') + marginMode = None + if marginModeRaw is not None: + marginMode = 'isolated' if marginModeRaw else 'cross' + marginTypeRaw = self.safe_string_lower(leverage, 'marginType') + if marginTypeRaw is not None: + marginMode = 'cross' if (marginTypeRaw == 'crossed') else 'isolated' + side = self.safe_string_lower(leverage, 'positionSide') + longLeverage = None + shortLeverage = None + leverageValue = self.safe_integer(leverage, 'leverage') + if (side is None) or (side == 'both'): + longLeverage = leverageValue + shortLeverage = leverageValue + elif side == 'long': + longLeverage = leverageValue + elif side == 'short': + shortLeverage = leverageValue + return { + 'info': leverage, + 'symbol': self.safe_symbol(marketId, market), + 'marginMode': marginMode, + 'longLeverage': longLeverage, + 'shortLeverage': shortLeverage, + } + + async def fetch_settlement_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical settlement records + + https://developers.binance.com/docs/derivatives/option/market-data/Historical-Exercise-Records + + :param str symbol: unified market symbol of the settlement history + :param int [since]: timestamp in ms + :param int [limit]: number of records, default 100, max 100 + :param dict [params]: exchange specific params + :returns dict[]: a list of `settlement history objects ` + """ + await self.load_markets() + market = None if (symbol is None) else self.market(symbol) + type = None + type, params = self.handle_market_type_and_params('fetchSettlementHistory', market, params) + if type != 'option': + raise NotSupported(self.id + ' fetchSettlementHistory() supports option markets only') + request: dict = {} + if symbol is not None: + symbol = market['symbol'] + request['underlying'] = market['baseId'] + market['quoteId'] + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + response = await self.eapiPublicGetExerciseHistory(self.extend(request, params)) + # + # [ + # { + # "symbol": "ETH-230223-1900-P", + # "strikePrice": "1900", + # "realStrikePrice": "1665.5897334", + # "expiryDate": 1677139200000, + # "strikeResult": "REALISTIC_VALUE_STRICKEN" + # } + # ] + # + settlements = self.parse_settlements(response, market) + sorted = self.sort_by(settlements, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, symbol, since, limit) + + async def fetch_my_settlement_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical settlement records of the user + + https://developers.binance.com/docs/derivatives/option/trade/User-Exercise-Record + + :param str symbol: unified market symbol of the settlement history + :param int [since]: timestamp in ms + :param int [limit]: number of records + :param dict [params]: exchange specific params + :returns dict[]: a list of [settlement history objects] + """ + await self.load_markets() + market = None if (symbol is None) else self.market(symbol) + type = None + type, params = self.handle_market_type_and_params('fetchMySettlementHistory', market, params) + if type != 'option': + raise NotSupported(self.id + ' fetchMySettlementHistory() supports option markets only') + request: dict = {} + if symbol is not None: + request['symbol'] = market['id'] + symbol = market['symbol'] + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + response = await self.eapiPrivateGetExerciseRecord(self.extend(request, params)) + # + # [ + # { + # "id": "1125899906842897036", + # "currency": "USDT", + # "symbol": "BTC-230728-30000-C", + # "exercisePrice": "30000.00000000", + # "markPrice": "29160.71284993", + # "quantity": "1.00000000", + # "amount": "0.00000000", + # "fee": "0.00000000", + # "createDate": 1690531200000, + # "priceScale": 0, + # "quantityScale": 2, + # "optionSide": "CALL", + # "positionSide": "LONG", + # "quoteAsset": "USDT" + # } + # ] + # + settlements = self.parse_settlements(response, market) + sorted = self.sort_by(settlements, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, symbol, since, limit) + + def parse_settlement(self, settlement, market): + # + # fetchSettlementHistory + # + # { + # "symbol": "ETH-230223-1900-P", + # "strikePrice": "1900", + # "realStrikePrice": "1665.5897334", + # "expiryDate": 1677139200000, + # "strikeResult": "REALISTIC_VALUE_STRICKEN" + # } + # + # fetchMySettlementHistory + # + # { + # "id": "1125899906842897036", + # "currency": "USDT", + # "symbol": "BTC-230728-30000-C", + # "exercisePrice": "30000.00000000", + # "markPrice": "29160.71284993", + # "quantity": "1.00000000", + # "amount": "0.00000000", + # "fee": "0.00000000", + # "createDate": 1690531200000, + # "priceScale": 0, + # "quantityScale": 2, + # "optionSide": "CALL", + # "positionSide": "LONG", + # "quoteAsset": "USDT" + # } + # + timestamp = self.safe_integer_2(settlement, 'expiryDate', 'createDate') + marketId = self.safe_string(settlement, 'symbol') + return { + 'info': settlement, + 'symbol': self.safe_symbol(marketId, market), + 'price': self.safe_number_2(settlement, 'realStrikePrice', 'exercisePrice'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + + def parse_settlements(self, settlements, market): + # + # fetchSettlementHistory + # + # [ + # { + # "symbol": "ETH-230223-1900-P", + # "strikePrice": "1900", + # "realStrikePrice": "1665.5897334", + # "expiryDate": 1677139200000, + # "strikeResult": "EXTRINSIC_VALUE_EXPIRED" + # } + # ] + # + # fetchMySettlementHistory + # + # [ + # { + # "id": "1125899906842897036", + # "currency": "USDT", + # "symbol": "BTC-230728-30000-C", + # "exercisePrice": "30000.00000000", + # "markPrice": "29160.71284993", + # "quantity": "1.00000000", + # "amount": "0.00000000", + # "fee": "0.00000000", + # "createDate": 1690531200000, + # "priceScale": 0, + # "quantityScale": 2, + # "optionSide": "CALL", + # "positionSide": "LONG", + # "quoteAsset": "USDT" + # } + # ] + # + result = [] + for i in range(0, len(settlements)): + result.append(self.parse_settlement(settlements[i], market)) + return result + + async def fetch_ledger_entry(self, id: str, code: Str = None, params={}) -> LedgerEntry: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + + https://developers.binance.com/docs/derivatives/option/account/Account-Funding-Flow + + :param str id: the identification number of the ledger entry + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ledger structure ` + """ + await self.load_markets() + type = None + type, params = self.handle_market_type_and_params('fetchLedgerEntry', None, params) + if type != 'option': + raise BadRequest(self.id + ' fetchLedgerEntry() can only be used for type option') + self.check_required_argument('fetchLedgerEntry', code, 'code') + currency = self.currency(code) + request: dict = { + 'recordId': id, + 'currency': currency['id'], + } + response = await self.eapiPrivateGetBill(self.extend(request, params)) + # + # [ + # { + # "id": "1125899906845701870", + # "asset": "USDT", + # "amount": "-0.16518203", + # "type": "FEE", + # "createDate": 1676621042489 + # } + # ] + # + first = self.safe_dict(response, 0, response) + return self.parse_ledger_entry(first, currency) + + async def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + + https://developers.binance.com/docs/derivatives/option/account/Account-Funding-Flow + https://developers.binance.com/docs/derivatives/usds-margined-futures/account/rest-api/Get-Income-History + https://developers.binance.com/docs/derivatives/coin-margined-futures/account/rest-api/Get-Income-History + https://developers.binance.com/docs/derivatives/portfolio-margin/account/Get-UM-Income-History + https://developers.binance.com/docs/derivatives/portfolio-margin/account/Get-CM-Income-History + + :param str [code]: unified currency code + :param int [since]: timestamp in ms of the earliest ledger entry + :param int [limit]: max number of ledger entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest ledger entry + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param boolean [params.portfolioMargin]: set to True if you would like to fetch the ledger for a portfolio margin account + :param str [params.subType]: "linear" or "inverse" + :returns dict: a `ledger structure ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchLedger', 'paginate') + if paginate: + return await self.fetch_paginated_call_dynamic('fetchLedger', code, since, limit, params, None, False) + type = None + subType = None + currency = None + if code is not None: + currency = self.currency(code) + request: dict = {} + type, params = self.handle_market_type_and_params('fetchLedger', None, params) + subType, params = self.handle_sub_type_and_params('fetchLedger', None, params) + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + until = self.safe_integer(params, 'until') + if until is not None: + params = self.omit(params, 'until') + request['endTime'] = until + isPortfolioMargin = None + isPortfolioMargin, params = self.handle_option_and_params_2(params, 'fetchLedger', 'papi', 'portfolioMargin', False) + response = None + if type == 'option': + self.check_required_argument('fetchLedger', code, 'code') + request['currency'] = currency['id'] + response = await self.eapiPrivateGetBill(self.extend(request, params)) + elif self.is_linear(type, subType): + if isPortfolioMargin: + response = await self.papiGetUmIncome(self.extend(request, params)) + else: + response = await self.fapiPrivateGetIncome(self.extend(request, params)) + elif self.is_inverse(type, subType): + if isPortfolioMargin: + response = await self.papiGetCmIncome(self.extend(request, params)) + else: + response = await self.dapiPrivateGetIncome(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchLedger() supports contract wallets only') + # + # options(eapi) + # + # [ + # { + # "id": "1125899906845701870", + # "asset": "USDT", + # "amount": "-0.16518203", + # "type": "FEE", + # "createDate": 1676621042489 + # } + # ] + # + # futures(fapi, dapi, papi) + # + # [ + # { + # "symbol": "", + # "incomeType": "TRANSFER", + # "income": "10.00000000", + # "asset": "USDT", + # "time": 1677645250000, + # "info": "TRANSFER", + # "tranId": 131001573082, + # "tradeId": "" + # } + # ] + # + return self.parse_ledger(response, currency, since, limit) + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + # + # options(eapi) + # + # { + # "id": "1125899906845701870", + # "asset": "USDT", + # "amount": "-0.16518203", + # "type": "FEE", + # "createDate": 167662104241 + # } + # + # futures(fapi, dapi, papi) + # + # { + # "symbol": "", + # "incomeType": "TRANSFER", + # "income": "10.00000000", + # "asset": "USDT", + # "time": 1677645250000, + # "info": "TRANSFER", + # "tranId": 131001573082, + # "tradeId": "" + # } + # + amount = self.safe_string_2(item, 'amount', 'income') + direction = None + if Precise.string_le(amount, '0'): + direction = 'out' + amount = Precise.string_mul('-1', amount) + else: + direction = 'in' + currencyId = self.safe_string(item, 'asset') + code = self.safe_currency_code(currencyId, currency) + currency = self.safe_currency(currencyId, currency) + timestamp = self.safe_integer_2(item, 'createDate', 'time') + type = self.safe_string_2(item, 'type', 'incomeType') + return self.safe_ledger_entry({ + 'info': item, + 'id': self.safe_string_2(item, 'id', 'tranId'), + 'direction': direction, + 'account': None, + 'referenceAccount': None, + 'referenceId': self.safe_string(item, 'tradeId'), + 'type': self.parse_ledger_entry_type(type), + 'currency': code, + 'amount': self.parse_number(amount), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'before': None, + 'after': None, + 'status': None, + 'fee': None, + }, currency) + + def parse_ledger_entry_type(self, type): + ledgerType: dict = { + 'FEE': 'fee', + 'FUNDING_FEE': 'fee', + 'OPTIONS_PREMIUM_FEE': 'fee', + 'POSITION_LIMIT_INCREASE_FEE': 'fee', + 'CONTRACT': 'trade', + 'REALIZED_PNL': 'trade', + 'TRANSFER': 'transfer', + 'CROSS_COLLATERAL_TRANSFER': 'transfer', + 'INTERNAL_TRANSFER': 'transfer', + 'COIN_SWAP_DEPOSIT': 'deposit', + 'COIN_SWAP_WITHDRAW': 'withdrawal', + 'OPTIONS_SETTLE_PROFIT': 'settlement', + 'DELIVERED_SETTELMENT': 'settlement', + 'WELCOME_BONUS': 'cashback', + 'CONTEST_REWARD': 'cashback', + 'COMMISSION_REBATE': 'rebate', + 'API_REBATE': 'rebate', + 'REFERRAL_KICKBACK': 'referral', + 'COMMISSION': 'commission', + } + return self.safe_string(ledgerType, type, type) + + def get_network_code_by_network_url(self, currencyCode: str, depositUrl: Str = None) -> Str: + # depositUrl is like : https://bscscan.com/address/0xEF238AB229342849.. + if depositUrl is None: + return None + networkCode = None + currency = self.currency(currencyCode) + networks = self.safe_dict(currency, 'networks', {}) + networkCodes = list(networks.keys()) + for i in range(0, len(networkCodes)): + currentNetworkCode = networkCodes[i] + info = self.safe_dict(networks[currentNetworkCode], 'info', {}) + siteUrl = self.safe_string(info, 'contractAddressUrl') + # check if url matches the field's value + if siteUrl is not None and depositUrl.startswith(self.get_base_domain_from_url(siteUrl)): + networkCode = currentNetworkCode + return networkCode + + def get_base_domain_from_url(self, url: Str) -> Str: + if url is None: + return None + urlParts = url.split('/') + scheme = self.safe_string(urlParts, 0) + if scheme is None: + return None + domain = self.safe_string(urlParts, 2) + if domain is None: + return None + return scheme + '//' + domain + '/' + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + urls = self.urls + if not (api in urls['api']): + raise NotSupported(self.id + ' does not have a testnet/sandbox URL for ' + api + ' endpoints') + url = self.urls['api'][api] + url += '/' + path + if path == 'historicalTrades': + if self.apiKey: + headers = { + 'X-MBX-APIKEY': self.apiKey, + } + else: + raise AuthenticationError(self.id + ' historicalTrades endpoint requires `apiKey` credential') + userDataStream = (path == 'userDataStream') or (path == 'listenKey') + if userDataStream: + if self.apiKey: + # v1 special case for userDataStream + headers = { + 'X-MBX-APIKEY': self.apiKey, + 'Content-Type': 'application/x-www-form-urlencoded', + } + if method != 'GET': + body = self.urlencode(params) + else: + raise AuthenticationError(self.id + ' userDataStream endpoint requires `apiKey` credential') + elif (api == 'private') or (api == 'eapiPrivate') or (api == 'sapi' and path != 'system/status') or (api == 'sapiV2') or (api == 'sapiV3') or (api == 'sapiV4') or (api == 'dapiPrivate') or (api == 'dapiPrivateV2') or (api == 'fapiPrivate') or (api == 'fapiPrivateV2') or (api == 'fapiPrivateV3') or (api == 'papiV2' or api == 'papi' and path != 'ping'): + self.check_required_credentials() + if (url.find('testnet.binancefuture.com') > -1) and self.isSandboxModeEnabled and (not self.safe_bool(self.options, 'disableFuturesSandboxWarning')): + raise NotSupported(self.id + ' testnet/sandbox mode is not supported for futures anymore, please check the deprecation announcement https://t.me/ccxt_announcements/92 and consider using the demo trading instead.') + if method == 'POST' and ((path == 'order') or (path == 'sor/order')): + # inject in implicit API calls + newClientOrderId = self.safe_string(params, 'newClientOrderId') + if newClientOrderId is None: + isSpotOrMargin = (api.find('sapi') > -1 or api == 'private') + marketType = 'spot' if isSpotOrMargin else 'future' + defaultId = 'x-xcKtGhcu' if (not isSpotOrMargin) else 'x-TKT5PX2F' + broker = self.safe_dict(self.options, 'broker', {}) + brokerId = self.safe_string(broker, marketType, defaultId) + params['newClientOrderId'] = brokerId + self.uuid22() + query = None + # handle batchOrders + if (path == 'batchOrders') and ((method == 'POST') or (method == 'PUT')): + batchOrders = self.safe_list(params, 'batchOrders') + checkedBatchOrders = batchOrders + if method == 'POST' and api == 'fapiPrivate': + # check broker id if batchOrders are called with fapiPrivatePostBatchOrders + checkedBatchOrders = [] + for i in range(0, len(batchOrders)): + batchOrder = batchOrders[i] + newClientOrderId = self.safe_string(batchOrder, 'newClientOrderId') + if newClientOrderId is None: + defaultId = 'x-xcKtGhcu' # batchOrders can not be spot or margin + broker = self.safe_dict(self.options, 'broker', {}) + brokerId = self.safe_string(broker, 'future', defaultId) + newClientOrderId = brokerId + self.uuid22() + batchOrder['newClientOrderId'] = newClientOrderId + checkedBatchOrders.append(batchOrder) + queryBatch = (self.json(checkedBatchOrders)) + params['batchOrders'] = queryBatch + defaultRecvWindow = self.safe_integer(self.options, 'recvWindow') + extendedParams = self.extend({ + 'timestamp': self.nonce(), + }, params) + if defaultRecvWindow is not None: + extendedParams['recvWindow'] = defaultRecvWindow + recvWindow = self.safe_integer(params, 'recvWindow') + if recvWindow is not None: + extendedParams['recvWindow'] = recvWindow + if (api == 'sapi') and (path == 'asset/dust'): + query = self.urlencode_with_array_repeat(extendedParams) + elif (path == 'batchOrders') or (path.find('sub-account') >= 0) or (path == 'capital/withdraw/apply') or (path.find('staking') >= 0) or (path.find('simple-earn') >= 0): + if (method == 'DELETE') and (path == 'batchOrders'): + orderidlist = self.safe_list(extendedParams, 'orderidlist', []) + origclientorderidlist = self.safe_list_2(extendedParams, 'origclientorderidlist', 'origClientOrderIdList', []) + extendedParams = self.omit(extendedParams, ['orderidlist', 'origclientorderidlist', 'origClientOrderIdList']) + if 'symbol' in extendedParams: + extendedParams['symbol'] = self.encode_uri_component(extendedParams['symbol']) + query = self.rawencode(extendedParams) + orderidlistLength = len(orderidlist) + origclientorderidlistLength = len(origclientorderidlist) + if orderidlistLength > 0: + query = query + '&' + 'orderidlist=%5B' + '%2C'.join(orderidlist) + '%5D' + if origclientorderidlistLength > 0: + # wrap clientOrderids around "" + newClientOrderIds = [] + for i in range(0, origclientorderidlistLength): + newClientOrderIds.append('%22' + origclientorderidlist[i] + '%22') + query = query + '&' + 'origclientorderidlist=%5B' + '%2C'.join(newClientOrderIds) + '%5D' + else: + query = self.rawencode(extendedParams) + else: + query = self.urlencode(extendedParams) + signature = None + if self.secret.find('PRIVATE KEY') > -1: + if len(self.secret) > 120: + signature = self.encode_uri_component(self.rsa(query, self.secret, 'sha256')) + else: + signature = self.encode_uri_component(self.eddsa(self.encode(query), self.secret, 'ed25519')) + else: + signature = self.hmac(self.encode(query), self.encode(self.secret), hashlib.sha256) + query += '&' + 'signature=' + signature + headers = { + 'X-MBX-APIKEY': self.apiKey, + } + if (method == 'GET') or (method == 'DELETE'): + url += '?' + query + else: + body = query + headers['Content-Type'] = 'application/x-www-form-urlencoded' + else: + if params: + url += '?' + self.urlencode(params) + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def get_exceptions_by_url(self, url: str, exactOrBroad: str): + marketType = None + hostname = self.hostname if (self.hostname is not None) else 'binance.com' + if url.startswith('https://api.' + hostname + '/') or url.startswith('https://testnet.binance.vision'): + marketType = 'spot' + elif url.startswith('https://dapi.' + hostname + '/') or url.startswith('https://testnet.binancefuture.com/dapi'): + marketType = 'inverse' + elif url.startswith('https://fapi.' + hostname + '/') or url.startswith('https://testnet.binancefuture.com/fapi'): + marketType = 'linear' + elif url.startswith('https://eapi.' + hostname + '/'): + marketType = 'option' + elif url.startswith('https://papi.' + hostname + '/'): + marketType = 'portfolioMargin' + if marketType is not None: + exceptionsForMarketType = self.safe_dict(self.exceptions, marketType, {}) + return self.safe_dict(exceptionsForMarketType, exactOrBroad, {}) + return {} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if (code == 418) or (code == 429): + raise DDoSProtection(self.id + ' ' + str(code) + ' ' + reason + ' ' + body) + # error response in a form: {"code": -1013, "msg": "Invalid quantity."} + # following block cointains legacy checks against message patterns in "msg" property + # will switch "code" checks eventually, when we know all of them + if code >= 400: + if body.find('Price * QTY is zero or less') >= 0: + raise InvalidOrder(self.id + ' order cost = amount * price is zero or less ' + body) + if body.find('LOT_SIZE') >= 0: + raise InvalidOrder(self.id + ' order amount should be evenly divisible by lot size ' + body) + if body.find('PRICE_FILTER') >= 0: + raise InvalidOrder(self.id + ' order price is invalid, i.e. exceeds allowed price precision, exceeds min price or max price limits or is invalid value in general, use self.price_to_precision(symbol, amount) ' + body) + if response is None: + return None # fallback to default error handler + # response in format {'msg': 'The coin does not exist.', 'success': True/false} + success = self.safe_bool(response, 'success', True) + if not success: + messageNew = self.safe_string(response, 'msg') + parsedMessage = None + if messageNew is not None: + try: + parsedMessage = json.loads(messageNew) + except Exception as e: + # do nothing + parsedMessage = None + if parsedMessage is not None: + response = parsedMessage + message = self.safe_string(response, 'msg') + if message is not None: + self.throw_exactly_matched_exception(self.get_exceptions_by_url(url, 'exact'), message, self.id + ' ' + message) + self.throw_exactly_matched_exception(self.exceptions['exact'], message, self.id + ' ' + message) + self.throw_broadly_matched_exception(self.get_exceptions_by_url(url, 'broad'), message, self.id + ' ' + message) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, self.id + ' ' + message) + # checks against error codes + error = self.safe_string(response, 'code') + if error is not None: + # https://github.com/ccxt/ccxt/issues/6501 + # https://github.com/ccxt/ccxt/issues/7742 + if (error == '200') or Precise.string_equals(error, '0'): + return None + # a workaround for {"code":-2015,"msg":"Invalid API-key, IP, or permissions for action."} + # despite that their message is very confusing, it is raised by Binance + # on a temporary ban, the API key is valid, but disabled for a while + if (error == '-2015') and self.options['hasAlreadyAuthenticatedSuccessfully']: + raise DDoSProtection(self.id + ' ' + body) + feedback = self.id + ' ' + body + if message == 'No need to change margin type.': + # not an error + # https://github.com/ccxt/ccxt/issues/11268 + # https://github.com/ccxt/ccxt/pull/11624 + # POST https://fapi.binance.com/fapi/v1/marginType 400 Bad Request + # binanceusdm {"code":-4046,"msg":"No need to change margin type."} + raise MarginModeAlreadySet(feedback) + self.throw_exactly_matched_exception(self.get_exceptions_by_url(url, 'exact'), error, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], error, feedback) + raise ExchangeError(feedback) + if not success: + raise ExchangeError(self.id + ' ' + body) + if isinstance(response, list): + # cancelOrders returns an array like self: [{"code":-2011,"msg":"Unknown order sent."}] + arrayLength = len(response) + if arrayLength == 1: # when there's a single error we can throw, otherwise we have a partial success + element = response[0] + errorCode = self.safe_string(element, 'code') + if errorCode is not None: + self.throw_exactly_matched_exception(self.get_exceptions_by_url(url, 'exact'), errorCode, self.id + ' ' + body) + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, self.id + ' ' + body) + return None + + def calculate_rate_limiter_cost(self, api, method, path, params, config={}): + if ('noCoin' in config) and not ('coin' in params): + return config['noCoin'] + elif ('noSymbol' in config) and not ('symbol' in params): + return config['noSymbol'] + elif ('noPoolId' in config) and not ('poolId' in params): + return config['noPoolId'] + elif ('byLimit' in config) and ('limit' in params): + limit = params['limit'] + byLimit = config['byLimit'] + for i in range(0, len(byLimit)): + entry = byLimit[i] + if limit <= entry[0]: + return entry[1] + return self.safe_value(config, 'cost', 1) + + async def request(self, path, api='public', method='GET', params={}, headers=None, body=None, config={}): + response = await self.fetch2(path, api, method, params, headers, body, config) + # a workaround for {"code":-2015,"msg":"Invalid API-key, IP, or permissions for action."} + if api == 'private': + self.options['hasAlreadyAuthenticatedSuccessfully'] = True + return response + + async def modify_margin_helper(self, symbol: str, amount, addOrReduce, params={}): + # used to modify isolated positions + defaultType = self.safe_string(self.options, 'defaultType', 'future') + if defaultType == 'spot': + defaultType = 'future' + type = self.safe_string(params, 'type', defaultType) + if (type == 'margin') or (type == 'spot'): + raise NotSupported(self.id + ' add / reduce margin only supported with type future or delivery') + await self.load_markets() + market = self.market(symbol) + amount = self.amount_to_precision(symbol, amount) + request: dict = { + 'type': addOrReduce, + 'symbol': market['id'], + 'amount': amount, + } + response = None + code = None + if market['linear']: + code = market['quote'] + response = await self.fapiPrivatePostPositionMargin(self.extend(request, params)) + else: + code = market['base'] + response = await self.dapiPrivatePostPositionMargin(self.extend(request, params)) + # + # { + # "code": 200, + # "msg": "Successfully modify position margin.", + # "amount": 0.001, + # "type": 1 + # } + # + return self.extend(self.parse_margin_modification(response, market), { + 'code': code, + }) + + def parse_margin_modification(self, data: dict, market: Market = None) -> MarginModification: + # + # add/reduce margin + # + # { + # "code": 200, + # "msg": "Successfully modify position margin.", + # "amount": 0.001, + # "type": 1 + # } + # + # fetchMarginAdjustmentHistory + # + # { + # symbol: "XRPUSDT", + # type: "1", + # deltaType: "TRADE", + # amount: "2.57148240", + # asset: "USDT", + # time: "1711046271555", + # positionSide: "BOTH", + # clientTranId: "" + # } + # + rawType = self.safe_integer(data, 'type') + errorCode = self.safe_string(data, 'code') + marketId = self.safe_string(data, 'symbol') + timestamp = self.safe_integer(data, 'time') + market = self.safe_market(marketId, market, None, 'swap') + noErrorCode = errorCode is None + success = errorCode == '200' + return { + 'info': data, + 'symbol': market['symbol'], + 'type': 'add' if (rawType == 1) else 'reduce', + 'marginMode': 'isolated', + 'amount': self.safe_number(data, 'amount'), + 'code': self.safe_string(data, 'asset'), + 'total': None, + 'status': 'ok' if (success or noErrorCode) else 'failed', + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + + async def reduce_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + remove margin from a position + + https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Modify-Isolated-Position-Margin + https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/rest-api/Modify-Isolated-Position-Margin + + :param str symbol: unified market symbol + :param float amount: the amount of margin to remove + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin structure ` + """ + return await self.modify_margin_helper(symbol, amount, 2, params) + + async def add_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + add margin + + https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Modify-Isolated-Position-Margin + https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/rest-api/Modify-Isolated-Position-Margin + + :param str symbol: unified market symbol + :param float amount: amount of margin to add + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin structure ` + """ + return await self.modify_margin_helper(symbol, amount, 1, params) + + async def fetch_cross_borrow_rate(self, code: str, params={}) -> CrossBorrowRate: + """ + fetch the rate of interest to borrow a currency for margin trading + + https://developers.binance.com/docs/margin_trading/borrow-and-repay/Query-Margin-Interest-Rate-History + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `borrow rate structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'asset': currency['id'], + # 'vipLevel': self.safe_integer(params, 'vipLevel'), + } + response = await self.sapiGetMarginInterestRateHistory(self.extend(request, params)) + # + # [ + # { + # "asset": "USDT", + # "timestamp": 1638230400000, + # "dailyInterestRate": "0.0006", + # "vipLevel": 0 + # }, + # ] + # + rate = self.safe_dict(response, 0) + return self.parse_borrow_rate(rate) + + async def fetch_isolated_borrow_rate(self, symbol: str, params={}) -> IsolatedBorrowRate: + """ + fetch the rate of interest to borrow a currency for margin trading + + https://developers.binance.com/docs/margin_trading/account/Query-Isolated-Margin-Fee-Data + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + + EXCHANGE SPECIFIC PARAMETERS + :param dict [params.vipLevel]: user's current specific margin data will be returned if viplevel is omitted + :returns dict: an `isolated borrow rate structure ` + """ + request: dict = { + 'symbol': symbol, + } + borrowRates = await self.fetch_isolated_borrow_rates(self.extend(request, params)) + return self.safe_dict(borrowRates, symbol) + + async def fetch_isolated_borrow_rates(self, params={}) -> IsolatedBorrowRates: + """ + fetch the borrow interest rates of all currencies + + https://developers.binance.com/docs/margin_trading/account/Query-Isolated-Margin-Fee-Data + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param dict [params.symbol]: unified market symbol + + EXCHANGE SPECIFIC PARAMETERS + :param dict [params.vipLevel]: user's current specific margin data will be returned if viplevel is omitted + :returns dict: a `borrow rate structure ` + """ + await self.load_markets() + request: dict = {} + symbol = self.safe_string(params, 'symbol') + params = self.omit(params, 'symbol') + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + response = await self.sapiGetMarginIsolatedMarginData(self.extend(request, params)) + # + # [ + # { + # "vipLevel": 0, + # "symbol": "BTCUSDT", + # "leverage": "10", + # "data": [ + # { + # "coin": "BTC", + # "dailyInterest": "0.00026125", + # "borrowLimit": "270" + # }, + # { + # "coin": "USDT", + # "dailyInterest": "0.000475", + # "borrowLimit": "2100000" + # } + # ] + # } + # ] + # + return self.parse_isolated_borrow_rates(response) + + async def fetch_borrow_rate_history(self, code: str, since: Int = None, limit: Int = None, params={}): + """ + retrieves a history of a currencies borrow interest rate at specific time slots + + https://developers.binance.com/docs/margin_trading/borrow-and-repay/Query-Margin-Interest-Rate-History + + :param str code: unified currency code + :param int [since]: timestamp for the earliest borrow rate + :param int [limit]: the maximum number of `borrow rate structures ` to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of `borrow rate structures ` + """ + await self.load_markets() + if limit is None: + limit = 93 + elif limit > 93: + # Binance API says the limit is 100, but "Illegal characters found in a parameter." is returned when limit is > 93 + raise BadRequest(self.id + ' fetchBorrowRateHistory() limit parameter cannot exceed 92') + currency = self.currency(code) + request: dict = { + 'asset': currency['id'], + 'limit': limit, + } + if since is not None: + request['startTime'] = since + endTime = self.sum(since, limit * 86400000) - 1 # required when startTime is further than 93 days in the past + now = self.milliseconds() + request['endTime'] = min(endTime, now) # cannot have an endTime later than current time + response = await self.sapiGetMarginInterestRateHistory(self.extend(request, params)) + # + # [ + # { + # "asset": "USDT", + # "timestamp": 1638230400000, + # "dailyInterestRate": "0.0006", + # "vipLevel": 0 + # }, + # ] + # + return self.parse_borrow_rate_history(response, code, since, limit) + + def parse_borrow_rate(self, info, currency: Currency = None): + # + # { + # "asset": "USDT", + # "timestamp": 1638230400000, + # "dailyInterestRate": "0.0006", + # "vipLevel": 0 + # } + # + timestamp = self.safe_integer(info, 'timestamp') + currencyId = self.safe_string(info, 'asset') + return { + 'currency': self.safe_currency_code(currencyId, currency), + 'rate': self.safe_number(info, 'dailyInterestRate'), + 'period': 86400000, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'info': info, + } + + def parse_isolated_borrow_rate(self, info: dict, market: Market = None) -> IsolatedBorrowRate: + # + # { + # "vipLevel": 0, + # "symbol": "BTCUSDT", + # "leverage": "10", + # "data": [ + # { + # "coin": "BTC", + # "dailyInterest": "0.00026125", + # "borrowLimit": "270" + # }, + # { + # "coin": "USDT", + # "dailyInterest": "0.000475", + # "borrowLimit": "2100000" + # } + # ] + # } + # + marketId = self.safe_string(info, 'symbol') + market = self.safe_market(marketId, market, None, 'spot') + data = self.safe_list(info, 'data') + baseInfo = self.safe_dict(data, 0) + quoteInfo = self.safe_dict(data, 1) + return { + 'info': info, + 'symbol': self.safe_string(market, 'symbol'), + 'base': self.safe_string(baseInfo, 'coin'), + 'baseRate': self.safe_number(baseInfo, 'dailyInterest'), + 'quote': self.safe_string(quoteInfo, 'coin'), + 'quoteRate': self.safe_number(quoteInfo, 'dailyInterest'), + 'period': 86400000, + 'timestamp': None, + 'datetime': None, + } + + async def create_gift_code(self, code: str, amount, params={}): + """ + create gift code + + https://developers.binance.com/docs/gift_card/market-data/Create-a-single-token-gift-card + + :param str code: gift code + :param float amount: amount of currency for the gift + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: The gift code id, code, currency and amount + """ + await self.load_markets() + currency = self.currency(code) + # ensure you have enough token in your funding account before calling self code + request: dict = { + 'token': currency['id'], + 'amount': amount, + } + response = await self.sapiPostGiftcardCreateCode(self.extend(request, params)) + # + # { + # "code": "000000", + # "message": "success", + # "data": {referenceNo: "0033002404219823", code: "AP6EXTLKNHM6CEX7"}, + # "success": True + # } + # + data = self.safe_dict(response, 'data') + giftcardCode = self.safe_string(data, 'code') + id = self.safe_string(data, 'referenceNo') + return { + 'info': response, + 'id': id, + 'code': giftcardCode, + 'currency': code, + 'amount': amount, + } + + async def redeem_gift_code(self, giftcardCode, params={}): + """ + redeem gift code + + https://developers.binance.com/docs/gift_card/market-data/Redeem-a-Binance-Gift-Card + + :param str giftcardCode: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + request: dict = { + 'code': giftcardCode, + } + response = await self.sapiPostGiftcardRedeemCode(self.extend(request, params)) + # + # { + # "code": "000000", + # "message": "success", + # "data": { + # "referenceNo": "0033002404219823", + # "identityNo": "10316431732801474560" + # }, + # "success": True + # } + # + return response + + async def verify_gift_code(self, id: str, params={}): + """ + verify gift code + + https://developers.binance.com/docs/gift_card/market-data/Verify-Binance-Gift-Card-by-Gift-Card-Number + + :param str id: reference number id + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + request: dict = { + 'referenceNo': id, + } + response = await self.sapiGetGiftcardVerify(self.extend(request, params)) + # + # { + # "code": "000000", + # "message": "success", + # "data": {valid: True}, + # "success": True + # } + # + return response + + async def fetch_borrow_interest(self, code: Str = None, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[BorrowInterest]: + """ + fetch the interest owed by the user for borrowing currency for margin trading + + https://developers.binance.com/docs/margin_trading/borrow-and-repay/Get-Interest-History + https://developers.binance.com/docs/derivatives/portfolio-margin/account/Get-Margin-BorrowLoan-Interest-History + + :param str [code]: unified currency code + :param str [symbol]: unified market symbol when fetch interest in isolated markets + :param int [since]: the earliest time in ms to fetch borrrow interest for + :param int [limit]: the maximum number of structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.portfolioMargin]: set to True if you would like to fetch the borrow interest in a portfolio margin account + :returns dict[]: a list of `borrow interest structures ` + """ + await self.load_markets() + isPortfolioMargin = None + isPortfolioMargin, params = self.handle_option_and_params_2(params, 'fetchBorrowInterest', 'papi', 'portfolioMargin', False) + request: dict = {} + market = None + if code is not None: + currency = self.currency(code) + request['asset'] = currency['id'] + if since is not None: + request['startTime'] = since + if limit is not None: + request['size'] = limit + request, params = self.handle_until_option('endTime', request, params) + response = None + if isPortfolioMargin: + response = await self.papiGetMarginMarginInterestHistory(self.extend(request, params)) + else: + if symbol is not None: + market = self.market(symbol) + request['isolatedSymbol'] = market['id'] + response = await self.sapiGetMarginInterestHistory(self.extend(request, params)) + # + # spot margin + # + # { + # "rows":[ + # { + # "isolatedSymbol": "BNBUSDT", # isolated symbol, will not be returned for crossed margin + # "asset": "BNB", + # "interest": "0.02414667", + # "interestAccuredTime": 1566813600000, + # "interestRate": "0.01600000", + # "principal": "36.22000000", + # "type": "ON_BORROW" + # } + # ], + # "total": 1 + # } + # + # spot margin portfolio margin + # + # { + # "total": 49, + # "rows": [ + # { + # "txId": 1656187724899910076, + # "interestAccuredTime": 1707541200000, + # "asset": "USDT", + # "rawAsset": "USDT", + # "principal": "0.00011146", + # "interest": "0.00000001", + # "interestRate": "0.00089489", + # "type": "PERIODIC" + # }, + # ] + # } + # + rows = self.safe_list(response, 'rows') + interest = self.parse_borrow_interests(rows, market) + return self.filter_by_currency_since_limit(interest, code, since, limit) + + def parse_borrow_interest(self, info: dict, market: Market = None) -> BorrowInterest: + symbol = self.safe_string(info, 'isolatedSymbol') + timestamp = self.safe_integer(info, 'interestAccuredTime') + marginMode = 'cross' if (symbol is None) else 'isolated' + return { + 'info': info, + 'symbol': symbol, + 'currency': self.safe_currency_code(self.safe_string(info, 'asset')), + 'interest': self.safe_number(info, 'interest'), + 'interestRate': self.safe_number(info, 'interestRate'), + 'amountBorrowed': self.safe_number(info, 'principal'), + 'marginMode': marginMode, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + + async def repay_cross_margin(self, code: str, amount, params={}): + """ + repay borrowed margin and interest + + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Margin-Account-Repay + https://developers.binance.com/docs/margin_trading/borrow-and-repay/Margin-Account-Borrow-Repay + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Margin-Account-Repay-Debt + + :param str code: unified currency code of the currency to repay + :param float amount: the amount to repay + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.portfolioMargin]: set to True if you would like to repay margin in a portfolio margin account + :param str [params.repayCrossMarginMethod]: *portfolio margin only* 'papiPostRepayLoan'(default), 'papiPostMarginRepayDebt'(alternative) + :param str [params.specifyRepayAssets]: *portfolio margin papiPostMarginRepayDebt only* specific asset list to repay debt + :returns dict: a `margin loan structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'asset': currency['id'], + 'amount': self.currency_to_precision(code, amount), + } + response = None + isPortfolioMargin = None + isPortfolioMargin, params = self.handle_option_and_params_2(params, 'repayCrossMargin', 'papi', 'portfolioMargin', False) + if isPortfolioMargin: + method = None + method, params = self.handle_option_and_params_2(params, 'repayCrossMargin', 'repayCrossMarginMethod', 'method') + if method == 'papiPostMarginRepayDebt': + response = await self.papiPostMarginRepayDebt(self.extend(request, params)) + # + # { + # "asset": "USDC", + # "amount": 10, + # "specifyRepayAssets": null, + # "updateTime": 1727170761267, + # "success": True + # } + # + else: + response = await self.papiPostRepayLoan(self.extend(request, params)) + # + # { + # "tranId": 108988250265, + # "clientTag":"" + # } + # + else: + request['isIsolated'] = 'FALSE' + request['type'] = 'REPAY' + response = await self.sapiPostMarginBorrowRepay(self.extend(request, params)) + # + # { + # "tranId": 108988250265, + # "clientTag":"" + # } + # + return self.parse_margin_loan(response, currency) + + async def repay_isolated_margin(self, symbol: str, code: str, amount, params={}): + """ + repay borrowed margin and interest + + https://developers.binance.com/docs/margin_trading/borrow-and-repay/Margin-Account-Borrow-Repay + + :param str symbol: unified market symbol, required for isolated margin + :param str code: unified currency code of the currency to repay + :param float amount: the amount to repay + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin loan structure ` + """ + await self.load_markets() + currency = self.currency(code) + market = self.market(symbol) + request: dict = { + 'asset': currency['id'], + 'amount': self.currency_to_precision(code, amount), + 'symbol': market['id'], + 'isIsolated': 'TRUE', + 'type': 'REPAY', + } + response = await self.sapiPostMarginBorrowRepay(self.extend(request, params)) + # + # { + # "tranId": 108988250265, + # "clientTag":"" + # } + # + return self.parse_margin_loan(response, currency) + + async def borrow_cross_margin(self, code: str, amount: float, params={}): + """ + create a loan to borrow margin + + https://developers.binance.com/docs/margin_trading/borrow-and-repay/Margin-Account-Borrow-Repay + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Margin-Account-Borrow + + :param str code: unified currency code of the currency to borrow + :param float amount: the amount to borrow + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.portfolioMargin]: set to True if you would like to borrow margin in a portfolio margin account + :returns dict: a `margin loan structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'asset': currency['id'], + 'amount': self.currency_to_precision(code, amount), + } + response = None + isPortfolioMargin = None + isPortfolioMargin, params = self.handle_option_and_params_2(params, 'borrowCrossMargin', 'papi', 'portfolioMargin', False) + if isPortfolioMargin: + response = await self.papiPostMarginLoan(self.extend(request, params)) + else: + request['isIsolated'] = 'FALSE' + request['type'] = 'BORROW' + response = await self.sapiPostMarginBorrowRepay(self.extend(request, params)) + # + # { + # "tranId": 108988250265, + # "clientTag":"" + # } + # + return self.parse_margin_loan(response, currency) + + async def borrow_isolated_margin(self, symbol: str, code: str, amount: float, params={}): + """ + create a loan to borrow margin + + https://developers.binance.com/docs/margin_trading/borrow-and-repay/Margin-Account-Borrow-Repay + + :param str symbol: unified market symbol, required for isolated margin + :param str code: unified currency code of the currency to borrow + :param float amount: the amount to borrow + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin loan structure ` + """ + await self.load_markets() + currency = self.currency(code) + market = self.market(symbol) + request: dict = { + 'asset': currency['id'], + 'amount': self.currency_to_precision(code, amount), + 'symbol': market['id'], + 'isIsolated': 'TRUE', + 'type': 'BORROW', + } + response = await self.sapiPostMarginBorrowRepay(self.extend(request, params)) + # + # { + # "tranId": 108988250265, + # "clientTag":"" + # } + # + return self.parse_margin_loan(response, currency) + + def parse_margin_loan(self, info, currency: Currency = None): + # + # { + # "tranId": 108988250265, + # "clientTag":"" + # } + # + # repayCrossMargin alternative endpoint + # + # { + # "asset": "USDC", + # "amount": 10, + # "specifyRepayAssets": null, + # "updateTime": 1727170761267, + # "success": True + # } + # + currencyId = self.safe_string(info, 'asset') + timestamp = self.safe_integer(info, 'updateTime') + return { + 'id': self.safe_integer(info, 'tranId'), + 'currency': self.safe_currency_code(currencyId, currency), + 'amount': self.safe_number(info, 'amount'), + 'symbol': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'info': info, + } + + async def fetch_open_interest_history(self, symbol: str, timeframe='5m', since: Int = None, limit: Int = None, params={}): + """ + Retrieves the open interest history of a currency + + https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/Open-Interest-Statistics + https://developers.binance.com/docs/derivatives/coin-margined-futures/market-data/rest-api/Open-Interest-Statistics + + :param str symbol: Unified CCXT market symbol + :param str timeframe: "5m","15m","30m","1h","2h","4h","6h","12h", or "1d" + :param int [since]: the time(ms) of the earliest record to retrieve unix timestamp + :param int [limit]: default 30, max 500 + :param dict [params]: exchange specific parameters + :param int [params.until]: the time(ms) of the latest record to retrieve unix timestamp + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict: an array of `open interest structure ` + """ + if timeframe == '1m': + raise BadRequest(self.id + ' fetchOpenInterestHistory cannot use the 1m timeframe') + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOpenInterestHistory', 'paginate', False) + if paginate: + return await self.fetch_paginated_call_deterministic('fetchOpenInterestHistory', symbol, since, limit, timeframe, params, 500) + market = self.market(symbol) + request: dict = { + 'period': self.safe_string(self.timeframes, timeframe, timeframe), + } + if limit is not None: + request['limit'] = limit + symbolKey = 'symbol' if market['linear'] else 'pair' + request[symbolKey] = market['id'] + if market['inverse']: + request['contractType'] = self.safe_string(params, 'contractType', 'CURRENT_QUARTER') + if since is not None: + request['startTime'] = since + until = self.safe_integer(params, 'until') # unified in milliseconds + endTime = self.safe_integer(params, 'endTime', until) # exchange-specific in milliseconds + params = self.omit(params, ['endTime', 'until']) + if endTime: + request['endTime'] = endTime + elif since: + if limit is None: + limit = 30 # Exchange default + duration = self.parse_timeframe(timeframe) + request['endTime'] = self.sum(since, duration * limit * 1000) + response = None + if market['inverse']: + response = await self.dapiDataGetOpenInterestHist(self.extend(request, params)) + else: + response = await self.fapiDataGetOpenInterestHist(self.extend(request, params)) + # + # [ + # { + # "symbol":"BTCUSDT", + # "sumOpenInterest":"75375.61700000", + # "sumOpenInterestValue":"3248828883.71251440", + # "timestamp":1642179900000 + # }, + # ... + # ] + # + return self.parse_open_interests_history(response, market, since, limit) + + async def fetch_open_interest(self, symbol: str, params={}): + """ + retrieves the open interest of a contract trading pair + + https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/Open-Interest + https://developers.binance.com/docs/derivatives/coin-margined-futures/market-data/rest-api/Open-Interest + https://developers.binance.com/docs/derivatives/option/market-data/Open-Interest + + :param str symbol: unified CCXT market symbol + :param dict [params]: exchange specific parameters + :returns dict} an open interest structure{@link https://docs.ccxt.com/#/?id=open-interest-structure: + """ + await self.load_markets() + market = self.market(symbol) + request: dict = {} + if market['option']: + request['underlyingAsset'] = market['baseId'] + if market['expiry'] is None: + raise NotSupported(self.id + ' fetchOpenInterest does not support ' + symbol) + request['expiration'] = self.yymmdd(market['expiry']) + else: + request['symbol'] = market['id'] + response = None + if market['option']: + response = await self.eapiPublicGetOpenInterest(self.extend(request, params)) + elif market['inverse']: + response = await self.dapiPublicGetOpenInterest(self.extend(request, params)) + else: + response = await self.fapiPublicGetOpenInterest(self.extend(request, params)) + # + # futures(fapi) + # + # { + # "symbol": "ETHUSDT_230331", + # "openInterest": "23581.677", + # "time": 1677356872265 + # } + # + # futures(dapi) + # + # { + # "symbol": "ETHUSD_PERP", + # "pair": "ETHUSD", + # "openInterest": "26542436", + # "contractType": "PERPETUAL", + # "time": 1677360272224 + # } + # + # options(eapi) + # + # [ + # { + # "symbol": "ETH-230225-1625-C", + # "sumOpenInterest": "460.50", + # "sumOpenInterestUsd": "734957.4358092150", + # "timestamp": "1677304860000" + # } + # ] + # + if market['option']: + symbol = market['symbol'] + result = self.parse_open_interests_history(response, market) + for i in range(0, len(result)): + item = result[i] + if item['symbol'] == symbol: + return item + else: + return self.parse_open_interest(response, market) + return None + + def parse_open_interest(self, interest, market: Market = None): + timestamp = self.safe_integer_2(interest, 'timestamp', 'time') + id = self.safe_string(interest, 'symbol') + amount = self.safe_number_2(interest, 'sumOpenInterest', 'openInterest') + value = self.safe_number_2(interest, 'sumOpenInterestValue', 'sumOpenInterestUsd') + # Inverse returns the number of contracts different from the base or quote hasattr(self, volume) case + # compared with https://www.binance.com/en/futures/funding-history/quarterly/4 + return self.safe_open_interest({ + 'symbol': self.safe_symbol(id, market, None, 'contract'), + 'baseVolume': None if market['inverse'] else amount, # deprecated + 'quoteVolume': value, # deprecated + 'openInterestAmount': amount, + 'openInterestValue': value, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'info': interest, + }, market) + + async def fetch_my_liquidations(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + retrieves the users liquidated positions + + https://developers.binance.com/docs/margin_trading/trade/Get-Force-Liquidation-Record + https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Users-Force-Orders + https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/rest-api/Users-Force-Orders + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Query-Users-UM-Force-Orders + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Query-Users-CM-Force-Orders + + :param str [symbol]: unified CCXT market symbol + :param int [since]: the earliest time in ms to fetch liquidations for + :param int [limit]: the maximum number of liquidation structures to retrieve + :param dict [params]: exchange specific parameters for the binance api endpoint + :param int [params.until]: timestamp in ms of the latest liquidation + :param boolean [params.paginate]: *spot only* default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param boolean [params.portfolioMargin]: set to True if you would like to fetch liquidations in a portfolio margin account + :param str [params.type]: "spot" + :param str [params.subType]: "linear" or "inverse" + :returns dict: an array of `liquidation structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchMyLiquidations', 'paginate') + if paginate: + return await self.fetch_paginated_call_incremental('fetchMyLiquidations', symbol, since, limit, params, 'current', 100) + market = None + if symbol is not None: + market = self.market(symbol) + type = None + type, params = self.handle_market_type_and_params('fetchMyLiquidations', market, params) + subType = None + subType, params = self.handle_sub_type_and_params('fetchMyLiquidations', market, params, 'linear') + isPortfolioMargin = None + isPortfolioMargin, params = self.handle_option_and_params_2(params, 'fetchMyLiquidations', 'papi', 'portfolioMargin', False) + request: dict = {} + if type != 'spot': + request['autoCloseType'] = 'LIQUIDATION' + if market is not None: + symbolKey = 'isolatedSymbol' if market['spot'] else 'symbol' + if not isPortfolioMargin: + request[symbolKey] = market['id'] + if since is not None: + request['startTime'] = since + if limit is not None: + if type == 'spot': + request['size'] = limit + else: + request['limit'] = limit + request, params = self.handle_until_option('endTime', request, params) + response = None + if type == 'spot': + if isPortfolioMargin: + response = await self.papiGetMarginForceOrders(self.extend(request, params)) + else: + response = await self.sapiGetMarginForceLiquidationRec(self.extend(request, params)) + elif subType == 'linear': + if isPortfolioMargin: + response = await self.papiGetUmForceOrders(self.extend(request, params)) + else: + response = await self.fapiPrivateGetForceOrders(self.extend(request, params)) + elif subType == 'inverse': + if isPortfolioMargin: + response = await self.papiGetCmForceOrders(self.extend(request, params)) + else: + response = await self.dapiPrivateGetForceOrders(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchMyLiquidations() does not support ' + market['type'] + ' markets') + # + # margin + # + # { + # "rows": [ + # { + # "avgPrice": "0.00388359", + # "executedQty": "31.39000000", + # "orderId": 180015097, + # "price": "0.00388110", + # "qty": "31.39000000", + # "side": "SELL", + # "symbol": "BNBBTC", + # "timeInForce": "GTC", + # "isIsolated": True, + # "updatedTime": 1558941374745 + # } + # ], + # "total": 1 + # } + # + # linear + # + # [ + # { + # "orderId": 6071832819, + # "symbol": "BTCUSDT", + # "status": "FILLED", + # "clientOrderId": "autoclose-1596107620040000020", + # "price": "10871.09", + # "avgPrice": "10913.21000", + # "origQty": "0.001", + # "executedQty": "0.001", + # "cumQuote": "10.91321", + # "timeInForce": "IOC", + # "type": "LIMIT", + # "reduceOnly": False, + # "closePosition": False, + # "side": "SELL", + # "positionSide": "BOTH", + # "stopPrice": "0", + # "workingType": "CONTRACT_PRICE", + # "origType": "LIMIT", + # "time": 1596107620044, + # "updateTime": 1596107620087 + # }, + # ] + # + # inverse + # + # [ + # { + # "orderId": 165123080, + # "symbol": "BTCUSD_200925", + # "pair": "BTCUSD", + # "status": "FILLED", + # "clientOrderId": "autoclose-1596542005017000006", + # "price": "11326.9", + # "avgPrice": "11326.9", + # "origQty": "1", + # "executedQty": "1", + # "cumBase": "0.00882854", + # "timeInForce": "IOC", + # "type": "LIMIT", + # "reduceOnly": False, + # "closePosition": False, + # "side": "SELL", + # "positionSide": "BOTH", + # "stopPrice": "0", + # "workingType": "CONTRACT_PRICE", + # "priceProtect": False, + # "origType": "LIMIT", + # "time": 1596542005019, + # "updateTime": 1596542005050 + # }, + # ] + # + liquidations = self.safe_list(response, 'rows', response) + return self.parse_liquidations(liquidations, market, since, limit) + + def parse_liquidation(self, liquidation, market: Market = None): + # + # margin + # + # { + # "avgPrice": "0.00388359", + # "executedQty": "31.39000000", + # "orderId": 180015097, + # "price": "0.00388110", + # "qty": "31.39000000", + # "side": "SELL", + # "symbol": "BNBBTC", + # "timeInForce": "GTC", + # "isIsolated": True, + # "updatedTime": 1558941374745 + # } + # + # linear + # + # { + # "orderId": 6071832819, + # "symbol": "BTCUSDT", + # "status": "FILLED", + # "clientOrderId": "autoclose-1596107620040000020", + # "price": "10871.09", + # "avgPrice": "10913.21000", + # "origQty": "0.001", + # "executedQty": "0.002", + # "cumQuote": "10.91321", + # "timeInForce": "IOC", + # "type": "LIMIT", + # "reduceOnly": False, + # "closePosition": False, + # "side": "SELL", + # "positionSide": "BOTH", + # "stopPrice": "0", + # "workingType": "CONTRACT_PRICE", + # "origType": "LIMIT", + # "time": 1596107620044, + # "updateTime": 1596107620087 + # } + # + # inverse + # + # { + # "orderId": 165123080, + # "symbol": "BTCUSD_200925", + # "pair": "BTCUSD", + # "status": "FILLED", + # "clientOrderId": "autoclose-1596542005017000006", + # "price": "11326.9", + # "avgPrice": "11326.9", + # "origQty": "1", + # "executedQty": "1", + # "cumBase": "0.00882854", + # "timeInForce": "IOC", + # "type": "LIMIT", + # "reduceOnly": False, + # "closePosition": False, + # "side": "SELL", + # "positionSide": "BOTH", + # "stopPrice": "0", + # "workingType": "CONTRACT_PRICE", + # "priceProtect": False, + # "origType": "LIMIT", + # "time": 1596542005019, + # "updateTime": 1596542005050 + # } + # + marketId = self.safe_string(liquidation, 'symbol') + timestamp = self.safe_integer_2(liquidation, 'updatedTime', 'updateTime') + return self.safe_liquidation({ + 'info': liquidation, + 'symbol': self.safe_symbol(marketId, market), + 'contracts': self.safe_number(liquidation, 'executedQty'), + 'contractSize': self.safe_number(market, 'contractSize'), + 'price': self.safe_number(liquidation, 'avgPrice'), + 'side': self.safe_string_lower(liquidation, 'side'), + 'baseValue': self.safe_number(liquidation, 'cumBase'), + 'quoteValue': self.safe_number(liquidation, 'cumQuote'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + + async def fetch_greeks(self, symbol: str, params={}) -> Greeks: + """ + fetches an option contracts greeks, financial metrics used to measure the factors that affect the price of an options contract + + https://developers.binance.com/docs/derivatives/option/market-data/Option-Mark-Price + + :param str symbol: unified symbol of the market to fetch greeks for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `greeks structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = await self.eapiPublicGetMark(self.extend(request, params)) + # + # [ + # { + # "symbol": "BTC-231229-40000-C", + # "markPrice": "2012", + # "bidIV": "0.60236275", + # "askIV": "0.62267244", + # "markIV": "0.6125176", + # "delta": "0.39111646", + # "theta": "-32.13948531", + # "gamma": "0.00004656", + # "vega": "51.70062218", + # "highPriceLimit": "6474", + # "lowPriceLimit": "5" + # } + # ] + # + return self.parse_greeks(response[0], market) + + async def fetch_all_greeks(self, symbols: Strings = None, params={}) -> List[Greeks]: + """ + fetches all option contracts greeks, financial metrics used to measure the factors that affect the price of an options contract + + https://developers.binance.com/docs/derivatives/option/market-data/Option-Mark-Price + + :param str[] [symbols]: unified symbols of the markets to fetch greeks for, all markets are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `greeks structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, True, True, True) + request: dict = {} + market = None + if symbols is not None: + symbolsLength = len(symbols) + if symbolsLength == 1: + market = self.market(symbols[0]) + request['symbol'] = market['id'] + response = await self.eapiPublicGetMark(self.extend(request, params)) + # + # [ + # { + # "symbol": "BTC-231229-40000-C", + # "markPrice": "2012", + # "bidIV": "0.60236275", + # "askIV": "0.62267244", + # "markIV": "0.6125176", + # "delta": "0.39111646", + # "theta": "-32.13948531", + # "gamma": "0.00004656", + # "vega": "51.70062218", + # "highPriceLimit": "6474", + # "lowPriceLimit": "5" + # } + # ] + # + return self.parse_all_greeks(response, symbols) + + def parse_greeks(self, greeks: dict, market: Market = None) -> Greeks: + # + # { + # "symbol": "BTC-231229-40000-C", + # "markPrice": "2012", + # "bidIV": "0.60236275", + # "askIV": "0.62267244", + # "markIV": "0.6125176", + # "delta": "0.39111646", + # "theta": "-32.13948531", + # "gamma": "0.00004656", + # "vega": "51.70062218", + # "highPriceLimit": "6474", + # "lowPriceLimit": "5" + # } + # + marketId = self.safe_string(greeks, 'symbol') + symbol = self.safe_symbol(marketId, market) + return { + 'symbol': symbol, + 'timestamp': None, + 'datetime': None, + 'delta': self.safe_number(greeks, 'delta'), + 'gamma': self.safe_number(greeks, 'gamma'), + 'theta': self.safe_number(greeks, 'theta'), + 'vega': self.safe_number(greeks, 'vega'), + 'rho': None, + 'bidSize': None, + 'askSize': None, + 'bidImpliedVolatility': self.safe_number(greeks, 'bidIV'), + 'askImpliedVolatility': self.safe_number(greeks, 'askIV'), + 'markImpliedVolatility': self.safe_number(greeks, 'markIV'), + 'bidPrice': None, + 'askPrice': None, + 'markPrice': self.safe_number(greeks, 'markPrice'), + 'lastPrice': None, + 'underlyingPrice': None, + 'info': greeks, + } + + async def fetch_trading_limits(self, symbols: Strings = None, params={}): + # self method should not be called directly, use loadTradingLimits() instead + markets = await self.fetch_markets() + tradingLimits: dict = {} + for i in range(0, len(markets)): + market = markets[i] + symbol = market['symbol'] + if (symbols is None) or (self.in_array(symbol, symbols)): + tradingLimits[symbol] = market['limits']['amount'] + return tradingLimits + + async def fetch_position_mode(self, symbol: Str = None, params={}): + """ + fetchs the position mode, hedged or one way, hedged for binance is set identically for all linear markets or all inverse markets + + https://developers.binance.com/docs/derivatives/usds-margined-futures/account/rest-api/Get-Current-Position-Mode + https://developers.binance.com/docs/derivatives/coin-margined-futures/account/rest-api/Get-Current-Position-Mode + + :param str symbol: unified symbol of the market to fetch the order book for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.subType]: "linear" or "inverse" + :returns dict: an object detailing whether the market is in hedged or one-way mode + """ + market = None + if symbol is not None: + market = self.market(symbol) + subType = None + subType, params = self.handle_sub_type_and_params('fetchPositionMode', market, params) + response = None + if subType == 'linear': + response = await self.fapiPrivateGetPositionSideDual(params) + elif subType == 'inverse': + response = await self.dapiPrivateGetPositionSideDual(params) + else: + raise BadRequest(self.id + ' fetchPositionMode requires either a symbol argument or params["subType"]') + # + # { + # dualSidePosition: False + # } + # + dualSidePosition = self.safe_bool(response, 'dualSidePosition') + return { + 'info': response, + 'hedged': dualSidePosition, + } + + async def fetch_margin_modes(self, symbols: Strings = None, params={}) -> MarginModes: + """ + fetches margin modes("isolated" or "cross") that the market for the symbol in in, with symbol=None all markets for a subType(linear/inverse) are returned + + https://developers.binance.com/docs/derivatives/coin-margined-futures/account/rest-api/Account-Information + https://developers.binance.com/docs/derivatives/usds-margined-futures/account/rest-api/Account-Information-V2 + https://developers.binance.com/docs/derivatives/usds-margined-futures/account/rest-api/Symbol-Config + + :param str[] symbols: unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.subType]: "linear" or "inverse" + :returns dict: a list of `margin mode structures ` + """ + await self.load_markets() + market = None + if symbols is not None: + symbols = self.market_symbols(symbols) + market = self.market(symbols[0]) + subType = None + subType, params = self.handle_sub_type_and_params('fetchMarginMode', market, params) + response = None + if subType == 'linear': + response = await self.fapiPrivateGetSymbolConfig(params) + # + # [ + # { + # "symbol": "BTCUSDT", + # "marginType": "CROSSED", + # "isAutoAddMargin": "false", + # "leverage": 21, + # "maxNotionalValue": "1000000", + # } + # ] + # + elif subType == 'inverse': + response = await self.dapiPrivateGetAccount(params) + # + # { + # feeTier: '0', + # canTrade: True, + # canDeposit: True, + # canWithdraw: True, + # updateTime: '0', + # assets: [ + # { + # asset: 'APT', + # walletBalance: '0.00000000', + # unrealizedProfit: '0.00000000', + # marginBalance: '0.00000000', + # maintMargin: '0.00000000', + # initialMargin: '0.00000000', + # positionInitialMargin: '0.00000000', + # openOrderInitialMargin: '0.00000000', + # maxWithdrawAmount: '0.00000000', + # crossWalletBalance: '0.00000000', + # crossUnPnl: '0.00000000', + # availableBalance: '0.00000000', + # updateTime: '0' + # }, + # ... + # ], + # positions: [ + # { + # symbol: 'BCHUSD_240329', + # initialMargin: '0', + # maintMargin: '0', + # unrealizedProfit: '0.00000000', + # positionInitialMargin: '0', + # openOrderInitialMargin: '0', + # leverage: '20', + # isolated: False, + # positionSide: 'BOTH', + # entryPrice: '0.00000000', + # maxQty: '1000', + # notionalValue: '0', + # isolatedWallet: '0', + # updateTime: '0', + # positionAmt: '0', + # breakEvenPrice: '0.00000000' + # }, + # ... + # ] + # } + # + else: + raise BadRequest(self.id + ' fetchMarginModes() supports linear and inverse subTypes only') + assets = self.safe_list(response, 'positions', []) + if isinstance(response, list): + assets = response + return self.parse_margin_modes(assets, symbols, 'symbol', 'swap') + + async def fetch_margin_mode(self, symbol: str, params={}) -> MarginMode: + """ + fetches the margin mode of a specific symbol + + https://developers.binance.com/docs/derivatives/usds-margined-futures/account/rest-api/Symbol-Config + https://developers.binance.com/docs/derivatives/coin-margined-futures/account/rest-api/Account-Information + + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.subType]: "linear" or "inverse" + :returns dict: a `margin mode structure ` + """ + await self.load_markets() + market = self.market(symbol) + subType = None + subType, params = self.handle_sub_type_and_params('fetchMarginMode', market, params) + response = None + if subType == 'linear': + request: dict = { + 'symbol': market['id'], + } + response = await self.fapiPrivateGetSymbolConfig(self.extend(request, params)) + # + # [ + # { + # "symbol": "BTCUSDT", + # "marginType": "CROSSED", + # "isAutoAddMargin": "false", + # "leverage": 21, + # "maxNotionalValue": "1000000", + # } + # ] + # + elif subType == 'inverse': + fetchMarginModesResponse = await self.fetch_margin_modes([symbol], params) + return fetchMarginModesResponse[symbol] + else: + raise BadRequest(self.id + ' fetchMarginMode() supports linear and inverse subTypes only') + return self.parse_margin_mode(response[0], market) + + def parse_margin_mode(self, marginMode: dict, market=None) -> MarginMode: + marketId = self.safe_string(marginMode, 'symbol') + market = self.safe_market(marketId, market) + marginModeRaw = self.safe_bool(marginMode, 'isolated') + reMarginMode = None + if marginModeRaw is not None: + reMarginMode = 'isolated' if marginModeRaw else 'cross' + marginTypeRaw = self.safe_string_lower(marginMode, 'marginType') + if marginTypeRaw is not None: + reMarginMode = 'cross' if (marginTypeRaw == 'crossed') else 'isolated' + return { + 'info': marginMode, + 'symbol': market['symbol'], + 'marginMode': reMarginMode, + } + + async def fetch_option(self, symbol: str, params={}) -> Option: + """ + fetches option data that is commonly found in an option chain + + https://developers.binance.com/docs/derivatives/option/market-data/24hr-Ticker-Price-Change-Statistics + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `option chain structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = await self.eapiPublicGetTicker(self.extend(request, params)) + # + # [ + # { + # "symbol": "BTC-241227-80000-C", + # "priceChange": "0", + # "priceChangePercent": "0", + # "lastPrice": "2750", + # "lastQty": "0", + # "open": "2750", + # "high": "2750", + # "low": "2750", + # "volume": "0", + # "amount": "0", + # "bidPrice": "4880", + # "askPrice": "0", + # "openTime": 0, + # "closeTime": 0, + # "firstTradeId": 0, + # "tradeCount": 0, + # "strikePrice": "80000", + # "exercisePrice": "63944.09893617" + # } + # ] + # + chain = self.safe_dict(response, 0, {}) + return self.parse_option(chain, None, market) + + def parse_option(self, chain: dict, currency: Currency = None, market: Market = None) -> Option: + # + # { + # "symbol": "BTC-241227-80000-C", + # "priceChange": "0", + # "priceChangePercent": "0", + # "lastPrice": "2750", + # "lastQty": "0", + # "open": "2750", + # "high": "2750", + # "low": "2750", + # "volume": "0", + # "amount": "0", + # "bidPrice": "4880", + # "askPrice": "0", + # "openTime": 0, + # "closeTime": 0, + # "firstTradeId": 0, + # "tradeCount": 0, + # "strikePrice": "80000", + # "exercisePrice": "63944.09893617" + # } + # + marketId = self.safe_string(chain, 'symbol') + market = self.safe_market(marketId, market) + return { + 'info': chain, + 'currency': None, + 'symbol': market['symbol'], + 'timestamp': None, + 'datetime': None, + 'impliedVolatility': None, + 'openInterest': None, + 'bidPrice': self.safe_number(chain, 'bidPrice'), + 'askPrice': self.safe_number(chain, 'askPrice'), + 'midPrice': None, + 'markPrice': None, + 'lastPrice': self.safe_number(chain, 'lastPrice'), + 'underlyingPrice': self.safe_number(chain, 'exercisePrice'), + 'change': self.safe_number(chain, 'priceChange'), + 'percentage': self.safe_number(chain, 'priceChangePercent'), + 'baseVolume': self.safe_number(chain, 'volume'), + 'quoteVolume': None, + } + + async def fetch_margin_adjustment_history(self, symbol: Str = None, type: Str = None, since: Num = None, limit: Num = None, params={}) -> List[MarginModification]: + """ + fetches the history of margin added or reduced from contract isolated positions + + https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Get-Position-Margin-Change-History + https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/rest-api/Get-Position-Margin-Change-History + + :param str symbol: unified market symbol + :param str [type]: "add" or "reduce" + :param int [since]: timestamp in ms of the earliest change to fetch + :param int [limit]: the maximum amount of changes to fetch + :param dict params: extra parameters specific to the exchange api endpoint + :param int [params.until]: timestamp in ms of the latest change to fetch + :returns dict[]: a list of `margin structures ` + """ + await self.load_markets() + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMarginAdjustmentHistory() requires a symbol argument') + market = self.market(symbol) + until = self.safe_integer(params, 'until') + params = self.omit(params, 'until') + request: dict = { + 'symbol': market['id'], + } + if type is not None: + request['type'] = 1 if (type == 'add') else 2 + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + if until is not None: + request['endTime'] = until + response = None + if market['linear']: + response = await self.fapiPrivateGetPositionMarginHistory(self.extend(request, params)) + elif market['inverse']: + response = await self.dapiPrivateGetPositionMarginHistory(self.extend(request, params)) + else: + raise BadRequest(self.id + ' fetchMarginAdjustmentHistory() is not supported for markets of type ' + market['type']) + # + # [ + # { + # symbol: "XRPUSDT", + # type: "1", + # deltaType: "TRADE", + # amount: "2.57148240", + # asset: "USDT", + # time: "1711046271555", + # positionSide: "BOTH", + # clientTranId: "" + # } + # ... + # ] + # + modifications = self.parse_margin_modifications(response) + return self.filter_by_symbol_since_limit(modifications, symbol, since, limit) + + async def fetch_convert_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies that can be converted + + https://developers.binance.com/docs/convert/market-data/Query-order-quantity-precision-per-asset + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + await self.load_markets() + response = await self.sapiGetConvertAssetInfo(params) + # + # [ + # { + # "asset": "BTC", + # "fraction": 8 + # }, + # ] + # + result: dict = {} + for i in range(0, len(response)): + entry = response[i] + id = self.safe_string(entry, 'asset') + code = self.safe_currency_code(id) + result[code] = { + 'info': entry, + 'id': id, + 'code': code, + 'networks': None, + 'type': None, + 'name': None, + 'active': None, + 'deposit': None, + 'withdraw': None, + 'fee': None, + 'precision': self.parse_number(self.parse_precision(self.safe_string(entry, 'fraction'))), + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + 'deposit': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + } + return result + + async def fetch_convert_quote(self, fromCode: str, toCode: str, amount: Num = None, params={}) -> Conversion: + """ + fetch a quote for converting from one currency to another + + https://developers.binance.com/docs/convert/trade/Send-quote-request + + :param str fromCode: the currency that you want to sell and convert from + :param str toCode: the currency that you want to buy and convert into + :param float amount: how much you want to trade in units of the from currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.walletType]: either 'SPOT' or 'FUNDING', the default is 'SPOT' + :returns dict: a `conversion structure ` + """ + if amount is None: + raise ArgumentsRequired(self.id + ' fetchConvertQuote() requires an amount argument') + await self.load_markets() + request: dict = { + 'fromAsset': fromCode, + 'toAsset': toCode, + 'fromAmount': amount, + } + response = await self.sapiPostConvertGetQuote(self.extend(request, params)) + # + # { + # "quoteId":"12415572564", + # "ratio":"38163.7", + # "inverseRatio":"0.0000262", + # "validTimestamp":1623319461670, + # "toAmount":"3816.37", + # "fromAmount":"0.1" + # } + # + fromCurrency = self.currency(fromCode) + toCurrency = self.currency(toCode) + return self.parse_conversion(response, fromCurrency, toCurrency) + + async def create_convert_trade(self, id: str, fromCode: str, toCode: str, amount: Num = None, params={}) -> Conversion: + """ + convert from one currency to another + + https://developers.binance.com/docs/convert/trade/Accept-Quote + + :param str id: the id of the trade that you want to make + :param str fromCode: the currency that you want to sell and convert from + :param str toCode: the currency that you want to buy and convert into + :param float [amount]: how much you want to trade in units of the from currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `conversion structure ` + """ + await self.load_markets() + request: dict = {} + response = None + if (fromCode == 'BUSD') or (toCode == 'BUSD'): + if amount is None: + raise ArgumentsRequired(self.id + ' createConvertTrade() requires an amount argument') + request['clientTranId'] = id + request['asset'] = fromCode + request['targetAsset'] = toCode + request['amount'] = amount + response = await self.sapiPostAssetConvertTransfer(self.extend(request, params)) + # + # { + # "tranId": 118263407119, + # "status": "S" + # } + # + else: + request['quoteId'] = id + response = await self.sapiPostConvertAcceptQuote(self.extend(request, params)) + # + # { + # "orderId":"933256278426274426", + # "createTime":1623381330472, + # "orderStatus":"PROCESS" + # } + # + fromCurrency = self.currency(fromCode) + toCurrency = self.currency(toCode) + return self.parse_conversion(response, fromCurrency, toCurrency) + + async def fetch_convert_trade(self, id: str, code: Str = None, params={}) -> Conversion: + """ + fetch the data for a conversion trade + + https://developers.binance.com/docs/convert/trade/Order-Status + + :param str id: the id of the trade that you want to fetch + :param str [code]: the unified currency code of the conversion trade + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `conversion structure ` + """ + await self.load_markets() + request: dict = {} + response = None + if code == 'BUSD': + msInDay = 86400000 + now = self.milliseconds() + if code is not None: + currency = self.currency(code) + request['asset'] = currency['id'] + request['tranId'] = id + request['startTime'] = now - msInDay + request['endTime'] = now + response = await self.sapiGetAssetConvertTransferQueryByPage(self.extend(request, params)) + # + # { + # "total": 3, + # "rows": [ + # { + # "tranId": 118263615991, + # "type": 244, + # "time": 1664442078000, + # "deductedAsset": "BUSD", + # "deductedAmount": "1", + # "targetAsset": "USDC", + # "targetAmount": "1", + # "status": "S", + # "accountType": "MAIN" + # }, + # ] + # } + # + else: + request['orderId'] = id + response = await self.sapiGetConvertOrderStatus(self.extend(request, params)) + # + # { + # "orderId":933256278426274426, + # "orderStatus":"SUCCESS", + # "fromAsset":"BTC", + # "fromAmount":"0.00054414", + # "toAsset":"USDT", + # "toAmount":"20", + # "ratio":"36755", + # "inverseRatio":"0.00002721", + # "createTime":1623381330472 + # } + # + data = response + if code == 'BUSD': + rows = self.safe_list(response, 'rows', []) + data = self.safe_dict(rows, 0, {}) + fromCurrencyId = self.safe_string_2(data, 'deductedAsset', 'fromAsset') + toCurrencyId = self.safe_string_2(data, 'targetAsset', 'toAsset') + fromCurrency = None + toCurrency = None + if fromCurrencyId is not None: + fromCurrency = self.currency(fromCurrencyId) + if toCurrencyId is not None: + toCurrency = self.currency(toCurrencyId) + return self.parse_conversion(data, fromCurrency, toCurrency) + + async def fetch_convert_trade_history(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Conversion]: + """ + fetch the users history of conversion trades + + https://developers.binance.com/docs/convert/trade/Get-Convert-Trade-History + + :param str [code]: the unified currency code + :param int [since]: the earliest time in ms to fetch conversions for + :param int [limit]: the maximum number of conversion structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest conversion to fetch + :returns dict[]: a list of `conversion structures ` + """ + await self.load_markets() + request: dict = {} + msInThirtyDays = 2592000000 + now = self.milliseconds() + if since is not None: + request['startTime'] = since + else: + request['startTime'] = now - msInThirtyDays + endTime = self.safe_integer_2(params, 'endTime', 'until') + if endTime is not None: + request['endTime'] = endTime + else: + request['endTime'] = now + params = self.omit(params, 'until') + response = None + responseQuery = None + fromCurrencyKey = None + toCurrencyKey = None + if code == 'BUSD': + currency = self.currency(code) + request['asset'] = currency['id'] + if limit is not None: + request['size'] = limit + fromCurrencyKey = 'deductedAsset' + toCurrencyKey = 'targetAsset' + responseQuery = 'rows' + response = await self.sapiGetAssetConvertTransferQueryByPage(self.extend(request, params)) + # + # { + # "total": 3, + # "rows": [ + # { + # "tranId": 118263615991, + # "type": 244, + # "time": 1664442078000, + # "deductedAsset": "BUSD", + # "deductedAmount": "1", + # "targetAsset": "USDC", + # "targetAmount": "1", + # "status": "S", + # "accountType": "MAIN" + # }, + # ] + # } + # + else: + if (request['endTime'] - request['startTime']) > msInThirtyDays: + raise BadRequest(self.id + ' fetchConvertTradeHistory() the max interval between startTime and endTime is 30 days.') + if limit is not None: + request['limit'] = limit + fromCurrencyKey = 'fromAsset' + toCurrencyKey = 'toAsset' + responseQuery = 'list' + response = await self.sapiGetConvertTradeFlow(self.extend(request, params)) + # + # { + # "list": [ + # { + # "quoteId": "f3b91c525b2644c7bc1e1cd31b6e1aa6", + # "orderId": 940708407462087195, + # "orderStatus": "SUCCESS", + # "fromAsset": "USDT", + # "fromAmount": "20", + # "toAsset": "BNB", + # "toAmount": "0.06154036", + # "ratio": "0.00307702", + # "inverseRatio": "324.99", + # "createTime": 1624248872184 + # } + # ], + # "startTime": 1623824139000, + # "endTime": 1626416139000, + # "limit": 100, + # "moreData": False + # } + # + rows = self.safe_list(response, responseQuery, []) + return self.parse_conversions(rows, code, fromCurrencyKey, toCurrencyKey, since, limit) + + def parse_conversion(self, conversion: dict, fromCurrency: Currency = None, toCurrency: Currency = None) -> Conversion: + # + # fetchConvertQuote + # + # { + # "quoteId":"12415572564", + # "ratio":"38163.7", + # "inverseRatio":"0.0000262", + # "validTimestamp":1623319461670, + # "toAmount":"3816.37", + # "fromAmount":"0.1" + # } + # + # createConvertTrade + # + # { + # "orderId":"933256278426274426", + # "createTime":1623381330472, + # "orderStatus":"PROCESS" + # } + # + # createConvertTrade BUSD + # + # { + # "tranId": 118263407119, + # "status": "S" + # } + # + # fetchConvertTrade, fetchConvertTradeHistory BUSD + # + # { + # "tranId": 118263615991, + # "type": 244, + # "time": 1664442078000, + # "deductedAsset": "BUSD", + # "deductedAmount": "1", + # "targetAsset": "USDC", + # "targetAmount": "1", + # "status": "S", + # "accountType": "MAIN" + # } + # + # fetchConvertTrade + # + # { + # "orderId":933256278426274426, + # "orderStatus":"SUCCESS", + # "fromAsset":"BTC", + # "fromAmount":"0.00054414", + # "toAsset":"USDT", + # "toAmount":"20", + # "ratio":"36755", + # "inverseRatio":"0.00002721", + # "createTime":1623381330472 + # } + # + # fetchConvertTradeHistory + # + # { + # "quoteId": "f3b91c525b2644c7bc1e1cd31b6e1aa6", + # "orderId": 940708407462087195, + # "orderStatus": "SUCCESS", + # "fromAsset": "USDT", + # "fromAmount": "20", + # "toAsset": "BNB", + # "toAmount": "0.06154036", + # "ratio": "0.00307702", + # "inverseRatio": "324.99", + # "createTime": 1624248872184 + # } + # + timestamp = self.safe_integer_n(conversion, ['time', 'validTimestamp', 'createTime']) + fromCur = self.safe_string_2(conversion, 'deductedAsset', 'fromAsset') + fromCode = self.safe_currency_code(fromCur, fromCurrency) + to = self.safe_string_2(conversion, 'targetAsset', 'toAsset') + toCode = self.safe_currency_code(to, toCurrency) + return { + 'info': conversion, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': self.safe_string_n(conversion, ['tranId', 'orderId', 'quoteId']), + 'fromCurrency': fromCode, + 'fromAmount': self.safe_number_2(conversion, 'deductedAmount', 'fromAmount'), + 'toCurrency': toCode, + 'toAmount': self.safe_number_2(conversion, 'targetAmount', 'toAmount'), + 'price': None, + 'fee': None, + } + + async def fetch_funding_intervals(self, symbols: Strings = None, params={}) -> FundingRates: + """ + fetch the funding rate interval for multiple markets + + https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/Get-Funding-Rate-Info + https://developers.binance.com/docs/derivatives/coin-margined-futures/market-data/rest-api/Get-Funding-Info + + :param str[] [symbols]: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.subType]: "linear" or "inverse" + :returns dict[]: a list of `funding rate structures ` + """ + await self.load_markets() + market = None + if symbols is not None: + symbols = self.market_symbols(symbols) + market = self.market(symbols[0]) + type = 'swap' + subType = None + subType, params = self.handle_sub_type_and_params('fetchFundingIntervals', market, params, 'linear') + response = None + if self.is_linear(type, subType): + response = await self.fapiPublicGetFundingInfo(params) + elif self.is_inverse(type, subType): + response = await self.dapiPublicGetFundingInfo(params) + else: + raise NotSupported(self.id + ' fetchFundingIntervals() supports linear and inverse swap contracts only') + # + # [ + # { + # "symbol": "BLZUSDT", + # "adjustedFundingRateCap": "0.03000000", + # "adjustedFundingRateFloor": "-0.03000000", + # "fundingIntervalHours": 4, + # "disclaimer": False + # }, + # ] + # + return self.parse_funding_rates(response, symbols) + + async def fetch_long_short_ratio_history(self, symbol: Str = None, timeframe: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LongShortRatio]: + """ + fetches the long short ratio history for a unified market symbol + + https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/Long-Short-Ratio + https://developers.binance.com/docs/derivatives/coin-margined-futures/market-data/rest-api/Long-Short-Ratio + + :param str symbol: unified symbol of the market to fetch the long short ratio for + :param str [timeframe]: the period for the ratio, default is 24 hours + :param int [since]: the earliest time in ms to fetch ratios for + :param int [limit]: the maximum number of long short ratio structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest ratio to fetch + :returns dict[]: an array of `long short ratio structures ` + """ + await self.load_markets() + market = self.market(symbol) + if timeframe is None: + timeframe = '1d' + request: dict = { + 'period': timeframe, + } + request, params = self.handle_until_option('endTime', request, params) + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + subType = None + subType, params = self.handle_sub_type_and_params('fetchLongShortRatioHistory', market, params) + response = None + if subType == 'linear': + request['symbol'] = market['id'] + response = await self.fapiDataGetGlobalLongShortAccountRatio(self.extend(request, params)) + # + # [ + # { + # "symbol": "BTCUSDT", + # "longAccount": "0.4558", + # "longShortRatio": "0.8376", + # "shortAccount": "0.5442", + # "timestamp": 1726790400000 + # }, + # ] + # + elif subType == 'inverse': + request['pair'] = market['info']['pair'] + response = await self.dapiDataGetGlobalLongShortAccountRatio(self.extend(request, params)) + # + # [ + # { + # "longAccount": "0.7262", + # "longShortRatio": "2.6523", + # "shortAccount": "0.2738", + # "pair": "BTCUSD", + # "timestamp": 1726790400000 + # }, + # ] + # + else: + raise BadRequest(self.id + ' fetchLongShortRatioHistory() supports linear and inverse subTypes only') + return self.parse_long_short_ratio_history(response, market) + + def parse_long_short_ratio(self, info: dict, market: Market = None) -> LongShortRatio: + # + # linear + # + # { + # "symbol": "BTCUSDT", + # "longAccount": "0.4558", + # "longShortRatio": "0.8376", + # "shortAccount": "0.5442", + # "timestamp": 1726790400000 + # } + # + # inverse + # + # { + # "longAccount": "0.7262", + # "longShortRatio": "2.6523", + # "shortAccount": "0.2738", + # "pair": "BTCUSD", + # "timestamp": 1726790400000 + # } + # + marketId = self.safe_string(info, 'symbol') + timestamp = self.safe_integer_omit_zero(info, 'timestamp') + return { + 'info': info, + 'symbol': self.safe_symbol(marketId, market, None, 'contract'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'timeframe': None, + 'longShortRatio': self.safe_number(info, 'longShortRatio'), + } diff --git a/ccxt/async_support/binancecoinm.py b/ccxt/async_support/binancecoinm.py new file mode 100644 index 0000000..326207d --- /dev/null +++ b/ccxt/async_support/binancecoinm.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.binance import binance +from ccxt.abstract.binancecoinm import ImplicitAPI +from ccxt.base.types import Any + + +class binancecoinm(binance, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(binancecoinm, self).describe(), { + 'id': 'binancecoinm', + 'name': 'Binance COIN-M', + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/387cfc4e-5f33-48cd-8f5c-cd4854dabf0c', + 'doc': [ + 'https://binance-docs.github.io/apidocs/delivery/en/', + 'https://binance-docs.github.io/apidocs/spot/en', + 'https://developers.binance.com/en', + ], + }, + 'has': { + 'CORS': None, + 'spot': False, + 'margin': False, + 'swap': True, + 'future': True, + 'option': None, + 'createStopMarketOrder': True, + }, + 'options': { + 'fetchMarkets': { + 'types': [ + 'inverse', + ], + }, + 'defaultSubType': 'inverse', + 'leverageBrackets': None, + }, + }) + + async def transfer_in(self, code: str, amount, params={}): + # transfer from spot wallet to coinm futures wallet + return await self.futuresTransfer(code, amount, 3, params) + + async def transfer_out(self, code: str, amount, params={}): + # transfer from coinm futures wallet to spot wallet + return await self.futuresTransfer(code, amount, 4, params) diff --git a/ccxt/async_support/binanceus.py b/ccxt/async_support/binanceus.py new file mode 100644 index 0000000..b919638 --- /dev/null +++ b/ccxt/async_support/binanceus.py @@ -0,0 +1,255 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.binance import binance +from ccxt.abstract.binanceus import ImplicitAPI +from ccxt.base.types import Any + + +class binanceus(binance, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(binanceus, self).describe(), { + 'id': 'binanceus', + 'name': 'Binance US', + 'countries': ['US'], # US + 'hostname': 'binance.us', + 'rateLimit': 50, # 1200 req per min + 'certified': False, + 'pro': True, + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/a9667919-b632-4d52-a832-df89f8a35e8c', + 'api': { + 'web': 'https://www.binance.us', + 'public': 'https://api.binance.us/api/v3', + 'private': 'https://api.binance.us/api/v3', + 'sapi': 'https://api.binance.us/sapi/v1', + 'sapiV2': 'https://api.binance.us/sapi/v2', + 'sapiV3': 'https://api.binance.us/sapi/v3', + }, + 'www': 'https://www.binance.us', + 'referral': 'https://www.binance.us/?ref=35005074', + 'doc': 'https://github.com/binance-us/binance-official-api-docs', + 'fees': 'https://www.binance.us/en/fee/schedule', + }, + 'fees': { + 'trading': { + 'tierBased': True, + 'percentage': True, + 'taker': self.parse_number('0.001'), # 0.1% trading fee, zero fees for all trading pairs before November 1. + 'maker': self.parse_number('0.001'), # 0.1% trading fee, zero fees for all trading pairs before November 1. + }, + }, + 'options': { + 'fetchMarkets': { + 'types': ['spot'], + }, + 'defaultType': 'spot', + 'fetchMargins': False, + 'quoteOrderQty': False, + }, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'closeAllPositions': False, + 'closePosition': False, + 'createReduceOnlyOrder': False, + 'createStopLossOrder': False, + 'createTakeProfitOrder': False, + 'fetchAllGreeks': False, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLeverage': False, + 'fetchLeverageTiers': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkOHLCV': False, + 'fetchMarkPrice': False, + 'fetchMarkPrices': False, + 'fetchMySettlementHistory': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchUnderlyingAssets': False, + 'fetchVolatilityHistory': False, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + }, + 'api': { + 'public': { + 'get': { + 'ping': 1, + 'time': 1, + 'exchangeInfo': 10, + 'trades': 1, + 'historicalTrades': 5, + 'aggTrades': 1, + 'depth': {'cost': 1, 'byLimit': [[100, 1], [500, 5], [1000, 10], [5000, 50]]}, + 'klines': 1, + 'ticker/price': {'cost': 1, 'noSymbol': 2}, + 'avgPrice': 1, + 'ticker/bookTicker': {'cost': 1, 'noSymbol': 2}, + 'ticker/24hr': {'cost': 1, 'noSymbol': 40}, + 'ticker': {'cost': 2, 'noSymbol': 100}, + }, + }, + 'private': { + 'get': { + 'account': 10, + 'rateLimit/order': 20, + 'order': 2, + 'openOrders': {'cost': 3, 'noSymbol': 40}, + 'myTrades': 10, + 'myPreventedMatches': 10, # with ID it has weight 1, but we don't have that complex handling yet + 'allOrders': 10, + 'orderList': 2, + 'allOrderList': 10, + 'openOrderList': 3, + }, + 'post': { + 'order': 1, + 'order/test': 1, + 'order/cancelReplace': 1, + 'order/oco': 1, + }, + 'delete': { + 'order': 1, + 'openOrders': 1, + 'orderList': 1, + }, + }, + 'sapi': { + 'get': { + 'system/status': 1, + 'asset/assetDistributionHistory': 1, + 'asset/query/trading-fee': 1, + 'asset/query/trading-volume': 1, + 'sub-account/spotSummary': 1, + 'sub-account/status': 1, + 'otc/coinPairs': 1, + 'otc/orders/{orderId}': 1, + 'otc/orders': 1, + 'ocbs/orders': 1, + 'capital/config/getall': 1, + 'capital/withdraw/history': 1, + 'fiatpayment/query/withdraw/history': 1, + 'capital/deposit/address': 1, + 'capital/deposit/hisrec': 1, + 'fiatpayment/query/deposit/history': 1, + 'capital/sub-account/deposit/address': 1, + 'capital/sub-account/deposit/history': 1, + 'asset/query/dust-logs': 1, + 'asset/query/dust-assets': 1, + 'marketing/referral/reward/history': 1, + 'staking/asset': 1, + 'staking/stakingBalance': 1, + 'staking/history': 1, + 'staking/stakingRewardsHistory': 1, + 'custodian/balance': 1, + 'custodian/supportedAssetList': 1, + 'custodian/walletTransferHistory': 1, + 'custodian/custodianTransferHistory': 1, + 'custodian/openOrders': 1, + 'custodian/order': 1, + 'custodian/orderHistory': 1, + 'custodian/tradeHistory': 1, + 'custodian/settlementSetting': 1, + 'custodian/settlementHistory': 1, + 'cl/transferHistory': 1, + 'apipartner/checkEligibility': 1, + 'apipartner/rebateHistory': 1, + }, + 'post': { + 'otc/quotes': 1, + 'otc/orders': 1, + 'fiatpayment/withdraw/apply': 1, + 'capital/withdraw/apply': 1, + 'asset/dust': 10, + 'staking/stake': 1, + 'staking/unstake': 1, + 'custodian/walletTransfer': 1, + 'custodian/custodianTransfer': 1, + 'custodian/undoTransfer': 1, + 'custodian/order': 1, + 'custodian/ocoOrder': 1, + 'cl/transfer': 1, + }, + 'delete': { + 'custodian/cancelOrder': 1, + 'custodian/cancelOrdersBySymbol': 1, + 'custodian/cancelOcoOrder': 1, + }, + }, + 'sapiV2': { + 'get': { + 'cl/account': 10, + 'cl/alertHistory': 1, + }, + }, + 'sapiV3': { + 'get': { + 'accountStatus': 1, + 'apiTradingStatus': 1, + 'sub-account/list': 1, + 'sub-account/transfer/history': 1, + 'sub-account/assets': 1, + }, + 'post': { + 'sub-account/transfer': 1, + }, + }, + }, + 'features': { + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + }) diff --git a/ccxt/async_support/binanceusdm.py b/ccxt/async_support/binanceusdm.py new file mode 100644 index 0000000..5b70539 --- /dev/null +++ b/ccxt/async_support/binanceusdm.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.binance import binance +from ccxt.abstract.binanceusdm import ImplicitAPI +from ccxt.base.types import Any +from ccxt.base.errors import InvalidOrder + + +class binanceusdm(binance, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(binanceusdm, self).describe(), { + 'id': 'binanceusdm', + 'name': 'Binance USDⓈ-M', + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/871cbea7-eebb-4b28-b260-c1c91df0487a', + 'doc': [ + 'https://binance-docs.github.io/apidocs/futures/en/', + 'https://binance-docs.github.io/apidocs/spot/en', + 'https://developers.binance.com/en', + ], + }, + 'has': { + 'CORS': None, + 'spot': False, + 'margin': False, + 'swap': True, + 'future': True, + 'option': None, + 'createStopMarketOrder': True, + }, + 'options': { + 'fetchMarkets': { + 'types': ['linear'], + }, + 'defaultSubType': 'linear', + # https://www.binance.com/en/support/faq/360033162192 + # tier amount, maintenance margin, initial margin, + 'leverageBrackets': None, + 'marginTypes': {}, + 'marginModes': {}, + }, + # https://binance-docs.github.io/apidocs/futures/en/#error-codes + # https://developers.binance.com/docs/derivatives/usds-margined-futures/error-code + 'exceptions': { + 'exact': { + '-5021': InvalidOrder, # {"code":-5021,"msg":"Due to the order could not be filled immediately, the FOK order has been rejected."} + '-5022': InvalidOrder, # {"code":-5022,"msg":"Due to the order could not be executed, the Post Only order will be rejected."} + '-5028': InvalidOrder, # {"code":-5028,"msg":"Timestamp for self request is outside of the ME recvWindow."} + }, + }, + }) + + async def transfer_in(self, code: str, amount, params={}): + # transfer from spot wallet to usdm futures wallet + return await self.futuresTransfer(code, amount, 1, params) + + async def transfer_out(self, code: str, amount, params={}): + # transfer from usdm futures wallet to spot wallet + return await self.futuresTransfer(code, amount, 2, params) diff --git a/ccxt/async_support/bingx.py b/ccxt/async_support/bingx.py new file mode 100644 index 0000000..e1d9f3d --- /dev/null +++ b/ccxt/async_support/bingx.py @@ -0,0 +1,6418 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.bingx import ImplicitAPI +import asyncio +import hashlib +import numbers +from ccxt.base.types import Any, Balances, Currencies, Currency, DepositAddress, Int, Leverage, MarginMode, MarginModification, Market, Num, Order, OrderBook, OrderRequest, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, FundingRates, Trade, TradingFeeInterface, Transaction, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import AccountSuspended +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import NotSupported +from ccxt.base.errors import OperationFailed +from ccxt.base.errors import DDoSProtection +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class bingx(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(bingx, self).describe(), { + 'id': 'bingx', + 'name': 'BingX', + 'countries': ['US'], # North America, Canada, the EU, Hong Kong and Taiwan + 'rateLimit': 100, + 'version': 'v1', + 'certified': True, + 'pro': True, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': True, + 'future': False, + 'option': False, + 'addMargin': True, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelAllOrders': True, + 'cancelAllOrdersAfter': True, + 'cancelOrder': True, + 'cancelOrders': True, + 'closeAllPositions': True, + 'closePosition': True, + 'createMarketBuyOrderWithCost': True, + 'createMarketOrderWithCost': True, + 'createMarketSellOrderWithCost': True, + 'createOrder': True, + 'createOrders': True, + 'createOrderWithTakeProfitAndStopLoss': True, + 'createReduceOnlyOrder': True, + 'createStopLossOrder': True, + 'createStopOrder': True, + 'createTakeProfitOrder': True, + 'createTrailingAmountOrder': True, + 'createTrailingPercentOrder': True, + 'createTriggerOrder': True, + 'editOrder': True, + 'fetchAllGreeks': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchCanceledOrders': True, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': True, + 'fetchDeposits': True, + 'fetchDepositWithdrawFee': 'emulated', + 'fetchDepositWithdrawFees': True, + 'fetchFundingRate': True, + 'fetchFundingRateHistory': True, + 'fetchFundingRates': True, + 'fetchGreeks': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchLeverage': True, + 'fetchLiquidations': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': True, + 'fetchMarkets': True, + 'fetchMarkOHLCV': True, + 'fetchMarkPrice': True, + 'fetchMarkPrices': True, + 'fetchMyLiquidations': True, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': True, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchPosition': True, + 'fetchPositionHistory': False, + 'fetchPositionMode': True, + 'fetchPositions': True, + 'fetchPositionsHistory': True, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': True, + 'fetchTransfers': True, + 'fetchVolatilityHistory': False, + 'fetchWithdrawals': True, + 'reduceMargin': True, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'sandbox': True, + 'setLeverage': True, + 'setMargin': True, + 'setMarginMode': True, + 'setPositionMode': True, + 'transfer': True, + }, + 'hostname': 'bingx.com', + 'urls': { + 'logo': 'https://github-production-user-asset-6210df.s3.amazonaws.com/1294454/253675376-6983b72e-4999-4549-b177-33b374c195e3.jpg', + 'api': { + 'fund': 'https://open-api.{hostname}/openApi', + 'spot': 'https://open-api.{hostname}/openApi', + 'swap': 'https://open-api.{hostname}/openApi', + 'contract': 'https://open-api.{hostname}/openApi', + 'wallets': 'https://open-api.{hostname}/openApi', + 'user': 'https://open-api.{hostname}/openApi', + 'subAccount': 'https://open-api.{hostname}/openApi', + 'account': 'https://open-api.{hostname}/openApi', + 'copyTrading': 'https://open-api.{hostname}/openApi', + 'cswap': 'https://open-api.{hostname}/openApi', + 'api': 'https://open-api.{hostname}/openApi', + }, + 'test': { + 'swap': 'https://open-api-vst.{hostname}/openApi', # only swap is really "test" but since the API keys are the same, we want to keep all the functionalities when the user enables the sandboxmode + }, + 'www': 'https://bingx.com/', + 'doc': 'https://bingx-api.github.io/docs/', + 'referral': 'https://bingx.com/invite/OHETOM', + }, + 'fees': { + 'tierBased': True, + 'spot': { + 'feeSide': 'get', + 'maker': self.parse_number('0.001'), + 'taker': self.parse_number('0.001'), + }, + 'swap': { + 'feeSide': 'quote', + 'maker': self.parse_number('0.0002'), + 'taker': self.parse_number('0.0005'), + }, + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + }, + 'api': { + 'fund': { + 'v1': { + 'private': { + 'get': { + 'account/balance': 1, + }, + }, + }, + }, + 'spot': { + 'v1': { + 'public': { + 'get': { + 'server/time': 1, + 'common/symbols': 1, + 'market/trades': 1, + 'market/depth': 1, + 'market/kline': 1, + 'ticker/24hr': 1, + 'ticker/price': 1, + 'ticker/bookTicker': 1, + }, + }, + 'private': { + 'get': { + 'trade/query': 1, + 'trade/openOrders': 1, + 'trade/historyOrders': 1, + 'trade/myTrades': 2, + 'user/commissionRate': 5, + 'account/balance': 2, + 'oco/orderList': 5, + 'oco/openOrderList': 5, + 'oco/historyOrderList': 5, + }, + 'post': { + 'trade/order': 2, + 'trade/cancel': 2, + 'trade/batchOrders': 5, + 'trade/order/cancelReplace': 5, + 'trade/cancelOrders': 5, + 'trade/cancelOpenOrders': 5, + 'trade/cancelAllAfter': 5, + 'oco/order': 5, + 'oco/cancel': 5, + }, + }, + }, + 'v2': { + 'public': { + 'get': { + 'market/depth': 1, + 'market/kline': 1, + }, + }, + }, + 'v3': { + 'private': { + 'get': { + 'get/asset/transfer': 1, + 'asset/transfer': 1, + 'capital/deposit/hisrec': 1, + 'capital/withdraw/history': 1, + }, + 'post': { + 'post/asset/transfer': 5, + }, + }, + }, + }, + 'swap': { + 'v1': { + 'public': { + 'get': { + 'ticker/price': 1, + 'market/historicalTrades': 1, + 'market/markPriceKlines': 1, + 'trade/multiAssetsRules': 1, + 'tradingRules': 1, + }, + }, + 'private': { + 'get': { + 'positionSide/dual': 5, + 'trade/batchCancelReplace': 5, + 'trade/fullOrder': 2, + 'maintMarginRatio': 2, + 'trade/positionHistory': 2, + 'positionMargin/history': 2, + 'twap/openOrders': 5, + 'twap/historyOrders': 5, + 'twap/orderDetail': 5, + 'trade/assetMode': 5, + 'user/marginAssets': 5, + }, + 'post': { + 'trade/cancelReplace': 2, + 'positionSide/dual': 5, + 'trade/batchCancelReplace': 5, + 'trade/closePosition': 2, + 'trade/getVst': 5, + 'twap/order': 5, + 'twap/cancelOrder': 5, + 'trade/assetMode': 5, + 'trade/reverse': 5, + 'trade/autoAddMargin': 5, + }, + }, + }, + 'v2': { + 'public': { + 'get': { + 'server/time': 1, + 'quote/contracts': 1, + 'quote/price': 1, + 'quote/depth': 1, + 'quote/trades': 1, + 'quote/premiumIndex': 1, + 'quote/fundingRate': 1, + 'quote/klines': 1, + 'quote/openInterest': 1, + 'quote/ticker': 1, + 'quote/bookTicker': 1, + }, + }, + 'private': { + 'get': { + 'user/balance': 2, + 'user/positions': 2, + 'user/income': 2, + 'trade/openOrders': 2, + 'trade/openOrder': 2, + 'trade/order': 2, + 'trade/marginType': 5, + 'trade/leverage': 2, + 'trade/forceOrders': 1, + 'trade/allOrders': 2, + 'trade/allFillOrders': 2, + 'trade/fillHistory': 2, + 'user/income/export': 2, + 'user/commissionRate': 2, + 'quote/bookTicker': 1, + }, + 'post': { + 'trade/order': 2, + 'trade/batchOrders': 2, + 'trade/closeAllPositions': 2, + 'trade/cancelAllAfter': 5, + 'trade/marginType': 5, + 'trade/leverage': 5, + 'trade/positionMargin': 5, + 'trade/order/test': 2, + }, + 'delete': { + 'trade/order': 2, + 'trade/batchOrders': 2, + 'trade/allOpenOrders': 2, + }, + }, + }, + 'v3': { + 'public': { + 'get': { + 'quote/klines': 1, + }, + }, + 'private': { + 'get': { + 'user/balance': 2, + }, + }, + }, + }, + 'cswap': { + 'v1': { + 'public': { + 'get': { + 'market/contracts': 1, + 'market/premiumIndex': 1, + 'market/openInterest': 1, + 'market/klines': 1, + 'market/depth': 1, + 'market/ticker': 1, + }, + }, + 'private': { + 'get': { + 'trade/leverage': 2, + 'trade/forceOrders': 2, + 'trade/allFillOrders': 2, + 'trade/openOrders': 2, + 'trade/orderDetail': 2, + 'trade/orderHistory': 2, + 'trade/marginType': 2, + 'user/commissionRate': 2, + 'user/positions': 2, + 'user/balance': 2, + }, + 'post': { + 'trade/order': 2, + 'trade/leverage': 2, + 'trade/allOpenOrders': 2, + 'trade/closeAllPositions': 2, + 'trade/marginType': 2, + 'trade/positionMargin': 2, + }, + 'delete': { + 'trade/allOpenOrders': 2, # post method in doc + 'trade/cancelOrder': 2, + }, + }, + }, + }, + 'contract': { + 'v1': { + 'private': { + 'get': { + 'allPosition': 2, + 'allOrders': 2, + 'balance': 2, + }, + }, + }, + }, + 'wallets': { + 'v1': { + 'private': { + 'get': { + 'capital/config/getall': 5, + 'capital/deposit/address': 5, + 'capital/innerTransfer/records': 1, + 'capital/subAccount/deposit/address': 5, + 'capital/deposit/subHisrec': 2, + 'capital/subAccount/innerTransfer/records': 1, + 'capital/deposit/riskRecords': 5, + }, + 'post': { + 'capital/withdraw/apply': 5, + 'capital/innerTransfer/apply': 5, + 'capital/subAccountInnerTransfer/apply': 2, + 'capital/deposit/createSubAddress': 2, + }, + }, + }, + }, + 'subAccount': { + 'v1': { + 'private': { + 'get': { + 'list': 10, + 'assets': 2, + 'allAccountBalance': 2, + }, + 'post': { + 'create': 10, + 'apiKey/create': 2, + 'apiKey/edit': 2, + 'apiKey/del': 2, + 'updateStatus': 10, + }, + }, + }, + }, + 'account': { + 'v1': { + 'private': { + 'get': { + 'uid': 1, + 'apiKey/query': 2, + 'account/apiPermissions': 5, + 'allAccountBalance': 2, + }, + 'post': { + 'innerTransfer/authorizeSubAccount': 1, + }, + }, + }, + 'transfer': { + 'v1': { + 'private': { + 'get': { + 'subAccount/asset/transferHistory': 1, + }, + 'post': { + 'subAccount/transferAsset/supportCoins': 1, + 'subAccount/transferAsset': 1, + }, + }, + }, + }, + }, + 'user': { + 'auth': { + 'private': { + 'post': { + 'userDataStream': 2, + }, + 'put': { + 'userDataStream': 2, + }, + 'delete': { + 'userDataStream': 2, + }, + }, + }, + }, + 'copyTrading': { + 'v1': { + 'private': { + 'get': { + 'swap/trace/currentTrack': 2, + }, + 'post': { + 'swap/trace/closeTrackOrder': 2, + 'swap/trace/setTPSL': 2, + 'spot/trader/sellOrder': 10, + }, + }, + }, + }, + 'api': { + 'v3': { + 'private': { + 'get': { + 'asset/transfer': 1, + 'asset/transferRecord': 5, + 'capital/deposit/hisrec': 1, + 'capital/withdraw/history': 1, + }, + 'post': { + 'post/asset/transfer': 1, + }, + }, + }, + 'asset': { + 'v1': { + 'private': { + 'post': { + 'transfer': 5, + }, + }, + 'public': { + 'get': { + 'transfer/supportCoins': 5, + }, + }, + }, + }, + }, + 'agent': { + 'v1': { + 'private': { + 'get': { + 'account/inviteAccountList': 5, + 'reward/commissionDataList': 5, + 'account/inviteRelationCheck': 5, + 'asset/depositDetailList': 5, + 'reward/third/commissionDataList': 5, + 'asset/partnerData': 5, + 'commissionDataList/referralCode': 5, + 'account/superiorCheck': 5, + }, + }, + }, + }, + }, + 'timeframes': { + '1m': '1m', + '3m': '3m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1h', + '2h': '2h', + '4h': '4h', + '6h': '6h', + '12h': '12h', + '1d': '1d', + '3d': '3d', + '1w': '1w', + '1M': '1M', + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'exact': { + '400': BadRequest, + '401': AuthenticationError, + '403': PermissionDenied, + '404': BadRequest, + '429': DDoSProtection, + '418': PermissionDenied, + '500': ExchangeError, + '504': ExchangeError, + '100001': AuthenticationError, + '100412': AuthenticationError, + '100202': InsufficientFunds, + '100204': BadRequest, + '100400': BadRequest, + '100410': OperationFailed, # {"code":100410,"msg":"The current system is busy, please try again later"} + '100421': BadSymbol, # {"code":100421,"msg":"This pair is currently restricted from API trading","debugMsg":""} + '100440': ExchangeError, + '100500': OperationFailed, # {"code":100500,"msg":"The current system is busy, please try again later","debugMsg":""} + '100503': ExchangeError, + '80001': BadRequest, + '80012': InsufficientFunds, # {"code":80012,"msg":"{\"Code\":101253,\"Msg\":\"margin is not enough\"}} + '80014': BadRequest, + '80016': OrderNotFound, + '80017': OrderNotFound, + '100414': AccountSuspended, # {"code":100414,"msg":"Code: 100414, Msg: risk control check fail,code(1)","debugMsg":""} + '100419': PermissionDenied, # {"code":100419,"msg":"IP does not match IP whitelist","success":false,"timestamp":1705274099347} + '100437': BadRequest, # {"code":100437,"msg":"The withdrawal amount is lower than the minimum limit, please re-enter.","timestamp":1689258588845} + '101204': InsufficientFunds, # {"code":101204,"msg":"","data":{}} + '110425': InvalidOrder, # {"code":110425,"msg":"Please ensure that the minimum nominal value of the order placed must be greater than 2u","data":{}} + 'Insufficient assets': InsufficientFunds, # {"transferErrorMsg":"Insufficient assets"} + 'illegal transferType': BadRequest, # {"transferErrorMsg":"illegal transferType"} + }, + 'broad': {}, + }, + 'commonCurrencies': { + 'SNOW': 'Snowman', # Snowman vs SnowSwap conflict + 'OMNI': 'OmniCat', + 'NAP': '$NAP', # NAP on SOL = SNAP + 'TRUMP': 'TRUMPMAGA', + 'TRUMPSOL': 'TRUMP', + }, + 'options': { + 'defaultType': 'spot', + 'accountsByType': { + 'funding': 'fund', + 'spot': 'spot', + 'future': 'stdFutures', + 'swap': 'USDTMPerp', + 'linear': 'USDTMPerp', + 'inverse': 'coinMPerp', + }, + 'accountsById': { + 'fund': 'funding', + 'spot': 'spot', + 'stdFutures': 'future', + 'USDTMPerp': 'linear', + 'coinMPerp': 'inverse', + }, + 'recvWindow': 5 * 1000, # 5 sec + 'broker': 'CCXT', + 'defaultNetworks': { + 'ETH': 'ETH', + 'USDT': 'ERC20', + 'USDC': 'ERC20', + 'BTC': 'BTC', + 'LTC': 'LTC', + }, + 'networks': { + 'ARBITRUM': 'ARB', + 'MATIC': 'POLYGON', + 'ZKSYNC': 'ZKSYNCERA', + 'AVAXC': 'AVAX-C', + 'HBAR': 'HEDERA', + }, + }, + 'features': { + 'defaultForLinear': { + 'sandbox': True, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerPriceType': { + 'last': True, + 'mark': True, + 'index': True, + }, + 'triggerDirection': False, + 'stopLossPrice': True, + 'takeProfitPrice': True, + 'attachedStopLossTakeProfit': { + 'triggerPriceType': { + 'last': True, + 'mark': True, + 'index': True, + }, + 'price': True, + }, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': True, + 'trailing': True, + 'leverage': False, + 'marketBuyRequiresPrice': False, + 'marketBuyByCost': True, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': { + 'max': 5, + }, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 512, # 512 days for 'allFillOrders', 1000 days for 'fillOrders' + 'daysBack': 30, # 30 for 'allFillOrders', 7 for 'fillHistory' + 'untilDays': 30, # 30 for 'allFillOrders', 7 for 'fillHistory' + 'symbolRequired': True, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 20000, # since epoch + 'untilDays': 7, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': None, + 'daysBackCanceled': None, + 'untilDays': 7, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOHLCV': { + 'limit': 1440, + }, + }, + 'defaultForInverse': { + 'extends': 'defaultForLinear', + 'fetchMyTrades': { + 'limit': 1000, + 'daysBack': None, + 'untilDays': None, + }, + 'fetchOrders': None, + }, + # + 'spot': { + 'extends': 'defaultForLinear', + 'fetchCurrencies': { + 'private': True, + }, + 'createOrder': { + 'triggerPriceType': None, + 'attachedStopLossTakeProfit': None, + 'trailing': False, + }, + 'fetchMyTrades': { + 'limit': 1000, + 'daysBack': 1, + 'untilDays': 1, + }, + 'fetchOrders': None, + 'fetchClosedOrders': { + 'limit': 100, + 'untilDays': None, + }, + }, + 'swap': { + 'linear': { + 'extends': 'defaultForLinear', + }, + 'inverse': { + 'extends': 'defaultForInverse', + }, + }, + 'defaultForFuture': { + 'extends': 'defaultForLinear', + 'fetchOrders': None, + }, + 'future': { + 'linear': { + 'extends': 'defaultForFuture', + }, + 'inverse': { + 'extends': 'defaultForFuture', + }, + }, + }, + }) + + async def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the bingx server + + https://bingx-api.github.io/docs/#/swapV2/base-info.html#Get%20Server%20Time + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the bingx server + """ + response = await self.swapV2PublicGetServerTime(params) + # + # { + # "code": 0, + # "msg": "", + # "data": { + # "serverTime": 1675319535362 + # } + # } + # + data = self.safe_dict(response, 'data') + return self.safe_integer(data, 'serverTime') + + async def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://bingx-api.github.io/docs/#/common/account-api.html#All%20Coins + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + if not self.check_required_credentials(False): + return {} + isSandbox = self.safe_bool(self.options, 'sandboxMode', False) + if isSandbox: + return {} + response = await self.walletsV1PrivateGetCapitalConfigGetall(params) + # + # { + # "code": 0, + # "timestamp": 1702623271476, + # "data": [ + # { + # "coin": "BTC", + # "name": "BTC", + # "networkList": [ + # { + # "name": "BTC", + # "network": "BTC", + # "isDefault": True, + # "minConfirm": 2, + # "withdrawEnable": True, + # "depositEnable": True, + # "withdrawFee": "0.0006", + # "withdrawMax": "1.17522", + # "withdrawMin": "0.0005", + # "depositMin": "0.0002" + # }, + # { + # "name": "BTC", + # "network": "BEP20", + # "isDefault": False, + # "minConfirm": 15, + # "withdrawEnable": True, + # "depositEnable": True, + # "withdrawFee": "0.0000066", + # "withdrawMax": "1.17522", + # "withdrawMin": "0.0000066", + # "depositMin": "0.0002" + # } + # ] + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + result: dict = {} + for i in range(0, len(data)): + entry = data[i] + currencyId = self.safe_string(entry, 'coin') + code = self.safe_currency_code(currencyId) + name = self.safe_string(entry, 'name') + networkList = self.safe_list(entry, 'networkList') + networks: dict = {} + for j in range(0, len(networkList)): + rawNetwork = networkList[j] + network = self.safe_string(rawNetwork, 'network') + networkCode = self.network_id_to_code(network) + limits: dict = { + 'withdraw': { + 'min': self.safe_number(rawNetwork, 'withdrawMin'), + 'max': self.safe_number(rawNetwork, 'withdrawMax'), + }, + 'deposit': { + 'min': self.safe_number(rawNetwork, 'depositMin'), + 'max': None, + }, + } + precision = self.parse_number(self.parse_precision(self.safe_string(rawNetwork, 'withdrawPrecision'))) + networks[networkCode] = { + 'info': rawNetwork, + 'id': network, + 'network': networkCode, + 'fee': self.safe_number(rawNetwork, 'withdrawFee'), + 'active': None, + 'deposit': self.safe_bool(rawNetwork, 'depositEnable'), + 'withdraw': self.safe_bool(rawNetwork, 'withdrawEnable'), + 'precision': precision, + 'limits': limits, + } + if not (code in result): # the exchange could return the same currency with different networks + result[code] = { + 'info': entry, + 'code': code, + 'id': currencyId, + 'precision': None, + 'name': name, + 'active': None, + 'deposit': None, + 'withdraw': None, + 'networks': networks, + 'fee': None, + 'limits': None, + 'type': 'crypto', # only cryptos now + } + else: + existing = result[code] + existingNetworks = self.safe_dict(existing, 'networks', {}) + newNetworkCodes = list(networks.keys()) + for j in range(0, len(newNetworkCodes)): + newNetworkCode = newNetworkCodes[j] + if not (newNetworkCode in existingNetworks): + existingNetworks[newNetworkCode] = networks[newNetworkCode] + result[code]['networks'] = existingNetworks + codes = list(result.keys()) + for i in range(0, len(codes)): + code = codes[i] + currency = result[code] + result[code] = self.safe_currency_structure(currency) + return result + + async def fetch_spot_markets(self, params) -> List[Market]: + response = await self.spotV1PublicGetCommonSymbols(params) + # + # { + # "code": 0, + # "msg": "", + # "debugMsg": "", + # "data": { + # "symbols": [ + # { + # "symbol": "GEAR-USDT", + # "minQty": 735, # deprecated + # "maxQty": 2941177, # deprecated. + # "minNotional": 5, + # "maxNotional": 20000, + # "status": 1, + # "tickSize": 0.000001, + # "stepSize": 1, + # "apiStateSell": True, + # "apiStateBuy": True, + # "timeOnline": 0, + # "offTime": 0, + # "maintainTime": 0 + # }, + # ... + # ] + # } + # } + # + data = self.safe_dict(response, 'data') + markets = self.safe_list(data, 'symbols', []) + return self.parse_markets(markets) + + async def fetch_swap_markets(self, params): + response = await self.swapV2PublicGetQuoteContracts(params) + # + # { + # "code": 0, + # "msg": "", + # "data": [ + # { + # "contractId": "100", + # "symbol": "BTC-USDT", + # "size": "0.0001", + # "quantityPrecision": "4", + # "pricePrecision": "1", + # "feeRate": "0.0005", + # "makerFeeRate": "0.0002", + # "takerFeeRate": "0.0005", + # "tradeMinLimit": "0", + # "tradeMinQuantity": "0.0001", + # "tradeMinUSDT": "2", + # "maxLongLeverage": "125", + # "maxShortLeverage": "125", + # "currency": "USDT", + # "asset": "BTC", + # "status": "1", + # "apiStateOpen": "true", + # "apiStateClose": "true", + # "ensureTrigger": True, + # "triggerFeeRate": "0.00020000" + # }, + # ... + # ] + # } + # + markets = self.safe_list(response, 'data', []) + return self.parse_markets(markets) + + async def fetch_inverse_swap_markets(self, params): + response = await self.cswapV1PublicGetMarketContracts(params) + # + # { + # "code": 0, + # "msg": "", + # "timestamp": 1720074487610, + # "data": [ + # { + # "symbol": "BNB-USD", + # "pricePrecision": 2, + # "minTickSize": "10", + # "minTradeValue": "10", + # "minQty": "1.00000000", + # "status": 1, + # "timeOnline": 1713175200000 + # }, + # ] + # } + # + markets = self.safe_list(response, 'data', []) + return self.parse_markets(markets) + + def parse_market(self, market: dict) -> Market: + id = self.safe_string(market, 'symbol') + symbolParts = id.split('-') + baseId = symbolParts[0] + quoteId = symbolParts[1] + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + currency = self.safe_string(market, 'currency') + checkIsInverse = False + checkIsLinear = True + minTickSize = self.safe_number(market, 'minTickSize') + if minTickSize is not None: + # inverse swap market + currency = baseId + checkIsInverse = True + checkIsLinear = False + settle = self.safe_currency_code(currency) + pricePrecision = self.safe_number(market, 'tickSize') + if pricePrecision is None: + pricePrecision = self.parse_number(self.parse_precision(self.safe_string(market, 'pricePrecision'))) + quantityPrecision = self.safe_number(market, 'stepSize') + if quantityPrecision is None: + quantityPrecision = self.parse_number(self.parse_precision(self.safe_string(market, 'quantityPrecision'))) + type = 'swap' if (settle is not None) else 'spot' + spot = type == 'spot' + swap = type == 'swap' + symbol = base + '/' + quote + if settle is not None: + symbol += ':' + settle + fees = self.safe_dict(self.fees, type, {}) + contractSize = self.parse_number('1') if (swap) else None + isActive = False + if (self.safe_string(market, 'apiStateOpen') == 'true') and (self.safe_string(market, 'apiStateClose') == 'true'): + isActive = True # swap active + elif self.safe_bool(market, 'apiStateSell') and self.safe_bool(market, 'apiStateBuy') and (self.safe_string(market, 'status') == '1'): + isActive = True # spot active + isInverse = None if (spot) else checkIsInverse + isLinear = None if (spot) else checkIsLinear + minAmount = None + if not spot: + minAmount = self.safe_number_2(market, 'minQty', 'tradeMinQuantity') + timeOnline = self.safe_integer(market, 'timeOnline') + if timeOnline == 0: + timeOnline = None + return self.safe_market_structure({ + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': currency, + 'type': type, + 'spot': spot, + 'margin': False, + 'swap': swap, + 'future': False, + 'option': False, + 'active': isActive, + 'contract': swap, + 'linear': isLinear, + 'inverse': isInverse, + 'taker': self.safe_number(fees, 'taker'), + 'maker': self.safe_number(fees, 'maker'), + 'feeSide': self.safe_string(fees, 'feeSide'), + 'contractSize': contractSize, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': quantityPrecision, + 'price': pricePrecision, + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': minAmount, + 'max': None, + }, + 'price': { + 'min': minTickSize, + 'max': None, + }, + 'cost': { + 'min': self.safe_number_n(market, ['minNotional', 'tradeMinUSDT', 'minTradeValue']), + 'max': self.safe_number(market, 'maxNotional'), + }, + }, + 'created': timeOnline, + 'info': market, + }) + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for bingx + + https://bingx-api.github.io/docs/#/spot/market-api.html#Query%20Symbols + https://bingx-api.github.io/docs/#/swapV2/market-api.html#Contract%20Information + https://bingx-api.github.io/docs/#/en-us/cswap/market-api.html#Contract%20Information + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + requests = [self.fetch_swap_markets(params)] + isSandbox = self.safe_bool(self.options, 'sandboxMode', False) + if not isSandbox: + requests.append(self.fetch_inverse_swap_markets(params)) + requests.append(self.fetch_spot_markets(params)) # sandbox is swap only + promises = await asyncio.gather(*requests) + linearSwapMarkets = self.safe_list(promises, 0, []) + inverseSwapMarkets = self.safe_list(promises, 1, []) + spotMarkets = self.safe_list(promises, 2, []) + swapMarkets = self.array_concat(linearSwapMarkets, inverseSwapMarkets) + return self.array_concat(spotMarkets, swapMarkets) + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://bingx-api.github.io/docs/#/swapV2/market-api.html#K-Line%20Data + https://bingx-api.github.io/docs/#/spot/market-api.html#Candlestick%20chart%20data + https://bingx-api.github.io/docs/#/swapV2/market-api.html#%20K-Line%20Data + https://bingx-api.github.io/docs/#/en-us/swapV2/market-api.html#Mark%20Price%20Kline/Candlestick%20Data + https://bingx-api.github.io/docs/#/en-us/cswap/market-api.html#Get%20K-line%20Data + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest candle to fetch + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate', False) + if paginate: + return await self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params, 1440) + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + request['interval'] = self.safe_string(self.timeframes, timeframe, timeframe) + if since is not None: + request['startTime'] = max(since - 1, 0) + if limit is not None: + request['limit'] = limit + until = self.safe_integer_2(params, 'until', 'endTime') + if until is not None: + params = self.omit(params, ['until']) + request['endTime'] = until + response = None + if market['spot']: + response = await self.spotV1PublicGetMarketKline(self.extend(request, params)) + else: + if market['inverse']: + response = await self.cswapV1PublicGetMarketKlines(self.extend(request, params)) + else: + price = self.safe_string(params, 'price') + params = self.omit(params, 'price') + if price == 'mark': + response = await self.swapV1PublicGetMarketMarkPriceKlines(self.extend(request, params)) + else: + response = await self.swapV3PublicGetQuoteKlines(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "data": [ + # { + # "open": "19396.8", + # "close": "19394.4", + # "high": "19397.5", + # "low": "19385.7", + # "volume": "110.05", + # "time": 1666583700000 + # }, + # ... + # ] + # } + # + # fetchMarkOHLCV + # + # { + # "code": 0, + # "msg": "", + # "data": [ + # { + # "open": "42191.7", + # "close": "42189.5", + # "high": "42196.5", + # "low": "42189.5", + # "volume": "0.00", + # "openTime": 1706508840000, + # "closeTime": 1706508840000 + # } + # ] + # } + # + ohlcvs = self.safe_value(response, 'data', []) + if not isinstance(ohlcvs, list): + ohlcvs = [ohlcvs] + return self.parse_ohlcvs(ohlcvs, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # { + # "open": "19394.4", + # "close": "19379.0", + # "high": "19394.4", + # "low": "19368.3", + # "volume": "167.44", + # "time": 1666584000000 + # } + # + # fetchMarkOHLCV + # + # { + # "open": "42191.7", + # "close": "42189.5", + # "high": "42196.5", + # "low": "42189.5", + # "volume": "0.00", + # "openTime": 1706508840000, + # "closeTime": 1706508840000 + # } + # spot + # [ + # 1691402580000, + # 29093.61, + # 29093.93, + # 29087.73, + # 29093.24, + # 0.59, + # 1691402639999, + # 17221.07 + # ] + # + if isinstance(ohlcv, list): + return [ + self.safe_integer(ohlcv, 0), + self.safe_number(ohlcv, 1), + self.safe_number(ohlcv, 2), + self.safe_number(ohlcv, 3), + self.safe_number(ohlcv, 4), + self.safe_number(ohlcv, 5), + ] + return [ + self.safe_integer_2(ohlcv, 'time', 'closeTime'), + self.safe_number(ohlcv, 'open'), + self.safe_number(ohlcv, 'high'), + self.safe_number(ohlcv, 'low'), + self.safe_number(ohlcv, 'close'), + self.safe_number(ohlcv, 'volume'), + ] + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://bingx-api.github.io/docs/#/spot/market-api.html#Query%20transaction%20records + https://bingx-api.github.io/docs/#/swapV2/market-api.html#The%20latest%20Trade%20of%20a%20Trading%20Pair + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['limit'] = min(limit, 100) # avoid API exception "limit should less than 100" + response = None + marketType = None + marketType, params = self.handle_market_type_and_params('fetchTrades', market, params) + if marketType == 'spot': + response = await self.spotV1PublicGetMarketTrades(self.extend(request, params)) + else: + response = await self.swapV2PublicGetQuoteTrades(self.extend(request, params)) + # + # spot + # + # { + # "code": 0, + # "data": [ + # { + # "id": 43148253, + # "price": 25714.71, + # "qty": 1.674571, + # "time": 1655085975589, + # "buyerMaker": False + # } + # ] + # } + # + # swap + # + # { + # "code":0, + # "msg":"", + # "data":[ + # { + # "time": 1672025549368, + # "isBuyerMaker": True, + # "price": "16885.0", + # "qty": "3.3002", + # "quoteQty": "55723.87" + # }, + # ... + # ] + # } + # + trades = self.safe_list(response, 'data', []) + return self.parse_trades(trades, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # spot fetchTrades + # + # { + # "id": 43148253, + # "price": 25714.71, + # "qty": 1.674571, + # "time": 1655085975589, + # "buyerMaker": False + # } + # + # spot fetchMyTrades + # + # { + # "symbol": "LTC-USDT", + # "id": 36237072, + # "orderId": 1674069326895775744, + # "price": "85.891", + # "qty": "0.0582", + # "quoteQty": "4.9988562000000005", + # "commission": -0.00005820000000000001, + # "commissionAsset": "LTC", + # "time": 1687964205000, + # "isBuyer": True, + # "isMaker": False + # } + # + # swap fetchTrades + # + # { + # "time": 1672025549368, + # "isBuyerMaker": True, + # "price": "16885.0", + # "qty": "3.3002", + # "quoteQty": "55723.87" + # } + # + # swap fetchMyTrades + # + # { + # "volume": "0.1", + # "price": "106.75", + # "amount": "10.6750", + # "commission": "-0.0053", + # "currency": "USDT", + # "orderId": "1676213270274379776", + # "liquidatedPrice": "0.00", + # "liquidatedMarginRatio": "0.00", + # "filledTime": "2023-07-04T20:56:01.000+0800" + # } + # + # ws spot + # + # { + # "E": 1690214529432, + # "T": 1690214529386, + # "e": "trade", + # "m": True, + # "p": "29110.19", + # "q": "0.1868", + # "s": "BTC-USDT", + # "t": "57903921" + # } + # + # ws linear swap + # + # { + # "q": "0.0421", + # "p": "29023.5", + # "T": 1690221401344, + # "m": False, + # "s": "BTC-USDT" + # } + # + # ws inverse swap + # + # { + # "e": "trade", + # "E": 1722920589665, + # "s": "BTC-USD", + # "t": "39125001", + # "p": "55360.0", + # "q": "1", + # "T": 1722920589582, + # "m": False + # } + # + # inverse swap fetchMyTrades + # + # { + # "orderId": "1817441228670648320", + # "symbol": "SOL-USD", + # "type": "MARKET", + # "side": "BUY", + # "positionSide": "LONG", + # "tradeId": "97244554", + # "volume": "2", + # "tradePrice": "182.652", + # "amount": "20.00000000", + # "realizedPnl": "0.00000000", + # "commission": "-0.00005475", + # "currency": "SOL", + # "buyer": True, + # "maker": False, + # "tradeTime": 1722146730000 + # } + # + time = self.safe_integer_n(trade, ['time', 'filledTm', 'T', 'tradeTime']) + datetimeId = self.safe_string(trade, 'filledTm') + if datetimeId is not None: + time = self.parse8601(datetimeId) + if time == 0: + time = None + cost = self.safe_string(trade, 'quoteQty') + # type = 'spot' if (cost is None) else 'swap'; self is not reliable + currencyId = self.safe_string_n(trade, ['currency', 'N', 'commissionAsset']) + currencyCode = self.safe_currency_code(currencyId) + m = self.safe_bool(trade, 'm') + marketId = self.safe_string_2(trade, 's', 'symbol') + isBuyerMaker = self.safe_bool_n(trade, ['buyerMaker', 'isBuyerMaker', 'maker']) + takeOrMaker = None + if (isBuyerMaker is not None) or (m is not None): + takeOrMaker = 'maker' if (isBuyerMaker or m) else 'taker' + side = self.safe_string_lower_2(trade, 'side', 'S') + if side is None: + if (isBuyerMaker is not None) or (m is not None): + side = 'sell' if (isBuyerMaker or m) else 'buy' + takeOrMaker = 'taker' + isBuyer = self.safe_bool(trade, 'isBuyer') + if isBuyer is not None: + side = 'buy' if isBuyer else 'sell' + isMaker = self.safe_bool(trade, 'isMaker') + if isMaker is not None: + takeOrMaker = 'maker' if isMaker else 'taker' + amount = self.safe_string_n(trade, ['qty', 'amount', 'q']) + if (market is not None) and market['swap'] and ('volume' in trade): + # private trade returns num of contracts instead of base currency(as the order-related methods do) + contractSize = self.safe_string(market['info'], 'tradeMinQuantity') + volume = self.safe_string(trade, 'volume') + amount = Precise.string_mul(volume, contractSize) + return self.safe_trade({ + 'id': self.safe_string_n(trade, ['id', 't']), + 'info': trade, + 'timestamp': time, + 'datetime': self.iso8601(time), + 'symbol': self.safe_symbol(marketId, market, '-'), + 'order': self.safe_string_2(trade, 'orderId', 'i'), + 'type': self.safe_string_lower(trade, 'o'), + 'side': self.parse_order_side(side), + 'takerOrMaker': takeOrMaker, + 'price': self.safe_string_n(trade, ['price', 'p', 'tradePrice']), + 'amount': amount, + 'cost': cost, + 'fee': { + 'cost': self.parse_number(Precise.string_abs(self.safe_string_2(trade, 'commission', 'n'))), + 'currency': currencyCode, + }, + }, market) + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://bingx-api.github.io/docs/#/spot/market-api.html#Query%20depth%20information + https://bingx-api.github.io/docs/#/swapV2/market-api.html#Get%20Market%20Depth + https://bingx-api.github.io/docs/#/en-us/cswap/market-api.html#Query%20Depth%20Data + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['limit'] = limit + response = None + marketType = None + marketType, params = self.handle_market_type_and_params('fetchOrderBook', market, params) + if marketType == 'spot': + response = await self.spotV1PublicGetMarketDepth(self.extend(request, params)) + else: + if market['inverse']: + response = await self.cswapV1PublicGetMarketDepth(self.extend(request, params)) + else: + response = await self.swapV2PublicGetQuoteDepth(self.extend(request, params)) + # + # spot + # + # { + # "code":0, + # "timestamp":1743240504535, + # "data":{ + # "bids":[ + # ["83775.39","1.981875"], + # ["83775.38","0.001076"], + # ["83775.34","0.254716"], + # ], + # "asks":[ + # ["83985.40","0.000013"], + # ["83980.00","0.000011"], + # ["83975.70","0.000061000000000000005"], + # ], + # "ts":1743240504535, + # "lastUpdateId":13565639906 + # } + # } + # + # + # linear swap + # + # { + # "code":0, + # "msg":"", + # "data":{ + # "T":1743240836255, + # "bids":[ + # ["83760.7","7.0861"], + # ["83760.6","0.0044"], + # ["83757.7","1.9526"], + # ], + # "asks":[ + # ["83784.3","8.3531"], + # ["83782.8","23.7289"], + # ["83780.1","18.0617"], + # ], + # "bidsCoin":[ + # ["83760.7","0.0007"], + # ["83760.6","0.0000"], + # ["83757.7","0.0002"], + # ], + # "asksCoin":[ + # ["83784.3","0.0008"], + # ["83782.8","0.0024"], + # ["83780.1","0.0018"], + # ] + # } + # } + # + # inverse swap + # + # { + # "code":0, + # "msg":"", + # "timestamp":1743240979146, + # "data":{ + # "T":1743240978691, + # "bids":[ + # ["83611.4","241.0"], + # ["83611.3","1.0"], + # ["83602.9","666.0"], + # ], + # "asks":[ + # ["83645.0","4253.0"], + # ["83640.5","3188.0"], + # ["83636.0","5540.0"], + # ] + # } + # } + # + orderbook = self.safe_dict(response, 'data', {}) + nonce = self.safe_integer(orderbook, 'lastUpdateId') + timestamp = self.safe_integer_2(orderbook, 'T', 'ts') + result = self.parse_order_book(orderbook, market['symbol'], timestamp, 'bids', 'asks', 0, 1) + result['nonce'] = nonce + return result + + async def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate + + https://bingx-api.github.io/docs/#/swapV2/market-api.html#Current%20Funding%20Rate + https://bingx-api.github.io/docs/#/en-us/cswap/market-api.html#Price%20&%20Current%20Funding%20Rate + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = None + if market['inverse']: + response = await self.cswapV1PublicGetMarketPremiumIndex(self.extend(request, params)) + else: + response = await self.swapV2PublicGetQuotePremiumIndex(self.extend(request, params)) + # + # { + # "code":0, + # "msg":"", + # "data":[ + # { + # "symbol": "BTC-USDT", + # "markPrice": "16884.5", + # "indexPrice": "16886.9", + # "lastFundingRate": "0.0001", + # "nextFundingTime": 1672041600000 + # }, + # ... + # ] + # } + # + data = self.safe_dict(response, 'data') + return self.parse_funding_rate(data, market) + + async def fetch_funding_rates(self, symbols: Strings = None, params={}) -> FundingRates: + """ + fetch the current funding rate for multiple symbols + + https://bingx-api.github.io/docs/#/swapV2/market-api.html#Current%20Funding%20Rate + + :param str[] [symbols]: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `funding rate structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, 'swap', True) + response = await self.swapV2PublicGetQuotePremiumIndex(self.extend(params)) + data = self.safe_list(response, 'data', []) + return self.parse_funding_rates(data, symbols) + + def parse_funding_rate(self, contract, market: Market = None) -> FundingRate: + # + # { + # "symbol": "BTC-USDT", + # "markPrice": "16884.5", + # "indexPrice": "16886.9", + # "lastFundingRate": "0.0001", + # "nextFundingTime": 1672041600000 + # } + # + marketId = self.safe_string(contract, 'symbol') + nextFundingTimestamp = self.safe_integer(contract, 'nextFundingTime') + return { + 'info': contract, + 'symbol': self.safe_symbol(marketId, market, '-', 'swap'), + 'markPrice': self.safe_number(contract, 'markPrice'), + 'indexPrice': self.safe_number(contract, 'indexPrice'), + 'interestRate': None, + 'estimatedSettlePrice': None, + 'timestamp': None, + 'datetime': None, + 'fundingRate': self.safe_number(contract, 'lastFundingRate'), + 'fundingTimestamp': None, + 'fundingDatetime': None, + 'nextFundingRate': None, + 'nextFundingTimestamp': nextFundingTimestamp, + 'nextFundingDatetime': self.iso8601(nextFundingTimestamp), + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': None, + } + + async def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical funding rate prices + + https://bingx-api.github.io/docs/#/swapV2/market-api.html#Funding%20Rate%20History + + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: timestamp in ms of the earliest funding rate to fetch + :param int [limit]: the maximum amount of `funding rate structures ` to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest funding rate to fetch + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `funding rate structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchFundingRateHistory() requires a symbol argument') + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchFundingRateHistory', 'paginate') + if paginate: + return await self.fetch_paginated_call_deterministic('fetchFundingRateHistory', symbol, since, limit, '8h', params) + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + until = self.safe_integer_2(params, 'until', 'startTime') + if until is not None: + params = self.omit(params, ['until']) + request['startTime'] = until + response = await self.swapV2PublicGetQuoteFundingRate(self.extend(request, params)) + # + # { + # "code":0, + # "msg":"", + # "data":[ + # { + # "symbol": "BTC-USDT", + # "fundingRate": "0.0001", + # "fundingTime": 1585684800000 + # }, + # ... + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_funding_rate_histories(data, market, since, limit) + + def parse_funding_rate_history(self, contract, market: Market = None): + # + # { + # "symbol": "BTC-USDT", + # "fundingRate": "0.0001", + # "fundingTime": 1585684800000 + # } + # + timestamp = self.safe_integer(contract, 'fundingTime') + return { + 'info': contract, + 'symbol': self.safe_symbol(self.safe_string(contract, 'symbol'), market, '-', 'swap'), + 'fundingRate': self.safe_number(contract, 'fundingRate'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + + async def fetch_open_interest(self, symbol: str, params={}): + """ + retrieves the open interest of a trading pair + + https://bingx-api.github.io/docs/#/swapV2/market-api.html#Get%20Swap%20Open%20Positions + https://bingx-api.github.io/docs/#/en-us/cswap/market-api.html#Get%20Swap%20Open%20Positions + + :param str symbol: unified CCXT market symbol + :param dict [params]: exchange specific parameters + :returns dict} an open interest structure{@link https://docs.ccxt.com/#/?id=open-interest-structure: + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = None + if market['inverse']: + response = await self.cswapV1PublicGetMarketOpenInterest(self.extend(request, params)) + else: + response = await self.swapV2PublicGetQuoteOpenInterest(self.extend(request, params)) + # + # linear swap + # + # { + # "code": 0, + # "msg": "", + # "data": { + # "openInterest": "3289641547.10", + # "symbol": "BTC-USDT", + # "time": 1672026617364 + # } + # } + # + # inverse swap + # + # { + # "code": 0, + # "msg": "", + # "timestamp": 1720328247986, + # "data": [ + # { + # "symbol": "BTC-USD", + # "openInterest": "749.1160", + # "timestamp": 1720310400000 + # } + # ] + # } + # + result: dict = {} + if market['inverse']: + data = self.safe_list(response, 'data', []) + result = self.safe_dict(data, 0, {}) + else: + result = self.safe_dict(response, 'data', {}) + return self.parse_open_interest(result, market) + + def parse_open_interest(self, interest, market: Market = None): + # + # linear swap + # + # { + # "openInterest": "3289641547.10", + # "symbol": "BTC-USDT", + # "time": 1672026617364 + # } + # + # inverse swap + # + # { + # "symbol": "BTC-USD", + # "openInterest": "749.1160", + # "timestamp": 1720310400000 + # } + # + timestamp = self.safe_integer_2(interest, 'time', 'timestamp') + id = self.safe_string(interest, 'symbol') + symbol = self.safe_symbol(id, market, '-', 'swap') + openInterest = self.safe_number(interest, 'openInterest') + return self.safe_open_interest({ + 'symbol': symbol, + 'baseVolume': None, + 'quoteVolume': None, # deprecated + 'openInterestAmount': None, + 'openInterestValue': openInterest, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'info': interest, + }, market) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://bingx-api.github.io/docs/#/en-us/swapV2/market-api.html#Get%20Ticker + https://bingx-api.github.io/docs/#/en-us/spot/market-api.html#24-hour%20price%20changes + https://bingx-api.github.io/docs/#/en-us/cswap/market-api.html#Query%2024-Hour%20Price%20Change + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = None + if market['spot']: + response = await self.spotV1PublicGetTicker24hr(self.extend(request, params)) + else: + if market['inverse']: + response = await self.cswapV1PublicGetMarketTicker(self.extend(request, params)) + else: + response = await self.swapV2PublicGetQuoteTicker(self.extend(request, params)) + # + # spot and swap + # + # { + # "code": 0, + # "msg": "", + # "timestamp": 1720647285296, + # "data": [ + # { + # "symbol": "SOL-USD", + # "priceChange": "-2.418", + # "priceChangePercent": "-1.6900%", + # "lastPrice": "140.574", + # "lastQty": "1", + # "highPrice": "146.190", + # "lowPrice": "138.586", + # "volume": "1464648.00", + # "quoteVolume": "102928.12", + # "openPrice": "142.994", + # "closeTime": "1720647284976", + # "bidPrice": "140.573", + # "bidQty": "372", + # "askPrice": "140.577", + # "askQty": "58" + # } + # ] + # } + # + data = self.safe_list(response, 'data') + if data is not None: + first = self.safe_dict(data, 0, {}) + return self.parse_ticker(first, market) + dataDict = self.safe_dict(response, 'data', {}) + return self.parse_ticker(dataDict, market) + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://bingx-api.github.io/docs/#/en-us/swapV2/market-api.html#Get%20Ticker + https://bingx-api.github.io/docs/#/en-us/spot/market-api.html#24-hour%20price%20changes + https://bingx-api.github.io/docs/#/en-us/cswap/market-api.html#Query%2024-Hour%20Price%20Change + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + market = None + if symbols is not None: + symbols = self.market_symbols(symbols) + firstSymbol = self.safe_string(symbols, 0) + if firstSymbol is not None: + market = self.market(firstSymbol) + type = None + type, params = self.handle_market_type_and_params('fetchTickers', market, params) + subType = None + subType, params = self.handle_sub_type_and_params('fetchTickers', market, params) + response = None + if type == 'spot': + response = await self.spotV1PublicGetTicker24hr(params) + else: + if subType == 'inverse': + response = await self.cswapV1PublicGetMarketTicker(params) + else: + response = await self.swapV2PublicGetQuoteTicker(params) + # + # spot and swap + # + # { + # "code": 0, + # "msg": "", + # "timestamp": 1720647285296, + # "data": [ + # { + # "symbol": "SOL-USD", + # "priceChange": "-2.418", + # "priceChangePercent": "-1.6900%", + # "lastPrice": "140.574", + # "lastQty": "1", + # "highPrice": "146.190", + # "lowPrice": "138.586", + # "volume": "1464648.00", + # "quoteVolume": "102928.12", + # "openPrice": "142.994", + # "closeTime": "1720647284976", + # "bidPrice": "140.573", + # "bidQty": "372", + # "askPrice": "140.577", + # "askQty": "58" + # }, + # ... + # ] + # } + # + tickers = self.safe_list(response, 'data') + return self.parse_tickers(tickers, symbols) + + async def fetch_mark_price(self, symbol: str, params={}) -> Ticker: + """ + fetches mark prices for the market + + https://bingx-api.github.io/docs/#/en-us/swapV2/market-api.html#Mark%20Price%20and%20Funding%20Rate + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + market = self.market(symbol) + subType = None + subType, params = self.handle_sub_type_and_params('fetchMarkPrice', market, params, 'linear') + request = { + 'symbol': market['id'], + } + response = None + if subType == 'inverse': + response = await self.cswapV1PublicGetMarketPremiumIndex(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "timestamp": 1728577213289, + # "data": [ + # { + # "symbol": "ETH-USD", + # "lastFundingRate": "0.0001", + # "markPrice": "2402.68", + # "indexPrice": "2404.92", + # "nextFundingTime": 1728604800000 + # } + # ] + # } + # + else: + response = await self.swapV2PublicGetQuotePremiumIndex(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "data": { + # "symbol": "ETH-USDT", + # "markPrice": "2408.40", + # "indexPrice": "2409.62", + # "lastFundingRate": "0.00009900", + # "nextFundingTime": 1728604800000 + # } + # } + # + if isinstance(response['data'], list): + return self.parse_ticker(self.safe_dict(response['data'], 0, {}), market) + return self.parse_ticker(response['data'], market) + + async def fetch_mark_prices(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches mark prices for multiple markets + + https://bingx-api.github.io/docs/#/en-us/swapV2/market-api.html#Mark%20Price%20and%20Funding%20Rate + + :param str[] [symbols]: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + market = None + if symbols is not None: + symbols = self.market_symbols(symbols) + firstSymbol = self.safe_string(symbols, 0) + if firstSymbol is not None: + market = self.market(firstSymbol) + subType = None + subType, params = self.handle_sub_type_and_params('fetchMarkPrices', market, params, 'linear') + response = None + if subType == 'inverse': + response = await self.cswapV1PublicGetMarketPremiumIndex(params) + else: + response = await self.swapV2PublicGetQuotePremiumIndex(params) + # + # spot and swap + # + # { + # "code": 0, + # "msg": "", + # "timestamp": 1720647285296, + # "data": [ + # { + # "symbol": "SOL-USD", + # "priceChange": "-2.418", + # "priceChangePercent": "-1.6900%", + # "lastPrice": "140.574", + # "lastQty": "1", + # "highPrice": "146.190", + # "lowPrice": "138.586", + # "volume": "1464648.00", + # "quoteVolume": "102928.12", + # "openPrice": "142.994", + # "closeTime": "1720647284976", + # "bidPrice": "140.573", + # "bidQty": "372", + # "askPrice": "140.577", + # "askQty": "58" + # }, + # ... + # ] + # } + # + tickers = self.safe_list(response, 'data') + return self.parse_tickers(tickers, symbols) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # mark price + # { + # "symbol": "string", + # "lastFundingRate": "string", + # "markPrice": "string", + # "indexPrice": "string", + # "nextFundingTime": "int64" + # } + # + # spot + # { + # "symbol": "BTC-USDT", + # "openPrice": "26032.08", + # "highPrice": "26178.86", + # "lowPrice": "25968.18", + # "lastPrice": "26113.60", + # "volume": "1161.79", + # "quoteVolume": "30288466.44", + # "openTime": "1693081020762", + # "closeTime": "1693167420762", + # added 2023-11-10: + # "bidPrice": 16726.0, + # "bidQty": 0.05, + # "askPrice": 16726.0, + # "askQty": 0.05, + # } + # swap + # + # { + # "symbol": "BTC-USDT", + # "priceChange": "52.5", + # "priceChangePercent": "0.31%", # they started to add the percent sign in value + # "lastPrice": "16880.5", + # "lastQty": "2.2238", # only present in swap! + # "highPrice": "16897.5", + # "lowPrice": "16726.0", + # "volume": "245870.1692", + # "quoteVolume": "4151395117.73", + # "openPrice": "16832.0", + # "openTime": 1672026667803, + # "closeTime": 1672026648425, + # added 2023-11-10: + # "bidPrice": 16726.0, + # "bidQty": 0.05, + # "askPrice": 16726.0, + # "askQty": 0.05, + # } + # + marketId = self.safe_string(ticker, 'symbol') + lastQty = self.safe_string(ticker, 'lastQty') + # in spot markets, lastQty is not present + # it's(bad, but) the only way we can check the tickers origin + type = 'spot' if (lastQty is None) else 'swap' + market = self.safe_market(marketId, market, None, type) + symbol = market['symbol'] + open = self.safe_string(ticker, 'openPrice') + high = self.safe_string(ticker, 'highPrice') + low = self.safe_string(ticker, 'lowPrice') + close = self.safe_string(ticker, 'lastPrice') + quoteVolume = self.safe_string(ticker, 'quoteVolume') + baseVolume = self.safe_string(ticker, 'volume') + percentage = self.safe_string(ticker, 'priceChangePercent') + if percentage is not None: + percentage = percentage.replace('%', '') + change = self.safe_string(ticker, 'priceChange') + ts = self.safe_integer(ticker, 'closeTime') + if ts == 0: + ts = None + datetime = self.iso8601(ts) + bid = self.safe_string(ticker, 'bidPrice') + bidVolume = self.safe_string(ticker, 'bidQty') + ask = self.safe_string(ticker, 'askPrice') + askVolume = self.safe_string(ticker, 'askQty') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': ts, + 'datetime': datetime, + 'high': high, + 'low': low, + 'bid': bid, + 'bidVolume': bidVolume, + 'ask': ask, + 'askVolume': askVolume, + 'vwap': None, + 'open': open, + 'close': close, + 'last': None, + 'previousClose': None, + 'change': change, + 'percentage': percentage, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'markPrice': self.safe_string(ticker, 'markPrice'), + 'indexPrice': self.safe_string(ticker, 'indexPrice'), + 'info': ticker, + }, market) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://bingx-api.github.io/docs/#/spot/trade-api.html#Query%20Assets + https://bingx-api.github.io/docs/#/en-us/swapV2/account-api.html#Query%20account%20data + https://bingx-api.github.io/docs/#/standard/contract-interface.html#Query%20standard%20contract%20balance + https://bingx-api.github.io/docs/#/en-us/cswap/trade-api.html#Query%20Account%20Assets + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.standard]: whether to fetch standard contract balances + :param str [params.type]: the type of balance to fetch(spot, swap, funding) default is `spot` + :returns dict: a `balance structure ` + """ + await self.load_markets() + response = None + standard = None + standard, params = self.handle_option_and_params(params, 'fetchBalance', 'standard', False) + subType = None + subType, params = self.handle_sub_type_and_params('fetchBalance', None, params) + marketType, marketTypeQuery = self.handle_market_type_and_params('fetchBalance', None, params) + if standard: + response = await self.contractV1PrivateGetBalance(marketTypeQuery) + # + # { + # "code": 0, + # "timestamp": 1721192833454, + # "data": [ + # { + # "asset": "USDT", + # "balance": "4.72644300000000000000", + # "crossWalletBalance": "4.72644300000000000000", + # "crossUnPnl": "0", + # "availableBalance": "4.72644300000000000000", + # "maxWithdrawAmount": "4.72644300000000000000", + # "marginAvailable": False, + # "updateTime": 1721192833443 + # }, + # ] + # } + # + elif (marketType == 'funding') or (marketType == 'fund'): + response = await self.fundV1PrivateGetAccountBalance(marketTypeQuery) + # { + # code: '0', + # timestamp: '1754906016631', + # data: { + # assets: [ + # { + # asset: 'USDT', + # free: '44.37692200000000237300', + # locked: '0.00000000000000000000' + # } + # ] + # } + # } + elif marketType == 'spot': + response = await self.spotV1PrivateGetAccountBalance(marketTypeQuery) + # + # { + # "code": 0, + # "msg": "", + # "debugMsg": "", + # "data": { + # "balances": [ + # { + # "asset": "USDT", + # "free": "45.733046995800514", + # "locked": "0" + # }, + # ] + # } + # } + # + else: + if subType == 'inverse': + response = await self.cswapV1PrivateGetUserBalance(marketTypeQuery) + # + # { + # "code": 0, + # "msg": "", + # "timestamp": 1721191833813, + # "data": [ + # { + # "asset": "SOL", + # "balance": "0.35707951", + # "equity": "0.35791051", + # "unrealizedProfit": "0.00083099", + # "availableMargin": "0.35160653", + # "usedMargin": "0.00630397", + # "freezedMargin": "0", + # "shortUid": "12851936" + # } + # ] + # } + # + else: + response = await self.swapV3PrivateGetUserBalance(marketTypeQuery) + # + # { + # "code": 0, + # "msg": "", + # "data": [ + # { + # "userId": "116***295", + # "asset": "USDT", + # "balance": "194.8212", + # "equity": "196.7431", + # "unrealizedProfit": "1.9219", + # "realisedProfit": "-109.2504", + # "availableMargin": "193.7609", + # "usedMargin": "1.0602", + # "freezedMargin": "0.0000", + # "shortUid": "12851936" + # } + # ] + # } + return self.parse_balance(response) + + def parse_balance(self, response) -> Balances: + # + # standard + # + # { + # "code": 0, + # "timestamp": 1721192833454, + # "data": [ + # { + # "asset": "USDT", + # "balance": "4.72644300000000000000", + # "crossWalletBalance": "4.72644300000000000000", + # "crossUnPnl": "0", + # "availableBalance": "4.72644300000000000000", + # "maxWithdrawAmount": "4.72644300000000000000", + # "marginAvailable": False, + # "updateTime": 1721192833443 + # }, + # ] + # } + # + # spot + # + # { + # "code": 0, + # "msg": "", + # "debugMsg": "", + # "data": { + # "balances": [ + # { + # "asset": "USDT", + # "free": "45.733046995800514", + # "locked": "0" + # }, + # ] + # } + # } + # + # inverse swap + # + # { + # "code": 0, + # "msg": "", + # "timestamp": 1721191833813, + # "data": [ + # { + # "asset": "SOL", + # "balance": "0.35707951", + # "equity": "0.35791051", + # "unrealizedProfit": "0.00083099", + # "availableMargin": "0.35160653", + # "usedMargin": "0.00630397", + # "freezedMargin": "0", + # "shortUid": "12851936" + # } + # ] + # } + # + # linear swap + # + # { + # "code": 0, + # "msg": "", + # "data": [ + # { + # "userId": "116***295", + # "asset": "USDT", + # "balance": "194.8212", + # "equity": "196.7431", + # "unrealizedProfit": "1.9219", + # "realisedProfit": "-109.2504", + # "availableMargin": "193.7609", + # "usedMargin": "1.0602", + # "freezedMargin": "0.0000", + # "shortUid": "12851936" + # } + # ] + # } + # + result: dict = {'info': response} + contractBalances = self.safe_list(response, 'data') + firstContractBalances = self.safe_dict(contractBalances, 0) + isContract = firstContractBalances is not None + spotData = self.safe_dict(response, 'data', {}) + spotBalances = self.safe_list_2(spotData, 'balances', 'assets', []) + if isContract: + for i in range(0, len(contractBalances)): + balance = contractBalances[i] + currencyId = self.safe_string(balance, 'asset') + if currencyId is None: # linear v3 returns empty asset + break + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string_2(balance, 'availableMargin', 'availableBalance') + account['used'] = self.safe_string(balance, 'usedMargin') + account['total'] = self.safe_string(balance, 'maxWithdrawAmount') + result[code] = account + else: + for i in range(0, len(spotBalances)): + balance = spotBalances[i] + currencyId = self.safe_string(balance, 'asset') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(balance, 'free') + account['used'] = self.safe_string(balance, 'locked') + result[code] = account + return self.safe_balance(result) + + async def fetch_position_history(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Position]: + """ + fetches historical positions + + https://bingx-api.github.io/docs/#/en-us/swapV2/trade-api.html#Query%20Position%20History + + :param str symbol: unified contract symbol + :param int [since]: the earliest time in ms to fetch positions for + :param int [limit]: the maximum amount of records to fetch + :param dict [params]: extra parameters specific to the exchange api endpoint + :param int [params.until]: the latest time in ms to fetch positions for + :returns dict[]: a list of `position structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['pageSize'] = limit + if since is not None: + request['startTs'] = since + request, params = self.handle_until_option('endTs', request, params) + response = None + if market['linear']: + response = await self.swapV1PrivateGetTradePositionHistory(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchPositionHistory() is not supported for inverse swap positions') + # + # { + # "code": 0, + # "msg": "", + # "data": { + # "positionHistory": [ + # { + # "positionId": "1861675561156571136", + # "symbol": "LTC-USDT", + # "isolated": False, + # "positionSide": "LONG", + # "openTime": 1732693017000, + # "updateTime": 1733310292000, + # "avgPrice": "95.18", + # "avgClosePrice": "129.48", + # "realisedProfit": "102.89", + # "netProfit": "99.63", + # "positionAmt": "30.0", + # "closePositionAmt": "30.0", + # "leverage": 6, + # "closeAllPositions": True, + # "positionCommission": "-0.33699650000000003", + # "totalFunding": "-2.921461693902908" + # }, + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + records = self.safe_list(data, 'positionHistory', []) + positions = self.parse_positions(records) + return self.filter_by_symbol_since_limit(positions, symbol, since, limit) + + async def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://bingx-api.github.io/docs/#/en-us/swapV2/account-api.html#Query%20position%20data + https://bingx-api.github.io/docs/#/en-us/standard/contract-interface.html#position + https://bingx-api.github.io/docs/#/en-us/cswap/trade-api.html#Query%20warehouse + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.standard]: whether to fetch standard contract positions + :returns dict[]: a list of `position structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + standard = None + standard, params = self.handle_option_and_params(params, 'fetchPositions', 'standard', False) + response = None + if standard: + response = await self.contractV1PrivateGetAllPosition(params) + else: + market = None + if symbols is not None: + symbols = self.market_symbols(symbols) + firstSymbol = self.safe_string(symbols, 0) + if firstSymbol is not None: + market = self.market(firstSymbol) + subType = None + subType, params = self.handle_sub_type_and_params('fetchPositions', market, params) + if subType == 'inverse': + response = await self.cswapV1PrivateGetUserPositions(params) + # + # { + # "code": 0, + # "msg": "", + # "timestamp": 0, + # "data": [ + # { + # "symbol": "SOL-USD", + # "positionId": "1813080351385337856", + # "positionSide": "LONG", + # "isolated": False, + # "positionAmt": "1", + # "availableAmt": "1", + # "unrealizedProfit": "-0.00009074", + # "initialMargin": "0.00630398", + # "liquidationPrice": 23.968303426677032, + # "avgPrice": "158.63", + # "leverage": 10, + # "markPrice": "158.402", + # "riskRate": "0.00123783", + # "maxMarginReduction": "0", + # "updateTime": 1721107015848 + # } + # ] + # } + # + else: + response = await self.swapV2PrivateGetUserPositions(params) + # + # { + # "code": 0, + # "msg": "", + # "data": [ + # { + # "positionId": "1792480725958881280", + # "symbol": "LTC-USDT", + # "currency": "USDT", + # "positionAmt": "0.1", + # "availableAmt": "0.1", + # "positionSide": "LONG", + # "isolated": False, + # "avgPrice": "83.53", + # "initialMargin": "1.3922", + # "margin": "0.3528", + # "leverage": 6, + # "unrealizedProfit": "-1.0393", + # "realisedProfit": "-0.2119", + # "liquidationPrice": 0, + # "pnlRatio": "-0.7465", + # "maxMarginReduction": "0.0000", + # "riskRate": "0.0008", + # "markPrice": "73.14", + # "positionValue": "7.3136", + # "onlyOnePosition": True, + # "updateTime": 1721088016688 + # } + # ] + # } + # + positions = self.safe_list(response, 'data', []) + return self.parse_positions(positions, symbols) + + async def fetch_position(self, symbol: str, params={}): + """ + fetch data on a single open contract trade position + + https://bingx-api.github.io/docs/#/en-us/swapV2/account-api.html#Query%20position%20data + https://bingx-api.github.io/docs/#/en-us/cswap/trade-api.html#Query%20warehouse + + :param str symbol: unified market symbol of the market the position is held in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `position structure ` + """ + await self.load_markets() + market = self.market(symbol) + if not market['swap']: + raise BadRequest(self.id + ' fetchPosition() supports swap markets only') + request: dict = { + 'symbol': market['id'], + } + response = None + if market['inverse']: + response = await self.cswapV1PrivateGetUserPositions(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "timestamp": 0, + # "data": [ + # { + # "symbol": "SOL-USD", + # "positionId": "1813080351385337856", + # "positionSide": "LONG", + # "isolated": False, + # "positionAmt": "1", + # "availableAmt": "1", + # "unrealizedProfit": "-0.00009074", + # "initialMargin": "0.00630398", + # "liquidationPrice": 23.968303426677032, + # "avgPrice": "158.63", + # "leverage": 10, + # "markPrice": "158.402", + # "riskRate": "0.00123783", + # "maxMarginReduction": "0", + # "updateTime": 1721107015848 + # } + # ] + # } + # + else: + response = await self.swapV2PrivateGetUserPositions(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "data": [ + # { + # "positionId": "1792480725958881280", + # "symbol": "LTC-USDT", + # "currency": "USDT", + # "positionAmt": "0.1", + # "availableAmt": "0.1", + # "positionSide": "LONG", + # "isolated": False, + # "avgPrice": "83.53", + # "initialMargin": "1.3922", + # "margin": "0.3528", + # "leverage": 6, + # "unrealizedProfit": "-1.0393", + # "realisedProfit": "-0.2119", + # "liquidationPrice": 0, + # "pnlRatio": "-0.7465", + # "maxMarginReduction": "0.0000", + # "riskRate": "0.0008", + # "markPrice": "73.14", + # "positionValue": "7.3136", + # "onlyOnePosition": True, + # "updateTime": 1721088016688 + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + first = self.safe_dict(data, 0, {}) + return self.parse_position(first, market) + + def parse_position(self, position: dict, market: Market = None): + # + # inverse swap + # + # { + # "symbol": "SOL-USD", + # "positionId": "1813080351385337856", + # "positionSide": "LONG", + # "isolated": False, + # "positionAmt": "1", + # "availableAmt": "1", + # "unrealizedProfit": "-0.00009074", + # "initialMargin": "0.00630398", + # "liquidationPrice": 23.968303426677032, + # "avgPrice": "158.63", + # "leverage": 10, + # "markPrice": "158.402", + # "riskRate": "0.00123783", + # "maxMarginReduction": "0", + # "updateTime": 1721107015848 + # } + # + # linear swap + # + # { + # "positionId": "1792480725958881280", + # "symbol": "LTC-USDT", + # "currency": "USDT", + # "positionAmt": "0.1", + # "availableAmt": "0.1", + # "positionSide": "LONG", + # "isolated": False, + # "avgPrice": "83.53", + # "initialMargin": "1.3922", + # "margin": "0.3528", + # "leverage": 6, + # "unrealizedProfit": "-1.0393", + # "realisedProfit": "-0.2119", + # "liquidationPrice": 0, + # "pnlRatio": "-0.7465", + # "maxMarginReduction": "0.0000", + # "riskRate": "0.0008", + # "markPrice": "73.14", + # "positionValue": "7.3136", + # "onlyOnePosition": True, + # "updateTime": 1721088016688 + # } + # + # standard position + # + # { + # "currentPrice": "82.91", + # "symbol": "LTC/USDT", + # "initialMargin": "5.00000000000000000000", + # "unrealizedProfit": "-0.26464500", + # "leverage": "20.000000000", + # "isolated": True, + # "entryPrice": "83.13", + # "positionSide": "LONG", + # "positionAmt": "1.20365912", + # } + # + # linear swap fetchPositionHistory + # + # { + # "positionId": "1861675561156571136", + # "symbol": "LTC-USDT", + # "isolated": False, + # "positionSide": "LONG", + # "openTime": 1732693017000, + # "updateTime": 1733310292000, + # "avgPrice": "95.18", + # "avgClosePrice": "129.48", + # "realisedProfit": "102.89", + # "netProfit": "99.63", + # "positionAmt": "30.0", + # "closePositionAmt": "30.0", + # "leverage": 6, + # "closeAllPositions": True, + # "positionCommission": "-0.33699650000000003", + # "totalFunding": "-2.921461693902908" + # } + # + marketId = self.safe_string(position, 'symbol', '') + marketId = marketId.replace('/', '-') # standard return different format + isolated = self.safe_bool(position, 'isolated') + marginMode = None + if isolated is not None: + marginMode = 'isolated' if isolated else 'cross' + timestamp = self.safe_integer(position, 'openTime') + return self.safe_position({ + 'info': position, + 'id': self.safe_string(position, 'positionId'), + 'symbol': self.safe_symbol(marketId, market, '-', 'swap'), + 'notional': self.safe_number(position, 'positionValue'), + 'marginMode': marginMode, + 'liquidationPrice': None, + 'entryPrice': self.safe_number_2(position, 'avgPrice', 'entryPrice'), + 'unrealizedPnl': self.safe_number(position, 'unrealizedProfit'), + 'realizedPnl': self.safe_number(position, 'realisedProfit'), + 'percentage': None, + 'contracts': self.safe_number(position, 'positionAmt'), + 'contractSize': None, + 'markPrice': self.safe_number(position, 'markPrice'), + 'lastPrice': None, + 'side': self.safe_string_lower(position, 'positionSide'), + 'hedged': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastUpdateTimestamp': self.safe_integer(position, 'updateTime'), + 'maintenanceMargin': None, + 'maintenanceMarginPercentage': None, + 'collateral': None, + 'initialMargin': self.safe_number(position, 'initialMargin'), + 'initialMarginPercentage': None, + 'leverage': self.safe_number(position, 'leverage'), + 'marginRatio': None, + 'stopLossPrice': None, + 'takeProfitPrice': None, + }) + + async def create_market_order_with_cost(self, symbol: str, side: OrderSide, cost: float, params={}): + """ + create a market order by providing the symbol, side and cost + :param str symbol: unified symbol of the market to create an order in + :param str side: 'buy' or 'sell' + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + params['quoteOrderQty'] = cost + return await self.create_order(symbol, 'market', side, cost, None, params) + + async def create_market_buy_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market buy order by providing the symbol and cost + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + params['quoteOrderQty'] = cost + return await self.create_order(symbol, 'market', 'buy', cost, None, params) + + async def create_market_sell_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market sell order by providing the symbol and cost + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + params['quoteOrderQty'] = cost + return await self.create_order(symbol, 'market', 'sell', cost, None, params) + + def create_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + @ignore + helper function to build request + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: request to be sent to the exchange + """ + market = self.market(symbol) + postOnly = None + marketType = None + marketType, params = self.handle_market_type_and_params('createOrder', market, params) + type = type.upper() + request: dict = { + 'symbol': market['id'], + 'type': type, + 'side': side.upper(), + } + isMarketOrder = type == 'MARKET' + isSpot = marketType == 'spot' + isTwapOrder = type == 'TWAP' + if isTwapOrder and isSpot: + raise BadSymbol(self.id + ' createOrder() twap order supports swap contracts only') + stopLossPrice = self.safe_string(params, 'stopLossPrice') + takeProfitPrice = self.safe_string(params, 'takeProfitPrice') + triggerPrice = self.safe_string_2(params, 'stopPrice', 'triggerPrice') + isTriggerOrder = triggerPrice is not None + isStopLossPriceOrder = stopLossPrice is not None + isTakeProfitPriceOrder = takeProfitPrice is not None + exchangeClientOrderId = 'newClientOrderId' if isSpot else 'clientOrderID' + clientOrderId = self.safe_string_2(params, exchangeClientOrderId, 'clientOrderId') + if clientOrderId is not None: + request[exchangeClientOrderId] = clientOrderId + timeInForce = self.safe_string_upper(params, 'timeInForce') + postOnly, params = self.handle_post_only(isMarketOrder, timeInForce == 'PostOnly', params) + if postOnly or (timeInForce == 'PostOnly'): + request['timeInForce'] = 'PostOnly' + elif timeInForce == 'IOC': + request['timeInForce'] = 'IOC' + elif timeInForce == 'GTC': + request['timeInForce'] = 'GTC' + if isSpot: + cost = self.safe_string_2(params, 'cost', 'quoteOrderQty') + params = self.omit(params, 'cost') + if cost is not None: + request['quoteOrderQty'] = self.parse_to_numeric(self.cost_to_precision(symbol, cost)) + else: + if isMarketOrder and (price is not None): + # keep the legacy behavior, to avoid breaking the old spot-market-buying code + calculatedCost = Precise.string_mul(self.number_to_string(amount), self.number_to_string(price)) + request['quoteOrderQty'] = self.parse_to_numeric(calculatedCost) + else: + request['quantity'] = self.parse_to_numeric(self.amount_to_precision(symbol, amount)) + if not isMarketOrder: + request['price'] = self.parse_to_numeric(self.price_to_precision(symbol, price)) + if triggerPrice is not None: + if isMarketOrder and self.safe_string(request, 'quoteOrderQty') is None: + raise ArgumentsRequired(self.id + ' createOrder() requires the cost parameter(or the amount + price) for placing spot market-buy trigger orders') + request['stopPrice'] = self.price_to_precision(symbol, triggerPrice) + if type == 'LIMIT': + request['type'] = 'TRIGGER_LIMIT' + elif type == 'MARKET': + request['type'] = 'TRIGGER_MARKET' + elif (stopLossPrice is not None) or (takeProfitPrice is not None): + stopTakePrice = stopLossPrice if (stopLossPrice is not None) else takeProfitPrice + if type == 'LIMIT': + request['type'] = 'TAKE_STOP_LIMIT' + elif type == 'MARKET': + request['type'] = 'TAKE_STOP_MARKET' + request['stopPrice'] = self.parse_to_numeric(self.price_to_precision(symbol, stopTakePrice)) + else: + if isTwapOrder: + twapRequest: dict = { + 'symbol': request['symbol'], + 'side': request['side'], + 'positionSide': 'LONG' if (side == 'buy') else 'SHORT', + 'triggerPrice': self.parse_to_numeric(self.price_to_precision(symbol, triggerPrice)), + 'totalAmount': self.parse_to_numeric(self.amount_to_precision(symbol, amount)), + } + # { + # "symbol": "LTC-USDT", + # "side": "BUY", + # "positionSide": "LONG", + # "priceType": "constant", + # "priceVariance": "10", + # "triggerPrice": "120", + # "interval": 8, + # "amountPerOrder": "0.5", + # "totalAmount": "1" + # } + return self.extend(twapRequest, params) + if timeInForce == 'FOK': + request['timeInForce'] = 'FOK' + trailingAmount = self.safe_string(params, 'trailingAmount') + trailingPercent = self.safe_string_2(params, 'trailingPercent', 'priceRate') + trailingType = self.safe_string(params, 'trailingType', 'TRAILING_STOP_MARKET') + isTrailingAmountOrder = trailingAmount is not None + isTrailingPercentOrder = trailingPercent is not None + isTrailing = isTrailingAmountOrder or isTrailingPercentOrder + stopLoss = self.safe_value(params, 'stopLoss') + takeProfit = self.safe_value(params, 'takeProfit') + isStopLoss = stopLoss is not None + isTakeProfit = takeProfit is not None + if ((type == 'LIMIT') or (type == 'TRIGGER_LIMIT') or (type == 'STOP') or (type == 'TAKE_PROFIT')) and not isTrailing: + request['price'] = self.parse_to_numeric(self.price_to_precision(symbol, price)) + reduceOnly = self.safe_bool(params, 'reduceOnly', False) + if isTriggerOrder: + request['stopPrice'] = self.parse_to_numeric(self.price_to_precision(symbol, triggerPrice)) + if isMarketOrder or (type == 'TRIGGER_MARKET'): + request['type'] = 'TRIGGER_MARKET' + elif (type == 'LIMIT') or (type == 'TRIGGER_LIMIT'): + request['type'] = 'TRIGGER_LIMIT' + elif isStopLossPriceOrder or isTakeProfitPriceOrder: + # This can be used to set the stop loss and take profit, but the position needs to be opened first + reduceOnly = True + if isStopLossPriceOrder: + request['stopPrice'] = self.parse_to_numeric(self.price_to_precision(symbol, stopLossPrice)) + if isMarketOrder or (type == 'STOP_MARKET'): + request['type'] = 'STOP_MARKET' + elif (type == 'LIMIT') or (type == 'STOP'): + request['type'] = 'STOP' + elif isTakeProfitPriceOrder: + request['stopPrice'] = self.parse_to_numeric(self.price_to_precision(symbol, takeProfitPrice)) + if isMarketOrder or (type == 'TAKE_PROFIT_MARKET'): + request['type'] = 'TAKE_PROFIT_MARKET' + elif (type == 'LIMIT') or (type == 'TAKE_PROFIT'): + request['type'] = 'TAKE_PROFIT' + elif isTrailing: + request['type'] = trailingType + if isTrailingAmountOrder: + request['price'] = self.parse_to_numeric(trailingAmount) + elif isTrailingPercentOrder: + requestTrailingPercent = Precise.string_div(trailingPercent, '100') + request['priceRate'] = self.parse_to_numeric(requestTrailingPercent) + if isStopLoss or isTakeProfit: + stringifiedAmount = self.number_to_string(amount) + if isStopLoss: + slTriggerPrice = self.safe_string_2(stopLoss, 'triggerPrice', 'stopPrice', stopLoss) + slWorkingType = self.safe_string(stopLoss, 'workingType', 'MARK_PRICE') + slType = self.safe_string(stopLoss, 'type', 'STOP_MARKET') + slRequest: dict = { + 'stopPrice': self.parse_to_numeric(self.price_to_precision(symbol, slTriggerPrice)), + 'workingType': slWorkingType, + 'type': slType, + } + slPrice = self.safe_string(stopLoss, 'price') + if slPrice is not None: + slRequest['price'] = self.parse_to_numeric(self.price_to_precision(symbol, slPrice)) + slQuantity = self.safe_string(stopLoss, 'quantity', stringifiedAmount) + slRequest['quantity'] = self.parse_to_numeric(self.amount_to_precision(symbol, slQuantity)) + request['stopLoss'] = self.json(slRequest) + if isTakeProfit: + tkTriggerPrice = self.safe_string_2(takeProfit, 'triggerPrice', 'stopPrice', takeProfit) + tkWorkingType = self.safe_string(takeProfit, 'workingType', 'MARK_PRICE') + tpType = self.safe_string(takeProfit, 'type', 'TAKE_PROFIT_MARKET') + tpRequest: dict = { + 'stopPrice': self.parse_to_numeric(self.price_to_precision(symbol, tkTriggerPrice)), + 'workingType': tkWorkingType, + 'type': tpType, + } + slPrice = self.safe_string(takeProfit, 'price') + if slPrice is not None: + tpRequest['price'] = self.parse_to_numeric(self.price_to_precision(symbol, slPrice)) + tkQuantity = self.safe_string(takeProfit, 'quantity', stringifiedAmount) + tpRequest['quantity'] = self.parse_to_numeric(self.amount_to_precision(symbol, tkQuantity)) + request['takeProfit'] = self.json(tpRequest) + positionSide = None + hedged = self.safe_bool(params, 'hedged', False) + if hedged: + params = self.omit(params, 'reduceOnly') + if reduceOnly: + positionSide = 'SHORT' if (side == 'buy') else 'LONG' + else: + positionSide = 'LONG' if (side == 'buy') else 'SHORT' + else: + positionSide = 'BOTH' + request['positionSide'] = positionSide + amountReq = amount + if not market['inverse']: + amountReq = self.parse_to_numeric(self.amount_to_precision(symbol, amount)) + request['quantity'] = amountReq # precision not available for inverse contracts + params = self.omit(params, ['hedged', 'triggerPrice', 'stopLossPrice', 'takeProfitPrice', 'trailingAmount', 'trailingPercent', 'trailingType', 'takeProfit', 'stopLoss', 'clientOrderId']) + return self.extend(request, params) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://bingx-api.github.io/docs/#/en-us/swapV2/trade-api.html#Trade%20order + https://bingx-api.github.io/docs/#/en-us/spot/trade-api.html#Create%20an%20Order + https://bingx-api.github.io/docs/#/en-us/cswap/trade-api.html#Trade%20order + https://bingx-api.github.io/docs/#/en-us/swapV2/trade-api.html#Place%20TWAP%20Order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.clientOrderId]: a unique id for the order + :param bool [params.postOnly]: True to place a post only order + :param str [params.timeInForce]: spot supports 'PO', 'GTC' and 'IOC', swap supports 'PO', 'GTC', 'IOC' and 'FOK' + :param bool [params.reduceOnly]: *swap only* True or False whether the order is reduce only + :param float [params.triggerPrice]: triggerPrice at which the attached take profit / stop loss order will be triggered + :param float [params.stopLossPrice]: stop loss trigger price + :param float [params.takeProfitPrice]: take profit trigger price + :param float [params.cost]: the quote quantity that can be used alternative for the amount + :param float [params.trailingAmount]: *swap only* the quote amount to trail away from the current market price + :param float [params.trailingPercent]: *swap only* the percent to trail away from the current market price + :param dict [params.takeProfit]: *takeProfit object in params* containing the triggerPrice at which the attached take profit order will be triggered + :param float [params.takeProfit.triggerPrice]: take profit trigger price + :param dict [params.stopLoss]: *stopLoss object in params* containing the triggerPrice at which the attached stop loss order will be triggered + :param float [params.stopLoss.triggerPrice]: stop loss trigger price + :param boolean [params.test]: *swap only* whether to use the test endpoint or not, default is False + :param str [params.positionSide]: *contracts only* "BOTH" for one way mode, "LONG" for buy side of hedged mode, "SHORT" for sell side of hedged mode + :param boolean [params.hedged]: *swap only* whether the order is in hedged mode or one way mode + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + test = self.safe_bool(params, 'test', False) + params = self.omit(params, 'test') + request = self.create_order_request(symbol, type, side, amount, price, params) + response = None + if market['swap']: + if test: + response = await self.swapV2PrivatePostTradeOrderTest(request) + elif market['inverse']: + response = await self.cswapV1PrivatePostTradeOrder(request) + elif type == 'twap': + response = await self.swapV1PrivatePostTwapOrder(request) + else: + response = await self.swapV2PrivatePostTradeOrder(request) + else: + response = await self.spotV1PrivatePostTradeOrder(request) + # + # spot + # + # { + # "code": 0, + # "msg": "", + # "data": { + # "symbol": "XRP-USDT", + # "orderId": 1514090846268424192, + # "transactTime": 1649822362855, + # "price": "0.5", + # "origQty": "10", + # "executedQty": "0", + # "cummulativeQuoteQty": "0", + # "status": "PENDING", + # "type": "LIMIT", + # "side": "BUY" + # } + # } + # + # linear swap + # + # { + # "code": 0, + # "msg": "", + # "data": { + # "order": { + # "symbol": "BTC-USDT", + # "orderId": 1709036527545438208, + # "side": "BUY", + # "positionSide": "LONG", + # "type": "TRIGGER_LIMIT", + # "clientOrderID": "", + # "workingType": "" + # } + # } + # } + # + # inverse swap + # + # { + # "orderId": 1809841379603398656, + # "symbol": "SOL-USD", + # "positionSide": "LONG", + # "side": "BUY", + # "type": "LIMIT", + # "price": 100, + # "quantity": 1, + # "stopPrice": 0, + # "workingType": "", + # "timeInForce": "" + # } + # + # twap order + # + # { + # "code": 0, + # "msg": "", + # "timestamp": 1732693774386, + # "data": { + # "mainOrderId": "4633860139993029715" + # } + # } + # + if isinstance(response, str): + # broken api engine : order-ids are too long numbers(i.e. 1742930526912864656) + # and json.loadscan not handle them in JS, so we have to use .parseJson + # however, when order has an attached SL/TP, their value types need extra parsing + response = self.fix_stringified_json_members(response) + response = self.parse_json(response) + data = self.safe_dict(response, 'data', {}) + result: dict = {} + if market['swap']: + if market['inverse']: + result = response + else: + result = self.safe_dict(data, 'order', data) + else: + result = data + return self.parse_order(result, market) + + async def create_orders(self, orders: List[OrderRequest], params={}): + """ + create a list of trade orders + + https://bingx-api.github.io/docs/#/spot/trade-api.html#Batch%20Placing%20Orders + https://bingx-api.github.io/docs/#/swapV2/trade-api.html#Bulk%20order + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.sync]: *spot only* if True, multiple orders are ordered serially and all orders do not require the same symbol/side/type + :returns dict: an `order structure ` + """ + await self.load_markets() + ordersRequests = [] + marketIds = [] + for i in range(0, len(orders)): + rawOrder = orders[i] + marketId = self.safe_string(rawOrder, 'symbol') + type = self.safe_string(rawOrder, 'type') + marketIds.append(marketId) + side = self.safe_string(rawOrder, 'side') + amount = self.safe_number(rawOrder, 'amount') + price = self.safe_number(rawOrder, 'price') + orderParams = self.safe_dict(rawOrder, 'params', {}) + orderRequest = self.create_order_request(marketId, type, side, amount, price, orderParams) + ordersRequests.append(orderRequest) + symbols = self.market_symbols(marketIds, None, False, True, True) + symbolsLength = len(symbols) + market = self.market(symbols[0]) + request: dict = {} + response = None + if market['swap']: + if symbolsLength > 5: + raise InvalidOrder(self.id + ' createOrders() can not create more than 5 orders at once for swap markets') + request['batchOrders'] = self.json(ordersRequests) + response = await self.swapV2PrivatePostTradeBatchOrders(request) + else: + sync = self.safe_bool(params, 'sync', False) + if sync: + request['sync'] = True + request['data'] = self.json(ordersRequests) + response = await self.spotV1PrivatePostTradeBatchOrders(request) + # + # spot + # + # { + # "code": 0, + # "msg": "", + # "debugMsg": "", + # "data": { + # "orders": [ + # { + # "symbol": "BTC-USDT", + # "orderId": 1720661389564968960, + # "transactTime": 1699072618272, + # "price": "25000", + # "origQty": "0.0002", + # "executedQty": "0", + # "cummulativeQuoteQty": "0", + # "status": "PENDING", + # "type": "LIMIT", + # "side": "BUY" + # }, + # ] + # } + # } + # + # swap + # + # { + # "code": 0, + # "msg": "", + # "data": { + # "orders": [ + # { + # "symbol": "BTC-USDT", + # "orderId": 1720657081994006528, + # "side": "BUY", + # "positionSide": "LONG", + # "type": "LIMIT", + # "clientOrderID": "", + # "workingType": "" + # }, + # ] + # } + # } + # + if isinstance(response, str): + # broken api engine : order-ids are too long numbers(i.e. 1742930526912864656) + # and json.loadscan not handle them in JS, so we have to use .parseJson + # however, when order has an attached SL/TP, their value types need extra parsing + response = self.fix_stringified_json_members(response) + response = self.parse_json(response) + data = self.safe_dict(response, 'data', {}) + result = self.safe_list(data, 'orders', []) + return self.parse_orders(result, market) + + def parse_order_side(self, side): + sides: dict = { + 'BUY': 'buy', + 'SELL': 'sell', + 'SHORT': 'sell', + 'LONG': 'buy', + 'ask': 'sell', + 'bid': 'buy', + } + return self.safe_string(sides, side, side) + + def parse_order_type(self, type: Str): + types: dict = { + 'trigger_market': 'market', + 'trigger_limit': 'limit', + 'stop_limit': 'limit', + 'stop_market': 'market', + 'take_profit_market': 'market', + 'stop': 'limit', + } + return self.safe_string(types, type, type) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # spot + # createOrder, createOrders, cancelOrder + # + # { + # "symbol": "XRP-USDT", + # "orderId": 1514090846268424192, + # "transactTime": 1649822362855, + # "price": "0.5", + # "origQty": "10", + # "executedQty": "0", + # "cummulativeQuoteQty": "0", + # "status": "PENDING", + # "type": "LIMIT", + # "side": "BUY" + # } + # + # fetchOrder + # + # { + # "symbol": "ETH-USDT", + # "orderId": "1660602123001266176", + # "price": "1700", + # "origQty": "0.003", + # "executedQty": "0", + # "cummulativeQuoteQty": "0", + # "status": "PENDING", + # "type": "LIMIT", + # "side": "BUY", + # "time": "1684753373276", + # "updateTime": "1684753373276", + # "origQuoteOrderQty": "0", + # "fee": "0", + # "feeAsset": "ETH" + # } + # + # fetchOpenOrders, fetchClosedOrders + # + # { + # "symbol": "XRP-USDT", + # "orderId": 1514073325788200960, + # "price": "0.5", + # "StopPrice": "0", + # "origQty": "20", + # "executedQty": "10", + # "cummulativeQuoteQty": "5", + # "status": "PENDING", + # "type": "LIMIT", + # "side": "BUY", + # "time": 1649818185647, + # "updateTime": 1649818185647, + # "origQuoteOrderQty": "0" + # "fee": "-0.01" + # } + # + # + # linear swap + # createOrder, createOrders + # + # { + # "symbol": "BTC-USDT", + # "orderId": 1590973236294713344, + # "side": "BUY", + # "positionSide": "LONG", + # "type": "LIMIT" + # } + # + # inverse swap createOrder + # + # { + # "orderId": 1809841379603398656, + # "symbol": "SOL-USD", + # "positionSide": "LONG", + # "side": "BUY", + # "type": "LIMIT", + # "price": 100, + # "quantity": 1, + # "stopPrice": 0, + # "workingType": "", + # "timeInForce": "" + # } + # + # fetchOrder, fetchOpenOrders, fetchClosedOrders + # + # { + # "symbol": "BTC-USDT", + # "orderId": 1709036527545438208, + # "side": "BUY", + # "positionSide": "LONG", + # "type": "TRIGGER_LIMIT", + # "origQty": "0.0010", + # "price": "22000.0", + # "executedQty": "0.0000", + # "avgPrice": "0.0", + # "cumQuote": "", + # "stopPrice": "23000.0", + # "profit": "", + # "commission": "", + # "status": "NEW", + # "time": 1696301035187, + # "updateTime": 1696301035187, + # "clientOrderId": "", + # "leverage": "", + # "takeProfit": "", + # "stopLoss": "", + # "advanceAttr": 0, + # "positionID": 0, + # "takeProfitEntrustPrice": 0, + # "stopLossEntrustPrice": 0, + # "orderType": "", + # "workingType": "MARK_PRICE" + # } + # with tp and sl + # { + # orderId: 1741440894764281900, + # symbol: 'LTC-USDT', + # positionSide: 'LONG', + # side: 'BUY', + # type: 'MARKET', + # price: 0, + # quantity: 1, + # stopPrice: 0, + # workingType: 'MARK_PRICE', + # clientOrderID: '', + # timeInForce: 'GTC', + # priceRate: 0, + # stopLoss: '{"stopPrice":50,"workingType":"MARK_PRICE","type":"STOP_MARKET","quantity":1}', + # takeProfit: '{"stopPrice":150,"workingType":"MARK_PRICE","type":"TAKE_PROFIT_MARKET","quantity":1}', + # reduceOnly: False + # } + # + # editOrder(swap) + # + # { + # cancelResult: 'true', + # cancelMsg: '', + # cancelResponse: { + # cancelClientOrderId: '', + # cancelOrderId: '1755336244265705472', + # symbol: 'SOL-USDT', + # orderId: '1755336244265705472', + # side: 'SELL', + # positionSide: 'SHORT', + # type: 'LIMIT', + # origQty: '1', + # price: '100.000', + # executedQty: '0', + # avgPrice: '0.000', + # cumQuote: '0', + # stopPrice: '', + # profit: '0.0000', + # commission: '0.000000', + # status: 'PENDING', + # time: '1707339747860', + # updateTime: '1707339747860', + # clientOrderId: '', + # leverage: '20X', + # workingType: 'MARK_PRICE', + # onlyOnePosition: False, + # reduceOnly: False + # }, + # replaceResult: 'true', + # replaceMsg: '', + # newOrderResponse: { + # orderId: '1755338440612995072', + # symbol: 'SOL-USDT', + # positionSide: 'SHORT', + # side: 'SELL', + # type: 'LIMIT', + # price: '99', + # quantity: '2', + # stopPrice: '0', + # workingType: 'MARK_PRICE', + # clientOrderID: '', + # timeInForce: 'GTC', + # priceRate: '0', + # stopLoss: '', + # takeProfit: '', + # reduceOnly: False + # } + # } + # + # editOrder(spot) + # + # { + # cancelResult: {code: '0', msg: '', result: True}, + # openResult: {code: '0', msg: '', result: True}, + # orderOpenResponse: { + # symbol: 'SOL-USDT', + # orderId: '1755334007697866752', + # transactTime: '1707339214620', + # price: '99', + # stopPrice: '0', + # origQty: '0.2', + # executedQty: '0', + # cummulativeQuoteQty: '0', + # status: 'PENDING', + # type: 'LIMIT', + # side: 'SELL', + # clientOrderID: '' + # }, + # orderCancelResponse: { + # symbol: 'SOL-USDT', + # orderId: '1755117055251480576', + # price: '100', + # stopPrice: '0', + # origQty: '0.2', + # executedQty: '0', + # cummulativeQuoteQty: '0', + # status: 'CANCELED', + # type: 'LIMIT', + # side: 'SELL' + # } + # } + # + # stop loss order + # + # { + # "symbol": "ETH-USDT", + # "orderId": "1792461744476422144", + # "price": "2775.65", + # "StopPrice": "2778.42", + # "origQty": "0.032359", + # "executedQty": "0", + # "cummulativeQuoteQty": "0", + # "status": "NEW", + # "type": "TAKE_STOP_LIMIT", + # "side": "SELL", + # "time": "1716191156868", + # "updateTime": "1716191156868", + # "origQuoteOrderQty": "0", + # "fee": "0", + # "feeAsset": "USDT", + # "clientOrderID": "" + # } + # + # inverse swap cancelAllOrders, cancelOrder, fetchOrder, fetchOpenOrders, fetchClosedOrders, fetchCanceledOrders + # + # { + # "symbol": "SOL-USD", + # "orderId": "1809845251327672320", + # "side": "BUY", + # "positionSide": "LONG", + # "type": "LIMIT", + # "quantity": 1, + # "origQty": "0", + # "price": "90", + # "executedQty": "0", + # "avgPrice": "0", + # "cumQuote": "0", + # "stopPrice": "", + # "profit": "0.0000", + # "commission": "0.000000", + # "status": "CANCELLED", + # "time": 1720335707872, + # "updateTime": 1720335707912, + # "clientOrderId": "", + # "leverage": "", + # "takeProfit": { + # "type": "", + # "quantity": 0, + # "stopPrice": 0, + # "price": 0, + # "workingType": "", + # "stopGuaranteed": "" + # }, + # "stopLoss": { + # "type": "", + # "quantity": 0, + # "stopPrice": 0, + # "price": 0, + # "workingType": "", + # "stopGuaranteed": "" + # }, + # "advanceAttr": 0, + # "positionID": 0, + # "takeProfitEntrustPrice": 0, + # "stopLossEntrustPrice": 0, + # "orderType": "", + # "workingType": "" + # } + # + info = order + newOrder = self.safe_dict_2(order, 'newOrderResponse', 'orderOpenResponse') + if newOrder is not None: + order = newOrder + positionSide = self.safe_string_2(order, 'positionSide', 'ps') + marketType = 'spot' if (positionSide is None) else 'swap' + marketId = self.safe_string_2(order, 'symbol', 's') + if market is None: + market = self.safe_market(marketId, None, None, marketType) + side = self.safe_string_lower_2(order, 'side', 'S') + timestamp = self.safe_integer_n(order, ['time', 'transactTime', 'E', 'createdTime']) + lastTradeTimestamp = self.safe_integer_2(order, 'updateTime', 'T') + statusId = self.safe_string_upper_n(order, ['status', 'X', 'orderStatus']) + feeCurrencyCode = self.safe_string_2(order, 'feeAsset', 'N') + feeCost = self.safe_string_n(order, ['fee', 'commission', 'n']) + if (feeCurrencyCode is None): + if market['spot']: + if side == 'buy': + feeCurrencyCode = market['base'] + else: + feeCurrencyCode = market['quote'] + else: + feeCurrencyCode = market['quote'] + stopLoss = self.safe_value(order, 'stopLoss') + stopLossPrice = None + if (stopLoss is not None) and (stopLoss != ''): + stopLossPrice = self.omit_zero(self.safe_string(stopLoss, 'stopLoss')) + if (stopLoss is not None) and ((not isinstance(stopLoss, numbers.Real))) and (stopLoss != ''): + # stopLoss: '{"stopPrice":50,"workingType":"MARK_PRICE","type":"STOP_MARKET","quantity":1}', + if isinstance(stopLoss, str): + stopLoss = self.parse_json(stopLoss) + stopLossPrice = self.omit_zero(self.safe_string(stopLoss, 'stopPrice')) + takeProfit = self.safe_value(order, 'takeProfit') + takeProfitPrice = None + if takeProfit is not None and (takeProfit != ''): + takeProfitPrice = self.omit_zero(self.safe_string(takeProfit, 'takeProfit')) + if (takeProfit is not None) and ((not isinstance(takeProfit, numbers.Real))) and (takeProfit != ''): + # takeProfit: '{"stopPrice":150,"workingType":"MARK_PRICE","type":"TAKE_PROFIT_MARKET","quantity":1}', + if isinstance(takeProfit, str): + takeProfit = self.parse_json(takeProfit) + takeProfitPrice = self.omit_zero(self.safe_string(takeProfit, 'stopPrice')) + rawType = self.safe_string_lower_2(order, 'type', 'o') + stopPrice = self.omit_zero(self.safe_string_2(order, 'StopPrice', 'stopPrice')) + triggerPrice = stopPrice + if stopPrice is not None: + if (rawType.find('stop') > -1) and (stopLossPrice is None): + stopLossPrice = stopPrice + triggerPrice = None + if (rawType.find('take') > -1) and (takeProfitPrice is None): + takeProfitPrice = stopPrice + triggerPrice = None + return self.safe_order({ + 'info': info, + 'id': self.safe_string_n(order, ['orderId', 'i', 'mainOrderId']), + 'clientOrderId': self.safe_string_n(order, ['clientOrderID', 'clientOrderId', 'origClientOrderId', 'c']), + 'symbol': self.safe_symbol(marketId, market, '-', marketType), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': lastTradeTimestamp, + 'lastUpdateTimestamp': self.safe_integer(order, 'updateTime'), + 'type': self.parse_order_type(rawType), + 'timeInForce': self.safe_string(order, 'timeInForce'), + 'postOnly': None, + 'side': self.parse_order_side(side), + 'price': self.safe_string_2(order, 'price', 'p'), + 'triggerPrice': triggerPrice, + 'stopLossPrice': stopLossPrice, + 'takeProfitPrice': takeProfitPrice, + 'average': self.safe_string_2(order, 'avgPrice', 'ap'), + 'cost': self.safe_string(order, 'cummulativeQuoteQty'), + 'amount': self.safe_string_n(order, ['origQty', 'q', 'quantity', 'totalAmount']), + 'filled': self.safe_string_2(order, 'executedQty', 'z'), + 'remaining': None, + 'status': self.parse_order_status(statusId), + 'fee': { + 'currency': feeCurrencyCode, + 'cost': Precise.string_abs(feeCost), + }, + 'trades': None, + 'reduceOnly': self.safe_bool_2(order, 'reduceOnly', 'ro'), + }, market) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'NEW': 'open', + 'PENDING': 'open', + 'PARTIALLY_FILLED': 'open', + 'RUNNING': 'open', + 'FILLED': 'closed', + 'CANCELED': 'canceled', + 'CANCELLED': 'canceled', + 'FAILED': 'canceled', + } + return self.safe_string(statuses, status, status) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://bingx-api.github.io/docs/#/en-us/spot/trade-api.html#Cancel%20Order + https://bingx-api.github.io/docs/#/en-us/swapV2/trade-api.html#Cancel%20Order + https://bingx-api.github.io/docs/#/en-us/cswap/trade-api.html#Cancel%20an%20Order + https://bingx-api.github.io/docs/#/en-us/swapV2/trade-api.html#Cancel%20TWAP%20Order + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.clientOrderId]: a unique id for the order + :returns dict: an `order structure ` + """ + await self.load_markets() + isTwapOrder = self.safe_bool(params, 'twap', False) + params = self.omit(params, 'twap') + response = None + market = None + if isTwapOrder: + twapRequest: dict = { + 'mainOrderId': id, + } + response = await self.swapV1PrivatePostTwapCancelOrder(self.extend(twapRequest, params)) + # + # { + # "code": 0, + # "msg": "", + # "timestamp": 1702731661854, + # "data": { + # "symbol": "BNB-USDT", + # "side": "BUY", + # "positionSide": "LONG", + # "priceType": "constant", + # "priceVariance": "2000", + # "triggerPrice": "68000", + # "interval": 8, + # "amountPerOrder": "0.111", + # "totalAmount": "0.511", + # "orderStatus": "Running", + # "executedQty": "0.1", + # "duration": 800, + # "maxDuration": 9000, + # "createdTime": 1702731661854, + # "updateTime": 1702731661854 + # } + # } + # + else: + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + clientOrderId = self.safe_string_2(params, 'clientOrderId', 'clientOrderID') + params = self.omit(params, ['clientOrderId']) + if clientOrderId is not None: + request['clientOrderID'] = clientOrderId + else: + request['orderId'] = id + type = None + subType = None + type, params = self.handle_market_type_and_params('cancelOrder', market, params) + subType, params = self.handle_sub_type_and_params('cancelOrder', market, params) + if type == 'spot': + response = await self.spotV1PrivatePostTradeCancel(self.extend(request, params)) + else: + if subType == 'inverse': + response = await self.cswapV1PrivateDeleteTradeCancelOrder(self.extend(request, params)) + else: + response = await self.swapV2PrivateDeleteTradeOrder(self.extend(request, params)) + # + # spot + # + # { + # "code": 0, + # "msg": "", + # "data": { + # "symbol": "XRP-USDT", + # "orderId": 1514090846268424192, + # "price": "0.5", + # "origQty": "10", + # "executedQty": "0", + # "cummulativeQuoteQty": "0", + # "status": "CANCELED", + # "type": "LIMIT", + # "side": "BUY" + # } + # } + # + # inverse swap + # + # { + # "code": 0, + # "msg": "", + # "data": { + # "order": { + # "symbol": "SOL-USD", + # "orderId": "1816002957423951872", + # "side": "BUY", + # "positionSide": "Long", + # "type": "Pending", + # "quantity": 0, + # "origQty": "0", + # "price": "150", + # "executedQty": "0", + # "avgPrice": "0", + # "cumQuote": "0", + # "stopPrice": "", + # "profit": "0.0000", + # "commission": "0.000000", + # "status": "CANCELLED", + # "time": 1721803819410, + # "updateTime": 1721803819427, + # "clientOrderId": "", + # "leverage": "", + # "takeProfit": { + # "type": "", + # "quantity": 0, + # "stopPrice": 0, + # "price": 0, + # "workingType": "", + # "stopGuaranteed": "" + # }, + # "stopLoss": { + # "type": "", + # "quantity": 0, + # "stopPrice": 0, + # "price": 0, + # "workingType": "", + # "stopGuaranteed": "" + # }, + # "advanceAttr": 0, + # "positionID": 0, + # "takeProfitEntrustPrice": 0, + # "stopLossEntrustPrice": 0, + # "orderType": "", + # "workingType": "" + # } + # } + # } + # + # linear swap + # + # { + # "code": 0, + # "msg": "", + # "data": { + # "order": { + # "symbol": "LINK-USDT", + # "orderId": 1597783850786750464, + # "side": "BUY", + # "positionSide": "LONG", + # "type": "TRIGGER_MARKET", + # "origQty": "5.0", + # "price": "5.0000", + # "executedQty": "0.0", + # "avgPrice": "0.0000", + # "cumQuote": "0", + # "stopPrice": "5.0000", + # "profit": "", + # "commission": "", + # "status": "CANCELLED", + # "time": 1669776330000, + # "updateTime": 1669776330000 + # } + # } + # } + # + data = self.safe_dict(response, 'data', {}) + order = self.safe_dict(data, 'order', data) + return self.parse_order(order, market) + + async def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders + + https://bingx-api.github.io/docs/#/en-us/spot/trade-api.html#Cancel%20orders%20by%20symbol + https://bingx-api.github.io/docs/#/swapV2/trade-api.html#Cancel%20All%20Orders + https://bingx-api.github.io/docs/#/en-us/cswap/trade-api.html#Cancel%20all%20orders + + :param str [symbol]: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelAllOrders() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = None + if market['spot']: + response = await self.spotV1PrivatePostTradeCancelOpenOrders(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "debugMsg": "", + # "data": { + # "orders": [{ + # "symbol": "ADA-USDT", + # "orderId": 1740659971369992192, + # "transactTime": 1703840651730, + # "price": 5, + # "stopPrice": 0, + # "origQty": 10, + # "executedQty": 0, + # "cummulativeQuoteQty": 0, + # "status": "CANCELED", + # "type": "LIMIT", + # "side": "SELL" + # }] + # } + # } + # + elif market['swap']: + if market['inverse']: + response = await self.cswapV1PrivateDeleteTradeAllOpenOrders(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "timestamp": 1720501468364, + # "data": { + # "success": [ + # { + # "symbol": "SOL-USD", + # "orderId": "1809845251327672320", + # "side": "BUY", + # "positionSide": "LONG", + # "type": "LIMIT", + # "quantity": 1, + # "origQty": "0", + # "price": "90", + # "executedQty": "0", + # "avgPrice": "0", + # "cumQuote": "0", + # "stopPrice": "", + # "profit": "0.0000", + # "commission": "0.000000", + # "status": "CANCELLED", + # "time": 1720335707872, + # "updateTime": 1720335707912, + # "clientOrderId": "", + # "leverage": "", + # "takeProfit": { + # "type": "", + # "quantity": 0, + # "stopPrice": 0, + # "price": 0, + # "workingType": "", + # "stopGuaranteed": "" + # }, + # "stopLoss": { + # "type": "", + # "quantity": 0, + # "stopPrice": 0, + # "price": 0, + # "workingType": "", + # "stopGuaranteed": "" + # }, + # "advanceAttr": 0, + # "positionID": 0, + # "takeProfitEntrustPrice": 0, + # "stopLossEntrustPrice": 0, + # "orderType": "", + # "workingType": "" + # } + # ], + # "failed": null + # } + # } + # + else: + response = await self.swapV2PrivateDeleteTradeAllOpenOrders(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "data": { + # "success": [ + # { + # "symbol": "LINK-USDT", + # "orderId": 1597783835095859200, + # "side": "BUY", + # "positionSide": "LONG", + # "type": "TRIGGER_LIMIT", + # "origQty": "5.0", + # "price": "9.0000", + # "executedQty": "0.0", + # "avgPrice": "0.0000", + # "cumQuote": "0", + # "stopPrice": "9.5000", + # "profit": "", + # "commission": "", + # "status": "NEW", + # "time": 1669776326000, + # "updateTime": 1669776326000 + # } + # ], + # "failed": null + # } + # } + # + else: + raise BadRequest(self.id + ' cancelAllOrders is only supported for spot and swap markets.') + data = self.safe_dict(response, 'data', {}) + orders = self.safe_list_2(data, 'success', 'orders', []) + return self.parse_orders(orders) + + async def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): + """ + cancel multiple orders + + https://bingx-api.github.io/docs/#/swapV2/trade-api.html#Cancel%20a%20Batch%20of%20Orders + https://bingx-api.github.io/docs/#/spot/trade-api.html#Cancel%20a%20Batch%20of%20Orders + + :param str[] ids: order ids + :param str symbol: unified market symbol, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str[] [params.clientOrderIds]: client order ids + :returns dict: an list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrders() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + clientOrderIds = self.safe_value(params, 'clientOrderIds') + params = self.omit(params, 'clientOrderIds') + idsToParse = ids + areClientOrderIds = (clientOrderIds is not None) + if areClientOrderIds: + idsToParse = clientOrderIds + parsedIds = [] + for i in range(0, len(idsToParse)): + id = idsToParse[i] + stringId = str(id) + parsedIds.append(stringId) + response = None + if market['spot']: + spotReqKey = 'clientOrderIDs' if areClientOrderIds else 'orderIds' + request[spotReqKey] = ','.join(parsedIds) + response = await self.spotV1PrivatePostTradeCancelOrders(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "debugMsg": "", + # "data": { + # "orders": [ + # { + # "symbol": "SOL-USDT", + # "orderId": 1795970045910614016, + # "transactTime": 1717027601111, + # "price": "180.25", + # "stopPrice": "0", + # "origQty": "0.03", + # "executedQty": "0", + # "cummulativeQuoteQty": "0", + # "status": "CANCELED", + # "type": "LIMIT", + # "side": "SELL", + # "clientOrderID": "" + # }, + # ... + # ] + # } + # } + # + else: + if areClientOrderIds: + request['clientOrderIDList'] = self.json(parsedIds) + else: + request['orderIdList'] = parsedIds + response = await self.swapV2PrivateDeleteTradeBatchOrders(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "data": { + # "success": [ + # { + # "symbol": "LINK-USDT", + # "orderId": 1597783850786750464, + # "side": "BUY", + # "positionSide": "LONG", + # "type": "TRIGGER_MARKET", + # "origQty": "5.0", + # "price": "5.5710", + # "executedQty": "0.0", + # "avgPrice": "0.0000", + # "cumQuote": "0", + # "stopPrice": "5.0000", + # "profit": "0.0000", + # "commission": "0.000000", + # "status": "CANCELLED", + # "time": 1669776330000, + # "updateTime": 1672370837000 + # } + # ], + # "failed": null + # } + # } + # + data = self.safe_dict(response, 'data', {}) + success = self.safe_list_2(data, 'success', 'orders', []) + return self.parse_orders(success) + + async def cancel_all_orders_after(self, timeout: Int, params={}): + """ + dead man's switch, cancel all orders after the given timeout + + https://bingx-api.github.io/docs/#/en-us/spot/trade-api.html#Cancel%20all%20orders%20in%20countdown + https://bingx-api.github.io/docs/#/en-us/swapV2/trade-api.html#Cancel%20all%20orders%20in%20countdown + + :param number timeout: time in milliseconds, 0 represents cancel the timer + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: spot or swap market + :returns dict: the api result + """ + await self.load_markets() + isActive = (timeout > 0) + request: dict = { + 'type': 'ACTIVATE' if (isActive) else 'CLOSE', + 'timeOut': (self.parse_to_int(timeout / 1000)) if (isActive) else 0, + } + response = None + type = None + type, params = self.handle_market_type_and_params('cancelAllOrdersAfter', None, params) + if type == 'spot': + response = await self.spotV1PrivatePostTradeCancelAllAfter(self.extend(request, params)) + elif type == 'swap': + response = await self.swapV2PrivatePostTradeCancelAllAfter(self.extend(request, params)) + else: + raise NotSupported(self.id + ' cancelAllOrdersAfter() is not supported for ' + type + ' markets') + # + # { + # code: '0', + # msg: '', + # data: { + # triggerTime: '1712645434', + # status: 'ACTIVATED', + # note: 'All your perpetual pending orders will be closed automatically at 2024-04-09 06:50:34 UTC(+0),before that you can cancel the timer, or self.extend triggerTime time by self request' + # } + # } + # + return response + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://bingx-api.github.io/docs/#/en-us/spot/trade-api.html#Query%20Order%20details + https://bingx-api.github.io/docs/#/en-us/swapV2/trade-api.html#Query%20Order%20details + https://bingx-api.github.io/docs/#/en-us/cswap/trade-api.html#Query%20Order + https://bingx-api.github.io/docs/#/en-us/swapV2/trade-api.html#TWAP%20Order%20Details + + :param str id: the order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.twap]: if fetching twap order + :returns dict: an `order structure ` + """ + await self.load_markets() + isTwapOrder = self.safe_bool(params, 'twap', False) + params = self.omit(params, 'twap') + response = None + market = None + if isTwapOrder: + twapRequest: dict = { + 'mainOrderId': id, + } + response = await self.swapV1PrivateGetTwapOrderDetail(self.extend(twapRequest, params)) + # + # { + # "code": 0, + # "msg": "success cancel order", + # "timestamp": 1732760856617, + # "data": { + # "symbol": "LTC-USDT", + # "mainOrderId": "5596903086063901779", + # "side": "BUY", + # "positionSide": "LONG", + # "priceType": "constant", + # "priceVariance": "10.00", + # "triggerPrice": "120.00", + # "interval": 8, + # "amountPerOrder": "0.5", + # "totalAmount": "1.0", + # "orderStatus": "Filled", + # "executedQty": "1.0", + # "duration": 16, + # "maxDuration": 86400, + # "createdTime": 1732693017000, + # "updateTime": 1732693033000 + # } + # } + # + else: + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrder() requires a symbol argument') + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'orderId': id, + } + type = None + subType = None + type, params = self.handle_market_type_and_params('fetchOrder', market, params) + subType, params = self.handle_sub_type_and_params('fetchOrder', market, params) + if type == 'spot': + response = await self.spotV1PrivateGetTradeQuery(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "data": { + # "symbol": "XRP-USDT", + # "orderId": 1514087361158316032, + # "price": "0.5", + # "origQty": "10", + # "executedQty": "0", + # "cummulativeQuoteQty": "0", + # "status": "CANCELED", + # "type": "LIMIT", + # "side": "BUY", + # "time": 1649821532000, + # "updateTime": 1649821543000, + # "origQuoteOrderQty": "0", + # "fee": "0", + # "feeAsset": "XRP" + # } + # } + # + else: + if subType == 'inverse': + response = await self.cswapV1PrivateGetTradeOrderDetail(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "data": { + # "order": { + # "symbol": "SOL-USD", + # "orderId": "1816342420721254400", + # "side": "BUY", + # "positionSide": "Long", + # "type": "LIMIT", + # "quantity": 1, + # "origQty": "", + # "price": "150", + # "executedQty": "0", + # "avgPrice": "0.000", + # "cumQuote": "", + # "stopPrice": "", + # "profit": "0.0000", + # "commission": "0.0000", + # "status": "Pending", + # "time": 1721884753767, + # "updateTime": 1721884753786, + # "clientOrderId": "", + # "leverage": "", + # "takeProfit": { + # "type": "TAKE_PROFIT", + # "quantity": 0, + # "stopPrice": 0, + # "price": 0, + # "workingType": "MARK_PRICE", + # "stopGuaranteed": "" + # }, + # "stopLoss": { + # "type": "STOP", + # "quantity": 0, + # "stopPrice": 0, + # "price": 0, + # "workingType": "MARK_PRICE", + # "stopGuaranteed": "" + # }, + # "advanceAttr": 0, + # "positionID": 0, + # "takeProfitEntrustPrice": 0, + # "stopLossEntrustPrice": 0, + # "orderType": "", + # "workingType": "MARK_PRICE" + # } + # } + # } + # + else: + response = await self.swapV2PrivateGetTradeOrder(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "data": { + # "order": { + # "symbol": "BTC-USDT", + # "orderId": 1597597642269917184, + # "side": "SELL", + # "positionSide": "LONG", + # "type": "TAKE_PROFIT_MARKET", + # "origQty": "1.0000", + # "price": "0.0", + # "executedQty": "0.0000", + # "avgPrice": "0.0", + # "cumQuote": "", + # "stopPrice": "16494.0", + # "profit": "", + # "commission": "", + # "status": "FILLED", + # "time": 1669731935000, + # "updateTime": 1669752524000 + # } + # } + # } + # + data = self.safe_dict(response, 'data', {}) + order = self.safe_dict(data, 'order', data) + return self.parse_order(order, market) + + async def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://bingx-api.github.io/docs/#/en-us/swapV2/trade-api.html#All%20Orders + https://bingx-api.github.io/docs/#/en-us/swapV2/trade-api.html#Query%20Order%20history(returns less fields than above) + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch entries for + :param int [params.orderId]: Only return subsequent orders, and return the latest order by default + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + type = None + type, params = self.handle_market_type_and_params('fetchOrders', market, params) + if type != 'swap': + raise NotSupported(self.id + ' fetchOrders() is only supported for swap markets') + if limit is not None: + request['limit'] = limit + if since is not None: + request['startTime'] = since + request, params = self.handle_until_option('endTime', request, params) + response = await self.swapV1PrivateGetTradeFullOrder(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "data": { + # "orders": [ + # { + # "symbol": "PYTH-USDT", + # "orderId": 1736007506620112100, + # "side": "SELL", + # "positionSide": "SHORT", + # "type": "LIMIT", + # "origQty": "33", + # "price": "0.3916", + # "executedQty": "33", + # "avgPrice": "0.3916", + # "cumQuote": "13", + # "stopPrice": "", + # "profit": "0.0000", + # "commission": "-0.002585", + # "status": "FILLED", + # "time": 1702731418000, + # "updateTime": 1702731470000, + # "clientOrderId": "", + # "leverage": "15X", + # "takeProfit": { + # "type": "TAKE_PROFIT", + # "quantity": 0, + # "stopPrice": 0, + # "price": 0, + # "workingType": "" + # }, + # "stopLoss": { + # "type": "STOP", + # "quantity": 0, + # "stopPrice": 0, + # "price": 0, + # "workingType": "" + # }, + # "advanceAttr": 0, + # "positionID": 0, + # "takeProfitEntrustPrice": 0, + # "stopLossEntrustPrice": 0, + # "orderType": "", + # "workingType": "MARK_PRICE", + # "stopGuaranteed": False, + # "triggerOrderId": 1736012449498123500 + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + orders = self.safe_list(data, 'orders', []) + return self.parse_orders(orders, market, since, limit) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://bingx-api.github.io/docs/#/en-us/spot/trade-api.html#Current%20Open%20Orders + https://bingx-api.github.io/docs/#/en-us/swapV2/trade-api.html#Current%20All%20Open%20Orders + https://bingx-api.github.io/docs/#/en-us/cswap/trade-api.html#Query%20all%20current%20pending%20orders + https://bingx-api.github.io/docs/#/en-us/swapV2/trade-api.html#Query%20TWAP%20Entrusted%20Order + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.twap]: if fetching twap open orders + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + market = None + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + type = None + subType = None + response = None + type, params = self.handle_market_type_and_params('fetchOpenOrders', market, params) + subType, params = self.handle_sub_type_and_params('fetchOpenOrders', market, params) + if type == 'spot': + response = await self.spotV1PrivateGetTradeOpenOrders(self.extend(request, params)) + else: + isTwapOrder = self.safe_bool(params, 'twap', False) + params = self.omit(params, 'twap') + if isTwapOrder: + response = await self.swapV1PrivateGetTwapOpenOrders(self.extend(request, params)) + elif subType == 'inverse': + response = await self.cswapV1PrivateGetTradeOpenOrders(self.extend(request, params)) + else: + response = await self.swapV2PrivateGetTradeOpenOrders(self.extend(request, params)) + # + # spot + # + # { + # "code": 0, + # "msg": "", + # "data": { + # "orders": [ + # { + # "symbol": "XRP-USDT", + # "orderId": 1514073325788200960, + # "price": "0.5", + # "origQty": "20", + # "executedQty": "0", + # "cummulativeQuoteQty": "0", + # "status": "PENDING", + # "type": "LIMIT", + # "side": "BUY", + # "time": 1649818185647, + # "updateTime": 1649818185647, + # "origQuoteOrderQty": "0" + # } + # ] + # } + # } + # + # inverse swap + # + # { + # "code": 0, + # "msg": "", + # "data": { + # "orders": [ + # { + # "symbol": "SOL-USD", + # "orderId": "1816013900044320768", + # "side": "BUY", + # "positionSide": "Long", + # "type": "LIMIT", + # "quantity": 1, + # "origQty": "", + # "price": "150", + # "executedQty": "0", + # "avgPrice": "0.000", + # "cumQuote": "", + # "stopPrice": "", + # "profit": "0.0000", + # "commission": "0.0000", + # "status": "Pending", + # "time": 1721806428334, + # "updateTime": 1721806428352, + # "clientOrderId": "", + # "leverage": "", + # "takeProfit": { + # "type": "TAKE_PROFIT", + # "quantity": 0, + # "stopPrice": 0, + # "price": 0, + # "workingType": "MARK_PRICE", + # "stopGuaranteed": "" + # }, + # "stopLoss": { + # "type": "STOP", + # "quantity": 0, + # "stopPrice": 0, + # "price": 0, + # "workingType": "MARK_PRICE", + # "stopGuaranteed": "" + # }, + # "advanceAttr": 0, + # "positionID": 0, + # "takeProfitEntrustPrice": 0, + # "stopLossEntrustPrice": 0, + # "orderType": "", + # "workingType": "MARK_PRICE" + # } + # ] + # } + # } + # + # linear swap + # + # { + # "code": 0, + # "msg": "", + # "data": { + # "orders": [ + # { + # "symbol": "LINK-USDT", + # "orderId": 1585839271162413056, + # "side": "BUY", + # "positionSide": "LONG", + # "type": "TRIGGER_MARKET", + # "origQty": "5.0", + # "price": "9", + # "executedQty": "0.0", + # "avgPrice": "0", + # "cumQuote": "0", + # "stopPrice": "5", + # "profit": "0.0000", + # "commission": "0.000000", + # "status": "CANCELLED", + # "time": 1667631605000, + # "updateTime": 1667631605000 + # }, + # ] + # } + # } + # + # twap + # + # { + # "code": 0, + # "msg": "", + # "timestamp": 1702731661854, + # "data": { + # "list": [ + # { + # "symbol": "BNB-USDT", + # "side": "BUY", + # "positionSide": "LONG", + # "priceType": "constant", + # "priceVariance": "2000", + # "triggerPrice": "68000", + # "interval": 8, + # "amountPerOrder": "0.111", + # "totalAmount": "0.511", + # "orderStatus": "Running", + # "executedQty": "0.1", + # "duration": 800, + # "maxDuration": 9000, + # "createdTime": 1702731661854, + # "updateTime": 1702731661854 + # } + # ], + # "total": 1 + # } + # } + # + data = self.safe_dict(response, 'data', {}) + orders = self.safe_list_2(data, 'orders', 'list', []) + return self.parse_orders(orders, market, since, limit) + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://bingx-api.github.io/docs/#/en-us/spot/trade-api.html#Query%20Order%20history + https://bingx-api.github.io/docs/#/en-us/swapV2/trade-api.html#Query%20Order%20history + https://bingx-api.github.io/docs/#/en-us/cswap/trade-api.html#User's%20History%20Orders + https://bingx-api.github.io/docs/#/standard/contract-interface.html#Historical%20order + + :param str symbol: unified market symbol of the closed orders + :param int [since]: timestamp in ms of the earliest order + :param int [limit]: the max number of closed orders to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch orders for + :param boolean [params.standard]: whether to fetch standard contract orders + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + orders = await self.fetch_canceled_and_closed_orders(symbol, since, limit, params) + return self.filter_by(orders, 'status', 'closed') + + async def fetch_canceled_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches information on multiple canceled orders made by the user + + https://bingx-api.github.io/docs/#/en-us/spot/trade-api.html#Query%20Order%20history + https://bingx-api.github.io/docs/#/en-us/swapV2/trade-api.html#Query%20Order%20history + https://bingx-api.github.io/docs/#/en-us/cswap/trade-api.html#User's%20History%20Orders + https://bingx-api.github.io/docs/#/standard/contract-interface.html#Historical%20order + + :param str symbol: unified market symbol of the canceled orders + :param int [since]: timestamp in ms of the earliest order + :param int [limit]: the max number of canceled orders to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch orders for + :param boolean [params.standard]: whether to fetch standard contract orders + :returns dict: a list of `order structures ` + """ + await self.load_markets() + orders = await self.fetch_canceled_and_closed_orders(symbol, since, limit, params) + return self.filter_by(orders, 'status', 'canceled') + + async def fetch_canceled_and_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches information on multiple closed orders made by the user + + https://bingx-api.github.io/docs/#/en-us/spot/trade-api.html#Query%20Order%20history + https://bingx-api.github.io/docs/#/en-us/swapV2/trade-api.html#Query%20Order%20history + https://bingx-api.github.io/docs/#/en-us/cswap/trade-api.html#User's%20History%20Orders + https://bingx-api.github.io/docs/#/standard/contract-interface.html#Historical%20order + https://bingx-api.github.io/docs/#/en-us/swapV2/trade-api.html#Query%20TWAP%20Historical%20Orders + + :param str [symbol]: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch orders for + :param boolean [params.standard]: whether to fetch standard contract orders + :param boolean [params.twap]: if fetching twap orders + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + market = None + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + type = None + subType = None + standard = None + response = None + type, params = self.handle_market_type_and_params('fetchClosedOrders', market, params) + subType, params = self.handle_sub_type_and_params('fetchClosedOrders', market, params) + standard, params = self.handle_option_and_params(params, 'fetchClosedOrders', 'standard', False) + if standard: + response = await self.contractV1PrivateGetAllOrders(self.extend(request, params)) + elif type == 'spot': + if limit is not None: + request['pageSize'] = limit + response = await self.spotV1PrivateGetTradeHistoryOrders(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "data": { + # "orders": [ + # { + # "symbol": "XRP-USDT", + # "orderId": 1514073325788200960, + # "price": "0.5", + # "origQty": "20", + # "executedQty": "0", + # "cummulativeQuoteQty": "0", + # "status": "PENDING", + # "type": "LIMIT", + # "side": "BUY", + # "time": 1649818185647, + # "updateTime": 1649818185647, + # "origQuoteOrderQty": "0" + # } + # ] + # } + # } + # + else: + isTwapOrder = self.safe_bool(params, 'twap', False) + params = self.omit(params, 'twap') + if isTwapOrder: + request['pageIndex'] = 1 + request['pageSize'] = 100 if (limit is None) else limit + request['startTime'] = 1 if (since is None) else since + until = self.safe_integer(params, 'until', self.milliseconds()) + params = self.omit(params, 'until') + request['endTime'] = until + response = await self.swapV1PrivateGetTwapHistoryOrders(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "timestamp": 1702731661854, + # "data": { + # "list": [ + # { + # "symbol": "BNB-USDT", + # "side": "BUY", + # "positionSide": "LONG", + # "priceType": "constant", + # "priceVariance": "2000", + # "triggerPrice": "68000", + # "interval": 8, + # "amountPerOrder": "0.111", + # "totalAmount": "0.511", + # "orderStatus": "Running", + # "executedQty": "0.1", + # "duration": 800, + # "maxDuration": 9000, + # "createdTime": 1702731661854, + # "updateTime": 1702731661854 + # } + # ], + # "total": 1 + # } + # } + # + elif subType == 'inverse': + response = await self.cswapV1PrivateGetTradeOrderHistory(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "data": { + # "orders": [ + # { + # "symbol": "SOL-USD", + # "orderId": "1816002957423951872", + # "side": "BUY", + # "positionSide": "LONG", + # "type": "LIMIT", + # "quantity": 1, + # "origQty": "10.00000000", + # "price": "150.000", + # "executedQty": "0.00000000", + # "avgPrice": "0.000", + # "cumQuote": "", + # "stopPrice": "0.000", + # "profit": "0.0000", + # "commission": "0.000000", + # "status": "Filled", + # "time": 1721803819000, + # "updateTime": 1721803856000, + # "clientOrderId": "", + # "leverage": "", + # "takeProfit": { + # "type": "", + # "quantity": 0, + # "stopPrice": 0, + # "price": 0, + # "workingType": "", + # "stopGuaranteed": "" + # }, + # "stopLoss": { + # "type": "", + # "quantity": 0, + # "stopPrice": 0, + # "price": 0, + # "workingType": "", + # "stopGuaranteed": "" + # }, + # "advanceAttr": 0, + # "positionID": 0, + # "takeProfitEntrustPrice": 0, + # "stopLossEntrustPrice": 0, + # "orderType": "", + # "workingType": "MARK_PRICE" + # }, + # ] + # } + # } + # + else: + response = await self.swapV2PrivateGetTradeAllOrders(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "data": { + # "orders": [ + # { + # "symbol": "LINK-USDT", + # "orderId": 1585839271162413056, + # "side": "BUY", + # "positionSide": "LONG", + # "type": "TRIGGER_MARKET", + # "origQty": "5.0", + # "price": "9", + # "executedQty": "0.0", + # "avgPrice": "0", + # "cumQuote": "0", + # "stopPrice": "5", + # "profit": "0.0000", + # "commission": "0.000000", + # "status": "CANCELLED", + # "time": 1667631605000, + # "updateTime": 1667631605000 + # }, + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + orders = self.safe_list_2(data, 'orders', 'list', []) + return self.parse_orders(orders, market, since, limit) + + async def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://bingx-api.github.io/docs/#/en-us/common/account-api.html#Asset%20Transfer%20New + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account to transfer from(spot, swap, futures, or funding) + :param str toAccount: account to transfer to(spot, swap(linear or inverse), future, or funding) + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transfer structure ` + """ + await self.load_markets() + currency = self.currency(code) + accountsByType = self.safe_dict(self.options, 'accountsByType', {}) + subType = None + subType, params = self.handle_sub_type_and_params('transfer', None, params) + fromId = self.safe_string(accountsByType, fromAccount, fromAccount) + toId = self.safe_string(accountsByType, toAccount, toAccount) + if fromId == 'swap': + if subType == 'inverse': + fromId = 'coinMPerp' + else: + fromId = 'USDTMPerp' + if toId == 'swap': + if subType == 'inverse': + toId = 'coinMPerp' + else: + toId = 'USDTMPerp' + request: dict = { + 'fromAccount': fromId, + 'toAccount': toId, + 'asset': currency['id'], + 'amount': self.currency_to_precision(code, amount), + } + response = await self.apiAssetV1PrivatePostTransfer(self.extend(request, params)) + # + # { + # "tranId": 1933130865269936128, + # "transferId": "1051450703949464903736" + # } + # + return { + 'info': response, + 'id': self.safe_string(response, 'transferId'), + 'timestamp': None, + 'datetime': None, + 'currency': code, + 'amount': amount, + 'fromAccount': fromAccount, + 'toAccount': toAccount, + 'status': None, + } + + async def fetch_transfers(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[TransferEntry]: + """ + fetch a history of internal transfers made on an account + + https://bingx-api.github.io/docs/#/en-us/common/account-api.html#Asset%20transfer%20records%20new + + :param str [code]: unified currency code of the currency transferred + :param int [since]: the earliest time in ms to fetch transfers for + :param int [limit]: the maximum number of transfers structures to retrieve(default 10, max 100) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str params.fromAccount:(mandatory) transfer from(spot, swap(linear or inverse), future, or funding) + :param str params.toAccount:(mandatory) transfer to(spot, swap(linear or inverse), future, or funding) + :param boolean [params.paginate]: whether to paginate the results(default False) + :returns dict[]: a list of `transfer structures ` + """ + await self.load_markets() + request: dict = {} + currency = None + if code is not None: + currency = self.currency(code) + accountsByType = self.safe_dict(self.options, 'accountsByType', {}) + fromAccount = self.safe_string(params, 'fromAccount') + toAccount = self.safe_string(params, 'toAccount') + fromId = self.safe_string(accountsByType, fromAccount, fromAccount) + toId = self.safe_string(accountsByType, toAccount, toAccount) + if fromId is None or toId is None: + raise ExchangeError(self.id + ' fromAccount & toAccount parameters are required') + if fromAccount is not None: + request['fromAccount'] = fromId + if toAccount is not None: + request['toAccount'] = toId + params = self.omit(params, ['fromAccount', 'toAccount']) + maxLimit = 100 + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchTransfers', 'paginate', False) + if paginate: + return await self.fetch_paginated_call_dynamic('fetchTransfers', None, since, limit, params, maxLimit) + if since is not None: + request['startTime'] = since + if limit is not None: + request['pageSize'] = limit + request, params = self.handle_until_option('endTime', request, params) + response = await self.apiV3PrivateGetAssetTransferRecord(self.extend(request, params)) + # + # { + # "total": 2, + # "rows": [ + # { + # "asset": "LTC", + # "amount": "0.05000000000000000000", + # "status": "CONFIRMED", + # "transferId": "1051461075661819338791", + # "timestamp": 1752202092000, + # "fromAccount": "spot", + # "toAccount": "USDTMPerp" + # } + # ] + # } + # + rows = self.safe_list(response, 'rows', []) + return self.parse_transfers(rows, currency, since, limit) + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + tranId = self.safe_string(transfer, 'transferId') + timestamp = self.safe_integer(transfer, 'timestamp') + currencyId = self.safe_string(transfer, 'asset') + currencyCode = self.safe_currency_code(currencyId, currency) + status = self.safe_string(transfer, 'status') + accountsById = self.safe_dict(self.options, 'accountsById', {}) + fromId = self.safe_string(transfer, 'fromAccount') + toId = self.safe_string(transfer, 'toAccount') + fromAccount = self.safe_string(accountsById, fromId, fromId) + toAccount = self.safe_string(accountsById, toId, toId) + return { + 'info': transfer, + 'id': tranId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'currency': currencyCode, + 'amount': self.safe_number(transfer, 'amount'), + 'fromAccount': fromAccount, + 'toAccount': toAccount, + 'status': self.parse_transfer_status(status), + } + + def parse_transfer_status(self, status: Str) -> str: + statuses: dict = { + 'CONFIRMED': 'ok', + } + return self.safe_string(statuses, status, status) + + async def fetch_deposit_addresses_by_network(self, code: str, params={}) -> List[DepositAddress]: + """ + fetch the deposit addresses for a currency associated with self account + + https://bingx-api.github.io/docs/#/en-us/common/wallet-api.html#Query%20Main%20Account%20Deposit%20Address + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary `address structures `, indexed by the network + """ + await self.load_markets() + currency = self.currency(code) + defaultRecvWindow = self.safe_integer(self.options, 'recvWindow') + recvWindow = self.safe_integer(self.parse_params, 'recvWindow', defaultRecvWindow) + request: dict = { + 'coin': currency['id'], + 'offset': 0, + 'limit': 1000, + 'recvWindow': recvWindow, + } + response = await self.walletsV1PrivateGetCapitalDepositAddress(self.extend(request, params)) + # + # { + # "code": "0", + # "timestamp": "1695200226859", + # "data": { + # "data": [ + # { + # "coinId": "799", + # "coin": "USDT", + # "network": "BEP20", + # "address": "6a7eda2817462dabb6493277a2cfe0f5c3f2550b", + # "tag": '' + # } + # ], + # "total": "1" + # } + # } + # + data = self.safe_list(self.safe_dict(response, 'data'), 'data') + parsed = self.parse_deposit_addresses(data, [currency['code']], False) + return self.index_by(parsed, 'network') + + async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://bingx-api.github.io/docs/#/en-us/common/wallet-api.html#Query%20Main%20Account%20Deposit%20Address + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.network]: The chain of currency. This only apply for multi-chain currency, and there is no need for single chain currency + :returns dict: an `address structure ` + """ + network = self.safe_string(params, 'network') + params = self.omit(params, ['network']) + addressStructures = await self.fetch_deposit_addresses_by_network(code, params) + if network is not None: + return self.safe_dict(addressStructures, network) + else: + options = self.safe_dict(self.options, 'defaultNetworks') + defaultNetworkForCurrency = self.safe_string(options, code) + if defaultNetworkForCurrency is not None: + return self.safe_dict(addressStructures, defaultNetworkForCurrency) + else: + keys = list(addressStructures.keys()) + key = self.safe_string(keys, 0) + return self.safe_dict(addressStructures, key) + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + # + # { + # "coinId":"4", + # "coin":"USDT", + # "network":"OMNI", + # "address":"1HXyx8HVQRY7Nhqz63nwnRB7SpS9xQPzLN", + # "addressWithPrefix":"1HXyx8HVQRY7Nhqz63nwnRB7SpS9xQPzLN" + # } + # + tag = self.safe_string(depositAddress, 'tag') + currencyId = self.safe_string(depositAddress, 'coin') + currency = self.safe_currency(currencyId, currency) + code = currency['code'] + address = self.safe_string(depositAddress, 'addressWithPrefix') + networkdId = self.safe_string(depositAddress, 'network') + networkCode = self.network_id_to_code(networkdId, code) + self.check_address(address) + return { + 'info': depositAddress, + 'currency': code, + 'network': networkCode, + 'address': address, + 'tag': tag, + } + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://bingx-api.github.io/docs/#/spot/account-api.html#Deposit%20History(supporting%20network) + + :param str [code]: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + request: dict = { + } + currency = None + if code is not None: + currency = self.currency(code) + request['coin'] = currency['id'] + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit # default 1000 + response = await self.spotV3PrivateGetCapitalDepositHisrec(self.extend(request, params)) + # + # [ + # { + # "amount":"0.00999800", + # "coin":"PAXG", + # "network":"ETH", + # "status":1, + # "address":"0x788cabe9236ce061e5a892e1a59395a81fc8d62c", + # "addressTag":"", + # "txId":"0xaad4654a3234aa6118af9b4b335f5ae81c360b2394721c019b5d1e75328b09f3", + # "insertTime":1599621997000, + # "transferType":0, + # "unlockConfirm":"12/12", # confirm times for unlocking + # "confirmTimes":"12/12" + # }, + # ] + # + return self.parse_transactions(response, currency, since, limit) + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://bingx-api.github.io/docs/#/spot/account-api.html#Withdraw%20History%20(supporting%20network) + + :param str [code]: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + request: dict = { + } + currency = None + if code is not None: + currency = self.currency(code) + request['coin'] = currency['id'] + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit # default 1000 + response = await self.spotV3PrivateGetCapitalWithdrawHistory(self.extend(request, params)) + # + # [ + # { + # "address": "0x94df8b352de7f46f64b01d3666bf6e936e44ce60", + # "amount": "8.91000000", + # "applyTime": "2019-10-12 11:12:02", + # "coin": "USDT", + # "id": "b6ae22b3aa844210a7041aee7589627c", + # "withdrawOrderId": "WITHDRAWtest123", + # "network": "ETH", + # "transferType": 0 + # "status": 6, + # "transactionFee": "0.004", + # "confirmNo":3, + # "info": "The address is not valid. Please confirm with the recipient", + # "txId": "0xb5ef8c13b968a406cc62a93a8bd80f9e9a906ef1b3fcf20a2e48573c17659268" + # }, + # ] + # + return self.parse_transactions(response, currency, since, limit) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fetchDeposits + # + # { + # "amount":"0.00999800", + # "coin":"PAXG", + # "network":"ETH", + # "status":1, + # "address":"0x788cabe9236ce061e5a892e1a59395a81fc8d62c", + # "addressTag":"", + # "txId":"0xaad4654a3234aa6118af9b4b335f5ae81c360b2394721c019b5d1e75328b09f3", + # "insertTime":1599621997000, + # "transferType":0, + # "unlockConfirm":"12/12", # confirm times for unlocking + # "confirmTimes":"12/12" + # } + # + # fetchWithdrawals + # + # { + # "address": "0x94df8b352de7f46f64b01d3666bf6e936e44ce60", + # "amount": "8.91000000", + # "applyTime": "2019-10-12 11:12:02", + # "coin": "USDT", + # "id": "b6ae22b3aa844210a7041aee7589627c", + # "withdrawOrderId": "WITHDRAWtest123", + # "network": "ETH", + # "transferType": 0 + # "status": 6, + # "transactionFee": "0.004", + # "confirmNo":3, + # "info": "The address is not valid. Please confirm with the recipient", + # "txId": "0xb5ef8c13b968a406cc62a93a8bd80f9e9a906ef1b3fcf20a2e48573c17659268" + # } + # + # withdraw + # + # { + # "code":0, + # "timestamp":1705274263621, + # "data":{ + # "id":"1264246141278773252" + # } + # } + # + # parse withdraw-type output first... + # + data = self.safe_value(transaction, 'data') + dataId = None if (data is None) else self.safe_string(data, 'id') + id = self.safe_string(transaction, 'id', dataId) + address = self.safe_string(transaction, 'address') + tag = self.safe_string(transaction, 'addressTag') + timestamp = self.safe_integer_2(transaction, 'insertTime', 'timestamp') + datetime = self.iso8601(timestamp) + if timestamp is None: + datetime = self.safe_string(transaction, 'applyTime') + timestamp = self.parse8601(datetime) + network = self.safe_string(transaction, 'network') + currencyId = self.safe_string(transaction, 'coin') + code = self.safe_currency_code(currencyId, currency) + if (code is not None) and (code != network) and code.find(network) >= 0: + if network is not None: + code = code.replace(network, '') + rawType = self.safe_string(transaction, 'transferType') + type = 'deposit' if (rawType == '0') else 'withdrawal' + return { + 'info': transaction, + 'id': id, + 'txid': self.safe_string(transaction, 'txId'), + 'type': type, + 'currency': code, + 'network': self.network_id_to_code(network), + 'amount': self.safe_number(transaction, 'amount'), + 'status': self.parse_transaction_status(self.safe_string(transaction, 'status')), + 'timestamp': timestamp, + 'datetime': datetime, + 'address': address, + 'addressFrom': None, + 'addressTo': address, + 'tag': tag, + 'tagFrom': tag, + 'tagTo': None, + 'updated': None, + 'comment': self.safe_string(transaction, 'info'), + 'fee': { + 'currency': code, + 'cost': self.safe_number(transaction, 'transactionFee'), + 'rate': None, + }, + 'internal': None, + } + + def parse_transaction_status(self, status: str): + statuses: dict = { + '0': 'pending', + '1': 'ok', + '10': 'pending', + '20': 'rejected', + '30': 'ok', + '40': 'rejected', + '50': 'ok', + '60': 'pending', + '70': 'rejected', + '2': 'pending', + '3': 'rejected', + '4': 'pending', + '5': 'rejected', + '6': 'ok', + } + return self.safe_string(statuses, status, status) + + async def set_margin_mode(self, marginMode: str, symbol: Str = None, params={}): + """ + set margin mode to 'cross' or 'isolated' + + https://bingx-api.github.io/docs/#/en-us/swapV2/trade-api.html#Change%20Margin%20Type + https://bingx-api.github.io/docs/#/en-us/cswap/trade-api.html#Set%20Margin%20Type + + :param str marginMode: 'cross' or 'isolated' + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setMarginMode() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + if market['type'] != 'swap': + raise BadSymbol(self.id + ' setMarginMode() supports swap contracts only') + marginMode = marginMode.upper() + if marginMode == 'CROSS': + marginMode = 'CROSSED' + if marginMode != 'ISOLATED' and marginMode != 'CROSSED': + raise BadRequest(self.id + ' setMarginMode() marginMode argument should be isolated or cross') + request: dict = { + 'symbol': market['id'], + 'marginType': marginMode, + } + subType = None + subType, params = self.handle_sub_type_and_params('setMarginMode', market, params) + if subType == 'inverse': + return await self.cswapV1PrivatePostTradeMarginType(self.extend(request, params)) + else: + return await self.swapV2PrivatePostTradeMarginType(self.extend(request, params)) + + async def add_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + request: dict = { + 'type': 1, + } + return await self.set_margin(symbol, amount, self.extend(request, params)) + + async def reduce_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + request: dict = { + 'type': 2, + } + return await self.set_margin(symbol, amount, self.extend(request, params)) + + async def set_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + Either adds or reduces margin in an isolated position in order to set the margin to a specific value + + https://bingx-api.github.io/docs/#/swapV2/trade-api.html#Adjust%20isolated%20margin + + :param str symbol: unified market symbol of the market to set margin in + :param float amount: the amount to set the margin to + :param dict [params]: parameters specific to the bingx api endpoint + :returns dict: A `margin structure ` + """ + type = self.safe_integer(params, 'type') # 1 increase margin 2 decrease margin + if type is None: + raise ArgumentsRequired(self.id + ' setMargin() requires a type parameter either 1(increase margin) or 2(decrease margin)') + if not self.in_array(type, [1, 2]): + raise ArgumentsRequired(self.id + ' setMargin() requires a type parameter either 1(increase margin) or 2(decrease margin)') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'amount': self.amount_to_precision(market['symbol'], amount), + 'type': type, + } + response = await self.swapV2PrivatePostTradePositionMargin(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "amount": 1, + # "type": 1 + # } + # + return self.parse_margin_modification(response, market) + + def parse_margin_modification(self, data: dict, market: Market = None) -> MarginModification: + # + # { + # "code": 0, + # "msg": "", + # "amount": 1, + # "type": 1 + # } + # + type = self.safe_string(data, 'type') + return { + 'info': data, + 'symbol': self.safe_string(market, 'symbol'), + 'type': 'add' if (type == '1') else 'reduce', + 'marginMode': 'isolated', + 'amount': self.safe_number(data, 'amount'), + 'total': self.safe_number(data, 'margin'), + 'code': self.safe_string(market, 'settle'), + 'status': None, + 'timestamp': None, + 'datetime': None, + } + + async def fetch_leverage(self, symbol: str, params={}) -> Leverage: + """ + fetch the set leverage for a market + + https://bingx-api.github.io/docs/#/swapV2/trade-api.html#Query%20Leverage + https://bingx-api.github.io/docs/#/en-us/cswap/trade-api.html#Query%20Leverage + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `leverage structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = None + if market['inverse']: + response = await self.cswapV1PrivateGetTradeLeverage(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "timestamp": 1720683803391, + # "data": { + # "symbol": "SOL-USD", + # "longLeverage": 5, + # "shortLeverage": 5, + # "maxLongLeverage": 50, + # "maxShortLeverage": 50, + # "availableLongVol": "4000000", + # "availableShortVol": "4000000" + # } + # } + # + else: + response = await self.swapV2PrivateGetTradeLeverage(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "data": { + # "longLeverage": 5, + # "shortLeverage": 5, + # "maxLongLeverage": 125, + # "maxShortLeverage": 125, + # "availableLongVol": "0.0000", + # "availableShortVol": "0.0000", + # "availableLongVal": "0.0", + # "availableShortVal": "0.0", + # "maxPositionLongVal": "0.0", + # "maxPositionShortVal": "0.0" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_leverage(data, market) + + def parse_leverage(self, leverage: dict, market: Market = None) -> Leverage: + # + # linear swap + # + # { + # "longLeverage": 5, + # "shortLeverage": 5, + # "maxLongLeverage": 125, + # "maxShortLeverage": 125, + # "availableLongVol": "0.0000", + # "availableShortVol": "0.0000", + # "availableLongVal": "0.0", + # "availableShortVal": "0.0", + # "maxPositionLongVal": "0.0", + # "maxPositionShortVal": "0.0" + # } + # + # inverse swap + # + # { + # "symbol": "SOL-USD", + # "longLeverage": 5, + # "shortLeverage": 5, + # "maxLongLeverage": 50, + # "maxShortLeverage": 50, + # "availableLongVol": "4000000", + # "availableShortVol": "4000000" + # } + # + marketId = self.safe_string(leverage, 'symbol') + return { + 'info': leverage, + 'symbol': self.safe_symbol(marketId, market), + 'marginMode': None, + 'longLeverage': self.safe_integer(leverage, 'longLeverage'), + 'shortLeverage': self.safe_integer(leverage, 'shortLeverage'), + } + + async def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://bingx-api.github.io/docs/#/swapV2/trade-api.html#Switch%20Leverage + https://bingx-api.github.io/docs/#/en-us/cswap/trade-api.html#Modify%20Leverage + + :param float leverage: the rate of leverage + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.side]: hedged: ['long' or 'short']. one way: ['both'] + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setLeverage() requires a symbol argument') + side = self.safe_string_upper(params, 'side') + self.check_required_argument('setLeverage', side, 'side', ['LONG', 'SHORT', 'BOTH']) + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'side': side, + 'leverage': leverage, + } + if market['inverse']: + return await self.cswapV1PrivatePostTradeLeverage(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "timestamp": 1720725058059, + # "data": { + # "symbol": "SOL-USD", + # "longLeverage": 10, + # "shortLeverage": 5, + # "maxLongLeverage": 50, + # "maxShortLeverage": 50, + # "availableLongVol": "4000000", + # "availableShortVol": "4000000" + # } + # } + # + else: + return await self.swapV2PrivatePostTradeLeverage(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "data": { + # "leverage": 10, + # "symbol": "BTC-USDT", + # "availableLongVol": "0.0000", + # "availableShortVol": "0.0000", + # "availableLongVal": "0.0", + # "availableShortVal": "0.0", + # "maxPositionLongVal": "0.0", + # "maxPositionShortVal": "0.0" + # } + # } + # + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://bingx-api.github.io/docs/#/en-us/spot/trade-api.html#Query%20transaction%20details + https://bingx-api.github.io/docs/#/en-us/swapV2/trade-api.html#Query%20historical%20transaction%20orders + https://bingx-api.github.io/docs/#/en-us/swapV2/trade-api.html#Query%20historical%20transaction%20details + https://bingx-api.github.io/docs/#/en-us/cswap/trade-api.html#Query%20Order%20Trade%20Detail + + :param str [symbol]: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms for the ending date filter, default is None + :param str params['trandingUnit']: COIN(directly represent assets such and ETH) or CONT(represents the number of contract sheets) + :param str params['orderId']: the order id required for inverse swap + :returns dict[]: a list of `trade structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMyTrades() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = {} + fills = None + response = None + subType = None + subType, params = self.handle_sub_type_and_params('fetchMyTrades', market, params) + if subType == 'inverse': + orderId = self.safe_string(params, 'orderId') + if orderId is None: + raise ArgumentsRequired(self.id + ' fetchMyTrades() requires an orderId argument for inverse swap trades') + response = await self.cswapV1PrivateGetTradeAllFillOrders(self.extend(request, params)) + fills = self.safe_list(response, 'data', []) + # + # { + # "code": 0, + # "msg": "", + # "timestamp": 1722147756019, + # "data": [ + # { + # "orderId": "1817441228670648320", + # "symbol": "SOL-USD", + # "type": "MARKET", + # "side": "BUY", + # "positionSide": "LONG", + # "tradeId": "97244554", + # "volume": "2", + # "tradePrice": "182.652", + # "amount": "20.00000000", + # "realizedPnl": "0.00000000", + # "commission": "-0.00005475", + # "currency": "SOL", + # "buyer": True, + # "maker": False, + # "tradeTime": 1722146730000 + # } + # ] + # } + # + else: + request['symbol'] = market['id'] + now = self.milliseconds() + if since is not None: + startTimeReq = 'startTime' if market['spot'] else 'startTs' + request[startTimeReq] = since + elif market['swap']: + request['startTs'] = now - 30 * 24 * 60 * 60 * 1000 # 30 days for swap + until = self.safe_integer(params, 'until') + params = self.omit(params, 'until') + if until is not None: + endTimeReq = 'endTime' if market['spot'] else 'endTs' + request[endTimeReq] = until + elif market['swap']: + request['endTs'] = now + if market['spot']: + if limit is not None: + request['limit'] = limit # default 500, maximum 1000 + response = await self.spotV1PrivateGetTradeMyTrades(self.extend(request, params)) + data = self.safe_dict(response, 'data', {}) + fills = self.safe_list(data, 'fills', []) + # + # { + # "code": 0, + # "msg": "", + # "debugMsg": "", + # "data": { + # "fills": [ + # { + # "symbol": "LTC-USDT", + # "id": 36237072, + # "orderId": 1674069326895775744, + # "price": "85.891", + # "qty": "0.0582", + # "quoteQty": "4.9988562000000005", + # "commission": -0.00005820000000000001, + # "commissionAsset": "LTC", + # "time": 1687964205000, + # "isBuyer": True, + # "isMaker": False + # } + # ] + # } + # } + # + else: + tradingUnit = self.safe_string_upper(params, 'tradingUnit', 'CONT') + params = self.omit(params, 'tradingUnit') + request['tradingUnit'] = tradingUnit + response = await self.swapV2PrivateGetTradeAllFillOrders(self.extend(request, params)) + data = self.safe_dict(response, 'data', {}) + fills = self.safe_list(data, 'fill_orders', []) + # + # { + # "code": "0", + # "msg": '', + # "data": {fill_orders: [ + # { + # "volume": "0.1", + # "price": "106.75", + # "amount": "10.6750", + # "commission": "-0.0053", + # "currency": "USDT", + # "orderId": "1676213270274379776", + # "liquidatedPrice": "0.00", + # "liquidatedMarginRatio": "0.00", + # "filledTime": "2023-07-04T20:56:01.000+0800" + # } + # ] + # } + # } + # + return self.parse_trades(fills, market, since, limit, params) + + def parse_deposit_withdraw_fee(self, fee, currency: Currency = None): + # + # currencie structure + # + networks = self.safe_dict(fee, 'networks', {}) + networkCodes = list(networks.keys()) + networksLength = len(networkCodes) + result: dict = { + 'info': networks, + 'withdraw': { + 'fee': None, + 'percentage': None, + }, + 'deposit': { + 'fee': None, + 'percentage': None, + }, + 'networks': {}, + } + if networksLength != 0: + for i in range(0, networksLength): + networkCode = networkCodes[i] + network = networks[networkCode] + result['networks'][networkCode] = { + 'deposit': {'fee': None, 'percentage': None}, + 'withdraw': {'fee': self.safe_number(network, 'fee'), 'percentage': False}, + } + if networksLength == 1: + result['withdraw']['fee'] = self.safe_number(network, 'withdrawFee') + result['withdraw']['percentage'] = False + return result + + async def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}): + """ + fetch deposit and withdraw fees + + https://bingx-api.github.io/docs/#/common/account-api.html#All%20Coins'%20Information + + :param str[]|None codes: list of unified currency codes + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `fee structures ` + """ + await self.load_markets() + response = await self.fetch_currencies(params) + depositWithdrawFees: dict = {} + responseCodes = list(response.keys()) + for i in range(0, len(responseCodes)): + code = responseCodes[i] + if (codes is None) or (self.in_array(code, codes)): + entry = response[code] + depositWithdrawFees[code] = self.parse_deposit_withdraw_fee(entry) + return depositWithdrawFees + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://bingx-api.github.io/docs/#/en-us/spot/wallet-api.html#Withdraw + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str [tag]: + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.walletType]: 1 fund(funding) account, 2 standard account, 3 perpetual account, 15 spot account + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.check_address(address) + await self.load_markets() + currency = self.currency(code) + defaultWalletType = 15 # spot + walletType = None + walletType, params = self.handle_option_and_params_2(params, 'withdraw', 'type', 'walletType', defaultWalletType) + walletTypes = { + 'funding': 1, + 'fund': 1, + 'standard': 2, + 'perpetual': 3, + 'spot': 15, + } + walletType = self.safe_integer(walletTypes, walletType, defaultWalletType) + request: dict = { + 'coin': currency['id'], + 'address': address, + 'amount': self.currency_to_precision(code, amount), + 'walletType': walletType, + } + network = self.safe_string_upper(params, 'network') + if network is not None: + request['network'] = self.network_code_to_id(network) + if tag is not None: + request['addressTag'] = tag + params = self.omit(params, ['walletType', 'network']) + response = await self.walletsV1PrivatePostCapitalWithdrawApply(self.extend(request, params)) + data = self.safe_value(response, 'data') + # { + # "code":0, + # "timestamp":1689258953651, + # "data":{ + # "id":"1197073063359000577" + # } + # } + return self.parse_transaction(data) + + def parse_params(self, params): + # sortedParams = self.keysort(params) + rawKeys = list(params.keys()) + keys = self.sort(rawKeys) + for i in range(0, len(keys)): + key = keys[i] + value = params[key] + if isinstance(value, list): + arrStr = '[' + for j in range(0, len(value)): + arrayElement = value[j] + if j > 0: + arrStr += ',' + arrStr += str(arrayElement) + arrStr += ']' + params[key] = arrStr + return params + + async def fetch_my_liquidations(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + retrieves the users liquidated positions + + https://bingx-api.github.io/docs/#/swapV2/trade-api.html#User's%20Force%20Orders + https://bingx-api.github.io/docs/#/en-us/cswap/trade-api.html#Query%20force%20orders + + :param str [symbol]: unified CCXT market symbol + :param int [since]: the earliest time in ms to fetch liquidations for + :param int [limit]: the maximum number of liquidation structures to retrieve + :param dict [params]: exchange specific parameters for the bingx api endpoint + :param int [params.until]: timestamp in ms of the latest liquidation + :returns dict: an array of `liquidation structures ` + """ + await self.load_markets() + request: dict = { + 'autoCloseType': 'LIQUIDATION', + } + request, params = self.handle_until_option('endTime', request, params) + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + subType = None + subType, params = self.handle_sub_type_and_params('fetchMyLiquidations', market, params) + response = None + liquidations = None + if subType == 'inverse': + response = await self.cswapV1PrivateGetTradeForceOrders(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "timestamp": 1721280071678, + # "data": [ + # { + # "orderId": "string", + # "symbol": "string", + # "type": "string", + # "side": "string", + # "positionSide": "string", + # "price": "string", + # "quantity": "float64", + # "stopPrice": "string", + # "workingType": "string", + # "status": "string", + # "time": "int64", + # "avgPrice": "string", + # "executedQty": "string", + # "profit": "string", + # "commission": "string", + # "updateTime": "string" + # } + # ] + # } + # + liquidations = self.safe_list(response, 'data', []) + else: + response = await self.swapV2PrivateGetTradeForceOrders(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "data": { + # "orders": [ + # { + # "time": "int64", + # "symbol": "string", + # "side": "string", + # "type": "string", + # "positionSide": "string", + # "cumQuote": "string", + # "status": "string", + # "stopPrice": "string", + # "price": "string", + # "origQty": "string", + # "avgPrice": "string", + # "executedQty": "string", + # "orderId": "int64", + # "profit": "string", + # "commission": "string", + # "workingType": "string", + # "updateTime": "int64" + # }, + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + liquidations = self.safe_list(data, 'orders', []) + return self.parse_liquidations(liquidations, market, since, limit) + + def parse_liquidation(self, liquidation, market: Market = None): + # + # { + # "time": "int64", + # "symbol": "string", + # "side": "string", + # "type": "string", + # "positionSide": "string", + # "cumQuote": "string", + # "status": "string", + # "stopPrice": "string", + # "price": "string", + # "origQty": "string", + # "avgPrice": "string", + # "executedQty": "string", + # "orderId": "int64", + # "profit": "string", + # "commission": "string", + # "workingType": "string", + # "updateTime": "int64" + # } + # + marketId = self.safe_string(liquidation, 'symbol') + timestamp = self.safe_integer(liquidation, 'time') + contractsString = self.safe_string(liquidation, 'executedQty') + contractSizeString = self.safe_string(market, 'contractSize') + priceString = self.safe_string(liquidation, 'avgPrice') + baseValueString = Precise.string_mul(contractsString, contractSizeString) + quoteValueString = Precise.string_mul(baseValueString, priceString) + return self.safe_liquidation({ + 'info': liquidation, + 'symbol': self.safe_symbol(marketId, market), + 'contracts': self.parse_number(contractsString), + 'contractSize': self.parse_number(contractSizeString), + 'price': self.parse_number(priceString), + 'baseValue': self.parse_number(baseValueString), + 'quoteValue': self.parse_number(quoteValueString), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + + async def close_position(self, symbol: str, side: OrderSide = None, params={}) -> Order: + """ + closes open positions for a market + + https://bingx-api.github.io/docs/#/en-us/swapV2/trade-api.html#One-Click%20Close%20All%20Positions + https://bingx-api.github.io/docs/#/en-us/cswap/trade-api.html#Close%20all%20positions%20in%20bulk + + :param str symbol: Unified CCXT market symbol + :param str [side]: not used by bingx + :param dict [params]: extra parameters specific to the bingx api endpoint + :param str|None [params.positionId]: the id of the position you would like to close + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + positionId = self.safe_string(params, 'positionId') + request: dict = {} + response = None + if positionId is not None: + response = await self.swapV1PrivatePostTradeClosePosition(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "timestamp": 1710992264190, + # "data": { + # "orderId": 1770656007907930112, + # "positionId": "1751667128353910784", + # "symbol": "LTC-USDT", + # "side": "Ask", + # "type": "MARKET", + # "positionSide": "Long", + # "origQty": "0.2" + # } + # } + # + else: + request['symbol'] = market['id'] + if market['inverse']: + response = await self.cswapV1PrivatePostTradeCloseAllPositions(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "timestamp": 1720771601428, + # "data": { + # "success": ["1811673520637231104"], + # "failed": null + # } + # } + # + else: + response = await self.swapV2PrivatePostTradeCloseAllPositions(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "data": { + # "success": [ + # 1727686766700486656, + # ], + # "failed": null + # } + # } + # + data = self.safe_dict(response, 'data') + return self.parse_order(data, market) + + async def close_all_positions(self, params={}) -> List[Position]: + """ + closes open positions for a market + + https://bingx-api.github.io/docs/#/en-us/swapV2/trade-api.html#One-Click%20Close%20All%20Positions + https://bingx-api.github.io/docs/#/en-us/cswap/trade-api.html#Close%20all%20positions%20in%20bulk + + :param dict [params]: extra parameters specific to the bingx api endpoint + :param str [params.recvWindow]: request valid time window value + :returns dict[]: `a list of position structures ` + """ + await self.load_markets() + defaultRecvWindow = self.safe_integer(self.options, 'recvWindow') + recvWindow = self.safe_integer(self.parse_params, 'recvWindow', defaultRecvWindow) + marketType = None + marketType, params = self.handle_market_type_and_params('closeAllPositions', None, params) + subType = None + subType, params = self.handle_sub_type_and_params('closeAllPositions', None, params) + if marketType == 'margin': + raise BadRequest(self.id + ' closePositions() cannot be used for ' + marketType + ' markets') + request: dict = { + 'recvWindow': recvWindow, + } + response = None + if subType == 'inverse': + response = await self.cswapV1PrivatePostTradeCloseAllPositions(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "timestamp": 1720771601428, + # "data": { + # "success": ["1811673520637231104"], + # "failed": null + # } + # } + # + else: + response = await self.swapV2PrivatePostTradeCloseAllPositions(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "data": { + # "success": [ + # 1727686766700486656, + # 1727686767048613888 + # ], + # "failed": null + # } + # } + # + data = self.safe_dict(response, 'data', {}) + success = self.safe_list(data, 'success', []) + positions = [] + for i in range(0, len(success)): + position = self.parse_position({'positionId': success[i]}) + positions.append(position) + return positions + + async def fetch_position_mode(self, symbol: Str = None, params={}): + """ + fetchs the position mode, hedged or one way, hedged for binance is set identically for all linear markets or all inverse markets + + https://bingx-api.github.io/docs/#/en-us/swapV2/trade-api.html#Get%20Position%20Mode + + :param str symbol: unified symbol of the market to fetch the order book for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an object detailing whether the market is in hedged or one-way mode + """ + response = await self.swapV1PrivateGetPositionSideDual(params) + # + # { + # "code": "0", + # "msg": "", + # "timeStamp": "1709002057516", + # "data": { + # "dualSidePosition": "false" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + dualSidePosition = self.safe_string(data, 'dualSidePosition') + return { + 'info': response, + 'hedged': (dualSidePosition == 'true'), + } + + async def set_position_mode(self, hedged: bool, symbol: Str = None, params={}): + """ + set hedged to True or False for a market + + https://bingx-api.github.io/docs/#/en-us/swapV2/trade-api.html#Set%20Position%20Mode + + :param bool hedged: set to True to use dualSidePosition + :param str symbol: not used by bingx setPositionMode() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + dualSidePosition = None + if hedged: + dualSidePosition = 'true' + else: + dualSidePosition = 'false' + request: dict = { + 'dualSidePosition': dualSidePosition, + } + # + # { + # code: '0', + # msg: '', + # timeStamp: '1703327432734', + # data: {dualSidePosition: 'false'} + # } + # + return await self.swapV1PrivatePostPositionSideDual(self.extend(request, params)) + + async def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}) -> Order: + """ + cancels an order and places a new order + + https://bingx-api.github.io/docs/#/en-us/spot/trade-api.html#Cancel%20order%20and%20place%20a%20new%20order # spot + https://bingx-api.github.io/docs/#/en-us/swapV2/trade-api.html#Cancel%20an%20order%20and%20then%20Place%20a%20new%20order # swap + + :param str id: order id + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of the currency you want to trade in units of the base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.triggerPrice]: Trigger price used for TAKE_STOP_LIMIT, TAKE_STOP_MARKET, TRIGGER_LIMIT, TRIGGER_MARKET order types. + :param dict [params.takeProfit]: *takeProfit object in params* containing the triggerPrice at which the attached take profit order will be triggered + :param float [params.takeProfit.triggerPrice]: take profit trigger price + :param dict [params.stopLoss]: *stopLoss object in params* containing the triggerPrice at which the attached stop loss order will be triggered + :param float [params.stopLoss.triggerPrice]: stop loss trigger price + + EXCHANGE SPECIFIC PARAMETERS + :param str [params.cancelClientOrderID]: the user-defined id of the order to be canceled, 1-40 characters, different orders cannot use the same clientOrderID, only supports a query range of 2 hours + :param str [params.cancelRestrictions]: cancel orders with specified status, NEW: New order, PENDING: Pending order, PARTIALLY_FILLED: Partially filled + :param str [params.cancelReplaceMode]: STOP_ON_FAILURE - if the cancel order fails, it will not continue to place a new order, ALLOW_FAILURE - regardless of whether the cancel order succeeds or fails, it will continue to place a new order + :param float [params.quoteOrderQty]: order amount + :param str [params.newClientOrderId]: custom order id consisting of letters, numbers, and _, 1-40 characters, different orders cannot use the same newClientOrderId. + :param str [params.positionSide]: *contract only* position direction, required for single position, for both long and short positions only LONG or SHORT can be chosen, defaults to LONG if empty + :param str [params.reduceOnly]: *contract only* True or False, default=false for single position mode. self parameter is not accepted for both long and short positions mode + :param float [params.priceRate]: *contract only* for type TRAILING_STOP_Market or TRAILING_TP_SL, Max = 1 + :param str [params.workingType]: *contract only* StopPrice trigger price types, MARK_PRICE(default), CONTRACT_PRICE, or INDEX_PRICE + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + request = self.create_order_request(symbol, type, side, amount, price, params) + request['cancelOrderId'] = id + request['cancelReplaceMode'] = 'STOP_ON_FAILURE' + response = None + if market['swap']: + response = await self.swapV1PrivatePostTradeCancelReplace(self.extend(request, params)) + # + # { + # code: '0', + # msg: '', + # data: { + # cancelResult: 'true', + # cancelMsg: '', + # cancelResponse: { + # cancelClientOrderId: '', + # cancelOrderId: '1755336244265705472', + # symbol: 'SOL-USDT', + # orderId: '1755336244265705472', + # side: 'SELL', + # positionSide: 'SHORT', + # type: 'LIMIT', + # origQty: '1', + # price: '100.000', + # executedQty: '0', + # avgPrice: '0.000', + # cumQuote: '0', + # stopPrice: '', + # profit: '0.0000', + # commission: '0.000000', + # status: 'PENDING', + # time: '1707339747860', + # updateTime: '1707339747860', + # clientOrderId: '', + # leverage: '20X', + # workingType: 'MARK_PRICE', + # onlyOnePosition: False, + # reduceOnly: False + # }, + # replaceResult: 'true', + # replaceMsg: '', + # newOrderResponse: { + # orderId: '1755338440612995072', + # symbol: 'SOL-USDT', + # positionSide: 'SHORT', + # side: 'SELL', + # type: 'LIMIT', + # price: '99', + # quantity: '2', + # stopPrice: '0', + # workingType: 'MARK_PRICE', + # clientOrderID: '', + # timeInForce: 'GTC', + # priceRate: '0', + # stopLoss: '', + # takeProfit: '', + # reduceOnly: False + # } + # } + # } + # + else: + response = await self.spotV1PrivatePostTradeOrderCancelReplace(self.extend(request, params)) + # + # { + # code: '0', + # msg: '', + # debugMsg: '', + # data: { + # cancelResult: {code: '0', msg: '', result: True}, + # openResult: {code: '0', msg: '', result: True}, + # orderOpenResponse: { + # symbol: 'SOL-USDT', + # orderId: '1755334007697866752', + # transactTime: '1707339214620', + # price: '99', + # stopPrice: '0', + # origQty: '0.2', + # executedQty: '0', + # cummulativeQuoteQty: '0', + # status: 'PENDING', + # type: 'LIMIT', + # side: 'SELL', + # clientOrderID: '' + # }, + # orderCancelResponse: { + # symbol: 'SOL-USDT', + # orderId: '1755117055251480576', + # price: '100', + # stopPrice: '0', + # origQty: '0.2', + # executedQty: '0', + # cummulativeQuoteQty: '0', + # status: 'CANCELED', + # type: 'LIMIT', + # side: 'SELL' + # } + # } + # } + # + data = self.safe_dict(response, 'data') + return self.parse_order(data, market) + + async def fetch_margin_mode(self, symbol: str, params={}) -> MarginMode: + """ + fetches the margin mode of the trading pair + + https://bingx-api.github.io/docs/#/en-us/swapV2/trade-api.html#Query%20Margin%20Type + https://bingx-api.github.io/docs/#/en-us/cswap/trade-api.html#Query%20Margin%20Type + + :param str symbol: unified symbol of the market to fetch the margin mode for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin mode structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + subType = None + response = None + subType, params = self.handle_sub_type_and_params('fetchMarginMode', market, params) + if subType == 'inverse': + response = await self.cswapV1PrivateGetTradeMarginType(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "timestamp": 1721966069132, + # "data": { + # "symbol": "SOL-USD", + # "marginType": "CROSSED" + # } + # } + # + else: + response = await self.swapV2PrivateGetTradeMarginType(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "data": { + # "marginType": "CROSSED" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_margin_mode(data, market) + + def parse_margin_mode(self, marginMode: dict, market=None) -> MarginMode: + marketId = self.safe_string(marginMode, 'symbol') + marginType = self.safe_string_lower(marginMode, 'marginType') + marginType = 'cross' if (marginType == 'crossed') else marginType + return { + 'info': marginMode, + 'symbol': self.safe_symbol(marketId, market, '-', 'swap'), + 'marginMode': marginType, + } + + async def fetch_trading_fee(self, symbol: str, params={}) -> TradingFeeInterface: + """ + fetch the trading fees for a market + + https://bingx-api.github.io/docs/#/en-us/spot/trade-api.html#Query%20Trading%20Commission%20Rate + https://bingx-api.github.io/docs/#/en-us/swapV2/account-api.html#Query%20Trading%20Commission%20Rate + https://bingx-api.github.io/docs/#/en-us/cswap/trade-api.html#Query%20Trade%20Commission%20Rate + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `fee structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = None + commission: dict = {} + data = self.safe_dict(response, 'data', {}) + if market['spot']: + response = await self.spotV1PrivateGetUserCommissionRate(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "debugMsg": "", + # "data": { + # "takerCommissionRate": 0.001, + # "makerCommissionRate": 0.001 + # } + # } + # + commission = data + else: + if market['inverse']: + response = await self.cswapV1PrivateGetUserCommissionRate(params) + # + # { + # "code": 0, + # "msg": "", + # "timestamp": 1721365261438, + # "data": { + # "takerCommissionRate": "0.0005", + # "makerCommissionRate": "0.0002" + # } + # } + # + commission = data + else: + response = await self.swapV2PrivateGetUserCommissionRate(params) + # + # { + # "code": 0, + # "msg": "", + # "data": { + # "commission": { + # "takerCommissionRate": 0.0005, + # "makerCommissionRate": 0.0002 + # } + # } + # } + # + commission = self.safe_dict(data, 'commission', {}) + return self.parse_trading_fee(commission, market) + + def parse_trading_fee(self, fee: dict, market: Market = None) -> TradingFeeInterface: + # + # { + # "takerCommissionRate": 0.001, + # "makerCommissionRate": 0.001 + # } + # + symbol = market['symbol'] if (market is not None) else None + return { + 'info': fee, + 'symbol': symbol, + 'maker': self.safe_number(fee, 'makerCommissionRate'), + 'taker': self.safe_number(fee, 'takerCommissionRate'), + 'percentage': False, + 'tierBased': False, + } + + def custom_encode(self, params): + # sortedParams = self.keysort(params) + rawKeys = list(params.keys()) + keys = self.sort(rawKeys) + adjustedValue = None + result = None + for i in range(0, len(keys)): + key = keys[i] + value = params[key] + if isinstance(value, list): + arrStr = None + for j in range(0, len(value)): + arrayElement = value[j] + isString = (isinstance(arrayElement, str)) + if isString: + if j > 0: + arrStr += ',' + '"' + str(arrayElement) + '"' + else: + arrStr = '"' + str(arrayElement) + '"' + else: + if j > 0: + arrStr += ',' + str(arrayElement) + else: + arrStr = str(arrayElement) + adjustedValue = '[' + arrStr + ']' + value = adjustedValue + if i == 0: + result = key + '=' + value + else: + result += '&' + key + '=' + value + return result + + def sign(self, path, section='public', method='GET', params={}, headers=None, body=None): + type = section[0] + version = section[1] + access = section[2] + isSandbox = self.safe_bool(self.options, 'sandboxMode', False) + if isSandbox and (type != 'swap'): + raise NotSupported(self.id + ' does not have a testnet/sandbox URL for ' + type + ' endpoints') + url = self.implode_hostname(self.urls['api'][type]) + path = self.implode_params(path, params) + versionIsTransfer = (version == 'transfer') + versionIsAsset = (version == 'asset') + if versionIsTransfer or versionIsAsset: + if versionIsTransfer: + type = 'account/transfer' + else: + type = 'api/asset' + version = section[2] + access = section[3] + if path != 'account/apiPermissions': + if type == 'spot' and version == 'v3': + url += '/api' + else: + url += '/' + type + url += '/' + version + '/' + path + params = self.omit(params, self.extract_params(path)) + params['timestamp'] = self.nonce() + params = self.keysort(params) + if access == 'public': + if params: + url += '?' + self.urlencode(params) + elif access == 'private': + self.check_required_credentials() + isJsonContentType = (((type == 'subAccount') or (type == 'account/transfer')) and (method == 'POST')) + parsedParams = None + encodeRequest = None + if isJsonContentType: + encodeRequest = self.custom_encode(params) + else: + parsedParams = self.parse_params(params) + encodeRequest = self.rawencode(parsedParams, True) + signature = self.hmac(self.encode(encodeRequest), self.encode(self.secret), hashlib.sha256) + headers = { + 'X-BX-APIKEY': self.apiKey, + 'X-SOURCE-KEY': self.safe_string(self.options, 'broker', 'CCXT'), + } + if isJsonContentType: + headers['Content-Type'] = 'application/json' + params['signature'] = signature + body = self.json(params) + else: + query = self.urlencode(parsedParams, True) + url += '?' + query + '&' + 'signature=' + signature + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def nonce(self): + return self.milliseconds() + + def set_sandbox_mode(self, enable: bool): + super(bingx, self).set_sandbox_mode(enable) + self.options['sandboxMode'] = enable + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None # fallback to default error handler + # + # { + # "code": 80014, + # "msg": "Invalid parameters, err:Key: 'GetTickerRequest.Symbol' Error:Field validation for "Symbol" failed on the "len=0|endswith=-USDT" tag", + # "data": { + # } + # } + # + code = self.safe_string(response, 'code') + message = self.safe_string(response, 'msg') + transferErrorMsg = self.safe_string(response, 'transferErrorMsg') # handling with errors from transfer endpoint + if (transferErrorMsg is not None) or (code is not None and code != '0'): + if transferErrorMsg is not None: + message = transferErrorMsg + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], code, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + raise ExchangeError(feedback) # unknown message + return None diff --git a/ccxt/async_support/bit2c.py b/ccxt/async_support/bit2c.py new file mode 100644 index 0000000..775809e --- /dev/null +++ b/ccxt/async_support/bit2c.py @@ -0,0 +1,986 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.bit2c import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currency, DepositAddress, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Ticker, Trade, TradingFees +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import NotSupported +from ccxt.base.errors import InvalidNonce +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class bit2c(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(bit2c, self).describe(), { + 'id': 'bit2c', + 'name': 'Bit2C', + 'countries': ['IL'], # Israel + 'rateLimit': 3000, + 'pro': False, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelAllOrders': False, + 'cancelOrder': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createOrder': True, + 'createOrderWithTakeProfitAndStopLoss': False, + 'createOrderWithTakeProfitAndStopLossWs': False, + 'createReduceOnlyOrder': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkOHLCV': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': True, + 'fetchTransfer': False, + 'fetchTransfers': False, + 'fetchUnderlyingAssets': False, + 'fetchVolatilityHistory': False, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'repayMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'transfer': False, + 'ws': False, + }, + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/db0bce50-6842-4c09-a1d5-0c87d22118aa', + 'api': { + 'rest': 'https://bit2c.co.il', + }, + 'www': 'https://www.bit2c.co.il', + 'referral': 'https://bit2c.co.il/Aff/63bfed10-e359-420c-ab5a-ad368dab0baf', + 'doc': [ + 'https://www.bit2c.co.il/home/api', + 'https://github.com/OferE/bit2c', + ], + }, + 'api': { + 'public': { + 'get': [ + 'Exchanges/{pair}/Ticker', + 'Exchanges/{pair}/orderbook', + 'Exchanges/{pair}/trades', + 'Exchanges/{pair}/lasttrades', + ], + }, + 'private': { + 'post': [ + 'Merchant/CreateCheckout', + 'Funds/AddCoinFundsRequest', + 'Order/AddFund', + 'Order/AddOrder', + 'Order/GetById', + 'Order/AddOrderMarketPriceBuy', + 'Order/AddOrderMarketPriceSell', + 'Order/CancelOrder', + 'Order/AddCoinFundsRequest', + 'Order/AddStopOrder', + 'Payment/GetMyId', + 'Payment/Send', + 'Payment/Pay', + ], + 'get': [ + 'Account/Balance', + 'Account/Balance/v2', + 'Order/MyOrders', + 'Order/GetById', + 'Order/AccountHistory', + 'Order/OrderHistory', + ], + }, + }, + 'markets': { + 'BTC/NIS': self.safe_market_structure({'id': 'BtcNis', 'symbol': 'BTC/NIS', 'base': 'BTC', 'quote': 'NIS', 'baseId': 'Btc', 'quoteId': 'Nis', 'type': 'spot', 'spot': True}), + 'ETH/NIS': self.safe_market_structure({'id': 'EthNis', 'symbol': 'ETH/NIS', 'base': 'ETH', 'quote': 'NIS', 'baseId': 'Eth', 'quoteId': 'Nis', 'type': 'spot', 'spot': True}), + 'LTC/NIS': self.safe_market_structure({'id': 'LtcNis', 'symbol': 'LTC/NIS', 'base': 'LTC', 'quote': 'NIS', 'baseId': 'Ltc', 'quoteId': 'Nis', 'type': 'spot', 'spot': True}), + 'USDC/NIS': self.safe_market_structure({'id': 'UsdcNis', 'symbol': 'USDC/NIS', 'base': 'USDC', 'quote': 'NIS', 'baseId': 'Usdc', 'quoteId': 'Nis', 'type': 'spot', 'spot': True}), + }, + 'fees': { + 'trading': { + 'tierBased': True, + 'percentage': True, + 'maker': self.parse_number('0.025'), + 'taker': self.parse_number('0.03'), + 'tiers': { + 'taker': [ + [self.parse_number('0'), self.parse_number('0.03')], + [self.parse_number('20000'), self.parse_number('0.0275')], + [self.parse_number('50000'), self.parse_number('0.025')], + [self.parse_number('75000'), self.parse_number('0.0225')], + [self.parse_number('100000'), self.parse_number('0.02')], + [self.parse_number('250000'), self.parse_number('0.015')], + [self.parse_number('500000'), self.parse_number('0.0125')], + [self.parse_number('750000'), self.parse_number('0.01')], + [self.parse_number('1000000'), self.parse_number('0.008')], + [self.parse_number('2000000'), self.parse_number('0.006')], + [self.parse_number('3000000'), self.parse_number('0.004')], + [self.parse_number('4000000'), self.parse_number('0.002')], + ], + 'maker': [ + [self.parse_number('0'), self.parse_number('0.025')], + [self.parse_number('20000'), self.parse_number('0.0225')], + [self.parse_number('50000'), self.parse_number('0.02')], + [self.parse_number('75000'), self.parse_number('0.0175')], + [self.parse_number('100000'), self.parse_number('0.015')], + [self.parse_number('250000'), self.parse_number('0.01')], + [self.parse_number('500000'), self.parse_number('0.0075')], + [self.parse_number('750000'), self.parse_number('0.005')], + [self.parse_number('1000000'), self.parse_number('0.004')], + [self.parse_number('2000000'), self.parse_number('0.003')], + [self.parse_number('3000000'), self.parse_number('0.002')], + [self.parse_number('4000000'), self.parse_number('0.001')], + ], + }, + }, + }, + 'options': { + 'fetchTradesMethod': 'public_get_exchanges_pair_trades', + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': False, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': False, + 'FOK': False, + 'PO': False, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyRequiresPrice': False, + 'marketBuyByCost': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 30, + 'untilDays': 30, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOrders': None, + 'fetchClosedOrders': None, # todo implement + 'fetchOHLCV': None, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'exact': { + 'Please provide valid APIkey': AuthenticationError, # {"error" : "Please provide valid APIkey"} + 'No order found.': OrderNotFound, # {"Error" : "No order found."} + }, + 'broad': { + # {"error": "Please provide valid nonce in Request Nonce(1598218490) is not bigger than last nonce(1598218490)."} + # {"error": "Please provide valid nonce in Request UInt64.TryParse failed for nonce :"} + 'Please provide valid nonce': InvalidNonce, + 'please approve new terms of use on site': PermissionDenied, # {"error" : "please approve new terms of use on site."} + }, + }, + }) + + def parse_balance(self, response) -> Balances: + result: dict = { + 'info': response, + 'timestamp': None, + 'datetime': None, + } + codes = list(self.currencies.keys()) + for i in range(0, len(codes)): + code = codes[i] + account = self.account() + currency = self.currency(code) + uppercase = currency['id'].upper() + if uppercase in response: + account['free'] = self.safe_string(response, 'AVAILABLE_' + uppercase) + account['total'] = self.safe_string(response, uppercase) + result[code] = account + return self.safe_balance(result) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://bit2c.co.il/home/api#balance + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + response = await self.privateGetAccountBalanceV2(params) + # + # { + # "AVAILABLE_NIS": 0.0, + # "NIS": 0.0, + # "LOCKED_NIS": 0.0, + # "AVAILABLE_BTC": 0.0, + # "BTC": 0.0, + # "LOCKED_BTC": 0.0, + # "AVAILABLE_ETH": 0.0, + # "ETH": 0.0, + # "LOCKED_ETH": 0.0, + # "AVAILABLE_BCHSV": 0.0, + # "BCHSV": 0.0, + # "LOCKED_BCHSV": 0.0, + # "AVAILABLE_BCHABC": 0.0, + # "BCHABC": 0.0, + # "LOCKED_BCHABC": 0.0, + # "AVAILABLE_LTC": 0.0, + # "LTC": 0.0, + # "LOCKED_LTC": 0.0, + # "AVAILABLE_ETC": 0.0, + # "ETC": 0.0, + # "LOCKED_ETC": 0.0, + # "AVAILABLE_BTG": 0.0, + # "BTG": 0.0, + # "LOCKED_BTG": 0.0, + # "AVAILABLE_GRIN": 0.0, + # "GRIN": 0.0, + # "LOCKED_GRIN": 0.0, + # "Fees": { + # "BtcNis": {"FeeMaker": 1.0, "FeeTaker": 1.0}, + # "EthNis": {"FeeMaker": 1.0, "FeeTaker": 1.0}, + # "BchabcNis": {"FeeMaker": 1.0, "FeeTaker": 1.0}, + # "LtcNis": {"FeeMaker": 1.0, "FeeTaker": 1.0}, + # "EtcNis": {"FeeMaker": 1.0, "FeeTaker": 1.0}, + # "BtgNis": {"FeeMaker": 1.0, "FeeTaker": 1.0}, + # "LtcBtc": {"FeeMaker": 1.0, "FeeTaker": 1.0}, + # "BchsvNis": {"FeeMaker": 1.0, "FeeTaker": 1.0}, + # "GrinNis": {"FeeMaker": 1.0, "FeeTaker": 1.0} + # } + # } + # + return self.parse_balance(response) + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://bit2c.co.il/home/api#orderb + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + orderbook = await self.publicGetExchangesPairOrderbook(self.extend(request, params)) + return self.parse_order_book(orderbook, symbol) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + symbol = self.safe_symbol(None, market) + averagePrice = self.safe_string(ticker, 'av') + baseVolume = self.safe_string(ticker, 'a') + last = self.safe_string(ticker, 'll') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': None, + 'datetime': None, + 'high': None, + 'low': None, + 'bid': self.safe_string(ticker, 'h'), + 'bidVolume': None, + 'ask': self.safe_string(ticker, 'l'), + 'askVolume': None, + 'vwap': None, + 'open': None, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': averagePrice, + 'baseVolume': baseVolume, + 'quoteVolume': None, + 'info': ticker, + }, market) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://bit2c.co.il/home/api#ticker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + response = await self.publicGetExchangesPairTicker(self.extend(request, params)) + return self.parse_ticker(response, market) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://bit2c.co.il/home/api#transactions + https://bit2c.co.il/home/api#trades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + method = self.options['fetchTradesMethod'] # public_get_exchanges_pair_trades or public_get_exchanges_pair_lasttrades + request: dict = { + 'pair': market['id'], + } + if since is not None: + request['date'] = self.parse_to_int(since) + if limit is not None: + request['limit'] = limit # max 100000 + response = None + if method == 'public_get_exchanges_pair_trades': + response = await self.publicGetExchangesPairTrades(self.extend(request, params)) + else: + response = await self.publicGetExchangesPairLasttrades(self.extend(request, params)) + # + # [ + # {"date":1651785980,"price":127975.68,"amount":0.3750321,"isBid":true,"tid":1261018}, + # {"date":1651785980,"price":127987.70,"amount":0.0389527820303982335802581029,"isBid":true,"tid":1261020}, + # {"date":1651786701,"price":128084.03,"amount":0.0015614749161156156626239821,"isBid":true,"tid":1261022}, + # ] + # + if isinstance(response, str): + raise ExchangeError(response) + return self.parse_trades(response, market, since, limit) + + async def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://bit2c.co.il/home/api#balance + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + await self.load_markets() + response = await self.privateGetAccountBalance(params) + # + # { + # "AVAILABLE_NIS": 0.0, + # "NIS": 0.0, + # "LOCKED_NIS": 0.0, + # "AVAILABLE_BTC": 0.0, + # "BTC": 0.0, + # "LOCKED_BTC": 0.0, + # ... + # "Fees": { + # "BtcNis": {"FeeMaker": 1.0, "FeeTaker": 1.0}, + # "EthNis": {"FeeMaker": 1.0, "FeeTaker": 1.0}, + # ... + # } + # } + # + fees = self.safe_value(response, 'Fees', {}) + keys = list(fees.keys()) + result: dict = {} + for i in range(0, len(keys)): + marketId = keys[i] + symbol = self.safe_symbol(marketId) + fee = self.safe_value(fees, marketId) + makerString = self.safe_string(fee, 'FeeMaker') + takerString = self.safe_string(fee, 'FeeTaker') + maker = self.parse_number(Precise.string_div(makerString, '100')) + taker = self.parse_number(Precise.string_div(takerString, '100')) + result[symbol] = { + 'info': fee, + 'symbol': symbol, + 'taker': taker, + 'maker': maker, + 'percentage': True, + 'tierBased': True, + } + return result + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://bit2c.co.il/home/api#addo + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + method = 'privatePostOrderAddOrder' + market = self.market(symbol) + request: dict = { + 'Amount': amount, + 'Pair': market['id'], + } + if type == 'market': + method += 'MarketPrice' + self.capitalize(side) + else: + request['Price'] = price + amountString = self.number_to_string(amount) + priceString = self.number_to_string(price) + request['Total'] = self.parse_to_numeric(Precise.string_mul(amountString, priceString)) + request['IsBid'] = (side == 'buy') + response = await getattr(self, method)(self.extend(request, params)) + return self.parse_order(response, market) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://bit2c.co.il/home/api#cancelo + + :param str id: order id + :param str symbol: Not used by bit2c cancelOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + request: dict = { + 'id': id, + } + response = await self.privatePostOrderCancelOrder(self.extend(request, params)) + return self.parse_order(response) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://bit2c.co.il/home/api#geto + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOpenOrders() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + response = await self.privateGetOrderMyOrders(self.extend(request, params)) + orders = self.safe_value(response, market['id'], {}) + asks = self.safe_value(orders, 'ask', []) + bids = self.safe_list(orders, 'bid', []) + return self.parse_orders(self.array_concat(asks, bids), market, since, limit) + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://bit2c.co.il/home/api#getoid + + :param str id: the order id + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'id': id, + } + response = await self.privateGetOrderGetById(self.extend(request, params)) + # + # { + # "pair": "BtcNis", + # "status": "Completed", + # "created": 1666689837, + # "type": 0, + # "order_type": 0, + # "amount": 0.00000000, + # "price": 50000.00000000, + # "stop": 0, + # "id": 10951473, + # "initialAmount": 2.00000000 + # } + # + return self.parse_order(response, market) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder + # { + # "OrderResponse": {"pair": "BtcNis", "HasError": False, "Error": "", "Message": ""}, + # "NewOrder": { + # "created": 1505531577, + # "type": 0, + # "order_type": 0, + # "status_type": 0, + # "amount": 0.01, + # "price": 10000, + # "stop": 0, + # "id": 9244416, + # "initialAmount": None, + # }, + # } + # fetchOrder, fetchOpenOrders + # { + # "pair": "BtcNis", + # "status": "Completed", + # "created": 1535555837, + # "type": 0, + # "order_type": 0, + # "amount": 0.00000000, + # "price": 120000.00000000, + # "stop": 0, + # "id": 10555173, + # "initialAmount": 2.00000000 + # } + # + orderUnified = None + isNewOrder = False + if 'NewOrder' in order: + orderUnified = order['NewOrder'] + isNewOrder = True + else: + orderUnified = order + id = self.safe_string(orderUnified, 'id') + symbol = self.safe_symbol(None, market) + timestamp = self.safe_integer_product(orderUnified, 'created', 1000) + # status field vary between responses + # bit2c status type: + # 0 = New + # 1 = Open + # 5 = Completed + status: str + if isNewOrder: + tempStatus = self.safe_integer(orderUnified, 'status_type') + if tempStatus == 0 or tempStatus == 1: + status = 'open' + elif tempStatus == 5: + status = 'closed' + else: + tempStatus = self.safe_string(orderUnified, 'status') + if tempStatus == 'New' or tempStatus == 'Open': + status = 'open' + elif tempStatus == 'Completed': + status = 'closed' + # bit2c order type: + # 0 = LMT, 1 = MKT + type = self.safe_string(orderUnified, 'order_type') + if type == '0': + type = 'limit' + elif type == '1': + type = 'market' + # bit2c side: + # 0 = buy, 1 = sell + side = self.safe_string(orderUnified, 'type') + if side == '0': + side = 'buy' + elif side == '1': + side = 'sell' + price = self.safe_string(orderUnified, 'price') + amount = None + remaining = None + if isNewOrder: + amount = self.safe_string(orderUnified, 'amount') # NOTE:'initialAmount' is currently not set on new order + remaining = self.safe_string(orderUnified, 'amount') + else: + amount = self.safe_string(orderUnified, 'initialAmount') + remaining = self.safe_string(orderUnified, 'amount') + return self.safe_order({ + 'id': id, + 'clientOrderId': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'status': status, + 'symbol': symbol, + 'type': type, + 'timeInForce': None, + 'postOnly': None, + 'side': side, + 'price': price, + 'triggerPrice': None, + 'amount': amount, + 'filled': None, + 'remaining': remaining, + 'cost': None, + 'trades': None, + 'fee': None, + 'info': order, + 'average': None, + }, market) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://bit2c.co.il/home/api#orderh + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = None + request: dict = {} + if limit is not None: + request['take'] = limit + request['take'] = limit + if since is not None: + request['toTime'] = self.yyyymmdd(self.milliseconds(), '.') + request['fromTime'] = self.yyyymmdd(since, '.') + if symbol is not None: + market = self.market(symbol) + request['pair'] = market['id'] + response = await self.privateGetOrderOrderHistory(self.extend(request, params)) + # + # [ + # { + # "ticks":1574767951, + # "created":"26/11/19 13:32", + # "action":1, + # "price":"1000", + # "pair":"EthNis", + # "reference":"EthNis|10867390|10867377", + # "fee":"0.5", + # "feeAmount":"0.08", + # "feeCoin":"₪", + # "firstAmount":"-0.015", + # "firstAmountBalance":"9", + # "secondAmount":"14.93", + # "secondAmountBalance":"130,233.28", + # "firstCoin":"ETH", + # "secondCoin":"₪" + # }, + # { + # "ticks":1574767951, + # "created":"26/11/19 13:32", + # "action":0, + # "price":"1000", + # "pair":"EthNis", + # "reference":"EthNis|10867390|10867377", + # "fee":"0.5", + # "feeAmount":"0.08", + # "feeCoin":"₪", + # "firstAmount":"0.015", + # "firstAmountBalance":"9.015", + # "secondAmount":"-15.08", + # "secondAmountBalance":"130,218.35", + # "firstCoin":"ETH", + # "secondCoin":"₪" + # } + # ] + # + return self.parse_trades(response, market, since, limit) + + def remove_comma_from_value(self, str): + newString = '' + strParts = str.split(',') + for i in range(0, len(strParts)): + newString += strParts[i] + return newString + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # public fetchTrades + # + # { + # "date":1651785980, + # "price":127975.68, + # "amount":0.3750321, + # "isBid":true, + # "tid":1261018 + # } + # + # private fetchMyTrades + # + # { + # "ticks":1574767951, + # "created":"26/11/19 13:32", + # "action":1, + # "price":"1,000", + # "pair":"EthNis", + # "reference":"EthNis|10867390|10867377", + # "fee":"0.5", + # "feeAmount":"0.08", + # "feeCoin":"₪", + # "firstAmount":"-0.015", + # "firstAmountBalance":"9", + # "secondAmount":"14.93", + # "secondAmountBalance":"130,233.28", + # "firstCoin":"ETH", + # "secondCoin":"₪" + # "isMaker": True, + # } + # + timestamp: Int + id: Str + price = None + amount = None + orderId = None + fee = None + side: str + makerOrTaker = None + reference = self.safe_string(trade, 'reference') + if reference is not None: + id = reference + timestamp = self.safe_timestamp(trade, 'ticks') + price = self.safe_string(trade, 'price') + price = self.remove_comma_from_value(price) + amount = self.safe_string(trade, 'firstAmount') + reference_parts = reference.split('|') # reference contains 'pair|orderId_by_taker|orderId_by_maker' + marketId = self.safe_string(trade, 'pair') + market = self.safe_market(marketId, market) + market = self.safe_market(reference_parts[0], market) + isMaker = self.safe_value(trade, 'isMaker') + makerOrTaker = 'maker' if isMaker else 'taker' + orderId = reference_parts[2] if isMaker else reference_parts[1] + action = self.safe_integer(trade, 'action') + if action == 0: + side = 'buy' + else: + side = 'sell' + feeCost = self.safe_string(trade, 'feeAmount') + if feeCost is not None: + fee = { + 'cost': feeCost, + 'currency': 'NIS', + } + else: + timestamp = self.safe_timestamp(trade, 'date') + id = self.safe_string(trade, 'tid') + price = self.safe_string(trade, 'price') + amount = self.safe_string(trade, 'amount') + side = self.safe_value(trade, 'isBid') + if side is not None: + if side: + side = 'buy' + else: + side = 'sell' + market = self.safe_market(None, market) + return self.safe_trade({ + 'info': trade, + 'id': id, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'order': orderId, + 'type': None, + 'side': side, + 'takerOrMaker': makerOrTaker, + 'price': price, + 'amount': amount, + 'cost': None, + 'fee': fee, + }, market) + + def is_fiat(self, code): + return code == 'NIS' + + async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://bit2c.co.il/home/api#addc + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + await self.load_markets() + currency = self.currency(code) + if self.is_fiat(code): + raise NotSupported(self.id + ' fetchDepositAddress() does not support fiat currencies') + request: dict = { + 'Coin': currency['id'], + } + response = await self.privatePostFundsAddCoinFundsRequest(self.extend(request, params)) + # + # { + # "address": "0xf14b94518d74aff2b1a6d3429471bcfcd3881d42", + # "hasTx": False + # } + # + return self.parse_deposit_address(response, currency) + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + # + # { + # "address": "0xf14b94518d74aff2b1a6d3429471bcfcd3881d42", + # "hasTx": False + # } + # + address = self.safe_string(depositAddress, 'address') + self.check_address(address) + code = self.safe_currency_code(None, currency) + return { + 'info': depositAddress, + 'currency': code, + 'network': None, + 'address': address, + 'tag': None, + } + + def nonce(self): + return self.milliseconds() + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = self.urls['api']['rest'] + '/' + self.implode_params(path, params) + if api == 'public': + url += '.json' + else: + self.check_required_credentials() + nonce = self.nonce() + query = self.extend({ + 'nonce': nonce, + }, params) + auth = self.urlencode(query) + if method == 'GET': + if query: + url += '?' + auth + else: + body = auth + signature = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha512, 'base64') + headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + 'key': self.apiKey, + 'sign': signature, + } + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None # fallback to default error handler + # + # {"error" : "please approve new terms of use on site."} + # {"error": "Please provide valid nonce in Request Nonce(1598218490) is not bigger than last nonce(1598218490)."} + # {"Error" : "No order found."} + # + error = self.safe_string(response, 'error') + if error is None: + error = self.safe_string(response, 'Error') + if error is not None: + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], error, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], error, feedback) + raise ExchangeError(feedback) # unknown message + return None diff --git a/ccxt/async_support/bitbank.py b/ccxt/async_support/bitbank.py new file mode 100644 index 0000000..6d4cc66 --- /dev/null +++ b/ccxt/async_support/bitbank.py @@ -0,0 +1,1130 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.bitbank import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currency, DepositAddress, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Ticker, Trade, TradingFees, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import InvalidNonce +from ccxt.base.decimal_to_precision import TICK_SIZE + + +class bitbank(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(bitbank, self).describe(), { + 'id': 'bitbank', + 'name': 'bitbank', + 'countries': ['JP'], + 'version': 'v1', + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelAllOrders': False, + 'cancelOrder': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createOrder': True, + 'createOrderWithTakeProfitAndStopLoss': False, + 'createOrderWithTakeProfitAndStopLossWs': False, + 'createReduceOnlyOrder': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': False, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkOHLCV': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': True, + 'fetchTransfer': False, + 'fetchTransfers': False, + 'fetchVolatilityHistory': False, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'transfer': False, + 'withdraw': True, + }, + 'timeframes': { + '1m': '1min', + '5m': '5min', + '15m': '15min', + '30m': '30min', + '1h': '1hour', + '4h': '4hour', + '8h': '8hour', + '12h': '12hour', + '1d': '1day', + '1w': '1week', + }, + 'hostname': 'bitbank.cc', + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/9d616de0-8a88-4468-8e38-d269acab0348', + 'api': { + 'public': 'https://public.{hostname}', + 'private': 'https://api.{hostname}', + 'markets': 'https://api.{hostname}', + }, + 'www': 'https://bitbank.cc/', + 'doc': 'https://docs.bitbank.cc/', + 'fees': 'https://bitbank.cc/docs/fees/', + }, + 'api': { + 'public': { + 'get': [ + '{pair}/ticker', + 'tickers', + 'tickers_jpy', + '{pair}/depth', + '{pair}/transactions', + '{pair}/transactions/{yyyymmdd}', + '{pair}/candlestick/{candletype}/{yyyymmdd}', + '{pair}/circuit_break_info', + ], + }, + 'private': { + 'get': [ + 'user/assets', + 'user/spot/order', + 'user/spot/active_orders', + 'user/margin/positions', + 'user/spot/trade_history', + 'user/deposit_history', + 'user/unconfirmed_deposits', + 'user/deposit_originators', + 'user/withdrawal_account', + 'user/withdrawal_history', + 'spot/status', + 'spot/pairs', + ], + 'post': [ + 'user/spot/order', + 'user/spot/cancel_order', + 'user/spot/cancel_orders', + 'user/spot/orders_info', + 'user/confirm_deposits', + 'user/confirm_deposits_all', + 'user/request_withdrawal', + ], + }, + 'markets': { + 'get': [ + 'spot/pairs', + ], + }, + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, # todo implement + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': False, + 'FOK': False, + 'PO': True, # todo: implement + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyRequiresPrice': False, + 'marketBuyByCost': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': None, + 'untilDays': None, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 1000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': None, + 'fetchOHLCV': { + 'limit': 1000, + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'exact': { + '20001': AuthenticationError, + '20002': AuthenticationError, + '20003': AuthenticationError, + '20005': AuthenticationError, + '20004': InvalidNonce, + '40020': InvalidOrder, + '40021': InvalidOrder, + '40025': ExchangeError, + '40013': OrderNotFound, + '40014': OrderNotFound, + '50008': PermissionDenied, + '50009': OrderNotFound, + '50010': OrderNotFound, + '60001': InsufficientFunds, + '60005': InvalidOrder, + }, + }, + }) + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for bitbank + + https://github.com/bitbankinc/bitbank-api-docs/blob/38d6d7c6f486c793872fd4b4087a0d090a04cd0a/rest-api.md#get-all-pairs-info + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = await self.marketsGetSpotPairs(params) + # + # { + # "success": 1, + # "data": { + # "pairs": [ + # { + # "name": "btc_jpy", + # "base_asset": "btc", + # "quote_asset": "jpy", + # "maker_fee_rate_base": "0", + # "taker_fee_rate_base": "0", + # "maker_fee_rate_quote": "-0.0002", + # "taker_fee_rate_quote": "0.0012", + # "unit_amount": "0.0001", + # "limit_max_amount": "1000", + # "market_max_amount": "10", + # "market_allowance_rate": "0.2", + # "price_digits": 0, + # "amount_digits": 4, + # "is_enabled": True, + # "stop_order": False, + # "stop_order_and_cancel": False + # } + # ] + # } + # } + # + data = self.safe_value(response, 'data') + pairs = self.safe_value(data, 'pairs', []) + return self.parse_markets(pairs) + + def parse_market(self, entry) -> Market: + id = self.safe_string(entry, 'name') + baseId = self.safe_string(entry, 'base_asset') + quoteId = self.safe_string(entry, 'quote_asset') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + return { + 'id': id, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': self.safe_value(entry, 'is_enabled'), + 'contract': False, + 'linear': None, + 'inverse': None, + 'taker': self.safe_number(entry, 'taker_fee_rate_quote'), + 'maker': self.safe_number(entry, 'maker_fee_rate_quote'), + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(self.parse_precision(self.safe_string(entry, 'amount_digits'))), + 'price': self.parse_number(self.parse_precision(self.safe_string(entry, 'price_digits'))), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(entry, 'unit_amount'), + 'max': self.safe_number(entry, 'limit_max_amount'), + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + 'info': entry, + } + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + symbol = self.safe_symbol(None, market) + timestamp = self.safe_integer(ticker, 'timestamp') + last = self.safe_string(ticker, 'last') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': self.safe_string(ticker, 'buy'), + 'bidVolume': None, + 'ask': self.safe_string(ticker, 'sell'), + 'askVolume': None, + 'vwap': None, + 'open': None, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': self.safe_string(ticker, 'vol'), + 'quoteVolume': None, + 'info': ticker, + }, market) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://github.com/bitbankinc/bitbank-api-docs/blob/38d6d7c6f486c793872fd4b4087a0d090a04cd0a/public-api.md#ticker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + response = await self.publicGetPairTicker(self.extend(request, params)) + data = self.safe_dict(response, 'data', {}) + return self.parse_ticker(data, market) + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://github.com/bitbankinc/bitbank-api-docs/blob/38d6d7c6f486c793872fd4b4087a0d090a04cd0a/public-api.md#depth + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + response = await self.publicGetPairDepth(self.extend(request, params)) + orderbook = self.safe_value(response, 'data', {}) + timestamp = self.safe_integer(orderbook, 'timestamp') + return self.parse_order_book(orderbook, market['symbol'], timestamp) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades + # + # { + # "transaction_id": "1143247037", + # "side": "buy", + # "price": "3836025", + # "amount": "0.0005", + # "executed_at": "1694249441593" + # } + # + timestamp = self.safe_integer(trade, 'executed_at') + market = self.safe_market(None, market) + priceString = self.safe_string(trade, 'price') + amountString = self.safe_string(trade, 'amount') + id = self.safe_string_2(trade, 'transaction_id', 'trade_id') + takerOrMaker = self.safe_string(trade, 'maker_taker') + fee = None + feeCostString = self.safe_string(trade, 'fee_amount_quote') + if feeCostString is not None: + fee = { + 'currency': market['quote'], + 'cost': feeCostString, + } + orderId = self.safe_string(trade, 'order_id') + type = self.safe_string(trade, 'type') + side = self.safe_string(trade, 'side') + return self.safe_trade({ + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'id': id, + 'order': orderId, + 'type': type, + 'side': side, + 'takerOrMaker': takerOrMaker, + 'price': priceString, + 'amount': amountString, + 'cost': None, + 'fee': fee, + 'info': trade, + }, market) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://github.com/bitbankinc/bitbank-api-docs/blob/38d6d7c6f486c793872fd4b4087a0d090a04cd0a/public-api.md#transactions + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + response = await self.publicGetPairTransactions(self.extend(request, params)) + data = self.safe_value(response, 'data', {}) + trades = self.safe_list(data, 'transactions', []) + return self.parse_trades(trades, market, since, limit) + + async def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://github.com/bitbankinc/bitbank-api-docs/blob/38d6d7c6f486c793872fd4b4087a0d090a04cd0a/rest-api.md#get-all-pairs-info + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + await self.load_markets() + response = await self.marketsGetSpotPairs(params) + # + # { + # "success": "1", + # "data": { + # "pairs": [ + # { + # "name": "btc_jpy", + # "base_asset": "btc", + # "quote_asset": "jpy", + # "maker_fee_rate_base": "0", + # "taker_fee_rate_base": "0", + # "maker_fee_rate_quote": "-0.0002", + # "taker_fee_rate_quote": "0.0012", + # "unit_amount": "0.0001", + # "limit_max_amount": "1000", + # "market_max_amount": "10", + # "market_allowance_rate": "0.2", + # "price_digits": "0", + # "amount_digits": "4", + # "is_enabled": True, + # "stop_order": False, + # "stop_order_and_cancel": False + # }, + # ... + # ] + # } + # } + # + data = self.safe_value(response, 'data', {}) + pairs = self.safe_value(data, 'pairs', []) + result: dict = {} + for i in range(0, len(pairs)): + pair = pairs[i] + marketId = self.safe_string(pair, 'name') + market = self.safe_market(marketId) + symbol = market['symbol'] + result[symbol] = { + 'info': pair, + 'symbol': symbol, + 'maker': self.safe_number(pair, 'maker_fee_rate_quote'), + 'taker': self.safe_number(pair, 'taker_fee_rate_quote'), + 'percentage': True, + 'tierBased': False, + } + return result + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # [ + # "0.02501786", + # "0.02501786", + # "0.02501786", + # "0.02501786", + # "0.0000", + # 1591488000000 + # ] + # + return [ + self.safe_integer(ohlcv, 5), + self.safe_number(ohlcv, 0), + self.safe_number(ohlcv, 1), + self.safe_number(ohlcv, 2), + self.safe_number(ohlcv, 3), + self.safe_number(ohlcv, 4), + ] + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://github.com/bitbankinc/bitbank-api-docs/blob/38d6d7c6f486c793872fd4b4087a0d090a04cd0a/public-api.md#candlestick + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + if since is None: + if limit is None: + limit = 1000 # it doesn't have any defaults, might return 200, might 2000(i.e. https://public.bitbank.cc/btc_jpy/candlestick/4hour/2020) + duration = self.parse_timeframe(timeframe) + since = self.milliseconds() - duration * 1000 * limit + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + 'candletype': self.safe_string(self.timeframes, timeframe, timeframe), + 'yyyymmdd': self.yyyymmdd(since, ''), + } + response = await self.publicGetPairCandlestickCandletypeYyyymmdd(self.extend(request, params)) + # + # { + # "success":1, + # "data":{ + # "candlestick":[ + # { + # "type":"5min", + # "ohlcv":[ + # ["0.02501786","0.02501786","0.02501786","0.02501786","0.0000",1591488000000], + # ["0.02501747","0.02501953","0.02501747","0.02501953","0.3017",1591488300000], + # ["0.02501762","0.02501762","0.02500392","0.02500392","0.1500",1591488600000], + # ] + # } + # ], + # "timestamp":1591508668190 + # } + # } + # + data = self.safe_value(response, 'data', {}) + candlestick = self.safe_value(data, 'candlestick', []) + first = self.safe_value(candlestick, 0, {}) + ohlcv = self.safe_list(first, 'ohlcv', []) + return self.parse_ohlcvs(ohlcv, market, timeframe, since, limit) + + def parse_balance(self, response) -> Balances: + result: dict = { + 'info': response, + 'timestamp': None, + 'datetime': None, + } + data = self.safe_value(response, 'data', {}) + assets = self.safe_value(data, 'assets', []) + for i in range(0, len(assets)): + balance = assets[i] + currencyId = self.safe_string(balance, 'asset') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(balance, 'free_amount') + account['used'] = self.safe_string(balance, 'locked_amount') + account['total'] = self.safe_string(balance, 'onhand_amount') + result[code] = account + return self.safe_balance(result) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://github.com/bitbankinc/bitbank-api-docs/blob/38d6d7c6f486c793872fd4b4087a0d090a04cd0a/rest-api.md#assets + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + response = await self.privateGetUserAssets(params) + # + # { + # "success": "1", + # "data": { + # "assets": [ + # { + # "asset": "jpy", + # "amount_precision": "4", + # "onhand_amount": "0.0000", + # "locked_amount": "0.0000", + # "free_amount": "0.0000", + # "stop_deposit": False, + # "stop_withdrawal": False, + # "withdrawal_fee": { + # "threshold": "30000.0000", + # "under": "550.0000", + # "over": "770.0000" + # } + # }, + # { + # "asset": "btc", + # "amount_precision": "8", + # "onhand_amount": "0.00000000", + # "locked_amount": "0.00000000", + # "free_amount": "0.00000000", + # "stop_deposit": False, + # "stop_withdrawal": False, + # "withdrawal_fee": "0.00060000" + # }, + # ] + # } + # } + # + return self.parse_balance(response) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'UNFILLED': 'open', + 'PARTIALLY_FILLED': 'open', + 'FULLY_FILLED': 'closed', + 'CANCELED_UNFILLED': 'canceled', + 'CANCELED_PARTIALLY_FILLED': 'canceled', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + id = self.safe_string(order, 'order_id') + marketId = self.safe_string(order, 'pair') + market = self.safe_market(marketId, market) + timestamp = self.safe_integer(order, 'ordered_at') + price = self.safe_string(order, 'price') + amount = self.safe_string(order, 'start_amount') + filled = self.safe_string(order, 'executed_amount') + remaining = self.safe_string(order, 'remaining_amount') + average = self.safe_string(order, 'average_price') + status = self.parse_order_status(self.safe_string(order, 'status')) + type = self.safe_string_lower(order, 'type') + side = self.safe_string_lower(order, 'side') + return self.safe_order({ + 'id': id, + 'clientOrderId': None, + 'datetime': self.iso8601(timestamp), + 'timestamp': timestamp, + 'lastTradeTimestamp': None, + 'status': status, + 'symbol': market['symbol'], + 'type': type, + 'timeInForce': None, + 'postOnly': None, + 'side': side, + 'price': price, + 'triggerPrice': None, + 'cost': None, + 'average': average, + 'amount': amount, + 'filled': filled, + 'remaining': remaining, + 'trades': None, + 'fee': None, + 'info': order, + }, market) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://github.com/bitbankinc/bitbank-api-docs/blob/38d6d7c6f486c793872fd4b4087a0d090a04cd0a/rest-api.md#create-new-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + 'amount': self.amount_to_precision(symbol, amount), + 'side': side, + 'type': type, + } + if type == 'limit': + request['price'] = self.price_to_precision(symbol, price) + response = await self.privatePostUserSpotOrder(self.extend(request, params)) + data = self.safe_dict(response, 'data') + return self.parse_order(data, market) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://github.com/bitbankinc/bitbank-api-docs/blob/38d6d7c6f486c793872fd4b4087a0d090a04cd0a/rest-api.md#cancel-order + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'order_id': id, + 'pair': market['id'], + } + response = await self.privatePostUserSpotCancelOrder(self.extend(request, params)) + # + # { + # "success": 1, + # "data": { + # "order_id": 0, + # "pair": "string", + # "side": "string", + # "type": "string", + # "start_amount": "string", + # "remaining_amount": "string", + # "executed_amount": "string", + # "price": "string", + # "post_only": False, + # "average_price": "string", + # "ordered_at": 0, + # "expire_at": 0, + # "canceled_at": 0, + # "triggered_at": 0, + # "trigger_price": "string", + # "status": "string" + # } + # } + # + data = self.safe_value(response, 'data') + return self.parse_order(data) + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://github.com/bitbankinc/bitbank-api-docs/blob/38d6d7c6f486c793872fd4b4087a0d090a04cd0a/rest-api.md#fetch-order-information + + :param str id: the order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'order_id': id, + 'pair': market['id'], + } + response = await self.privateGetUserSpotOrder(self.extend(request, params)) + # + # { + # "success": 1, + # "data": { + # "order_id": 0, + # "pair": "string", + # "side": "string", + # "type": "string", + # "start_amount": "string", + # "remaining_amount": "string", + # "executed_amount": "string", + # "price": "string", + # "post_only": False, + # "average_price": "string", + # "ordered_at": 0, + # "expire_at": 0, + # "triggered_at": 0, + # "triger_price": "string", + # "status": "string" + # } + # } + # + data = self.safe_dict(response, 'data') + return self.parse_order(data, market) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://github.com/bitbankinc/bitbank-api-docs/blob/38d6d7c6f486c793872fd4b4087a0d090a04cd0a/rest-api.md#fetch-active-orders + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + if limit is not None: + request['count'] = limit + if since is not None: + request['since'] = self.parse_to_int(since / 1000) + response = await self.privateGetUserSpotActiveOrders(self.extend(request, params)) + data = self.safe_value(response, 'data', {}) + orders = self.safe_list(data, 'orders', []) + return self.parse_orders(orders, market, since, limit) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://github.com/bitbankinc/bitbank-api-docs/blob/38d6d7c6f486c793872fd4b4087a0d090a04cd0a/rest-api.md#fetch-trade-history + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['pair'] = market['id'] + if limit is not None: + request['count'] = limit + if since is not None: + request['since'] = self.parse_to_int(since / 1000) + response = await self.privateGetUserSpotTradeHistory(self.extend(request, params)) + data = self.safe_value(response, 'data', {}) + trades = self.safe_list(data, 'trades', []) + return self.parse_trades(trades, market, since, limit) + + async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://github.com/bitbankinc/bitbank-api-docs/blob/38d6d7c6f486c793872fd4b4087a0d090a04cd0a/rest-api.md#get-withdrawal-accounts + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'asset': currency['id'], + } + response = await self.privateGetUserWithdrawalAccount(self.extend(request, params)) + data = self.safe_value(response, 'data', {}) + # Not sure about self if there could be more than one account... + accounts = self.safe_value(data, 'accounts', []) + firstAccount = self.safe_value(accounts, 0, {}) + address = self.safe_string(firstAccount, 'address') + return { + 'info': response, + 'currency': currency['code'], + 'network': None, + 'address': address, + 'tag': None, + } + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://github.com/bitbankinc/bitbank-api-docs/blob/38d6d7c6f486c793872fd4b4087a0d090a04cd0a/rest-api.md#new-withdrawal-request + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + if not ('uuid' in params): + raise ExchangeError(self.id + ' uuid is required for withdrawal') + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'asset': currency['id'], + 'amount': amount, + } + response = await self.privatePostUserRequestWithdrawal(self.extend(request, params)) + # + # { + # "success": 1, + # "data": { + # "uuid": "string", + # "asset": "btc", + # "amount": 0, + # "account_uuid": "string", + # "fee": 0, + # "status": "DONE", + # "label": "string", + # "txid": "string", + # "address": "string", + # "requested_at": 0 + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_transaction(data, currency) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # withdraw + # + # { + # "uuid": "string", + # "asset": "btc", + # "amount": 0, + # "account_uuid": "string", + # "fee": 0, + # "status": "DONE", + # "label": "string", + # "txid": "string", + # "address": "string", + # "requested_at": 0 + # } + # + txid = self.safe_string(transaction, 'txid') + currency = self.safe_currency(None, currency) + return { + 'id': txid, + 'txid': txid, + 'timestamp': None, + 'datetime': None, + 'network': None, + 'addressFrom': None, + 'address': None, + 'addressTo': None, + 'amount': None, + 'type': None, + 'currency': currency['code'], + 'status': None, + 'updated': None, + 'tagFrom': None, + 'tag': None, + 'tagTo': None, + 'comment': None, + 'internal': None, + 'fee': None, + 'info': transaction, + } + + def nonce(self): + return self.milliseconds() + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + query = self.omit(params, self.extract_params(path)) + url = self.implode_hostname(self.urls['api'][api]) + '/' + if (api == 'public') or (api == 'markets'): + url += self.implode_params(path, params) + if query: + url += '?' + self.urlencode(query) + else: + self.check_required_credentials() + nonce = str(self.nonce()) + auth = nonce + url += self.version + '/' + self.implode_params(path, params) + if method == 'POST': + body = self.json(query) + auth += body + else: + auth += '/' + self.version + '/' + path + if query: + query = self.urlencode(query) + url += '?' + query + auth += '?' + query + headers = { + 'Content-Type': 'application/json', + 'ACCESS-KEY': self.apiKey, + 'ACCESS-NONCE': nonce, + 'ACCESS-SIGNATURE': self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256), + } + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None + success = self.safe_integer(response, 'success') + data = self.safe_value(response, 'data') + if not success or not data: + errorMessages: dict = { + '10000': 'URL does not exist', + '10001': 'A system error occurred. Please contact support', + '10002': 'Invalid JSON format. Please check the contents of transmission', + '10003': 'A system error occurred. Please contact support', + '10005': 'A timeout error occurred. Please wait for a while and try again', + '20001': 'API authentication failed', + '20002': 'Illegal API key', + '20003': 'API key does not exist', + '20004': 'API Nonce does not exist', + '20005': 'API signature does not exist', + '20011': 'Two-step verification failed', + '20014': 'SMS authentication failed', + '30001': 'Please specify the order quantity', + '30006': 'Please specify the order ID', + '30007': 'Please specify the order ID array', + '30009': 'Please specify the stock', + '30012': 'Please specify the order price', + '30013': 'Trade Please specify either', + '30015': 'Please specify the order type', + '30016': 'Please specify asset name', + '30019': 'Please specify uuid', + '30039': 'Please specify the amount to be withdrawn', + '40001': 'The order quantity is invalid', + '40006': 'Count value is invalid', + '40007': 'End time is invalid', + '40008': 'end_id Value is invalid', + '40009': 'The from_id value is invalid', + '40013': 'The order ID is invalid', + '40014': 'The order ID array is invalid', + '40015': 'Too many specified orders', + '40017': 'Incorrect issue name', + '40020': 'The order price is invalid', + '40021': 'The trading classification is invalid', + '40022': 'Start date is invalid', + '40024': 'The order type is invalid', + '40025': 'Incorrect asset name', + '40028': 'uuid is invalid', + '40048': 'The amount of withdrawal is illegal', + '50003': 'Currently, self account is in a state where you can not perform the operation you specified. Please contact support', + '50004': 'Currently, self account is temporarily registered. Please try again after registering your account', + '50005': 'Currently, self account is locked. Please contact support', + '50006': 'Currently, self account is locked. Please contact support', + '50008': 'User identification has not been completed', + '50009': 'Your order does not exist', + '50010': 'Can not cancel specified order', + '50011': 'API not found', + '60001': 'The number of possessions is insufficient', + '60002': 'It exceeds the quantity upper limit of the tender buying order', + '60003': 'The specified quantity exceeds the limit', + '60004': 'The specified quantity is below the threshold', + '60005': 'The specified price is above the limit', + '60006': 'The specified price is below the lower limit', + '70001': 'A system error occurred. Please contact support', + '70002': 'A system error occurred. Please contact support', + '70003': 'A system error occurred. Please contact support', + '70004': 'We are unable to accept orders transaction is currently suspended', + '70005': 'Order can not be accepted because purchase order is currently suspended', + '70006': 'We can not accept orders because we are currently unsubscribed ', + '70009': 'We are currently temporarily restricting orders to be carried out. Please use the limit order.', + '70010': 'We are temporarily raising the minimum order quantity system load is now rising.', + } + code = self.safe_string(data, 'code') + message = self.safe_string(errorMessages, code, 'Error') + self.throw_exactly_matched_exception(self.exceptions['exact'], code, message) + raise ExchangeError(self.id + ' ' + self.json(response)) + return None diff --git a/ccxt/async_support/bitbns.py b/ccxt/async_support/bitbns.py new file mode 100644 index 0000000..fbf04d6 --- /dev/null +++ b/ccxt/async_support/bitbns.py @@ -0,0 +1,1229 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.bitbns import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currency, DepositAddress, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import OrderNotFound +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class bitbns(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(bitbns, self).describe(), { + 'id': 'bitbns', + 'name': 'Bitbns', + 'countries': ['IN'], # India + 'rateLimit': 1000, + 'certified': False, + 'version': 'v2', + # new metainfo interface + 'has': { + 'CORS': None, + 'spot': True, + 'margin': None, # has but unimplemented + 'swap': False, + 'future': False, + 'option': None, # coming soon + 'cancelAllOrders': False, + 'cancelOrder': True, + 'createOrder': True, + 'createStopOrder': True, + 'createTriggerOrder': True, + 'fechCurrencies': False, + 'fetchBalance': True, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchFundingHistory': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchIndexOHLCV': False, + 'fetchMarginMode': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMyTrades': True, + 'fetchOHLCV': False, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchPositionMode': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchStatus': True, + 'fetchTicker': 'emulated', + 'fetchTickers': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': False, + 'fetchTransfer': False, + 'fetchTransfers': False, + 'fetchWithdrawal': False, + 'fetchWithdrawals': True, + 'transfer': False, + 'withdraw': False, + }, + 'hostname': 'bitbns.com', + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/a5b9a562-cdd8-4bea-9fa7-fd24c1dad3d9', + 'api': { + 'www': 'https://{hostname}', + 'v1': 'https://api.{hostname}/api/trade/v1', + 'v2': 'https://api.{hostname}/api/trade/v2', + }, + 'www': 'https://bitbns.com', + 'referral': 'https://ref.bitbns.com/1090961', + 'doc': [ + 'https://bitbns.com/trade/#/api-trading/', + ], + 'fees': 'https://bitbns.com/fees', + }, + 'api': { + 'www': { + 'get': [ + 'order/fetchMarkets', + 'order/fetchTickers', + 'order/fetchOrderbook', + 'order/getTickerWithVolume', + 'exchangeData/ohlc', # ?coin=${coin_name}&page=${page} + 'exchangeData/orderBook', + 'exchangeData/tradedetails', + ], + }, + 'v1': { + 'get': [ + 'platform/status', + 'tickers', + 'orderbook/sell/{symbol}', + 'orderbook/buy/{symbol}', + ], + 'post': [ + 'currentCoinBalance/EVERYTHING', + 'getApiUsageStatus/USAGE', + 'getOrderSocketToken/USAGE', + 'currentCoinBalance/{symbol}', + 'orderStatus/{symbol}', + 'depositHistory/{symbol}', + 'withdrawHistory/{symbol}', + 'withdrawHistoryAll/{symbol}', + 'depositHistoryAll/{symbol}', + 'listOpenOrders/{symbol}', + 'listOpenStopOrders/{symbol}', + 'getCoinAddress/{symbol}', + 'placeSellOrder/{symbol}', + 'placeBuyOrder/{symbol}', + 'buyStopLoss/{symbol}', + 'sellStopLoss/{symbol}', + 'cancelOrder/{symbol}', + 'cancelStopLossOrder/{symbol}', + 'listExecutedOrders/{symbol}', + 'placeMarketOrder/{symbol}', + 'placeMarketOrderQnty/{symbol}', + ], + }, + 'v2': { + 'post': [ + 'orders', + 'cancel', + 'getordersnew', + 'marginOrders', + ], + }, + }, + 'fees': { + 'trading': { + 'feeSide': 'quote', + 'tierBased': False, + 'percentage': True, + 'taker': self.parse_number('0.0025'), + 'maker': self.parse_number('0.0025'), + }, + }, + 'precisionMode': TICK_SIZE, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, # todo with triggerPrice + 'takeProfitPrice': False, # todo with triggerPrice + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': False, + 'FOK': False, + 'PO': False, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, # todo recheck + 'leverage': False, + 'marketBuyRequiresPrice': False, + 'marketBuyByCost': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': None, + 'daysBack': None, + 'untilDays': None, + 'symbolRequired': True, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': True, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOrders': None, + 'fetchClosedOrders': None, + # todo: implement fetchOHLCV + 'fetchOHLCV': { + 'limit': 100, + }, + }, + # todo: implement swap methods + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'exceptions': { + 'exact': { + '400': BadRequest, # {"msg":"Invalid Request","status":-1,"code":400} + '409': BadSymbol, # {"data":"","status":0,"error":"coin name not supplied or not yet supported","code":409} + '416': InsufficientFunds, # {"data":"Oops ! Not sufficient currency to sell","status":0,"error":null,"code":416} + '417': OrderNotFound, # {"data":[],"status":0,"error":"Nothing to show","code":417} + }, + 'broad': {}, + }, + }) + + async def fetch_status(self, params={}): + """ + the latest known information on the availability of the exchange API + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `status structure ` + """ + response = await self.v1GetPlatformStatus(params) + # + # { + # "data":{ + # "BTC":{"status":1}, + # "ETH":{"status":1}, + # "XRP":{"status":1}, + # }, + # "status":1, + # "error":null, + # "code":200 + # } + # + statusRaw = self.safe_string(response, 'status') + return { + 'status': self.safe_string({'1': 'ok'}, statusRaw, statusRaw), + 'updated': None, + 'eta': None, + 'url': None, + 'info': response, + } + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for bitbns + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = await self.wwwGetOrderFetchMarkets(params) + # + # [ + # { + # "id":"BTC", + # "symbol":"BTC/INR", + # "base":"BTC", + # "quote":"INR", + # "baseId":"BTC", + # "quoteId":"", + # "active":true, + # "limits":{ + # "amount":{"min":"0.00017376","max":20}, + # "price":{"min":2762353.2359999996,"max":6445490.883999999}, + # "cost":{"min":800,"max":128909817.67999998} + # }, + # "precision":{ + # "amount":8, + # "price":2 + # }, + # "info":{} + # }, + # ] + # + result = [] + for i in range(0, len(response)): + market = response[i] + id = self.safe_string(market, 'id') + baseId = self.safe_string(market, 'base') + quoteId = self.safe_string(market, 'quote') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + marketPrecision = self.safe_dict(market, 'precision', {}) + marketLimits = self.safe_dict(market, 'limits', {}) + amountLimits = self.safe_dict(marketLimits, 'amount', {}) + priceLimits = self.safe_dict(marketLimits, 'price', {}) + costLimits = self.safe_dict(marketLimits, 'cost', {}) + usdt = (quoteId == 'USDT') + # INR markets don't need a _INR prefix + uppercaseId = (baseId + '_' + quoteId) if usdt else baseId + result.append({ + 'id': id, + 'uppercaseId': uppercaseId, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': self.safe_bool(market, 'active'), + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(self.parse_precision(self.safe_string(marketPrecision, 'amount'))), + 'price': self.parse_number(self.parse_precision(self.safe_string(marketPrecision, 'price'))), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(amountLimits, 'min'), + 'max': self.safe_number(amountLimits, 'max'), + }, + 'price': { + 'min': self.safe_number(priceLimits, 'min'), + 'max': self.safe_number(priceLimits, 'max'), + }, + 'cost': { + 'min': self.safe_number(costLimits, 'min'), + 'max': self.safe_number(costLimits, 'max'), + }, + }, + 'created': None, + 'info': market, + }) + return result + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['limit'] = limit # default 100, max 5000, see https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md#order-book + response = await self.wwwGetOrderFetchOrderbook(self.extend(request, params)) + # + # { + # "bids":[ + # [49352.04,0.843948], + # [49352.03,0.742048], + # [49349.78,0.686239], + # ], + # "asks":[ + # [49443.59,0.065137], + # [49444.63,0.098211], + # [49449.01,0.066309], + # ], + # "timestamp":1619172786577, + # "datetime":"2021-04-23T10:13:06.577Z", + # "nonce":"" + # } + # + timestamp = self.safe_integer(response, 'timestamp') + return self.parse_order_book(response, market['symbol'], timestamp) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "symbol":"BTC/INR", + # "info":{ + # "highest_buy_bid":4368494.31, + # "lowest_sell_bid":4374835.09, + # "last_traded_price":4374835.09, + # "yes_price":4531016.27, + # "volume":{"max":"4569119.23","min":"4254552.13","volume":62.17722344} + # }, + # "timestamp":1619100020845, + # "datetime":1619100020845, + # "high":"4569119.23", + # "low":"4254552.13", + # "bid":4368494.31, + # "bidVolume":"", + # "ask":4374835.09, + # "askVolume":"", + # "vwap":"", + # "open":4531016.27, + # "close":4374835.09, + # "last":4374835.09, + # "baseVolume":62.17722344, + # "quoteVolume":"", + # "previousClose":"", + # "change":-156181.1799999997, + # "percentage":-3.446934874943623, + # "average":4452925.68 + # } + # + timestamp = self.safe_integer(ticker, 'timestamp') + marketId = self.safe_string(ticker, 'symbol') + symbol = self.safe_symbol(marketId, market) + last = self.safe_string(ticker, 'last') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': self.safe_string(ticker, 'bid'), + 'bidVolume': self.safe_string(ticker, 'bidVolume'), + 'ask': self.safe_string(ticker, 'ask'), + 'askVolume': self.safe_string(ticker, 'askVolume'), + 'vwap': self.safe_string(ticker, 'vwap'), + 'open': self.safe_string(ticker, 'open'), + 'close': last, + 'last': last, + 'previousClose': self.safe_string(ticker, 'previousClose'), # previous day close + 'change': self.safe_string(ticker, 'change'), + 'percentage': self.safe_string(ticker, 'percentage'), + 'average': self.safe_string(ticker, 'average'), + 'baseVolume': self.safe_string(ticker, 'baseVolume'), + 'quoteVolume': self.safe_string(ticker, 'quoteVolume'), + 'info': ticker, + }, market) + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + response = await self.wwwGetOrderFetchTickers(params) + # + # { + # "BTC/INR":{ + # "symbol":"BTC/INR", + # "info":{ + # "highest_buy_bid":4368494.31, + # "lowest_sell_bid":4374835.09, + # "last_traded_price":4374835.09, + # "yes_price":4531016.27, + # "volume":{"max":"4569119.23","min":"4254552.13","volume":62.17722344} + # }, + # "timestamp":1619100020845, + # "datetime":1619100020845, + # "high":"4569119.23", + # "low":"4254552.13", + # "bid":4368494.31, + # "bidVolume":"", + # "ask":4374835.09, + # "askVolume":"", + # "vwap":"", + # "open":4531016.27, + # "close":4374835.09, + # "last":4374835.09, + # "baseVolume":62.17722344, + # "quoteVolume":"", + # "previousClose":"", + # "change":-156181.1799999997, + # "percentage":-3.446934874943623, + # "average":4452925.68 + # } + # } + # + return self.parse_tickers(response, symbols) + + def parse_balance(self, response) -> Balances: + timestamp = None + result: dict = { + 'info': response, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + data = self.safe_dict(response, 'data', {}) + keys = list(data.keys()) + for i in range(0, len(keys)): + key = keys[i] + parts = key.split('availableorder') + numParts = len(parts) + if numParts > 1: + currencyId = self.safe_string(parts, 1) + # note that "Money" stands for INR - the only fiat in bitbns + account = self.account() + account['free'] = self.safe_string(data, key) + account['used'] = self.safe_string(data, 'inorder' + currencyId) + if currencyId == 'Money': + currencyId = 'INR' + code = self.safe_currency_code(currencyId) + result[code] = account + return self.safe_balance(result) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + response = await self.v1PostCurrentCoinBalanceEVERYTHING(params) + # + # { + # "data":{ + # "availableorderMoney":12.34, # INR + # "availableorderBTC":0, + # "availableorderXRP":0, + # "inorderMoney":0, # INR + # "inorderBTC":0, + # "inorderXRP":0, + # "inorderNEO":0, + # }, + # "status":1, + # "error":null, + # "code":200 + # } + # + # note that "Money" stands for INR - the only fiat in bitbns + return self.parse_balance(response) + + def parse_status(self, status): + statuses: dict = { + '-1': 'cancelled', + '0': 'open', + '1': 'open', + '2': 'done', + # 'PARTIALLY_FILLED': 'open', + # 'FILLED': 'closed', + # 'CANCELED': 'canceled', + # 'PENDING_CANCEL': 'canceling', # currently unused + # 'REJECTED': 'rejected', + # 'EXPIRED': 'expired', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder + # + # { + # "data": "Successfully placed bid to purchase currency", + # "status": 1, + # "error": null, + # "id": 5424475, + # "code": 200 + # } + # + # fetchOpenOrders, fetchOrder + # + # { + # "entry_id": 5424475, + # "btc": 0.01, + # "rate": 2000, + # "time": "2021-04-25T17:05:42.000Z", + # "type": 0, + # "status": 0 + # "t_rate": 0.45, # only stop orders + # "trail": 0 # only stop orders + # } + # + # cancelOrder + # + # { + # "data": "Successfully cancelled the order", + # "status": 1, + # "error": null, + # "code": 200 + # } + # + id = self.safe_string_2(order, 'id', 'entry_id') + datetime = self.safe_string(order, 'time') + triggerPrice = self.safe_string(order, 't_rate') + side = self.safe_string(order, 'type') + if side == '0': + side = 'buy' + elif side == '1': + side = 'sell' + data = self.safe_string(order, 'data') + status = self.safe_string(order, 'status') + if data == 'Successfully cancelled the order': + status = 'cancelled' + else: + status = self.parse_status(status) + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': None, + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + 'lastTradeTimestamp': None, + 'symbol': self.safe_string(market, 'symbol'), + 'timeInForce': None, + 'postOnly': None, + 'side': side, + 'price': self.safe_string(order, 'rate'), + 'triggerPrice': triggerPrice, + 'amount': self.safe_string(order, 'btc'), + 'cost': None, + 'average': None, + 'filled': None, + 'remaining': None, + 'status': status, + 'fee': { + 'cost': None, + 'currency': None, + 'rate': None, + }, + 'trades': None, + }, market) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://docs.bitbns.com/bitbns/rest-endpoints/order-apis/version-2/place-orders + https://docs.bitbns.com/bitbns/rest-endpoints/order-apis/version-1/market-orders-quantity # market orders + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: the price at which a trigger order is triggered at + + EXCHANGE SPECIFIC PARAMETERS + :param float [params.target_rate]: *requires params.trail_rate when set, type must be 'limit'* a bracket order is placed when set + :param float [params.trail_rate]: *requires params.target_rate when set, type must be 'limit'* a bracket order is placed when set + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + triggerPrice = self.safe_string_n(params, ['triggerPrice', 'stopPrice', 't_rate']) + targetRate = self.safe_string(params, 'target_rate') + trailRate = self.safe_string(params, 'trail_rate') + params = self.omit(params, ['triggerPrice', 'stopPrice', 'trail_rate', 'target_rate', 't_rate']) + request: dict = { + 'side': side.upper(), + 'symbol': market['uppercaseId'], + 'quantity': self.amount_to_precision(symbol, amount), + # 'target_rate': self.price_to_precision(symbol, targetRate), + # 't_rate': self.price_to_precision(symbol, stopPrice), + # 'trail_rate': self.price_to_precision(symbol, trailRate), + } + method = 'v2PostOrders' + if type == 'limit': + request['rate'] = self.price_to_precision(symbol, price) + else: + method = 'v1PostPlaceMarketOrderQntySymbol' + request['market'] = market['quoteId'] + if triggerPrice is not None: + request['t_rate'] = self.price_to_precision(symbol, triggerPrice) + if targetRate is not None: + request['target_rate'] = self.price_to_precision(symbol, targetRate) + if trailRate is not None: + request['trail_rate'] = self.price_to_precision(symbol, trailRate) + response = await getattr(self, method)(self.extend(request, params)) + # + # { + # "data":"Successfully placed bid to purchase currency", + # "status":1, + # "error":null, + # "id":5424475, + # "code":200 + # } + # + return self.parse_order(response, market) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://docs.bitbns.com/bitbns/rest-endpoints/order-apis/version-2/cancel-orders + https://docs.bitbns.com/bitbns/rest-endpoints/order-apis/version-1/cancel-stop-loss-orders + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: True if cancelling a trigger order + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + isTrigger = self.safe_bool_2(params, 'trigger', 'stop') + params = self.omit(params, ['trigger', 'stop']) + request: dict = { + 'entry_id': id, + 'symbol': market['uppercaseId'], + } + response = None + tail = 'StopLossOrder' if isTrigger else 'Order' + quoteSide = 'usdtcancel' if (market['quoteId'] == 'USDT') else 'cancel' + quoteSide += tail + request['side'] = quoteSide + response = await self.v2PostCancel(self.extend(request, params)) + return self.parse_order(response, market) + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://docs.bitbns.com/bitbns/rest-endpoints/order-apis/version-1/order-status + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrder() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'entry_id': id, + } + trigger = self.safe_bool_2(params, 'trigger', 'stop') + if trigger: + raise BadRequest(self.id + ' fetchOrder cannot fetch stop orders') + response = await self.v1PostOrderStatusSymbol(self.extend(request, params)) + # + # { + # "data":[ + # { + # "entry_id":5424475, + # "btc":0.01, + # "rate":2000, + # "time":"2021-04-25T17:05:42.000Z", + # "type":0, + # "status":0, + # "total":0.01, + # "avg_cost":null, + # "side":"BUY", + # "amount":0.01, + # "remaining":0.01, + # "filled":0, + # "cost":null, + # "fee":0.05 + # } + # ], + # "status":1, + # "error":null, + # "code":200 + # } + # + data = self.safe_list(response, 'data', []) + first = self.safe_dict(data, 0) + return self.parse_order(first, market) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://docs.bitbns.com/bitbns/rest-endpoints/order-apis/version-2/order-status-limit + https://docs.bitbns.com/bitbns/rest-endpoints/order-apis/version-2/order-status-limit/order-status-stop-limit + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: True if fetching trigger orders + :returns Order[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOpenOrders() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + isTrigger = self.safe_bool_2(params, 'trigger', 'stop') + params = self.omit(params, ['trigger', 'stop']) + quoteSide = 'usdtListOpen' if (market['quoteId'] == 'USDT') else 'listOpen' + request: dict = { + 'symbol': market['uppercaseId'], + 'page': 0, + 'side': (quoteSide + 'StopOrders') if isTrigger else (quoteSide + 'Orders'), + } + response = await self.v2PostGetordersnew(self.extend(request, params)) + # + # { + # "data":[ + # { + # "entry_id":5424475, + # "btc":0.01, + # "rate":2000, + # "time":"2021-04-25T17:05:42.000Z", + # "type":0, + # "status":0 + # "t_rate":0.45, # only stop orders + # "type":1, # only stop orders + # "trail":0 # only stop orders + # } + # ], + # "status":1, + # "error":null, + # "code":200 + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_orders(data, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchMyTrades + # + # { + # "type": "BTC Sell order executed", + # "typeI": 6, + # "crypto": 5000, + # "amount": 35.4, + # "rate": 709800, + # "date": "2020-05-22T15:05:34.000Z", + # "unit": "INR", + # "factor": 100000000, + # "fee": 0.09, + # "delh_btc": -5000, + # "delh_inr": 0, + # "del_btc": 0, + # "del_inr": 35.4, + # "id": "2938823" + # } + # + # fetchTrades + # + # { + # "tradeId":"1909151", + # "price":"61904.6300", + # "quote_volume":1618.05, + # "base_volume":0.02607254, + # "timestamp":1634548602000, + # "type":"buy" + # } + # + market = self.safe_market(None, market) + orderId = self.safe_string_2(trade, 'id', 'tradeId') + timestamp = self.parse8601(self.safe_string(trade, 'date')) + timestamp = self.safe_integer(trade, 'timestamp', timestamp) + priceString = self.safe_string_2(trade, 'rate', 'price') + amountString = self.safe_string(trade, 'amount') + side = self.safe_string_lower(trade, 'type') + if side is not None: + if side.find('buy') >= 0: + side = 'buy' + elif side.find('sell') >= 0: + side = 'sell' + factor = self.safe_string(trade, 'factor') + costString = None + if factor is not None: + amountString = Precise.string_div(amountString, factor) + else: + amountString = self.safe_string(trade, 'base_volume') + costString = self.safe_string(trade, 'quote_volume') + symbol = market['symbol'] + fee = None + feeCostString = self.safe_string(trade, 'fee') + if feeCostString is not None: + feeCurrencyCode = market['quote'] + fee = { + 'cost': feeCostString, + 'currency': feeCurrencyCode, + } + return self.safe_trade({ + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'id': orderId, + 'order': orderId, + 'type': None, + 'side': side, + 'takerOrMaker': None, + 'price': priceString, + 'amount': amountString, + 'cost': costString, + 'fee': fee, + }, market) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMyTrades() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'page': 0, + } + if since is not None: + request['since'] = self.iso8601(since) + response = await self.v1PostListExecutedOrdersSymbol(self.extend(request, params)) + # + # { + # "data": [ + # { + # "type": "BTC Sell order executed", + # "typeI": 6, + # "crypto": 5000, + # "amount": 35.4, + # "rate": 709800, + # "date": "2020-05-22T15:05:34.000Z", + # "unit": "INR", + # "factor": 100000000, + # "fee": 0.09, + # "delh_btc": -5000, + # "delh_inr": 0, + # "del_btc": 0, + # "del_inr": 35.4, + # "id": "2938823" + # }, + # { + # "type": "BTC Sell order executed", + # "typeI": 6, + # "crypto": 195000, + # "amount": 1380.58, + # "rate": 709765.5, + # "date": "2020-05-22T15:05:34.000Z", + # "unit": "INR", + # "factor": 100000000, + # "fee": 3.47, + # "delh_btc": -195000, + # "delh_inr": 0, + # "del_btc": 0, + # "del_inr": 1380.58, + # "id": "2938823" + # } + # ], + # "status": 1, + # "error": null, + # "code": 200 + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_trades(data, market, since, limit) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchTrades() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'coin': market['baseId'], + 'market': market['quoteId'], + } + response = await self.wwwGetExchangeDataTradedetails(self.extend(request, params)) + # + # [ + # {"tradeId":"1909151","price":"61904.6300","quote_volume":1618.05,"base_volume":0.02607254,"timestamp":1634548602000,"type":"buy"}, + # {"tradeId":"1909153","price":"61893.9000","quote_volume":16384.42,"base_volume":0.26405767,"timestamp":1634548999000,"type":"sell"}, + # {"tradeId":"1909155","price":"61853.1100","quote_volume":2304.37,"base_volume":0.03716263,"timestamp":1634549670000,"type":"sell"} + # } + # + return self.parse_trades(response, market, since, limit) + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + if code is None: + raise ArgumentsRequired(self.id + ' fetchDeposits() requires a currency code argument') + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'symbol': currency['id'], + 'page': 0, + } + response = await self.v1PostDepositHistorySymbol(self.extend(request, params)) + # + # { + # "data":[ + # { + # "type":"USDT deposited", + # "typeI":1, + # "amount":100, + # "date":"2021-04-24T14:56:04.000Z", + # "unit":"USDT", + # "factor":100, + # "fee":0, + # "delh_btc":0, + # "delh_inr":0, + # "rate":0, + # "del_btc":10000, + # "del_inr":0 + # } + # ], + # "status":1, + # "error":null, + # "code":200 + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_transactions(data, currency, since, limit) + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + if code is None: + raise ArgumentsRequired(self.id + ' fetchWithdrawals() requires a currency code argument') + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'symbol': currency['id'], + 'page': 0, + } + response = await self.v1PostWithdrawHistorySymbol(self.extend(request, params)) + # + # ... + # + data = self.safe_list(response, 'data', []) + return self.parse_transactions(data, currency, since, limit) + + def parse_transaction_status_by_type(self, status, type=None): + statusesByType: dict = { + 'deposit': { + '0': 'pending', + '1': 'ok', + }, + 'withdrawal': { + '0': 'pending', # Email Sent + '1': 'canceled', # Cancelled(different from 1 = ok in deposits) + '2': 'pending', # Awaiting Approval + '3': 'failed', # Rejected + '4': 'pending', # Processing + '5': 'failed', # Failure + '6': 'ok', # Completed + }, + } + statuses = self.safe_dict(statusesByType, type, {}) + return self.safe_string(statuses, status, status) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fetchDeposits + # + # { + # "type":"USDT deposited", + # "typeI":1, + # "amount":100, + # "date":"2021-04-24T14:56:04.000Z", + # "unit":"USDT", + # "factor":100, + # "fee":0, + # "delh_btc":0, + # "delh_inr":0, + # "rate":0, + # "del_btc":10000, + # "del_inr":0 + # } + # + # fetchWithdrawals + # + # ... + # + currencyId = self.safe_string(transaction, 'unit') + code = self.safe_currency_code(currencyId, currency) + timestamp = self.parse8601(self.safe_string_2(transaction, 'date', 'timestamp')) + type = self.safe_string(transaction, 'type') + expTime = self.safe_string(transaction, 'expTime', '') + status = None + if type is not None: + if type.find('deposit') >= 0: + type = 'deposit' + status = 'ok' + elif type.find('withdraw') >= 0 or expTime.find('withdraw') >= 0: + type = 'withdrawal' + # status = self.parse_transaction_status_by_type(self.safe_string(transaction, 'status'), type) + amount = self.safe_number(transaction, 'amount') + feeCost = self.safe_number(transaction, 'fee') + fee = None + if feeCost is not None: + fee = {'currency': code, 'cost': feeCost} + return { + 'info': transaction, + 'id': None, + 'txid': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': None, + 'address': None, + 'addressTo': None, + 'addressFrom': None, + 'tag': None, + 'tagTo': None, + 'tagFrom': None, + 'type': type, + 'amount': amount, + 'currency': code, + 'status': status, + 'updated': None, + 'comment': None, + 'internal': None, + 'fee': fee, + } + + async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'symbol': currency['id'], + } + response = await self.v1PostGetCoinAddressSymbol(self.extend(request, params)) + # + # { + # "data":{ + # "token":"0x680dee9edfff0c397736e10b017cf6a0aee4ba31", + # "expiry":"2022-04-24 22:30:11" + # }, + # "status":1, + # "error":null + # } + # + data = self.safe_dict(response, 'data', {}) + address = self.safe_string(data, 'token') + tag = self.safe_string(data, 'tag') + self.check_address(address) + return { + 'info': response, + 'currency': code, + 'network': None, + 'address': address, + 'tag': tag, + } + + def nonce(self): + return self.milliseconds() + + def sign(self, path, api='www', method='GET', params={}, headers=None, body=None): + urls = self.urls + if not (api in urls['api']): + raise ExchangeError(self.id + ' does not have a testnet/sandbox URL for ' + api + ' endpoints') + if api != 'www': + self.check_required_credentials() + headers = { + 'X-BITBNS-APIKEY': self.apiKey, + } + baseUrl = self.implode_hostname(self.urls['api'][api]) + url = baseUrl + '/' + self.implode_params(path, params) + query = self.omit(params, self.extract_params(path)) + nonce = str(self.nonce()) + if method == 'GET': + if query: + url += '?' + self.urlencode(query) + elif method == 'POST': + if query: + body = self.json(query) + else: + body = '{}' + auth: dict = { + 'timeStamp_nonce': nonce, + 'body': body, + } + payload = self.string_to_base64(self.json(auth)) + signature = self.hmac(self.encode(payload), self.encode(self.secret), hashlib.sha512) + headers['X-BITBNS-PAYLOAD'] = payload + headers['X-BITBNS-SIGNATURE'] = signature + headers['Content-Type'] = 'application/x-www-form-urlencoded' + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None # fallback to default error handler + # + # {"msg":"Invalid Request","status":-1,"code":400} + # {"data":[],"status":0,"error":"Nothing to show","code":417} + # + code = self.safe_string(response, 'code') + message = self.safe_string(response, 'msg') + error = (code is not None) and (code != '200') and (code != '204') + if error or (message is not None): + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], code, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + raise ExchangeError(feedback) # unknown message + return None diff --git a/ccxt/async_support/bitfinex.py b/ccxt/async_support/bitfinex.py new file mode 100644 index 0000000..41e2d26 --- /dev/null +++ b/ccxt/async_support/bitfinex.py @@ -0,0 +1,3797 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.bitfinex import ImplicitAPI +import asyncio +import hashlib +from ccxt.base.types import Any, Balances, Currencies, Currency, DepositAddress, Int, LedgerEntry, MarginModification, Market, Num, Order, OrderBook, OrderRequest, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, FundingRates, Trade, TradingFees, Transaction, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import NotSupported +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.errors import OnMaintenance +from ccxt.base.errors import InvalidNonce +from ccxt.base.decimal_to_precision import ROUND +from ccxt.base.decimal_to_precision import TRUNCATE +from ccxt.base.decimal_to_precision import DECIMAL_PLACES +from ccxt.base.decimal_to_precision import SIGNIFICANT_DIGITS +from ccxt.base.precise import Precise + + +class bitfinex(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(bitfinex, self).describe(), { + 'id': 'bitfinex', + 'name': 'Bitfinex', + 'countries': ['VG'], + 'version': 'v2', + 'certified': False, + 'pro': True, + # new metainfo interface + 'has': { + 'CORS': None, + 'spot': True, + 'margin': True, + 'swap': True, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'cancelOrders': True, + 'createDepositAddress': True, + 'createLimitOrder': True, + 'createMarketOrder': True, + 'createOrder': True, + 'createPostOnlyOrder': True, + 'createReduceOnlyOrder': True, + 'createStopLimitOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'createTrailingAmountOrder': True, + 'createTrailingPercentOrder': False, + 'createTriggerOrder': True, + 'editOrder': True, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchClosedOrder': True, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDepositsWithdrawals': True, + 'fetchFundingHistory': False, + 'fetchFundingRate': 'emulated', # emulated in exchange + 'fetchFundingRateHistory': True, + 'fetchFundingRates': True, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchLedger': True, + 'fetchLeverage': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': True, + 'fetchMarginMode': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkOHLCV': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': True, + 'fetchOpenInterestHistory': True, + 'fetchOpenInterests': True, + 'fetchOpenOrder': True, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrderBooks': False, + 'fetchOrderTrades': True, + 'fetchPosition': False, + 'fetchPositionMode': False, + 'fetchPositions': True, + 'fetchPremiumIndexOHLCV': False, + 'fetchStatus': True, + 'fetchTickers': True, + 'fetchTime': False, + 'fetchTradingFee': False, + 'fetchTradingFees': True, + 'fetchTransactionFees': None, + 'fetchTransactions': 'emulated', + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'setLeverage': False, + 'setMargin': True, + 'setMarginMode': False, + 'setPositionMode': False, + 'signIn': False, + 'transfer': True, + 'withdraw': True, + }, + 'timeframes': { + '1m': '1m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1h', + '3h': '3h', + '4h': '4h', + '6h': '6h', + '12h': '12h', + '1d': '1D', + '1w': '7D', + '2w': '14D', + '1M': '1M', + }, + # cheapest endpoint is 240 requests per minute => ~ 4 requests per second =>( 1000ms / 4 ) = 250ms between requests on average + 'rateLimit': 250, + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/4a8e947f-ab46-481a-a8ae-8b20e9b03178', + 'api': { + 'v1': 'https://api.bitfinex.com', + 'public': 'https://api-pub.bitfinex.com', + 'private': 'https://api.bitfinex.com', + }, + 'www': 'https://www.bitfinex.com', + 'doc': [ + 'https://docs.bitfinex.com/v2/docs/', + 'https://github.com/bitfinexcom/bitfinex-api-node', + ], + 'fees': 'https://www.bitfinex.com/fees', + }, + 'api': { + 'public': { + 'get': { + 'conf/{config}': 2.7, # 90 requests a minute, 90/60 = 1.5, 1000 / (250 * 2.66) = 1.503, use 2.7 instead of 2.66 to ensure rateLimitExceeded is not triggered + 'conf/pub:{action}:{object}': 2.7, + 'conf/pub:{action}:{object}:{detail}': 2.7, + 'conf/pub:map:{object}': 2.7, + 'conf/pub:map:{object}:{detail}': 2.7, + 'conf/pub:map:currency:{detail}': 2.7, + 'conf/pub:map:currency:sym': 2.7, # maps symbols to their API symbols, BAB > BCH + 'conf/pub:map:currency:label': 2.7, # verbose friendly names, BNT > Bancor + 'conf/pub:map:currency:unit': 2.7, # maps symbols to unit of measure where applicable + 'conf/pub:map:currency:undl': 2.7, # maps derivatives symbols to their underlying currency + 'conf/pub:map:currency:pool': 2.7, # maps symbols to underlying network/protocol they operate on + 'conf/pub:map:currency:explorer': 2.7, # maps symbols to their recognised block explorer URLs + 'conf/pub:map:currency:tx:fee': 2.7, # maps currencies to their withdrawal fees https://github.com/ccxt/ccxt/issues/7745 + 'conf/pub:map:tx:method': 2.7, + 'conf/pub:list:{object}': 2.7, + 'conf/pub:list:{object}:{detail}': 2.7, + 'conf/pub:list:currency': 2.7, + 'conf/pub:list:pair:exchange': 2.7, + 'conf/pub:list:pair:margin': 2.7, + 'conf/pub:list:pair:futures': 2.7, + 'conf/pub:list:competitions': 2.7, + 'conf/pub:info:{object}': 2.7, + 'conf/pub:info:{object}:{detail}': 2.7, + 'conf/pub:info:pair': 2.7, + 'conf/pub:info:pair:futures': 2.7, + 'conf/pub:info:tx:status': 2.7, # [deposit, withdrawal] statuses 1 = active, 0 = maintenance + 'conf/pub:fees': 2.7, + 'platform/status': 8, # 30 requests per minute = 0.5 requests per second =>( 1000ms / rateLimit ) / 0.5 = 8 + 'tickers': 2.7, # 90 requests a minute = 1.5 requests per second =>( 1000 / rateLimit ) / 1.5 = 2.666666666 + 'ticker/{symbol}': 2.7, + 'tickers/hist': 2.7, + 'trades/{symbol}/hist': 2.7, + 'book/{symbol}/{precision}': 1, # 240 requests a minute + 'book/{symbol}/P0': 1, + 'book/{symbol}/P1': 1, + 'book/{symbol}/P2': 1, + 'book/{symbol}/P3': 1, + 'book/{symbol}/R0': 1, + 'stats1/{key}:{size}:{symbol}:{side}/{section}': 2.7, + 'stats1/{key}:{size}:{symbol}:{side}/last': 2.7, + 'stats1/{key}:{size}:{symbol}:{side}/hist': 2.7, + 'stats1/{key}:{size}:{symbol}/{section}': 2.7, + 'stats1/{key}:{size}:{symbol}/last': 2.7, + 'stats1/{key}:{size}:{symbol}/hist': 2.7, + 'stats1/{key}:{size}:{symbol}:long/last': 2.7, + 'stats1/{key}:{size}:{symbol}:long/hist': 2.7, + 'stats1/{key}:{size}:{symbol}:short/last': 2.7, + 'stats1/{key}:{size}:{symbol}:short/hist': 2.7, + 'candles/trade:{timeframe}:{symbol}:{period}/{section}': 2.7, + 'candles/trade:{timeframe}:{symbol}/{section}': 2.7, + 'candles/trade:{timeframe}:{symbol}/last': 2.7, + 'candles/trade:{timeframe}:{symbol}/hist': 2.7, + 'status/{type}': 2.7, + 'status/deriv': 2.7, + 'status/deriv/{symbol}/hist': 2.7, + 'liquidations/hist': 80, # 3 requests a minute = 0.05 requests a second =>( 1000ms / rateLimit ) / 0.05 = 80 + 'rankings/{key}:{timeframe}:{symbol}/{section}': 2.7, + 'rankings/{key}:{timeframe}:{symbol}/hist': 2.7, + 'pulse/hist': 2.7, + 'pulse/profile/{nickname}': 2.7, + 'funding/stats/{symbol}/hist': 10, # ratelimit not in docs + 'ext/vasps': 1, + }, + 'post': { + 'calc/trade/avg': 2.7, + 'calc/fx': 2.7, + }, + }, + 'private': { + 'post': { + # 'auth/r/orders/{symbol}/new', # outdated + # 'auth/r/stats/perf:{timeframe}/hist', # outdated + 'auth/r/wallets': 2.7, + 'auth/r/wallets/hist': 2.7, + 'auth/r/orders': 2.7, + 'auth/r/orders/{symbol}': 2.7, + 'auth/w/order/submit': 2.7, + 'auth/w/order/update': 2.7, + 'auth/w/order/cancel': 2.7, + 'auth/w/order/multi': 2.7, + 'auth/w/order/cancel/multi': 2.7, + 'auth/r/orders/{symbol}/hist': 2.7, + 'auth/r/orders/hist': 2.7, + 'auth/r/order/{symbol}:{id}/trades': 2.7, + 'auth/r/trades/{symbol}/hist': 2.7, + 'auth/r/trades/hist': 2.7, + 'auth/r/ledgers/{currency}/hist': 2.7, + 'auth/r/ledgers/hist': 2.7, + 'auth/r/info/margin/{key}': 2.7, + 'auth/r/info/margin/base': 2.7, + 'auth/r/info/margin/sym_all': 2.7, + 'auth/r/positions': 2.7, + 'auth/w/position/claim': 2.7, + 'auth/w/position/increase:': 2.7, + 'auth/r/position/increase/info': 2.7, + 'auth/r/positions/hist': 2.7, + 'auth/r/positions/audit': 2.7, + 'auth/r/positions/snap': 2.7, + 'auth/w/deriv/collateral/set': 2.7, + 'auth/w/deriv/collateral/limits': 2.7, + 'auth/r/funding/offers': 2.7, + 'auth/r/funding/offers/{symbol}': 2.7, + 'auth/w/funding/offer/submit': 2.7, + 'auth/w/funding/offer/cancel': 2.7, + 'auth/w/funding/offer/cancel/all': 2.7, + 'auth/w/funding/close': 2.7, + 'auth/w/funding/auto': 2.7, + 'auth/w/funding/keep': 2.7, + 'auth/r/funding/offers/{symbol}/hist': 2.7, + 'auth/r/funding/offers/hist': 2.7, + 'auth/r/funding/loans': 2.7, + 'auth/r/funding/loans/hist': 2.7, + 'auth/r/funding/loans/{symbol}': 2.7, + 'auth/r/funding/loans/{symbol}/hist': 2.7, + 'auth/r/funding/credits': 2.7, + 'auth/r/funding/credits/hist': 2.7, + 'auth/r/funding/credits/{symbol}': 2.7, + 'auth/r/funding/credits/{symbol}/hist': 2.7, + 'auth/r/funding/trades/{symbol}/hist': 2.7, + 'auth/r/funding/trades/hist': 2.7, + 'auth/r/info/funding/{key}': 2.7, + 'auth/r/info/user': 2.7, + 'auth/r/summary': 2.7, + 'auth/r/logins/hist': 2.7, + 'auth/r/permissions': 2.7, + 'auth/w/token': 2.7, + 'auth/r/audit/hist': 2.7, + 'auth/w/transfer': 2.7, # ratelimit not in docs... + 'auth/w/deposit/address': 24, # 10 requests a minute = 0.166 requests per second =>( 1000ms / rateLimit ) / 0.166 = 24 + 'auth/w/deposit/invoice': 24, # ratelimit not in docs + 'auth/w/withdraw': 24, # ratelimit not in docs + 'auth/r/movements/{currency}/hist': 2.7, + 'auth/r/movements/hist': 2.7, + 'auth/r/alerts': 5.34, # 45 requests a minute = 0.75 requests per second =>( 1000ms / rateLimit ) / 0.749 => 5.34 + 'auth/w/alert/set': 2.7, + 'auth/w/alert/price:{symbol}:{price}/del': 2.7, + 'auth/w/alert/{type}:{symbol}:{price}/del': 2.7, + 'auth/calc/order/avail': 2.7, + 'auth/w/settings/set': 2.7, + 'auth/r/settings': 2.7, + 'auth/w/settings/del': 2.7, + 'auth/r/pulse/hist': 2.7, + 'auth/w/pulse/add': 16, # 15 requests a minute = 0.25 requests per second =>( 1000ms / rateLimit ) / 0.25 => 16 + 'auth/w/pulse/del': 2.7, + }, + }, + }, + 'fees': { + 'trading': { + 'feeSide': 'get', + 'percentage': True, + 'tierBased': True, + 'maker': self.parse_number('0.001'), + 'taker': self.parse_number('0.002'), + 'tiers': { + 'taker': [ + [self.parse_number('0'), self.parse_number('0.002')], + [self.parse_number('500000'), self.parse_number('0.002')], + [self.parse_number('1000000'), self.parse_number('0.002')], + [self.parse_number('2500000'), self.parse_number('0.002')], + [self.parse_number('5000000'), self.parse_number('0.002')], + [self.parse_number('7500000'), self.parse_number('0.002')], + [self.parse_number('10000000'), self.parse_number('0.0018')], + [self.parse_number('15000000'), self.parse_number('0.0016')], + [self.parse_number('20000000'), self.parse_number('0.0014')], + [self.parse_number('25000000'), self.parse_number('0.0012')], + [self.parse_number('30000000'), self.parse_number('0.001')], + ], + 'maker': [ + [self.parse_number('0'), self.parse_number('0.001')], + [self.parse_number('500000'), self.parse_number('0.0008')], + [self.parse_number('1000000'), self.parse_number('0.0006')], + [self.parse_number('2500000'), self.parse_number('0.0004')], + [self.parse_number('5000000'), self.parse_number('0.0002')], + [self.parse_number('7500000'), self.parse_number('0')], + [self.parse_number('10000000'), self.parse_number('0')], + [self.parse_number('15000000'), self.parse_number('0')], + [self.parse_number('20000000'), self.parse_number('0')], + [self.parse_number('25000000'), self.parse_number('0')], + [self.parse_number('30000000'), self.parse_number('0')], + ], + }, + }, + 'funding': { + 'withdraw': {}, + }, + }, + 'precisionMode': SIGNIFICANT_DIGITS, + 'options': { + 'precision': 'R0', # P0, P1, P2, P3, P4, R0 + # convert 'EXCHANGE MARKET' to lowercase 'market' + # convert 'EXCHANGE LIMIT' to lowercase 'limit' + # everything else remains uppercase + 'exchangeTypes': { + 'MARKET': 'market', + 'EXCHANGE MARKET': 'market', + 'LIMIT': 'limit', + 'EXCHANGE LIMIT': 'limit', + # 'STOP': None, + 'EXCHANGE STOP': 'market', + # 'TRAILING STOP': None, + # 'EXCHANGE TRAILING STOP': None, + # 'FOK': None, + 'EXCHANGE FOK': 'limit', + # 'STOP LIMIT': None, + 'EXCHANGE STOP LIMIT': 'limit', + # 'IOC': None, + 'EXCHANGE IOC': 'limit', + }, + # convert 'market' to 'EXCHANGE MARKET' + # convert 'limit' 'EXCHANGE LIMIT' + # everything else remains + 'orderTypes': { + 'market': 'EXCHANGE MARKET', + 'limit': 'EXCHANGE LIMIT', + }, + 'fiat': { + 'USD': 'USD', + 'EUR': 'EUR', + 'JPY': 'JPY', + 'GBP': 'GBP', + 'CHN': 'CHN', + }, + # actually the correct names unlike the v1 + # we don't want to self.extend self with accountsByType in v1 + 'v2AccountsByType': { + 'spot': 'exchange', + 'exchange': 'exchange', + 'funding': 'funding', + 'margin': 'margin', + 'derivatives': 'margin', + 'future': 'margin', + 'swap': 'margin', + }, + 'withdraw': { + 'includeFee': False, + }, + 'networks': { + 'BTC': 'BITCOIN', + 'LTC': 'LITECOIN', + 'ERC20': 'ETHEREUM', + 'OMNI': 'TETHERUSO', + 'LIQUID': 'TETHERUSL', + 'TRC20': 'TETHERUSX', + 'EOS': 'TETHERUSS', + 'AVAX': 'TETHERUSDTAVAX', + 'SOL': 'TETHERUSDTSOL', + 'ALGO': 'TETHERUSDTALG', + 'BCH': 'TETHERUSDTBCH', + 'KSM': 'TETHERUSDTKSM', + 'DVF': 'TETHERUSDTDVF', + 'OMG': 'TETHERUSDTOMG', + }, + 'networksById': { + 'TETHERUSE': 'ERC20', + }, + }, + 'features': { + 'default': { + 'sandbox': False, + 'createOrder': { + 'marginMode': True, + 'triggerPrice': True, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': True, + 'takeProfitPrice': True, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': True, # todo: implement + 'leverage': True, # todo: implement + 'marketBuyRequiresPrice': False, + 'marketBuyByCost': True, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': { + 'max': 75, + }, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 2500, + 'daysBack': None, + 'untilDays': 100000, # todo: implement + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': None, + 'daysBack': None, + 'daysBackCanceled': None, + 'untilDays': 100000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 10000, + }, + }, + 'spot': { + 'extends': 'default', + }, + 'swap': { + 'linear': { + 'extends': 'default', + }, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'exceptions': { + 'exact': { + '11010': RateLimitExceeded, + '10001': PermissionDenied, # api_key: permission invalid(#10001) + '10020': BadRequest, + '10100': AuthenticationError, + '10114': InvalidNonce, + '20060': OnMaintenance, + # {"code":503,"error":"temporarily_unavailable","error_description":"Sorry, the service is temporarily unavailable. See https://www.bitfinex.com/ for more info."} + 'temporarily_unavailable': ExchangeNotAvailable, + }, + 'broad': { + 'available balance is only': InsufficientFunds, + 'not enough exchange balance': InsufficientFunds, + 'Order not found': OrderNotFound, + 'symbol: invalid': BadSymbol, + }, + }, + 'commonCurrencies': { + 'UST': 'USDT', + 'EUTF0': 'EURT', + 'USTF0': 'USDT', + 'ALG': 'ALGO', # https://github.com/ccxt/ccxt/issues/6034 + 'AMP': 'AMPL', + 'ATO': 'ATOM', # https://github.com/ccxt/ccxt/issues/5118 + 'BCHABC': 'XEC', + 'BCHN': 'BCH', + 'DAT': 'DATA', + 'DOG': 'MDOGE', + 'DSH': 'DASH', + 'EDO': 'PNT', + 'EUS': 'EURS', + 'EUT': 'EURT', + 'HTX': 'HT', + 'IDX': 'ID', + 'IOT': 'IOTA', + 'IQX': 'IQ', + 'LUNA': 'LUNC', + 'LUNA2': 'LUNA', + 'MNA': 'MANA', + 'ORS': 'ORS Group', # conflict with Origin Sport #3230 + 'PAS': 'PASS', + 'QSH': 'QASH', + 'QTM': 'QTUM', + 'RBT': 'RBTC', + 'SNG': 'SNGLS', + 'STJ': 'STORJ', + 'TERRAUST': 'USTC', + 'TSD': 'TUSD', + 'YGG': 'YEED', # conflict with Yield Guild Games + 'YYW': 'YOYOW', + 'UDC': 'USDC', + 'VSY': 'VSYS', + 'WAX': 'WAXP', + 'XCH': 'XCHF', + 'ZBT': 'ZB', + }, + }) + + def is_fiat(self, code): + return(code in self.options['fiat']) + + def get_currency_id(self, code): + return 'f' + code + + def get_currency_name(self, code): + # temporary fix for transpiler recognition, even though self is in parent class + if code in self.options['currencyNames']: + return self.options['currencyNames'][code] + raise NotSupported(self.id + ' ' + code + ' not supported for withdrawal') + + def amount_to_precision(self, symbol, amount): + # https://docs.bitfinex.com/docs/introduction#amount-precision + # The amount field allows up to 8 decimals. + # Anything exceeding self will be rounded to the 8th decimal. + symbol = self.safe_symbol(symbol) + return self.decimal_to_precision(amount, TRUNCATE, self.markets[symbol]['precision']['amount'], DECIMAL_PLACES) + + def price_to_precision(self, symbol, price): + symbol = self.safe_symbol(symbol) + price = self.decimal_to_precision(price, ROUND, self.markets[symbol]['precision']['price'], self.precisionMode) + # https://docs.bitfinex.com/docs/introduction#price-precision + # The precision level of all trading prices is based on significant figures. + # All pairs on Bitfinex use up to 5 significant digits and up to 8 decimals(e.g. 1.2345, 123.45, 1234.5, 0.00012345). + # Prices submit with a precision larger than 5 will be cut by the API. + return self.decimal_to_precision(price, TRUNCATE, 8, DECIMAL_PLACES) + + async def fetch_status(self, params={}): + """ + the latest known information on the availability of the exchange API + + https://docs.bitfinex.com/reference/rest-public-platform-status + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `status structure ` + """ + # + # [1] # operative + # [0] # maintenance + # + response = await self.publicGetPlatformStatus(params) + statusRaw = self.safe_string(response, 0) + return { + 'status': self.safe_string({'0': 'maintenance', '1': 'ok'}, statusRaw, statusRaw), + 'updated': None, + 'eta': None, + 'url': None, + 'info': response, + } + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for bitfinex + + https://docs.bitfinex.com/reference/rest-public-conf + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + spotMarketsInfoPromise = self.publicGetConfPubInfoPair(params) + futuresMarketsInfoPromise = self.publicGetConfPubInfoPairFutures(params) + marginIdsPromise = self.publicGetConfPubListPairMargin(params) + spotMarketsInfo, futuresMarketsInfo, marginIds = await asyncio.gather(*[spotMarketsInfoPromise, futuresMarketsInfoPromise, marginIdsPromise]) + spotMarketsInfo = self.safe_list(spotMarketsInfo, 0, []) + futuresMarketsInfo = self.safe_list(futuresMarketsInfo, 0, []) + markets = self.array_concat(spotMarketsInfo, futuresMarketsInfo) + marginIds = self.safe_value(marginIds, 0, []) + # + # [ + # "1INCH:USD", + # [ + # null, + # null, + # null, + # "2.0", + # "100000.0", + # null, + # null, + # null, + # null, + # null, + # null, + # null + # ] + # ] + # + result = [] + for i in range(0, len(markets)): + pair = markets[i] + id = self.safe_string_upper(pair, 0) + market = self.safe_value(pair, 1, {}) + spot = True + if id.find('F0') >= 0: + spot = False + swap = not spot + baseId = None + quoteId = None + if id.find(':') >= 0: + parts = id.split(':') + baseId = parts[0] + quoteId = parts[1] + else: + baseId = id[0:3] + quoteId = id[3:6] + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + splitBase = base.split('F0') + splitQuote = quote.split('F0') + base = self.safe_string(splitBase, 0) + quote = self.safe_string(splitQuote, 0) + symbol = base + '/' + quote + baseId = self.get_currency_id(baseId) + quoteId = self.get_currency_id(quoteId) + settle = None + settleId = None + if swap: + settle = quote + settleId = quote + symbol = symbol + ':' + settle + minOrderSizeString = self.safe_string(market, 3) + maxOrderSizeString = self.safe_string(market, 4) + margin = False + if spot and self.in_array(id, marginIds): + margin = True + result.append({ + 'id': 't' + id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': 'spot' if spot else 'swap', + 'spot': spot, + 'margin': margin, + 'swap': swap, + 'future': False, + 'option': False, + 'active': True, + 'contract': swap, + 'linear': True if swap else None, + 'inverse': False if swap else None, + 'contractSize': self.parse_number('1') if swap else None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': int('8'), # https://github.com/ccxt/ccxt/issues/7310 + 'price': int('5'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.parse_number(minOrderSizeString), + 'max': self.parse_number(maxOrderSizeString), + }, + 'price': { + 'min': self.parse_number('1e-8'), + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': None, # todo: the api needs revision for extra params & endpoints for possibility of returning a timestamp for self + 'info': market, + }) + return result + + async def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://docs.bitfinex.com/reference/rest-public-conf + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + labels = [ + 'pub:list:currency', + 'pub:map:currency:sym', # maps symbols to their API symbols, BAB > BCH + 'pub:map:currency:label', # verbose friendly names, BNT > Bancor + 'pub:map:currency:unit', # maps symbols to unit of measure where applicable + 'pub:map:currency:undl', # maps derivatives symbols to their underlying currency + 'pub:map:currency:pool', # maps symbols to underlying network/protocol they operate on + 'pub:map:currency:explorer', # maps symbols to their recognised block explorer URLs + 'pub:map:currency:tx:fee', # maps currencies to their withdrawal fees https://github.com/ccxt/ccxt/issues/7745, + 'pub:map:tx:method', # maps withdrawal/deposit methods to their API symbols + 'pub:info:tx:status', # maps withdrawal/deposit statuses, coins: 1 = enabled, 0 = maintenance + ] + config = ','.join(labels) + request: dict = { + 'config': config, + } + response = await self.publicGetConfConfig(self.extend(request, params)) + # + # [ + # + # a list of symbols + # ["AAA","ABS","ADA"], + # + # # sym + # # maps symbols to their API symbols, BAB > BCH + # [ + # ["BAB", "BCH"], + # ["CNHT", "CNHt"], + # ["DSH", "DASH"], + # ["IOT", "IOTA"], + # ["LES", "LEO-EOS"], + # ["LET", "LEO-ERC20"], + # ["STJ", "STORJ"], + # ["TSD", "TUSD"], + # ["UDC", "USDC"], + # ["USK", "USDK"], + # ["UST", "USDt"], + # ["USTF0", "USDt0"], + # ["XCH", "XCHF"], + # ["YYW", "YOYOW"], + # # ... + # ], + # # label + # # verbose friendly names, BNT > Bancor + # [ + # ["BAB", "Bitcoin Cash"], + # ["BCH", "Bitcoin Cash"], + # ["LEO", "Unus Sed LEO"], + # ["LES", "Unus Sed LEO(EOS)"], + # ["LET", "Unus Sed LEO(ERC20)"], + # # ... + # ], + # # unit + # # maps symbols to unit of measure where applicable + # [ + # ["IOT", "Mi|MegaIOTA"], + # ], + # # undl + # # maps derivatives symbols to their underlying currency + # [ + # ["USTF0", "UST"], + # ["BTCF0", "BTC"], + # ["ETHF0", "ETH"], + # ], + # # pool + # # maps symbols to underlying network/protocol they operate on + # [ + # ['SAN', 'ETH'], ['OMG', 'ETH'], ['AVT', 'ETH'], ["EDO", "ETH"], + # ['ESS', 'ETH'], ['ATD', 'EOS'], ['ADD', 'EOS'], ["MTO", "EOS"], + # ['PNK', 'ETH'], ['BAB', 'BCH'], ['WLO', 'XLM'], ["VLD", "ETH"], + # ['BTT', 'TRX'], ['IMP', 'ETH'], ['SCR', 'ETH'], ["GNO", "ETH"], + # # ... + # ], + # # explorer + # # maps symbols to their recognised block explorer URLs + # [ + # [ + # "AIO", + # [ + # "https://mainnet.aion.network", + # "https://mainnet.aion.network/#/account/VAL", + # "https://mainnet.aion.network/#/transaction/VAL" + # ] + # ], + # # ... + # ], + # # fee + # # maps currencies to their withdrawal fees + # [ + # ["AAA",[0,0]], + # ["ABS",[0,131.3]], + # ["ADA",[0,0.3]], + # ], + # # deposit/withdrawal data + # [ + # ["BITCOIN", 1, 1, null, null, null, null, 0, 0, null, null, 3], + # ... + # ] + # ] + # + indexed: dict = { + 'sym': self.index_by(self.safe_list(response, 1, []), 0), + 'label': self.index_by(self.safe_list(response, 2, []), 0), + 'unit': self.index_by(self.safe_list(response, 3, []), 0), + 'undl': self.index_by(self.safe_list(response, 4, []), 0), + 'pool': self.index_by(self.safe_list(response, 5, []), 0), + 'explorer': self.index_by(self.safe_list(response, 6, []), 0), + 'fees': self.index_by(self.safe_list(response, 7, []), 0), + 'networks': self.safe_list(response, 8, []), + 'statuses': self.index_by(self.safe_list(response, 9, []), 0), + } + indexedNetworks: dict = {} + for i in range(0, len(indexed['networks'])): + networkObj = indexed['networks'][i] + networkId = self.safe_string(networkObj, 0) + valuesList = self.safe_list(networkObj, 1) + networkName = self.safe_string(valuesList, 0) + # for GOlang transpiler, do with "safe" method + networksList = self.safe_list(indexedNetworks, networkName, []) + networksList.append(networkId) + indexedNetworks[networkName] = networksList + ids = self.safe_list(response, 0, []) + result: dict = {} + for i in range(0, len(ids)): + id = ids[i] + if id.endswith('F0'): + # we get a lot of F0 currencies, skip those + continue + code = self.safe_currency_code(id) + label = self.safe_list(indexed['label'], id, []) + name = self.safe_string(label, 1) + pool = self.safe_list(indexed['pool'], id, []) + rawType = self.safe_string(pool, 1) + isCryptoCoin = (rawType is not None) or (id in indexed['explorer']) # "hacky" solution + type = None + if isCryptoCoin: + type = 'crypto' + feeValues = self.safe_list(indexed['fees'], id, []) + fees = self.safe_list(feeValues, 1, []) + fee = self.safe_number(fees, 1) + undl = self.safe_list(indexed['undl'], id, []) + precision = '8' # default precision, todo: fix "magic constants" + fid = 'f' + id + dwStatuses = self.safe_list(indexed['statuses'], id, []) + depositEnabled = self.safe_integer(dwStatuses, 1) == 1 + withdrawEnabled = self.safe_integer(dwStatuses, 2) == 1 + networks: dict = {} + netwokIds = self.safe_list(indexedNetworks, id, []) + for j in range(0, len(netwokIds)): + networkId = netwokIds[j] + network = self.network_id_to_code(networkId) + networks[network] = { + 'info': networkId, + 'id': networkId.lower(), + 'network': networkId, + 'active': None, + 'deposit': None, + 'withdraw': None, + 'fee': None, + 'precision': None, + 'limits': { + 'withdraw': { + 'min': None, + 'max': None, + }, + }, + } + result[code] = self.safe_currency_structure({ + 'id': fid, + 'uppercaseId': id, + 'code': code, + 'info': [id, label, pool, feeValues, undl], + 'type': type, + 'name': name, + 'active': True, + 'deposit': depositEnabled, + 'withdraw': withdrawEnabled, + 'fee': fee, + 'precision': int(precision), + 'limits': { + 'amount': { + 'min': self.parse_number(self.parse_precision(precision)), + 'max': None, + }, + 'withdraw': { + 'min': fee, + 'max': None, + }, + }, + 'networks': networks, + }) + return result + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://docs.bitfinex.com/reference/rest-auth-wallets + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + # self api call does not return the 'used' amount - use the v1 version instead(which also returns zero balances) + # there is a difference between self and the v1 api, namely trading wallet is called margin in v2 + await self.load_markets() + accountsByType = self.safe_value(self.options, 'v2AccountsByType', {}) + requestedType = self.safe_string(params, 'type', 'exchange') + accountType = self.safe_string(accountsByType, requestedType, requestedType) + if accountType is None: + keys = list(accountsByType.keys()) + raise ExchangeError(self.id + ' fetchBalance() type parameter must be one of ' + ', '.join(keys)) + isDerivative = requestedType == 'derivatives' + query = self.omit(params, 'type') + response = await self.privatePostAuthRWallets(query) + result: dict = {'info': response} + for i in range(0, len(response)): + balance = response[i] + account = self.account() + interest = self.safe_string(balance, 3) + if interest != '0': + account['debt'] = interest + type = self.safe_string(balance, 0) + currencyId = self.safe_string_lower(balance, 1, '') + start = len(currencyId) - 2 + isDerivativeCode = currencyId[start:] == 'f0' + # self will only filter the derivative codes if the requestedType is 'derivatives' + derivativeCondition = (not isDerivative or isDerivativeCode) + if (accountType == type) and derivativeCondition: + code = self.safe_currency_code(currencyId) + account['total'] = self.safe_string(balance, 2) + account['free'] = self.safe_string(balance, 4) + result[code] = account + return self.safe_balance(result) + + async def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://docs.bitfinex.com/reference/rest-auth-transfer + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account to transfer from + :param str toAccount: account to transfer to + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transfer structure ` + """ + # transferring between derivatives wallet and regular wallet is not documented in their API + # however we support it in CCXT(from just looking at web inspector) + await self.load_markets() + accountsByType = self.safe_value(self.options, 'v2AccountsByType', {}) + fromId = self.safe_string(accountsByType, fromAccount) + if fromId is None: + keys = list(accountsByType.keys()) + raise ArgumentsRequired(self.id + ' transfer() fromAccount must be one of ' + ', '.join(keys)) + toId = self.safe_string(accountsByType, toAccount) + if toId is None: + keys = list(accountsByType.keys()) + raise ArgumentsRequired(self.id + ' transfer() toAccount must be one of ' + ', '.join(keys)) + currency = self.currency(code) + fromCurrencyId = self.convert_derivatives_id(currency, fromAccount) + toCurrencyId = self.convert_derivatives_id(currency, toAccount) + requestedAmount = self.currency_to_precision(code, amount) + # self request is slightly different from v1 fromAccount -> from + request: dict = { + 'amount': requestedAmount, + 'currency': fromCurrencyId, + 'currency_to': toCurrencyId, + 'from': fromId, + 'to': toId, + } + response = await self.privatePostAuthWTransfer(self.extend(request, params)) + # + # [ + # 1616451183763, + # "acc_tf", + # null, + # null, + # [ + # 1616451183763, + # "exchange", + # "margin", + # null, + # "UST", + # "UST", + # null, + # 1 + # ], + # null, + # "SUCCESS", + # "1.0 Tether USDt transfered from Exchange to Margin" + # ] + # + error = self.safe_string(response, 0) + if error == 'error': + message = self.safe_string(response, 2, '') + # same message v1 + self.throw_exactly_matched_exception(self.exceptions['exact'], message, self.id + ' ' + message) + raise ExchangeError(self.id + ' ' + message) + return self.parse_transfer({'result': response}, currency) + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + # + # transfer + # + # [ + # 1616451183763, + # "acc_tf", + # null, + # null, + # [ + # 1616451183763, + # "exchange", + # "margin", + # null, + # "UST", + # "UST", + # null, + # 1 + # ], + # null, + # "SUCCESS", + # "1.0 Tether USDt transfered from Exchange to Margin" + # ] + # + result = self.safe_list(transfer, 'result') + timestamp = self.safe_integer(result, 0) + info = self.safe_value(result, 4) + fromAccount = self.safe_string(info, 1) + toAccount = self.safe_string(info, 2) + currencyId = self.safe_string(info, 5) + status = self.safe_string(result, 6) + return { + 'id': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'status': self.parse_transfer_status(status), + 'amount': self.safe_number(info, 7), + 'currency': self.safe_currency_code(currencyId, currency), + 'fromAccount': fromAccount, + 'toAccount': toAccount, + 'info': result, + } + + def parse_transfer_status(self, status: Str) -> Str: + statuses: dict = { + 'SUCCESS': 'ok', + 'ERROR': 'failed', + 'FAILURE': 'failed', + } + return self.safe_string(statuses, status, status) + + def convert_derivatives_id(self, currency, type): + # there is a difference between self and the v1 api, namely trading wallet is called margin in v2 + # { + # "id": "fUSTF0", + # "code": "USTF0", + # "info": ['USTF0', [], [], [], ["USTF0", "UST"]], + info = self.safe_value(currency, 'info') + transferId = self.safe_string(info, 0) + underlying = self.safe_value(info, 4, []) + currencyId = None + if type == 'derivatives': + currencyId = self.safe_string(underlying, 0, transferId) + start = len(currencyId) - 2 + isDerivativeCode = currencyId[start:] == 'F0' + if not isDerivativeCode: + currencyId = currencyId + 'F0' + elif type != 'margin': + currencyId = self.safe_string(underlying, 1, transferId) + else: + currencyId = transferId + return currencyId + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://docs.bitfinex.com/reference/rest-public-book + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return, bitfinex only allows 1, 25, or 100 + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + precision = self.safe_value(self.options, 'precision', 'R0') + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'precision': precision, + } + if limit is not None: + request['len'] = limit + fullRequest = self.extend(request, params) + orderbook = await self.publicGetBookSymbolPrecision(fullRequest) + timestamp = self.milliseconds() + result: dict = { + 'symbol': market['symbol'], + 'bids': [], + 'asks': [], + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'nonce': None, + } + priceIndex = 1 if (fullRequest['precision'] == 'R0') else 0 + for i in range(0, len(orderbook)): + order = orderbook[i] + price = self.safe_number(order, priceIndex) + signedAmount = self.safe_string(order, 2) + amount = Precise.string_abs(signedAmount) + side = 'bids' if Precise.string_gt(signedAmount, '0') else 'asks' + resultSide = result[side] + resultSide.append([price, self.parse_number(amount)]) + result['bids'] = self.sort_by(result['bids'], 0, True) + result['asks'] = self.sort_by(result['asks'], 0) + return result + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # on trading pairs(ex. tBTCUSD) + # + # [ + # SYMBOL, # self index is not present in singular-ticker + # BID, + # BID_SIZE, + # ASK, + # ASK_SIZE, + # DAILY_CHANGE, + # DAILY_CHANGE_RELATIVE, + # LAST_PRICE, + # VOLUME, + # HIGH, + # LOW + # ] + # + # + # on funding currencies(ex. fUSD) + # + # [ + # SYMBOL, # self index is not present in singular-ticker + # FRR, + # BID, + # BID_PERIOD, + # BID_SIZE, + # ASK, + # ASK_PERIOD, + # ASK_SIZE, + # DAILY_CHANGE, + # DAILY_CHANGE_RELATIVE, + # LAST_PRICE, + # VOLUME, + # HIGH, + # LOW, + # _PLACEHOLDER, + # _PLACEHOLDER, + # FRR_AMOUNT_AVAILABLE + # ] + # + length = len(ticker) + isFetchTicker = (length == 10) or (length == 16) + symbol: Str = None + minusIndex = 0 + isFundingCurrency = False + if isFetchTicker: + minusIndex = 1 + isFundingCurrency = (length == 16) + else: + marketId = self.safe_string(ticker, 0) + market = self.safe_market(marketId, market) + isFundingCurrency = (length == 17) + symbol = self.safe_symbol(None, market) + last: Str = None + bid: Str = None + ask: Str = None + change: Str = None + percentage: Str = None + volume: Str = None + high: Str = None + low: Str = None + if isFundingCurrency: + # per api docs, they are different array type + last = self.safe_string(ticker, 10 - minusIndex) + bid = self.safe_string(ticker, 2 - minusIndex) + ask = self.safe_string(ticker, 5 - minusIndex) + change = self.safe_string(ticker, 8 - minusIndex) + percentage = self.safe_string(ticker, 9 - minusIndex) + volume = self.safe_string(ticker, 11 - minusIndex) + high = self.safe_string(ticker, 12 - minusIndex) + low = self.safe_string(ticker, 13 - minusIndex) + else: + # on trading pairs(ex. tBTCUSD or tHMSTR:USD) + last = self.safe_string(ticker, 7 - minusIndex) + bid = self.safe_string(ticker, 1 - minusIndex) + ask = self.safe_string(ticker, 3 - minusIndex) + change = self.safe_string(ticker, 5 - minusIndex) + percentage = self.safe_string(ticker, 6 - minusIndex) + percentage = Precise.string_mul(percentage, '100') + volume = self.safe_string(ticker, 8 - minusIndex) + high = self.safe_string(ticker, 9 - minusIndex) + low = self.safe_string(ticker, 10 - minusIndex) + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': None, + 'datetime': None, + 'high': high, + 'low': low, + 'bid': bid, + 'bidVolume': None, + 'ask': ask, + 'askVolume': None, + 'vwap': None, + 'open': None, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': change, + 'percentage': percentage, + 'average': None, + 'baseVolume': volume, + 'quoteVolume': None, + 'info': ticker, + }, market) + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://docs.bitfinex.com/reference/rest-public-tickers + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + request: dict = {} + if symbols is not None: + ids = self.market_ids(symbols) + request['symbols'] = ','.join(ids) + else: + request['symbols'] = 'ALL' + tickers = await self.publicGetTickers(self.extend(request, params)) + # + # [ + # # on trading pairs(ex. tBTCUSD) + # [ + # SYMBOL, + # BID, + # BID_SIZE, + # ASK, + # ASK_SIZE, + # DAILY_CHANGE, + # DAILY_CHANGE_RELATIVE, + # LAST_PRICE, + # VOLUME, + # HIGH, + # LOW + # ], + # # on funding currencies(ex. fUSD) + # [ + # SYMBOL, + # FRR, + # BID, + # BID_PERIOD, + # BID_SIZE, + # ASK, + # ASK_PERIOD, + # ASK_SIZE, + # DAILY_CHANGE, + # DAILY_CHANGE_RELATIVE, + # LAST_PRICE, + # VOLUME, + # HIGH, + # LOW, + # _PLACEHOLDER, + # _PLACEHOLDER, + # FRR_AMOUNT_AVAILABLE + # ], + # ... + # ] + # + return self.parse_tickers(tickers, symbols) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://docs.bitfinex.com/reference/rest-public-ticker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + ticker = await self.publicGetTickerSymbol(self.extend(request, params)) + return self.parse_ticker(ticker, market) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(public) + # + # [ + # ID, + # MTS, # timestamp + # AMOUNT, + # PRICE + # ] + # + # fetchMyTrades(private) + # + # [ + # ID, + # PAIR, + # MTS_CREATE, + # ORDER_ID, + # EXEC_AMOUNT, + # EXEC_PRICE, + # ORDER_TYPE, + # ORDER_PRICE, + # MAKER, + # FEE, + # FEE_CURRENCY, + # ... + # ] + # + tradeList = self.safe_list(trade, 'result', []) + tradeLength = len(tradeList) + isPrivate = (tradeLength > 5) + id = self.safe_string(tradeList, 0) + amountIndex = 4 if isPrivate else 2 + side = None + amountString = self.safe_string(tradeList, amountIndex) + priceIndex = 5 if isPrivate else 3 + priceString = self.safe_string(tradeList, priceIndex) + if amountString[0] == '-': + side = 'sell' + amountString = Precise.string_abs(amountString) + else: + side = 'buy' + orderId = None + takerOrMaker = None + type = None + fee = None + symbol = self.safe_symbol(None, market) + timestampIndex = 2 if isPrivate else 1 + timestamp = self.safe_integer(tradeList, timestampIndex) + if isPrivate: + marketId = tradeList[1] + symbol = self.safe_symbol(marketId) + orderId = self.safe_string(tradeList, 3) + maker = self.safe_integer(tradeList, 8) + takerOrMaker = 'maker' if (maker == 1) else 'taker' + feeCostString = self.safe_string(tradeList, 9) + feeCostString = Precise.string_neg(feeCostString) + feeCurrencyId = self.safe_string(tradeList, 10) + feeCurrency = self.safe_currency_code(feeCurrencyId) + fee = { + 'cost': feeCostString, + 'currency': feeCurrency, + } + orderType = tradeList[6] + type = self.safe_string(self.options['exchangeTypes'], orderType) + return self.safe_trade({ + 'id': id, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'order': orderId, + 'side': side, + 'type': type, + 'takerOrMaker': takerOrMaker, + 'price': priceString, + 'amount': amountString, + 'cost': None, + 'fee': fee, + 'info': tradeList, + }, market) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://docs.bitfinex.com/reference/rest-public-trades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch, default 120, max 10000 + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param int [params.until]: the latest time in ms to fetch entries for + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchTrades', 'paginate') + if paginate: + return await self.fetch_paginated_call_dynamic('fetchTrades', symbol, since, limit, params, 10000) + market = self.market(symbol) + sort = '-1' + request: dict = { + 'symbol': market['id'], + } + if since is not None: + request['start'] = since + sort = '1' + if limit is not None: + request['limit'] = min(limit, 10000) # default 120, max 10000 + request['sort'] = sort + request, params = self.handle_until_option('end', request, params) + response = await self.publicGetTradesSymbolHist(self.extend(request, params)) + # + # [ + # [ + # ID, + # MTS, # timestamp + # AMOUNT, + # PRICE + # ] + # ] + # + trades = self.sort_by(response, 1) + tradesList = [] + for i in range(0, len(trades)): + tradesList.append({'result': trades[i]}) # convert to array of dicts to match parseOrder signature + return self.parse_trades(tradesList, market, None, limit) + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = 100, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://docs.bitfinex.com/reference/rest-public-candles + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch, default 100 max 10000 + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + :param int [params.until]: timestamp in ms of the latest candle to fetch + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate') + if paginate: + return await self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params, 10000) + market = self.market(symbol) + if limit is None: + limit = 10000 + else: + limit = min(limit, 10000) + request: dict = { + 'symbol': market['id'], + 'timeframe': self.safe_string(self.timeframes, timeframe, timeframe), + 'sort': 1, + 'limit': limit, + } + if since is not None: + request['start'] = since + request, params = self.handle_until_option('end', request, params) + response = await self.publicGetCandlesTradeTimeframeSymbolHist(self.extend(request, params)) + # + # [ + # [1591503840000,0.025069,0.025068,0.025069,0.025068,1.97828998], + # [1591504500000,0.025065,0.025065,0.025065,0.025065,1.0164], + # [1591504620000,0.025062,0.025062,0.025062,0.025062,0.5], + # ] + # + return self.parse_ohlcvs(response, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # [ + # 1457539800000, + # 0.02594, + # 0.02594, + # 0.02594, + # 0.02594, + # 0.1 + # ] + # + return [ + self.safe_integer(ohlcv, 0), + self.safe_number(ohlcv, 1), + self.safe_number(ohlcv, 3), + self.safe_number(ohlcv, 4), + self.safe_number(ohlcv, 2), + self.safe_number(ohlcv, 5), + ] + + def parse_order_status(self, status: Str): + if status is None: + return status + parts = status.split(' ') + state = self.safe_string(parts, 0) + statuses: dict = { + 'ACTIVE': 'open', + 'PARTIALLY': 'open', + 'EXECUTED': 'closed', + 'CANCELED': 'canceled', + 'INSUFFICIENT': 'canceled', + 'POSTONLY CANCELED': 'canceled', + 'RSN_DUST': 'rejected', + 'RSN_PAUSE': 'rejected', + 'IOC CANCELED': 'canceled', + 'FILLORKILL CANCELED': 'canceled', + } + return self.safe_string(statuses, state, status) + + def parse_order_flags(self, flags): + # flags can be added to each other... + flagValues: dict = { + '1024': ['reduceOnly'], + '4096': ['postOnly'], + '5120': ['reduceOnly', 'postOnly'], + # '64': 'hidden', # The hidden order option ensures an order does not appear in the order book + # '512': 'close', # Close position if position present. + # '16384': 'OCO', # The one cancels other order option allows you to place a pair of orders stipulating that if one order is executed fully or partially, then the other is automatically canceled. + # '524288': 'No Var Rates' # Excludes variable rate funding offers from matching against self order, if on margin + } + return self.safe_value(flagValues, flags, None) + + def parse_time_in_force(self, orderType): + orderTypes: dict = { + 'EXCHANGE IOC': 'IOC', + 'EXCHANGE FOK': 'FOK', + 'IOC': 'IOC', # Margin + 'FOK': 'FOK', # Margin + } + return self.safe_string(orderTypes, orderType, 'GTC') + + def parse_order(self, order: dict, market: Market = None) -> Order: + orderList = self.safe_list(order, 'result') + id = self.safe_string(orderList, 0) + marketId = self.safe_string(orderList, 3) + symbol = self.safe_symbol(marketId) + # https://github.com/ccxt/ccxt/issues/6686 + # timestamp = self.safe_timestamp(orderObject, 5) + timestamp = self.safe_integer(orderList, 5) + remaining = Precise.string_abs(self.safe_string(orderList, 6)) + signedAmount = self.safe_string(orderList, 7) + amount = Precise.string_abs(signedAmount) + side = 'sell' if Precise.string_lt(signedAmount, '0') else 'buy' + orderType = self.safe_string(orderList, 8) + type = self.safe_string(self.safe_value(self.options, 'exchangeTypes'), orderType) + timeInForce = self.parse_time_in_force(orderType) + rawFlags = self.safe_string(orderList, 12) + flags = self.parse_order_flags(rawFlags) + postOnly = False + if flags is not None: + for i in range(0, len(flags)): + if flags[i] == 'postOnly': + postOnly = True + price = self.safe_string(orderList, 16) + triggerPrice = None + if (orderType == 'EXCHANGE STOP') or (orderType == 'EXCHANGE STOP LIMIT'): + price = None + triggerPrice = self.safe_string(orderList, 16) + if orderType == 'EXCHANGE STOP LIMIT': + price = self.safe_string(orderList, 19) + status = None + statusString = self.safe_string(orderList, 13) + if statusString is not None: + parts = statusString.split(' @ ') + status = self.parse_order_status(self.safe_string(parts, 0)) + average = self.safe_string(orderList, 17) + clientOrderId = self.safe_string(orderList, 2) + return self.safe_order({ + 'info': orderList, + 'id': id, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'symbol': symbol, + 'type': type, + 'timeInForce': timeInForce, + 'postOnly': postOnly, + 'side': side, + 'price': price, + 'triggerPrice': triggerPrice, + 'amount': amount, + 'cost': None, + 'average': average, + 'filled': None, + 'remaining': remaining, + 'status': status, + 'fee': None, + 'trades': None, + }, market) + + def create_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + @ignore + helper function to build an order request + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency + :param float [price]: the price of the order, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: The price at which a trigger order is triggered at + :param str [params.timeInForce]: "GTC", "IOC", "FOK", or "PO" + :param bool [params.postOnly]: + :param bool [params.reduceOnly]: Ensures that the executed order does not flip the opened position. + :param int [params.flags]: additional order parameters: 4096(Post Only), 1024(Reduce Only), 16384(OCO), 64(Hidden), 512(Close), 524288(No Var Rates) + :param int [params.lev]: leverage for a derivative order, supported by derivative symbol orders only. The value should be between 1 and 100 inclusive. + :param str [params.price_traling]: The trailing price for a trailing stop order + :param str [params.price_aux_limit]: Order price for stop limit orders + :param str [params.price_oco_stop]: OCO stop price + :returns dict: an `order structure ` + """ + market = self.market(symbol) + amountString = self.amount_to_precision(symbol, amount) + amountString = amountString if (side == 'buy') else Precise.string_neg(amountString) + request: dict = { + 'symbol': market['id'], + 'amount': amountString, + } + triggerPrice = self.safe_string_2(params, 'stopPrice', 'triggerPrice') + trailingAmount = self.safe_string(params, 'trailingAmount') + timeInForce = self.safe_string(params, 'timeInForce') + postOnlyParam = self.safe_bool(params, 'postOnly', False) + reduceOnly = self.safe_bool(params, 'reduceOnly', False) + clientOrderId = self.safe_value_2(params, 'cid', 'clientOrderId') + orderType = type.upper() + if trailingAmount is not None: + orderType = 'TRAILING STOP' + request['price_trailing'] = trailingAmount + elif triggerPrice is not None: + # request['price'] is taken for stop orders + request['price'] = self.price_to_precision(symbol, triggerPrice) + if type == 'limit': + orderType = 'STOP LIMIT' + request['price_aux_limit'] = self.price_to_precision(symbol, price) + else: + orderType = 'STOP' + ioc = (timeInForce == 'IOC') + fok = (timeInForce == 'FOK') + postOnly = (postOnlyParam or (timeInForce == 'PO')) + if (ioc or fok) and (price is None): + raise InvalidOrder(self.id + ' createOrder() requires a price argument with IOC and FOK orders') + if (ioc or fok) and (type == 'market'): + raise InvalidOrder(self.id + ' createOrder() does not allow market IOC and FOK orders') + if (type != 'market') and (triggerPrice is None): + request['price'] = self.price_to_precision(symbol, price) + if ioc: + orderType = 'IOC' + elif fok: + orderType = 'FOK' + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('createOrder', params) + if market['spot'] and (marginMode is None): + # The EXCHANGE prefix is only required for non margin spot markets + orderType = 'EXCHANGE ' + orderType + request['type'] = orderType + # flag values may be summed to combine flags + flags = 0 + if postOnly: + flags = self.sum(flags, 4096) + if reduceOnly: + flags = self.sum(flags, 1024) + if flags != 0: + request['flags'] = flags + if clientOrderId is not None: + request['cid'] = clientOrderId + params = self.omit(params, ['triggerPrice', 'stopPrice', 'timeInForce', 'postOnly', 'reduceOnly', 'trailingAmount', 'clientOrderId']) + return self.extend(request, params) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create an order on the exchange + + https://docs.bitfinex.com/reference/rest-auth-submit-order + + :param str symbol: unified CCXT market symbol + :param str type: 'limit' or 'market' + :param str side: 'buy' or 'sell' + :param float amount: the amount of currency to trade + :param float [price]: price of the order + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: the price that triggers a trigger order + :param str [params.timeInForce]: "GTC", "IOC", "FOK", or "PO" + :param boolean [params.postOnly]: set to True if you want to make a post only order + :param boolean [params.reduceOnly]: indicates that the order is to reduce the size of a position + :param int [params.flags]: additional order parameters: 4096(Post Only), 1024(Reduce Only), 16384(OCO), 64(Hidden), 512(Close), 524288(No Var Rates) + :param int [params.lev]: leverage for a derivative order, supported by derivative symbol orders only. The value should be between 1 and 100 inclusive. + :param str [params.price_aux_limit]: order price for stop limit orders + :param str [params.price_oco_stop]: OCO stop price + :param str [params.trailingAmount]: *swap only* the quote amount to trail away from the current market price + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + request = self.create_order_request(symbol, type, side, amount, price, params) + response = await self.privatePostAuthWOrderSubmit(request) + # + # [ + # 1653325121, # Timestamp in milliseconds + # "on-req", # Purpose of notification('on-req', 'oc-req', "uca", 'fon-req', "foc-req") + # null, # unique ID of the message + # null, + # [ + # [ + # 95412102131, # Order ID + # null, # Group ID + # 1653325121798, # Client Order ID + # "tDOGE:UST", # Market ID + # 1653325121798, # Millisecond timestamp of creation + # 1653325121798, # Millisecond timestamp of update + # -10, # Amount(Positive means buy, negative means sell) + # -10, # Original amount + # "EXCHANGE LIMIT", # Type of the order: LIMIT, EXCHANGE LIMIT, MARKET, EXCHANGE MARKET, STOP, EXCHANGE STOP, STOP LIMIT, EXCHANGE STOP LIMIT, TRAILING STOP, EXCHANGE TRAILING STOP, FOK, EXCHANGE FOK, IOC, EXCHANGE IOC. + # null, # Previous order type(stop-limit orders are converted to limit orders so for them previous type is always STOP) + # null, # Millisecond timestamp of Time-In-Force: automatic order cancellation + # null, # _PLACEHOLDER + # 4096, # Flags, see parseOrderFlags() + # "ACTIVE", # Order Status, see parseOrderStatus() + # null, # _PLACEHOLDER + # null, # _PLACEHOLDER + # 0.071, # Price(Stop Price for stop-limit orders, Limit Price for limit orders) + # 0, # Average Price + # 0, # Trailing Price + # 0, # Auxiliary Limit price(for STOP LIMIT) + # null, # _PLACEHOLDER + # null, # _PLACEHOLDER + # null, # _PLACEHOLDER + # 0, # Hidden(0 if False, 1 if True) + # 0, # Placed ID(If another order caused self order to be placed(OCO) self will be that other order's ID) + # null, # _PLACEHOLDER + # null, # _PLACEHOLDER + # null, # _PLACEHOLDER + # "API>BFX", # Routing, indicates origin of action: BFX, ETHFX, API>BFX, API>ETHFX + # null, # _PLACEHOLDER + # null, # _PLACEHOLDER + # {"$F7":1} # additional meta information about the order( $F7 = IS_POST_ONLY(0 if False, 1 if True), $F33 = Leverage(int)) + # ] + # ], + # null, # CODE(work in progress) + # "SUCCESS", # Status of the request + # "Submitting 1 orders." # Message + # ] + # + status = self.safe_string(response, 6) + if status != 'SUCCESS': + errorCode = response[5] + errorText = response[7] + raise ExchangeError(self.id + ' ' + response[6] + ': ' + errorText + '(#' + errorCode + ')') + orders = self.safe_list(response, 4, []) + order = self.safe_list(orders, 0) + newOrder = {'result': order} + return self.parse_order(newOrder, market) + + async def create_orders(self, orders: List[OrderRequest], params={}): + """ + create a list of trade orders + + https://docs.bitfinex.com/reference/rest-auth-order-multi + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + ordersRequests = [] + for i in range(0, len(orders)): + rawOrder = orders[i] + symbol = self.safe_string(rawOrder, 'symbol') + type = self.safe_string(rawOrder, 'type') + side = self.safe_string(rawOrder, 'side') + amount = self.safe_number(rawOrder, 'amount') + price = self.safe_number(rawOrder, 'price') + orderParams = self.safe_dict(rawOrder, 'params', {}) + orderRequest = self.create_order_request(symbol, type, side, amount, price, orderParams) + ordersRequests.append(['on', orderRequest]) + request: dict = { + 'ops': ordersRequests, + } + response = await self.privatePostAuthWOrderMulti(request) + # + # [ + # 1706762515553, + # "ox_multi-req", + # null, + # null, + # [ + # [ + # 1706762515, + # "on-req", + # null, + # null, + # [ + # [139567428547,null,1706762515551,"tBTCUST",1706762515551,1706762515551,0.0001,0.0001,"EXCHANGE LIMIT",null,null,null,0,"ACTIVE",null,null,35000,0,0,0,null,null,null,0,0,null,null,null,"API>BFX",null,null,{}] + # ], + # null, + # "SUCCESS", + # "Submitting 1 orders." + # ], + # ], + # null, + # "SUCCESS", + # "Submitting 2 order operations." + # ] + # + results = [] + data = self.safe_list(response, 4, []) + for i in range(0, len(data)): + entry = data[i] + individualOrder = entry[4] + results.append({'result': individualOrder[0]}) + return self.parse_orders(results) + + async def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders + + https://docs.bitfinex.com/reference/rest-auth-cancel-orders-multiple + + :param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + request: dict = { + 'all': 1, + } + response = await self.privatePostAuthWOrderCancelMulti(self.extend(request, params)) + orders = self.safe_list(response, 4, []) + ordersList = [] + for i in range(0, len(orders)): + ordersList.append({'result': orders[i]}) + return self.parse_orders(ordersList) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://docs.bitfinex.com/reference/rest-auth-cancel-order + + :param str id: order id + :param str symbol: Not used by bitfinex cancelOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + cid = self.safe_value_2(params, 'cid', 'clientOrderId') # client order id + request = None + market = None + if symbol is not None: + market = self.market(symbol) + if cid is not None: + cidDate = self.safe_value(params, 'cidDate') # client order id date + if cidDate is None: + raise InvalidOrder(self.id + " canceling an order by clientOrderId('cid') requires both 'cid' and 'cid_date'('YYYY-MM-DD')") + request = { + 'cid': cid, + 'cid_date': cidDate, + } + params = self.omit(params, ['cid', 'clientOrderId']) + else: + request = { + 'id': int(id), + } + response = await self.privatePostAuthWOrderCancel(self.extend(request, params)) + order = self.safe_value(response, 4) + newOrder = {'result': order} + return self.parse_order(newOrder, market) + + async def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): + """ + cancel multiple orders at the same time + + https://docs.bitfinex.com/reference/rest-auth-cancel-orders-multiple + + :param str[] ids: order ids + :param str symbol: unified market symbol, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an array of `order structures ` + """ + await self.load_markets() + numericIds = [] + for i in range(0, len(ids)): + # numericIds[i] = self.parse_to_numeric(ids[i]) + numericIds.append(self.parse_to_numeric(ids[i])) + request: dict = { + 'id': numericIds, + } + market = None + if symbol is not None: + market = self.market(symbol) + response = await self.privatePostAuthWOrderCancelMulti(self.extend(request, params)) + # + # [ + # 1706740198811, + # "oc_multi-req", + # null, + # null, + # [ + # [ + # 139530205057, + # null, + # 1706740132275, + # "tBTCF0:USTF0", + # 1706740132276, + # 1706740132276, + # 0.0001, + # 0.0001, + # "LIMIT", + # null, + # null, + # null, + # 0, + # "ACTIVE", + # null, + # null, + # 39000, + # 0, + # 0, + # 0, + # null, + # null, + # null, + # 0, + # 0, + # null, + # null, + # null, + # "API>BFX", + # null, + # null, + # { + # "lev": 10, + # "$F33": 10 + # } + # ], + # ], + # null, + # "SUCCESS", + # "Submitting 2 order cancellations." + # ] + # + orders = self.safe_list(response, 4, []) + ordersList = [] + for i in range(0, len(orders)): + ordersList.append({'result': orders[i]}) + return self.parse_orders(ordersList, market) + + async def fetch_open_order(self, id: str, symbol: Str = None, params={}): + """ + fetch an open order by it's id + + https://docs.bitfinex.com/reference/rest-auth-retrieve-orders + https://docs.bitfinex.com/reference/rest-auth-retrieve-orders-by-symbol + + :param str id: order id + :param str symbol: unified market symbol, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + request: dict = { + 'id': [int(id)], + } + orders = await self.fetch_open_orders(symbol, None, None, self.extend(request, params)) + order = self.safe_value(orders, 0) + if order is None: + raise OrderNotFound(self.id + ' order ' + id + ' not found') + return order + + async def fetch_closed_order(self, id: str, symbol: Str = None, params={}): + """ + fetch an open order by it's id + + https://docs.bitfinex.com/reference/rest-auth-retrieve-orders + https://docs.bitfinex.com/reference/rest-auth-retrieve-orders-by-symbol + + :param str id: order id + :param str symbol: unified market symbol, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + request: dict = { + 'id': [int(id)], + } + orders = await self.fetch_closed_orders(symbol, None, None, self.extend(request, params)) + order = self.safe_value(orders, 0) + if order is None: + raise OrderNotFound(self.id + ' order ' + id + ' not found') + return order + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://docs.bitfinex.com/reference/rest-auth-retrieve-orders + https://docs.bitfinex.com/reference/rest-auth-retrieve-orders-by-symbol + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + request: dict = {} + market = None + response = None + if symbol is None: + response = await self.privatePostAuthROrders(self.extend(request, params)) + else: + market = self.market(symbol) + request['symbol'] = market['id'] + response = await self.privatePostAuthROrdersSymbol(self.extend(request, params)) + # + # [ + # [ + # 95408916206, # Order ID + # null, # Group Order ID + # 1653322349926, # Client Order ID + # "tDOGE:UST", # Market ID + # 1653322349926, # Created Timestamp in milliseconds + # 1653322349927, # Updated Timestamp in milliseconds + # -10, # Amount remaining(Positive means buy, negative means sell) + # -10, # Original amount + # "EXCHANGE LIMIT", # Order type + # null, # Previous Order Type + # null, # _PLACEHOLDER + # null, # _PLACEHOLDER + # 0, # Flags, see parseOrderFlags() + # "ACTIVE", # Order Status, see parseOrderStatus() + # null, # _PLACEHOLDER + # null, # _PLACEHOLDER + # 0.11, # Price + # 0, # Average Price + # 0, # Trailing Price + # 0, # Auxiliary Limit price(for STOP LIMIT) + # null, # _PLACEHOLDER + # null, # _PLACEHOLDER + # null, # _PLACEHOLDER + # 0, # Hidden(0 if False, 1 if True) + # 0, # Placed ID(If another order caused self order to be placed(OCO) self will be that other order's ID) + # null, # _PLACEHOLDER + # null, # _PLACEHOLDER + # null, # _PLACEHOLDER + # "API>BFX", # Routing, indicates origin of action: BFX, ETHFX, API>BFX, API>ETHFX + # null, # _PLACEHOLDER + # null, # _PLACEHOLDER + # {"$F7":1} # additional meta information about the order( $F7 = IS_POST_ONLY(0 if False, 1 if True), $F33 = Leverage(int)) + # ], + # ] + # + ordersList = [] + for i in range(0, len(response)): + ordersList.append({'result': response[i]}) + return self.parse_orders(ordersList, market, since, limit) + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://docs.bitfinex.com/reference/rest-auth-retrieve-orders + https://docs.bitfinex.com/reference/rest-auth-retrieve-orders-by-symbol + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch entries for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Order[]: a list of `order structures ` + """ + # returns the most recent closed or canceled orders up to circa two weeks ago + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchClosedOrders', 'paginate') + if paginate: + return await self.fetch_paginated_call_dynamic('fetchClosedOrders', symbol, since, limit, params) + request: dict = {} + if since is not None: + request['start'] = since + if limit is not None: + request['limit'] = limit # default 25, max 2500 + request, params = self.handle_until_option('end', request, params) + market = None + response = None + if symbol is None: + response = await self.privatePostAuthROrdersHist(self.extend(request, params)) + else: + market = self.market(symbol) + request['symbol'] = market['id'] + response = await self.privatePostAuthROrdersSymbolHist(self.extend(request, params)) + # + # [ + # [ + # 95412102131, # Order ID + # null, # Group Order ID + # 1653325121798, # Client Order ID + # "tDOGE:UST", # Market ID + # 1653325122000, # Created Timestamp in milliseconds + # 1653325122000, # Updated Timestamp in milliseconds + # -10, # Amount remaining(Positive means buy, negative means sell) + # -10, # Original amount + # "EXCHANGE LIMIT", # Order type + # null, # Previous Order Type + # null, # Millisecond timestamp of Time-In-Force: automatic order cancellation + # null, # _PLACEHOLDER + # "4096", # Flags, see parseOrderFlags() + # "POSTONLY CANCELED", # Order Status, see parseOrderStatus() + # null, # _PLACEHOLDER + # null, # _PLACEHOLDER + # 0.071, # Price + # 0, # Average Price + # 0, # Trailing Price + # 0, # Auxiliary Limit price(for STOP LIMIT) + # null, # _PLACEHOLDER + # null, # _PLACEHOLDER + # null, # _PLACEHOLDER + # 0, # Notify(0 if False, 1 if True) + # 0, # Hidden(0 if False, 1 if True) + # null, # Placed ID(If another order caused self order to be placed(OCO) self will be that other order's ID) + # null, # _PLACEHOLDER + # null, # _PLACEHOLDER + # "API>BFX", # Routing, indicates origin of action: BFX, ETHFX, API>BFX, API>ETHFX + # null, # _PLACEHOLDER + # null, # _PLACEHOLDER + # {"_$F7":1} # additional meta information about the order( _$F7 = IS_POST_ONLY(0 if False, 1 if True), _$F33 = Leverage(int)) + # ] + # ] + # + ordersList = [] + for i in range(0, len(response)): + ordersList.append({'result': response[i]}) + return self.parse_orders(ordersList, market, since, limit) + + async def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all the trades made from a single order + + https://docs.bitfinex.com/reference/rest-auth-order-trades + + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrderTrades() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + orderId = int(id) + request: dict = { + 'id': orderId, + 'symbol': market['id'], + } + # valid for trades upto 10 days old + response = await self.privatePostAuthROrderSymbolIdTrades(self.extend(request, params)) + tradesList = [] + for i in range(0, len(response)): + tradesList.append({'result': response[i]}) # convert to array of dicts to match parseOrder signature + return self.parse_trades(tradesList, market, since, limit) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://docs.bitfinex.com/reference/rest-auth-trades + https://docs.bitfinex.com/reference/rest-auth-trades-by-symbol + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = None + request: dict = { + 'end': self.milliseconds(), + } + if since is not None: + request['start'] = since + if limit is not None: + request['limit'] = limit # default 25, max 1000 + response = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + response = await self.privatePostAuthRTradesSymbolHist(self.extend(request, params)) + else: + response = await self.privatePostAuthRTradesHist(self.extend(request, params)) + tradesList = [] + for i in range(0, len(response)): + tradesList.append({'result': response[i]}) # convert to array of dicts to match parseOrder signature + return self.parse_trades(tradesList, market, since, limit) + + async def create_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + create a currency deposit address + + https://docs.bitfinex.com/reference/rest-auth-deposit-address + + :param str code: unified currency code of the currency for the deposit address + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + await self.load_markets() + request: dict = { + 'op_renew': 1, + } + return await self.fetch_deposit_address(code, self.extend(request, params)) + + async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://docs.bitfinex.com/reference/rest-auth-deposit-address + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + await self.load_markets() + currency = self.currency(code) + # if not provided explicitly we will try to match using the currency name + network = self.safe_string(params, 'network', code) + currencyNetworks = self.safe_value(currency, 'networks', {}) + currencyNetwork = self.safe_value(currencyNetworks, network) + networkId = self.safe_string(currencyNetwork, 'id') + if networkId is None: + raise ArgumentsRequired(self.id + " fetchDepositAddress() could not find a network for '" + code + "'. You can specify it by providing the 'network' value inside params") + wallet = self.safe_string(params, 'wallet', 'exchange') # 'exchange', 'margin', 'funding' and also old labels 'exchange', 'trading', 'deposit', respectively + params = self.omit(params, 'network', 'wallet') + request: dict = { + 'method': networkId, + 'wallet': wallet, + 'op_renew': 0, # a value of 1 will generate a new address + } + response = await self.privatePostAuthWDepositAddress(self.extend(request, params)) + # + # [ + # 1582269616687, # MTS Millisecond Time Stamp of the update + # "acc_dep", # TYPE Purpose of notification "acc_dep" for account deposit + # null, # MESSAGE_ID unique ID of the message + # null, # not documented + # [ + # null, # PLACEHOLDER + # "BITCOIN", # METHOD Method of deposit + # "BTC", # CURRENCY_CODE Currency code of new address + # null, # PLACEHOLDER + # "1BC9PZqpUmjyEB54uggn8TFKj49zSDYzqG", # ADDRESS + # null, # POOL_ADDRESS + # ], + # null, # CODE null or integer work in progress + # "SUCCESS", # STATUS Status of the notification, SUCCESS, ERROR, FAILURE + # "success", # TEXT Text of the notification + # ] + # + result = self.safe_value(response, 4, []) + poolAddress = self.safe_string(result, 5) + address = self.safe_string(result, 4) if (poolAddress is None) else poolAddress + tag = None if (poolAddress is None) else self.safe_string(result, 4) + self.check_address(address) + return { + 'currency': code, + 'address': address, + 'tag': tag, + 'network': None, + 'info': response, + } + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'SUCCESS': 'ok', + 'COMPLETED': 'ok', + 'ERROR': 'failed', + 'FAILURE': 'failed', + 'CANCELED': 'canceled', + 'PENDING APPROVAL': 'pending', + 'PENDING': 'pending', + 'PENDING REVIEW': 'pending', + 'PENDING CANCELLATION': 'pending', + 'SENDING': 'pending', + 'USER APPROVED': 'pending', + } + return self.safe_string(statuses, status, status) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # withdraw + # + # [ + # 1582271520931, # MTS Millisecond Time Stamp of the update + # "acc_wd-req", # TYPE Purpose of notification "acc_wd-req" account withdrawal request + # null, # MESSAGE_ID unique ID of the message + # null, # not documented + # [ + # 0, # WITHDRAWAL_ID Unique Withdrawal ID + # null, # PLACEHOLDER + # "bitcoin", # METHOD Method of withdrawal + # null, # PAYMENT_ID Payment ID if relevant + # "exchange", # WALLET Sending wallet + # 1, # AMOUNT Amount of Withdrawal less fee + # null, # PLACEHOLDER + # null, # PLACEHOLDER + # 0.0004, # WITHDRAWAL_FEE Fee on withdrawal + # ], + # null, # CODE null or integer Work in progress + # "SUCCESS", # STATUS Status of the notification, it may vary over time SUCCESS, ERROR, FAILURE + # "Invalid bitcoin address(abcdef)", # TEXT Text of the notification + # ] + # + # fetchDepositsWithdrawals + # + # [ + # 13293039, # ID + # "ETH", # CURRENCY + # "ETHEREUM", # CURRENCY_NAME + # null, + # null, + # 1574175052000, # MTS_STARTED + # 1574181326000, # MTS_UPDATED + # null, + # null, + # "CANCELED", # STATUS + # null, + # null, + # -0.24, # AMOUNT, negative for withdrawals + # -0.00135, # FEES + # null, + # null, + # "0x38110e0Fc932CB2BE...........", # DESTINATION_ADDRESS + # null, + # null, + # null, + # "0x523ec8945500.....................................", # TRANSACTION_ID + # "Purchase of 100 pizzas", # WITHDRAW_TRANSACTION_NOTE, might also be: null + # ] + # + transactionLength = len(transaction) + timestamp = None + updated = None + code = None + amount = None + id = None + status = None + tag = None + type = None + feeCost = None + txid = None + addressTo = None + network = None + comment = None + if transactionLength == 8: + data = self.safe_value(transaction, 4, []) + timestamp = self.safe_integer(transaction, 0) + if currency is not None: + code = currency['code'] + feeCost = self.safe_string(data, 8) + if feeCost is not None: + feeCost = Precise.string_abs(feeCost) + amount = self.safe_number(data, 5) + id = self.safe_integer(data, 0) + status = 'ok' + if id == 0: + id = None + status = 'failed' + tag = self.safe_string(data, 3) + type = 'withdrawal' + networkId = self.safe_string(data, 2) + network = self.network_id_to_code(networkId.upper()) # withdraw returns in lowercase + elif transactionLength == 22: + id = self.safe_string(transaction, 0) + currencyId = self.safe_string(transaction, 1) + code = self.safe_currency_code(currencyId, currency) + networkId = self.safe_string(transaction, 2) + network = self.network_id_to_code(networkId) + timestamp = self.safe_integer(transaction, 5) + updated = self.safe_integer(transaction, 6) + status = self.parse_transaction_status(self.safe_string(transaction, 9)) + signedAmount = self.safe_string(transaction, 12) + amount = Precise.string_abs(signedAmount) + if signedAmount is not None: + if Precise.string_lt(signedAmount, '0'): + type = 'withdrawal' + else: + type = 'deposit' + feeCost = self.safe_string(transaction, 13) + if feeCost is not None: + feeCost = Precise.string_abs(feeCost) + addressTo = self.safe_string(transaction, 16) + txid = self.safe_string(transaction, 20) + comment = self.safe_string(transaction, 21) + return { + 'info': transaction, + 'id': id, + 'txid': txid, + 'type': type, + 'currency': code, + 'network': network, + 'amount': self.parse_number(amount), + 'status': status, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'address': addressTo, # self is actually the tag for XRP transfers(the address is missing) + 'addressFrom': None, + 'addressTo': addressTo, + 'tag': tag, # refix it properly for the tag from description + 'tagFrom': None, + 'tagTo': tag, + 'updated': updated, + 'comment': comment, + 'internal': None, + 'fee': { + 'currency': code, + 'cost': self.parse_number(feeCost), + 'rate': None, + }, + } + + async def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://docs.bitfinex.com/reference/rest-auth-summary + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + await self.load_markets() + response = await self.privatePostAuthRSummary(params) + # + # Response Spec: + # [ + # PLACEHOLDER, + # PLACEHOLDER, + # PLACEHOLDER, + # PLACEHOLDER, + # [ + # [ + # MAKER_FEE, + # MAKER_FEE, + # MAKER_FEE, + # PLACEHOLDER, + # PLACEHOLDER, + # DERIV_REBATE + # ], + # [ + # TAKER_FEE_TO_CRYPTO, + # TAKER_FEE_TO_STABLE, + # TAKER_FEE_TO_FIAT, + # PLACEHOLDER, + # PLACEHOLDER, + # DERIV_TAKER_FEE + # ] + # ], + # PLACEHOLDER, + # PLACEHOLDER, + # PLACEHOLDER, + # PLACEHOLDER, + # { + # LEO_LEV, + # LEO_AMOUNT_AVG + # } + # ] + # + # Example response: + # + # [ + # null, + # null, + # null, + # null, + # [ + # [0.001, 0.001, 0.001, null, null, 0.0002], + # [0.002, 0.002, 0.002, null, null, 0.00065] + # ], + # [ + # [ + # { + # "curr": "Total(USD)", + # "vol": "0", + # "vol_safe": "0", + # "vol_maker": "0", + # "vol_BFX": "0", + # "vol_BFX_safe": "0", + # "vol_BFX_maker": "0" + # } + # ], + # {}, + # 0 + # ], + # [null, {}, 0], + # null, + # null, + # {leo_lev: "0", leo_amount_avg: "0"} + # ] + # + result: dict = {} + fiat = self.safe_value(self.options, 'fiat', {}) + feeData = self.safe_value(response, 4, []) + makerData = self.safe_value(feeData, 0, []) + takerData = self.safe_value(feeData, 1, []) + makerFee = self.safe_number(makerData, 0) + makerFeeFiat = self.safe_number(makerData, 2) + makerFeeDeriv = self.safe_number(makerData, 5) + takerFee = self.safe_number(takerData, 0) + takerFeeFiat = self.safe_number(takerData, 2) + takerFeeDeriv = self.safe_number(takerData, 5) + for i in range(0, len(self.symbols)): + symbol = self.symbols[i] + market = self.market(symbol) + fee = { + 'info': response, + 'symbol': symbol, + 'percentage': True, + 'tierBased': True, + } + if market['quote'] in fiat: + fee['maker'] = makerFeeFiat + fee['taker'] = takerFeeFiat + elif market['contract']: + fee['maker'] = makerFeeDeriv + fee['taker'] = takerFeeDeriv + else: # TODO check if stable coin + fee['maker'] = makerFee + fee['taker'] = takerFee + result[symbol] = fee + return result + + async def fetch_deposits_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch history of deposits and withdrawals + + https://docs.bitfinex.com/reference/movement-info + https://docs.bitfinex.com/reference/rest-auth-movements + + :param str [code]: unified currency code for the currency of the deposit/withdrawals, default is None + :param int [since]: timestamp in ms of the earliest deposit/withdrawal, default is None + :param int [limit]: max number of deposit/withdrawals to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `transaction structure ` + """ + await self.load_markets() + currency = None + request: dict = {} + if since is not None: + request['start'] = since + if limit is not None: + request['limit'] = limit # max 1000 + response = None + if code is not None: + currency = self.currency(code) + request['currency'] = currency['uppercaseId'] + response = await self.privatePostAuthRMovementsCurrencyHist(self.extend(request, params)) + else: + response = await self.privatePostAuthRMovementsHist(self.extend(request, params)) + # + # [ + # [ + # 13293039, # ID + # "ETH", # CURRENCY + # "ETHEREUM", # CURRENCY_NAME + # null, + # null, + # 1574175052000, # MTS_STARTED + # 1574181326000, # MTS_UPDATED + # null, + # null, + # "CANCELED", # STATUS + # null, + # null, + # -0.24, # AMOUNT, negative for withdrawals + # -0.00135, # FEES + # null, + # null, + # "0x38110e0Fc932CB2BE...........", # DESTINATION_ADDRESS + # null, + # null, + # null, + # "0x523ec8945500.....................................", # TRANSACTION_ID + # "Purchase of 100 pizzas", # WITHDRAW_TRANSACTION_NOTE, might also be: null + # ] + # ] + # + return self.parse_transactions(response, currency, since, limit) + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://docs.bitfinex.com/reference/rest-auth-withdraw + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + self.check_address(address) + await self.load_markets() + currency = self.currency(code) + # if not provided explicitly we will try to match using the currency name + network = self.safe_string(params, 'network', code) + params = self.omit(params, 'network') + currencyNetworks = self.safe_value(currency, 'networks', {}) + currencyNetwork = self.safe_value(currencyNetworks, network) + networkId = self.safe_string(currencyNetwork, 'id') + if networkId is None: + raise ArgumentsRequired(self.id + " withdraw() could not find a network for '" + code + "'. You can specify it by providing the 'network' value inside params") + wallet = self.safe_string(params, 'wallet', 'exchange') # 'exchange', 'margin', 'funding' and also old labels 'exchange', 'trading', 'deposit', respectively + params = self.omit(params, 'network', 'wallet') + request: dict = { + 'method': networkId, + 'wallet': wallet, + 'amount': self.number_to_string(amount), + 'address': address, + } + if tag is not None: + request['payment_id'] = tag + withdrawOptions = self.safe_value(self.options, 'withdraw', {}) + includeFee = self.safe_bool(withdrawOptions, 'includeFee', False) + if includeFee: + request['fee_deduct'] = 1 + response = await self.privatePostAuthWWithdraw(self.extend(request, params)) + # + # [ + # 1582271520931, # MTS Millisecond Time Stamp of the update + # "acc_wd-req", # TYPE Purpose of notification "acc_wd-req" account withdrawal request + # null, # MESSAGE_ID unique ID of the message + # null, # not documented + # [ + # 0, # WITHDRAWAL_ID Unique Withdrawal ID + # null, # PLACEHOLDER + # "bitcoin", # METHOD Method of withdrawal + # null, # PAYMENT_ID Payment ID if relevant + # "exchange", # WALLET Sending wallet + # 1, # AMOUNT Amount of Withdrawal less fee + # null, # PLACEHOLDER + # null, # PLACEHOLDER + # 0.0004, # WITHDRAWAL_FEE Fee on withdrawal + # ], + # null, # CODE null or integer Work in progress + # "SUCCESS", # STATUS Status of the notification, it may vary over time SUCCESS, ERROR, FAILURE + # "Invalid bitcoin address(abcdef)", # TEXT Text of the notification + # ] + # + # in case of failure: + # + # [ + # "error", + # 10001, + # "Momentary balance check. Please wait few seconds and try the transfer again." + # ] + # + statusMessage = self.safe_string(response, 0) + if statusMessage == 'error': + feedback = self.id + ' ' + response + message = self.safe_string(response, 2, '') + # same message v1 + self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + raise ExchangeError(feedback) # unknown message + text = self.safe_string(response, 7) + if text != 'success': + self.throw_broadly_matched_exception(self.exceptions['broad'], text, text) + return self.parse_transaction(response, currency) + + async def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://docs.bitfinex.com/reference/rest-auth-positions + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + response = await self.privatePostAuthRPositions(params) + # + # [ + # [ + # "tBTCUSD", # SYMBOL + # "ACTIVE", # STATUS + # 0.0195, # AMOUNT + # 8565.0267019, # BASE_PRICE + # 0, # MARGIN_FUNDING + # 0, # MARGIN_FUNDING_TYPE + # -0.33455568705000516, # PL + # -0.0003117550117425625, # PL_PERC + # 7045.876419249083, # PRICE_LIQ + # 3.0673001895895604, # LEVERAGE + # null, # _PLACEHOLDER + # 142355652, # POSITION_ID + # 1574002216000, # MTS_CREATE + # 1574002216000, # MTS_UPDATE + # null, # _PLACEHOLDER + # 0, # TYPE + # null, # _PLACEHOLDER + # 0, # COLLATERAL + # 0, # COLLATERAL_MIN + # # META + # { + # "reason":"TRADE", + # "order_id":34271018124, + # "liq_stage":null, + # "trade_price":"8565.0267019", + # "trade_amount":"0.0195", + # "order_id_oppo":34277498022 + # } + # ] + # ] + # + positionsList = [] + for i in range(0, len(response)): + positionsList.append({'result': response[i]}) + return self.parse_positions(positionsList, symbols) + + def parse_position(self, position: dict, market: Market = None): + # + # [ + # "tBTCUSD", # SYMBOL + # "ACTIVE", # STATUS + # 0.0195, # AMOUNT + # 8565.0267019, # BASE_PRICE + # 0, # MARGIN_FUNDING + # 0, # MARGIN_FUNDING_TYPE + # -0.33455568705000516, # PL + # -0.0003117550117425625, # PL_PERC + # 7045.876419249083, # PRICE_LIQ + # 3.0673001895895604, # LEVERAGE + # null, # _PLACEHOLDER + # 142355652, # POSITION_ID + # 1574002216000, # MTS_CREATE + # 1574002216000, # MTS_UPDATE + # null, # _PLACEHOLDER + # 0, # TYPE + # null, # _PLACEHOLDER + # 0, # COLLATERAL + # 0, # COLLATERAL_MIN + # # META + # { + # "reason": "TRADE", + # "order_id": 34271018124, + # "liq_stage": null, + # "trade_price": "8565.0267019", + # "trade_amount": "0.0195", + # "order_id_oppo": 34277498022 + # } + # ] + # + positionList = self.safe_list(position, 'result') + marketId = self.safe_string(positionList, 0) + amount = self.safe_string(positionList, 2) + timestamp = self.safe_integer(positionList, 12) + meta = self.safe_string(positionList, 19) + tradePrice = self.safe_string(meta, 'trade_price') + tradeAmount = self.safe_string(meta, 'trade_amount') + return self.safe_position({ + 'info': positionList, + 'id': self.safe_string(positionList, 11), + 'symbol': self.safe_symbol(marketId, market), + 'notional': self.parse_number(amount), + 'marginMode': 'isolated', # derivatives use isolated, margin uses cross, https://support.bitfinex.com/hc/en-us/articles/360035475374-Derivatives-Trading-on-Bitfinex + 'liquidationPrice': self.safe_number(positionList, 8), + 'entryPrice': self.safe_number(positionList, 3), + 'unrealizedPnl': self.safe_number(positionList, 6), + 'percentage': self.safe_number(positionList, 7), + 'contracts': None, + 'contractSize': None, + 'markPrice': None, + 'lastPrice': None, + 'side': 'long' if Precise.string_gt(amount, '0') else 'short', + 'hedged': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastUpdateTimestamp': self.safe_integer(positionList, 13), + 'maintenanceMargin': self.safe_number(positionList, 18), + 'maintenanceMarginPercentage': None, + 'collateral': self.safe_number(positionList, 17), + 'initialMargin': self.parse_number(Precise.string_mul(tradeAmount, tradePrice)), + 'initialMarginPercentage': None, + 'leverage': self.safe_number(positionList, 9), + 'marginRatio': None, + 'stopLossPrice': None, + 'takeProfitPrice': None, + }) + + def nonce(self): + return self.milliseconds() + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + request = '/' + self.implode_params(path, params) + query = self.omit(params, self.extract_params(path)) + if api == 'v1': + request = api + request + else: + request = self.version + request + url = self.urls['api'][api] + '/' + request + if api == 'public': + if query: + url += '?' + self.urlencode(query) + if api == 'private': + self.check_required_credentials() + nonce = str(self.nonce()) + body = self.json(query) + auth = '/api/' + request + nonce + body + signature = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha384) + headers = { + 'bfx-nonce': nonce, + 'bfx-apikey': self.apiKey, + 'bfx-signature': signature, + 'Content-Type': 'application/json', + } + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, statusCode, statusText, url, method, headers, body, response, requestHeaders, requestBody): + # ["error", 11010, "ratelimit: error"] + if response is not None: + if not isinstance(response, list): + message = self.safe_string_2(response, 'message', 'error') + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + raise ExchangeError(self.id + ' ' + body) + elif response == '': + raise ExchangeError(self.id + ' returned empty response') + if statusCode == 429: + raise RateLimitExceeded(self.id + ' ' + body) + if statusCode == 500: + # See https://docs.bitfinex.com/docs/abbreviations-glossary#section-errorinfo-codes + errorCode = self.safe_string(response, 1, '') + errorText = self.safe_string(response, 2, '') + feedback = self.id + ' ' + errorText + self.throw_broadly_matched_exception(self.exceptions['broad'], errorText, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], errorText, feedback) + raise ExchangeError(self.id + ' ' + errorText + '(#' + errorCode + ')') + return response + + def parse_ledger_entry_type(self, type: Str): + if type is None: + return None + elif type.find('fee') >= 0 or type.find('charged') >= 0: + return 'fee' + elif type.find('rebate') >= 0: + return 'rebate' + elif type.find('deposit') >= 0 or type.find('withdrawal') >= 0: + return 'transaction' + elif type.find('transfer') >= 0: + return 'transfer' + elif type.find('payment') >= 0: + return 'payout' + elif type.find('exchange') >= 0 or type.find('position') >= 0: + return 'trade' + else: + return type + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + # + # [ + # [ + # 2531822314, # ID: Ledger identifier + # "USD", # CURRENCY: The symbol of the currency(ex. "BTC") + # null, # PLACEHOLDER + # 1573521810000, # MTS: Timestamp in milliseconds + # null, # PLACEHOLDER + # 0.01644445, # AMOUNT: Amount of funds moved + # 0, # BALANCE: New balance + # null, # PLACEHOLDER + # "Settlement @ 185.79 on wallet margin" # DESCRIPTION: Description of ledger transaction + # ] + # ] + # + itemList = self.safe_list(item, 'result', []) + type = None + id = self.safe_string(itemList, 0) + currencyId = self.safe_string(itemList, 1) + code = self.safe_currency_code(currencyId, currency) + currency = self.safe_currency(currencyId, currency) + timestamp = self.safe_integer(itemList, 3) + amount = self.safe_number(itemList, 5) + after = self.safe_number(itemList, 6) + description = self.safe_string(itemList, 8) + if description is not None: + parts = description.split(' @ ') + first = self.safe_string_lower(parts, 0) + type = self.parse_ledger_entry_type(first) + return self.safe_ledger_entry({ + 'info': item, + 'id': id, + 'direction': None, + 'account': None, + 'referenceId': id, + 'referenceAccount': None, + 'type': type, + 'currency': code, + 'amount': amount, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'before': None, + 'after': after, + 'status': None, + 'fee': None, + }, currency) + + async def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + + https://docs.bitfinex.com/reference/rest-auth-ledgers + + :param str [code]: unified currency code, default is None + :param int [since]: timestamp in ms of the earliest ledger entry, default is None + :param int [limit]: max number of ledger entries to return, default is None, max is 2500 + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest ledger entry + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict: a `ledger structure ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchLedger', 'paginate') + if paginate: + return await self.fetch_paginated_call_dynamic('fetchLedger', code, since, limit, params, 2500) + currency = None + request: dict = {} + if since is not None: + request['start'] = since + if limit is not None: + request['limit'] = limit + request, params = self.handle_until_option('end', request, params) + response = None + if code is not None: + currency = self.currency(code) + request['currency'] = currency['uppercaseId'] + response = await self.privatePostAuthRLedgersCurrencyHist(self.extend(request, params)) + else: + response = await self.privatePostAuthRLedgersHist(self.extend(request, params)) + # + # [ + # [ + # 2531822314, # ID: Ledger identifier + # "USD", # CURRENCY: The symbol of the currency(ex. "BTC") + # null, # PLACEHOLDER + # 1573521810000, # MTS: Timestamp in milliseconds + # null, # PLACEHOLDER + # 0.01644445, # AMOUNT: Amount of funds moved + # 0, # BALANCE: New balance + # null, # PLACEHOLDER + # "Settlement @ 185.79 on wallet margin" # DESCRIPTION: Description of ledger transaction + # ] + # ] + # + ledgerObjects = [] + for i in range(0, len(response)): + item = response[i] + ledgerObjects.append({'result': item}) + return self.parse_ledger(ledgerObjects, currency, since, limit) + + async def fetch_funding_rates(self, symbols: Strings = None, params={}) -> FundingRates: + """ + fetch the current funding rate for multiple symbols + + https://docs.bitfinex.com/reference/rest-public-derivatives-status + + :param str[] symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `funding rate structures ` + """ + if symbols is None: + raise ArgumentsRequired(self.id + ' fetchFundingRates() requires a symbols argument') + await self.load_markets() + marketIds = self.market_ids(symbols) + request: dict = { + 'keys': ','.join(marketIds), + } + response = await self.publicGetStatusDeriv(self.extend(request, params)) + # + # [ + # [ + # "tBTCF0:USTF0", + # 1691165059000, + # null, + # 29297.851276225, + # 29277.5, + # null, + # 36950860.76010306, + # null, + # 1691193600000, + # 0.00000527, + # 82, + # null, + # 0.00014548, + # null, + # null, + # 29278.8925, + # null, + # null, + # 9636.07644994, + # null, + # null, + # null, + # 0.0005, + # 0.0025 + # ] + # ] + # + return self.parse_funding_rates(response, symbols) + + async def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical funding rate prices + + https://docs.bitfinex.com/reference/rest-public-derivatives-status-history + + :param str symbol: unified market symbol + :param int [since]: timestamp in ms of the earliest funding rate entry + :param int [limit]: max number of funding rate entrys to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest funding rate + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict: a `funding rate structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchFundingRateHistory() requires a symbol argument') + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchFundingRateHistory', 'paginate') + if paginate: + return await self.fetch_paginated_call_deterministic('fetchFundingRateHistory', symbol, since, limit, '8h', params, 5000) + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if since is not None: + request['start'] = since + request, params = self.handle_until_option('end', request, params) + response = await self.publicGetStatusDerivSymbolHist(self.extend(request, params)) + # + # [ + # [ + # "tBTCF0:USTF0", + # 1691165059000, + # null, + # 29297.851276225, + # 29277.5, + # null, + # 36950860.76010306, + # null, + # 1691193600000, + # 0.00000527, + # 82, + # null, + # 0.00014548, + # null, + # null, + # 29278.8925, + # null, + # null, + # 9636.07644994, + # null, + # null, + # null, + # 0.0005, + # 0.0025 + # ] + # ] + # + rates = [] + for i in range(0, len(response)): + fr = response[i] + rate = self.parse_funding_rate_history(fr, market) + rates.append(rate) + reversedArray = [] + rawRates = self.filter_by_symbol_since_limit(rates, symbol, since, limit) + ratesLength = len(rawRates) + for i in range(0, ratesLength): + index = ratesLength - i - 1 + valueAtIndex = rawRates[index] + reversedArray.append(valueAtIndex) + return reversedArray + + def parse_funding_rate(self, contract, market: Market = None) -> FundingRate: + # + # [ + # "tBTCF0:USTF0", + # 1691165059000, + # null, + # 29297.851276225, + # 29277.5, + # null, + # 36950860.76010306, + # null, + # 1691193600000, + # 0.00000527, + # 82, + # null, + # 0.00014548, + # null, + # null, + # 29278.8925, + # null, + # null, + # 9636.07644994, + # null, + # null, + # null, + # 0.0005, + # 0.0025 + # ] + # + marketId = self.safe_string(contract, 0) + timestamp = self.safe_integer(contract, 1) + nextFundingTimestamp = self.safe_integer(contract, 8) + return { + 'info': contract, + 'symbol': self.safe_symbol(marketId, market), + 'markPrice': self.safe_number(contract, 15), + 'indexPrice': self.safe_number(contract, 3), + 'interestRate': None, + 'estimatedSettlePrice': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'fundingRate': self.safe_number(contract, 12), + 'fundingTimestamp': None, + 'fundingDatetime': None, + 'nextFundingRate': self.safe_number(contract, 9), + 'nextFundingTimestamp': nextFundingTimestamp, + 'nextFundingDatetime': self.iso8601(nextFundingTimestamp), + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': None, + } + + def parse_funding_rate_history(self, contract, market: Market = None): + # + # [ + # 1691165494000, + # null, + # 29278.95838065, + # 29260.5, + # null, + # 36950860.76010305, + # null, + # 1691193600000, + # 0.00001449, + # 222, + # null, + # 0.00014548, + # null, + # null, + # 29260.005, + # null, + # null, + # 9635.86484562, + # null, + # null, + # null, + # 0.0005, + # 0.0025 + # ] + # + timestamp = self.safe_integer(contract, 0) + nextFundingTimestamp = self.safe_integer(contract, 7) + return { + 'info': contract, + 'symbol': self.safe_symbol(None, market), + 'markPrice': self.safe_number(contract, 14), + 'indexPrice': self.safe_number(contract, 2), + 'interestRate': None, + 'estimatedSettlePrice': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'fundingRate': self.safe_number(contract, 11), + 'fundingTimestamp': None, + 'fundingDatetime': None, + 'nextFundingRate': self.safe_number(contract, 8), + 'nextFundingTimestamp': nextFundingTimestamp, + 'nextFundingDatetime': self.iso8601(nextFundingTimestamp), + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + } + + async def fetch_open_interests(self, symbols: Strings = None, params={}): + """ + Retrieves the open interest for a list of symbols + + https://docs.bitfinex.com/reference/rest-public-derivatives-status + + :param str[] [symbols]: a list of unified CCXT market symbols + :param dict [params]: exchange specific parameters + :returns dict[]: a list of `open interest structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + marketIds = ['ALL'] + if symbols is not None: + marketIds = self.market_ids(symbols) + request: dict = { + 'keys': ','.join(marketIds), + } + response = await self.publicGetStatusDeriv(self.extend(request, params)) + # + # [ + # [ + # "tXRPF0:USTF0", # market id + # 1706256986000, # millisecond timestamp + # null, + # 0.512705, # derivative mid price + # 0.512395, # underlying spot mid price + # null, + # 37671483.04, # insurance fund balance + # null, + # 1706284800000, # timestamp of next funding + # 0.00002353, # accrued funding for next period + # 317, # next funding step + # null, + # 0, # current funding + # null, + # null, + # 0.5123016, # mark price + # null, + # null, + # 2233562.03115, # open interest in contracts + # null, + # null, + # null, + # 0.0005, # average spread without funding payment + # 0.0025 # funding payment cap + # ] + # ] + # + return self.parse_open_interests(response, symbols) + + async def fetch_open_interest(self, symbol: str, params={}): + """ + retrieves the open interest of a contract trading pair + + https://docs.bitfinex.com/reference/rest-public-derivatives-status + + :param str symbol: unified CCXT market symbol + :param dict [params]: exchange specific parameters + :returns dict: an `open interest structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'keys': market['id'], + } + response = await self.publicGetStatusDeriv(self.extend(request, params)) + # + # [ + # [ + # "tXRPF0:USTF0", # market id + # 1706256986000, # millisecond timestamp + # null, + # 0.512705, # derivative mid price + # 0.512395, # underlying spot mid price + # null, + # 37671483.04, # insurance fund balance + # null, + # 1706284800000, # timestamp of next funding + # 0.00002353, # accrued funding for next period + # 317, # next funding step + # null, + # 0, # current funding + # null, + # null, + # 0.5123016, # mark price + # null, + # null, + # 2233562.03115, # open interest in contracts + # null, + # null, + # null, + # 0.0005, # average spread without funding payment + # 0.0025 # funding payment cap + # ] + # ] + # + oi = self.safe_list(response, 0) + return self.parse_open_interest(oi, market) + + async def fetch_open_interest_history(self, symbol: str, timeframe='1m', since: Int = None, limit: Int = None, params={}): + """ + retrieves the open interest history of a currency + + https://docs.bitfinex.com/reference/rest-public-derivatives-status-history + + :param str symbol: unified CCXT market symbol + :param str timeframe: the time period of each row of data, not used by bitfinex + :param int [since]: the time in ms of the earliest record to retrieve unix timestamp + :param int [limit]: the number of records in the response + :param dict [params]: exchange specific parameters + :param int [params.until]: the time in ms of the latest record to retrieve unix timestamp + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns: An array of `open interest structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOpenInterestHistory', 'paginate') + if paginate: + return await self.fetch_paginated_call_deterministic('fetchOpenInterestHistory', symbol, since, limit, '8h', params, 5000) + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if since is not None: + request['start'] = since + if limit is not None: + request['limit'] = limit + request, params = self.handle_until_option('end', request, params) + response = await self.publicGetStatusDerivSymbolHist(self.extend(request, params)) + # + # [ + # [ + # 1706295191000, # timestamp + # null, + # 42152.425382, # derivative mid price + # 42133, # spot mid price + # null, + # 37671589.7853521, # insurance fund balance + # null, + # 1706313600000, # timestamp of next funding + # 0.00018734, # accrued funding for next period + # 3343, # next funding step + # null, + # 0.00007587, # current funding + # null, + # null, + # 42134.1, # mark price + # null, + # null, + # 5775.20348804, # open interest number of contracts + # null, + # null, + # null, + # 0.0005, # average spread without funding payment + # 0.0025 # funding payment cap + # ], + # ] + # + return self.parse_open_interests_history(response, market, since, limit) + + def parse_open_interest(self, interest, market: Market = None): + # + # fetchOpenInterest: + # + # [ + # "tXRPF0:USTF0", # market id + # 1706256986000, # millisecond timestamp + # null, + # 0.512705, # derivative mid price + # 0.512395, # underlying spot mid price + # null, + # 37671483.04, # insurance fund balance + # null, + # 1706284800000, # timestamp of next funding + # 0.00002353, # accrued funding for next period + # 317, # next funding step + # null, + # 0, # current funding + # null, + # null, + # 0.5123016, # mark price + # null, + # null, + # 2233562.03115, # open interest in contracts + # null, + # null, + # null, + # 0.0005, # average spread without funding payment + # 0.0025 # funding payment cap + # ] + # + # fetchOpenInterestHistory: + # + # [ + # 1706295191000, # timestamp + # null, + # 42152.425382, # derivative mid price + # 42133, # spot mid price + # null, + # 37671589.7853521, # insurance fund balance + # null, + # 1706313600000, # timestamp of next funding + # 0.00018734, # accrued funding for next period + # 3343, # next funding step + # null, + # 0.00007587, # current funding + # null, + # null, + # 42134.1, # mark price + # null, + # null, + # 5775.20348804, # open interest number of contracts + # null, + # null, + # null, + # 0.0005, # average spread without funding payment + # 0.0025 # funding payment cap + # ] + # + interestLength = len(interest) + openInterestIndex = 17 if (interestLength == 23) else 18 + timestamp = self.safe_integer(interest, 1) + marketId = self.safe_string(interest, 0) + return self.safe_open_interest({ + 'symbol': self.safe_symbol(marketId, market, None, 'swap'), + 'openInterestAmount': self.safe_number(interest, openInterestIndex), + 'openInterestValue': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'info': interest, + }, market) + + async def fetch_liquidations(self, symbol: str, since: Int = None, limit: Int = None, params={}): + """ + retrieves the public liquidations of a trading pair + + https://docs.bitfinex.com/reference/rest-public-liquidations + + :param str symbol: unified CCXT market symbol + :param int [since]: the earliest time in ms to fetch liquidations for + :param int [limit]: the maximum number of liquidation structures to retrieve + :param dict [params]: exchange specific parameters + :param int [params.until]: timestamp in ms of the latest liquidation + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict: an array of `liquidation structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchLiquidations', 'paginate') + if paginate: + return await self.fetch_paginated_call_deterministic('fetchLiquidations', symbol, since, limit, '8h', params, 500) + market = self.market(symbol) + request: dict = {} + if since is not None: + request['start'] = since + if limit is not None: + request['limit'] = limit + request, params = self.handle_until_option('end', request, params) + response = await self.publicGetLiquidationsHist(self.extend(request, params)) + # + # [ + # [ + # [ + # "pos", + # 171085137, + # 1706395919788, + # null, + # "tAVAXF0:USTF0", + # -8, + # 32.868, + # null, + # 1, + # 1, + # null, + # 33.255 + # ] + # ], + # ] + # + return self.parse_liquidations(response, market, since, limit) + + def parse_liquidation(self, liquidation, market: Market = None): + # + # [ + # [ + # "pos", + # 171085137, # position id + # 1706395919788, # timestamp + # null, + # "tAVAXF0:USTF0", # market id + # -8, # amount in contracts + # 32.868, # base price + # null, + # 1, + # 1, + # null, + # 33.255 # acquired price + # ] + # ] + # + entry = liquidation[0] + timestamp = self.safe_integer(entry, 2) + marketId = self.safe_string(entry, 4) + contracts = Precise.string_abs(self.safe_string(entry, 5)) + contractSize = self.safe_string(market, 'contractSize') + baseValue = Precise.string_mul(contracts, contractSize) + price = self.safe_string(entry, 11) + sideFlag = self.safe_integer(entry, 8) + side = 'buy' if (sideFlag == 1) else 'sell' + return self.safe_liquidation({ + 'info': entry, + 'symbol': self.safe_symbol(marketId, market, None, 'contract'), + 'contracts': self.parse_number(contracts), + 'contractSize': self.parse_number(contractSize), + 'price': self.parse_number(price), + 'side': side, + 'baseValue': self.parse_number(baseValue), + 'quoteValue': self.parse_number(Precise.string_mul(baseValue, price)), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + + async def set_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + either adds or reduces margin in a swap position in order to set the margin to a specific value + + https://docs.bitfinex.com/reference/rest-auth-deriv-pos-collateral-set + + :param str symbol: unified market symbol of the market to set margin in + :param float amount: the amount to set the margin to + :param dict [params]: parameters specific to the exchange API endpoint + :returns dict: A `margin structure ` + """ + await self.load_markets() + market = self.market(symbol) + if not market['swap']: + raise NotSupported(self.id + ' setMargin() only support swap markets') + request: dict = { + 'symbol': market['id'], + 'collateral': self.parse_to_numeric(amount), + } + response = await self.privatePostAuthWDerivCollateralSet(self.extend(request, params)) + # + # [ + # [ + # 1 + # ] + # ] + # + data = self.safe_value(response, 0) + return self.parse_margin_modification(data, market) + + def parse_margin_modification(self, data, market=None) -> MarginModification: + # + # setMargin + # + # [ + # [ + # 1 + # ] + # ] + # + marginStatusRaw = data[0] + marginStatus = 'ok' if (marginStatusRaw == 1) else 'failed' + return { + 'info': data, + 'symbol': market['symbol'], + 'type': None, + 'marginMode': 'isolated', + 'amount': None, + 'total': None, + 'code': None, + 'status': marginStatus, + 'timestamp': None, + 'datetime': None, + } + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://docs.bitfinex.com/reference/rest-auth-retrieve-orders + https://docs.bitfinex.com/reference/rest-auth-retrieve-orders-by-symbol + + :param str id: the order id + :param str [symbol]: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + request: dict = { + 'id': [self.parse_to_numeric(id)], + } + market = None + response = None + if symbol is None: + response = await self.privatePostAuthROrders(self.extend(request, params)) + else: + market = self.market(symbol) + request['symbol'] = market['id'] + response = await self.privatePostAuthROrdersSymbol(self.extend(request, params)) + # + # [ + # [ + # 139658969116, + # null, + # 1706843908637, + # "tBTCUST", + # 1706843908637, + # 1706843908638, + # 0.0001, + # 0.0001, + # "EXCHANGE LIMIT", + # null, + # null, + # null, + # 0, + # "ACTIVE", + # null, + # null, + # 35000, + # 0, + # 0, + # 0, + # null, + # null, + # null, + # 0, + # 0, + # null, + # null, + # null, + # "API>BFX", + # null, + # null, + # {} + # ] + # ] + # + order = self.safe_list(response, 0) + newOrder = {'result': order} + return self.parse_order(newOrder, market) + + async def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + """ + edit a trade order + + https://docs.bitfinex.com/reference/rest-auth-update-order + + :param str id: edit order id + :param str symbol: unified symbol of the market to edit an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: the price that triggers a trigger order + :param boolean [params.postOnly]: set to True if you want to make a post only order + :param boolean [params.reduceOnly]: indicates that the order is to reduce the size of a position + :param int [params.flags]: additional order parameters: 4096(Post Only), 1024(Reduce Only), 16384(OCO), 64(Hidden), 512(Close), 524288(No Var Rates) + :param int [params.leverage]: leverage for a derivative order, supported by derivative symbol orders only, the value should be between 1 and 100 inclusive + :param int [params.clientOrderId]: a unique client order id for the order + :param float [params.trailingAmount]: *swap only* the quote amount to trail away from the current market price + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'id': self.parse_to_numeric(id), + } + if amount is not None: + amountString = self.amount_to_precision(symbol, amount) + amountString = amountString if (side == 'buy') else Precise.string_neg(amountString) + request['amount'] = amountString + triggerPrice = self.safe_string_2(params, 'stopPrice', 'triggerPrice') + trailingAmount = self.safe_string(params, 'trailingAmount') + timeInForce = self.safe_string(params, 'timeInForce') + postOnlyParam = self.safe_bool(params, 'postOnly', False) + reduceOnly = self.safe_bool(params, 'reduceOnly', False) + clientOrderId = self.safe_integer_2(params, 'cid', 'clientOrderId') + if trailingAmount is not None: + request['price_trailing'] = trailingAmount + elif triggerPrice is not None: + # request['price'] is taken for stop orders + request['price'] = self.price_to_precision(symbol, triggerPrice) + if type == 'limit': + request['price_aux_limit'] = self.price_to_precision(symbol, price) + postOnly = (postOnlyParam or (timeInForce == 'PO')) + if (type != 'market') and (triggerPrice is None): + request['price'] = self.price_to_precision(symbol, price) + # flag values may be summed to combine flags + flags = 0 + if postOnly: + flags = self.sum(flags, 4096) + if reduceOnly: + flags = self.sum(flags, 1024) + if flags != 0: + request['flags'] = flags + if clientOrderId is not None: + request['cid'] = clientOrderId + leverage = self.safe_integer_2(params, 'leverage', 'lev') + if leverage is not None: + request['lev'] = leverage + params = self.omit(params, ['triggerPrice', 'stopPrice', 'timeInForce', 'postOnly', 'reduceOnly', 'trailingAmount', 'clientOrderId', 'leverage']) + response = await self.privatePostAuthWOrderUpdate(self.extend(request, params)) + # + # [ + # 1706845376402, + # "ou-req", + # null, + # null, + # [ + # 139658969116, + # null, + # 1706843908637, + # "tBTCUST", + # 1706843908637, + # 1706843908638, + # 0.0002, + # 0.0002, + # "EXCHANGE LIMIT", + # null, + # null, + # null, + # 0, + # "ACTIVE", + # null, + # null, + # 35000, + # 0, + # 0, + # 0, + # null, + # null, + # null, + # 0, + # 0, + # null, + # null, + # null, + # "API>BFX", + # null, + # null, + # {} + # ], + # null, + # "SUCCESS", + # "Submitting update to exchange limit buy order for 0.0002 BTC." + # ] + # + status = self.safe_string(response, 6) + if status != 'SUCCESS': + errorCode = response[5] + errorText = response[7] + raise ExchangeError(self.id + ' ' + response[6] + ': ' + errorText + '(#' + errorCode + ')') + order = self.safe_list(response, 4, []) + newOrder = {'result': order} + return self.parse_order(newOrder, market) diff --git a/ccxt/async_support/bitflyer.py b/ccxt/async_support/bitflyer.py new file mode 100644 index 0000000..8075cef --- /dev/null +++ b/ccxt/async_support/bitflyer.py @@ -0,0 +1,1171 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.bitflyer import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currency, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Position, Str, Strings, Ticker, FundingRate, Trade, TradingFeeInterface, Transaction, MarketInterface +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import OnMaintenance +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class bitflyer(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(bitflyer, self).describe(), { + 'id': 'bitflyer', + 'name': 'bitFlyer', + 'countries': ['JP'], + 'version': 'v1', + 'rateLimit': 1000, # their nonce-timestamp is in seconds... + 'hostname': 'bitflyer.com', # or bitflyer.com + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': None, # has but not fully implemented + 'future': None, # has but not fully implemented + 'option': False, + 'cancelAllOrders': None, # https://lightning.bitflyer.com/docs?lang=en#cancel-all-orders + 'cancelOrder': True, + 'createOrder': True, + 'fetchBalance': True, + 'fetchClosedOrders': 'emulated', + 'fetchCurrencies': False, + 'fetchDeposits': True, + 'fetchFundingRate': True, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchMarginMode': False, + 'fetchMarkets': True, + 'fetchMyTrades': True, + 'fetchOpenOrders': 'emulated', + 'fetchOrder': 'emulated', + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchPositionMode': False, + 'fetchPositions': True, + 'fetchTicker': True, + 'fetchTrades': True, + 'fetchTradingFee': True, + 'fetchTradingFees': False, + 'fetchTransfer': False, + 'fetchTransfers': False, + 'fetchWithdrawals': True, + 'transfer': False, + 'withdraw': True, + }, + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/d0217747-e54d-4533-8416-0d553dca74bb', + 'api': { + 'rest': 'https://api.{hostname}', + }, + 'www': 'https://bitflyer.com', + 'doc': 'https://lightning.bitflyer.com/docs?lang=en', + }, + 'api': { + 'public': { + 'get': [ + 'getmarkets/usa', # new(wip) + 'getmarkets/eu', # new(wip) + 'getmarkets', # or 'markets' + 'getboard', # ... + 'getticker', + 'getexecutions', + 'gethealth', + 'getboardstate', + 'getchats', + 'getfundingrate', + ], + }, + 'private': { + 'get': [ + 'getpermissions', + 'getbalance', + 'getbalancehistory', + 'getcollateral', + 'getcollateralhistory', + 'getcollateralaccounts', + 'getaddresses', + 'getcoinins', + 'getcoinouts', + 'getbankaccounts', + 'getdeposits', + 'getwithdrawals', + 'getchildorders', + 'getparentorders', + 'getparentorder', + 'getexecutions', + 'getpositions', + 'gettradingcommission', + ], + 'post': [ + 'sendcoin', + 'withdraw', + 'sendchildorder', + 'cancelchildorder', + 'sendparentorder', + 'cancelparentorder', + 'cancelallchildorders', + ], + }, + }, + 'fees': { + 'trading': { + 'maker': self.parse_number('0.002'), + 'taker': self.parse_number('0.002'), + }, + }, + 'precisionMode': TICK_SIZE, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': False, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': True, # todo implement + }, + 'hedged': False, + 'trailing': False, # todo recheck + 'leverage': False, + 'marketBuyRequiresPrice': False, + 'marketBuyByCost': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, + 'daysBack': None, + 'untilDays': None, + 'symbolRequired': True, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 100, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': None, + 'untilDays': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': None, + 'daysBackCanceled': None, + 'untilDays': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOHLCV': None, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'exceptions': { + 'exact': { + '-2': OnMaintenance, # {"status":-2,"error_message":"Under maintenance","data":null} + }, + }, + }) + + def parse_expiry_date(self, expiry): + day = expiry[0:2] + monthName = expiry[2:5] + year = expiry[5:9] + months: dict = { + 'JAN': '01', + 'FEB': '02', + 'MAR': '03', + 'APR': '04', + 'MAY': '05', + 'JUN': '06', + 'JUL': '07', + 'AUG': '08', + 'SEP': '09', + 'OCT': '10', + 'NOV': '11', + 'DEC': '12', + } + month = self.safe_string(months, monthName) + return self.parse8601(year + '-' + month + '-' + day + 'T00:00:00Z') + + def safe_market(self, marketId: Str = None, market: Market = None, delimiter: Str = None, marketType: Str = None) -> MarketInterface: + # Bitflyer has a different type of conflict in markets, because + # some of their ids(ETH/BTC and BTC/JPY) are duplicated in US, EU and JP. + # Since they're the same we just need to return one + return super(bitflyer, self).safe_market(marketId, market, delimiter, 'spot') + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for bitflyer + + https://lightning.bitflyer.com/docs?lang=en#market-list + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + jp_markets = await self.publicGetGetmarkets(params) + # + # [ + # # spot + # {"product_code": "BTC_JPY", "market_type": "Spot"}, + # {"product_code": "BCH_BTC", "market_type": "Spot"}, + # # forex swap + # {"product_code": "FX_BTC_JPY", "market_type": "FX"}, + # + # # future + # { + # "product_code": "BTCJPY11FEB2022", + # "alias": "BTCJPY_MAT1WK", + # "market_type": "Futures", + # }, + # ] + # + us_markets = await self.publicGetGetmarketsUsa(params) + # + # [ + # {"product_code": "BTC_USD", "market_type": "Spot"}, + # {"product_code": "BTC_JPY", "market_type": "Spot"}, + # ] + # + eu_markets = await self.publicGetGetmarketsEu(params) + # + # [ + # {"product_code": "BTC_EUR", "market_type": "Spot"}, + # {"product_code": "BTC_JPY", "market_type": "Spot"}, + # ] + # + markets = self.array_concat(jp_markets, us_markets) + markets = self.array_concat(markets, eu_markets) + result = [] + for i in range(0, len(markets)): + market = markets[i] + id = self.safe_string(market, 'product_code') + currencies = id.split('_') + marketType = self.safe_string(market, 'market_type') + swap = (marketType == 'FX') + future = (marketType == 'Futures') + spot = not swap and not future + type = 'spot' + settle = None + baseId = None + quoteId = None + expiry = None + if spot: + baseId = self.safe_string(currencies, 0) + quoteId = self.safe_string(currencies, 1) + elif swap: + type = 'swap' + baseId = self.safe_string(currencies, 1) + quoteId = self.safe_string(currencies, 2) + elif future: + alias = self.safe_string(market, 'alias') + if alias is None: + # no alias: + # {product_code: 'BTCJPY11MAR2022', market_type: 'Futures'} + # TODO self will break if there are products with 4 chars + baseId = id[0:3] + quoteId = id[3:6] + # last 9 chars are expiry date + expiryDate = id[-9:] + expiry = self.parse_expiry_date(expiryDate) + else: + splitAlias = alias.split('_') + currencyIds = self.safe_string(splitAlias, 0) + baseId = currencyIds[0:-3] + quoteId = currencyIds[-3:] + splitId = id.split(currencyIds) + expiryDate = self.safe_string(splitId, 1) + expiry = self.parse_expiry_date(expiryDate) + type = 'future' + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + symbol = base + '/' + quote + taker = self.fees['trading']['taker'] + maker = self.fees['trading']['maker'] + contract = swap or future + if contract: + maker = 0.0 + taker = 0.0 + settle = 'JPY' + symbol = symbol + ':' + settle + if future: + symbol = symbol + '-' + self.yymmdd(expiry) + result.append({ + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': type, + 'spot': spot, + 'margin': False, + 'swap': swap, + 'future': future, + 'option': False, + 'active': True, + 'contract': contract, + 'linear': None if spot else True, + 'inverse': None if spot else False, + 'taker': taker, + 'maker': maker, + 'contractSize': None, + 'expiry': expiry, + 'expiryDatetime': self.iso8601(expiry), + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': None, + 'price': None, + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': None, + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + 'info': market, + }) + return result + + def parse_balance(self, response) -> Balances: + result: dict = {'info': response} + for i in range(0, len(response)): + balance = response[i] + currencyId = self.safe_string(balance, 'currency_code') + code = self.safe_currency_code(currencyId) + account = self.account() + account['total'] = self.safe_string(balance, 'amount') + account['free'] = self.safe_string(balance, 'available') + result[code] = account + return self.safe_balance(result) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://lightning.bitflyer.com/docs?lang=en#get-account-asset-balance + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + response = await self.privateGetGetbalance(params) + # + # [ + # { + # "currency_code": "JPY", + # "amount": 1024078, + # "available": 508000 + # }, + # { + # "currency_code": "BTC", + # "amount": 10.24, + # "available": 4.12 + # }, + # { + # "currency_code": "ETH", + # "amount": 20.48, + # "available": 16.38 + # } + # ] + # + return self.parse_balance(response) + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://lightning.bitflyer.com/docs?lang=en#order-book + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'product_code': market['id'], + } + orderbook = await self.publicGetGetboard(self.extend(request, params)) + return self.parse_order_book(orderbook, market['symbol'], None, 'bids', 'asks', 'price', 'size') + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + symbol = self.safe_symbol(None, market) + timestamp = self.parse8601(self.safe_string(ticker, 'timestamp')) + last = self.safe_string(ticker, 'ltp') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': None, + 'low': None, + 'bid': self.safe_string(ticker, 'best_bid'), + 'bidVolume': None, + 'ask': self.safe_string(ticker, 'best_ask'), + 'askVolume': None, + 'vwap': None, + 'open': None, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': self.safe_string(ticker, 'volume_by_product'), + 'quoteVolume': None, + 'info': ticker, + }, market) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://lightning.bitflyer.com/docs?lang=en#ticker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'product_code': market['id'], + } + response = await self.publicGetGetticker(self.extend(request, params)) + return self.parse_ticker(response, market) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(public) v1 + # + # { + # "id":2278466664, + # "side":"SELL", + # "price":56810.7, + # "size":0.08798, + # "exec_date":"2021-11-19T11:46:39.323", + # "buy_child_order_acceptance_id":"JRF20211119-114209-236525", + # "sell_child_order_acceptance_id":"JRF20211119-114639-236919" + # } + # + # fetchMyTrades + # + # { + # "id": 37233, + # "side": "BUY", + # "price": 33470, + # "size": 0.01, + # "exec_date": "2015-07-07T09:57:40.397", + # "child_order_id": "JOR20150707-060559-021935", + # "child_order_acceptance_id": "JRF20150707-060559-396699" + # "commission": 0, + # }, + # + side = self.safe_string_lower(trade, 'side') + if side is not None: + if len(side) < 1: + side = None + order = None + if side is not None: + idInner = side + '_child_order_acceptance_id' + if idInner in trade: + order = trade[idInner] + if order is None: + order = self.safe_string(trade, 'child_order_acceptance_id') + timestamp = self.parse8601(self.safe_string(trade, 'exec_date')) + priceString = self.safe_string(trade, 'price') + amountString = self.safe_string(trade, 'size') + id = self.safe_string(trade, 'id') + market = self.safe_market(None, market) + return self.safe_trade({ + 'id': id, + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'order': order, + 'type': None, + 'side': side, + 'takerOrMaker': None, + 'price': priceString, + 'amount': amountString, + 'cost': None, + 'fee': None, + }, market) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://lightning.bitflyer.com/docs?lang=en#list-executions + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'product_code': market['id'], + } + if limit is not None: + request['count'] = limit + response = await self.publicGetGetexecutions(self.extend(request, params)) + # + # [ + # { + # "id": 39287, + # "side": "BUY", + # "price": 31690, + # "size": 27.04, + # "exec_date": "2015-07-08T02:43:34.823", + # "buy_child_order_acceptance_id": "JRF20150707-200203-452209", + # "sell_child_order_acceptance_id": "JRF20150708-024334-060234" + # }, + # ] + # + return self.parse_trades(response, market, since, limit) + + async def fetch_trading_fee(self, symbol: str, params={}) -> TradingFeeInterface: + """ + fetch the trading fees for a market + + https://lightning.bitflyer.com/docs?lang=en#get-trading-commission + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `fee structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'product_code': market['id'], + } + response = await self.privateGetGettradingcommission(self.extend(request, params)) + # + # { + # commission_rate: '0.0020' + # } + # + fee = self.safe_number(response, 'commission_rate') + return { + 'info': response, + 'symbol': market['symbol'], + 'maker': fee, + 'taker': fee, + 'percentage': None, + 'tierBased': None, + } + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://lightning.bitflyer.com/docs?lang=en#send-a-new-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + request: dict = { + 'product_code': self.market_id(symbol), + 'child_order_type': type.upper(), + 'side': side.upper(), + 'price': price, + 'size': amount, + } + result = await self.privatePostSendchildorder(self.extend(request, params)) + # {"status": - 200, "error_message": "Insufficient funds", "data": null} + id = self.safe_string(result, 'child_order_acceptance_id') + return self.safe_order({ + 'id': id, + 'info': result, + }) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://lightning.bitflyer.com/docs?lang=en#cancel-order + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + await self.load_markets() + request: dict = { + 'product_code': self.market_id(symbol), + 'child_order_acceptance_id': id, + } + response = await self.privatePostCancelchildorder(self.extend(request, params)) + # + # 200 OK. + # + return self.safe_order({ + 'info': response, + }) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'ACTIVE': 'open', + 'COMPLETED': 'closed', + 'CANCELED': 'canceled', + 'EXPIRED': 'canceled', + 'REJECTED': 'canceled', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + timestamp = self.parse8601(self.safe_string(order, 'child_order_date')) + price = self.safe_string(order, 'price') + amount = self.safe_string(order, 'size') + filled = self.safe_string(order, 'executed_size') + remaining = self.safe_string(order, 'outstanding_size') + status = self.parse_order_status(self.safe_string(order, 'child_order_state')) + type = self.safe_string_lower(order, 'child_order_type') + side = self.safe_string_lower(order, 'side') + marketId = self.safe_string(order, 'product_code') + symbol = self.safe_symbol(marketId, market) + fee = None + feeCost = self.safe_number(order, 'total_commission') + if feeCost is not None: + fee = { + 'cost': feeCost, + 'currency': None, + 'rate': None, + } + id = self.safe_string(order, 'child_order_acceptance_id') + return self.safe_order({ + 'id': id, + 'clientOrderId': None, + 'info': order, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'status': status, + 'symbol': symbol, + 'type': type, + 'timeInForce': None, + 'postOnly': None, + 'side': side, + 'price': price, + 'triggerPrice': None, + 'cost': None, + 'amount': amount, + 'filled': filled, + 'remaining': remaining, + 'fee': fee, + 'average': None, + 'trades': None, + }, market) + + async def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = 100, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://lightning.bitflyer.com/docs?lang=en#list-orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrders() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'product_code': market['id'], + 'count': limit, + } + response = await self.privateGetGetchildorders(self.extend(request, params)) + orders = self.parse_orders(response, market, since, limit) + if symbol is not None: + orders = self.filter_by(orders, 'symbol', symbol) + return orders + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = 100, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://lightning.bitflyer.com/docs?lang=en#list-orders + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + request: dict = { + 'child_order_state': 'ACTIVE', + } + return await self.fetch_orders(symbol, since, limit, self.extend(request, params)) + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = 100, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://lightning.bitflyer.com/docs?lang=en#list-orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + request: dict = { + 'child_order_state': 'COMPLETED', + } + return await self.fetch_orders(symbol, since, limit, self.extend(request, params)) + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://lightning.bitflyer.com/docs?lang=en#list-orders + + :param str id: the order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrder() requires a symbol argument') + orders = await self.fetch_orders(symbol) + ordersById = self.index_by(orders, 'id') + if id in ordersById: + return ordersById[id] + raise OrderNotFound(self.id + ' No order found with id ' + id) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://lightning.bitflyer.com/docs?lang=en#list-executions + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMyTrades() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'product_code': market['id'], + } + if limit is not None: + request['count'] = limit + response = await self.privateGetGetexecutions(self.extend(request, params)) + # + # [ + # { + # "id": 37233, + # "side": "BUY", + # "price": 33470, + # "size": 0.01, + # "exec_date": "2015-07-07T09:57:40.397", + # "child_order_id": "JOR20150707-060559-021935", + # "child_order_acceptance_id": "JRF20150707-060559-396699" + # "commission": 0, + # }, + # ] + # + return self.parse_trades(response, market, since, limit) + + async def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://lightning.bitflyer.com/docs?lang=en#get-open-interest-summary + + :param str[] symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + if symbols is None: + raise ArgumentsRequired(self.id + ' fetchPositions() requires a `symbols` argument, exactly one symbol in an array') + await self.load_markets() + request: dict = { + 'product_code': self.market_ids(symbols), + } + response = await self.privateGetGetpositions(self.extend(request, params)) + # + # [ + # { + # "product_code": "FX_BTC_JPY", + # "side": "BUY", + # "price": 36000, + # "size": 10, + # "commission": 0, + # "swap_point_accumulate": -35, + # "require_collateral": 120000, + # "open_date": "2015-11-03T10:04:45.011", + # "leverage": 3, + # "pnl": 965, + # "sfd": -0.5 + # } + # ] + # + # todo unify parsePosition/parsePositions + return response + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://lightning.bitflyer.com/docs?lang=en#withdrawing-funds + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + self.check_address(address) + await self.load_markets() + if code != 'JPY' and code != 'USD' and code != 'EUR': + raise ExchangeError(self.id + ' allows withdrawing JPY, USD, EUR only, ' + code + ' is not supported') + currency = self.currency(code) + request: dict = { + 'currency_code': currency['id'], + 'amount': amount, + # 'bank_account_id': 1234, + } + response = await self.privatePostWithdraw(self.extend(request, params)) + # + # { + # "message_id": "69476620-5056-4003-bcbe-42658a2b041b" + # } + # + return self.parse_transaction(response, currency) + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://lightning.bitflyer.com/docs?lang=en#get-crypto-assets-deposit-history + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + currency = None + request: dict = {} + if code is not None: + currency = self.currency(code) + if limit is not None: + request['count'] = limit # default 100 + response = await self.privateGetGetcoinins(self.extend(request, params)) + # + # [ + # { + # "id": 100, + # "order_id": "CDP20151227-024141-055555", + # "currency_code": "BTC", + # "amount": 0.00002, + # "address": "1WriteySQufKZ2pVuM1oMhPrTtTVFq35j", + # "tx_hash": "9f92ee65a176bb9545f7becb8706c50d07d4cee5ffca34d8be3ef11d411405ae", + # "status": "COMPLETED", + # "event_date": "2015-11-27T08:59:20.301" + # } + # ] + # + return self.parse_transactions(response, currency, since, limit) + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://lightning.bitflyer.com/docs?lang=en#get-crypto-assets-transaction-history + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + currency = None + request: dict = {} + if code is not None: + currency = self.currency(code) + if limit is not None: + request['count'] = limit # default 100 + response = await self.privateGetGetcoinouts(self.extend(request, params)) + # + # [ + # { + # "id": 500, + # "order_id": "CWD20151224-014040-077777", + # "currency_code": "BTC", + # "amount": 0.1234, + # "address": "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", + # "tx_hash": "724c07dfd4044abcb390b0412c3e707dd5c4f373f0a52b3bd295ce32b478c60a", + # "fee": 0.0005, + # "additional_fee": 0.0001, + # "status": "COMPLETED", + # "event_date": "2015-12-24T01:40:40.397" + # } + # ] + # + return self.parse_transactions(response, currency, since, limit) + + def parse_deposit_status(self, status): + statuses: dict = { + 'PENDING': 'pending', + 'COMPLETED': 'ok', + } + return self.safe_string(statuses, status, status) + + def parse_withdrawal_status(self, status): + statuses: dict = { + 'PENDING': 'pending', + 'COMPLETED': 'ok', + } + return self.safe_string(statuses, status, status) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fetchDeposits + # + # { + # "id": 100, + # "order_id": "CDP20151227-024141-055555", + # "currency_code": "BTC", + # "amount": 0.00002, + # "address": "1WriteySQufKZ2pVuM1oMhPrTtTVFq35j", + # "tx_hash": "9f92ee65a176bb9545f7becb8706c50d07d4cee5ffca34d8be3ef11d411405ae", + # "status": "COMPLETED", + # "event_date": "2015-11-27T08:59:20.301" + # } + # + # fetchWithdrawals + # + # { + # "id": 500, + # "order_id": "CWD20151224-014040-077777", + # "currency_code": "BTC", + # "amount": 0.1234, + # "address": "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", + # "tx_hash": "724c07dfd4044abcb390b0412c3e707dd5c4f373f0a52b3bd295ce32b478c60a", + # "fee": 0.0005, + # "additional_fee": 0.0001, + # "status": "COMPLETED", + # "event_date": "2015-12-24T01:40:40.397" + # } + # + # withdraw + # + # { + # "message_id": "69476620-5056-4003-bcbe-42658a2b041b" + # } + # + id = self.safe_string_2(transaction, 'id', 'message_id') + address = self.safe_string(transaction, 'address') + currencyId = self.safe_string(transaction, 'currency_code') + code = self.safe_currency_code(currencyId, currency) + timestamp = self.parse8601(self.safe_string(transaction, 'event_date')) + amount = self.safe_number(transaction, 'amount') + txId = self.safe_string(transaction, 'tx_hash') + rawStatus = self.safe_string(transaction, 'status') + type = None + status = None + fee = None + if 'fee' in transaction: + type = 'withdrawal' + status = self.parse_withdrawal_status(rawStatus) + feeCost = self.safe_string(transaction, 'fee') + additionalFee = self.safe_string(transaction, 'additional_fee') + fee = {'currency': code, 'cost': self.parse_number(Precise.string_add(feeCost, additionalFee))} + else: + type = 'deposit' + status = self.parse_deposit_status(rawStatus) + return { + 'info': transaction, + 'id': id, + 'txid': txId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': None, + 'address': address, + 'addressTo': address, + 'addressFrom': None, + 'tag': None, + 'tagTo': None, + 'tagFrom': None, + 'type': type, + 'amount': amount, + 'currency': code, + 'status': status, + 'updated': None, + 'comment': None, + 'internal': None, + 'fee': fee, + } + + async def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate + + https://lightning.bitflyer.com/docs#funding-rate + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'product_code': market['id'], + } + response = await self.publicGetGetfundingrate(self.extend(request, params)) + # + # { + # "current_funding_rate": -0.003750000000 + # "next_funding_rate_settledate": "2024-04-15T13:00:00" + # } + # + return self.parse_funding_rate(response, market) + + def parse_funding_rate(self, contract, market: Market = None) -> FundingRate: + # + # { + # "current_funding_rate": -0.003750000000 + # "next_funding_rate_settledate": "2024-04-15T13:00:00" + # } + # + nextFundingDatetime = self.safe_string(contract, 'next_funding_rate_settledate') + nextFundingTimestamp = self.parse8601(nextFundingDatetime) + return { + 'info': contract, + 'symbol': self.safe_string(market, 'symbol'), + 'markPrice': None, + 'indexPrice': None, + 'interestRate': None, + 'estimatedSettlePrice': None, + 'timestamp': None, + 'datetime': None, + 'fundingRate': None, + 'fundingTimestamp': None, + 'fundingDatetime': None, + 'nextFundingRate': self.safe_number(contract, 'current_funding_rate'), + 'nextFundingTimestamp': nextFundingTimestamp, + 'nextFundingDatetime': self.iso8601(nextFundingTimestamp), + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': None, + } + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + request = '/' + self.version + '/' + if api == 'private': + request += 'me/' + request += path + if method == 'GET': + if params: + request += '?' + self.urlencode(params) + baseUrl = self.implode_hostname(self.urls['api']['rest']) + url = baseUrl + request + if api == 'private': + self.check_required_credentials() + nonce = str(self.nonce()) + auth = ''.join([nonce, method, request]) + if params: + if method != 'GET': + body = self.json(params) + auth += body + headers = { + 'ACCESS-KEY': self.apiKey, + 'ACCESS-TIMESTAMP': nonce, + 'ACCESS-SIGN': self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256), + 'Content-Type': 'application/json', + } + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None # fallback to the default error handler + feedback = self.id + ' ' + body + # i.e. {"status":-2,"error_message":"Under maintenance","data":null} + errorMessage = self.safe_string(response, 'error_message') + statusCode = self.safe_integer(response, 'status') + if errorMessage is not None: + self.throw_exactly_matched_exception(self.exceptions['exact'], statusCode, feedback) + raise ExchangeError(feedback) + return None diff --git a/ccxt/async_support/bitget.py b/ccxt/async_support/bitget.py new file mode 100644 index 0000000..6bf9083 --- /dev/null +++ b/ccxt/async_support/bitget.py @@ -0,0 +1,10518 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.bitget import ImplicitAPI +import asyncio +import hashlib +import json +from ccxt.base.types import Any, Balances, BorrowInterest, Conversion, CrossBorrowRate, Currencies, Currency, DepositAddress, FundingHistory, Int, IsolatedBorrowRate, LedgerEntry, Leverage, LeverageTier, Liquidation, LongShortRatio, MarginMode, MarginModification, Market, Num, Order, OrderBook, OrderRequest, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, FundingRates, Trade, TradingFeeInterface, TradingFees, Transaction, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import AccountSuspended +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidAddress +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import NotSupported +from ccxt.base.errors import DDoSProtection +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.errors import OnMaintenance +from ccxt.base.errors import InvalidNonce +from ccxt.base.errors import RequestTimeout +from ccxt.base.errors import CancelPending +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class bitget(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(bitget, self).describe(), { + 'id': 'bitget', + 'name': 'Bitget', + 'countries': ['SG'], + 'version': 'v2', + 'rateLimit': 50, # up to 3000 requests per 5 minutes ≈ 600 requests per minute ≈ 10 requests per second ≈ 100 ms + 'certified': True, + 'pro': True, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': True, + 'swap': True, + 'future': True, + 'option': False, + 'addMargin': True, + 'borrowCrossMargin': True, + 'borrowIsolatedMargin': True, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'cancelOrders': True, + 'closeAllPositions': True, + 'closePosition': True, + 'createConvertTrade': True, + 'createDepositAddress': False, + 'createMarketBuyOrderWithCost': True, + 'createMarketOrderWithCost': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createOrders': True, + 'createOrderWithTakeProfitAndStopLoss': True, + 'createPostOnlyOrder': True, + 'createReduceOnlyOrder': False, + 'createStopLimitOrder': True, + 'createStopLossOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'createTakeProfitOrder': True, + 'createTrailingAmountOrder': False, + 'createTrailingPercentOrder': True, + 'createTriggerOrder': True, + 'editOrder': True, + 'fetchAccounts': False, + 'fetchBalance': True, + 'fetchBorrowInterest': True, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchCanceledAndClosedOrders': True, + 'fetchCanceledOrders': True, + 'fetchClosedOrders': True, + 'fetchConvertCurrencies': True, + 'fetchConvertQuote': True, + 'fetchConvertTrade': False, + 'fetchConvertTradeHistory': True, + 'fetchCrossBorrowRate': True, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDeposit': False, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchDepositsWithdrawals': False, + 'fetchDepositWithdrawFee': 'emulated', + 'fetchDepositWithdrawFees': True, + 'fetchFundingHistory': True, + 'fetchFundingInterval': True, + 'fetchFundingIntervals': True, + 'fetchFundingRate': True, + 'fetchFundingRateHistory': True, + 'fetchFundingRates': True, + 'fetchIndexOHLCV': True, + 'fetchIsolatedBorrowRate': True, + 'fetchIsolatedBorrowRates': False, + 'fetchLedger': True, + 'fetchLeverage': True, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': True, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': True, + 'fetchMarketLeverageTiers': True, + 'fetchMarkets': True, + 'fetchMarkOHLCV': True, + 'fetchMarkPrice': True, + 'fetchMyLiquidations': True, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': True, + 'fetchOpenInterestHistory': False, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrderBooks': False, + 'fetchOrders': False, + 'fetchOrderTrades': False, + 'fetchPosition': True, + 'fetchPositionHistory': 'emulated', + 'fetchPositionMode': False, + 'fetchPositions': True, + 'fetchPositionsHistory': True, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchStatus': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': True, + 'fetchTradingFees': True, + 'fetchTransactions': False, + 'fetchTransfer': False, + 'fetchTransfers': True, + 'fetchWithdrawAddresses': False, + 'fetchWithdrawal': False, + 'fetchWithdrawals': True, + 'reduceMargin': True, + 'repayCrossMargin': True, + 'repayIsolatedMargin': True, + 'setLeverage': True, + 'setMargin': False, + 'setMarginMode': True, + 'setPositionMode': True, + 'signIn': False, + 'transfer': True, + 'withdraw': True, + }, + 'timeframes': { + '1m': '1m', + '3m': '3m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1h', + '2h': '2h', + '4h': '4h', + '6h': '6h', + '12h': '12h', + '1d': '1d', + '3d': '3d', + '1w': '1w', + '1M': '1m', + }, + 'hostname': 'bitget.com', + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/fbaa10cc-a277-441d-a5b7-997dd9a87658', + 'api': { + 'spot': 'https://api.{hostname}', + 'mix': 'https://api.{hostname}', + 'user': 'https://api.{hostname}', + 'p2p': 'https://api.{hostname}', + 'broker': 'https://api.{hostname}', + 'margin': 'https://api.{hostname}', + 'common': 'https://api.{hostname}', + 'tax': 'https://api.{hostname}', + 'convert': 'https://api.{hostname}', + 'copy': 'https://api.{hostname}', + 'earn': 'https://api.{hostname}', + 'uta': 'https://api.{hostname}', + }, + 'www': 'https://www.bitget.com', + 'doc': [ + 'https://www.bitget.com/api-doc/common/intro', + 'https://www.bitget.com/api-doc/spot/intro', + 'https://www.bitget.com/api-doc/contract/intro', + 'https://www.bitget.com/api-doc/broker/intro', + 'https://www.bitget.com/api-doc/margin/intro', + 'https://www.bitget.com/api-doc/copytrading/intro', + 'https://www.bitget.com/api-doc/earn/intro', + 'https://bitgetlimited.github.io/apidoc/en/mix', + 'https://bitgetlimited.github.io/apidoc/en/spot', + 'https://bitgetlimited.github.io/apidoc/en/broker', + 'https://bitgetlimited.github.io/apidoc/en/margin', + ], + 'fees': 'https://www.bitget.cc/zh-CN/rate?tab=1', + 'referral': 'https://www.bitget.com/expressly?languageType=0&channelCode=ccxt&vipCode=tg9j', + }, + 'api': { + 'public': { + 'common': { + 'get': { + 'v2/public/annoucements': 1, + 'v2/public/time': 1, + }, + }, + 'spot': { + 'get': { + 'spot/v1/notice/queryAllNotices': 1, # 20 times/1s(IP) => 20/20 = 1 + 'spot/v1/public/time': 1, + 'spot/v1/public/currencies': 6.6667, # 3 times/1s(IP) => 20/3 = 6.6667 + 'spot/v1/public/products': 1, + 'spot/v1/public/product': 1, + 'spot/v1/market/ticker': 1, + 'spot/v1/market/tickers': 1, + 'spot/v1/market/fills': 2, # 10 times/1s(IP) => 20/10 = 2 + 'spot/v1/market/fills-history': 2, + 'spot/v1/market/candles': 1, + 'spot/v1/market/depth': 1, + 'spot/v1/market/spot-vip-level': 2, + 'spot/v1/market/merge-depth': 1, + 'spot/v1/market/history-candles': 1, + 'spot/v1/public/loan/coinInfos': 2, # 10 times/1s(IP) => 20/10 = 2 + 'spot/v1/public/loan/hour-interest': 2, # 10 times/1s(IP) => 20/10 = 2 + 'v2/spot/public/coins': 6.6667, + 'v2/spot/public/symbols': 1, + 'v2/spot/market/vip-fee-rate': 2, + 'v2/spot/market/tickers': 1, + 'v2/spot/market/merge-depth': 1, + 'v2/spot/market/orderbook': 1, + 'v2/spot/market/candles': 1, + 'v2/spot/market/history-candles': 1, + 'v2/spot/market/fills': 2, + 'v2/spot/market/fills-history': 2, + }, + }, + 'mix': { + 'get': { + 'mix/v1/market/contracts': 1, + 'mix/v1/market/depth': 1, + 'mix/v1/market/ticker': 1, + 'mix/v1/market/tickers': 1, + 'mix/v1/market/contract-vip-level': 2, + 'mix/v1/market/fills': 1, + 'mix/v1/market/fills-history': 2, + 'mix/v1/market/candles': 1, + 'mix/v1/market/index': 1, + 'mix/v1/market/funding-time': 1, + 'mix/v1/market/history-fundRate': 1, + 'mix/v1/market/current-fundRate': 1, + 'mix/v1/market/open-interest': 1, + 'mix/v1/market/mark-price': 1, + 'mix/v1/market/symbol-leverage': 1, + 'mix/v1/market/queryPositionLever': 1, + 'mix/v1/market/open-limit': 1, + 'mix/v1/market/history-candles': 1, + 'mix/v1/market/history-index-candles': 1, + 'mix/v1/market/history-mark-candles': 1, + 'mix/v1/market/merge-depth': 1, + 'v2/mix/market/vip-fee-rate': 2, + 'v2/mix/market/merge-depth': 1, + 'v2/mix/market/ticker': 1, + 'v2/mix/market/tickers': 1, + 'v2/mix/market/fills': 1, + 'v2/mix/market/fills-history': 2, + 'v2/mix/market/candles': 1, + 'v2/mix/market/history-candles': 1, + 'v2/mix/market/history-index-candles': 1, + 'v2/mix/market/history-mark-candles': 1, + 'v2/mix/market/open-interest': 1, + 'v2/mix/market/funding-time': 1, + 'v2/mix/market/symbol-price': 1, + 'v2/mix/market/history-fund-rate': 1, + 'v2/mix/market/current-fund-rate': 1, + 'v2/mix/market/contracts': 1, + 'v2/mix/market/query-position-lever': 2, + 'v2/mix/market/account-long-short': 20, + }, + }, + 'margin': { + 'get': { + 'margin/v1/cross/public/interestRateAndLimit': 2, # 10 times/1s(IP) => 20/10 = 2 + 'margin/v1/isolated/public/interestRateAndLimit': 2, # 10 times/1s(IP) => 20/10 = 2 + 'margin/v1/cross/public/tierData': 2, # 10 times/1s(IP) => 20/10 = 2 + 'margin/v1/isolated/public/tierData': 2, # 10 times/1s(IP) => 20/10 = 2 + 'margin/v1/public/currencies': 1, # 20 times/1s(IP) => 20/20 = 1 + 'v2/margin/currencies': 2, + 'v2/margin/market/long-short-ratio': 20, + }, + }, + 'earn': { + 'get': { + 'v2/earn/loan/public/coinInfos': 2, + 'v2/earn/loan/public/hour-interest': 2, + }, + }, + 'uta': { + 'get': { + 'v3/market/instruments': 1, + 'v3/market/tickers': 1, + 'v3/market/orderbook': 1, + 'v3/market/fills': 1, + 'v3/market/open-interest': 1, + 'v3/market/candles': 1, + 'v3/market/history-candles': 1, + 'v3/market/current-fund-rate': 1, + 'v3/market/history-fund-rate': 1, + 'v3/market/risk-reserve': 1, + 'v3/market/discount-rate': 1, + 'v3/market/margin-loans': 1, + 'v3/market/position-tier': 1, + 'v3/market/oi-limit': 2, + }, + }, + }, + 'private': { + 'spot': { + 'get': { + 'spot/v1/wallet/deposit-address': 4, + 'spot/v1/wallet/withdrawal-list': 1, + 'spot/v1/wallet/deposit-list': 1, + 'spot/v1/account/getInfo': 20, + 'spot/v1/account/assets': 2, + 'spot/v1/account/assets-lite': 2, # 10 times/1s(UID) => 20/10 = 2 + 'spot/v1/account/transferRecords': 1, # 20 times/1s(UID) => 20/20 = 1 + 'spot/v1/convert/currencies': 2, + 'spot/v1/convert/convert-record': 2, + 'spot/v1/loan/ongoing-orders': 2, # 10 times/1s(UID) => 20/10 = 2 + 'spot/v1/loan/repay-history': 2, # 10 times/1s(UID) => 20/10 = 2 + 'spot/v1/loan/revise-history': 2, # 10 times/1s(UID) => 20/10 = 2 + 'spot/v1/loan/borrow-history': 2, # 10 times/1s(UID) => 20/10 = 2 + 'spot/v1/loan/debts': 2, # 10 times/1s(UID) => 20/10 = 2 + 'v2/spot/trade/orderInfo': 1, + 'v2/spot/trade/unfilled-orders': 1, + 'v2/spot/trade/history-orders': 1, + 'v2/spot/trade/fills': 2, + 'v2/spot/trade/current-plan-order': 1, + 'v2/spot/trade/history-plan-order': 1, + 'v2/spot/account/info': 20, + 'v2/spot/account/assets': 2, + 'v2/spot/account/subaccount-assets': 2, + 'v2/spot/account/bills': 2, + 'v2/spot/account/transferRecords': 1, + 'v2/account/funding-assets': 2, + 'v2/account/bot-assets': 2, + 'v2/account/all-account-balance': 20, + 'v2/spot/wallet/deposit-address': 2, + 'v2/spot/wallet/deposit-records': 2, + 'v2/spot/wallet/withdrawal-records': 2, + }, + 'post': { + 'spot/v1/wallet/transfer': 4, + 'spot/v1/wallet/transfer-v2': 4, + 'spot/v1/wallet/subTransfer': 10, + 'spot/v1/wallet/withdrawal': 4, + 'spot/v1/wallet/withdrawal-v2': 4, + 'spot/v1/wallet/withdrawal-inner': 4, + 'spot/v1/wallet/withdrawal-inner-v2': 4, + 'spot/v1/account/sub-account-spot-assets': 200, + 'spot/v1/account/bills': 2, + 'spot/v1/trade/orders': 2, + 'spot/v1/trade/batch-orders': 4, + 'spot/v1/trade/cancel-order': 2, + 'spot/v1/trade/cancel-order-v2': 2, + 'spot/v1/trade/cancel-symbol-order': 2, + 'spot/v1/trade/cancel-batch-orders': 4, + 'spot/v1/trade/cancel-batch-orders-v2': 4, + 'spot/v1/trade/orderInfo': 1, + 'spot/v1/trade/open-orders': 1, + 'spot/v1/trade/history': 1, + 'spot/v1/trade/fills': 1, + 'spot/v1/plan/placePlan': 1, + 'spot/v1/plan/modifyPlan': 1, + 'spot/v1/plan/cancelPlan': 1, + 'spot/v1/plan/currentPlan': 1, + 'spot/v1/plan/historyPlan': 1, + 'spot/v1/plan/batchCancelPlan': 2, # 10 times/1s(UID) => 20/10 = 2 + 'spot/v1/convert/quoted-price': 4, + 'spot/v1/convert/trade': 4, + 'spot/v1/loan/borrow': 2, # 10 times/1s(UID) => 20/10 = 2 + 'spot/v1/loan/repay': 2, # 10 times/1s(UID) => 20/10 = 2 + 'spot/v1/loan/revise-pledge': 2, # 10 times/1s(UID) => 20/10 = 2 + 'spot/v1/trace/order/orderCurrentList': 2, # 10 times/1s(UID) => 20/10 = 2 + 'spot/v1/trace/order/orderHistoryList': 2, # 10 times/1s(UID) => 20/10 = 2 + 'spot/v1/trace/order/closeTrackingOrder': 2, # 10 times/1s(UID) => 20/10 = 2 + 'spot/v1/trace/order/updateTpsl': 2, # 10 times/1s(UID) => 20/10 = 2 + 'spot/v1/trace/order/followerEndOrder': 2, # 10 times/1s(UID) => 20/10 = 2 + 'spot/v1/trace/order/spotInfoList': 2, # 10 times/1s(UID) => 20/10 = 2 + 'spot/v1/trace/config/getTraderSettings': 2, # 10 times/1s(UID) => 20/10 = 2 + 'spot/v1/trace/config/getFollowerSettings': 2, # 10 times/1s(UID) => 20/10 = 2 + 'spot/v1/trace/user/myTraders': 2, # 10 times/1s(UID) => 20/10 = 2 + 'spot/v1/trace/config/setFollowerConfig': 2, # 10 times/1s(UID) => 20/10 = 2 + 'spot/v1/trace/user/myFollowers': 2, # 10 times/1s(UID) => 20/10 = 2 + 'spot/v1/trace/config/setProductCode': 2, # 10 times/1s(UID) => 20/10 = 2 + 'spot/v1/trace/user/removeTrader': 2, # 10 times/1s(UID) => 20/10 = 2 + 'spot/v1/trace/getRemovableFollower': 2, + 'spot/v1/trace/user/removeFollower': 2, + 'spot/v1/trace/profit/totalProfitInfo': 2, # 10 times/1s(UID) => 20/10 = 2 + 'spot/v1/trace/profit/totalProfitList': 2, # 10 times/1s(UID) => 20/10 = 2 + 'spot/v1/trace/profit/profitHisList': 2, # 10 times/1s(UID) => 20/10 = 2 + 'spot/v1/trace/profit/profitHisDetailList': 2, # 10 times/1s(UID) => 20/10 = 2 + 'spot/v1/trace/profit/waitProfitDetailList': 2, # 10 times/1s(UID) => 20/10 = 2 + 'spot/v1/trace/user/getTraderInfo': 2, # 10 times/1s(UID) => 20/10 = 2 + 'v2/spot/trade/place-order': 2, + 'v2/spot/trade/cancel-order': 2, + 'v2/spot/trade/batch-orders': 20, + 'v2/spot/trade/batch-cancel-order': 2, + 'v2/spot/trade/cancel-symbol-order': 4, + 'v2/spot/trade/place-plan-order': 1, + 'v2/spot/trade/modify-plan-order': 1, + 'v2/spot/trade/cancel-plan-order': 1, + 'v2/spot/trade/cancel-replace-order': 2, + 'v2/spot/trade/batch-cancel-plan-order': 2, + 'v2/spot/wallet/transfer': 2, + 'v2/spot/wallet/subaccount-transfer': 2, + 'v2/spot/wallet/withdrawal': 2, + 'v2/spot/wallet/cancel-withdrawal': 2, + 'v2/spot/wallet/modify-deposit-account': 2, + }, + }, + 'mix': { + 'get': { + 'mix/v1/account/account': 2, + 'mix/v1/account/accounts': 2, + 'mix/v1/position/singlePosition': 2, + 'mix/v1/position/singlePosition-v2': 2, + 'mix/v1/position/allPosition': 4, # 5 times/1s(UID) => 20/5 = 4 + 'mix/v1/position/allPosition-v2': 4, # 5 times/1s(UID) => 20/5 = 4 + 'mix/v1/position/history-position': 1, + 'mix/v1/account/accountBill': 2, + 'mix/v1/account/accountBusinessBill': 4, + 'mix/v1/order/current': 1, # 20 times/1s(UID) => 20/20 = 1 + 'mix/v1/order/marginCoinCurrent': 1, # 20 times/1s(UID) => 20/20 = 1 + 'mix/v1/order/history': 2, + 'mix/v1/order/historyProductType': 4, # 5 times/1s(UID) => 20/5 = 4 + 'mix/v1/order/detail': 2, + 'mix/v1/order/fills': 2, + 'mix/v1/order/allFills': 2, + 'mix/v1/plan/currentPlan': 1, # 20 times/1s(UID) => 20/20 = 1 + 'mix/v1/plan/historyPlan': 2, + 'mix/v1/trace/currentTrack': 2, + 'mix/v1/trace/followerOrder': 2, + 'mix/v1/trace/followerHistoryOrders': 2, + 'mix/v1/trace/historyTrack': 2, + 'mix/v1/trace/summary': 1, # 20 times/1s(UID) => 20/20 = 1 + 'mix/v1/trace/profitSettleTokenIdGroup': 1, # 20 times/1s(UID) => 20/20 = 1 + 'mix/v1/trace/profitDateGroupList': 1, # 20 times/1s(UID) => 20/20 = 1 + 'mix/v1/trade/profitDateList': 2, + 'mix/v1/trace/waitProfitDateList': 1, # 20 times/1s(UID) => 20/20 = 1 + 'mix/v1/trace/traderSymbols': 1, # 20 times/1s(UID) => 20/20 = 1 + 'mix/v1/trace/traderList': 2, + 'mix/v1/trace/traderDetail': 2, # 10 times/1s(UID) => 20/10 = 2 + 'mix/v1/trace/queryTraceConfig': 2, + 'v2/mix/account/account': 2, + 'v2/mix/account/accounts': 2, + 'v2/mix/account/sub-account-assets': 200, + 'v2/mix/account/open-count': 2, + 'v2/mix/account/bill': 2, + 'v2/mix/market/query-position-lever': 2, + 'v2/mix/position/single-position': 2, + 'v2/mix/position/all-position': 4, + 'v2/mix/position/history-position': 1, + 'v2/mix/order/detail': 2, + 'v2/mix/order/fills': 2, + 'v2/mix/order/fill-history': 2, + 'v2/mix/order/orders-pending': 2, + 'v2/mix/order/orders-history': 2, + 'v2/mix/order/orders-plan-pending': 2, + 'v2/mix/order/orders-plan-history': 2, + 'v2/mix/market/position-long-short': 20, + }, + 'post': { + 'mix/v1/account/sub-account-contract-assets': 200, # 0.1 times/1s(UID) => 20/0.1 = 200 + 'mix/v1/account/open-count': 1, + 'mix/v1/account/setLeverage': 4, # 5 times/1s(UID) => 20/5 = 4 + 'mix/v1/account/setMargin': 4, # 5 times/1s(UID) => 20/5 = 4 + 'mix/v1/account/setMarginMode': 4, # 5 times/1s(UID) => 20/5 = 4 + 'mix/v1/account/setPositionMode': 4, # 5 times/1s(UID) => 20/5 = 4 + 'mix/v1/order/placeOrder': 2, + 'mix/v1/order/batch-orders': 2, + 'mix/v1/order/cancel-order': 2, + 'mix/v1/order/cancel-batch-orders': 2, + 'mix/v1/order/modifyOrder': 2, # 10 times/1s(UID) => 20/10 = 2 + 'mix/v1/order/cancel-symbol-orders': 2, + 'mix/v1/order/cancel-all-orders': 2, + 'mix/v1/order/close-all-positions': 20, + 'mix/v1/plan/placePlan': 2, + 'mix/v1/plan/modifyPlan': 2, + 'mix/v1/plan/modifyPlanPreset': 2, + 'mix/v1/plan/placeTPSL': 2, + 'mix/v1/plan/placeTrailStop': 2, + 'mix/v1/plan/placePositionsTPSL': 2, + 'mix/v1/plan/modifyTPSLPlan': 2, + 'mix/v1/plan/cancelPlan': 2, + 'mix/v1/plan/cancelSymbolPlan': 2, + 'mix/v1/plan/cancelAllPlan': 2, + 'mix/v1/trace/closeTrackOrder': 2, + 'mix/v1/trace/modifyTPSL': 2, # 10 times/1s(UID) => 20/10 = 2 + 'mix/v1/trace/closeTrackOrderBySymbol': 2, + 'mix/v1/trace/setUpCopySymbols': 2, + 'mix/v1/trace/followerSetBatchTraceConfig': 2, + 'mix/v1/trace/followerCloseByTrackingNo': 2, + 'mix/v1/trace/followerCloseByAll': 2, + 'mix/v1/trace/followerSetTpsl': 2, + 'mix/v1/trace/cancelCopyTrader': 4, # 5 times/1s(UID) => 20/5 = 4 + 'mix/v1/trace/traderUpdateConfig': 2, # 10 times/1s(UID) => 20/10 = 2 + 'mix/v1/trace/myTraderList': 2, # 10 times/1s(UID) => 20/10 = 2 + 'mix/v1/trace/myFollowerList': 2, # 10 times/1s(UID) => 20/10 = 2 + 'mix/v1/trace/removeFollower': 2, # 10 times/1s(UID) => 20/10 = 2 + 'mix/v1/trace/public/getFollowerConfig': 2, # 10 times/1s(UID) => 20/10 = 2 + 'mix/v1/trace/report/order/historyList': 2, # 10 times/1s(IP) => 20/10 = 2 + 'mix/v1/trace/report/order/currentList': 2, # 10 times/1s(IP) => 20/10 = 2 + 'mix/v1/trace/queryTraderTpslRatioConfig': 2, # 10 times/1s(UID) => 20/10 = 2 + 'mix/v1/trace/traderUpdateTpslRatioConfig': 2, # 10 times/1s(UID) => 20/10 = 2 + 'v2/mix/account/set-leverage': 4, + 'v2/mix/account/set-margin': 4, + 'v2/mix/account/set-margin-mode': 4, + 'v2/mix/account/set-position-mode': 4, + 'v2/mix/order/place-order': 2, + 'v2/mix/order/click-backhand': 20, + 'v2/mix/order/batch-place-order': 20, + 'v2/mix/order/modify-order': 2, + 'v2/mix/order/cancel-order': 2, + 'v2/mix/order/batch-cancel-orders': 2, + 'v2/mix/order/close-positions': 20, + 'v2/mix/order/place-tpsl-order': 2, + 'v2/mix/order/place-plan-order': 2, + 'v2/mix/order/modify-tpsl-order': 2, + 'v2/mix/order/modify-plan-order': 2, + 'v2/mix/order/cancel-plan-order': 2, + }, + }, + 'user': { + 'get': { + 'user/v1/fee/query': 2, + 'user/v1/sub/virtual-list': 2, + 'user/v1/sub/virtual-api-list': 2, + 'user/v1/tax/spot-record': 1, + 'user/v1/tax/future-record': 1, + 'user/v1/tax/margin-record': 1, + 'user/v1/tax/p2p-record': 1, + 'v2/user/virtual-subaccount-list': 2, + 'v2/user/virtual-subaccount-apikey-list': 2, + }, + 'post': { + 'user/v1/sub/virtual-create': 4, + 'user/v1/sub/virtual-modify': 4, + 'user/v1/sub/virtual-api-batch-create': 20, # 1 times/1s(UID) => 20/1 = 20 + 'user/v1/sub/virtual-api-create': 4, + 'user/v1/sub/virtual-api-modify': 4, + 'v2/user/create-virtual-subaccount': 4, + 'v2/user/modify-virtual-subaccount': 4, + 'v2/user/batch-create-subaccount-and-apikey': 20, + 'v2/user/create-virtual-subaccount-apikey': 4, + 'v2/user/modify-virtual-subaccount-apikey': 4, + }, + }, + 'p2p': { + 'get': { + 'p2p/v1/merchant/merchantList': 2, # 10 times/1s(UID) => 20/10 = 2 + 'p2p/v1/merchant/merchantInfo': 2, # 10 times/1s(UID) => 20/10 = 2 + 'p2p/v1/merchant/advList': 2, # 10 times/1s(UID) => 20/10 = 2 + 'p2p/v1/merchant/orderList': 2, # 10 times/1s(UID) => 20/10 = 2 + 'v2/p2p/merchantList': 2, + 'v2/p2p/merchantInfo': 2, + 'v2/p2p/orderList': 2, + 'v2/p2p/advList': 2, + }, + }, + 'broker': { + 'get': { + 'broker/v1/account/info': 2, # 10 times/1s(UID) => 20/10 = 2 + 'broker/v1/account/sub-list': 20, # 1 times/1s(UID) => 20/1 = 20 + 'broker/v1/account/sub-email': 20, # 1 times/1s(UID) => 20/1 = 20 + 'broker/v1/account/sub-spot-assets': 2, # 10 times/1s(UID) => 20/10 = 2 + 'broker/v1/account/sub-future-assets': 2, # 10 times/1s(UID) => 20/10 = 2 + 'broker/v1/account/subaccount-transfer': 1, # unknown + 'broker/v1/account/subaccount-deposit': 1, # unknown + 'broker/v1/account/subaccount-withdrawal': 1, # unknown + 'broker/v1/account/sub-api-list': 2, # 10 times/1s(UID) => 20/10 = 2 + 'v2/broker/account/info': 2, + 'v2/broker/account/subaccount-list': 20, + 'v2/broker/account/subaccount-email': 2, + 'v2/broker/account/subaccount-spot-assets': 2, + 'v2/broker/account/subaccount-future-assets': 2, + 'v2/broker/manage/subaccount-apikey-list': 2, + }, + 'post': { + 'broker/v1/account/sub-create': 20, # 1 times/1s(UID) => 20/1 = 20 + 'broker/v1/account/sub-modify': 20, # 1 times/1s(UID) => 20/1 = 20 + 'broker/v1/account/sub-modify-email': 20, # 1 times/1s(UID) => 20/1 = 20 + 'broker/v1/account/sub-address': 2, # 10 times/1s(UID) => 20/10 = 2 + 'broker/v1/account/sub-withdrawal': 2, # 10 times/1s(UID) => 20/10 = 2 + 'broker/v1/account/sub-auto-transfer': 4, # 5 times/1s(UID) => 20/5 = 4 + 'broker/v1/account/sub-api-create': 2, # 10 times/1s(UID) => 20/10 = 2 + 'broker/v1/account/sub-api-modify': 2, # 10 times/1s(UID) => 20/10 = 2 + 'v2/broker/account/modify-subaccount-email': 2, + 'v2/broker/account/create-subaccount': 20, + 'v2/broker/account/modify-subaccount': 20, + 'v2/broker/account/subaccount-address': 2, + 'v2/broker/account/subaccount-withdrawal': 2, + 'v2/broker/account/set-subaccount-autotransfer': 2, + 'v2/broker/manage/create-subaccount-apikey': 2, + 'v2/broker/manage/modify-subaccount-apikey': 2, + }, + }, + 'margin': { + 'get': { + 'margin/v1/cross/account/riskRate': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/cross/account/maxTransferOutAmount': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/isolated/account/maxTransferOutAmount': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/isolated/order/openOrders': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/isolated/order/history': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/isolated/order/fills': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/isolated/loan/list': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/isolated/repay/list': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/isolated/interest/list': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/isolated/liquidation/list': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/isolated/fin/list': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/cross/order/openOrders': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/cross/order/history': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/cross/order/fills': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/cross/loan/list': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/cross/repay/list': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/cross/interest/list': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/cross/liquidation/list': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/cross/fin/list': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/cross/account/assets': 2, # 10 times/1s(IP) => 20/10 = 2 + 'margin/v1/isolated/account/assets': 2, # 10 times/1s(IP) => 20/10 = 2 + 'v2/margin/crossed/borrow-history': 2, + 'v2/margin/crossed/repay-history': 2, + 'v2/margin/crossed/interest-history': 2, + 'v2/margin/crossed/liquidation-history': 2, + 'v2/margin/crossed/financial-records': 2, + 'v2/margin/crossed/account/assets': 2, + 'v2/margin/crossed/account/risk-rate': 2, + 'v2/margin/crossed/account/max-borrowable-amount': 2, + 'v2/margin/crossed/account/max-transfer-out-amount': 2, + 'v2/margin/crossed/interest-rate-and-limit': 2, + 'v2/margin/crossed/tier-data': 2, + 'v2/margin/crossed/open-orders': 2, + 'v2/margin/crossed/history-orders': 2, + 'v2/margin/crossed/fills': 2, + 'v2/margin/isolated/borrow-history': 2, + 'v2/margin/isolated/repay-history': 2, + 'v2/margin/isolated/interest-history': 2, + 'v2/margin/isolated/liquidation-history': 2, + 'v2/margin/isolated/financial-records': 2, + 'v2/margin/isolated/account/assets': 2, + 'v2/margin/isolated/account/risk-rate': 2, + 'v2/margin/isolated/account/max-borrowable-amount': 2, + 'v2/margin/isolated/account/max-transfer-out-amount': 2, + 'v2/margin/isolated/interest-rate-and-limit': 2, + 'v2/margin/isolated/tier-data': 2, + 'v2/margin/isolated/open-orders': 2, + 'v2/margin/isolated/history-orders': 2, + 'v2/margin/isolated/fills': 2, + }, + 'post': { + 'margin/v1/cross/account/borrow': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/isolated/account/borrow': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/cross/account/repay': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/isolated/account/repay': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/isolated/account/riskRate': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/cross/account/maxBorrowableAmount': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/isolated/account/maxBorrowableAmount': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/isolated/account/flashRepay': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/isolated/account/queryFlashRepayStatus': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/cross/account/flashRepay': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/cross/account/queryFlashRepayStatus': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/isolated/order/placeOrder': 4, # 5 times/1s(UID) => 20/5 = 4 + 'margin/v1/isolated/order/batchPlaceOrder': 4, # 5 times/1s(UID) => 20/5 = 4 + 'margin/v1/isolated/order/cancelOrder': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/isolated/order/batchCancelOrder': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/cross/order/placeOrder': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/cross/order/batchPlaceOrder': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/cross/order/cancelOrder': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/cross/order/batchCancelOrder': 2, # 10 times/1s(UID) => 20/10 = 2 + 'v2/margin/crossed/account/borrow': 2, + 'v2/margin/crossed/account/repay': 2, + 'v2/margin/crossed/account/flash-repay': 2, + 'v2/margin/crossed/account/query-flash-repay-status': 2, + 'v2/margin/crossed/place-order': 2, + 'v2/margin/crossed/batch-place-order': 2, + 'v2/margin/crossed/cancel-order': 2, + 'v2/margin/crossed/batch-cancel-order': 2, + 'v2/margin/isolated/account/borrow': 2, + 'v2/margin/isolated/account/repay': 2, + 'v2/margin/isolated/account/flash-repay': 2, + 'v2/margin/isolated/account/query-flash-repay-status': 2, + 'v2/margin/isolated/place-order': 2, + 'v2/margin/isolated/batch-place-order': 2, + 'v2/margin/isolated/cancel-order': 2, + 'v2/margin/isolated/batch-cancel-order': 2, + }, + }, + 'copy': { + 'get': { + 'v2/copy/mix-trader/order-current-track': 2, + 'v2/copy/mix-trader/order-history-track': 2, + 'v2/copy/mix-trader/order-total-detail': 2, + 'v2/copy/mix-trader/profit-history-summarys': 1, + 'v2/copy/mix-trader/profit-history-details': 1, + 'v2/copy/mix-trader/profit-details': 1, + 'v2/copy/mix-trader/profits-group-coin-date': 1, + 'v2/copy/mix-trader/config-query-symbols': 1, + 'v2/copy/mix-trader/config-query-followers': 2, + 'v2/copy/mix-follower/query-current-orders': 2, + 'v2/copy/mix-follower/query-history-orders': 1, + 'v2/copy/mix-follower/query-settings': 2, + 'v2/copy/mix-follower/query-traders': 2, + 'v2/copy/mix-follower/query-quantity-limit': 2, + 'v2/copy/mix-broker/query-traders': 2, + 'v2/copy/mix-broker/query-history-traces': 2, + 'v2/copy/mix-broker/query-current-traces': 2, + 'v2/copy/spot-trader/profit-summarys': 2, + 'v2/copy/spot-trader/profit-history-details': 2, + 'v2/copy/spot-trader/profit-details': 2, + 'v2/copy/spot-trader/order-total-detail': 2, + 'v2/copy/spot-trader/order-history-track': 2, + 'v2/copy/spot-trader/order-current-track': 2, + 'v2/copy/spot-trader/config-query-settings': 2, + 'v2/copy/spot-trader/config-query-followers': 2, + 'v2/copy/spot-follower/query-traders': 2, + 'v2/copy/spot-follower/query-trader-symbols': 2, + 'v2/copy/spot-follower/query-settings': 2, + 'v2/copy/spot-follower/query-history-orders': 2, + 'v2/copy/spot-follower/query-current-orders': 2, + }, + 'post': { + 'v2/copy/mix-trader/order-modify-tpsl': 2, + 'v2/copy/mix-trader/order-close-positions': 2, + 'v2/copy/mix-trader/config-setting-symbols': 2, + 'v2/copy/mix-trader/config-setting-base': 2, + 'v2/copy/mix-trader/config-remove-follower': 2, + 'v2/copy/mix-follower/setting-tpsl': 1, + 'v2/copy/mix-follower/settings': 2, + 'v2/copy/mix-follower/close-positions': 2, + 'v2/copy/mix-follower/cancel-trader': 4, + 'v2/copy/spot-trader/order-modify-tpsl': 2, + 'v2/copy/spot-trader/order-close-tracking': 2, + 'v2/copy/spot-trader/config-setting-symbols': 2, + 'v2/copy/spot-trader/config-remove-follower': 2, + 'v2/copy/spot-follower/stop-order': 2, + 'v2/copy/spot-follower/settings': 2, + 'v2/copy/spot-follower/setting-tpsl': 2, + 'v2/copy/spot-follower/order-close-tracking': 2, + 'v2/copy/spot-follower/cancel-trader': 2, + }, + }, + 'tax': { + 'get': { + 'v2/tax/spot-record': 20, + 'v2/tax/future-record': 20, + 'v2/tax/margin-record': 20, + 'v2/tax/p2p-record': 20, + }, + }, + 'convert': { + 'get': { + 'v2/convert/currencies': 2, + 'v2/convert/quoted-price': 2, + 'v2/convert/convert-record': 2, + 'v2/convert/bgb-convert-coin-list': 2, + 'v2/convert/bgb-convert-records': 2, + }, + 'post': { + 'v2/convert/trade': 2, + 'v2/convert/bgb-convert': 2, + }, + }, + 'earn': { + 'get': { + 'v2/earn/savings/product': 2, + 'v2/earn/savings/account': 2, + 'v2/earn/savings/assets': 2, + 'v2/earn/savings/records': 2, + 'v2/earn/savings/subscribe-info': 2, + 'v2/earn/savings/subscribe-result': 2, + 'v2/earn/savings/redeem-result': 2, + 'v2/earn/sharkfin/product': 2, + 'v2/earn/sharkfin/account': 2, + 'v2/earn/sharkfin/assets': 2, + 'v2/earn/sharkfin/records': 2, + 'v2/earn/sharkfin/subscribe-info': 2, + 'v2/earn/sharkfin/subscribe-result': 4, + 'v2/earn/loan/ongoing-orders': 2, + 'v2/earn/loan/repay-history': 2, + 'v2/earn/loan/revise-history': 2, + 'v2/earn/loan/borrow-history': 2, + 'v2/earn/loan/debts': 2, + 'v2/earn/loan/reduces': 2, + 'v2/earn/account/assets': 2, + }, + 'post': { + 'v2/earn/savings/subscribe': 2, + 'v2/earn/savings/redeem': 2, + 'v2/earn/sharkfin/subscribe': 2, + 'v2/earn/loan/borrow': 2, + 'v2/earn/loan/repay': 2, + 'v2/earn/loan/revise-pledge': 2, + }, + }, + 'common': { + 'get': { + 'v2/common/trade-rate': 2, + }, + }, + 'uta': { + 'get': { + 'v3/account/assets': 1, + 'v3/account/settings': 1, + 'v3/account/deposit-records': 2, + 'v3/account/financial-records': 1, + 'v3/account/repayable-coins': 2, + 'v3/account/payment-coins': 2, + 'v3/account/convert-records': 1, + 'v3/account/transferable-coins': 2, + 'v3/account/sub-transfer-record': 4, + 'v3/ins-loan/transfered': 6.6667, + 'v3/ins-loan/symbols': 6.6667, + 'v3/ins-loan/risk-unit': 6.6667, + 'v3/ins-loan/repaid-history': 6.6667, + 'v3/ins-loan/product-infos': 6.6667, + 'v3/ins-loan/loan-order': 6.6667, + 'v3/ins-loan/ltv-convert': 6.6667, + 'v3/ins-loan/ensure-coins-convert': 6.6667, + 'v3/position/current-position': 1, + 'v3/position/history-position': 1, + 'v3/trade/order-info': 1, + 'v3/trade/unfilled-orders': 1, + 'v3/trade/unfilled-strategy-orders': 1, + 'v3/trade/history-orders': 1, + 'v3/trade/history-strategy-orders': 1, + 'v3/trade/fills': 1, + 'v3/user/sub-list': 2, + 'v3/user/sub-api-list': 2, + }, + 'post': { + 'v3/account/set-leverage': 2, + 'v3/account/set-hold-mode': 2, + 'v3/account/repay': 4, + 'v3/account/transfer': 4, + 'v3/account/sub-transfer': 4, + 'v3/account/max-open-available': 4, + 'v3/ins-loan/bind-uid': 6.6667, + 'v3/trade/place-order': 2, + 'v3/trade/place-strategy-order': 2, + 'v3/trade/modify-order': 2, + 'v3/trade/modify-strategy-order': 2, + 'v3/trade/cancel-order': 2, + 'v3/trade/cancel-strategy-order': 2, + 'v3/trade/place-batch': 4, + 'v3/trade/batch-modify-order': 2, + 'v3/trade/cancel-batch': 4, + 'v3/trade/cancel-symbol-order': 4, + 'v3/trade/close-positions': 4, + 'v3/user/create-sub': 2, + 'v3/user/freeze-sub': 2, + 'v3/user/create-sub-api': 2, + 'v3/user/update-sub-api': 2, + 'v3/user/delete-sub-api': 2, + }, + }, + }, + }, + 'fees': { + 'spot': { + 'taker': self.parse_number('0.002'), + 'maker': self.parse_number('0.002'), + }, + 'swap': { + 'taker': self.parse_number('0.0006'), + 'maker': self.parse_number('0.0004'), + }, + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + 'password': True, + }, + 'exceptions': { + # http error codes + # 400 Bad Request — Invalid request format + # 401 Unauthorized — Invalid API Key + # 403 Forbidden — You do not have access to the requested resource + # 404 Not Found + # 500 Internal Server Error — We had a problem with our server + 'exact': { + '1': ExchangeError, # {"code": 1, "message": "System error"} + # undocumented + 'failure to get a peer from the ring-balancer': ExchangeNotAvailable, # {"message": "failure to get a peer from the ring-balancer"} + '4010': PermissionDenied, # {"code": 4010, "message": "For the security of your funds, withdrawals are not permitted within 24 hours after changing fund password / mobile number / Google Authenticator settings "} + # common + # '0': ExchangeError, # 200 successful,when the order placement / cancellation / operation is successful + '4001': ExchangeError, # no data received in 30s + '4002': ExchangeError, # Buffer full. cannot write data + '40020': BadRequest, # {"code":"40020","msg":"Parameter orderId error","requestTime":1754305078588,"data":null} + # -------------------------------------------------------- + '30001': AuthenticationError, # {"code": 30001, "message": 'request header "OK_ACCESS_KEY" cannot be blank'} + '30002': AuthenticationError, # {"code": 30002, "message": 'request header "OK_ACCESS_SIGN" cannot be blank'} + '30003': AuthenticationError, # {"code": 30003, "message": 'request header "OK_ACCESS_TIMESTAMP" cannot be blank'} + '30004': AuthenticationError, # {"code": 30004, "message": 'request header "OK_ACCESS_PASSPHRASE" cannot be blank'} + '30005': InvalidNonce, # {"code": 30005, "message": "invalid OK_ACCESS_TIMESTAMP"} + '30006': AuthenticationError, # {"code": 30006, "message": "invalid OK_ACCESS_KEY"} + '30007': BadRequest, # {"code": 30007, "message": 'invalid Content_Type, please use "application/json" format'} + '30008': RequestTimeout, # {"code": 30008, "message": "timestamp request expired"} + '30009': ExchangeError, # {"code": 30009, "message": "system error"} + '30010': AuthenticationError, # {"code": 30010, "message": "API validation failed"} + '30011': PermissionDenied, # {"code": 30011, "message": "invalid IP"} + '30012': AuthenticationError, # {"code": 30012, "message": "invalid authorization"} + '30013': AuthenticationError, # {"code": 30013, "message": "invalid sign"} + '30014': DDoSProtection, # {"code": 30014, "message": "request too frequent"} + '30015': AuthenticationError, # {"code": 30015, "message": 'request header "OK_ACCESS_PASSPHRASE" incorrect'} + '30016': ExchangeError, # {"code": 30015, "message": "you are using v1 apiKey, please use v1 endpoint. If you would like to use v3 endpoint, please subscribe to v3 apiKey"} + '30017': ExchangeError, # {"code": 30017, "message": "apikey's broker id does not match"} + '30018': ExchangeError, # {"code": 30018, "message": "apikey's domain does not match"} + '30019': ExchangeNotAvailable, # {"code": 30019, "message": "Api is offline or unavailable"} + '30020': BadRequest, # {"code": 30020, "message": "body cannot be blank"} + '30021': BadRequest, # {"code": 30021, "message": "Json data format error"}, {"code": 30021, "message": "json data format error"} + '30022': PermissionDenied, # {"code": 30022, "message": "Api has been frozen"} + '30023': BadRequest, # {"code": 30023, "message": "{0} parameter cannot be blank"} + '30024': BadSymbol, # {"code":30024,"message":"\"instrument_id\" is an invalid parameter"} + '30025': BadRequest, # {"code": 30025, "message": "{0} parameter category error"} + '30026': DDoSProtection, # {"code": 30026, "message": "requested too frequent"} + '30027': AuthenticationError, # {"code": 30027, "message": "login failure"} + '30028': PermissionDenied, # {"code": 30028, "message": "unauthorized execution"} + '30029': AccountSuspended, # {"code": 30029, "message": "account suspended"} + '30030': ExchangeError, # {"code": 30030, "message": "endpoint request failed. Please try again"} + '30031': BadRequest, # {"code": 30031, "message": "token does not exist"} + '30032': BadSymbol, # {"code": 30032, "message": "pair does not exist"} + '30033': BadRequest, # {"code": 30033, "message": "exchange domain does not exist"} + '30034': ExchangeError, # {"code": 30034, "message": "exchange ID does not exist"} + '30035': ExchangeError, # {"code": 30035, "message": "trading is not hasattr(self, supported) website"} + '30036': ExchangeError, # {"code": 30036, "message": "no relevant data"} + '30037': ExchangeNotAvailable, # {"code": 30037, "message": "endpoint is offline or unavailable"} + # '30038': AuthenticationError, # {"code": 30038, "message": "user does not exist"} + '30038': OnMaintenance, # {"client_oid":"","code":"30038","error_code":"30038","error_message":"Matching engine is being upgraded. Please try in about 1 minute.","message":"Matching engine is being upgraded. Please try in about 1 minute.","order_id":"-1","result":false} + # futures + '32001': AccountSuspended, # {"code": 32001, "message": "futures account suspended"} + '32002': PermissionDenied, # {"code": 32002, "message": "futures account does not exist"} + '32003': CancelPending, # {"code": 32003, "message": "canceling, please wait"} + '32004': ExchangeError, # {"code": 32004, "message": "you have no unfilled orders"} + '32005': InvalidOrder, # {"code": 32005, "message": "max order quantity"} + '32006': InvalidOrder, # {"code": 32006, "message": "the order price or trigger price exceeds USD 1 million"} + '32007': InvalidOrder, # {"code": 32007, "message": "leverage level must be the same for orders on the same side of the contract"} + '32008': InvalidOrder, # {"code": 32008, "message": "Max. positions to open(cross margin)"} + '32009': InvalidOrder, # {"code": 32009, "message": "Max. positions to open(fixed margin)"} + '32010': ExchangeError, # {"code": 32010, "message": "leverage cannot be changed with open positions"} + '32011': ExchangeError, # {"code": 32011, "message": "futures status error"} + '32012': ExchangeError, # {"code": 32012, "message": "futures order update error"} + '32013': ExchangeError, # {"code": 32013, "message": "token type is blank"} + '32014': ExchangeError, # {"code": 32014, "message": "your number of contracts closing is larger than the number of contracts available"} + '32015': ExchangeError, # {"code": 32015, "message": "margin ratio is lower than 100% before opening positions"} + '32016': ExchangeError, # {"code": 32016, "message": "margin ratio is lower than 100% after opening position"} + '32017': ExchangeError, # {"code": 32017, "message": "no BBO"} + '32018': ExchangeError, # {"code": 32018, "message": "the order quantity is less than 1, please try again"} + '32019': ExchangeError, # {"code": 32019, "message": "the order price deviates from the price of the previous minute by more than 3%"} + '32020': ExchangeError, # {"code": 32020, "message": "the price is not in the range of the price limit"} + '32021': ExchangeError, # {"code": 32021, "message": "leverage error"} + '32022': ExchangeError, # {"code": 32022, "message": "self function is not supported in your country or region according to the regulations"} + '32023': ExchangeError, # {"code": 32023, "message": "self account has outstanding loan"} + '32024': ExchangeError, # {"code": 32024, "message": "order cannot be placed during delivery"} + '32025': ExchangeError, # {"code": 32025, "message": "order cannot be placed during settlement"} + '32026': ExchangeError, # {"code": 32026, "message": "your account is restricted from opening positions"} + '32027': ExchangeError, # {"code": 32027, "message": "cancelled over 20 orders"} + '32028': AccountSuspended, # {"code": 32028, "message": "account is suspended and liquidated"} + '32029': ExchangeError, # {"code": 32029, "message": "order info does not exist"} + '32030': InvalidOrder, # The order cannot be cancelled + '32031': ArgumentsRequired, # client_oid or order_id is required. + '32038': AuthenticationError, # User does not exist + '32040': ExchangeError, # User have open contract orders or position + '32044': ExchangeError, # {"code": 32044, "message": "The margin ratio after submitting self order is lower than the minimum requirement({0}) for your tier."} + '32045': ExchangeError, # str of commission over 1 million + '32046': ExchangeError, # Each user can hold up to 10 trade plans at the same time + '32047': ExchangeError, # system error + '32048': InvalidOrder, # Order strategy track range error + '32049': ExchangeError, # Each user can hold up to 10 track plans at the same time + '32050': InvalidOrder, # Order strategy rang error + '32051': InvalidOrder, # Order strategy ice depth error + '32052': ExchangeError, # str of commission over 100 thousand + '32053': ExchangeError, # Each user can hold up to 6 ice plans at the same time + '32057': ExchangeError, # The order price is zero. Market-close-all function cannot be executed + '32054': ExchangeError, # Trade not allow + '32055': InvalidOrder, # cancel order error + '32056': ExchangeError, # iceberg per order average should between {0}-{1} contracts + '32058': ExchangeError, # Each user can hold up to 6 initiative plans at the same time + '32059': InvalidOrder, # Total amount should exceed per order amount + '32060': InvalidOrder, # Order strategy type error + '32061': InvalidOrder, # Order strategy initiative limit error + '32062': InvalidOrder, # Order strategy initiative range error + '32063': InvalidOrder, # Order strategy initiative rate error + '32064': ExchangeError, # Time Stringerval of orders should set between 5-120s + '32065': ExchangeError, # Close amount exceeds the limit of Market-close-all(999 for BTC, and 9999 for the rest tokens) + '32066': ExchangeError, # You have open orders. Please cancel all open orders before changing your leverage level. + '32067': ExchangeError, # Account equity < required hasattr(self, margin) setting. Please adjust your leverage level again. + '32068': ExchangeError, # The margin for self position will fall short of the required hasattr(self, margin) setting. Please adjust your leverage level or increase your margin to proceed. + '32069': ExchangeError, # Target leverage level too low. Your account balance is insufficient to cover the margin required. Please adjust the leverage level again. + '32070': ExchangeError, # Please check open position or unfilled order + '32071': ExchangeError, # Your current liquidation mode does not support self action. + '32072': ExchangeError, # The highest available margin for your order’s tier is {0}. Please edit your margin and place a new order. + '32073': ExchangeError, # The action does not apply to the token + '32074': ExchangeError, # The number of contracts of your position, open orders, and the current order has exceeded the maximum order limit of self asset. + '32075': ExchangeError, # Account risk rate breach + '32076': ExchangeError, # Liquidation of the holding position(s) at market price will require cancellation of all pending close orders of the contracts. + '32077': ExchangeError, # Your margin for self asset in futures account is insufficient and the position has been taken over for liquidation.(You will not be able to place orders, close positions, transfer funds, or add margin during self period of time. Your account will be restored after the liquidation is complete.) + '32078': ExchangeError, # Please cancel all open orders before switching the liquidation mode(Please cancel all open orders before switching the liquidation mode) + '32079': ExchangeError, # Your open positions are at high risk.(Please add margin or reduce positions before switching the mode) + '32080': ExchangeError, # Funds cannot be transferred out within 30 minutes after futures settlement + '32083': ExchangeError, # The number of contracts should be a positive multiple of %%. Please place your order again + # token and margin trading + '33001': PermissionDenied, # {"code": 33001, "message": "margin account for self pair is not enabled yet"} + '33002': AccountSuspended, # {"code": 33002, "message": "margin account for self pair is suspended"} + '33003': InsufficientFunds, # {"code": 33003, "message": "no loan balance"} + '33004': ExchangeError, # {"code": 33004, "message": "loan amount cannot be smaller than the minimum limit"} + '33005': ExchangeError, # {"code": 33005, "message": "repayment amount must exceed 0"} + '33006': ExchangeError, # {"code": 33006, "message": "loan order not found"} + '33007': ExchangeError, # {"code": 33007, "message": "status not found"} + '33008': InsufficientFunds, # {"code": 33008, "message": "loan amount cannot exceed the maximum limit"} + '33009': ExchangeError, # {"code": 33009, "message": "user ID is blank"} + '33010': ExchangeError, # {"code": 33010, "message": "you cannot cancel an order during session 2 of call auction"} + '33011': ExchangeError, # {"code": 33011, "message": "no new market data"} + '33012': ExchangeError, # {"code": 33012, "message": "order cancellation failed"} + '33013': InvalidOrder, # {"code": 33013, "message": "order placement failed"} + '33014': OrderNotFound, # {"code": 33014, "message": "order does not exist"} + '33015': InvalidOrder, # {"code": 33015, "message": "exceeded maximum limit"} + '33016': ExchangeError, # {"code": 33016, "message": "margin trading is not open for self token"} + '33017': InsufficientFunds, # {"code": 33017, "message": "insufficient balance"} + '33018': ExchangeError, # {"code": 33018, "message": "self parameter must be smaller than 1"} + '33020': ExchangeError, # {"code": 33020, "message": "request not supported"} + '33021': BadRequest, # {"code": 33021, "message": "token and the pair do not match"} + '33022': InvalidOrder, # {"code": 33022, "message": "pair and the order do not match"} + '33023': ExchangeError, # {"code": 33023, "message": "you can only place market orders during call auction"} + '33024': InvalidOrder, # {"code": 33024, "message": "trading amount too small"} + '33025': InvalidOrder, # {"code": 33025, "message": "base token amount is blank"} + '33026': ExchangeError, # {"code": 33026, "message": "transaction completed"} + '33027': InvalidOrder, # {"code": 33027, "message": "cancelled order or order cancelling"} + '33028': InvalidOrder, # {"code": 33028, "message": "the decimal places of the trading price exceeded the limit"} + '33029': InvalidOrder, # {"code": 33029, "message": "the decimal places of the trading size exceeded the limit"} + '33034': ExchangeError, # {"code": 33034, "message": "You can only place limit order after Call Auction has started"} + '33035': ExchangeError, # This type of order cannot be canceled(This type of order cannot be canceled) + '33036': ExchangeError, # Exceeding the limit of entrust order + '33037': ExchangeError, # The buy order price should be lower than 130% of the trigger price + '33038': ExchangeError, # The sell order price should be higher than 70% of the trigger price + '33039': ExchangeError, # The limit of callback rate is 0 < x <= 5% + '33040': ExchangeError, # The trigger price of a buy order should be lower than the latest transaction price + '33041': ExchangeError, # The trigger price of a sell order should be higher than the latest transaction price + '33042': ExchangeError, # The limit of price variance is 0 < x <= 1% + '33043': ExchangeError, # The total amount must be larger than 0 + '33044': ExchangeError, # The average amount should be 1/1000 * total amount <= x <= total amount + '33045': ExchangeError, # The price should not be 0, including trigger price, order price, and price limit + '33046': ExchangeError, # Price variance should be 0 < x <= 1% + '33047': ExchangeError, # Sweep ratio should be 0 < x <= 100% + '33048': ExchangeError, # Per order limit: Total amount/1000 < x <= Total amount + '33049': ExchangeError, # Total amount should be X > 0 + '33050': ExchangeError, # Time interval should be 5 <= x <= 120s + '33051': ExchangeError, # cancel order number not higher limit: plan and track entrust no more than 10, ice and time entrust no more than 6 + '33059': BadRequest, # {"code": 33059, "message": "client_oid or order_id is required"} + '33060': BadRequest, # {"code": 33060, "message": "Only fill in either parameter client_oid or order_id"} + '33061': ExchangeError, # Value of a single market price order cannot exceed 100,000 USD + '33062': ExchangeError, # The leverage ratio is too high. The borrowed position has exceeded the maximum position of self leverage ratio. Please readjust the leverage ratio + '33063': ExchangeError, # Leverage multiple is too low, there is insufficient margin in the account, please readjust the leverage ratio + '33064': ExchangeError, # The setting of the leverage ratio cannot be less than 2, please readjust the leverage ratio + '33065': ExchangeError, # Leverage ratio exceeds maximum leverage ratio, please readjust leverage ratio + # account + '21009': ExchangeError, # Funds cannot be transferred out within 30 minutes after swap settlement(Funds cannot be transferred out within 30 minutes after swap settlement) + '34001': PermissionDenied, # {"code": 34001, "message": "withdrawal suspended"} + '34002': InvalidAddress, # {"code": 34002, "message": "please add a withdrawal address"} + '34003': ExchangeError, # {"code": 34003, "message": "sorry, self token cannot be withdrawn to xx at the moment"} + '34004': ExchangeError, # {"code": 34004, "message": "withdrawal fee is smaller than minimum limit"} + '34005': ExchangeError, # {"code": 34005, "message": "withdrawal fee exceeds the maximum limit"} + '34006': ExchangeError, # {"code": 34006, "message": "withdrawal amount is lower than the minimum limit"} + '34007': ExchangeError, # {"code": 34007, "message": "withdrawal amount exceeds the maximum limit"} + '34008': InsufficientFunds, # {"code": 34008, "message": "insufficient balance"} + '34009': ExchangeError, # {"code": 34009, "message": "your withdrawal amount exceeds the daily limit"} + '34010': ExchangeError, # {"code": 34010, "message": "transfer amount must be larger than 0"} + '34011': ExchangeError, # {"code": 34011, "message": "conditions not met"} + '34012': ExchangeError, # {"code": 34012, "message": "the minimum withdrawal amount for NEO is 1, and the amount must be an integer"} + '34013': ExchangeError, # {"code": 34013, "message": "please transfer"} + '34014': ExchangeError, # {"code": 34014, "message": "transfer limited"} + '34015': ExchangeError, # {"code": 34015, "message": "subaccount does not exist"} + '34016': PermissionDenied, # {"code": 34016, "message": "transfer suspended"} + '34017': AccountSuspended, # {"code": 34017, "message": "account suspended"} + '34018': AuthenticationError, # {"code": 34018, "message": "incorrect trades password"} + '34019': PermissionDenied, # {"code": 34019, "message": "please bind your email before withdrawal"} + '34020': PermissionDenied, # {"code": 34020, "message": "please bind your funds password before withdrawal"} + '34021': InvalidAddress, # {"code": 34021, "message": "Not verified address"} + '34022': ExchangeError, # {"code": 34022, "message": "Withdrawals are not available for sub accounts"} + '34023': PermissionDenied, # {"code": 34023, "message": "Please enable futures trading before transferring your funds"} + '34026': ExchangeError, # transfer too frequently(transfer too frequently) + '34036': ExchangeError, # Parameter is incorrect, please refer to API documentation + '34037': ExchangeError, # Get the sub-account balance interface, account type is not supported + '34038': ExchangeError, # Since your C2C transaction is unusual, you are restricted from fund transfer. Please contact our customer support to cancel the restriction + '34039': ExchangeError, # You are now restricted from transferring out your funds due to abnormal trades on C2C Market. Please transfer your fund on our website or app instead to verify your identity + # swap + '35001': ExchangeError, # {"code": 35001, "message": "Contract does not exist"} + '35002': ExchangeError, # {"code": 35002, "message": "Contract settling"} + '35003': ExchangeError, # {"code": 35003, "message": "Contract paused"} + '35004': ExchangeError, # {"code": 35004, "message": "Contract pending settlement"} + '35005': AuthenticationError, # {"code": 35005, "message": "User does not exist"} + '35008': InvalidOrder, # {"code": 35008, "message": "Risk ratio too high"} + '35010': InvalidOrder, # {"code": 35010, "message": "Position closing too large"} + '35012': InvalidOrder, # {"code": 35012, "message": "Incorrect order size"} + '35014': InvalidOrder, # {"code": 35014, "message": "Order price is not within limit"} + '35015': InvalidOrder, # {"code": 35015, "message": "Invalid leverage level"} + '35017': ExchangeError, # {"code": 35017, "message": "Open orders exist"} + '35019': InvalidOrder, # {"code": 35019, "message": "Order size too large"} + '35020': InvalidOrder, # {"code": 35020, "message": "Order price too high"} + '35021': InvalidOrder, # {"code": 35021, "message": "Order size exceeded current tier limit"} + '35022': ExchangeError, # {"code": 35022, "message": "Contract status error"} + '35024': ExchangeError, # {"code": 35024, "message": "Contract not initialized"} + '35025': InsufficientFunds, # {"code": 35025, "message": "No account balance"} + '35026': ExchangeError, # {"code": 35026, "message": "Contract settings not initialized"} + '35029': OrderNotFound, # {"code": 35029, "message": "Order does not exist"} + '35030': InvalidOrder, # {"code": 35030, "message": "Order size too large"} + '35031': InvalidOrder, # {"code": 35031, "message": "Cancel order size too large"} + '35032': ExchangeError, # {"code": 35032, "message": "Invalid user status"} + '35037': ExchangeError, # No last traded price in cache + '35039': ExchangeError, # {"code": 35039, "message": "Open order quantity exceeds limit"} + '35040': InvalidOrder, # {"error_message":"Invalid order type","result":"true","error_code":"35040","order_id":"-1"} + '35044': ExchangeError, # {"code": 35044, "message": "Invalid order status"} + '35046': InsufficientFunds, # {"code": 35046, "message": "Negative account balance"} + '35047': InsufficientFunds, # {"code": 35047, "message": "Insufficient account balance"} + '35048': ExchangeError, # {"code": 35048, "message": "User contract is frozen and liquidating"} + '35049': InvalidOrder, # {"code": 35049, "message": "Invalid order type"} + '35050': InvalidOrder, # {"code": 35050, "message": "Position settings are blank"} + '35052': InsufficientFunds, # {"code": 35052, "message": "Insufficient cross margin"} + '35053': ExchangeError, # {"code": 35053, "message": "Account risk too high"} + '35055': InsufficientFunds, # {"code": 35055, "message": "Insufficient account balance"} + '35057': ExchangeError, # {"code": 35057, "message": "No last traded price"} + '35058': ExchangeError, # {"code": 35058, "message": "No limit"} + '35059': BadRequest, # {"code": 35059, "message": "client_oid or order_id is required"} + '35060': BadRequest, # {"code": 35060, "message": "Only fill in either parameter client_oid or order_id"} + '35061': BadRequest, # {"code": 35061, "message": "Invalid instrument_id"} + '35062': InvalidOrder, # {"code": 35062, "message": "Invalid match_price"} + '35063': InvalidOrder, # {"code": 35063, "message": "Invalid order_size"} + '35064': InvalidOrder, # {"code": 35064, "message": "Invalid client_oid"} + '35066': InvalidOrder, # Order interval error + '35067': InvalidOrder, # Time-weighted order ratio error + '35068': InvalidOrder, # Time-weighted order range error + '35069': InvalidOrder, # Time-weighted single transaction limit error + '35070': InvalidOrder, # Algo order type error + '35071': InvalidOrder, # Order total must be larger than single order limit + '35072': InvalidOrder, # Maximum 6 unfulfilled time-weighted orders can be held at the same time + '35073': InvalidOrder, # Order price is 0. Market-close-all not available + '35074': InvalidOrder, # Iceberg order single transaction average error + '35075': InvalidOrder, # Failed to cancel order + '35076': InvalidOrder, # LTC 20x leverage. Not allowed to open position + '35077': InvalidOrder, # Maximum 6 unfulfilled iceberg orders can be held at the same time + '35078': InvalidOrder, # Order amount exceeded 100,000 + '35079': InvalidOrder, # Iceberg order price variance error + '35080': InvalidOrder, # Callback rate error + '35081': InvalidOrder, # Maximum 10 unfulfilled trail orders can be held at the same time + '35082': InvalidOrder, # Trail order callback rate error + '35083': InvalidOrder, # Each user can only hold a maximum of 10 unfulfilled stop-limit orders at the same time + '35084': InvalidOrder, # Order amount exceeded 1 million + '35085': InvalidOrder, # Order amount is not in the correct range + '35086': InvalidOrder, # Price exceeds 100 thousand + '35087': InvalidOrder, # Price exceeds 100 thousand + '35088': InvalidOrder, # Average amount error + '35089': InvalidOrder, # Price exceeds 100 thousand + '35090': ExchangeError, # No stop-limit orders available for cancelation + '35091': ExchangeError, # No trail orders available for cancellation + '35092': ExchangeError, # No iceberg orders available for cancellation + '35093': ExchangeError, # No trail orders available for cancellation + '35094': ExchangeError, # Stop-limit order last traded price error + '35095': BadRequest, # Instrument_id error + '35096': ExchangeError, # Algo order status error + '35097': ExchangeError, # Order status and order ID cannot exist at the same time + '35098': ExchangeError, # An order status or order ID must exist + '35099': ExchangeError, # Algo order ID error + # option + '36001': BadRequest, # Invalid underlying index. + '36002': BadRequest, # Instrument does not exist. + '36005': ExchangeError, # Instrument status is invalid. + '36101': AuthenticationError, # Account does not exist. + '36102': PermissionDenied, # Account status is invalid. + '36103': AccountSuspended, # Account is suspended due to ongoing liquidation. + '36104': PermissionDenied, # Account is not enabled for options trading. + '36105': PermissionDenied, # Please enable the account for option contract. + '36106': AccountSuspended, # Funds cannot be transferred in or out, is suspended. + '36107': PermissionDenied, # Funds cannot be transferred out within 30 minutes after option exercising or settlement. + '36108': InsufficientFunds, # Funds cannot be transferred in or out, of the account is less than zero. + '36109': PermissionDenied, # Funds cannot be transferred in or out during option exercising or settlement. + '36201': PermissionDenied, # New order function is blocked. + '36202': PermissionDenied, # Account does not have permission to short option. + '36203': InvalidOrder, # Invalid format for client_oid. + '36204': ExchangeError, # Invalid format for request_id. + '36205': BadRequest, # Instrument id does not match underlying index. + '36206': BadRequest, # Order_id and client_oid can not be used at the same time. + '36207': InvalidOrder, # Either order price or fartouch price must be present. + '36208': InvalidOrder, # Either order price or size must be present. + '36209': InvalidOrder, # Either order_id or client_oid must be present. + '36210': InvalidOrder, # Either order_ids or client_oids must be present. + '36211': InvalidOrder, # Exceeding max batch size for order submission. + '36212': InvalidOrder, # Exceeding max batch size for oder cancellation. + '36213': InvalidOrder, # Exceeding max batch size for order amendment. + '36214': ExchangeError, # Instrument does not have valid bid/ask quote. + '36216': OrderNotFound, # Order does not exist. + '36217': InvalidOrder, # Order submission failed. + '36218': InvalidOrder, # Order cancellation failed. + '36219': InvalidOrder, # Order amendment failed. + '36220': InvalidOrder, # Order is pending cancel. + '36221': InvalidOrder, # Order qty is not valid multiple of lot size. + '36222': InvalidOrder, # Order price is breaching highest buy limit. + '36223': InvalidOrder, # Order price is breaching lowest sell limit. + '36224': InvalidOrder, # Exceeding max order size. + '36225': InvalidOrder, # Exceeding max open order count for instrument. + '36226': InvalidOrder, # Exceeding max open order count for underlying. + '36227': InvalidOrder, # Exceeding max open size across all orders for underlying + '36228': InvalidOrder, # Exceeding max available qty for instrument. + '36229': InvalidOrder, # Exceeding max available qty for underlying. + '36230': InvalidOrder, # Exceeding max position limit for underlying. + # -------------------------------------------------------- + # swap + '400': BadRequest, # Bad Request + '401': AuthenticationError, # Unauthorized access + '403': PermissionDenied, # Access prohibited + '404': BadRequest, # Request address does not exist + '405': BadRequest, # The HTTP Method is not supported + '415': BadRequest, # The current media type is not supported + '429': DDoSProtection, # Too many requests + '500': ExchangeNotAvailable, # System busy + '1001': RateLimitExceeded, # The request is too frequent and has been throttled + '1002': ExchangeError, # {0} verifications within 24 hours + '1003': ExchangeError, # You failed more than {0} times today, the current operation is locked, please try again in 24 hours + # '00000': ExchangeError, # success + '40001': AuthenticationError, # ACCESS_KEY cannot be empty + '40002': AuthenticationError, # SECRET_KEY cannot be empty + '40003': AuthenticationError, # Signature cannot be empty + '40004': InvalidNonce, # Request timestamp expired + '40005': InvalidNonce, # Invalid ACCESS_TIMESTAMP + '40006': AuthenticationError, # Invalid ACCESS_KEY + '40007': BadRequest, # Invalid Content_Type + '40008': InvalidNonce, # Request timestamp expired + '40009': AuthenticationError, # sign signature error + '40010': AuthenticationError, # sign signature error + '40011': AuthenticationError, # ACCESS_PASSPHRASE cannot be empty + '40012': AuthenticationError, # apikey/password is incorrect + '40013': ExchangeError, # User status is abnormal + '40014': PermissionDenied, # Incorrect permissions + '40015': ExchangeError, # System is abnormal, please try again later + '40016': PermissionDenied, # The user must bind the phone or Google + '40017': ExchangeError, # Parameter verification failed + '40018': PermissionDenied, # Invalid IP + '40019': BadRequest, # {"code":"40019","msg":"Parameter QLCUSDT_SPBL cannot be empty","requestTime":1679196063659,"data":null} + '40031': AccountSuspended, # The account has been cancelled and cannot be used again + '40037': AuthenticationError, # Apikey does not exist + '40102': BadRequest, # Contract configuration does not exist, please check the parameters + '40103': BadRequest, # Request method cannot be empty + '40104': ExchangeError, # Lever adjustment failure + '40105': ExchangeError, # Abnormal access to current price limit data + '40106': ExchangeError, # Abnormal get next settlement time + '40107': ExchangeError, # Abnormal access to index price data + '40108': InvalidOrder, # Wrong order quantity + '40109': OrderNotFound, # The data of the order cannot be found, please confirm the order number + '40200': OnMaintenance, # Server upgrade, please try again later + '40201': InvalidOrder, # Order number cannot be empty + '40202': ExchangeError, # User information cannot be empty + '40203': BadRequest, # The amount of adjustment margin cannot be empty or negative + '40204': BadRequest, # Adjustment margin type cannot be empty + '40205': BadRequest, # Adjusted margin type data is wrong + '40206': BadRequest, # The direction of the adjustment margin cannot be empty + '40207': BadRequest, # The adjustment margin data is wrong + '40208': BadRequest, # The accuracy of the adjustment margin amount is incorrect + '40209': BadRequest, # The current page number is wrong, please confirm + '40300': ExchangeError, # User does not exist + '40301': PermissionDenied, # Permission has not been obtained yet. If you need to use it, please contact customer service + '40302': BadRequest, # Parameter abnormality + '40303': BadRequest, # Can only query up to 20,000 data + '40304': BadRequest, # Parameter type is abnormal + '40305': BadRequest, # Client_oid length is not greater than 50, and cannot be Martian characters + '40306': ExchangeError, # Batch processing orders can only process up to 20 + '40308': OnMaintenance, # The contract is being temporarily maintained + '40309': BadSymbol, # The contract has been removed + '40400': ExchangeError, # Status check abnormal + '40401': ExchangeError, # The operation cannot be performed + '40402': BadRequest, # The opening direction cannot be empty + '40403': BadRequest, # Wrong opening direction format + '40404': BadRequest, # Whether to enable automatic margin call parameters cannot be empty + '40405': BadRequest, # Whether to enable the automatic margin call parameter type is wrong + '40406': BadRequest, # Whether to enable automatic margin call parameters is of unknown type + '40407': ExchangeError, # The query direction is not the direction entrusted by the plan + '40408': ExchangeError, # Wrong time range + '40409': ExchangeError, # Time format error + '40500': InvalidOrder, # Client_oid check error + '40501': ExchangeError, # Channel name error + '40502': ExchangeError, # If it is a copy user, you must pass the copy to whom + '40503': ExchangeError, # With the single type + '40504': ExchangeError, # Platform code must pass + '40505': ExchangeError, # Not the same type + '40506': AuthenticationError, # Platform signature error + '40507': AuthenticationError, # Api signature error + '40508': ExchangeError, # KOL is not authorized + '40509': ExchangeError, # Abnormal copy end + '40600': ExchangeError, # Copy function suspended + '40601': ExchangeError, # Followers cannot be KOL + '40602': ExchangeError, # The number of copies has reached the limit and cannot process the request + '40603': ExchangeError, # Abnormal copy end + '40604': ExchangeNotAvailable, # Server is busy, please try again later + '40605': ExchangeError, # Copy type, the copy number must be passed + '40606': ExchangeError, # The type of document number is wrong + '40607': ExchangeError, # Document number must be passed + '40608': ExchangeError, # No documented products currently supported + '40609': ExchangeError, # The contract product does not support copying + '40700': BadRequest, # Cursor parameters are incorrect + '40701': ExchangeError, # KOL is not authorized + '40702': ExchangeError, # Unauthorized copying user + '40703': ExchangeError, # Bill inquiry start and end time cannot be empty + '40704': ExchangeError, # Can only check the data of the last three months + '40705': BadRequest, # The start and end time cannot exceed 90 days + '40706': InvalidOrder, # Wrong order price + '40707': BadRequest, # Start time is greater than end time + '40708': BadRequest, # Parameter verification is abnormal + '40709': ExchangeError, # There is no hasattr(self, position) position, and no automatic margin call can be set + '40710': ExchangeError, # Abnormal account status + '40711': InsufficientFunds, # Insufficient contract account balance + '40712': InsufficientFunds, # Insufficient margin + '40713': ExchangeError, # Cannot exceed the maximum transferable margin amount + '40714': ExchangeError, # No direct margin call is allowed + '40762': InsufficientFunds, # {"code":"40762","msg":"The order amount exceeds the balance","requestTime":1716572156622,"data":null} + '40768': OrderNotFound, # Order does not exist + '40808': InvalidOrder, # {"code":"40808","msg":"Parameter verification exception size checkBDScale error value=2293.577 checkScale=2","requestTime":1725638500052,"data":null} + '41103': InvalidOrder, # {"code":"41103","msg":"param price scale error error","requestTime":1725635883561,"data":null} + '41114': OnMaintenance, # {"code":"41114","msg":"The current trading pair is under maintenance, please refer to the official announcement for the opening time","requestTime":1679196062544,"data":null} + '43011': InvalidOrder, # The parameter does not meet the specification executePrice <= 0 + '43001': OrderNotFound, + '43012': InsufficientFunds, # {"code":"43012","msg":"Insufficient balance","requestTime":1711648951774,"data":null} + '43025': InvalidOrder, # Plan order does not exist + '43115': OnMaintenance, # {"code":"43115","msg":"The current trading pair is opening soon, please refer to the official announcement for the opening time","requestTime":1688907202434,"data":null} + '45110': InvalidOrder, # {"code":"45110","msg":"less than the minimum amount 5 USDT","requestTime":1669911118932,"data":null} + '40774': InvalidOrder, # {"code":"40774","msg":"The order type for unilateral position must also be the unilateral position type.","requestTime":1758709764409,"data":null} + '45122': InvalidOrder, # {"code":"45122","msg":"Short position stop loss price please > mark price 106.86","requestTime":1758709970499,"data":null} + # spot + 'invalid sign': AuthenticationError, + 'invalid currency': BadSymbol, # invalid trading pair + 'invalid symbol': BadSymbol, + 'invalid period': BadRequest, # invalid Kline type + 'invalid user': ExchangeError, + 'invalid amount': InvalidOrder, + 'invalid type': InvalidOrder, # {"status":"error","ts":1595700344504,"err_code":"invalid-parameter","err_msg":"invalid type"} + 'invalid orderId': InvalidOrder, + 'invalid record': ExchangeError, + 'invalid accountId': BadRequest, + 'invalid address': BadRequest, + 'accesskey not None': AuthenticationError, # {"status":"error","ts":1595704360508,"err_code":"invalid-parameter","err_msg":"accesskey not null"} + 'illegal accesskey': AuthenticationError, + 'sign not null': AuthenticationError, + 'req_time is too much difference from server time': InvalidNonce, + 'permissions not right': PermissionDenied, # {"status":"error","ts":1595704490084,"err_code":"invalid-parameter","err_msg":"permissions not right"} + 'illegal sign invalid': AuthenticationError, # {"status":"error","ts":1595684716042,"err_code":"invalid-parameter","err_msg":"illegal sign invalid"} + 'user locked': AccountSuspended, + 'Request Frequency Is Too High': RateLimitExceeded, + 'more than a daily rate of cash': BadRequest, + 'more than the maximum daily withdrawal amount': BadRequest, + 'need to bind email or mobile': ExchangeError, + 'user forbid': PermissionDenied, + 'User Prohibited Cash Withdrawal': PermissionDenied, + 'Cash Withdrawal Is Less Than The Minimum Value': BadRequest, + 'Cash Withdrawal Is More Than The Maximum Value': BadRequest, + 'the account with in 24 hours ban coin': PermissionDenied, + 'order cancel fail': BadRequest, # {"status":"error","ts":1595703343035,"err_code":"bad-request","err_msg":"order cancel fail"} + 'base symbol error': BadSymbol, + 'base date error': ExchangeError, + 'api signature not valid': AuthenticationError, + 'gateway internal error': ExchangeError, + 'audit failed': ExchangeError, + 'order queryorder invalid': BadRequest, + 'market no need price': InvalidOrder, + 'limit need price': InvalidOrder, + 'userid not equal to account_id': ExchangeError, + 'your balance is low': InsufficientFunds, # {"status":"error","ts":1595594160149,"err_code":"invalid-parameter","err_msg":"invalid size, valid range: [1,2000]"} + 'address invalid cointype': ExchangeError, + 'system exception': ExchangeError, # {"status":"error","ts":1595711862763,"err_code":"system exception","err_msg":"system exception"} + '50003': ExchangeError, # No record + '50004': BadSymbol, # The transaction pair is currently not supported or has been suspended + '50006': PermissionDenied, # The account is forbidden to withdraw. If you have any questions, please contact customer service. + '50007': PermissionDenied, # The account is forbidden to withdraw within 24 hours. If you have any questions, please contact customer service. + '50008': RequestTimeout, # network timeout + '50009': RateLimitExceeded, # The operation is too frequent, please try again later + '50010': ExchangeError, # The account is abnormally frozen. If you have any questions, please contact customer service. + '50014': InvalidOrder, # The transaction amount under minimum limits + '50015': InvalidOrder, # The transaction amount exceed maximum limits + '50016': InvalidOrder, # The price can't be higher than the current price + '50017': InvalidOrder, # Price under minimum limits + '50018': InvalidOrder, # The price exceed maximum limits + '50019': InvalidOrder, # The amount under minimum limits + '50020': InsufficientFunds, # Insufficient balance + '50021': InvalidOrder, # Price is under minimum limits + '50026': InvalidOrder, # Market price parameter error + 'invalid order query time': ExchangeError, # start time is greater than end time; or the time interval between start time and end time is greater than 48 hours + 'invalid start time': BadRequest, # start time is a date 30 days ago; or start time is a date in the future + 'invalid end time': BadRequest, # end time is a date 30 days ago; or end time is a date in the future + '20003': ExchangeError, # operation failed, {"status":"error","ts":1595730308979,"err_code":"bad-request","err_msg":"20003"} + '01001': ExchangeError, # order failed, {"status":"fail","err_code":"01001","err_msg":"系统异常,请稍后重试"} + '43111': PermissionDenied, # {"code":"43111","msg":"参数错误 address not in address book","requestTime":1665394201164,"data":null} + }, + 'broad': { + 'invalid size, valid range': ExchangeError, + }, + }, + 'precisionMode': TICK_SIZE, + 'commonCurrencies': { + 'APX': 'AstroPepeX', + 'DEGEN': 'DegenReborn', + 'EVA': 'Evadore', # conflict with EverValue Coin + 'JADE': 'Jade Protocol', + 'OMNI': 'omni', # conflict with Omni Network + 'TONCOIN': 'TON', + }, + 'options': { + 'uta': False, + 'timeDifference': 0, # the difference between system clock and Binance clock + 'adjustForTimeDifference': False, # controls the adjustment logic upon instantiation + 'timeframes': { + 'spot': { + '1m': '1min', + '5m': '5min', + '3m': '3min', + '15m': '15min', + '30m': '30min', + '1h': '1h', + '4h': '4h', + '6h': '6Hutc', + '12h': '12Hutc', + '1d': '1Dutc', + '3d': '3Dutc', + '1w': '1Wutc', + '1M': '1Mutc', + }, + 'swap': { + '1m': '1m', + '3m': '3m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1H', + '2h': '2H', + '4h': '4H', + '6h': '6Hutc', + '12h': '12Hutc', + '1d': '1Dutc', + '3d': '3Dutc', + '1w': '1Wutc', + '1M': '1Mutc', + }, + 'uta': { + '1m': '1m', + '3m': '3m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1H', + '2h': '2H', + '4h': '4H', + '6h': '6H', + '12h': '12H', + '1d': '1D', + }, + }, + 'fetchMarkets': { + 'types': ['spot', 'swap'], # there is future markets but they use the same endpoints + }, + 'defaultType': 'spot', # 'spot', 'swap', 'future' + 'defaultSubType': 'linear', # 'linear', 'inverse' + 'createMarketBuyOrderRequiresPrice': True, + 'broker': 'p4sve', + 'withdraw': { + 'fillResponseFromRequest': True, + }, + 'fetchOHLCV': { + # ### Timeframe settings ### + # after testing, the below values are real ones, because the values provided by API DOCS are wrong + # so, start timestamp should be within these thresholds to be able to call "recent" candles endpoint + 'maxRecentDaysPerTimeframe': { + '1m': 30, + '3m': 30, + '5m': 30, + '15m': 30, + '30m': 30, + '1h': 60, + '2h': 120, + '4h': 240, + '6h': 360, + '12h': 720, + '1d': 1440, + '3d': 1440 * 3, + '1w': 1440 * 7, + '1M': 1440 * 30, + }, + 'spot': { + 'maxLimitPerTimeframe': { + '1d': 300, + '3d': 100, + '1w': 100, + '1M': 100, + }, + 'method': 'publicSpotGetV2SpotMarketCandles', # publicSpotGetV2SpotMarketCandles or publicSpotGetV2SpotMarketHistoryCandles + }, + 'swap': { + 'maxLimitPerTimeframe': { + '4h': 540, + '6h': 360, + '12h': 180, + '1d': 90, + '3d': 30, + '1w': 13, + '1M': 4, + }, + 'method': 'publicMixGetV2MixMarketCandles', # publicMixGetV2MixMarketCandles or publicMixGetV2MixMarketHistoryCandles or publicMixGetV2MixMarketHistoryIndexCandles or publicMixGetV2MixMarketHistoryMarkCandles + }, + }, + 'fetchTrades': { + 'spot': { + 'method': 'publicSpotGetV2SpotMarketFillsHistory', # or publicSpotGetV2SpotMarketFills + }, + 'swap': { + 'method': 'publicMixGetV2MixMarketFillsHistory', # or publicMixGetV2MixMarketFills + }, + }, + 'fetchFundingRate': { + 'method': 'publicMixGetV2MixMarketCurrentFundRate', # or publicMixGetV2MixMarketFundingTime + }, + 'accountsByType': { + 'spot': 'spot', + 'cross': 'crossed_margin', + 'isolated': 'isolated_margin', + 'swap': 'usdt_futures', + 'usdc_swap': 'usdc_futures', + 'future': 'coin_futures', + 'p2p': 'p2p', + }, + 'accountsById': { + 'spot': 'spot', + 'crossed_margin': 'cross', + 'isolated_margin': 'isolated', + 'usdt_futures': 'swap', + 'usdc_futures': 'usdc_swap', + 'coin_futures': 'future', + 'p2p': 'p2p', + }, + 'sandboxMode': False, + 'networks': { + # 'TRX': 'TRX', # different code for mainnet + 'TRC20': 'TRC20', + # 'ETH': 'ETH', # different code for mainnet + 'ERC20': 'ERC20', + 'BEP20': 'BSC', + # 'BEP20': 'BEP20', # different for BEP20 + 'ATOM': 'ATOM', + 'ACA': 'AcalaToken', + 'APT': 'Aptos', + 'ARBONE': 'ArbitrumOne', + 'ARBNOVA': 'ArbitrumNova', + 'AVAXC': 'C-Chain', + 'AVAXX': 'X-Chain', + 'AR': 'Arweave', + 'BCH': 'BCH', + 'BCHA': 'BCHA', + 'BITCI': 'BITCI', + 'BTC': 'BTC', + 'CELO': 'CELO', + 'CSPR': 'CSPR', + 'ADA': 'Cardano', + 'CHZ': 'ChilizChain', + 'CRC20': 'CronosChain', + 'DOGE': 'DOGE', + 'DOT': 'DOT', + 'EOS': 'EOS', + 'ETHF': 'ETHFAIR', + 'ETHW': 'ETHW', + 'ETC': 'ETC', + 'EGLD': 'Elrond', + 'FIL': 'FIL', + 'FIO': 'FIO', + 'FTM': 'Fantom', + 'HRC20': 'HECO', + 'ONE': 'Harmony', + 'HNT': 'Helium', + 'ICP': 'ICP', + 'IOTX': 'IoTeX', + 'KARDIA': 'KAI', + 'KAVA': 'KAVA', + 'KDA': 'KDA', + 'KLAY': 'Klaytn', + 'KSM': 'Kusama', + 'LAT': 'LAT', + 'LTC': 'LTC', + 'MINA': 'MINA', + 'MOVR': 'MOVR', + 'METIS': 'MetisToken', + 'GLMR': 'Moonbeam', + 'NEAR': 'NEARProtocol', + 'NULS': 'NULS', + 'OASYS': 'OASYS', + 'OASIS': 'ROSE', + 'OMNI': 'OMNI', + 'ONT': 'Ontology', + 'OPTIMISM': 'Optimism', + 'OSMO': 'Osmosis', + 'POKT': 'PocketNetwork', + 'MATIC': 'Polygon', + 'QTUM': 'QTUM', + 'REEF': 'REEF', + 'SOL': 'SOL', + 'SYS': 'SYS', # SyscoinNEVM is different + 'SXP': 'Solar', + 'XYM': 'Symbol', + 'TON': 'TON', + 'TT': 'TT', + 'TLOS': 'Telos', + 'THETA': 'ThetaToken', + 'VITE': 'VITE', + 'WAVES': 'WAVES', + 'WAX': 'WAXP', + 'WEMIX': 'WEMIXMainnet', + 'XDC': 'XDCNetworkXDC', + 'XRP': 'XRP', + 'FET': 'FETCH', + 'NEM': 'NEM', + 'REI': 'REINetwork', + 'ZIL': 'ZIL', + 'ABBC': 'ABBCCoin', + 'RSK': 'RSK', + 'AZERO': 'AZERO', + 'TRC10': 'TRC10', + 'JUNO': 'JUNO', + # undetected: USDSP, more info at https://www.bitget.com/v1/spot/public/coinChainList + # todo: uncomment below after unification + # 'TERRACLASSIC': 'Terra', # tbd, that network id is also assigned to TERRANEW network + # 'CUBENETWORK': 'CUBE', + # 'CADUCEUS': 'CMP', + # 'CONFLUX': 'CFX', # CFXeSpace is different + # 'CERE': 'CERE', + # 'CANTO': 'CANTO', + 'ZKSYNC': 'zkSyncEra', + 'STARKNET': 'Starknet', + 'VIC': 'VICTION', + }, + 'networksById': { + }, + 'fetchPositions': { + 'method': 'privateMixGetV2MixPositionAllPosition', # or privateMixGetV2MixPositionHistoryPosition + }, + 'defaultTimeInForce': 'GTC', # 'GTC' = Good To Cancel(default), 'IOC' = Immediate Or Cancel + # fiat currencies on deposit page + 'fiatCurrencies': ['EUR', 'VND', 'PLN', 'CZK', 'HUF', 'DKK', 'AUD', 'CAD', 'NOK', 'SEK', 'CHF', 'MXN', 'COP', 'ARS', 'GBP', 'BRL', 'UAH', 'ZAR'], + }, + 'features': { + 'spot': { + 'sandbox': True, + 'createOrder': { + 'marginMode': True, + 'triggerPrice': True, + 'triggerPriceType': { + 'last': True, + 'mark': True, + 'index': False, # not on spot + }, + 'triggerDirection': False, + 'stopLossPrice': True, # todo: not yet implemented in spot + 'takeProfitPrice': True, # todo: not yet implemented in spot + 'attachedStopLossTakeProfit': { + 'triggerPriceType': { + 'last': False, + 'mark': False, + 'index': False, + }, + 'price': True, + }, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'marketBuyRequiresPrice': True, + 'marketBuyByCost': True, + # exchange-supported features + # 'selfTradePrevention': True, + # 'twap': False, + # 'iceberg': False, + # 'oco': False, + }, + 'createOrders': { + 'max': 50, + }, + 'fetchMyTrades': { + 'marginMode': True, + 'limit': 100, + 'daysBack': None, + 'untilDays': 90, + 'symbolRequired': True, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOpenOrders': { + 'marginMode': True, + 'limit': 100, + 'trigger': True, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': { + 'marginMode': True, + 'limit': 100, + 'daysBack': None, + 'daysBackCanceled': None, + 'untilDays': 90, + 'trigger': True, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 200, # variable timespans for recent endpoint, 200 for historical + }, + }, + 'forPerps': { + 'extends': 'spot', + 'createOrder': { + 'triggerPrice': True, + 'triggerPriceType': { + 'last': True, + 'mark': True, + 'index': False, # not on spot + }, + 'triggerDirection': False, + 'stopLossPrice': True, + 'takeProfitPrice': True, + 'attachedStopLossTakeProfit': { + 'triggerPriceType': { + 'last': True, + 'mark': True, + 'index': True, + }, + 'price': False, + }, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': True, + 'trailing': True, + 'marketBuyRequiresPrice': False, + 'marketBuyByCost': False, + # exchange-supported features + # 'selfTradePrevention': True, + # 'trailing': True, + # 'twap': False, + # 'iceberg': False, + # 'oco': False, + }, + 'fetchMyTrades': { + 'untilDays': 7, + }, + 'fetchClosedOrders': { + 'trailing': True, + }, + }, + 'swap': { + 'linear': { + 'extends': 'forPerps', + }, + 'inverse': { + 'extends': 'forPerps', + }, + }, + 'future': { + 'linear': { + 'extends': 'forPerps', + }, + 'inverse': { + 'extends': 'forPerps', + }, + }, + }, + }) + + def set_sandbox_mode(self, enabled: bool): + """ + enables or disables demo trading mode, if enabled will send PAPTRADING=1 in headers + @param enabled + """ + self.options['sandboxMode'] = enabled + + def enable_demo_trading(self, enabled: bool): + """ + enables or disables demo trading mode, if enabled will send PAPTRADING=1 in headers + @param enabled + """ + self.set_sandbox_mode(enabled) + + def handle_product_type_and_params(self, market=None, params={}): + subType = None + subType, params = self.handle_sub_type_and_params('handleProductTypeAndParams', None, params) + defaultProductType = None + if (subType is not None) and (market is None): + # set default only if subType is defined and market is not defined, since there is also USDC productTypes which are also linear + # sandboxMode = self.safe_bool(self.options, 'sandboxMode', False) + # if sandboxMode: + # defaultProductType = 'SUSDT-FUTURES' if (subType == 'linear') else 'SCOIN-FUTURES' + # else: + defaultProductType = 'USDT-FUTURES' if (subType == 'linear') else 'COIN-FUTURES' + # } + productType = self.safe_string_2(params, 'productType', 'category', defaultProductType) + if (productType is None) and (market is not None): + settle = market['settle'] + if market['spot']: + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('handleProductTypeAndParams', params) + if marginMode is not None: + productType = 'MARGIN' + else: + productType = 'SPOT' + elif settle == 'USDT': + productType = 'USDT-FUTURES' + elif settle == 'USDC': + productType = 'USDC-FUTURES' + elif settle == 'SUSDT': + productType = 'SUSDT-FUTURES' + elif settle == 'SUSDC': + productType = 'SUSDC-FUTURES' + elif (settle == 'SBTC') or (settle == 'SETH') or (settle == 'SEOS'): + productType = 'SCOIN-FUTURES' + else: + productType = 'COIN-FUTURES' + if productType is None: + raise ArgumentsRequired(self.id + ' requires a productType param, one of "USDT-FUTURES", "USDC-FUTURES", "COIN-FUTURES", "SUSDT-FUTURES", "SUSDC-FUTURES", "SCOIN-FUTURES" or for uta only "SPOT"') + params = self.omit(params, ['productType', 'category']) + return [productType, params] + + async def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://www.bitget.com/api-doc/common/public/Get-Server-Time + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = await self.publicCommonGetV2PublicTime(params) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700111073740, + # "data": { + # "serverTime": "1700111073740" + # } + # } + # + data = self.safe_value(response, 'data', {}) + return self.safe_integer(data, 'serverTime') + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for bitget + + https://www.bitget.com/api-doc/spot/market/Get-Symbols + https://www.bitget.com/api-doc/contract/market/Get-All-Symbols-Contracts + https://www.bitget.com/api-doc/margin/common/support-currencies + https://www.bitget.com/api-doc/uta/public/Instruments + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :returns dict[]: an array of objects representing market data + """ + if self.options['adjustForTimeDifference']: + await self.load_time_difference() + uta = None + uta, params = self.handle_option_and_params(params, 'fetchMarkets', 'uta', False) + if uta: + return await self.fetch_uta_markets(params) + else: + return await self.fetch_default_markets(params) + + async def fetch_default_markets(self, params) -> List[Market]: + types = None + fetchMarketsOptions = self.safe_dict(self.options, 'fetchMarkets') + defaultMarkets = ['spot', 'swap'] + if fetchMarketsOptions is not None: + types = self.safe_list(fetchMarketsOptions, 'types', defaultMarkets) + else: + # for backward-compatibility + types = self.safe_list(self.options, 'fetchMarkets', defaultMarkets) + promises = [] + fetchMargins = False + for i in range(0, len(types)): + type = types[i] + if (type == 'swap') or (type == 'future'): + subTypes = ['USDT-FUTURES', 'COIN-FUTURES', 'USDC-FUTURES', 'SUSDT-FUTURES', 'SCOIN-FUTURES', 'SUSDC-FUTURES'] + for j in range(0, len(subTypes)): + promises.append(self.publicMixGetV2MixMarketContracts(self.extend(params, { + 'productType': subTypes[j], + }))) + elif type == 'spot': + promises.append(self.publicSpotGetV2SpotPublicSymbols(params)) + fetchMargins = True + promises.append(self.publicMarginGetV2MarginCurrencies(params)) + else: + raise NotSupported(self.id + ' does not support ' + type + ' market') + results = await asyncio.gather(*promises) + markets = [] + self.options['crossMarginPairsData'] = [] + self.options['isolatedMarginPairsData'] = [] + for i in range(0, len(results)): + res = self.safe_dict(results, i) + data = self.safe_list(res, 'data', []) + firstData = self.safe_dict(data, 0, {}) + isBorrowable = self.safe_bool(firstData, 'isBorrowable') + if fetchMargins and isBorrowable is not None: + keysList = list(self.index_by(data, 'symbol').keys()) + self.options['crossMarginPairsData'] = keysList + self.options['isolatedMarginPairsData'] = keysList + else: + markets = self.array_concat(markets, data) + # + # spot + # + # { + # "symbol": "TRXUSDT", + # "baseCoin": "TRX", + # "quoteCoin": "USDT", + # "minTradeAmount": "0", + # "maxTradeAmount": "10000000000", + # "takerFeeRate": "0.002", + # "makerFeeRate": "0.002", + # "pricePrecision": "6", + # "quantityPrecision": "4", + # "quotePrecision": "6", + # "status": "online", + # "minTradeUSDT": "5", + # "buyLimitPriceRatio": "0.05", + # "sellLimitPriceRatio": "0.05" + # } + # + # swap and future + # + # { + # "symbol": "BTCUSDT", + # "baseCoin": "BTC", + # "quoteCoin": "USDT", + # "buyLimitPriceRatio": "0.01", + # "sellLimitPriceRatio": "0.01", + # "feeRateUpRatio": "0.005", + # "makerFeeRate": "0.0002", + # "takerFeeRate": "0.0006", + # "openCostUpRatio": "0.01", + # "supportMarginCoins": ["USDT"], + # "minTradeNum": "0.001", + # "priceEndStep": "1", + # "volumePlace": "3", + # "pricePlace": "1", + # "sizeMultiplier": "0.001", + # "symbolType": "perpetual", + # "minTradeUSDT": "5", + # "maxSymbolOrderNum": "200", + # "maxProductOrderNum": "400", + # "maxPositionNum": "150", + # "symbolStatus": "normal", + # "offTime": "-1", + # "limitOpenTime": "-1", + # "deliveryTime": "", + # "deliveryStartTime": "", + # "deliveryPeriod": "", + # "launchTime": "", + # "fundInterval": "8", + # "minLever": "1", + # "maxLever": "125", + # "posLimit": "0.05", + # "maintainTime": "" + # } + # + result = [] + for i in range(0, len(markets)): + market = markets[i] + marketId = self.safe_string(market, 'symbol') + quoteId = self.safe_string(market, 'quoteCoin') + baseId = self.safe_string(market, 'baseCoin') + quote = self.safe_currency_code(quoteId) + base = self.safe_currency_code(baseId) + supportMarginCoins = self.safe_value(market, 'supportMarginCoins', []) + settleId = None + if self.in_array(baseId, supportMarginCoins): + settleId = baseId + elif self.in_array(quoteId, supportMarginCoins): + settleId = quoteId + else: + settleId = self.safe_string(supportMarginCoins, 0) + settle = self.safe_currency_code(settleId) + symbol = base + '/' + quote + type = None + swap = False + spot = False + future = False + contract = False + pricePrecision = None + amountPrecision = None + linear = None + inverse = None + expiry = None + expiryDatetime = None + symbolType = self.safe_string(market, 'symbolType') + marginModes = None + isMarginTradingAllowed = False + if symbolType is None: + type = 'spot' + spot = True + pricePrecision = self.parse_number(self.parse_precision(self.safe_string(market, 'pricePrecision'))) + amountPrecision = self.parse_number(self.parse_precision(self.safe_string(market, 'quantityPrecision'))) + hasCrossMargin = self.in_array(marketId, self.options['crossMarginPairsData']) + hasIsolatedMargin = self.in_array(marketId, self.options['isolatedMarginPairsData']) + marginModes = { + 'cross': hasCrossMargin, + 'isolated': hasIsolatedMargin, + } + isMarginTradingAllowed = hasCrossMargin or hasIsolatedMargin + else: + if symbolType == 'perpetual': + type = 'swap' + swap = True + symbol = symbol + ':' + settle + elif symbolType == 'delivery': + expiry = self.safe_integer(market, 'deliveryTime') + expiryDatetime = self.iso8601(expiry) + expiryParts = expiryDatetime.split('-') + yearPart = self.safe_string(expiryParts, 0) + dayPart = self.safe_string(expiryParts, 2) + year = yearPart[2:4] + month = self.safe_string(expiryParts, 1) + day = dayPart[0:2] + expiryString = year + month + day + type = 'future' + future = True + symbol = symbol + ':' + settle + '-' + expiryString + contract = True + inverse = (base == settle) + linear = not inverse + priceDecimals = self.safe_integer(market, 'pricePlace') + amountDecimals = self.safe_integer(market, 'volumePlace') + priceStep = self.safe_string(market, 'priceEndStep') + amountStep = self.safe_string(market, 'sizeMultiplier') + precise = Precise(priceStep) + precise.decimals = max(precise.decimals, priceDecimals) + precise.reduce() + priceString = str(precise) + pricePrecision = self.parse_number(priceString) + preciseAmount = Precise(amountStep) + preciseAmount.decimals = max(preciseAmount.decimals, amountDecimals) + preciseAmount.reduce() + amountString = str(preciseAmount) + amountPrecision = self.parse_number(amountString) + marginModes = { + 'cross': True, + 'isolated': True, + } + status = self.safe_string_2(market, 'status', 'symbolStatus') + active = None + if status is not None: + active = ((status == 'online') or (status == 'normal')) + minCost = None + if quote == 'USDT': + minCost = self.safe_number(market, 'minTradeUSDT') + contractSize = 1 if contract else None + result.append(self.safe_market_structure({ + 'id': marketId, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': type, + 'spot': spot, + 'margin': spot and isMarginTradingAllowed, + 'marginModes': marginModes, + 'swap': swap, + 'future': future, + 'option': False, + 'active': active, + 'contract': contract, + 'linear': linear, + 'inverse': inverse, + 'taker': self.safe_number(market, 'takerFeeRate'), + 'maker': self.safe_number(market, 'makerFeeRate'), + 'contractSize': contractSize, + 'expiry': expiry, + 'expiryDatetime': expiryDatetime, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': amountPrecision, + 'price': pricePrecision, + }, + 'limits': { + 'leverage': { + 'min': self.safe_number(market, 'minLever'), + 'max': self.safe_number(market, 'maxLever'), + }, + 'amount': { + 'min': self.safe_number_2(market, 'minTradeNum', 'minTradeAmount'), + 'max': self.safe_number(market, 'maxTradeAmount'), + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': minCost, + 'max': None, + }, + }, + 'created': self.safe_integer(market, 'launchTime'), + 'info': market, + })) + return result + + async def fetch_uta_markets(self, params) -> List[Market]: + subTypes = ['SPOT', 'USDT-FUTURES', 'COIN-FUTURES', 'USDC-FUTURES'] + promises = [] + for i in range(0, len(subTypes)): + req = self.extend(params, { + 'category': subTypes[i], + }) + promises.append(self.publicUtaGetV3MarketInstruments(req)) + results = await asyncio.gather(*promises) + markets = [] + for i in range(0, len(results)): + res = self.safe_dict(results, i) + data = self.safe_list(res, 'data', []) + markets = self.array_concat(markets, data) + # + # spot uta + # + # { + # "symbol": "BTCUSDT", + # "category": "SPOT", + # "baseCoin": "BTC", + # "quoteCoin": "USDT", + # "buyLimitPriceRatio": "0.05", + # "sellLimitPriceRatio": "0.05", + # "minOrderQty": "0.000001", + # "maxOrderQty": "0", + # "pricePrecision": "2", + # "quantityPrecision": "6", + # "quotePrecision": "8", + # "minOrderAmount": "1", + # "maxSymbolOrderNum": "400", + # "maxProductOrderNum": "400", + # "status": "online", + # "maintainTime": "" + # } + # + # margin uta + # + # { + # "symbol": "BTCUSDC", + # "category": "MARGIN", + # "baseCoin": "BTC", + # "quoteCoin": "USDC", + # "buyLimitPriceRatio": "0.05", + # "sellLimitPriceRatio": "0.05", + # "minOrderQty": "0.00001", + # "maxOrderQty": "0", + # "pricePrecision": "2", + # "quantityPrecision": "5", + # "quotePrecision": "7", + # "minOrderAmount": "1", + # "maxSymbolOrderNum": "400", + # "maxProductOrderNum": "400", + # "status": "online", + # "maintainTime": "", + # "isIsolatedBaseBorrowable": "NO", + # "isIsolatedQuotedBorrowable": "NO", + # "warningRiskRatio": "0.8", + # "liquidationRiskRatio": "1", + # "maxCrossedLeverage": "3", + # "maxIsolatedLeverage": "0", + # "userMinBorrow": "0.00000001", + # "areaSymbol": "no" + # } + # + # swap and future uta + # + # { + # "symbol": "BTCPERP", + # "category": "USDC-FUTURES", + # "baseCoin": "BTC", + # "quoteCoin": "USDC", + # "buyLimitPriceRatio": "0.02", + # "sellLimitPriceRatio": "0.02", + # "feeRateUpRatio": "0.005", + # "makerFeeRate": "0.0002", + # "takerFeeRate": "0.0006", + # "openCostUpRatio": "0.01", + # "minOrderQty": "0.0001", + # "maxOrderQty": "", + # "pricePrecision": "1", + # "quantityPrecision": "4", + # "quotePrecision": null, + # "priceMultiplier": "0.5", + # "quantityMultiplier": "0.0001", + # "type": "perpetual", + # "minOrderAmount": "5", + # "maxSymbolOrderNum": "200", + # "maxProductOrderNum": "1000", + # "maxPositionNum": "150", + # "status": "online", + # "offTime": "-1", + # "limitOpenTime": "-1", + # "deliveryTime": "", + # "deliveryStartTime": "", + # "deliveryPeriod": "", + # "launchTime": "", + # "fundInterval": "8", + # "minLeverage": "1", + # "maxLeverage": "125", + # "maintainTime": "" + # } + # + result = [] + for i in range(0, len(markets)): + market = markets[i] + category = self.safe_string(market, 'category') + marketId = self.safe_string(market, 'symbol') + quoteId = self.safe_string(market, 'quoteCoin') + baseId = self.safe_string(market, 'baseCoin') + quote = self.safe_currency_code(quoteId) + base = self.safe_currency_code(baseId) + settleId = None + settle = None + if category == 'USDT-FUTURES': + settleId = 'USDT' + elif category == 'USDC-FUTURES': + settleId = 'USDC' + elif category == 'COIN-FUTURES': + settleId = base + if settleId is not None: + settle = self.safe_currency_code(settleId) + symbol = base + '/' + quote + type = None + swap = False + spot = False + future = False + contract = False + pricePrecision = None + amountPrecision = None + linear = None + inverse = None + expiry = None + expiryDatetime = None + symbolType = self.safe_string(market, 'type') + marginModes = None + isMarginTradingAllowed = False + isUtaMargin = (category == 'MARGIN') + if isUtaMargin or (category == 'SPOT'): + type = 'spot' + spot = True + if isUtaMargin: + isolatedBase = self.safe_string(market, 'isIsolatedBaseBorrowable') + isolatedQuote = self.safe_string(market, 'isIsolatedQuotedBorrowable') + isolated = (isolatedBase == 'YES') or (isolatedQuote == 'YES') + maxCrossLeverage = self.safe_string(market, 'maxCrossedLeverage') + cross = (maxCrossLeverage != '0') + marginModes = { + 'cross': cross, + 'isolated': isolated, + } + isMarginTradingAllowed = True + else: + if symbolType == 'perpetual': + type = 'swap' + swap = True + symbol = symbol + ':' + settle + elif symbolType == 'delivery': + expiry = self.safe_integer(market, 'deliveryTime') + expiryDatetime = self.iso8601(expiry) + expiryParts = expiryDatetime.split('-') + yearPart = self.safe_string(expiryParts, 0) + dayPart = self.safe_string(expiryParts, 2) + year = yearPart[2:4] + month = self.safe_string(expiryParts, 1) + day = dayPart[0:2] + expiryString = year + month + day + type = 'future' + future = True + symbol = symbol + ':' + settle + '-' + expiryString + contract = True + inverse = (base == settle) + linear = not inverse + marginModes = { + 'cross': True, + 'isolated': True, + } + pricePrecision = self.parse_number(self.parse_precision(self.safe_string(market, 'pricePrecision'))) + amountPrecision = self.parse_number(self.parse_precision(self.safe_string(market, 'quantityPrecision'))) + status = self.safe_string(market, 'status') + active = None + if status is not None: + active = ((status == 'online') or (status == 'normal')) + contractSize = 1 if contract else None + result.append(self.safe_market_structure({ + 'id': marketId, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': type, + 'spot': spot, + 'margin': spot and isMarginTradingAllowed, + 'marginModes': marginModes, + 'swap': swap, + 'future': future, + 'option': False, + 'active': active, + 'contract': contract, + 'linear': linear, + 'inverse': inverse, + 'taker': self.safe_number(market, 'takerFeeRate'), + 'maker': self.safe_number(market, 'makerFeeRate'), + 'contractSize': contractSize, + 'expiry': expiry, + 'expiryDatetime': expiryDatetime, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': amountPrecision, + 'price': pricePrecision, + }, + 'limits': { + 'leverage': { + 'min': self.safe_number(market, 'minLeverage'), + 'max': self.safe_number(market, 'maxLeverage'), + }, + 'amount': { + 'min': self.safe_number(market, 'minOrderQty'), + 'max': self.safe_number(market, 'maxOrderQty'), + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': self.safe_integer(market, 'launchTime'), + 'info': market, + })) + return result + + async def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://www.bitget.com/api-doc/spot/market/Get-Coin-List + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = await self.publicSpotGetV2SpotPublicCoins(params) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": "1746195617812", + # "data": [ + # { + # "coinId": "1456", + # "coin": "NEIROETH", + # "transfer": "false", + # "chains": [ + # { + # "chain": "ERC20", + # "needTag": "false", + # "withdrawable": "true", + # "rechargeable": "true", + # "withdrawFee": "44.91017965", + # "extraWithdrawFee": "0", + # "depositConfirm": "12", + # "withdrawConfirm": "64", + # "minDepositAmount": "0.06", + # "minWithdrawAmount": "60", + # "browserUrl": "https://etherscan.io/tx/", + # "contractAddress": "0xee2a03aa6dacf51c18679c516ad5283d8e7c2637", + # "withdrawStep": "0", + # "withdrawMinScale": "8", + # "congestion": "normal" + # } + # ], + # "areaCoin": "no" + # }, + # ... + # + result: dict = {} + data = self.safe_value(response, 'data', []) + fiatCurrencies = self.safe_list(self.options, 'fiatCurrencies', []) + for i in range(0, len(data)): + entry = data[i] + id = self.safe_string(entry, 'coin') # we don't use 'coinId' has no use. it is 'coin' field that needs to be used in currency related endpoints(deposit, withdraw, etc..) + code = self.safe_currency_code(id) + chains = self.safe_value(entry, 'chains', []) + networks: dict = {} + withdraw = None + deposit = None + chainsLength = len(chains) + if chainsLength == 0: + withdraw = False + deposit = False + for j in range(0, chainsLength): + chain = chains[j] + networkId = self.safe_string(chain, 'chain') + network = self.network_id_to_code(networkId, code) + network = network.upper() + withdrawable = (self.safe_string(chain, 'withdrawable') == 'true') + rechargeable = (self.safe_string(chain, 'rechargeable') == 'true') + withdraw = withdrawable if (withdraw is None) else (withdraw or withdrawable) + deposit = rechargeable if (deposit is None) else (deposit or rechargeable) + networks[network] = { + 'info': chain, + 'id': networkId, + 'network': network, + 'limits': { + 'withdraw': { + 'min': self.safe_number(chain, 'minWithdrawAmount'), + 'max': None, + }, + 'deposit': { + 'min': self.safe_number(chain, 'minDepositAmount'), + 'max': None, + }, + }, + 'active': None, + 'withdraw': withdrawable, + 'deposit': rechargeable, + 'fee': self.safe_number(chain, 'withdrawFee'), + 'precision': self.parse_number(self.parse_precision(self.safe_string(chain, 'withdrawMinScale'))), + } + active = withdraw and deposit + isFiat = self.in_array(code, fiatCurrencies) + result[code] = self.safe_currency_structure({ + 'info': entry, + 'id': id, + 'code': code, + 'networks': networks, + 'type': 'fiat' if isFiat else 'crypto', + 'name': None, + 'active': active, + 'deposit': deposit, + 'withdraw': withdraw, + 'fee': None, + 'precision': None, + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + 'deposit': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + }) + return result + + async def fetch_market_leverage_tiers(self, symbol: str, params={}) -> List[LeverageTier]: + """ + retrieve information on the maximum leverage, and maintenance margin for trades of varying trade sizes for a single market + + https://www.bitget.com/api-doc/contract/position/Get-Query-Position-Lever + https://www.bitget.com/api-doc/margin/cross/account/Cross-Tier-Data + https://www.bitget.com/api-doc/margin/isolated/account/Isolated-Tier-Data + https://www.bitget.com/api-doc/uta/public/Get-Position-Tier-Data + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: for spot margin 'cross' or 'isolated', default is 'isolated' + :param str [params.code]: required for cross spot margin + :param str [params.productType]: *contract and uta only* 'USDT-FUTURES', 'USDC-FUTURES', 'COIN-FUTURES', 'SUSDT-FUTURES', 'SUSDC-FUTURES' or 'SCOIN-FUTURES' + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :returns dict: a `leverage tiers structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = {} + response = None + marginMode = None + productType = None + uta = None + marginMode, params = self.handle_margin_mode_and_params('fetchMarketLeverageTiers', params, 'isolated') + productType, params = self.handle_product_type_and_params(market, params) + uta, params = self.handle_option_and_params(params, 'fetchMarketLeverageTiers', 'uta', False) + if uta: + if productType == 'SPOT': + if marginMode is not None: + productType = 'MARGIN' + request['symbol'] = market['id'] + request['category'] = productType + response = await self.publicUtaGetV3MarketPositionTier(self.extend(request, params)) + elif (market['swap']) or (market['future']): + request['productType'] = productType + request['symbol'] = market['id'] + response = await self.publicMixGetV2MixMarketQueryPositionLever(self.extend(request, params)) + elif marginMode == 'isolated': + request['symbol'] = market['id'] + response = await self.privateMarginGetV2MarginIsolatedTierData(self.extend(request, params)) + elif marginMode == 'cross': + code = self.safe_string(params, 'code') + if code is None: + raise ArgumentsRequired(self.id + ' fetchMarketLeverageTiers() requires a code argument') + params = self.omit(params, 'code') + currency = self.currency(code) + request['coin'] = currency['id'] + response = await self.privateMarginGetV2MarginCrossedTierData(self.extend(request, params)) + else: + raise BadRequest(self.id + ' fetchMarketLeverageTiers() symbol does not support market ' + market['symbol']) + # + # swap and future + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700290724614, + # "data": [ + # { + # "symbol": "BTCUSDT", + # "level": "1", + # "startUnit": "0", + # "endUnit": "150000", + # "leverage": "125", + # "keepMarginRate": "0.004" + # }, + # ] + # } + # + # isolated + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700291531894, + # "data": [ + # { + # "tier": "1", + # "symbol": "BTCUSDT", + # "leverage": "10", + # "baseCoin": "BTC", + # "quoteCoin": "USDT", + # "baseMaxBorrowableAmount": "2", + # "quoteMaxBorrowableAmount": "24000", + # "maintainMarginRate": "0.05", + # "initRate": "0.1111" + # }, + # ] + # } + # + # cross + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700291818831, + # "data": [ + # { + # "tier": "1", + # "leverage": "3", + # "coin": "BTC", + # "maxBorrowableAmount": "26", + # "maintainMarginRate": "0.1" + # } + # ] + # } + # + # uta + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1752735673127, + # "data": [ + # { + # "tier": "1", + # "minTierValue": "0", + # "maxTierValue": "150000", + # "leverage": "125", + # "mmr": "0.004" + # }, + # ] + # } + # + result = self.safe_value(response, 'data', []) + return self.parse_market_leverage_tiers(result, market) + + def parse_market_leverage_tiers(self, info, market: Market = None) -> List[LeverageTier]: + # + # swap and future + # + # { + # "symbol": "BTCUSDT", + # "level": "1", + # "startUnit": "0", + # "endUnit": "150000", + # "leverage": "125", + # "keepMarginRate": "0.004" + # } + # + # isolated + # + # { + # "tier": "1", + # "symbol": "BTCUSDT", + # "leverage": "10", + # "baseCoin": "BTC", + # "quoteCoin": "USDT", + # "baseMaxBorrowableAmount": "2", + # "quoteMaxBorrowableAmount": "24000", + # "maintainMarginRate": "0.05", + # "initRate": "0.1111" + # } + # + # cross + # + # { + # "tier": "1", + # "leverage": "3", + # "coin": "BTC", + # "maxBorrowableAmount": "26", + # "maintainMarginRate": "0.1" + # } + # + # uta + # + # { + # "tier": "1", + # "minTierValue": "0", + # "maxTierValue": "150000", + # "leverage": "125", + # "mmr": "0.004" + # } + # + tiers = [] + minNotional = 0 + for i in range(0, len(info)): + item = info[i] + minimumNotional = self.safe_number_2(item, 'startUnit', 'minTierValue') + if minimumNotional is not None: + minNotional = minimumNotional + maxNotional = self.safe_number_n(item, ['endUnit', 'maxBorrowableAmount', 'baseMaxBorrowableAmount', 'maxTierValue']) + marginCurrency = self.safe_string_2(item, 'coin', 'baseCoin') + currencyId = marginCurrency if (marginCurrency is not None) else market['base'] + marketId = self.safe_string(item, 'symbol') + tiers.append({ + 'tier': self.safe_integer_2(item, 'level', 'tier'), + 'symbol': self.safe_symbol(marketId, market), + 'currency': self.safe_currency_code(currencyId), + 'minNotional': minNotional, + 'maxNotional': maxNotional, + 'maintenanceMarginRate': self.safe_number_n(item, ['keepMarginRate', 'maintainMarginRate', 'mmr']), + 'maxLeverage': self.safe_number(item, 'leverage'), + 'info': item, + }) + minNotional = maxNotional + return tiers + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://www.bitget.com/api-doc/spot/account/Get-Deposit-Record + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: end time in milliseconds + :param str [params.idLessThan]: return records with id less than the provided value + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchDeposits', 'paginate') + if paginate: + return await self.fetch_paginated_call_cursor('fetchDeposits', None, since, limit, params, 'idLessThan', 'idLessThan', None, 100) + if since is None: + since = self.milliseconds() - 7776000000 # 90 days + request: dict = { + 'startTime': since, + 'endTime': self.milliseconds(), + } + currency = None + if code is not None: + currency = self.currency(code) + request['coin'] = currency['id'] + if limit is not None: + request['limit'] = limit + request, params = self.handle_until_option('endTime', request, params) + response = await self.privateSpotGetV2SpotWalletDepositRecords(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700528340608, + # "data": [ + # { + # "orderId": "1083832260799930368", + # "tradeId": "35bf0e588a42b25c71a9d45abe7308cabdeec6b7b423910b9bd4743d3a9a9efa", + # "coin": "BTC", + # "type": "deposit", + # "size": "0.00030000", + # "status": "success", + # "toAddress": "1BfZh7JESJGBUszCGeZnzxbVVvBycbJSbA", + # "dest": "on_chain", + # "chain": "BTC", + # "fromAddress": null, + # "cTime": "1694131668281", + # "uTime": "1694131680247" + # } + # ] + # } + # + rawTransactions = self.safe_list(response, 'data', []) + return self.parse_transactions(rawTransactions, None, since, limit) + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://www.bitget.com/api-doc/spot/account/Wallet-Withdrawal + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.chain]: the blockchain network the withdrawal is taking place on + :returns dict: a `transaction structure ` + """ + self.check_address(address) + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + if networkCode is None: + raise ArgumentsRequired(self.id + ' withdraw() requires a "network" parameter') + await self.load_markets() + currency = self.currency(code) + networkId = self.network_code_to_id(networkCode) + request: dict = { + 'coin': currency['id'], + 'address': address, + 'chain': networkId, + 'size': self.currency_to_precision(code, amount, networkCode), + 'transferType': 'on_chain', + } + if tag is not None: + request['tag'] = tag + response = await self.privateSpotPostV2SpotWalletWithdrawal(self.extend(request, params)) + # + # { + # "code":"00000", + # "msg":"success", + # "requestTime":1696784219602, + # "data": { + # "orderId":"1094957867615789056", + # "clientOid":"64f1e4ce842041d296b4517df1b5c2d7" + # } + # } + # + data = self.safe_value(response, 'data', {}) + result = self.parse_transaction(data, currency) + result['type'] = 'withdrawal' + withdrawOptions = self.safe_value(self.options, 'withdraw', {}) + fillResponseFromRequest = self.safe_bool(withdrawOptions, 'fillResponseFromRequest', True) + if fillResponseFromRequest: + result['currency'] = code + result['amount'] = amount + result['tag'] = tag + result['address'] = address + result['addressTo'] = address + result['network'] = networkCode + return result + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://www.bitget.com/api-doc/spot/account/Get-Withdraw-Record + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: end time in milliseconds + :param str [params.idLessThan]: return records with id less than the provided value + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchWithdrawals', 'paginate') + if paginate: + return await self.fetch_paginated_call_cursor('fetchWithdrawals', None, since, limit, params, 'idLessThan', 'idLessThan', None, 100) + currency = None + if code is not None: + currency = self.currency(code) + if since is None: + since = self.milliseconds() - 7776000000 # 90 days + request: dict = { + 'startTime': since, + 'endTime': self.milliseconds(), + } + if currency is not None: + request['coin'] = currency['id'] + request, params = self.handle_until_option('endTime', request, params) + if limit is not None: + request['limit'] = limit + response = await self.privateSpotGetV2SpotWalletWithdrawalRecords(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700528340608, + # "data": [ + # { + # "orderId": "1083832260799930368", + # "tradeId": "35bf0e588a42b25c71a9d45abe7308cabdeec6b7b423910b9bd4743d3a9a9efa", + # "clientOid": "123", + # "coin": "BTC", + # "type": "withdraw", + # "size": "0.00030000", + # "fee": "-1.0000000", + # "status": "success", + # "toAddress": "1BfZh7JESJGBUszCGeZnzxbVVvBycbJSbA", + # "dest": "on_chain", + # "chain": "BTC", + # "confirm": "100", + # "fromAddress": null, + # "cTime": "1694131668281", + # "uTime": "1694131680247" + # } + # ] + # } + # + rawTransactions = self.safe_list(response, 'data', []) + return self.parse_transactions(rawTransactions, currency, since, limit) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fetchDeposits + # + # { + # "orderId": "1083832260799930368", + # "tradeId": "35bf0e588a42b25c71a9d45abe7308cabdeec6b7b423910b9bd4743d3a9a9efa", + # "coin": "BTC", + # "type": "deposit", + # "size": "0.00030000", + # "status": "success", + # "toAddress": "1BfZh7JESJGBUszCGeZnzxbVVvBycbJSbA", + # "dest": "on_chain", + # "chain": "BTC", + # "fromAddress": null, + # "cTime": "1694131668281", + # "uTime": "1694131680247" + # } + # + # fetchWithdrawals + # + # { + # "orderId": "1083832260799930368", + # "tradeId": "35bf0e588a42b25c71a9d45abe7308cabdeec6b7b423910b9bd4743d3a9a9efa", + # "clientOid": "123", + # "coin": "BTC", + # "type": "withdraw", + # "size": "0.00030000", + # "fee": "-1.0000000", + # "status": "success", + # "toAddress": "1BfZh7JESJGBUszCGeZnzxbVVvBycbJSbA", + # "dest": "on_chain", + # "chain": "BTC", + # "confirm": "100", + # "fromAddress": null, + # "cTime": "1694131668281", + # "uTime": "1694131680247" + # } + # + currencyId = self.safe_string(transaction, 'coin') + code = self.safe_currency_code(currencyId, currency) + timestamp = self.safe_integer(transaction, 'cTime') + networkId = self.safe_string(transaction, 'chain') + status = self.safe_string(transaction, 'status') + tag = self.safe_string(transaction, 'tag') + feeCostString = self.safe_string(transaction, 'fee') + feeCostAbsString = None + if feeCostString is not None: + feeCostAbsString = Precise.string_abs(feeCostString) + fee = None + amountString = self.safe_string(transaction, 'size') + if feeCostAbsString is not None: + fee = {'currency': code, 'cost': self.parse_number(feeCostAbsString)} + amountString = Precise.string_sub(amountString, feeCostAbsString) + return { + 'id': self.safe_string(transaction, 'orderId'), + 'info': transaction, + 'txid': self.safe_string(transaction, 'tradeId'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': self.network_id_to_code(networkId), + 'addressFrom': self.safe_string(transaction, 'fromAddress'), + 'address': self.safe_string(transaction, 'toAddress'), + 'addressTo': self.safe_string(transaction, 'toAddress'), + 'amount': self.parse_number(amountString), + 'type': self.safe_string(transaction, 'type'), + 'currency': code, + 'status': self.parse_transaction_status(status), + 'updated': self.safe_integer(transaction, 'uTime'), + 'tagFrom': None, + 'tag': tag, + 'tagTo': tag, + 'comment': None, + 'internal': None, + 'fee': fee, + } + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'success': 'ok', + 'Pending': 'pending', + 'pending_review': 'pending', + 'pending_review_fail': 'failed', + 'reject': 'failed', + } + return self.safe_string(statuses, status, status) + + async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://www.bitget.com/api-doc/spot/account/Get-Deposit-Address + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + await self.load_markets() + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + currency = self.currency(code) + request: dict = { + 'coin': currency['id'], + } + if networkCode is not None: + request['chain'] = self.network_code_to_id(networkCode, code) + response = await self.privateSpotGetV2SpotWalletDepositAddress(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700532244807, + # "data": { + # "coin": "BTC", + # "address": "1BfZh7JESJGBUszCGeZnzxbVVvBycbJSbA", + # "chain": "", + # "tag": null, + # "url": "https://blockchair.com/bitcoin/transaction/" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_deposit_address(data, currency) + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + # + # { + # "coin": "BTC", + # "address": "1BfZh7JESJGBUszCGeZnzxbVVvBycbJSbA", + # "chain": "", + # "tag": null, + # "url": "https://blockchair.com/bitcoin/transaction/" + # } + # + currencyId = self.safe_string(depositAddress, 'coin') + networkId = self.safe_string(depositAddress, 'chain') + parsedCurrency = self.safe_currency_code(currencyId, currency) + network = None + if networkId is not None: + network = self.network_id_to_code(networkId, parsedCurrency) + return { + 'info': depositAddress, + 'currency': parsedCurrency, + 'network': network, + 'address': self.safe_string(depositAddress, 'address'), + 'tag': self.safe_string(depositAddress, 'tag'), + } + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://www.bitget.com/api-doc/spot/market/Get-Orderbook + https://www.bitget.com/api-doc/contract/market/Get-Merge-Depth + https://www.bitget.com/api-doc/uta/public/OrderBook + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['limit'] = limit + productType = None + productType, params = self.handle_product_type_and_params(market, params) + response = None + uta = None + uta, params = self.handle_option_and_params(params, 'fetchOrderBook', 'uta', False) + if uta: + request['category'] = productType + response = await self.publicUtaGetV3MarketOrderbook(self.extend(request, params)) + elif market['spot']: + response = await self.publicSpotGetV2SpotMarketOrderbook(self.extend(request, params)) + else: + request['productType'] = productType + response = await self.publicMixGetV2MixMarketMergeDepth(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1645854610294, + # "data": { + # "asks": [["39102", "11.026"]], + # "bids": [['39100.5', "1.773"]], + # "ts": "1645854610294" + # } + # } + # + # uta + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1750329437753, + # "data": { + # "a": [[104992.60, 0.018411]], + # "b":[[104927.40, 0.229914]], + # "ts": "1750329437763" + # } + # } + # + data = self.safe_value(response, 'data', {}) + bidsKey = 'b' if uta else 'bids' + asksKey = 'a' if uta else 'asks' + timestamp = self.safe_integer(data, 'ts') + return self.parse_order_book(data, market['symbol'], timestamp, bidsKey, asksKey) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "symbol": "BTCUSDT", + # "price": "26242", + # "indexPrice": "34867", + # "markPrice": "25555", + # "ts": "1695793390482" + # } + # + # spot + # + # { + # "open": "37202.46", + # "symbol": "BTCUSDT", + # "high24h": "37744.75", + # "low24h": "36666", + # "lastPr": "37583.69", + # "quoteVolume": "519127705.303", + # "baseVolume": "13907.0386", + # "usdtVolume": "519127705.302908", + # "ts": "1700532903261", + # "bidPr": "37583.68", + # "askPr": "37583.69", + # "bidSz": "0.0007", + # "askSz": "0.0829", + # "openUtc": "37449.4", + # "changeUtc24h": "0.00359", + # "change24h": "0.00321" + # } + # + # swap and future + # + # { + # "symbol": "BTCUSDT", + # "lastPr": "104823.8", + # "askPr": "104823.8", + # "bidPr": "104823.5", + # "bidSz": "0.703", + # "askSz": "13.894", + # "high24h": "105289.3", + # "low24h": "103447.9", + # "ts": "1750332210370", + # "change24h": "0.00471", + # "baseVolume": "79089.5675", + # "quoteVolume": "8274870921.80485", + # "usdtVolume": "8274870921.80485", + # "openUtc": "104833", + # "changeUtc24h": "-0.00009", + # "indexPrice": "104881.953125", + # "fundingRate": "-0.000014", + # "holdingAmount": "7452.6421", + # "deliveryStartTime": null, + # "deliveryTime": null, + # "deliveryStatus": "", + # "open24h": "104332.3", + # "markPrice": "104824.2" + # } + # + # spot uta + # + # { + # "category": "SPOT", + # "symbol": "BTCUSDT", + # "ts": "1750330651972", + # "lastPrice": "104900.2", + # "openPrice24h": "104321.2", + # "highPrice24h": "107956.8", + # "lowPrice24h": "103600.1", + # "ask1Price": "104945.8", + # "bid1Price": "104880.6", + # "bid1Size": "0.266534", + # "ask1Size": "0.014001", + # "price24hPcnt": "0.00555", + # "volume24h": "355.941109", + # "turnover24h": "37302936.008134" + # } + # + # swap and future uta + # + # { + # "category": "USDT-FUTURES", + # "symbol": "BTCUSDT", + # "ts": "1750332730472", + # "lastPrice": "104738", + # "openPrice24h": "104374", + # "highPrice24h": "105289.3", + # "lowPrice24h": "103447.9", + # "ask1Price": "104738", + # "bid1Price": "104737.7", + # "bid1Size": "2.036", + # "ask1Size": "8.094", + # "price24hPcnt": "0.00349", + # "volume24h": "79101.6477", + # "turnover24h": "8276293391.45973", + # "indexPrice": "104785.956168", + # "markPrice": "104738", + # "fundingRate": "-0.000007", + # "openInterest": "7465.5938", + # "deliveryStartTime": "", + # "deliveryTime": "", + # "deliveryStatus": "" + # } + # + marketId = self.safe_string(ticker, 'symbol') + close = self.safe_string_2(ticker, 'lastPr', 'lastPrice') + timestamp = self.safe_integer_omit_zero(ticker, 'ts') # exchange bitget provided 0 + change = self.safe_string(ticker, 'change24h') + category = self.safe_string(ticker, 'category') + markPrice = self.safe_string(ticker, 'markPrice') + marketType: str + if (markPrice is not None) and (category != 'SPOT'): + marketType = 'contract' + else: + marketType = 'spot' + percentage = self.safe_string(ticker, 'price24hPcnt') + if percentage is None: + percentage = Precise.string_mul(change, '100') + return self.safe_ticker({ + 'symbol': self.safe_symbol(marketId, market, None, marketType), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string_2(ticker, 'high24h', 'highPrice24h'), + 'low': self.safe_string_2(ticker, 'low24h', 'lowPrice24h'), + 'bid': self.safe_string_2(ticker, 'bidPr', 'bid1Price'), + 'bidVolume': self.safe_string_2(ticker, 'bidSz', 'bid1Size'), + 'ask': self.safe_string_2(ticker, 'askPr', 'ask1Price'), + 'askVolume': self.safe_string_2(ticker, 'askSz', 'ask1Size'), + 'vwap': None, + 'open': self.safe_string_n(ticker, ['open', 'open24h', 'openPrice24h']), + 'close': close, + 'last': close, + 'previousClose': None, + 'change': change, + 'percentage': percentage, + 'average': None, + 'baseVolume': self.safe_string_2(ticker, 'baseVolume', 'volume24h'), + 'quoteVolume': self.safe_string_2(ticker, 'quoteVolume', 'turnover24h'), + 'indexPrice': self.safe_string(ticker, 'indexPrice'), + 'markPrice': markPrice, + 'info': ticker, + }, market) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://www.bitget.com/api-doc/spot/market/Get-Tickers + https://www.bitget.com/api-doc/contract/market/Get-Ticker + https://www.bitget.com/api-doc/uta/public/Tickers + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + productType = None + productType, params = self.handle_product_type_and_params(market, params) + response = None + uta = None + uta, params = self.handle_option_and_params(params, 'fetchTicker', 'uta', False) + if uta: + request['category'] = productType + response = await self.publicUtaGetV3MarketTickers(self.extend(request, params)) + elif market['spot']: + response = await self.publicSpotGetV2SpotMarketTickers(self.extend(request, params)) + else: + request['productType'] = productType + response = await self.publicMixGetV2MixMarketTicker(self.extend(request, params)) + # + # spot + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700532903782, + # "data": [ + # { + # "open": "37202.46", + # "symbol": "BTCUSDT", + # "high24h": "37744.75", + # "low24h": "36666", + # "lastPr": "37583.69", + # "quoteVolume": "519127705.303", + # "baseVolume": "13907.0386", + # "usdtVolume": "519127705.302908", + # "ts": "1700532903261", + # "bidPr": "37583.68", + # "askPr": "37583.69", + # "bidSz": "0.0007", + # "askSz": "0.0829", + # "openUtc": "37449.4", + # "changeUtc24h": "0.00359", + # "change24h": "0.00321" + # } + # ] + # } + # + # swap and future + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1750332210369, + # "data": [ + # { + # "symbol": "BTCUSDT", + # "lastPr": "104823.8", + # "askPr": "104823.8", + # "bidPr": "104823.5", + # "bidSz": "0.703", + # "askSz": "13.894", + # "high24h": "105289.3", + # "low24h": "103447.9", + # "ts": "1750332210370", + # "change24h": "0.00471", + # "baseVolume": "79089.5675", + # "quoteVolume": "8274870921.80485", + # "usdtVolume": "8274870921.80485", + # "openUtc": "104833", + # "changeUtc24h": "-0.00009", + # "indexPrice": "104881.953125", + # "fundingRate": "-0.000014", + # "holdingAmount": "7452.6421", + # "deliveryStartTime": null, + # "deliveryTime": null, + # "deliveryStatus": "", + # "open24h": "104332.3", + # "markPrice": "104824.2" + # } + # ] + # } + # + # spot uta + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1750330653575, + # "data": [ + # { + # "category": "SPOT", + # "symbol": "BTCUSDT", + # "ts": "1750330651972", + # "lastPrice": "104900.2", + # "openPrice24h": "104321.2", + # "highPrice24h": "107956.8", + # "lowPrice24h": "103600.1", + # "ask1Price": "104945.8", + # "bid1Price": "104880.6", + # "bid1Size": "0.266534", + # "ask1Size": "0.014001", + # "price24hPcnt": "0.00555", + # "volume24h": "355.941109", + # "turnover24h": "37302936.008134" + # } + # ] + # } + # + # swap and future uta + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1750332731203, + # "data": [ + # { + # "category": "USDT-FUTURES", + # "symbol": "BTCUSDT", + # "ts": "1750332730472", + # "lastPrice": "104738", + # "openPrice24h": "104374", + # "highPrice24h": "105289.3", + # "lowPrice24h": "103447.9", + # "ask1Price": "104738", + # "bid1Price": "104737.7", + # "bid1Size": "2.036", + # "ask1Size": "8.094", + # "price24hPcnt": "0.00349", + # "volume24h": "79101.6477", + # "turnover24h": "8276293391.45973", + # "indexPrice": "104785.956168", + # "markPrice": "104738", + # "fundingRate": "-0.000007", + # "openInterest": "7465.5938", + # "deliveryStartTime": "", + # "deliveryTime": "", + # "deliveryStatus": "" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_ticker(data[0], market) + + async def fetch_mark_price(self, symbol: str, params={}) -> Ticker: + """ + fetches the mark price for a specific market + + https://www.bitget.com/api-doc/contract/market/Get-Symbol-Price + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = None + if market['spot']: + raise NotSupported(self.id + ' fetchMarkPrice() is not supported for spot markets') + else: + productType = None + productType, params = self.handle_product_type_and_params(market, params) + request['productType'] = productType + response = await self.publicMixGetV2MixMarketSymbolPrice(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + return self.parse_ticker(data[0], market) + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://www.bitget.com/api-doc/spot/market/Get-Tickers + https://www.bitget.com/api-doc/contract/market/Get-All-Symbol-Ticker + https://www.bitget.com/api-doc/uta/public/Tickers + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :param str [params.subType]: *contract only* 'linear', 'inverse' + :param str [params.productType]: *contract only* 'USDT-FUTURES', 'USDC-FUTURES', 'COIN-FUTURES', 'SUSDT-FUTURES', 'SUSDC-FUTURES' or 'SCOIN-FUTURES' + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + market = None + if symbols is not None: + symbol = self.safe_value(symbols, 0) + market = self.market(symbol) + response = None + request: dict = {} + type = None + type, params = self.handle_market_type_and_params('fetchTickers', market, params) + # Calls like `.fetchTickers(None, {subType:'inverse'})` should be supported for self exchange, so + # as "options.defaultSubType" is also set in exchange options, we should consider `params.subType` + # with higher priority and only default to spot, if `subType` is not set in params + passedSubType = self.safe_string(params, 'subType') + productType = None + productType, params = self.handle_product_type_and_params(market, params) + # only if passedSubType and productType is None, then use spot + uta = None + uta, params = self.handle_option_and_params(params, 'fetchTickers', 'uta', False) + if uta: + symbolsLength = len(symbols) + if (symbols is not None) and (symbolsLength == 1): + request['symbol'] = market['id'] + request['category'] = productType + response = await self.publicUtaGetV3MarketTickers(self.extend(request, params)) + elif type == 'spot' and passedSubType is None: + response = await self.publicSpotGetV2SpotMarketTickers(self.extend(request, params)) + else: + request['productType'] = productType + response = await self.publicMixGetV2MixMarketTickers(self.extend(request, params)) + # + # spot + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700532903782, + # "data": [ + # { + # "open": "37202.46", + # "symbol": "BTCUSDT", + # "high24h": "37744.75", + # "low24h": "36666", + # "lastPr": "37583.69", + # "quoteVolume": "519127705.303", + # "baseVolume": "13907.0386", + # "usdtVolume": "519127705.302908", + # "ts": "1700532903261", + # "bidPr": "37583.68", + # "askPr": "37583.69", + # "bidSz": "0.0007", + # "askSz": "0.0829", + # "openUtc": "37449.4", + # "changeUtc24h": "0.00359", + # "change24h": "0.00321" + # } + # ] + # } + # + # swap and future + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700533773477, + # "data": [ + # { + # "open": "14.9776", + # "symbol": "LINKUSDT", + # "high24h": "15.3942", + # "low24h": "14.3457", + # "lastPr": "14.3748", + # "quoteVolume": "7008612.4299", + # "baseVolume": "469908.8523", + # "usdtVolume": "7008612.42986561", + # "ts": "1700533772309", + # "bidPr": "14.375", + # "askPr": "14.3769", + # "bidSz": "50.004", + # "askSz": "0.7647", + # "openUtc": "14.478", + # "changeUtc24h": "-0.00713", + # "change24h": "-0.04978" + # }, + # ] + # } + # + # spot uta + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1750330653575, + # "data": [ + # { + # "category": "SPOT", + # "symbol": "BTCUSDT", + # "ts": "1750330651972", + # "lastPrice": "104900.2", + # "openPrice24h": "104321.2", + # "highPrice24h": "107956.8", + # "lowPrice24h": "103600.1", + # "ask1Price": "104945.8", + # "bid1Price": "104880.6", + # "bid1Size": "0.266534", + # "ask1Size": "0.014001", + # "price24hPcnt": "0.00555", + # "volume24h": "355.941109", + # "turnover24h": "37302936.008134" + # } + # ] + # } + # + # swap and future uta + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1750332731203, + # "data": [ + # { + # "category": "USDT-FUTURES", + # "symbol": "BTCUSDT", + # "ts": "1750332730472", + # "lastPrice": "104738", + # "openPrice24h": "104374", + # "highPrice24h": "105289.3", + # "lowPrice24h": "103447.9", + # "ask1Price": "104738", + # "bid1Price": "104737.7", + # "bid1Size": "2.036", + # "ask1Size": "8.094", + # "price24hPcnt": "0.00349", + # "volume24h": "79101.6477", + # "turnover24h": "8276293391.45973", + # "indexPrice": "104785.956168", + # "markPrice": "104738", + # "fundingRate": "-0.000007", + # "openInterest": "7465.5938", + # "deliveryStartTime": "", + # "deliveryTime": "", + # "deliveryStatus": "" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_tickers(data, symbols) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # spot, swap and future: fetchTrades + # + # { + # "tradeId": "1075199767891652609", + # "price": "29376.5", + # "size": "6.035", + # "side": "Buy", + # "ts": "1692073521000", + # "symbol": "BTCUSDT" + # } + # + # spot: fetchMyTrades + # + # { + # "userId": "7264631750", + # "symbol": "BTCUSDT", + # "orderId": "1098394344925597696", + # "tradeId": "1098394344974925824", + # "orderType": "market", + # "side": "sell", + # "priceAvg": "28467.68", + # "size": "0.0002", + # "amount": "5.693536", + # "feeDetail": { + # "deduction": "no", + # "feeCoin": "USDT", + # "totalDeductionFee": "", + # "totalFee": "-0.005693536" + # }, + # "tradeScope": "taker", + # "cTime": "1697603539699", + # "uTime": "1697603539754" + # } + # + # spot margin: fetchMyTrades + # + # { + # "orderId": "1099353730455318528", + # "tradeId": "1099353730627092481", + # "orderType": "market", + # "side": "sell", + # "priceAvg": "29543.7", + # "size": "0.0001", + # "amount": "2.95437", + # "tradeScope": "taker", + # "feeDetail": { + # "deduction": "no", + # "feeCoin": "USDT", + # "totalDeductionFee": "0", + # "totalFee": "-0.00295437" + # }, + # "cTime": "1697832275063", + # "uTime": "1697832275150" + # } + # + # swap and future: fetchMyTrades + # + # { + # "tradeId": "1111468664328269825", + # "symbol": "BTCUSDT", + # "orderId": "1111468664264753162", + # "price": "37271.4", + # "baseVolume": "0.001", + # "feeDetail": [ + # { + # "deduction": "no", + # "feeCoin": "USDT", + # "totalDeductionFee": null, + # "totalFee": "-0.02236284" + # } + # ], + # "side": "buy", + # "quoteVolume": "37.2714", + # "profit": "-0.0007", + # "enterPointSource": "web", + # "tradeSide": "close", + # "posMode": "hedge_mode", + # "tradeScope": "taker", + # "cTime": "1700720700342" + # } + # + # uta fetchTrades + # + # { + # "execId": "1319896716324937729", + # "price": "105909.1", + # "size": "6.3090", + # "side": "sell", + # "ts": "1750413820344" + # } + # + # uta fetchMyTrades + # + # { + # "execId": "1322441401010528257", + # "orderId": "1322441400976261120", + # "category": "USDT-FUTURES", + # "symbol": "BTCUSDT", + # "orderType": "market", + # "side": "sell", + # "execPrice": "107005.4", + # "execQty": "0.0001", + # "execValue": "10.7005", + # "tradeScope": "taker", + # "feeDetail": [{ + # "feeCoin": "USDT", + # "fee":"0.00642032" + # }], + # "createdTime": "1751020520451", + # "updatedTime": "1751020520458", + # "execPnl": "0.00017" + # } + # + marketId = self.safe_string(trade, 'symbol') + symbol = self.safe_symbol(marketId, market) + timestamp = self.safe_integer_n(trade, ['cTime', 'ts', 'createdTime']) + fee = None + feeDetail = self.safe_value(trade, 'feeDetail') + posMode = self.safe_string(trade, 'posMode') + category = self.safe_string(trade, 'category') + isFeeStructure = (posMode is not None) or (category is not None) + feeStructure = feeDetail[0] if isFeeStructure else feeDetail + if feeStructure is not None: + currencyCode = self.safe_currency_code(self.safe_string(feeStructure, 'feeCoin')) + fee = { + 'currency': currencyCode, + } + feeCostString = self.safe_string_2(feeStructure, 'totalFee', 'fee') + deduction = self.safe_string(feeStructure, 'deduction') is True if 'yes' else False + if deduction: + fee['cost'] = feeCostString + else: + fee['cost'] = Precise.string_neg(feeCostString) + return self.safe_trade({ + 'info': trade, + 'id': self.safe_string_2(trade, 'tradeId', 'execId'), + 'order': self.safe_string(trade, 'orderId'), + 'symbol': symbol, + 'side': self.safe_string_lower(trade, 'side'), + 'type': self.safe_string(trade, 'orderType'), + 'takerOrMaker': self.safe_string(trade, 'tradeScope'), + 'price': self.safe_string_n(trade, ['priceAvg', 'price', 'execPrice']), + 'amount': self.safe_string_n(trade, ['baseVolume', 'size', 'execQty']), + 'cost': self.safe_string_n(trade, ['quoteVolume', 'amount', 'execValue']), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'fee': fee, + }, market) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://www.bitget.com/api-doc/spot/market/Get-Recent-Trades + https://www.bitget.com/api-doc/spot/market/Get-Market-Trades + https://www.bitget.com/api-doc/contract/market/Get-Recent-Fills + https://www.bitget.com/api-doc/contract/market/Get-Fills-History + https://www.bitget.com/api-doc/uta/public/Fills + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :param int [params.until]: *only applies to publicSpotGetV2SpotMarketFillsHistory and publicMixGetV2MixMarketFillsHistory* the latest time in ms to fetch trades for + :param boolean [params.paginate]: *only applies to publicSpotGetV2SpotMarketFillsHistory and publicMixGetV2MixMarketFillsHistory* default False, when True will automatically paginate by calling self endpoint multiple times + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchTrades', 'paginate') + if paginate: + return await self.fetch_paginated_call_cursor('fetchTrades', symbol, since, limit, params, 'idLessThan', 'idLessThan') + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + uta = None + uta, params = self.handle_option_and_params(params, 'fetchTrades', 'uta', False) + if limit is not None: + if uta: + request['limit'] = min(limit, 100) + elif market['contract']: + request['limit'] = min(limit, 1000) + else: + request['limit'] = limit + options = self.safe_value(self.options, 'fetchTrades', {}) + response = None + productType = None + productType, params = self.handle_product_type_and_params(market, params) + if uta: + if productType == 'SPOT': + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchTrades', params) + if marginMode is not None: + productType = 'MARGIN' + request['category'] = productType + response = await self.publicUtaGetV3MarketFills(self.extend(request, params)) + elif market['spot']: + spotOptions = self.safe_value(options, 'spot', {}) + defaultSpotMethod = self.safe_string(spotOptions, 'method', 'publicSpotGetV2SpotMarketFillsHistory') + spotMethod = self.safe_string(params, 'method', defaultSpotMethod) + params = self.omit(params, 'method') + if spotMethod == 'publicSpotGetV2SpotMarketFillsHistory': + request, params = self.handle_until_option('endTime', request, params) + if since is not None: + request['startTime'] = since + response = await self.publicSpotGetV2SpotMarketFillsHistory(self.extend(request, params)) + elif spotMethod == 'publicSpotGetV2SpotMarketFills': + response = await self.publicSpotGetV2SpotMarketFills(self.extend(request, params)) + else: + swapOptions = self.safe_value(options, 'swap', {}) + defaultSwapMethod = self.safe_string(swapOptions, 'method', 'publicMixGetV2MixMarketFillsHistory') + swapMethod = self.safe_string(params, 'method', defaultSwapMethod) + params = self.omit(params, 'method') + request['productType'] = productType + if swapMethod == 'publicMixGetV2MixMarketFillsHistory': + request, params = self.handle_until_option('endTime', request, params) + if since is not None: + request['startTime'] = since + response = await self.publicMixGetV2MixMarketFillsHistory(self.extend(request, params)) + elif swapMethod == 'publicMixGetV2MixMarketFills': + response = await self.publicMixGetV2MixMarketFills(self.extend(request, params)) + # + # spot + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1692073693562, + # "data": [ + # { + # "symbol": "BTCUSDT_SPBL", + # "tradeId": "1075200479040323585", + # "side": "Sell", + # "price": "29381.54", + # "size": "0.0056", + # "ts": "1692073691000" + # }, + # ] + # } + # + # swap + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1692073522689, + # "data": [ + # { + # "tradeId": "1075199767891652609", + # "price": "29376.5", + # "size": "6.035", + # "side": "Buy", + # "ts": "1692073521000", + # "symbol": "BTCUSDT_UMCBL" + # }, + # ] + # } + # + # uta + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1750413823980, + # "data": [ + # { + # "execId": "1319896716324937729", + # "price": "105909.1", + # "size": "6.3090", + # "side": "sell", + # "ts": "1750413820344" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_trades(data, market, since, limit) + + async def fetch_trading_fee(self, symbol: str, params={}) -> TradingFeeInterface: + """ + fetch the trading fees for a market + + https://www.bitget.com/api-doc/common/public/Get-Trade-Rate + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'isolated' or 'cross', for finding the fee rate of spot margin trading pairs + :returns dict: a `fee structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchTradingFee', params) + if market['spot']: + if marginMode is not None: + request['businessType'] = 'margin' + else: + request['businessType'] = 'spot' + else: + request['businessType'] = 'mix' + response = await self.privateCommonGetV2CommonTradeRate(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700549524887, + # "data": { + # "makerFeeRate": "0.001", + # "takerFeeRate": "0.001" + # } + # } + # + data = self.safe_value(response, 'data', {}) + return self.parse_trading_fee(data, market) + + async def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://www.bitget.com/api-doc/spot/market/Get-Symbols + https://www.bitget.com/api-doc/contract/market/Get-All-Symbols-Contracts + https://www.bitget.com/api-doc/margin/common/support-currencies + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.productType]: *contract only* 'USDT-FUTURES', 'USDC-FUTURES', 'COIN-FUTURES', 'SUSDT-FUTURES', 'SUSDC-FUTURES' or 'SCOIN-FUTURES' + :param boolean [params.margin]: set to True for spot margin + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + await self.load_markets() + response = None + marginMode = None + marketType = None + marginMode, params = self.handle_margin_mode_and_params('fetchTradingFees', params) + marketType, params = self.handle_market_type_and_params('fetchTradingFees', None, params) + if marketType == 'spot': + margin = self.safe_bool(params, 'margin', False) + params = self.omit(params, 'margin') + if (marginMode is not None) or margin: + response = await self.publicMarginGetV2MarginCurrencies(params) + else: + response = await self.publicSpotGetV2SpotPublicSymbols(params) + elif (marketType == 'swap') or (marketType == 'future'): + productType = None + productType, params = self.handle_product_type_and_params(None, params) + params['productType'] = productType + response = await self.publicMixGetV2MixMarketContracts(params) + else: + raise NotSupported(self.id + ' does not support ' + marketType + ' market') + # + # spot and margin + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700102364653, + # "data": [ + # { + # "symbol": "TRXUSDT", + # "baseCoin": "TRX", + # "quoteCoin": "USDT", + # "minTradeAmount": "0", + # "maxTradeAmount": "10000000000", + # "takerFeeRate": "0.002", + # "makerFeeRate": "0.002", + # "pricePrecision": "6", + # "quantityPrecision": "4", + # "quotePrecision": "6", + # "status": "online", + # "minTradeUSDT": "5", + # "buyLimitPriceRatio": "0.05", + # "sellLimitPriceRatio": "0.05" + # }, + # ] + # } + # + # swap and future + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700102364709, + # "data": [ + # { + # "symbol": "BTCUSDT", + # "baseCoin": "BTC", + # "quoteCoin": "USDT", + # "buyLimitPriceRatio": "0.01", + # "sellLimitPriceRatio": "0.01", + # "feeRateUpRatio": "0.005", + # "makerFeeRate": "0.0002", + # "takerFeeRate": "0.0006", + # "openCostUpRatio": "0.01", + # "supportMarginCoins": ["USDT"], + # "minTradeNum": "0.001", + # "priceEndStep": "1", + # "volumePlace": "3", + # "pricePlace": "1", + # "sizeMultiplier": "0.001", + # "symbolType": "perpetual", + # "minTradeUSDT": "5", + # "maxSymbolOrderNum": "200", + # "maxProductOrderNum": "400", + # "maxPositionNum": "150", + # "symbolStatus": "normal", + # "offTime": "-1", + # "limitOpenTime": "-1", + # "deliveryTime": "", + # "deliveryStartTime": "", + # "deliveryPeriod": "", + # "launchTime": "", + # "fundInterval": "8", + # "minLever": "1", + # "maxLever": "125", + # "posLimit": "0.05", + # "maintainTime": "" + # }, + # ] + # } + # + data = self.safe_value(response, 'data', []) + result: dict = {} + for i in range(0, len(data)): + entry = data[i] + marketId = self.safe_string(entry, 'symbol') + symbol = self.safe_symbol(marketId, None, None, marketType) + market = self.market(symbol) + fee = self.parse_trading_fee(entry, market) + result[symbol] = fee + return result + + def parse_trading_fee(self, data, market: Market = None): + marketId = self.safe_string(data, 'symbol') + return { + 'info': data, + 'symbol': self.safe_symbol(marketId, market), + 'maker': self.safe_number(data, 'makerFeeRate'), + 'taker': self.safe_number(data, 'takerFeeRate'), + 'percentage': None, + 'tierBased': None, + } + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # [ + # "1645911960000", + # "39406", + # "39407", + # "39374.5", + # "39379", + # "35.526", + # "1399132.341" + # ] + # + inverse = self.safe_bool(market, 'inverse') + volumeIndex = 6 if inverse else 5 + return [ + self.safe_integer(ohlcv, 0), + self.safe_number(ohlcv, 1), + self.safe_number(ohlcv, 2), + self.safe_number(ohlcv, 3), + self.safe_number(ohlcv, 4), + self.safe_number(ohlcv, volumeIndex), + ] + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://www.bitget.com/api-doc/spot/market/Get-Candle-Data + https://www.bitget.com/api-doc/spot/market/Get-History-Candle-Data + https://www.bitget.com/api-doc/contract/market/Get-Candle-Data + https://www.bitget.com/api-doc/contract/market/Get-History-Candle-Data + https://www.bitget.com/api-doc/contract/market/Get-History-Index-Candle-Data + https://www.bitget.com/api-doc/contract/market/Get-History-Mark-Candle-Data + https://www.bitget.com/api-doc/uta/public/Get-Candle-Data + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :param int [params.until]: timestamp in ms of the latest candle to fetch + :param boolean [params.useHistoryEndpoint]: whether to force to use historical endpoint(it has max limit of 200) + :param boolean [params.useHistoryEndpointForPagination]: whether to force to use historical endpoint for pagination(default True) + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param str [params.price]: *swap only* "mark"(to fetch mark price candles) or "index"(to fetch index price candles) + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + defaultLimit = 100 # default 100, max 1000 + maxLimitForRecentEndpoint = 1000 + maxLimitForHistoryEndpoint = 200 # note, max 1000 bars are supported for "recent-candles" endpoint, but "historical-candles" support only max 200 + useHistoryEndpoint = self.safe_bool(params, 'useHistoryEndpoint', False) + useHistoryEndpointForPagination = self.safe_bool(params, 'useHistoryEndpointForPagination', True) + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate') + if paginate: + limitForPagination = maxLimitForHistoryEndpoint if useHistoryEndpointForPagination else maxLimitForRecentEndpoint + return await self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params, limitForPagination) + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + marketType = None + timeframes = None + uta = None + uta, params = self.handle_option_and_params(params, 'fetchOHLCV', 'uta', False) + if uta: + timeframes = self.options['timeframes']['uta'] + request['interval'] = self.safe_string(timeframes, timeframe, timeframe) + else: + marketType = 'spot' if market['spot'] else 'swap' + timeframes = self.options['timeframes'][marketType] + request['granularity'] = self.safe_string(timeframes, timeframe, timeframe) + msInDay = 86400000 + now = self.milliseconds() + duration = self.parse_timeframe(timeframe) * 1000 + until = self.safe_integer(params, 'until') + limitDefined = limit is not None + sinceDefined = since is not None + untilDefined = until is not None + params = self.omit(params, ['until']) + # retrievable periods listed here: + # - https://www.bitget.com/api-doc/spot/market/Get-Candle-Data#request-parameters + # - https://www.bitget.com/api-doc/contract/market/Get-Candle-Data#description + key = 'spot' if market['spot'] else 'swap' + ohlcOptions = self.safe_dict(self.options['fetchOHLCV'], key, {}) + maxLimitPerTimeframe = self.safe_dict(ohlcOptions, 'maxLimitPerTimeframe', {}) + maxLimitForThisTimeframe = self.safe_integer(maxLimitPerTimeframe, timeframe, limit) + recentEndpointDaysMap = self.safe_dict(self.options['fetchOHLCV'], 'maxRecentDaysPerTimeframe', {}) + recentEndpointAvailableDays = self.safe_integer(recentEndpointDaysMap, timeframe) + recentEndpointBoundaryTs = now - (recentEndpointAvailableDays - 1) * msInDay + if limitDefined: + limit = min(limit, maxLimitForRecentEndpoint) + limit = min(limit, maxLimitForThisTimeframe) + else: + limit = defaultLimit + limitMultipliedDuration = limit * duration + # exchange aligns from endTime, so it's important, not startTime + # startTime is supported only on "recent" endpoint, not on "historical" endpoint + calculatedStartTime = None + calculatedEndTime = None + if sinceDefined: + calculatedStartTime = since + request['startTime'] = since + if not untilDefined: + calculatedEndTime = self.sum(calculatedStartTime, limitMultipliedDuration) + if calculatedEndTime > now: + calculatedEndTime = now + request['endTime'] = calculatedEndTime + if untilDefined: + calculatedEndTime = until + if calculatedEndTime > now: + calculatedEndTime = now + request['endTime'] = calculatedEndTime + if not sinceDefined: + calculatedStartTime = calculatedEndTime - limitMultipliedDuration + # we do not need to set "startTime" here + # if historical endpoint is needed, we should re-set the variables + historicalEndpointNeeded = False + if (calculatedStartTime is not None and calculatedStartTime <= recentEndpointBoundaryTs) or useHistoryEndpoint: + historicalEndpointNeeded = True + # only for "historical-candles" - ensure we use correct max limit + limit = min(limit, maxLimitForHistoryEndpoint) + limitMultipliedDuration = limit * duration + calculatedStartTime = calculatedEndTime - limitMultipliedDuration + request['startTime'] = calculatedStartTime + # for contract, maximum 90 days allowed between start-end times + if not market['spot']: + maxDistanceDaysForContracts = 90 + # only correct if request is larger + if calculatedEndTime - calculatedStartTime > maxDistanceDaysForContracts * msInDay: + calculatedEndTime = self.sum(calculatedStartTime, maxDistanceDaysForContracts * msInDay) + request['endTime'] = calculatedEndTime + # we need to set limit to safely cover the period + request['limit'] = limit + # make request + response = None + productType = None + priceType = None + priceType, params = self.handle_param_string(params, 'price') + productType, params = self.handle_product_type_and_params(market, params) + if uta: + if priceType is not None: + if priceType == 'mark': + request['type'] = 'MARK' + elif priceType == 'index': + request['type'] = 'INDEX' + request['category'] = productType + response = await self.publicUtaGetV3MarketCandles(self.extend(request, params)) + elif market['spot']: + # checks if we need history endpoint + if historicalEndpointNeeded: + response = await self.publicSpotGetV2SpotMarketHistoryCandles(self.extend(request, params)) + else: + if not limitDefined: + request['limit'] = 1000 + limit = 1000 + response = await self.publicSpotGetV2SpotMarketCandles(self.extend(request, params)) + else: + request['productType'] = productType + extended = self.extend(request, params) + if not historicalEndpointNeeded and (priceType == 'mark' or priceType == 'index'): + if not limitDefined: + extended['limit'] = 1000 + limit = 1000 + # Recent endpoint for mark/index prices + # https://www.bitget.com/api-doc/contract/market/Get-Candle-Data + response = await self.publicMixGetV2MixMarketCandles(self.extend({'kLineType': priceType}, extended)) + elif priceType == 'mark': + response = await self.publicMixGetV2MixMarketHistoryMarkCandles(extended) + elif priceType == 'index': + response = await self.publicMixGetV2MixMarketHistoryIndexCandles(extended) + else: + if historicalEndpointNeeded: + response = await self.publicMixGetV2MixMarketHistoryCandles(extended) + else: + if not limitDefined: + extended['limit'] = 1000 + limit = 1000 + response = await self.publicMixGetV2MixMarketCandles(extended) + if response == '': + return [] # happens when a new token is listed + # [["1645911960000","39406","39407","39374.5","39379","35.526","1399132.341"]] + data = self.safe_list(response, 'data', response) + return self.parse_ohlcvs(data, market, timeframe, since, limit) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://www.bitget.com/api-doc/spot/account/Get-Account-Assets + https://www.bitget.com/api-doc/contract/account/Get-Account-List + https://www.bitget.com/api-doc/margin/cross/account/Get-Cross-Assets + https://www.bitget.com/api-doc/margin/isolated/account/Get-Isolated-Assets + https://bitgetlimited.github.io/apidoc/en/margin/#get-cross-assets + https://bitgetlimited.github.io/apidoc/en/margin/#get-isolated-assets + https://www.bitget.com/api-doc/uta/account/Get-Account + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.productType]: *contract only* 'USDT-FUTURES', 'USDC-FUTURES', 'COIN-FUTURES', 'SUSDT-FUTURES', 'SUSDC-FUTURES' or 'SCOIN-FUTURES' + :param str [params.uta]: set to True for the unified trading account(uta), defaults to False + :returns dict: a `balance structure ` + """ + await self.load_markets() + request: dict = {} + marketType = None + marginMode = None + response = None + uta = None + uta, params = self.handle_option_and_params(params, 'fetchBalance', 'uta', False) + marketType, params = self.handle_market_type_and_params('fetchBalance', None, params) + marginMode, params = self.handle_margin_mode_and_params('fetchBalance', params) + if uta: + response = await self.privateUtaGetV3AccountAssets(self.extend(request, params)) + results = self.safe_dict(response, 'data', {}) + assets = self.safe_list(results, 'assets', []) + return self.parse_uta_balance(assets) + elif (marketType == 'swap') or (marketType == 'future'): + productType = None + productType, params = self.handle_product_type_and_params(None, params) + request['productType'] = productType + response = await self.privateMixGetV2MixAccountAccounts(self.extend(request, params)) + elif marginMode == 'isolated': + response = await self.privateMarginGetV2MarginIsolatedAccountAssets(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": "1759829170717", + # "data": [ + # { + # "symbol": "BTCUSDT", + # "coin": "USDT", + # "totalAmount": "0.000001", + # "available": "0.000001", + # "frozen": "0", + # "borrow": "0", + # "interest": "0", + # "net": "0.000001", + # "coupon": "0", + # "cTime": "1759826434145", + # "uTime": "1759826434146" + # }, + # ] + # } + # + elif marginMode == 'cross': + response = await self.privateMarginGetV2MarginCrossedAccountAssets(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": "1759828519501", + # "data": [ + # { + # "coin": "USDT", + # "totalAmount": "0.01", + # "available": "0.01", + # "frozen": "0", + # "borrow": "0", + # "interest": "0", + # "net": "0.01", + # "coupon": "0", + # "cTime": "1759828511592", + # "uTime": "1759828511592" + # } + # ] + # } + # + elif marketType == 'spot': + response = await self.privateSpotGetV2SpotAccountAssets(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchBalance() does not support ' + marketType + ' accounts') + # spot + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700623852854, + # "data": [ + # { + # "coin": "USDT", + # "available": "0.00000000", + # "limitAvailable": "0", + # "frozen": "0.00000000", + # "locked": "0.00000000", + # "uTime": "1699937566000" + # } + # ] + # } + # + # swap + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700625127294, + # "data": [ + # { + # "marginCoin": "USDT", + # "locked": "0", + # "available": "0", + # "crossedMaxAvailable": "0", + # "isolatedMaxAvailable": "0", + # "maxTransferOut": "0", + # "accountEquity": "0", + # "usdtEquity": "0.000000005166", + # "btcEquity": "0", + # "crossedRiskRate": "0", + # "unrealizedPL": "0", + # "coupon": "0", + # "crossedUnrealizedPL": null, + # "isolatedUnrealizedPL": null + # } + # ] + # } + # + # uta + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1749980065089, + # "data": { + # "accountEquity": "11.13919278", + # "usdtEquity": "11.13921165", + # "btcEquity": "0.00011256", + # "unrealisedPnl": "0", + # "usdtUnrealisedPnl": "0", + # "btcUnrealizedPnl": "0", + # "effEquity": "6.19299777", + # "mmr": "0", + # "imr": "0", + # "mgnRatio": "0", + # "positionMgnRatio": "0", + # "assets": [ + # { + # "coin": "USDT", + # "equity": "6.19300826", + # "usdValue": "6.19299777", + # "balance": "6.19300826", + # "available": "6.19300826", + # "debt": "0", + # "locked": "0" + # } + # ] + # } + # } + # + data = self.safe_value(response, 'data', []) + return self.parse_balance(data) + + def parse_uta_balance(self, balance) -> Balances: + result: dict = {'info': balance} + # + # { + # "coin": "USDT", + # "equity": "6.19300826", + # "usdValue": "6.19299777", + # "balance": "6.19300826", + # "available": "6.19300826", + # "debt": "0", + # "locked": "0" + # } + # + for i in range(0, len(balance)): + entry = balance[i] + account = self.account() + currencyId = self.safe_string(entry, 'coin') + code = self.safe_currency_code(currencyId) + account['debt'] = self.safe_string(entry, 'debt') + account['used'] = self.safe_string(entry, 'locked') + account['free'] = self.safe_string(entry, 'available') + account['total'] = self.safe_string(entry, 'balance') + result[code] = account + return self.safe_balance(result) + + def parse_balance(self, balance) -> Balances: + result: dict = {'info': balance} + # + # spot + # + # { + # "coin": "USDT", + # "available": "0.00000000", + # "limitAvailable": "0", + # "frozen": "0.00000000", + # "locked": "0.00000000", + # "uTime": "1699937566000" + # } + # + # swap + # + # { + # "marginCoin": "USDT", + # "locked": "0", + # "available": "0", + # "crossedMaxAvailable": "0", + # "isolatedMaxAvailable": "0", + # "maxTransferOut": "0", + # "accountEquity": "0", + # "usdtEquity": "0.000000005166", + # "btcEquity": "0", + # "crossedRiskRate": "0", + # "unrealizedPL": "0", + # "coupon": "0", + # "crossedUnrealizedPL": null, + # "isolatedUnrealizedPL": null + # } + # + # cross & isolated margin + # + # { + # "coin": "USDT", + # "totalAmount": "0.01", + # "available": "0.01", + # "frozen": "0", + # "borrow": "0", + # "interest": "0", + # "net": "0.01", + # "coupon": "0", + # "cTime": "1759828511592", + # "uTime": "1759828511592" + # # "symbol": "BTCUSDT" # only for isolated margin + # } + # + for i in range(0, len(balance)): + entry = balance[i] + account = self.account() + currencyId = self.safe_string_2(entry, 'marginCoin', 'coin') + code = self.safe_currency_code(currencyId) + borrow = self.safe_string(entry, 'borrow') + if borrow is not None: + interest = self.safe_string(entry, 'interest') + account['free'] = self.safe_string(entry, 'transferable') + account['total'] = self.safe_string(entry, 'totalAmount') + account['debt'] = Precise.string_add(borrow, interest) + else: + # Use transferable instead of available for swap and margin https://github.com/ccxt/ccxt/pull/19127 + spotAccountFree = self.safe_string(entry, 'available') + contractAccountFree = self.safe_string(entry, 'maxTransferOut') + if contractAccountFree is not None: + account['free'] = contractAccountFree + account['total'] = self.safe_string(entry, 'accountEquity') + else: + account['free'] = spotAccountFree + frozen = self.safe_string(entry, 'frozen') + locked = self.safe_string(entry, 'locked') + account['used'] = Precise.string_add(frozen, locked) + result[code] = account + return self.safe_balance(result) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'new': 'open', + 'init': 'open', + 'not_trigger': 'open', + 'partial_fill': 'open', + 'partially_fill': 'open', + 'partially_filled': 'open', + 'triggered': 'closed', + 'full_fill': 'closed', + 'filled': 'closed', + 'fail_trigger': 'rejected', + 'cancel': 'canceled', + 'cancelled': 'canceled', + 'canceled': 'canceled', + 'live': 'open', + 'fail_execute': 'rejected', + 'executed': 'closed', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder, editOrder, closePosition + # + # { + # "clientOid": "abe95dbe-6081-4a6f-a2d3-ae49601cd479", + # "orderId": null + # } + # + # createOrders + # + # [ + # { + # "orderId": "1111397214281175046", + # "clientOid": "766d3fc3-7321-4406-a689-15c9987a2e75" + # }, + # { + # "orderId": "", + # "clientOid": "d1b75cb3-cc15-4ede-ad4c-3937396f75ab", + # "errorMsg": "less than the minimum amount 5 USDT", + # "errorCode": "45110" + # }, + # ] + # + # spot, swap, future, spot margin and uta: cancelOrder, cancelOrders, cancelAllOrders + # + # { + # "orderId": "1098758604547850241", + # "clientOid": "1098758604585598977" + # } + # + # spot trigger: cancelOrder + # + # { + # "result": "success" + # } + # + # spot: fetchOrder, fetchOpenOrders, fetchCanceledAndClosedOrders + # + # { + # "userId": "7264631750", + # "symbol": "BTCUSDT", + # "orderId": "1111499608327360513", + # "clientOid": "d0d4dad5-18d0-4869-a074-ec40bb47cba6", + # "size": "0.0002000000000000", # COST for 'buy market' order! AMOUNT in all other cases + # "price": "0", # in fetchOrder: 0 for market order, otherwise limit price(field not present in fetchOpenOrders + # "orderType": "limit", + # "side": "buy", + # "status": "live", + # "basePrice": "0", + # "priceAvg": "25000.0000000000000000", # 0 if nothing filled + # "baseVolume": "0.0000000000000000", # 0 if nothing filled + # "quoteVolume": "0.0000000000000000", # 0 if nothing filled + # "enterPointSource": "WEB", + # "orderSource": "normal", + # "cTime": "1700728077966", + # "uTime": "1700728077966" + # "feeDetail": "{\\"newFees\\":{\\"c\\":0,\\"d\\":0,\\"deduction\\":false,\\"r\\":-0.0064699886,\\"t\\":-0.0064699886,\\"totalDeductionFee\\":0},\\"USDT\\":{\\"deduction\\":false,\\"feeCoinCode\\":\\"USDT\\",\\"totalDeductionFee\\":0,\\"totalFee\\":-0.0064699886000000}}", # might not be present in fetchOpenOrders + # "triggerPrice": null, + # "tpslType": "normal", + # "quoteCoin": "USDT", # not present in fetchOpenOrders + # "baseCoin": "DOT", # not present in fetchOpenOrders + # "cancelReason": "", # not present in fetchOpenOrders + # } + # + # spot trigger: fetchOpenOrders, fetchCanceledAndClosedOrders + # + # { + # "orderId": "1111503385931620352", + # "clientOid": "1111503385910648832", + # "symbol": "BTCUSDT", + # "size": "0.0002", + # "planType": "AMOUNT", + # "executePrice": "25000", + # "triggerPrice": "26000", + # "status": "live", + # "orderType": "limit", + # "side": "buy", + # "triggerType": "fill_price", + # "enterPointSource": "API", + # "cTime": "1700728978617", + # "uTime": "1700728978617" + # } + # + # spot margin: fetchOpenOrders, fetchCanceledAndClosedOrders + # + # { + # "symbol": "BTCUSDT", + # "orderType": "limit", + # "enterPointSource": "WEB", + # "orderId": "1111506377509580801", + # "clientOid": "2043a3b59a60445f9d9f7365bf3e960c", + # "loanType": "autoLoanAndRepay", + # "price": "25000", + # "side": "buy", + # "status": "live", + # "baseSize": "0.0002", + # "quoteSize": "5", + # "priceAvg": "0", + # "size": "0", + # "amount": "0", + # "force": "gtc", + # "cTime": "1700729691866", + # "uTime": "1700729691866" + # } + # + # swap and future: fetchOrder, fetchOpenOrders, fetchCanceledAndClosedOrders + # + # { + # "symbol": "BTCUSDT", + # "size": "0.001", + # "orderId": "1111465253393825792", + # "clientOid": "1111465253431574529", + # "baseVolume": "0", + # "fee": "0", + # "price": "27000", + # "priceAvg": "", + # "state": "live", + # # "status": "live", # key for fetchOpenOrders, fetchClosedOrders + # "side": "buy", + # "force": "gtc", + # "totalProfits": "0", + # "posSide": "long", + # "marginCoin": "USDT", + # "quoteVolume": "0", + # "leverage": "20", + # "marginMode": "crossed", + # "enterPointSource": "API", + # "tradeSide": "open", + # "posMode": "hedge_mode", + # "orderType": "limit", + # "orderSource": "normal", + # "presetStopSurplusPrice": "", + # "presetStopLossPrice": "", + # "reduceOnly": "NO", + # "cTime": "1700719887120", + # "uTime": "1700719887120" + # + # for swap trigger order, the additional below fields are present: + # + # "planType": "normal_plan", + # "callbackRatio": "", + # "triggerPrice": "24000", + # "triggerType": "mark_price", + # "planStatus": "live", + # "stopSurplusTriggerPrice": "", + # "stopSurplusExecutePrice": "", + # "stopSurplusTriggerType": "fill_price", + # "stopLossTriggerPrice": "", + # "stopLossExecutePrice": "", + # "stopLossTriggerType": "fill_price", + # } + # + # uta: fetchOrder, fetchOpenOrders, fetchCanceledAndClosedOrders + # + # { + # "orderId": "1320244799629316096", + # "clientOid": "1320244799633510400", + # "category": "USDT-FUTURES", + # "symbol": "BTCUSDT", + # "orderType": "limit", + # "side": "buy", + # "price": "50000", + # "qty": "0.001", + # "amount": "0", + # "cumExecQty": "0", + # "cumExecValue": "0", + # "avgPrice": "0", + # "timeInForce": "gtc", + # "orderStatus": "live", + # "posSide": "long", + # "holdMode": "hedge_mode", + # "reduceOnly": "NO", + # "feeDetail": [{ + # "feeCoin": "", + # "fee": "" + # }], + # "createdTime": "1750496809871", + # "updatedTime": "1750496809886", + # "cancelReason": "", + # "execType": "normal", + # "stpMode": "none", + # "tpTriggerBy": null, + # "slTriggerBy": null, + # "takeProfit": null, + # "stopLoss": null, + # "tpOrderType": null, + # "slOrderType": null, + # "tpLimitPrice": null, + # "slLimitPrice": null + # } + # + # uta trigger: fetchClosedOrders, fetchCanceledOrders + # + # { + # "orderId": "1330984742276198400", + # "clientOid": "1330984742276198400", + # "symbol": "BTCUSDT", + # "category": "USDT-FUTURES", + # "qty": "0.001", + # "posSide": "long", + # "tpTriggerBy": "market", + # "slTriggerBy": "mark", + # "takeProfit": "", + # "stopLoss": "112000", + # "tpOrderType": "market", + # "slOrderType": "limit", + # "tpLimitPrice": "", + # "slLimitPrice": "111000", + # "createdTime": "1753057411736", + # "updatedTime": "1753058267412" + # } + # + errorMessage = self.safe_string(order, 'errorMsg') + if errorMessage is not None: + return self.safe_order({ + 'info': order, + 'id': self.safe_string(order, 'orderId'), + 'clientOrderId': self.safe_string_2(order, 'clientOrderId', 'clientOid'), + 'status': 'rejected', + }, market) + posSide = self.safe_string(order, 'posSide') + isContractOrder = (posSide is not None) + marketType = 'contract' if isContractOrder else 'spot' + if market is not None: + marketType = market['type'] + marketId = self.safe_string(order, 'symbol') + market = self.safe_market(marketId, market, None, marketType) + timestamp = self.safe_integer_n(order, ['cTime', 'ctime', 'createdTime']) + updateTimestamp = self.safe_integer_2(order, 'uTime', 'updatedTime') + rawStatus = self.safe_string_n(order, ['status', 'state', 'orderStatus', 'planStatus']) + fee = None + feeCostString = self.safe_string(order, 'fee') + if feeCostString is not None: + # swap + fee = { + 'cost': self.parse_number(Precise.string_neg(feeCostString)), + 'currency': market['settle'], + } + feeDetail = self.safe_value(order, 'feeDetail') + uta = self.safe_string(order, 'category') is not None + if uta: + feeResult = self.safe_dict(feeDetail, 0, {}) + utaFee = self.safe_string(feeResult, 'fee') + fee = { + 'cost': self.parse_number(Precise.string_neg(utaFee)), + 'currency': market['settle'], + } + else: + if feeDetail is not None: + parsedFeeDetail = json.loads(feeDetail) + feeValues = list(parsedFeeDetail.values()) + feeObject = None + for i in range(0, len(feeValues)): + feeValue = feeValues[i] + if self.safe_value(feeValue, 'feeCoinCode') is not None: + feeObject = feeValue + break + fee = { + 'cost': self.parse_number(Precise.string_neg(self.safe_string(feeObject, 'totalFee'))), + 'currency': self.safe_currency_code(self.safe_string(feeObject, 'feeCoinCode')), + } + postOnly = None + timeInForce = self.safe_string_upper_2(order, 'force', 'timeInForce') + if timeInForce == 'POST_ONLY': + postOnly = True + timeInForce = 'PO' + reduceOnly = None + reduceOnlyRaw = self.safe_string(order, 'reduceOnly') + if reduceOnlyRaw is not None: + reduceOnly = False if (reduceOnlyRaw == 'NO') else True + price = None + average = None + basePrice = self.safe_string(order, 'basePrice') + if basePrice is not None: + # for spot fetchOpenOrders, the price is priceAvg and the filled price is basePrice + price = self.safe_string(order, 'priceAvg') + average = self.safe_string(order, 'basePrice') + else: + price = self.safe_string_n(order, ['price', 'executePrice', 'slLimitPrice', 'tpLimitPrice']) + average = self.safe_string(order, 'priceAvg') + size = None + filled = None + baseSize = self.safe_string(order, 'baseSize') + if baseSize is not None: + # for spot margin fetchOpenOrders, the order size is baseSize and the filled amount is size + size = baseSize + filled = self.safe_string(order, 'size') + else: + size = self.safe_string_2(order, 'size', 'qty') + filled = self.safe_string_2(order, 'baseVolume', 'cumExecQty') + side = self.safe_string(order, 'side') + posMode = self.safe_string(order, 'posMode') + if posMode == 'hedge_mode' and reduceOnly: + side = 'sell' if (side == 'buy') else 'buy' + # on bitget hedge mode if the position is long the side is always buy, and if the position is short the side is always sell + # so the side of the reduceOnly order is inversed + orderType = self.safe_string(order, 'orderType') + isBuyMarket = (side == 'buy') and (orderType == 'market') + if market['spot'] and isBuyMarket: + # in top comment, for 'buy market' the 'size' field is COST, not AMOUNT + size = self.safe_string(order, 'baseVolume') + return self.safe_order({ + 'info': order, + 'id': self.safe_string_2(order, 'orderId', 'data'), + 'clientOrderId': self.safe_string_2(order, 'clientOrderId', 'clientOid'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': updateTimestamp, + 'lastUpdateTimestamp': updateTimestamp, + 'symbol': market['symbol'], + 'type': orderType, + 'side': side, + 'price': price, + 'amount': size, + 'cost': self.safe_string_2(order, 'quoteVolume', 'quoteSize'), + 'average': average, + 'filled': filled, + 'remaining': None, + 'timeInForce': timeInForce, + 'postOnly': postOnly, + 'reduceOnly': reduceOnly, + 'triggerPrice': self.safe_number(order, 'triggerPrice'), + 'takeProfitPrice': self.safe_number_n(order, ['presetStopSurplusPrice', 'stopSurplusTriggerPrice', 'takeProfit']), + 'stopLossPrice': self.safe_number_n(order, ['presetStopLossPrice', 'stopLossTriggerPrice', 'stopLoss']), + 'status': self.parse_order_status(rawStatus), + 'fee': fee, + 'trades': None, + }, market) + + async def create_market_buy_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market buy order by providing the symbol and cost + + https://www.bitget.com/api-doc/spot/trade/Place-Order + https://www.bitget.com/api-doc/margin/cross/trade/Cross-Place-Order + https://www.bitget.com/api-doc/margin/isolated/trade/Isolated-Place-Order + + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' createMarketBuyOrderWithCost() supports spot orders only') + req = { + 'createMarketBuyOrderRequiresPrice': False, + } + return await self.create_order(symbol, 'market', 'buy', cost, None, self.extend(req, params)) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://www.bitget.com/api-doc/spot/trade/Place-Order + https://www.bitget.com/api-doc/spot/plan/Place-Plan-Order + https://www.bitget.com/api-doc/contract/trade/Place-Order + https://www.bitget.com/api-doc/contract/plan/Place-Tpsl-Order + https://www.bitget.com/api-doc/contract/plan/Place-Plan-Order + https://www.bitget.com/api-doc/margin/cross/trade/Cross-Place-Order + https://www.bitget.com/api-doc/margin/isolated/trade/Isolated-Place-Order + https://www.bitget.com/api-doc/uta/trade/Place-Order + https://www.bitget.com/api-doc/uta/strategy/Place-Strategy-Order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.cost]: *spot only* how much you want to trade in units of the quote currency, for market buy orders only + :param float [params.triggerPrice]: *swap only* The price at which a trigger order is triggered at + :param float [params.stopLossPrice]: *swap only* The price at which a stop loss order is triggered at + :param float [params.takeProfitPrice]: *swap only* The price at which a take profit order is triggered at + :param dict [params.takeProfit]: *takeProfit object in params* containing the triggerPrice at which the attached take profit order will be triggered(perpetual swap markets only) + :param float [params.takeProfit.triggerPrice]: *swap only* take profit trigger price + :param dict [params.stopLoss]: *stopLoss object in params* containing the triggerPrice at which the attached stop loss order will be triggered(perpetual swap markets only) + :param float [params.stopLoss.triggerPrice]: *swap only* stop loss trigger price + :param str [params.timeInForce]: "GTC", "IOC", "FOK", or "PO" + :param str [params.marginMode]: 'isolated' or 'cross' for spot margin trading + :param str [params.loanType]: *spot margin only* 'normal', 'autoLoan', 'autoRepay', or 'autoLoanAndRepay' default is 'normal' + :param str [params.holdSide]: *contract stopLossPrice, takeProfitPrice only* Two-way position: ('long' or 'short'), one-way position: ('buy' or 'sell') + :param float [params.stopLoss.price]: *swap only* the execution price for a stop loss attached to a trigger order + :param float [params.takeProfit.price]: *swap only* the execution price for a take profit attached to a trigger order + :param str [params.stopLoss.type]: *swap only* the type for a stop loss attached to a trigger order, 'fill_price', 'index_price' or 'mark_price', default is 'mark_price' + :param str [params.takeProfit.type]: *swap only* the type for a take profit attached to a trigger order, 'fill_price', 'index_price' or 'mark_price', default is 'mark_price' + :param str [params.trailingPercent]: *swap and future only* the percent to trail away from the current market price, rate can not be greater than 10 + :param str [params.trailingTriggerPrice]: *swap and future only* the price to trigger a trailing stop order, default uses the price argument + :param str [params.triggerType]: *swap and future only* 'fill_price', 'mark_price' or 'index_price' + :param boolean [params.oneWayMode]: *swap and future only* required to set self to True in one_way_mode and you can leave self in hedge_mode, can adjust the mode using the setPositionMode() method + :param bool [params.hedged]: *swap and future only* True for hedged mode, False for one way mode, default is False + :param bool [params.reduceOnly]: True or False whether the order is reduce-only + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :param str [params.posSide]: *uta only* hedged two-way position side, long or short + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + marginParams = self.handle_margin_mode_and_params('createOrder', params) + marginMode = marginParams[0] + triggerPrice = self.safe_value_2(params, 'stopPrice', 'triggerPrice') + stopLossTriggerPrice = self.safe_value(params, 'stopLossPrice') + takeProfitTriggerPrice = self.safe_value(params, 'takeProfitPrice') + trailingPercent = self.safe_string_2(params, 'trailingPercent', 'callbackRatio') + isTrailingPercentOrder = trailingPercent is not None + isTriggerOrder = triggerPrice is not None + isStopLossTriggerOrder = stopLossTriggerPrice is not None + isTakeProfitTriggerOrder = takeProfitTriggerPrice is not None + isStopLossOrTakeProfitTrigger = isStopLossTriggerOrder or isTakeProfitTriggerOrder + response = None + uta = None + uta, params = self.handle_option_and_params(params, 'createOrder', 'uta', False) + if uta: + request = self.create_uta_order_request(symbol, type, side, amount, price, params) + if isStopLossOrTakeProfitTrigger: + response = await self.privateUtaPostV3TradePlaceStrategyOrder(request) + else: + response = await self.privateUtaPostV3TradePlaceOrder(request) + else: + request = self.create_order_request(symbol, type, side, amount, price, params) + if market['spot']: + if isTriggerOrder: + response = await self.privateSpotPostV2SpotTradePlacePlanOrder(request) + elif marginMode == 'isolated': + response = await self.privateMarginPostV2MarginIsolatedPlaceOrder(request) + elif marginMode == 'cross': + response = await self.privateMarginPostV2MarginCrossedPlaceOrder(request) + else: + response = await self.privateSpotPostV2SpotTradePlaceOrder(request) + else: + if isTriggerOrder or isTrailingPercentOrder: + response = await self.privateMixPostV2MixOrderPlacePlanOrder(request) + elif isStopLossOrTakeProfitTrigger: + response = await self.privateMixPostV2MixOrderPlaceTpslOrder(request) + else: + response = await self.privateMixPostV2MixOrderPlaceOrder(request) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1645932209602, + # "data": { + # "orderId": "881669078313766912", + # "clientOid": "iauIBf#a45b595f96474d888d0ada" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data, market) + + def create_uta_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + market = self.market(symbol) + productType = None + productType, params = self.handle_product_type_and_params(market, params) + if productType == 'SPOT': + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('createOrder', params) + if marginMode is not None: + productType = 'MARGIN' + request: dict = { + 'category': productType, + 'symbol': market['id'], + 'qty': self.amount_to_precision(symbol, amount), + 'side': side, + } + clientOrderId = self.safe_string_2(params, 'clientOid', 'clientOrderId') + if clientOrderId is not None: + request['clientOid'] = clientOrderId + params = self.omit(params, 'clientOrderId') + stopLossTriggerPrice = self.safe_number(params, 'stopLossPrice') + takeProfitTriggerPrice = self.safe_number(params, 'takeProfitPrice') + stopLoss = self.safe_value(params, 'stopLoss') + takeProfit = self.safe_value(params, 'takeProfit') + isStopLoss = stopLoss is not None + isTakeProfit = takeProfit is not None + isStopLossTrigger = stopLossTriggerPrice is not None + isTakeProfitTrigger = takeProfitTriggerPrice is not None + isStopLossOrTakeProfitTrigger = isStopLossTrigger or isTakeProfitTrigger + if isStopLossOrTakeProfitTrigger: + if isStopLossTrigger: + slType = self.safe_string(params, 'slTriggerBy', 'mark') + request['slTriggerBy'] = slType + request['stopLoss'] = self.price_to_precision(symbol, stopLossTriggerPrice) + if price is not None: + request['slLimitPrice'] = self.price_to_precision(symbol, price) + request['slOrderType'] = self.safe_string(params, 'slOrderType', 'limit') + else: + request['slOrderType'] = self.safe_string(params, 'slOrderType', 'market') + elif isTakeProfitTrigger: + tpType = self.safe_string(params, 'tpTriggerBy', 'mark') + request['tpTriggerBy'] = tpType + request['takeProfit'] = self.price_to_precision(symbol, takeProfitTriggerPrice) + if price is not None: + request['tpLimitPrice'] = self.price_to_precision(symbol, price) + request['tpOrderType'] = self.safe_string(params, 'tpOrderType', 'limit') + else: + request['tpOrderType'] = self.safe_string(params, 'tpOrderType', 'market') + params = self.omit(params, ['stopLossPrice', 'takeProfitPrice']) + else: + if isStopLoss: + slTriggerPrice = self.safe_number_2(stopLoss, 'triggerPrice', 'stopPrice') + slLimitPrice = self.safe_number(stopLoss, 'price') + request['stopLoss'] = self.price_to_precision(symbol, slTriggerPrice) + if slLimitPrice is not None: + request['slLimitPrice'] = self.price_to_precision(symbol, slLimitPrice) + request['slOrderType'] = self.safe_string(params, 'slOrderType', 'limit') + else: + request['slOrderType'] = self.safe_string(params, 'slOrderType', 'market') + if isTakeProfit: + tpTriggerPrice = self.safe_number_2(takeProfit, 'triggerPrice', 'stopPrice') + tpLimitPrice = self.safe_number(takeProfit, 'price') + request['takeProfit'] = self.price_to_precision(symbol, tpTriggerPrice) + if tpLimitPrice is not None: + request['tpLimitPrice'] = self.price_to_precision(symbol, tpLimitPrice) + request['tpOrderType'] = self.safe_string(params, 'tpOrderType', 'limit') + else: + request['tpOrderType'] = self.safe_string(params, 'tpOrderType', 'market') + isMarketOrder = type == 'market' + if not isMarketOrder: + request['price'] = self.price_to_precision(symbol, price) + request['orderType'] = type + exchangeSpecificTifParam = self.safe_string(params, 'timeInForce') + postOnly = None + postOnly, params = self.handle_post_only(isMarketOrder, exchangeSpecificTifParam == 'post_only', params) + defaultTimeInForce = self.safe_string_upper(self.options, 'defaultTimeInForce') + timeInForce = self.safe_string_upper(params, 'timeInForce', defaultTimeInForce) + if postOnly: + request['timeInForce'] = 'post_only' + elif timeInForce == 'GTC': + request['timeInForce'] = 'gtc' + elif timeInForce == 'FOK': + request['timeInForce'] = 'fok' + elif timeInForce == 'IOC': + request['timeInForce'] = 'ioc' + reduceOnly = self.safe_bool(params, 'reduceOnly', False) + hedged = None + hedged, params = self.handle_param_bool(params, 'hedged', False) + if reduceOnly: + if hedged or isStopLossOrTakeProfitTrigger: + reduceOnlyPosSide = 'long' if (side == 'sell') else 'short' + request['posSide'] = reduceOnlyPosSide + elif not isStopLossOrTakeProfitTrigger: + request['reduceOnly'] = 'yes' + else: + if hedged: + posSide = 'long' if (side == 'buy') else 'short' + request['posSide'] = posSide + params = self.omit(params, ['stopLoss', 'takeProfit', 'postOnly', 'reduceOnly', 'hedged']) + return self.extend(request, params) + + def create_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + market = self.market(symbol) + marketType = None + marginMode = None + marketType, params = self.handle_market_type_and_params('createOrder', market, params) + marginMode, params = self.handle_margin_mode_and_params('createOrder', params) + request: dict = { + 'symbol': market['id'], + 'orderType': type, + } + hedged = None + hedged, params = self.handle_param_bool(params, 'hedged', False) + # backward compatibility for `oneWayMode` + oneWayMode = None + oneWayMode, params = self.handle_param_bool(params, 'oneWayMode') + if oneWayMode is not None: + hedged = not oneWayMode + isMarketOrder = type == 'market' + triggerPrice = self.safe_value_2(params, 'stopPrice', 'triggerPrice') + stopLossTriggerPrice = self.safe_value(params, 'stopLossPrice') + takeProfitTriggerPrice = self.safe_value(params, 'takeProfitPrice') + stopLoss = self.safe_value(params, 'stopLoss') + takeProfit = self.safe_value(params, 'takeProfit') + isTriggerOrder = triggerPrice is not None + isStopLossTriggerOrder = stopLossTriggerPrice is not None + isTakeProfitTriggerOrder = takeProfitTriggerPrice is not None + isStopLoss = stopLoss is not None + isTakeProfit = takeProfit is not None + isStopLossOrTakeProfitTrigger = isStopLossTriggerOrder or isTakeProfitTriggerOrder + isStopLossOrTakeProfit = isStopLoss or isTakeProfit + trailingTriggerPrice = self.safe_string(params, 'trailingTriggerPrice', self.number_to_string(price)) + trailingPercent = self.safe_string_2(params, 'trailingPercent', 'callbackRatio') + isTrailingPercentOrder = trailingPercent is not None + if self.sum(isTriggerOrder, isStopLossTriggerOrder, isTakeProfitTriggerOrder, isTrailingPercentOrder) > 1: + raise ExchangeError(self.id + ' createOrder() params can only contain one of triggerPrice, stopLossPrice, takeProfitPrice, trailingPercent') + if type == 'limit': + request['price'] = self.price_to_precision(symbol, price) + triggerPriceType = self.safe_string_2(params, 'triggerPriceType', 'triggerType', 'mark_price') + reduceOnly = self.safe_bool(params, 'reduceOnly', False) + clientOrderId = self.safe_string_2(params, 'clientOid', 'clientOrderId') + exchangeSpecificTifParam = self.safe_string_2(params, 'force', 'timeInForce') + postOnly = None + postOnly, params = self.handle_post_only(isMarketOrder, exchangeSpecificTifParam == 'post_only', params) + defaultTimeInForce = self.safe_string_upper(self.options, 'defaultTimeInForce') + timeInForce = self.safe_string_upper(params, 'timeInForce', defaultTimeInForce) + if postOnly: + request['force'] = 'post_only' + elif timeInForce == 'GTC': + request['force'] = 'GTC' + elif timeInForce == 'FOK': + request['force'] = 'FOK' + elif timeInForce == 'IOC': + request['force'] = 'IOC' + params = self.omit(params, ['stopPrice', 'triggerType', 'stopLossPrice', 'takeProfitPrice', 'stopLoss', 'takeProfit', 'postOnly', 'reduceOnly', 'clientOrderId', 'trailingPercent', 'trailingTriggerPrice']) + if (marketType == 'swap') or (marketType == 'future'): + request['marginCoin'] = market['settleId'] + request['size'] = self.amount_to_precision(symbol, amount) + productType = None + productType, params = self.handle_product_type_and_params(market, params) + request['productType'] = productType + if clientOrderId is not None: + request['clientOid'] = clientOrderId + if isTriggerOrder or isStopLossOrTakeProfitTrigger or isTrailingPercentOrder: + request['triggerType'] = triggerPriceType + if isTrailingPercentOrder: + if not isMarketOrder: + raise BadRequest(self.id + ' createOrder() bitget trailing orders must be market orders') + if trailingTriggerPrice is None: + raise ArgumentsRequired(self.id + ' createOrder() bitget trailing orders must have a trailingTriggerPrice param') + request['planType'] = 'track_plan' + request['triggerPrice'] = self.price_to_precision(symbol, trailingTriggerPrice) + request['callbackRatio'] = trailingPercent + elif isTriggerOrder: + request['planType'] = 'normal_plan' + request['triggerPrice'] = self.price_to_precision(symbol, triggerPrice) + if price is not None: + request['executePrice'] = self.price_to_precision(symbol, price) + if isStopLoss: + slTriggerPrice = self.safe_string_2(stopLoss, 'triggerPrice', 'stopPrice') + request['stopLossTriggerPrice'] = self.price_to_precision(symbol, slTriggerPrice) + slPrice = self.safe_string(stopLoss, 'price') + request['stopLossExecutePrice'] = self.price_to_precision(symbol, slPrice) + slType = self.safe_string(stopLoss, 'type', 'mark_price') + request['stopLossTriggerType'] = slType + if isTakeProfit: + tpTriggerPrice = self.safe_string_2(takeProfit, 'triggerPrice', 'stopPrice') + request['stopSurplusTriggerPrice'] = self.price_to_precision(symbol, tpTriggerPrice) + tpPrice = self.safe_string(takeProfit, 'price') + request['stopSurplusExecutePrice'] = self.price_to_precision(symbol, tpPrice) + tpType = self.safe_string(takeProfit, 'type', 'mark_price') + request['stopSurplusTriggerType'] = tpType + elif isStopLossOrTakeProfitTrigger: + if not isMarketOrder: + raise ExchangeError(self.id + ' createOrder() bitget stopLoss or takeProfit orders must be market orders') + if hedged: + request['holdSide'] = 'long' if (side == 'sell') else 'short' + else: + request['holdSide'] = 'buy' if (side == 'sell') else 'sell' + if isStopLossTriggerOrder: + request['triggerPrice'] = self.price_to_precision(symbol, stopLossTriggerPrice) + request['planType'] = 'pos_loss' + elif isTakeProfitTriggerOrder: + request['triggerPrice'] = self.price_to_precision(symbol, takeProfitTriggerPrice) + request['planType'] = 'pos_profit' + else: + if isStopLoss: + slTriggerPrice = self.safe_value_2(stopLoss, 'triggerPrice', 'stopPrice') + request['presetStopLossPrice'] = self.price_to_precision(symbol, slTriggerPrice) + if isTakeProfit: + tpTriggerPrice = self.safe_value_2(takeProfit, 'triggerPrice', 'stopPrice') + request['presetStopSurplusPrice'] = self.price_to_precision(symbol, tpTriggerPrice) + if not isStopLossOrTakeProfitTrigger: + if marginMode is None: + marginMode = 'cross' + marginModeRequest = 'crossed' if (marginMode == 'cross') else 'isolated' + request['marginMode'] = marginModeRequest + requestSide = side + if reduceOnly: + if not hedged: + request['reduceOnly'] = 'YES' + else: + # on bitget hedge mode if the position is long the side is always buy, and if the position is short the side is always sell + requestSide = 'sell' if (side == 'buy') else 'buy' + request['tradeSide'] = 'Close' + else: + if hedged: + request['tradeSide'] = 'Open' + request['side'] = requestSide + elif marketType == 'spot': + if isStopLossOrTakeProfitTrigger or isStopLossOrTakeProfit: + raise InvalidOrder(self.id + ' createOrder() does not support stop loss/take profit orders on spot markets, only swap markets') + request['side'] = side + quantity = None + planType = None + createMarketBuyOrderRequiresPrice = True + createMarketBuyOrderRequiresPrice, params = self.handle_option_and_params(params, 'createOrder', 'createMarketBuyOrderRequiresPrice', True) + if isMarketOrder and (side == 'buy'): + planType = 'total' + cost = self.safe_number(params, 'cost') + params = self.omit(params, 'cost') + if cost is not None: + quantity = self.cost_to_precision(symbol, cost) + elif createMarketBuyOrderRequiresPrice: + if price is None: + raise InvalidOrder(self.id + ' createOrder() requires the price argument for market buy orders to calculate the total cost to spend(amount * price), alternatively set the createMarketBuyOrderRequiresPrice option or param to False and pass the cost to spend in the amount argument') + else: + amountString = self.number_to_string(amount) + priceString = self.number_to_string(price) + quoteAmount = Precise.string_mul(amountString, priceString) + quantity = self.cost_to_precision(symbol, quoteAmount) + else: + quantity = self.cost_to_precision(symbol, amount) + else: + planType = 'amount' + quantity = self.amount_to_precision(symbol, amount) + if clientOrderId is not None: + request['clientOid'] = clientOrderId + if marginMode is not None: + request['loanType'] = 'normal' + if isMarketOrder and (side == 'buy'): + request['quoteSize'] = quantity + else: + request['baseSize'] = quantity + else: + if quantity is not None: + request['size'] = quantity + if triggerPrice is not None: + request['planType'] = planType + request['triggerType'] = triggerPriceType + request['triggerPrice'] = self.price_to_precision(symbol, triggerPrice) + if price is not None: + request['executePrice'] = self.price_to_precision(symbol, price) + else: + raise NotSupported(self.id + ' createOrder() does not support ' + marketType + ' orders') + return self.extend(request, params) + + async def create_uta_orders(self, orders: List[OrderRequest], params={}): + await self.load_markets() + ordersRequests = [] + symbol = None + marginMode = None + for i in range(0, len(orders)): + rawOrder = orders[i] + marketId = self.safe_string(rawOrder, 'symbol') + if symbol is None: + symbol = marketId + else: + if symbol != marketId: + raise BadRequest(self.id + ' createOrders() requires all orders to have the same symbol') + type = self.safe_string(rawOrder, 'type') + side = self.safe_string(rawOrder, 'side') + amount = self.safe_value(rawOrder, 'amount') + price = self.safe_value(rawOrder, 'price') + orderParams = self.safe_value(rawOrder, 'params', {}) + marginResult = self.handle_margin_mode_and_params('createOrders', orderParams) + currentMarginMode = marginResult[0] + if currentMarginMode is not None: + if marginMode is None: + marginMode = currentMarginMode + else: + if marginMode != currentMarginMode: + raise BadRequest(self.id + ' createOrders() requires all orders to have the same margin mode(isolated or cross)') + orderRequest = self.create_uta_order_request(marketId, type, side, amount, price, orderParams) + ordersRequests.append(orderRequest) + market = self.market(symbol) + response = await self.privateUtaPostV3TradePlaceBatch(ordersRequests) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1752810184560, + # "data": [ + # { + # "orderId": "1329947796441513984", + # "clientOid": "1329947796483457024" + # }, + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_orders(data, market) + + async def create_orders(self, orders: List[OrderRequest], params={}): + """ + create a list of trade orders(all orders should be of the same symbol) + + https://www.bitget.com/api-doc/spot/trade/Batch-Place-Orders + https://www.bitget.com/api-doc/contract/trade/Batch-Order + https://www.bitget.com/api-doc/margin/isolated/trade/Isolated-Batch-Order + https://www.bitget.com/api-doc/margin/cross/trade/Cross-Batch-Order + https://www.bitget.com/api-doc/uta/trade/Place-Batch + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the api endpoint + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :returns dict: an `order structure ` + """ + await self.load_markets() + uta = None + uta, params = self.handle_option_and_params(params, 'createOrders', 'uta', False) + if uta: + return await self.create_uta_orders(orders, params) + ordersRequests = [] + symbol = None + marginMode = None + for i in range(0, len(orders)): + rawOrder = orders[i] + marketId = self.safe_string(rawOrder, 'symbol') + if symbol is None: + symbol = marketId + else: + if symbol != marketId: + raise BadRequest(self.id + ' createOrders() requires all orders to have the same symbol') + type = self.safe_string(rawOrder, 'type') + side = self.safe_string(rawOrder, 'side') + amount = self.safe_value(rawOrder, 'amount') + price = self.safe_value(rawOrder, 'price') + orderParams = self.safe_value(rawOrder, 'params', {}) + marginResult = self.handle_margin_mode_and_params('createOrders', orderParams) + currentMarginMode = marginResult[0] + if currentMarginMode is not None: + if marginMode is None: + marginMode = currentMarginMode + else: + if marginMode != currentMarginMode: + raise BadRequest(self.id + ' createOrders() requires all orders to have the same margin mode(isolated or cross)') + orderRequest = self.create_order_request(marketId, type, side, amount, price, orderParams) + ordersRequests.append(orderRequest) + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'orderList': ordersRequests, + } + response = None + if (market['swap']) or (market['future']): + if marginMode is None: + marginMode = 'cross' + marginModeRequest = 'crossed' if (marginMode == 'cross') else 'isolated' + request['marginMode'] = marginModeRequest + request['marginCoin'] = market['settleId'] + productType = None + productType, params = self.handle_product_type_and_params(market, params) + request['productType'] = productType + response = await self.privateMixPostV2MixOrderBatchPlaceOrder(request) + elif marginMode == 'isolated': + response = await self.privateMarginPostV2MarginIsolatedBatchPlaceOrder(request) + elif marginMode == 'cross': + response = await self.privateMarginPostV2MarginCrossedBatchPlaceOrder(request) + else: + response = await self.privateSpotPostV2SpotTradeBatchOrders(request) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700703539416, + # "data": { + # "successList": [ + # { + # "orderId": "1111397214281175046", + # "clientOid": "766d3fc3-7321-4406-a689-15c9987a2e75" + # }, + # ], + # "failureList": [ + # { + # "orderId": "", + # "clientOid": "d1b75cb3-cc15-4ede-ad4c-3937396f75ab", + # "errorMsg": "less than the minimum amount 5 USDT", + # "errorCode": "45110" + # }, + # ] + # } + # } + # + data = self.safe_value(response, 'data', {}) + failure = self.safe_value(data, 'failureList', []) + orderInfo = self.safe_value(data, 'successList', []) + both = self.array_concat(orderInfo, failure) + return self.parse_orders(both, market) + + async def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + """ + edit a trade order + + https://www.bitget.com/api-doc/spot/plan/Modify-Plan-Order + https://www.bitget.com/api-doc/spot/trade/Cancel-Replace-Order + https://www.bitget.com/api-doc/contract/trade/Modify-Order + https://www.bitget.com/api-doc/contract/plan/Modify-Tpsl-Order + https://www.bitget.com/api-doc/contract/plan/Modify-Plan-Order + https://www.bitget.com/api-doc/uta/trade/Modify-Order + https://www.bitget.com/api-doc/uta/strategy/Modify-Strategy-Order + + :param str id: cancel order id + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: the price that a trigger order is triggered at + :param float [params.stopLossPrice]: *swap only* The price at which a stop loss order is triggered at + :param float [params.takeProfitPrice]: *swap only* The price at which a take profit order is triggered at + :param dict [params.takeProfit]: *takeProfit object in params* containing the triggerPrice at which the attached take profit order will be triggered(perpetual swap markets only) + :param float [params.takeProfit.triggerPrice]: *swap only* take profit trigger price + :param dict [params.stopLoss]: *stopLoss object in params* containing the triggerPrice at which the attached stop loss order will be triggered(perpetual swap markets only) + :param float [params.stopLoss.triggerPrice]: *swap only* stop loss trigger price + :param float [params.stopLoss.price]: *swap only* the execution price for a stop loss attached to a trigger order + :param float [params.takeProfit.price]: *swap only* the execution price for a take profit attached to a trigger order + :param str [params.stopLoss.type]: *swap only* the type for a stop loss attached to a trigger order, 'fill_price', 'index_price' or 'mark_price', default is 'mark_price' + :param str [params.takeProfit.type]: *swap only* the type for a take profit attached to a trigger order, 'fill_price', 'index_price' or 'mark_price', default is 'mark_price' + :param str [params.trailingPercent]: *swap and future only* the percent to trail away from the current market price, rate can not be greater than 10 + :param str [params.trailingTriggerPrice]: *swap and future only* the price to trigger a trailing stop order, default uses the price argument + :param str [params.newTriggerType]: *swap and future only* 'fill_price', 'mark_price' or 'index_price' + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + # 'orderId': id, + } + clientOrderId = self.safe_string_2(params, 'clientOrderId', 'clientOid') + if clientOrderId is not None: + params = self.omit(params, ['clientOrderId']) + request['clientOid'] = clientOrderId + else: + request['orderId'] = id + isMarketOrder = type == 'market' + triggerPrice = self.safe_value_2(params, 'stopPrice', 'triggerPrice') + isTriggerOrder = triggerPrice is not None + stopLossPrice = self.safe_value(params, 'stopLossPrice') + isStopLossOrder = stopLossPrice is not None + takeProfitPrice = self.safe_value(params, 'takeProfitPrice') + isTakeProfitOrder = takeProfitPrice is not None + stopLoss = self.safe_value(params, 'stopLoss') + takeProfit = self.safe_value(params, 'takeProfit') + isStopLoss = stopLoss is not None + isTakeProfit = takeProfit is not None + trailingTriggerPrice = self.safe_string(params, 'trailingTriggerPrice', self.number_to_string(price)) + trailingPercent = self.safe_string_2(params, 'trailingPercent', 'newCallbackRatio') + isTrailingPercentOrder = trailingPercent is not None + if self.sum(isTriggerOrder, isStopLossOrder, isTakeProfitOrder, isTrailingPercentOrder) > 1: + raise ExchangeError(self.id + ' editOrder() params can only contain one of triggerPrice, stopLossPrice, takeProfitPrice, trailingPercent') + params = self.omit(params, ['stopPrice', 'triggerType', 'stopLossPrice', 'takeProfitPrice', 'stopLoss', 'takeProfit', 'clientOrderId', 'trailingTriggerPrice', 'trailingPercent']) + response = None + productType = None + uta = None + productType, params = self.handle_product_type_and_params(market, params) + uta, params = self.handle_option_and_params(params, 'editOrder', 'uta', False) + if uta: + if amount is not None: + request['qty'] = self.amount_to_precision(symbol, amount) + if isStopLossOrder or isTakeProfitOrder: + if isStopLossOrder: + slType = self.safe_string(params, 'slTriggerBy', 'mark') + request['slTriggerBy'] = slType + request['stopLoss'] = self.price_to_precision(symbol, stopLossPrice) + if price is not None: + request['slLimitPrice'] = self.price_to_precision(symbol, price) + request['slOrderType'] = self.safe_string(params, 'slOrderType', 'limit') + else: + request['slOrderType'] = self.safe_string(params, 'slOrderType', 'market') + elif isTakeProfitOrder: + tpType = self.safe_string(params, 'tpTriggerBy', 'mark') + request['tpTriggerBy'] = tpType + request['takeProfit'] = self.price_to_precision(symbol, takeProfitPrice) + if price is not None: + request['tpLimitPrice'] = self.price_to_precision(symbol, price) + request['tpOrderType'] = self.safe_string(params, 'tpOrderType', 'limit') + else: + request['tpOrderType'] = self.safe_string(params, 'tpOrderType', 'market') + params = self.omit(params, ['stopLossPrice', 'takeProfitPrice']) + response = await self.privateUtaPostV3TradeModifyStrategyOrder(self.extend(request, params)) + else: + if price is not None: + request['price'] = self.price_to_precision(symbol, price) + response = await self.privateUtaPostV3TradeModifyOrder(self.extend(request, params)) + elif market['spot']: + cost = self.safe_string(params, 'cost') + params = self.omit(params, 'cost') + editMarketBuyOrderRequiresPrice = self.safe_bool(self.options, 'editMarketBuyOrderRequiresPrice', True) + if (editMarketBuyOrderRequiresPrice or (cost is not None)) and isMarketOrder and (side == 'buy'): + if price is None and cost is None: + raise InvalidOrder(self.id + ' editOrder() requires price argument for market buy orders on spot markets to calculate the total amount to spend(amount * price), alternatively provide `cost` in the params') + else: + amountString = self.number_to_string(amount) + priceString = self.number_to_string(price) + finalCost = (Precise.string_mul(amountString, priceString)) if (cost is None) else cost + request['size'] = self.price_to_precision(symbol, finalCost) + else: + request['size'] = self.amount_to_precision(symbol, amount) + request['orderType'] = type + if triggerPrice is not None: + request['triggerPrice'] = self.price_to_precision(symbol, triggerPrice) + request['executePrice'] = self.price_to_precision(symbol, price) + else: + request['price'] = self.price_to_precision(symbol, price) + if triggerPrice is not None: + response = await self.privateSpotPostV2SpotTradeModifyPlanOrder(self.extend(request, params)) + else: + request['symbol'] = market['id'] + response = await self.privateSpotPostV2SpotTradeCancelReplaceOrder(self.extend(request, params)) + else: + if (not market['swap']) and (not market['future']): + raise NotSupported(self.id + ' editOrder() does not support ' + market['type'] + ' orders') + request['symbol'] = market['id'] + request['productType'] = productType + if not isTakeProfitOrder and not isStopLossOrder: + request['newSize'] = self.amount_to_precision(symbol, amount) + if (price is not None) and not isTrailingPercentOrder: + request['newPrice'] = self.price_to_precision(symbol, price) + if isTrailingPercentOrder: + if not isMarketOrder: + raise BadRequest(self.id + ' editOrder() bitget trailing orders must be market orders') + if trailingTriggerPrice is not None: + request['newTriggerPrice'] = self.price_to_precision(symbol, trailingTriggerPrice) + request['newCallbackRatio'] = trailingPercent + response = await self.privateMixPostV2MixOrderModifyPlanOrder(self.extend(request, params)) + elif isTakeProfitOrder or isStopLossOrder: + request['marginCoin'] = market['settleId'] + request['size'] = self.amount_to_precision(symbol, amount) + if price is not None: + request['executePrice'] = self.price_to_precision(symbol, price) + if isStopLossOrder: + request['triggerPrice'] = self.price_to_precision(symbol, stopLossPrice) + elif isTakeProfitOrder: + request['triggerPrice'] = self.price_to_precision(symbol, takeProfitPrice) + response = await self.privateMixPostV2MixOrderModifyTpslOrder(self.extend(request, params)) + elif isTriggerOrder: + request['newTriggerPrice'] = self.price_to_precision(symbol, triggerPrice) + if isStopLoss: + slTriggerPrice = self.safe_number_2(stopLoss, 'triggerPrice', 'stopPrice') + request['newStopLossTriggerPrice'] = self.price_to_precision(symbol, slTriggerPrice) + slPrice = self.safe_number(stopLoss, 'price') + request['newStopLossExecutePrice'] = self.price_to_precision(symbol, slPrice) + slType = self.safe_string(stopLoss, 'type', 'mark_price') + request['newStopLossTriggerType'] = slType + if isTakeProfit: + tpTriggerPrice = self.safe_number_2(takeProfit, 'triggerPrice', 'stopPrice') + request['newSurplusTriggerPrice'] = self.price_to_precision(symbol, tpTriggerPrice) + tpPrice = self.safe_number(takeProfit, 'price') + request['newStopSurplusExecutePrice'] = self.price_to_precision(symbol, tpPrice) + tpType = self.safe_string(takeProfit, 'type', 'mark_price') + request['newStopSurplusTriggerType'] = tpType + response = await self.privateMixPostV2MixOrderModifyPlanOrder(self.extend(request, params)) + else: + defaultNewClientOrderId = self.uuid() + newClientOrderId = self.safe_string_2(params, 'newClientOid', 'newClientOrderId', defaultNewClientOrderId) + params = self.omit(params, 'newClientOrderId') + request['newClientOid'] = newClientOrderId + if isStopLoss: + slTriggerPrice = self.safe_value_2(stopLoss, 'triggerPrice', 'stopPrice') + request['newPresetStopLossPrice'] = self.price_to_precision(symbol, slTriggerPrice) + if isTakeProfit: + tpTriggerPrice = self.safe_value_2(takeProfit, 'triggerPrice', 'stopPrice') + request['newPresetStopSurplusPrice'] = self.price_to_precision(symbol, tpTriggerPrice) + response = await self.privateMixPostV2MixOrderModifyOrder(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700708275737, + # "data": { + # "clientOid": "abe95dbe-6081-4a6f-a2d3-ae49601cd459", + # "orderId": null + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data, market) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://www.bitget.com/api-doc/spot/trade/Cancel-Order + https://www.bitget.com/api-doc/spot/plan/Cancel-Plan-Order + https://www.bitget.com/api-doc/contract/trade/Cancel-Order + https://www.bitget.com/api-doc/contract/plan/Cancel-Plan-Order + https://www.bitget.com/api-doc/margin/cross/trade/Cross-Cancel-Order + https://www.bitget.com/api-doc/margin/isolated/trade/Isolated-Cancel-Order + https://www.bitget.com/api-doc/uta/trade/Cancel-Order + https://www.bitget.com/api-doc/uta/strategy/Cancel-Strategy-Order + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'isolated' or 'cross' for spot margin trading + :param boolean [params.trigger]: set to True for canceling trigger orders + :param str [params.planType]: *swap only* either profit_plan, loss_plan, normal_plan, pos_profit, pos_loss, moving_plan or track_plan + :param boolean [params.trailing]: set to True if you want to cancel a trailing order + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :param str [params.clientOrderId]: the clientOrderId of the order, id does not need to be provided if clientOrderId is provided + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + marginMode = None + response = None + marginMode, params = self.handle_margin_mode_and_params('cancelOrder', params) + request: dict = {} + trailing = self.safe_value(params, 'trailing') + trigger = self.safe_value_2(params, 'stop', 'trigger') + params = self.omit(params, ['stop', 'trigger', 'trailing']) + if not (market['spot'] and trigger): + request['symbol'] = market['id'] + uta = None + uta, params = self.handle_option_and_params(params, 'cancelOrder', 'uta', False) + isPlanOrder = trigger or trailing + isContract = market['swap'] or market['future'] + isContractTriggerEndpoint = isContract and isPlanOrder and not uta + clientOrderId = self.safe_string_2(params, 'clientOrderId', 'clientOid') + if isContractTriggerEndpoint: + orderIdList = [] + orderId: dict = {} + if clientOrderId is not None: + params = self.omit(params, 'clientOrderId') + orderId['clientOid'] = clientOrderId + else: + orderId['orderId'] = id + orderIdList.append(orderId) + request['orderIdList'] = orderIdList + else: + if clientOrderId is not None: + params = self.omit(params, 'clientOrderId') + request['clientOid'] = clientOrderId + else: + request['orderId'] = id + if uta: + if trigger: + response = await self.privateUtaPostV3TradeCancelStrategyOrder(self.extend(request, params)) + else: + response = await self.privateUtaPostV3TradeCancelOrder(self.extend(request, params)) + elif (market['swap']) or (market['future']): + productType = None + productType, params = self.handle_product_type_and_params(market, params) + request['productType'] = productType + if trailing: + planType = self.safe_string(params, 'planType', 'track_plan') + request['planType'] = planType + response = await self.privateMixPostV2MixOrderCancelPlanOrder(self.extend(request, params)) + elif trigger: + response = await self.privateMixPostV2MixOrderCancelPlanOrder(self.extend(request, params)) + else: + response = await self.privateMixPostV2MixOrderCancelOrder(self.extend(request, params)) + elif market['spot']: + if marginMode is not None: + if marginMode == 'isolated': + response = await self.privateMarginPostV2MarginIsolatedCancelOrder(self.extend(request, params)) + elif marginMode == 'cross': + response = await self.privateMarginPostV2MarginCrossedCancelOrder(self.extend(request, params)) + else: + if trigger: + response = await self.privateSpotPostV2SpotTradeCancelPlanOrder(self.extend(request, params)) + else: + response = await self.privateSpotPostV2SpotTradeCancelOrder(self.extend(request, params)) + else: + raise NotSupported(self.id + ' cancelOrder() does not support ' + market['type'] + ' orders') + # + # spot, swap, future and spot margin + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1697690413177, + # "data": { + # "orderId": "1098758604547850241", + # "clientOid": "1098758604585598977" + # } + # } + # + # swap trigger + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700711311791, + # "data": { + # "successList": [ + # { + # "clientOid": "1111428059067125760", + # "orderId": "1111428059067125761" + # } + # ], + # "failureList": [] + # } + # } + # + # spot trigger + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700711728063, + # "data": { + # "result": "success" + # } + # } + # + # uta trigger + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": "1753058267399", + # "data": null + # } + # + data = self.safe_value(response, 'data', {}) + order = None + if isContractTriggerEndpoint: + orderInfo = self.safe_value(data, 'successList', []) + order = orderInfo[0] + else: + if uta and trigger: + order = response + else: + order = data + return self.parse_order(order, market) + + async def cancel_uta_orders(self, ids, symbol: Str = None, params={}): + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrders() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + productType = None + productType, params = self.handle_product_type_and_params(market, params) + requestList = [] + for i in range(0, len(ids)): + individualId = ids[i] + order: dict = { + 'orderId': individualId, + 'symbol': market['id'], + 'category': productType, + } + requestList.append(order) + response = await self.privateUtaPostV3TradeCancelBatch(requestList) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1752813731517, + # "data": [ + # { + # "orderId": "1329948909442023424", + # "clientOid": "1329948909446217728" + # }, + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_orders(data, market) + + async def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): + """ + cancel multiple orders + + https://www.bitget.com/api-doc/spot/trade/Batch-Cancel-Orders + https://www.bitget.com/api-doc/contract/trade/Batch-Cancel-Orders + https://www.bitget.com/api-doc/contract/plan/Cancel-Plan-Order + https://www.bitget.com/api-doc/margin/cross/trade/Cross-Batch-Cancel-Order + https://www.bitget.com/api-doc/margin/isolated/trade/Isolated-Batch-Cancel-Orders + https://www.bitget.com/api-doc/uta/trade/Cancel-Batch + + :param str[] ids: order ids + :param str symbol: unified market symbol, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'isolated' or 'cross' for spot margin trading + :param boolean [params.trigger]: *contract only* set to True for canceling trigger orders + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :returns dict: an array of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrders() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + uta = None + uta, params = self.handle_option_and_params(params, 'cancelOrders', 'uta', False) + if uta: + return await self.cancel_uta_orders(ids, symbol, params) + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('cancelOrders', params) + trigger = self.safe_value_2(params, 'stop', 'trigger') + params = self.omit(params, ['stop', 'trigger']) + orderIdList = [] + for i in range(0, len(ids)): + individualId = ids[i] + orderId: dict = { + 'orderId': individualId, + } + orderIdList.append(orderId) + request: dict = { + 'symbol': market['id'], + } + if market['spot'] and (marginMode is None): + request['orderList'] = orderIdList + else: + request['orderIdList'] = orderIdList + response = None + if market['spot']: + if marginMode is not None: + if marginMode == 'cross': + response = await self.privateMarginPostV2MarginCrossedBatchCancelOrder(self.extend(request, params)) + else: + response = await self.privateMarginPostV2MarginIsolatedBatchCancelOrder(self.extend(request, params)) + else: + response = await self.privateSpotPostV2SpotTradeBatchCancelOrder(self.extend(request, params)) + else: + productType = None + productType, params = self.handle_product_type_and_params(market, params) + request['productType'] = productType + if trigger: + response = await self.privateMixPostV2MixOrderCancelPlanOrder(self.extend(request, params)) + else: + response = await self.privateMixPostV2MixOrderBatchCancelOrders(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": "1680008815965", + # "data": { + # "successList": [ + # { + # "orderId": "1024598257429823488", + # "clientOid": "876493ce-c287-4bfc-9f4a-8b1905881313" + # }, + # ], + # "failureList": [] + # } + # } + # + data = self.safe_value(response, 'data', {}) + orders = self.safe_list(data, 'successList', []) + return self.parse_orders(orders, market) + + async def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders + + https://www.bitget.com/api-doc/spot/trade/Cancel-Symbol-Orders + https://www.bitget.com/api-doc/spot/plan/Batch-Cancel-Plan-Order + https://www.bitget.com/api-doc/contract/trade/Batch-Cancel-Orders + https://www.bitget.com/api-doc/margin/cross/trade/Cross-Batch-Cancel-Order + https://www.bitget.com/api-doc/margin/isolated/trade/Isolated-Batch-Cancel-Orders + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'isolated' or 'cross' for spot margin trading + :param boolean [params.trigger]: *contract only* set to True for canceling trigger orders + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :returns dict[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelAllOrders() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('cancelAllOrders', params) + productType = None + productType, params = self.handle_product_type_and_params(market, params) + request: dict = { + 'symbol': market['id'], + } + trigger = self.safe_bool_2(params, 'stop', 'trigger') + params = self.omit(params, ['stop', 'trigger']) + response = None + uta = None + uta, params = self.handle_option_and_params(params, 'cancelAllOrders', 'uta', False) + if uta: + if productType == 'SPOT': + if marginMode is not None: + productType = 'MARGIN' + request['category'] = productType + response = await self.privateUtaPostV3TradeCancelSymbolOrder(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1750751578138, + # "data": { + # "list": [ + # { + # "orderId": "1321313242969427968", + # "clientOid": "1321313242969427969" + # } + # ] + # } + # } + # + elif market['spot']: + if marginMode is not None: + raise NotSupported(self.id + ' cancelAllOrders() does not support margin markets, you can use cancelOrders() instead') + else: + if trigger: + stopRequest: dict = { + 'symbolList': [market['id']], + } + response = await self.privateSpotPostV2SpotTradeBatchCancelPlanOrder(self.extend(stopRequest, params)) + else: + response = await self.privateSpotPostV2SpotTradeCancelSymbolOrder(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700716953996, + # "data": { + # "symbol": "BTCUSDT" + # } + # } + # + timestamp = self.safe_integer(response, 'requestTime') + responseData = self.safe_dict(response, 'data') + marketId = self.safe_string(responseData, 'symbol') + return [ + self.safe_order({ + 'info': response, + 'symbol': self.safe_symbol(marketId, None, None, 'spot'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }), + ] + else: + request['productType'] = productType + if trigger: + response = await self.privateMixPostV2MixOrderCancelPlanOrder(self.extend(request, params)) + else: + response = await self.privateMixPostV2MixOrderBatchCancelOrders(self.extend(request, params)) + # { + # "code": "00000", + # "msg": "success", + # "requestTime": "1680008815965", + # "data": { + # "successList": [ + # { + # "orderId": "1024598257429823488", + # "clientOid": "876493ce-c287-4bfc-9f4a-8b1905881313" + # }, + # ], + # "failureList": [] + # } + # } + data = self.safe_dict(response, 'data') + resultList = self.safe_list_n(data, ['resultList', 'successList', 'list']) + failureList = self.safe_list_2(data, 'failure', 'failureList') + responseList = None + if (resultList is not None) and (failureList is not None): + responseList = self.array_concat(resultList, failureList) + else: + responseList = resultList + return self.parse_orders(responseList) + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://www.bitget.com/api-doc/spot/trade/Get-Order-Info + https://www.bitget.com/api-doc/contract/trade/Get-Order-Details + https://www.bitget.com/api-doc/uta/trade/Get-Order-Details + + :param str id: the order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :param str [params.clientOrderId]: the clientOrderId of the order, id does not need to be provided if clientOrderId is provided + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrder() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + # 'orderId': id, + } + clientOrderId = self.safe_string_2(params, 'clientOrderId', 'clientOid') + if clientOrderId is not None: + params = self.omit(params, ['clientOrderId']) + request['clientOid'] = clientOrderId + else: + request['orderId'] = id + response = None + uta = None + uta, params = self.handle_option_and_params(params, 'fetchOrder', 'uta', False) + if uta: + response = await self.privateUtaGetV3TradeOrderInfo(self.extend(request, params)) + elif market['spot']: + response = await self.privateSpotGetV2SpotTradeOrderInfo(self.extend(request, params)) + elif market['swap'] or market['future']: + request['symbol'] = market['id'] + productType = None + productType, params = self.handle_product_type_and_params(market, params) + request['productType'] = productType + response = await self.privateMixGetV2MixOrderDetail(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchOrder() does not support ' + market['type'] + ' orders') + # + # spot + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700719076263, + # "data": [ + # { + # "userId": "7264631750", + # "symbol": "BTCUSDT", + # "orderId": "1111461743123927040", + # "clientOid": "63f95110-93b5-4309-8f77-46339f1bcf3c", + # "price": "25000.0000000000000000", + # "size": "0.0002000000000000", + # "orderType": "limit", + # "side": "buy", + # "status": "live", + # "priceAvg": "0", + # "baseVolume": "0.0000000000000000", + # "quoteVolume": "0.0000000000000000", + # "enterPointSource": "API", + # "feeDetail": "", + # "orderSource": "normal", + # "cTime": "1700719050198", + # "uTime": "1700719050198" + # } + # ] + # } + # + # swap and future + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700719918781, + # "data": { + # "symbol": "BTCUSDT", + # "size": "0.001", + # "orderId": "1111465253393825792", + # "clientOid": "1111465253431574529", + # "baseVolume": "0", + # "fee": "0", + # "price": "27000", + # "priceAvg": "", + # "state": "live", + # "side": "buy", + # "force": "gtc", + # "totalProfits": "0", + # "posSide": "long", + # "marginCoin": "USDT", + # "presetStopSurplusPrice": "", + # "presetStopLossPrice": "", + # "quoteVolume": "0", + # "orderType": "limit", + # "leverage": "20", + # "marginMode": "crossed", + # "reduceOnly": "NO", + # "enterPointSource": "API", + # "tradeSide": "open", + # "posMode": "hedge_mode", + # "orderSource": "normal", + # "cTime": "1700719887120", + # "uTime": "1700719887120" + # } + # } + # + # uta + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1750496858333, + # "data": { + # "orderId": "1320244799629316096", + # "clientOid": "1320244799633510400", + # "category": "USDT-FUTURES", + # "symbol": "BTCUSDT", + # "orderType": "limit", + # "side": "buy", + # "price": "50000", + # "qty": "0.001", + # "amount": "0", + # "cumExecQty": "0", + # "cumExecValue": "0", + # "avgPrice": "0", + # "timeInForce": "gtc", + # "orderStatus": "live", + # "posSide": "long", + # "holdMode": "hedge_mode", + # "reduceOnly": "NO", + # "feeDetail": [{ + # "feeCoin": "", + # "fee": "" + # }], + # "createdTime": "1750496809871", + # "updatedTime": "1750496809886", + # "cancelReason": "", + # "execType": "normal", + # "stpMode": "none", + # "tpTriggerBy": null, + # "slTriggerBy": null, + # "takeProfit": null, + # "stopLoss": null, + # "tpOrderType": null, + # "slOrderType": null, + # "tpLimitPrice": null, + # "slLimitPrice": null + # } + # } + # + if not uta and (isinstance(response, str)): + response = json.loads(response) + data = self.safe_dict(response, 'data') + if (data is not None): + if not isinstance(data, list): + return self.parse_order(data, market) + dataList = self.safe_list(response, 'data', []) + dataListLength = len(dataList) + if dataListLength == 0: + raise OrderNotFound(self.id + ' fetchOrder() could not find order id ' + id + ' in ' + self.json(response)) + first = self.safe_dict(dataList, 0, {}) + return self.parse_order(first, market) + # first = self.safe_dict(data, 0, data) + # return self.parse_order(first, market) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://www.bitget.com/api-doc/spot/trade/Get-Unfilled-Orders + https://www.bitget.com/api-doc/spot/plan/Get-Current-Plan-Order + https://www.bitget.com/api-doc/contract/trade/Get-Orders-Pending + https://www.bitget.com/api-doc/contract/plan/get-orders-plan-pending + https://www.bitget.com/api-doc/margin/cross/trade/Get-Cross-Open-Orders + https://www.bitget.com/api-doc/margin/isolated/trade/Isolated-Open-Orders + https://www.bitget.com/api-doc/uta/strategy/Get-Unfilled-Strategy-Orders + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch orders for + :param str [params.planType]: *contract stop only* 'normal_plan': average trigger order, 'profit_loss': opened tp/sl orders, 'track_plan': trailing stop order, default is 'normal_plan' + :param boolean [params.trigger]: set to True for fetching trigger orders + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param str [params.isPlan]: *swap only* 'plan' for stop orders and 'profit_loss' for tp/sl orders, default is 'plan' + :param boolean [params.trailing]: set to True if you want to fetch trailing orders + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + market = None + type = None + request: dict = {} + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchOpenOrders', params) + uta = None + uta, params = self.handle_option_and_params(params, 'fetchOpenOrders', 'uta', False) + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + defaultType = self.safe_string_2(self.options, 'fetchOpenOrders', 'defaultType', 'spot') + marketType = market['type'] if ('type' in market) else defaultType + type = self.safe_string(params, 'type', marketType) + else: + defaultType = self.safe_string_2(self.options, 'fetchOpenOrders', 'defaultType', 'spot') + type = self.safe_string(params, 'type', defaultType) + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOpenOrders', 'paginate') + if paginate: + cursorReceived = None + cursorSent = None + if uta: + cursorReceived = 'cursor' + cursorSent = 'cursor' + elif type == 'spot': + if marginMode is not None: + cursorReceived = 'minId' + cursorSent = 'idLessThan' + else: + cursorReceived = 'endId' + cursorSent = 'idLessThan' + return await self.fetch_paginated_call_cursor('fetchOpenOrders', symbol, since, limit, params, cursorReceived, cursorSent) + response = None + trailing = self.safe_bool(params, 'trailing') + trigger = self.safe_bool_2(params, 'stop', 'trigger') + planTypeDefined = self.safe_string(params, 'planType') is not None + isTrigger = (trigger or planTypeDefined) + request, params = self.handle_until_option('endTime', request, params) + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + if not uta and ((type == 'swap') or (type == 'future') or (marginMode is not None)): + clientOrderId = self.safe_string_2(params, 'clientOid', 'clientOrderId') + params = self.omit(params, 'clientOrderId') + if clientOrderId is not None: + request['clientOid'] = clientOrderId + productType = None + productType, params = self.handle_product_type_and_params(market, params) + params = self.omit(params, ['type', 'stop', 'trigger', 'trailing']) + if uta: + if type == 'spot': + if marginMode is not None: + productType = 'MARGIN' + request['category'] = productType + if trigger: + response = await self.privateUtaGetV3TradeUnfilledStrategyOrders(self.extend(request, params)) + else: + response = await self.privateUtaGetV3TradeUnfilledOrders(self.extend(request, params)) + elif type == 'spot': + if marginMode is not None: + if since is None: + since = self.milliseconds() - 7776000000 + request['startTime'] = since + if marginMode == 'isolated': + response = await self.privateMarginGetV2MarginIsolatedOpenOrders(self.extend(request, params)) + elif marginMode == 'cross': + response = await self.privateMarginGetV2MarginCrossedOpenOrders(self.extend(request, params)) + else: + if trigger: + response = await self.privateSpotGetV2SpotTradeCurrentPlanOrder(self.extend(request, params)) + else: + response = await self.privateSpotGetV2SpotTradeUnfilledOrders(self.extend(request, params)) + else: + request['productType'] = productType + if trailing: + planType = self.safe_string(params, 'planType', 'track_plan') + request['planType'] = planType + response = await self.privateMixGetV2MixOrderOrdersPlanPending(self.extend(request, params)) + elif isTrigger: + planType = self.safe_string(params, 'planType', 'normal_plan') + request['planType'] = planType + response = await self.privateMixGetV2MixOrderOrdersPlanPending(self.extend(request, params)) + else: + response = await self.privateMixGetV2MixOrderOrdersPending(self.extend(request, params)) + # + # spot + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700728123994, + # "data": [ + # { + # "userId": "7264631750", + # "symbol": "BTCUSDT", + # "orderId": "1111499608327360513", + # "clientOid": "d0d4dad5-18d0-4869-a074-ec40bb47cba6", + # "priceAvg": "25000.0000000000000000", + # "size": "0.0002000000000000", + # "orderType": "limit", + # "side": "buy", + # "status": "live", + # "basePrice": "0", + # "baseVolume": "0.0000000000000000", + # "quoteVolume": "0.0000000000000000", + # "enterPointSource": "WEB", + # "orderSource": "normal", + # "cTime": "1700728077966", + # "uTime": "1700728077966" + # } + # ] + # } + # + # spot stop + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700729361609, + # "data": { + # "nextFlag": False, + # "idLessThan": "1111503385931620352", + # "orderList": [ + # { + # "orderId": "1111503385931620352", + # "clientOid": "1111503385910648832", + # "symbol": "BTCUSDT", + # "size": "0.0002", + # "planType": "AMOUNT", + # "executePrice": "25000", + # "triggerPrice": "26000", + # "status": "live", + # "orderType": "limit", + # "side": "buy", + # "triggerType": "fill_price", + # "enterPointSource": "API", + # "cTime": "1700728978617", + # "uTime": "1700728978617" + # } + # ] + # } + # } + # + # spot margin + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700729887686, + # "data": { + # "orderList": [ + # { + # "symbol": "BTCUSDT", + # "orderType": "limit", + # "enterPointSource": "WEB", + # "orderId": "1111506377509580801", + # "clientOid": "2043a3b59a60445f9d9f7365bf3e960c", + # "loanType": "autoLoanAndRepay", + # "price": "25000", + # "side": "buy", + # "status": "live", + # "baseSize": "0.0002", + # "quoteSize": "5", + # "priceAvg": "0", + # "size": "0", + # "amount": "0", + # "force": "gtc", + # "cTime": "1700729691866", + # "uTime": "1700729691866" + # } + # ], + # "maxId": "1111506377509580801", + # "minId": "1111506377509580801" + # } + # } + # + # swap and future + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700725609065, + # "data": { + # "entrustedList": [ + # { + # "symbol": "BTCUSDT", + # "size": "0.002", + # "orderId": "1111488897767604224", + # "clientOid": "1111488897805352960", + # "baseVolume": "0", + # "fee": "0", + # "price": "25000", + # "priceAvg": "", + # "status": "live", + # "side": "buy", + # "force": "gtc", + # "totalProfits": "0", + # "posSide": "long", + # "marginCoin": "USDT", + # "quoteVolume": "0", + # "leverage": "20", + # "marginMode": "crossed", + # "enterPointSource": "web", + # "tradeSide": "open", + # "posMode": "hedge_mode", + # "orderType": "limit", + # "orderSource": "normal", + # "presetStopSurplusPrice": "", + # "presetStopLossPrice": "", + # "reduceOnly": "NO", + # "cTime": "1700725524378", + # "uTime": "1700725524378" + # } + # ], + # "endId": "1111488897767604224" + # } + # } + # + # swap and future stop + # + # { + # "code": "00000",\ + # "msg": "success", + # "requestTime": 1700726417495, + # "data": { + # "entrustedList": [ + # { + # "planType": "normal_plan", + # "symbol": "BTCUSDT", + # "size": "0.001", + # "orderId": "1111491399869075457", + # "clientOid": "1111491399869075456", + # "price": "27000", + # "callbackRatio": "", + # "triggerPrice": "24000", + # "triggerType": "mark_price", + # "planStatus": "live", + # "side": "buy", + # "posSide": "long", + # "marginCoin": "USDT", + # "marginMode": "crossed", + # "enterPointSource": "API", + # "tradeSide": "open", + # "posMode": "hedge_mode", + # "orderType": "limit", + # "stopSurplusTriggerPrice": "", + # "stopSurplusExecutePrice": "", + # "stopSurplusTriggerType": "fill_price", + # "stopLossTriggerPrice": "", + # "stopLossExecutePrice": "", + # "stopLossTriggerType": "fill_price", + # "cTime": "1700726120917", + # "uTime": "1700726120917" + # } + # ], + # "endId": "1111491399869075457" + # } + # } + # + # uta + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1750753395850, + # "data": { + # "list": [ + # { + # "orderId": "1321320757371228160", + # "clientOid": "1321320757371228161", + # "category": "USDT-FUTURES", + # "symbol": "BTCUSDT", + # "orderType": "limit", + # "side": "buy", + # "price": "50000", + # "qty": "0.001", + # "amount": "0", + # "cumExecQty": "0", + # "cumExecValue": "0", + # "avgPrice": "0", + # "timeInForce": "gtc", + # "orderStatus": "live", + # "posSide": "long", + # "holdMode": "hedge_mode", + # "reduceOnly": "NO", + # "feeDetail": [ + # { + # "feeCoin": "", + # "fee": "" + # } + # ], + # "createdTime": "1750753338186", + # "updatedTime": "1750753338203", + # "stpMode": "none", + # "tpTriggerBy": null, + # "slTriggerBy": null, + # "takeProfit": null, + # "stopLoss": null, + # "tpOrderType": null, + # "slOrderType": null, + # "tpLimitPrice": null, + # "slLimitPrice": null + # } + # ], + # "cursor": "1321320757371228160" + # } + # } + # + # uta trigger + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1753057527060, + # "data": [ + # { + # "orderId": "1330984742276198400", + # "clientOid": "1330984742276198400", + # "symbol": "BTCUSDT", + # "category": "USDT-FUTURES", + # "qty": "0.001", + # "posSide": "long", + # "tpTriggerBy": "market", + # "slTriggerBy": "mark", + # "takeProfit": "", + # "stopLoss":"114000", + # "tpOrderType": "market", + # "slOrderType": "limit", + # "tpLimitPrice": "", + # "slLimitPrice": "113000", + # "createdTime": "1753057411736", + # "updatedTime": "1753057411747" + # } + # ] + # } + # + data = self.safe_value(response, 'data') + if uta: + result = None + if trigger: + result = self.safe_list(response, 'data', []) + else: + result = self.safe_list(data, 'list', []) + return self.parse_orders(result, market, since, limit) + elif type == 'spot': + if (marginMode is not None) or trigger: + resultList = self.safe_list(data, 'orderList', []) + return self.parse_orders(resultList, market, since, limit) + else: + result = self.safe_list(data, 'entrustedList', []) + return self.parse_orders(result, market, since, limit) + return self.parse_orders(data, market, since, limit) + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://www.bitget.com/api-doc/spot/trade/Get-History-Orders + https://www.bitget.com/api-doc/spot/plan/Get-History-Plan-Order + https://www.bitget.com/api-doc/contract/trade/Get-Orders-History + https://www.bitget.com/api-doc/contract/plan/orders-plan-history + https://www.bitget.com/api-doc/margin/cross/trade/Get-Cross-Order-History + https://www.bitget.com/api-doc/margin/isolated/trade/Get-Isolated-Order-History + https://www.bitget.com/api-doc/uta/trade/Get-Order-History + + :param str symbol: unified market symbol of the closed orders + :param int [since]: timestamp in ms of the earliest order + :param int [limit]: the max number of closed orders to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch orders for + :param str [params.planType]: *contract stop only* 'normal_plan': average trigger order, 'profit_loss': opened tp/sl orders, 'track_plan': trailing stop order, default is 'normal_plan' + :param boolean [params.trigger]: set to True for fetching trigger orders + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param str [params.isPlan]: *swap only* 'plan' for stop orders and 'profit_loss' for tp/sl orders, default is 'plan' + :param boolean [params.trailing]: set to True if you want to fetch trailing orders + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + orders = await self.fetch_canceled_and_closed_orders(symbol, since, limit, params) + return self.filter_by(orders, 'status', 'closed') + + async def fetch_canceled_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches information on multiple canceled orders made by the user + + https://www.bitget.com/api-doc/spot/trade/Get-History-Orders + https://www.bitget.com/api-doc/spot/plan/Get-History-Plan-Order + https://www.bitget.com/api-doc/contract/trade/Get-Orders-History + https://www.bitget.com/api-doc/contract/plan/orders-plan-history + https://www.bitget.com/api-doc/margin/cross/trade/Get-Cross-Order-History + https://www.bitget.com/api-doc/margin/isolated/trade/Get-Isolated-Order-History + https://www.bitget.com/api-doc/uta/trade/Get-Order-History + + :param str symbol: unified market symbol of the canceled orders + :param int [since]: timestamp in ms of the earliest order + :param int [limit]: the max number of canceled orders to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch orders for + :param str [params.planType]: *contract stop only* 'normal_plan': average trigger order, 'profit_loss': opened tp/sl orders, 'track_plan': trailing stop order, default is 'normal_plan' + :param boolean [params.trigger]: set to True for fetching trigger orders + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param str [params.isPlan]: *swap only* 'plan' for stop orders and 'profit_loss' for tp/sl orders, default is 'plan' + :param boolean [params.trailing]: set to True if you want to fetch trailing orders + :returns dict: a list of `order structures ` + """ + await self.load_markets() + orders = await self.fetch_canceled_and_closed_orders(symbol, since, limit, params) + return self.filter_by(orders, 'status', 'canceled') + + async def fetch_canceled_and_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + + https://www.bitget.com/api-doc/spot/trade/Get-History-Orders + https://www.bitget.com/api-doc/spot/plan/Get-History-Plan-Order + https://www.bitget.com/api-doc/contract/trade/Get-Orders-History + https://www.bitget.com/api-doc/contract/plan/orders-plan-history + https://www.bitget.com/api-doc/margin/cross/trade/Get-Cross-Order-History + https://www.bitget.com/api-doc/margin/isolated/trade/Get-Isolated-Order-History + https://www.bitget.com/api-doc/uta/trade/Get-Order-History + https://www.bitget.com/api-doc/uta/strategy/Get-History-Strategy-Orders + + fetches information on multiple canceled and closed orders made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch orders for + :param str [params.planType]: *contract stop only* 'normal_plan': average trigger order, 'profit_loss': opened tp/sl orders, 'track_plan': trailing stop order, default is 'normal_plan' + :param boolean [params.trigger]: set to True for fetching trigger orders + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param str [params.isPlan]: *swap only* 'plan' for stop orders and 'profit_loss' for tp/sl orders, default is 'plan' + :param boolean [params.trailing]: set to True if you want to fetch trailing orders + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :returns Order[]: a list of `order structures ` + """ + uta = None + uta, params = self.handle_option_and_params(params, 'fetchCanceledAndClosedOrders', 'uta', False) + if uta: + return await self.fetch_uta_canceled_and_closed_orders(symbol, since, limit, params) + await self.load_markets() + market = None + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + marketType = None + marketType, params = self.handle_market_type_and_params('fetchCanceledAndClosedOrders', market, params) + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchCanceledAndClosedOrders', params) + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchCanceledAndClosedOrders', 'paginate') + if paginate: + cursorReceived = None + if marketType == 'spot': + if marginMode is not None: + cursorReceived = 'minId' + else: + cursorReceived = 'endId' + return await self.fetch_paginated_call_cursor('fetchCanceledAndClosedOrders', symbol, since, limit, params, cursorReceived, 'idLessThan') + response = None + trailing = self.safe_bool(params, 'trailing') + trigger = self.safe_bool_2(params, 'stop', 'trigger') + params = self.omit(params, ['stop', 'trigger', 'trailing']) + request, params = self.handle_until_option('endTime', request, params) + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + if (marketType == 'swap') or (marketType == 'future') or (marginMode is not None): + clientOrderId = self.safe_string_2(params, 'clientOid', 'clientOrderId') + params = self.omit(params, 'clientOrderId') + if clientOrderId is not None: + request['clientOid'] = clientOrderId + now = self.milliseconds() + if marketType == 'spot': + if marginMode is not None: + if since is None: + since = now - 7776000000 + request['startTime'] = since + if marginMode == 'isolated': + response = await self.privateMarginGetV2MarginIsolatedHistoryOrders(self.extend(request, params)) + elif marginMode == 'cross': + response = await self.privateMarginGetV2MarginCrossedHistoryOrders(self.extend(request, params)) + elif trigger: + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchCanceledAndClosedOrders() requires a symbol argument') + endTime = self.safe_integer_n(params, ['endTime', 'until']) + params = self.omit(params, ['until']) + if since is None: + since = now - 7776000000 + request['startTime'] = since + if endTime is None: + request['endTime'] = now + response = await self.privateSpotGetV2SpotTradeHistoryPlanOrder(self.extend(request, params)) + else: + response = await self.privateSpotGetV2SpotTradeHistoryOrders(self.extend(request, params)) + else: + productType = None + productType, params = self.handle_product_type_and_params(market, params) + request['productType'] = productType + planTypeDefined = self.safe_string(params, 'planType') is not None + if trailing: + planType = self.safe_string(params, 'planType', 'track_plan') + request['planType'] = planType + response = await self.privateMixGetV2MixOrderOrdersPlanHistory(self.extend(request, params)) + elif trigger or planTypeDefined: + planType = self.safe_string(params, 'planType', 'normal_plan') + request['planType'] = planType + response = await self.privateMixGetV2MixOrderOrdersPlanHistory(self.extend(request, params)) + else: + response = await self.privateMixGetV2MixOrderOrdersHistory(self.extend(request, params)) + # + # spot + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700791085380, + # "data": [ + # { + # "userId": "7264631750", + # "symbol": "BTCUSDT", + # "orderId": "1111499608327360513", + # "clientOid": "d0d4dad5-18d0-4869-a074-ec40bb47cba6", + # "price": "25000.0000000000000000", + # "size": "0.0002000000000000", + # "orderType": "limit", + # "side": "buy", + # "status": "cancelled", + # "priceAvg": "0", + # "baseVolume": "0.0000000000000000", + # "quoteVolume": "0.0000000000000000", + # "enterPointSource": "WEB", + # "feeDetail": "", + # "orderSource": "normal", + # "cTime": "1700728077966", + # "uTime": "1700728911471" + # }, + # ] + # } + # + # spot stop + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700792099146, + # "data": { + # "nextFlag": False, + # "idLessThan": "1098757597417775104", + # "orderList": [ + # { + # "orderId": "1111503385931620352", + # "clientOid": "1111503385910648832", + # "symbol": "BTCUSDT", + # "size": "0.0002", + # "planType": "AMOUNT", + # "executePrice": "25000", + # "triggerPrice": "26000", + # "status": "cancelled", + # "orderType": "limit", + # "side": "buy", + # "triggerType": "fill_price", + # "enterPointSource": "API", + # "cTime": "1700728978617", + # "uTime": "1700729666868" + # }, + # ] + # } + # } + # + # spot margin + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700792381435, + # "data": { + # "orderList": [ + # { + # "symbol": "BTCUSDT", + # "orderType": "limit", + # "enterPointSource": "WEB", + # "orderId": "1111456274707001345", + # "clientOid": "41e428dd305a4f668671b7f1ed00dc50", + # "loanType": "autoLoanAndRepay", + # "price": "27000", + # "side": "buy", + # "status": "cancelled", + # "baseSize": "0.0002", + # "quoteSize": "5.4", + # "priceAvg": "0", + # "size": "0", + # "amount": "0", + # "force": "gtc", + # "cTime": "1700717746427", + # "uTime": "1700717780636" + # }, + # ], + # "maxId": "1111456274707001345", + # "minId": "1098396464990269440" + # } + # } + # + # swap and future + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700792674673, + # "data": { + # "entrustedList": [ + # { + # "symbol": "BTCUSDT", + # "size": "0.002", + # "orderId": "1111498800817143808", + # "clientOid": "1111498800850698240", + # "baseVolume": "0", + # "fee": "0", + # "price": "25000", + # "priceAvg": "", + # "status": "canceled", + # "side": "buy", + # "force": "gtc", + # "totalProfits": "0", + # "posSide": "long", + # "marginCoin": "USDT", + # "quoteVolume": "0", + # "leverage": "20", + # "marginMode": "crossed", + # "enterPointSource": "web", + # "tradeSide": "open", + # "posMode": "hedge_mode", + # "orderType": "limit", + # "orderSource": "normal", + # "presetStopSurplusPrice": "", + # "presetStopLossPrice": "", + # "reduceOnly": "NO", + # "cTime": "1700727885449", + # "uTime": "1700727944563" + # }, + # ], + # "endId": "1098397008323575809" + # } + # } + # + # swap and future stop + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700792938359, + # "data": { + # "entrustedList": [ + # { + # "planType": "normal_plan", + # "symbol": "BTCUSDT", + # "size": "0.001", + # "orderId": "1111491399869075457", + # "clientOid": "1111491399869075456", + # "planStatus": "cancelled", + # "price": "27000", + # "feeDetail": null, + # "baseVolume": "0", + # "callbackRatio": "", + # "triggerPrice": "24000", + # "triggerType": "mark_price", + # "side": "buy", + # "posSide": "long", + # "marginCoin": "USDT", + # "marginMode": "crossed", + # "enterPointSource": "API", + # "tradeSide": "open", + # "posMode": "hedge_mode", + # "orderType": "limit", + # "stopSurplusTriggerPrice": "", + # "stopSurplusExecutePrice": "", + # "stopSurplusTriggerType": "fill_price", + # "stopLossTriggerPrice": "", + # "stopLossExecutePrice": "", + # "stopLossTriggerType": "fill_price", + # "cTime": "1700726120917", + # "uTime": "1700727879652" + # }, + # ], + # "endId": "1098760007867502593" + # } + # } + # + data = self.safe_value(response, 'data', {}) + if marketType == 'spot': + if (marginMode is not None) or trigger: + return self.parse_orders(self.safe_value(data, 'orderList', []), market, since, limit) + else: + return self.parse_orders(self.safe_value(data, 'entrustedList', []), market, since, limit) + if isinstance(response, str): + response = json.loads(response) + orders = self.safe_list(response, 'data', []) + return self.parse_orders(orders, market, since, limit) + + async def fetch_uta_canceled_and_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + productType = None + productType, params = self.handle_product_type_and_params(market, params) + if productType == 'SPOT': + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchCanceledAndClosedOrders', params) + if marginMode is not None: + productType = 'MARGIN' + request: dict = { + 'category': productType, + } + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchCanceledAndClosedOrders', 'paginate') + if paginate: + return await self.fetch_paginated_call_cursor('fetchCanceledAndClosedOrders', symbol, since, limit, params, 'cursor', 'cursor') + request, params = self.handle_until_option('endTime', request, params) + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + response = None + trigger = self.safe_bool_2(params, 'stop', 'trigger') + params = self.omit(params, ['stop', 'trigger']) + if trigger: + response = await self.privateUtaGetV3TradeHistoryStrategyOrders(self.extend(request, params)) + else: + response = await self.privateUtaGetV3TradeHistoryOrders(self.extend(request, params)) + # + # uta + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1752531592855, + # "data": { + # "list": [ + # { + # "orderId": "1322441400976261120", + # "clientOid": "1322441400976261121", + # "category": "USDT-FUTURES", + # "symbol": "BTCUSDT", + # "orderType": "market", + # "side": "sell", + # "price": "0", + # "qty": "0.0001", + # "amount": "0", + # "cumExecQty": "0.0001", + # "cumExecValue": "10.7005", + # "avgPrice": "107005.4", + # "timeInForce": "gtc", + # "orderStatus": "filled", + # "posSide": "long", + # "holdMode": "hedge_mode", + # "reduceOnly": "NO", + # "feeDetail": [ + # { + # "feeCoin": "USDT", + # "fee": "0.00642032" + # } + # ], + # "createdTime": "1751020520442", + # "updatedTime": "1751020520457", + # "cancelReason": "", + # "execType": "normal", + # "stpMode": "none", + # "tpTriggerBy": null, + # "slTriggerBy": null, + # "takeProfit": null, + # "stopLoss": null, + # "tpOrderType": null, + # "slOrderType": null, + # "tpLimitPrice": null, + # "slLimitPrice": null + # }, + # ], + # "cursor": "1322441328637100035" + # } + # } + # + # uta trigger + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1753058447920, + # "data": { + # "list": [ + # { + # "orderId": "1330984742276198400", + # "clientOid": "1330984742276198400", + # "symbol": "BTCUSDT", + # "category": "USDT-FUTURES", + # "qty": "0.001", + # "posSide": "long", + # "tpTriggerBy": "market", + # "slTriggerBy": "mark", + # "takeProfit": "", + # "stopLoss": "112000", + # "tpOrderType": "market", + # "slOrderType": "limit", + # "tpLimitPrice": "", + # "slLimitPrice": "111000", + # "createdTime": "1753057411736", + # "updatedTime": "1753058267412" + # }, + # ], + # "cursor": 1330960754317619202 + # } + # } + # + data = self.safe_dict(response, 'data', {}) + orders = self.safe_list(data, 'list', []) + return self.parse_orders(orders, market, since, limit) + + async def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + + https://www.bitget.com/api-doc/spot/account/Get-Account-Bills + https://www.bitget.com/api-doc/contract/account/Get-Account-Bill + + :param str [code]: unified currency code, default is None + :param int [since]: timestamp in ms of the earliest ledger entry, default is None + :param int [limit]: max number of ledger entries to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: end time in ms + :param str [params.symbol]: *contract only* unified market symbol + :param str [params.productType]: *contract only* 'USDT-FUTURES', 'USDC-FUTURES', 'COIN-FUTURES', 'SUSDT-FUTURES', 'SUSDC-FUTURES' or 'SCOIN-FUTURES' + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict: a `ledger structure ` + """ + await self.load_markets() + symbol = self.safe_string(params, 'symbol') + params = self.omit(params, 'symbol') + market = None + if symbol is not None: + market = self.market(symbol) + marketType = None + marketType, params = self.handle_market_type_and_params('fetchLedger', market, params) + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchLedger', 'paginate') + if paginate: + cursorReceived = None + if marketType != 'spot': + cursorReceived = 'endId' + return await self.fetch_paginated_call_cursor('fetchLedger', symbol, since, limit, params, cursorReceived, 'idLessThan') + currency = None + request: dict = {} + if code is not None: + currency = self.currency(code) + request['coin'] = currency['id'] + request, params = self.handle_until_option('endTime', request, params) + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + response = None + if marketType == 'spot': + response = await self.privateSpotGetV2SpotAccountBills(self.extend(request, params)) + else: + if symbol is not None: + request['symbol'] = market['id'] + productType = None + productType, params = self.handle_product_type_and_params(market, params) + request['productType'] = productType + response = await self.privateMixGetV2MixAccountBill(self.extend(request, params)) + # + # spot + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700795836415, + # "data": [ + # { + # "billId": "1111506298997215233", + # "coin": "USDT", + # "groupType": "transfer", + # "businessType": "transfer_out", + # "size": "-11.64958799", + # "balance": "0.00000000", + # "fees": "0.00000000", + # "cTime": "1700729673028" + # }, + # ] + # } + # + # swap and future + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700795977890, + # "data": { + # "bills": [ + # { + # "billId": "1111499428100472833", + # "symbol": "", + # "amount": "-11.64958799", + # "fee": "0", + # "feeByCoupon": "", + # "businessType": "trans_to_exchange", + # "coin": "USDT", + # "cTime": "1700728034996" + # }, + # ], + # "endId": "1098396773329305606" + # } + # } + # + data = self.safe_value(response, 'data') + if (marketType == 'swap') or (marketType == 'future'): + bills = self.safe_value(data, 'bills', []) + return self.parse_ledger(bills, currency, since, limit) + return self.parse_ledger(data, currency, since, limit) + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + # + # spot + # + # { + # "billId": "1111506298997215233", + # "coin": "USDT", + # "groupType": "transfer", + # "businessType": "transfer_out", + # "size": "-11.64958799", + # "balance": "0.00000000", + # "fees": "0.00000000", + # "cTime": "1700729673028" + # } + # + # swap and future + # + # { + # "billId": "1111499428100472833", + # "symbol": "", + # "amount": "-11.64958799", + # "fee": "0", + # "feeByCoupon": "", + # "businessType": "trans_to_exchange", + # "coin": "USDT", + # "cTime": "1700728034996" + # } + # + currencyId = self.safe_string(item, 'coin') + code = self.safe_currency_code(currencyId, currency) + currency = self.safe_currency(currencyId, currency) + timestamp = self.safe_integer(item, 'cTime') + after = self.safe_number(item, 'balance') + fee = self.safe_number_2(item, 'fees', 'fee') + amountRaw = self.safe_string_2(item, 'size', 'amount') + amount = self.parse_number(Precise.string_abs(amountRaw)) + direction = 'in' + if amountRaw.find('-') >= 0: + direction = 'out' + return self.safe_ledger_entry({ + 'info': item, + 'id': self.safe_string(item, 'billId'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'direction': direction, + 'account': None, + 'referenceId': None, + 'referenceAccount': None, + 'type': self.parse_ledger_type(self.safe_string(item, 'businessType')), + 'currency': code, + 'amount': amount, + 'before': None, + 'after': after, + 'status': None, + 'fee': { + 'currency': code, + 'cost': fee, + }, + }, currency) + + def parse_ledger_type(self, type): + types: dict = { + 'trans_to_cross': 'transfer', + 'trans_from_cross': 'transfer', + 'trans_to_exchange': 'transfer', + 'trans_from_exchange': 'transfer', + 'trans_to_isolated': 'transfer', + 'trans_from_isolated': 'transfer', + 'trans_to_contract': 'transfer', + 'trans_from_contract': 'transfer', + 'trans_to_otc': 'transfer', + 'trans_from_otc': 'transfer', + 'open_long': 'trade', + 'close_long': 'trade', + 'open_short': 'trade', + 'close_short': 'trade', + 'force_close_long': 'trade', + 'force_close_short': 'trade', + 'burst_long_loss_query': 'trade', + 'burst_short_loss_query': 'trade', + 'force_buy': 'trade', + 'force_sell': 'trade', + 'burst_buy': 'trade', + 'burst_sell': 'trade', + 'delivery_long': 'settlement', + 'delivery_short': 'settlement', + 'contract_settle_fee': 'fee', + 'append_margin': 'transaction', + 'adjust_down_lever_append_margin': 'transaction', + 'reduce_margin': 'transaction', + 'auto_append_margin': 'transaction', + 'cash_gift_issue': 'cashback', + 'cash_gift_recycle': 'cashback', + 'bonus_issue': 'rebate', + 'bonus_recycle': 'rebate', + 'bonus_expired': 'rebate', + 'transfer_in': 'transfer', + 'transfer_out': 'transfer', + 'deposit': 'deposit', + 'withdraw': 'withdrawal', + 'buy': 'trade', + 'sell': 'trade', + } + return self.safe_string(types, type, type) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + fetch all trades made by the user + + https://www.bitget.com/api-doc/spot/trade/Get-Fills + https://www.bitget.com/api-doc/contract/trade/Get-Order-Fills + https://www.bitget.com/api-doc/margin/cross/trade/Get-Cross-Order-Fills + https://www.bitget.com/api-doc/margin/isolated/trade/Get-Isolated-Transaction-Details + https://www.bitget.com/api-doc/uta/trade/Get-Order-Fills + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch trades for + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Trade[]: a list of `trade structures ` + """ + uta = None + uta, params = self.handle_option_and_params(params, 'fetchMyTrades', 'uta', False) + if not uta and (symbol is None): + raise ArgumentsRequired(self.id + ' fetchMyTrades() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = {} + request, params = self.handle_until_option('endTime', request, params) + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + paginate = False + marginMode = None + paginate, params = self.handle_option_and_params(params, 'fetchMyTrades', 'paginate') + marginMode, params = self.handle_margin_mode_and_params('fetchMyTrades', params) + if paginate: + cursorReceived = None + cursorSent = None + if uta: + cursorReceived = 'cursor' + cursorSent = 'cursor' + elif market['spot']: + if marginMode is not None: + cursorReceived = 'minId' + cursorSent = 'idLessThan' + else: + cursorReceived = 'endId' + cursorSent = 'idLessThan' + return await self.fetch_paginated_call_cursor('fetchMyTrades', symbol, since, limit, params, cursorReceived, cursorSent) + response = None + if uta: + response = await self.privateUtaGetV3TradeFills(self.extend(request, params)) + else: + request['symbol'] = market['id'] + if market['spot']: + if marginMode is not None: + if since is None: + request['startTime'] = self.milliseconds() - 7776000000 + if marginMode == 'isolated': + response = await self.privateMarginGetV2MarginIsolatedFills(self.extend(request, params)) + elif marginMode == 'cross': + response = await self.privateMarginGetV2MarginCrossedFills(self.extend(request, params)) + else: + response = await self.privateSpotGetV2SpotTradeFills(self.extend(request, params)) + else: + productType = None + productType, params = self.handle_product_type_and_params(market, params) + request['productType'] = productType + response = await self.privateMixGetV2MixOrderFills(self.extend(request, params)) + # + # spot + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700802995406, + # "data": [ + # { + # "userId": "7264631751", + # "symbol": "BTCUSDT", + # "orderId": "1098394344925597696", + # "tradeId": "1098394344974925824", + # "orderType": "market", + # "side": "sell", + # "priceAvg": "28467.68", + # "size": "0.0002", + # "amount": "5.693536", + # "feeDetail": { + # "deduction": "no", + # "feeCoin": "USDT", + # "totalDeductionFee": "", + # "totalFee": "-0.005693536" + # }, + # "tradeScope": "taker", + # "cTime": "1697603539699", + # "uTime": "1697603539754" + # } + # ] + # } + # + # spot margin + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700803176399, + # "data": { + # "fills": [ + # { + # "orderId": "1099353730455318528", + # "tradeId": "1099353730627092481", + # "orderType": "market", + # "side": "sell", + # "priceAvg": "29543.7", + # "size": "0.0001", + # "amount": "2.95437", + # "tradeScope": "taker", + # "feeDetail": { + # "deduction": "no", + # "feeCoin": "USDT", + # "totalDeductionFee": "0", + # "totalFee": "-0.00295437" + # }, + # "cTime": "1697832275063", + # "uTime": "1697832275150" + # }, + # ], + # "minId": "1099353591699161118", + # "maxId": "1099353730627092481" + # } + # } + # + # swap and future + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700803357487, + # "data": { + # "fillList": [ + # { + # "tradeId": "1111468664328269825", + # "symbol": "BTCUSDT", + # "orderId": "1111468664264753162", + # "price": "37271.4", + # "baseVolume": "0.001", + # "feeDetail": [ + # { + # "deduction": "no", + # "feeCoin": "USDT", + # "totalDeductionFee": null, + # "totalFee": "-0.02236284" + # } + # ], + # "side": "buy", + # "quoteVolume": "37.2714", + # "profit": "-0.0007", + # "enterPointSource": "web", + # "tradeSide": "close", + # "posMode": "hedge_mode", + # "tradeScope": "taker", + # "cTime": "1700720700342" + # }, + # ], + # "endId": "1099351587643699201" + # } + # } + # + # uta + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1751099666579, + # "data": { + # "list": [ + # { + # "execId": "1322441401010528257", + # "orderId": "1322441400976261120", + # "category": "USDT-FUTURES", + # "symbol": "BTCUSDT", + # "orderType": "market", + # "side": "sell", + # "execPrice": "107005.4", + # "execQty": "0.0001", + # "execValue": "10.7005", + # "tradeScope": "taker", + # "feeDetail": [{ + # "feeCoin": "USDT", + # "fee":"0.00642032" + # }], + # "createdTime": "1751020520451", + # "updatedTime": "1751020520458", + # "execPnl": "0.00017" + # }, + # ], + # "cursor": "1322061241878880257" + # } + # } + # + data = self.safe_value(response, 'data') + if uta: + fills = self.safe_list(data, 'list', []) + return self.parse_trades(fills, market, since, limit) + elif (market['swap'] or (market['future'])): + fills = self.safe_list(data, 'fillList', []) + return self.parse_trades(fills, market, since, limit) + elif marginMode is not None: + fills = self.safe_list(data, 'fills', []) + return self.parse_trades(fills, market, since, limit) + return self.parse_trades(data, market, since, limit) + + async def fetch_position(self, symbol: str, params={}): + """ + fetch data on a single open contract trade position + + https://www.bitget.com/api-doc/contract/position/get-single-position + https://www.bitget.com/api-doc/uta/trade/Get-Position + + :param str symbol: unified market symbol of the market the position is held in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :returns dict: a `position structure ` + """ + await self.load_markets() + market = self.market(symbol) + productType = None + productType, params = self.handle_product_type_and_params(market, params) + request: dict = { + 'symbol': market['id'], + } + response = None + uta = None + result = None + uta, params = self.handle_option_and_params(params, 'fetchPosition', 'uta', False) + if uta: + request['category'] = productType + response = await self.privateUtaGetV3PositionCurrentPosition(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1750929905423, + # "data": { + # "list": [ + # { + # "category": "USDT-FUTURES", + # "symbol": "BTCUSDT", + # "marginCoin": "USDT", + # "holdMode": "hedge_mode", + # "posSide": "long", + # "marginMode": "crossed", + # "positionBalance": "5.435199", + # "available": "0.001", + # "frozen": "0", + # "total": "0.001", + # "leverage": "20", + # "curRealisedPnl": "0", + # "avgPrice": "107410.3", + # "positionStatus": "normal", + # "unrealisedPnl": "0.0047", + # "liquidationPrice": "0", + # "mmr": "0.004", + # "profitRate": "0.0008647337475591", + # "markPrice": "107415.3", + # "breakEvenPrice": "107539.2", + # "totalFunding": "0", + # "openFeeTotal": "-0.06444618", + # "closeFeeTotal": "0", + # "createdTime": "1750495670699", + # "updatedTime": "1750929883465" + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + result = self.safe_list(data, 'list', []) + else: + request['marginCoin'] = market['settleId'] + request['productType'] = productType + response = await self.privateMixGetV2MixPositionSinglePosition(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700807531673, + # "data": [ + # { + # "marginCoin": "USDT", + # "symbol": "BTCUSDT", + # "holdSide": "long", + # "openDelegateSize": "0", + # "marginSize": "3.73555", + # "available": "0.002", + # "locked": "0", + # "total": "0.002", + # "leverage": "20", + # "achievedProfits": "0", + # "openPriceAvg": "37355.5", + # "marginMode": "crossed", + # "posMode": "hedge_mode", + # "unrealizedPL": "0.007", + # "liquidationPrice": "31724.970702417", + # "keepMarginRate": "0.004", + # "markPrice": "37359", + # "marginRatio": "0.029599540355", + # "cTime": "1700807507275" + # } + # ] + # } + # + result = self.safe_list(response, 'data', []) + first = self.safe_dict(result, 0, {}) + return self.parse_position(first, market) + + async def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://www.bitget.com/api-doc/contract/position/get-all-position + https://www.bitget.com/api-doc/contract/position/Get-History-Position + https://www.bitget.com/api-doc/uta/trade/Get-Position + + :param str[] [symbols]: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginCoin]: the settle currency of the positions, needs to match the productType + :param str [params.productType]: 'USDT-FUTURES', 'USDC-FUTURES', 'COIN-FUTURES', 'SUSDT-FUTURES', 'SUSDC-FUTURES' or 'SCOIN-FUTURES' + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param boolean [params.useHistoryEndpoint]: default False, when True will use the historic endpoint to fetch positions + :param str [params.method]: either(default) 'privateMixGetV2MixPositionAllPosition', 'privateMixGetV2MixPositionHistoryPosition', or 'privateUtaGetV3PositionCurrentPosition' + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :returns dict[]: a list of `position structure ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchPositions', 'paginate') + if paginate: + return await self.fetch_paginated_call_cursor('fetchPositions', None, None, None, params, 'endId', 'idLessThan') + method = None + useHistoryEndpoint = self.safe_bool(params, 'useHistoryEndpoint', False) + if useHistoryEndpoint: + method = 'privateMixGetV2MixPositionHistoryPosition' + else: + method, params = self.handle_option_and_params(params, 'fetchPositions', 'method', 'privateMixGetV2MixPositionAllPosition') + market = None + if symbols is not None: + first = self.safe_string(symbols, 0) + # symbols can be None or [] + if first is not None: + market = self.market(first) + productType = None + productType, params = self.handle_product_type_and_params(market, params) + request: dict = {} + response = None + isHistory = False + uta = None + uta, params = self.handle_option_and_params(params, 'fetchPositions', 'uta', False) + if uta: + request['category'] = productType + response = await self.privateUtaGetV3PositionCurrentPosition(self.extend(request, params)) + elif method == 'privateMixGetV2MixPositionAllPosition': + marginCoin = self.safe_string(params, 'marginCoin', 'USDT') + if market is not None: + marginCoin = market['settleId'] + elif productType == 'USDT-FUTURES': + marginCoin = 'USDT' + elif productType == 'USDC-FUTURES': + marginCoin = 'USDC' + elif productType == 'SUSDT-FUTURES': + marginCoin = 'SUSDT' + elif productType == 'SUSDC-FUTURES': + marginCoin = 'SUSDC' + elif (productType == 'SCOIN-FUTURES') or (productType == 'COIN-FUTURES'): + if marginCoin is None: + raise ArgumentsRequired(self.id + ' fetchPositions() requires a marginCoin parameter that matches the productType') + request['marginCoin'] = marginCoin + request['productType'] = productType + response = await self.privateMixGetV2MixPositionAllPosition(self.extend(request, params)) + else: + isHistory = True + if market is not None: + request['symbol'] = market['id'] + request['productType'] = productType + response = await self.privateMixGetV2MixPositionHistoryPosition(self.extend(request, params)) + # + # privateMixGetV2MixPositionAllPosition + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700807810221, + # "data": [ + # { + # "marginCoin": "USDT", + # "symbol": "BTCUSDT", + # "holdSide": "long", + # "openDelegateSize": "0", + # "marginSize": "3.73555", + # "available": "0.002", + # "locked": "0", + # "total": "0.002", + # "leverage": "20", + # "achievedProfits": "0", + # "openPriceAvg": "37355.5", + # "marginMode": "crossed", + # "posMode": "hedge_mode", + # "unrealizedPL": "0.03", + # "liquidationPrice": "31725.023602417", + # "keepMarginRate": "0.004", + # "markPrice": "37370.5", + # "marginRatio": "0.029550120396", + # "cTime": "1700807507275" + # } + # ] + # } + # + # privateMixGetV2MixPositionHistoryPosition + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700808051002, + # "data": { + # "list": [ + # { + # "symbol": "BTCUSDT", + # "marginCoin": "USDT", + # "holdSide": "long", + # "openAvgPrice": "37272.1", + # "closeAvgPrice": "37271.4", + # "marginMode": "crossed", + # "openTotalPos": "0.001", + # "closeTotalPos": "0.001", + # "pnl": "-0.0007", + # "netProfit": "-0.0454261", + # "totalFunding": "0", + # "openFee": "-0.02236326", + # "closeFee": "-0.02236284", + # "utime": "1700720700400", + # "ctime": "1700720651684" + # }, + # ], + # "endId": "1099351653866962944" + # } + # } + # + # privateUtaGetV3PositionCurrentPosition + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1750929905423, + # "data": { + # "list": [ + # { + # "category": "USDT-FUTURES", + # "symbol": "BTCUSDT", + # "marginCoin": "USDT", + # "holdMode": "hedge_mode", + # "posSide": "long", + # "marginMode": "crossed", + # "positionBalance": "5.435199", + # "available": "0.001", + # "frozen": "0", + # "total": "0.001", + # "leverage": "20", + # "curRealisedPnl": "0", + # "avgPrice": "107410.3", + # "positionStatus": "normal", + # "unrealisedPnl": "0.0047", + # "liquidationPrice": "0", + # "mmr": "0.004", + # "profitRate": "0.0008647337475591", + # "markPrice": "107415.3", + # "breakEvenPrice": "107539.2", + # "totalFunding": "0", + # "openFeeTotal": "-0.06444618", + # "closeFeeTotal": "0", + # "createdTime": "1750495670699", + # "updatedTime": "1750929883465" + # } + # ] + # } + # } + # + position = [] + if uta or isHistory: + data = self.safe_dict(response, 'data', {}) + position = self.safe_list(data, 'list', []) + else: + position = self.safe_list(response, 'data', []) + result = [] + for i in range(0, len(position)): + result.append(self.parse_position(position[i], market)) + symbols = self.market_symbols(symbols) + return self.filter_by_array_positions(result, 'symbol', symbols, False) + + def parse_position(self, position: dict, market: Market = None): + # + # fetchPosition + # + # { + # "marginCoin": "USDT", + # "symbol": "BTCUSDT", + # "holdSide": "long", + # "openDelegateSize": "0", + # "marginSize": "3.73555", + # "available": "0.002", + # "locked": "0", + # "total": "0.002", + # "leverage": "20", + # "achievedProfits": "0", + # "openPriceAvg": "37355.5", + # "marginMode": "crossed", + # "posMode": "hedge_mode", + # "unrealizedPL": "0.007", + # "liquidationPrice": "31724.970702417", + # "keepMarginRate": "0.004", + # "markPrice": "37359", + # "marginRatio": "0.029599540355", + # "cTime": "1700807507275" + # } + # + # uta: fetchPosition + # + # { + # "category": "USDT-FUTURES", + # "symbol": "BTCUSDT", + # "marginCoin": "USDT", + # "holdMode": "hedge_mode", + # "posSide": "long", + # "marginMode": "crossed", + # "positionBalance": "5.435199", + # "available": "0.001", + # "frozen": "0", + # "total": "0.001", + # "leverage": "20", + # "curRealisedPnl": "0", + # "avgPrice": "107410.3", + # "positionStatus": "normal", + # "unrealisedPnl": "0.0047", + # "liquidationPrice": "0", + # "mmr": "0.004", + # "profitRate": "0.0008647337475591", + # "markPrice": "107415.3", + # "breakEvenPrice": "107539.2", + # "totalFunding": "0", + # "openFeeTotal": "-0.06444618", + # "closeFeeTotal": "0", + # "createdTime": "1750495670699", + # "updatedTime": "1750929883465" + # } + # + # fetchPositions: privateMixGetV2MixPositionAllPosition + # + # { + # "marginCoin": "USDT", + # "symbol": "BTCUSDT", + # "holdSide": "long", + # "openDelegateSize": "0", + # "marginSize": "3.73555", + # "available": "0.002", + # "locked": "0", + # "total": "0.002", + # "leverage": "20", + # "achievedProfits": "0", + # "openPriceAvg": "37355.5", + # "marginMode": "crossed", + # "posMode": "hedge_mode", + # "unrealizedPL": "0.03", + # "liquidationPrice": "31725.023602417", + # "keepMarginRate": "0.004", + # "markPrice": "37370.5", + # "marginRatio": "0.029550120396", + # "cTime": "1700807507275" + # } + # + # fetchPositionsHistory: privateMixGetV2MixPositionHistoryPosition + # + # { + # "symbol": "BTCUSDT", + # "marginCoin": "USDT", + # "holdSide": "long", + # "openAvgPrice": "37272.1", + # "closeAvgPrice": "37271.4", + # "marginMode": "crossed", + # "openTotalPos": "0.001", + # "closeTotalPos": "0.001", + # "pnl": "-0.0007", + # "netProfit": "-0.0454261", + # "totalFunding": "0", + # "openFee": "-0.02236326", + # "closeFee": "-0.02236284", + # "utime": "1700720700400", + # "ctime": "1700720651684" + # } + # + # closeAllPositions + # + # { + # "orderId": "1120923953904893955", + # "clientOid": "1120923953904893956" + # } + # + # uta: fetchPositionsHistory + # + # { + # "positionId": "1322441328637100049", + # "category": "USDT-FUTURES", + # "symbol": "BTCUSDT", + # "marginCoin": "USDT", + # "holdMode": "hedge_mode", + # "posSide": "long", + # "marginMode": "crossed", + # "openPriceAvg": "107003.7", + # "closePriceAvg": "107005.4", + # "openTotalPos": "0.0001", + # "closeTotalPos": "0.0001", + # "cumRealisedPnl": "0.00017", + # "netProfit": "-0.01267055", + # "totalFunding": "0", + # "openFeeTotal": "-0.00642022", + # "closeFeeTotal": "-0.00642032", + # "createdTime": "1751020503195", + # "updatedTime": "1751020520458" + # } + # + marketId = self.safe_string(position, 'symbol') + market = self.safe_market(marketId, market, None, 'contract') + symbol = market['symbol'] + timestamp = self.safe_integer_n(position, ['cTime', 'ctime', 'createdTime']) + marginMode = self.safe_string(position, 'marginMode') + collateral = None + initialMargin = None + unrealizedPnl = self.safe_string_2(position, 'unrealizedPL', 'unrealisedPnl') + rawCollateral = self.safe_string_2(position, 'marginSize', 'positionBalance') + if marginMode == 'isolated': + collateral = Precise.string_add(rawCollateral, unrealizedPnl) + elif marginMode == 'crossed': + marginMode = 'cross' + initialMargin = rawCollateral + holdMode = self.safe_string_2(position, 'posMode', 'holdMode') + hedged = None + if holdMode == 'hedge_mode': + hedged = True + elif holdMode == 'one_way_mode': + hedged = False + side = self.safe_string_2(position, 'holdSide', 'posSide') + leverage = self.safe_string(position, 'leverage') + contractSizeNumber = self.safe_value(market, 'contractSize') + contractSize = self.number_to_string(contractSizeNumber) + baseAmount = self.safe_string_2(position, 'total', 'openTotalPos') + entryPrice = self.safe_string_n(position, ['openPriceAvg', 'openAvgPrice', 'avgPrice']) + maintenanceMarginPercentage = self.safe_string(position, 'keepMarginRate') + openNotional = Precise.string_mul(entryPrice, baseAmount) + if initialMargin is None: + initialMargin = Precise.string_div(openNotional, leverage) + contracts = self.parse_number(Precise.string_div(baseAmount, contractSize)) + if contracts is None: + contracts = self.safe_number(position, 'closeTotalPos') + markPrice = self.safe_string(position, 'markPrice') + notional = Precise.string_mul(baseAmount, markPrice) + initialMarginPercentage = Precise.string_div(initialMargin, notional) + liquidationPrice = self.parse_number(self.omit_zero(self.safe_string(position, 'liquidationPrice'))) + calcTakerFeeRate = '0.0006' + calcTakerFeeMult = '0.9994' + if (liquidationPrice is None) and (marginMode == 'isolated') and Precise.string_gt(baseAmount, '0'): + signedMargin = Precise.string_div(rawCollateral, baseAmount) + signedMmp = maintenanceMarginPercentage + if side == 'short': + signedMargin = Precise.string_neg(signedMargin) + signedMmp = Precise.string_neg(signedMmp) + mmrMinusOne = Precise.string_sub('1', signedMmp) + numerator = Precise.string_sub(entryPrice, signedMargin) + if side == 'long': + mmrMinusOne = Precise.string_mul(mmrMinusOne, calcTakerFeeMult) + else: + numerator = Precise.string_mul(numerator, calcTakerFeeMult) + liquidationPrice = self.parse_number(Precise.string_div(numerator, mmrMinusOne)) + feeToClose = Precise.string_mul(notional, calcTakerFeeRate) + maintenanceMargin = Precise.string_add(Precise.string_mul(maintenanceMarginPercentage, notional), feeToClose) + percentage = Precise.string_mul(Precise.string_div(unrealizedPnl, initialMargin, 4), '100') + return self.safe_position({ + 'info': position, + 'id': self.safe_string_2(position, 'orderId', 'positionId'), + 'symbol': symbol, + 'notional': self.parse_number(notional), + 'marginMode': marginMode, + 'liquidationPrice': liquidationPrice, + 'entryPrice': self.parse_number(entryPrice), + 'unrealizedPnl': self.parse_number(unrealizedPnl), + 'realizedPnl': self.safe_number_n(position, ['pnl', 'curRealisedPnl', 'cumRealisedPnl']), + 'percentage': self.parse_number(percentage), + 'contracts': contracts, + 'contractSize': contractSizeNumber, + 'markPrice': self.parse_number(markPrice), + 'lastPrice': self.safe_number_2(position, 'closeAvgPrice', 'closePriceAvg'), + 'side': side, + 'hedged': hedged, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastUpdateTimestamp': self.safe_integer_2(position, 'utime', 'updatedTime'), + 'maintenanceMargin': self.parse_number(maintenanceMargin), + 'maintenanceMarginPercentage': self.parse_number(maintenanceMarginPercentage), + 'collateral': self.parse_number(collateral), + 'initialMargin': self.parse_number(initialMargin), + 'initialMarginPercentage': self.parse_number(initialMarginPercentage), + 'leverage': self.parse_number(leverage), + 'marginRatio': self.safe_number_2(position, 'marginRatio', 'mmr'), + 'stopLossPrice': None, + 'takeProfitPrice': None, + }) + + async def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical funding rate prices + + https://www.bitget.com/api-doc/contract/market/Get-History-Funding-Rate + https://www.bitget.com/api-doc/uta/public/Get-History-Funding-Rate + + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: timestamp in ms of the earliest funding rate to fetch + :param int [limit]: the maximum amount of funding rate structures to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `funding rate structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchFundingRateHistory() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + productType = None + uta = None + response = None + result = None + productType, params = self.handle_product_type_and_params(market, params) + uta, params = self.handle_option_and_params(params, 'fetchFundingRateHistory', 'uta', False) + if uta: + if limit is not None: + request['limit'] = limit + request['category'] = productType + response = await self.publicUtaGetV3MarketHistoryFundRate(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1750435113658, + # "data": { + # "resultList": [ + # { + # "symbol": "BTCUSDT", + # "fundingRate": "-0.000017", + # "fundingRateTimestamp": "1750431600000" + # }, + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + result = self.safe_list(data, 'resultList', []) + else: + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchFundingRateHistory', 'paginate') + if paginate: + return await self.fetch_paginated_call_incremental('fetchFundingRateHistory', symbol, since, limit, params, 'pageNo', 100) + if limit is not None: + request['pageSize'] = limit + request['productType'] = productType + response = await self.publicMixGetV2MixMarketHistoryFundRate(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1652406728393, + # "data": [ + # { + # "symbol": "BTCUSDT", + # "fundingRate": "-0.0003", + # "fundingTime": "1652396400000" + # }, + # ] + # } + # + result = self.safe_list(response, 'data', []) + rates = [] + for i in range(0, len(result)): + entry = result[i] + marketId = self.safe_string(entry, 'symbol') + symbolInner = self.safe_symbol(marketId, market) + timestamp = self.safe_integer_2(entry, 'fundingTime', 'fundingRateTimestamp') + rates.append({ + 'info': entry, + 'symbol': symbolInner, + 'fundingRate': self.safe_number(entry, 'fundingRate'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + sorted = self.sort_by(rates, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, market['symbol'], since, limit) + + async def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate + + https://www.bitget.com/api-doc/contract/market/Get-Current-Funding-Rate + https://www.bitget.com/api-doc/contract/market/Get-Symbol-Next-Funding-Time + https://www.bitget.com/api-doc/uta/public/Get-Current-Funding-Rate + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :param str [params.method]: either(default) 'publicMixGetV2MixMarketCurrentFundRate' or 'publicMixGetV2MixMarketFundingTime' + :returns dict: a `funding rate structure ` + """ + await self.load_markets() + market = self.market(symbol) + if not market['swap']: + raise BadSymbol(self.id + ' fetchFundingRate() supports swap contracts only') + productType = None + productType, params = self.handle_product_type_and_params(market, params) + request: dict = { + 'symbol': market['id'], + } + uta = None + response = None + uta, params = self.handle_option_and_params(params, 'fetchFundingRate', 'uta', False) + if uta: + response = await self.publicUtaGetV3MarketCurrentFundRate(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1750897372153, + # "data": [ + # { + # "symbol": "BTCUSDT", + # "fundingRate": "0.00001", + # "fundingRateInterval": "8", + # "nextUpdate": "1750924800000", + # "minFundingRate": "-0.003", + # "maxFundingRate": "0.003" + # } + # ] + # } + # + else: + request['productType'] = productType + method = None + method, params = self.handle_option_and_params(params, 'fetchFundingRate', 'method', 'publicMixGetV2MixMarketCurrentFundRate') + if method == 'publicMixGetV2MixMarketCurrentFundRate': + response = await self.publicMixGetV2MixMarketCurrentFundRate(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1745500709429, + # "data": [ + # { + # "symbol": "BTCUSDT", + # "fundingRate": "-0.000013", + # "fundingRateInterval": "8", + # "nextUpdate": "1745510400000", + # "minFundingRate": "-0.003", + # "maxFundingRate": "0.003" + # } + # ] + # } + # + elif method == 'publicMixGetV2MixMarketFundingTime': + response = await self.publicMixGetV2MixMarketFundingTime(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1745402092428, + # "data": [ + # { + # "symbol": "BTCUSDT", + # "nextFundingTime": "1745424000000", + # "ratePeriod": "8" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_funding_rate(data[0], market) + + async def fetch_funding_rates(self, symbols: Strings = None, params={}) -> FundingRates: + """ + fetch the current funding rates for all markets + + https://www.bitget.com/api-doc/contract/market/Get-All-Symbol-Ticker + + :param str[] [symbols]: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.subType]: *contract only* 'linear', 'inverse' + :param str [params.productType]: *contract only* 'USDT-FUTURES', 'USDC-FUTURES', 'COIN-FUTURES', 'SUSDT-FUTURES', 'SUSDC-FUTURES' or 'SCOIN-FUTURES' + :param str [params.method]: either(default) 'publicMixGetV2MixMarketTickers' or 'publicMixGetV2MixMarketCurrentFundRate' + :returns dict: a dictionary of `funding rate structures `, indexed by market symbols + """ + await self.load_markets() + market = None + if symbols is not None: + symbol = self.safe_value(symbols, 0) + market = self.market(symbol) + request: dict = {} + productType = None + productType, params = self.handle_product_type_and_params(market, params) + method = 'publicMixGetV2MixMarketTickers' + method, params = self.handle_option_and_params(params, 'fetchFundingRates', 'method', method) + response = None + request['productType'] = productType + if method == 'publicMixGetV2MixMarketTickers': + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700533773477, + # "data": [ + # { + # "symbol": "BTCUSD", + # "lastPr": "29904.5", + # "askPr": "29904.5", + # "bidPr": "29903.5", + # "bidSz": "0.5091", + # "askSz": "2.2694", + # "high24h": "0", + # "low24h": "0", + # "ts": "1695794271400", + # "change24h": "0", + # "baseVolume": "0", + # "quoteVolume": "0", + # "usdtVolume": "0", + # "openUtc": "0", + # "changeUtc24h": "0", + # "indexPrice": "29132.353333", + # "fundingRate": "-0.0007", + # "holdingAmount": "125.6844", + # "deliveryStartTime": null, + # "deliveryTime": null, + # "deliveryStatus": "delivery_normal", + # "open24h": "0", + # "markPrice": "12345" + # }, + # ] + # } + response = await self.publicMixGetV2MixMarketTickers(self.extend(request, params)) + elif method == 'publicMixGetV2MixMarketCurrentFundRate': + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime":1761659449917, + # "data":[ + # { + # "symbol": "BTCUSDT", + # "fundingRate": "-0.000024", + # "fundingRateInterval": "8", + # "nextUpdate": "1761667200000", + # "minFundingRate": "-0.003", + # "maxFundingRate": "0.003" + # } + # ] + # } + # + response = await self.publicMixGetV2MixMarketCurrentFundRate(self.extend(request, params)) + symbols = self.market_symbols(symbols) + data = self.safe_list(response, 'data', []) + return self.parse_funding_rates(data, symbols) + + async def fetch_funding_intervals(self, symbols: Strings = None, params={}) -> FundingRates: + """ + fetch the funding rate interval for multiple markets + + https://www.bitget.com/api-doc/contract/market/Get-All-Symbol-Ticker + + :param str[] [symbols]: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.productType]: 'USDT-FUTURES'(default), 'USDC-FUTURES', 'COIN-FUTURES', 'SUSDT-FUTURES', 'SUSDC-FUTURES' or 'SCOIN-FUTURES' + :returns dict[]: a list of `funding rate structures ` + """ + await self.load_markets() + params = self.extend({'method': 'publicMixGetV2MixMarketCurrentFundRate'}, params) + return await self.fetch_funding_rates(symbols, params) + + def parse_funding_rate(self, contract, market: Market = None) -> FundingRate: + # + # fetchFundingRate: publicMixGetV2MixMarketCurrentFundRate, publicUtaGetV3MarketCurrentFundRate + # + # { + # "symbol": "BTCUSDT", + # "fundingRate": "-0.000013", + # "fundingRateInterval": "8", + # "nextUpdate": "1745510400000", + # "minFundingRate": "-0.003", + # "maxFundingRate": "0.003" + # } + # + # fetchFundingRate: publicMixGetV2MixMarketFundingTime + # + # { + # "symbol": "BTCUSDT", + # "nextFundingTime": "1745424000000", + # "ratePeriod": "8" + # } + # + # fetchFundingInterval + # + # { + # "symbol": "BTCUSDT", + # "nextFundingTime": "1727942400000", + # "ratePeriod": "8" + # } + # + # fetchFundingRates + # + # { + # "symbol": "BTCUSD", + # "lastPr": "29904.5", + # "askPr": "29904.5", + # "bidPr": "29903.5", + # "bidSz": "0.5091", + # "askSz": "2.2694", + # "high24h": "0", + # "low24h": "0", + # "ts": "1695794271400", + # "change24h": "0", + # "baseVolume": "0", + # "quoteVolume": "0", + # "usdtVolume": "0", + # "openUtc": "0", + # "changeUtc24h": "0", + # "indexPrice": "29132.353333", + # "fundingRate": "-0.0007", + # "holdingAmount": "125.6844", + # "deliveryStartTime": null, + # "deliveryTime": null, + # "deliveryStatus": "delivery_normal", + # "open24h": "0", + # "markPrice": "12345" + # } + # + marketId = self.safe_string(contract, 'symbol') + symbol = self.safe_symbol(marketId, market, None, 'swap') + fundingTimestamp = self.safe_integer_2(contract, 'nextFundingTime', 'nextUpdate') + interval = self.safe_string_2(contract, 'ratePeriod', 'fundingRateInterval') + timestamp = self.safe_integer(contract, 'ts') + markPrice = self.safe_number(contract, 'markPrice') + indexPrice = self.safe_number(contract, 'indexPrice') + intervalString = None + if interval is not None: + intervalString = interval + 'h' + return { + 'info': contract, + 'symbol': symbol, + 'markPrice': markPrice, + 'indexPrice': indexPrice, + 'interestRate': None, + 'estimatedSettlePrice': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'fundingRate': self.safe_number(contract, 'fundingRate'), + 'fundingTimestamp': fundingTimestamp, + 'fundingDatetime': self.iso8601(fundingTimestamp), + 'nextFundingRate': None, + 'nextFundingTimestamp': None, + 'nextFundingDatetime': None, + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': intervalString, + } + + async def fetch_funding_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[FundingHistory]: + """ + fetch the funding history + + https://www.bitget.com/api-doc/contract/account/Get-Account-Bill + + :param str symbol: unified market symbol + :param int [since]: the starting timestamp in milliseconds + :param int [limit]: the number of entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch funding history for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `funding history structures ` + """ + await self.load_markets() + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchFundingHistory() requires a symbol argument') + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchFundingHistory', 'paginate') + if paginate: + return await self.fetch_paginated_call_cursor('fetchFundingHistory', symbol, since, limit, params, 'endId', 'idLessThan') + market = self.market(symbol) + if not market['swap']: + raise BadSymbol(self.id + ' fetchFundingHistory() supports swap contracts only') + productType = None + productType, params = self.handle_product_type_and_params(market, params) + request: dict = { + 'symbol': market['id'], + 'marginCoin': market['settleId'], + 'businessType': 'contract_settle_fee', + 'productType': productType, + } + request, params = self.handle_until_option('endTime', request, params) + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + response = await self.privateMixGetV2MixAccountBill(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700795977890, + # "data": { + # "bills": [ + # { + # "billId": "1111499428100472833", + # "symbol": "BTCUSDT", + # "amount": "-0.004992", + # "fee": "0", + # "feeByCoupon": "", + # "businessType": "contract_settle_fee", + # "coin": "USDT", + # "cTime": "1700728034996" + # }, + # ], + # "endId": "1098396773329305606" + # } + # } + # + data = self.safe_value(response, 'data', {}) + result = self.safe_value(data, 'bills', []) + return self.parse_funding_histories(result, market, since, limit) + + def parse_funding_history(self, contract, market: Market = None): + # + # { + # "billId": "1111499428100472833", + # "symbol": "BTCUSDT", + # "amount": "-0.004992", + # "fee": "0", + # "feeByCoupon": "", + # "businessType": "contract_settle_fee", + # "coin": "USDT", + # "cTime": "1700728034996" + # } + # + marketId = self.safe_string(contract, 'symbol') + currencyId = self.safe_string(contract, 'coin') + timestamp = self.safe_integer(contract, 'cTime') + return { + 'info': contract, + 'symbol': self.safe_symbol(marketId, market, None, 'swap'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'code': self.safe_currency_code(currencyId), + 'amount': self.safe_number(contract, 'amount'), + 'id': self.safe_string(contract, 'billId'), + } + + def parse_funding_histories(self, contracts, market=None, since: Int = None, limit: Int = None) -> List[FundingHistory]: + result = [] + for i in range(0, len(contracts)): + contract = contracts[i] + business = self.safe_string(contract, 'businessType') + if business != 'contract_settle_fee': + continue + result.append(self.parse_funding_history(contract, market)) + sorted = self.sort_by(result, 'timestamp') + symbol = None + if market is not None: + symbol = market['symbol'] + return self.filter_by_symbol_since_limit(sorted, symbol, since, limit) + + async def modify_margin_helper(self, symbol: str, amount, type, params={}) -> MarginModification: + await self.load_markets() + holdSide = self.safe_string(params, 'holdSide') + market = self.market(symbol) + productType = None + productType, params = self.handle_product_type_and_params(market, params) + request: dict = { + 'symbol': market['id'], + 'marginCoin': market['settleId'], + 'amount': self.amount_to_precision(symbol, amount), # positive value for adding margin, negative for reducing + 'holdSide': holdSide, # long or short + 'productType': productType, + } + params = self.omit(params, 'holdSide') + response = await self.privateMixPostV2MixAccountSetMargin(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700813444618, + # "data": "" + # } + # + return self.extend(self.parse_margin_modification(response, market), { + 'amount': self.parse_number(amount), + 'type': type, + }) + + def parse_margin_modification(self, data: dict, market: Market = None) -> MarginModification: + # + # addMargin/reduceMargin + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700813444618, + # "data": "" + # } + # + errorCode = self.safe_string(data, 'code') + status = 'ok' if (errorCode == '00000') else 'failed' + return { + 'info': data, + 'symbol': market['symbol'], + 'type': None, + 'marginMode': 'isolated', + 'amount': None, + 'total': None, + 'code': market['settle'], + 'status': status, + 'timestamp': None, + 'datetime': None, + } + + async def reduce_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + remove margin from a position + + https://www.bitget.com/api-doc/contract/account/Change-Margin + + :param str symbol: unified market symbol + :param float amount: the amount of margin to remove + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin structure ` + """ + if amount > 0: + raise BadRequest(self.id + ' reduceMargin() amount parameter must be a negative value') + holdSide = self.safe_string(params, 'holdSide') + if holdSide is None: + raise ArgumentsRequired(self.id + ' reduceMargin() requires a holdSide parameter, either long or short') + return await self.modify_margin_helper(symbol, amount, 'reduce', params) + + async def add_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + add margin + + https://www.bitget.com/api-doc/contract/account/Change-Margin + + :param str symbol: unified market symbol + :param float amount: the amount of margin to add + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin structure ` + """ + holdSide = self.safe_string(params, 'holdSide') + if holdSide is None: + raise ArgumentsRequired(self.id + ' addMargin() requires a holdSide parameter, either long or short') + return await self.modify_margin_helper(symbol, amount, 'add', params) + + async def fetch_leverage(self, symbol: str, params={}) -> Leverage: + """ + fetch the set leverage for a market + + https://www.bitget.com/api-doc/contract/account/Get-Single-Account + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `leverage structure ` + """ + await self.load_markets() + market = self.market(symbol) + productType = None + productType, params = self.handle_product_type_and_params(market, params) + request: dict = { + 'symbol': market['id'], + 'marginCoin': market['settleId'], + 'productType': productType, + } + response = await self.privateMixGetV2MixAccountAccount(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1709366911964, + # "data": { + # "marginCoin": "USDT", + # "locked": "0", + # "available": "0", + # "crossedMaxAvailable": "0", + # "isolatedMaxAvailable": "0", + # "maxTransferOut": "0", + # "accountEquity": "0", + # "usdtEquity": "0.000000009166", + # "btcEquity": "0", + # "crossedRiskRate": "0", + # "crossedMarginLeverage": 20, + # "isolatedLongLever": 20, + # "isolatedShortLever": 20, + # "marginMode": "crossed", + # "posMode": "hedge_mode", + # "unrealizedPL": "0", + # "coupon": "0", + # "crossedUnrealizedPL": "0", + # "isolatedUnrealizedPL": "" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_leverage(data, market) + + def parse_leverage(self, leverage: dict, market: Market = None) -> Leverage: + isCrossMarginMode = self.safe_string(leverage, 'marginMode') == 'crossed' + longLevKey = 'crossedMarginLeverage' if isCrossMarginMode else 'isolatedLongLever' + shortLevKey = 'crossedMarginLeverage' if isCrossMarginMode else 'isolatedShortLever' + return { + 'info': leverage, + 'symbol': market['symbol'], + 'marginMode': 'cross' if isCrossMarginMode else 'isolated', + 'longLeverage': self.safe_integer(leverage, longLevKey), + 'shortLeverage': self.safe_integer(leverage, shortLevKey), + } + + async def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://www.bitget.com/api-doc/contract/account/Change-Leverage + https://www.bitget.com/api-doc/uta/account/Change-Leverage + + :param int leverage: the rate of leverage + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.holdSide]: *isolated only* position direction, 'long' or 'short' + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :param boolean [params.posSide]: required for uta isolated margin, long or short + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setLeverage() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + productType = None + productType, params = self.handle_product_type_and_params(market, params) + request: dict = { + 'symbol': market['id'], + 'leverage': self.number_to_string(leverage), + } + uta = None + response = None + uta, params = self.handle_option_and_params(params, 'setLeverage', 'uta', False) + if uta: + if productType == 'SPOT': + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchTrades', params) + if marginMode is not None: + productType = 'MARGIN' + request['coin'] = market['settleId'] + request['category'] = productType + response = await self.privateUtaPostV3AccountSetLeverage(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1752815940833, + # "data": "success" + # } + # + else: + request['marginCoin'] = market['settleId'] + request['productType'] = productType + response = await self.privateMixPostV2MixAccountSetLeverage(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700864711517, + # "data": { + # "symbol": "BTCUSDT", + # "marginCoin": "USDT", + # "longLeverage": "25", + # "shortLeverage": "25", + # "crossMarginLeverage": "25", + # "marginMode": "crossed" + # } + # } + # + return response + + async def set_margin_mode(self, marginMode: str, symbol: Str = None, params={}): + """ + set margin mode to 'cross' or 'isolated' + + https://www.bitget.com/api-doc/contract/account/Change-Margin-Mode + + :param str marginMode: 'cross' or 'isolated' + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setMarginMode() requires a symbol argument') + marginMode = marginMode.lower() + if marginMode == 'cross': + marginMode = 'crossed' + if (marginMode != 'isolated') and (marginMode != 'crossed'): + raise ArgumentsRequired(self.id + ' setMarginMode() marginMode must be either isolated or crossed(cross)') + await self.load_markets() + market = self.market(symbol) + productType = None + productType, params = self.handle_product_type_and_params(market, params) + request: dict = { + 'symbol': market['id'], + 'marginCoin': market['settleId'], + 'marginMode': marginMode, + 'productType': productType, + } + response = await self.privateMixPostV2MixAccountSetMarginMode(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700865205552, + # "data": { + # "symbol": "BTCUSDT", + # "marginCoin": "USDT", + # "longLeverage": "20", + # "shortLeverage": "3", + # "marginMode": "isolated" + # } + # } + # + return response + + async def set_position_mode(self, hedged: bool, symbol: Str = None, params={}): + """ + set hedged to True or False for a market + + https://www.bitget.com/api-doc/contract/account/Change-Hold-Mode + https://www.bitget.com/api-doc/uta/account/Change-Position-Mode + + :param bool hedged: set to True to use dualSidePosition + :param str symbol: not used by bitget setPositionMode() + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.productType]: required if not uta and symbol is None: 'USDT-FUTURES', 'USDC-FUTURES', 'COIN-FUTURES', 'SUSDT-FUTURES', 'SUSDC-FUTURES' or 'SCOIN-FUTURES' + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :returns dict: response from the exchange + """ + await self.load_markets() + posMode = 'hedge_mode' if hedged else 'one_way_mode' + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + productType = None + uta = None + response = None + productType, params = self.handle_product_type_and_params(market, params) + uta, params = self.handle_option_and_params(params, 'setPositionMode', 'uta', False) + if uta: + request['holdMode'] = posMode + response = await self.privateUtaPostV3AccountSetHoldMode(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1752816734592, + # "data": "success" + # } + # + else: + request['posMode'] = posMode + request['productType'] = productType + response = await self.privateMixPostV2MixAccountSetPositionMode(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700865608009, + # "data": { + # "posMode": "hedge_mode" + # } + # } + # + return response + + async def fetch_open_interest(self, symbol: str, params={}): + """ + retrieves the open interest of a contract trading pair + + https://www.bitget.com/api-doc/contract/market/Get-Open-Interest + https://www.bitget.com/api-doc/uta/public/Get-Open-Interest + + :param str symbol: unified CCXT market symbol + :param dict [params]: exchange specific parameters + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :returns dict} an open interest structure{@link https://docs.ccxt.com/#/?id=open-interest-structure: + """ + await self.load_markets() + market = self.market(symbol) + if not market['contract']: + raise BadRequest(self.id + ' fetchOpenInterest() supports contract markets only') + productType = None + productType, params = self.handle_product_type_and_params(market, params) + request: dict = { + 'symbol': market['id'], + } + uta = None + response = None + uta, params = self.handle_option_and_params(params, 'fetchOpenInterest', 'uta', False) + if uta: + request['category'] = productType + response = await self.publicUtaGetV3MarketOpenInterest(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1751101221545, + # "data": { + # "list": [ + # { + # "symbol": "BTCUSDT", + # "openInterest": "18166.3583" + # } + # ], + # "ts": "1751101220993" + # } + # } + # + else: + request['productType'] = productType + response = await self.publicMixGetV2MixMarketOpenInterest(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700866041022, + # "data": { + # "openInterestList": [ + # { + # "symbol": "BTCUSDT", + # "size": "52234.134" + # } + # ], + # "ts": "1700866041023" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_open_interest(data, market) + + def parse_open_interest(self, interest, market: Market = None): + # + # default + # + # { + # "openInterestList": [ + # { + # "symbol": "BTCUSDT", + # "size": "52234.134" + # } + # ], + # "ts": "1700866041023" + # } + # + # uta + # + # { + # "list": [ + # { + # "symbol": "BTCUSDT", + # "openInterest": "18166.3583" + # } + # ], + # "ts": "1751101220993" + # } + # + data = self.safe_list_2(interest, 'openInterestList', 'list', []) + timestamp = self.safe_integer(interest, 'ts') + marketId = self.safe_string(data[0], 'symbol') + return self.safe_open_interest({ + 'symbol': self.safe_symbol(marketId, market, None, 'contract'), + 'openInterestAmount': self.safe_number_2(data[0], 'size', 'openInterest'), + 'openInterestValue': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'info': interest, + }, market) + + async def fetch_transfers(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[TransferEntry]: + """ + fetch a history of internal transfers made on an account + + https://www.bitget.com/api-doc/spot/account/Get-Account-TransferRecords + + :param str code: unified currency code of the currency transferred + :param int [since]: the earliest time in ms to fetch transfers for + :param int [limit]: the maximum number of transfers structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch entries for + :returns dict[]: a list of `transfer structures ` + """ + if code is None: + raise ArgumentsRequired(self.id + ' fetchTransfers() requires a code argument') + await self.load_markets() + type = None + type, params = self.handle_market_type_and_params('fetchTransfers', None, params) + fromAccount = self.safe_string(params, 'fromAccount', type) + params = self.omit(params, 'fromAccount') + accountsByType = self.safe_value(self.options, 'accountsByType', {}) + type = self.safe_string(accountsByType, fromAccount) + currency = self.currency(code) + request: dict = { + 'coin': currency['id'], + 'fromType': type, + } + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + request, params = self.handle_until_option('endTime', request, params) + response = await self.privateSpotGetV2SpotAccountTransferRecords(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700873854651, + # "data": [ + # { + # "coin": "USDT", + # "status": "Successful", + # "toType": "crossed_margin", + # "toSymbol": "", + # "fromType": "spot", + # "fromSymbol": "", + # "size": "11.64958799", + # "ts": "1700729673028", + # "clientOid": "1111506298504744960", + # "transferId": "24930940" + # }, + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_transfers(data, currency, since, limit) + + async def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://www.bitget.com/api-doc/spot/account/Wallet-Transfer + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account to transfer from + :param str toAccount: account to transfer to + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.symbol]: unified CCXT market symbol, required when transferring to or from an account type that is a leveraged position-by-position account + :param str [params.clientOid]: custom id + :returns dict: a `transfer structure ` + """ + await self.load_markets() + currency = self.currency(code) + accountsByType = self.safe_value(self.options, 'accountsByType', {}) + fromType = self.safe_string(accountsByType, fromAccount) + toType = self.safe_string(accountsByType, toAccount) + request: dict = { + 'fromType': fromType, + 'toType': toType, + 'amount': amount, + 'coin': currency['id'], + } + symbol = self.safe_string(params, 'symbol') + params = self.omit(params, 'symbol') + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + response = await self.privateSpotPostV2SpotWalletTransfer(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700874302021, + # "data": { + # "transferId": "1112112916581847040", + # "clientOrderId": null + # } + # } + # + data = self.safe_value(response, 'data', {}) + data['ts'] = self.safe_integer(response, 'requestTime') + return self.parse_transfer(data, currency) + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + # + # transfer + # + # { + # "transferId": "1112112916581847040", + # "clientOrderId": null, + # "ts": 1700874302021 + # } + # + # fetchTransfers + # + # { + # "coin": "USDT", + # "status": "Successful", + # "toType": "crossed_margin", + # "toSymbol": "", + # "fromType": "spot", + # "fromSymbol": "", + # "size": "11.64958799", + # "ts": "1700729673028", + # "clientOid": "1111506298504744960", + # "transferId": "24930940" + # } + # + timestamp = self.safe_integer(transfer, 'ts') + status = self.safe_string_lower(transfer, 'status') + currencyId = self.safe_string(transfer, 'coin') + fromAccountRaw = self.safe_string(transfer, 'fromType') + accountsById = self.safe_value(self.options, 'accountsById', {}) + fromAccount = self.safe_string(accountsById, fromAccountRaw, fromAccountRaw) + toAccountRaw = self.safe_string(transfer, 'toType') + toAccount = self.safe_string(accountsById, toAccountRaw, toAccountRaw) + return { + 'info': transfer, + 'id': self.safe_string(transfer, 'transferId'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'currency': self.safe_currency_code(currencyId, currency), + 'amount': self.safe_number(transfer, 'size'), + 'fromAccount': fromAccount, + 'toAccount': toAccount, + 'status': self.parse_transfer_status(status), + } + + def parse_transfer_status(self, status: Str) -> Str: + statuses: dict = { + 'successful': 'ok', + } + return self.safe_string(statuses, status, status) + + def parse_deposit_withdraw_fee(self, fee, currency: Currency = None): + # + # { + # "chains": [ + # { + # "browserUrl": "https://blockchair.com/bitcoin/transaction/", + # "chain": "BTC", + # "depositConfirm": "1", + # "extraWithdrawFee": "0", + # "minDepositAmount": "0.0001", + # "minWithdrawAmount": "0.005", + # "needTag": "false", + # "rechargeable": "true", + # "withdrawConfirm": "1", + # "withdrawFee": "0.0004", + # "withdrawable": "true" + # }, + # ], + # "coin": "BTC", + # "coinId": "1", + # "transfer": "true"" + # } + # + chains = self.safe_value(fee, 'chains', []) + chainsLength = len(chains) + result: dict = { + 'info': fee, + 'withdraw': { + 'fee': None, + 'percentage': None, + }, + 'deposit': { + 'fee': None, + 'percentage': None, + }, + 'networks': {}, + } + for i in range(0, chainsLength): + chain = chains[i] + networkId = self.safe_string(chain, 'chain') + currencyCode = self.safe_string(currency, 'code') + networkCode = self.network_id_to_code(networkId, currencyCode) + result['networks'][networkCode] = { + 'deposit': {'fee': None, 'percentage': None}, + 'withdraw': {'fee': self.safe_number(chain, 'withdrawFee'), 'percentage': False}, + } + if chainsLength == 1: + result['withdraw']['fee'] = self.safe_number(chain, 'withdrawFee') + result['withdraw']['percentage'] = False + return result + + async def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}): + """ + fetch deposit and withdraw fees + + https://www.bitget.com/api-doc/spot/market/Get-Coin-List + + :param str[]|None codes: list of unified currency codes + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `fee structures ` + """ + await self.load_markets() + response = await self.publicSpotGetV2SpotPublicCoins(params) + # + # { + # "code": "00000", + # "data": [ + # { + # "chains": [ + # { + # "browserUrl": "https://blockchair.com/bitcoin/transaction/", + # "chain": "BTC", + # "depositConfirm": "1", + # "extraWithdrawFee": "0", + # "minDepositAmount": "0.0001", + # "minWithdrawAmount": "0.005", + # "needTag": "false", + # "rechargeable": "true", + # "withdrawConfirm": "1", + # "withdrawFee": "0.0004", + # "withdrawable": "true" + # }, + # ], + # "coin": "BTC", + # "coinId": "1", + # "transfer": "true"" + # } + # ], + # "msg": "success", + # "requestTime": "1700120731773" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_deposit_withdraw_fees(data, codes, 'coin') + + async def borrow_cross_margin(self, code: str, amount: float, params={}): + """ + create a loan to borrow margin + + https://www.bitget.com/api-doc/margin/cross/account/Cross-Borrow + + :param str code: unified currency code of the currency to borrow + :param str amount: the amount to borrow + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin loan structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'coin': currency['id'], + 'borrowAmount': self.currency_to_precision(code, amount), + } + response = await self.privateMarginPostV2MarginCrossedAccountBorrow(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700876470931, + # "data": { + # "loanId": "1112122013642272769", + # "coin": "USDT", + # "borrowAmount": "4" + # } + # } + # + data = self.safe_value(response, 'data', {}) + return self.parse_margin_loan(data, currency) + + async def borrow_isolated_margin(self, symbol: str, code: str, amount: float, params={}): + """ + create a loan to borrow margin + + https://www.bitget.com/api-doc/margin/isolated/account/Isolated-Borrow + + :param str symbol: unified market symbol + :param str code: unified currency code of the currency to borrow + :param str amount: the amount to borrow + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin loan structure ` + """ + await self.load_markets() + currency = self.currency(code) + market = self.market(symbol) + request: dict = { + 'coin': currency['id'], + 'borrowAmount': self.currency_to_precision(code, amount), + 'symbol': market['id'], + } + response = await self.privateMarginPostV2MarginIsolatedAccountBorrow(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700877255605, + # "data": { + # "loanId": "1112125304879067137", + # "symbol": "BTCUSDT", + # "coin": "USDT", + # "borrowAmount": "4" + # } + # } + # + data = self.safe_value(response, 'data', {}) + return self.parse_margin_loan(data, currency, market) + + async def repay_isolated_margin(self, symbol: str, code: str, amount, params={}): + """ + repay borrowed margin and interest + + https://www.bitget.com/api-doc/margin/isolated/account/Isolated-Repay + + :param str symbol: unified market symbol + :param str code: unified currency code of the currency to repay + :param str amount: the amount to repay + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin loan structure ` + """ + await self.load_markets() + currency = self.currency(code) + market = self.market(symbol) + request: dict = { + 'coin': currency['id'], + 'repayAmount': self.currency_to_precision(code, amount), + 'symbol': market['id'], + } + response = await self.privateMarginPostV2MarginIsolatedAccountRepay(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700877518012, + # "data": { + # "remainDebtAmount": "0", + # "repayId": "1112126405439270912", + # "symbol": "BTCUSDT", + # "coin": "USDT", + # "repayAmount": "8.000137" + # } + # } + # + data = self.safe_value(response, 'data', {}) + return self.parse_margin_loan(data, currency, market) + + async def repay_cross_margin(self, code: str, amount, params={}): + """ + repay borrowed margin and interest + + https://www.bitget.com/api-doc/margin/cross/account/Cross-Repay + + :param str code: unified currency code of the currency to repay + :param str amount: the amount to repay + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin loan structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'coin': currency['id'], + 'repayAmount': self.currency_to_precision(code, amount), + } + response = await self.privateMarginPostV2MarginCrossedAccountRepay(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700876704885, + # "data": { + # "remainDebtAmount": "0", + # "repayId": "1112122994945830912", + # "coin": "USDT", + # "repayAmount": "4.00006834" + # } + # } + # + data = self.safe_value(response, 'data', {}) + return self.parse_margin_loan(data, currency) + + def parse_margin_loan(self, info, currency: Currency = None, market: Market = None): + # + # isolated: borrowMargin + # + # { + # "loanId": "1112125304879067137", + # "symbol": "BTCUSDT", + # "coin": "USDT", + # "borrowAmount": "4" + # } + # + # cross: borrowMargin + # + # { + # "loanId": "1112122013642272769", + # "coin": "USDT", + # "borrowAmount": "4" + # } + # + # isolated: repayMargin + # + # { + # "remainDebtAmount": "0", + # "repayId": "1112126405439270912", + # "symbol": "BTCUSDT", + # "coin": "USDT", + # "repayAmount": "8.000137" + # } + # + # cross: repayMargin + # + # { + # "remainDebtAmount": "0", + # "repayId": "1112122994945830912", + # "coin": "USDT", + # "repayAmount": "4.00006834" + # } + # + currencyId = self.safe_string(info, 'coin') + marketId = self.safe_string(info, 'symbol') + symbol = None + if marketId is not None: + symbol = self.safe_symbol(marketId, market, None, 'spot') + return { + 'id': self.safe_string_2(info, 'loanId', 'repayId'), + 'currency': self.safe_currency_code(currencyId, currency), + 'amount': self.safe_number_2(info, 'borrowAmount', 'repayAmount'), + 'symbol': symbol, + 'timestamp': None, + 'datetime': None, + 'info': info, + } + + async def fetch_my_liquidations(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Liquidation]: + """ + retrieves the users liquidated positions + + https://www.bitget.com/api-doc/margin/cross/record/Get-Cross-Liquidation-Records + https://www.bitget.com/api-doc/margin/isolated/record/Get-Isolated-Liquidation-Records + + :param str [symbol]: unified CCXT market symbol + :param int [since]: the earliest time in ms to fetch liquidations for + :param int [limit]: the maximum number of liquidation structures to retrieve + :param dict [params]: exchange specific parameters for the bitget api endpoint + :param int [params.until]: timestamp in ms of the latest liquidation + :param str [params.marginMode]: 'cross' or 'isolated' default value is 'cross' + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict: an array of `liquidation structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchMyLiquidations', 'paginate') + if paginate: + return await self.fetch_paginated_call_cursor('fetchMyLiquidations', symbol, since, limit, params, 'minId', 'idLessThan') + market = None + if symbol is not None: + market = self.market(symbol) + type = None + type, params = self.handle_market_type_and_params('fetchMyLiquidations', market, params) + if type != 'spot': + raise NotSupported(self.id + ' fetchMyLiquidations() supports spot margin markets only') + request: dict = {} + request, params = self.handle_until_option('endTime', request, params) + if since is not None: + request['startTime'] = since + else: + request['startTime'] = self.milliseconds() - 7776000000 + if limit is not None: + request['limit'] = limit + response = None + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchMyLiquidations', params, 'cross') + if marginMode == 'isolated': + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMyLiquidations() requires a symbol argument') + request['symbol'] = market['id'] + response = await self.privateMarginGetV2MarginIsolatedLiquidationHistory(self.extend(request, params)) + elif marginMode == 'cross': + response = await self.privateMarginGetV2MarginCrossedLiquidationHistory(self.extend(request, params)) + # + # isolated + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1698114119193, + # "data": { + # "resultList": [ + # { + # "liqId": "123", + # "symbol": "BTCUSDT", + # "liqStartTime": "1653453245342", + # "liqEndTime": "16312423423432", + # "liqRiskRatio": "1.01", + # "totalAssets": "1242.34", + # "totalDebt": "1100", + # "liqFee": "1.2", + # "uTime": "1668134458717", + # "cTime": "1653453245342" + # } + # ], + # "maxId": "0", + # "minId": "0" + # } + # } + # + # cross + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1698114119193, + # "data": { + # "resultList": [ + # { + # "liqId": "123", + # "liqStartTime": "1653453245342", + # "liqEndTime": "16312423423432", + # "liqRiskRatio": "1.01", + # "totalAssets": "1242.34", + # "totalDebt": "1100", + # "LiqFee": "1.2", + # "uTime": "1668134458717", + # "cTime": "1653453245342" + # } + # ], + # "maxId": "0", + # "minId": "0" + # } + # } + # + data = self.safe_value(response, 'data', {}) + liquidations = self.safe_list(data, 'resultList', []) + return self.parse_liquidations(liquidations, market, since, limit) + + def parse_liquidation(self, liquidation, market: Market = None): + # + # isolated + # + # { + # "liqId": "123", + # "symbol": "BTCUSDT", + # "liqStartTime": "1653453245342", + # "liqEndTime": "16312423423432", + # "liqRiskRatio": "1.01", + # "totalAssets": "1242.34", + # "totalDebt": "1100", + # "liqFee": "1.2", + # "uTime": "1692690126000" + # "cTime": "1653453245342" + # } + # + # cross + # + # { + # "liqId": "123", + # "liqStartTime": "1653453245342", + # "liqEndTime": "16312423423432", + # "liqRiskRatio": "1.01", + # "totalAssets": "1242.34", + # "totalDebt": "1100", + # "LiqFee": "1.2", + # "uTime": "1692690126000" + # "cTime": "1653453245342" + # } + # + marketId = self.safe_string(liquidation, 'symbol') + timestamp = self.safe_integer(liquidation, 'liqEndTime') + liquidationFee = self.safe_string_2(liquidation, 'LiqFee', 'liqFee') + totalDebt = self.safe_string(liquidation, 'totalDebt') + quoteValueString = Precise.string_add(liquidationFee, totalDebt) + return self.safe_liquidation({ + 'info': liquidation, + 'symbol': self.safe_symbol(marketId, market), + 'contracts': None, + 'contractSize': None, + 'price': None, + 'baseValue': None, + 'quoteValue': self.parse_number(quoteValueString), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + + async def fetch_isolated_borrow_rate(self, symbol: str, params={}) -> IsolatedBorrowRate: + """ + fetch the rate of interest to borrow a currency for margin trading + + https://www.bitget.com/api-doc/margin/isolated/account/Isolated-Margin-Interest-Rate-And-Max-Borrowable-Amount + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `isolated borrow rate structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = await self.privateMarginGetV2MarginIsolatedInterestRateAndLimit(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700878692567, + # "data": [ + # { + # "symbol": "BTCUSDT", + # "leverage": "10", + # "baseCoin": "BTC", + # "baseTransferable": True, + # "baseBorrowable": True, + # "baseDailyInterestRate": "0.00007", + # "baseAnnuallyInterestRate": "0.02555", + # "baseMaxBorrowableAmount": "27", + # "baseVipList": [ + # {"level":"0","dailyInterestRate":"0.00007","limit":"27","annuallyInterestRate":"0.02555","discountRate":"1"}, + # {"level":"1","dailyInterestRate":"0.0000679","limit":"27.81","annuallyInterestRate":"0.0247835","discountRate":"0.97"}, + # {"level":"2","dailyInterestRate":"0.0000644","limit":"29.16","annuallyInterestRate":"0.023506","discountRate":"0.92"}, + # {"level":"3","dailyInterestRate":"0.0000602","limit":"31.32","annuallyInterestRate":"0.021973","discountRate":"0.86"}, + # {"level":"4","dailyInterestRate":"0.0000525","limit":"35.91","annuallyInterestRate":"0.0191625","discountRate":"0.75"}, + # {"level":"5","dailyInterestRate":"0.000042","limit":"44.82","annuallyInterestRate":"0.01533","discountRate":"0.6"} + # ], + # "quoteCoin": "USDT", + # "quoteTransferable": True, + # "quoteBorrowable": True, + # "quoteDailyInterestRate": "0.00041095", + # "quoteAnnuallyInterestRate": "0.14999675", + # "quoteMaxBorrowableAmount": "300000", + # "quoteList": [ + # {"level":"0","dailyInterestRate":"0.00041095","limit":"300000","annuallyInterestRate":"0.14999675","discountRate":"1"}, + # {"level":"1","dailyInterestRate":"0.00039863","limit":"309000","annuallyInterestRate":"0.14549995","discountRate":"0.97"}, + # {"level":"2","dailyInterestRate":"0.00037808","limit":"324000","annuallyInterestRate":"0.1379992","discountRate":"0.92"}, + # {"level":"3","dailyInterestRate":"0.00035342","limit":"348000","annuallyInterestRate":"0.1289983","discountRate":"0.86"}, + # {"level":"4","dailyInterestRate":"0.00030822","limit":"399000","annuallyInterestRate":"0.1125003","discountRate":"0.75"}, + # {"level":"5","dailyInterestRate":"0.00024657","limit":"498000","annuallyInterestRate":"0.08999805","discountRate":"0.6"} + # ] + # } + # ] + # } + # + timestamp = self.safe_integer(response, 'requestTime') + data = self.safe_value(response, 'data', []) + first = self.safe_value(data, 0, {}) + first['timestamp'] = timestamp + return self.parse_isolated_borrow_rate(first, market) + + def parse_isolated_borrow_rate(self, info: dict, market: Market = None) -> IsolatedBorrowRate: + # + # { + # "symbol": "BTCUSDT", + # "leverage": "10", + # "baseCoin": "BTC", + # "baseTransferable": True, + # "baseBorrowable": True, + # "baseDailyInterestRate": "0.00007", + # "baseAnnuallyInterestRate": "0.02555", + # "baseMaxBorrowableAmount": "27", + # "baseVipList": [ + # {"level":"0","dailyInterestRate":"0.00007","limit":"27","annuallyInterestRate":"0.02555","discountRate":"1"}, + # {"level":"1","dailyInterestRate":"0.0000679","limit":"27.81","annuallyInterestRate":"0.0247835","discountRate":"0.97"}, + # {"level":"2","dailyInterestRate":"0.0000644","limit":"29.16","annuallyInterestRate":"0.023506","discountRate":"0.92"}, + # {"level":"3","dailyInterestRate":"0.0000602","limit":"31.32","annuallyInterestRate":"0.021973","discountRate":"0.86"}, + # {"level":"4","dailyInterestRate":"0.0000525","limit":"35.91","annuallyInterestRate":"0.0191625","discountRate":"0.75"}, + # {"level":"5","dailyInterestRate":"0.000042","limit":"44.82","annuallyInterestRate":"0.01533","discountRate":"0.6"} + # ], + # "quoteCoin": "USDT", + # "quoteTransferable": True, + # "quoteBorrowable": True, + # "quoteDailyInterestRate": "0.00041095", + # "quoteAnnuallyInterestRate": "0.14999675", + # "quoteMaxBorrowableAmount": "300000", + # "quoteList": [ + # {"level":"0","dailyInterestRate":"0.00041095","limit":"300000","annuallyInterestRate":"0.14999675","discountRate":"1"}, + # {"level":"1","dailyInterestRate":"0.00039863","limit":"309000","annuallyInterestRate":"0.14549995","discountRate":"0.97"}, + # {"level":"2","dailyInterestRate":"0.00037808","limit":"324000","annuallyInterestRate":"0.1379992","discountRate":"0.92"}, + # {"level":"3","dailyInterestRate":"0.00035342","limit":"348000","annuallyInterestRate":"0.1289983","discountRate":"0.86"}, + # {"level":"4","dailyInterestRate":"0.00030822","limit":"399000","annuallyInterestRate":"0.1125003","discountRate":"0.75"}, + # {"level":"5","dailyInterestRate":"0.00024657","limit":"498000","annuallyInterestRate":"0.08999805","discountRate":"0.6"} + # ] + # } + # + marketId = self.safe_string(info, 'symbol') + symbol = self.safe_symbol(marketId, market, None, 'spot') + baseId = self.safe_string(info, 'baseCoin') + quoteId = self.safe_string(info, 'quoteCoin') + timestamp = self.safe_integer(info, 'timestamp') + return { + 'symbol': symbol, + 'base': self.safe_currency_code(baseId), + 'baseRate': self.safe_number(info, 'baseDailyInterestRate'), + 'quote': self.safe_currency_code(quoteId), + 'quoteRate': self.safe_number(info, 'quoteDailyInterestRate'), + 'period': 86400000, # 1-Day + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'info': info, + } + + async def fetch_cross_borrow_rate(self, code: str, params={}) -> CrossBorrowRate: + """ + fetch the rate of interest to borrow a currency for margin trading + + https://www.bitget.com/api-doc/margin/cross/account/Get-Cross-Margin-Interest-Rate-And-Borrowable + https://www.bitget.com/api-doc/uta/public/Get-Margin-Loans + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :returns dict: a `borrow rate structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'coin': currency['id'], + } + uta = None + response = None + result = None + uta, params = self.handle_option_and_params(params, 'fetchCrossBorrowRate', 'uta', False) + if uta: + response = await self.publicUtaGetV3MarketMarginLoans(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1752817798893, + # "data": { + # "dailyInterest": "0.00100008", + # "annualInterest": "0.3650292", + # "limit": "100" + # } + # } + # + result = self.safe_dict(response, 'data', {}) + else: + response = await self.privateMarginGetV2MarginCrossedInterestRateAndLimit(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700879047861, + # "data": [ + # { + # "coin": "BTC", + # "leverage": "3", + # "transferable": True, + # "borrowable": True, + # "dailyInterestRate": "0.00007", + # "annualInterestRate": "0.02555", + # "maxBorrowableAmount": "26", + # "vipList": [ + # {"level":"0","limit":"26","dailyInterestRate":"0.00007","annualInterestRate":"0.02555","discountRate":"1"}, + # {"level":"1","limit":"26.78","dailyInterestRate":"0.0000679","annualInterestRate":"0.0247835","discountRate":"0.97"}, + # {"level":"2","limit":"28.08","dailyInterestRate":"0.0000644","annualInterestRate":"0.023506","discountRate":"0.92"}, + # {"level":"3","limit":"30.16","dailyInterestRate":"0.0000602","annualInterestRate":"0.021973","discountRate":"0.86"}, + # {"level":"4","limit":"34.58","dailyInterestRate":"0.0000525","annualInterestRate":"0.0191625","discountRate":"0.75"}, + # {"level":"5","limit":"43.16","dailyInterestRate":"0.000042","annualInterestRate":"0.01533","discountRate":"0.6"} + # ] + # } + # ] + # } + # + data = self.safe_value(response, 'data', []) + result = self.safe_value(data, 0, {}) + timestamp = self.safe_integer(response, 'requestTime') + result['timestamp'] = timestamp + return self.parse_borrow_rate(result, currency) + + def parse_borrow_rate(self, info, currency: Currency = None): + # + # default + # + # { + # "coin": "BTC", + # "leverage": "3", + # "transferable": True, + # "borrowable": True, + # "dailyInterestRate": "0.00007", + # "annualInterestRate": "0.02555", + # "maxBorrowableAmount": "26", + # "vipList": [ + # {"level":"0","limit":"26","dailyInterestRate":"0.00007","annualInterestRate":"0.02555","discountRate":"1"}, + # {"level":"1","limit":"26.78","dailyInterestRate":"0.0000679","annualInterestRate":"0.0247835","discountRate":"0.97"}, + # {"level":"2","limit":"28.08","dailyInterestRate":"0.0000644","annualInterestRate":"0.023506","discountRate":"0.92"}, + # {"level":"3","limit":"30.16","dailyInterestRate":"0.0000602","annualInterestRate":"0.021973","discountRate":"0.86"}, + # {"level":"4","limit":"34.58","dailyInterestRate":"0.0000525","annualInterestRate":"0.0191625","discountRate":"0.75"}, + # {"level":"5","limit":"43.16","dailyInterestRate":"0.000042","annualInterestRate":"0.01533","discountRate":"0.6"} + # ] + # } + # + # uta + # + # { + # "dailyInterest": "0.00100008", + # "annualInterest": "0.3650292", + # "limit": "100" + # } + # + currencyId = self.safe_string(info, 'coin') + timestamp = self.safe_integer(info, 'timestamp') + return { + 'currency': self.safe_currency_code(currencyId, currency), + 'rate': self.safe_number_2(info, 'dailyInterestRate', 'dailyInterest'), + 'period': 86400000, # 1-Day + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'info': info, + } + + async def fetch_borrow_interest(self, code: Str = None, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[BorrowInterest]: + """ + fetch the interest owed by the user for borrowing currency for margin trading + + https://www.bitget.com/api-doc/margin/cross/record/Get-Cross-Interest-Records + https://www.bitget.com/api-doc/margin/isolated/record/Get-Isolated-Interest-Records + + :param str [code]: unified currency code + :param str [symbol]: unified market symbol when fetching interest in isolated markets + :param int [since]: the earliest time in ms to fetch borrow interest for + :param int [limit]: the maximum number of structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `borrow interest structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchBorrowInterest', 'paginate') + if paginate: + return await self.fetch_paginated_call_cursor('fetchBorrowInterest', symbol, since, limit, params, 'minId', 'idLessThan') + market = None + if symbol is not None: + market = self.market(symbol) + request: dict = {} + currency = None + if code is not None: + currency = self.currency(code) + request['coin'] = currency['id'] + if since is not None: + request['startTime'] = since + else: + request['startTime'] = self.milliseconds() - 7776000000 + if limit is not None: + request['limit'] = limit + response = None + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchBorrowInterest', params, 'cross') + if marginMode == 'isolated': + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchBorrowInterest() requires a symbol argument') + request['symbol'] = market['id'] + response = await self.privateMarginGetV2MarginIsolatedInterestHistory(self.extend(request, params)) + elif marginMode == 'cross': + response = await self.privateMarginGetV2MarginCrossedInterestHistory(self.extend(request, params)) + # + # isolated + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700879935189, + # "data": { + # "resultList": [ + # { + # "interestId": "1112125304879067137", + # "interestCoin": "USDT", + # "dailyInterestRate": "0.00041095", + # "loanCoin": "USDT", + # "interestAmount": "0.0000685", + # "interstType": "first", + # "symbol": "BTCUSDT", + # "cTime": "1700877255648", + # "uTime": "1700877255648" + # }, + # ], + # "maxId": "1112125304879067137", + # "minId": "1100138015672119298" + # } + # } + # + # cross + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700879597044, + # "data": { + # "resultList": [ + # { + # "interestId": "1112122013642272769", + # "interestCoin": "USDT", + # "dailyInterestRate": "0.00041", + # "loanCoin": "USDT", + # "interestAmount": "0.00006834", + # "interstType": "first", + # "cTime": "1700876470957", + # "uTime": "1700876470957" + # }, + # ], + # "maxId": "1112122013642272769", + # "minId": "1096917004629716993" + # } + # } + # + data = self.safe_value(response, 'data', {}) + rows = self.safe_value(data, 'resultList', []) + interest = self.parse_borrow_interests(rows, market) + return self.filter_by_currency_since_limit(interest, code, since, limit) + + def parse_borrow_interest(self, info: dict, market: Market = None) -> BorrowInterest: + # + # isolated + # + # { + # "interestId": "1112125304879067137", + # "interestCoin": "USDT", + # "dailyInterestRate": "0.00041095", + # "loanCoin": "USDT", + # "interestAmount": "0.0000685", + # "interstType": "first", + # "symbol": "BTCUSDT", + # "cTime": "1700877255648", + # "uTime": "1700877255648" + # } + # + # cross + # + # { + # "interestId": "1112122013642272769", + # "interestCoin": "USDT", + # "dailyInterestRate": "0.00041", + # "loanCoin": "USDT", + # "interestAmount": "0.00006834", + # "interstType": "first", + # "cTime": "1700876470957", + # "uTime": "1700876470957" + # } + # + marketId = self.safe_string(info, 'symbol') + market = self.safe_market(marketId, market) + marginMode = 'isolated' if (marketId is not None) else 'cross' + timestamp = self.safe_integer(info, 'cTime') + return { + 'info': info, + 'symbol': self.safe_string(market, 'symbol'), + 'currency': self.safe_currency_code(self.safe_string(info, 'interestCoin')), + 'interest': self.safe_number(info, 'interestAmount'), + 'interestRate': self.safe_number(info, 'dailyInterestRate'), + 'amountBorrowed': None, + 'marginMode': marginMode, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + + async def close_position(self, symbol: str, side: OrderSide = None, params={}) -> Order: + """ + closes an open position for a market + + https://www.bitget.com/api-doc/contract/trade/Flash-Close-Position + https://www.bitget.com/api-doc/uta/trade/Close-All-Positions + + :param str symbol: unified CCXT market symbol + :param str [side]: one-way mode: 'buy' or 'sell', hedge-mode: 'long' or 'short' + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :returns dict: An `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + productType = None + uta = None + response = None + productType, params = self.handle_product_type_and_params(market, params) + uta, params = self.handle_option_and_params(params, 'closePosition', 'uta', False) + if uta: + if side is not None: + request['posSide'] = side + request['category'] = productType + response = await self.privateUtaPostV3TradeClosePositions(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1751020218384, + # "data": { + # "list": [ + # { + # "orderId": "1322440134099320832", + # "clientOid": "1322440134099320833" + # } + # ] + # } + # } + # + else: + if side is not None: + request['holdSide'] = side + request['productType'] = productType + response = await self.privateMixPostV2MixOrderClosePositions(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1702975017017, + # "data": { + # "successList": [ + # { + # "orderId": "1120923953904893955", + # "clientOid": "1120923953904893956" + # } + # ], + # "failureList": [], + # "result": False + # } + # } + # + data = self.safe_value(response, 'data', {}) + order = self.safe_list_2(data, 'successList', 'list', []) + return self.parse_order(order[0], market) + + async def close_all_positions(self, params={}) -> List[Position]: + """ + closes all open positions for a market type + + https://www.bitget.com/api-doc/contract/trade/Flash-Close-Position + https://www.bitget.com/api-doc/uta/trade/Close-All-Positions + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.productType]: 'USDT-FUTURES', 'USDC-FUTURES', 'COIN-FUTURES', 'SUSDT-FUTURES', 'SUSDC-FUTURES' or 'SCOIN-FUTURES' + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :returns dict[]: A list of `position structures ` + """ + await self.load_markets() + request: dict = {} + productType = None + uta = None + response = None + productType, params = self.handle_product_type_and_params(None, params) + uta, params = self.handle_option_and_params(params, 'closeAllPositions', 'uta', False) + if uta: + request['category'] = productType + response = await self.privateUtaPostV3TradeClosePositions(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1751020218384, + # "data": { + # "list": [ + # { + # "orderId": "1322440134099320832", + # "clientOid": "1322440134099320833" + # } + # ] + # } + # } + # + else: + request['productType'] = productType + response = await self.privateMixPostV2MixOrderClosePositions(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1702975017017, + # "data": { + # "successList": [ + # { + # "orderId": "1120923953904893955", + # "clientOid": "1120923953904893956" + # } + # ], + # "failureList": [], + # "result": False + # } + # } + # + data = self.safe_value(response, 'data', {}) + orderInfo = self.safe_list_2(data, 'successList', 'list', []) + return self.parse_positions(orderInfo, None, params) + + async def fetch_margin_mode(self, symbol: str, params={}) -> MarginMode: + """ + fetches the margin mode of a trading pair + + https://www.bitget.com/api-doc/contract/account/Get-Single-Account + + :param str symbol: unified symbol of the market to fetch the margin mode for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin mode structure ` + """ + await self.load_markets() + market = self.market(symbol) + productType = None + productType, params = self.handle_product_type_and_params(market, params) + request: dict = { + 'symbol': market['id'], + 'marginCoin': market['settleId'], + 'productType': productType, + } + response = await self.privateMixGetV2MixAccountAccount(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1709791216652, + # "data": { + # "marginCoin": "USDT", + # "locked": "0", + # "available": "19.88811074", + # "crossedMaxAvailable": "19.88811074", + # "isolatedMaxAvailable": "19.88811074", + # "maxTransferOut": "19.88811074", + # "accountEquity": "19.88811074", + # "usdtEquity": "19.888110749166", + # "btcEquity": "0.000302183391", + # "crossedRiskRate": "0", + # "crossedMarginLeverage": 20, + # "isolatedLongLever": 20, + # "isolatedShortLever": 20, + # "marginMode": "crossed", + # "posMode": "hedge_mode", + # "unrealizedPL": "0", + # "coupon": "0", + # "crossedUnrealizedPL": "0", + # "isolatedUnrealizedPL": "" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_margin_mode(data, market) + + def parse_margin_mode(self, marginMode: dict, market=None) -> MarginMode: + marginType = self.safe_string(marginMode, 'marginMode') + marginType = 'cross' if (marginType == 'crossed') else marginType + return { + 'info': marginMode, + 'symbol': market['symbol'], + 'marginMode': marginType, + } + + async def fetch_positions_history(self, symbols: Strings = None, since: Int = None, limit: Int = None, params={}) -> List[Position]: + """ + fetches historical positions + + https://www.bitget.com/api-doc/contract/position/Get-History-Position + https://www.bitget.com/api-doc/uta/trade/Get-Position-History + + :param str[] [symbols]: unified contract symbols + :param int [since]: timestamp in ms of the earliest position to fetch, default=3 months ago, max range for params["until"] - since is 3 months + :param int [limit]: the maximum amount of records to fetch, default=20, max=100 + :param dict params: extra parameters specific to the exchange api endpoint + :param int [params.until]: timestamp in ms of the latest position to fetch, max range for params["until"] - since is 3 months + :param str [params.productType]: USDT-FUTURES(default), COIN-FUTURES, USDC-FUTURES, SUSDT-FUTURES, SCOIN-FUTURES, or SUSDC-FUTURES + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :returns dict[]: a list of `position structures ` + """ + await self.load_markets() + request: dict = {} + market = None + productType = None + uta = None + response = None + if symbols is not None: + symbolsLength = len(symbols) + if symbolsLength > 0: + market = self.market(symbols[0]) + request['symbol'] = market['id'] + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + request, params = self.handle_until_option('endTime', request, params) + productType, params = self.handle_product_type_and_params(market, params) + uta, params = self.handle_option_and_params(params, 'fetchPositionsHistory', 'uta', False) + if uta: + request['category'] = productType + response = await self.privateUtaGetV3PositionHistoryPosition(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1751020950427, + # "data": { + # "list": [ + # { + # "positionId": "1322441328637100049", + # "category": "USDT-FUTURES", + # "symbol": "BTCUSDT", + # "marginCoin": "USDT", + # "holdMode": "hedge_mode", + # "posSide": "long", + # "marginMode": "crossed", + # "openPriceAvg": "107003.7", + # "closePriceAvg": "107005.4", + # "openTotalPos": "0.0001", + # "closeTotalPos": "0.0001", + # "cumRealisedPnl": "0.00017", + # "netProfit": "-0.01267055", + # "totalFunding": "0", + # "openFeeTotal": "-0.00642022", + # "closeFeeTotal": "-0.00642032", + # "createdTime": "1751020503195", + # "updatedTime": "1751020520458" + # }, + # ], + # "cursor": "1322440134158041089" + # } + # } + # + else: + response = await self.privateMixGetV2MixPositionHistoryPosition(self.extend(request, params)) + # + # { + # code: '00000', + # msg: 'success', + # requestTime: '1712794148791', + # data: { + # list: [ + # { + # symbol: 'XRPUSDT', + # marginCoin: 'USDT', + # holdSide: 'long', + # openAvgPrice: '0.64967', + # closeAvgPrice: '0.58799', + # marginMode: 'isolated', + # openTotalPos: '10', + # closeTotalPos: '10', + # pnl: '-0.62976205', + # netProfit: '-0.65356802', + # totalFunding: '-0.01638', + # openFee: '-0.00389802', + # closeFee: '-0.00352794', + # ctime: '1709590322199', + # utime: '1709667583395' + # }, + # ... + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + responseList = self.safe_list(data, 'list', []) + positions = self.parse_positions(responseList, symbols, params) + return self.filter_by_since_limit(positions, since, limit) + + async def fetch_convert_quote(self, fromCode: str, toCode: str, amount: Num = None, params={}) -> Conversion: + """ + fetch a quote for converting from one currency to another + + https://www.bitget.com/api-doc/common/convert/Get-Quoted-Price + + :param str fromCode: the currency that you want to sell and convert from + :param str toCode: the currency that you want to buy and convert into + :param float [amount]: how much you want to trade in units of the from currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `conversion structure ` + """ + await self.load_markets() + request: dict = { + 'fromCoin': fromCode, + 'toCoin': toCode, + 'fromCoinSize': self.number_to_string(amount), + } + response = await self.privateConvertGetV2ConvertQuotedPrice(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1712121940158, + # "data": { + # "fromCoin": "USDT", + # "fromCoinSize": "5", + # "cnvtPrice": "0.9993007892377704", + # "toCoin": "USDC", + # "toCoinSize": "4.99650394", + # "traceId": "1159288930228187140", + # "fee": "0" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + fromCurrencyId = self.safe_string(data, 'fromCoin', fromCode) + fromCurrency = self.currency(fromCurrencyId) + toCurrencyId = self.safe_string(data, 'toCoin', toCode) + toCurrency = self.currency(toCurrencyId) + return self.parse_conversion(data, fromCurrency, toCurrency) + + async def create_convert_trade(self, id: str, fromCode: str, toCode: str, amount: Num = None, params={}) -> Conversion: + """ + convert from one currency to another + + https://www.bitget.com/api-doc/common/convert/Trade + + :param str id: the id of the trade that you want to make + :param str fromCode: the currency that you want to sell and convert from + :param str toCode: the currency that you want to buy and convert into + :param float amount: how much you want to trade in units of the from currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str params['price']: the price of the conversion, obtained from fetchConvertQuote() + :param str params['toAmount']: the amount you want to trade in units of the toCurrency, obtained from fetchConvertQuote() + :returns dict: a `conversion structure ` + """ + await self.load_markets() + price = self.safe_string_2(params, 'price', 'cnvtPrice') + if price is None: + raise ArgumentsRequired(self.id + ' createConvertTrade() requires a price parameter') + toAmount = self.safe_string_2(params, 'toAmount', 'toCoinSize') + if toAmount is None: + raise ArgumentsRequired(self.id + ' createConvertTrade() requires a toAmount parameter') + params = self.omit(params, ['price', 'toAmount']) + request: dict = { + 'traceId': id, + 'fromCoin': fromCode, + 'toCoin': toCode, + 'fromCoinSize': self.number_to_string(amount), + 'toCoinSize': toAmount, + 'cnvtPrice': price, + } + response = await self.privateConvertPostV2ConvertTrade(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1712123746203, + # "data": { + # "cnvtPrice": "0.99940076", + # "toCoin": "USDC", + # "toCoinSize": "4.99700379", + # "ts": "1712123746217" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + toCurrencyId = self.safe_string(data, 'toCoin', toCode) + toCurrency = self.currency(toCurrencyId) + return self.parse_conversion(data, None, toCurrency) + + async def fetch_convert_trade_history(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Conversion]: + """ + fetch the users history of conversion trades + + https://www.bitget.com/api-doc/common/convert/Get-Convert-Record + + :param str [code]: the unified currency code + :param int [since]: the earliest time in ms to fetch conversions for + :param int [limit]: the maximum number of conversion structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `conversion structures ` + """ + await self.load_markets() + request: dict = {} + msInDay = 86400000 + now = self.milliseconds() + if since is not None: + request['startTime'] = since + else: + request['startTime'] = now - msInDay + endTime = self.safe_string_2(params, 'endTime', 'until') + if endTime is not None: + request['endTime'] = endTime + else: + request['endTime'] = now + if limit is not None: + request['limit'] = limit + params = self.omit(params, 'until') + response = await self.privateConvertGetV2ConvertConvertRecord(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1712124371799, + # "data": { + # "dataList": [ + # { + # "id": "1159296505255219205", + # "fromCoin": "USDT", + # "fromCoinSize": "5", + # "cnvtPrice": "0.99940076", + # "toCoin": "USDC", + # "toCoinSize": "4.99700379", + # "ts": "1712123746217", + # "fee": "0" + # } + # ], + # "endId": "1159296505255219205" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + dataList = self.safe_list(data, 'dataList', []) + return self.parse_conversions(dataList, code, 'fromCoin', 'toCoin', since, limit) + + def parse_conversion(self, conversion: dict, fromCurrency: Currency = None, toCurrency: Currency = None) -> Conversion: + # + # fetchConvertQuote + # + # { + # "fromCoin": "USDT", + # "fromCoinSize": "5", + # "cnvtPrice": "0.9993007892377704", + # "toCoin": "USDC", + # "toCoinSize": "4.99650394", + # "traceId": "1159288930228187140", + # "fee": "0" + # } + # + # createConvertTrade + # + # { + # "cnvtPrice": "0.99940076", + # "toCoin": "USDC", + # "toCoinSize": "4.99700379", + # "ts": "1712123746217" + # } + # + # fetchConvertTradeHistory + # + # { + # "id": "1159296505255219205", + # "fromCoin": "USDT", + # "fromCoinSize": "5", + # "cnvtPrice": "0.99940076", + # "toCoin": "USDC", + # "toCoinSize": "4.99700379", + # "ts": "1712123746217", + # "fee": "0" + # } + # + timestamp = self.safe_integer(conversion, 'ts') + fromCoin = self.safe_string(conversion, 'fromCoin') + fromCode = self.safe_currency_code(fromCoin, fromCurrency) + to = self.safe_string(conversion, 'toCoin') + toCode = self.safe_currency_code(to, toCurrency) + return { + 'info': conversion, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': self.safe_string_2(conversion, 'id', 'traceId'), + 'fromCurrency': fromCode, + 'fromAmount': self.safe_number(conversion, 'fromCoinSize'), + 'toCurrency': toCode, + 'toAmount': self.safe_number(conversion, 'toCoinSize'), + 'price': self.safe_number(conversion, 'cnvtPrice'), + 'fee': self.safe_number(conversion, 'fee'), + } + + async def fetch_convert_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies that can be converted + + https://www.bitget.com/api-doc/common/convert/Get-Convert-Currencies + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + await self.load_markets() + response = await self.privateConvertGetV2ConvertCurrencies(params) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1712121755897, + # "data": [ + # { + # "coin": "BTC", + # "available": "0.00009850", + # "maxAmount": "0.756266", + # "minAmount": "0.00001" + # }, + # ] + # } + # + result: dict = {} + data = self.safe_list(response, 'data', []) + for i in range(0, len(data)): + entry = data[i] + id = self.safe_string(entry, 'coin') + code = self.safe_currency_code(id) + result[code] = { + 'info': entry, + 'id': id, + 'code': code, + 'networks': None, + 'type': None, + 'name': None, + 'active': None, + 'deposit': None, + 'withdraw': self.safe_number(entry, 'available'), + 'fee': None, + 'precision': None, + 'limits': { + 'amount': { + 'min': self.safe_number(entry, 'minAmount'), + 'max': self.safe_number(entry, 'maxAmount'), + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + 'deposit': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + } + return result + + async def fetch_funding_interval(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate interval + + https://www.bitget.com/api-doc/contract/market/Get-Symbol-Next-Funding-Time + https://www.bitget.com/api-doc/uta/public/Get-Current-Funding-Rate + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :returns dict: a `funding rate structure ` + """ + await self.load_markets() + market = self.market(symbol) + productType = None + productType, params = self.handle_product_type_and_params(market, params) + request: dict = { + 'symbol': market['id'], + } + response = None + uta = None + uta, params = self.handle_option_and_params(params, 'fetchFundingInterval', 'uta', False) + if uta: + response = await self.publicUtaGetV3MarketCurrentFundRate(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1752880157959, + # "data": [ + # { + # "symbol": "BTCUSDT", + # "fundingRate": "0.0001", + # "fundingRateInterval": "8", + # "nextUpdate": "1752883200000", + # "minFundingRate": "-0.003", + # "maxFundingRate": "0.003" + # } + # ] + # } + # + else: + request['productType'] = productType + response = await self.publicMixGetV2MixMarketFundingTime(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1727930153888, + # "data": [ + # { + # "symbol": "BTCUSDT", + # "nextFundingTime": "1727942400000", + # "ratePeriod": "8" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + first = self.safe_dict(data, 0, {}) + return self.parse_funding_rate(first, market) + + async def fetch_long_short_ratio_history(self, symbol: Str = None, timeframe: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LongShortRatio]: + """ + fetches the long short ratio history for a unified market symbol + + https://www.bitget.com/api-doc/common/apidata/Margin-Ls-Ratio + https://www.bitget.com/api-doc/common/apidata/Account-Long-Short + + :param str symbol: unified symbol of the market to fetch the long short ratio for + :param str [timeframe]: the period for the ratio + :param int [since]: the earliest time in ms to fetch ratios for + :param int [limit]: the maximum number of long short ratio structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of `long short ratio structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if timeframe is not None: + request['period'] = timeframe + response = None + if market['swap'] or market['future']: + response = await self.publicMixGetV2MixMarketAccountLongShort(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1729321233281, + # "data": [ + # { + # "longAccountRatio": "0.58", + # "shortAccountRatio": "0.42", + # "longShortAccountRatio": "0.0138", + # "ts": "1729312200000" + # }, + # ] + # } + # + else: + response = await self.publicMarginGetV2MarginMarketLongShortRatio(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1729306974712, + # "data": [ + # { + # "longShortRatio": "40.66", + # "ts": "1729306800000" + # }, + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_long_short_ratio_history(data, market) + + def parse_long_short_ratio(self, info: dict, market: Market = None) -> LongShortRatio: + marketId = self.safe_string(info, 'symbol') + timestamp = self.safe_integer_omit_zero(info, 'ts') + return { + 'info': info, + 'symbol': self.safe_symbol(marketId, market, None, 'contract'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'timeframe': None, + 'longShortRatio': self.safe_number_2(info, 'longShortRatio', 'longShortAccountRatio'), + } + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if not response: + return None # fallback to default error handler + # + # spot + # + # {"code":"00000","msg":"success","requestTime":1713294492511,"data":[...]}" + # + # {"status":"fail","err_code":"01001","err_msg":"系统异常,请稍后重试"} + # {"status":"error","ts":1595594160149,"err_code":"invalid-parameter","err_msg":"invalid size, valid range: [1,2000]"} + # {"status":"error","ts":1595684716042,"err_code":"invalid-parameter","err_msg":"illegal sign invalid"} + # {"status":"error","ts":1595700216275,"err_code":"bad-request","err_msg":"your balance is low!"} + # {"status":"error","ts":1595700344504,"err_code":"invalid-parameter","err_msg":"invalid type"} + # {"status":"error","ts":1595703343035,"err_code":"bad-request","err_msg":"order cancel fail"} + # {"status":"error","ts":1595704360508,"err_code":"invalid-parameter","err_msg":"accesskey not null"} + # {"status":"error","ts":1595704490084,"err_code":"invalid-parameter","err_msg":"permissions not right"} + # {"status":"error","ts":1595711862763,"err_code":"system exception","err_msg":"system exception"} + # {"status":"error","ts":1595730308979,"err_code":"bad-request","err_msg":"20003"} + # + # swap + # + # {"code":"40015","msg":"","requestTime":1595698564931,"data":null} + # {"code":"40017","msg":"Order id must not be blank","requestTime":1595702477835,"data":null} + # {"code":"40017","msg":"Order Type must not be blank","requestTime":1595698516162,"data":null} + # {"code":"40301","msg":"","requestTime":1595667662503,"data":null} + # {"code":"40017","msg":"Contract code must not be blank","requestTime":1595703151651,"data":null} + # {"code":"40108","msg":"","requestTime":1595885064600,"data":null} + # {"order_id":"513468410013679613","client_oid":null,"symbol":"ethusd","result":false,"err_code":"order_no_exist_error","err_msg":"订单不存在!"} + # + message = self.safe_string_2(response, 'err_msg', 'msg') + feedback = self.id + ' ' + body + nonEmptyMessage = ((message is not None) and (message != '') and (message != 'success')) + if nonEmptyMessage: + self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + errorCode = self.safe_string_2(response, 'code', 'err_code') + nonZeroErrorCode = (errorCode is not None) and (errorCode != '00000') + if nonZeroErrorCode: + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + if nonZeroErrorCode or nonEmptyMessage: + raise ExchangeError(feedback) # unknown message + return None + + def nonce(self): + return self.milliseconds() - self.options['timeDifference'] + + def sign(self, path, api=[], method='GET', params={}, headers=None, body=None): + signed = api[0] == 'private' + endpoint = api[1] + pathPart = '/api' + request = '/' + self.implode_params(path, params) + payload = pathPart + request + url = self.implode_hostname(self.urls['api'][endpoint]) + payload + query = self.omit(params, self.extract_params(path)) + if not signed and (method == 'GET'): + keys = list(query.keys()) + keysLength = len(keys) + if keysLength > 0: + url = url + '?' + self.urlencode(query) + if signed: + self.check_required_credentials() + timestamp = str(self.nonce()) + auth = timestamp + method + payload + if method == 'POST': + body = self.json(params) + auth += body + else: + if params: + queryInner = '?' + self.urlencode(self.keysort(params)) + # check #21169 pr + if queryInner.find('%24') > -1: + queryInner = queryInner.replace('%24', '$') + url += queryInner + auth += queryInner + signature = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256, 'base64') + broker = self.safe_string(self.options, 'broker') + headers = { + 'ACCESS-KEY': self.apiKey, + 'ACCESS-SIGN': signature, + 'ACCESS-TIMESTAMP': timestamp, + 'ACCESS-PASSPHRASE': self.password, + 'X-CHANNEL-API-CODE': broker, + } + if method == 'POST': + headers['Content-Type'] = 'application/json' + sandboxMode = self.safe_bool_2(self.options, 'sandboxMode', 'sandbox', False) + if sandboxMode and (path != 'v2/public/time') and (path != 'v3/market/current-fund-rate'): + # https://github.com/ccxt/ccxt/issues/25252#issuecomment-2662742336 + if headers is None: + headers = {} + productType = self.safe_string(params, 'productType') + if (productType != 'SCOIN-FUTURES') and (productType != 'SUSDT-FUTURES') and (productType != 'SUSDC-FUTURES'): + headers['PAPTRADING'] = '1' + return {'url': url, 'method': method, 'body': body, 'headers': headers} diff --git a/ccxt/async_support/bithumb.py b/ccxt/async_support/bithumb.py new file mode 100644 index 0000000..5fd6d75 --- /dev/null +++ b/ccxt/async_support/bithumb.py @@ -0,0 +1,1227 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.bithumb import ImplicitAPI +import asyncio +import hashlib +from ccxt.base.types import Any, Balances, Currency, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, Transaction, MarketInterface +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import InvalidAddress +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.decimal_to_precision import TRUNCATE +from ccxt.base.decimal_to_precision import DECIMAL_PLACES +from ccxt.base.decimal_to_precision import SIGNIFICANT_DIGITS +from ccxt.base.precise import Precise + + +class bithumb(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(bithumb, self).describe(), { + 'id': 'bithumb', + 'name': 'Bithumb', + 'countries': ['KR'], # South Korea + 'rateLimit': 500, + 'pro': True, + 'has': { + 'CORS': True, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelOrder': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createMarketOrder': True, + 'createOrder': True, + 'createOrderWithTakeProfitAndStopLoss': False, + 'createOrderWithTakeProfitAndStopLossWs': False, + 'createReduceOnlyOrder': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': False, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTrades': True, + 'fetchTransfer': False, + 'fetchTransfers': False, + 'fetchVolatilityHistory': False, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'transfer': False, + 'withdraw': True, + }, + 'hostname': 'bithumb.com', + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/c9e0eefb-4777-46b9-8f09-9d7f7c4af82d', + 'api': { + 'public': 'https://api.{hostname}/public', + 'private': 'https://api.{hostname}', + }, + 'www': 'https://www.bithumb.com', + 'doc': 'https://apidocs.bithumb.com', + 'fees': 'https://en.bithumb.com/customer_support/info_fee', + }, + 'api': { + 'public': { + 'get': [ + 'ticker/ALL_{quoteId}', + 'ticker/{baseId}_{quoteId}', + 'orderbook/ALL_{quoteId}', + 'orderbook/{baseId}_{quoteId}', + 'transaction_history/{baseId}_{quoteId}', + 'network-info', + 'assetsstatus/multichain/ALL', + 'assetsstatus/multichain/{currency}', + 'withdraw/minimum/ALL', + 'withdraw/minimum/{currency}', + 'assetsstatus/ALL', + 'assetsstatus/{baseId}', + 'candlestick/{baseId}_{quoteId}/{interval}', + ], + }, + 'private': { + 'post': [ + 'info/account', + 'info/balance', + 'info/wallet_address', + 'info/ticker', + 'info/orders', + 'info/user_transactions', + 'info/order_detail', + 'trade/place', + 'trade/cancel', + 'trade/btc_withdrawal', + 'trade/krw_deposit', + 'trade/krw_withdrawal', + 'trade/market_buy', + 'trade/market_sell', + 'trade/stop_limit', + ], + }, + }, + 'fees': { + 'trading': { + 'maker': self.parse_number('0.0025'), + 'taker': self.parse_number('0.0025'), + }, + }, + 'precisionMode': SIGNIFICANT_DIGITS, + # todo: update to v2 apis + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': False, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': False, + 'FOK': False, + 'PO': False, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyRequiresPrice': False, + 'marketBuyByCost': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': None, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 1000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOrders': None, + 'fetchClosedOrders': None, + 'fetchOHLCV': { + 'limit': 1000, + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'exceptions': { + 'Bad Request(SSL)': BadRequest, + 'Bad Request(Bad Method)': BadRequest, + 'Bad Request.(Auth Data)': AuthenticationError, # {"status": "5100", "message": "Bad Request.(Auth Data)"} + 'Not Member': AuthenticationError, + 'Invalid Apikey': AuthenticationError, # {"status":"5300","message":"Invalid Apikey"} + 'Method Not Allowed.(Access IP)': PermissionDenied, + 'Method Not Allowed.(BTC Adress)': InvalidAddress, + 'Method Not Allowed.(Access)': PermissionDenied, + 'Database Fail': ExchangeNotAvailable, + 'Invalid Parameter': BadRequest, + '5600': ExchangeError, + 'Unknown Error': ExchangeError, + 'After May 23th, recent_transactions is no longer, hence users will not be able to connect to recent_transactions': ExchangeError, # {"status":"5100","message":"After May 23th, recent_transactions is no longer, hence users will not be able to connect to recent_transactions"} + }, + 'timeframes': { + '1m': '1m', + '3m': '3m', + '5m': '5m', + '10m': '10m', + '30m': '30m', + '1h': '1h', + '6h': '6h', + '12h': '12h', + '1d': '24h', + }, + 'options': { + 'quoteCurrencies': { + 'BTC': { + 'limits': { + 'cost': { + 'min': 0.0002, + 'max': 100, + }, + }, + }, + 'KRW': { + 'limits': { + 'cost': { + 'min': 500, + 'max': 5000000000, + }, + }, + }, + 'USDT': { + 'limits': { + 'cost': { + 'min': None, + 'max': None, + }, + }, + }, + }, + }, + 'commonCurrencies': { + 'ALT': 'ArchLoot', + 'FTC': 'FTC2', + 'SOC': 'Soda Coin', + }, + }) + + def safe_market(self, marketId: Str = None, market: Market = None, delimiter: Str = None, marketType: Str = None) -> MarketInterface: + # bithumb has a different type of conflict in markets, because + # their ids are the base currency(BTC for instance), so we can have + # multiple "BTC" ids representing the different markets(BTC/ETH, "BTC/DOGE", etc) + # since they're the same we just need to return one + return super(bithumb, self).safe_market(marketId, market, delimiter, 'spot') + + def amount_to_precision(self, symbol, amount): + return self.decimal_to_precision(amount, TRUNCATE, self.markets[symbol]['precision']['amount'], DECIMAL_PLACES) + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for bithumb + + https://apidocs.bithumb.com/v1.2.0/reference/%ED%98%84%EC%9E%AC%EA%B0%80-%EC%A0%95%EB%B3%B4-%EC%A1%B0%ED%9A%8C-all + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + result = [] + quoteCurrencies = self.safe_dict(self.options, 'quoteCurrencies', {}) + quotes = list(quoteCurrencies.keys()) + promises = [] + for i in range(0, len(quotes)): + request = { + 'quoteId': quotes[i], + } + promises.append(self.publicGetTickerALLQuoteId(self.extend(request, params))) + # + # { + # "status": "0000", + # "data": { + # "ETH": { + # "opening_price": "0.05153399", + # "closing_price": "0.05145144", + # "min_price": "0.05145144", + # "max_price": "0.05160781", + # "units_traded": "6.541124172077830855", + # "acc_trade_value": "0.33705472498492329997697755", + # "prev_closing_price": "0.0515943", + # "units_traded_24H": "43.368879902677400513", + # "acc_trade_value_24H": "2.24165339555398079994373342", + # "fluctate_24H": "-0.00018203", + # "fluctate_rate_24H": "-0.35" + # }, + # "XRP": { + # "opening_price": "0.00000918", + # "closing_price": "0.0000092", + # "min_price": "0.00000918", + # "max_price": "0.0000092", + # "units_traded": "6516.949363", + # "acc_trade_value": "0.0598792533602796", + # "prev_closing_price": "0.00000916", + # "units_traded_24H": "229161.50354738", + # "acc_trade_value_24H": "2.0446589371637117", + # "fluctate_24H": "0.00000049", + # "fluctate_rate_24H": "5.63" + # }, + # ... + # "date": "1721675913145" + # } + # } + # + results = await asyncio.gather(*promises) + for i in range(0, len(quotes)): + quote = quotes[i] + quoteId = quote + response = results[i] + data = self.safe_dict(response, 'data') + extension = self.safe_dict(quoteCurrencies, quote, {}) + currencyIds = list(data.keys()) + for j in range(0, len(currencyIds)): + currencyId = currencyIds[j] + if currencyId == 'date': + continue + market = data[currencyId] + base = self.safe_currency_code(currencyId) + active = True + if isinstance(market, list): + numElements = len(market) + if numElements == 0: + active = False + entry = self.deep_extend({ + 'id': currencyId, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': currencyId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': active, + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDateTime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': int('4'), + 'price': int('4'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': None, + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': {}, # set via options + }, + 'created': None, + 'info': market, + }, extension) + result.append(entry) + return result + + def parse_balance(self, response) -> Balances: + result: dict = {'info': response} + balances = self.safe_dict(response, 'data') + codes = list(self.currencies.keys()) + for i in range(0, len(codes)): + code = codes[i] + account = self.account() + currency = self.currency(code) + lowerCurrencyId = self.safe_string_lower(currency, 'id') + account['total'] = self.safe_string(balances, 'total_' + lowerCurrencyId) + account['used'] = self.safe_string(balances, 'in_use_' + lowerCurrencyId) + account['free'] = self.safe_string(balances, 'available_' + lowerCurrencyId) + result[code] = account + return self.safe_balance(result) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://apidocs.bithumb.com/v1.2.0/reference/%EB%B3%B4%EC%9C%A0%EC%9E%90%EC%82%B0-%EC%A1%B0%ED%9A%8C + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + request: dict = { + 'currency': 'ALL', + } + response = await self.privatePostInfoBalance(self.extend(request, params)) + return self.parse_balance(response) + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://apidocs.bithumb.com/v1.2.0/reference/%ED%98%B8%EA%B0%80-%EC%A0%95%EB%B3%B4-%EC%A1%B0%ED%9A%8C + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'baseId': market['baseId'], + 'quoteId': market['quoteId'], + } + if limit is not None: + request['count'] = limit # default 30, max 30 + response = await self.publicGetOrderbookBaseIdQuoteId(self.extend(request, params)) + # + # { + # "status":"0000", + # "data":{ + # "timestamp":"1587621553942", + # "payment_currency":"KRW", + # "order_currency":"BTC", + # "bids":[ + # {"price":"8652000","quantity":"0.0043"}, + # {"price":"8651000","quantity":"0.0049"}, + # {"price":"8650000","quantity":"8.4791"}, + # ], + # "asks":[ + # {"price":"8654000","quantity":"0.119"}, + # {"price":"8655000","quantity":"0.254"}, + # {"price":"8658000","quantity":"0.119"}, + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + timestamp = self.safe_integer(data, 'timestamp') + return self.parse_order_book(data, symbol, timestamp, 'bids', 'asks', 'price', 'quantity') + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # fetchTicker, fetchTickers + # + # { + # "opening_price":"227100", + # "closing_price":"228400", + # "min_price":"222300", + # "max_price":"230000", + # "units_traded":"82618.56075337", + # "acc_trade_value":"18767376138.6031", + # "prev_closing_price":"227100", + # "units_traded_24H":"151871.13484676", + # "acc_trade_value_24H":"34247610416.8974", + # "fluctate_24H":"8700", + # "fluctate_rate_24H":"3.96", + # "date":"1587710327264", # fetchTickers inject self + # } + # + timestamp = self.safe_integer(ticker, 'date') + symbol = self.safe_symbol(None, market) + open = self.safe_string(ticker, 'opening_price') + close = self.safe_string(ticker, 'closing_price') + baseVolume = self.safe_string(ticker, 'units_traded_24H') + quoteVolume = self.safe_string(ticker, 'acc_trade_value_24H') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'max_price'), + 'low': self.safe_string(ticker, 'min_price'), + 'bid': self.safe_string(ticker, 'buy_price'), + 'bidVolume': None, + 'ask': self.safe_string(ticker, 'sell_price'), + 'askVolume': None, + 'vwap': None, + 'open': open, + 'close': close, + 'last': close, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'info': ticker, + }, market) + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://apidocs.bithumb.com/v1.2.0/reference/%ED%98%84%EC%9E%AC%EA%B0%80-%EC%A0%95%EB%B3%B4-%EC%A1%B0%ED%9A%8C-all + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + result: dict = {} + quoteCurrencies = self.safe_dict(self.options, 'quoteCurrencies', {}) + quotes = list(quoteCurrencies.keys()) + promises = [] + for i in range(0, len(quotes)): + request: dict = { + 'quoteId': quotes[i], + } + promises.append(self.publicGetTickerALLQuoteId(self.extend(request, params))) + responses = await asyncio.gather(*promises) + for i in range(0, len(quotes)): + quote = quotes[i] + response = responses[i] + # + # { + # "status":"0000", + # "data":{ + # "BTC":{ + # "opening_price":"9045000", + # "closing_price":"9132000", + # "min_price":"8938000", + # "max_price":"9168000", + # "units_traded":"4619.79967497", + # "acc_trade_value":"42021363832.5187", + # "prev_closing_price":"9041000", + # "units_traded_24H":"8793.5045804", + # "acc_trade_value_24H":"78933458515.4962", + # "fluctate_24H":"530000", + # "fluctate_rate_24H":"6.16" + # }, + # "date":"1587710878669" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + timestamp = self.safe_integer(data, 'date') + tickers = self.omit(data, 'date') + currencyIds = list(tickers.keys()) + for j in range(0, len(currencyIds)): + currencyId = currencyIds[j] + ticker = data[currencyId] + base = self.safe_currency_code(currencyId) + symbol = base + '/' + quote + market = self.safe_market(symbol) + ticker['date'] = timestamp + result[symbol] = self.parse_ticker(ticker, market) + return self.filter_by_array_tickers(result, 'symbol', symbols) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://apidocs.bithumb.com/v1.2.0/reference/%ED%98%84%EC%9E%AC%EA%B0%80-%EC%A0%95%EB%B3%B4-%EC%A1%B0%ED%9A%8C + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'baseId': market['baseId'], + 'quoteId': market['quoteId'], + } + response = await self.publicGetTickerBaseIdQuoteId(self.extend(request, params)) + # + # { + # "status":"0000", + # "data":{ + # "opening_price":"227100", + # "closing_price":"228400", + # "min_price":"222300", + # "max_price":"230000", + # "units_traded":"82618.56075337", + # "acc_trade_value":"18767376138.6031", + # "prev_closing_price":"227100", + # "units_traded_24H":"151871.13484676", + # "acc_trade_value_24H":"34247610416.8974", + # "fluctate_24H":"8700", + # "fluctate_rate_24H":"3.96", + # "date":"1587710327264" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_ticker(data, market) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # [ + # 1576823400000, # 기준 시간 + # "8284000", # 시가 + # "8286000", # 종가 + # "8289000", # 고가 + # "8276000", # 저가 + # "15.41503692" # 거래량 + # ] + # + return [ + self.safe_integer(ohlcv, 0), + self.safe_number(ohlcv, 1), + self.safe_number(ohlcv, 3), + self.safe_number(ohlcv, 4), + self.safe_number(ohlcv, 2), + self.safe_number(ohlcv, 5), + ] + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://apidocs.bithumb.com/v1.2.0/reference/candlestick-rest-api + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'baseId': market['baseId'], + 'quoteId': market['quoteId'], + 'interval': self.safe_string(self.timeframes, timeframe, timeframe), + } + response = await self.publicGetCandlestickBaseIdQuoteIdInterval(self.extend(request, params)) + # + # { + # "status": "0000", + # "data": { + # [ + # 1576823400000, # 기준 시간 + # "8284000", # 시가 + # "8286000", # 종가 + # "8289000", # 고가 + # "8276000", # 저가 + # "15.41503692" # 거래량 + # ], + # [ + # 1576824000000, # 기준 시간 + # "8284000", # 시가 + # "8281000", # 종가 + # "8289000", # 고가 + # "8275000", # 저가 + # "6.19584467" # 거래량 + # ], + # } + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_ohlcvs(data, market, timeframe, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(public) + # + # { + # "transaction_date":"2020-04-23 22:21:46", + # "type":"ask", + # "units_traded":"0.0125", + # "price":"8667000", + # "total":"108337" + # } + # + # fetchOrder(private) + # + # { + # "transaction_date": "1572497603902030", + # "price": "8601000", + # "units": "0.005", + # "fee_currency": "KRW", + # "fee": "107.51", + # "total": "43005" + # } + # + # a workaround for their bug in date format, hours are not 0-padded + timestamp = None + transactionDatetime = self.safe_string(trade, 'transaction_date') + if transactionDatetime is not None: + parts = transactionDatetime.split(' ') + numParts = len(parts) + if numParts > 1: + transactionDate = parts[0] + transactionTime = parts[1] + if len(transactionTime) < 8: + transactionTime = '0' + transactionTime + timestamp = self.parse8601(transactionDate + ' ' + transactionTime) + else: + timestamp = self.safe_integer_product(trade, 'transaction_date', 0.001) + if timestamp is not None: + timestamp -= 9 * 3600000 # they report UTC + 9 hours, server in Korean timezone + type = None + side = self.safe_string(trade, 'type') + side = 'sell' if (side == 'ask') else 'buy' + id = self.safe_string(trade, 'cont_no') + market = self.safe_market(None, market) + priceString = self.safe_string(trade, 'price') + amountString = self.fix_comma_number(self.safe_string_2(trade, 'units_traded', 'units')) + costString = self.safe_string(trade, 'total') + fee = None + feeCostString = self.safe_string(trade, 'fee') + if feeCostString is not None: + feeCurrencyId = self.safe_string(trade, 'fee_currency') + feeCurrencyCode = self.common_currency_code(feeCurrencyId) + fee = { + 'cost': feeCostString, + 'currency': feeCurrencyCode, + } + return self.safe_trade({ + 'id': id, + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'order': None, + 'type': type, + 'side': side, + 'takerOrMaker': None, + 'price': priceString, + 'amount': amountString, + 'cost': costString, + 'fee': fee, + }, market) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://apidocs.bithumb.com/v1.2.0/reference/%EC%B5%9C%EA%B7%BC-%EC%B2%B4%EA%B2%B0-%EB%82%B4%EC%97%AD + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'baseId': market['baseId'], + 'quoteId': market['quoteId'], + } + if limit is not None: + request['count'] = limit # default 20, max 100 + response = await self.publicGetTransactionHistoryBaseIdQuoteId(self.extend(request, params)) + # + # { + # "status":"0000", + # "data":[ + # { + # "transaction_date":"2020-04-23 22:21:46", + # "type":"ask", + # "units_traded":"0.0125", + # "price":"8667000", + # "total":"108337" + # }, + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_trades(data, market, since, limit) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://apidocs.bithumb.com/v1.2.0/reference/%EC%A7%80%EC%A0%95%EA%B0%80-%EC%A3%BC%EB%AC%B8%ED%95%98%EA%B8%B0 + https://apidocs.bithumb.com/v1.2.0/reference/%EC%8B%9C%EC%9E%A5%EA%B0%80-%EB%A7%A4%EC%88%98%ED%95%98%EA%B8%B0 + https://apidocs.bithumb.com/v1.2.0/reference/%EC%8B%9C%EC%9E%A5%EA%B0%80-%EB%A7%A4%EB%8F%84%ED%95%98%EA%B8%B0 + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'order_currency': market['id'], + 'payment_currency': market['quote'], + 'units': amount, + } + method = 'privatePostTradePlace' + if type == 'limit': + request['price'] = price + request['type'] = 'bid' if (side == 'buy') else 'ask' + else: + method = 'privatePostTradeMarket' + self.capitalize(side) + response = await getattr(self, method)(self.extend(request, params)) + id = self.safe_string(response, 'order_id') + if id is None: + raise InvalidOrder(self.id + ' createOrder() did not return an order id') + return self.safe_order({ + 'info': response, + 'symbol': symbol, + 'type': type, + 'side': side, + 'id': id, + }, market) + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://apidocs.bithumb.com/v1.2.0/reference/%EA%B1%B0%EB%9E%98-%EC%A3%BC%EB%AC%B8%EB%82%B4%EC%97%AD-%EC%83%81%EC%84%B8-%EC%A1%B0%ED%9A%8C + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrder() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'order_id': id, + 'count': 1, + 'order_currency': market['base'], + 'payment_currency': market['quote'], + } + response = await self.privatePostInfoOrderDetail(self.extend(request, params)) + # + # { + # "status": "0000", + # "data": { + # "order_date": "1603161798539254", + # "type": "ask", + # "order_status": "Cancel", + # "order_currency": "BTC", + # "payment_currency": "KRW", + # "watch_price": "0", + # "order_price": "13344000", + # "order_qty": "0.0125", + # "cancel_date": "1603161803809993", + # "cancel_type": "사용자취소", + # "contract": [ + # { + # "transaction_date": "1603161799976383", + # "price": "13344000", + # "units": "0.0015", + # "fee_currency": "KRW", + # "fee": "0", + # "total": "20016" + # } + # ], + # } + # } + # + data = self.safe_dict(response, 'data') + return self.parse_order(self.extend(data, {'order_id': id}), market) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'Pending': 'open', + 'Completed': 'closed', + 'Cancel': 'canceled', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # + # fetchOrder + # + # { + # "transaction_date": "1572497603668315", + # "type": "bid", + # "order_status": "Completed", # Completed, Cancel ... + # "order_currency": "BTC", + # "payment_currency": "KRW", + # "watch_price": "0", # present in Cancel order + # "order_price": "8601000", + # "order_qty": "0.007", + # "cancel_date": "", # filled in Cancel order + # "cancel_type": "", # filled in Cancel order, i.e. 사용자취소 + # "contract": [ + # { + # "transaction_date": "1572497603902030", + # "price": "8601000", + # "units": "0.005", + # "fee_currency": "KRW", + # "fee": "107.51", + # "total": "43005" + # }, + # ] + # } + # + # fetchOpenOrders + # + # { + # "order_currency": "BTC", + # "payment_currency": "KRW", + # "order_id": "C0101000007408440032", + # "order_date": "1571728739360570", + # "type": "bid", + # "units": "5.0", + # "units_remaining": "5.0", + # "price": "501000", + # } + # + timestamp = self.safe_integer_product(order, 'order_date', 0.001) + sideProperty = self.safe_string_2(order, 'type', 'side') + side = 'buy' if (sideProperty == 'bid') else 'sell' + status = self.parse_order_status(self.safe_string(order, 'order_status')) + price = self.safe_string_2(order, 'order_price', 'price') + type = 'limit' + if Precise.string_equals(price, '0'): + type = 'market' + amount = self.fix_comma_number(self.safe_string_2(order, 'order_qty', 'units')) + remaining = self.fix_comma_number(self.safe_string(order, 'units_remaining')) + if remaining is None: + if status == 'closed': + remaining = '0' + elif status != 'canceled': + remaining = amount + symbol = None + baseId = self.safe_string(order, 'order_currency') + quoteId = self.safe_string(order, 'payment_currency') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + if (base is not None) and (quote is not None): + symbol = base + '/' + quote + if symbol is None: + market = self.safe_market(None, market) + symbol = market['symbol'] + id = self.safe_string(order, 'order_id') + rawTrades = self.safe_list(order, 'contract', []) + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'symbol': symbol, + 'type': type, + 'timeInForce': None, + 'postOnly': None, + 'side': side, + 'price': price, + 'triggerPrice': None, + 'amount': amount, + 'cost': None, + 'average': None, + 'filled': None, + 'remaining': remaining, + 'status': status, + 'fee': None, + 'trades': rawTrades, + }, market) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://apidocs.bithumb.com/v1.2.0/reference/%EA%B1%B0%EB%9E%98-%EC%A3%BC%EB%AC%B8%EB%82%B4%EC%97%AD-%EC%A1%B0%ED%9A%8C + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOpenOrders() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + if limit is None: + limit = 100 + request: dict = { + 'count': limit, + 'order_currency': market['base'], + 'payment_currency': market['quote'], + } + if since is not None: + request['after'] = since + response = await self.privatePostInfoOrders(self.extend(request, params)) + # + # { + # "status": "0000", + # "data": [ + # { + # "order_currency": "BTC", + # "payment_currency": "KRW", + # "order_id": "C0101000007408440032", + # "order_date": "1571728739360570", + # "type": "bid", + # "units": "5.0", + # "units_remaining": "5.0", + # "price": "501000", + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_orders(data, market, since, limit) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://apidocs.bithumb.com/v1.2.0/reference/%EC%A3%BC%EB%AC%B8-%EC%B7%A8%EC%86%8C%ED%95%98%EA%B8%B0 + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + side_in_params = ('side' in params) + if not side_in_params: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a `side` parameter(sell or buy)') + market = self.market(symbol) + side = 'bid' if (params['side'] == 'buy') else 'ask' + params = self.omit(params, ['side', 'currency']) + # https://github.com/ccxt/ccxt/issues/6771 + request: dict = { + 'order_id': id, + 'type': side, + 'order_currency': market['base'], + 'payment_currency': market['quote'], + } + response = await self.privatePostTradeCancel(self.extend(request, params)) + # + # { + # 'status': 'string', + # } + # + return self.safe_order({ + 'info': response, + }) + + async def cancel_unified_order(self, order: Order, params={}): + request: dict = { + 'side': order['side'], + } + return await self.cancel_order(order['id'], order['symbol'], self.extend(request, params)) + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://apidocs.bithumb.com/v1.2.0/reference/%EC%BD%94%EC%9D%B8-%EC%B6%9C%EA%B8%88%ED%95%98%EA%B8%B0-%EA%B0%9C%EC%9D%B8 + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.check_address(address) + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'units': amount, + 'address': address, + 'currency': currency['id'], + } + if code == 'XRP' or code == 'XMR' or code == 'EOS' or code == 'STEEM' or code == 'TON': + destination = self.safe_string(params, 'destination') + if (tag is None) and (destination is None): + raise ArgumentsRequired(self.id + ' ' + code + ' withdraw() requires a tag argument or an extra destination param') + elif tag is not None: + request['destination'] = tag + response = await self.privatePostTradeBtcWithdrawal(self.extend(request, params)) + # + # {"status" : "0000"} + # + return self.parse_transaction(response, currency) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # withdraw + # + # {"status" : "0000"} + # + currency = self.safe_currency(None, currency) + return { + 'id': None, + 'txid': None, + 'timestamp': None, + 'datetime': None, + 'network': None, + 'addressFrom': None, + 'address': None, + 'addressTo': None, + 'amount': None, + 'type': None, + 'currency': currency['code'], + 'status': None, + 'updated': None, + 'tagFrom': None, + 'tag': None, + 'tagTo': None, + 'comment': None, + 'internal': None, + 'fee': None, + 'info': transaction, + } + + def fix_comma_number(self, numberStr): + # some endpoints need self https://github.com/ccxt/ccxt/issues/11031 + if numberStr is None: + return None + finalNumberStr = numberStr + while(finalNumberStr.find(',') > -1): + finalNumberStr = finalNumberStr.replace(',', '') + return finalNumberStr + + def nonce(self): + return self.milliseconds() + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + endpoint = '/' + self.implode_params(path, params) + url = self.implode_hostname(self.urls['api'][api]) + endpoint + query = self.omit(params, self.extract_params(path)) + if api == 'public': + if query: + url += '?' + self.urlencode(query) + else: + self.check_required_credentials() + body = self.urlencode(self.extend({ + 'endpoint': endpoint, + }, query)) + nonce = str(self.nonce()) + auth = endpoint + "\0" + body + "\0" + nonce # eslint-disable-line quotes + signature = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha512) + signature64 = self.string_to_base64(signature) + headers = { + 'Accept': 'application/json', + 'Content-Type': 'application/x-www-form-urlencoded', + 'Api-Key': self.apiKey, + 'Api-Sign': signature64, + 'Api-Nonce': nonce, + } + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None # fallback to default error handler + if 'status' in response: + # + # {"status":"5100","message":"After May 23th, recent_transactions is no longer, hence users will not be able to connect to recent_transactions"} + # + status = self.safe_string(response, 'status') + message = self.safe_string(response, 'message') + if status is not None: + if status == '0000': + return None # no error + elif message == '거래 진행중인 내역이 존재하지 않습니다.': + # https://github.com/ccxt/ccxt/issues/9017 + return None # no error + feedback = self.id + ' ' + message + self.throw_exactly_matched_exception(self.exceptions, status, feedback) + self.throw_exactly_matched_exception(self.exceptions, message, feedback) + raise ExchangeError(feedback) + return None diff --git a/ccxt/async_support/bitmart.py b/ccxt/async_support/bitmart.py new file mode 100644 index 0000000..a280c96 --- /dev/null +++ b/ccxt/async_support/bitmart.py @@ -0,0 +1,5360 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.bitmart import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, BorrowInterest, Currencies, Currency, DepositAddress, FundingHistory, Int, IsolatedBorrowRate, IsolatedBorrowRates, LedgerEntry, Market, Num, Order, OrderBook, OrderRequest, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, Trade, TradingFeeInterface, Transaction, MarketInterface, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import AccountSuspended +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidAddress +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import NotSupported +from ccxt.base.errors import NetworkError +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.errors import OnMaintenance +from ccxt.base.errors import InvalidNonce +from ccxt.base.decimal_to_precision import TRUNCATE +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class bitmart(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(bitmart, self).describe(), { + 'id': 'bitmart', + 'name': 'BitMart', + 'countries': ['US', 'CN', 'HK', 'KR'], + # 150 per 5 seconds = 30 per second + # rateLimit = 1000ms / 30 ~= 33.334 + 'rateLimit': 33.34, + 'version': 'v2', + 'certified': True, + 'pro': True, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': True, + 'swap': True, + 'future': False, + 'option': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': True, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'cancelOrders': True, + 'createMarketBuyOrderWithCost': True, + 'createMarketOrderWithCost': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createOrders': True, + 'createPostOnlyOrder': True, + 'createReduceOnlyOrder': True, + 'createStopLimitOrder': False, + 'createStopMarketOrder': False, + 'createStopOrder': False, + 'createTrailingPercentOrder': True, + 'fetchBalance': True, + 'fetchBorrowInterest': True, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchCanceledOrders': True, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDeposit': True, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchDepositWithdrawFee': True, + 'fetchDepositWithdrawFees': False, + 'fetchFundingHistory': True, + 'fetchFundingRate': True, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchIsolatedBorrowRate': True, + 'fetchIsolatedBorrowRates': True, + 'fetchLedger': True, + 'fetchLiquidations': False, + 'fetchMarginMode': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': True, + 'fetchMyLiquidations': True, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': True, + 'fetchOpenInterestHistory': False, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': False, + 'fetchOrderTrades': True, + 'fetchPosition': True, + 'fetchPositionMode': True, + 'fetchPositions': True, + 'fetchStatus': True, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': True, + 'fetchTradingFees': False, + 'fetchTransactionFee': True, + 'fetchTransactionFees': False, + 'fetchTransfer': False, + 'fetchTransfers': True, + 'fetchWithdrawAddresses': True, + 'fetchWithdrawAddressesByNetwork': False, + 'fetchWithdrawal': True, + 'fetchWithdrawals': True, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': True, + 'setLeverage': True, + 'setMarginMode': False, + 'setPositionMode': True, + 'transfer': True, + 'withdraw': True, + }, + 'hostname': 'bitmart.com', # bitmart.info, bitmart.news for Hong Kong users + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/0623e9c4-f50e-48c9-82bd-65c3908c3a14', + 'api': { + 'spot': 'https://api-cloud.{hostname}', + 'swap': 'https://api-cloud-v2.{hostname}', # bitmart.info for Hong Kong users + }, + 'www': 'https://www.bitmart.com/', + 'doc': 'https://developer-pro.bitmart.com/', + 'referral': { + 'url': 'http://www.bitmart.com/?r=rQCFLh', + 'discount': 0.3, + }, + 'fees': 'https://www.bitmart.com/fee/en', + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + 'uid': True, + }, + 'api': { + 'public': { + 'get': { + 'system/time': 3, # 10 times/sec => 30/10 = 3 + 'system/service': 3, + # spot markets + 'spot/v1/currencies': 7.5, + 'spot/v1/symbols': 7.5, + 'spot/v1/symbols/details': 5, + 'spot/quotation/v3/tickers': 6, # 10 times/2 sec = 5/s => 30/5 = 6 + 'spot/quotation/v3/ticker': 4, # 15 times/2 sec = 7.5/s => 30/7.5 = 4 + 'spot/quotation/v3/lite-klines': 5, # should be 4 but errors + 'spot/quotation/v3/klines': 7, # should be 6 but errors + 'spot/quotation/v3/books': 4, # 15 times/2 sec = 7.5/s => 30/7.5 = 4 + 'spot/quotation/v3/trades': 4, # 15 times/2 sec = 7.5/s => 30/7.5 = 4 + 'spot/v1/ticker': 5, + 'spot/v2/ticker': 30, + 'spot/v1/ticker_detail': 5, # 12 times/2 sec = 6/s => 30/6 = 5 + 'spot/v1/steps': 30, + 'spot/v1/symbols/kline': 6, # should be 5 but errors + 'spot/v1/symbols/book': 5, + 'spot/v1/symbols/trades': 5, + # contract markets + 'contract/v1/tickers': 15, + 'contract/public/details': 5, + 'contract/public/depth': 5, + 'contract/public/open-interest': 30, + 'contract/public/funding-rate': 30, + 'contract/public/funding-rate-history': 30, + 'contract/public/kline': 6, # should be 5 but errors + 'account/v1/currencies': 30, + 'contract/public/markprice-kline': 5, # 6 times per 1 second + }, + }, + 'private': { + 'get': { + # sub-account + 'account/sub-account/v1/transfer-list': 7.5, + 'account/sub-account/v1/transfer-history': 7.5, + 'account/sub-account/main/v1/wallet': 5, + 'account/sub-account/main/v1/subaccount-list': 7.5, + 'account/contract/sub-account/main/v1/wallet': 5, + 'account/contract/sub-account/main/v1/transfer-list': 7.5, + 'account/contract/sub-account/v1/transfer-history': 7.5, + # account + 'account/v1/wallet': 5, + 'account/v1/currencies': 30, + 'spot/v1/wallet': 5, + 'account/v1/deposit/address': 30, + 'account/v1/withdraw/charge': 32, # should be 30 but errors + 'account/v2/deposit-withdraw/history': 7.5, + 'account/v1/deposit-withdraw/detail': 7.5, + 'account/v1/withdraw/address/list': 30, # 2 times per 2 seconds + # order + 'spot/v1/order_detail': 1, + 'spot/v2/orders': 5, + 'spot/v1/trades': 5, + # newer order endpoint + 'spot/v2/trades': 4, + 'spot/v3/orders': 5, + 'spot/v2/order_detail': 1, + # margin + 'spot/v1/margin/isolated/borrow_record': 1, + 'spot/v1/margin/isolated/repay_record': 1, + 'spot/v1/margin/isolated/pairs': 30, + 'spot/v1/margin/isolated/account': 5, + 'spot/v1/trade_fee': 30, + 'spot/v1/user_fee': 30, + # broker + 'spot/v1/broker/rebate': 1, + # contract + 'contract/private/assets-detail': 5, + 'contract/private/order': 1.2, + 'contract/private/order-history': 10, + 'contract/private/position': 10, + 'contract/private/position-v2': 10, + 'contract/private/get-open-orders': 1.2, + 'contract/private/current-plan-order': 1.2, + 'contract/private/trades': 10, + 'contract/private/position-risk': 10, + 'contract/private/affilate/rebate-list': 10, + 'contract/private/affilate/trade-list': 10, + 'contract/private/transaction-history': 10, + 'contract/private/get-position-mode': 1, + }, + 'post': { + # sub-account endpoints + 'account/sub-account/main/v1/sub-to-main': 30, + 'account/sub-account/sub/v1/sub-to-main': 30, + 'account/sub-account/main/v1/main-to-sub': 30, + 'account/sub-account/sub/v1/sub-to-sub': 30, + 'account/sub-account/main/v1/sub-to-sub': 30, + 'account/contract/sub-account/main/v1/sub-to-main': 7.5, + 'account/contract/sub-account/main/v1/main-to-sub': 7.5, + 'account/contract/sub-account/sub/v1/sub-to-main': 7.5, + # account + 'account/v1/withdraw/apply': 7.5, + # transaction and trading + 'spot/v1/submit_order': 1, + 'spot/v1/batch_orders': 1, + 'spot/v2/cancel_order': 1, + 'spot/v1/cancel_orders': 15, + 'spot/v4/query/order': 1, # 60 times/2 sec = 30/s => 30/30 = 1 + 'spot/v4/query/client-order': 1, # 60 times/2 sec = 30/s => 30/30 = 1 + 'spot/v4/query/open-orders': 5, # 12 times/2 sec = 6/s => 30/6 = 5 + 'spot/v4/query/history-orders': 5, # 12 times/2 sec = 6/s => 30/6 = 5 + 'spot/v4/query/trades': 5, # 12 times/2 sec = 6/s => 30/6 = 5 + 'spot/v4/query/order-trades': 5, # 12 times/2 sec = 6/s => 30/6 = 5 + 'spot/v4/cancel_orders': 3, + 'spot/v4/cancel_all': 90, + 'spot/v4/batch_orders': 3, + # newer endpoint + 'spot/v3/cancel_order': 1, + 'spot/v2/batch_orders': 1, + 'spot/v2/submit_order': 1, + # margin + 'spot/v1/margin/submit_order': 1.5, # 20 times per second + 'spot/v1/margin/isolated/borrow': 30, + 'spot/v1/margin/isolated/repay': 30, + 'spot/v1/margin/isolated/transfer': 30, + # contract + 'account/v1/transfer-contract-list': 60, + 'account/v1/transfer-contract': 60, + 'contract/private/submit-order': 2.5, + 'contract/private/cancel-order': 1.5, + 'contract/private/cancel-orders': 30, + 'contract/private/submit-plan-order': 2.5, + 'contract/private/cancel-plan-order': 1.5, + 'contract/private/submit-leverage': 2.5, + 'contract/private/submit-tp-sl-order': 2.5, + 'contract/private/modify-plan-order': 2.5, + 'contract/private/modify-preset-plan-order': 2.5, + 'contract/private/modify-limit-order': 2.5, + 'contract/private/modify-tp-sl-order': 2.5, + 'contract/private/submit-trail-order': 2.5, # weight is not provided by the exchange, is set order + 'contract/private/cancel-trail-order': 1.5, # weight is not provided by the exchange, is set order + 'contract/private/set-position-mode': 1, + }, + }, + }, + 'timeframes': { + '1m': 1, + '3m': 3, + '5m': 5, + '15m': 15, + '30m': 30, + '45m': 45, + '1h': 60, + '2h': 120, + '3h': 180, + '4h': 240, + '1d': 1440, + '1w': 10080, + '1M': 43200, + }, + 'fees': { + 'trading': { + 'tierBased': True, + 'percentage': True, + 'taker': self.parse_number('0.0040'), + 'maker': self.parse_number('0.0035'), + 'tiers': { + 'taker': [ + [self.parse_number('0'), self.parse_number('0.0020')], + [self.parse_number('10'), self.parse_number('0.18')], + [self.parse_number('50'), self.parse_number('0.0016')], + [self.parse_number('250'), self.parse_number('0.0014')], + [self.parse_number('1000'), self.parse_number('0.0012')], + [self.parse_number('5000'), self.parse_number('0.0010')], + [self.parse_number('25000'), self.parse_number('0.0008')], + [self.parse_number('50000'), self.parse_number('0.0006')], + ], + 'maker': [ + [self.parse_number('0'), self.parse_number('0.001')], + [self.parse_number('10'), self.parse_number('0.0009')], + [self.parse_number('50'), self.parse_number('0.0008')], + [self.parse_number('250'), self.parse_number('0.0007')], + [self.parse_number('1000'), self.parse_number('0.0006')], + [self.parse_number('5000'), self.parse_number('0.0005')], + [self.parse_number('25000'), self.parse_number('0.0004')], + [self.parse_number('50000'), self.parse_number('0.0003')], + ], + }, + }, + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'exact': { + # general errors + '30000': ExchangeError, # 404, Not found + '30001': AuthenticationError, # 401, Header X-BM-KEY is empty + '30002': AuthenticationError, # 401, Header X-BM-KEY not found + '30003': AccountSuspended, # 401, Header X-BM-KEY has frozen + '30004': AuthenticationError, # 401, Header X-BM-SIGN is empty + '30005': AuthenticationError, # 401, Header X-BM-SIGN is wrong + '30006': AuthenticationError, # 401, Header X-BM-TIMESTAMP is empty + '30007': AuthenticationError, # 401, Header X-BM-TIMESTAMP range. Within a minute + '30008': AuthenticationError, # 401, Header X-BM-TIMESTAMP invalid format + '30010': PermissionDenied, # 403, IP is forbidden. We recommend enabling IP whitelist for API trading. After that reauth your account + '30011': AuthenticationError, # 403, Header X-BM-KEY over expire time + '30012': AuthenticationError, # 403, Header X-BM-KEY is forbidden to request it + '30013': RateLimitExceeded, # 429, Request too many requests + '30014': ExchangeNotAvailable, # 503, Service unavailable + '30016': OnMaintenance, # 200, Service maintenance, the function is temporarily unavailable + '30017': RateLimitExceeded, # 418, Your account request is temporarily rejected due to violation of current limiting rules + '30018': BadRequest, # 503, Request Body requires JSON format + '30019': PermissionDenied, # 200, You do not have the permissions to perform self operation + # funding account & sub account errors + '60000': BadRequest, # 400, Invalid request(maybe the body is empty, or the int parameter passes string data) + '60001': BadRequest, # 400, Asset account type does not exist + '60002': BadRequest, # 400, currency does not exist + '60003': ExchangeError, # 400, Currency has been closed recharge channel, if there is any problem, please consult customer service + '60004': ExchangeError, # 400, Currency has been closed withdraw channel, if there is any problem, please consult customer service + '60005': ExchangeError, # 400, Minimum amount is %s + '60006': ExchangeError, # 400, Maximum withdraw precision is %d + '60007': InvalidAddress, # 400, Only withdrawals from added addresses are allowed + '60008': InsufficientFunds, # 400, Balance not enough + '60009': ExchangeError, # 400, Beyond the limit + '60010': ExchangeError, # 400, Withdraw id or deposit id not found + '60011': InvalidAddress, # 400, Address is not valid + '60012': ExchangeError, # 400, This action is not hasattr(self, supported) currency(If IOTA, HLX recharge and withdraw calls are prohibited) + '60020': PermissionDenied, # 403, Your account is not allowed to recharge + '60021': PermissionDenied, # 403, Your account is not allowed to withdraw + '60022': PermissionDenied, # 403, No withdrawals for 24 hours + '60026': PermissionDenied, # 403, Sub-account does not have permission to operate + '60027': PermissionDenied, # 403, Only supports sub-account calls + '60028': AccountSuspended, # 403, Account is disabled for security reasons, please contact customer service + '60029': AccountSuspended, # 403, The account is frozen by the master account, please contact the master account to unfreeze the account + '60030': BadRequest, # 405, Method Not Allowed + '60031': BadRequest, # 415, Unsupported Media Type + '60050': ExchangeError, # 500, User account not found + '60051': ExchangeError, # 500, Internal Server Error + '61001': InsufficientFunds, # {"message":"Balance not enough","code":61001,"trace":"b85ea1f8-b9af-4001-ac5f-9e061fe93d78","data":{}} + '61003': BadRequest, # 400, {"message":"sub-account not found","code":61003,"trace":"b35ec2fd-0bc9-4ef2-a3c0-6f78d4f335a4","data":{}} + '61004': BadRequest, # 400, Duplicate requests(such an existing requestNo) + '61005': BadRequest, # 403, Asset transfer between accounts is not available + '61006': NotSupported, # 403, The sub-account api only supports organization accounts + '61007': ExchangeError, # 403, Please complete your institution verification to enable withdrawal function. + '61008': ExchangeError, # 403, Suspend transfer out + # spot public errors + '70000': ExchangeError, # 200, no data + '70001': BadRequest, # 200, request param can not be null + '70002': BadSymbol, # 200, symbol is invalid + '70003': NetworkError, # {"code":70003,"trace":"81a9d57b63be4819b65d3065e6a4682b.105.17105295323593915","message":"net error, please try later","data":null} + '71001': BadRequest, # 200, after is invalid + '71002': BadRequest, # 200, before is invalid + '71003': BadRequest, # 200, request after or before is invalid + '71004': BadRequest, # 200, request kline count limit + '71005': BadRequest, # 200, request step error + # spot & margin errors + '50000': BadRequest, # 400, Bad Request + '50001': BadSymbol, # 400, Symbol not found + '50002': BadRequest, # 400, From Or To format error + '50003': BadRequest, # 400, Step format error + '50004': BadRequest, # 400, Kline size over 500 + '50005': OrderNotFound, # 400, Order Id not found + '50006': InvalidOrder, # 400, Minimum size is %s + '50007': InvalidOrder, # 400, Maximum size is %s + '50008': InvalidOrder, # 400, Minimum price is %s + '50009': InvalidOrder, # 400, Minimum count*price is %s + '50010': InvalidOrder, # 400, RequestParam size is required + '50011': InvalidOrder, # 400, RequestParam price is required + '50012': InvalidOrder, # 400, RequestParam notional is required + '50013': InvalidOrder, # 400, Maximum limit*offset is %d + '50014': BadRequest, # 400, RequestParam limit is required + '50015': BadRequest, # 400, Minimum limit is 1 + '50016': BadRequest, # 400, Maximum limit is %d + '50017': BadRequest, # 400, RequestParam offset is required + '50018': BadRequest, # 400, Minimum offset is 1 + '50019': ExchangeError, # 400, Invalid status. validate status is [1=Failed, 2=Success, 3=Frozen Failed, 4=Frozen Success, 5=Partially Filled, 6=Fully Fulled, 7=Canceling, 8=Canceled] '50020': InsufficientFunds, # 400, Balance not enough + '50020': InsufficientFunds, # 400, Balance not enough + '50021': BadRequest, # 400, Invalid %s + '50022': ExchangeNotAvailable, # 400, Service unavailable + '50023': BadSymbol, # 400, This Symbol can't place order by api + '50024': BadRequest, # 400, Order book size over 200 + '50025': BadRequest, # 400, Maximum price is %s + '50026': BadRequest, # 400, The buy order price cannot be higher than the open price + '50027': BadRequest, # 400, The sell order price cannot be lower than the open price + '50028': BadRequest, # 400, Missing parameters + '50029': InvalidOrder, # 400, {"message":"param not match : size * price >=1000","code":50029,"trace":"f931f030-b692-401b-a0c5-65edbeadc598","data":{}} + '50030': OrderNotFound, # 400, {"message":"Order is already canceled","code":50030,"trace":"8d6f64ee-ad26-45a4-9efd-1080f9fca1fa","data":{}} + '50031': OrderNotFound, # 400, Order is already completed + '50032': OrderNotFound, # 400, {"message":"Order does not exist","code":50032,"trace":"8d6b482d-4bf2-4e6c-aab2-9dcd22bf2481","data":{}} + '50033': InvalidOrder, # 400, The order quantity should be greater than 0 and less than or equal to 10 + # below Error codes used interchangeably for both failed postOnly and IOC orders depending on market price and order side + '50034': InvalidOrder, # 400, {"message":"The price is high and there is no matching depth","code":50034,"trace":"ebfae59a-ba69-4735-86b2-0ed7b9ca14ea","data":{}} + '50035': InvalidOrder, # 400, {"message":"The price is low and there is no matching depth","code":50035,"trace":"677f01c7-8b88-4346-b097-b4226c75c90e","data":{}} + '50036': ExchangeError, # 400, Cancel failed, order is not revocable status + '50037': BadRequest, # 400, The maximum length of clientOrderId cannot exceed 32 + '50038': BadRequest, # 400, ClientOrderId only allows a combination of numbers and letters + '50039': BadRequest, # 400, Order_id and clientOrderId cannot be empty at the same time + '50040': BadSymbol, # 400, Symbol Not Available + '50041': ExchangeError, # 400, Out of query time range + '50042': BadRequest, # 400, clientOrderId is duplicate + '51000': BadSymbol, # 400, Currency not found + '51001': ExchangeError, # 400, Margin Account not Opened + '51002': ExchangeError, # 400, Margin Account Not Available + '51003': ExchangeError, # 400, Account Limit + '51004': InsufficientFunds, # 400, {"message":"Exceed the maximum number of borrows available.","code":51004,"trace":"4030b753-9beb-44e6-8352-1633c5edcd47","data":{}} + '51005': InvalidOrder, # 400, Less than the minimum borrowable amount + '51006': InvalidOrder, # 400, Exceeds the amount to be repaid + '51007': BadRequest, # 400, order_mode not found + '51008': ExchangeError, # 400, Operation is limited, please try again later + '51009': InvalidOrder, # 400, Parameter mismatch: limit order/market order quantity should be greater than the minimum number of should buy/sell + '51010': InvalidOrder, # 400, Parameter mismatch: limit order price should be greater than the minimum buy price + '51011': InvalidOrder, # 400, {"message":"param not match : size * price >=5","code":51011,"trace":"525e1d27bfd34d60b2d90ba13a7c0aa9.74.16696421352220797","data":{}} + '51012': InvalidOrder, # 400, Parameter mismatch: limit order price should be greater than the minimum buy price + '51013': InvalidOrder, # 400, Parameter mismatch: Limit order quantity * price should be greater than the minimum transaction amount + '51014': InvalidOrder, # 400, Participation mismatch: the number of market order buy orders should be greater than the minimum buyable amount + '51015': InvalidOrder, # 400, Parameter mismatch: the price of market order buy order placed is too small + '52000': BadRequest, # 400, Unsupported OrderMode Type + '52001': BadRequest, # 400, Unsupported Trade Type + '52002': BadRequest, # 400, Unsupported Side Type + '52003': BadRequest, # 400, Unsupported Query State Type + '52004': BadRequest, # 400, End time must be greater than or equal to Start time + '53000': AccountSuspended, # 403, Your account is frozen due to security policies. Please contact customer service + '53001': AccountSuspended, # 403, {"message":"Your kyc country is restricted. Please contact customer service.","code":53001,"trace":"8b445940-c123-4de9-86d7-73c5be2e7a24","data":{}} + '53002': PermissionDenied, # 403, Your account has not yet completed the kyc advanced certification, please complete first + '53003': PermissionDenied, # 403 No permission, please contact the main account + '53005': PermissionDenied, # 403 Don't have permission to access the interface + '53006': PermissionDenied, # 403 Please complete your personal verification(Starter) + '53007': PermissionDenied, # 403 Please complete your personal verification(Advanced) + '53008': PermissionDenied, # 403 Services is not available in your countries and areas + '53009': PermissionDenied, # 403 Your account has not yet completed the qr code certification, please complete first + '53010': PermissionDenied, # 403 This account is restricted from borrowing + '57001': BadRequest, # 405, Method Not Allowed + '58001': BadRequest, # 415, Unsupported Media Type + '59001': ExchangeError, # 500, User account not found + '59002': ExchangeError, # 500, Internal Server Error + '59003': ExchangeError, # 500, Spot wallet call fail + '59004': ExchangeError, # 500, Margin wallet service call exception + '59005': PermissionDenied, # 500, Margin wallet service restricted + '59006': ExchangeError, # 500, Transfer fail + '59007': ExchangeError, # 500, Get symbol risk data fail + '59008': ExchangeError, # 500, Trading order failure + '59009': ExchangeError, # 500, Loan success,but trading order failure + '59010': InsufficientFunds, # 500, Insufficient loan amount. + '59011': ExchangeError, # 500, The Get Wallet Balance service call fail, please try again later + # contract errors + '40001': ExchangeError, # 400, Cloud account not found + '40002': ExchangeError, # 400, out_trade_no not found + '40003': ExchangeError, # 400, out_trade_no already existed + '40004': ExchangeError, # 400, Cloud account count limit + '40005': ExchangeError, # 400, Transfer vol precision error + '40006': PermissionDenied, # 400, Invalid ip error + '40007': BadRequest, # 400, Parse parameter error + '40008': InvalidNonce, # 400, Check nonce error + '40009': BadRequest, # 400, Check ver error + '40010': BadRequest, # 400, Not found func error + '40011': BadRequest, # 400, Invalid request + '40012': ExchangeError, # 500, System error + '40013': ExchangeError, # 400, Access too often" CLIENT_TIME_INVALID, "Please check your system time. + '40014': BadSymbol, # 400, This contract is offline + '40015': BadSymbol, # 400, This contract's exchange has been paused + '40016': InvalidOrder, # 400, This order would trigger user position liquidate + '40017': InvalidOrder, # 400, It is not possible to open and close simultaneously in the same position + '40018': InvalidOrder, # 400, Your position is closed + '40019': ExchangeError, # 400, Your position is in liquidation delegating + '40020': InvalidOrder, # 400, Your position volume is not enough + '40021': ExchangeError, # 400, The position is not exsit + '40022': ExchangeError, # 400, The position is not isolated + '40023': ExchangeError, # 400, The position would liquidate when sub margin + '40024': ExchangeError, # 400, The position would be warnning of liquidation when sub margin + '40025': ExchangeError, # 400, The position’s margin shouldn’t be lower than the base limit + '40026': ExchangeError, # 400, You cross margin position is in liquidation delegating + '40027': InsufficientFunds, # 400, You contract account available balance not enough + '40028': PermissionDenied, # 400, Your plan order's count is more than system maximum limit. + '40029': InvalidOrder, # 400, The order's leverage is too large. + '40030': InvalidOrder, # 400, The order's leverage is too small. + '40031': InvalidOrder, # 400, The deviation between current price and trigger price is too large. + '40032': InvalidOrder, # 400, The plan order's life cycle is too long. + '40033': InvalidOrder, # 400, The plan order's life cycle is too short. + '40034': BadSymbol, # 400, This contract is not found + '40035': OrderNotFound, # 400, The order is not exist + '40036': InvalidOrder, # 400, The order status is invalid + '40037': OrderNotFound, # 400, The order id is not exist + '40038': BadRequest, # 400, The k-line step is invalid + '40039': BadRequest, # 400, The timestamp is invalid + '40040': InvalidOrder, # 400, The order leverage is invalid + '40041': InvalidOrder, # 400, The order side is invalid + '40042': InvalidOrder, # 400, The order type is invalid + '40043': InvalidOrder, # 400, The order precision is invalid + '40044': InvalidOrder, # 400, The order range is invalid + '40045': InvalidOrder, # 400, The order open type is invalid + '40046': PermissionDenied, # 403, The account is not opened futures + '40047': PermissionDenied, # 403, Services is not available in you countries and areas + '40048': InvalidOrder, # 403, ClientOrderId only allows a combination of numbers and letters + '40049': InvalidOrder, # 403, The maximum length of clientOrderId cannot exceed 32 + '40050': InvalidOrder, # 403, Client OrderId duplicated with existing orders + }, + 'broad': { + 'You contract account available balance not enough': InsufficientFunds, + 'you contract account available balance not enough': InsufficientFunds, + }, + }, + 'commonCurrencies': { + '$GM': 'GOLDMINER', + '$HERO': 'Step Hero', + '$PAC': 'PAC', + 'BP': 'BEYOND', + 'GDT': 'Gorilla Diamond', + 'GLD': 'Goldario', + 'MVP': 'MVP Coin', + 'TRU': 'Truebit', # conflict with TrueFi + }, + 'options': { + 'defaultNetworks': { + 'USDT': 'TRC20', + 'BTC': 'BTC', + 'ETH': 'ERC20', + }, + 'timeDifference': 0, # the difference between system clock and exchange clock + 'adjustForTimeDifference': False, # controls the adjustment logic upon instantiation + 'networks': { + 'ERC20': 'ERC20', + 'SOL': 'SOL', + 'BTC': 'BTC', + 'TRC20': 'TRC20', + # todo: should be TRX after unification + # 'TRC20': ['TRC20', 'trc20', 'TRON'], # todo: after unification i.e. TRON is returned from fetchDepositAddress + # 'ERC20': ['ERC20', 'ERC-20', 'ERC20 '], # todo: after unification + 'OMNI': 'OMNI', + 'XLM': 'XLM', + 'EOS': 'EOS', + 'NEO': 'NEO', + 'BTM': 'BTM', + 'BCH': 'BCH', + 'LTC': 'LTC', + 'BSV': 'BSV', + 'XRP': 'XRP', + # 'VECHAIN': ['VET', 'Vechain'], # todo: after unification + 'PLEX': 'PLEX', + 'XCH': 'XCH', + # 'AVALANCHE_C': ['AVAX', 'AVAX-C'], # todo: after unification + 'NEAR': 'NEAR', + 'FIO': 'FIO', + 'SCRT': 'SCRT', + 'IOTX': 'IOTX', + 'ALGO': 'ALGO', + 'ATOM': 'ATOM', + 'DOT': 'DOT', + 'ADA': 'ADA', + 'DOGE': 'DOGE', + 'XYM': 'XYM', + 'GLMR': 'GLMR', + 'MOVR': 'MOVR', + 'ZIL': 'ZIL', + 'INJ': 'INJ', + 'KSM': 'KSM', + 'ZEC': 'ZEC', + 'NAS': 'NAS', + 'POLYGON': 'MATIC', + 'HRC20': 'HECO', + 'XDC': 'XDC', + 'ONE': 'ONE', + 'LAT': 'LAT', + 'CSPR': 'Casper', + 'ICP': 'Computer', + 'XTZ': 'XTZ', + 'MINA': 'MINA', + 'BEP20': 'BSC_BNB', + 'THETA': 'THETA', + 'AKT': 'AKT', + 'AR': 'AR', + 'CELO': 'CELO', + 'FIL': 'FIL', + 'NULS': 'NULS', + 'ETC': 'ETC', + 'DASH': 'DASH', + 'DGB': 'DGB', + 'BEP2': 'BEP2', + 'GRIN': 'GRIN', + 'WAVES': 'WAVES', + 'ABBC': 'ABBC', + 'ACA': 'ACA', + 'QTUM': 'QTUM', + 'PAC': 'PAC', + # 'TERRACLASSIC': 'LUNC', # TBD + # 'TERRA': 'Terra', # TBD + # 'HEDERA': ['HBAR', 'Hedera', 'Hedera Mainnet'], # todo: after unification + 'TLOS': 'TLOS', + 'KARDIA': 'KardiaChain', + 'FUSE': 'FUSE', + 'TRC10': 'TRC10', + 'FIRO': 'FIRO', + 'FTM': 'Fantom', + # 'KLAYTN': ['klaytn', 'KLAY', 'Klaytn'], # todo: after unification + # 'ELROND': ['EGLD', 'Elrond eGold', 'MultiversX'], # todo: after unification + 'EVER': 'EVER', + 'KAVA': 'KAVA', + 'HYDRA': 'HYDRA', + 'PLCU': 'PLCU', + 'BRISE': 'BRISE', + # 'CRC20': ['CRO', 'CRO_Chain'], # todo: after unification + # 'CONFLUX': ['CFX eSpace', 'CFX'], # todo: after unification + 'OPTIMISM': 'OPTIMISM', + 'REEF': 'REEF', + 'SYS': 'SYS', # NEVM is different + 'VITE': 'VITE', + 'STX': 'STX', + 'SXP': 'SXP', + 'BITCI': 'BITCI', + # 'ARBITRUM': ['ARBI', 'Arbitrum'], # todo: after unification + 'XRD': 'XRD', + 'ASTR': 'ASTAR', + 'ZEN': 'HORIZEN', + 'LTO': 'LTO', + 'ETHW': 'ETHW', + 'ETHF': 'ETHF', + 'IOST': 'IOST', + # 'CHILIZ': ['CHZ', 'CHILIZ'], # todo: after unification + 'APT': 'APT', + # 'FLOW': ['FLOW', 'Flow'], # todo: after unification + 'ONT': 'ONT', + 'EVMOS': 'EVMOS', + 'XMR': 'XMR', + 'OASYS': 'OAS', + 'OSMO': 'OSMO', + 'OMAX': 'OMAX Chain', + 'DESO': 'DESO', + 'BFIC': 'BFIC', + 'OHO': 'OHO', + 'CS': 'CS', + 'CHEQ': 'CHEQ', + 'NODL': 'NODL', + 'NEM': 'XEM', + 'FRA': 'FRA', + 'ERGO': 'ERG', + # todo: below will be uncommented after unification + # 'BITCOINHD': 'BHD', + # 'CRUST': 'CRU', + # 'MINTME': 'MINTME', + # 'ZENITH': 'ZENITH', + # 'ZENIQ': 'ZENIQ', # "ZEN-20" is different + # 'BITCOINVAULT': 'BTCV', + # 'MOBILECOIN': 'MBX', + # 'PINETWORK': 'PI', + # 'PI': 'PI', + # 'REBUS': 'REBUS', + # 'XODEX': 'XODEX', + # 'ULTRONGLOW': 'UTG' + # 'QIBLOCKCHAIN': 'QIE', + # 'XIDEN': 'XDEN', + # 'PHAETON': 'PHAE', + # 'REDLIGHT': 'REDLC', + # 'VERITISE': 'VTS', + # 'VERIBLOCK': 'VBK', + # 'RAMESTTA': 'RAMA', + # 'BITICA': 'BDCC', + # 'CROWNSOVEREIGN': 'CSOV', + # 'DRAC': 'DRC20', + # 'QCHAIN': 'QDT', + # 'KINGARU': 'KRU', + # 'PROOFOFMEMES': 'POM', + # 'CUBE': 'CUBE', + # 'CADUCEUS': 'CMP', + # 'VEIL': 'VEIL', + # 'ENERGYWEB': 'EWT', + # 'CYPHERIUM': 'CPH', + # 'LBRY': 'LBC', + # 'ETHERCOIN': 'ETE', + # undetermined chains: + # LEX(for LexThum), TAYCAN(for TRICE), SFL(probably TAYCAN), OMNIA(for APEX), NAC(for NAC), KAG(Kinesis), CEM(crypto emergency), XVM(for Venidium), NEVM(for NEVM), IGT20(for IGNITE), FILM(FILMCredits), CC(CloudCoin), MERGE(MERGE), LTNM(Bitcoin latinum), PLUGCN( PlugChain), DINGO(dingo), LED(LEDGIS), AVAT(AVAT), VSOL(Vsolidus), EPIC(EPIC cash), NFC(netflowcoin), mrx(Metrix Coin), Idena(idena network), PKT(PKT Cash), BondDex(BondDex), XBN(XBN), KALAM(Kalamint), REV(RChain), KRC20(MyDeFiPet), ARC20(Hurricane Token), GMD(Coop network), BERS(Berith), ZEBI(Zebi), BRC(Baer Chain), DAPS(DAPS Coin), APL(Gold Secured Currency), NDAU(NDAU), WICC(WICC), UPG(Unipay God), TSL(TreasureSL), MXW(Maxonrow), CLC(Cifculation), SMH(SMH Coin), XIN(CPCoin), RDD(ReddCoin), OK(Okcash), KAR(KAR), CCX(ConcealNetwork), + }, + 'networksById': { + 'ETH': 'ERC20', + 'Ethereum': 'ERC20', + 'USDT': 'OMNI', # the default USDT network for bitmart is OMNI + 'Bitcoin': 'BTC', + }, + 'defaultType': 'spot', # 'spot', 'swap' + 'fetchBalance': { + 'type': 'spot', # 'spot', 'swap', 'account' + }, + 'accountsByType': { + 'spot': 'spot', + 'swap': 'swap', + }, + 'createMarketBuyOrderRequiresPrice': True, + 'brokerId': 'CCXTxBitmart000', + }, + 'features': { + 'default': { + 'sandbox': False, + 'createOrder': { + 'marginMode': True, + 'triggerPrice': False, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': False, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'marketBuyRequiresPrice': False, # todo: https://developer-pro.bitmart.com/en/spot/#new-order-v2-signed + 'marketBuyByCost': True, + 'leverage': True, # todo: implement + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': { + 'max': 10, + }, + 'fetchMyTrades': { + 'marginMode': True, + 'limit': 200, + 'daysBack': None, + 'untilDays': 99999, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': True, + 'limit': 200, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': { + 'marginMode': True, + 'limit': 200, + 'daysBack': None, + 'daysBackCanceled': None, + 'untilDays': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 1000, # variable timespans for recent endpoint, 200 for historical + }, + }, + 'forDerivatives': { + 'extends': 'default', + 'createOrder': { + 'marginMode': True, + 'triggerPrice': True, + 'triggerPriceType': { + 'last': True, + 'mark': True, + 'index': False, + }, + 'triggerDirection': True, # todo: implementation broken + 'stopLossPrice': True, + 'takeProfitPrice': True, + 'attachedStopLossTakeProfit': { + 'triggerPriceType': { + 'last': True, + 'mark': True, + 'index': False, + }, + 'price': False, + }, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': True, + 'marketBuyRequiresPrice': True, + 'marketBuyByCost': True, + # exchange-supported features + # 'selfTradePrevention': True, + # 'twap': False, + # 'iceberg': False, + # 'oco': False, + }, + 'fetchMyTrades': { + 'marginMode': True, + 'limit': None, + 'daysBack': None, + 'untilDays': 99999, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': True, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 100, + 'trigger': True, + 'trailing': False, + }, + 'fetchClosedOrders': { + 'marginMode': True, + 'limit': 200, + 'daysBack': None, + 'daysBackCanceled': None, + 'untilDays': None, + 'trigger': False, + 'trailing': False, + }, + 'fetchOHLCV': { + 'limit': 500, + }, + }, + 'spot': { + 'extends': 'default', + }, + 'swap': { + 'linear': { + 'extends': 'forDerivatives', + }, + 'inverse': { + 'extends': 'forDerivatives', + }, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + }) + + async def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://developer-pro.bitmart.com/en/spot/#get-system-time + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = await self.publicGetSystemTime(params) + # + # { + # "message":"OK", + # "code":1000, + # "trace":"c4e5e5b7-fe9f-4191-89f7-53f6c5bf9030", + # "data":{ + # "server_time":1599843709578 + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.safe_integer(data, 'server_time') + + async def fetch_status(self, params={}): + """ + the latest known information on the availability of the exchange API + + https://developer-pro.bitmart.com/en/spot/#get-system-service-status + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `status structure ` + """ + options = self.safe_dict(self.options, 'fetchStatus', {}) + defaultType = self.safe_string(self.options, 'defaultType') + type = self.safe_string(options, 'type', defaultType) + type = self.safe_string(params, 'type', type) + params = self.omit(params, 'type') + response = await self.publicGetSystemService(params) + # + # { + # "message": "OK", + # "code": 1000, + # "trace": "1d3f28b0-763e-4f78-90c4-5e3ad19dc595", + # "data": { + # "service": [ + # { + # "title": "Spot API Stop", + # "service_type": "spot", + # "status": 2, + # "start_time": 1648639069125, + # "end_time": 1648639069125 + # }, + # { + # "title": "Contract API Stop", + # "service_type": "contract", + # "status": 2, + # "start_time": 1648639069125, + # "end_time": 1648639069125 + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + services = self.safe_list(data, 'service', []) + servicesByType = self.index_by(services, 'service_type') + if type == 'swap': + type = 'contract' + service = self.safe_string(servicesByType, type) + status = None + eta = None + if service is not None: + statusCode = self.safe_integer(service, 'status') + if statusCode == 2: + status = 'ok' + else: + status = 'maintenance' + eta = self.safe_integer(service, 'end_time') + return { + 'status': status, + 'updated': None, + 'eta': eta, + 'url': None, + 'info': response, + } + + async def fetch_spot_markets(self, params={}) -> List[MarketInterface]: + response = await self.publicGetSpotV1SymbolsDetails(params) + # + # { + # "message":"OK", + # "code":1000, + # "trace":"a67c9146-086d-4d3f-9897-5636a9bb26e1", + # "data":{ + # "symbols":[ + # { + # "symbol": "BTC_USDT", + # "symbol_id": 53, + # "base_currency": "BTC", + # "quote_currency": "USDT", + # "base_min_size": "0.000010000000000000000000000000", + # "base_max_size": "100000000.000000000000000000000000000000", + # "price_min_precision": -1, + # "price_max_precision": 2, + # "quote_increment": "0.00001", # Api docs says "The minimum order quantity is also the minimum order quantity increment", however I think they mistakenly use the term 'order quantity' + # "expiration": "NA", + # "min_buy_amount": "5.000000000000000000000000000000", + # "min_sell_amount": "5.000000000000000000000000000000", + # "trade_status": "trading" + # }, + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + symbols = self.safe_list(data, 'symbols', []) + result = [] + fees = self.fees['trading'] + for i in range(0, len(symbols)): + market = symbols[i] + id = self.safe_string(market, 'symbol') + numericId = self.safe_integer(market, 'symbol_id') + baseId = self.safe_string(market, 'base_currency') + quoteId = self.safe_string(market, 'quote_currency') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + symbol = base + '/' + quote + minBuyCost = self.safe_string(market, 'min_buy_amount') + minSellCost = self.safe_string(market, 'min_sell_amount') + minCost = Precise.string_max(minBuyCost, minSellCost) + baseMinSize = self.safe_number(market, 'base_min_size') + result.append(self.safe_market_structure({ + 'id': id, + 'numericId': numericId, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': self.safe_string_lower_2(market, 'status', 'trade_status') == 'trading', + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'maker': fees['maker'], + 'taker': fees['taker'], + 'precision': { + 'amount': baseMinSize, + 'price': self.parse_number(self.parse_precision(self.safe_string(market, 'price_max_precision'))), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': baseMinSize, + 'max': self.safe_number(market, 'base_max_size'), + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': self.parse_number(minCost), + 'max': None, + }, + }, + 'created': None, + 'info': market, + })) + return result + + async def fetch_contract_markets(self, params={}) -> List[MarketInterface]: + response = await self.publicGetContractPublicDetails(params) + # + # { + # "code": 1000, + # "message": "Ok", + # "data": { + # "symbols": [ + # { + # "symbol": "BTCUSDT", + # "product_type": 1, + # "open_timestamp": 1645977600000, + # "expire_timestamp": 0, + # "settle_timestamp": 0, + # "base_currency": "BTC", + # "quote_currency": "USDT", + # "last_price": "63547.4", + # "volume_24h": "110938430", + # "turnover_24h": "7004836342.6944", + # "index_price": "63587.85404255", + # "index_name": "BTCUSDT", + # "contract_size": "0.001", + # "min_leverage": "1", + # "max_leverage": "100", + # "price_precision": "0.1", + # "vol_precision": "1", + # "max_volume": "1000000", + # "min_volume": "1", + # "funding_rate": "0.0000801", + # "expected_funding_rate": "-0.0000035", + # "open_interest": "278214", + # "open_interest_value": "17555316.9355496", + # "high_24h": "64109.4", + # "low_24h": "61857.6", + # "change_24h": "0.0239264900886327", + # "funding_time": 1726819200000 + # }, + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + symbols = self.safe_list(data, 'symbols', []) + result = [] + fees = self.fees['trading'] + for i in range(0, len(symbols)): + market = symbols[i] + id = self.safe_string(market, 'symbol') + baseId = self.safe_string(market, 'base_currency') + quoteId = self.safe_string(market, 'quote_currency') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + settleId = 'USDT' # self is bitmart's ID for usdt + settle = self.safe_currency_code(settleId) + symbol = base + '/' + quote + ':' + settle + productType = self.safe_integer(market, 'product_type') + isSwap = (productType == 1) + isFutures = (productType == 2) + expiry = self.safe_integer(market, 'expire_timestamp') + if not isFutures and (expiry == 0): + expiry = None + result.append(self.safe_market_structure({ + 'id': id, + 'numericId': None, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': 'swap' if isSwap else 'future', + 'spot': False, + 'margin': False, + 'swap': isSwap, + 'future': isFutures, + 'option': False, + 'active': self.safe_string_lower(market, 'status') == 'trading', + 'contract': True, + 'linear': True, + 'inverse': False, + 'contractSize': self.safe_number(market, 'contract_size'), + 'expiry': expiry, + 'expiryDatetime': self.iso8601(expiry), + 'strike': None, + 'optionType': None, + 'maker': fees['maker'], + 'taker': fees['taker'], + 'precision': { + 'amount': self.safe_number(market, 'vol_precision'), + 'price': self.safe_number(market, 'price_precision'), + }, + 'limits': { + 'leverage': { + 'min': self.safe_number(market, 'min_leverage'), + 'max': self.safe_number(market, 'max_leverage'), + }, + 'amount': { + 'min': self.safe_number(market, 'min_volume'), + 'max': self.safe_number(market, 'max_volume'), + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': self.safe_integer(market, 'open_timestamp'), + 'info': market, + })) + return result + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for bitmart + + https://developer-pro.bitmart.com/en/spot/#get-trading-pair-details-v1 + https://developer-pro.bitmart.com/en/futuresv2/#get-contract-details + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + if self.options['adjustForTimeDifference']: + await self.load_time_difference() + spot = await self.fetch_spot_markets(params) + contract = await self.fetch_contract_markets(params) + return self.array_concat(spot, contract) + + async def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://developer-pro.bitmart.com/en/spot/#get-currency-list-v1 + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = await self.publicGetAccountV1Currencies(params) + # + # { + # "message": "OK", + # "code": 1000, + # "trace": "619294ecef584282b26a3be322b1e01f.66.17403093228242229", + # "data": { + # "currencies": [ + # { + # "currency": "BTC", + # "name": "Bitcoin", + # "recharge_minsize": '0.00000001', + # "contract_address": null, + # "network": "BTC", + # "withdraw_enabled": True, + # "deposit_enabled": True, + # "withdraw_minsize": "0.0003", + # "withdraw_minfee": "9.61", + # "withdraw_fee_estimate": "9.61", + # "withdraw_fee": "0.0001" + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + currencies = self.safe_list(data, 'currencies', []) + result = {} + for i in range(0, len(currencies)): + currency = currencies[i] + fullId = self.safe_string(currency, 'currency') + currencyId = fullId + networkId = self.safe_string(currency, 'network') + isNtf = (fullId.find('NFT') >= 0) + if not isNtf: + parts = fullId.split('-') + currencyId = self.safe_string(parts, 0) + second = self.safe_string(parts, 1) + if second is not None: + networkId = second.upper() + currencyCode = self.safe_currency_code(currencyId) + entry = self.safe_dict(result, currencyCode) + if entry is None: + entry = { + 'info': currency, + 'id': currencyId, + 'code': currencyCode, + 'precision': None, + 'name': self.safe_string(currency, 'name'), + 'deposit': None, + 'withdraw': None, + 'active': None, + 'networks': {}, + 'type': 'other' if isNtf else 'crypto', + } + networkCode = self.network_id_to_code(networkId) + withdraw = self.safe_bool(currency, 'withdraw_enabled') + deposit = self.safe_bool(currency, 'deposit_enabled') + entry['networks'][networkCode] = { + 'info': currency, + 'id': networkId, + 'code': networkCode, + 'withdraw': withdraw, + 'deposit': deposit, + 'active': withdraw and deposit, + 'fee': self.safe_number(currency, 'withdraw_fee'), + 'limits': { + 'withdraw': { + 'min': self.safe_number(currency, 'withdraw_minsize'), + 'max': None, + }, + 'deposit': { + 'min': None, + 'max': None, + }, + }, + } + result[currencyCode] = entry + keys = list(result.keys()) + for i in range(0, len(keys)): + key = keys[i] + currency = result[key] + result[key] = self.safe_currency_structure(currency) + return result + + def get_currency_id_from_code_and_network(self, currencyCode: Str, networkCode: Str) -> Str: + if networkCode is None: + networkCode = self.default_network_code(currencyCode) # use default network code if not provided + currency = self.currency(currencyCode) + id = currency['id'] + idFromNetwork: Str = None + networks = self.safe_dict(currency, 'networks', {}) + networkInfo: dict = {} + if networkCode is None: + # network code is not provided and there is no default network code + network = self.safe_dict(networks, currencyCode) # trying to find network that has the same code + if network is None: + # use the first network in the networks list if there is no network code with the same code + keys = list(networks.keys()) + length = len(keys) + if length > 0: + network = self.safe_value(networks, keys[0]) + networkInfo = self.safe_dict(network, 'info', {}) + idFromNetwork = self.safe_string(networkInfo, 'currency') # use currency name from network + else: + providedOrDefaultNetwork = self.safe_dict(networks, networkCode) + if providedOrDefaultNetwork is not None: + networkInfo = self.safe_dict(providedOrDefaultNetwork, 'info', {}) + idFromNetwork = self.safe_string(networkInfo, 'currency') # use currency name from network + else: + id += '-' + self.network_code_to_id(networkCode, currencyCode) # use concatenated currency id and network code if network is not found + return idFromNetwork if (idFromNetwork is not None) else id + + async def fetch_transaction_fee(self, code: str, params={}): + """ + @deprecated + please use fetchDepositWithdrawFee instead + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.network]: the network code of the currency + :returns dict: a `fee structure ` + """ + await self.load_markets() + currency = self.currency(code) + network: Str = None + network, params = self.handle_network_code_and_params(params) + request: dict = { + 'currency': self.get_currency_id_from_code_and_network(currency['code'], network), + } + response = await self.privateGetAccountV1WithdrawCharge(self.extend(request, params)) + # + # { + # "message": "OK", + # "code": "1000", + # "trace": "3ecc0adf-91bd-4de7-aca1-886c1122f54f", + # "data": { + # "today_available_withdraw_BTC": "100.0000", + # "min_withdraw": "0.005", + # "withdraw_precision": "8", + # "withdraw_fee": "0.000500000000000000000000000000" + # } + # } + # + data = response['data'] + withdrawFees: dict = {} + withdrawFees[code] = self.safe_number(data, 'withdraw_fee') + return { + 'info': response, + 'withdraw': withdrawFees, + 'deposit': {}, + } + + def parse_deposit_withdraw_fee(self, fee, currency: Currency = None): + # + # { + # "today_available_withdraw_BTC": "100.0000", + # "min_withdraw": "0.005", + # "withdraw_precision": "8", + # "withdraw_fee": "0.000500000000000000000000000000" + # } + # + return { + 'info': fee, + 'withdraw': { + 'fee': self.safe_number(fee, 'withdraw_fee'), + 'percentage': None, + }, + 'deposit': { + 'fee': None, + 'percentage': None, + }, + 'networks': {}, + } + + async def fetch_deposit_withdraw_fee(self, code: str, params={}): + """ + fetch the fee for deposits and withdrawals + + https://developer-pro.bitmart.com/en/spot/#withdraw-quota-keyed + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.network]: the network code of the currency + :returns dict: a `fee structure ` + """ + await self.load_markets() + network: Str = None + network, params = self.handle_network_code_and_params(params) + request: dict = { + 'currency': self.get_currency_id_from_code_and_network(code, network), + } + response = await self.privateGetAccountV1WithdrawCharge(self.extend(request, params)) + # + # { + # "message": "OK", + # "code": "1000", + # "trace": "3ecc0adf-91bd-4de7-aca1-886c1122f54f", + # "data": { + # "today_available_withdraw_BTC": "100.0000", + # "min_withdraw": "0.005", + # "withdraw_precision": "8", + # "withdraw_fee": "0.000500000000000000000000000000" + # } + # } + # + data = response['data'] + return self.parse_deposit_withdraw_fee(data) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # spot(REST) fetchTickers + # + # { + # 'result': [ + # "AFIN_USDT", # symbol + # "0.001047", # last + # "11110", # v_24h + # "11.632170", # qv_24h + # "0.001048", # open_24h + # "0.001048", # high_24h + # "0.001047", # low_24h + # "-0.00095", # price_change_24h + # "0.001029", # bid_px + # "5555", # bid_sz + # "0.001041", # ask_px + # "5297", # ask_sz + # "1717122550482" # timestamp + # ] + # } + # + # spot(REST) fetchTicker + # + # { + # "symbol": "BTC_USDT", + # "last": "68500.00", + # "v_24h": "10491.65490", + # "qv_24h": "717178990.42", + # "open_24h": "68149.75", + # "high_24h": "69499.99", + # "low_24h": "67132.40", + # "fluctuation": "0.00514", + # "bid_px": "68500", + # "bid_sz": "0.00162", + # "ask_px": "68500.01", + # "ask_sz": "0.01722", + # "ts": "1717131391671" + # } + # + # spot(WS) + # + # { + # "symbol":"BTC_USDT", + # "last_price":"146.24", + # "open_24h":"147.17", + # "high_24h":"147.48", + # "low_24h":"143.88", + # "base_volume_24h":"117387.58", # NOT base, but quote currencynot !! + # "s_t": 1610936002 + # } + # + # swap + # + # { + # "symbol": "BTCUSDT", + # "product_type": 1, + # "open_timestamp": 1645977600000, + # "expire_timestamp": 0, + # "settle_timestamp": 0, + # "base_currency": "BTC", + # "quote_currency": "USDT", + # "last_price": "63547.4", + # "volume_24h": "110938430", + # "turnover_24h": "7004836342.6944", + # "index_price": "63587.85404255", + # "index_name": "BTCUSDT", + # "contract_size": "0.001", + # "min_leverage": "1", + # "max_leverage": "100", + # "price_precision": "0.1", + # "vol_precision": "1", + # "max_volume": "1000000", + # "min_volume": "1", + # "funding_rate": "0.0000801", + # "expected_funding_rate": "-0.0000035", + # "open_interest": "278214", + # "open_interest_value": "17555316.9355496", + # "high_24h": "64109.4", + # "low_24h": "61857.6", + # "change_24h": "0.0239264900886327", + # "funding_time": 1726819200000 + # } + # + result = self.safe_list(ticker, 'result', []) + average = self.safe_string_2(ticker, 'avg_price', 'index_price') + marketId = self.safe_string_2(ticker, 'symbol', 'contract_symbol') + timestamp = self.safe_integer_2(ticker, 'timestamp', 'ts') + last = self.safe_string_2(ticker, 'last_price', 'last') + percentage = self.safe_string_2(ticker, 'price_change_percent_24h', 'change_24h') + change = self.safe_string(ticker, 'fluctuation') + high = self.safe_string_2(ticker, 'high_24h', 'high_price') + low = self.safe_string_2(ticker, 'low_24h', 'low_price') + bid = self.safe_string_2(ticker, 'best_bid', 'bid_px') + bidVolume = self.safe_string_2(ticker, 'best_bid_size', 'bid_sz') + ask = self.safe_string_2(ticker, 'best_ask', 'ask_px') + askVolume = self.safe_string_2(ticker, 'best_ask_size', 'ask_sz') + open = self.safe_string(ticker, 'open_24h') + baseVolume = self.safe_string_n(ticker, ['base_volume_24h', 'v_24h', 'volume_24h']) + quoteVolume = self.safe_string_lower_n(ticker, ['quote_volume_24h', 'qv_24h', 'turnover_24h']) + listMarketId = self.safe_string(result, 0) + if listMarketId is not None: + marketId = listMarketId + timestamp = self.safe_integer(result, 12) + high = self.safe_string(result, 5) + low = self.safe_string(result, 6) + bid = self.safe_string(result, 8) + bidVolume = self.safe_string(result, 9) + ask = self.safe_string(result, 10) + askVolume = self.safe_string(result, 11) + open = self.safe_string(result, 4) + last = self.safe_string(result, 1) + change = self.safe_string(result, 7) + baseVolume = self.safe_string(result, 2) + quoteVolume = self.safe_string_lower(result, 3) + market = self.safe_market(marketId, market) + symbol = market['symbol'] + if timestamp is None: + # ticker from WS has a different field(in seconds) + timestamp = self.safe_integer_product(ticker, 's_t', 1000) + if percentage is None: + percentage = Precise.string_mul(change, '100') + if quoteVolume is None: + if baseVolume is None: + # self is swap + quoteVolume = self.safe_string(ticker, 'volume_24h', quoteVolume) + else: + # self is a ticker from websockets + # contrary to name and documentation, base_volume_24h is actually the quote volume + quoteVolume = baseVolume + baseVolume = None + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': high, + 'low': low, + 'bid': bid, + 'bidVolume': bidVolume, + 'ask': ask, + 'askVolume': askVolume, + 'vwap': None, + 'open': open, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': percentage, + 'average': average, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'indexPrice': self.safe_string(ticker, 'index_price'), + 'info': ticker, + }, market) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://developer-pro.bitmart.com/en/spot/#get-ticker-of-a-trading-pair-v3 + https://developer-pro.bitmart.com/en/futuresv2/#get-contract-details + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = {} + response = None + if market['swap']: + request['symbol'] = market['id'] + response = await self.publicGetContractPublicDetails(self.extend(request, params)) + # + # { + # "code": 1000, + # "message": "Ok", + # "data": { + # "symbols": [ + # { + # "symbol": "BTCUSDT", + # "product_type": 1, + # "open_timestamp": 1645977600000, + # "expire_timestamp": 0, + # "settle_timestamp": 0, + # "base_currency": "BTC", + # "quote_currency": "USDT", + # "last_price": "63547.4", + # "volume_24h": "110938430", + # "turnover_24h": "7004836342.6944", + # "index_price": "63587.85404255", + # "index_name": "BTCUSDT", + # "contract_size": "0.001", + # "min_leverage": "1", + # "max_leverage": "100", + # "price_precision": "0.1", + # "vol_precision": "1", + # "max_volume": "1000000", + # "min_volume": "1", + # "funding_rate": "0.0000801", + # "expected_funding_rate": "-0.0000035", + # "open_interest": "278214", + # "open_interest_value": "17555316.9355496", + # "high_24h": "64109.4", + # "low_24h": "61857.6", + # "change_24h": "0.0239264900886327", + # "funding_time": 1726819200000 + # }, + # ] + # } + # } + # + elif market['spot']: + request['symbol'] = market['id'] + response = await self.publicGetSpotQuotationV3Ticker(self.extend(request, params)) + # + # { + # "code": 1000, + # "trace": "f2194c2c202d2.99.1717535", + # "message": "success", + # "data": { + # "symbol": "BTC_USDT", + # "last": "68500.00", + # "v_24h": "10491.65490", + # "qv_24h": "717178990.42", + # "open_24h": "68149.75", + # "high_24h": "69499.99", + # "low_24h": "67132.40", + # "fluctuation": "0.00514", + # "bid_px": "68500", + # "bid_sz": "0.00162", + # "ask_px": "68500.01", + # "ask_sz": "0.01722", + # "ts": "1717131391671" + # } + # } + # + else: + raise NotSupported(self.id + ' fetchTicker() does not support ' + market['type'] + ' markets, only spot and swap markets are accepted') + # fails in naming for contract tickers 'contract_symbol' + tickers = [] + ticker: dict = {} + if market['spot']: + ticker = self.safe_dict(response, 'data', {}) + else: + data = self.safe_dict(response, 'data', {}) + tickers = self.safe_list(data, 'symbols', []) + ticker = self.safe_dict(tickers, 0, {}) + return self.parse_ticker(ticker, market) + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://developer-pro.bitmart.com/en/spot/#get-ticker-of-all-pairs-v3 + https://developer-pro.bitmart.com/en/futuresv2/#get-contract-details + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + type = None + market = None + if symbols is not None: + symbol = self.safe_string(symbols, 0) + market = self.market(symbol) + type, params = self.handle_market_type_and_params('fetchTickers', market, params) + response = None + if type == 'spot': + response = await self.publicGetSpotQuotationV3Tickers(params) + # + # { + # "code": 1000, + # "trace": "17c5e5d9ac49f9b71efca2bed55f1a.105.171225637482393", + # "message": "success", + # "data": [ + # [ + # "AFIN_USDT", + # "0.001047", + # "11110", + # "11.632170", + # "0.001048", + # "0.001048", + # "0.001047", + # "-0.00095", + # "0.001029", + # "5555", + # "0.001041", + # "5297", + # "1717122550482" + # ], + # ] + # } + # + elif type == 'swap': + response = await self.publicGetContractPublicDetails(params) + # + # { + # "code": 1000, + # "message": "Ok", + # "data": { + # "symbols": [ + # { + # "symbol": "BTCUSDT", + # "product_type": 1, + # "open_timestamp": 1645977600000, + # "expire_timestamp": 0, + # "settle_timestamp": 0, + # "base_currency": "BTC", + # "quote_currency": "USDT", + # "last_price": "63547.4", + # "volume_24h": "110938430", + # "turnover_24h": "7004836342.6944", + # "index_price": "63587.85404255", + # "index_name": "BTCUSDT", + # "contract_size": "0.001", + # "min_leverage": "1", + # "max_leverage": "100", + # "price_precision": "0.1", + # "vol_precision": "1", + # "max_volume": "1000000", + # "min_volume": "1", + # "funding_rate": "0.0000801", + # "expected_funding_rate": "-0.0000035", + # "open_interest": "278214", + # "open_interest_value": "17555316.9355496", + # "high_24h": "64109.4", + # "low_24h": "61857.6", + # "change_24h": "0.0239264900886327", + # "funding_time": 1726819200000 + # }, + # ] + # } + # } + # + else: + raise NotSupported(self.id + ' fetchTickers() does not support ' + type + ' markets, only spot and swap markets are accepted') + tickers = [] + if type == 'spot': + tickers = self.safe_list(response, 'data', []) + else: + data = self.safe_dict(response, 'data', {}) + tickers = self.safe_list(data, 'symbols', []) + result: dict = {} + for i in range(0, len(tickers)): + ticker: dict = {} + if type == 'spot': + ticker = self.parse_ticker({'result': tickers[i]}) + else: + ticker = self.parse_ticker(tickers[i]) + symbol = ticker['symbol'] + result[symbol] = ticker + return self.filter_by_array_tickers(result, 'symbol', symbols) + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://developer-pro.bitmart.com/en/spot/#get-depth-v3 + https://developer-pro.bitmart.com/en/futuresv2/#get-market-depth + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = None + if market['spot']: + if limit is not None: + request['limit'] = limit # default 35, max 50 + response = await self.publicGetSpotQuotationV3Books(self.extend(request, params)) + elif market['swap']: + response = await self.publicGetContractPublicDepth(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchOrderBook() does not support ' + market['type'] + ' markets, only spot and swap markets are accepted') + # + # spot + # + # { + # "code": 1000, + # "message": "success", + # "data": { + # "ts": "1695264191808", + # "symbol": "BTC_USDT", + # "asks": [ + # ["26942.57","0.06492"], + # ["26942.73","0.05447"], + # ["26943.00","0.07154"] + # ], + # "bids": [ + # ["26942.45","0.00074"], + # ["26941.53","0.00371"], + # ["26940.94","0.08992"] + # ] + # }, + # "trace": "430a7f69581d4258a8e4b424dfb10782.73.16952341919017619" + # } + # + # swap + # + # { + # "code": 1000, + # "message": "Ok", + # "data": { + # "asks": [ + # ["26938.3","3499","3499"], + # ["26938.5","14702","18201"], + # ["26938.6","20457","38658"] + # ], + # "bids": [ + # ["26938.2","20","20"], + # ["26937.9","1913","1933"], + # ["26937.8","2588","4521"] + # ], + # "timestamp": 1695264383999, + # "symbol": "BTCUSDT" + # }, + # "trace": "4cad855074664097ac6ba5258c47305d.72.16952643834721135" + # } + # + data = self.safe_dict(response, 'data', {}) + timestamp = self.safe_integer_2(data, 'ts', 'timestamp') + return self.parse_order_book(data, market['symbol'], timestamp) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # public fetchTrades spot( amount = count * price ) + # + # [ + # "BTC_USDT", # symbol + # "1717212457302", # timestamp + # "67643.11", # price + # "0.00106", # size + # "sell" # side + # ] + # + # spot: fetchMyTrades + # + # { + # "tradeId":"182342999769370687", + # "orderId":"183270218784142990", + # "clientOrderId":"183270218784142990", + # "symbol":"ADA_USDT", + # "side":"buy", + # "orderMode":"spot", + # "type":"market", + # "price":"0.245948", + # "size":"20.71", + # "notional":"5.09358308", + # "fee":"0.00509358", + # "feeCoinName":"USDT", + # "tradeRole":"taker", + # "createTime":1695658457836, + # } + # + # swap: fetchMyTrades + # + # { + # "order_id": "230930336848609", + # "trade_id": "6212604014", + # "symbol": "BTCUSDT", + # "side": 3, + # "price": "26910.4", + # "vol": "1", + # "exec_type": "Taker", + # "profit": False, + # "create_time": 1695961596692, + # "realised_profit": "-0.0003", + # "paid_fees": "0.01614624" + # } + # + # ws swap + # + # { + # 'fee': '-0.000044502', + # 'feeCcy': 'USDT', + # 'fillPrice': '74.17', + # 'fillQty': '1', + # 'lastTradeID': 6802340762 + # } + # + timestamp = self.safe_integer_n(trade, ['createTime', 'create_time', 1]) + isPublic = self.safe_string(trade, 0) + isPublicTrade = (isPublic is not None) + amount = None + cost = None + type = None + side = None + if isPublicTrade: + amount = self.safe_string_2(trade, 'count', 3) + cost = self.safe_string(trade, 'amount') + side = self.safe_string_2(trade, 'type', 4) + else: + amount = self.safe_string_n(trade, ['size', 'vol', 'fillQty']) + cost = self.safe_string(trade, 'notional') + type = self.safe_string(trade, 'type') + side = self.parse_order_side(self.safe_string(trade, 'side')) + marketId = self.safe_string_2(trade, 'symbol', 0) + market = self.safe_market(marketId, market) + feeCostString = self.safe_string_2(trade, 'fee', 'paid_fees') + fee = None + if feeCostString is not None: + feeCurrencyId = self.safe_string(trade, 'feeCoinName') + feeCurrencyCode = self.safe_currency_code(feeCurrencyId) + if feeCurrencyCode is None: + feeCurrencyCode = market['base'] if (side == 'buy') else market['quote'] + fee = { + 'cost': feeCostString, + 'currency': feeCurrencyCode, + } + return self.safe_trade({ + 'info': trade, + 'id': self.safe_string_n(trade, ['tradeId', 'trade_id', 'lastTradeID']), + 'order': self.safe_string_2(trade, 'orderId', 'order_id'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'type': type, + 'side': side, + 'price': self.safe_string_n(trade, ['price', 'fillPrice', 2]), + 'amount': amount, + 'cost': cost, + 'takerOrMaker': self.safe_string_lower_2(trade, 'tradeRole', 'exec_type'), + 'fee': fee, + }, market) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get a list of the most recent trades for a particular symbol + + https://developer-pro.bitmart.com/en/spot/#get-recent-trades-v3 + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum number of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' fetchTrades() does not support ' + market['type'] + ' orders, only spot orders are accepted') + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['limit'] = limit + response = await self.publicGetSpotQuotationV3Trades(self.extend(request, params)) + # + # { + # "code": 1000, + # "trace": "58031f9a5bd.111.17117", + # "message": "success", + # "data": [ + # [ + # "BTC_USDT", + # "1717212457302", + # "67643.11", + # "0.00106", + # "sell" + # ], + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_trades(data, market, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # spot + # [ + # "1699512060", # timestamp + # "36746.49", # open + # "36758.71", # high + # "36736.13", # low + # "36755.99", # close + # "2.83965", # base volume + # "104353.57" # quote volume + # ] + # + # swap + # { + # "low_price": "20090.3", + # "high_price": "20095.5", + # "open_price": "20092.6", + # "close_price": "20091.4", + # "volume": "8748", + # "timestamp": 1665002281 + # } + # + # ws + # [ + # 1631056350, # timestamp + # "46532.83", # open + # "46555.71", # high + # "46511.41", # low + # "46555.71", # close + # "0.25", # volume + # ] + # + # ws swap + # { + # "symbol":"BTCUSDT", + # "o":"146.24", + # "h":"146.24", + # "l":"146.24", + # "c":"146.24", + # "v":"146" + # } + # + if isinstance(ohlcv, list): + return [ + self.safe_timestamp(ohlcv, 0), + self.safe_number(ohlcv, 1), + self.safe_number(ohlcv, 2), + self.safe_number(ohlcv, 3), + self.safe_number(ohlcv, 4), + self.safe_number(ohlcv, 5), + ] + else: + return [ + self.safe_timestamp_2(ohlcv, 'timestamp', 'ts'), + self.safe_number_2(ohlcv, 'open_price', 'o'), + self.safe_number_2(ohlcv, 'high_price', 'h'), + self.safe_number_2(ohlcv, 'low_price', 'l'), + self.safe_number_2(ohlcv, 'close_price', 'c'), + self.safe_number_2(ohlcv, 'volume', 'v'), + ] + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://developer-pro.bitmart.com/en/spot/#get-history-k-line-v3 + https://developer-pro.bitmart.com/en/futuresv2/#get-k-line + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp of the latest candle in ms + :param boolean [params.paginate]: *spot only* default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate', False) + if paginate: + return await self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params, 200) + market = self.market(symbol) + duration = self.parse_timeframe(timeframe) + parsedTimeframe = self.safe_integer(self.timeframes, timeframe) + request: dict = { + 'symbol': market['id'], + } + if parsedTimeframe is not None: + request['step'] = parsedTimeframe + else: + request['step'] = timeframe + if market['spot']: + request, params = self.handle_until_option('before', request, params, 0.001) + if limit is not None: + request['limit'] = limit + if since is not None: + request['after'] = self.parse_to_int((since / 1000)) - 1 + else: + maxLimit = 500 + if limit is None: + limit = maxLimit + limit = min(maxLimit, limit) + now = self.parse_to_int(self.milliseconds() / 1000) + if since is None: + start = now - limit * duration + request['start_time'] = start + request['end_time'] = now + else: + start = self.parse_to_int((since / 1000)) - 1 + end = self.sum(start, limit * duration) + request['start_time'] = start + request['end_time'] = min(end, now) + request, params = self.handle_until_option('end_time', request, params, 0.001) + response = None + if market['swap']: + price = self.safe_string(params, 'price') + if price == 'mark': + params = self.omit(params, 'price') + response = await self.publicGetContractPublicMarkpriceKline(self.extend(request, params)) + else: + response = await self.publicGetContractPublicKline(self.extend(request, params)) + else: + response = await self.publicGetSpotQuotationV3Klines(self.extend(request, params)) + # + # spot + # + # { + # "code": 1000, + # "message": "success", + # "data": [ + # ["1699512060","36746.49","36758.71","36736.13","36755.99","2.83965","104353.57"], + # ["1699512120","36756.00","36758.70","36737.14","36737.63","1.96070","72047.10"], + # ["1699512180","36737.63","36740.45","36737.62","36740.44","0.63194","23217.62"] + # ], + # "trace": "6591fc7b508845359d5fa442e3b3a4fb.72.16995122398750695" + # } + # + # swap + # + # { + # "code": 1000, + # "message": "Ok", + # "data": [ + # { + # "low_price": "20090.3", + # "high_price": "20095.5", + # "open_price": "20092.6", + # "close_price": "20091.4", + # "volume": "8748", + # "timestamp": 1665002281 + # }, + # ... + # ], + # "trace": "96c989db-e0f5-46f5-bba6-60cfcbde699b" + # } + # + ohlcv = self.safe_list(response, 'data', []) + return self.parse_ohlcvs(ohlcv, market, timeframe, since, limit) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + + https://developer-pro.bitmart.com/en/spot/#account-trade-list-v4-signed + https://developer-pro.bitmart.com/en/futuresv2/#get-order-trade-keyed + + fetch all trades made by the user + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch trades for + :param boolean [params.marginMode]: *spot* whether to fetch trades for margin orders or spot orders, defaults to spot orders(only isolated margin orders are supported) + :param str [params.stpMode]: self-trade prevention only for spot, defaults to none, ['none', 'cancel_maker', 'cancel_taker', 'cancel_both'] + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = None + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + type = None + response = None + type, params = self.handle_market_type_and_params('fetchMyTrades', market, params) + until = self.safe_integer_n(params, ['until', 'endTime', 'end_time']) + params = self.omit(params, ['until']) + if type == 'spot': + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchMyTrades', params) + if marginMode == 'isolated': + request['orderMode'] = 'iso_margin' + options = self.safe_dict(self.options, 'fetchMyTrades', {}) + maxLimit = 200 + defaultLimit = self.safe_integer(options, 'limit', maxLimit) + if limit is None: + limit = defaultLimit + request['limit'] = min(limit, maxLimit) + if since is not None: + request['startTime'] = since + if until is not None: + request['endTime'] = until + response = await self.privatePostSpotV4QueryTrades(self.extend(request, params)) + elif type == 'swap': + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMyTrades() requires a symbol argument') + if since is not None: + request['start_time'] = since + if until is not None: + request['end_time'] = until + response = await self.privateGetContractPrivateTrades(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchMyTrades() does not support ' + type + ' orders, only spot and swap orders are accepted') + # + # spot + # + # { + # "code":1000, + # "message":"success", + # "data":[ + # { + # "tradeId":"182342999769370687", + # "orderId":"183270218784142990", + # "clientOrderId":"183270218784142990", + # "symbol":"ADA_USDT", + # "side":"buy", + # "orderMode":"spot", + # "type":"market", + # "price":"0.245948", + # "size":"20.71", + # "notional":"5.09358308", + # "fee":"0.00509358", + # "feeCoinName":"USDT", + # "tradeRole":"taker", + # "createTime":1695658457836, + # "updateTime":1695658457836 + # } + # ], + # "trace":"fbaee9e0e2f5442fba5b3262fc86b0ac.65.16956593456523085" + # } + # + # swap + # + # { + # "code": 1000, + # "message": "Ok", + # "data": [ + # { + # "order_id": "230930336848609", + # "trade_id": "6212604014", + # "symbol": "BTCUSDT", + # "side": 3, + # "price": "26910.4", + # "vol": "1", + # "exec_type": "Taker", + # "profit": False, + # "create_time": 1695961596692, + # "realised_profit": "-0.0003", + # "paid_fees": "0.01614624" + # }, + # ], + # "trace": "4cad855074634097ac6ba5257c47305d.62.16959616054873723" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_trades(data, market, since, limit) + + async def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + + https://developer-pro.bitmart.com/en/spot/#order-trade-list-v4-signed + + fetch all the trades made from a single order + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.stpMode]: self-trade prevention only for spot, defaults to none, ['none', 'cancel_maker', 'cancel_taker', 'cancel_both'] + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + request: dict = { + 'orderId': id, + } + response = await self.privatePostSpotV4QueryOrderTrades(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + return self.parse_trades(data, None, since, limit) + + def custom_parse_balance(self, response, marketType) -> Balances: + data = self.safe_dict(response, 'data', {}) + wallet = None + if marketType == 'swap': + wallet = self.safe_list(response, 'data', []) + elif marketType == 'margin': + wallet = self.safe_list(data, 'symbols', []) + else: + wallet = self.safe_list(data, 'wallet', []) + result = {'info': response} + if marketType == 'margin': + for i in range(0, len(wallet)): + entry = wallet[i] + marketId = self.safe_string(entry, 'symbol') + symbol = self.safe_symbol(marketId, None, '_') + base = self.safe_dict(entry, 'base', {}) + quote = self.safe_dict(entry, 'quote', {}) + baseCode = self.safe_currency_code(self.safe_string(base, 'currency')) + quoteCode = self.safe_currency_code(self.safe_string(quote, 'currency')) + subResult: dict = {} + subResult[baseCode] = self.parse_balance_helper(base) + subResult[quoteCode] = self.parse_balance_helper(quote) + result[symbol] = self.safe_balance(subResult) + return result + else: + for i in range(0, len(wallet)): + balance = wallet[i] + currencyId = self.safe_string_2(balance, 'id', 'currency') + currencyId = self.safe_string(balance, 'coin_code', currencyId) + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string_2(balance, 'available', 'available_balance') + account['used'] = self.safe_string_n(balance, ['unAvailable', 'frozen', 'frozen_balance']) + result[code] = account + return self.safe_balance(result) + + def parse_balance_helper(self, entry): + account = self.account() + account['used'] = self.safe_string(entry, 'frozen') + account['free'] = self.safe_string(entry, 'available') + account['total'] = self.safe_string(entry, 'total_asset') + debt = self.safe_string(entry, 'borrow_unpaid') + interest = self.safe_string(entry, 'interest_unpaid') + account['debt'] = Precise.string_add(debt, interest) + return account + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://developer-pro.bitmart.com/en/spot/#get-spot-wallet-balance-keyed + https://developer-pro.bitmart.com/en/futuresv2/#get-contract-assets-keyed + https://developer-pro.bitmart.com/en/spot/#get-account-balance-keyed + https://developer-pro.bitmart.com/en/spot/#get-margin-account-details-isolated-keyed + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + marketType = None + marketType, params = self.handle_market_type_and_params('fetchBalance', None, params) + marginMode = self.safe_string(params, 'marginMode') + isMargin = self.safe_bool(params, 'margin', False) + params = self.omit(params, ['margin', 'marginMode']) + if marginMode is not None or isMargin: + marketType = 'margin' + response = None + if marketType == 'spot': + response = await self.privateGetSpotV1Wallet(params) + elif marketType == 'swap': + response = await self.privateGetContractPrivateAssetsDetail(params) + elif marketType == 'account': + response = await self.privateGetAccountV1Wallet(params) + elif marketType == 'margin': + response = await self.privateGetSpotV1MarginIsolatedAccount(params) + else: + raise NotSupported(self.id + ' fetchBalance() does not support ' + marketType + ' markets, only spot, swap and account and margin markets are accepted') + # + # spot + # + # { + # "message":"OK", + # "code":1000, + # "trace":"39069916-72f9-44c7-acde-2ad5afd21cad", + # "data":{ + # "wallet":[ + # {"id":"BTC","name":"Bitcoin","available":"0.00000062","frozen":"0.00000000"}, + # {"id":"ETH","name":"Ethereum","available":"0.00002277","frozen":"0.00000000"}, + # {"id":"BMX","name":"BitMart Token","available":"0.00000000","frozen":"0.00000000"} + # ] + # } + # } + # + # account + # + # { + # "message":"OK", + # "code":1000, + # "trace":"5c3b7fc7-93b2-49ef-bb59-7fdc56915b59", + # "data":{ + # "wallet":[ + # {"currency":"BTC","name":"Bitcoin","available":"0.00000062","frozen":"0.00000000","available_usd_valuation":null}, + # {"currency":"ETH","name":"Ethereum","available":"0.00002277","frozen":"0.00000000","available_usd_valuation":null} + # ] + # } + # } + # + # swap + # + # { + # "code": 1000, + # "message": "Ok", + # "data": [ + # { + # "currency": "USDT", + # "available_balance": "0", + # "frozen_balance": "0", + # "unrealized": "0", + # "equity": "0", + # "position_deposit": "0" + # }, + # ... + # ], + # "trace": "f9da3a39-cf45-42e7-914d-294f565dfc33" + # } + # + # margin + # + # { + # "message": "OK", + # "code": 1000, + # "trace": "61dd6ab265c04064b72d8bc9b205f741.71.16701055600915302", + # "data": { + # "symbols": [ + # { + # "symbol": "BTC_USDT", + # "risk_rate": "999.00", + # "risk_level": "1", + # "buy_enabled": False, + # "sell_enabled": False, + # "liquidate_price": null, + # "liquidate_rate": "1.15", + # "base": { + # "currency": "BTC", + # "borrow_enabled": True, + # "borrowed": "0.00000000", + # "available": "0.00000000", + # "frozen": "0.00000000", + # "net_asset": "0.00000000", + # "net_assetBTC": "0.00000000", + # "total_asset": "0.00000000", + # "borrow_unpaid": "0.00000000", + # "interest_unpaid": "0.00000000" + # }, + # "quote": { + # "currency": "USDT", + # "borrow_enabled": True, + # "borrowed": "0.00000000", + # "available": "20.00000000", + # "frozen": "0.00000000", + # "net_asset": "20.00000000", + # "net_assetBTC": "0.00118008", + # "total_asset": "20.00000000", + # "borrow_unpaid": "0.00000000", + # "interest_unpaid": "0.00000000" + # } + # } + # ] + # } + # } + # + return self.custom_parse_balance(response, marketType) + + def parse_trading_fee(self, fee: dict, market: Market = None) -> TradingFeeInterface: + # + # { + # "symbol": "ETH_USDT", + # "taker_fee_rate": "0.0025", + # "maker_fee_rate": "0.0025" + # } + # + marketId = self.safe_string(fee, 'symbol') + symbol = self.safe_symbol(marketId) + return { + 'info': fee, + 'symbol': symbol, + 'maker': self.safe_number(fee, 'maker_fee_rate'), + 'taker': self.safe_number(fee, 'taker_fee_rate'), + 'percentage': None, + 'tierBased': None, + } + + async def fetch_trading_fee(self, symbol: str, params={}) -> TradingFeeInterface: + """ + fetch the trading fees for a market + + https://developer-pro.bitmart.com/en/spot/#get-actual-trade-fee-rate-keyed + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `fee structure ` + """ + await self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' fetchTradingFee() does not support ' + market['type'] + ' orders, only spot orders are accepted') + request: dict = { + 'symbol': market['id'], + } + response = await self.privateGetSpotV1TradeFee(self.extend(request, params)) + # + # { + # "message": "OK", + # "code": "1000", + # "trace": "5a6f1e40-37fe-4849-a494-03279fadcc62", + # "data": { + # "symbol": "ETH_USDT", + # "taker_fee_rate": "0.0025", + # "maker_fee_rate": "0.0025" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_trading_fee(data) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder, editOrder + # + # { + # "order_id": 2707217580 + # } + # + # swap + # "data": { + # "order_id": 231116359426639, + # "price": "market price" + # }, + # + # cancelOrder + # + # "2707217580" # order id + # + # spot fetchOrder, fetchOrdersByStatus, fetchOpenOrders, fetchClosedOrders + # + # { + # "order_id":1736871726781, + # "symbol":"BTC_USDT", + # "create_time":1591096004000, + # "side":"sell", + # "type":"market", # limit, market, limit_maker, ioc + # "price":"0.00", + # "price_avg":"0.00", + # "size":"0.02000", + # "notional":"0.00000000", + # "filled_notional":"0.00000000", + # "filled_size":"0.00000", + # "status":"8" + # } + # + # spot v4 + # { + # "orderId" : "118100034543076010", + # "clientOrderId" : "118100034543076010", + # "symbol" : "BTC_USDT", + # "side" : "buy", + # "orderMode" : "spot", + # "type" : "limit", + # "state" : "filled", + # "price" : "48800.00", + # "priceAvg" : "39999.00", + # "size" : "0.10000", + # "filledSize" : "0.10000", + # "notional" : "4880.00000000", + # "filledNotional" : "3999.90000000", + # "createTime" : 1681701557927, + # "updateTime" : 1681701559408 + # } + # + # swap: fetchOrder, fetchOpenOrders, fetchClosedOrders + # + # { + # "order_id": "230935812485489", + # "client_order_id": "", + # "price": "24000", + # "size": "1", + # "symbol": "BTCUSDT", + # "state": 2, + # "side": 1, + # "type": "limit", + # "leverage": "10", + # "open_type": "isolated", + # "deal_avg_price": "0", + # "deal_size": "0", + # "create_time": 1695702258629, + # "update_time": 1695702258642, + # "activation_price_type": 0, + # "activation_price": "", + # "callback_rate": "" + # } + # + id = None + if isinstance(order, str): + id = order + order = {} + id = self.safe_string_2(order, 'order_id', 'orderId', id) + timestamp = self.safe_integer_2(order, 'create_time', 'createTime') + marketId = self.safe_string(order, 'symbol') + symbol = self.safe_symbol(marketId, market) + market = self.safe_market(symbol, market) + orderType = self.safe_string(market, 'type', 'spot') + type = self.safe_string(order, 'type') + timeInForce = None + postOnly = None + if type == 'limit_maker': + type = 'limit' + postOnly = True + timeInForce = 'PO' + if type == 'ioc': + type = 'limit' + timeInForce = 'IOC' + priceString = self.safe_string(order, 'price') + if priceString == 'market price': + priceString = None + trailingActivationPrice = self.safe_number(order, 'activation_price') + return self.safe_order({ + 'id': id, + 'clientOrderId': self.safe_string_2(order, 'client_order_id', 'clientOrderId'), + 'info': order, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': self.safe_integer(order, 'update_time'), + 'symbol': symbol, + 'type': type, + 'timeInForce': timeInForce, + 'postOnly': postOnly, + 'side': self.parse_order_side(self.safe_string(order, 'side')), + 'price': self.omit_zero(priceString), + 'triggerPrice': trailingActivationPrice, + 'amount': self.omit_zero(self.safe_string(order, 'size')), + 'cost': self.safe_string_2(order, 'filled_notional', 'filledNotional'), + 'average': self.safe_string_n(order, ['price_avg', 'priceAvg', 'deal_avg_price']), + 'filled': self.safe_string_n(order, ['filled_size', 'filledSize', 'deal_size']), + 'remaining': None, + 'status': self.parse_order_status_by_type(orderType, self.safe_string_2(order, 'status', 'state')), + 'fee': None, + 'trades': None, + }, market) + + def parse_order_side(self, side): + sides: dict = { + '1': 'buy', + '2': 'buy', + '3': 'sell', + '4': 'sell', + } + return self.safe_string(sides, side, side) + + def parse_order_status_by_type(self, type, status): + statusesByType: dict = { + 'spot': { + '1': 'rejected', # Order failure + '2': 'open', # Placing order + '3': 'rejected', # Order failure, Freeze failure + '4': 'open', # Order success, Pending for fulfilment + '5': 'open', # Partially filled + '6': 'closed', # Fully filled + '7': 'canceled', # Canceling + '8': 'canceled', # Canceled + 'new': 'open', + 'partially_filled': 'open', + 'filled': 'closed', + 'partially_canceled': 'canceled', + }, + 'swap': { + '1': 'open', # Submitting + '2': 'open', # Commissioned + '4': 'closed', # Completed + }, + } + statuses = self.safe_dict(statusesByType, type, {}) + return self.safe_string(statuses, status, status) + + async def create_market_buy_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market buy order by providing the symbol and cost + + https://developer-pro.bitmart.com/en/spot/#new-order-v2-signed + + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' createMarketBuyOrderWithCost() supports spot orders only') + params['createMarketBuyOrderRequiresPrice'] = False + return await self.create_order(symbol, 'market', 'buy', cost, None, params) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://developer-pro.bitmart.com/en/spot/#new-order-v2-signed + https://developer-pro.bitmart.com/en/spot/#new-margin-order-v1-signed + https://developer-pro.bitmart.com/en/futuresv2/#submit-order-signed + https://developer-pro.bitmart.com/en/futuresv2/#submit-plan-order-signed + https://developer-pro.bitmart.com/en/futuresv2/#submit-tp-sl-order-signed + https://developer-pro.bitmart.com/en/futuresv2/#submit-trail-order-signed + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market', 'limit' or 'trailing' for swap markets only + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' + :param str [params.leverage]: *swap only* leverage level + :param str [params.clientOrderId]: client order id of the order + :param boolean [params.reduceOnly]: *swap only* reduce only + :param boolean [params.postOnly]: make sure the order is posted to the order book and not matched immediately + :param str [params.triggerPrice]: *swap only* the price to trigger a stop order + :param int [params.price_type]: *swap only* 1: last price, 2: fair price, default is 1 + :param int [params.price_way]: *swap only* 1: price way long, 2: price way short + :param int [params.activation_price_type]: *swap trailing order only* 1: last price, 2: fair price, default is 1 + :param str [params.trailingPercent]: *swap only* the percent to trail away from the current market price, min 0.1 max 5 + :param str [params.trailingTriggerPrice]: *swap only* the price to trigger a trailing order, default uses the price argument + :param str [params.stopLossPrice]: *swap only* the price to trigger a stop-loss order + :param str [params.takeProfitPrice]: *swap only* the price to trigger a take-profit order + :param int [params.plan_category]: *swap tp/sl only* 1: tp/sl, 2: position tp/sl, default is 1 + :param str [params.stpMode]: self-trade prevention only for spot, defaults to none, ['none', 'cancel_maker', 'cancel_taker', 'cancel_both'] + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + result = self.handle_margin_mode_and_params('createOrder', params) + marginMode = self.safe_string(result, 0) + triggerPrice = self.safe_string_n(params, ['triggerPrice', 'stopPrice', 'trigger_price']) + stopLossPrice = self.safe_string(params, 'stopLossPrice') + takeProfitPrice = self.safe_string(params, 'takeProfitPrice') + isStopLoss = stopLossPrice is not None + isTakeProfit = takeProfitPrice is not None + isTriggerOrder = triggerPrice is not None + response = None + if market['spot']: + spotRequest = self.create_spot_order_request(symbol, type, side, amount, price, params) + if marginMode == 'isolated': + response = await self.privatePostSpotV1MarginSubmitOrder(spotRequest) + else: + response = await self.privatePostSpotV2SubmitOrder(spotRequest) + else: + swapRequest = self.create_swap_order_request(symbol, type, side, amount, price, params) + activationPrice = self.safe_string(swapRequest, 'activation_price') + if activationPrice is not None: + # if type is trailing + response = await self.privatePostContractPrivateSubmitTrailOrder(swapRequest) + elif isTriggerOrder: + response = await self.privatePostContractPrivateSubmitPlanOrder(swapRequest) + elif isStopLoss or isTakeProfit: + response = await self.privatePostContractPrivateSubmitTpSlOrder(swapRequest) + else: + response = await self.privatePostContractPrivateSubmitOrder(swapRequest) + # + # spot and margin + # + # { + # "code": 1000, + # "trace":"886fb6ae-456b-4654-b4e0-d681ac05cea1", + # "message": "OK", + # "data": { + # "order_id": 2707217580 + # } + # } + # + # swap + # {"code":1000,"message":"Ok","data":{"order_id":231116359426639,"price":"market price"},"trace":"7f9c94e10f9d4513bc08a7bfc2a5559a.62.16996369620521911"} + # + data = self.safe_dict(response, 'data', {}) + order = self.parse_order(data, market) + order['type'] = type + order['side'] = side + order['amount'] = amount + order['price'] = price + return order + + async def create_orders(self, orders: List[OrderRequest], params={}): + """ + create a list of trade orders + + https://developer-pro.bitmart.com/en/spot/#new-batch-order-v4-signed + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.stpMode]: self-trade prevention only for spot, defaults to none, ['none', 'cancel_maker', 'cancel_taker', 'cancel_both'] + :returns dict: an `order structure ` + """ + await self.load_markets() + ordersRequests = [] + symbol = None + market = None + for i in range(0, len(orders)): + rawOrder = orders[i] + marketId = self.safe_string(rawOrder, 'symbol') + market = self.market(marketId) + if not market['spot']: + raise NotSupported(self.id + ' createOrders() supports spot orders only') + if symbol is None: + symbol = marketId + else: + if symbol != marketId: + raise BadRequest(self.id + ' createOrders() requires all orders to have the same symbol') + type = self.safe_string(rawOrder, 'type') + side = self.safe_string(rawOrder, 'side') + amount = self.safe_value(rawOrder, 'amount') + price = self.safe_value(rawOrder, 'price') + orderParams = self.safe_dict(rawOrder, 'params', {}) + orderRequest = self.create_spot_order_request(marketId, type, side, amount, price, orderParams) + orderRequest = self.omit(orderRequest, ['symbol']) # not needed because it goes in the outter object + ordersRequests.append(orderRequest) + request: dict = { + 'symbol': market['id'], + 'orderParams': ordersRequests, + } + response = await self.privatePostSpotV4BatchOrders(request) + # + # { + # "message": "OK", + # "code": 1000, + # "trace": "5fc697fb817a4b5396284786a9b2609a.263.17022620476480263", + # "data": { + # "code": 0, + # "msg": "success", + # "data": { + # "orderIds": [ + # "212751308355553320" + # ] + # } + # } + # } + # + data = self.safe_dict(response, 'data', {}) + innderData = self.safe_dict(data, 'data', {}) + orderIds = self.safe_list(innderData, 'orderIds', []) + parsedOrders = [] + for i in range(0, len(orderIds)): + orderId = orderIds[i] + order = self.safe_order({'id': orderId}, market) + parsedOrders.append(order) + return parsedOrders + + def create_swap_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + @ignore + create a trade order + https://developer-pro.bitmart.com/en/futuresv2/#submit-order-signed + https://developer-pro.bitmart.com/en/futuresv2/#submit-plan-order-signed + https://developer-pro.bitmart.com/en/futuresv2/#submit-tp-sl-order-signed + https://developer-pro.bitmart.com/en/futuresv2/#submit-trail-order-signed + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market', 'limit', 'trailing', 'stop_loss', or 'take_profit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.leverage]: leverage level + :param boolean [params.reduceOnly]: *swap only* reduce only + :param str [params.marginMode]: 'cross' or 'isolated', default is 'cross' + :param str [params.clientOrderId]: client order id of the order + :param str [params.triggerPrice]: *swap only* the price to trigger a stop order + :param int [params.price_type]: *swap only* 1: last price, 2: fair price, default is 1 + :param int [params.price_way]: *swap only* 1: price way long, 2: price way short + :param int [params.activation_price_type]: *swap trailing order only* 1: last price, 2: fair price, default is 1 + :param str [params.trailingPercent]: *swap only* the percent to trail away from the current market price, min 0.1 max 5 + :param str [params.trailingTriggerPrice]: *swap only* the price to trigger a trailing order, default uses the price argument + :param str [params.stopLossPrice]: *swap only* the price to trigger a stop-loss order + :param str [params.takeProfitPrice]: *swap only* the price to trigger a take-profit order + :param int [params.plan_category]: *swap tp/sl only* 1: tp/sl, 2: position tp/sl, default is 1 + :returns dict: an `order structure ` + """ + market = self.market(symbol) + stopLossPrice = self.safe_string(params, 'stopLossPrice') + takeProfitPrice = self.safe_string(params, 'takeProfitPrice') + isStopLoss = stopLossPrice is not None + isTakeProfit = takeProfitPrice is not None + if isStopLoss: + type = 'stop_loss' + elif isTakeProfit: + type = 'take_profit' + request: dict = { + 'symbol': market['id'], + 'size': int(self.amount_to_precision(symbol, amount)), + } + timeInForce = self.safe_string(params, 'timeInForce') + mode = self.safe_integer(params, 'mode') # only for swap + isMarketOrder = type == 'market' + postOnly = None + reduceOnly = self.safe_bool(params, 'reduceOnly') + isExchangeSpecificPo = (mode == 4) + postOnly, params = self.handle_post_only(isMarketOrder, isExchangeSpecificPo, params) + ioc = ((timeInForce == 'IOC') or (mode == 3)) + isLimitOrder = (type == 'limit') or postOnly or ioc + if timeInForce == 'GTC': + request['mode'] = 1 + elif timeInForce == 'FOK': + request['mode'] = 2 + elif timeInForce == 'IOC': + request['mode'] = 3 + if postOnly: + request['mode'] = 4 + triggerPrice = self.safe_string_n(params, ['triggerPrice', 'stopPrice', 'trigger_price']) + isTriggerOrder = triggerPrice is not None + trailingTriggerPrice = self.safe_string_2(params, 'trailingTriggerPrice', 'activation_price', self.number_to_string(price)) + trailingPercent = self.safe_string_2(params, 'trailingPercent', 'callback_rate') + isTrailingPercentOrder = trailingPercent is not None + if isLimitOrder: + request['price'] = self.price_to_precision(symbol, price) + elif type == 'trailing' or isTrailingPercentOrder: + type = 'trailing' + request['callback_rate'] = trailingPercent + request['activation_price'] = self.price_to_precision(symbol, trailingTriggerPrice) + request['activation_price_type'] = self.safe_integer(params, 'activation_price_type', 1) + if isTriggerOrder: + if isLimitOrder or price is not None: + request['executive_price'] = self.price_to_precision(symbol, price) + request['trigger_price'] = self.price_to_precision(symbol, triggerPrice) + request['price_type'] = self.safe_integer(params, 'price_type', 1) + if side == 'buy': + if reduceOnly: + request['price_way'] = 2 + else: + request['price_way'] = 1 + elif side == 'sell': + if reduceOnly: + request['price_way'] = 1 + else: + request['price_way'] = 2 + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('createOrder', params, 'cross') + if isStopLoss or isTakeProfit: + reduceOnly = True + request['price_type'] = self.safe_integer(params, 'price_type', 1) + request['executive_price'] = self.price_to_precision(symbol, price) + if isStopLoss: + request['trigger_price'] = self.price_to_precision(symbol, stopLossPrice) + else: + request['trigger_price'] = self.price_to_precision(symbol, takeProfitPrice) + else: + request['open_type'] = marginMode + if side == 'buy': + if reduceOnly: + request['side'] = 2 # buy close short + else: + request['side'] = 1 # buy open long + elif side == 'sell': + if reduceOnly: + request['side'] = 3 # sell close long + else: + request['side'] = 4 # sell open short + clientOrderId = self.safe_string(params, 'clientOrderId') + if clientOrderId is not None: + params = self.omit(params, 'clientOrderId') + request['client_order_id'] = clientOrderId + leverage = self.safe_integer(params, 'leverage') + params = self.omit(params, ['timeInForce', 'postOnly', 'reduceOnly', 'leverage', 'trailingTriggerPrice', 'trailingPercent', 'triggerPrice', 'stopPrice', 'stopLossPrice', 'takeProfitPrice']) + if leverage is not None: + request['leverage'] = self.number_to_string(leverage) + elif isTriggerOrder: + request['leverage'] = '1' # for plan orders leverage is required, if not available default to 1 + if type != 'trailing': + request['type'] = type + return self.extend(request, params) + + def create_spot_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + @ignore + create a spot order request + https://developer-pro.bitmart.com/en/spot/#new-order-v2-signed + https://developer-pro.bitmart.com/en/spot/#new-margin-order-v1-signed + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' + :returns dict: an `order structure ` + """ + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'side': side, + 'type': type, + } + timeInForce = self.safe_string(params, 'timeInForce') + if timeInForce == 'FOK': + raise InvalidOrder(self.id + ' createOrder() only accepts timeInForce parameter values of IOC or PO') + mode = self.safe_integer(params, 'mode') # only for swap + isMarketOrder = type == 'market' + postOnly = None + isExchangeSpecificPo = (type == 'limit_maker') or (mode == 4) + postOnly, params = self.handle_post_only(isMarketOrder, isExchangeSpecificPo, params) + params = self.omit(params, ['timeInForce', 'postOnly']) + ioc = ((timeInForce == 'IOC') or (type == 'ioc')) + isLimitOrder = (type == 'limit') or postOnly or ioc + # method = 'privatePostSpotV2SubmitOrder' + if isLimitOrder: + request['size'] = self.amount_to_precision(symbol, amount) + request['price'] = self.price_to_precision(symbol, price) + elif isMarketOrder: + # for market buy it requires the amount of quote currency to spend + if side == 'buy': + notional = self.safe_string_2(params, 'cost', 'notional') + params = self.omit(params, 'cost') + createMarketBuyOrderRequiresPrice = True + createMarketBuyOrderRequiresPrice, params = self.handle_option_and_params(params, 'createOrder', 'createMarketBuyOrderRequiresPrice', True) + if createMarketBuyOrderRequiresPrice: + if (price is None) and (notional is None): + raise InvalidOrder(self.id + ' createOrder() requires the price argument for market buy orders to calculate the total cost to spend(amount * price), alternatively set the createMarketBuyOrderRequiresPrice option or param to False and pass the cost to spend in the amount argument or in the "notional" extra parameter(the exchange-specific behaviour)') + else: + amountString = self.number_to_string(amount) + priceString = self.number_to_string(price) + notional = Precise.string_mul(amountString, priceString) + else: + notional = self.number_to_string(amount) if (notional is None) else notional + request['notional'] = self.decimal_to_precision(notional, TRUNCATE, market['precision']['price'], self.precisionMode) + elif side == 'sell': + request['size'] = self.amount_to_precision(symbol, amount) + if postOnly: + request['type'] = 'limit_maker' + if ioc: + request['type'] = 'ioc' + clientOrderId = self.safe_string(params, 'clientOrderId') + if clientOrderId is not None: + params = self.omit(params, 'clientOrderId') + request['client_order_id'] = clientOrderId + return self.extend(request, params) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://developer-pro.bitmart.com/en/futuresv2/#cancel-order-signed + https://developer-pro.bitmart.com/en/spot/#cancel-order-v3-signed + https://developer-pro.bitmart.com/en/futuresv2/#cancel-plan-order-signed + https://developer-pro.bitmart.com/en/futuresv2/#cancel-order-signed + https://developer-pro.bitmart.com/en/futuresv2/#cancel-trail-order-signed + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.clientOrderId]: *spot only* the client order id of the order to cancel + :param boolean [params.trigger]: *swap only* whether the order is a trigger order + :param boolean [params.trailing]: *swap only* whether the order is a stop order + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + clientOrderId = self.safe_string_2(params, 'clientOrderId', 'client_order_id') + if clientOrderId is not None: + request['client_order_id'] = clientOrderId + else: + request['order_id'] = str(id) + params = self.omit(params, ['clientOrderId']) + response = None + if market['spot']: + response = await self.privatePostSpotV3CancelOrder(self.extend(request, params)) + else: + trigger = self.safe_bool_2(params, 'stop', 'trigger') + trailing = self.safe_bool(params, 'trailing') + params = self.omit(params, ['stop', 'trigger']) + if trigger: + response = await self.privatePostContractPrivateCancelPlanOrder(self.extend(request, params)) + elif trailing: + response = await self.privatePostContractPrivateCancelTrailOrder(self.extend(request, params)) + else: + response = await self.privatePostContractPrivateCancelOrder(self.extend(request, params)) + # swap + # {"code":1000,"message":"Ok","trace":"7f9c94e10f9d4513bc08a7bfc2a5559a.55.16959817848001851"} + # + # spot + # + # { + # "code": 1000, + # "trace":"886fb6ae-456b-4654-b4e0-d681ac05cea1", + # "message": "OK", + # "data": { + # "result": True + # } + # } + # + # spot alternative + # + # { + # "code": 1000, + # "trace":"886fb6ae-456b-4654-b4e0-d681ac05cea1", + # "message": "OK", + # "data": True + # } + # + if market['swap']: + return self.safe_order({'info': response}) + data = self.safe_value(response, 'data') + if data is True: + return self.safe_order({'id': id}, market) + succeeded = self.safe_value(data, 'succeed') + if succeeded is not None: + id = self.safe_string(succeeded, 0) + if id is None: + raise InvalidOrder(self.id + ' cancelOrder() failed to cancel ' + symbol + ' order id ' + id) + else: + result = self.safe_value(data, 'result') + if not result: + raise InvalidOrder(self.id + ' cancelOrder() ' + symbol + ' order id ' + id + ' is filled or canceled') + order = self.safe_order({'id': id, 'symbol': market['symbol'], 'info': {}}, market) + return order + + async def cancel_orders(self, ids: List[str], symbol: Str = None, params={}) -> List[Order]: + """ + cancel multiple orders + + https://developer-pro.bitmart.com/en/spot/#cancel-batch-order-v4-signed + + :param str[] ids: order ids + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str[] [params.clientOrderIds]: client order ids + :returns dict: an list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrders() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' cancelOrders() does not support ' + market['type'] + ' orders, only spot orders are accepted') + clientOrderIds = self.safe_list(params, 'clientOrderIds') + params = self.omit(params, ['clientOrderIds']) + request: dict = { + 'symbol': market['id'], + } + if clientOrderIds is not None: + request['clientOrderIds'] = clientOrderIds + else: + request['orderIds'] = ids + response = await self.privatePostSpotV4CancelOrders(self.extend(request, params)) + # + # { + # "message": "OK", + # "code": 1000, + # "trace": "c4edbce860164203954f7c3c81d60fc6.309.17022669632770001", + # "data": { + # "successIds": [ + # "213055379155243012" + # ], + # "failIds": [], + # "totalCount": 1, + # "successCount": 1, + # "failedCount": 0 + # } + # } + # + data = self.safe_dict(response, 'data', {}) + allOrders = [] + successIds = self.safe_list(data, 'successIds', []) + for i in range(0, len(successIds)): + id = successIds[i] + allOrders.append(self.safe_order({'id': id, 'status': 'canceled'}, market)) + failIds = self.safe_list(data, 'failIds', []) + for i in range(0, len(failIds)): + id = failIds[i] + allOrders.append(self.safe_order({'id': id, 'status': 'failed'}, market)) + return allOrders + + async def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders in a market + + https://developer-pro.bitmart.com/en/spot/#cancel-all-order-v4-signed + https://developer-pro.bitmart.com/en/futuresv2/#cancel-all-orders-signed + + :param str symbol: unified market symbol of the market to cancel orders in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.side]: *spot only* 'buy' or 'sell' + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + response = None + type = None + type, params = self.handle_market_type_and_params('cancelAllOrders', market, params) + if type == 'spot': + response = await self.privatePostSpotV4CancelAll(self.extend(request, params)) + elif type == 'swap': + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelAllOrders() requires a symbol argument') + response = await self.privatePostContractPrivateCancelOrders(self.extend(request, params)) + # + # spot + # + # { + # "code": 1000, + # "trace":"886fb6ae-456b-4654-b4e0-d681ac05cea1", + # "message": "OK", + # "data": {} + # } + # + # swap + # + # { + # "code": 1000, + # "message": "Ok", + # "trace": "7f9c94e10f9d4513bc08a7bfc2a5559a.70.16954131323145323" + # } + # + return [self.safe_order({'info': response})] + + async def fetch_orders_by_status(self, status, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrdersByStatus() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' fetchOrdersByStatus() does not support ' + market['type'] + ' orders, only spot orders are accepted') + request: dict = { + 'symbol': market['id'], + 'offset': 1, # max offset * limit < 500 + 'N': 100, # max limit is 100 + } + if status == 'open': + request['status'] = 9 + elif status == 'closed': + request['status'] = 6 + elif status == 'canceled': + request['status'] = 8 + else: + request['status'] = status + response = await self.privateGetSpotV3Orders(self.extend(request, params)) + # + # spot + # + # { + # "message":"OK", + # "code":1000, + # "trace":"70e7d427-7436-4fb8-8cdd-97e1f5eadbe9", + # "data":{ + # "current_page":1, + # "orders":[ + # { + # "order_id":2147601241, + # "symbol":"BTC_USDT", + # "create_time":1591099963000, + # "side":"sell", + # "type":"limit", + # "price":"9000.00", + # "price_avg":"0.00", + # "size":"1.00000", + # "notional":"9000.00000000", + # "filled_notional":"0.00000000", + # "filled_size":"0.00000", + # "status":"4" + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + orders = self.safe_list(data, 'orders', []) + return self.parse_orders(orders, market, since, limit) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + + https://developer-pro.bitmart.com/en/spot/#current-open-orders-v4-signed + https://developer-pro.bitmart.com/en/futuresv2/#get-all-open-orders-keyed + https://developer-pro.bitmart.com/en/futuresv2/#get-all-current-plan-orders-keyed + + fetch all unfilled currently open orders + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.marginMode]: *spot* whether to fetch trades for margin orders or spot orders, defaults to spot orders(only isolated margin orders are supported) + :param int [params.until]: *spot* the latest time in ms to fetch orders for + :param str [params.type]: *swap* order type, 'limit' or 'market' + :param str [params.order_state]: *swap* the order state, 'all' or 'partially_filled', default is 'all' + :param str [params.orderType]: *swap only* 'limit', 'market', or 'trailing' + :param boolean [params.trailing]: *swap only* set to True if you want to fetch trailing orders + :param boolean [params.trigger]: *swap only* set to True if you want to fetch trigger orders + :param str [params.stpMode]: self-trade prevention only for spot, defaults to none, ['none', 'cancel_maker', 'cancel_taker', 'cancel_both'] + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + market = None + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + type = None + response = None + type, params = self.handle_market_type_and_params('fetchOpenOrders', market, params) + if type == 'spot': + if limit is not None: + request['limit'] = min(limit, 200) + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchOpenOrders', params) + if marginMode == 'isolated': + request['orderMode'] = 'iso_margin' + if since is not None: + request['startTime'] = since + until = self.safe_integer_2(params, 'until', 'endTime') + if until is not None: + params = self.omit(params, ['endTime']) + request['endTime'] = until + response = await self.privatePostSpotV4QueryOpenOrders(self.extend(request, params)) + elif type == 'swap': + if limit is not None: + request['limit'] = min(limit, 100) + isTrigger = self.safe_bool_2(params, 'stop', 'trigger') + params = self.omit(params, ['stop', 'trigger']) + if isTrigger: + response = await self.privateGetContractPrivateCurrentPlanOrder(self.extend(request, params)) + else: + trailing = self.safe_bool(params, 'trailing', False) + orderType = self.safe_string(params, 'orderType') + params = self.omit(params, ['orderType', 'trailing']) + if trailing: + orderType = 'trailing' + if orderType is not None: + request['type'] = orderType + response = await self.privateGetContractPrivateGetOpenOrders(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchOpenOrders() does not support ' + type + ' orders, only spot and swap orders are accepted') + # + # spot + # + # { + # "code": 1000, + # "message": "success", + # "data": [ + # { + # "orderId": "183299373022163211", + # "clientOrderId": "183299373022163211", + # "symbol": "BTC_USDT", + # "side": "buy", + # "orderMode": "spot", + # "type": "limit", + # "state": "new", + # "price": "25000.00", + # "priceAvg": "0.00", + # "size": "0.00020", + # "filledSize": "0.00000", + # "notional": "5.00000000", + # "filledNotional": "0.00000000", + # "createTime": 1695703703338, + # "updateTime": 1695703703359 + # } + # ], + # "trace": "15f11d48e3234c81a2e786cr2e7a38e6.71.16957022303515933" + # } + # + # swap + # + # { + # "code": 1000, + # "message": "Ok", + # "data": [ + # { + # "order_id": "230935812485489", + # "client_order_id": "", + # "price": "24000", + # "size": "1", + # "symbol": "BTCUSDT", + # "state": 2, + # "side": 1, + # "type": "limit", + # "leverage": "10", + # "open_type": "isolated", + # "deal_avg_price": "0", + # "deal_size": "0", + # "create_time": 1695702258629, + # "update_time": 1695702258642 + # } + # ], + # "trace": "7f9d94g10f9d4513bc08a7rfc3a5559a.71.16957022303515933" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_orders(data, market, since, limit) + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + + https://developer-pro.bitmart.com/en/spot/#account-orders-v4-signed + https://developer-pro.bitmart.com/en/futuresv2/#get-order-history-keyed + + fetches information on multiple closed orders made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest entry + :param str [params.marginMode]: *spot only* 'cross' or 'isolated', for margin trading + :param str [params.stpMode]: self-trade prevention only for spot, defaults to none, ['none', 'cancel_maker', 'cancel_taker', 'cancel_both'] + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + market = None + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + type = None + type, params = self.handle_market_type_and_params('fetchClosedOrders', market, params) + if type != 'spot': + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchClosedOrders() requires a symbol argument') + if since is not None: + startTimeKey = 'startTime' if (type == 'spot') else 'start_time' + request[startTimeKey] = since + endTimeKey = 'endTime' if (type == 'spot') else 'end_time' + until = self.safe_integer_2(params, 'until', endTimeKey) + if until is not None: + params = self.omit(params, ['until']) + request[endTimeKey] = until + response = None + if type == 'spot': + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchClosedOrders', params) + if marginMode == 'isolated': + request['orderMode'] = 'iso_margin' + response = await self.privatePostSpotV4QueryHistoryOrders(self.extend(request, params)) + else: + response = await self.privateGetContractPrivateOrderHistory(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + return self.parse_orders(data, market, since, limit) + + async def fetch_canceled_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches information on multiple canceled orders made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: timestamp in ms of the earliest order, default is None + :param int [limit]: max number of orders to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `order structures ` + """ + return await self.fetch_orders_by_status('canceled', symbol, since, limit, params) + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://developer-pro.bitmart.com/en/spot/#query-order-by-id-v4-signed + https://developer-pro.bitmart.com/en/spot/#query-order-by-clientorderid-v4-signed + https://developer-pro.bitmart.com/en/futuresv2/#get-order-detail-keyed + + :param str id: the id of the order + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.clientOrderId]: *spot* fetch the order by client order id instead of order id + :param str [params.orderType]: *swap only* 'limit', 'market', 'liquidate', 'bankruptcy', 'adl' or 'trailing' + :param boolean [params.trailing]: *swap only* set to True if you want to fetch a trailing order + :param str [params.stpMode]: self-trade prevention only for spot, defaults to none, ['none', 'cancel_maker', 'cancel_taker', 'cancel_both'] + :returns dict: An `order structure ` + """ + await self.load_markets() + request: dict = {} + type = None + market = None + response = None + if symbol is not None: + market = self.market(symbol) + type, params = self.handle_market_type_and_params('fetchOrder', market, params) + if type == 'spot': + clientOrderId = self.safe_string(params, 'clientOrderId') + if not clientOrderId: + request['orderId'] = id + if clientOrderId is not None: + response = await self.privatePostSpotV4QueryClientOrder(self.extend(request, params)) + else: + response = await self.privatePostSpotV4QueryOrder(self.extend(request, params)) + elif type == 'swap': + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrder() requires a symbol argument') + trailing = self.safe_bool(params, 'trailing', False) + orderType = self.safe_string(params, 'orderType') + params = self.omit(params, ['orderType', 'trailing']) + if trailing: + orderType = 'trailing' + if orderType is not None: + request['type'] = orderType + request['symbol'] = market['id'] + request['order_id'] = id + response = await self.privateGetContractPrivateOrder(self.extend(request, params)) + # + # spot + # + # { + # "code": 1000, + # "message": "success", + # "data": { + # "orderId": "183347420821295423", + # "clientOrderId": "183347420821295423", + # "symbol": "BTC_USDT", + # "side": "buy", + # "orderMode": "spot", + # "type": "limit", + # "state": "new", + # "price": "24000.00", + # "priceAvg": "0.00", + # "size": "0.00022", + # "filledSize": "0.00000", + # "notional": "5.28000000", + # "filledNotional": "0.00000000", + # "createTime": 1695783014734, + # "updateTime": 1695783014762 + # }, + # "trace": "ce3e6422c8b44d5fag855348a68693ed.63.14957831547451715" + # } + # + # swap + # + # { + # "code": 1000, + # "message": "Ok", + # "data": { + # "order_id": "230927283405028", + # "client_order_id": "", + # "price": "23000", + # "size": "1", + # "symbol": "BTCUSDT", + # "state": 2, + # "side": 1, + # "type": "limit", + # "leverage": "10", + # "open_type": "isolated", + # "deal_avg_price": "0", + # "deal_size": "0", + # "create_time": 1695783433600, + # "update_time": 1695783433613 + # }, + # "trace": "4cad855075664097af6ba5257c47605d.63.14957831547451715" + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data, market) + + async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://developer-pro.bitmart.com/en/spot/#deposit-address-keyed + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + await self.load_markets() + currency = self.currency(code) + network: Str = None + network, params = self.handle_network_code_and_params(params) + request: dict = { + 'currency': self.get_currency_id_from_code_and_network(code, network), + } + response = await self.privateGetAccountV1DepositAddress(self.extend(request, params)) + # + # { + # "message": "OK", + # "code": 1000, + # "trace": "0e6edd79-f77f-4251-abe5-83ba75d06c1a", + # "data": { + # currency: 'ETH', + # chain: 'Ethereum', + # address: '0x99B5EEc2C520f86F0F62F05820d28D05D36EccCf', + # address_memo: '' + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_deposit_address(data, currency) + + def parse_deposit_address(self, depositAddress, currency=None) -> DepositAddress: + # + # fetchDepositAddress + # { + # currency: 'ETH', + # chain: 'Ethereum', + # address: '0x99B5EEc2C520f86F0F62F05820d28D05D36EccCf', + # address_memo: '' + # } + # + # fetchWithdrawAddress + # { + # "currency": "ETH", + # "network": "ETH", + # "address": "0x1121", + # "memo": "12", + # "remark": "12", + # "addressType": 0, + # "verifyStatus": 0 + # } + # + currencyId = self.safe_string(depositAddress, 'currency') + network = self.safe_string_2(depositAddress, 'chain', 'network') + if currencyId.find('NFT') < 0: + parts = currencyId.split('-') + currencyId = self.safe_string(parts, 0) + secondPart = self.safe_string(parts, 1) + if secondPart is not None: + network = secondPart + address = self.safe_string(depositAddress, 'address') + currency = self.safe_currency(currencyId, currency) + self.check_address(address) + return { + 'info': depositAddress, + 'currency': self.safe_string(currency, 'code'), + 'network': self.network_id_to_code(network), + 'address': address, + 'tag': self.safe_string_2(depositAddress, 'address_memo', 'memo'), + } + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://developer-pro.bitmart.com/en/spot/#withdraw-signed + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.network]: the network name for self withdrawal + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.check_address(address) + await self.load_markets() + currency = self.currency(code) + network: Str = None + network, params = self.handle_network_code_and_params(params) + request: dict = { + 'currency': self.get_currency_id_from_code_and_network(code, network), + 'amount': amount, + 'destination': 'To Digital Address', # To Digital Address, To Binance, To OKEX + 'address': address, + } + if tag is not None: + request['address_memo'] = tag + response = await self.privatePostAccountV1WithdrawApply(self.extend(request, params)) + # + # { + # "code": 1000, + # "trace":"886fb6ae-456b-4654-b4e0-d681ac05cea1", + # "message": "OK", + # "data": { + # "withdraw_id": "121212" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + transaction = self.parse_transaction(data, currency) + return self.extend(transaction, { + 'code': code, + 'address': address, + 'tag': tag, + }) + + async def fetch_transactions_by_type(self, type, code: Str = None, since: Int = None, limit: Int = None, params={}): + await self.load_markets() + if limit is None: + limit = 1000 # max 1000 + request: dict = { + 'operation_type': type, # deposit or withdraw + 'N': limit, + } + currency = None + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + if since is not None: + request['startTime'] = since + until = self.safe_integer(params, 'until') + if until is not None: + params = self.omit(params, 'until') + request['endTime'] = until + response = await self.privateGetAccountV2DepositWithdrawHistory(self.extend(request, params)) + # + # { + # "message":"OK", + # "code":1000, + # "trace":"142bf92a-fc50-4689-92b6-590886f90b97", + # "data":{ + # "records":[ + # { + # "withdraw_id":"1679952", + # "deposit_id":"", + # "operation_type":"withdraw", + # "currency":"BMX", + # "apply_time":1588867374000, + # "arrival_amount":"59.000000000000", + # "fee":"1.000000000000", + # "status":0, + # "address":"0xe57b69a8776b37860407965B73cdFFBDFe668Bb5", + # "address_memo":"", + # "tx_id":"" + # }, + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + records = self.safe_list(data, 'records', []) + return self.parse_transactions(records, currency, since, limit) + + async def fetch_deposit(self, id: str, code: Str = None, params={}): + """ + fetch information on a deposit + + https://developer-pro.bitmart.com/en/spot/#get-a-deposit-or-withdraw-detail-keyed + + :param str id: deposit id + :param str code: not used by bitmart fetchDeposit() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + await self.load_markets() + request: dict = { + 'id': id, + } + response = await self.privateGetAccountV1DepositWithdrawDetail(self.extend(request, params)) + # + # { + # "message":"OK", + # "code":1000, + # "trace":"f7f74924-14da-42a6-b7f2-d3799dd9a612", + # "data":{ + # "record":{ + # "withdraw_id":"", + # "deposit_id":"1679952", + # "operation_type":"deposit", + # "currency":"BMX", + # "apply_time":1588867374000, + # "arrival_amount":"59.000000000000", + # "fee":"1.000000000000", + # "status":0, + # "address":"0xe57b69a8776b37860407965B73cdFFBDFe668Bb5", + # "address_memo":"", + # "tx_id":"" + # } + # } + # } + # + data = self.safe_dict(response, 'data', {}) + record = self.safe_dict(data, 'record', {}) + return self.parse_transaction(record) + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://developer-pro.bitmart.com/en/spot/#get-deposit-and-withdraw-history-keyed + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + return await self.fetch_transactions_by_type('deposit', code, since, limit, params) + + async def fetch_withdrawal(self, id: str, code: Str = None, params={}): + """ + fetch data on a currency withdrawal via the withdrawal id + + https://developer-pro.bitmart.com/en/spot/#get-a-deposit-or-withdraw-detail-keyed + + :param str id: withdrawal id + :param str code: not used by bitmart.fetchWithdrawal + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + await self.load_markets() + request: dict = { + 'id': id, + } + response = await self.privateGetAccountV1DepositWithdrawDetail(self.extend(request, params)) + # + # { + # "message":"OK", + # "code":1000, + # "trace":"f7f74924-14da-42a6-b7f2-d3799dd9a612", + # "data":{ + # "record":{ + # "withdraw_id":"1679952", + # "deposit_id":"", + # "operation_type":"withdraw", + # "currency":"BMX", + # "apply_time":1588867374000, + # "arrival_amount":"59.000000000000", + # "fee":"1.000000000000", + # "status":0, + # "address":"0xe57b69a8776b37860407965B73cdFFBDFe668Bb5", + # "address_memo":"", + # "tx_id":"" + # } + # } + # } + # + data = self.safe_dict(response, 'data', {}) + record = self.safe_dict(data, 'record', {}) + return self.parse_transaction(record) + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://developer-pro.bitmart.com/en/spot/#get-deposit-and-withdraw-history-keyed + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + return await self.fetch_transactions_by_type('withdraw', code, since, limit, params) + + def parse_transaction_status(self, status: Str): + statuses: dict = { + '0': 'pending', # Create + '1': 'pending', # Submitted, waiting for withdrawal + '2': 'pending', # Processing + '3': 'ok', # Success + '4': 'canceled', # Cancel + '5': 'failed', # Fail + } + return self.safe_string(statuses, status, status) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # withdraw + # + # { + # "withdraw_id": "121212" + # } + # + # fetchDeposits, fetchWithdrawals, fetchWithdrawal + # + # { + # "withdraw_id":"1679952", + # "deposit_id":"", + # "operation_type":"withdraw", + # "currency":"BMX", + # "apply_time":1588867374000, + # "arrival_amount":"59.000000000000", + # "fee":"1.000000000000", + # "status":0, + # "address":"0xe57b69a8776b37860407965B73cdFFBDFe668Bb5", + # "address_memo":"", + # "tx_id":"" + # } + # + id = None + withdrawId = self.safe_string(transaction, 'withdraw_id') + depositId = self.safe_string(transaction, 'deposit_id') + type = None + if (withdrawId is not None) and (withdrawId != ''): + type = 'withdraw' + id = withdrawId + elif (depositId is not None) and (depositId != ''): + type = 'deposit' + id = depositId + amount = self.safe_number(transaction, 'arrival_amount') + timestamp = self.safe_integer(transaction, 'apply_time') + currencyId = self.safe_string(transaction, 'currency') + networkId: Str = None + if currencyId is not None: + if currencyId.find('NFT') < 0: + parts = currencyId.split('-') + currencyId = self.safe_string(parts, 0) + networkId = self.safe_string(parts, 1) + code = self.safe_currency_code(currencyId, currency) + status = self.parse_transaction_status(self.safe_string(transaction, 'status')) + feeCost = self.safe_number(transaction, 'fee') + fee = None + if feeCost is not None: + fee = { + 'cost': feeCost, + 'currency': code, + } + txid = self.safe_string(transaction, 'tx_id') + address = self.safe_string(transaction, 'address') + tag = self.safe_string(transaction, 'address_memo') + return { + 'info': transaction, + 'id': id, + 'currency': code, + 'amount': amount, + 'network': self.network_id_to_code(networkId), + 'address': address, + 'addressFrom': None, + 'addressTo': None, + 'tag': tag, + 'tagFrom': None, + 'tagTo': None, + 'status': status, + 'type': type, + 'updated': None, + 'txid': txid, + 'internal': None, + 'comment': None, + 'timestamp': timestamp if (timestamp != 0) else None, + 'datetime': self.iso8601(timestamp) if (timestamp != 0) else None, + 'fee': fee, + } + + async def repay_isolated_margin(self, symbol: str, code: str, amount, params={}): + """ + repay borrowed margin and interest + + https://developer-pro.bitmart.com/en/spot/#margin-repay-isolated-signed + + :param str symbol: unified market symbol + :param str code: unified currency code of the currency to repay + :param str amount: the amount to repay + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin loan structure ` + """ + await self.load_markets() + market = self.market(symbol) + currency = self.currency(code) + request: dict = { + 'symbol': market['id'], + 'currency': currency['id'], + 'amount': self.currency_to_precision(code, amount), + } + response = await self.privatePostSpotV1MarginIsolatedRepay(self.extend(request, params)) + # + # { + # "message": "OK", + # "code": 1000, + # "trace": "b0a60b4c-e986-4b54-a190-8f7c05ddf685", + # "data": { + # "repay_id": "2afcc16d99bd4707818c5a355dc89bed" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + transaction = self.parse_margin_loan(data, currency) + return self.extend(transaction, { + 'amount': amount, + 'symbol': symbol, + }) + + async def borrow_isolated_margin(self, symbol: str, code: str, amount: float, params={}): + """ + create a loan to borrow margin + + https://developer-pro.bitmart.com/en/spot/#margin-borrow-isolated-signed + + :param str symbol: unified market symbol + :param str code: unified currency code of the currency to borrow + :param str amount: the amount to borrow + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin loan structure ` + """ + await self.load_markets() + market = self.market(symbol) + currency = self.currency(code) + request: dict = { + 'symbol': market['id'], + 'currency': currency['id'], + 'amount': self.currency_to_precision(code, amount), + } + response = await self.privatePostSpotV1MarginIsolatedBorrow(self.extend(request, params)) + # + # { + # "message": "OK", + # "code": 1000, + # "trace": "e6fda683-181e-4e78-ac9c-b27c4c8ba035", + # "data": { + # "borrow_id": "629a7177a4ed4cf09869c6a4343b788c" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + transaction = self.parse_margin_loan(data, currency) + return self.extend(transaction, { + 'amount': amount, + 'symbol': symbol, + }) + + def parse_margin_loan(self, info, currency: Currency = None): + # + # borrowMargin + # + # { + # "borrow_id": "629a7177a4ed4cf09869c6a4343b788c", + # } + # + # repayMargin + # + # { + # "repay_id": "2afcc16d99bd4707818c5a355dc89bed", + # } + # + return { + 'id': self.safe_string_2(info, 'borrow_id', 'repay_id'), + 'currency': self.safe_currency_code(None, currency), + 'amount': None, + 'symbol': None, + 'timestamp': None, + 'datetime': None, + 'info': info, + } + + async def fetch_isolated_borrow_rate(self, symbol: str, params={}) -> IsolatedBorrowRate: + """ + fetch the rate of interest to borrow a currency for margin trading + + https://developer-pro.bitmart.com/en/spot/#get-trading-pair-borrowing-rate-and-amount-keyed + + :param str symbol: unified symbol of the market to fetch the borrow rate for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `isolated borrow rate structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = await self.privateGetSpotV1MarginIsolatedPairs(self.extend(request, params)) + # + # { + # "message": "OK", + # "code": 1000, + # "trace": "0985a130-a5ae-4fc1-863f-4704e214f585", + # "data": { + # "symbols": [ + # { + # "symbol": "BTC_USDT", + # "max_leverage": "5", + # "symbol_enabled": True, + # "base": { + # "currency": "BTC", + # "daily_interest": "0.00055000", + # "hourly_interest": "0.00002291", + # "max_borrow_amount": "2.00000000", + # "min_borrow_amount": "0.00000001", + # "borrowable_amount": "0.00670810" + # }, + # "quote": { + # "currency": "USDT", + # "daily_interest": "0.00055000", + # "hourly_interest": "0.00002291", + # "max_borrow_amount": "50000.00000000", + # "min_borrow_amount": "0.00000001", + # "borrowable_amount": "135.12575038" + # } + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + symbols = self.safe_list(data, 'symbols', []) + borrowRate = self.safe_dict(symbols, 0, []) + return self.parse_isolated_borrow_rate(borrowRate, market) + + def parse_isolated_borrow_rate(self, info: dict, market: Market = None) -> IsolatedBorrowRate: + # + # { + # "symbol": "BTC_USDT", + # "max_leverage": "5", + # "symbol_enabled": True, + # "base": { + # "currency": "BTC", + # "daily_interest": "0.00055000", + # "hourly_interest": "0.00002291", + # "max_borrow_amount": "2.00000000", + # "min_borrow_amount": "0.00000001", + # "borrowable_amount": "0.00670810" + # }, + # "quote": { + # "currency": "USDT", + # "daily_interest": "0.00055000", + # "hourly_interest": "0.00002291", + # "max_borrow_amount": "50000.00000000", + # "min_borrow_amount": "0.00000001", + # "borrowable_amount": "135.12575038" + # } + # } + # + marketId = self.safe_string(info, 'symbol') + symbol = self.safe_symbol(marketId, market) + baseData = self.safe_dict(info, 'base', {}) + quoteData = self.safe_dict(info, 'quote', {}) + baseId = self.safe_string(baseData, 'currency') + quoteId = self.safe_string(quoteData, 'currency') + return { + 'symbol': symbol, + 'base': self.safe_currency_code(baseId), + 'baseRate': self.safe_number(baseData, 'hourly_interest'), + 'quote': self.safe_currency_code(quoteId), + 'quoteRate': self.safe_number(quoteData, 'hourly_interest'), + 'period': 3600000, # 1-Hour + 'timestamp': None, + 'datetime': None, + 'info': info, + } + + async def fetch_isolated_borrow_rates(self, params={}) -> IsolatedBorrowRates: + """ + fetch the borrow interest rates of all currencies, currently only works for isolated margin + + https://developer-pro.bitmart.com/en/spot/#get-trading-pair-borrowing-rate-and-amount-keyed + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `isolated borrow rate structures ` + """ + await self.load_markets() + response = await self.privateGetSpotV1MarginIsolatedPairs(params) + # + # { + # "message": "OK", + # "code": 1000, + # "trace": "0985a130-a5ae-4fc1-863f-4704e214f585", + # "data": { + # "symbols": [ + # { + # "symbol": "BTC_USDT", + # "max_leverage": "5", + # "symbol_enabled": True, + # "base": { + # "currency": "BTC", + # "daily_interest": "0.00055000", + # "hourly_interest": "0.00002291", + # "max_borrow_amount": "2.00000000", + # "min_borrow_amount": "0.00000001", + # "borrowable_amount": "0.00670810" + # }, + # "quote": { + # "currency": "USDT", + # "daily_interest": "0.00055000", + # "hourly_interest": "0.00002291", + # "max_borrow_amount": "50000.00000000", + # "min_borrow_amount": "0.00000001", + # "borrowable_amount": "135.12575038" + # } + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + symbols = self.safe_list(data, 'symbols', []) + return self.parse_isolated_borrow_rates(symbols) + + async def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account, currently only supports transfer between spot and margin + + https://developer-pro.bitmart.com/en/spot/#margin-asset-transfer-signed + https://developer-pro.bitmart.com/en/futuresv2/#transfer-signed + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account to transfer from + :param str toAccount: account to transfer to + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transfer structure ` + """ + await self.load_markets() + currency = self.currency(code) + amountToPrecision = self.currency_to_precision(code, amount) + request: dict = { + 'amount': amountToPrecision, + 'currency': currency['id'], + } + fromId = self.convert_type_to_account(fromAccount) + toId = self.convert_type_to_account(toAccount) + if fromAccount == 'spot': + if toAccount == 'margin': + request['side'] = 'in' + request['symbol'] = toId + elif toAccount == 'swap': + request['type'] = 'spot_to_contract' + elif toAccount == 'spot': + if fromAccount == 'margin': + request['side'] = 'out' + request['symbol'] = fromId + elif fromAccount == 'swap': + request['type'] = 'contract_to_spot' + else: + raise ArgumentsRequired(self.id + ' transfer() requires either fromAccount or toAccount to be spot') + response = None + if (fromAccount == 'margin') or (toAccount == 'margin'): + response = await self.privatePostSpotV1MarginIsolatedTransfer(self.extend(request, params)) + elif (fromAccount == 'swap') or (toAccount == 'swap'): + response = await self.privatePostAccountV1TransferContract(self.extend(request, params)) + # + # margin + # + # { + # "message": "OK", + # "code": 1000, + # "trace": "b26cecec-ef5a-47d9-9531-2bd3911d3d55", + # "data": { + # "transfer_id": "ca90d97a621e47d49774f19af6b029f5" + # } + # } + # + # swap + # + # { + # "message": "OK", + # "code": 1000, + # "trace": "4cad858074667097ac6ba5257c57305d.68.16953302431189455", + # "data": { + # "currency": "USDT", + # "amount": "5" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.extend(self.parse_transfer(data, currency), { + 'status': self.parse_transfer_status(self.safe_string_2(response, 'code', 'message')), + }) + + def parse_transfer_status(self, status: Str) -> Str: + statuses: dict = { + '1000': 'ok', + 'OK': 'ok', + 'FINISHED': 'ok', + } + return self.safe_string(statuses, status, status) + + def parse_transfer_to_account(self, type): + types: dict = { + 'contract_to_spot': 'spot', + 'spot_to_contract': 'swap', + } + return self.safe_string(types, type, type) + + def parse_transfer_from_account(self, type): + types: dict = { + 'contract_to_spot': 'swap', + 'spot_to_contract': 'spot', + } + return self.safe_string(types, type, type) + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + # + # margin + # + # { + # "transfer_id": "ca90d97a621e47d49774f19af6b029f5" + # } + # + # swap + # + # { + # "currency": "USDT", + # "amount": "5" + # } + # + # fetchTransfers + # + # { + # "transfer_id": "902463535961567232", + # "currency": "USDT", + # "amount": "5", + # "type": "contract_to_spot", + # "state": "FINISHED", + # "timestamp": 1695330539565 + # } + # + currencyId = self.safe_string(transfer, 'currency') + timestamp = self.safe_integer(transfer, 'timestamp') + return { + 'id': self.safe_string(transfer, 'transfer_id'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'currency': self.safe_currency_code(currencyId, currency), + 'amount': self.safe_number(transfer, 'amount'), + 'fromAccount': self.parse_transfer_from_account(self.safe_string(transfer, 'type')), + 'toAccount': self.parse_transfer_to_account(self.safe_string(transfer, 'type')), + 'status': self.parse_transfer_status(self.safe_string(transfer, 'state')), + } + + async def fetch_transfers(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[TransferEntry]: + """ + fetch a history of internal transfers made on an account, only transfers between spot and swap are supported + + https://developer-pro.bitmart.com/en/futuresv2/#get-transfer-list-signed + + :param str code: unified currency code of the currency transferred + :param int [since]: the earliest time in ms to fetch transfers for + :param int [limit]: the maximum number of transfer structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.page]: the required number of pages, default is 1, max is 1000 + :param int [params.until]: the latest time in ms to fetch transfers for + :returns dict[]: a list of `transfer structures ` + """ + await self.load_markets() + if limit is None: + limit = 10 + request: dict = { + 'page': self.safe_integer(params, 'page', 1), # default is 1, max is 1000 + 'limit': limit, # default is 10, max is 100 + } + currency = None + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + if since is not None: + request['time_start'] = since + if limit is not None: + request['limit'] = limit + until = self.safe_integer(params, 'until') # unified in milliseconds + endTime = self.safe_integer(params, 'time_end', until) # exchange-specific in milliseconds + params = self.omit(params, ['until']) + if endTime is not None: + request['time_end'] = endTime + response = await self.privatePostAccountV1TransferContractList(self.extend(request, params)) + # + # { + # "message": "OK", + # "code": 1000, + # "trace": "7f9d93e10f9g4513bc08a7btc2a5559a.69.16953325693032193", + # "data": { + # "records": [ + # { + # "transfer_id": "902463535961567232", + # "currency": "USDT", + # "amount": "5", + # "type": "contract_to_spot", + # "state": "FINISHED", + # "timestamp": 1695330539565 + # }, + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + records = self.safe_list(data, 'records', []) + return self.parse_transfers(records, currency, since, limit) + + async def fetch_borrow_interest(self, code: Str = None, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[BorrowInterest]: + """ + fetch the interest owed by the user for borrowing currency for margin trading + + https://developer-pro.bitmart.com/en/spot/#get-borrow-record-isolated-keyed + + :param str code: unified currency code + :param str symbol: unified market symbol when fetch interest in isolated markets + :param int [since]: the earliest time in ms to fetch borrrow interest for + :param int [limit]: the maximum number of structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `borrow interest structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchBorrowInterest() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['N'] = limit + if since is not None: + request['start_time'] = since + response = await self.privateGetSpotV1MarginIsolatedBorrowRecord(self.extend(request, params)) + # + # { + # "message": "OK", + # "code": 1000, + # "trace": "8ea27a2a-4aba-49fa-961d-43a0137b0ef3", + # "data": { + # "records": [ + # { + # "borrow_id": "1659045283903rNvJnuRTJNL5J53n", + # "symbol": "BTC_USDT", + # "currency": "USDT", + # "borrow_amount": "100.00000000", + # "daily_interest": "0.00055000", + # "hourly_interest": "0.00002291", + # "interest_amount": "0.00229166", + # "create_time": 1659045284000 + # }, + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + rows = self.safe_list(data, 'records', []) + interest = self.parse_borrow_interests(rows, market) + return self.filter_by_currency_since_limit(interest, code, since, limit) + + def parse_borrow_interest(self, info: dict, market: Market = None) -> BorrowInterest: + # + # { + # "borrow_id": "1657664327844Lk5eJJugXmdHHZoe", + # "symbol": "BTC_USDT", + # "currency": "USDT", + # "borrow_amount": "20.00000000", + # "daily_interest": "0.00055000", + # "hourly_interest": "0.00002291", + # "interest_amount": "0.00045833", + # "create_time": 1657664329000 + # } + # + marketId = self.safe_string(info, 'symbol') + market = self.safe_market(marketId, market) + timestamp = self.safe_integer(info, 'create_time') + return { + 'info': info, + 'symbol': self.safe_string(market, 'symbol'), + 'currency': self.safe_currency_code(self.safe_string(info, 'currency')), + 'interest': self.safe_number(info, 'interest_amount'), + 'interestRate': self.safe_number(info, 'hourly_interest'), + 'amountBorrowed': self.safe_number(info, 'borrow_amount'), + 'marginMode': 'isolated', + 'timestamp': timestamp, # borrow creation time + 'datetime': self.iso8601(timestamp), + } + + async def fetch_open_interest(self, symbol: str, params={}): + """ + Retrieves the open interest of a currency + + https://developer-pro.bitmart.com/en/futuresv2/#get-futures-openinterest + + :param str symbol: Unified CCXT market symbol + :param dict [params]: exchange specific parameters + :returns dict} an open interest structure{@link https://docs.ccxt.com/#/?id=open-interest-structure: + """ + await self.load_markets() + market = self.market(symbol) + if not market['contract']: + raise BadRequest(self.id + ' fetchOpenInterest() supports contract markets only') + request: dict = { + 'symbol': market['id'], + } + response = await self.publicGetContractPublicOpenInterest(self.extend(request, params)) + # + # { + # "code": 1000, + # "message": "Ok", + # "data": { + # "timestamp": 1694657502415, + # "symbol": "BTCUSDT", + # "open_interest": "265231.721368593081729069", + # "open_interest_value": "7006353.83988919" + # }, + # "trace": "7f9c94e10f9d4513bc08a7bfc2a5559a.72.16946575108274991" + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_open_interest(data, market) + + def parse_open_interest(self, interest, market: Market = None): + # + # { + # "timestamp": 1694657502415, + # "symbol": "BTCUSDT", + # "open_interest": "265231.721368593081729069", + # "open_interest_value": "7006353.83988919" + # } + # + timestamp = self.safe_integer(interest, 'timestamp') + id = self.safe_string(interest, 'symbol') + return self.safe_open_interest({ + 'symbol': self.safe_symbol(id, market), + 'openInterestAmount': self.safe_number(interest, 'open_interest'), + 'openInterestValue': self.safe_number(interest, 'open_interest_value'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'info': interest, + }, market) + + async def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://developer-pro.bitmart.com/en/futuresv2/#submit-leverage-signed + + :param float leverage: the rate of leverage + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'isolated' or 'cross' + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setLeverage() requires a symbol argument') + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('setLeverage', params) + self.check_required_argument('setLeverage', marginMode, 'marginMode', ['isolated', 'cross']) + await self.load_markets() + market = self.market(symbol) + if not market['swap']: + raise BadSymbol(self.id + ' setLeverage() supports swap contracts only') + request: dict = { + 'symbol': market['id'], + 'leverage': str(leverage), + 'open_type': marginMode, + } + return await self.privatePostContractPrivateSubmitLeverage(self.extend(request, params)) + + async def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate + + https://developer-pro.bitmart.com/en/futuresv2/#get-current-funding-rate + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + await self.load_markets() + market = self.market(symbol) + if not market['swap']: + raise BadSymbol(self.id + ' fetchFundingRate() supports swap contracts only') + request: dict = { + 'symbol': market['id'], + } + response = await self.publicGetContractPublicFundingRate(self.extend(request, params)) + # + # { + # "code": 1000, + # "message": "Ok", + # "data": { + # "symbol": "BTCUSDT", + # "expected_rate": "-0.0000238", + # "rate_value": "0.000009601106", + # "funding_time": 1761292800000, + # "funding_upper_limit": "0.0375", + # "funding_lower_limit": "-0.0375", + # "timestamp": 1761291544336 + # }, + # "trace": "64b7a589-e1e-4ac2-86b1-41058757421" + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_funding_rate(data, market) + + async def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical funding rate prices + + https://developer-pro.bitmart.com/en/futuresv2/#get-funding-rate-history + + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: not sent to exchange api, exchange api always returns the most recent data, only used to filter exchange response + :param int [limit]: the maximum amount of funding rate structures to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `funding rate structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchFundingRateHistory() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['limit'] = limit + response = await self.publicGetContractPublicFundingRateHistory(self.extend(request, params)) + # + # { + # "code": 1000, + # "message": "Ok", + # "data": { + # "list": [ + # { + # "symbol": "BTCUSDT", + # "funding_rate": "0.000091412174", + # "funding_time": "1734336000000" + # }, + # ] + # }, + # "trace": "fg73d949fgfdf6a40c8fc7f5ae6738.54.345345345345" + # } + # + data = self.safe_dict(response, 'data', {}) + result = self.safe_list(data, 'list', []) + rates = [] + for i in range(0, len(result)): + entry = result[i] + marketId = self.safe_string(entry, 'symbol') + symbolInner = self.safe_symbol(marketId, market, '-', 'swap') + timestamp = self.safe_integer(entry, 'funding_time') + rates.append({ + 'info': entry, + 'symbol': symbolInner, + 'fundingRate': self.safe_number(entry, 'funding_rate'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + sorted = self.sort_by(rates, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, market['symbol'], since, limit) + + def parse_funding_rate(self, contract, market: Market = None) -> FundingRate: + # + # { + # "symbol": "BTCUSDT", + # "expected_rate": "-0.0000238", + # "rate_value": "0.000009601106", + # "funding_time": 1761292800000, + # "funding_upper_limit": "0.0375", + # "funding_lower_limit": "-0.0375", + # "timestamp": 1761291544336 + # } + # + marketId = self.safe_string(contract, 'symbol') + timestamp = self.safe_integer(contract, 'timestamp') + fundingTimestamp = self.safe_integer(contract, 'funding_time') + return { + 'info': contract, + 'symbol': self.safe_symbol(marketId, market), + 'markPrice': None, + 'indexPrice': None, + 'interestRate': None, + 'estimatedSettlePrice': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'fundingRate': self.safe_number(contract, 'expected_rate'), + 'fundingTimestamp': fundingTimestamp, + 'fundingDatetime': self.iso8601(fundingTimestamp), + 'nextFundingRate': None, + 'nextFundingTimestamp': None, + 'nextFundingDatetime': None, + 'previousFundingRate': self.safe_number(contract, 'rate_value'), + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': None, + } + + async def fetch_position(self, symbol: str, params={}): + """ + fetch data on a single open contract trade position + + https://developer-pro.bitmart.com/en/futuresv2/#get-current-position-keyed + + :param str symbol: unified market symbol of the market the position is held in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `position structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = await self.privateGetContractPrivatePosition(self.extend(request, params)) + # + # { + # "code": 1000, + # "message": "Ok", + # "data": [ + # { + # "symbol": "BTCUSDT", + # "leverage": "10", + # "timestamp": 1696392515269, + # "current_fee": "0.0014250028", + # "open_timestamp": 1696392256998, + # "current_value": "27.4039", + # "mark_price": "27.4039", + # "position_value": "27.4079", + # "position_cross": "3.75723474", + # "maintenance_margin": "0.1370395", + # "close_vol": "0", + # "close_avg_price": "0", + # "open_avg_price": "27407.9", + # "entry_price": "27407.9", + # "current_amount": "1", + # "unrealized_value": "-0.004", + # "realized_value": "-0.01644474", + # "position_type": 1 + # } + # ], + # "trace":"4cad855074664097ac5ba5257c47305d.67.16963925142065945" + # } + # + data = self.safe_list(response, 'data', []) + first = self.safe_dict(data, 0, {}) + return self.parse_position(first, market) + + async def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open contract positions + + https://developer-pro.bitmart.com/en/futuresv2/#get-current-position-keyed + https://developer-pro.bitmart.com/en/futuresv2/#get-current-position-v2-keyed + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structures ` + """ + await self.load_markets() + market = None + symbolsLength = None + if symbols is not None: + symbolsLength = len(symbols) + first = self.safe_string(symbols, 0) + market = self.market(first) + request: dict = {} + if symbolsLength == 1: + # only supports symbols or sending one symbol + request['symbol'] = market['id'] + response = await self.privateGetContractPrivatePositionV2(self.extend(request, params)) + # + # { + # "code": 1000, + # "message": "Ok", + # "data": [ + # { + # "symbol": "BTCUSDT", + # "leverage": "10", + # "timestamp": 1696392515269, + # "current_fee": "0.0014250028", + # "open_timestamp": 1696392256998, + # "current_value": "27.4039", + # "mark_price": "27.4039", + # "position_value": "27.4079", + # "position_cross": "3.75723474", + # "maintenance_margin": "0.1370395", + # "close_vol": "0", + # "close_avg_price": "0", + # "open_avg_price": "27407.9", + # "entry_price": "27407.9", + # "current_amount": "1", + # "unrealized_value": "-0.004", + # "realized_value": "-0.01644474", + # "position_type": 1 + # }, + # ], + # "trace":"4cad855074664097ac5ba5257c47305d.67.16963925142065945" + # } + # + positions = self.safe_list(response, 'data', []) + result = [] + for i in range(0, len(positions)): + result.append(self.parse_position(positions[i])) + symbols = self.market_symbols(symbols) + return self.filter_by_array_positions(result, 'symbol', symbols, False) + + def parse_position(self, position: dict, market: Market = None): + # + # { + # "symbol": "BTCUSDT", + # "leverage": "10", + # "timestamp": 1696392515269, + # "current_fee": "0.0014250028", + # "open_timestamp": 1696392256998, + # "current_value": "27.4039", + # "mark_price": "27.4039", + # "position_value": "27.4079", + # "position_cross": "3.75723474", + # "maintenance_margin": "0.1370395", + # "close_vol": "0", + # "close_avg_price": "0", + # "open_avg_price": "27407.9", + # "entry_price": "27407.9", + # "current_amount": "1", + # "unrealized_value": "-0.004", + # "realized_value": "-0.01644474", + # "position_type": 1 + # } + # + marketId = self.safe_string(position, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + timestamp = self.safe_integer(position, 'timestamp') + side = self.safe_integer(position, 'position_type') + maintenanceMargin = self.safe_string(position, 'maintenance_margin') + notional = self.safe_string(position, 'current_value') + collateral = self.safe_string(position, 'position_cross') + maintenanceMarginPercentage = Precise.string_div(maintenanceMargin, notional) + marginRatio = Precise.string_div(maintenanceMargin, collateral) + return self.safe_position({ + 'info': position, + 'id': None, + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastUpdateTimestamp': None, + 'hedged': None, + 'side': 'long' if (side == 1) else 'short', + 'contracts': self.safe_number(position, 'current_amount'), + 'contractSize': self.safe_number(market, 'contractSize'), + 'entryPrice': self.safe_number(position, 'entry_price'), + 'markPrice': self.safe_number(position, 'mark_price'), + 'lastPrice': None, + 'notional': self.parse_number(notional), + 'leverage': self.safe_number(position, 'leverage'), + 'collateral': self.parse_number(collateral), + 'initialMargin': None, + 'initialMarginPercentage': None, + 'maintenanceMargin': self.parse_number(maintenanceMargin), + 'maintenanceMarginPercentage': self.parse_number(maintenanceMarginPercentage), + 'unrealizedPnl': self.safe_number(position, 'unrealized_value'), + 'realizedPnl': self.safe_number(position, 'realized_value'), + 'liquidationPrice': None, + 'marginMode': None, + 'percentage': None, + 'marginRatio': self.parse_number(marginRatio), + 'stopLossPrice': None, + 'takeProfitPrice': None, + }) + + async def fetch_my_liquidations(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + retrieves the users liquidated positions + + https://developer-pro.bitmart.com/en/futuresv2/#get-order-history-keyed + + :param str symbol: unified CCXT market symbol + :param int [since]: the earliest time in ms to fetch liquidations for + :param int [limit]: the maximum number of liquidation structures to retrieve + :param dict [params]: exchange specific parameters for the bitmart api endpoint + :param int [params.until]: timestamp in ms of the latest liquidation + :returns dict: an array of `liquidation structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMyLiquidations() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + if not market['swap']: + raise NotSupported(self.id + ' fetchMyLiquidations() supports swap markets only') + request: dict = { + 'symbol': market['id'], + } + if since is not None: + request['start_time'] = since + request, params = self.handle_until_option('end_time', request, params) + response = await self.privateGetContractPrivateOrderHistory(self.extend(request, params)) + # + # { + # "code": 1000, + # "message": "Ok", + # "data": [ + # { + # "order_id": "231007865458273", + # "client_order_id": "", + # "price": "27407.9", + # "size": "1", + # "symbol": "BTCUSDT", + # "state": 4, + # "side": 3, + # "type": "liquidate", + # "leverage": "10", + # "open_type": "isolated", + # "deal_avg_price": "27422.6", + # "deal_size": "1", + # "create_time": 1696405864011, + # "update_time": 1696405864045 + # }, + # ], + # "trace": "4cad855074664097ac6ba4257c47305d.71.16965658195443021" + # } + # + data = self.safe_list(response, 'data', []) + result = [] + for i in range(0, len(data)): + entry = data[i] + checkLiquidation = self.safe_string(entry, 'type') + if checkLiquidation == 'liquidate': + result.append(entry) + return self.parse_liquidations(result, market, since, limit) + + def parse_liquidation(self, liquidation, market: Market = None): + # + # { + # "order_id": "231007865458273", + # "client_order_id": "", + # "price": "27407.9", + # "size": "1", + # "symbol": "BTCUSDT", + # "state": 4, + # "side": 3, + # "type": "market", + # "leverage": "10", + # "open_type": "isolated", + # "deal_avg_price": "27422.6", + # "deal_size": "1", + # "create_time": 1696405864011, + # "update_time": 1696405864045 + # } + # + marketId = self.safe_string(liquidation, 'symbol') + timestamp = self.safe_integer(liquidation, 'update_time') + contractsString = self.safe_string(liquidation, 'deal_size') + contractSizeString = self.safe_string(market, 'contractSize') + priceString = self.safe_string(liquidation, 'deal_avg_price') + baseValueString = Precise.string_mul(contractsString, contractSizeString) + quoteValueString = Precise.string_mul(baseValueString, priceString) + return self.safe_liquidation({ + 'info': liquidation, + 'symbol': self.safe_symbol(marketId, market), + 'contracts': self.parse_number(contractsString), + 'contractSize': self.parse_number(contractSizeString), + 'price': self.parse_number(priceString), + 'baseValue': self.parse_number(baseValueString), + 'quoteValue': self.parse_number(quoteValueString), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + + async def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}) -> Order: + """ + edits an open order + + https://developer-pro.bitmart.com/en/futuresv2/#modify-plan-order-signed + https://developer-pro.bitmart.com/en/futuresv2/#modify-tp-sl-order-signed + https://developer-pro.bitmart.com/en/futuresv2/#modify-preset-plan-order-signed + https://developer-pro.bitmart.com/en/futuresv2/#modify-limit-order-signed + + :param str id: order id + :param str symbol: unified symbol of the market to edit an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float [amount]: how much you want to trade in units of the base currency + :param float [price]: the price to fulfill the order, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.triggerPrice]: *swap only* the price to trigger a stop order + :param str [params.stopLossPrice]: *swap only* the price to trigger a stop-loss order + :param str [params.takeProfitPrice]: *swap only* the price to trigger a take-profit order + :param str [params.stopLoss.triggerPrice]: *swap only* the price to trigger a preset stop-loss order + :param str [params.takeProfit.triggerPrice]: *swap only* the price to trigger a preset take-profit order + :param str [params.clientOrderId]: client order id of the order + :param int [params.price_type]: *swap only* 1: last price, 2: fair price, default is 1 + :param int [params.plan_category]: *swap tp/sl only* 1: tp/sl, 2: position tp/sl, default is 1 + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + if not market['swap']: + raise NotSupported(self.id + ' editOrder() does not support ' + market['type'] + ' markets, only swap markets are supported') + stopLossPrice = self.safe_string(params, 'stopLossPrice') + takeProfitPrice = self.safe_string(params, 'takeProfitPrice') + triggerPrice = self.safe_string_n(params, ['triggerPrice', 'stopPrice', 'trigger_price']) + stopLoss = self.safe_dict(params, 'stopLoss', {}) + takeProfit = self.safe_dict(params, 'takeProfit', {}) + presetStopLoss = self.safe_string(stopLoss, 'triggerPrice') + presetTakeProfit = self.safe_string(takeProfit, 'triggerPrice') + isTriggerOrder = triggerPrice is not None + isStopLoss = stopLossPrice is not None + isTakeProfit = takeProfitPrice is not None + isPresetStopLoss = presetStopLoss is not None + isPresetTakeProfit = presetTakeProfit is not None + isLimitOrder = (type == 'limit') + request: dict = { + 'symbol': market['id'], + } + clientOrderId = self.safe_string(params, 'clientOrderId') + if clientOrderId is not None: + params = self.omit(params, 'clientOrderId') + request['client_order_id'] = clientOrderId + if id is not None: + request['order_id'] = id + params = self.omit(params, ['triggerPrice', 'stopPrice', 'stopLossPrice', 'takeProfitPrice', 'stopLoss', 'takeProfit']) + response = None + if isTriggerOrder or isStopLoss or isTakeProfit: + request['price_type'] = self.safe_integer(params, 'price_type', 1) + if price is not None: + request['executive_price'] = self.price_to_precision(symbol, price) + if isTriggerOrder: + request['type'] = type + request['trigger_price'] = self.price_to_precision(symbol, triggerPrice) + response = await self.privatePostContractPrivateModifyPlanOrder(self.extend(request, params)) + # + # { + # "code": 1000, + # "message": "Ok", + # "data": { + # "order_id": "3000023150003503" + # }, + # "trace": "324523453245.108.1734567125596324575" + # } + # + elif isStopLoss or isTakeProfit: + request['category'] = type + if isStopLoss: + request['trigger_price'] = self.price_to_precision(symbol, stopLossPrice) + else: + request['trigger_price'] = self.price_to_precision(symbol, takeProfitPrice) + response = await self.privatePostContractPrivateModifyTpSlOrder(self.extend(request, params)) + # + # { + # "code": 1000, + # "message": "Ok", + # "data": { + # "order_id": "3000023150003480" + # }, + # "trace": "23452345.104.1724536582682345459" + # } + # + elif isPresetStopLoss or isPresetTakeProfit: + if isPresetStopLoss: + request['preset_stop_loss_price_type'] = self.safe_integer(params, 'price_type', 1) + request['preset_stop_loss_price'] = self.price_to_precision(symbol, presetStopLoss) + else: + request['preset_take_profit_price_type'] = self.safe_integer(params, 'price_type', 1) + request['preset_take_profit_price'] = self.price_to_precision(symbol, presetTakeProfit) + response = await self.privatePostContractPrivateModifyPresetPlanOrder(self.extend(request, params)) + # + # { + # "code": 1000, + # "message": "Ok", + # "data": { + # "order_id": "3000023150003496" + # }, + # "trace": "a5c3234534534a836bc476a203.123452.172716624359200197" + # } + # + elif isLimitOrder: + request['order_id'] = self.parse_to_int(id) # reparse id self endpoint is the only one requiring it + if amount is not None: + request['size'] = self.amount_to_precision(symbol, amount) + if price is not None: + request['price'] = self.price_to_precision(symbol, price) + response = await self.privatePostContractPrivateModifyLimitOrder(self.extend(request, params)) + else: + raise NotSupported(self.id + ' editOrder() only supports limit, trigger, stop loss and take profit orders') + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data, market) + + async def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + + https://developer-pro.bitmart.com/en/futuresv2/#get-transaction-history-keyed + + :param str [code]: unified currency code + :param int [since]: timestamp in ms of the earliest ledger entry + :param int [limit]: max number of ledger entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest ledger entry + :returns dict[]: a list of `ledger structures ` + """ + await self.load_markets() + currency = None + if code is not None: + currency = self.currency(code) + request: dict = {} + request, params = self.handle_until_option('end_time', request, params) + transactionsRequest = self.fetch_transactions_request(0, None, since, limit, params) + response = await self.privateGetContractPrivateTransactionHistory(transactionsRequest) + # + # { + # "code": 1000, + # "message": "Ok", + # "data": [ + # { + # "time": "1734422402121", + # "type": "Funding Fee", + # "amount": "-0.00008253", + # "asset": "USDT", + # "symbol": "LTCUSDT", + # "tran_id": "1734422402121", + # "flow_type": 3 + # }, + # ], + # "trace": "4cd11f83c71egfhfgh842790f07241e.23.173442343427772866" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_ledger(data, currency, since, limit) + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + # + # { + # "time": "1734422402121", + # "type": "Funding Fee", + # "amount": "-0.00008253", + # "asset": "USDT", + # "symbol": "LTCUSDT", + # "tran_id": "1734422402121", + # "flow_type": 3 + # } + # + amount = self.safe_string(item, 'amount') + direction = None + if Precise.string_le(amount, '0'): + direction = 'out' + amount = Precise.string_mul('-1', amount) + else: + direction = 'in' + currencyId = self.safe_string(item, 'asset') + currency = self.safe_currency(currencyId, currency) + timestamp = self.safe_integer(item, 'time') + type = self.safe_string(item, 'type') + return self.safe_ledger_entry({ + 'info': item, + 'id': self.safe_string(item, 'tran_id'), + 'direction': direction, + 'account': None, + 'referenceAccount': None, + 'referenceId': self.safe_string(item, 'tradeId'), + 'type': self.parse_ledger_entry_type(type), + 'currency': currency['code'], + 'amount': self.parse_number(amount), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'before': None, + 'after': None, + 'status': None, + 'fee': None, + }, currency) + + def parse_ledger_entry_type(self, type): + ledgerType: dict = { + 'Commission Fee': 'fee', + 'Funding Fee': 'fee', + 'Realized PNL': 'trade', + 'Transfer': 'transfer', + 'Liquidation Clearance': 'settlement', + } + return self.safe_string(ledgerType, type, type) + + def fetch_transactions_request(self, flowType: Int = None, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + request: dict = {} + if flowType is not None: + request['flow_type'] = flowType + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if since is not None: + request['start_time'] = since + if limit is not None: + request['page_size'] = limit + request, params = self.handle_until_option('end_time', request, params) + return self.extend(request, params) + + async def fetch_funding_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[FundingHistory]: + """ + fetch the history of funding payments paid and received on self account + + https://developer-pro.bitmart.com/en/futuresv2/#get-transaction-history-keyed + + :param str [symbol]: unified market symbol + :param int [since]: the starting timestamp in milliseconds + :param int [limit]: the number of entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch funding history for + :returns dict[]: a list of `funding history structures ` + """ + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + request: dict = {} + request, params = self.handle_until_option('end_time', request, params) + transactionsRequest = self.fetch_transactions_request(3, symbol, since, limit, params) + response = await self.privateGetContractPrivateTransactionHistory(transactionsRequest) + # + # { + # "code": 1000, + # "message": "Ok", + # "data": [ + # { + # "time": "1734422402121", + # "type": "Funding Fee", + # "amount": "-0.00008253", + # "asset": "USDT", + # "symbol": "LTCUSDT", + # "tran_id": "1734422402121", + # "flow_type": 3 + # }, + # ], + # "trace": "4cd11f83c71egfhfgh842790f07241e.23.173442343427772866" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_funding_histories(data, market, since, limit) + + def parse_funding_history(self, contract, market: Market = None): + # + # { + # "time": "1734422402121", + # "type": "Funding Fee", + # "amount": "-0.00008253", + # "asset": "USDT", + # "symbol": "LTCUSDT", + # "tran_id": "1734422402121", + # "flow_type": 3 + # } + # + marketId = self.safe_string(contract, 'symbol') + currencyId = self.safe_string(contract, 'asset') + timestamp = self.safe_integer(contract, 'time') + return { + 'info': contract, + 'symbol': self.safe_symbol(marketId, market, None, 'swap'), + 'code': self.safe_currency_code(currencyId), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': self.safe_string(contract, 'tran_id'), + 'amount': self.safe_number(contract, 'amount'), + } + + def parse_funding_histories(self, contracts, market=None, since: Int = None, limit: Int = None) -> List[FundingHistory]: + result = [] + for i in range(0, len(contracts)): + contract = contracts[i] + result.append(self.parse_funding_history(contract, market)) + sorted = self.sort_by(result, 'timestamp') + return self.filter_by_since_limit(sorted, since, limit) + + async def fetch_withdraw_addresses(self, code: str, note=None, networkCode=None, params={}): + await self.load_markets() + codes = None + if code is not None: + currency = self.currency(code) + code = currency['code'] + codes = [code] + response = await self.privateGetAccountV1WithdrawAddressList(params) + # + # { + # "message": "OK", + # "code": 1000, + # "trace": "0e6edd79-f77f-4251-abe5-83ba75d06c1a", + # "data": { + # "list": [ + # { + # "currency": "ETH", + # "network": "ETH", + # "address": "0x1121", + # "memo": "12", + # "remark": "12", + # "addressType": 0, + # "verifyStatus": 0 + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + list = self.safe_list(data, 'list', []) + allAddresses = self.parse_deposit_addresses(list, codes, False) + addresses = [] + for i in range(0, len(allAddresses)): + address = allAddresses[i] + noteMatch = (note is None) or (address['note'] == note) + networkMatch = (networkCode is None) or (address['network'] == networkCode) + if noteMatch and networkMatch: + addresses.append(address) + return addresses + + async def set_position_mode(self, hedged: bool, symbol: Str = None, params={}): + """ + set hedged to True or False for a market + + https://developer-pro.bitmart.com/en/futuresv2/#submit-leverage-signed + + :param bool hedged: set to True to use dualSidePosition + :param str symbol: not used by bingx setPositionMode() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + await self.load_markets() + positionMode = None + if hedged: + positionMode = 'hedge_mode' + else: + positionMode = 'one_way_mode' + request: dict = { + 'position_mode': positionMode, + } + # + # { + # "code": 1000, + # "trace": "0cc6f4c4-8b8c-4253-8e90-8d3195aa109c", + # "message": "Ok", + # "data": { + # "position_mode":"one_way_mode" + # } + # } + # + return await self.privatePostContractPrivateSetPositionMode(self.extend(request, params)) + + async def fetch_position_mode(self, symbol: Str = None, params={}): + """ + fetchs the position mode, hedged or one way, hedged for binance is set identically for all linear markets or all inverse markets + + https://developer-pro.bitmart.com/en/futuresv2/#get-position-mode-keyed + + :param str symbol: not used + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an object detailing whether the market is in hedged or one-way mode + """ + response = await self.privateGetContractPrivateGetPositionMode(params) + # + # { + # "code": 1000, + # "trace": "0cc6f4c4-8b8c-4253-8e90-8d3195aa109c", + # "message": "Ok", + # "data": { + # "position_mode":"one_way_mode" + # } + # } + # + data = self.safe_dict(response, 'data') + positionMode = self.safe_string(data, 'position_mode') + return { + 'info': response, + 'hedged': (positionMode == 'hedge_mode'), + } + + def nonce(self): + return self.milliseconds() - self.options['timeDifference'] + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + parts = path.split('/') + # to do: refactor api endpoints with spot/swap sections + category = self.safe_string(parts, 0, 'spot') + market = 'spot' if (category == 'spot' or category == 'account') else 'swap' + baseUrl = self.implode_hostname(self.urls['api'][market]) + url = baseUrl + '/' + self.implode_params(path, params) + query = self.omit(params, self.extract_params(path)) + queryString = '' + getOrDelete = (method == 'GET') or (method == 'DELETE') + if getOrDelete: + if query: + queryString = self.urlencode(query) + url += '?' + queryString + if api == 'private': + self.check_required_credentials() + timestamp = str(self.nonce()) + brokerId = self.safe_string(self.options, 'brokerId', 'CCXTxBitmart000') + headers = { + 'X-BM-KEY': self.apiKey, + 'X-BM-TIMESTAMP': timestamp, + 'X-BM-BROKER-ID': brokerId, + 'Content-Type': 'application/json', + } + if not getOrDelete: + body = self.json(query) + queryString = body + auth = timestamp + '#' + self.uid + '#' + queryString + signature = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256) + headers['X-BM-SIGN'] = signature + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None + # + # spot + # + # {"message":"Bad Request [to is empty]","code":50000,"trace":"f9d46e1b-4edb-4d07-a06e-4895fb2fc8fc","data":{}} + # {"message":"Bad Request [from is empty]","code":50000,"trace":"579986f7-c93a-4559-926b-06ba9fa79d76","data":{}} + # {"message":"Kline size over 500","code":50004,"trace":"d625caa8-e8ca-4bd2-b77c-958776965819","data":{}} + # {"message":"Balance not enough","code":50020,"trace":"7c709d6a-3292-462c-98c5-32362540aeef","data":{}} + # {"code":40012,"message":"You contract account available balance not enough.","trace":"..."} + # + # contract + # + # {"errno":"OK","message":"INVALID_PARAMETER","code":49998,"trace":"eb5ebb54-23cd-4de2-9064-e090b6c3b2e3","data":null} + # + message = self.safe_string_lower(response, 'message') + isErrorMessage = (message is not None) and (message != 'ok') and (message != 'success') + errorCode = self.safe_string(response, 'code') + isErrorCode = (errorCode is not None) and (errorCode != '1000') + if isErrorCode or isErrorMessage: + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], errorCode, feedback) + raise ExchangeError(feedback) # unknown message + return None diff --git a/ccxt/async_support/bitmex.py b/ccxt/async_support/bitmex.py new file mode 100644 index 0000000..e051f2f --- /dev/null +++ b/ccxt/async_support/bitmex.py @@ -0,0 +1,2941 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.bitmex import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currencies, Currency, DepositAddress, Int, LedgerEntry, Leverage, Leverages, Market, MarketType, Num, Order, OrderBook, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, FundingRates, Trade, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import DDoSProtection +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class bitmex(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(bitmex, self).describe(), { + 'id': 'bitmex', + 'name': 'BitMEX', + 'countries': ['SC'], # Seychelles + 'version': 'v1', + 'userAgent': None, + # cheapest endpoints are 10 requests per second(trading) + # 10 per second => rateLimit = 1000ms / 10 = 100ms + # 120 per minute => 2 per second => weight = 5(authenticated) + # 30 per minute => 0.5 per second => weight = 20(unauthenticated) + 'rateLimit': 100, + 'certified': True, + 'pro': True, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': True, + 'future': True, + 'option': False, + 'addMargin': None, + 'cancelAllOrders': True, + 'cancelAllOrdersAfter': True, + 'cancelOrder': True, + 'cancelOrders': True, + 'closeAllPositions': False, + 'closePosition': True, + 'createOrder': True, + 'createReduceOnlyOrder': True, + 'createStopOrder': True, + 'createTrailingAmountOrder': True, + 'createTriggerOrder': True, + 'editOrder': True, + 'fetchBalance': True, + 'fetchClosedOrders': True, + 'fetchCurrencies': True, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDepositsWithdrawals': 'emulated', + 'fetchDepositWithdrawFee': 'emulated', + 'fetchDepositWithdrawFees': True, + 'fetchFundingHistory': False, + 'fetchFundingRate': 'emulated', # emulated in exchange + 'fetchFundingRateHistory': True, + 'fetchFundingRates': True, + 'fetchIndexOHLCV': False, + 'fetchLedger': True, + 'fetchLeverage': 'emulated', + 'fetchLeverages': True, + 'fetchLeverageTiers': False, + 'fetchLiquidations': True, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMyLiquidations': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositions': True, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTrades': True, + 'fetchTransactions': 'emulated', + 'fetchTransfer': False, + 'fetchTransfers': False, + 'index': True, + 'reduceMargin': None, + 'sandbox': True, + 'setLeverage': True, + 'setMargin': None, + 'setMarginMode': True, + 'setPositionMode': False, + 'transfer': False, + 'withdraw': True, + }, + 'timeframes': { + '1m': '1m', + '5m': '5m', + '1h': '1h', + '1d': '1d', + }, + 'urls': { + 'test': { + 'public': 'https://testnet.bitmex.com', + 'private': 'https://testnet.bitmex.com', + }, + 'logo': 'https://github.com/user-attachments/assets/c78425ab-78d5-49d6-bd14-db7734798f04', + 'api': { + 'public': 'https://www.bitmex.com', + 'private': 'https://www.bitmex.com', + }, + 'www': 'https://www.bitmex.com', + 'doc': [ + 'https://www.bitmex.com/app/apiOverview', + 'https://github.com/BitMEX/api-connectors/tree/master/official-http', + ], + 'fees': 'https://www.bitmex.com/app/fees', + 'referral': { + 'url': 'https://www.bitmex.com/app/register/NZTR1q', + 'discount': 0.1, + }, + }, + 'api': { + 'public': { + 'get': { + 'announcement': 5, + 'announcement/urgent': 5, + 'chat': 5, + 'chat/channels': 5, + 'chat/connected': 5, + 'chat/pinned': 5, + 'funding': 5, + 'guild': 5, + 'instrument': 5, + 'instrument/active': 5, + 'instrument/activeAndIndices': 5, + 'instrument/activeIntervals': 5, + 'instrument/compositeIndex': 5, + 'instrument/indices': 5, + 'instrument/usdVolume': 5, + 'insurance': 5, + 'leaderboard': 5, + 'liquidation': 5, + 'orderBook/L2': 5, + 'porl/nonce': 5, + 'quote': 5, + 'quote/bucketed': 5, + 'schema': 5, + 'schema/websocketHelp': 5, + 'settlement': 5, + 'stats': 5, + 'stats/history': 5, + 'stats/historyUSD': 5, + 'trade': 5, + 'trade/bucketed': 5, + 'wallet/assets': 5, + 'wallet/networks': 5, + }, + }, + 'private': { + 'get': { + 'address': 5, + 'apiKey': 5, + 'execution': 5, + 'execution/tradeHistory': 5, + 'globalNotification': 5, + 'leaderboard/name': 5, + 'order': 5, + 'porl/snapshots': 5, + 'position': 5, + 'user': 5, + 'user/affiliateStatus': 5, + 'user/checkReferralCode': 5, + 'user/commission': 5, + 'user/csa': 5, + 'user/depositAddress': 5, + 'user/executionHistory': 5, + 'user/getWalletTransferAccounts': 5, + 'user/margin': 5, + 'user/quoteFillRatio': 5, + 'user/quoteValueRatio': 5, + 'user/staking': 5, + 'user/staking/instruments': 5, + 'user/staking/tiers': 5, + 'user/tradingVolume': 5, + 'user/unstakingRequests': 5, + 'user/wallet': 5, + 'user/walletHistory': 5, + 'user/walletSummary': 5, + 'userAffiliates': 5, + 'userEvent': 5, + }, + 'post': { + 'address': 5, + 'chat': 5, + 'guild': 5, + 'guild/archive': 5, + 'guild/join': 5, + 'guild/kick': 5, + 'guild/leave': 5, + 'guild/sharesTrades': 5, + 'order': 1, + 'order/cancelAllAfter': 5, + 'order/closePosition': 5, + 'position/isolate': 1, + 'position/leverage': 1, + 'position/riskLimit': 5, + 'position/transferMargin': 1, + 'user/addSubaccount': 5, + 'user/cancelWithdrawal': 5, + 'user/communicationToken': 5, + 'user/confirmEmail': 5, + 'user/confirmWithdrawal': 5, + 'user/logout': 5, + 'user/preferences': 5, + 'user/requestWithdrawal': 5, + 'user/unstakingRequests': 5, + 'user/updateSubaccount': 5, + 'user/walletTransfer': 5, + }, + 'put': { + 'guild': 5, + 'order': 1, + }, + 'delete': { + 'order': 1, + 'order/all': 1, + 'user/unstakingRequests': 5, + }, + }, + }, + 'exceptions': { + 'exact': { + 'Invalid API Key.': AuthenticationError, + 'This key is disabled.': PermissionDenied, + 'Access Denied': PermissionDenied, + 'Duplicate clOrdID': InvalidOrder, + 'orderQty is invalid': InvalidOrder, + 'Invalid price': InvalidOrder, + 'Invalid stopPx for ordType': InvalidOrder, + 'Account is restricted': PermissionDenied, # {"error":{"message":"Account is restricted","name":"HTTPError"}} + }, + 'broad': { + 'Signature not valid': AuthenticationError, + 'overloaded': ExchangeNotAvailable, + 'Account has insufficient Available Balance': InsufficientFunds, + 'Service unavailable': ExchangeNotAvailable, # {"error":{"message":"Service unavailable","name":"HTTPError"}} + 'Server Error': ExchangeError, # {"error":{"message":"Server Error","name":"HTTPError"}} + 'Unable to cancel order due to existing state': InvalidOrder, + 'We require all new traders to verify': PermissionDenied, # {"message":"We require all new traders to verify their identity before their first deposit. Please visit bitmex.com/verify to complete the process.","name":"HTTPError"} + }, + }, + 'precisionMode': TICK_SIZE, + 'options': { + # https://blog.bitmex.com/api_announcement/deprecation-of-api-nonce-header/ + # https://github.com/ccxt/ccxt/issues/4789 + 'api-expires': 5, # in seconds + 'fetchOHLCVOpenTimestamp': True, + 'oldPrecision': False, + 'networks': { + 'BTC': 'btc', + 'ERC20': 'eth', + 'BEP20': 'bsc', + 'TRC20': 'tron', + 'AVAXC': 'avax', + 'NEAR': 'near', + 'XTZ': 'xtz', + 'DOT': 'dot', + 'SOL': 'sol', + 'ADA': 'ada', + }, + }, + 'features': { + 'default': { + 'sandbox': True, + 'createOrder': { + 'marginMode': True, + 'triggerPrice': True, + 'triggerPriceType': { + 'last': True, + 'mark': True, + }, + 'triggerDirection': True, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': True, + 'marketBuyRequiresPrice': False, + 'marketBuyByCost': False, + # exchange-supported features + # 'selfTradePrevention': True, + # 'twap': False, + # 'iceberg': False, + # 'oco': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 500, + 'daysBack': None, + 'untilDays': 1000000, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 500, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': 500, + 'daysBack': None, + 'untilDays': 1000000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 500, + 'daysBack': None, + 'daysBackCanceled': None, + 'untilDays': 1000000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 10000, + }, + }, + 'spot': { + 'extends': 'default', + 'createOrder': { + 'triggerPriceType': { + 'index': False, + }, + }, + }, + 'derivatives': { + 'extends': 'default', + 'createOrder': { + 'triggerPriceType': { + 'index': True, + }, + }, + }, + 'swap': { + 'linear': { + 'extends': 'derivatives', + }, + 'inverse': { + 'extends': 'derivatives', + }, + }, + 'future': { + 'linear': { + 'extends': 'derivatives', + }, + 'inverse': { + 'extends': 'derivatives', + }, + }, + }, + 'commonCurrencies': { + 'USDt': 'USDT', + 'XBt': 'BTC', + 'XBT': 'BTC', + 'Gwei': 'ETH', + 'GWEI': 'ETH', + 'LAMP': 'SOL', + 'LAMp': 'SOL', + }, + }) + + async def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://www.bitmex.com/api/explorer/#not /Wallet/Wallet_getAssetsConfig + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = await self.publicGetWalletAssets(params) + # + # { + # "XBt": { + # "asset": "XBT", + # "currency": "XBt", + # "majorCurrency": "XBT", + # "name": "Bitcoin", + # "currencyType": "Crypto", + # "scale": "8", + # # "mediumPrecision": "8", + # # "shorterPrecision": "4", + # # "symbol": "₿", + # # "tickLog": "0", + # # "weight": "1", + # "enabled": True, + # "isMarginCurrency": True, + # "minDepositAmount": "10000", + # "minWithdrawalAmount": "1000", + # "maxWithdrawalAmount": "100000000000000", + # "networks": [ + # { + # "asset": "btc", + # "tokenAddress": "", + # "depositEnabled": True, + # "withdrawalEnabled": True, + # "withdrawalFee": "20000", + # "minFee": "20000", + # "maxFee": "10000000" + # } + # ] + # }, + # } + # + result: dict = {} + for i in range(0, len(response)): + currency = response[i] + asset = self.safe_string(currency, 'asset') + code = self.safe_currency_code(asset) + id = self.safe_string(currency, 'currency') + name = self.safe_string(currency, 'name') + chains = self.safe_value(currency, 'networks', []) + depositEnabled = False + withdrawEnabled = False + networks: dict = {} + scale = self.safe_string(currency, 'scale') + precisionString = self.parse_precision(scale) + precision = self.parse_number(precisionString) + for j in range(0, len(chains)): + chain = chains[j] + networkId = self.safe_string(chain, 'asset') + network = self.network_id_to_code(networkId) + withdrawalFeeRaw = self.safe_string(chain, 'withdrawalFee') + withdrawalFee = self.parse_number(Precise.string_mul(withdrawalFeeRaw, precisionString)) + isDepositEnabled = self.safe_bool(chain, 'depositEnabled', False) + isWithdrawEnabled = self.safe_bool(chain, 'withdrawalEnabled', False) + active = (isDepositEnabled and isWithdrawEnabled) + if isDepositEnabled: + depositEnabled = True + if isWithdrawEnabled: + withdrawEnabled = True + networks[network] = { + 'info': chain, + 'id': networkId, + 'network': network, + 'active': active, + 'deposit': isDepositEnabled, + 'withdraw': isWithdrawEnabled, + 'fee': withdrawalFee, + 'precision': None, + 'limits': { + 'withdraw': { + 'min': None, + 'max': None, + }, + 'deposit': { + 'min': None, + 'max': None, + }, + }, + } + currencyEnabled = self.safe_value(currency, 'enabled') + currencyActive = currencyEnabled or (depositEnabled or withdrawEnabled) + minWithdrawalString = self.safe_string(currency, 'minWithdrawalAmount') + minWithdrawal = self.parse_number(Precise.string_mul(minWithdrawalString, precisionString)) + maxWithdrawalString = self.safe_string(currency, 'maxWithdrawalAmount') + maxWithdrawal = self.parse_number(Precise.string_mul(maxWithdrawalString, precisionString)) + minDepositString = self.safe_string(currency, 'minDepositAmount') + minDeposit = self.parse_number(Precise.string_mul(minDepositString, precisionString)) + isCrypto = self.safe_string(currency, 'currencyType') == 'Crypto' + result[code] = { + 'id': id, + 'code': code, + 'info': currency, + 'name': name, + 'active': currencyActive, + 'deposit': depositEnabled, + 'withdraw': withdrawEnabled, + 'fee': None, + 'precision': precision, + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': minWithdrawal, + 'max': maxWithdrawal, + }, + 'deposit': { + 'min': minDeposit, + 'max': None, + }, + }, + 'networks': networks, + 'type': 'crypto' if isCrypto else 'other', + } + return result + + def convert_from_real_amount(self, code, amount): + currency = self.currency(code) + precision = self.safe_string(currency, 'precision') + amountString = self.number_to_string(amount) + finalAmount = Precise.string_div(amountString, precision) + return self.parse_number(finalAmount) + + def convert_to_real_amount(self, code: Str, amount: Str): + if code is None: + return amount + elif amount is None: + return None + currency = self.currency(code) + precision = self.safe_string(currency, 'precision') + return Precise.string_mul(amount, precision) + + def amount_to_precision(self, symbol, amount): + symbol = self.safe_symbol(symbol) + market = self.market(symbol) + oldPrecision = self.safe_value(self.options, 'oldPrecision') + if market['spot'] and not oldPrecision: + amount = self.convert_from_real_amount(market['base'], amount) + return super(bitmex, self).amount_to_precision(symbol, amount) + + def convert_from_raw_quantity(self, symbol, rawQuantity, currencySide='base'): + if self.safe_value(self.options, 'oldPrecision'): + return self.parse_number(rawQuantity) + symbol = self.safe_symbol(symbol) + marketExists = self.in_array(symbol, self.symbols) + if not marketExists: + return self.parse_number(rawQuantity) + market = self.market(symbol) + if market['spot']: + return self.parse_number(self.convert_to_real_amount(market[currencySide], rawQuantity)) + return self.parse_number(rawQuantity) + + def convert_from_raw_cost(self, symbol, rawQuantity): + return self.convert_from_raw_quantity(symbol, rawQuantity, 'quote') + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for bitmex + + https://www.bitmex.com/api/explorer/#not /Instrument/Instrument_getActive + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = await self.publicGetInstrumentActive(params) + # + # [ + # { + # "symbol": "LTCUSDT", + # "rootSymbol": "LTC", + # "state": "Open", + # "typ": "FFWCSX", + # "listing": "2021-11-10T04:00:00.000Z", + # "front": "2021-11-10T04:00:00.000Z", + # "expiry": null, + # "settle": null, + # "listedSettle": null, + # "relistInterval": null, + # "inverseLeg": "", + # "sellLeg": "", + # "buyLeg": "", + # "optionStrikePcnt": null, + # "optionStrikeRound": null, + # "optionStrikePrice": null, + # "optionMultiplier": null, + # "positionCurrency": "LTC", # can be empty for spot markets + # "underlying": "LTC", + # "quoteCurrency": "USDT", + # "underlyingSymbol": "LTCT=", # can be empty for spot markets + # "reference": "BMEX", + # "referenceSymbol": ".BLTCT", # can be empty for spot markets + # "calcInterval": null, + # "publishInterval": null, + # "publishTime": null, + # "maxOrderQty": 1000000000, + # "maxPrice": 1000000, + # "lotSize": 1000, + # "tickSize": 0.01, + # "multiplier": 100, + # "settlCurrency": "USDt", # can be empty for spot markets + # "underlyingToPositionMultiplier": 10000, + # "underlyingToSettleMultiplier": null, + # "quoteToSettleMultiplier": 1000000, + # "isQuanto": False, + # "isInverse": False, + # "initMargin": 0.03, + # "maintMargin": 0.015, + # "riskLimit": 1000000000000, # can be null for spot markets + # "riskStep": 1000000000000, # can be null for spot markets + # "limit": null, + # "capped": False, + # "taxed": True, + # "deleverage": True, + # "makerFee": -0.0001, + # "takerFee": 0.0005, + # "settlementFee": 0, + # "insuranceFee": 0, + # "fundingBaseSymbol": ".LTCBON8H", # can be empty for spot markets + # "fundingQuoteSymbol": ".USDTBON8H", # can be empty for spot markets + # "fundingPremiumSymbol": ".LTCUSDTPI8H", # can be empty for spot markets + # "fundingTimestamp": "2022-01-14T20:00:00.000Z", + # "fundingInterval": "2000-01-01T08:00:00.000Z", + # "fundingRate": 0.0001, + # "indicativeFundingRate": 0.0001, + # "rebalanceTimestamp": null, + # "rebalanceInterval": null, + # "openingTimestamp": "2022-01-14T17:00:00.000Z", + # "closingTimestamp": "2022-01-14T18:00:00.000Z", + # "sessionInterval": "2000-01-01T01:00:00.000Z", + # "prevClosePrice": 138.511, + # "limitDownPrice": null, + # "limitUpPrice": null, + # "bankruptLimitDownPrice": null, + # "bankruptLimitUpPrice": null, + # "prevTotalVolume": 12699024000, + # "totalVolume": 12702160000, + # "volume": 3136000, + # "volume24h": 114251000, + # "prevTotalTurnover": 232418052349000, + # "totalTurnover": 232463353260000, + # "turnover": 45300911000, + # "turnover24h": 1604331340000, + # "homeNotional24h": 11425.1, + # "foreignNotional24h": 1604331.3400000003, + # "prevPrice24h": 135.48, + # "vwap": 140.42165, + # "highPrice": 146.42, + # "lowPrice": 135.08, + # "lastPrice": 144.36, + # "lastPriceProtected": 144.36, + # "lastTickDirection": "MinusTick", + # "lastChangePcnt": 0.0655, + # "bidPrice": 143.75, + # "midPrice": 143.855, + # "askPrice": 143.96, + # "impactBidPrice": 143.75, + # "impactMidPrice": 143.855, + # "impactAskPrice": 143.96, + # "hasLiquidity": True, + # "openInterest": 38103000, + # "openValue": 547963053300, + # "fairMethod": "FundingRate", + # "fairBasisRate": 0.1095, + # "fairBasis": 0.004, + # "fairPrice": 143.811, + # "markMethod": "FairPrice", + # "markPrice": 143.811, + # "indicativeTaxRate": null, + # "indicativeSettlePrice": 143.807, + # "optionUnderlyingPrice": null, + # "settledPriceAdjustmentRate": null, + # "settledPrice": null, + # "timestamp": "2022-01-14T17:49:55.000Z" + # } + # ] + # + return self.parse_markets(response) + + def parse_market(self, market: dict) -> Market: + id = self.safe_string(market, 'symbol') + baseId = self.safe_string(market, 'underlying') + quoteId = self.safe_string(market, 'quoteCurrency') + settleId = self.safe_string(market, 'settlCurrency') + settle = self.safe_currency_code(settleId) + # 'positionCurrency' may be empty("", currently returns for ETHUSD) + # so let's take the settlCurrency first and then adjust if needed + typ = self.safe_string(market, 'typ') # type definitions at: https://www.bitmex.com/api/explorer/#not /Instrument/Instrument_get + type: MarketType + swap = False + spot = False + future = False + if typ == 'FFWCSX': + type = 'swap' + swap = True + elif typ == 'IFXXXP': + type = 'spot' + spot = True + elif typ == 'FFCCSX': + type = 'future' + future = True + elif typ == 'FFICSX': + # prediction markets(without any volume) + quoteId = baseId + baseId = self.safe_string(market, 'rootSymbol') + type = 'future' + future = True + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + contract = swap or future + contractSize = None + isInverse = self.safe_value(market, 'isInverse') # self is True when BASE and SETTLE are same, i.e. BTC/XXX:BTC + isQuanto = self.safe_value(market, 'isQuanto') # self is True when BASE and SETTLE are different, i.e. AXS/XXX:BTC + linear = (not isInverse and not isQuanto) if contract else None + status = self.safe_string(market, 'state') + active = status == 'Open' # Open, Settled, Unlisted + expiry = None + expiryDatetime = None + symbol = None + if spot: + symbol = base + '/' + quote + elif contract: + symbol = base + '/' + quote + ':' + settle + if linear: + multiplierString = self.safe_string_2(market, 'underlyingToPositionMultiplier', 'underlyingToSettleMultiplier') + contractSize = self.parse_number(Precise.string_div('1', multiplierString)) + else: + multiplierString = Precise.string_abs(self.safe_string(market, 'multiplier')) + contractSize = self.parse_number(multiplierString) + expiryDatetime = self.safe_string(market, 'expiry') + expiry = self.parse8601(expiryDatetime) + if expiry is not None: + symbol = symbol + '-' + self.yymmdd(expiry) + else: + # for index/exotic markets, default to id + symbol = id + positionId = self.safe_string_2(market, 'positionCurrency', 'underlying') + position = self.safe_currency_code(positionId) + positionIsQuote = (position == quote) + maxOrderQty = self.safe_number(market, 'maxOrderQty') + initMargin = self.safe_string(market, 'initMargin', '1') + maxLeverage = self.parse_number(Precise.string_div('1', initMargin)) + # subtype should be None for spot markets + if spot: + isInverse = None + isQuanto = None + linear = None + return { + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': type, + 'spot': spot, + 'margin': False, + 'swap': swap, + 'future': future, + 'option': False, + 'active': active, + 'contract': contract, + 'linear': linear, + 'inverse': isInverse, + 'quanto': isQuanto, + 'taker': self.safe_number(market, 'takerFee'), + 'maker': self.safe_number(market, 'makerFee'), + 'contractSize': contractSize, + 'expiry': expiry, + 'expiryDatetime': expiryDatetime, + 'strike': self.safe_number(market, 'optionStrikePrice'), + 'optionType': None, + 'precision': { + 'amount': self.safe_number(market, 'lotSize'), + 'price': self.safe_number(market, 'tickSize'), + }, + 'limits': { + 'leverage': { + 'min': self.parse_number('1') if contract else None, + 'max': maxLeverage if contract else None, + }, + 'amount': { + 'min': None, + 'max': None if positionIsQuote else maxOrderQty, + }, + 'price': { + 'min': None, + 'max': self.safe_number(market, 'maxPrice'), + }, + 'cost': { + 'min': None, + 'max': maxOrderQty if positionIsQuote else None, + }, + }, + 'created': None, # 'listing' field is buggy, e.g. 2200-02-01T00:00:00.000Z + 'info': market, + } + + def parse_balance(self, response) -> Balances: + # + # [ + # { + # "account":1455728, + # "currency":"XBt", + # "riskLimit":1000000000000, + # "prevState":"", + # "state":"", + # "action":"", + # "amount":263542, + # "pendingCredit":0, + # "pendingDebit":0, + # "confirmedDebit":0, + # "prevRealisedPnl":0, + # "prevUnrealisedPnl":0, + # "grossComm":0, + # "grossOpenCost":0, + # "grossOpenPremium":0, + # "grossExecCost":0, + # "grossMarkValue":0, + # "riskValue":0, + # "taxableMargin":0, + # "initMargin":0, + # "maintMargin":0, + # "sessionMargin":0, + # "targetExcessMargin":0, + # "varMargin":0, + # "realisedPnl":0, + # "unrealisedPnl":0, + # "indicativeTax":0, + # "unrealisedProfit":0, + # "syntheticMargin":null, + # "walletBalance":263542, + # "marginBalance":263542, + # "marginBalancePcnt":1, + # "marginLeverage":0, + # "marginUsedPcnt":0, + # "excessMargin":263542, + # "excessMarginPcnt":1, + # "availableMargin":263542, + # "withdrawableMargin":263542, + # "timestamp":"2020-08-03T12:01:01.246Z", + # "grossLastValue":0, + # "commission":null + # } + # ] + # + result: dict = {'info': response} + for i in range(0, len(response)): + balance = response[i] + currencyId = self.safe_string(balance, 'currency') + code = self.safe_currency_code(currencyId) + account = self.account() + free = self.safe_string(balance, 'availableMargin') + total = self.safe_string(balance, 'marginBalance') + account['free'] = self.convert_to_real_amount(code, free) + account['total'] = self.convert_to_real_amount(code, total) + result[code] = account + return self.safe_balance(result) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://www.bitmex.com/api/explorer/#not /User/User_getMargin + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + request: dict = { + 'currency': 'all', + } + response = await self.privateGetUserMargin(self.extend(request, params)) + # + # [ + # { + # "account":1455728, + # "currency":"XBt", + # "riskLimit":1000000000000, + # "prevState":"", + # "state":"", + # "action":"", + # "amount":263542, + # "pendingCredit":0, + # "pendingDebit":0, + # "confirmedDebit":0, + # "prevRealisedPnl":0, + # "prevUnrealisedPnl":0, + # "grossComm":0, + # "grossOpenCost":0, + # "grossOpenPremium":0, + # "grossExecCost":0, + # "grossMarkValue":0, + # "riskValue":0, + # "taxableMargin":0, + # "initMargin":0, + # "maintMargin":0, + # "sessionMargin":0, + # "targetExcessMargin":0, + # "varMargin":0, + # "realisedPnl":0, + # "unrealisedPnl":0, + # "indicativeTax":0, + # "unrealisedProfit":0, + # "syntheticMargin":null, + # "walletBalance":263542, + # "marginBalance":263542, + # "marginBalancePcnt":1, + # "marginLeverage":0, + # "marginUsedPcnt":0, + # "excessMargin":263542, + # "excessMarginPcnt":1, + # "availableMargin":263542, + # "withdrawableMargin":263542, + # "timestamp":"2020-08-03T12:01:01.246Z", + # "grossLastValue":0, + # "commission":null + # } + # ] + # + return self.parse_balance(response) + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://www.bitmex.com/api/explorer/#not /OrderBook/OrderBook_getL2 + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['depth'] = limit + response = await self.publicGetOrderBookL2(self.extend(request, params)) + result: dict = { + 'symbol': symbol, + 'bids': [], + 'asks': [], + 'timestamp': None, + 'datetime': None, + 'nonce': None, + } + for i in range(0, len(response)): + order = response[i] + side = 'asks' if (order['side'] == 'Sell') else 'bids' + amount = self.convert_from_raw_quantity(symbol, self.safe_string(order, 'size')) + price = self.safe_number(order, 'price') + # https://github.com/ccxt/ccxt/issues/4926 + # https://github.com/ccxt/ccxt/issues/4927 + # the exchange sometimes returns null price in the orderbook + if price is not None: + resultSide = result[side] + resultSide.append([price, amount]) + result['bids'] = self.sort_by(result['bids'], 0, True) + result['asks'] = self.sort_by(result['asks'], 0) + return result + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://www.bitmex.com/api/explorer/#not /Order/Order_getOrders + + :param str id: the order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + filter: dict = { + 'filter': { + 'orderID': id, + }, + } + response = await self.fetch_orders(symbol, None, None, self.deep_extend(filter, params)) + numResults = len(response) + if numResults == 1: + return response[0] + raise OrderNotFound(self.id + ': The order ' + id + ' not found.') + + async def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + + https://www.bitmex.com/api/explorer/#not /Order/Order_getOrders + + fetches information on multiple orders made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the earliest time in ms to fetch orders for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOrders', 'paginate') + if paginate: + return await self.fetch_paginated_call_dynamic('fetchOrders', symbol, since, limit, params, 100) + market = None + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if since is not None: + request['startTime'] = self.iso8601(since) + if limit is not None: + request['count'] = limit + until = self.safe_integer_2(params, 'until', 'endTime') + if until is not None: + params = self.omit(params, ['until']) + request['endTime'] = self.iso8601(until) + request = self.deep_extend(request, params) + # why the hassle? urlencode in python is kinda broken for nested dicts. + # E.g. self.urlencode({"filter": {"open": True}}) will return "filter={'open':+True}" + # Bitmex doesn't like that. Hence resorting to self hack. + if 'filter' in request: + request['filter'] = self.json(request['filter']) + response = await self.privateGetOrder(request) + return self.parse_orders(response, market, since, limit) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://www.bitmex.com/api/explorer/#not /Order/Order_getOrders + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + request: dict = { + 'filter': { + 'open': True, + }, + } + return await self.fetch_orders(symbol, since, limit, self.deep_extend(request, params)) + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://www.bitmex.com/api/explorer/#not /Order/Order_getOrders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + # Bitmex barfs if you set 'open': False in the filter... + orders = await self.fetch_orders(symbol, since, limit, params) + return self.filter_by_array(orders, 'status', ['closed', 'canceled'], False) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://www.bitmex.com/api/explorer/#not /Execution/Execution_getTradeHistory + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchMyTrades', 'paginate') + if paginate: + return await self.fetch_paginated_call_dynamic('fetchMyTrades', symbol, since, limit, params, 100) + market = None + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if since is not None: + request['startTime'] = self.iso8601(since) + if limit is not None: + request['count'] = min(500, limit) + until = self.safe_integer_2(params, 'until', 'endTime') + if until is not None: + params = self.omit(params, ['until']) + request['endTime'] = self.iso8601(until) + request = self.deep_extend(request, params) + # why the hassle? urlencode in python is kinda broken for nested dicts. + # E.g. self.urlencode({"filter": {"open": True}}) will return "filter={'open':+True}" + # Bitmex doesn't like that. Hence resorting to self hack. + if 'filter' in request: + request['filter'] = self.json(request['filter']) + response = await self.privateGetExecutionTradeHistory(request) + # + # [ + # { + # "execID": "string", + # "orderID": "string", + # "clOrdID": "string", + # "clOrdLinkID": "string", + # "account": 0, + # "symbol": "string", + # "side": "string", + # "lastQty": 0, + # "lastPx": 0, + # "underlyingLastPx": 0, + # "lastMkt": "string", + # "lastLiquidityInd": "string", + # "simpleOrderQty": 0, + # "orderQty": 0, + # "price": 0, + # "displayQty": 0, + # "stopPx": 0, + # "pegOffsetValue": 0, + # "pegPriceType": "string", + # "currency": "string", + # "settlCurrency": "string", + # "execType": "string", + # "ordType": "string", + # "timeInForce": "string", + # "execInst": "string", + # "contingencyType": "string", + # "exDestination": "string", + # "ordStatus": "string", + # "triggered": "string", + # "workingIndicator": True, + # "ordRejReason": "string", + # "simpleLeavesQty": 0, + # "leavesQty": 0, + # "simpleCumQty": 0, + # "cumQty": 0, + # "avgPx": 0, + # "commission": 0, + # "tradePublishIndicator": "string", + # "multiLegReportingType": "string", + # "text": "string", + # "trdMatchID": "string", + # "execCost": 0, + # "execComm": 0, + # "homeNotional": 0, + # "foreignNotional": 0, + # "transactTime": "2019-03-05T12:47:02.762Z", + # "timestamp": "2019-03-05T12:47:02.762Z" + # } + # ] + # + return self.parse_trades(response, market, since, limit) + + def parse_ledger_entry_type(self, type): + types: dict = { + 'Withdrawal': 'transaction', + 'RealisedPNL': 'margin', + 'UnrealisedPNL': 'margin', + 'Deposit': 'transaction', + 'Transfer': 'transfer', + 'AffiliatePayout': 'referral', + 'SpotTrade': 'trade', + } + return self.safe_string(types, type, type) + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + # + # { + # "transactID": "69573da3-7744-5467-3207-89fd6efe7a47", + # "account": 24321, + # "currency": "XBt", + # "transactType": "Withdrawal", # "AffiliatePayout", "Transfer", "Deposit", "RealisedPNL", ... + # "amount": -1000000, + # "fee": 300000, + # "transactStatus": "Completed", # "Canceled", ... + # "address": "1Ex4fkF4NhQaQdRWNoYpqiPbDBbq18Kdd9", + # "tx": "3BMEX91ZhhKoWtsH9QRb5dNXnmnGpiEetA", + # "text": "", + # "transactTime": "2017-03-21T20:05:14.388Z", + # "walletBalance": 0, # balance after + # "marginBalance": null, + # "timestamp": "2017-03-22T13:09:23.514Z" + # } + # + # ButMEX returns the unrealized pnl from the wallet history endpoint. + # The unrealized pnl transaction has an empty timestamp. + # It is not related to historical pnl it has status set to "Pending". + # Therefore it's not a part of the history at all. + # https://github.com/ccxt/ccxt/issues/6047 + # + # { + # "transactID":"00000000-0000-0000-0000-000000000000", + # "account":121210, + # "currency":"XBt", + # "transactType":"UnrealisedPNL", + # "amount":-5508, + # "fee":0, + # "transactStatus":"Pending", + # "address":"XBTUSD", + # "tx":"", + # "text":"", + # "transactTime":null, # ←---------------------------- null + # "walletBalance":139198767, + # "marginBalance":139193259, + # "timestamp":null # ←---------------------------- null + # } + # + id = self.safe_string(item, 'transactID') + account = self.safe_string(item, 'account') + referenceId = self.safe_string(item, 'tx') + referenceAccount = None + type = self.parse_ledger_entry_type(self.safe_string(item, 'transactType')) + currencyId = self.safe_string(item, 'currency') + code = self.safe_currency_code(currencyId, currency) + currency = self.safe_currency(currencyId, currency) + amountString = self.safe_string(item, 'amount') + amount = self.convert_to_real_amount(code, amountString) + timestamp = self.parse8601(self.safe_string(item, 'transactTime')) + if timestamp is None: + # https://github.com/ccxt/ccxt/issues/6047 + # set the timestamp to zero, 1970 Jan 1 00:00:00 + # for unrealized pnl and other transactions without a timestamp + timestamp = 0 # see comments above + fee = None + feeCost = self.safe_string(item, 'fee') + if feeCost is not None: + feeCost = self.convert_to_real_amount(code, feeCost) + fee = { + 'cost': self.parse_number(feeCost), + 'currency': code, + } + after = self.safe_string(item, 'walletBalance') + if after is not None: + after = self.convert_to_real_amount(code, after) + before = self.parse_number(Precise.string_sub(self.number_to_string(after), self.number_to_string(amount))) + direction = None + if Precise.string_lt(amountString, '0'): + direction = 'out' + amount = self.convert_to_real_amount(code, Precise.string_abs(amountString)) + else: + direction = 'in' + status = self.parse_transaction_status(self.safe_string(item, 'transactStatus')) + return self.safe_ledger_entry({ + 'info': item, + 'id': id, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'direction': direction, + 'account': account, + 'referenceId': referenceId, + 'referenceAccount': referenceAccount, + 'type': type, + 'currency': code, + 'amount': self.parse_number(amount), + 'before': before, + 'after': self.parse_number(after), + 'status': status, + 'fee': fee, + }, currency) + + async def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + + https://www.bitmex.com/api/explorer/#not /User/User_getWalletHistory + + :param str [code]: unified currency code, default is None + :param int [since]: timestamp in ms of the earliest ledger entry, default is None + :param int [limit]: max number of ledger entries to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ledger structure ` + """ + await self.load_markets() + request: dict = { + # 'start': 123, + } + # + # if since is not None: + # # date-based pagination not supported + # } + # + if limit is not None: + request['count'] = limit + currency = None + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + response = await self.privateGetUserWalletHistory(self.extend(request, params)) + # + # [ + # { + # "transactID": "69573da3-7744-5467-3207-89fd6efe7a47", + # "account": 24321, + # "currency": "XBt", + # "transactType": "Withdrawal", # "AffiliatePayout", "Transfer", "Deposit", "RealisedPNL", ... + # "amount": -1000000, + # "fee": 300000, + # "transactStatus": "Completed", # "Canceled", ... + # "address": "1Ex4fkF4NhQaQdRWNoYpqiPbDBbq18Kdd9", + # "tx": "3BMEX91ZhhKoWtsH9QRb5dNXnmnGpiEetA", + # "text": "", + # "transactTime": "2017-03-21T20:05:14.388Z", + # "walletBalance": 0, # balance after + # "marginBalance": null, + # "timestamp": "2017-03-22T13:09:23.514Z" + # } + # ] + # + return self.parse_ledger(response, currency, since, limit) + + async def fetch_deposits_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch history of deposits and withdrawals + + https://www.bitmex.com/api/explorer/#not /User/User_getWalletHistory + + :param str [code]: unified currency code for the currency of the deposit/withdrawals, default is None + :param int [since]: timestamp in ms of the earliest deposit/withdrawal, default is None + :param int [limit]: max number of deposit/withdrawals to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `transaction structure ` + """ + await self.load_markets() + request: dict = { + 'currency': 'all', + # 'start': 123, + } + # + # if since is not None: + # # date-based pagination not supported + # } + # + currency = None + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + if limit is not None: + request['count'] = limit + response = await self.privateGetUserWalletHistory(self.extend(request, params)) + transactions = self.filter_by_array(response, 'transactType', ['Withdrawal', 'Deposit'], False) + return self.parse_transactions(transactions, currency, since, limit) + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'Confirmed': 'pending', + 'Canceled': 'canceled', + 'Completed': 'ok', + 'Pending': 'pending', + } + return self.safe_string(statuses, status, status) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # { + # "transactID": "ffe699c2-95ee-4c13-91f9-0faf41daec25", + # "account": 123456, + # "currency": "XBt", + # "network":'', # "tron" for USDt, etc... + # "transactType": "Withdrawal", + # "amount": -100100000, + # "fee": 100000, + # "transactStatus": "Completed", + # "address": "385cR5DM96n1HvBDMzLHPYcw89fZAXULJP", + # "tx": "3BMEXabcdefghijklmnopqrstuvwxyz123", + # "text": '', + # "transactTime": "2019-01-02T01:00:00.000Z", + # "walletBalance": 99900000, # self field might be inexistent + # "marginBalance": None, # self field might be inexistent + # "timestamp": "2019-01-02T13:00:00.000Z" + # } + # + currencyId = self.safe_string(transaction, 'currency') + currency = self.safe_currency(currencyId, currency) + # For deposits, transactTime == timestamp + # For withdrawals, transactTime is submission, timestamp is processed + transactTime = self.parse8601(self.safe_string(transaction, 'transactTime')) + timestamp = self.parse8601(self.safe_string(transaction, 'timestamp')) + type = self.safe_string_lower(transaction, 'transactType') + # Deposits have no from address or to address, withdrawals have both + address = None + addressFrom = None + addressTo = None + if type == 'withdrawal': + address = self.safe_string(transaction, 'address') + addressFrom = self.safe_string(transaction, 'tx') + addressTo = address + elif type == 'deposit': + addressTo = self.safe_string(transaction, 'address') + addressFrom = self.safe_string(transaction, 'tx') + amountString = self.safe_string(transaction, 'amount') + amountStringAbs = Precise.string_abs(amountString) + amount = self.convert_to_real_amount(currency['code'], amountStringAbs) + feeCostString = self.safe_string(transaction, 'fee') + feeCost = self.convert_to_real_amount(currency['code'], feeCostString) + status = self.safe_string(transaction, 'transactStatus') + if status is not None: + status = self.parse_transaction_status(status) + return { + 'info': transaction, + 'id': self.safe_string(transaction, 'transactID'), + 'txid': self.safe_string(transaction, 'tx'), + 'type': type, + 'currency': currency['code'], + 'network': self.network_id_to_code(self.safe_string(transaction, 'network'), currency['code']), + 'amount': self.parse_number(amount), + 'status': status, + 'timestamp': transactTime, + 'datetime': self.iso8601(transactTime), + 'address': address, + 'addressFrom': addressFrom, + 'addressTo': addressTo, + 'tag': None, + 'tagFrom': None, + 'tagTo': None, + 'updated': timestamp, + 'internal': None, + 'comment': None, + 'fee': { + 'currency': currency['code'], + 'cost': self.parse_number(feeCost), + 'rate': None, + }, + } + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://www.bitmex.com/api/explorer/#not /Instrument/Instrument_get + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = await self.publicGetInstrument(self.extend(request, params)) + ticker = self.safe_value(response, 0) + if ticker is None: + raise BadSymbol(self.id + ' fetchTicker() symbol ' + symbol + ' not found') + return self.parse_ticker(ticker, market) + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://www.bitmex.com/api/explorer/#not /Instrument/Instrument_getActiveAndIndices + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + response = await self.publicGetInstrumentActiveAndIndices(params) + # same response "fetchMarkets" + result: dict = {} + for i in range(0, len(response)): + ticker = self.parse_ticker(response[i]) + symbol = self.safe_string(ticker, 'symbol') + if symbol is not None: + result[symbol] = ticker + return self.filter_by_array_tickers(result, 'symbol', symbols) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # see response sample under "fetchMarkets" because same endpoint is being used here + marketId = self.safe_string(ticker, 'symbol') + symbol = self.safe_symbol(marketId, market) + timestamp = self.parse8601(self.safe_string(ticker, 'timestamp')) + open = self.safe_string(ticker, 'prevPrice24h') + last = self.safe_string(ticker, 'lastPrice') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'highPrice'), + 'low': self.safe_string(ticker, 'lowPrice'), + 'bid': self.safe_string(ticker, 'bidPrice'), + 'bidVolume': None, + 'ask': self.safe_string(ticker, 'askPrice'), + 'askVolume': None, + 'vwap': self.safe_string(ticker, 'vwap'), + 'open': open, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': self.safe_string(ticker, 'homeNotional24h'), + 'quoteVolume': self.safe_string(ticker, 'foreignNotional24h'), + 'markPrice': self.safe_string(ticker, 'markPrice'), + 'info': ticker, + }, market) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # { + # "timestamp":"2015-09-25T13:38:00.000Z", + # "symbol":"XBTUSD", + # "open":237.45, + # "high":237.45, + # "low":237.45, + # "close":237.45, + # "trades":0, + # "volume":0, + # "vwap":null, + # "lastSize":null, + # "turnover":0, + # "homeNotional":0, + # "foreignNotional":0 + # } + # + marketId = self.safe_string(ohlcv, 'symbol') + market = self.safe_market(marketId, market) + volume = self.convert_from_raw_quantity(market['symbol'], self.safe_string(ohlcv, 'volume')) + return [ + self.parse8601(self.safe_string(ohlcv, 'timestamp')), + self.safe_number(ohlcv, 'open'), + self.safe_number(ohlcv, 'high'), + self.safe_number(ohlcv, 'low'), + self.safe_number(ohlcv, 'close'), + volume, + ] + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://www.bitmex.com/api/explorer/#not /Trade/Trade_getBucketed + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate') + if paginate: + return await self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params) + # send JSON key/value pairs, such as {"key": "value"} + # filter by individual fields and do advanced queries on timestamps + # filter: Dict = {'key': 'value'} + # send a bare series(e.g. XBU) to nearest expiring contract in that series + # you can also send a timeframe, e.g. XBU:monthly + # timeframes: daily, weekly, monthly, quarterly, and biquarterly + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'binSize': self.safe_string(self.timeframes, timeframe, timeframe), + 'partial': True, # True == include yet-incomplete current bins + # 'filter': filter, # filter by individual fields and do advanced queries + # 'columns': [], # will return all columns if omitted + # 'start': 0, # starting point for results(wtf?) + # 'reverse': False, # True == newest first + # 'endTime': '', # ending date filter for results + } + if limit is not None: + request['count'] = limit # default 100, max 500 + until = self.safe_integer(params, 'until') + if until is not None: + params = self.omit(params, ['until']) + request['endTime'] = self.iso8601(until) + duration = self.parse_timeframe(timeframe) * 1000 + fetchOHLCVOpenTimestamp = self.safe_bool(self.options, 'fetchOHLCVOpenTimestamp', True) + # if since is not set, they will return candles starting from 2017-01-01 + if since is not None: + timestamp = since + if fetchOHLCVOpenTimestamp: + timestamp = self.sum(timestamp, duration) + startTime = self.iso8601(timestamp) + request['startTime'] = startTime # starting date filter for results + else: + request['reverse'] = True + response = await self.publicGetTradeBucketed(self.extend(request, params)) + # + # [ + # {"timestamp":"2015-09-25T13:38:00.000Z","symbol":"XBTUSD","open":237.45,"high":237.45,"low":237.45,"close":237.45,"trades":0,"volume":0,"vwap":null,"lastSize":null,"turnover":0,"homeNotional":0,"foreignNotional":0}, + # {"timestamp":"2015-09-25T13:39:00.000Z","symbol":"XBTUSD","open":237.45,"high":237.45,"low":237.45,"close":237.45,"trades":0,"volume":0,"vwap":null,"lastSize":null,"turnover":0,"homeNotional":0,"foreignNotional":0}, + # {"timestamp":"2015-09-25T13:40:00.000Z","symbol":"XBTUSD","open":237.45,"high":237.45,"low":237.45,"close":237.45,"trades":0,"volume":0,"vwap":null,"lastSize":null,"turnover":0,"homeNotional":0,"foreignNotional":0} + # ] + # + result = self.parse_ohlcvs(response, market, timeframe, since, limit) + if fetchOHLCVOpenTimestamp: + # bitmex returns the candle's close timestamp - https://github.com/ccxt/ccxt/issues/4446 + # we can emulate the open timestamp by shifting all the timestamps one place + # so the previous close becomes the current open, and we drop the first candle + for i in range(0, len(result)): + result[i][0] = result[i][0] - duration + return result + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(public) + # + # { + # "timestamp": "2018-08-28T00:00:02.735Z", + # "symbol": "XBTUSD", + # "side": "Buy", + # "size": 2000, + # "price": 6906.5, + # "tickDirection": "PlusTick", + # "trdMatchID": "b9a42432-0a46-6a2f-5ecc-c32e9ca4baf8", + # "grossValue": 28958000, + # "homeNotional": 0.28958, + # "foreignNotional": 2000 + # } + # + # fetchMyTrades(private) + # + # { + # "execID": "string", + # "orderID": "string", + # "clOrdID": "string", + # "clOrdLinkID": "string", + # "account": 0, + # "symbol": "string", + # "side": "string", + # "lastQty": 0, + # "lastPx": 0, + # "underlyingLastPx": 0, + # "lastMkt": "string", + # "lastLiquidityInd": "string", + # "simpleOrderQty": 0, + # "orderQty": 0, + # "price": 0, + # "displayQty": 0, + # "stopPx": 0, + # "pegOffsetValue": 0, + # "pegPriceType": "string", + # "currency": "string", + # "settlCurrency": "string", + # "execType": "string", + # "ordType": "string", + # "timeInForce": "string", + # "execInst": "string", + # "contingencyType": "string", + # "exDestination": "string", + # "ordStatus": "string", + # "triggered": "string", + # "workingIndicator": True, + # "ordRejReason": "string", + # "simpleLeavesQty": 0, + # "leavesQty": 0, + # "simpleCumQty": 0, + # "cumQty": 0, + # "avgPx": 0, + # "commission": 0, + # "tradePublishIndicator": "string", + # "multiLegReportingType": "string", + # "text": "string", + # "trdMatchID": "string", + # "execCost": 0, + # "execComm": 0, + # "homeNotional": 0, + # "foreignNotional": 0, + # "transactTime": "2019-03-05T12:47:02.762Z", + # "timestamp": "2019-03-05T12:47:02.762Z" + # } + # + marketId = self.safe_string(trade, 'symbol') + symbol = self.safe_symbol(marketId, market) + timestamp = self.parse8601(self.safe_string(trade, 'timestamp')) + priceString = self.safe_string_2(trade, 'avgPx', 'price') + amountString = self.convert_from_raw_quantity(symbol, self.safe_string_2(trade, 'size', 'lastQty')) + execCost = self.number_to_string(self.convert_from_raw_cost(symbol, self.safe_string(trade, 'execCost'))) + id = self.safe_string(trade, 'trdMatchID') + order = self.safe_string(trade, 'orderID') + side = self.safe_string_lower(trade, 'side') + # price * amount doesn't work for all symbols(e.g. XBT, ETH) + fee = None + feeCostString = self.number_to_string(self.convert_from_raw_cost(symbol, self.safe_string(trade, 'execComm'))) + if feeCostString is not None: + currencyId = self.safe_string_2(trade, 'settlCurrency', 'currency') + fee = { + 'cost': feeCostString, + 'currency': self.safe_currency_code(currencyId), + 'rate': self.safe_string(trade, 'commission'), + } + # Trade or Funding + execType = self.safe_string(trade, 'execType') + takerOrMaker = None + if feeCostString is not None and execType == 'Trade': + takerOrMaker = 'maker' if Precise.string_lt(feeCostString, '0') else 'taker' + type = self.safe_string_lower(trade, 'ordType') + return self.safe_trade({ + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'id': id, + 'order': order, + 'type': type, + 'takerOrMaker': takerOrMaker, + 'side': side, + 'price': priceString, + 'cost': Precise.string_abs(execCost), + 'amount': amountString, + 'fee': fee, + }, market) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'New': 'open', + 'PartiallyFilled': 'open', + 'Filled': 'closed', + 'DoneForDay': 'open', + 'Canceled': 'canceled', + 'PendingCancel': 'open', + 'PendingNew': 'open', + 'Rejected': 'rejected', + 'Expired': 'expired', + 'Stopped': 'open', + 'Untriggered': 'open', + 'Triggered': 'open', + } + return self.safe_string(statuses, status, status) + + def parse_time_in_force(self, timeInForce: Str): + timeInForces: dict = { + 'Day': 'Day', + 'GoodTillCancel': 'GTC', + 'ImmediateOrCancel': 'IOC', + 'FillOrKill': 'FOK', + } + return self.safe_string(timeInForces, timeInForce, timeInForce) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # { + # "orderID":"56222c7a-9956-413a-82cf-99f4812c214b", + # "clOrdID":"", + # "clOrdLinkID":"", + # "account":1455728, + # "symbol":"XBTUSD", + # "side":"Sell", + # "simpleOrderQty":null, + # "orderQty":1, + # "price":40000, + # "displayQty":null, + # "stopPx":null, + # "pegOffsetValue":null, + # "pegPriceType":"", + # "currency":"USD", + # "settlCurrency":"XBt", + # "ordType":"Limit", + # "timeInForce":"GoodTillCancel", + # "execInst":"", + # "contingencyType":"", + # "exDestination":"XBME", + # "ordStatus":"New", + # "triggered":"", + # "workingIndicator":true, + # "ordRejReason":"", + # "simpleLeavesQty":null, + # "leavesQty":1, + # "simpleCumQty":null, + # "cumQty":0, + # "avgPx":null, + # "multiLegReportingType":"SingleSecurity", + # "text":"Submitted via API.", + # "transactTime":"2021-01-02T21:38:49.246Z", + # "timestamp":"2021-01-02T21:38:49.246Z" + # } + # + marketId = self.safe_string(order, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + qty = self.safe_string(order, 'orderQty') + cost = None + amount = None + isInverse = False + if marketId is None: + defaultSubType = self.safe_string(self.options, 'defaultSubType', 'linear') + isInverse = (defaultSubType == 'inverse') + else: + isInverse = self.safe_bool(market, 'inverse', False) + if isInverse: + cost = self.convert_from_raw_quantity(symbol, qty) + else: + amount = self.convert_from_raw_quantity(symbol, qty) + average = self.safe_string(order, 'avgPx') + filled = None + cumQty = self.number_to_string(self.convert_from_raw_quantity(symbol, self.safe_string(order, 'cumQty'))) + if isInverse: + filled = Precise.string_div(cumQty, average) + else: + filled = cumQty + execInst = self.safe_string(order, 'execInst') + postOnly = None + if execInst is not None: + postOnly = (execInst == 'ParticipateDoNotInitiate') + timestamp = self.parse8601(self.safe_string(order, 'timestamp')) + triggerPrice = self.safe_number(order, 'stopPx') + remaining = self.safe_string(order, 'leavesQty') + return self.safe_order({ + 'info': order, + 'id': self.safe_string(order, 'orderID'), + 'clientOrderId': self.safe_string(order, 'clOrdID'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': self.parse8601(self.safe_string(order, 'transactTime')), + 'symbol': symbol, + 'type': self.safe_string_lower(order, 'ordType'), + 'timeInForce': self.parse_time_in_force(self.safe_string(order, 'timeInForce')), + 'postOnly': postOnly, + 'side': self.safe_string_lower(order, 'side'), + 'price': self.safe_string(order, 'price'), + 'triggerPrice': triggerPrice, + 'amount': amount, + 'cost': cost, + 'average': average, + 'filled': filled, + 'remaining': self.convert_from_raw_quantity(symbol, remaining), + 'status': self.parse_order_status(self.safe_string(order, 'ordStatus')), + 'fee': None, + 'trades': None, + }, market) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://www.bitmex.com/api/explorer/#not /Trade/Trade_get + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchTrades', 'paginate') + if paginate: + return await self.fetch_paginated_call_dynamic('fetchTrades', symbol, since, limit, params) + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if since is not None: + request['startTime'] = self.iso8601(since) + else: + # by default reverse=false, i.e. trades are fetched since the time of market inception(year 2015 for XBTUSD) + request['reverse'] = True + if limit is not None: + request['count'] = min(limit, 1000) # api maximum 1000 + until = self.safe_integer_2(params, 'until', 'endTime') + if until is not None: + params = self.omit(params, ['until']) + request['endTime'] = self.iso8601(until) + response = await self.publicGetTrade(self.extend(request, params)) + # + # [ + # { + # "timestamp": "2018-08-28T00:00:02.735Z", + # "symbol": "XBTUSD", + # "side": "Buy", + # "size": 2000, + # "price": 6906.5, + # "tickDirection": "PlusTick", + # "trdMatchID": "b9a42432-0a46-6a2f-5ecc-c32e9ca4baf8", + # "grossValue": 28958000, + # "homeNotional": 0.28958, + # "foreignNotional": 2000 + # }, + # { + # "timestamp": "2018-08-28T00:00:03.778Z", + # "symbol": "XBTUSD", + # "side": "Sell", + # "size": 1000, + # "price": 6906, + # "tickDirection": "MinusTick", + # "trdMatchID": "0d4f1682-5270-a800-569b-4a0eb92db97c", + # "grossValue": 14480000, + # "homeNotional": 0.1448, + # "foreignNotional": 1000 + # }, + # ] + # + return self.parse_trades(response, market, since, limit) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://www.bitmex.com/api/explorer/#not /Order/Order_new + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param dict [params.triggerPrice]: the price at which a trigger order is triggered at + :param dict [params.triggerDirection]: the direction whenever the trigger happens with relation to price - 'ascending' or 'descending' + :param float [params.trailingAmount]: the quote amount to trail away from the current market price + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + orderType = self.capitalize(type) + reduceOnly = self.safe_value(params, 'reduceOnly') + if reduceOnly is not None: + if (not market['swap']) and (not market['future']): + raise InvalidOrder(self.id + ' createOrder() does not support reduceOnly for ' + market['type'] + ' orders, reduceOnly orders are supported for swap and future markets only') + brokerId = self.safe_string(self.options, 'brokerId', 'CCXT') + qty = self.parse_to_int(self.amount_to_precision(symbol, amount)) + request: dict = { + 'symbol': market['id'], + 'side': self.capitalize(side), + 'orderQty': qty, # lot size multiplied by the number of contracts + 'ordType': orderType, + 'text': brokerId, + } + # support for unified trigger format + triggerPrice = self.safe_number_n(params, ['triggerPrice', 'stopPx', 'stopPrice']) + trailingAmount = self.safe_string_2(params, 'trailingAmount', 'pegOffsetValue') + isTriggerOrder = triggerPrice is not None + isTrailingAmountOrder = trailingAmount is not None + if isTriggerOrder or isTrailingAmountOrder: + triggerDirection = self.safe_string(params, 'triggerDirection') + triggerAbove = ((triggerDirection == 'ascending') or (triggerDirection == 'above')) + if (type == 'limit') or (type == 'market'): + self.check_required_argument('createOrder', triggerDirection, 'triggerDirection', ['above', 'below']) + if type == 'limit': + if side == 'buy': + orderType = 'StopLimit' if triggerAbove else 'LimitIfTouched' + else: + orderType = 'LimitIfTouched' if triggerAbove else 'StopLimit' + elif type == 'market': + if side == 'buy': + orderType = 'Stop' if triggerAbove else 'MarketIfTouched' + else: + orderType = 'MarketIfTouched' if triggerAbove else 'Stop' + if isTrailingAmountOrder: + isStopSellOrder = (side == 'sell') and ((orderType == 'Stop') or (orderType == 'StopLimit')) + isBuyIfTouchedOrder = (side == 'buy') and ((orderType == 'MarketIfTouched') or (orderType == 'LimitIfTouched')) + if isStopSellOrder or isBuyIfTouchedOrder: + trailingAmount = '-' + trailingAmount + request['pegOffsetValue'] = self.parse_to_numeric(trailingAmount) + request['pegPriceType'] = 'TrailingStopPeg' + else: + if triggerPrice is None: + # if exchange specific trigger types were provided + raise ArgumentsRequired(self.id + ' createOrder() requires a triggerPrice parameter for the ' + orderType + ' order type') + request['stopPx'] = self.parse_to_numeric(self.price_to_precision(symbol, triggerPrice)) + request['ordType'] = orderType + params = self.omit(params, ['triggerPrice', 'stopPrice', 'stopPx', 'triggerDirection', 'trailingAmount']) + if (orderType == 'Limit') or (orderType == 'StopLimit') or (orderType == 'LimitIfTouched'): + request['price'] = self.parse_to_numeric(self.price_to_precision(symbol, price)) + clientOrderId = self.safe_string_2(params, 'clOrdID', 'clientOrderId') + if clientOrderId is not None: + request['clOrdID'] = clientOrderId + params = self.omit(params, ['clOrdID', 'clientOrderId']) + response = await self.privatePostOrder(self.extend(request, params)) + return self.parse_order(response, market) + + async def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + await self.load_markets() + request: dict = {} + trailingAmount = self.safe_string_2(params, 'trailingAmount', 'pegOffsetValue') + isTrailingAmountOrder = trailingAmount is not None + if isTrailingAmountOrder: + triggerDirection = self.safe_string(params, 'triggerDirection') + triggerAbove = ((triggerDirection == 'ascending') or (triggerDirection == 'above')) + if (type == 'limit') or (type == 'market'): + self.check_required_argument('createOrder', triggerDirection, 'triggerDirection', ['above', 'below']) + orderType = None + if type == 'limit': + if side == 'buy': + orderType = 'StopLimit' if triggerAbove else 'LimitIfTouched' + else: + orderType = 'LimitIfTouched' if triggerAbove else 'StopLimit' + elif type == 'market': + if side == 'buy': + orderType = 'Stop' if triggerAbove else 'MarketIfTouched' + else: + orderType = 'MarketIfTouched' if triggerAbove else 'Stop' + isStopSellOrder = (side == 'sell') and ((orderType == 'Stop') or (orderType == 'StopLimit')) + isBuyIfTouchedOrder = (side == 'buy') and ((orderType == 'MarketIfTouched') or (orderType == 'LimitIfTouched')) + if isStopSellOrder or isBuyIfTouchedOrder: + trailingAmount = '-' + trailingAmount + request['pegOffsetValue'] = self.parse_to_numeric(trailingAmount) + params = self.omit(params, ['triggerDirection', 'trailingAmount']) + origClOrdID = self.safe_string_2(params, 'origClOrdID', 'clientOrderId') + if origClOrdID is not None: + request['origClOrdID'] = origClOrdID + clientOrderId = self.safe_string(params, 'clOrdID', 'clientOrderId') + if clientOrderId is not None: + request['clOrdID'] = clientOrderId + params = self.omit(params, ['origClOrdID', 'clOrdID', 'clientOrderId']) + else: + request['orderID'] = id + if amount is not None: + qty = self.parse_to_int(self.amount_to_precision(symbol, amount)) + request['orderQty'] = qty + if price is not None: + request['price'] = price + brokerId = self.safe_string(self.options, 'brokerId', 'CCXT') + request['text'] = brokerId + response = await self.privatePutOrder(self.extend(request, params)) + return self.parse_order(response) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://www.bitmex.com/api/explorer/#not /Order/Order_cancel + + :param str id: order id + :param str symbol: not used by bitmex cancelOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + # https://github.com/ccxt/ccxt/issues/6507 + clientOrderId = self.safe_value_2(params, 'clOrdID', 'clientOrderId') + request: dict = {} + if clientOrderId is None: + request['orderID'] = id + else: + request['clOrdID'] = clientOrderId + params = self.omit(params, ['clOrdID', 'clientOrderId']) + response = await self.privateDeleteOrder(self.extend(request, params)) + order = self.safe_value(response, 0, {}) + error = self.safe_string(order, 'error') + if error is not None: + if error.find('Unable to cancel order due to existing state') >= 0: + raise OrderNotFound(self.id + ' cancelOrder() failed: ' + error) + return self.parse_order(order) + + async def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): + """ + cancel multiple orders + + https://www.bitmex.com/api/explorer/#not /Order/Order_cancel + + :param str[] ids: order ids + :param str symbol: not used by bitmex cancelOrders() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an list of `order structures ` + """ + # return await self.cancel_order(ids, symbol, params) + await self.load_markets() + # https://github.com/ccxt/ccxt/issues/6507 + clientOrderId = self.safe_value_2(params, 'clOrdID', 'clientOrderId') + request: dict = {} + if clientOrderId is None: + request['orderID'] = ids + else: + request['clOrdID'] = clientOrderId + params = self.omit(params, ['clOrdID', 'clientOrderId']) + response = await self.privateDeleteOrder(self.extend(request, params)) + return self.parse_orders(response) + + async def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders + + https://www.bitmex.com/api/explorer/#not /Order/Order_cancelAll + + :param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + response = await self.privateDeleteOrderAll(self.extend(request, params)) + # + # [ + # { + # "orderID": "string", + # "clOrdID": "string", + # "clOrdLinkID": "string", + # "account": 0, + # "symbol": "string", + # "side": "string", + # "simpleOrderQty": 0, + # "orderQty": 0, + # "price": 0, + # "displayQty": 0, + # "stopPx": 0, + # "pegOffsetValue": 0, + # "pegPriceType": "string", + # "currency": "string", + # "settlCurrency": "string", + # "ordType": "string", + # "timeInForce": "string", + # "execInst": "string", + # "contingencyType": "string", + # "exDestination": "string", + # "ordStatus": "string", + # "triggered": "string", + # "workingIndicator": True, + # "ordRejReason": "string", + # "simpleLeavesQty": 0, + # "leavesQty": 0, + # "simpleCumQty": 0, + # "cumQty": 0, + # "avgPx": 0, + # "multiLegReportingType": "string", + # "text": "string", + # "transactTime": "2020-06-01T09:36:35.290Z", + # "timestamp": "2020-06-01T09:36:35.290Z" + # } + # ] + # + return self.parse_orders(response, market) + + async def cancel_all_orders_after(self, timeout: Int, params={}): + """ + dead man's switch, cancel all orders after the given timeout + + https://www.bitmex.com/api/explorer/#not /Order/Order_cancelAllAfter + + :param number timeout: time in milliseconds, 0 represents cancel the timer + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: the api result + """ + await self.load_markets() + request: dict = { + 'timeout': self.parse_to_int(timeout / 1000) if (timeout > 0) else 0, + } + response = await self.privatePostOrderCancelAllAfter(self.extend(request, params)) + # + # { + # now: '2024-04-09T09:01:56.560Z', + # cancelTime: '2024-04-09T09:01:56.660Z' + # } + # + return response + + async def fetch_leverages(self, symbols: Strings = None, params={}) -> Leverages: + """ + fetch the set leverage for all contract markets + + https://www.bitmex.com/api/explorer/#not /Position/Position_get + + :param str[] [symbols]: a list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `leverage structures ` + """ + await self.load_markets() + leverages = await self.fetch_positions(symbols, params) + return self.parse_leverages(leverages, symbols, 'symbol') + + def parse_leverage(self, leverage: dict, market: Market = None) -> Leverage: + marketId = self.safe_string(leverage, 'symbol') + return { + 'info': leverage, + 'symbol': self.safe_symbol(marketId, market), + 'marginMode': self.safe_string_lower(leverage, 'marginMode'), + 'longLeverage': self.safe_integer(leverage, 'leverage'), + 'shortLeverage': self.safe_integer(leverage, 'leverage'), + } + + async def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://www.bitmex.com/api/explorer/#not /Position/Position_get + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + await self.load_markets() + response = await self.privateGetPosition(params) + # + # [ + # { + # "account": 0, + # "symbol": "string", + # "currency": "string", + # "underlying": "string", + # "quoteCurrency": "string", + # "commission": 0, + # "initMarginReq": 0, + # "maintMarginReq": 0, + # "riskLimit": 0, + # "leverage": 0, + # "crossMargin": True, + # "deleveragePercentile": 0, + # "rebalancedPnl": 0, + # "prevRealisedPnl": 0, + # "prevUnrealisedPnl": 0, + # "prevClosePrice": 0, + # "openingTimestamp": "2020-11-09T06:53:59.892Z", + # "openingQty": 0, + # "openingCost": 0, + # "openingComm": 0, + # "openOrderBuyQty": 0, + # "openOrderBuyCost": 0, + # "openOrderBuyPremium": 0, + # "openOrderSellQty": 0, + # "openOrderSellCost": 0, + # "openOrderSellPremium": 0, + # "execBuyQty": 0, + # "execBuyCost": 0, + # "execSellQty": 0, + # "execSellCost": 0, + # "execQty": 0, + # "execCost": 0, + # "execComm": 0, + # "currentTimestamp": "2020-11-09T06:53:59.893Z", + # "currentQty": 0, + # "currentCost": 0, + # "currentComm": 0, + # "realisedCost": 0, + # "unrealisedCost": 0, + # "grossOpenCost": 0, + # "grossOpenPremium": 0, + # "grossExecCost": 0, + # "isOpen": True, + # "markPrice": 0, + # "markValue": 0, + # "riskValue": 0, + # "homeNotional": 0, + # "foreignNotional": 0, + # "posState": "string", + # "posCost": 0, + # "posCost2": 0, + # "posCross": 0, + # "posInit": 0, + # "posComm": 0, + # "posLoss": 0, + # "posMargin": 0, + # "posMaint": 0, + # "posAllowance": 0, + # "taxableMargin": 0, + # "initMargin": 0, + # "maintMargin": 0, + # "sessionMargin": 0, + # "targetExcessMargin": 0, + # "varMargin": 0, + # "realisedGrossPnl": 0, + # "realisedTax": 0, + # "realisedPnl": 0, + # "unrealisedGrossPnl": 0, + # "longBankrupt": 0, + # "shortBankrupt": 0, + # "taxBase": 0, + # "indicativeTaxRate": 0, + # "indicativeTax": 0, + # "unrealisedTax": 0, + # "unrealisedPnl": 0, + # "unrealisedPnlPcnt": 0, + # "unrealisedRoePcnt": 0, + # "simpleQty": 0, + # "simpleCost": 0, + # "simpleValue": 0, + # "simplePnl": 0, + # "simplePnlPcnt": 0, + # "avgCostPrice": 0, + # "avgEntryPrice": 0, + # "breakEvenPrice": 0, + # "marginCallPrice": 0, + # "liquidationPrice": 0, + # "bankruptPrice": 0, + # "timestamp": "2020-11-09T06:53:59.894Z", + # "lastPrice": 0, + # "lastValue": 0 + # } + # ] + # + results = self.parse_positions(response, symbols) + return self.filter_by_array_positions(results, 'symbol', symbols, False) + + def parse_position(self, position: dict, market: Market = None): + # + # { + # "account": 9371654, + # "symbol": "ETHUSDT", + # "currency": "USDt", + # "underlying": "ETH", + # "quoteCurrency": "USDT", + # "commission": 0.00075, + # "initMarginReq": 0.3333333333333333, + # "maintMarginReq": 0.01, + # "riskLimit": 1000000000000, + # "leverage": 3, + # "crossMargin": False, + # "deleveragePercentile": 1, + # "rebalancedPnl": 0, + # "prevRealisedPnl": 0, + # "prevUnrealisedPnl": 0, + # "prevClosePrice": 2053.738, + # "openingTimestamp": "2022-05-21T04:00:00.000Z", + # "openingQty": 0, + # "openingCost": 0, + # "openingComm": 0, + # "openOrderBuyQty": 0, + # "openOrderBuyCost": 0, + # "openOrderBuyPremium": 0, + # "openOrderSellQty": 0, + # "openOrderSellCost": 0, + # "openOrderSellPremium": 0, + # "execBuyQty": 2000, + # "execBuyCost": 39260000, + # "execSellQty": 0, + # "execSellCost": 0, + # "execQty": 2000, + # "execCost": 39260000, + # "execComm": 26500, + # "currentTimestamp": "2022-05-21T04:35:16.397Z", + # "currentQty": 2000, + # "currentCost": 39260000, + # "currentComm": 26500, + # "realisedCost": 0, + # "unrealisedCost": 39260000, + # "grossOpenCost": 0, + # "grossOpenPremium": 0, + # "grossExecCost": 39260000, + # "isOpen": True, + # "markPrice": 1964.195, + # "markValue": 39283900, + # "riskValue": 39283900, + # "homeNotional": 0.02, + # "foreignNotional": -39.2839, + # "posState": "", + # "posCost": 39260000, + # "posCost2": 39260000, + # "posCross": 0, + # "posInit": 13086667, + # "posComm": 39261, + # "posLoss": 0, + # "posMargin": 13125928, + # "posMaint": 435787, + # "posAllowance": 0, + # "taxableMargin": 0, + # "initMargin": 0, + # "maintMargin": 13149828, + # "sessionMargin": 0, + # "targetExcessMargin": 0, + # "varMargin": 0, + # "realisedGrossPnl": 0, + # "realisedTax": 0, + # "realisedPnl": -26500, + # "unrealisedGrossPnl": 23900, + # "longBankrupt": 0, + # "shortBankrupt": 0, + # "taxBase": 0, + # "indicativeTaxRate": null, + # "indicativeTax": 0, + # "unrealisedTax": 0, + # "unrealisedPnl": 23900, + # "unrealisedPnlPcnt": 0.0006, + # "unrealisedRoePcnt": 0.0018, + # "simpleQty": null, + # "simpleCost": null, + # "simpleValue": null, + # "simplePnl": null, + # "simplePnlPcnt": null, + # "avgCostPrice": 1963, + # "avgEntryPrice": 1963, + # "breakEvenPrice": 1964.35, + # "marginCallPrice": 1328.5, + # "liquidationPrice": 1328.5, + # "bankruptPrice": 1308.7, + # "timestamp": "2022-05-21T04:35:16.397Z", + # "lastPrice": 1964.195, + # "lastValue": 39283900 + # } + # + market = self.safe_market(self.safe_string(position, 'symbol'), market) + symbol = market['symbol'] + datetime = self.safe_string(position, 'timestamp') + crossMargin = self.safe_value(position, 'crossMargin') + marginMode = 'cross' if (crossMargin is True) else 'isolated' + notionalString = Precise.string_abs(self.safe_string_2(position, 'foreignNotional', 'homeNotional')) + settleCurrencyCode = self.safe_string(market, 'settle') + maintenanceMargin = self.convert_to_real_amount(settleCurrencyCode, self.safe_string(position, 'maintMargin')) + unrealisedPnl = self.convert_to_real_amount(settleCurrencyCode, self.safe_string(position, 'unrealisedPnl')) + contracts = self.parse_number(Precise.string_abs(self.safe_string(position, 'currentQty'))) + contractSize = self.safe_number(market, 'contractSize') + side = None + homeNotional = self.safe_string(position, 'homeNotional') + if homeNotional is not None: + if homeNotional[0] == '-': + side = 'short' + else: + side = 'long' + return self.safe_position({ + 'info': position, + 'id': self.safe_string(position, 'account'), + 'symbol': symbol, + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + 'lastUpdateTimestamp': None, + 'hedged': None, + 'side': side, + 'contracts': contracts, + 'contractSize': contractSize, + 'entryPrice': self.safe_number(position, 'avgEntryPrice'), + 'markPrice': self.safe_number(position, 'markPrice'), + 'lastPrice': None, + 'notional': self.parse_number(notionalString), + 'leverage': self.safe_number(position, 'leverage'), + 'collateral': None, + 'initialMargin': self.safe_number(position, 'initMargin'), + 'initialMarginPercentage': self.safe_number(position, 'initMarginReq'), + 'maintenanceMargin': maintenanceMargin, + 'maintenanceMarginPercentage': self.safe_number(position, 'maintMarginReq'), + 'unrealizedPnl': unrealisedPnl, + 'liquidationPrice': self.safe_number(position, 'liquidationPrice'), + 'marginMode': marginMode, + 'marginRatio': None, + 'percentage': self.safe_number(position, 'unrealisedPnlPcnt'), + 'stopLossPrice': None, + 'takeProfitPrice': None, + }) + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://www.bitmex.com/api/explorer/#not /User/User_requestWithdrawal + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.check_address(address) + await self.load_markets() + currency = self.currency(code) + qty = self.convert_from_real_amount(code, amount) + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + request: dict = { + 'currency': currency['id'], + 'amount': qty, + 'address': address, + 'network': self.network_code_to_id(networkCode, currency['code']), + # 'otpToken': '123456', # requires if two-factor auth(OTP) is enabled + # 'fee': 0.001, # bitcoin network fee + } + if self.twofa is not None: + request['otpToken'] = self.totp(self.twofa) + response = await self.privatePostUserRequestWithdrawal(self.extend(request, params)) + # + # { + # "transactID": "3aece414-bb29-76c8-6c6d-16a477a51a1e", + # "account": 1403035, + # "currency": "USDt", + # "network": "tron", + # "transactType": "Withdrawal", + # "amount": -11000000, + # "fee": 1000000, + # "transactStatus": "Pending", + # "address": "TAf5JxcAQQsC2Nm2zu21XE2iDtnisxPo1x", + # "tx": "", + # "text": "", + # "transactTime": "2022-12-16T07:37:06.500Z", + # "timestamp": "2022-12-16T07:37:06.500Z", + # } + # + return self.parse_transaction(response, currency) + + async def fetch_funding_rates(self, symbols: Strings = None, params={}) -> FundingRates: + """ + fetch the funding rate for multiple markets + + https://www.bitmex.com/api/explorer/#not /Instrument/Instrument_getActiveAndIndices + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `funding rate structures `, indexed by market symbols + """ + await self.load_markets() + response = await self.publicGetInstrumentActiveAndIndices(params) + # same response "fetchMarkets" + filteredResponse = [] + for i in range(0, len(response)): + item = response[i] + marketId = self.safe_string(item, 'symbol') + market = self.safe_market(marketId) + swap = self.safe_bool(market, 'swap', False) + if swap: + filteredResponse.append(item) + symbols = self.market_symbols(symbols) + result = self.parse_funding_rates(filteredResponse) + return self.filter_by_array(result, 'symbol', symbols) + + def parse_funding_rate(self, contract, market: Market = None) -> FundingRate: + # see response sample under "fetchMarkets" because same endpoint is being used here + datetime = self.safe_string(contract, 'timestamp') + marketId = self.safe_string(contract, 'symbol') + fundingDatetime = self.safe_string(contract, 'fundingTimestamp') + return { + 'info': contract, + 'symbol': self.safe_symbol(marketId, market), + 'markPrice': self.safe_number(contract, 'markPrice'), + 'indexPrice': None, + 'interestRate': None, + 'estimatedSettlePrice': self.safe_number(contract, 'indicativeSettlePrice'), + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + 'fundingRate': self.safe_number(contract, 'fundingRate'), + 'fundingTimestamp': self.parse8601(fundingDatetime), + 'fundingDatetime': fundingDatetime, + 'nextFundingRate': self.safe_number(contract, 'indicativeFundingRate'), + 'nextFundingTimestamp': None, + 'nextFundingDatetime': None, + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': None, + } + + async def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + Fetches the history of funding rates + + https://www.bitmex.com/api/explorer/#not /Funding/Funding_get + + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: timestamp in ms of the earliest funding rate to fetch + :param int [limit]: the maximum amount of `funding rate structures ` to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms for ending date filter + :param bool [params.reverse]: if True, will sort results newest first + :param int [params.start]: starting point for results + :param str [params.columns]: array of column names to fetch in info, if omitted, will return all columns + :param str [params.filter]: generic table filter, send json key/value pairs, such as {"key": "value"}, you can key on individual fields, and do more advanced querying on timestamps, see the `timestamp docs ` for more details + :returns dict[]: a list of `funding rate structures ` + """ + await self.load_markets() + request: dict = {} + market = None + if symbol in self.currencies: + code = self.currency(symbol) + request['symbol'] = code['id'] + elif symbol is not None: + splitSymbol = symbol.split(':') + splitSymbolLength = len(splitSymbol) + timeframes = ['nearest', 'daily', 'weekly', 'monthly', 'quarterly', 'biquarterly', 'perpetual'] + if (splitSymbolLength > 1) and self.in_array(splitSymbol[1], timeframes): + code = self.currency(splitSymbol[0]) + symbol = code['id'] + ':' + splitSymbol[1] + request['symbol'] = symbol + else: + market = self.market(symbol) + request['symbol'] = market['id'] + if since is not None: + request['startTime'] = self.iso8601(since) + if limit is not None: + request['count'] = limit + until = self.safe_integer(params, 'until') + params = self.omit(params, ['until']) + if until is not None: + request['endTime'] = self.iso8601(until) + if (since is None) and (until is None): + request['reverse'] = True + response = await self.publicGetFunding(self.extend(request, params)) + # + # [ + # { + # "timestamp": "2016-05-07T12:00:00.000Z", + # "symbol": "ETHXBT", + # "fundingInterval": "2000-01-02T00:00:00.000Z", + # "fundingRate": 0.0010890000000000001, + # "fundingRateDaily": 0.0010890000000000001 + # } + # ] + # + return self.parse_funding_rate_histories(response, market, since, limit) + + def parse_funding_rate_history(self, info, market: Market = None): + # + # { + # "timestamp": "2016-05-07T12:00:00.000Z", + # "symbol": "ETHXBT", + # "fundingInterval": "2000-01-02T00:00:00.000Z", + # "fundingRate": 0.0010890000000000001, + # "fundingRateDaily": 0.0010890000000000001 + # } + # + marketId = self.safe_string(info, 'symbol') + datetime = self.safe_string(info, 'timestamp') + return { + 'info': info, + 'symbol': self.safe_symbol(marketId, market), + 'fundingRate': self.safe_number(info, 'fundingRate'), + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + } + + async def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://www.bitmex.com/api/explorer/#not /Position/Position_updateLeverage + + :param float leverage: the rate of leverage + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setLeverage() requires a symbol argument') + if (leverage < 0.01) or (leverage > 100): + raise BadRequest(self.id + ' leverage should be between 0.01 and 100') + await self.load_markets() + market = self.market(symbol) + if market['type'] != 'swap' and market['type'] != 'future': + raise BadSymbol(self.id + ' setLeverage() supports future and swap contracts only') + request: dict = { + 'symbol': market['id'], + 'leverage': leverage, + } + return await self.privatePostPositionLeverage(self.extend(request, params)) + + async def set_margin_mode(self, marginMode: str, symbol: Str = None, params={}): + """ + set margin mode to 'cross' or 'isolated' + + https://www.bitmex.com/api/explorer/#not /Position/Position_isolateMargin + + :param str marginMode: 'cross' or 'isolated' + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setMarginMode() requires a symbol argument') + marginMode = marginMode.lower() + if marginMode != 'isolated' and marginMode != 'cross': + raise BadRequest(self.id + ' setMarginMode() marginMode argument should be isolated or cross') + await self.load_markets() + market = self.market(symbol) + if (market['type'] != 'swap') and (market['type'] != 'future'): + raise BadSymbol(self.id + ' setMarginMode() supports swap and future contracts only') + enabled = False if (marginMode == 'cross') else True + request: dict = { + 'symbol': market['id'], + 'enabled': enabled, + } + return await self.privatePostPositionIsolate(self.extend(request, params)) + + async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://www.bitmex.com/api/explorer/#not /User/User_getDepositAddress + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.network]: deposit chain, can view all chains via self.publicGetWalletAssets, default is eth, unless the currency has a default chain within self.options['networks'] + :returns dict: an `address structure ` + """ + await self.load_markets() + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + if networkCode is None: + raise ArgumentsRequired(self.id + ' fetchDepositAddress requires params["network"]') + currency = self.currency(code) + params = self.omit(params, 'network') + request: dict = { + 'currency': currency['id'], + 'network': self.network_code_to_id(networkCode, currency['code']), + } + response = await self.privateGetUserDepositAddress(self.extend(request, params)) + # + # '"bc1qmex3puyrzn2gduqcnlu70c2uscpyaa9nm2l2j9le2lt2wkgmw33sy7ndjg"' + # + return { + 'info': response, + 'currency': code, + 'network': networkCode, + 'address': response.replace('"', '').replace('"', ''), # Done twice because some languages only replace the first instance + 'tag': None, + } + + def parse_deposit_withdraw_fee(self, fee, currency: Currency = None): + # + # { + # "asset": "XBT", + # "currency": "XBt", + # "majorCurrency": "XBT", + # "name": "Bitcoin", + # "currencyType": "Crypto", + # "scale": "8", + # "enabled": True, + # "isMarginCurrency": True, + # "minDepositAmount": "10000", + # "minWithdrawalAmount": "1000", + # "maxWithdrawalAmount": "100000000000000", + # "networks": [ + # { + # "asset": "btc", + # "tokenAddress": '', + # "depositEnabled": True, + # "withdrawalEnabled": True, + # "withdrawalFee": "20000", + # "minFee": "20000", + # "maxFee": "10000000" + # } + # ] + # } + # + networks = self.safe_value(fee, 'networks', []) + networksLength = len(networks) + result: dict = { + 'info': fee, + 'withdraw': { + 'fee': None, + 'percentage': None, + }, + 'deposit': { + 'fee': None, + 'percentage': None, + }, + 'networks': {}, + } + if networksLength != 0: + scale = self.safe_string(fee, 'scale') + precision = self.parse_precision(scale) + for i in range(0, networksLength): + network = networks[i] + networkId = self.safe_string(network, 'asset') + currencyCode = self.safe_string(currency, 'code') + networkCode = self.network_id_to_code(networkId, currencyCode) + withdrawalFeeId = self.safe_string(network, 'withdrawalFee') + withdrawalFee = self.parse_number(Precise.string_mul(withdrawalFeeId, precision)) + result['networks'][networkCode] = { + 'deposit': {'fee': None, 'percentage': None}, + 'withdraw': {'fee': withdrawalFee, 'percentage': False}, + } + if networksLength == 1: + result['withdraw']['fee'] = withdrawalFee + result['withdraw']['percentage'] = False + return result + + async def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}): + """ + fetch deposit and withdraw fees + + https://www.bitmex.com/api/explorer/#not /Wallet/Wallet_getAssetsConfig + + :param str[]|None codes: list of unified currency codes + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `fee structures ` + """ + await self.load_markets() + assets = await self.publicGetWalletAssets(params) + # + # [ + # { + # "asset": "XBT", + # "currency": "XBt", + # "majorCurrency": "XBT", + # "name": "Bitcoin", + # "currencyType": "Crypto", + # "scale": "8", + # "enabled": True, + # "isMarginCurrency": True, + # "minDepositAmount": "10000", + # "minWithdrawalAmount": "1000", + # "maxWithdrawalAmount": "100000000000000", + # "networks": [ + # { + # "asset": "btc", + # "tokenAddress": '', + # "depositEnabled": True, + # "withdrawalEnabled": True, + # "withdrawalFee": "20000", + # "minFee": "20000", + # "maxFee": "10000000" + # } + # ] + # }, + # ... + # ] + # + return self.parse_deposit_withdraw_fees(assets, codes, 'asset') + + def calculate_rate_limiter_cost(self, api, method, path, params, config={}): + isAuthenticated = self.check_required_credentials(False) + cost = self.safe_value(config, 'cost', 1) + if cost != 1: # trading endpoints + if isAuthenticated: + return cost + else: + return 20 + return cost + + async def fetch_liquidations(self, symbol: str, since: Int = None, limit: Int = None, params={}): + """ + retrieves the public liquidations of a trading pair + + https://www.bitmex.com/api/explorer/#not /Liquidation/Liquidation_get + + :param str symbol: unified CCXT market symbol + :param int [since]: the earliest time in ms to fetch liquidations for + :param int [limit]: the maximum number of liquidation structures to retrieve + :param dict [params]: exchange specific parameters for the bitmex api endpoint + :param int [params.until]: timestamp in ms of the latest liquidation + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict: an array of `liquidation structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchLiquidations', 'paginate') + if paginate: + return await self.fetch_paginated_call_dynamic('fetchLiquidations', symbol, since, limit, params) + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if since is not None: + request['startTime'] = since + if limit is not None: + request['count'] = limit + request, params = self.handle_until_option('endTime', request, params) + response = await self.publicGetLiquidation(self.extend(request, params)) + # + # [ + # { + # "orderID": "string", + # "symbol": "string", + # "side": "string", + # "price": 0, + # "leavesQty": 0 + # } + # ] + # + return self.parse_liquidations(response, market, since, limit) + + def parse_liquidation(self, liquidation, market: Market = None): + # + # { + # "orderID": "string", + # "symbol": "string", + # "side": "string", + # "price": 0, + # "leavesQty": 0 + # } + # + marketId = self.safe_string(liquidation, 'symbol') + return self.safe_liquidation({ + 'info': liquidation, + 'symbol': self.safe_symbol(marketId, market), + 'contracts': None, + 'contractSize': self.safe_number(market, 'contractSize'), + 'price': self.safe_number(liquidation, 'price'), + 'side': self.safe_string_lower(liquidation, 'side'), + 'baseValue': None, + 'quoteValue': None, + 'timestamp': None, + 'datetime': None, + }) + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None + if code == 429: + raise DDoSProtection(self.id + ' ' + body) + if code >= 400: + error = self.safe_value(response, 'error', {}) + message = self.safe_string(error, 'message') + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + if code == 400: + raise BadRequest(feedback) + raise ExchangeError(feedback) # unknown message + return None + + def nonce(self): + return self.milliseconds() + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + query = '/api/' + self.version + '/' + path + if method == 'GET': + if params: + query += '?' + self.urlencode(params) + else: + format = self.safe_string(params, '_format') + if format is not None: + query += '?' + self.urlencode({'_format': format}) + params = self.omit(params, '_format') + url = self.urls['api'][api] + query + isAuthenticated = self.check_required_credentials(False) + if api == 'private' or (api == 'public' and isAuthenticated): + self.check_required_credentials() + auth = method + query + expires = self.safe_integer(self.options, 'api-expires') + headers = { + 'Content-Type': 'application/json', + 'api-key': self.apiKey, + } + expires = self.sum(self.seconds(), expires) + stringExpires = str(expires) + auth += stringExpires + headers['api-expires'] = stringExpires + if method == 'POST' or method == 'PUT' or method == 'DELETE': + if params: + body = self.json(params) + auth += body + headers['api-signature'] = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256) + return {'url': url, 'method': method, 'body': body, 'headers': headers} diff --git a/ccxt/async_support/bitopro.py b/ccxt/async_support/bitopro.py new file mode 100644 index 0000000..5026e87 --- /dev/null +++ b/ccxt/async_support/bitopro.py @@ -0,0 +1,1830 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.bitopro import ImplicitAPI +import hashlib +import math +from ccxt.base.types import Any, Balances, Currencies, Currency, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, TradingFees, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class bitopro(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(bitopro, self).describe(), { + 'id': 'bitopro', + 'name': 'BitoPro', + 'countries': ['TW'], # Taiwan + 'version': 'v3', + 'rateLimit': 100, + 'pro': True, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'cancelOrders': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createOrder': True, + 'createOrderWithTakeProfitAndStopLoss': False, + 'createOrderWithTakeProfitAndStopLossWs': False, + 'createReduceOnlyOrder': False, + 'createStopOrder': True, + 'createTriggerOrder': True, + 'editOrder': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': False, + 'fetchDeposits': True, + 'fetchDepositsWithdrawals': False, + 'fetchDepositWithdrawFee': 'emulated', + 'fetchDepositWithdrawFees': True, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': False, + 'fetchOrderTrades': False, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': False, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': True, + 'fetchTransactionFees': False, + 'fetchTransactions': False, + 'fetchTransfer': False, + 'fetchTransfers': False, + 'fetchVolatilityHistory': False, + 'fetchWithdrawal': True, + 'fetchWithdrawals': True, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'transfer': False, + 'withdraw': True, + }, + 'timeframes': { + '1m': '1m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1h', + '3h': '3h', + '6h': '6h', + '12h': '12h', + '1d': '1d', + '1w': '1w', + '1M': '1M', + }, + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/affc6337-b95a-44bf-aacd-04f9722364f6', + 'api': { + 'rest': 'https://api.bitopro.com/v3', + }, + 'www': 'https://www.bitopro.com', + 'doc': [ + 'https://github.com/bitoex/bitopro-offical-api-docs/blob/master/v3-1/rest-1/rest.md', + ], + 'fees': 'https://www.bitopro.com/fees', + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + }, + 'api': { + 'public': { + 'get': { + 'order-book/{pair}': 1, + 'tickers': 1, + 'tickers/{pair}': 1, + 'trades/{pair}': 1, + 'provisioning/currencies': 1, + 'provisioning/trading-pairs': 1, + 'provisioning/limitations-and-fees': 1, + 'trading-history/{pair}': 1, + 'price/otc/{currency}': 1, + }, + }, + 'private': { + 'get': { + 'accounts/balance': 1, + 'orders/history': 1, + 'orders/all/{pair}': 1, + 'orders/trades/{pair}': 1, + 'orders/{pair}/{orderId}': 1, + 'wallet/withdraw/{currency}/{serial}': 1, + 'wallet/withdraw/{currency}/id/{id}': 1, + 'wallet/depositHistory/{currency}': 1, + 'wallet/withdrawHistory/{currency}': 1, + 'orders/open': 1, + }, + 'post': { + 'orders/{pair}': 1 / 2, # 1200/m => 20/s => 10/20 = 1/2 + 'orders/batch': 20 / 3, # 90/m => 1.5/s => 10/1.5 = 20/3 + 'wallet/withdraw/{currency}': 10, # 60/m => 1/s => 10/1 = 10 + }, + 'put': { + 'orders': 5, # 2/s => 10/2 = 5 + }, + 'delete': { + 'orders/{pair}/{id}': 2 / 3, # 900/m => 15/s => 10/15 = 2/3 + 'orders/all': 5, # 2/s => 10/2 = 5 + 'orders/{pair}': 5, # 2/s => 10/2 = 5 + }, + }, + }, + 'fees': { + 'trading': { + 'tierBased': True, + 'percentage': True, + 'maker': self.parse_number('0.001'), + 'taker': self.parse_number('0.002'), + 'tiers': { + 'taker': [ + [self.parse_number('0'), self.parse_number('0.002')], + [self.parse_number('3000000'), self.parse_number('0.00194')], + [self.parse_number('5000000'), self.parse_number('0.0015')], + [self.parse_number('30000000'), self.parse_number('0.0014')], + [self.parse_number('300000000'), self.parse_number('0.0013')], + [self.parse_number('550000000'), self.parse_number('0.0012')], + [self.parse_number('1300000000'), self.parse_number('0.0011')], + ], + 'maker': [ + [self.parse_number('0'), self.parse_number('0.001')], + [self.parse_number('3000000'), self.parse_number('0.00097')], + [self.parse_number('5000000'), self.parse_number('0.0007')], + [self.parse_number('30000000'), self.parse_number('0.0006')], + [self.parse_number('300000000'), self.parse_number('0.0005')], + [self.parse_number('550000000'), self.parse_number('0.0004')], + [self.parse_number('1300000000'), self.parse_number('0.0003')], + ], + }, + }, + }, + 'options': { + 'networks': { + 'ERC20': 'ERC20', + 'ETH': 'ERC20', + 'TRX': 'TRX', + 'TRC20': 'TRX', + 'BEP20': 'BSC', + 'BSC': 'BSC', + }, + 'fiatCurrencies': ['TWD'], # the only fiat currency for exchange + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerPriceType': None, + 'triggerDirection': True, # todo implement + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': False, + 'FOK': False, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyRequiresPrice': False, + 'marketBuyByCost': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 100000, + 'untilDays': 100000, + 'symbolRequired': True, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + # todo: implement through fetchOrders + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 100000, + 'untilDays': 100000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 100000, + 'daysBackCanceled': 1, + 'untilDays': 10000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOHLCV': { + 'limit': 1000, + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'exact': { + 'Unsupported currency.': BadRequest, # {"error":"Unsupported currency."} + 'Unsupported order type': BadRequest, # {"error":"Unsupported order type"} + 'Invalid body': BadRequest, # {"error":"Invalid body"} + 'Invalid Signature': AuthenticationError, # {"error":"Invalid Signature"} + 'Address not in whitelist.': BadRequest, + }, + 'broad': { + 'Invalid amount': InvalidOrder, # {"error":"Invalid amount 0.0000000001, decimal limit is 8."} + 'Balance for ': InsufficientFunds, # {"error":"Balance for eth not enough, only has 0, but ordered 0.01."} + 'Invalid ': BadRequest, # {"error":"Invalid price -1."} + 'Wrong parameter': BadRequest, # {"error":"Wrong parameter: from"} + }, + }, + 'commonCurrencies': { + }, + }) + + async def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/public/get_currency_info.md + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = await self.publicGetProvisioningCurrencies(params) + currencies = self.safe_list(response, 'data', []) + # + # { + # "data":[ + # { + # "currency":"eth", + # "withdrawFee":"0.007", + # "minWithdraw":"0.001", + # "maxWithdraw":"1000", + # "maxDailyWithdraw":"2000", + # "withdraw":true, + # "deposit":true, + # "depositConfirmation":"12" + # } + # ] + # } + # + result: dict = {} + fiatCurrencies = self.safe_list(self.options, 'fiatCurrencies', []) + for i in range(0, len(currencies)): + currency = currencies[i] + currencyId = self.safe_string(currency, 'currency') + code = self.safe_currency_code(currencyId) + deposit = self.safe_bool(currency, 'deposit') + withdraw = self.safe_bool(currency, 'withdraw') + fee = self.safe_number(currency, 'withdrawFee') + withdrawMin = self.safe_number(currency, 'minWithdraw') + withdrawMax = self.safe_number(currency, 'maxWithdraw') + limits: dict = { + 'withdraw': { + 'min': withdrawMin, + 'max': withdrawMax, + }, + 'amount': { + 'min': None, + 'max': None, + }, + } + isFiat = self.in_array(code, fiatCurrencies) + result[code] = { + 'id': currencyId, + 'code': code, + 'info': currency, + 'type': 'fiat' if isFiat else 'crypto', + 'name': None, + 'active': deposit and withdraw, + 'deposit': deposit, + 'withdraw': withdraw, + 'fee': fee, + 'precision': None, + 'limits': limits, + 'networks': None, + } + return result + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for bitopro + + https://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/public/get_trading_pair_info.md + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = await self.publicGetProvisioningTradingPairs() + markets = self.safe_list(response, 'data', []) + # + # { + # "data":[ + # { + # "pair":"shib_twd", + # "base":"shib", + # "quote":"twd", + # "basePrecision":"8", + # "quotePrecision":"6", + # "minLimitBaseAmount":"100000", + # "maxLimitBaseAmount":"5500000000", + # "minMarketBuyQuoteAmount":"1000", + # "orderOpenLimit":"200", + # "maintain":false, + # "orderBookQuotePrecision":"6", + # "orderBookQuoteScaleLevel":"5" + # } + # ] + # } + # + return self.parse_markets(markets) + + def parse_market(self, market: dict) -> Market: + active = not self.safe_bool(market, 'maintain') + id = self.safe_string(market, 'pair') + uppercaseId = id.upper() + baseId = self.safe_string(market, 'base') + quoteId = self.safe_string(market, 'quote') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + symbol = base + '/' + quote + limits: dict = { + 'amount': { + 'min': self.safe_number(market, 'minLimitBaseAmount'), + 'max': self.safe_number(market, 'maxLimitBaseAmount'), + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + 'leverage': { + 'min': None, + 'max': None, + }, + } + return { + 'id': id, + 'uppercaseId': uppercaseId, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'baseId': base, + 'quoteId': quote, + 'settle': None, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'limits': limits, + 'precision': { + 'price': self.parse_number(self.parse_precision(self.safe_string(market, 'quotePrecision'))), + 'amount': self.parse_number(self.parse_precision(self.safe_string(market, 'basePrecision'))), + }, + 'active': active, + 'created': None, + 'info': market, + } + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "pair":"btc_twd", + # "lastPrice":"1182449.00000000", + # "isBuyer":false, + # "priceChange24hr":"-1.99", + # "volume24hr":"9.13089740", + # "high24hr":"1226097.00000000", + # "low24hr":"1181000.00000000" + # } + # + marketId = self.safe_string(ticker, 'pair') + market = self.safe_market(marketId, market) + symbol = self.safe_string(market, 'symbol') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': None, + 'datetime': None, + 'high': self.safe_string(ticker, 'high24hr'), + 'low': self.safe_string(ticker, 'low24hr'), + 'bid': None, + 'bidVolume': None, + 'ask': None, + 'askVolume': None, + 'vwap': None, + 'open': None, + 'close': self.safe_string(ticker, 'lastPrice'), + 'last': self.safe_string(ticker, 'lastPrice'), + 'previousClose': None, + 'change': None, + 'percentage': self.safe_string(ticker, 'priceChange24hr'), + 'average': None, + 'baseVolume': self.safe_string(ticker, 'volume24hr'), + 'quoteVolume': None, + 'info': ticker, + }, market) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/public/get_ticker_data.md + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + response = await self.publicGetTickersPair(self.extend(request, params)) + ticker = self.safe_dict(response, 'data', {}) + # + # { + # "data":{ + # "pair":"btc_twd", + # "lastPrice":"1182449.00000000", + # "isBuyer":false, + # "priceChange24hr":"-1.99", + # "volume24hr":"9.13089740", + # "high24hr":"1226097.00000000", + # "low24hr":"1181000.00000000" + # } + # } + # + return self.parse_ticker(ticker, market) + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/public/get_ticker_data.md + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + response = await self.publicGetTickers() + tickers = self.safe_list(response, 'data', []) + # + # { + # "data":[ + # { + # "pair":"xrp_twd", + # "lastPrice":"21.26110000", + # "isBuyer":false, + # "priceChange24hr":"-6.53", + # "volume24hr":"102846.47084802", + # "high24hr":"23.24460000", + # "low24hr":"21.13730000" + # } + # ] + # } + # + return self.parse_tickers(tickers, symbols) + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/public/get_orderbook_data.md + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + if limit is not None: + request['limit'] = limit + response = await self.publicGetOrderBookPair(self.extend(request, params)) + # + # { + # "bids":[ + # { + # "price":"1175271", + # "amount":"0.00022804", + # "count":1, + # "total":"0.00022804" + # } + # ], + # "asks":[ + # { + # "price":"1176906", + # "amount":"0.0496", + # "count":1, + # "total":"0.0496" + # } + # ] + # } + # + return self.parse_order_book(response, market['symbol'], None, 'bids', 'asks', 'price', 'amount') + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades + # { + # "timestamp":1644651458, + # "price":"1180785.00000000", + # "amount":"0.00020000", + # "isBuyer":false + # } + # + # fetchMyTrades + # { + # "tradeId":"5685030251", + # "orderId":"9669168142", + # "price":"11821.8", + # "action":"SELL", + # "baseAmount":"0.01", + # "quoteAmount":"118.218", + # "fee":"0.236436", + # "feeSymbol":"BNB", + # "isTaker":true, + # "timestamp":1644905714862, + # "createdTimestamp":1644905714862 + # } + # + id = self.safe_string(trade, 'tradeId') + orderId = self.safe_string(trade, 'orderId') + timestamp = None + if id is None: + timestamp = self.safe_timestamp(trade, 'timestamp') + else: + timestamp = self.safe_integer(trade, 'timestamp') + marketId = self.safe_string(trade, 'pair') + market = self.safe_market(marketId, market) + symbol = self.safe_string(market, 'symbol') + price = self.safe_string(trade, 'price') + type = self.safe_string_lower(trade, 'type') + side = self.safe_string_lower(trade, 'action') + if side is None: + isBuyer = self.safe_bool(trade, 'isBuyer') + if isBuyer: + side = 'buy' + else: + side = 'sell' + amount = self.safe_string(trade, 'amount') + if amount is None: + amount = self.safe_string(trade, 'baseAmount') + fee = None + feeAmount = self.safe_string(trade, 'fee') + feeSymbol = self.safe_currency_code(self.safe_string(trade, 'feeSymbol')) + if feeAmount is not None: + fee = { + 'cost': feeAmount, + 'currency': feeSymbol, + 'rate': None, + } + isTaker = self.safe_bool(trade, 'isTaker') + takerOrMaker = None + if isTaker is not None: + if isTaker: + takerOrMaker = 'taker' + else: + takerOrMaker = 'maker' + return self.safe_trade({ + 'id': id, + 'info': trade, + 'order': orderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'takerOrMaker': takerOrMaker, + 'type': type, + 'side': side, + 'price': price, + 'amount': amount, + 'cost': None, + 'fee': fee, + }, market) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/public/get_trades_data.md + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + response = await self.publicGetTradesPair(self.extend(request, params)) + trades = self.safe_list(response, 'data', []) + # + # { + # "data":[ + # { + # "timestamp":1644651458, + # "price":"1180785.00000000", + # "amount":"0.00020000", + # "isBuyer":false + # } + # ] + # } + # + return self.parse_trades(trades, market, since, limit) + + async def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/public/get_limitations_and_fees.md + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + await self.load_markets() + response = await self.publicGetProvisioningLimitationsAndFees(params) + tradingFeeRate = self.safe_dict(response, 'tradingFeeRate', {}) + first = self.safe_value(tradingFeeRate, 0) + # + # { + # "tradingFeeRate":[ + # { + # "rank":0, + # "twdVolumeSymbol":"\u003c", + # "twdVolume":"3000000", + # "bitoAmountSymbol":"\u003c", + # "bitoAmount":"7500", + # "makerFee":"0.001", + # "takerFee":"0.002", + # "makerBitoFee":"0.0008", + # "takerBitoFee":"0.0016" + # } + # ], + # "orderFeesAndLimitations":[ + # { + # "pair":"BTC/TWD", + # "minimumOrderAmount":"0.0001", + # "minimumOrderAmountBase":"BTC", + # "minimumOrderNumberOfDigits":"0" + # } + # ], + # "restrictionsOfWithdrawalFees":[ + # { + # "currency":"TWD", + # "fee":"15", + # "minimumTradingAmount":"100", + # "maximumTradingAmount":"1000000", + # "dailyCumulativeMaximumAmount":"2000000", + # "remarks":"", + # "protocol":"" + # } + # ], + # "cryptocurrencyDepositFeeAndConfirmation":[ + # { + # "currency":"TWD", + # "generalDepositFees":"0", + # "blockchainConfirmationRequired":"" + # } + # ], + # "ttCheckFeesAndLimitationsLevel1":[ + # { + # "currency":"TWD", + # "redeemDailyCumulativeMaximumAmount":"", + # "generateMinimumTradingAmount":"", + # "generateMaximumTradingAmount":"", + # "generateDailyCumulativeMaximumAmount":"" + # } + # ], + # "ttCheckFeesAndLimitationsLevel2":[ + # { + # "currency":"TWD", + # "redeemDailyCumulativeMaximumAmount":"20000000", + # "generateMinimumTradingAmount":"30", + # "generateMaximumTradingAmount":"10000000", + # "generateDailyCumulativeMaximumAmount":"10000000" + # } + # ] + # } + # + result: dict = {} + maker = self.safe_number(first, 'makerFee') + taker = self.safe_number(first, 'takerFee') + for i in range(0, len(self.symbols)): + symbol = self.symbols[i] + result[symbol] = { + 'info': first, + 'symbol': symbol, + 'maker': maker, + 'taker': taker, + 'percentage': True, + 'tierBased': True, + } + return result + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + return [ + self.safe_integer(ohlcv, 'timestamp'), + self.safe_number(ohlcv, 'open'), + self.safe_number(ohlcv, 'high'), + self.safe_number(ohlcv, 'low'), + self.safe_number(ohlcv, 'close'), + self.safe_number(ohlcv, 'volume'), + ] + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/public/get_ohlc_data.md + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + resolution = self.safe_string(self.timeframes, timeframe, timeframe) + request: dict = { + 'pair': market['id'], + 'resolution': resolution, + } + # we need to have a limit argument because "to" and "from" are required + if limit is None: + limit = 500 + else: + limit = min(limit, 75000) # supports slightly more than 75k candles atm, but limit here to avoid errors + timeframeInSeconds = self.parse_timeframe(timeframe) + alignedSince = None + if since is None: + request['to'] = self.seconds() + request['from'] = request['to'] - (limit * timeframeInSeconds) + else: + timeframeInMilliseconds = timeframeInSeconds * 1000 + alignedSince = int(math.floor(since / timeframeInMilliseconds)) * timeframeInMilliseconds + request['from'] = int(math.floor(since / 1000)) + request['to'] = self.sum(request['from'], limit * timeframeInSeconds) + response = await self.publicGetTradingHistoryPair(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + # + # { + # "data":[ + # { + # "timestamp":1644581100000, + # "open":"1214737", + # "high":"1215110", + # "low":"1214737", + # "close":"1215110", + # "volume":"0.08423959" + # } + # ] + # } + # + sparse = self.parse_ohlcvs(data, market, timeframe, since, limit) + return self.insert_missing_candles(sparse, timeframeInSeconds, alignedSince, limit) + + def insert_missing_candles(self, candles, distance, since, limit): + # the exchange doesn't send zero volume candles so we emulate them instead + # otherwise sending a limit arg leads to unexpected results + length = len(candles) + if length == 0: + return candles + result = [] + copyFrom = candles[0] + timestamp = None + if since is None: + timestamp = copyFrom[0] + else: + timestamp = since + i = 0 + candleLength = len(candles) + resultLength = 0 + while((resultLength < limit) and (i < candleLength)): + candle = candles[i] + if candle[0] == timestamp: + result.append(candle) + i = self.sum(i, 1) + else: + copy = self.array_concat([], copyFrom) + copy[0] = timestamp + # set open, high, low to close + copy[1] = copy[4] + copy[2] = copy[4] + copy[3] = copy[4] + copy[5] = self.parse_number('0') + result.append(copy) + timestamp = self.sum(timestamp, distance * 1000) + resultLength = len(result) + copyFrom = result[resultLength - 1] + return result + + def parse_balance(self, response) -> Balances: + # + # [{ + # "currency":"twd", + # "amount":"0", + # "available":"0", + # "stake":"0", + # "tradable":true + # }] + # + result: dict = { + 'info': response, + } + for i in range(0, len(response)): + balance = response[i] + currencyId = self.safe_string(balance, 'currency') + code = self.safe_currency_code(currencyId) + amount = self.safe_string(balance, 'amount') + available = self.safe_string(balance, 'available') + account: dict = { + 'free': available, + 'total': amount, + } + result[code] = account + return self.safe_balance(result) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/private/get_account_balance.md + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + response = await self.privateGetAccountsBalance(params) + balances = self.safe_list(response, 'data', []) + # + # { + # "data":[ + # { + # "currency":"twd", + # "amount":"0", + # "available":"0", + # "stake":"0", + # "tradable":true + # } + # ] + # } + # + return self.parse_balance(balances) + + def parse_order_status(self, status: Str): + statuses: dict = { + '-1': 'open', + '0': 'open', + '1': 'open', + '2': 'closed', + '3': 'closed', + '4': 'canceled', + '6': 'canceled', + } + return self.safe_string(statuses, status, None) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder + # { + # "orderId": "2220595581", + # "timestamp": "1644896744886", + # "action": "SELL", + # "amount": "0.01", + # "price": "15000", + # "timeInForce": "GTC" + # } + # + # fetchOrder + # { + # "id":"8777138788", + # "pair":"bnb_twd", + # "price":"16000", + # "avgExecutionPrice":"0", + # "action":"SELL", + # "type":"LIMIT", + # "timestamp":1644899002598, + # "status":4, + # "originalAmount":"0.01", + # "remainingAmount":"0.01", + # "executedAmount":"0", + # "fee":"0", + # "feeSymbol":"twd", + # "bitoFee":"0", + # "total":"0", + # "seq":"BNBTWD548774666", + # "timeInForce":"GTC", + # "createdTimestamp":1644898944074, + # "updatedTimestamp":1644899002598 + # } + # + id = self.safe_string_2(order, 'id', 'orderId') + timestamp = self.safe_integer_2(order, 'timestamp', 'createdTimestamp') + side = self.safe_string(order, 'action') + side = side.lower() + amount = self.safe_string_2(order, 'amount', 'originalAmount') + price = self.safe_string(order, 'price') + marketId = self.safe_string(order, 'pair') + market = self.safe_market(marketId, market, '_') + symbol = self.safe_string(market, 'symbol') + orderStatus = self.safe_string(order, 'status') + status = self.parse_order_status(orderStatus) + type = self.safe_string_lower(order, 'type') + average = self.safe_string(order, 'avgExecutionPrice') + filled = self.safe_string(order, 'executedAmount') + remaining = self.safe_string(order, 'remainingAmount') + timeInForce = self.safe_string(order, 'timeInForce') + postOnly = None + if timeInForce == 'POST_ONLY': + postOnly = True + fee = None + feeAmount = self.safe_string(order, 'fee') + feeSymbol = self.safe_currency_code(self.safe_string(order, 'feeSymbol')) + if Precise.string_gt(feeAmount, '0'): + fee = { + 'currency': feeSymbol, + 'cost': feeAmount, + } + return self.safe_order({ + 'id': id, + 'clientOrderId': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': self.safe_integer(order, 'updatedTimestamp'), + 'symbol': symbol, + 'type': type, + 'timeInForce': timeInForce, + 'postOnly': postOnly, + 'side': side, + 'price': price, + 'triggerPrice': None, + 'amount': amount, + 'cost': None, + 'average': average, + 'filled': filled, + 'remaining': remaining, + 'status': status, + 'fee': fee, + 'trades': None, + 'info': order, + }, market) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/private/create_an_order.md + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param dict [params.triggerPrice]: the price at which a trigger order is triggered at + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'type': type, + 'pair': market['id'], + 'action': side, + 'amount': self.amount_to_precision(symbol, amount), + 'timestamp': self.milliseconds(), + } + orderType = type.upper() + if orderType == 'LIMIT': + request['price'] = self.price_to_precision(symbol, price) + if orderType == 'STOP_LIMIT': + request['price'] = self.price_to_precision(symbol, price) + triggerPrice = self.safe_value_2(params, 'triggerPrice', 'stopPrice') + params = self.omit(params, ['triggerPrice', 'stopPrice']) + if triggerPrice is None: + raise InvalidOrder(self.id + ' createOrder() requires a triggerPrice parameter for ' + orderType + ' orders') + else: + request['stopPrice'] = self.price_to_precision(symbol, triggerPrice) + condition = self.safe_string(params, 'condition') + if condition is None: + raise InvalidOrder(self.id + ' createOrder() requires a condition parameter for ' + orderType + ' orders') + else: + request['condition'] = condition + postOnly = self.is_post_only(orderType == 'MARKET', None, params) + if postOnly: + request['timeInForce'] = 'POST_ONLY' + response = await self.privatePostOrdersPair(self.extend(request, params)) + # + # { + # "orderId": "2220595581", + # "timestamp": "1644896744886", + # "action": "SELL", + # "amount": "0.01", + # "price": "15000", + # "timeInForce": "GTC" + # } + # + return self.parse_order(response, market) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/private/cancel_an_order.md + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'id': id, + 'pair': market['id'], + } + response = await self.privateDeleteOrdersPairId(self.extend(request, params)) + # + # { + # "orderId":"8777138788", + # "action":"SELL", + # "timestamp":1644899002465, + # "price":"16000", + # "amount":"0.01" + # } + # + return self.parse_order(response, market) + + def parse_cancel_orders(self, data): + dataKeys = list(data.keys()) + orders = [] + for i in range(0, len(dataKeys)): + marketId = dataKeys[i] + orderIds = data[marketId] + for j in range(0, len(orderIds)): + orders.append(self.safe_order({ + 'info': orderIds[j], + 'id': orderIds[j], + 'symbol': self.safe_symbol(marketId), + })) + return orders + + async def cancel_orders(self, ids: List[str], symbol: Str = None, params={}) -> List[Order]: + """ + cancel multiple orders + + https://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/private/cancel_batch_orders.md + + :param str[] ids: order ids + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrders() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + id = market['uppercaseId'] + request: dict = {} + request[id] = ids + response = await self.privatePutOrders(self.extend(request, params)) + # + # { + # "data":{ + # "BNB_TWD":[ + # "5236347105", + # "359488711" + # ] + # } + # } + # + data = self.safe_dict(response, 'data') + return self.parse_cancel_orders(data) + + async def cancel_all_orders(self, symbol: Str = None, params={}) -> List[Order]: + """ + cancel all open orders + + https://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/private/cancel_all_orders.md + + :param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + request: dict = { + # 'pair': market['id'], # optional + } + response = None + if symbol is not None: + market = self.market(symbol) + request['pair'] = market['id'] + response = await self.privateDeleteOrdersPair(self.extend(request, params)) + else: + response = await self.privateDeleteOrdersAll(self.extend(request, params)) + data = self.safe_dict(response, 'data', {}) + # + # { + # "data":{ + # "BNB_TWD":[ + # "9515988421", + # "4639130027" + # ] + # } + # } + # + return self.parse_cancel_orders(data) + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/private/get_an_order_data.md + + :param str id: the order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrder() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'orderId': id, + 'pair': market['id'], + } + response = await self.privateGetOrdersPairOrderId(self.extend(request, params)) + # + # { + # "id":"8777138788", + # "pair":"bnb_twd", + # "price":"16000", + # "avgExecutionPrice":"0", + # "action":"SELL", + # "type":"LIMIT", + # "timestamp":1644899002598, + # "status":4, + # "originalAmount":"0.01", + # "remainingAmount":"0.01", + # "executedAmount":"0", + # "fee":"0", + # "feeSymbol":"twd", + # "bitoFee":"0", + # "total":"0", + # "seq":"BNBTWD548774666", + # "timeInForce":"GTC", + # "createdTimestamp":1644898944074, + # "updatedTimestamp":1644899002598 + # } + # + return self.parse_order(response, market) + + async def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/private/get_orders_data.md + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrders() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + # 'startTimestamp': 0, + # 'endTimestamp': 0, + # 'statusKind': '', + # 'orderId': '', + } + if since is not None: + request['startTimestamp'] = since + if limit is not None: + request['limit'] = limit + response = await self.privateGetOrdersAllPair(self.extend(request, params)) + orders = self.safe_list(response, 'data', []) + if orders is None: + orders = [] + # + # { + # "data":[ + # { + # "id":"2220595581", + # "pair":"bnb_twd", + # "price":"15000", + # "avgExecutionPrice":"0", + # "action":"SELL", + # "type":"LIMIT", + # "createdTimestamp":1644896744886, + # "updatedTimestamp":1644898706236, + # "status":4, + # "originalAmount":"0.01", + # "remainingAmount":"0.01", + # "executedAmount":"0", + # "fee":"0", + # "feeSymbol":"twd", + # "bitoFee":"0", + # "total":"0", + # "seq":"BNBTWD8540871774", + # "timeInForce":"GTC" + # } + # ] + # } + # + return self.parse_orders(orders, market, since, limit) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all unfilled currently open orders + + https://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/private/get_open_orders_data.md + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['pair'] = market['id'] + response = await self.privateGetOrdersOpen(self.extend(request, params)) + orders = self.safe_list(response, 'data', []) + return self.parse_orders(orders, market, since, limit) + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/private/get_orders_data.md + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + request: dict = { + 'statusKind': 'DONE', + } + return self.fetch_orders(symbol, since, limit, self.extend(request, params)) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/private/get_trades_data.md + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMyTrades() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + response = await self.privateGetOrdersTradesPair(self.extend(request, params)) + trades = self.safe_list(response, 'data', []) + # + # { + # "data":[ + # { + # "tradeId":"5685030251", + # "orderId":"9669168142", + # "price":"11821.8", + # "action":"SELL", + # "baseAmount":"0.01", + # "quoteAmount":"118.218", + # "fee":"0.236436", + # "feeSymbol":"BNB", + # "isTaker":true, + # "timestamp":1644905714862, + # "createdTimestamp":1644905714862 + # } + # ] + # } + # + return self.parse_trades(trades, market, since, limit) + + def parse_transaction_status(self, status: Str): + states: dict = { + 'COMPLETE': 'ok', + 'INVALID': 'failed', + 'PROCESSING': 'pending', + 'WAIT_PROCESS': 'pending', + 'FAILED': 'failed', + 'EXPIRED': 'failed', + 'CANCELLED': 'failed', + 'EMAIL_VERIFICATION': 'pending', + 'WAIT_CONFIRMATION': 'pending', + } + return self.safe_string(states, status, status) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fetchDeposits + # + # { + # "serial": "20220214X766799", + # "timestamp": "1644833015053", + # "address": "bnb1xml62k5a9dcewgc542fha75fyxdcp0zv8eqfsh", + # "amount": "0.20000000", + # "fee": "0.00000000", + # "total": "0.20000000", + # "status": "COMPLETE", + # "txid": "A3CC4F6828CC752B9F3737F48B5826B9EC2857040CB5141D0CC955F7E53DB6D9", + # "message": "778553959", + # "protocol": "MAIN", + # "id": "2905906537" + # } + # + # fetchWithdrawals or fetchWithdraw + # + # { + # "serial": "20220215BW14069838", + # "timestamp": "1644907716044", + # "address": "TKrwMaZaGiAvtXCFT41xHuusNcs4LPWS7w", + # "amount": "8.00000000", + # "fee": "2.00000000", + # "total": "10.00000000", + # "status": "COMPLETE", + # "txid": "50bf250c71a582f40cf699fb58bab978437ea9bdf7259ff8072e669aab30c32b", + # "protocol": "TRX", + # "id": "9925310345" + # } + # + # withdraw + # + # { + # "serial": "20220215BW14069838", + # "currency": "USDT", + # "protocol": "TRX", + # "address": "TKrwMaZaGiAvtXCFT41xHuusNcs4LPWS7w", + # "amount": "8", + # "fee": "2", + # "total": "10" + # } + # + currencyId = self.safe_string(transaction, 'coin') + code = self.safe_currency_code(currencyId, currency) + timestamp = self.safe_integer(transaction, 'timestamp') + address = self.safe_string(transaction, 'address') + tag = self.safe_string(transaction, 'message') + status = self.safe_string(transaction, 'status') + networkId = self.safe_string(transaction, 'protocol') + if networkId == 'MAIN': + networkId = code + return { + 'info': transaction, + 'id': self.safe_string(transaction, 'serial'), + 'txid': self.safe_string(transaction, 'txid'), + 'type': None, + 'currency': code, + 'network': self.network_id_to_code(networkId), + 'amount': self.safe_number(transaction, 'total'), + 'status': self.parse_transaction_status(status), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'address': address, + 'addressFrom': None, + 'addressTo': address, + 'tag': tag, + 'tagFrom': None, + 'tagTo': tag, + 'updated': None, + 'comment': None, + 'internal': None, + 'fee': { + 'currency': code, + 'cost': self.safe_number(transaction, 'fee'), + 'rate': None, + }, + } + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/private/get_deposit_invoices_data.md + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + if code is None: + raise ArgumentsRequired(self.id + ' fetchDeposits() requires the code argument') + await self.load_markets() + currency = self.safe_currency(code) + request: dict = { + 'currency': currency['id'], + # 'endTimestamp': 0, + # 'id': '', + # 'statuses': '', # 'ROCESSING,COMPLETE,INVALID,WAIT_PROCESS,CANCELLED,FAILED' + } + if since is not None: + request['startTimestamp'] = since + if limit is not None: + request['limit'] = limit + response = await self.privateGetWalletDepositHistoryCurrency(self.extend(request, params)) + result = self.safe_list(response, 'data', []) + # + # { + # "data":[ + # { + # "serial":"20220214X766799", + # "timestamp":"1644833015053", + # "address":"bnb1xml62k5a9dcewgc542fha75fyxdcp0zv8eqfsh", + # "amount":"0.20000000", + # "fee":"0.00000000", + # "total":"0.20000000", + # "status":"COMPLETE", + # "txid":"A3CC4F6828CC752B9F3737F48B5826B9EC2857040CB5141D0CC955F7E53DB6D9", + # "message":"778553959", + # "protocol":"MAIN", + # "id":"2905906537" + # } + # ] + # } + # + return self.parse_transactions(result, currency, since, limit, {'type': 'deposit'}) + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/private/get_withdraw_invoices_data.md + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + if code is None: + raise ArgumentsRequired(self.id + ' fetchWithdrawals() requires the code argument') + await self.load_markets() + currency = self.safe_currency(code) + request: dict = { + 'currency': currency['id'], + # 'endTimestamp': 0, + # 'id': '', + # 'statuses': '', # 'PROCESSING,COMPLETE,EXPIRED,INVALID,WAIT_PROCESS,WAIT_CONFIRMATION,EMAIL_VERIFICATION,CANCELLED' + } + if since is not None: + request['startTimestamp'] = since + if limit is not None: + request['limit'] = limit + response = await self.privateGetWalletWithdrawHistoryCurrency(self.extend(request, params)) + result = self.safe_list(response, 'data', []) + # + # { + # "data":[ + # { + # "serial":"20220215BW14069838", + # "timestamp":"1644907716044", + # "address":"TKrwMaZaGiAvtXCFT41xHuusNcs4LPWS7w", + # "amount":"8.00000000", + # "fee":"2.00000000", + # "total":"10.00000000", + # "status":"COMPLETE", + # "txid":"50bf250c71a582f40cf699fb58bab978437ea9bdf7259ff8072e669aab30c32b", + # "protocol":"TRX", + # "id":"9925310345" + # } + # ] + # } + # + return self.parse_transactions(result, currency, since, limit, {'type': 'withdrawal'}) + + async def fetch_withdrawal(self, id: str, code: Str = None, params={}): + """ + fetch data on a currency withdrawal via the withdrawal id + + https://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/private/get_an_withdraw_invoice_data.md + + :param str id: withdrawal id + :param str code: unified currency code of the currency withdrawn, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + if code is None: + raise ArgumentsRequired(self.id + ' fetchWithdrawal() requires the code argument') + await self.load_markets() + currency = self.safe_currency(code) + request: dict = { + 'serial': id, + 'currency': currency['id'], + } + response = await self.privateGetWalletWithdrawCurrencySerial(self.extend(request, params)) + result = self.safe_dict(response, 'data', {}) + # + # { + # "data":{ + # "serial":"20220215BW14069838", + # "address":"TKrwMaZaGiAvtXCFT41xHuusNcs4LPWS7w", + # "amount":"8.00000000", + # "fee":"2.00000000", + # "total":"10.00000000", + # "status":"COMPLETE", + # "txid":"50bf250c71a582f40cf699fb58bab978437ea9bdf7259ff8072e669aab30c32b", + # "protocol":"TRX", + # "id":"9925310345", + # "timestamp":"1644907716044" + # } + # } + # + return self.parse_transaction(result, currency) + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/private/create_an_withdraw_invoice.md + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + await self.load_markets() + self.check_address(address) + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + 'amount': self.number_to_string(amount), + 'address': address, + } + if 'network' in params: + networks = self.safe_dict(self.options, 'networks', {}) + requestedNetwork = self.safe_string_upper(params, 'network') + params = self.omit(params, ['network']) + networkId = self.safe_string(networks, requestedNetwork) + if networkId is None: + raise ExchangeError(self.id + ' invalid network ' + requestedNetwork) + request['protocol'] = networkId + if tag is not None: + request['message'] = tag + response = await self.privatePostWalletWithdrawCurrency(self.extend(request, params)) + result = self.safe_dict(response, 'data', {}) + # + # { + # "data":{ + # "serial":"20220215BW14069838", + # "currency":"USDT", + # "protocol":"TRX", + # "address":"TKrwMaZaGiAvtXCFT41xHuusNcs4LPWS7w", + # "amount":"8", + # "fee":"2", + # "total":"10" + # } + # } + # + return self.parse_transaction(result, currency) + + def parse_deposit_withdraw_fee(self, fee, currency: Currency = None): + # { + # "currency":"eth", + # "withdrawFee":"0.007", + # "minWithdraw":"0.001", + # "maxWithdraw":"1000", + # "maxDailyWithdraw":"2000", + # "withdraw":true, + # "deposit":true, + # "depositConfirmation":"12" + # } + return { + 'info': fee, + 'withdraw': { + 'fee': self.safe_number(fee, 'withdrawFee'), + 'percentage': False, + }, + 'deposit': { + 'fee': None, + 'percentage': None, + }, + 'networks': {}, + } + + async def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}): + """ + fetch deposit and withdraw fees + + https://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/public/get_currency_info.md + + :param str[]|None codes: list of unified currency codes + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `fee structures ` + """ + await self.load_markets() + response = await self.publicGetProvisioningCurrencies(params) + # + # { + # "data":[ + # { + # "currency":"eth", + # "withdrawFee":"0.007", + # "minWithdraw":"0.001", + # "maxWithdraw":"1000", + # "maxDailyWithdraw":"2000", + # "withdraw":true, + # "deposit":true, + # "depositConfirmation":"12" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_deposit_withdraw_fees(data, codes, 'currency') + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = '/' + self.implode_params(path, params) + query = self.omit(params, self.extract_params(path)) + if headers is None: + headers = {} + headers['X-BITOPRO-API'] = 'ccxt' + if api == 'private': + self.check_required_credentials() + if method == 'POST' or method == 'PUT': + body = self.json(params) + payload = self.string_to_base64(body) + signature = self.hmac(self.encode(payload), self.encode(self.secret), hashlib.sha384) + headers['X-BITOPRO-APIKEY'] = self.apiKey + headers['X-BITOPRO-PAYLOAD'] = payload + headers['X-BITOPRO-SIGNATURE'] = signature + elif method == 'GET' or method == 'DELETE': + if query: + url += '?' + self.urlencode(query) + nonce = self.milliseconds() + rawData: dict = { + 'nonce': nonce, + } + data = self.json(rawData) + payload = self.string_to_base64(data) + signature = self.hmac(self.encode(payload), self.encode(self.secret), hashlib.sha384) + headers['X-BITOPRO-APIKEY'] = self.apiKey + headers['X-BITOPRO-PAYLOAD'] = payload + headers['X-BITOPRO-SIGNATURE'] = signature + elif api == 'public' and method == 'GET': + if query: + url += '?' + self.urlencode(query) + url = self.urls['api']['rest'] + url + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None # fallback to the default error handler + if code >= 200 and code < 300: + return None + feedback = self.id + ' ' + body + error = self.safe_string(response, 'error') + self.throw_exactly_matched_exception(self.exceptions['exact'], error, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], error, feedback) + raise ExchangeError(feedback) # unknown message diff --git a/ccxt/async_support/bitrue.py b/ccxt/async_support/bitrue.py new file mode 100644 index 0000000..d2a2dd5 --- /dev/null +++ b/ccxt/async_support/bitrue.py @@ -0,0 +1,3196 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.bitrue import ImplicitAPI +import asyncio +import hashlib +import json +from ccxt.base.types import Any, Balances, Currencies, Currency, Int, MarginModification, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, Transaction, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import AccountSuspended +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import OrderImmediatelyFillable +from ccxt.base.errors import NotSupported +from ccxt.base.errors import DDoSProtection +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.errors import OnMaintenance +from ccxt.base.errors import InvalidNonce +from ccxt.base.decimal_to_precision import TRUNCATE +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class bitrue(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(bitrue, self).describe(), { + 'id': 'bitrue', + 'name': 'Bitrue', + 'countries': ['SG'], # Singapore, Malta + 'rateLimit': 10, + 'certified': False, + 'version': 'v1', + 'pro': True, + # new metainfo interface + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': True, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createMarketBuyOrderWithCost': True, + 'createMarketOrderWithCost': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createOrderWithTakeProfitAndStopLoss': False, + 'createOrderWithTakeProfitAndStopLossWs': False, + 'createReduceOnlyOrder': True, + 'createStopLimitOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'fetchBalance': True, + 'fetchBidsAsks': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': False, + 'fetchDeposits': True, + 'fetchDepositsWithdrawals': False, + 'fetchDepositWithdrawFee': 'emulated', + 'fetchDepositWithdrawFees': True, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': False, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchStatus': True, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': False, + 'fetchTransactionFees': False, + 'fetchTransactions': False, + 'fetchTransfers': True, + 'fetchVolatilityHistory': False, + 'fetchWithdrawals': True, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'setLeverage': True, + 'setMargin': True, + 'setMarginMode': False, + 'setPositionMode': False, + 'transfer': True, + 'withdraw': True, + }, + 'timeframes': { + '1m': '1m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1H', + '2h': '2H', + '4h': '4H', + '1d': '1D', + '1w': '1W', + }, + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/67abe346-1273-461a-bd7c-42fa32907c8e', + 'api': { + 'spot': 'https://www.bitrue.com/api', + 'fapi': 'https://fapi.bitrue.com/fapi', + 'dapi': 'https://fapi.bitrue.com/dapi', + 'kline': 'https://www.bitrue.com/kline-api', + }, + 'www': 'https://www.bitrue.com', + 'referral': 'https://www.bitrue.com/affiliate/landing?cn=600000&inviteCode=EZWETQE', + 'doc': [ + 'https://github.com/Bitrue-exchange/bitrue-official-api-docs', + 'https://www.bitrue.com/api-docs', + ], + 'fees': 'https://bitrue.zendesk.com/hc/en-001/articles/4405479952537', + }, + # from spotV1PublicGetExchangeInfo: + # general 25000 weight in 1 minute per IP. = 416.66 per second a weight of 0.24 for 1 + # orders 750 weight in 6 seconds per IP. = 125 per second a weight of 0.8 for 1 + # orders 200 weight in 10 seconds per User. = 20 per second a weight of 5 for 1 + # withdraw 3000 weight in 1 hour per User. = 0.833 per second a weight of 120 for 1 + # withdraw 1000 weight in 1 day per User. = 0.011574 per second a weight of 8640 for 1 + 'api': { + 'spot': { + 'kline': { + 'public': { + 'get': { + 'public.json': 0.24, + 'public{currency}.json': 0.24, + }, + }, + }, + 'v1': { + 'public': { + 'get': { + 'ping': 0.24, + 'time': 0.24, + 'exchangeInfo': 0.24, + 'depth': {'cost': 1, 'byLimit': [[100, 0.24], [500, 1.2], [1000, 2.4]]}, + 'trades': 0.24, + 'historicalTrades': 1.2, + 'aggTrades': 0.24, + 'ticker/24hr': {'cost': 0.24, 'noSymbol': 9.6}, + 'ticker/price': 0.24, + 'ticker/bookTicker': 0.24, + 'market/kline': 0.24, + }, + }, + 'private': { + 'get': { + 'order': 5, + 'openOrders': 5, + 'allOrders': 25, + 'account': 25, + 'myTrades': 25, + 'etf/net-value/{symbol}': 0.24, + 'withdraw/history': 120, + 'deposit/history': 120, + }, + 'post': { + 'order': 5, + 'withdraw/commit': 120, + }, + 'delete': { + 'order': 5, + }, + }, + }, + 'v2': { + 'private': { + 'get': { + 'myTrades': 1.2, + }, + }, + }, + }, + 'fapi': { + 'v1': { + 'public': { + 'get': { + 'ping': 0.24, + 'time': 0.24, + 'contracts': 0.24, + 'depth': 0.24, + 'ticker': 0.24, + 'klines': 0.24, + }, + }, + }, + 'v2': { + 'private': { + 'get': { + 'myTrades': 5, + 'openOrders': 5, + 'order': 5, + 'account': 5, + 'leverageBracket': 5, + 'commissionRate': 5, + 'futures_transfer_history': 5, + 'forceOrdersHistory': 5, + }, + 'post': { + 'positionMargin': 5, + 'level_edit': 5, + 'cancel': 5, + 'order': 25, + 'allOpenOrders': 5, + 'futures_transfer': 5, + }, + }, + }, + }, + 'dapi': { + 'v1': { + 'public': { + 'get': { + 'ping': 0.24, + 'time': 0.24, + 'contracts': 0.24, + 'depth': 0.24, + 'ticker': 0.24, + 'klines': 0.24, + }, + }, + }, + 'v2': { + 'private': { + 'get': { + 'myTrades': 5, + 'openOrders': 5, + 'order': 5, + 'account': 5, + 'leverageBracket': 5, + 'commissionRate': 5, + 'futures_transfer_history': 5, + 'forceOrdersHistory': 5, + }, + 'post': { + 'positionMargin': 5, + 'level_edit': 5, + 'cancel': 5, + 'order': 5, + 'allOpenOrders': 5, + 'futures_transfer': 5, + }, + }, + }, + }, + }, + 'fees': { + 'trading': { + 'feeSide': 'get', + 'tierBased': False, + 'percentage': True, + 'taker': self.parse_number('0.00098'), + 'maker': self.parse_number('0.00098'), + }, + 'future': { + 'trading': { + 'feeSide': 'quote', + 'tierBased': True, + 'percentage': True, + 'taker': self.parse_number('0.000400'), + 'maker': self.parse_number('0.000200'), + 'tiers': { + 'taker': [ + [self.parse_number('0'), self.parse_number('0.000400')], + [self.parse_number('250'), self.parse_number('0.000400')], + [self.parse_number('2500'), self.parse_number('0.000350')], + [self.parse_number('7500'), self.parse_number('0.000320')], + [self.parse_number('22500'), self.parse_number('0.000300')], + [self.parse_number('50000'), self.parse_number('0.000270')], + [self.parse_number('100000'), self.parse_number('0.000250')], + [self.parse_number('200000'), self.parse_number('0.000220')], + [self.parse_number('400000'), self.parse_number('0.000200')], + [self.parse_number('750000'), self.parse_number('0.000170')], + ], + 'maker': [ + [self.parse_number('0'), self.parse_number('0.000200')], + [self.parse_number('250'), self.parse_number('0.000160')], + [self.parse_number('2500'), self.parse_number('0.000140')], + [self.parse_number('7500'), self.parse_number('0.000120')], + [self.parse_number('22500'), self.parse_number('0.000100')], + [self.parse_number('50000'), self.parse_number('0.000080')], + [self.parse_number('100000'), self.parse_number('0.000060')], + [self.parse_number('200000'), self.parse_number('0.000040')], + [self.parse_number('400000'), self.parse_number('0.000020')], + [self.parse_number('750000'), self.parse_number('0')], + ], + }, + }, + }, + 'delivery': { + 'trading': { + 'feeSide': 'base', + 'tierBased': True, + 'percentage': True, + 'taker': self.parse_number('0.000500'), + 'maker': self.parse_number('0.000100'), + 'tiers': { + 'taker': [ + [self.parse_number('0'), self.parse_number('0.000500')], + [self.parse_number('250'), self.parse_number('0.000450')], + [self.parse_number('2500'), self.parse_number('0.000400')], + [self.parse_number('7500'), self.parse_number('0.000300')], + [self.parse_number('22500'), self.parse_number('0.000250')], + [self.parse_number('50000'), self.parse_number('0.000240')], + [self.parse_number('100000'), self.parse_number('0.000240')], + [self.parse_number('200000'), self.parse_number('0.000240')], + [self.parse_number('400000'), self.parse_number('0.000240')], + [self.parse_number('750000'), self.parse_number('0.000240')], + ], + 'maker': [ + [self.parse_number('0'), self.parse_number('0.000100')], + [self.parse_number('250'), self.parse_number('0.000080')], + [self.parse_number('2500'), self.parse_number('0.000050')], + [self.parse_number('7500'), self.parse_number('0.0000030')], + [self.parse_number('22500'), self.parse_number('0')], + [self.parse_number('50000'), self.parse_number('-0.000050')], + [self.parse_number('100000'), self.parse_number('-0.000060')], + [self.parse_number('200000'), self.parse_number('-0.000070')], + [self.parse_number('400000'), self.parse_number('-0.000080')], + [self.parse_number('750000'), self.parse_number('-0.000090')], + ], + }, + }, + }, + }, + # exchange-specific options + 'options': { + 'createMarketBuyOrderRequiresPrice': True, + 'fetchMarkets': { + 'types': ['spot', 'linear', 'inverse'], + }, + # 'fetchTradesMethod': 'publicGetAggTrades', # publicGetTrades, publicGetHistoricalTrades + 'fetchMyTradesMethod': 'v2PrivateGetMyTrades', # spotV1PrivateGetMyTrades + 'hasAlreadyAuthenticatedSuccessfully': False, + 'currencyToPrecisionRoundingMode': TRUNCATE, + 'recvWindow': 5 * 1000, # 5 sec, binance default + 'timeDifference': 0, # the difference between system clock and Binance clock + 'adjustForTimeDifference': False, # controls the adjustment logic upon instantiation + 'parseOrderToPrecision': False, # force amounts and costs in parseOrder to precision + 'newOrderRespType': { + 'market': 'FULL', # 'ACK' for order id, 'RESULT' for full order or 'FULL' for order with fills + 'limit': 'FULL', # we change it from 'ACK' by default to 'FULL'(returns immediately if limit is not hit) + }, + 'networks': { + 'ERC20': 'ETH', + 'TRC20': 'TRX', + 'AETERNITY': 'Aeternity', + 'AION': 'AION', + 'ALGO': 'Algorand', + 'ASK': 'ASK', + 'ATOM': 'ATOM', + 'AVAXC': 'AVAX C-Chain', + 'BCH': 'BCH', + 'BEP2': 'BEP2', + 'BEP20': 'BEP20', + 'Bitcoin': 'Bitcoin', + 'BRP20': 'BRP20', + 'ADA': 'Cardano', + 'CASINOCOIN': 'CasinoCoin', + 'CASINOCOIN-XRPL': 'CasinoCoin XRPL', + 'CONTENTOS': 'Contentos', + 'DASH': 'Dash', + 'DECOIN': 'Decoin', + 'DFI': 'DeFiChain', + 'DGB': 'DGB', + 'DIVI': 'Divi', + 'DOGE': 'dogecoin', + 'EOS': 'EOS', + 'ETC': 'ETC', + 'FILECOIN': 'Filecoin', + 'FREETON': 'FREETON', + 'HBAR': 'HBAR', + 'HEDERA': 'Hedera Hashgraph', + 'HRC20': 'HRC20', + 'ICON': 'ICON', + 'ICP': 'ICP', + 'IGNIS': 'Ignis', + 'INTERNETCOMPUTER': 'Internet Computer', + 'IOTA': 'IOTA', + 'KAVA': 'KAVA', + 'KSM': 'KSM', + 'LTC': 'LiteCoin', + 'LUNA': 'Luna', + 'MATIC': 'MATIC', + 'MOBILECOIN': 'Mobile Coin', + 'MONACOIN': 'MonaCoin', + 'XMR': 'Monero', + 'NEM': 'NEM', + 'NEP5': 'NEP5', + 'OMNI': 'OMNI', + 'PAC': 'PAC', + 'DOT': 'Polkadot', + 'RAVEN': 'Ravencoin', + 'SAFEX': 'Safex', + 'SOL': 'SOLANA', + 'SGB': 'Songbird', + 'XML': 'Stellar Lumens', + 'XYM': 'Symbol', + 'XTZ': 'Tezos', + 'theta': 'theta', + 'THETA': 'THETA', + 'VECHAIN': 'VeChain', + 'WANCHAIN': 'Wanchain', + 'XINFIN': 'XinFin Network', + 'XRP': 'XRP', + 'XRPL': 'XRPL', + 'ZIL': 'ZIL', + }, + 'defaultType': 'spot', + 'timeframes': { + 'spot': { + '1m': '1m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1H', + '2h': '2H', + '4h': '4H', + '12h': '12H', + '1d': '1D', + '1w': '1W', + }, + 'future': { + '1m': '1min', + '5m': '5min', + '15m': '15min', + '30m': '30min', + '1h': '1h', + '1d': '1day', + '1w': '1week', + '1M': '1month', + }, + }, + 'accountsByType': { + 'spot': 'wallet', + 'future': 'contract', + 'swap': 'contract', + 'funding': 'wallet', + 'fund': 'wallet', + 'contract': 'contract', + }, + }, + 'commonCurrencies': { + 'MIM': 'MIM Swarm', + }, + 'precisionMode': TICK_SIZE, + 'features': { + 'default': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerPriceType': None, + 'triggerDirection': None, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyRequiresPrice': True, # todo revise + 'marketBuyByCost': True, + 'selfTradePrevention': False, + 'iceberg': True, # todo implement + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 100000, + 'untilDays': 100000, + 'symbolRequired': True, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOrders': None, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 90, + 'daysBackCanceled': 1, + 'untilDays': 90, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOHLCV': { + 'limit': 1440, + }, + }, + 'spot': { + 'extends': 'default', + }, + 'forDerivatives': { + 'extends': 'default', + 'createOrder': { + 'marginMode': True, + 'leverage': True, + 'marketBuyRequiresPrice': False, + 'marketBuyByCost': False, + }, + 'fetchOHLCV': { + 'limit': 300, + }, + 'fetchClosedOrders': None, + }, + 'swap': { + 'linear': { + 'extends': 'forDerivatives', + }, + 'inverse': { + 'extends': 'forDerivatives', + }, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'exceptions': { + 'exact': { + 'System is under maintenance.': OnMaintenance, # {"code":1,"msg":"System is under maintenance."} + 'System abnormality': ExchangeError, # {"code":-1000,"msg":"System abnormality"} + 'You are not authorized to execute self request.': PermissionDenied, # {"msg":"You are not authorized to execute self request."} + 'API key does not exist': AuthenticationError, + 'Order would trigger immediately.': OrderImmediatelyFillable, + 'Stop price would trigger immediately.': OrderImmediatelyFillable, # {"code":-2010,"msg":"Stop price would trigger immediately."} + 'Order would immediately match and take.': OrderImmediatelyFillable, # {"code":-2010,"msg":"Order would immediately match and take."} + 'Account has insufficient balance for requested action.': InsufficientFunds, + 'Rest API trading is not enabled.': ExchangeNotAvailable, + "You don't have permission.": PermissionDenied, # {"msg":"You don't have permission.","success":false} + 'Market is closed.': ExchangeNotAvailable, # {"code":-1013,"msg":"Market is closed."} + 'Too many requests. Please try again later.': DDoSProtection, # {"msg":"Too many requests. Please try again later.","success":false} + '-1000': ExchangeNotAvailable, # {"code":-1000,"msg":"An unknown error occured while processing the request."} + '-1001': ExchangeNotAvailable, # 'Internal error; unable to process your request. Please try again.' + '-1002': AuthenticationError, # 'You are not authorized to execute self request.' + '-1003': RateLimitExceeded, # {"code":-1003,"msg":"Too much request weight used, current limit is 1200 request weight per 1 MINUTE. Please use the websocket for live updates to avoid polling the API."} + '-1013': InvalidOrder, # createOrder -> 'invalid quantity'/'invalid price'/MIN_NOTIONAL + '-1015': RateLimitExceeded, # 'Too many new orders; current limit is %s orders per %s.' + '-1016': ExchangeNotAvailable, # 'This service is no longer available.', + '-1020': BadRequest, # 'This operation is not supported.' + '-1021': InvalidNonce, # 'your time is ahead of server' + '-1022': AuthenticationError, # {"code":-1022,"msg":"Signature for self request is not valid."} + '-1100': BadRequest, # createOrder(symbol, 1, asdf) -> 'Illegal characters found in parameter 'price' + '-1101': BadRequest, # Too many parameters; expected %s and received %s. + '-1102': BadRequest, # Param %s or %s must be sent, but both were empty # {"code":-1102,"msg":"timestamp IllegalArgumentException.","data":null} + '-1103': BadRequest, # An unknown parameter was sent. + '-1104': BadRequest, # Not all sent parameters were read, read 8 parameters but was sent 9 + '-1105': BadRequest, # Parameter %s was empty. + '-1106': BadRequest, # Parameter %s sent when not required. + '-1111': BadRequest, # Precision is over the maximum defined for self asset. + '-1112': InvalidOrder, # No orders on book for symbol. + '-1114': BadRequest, # TimeInForce parameter sent when not required. + '-1115': BadRequest, # Invalid timeInForce. + '-1116': BadRequest, # Invalid orderType. + '-1117': BadRequest, # Invalid side. + '-1166': InvalidOrder, # {"code":"-1166","msg":"The leverage value of the order is inconsistent with the user contract configuration 5","data":null} + '-1118': BadRequest, # New client order ID was empty. + '-1119': BadRequest, # Original client order ID was empty. + '-1120': BadRequest, # Invalid interval. + '-1121': BadSymbol, # Invalid symbol. + '-1125': AuthenticationError, # This listenKey does not exist. + '-1127': BadRequest, # More than %s hours between startTime and endTime. + '-1128': BadRequest, # {"code":-1128,"msg":"Combination of optional parameters invalid."} + '-1130': BadRequest, # Data sent for paramter %s is not valid. + '-1131': BadRequest, # recvWindow must be less than 60000 + '-1160': InvalidOrder, # {"code":"-1160","msg":"Minimum order amount 10","data":null} + '-1156': InvalidOrder, # {"code":"-1156","msg":"The number of closed positions exceeds the total number of positions","data":null} + '-2008': AuthenticationError, # {"code":-2008,"msg":"Invalid Api-Key ID."} + '-2010': ExchangeError, # generic error code for createOrder -> 'Account has insufficient balance for requested action.', {"code":-2010,"msg":"Rest API trading is not enabled."}, etc... + '-2011': OrderNotFound, # cancelOrder(1, 'BTC/USDT') -> 'UNKNOWN_ORDER' + '-2013': OrderNotFound, # fetchOrder(1, 'BTC/USDT') -> 'Order does not exist' + '-2014': AuthenticationError, # {"code":-2014, "msg": "API-key format invalid."} + '-2015': AuthenticationError, # "Invalid API-key, IP, or permissions for action." + '-2017': InsufficientFunds, # {code":"-2017","msg":"Insufficient balance","data":null} + '-2019': InsufficientFunds, # {"code":-2019,"msg":"Margin is insufficient."} + '-3005': InsufficientFunds, # {"code":-3005,"msg":"Transferring out not allowed. Transfer out amount exceeds max amount."} + '-3006': InsufficientFunds, # {"code":-3006,"msg":"Your borrow amount has exceed maximum borrow amount."} + '-3008': InsufficientFunds, # {"code":-3008,"msg":"Borrow not allowed. Your borrow amount has exceed maximum borrow amount."} + '-3010': ExchangeError, # {"code":-3010,"msg":"Repay not allowed. Repay amount exceeds borrow amount."} + '-3015': ExchangeError, # {"code":-3015,"msg":"Repay amount exceeds borrow amount."} + '-3022': AccountSuspended, # You account's trading is banned. + '-4028': BadRequest, # {"code":-4028,"msg":"Leverage 100 is not valid"} + '-3020': InsufficientFunds, # {"code":-3020,"msg":"Transfer out amount exceeds max amount."} + '-3041': InsufficientFunds, # {"code":-3041,"msg":"Balance is not enough"} + '-5013': InsufficientFunds, # Asset transfer failed: insufficient balance" + '-11008': InsufficientFunds, # {"code":-11008,"msg":"Exceeding the account's maximum borrowable limit."} + '-4051': InsufficientFunds, # {"code":-4051,"msg":"Isolated balance insufficient."} + }, + 'broad': { + 'Insufficient account balance': InsufficientFunds, # {"code":-2010,"msg":"Insufficient account balance.","data":null} + 'has no operation privilege': PermissionDenied, + 'MAX_POSITION': InvalidOrder, # {"code":-2010,"msg":"Filter failure: MAX_POSITION"} + }, + }, + }) + + def nonce(self): + return self.milliseconds() - self.options['timeDifference'] + + async def fetch_status(self, params={}): + """ + the latest known information on the availability of the exchange API + + https://github.com/Bitrue-exchange/Spot-official-api-docs#test-connectivity + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `status structure ` + """ + response = await self.spotV1PublicGetPing(params) + # + # empty means working status. + # + # {} + # + keys = list(response.keys()) + keysLength = len(keys) + formattedStatus = 'maintenance' if keysLength else 'ok' + return { + 'status': formattedStatus, + 'updated': None, + 'eta': None, + 'url': None, + 'info': response, + } + + async def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://github.com/Bitrue-exchange/Spot-official-api-docs#check-server-time + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = await self.spotV1PublicGetTime(params) + # + # { + # "serverTime":1635467280514 + # } + # + return self.safe_integer(response, 'serverTime') + + async def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = await self.spotV1PublicGetExchangeInfo(params) + # + # { + # "timezone":"CTT", + # "serverTime":1635464889117, + # "rateLimits":[ + # {"rateLimitType":"REQUESTS_WEIGHT","interval":"MINUTES","limit":6000}, + # {"rateLimitType":"ORDERS","interval":"SECONDS","limit":150}, + # {"rateLimitType":"ORDERS","interval":"DAYS","limit":288000}, + # ], + # "exchangeFilters":[], + # "symbols":[ + # { + # "symbol":"SHABTC", + # "status":"TRADING", + # "baseAsset":"sha", + # "baseAssetPrecision":0, + # "quoteAsset":"btc", + # "quotePrecision":10, + # "orderTypes":["MARKET","LIMIT"], + # "icebergAllowed":false, + # "filters":[ + # {"filterType":"PRICE_FILTER","minPrice":"0.00000001349","maxPrice":"0.00000017537","priceScale":10}, + # {"filterType":"LOT_SIZE","minQty":"1.0","minVal":"0.00020","maxQty":"1000000000","volumeScale":0}, + # ], + # "defaultPrice":"0.0000006100", + # }, + # ], + # "coins":[ + # { + # "coin": "near", + # "coinFulName": "NEAR Protocol", + # "chains": ["BEP20",], + # "chainDetail": [ + # { + # "chain": "BEP20", + # "enableWithdraw": True, + # "enableDeposit": True, + # "withdrawFee": "0.2000", + # "minWithdraw": "5.0000", + # "maxWithdraw": "1000000000000000.0000", + # }, + # ], + # }, + # ], + # } + # + result: dict = {} + coins = self.safe_list(response, 'coins', []) + for i in range(0, len(coins)): + currency = coins[i] + id = self.safe_string(currency, 'coin') + name = self.safe_string(currency, 'coinFulName') + code = self.safe_currency_code(id) + networkDetails = self.safe_list(currency, 'chainDetail', []) + networks: dict = {} + for j in range(0, len(networkDetails)): + entry = networkDetails[j] + networkId = self.safe_string(entry, 'chain') + network = self.network_id_to_code(networkId, code) + networks[network] = { + 'info': entry, + 'id': networkId, + 'network': network, + 'deposit': self.safe_bool(entry, 'enableDeposit'), + 'withdraw': self.safe_bool(entry, 'enableWithdraw'), + 'active': None, + 'fee': self.safe_number(entry, 'withdrawFee'), + 'precision': None, + 'limits': { + 'withdraw': { + 'min': self.safe_number(entry, 'minWithdraw'), + 'max': self.safe_number(entry, 'maxWithdraw'), + }, + }, + } + result[code] = self.safe_currency_structure({ + 'id': id, + 'name': name, + 'code': code, + 'precision': None, + 'info': currency, + 'active': None, + 'deposit': None, + 'withdraw': None, + 'networks': networks, + 'fee': None, + 'fees': None, + 'type': 'crypto', + 'limits': { + 'withdraw': { + 'min': None, + 'max': None, + }, + }, + }) + return result + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for bitrue + + https://github.com/Bitrue-exchange/Spot-official-api-docs#exchangeInfo_endpoint + https://www.bitrue.com/api-docs#current-open-contract + https://www.bitrue.com/api_docs_includes_file/delivery.html#current-open-contract + + :param dict [params]: extra parameters specific to the exchange api endpoint + :returns dict[]: an array of objects representing market data + """ + promisesRaw = [] + types = None + defaultTypes = ['spot', 'linear', 'inverse'] + fetchMarketsOptions = self.safe_dict(self.options, 'fetchMarkets') + if fetchMarketsOptions is not None: + types = self.safe_list(fetchMarketsOptions, 'types', defaultTypes) + else: + # for backward-compatibility + types = self.safe_list(self.options, 'fetchMarkets', defaultTypes) + for i in range(0, len(types)): + marketType = types[i] + if marketType == 'spot': + promisesRaw.append(self.spotV1PublicGetExchangeInfo(params)) + elif marketType == 'linear': + promisesRaw.append(self.fapiV1PublicGetContracts(params)) + elif marketType == 'inverse': + promisesRaw.append(self.dapiV1PublicGetContracts(params)) + else: + raise ExchangeError(self.id + ' fetchMarkets() self.options fetchMarkets "' + marketType + '" is not a supported market type') + promises = await asyncio.gather(*promisesRaw) + spotMarkets = self.safe_value(self.safe_value(promises, 0), 'symbols', []) + futureMarkets = self.safe_value(promises, 1) + deliveryMarkets = self.safe_value(promises, 2) + markets = spotMarkets + markets = self.array_concat(markets, futureMarkets) + markets = self.array_concat(markets, deliveryMarkets) + # + # spot + # + # { + # "timezone":"CTT", + # "serverTime":1635464889117, + # "rateLimits":[ + # {"rateLimitType":"REQUESTS_WEIGHT","interval":"MINUTES","limit":6000}, + # {"rateLimitType":"ORDERS","interval":"SECONDS","limit":150}, + # {"rateLimitType":"ORDERS","interval":"DAYS","limit":288000}, + # ], + # "exchangeFilters":[], + # "symbols":[ + # { + # "symbol":"SHABTC", + # "status":"TRADING", + # "baseAsset":"sha", + # "baseAssetPrecision":0, + # "quoteAsset":"btc", + # "quotePrecision":10, + # "orderTypes":["MARKET","LIMIT"], + # "icebergAllowed":false, + # "filters":[ + # {"filterType":"PRICE_FILTER","minPrice":"0.00000001349","maxPrice":"0.00000017537","priceScale":10}, + # {"filterType":"LOT_SIZE","minQty":"1.0","minVal":"0.00020","maxQty":"1000000000","volumeScale":0}, + # ], + # "defaultPrice":"0.0000006100", + # }, + # ], + # "coins":[ + # { + # "coin":"sbr", + # "coinFulName":"Saber", + # "enableWithdraw":true, + # "enableDeposit":true, + # "chains":["SOLANA"], + # "withdrawFee":"2.0", + # "minWithdraw":"5.0", + # "maxWithdraw":"1000000000000000", + # }, + # ], + # } + # + # swap / delivery + # + # [ + # { + # "symbol": "H-HT-USDT", + # "pricePrecision": 8, + # "side": 1, + # "maxMarketVolume": 100000, + # "multiplier": 6, + # "minOrderVolume": 1, + # "maxMarketMoney": 10000000, + # "type": "H", # E: perpetual contract, S: test contract, others are mixed contract + # "maxLimitVolume": 1000000, + # "maxValidOrder": 20, + # "multiplierCoin": "HT", + # "minOrderMoney": 0.001, + # "maxLimitMoney": 1000000, + # "status": 1 + # } + # ] + # + if self.options['adjustForTimeDifference']: + await self.load_time_difference() + return self.parse_markets(markets) + + def parse_market(self, market: dict) -> Market: + id = self.safe_string(market, 'symbol') + lowercaseId = self.safe_string_lower(market, 'symbol') + side = self.safe_integer(market, 'side') # 1 linear, 0 inverse, None spot + type = None + isLinear = None + isInverse = None + if side is None: + type = 'spot' + else: + type = 'swap' + isLinear = (side == 1) + isInverse = (side == 0) + isContract = (type != 'spot') + baseId = self.safe_string(market, 'baseAsset') + quoteId = self.safe_string(market, 'quoteAsset') + settleId = None + settle = None + if isContract: + symbolSplit = id.split('-') + baseId = self.safe_string(symbolSplit, 1) + quoteId = self.safe_string(symbolSplit, 2) + if isLinear: + settleId = quoteId + else: + settleId = baseId + settle = self.safe_currency_code(settleId) + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + symbol = base + '/' + quote + if settle is not None: + symbol += ':' + settle + filters = self.safe_list(market, 'filters', []) + filtersByType = self.index_by(filters, 'filterType') + status = self.safe_string(market, 'status') + priceFilter = self.safe_dict(filtersByType, 'PRICE_FILTER', {}) + amountFilter = self.safe_dict(filtersByType, 'LOT_SIZE', {}) + defaultPricePrecision = self.safe_string(market, 'pricePrecision') + defaultAmountPrecision = self.safe_string(market, 'quantityPrecision') + pricePrecision = self.safe_string(priceFilter, 'priceScale', defaultPricePrecision) + amountPrecision = self.safe_string(amountFilter, 'volumeScale', defaultAmountPrecision) + multiplier = self.safe_string(market, 'multiplier') + maxQuantity = self.safe_number(amountFilter, 'maxQty') + if maxQuantity is None: + maxQuantity = self.safe_number(market, 'maxValidOrder') + minCost = self.safe_number(amountFilter, 'minVal') + if minCost is None: + minCost = self.safe_number(market, 'minOrderMoney') + return { + 'id': id, + 'lowercaseId': lowercaseId, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': type, + 'spot': (type == 'spot'), + 'margin': False, + 'swap': isContract, + 'future': False, + 'option': False, + 'active': (status == 'TRADING'), + 'contract': isContract, + 'linear': isLinear, + 'inverse': isInverse, + 'contractSize': self.parse_number(Precise.string_abs(multiplier)), + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(self.parse_precision(amountPrecision)), + 'price': self.parse_number(self.parse_precision(pricePrecision)), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(amountFilter, 'minQty'), + 'max': maxQuantity, + }, + 'price': { + 'min': self.safe_number(priceFilter, 'minPrice'), + 'max': self.safe_number(priceFilter, 'maxPrice'), + }, + 'cost': { + 'min': minCost, + 'max': None, + }, + }, + 'created': None, + 'info': market, + } + + def parse_balance(self, response) -> Balances: + # + # spot + # + # { + # "makerCommission":0, + # "takerCommission":0, + # "buyerCommission":0, + # "sellerCommission":0, + # "updateTime":null, + # "balances":[ + # {"asset":"sbr","free":"0","locked":"0"}, + # {"asset":"ksm","free":"0","locked":"0"}, + # {"asset":"neo3s","free":"0","locked":"0"}, + # ], + # "canTrade":false, + # "canWithdraw":false, + # "canDeposit":false + # } + # + # swap + # + # { + # "account":[ + # { + # "marginCoin":"USDT", + # "coinPrecious":4, + # "accountNormal":1010.4043400372839856, + # "accountLock":2.9827889600000006, + # "partPositionNormal":0, + # "totalPositionNormal":0, + # "achievedAmount":0, + # "unrealizedAmount":0, + # "totalMarginRate":0, + # "totalEquity":1010.4043400372839856, + # "partEquity":0, + # "totalCost":0, + # "sumMarginRate":0, + # "sumOpenRealizedAmount":0, + # "canUseTrialFund":0, + # "sumMaintenanceMargin":null, + # "futureModel":null, + # "positionVos":[] + # } + # ] + # } + # + result: dict = { + 'info': response, + } + timestamp = self.safe_integer(response, 'updateTime') + balances = self.safe_value_2(response, 'balances', 'account', []) + for i in range(0, len(balances)): + balance = balances[i] + currencyId = self.safe_string_2(balance, 'asset', 'marginCoin') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string_2(balance, 'free', 'accountNormal') + account['used'] = self.safe_string_2(balance, 'locked', 'accountLock') + result[code] = account + result['timestamp'] = timestamp + result['datetime'] = self.iso8601(timestamp) + return self.safe_balance(result) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://github.com/Bitrue-exchange/Spot-official-api-docs#account-information-user_data + https://www.bitrue.com/api-docs#account-information-v2-user_data-hmac-sha256 + https://www.bitrue.com/api_docs_includes_file/delivery.html#account-information-v2-user_data-hmac-sha256 + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: 'future', 'delivery', 'spot', 'swap' + :param str [params.subType]: 'linear', 'inverse' + :returns dict: a `balance structure ` + """ + await self.load_markets() + type = None + type, params = self.handle_market_type_and_params('fetchBalance', None, params) + subType = None + subType, params = self.handle_sub_type_and_params('fetchBalance', None, params) + response = None + result = None + if type == 'swap': + if subType is not None and subType == 'inverse': + response = await self.dapiV2PrivateGetAccount(params) + result = self.safe_dict(response, 'data', {}) + # + # { + # "code":"0", + # "msg":"Success", + # "data":{ + # "account":[ + # { + # "marginCoin":"USD", + # "coinPrecious":4, + # "accountNormal":1010.4043400372839856, + # "accountLock":2.9827889600000006, + # "partPositionNormal":0, + # "totalPositionNormal":0, + # "achievedAmount":0, + # "unrealizedAmount":0, + # "totalMarginRate":0, + # "totalEquity":1010.4043400372839856, + # "partEquity":0, + # "totalCost":0, + # "sumMarginRate":0, + # "sumOpenRealizedAmount":0, + # "canUseTrialFund":0, + # "sumMaintenanceMargin":null, + # "futureModel":null, + # "positionVos":[] + # } + # ] + # } + # } + # + else: + response = await self.fapiV2PrivateGetAccount(params) + result = self.safe_dict(response, 'data', {}) + # + # { + # "code":"0", + # "msg":"Success", + # "data":{ + # "account":[ + # { + # "marginCoin":"USDT", + # "coinPrecious":4, + # "accountNormal":1010.4043400372839856, + # "accountLock":2.9827889600000006, + # "partPositionNormal":0, + # "totalPositionNormal":0, + # "achievedAmount":0, + # "unrealizedAmount":0, + # "totalMarginRate":0, + # "totalEquity":1010.4043400372839856, + # "partEquity":0, + # "totalCost":0, + # "sumMarginRate":0, + # "sumOpenRealizedAmount":0, + # "canUseTrialFund":0, + # "sumMaintenanceMargin":null, + # "futureModel":null, + # "positionVos":[] + # } + # ] + # } + # } + # + else: + response = await self.spotV1PrivateGetAccount(params) + result = response + # + # { + # "makerCommission":0, + # "takerCommission":0, + # "buyerCommission":0, + # "sellerCommission":0, + # "updateTime":null, + # "balances":[ + # {"asset":"sbr","free":"0","locked":"0"}, + # {"asset":"ksm","free":"0","locked":"0"}, + # {"asset":"neo3s","free":"0","locked":"0"}, + # ], + # "canTrade":false, + # "canWithdraw":false, + # "canDeposit":false + # } + # + return self.parse_balance(result) + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://github.com/Bitrue-exchange/Spot-official-api-docs#order-book + https://www.bitrue.com/api-docs#order-book + https://www.bitrue.com/api_docs_includes_file/delivery.html#order-book + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + response = None + if market['swap']: + request: dict = { + 'contractName': market['id'], + } + if limit is not None: + if limit > 100: + limit = 100 + request['limit'] = limit # default 100, max 100, see https://www.bitrue.com/api-docs#order-book + if market['linear']: + response = await self.fapiV1PublicGetDepth(self.extend(request, params)) + elif market['inverse']: + response = await self.dapiV1PublicGetDepth(self.extend(request, params)) + elif market['spot']: + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + if limit > 1000: + limit = 1000 + request['limit'] = limit # default 100, max 1000, see https://github.com/Bitrue-exchange/bitrue-official-api-docs#order-book + response = await self.spotV1PublicGetDepth(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchOrderBook only support spot & swap markets') + # + # spot + # + # { + # "lastUpdateId":1635474910177, + # "bids":[ + # ["61436.84","0.05",[]], + # ["61435.77","0.0124",[]], + # ["61434.88","0.012",[]], + # ], + # "asks":[ + # ["61452.46","0.0001",[]], + # ["61452.47","0.0597",[]], + # ["61452.76","0.0713",[]], + # ] + # } + # + # swap + # + # { + # "asks": [[34916.5, 2582], [34916.6, 2193], [34916.7, 2629], [34916.8, 3478], [34916.9, 2718]], + # "bids": [[34916.4, 92065], [34916.3, 25703], [34916.2, 37259], [34916.1, 26446], [34916, 44456]], + # "time": 1699338305000 + # } + # + timestamp = self.safe_integer_2(response, 'time', 'lastUpdateId') + orderbook = self.parse_order_book(response, symbol, timestamp) + orderbook['nonce'] = self.safe_integer(response, 'lastUpdateId') + return orderbook + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # fetchBidsAsks + # + # { + # "symbol": "LTCBTC", + # "bidPrice": "4.00000000", + # "bidQty": "431.00000000", + # "askPrice": "4.00000200", + # "askQty": "9.00000000" + # } + # + # fetchTicker + # + # { + # "symbol": "BNBBTC", + # "priceChange": "0.000248", + # "priceChangePercent": "3.5500", + # "weightedAvgPrice": null, + # "prevClosePrice": null, + # "lastPrice": "0.007226", + # "lastQty": null, + # "bidPrice": "0.007208", + # "askPrice": "0.007240", + # "openPrice": "0.006978", + # "highPrice": "0.007295", + # "lowPrice": "0.006935", + # "volume": "11749.86", + # "quoteVolume": "84.1066211", + # "openTime": 0, + # "closeTime": 0, + # "firstId": 0, + # "lastId": 0, + # "count": 0 + # } + # + symbol = self.safe_symbol(None, market) + last = self.safe_string_2(ticker, 'lastPrice', 'last') + timestamp = self.safe_integer(ticker, 'time') + percentage = None + if market['swap']: + percentage = Precise.string_mul(self.safe_string(ticker, 'rose'), '100') + else: + percentage = self.safe_string(ticker, 'priceChangePercent') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string_2(ticker, 'highPrice', 'high'), + 'low': self.safe_string_2(ticker, 'lowPrice', 'low'), + 'bid': self.safe_string_2(ticker, 'bidPrice', 'buy'), + 'bidVolume': self.safe_string(ticker, 'bidQty'), + 'ask': self.safe_string_2(ticker, 'askPrice', 'sell'), + 'askVolume': self.safe_string(ticker, 'askQty'), + 'vwap': self.safe_string(ticker, 'weightedAvgPrice'), + 'open': self.safe_string(ticker, 'openPrice'), + 'close': last, + 'last': last, + 'previousClose': None, + 'change': self.safe_string(ticker, 'priceChange'), + 'percentage': percentage, + 'average': None, + 'baseVolume': self.safe_string_2(ticker, 'volume', 'vol'), + 'quoteVolume': self.safe_string(ticker, 'quoteVolume'), + 'info': ticker, + }, market) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://github.com/Bitrue-exchange/Spot-official-api-docs#24hr-ticker-price-change-statistics + https://www.bitrue.com/api-docs#ticker + https://www.bitrue.com/api_docs_includes_file/delivery.html#ticker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + response = None + data = None + if market['swap']: + request: dict = { + 'contractName': market['id'], + } + if market['linear']: + response = await self.fapiV1PublicGetTicker(self.extend(request, params)) + elif market['inverse']: + response = await self.dapiV1PublicGetTicker(self.extend(request, params)) + data = response + elif market['spot']: + request: dict = { + 'symbol': market['id'], + } + response = await self.spotV1PublicGetTicker24hr(self.extend(request, params)) + data = self.safe_dict(response, 0, {}) + else: + raise NotSupported(self.id + ' fetchTicker only support spot & swap markets') + # + # spot + # + # [{ + # symbol: 'BTCUSDT', + # priceChange: '105.20', + # priceChangePercent: '0.3000', + # weightedAvgPrice: null, + # prevClosePrice: null, + # lastPrice: '34905.21', + # lastQty: null, + # bidPrice: '34905.21', + # askPrice: '34905.22', + # openPrice: '34800.01', + # highPrice: '35276.33', + # lowPrice: '34787.51', + # volume: '12549.6481', + # quoteVolume: '439390492.917', + # openTime: '0', + # closeTime: '0', + # firstId: '0', + # lastId: '0', + # count: '0' + # }] + # + # swap + # + # { + # "high": "35296", + # "vol": "779308354", + # "last": "34884.1", + # "low": "34806.7", + # "buy": 34883.9, + # "sell": 34884, + # "rose": "-0.0027957315", + # "time": 1699348013000 + # } + # + return self.parse_ticker(data, market) + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://www.bitrue.com/api_docs_includes_file/spot/index.html#kline-data + https://www.bitrue.com/api_docs_includes_file/futures/index.html#kline-candlestick-data + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch transfers for + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + timeframes = self.safe_dict(self.options, 'timeframes', {}) + response = None + data = None + if market['swap']: + timeframesFuture = self.safe_dict(timeframes, 'future', {}) + request: dict = { + 'contractName': market['id'], + # 1min / 5min / 15min / 30min / 1h / 1day / 1week / 1month + 'interval': self.safe_string(timeframesFuture, timeframe, '1min'), + } + if limit is not None: + request['limit'] = limit + if market['linear']: + response = await self.fapiV1PublicGetKlines(self.extend(request, params)) + elif market['inverse']: + response = await self.dapiV1PublicGetKlines(self.extend(request, params)) + data = response + elif market['spot']: + timeframesSpot = self.safe_dict(timeframes, 'spot', {}) + request: dict = { + 'symbol': market['id'], + # 1m / 5m / 15m / 30m / 1H / 2H / 4H / 12H / 1D / 1W + 'scale': self.safe_string(timeframesSpot, timeframe, '1m'), + } + if limit is not None: + request['limit'] = limit + until = self.safe_integer(params, 'until') + if until is not None: + params = self.omit(params, 'until') + request['fromIdx'] = until + response = await self.spotV1PublicGetMarketKline(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + else: + raise NotSupported(self.id + ' fetchOHLCV only support spot & swap markets') + # + # spot + # + # { + # "symbol":"BTCUSDT", + # "scale":"KLINE_1MIN", + # "data":[ + # { + # "i":"1660825020", + # "a":"93458.778", + # "v":"3.9774", + # "c":"23494.99", + # "h":"23509.63", + # "l":"23491.93", + # "o":"23508.34" + # } + # ] + # } + # + # swap + # + # [ + # { + # "high": "35360.7", + # "vol": "110288", + # "low": "35347.9", + # "idx": 1699411680000, + # "close": "35347.9", + # "open": "35349.4" + # } + # ] + # + return self.parse_ohlcvs(data, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # spot + # + # { + # "i":"1660825020", + # "a":"93458.778", + # "v":"3.9774", + # "c":"23494.99", + # "h":"23509.63", + # "l":"23491.93", + # "o":"23508.34" + # } + # + # swap + # + # { + # "high": "35360.7", + # "vol": "110288", + # "low": "35347.9", + # "idx": 1699411680000, + # "close": "35347.9", + # "open": "35349.4" + # } + # + timestamp = self.safe_timestamp(ohlcv, 'i') + if timestamp is None: + timestamp = self.safe_integer(ohlcv, 'idx') + return [ + timestamp, + self.safe_number_2(ohlcv, 'o', 'open'), + self.safe_number_2(ohlcv, 'h', 'high'), + self.safe_number_2(ohlcv, 'l', 'low'), + self.safe_number_2(ohlcv, 'c', 'close'), + self.safe_number_2(ohlcv, 'v', 'vol'), + ] + + async def fetch_bids_asks(self, symbols: Strings = None, params={}): + """ + fetches the bid and ask price and volume for multiple markets + + https://github.com/Bitrue-exchange/Spot-official-api-docs#symbol-order-book-ticker + https://www.bitrue.com/api-docs#ticker + https://www.bitrue.com/api_docs_includes_file/delivery.html#ticker + + :param str[]|None symbols: unified symbols of the markets to fetch the bids and asks for, all markets are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, False) + first = self.safe_string(symbols, 0) + market = self.market(first) + response = None + if market['swap']: + request: dict = { + 'contractName': market['id'], + } + if market['linear']: + response = await self.fapiV1PublicGetTicker(self.extend(request, params)) + elif market['inverse']: + response = await self.dapiV1PublicGetTicker(self.extend(request, params)) + elif market['spot']: + request: dict = { + 'symbol': market['id'], + } + response = await self.spotV1PublicGetTickerBookTicker(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchBidsAsks only support spot & swap markets') + # + # spot + # + # { + # "symbol": "LTCBTC", + # "bidPrice": "4.00000000", + # "bidQty": "431.00000000", + # "askPrice": "4.00000200", + # "askQty": "9.00000000" + # } + # + # swap + # + # { + # "high": "35296", + # "vol": "779308354", + # "last": "34884.1", + # "low": "34806.7", + # "buy": 34883.9, + # "sell": 34884, + # "rose": "-0.0027957315", + # "time": 1699348013000 + # } + # + data: dict = {} + data[market['id']] = response + return self.parse_tickers(data, symbols) + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://github.com/Bitrue-exchange/Spot-official-api-docs#24hr-ticker-price-change-statistics + https://www.bitrue.com/api-docs#ticker + https://www.bitrue.com/api_docs_includes_file/delivery.html#ticker + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + response = None + data = None + request: dict = {} + type = None + if symbols is not None: + first = self.safe_string(symbols, 0) + market = self.market(first) + if market['swap']: + raise NotSupported(self.id + ' fetchTickers does not support swap markets, please use fetchTicker instead') + elif market['spot']: + response = await self.spotV1PublicGetTicker24hr(self.extend(request, params)) + data = response + else: + raise NotSupported(self.id + ' fetchTickers only support spot & swap markets') + else: + type, params = self.handle_market_type_and_params('fetchTickers', None, params) + if type != 'spot': + raise NotSupported(self.id + ' fetchTickers only support spot when symbols are not proved') + response = await self.spotV1PublicGetTicker24hr(self.extend(request, params)) + data = response + # + # spot + # + # [{ + # symbol: 'BTCUSDT', + # priceChange: '105.20', + # priceChangePercent: '0.3000', + # weightedAvgPrice: null, + # prevClosePrice: null, + # lastPrice: '34905.21', + # lastQty: null, + # bidPrice: '34905.21', + # askPrice: '34905.22', + # openPrice: '34800.01', + # highPrice: '35276.33', + # lowPrice: '34787.51', + # volume: '12549.6481', + # quoteVolume: '439390492.917', + # openTime: '0', + # closeTime: '0', + # firstId: '0', + # lastId: '0', + # count: '0' + # }] + # + # swap + # + # { + # "high": "35296", + # "vol": "779308354", + # "last": "34884.1", + # "low": "34806.7", + # "buy": 34883.9, + # "sell": 34884, + # "rose": "-0.0027957315", + # "time": 1699348013000 + # } + # + # the exchange returns market ids with an underscore from the tickers endpoint + # the market ids do not have an underscore, so it has to be removed + # https://github.com/ccxt/ccxt/issues/13856 + tickers: dict = {} + for i in range(0, len(data)): + ticker = self.safe_dict(data, i, {}) + market = self.safe_market(self.safe_string(ticker, 'symbol')) + tickers[market['id']] = ticker + return self.parse_tickers(tickers, symbols) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades + # + # { + # "id": 28457, + # "price": "4.00000100", + # "qty": "12.00000000", + # "time": 1499865549590, # Actual timestamp of trade + # "isBuyerMaker": True, + # "isBestMatch": True + # } + # + # fetchTrades - spot + # + # { + # "symbol":"USDCUSDT", + # "id":20725156, + # "orderId":2880918576, + # "origClientOrderId":null, + # "price":"0.9996000000000000", + # "qty":"100.0000000000000000", + # "commission":null, + # "commissionAssert":null, + # "time":1635558511000, + # "isBuyer":false, + # "isMaker":false, + # "isBestMatch":true + # } + # + # fetchTrades - future + # + # { + # "tradeId":12, + # "price":0.9, + # "qty":1, + # "amount":9, + # "contractName":"E-SAND-USDT", + # "side":"BUY", + # "fee":"0.0018", + # "bidId":1558124009467904992, + # "askId":1558124043827644908, + # "bidUserId":10294, + # "askUserId":10467, + # "isBuyer":true, + # "isMaker":true, + # "ctime":1678426306000 + # } + # + timestamp = self.safe_integer_2(trade, 'ctime', 'time') + priceString = self.safe_string(trade, 'price') + amountString = self.safe_string(trade, 'qty') + marketId = self.safe_string_2(trade, 'symbol', 'contractName') + symbol = self.safe_symbol(marketId, market) + orderId = self.safe_string(trade, 'orderId') + id = self.safe_string_2(trade, 'id', 'tradeId') + side = None + buyerMaker = self.safe_bool(trade, 'isBuyerMaker') # ignore "m" until Bitrue fixes api + isBuyer = self.safe_bool(trade, 'isBuyer') + if buyerMaker is not None: + side = 'sell' if buyerMaker else 'buy' + if isBuyer is not None: + side = 'buy' if isBuyer else 'sell' # self is a True side + fee = None + if 'commission' in trade: + fee = { + 'cost': self.safe_string_2(trade, 'commission', 'fee'), + 'currency': self.safe_currency_code(self.safe_string(trade, 'commissionAssert')), + } + takerOrMaker = None + isMaker = self.safe_bool(trade, 'isMaker') + if isMaker is not None: + takerOrMaker = 'maker' if isMaker else 'taker' + return self.safe_trade({ + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'id': id, + 'order': orderId, + 'type': None, + 'side': side, + 'takerOrMaker': takerOrMaker, + 'price': priceString, + 'amount': amountString, + 'cost': None, + 'fee': fee, + }, market) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://github.com/Bitrue-exchange/Spot-official-api-docs#recent-trades-list + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + response = None + if market['spot']: + request: dict = { + 'symbol': market['id'], + # 'limit': 100, # default 100, max = 1000 + } + if limit is not None: + request['limit'] = limit # default 100, max 1000 + response = await self.spotV1PublicGetTrades(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchTrades only support spot markets') + # + # spot + # + # [ + # { + # "id": 28457, + # "price": "4.00000100", + # "qty": "12.00000000", + # "time": 1499865549590, + # "isBuyerMaker": True, + # "isBestMatch": True + # } + # ] + # + return self.parse_trades(response, market, since, limit) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'INIT': 'open', + 'PENDING_CREATE': 'open', + 'NEW': 'open', + 'PARTIALLY_FILLED': 'open', + 'FILLED': 'closed', + 'CANCELED': 'canceled', + 'PENDING_CANCEL': 'canceling', # currently unused + 'REJECTED': 'rejected', + 'EXPIRED': 'expired', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder - spot + # + # { + # "symbol":"USDCUSDT", + # "orderId":2878854881, + # "clientOrderId":"", + # "transactTime":1635551031276 + # } + # + # createOrder - future + # + # { + # "orderId":1690615676032452985, + # } + # + # fetchOrders - spot + # + # { + # "symbol":"USDCUSDT", + # "orderId":"2878854881", + # "clientOrderId":"", + # "price":"1.1000000000000000", + # "origQty":"100.0000000000000000", + # "executedQty":"0.0000000000000000", + # "cummulativeQuoteQty":"0.0000000000000000", + # "status":"NEW", + # "timeInForce":"", + # "type":"LIMIT", + # "side":"SELL", + # "stopPrice":"", + # "icebergQty":"", + # "time":1635551031000, + # "updateTime":1635551031000, + # "isWorking":false + # } + # + # fetchOrders - future + # + # { + # "orderId":1917641, + # "price":100, + # "origQty":10, + # "origAmount":10, + # "executedQty":1, + # "avgPrice":10000, + # "status":"INIT", + # "type":"LIMIT", + # "side":"BUY", + # "action":"OPEN", + # "transactTime":1686716571425 + # "clientOrderId":4949299210 + # } + # + status = self.parse_order_status(self.safe_string_2(order, 'status', 'orderStatus')) + marketId = self.safe_string(order, 'symbol') + symbol = self.safe_symbol(marketId, market) + filled = self.safe_string(order, 'executedQty') + timestamp = None + lastTradeTimestamp = None + if 'time' in order: + timestamp = self.safe_integer(order, 'time') + elif 'transactTime' in order: + timestamp = self.safe_integer(order, 'transactTime') + elif 'updateTime' in order: + if status == 'open': + if Precise.string_gt(filled, '0'): + lastTradeTimestamp = self.safe_integer(order, 'updateTime') + else: + timestamp = self.safe_integer(order, 'updateTime') + average = self.safe_string(order, 'avgPrice') + price = self.safe_string(order, 'price') + amount = self.safe_string(order, 'origQty') + # - Spot/Margin market: cummulativeQuoteQty + # - Futures market: cumQuote. + # Note self is not the actual cost, since Binance futures uses leverage to calculate margins. + cost = self.safe_string_2(order, 'cummulativeQuoteQty', 'cumQuote') + id = self.safe_string(order, 'orderId') + type = self.safe_string_lower(order, 'type') + side = self.safe_string_lower(order, 'side') + fills = self.safe_list(order, 'fills', []) + clientOrderId = self.safe_string(order, 'clientOrderId') + timeInForce = self.safe_string(order, 'timeInForce') + postOnly = (type == 'limit_maker') or (timeInForce == 'GTX') or (type == 'post_only') + if type == 'limit_maker': + type = 'limit' + triggerPrice = self.parse_number(self.omit_zero(self.safe_string(order, 'stopPrice'))) + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': lastTradeTimestamp, + 'symbol': symbol, + 'type': type, + 'timeInForce': timeInForce, + 'postOnly': postOnly, + 'side': side, + 'price': price, + 'triggerPrice': triggerPrice, + 'amount': amount, + 'cost': cost, + 'average': average, + 'filled': filled, + 'remaining': None, + 'status': status, + 'fee': None, + 'trades': fills, + }, market) + + async def create_market_buy_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market buy order by providing the symbol and cost + + https://www.bitrue.com/api-docs#new-order-trade-hmac-sha256 + https://www.bitrue.com/api_docs_includes_file/delivery.html#new-order-trade-hmac-sha256 + + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + if not market['swap']: + raise NotSupported(self.id + ' createMarketBuyOrderWithCost() supports swap orders only') + params['createMarketBuyOrderRequiresPrice'] = False + return await self.create_order(symbol, 'market', 'buy', cost, None, params) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://www.bitrue.com/api_docs_includes_file/spot/index.html#new-order-trade + https://www.bitrue.com/api_docs_includes_file/futures/index.html#new-order-trade-hmac-sha256 + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: *spot only* the price at which a trigger order is triggered at + :param str [params.clientOrderId]: a unique id for the order, automatically generated if not sent + :param decimal [params.leverage]: in future order, the leverage value of the order should consistent with the user contract configuration, default is 1 + :param str [params.timeInForce]: 'fok', 'ioc' or 'po' + :param bool [params.postOnly]: default False + :param bool [params.reduceOnly]: default False + EXCHANGE SPECIFIC PARAMETERS + :param decimal [params.icebergQty]: + :param long [params.recvWindow]: + :param float [params.cost]: *swap market buy only* the quote quantity that can be used alternative for the amount + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + response = None + data = None + uppercaseType = type.upper() + request: dict = { + 'side': side.upper(), + 'type': uppercaseType, + # 'timeInForce': '', + # 'price': self.price_to_precision(symbol, price), + # 'newClientOrderId': clientOrderId, # automatically generated if not sent + # 'stopPrice': self.price_to_precision(symbol, 'stopPrice'), + # 'icebergQty': self.amount_to_precision(symbol, icebergQty), + } + if uppercaseType == 'LIMIT': + if price is None: + raise InvalidOrder(self.id + ' createOrder() requires a price argument') + request['price'] = self.price_to_precision(symbol, price) + if market['swap']: + isMarket = uppercaseType == 'MARKET' + timeInForce = self.safe_string_lower(params, 'timeInForce') + postOnly = self.is_post_only(isMarket, None, params) + if postOnly: + request['type'] = 'POST_ONLY' + elif timeInForce == 'fok': + request['type'] = 'FOK' + elif timeInForce == 'ioc': + request['type'] = 'IOC' + request['contractName'] = market['id'] + createMarketBuyOrderRequiresPrice = True + createMarketBuyOrderRequiresPrice, params = self.handle_option_and_params(params, 'createOrder', 'createMarketBuyOrderRequiresPrice', True) + if isMarket and (side == 'buy') and createMarketBuyOrderRequiresPrice: + cost = self.safe_string(params, 'cost') + params = self.omit(params, 'cost') + if price is None and cost is None: + raise InvalidOrder(self.id + ' createOrder() requires the price argument with swap market buy orders to calculate total order cost(amount to spend), where cost = amount * price. Supply a price argument to createOrder() call if you want the cost to be calculated for you from price and amount, or, alternatively, add .options["createMarketBuyOrderRequiresPrice"] = False to supply the cost in the amount argument(the exchange-specific behaviour)') + else: + amountString = self.number_to_string(amount) + priceString = self.number_to_string(price) + quoteAmount = Precise.string_mul(amountString, priceString) + requestAmount = cost if (cost is not None) else quoteAmount + request['amount'] = self.cost_to_precision(symbol, requestAmount) + request['volume'] = self.cost_to_precision(symbol, requestAmount) + else: + request['amount'] = self.parse_to_numeric(amount) + request['volume'] = self.parse_to_numeric(amount) + request['positionType'] = 1 + reduceOnly = self.safe_value_2(params, 'reduceOnly', 'reduce_only') + request['open'] = 'CLOSE' if reduceOnly else 'OPEN' + leverage = self.safe_string(params, 'leverage', '1') + request['leverage'] = self.parse_to_numeric(leverage) + params = self.omit(params, ['leverage', 'reduceOnly', 'reduce_only', 'timeInForce']) + if market['linear']: + response = await self.fapiV2PrivatePostOrder(self.extend(request, params)) + elif market['inverse']: + response = await self.dapiV2PrivatePostOrder(self.extend(request, params)) + data = self.safe_dict(response, 'data', {}) + elif market['spot']: + request['symbol'] = market['id'] + request['quantity'] = self.amount_to_precision(symbol, amount) + validOrderTypes = self.safe_value(market['info'], 'orderTypes') + if not self.in_array(uppercaseType, validOrderTypes): + raise InvalidOrder(self.id + ' ' + type + ' is not a valid order type in market ' + symbol) + clientOrderId = self.safe_string_2(params, 'newClientOrderId', 'clientOrderId') + if clientOrderId is not None: + params = self.omit(params, ['newClientOrderId', 'clientOrderId']) + request['newClientOrderId'] = clientOrderId + triggerPrice = self.safe_value_2(params, 'triggerPrice', 'stopPrice') + if triggerPrice is not None: + params = self.omit(params, ['triggerPrice', 'stopPrice']) + request['stopPrice'] = self.price_to_precision(symbol, triggerPrice) + response = await self.spotV1PrivatePostOrder(self.extend(request, params)) + data = response + else: + raise NotSupported(self.id + ' createOrder only support spot & swap markets') + # + # spot + # + # { + # "symbol": "BTCUSDT", + # "orderId": 307650651173648896, + # "orderIdStr": "307650651173648896", + # "clientOrderId": "6gCrw2kRUAF9CvJDGP16IP", + # "transactTime": 1507725176595 + # } + # + # swap + # + # { + # "code": "0", + # "msg": "Success", + # "data": { + # "orderId": 1690615676032452985 + # } + # } + # + return self.parse_order(data, market) + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://www.bitrue.com/api_docs_includes_file/spot/index.html#query-order-user_data + https://www.bitrue.com/api_docs_includes_file/futures/index.html#query-order-user_data-hmac-sha256 + + :param str id: the order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrder() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + origClientOrderId = self.safe_value_2(params, 'origClientOrderId', 'clientOrderId') + params = self.omit(params, ['origClientOrderId', 'clientOrderId']) + response = None + data = None + request: dict = {} + if origClientOrderId is None: + request['orderId'] = id + else: + if market['swap']: + request['clientOrderId'] = origClientOrderId + else: + request['origClientOrderId'] = origClientOrderId + if market['swap']: + request['contractName'] = market['id'] + if market['linear']: + response = await self.fapiV2PrivateGetOrder(self.extend(request, params)) + elif market['inverse']: + response = await self.dapiV2PrivateGetOrder(self.extend(request, params)) + data = self.safe_dict(response, 'data', {}) + elif market['spot']: + request['orderId'] = id # spot market id is mandatory + request['symbol'] = market['id'] + response = await self.spotV1PrivateGetOrder(self.extend(request, params)) + data = response + else: + raise NotSupported(self.id + ' fetchOrder only support spot & swap markets') + # + # spot + # + # { + # "symbol": "LTCBTC", + # "orderId": 1, + # "clientOrderId": "myOrder1", + # "price": "0.1", + # "origQty": "1.0", + # "executedQty": "0.0", + # "cummulativeQuoteQty": "0.0", + # "status": "NEW", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "side": "BUY", + # "stopPrice": "0.0", + # "icebergQty": "0.0", + # "time": 1499827319559, + # "updateTime": 1499827319559, + # "isWorking": True + # } + # + # swap + # + # { + # "code":0, + # "msg":"success", + # "data":{ + # "orderId":1917641, + # "price":100, + # "origQty":10, + # "origAmount":10, + # "executedQty":1, + # "avgPrice":10000, + # "status":"INIT", + # "type":"LIMIT", + # "side":"BUY", + # "action":"OPEN", + # "transactTime":1686716571425 + # "clientOrderId":4949299210 + # } + # } + # + return self.parse_order(data, market) + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://www.bitrue.com/api_docs_includes_file/spot/index.html#all-orders-user_data + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchClosedOrders() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' fetchClosedOrders only support spot markets') + request: dict = { + 'symbol': market['id'], + # 'orderId': 123445, # long + # 'startTime': since, + # 'endTime': self.milliseconds(), + # 'limit': limit, # default 100, max 1000 + } + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit # default 100, max 1000 + response = await self.spotV1PrivateGetAllOrders(self.extend(request, params)) + # + # [ + # { + # "symbol": "LTCBTC", + # "orderId": 1, + # "clientOrderId": "myOrder1", + # "price": "0.1", + # "origQty": "1.0", + # "executedQty": "0.0", + # "cummulativeQuoteQty": "0.0", + # "status": "NEW", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "side": "BUY", + # "stopPrice": "0.0", + # "icebergQty": "0.0", + # "time": 1499827319559, + # "updateTime": 1499827319559, + # "isWorking": True + # } + # ] + # + return self.parse_orders(response, market, since, limit) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://www.bitrue.com/api_docs_includes_file/spot/index.html#current-open-orders-user_data + https://www.bitrue.com/api_docs_includes_file/futures/index.html#cancel-all-open-orders-trade-hmac-sha256 + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOpenOrders() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + response = None + data = None + request: dict = {} + if market['swap']: + request['contractName'] = market['id'] + if market['linear']: + response = await self.fapiV2PrivateGetOpenOrders(self.extend(request, params)) + elif market['inverse']: + response = await self.dapiV2PrivateGetOpenOrders(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + elif market['spot']: + request['symbol'] = market['id'] + response = await self.spotV1PrivateGetOpenOrders(self.extend(request, params)) + data = response + else: + raise NotSupported(self.id + ' fetchOpenOrders only support spot & swap markets') + # + # spot + # + # [ + # { + # "symbol":"USDCUSDT", + # "orderId":"2878854881", + # "clientOrderId":"", + # "price":"1.1000000000000000", + # "origQty":"100.0000000000000000", + # "executedQty":"0.0000000000000000", + # "cummulativeQuoteQty":"0.0000000000000000", + # "status":"NEW", + # "timeInForce":"", + # "type":"LIMIT", + # "side":"SELL", + # "stopPrice":"", + # "icebergQty":"", + # "time":1635551031000, + # "updateTime":1635551031000, + # "isWorking":false + # } + # ] + # + # swap + # + # { + # "code": "0", + # "msg": "Success", + # "data": [{ + # "orderId": 1917641, + # "clientOrderId": "2488514315", + # "price": 100, + # "origQty": 10, + # "origAmount": 10, + # "executedQty": 1, + # "avgPrice": 12451, + # "status": "INIT", + # "type": "LIMIT", + # "side": "BUY", + # "action": "OPEN", + # "transactTime": 1686717303975 + # } + # ] + # } + # + return self.parse_orders(data, market, since, limit) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://github.com/Bitrue-exchange/Spot-official-api-docs#cancel-order-trade + https://www.bitrue.com/api-docs#cancel-order-trade-hmac-sha256 + https://www.bitrue.com/api_docs_includes_file/delivery.html#cancel-order-trade-hmac-sha256 + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + origClientOrderId = self.safe_value_2(params, 'origClientOrderId', 'clientOrderId') + params = self.omit(params, ['origClientOrderId', 'clientOrderId']) + response = None + data = None + request: dict = {} + if origClientOrderId is None: + request['orderId'] = id + else: + if market['swap']: + request['clientOrderId'] = origClientOrderId + else: + request['origClientOrderId'] = origClientOrderId + if market['swap']: + request['contractName'] = market['id'] + if market['linear']: + response = await self.fapiV2PrivatePostCancel(self.extend(request, params)) + elif market['inverse']: + response = await self.dapiV2PrivatePostCancel(self.extend(request, params)) + data = self.safe_dict(response, 'data', {}) + elif market['spot']: + request['symbol'] = market['id'] + response = await self.spotV1PrivateDeleteOrder(self.extend(request, params)) + data = response + else: + raise NotSupported(self.id + ' cancelOrder only support spot & swap markets') + # + # spot + # + # { + # "symbol": "LTCBTC", + # "origClientOrderId": "myOrder1", + # "orderId": 1, + # "clientOrderId": "cancelMyOrder1" + # } + # + # swap + # + # { + # "code": "0", + # "msg": "Success", + # "data": { + # "orderId": 1690615847831143159 + # } + # } + # + return self.parse_order(data, market) + + async def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders in a market + + https://www.bitrue.com/api-docs#cancel-all-open-orders-trade-hmac-sha256 + https://www.bitrue.com/api_docs_includes_file/delivery.html#cancel-all-open-orders-trade-hmac-sha256 + + :param str symbol: unified market symbol of the market to cancel orders in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated', for spot margin trading + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + market = self.market(symbol) + response = None + data = None + if market['swap']: + request: dict = { + 'contractName': market['id'], + } + if market['linear']: + response = await self.fapiV2PrivatePostAllOpenOrders(self.extend(request, params)) + elif market['inverse']: + response = await self.dapiV2PrivatePostAllOpenOrders(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + else: + raise NotSupported(self.id + ' cancelAllOrders only support future markets') + # + # swap + # + # { + # 'code': '0', + # 'msg': 'Success', + # 'data': null + # } + # + return self.parse_orders(data, market) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://www.bitrue.com/api_docs_includes_file/spot/index.html#account-trade-list-user_data + https://www.bitrue.com/api_docs_includes_file/futures/index.html#account-trade-list-user_data-hmac-sha256 + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMyTrades() requires a symbol argument') + market = self.market(symbol) + response = None + data = None + request: dict = {} + if since is not None: + request['startTime'] = since + if limit is not None: + if limit > 1000: + limit = 1000 + request['limit'] = limit + if market['swap']: + request['contractName'] = market['id'] + if market['linear']: + response = await self.fapiV2PrivateGetMyTrades(self.extend(request, params)) + elif market['inverse']: + response = await self.dapiV2PrivateGetMyTrades(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + elif market['spot']: + request['symbol'] = market['id'] + response = await self.spotV2PrivateGetMyTrades(self.extend(request, params)) + data = response + else: + raise NotSupported(self.id + ' fetchMyTrades only support spot & swap markets') + # + # spot + # + # [ + # { + # "symbol":"USDCUSDT", + # "id":20725156, + # "orderId":2880918576, + # "origClientOrderId":null, + # "price":"0.9996000000000000", + # "qty":"100.0000000000000000", + # "commission":null, + # "commissionAssert":null, + # "time":1635558511000, + # "isBuyer":false, + # "isMaker":false, + # "isBestMatch":true + # } + # ] + # + # swap + # + # { + # "code":"0", + # "msg":"Success", + # "data":[ + # { + # "tradeId":12, + # "price":0.9, + # "qty":1, + # "amount":9, + # "contractName":"E-SAND-USDT", + # "side":"BUY", + # "fee":"0.0018", + # "bidId":1558124009467904992, + # "askId":1558124043827644908, + # "bidUserId":10294, + # "askUserId":10467, + # "isBuyer":true, + # "isMaker":true, + # "ctime":1678426306000 + # } + # ] + # } + # + return self.parse_trades(data, market, since, limit) + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://github.com/Bitrue-exchange/Spot-official-api-docs#deposit-history--withdraw_data + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + if code is None: + raise ArgumentsRequired(self.id + ' fetchDeposits() requires a code argument') + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'coin': currency['id'], + 'status': 1, # 0 init, 1 finished, default 0 + # 'offset': 0, + # 'limit': limit, # default 10, max 1000 + # 'startTime': since, + # 'endTime': self.milliseconds(), + } + if since is not None: + request['startTime'] = since + # request['endTime'] = self.sum(since, 7776000000) + if limit is not None: + request['limit'] = limit + response = await self.spotV1PrivateGetDepositHistory(self.extend(request, params)) + # + # { + # "code":200, + # "msg":"succ", + # "data":[ + # { + # "id":2659137, + # "symbol":"USDC", + # "amount":"200.0000000000000000", + # "fee":"0.0E-15", + # "createdAt":1635503169000, + # "updatedAt":1635503202000, + # "addressFrom":"0x2faf487a4414fe77e2327f0bf4ae2a264a776ad2", + # "addressTo":"0x190ceccb1f8bfbec1749180f0ba8922b488d865b", + # "txid":"0x9970aec41099ac385568859517308707bc7d716df8dabae7b52f5b17351c3ed0", + # "confirmations":5, + # "status":0, + # "tagType":null, + # }, + # { + # "id":2659137, + # "symbol": "XRP", + # "amount": "20.0000000000000000", + # "fee": "0.0E-15", + # "createdAt": 1544669393000, + # "updatedAt": 1544669413000, + # "addressFrom": "", + # "addressTo": "raLPjTYeGezfdb6crXZzcC8RkLBEwbBHJ5_18113641", + # "txid": "515B23E1F9864D3AF7F5B4C4FCBED784BAE861854FAB95F4031922B6AAEFC7AC", + # "confirmations": 7, + # "status": 1, + # "tagType": "Tag" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_transactions(data, currency, since, limit) + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://github.com/Bitrue-exchange/Spot-official-api-docs#withdraw-history--withdraw_data + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + if code is None: + raise ArgumentsRequired(self.id + ' fetchWithdrawals() requires a code argument') + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'coin': currency['id'], + 'status': 5, # 0 init, 5 finished, 6 canceled, default 0 + # 'offset': 0, + # 'limit': limit, # default 10, max 1000 + # 'startTime': since, + # 'endTime': self.milliseconds(), + } + if since is not None: + request['startTime'] = since + # request['endTime'] = self.sum(since, 7776000000) + if limit is not None: + request['limit'] = limit + response = await self.spotV1PrivateGetWithdrawHistory(self.extend(request, params)) + # + # { + # "code": 200, + # "msg": "succ", + # "data": [ + # { + # "id": 183745, + # "symbol": "usdt_erc20", + # "amount": "8.4000000000000000", + # "fee": "1.6000000000000000", + # "payAmount": "0.0000000000000000", + # "createdAt": 1595336441000, + # "updatedAt": 1595336576000, + # "addressFrom": "", + # "addressTo": "0x2edfae3878d7b6db70ce4abed177ab2636f60c83", + # "txid": "", + # "confirmations": 0, + # "status": 6, + # "tagType": null + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_transactions(data, currency) + + def parse_transaction_status_by_type(self, status, type=None): + statusesByType: dict = { + 'deposit': { + '0': 'pending', + '1': 'ok', + }, + 'withdrawal': { + '0': 'pending', # Email Sent + '5': 'ok', # Failure + '6': 'canceled', + }, + } + statuses = self.safe_dict(statusesByType, type, {}) + return self.safe_string(statuses, status, status) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fetchDeposits + # + # { + # "symbol": "XRP", + # "amount": "261.3361000000000000", + # "fee": "0.0E-15", + # "createdAt": 1548816979000, + # "updatedAt": 1548816999000, + # "addressFrom": "", + # "addressTo": "raLPjTYeGezfdb6crXZzcC8RkLBEwbBHJ5_18113641", + # "txid": "86D6EB68A7A28938BCE06BD348F8C07DEF500C5F7FE92069EF8C0551CE0F2C7D", + # "confirmations": 8, + # "status": 1, + # "tagType": "Tag" + # }, + # { + # "symbol": "XRP", + # "amount": "20.0000000000000000", + # "fee": "0.0E-15", + # "createdAt": 1544669393000, + # "updatedAt": 1544669413000, + # "addressFrom": "", + # "addressTo": "raLPjTYeGezfdb6crXZzcC8RkLBEwbBHJ5_18113641", + # "txid": "515B23E1F9864D3AF7F5B4C4FCBED784BAE861854FAB95F4031922B6AAEFC7AC", + # "confirmations": 7, + # "status": 1, + # "tagType": "Tag" + # } + # + # fetchWithdrawals + # + # { + # "id": 183745, + # "symbol": "usdt_erc20", + # "amount": "8.4000000000000000", + # "fee": "1.6000000000000000", + # "payAmount": "0.0000000000000000", + # "createdAt": 1595336441000, + # "updatedAt": 1595336576000, + # "addressFrom": "", + # "addressTo": "0x2edfae3878d7b6db70ce4abed177ab2636f60c83", + # "txid": "", + # "confirmations": 0, + # "status": 6, + # "tagType": null + # } + # + # withdraw + # + # { + # "msg": null, + # "amount": 1000, + # "fee": 1, + # "ctime": null, + # "coin": "usdt_erc20", + # "withdrawId": 1156423, + # "addressTo": "0x2edfae3878d7b6db70ce4abed177ab2636f60c83" + # } + # + id = self.safe_string_2(transaction, 'id', 'withdrawId') + tagType = self.safe_string(transaction, 'tagType') + addressTo = self.safe_string(transaction, 'addressTo') + addressFrom = self.safe_string(transaction, 'addressFrom') + tagTo = None + tagFrom = None + if tagType is not None: + if addressTo is not None: + parts = addressTo.split('_') + addressTo = self.safe_string(parts, 0) + tagTo = self.safe_string(parts, 1) + if addressFrom is not None: + parts = addressFrom.split('_') + addressFrom = self.safe_string(parts, 0) + tagFrom = self.safe_string(parts, 1) + txid = self.safe_string(transaction, 'txid') + timestamp = self.safe_integer(transaction, 'createdAt') + updated = self.safe_integer(transaction, 'updatedAt') + payAmount = ('payAmount' in transaction) + ctime = ('ctime' in transaction) + type = 'withdrawal' if (payAmount or ctime) else 'deposit' + status = self.parse_transaction_status_by_type(self.safe_string(transaction, 'status'), type) + amount = self.safe_number(transaction, 'amount') + network = None + currencyId = self.safe_string_2(transaction, 'symbol', 'coin') + if currencyId is not None: + parts = currencyId.split('_') + currencyId = self.safe_string(parts, 0) + networkId = self.safe_string(parts, 1) + if networkId is not None: + network = networkId.upper() + code = self.safe_currency_code(currencyId, currency) + feeCost = self.safe_number(transaction, 'fee') + fee = None + if feeCost is not None: + fee = {'currency': code, 'cost': feeCost} + return { + 'info': transaction, + 'id': id, + 'txid': txid, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': network, + 'address': addressTo, + 'addressTo': addressTo, + 'addressFrom': addressFrom, + 'tag': tagTo, + 'tagTo': tagTo, + 'tagFrom': tagFrom, + 'type': type, + 'amount': amount, + 'currency': code, + 'status': status, + 'updated': updated, + 'internal': False, + 'comment': None, + 'fee': fee, + } + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://github.com/Bitrue-exchange/Spot-official-api-docs#withdraw-commit--withdraw_data + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.check_address(address) + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'coin': currency['id'], + 'amount': amount, + 'addressTo': address, + # 'chainName': chainName, # 'ERC20', 'TRC20', 'SOL' + # 'addressMark': '', # mark of address + # 'addrType': '', # type of address + # 'tag': tag, + } + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + if networkCode is not None: + request['chainName'] = self.network_code_to_id(networkCode) + if tag is not None: + request['tag'] = tag + response = await self.spotV1PrivatePostWithdrawCommit(self.extend(request, params)) + # + # { + # "code": 200, + # "msg": "succ", + # "data": { + # "msg": null, + # "amount": 1000, + # "fee": 1, + # "ctime": null, + # "coin": "usdt_erc20", + # "withdrawId": 1156423, + # "addressTo": "0x2edfae3878d7b6db70ce4abed177ab2636f60c83" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_transaction(data, currency) + + def parse_deposit_withdraw_fee(self, fee, currency: Currency = None): + # + # { + # "coin": "adx", + # "coinFulName": "Ambire AdEx", + # "chains": ["BSC"], + # "chainDetail": [[Object]] + # } + # + chainDetails = self.safe_list(fee, 'chainDetail', []) + chainDetailLength = len(chainDetails) + result: dict = { + 'info': fee, + 'withdraw': { + 'fee': None, + 'percentage': None, + }, + 'deposit': { + 'fee': None, + 'percentage': None, + }, + 'networks': {}, + } + if chainDetailLength != 0: + for i in range(0, chainDetailLength): + chainDetail = chainDetails[i] + networkId = self.safe_string(chainDetail, 'chain') + currencyCode = self.safe_string(currency, 'code') + networkCode = self.network_id_to_code(networkId, currencyCode) + result['networks'][networkCode] = { + 'deposit': {'fee': None, 'percentage': None}, + 'withdraw': {'fee': self.safe_number(chainDetail, 'withdrawFee'), 'percentage': False}, + } + if chainDetailLength == 1: + result['withdraw']['fee'] = self.safe_number(chainDetail, 'withdrawFee') + result['withdraw']['percentage'] = False + return result + + async def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}): + """ + fetch deposit and withdraw fees + + https://github.com/Bitrue-exchange/Spot-official-api-docs#exchangeInfo_endpoint + + :param str[]|None codes: list of unified currency codes + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `fee structures ` + """ + await self.load_markets() + response = await self.spotV1PublicGetExchangeInfo(params) + coins = self.safe_list(response, 'coins') + return self.parse_deposit_withdraw_fees(coins, codes, 'coin') + + def parse_transfer(self, transfer, currency=None): + # + # fetchTransfers + # + # { + # 'transferType': 'wallet_to_contract', + # 'symbol': 'USDT', + # 'amount': 1.0, + # 'status': 1, + # 'ctime': 1685404575000 + # } + # + # transfer + # + # {} + # + transferType = self.safe_string(transfer, 'transferType') + fromAccount = None + toAccount = None + if transferType is not None: + accountSplit = transferType.split('_to_') + fromAccount = self.safe_string(accountSplit, 0) + toAccount = self.safe_string(accountSplit, 1) + timestamp = self.safe_integer(transfer, 'ctime') + return { + 'info': transfer, + 'id': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'currency': self.safe_string(currency, 'code'), + 'amount': self.safe_number(transfer, 'amount'), + 'fromAccount': fromAccount, + 'toAccount': toAccount, + 'status': 'ok', + } + + async def fetch_transfers(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[TransferEntry]: + """ + fetch a history of internal transfers made on an account + + https://www.bitrue.com/api-docs#get-future-account-transfer-history-list-user_data-hmac-sha256 + https://www.bitrue.com/api_docs_includes_file/delivery.html#get-future-account-transfer-history-list-user_data-hmac-sha256 + + :param str code: unified currency code of the currency transferred + :param int [since]: the earliest time in ms to fetch transfers for + :param int [limit]: the maximum number of transfers structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch transfers for + :param str [params.type]: transfer type wallet_to_contract or contract_to_wallet + :returns dict[]: a list of `transfer structures ` + """ + await self.load_markets() + type = self.safe_string_2(params, 'type', 'transferType') + request: dict = { + 'transferType': type, + } + currency = None + if code is not None: + currency = self.currency(code) + request['coinSymbol'] = currency['id'] + if since is not None: + request['beginTime'] = since + if limit is not None: + if limit > 200: + limit = 200 + request['limit'] = limit + until = self.safe_integer(params, 'until') + if until is not None: + params = self.omit(params, 'until') + request['endTime'] = until + response = await self.fapiV2PrivateGetFuturesTransferHistory(self.extend(request, params)) + # + # { + # 'code': '0', + # 'msg': 'Success', + # 'data': [{ + # 'transferType': 'wallet_to_contract', + # 'symbol': 'USDT', + # 'amount': 1.0, + # 'status': 1, + # 'ctime': 1685404575000 + # }] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_transfers(data, currency, since, limit) + + async def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://www.bitrue.com/api-docs#new-future-account-transfer-user_data-hmac-sha256 + https://www.bitrue.com/api_docs_includes_file/delivery.html#user-commission-rate-user_data-hmac-sha256 + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account to transfer from + :param str toAccount: account to transfer to + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transfer structure ` + """ + await self.load_markets() + currency = self.currency(code) + accountTypes = self.safe_dict(self.options, 'accountsByType', {}) + fromId = self.safe_string(accountTypes, fromAccount, fromAccount) + toId = self.safe_string(accountTypes, toAccount, toAccount) + request: dict = { + 'coinSymbol': currency['id'], + 'amount': self.currency_to_precision(code, amount), + 'transferType': fromId + '_to_' + toId, + } + response = await self.fapiV2PrivatePostFuturesTransfer(self.extend(request, params)) + # + # { + # 'code': '0', + # 'msg': 'Success', + # 'data': null + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_transfer(data, currency) + + async def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://www.bitrue.com/api-docs#change-initial-leverage-trade-hmac-sha256 + https://www.bitrue.com/api_docs_includes_file/delivery.html#change-initial-leverage-trade-hmac-sha256 + + :param float leverage: the rate of leverage + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setLeverage() requires a symbol argument') + if (leverage < 1) or (leverage > 125): + raise BadRequest(self.id + ' leverage should be between 1 and 125') + await self.load_markets() + market = self.market(symbol) + response = None + request: dict = { + 'contractName': market['id'], + 'leverage': leverage, + } + if not market['swap']: + raise NotSupported(self.id + ' setLeverage only support swap markets') + if market['linear']: + response = await self.fapiV2PrivatePostLevelEdit(self.extend(request, params)) + elif market['inverse']: + response = await self.dapiV2PrivatePostLevelEdit(self.extend(request, params)) + return response + + def parse_margin_modification(self, data, market=None) -> MarginModification: + # + # setMargin + # + # { + # "code": 0, + # "msg": "success" + # "data": null + # } + # + return { + 'info': data, + 'symbol': market['symbol'], + 'type': None, + 'marginMode': 'isolated', + 'amount': None, + 'total': None, + 'code': None, + 'status': None, + 'timestamp': None, + 'datetime': None, + } + + async def set_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + Either adds or reduces margin in an isolated position in order to set the margin to a specific value + + https://www.bitrue.com/api-docs#modify-isolated-position-margin-trade-hmac-sha256 + https://www.bitrue.com/api_docs_includes_file/delivery.html#modify-isolated-position-margin-trade-hmac-sha256 + + :param str symbol: unified market symbol of the market to set margin in + :param float amount: the amount to set the margin to + :param dict [params]: parameters specific to the exchange API endpoint + :returns dict: A `margin structure ` + """ + await self.load_markets() + market = self.market(symbol) + if not market['swap']: + raise NotSupported(self.id + ' setMargin only support swap markets') + response = None + request: dict = { + 'contractName': market['id'], + 'amount': self.parse_to_numeric(amount), + } + if market['linear']: + response = await self.fapiV2PrivatePostPositionMargin(self.extend(request, params)) + elif market['inverse']: + response = await self.dapiV2PrivatePostPositionMargin(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "success" + # "data": null + # } + # + return self.parse_margin_modification(response, market) + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + type = self.safe_string(api, 0) + version = self.safe_string(api, 1) + access = self.safe_string(api, 2) + url = None + if (type == 'api' and version == 'kline') or (type == 'open' and path.find('listenKey') >= 0): + url = self.urls['api'][type] + else: + url = self.urls['api'][type] + '/' + version + url = url + '/' + self.implode_params(path, params) + params = self.omit(params, self.extract_params(path)) + if access == 'private': + self.check_required_credentials() + recvWindow = self.safe_integer(self.options, 'recvWindow', 5000) + if type == 'spot' or type == 'open': + query = self.urlencode(self.extend({ + 'timestamp': self.nonce(), + 'recvWindow': recvWindow, + }, params)) + signature = self.hmac(self.encode(query), self.encode(self.secret), hashlib.sha256) + query += '&' + 'signature=' + signature + headers = { + 'X-MBX-APIKEY': self.apiKey, + } + if (method == 'GET') or (method == 'DELETE'): + url += '?' + query + else: + body = query + headers['Content-Type'] = 'application/x-www-form-urlencoded' + else: + timestamp = str(self.nonce()) + signPath = None + if type == 'fapi': + signPath = '/fapi' + elif type == 'dapi': + signPath = '/dapi' + signPath = signPath + '/' + version + '/' + path + signMessage = timestamp + method + signPath + if method == 'GET': + keys = list(params.keys()) + keysLength = len(keys) + if keysLength > 0: + signMessage += '?' + self.urlencode(params) + signature = self.hmac(self.encode(signMessage), self.encode(self.secret), hashlib.sha256) + headers = { + 'X-CH-APIKEY': self.apiKey, + 'X-CH-SIGN': signature, + 'X-CH-TS': timestamp, + } + url += '?' + self.urlencode(params) + else: + query = self.extend({ + 'recvWindow': recvWindow, + }, params) + body = self.json(query) + signMessage += body + signature = self.hmac(self.encode(signMessage), self.encode(self.secret), hashlib.sha256) + headers = { + 'Content-Type': 'application/json', + 'X-CH-APIKEY': self.apiKey, + 'X-CH-SIGN': signature, + 'X-CH-TS': timestamp, + } + else: + if params: + url += '?' + self.urlencode(params) + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if (code == 418) or (code == 429): + raise DDoSProtection(self.id + ' ' + str(code) + ' ' + reason + ' ' + body) + # error response in a form: {"code": -1013, "msg": "Invalid quantity."} + # following block cointains legacy checks against message patterns in "msg" property + # will switch "code" checks eventually, when we know all of them + if code >= 400: + if body.find('Price * QTY is zero or less') >= 0: + raise InvalidOrder(self.id + ' order cost = amount * price is zero or less ' + body) + if body.find('LOT_SIZE') >= 0: + raise InvalidOrder(self.id + ' order amount should be evenly divisible by lot size ' + body) + if body.find('PRICE_FILTER') >= 0: + raise InvalidOrder(self.id + ' order price is invalid, i.e. exceeds allowed price precision, exceeds min price or max price limits or is invalid float value in general, use self.price_to_precision(symbol, amount) ' + body) + if response is None: + return None # fallback to default error handler + # check success value for wapi endpoints + # response in format {'msg': 'The coin does not exist.', 'success': True/false} + success = self.safe_bool(response, 'success', True) + if not success: + messageInner = self.safe_string(response, 'msg') + parsedMessage = None + if messageInner is not None: + try: + parsedMessage = json.loads(messageInner) + except Exception as e: + # do nothing + parsedMessage = None + if parsedMessage is not None: + response = parsedMessage + message = self.safe_string(response, 'msg') + if message is not None: + self.throw_exactly_matched_exception(self.exceptions['exact'], message, self.id + ' ' + message) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, self.id + ' ' + message) + # checks against error codes + error = self.safe_string(response, 'code') + if error is not None: + # https://github.com/ccxt/ccxt/issues/6501 + # https://github.com/ccxt/ccxt/issues/7742 + if (error == '200') or Precise.string_equals(error, '0'): + return None + # a workaround for {"code":-2015,"msg":"Invalid API-key, IP, or permissions for action."} + # despite that their message is very confusing, it is raised by Binance + # on a temporary ban, the API key is valid, but disabled for a while + if (error == '-2015') and self.options['hasAlreadyAuthenticatedSuccessfully']: + raise DDoSProtection(self.id + ' temporary banned: ' + body) + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], error, feedback) + raise ExchangeError(feedback) + if not success: + raise ExchangeError(self.id + ' ' + body) + return None + + def calculate_rate_limiter_cost(self, api, method, path, params, config={}): + if ('noSymbol' in config) and not ('symbol' in params): + return config['noSymbol'] + elif ('byLimit' in config) and ('limit' in params): + limit = params['limit'] + byLimit = config['byLimit'] + for i in range(0, len(byLimit)): + entry = byLimit[i] + if limit <= entry[0]: + return entry[1] + return self.safe_value(config, 'cost', 1) diff --git a/ccxt/async_support/bitso.py b/ccxt/async_support/bitso.py new file mode 100644 index 0000000..c8238e3 --- /dev/null +++ b/ccxt/async_support/bitso.py @@ -0,0 +1,1816 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.bitso import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currency, DepositAddress, Int, LedgerEntry, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Trade, TradingFees, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import NotSupported +from ccxt.base.errors import InvalidNonce +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class bitso(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(bitso, self).describe(), { + 'id': 'bitso', + 'name': 'Bitso', + 'countries': ['MX'], # Mexico + 'rateLimit': 2000, # 30 requests per minute + 'version': 'v3', + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'cancelOrders': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createDepositAddress': False, + 'createOrder': True, + 'createOrderWithTakeProfitAndStopLoss': False, + 'createOrderWithTakeProfitAndStopLossWs': False, + 'createReduceOnlyOrder': False, + 'fetchAccounts': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': False, + 'fetchDeposit': True, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchDepositsWithdrawals': False, + 'fetchDepositWithdrawFee': 'emulated', + 'fetchDepositWithdrawFees': True, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLedger': True, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrderTrades': True, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTickers': False, + 'fetchTime': False, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': True, + 'fetchTransactionFee': False, + 'fetchTransactionFees': True, + 'fetchTransactions': False, + 'fetchTransfer': False, + 'fetchTransfers': False, + 'fetchVolatilityHistory': False, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'transfer': False, + 'withdraw': True, + }, + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/178c8e56-9054-4107-b192-5e5053d4f975', + 'api': { + 'rest': 'https://bitso.com/api', + }, + 'test': { + 'rest': 'https://stage.bitso.com/api', + }, + 'www': 'https://bitso.com', + 'doc': 'https://bitso.com/api_info', + 'fees': 'https://bitso.com/fees', + 'referral': 'https://bitso.com/?ref=itej', + }, + 'precisionMode': TICK_SIZE, + 'options': { + 'precision': { + 'XRP': 0.000001, + 'MXN': 0.01, + 'TUSD': 0.01, + }, + 'defaultPrecision': 0.00000001, + 'networks': { + 'TRC20': 'trx', + 'ERC20': 'erc20', + 'BEP20': 'bsc', + 'BEP2': 'bep2', + }, + }, + 'timeframes': { + '1m': '60', + '5m': '300', + '15m': '900', + '30m': '1800', + '1h': '3600', + '4h': '14400', + '12h': '43200', + '1d': '86400', + '1w': '604800', + }, + 'api': { + 'public': { + 'get': [ + 'available_books', + 'ticker', + 'order_book', + 'trades', + 'ohlc', + ], + }, + 'private': { + 'get': [ + 'account_status', + 'balance', + 'fees', + 'fundings', + 'fundings/{fid}', + 'funding_destination', + 'kyc_documents', + 'ledger', + 'ledger/trades', + 'ledger/fees', + 'ledger/fundings', + 'ledger/withdrawals', + 'mx_bank_codes', + 'open_orders', + 'order_trades/{oid}', + 'orders/{oid}', + 'user_trades', + 'user_trades/{tid}', + 'withdrawals/', + 'withdrawals/{wid}', + ], + 'post': [ + 'bitcoin_withdrawal', + 'debit_card_withdrawal', + 'ether_withdrawal', + 'orders', + 'phone_number', + 'phone_verification', + 'phone_withdrawal', + 'spei_withdrawal', + 'ripple_withdrawal', + 'bcash_withdrawal', + 'litecoin_withdrawal', + ], + 'delete': [ + 'orders', + 'orders/{oid}', + 'orders/all', + ], + }, + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, # todo implementation + 'triggerPriceType': None, + 'triggerDirection': None, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': None, + # todo: implementation for TIF + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyRequiresPrice': False, + 'marketBuyByCost': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, + 'daysBack': None, + 'untilDays': None, + 'symbolRequired': True, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 500, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOrders': None, + 'fetchClosedOrders': None, + 'fetchOHLCV': { + 'limit': 300, + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'exceptions': { + '0201': AuthenticationError, # Invalid Nonce or Invalid Credentials + '104': InvalidNonce, # Cannot perform request - nonce must be higher than 1520307203724237 + '0304': BadRequest, # {"success":false,"error":{"code":"0304","message":"The field time_bucket() is either invalid or missing"}} + }, + }) + + async def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + :param str [code]: unified currency code, default is None + :param int [since]: timestamp in ms of the earliest ledger entry, default is None + :param int [limit]: max number of ledger entries to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ledger structure ` + """ + request: dict = {} + if limit is not None: + request['limit'] = limit + response = await self.privateGetLedger(self.extend(request, params)) + # + # { + # "success": True, + # "payload": [{ + # "eid": "2510b3e2bc1c87f584500a18084f35ed", + # "created_at": "2022-06-08T12:21:42+0000", + # "balance_updates": [{ + # "amount": "0.00080000", + # "currency": "btc" + # }], + # "operation": "funding", + # "details": { + # "network": "btc", + # "method": "btc", + # "method_name": "Bitcoin", + # "asset": "btc", + # "protocol": "btc", + # "integration": "bitgo-v2", + # "fid": "6112c6369100d6ecceb7f54f17cf0511" + # } + # }] + # } + # + payload = self.safe_value(response, 'payload', []) + currency = self.safe_currency(code) + return self.parse_ledger(payload, currency, since, limit) + + def parse_ledger_entry_type(self, type): + types: dict = { + 'funding': 'transaction', + 'withdrawal': 'transaction', + 'trade': 'trade', + 'fee': 'fee', + } + return self.safe_string(types, type, type) + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + # + # { + # "eid": "2510b3e2bc1c87f584500a18084f35ed", + # "created_at": "2022-06-08T12:21:42+0000", + # "balance_updates": [{ + # "amount": "0.00080000", + # "currency": "btc" + # }], + # "operation": "funding", + # "details": { + # "network": "btc", + # "method": "btc", + # "method_name": "Bitcoin", + # "asset": "btc", + # "protocol": "btc", + # "integration": "bitgo-v2", + # "fid": "6112c6369100d6ecceb7f54f17cf0511" + # } + # } + # + # trade + # { + # "eid": "8976c6053f078f704f037d82a813678a", + # "created_at": "2022-06-08T17:01:48+0000", + # "balance_updates": [{ + # "amount": "59.21320500", + # "currency": "mxn" + # }, + # { + # "amount": "-0.00010000", + # "currency": "btc" + # } + # ], + # "operation": "trade", + # "details": { + # "tid": "72145428", + # "oid": "JO5TZmMZjzjlZDyT" + # } + # } + # + # fee + # { + # "eid": "cbbb3c8d4e41723d25d2850dcb7c3c74", + # "created_at": "2022-06-08T17:01:48+0000", + # "balance_updates": [{ + # "amount": "-0.38488583", + # "currency": "mxn" + # }], + # "operation": "fee", + # "details": { + # "tid": "72145428", + # "oid": "JO5TZmMZjzjlZDyT" + # } + # } + operation = self.safe_string(item, 'operation') + type = self.parse_ledger_entry_type(operation) + balanceUpdates = self.safe_value(item, 'balance_updates', []) + firstBalance = self.safe_value(balanceUpdates, 0, {}) + direction = None + fee = None + amount = self.safe_string(firstBalance, 'amount') + currencyId = self.safe_string(firstBalance, 'currency') + code = self.safe_currency_code(currencyId, currency) + currency = self.safe_currency(currencyId, currency) + details = self.safe_value(item, 'details', {}) + referenceId = self.safe_string_2(details, 'fid', 'wid') + if referenceId is None: + referenceId = self.safe_string(details, 'tid') + if operation == 'funding': + direction = 'in' + elif operation == 'withdrawal': + direction = 'out' + elif operation == 'trade': + direction = None + elif operation == 'fee': + direction = 'out' + cost = Precise.string_abs(amount) + fee = { + 'cost': cost, + 'currency': currency, + } + timestamp = self.parse8601(self.safe_string(item, 'created_at')) + return self.safe_ledger_entry({ + 'info': item, + 'id': self.safe_string(item, 'eid'), + 'direction': direction, + 'account': None, + 'referenceId': referenceId, + 'referenceAccount': None, + 'type': type, + 'currency': code, + 'amount': amount, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'before': None, + 'after': None, + 'status': 'ok', + 'fee': fee, + }, currency) + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for bitso + + https://docs.bitso.com/bitso-api/docs/list-available-books + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = await self.publicGetAvailableBooks(params) + # + # { + # "success":true, + # "payload":[ + # { + # "book":"btc_mxn", + # "minimum_price":"500", + # "maximum_price":"10000000", + # "minimum_amount":"0.00005", + # "maximum_amount":"500", + # "minimum_value":"5", + # "maximum_value":"10000000", + # "tick_size":"0.01", + # "fees":{ + # "flat_rate":{"maker":"0.500","taker":"0.650"}, + # "structure":[ + # {"volume":"1500000","maker":"0.00500","taker":"0.00650"}, + # {"volume":"2000000","maker":"0.00490","taker":"0.00637"}, + # {"volume":"5000000","maker":"0.00480","taker":"0.00624"}, + # {"volume":"7000000","maker":"0.00440","taker":"0.00572"}, + # {"volume":"10000000","maker":"0.00420","taker":"0.00546"}, + # {"volume":"15000000","maker":"0.00400","taker":"0.00520"}, + # {"volume":"35000000","maker":"0.00370","taker":"0.00481"}, + # {"volume":"50000000","maker":"0.00300","taker":"0.00390"}, + # {"volume":"150000000","maker":"0.00200","taker":"0.00260"}, + # {"volume":"250000000","maker":"0.00100","taker":"0.00130"}, + # {"volume":"9999999999","maker":"0.00000","taker":"0.00130"}, + # ] + # } + # }, + # ] + # } + markets = self.safe_value(response, 'payload', []) + result = [] + for i in range(0, len(markets)): + market = markets[i] + id = self.safe_string(market, 'book') + baseId, quoteId = id.split('_') + base = baseId.upper() + quote = quoteId.upper() + base = self.safe_currency_code(base) + quote = self.safe_currency_code(quote) + fees = self.safe_value(market, 'fees', {}) + flatRate = self.safe_value(fees, 'flat_rate', {}) + takerString = self.safe_string(flatRate, 'taker') + makerString = self.safe_string(flatRate, 'maker') + taker = self.parse_number(Precise.string_div(takerString, '100')) + maker = self.parse_number(Precise.string_div(makerString, '100')) + feeTiers = self.safe_value(fees, 'structure', []) + fee = { + 'taker': taker, + 'maker': maker, + 'percentage': True, + 'tierBased': True, + } + takerFees = [] + makerFees = [] + for j in range(0, len(feeTiers)): + tier = feeTiers[j] + volume = self.safe_number(tier, 'volume') + takerFee = self.safe_number(tier, 'taker') + makerFee = self.safe_number(tier, 'maker') + takerFees.append([volume, takerFee]) + makerFees.append([volume, makerFee]) + if j == 0: + fee['taker'] = takerFee + fee['maker'] = makerFee + tiers: dict = { + 'taker': takerFees, + 'maker': makerFees, + } + fee['tiers'] = tiers + # TODO: precisions can be also set from https://bitso.com/api/v3/catalogues ->available_currency_conversions->currencies(or ->currencies->metadata) or https://bitso.com/api/v3/get_exchange_rates/mxn + defaultPricePrecision = self.safe_number(self.options['precision'], quote, self.options['defaultPrecision']) + result.append(self.extend({ + 'id': id, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': None, + 'contract': False, + 'linear': None, + 'inverse': None, + 'taker': taker, + 'maker': maker, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.safe_number(self.options['precision'], base, self.options['defaultPrecision']), + 'price': self.safe_number(market, 'tick_size', defaultPricePrecision), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(market, 'minimum_amount'), + 'max': self.safe_number(market, 'maximum_amount'), + }, + 'price': { + 'min': self.safe_number(market, 'minimum_price'), + 'max': self.safe_number(market, 'maximum_price'), + }, + 'cost': { + 'min': self.safe_number(market, 'minimum_value'), + 'max': self.safe_number(market, 'maximum_value'), + }, + }, + 'created': None, + 'info': market, + }, fee)) + return result + + def parse_balance(self, response) -> Balances: + payload = self.safe_value(response, 'payload', {}) + balances = self.safe_value(payload, 'balances', []) + result: dict = { + 'info': response, + 'timestamp': None, + 'datetime': None, + } + for i in range(0, len(balances)): + balance = balances[i] + currencyId = self.safe_string(balance, 'currency') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(balance, 'available') + account['used'] = self.safe_string(balance, 'locked') + account['total'] = self.safe_string(balance, 'total') + result[code] = account + return self.safe_balance(result) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://docs.bitso.com/bitso-api/docs/get-account-balance + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + response = await self.privateGetBalance(params) + # + # { + # "success": True, + # "payload": { + # "balances": [ + # { + # "currency": "bat", + # "available": "0.00000000", + # "locked": "0.00000000", + # "total": "0.00000000", + # "pending_deposit": "0.00000000", + # "pending_withdrawal": "0.00000000" + # }, + # { + # "currency": "bch", + # "available": "0.00000000", + # "locked": "0.00000000", + # "total": "0.00000000", + # "pending_deposit": "0.00000000", + # "pending_withdrawal": "0.00000000" + # }, + # ], + # }, + # } + # + return self.parse_balance(response) + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://docs.bitso.com/bitso-api/docs/list-order-book + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'book': market['id'], + } + response = await self.publicGetOrderBook(self.extend(request, params)) + orderbook = self.safe_value(response, 'payload') + timestamp = self.parse8601(self.safe_string(orderbook, 'updated_at')) + return self.parse_order_book(orderbook, market['symbol'], timestamp, 'bids', 'asks', 'price', 'amount') + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "high":"37446.85", + # "last":"36599.54", + # "created_at":"2022-01-28T12:06:11+00:00", + # "book":"btc_usdt", + # "volume":"7.29075419", + # "vwap":"36579.1564400307", + # "low":"35578.52", + # "ask":"36574.76", + # "bid":"36538.22", + # "change_24":"-105.64" + # } + # + symbol = self.safe_symbol(None, market) + timestamp = self.parse8601(self.safe_string(ticker, 'created_at')) + vwap = self.safe_string(ticker, 'vwap') + baseVolume = self.safe_string(ticker, 'volume') + quoteVolume = Precise.string_mul(baseVolume, vwap) + last = self.safe_string(ticker, 'last') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': self.safe_string(ticker, 'bid'), + 'bidVolume': None, + 'ask': self.safe_string(ticker, 'ask'), + 'askVolume': None, + 'vwap': vwap, + 'open': None, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'info': ticker, + }, market) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://docs.bitso.com/bitso-api/docs/ticker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'book': market['id'], + } + response = await self.publicGetTicker(self.extend(request, params)) + ticker = self.safe_value(response, 'payload') + # + # { + # "success":true, + # "payload":{ + # "high":"37446.85", + # "last":"37051.96", + # "created_at":"2022-01-28T17:03:29+00:00", + # "book":"btc_usdt", + # "volume":"6.16176186", + # "vwap":"36582.6293169472", + # "low":"35578.52", + # "ask":"37083.62", + # "bid":"37039.66", + # "change_24":"478.45" + # } + # } + # + return self.parse_ticker(ticker, market) + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'book': market['id'], + 'time_bucket': self.safe_string(self.timeframes, timeframe, timeframe), + } + if since is not None: + request['start'] = since + if limit is not None: + duration = self.parse_timeframe(timeframe) + request['end'] = self.sum(since, duration * limit * 1000) + elif limit is not None: + now = self.milliseconds() + request['end'] = now + request['start'] = now - self.parse_timeframe(timeframe) * 1000 * limit + response = await self.publicGetOhlc(self.extend(request, params)) + # + # { + # "success":true, + # "payload": [ + # { + # "bucket_start_time":1648219140000, + # "first_trade_time":1648219154990, + # "last_trade_time":1648219189442, + # "first_rate":"44958.60", + # "last_rate":"44979.88", + # "min_rate":"44957.33", + # "max_rate":"44979.88", + # "trade_count":8, + # "volume":"0.00082814", + # "vwap":"44965.02" + # }, + # ] + # } + # + payload = self.safe_list(response, 'payload', []) + return self.parse_ohlcvs(payload, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # { + # "bucket_start_time":1648219140000, + # "first_trade_time":1648219154990, + # "last_trade_time":1648219189441, + # "first_rate":"44958.60", + # "last_rate":"44979.88", + # "min_rate":"44957.33", + # "max_rate":"44979.88", + # "trade_count":8, + # "volume":"0.00082814", + # "vwap":"44965.02" + # }, + # + return [ + self.safe_integer(ohlcv, 'bucket_start_time'), + self.safe_number(ohlcv, 'first_rate'), + self.safe_number(ohlcv, 'max_rate'), + self.safe_number(ohlcv, 'min_rate'), + self.safe_number(ohlcv, 'last_rate'), + self.safe_number(ohlcv, 'volume'), + ] + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(public) + # + # { + # "book": "btc_usdt", + # "created_at": "2021-11-24T12:14:53+0000", + # "amount": "0.00026562", + # "maker_side": "sell", + # "price": "56471.55", + # "tid": "52557338" + # } + # + # fetchMyTrades(private) + # + # { + # "book": "btc_usdt", + # "created_at": "2021-11-24T12:31:03+0000", + # "minor": "11.30356000", + # "major": "-0.00020000", + # "fees_amount": "0.01119052", + # "fees_currency": "usdt", + # "minor_currency": "usdt", + # "major_currency": "btc", + # "oid": "djTzMIWx2Vi3iMjl", + # "tid": "52559051", + # "price": "56517.80", + # "side": "sell", + # "maker_side": "buy" + # } + # + # fetchOrderTrades(private) + # + # { + # "book": "btc_usdt", + # "created_at": "2021-11-24T12:30:52+0000", + # "minor": "-11.33047916", + # "major": "0.00020020", + # "fees_amount": "0.00000020", + # "fees_currency": "btc", + # "minor_currency": "usdt", + # "major_currency": "btc", + # "oid": "O0D2zcljjjQF5xlG", + # "tid": "52559030", + # "price": "56595.80", + # "side": "buy", + # "maker_side": "sell" + # } + # + timestamp = self.parse8601(self.safe_string(trade, 'created_at')) + marketId = self.safe_string(trade, 'book') + symbol = self.safe_symbol(marketId, market, '_') + side = self.safe_string(trade, 'side') + makerSide = self.safe_string(trade, 'maker_side') + takerOrMaker = None + if side is not None: + if side == makerSide: + takerOrMaker = 'maker' + else: + takerOrMaker = 'taker' + else: + if makerSide == 'buy': + side = 'sell' + else: + side = 'buy' + amount = self.safe_string_2(trade, 'amount', 'major') + if amount is not None: + amount = Precise.string_abs(amount) + fee = None + feeCost = self.safe_string(trade, 'fees_amount') + if feeCost is not None: + feeCurrencyId = self.safe_string(trade, 'fees_currency') + feeCurrency = self.safe_currency_code(feeCurrencyId) + fee = { + 'cost': feeCost, + 'currency': feeCurrency, + } + cost = self.safe_string(trade, 'minor') + if cost is not None: + cost = Precise.string_abs(cost) + price = self.safe_string(trade, 'price') + orderId = self.safe_string(trade, 'oid') + id = self.safe_string(trade, 'tid') + return self.safe_trade({ + 'id': id, + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'order': orderId, + 'type': None, + 'side': side, + 'takerOrMaker': takerOrMaker, + 'price': price, + 'amount': amount, + 'cost': cost, + 'fee': fee, + }, market) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://docs.bitso.com/bitso-api/docs/list-trades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'book': market['id'], + } + response = await self.publicGetTrades(self.extend(request, params)) + return self.parse_trades(response['payload'], market, since, limit) + + async def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://docs.bitso.com/bitso-api/docs/list-fees + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + await self.load_markets() + response = await self.privateGetFees(params) + # + # { + # "success": True, + # "payload": { + # "fees": [ + # { + # "book": "btc_mxn", + # "fee_percent": "0.6500", + # "fee_decimal": "0.00650000", + # "taker_fee_percent": "0.6500", + # "taker_fee_decimal": "0.00650000", + # "maker_fee_percent": "0.5000", + # "maker_fee_decimal": "0.00500000", + # "volume_currency": "mxn", + # "current_volume": "0.00", + # "next_volume": "1500000.00", + # "next_maker_fee_percent": "0.490", + # "next_taker_fee_percent": "0.637", + # "nextVolume": "1500000.00", + # "nextFee": "0.490", + # "nextTakerFee": "0.637" + # }, + # ... + # ], + # "deposit_fees": [ + # { + # "currency": "btc", + # "method": "rewards", + # "fee": "0.00", + # "is_fixed": False + # }, + # ... + # ], + # "withdrawal_fees": { + # "ada": "0.20958100", + # "bch": "0.00009437", + # "ars": "0", + # "btc": "0.00001209", + # ... + # } + # } + # } + # + payload = self.safe_value(response, 'payload', {}) + fees = self.safe_value(payload, 'fees', []) + result: dict = {} + for i in range(0, len(fees)): + fee = fees[i] + marketId = self.safe_string(fee, 'book') + symbol = self.safe_symbol(marketId, None, '_') + result[symbol] = { + 'info': fee, + 'symbol': symbol, + 'maker': self.safe_number(fee, 'maker_fee_decimal'), + 'taker': self.safe_number(fee, 'taker_fee_decimal'), + 'percentage': True, + 'tierBased': True, + } + return result + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = 25, params={}): + """ + fetch all trades made by the user + + https://docs.bitso.com/bitso-api/docs/user-trades + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + # the don't support fetching trades starting from a date yet + # use the `marker` extra param for that + # self is not a typo, the variable name is 'marker'(don't confuse with 'market') + markerInParams = ('marker' in params) + # warn the user with an exception if the user wants to filter + # starting from since timestamp, but does not set the trade id with an extra 'marker' param + if (since is not None) and not markerInParams: + raise ExchangeError(self.id + ' fetchMyTrades() does not support fetching trades starting from a timestamp with the `since` argument, use the `marker` extra param to filter starting from an integer trade id') + # convert it to an integer unconditionally + if markerInParams: + params = self.extend(params, { + 'marker': int(params['marker']), + }) + request: dict = { + 'book': market['id'], + 'limit': limit, # default = 25, max = 100 + # 'sort': 'desc', # default = desc + # 'marker': id, # integer id to start from + } + response = await self.privateGetUserTrades(self.extend(request, params)) + return self.parse_trades(response['payload'], market, since, limit) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://docs.bitso.com/bitso-api/docs/place-an-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'book': market['id'], + 'side': side, + 'type': type, + 'major': self.amount_to_precision(market['symbol'], amount), + } + if type == 'limit': + request['price'] = self.price_to_precision(market['symbol'], price) + response = await self.privatePostOrders(self.extend(request, params)) + id = self.safe_string(response['payload'], 'oid') + return self.safe_order({ + 'info': response, + 'id': id, + }, market) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://docs.bitso.com/bitso-api/docs/cancel-an-order + + :param str id: order id + :param str symbol: not used by bitso cancelOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + request: dict = { + 'oid': id, + } + response = await self.privateDeleteOrdersOid(self.extend(request, params)) + # + # { + # "success": True, + # "payload": ["yWTQGxDMZ0VimZgZ"] + # } + # + payload = self.safe_list(response, 'payload', []) + orderId = self.safe_string(payload, 0) + return self.safe_order({ + 'info': response, + 'id': orderId, + }) + + async def cancel_orders(self, ids: List[str], symbol: Str = None, params={}) -> List[Order]: + """ + cancel multiple orders + + https://docs.bitso.com/bitso-api/docs/cancel-an-order + + :param str[] ids: order ids + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an list of `order structures ` + """ + if not isinstance(ids, list): + raise ArgumentsRequired(self.id + ' cancelOrders() ids argument should be an array') + market = None + if symbol is not None: + market = self.market(symbol) + oids = ','.join(ids) + request: dict = { + 'oids': oids, + } + response = await self.privateDeleteOrders(self.extend(request, params)) + # + # { + # "success": True, + # "payload": ["yWTQGxDMZ0VimZgZ"] + # } + # + payload = self.safe_value(response, 'payload', []) + orders = [] + for i in range(0, len(payload)): + id = payload[i] + orders.append(self.parse_order(id, market)) + return orders + + async def cancel_all_orders(self, symbol: Str = None, params={}) -> List[Order]: + """ + cancel all open orders + + https://docs.bitso.com/bitso-api/docs/cancel-an-order + + :param None symbol: bitso does not support canceling orders for only a specific market + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + if symbol is not None: + raise NotSupported(self.id + ' cancelAllOrders() deletes all orders for user, it does not support filtering by symbol.') + response = await self.privateDeleteOrdersAll(params) + # + # { + # "success": True, + # "payload": ["NWUZUYNT12ljwzDT", "kZUkZmQ2TTjkkYTY"] + # } + # + payload = self.safe_value(response, 'payload', []) + canceledOrders = [] + for i in range(0, len(payload)): + order = self.parse_order(payload[i]) + canceledOrders.append(order) + return canceledOrders + + def parse_order_status(self, status: Str): + statuses: dict = { + 'partial-fill': 'open', # self is a common substitution in ccxt + 'partially filled': 'open', + 'queued': 'open', + 'completed': 'closed', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # + # canceledOrder + # yWTQGxDMZ0VimZgZ + # + id = None + if isinstance(order, str): + id = order + else: + id = self.safe_string(order, 'oid') + side = self.safe_string(order, 'side') + status = self.parse_order_status(self.safe_string(order, 'status')) + marketId = self.safe_string(order, 'book') + symbol = self.safe_symbol(marketId, market, '_') + orderType = self.safe_string(order, 'type') + timestamp = self.parse8601(self.safe_string(order, 'created_at')) + price = self.safe_string(order, 'price') + amount = self.safe_string(order, 'original_amount') + remaining = self.safe_string(order, 'unfilled_amount') + clientOrderId = self.safe_string(order, 'client_id') + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'symbol': symbol, + 'type': orderType, + 'timeInForce': None, + 'postOnly': None, + 'side': side, + 'price': price, + 'triggerPrice': None, + 'amount': amount, + 'cost': None, + 'remaining': remaining, + 'filled': None, + 'status': status, + 'fee': None, + 'average': None, + 'trades': None, + }, market) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = 25, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://docs.bitso.com/bitso-api/docs/list-open-orders + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + market = self.market(symbol) + # the don't support fetching trades starting from a date yet + # use the `marker` extra param for that + # self is not a typo, the variable name is 'marker'(don't confuse with 'market') + markerInParams = ('marker' in params) + # warn the user with an exception if the user wants to filter + # starting from since timestamp, but does not set the trade id with an extra 'marker' param + if (since is not None) and not markerInParams: + raise ExchangeError(self.id + ' fetchOpenOrders() does not support fetching orders starting from a timestamp with the `since` argument, use the `marker` extra param to filter starting from an integer trade id') + # convert it to an integer unconditionally + if markerInParams: + params = self.extend(params, { + 'marker': int(params['marker']), + }) + request: dict = { + 'book': market['id'], + 'limit': limit, # default = 25, max = 100 + # 'sort': 'desc', # default = desc + # 'marker': id, # integer id to start from + } + response = await self.privateGetOpenOrders(self.extend(request, params)) + orders = self.parse_orders(response['payload'], market, since, limit) + return orders + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://docs.bitso.com/bitso-api/docs/look-up-orders + + :param str id: the order id + :param str symbol: not used by bitso fetchOrder + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + response = await self.privateGetOrdersOid({ + 'oid': id, + }) + payload = self.safe_value(response, 'payload') + if isinstance(payload, list): + numOrders = len(response['payload']) + if numOrders == 1: + return self.parse_order(payload[0]) + raise OrderNotFound(self.id + ': The order ' + id + ' not found.') + + async def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all the trades made from a single order + + https://docs.bitso.com/bitso-api/docs/list-user-trades + + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'oid': id, + } + response = await self.privateGetOrderTradesOid(self.extend(request, params)) + return self.parse_trades(response['payload'], market) + + async def fetch_deposit(self, id: str, code: Str = None, params={}): + """ + fetch information on a deposit + + https://docs.bitso.com/bitso-payouts-funding/docs/fundings + + :param str id: deposit id + :param str code: bitso does not support filtering by currency code and will ignore self argument + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + await self.load_markets() + request: dict = { + 'fid': id, + } + response = await self.privateGetFundingsFid(self.extend(request, params)) + # + # { + # "success": True, + # "payload": [{ + # "fid": "6112c6369100d6ecceb7f54f17cf0511", + # "status": "complete", + # "created_at": "2022-06-08T12:02:49+0000", + # "currency": "btc", + # "method": "btc", + # "method_name": "Bitcoin", + # "amount": "0.00080000", + # "asset": "btc", + # "network": "btc", + # "protocol": "btc", + # "integration": "bitgo-v2", + # "details": { + # "receiving_address": "3N2vbcYKhogs6RoTb4eYCUJ3beRSqLgSif", + # "tx_hash": "327f3838531f211485ec59f9d0a119fea1595591e274d942b2c10b9b8262eb1d", + # "confirmations": "4" + # } + # }] + # } + # + transactions = self.safe_value(response, 'payload', []) + first = self.safe_dict(transactions, 0, {}) + return self.parse_transaction(first) + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://docs.bitso.com/bitso-payouts-funding/docs/fundings + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + currency = None + if code is not None: + currency = self.currency(code) + response = await self.privateGetFundings(params) + # + # { + # "success": True, + # "payload": [{ + # "fid": "6112c6369100d6ecceb7f54f17cf0511", + # "status": "complete", + # "created_at": "2022-06-08T12:02:49+0000", + # "currency": "btc", + # "method": "btc", + # "method_name": "Bitcoin", + # "amount": "0.00080000", + # "asset": "btc", + # "network": "btc", + # "protocol": "btc", + # "integration": "bitgo-v2", + # "details": { + # "receiving_address": "3N2vbcYKhogs6RoTb4eYCUJ3beRSqLgSif", + # "tx_hash": "327f3838531f211485ec59f9d0a119fea1595591e274d942b2c10b9b8262eb1d", + # "confirmations": "4" + # } + # }] + # } + # + transactions = self.safe_list(response, 'payload', []) + return self.parse_transactions(transactions, currency, since, limit, params) + + async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'fund_currency': currency['id'], + } + response = await self.privateGetFundingDestination(self.extend(request, params)) + address = self.safe_string(response['payload'], 'account_identifier') + tag = None + if address.find('?dt=') >= 0: + parts = address.split('?dt=') + address = self.safe_string(parts, 0) + tag = self.safe_string(parts, 1) + self.check_address(address) + return { + 'info': response, + 'currency': code, + 'network': None, + 'address': address, + 'tag': tag, + } + + async def fetch_transaction_fees(self, codes: Strings = None, params={}): + """ + @deprecated + please use fetchDepositWithdrawFees instead + + https://docs.bitso.com/bitso-api/docs/list-fees + + :param str[]|None codes: list of unified currency codes + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `fee structures ` + """ + await self.load_markets() + response = await self.privateGetFees(params) + # + # { + # "success": True, + # "payload": { + # "fees": [ + # { + # "book": "btc_mxn", + # "fee_percent": "0.6500", + # "fee_decimal": "0.00650000", + # "taker_fee_percent": "0.6500", + # "taker_fee_decimal": "0.00650000", + # "maker_fee_percent": "0.5000", + # "maker_fee_decimal": "0.00500000", + # "volume_currency": "mxn", + # "current_volume": "0.00", + # "next_volume": "1500000.00", + # "next_maker_fee_percent": "0.490", + # "next_taker_fee_percent": "0.637", + # "nextVolume": "1500000.00", + # "nextFee": "0.490", + # "nextTakerFee": "0.637" + # }, + # ... + # ], + # "deposit_fees": [ + # { + # "currency": "btc", + # "method": "rewards", + # "fee": "0.00", + # "is_fixed": False + # }, + # ... + # ], + # "withdrawal_fees": { + # "ada": "0.20958100", + # "bch": "0.00009437", + # "ars": "0", + # "btc": "0.00001209", + # ... + # } + # } + # } + # + result: dict = {} + payload = self.safe_value(response, 'payload', {}) + depositFees = self.safe_value(payload, 'deposit_fees', []) + for i in range(0, len(depositFees)): + depositFee = depositFees[i] + currencyId = self.safe_string(depositFee, 'currency') + code = self.safe_currency_code(currencyId) + if (codes is not None) and not self.in_array(code, codes): + continue + result[code] = { + 'deposit': self.safe_number(depositFee, 'fee'), + 'withdraw': None, + 'info': { + 'deposit': depositFee, + 'withdraw': None, + }, + } + withdrawalFees = self.safe_value(payload, 'withdrawal_fees', []) + currencyIds = list(withdrawalFees.keys()) + for i in range(0, len(currencyIds)): + currencyId = currencyIds[i] + code = self.safe_currency_code(currencyId) + if (codes is not None) and not self.in_array(code, codes): + continue + result[code] = { + 'deposit': self.safe_value(result[code], 'deposit'), + 'withdraw': self.safe_number(withdrawalFees, currencyId), + 'info': { + 'deposit': self.safe_value(result[code]['info'], 'deposit'), + 'withdraw': self.safe_number(withdrawalFees, currencyId), + }, + } + return result + + async def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}): + """ + fetch deposit and withdraw fees + + https://docs.bitso.com/bitso-api/docs/list-fees + + :param str[]|None codes: list of unified currency codes + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `fee structures ` + """ + await self.load_markets() + response = await self.privateGetFees(params) + # + # { + # "success": True, + # "payload": { + # "fees": [ + # { + # "book": "btc_mxn", + # "fee_percent": "0.6500", + # "fee_decimal": "0.00650000", + # "taker_fee_percent": "0.6500", + # "taker_fee_decimal": "0.00650000", + # "maker_fee_percent": "0.5000", + # "maker_fee_decimal": "0.00500000", + # "volume_currency": "mxn", + # "current_volume": "0.00", + # "next_volume": "1500000.00", + # "next_maker_fee_percent": "0.490", + # "next_taker_fee_percent": "0.637", + # "nextVolume": "1500000.00", + # "nextFee": "0.490", + # "nextTakerFee": "0.637" + # }, + # ... + # ], + # "deposit_fees": [ + # { + # "currency": "btc", + # "method": "rewards", + # "fee": "0.00", + # "is_fixed": False + # }, + # ... + # ], + # "withdrawal_fees": { + # "ada": "0.20958100", + # "bch": "0.00009437", + # "ars": "0", + # "btc": "0.00001209", + # ... + # } + # } + # } + # + payload = self.safe_list(response, 'payload', []) + return self.parse_deposit_withdraw_fees(payload, codes) + + def parse_deposit_withdraw_fees(self, response, codes=None, currencyIdKey=None): + # + # { + # "fees": [ + # { + # "book": "btc_mxn", + # "fee_percent": "0.6500", + # "fee_decimal": "0.00650000", + # "taker_fee_percent": "0.6500", + # "taker_fee_decimal": "0.00650000", + # "maker_fee_percent": "0.5000", + # "maker_fee_decimal": "0.00500000", + # "volume_currency": "mxn", + # "current_volume": "0.00", + # "next_volume": "1500000.00", + # "next_maker_fee_percent": "0.490", + # "next_taker_fee_percent": "0.637", + # "nextVolume": "1500000.00", + # "nextFee": "0.490", + # "nextTakerFee": "0.637" + # }, + # ... + # ], + # "deposit_fees": [ + # { + # "currency": "btc", + # "method": "rewards", + # "fee": "0.00", + # "is_fixed": False + # }, + # ... + # ], + # "withdrawal_fees": { + # "ada": "0.20958100", + # "bch": "0.00009437", + # "ars": "0", + # "btc": "0.00001209", + # ... + # } + # } + # + result: dict = {} + depositResponse = self.safe_value(response, 'deposit_fees', []) + withdrawalResponse = self.safe_value(response, 'withdrawal_fees', []) + for i in range(0, len(depositResponse)): + entry = depositResponse[i] + currencyId = self.safe_string(entry, 'currency') + code = self.safe_currency_code(currencyId) + if (codes is None) or (code in codes): + result[code] = { + 'deposit': { + 'fee': self.safe_number(entry, 'fee'), + 'percentage': not self.safe_value(entry, 'is_fixed'), + }, + 'withdraw': { + 'fee': None, + 'percentage': None, + }, + 'networks': {}, + 'info': entry, + } + withdrawalKeys = list(withdrawalResponse.keys()) + for i in range(0, len(withdrawalKeys)): + currencyId = withdrawalKeys[i] + code = self.safe_currency_code(currencyId) + if (codes is None) or (code in codes): + withdrawFee = self.parse_number(withdrawalResponse[currencyId]) + resultValue = self.safe_value(result, code) + if resultValue is None: + result[code] = self.deposit_withdraw_fee({}) + result[code]['withdraw']['fee'] = withdrawFee + result[code]['info'][code] = withdrawFee + return result + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.check_address(address) + await self.load_markets() + methods: dict = { + 'BTC': 'Bitcoin', + 'ETH': 'Ether', + 'XRP': 'Ripple', + 'BCH': 'Bcash', + 'LTC': 'Litecoin', + } + currency = self.currency(code) + method = methods[code] if (code in methods) else None + if method is None: + raise ExchangeError(self.id + ' not valid withdraw coin: ' + code) + request: dict = { + 'amount': amount, + 'address': address, + 'destination_tag': tag, + } + classMethod = 'privatePost' + method + 'Withdrawal' + response = await getattr(self, classMethod)(self.extend(request, params)) + # + # { + # "success": True, + # "payload": [ + # { + # "wid": "c5b8d7f0768ee91d3b33bee648318688", + # "status": "pending", + # "created_at": "2016-04-08T17:52:31.000+00:00", + # "currency": "btc", + # "method": "Bitcoin", + # "amount": "0.48650929", + # "details": { + # "withdrawal_address": "18MsnATiNiKLqUHDTRKjurwMg7inCrdNEp", + # "tx_hash": "d4f28394693e9fb5fffcaf730c11f32d1922e5837f76ca82189d3bfe30ded433" + # } + # }, + # ] + # } + # + payload = self.safe_value(response, 'payload', []) + first = self.safe_dict(payload, 0) + return self.parse_transaction(first, currency) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # deposit + # { + # "fid": "6112c6369100d6ecceb7f54f17cf0511", + # "status": "complete", + # "created_at": "2022-06-08T12:02:49+0000", + # "currency": "btc", + # "method": "btc", + # "method_name": "Bitcoin", + # "amount": "0.00080000", + # "asset": "btc", + # "network": "btc", + # "protocol": "btc", + # "integration": "bitgo-v2", + # "details": { + # "receiving_address": "3NmvbcYKhogs6RoTb4eYCUJ3beRSqLgSif", + # "tx_hash": "327f3838531f611485ec59f9d0a119fea1595591e274d942b2c10b9b8262eb1d", + # "confirmations": "4" + # } + # } + # + # withdraw + # + # { + # "wid": "c5b8d7f0768ee91d3b33bee648318688", + # "status": "pending", + # "created_at": "2016-04-08T17:52:31.000+00:00", + # "currency": "btc", + # "method": "Bitcoin", + # "amount": "0.48650929", + # "details": { + # "withdrawal_address": "18MsnATiNiKLqUHDTRKjurwMg7inCrdNEp", + # "tx_hash": "d4f28394693e9fb5fffcaf730c11f32d1922e5837f76ca82189d3bfe30ded433" + # } + # } + # + currencyId = self.safe_string_2(transaction, 'currency', 'asset') + currency = self.safe_currency(currencyId, currency) + details = self.safe_value(transaction, 'details', {}) + datetime = self.safe_string(transaction, 'created_at') + withdrawalAddress = self.safe_string(details, 'withdrawal_address') + receivingAddress = self.safe_string(details, 'receiving_address') + networkId = self.safe_string_2(transaction, 'network', 'method') + status = self.safe_string(transaction, 'status') + withdrawId = self.safe_string(transaction, 'wid') + networkCode = self.network_id_to_code(networkId) + networkCodeUpper = networkCode.upper() if (networkCode is not None) else None + return { + 'id': self.safe_string_2(transaction, 'wid', 'fid'), + 'txid': self.safe_string(details, 'tx_hash'), + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + 'network': networkCodeUpper, + 'addressFrom': receivingAddress, + 'address': withdrawalAddress if (withdrawalAddress is not None) else receivingAddress, + 'addressTo': withdrawalAddress, + 'amount': self.safe_number(transaction, 'amount'), + 'type': 'deposit' if (withdrawId is None) else 'withdrawal', + 'currency': self.safe_currency_code(currencyId, currency), + 'status': self.parse_transaction_status(status), + 'updated': None, + 'tagFrom': None, + 'tag': None, + 'tagTo': None, + 'comment': None, + 'internal': None, + 'fee': None, + 'info': transaction, + } + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'pending': 'pending', + 'in_progress': 'pending', + 'complete': 'ok', + 'failed': 'failed', + } + return self.safe_string(statuses, status, status) + + def nonce(self): + return self.milliseconds() + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + endpoint = '/' + self.version + '/' + self.implode_params(path, params) + query = self.omit(params, self.extract_params(path)) + if method == 'GET' or method == 'DELETE': + if query: + endpoint += '?' + self.urlencode(query) + url = self.urls['api']['rest'] + endpoint + if api == 'private': + self.check_required_credentials() + nonce = str(self.nonce()) + endpoint = '/api' + endpoint + request = ''.join([nonce, method, endpoint]) + if method != 'GET' and method != 'DELETE': + if query: + body = self.json(query) + request += body + signature = self.hmac(self.encode(request), self.encode(self.secret), hashlib.sha256) + auth = self.apiKey + ':' + nonce + ':' + signature + headers = { + 'Authorization': 'Bitso ' + auth, + # 'Content-Type': 'application/json', + } + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None # fallback to default error handler + if 'success' in response: + # + # {"success":false,"error":{"code":104,"message":"Cannot perform request - nonce must be higher than 1520307203724237"}} + # + success = self.safe_bool(response, 'success', False) + if isinstance(success, str): + if (success == 'true') or (success == '1'): + success = True + else: + success = False + if not success: + feedback = self.id + ' ' + self.json(response) + error = self.safe_value(response, 'error') + if error is None: + raise ExchangeError(feedback) + code = self.safe_string(error, 'code') + self.throw_exactly_matched_exception(self.exceptions, code, feedback) + raise ExchangeError(feedback) + return None diff --git a/ccxt/async_support/bitstamp.py b/ccxt/async_support/bitstamp.py new file mode 100644 index 0000000..fb3f5db --- /dev/null +++ b/ccxt/async_support/bitstamp.py @@ -0,0 +1,2337 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.bitstamp import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currencies, Currency, DepositAddress, Int, LedgerEntry, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, TradingFeeInterface, TradingFees, Transaction, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import AccountSuspended +from ccxt.base.errors import BadRequest +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidAddress +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import NotSupported +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.errors import OnMaintenance +from ccxt.base.errors import InvalidNonce +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class bitstamp(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(bitstamp, self).describe(), { + 'id': 'bitstamp', + 'name': 'Bitstamp', + 'countries': ['GB'], + # 8000 requests per 10 minutes = 8000 / 600 = 13.33333333 requests per second => 1000ms / 13.33333333 = 75ms between requests on average + 'rateLimit': 75, + 'version': 'v2', + 'userAgent': self.userAgents['chrome'], + 'pro': True, + 'has': { + 'CORS': True, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createOrder': True, + 'createOrderWithTakeProfitAndStopLoss': False, + 'createOrderWithTakeProfitAndStopLossWs': False, + 'createReduceOnlyOrder': False, + 'createStopLimitOrder': False, + 'createStopMarketOrder': False, + 'createStopOrder': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDepositsWithdrawals': True, + 'fetchDepositWithdrawFee': 'emulated', + 'fetchDepositWithdrawFees': True, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLedger': True, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTrades': True, + 'fetchTradingFee': True, + 'fetchTradingFees': True, + 'fetchTransactionFees': True, + 'fetchTransactions': 'emulated', + 'fetchVolatilityHistory': False, + 'fetchWithdrawals': True, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'transfer': True, + 'withdraw': True, + }, + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/d5480572-1fee-43cb-b900-d38c522d0024', + 'api': { + 'public': 'https://www.bitstamp.net/api', + 'private': 'https://www.bitstamp.net/api', + }, + 'www': 'https://www.bitstamp.net', + 'doc': 'https://www.bitstamp.net/api', + }, + 'timeframes': { + '1m': '60', + '3m': '180', + '5m': '300', + '15m': '900', + '30m': '1800', + '1h': '3600', + '2h': '7200', + '4h': '14400', + '6h': '21600', + '12h': '43200', + '1d': '86400', + '1w': '259200', + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + }, + 'api': { + 'public': { + 'get': { + 'ohlc/{pair}/': 1, + 'order_book/{pair}/': 1, + 'ticker/': 1, + 'ticker_hour/{pair}/': 1, + 'ticker/{pair}/': 1, + 'transactions/{pair}/': 1, + 'trading-pairs-info/': 1, + 'currencies/': 1, + 'eur_usd/': 1, + 'travel_rule/vasps/': 1, + }, + }, + 'private': { + 'get': { + 'travel_rule/contacts/': 1, + 'contacts/{contact_uuid}/': 1, + 'earn/subscriptions/': 1, + 'earn/transactions/': 1, + }, + 'post': { + 'account_balances/': 1, + 'account_balances/{currency}/': 1, + 'balance/': 1, + 'balance/{pair}/': 1, + 'bch_withdrawal/': 1, + 'bch_address/': 1, + 'user_transactions/': 1, + 'user_transactions/{pair}/': 1, + 'crypto-transactions/': 1, + 'open_order': 1, + 'open_orders/all/': 1, + 'open_orders/{pair}/': 1, + 'order_status/': 1, + 'cancel_order/': 1, + 'cancel_all_orders/': 1, + 'cancel_all_orders/{pair}/': 1, + 'buy/{pair}/': 1, + 'buy/market/{pair}/': 1, + 'buy/instant/{pair}/': 1, + 'sell/{pair}/': 1, + 'sell/market/{pair}/': 1, + 'sell/instant/{pair}/': 1, + 'transfer-to-main/': 1, + 'transfer-from-main/': 1, + 'my_trading_pairs/': 1, + 'fees/trading/': 1, + 'fees/trading/{market_symbol}': 1, + 'fees/withdrawal/': 1, + 'fees/withdrawal/{currency}/': 1, + 'withdrawal-requests/': 1, + 'withdrawal/open/': 1, + 'withdrawal/status/': 1, + 'withdrawal/cancel/': 1, + 'liquidation_address/new/': 1, + 'liquidation_address/info/': 1, + 'btc_unconfirmed/': 1, + 'websockets_token/': 1, + # individual coins + 'btc_withdrawal/': 1, + 'btc_address/': 1, + 'ripple_withdrawal/': 1, + 'ripple_address/': 1, + 'ltc_withdrawal/': 1, + 'ltc_address/': 1, + 'eth_withdrawal/': 1, + 'eth_address/': 1, + 'xrp_withdrawal/': 1, + 'xrp_address/': 1, + 'xlm_withdrawal/': 1, + 'xlm_address/': 1, + 'pax_withdrawal/': 1, + 'pax_address/': 1, + 'link_withdrawal/': 1, + 'link_address/': 1, + 'usdc_withdrawal/': 1, + 'usdc_address/': 1, + 'omg_withdrawal/': 1, + 'omg_address/': 1, + 'dai_withdrawal/': 1, + 'dai_address/': 1, + 'knc_withdrawal/': 1, + 'knc_address/': 1, + 'mkr_withdrawal/': 1, + 'mkr_address/': 1, + 'zrx_withdrawal/': 1, + 'zrx_address/': 1, + 'gusd_withdrawal/': 1, + 'gusd_address/': 1, + 'aave_withdrawal/': 1, + 'aave_address/': 1, + 'bat_withdrawal/': 1, + 'bat_address/': 1, + 'uma_withdrawal/': 1, + 'uma_address/': 1, + 'snx_withdrawal/': 1, + 'snx_address/': 1, + 'uni_withdrawal/': 1, + 'uni_address/': 1, + 'yfi_withdrawal/': 1, + 'yfi_address/': 1, + 'audio_withdrawal/': 1, + 'audio_address/': 1, + 'crv_withdrawal/': 1, + 'crv_address/': 1, + 'algo_withdrawal/': 1, + 'algo_address/': 1, + 'comp_withdrawal/': 1, + 'comp_address/': 1, + 'grt_withdrawal/': 1, + 'grt_address/': 1, + 'usdt_withdrawal/': 1, + 'usdt_address/': 1, + 'eurt_withdrawal/': 1, + 'eurt_address/': 1, + 'matic_withdrawal/': 1, + 'matic_address/': 1, + 'sushi_withdrawal/': 1, + 'sushi_address/': 1, + 'chz_withdrawal/': 1, + 'chz_address/': 1, + 'enj_withdrawal/': 1, + 'enj_address/': 1, + 'alpha_withdrawal/': 1, + 'alpha_address/': 1, + 'ftt_withdrawal/': 1, + 'ftt_address/': 1, + 'storj_withdrawal/': 1, + 'storj_address/': 1, + 'axs_withdrawal/': 1, + 'axs_address/': 1, + 'sand_withdrawal/': 1, + 'sand_address/': 1, + 'hbar_withdrawal/': 1, + 'hbar_address/': 1, + 'rgt_withdrawal/': 1, + 'rgt_address/': 1, + 'fet_withdrawal/': 1, + 'fet_address/': 1, + 'skl_withdrawal/': 1, + 'skl_address/': 1, + 'cel_withdrawal/': 1, + 'cel_address/': 1, + 'sxp_withdrawal/': 1, + 'sxp_address/': 1, + 'ada_withdrawal/': 1, + 'ada_address/': 1, + 'slp_withdrawal/': 1, + 'slp_address/': 1, + 'ftm_withdrawal/': 1, + 'ftm_address/': 1, + 'perp_withdrawal/': 1, + 'perp_address/': 1, + 'dydx_withdrawal/': 1, + 'dydx_address/': 1, + 'gala_withdrawal/': 1, + 'gala_address/': 1, + 'shib_withdrawal/': 1, + 'shib_address/': 1, + 'amp_withdrawal/': 1, + 'amp_address/': 1, + 'sgb_withdrawal/': 1, + 'sgb_address/': 1, + 'avax_withdrawal/': 1, + 'avax_address/': 1, + 'wbtc_withdrawal/': 1, + 'wbtc_address/': 1, + 'ctsi_withdrawal/': 1, + 'ctsi_address/': 1, + 'cvx_withdrawal/': 1, + 'cvx_address/': 1, + 'imx_withdrawal/': 1, + 'imx_address/': 1, + 'nexo_withdrawal/': 1, + 'nexo_address/': 1, + 'ust_withdrawal/': 1, + 'ust_address/': 1, + 'ant_withdrawal/': 1, + 'ant_address/': 1, + 'gods_withdrawal/': 1, + 'gods_address/': 1, + 'rad_withdrawal/': 1, + 'rad_address/': 1, + 'band_withdrawal/': 1, + 'band_address/': 1, + 'inj_withdrawal/': 1, + 'inj_address/': 1, + 'rly_withdrawal/': 1, + 'rly_address/': 1, + 'rndr_withdrawal/': 1, + 'rndr_address/': 1, + 'vega_withdrawal/': 1, + 'vega_address/': 1, + '1inch_withdrawal/': 1, + '1inch_address/': 1, + 'ens_withdrawal/': 1, + 'ens_address/': 1, + 'mana_withdrawal/': 1, + 'mana_address/': 1, + 'lrc_withdrawal/': 1, + 'lrc_address/': 1, + 'ape_withdrawal/': 1, + 'ape_address/': 1, + 'mpl_withdrawal/': 1, + 'mpl_address/': 1, + 'euroc_withdrawal/': 1, + 'euroc_address/': 1, + 'sol_withdrawal/': 1, + 'sol_address/': 1, + 'dot_withdrawal/': 1, + 'dot_address/': 1, + 'near_withdrawal/': 1, + 'near_address/': 1, + 'doge_withdrawal/': 1, + 'doge_address/': 1, + 'flr_withdrawal/': 1, + 'flr_address/': 1, + 'dgld_withdrawal/': 1, + 'dgld_address/': 1, + 'ldo_withdrawal/': 1, + 'ldo_address/': 1, + 'travel_rule/contacts/': 1, + 'earn/subscribe/': 1, + 'earn/subscriptions/setting/': 1, + 'earn/unsubscribe': 1, + 'wecan_withdrawal/': 1, + 'wecan_address/': 1, + 'trac_withdrawal/': 1, + 'trac_address/': 1, + 'eurcv_withdrawal/': 1, + 'eurcv_address/': 1, + 'pyusd_withdrawal/': 1, + 'pyusd_address/': 1, + 'lmwr_withdrawal/': 1, + 'lmwr_address/': 1, + 'pepe_withdrawal/': 1, + 'pepe_address/': 1, + 'blur_withdrawal/': 1, + 'blur_address/': 1, + 'vext_withdrawal/': 1, + 'vext_address/': 1, + 'cspr_withdrawal/': 1, + 'cspr_address/': 1, + 'vchf_withdrawal/': 1, + 'vchf_address/': 1, + 'veur_withdrawal/': 1, + 'veur_address/': 1, + 'truf_withdrawal/': 1, + 'truf_address/': 1, + 'wif_withdrawal/': 1, + 'wif_address/': 1, + 'smt_withdrawal/': 1, + 'smt_address/': 1, + 'sui_withdrawal/': 1, + 'sui_address/': 1, + 'jup_withdrawal/': 1, + 'jup_address/': 1, + 'ondo_withdrawal/': 1, + 'ondo_address/': 1, + 'boba_withdrawal/': 1, + 'boba_address/': 1, + 'pyth_withdrawal/': 1, + 'pyth_address/': 1, + }, + }, + }, + 'fees': { + 'trading': { + 'tierBased': True, + 'percentage': True, + 'taker': self.parse_number('0.004'), + 'maker': self.parse_number('0.004'), + 'tiers': { + 'taker': [ + [self.parse_number('0'), self.parse_number('0.004')], + [self.parse_number('10000'), self.parse_number('0.003')], + [self.parse_number('100000'), self.parse_number('0.002')], + [self.parse_number('500000'), self.parse_number('0.0018')], + [self.parse_number('1500000'), self.parse_number('0.0016')], + [self.parse_number('5000000'), self.parse_number('0.0012')], + [self.parse_number('20000000'), self.parse_number('0.001')], + [self.parse_number('50000000'), self.parse_number('0.0008')], + [self.parse_number('100000000'), self.parse_number('0.0006')], + [self.parse_number('250000000'), self.parse_number('0.0005')], + [self.parse_number('1000000000'), self.parse_number('0.0003')], + ], + 'maker': [ + [self.parse_number('0'), self.parse_number('0.003')], + [self.parse_number('10000'), self.parse_number('0.002')], + [self.parse_number('100000'), self.parse_number('0.001')], + [self.parse_number('500000'), self.parse_number('0.0008')], + [self.parse_number('1500000'), self.parse_number('0.0006')], + [self.parse_number('5000000'), self.parse_number('0.0003')], + [self.parse_number('20000000'), self.parse_number('0.002')], + [self.parse_number('50000000'), self.parse_number('0.0001')], + [self.parse_number('100000000'), self.parse_number('0')], + [self.parse_number('250000000'), self.parse_number('0')], + [self.parse_number('1000000000'), self.parse_number('0')], + ], + }, + }, + 'funding': { + 'tierBased': False, + 'percentage': False, + 'withdraw': {}, + 'deposit': { + 'BTC': 0, + 'BCH': 0, + 'LTC': 0, + 'ETH': 0, + 'XRP': 0, + 'XLM': 0, + 'PAX': 0, + 'USD': 7.5, + 'EUR': 0, + }, + }, + }, + 'precisionMode': TICK_SIZE, + 'commonCurrencies': { + 'UST': 'USTC', + }, + # exchange-specific options + 'options': { + 'networksById': { + 'bitcoin-cash': 'BCH', + 'bitcoin': 'BTC', + 'ethereum': 'ERC20', + 'litecoin': 'LTC', + 'stellar': 'XLM', + 'xrpl': 'XRP', + 'tron': 'TRC20', + 'algorand': 'ALGO', + 'flare': 'FLR', + 'hedera': 'HBAR', + 'cardana': 'ADA', + 'songbird': 'FLR', + 'avalanche-c-chain': 'AVAX', + 'solana': 'SOL', + 'polkadot': 'DOT', + 'near': 'NEAR', + 'doge': 'DOGE', + 'sui': 'SUI', + 'casper': 'CSRP', + }, + }, + 'exceptions': { + 'exact': { + 'No permission found': PermissionDenied, + 'API key not found': AuthenticationError, + 'IP address not allowed': PermissionDenied, + 'Invalid nonce': InvalidNonce, + 'Invalid signature': AuthenticationError, + 'Authentication failed': AuthenticationError, + 'Missing key, signature and nonce parameters': AuthenticationError, + 'Wrong API key format': AuthenticationError, + 'Your account is frozen': PermissionDenied, + 'Please update your profile with your FATCA information, before using API.': PermissionDenied, + 'Order not found.': OrderNotFound, + 'Price is more than 20% below market price.': InvalidOrder, + "Bitstamp.net is under scheduled maintenance. We'll be back soon.": OnMaintenance, # {"error": "Bitstamp.net is under scheduled maintenance. We'll be back soon."} + 'Order could not be placed.': ExchangeNotAvailable, # Order could not be placed(perhaps due to internal error or trade halt). Please retry placing order. + 'Invalid offset.': BadRequest, + 'Trading is currently unavailable for your account.': AccountSuspended, # {"status": "error", "reason": {"__all__": ["Trading is currently unavailable for your account."]}, "response_code": "403.004"} + }, + 'broad': { + 'Minimum order size is': InvalidOrder, # Minimum order size is 5.0 EUR. + 'Check your account balance for details.': InsufficientFunds, # You have only 0.00100000 BTC available. Check your account balance for details. + 'Ensure self value has at least': InvalidAddress, # Ensure self value has at least 25 characters(it has 4). + 'Ensure that there are no more than': InvalidOrder, # {"status": "error", "reason": {"amount": ["Ensure that there are no more than 0 decimal places."], "__all__": [""]}} + }, + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': False, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': True, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': False, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': None, + 'untilDays': 30, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': None, + 'fetchOHLCV': { + 'limit': 1000, + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + }) + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for bitstamp + + https://www.bitstamp.net/api/#tag/Market-info/operation/GetTradingPairsInfo + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = await self.fetch_markets_from_cache(params) + # + # [ + # { + # "trading": "Enabled", + # "base_decimals": 8, + # "url_symbol": "btcusd", + # "name": "BTC/USD", + # "instant_and_market_orders": "Enabled", + # "minimum_order": "20.0 USD", + # "counter_decimals": 2, + # "description": "Bitcoin / U.S. dollar" + # } + # ] + # + result = [] + for i in range(0, len(response)): + market = response[i] + name = self.safe_string(market, 'name') + base, quote = name.split('/') + baseId = base.lower() + quoteId = quote.lower() + base = self.safe_currency_code(base) + quote = self.safe_currency_code(quote) + minimumOrder = self.safe_string(market, 'minimum_order') + parts = minimumOrder.split(' ') + status = self.safe_string(market, 'trading') + result.append({ + 'id': self.safe_string(market, 'url_symbol'), + 'marketId': baseId + '_' + quoteId, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'future': False, + 'swap': False, + 'option': False, + 'active': (status == 'Enabled'), + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(self.parse_precision(self.safe_string(market, 'base_decimals'))), + 'price': self.parse_number(self.parse_precision(self.safe_string(market, 'counter_decimals'))), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': None, + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': self.safe_number(parts, 0), + 'max': None, + }, + }, + 'created': None, + 'info': market, + }) + return result + + def construct_currency_object(self, id, code, name, precision, minCost, originalPayload): + currencyType = 'crypto' + description = self.describe() + if self.is_fiat(code): + currencyType = 'fiat' + tickSize = self.parse_number(self.parse_precision(self.number_to_string(precision))) + return { + 'id': id, + 'code': code, + 'info': originalPayload, # the original payload + 'type': currencyType, + 'name': name, + 'active': True, + 'deposit': None, + 'withdraw': None, + 'fee': self.safe_number(description['fees']['funding']['withdraw'], code), + 'precision': tickSize, + 'limits': { + 'amount': { + 'min': tickSize, + 'max': None, + }, + 'price': { + 'min': tickSize, + 'max': None, + }, + 'cost': { + 'min': minCost, + 'max': None, + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + }, + 'networks': {}, + } + + async def fetch_markets_from_cache(self, params={}): + # self method is now redundant + # currencies are now fetched before markets + options = self.safe_value(self.options, 'fetchMarkets', {}) + timestamp = self.safe_integer(options, 'timestamp') + expires = self.safe_integer(options, 'expires', 1000) + now = self.milliseconds() + if (timestamp is None) or ((now - timestamp) > expires): + response = await self.publicGetTradingPairsInfo(params) + self.options['fetchMarkets'] = self.extend(options, { + 'response': response, + 'timestamp': now, + }) + return self.safe_value(self.options['fetchMarkets'], 'response') + + async def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://www.bitstamp.net/api/#tag/Market-info/operation/GetTradingPairsInfo + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = await self.fetch_markets_from_cache(params) + # + # [ + # { + # "trading": "Enabled", + # "base_decimals": 8, + # "url_symbol": "btcusd", + # "name": "BTC/USD", + # "instant_and_market_orders": "Enabled", + # "minimum_order": "20.0 USD", + # "counter_decimals": 2, + # "description": "Bitcoin / U.S. dollar" + # }, + # ] + # + result: dict = {} + for i in range(0, len(response)): + market = response[i] + name = self.safe_string(market, 'name') + base, quote = name.split('/') + baseId = base.lower() + quoteId = quote.lower() + base = self.safe_currency_code(base) + quote = self.safe_currency_code(quote) + description = self.safe_string(market, 'description') + baseDescription, quoteDescription = description.split(' / ') + minimumOrder = self.safe_string(market, 'minimum_order') + parts = minimumOrder.split(' ') + cost = parts[0] + if not (base in result): + baseDecimals = self.safe_integer(market, 'base_decimals') + result[base] = self.construct_currency_object(baseId, base, baseDescription, baseDecimals, None, market) + if not (quote in result): + counterDecimals = self.safe_integer(market, 'counter_decimals') + result[quote] = self.construct_currency_object(quoteId, quote, quoteDescription, counterDecimals, self.parse_number(cost), market) + return result + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://www.bitstamp.net/api/#tag/Order-book/operation/GetOrderBook + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + response = await self.publicGetOrderBookPair(self.extend(request, params)) + # + # { + # "timestamp": "1583652948", + # "microtimestamp": "1583652948955826", + # "bids": [ + # ["8750.00", "1.33685271"], + # ["8749.39", "0.07700000"], + # ["8746.98", "0.07400000"], + # ] + # "asks": [ + # ["8754.10", "1.51995636"], + # ["8754.71", "1.40000000"], + # ["8754.72", "2.50000000"], + # ] + # } + # + microtimestamp = self.safe_integer(response, 'microtimestamp') + timestamp = self.parse_to_int(microtimestamp / 1000) + orderbook = self.parse_order_book(response, market['symbol'], timestamp) + orderbook['nonce'] = microtimestamp + return orderbook + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "timestamp": "1686068944", + # "high": "26252", + # "last": "26216", + # "bid": "26208", + # "vwap": "25681", + # "volume": "3563.13819902", + # "low": "25350", + # "ask": "26211", + # "open": "25730", + # "open_24": "25895", + # "percent_change_24": "1.24", + # "pair": "BTC/USD" + # } + # + marketId = self.safe_string(ticker, 'pair') + symbol = self.safe_symbol(marketId, market, None) + timestamp = self.safe_timestamp(ticker, 'timestamp') + vwap = self.safe_string(ticker, 'vwap') + baseVolume = self.safe_string(ticker, 'volume') + quoteVolume = Precise.string_mul(baseVolume, vwap) + last = self.safe_string(ticker, 'last') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': self.safe_string(ticker, 'bid'), + 'bidVolume': None, + 'ask': self.safe_string(ticker, 'ask'), + 'askVolume': None, + 'vwap': vwap, + 'open': self.safe_string(ticker, 'open'), + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'info': ticker, + }, market) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://www.bitstamp.net/api/#tag/Tickers/operation/GetMarketTicker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + ticker = await self.publicGetTickerPair(self.extend(request, params)) + # + # { + # "timestamp": "1686068944", + # "high": "26252", + # "last": "26216", + # "bid": "26208", + # "vwap": "25681", + # "volume": "3563.13819902", + # "low": "25350", + # "ask": "26211", + # "open": "25730", + # "open_24": "25895", + # "percent_change_24": "1.24" + # } + # + return self.parse_ticker(ticker, market) + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://www.bitstamp.net/api/#tag/Tickers/operation/GetCurrencyPairTickers + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + response = await self.publicGetTicker(params) + # + # { + # "timestamp": "1686068944", + # "high": "26252", + # "last": "26216", + # "bid": "26208", + # "vwap": "25681", + # "volume": "3563.13819902", + # "low": "25350", + # "ask": "26211", + # "open": "25730", + # "open_24": "25895", + # "percent_change_24": "1.24", + # "pair": "BTC/USD" + # } + # + return self.parse_tickers(response, symbols) + + def get_currency_id_from_transaction(self, transaction): + # + # { + # "fee": "0.00000000", + # "btc_usd": "0.00", + # "datetime": XXX, + # "usd": 0.0, + # "btc": 0.0, + # "eth": "0.05000000", + # "type": "0", + # "id": XXX, + # "eur": 0.0 + # } + # + currencyId = self.safe_string_lower(transaction, 'currency') + if currencyId is not None: + return currencyId + transaction = self.omit(transaction, [ + 'fee', + 'price', + 'datetime', + 'type', + 'status', + 'id', + ]) + ids = list(transaction.keys()) + for i in range(0, len(ids)): + id = ids[i] + if id.find('_') < 0: + value = self.safe_integer(transaction, id) + if (value is not None) and (value != 0): + return id + return None + + def get_market_from_trade(self, trade): + trade = self.omit(trade, [ + 'fee', + 'price', + 'datetime', + 'tid', + 'type', + 'order_id', + 'side', + ]) + currencyIds = list(trade.keys()) + numCurrencyIds = len(currencyIds) + if numCurrencyIds > 2: + raise ExchangeError(self.id + ' getMarketFromTrade() too many keys: ' + self.json(currencyIds) + ' in the trade: ' + self.json(trade)) + if numCurrencyIds == 2: + marketId = currencyIds[0] + currencyIds[1] + if marketId in self.markets_by_id: + return self.safe_market(marketId) + marketId = currencyIds[1] + currencyIds[0] + if marketId in self.markets_by_id: + return self.safe_market(marketId) + return None + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(public) + # + # { + # "date": "1637845199", + # "tid": "209895701", + # "amount": "0.00500000", + # "type": "0", # Transaction type: 0 - buy; 1 - sell + # "price": "4451.25" + # } + # + # fetchMyTrades, trades returned within fetchOrder(private) + # + # { + # "fee": "0.11128", + # "eth_usdt": 4451.25, + # "datetime": "2021-11-25 12:59:59.322000", + # "usdt": "-22.26", + # "order_id": 1429545880227846, + # "usd": 0, + # "btc": 0, + # "eth": "0.00500000", + # "type": "2", # Transaction type: 0 - deposit; 1 - withdrawal; 2 - market trade; 14 - sub account transfer; 25 - credited with staked assets; 26 - sent assets to staking; 27 - staking reward; 32 - referral reward; 35 - inter account transfer. + # "id": 209895701, + # "eur": 0 + # } + # + # from fetchOrder(private) + # + # { + # "fee": "0.11128", + # "price": "4451.25000000", + # "datetime": "2021-11-25 12:59:59.322000", + # "usdt": "22.25625000", + # "tid": 209895701, + # "eth": "0.00500000", + # "type": 2 # Transaction type: 0 - deposit; 1 - withdrawal; 2 - market trade + # } + # + id = self.safe_string_2(trade, 'id', 'tid') + symbol = None + side = None + priceString = self.safe_string(trade, 'price') + amountString = self.safe_string(trade, 'amount') + orderId = self.safe_string(trade, 'order_id') + type = None + costString = self.safe_string(trade, 'cost') + rawMarketId = None + if market is None: + keys = list(trade.keys()) + for i in range(0, len(keys)): + currentKey = keys[i] + if currentKey != 'order_id' and currentKey.find('_') >= 0: + rawMarketId = currentKey + market = self.safe_market(rawMarketId, market, '_') + # if the market is still not defined + # try to deduce it from used keys + if market is None: + market = self.get_market_from_trade(trade) + feeCostString = self.safe_string(trade, 'fee') + feeCurrency = market['quote'] + priceId = rawMarketId if (rawMarketId is not None) else market['marketId'] + priceString = self.safe_string(trade, priceId, priceString) + amountString = self.safe_string(trade, market['baseId'], amountString) + costString = self.safe_string(trade, market['quoteId'], costString) + symbol = market['symbol'] + datetimeString = self.safe_string_2(trade, 'date', 'datetime') + timestamp = None + if datetimeString is not None: + if datetimeString.find(' ') >= 0: + # iso8601 + timestamp = self.parse8601(datetimeString) + else: + # string unix epoch in seconds + timestamp = int(datetimeString) + timestamp = timestamp * 1000 + # if it is a private trade + if 'id' in trade: + if amountString is not None: + isAmountNeg = Precise.string_lt(amountString, '0') + if isAmountNeg: + side = 'sell' + amountString = Precise.string_neg(amountString) + else: + side = 'buy' + else: + side = self.safe_string(trade, 'type') + if side == '1': + side = 'sell' + elif side == '0': + side = 'buy' + else: + side = None + if costString is not None: + costString = Precise.string_abs(costString) + fee = None + if feeCostString is not None: + fee = { + 'cost': feeCostString, + 'currency': feeCurrency, + } + return self.safe_trade({ + 'id': id, + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'order': orderId, + 'type': type, + 'side': side, + 'takerOrMaker': None, + 'price': priceString, + 'amount': amountString, + 'cost': costString, + 'fee': fee, + }, market) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://www.bitstamp.net/api/#tag/Transactions-public/operation/GetTransactions + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + 'time': 'hour', + } + response = await self.publicGetTransactionsPair(self.extend(request, params)) + # + # [ + # { + # "date": "1551814435", + # "tid": "83581898", + # "price": "0.03532850", + # "type": "1", + # "amount": "0.85945907" + # }, + # { + # "date": "1551814434", + # "tid": "83581896", + # "price": "0.03532851", + # "type": "1", + # "amount": "11.34130961" + # }, + # ] + # + return self.parse_trades(response, market, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # { + # "high": "9064.77", + # "timestamp": "1593961440", + # "volume": "18.49436608", + # "low": "9040.87", + # "close": "9064.77", + # "open": "9040.87" + # } + # + return [ + self.safe_timestamp(ohlcv, 'timestamp'), + self.safe_number(ohlcv, 'open'), + self.safe_number(ohlcv, 'high'), + self.safe_number(ohlcv, 'low'), + self.safe_number(ohlcv, 'close'), + self.safe_number(ohlcv, 'volume'), + ] + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://www.bitstamp.net/api/#tag/Market-info/operation/GetOHLCData + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + 'step': self.safe_string(self.timeframes, timeframe, timeframe), + } + duration = self.parse_timeframe(timeframe) + if limit is None: + if since is None: + request['limit'] = 1000 # we need to specify an allowed amount of `limit` if no `since` is set and there is no default limit by exchange + else: + limit = 1000 + start = self.parse_to_int(since / 1000) + request['start'] = start + request['end'] = self.sum(start, duration * (limit - 1)) + request['limit'] = limit + else: + if since is not None: + start = self.parse_to_int(since / 1000) + request['start'] = start + request['end'] = self.sum(start, duration * (limit - 1)) + request['limit'] = min(limit, 1000) # min 1, max 1000 + response = await self.publicGetOhlcPair(self.extend(request, params)) + # + # { + # "data": { + # "pair": "BTC/USD", + # "ohlc": [ + # {"high": "9064.77", "timestamp": "1593961440", "volume": "18.49436608", "low": "9040.87", "close": "9064.77", "open": "9040.87"}, + # {"high": "9071.59", "timestamp": "1593961500", "volume": "3.48631711", "low": "9058.76", "close": "9061.07", "open": "9064.66"}, + # {"high": "9067.33", "timestamp": "1593961560", "volume": "0.04142833", "low": "9061.94", "close": "9061.94", "open": "9067.33"}, + # ], + # } + # } + # + data = self.safe_value(response, 'data', {}) + ohlc = self.safe_list(data, 'ohlc', []) + return self.parse_ohlcvs(ohlc, market, timeframe, since, limit) + + def parse_balance(self, response) -> Balances: + result: dict = { + 'info': response, + 'timestamp': None, + 'datetime': None, + } + if response is None: + response = [] + for i in range(0, len(response)): + currencyBalance = response[i] + currencyId = self.safe_string(currencyBalance, 'currency') + currencyCode = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(currencyBalance, 'available') + account['used'] = self.safe_string(currencyBalance, 'reserved') + account['total'] = self.safe_string(currencyBalance, 'total') + result[currencyCode] = account + return self.safe_balance(result) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://www.bitstamp.net/api/#tag/Account-balances/operation/GetAccountBalances + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + response = await self.privatePostAccountBalances(params) + # + # [ + # { + # "currency": "usdt", + # "total": "7.00000", + # "available": "7.00000", + # "reserved": "0.00000" + # }, + # ... + # ] + # + return self.parse_balance(response) + + async def fetch_trading_fee(self, symbol: str, params={}) -> TradingFeeInterface: + """ + fetch the trading fees for a market + + https://www.bitstamp.net/api/#tag/Fees/operation/GetTradingFeesForCurrency + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `fee structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'market_symbol': market['id'], + } + response = await self.privatePostFeesTrading(self.extend(request, params)) + # + # [ + # { + # "currency_pair": "btcusd", + # "fees": + # { + # "maker": "0.15000", + # "taker": "0.16000" + # }, + # "market": "btcusd" + # } + # ... + # ] + # + tradingFeesByMarketId = self.index_by(response, 'currency_pair') + tradingFee = self.safe_dict(tradingFeesByMarketId, market['id']) + return self.parse_trading_fee(tradingFee, market) + + def parse_trading_fee(self, fee: dict, market: Market = None) -> TradingFeeInterface: + marketId = self.safe_string(fee, 'market') + fees = self.safe_dict(fee, 'fees', {}) + return { + 'info': fee, + 'symbol': self.safe_symbol(marketId, market), + 'maker': self.safe_number(fees, 'maker'), + 'taker': self.safe_number(fees, 'taker'), + 'percentage': None, + 'tierBased': None, + } + + def parse_trading_fees(self, fees): + result: dict = {'info': fees} + for i in range(0, len(fees)): + fee = self.parse_trading_fee(fees[i]) + symbol = fee['symbol'] + result[symbol] = fee + return result + + async def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://www.bitstamp.net/api/#tag/Fees/operation/GetAllTradingFees + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + await self.load_markets() + response = await self.privatePostFeesTrading(params) + # + # [ + # { + # "currency_pair": "btcusd", + # "fees": + # { + # "maker": "0.15000", + # "taker": "0.16000" + # }, + # "market": "btcusd" + # } + # ... + # ] + # + return self.parse_trading_fees(response) + + async def fetch_transaction_fees(self, codes: Strings = None, params={}): + """ + @deprecated + please use fetchDepositWithdrawFees instead + + https://www.bitstamp.net/api/#tag/Fees + + :param str[]|None codes: list of unified currency codes + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `fee structures ` + """ + await self.load_markets() + response = await self.privatePostFeesWithdrawal(params) + # + # [ + # { + # "currency": "btc", + # "fee": "0.00015000", + # "network": "bitcoin" + # } + # ... + # ] + # + return self.parse_transaction_fees(response) + + def parse_transaction_fees(self, response, codes=None): + result: dict = {} + currencies = self.index_by(response, 'currency') + ids = list(currencies.keys()) + for i in range(0, len(ids)): + id = ids[i] + fees = self.safe_value(response, i, {}) + code = self.safe_currency_code(id) + if (codes is not None) and not self.in_array(code, codes): + continue + result[code] = { + 'withdraw_fee': self.safe_number(fees, 'fee'), + 'deposit': {}, + 'info': self.safe_dict(currencies, id), + } + return result + + async def fetch_deposit_withdraw_fees(self, codes=None, params={}): + """ + fetch deposit and withdraw fees + + https://www.bitstamp.net/api/#tag/Fees/operation/GetAllWithdrawalFees + + :param str[]|None codes: list of unified currency codes + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `fee structures ` + """ + await self.load_markets() + response = await self.privatePostFeesWithdrawal(params) + # + # [ + # { + # "currency": "btc", + # "fee": "0.00015000", + # "network": "bitcoin" + # } + # ... + # ] + # + responseByCurrencyId = self.group_by(response, 'currency') + return self.parse_deposit_withdraw_fees(responseByCurrencyId, codes) + + def parse_deposit_withdraw_fee(self, fee, currency=None): + result = self.deposit_withdraw_fee(fee) + for j in range(0, len(fee)): + networkEntry = fee[j] + networkId = self.safe_string(networkEntry, 'network') + networkCode = self.network_id_to_code(networkId) + withdrawFee = self.safe_number(networkEntry, 'fee') + result['withdraw'] = { + 'fee': withdrawFee, + 'percentage': None, + } + result['networks'][networkCode] = { + 'withdraw': { + 'fee': withdrawFee, + 'percentage': None, + }, + 'deposit': { + 'fee': None, + 'percentage': None, + }, + } + return result + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://www.bitstamp.net/api/#tag/Orders/operation/OpenInstantBuyOrder + https://www.bitstamp.net/api/#tag/Orders/operation/OpenMarketBuyOrder + https://www.bitstamp.net/api/#tag/Orders/operation/OpenLimitBuyOrder + https://www.bitstamp.net/api/#tag/Orders/operation/OpenInstantSellOrder + https://www.bitstamp.net/api/#tag/Orders/operation/OpenMarketSellOrder + https://www.bitstamp.net/api/#tag/Orders/operation/OpenLimitSellOrder + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + 'amount': self.amount_to_precision(symbol, amount), + } + clientOrderId = self.safe_string_2(params, 'client_order_id', 'clientOrderId') + if clientOrderId is not None: + request['client_order_id'] = clientOrderId + params = self.omit(params, ['clientOrderId']) + response = None + capitalizedSide = self.capitalize(side) + if type == 'market': + if capitalizedSide == 'Buy': + response = await self.privatePostBuyMarketPair(self.extend(request, params)) + else: + response = await self.privatePostSellMarketPair(self.extend(request, params)) + elif type == 'instant': + if capitalizedSide == 'Buy': + response = await self.privatePostBuyInstantPair(self.extend(request, params)) + else: + response = await self.privatePostSellInstantPair(self.extend(request, params)) + else: + request['price'] = self.price_to_precision(symbol, price) + if capitalizedSide == 'Buy': + response = await self.privatePostBuyPair(self.extend(request, params)) + else: + response = await self.privatePostSellPair(self.extend(request, params)) + order = self.parse_order(response, market) + order['type'] = type + return order + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://www.bitstamp.net/api/#tag/Orders/operation/CancelOrder + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + request: dict = { + 'id': id, + } + response = await self.privatePostCancelOrder(self.extend(request, params)) + # + # { + # "id": 1453282316578816, + # "amount": "0.02035278", + # "price": "2100.45", + # "type": 0, + # "market": "BTC/USD" + # } + # + return self.parse_order(response) + + async def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders + + https://www.bitstamp.net/api/#tag/Orders/operation/CancelAllOrders + https://www.bitstamp.net/api/#tag/Orders/operation/CancelOrdersForMarket + + :param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + market = None + request: dict = {} + response = None + if symbol is not None: + market = self.market(symbol) + request['pair'] = market['id'] + response = await self.privatePostCancelAllOrdersPair(self.extend(request, params)) + else: + response = await self.privatePostCancelAllOrders(self.extend(request, params)) + # + # { + # "canceled": [ + # { + # "id": 1453282316578816, + # "amount": "0.02035278", + # "price": "2100.45", + # "type": 0, + # "currency_pair": "BTC/USD", + # "market": "BTC/USD" + # } + # ], + # "success": True + # } + # + canceled = self.safe_list(response, 'canceled') + return self.parse_orders(canceled) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'In Queue': 'open', + 'Open': 'open', + 'Finished': 'closed', + 'Canceled': 'canceled', + } + return self.safe_string(statuses, status, status) + + async def fetch_order_status(self, id: str, symbol: Str = None, params={}): + await self.load_markets() + clientOrderId = self.safe_value_2(params, 'client_order_id', 'clientOrderId') + request: dict = {} + if clientOrderId is not None: + request['client_order_id'] = clientOrderId + params = self.omit(params, ['client_order_id', 'clientOrderId']) + else: + request['id'] = id + response = await self.privatePostOrderStatus(self.extend(request, params)) + return self.parse_order_status(self.safe_string(response, 'status')) + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://www.bitstamp.net/api/#tag/Orders/operation/GetOrderStatus + + :param str id: the order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + clientOrderId = self.safe_value_2(params, 'client_order_id', 'clientOrderId') + request: dict = {} + if clientOrderId is not None: + request['client_order_id'] = clientOrderId + params = self.omit(params, ['client_order_id', 'clientOrderId']) + else: + request['id'] = id + response = await self.privatePostOrderStatus(self.extend(request, params)) + # + # { + # "status": "Finished", + # "id": 1429545880227846, + # "amount_remaining": "0.00000000", + # "transactions": [ + # { + # "fee": "0.11128", + # "price": "4451.25000000", + # "datetime": "2021-11-25 12:59:59.322000", + # "usdt": "22.25625000", + # "tid": 209895701, + # "eth": "0.00500000", + # "type": 2 + # } + # ] + # } + # + return self.parse_order(response, market) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://www.bitstamp.net/api/#tag/Transactions-private/operation/GetUserTransactions + https://www.bitstamp.net/api/#tag/Transactions-private/operation/GetUserTransactionsForMarket + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + request: dict = {} + method = 'privatePostUserTransactions' + market = None + if symbol is not None: + market = self.market(symbol) + request['pair'] = market['id'] + method += 'Pair' + if limit is not None: + request['limit'] = limit + response = await getattr(self, method)(self.extend(request, params)) + result = self.filter_by(response, 'type', '2') + return self.parse_trades(result, market, since, limit) + + async def fetch_deposits_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch history of deposits and withdrawals + + https://www.bitstamp.net/api/#tag/Transactions-private/operation/GetUserTransactions + + :param str [code]: unified currency code for the currency of the deposit/withdrawals, default is None + :param int [since]: timestamp in ms of the earliest deposit/withdrawal, default is None + :param int [limit]: max number of deposit/withdrawals to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `transaction structure ` + """ + await self.load_markets() + request: dict = {} + if limit is not None: + request['limit'] = limit + response = await self.privatePostUserTransactions(self.extend(request, params)) + # + # [ + # { + # "fee": "0.00000000", + # "btc_usd": "0.00", + # "id": 1234567894, + # "usd": 0, + # "btc": 0, + # "datetime": "2018-09-08 09:00:31", + # "type": "1", + # "xrp": "-20.00000000", + # "eur": 0, + # }, + # { + # "fee": "0.00000000", + # "btc_usd": "0.00", + # "id": 1134567891, + # "usd": 0, + # "btc": 0, + # "datetime": "2018-09-07 18:47:52", + # "type": "0", + # "xrp": "20.00000000", + # "eur": 0, + # }, + # ] + # + currency = None + if code is not None: + currency = self.currency(code) + transactions = self.filter_by_array(response, 'type', ['0', '1'], False) + return self.parse_transactions(transactions, currency, since, limit) + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://www.bitstamp.net/api/#tag/Withdrawals/operation/GetWithdrawalRequests + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + request: dict = {} + if since is not None: + request['timedelta'] = self.milliseconds() - since + else: + request['timedelta'] = 50000000 # use max bitstamp approved value + response = await self.privatePostWithdrawalRequests(self.extend(request, params)) + # + # [ + # { + # "status": 2, + # "datetime": "2018-10-17 10:58:13", + # "currency": "BTC", + # "amount": "0.29669259", + # "address": "aaaaa", + # "type": 1, + # "id": 111111, + # "transaction_id": "xxxx", + # }, + # { + # "status": 2, + # "datetime": "2018-10-17 10:55:17", + # "currency": "ETH", + # "amount": "1.11010664", + # "address": "aaaa", + # "type": 16, + # "id": 222222, + # "transaction_id": "xxxxx", + # }, + # ] + # + return self.parse_transactions(response, None, since, limit) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fetchDepositsWithdrawals + # + # { + # "fee": "0.00000000", + # "btc_usd": "0.00", + # "id": 1234567894, + # "usd": 0, + # "btc": 0, + # "datetime": "2018-09-08 09:00:31", + # "type": "1", + # "xrp": "-20.00000000", + # "eur": 0, + # } + # + # fetchWithdrawals + # + # { + # "status": 2, + # "datetime": "2018-10-17 10:58:13", + # "currency": "BTC", + # "amount": "0.29669259", + # "address": "aaaaa", + # "type": 1, + # "id": 111111, + # "transaction_id": "xxxx", + # } + # + # { + # "id": 3386432, + # "type": 14, + # "amount": "863.21332500", + # "status": 2, + # "address": "rE1sdh25BJQ3qFwngiTBwaq3zPGGYcrjp1?dt=1455", + # "currency": "XRP", + # "datetime": "2018-01-05 15:27:55", + # "transaction_id": "001743B03B0C79BA166A064AC0142917B050347B4CB23BA2AB4B91B3C5608F4C" + # } + # + timestamp = self.parse8601(self.safe_string(transaction, 'datetime')) + currencyId = self.get_currency_id_from_transaction(transaction) + code = self.safe_currency_code(currencyId, currency) + feeCost = self.safe_string(transaction, 'fee') + feeCurrency = None + amount = None + if 'amount' in transaction: + amount = self.safe_string(transaction, 'amount') + elif currency is not None: + amount = self.safe_string(transaction, currency['id'], amount) + feeCurrency = currency['code'] + elif (code is not None) and (currencyId is not None): + amount = self.safe_string(transaction, currencyId, amount) + feeCurrency = code + if amount is not None: + # withdrawals have a negative amount + amount = Precise.string_abs(amount) + status = 'ok' + if 'status' in transaction: + status = self.parse_transaction_status(self.safe_string(transaction, 'status')) + type = None + if 'type' in transaction: + # from fetchDepositsWithdrawals + rawType = self.safe_string(transaction, 'type') + if rawType == '0': + type = 'deposit' + elif rawType == '1': + type = 'withdrawal' + else: + # from fetchWithdrawals + type = 'withdrawal' + tag = None + address = self.safe_string(transaction, 'address') + if address is not None: + # dt(destination tag) is embedded into the address field + addressParts = address.split('?dt=') + numParts = len(addressParts) + if numParts > 1: + address = addressParts[0] + tag = addressParts[1] + fee = { + 'currency': None, + 'cost': None, + 'rate': None, + } + if feeCost is not None: + fee = { + 'currency': feeCurrency, + 'cost': feeCost, + 'rate': None, + } + return { + 'info': transaction, + 'id': self.safe_string(transaction, 'id'), + 'txid': self.safe_string(transaction, 'transaction_id'), + 'type': type, + 'currency': code, + 'network': None, + 'amount': self.parse_number(amount), + 'status': status, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'address': address, + 'addressFrom': None, + 'addressTo': address, + 'tag': tag, + 'tagFrom': None, + 'tagTo': tag, + 'updated': None, + 'comment': None, + 'internal': None, + 'fee': fee, + } + + def parse_transaction_status(self, status: Str): + # + # withdrawals: + # 0(open), 1(in process), 2(finished), 3(canceled) or 4(failed). + # + statuses: dict = { + '0': 'pending', # Open + '1': 'pending', # In process + '2': 'ok', # Finished + '3': 'canceled', # Canceled + '4': 'failed', # Failed + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # from fetch order: + # {status: "Finished", + # "id": 731693945, + # "client_order_id": '', + # "transactions": + # [{fee: "0.000019", + # "price": "0.00015803", + # "datetime": "2018-01-07 10:45:34.132551", + # "btc": "0.0079015000000000", + # "tid": 42777395, + # "type": 2, + # "xrp": "50.00000000"}]} + # + # partially filled order: + # {"id": 468646390, + # "client_order_id": "", + # "status": "Canceled", + # "transactions": [{ + # "eth": "0.23000000", + # "fee": "0.09", + # "tid": 25810126, + # "usd": "69.8947000000000000", + # "type": 2, + # "price": "303.89000000", + # "datetime": "2017-11-11 07:22:20.710567" + # }]} + # + # from create order response: + # { + # "price": "0.00008012", + # "client_order_id": '', + # "currency_pair": "XRP/BTC", + # "datetime": "2019-01-31 21:23:36", + # "amount": "15.00000000", + # "type": "0", + # "id": "2814205012" + # } + # + # cancelOrder + # + # { + # "id": 1453282316578816, + # "amount": "0.02035278", + # "price": "2100.45", + # "type": 0, + # "market": "BTC/USD" + # } + # + id = self.safe_string(order, 'id') + clientOrderId = self.safe_string(order, 'client_order_id') + side = self.safe_string(order, 'type') + if side is not None: + side = 'sell' if (side == '1') else 'buy' + # there is no timestamp from fetchOrder + timestamp = self.parse8601(self.safe_string(order, 'datetime')) + marketId = self.safe_string_lower(order, 'currency_pair') + symbol = self.safe_symbol(marketId, market, '/') + status = self.parse_order_status(self.safe_string(order, 'status')) + amount = self.safe_string(order, 'amount') + transactions = self.safe_value(order, 'transactions', []) + price = self.safe_string(order, 'price') + return self.safe_order({ + 'id': id, + 'clientOrderId': clientOrderId, + 'datetime': self.iso8601(timestamp), + 'timestamp': timestamp, + 'lastTradeTimestamp': None, + 'status': status, + 'symbol': symbol, + 'type': None, + 'timeInForce': None, + 'postOnly': None, + 'side': side, + 'price': price, + 'triggerPrice': None, + 'cost': None, + 'amount': amount, + 'filled': None, + 'remaining': None, + 'trades': transactions, + 'fee': None, + 'info': order, + 'average': None, + }, market) + + def parse_ledger_entry_type(self, type): + types: dict = { + '0': 'transaction', + '1': 'transaction', + '2': 'trade', + '14': 'transfer', + } + return self.safe_string(types, type, type) + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + # + # [ + # { + # "fee": "0.00000000", + # "btc_usd": "0.00", + # "id": 1234567894, + # "usd": 0, + # "btc": 0, + # "datetime": "2018-09-08 09:00:31", + # "type": "1", + # "xrp": "-20.00000000", + # "eur": 0, + # }, + # { + # "fee": "0.00000000", + # "btc_usd": "0.00", + # "id": 1134567891, + # "usd": 0, + # "btc": 0, + # "datetime": "2018-09-07 18:47:52", + # "type": "0", + # "xrp": "20.00000000", + # "eur": 0, + # }, + # ] + # + type = self.parse_ledger_entry_type(self.safe_string(item, 'type')) + if type == 'trade': + parsedTrade = self.parse_trade(item) + market = None + keys = list(item.keys()) + for i in range(0, len(keys)): + if keys[i].find('_') >= 0: + marketId = keys[i].replace('_', '') + market = self.safe_market(marketId, market) + # if the market is still not defined + # try to deduce it from used keys + if market is None: + market = self.get_market_from_trade(item) + direction = 'in' if (parsedTrade['side'] == 'buy') else 'out' + return self.safe_ledger_entry({ + 'info': item, + 'id': parsedTrade['id'], + 'timestamp': parsedTrade['timestamp'], + 'datetime': parsedTrade['datetime'], + 'direction': direction, + 'account': None, + 'referenceId': parsedTrade['order'], + 'referenceAccount': None, + 'type': type, + 'currency': market['base'], + 'amount': parsedTrade['amount'], + 'before': None, + 'after': None, + 'status': 'ok', + 'fee': parsedTrade['fee'], + }, currency) + else: + parsedTransaction = self.parse_transaction(item, currency) + direction = None + if 'amount' in item: + amount = self.safe_string(item, 'amount') + direction = 'in' if Precise.string_gt(amount, '0') else 'out' + elif ('currency' in parsedTransaction) and parsedTransaction['currency'] is not None: + currencyCode = self.safe_string(parsedTransaction, 'currency') + currency = self.currency(currencyCode) + amount = self.safe_string(item, currency['id']) + direction = 'in' if Precise.string_gt(amount, '0') else 'out' + return self.safe_ledger_entry({ + 'info': item, + 'id': parsedTransaction['id'], + 'timestamp': parsedTransaction['timestamp'], + 'datetime': parsedTransaction['datetime'], + 'direction': direction, + 'account': None, + 'referenceId': parsedTransaction['txid'], + 'referenceAccount': None, + 'type': type, + 'currency': parsedTransaction['currency'], + 'amount': parsedTransaction['amount'], + 'before': None, + 'after': None, + 'status': parsedTransaction['status'], + 'fee': parsedTransaction['fee'], + }, currency) + + async def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + + https://www.bitstamp.net/api/#tag/Transactions-private/operation/GetUserTransactions + + :param str [code]: unified currency code, default is None + :param int [since]: timestamp in ms of the earliest ledger entry, default is None + :param int [limit]: max number of ledger entries to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ledger structure ` + """ + await self.load_markets() + request: dict = {} + if limit is not None: + request['limit'] = limit + response = await self.privatePostUserTransactions(self.extend(request, params)) + currency = None + if code is not None: + currency = self.currency(code) + return self.parse_ledger(response, currency, since, limit) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://www.bitstamp.net/api/#tag/Orders/operation/GetAllOpenOrders + https://www.bitstamp.net/api/#tag/Orders/operation/GetOpenOrdersForMarket + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + market = None + await self.load_markets() + if symbol is not None: + market = self.market(symbol) + response = await self.privatePostOpenOrdersAll(params) + # + # [ + # { + # "price": "0.00008012", + # "currency_pair": "XRP/BTC", + # "client_order_id": '', + # "datetime": "2019-01-31 21:23:36", + # "amount": "15.00000000", + # "type": "0", + # "id": "2814205012", + # } + # ] + # + return self.parse_orders(response, market, since, limit, { + 'status': 'open', + 'type': 'limit', + }) + + def get_currency_name(self, code): + """ + @ignore + :param str code: Unified currency code + :returns str: lowercase version of code + """ + return code.lower() + + def is_fiat(self, code): + return code == 'USD' or code == 'EUR' or code == 'GBP' + + async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://www.bitstamp.net/api/#tag/Deposits/operation/GetCryptoDepositAddress + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + if self.is_fiat(code): + raise NotSupported(self.id + ' fiat fetchDepositAddress() for ' + code + ' is not supported!') + name = self.get_currency_name(code) + method = 'privatePost' + self.capitalize(name) + 'Address' + response = await getattr(self, method)(params) + address = self.safe_string(response, 'address') + tag = self.safe_string_2(response, 'memo_id', 'destination_tag') + self.check_address(address) + return { + 'info': response, + 'currency': code, + 'network': None, + 'address': address, + 'tag': tag, + } + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://www.bitstamp.net/api/#tag/Withdrawals/operation/RequestFiatWithdrawal + https://www.bitstamp.net/api/#tag/Withdrawals/operation/RequestCryptoWithdrawal + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + # For fiat withdrawals please provide all required additional parameters in the 'params' + # Check https://www.bitstamp.net/api/ under 'Open bank withdrawal' for list and description. + tag, params = self.handle_withdraw_tag_and_params(tag, params) + await self.load_markets() + self.check_address(address) + request: dict = { + 'amount': amount, + } + currency = None + method = None + if not self.is_fiat(code): + name = self.get_currency_name(code) + method = 'privatePost' + self.capitalize(name) + 'Withdrawal' + if code == 'XRP': + if tag is not None: + request['destination_tag'] = tag + elif code == 'XLM' or code == 'HBAR': + if tag is not None: + request['memo_id'] = tag + request['address'] = address + else: + method = 'privatePostWithdrawalOpen' + currency = self.currency(code) + request['iban'] = address + request['account_currency'] = currency['id'] + response = await getattr(self, method)(self.extend(request, params)) + return self.parse_transaction(response, currency) + + async def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://www.bitstamp.net/api/#tag/Sub-account/operation/TransferFromMainToSub + https://www.bitstamp.net/api/#tag/Sub-account/operation/TransferFromSubToMain + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account to transfer from + :param str toAccount: account to transfer to + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transfer structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'amount': self.parse_to_numeric(self.currency_to_precision(code, amount)), + 'currency': currency['id'].upper(), + } + response = None + if fromAccount == 'main': + request['subAccount'] = toAccount + response = await self.privatePostTransferFromMain(self.extend(request, params)) + elif toAccount == 'main': + request['subAccount'] = fromAccount + response = await self.privatePostTransferToMain(self.extend(request, params)) + else: + raise BadRequest(self.id + ' transfer() only supports from or to main') + # + # {status: 'ok'} + # + transfer = self.parse_transfer(response, currency) + transfer['amount'] = amount + transfer['fromAccount'] = fromAccount + transfer['toAccount'] = toAccount + return transfer + + def parse_transfer(self, transfer, currency=None): + # + # {status: 'ok'} + # + status = self.safe_string(transfer, 'status') + return { + 'info': transfer, + 'id': None, + 'timestamp': None, + 'datetime': None, + 'currency': currency['code'], + 'amount': None, + 'fromAccount': None, + 'toAccount': None, + 'status': self.parse_transfer_status(status), + } + + def parse_transfer_status(self, status: Str) -> Str: + statuses: dict = { + 'ok': 'ok', + 'error': 'failed', + } + return self.safe_string(statuses, status, status) + + def nonce(self): + return self.milliseconds() + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = self.urls['api'][api] + '/' + url += self.version + '/' + url += self.implode_params(path, params) + query = self.omit(params, self.extract_params(path)) + if api == 'public': + if query: + url += '?' + self.urlencode(query) + else: + self.check_required_credentials() + xAuth = 'BITSTAMP ' + self.apiKey + xAuthNonce = self.uuid() + xAuthTimestamp = str(self.milliseconds()) + xAuthVersion = 'v2' + contentType = '' + headers = { + 'X-Auth': xAuth, + 'X-Auth-Nonce': xAuthNonce, + 'X-Auth-Timestamp': xAuthTimestamp, + 'X-Auth-Version': xAuthVersion, + } + if method == 'POST': + if query: + body = self.urlencode(query) + contentType = 'application/x-www-form-urlencoded' + headers['Content-Type'] = contentType + else: + # sending an empty POST request will trigger + # an API0020 error returned by the exchange + # therefore for empty requests we send a dummy object + # https://github.com/ccxt/ccxt/issues/6846 + body = self.urlencode({'foo': 'bar'}) + contentType = 'application/x-www-form-urlencoded' + headers['Content-Type'] = contentType + authBody = body if body else '' + auth = xAuth + method + url.replace('https://', '') + contentType + xAuthNonce + xAuthTimestamp + xAuthVersion + authBody + signature = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256) + headers['X-Auth-Signature'] = signature + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None + # + # {"error": "No permission found"} # fetchDepositAddress returns self on apiKeys that don't have the permission required + # {"status": "error", "reason": {"__all__": ["Minimum order size is 5.0 EUR."]}} + # reuse of a nonce gives: {status: 'error', reason: 'Invalid nonce', code: 'API0004'} + # + status = self.safe_string(response, 'status') + error = self.safe_value(response, 'error') + if (status == 'error') or (error is not None): + errors = [] + if isinstance(error, str): + errors.append(error) + elif error is not None: + keys = list(error.keys()) + for i in range(0, len(keys)): + key = keys[i] + value = self.safe_value(error, key) + if isinstance(value, list): + errors = self.array_concat(errors, value) + else: + errors.append(value) + reasonInner = self.safe_value(response, 'reason', {}) + if isinstance(reasonInner, str): + errors.append(reasonInner) + else: + all = self.safe_value(reasonInner, '__all__', []) + for i in range(0, len(all)): + errors.append(all[i]) + code = self.safe_string(response, 'code') + if code == 'API0005': + raise AuthenticationError(self.id + ' invalid signature, use the uid for the main account if you have subaccounts') + feedback = self.id + ' ' + body + for i in range(0, len(errors)): + value = errors[i] + self.throw_exactly_matched_exception(self.exceptions['exact'], value, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], value, feedback) + raise ExchangeError(feedback) + return None diff --git a/ccxt/async_support/bitteam.py b/ccxt/async_support/bitteam.py new file mode 100644 index 0000000..c0b1073 --- /dev/null +++ b/ccxt/async_support/bitteam.py @@ -0,0 +1,2381 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.bitteam import ImplicitAPI +from ccxt.base.types import Any, Balances, Currencies, Currency, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class bitteam(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(bitteam, self).describe(), { + 'id': 'bitteam', + 'name': 'BIT.TEAM', + 'countries': ['UK'], + 'version': 'v2.0.6', + 'rateLimit': 1, # the exchange has no rate limit + 'certified': False, + 'pro': False, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'cancelOrders': False, + 'closeAllPositions': False, + 'closePosition': False, + 'createDepositAddress': False, + 'createOrder': True, + 'createOrderWithTakeProfitAndStopLoss': False, + 'createOrderWithTakeProfitAndStopLossWs': False, + 'createPostOnlyOrder': False, + 'createReduceOnlyOrder': False, + 'createStopLimitOrder': False, + 'createStopMarketOrder': False, + 'createStopOrder': False, + 'deposit': False, + 'editOrder': False, + 'fetchAccounts': False, + 'fetchBalance': True, + 'fetchBidsAsks': False, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchCanceledOrders': True, + 'fetchClosedOrder': False, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDeposit': False, + 'fetchDepositAddress': False, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': False, + 'fetchDepositsWithdrawals': True, + 'fetchDepositWithdrawFee': False, + 'fetchDepositWithdrawFees': False, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchL3OrderBook': False, + 'fetchLedger': False, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrder': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrderBooks': False, + 'fetchOrders': True, + 'fetchOrderTrades': False, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchStatus': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': False, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': False, + 'fetchTradingLimits': False, + 'fetchTransactionFee': False, + 'fetchTransactionFees': False, + 'fetchTransactions': True, + 'fetchTransfers': False, + 'fetchVolatilityHistory': False, + 'fetchWithdrawal': False, + 'fetchWithdrawals': False, + 'fetchWithdrawalWhitelist': False, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'repayMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'signIn': False, + 'transfer': False, + 'withdraw': False, + 'ws': False, + }, + 'timeframes': { + '1m': '1', + '5m': '5', + '15m': '15', + '1h': '60', + '1d': '1D', + }, + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/b41b5e0d-98e5-4bd3-8a6e-aeb230a4a135', + 'api': { + 'history': 'https://history.bit.team', + 'public': 'https://bit.team', + 'private': 'https://bit.team', + }, + 'www': 'https://bit.team/', + 'referral': 'https://bit.team/auth/sign-up?ref=bitboy2023', + 'doc': [ + 'https://bit.team/trade/api/documentation', + ], + }, + 'api': { + 'history': { + 'get': { + 'api/tw/history/{pairName}/{resolution}': 1, + }, + }, + 'public': { + 'get': { + 'trade/api/asset': 1, # not unified + 'trade/api/currencies': 1, + 'trade/api/orderbooks/{symbol}': 1, # not unified + 'trade/api/orders': 1, # not unified + 'trade/api/pair/{name}': 1, + 'trade/api/pairs': 1, # not unified + 'trade/api/pairs/precisions': 1, # not unified + 'trade/api/rates': 1, # not unified + 'trade/api/trade/{id}': 1, # not unified + 'trade/api/trades': 1, # not unified + 'trade/api/ccxt/pairs': 1, + 'trade/api/cmc/assets': 1, + 'trade/api/cmc/orderbook/{pair}': 1, + 'trade/api/cmc/summary': 1, + 'trade/api/cmc/ticker': 1, # not unified + 'trade/api/cmc/trades/{pair}': 1, + }, + }, + 'private': { + 'get': { + 'trade/api/ccxt/balance': 1, + 'trade/api/ccxt/order/{id}': 1, + 'trade/api/ccxt/ordersOfUser': 1, + 'trade/api/ccxt/tradesOfUser': 1, + 'trade/api/transactionsOfUser': 1, + }, + 'post': { + 'trade/api/ccxt/cancel-all-order': 1, + 'trade/api/ccxt/cancelorder': 1, + 'trade/api/ccxt/ordercreate': 1, + }, + }, + }, + 'fees': { + 'trading': { + 'feeSide': 'get', + 'tierBased': False, + 'percentage': True, + 'taker': self.parse_number('0.002'), + 'maker': self.parse_number('0.002'), + }, + }, + 'precisionMode': TICK_SIZE, + # exchange-specific options + 'options': { + 'networksById': { + 'Ethereum': 'ERC20', + 'ethereum': 'ERC20', + 'Tron': 'TRC20', + 'tron': 'TRC20', + 'Binance': 'BSC', + 'binance': 'BSC', + 'Binance Smart Chain': 'BSC', + 'bscscan': 'BSC', + 'Bitcoin': 'BTC', + 'bitcoin': 'BTC', + 'Litecoin': 'LTC', + 'litecoin': 'LTC', + 'Polygon': 'POLYGON', + 'polygon': 'POLYGON', + 'PRIZM': 'PRIZM', + 'Decimal': 'Decimal', + 'ufobject': 'ufobject', + 'tonchain': 'tonchain', + }, + 'currenciesValuedInUsd': { + 'USDT': True, + 'BUSD': True, + }, + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': False, + 'triggerPriceType': None, + 'triggerDirection': None, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': False, + 'FOK': False, + 'PO': False, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyRequiresPrice': False, + 'marketBuyByCost': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 100000, + 'untilDays': 100000, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 100, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': True, + 'limit': 100, + 'daysBack': None, + 'untilDays': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': None, + 'daysBackCanceled': None, + 'untilDays': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 1000, + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'exceptions': { + 'exact': { + '400002': BadSymbol, # {"ok":false,"code":400002,"message":"An order cannot be created on a deactivated pair"} + '401000': AuthenticationError, # {"ok":false,"code":401000,"data": {},"message": "Missing authentication"} + '403002': BadRequest, # {"ok":false,"code":403002,"data":{},"message":"Order cannot be deleted, status does not match"} + '404200': BadSymbol, # {"ok":false,"code":404200,"data":{},"message":"Pair was not found"} + }, + 'broad': { + 'is not allowed': BadRequest, # {"message":"\"createdAt\" is not allowed","path":["createdAt"],"type":"object.unknown","context":{"child":"createdAt","label":"createdAt","value":"DESC","key":"createdAt"}} + 'Insufficient funds': InsufficientFunds, # {"ok":false,"code":450000,"data":null,"message":"Insufficient funds"} + 'Invalid request params input': BadRequest, # {"ok":false,"code":400000,"data":{},"message":"Invalid request params input"} + 'must be a number': BadRequest, # [ExchangeError] bitteam {"message":"\"currency\" must be a number","path":["currency"],"type":"number.base","context":{"label":"currency","value":"adsf","key":"currency"}} + 'must be a string': BadRequest, # {"message":"\"pairId\" must be a string","path":["pairId"],"type":"string.base","context":{"label":"pairId","value":87,"key":"pairId"}} + 'must be of type': BadRequest, # {"message":"\"order\" must be of type object","path":["order"],"type":"object.base","context":{"type":"object","label":"order","value":"107218781","key":"order"}} + 'must be one of': BadRequest, # {"message":"\"resolution\" must be one of [1, 5, 15, 60, 1D]","path":["resolution"],"type":"any.only","context":{"valids":["1","5","15","60","1D"],"label":"resolution","value":"1d","key":"resolution"}} + 'Order not found': OrderNotFound, # {"ok":false,"code":404300,"data":{},"message":"Order not found"} + 'Pair with pair name': BadSymbol, # {"ok":false,"code":404000,"data":{"pairName":"ETH_USasdf"},"msg":"Pair with pair name ETH_USasdf was not found"} + 'pairName': BadSymbol, # {"message":"\"pairName\" length must be at least 7 characters long","path":["pairName"],"type":"string.min","context":{"limit":7,"value":"ETH_US","label":"pairName","key":"pairName"}} + 'Service Unavailable': ExchangeNotAvailable, # {"message":"Service Unavailable","code":403000,"ok":false} + 'Symbol ': BadSymbol, # {"ok":false,"code":404000,"data":{},"message":"Symbol asdfasdfas was not found"} + }, + }, + }) + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for bitteam + + https://bit.team/trade/api/documentation#/CCXT/getTradeApiCcxtPairs + + :param dict [params]: extra parameters specific to the exchange api endpoint + :returns dict[]: an array of objects representing market data + """ + response = await self.publicGetTradeApiCcxtPairs(params) + # + # { + # "ok": True, + # "result": { + # "count": 28, + # "pairs": [ + # { + # "id": 2, + # "name": "eth_usdt", + # "baseAssetId": 2, + # "quoteAssetId": 3, + # "fullName": "ETH USDT", + # "description": "ETH USDT", + # "lastBuy": 1964.665001, + # "lastSell": 1959.835005, + # "lastPrice": 1964.665001, + # "change24": 1.41, + # "volume24": 28.22627543, + # "volume24USD": 55662.35636401598, + # "active": True, + # "baseStep": 8, + # "quoteStep": 6, + # "status": 1, + # "settings": { + # "limit_usd": "0.1", + # "price_max": "10000000000000", + # "price_min": "1", + # "price_tick": "1", + # "pricescale": 10000, + # "lot_size_max": "1000000000000000", + # "lot_size_min": "1", + # "lot_size_tick": "1", + # "price_view_min": 6, + # "default_slippage": 10, + # "lot_size_view_min": 6 + # }, + # "updateId": "50620", + # "timeStart": "2021-01-28T09:19:30.706Z", + # "makerFee": 200, + # "takerFee": 200, + # "quoteVolume24": 54921.93404134529, + # "lowPrice24": 1919.355, + # "highPrice24": 1971.204995 + # }, + # { + # "id": 27, + # "name": "ltc_usdt", + # "baseAssetId": 13, + # "quoteAssetId": 3, + # "fullName": "LTC USDT", + # "description": "This is LTC USDT", + # "lastBuy": 53.14, + # "lastSell": 53.58, + # "lastPrice": 53.58, + # "change24": -6.72, + # "volume24": 0, + # "volume24USD": null, + # "active": True, + # "baseStep": 8, + # "quoteStep": 6, + # "status": 0, + # "settings": { + # "limit_usd": "0.1", + # "price_max": "1000000000000", + # "price_min": "1", + # "price_tick": "1", + # "pricescale": 10000, + # "lot_size_max": "1000000000000", + # "lot_size_min": "1", + # "lot_size_tick": "1", + # "price_view_min": 6, + # "default_slippage": 10, + # "lot_size_view_min": 6 + # }, + # "updateId": "30", + # "timeStart": "2021-10-13T12:11:05.359Z", + # "makerFee": 200, + # "takerFee": 200, + # "quoteVolume24": 0, + # "lowPrice24": null, + # "highPrice24": null + # } + # ] + # } + # } + # + result = self.safe_value(response, 'result', {}) + markets = self.safe_value(result, 'pairs', []) + return self.parse_markets(markets) + + def parse_market(self, market: dict) -> Market: + id = self.safe_string(market, 'name') + numericId = self.safe_integer(market, 'id') + parts = id.split('_') + baseId = self.safe_string(parts, 0) + quoteId = self.safe_string(parts, 1) + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + active = self.safe_value(market, 'active') + timeStart = self.safe_string(market, 'timeStart') + created = self.parse8601(timeStart) + minCost = None + currenciesValuedInUsd = self.safe_value(self.options, 'currenciesValuedInUsd', {}) + quoteInUsd = self.safe_bool(currenciesValuedInUsd, quote, False) + if quoteInUsd: + settings = self.safe_value(market, 'settings', {}) + minCost = self.safe_number(settings, 'limit_usd') + return self.safe_market_structure({ + 'id': id, + 'numericId': numericId, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': active, + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(self.parse_precision(self.safe_string(market, 'baseStep'))), + 'price': self.parse_number(self.parse_precision(self.safe_string(market, 'quoteStep'))), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': None, + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': minCost, + 'max': None, + }, + }, + 'created': created, + 'info': market, + }) + + async def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://bit.team/trade/api/documentation#/PUBLIC/getTradeApiCurrencies + + :param dict [params]: extra parameters specific to the bitteam api endpoint + :returns dict: an associative dictionary of currencies + """ + response = await self.publicGetTradeApiCurrencies(params) + # + # { + # "ok": True, + # "result": { + # "count": 24, + # "currencies": [ + # { + # "txLimits": { + # "minDeposit": "0.0001", + # "minWithdraw": "0.02", + # "maxWithdraw": "10000", + # "withdrawCommissionPercentage": "NaN", + # "withdrawCommissionFixed": "0.005" + # }, + # "id": 2, + # "status": 1, + # "symbol": "eth", + # "title": "Ethereum", + # "logoURL": "https://ethereum.org/static/6b935ac0e6194247347855dc3d328e83/34ca5/eth-diamond-black.png", + # "isDiscount": False, + # "address": "https://ethereum.org/", + # "description": "Ethereum ETH", + # "decimals": 18, + # "blockChain": "Ethereum", + # "precision": 8, + # "currentRate": null, + # "active": True, + # "timeStart": "2021-01-28T08:57:41.719Z", + # "type": "crypto", + # "typeNetwork": "internalGW", + # "idSorting": 2, + # "links": [ + # { + # "tx": "https://etherscan.io/tx/", + # "address": "https://etherscan.io/address/", + # "blockChain": "Ethereum" + # } + # ] + # }, + # { + # "txLimits": { + # "minDeposit": "0.001", + # "minWithdraw": "1", + # "maxWithdraw": "100000", + # "withdrawCommissionPercentage": "NaN", + # "withdrawCommissionFixed": { + # "Tron": "2", + # "Binance": "2", + # "Ethereum": "20" + # } + # }, + # "id": 3, + # "status": 1, + # "symbol": "usdt", + # "title": "Tether USD", + # "logoURL": "https://cryptologos.cc/logos/tether-usdt-logo.png?v=010", + # "isDiscount": False, + # "address": "https://tether.to/", + # "description": "Tether USD", + # "decimals": 6, + # "blockChain": "", + # "precision": 6, + # "currentRate": null, + # "active": True, + # "timeStart": "2021-01-28T09:04:17.170Z", + # "type": "crypto", + # "typeNetwork": "internalGW", + # "idSorting": 0, + # "links": [ + # { + # "tx": "https://etherscan.io/tx/", + # "address": "https://etherscan.io/address/", + # "blockChain": "Ethereum" + # }, + # { + # "tx": "https://tronscan.org/#/transaction/", + # "address": "https://tronscan.org/#/address/", + # "blockChain": "Tron" + # }, + # { + # "tx": "https://bscscan.com/tx/", + # "address": "https://bscscan.com/address/", + # "blockChain": "Binance" + # } + # ] + # } + # ] + # } + # } + # + responseResult = self.safe_value(response, 'result', {}) + currencies = self.safe_value(responseResult, 'currencies', []) + # usding another endpoint to fetch statuses of deposits and withdrawals + statusesResponse = await self.publicGetTradeApiCmcAssets() + # + # { + # "ZNX": { + # "name": "ZeNeX Coin", + # "unified_cryptoasset_id": 30, + # "withdrawStatus": True, + # "depositStatus": True, + # "min_withdraw": 0.00001, + # "max_withdraw": 10000 + # }, + # "USDT": { + # "name": "Tether USD", + # "unified_cryptoasset_id": 3, + # "withdrawStatus": True, + # "depositStatus": True, + # "min_withdraw": 1, + # "max_withdraw": 100000 + # }, + # } + # + statusesResponse = self.index_by(statusesResponse, 'unified_cryptoasset_id') + result: dict = {} + for i in range(0, len(currencies)): + currency = currencies[i] + id = self.safe_string(currency, 'symbol') + numericId = self.safe_integer(currency, 'id') + code = self.safe_currency_code(id) + active = self.safe_bool(currency, 'active', False) + precision = self.parse_number(self.parse_precision(self.safe_string(currency, 'precision'))) + txLimits = self.safe_value(currency, 'txLimits', {}) + minWithdraw = self.safe_string(txLimits, 'minWithdraw') + maxWithdraw = self.safe_string(txLimits, 'maxWithdraw') + minDeposit = self.safe_string(txLimits, 'minDeposit') + fee = None + withdrawCommissionFixed = self.safe_value(txLimits, 'withdrawCommissionFixed', {}) + feesByNetworkId: dict = {} + blockChain = self.safe_string(currency, 'blockChain') + # if only one blockChain + if (blockChain is not None) and (blockChain != ''): + fee = self.parse_number(withdrawCommissionFixed) + feesByNetworkId[blockChain] = fee + else: + feesByNetworkId = withdrawCommissionFixed + statuses = self.safe_value(statusesResponse, numericId, {}) + deposit = self.safe_value(statuses, 'depositStatus') + withdraw = self.safe_value(statuses, 'withdrawStatus') + networkIds = list(feesByNetworkId.keys()) + networks: dict = {} + networkPrecision = self.parse_number(self.parse_precision(self.safe_string(currency, 'decimals'))) + typeRaw = self.safe_string(currency, 'type') + for j in range(0, len(networkIds)): + networkId = networkIds[j] + networkCode = self.network_id_to_code(networkId, code) + networkFee = self.safe_number(feesByNetworkId, networkId) + networks[networkCode] = { + 'id': networkId, + 'network': networkCode, + 'deposit': deposit, + 'withdraw': withdraw, + 'active': active, + 'fee': networkFee, + 'precision': networkPrecision, + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': self.parse_number(minWithdraw), + 'max': self.parse_number(maxWithdraw), + }, + 'deposit': { + 'min': self.parse_number(minDeposit), + 'max': None, + }, + }, + 'info': currency, + } + result[code] = { + 'id': id, + 'numericId': numericId, + 'code': code, + 'name': code, + 'info': currency, + 'active': active, + 'deposit': deposit, + 'withdraw': withdraw, + 'fee': fee, + 'precision': precision, + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': self.parse_number(minWithdraw), + 'max': self.parse_number(maxWithdraw), + }, + 'deposit': { + 'min': self.parse_number(minDeposit), + 'max': None, + }, + }, + 'type': typeRaw, # 'crypto' or 'fiat' + 'networks': networks, + } + return result + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the bitteam api endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + resolution = self.safe_string(self.timeframes, timeframe, timeframe) + request: dict = { + 'pairName': market['id'], + 'resolution': resolution, + } + response = await self.historyGetApiTwHistoryPairNameResolution(self.extend(request, params)) + # + # { + # "ok": True, + # "result": { + # "count": 364, + # "data": [ + # { + # "t": 1669593600, + # "o": 16211.259266, + # "h": 16476.985001, + # "l": 16023.714999, + # "c": 16430.636894, + # "v": 2.60150368999999 + # }, + # { + # "t": 1669680000, + # "o": 16430.636894, + # "h": 17065.229582, + # "l": 16346.114155, + # "c": 16882.297736, + # "v": 3.0872548400000115 + # }, + # ... + # ] + # } + # } + # + result = self.safe_value(response, 'result', {}) + data = self.safe_list(result, 'data', []) + return self.parse_ohlcvs(data, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # { + # "t": 1669680000, + # "o": 16430.636894, + # "h": 17065.229582, + # "l": 16346.114155, + # "c": 16882.297736, + # "v": 3.0872548400000115 + # }, + # + return [ + self.safe_timestamp(ohlcv, 't'), + self.safe_number(ohlcv, 'o'), + self.safe_number(ohlcv, 'h'), + self.safe_number(ohlcv, 'l'), + self.safe_number(ohlcv, 'c'), + self.safe_number(ohlcv, 'v'), + ] + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://bit.team/trade/api/documentation#/CMC/getTradeApiCmcOrderbookPair + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return(default 100, max 200) + :param dict [params]: extra parameters specific to the bitteam api endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + response = await self.publicGetTradeApiCmcOrderbookPair(self.extend(request, params)) + # + # { + # "timestamp": 1701166703285, + # "bids": [ + # [ + # 2019.334988, + # 0.09048525 + # ], + # [ + # 1999.860002, + # 0.0225 + # ], + # ... + # ], + # "asks": [ + # [ + # 2019.334995, + # 0.00899078 + # ], + # [ + # 2019.335013, + # 0.09833052 + # ], + # ... + # ] + # } + # + timestamp = self.safe_integer(response, 'timestamp') + orderbook = self.parse_order_book(response, symbol, timestamp) + return orderbook + + async def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://bit.team/trade/api/documentation#/PRIVATE/getTradeApiCcxtOrdersofuser + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of orde structures to retrieve(default 10) + :param dict [params]: extra parameters specific to the bitteam api endpoint + :param str [params.type]: the status of the order - 'active', 'closed', 'cancelled', 'all', 'history'(default 'all') + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + type = self.safe_string(params, 'type', 'all') + request: dict = { + 'type': type, + } + market = None + if symbol is not None: + market = self.market(symbol) + request['pair'] = market['id'] + if limit is not None: + request['limit'] = limit + response = await self.privateGetTradeApiCcxtOrdersOfUser(self.extend(request, params)) + # + # { + # "ok": True, + # "result": { + # "count": 3, + # "orders": [ + # { + # "id": 106733026, + # "orderId": null, + # "userId": 21639, + # "pair": "btc_usdt", + # "pairId": 22, + # "quantity": "0.00001", + # "price": "40", + # "executedPrice": "0", + # "fee": null, + # "orderCid": null, + # "executed": "0", + # "expires": null, + # "baseDecimals": 8, + # "quoteDecimals": 6, + # "timestamp": 1700594804, + # "status": "inactive", + # "side": "buy", + # "type": "limit", + # "createdAt": "2023-11-21T19:26:43.868Z", + # "updatedAt": "2023-11-21T19:26:43.868Z" + # }, + # { + # "id": 106733308, + # "orderId": "13074362", + # "userId": 21639, + # "pair": "btc_usdt", + # "pairId": 22, + # "quantity": "0.00001", + # "price": "50000", + # "executedPrice": "37017.495008", + # "fee": { + # "amount": "0.00000002", + # "symbol": "btc", + # "userId": 21639, + # "decimals": 8, + # "symbolId": 11 + # }, + # "orderCid": null, + # "executed": "0.00001", + # "expires": null, + # "baseDecimals": 8, + # "quoteDecimals": 6, + # "timestamp": 1700594959, + # "status": "executed", + # "side": "buy", + # "type": "limit", + # "createdAt": "2023-11-21T19:29:19.946Z", + # "updatedAt": "2023-11-21T19:29:19.946Z" + # }, + # { + # "id": 106734455, + # "orderId": "13248984", + # "userId": 21639, + # "pair": "eth_usdt", + # "pairId": 2, + # "quantity": "0.001", + # "price": "1750", + # "executedPrice": "0", + # "fee": null, + # "orderCid": null, + # "executed": "0", + # "expires": null, + # "baseDecimals": 18, + # "quoteDecimals": 6, + # "timestamp": 1700595523, + # "status": "accepted", + # "side": "buy", + # "type": "limit", + # "createdAt": "2023-11-21T19:38:43.530Z", + # "updatedAt": "2023-11-21T19:38:43.530Z" + # } + # ] + # } + # } + # + result = self.safe_value(response, 'result', {}) + orders = self.safe_list(result, 'orders', []) + return self.parse_orders(orders, market, since, limit) + + async def fetch_order(self, id: str, symbol: Str = None, params={}) -> Order: + """ + fetches information on an order + + https://bit.team/trade/api/documentation#/PRIVATE/getTradeApiCcxtOrderId + + :param int|str id: order id + :param str symbol: not used by bitteam fetchOrder() + :param dict [params]: extra parameters specific to the bitteam api endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + request: dict = { + 'id': id, + } + market = None + if symbol is not None: + market = self.market(symbol) + response = await self.privateGetTradeApiCcxtOrderId(self.extend(request, params)) + # + # { + # "ok": True, + # "result": { + # "id": 106494347, + # "orderId": "13214332", + # "userId": 15912, + # "pair": "eth_usdt", + # "pairId": 2, + # "quantity": "0.00448598", + # "price": "2015.644995", + # "executedPrice": "2015.644995", + # "fee": { + # "amount": "0", + # "symbol": "eth", + # "userId": 15912, + # "decimals": 18, + # "symbolId": 2, + # "discountAmount": "0", + # "discountSymbol": "btt", + # "discountDecimals": 18, + # "discountSymbolId": 5 + # }, + # "orderCid": null, + # "executed": "0.00448598", + # "expires": null, + # "baseDecimals": 18, + # "quoteDecimals": 6, + # "timestamp": 1700470476, + # "status": "executed", + # "side": "buy", + # "type": "limit", + # "stopPrice": null, + # "slippage": null + # } + # } + # + result = self.safe_dict(response, 'result') + return self.parse_order(result, market) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://bit.team/trade/api/documentation#/PRIVATE/getTradeApiCcxtOrdersofuser + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open order structures to retrieve(default 10) + :param dict [params]: extra parameters specific to the bitteam api endpoint + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + request: dict = { + 'type': 'active', + } + return await self.fetch_orders(symbol, since, limit, self.extend(request, params)) + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://bit.team/trade/api/documentation#/PRIVATE/getTradeApiCcxtOrdersofuser + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of closed order structures to retrieve(default 10) + :param dict [params]: extra parameters specific to the bitteam api endpoint + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + request: dict = { + 'type': 'closed', + } + return await self.fetch_orders(symbol, since, limit, self.extend(request, params)) + + async def fetch_canceled_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches information on multiple canceled orders made by the user + + https://bit.team/trade/api/documentation#/PRIVATE/getTradeApiCcxtOrdersofuser + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of canceled order structures to retrieve(default 10) + :param dict [params]: extra parameters specific to the bitteam api endpoint + :returns dict: a list of `order structures ` + """ + await self.load_markets() + request: dict = { + 'type': 'cancelled', + } + return await self.fetch_orders(symbol, since, limit, self.extend(request, params)) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://bit.team/trade/api/documentation#/PRIVATE/postTradeApiCcxtOrdercreate + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the bitteam api endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pairId': str(market['numericId']), + 'type': type, + 'side': side, + 'amount': self.amount_to_precision(symbol, amount), + } + if type == 'limit': + if price is None: + raise ArgumentsRequired(self.id + ' createOrder() requires a price argument for a ' + type + ' order') + else: + request['price'] = self.price_to_precision(symbol, price) + response = await self.privatePostTradeApiCcxtOrdercreate(self.extend(request, params)) + # + # { + # "ok": True, + # "result": { + # "id": 106733308, + # "userId": 21639, + # "quantity": "0.00001", + # "pair": "btc_usdt", + # "side": "buy", + # "price": "50000", + # "executed": "0", + # "executedPrice": "0", + # "status": "created", + # "baseDecimals": 8, + # "quoteDecimals": 6, + # "pairId": 22, + # "type": "limit", + # "stopPrice": null, + # "slippage": null, + # "timestamp": "1700594959" + # } + # } + # + order = self.safe_dict(response, 'result', {}) + return self.parse_order(order, market) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://bit.team/trade/api/documentation#/PRIVATE/postTradeApiCcxtCancelorder + + :param str id: order id + :param str symbol: not used by bitteam cancelOrder() + :param dict [params]: extra parameters specific to the bitteam api endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + request: dict = { + 'id': id, + } + response = await self.privatePostTradeApiCcxtCancelorder(self.extend(request, params)) + # + # { + # "ok": True, + # "result": { + # "message": "The request to cancel your order was received" + # } + # } + # + result = self.safe_dict(response, 'result', {}) + return self.parse_order(result) + + async def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel open orders of market + + https://bit.team/trade/api/documentation#/PRIVATE/postTradeApiCcxtCancelallorder + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the bitteam api endpoint + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + market = None + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['pairId'] = str(market['numericId']) + else: + request['pairId'] = '0' # '0' for all markets + response = await self.privatePostTradeApiCcxtCancelAllOrder(self.extend(request, params)) + # + # { + # "ok": True, + # "result": { + # "message":"The request to cancel all your orders was received" + # } + # } + # + result = self.safe_value(response, 'result', {}) + orders = [result] + return self.parse_orders(orders, market) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # fetchOrders + # { + # "id": 106733308, + # "orderId": "13074362", + # "userId": 21639, + # "pair": "btc_usdt", + # "pairId": 22, + # "quantity": "0.00001", + # "price": "50000", + # "executedPrice": "37017.495008", + # "fee": { + # "amount": "0.00000002", + # "symbol": "btc", + # "userId": 21639, + # "decimals": 8, + # "symbolId": 11 + # }, + # "orderCid": null, + # "executed": "0.00001", + # "expires": null, + # "baseDecimals": 8, + # "quoteDecimals": 6, + # "timestamp": 1700594959, + # "status": "executed", + # "side": "buy", + # "type": "limit", + # "createdAt": "2023-11-21T19:29:19.946Z", + # "updatedAt": "2023-11-21T19:29:19.946Z" + # }, + # + # fetchOrder + # { + # "id": 106494347, + # "orderId": "13214332", + # "userId": 15912, + # "pair": "eth_usdt", + # "pairId": 2, + # "quantity": "0.00448598", + # "price": "2015.644995", + # "executedPrice": "2015.644995", + # "fee": { + # "amount": "0", + # "symbol": "eth", + # "userId": 15912, + # "decimals": 18, + # "symbolId": 2, + # "discountAmount": "0", + # "discountSymbol": "btt", + # "discountDecimals": 18, + # "discountSymbolId": 5 + # }, + # "orderCid": null, + # "executed": "0.00448598", + # "expires": null, + # "baseDecimals": 18, + # "quoteDecimals": 6, + # "timestamp": 1700470476, + # "status": "executed", + # "side": "buy", + # "type": "limit", + # "stopPrice": null, + # "slippage": null + # } + # + # createOrder + # { + # "id": 106733308, + # "userId": 21639, + # "quantity": "0.00001", + # "pair": "btc_usdt", + # "side": "buy", + # "price": "50000", + # "executed": "0", + # "executedPrice": "0", + # "status": "created", + # "baseDecimals": 8, + # "quoteDecimals": 6, + # "pairId": 22, + # "type": "limit", + # "stopPrice": null, + # "slippage": null, + # "timestamp": "1700594959" + # } + # + id = self.safe_string(order, 'id') + marketId = self.safe_string(order, 'pair') + market = self.safe_market(marketId, market) + clientOrderId = self.safe_string(order, 'orderCid') + timestamp = None + createdAt = self.safe_string(order, 'createdAt') + if createdAt is not None: + timestamp = self.parse8601(createdAt) + else: + timestamp = self.safe_timestamp(order, 'timestamp') + updatedAt = self.safe_string(order, 'updatedAt') + lastUpdateTimestamp = self.parse8601(updatedAt) + status = self.parse_order_status(self.safe_string(order, 'status')) + type = self.parse_order_type(self.safe_string(order, 'type')) + side = self.safe_string(order, 'side') + feeRaw = self.safe_value(order, 'fee') + price = self.safe_string(order, 'price') + amount = self.safe_string(order, 'quantity') + filled = self.safe_string(order, 'executed') + fee = None + if feeRaw is not None: + feeCost = self.safe_string(feeRaw, 'amount') + feeCurrencyId = self.safe_string(feeRaw, 'symbol') + fee = { + 'currency': self.safe_currency_code(feeCurrencyId), + 'cost': feeCost, + 'rate': None, + } + return self.safe_order({ + 'id': id, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'lastUpdateTimestamp': lastUpdateTimestamp, + 'status': status, + 'symbol': market['symbol'], + 'type': type, + 'timeInForce': 'GTC', + 'side': side, + 'price': price, + 'triggerPrice': self.safe_string(order, 'stopPrice'), + 'average': None, + 'amount': amount, + 'cost': None, + 'filled': filled, + 'remaining': None, + 'fee': fee, + 'trades': None, + 'info': order, + 'postOnly': False, + }, market) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'accepted': 'open', + 'executed': 'closed', + 'cancelled': 'canceled', + 'partiallyCancelled': 'canceled', + 'delete': 'rejected', + 'inactive': 'rejected', + 'executing': 'open', + 'created': 'open', + } + return self.safe_string(statuses, status, status) + + def parse_order_type(self, status): + statuses: dict = { + 'market': 'market', + 'limit': 'limit', + } + return self.safe_string(statuses, status, status) + + def parse_value_to_pricision(self, valueObject, valueKey, preciseObject, precisionKey): + valueRawString = self.safe_string(valueObject, valueKey) + precisionRawString = self.safe_string(preciseObject, precisionKey) + if valueRawString is None or precisionRawString is None: + return None + precisionString = self.parse_precision(precisionRawString) + return Precise.string_mul(valueRawString, precisionString) + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical calculations with the information calculated over the past 24 hours each market + + https://bit.team/trade/api/documentation#/CMC/getTradeApiCmcSummary + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the bitteam api endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + response = await self.publicGetTradeApiCmcSummary() + # + # [ + # { + # "trading_pairs": "BTC_USDT", + # "base_currency": "BTC", + # "quote_currency": "USDT", + # "last_price": 37669.955001, + # "lowest_ask": 37670.055, + # "highest_bid": 37669.955, + # "base_volume": 6.81156888, + # "quote_volume": 257400.516878529, + # "price_change_percent_24h": -0.29, + # "highest_price_24h": 38389.994463, + # "lowest_price_24h": 37574.894999 + # }, + # { + # "trading_pairs": "BNB_USDT", + # "base_currency": "BNB", + # "quote_currency": "USDT", + # "last_price": 233.525142, + # "lowest_ask": 233.675, + # "highest_bid": 233.425, + # "base_volume": 245.0199339, + # "quote_volume": 57356.91823827642, + # "price_change_percent_24h": -0.32, + # "highest_price_24h": 236.171123, + # "lowest_price_24h": 231.634637 + # }, + # ... + # ] + # + tickers = [] + if not isinstance(response, list): + response = [] + for i in range(0, len(response)): + rawTicker = response[i] + ticker = self.parse_ticker(rawTicker) + tickers.append(ticker) + return self.filter_by_array_tickers(tickers, 'symbol', symbols) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://bit.team/trade/api/documentation#/PUBLIC/getTradeApiPairName + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the bitteam api endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'name': market['id'], + } + response = await self.publicGetTradeApiPairName(self.extend(request, params)) + # + # { + # "ok": True, + # "result": { + # "pair": { + # "id": 2, + # "name": "eth_usdt", + # "baseAssetId": 2, + # "quoteAssetId": 3, + # "fullName": "ETH USDT", + # "description": "ETH USDT", + # "lastBuy": "1976.715012", + # "lastSell": "1971.995006", + # "lastPrice": "1976.715012", + # "change24": "1.02", + # "volume24": 24.0796457, + # "volume24USD": 44282.347995912205, + # "active": True, + # "baseStep": 8, + # "quoteStep": 6, + # "status": 1, + # "settings": { + # "limit_usd": "0.1", + # "price_max": "10000000000000", + # "price_min": "1", + # "price_tick": "1", + # "pricescale": 10000, + # "lot_size_max": "1000000000000000", + # "lot_size_min": "1", + # "lot_size_tick": "1", + # "price_view_min": 6, + # "default_slippage": 10, + # "lot_size_view_min": 6 + # }, + # "asks": [ + # { + # "price": "1976.405003", + # "quantity": "0.0051171", + # "amount": "10.1134620408513" + # }, + # { + # "price": "1976.405013", + # "quantity": "0.09001559", + # "amount": "177.90726332415267" + # }, + # { + # "price": "2010.704988", + # "quantity": "0.00127892", + # "amount": "2.57153082325296" + # } + # ], + # "bids": [ + # { + # "price": "1976.404988", + # "quantity": "0.09875861", + # "amount": "195.18700941194668" + # }, + # { + # "price": "1905.472973", + # "quantity": "0.00263591", + # "amount": "5.02265526426043" + # }, + # { + # "price": "1904.274973", + # "quantity": "0.09425304", + # "amount": "179.48370520116792" + # } + # ], + # "updateId": "78", + # "timeStart": "2021-01-28T09:19:30.706Z", + # "makerFee": 200, + # "takerFee": 200, + # "quoteVolume24": 49125.1374009045, + # "lowPrice24": 1966.704999, + # "highPrice24": 2080.354997, + # "baseCurrency": { + # "id": 2, + # "status": 1, + # "symbol": "eth", + # "title": "Ethereum", + # "logoURL": "https://ethereum.org/static/6b935ac0e6194247347855dc3d328e83/34ca5/eth-diamond-black.png", + # "isDiscount": False, + # "address": "https://ethereum.org/", + # "description": "Ethereum ETH", + # "decimals": 18, + # "blockChain": "Ethereum", + # "precision": 8, + # "currentRate": null, + # "active": True, + # "timeStart": "2021-01-28T08:57:41.719Z", + # "txLimits": { + # "minDeposit": "100000000000000", + # "maxWithdraw": "10000000000000000000000", + # "minWithdraw": "20000000000000000", + # "withdrawCommissionFixed": "5000000000000000", + # "withdrawCommissionPercentage": "NaN" + # }, + # "type": "crypto", + # "typeNetwork": "internalGW", + # "icon": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzAiIGhlaWdodD0iMzAiIHZpZXdCb3g9IjAgMCAzMCAzMCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTAgMTVDMCA2LjcxNTczIDYuNzE1NzMgMCAxNSAwVjBDMjMuMjg0MyAwIDMwIDYuNzE1NzMgMzAgMTVWMTVDMzAgMjMuMjg0MyAyMy4yODQzIDMwIDE1IDMwVjMwQzYuNzE1NzMgMzAgMCAyMy4yODQzIDAgMTVWMTVaIiBmaWxsPSJibGFjayIvPgo8cGF0aCBkPSJNMTQuOTU1NyAxOS45NzM5TDkgMTYuMzUwOUwxNC45NTIxIDI1TDIwLjkxMDkgMTYuMzUwOUwxNC45NTIxIDE5Ljk3MzlIMTQuOTU1N1pNMTUuMDQ0MyA1TDkuMDkwOTUgMTUuMTg1M0wxNS4wNDQzIDE4LjgxNDZMMjEgMTUuMTg5MUwxNS4wNDQzIDVaIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K", + # "idSorting": 2, + # "links": [ + # { + # "tx": "https://etherscan.io/tx/", + # "address": "https://etherscan.io/address/", + # "blockChain": "Ethereum" + # } + # ], + # "clientTxLimits": { + # "minDeposit": "0.0001", + # "minWithdraw": "0.02", + # "maxWithdraw": "10000", + # "withdrawCommissionPercentage": "NaN", + # "withdrawCommissionFixed": "0.005" + # } + # }, + # "quoteCurrency": { + # "id": 3, + # "status": 1, + # "symbol": "usdt", + # "title": "Tether USD", + # "logoURL": "https://cryptologos.cc/logos/tether-usdt-logo.png?v=010", + # "isDiscount": False, + # "address": "https://tether.to/", + # "description": "Tether USD", + # "decimals": 6, + # "blockChain": "", + # "precision": 6, + # "currentRate": null, + # "active": True, + # "timeStart": "2021-01-28T09:04:17.170Z", + # "txLimits": { + # "minDeposit": "1000", + # "maxWithdraw": "100000000000", + # "minWithdraw": "1000000", + # "withdrawCommissionFixed": { + # "Tron": "2000000", + # "Binance": "2000000000000000000", + # "Ethereum": "20000000" + # }, + # "withdrawCommissionPercentage": "NaN" + # }, + # "type": "crypto", + # "typeNetwork": "internalGW", + # "icon": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzAiIGhlaWdodD0iMzAiIHZpZXdCb3g9IjAgMCAzMCAzMCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTAgMTVDMCA2LjcxNTczIDYuNzE1NzMgMCAxNSAwVjBDMjMuMjg0MyAwIDMwIDYuNzE1NzMgMzAgMTVWMTVDMzAgMjMuMjg0MyAyMy4yODQzIDMwIDE1IDMwVjMwQzYuNzE1NzMgMzAgMCAyMy4yODQzIDAgMTVWMTVaIiBmaWxsPSIjNkZBNjg4Ii8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMjMgN0g3VjExSDEzVjEyLjA2MkM4Ljk5MjAyIDEyLjMxNDYgNiAxMy4zMTAyIDYgMTQuNUM2IDE1LjY4OTggOC45OTIwMiAxNi42ODU0IDEzIDE2LjkzOFYyM0gxN1YxNi45MzhDMjEuMDA4IDE2LjY4NTQgMjQgMTUuNjg5OCAyNCAxNC41QzI0IDEzLjMxMDIgMjEuMDA4IDEyLjMxNDYgMTcgMTIuMDYyVjExSDIzVjdaTTcuNSAxNC41QzcuNSAxMy40NjA2IDkuMzMzMzMgMTIuMzY4IDEzIDEyLjA3NTZWMTUuNUgxN1YxMi4wNzU5QzIwLjkzODQgMTIuMzkyNyAyMi41IDEzLjYzMzkgMjIuNSAxNC41QzIyLjUgMTUuMzIyIDIwLjAwMDggMTUuODA2MSAxNyAxNS45NTI1QzE1LjcwODIgMTYuMDQ2MiAxMy43OTUxIDE1Ljk4MjYgMTMgMTUuOTM5MUM5Ljk5OTIxIDE1Ljc1NTkgNy41IDE1LjE4MDkgNy41IDE0LjVaIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K", + # "idSorting": 0, + # "links": [ + # { + # "tx": "https://etherscan.io/tx/", + # "address": "https://etherscan.io/address/", + # "blockChain": "Ethereum" + # }, + # { + # "tx": "https://tronscan.org/#/transaction/", + # "address": "https://tronscan.org/#/address/", + # "blockChain": "Tron" + # }, + # { + # "tx": "https://bscscan.com/tx/", + # "address": "https://bscscan.com/address/", + # "blockChain": "Binance" + # } + # ], + # "clientTxLimits": { + # "minDeposit": "0.001", + # "minWithdraw": "1", + # "maxWithdraw": "100000", + # "withdrawCommissionPercentage": "NaN", + # "withdrawCommissionFixed": { + # "Tron": "2", + # "Binance": "2", + # "Ethereum": "20" + # } + # } + # }, + # "quantities": { + # "asks": "5.58760757", + # "bids": "2226.98663823032198" + # } + # } + # } + # } + # + result = self.safe_value(response, 'result', {}) + pair = self.safe_dict(result, 'pair', {}) + return self.parse_ticker(pair, market) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # fetchTicker + # { + # "id": 2, + # "name": "eth_usdt", + # "baseAssetId": 2, + # "quoteAssetId": 3, + # "fullName": "ETH USDT", + # "description": "ETH USDT", + # "lastBuy": "1976.715012", + # "lastSell": "1971.995006", + # "lastPrice": "1976.715012", + # "change24": "1.02", + # "volume24": 24.0796457, + # "volume24USD": 44282.347995912205, + # "active": True, + # "baseStep": 8, + # "quoteStep": 6, + # "status": 1, + # "asks": [ + # { + # "price": "1976.405003", + # "quantity": "0.0051171", + # "amount": "10.1134620408513" + # }, + # { + # "price": "1976.405013", + # "quantity": "0.09001559", + # "amount": "177.90726332415267" + # }, + # { + # "price": "2010.704988", + # "quantity": "0.00127892", + # "amount": "2.57153082325296" + # } + # ... + # ], + # "bids": [ + # { + # "price": "1976.404988", + # "quantity": "0.09875861", + # "amount": "195.18700941194668" + # }, + # { + # "price": "1905.472973", + # "quantity": "0.00263591", + # "amount": "5.02265526426043" + # }, + # { + # "price": "1904.274973", + # "quantity": "0.09425304", + # "amount": "179.48370520116792" + # } + # ... + # ], + # "updateId": "78", + # "timeStart": "2021-01-28T09:19:30.706Z", + # "makerFee": 200, + # "takerFee": 200, + # "quoteVolume24": 49125.1374009045, + # "lowPrice24": 1966.704999, + # "highPrice24": 2080.354997, + # ... + # } + # + # fetchTickers + # { + # "trading_pairs": "BTC_USDT", + # "base_currency": "BTC", + # "quote_currency": "USDT", + # "last_price": 37669.955001, + # "lowest_ask": 37670.055, + # "highest_bid": 37669.955, + # "base_volume": 6.81156888, + # "quote_volume": 257400.516878529, + # "price_change_percent_24h": -0.29, + # "highest_price_24h": 38389.994463, + # "lowest_price_24h": 37574.894999 + # } + marketId = self.safe_string_lower(ticker, 'trading_pairs') + market = self.safe_market(marketId, market) + bestBidPrice = None + bestAskPrice = None + bestBidVolume = None + bestAskVolume = None + bids = self.safe_value(ticker, 'bids') + asks = self.safe_value(ticker, 'asks') + if (bids is not None) and (isinstance(bids, list)) and (asks is not None) and (isinstance(asks, list)): + bestBid = self.safe_value(bids, 0, {}) + bestBidPrice = self.safe_string(bestBid, 'price') + bestBidVolume = self.safe_string(bestBid, 'quantity') + bestAsk = self.safe_value(asks, 0, {}) + bestAskPrice = self.safe_string(bestAsk, 'price') + bestAskVolume = self.safe_string(bestAsk, 'quantity') + else: + bestBidPrice = self.safe_string(ticker, 'highest_bid') + bestAskPrice = self.safe_string(ticker, 'lowest_ask') + baseVolume = self.safe_string_2(ticker, 'volume24', 'base_volume') + quoteVolume = self.safe_string_2(ticker, 'quoteVolume24', 'quote_volume') + high = self.safe_string_2(ticker, 'highPrice24', 'highest_price_24h') + low = self.safe_string_2(ticker, 'lowPrice24', 'lowest_price_24h') + close = self.safe_string_2(ticker, 'lastPrice', 'last_price') + changePcnt = self.safe_string_2(ticker, 'change24', 'price_change_percent_24h') + return self.safe_ticker({ + 'symbol': market['symbol'], + 'timestamp': None, + 'datetime': None, + 'open': None, + 'high': high, + 'low': low, + 'close': close, + 'bid': bestBidPrice, + 'bidVolume': bestBidVolume, + 'ask': bestAskPrice, + 'askVolume': bestAskVolume, + 'vwap': None, + 'previousClose': None, + 'change': None, + 'percentage': changePcnt, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'info': ticker, + }, market) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://bit.team/trade/api/documentation#/CMC/getTradeApiCmcTradesPair + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the bitteam api endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + response = await self.publicGetTradeApiCmcTradesPair(self.extend(request, params)) + # + # [ + # { + # "trade_id": 34970337, + # "price": 37769.994793, + # "base_volume": 0.00119062, + # "quote_volume": 44.96971120044166, + # "timestamp": 1700827234000, + # "type": "buy" + # }, + # { + # "trade_id": 34970347, + # "price": 37769.634497, + # "base_volume": 0.00104009, + # "quote_volume": 39.28381914398473, + # "timestamp": 1700827248000, + # "type": "buy" + # }, + # ... + # ] + # + return self.parse_trades(response, market, since, limit) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://bit.team/trade/api/documentation#/PRIVATE/getTradeApiCcxtTradesofuser + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve(default 10) + :param dict [params]: extra parameters specific to the bitteam api endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['pairId'] = market['numericId'] + if limit is not None: + request['limit'] = limit + response = await self.privateGetTradeApiCcxtTradesOfUser(self.extend(request, params)) + # + # { + # "ok": True, + # "result": { + # "count": 3, + # "trades": [ + # { + # "id": 34880724, + # "tradeId": "4368041", + # "makerOrderId": 106742914, + # "takerOrderId": 106761614, + # "pairId": 2, + # "quantity": "0.00955449", + # "price": "1993.674994", + # "isBuyerMaker": True, + # "baseDecimals": 18, + # "quoteDecimals": 6, + # "side": "sell", + # "timestamp": 1700615250, + # "rewarded": True, + # "makerUserId": 21639, + # "takerUserId": 15913, + # "baseCurrencyId": 2, + # "quoteCurrencyId": 3, + # "feeMaker": { + # "amount": "0.0000191", + # "symbol": "eth", + # "userId": 21639, + # "decimals": 18, + # "symbolId": 2 + # }, + # "feeTaker": { + # "amount": "0", + # "symbol": "usdt", + # "userId": 15913, + # "decimals": 6, + # "symbolId": 3, + # "discountAmount": "0", + # "discountSymbol": "btt", + # "discountDecimals": 18, + # "discountSymbolId": 5 + # }, + # "pair": "eth_usdt", + # "createdAt": "2023-11-22T01:07:30.593Z", + # "updatedAt": "2023-11-22T01:10:00.117Z", + # "isCurrentSide": "maker" + # }, + # { + # "id": 34875793, + # "tradeId": "4368010", + # "makerOrderId": 106742914, + # "takerOrderId": 106745926, + # "pairId": 2, + # "quantity": "0.0027193", + # "price": "1993.674994", + # "isBuyerMaker": True, + # "baseDecimals": 18, + # "quoteDecimals": 6, + # "side": "sell", + # "timestamp": 1700602983, + # "rewarded": True, + # "makerUserId": 21639, + # "takerUserId": 15912, + # "baseCurrencyId": 2, + # "quoteCurrencyId": 3, + # "feeMaker": { + # "amount": "0.00000543", + # "symbol": "eth", + # "userId": 21639, + # "decimals": 18, + # "symbolId": 2 + # }, + # "feeTaker": { + # "amount": "0", + # "symbol": "usdt", + # "userId": 15912, + # "decimals": 6, + # "symbolId": 3, + # "discountAmount": "0", + # "discountSymbol": "btt", + # "discountDecimals": 18, + # "discountSymbolId": 5 + # }, + # "pair": "eth_usdt", + # "createdAt": "2023-11-21T21:43:02.758Z", + # "updatedAt": "2023-11-21T21:45:00.147Z", + # "isCurrentSide": "maker" + # }, + # { + # "id": 34871727, + # "tradeId": "3441840", + # "makerOrderId": 106733299, + # "takerOrderId": 106733308, + # "pairId": 22, + # "quantity": "0.00001", + # "price": "37017.495008", + # "isBuyerMaker": False, + # "baseDecimals": 8, + # "quoteDecimals": 6, + # "side": "buy", + # "timestamp": 1700594960, + # "rewarded": True, + # "makerUserId": 15909, + # "takerUserId": 21639, + # "baseCurrencyId": 11, + # "quoteCurrencyId": 3, + # "feeMaker": { + # "amount": "0", + # "symbol": "usdt", + # "userId": 15909, + # "decimals": 6, + # "symbolId": 3, + # "discountAmount": "0", + # "discountSymbol": "btt", + # "discountDecimals": 18, + # "discountSymbolId": 5 + # }, + # "feeTaker": { + # "amount": "0.00000002", + # "symbol": "btc", + # "userId": 21639, + # "decimals": 8, + # "symbolId": 11 + # }, + # "pair": "btc_usdt", + # "createdAt": "2023-11-21T19:29:20.092Z", + # "updatedAt": "2023-11-21T19:30:00.159Z" + # "isCurrentSide": "taker" + # } + # ] + # } + # } + # + result = self.safe_value(response, 'result', {}) + trades = self.safe_list(result, 'trades', []) + return self.parse_trades(trades, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades + # { + # "trade_id": 34970337, + # "price": 37769.994793, + # "base_volume": 0.00119062, + # "quote_volume": 44.96971120044166, + # "timestamp": 1700827234000, + # "type": "buy" + # }, + # + # fetchMyTrades + # { + # "id": 34875793, + # "tradeId": "4368010", + # "makerOrderId": 106742914, + # "takerOrderId": 106745926, + # "pairId": 2, + # "quantity": "0.0027193", + # "price": "1993.674994", + # "isBuyerMaker": True, + # "baseDecimals": 18, + # "quoteDecimals": 6, + # "side": "sell", + # "timestamp": 1700602983, + # "rewarded": True, + # "makerUserId": 21639, + # "takerUserId": 15912, + # "baseCurrencyId": 2, + # "quoteCurrencyId": 3, + # "feeMaker": { + # "amount": "0.00000543", + # "symbol": "eth", + # "userId": 21639, + # "decimals": 18, + # "symbolId": 2 + # }, + # "feeTaker": { + # "amount": "0", + # "symbol": "usdt", + # "userId": 15912, + # "decimals": 6, + # "symbolId": 3, + # "discountAmount": "0", + # "discountSymbol": "btt", + # "discountDecimals": 18, + # "discountSymbolId": 5 + # }, + # "pair": "eth_usdt", + # "createdAt": "2023-11-21T21:43:02.758Z", + # "updatedAt": "2023-11-21T21:45:00.147Z", + # "isCurrentSide": "maker" + # } + # + marketId = self.safe_string(trade, 'pair') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + id = self.safe_string_2(trade, 'id', 'trade_id') + price = self.safe_string(trade, 'price') + amount = self.safe_string_2(trade, 'quantity', 'base_volume') + cost = self.safe_string(trade, 'quote_volume') + takerOrMaker = self.safe_string(trade, 'isCurrentSide') + timestamp = self.safe_string(trade, 'timestamp') + if takerOrMaker is not None: + timestamp = Precise.string_mul(timestamp, '1000') + # the exchange returns the side of the taker + side = self.safe_string_2(trade, 'side', 'type') + feeInfo = None + order = None + if takerOrMaker == 'maker': + if side == 'sell': + side = 'buy' + elif side == 'buy': + side = 'sell' + order = self.safe_string(trade, 'makerOrderId') + feeInfo = self.safe_value(trade, 'feeMaker', {}) + elif takerOrMaker == 'taker': + order = self.safe_string(trade, 'takerOrderId') + feeInfo = self.safe_value(trade, 'feeTaker', {}) + feeCurrencyId = self.safe_string(feeInfo, 'symbol') + feeCost = self.safe_string(feeInfo, 'amount') + fee = { + 'currency': self.safe_currency_code(feeCurrencyId), + 'cost': feeCost, + } + intTs = self.parse_to_int(timestamp) + return self.safe_trade({ + 'id': id, + 'order': order, + 'timestamp': intTs, + 'datetime': self.iso8601(intTs), + 'symbol': symbol, + 'type': None, + 'side': side, + 'takerOrMaker': takerOrMaker, + 'price': price, + 'amount': amount, + 'cost': cost, + 'fee': fee, + 'info': trade, + }, market) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://bit.team/trade/api/documentation#/PRIVATE/getTradeApiCcxtBalance + + :param dict [params]: extra parameters specific to the betteam api endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + response = await self.privateGetTradeApiCcxtBalance(params) + return self.parse_balance(response) + + def parse_balance(self, response) -> Balances: + # + # { + # "ok": True, + # "result": { + # "free": { + # "USDT": "0", + # "DEL": "0", + # "BTC": "0", + # ... + # }, + # "used": { + # "USDT": "0", + # "DEL": "0", + # "BTC": "0", + # ... + # }, + # "total": { + # "USDT": "0", + # "DEL": "0", + # "BTC": "0", + # ... + # }, + # "USDT": { + # "free": "0", + # "used": "0", + # "total": "0", + # }, + # "DEL": { + # "free": "0", + # "used": "0", + # "total": "0", + # }, + # "BTC": { + # "free": "0", + # "used": "0", + # "total": "0", + # } + # ... + # } + # } + # + timestamp = self.milliseconds() + balance: dict = { + 'info': response, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + result = self.safe_value(response, 'result', {}) + balanceByCurrencies = self.omit(result, ['free', 'used', 'total']) + rawCurrencyIds = list(balanceByCurrencies.keys()) + for i in range(0, len(rawCurrencyIds)): + rawCurrencyId = rawCurrencyIds[i] + currencyBalance = self.safe_value(result, rawCurrencyId) + free = self.safe_string(currencyBalance, 'free') + used = self.safe_string(currencyBalance, 'used') + total = self.safe_string(currencyBalance, 'total') + currencyCode = self.safe_currency_code(rawCurrencyId.lower()) + balance[currencyCode] = { + 'free': free, + 'used': used, + 'total': total, + } + return self.safe_balance(balance) + + async def fetch_deposits_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch history of deposits and withdrawals from external wallets and between CoinList Pro trading account and CoinList wallet + + https://bit.team/trade/api/documentation#/PRIVATE/getTradeApiTransactionsofuser + + :param str [code]: unified currency code for the currency of the deposit/withdrawals + :param int [since]: timestamp in ms of the earliest deposit/withdrawal + :param int [limit]: max number of deposit/withdrawals to return(default 10) + :param dict [params]: extra parameters specific to the bitteam api endpoint + :returns dict: a list of `transaction structure ` + """ + await self.load_markets() + currency = None + request: dict = {} + if code is not None: + currency = self.currency(code) + request['currency'] = currency['numericId'] + if limit is not None: + request['limit'] = limit + response = await self.privateGetTradeApiTransactionsOfUser(self.extend(request, params)) + # + # { + # "ok": True, + # "result": { + # "count": 2, + # "transactions": [ + # { + # "id": 1329686, + # "orderId": "2f060ad5-30f7-4f2b-ac5f-1bb8f5fd34dc", + # "transactionCoreId": "561863", + # "userId": 21639, + # "recipient": "0x9050dfA063D1bE7cA711c750b18D51fDD13e90Ee", + # "sender": "0x6894a93B6fea044584649278621723cac51443Cd", + # "symbolId": 2, + # "CommissionId": 17571, + # "amount": "44000000000000000", + # "params": {}, + # "reason": null, + # "timestamp": 1700715341743, + # "status": "approving", + # "statusDescription": null, + # "type": "withdraw", + # "message": null, + # "blockChain": "", + # "before": null, + # "after": null, + # "currency": { + # "symbol": "eth", + # "decimals": 18, + # "blockChain": "Ethereum", + # "links": [ + # { + # "tx": "https://etherscan.io/tx/", + # "address": "https://etherscan.io/address/", + # "blockChain": "Ethereum" + # } + # ] + # } + # }, + # { + # "id": 1329229, + # "orderId": null, + # "transactionCoreId": "561418", + # "userId": 21639, + # "recipient": "0x7d6a797f2406e06b2f9b41d067df324affa315dd", + # "sender": null, + # "symbolId": 3, + # "CommissionId": null, + # "amount": "100000000", + # "params": { + # "tx_id": "0x2253823c828d838acd983fe6a348fb0e034efe3874b081871d8b80da76ec758b" + # }, + # "reason": null, + # "timestamp": 1700594180417, + # "status": "success", + # "statusDescription": null, + # "type": "deposit", + # "message": null, + # "blockChain": "Ethereum", + # "before": 0, + # "after": 100000000, + # "currency": { + # "symbol": "usdt", + # "decimals": 6, + # "blockChain": "", + # "links": [ + # { + # "tx": "https://etherscan.io/tx/", + # "address": "https://etherscan.io/address/", + # "blockChain": "Ethereum" + # }, + # { + # "tx": "https://tronscan.org/#/transaction/", + # "address": "https://tronscan.org/#/address/", + # "blockChain": "Tron" + # }, + # { + # "tx": "https://bscscan.com/tx/", + # "address": "https://bscscan.com/address/", + # "blockChain": "Binance" + # } + # ] + # } + # } + # ] + # } + # } + # + result = self.safe_value(response, 'result', {}) + transactions = self.safe_list(result, 'transactions', []) + return self.parse_transactions(transactions, currency, since, limit) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # { + # "id": 1329229, + # "orderId": null, + # "transactionCoreId": "561418", + # "userId": 21639, + # "recipient": "0x7d6a797f2406e06b2f9b41d067df324affa315dd", + # "sender": null, + # "symbolId": 3, + # "CommissionId": null, + # "amount": "100000000", + # "params": { + # "tx_id": "0x2253823c828d838acd983fe6a348fb0e034efe3874b081871d8b80da76ec758b" + # }, + # "reason": null, + # "timestamp": 1700594180417, + # "status": "success", + # "statusDescription": null, + # "type": "deposit", + # "message": null, + # "blockChain": "Ethereum", + # "before": 0, + # "after": 100000000, + # "currency": { + # "symbol": "usdt", + # "decimals": 6, + # "blockChain": "", + # "links": [ + # { + # "tx": "https://etherscan.io/tx/", + # "address": "https://etherscan.io/address/", + # "blockChain": "Ethereum" + # }, + # { + # "tx": "https://tronscan.org/#/transaction/", + # "address": "https://tronscan.org/#/address/", + # "blockChain": "Tron" + # }, + # { + # "tx": "https://bscscan.com/tx/", + # "address": "https://bscscan.com/address/", + # "blockChain": "Binance" + # } + # ] + # } + # } + # + currencyObject = self.safe_value(transaction, 'currency') + currencyId = self.safe_string(currencyObject, 'symbol') + code = self.safe_currency_code(currencyId, currency) + id = self.safe_string(transaction, 'id') + params = self.safe_value(transaction, 'params') + txid = self.safe_string(params, 'tx_id') + timestamp = self.safe_integer(transaction, 'timestamp') + networkId = self.safe_string(transaction, 'blockChain') + if networkId is None: + links = self.safe_value(currencyObject, 'links', []) + blockChain = self.safe_value(links, 0, {}) + networkId = self.safe_string(blockChain, 'blockChain') + addressFrom = self.safe_string(transaction, 'sender') + addressTo = self.safe_string(transaction, 'recipient') + tag = self.safe_string(transaction, 'message') + type = self.parse_transaction_type(self.safe_string(transaction, 'type')) + amount = self.parse_value_to_pricision(transaction, 'amount', currencyObject, 'decimals') + status = self.parse_transaction_status(self.safe_value(transaction, 'status')) + return { + 'info': transaction, + 'id': id, + 'txid': txid, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': self.network_id_to_code(networkId), + 'addressFrom': addressFrom, + 'address': None, + 'addressTo': addressTo, + 'tagFrom': None, + 'tag': tag, + 'tagTo': None, + 'type': type, + 'amount': self.parse_number(amount), + 'currency': code, + 'status': status, + 'updated': None, + 'fee': None, + 'comment': self.safe_string(transaction, 'description'), + 'internal': False, + } + + def parse_transaction_type(self, type): + types: dict = { + 'deposit': 'deposit', + 'withdraw': 'withdrawal', + } + return self.safe_string(types, type, type) + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'approving': 'pending', + 'success': 'ok', + } + return self.safe_string(statuses, status, status) + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + request = self.omit(params, self.extract_params(path)) + endpoint = '/' + self.implode_params(path, params) + url = self.urls['api'][api] + endpoint + query = self.urlencode(request) + if api == 'private': + self.check_required_credentials() + if method == 'POST': + body = self.json(request) + elif len(query) != 0: + url += '?' + query + auth = self.apiKey + ':' + self.secret + auth64 = self.string_to_base64(auth) + signature = 'Basic ' + auth64 + headers = { + 'Authorization': signature, + 'Content-Type': 'application/json', + } + elif len(query) != 0: + url += '?' + query + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None + if code != 200: + if code == 404: + if (url.find('/ccxt/order/') >= 0) and (method == 'GET'): + parts = url.split('/order/') + orderId = self.safe_string(parts, 1) + raise OrderNotFound(self.id + ' order ' + orderId + ' not found') + if url.find('/cmc/orderbook/') >= 0: + parts = url.split('/cmc/orderbook/') + symbolId = self.safe_string(parts, 1) + raise BadSymbol(self.id + ' symbolId ' + symbolId + ' not found') + feedback = self.id + ' ' + body + message = self.safe_string(response, 'message') + responseCode = self.safe_string(response, 'code') + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], responseCode, feedback) + raise ExchangeError(feedback) + return None diff --git a/ccxt/async_support/bittrade.py b/ccxt/async_support/bittrade.py new file mode 100644 index 0000000..e23d66c --- /dev/null +++ b/ccxt/async_support/bittrade.py @@ -0,0 +1,1929 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.bittrade import ImplicitAPI +import hashlib +from ccxt.base.types import Account, Any, Balances, Currencies, Currency, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import NotSupported +from ccxt.base.errors import NetworkError +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.errors import OnMaintenance +from ccxt.base.errors import RequestTimeout +from ccxt.base.decimal_to_precision import TRUNCATE +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class bittrade(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(bittrade, self).describe(), { + 'id': 'bittrade', + 'name': 'BitTrade', + 'countries': ['JP'], + 'rateLimit': 100, + 'userAgent': self.userAgents['chrome39'], + 'certified': False, + 'version': 'v1', + 'hostname': 'api-cloud.bittrade.co.jp', + 'pro': True, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': None, + 'swap': False, + 'future': False, + 'option': False, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'cancelOrders': True, + 'createMarketBuyOrderWithCost': True, + 'createMarketOrderWithCost': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createStopLimitOrder': False, + 'createStopMarketOrder': False, + 'createStopOrder': False, + 'fetchAccounts': True, + 'fetchBalance': True, + 'fetchClosedOrders': True, + 'fetchCurrencies': True, + 'fetchDepositAddress': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchFundingHistory': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchIndexOHLCV': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchOrderTrades': True, + 'fetchPremiumIndexOHLCV': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingLimits': True, + 'fetchWithdrawals': True, + 'withdraw': True, + }, + 'timeframes': { + '1m': '1min', + '5m': '5min', + '15m': '15min', + '30m': '30min', + '1h': '60min', + '4h': '4hour', + '1d': '1day', + '1w': '1week', + '1M': '1mon', + '1y': '1year', + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/85734211-85755480-b705-11ea-8b35-0b7f1db33a2f.jpg', + 'api': { + 'market': 'https://{hostname}', + 'public': 'https://{hostname}', + 'private': 'https://{hostname}', + 'v2Public': 'https://{hostname}', + 'v2Private': 'https://{hostname}', + }, + 'www': 'https://www.bittrade.co.jp', + 'referral': 'https://www.bittrade.co.jp/register/?invite_code=znnq3', + 'doc': 'https://api-doc.bittrade.co.jp', + 'fees': 'https://www.bittrade.co.jp/ja-jp/support/fee', + }, + 'api': { + 'v2Public': { + 'get': { + 'reference/currencies': 1, # 币链参考信息 + 'market-status': 1, # 获取当前市场状态 + }, + }, + 'v2Private': { + 'get': { + 'account/ledger': 1, + 'account/withdraw/quota': 1, + 'account/withdraw/address': 1, # 提币地址查询(限母用户可用) + 'account/deposit/address': 1, + 'account/repayment': 5, # 还币交易记录查询 + 'reference/transact-fee-rate': 1, + 'account/asset-valuation': 0.2, # 获取账户资产估值 + 'point/account': 5, # 点卡余额查询 + 'sub-user/user-list': 1, # 获取子用户列表 + 'sub-user/user-state': 1, # 获取特定子用户的用户状态 + 'sub-user/account-list': 1, # 获取特定子用户的账户列表 + 'sub-user/deposit-address': 1, # 子用户充币地址查询 + 'sub-user/query-deposit': 1, # 子用户充币记录查询 + 'user/api-key': 1, # 母子用户API key信息查询 + 'user/uid': 1, # 母子用户获取用户UID + 'algo-orders/opening': 1, # 查询未触发OPEN策略委托 + 'algo-orders/history': 1, # 查询策略委托历史 + 'algo-orders/specific': 1, # 查询特定策略委托 + 'c2c/offers': 1, # 查询借入借出订单 + 'c2c/offer': 1, # 查询特定借入借出订单及其交易记录 + 'c2c/transactions': 1, # 查询借入借出交易记录 + 'c2c/repayment': 1, # 查询还币交易记录 + 'c2c/account': 1, # 查询账户余额 + 'etp/reference': 1, # 基础参考信息 + 'etp/transactions': 5, # 获取杠杆ETP申赎记录 + 'etp/transaction': 5, # 获取特定杠杆ETP申赎记录 + 'etp/rebalance': 1, # 获取杠杆ETP调仓记录 + 'etp/limit': 1, # 获取ETP持仓限额 + }, + 'post': { + 'account/transfer': 1, + 'account/repayment': 5, # 归还借币(全仓逐仓通用) + 'point/transfer': 5, # 点卡划转 + 'sub-user/management': 1, # 冻结/解冻子用户 + 'sub-user/creation': 1, # 子用户创建 + 'sub-user/tradable-market': 1, # 设置子用户交易权限 + 'sub-user/transferability': 1, # 设置子用户资产转出权限 + 'sub-user/api-key-generation': 1, # 子用户API key创建 + 'sub-user/api-key-modification': 1, # 修改子用户API key + 'sub-user/api-key-deletion': 1, # 删除子用户API key + 'sub-user/deduct-mode': 1, # 设置子用户手续费抵扣模式 + 'algo-orders': 1, # 策略委托下单 + 'algo-orders/cancel-all-after': 1, # 自动撤销订单 + 'algo-orders/cancellation': 1, # 策略委托(触发前)撤单 + 'c2c/offer': 1, # 借入借出下单 + 'c2c/cancellation': 1, # 借入借出撤单 + 'c2c/cancel-all': 1, # 撤销所有借入借出订单 + 'c2c/repayment': 1, # 还币 + 'c2c/transfer': 1, # 资产划转 + 'etp/creation': 5, # 杠杆ETP换入 + 'etp/redemption': 5, # 杠杆ETP换出 + 'etp/{transactId}/cancel': 10, # 杠杆ETP单个撤单 + 'etp/batch-cancel': 50, # 杠杆ETP批量撤单 + }, + }, + 'market': { + 'get': { + 'history/kline': 1, # 获取K线数据 + 'detail/merged': 1, # 获取聚合行情(Ticker) + 'depth': 1, # 获取 Market Depth 数据 + 'trade': 1, # 获取 Trade Detail 数据 + 'history/trade': 1, # 批量获取最近的交易记录 + 'detail': 1, # 获取 Market Detail 24小时成交量数据 + 'tickers': 1, + 'etp': 1, # 获取杠杆ETP实时净值 + }, + }, + 'public': { + 'get': { + 'common/symbols': 1, # 查询系统支持的所有交易对 + 'common/currencys': 1, # 查询系统支持的所有币种 + 'common/timestamp': 1, # 查询系统当前时间 + 'common/exchange': 1, # order limits + 'settings/currencys': 1, # ?language=en-US + }, + }, + 'private': { + 'get': { + 'account/accounts': 0.2, # 查询当前用户的所有账户(即account-id) + 'account/accounts/{id}/balance': 0.2, # 查询指定账户的余额 + 'account/accounts/{sub-uid}': 1, + 'account/history': 4, + 'cross-margin/loan-info': 1, + 'margin/loan-info': 1, # 查询借币币息率及额度 + 'fee/fee-rate/get': 1, + 'order/openOrders': 0.4, + 'order/orders': 0.4, + 'order/orders/{id}': 0.4, # 查询某个订单详情 + 'order/orders/{id}/matchresults': 0.4, # 查询某个订单的成交明细 + 'order/orders/getClientOrder': 0.4, + 'order/history': 1, # 查询当前委托、历史委托 + 'order/matchresults': 1, # 查询当前成交、历史成交 + # 'dw/withdraw-virtual/addresses', # 查询虚拟币提现地址(Deprecated) + 'query/deposit-withdraw': 1, + # 'margin/loan-info', # duplicate + 'margin/loan-orders': 0.2, # 借贷订单 + 'margin/accounts/balance': 0.2, # 借贷账户详情 + 'cross-margin/loan-orders': 1, # 查询借币订单 + 'cross-margin/accounts/balance': 1, # 借币账户详情 + 'points/actions': 1, + 'points/orders': 1, + 'subuser/aggregate-balance': 10, + 'stable-coin/exchange_rate': 1, + 'stable-coin/quote': 1, + }, + 'post': { + 'account/transfer': 1, # 资产划转(该节点为母用户和子用户进行资产划转的通用接口。) + 'futures/transfer': 1, + 'order/batch-orders': 0.4, + 'order/orders/place': 0.2, # 创建并执行一个新订单(一步下单, 推荐使用) + 'order/orders/submitCancelClientOrder': 0.2, + 'order/orders/batchCancelOpenOrders': 0.4, + # 'order/orders', # 创建一个新的订单请求 (仅创建订单,不执行下单) + # 'order/orders/{id}/place', # 执行一个订单 (仅执行已创建的订单) + 'order/orders/{id}/submitcancel': 0.2, # 申请撤销一个订单请求 + 'order/orders/batchcancel': 0.4, # 批量撤销订单 + # 'dw/balance/transfer', # 资产划转 + 'dw/withdraw/api/create': 1, # 申请提现虚拟币 + # 'dw/withdraw-virtual/create', # 申请提现虚拟币 + # 'dw/withdraw-virtual/{id}/place', # 确认申请虚拟币提现(Deprecated) + 'dw/withdraw-virtual/{id}/cancel': 1, # 申请取消提现虚拟币 + 'dw/transfer-in/margin': 10, # 现货账户划入至借贷账户 + 'dw/transfer-out/margin': 10, # 借贷账户划出至现货账户 + 'margin/orders': 10, # 申请借贷 + 'margin/orders/{id}/repay': 10, # 归还借贷 + 'cross-margin/transfer-in': 1, # 资产划转 + 'cross-margin/transfer-out': 1, # 资产划转 + 'cross-margin/orders': 1, # 申请借币 + 'cross-margin/orders/{id}/repay': 1, # 归还借币 + 'stable-coin/exchange': 1, + 'subuser/transfer': 10, + }, + }, + }, + 'fees': { + 'trading': { + 'feeSide': 'get', + 'tierBased': False, + 'percentage': True, + 'maker': self.parse_number('0.002'), + 'taker': self.parse_number('0.002'), + }, + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, # todo: implement + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': False, + 'FOK': False, + 'PO': False, + 'GTD': False, + }, + 'hedged': False, + 'selfTradePrevention': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 120, + 'untilDays': 2, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, # todo + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': None, + 'daysBack': None, # todo + 'untilDays': None, # todo + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': None, + 'daysBack': None, # todo + 'daysBackCanceled': None, # todo + 'untilDays': None, # todo + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 2000, + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'broad': { + 'contract is restricted of closing positions on API. Please contact customer service': OnMaintenance, + 'maintain': OnMaintenance, + }, + 'exact': { + # err-code + 'bad-request': BadRequest, + 'base-date-limit-error': BadRequest, # {"status":"error","err-code":"base-date-limit-error","err-msg":"date less than system limit","data":null} + 'api-not-support-temp-addr': PermissionDenied, # {"status":"error","err-code":"api-not-support-temp-addr","err-msg":"API withdrawal does not support temporary addresses","data":null} + 'timeout': RequestTimeout, # {"ts":1571653730865,"status":"error","err-code":"timeout","err-msg":"Request Timeout"} + 'gateway-internal-error': ExchangeNotAvailable, # {"status":"error","err-code":"gateway-internal-error","err-msg":"Failed to load data. Try again later.","data":null} + 'account-frozen-balance-insufficient-error': InsufficientFunds, # {"status":"error","err-code":"account-frozen-balance-insufficient-error","err-msg":"trade account balance is not enough, left: `0.0027`","data":null} + 'invalid-amount': InvalidOrder, # eg "Paramemter `amount` is invalid." + 'order-limitorder-amount-min-error': InvalidOrder, # limit order amount error, min: `0.001` + 'order-limitorder-amount-max-error': InvalidOrder, # market order amount error, max: `1000000` + 'order-marketorder-amount-min-error': InvalidOrder, # market order amount error, min: `0.01` + 'order-limitorder-price-min-error': InvalidOrder, # limit order price error + 'order-limitorder-price-max-error': InvalidOrder, # limit order price error + 'order-holding-limit-failed': InvalidOrder, # {"status":"error","err-code":"order-holding-limit-failed","err-msg":"Order failed, exceeded the holding limit of self currency","data":null} + 'order-orderprice-precision-error': InvalidOrder, # {"status":"error","err-code":"order-orderprice-precision-error","err-msg":"order price precision error, scale: `4`","data":null} + 'order-etp-nav-price-max-error': InvalidOrder, # {"status":"error","err-code":"order-etp-nav-price-max-error","err-msg":"Order price cannot be higher than 5% of NAV","data":null} + 'order-orderstate-error': OrderNotFound, # canceling an already canceled order + 'order-queryorder-invalid': OrderNotFound, # querying a non-existent order + 'order-update-error': ExchangeNotAvailable, # undocumented error + 'api-signature-check-failed': AuthenticationError, + 'api-signature-not-valid': AuthenticationError, # {"status":"error","err-code":"api-signature-not-valid","err-msg":"Signature not valid: Incorrect Access key [Access key错误]","data":null} + 'base-record-invalid': OrderNotFound, # https://github.com/ccxt/ccxt/issues/5750 + 'base-symbol-trade-disabled': BadSymbol, # {"status":"error","err-code":"base-symbol-trade-disabled","err-msg":"Trading is disabled for self symbol","data":null} + 'base-symbol-error': BadSymbol, # {"status":"error","err-code":"base-symbol-error","err-msg":"The symbol is invalid","data":null} + 'system-maintenance': OnMaintenance, # {"status": "error", "err-code": "system-maintenance", "err-msg": "System is in maintenance!", "data": null} + # err-msg + 'invalid symbol': BadSymbol, # {"ts":1568813334794,"status":"error","err-code":"invalid-parameter","err-msg":"invalid symbol"} + 'symbol trade not open now': BadSymbol, # {"ts":1576210479343,"status":"error","err-code":"invalid-parameter","err-msg":"symbol trade not open now"}, + 'invalid-address': BadRequest, # {"status":"error","err-code":"invalid-address","err-msg":"Invalid address.","data":null}, + 'base-currency-chain-error': BadRequest, # {"status":"error","err-code":"base-currency-chain-error","err-msg":"The current currency chain does not exist","data":null}, + 'dw-insufficient-balance': InsufficientFunds, # {"status":"error","err-code":"dw-insufficient-balance","err-msg":"Insufficient balance. You can only transfer `12.3456` at most.","data":null} + }, + }, + 'options': { + 'defaultNetwork': 'ERC20', + 'networks': { + 'ETH': 'erc20', + 'TRX': 'trc20', + 'HRC20': 'hrc20', + 'HECO': 'hrc20', + 'HT': 'hrc20', + 'ALGO': 'algo', + 'OMNI': '', + }, + # https://github.com/ccxt/ccxt/issues/5376 + 'fetchOrdersByStatesMethod': 'private_get_order_orders', # 'private_get_order_history' # https://github.com/ccxt/ccxt/pull/5392 + 'fetchOpenOrdersMethod': 'fetch_open_orders_v1', # 'fetch_open_orders_v2' # https://github.com/ccxt/ccxt/issues/5388 + 'createMarketBuyOrderRequiresPrice': True, + 'fetchMarketsMethod': 'publicGetCommonSymbols', + 'fetchBalanceMethod': 'privateGetAccountAccountsIdBalance', + 'createOrderMethod': 'privatePostOrderOrdersPlace', + 'currencyToPrecisionRoundingMode': TRUNCATE, + 'language': 'en-US', + 'broker': { + 'id': 'AA03022abc', + }, + }, + 'commonCurrencies': { + # https://github.com/ccxt/ccxt/issues/6081 + # https://github.com/ccxt/ccxt/issues/3365 + # https://github.com/ccxt/ccxt/issues/2873 + 'GET': 'Themis', # conflict with GET(Guaranteed Entrance Token, GET Protocol) + 'GTC': 'Game.com', # conflict with Gitcoin and Gastrocoin + 'HIT': 'HitChain', + # https://github.com/ccxt/ccxt/issues/7399 + # https://coinmarketcap.com/currencies/pnetwork/ + # https://coinmarketcap.com/currencies/penta/markets/ + # https://en.cryptonomist.ch/blog/eidoo/the-edo-to-pnt-upgrade-what-you-need-to-know-updated/ + 'PNT': 'Penta', + 'SBTC': 'Super Bitcoin', + 'BIFI': 'Bitcoin File', # conflict with Beefy.Finance https://github.com/ccxt/ccxt/issues/8706 + }, + }) + + async def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = await self.publicGetCommonTimestamp(params) + return self.safe_integer(response, 'data') + + async def fetch_trading_limits(self, symbols: Strings = None, params={}): + # self method should not be called directly, use loadTradingLimits() instead + # by default it will try load withdrawal fees of all currencies(with separate requests) + # however if you define symbols = ['ETH/BTC', 'LTC/BTC'] in args it will only load those + await self.load_markets() + if symbols is None: + symbols = self.symbols + result: dict = {} + for i in range(0, len(symbols)): + symbol = symbols[i] + result[symbol] = await self.fetch_trading_limits_by_id(self.market_id(symbol), params) + return result + + async def fetch_trading_limits_by_id(self, id: str, params={}): + request: dict = { + 'symbol': id, + } + response = await self.publicGetCommonExchange(self.extend(request, params)) + # + # {status: "ok", + # "data": { symbol: "aidocbtc", + # "buy-limit-must-less-than": 1.1, + # "sell-limit-must-greater-than": 0.9, + # "limit-order-must-greater-than": 1, + # "limit-order-must-less-than": 5000000, + # "market-buy-order-must-greater-than": 0.0001, + # "market-buy-order-must-less-than": 100, + # "market-sell-order-must-greater-than": 1, + # "market-sell-order-must-less-than": 500000, + # "circuit-break-when-greater-than": 10000, + # "circuit-break-when-less-than": 10, + # "market-sell-order-rate-must-less-than": 0.1, + # "market-buy-order-rate-must-less-than": 0.1 }} + # + return self.parse_trading_limits(self.safe_value(response, 'data', {})) + + def parse_trading_limits(self, limits, symbol: Str = None, params={}): + # + # { symbol: "aidocbtc", + # "buy-limit-must-less-than": 1.1, + # "sell-limit-must-greater-than": 0.9, + # "limit-order-must-greater-than": 1, + # "limit-order-must-less-than": 5000000, + # "market-buy-order-must-greater-than": 0.0001, + # "market-buy-order-must-less-than": 100, + # "market-sell-order-must-greater-than": 1, + # "market-sell-order-must-less-than": 500000, + # "circuit-break-when-greater-than": 10000, + # "circuit-break-when-less-than": 10, + # "market-sell-order-rate-must-less-than": 0.1, + # "market-buy-order-rate-must-less-than": 0.1 } + # + return { + 'info': limits, + 'limits': { + 'amount': { + 'min': self.safe_number(limits, 'limit-order-must-greater-than'), + 'max': self.safe_number(limits, 'limit-order-must-less-than'), + }, + }, + } + + def cost_to_precision(self, symbol, cost): + return self.decimal_to_precision(cost, TRUNCATE, self.markets[symbol]['precision']['cost'], self.precisionMode) + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for huobijp + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + method = self.options['fetchMarketsMethod'] + response = await getattr(self, method)(params) + # + # { + # "status": "ok", + # "data": [ + # { + # "base-currency": "xrp", + # "quote-currency": "btc", + # "price-precision": 9, + # "amount-precision": 2, + # "symbol-partition": "default", + # "symbol": "xrpbtc", + # "state": "online", + # "value-precision": 8, + # "min-order-amt": 1, + # "max-order-amt": 5000000, + # "min-order-value": 0.0001, + # "limit-order-min-order-amt": 1, + # "limit-order-max-order-amt": 5000000, + # "limit-order-max-buy-amt": 5000000, + # "limit-order-max-sell-amt": 5000000, + # "sell-market-min-order-amt": 1, + # "sell-market-max-order-amt": 500000, + # "buy-market-max-order-value": 100, + # "leverage-ratio": 5, + # "super-margin-leverage-ratio": 3, + # "api-trading": "enabled", + # "tags": "" + # } + # ... + # ] + # } + # + markets = self.safe_value(response, 'data', []) + numMarkets = len(markets) + if numMarkets < 1: + raise NetworkError(self.id + ' fetchMarkets() returned empty response: ' + self.json(markets)) + result = [] + for i in range(0, len(markets)): + market = markets[i] + baseId = self.safe_string(market, 'base-currency') + quoteId = self.safe_string(market, 'quote-currency') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + state = self.safe_string(market, 'state') + leverageRatio = self.safe_string(market, 'leverage-ratio', '1') + superLeverageRatio = self.safe_string(market, 'super-margin-leverage-ratio', '1') + margin = Precise.string_gt(leverageRatio, '1') or Precise.string_gt(superLeverageRatio, '1') + fee = self.parse_number('0') if (base == 'OMG') else self.parse_number('0.002') + result.append({ + 'id': baseId + quoteId, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': margin, + 'swap': False, + 'future': False, + 'option': False, + 'active': (state == 'online'), + 'contract': False, + 'linear': None, + 'inverse': None, + 'taker': fee, + 'maker': fee, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'price': self.parse_number(self.parse_precision(self.safe_string(market, 'price-precision'))), + 'amount': self.parse_number(self.parse_precision(self.safe_string(market, 'amount-precision'))), + 'cost': self.parse_number(self.parse_precision(self.safe_string(market, 'value-precision'))), + }, + 'limits': { + 'leverage': { + 'min': self.parse_number('1'), + 'max': self.parse_number(leverageRatio), + 'superMax': self.parse_number(superLeverageRatio), + }, + 'amount': { + 'min': self.safe_number(market, 'min-order-amt'), + 'max': self.safe_number(market, 'max-order-amt'), + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': self.safe_number(market, 'min-order-value'), + 'max': None, + }, + }, + 'created': None, + 'info': market, + }) + return result + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # fetchTicker + # + # { + # "amount": 26228.672978342216, + # "open": 9078.95, + # "close": 9146.86, + # "high": 9155.41, + # "id": 209988544334, + # "count": 265846, + # "low": 8988.0, + # "version": 209988544334, + # "ask": [9146.87, 0.156134], + # "vol": 2.3822168242201668E8, + # "bid": [9146.86, 0.080758], + # } + # + # fetchTickers + # { + # "symbol": "bhdht", + # "open": 2.3938, + # "high": 2.4151, + # "low": 2.3323, + # "close": 2.3909, + # "amount": 628.992, + # "vol": 1493.71841095, + # "count": 2088, + # "bid": 2.3643, + # "bidSize": 0.7136, + # "ask": 2.4061, + # "askSize": 0.4156 + # } + # + symbol = self.safe_symbol(None, market) + timestamp = self.safe_integer(ticker, 'ts') + bid = None + bidVolume = None + ask = None + askVolume = None + if 'bid' in ticker: + if isinstance(ticker['bid'], list): + bid = self.safe_string(ticker['bid'], 0) + bidVolume = self.safe_string(ticker['bid'], 1) + else: + bid = self.safe_string(ticker, 'bid') + bidVolume = self.safe_string(ticker, 'bidSize') + if 'ask' in ticker: + if isinstance(ticker['ask'], list): + ask = self.safe_string(ticker['ask'], 0) + askVolume = self.safe_string(ticker['ask'], 1) + else: + ask = self.safe_string(ticker, 'ask') + askVolume = self.safe_string(ticker, 'askSize') + open = self.safe_string(ticker, 'open') + close = self.safe_string(ticker, 'close') + baseVolume = self.safe_string(ticker, 'amount') + quoteVolume = self.safe_string(ticker, 'vol') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': bid, + 'bidVolume': bidVolume, + 'ask': ask, + 'askVolume': askVolume, + 'vwap': None, + 'open': open, + 'close': close, + 'last': close, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'info': ticker, + }, market) + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'type': 'step0', + } + response = await self.marketGetDepth(self.extend(request, params)) + # + # { + # "status": "ok", + # "ch": "market.btcusdt.depth.step0", + # "ts": 1583474832790, + # "tick": { + # "bids": [ + # [9100.290000000000000000, 0.200000000000000000], + # [9099.820000000000000000, 0.200000000000000000], + # [9099.610000000000000000, 0.205000000000000000], + # ], + # "asks": [ + # [9100.640000000000000000, 0.005904000000000000], + # [9101.010000000000000000, 0.287311000000000000], + # [9101.030000000000000000, 0.012121000000000000], + # ], + # "ts":1583474832008, + # "version":104999698780 + # } + # } + # + if 'tick' in response: + if not response['tick']: + raise BadSymbol(self.id + ' fetchOrderBook() returned empty response: ' + self.json(response)) + tick = self.safe_value(response, 'tick') + timestamp = self.safe_integer(tick, 'ts', self.safe_integer(response, 'ts')) + result = self.parse_order_book(tick, symbol, timestamp) + result['nonce'] = self.safe_integer(tick, 'version') + return result + raise ExchangeError(self.id + ' fetchOrderBook() returned unrecognized response: ' + self.json(response)) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = await self.marketGetDetailMerged(self.extend(request, params)) + # + # { + # "status": "ok", + # "ch": "market.btcusdt.detail.merged", + # "ts": 1583494336669, + # "tick": { + # "amount": 26228.672978342216, + # "open": 9078.95, + # "close": 9146.86, + # "high": 9155.41, + # "id": 209988544334, + # "count": 265846, + # "low": 8988.0, + # "version": 209988544334, + # "ask": [9146.87, 0.156134], + # "vol": 2.3822168242201668E8, + # "bid": [9146.86, 0.080758], + # } + # } + # + ticker = self.parse_ticker(response['tick'], market) + timestamp = self.safe_integer(response, 'ts') + ticker['timestamp'] = timestamp + ticker['datetime'] = self.iso8601(timestamp) + return ticker + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + response = await self.marketGetTickers(params) + tickers = self.safe_value(response, 'data', []) + timestamp = self.safe_integer(response, 'ts') + result: dict = {} + for i in range(0, len(tickers)): + marketId = self.safe_string(tickers[i], 'symbol') + market = self.safe_market(marketId) + symbol = market['symbol'] + ticker = self.parse_ticker(tickers[i], market) + ticker['timestamp'] = timestamp + ticker['datetime'] = self.iso8601(timestamp) + result[symbol] = ticker + return self.filter_by_array_tickers(result, 'symbol', symbols) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(public) + # + # { + # "amount": 0.010411000000000000, + # "trade-id": 102090736910, + # "ts": 1583497692182, + # "id": 10500517034273194594947, + # "price": 9096.050000000000000000, + # "direction": "sell" + # } + # + # fetchMyTrades(private) + # + # { + # "symbol": "swftcbtc", + # "fee-currency": "swftc", + # "filled-fees": "0", + # "source": "spot-api", + # "id": 83789509854000, + # "type": "buy-limit", + # "order-id": 83711103204909, + # 'filled-points': "0.005826843283532154", + # "fee-deduct-currency": "ht", + # 'filled-amount': "45941.53", + # "price": "0.0000001401", + # "created-at": 1597933260729, + # "match-id": 100087455560, + # "role": "maker", + # "trade-id": 100050305348 + # }, + # + marketId = self.safe_string(trade, 'symbol') + symbol = self.safe_symbol(marketId, market) + timestamp = self.safe_integer_2(trade, 'ts', 'created-at') + order = self.safe_string(trade, 'order-id') + side = self.safe_string(trade, 'direction') + type = self.safe_string(trade, 'type') + if type is not None: + typeParts = type.split('-') + side = typeParts[0] + type = typeParts[1] + takerOrMaker = self.safe_string(trade, 'role') + price = self.safe_string(trade, 'price') + amount = self.safe_string_2(trade, 'filled-amount', 'amount') + cost = Precise.string_mul(price, amount) + fee = None + feeCost = self.safe_string(trade, 'filled-fees') + feeCurrency = self.safe_currency_code(self.safe_string(trade, 'fee-currency')) + filledPoints = self.safe_string(trade, 'filled-points') + if filledPoints is not None: + if (feeCost is None) or (Precise.string_eq(feeCost, '0.0')): + feeCost = filledPoints + feeCurrency = self.safe_currency_code(self.safe_string(trade, 'fee-deduct-currency')) + if feeCost is not None: + fee = { + 'cost': feeCost, + 'currency': feeCurrency, + } + tradeId = self.safe_string_2(trade, 'trade-id', 'tradeId') + id = self.safe_string(trade, 'id', tradeId) + return self.safe_trade({ + 'info': trade, + 'id': id, + 'symbol': symbol, + 'order': order, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'type': type, + 'side': side, + 'takerOrMaker': takerOrMaker, + 'price': price, + 'amount': amount, + 'cost': cost, + 'fee': fee, + }) + + async def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all the trades made from a single order + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + request: dict = { + 'id': id, + } + response = await self.privateGetOrderOrdersIdMatchresults(self.extend(request, params)) + return self.parse_trades(response['data'], None, since, limit) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = None + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if limit is not None: + request['size'] = limit # 1-100 orders, default is 100 + if since is not None: + request['start-time'] = since # a date within 120 days from today + # request['end-time'] = self.sum(since, 172800000) # 48 hours window + response = await self.privateGetOrderMatchresults(self.extend(request, params)) + return self.parse_trades(response['data'], market, since, limit) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = 1000, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['size'] = min(limit, 2000) + response = await self.marketGetHistoryTrade(self.extend(request, params)) + # + # { + # "status": "ok", + # "ch": "market.btcusdt.trade.detail", + # "ts": 1583497692365, + # "data": [ + # { + # "id": 105005170342, + # "ts": 1583497692182, + # "data": [ + # { + # "amount": 0.010411000000000000, + # "trade-id": 102090736910, + # "ts": 1583497692182, + # "id": 10500517034273194594947, + # "price": 9096.050000000000000000, + # "direction": "sell" + # } + # ] + # }, + # # ... + # ] + # } + # + data = self.safe_value(response, 'data', []) + result = [] + for i in range(0, len(data)): + trades = self.safe_value(data[i], 'data', []) + for j in range(0, len(trades)): + trade = self.parse_trade(trades[j], market) + result.append(trade) + result = self.sort_by(result, 'timestamp') + return self.filter_by_symbol_since_limit(result, market['symbol'], since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # { + # "amount":1.2082, + # "open":0.025096, + # "close":0.025095, + # "high":0.025096, + # "id":1591515300, + # "count":6, + # "low":0.025095, + # "vol":0.0303205097 + # } + # + return [ + self.safe_timestamp(ohlcv, 'id'), + self.safe_number(ohlcv, 'open'), + self.safe_number(ohlcv, 'high'), + self.safe_number(ohlcv, 'low'), + self.safe_number(ohlcv, 'close'), + self.safe_number(ohlcv, 'amount'), + ] + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = 1000, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'period': self.safe_string(self.timeframes, timeframe, timeframe), + } + if limit is not None: + request['size'] = min(limit, 2000) + response = await self.marketGetHistoryKline(self.extend(request, params)) + # + # { + # "status":"ok", + # "ch":"market.ethbtc.kline.1min", + # "ts":1591515374371, + # "data":[ + # {"amount":0.0,"open":0.025095,"close":0.025095,"high":0.025095,"id":1591515360,"count":0,"low":0.025095,"vol":0.0}, + # {"amount":1.2082,"open":0.025096,"close":0.025095,"high":0.025096,"id":1591515300,"count":6,"low":0.025095,"vol":0.0303205097}, + # {"amount":0.0648,"open":0.025096,"close":0.025096,"high":0.025096,"id":1591515240,"count":2,"low":0.025096,"vol":0.0016262208}, + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_ohlcvs(data, market, timeframe, since, limit) + + async def fetch_accounts(self, params={}) -> List[Account]: + """ + fetch all the accounts associated with a profile + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `account structures ` indexed by the account type + """ + await self.load_markets() + response = await self.privateGetAccountAccounts(params) + return response['data'] + + async def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + request: dict = { + 'language': self.options['language'], + } + response = await self.publicGetSettingsCurrencys(self.extend(request, params)) + # + # { + # "status":"ok", + # "data":[ + # { + # "currency-addr-with-tag":false, + # "fast-confirms":12, + # "safe-confirms":12, + # "currency-type":"eth", + # "quote-currency":true, + # "withdraw-enable-timestamp":1609430400000, + # "deposit-enable-timestamp":1609430400000, + # "currency-partition":"all", + # "support-sites":["OTC","INSTITUTION","MINEPOOL"], + # "withdraw-precision":6, + # "visible-assets-timestamp":1508839200000, + # "deposit-min-amount":"1", + # "withdraw-min-amount":"10", + # "show-precision":"8", + # "tags":"", + # "weight":23, + # "full-name":"Tether USDT", + # "otc-enable":1, + # "visible":true, + # "white-enabled":false, + # "country-disabled":false, + # "deposit-enabled":true, + # "withdraw-enabled":true, + # "name":"usdt", + # "state":"online", + # "display-name":"USDT", + # "suspend-withdraw-desc":null, + # "withdraw-desc":"Minimum withdrawal amount: 10 USDT(ERC20). not >______ Balances: + balances = self.safe_value(response['data'], 'list', []) + result: dict = {'info': response} + for i in range(0, len(balances)): + balance = balances[i] + currencyId = self.safe_string(balance, 'currency') + code = self.safe_currency_code(currencyId) + account = None + if code in result: + account = result[code] + else: + account = self.account() + if balance['type'] == 'trade': + account['free'] = self.safe_string(balance, 'balance') + if balance['type'] == 'frozen': + account['used'] = self.safe_string(balance, 'balance') + result[code] = account + return self.safe_balance(result) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + await self.load_accounts() + method = self.options['fetchBalanceMethod'] + request: dict = { + 'id': self.accounts[0]['id'], + } + response = await getattr(self, method)(self.extend(request, params)) + return self.parse_balance(response) + + async def fetch_orders_by_states(self, states, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + await self.load_markets() + request: dict = { + 'states': states, + } + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + method = self.safe_string(self.options, 'fetchOrdersByStatesMethod', 'private_get_order_orders') + response = await getattr(self, method)(self.extend(request, params)) + # + # {"status": "ok", + # "data": [{ id: 13997833014, + # "symbol": "ethbtc", + # "account-id": 3398321, + # "amount": "0.045000000000000000", + # "price": "0.034014000000000000", + # "created-at": 1545836976871, + # "type": "sell-limit", + # "field-amount": "0.045000000000000000", + # "field-cash-amount": "0.001530630000000000", + # "field-fees": "0.000003061260000000", + # "finished-at": 1545837948214, + # "source": "spot-api", + # "state": "filled", + # "canceled-at": 0 } ]} + # + return self.parse_orders(response['data'], market, since, limit) + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + request: dict = { + 'id': id, + } + response = await self.privateGetOrderOrdersId(self.extend(request, params)) + order = self.safe_dict(response, 'data') + return self.parse_order(order) + + async def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + return await self.fetch_orders_by_states('pre-submitted,submitted,partial-filled,filled,partial-canceled,canceled', symbol, since, limit, params) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + method = self.safe_string(self.options, 'fetchOpenOrdersMethod', 'fetch_open_orders_v1') + return await getattr(self, method)(symbol, since, limit, params) + + async def fetch_open_orders_v1(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOpenOrdersV1() requires a symbol argument') + return await self.fetch_orders_by_states('pre-submitted,submitted,partial-filled', symbol, since, limit, params) + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + return await self.fetch_orders_by_states('filled,partial-canceled,canceled', symbol, since, limit, params) + + async def fetch_open_orders_v2(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + await self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + accountId = self.safe_string(params, 'account-id') + if accountId is None: + # pick the first account + await self.load_accounts() + for i in range(0, len(self.accounts)): + account = self.accounts[i] + if account['type'] == 'spot': + accountId = self.safe_string(account, 'id') + if accountId is not None: + break + request['account-id'] = accountId + if limit is not None: + request['size'] = limit + omitted = self.omit(params, 'account-id') + response = await self.privateGetOrderOpenOrders(self.extend(request, omitted)) + # + # { + # "status":"ok", + # "data":[ + # { + # "symbol":"ethusdt", + # "source":"api", + # "amount":"0.010000000000000000", + # "account-id":1528640, + # "created-at":1561597491963, + # "price":"400.000000000000000000", + # "filled-amount":"0.0", + # "filled-cash-amount":"0.0", + # "filled-fees":"0.0", + # "id":38477101630, + # "state":"submitted", + # "type":"sell-limit" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_orders(data, market, since, limit) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'partial-filled': 'open', + 'partial-canceled': 'canceled', + 'filled': 'closed', + 'canceled': 'canceled', + 'submitted': 'open', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # { id: 13997833014, + # "symbol": "ethbtc", + # "account-id": 3398321, + # "amount": "0.045000000000000000", + # "price": "0.034014000000000000", + # "created-at": 1545836976871, + # "type": "sell-limit", + # "field-amount": "0.045000000000000000", # they have fixed it for filled-amount + # "field-cash-amount": "0.001530630000000000", # they have fixed it for filled-cash-amount + # "field-fees": "0.000003061260000000", # they have fixed it for filled-fees + # "finished-at": 1545837948214, + # "source": "spot-api", + # "state": "filled", + # "canceled-at": 0 } + # + # { id: 20395337822, + # "symbol": "ethbtc", + # "account-id": 5685075, + # "amount": "0.001000000000000000", + # "price": "0.0", + # "created-at": 1545831584023, + # "type": "buy-market", + # "field-amount": "0.029100000000000000", # they have fixed it for filled-amount + # "field-cash-amount": "0.000999788700000000", # they have fixed it for filled-cash-amount + # "field-fees": "0.000058200000000000", # they have fixed it for filled-fees + # "finished-at": 1545831584181, + # "source": "spot-api", + # "state": "filled", + # "canceled-at": 0 } + # + id = self.safe_string(order, 'id') + side = None + type = None + status = None + if 'type' in order: + orderType = order['type'].split('-') + side = orderType[0] + type = orderType[1] + status = self.parse_order_status(self.safe_string(order, 'state')) + marketId = self.safe_string(order, 'symbol') + market = self.safe_market(marketId, market) + timestamp = self.safe_integer(order, 'created-at') + clientOrderId = self.safe_string(order, 'client-order-id') + amount = self.safe_string(order, 'amount') + filled = self.safe_string_2(order, 'filled-amount', 'field-amount') # typo in their API, filled amount + price = self.safe_string(order, 'price') + cost = self.safe_string_2(order, 'filled-cash-amount', 'field-cash-amount') # same typo + feeCost = self.safe_string_2(order, 'filled-fees', 'field-fees') # typo in their API, filled fees + fee = None + if feeCost is not None: + feeCurrency = market['quote'] if (side == 'sell') else market['base'] + fee = { + 'cost': feeCost, + 'currency': feeCurrency, + } + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'symbol': market['symbol'], + 'type': type, + 'timeInForce': None, + 'postOnly': None, + 'side': side, + 'price': price, + 'triggerPrice': None, + 'average': None, + 'cost': cost, + 'amount': amount, + 'filled': filled, + 'remaining': None, + 'status': status, + 'fee': fee, + 'trades': None, + }, market) + + async def create_market_buy_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market buy order by providing the symbol and cost + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' createMarketBuyOrderWithCost() supports spot orders only') + params['createMarketBuyOrderRequiresPrice'] = False + return await self.create_order(symbol, 'market', 'buy', cost, None, params) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + await self.load_accounts() + market = self.market(symbol) + request: dict = { + 'account-id': self.accounts[0]['id'], + 'symbol': market['id'], + 'type': side + '-' + type, + } + clientOrderId = self.safe_string_2(params, 'clientOrderId', 'client-order-id') # must be 64 chars max and unique within 24 hours + if clientOrderId is None: + broker = self.safe_value(self.options, 'broker', {}) + brokerId = self.safe_string(broker, 'id') + request['client-order-id'] = brokerId + self.uuid() + else: + request['client-order-id'] = clientOrderId + params = self.omit(params, ['clientOrderId', 'client-order-id']) + if (type == 'market') and (side == 'buy'): + quoteAmount = None + createMarketBuyOrderRequiresPrice = True + createMarketBuyOrderRequiresPrice, params = self.handle_option_and_params(params, 'createOrder', 'createMarketBuyOrderRequiresPrice', True) + cost = self.safe_number(params, 'cost') + params = self.omit(params, 'cost') + if cost is not None: + quoteAmount = self.amount_to_precision(symbol, cost) + elif createMarketBuyOrderRequiresPrice: + if price is None: + raise InvalidOrder(self.id + ' createOrder() requires the price argument for market buy orders to calculate the total cost to spend(amount * price), alternatively set the createMarketBuyOrderRequiresPrice option or param to False and pass the cost to spend in the amount argument') + else: + # despite that cost = amount * price is in quote currency and should have quote precision + # the exchange API requires the cost supplied in 'amount' to be of base precision + # more about it here: + # https://github.com/ccxt/ccxt/pull/4395 + # https://github.com/ccxt/ccxt/issues/7611 + # we use amountToPrecision here because the exchange requires cost in base precision + amountString = self.number_to_string(amount) + priceString = self.number_to_string(price) + quoteAmount = self.amount_to_precision(symbol, Precise.string_mul(amountString, priceString)) + else: + quoteAmount = self.amount_to_precision(symbol, amount) + request['amount'] = quoteAmount + else: + request['amount'] = self.amount_to_precision(symbol, amount) + if type == 'limit' or type == 'ioc' or type == 'limit-maker' or type == 'stop-limit' or type == 'stop-limit-fok': + request['price'] = self.price_to_precision(symbol, price) + method = self.options['createOrderMethod'] + response = await getattr(self, method)(self.extend(request, params)) + id = self.safe_string(response, 'data') + return self.safe_order({ + 'info': response, + 'id': id, + 'timestamp': None, + 'datetime': None, + 'lastTradeTimestamp': None, + 'status': None, + 'symbol': symbol, + 'type': type, + 'side': side, + 'price': price, + 'amount': amount, + 'filled': None, + 'remaining': None, + 'cost': None, + 'trades': None, + 'fee': None, + 'clientOrderId': None, + 'average': None, + }, market) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + :param str id: order id + :param str symbol: not used by bittrade cancelOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + response = await self.privatePostOrderOrdersIdSubmitcancel({'id': id}) + # + # { + # "status": "ok", + # "data": "10138899000", + # } + # + return self.extend(self.parse_order(response), { + 'id': id, + 'status': 'canceled', + }) + + async def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): + """ + cancel multiple orders + :param str[] ids: order ids + :param str symbol: not used by bittrade cancelOrders() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an list of `order structures ` + """ + await self.load_markets() + clientOrderIds = self.safe_value_2(params, 'clientOrderIds', 'client-order-ids') + params = self.omit(params, ['clientOrderIds', 'client-order-ids']) + request: dict = {} + if clientOrderIds is None: + request['order-ids'] = ids + else: + request['client-order-ids'] = clientOrderIds + response = await self.privatePostOrderOrdersBatchcancel(self.extend(request, params)) + # + # { + # "status": "ok", + # "data": { + # "success": [ + # "5983466" + # ], + # "failed": [ + # { + # "err-msg": "Incorrect order state", + # "order-state": 7, + # "order-id": "", + # "err-code": "order-orderstate-error", + # "client-order-id": "first" + # }, + # { + # "err-msg": "Incorrect order state", + # "order-state": 7, + # "order-id": "", + # "err-code": "order-orderstate-error", + # "client-order-id": "second" + # }, + # { + # "err-msg": "The record is not found.", + # "order-id": "", + # "err-code": "base-not-found", + # "client-order-id": "third" + # } + # ] + # } + # } + # + return self.parse_cancel_orders(response) + + def parse_cancel_orders(self, orders): + # + # { + # "success": [ + # "5983466" + # ], + # "failed": [ + # { + # "err-msg": "Incorrect order state", + # "order-state": 7, + # "order-id": "", + # "err-code": "order-orderstate-error", + # "client-order-id": "first" + # }, + # ... + # ] + # } + # + # { + # "errors": [ + # { + # "order_id": "769206471845261312", + # "err_code": 1061, + # "err_msg": "This order doesnt exist." + # } + # ], + # "successes": "1258075374411399168,1258075393254871040" + # } + # + successes = self.safe_string(orders, 'successes') + success = None + if successes is not None: + success = successes.split(',') + else: + success = self.safe_list(orders, 'success', []) + failed = self.safe_list_2(orders, 'errors', 'failed', []) + result = [] + for i in range(0, len(success)): + order = success[i] + result.append(self.safe_order({ + 'info': order, + 'id': order, + 'status': 'canceled', + })) + for i in range(0, len(failed)): + order = failed[i] + result.append(self.safe_order({ + 'info': order, + 'id': self.safe_string_2(order, 'order-id', 'order_id'), + 'status': 'failed', + 'clientOrderId': self.safe_string(order, 'client-order-id'), + })) + return result + + async def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders + :param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + request: dict = { + # 'account-id' string False NA The account id used for self cancel Refer to GET /v1/account/accounts + # 'symbol': market['id'], # a list of comma-separated symbols, all symbols by default + # 'types' 'string', buy-market, sell-market, buy-limit, sell-limit, buy-ioc, sell-ioc, buy-stop-limit, sell-stop-limit, buy-limit-fok, sell-limit-fok, buy-stop-limit-fok, sell-stop-limit-fok + # 'side': 'buy', # or 'sell' + # 'size': 100, # the number of orders to cancel 1-100 + } + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + response = await self.privatePostOrderOrdersBatchCancelOpenOrders(self.extend(request, params)) + # + # { + # "code": 200, + # "data": { + # "success-count": 2, + # "failed-count": 0, + # "next-id": 5454600 + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return [ + self.safe_order({ + 'info': data, + }), + ] + + def parse_deposit_address(self, depositAddress, currency: Currency = None): + # + # { + # "currency": "usdt", + # "address": "0xf7292eb9ba7bc50358e27f0e025a4d225a64127b", + # "addressTag": "", + # "chain": "usdterc20", # trc20usdt, hrc20usdt, usdt, algousdt + # } + # + address = self.safe_string(depositAddress, 'address') + tag = self.safe_string(depositAddress, 'addressTag') + currencyId = self.safe_string(depositAddress, 'currency') + currency = self.safe_currency(currencyId, currency) + code = self.safe_currency_code(currencyId, currency) + networkId = self.safe_string(depositAddress, 'chain') + networks = self.safe_value(currency, 'networks', {}) + networksById = self.index_by(networks, 'id') + networkValue = self.safe_value(networksById, networkId, networkId) + network = self.safe_string(networkValue, 'network') + self.check_address(address) + return { + 'currency': code, + 'address': address, + 'tag': tag, + 'network': network, + 'info': depositAddress, + } + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + if limit is None or limit > 100: + limit = 100 + await self.load_markets() + currency = None + if code is not None: + currency = self.currency(code) + request: dict = { + 'type': 'deposit', + 'from': 0, # From 'id' ... if you want to get results after a particular transaction id, pass the id in params.from + } + if currency is not None: + request['currency'] = currency['id'] + if limit is not None: + request['size'] = limit # max 100 + response = await self.privateGetQueryDepositWithdraw(self.extend(request, params)) + # return response + return self.parse_transactions(response['data'], currency, since, limit) + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + if limit is None or limit > 100: + limit = 100 + await self.load_markets() + currency = None + if code is not None: + currency = self.currency(code) + request: dict = { + 'type': 'withdraw', + 'from': 0, # From 'id' ... if you want to get results after a particular transaction id, pass the id in params.from + } + if currency is not None: + request['currency'] = currency['id'] + if limit is not None: + request['size'] = limit # max 100 + response = await self.privateGetQueryDepositWithdraw(self.extend(request, params)) + # return response + return self.parse_transactions(response['data'], currency, since, limit) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fetchDeposits + # + # { + # "id": 8211029, + # "type": "deposit", + # "currency": "eth", + # "chain": "eth", + # 'tx-hash': "bd315....", + # "amount": 0.81162421, + # "address": "4b8b....", + # 'address-tag": '", + # "fee": 0, + # "state": "safe", + # "created-at": 1542180380965, + # "updated-at": 1542180788077 + # } + # + # fetchWithdrawals + # + # { + # "id": 6908275, + # "type": "withdraw", + # "currency": "btc", + # "chain": "btc", + # 'tx-hash': "c1a1a....", + # "amount": 0.80257005, + # "address": "1QR....", + # 'address-tag": '", + # "fee": 0.0005, + # "state": "confirmed", + # "created-at": 1552107295685, + # "updated-at": 1552108032859 + # } + # + # withdraw + # + # { + # "status": "ok", + # "data": "99562054" + # } + # + timestamp = self.safe_integer(transaction, 'created-at') + code = self.safe_currency_code(self.safe_string(transaction, 'currency')) + type = self.safe_string(transaction, 'type') + if type == 'withdraw': + type = 'withdrawal' + feeCost = self.safe_string(transaction, 'fee') + if feeCost is not None: + feeCost = Precise.string_abs(feeCost) + return { + 'info': transaction, + 'id': self.safe_string_2(transaction, 'id', 'data'), + 'txid': self.safe_string(transaction, 'tx-hash'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': self.safe_string_upper(transaction, 'chain'), + 'address': self.safe_string(transaction, 'address'), + 'addressTo': None, + 'addressFrom': None, + 'tag': self.safe_string(transaction, 'address-tag'), + 'tagTo': None, + 'tagFrom': None, + 'type': type, + 'amount': self.safe_number(transaction, 'amount'), + 'currency': code, + 'status': self.parse_transaction_status(self.safe_string(transaction, 'state')), + 'updated': self.safe_integer(transaction, 'updated-at'), + 'comment': None, + 'internal': None, + 'fee': { + 'currency': code, + 'cost': self.parse_number(feeCost), + 'rate': None, + }, + } + + def parse_transaction_status(self, status: Str): + statuses: dict = { + # deposit statuses + 'unknown': 'failed', + 'confirming': 'pending', + 'confirmed': 'ok', + 'safe': 'ok', + 'orphan': 'failed', + # withdrawal statuses + 'submitted': 'pending', + 'canceled': 'canceled', + 'reexamine': 'pending', + 'reject': 'failed', + 'pass': 'pending', + 'wallet-reject': 'failed', + # 'confirmed': 'ok', # present in deposit statuses + 'confirm-error': 'failed', + 'repealed': 'failed', + 'wallet-transfer': 'pending', + 'pre-transfer': 'pending', + } + return self.safe_string(statuses, status, status) + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + await self.load_markets() + self.check_address(address) + currency = self.currency(code) + request: dict = { + 'address': address, # only supports existing addresses in your withdraw address list + 'amount': amount, + 'currency': currency['id'].lower(), + } + if tag is not None: + request['addr-tag'] = tag # only for XRP? + networks = self.safe_value(self.options, 'networks', {}) + network = self.safe_string_upper(params, 'network') # self line allows the user to specify either ERC20 or ETH + network = self.safe_string_lower(networks, network, network) # handle ETH>ERC20 alias + if network is not None: + # possible chains - usdterc20, trc20usdt, hrc20usdt, usdt, algousdt + if network == 'erc20': + request['chain'] = currency['id'] + network + else: + request['chain'] = network + currency['id'] + params = self.omit(params, 'network') + response = await self.privatePostDwWithdrawApiCreate(self.extend(request, params)) + # + # { + # "status": "ok", + # "data": "99562054" + # } + # + return self.parse_transaction(response, currency) + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = '/' + if api == 'market': + url += api + elif (api == 'public') or (api == 'private'): + url += self.version + elif (api == 'v2Public') or (api == 'v2Private'): + url += 'v2' + url += '/' + self.implode_params(path, params) + query = self.omit(params, self.extract_params(path)) + if api == 'private' or api == 'v2Private': + self.check_required_credentials() + timestamp = self.ymdhms(self.milliseconds(), 'T') + request: dict = { + 'SignatureMethod': 'HmacSHA256', + 'SignatureVersion': '2', + 'AccessKeyId': self.apiKey, + 'Timestamp': timestamp, + } + if method != 'POST': + request = self.extend(request, query) + requestSorted = self.keysort(request) + auth = self.urlencode(requestSorted) + # unfortunately, PHP demands double quotes for the escaped newline symbol + # eslint-disable-next-line quotes + payload = "\n".join([method, self.hostname, url, auth]) + signature = self.hmac(self.encode(payload), self.encode(self.secret), hashlib.sha256, 'base64') + auth += '&' + self.urlencode({'Signature': signature}) + url += '?' + auth + if method == 'POST': + body = self.json(query) + headers = { + 'Content-Type': 'application/json', + } + else: + headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + } + else: + if params: + url += '?' + self.urlencode(params) + url = self.implode_params(self.urls['api'][api], { + 'hostname': self.hostname, + }) + url + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None # fallback to default error handler + if 'status' in response: + # + # {"status":"error","err-code":"order-limitorder-amount-min-error","err-msg":"limit order amount error, min: `0.001`","data":null} + # + status = self.safe_string(response, 'status') + if status == 'error': + code = self.safe_string(response, 'err-code') + feedback = self.id + ' ' + body + self.throw_broadly_matched_exception(self.exceptions['broad'], body, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], code, feedback) + message = self.safe_string(response, 'err-msg') + self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) + raise ExchangeError(feedback) + return None diff --git a/ccxt/async_support/bitvavo.py b/ccxt/async_support/bitvavo.py new file mode 100644 index 0000000..6a96c13 --- /dev/null +++ b/ccxt/async_support/bitvavo.py @@ -0,0 +1,2134 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.bitvavo import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currencies, Currency, DepositAddress, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, TradingFees, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import AccountSuspended +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidAddress +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.errors import OnMaintenance +from ccxt.base.decimal_to_precision import TRUNCATE +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class bitvavo(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(bitvavo, self).describe(), { + 'id': 'bitvavo', + 'name': 'Bitvavo', + 'countries': ['NL'], # Netherlands + 'rateLimit': 60, # 1000 requests per minute + 'version': 'v2', + 'certified': False, + 'pro': True, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createOrder': True, + 'createOrderWithTakeProfitAndStopLoss': False, + 'createOrderWithTakeProfitAndStopLossWs': False, + 'createPostOnlyOrder': False, + 'createReduceOnlyOrder': False, + 'createStopLimitOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'editOrder': True, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchDepositWithdrawFee': 'emulated', + 'fetchDepositWithdrawFees': True, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': True, + 'fetchTransfer': False, + 'fetchTransfers': False, + 'fetchVolatilityHistory': False, + 'fetchWithdrawals': True, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'repayMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'transfer': False, + 'withdraw': True, + }, + 'timeframes': { + '1m': '1m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1h', + '2h': '2h', + '4h': '4h', + '6h': '6h', + '8h': '8h', + '12h': '12h', + '1d': '1d', + }, + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/d213155c-8c71-4701-9bd5-45351febc2a8', + 'api': { + 'public': 'https://api.bitvavo.com', + 'private': 'https://api.bitvavo.com', + }, + 'www': 'https://bitvavo.com/', + 'doc': 'https://docs.bitvavo.com/', + 'fees': 'https://bitvavo.com/en/fees', + 'referral': 'https://bitvavo.com/?a=24F34952F7', + }, + 'api': { + 'public': { + 'get': { + 'time': 1, + 'markets': 1, + 'assets': 1, + '{market}/book': 1, + '{market}/trades': 5, + '{market}/candles': 1, + 'ticker/price': 1, + 'ticker/book': 1, + 'ticker/24h': {'cost': 1, 'noMarket': 25}, + }, + }, + 'private': { + 'get': { + 'account': 1, + 'order': 1, + 'orders': 5, + 'ordersOpen': {'cost': 1, 'noMarket': 25}, + 'trades': 5, + 'balance': 5, + 'deposit': 1, + 'depositHistory': 5, + 'withdrawalHistory': 5, + }, + 'post': { + 'order': 1, + 'withdrawal': 1, + }, + 'put': { + 'order': 1, + }, + 'delete': { + 'order': 1, + 'orders': 1, + }, + }, + }, + 'fees': { + 'trading': { + 'tierBased': True, + 'percentage': True, + 'taker': self.parse_number('0.0025'), + 'maker': self.parse_number('0.002'), + 'tiers': { + 'taker': [ + [self.parse_number('0'), self.parse_number('0.0025')], + [self.parse_number('100000'), self.parse_number('0.0020')], + [self.parse_number('250000'), self.parse_number('0.0016')], + [self.parse_number('500000'), self.parse_number('0.0012')], + [self.parse_number('1000000'), self.parse_number('0.0010')], + [self.parse_number('2500000'), self.parse_number('0.0008')], + [self.parse_number('5000000'), self.parse_number('0.0006')], + [self.parse_number('10000000'), self.parse_number('0.0005')], + [self.parse_number('25000000'), self.parse_number('0.0004')], + ], + 'maker': [ + [self.parse_number('0'), self.parse_number('0.0015')], + [self.parse_number('100000'), self.parse_number('0.0010')], + [self.parse_number('250000'), self.parse_number('0.0008')], + [self.parse_number('500000'), self.parse_number('0.0006')], + [self.parse_number('1000000'), self.parse_number('0.0005')], + [self.parse_number('2500000'), self.parse_number('0.0004')], + [self.parse_number('5000000'), self.parse_number('0.0004')], + [self.parse_number('10000000'), self.parse_number('0.0003')], + [self.parse_number('25000000'), self.parse_number('0.0003')], + ], + }, + }, + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerPriceType': None, + 'triggerDirection': None, + 'stopLossPrice': True, + 'takeProfitPrice': True, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyRequiresPrice': False, + 'marketBuyByCost': True, + 'selfTradePrevention': { + 'EXPIRE_MAKER': False, + 'EXPIRE_TAKER': False, + 'EXPIRE_BOTH': True, + 'NONE': False, + }, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 100000, + 'untilDays': 100000, + 'symbolRequired': True, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOrders': { + 'marginMode': True, + 'limit': 1000, + 'daysBack': 100000, + 'untilDays': 100000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchClosedOrders': None, + 'fetchOHLCV': { + 'limit': 1440, + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'exceptions': { + 'exact': { + '101': ExchangeError, # Unknown error. Operation may or may not have succeeded. + '102': BadRequest, # Invalid JSON. + '103': RateLimitExceeded, # You have been rate limited. Please observe the Bitvavo-Ratelimit-AllowAt header to see when you can send requests again. Failure to respect self limit will result in an IP ban. The default value is 1000 weighted requests per minute. Please contact support if you wish to increase self limit. + '104': RateLimitExceeded, # You have been rate limited by the number of new orders. The default value is 100 new orders per second or 100.000 new orders per day. Please update existing orders instead of cancelling and creating orders. Please contact support if you wish to increase self limit. + '105': PermissionDenied, # Your IP or API key has been banned for not respecting the rate limit. The ban expires at ${expiryInMs}. + '107': ExchangeNotAvailable, # The matching engine is overloaded. Please wait 500ms and resubmit your order. + '108': ExchangeNotAvailable, # The matching engine could not process your order in time. Please consider increasing the access window or resubmit your order. + '109': ExchangeNotAvailable, # The matching engine did not respond in time. Operation may or may not have succeeded. + '110': BadRequest, # Invalid endpoint. Please check url and HTTP method. + '200': BadRequest, # ${param} url parameter is not supported. Please note that parameters are case-sensitive and use body parameters for PUT and POST requests. + '201': BadRequest, # ${param} body parameter is not supported. Please note that parameters are case-sensitive and use url parameters for GET and DELETE requests. + '202': BadRequest, # ${param} order parameter is not supported. Please note that certain parameters are only allowed for market or limit orders. + '203': BadSymbol, # {"errorCode":203,"error":"symbol parameter is required."} + '204': BadRequest, # ${param} parameter is not supported. + '205': BadRequest, # ${param} parameter is invalid. + '206': BadRequest, # Use either ${paramA} or ${paramB}. The usage of both parameters at the same time is not supported. + '210': InvalidOrder, # Amount exceeds the maximum allowed amount(1000000000). + '211': InvalidOrder, # Price exceeds the maximum allowed amount(100000000000). + '212': InvalidOrder, # Amount is below the minimum allowed amount for self asset. + '213': InvalidOrder, # Price is below the minimum allowed amount(0.000000000000001). + '214': InvalidOrder, # Price is too detailed + '215': InvalidOrder, # Price is too detailed. A maximum of 15 digits behind the decimal point are allowed. + '216': InsufficientFunds, # {"errorCode":216,"error":"You do not have sufficient balance to complete self operation."} + '217': InvalidOrder, # {"errorCode":217,"error":"Minimum order size in quote currency is 5 EUR or 0.001 BTC."} + '230': ExchangeError, # The order is rejected by the matching engine. + '231': ExchangeError, # The order is rejected by the matching engine. TimeInForce must be GTC when markets are paused. + '232': BadRequest, # You must change at least one of amount, amountRemaining, price, timeInForce, selfTradePrevention or postOnly. + '233': InvalidOrder, # {"errorCode":233,"error":"Order must be active(status new or partiallyFilled) to allow updating/cancelling."} + '234': InvalidOrder, # Market orders cannot be updated. + '235': ExchangeError, # You can only have 100 open orders on each book. + '236': BadRequest, # You can only update amount or amountRemaining, not both. + '240': OrderNotFound, # {"errorCode":240,"error":"No order found. Please be aware that simultaneously updating the same order may return self error."} + '300': AuthenticationError, # Authentication is required for self endpoint. + '301': AuthenticationError, # {"errorCode":301,"error":"API Key must be of length 64."} + '302': AuthenticationError, # Timestamp is invalid. This must be a timestamp in ms. See Bitvavo-Access-Timestamp header or timestamp parameter for websocket. + '303': AuthenticationError, # Window must be between 100 and 60000 ms. + '304': AuthenticationError, # Request was not received within acceptable window(default 30s, or custom with Bitvavo-Access-Window header) of Bitvavo-Access-Timestamp header(or timestamp parameter for websocket). + # "304": AuthenticationError, # Authentication is required for self endpoint. + '305': AuthenticationError, # {"errorCode":305,"error":"No active API key found."} + '306': AuthenticationError, # No active API key found. Please ensure that you have confirmed the API key by e-mail. + '307': PermissionDenied, # This key does not allow access from self IP. + '308': AuthenticationError, # {"errorCode":308,"error":"The signature length is invalid(HMAC-SHA256 should return a 64 length hexadecimal string)."} + '309': AuthenticationError, # {"errorCode":309,"error":"The signature is invalid."} + '310': PermissionDenied, # This key does not allow trading actions. + '311': PermissionDenied, # This key does not allow showing account information. + '312': PermissionDenied, # This key does not allow withdrawal of funds. + '315': BadRequest, # Websocket connections may not be used in a browser. Please use REST requests for self. + '317': AccountSuspended, # This account is locked. Please contact support. + '400': ExchangeError, # Unknown error. Please contact support with a copy of your request. + '401': ExchangeError, # Deposits for self asset are not available at self time. + '402': PermissionDenied, # You need to verify your identitiy before you can deposit and withdraw digital assets. + '403': PermissionDenied, # You need to verify your phone number before you can deposit and withdraw digital assets. + '404': OnMaintenance, # Could not complete self operation, because our node cannot be reached. Possibly under maintenance. + '405': ExchangeError, # You cannot withdraw digital assets during a cooldown period. This is the result of newly added bank accounts. + '406': BadRequest, # {"errorCode":406,"error":"Your withdrawal is too small."} + '407': ExchangeError, # Internal transfer is not possible. + '408': InsufficientFunds, # {"errorCode":408,"error":"You do not have sufficient balance to complete self operation."} + '409': InvalidAddress, # {"errorCode":409,"error":"This is not a verified bank account."} + '410': ExchangeError, # Withdrawals for self asset are not available at self time. + '411': BadRequest, # You can not transfer assets to yourself. + '412': InvalidAddress, # {"errorCode":412,"error":"eth_address_invalid."} + '413': InvalidAddress, # This address violates the whitelist. + '414': ExchangeError, # You cannot withdraw assets within 2 minutes of logging in. + }, + 'broad': { + 'start parameter is invalid': BadRequest, # {"errorCode":205,"error":"start parameter is invalid."} + 'symbol parameter is invalid': BadSymbol, # {"errorCode":205,"error":"symbol parameter is invalid."} + 'amount parameter is invalid': InvalidOrder, # {"errorCode":205,"error":"amount parameter is invalid."} + 'orderId parameter is invalid': InvalidOrder, # {"errorCode":205,"error":"orderId parameter is invalid."} + }, + }, + 'options': { + 'currencyToPrecisionRoundingMode': TRUNCATE, + 'BITVAVO-ACCESS-WINDOW': 10000, # default 10 sec + 'networks': { + 'ERC20': 'ETH', + 'TRC20': 'TRX', + }, + 'operatorId': None, # self will be required soon for order-related endpoints + 'fiatCurrencies': ['EUR'], # only fiat atm + }, + 'precisionMode': TICK_SIZE, + 'commonCurrencies': { + 'MIOTA': 'IOTA', # https://github.com/ccxt/ccxt/issues/7487 + }, + }) + + async def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = await self.publicGetTime(params) + # + # {"time": 1590379519148} + # + return self.safe_integer(response, 'time') + + async def fetch_markets(self, params={}) -> List[Market]: + """ + + https://docs.bitvavo.com/#tag/General/paths/~1markets/get + + retrieves data on all markets for bitvavo + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = await self.publicGetMarkets(params) + # + # { + # "market": "BTC-EUR", + # "status": "trading", + # "base": "BTC", + # "quote": "EUR", + # "pricePrecision": "0", # deprecated, self is mostly 0 across other markets too, which is abnormal, so we ignore self. + # "tickSize": "1.00", + # "minOrderInBaseAsset": "0.00006100", + # "minOrderInQuoteAsset": "5.00", + # "maxOrderInBaseAsset": "1000000000.00000000", + # "maxOrderInQuoteAsset": "1000000000.00", + # "quantityDecimals": "8", + # "notionalDecimals": "2", + # "maxOpenOrders": "100", + # "feeCategory": "A", + # "orderTypes": ["market", "limit", "stopLoss", "stopLossLimit", "takeProfit", "takeProfitLimit"] + # } + # + return self.parse_markets(response) + + def parse_markets(self, markets): + result = [] + fees = self.fees + for i in range(0, len(markets)): + market = markets[i] + id = self.safe_string(market, 'market') + baseId = self.safe_string(market, 'base') + quoteId = self.safe_string(market, 'quote') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + status = self.safe_string(market, 'status') + result.append(self.safe_market_structure({ + 'id': id, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': (status == 'trading'), + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'taker': fees['trading']['taker'], + 'maker': fees['trading']['maker'], + 'precision': { + 'amount': self.parse_number(self.parse_precision(self.safe_string(market, 'quantityDecimals'))), + 'price': self.safe_number(market, 'tickSize'), + 'cost': self.parse_number(self.parse_precision(self.safe_string(market, 'notionalDecimals'))), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(market, 'minOrderInBaseAsset'), + 'max': self.safe_number(market, 'maxOrderInBaseAsset'), + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': self.safe_number(market, 'minOrderInQuoteAsset'), + 'max': self.safe_number(market, 'maxOrderInQuoteAsset'), + }, + }, + 'created': None, + 'info': market, + })) + return result + + async def fetch_currencies(self, params={}) -> Currencies: + """ + + https://docs.bitvavo.com/#tag/General/paths/~1assets/get + + fetches all available currencies on an exchange + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = await self.publicGetAssets(params) + # + # [ + # { + # "symbol": "USDT", + # "displayTicker": "USDT", + # "name": "Tether", + # "slug": "tether", + # "popularity": -1, + # "decimals": 6, + # "depositFee": "0", + # "depositConfirmations": 64, + # "depositStatus": "OK", + # "withdrawalFee": "3.2", + # "withdrawalMinAmount": "3.2", + # "withdrawalStatus": "OK", + # "networks": [ + # "ETH" + # ], + # "light": { + # "color": "#009393", + # "icon": {"hash": "4ad7c699", "svg": "https://...", "webp16": "https://...", "webp32": "https://...", "webp64": "https://...", "webp128": "https://...", "webp256": "https://...", "png16": "https://...", "png32": "https://...", "png64": "https://...", "png128": "https://...", "png256": "https://..." + # } + # }, + # "dark": { + # "color": "#009393", + # "icon": {"hash": "4ad7c699", "svg": "https://...", "webp16": "https://...", "webp32": "https://...", "webp64": "https://...", "webp128": "https://...", "webp256": "https://...", "png16": "https://...", "png32": "https://...", "png64": "https://...", "png128": "https://...", "png256": "https://..." + # } + # }, + # "visibility": "PUBLIC", + # "message": "" + # }, + # ] + # + return self.parse_currencies_custom(response) + + def parse_currencies_custom(self, currencies): + # + # [ + # { + # "symbol": "USDT", + # "displayTicker": "USDT", + # "name": "Tether", + # "slug": "tether", + # "popularity": -1, + # "decimals": 6, + # "depositFee": "0", + # "depositConfirmations": 64, + # "depositStatus": "OK", + # "withdrawalFee": "3.2", + # "withdrawalMinAmount": "3.2", + # "withdrawalStatus": "OK", + # "networks": [ + # "ETH" + # ], + # "light": { + # "color": "#009393", + # "icon": {"hash": "4ad7c699", "svg": "https://...", "webp16": "https://...", "webp32": "https://...", "webp64": "https://...", "webp128": "https://...", "webp256": "https://...", "png16": "https://...", "png32": "https://...", "png64": "https://...", "png128": "https://...", "png256": "https://..." + # } + # }, + # "dark": { + # "color": "#009393", + # "icon": {"hash": "4ad7c699", "svg": "https://...", "webp16": "https://...", "webp32": "https://...", "webp64": "https://...", "webp128": "https://...", "webp256": "https://...", "png16": "https://...", "png32": "https://...", "png64": "https://...", "png128": "https://...", "png256": "https://..." + # } + # }, + # "visibility": "PUBLIC", + # "message": "" + # }, + # ] + # + fiatCurrencies = self.safe_list(self.options, 'fiatCurrencies', []) + result: dict = {} + for i in range(0, len(currencies)): + currency = currencies[i] + id = self.safe_string(currency, 'symbol') + code = self.safe_currency_code(id) + isFiat = self.in_array(code, fiatCurrencies) + networks: dict = {} + networksArray = self.safe_list(currency, 'networks', []) + deposit = self.safe_string(currency, 'depositStatus') == 'OK' + withdrawal = self.safe_string(currency, 'withdrawalStatus') == 'OK' + active = deposit and withdrawal + withdrawFee = self.safe_number(currency, 'withdrawalFee') + precision = self.safe_string(currency, 'decimals', '8') + minWithdraw = self.safe_number(currency, 'withdrawalMinAmount') + # btw, absolutely all of them have 1 network atm + for j in range(0, len(networksArray)): + networkId = networksArray[j] + networkCode = self.network_id_to_code(networkId) + networks[networkCode] = { + 'info': currency, + 'id': networkId, + 'network': networkCode, + 'active': active, + 'deposit': deposit, + 'withdraw': withdrawal, + 'fee': withdrawFee, + 'precision': self.parse_number(self.parse_precision(precision)), + 'limits': { + 'withdraw': { + 'min': minWithdraw, + 'max': None, + }, + }, + } + result[code] = self.safe_currency_structure({ + 'info': currency, + 'id': id, + 'code': code, + 'name': self.safe_string(currency, 'name'), + 'active': active, + 'deposit': deposit, + 'withdraw': withdrawal, + 'networks': networks, + 'fee': withdrawFee, + 'precision': None, + 'type': 'fiat' if isFiat else 'crypto', + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'deposit': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': minWithdraw, + 'max': None, + }, + }, + }) + return result + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + + https://docs.bitvavo.com/#tag/Market-Data/paths/~1ticker~124h/get + + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + response = await self.publicGetTicker24h(self.extend(request, params)) + # + # { + # "market":"ETH-BTC", + # "open":"0.022578", + # "high":"0.023019", + # "low":"0.022572", + # "last":"0.023019", + # "volume":"25.16366324", + # "volumeQuote":"0.57333305", + # "bid":"0.023039", + # "bidSize":"0.53500578", + # "ask":"0.023041", + # "askSize":"0.47859202", + # "timestamp":1590381666900 + # } + # + return self.parse_ticker(response, market) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # fetchTicker + # + # { + # "market":"ETH-BTC", + # "open":"0.022578", + # "high":"0.023019", + # "low":"0.022573", + # "last":"0.023019", + # "volume":"25.16366324", + # "volumeQuote":"0.57333305", + # "bid":"0.023039", + # "bidSize":"0.53500578", + # "ask":"0.023041", + # "askSize":"0.47859202", + # "timestamp":1590381666900 + # } + # + marketId = self.safe_string(ticker, 'market') + symbol = self.safe_symbol(marketId, market, '-') + timestamp = self.safe_integer(ticker, 'timestamp') + last = self.safe_string(ticker, 'last') + baseVolume = self.safe_string(ticker, 'volume') + quoteVolume = self.safe_string(ticker, 'volumeQuote') + open = self.safe_string(ticker, 'open') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': self.safe_string(ticker, 'bid'), + 'bidVolume': self.safe_string(ticker, 'bidSize'), + 'ask': self.safe_string(ticker, 'ask'), + 'askVolume': self.safe_string(ticker, 'askSize'), + 'vwap': None, + 'open': open, + 'close': last, + 'last': last, + 'previousClose': None, # previous day close + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'info': ticker, + }, market) + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + response = await self.publicGetTicker24h(params) + # + # [ + # { + # "market":"ADA-BTC", + # "open":"0.0000059595", + # "high":"0.0000059765", + # "low":"0.0000059595", + # "last":"0.0000059765", + # "volume":"2923.172", + # "volumeQuote":"0.01743483", + # "bid":"0.0000059515", + # "bidSize":"1117.630919", + # "ask":"0.0000059585", + # "askSize":"809.999739", + # "timestamp":1590382266324 + # } + # ] + # + return self.parse_tickers(response, symbols) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + + https://docs.bitvavo.com/#tag/Market-Data/paths/~1{market}~1trades/get + + get the list of most recent trades for a particular symbol + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch entries for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchTrades', 'paginate') + if paginate: + return await self.fetch_paginated_call_dynamic('fetchTrades', symbol, since, limit, params) + request: dict = { + 'market': market['id'], + # "limit": 500, # default 500, max 1000 + # "start": since, + # "end": self.milliseconds(), + # "tradeIdFrom": "57b1159b-6bf5-4cde-9e2c-6bd6a5678baf", + # "tradeIdTo": "57b1159b-6bf5-4cde-9e2c-6bd6a5678baf", + } + if limit is not None: + request['limit'] = min(limit, 1000) + if since is not None: + request['start'] = since + request, params = self.handle_until_option('end', request, params) + response = await self.publicGetMarketTrades(self.extend(request, params)) + # + # [ + # { + # "id":"94154c98-6e8b-4e33-92a8-74e33fc05650", + # "timestamp":1590382761859, + # "amount":"0.06026079", + # "price":"8095.3", + # "side":"buy" + # } + # ] + # + return self.parse_trades(response, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(public) + # + # { + # "id":"94154c98-6e8b-4e33-92a8-74e33fc05650", + # "timestamp":1590382761859, + # "amount":"0.06026079", + # "price":"8095.3", + # "side":"buy" + # } + # + # createOrder, fetchOpenOrders, fetchOrders, editOrder(private) + # + # { + # "id":"b0c86aa5-6ed3-4a2d-ba3a-be9a964220f4", + # "timestamp":1590505649245, + # "amount":"0.249825", + # "price":"183.49", + # "taker":true, + # "fee":"0.12038925", + # "feeCurrency":"EUR", + # "settled":true + # } + # + # fetchMyTrades(private) + # + # { + # "id":"b0c86aa5-6ed3-4a2d-ba3a-be9a964220f4", + # "orderId":"af76d6ce-9f7c-4006-b715-bb5d430652d0", + # "timestamp":1590505649245, + # "market":"ETH-EUR", + # "side":"sell", + # "amount":"0.249825", + # "price":"183.49", + # "taker":true, + # "fee":"0.12038925", + # "feeCurrency":"EUR", + # "settled":true + # } + # + # watchMyTrades(private) + # + # { + # "event": "fill", + # "timestamp": 1590964470132, + # "market": "ETH-EUR", + # "orderId": "85d082e1-eda4-4209-9580-248281a29a9a", + # "fillId": "861d2da5-aa93-475c-8d9a-dce431bd4211", + # "side": "sell", + # "amount": "0.1", + # "price": "211.46", + # "taker": True, + # "fee": "0.056", + # "feeCurrency": "EUR" + # } + # + priceString = self.safe_string(trade, 'price') + amountString = self.safe_string(trade, 'amount') + timestamp = self.safe_integer(trade, 'timestamp') + side = self.safe_string(trade, 'side') + id = self.safe_string_2(trade, 'id', 'fillId') + marketId = self.safe_string(trade, 'market') + symbol = self.safe_symbol(marketId, market, '-') + taker = self.safe_value(trade, 'taker') + takerOrMaker = None + if taker is not None: + takerOrMaker = 'taker' if taker else 'maker' + feeCostString = self.safe_string(trade, 'fee') + fee = None + if feeCostString is not None: + feeCurrencyId = self.safe_string(trade, 'feeCurrency') + feeCurrencyCode = self.safe_currency_code(feeCurrencyId) + fee = { + 'cost': feeCostString, + 'currency': feeCurrencyCode, + } + orderId = self.safe_string(trade, 'orderId') + return self.safe_trade({ + 'info': trade, + 'id': id, + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'order': orderId, + 'type': None, + 'side': side, + 'takerOrMaker': takerOrMaker, + 'price': priceString, + 'amount': amountString, + 'cost': None, + 'fee': fee, + }, market) + + async def fetch_trading_fees(self, params={}) -> TradingFees: + """ + + https://docs.bitvavo.com/#tag/Account/paths/~1account/get + + fetch the trading fees for multiple markets + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + await self.load_markets() + response = await self.privateGetAccount(params) + # + # { + # "fees": { + # "taker": "0.0025", + # "maker": "0.0015", + # "volume": "10000.00" + # } + # } + # + return self.parse_trading_fees(response) + + def parse_trading_fees(self, fees, market=None): + # + # { + # "fees": { + # "taker": "0.0025", + # "maker": "0.0015", + # "volume": "10000.00" + # } + # } + # + feesValue = self.safe_value(fees, 'fees') + maker = self.safe_number(feesValue, 'maker') + taker = self.safe_number(feesValue, 'taker') + result: dict = {} + for i in range(0, len(self.symbols)): + symbol = self.symbols[i] + result[symbol] = { + 'info': fees, + 'symbol': symbol, + 'maker': maker, + 'taker': taker, + 'percentage': True, + 'tierBased': True, + } + return result + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + + https://docs.bitvavo.com/#tag/Market-Data/paths/~1{market}~1book/get + + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + if limit is not None: + request['depth'] = limit + response = await self.publicGetMarketBook(self.extend(request, params)) + # + # { + # "market":"BTC-EUR", + # "nonce":35883831, + # "bids":[ + # ["8097.4","0.6229099"], + # ["8097.2","0.64151283"], + # ["8097.1","0.24966294"], + # ], + # "asks":[ + # ["8097.5","1.36916911"], + # ["8098.8","0.33462248"], + # ["8099.3","1.12908646"], + # ] + # } + # + orderbook = self.parse_order_book(response, market['symbol']) + orderbook['nonce'] = self.safe_integer(response, 'nonce') + return orderbook + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # [ + # 1590383700000, + # "8088.5", + # "8088.5", + # "8088.5", + # "8088.5", + # "0.04788623" + # ] + # + return [ + self.safe_integer(ohlcv, 0), + self.safe_number(ohlcv, 1), + self.safe_number(ohlcv, 2), + self.safe_number(ohlcv, 3), + self.safe_number(ohlcv, 4), + self.safe_number(ohlcv, 5), + ] + + def fetch_ohlcv_request(self, symbol: Str, timeframe='1m', since: Int = None, limit: Int = None, params={}): + market = self.market(symbol) + request: dict = { + 'market': market['id'], + 'interval': self.safe_string(self.timeframes, timeframe, timeframe), + # "limit": 1440, # default 1440, max 1440 + # "start": since, + # "end": self.milliseconds(), + } + if since is not None: + # https://github.com/ccxt/ccxt/issues/9227 + duration = self.parse_timeframe(timeframe) + request['start'] = since + if limit is None: + limit = 1440 + else: + limit = min(limit, 1440) + request['end'] = self.sum(since, limit * duration * 1000) + request, params = self.handle_until_option('end', request, params) + if limit is not None: + request['limit'] = limit # default 1440, max 1440 + return self.extend(request, params) + + async def fetch_ohlcv(self, symbol: Str, timeframe='1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + + https://docs.bitvavo.com/#tag/Market-Data/paths/~1{market}~1candles/get + + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch entries for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate') + if paginate: + return await self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params, 1440) + request = self.fetch_ohlcv_request(symbol, timeframe, since, limit, params) + response = await self.publicGetMarketCandles(request) + # + # [ + # [1590383700000,"8088.5","8088.5","8088.5","8088.5","0.04788623"], + # [1590383580000,"8091.3","8091.5","8091.3","8091.5","0.04931221"], + # [1590383520000,"8090.3","8092.7","8090.3","8092.5","0.04001286"], + # ] + # + return self.parse_ohlcvs(response, market, timeframe, since, limit) + + def parse_balance(self, response) -> Balances: + result: dict = { + 'info': response, + 'timestamp': None, + 'datetime': None, + } + for i in range(0, len(response)): + balance = response[i] + currencyId = self.safe_string(balance, 'symbol') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(balance, 'available') + account['used'] = self.safe_string(balance, 'inOrder') + result[code] = account + return self.safe_balance(result) + + async def fetch_balance(self, params={}) -> Balances: + """ + + https://docs.bitvavo.com/#tag/Account/paths/~1balance/get + + query for balance and get the amount of funds available for trading or funds locked in orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + response = await self.privateGetBalance(params) + # + # [ + # { + # "symbol": "BTC", + # "available": "1.57593193", + # "inOrder": "0.74832374" + # } + # ] + # + return self.parse_balance(response) + + async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'symbol': currency['id'], + } + response = await self.privateGetDeposit(self.extend(request, params)) + # + # { + # "address": "0x449889e3234514c45d57f7c5a571feba0c7ad567", + # "paymentId": "10002653" + # } + # + address = self.safe_string(response, 'address') + tag = self.safe_string(response, 'paymentId') + self.check_address(address) + return { + 'info': response, + 'currency': code, + 'network': None, + 'address': address, + 'tag': tag, + } + + def create_order_request(self, symbol: Str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + market = self.market(symbol) + request: dict = { + 'market': market['id'], + 'side': side, + 'orderType': type, + } + isMarketOrder = (type == 'market') or (type == 'stopLoss') or (type == 'takeProfit') + isLimitOrder = (type == 'limit') or (type == 'stopLossLimit') or (type == 'takeProfitLimit') + timeInForce = self.safe_string(params, 'timeInForce') + triggerPrice = self.safe_string_n(params, ['triggerPrice', 'stopPrice', 'triggerAmount']) + postOnly = self.is_post_only(isMarketOrder, False, params) + stopLossPrice = self.safe_value(params, 'stopLossPrice') # trigger when price crosses from above to below self value + takeProfitPrice = self.safe_value(params, 'takeProfitPrice') # trigger when price crosses from below to above self value + params = self.omit(params, ['timeInForce', 'triggerPrice', 'stopPrice', 'stopLossPrice', 'takeProfitPrice']) + if isMarketOrder: + cost = None + if price is not None: + priceString = self.number_to_string(price) + amountString = self.number_to_string(amount) + quoteAmount = Precise.string_mul(amountString, priceString) + cost = self.parse_number(quoteAmount) + else: + cost = self.safe_number(params, 'cost') + if cost is not None: + precision = self.currency(market['quote'])['precision'] + request['amountQuote'] = self.decimal_to_precision(cost, TRUNCATE, precision, self.precisionMode) + else: + request['amount'] = self.amount_to_precision(symbol, amount) + params = self.omit(params, ['cost']) + elif isLimitOrder: + request['price'] = self.price_to_precision(symbol, price) + request['amount'] = self.amount_to_precision(symbol, amount) + isTakeProfit = (takeProfitPrice is not None) or (type == 'takeProfit') or (type == 'takeProfitLimit') + isStopLoss = (stopLossPrice is not None) or (triggerPrice is not None) and (not isTakeProfit) or (type == 'stopLoss') or (type == 'stopLossLimit') + if isStopLoss: + if stopLossPrice is not None: + triggerPrice = stopLossPrice + request['orderType'] = 'stopLoss' if isMarketOrder else 'stopLossLimit' + elif isTakeProfit: + if takeProfitPrice is not None: + triggerPrice = takeProfitPrice + request['orderType'] = 'takeProfit' if isMarketOrder else 'takeProfitLimit' + if triggerPrice is not None: + request['triggerAmount'] = self.price_to_precision(symbol, triggerPrice) + request['triggerType'] = 'price' + request['triggerReference'] = 'lastTrade' # 'bestBid', 'bestAsk', 'midPrice' + if (timeInForce is not None) and (timeInForce != 'PO'): + request['timeInForce'] = timeInForce + if postOnly: + request['postOnly'] = True + operatorId = None + operatorId, params = self.handle_option_and_params(params, 'createOrder', 'operatorId') + if operatorId is not None: + request['operatorId'] = self.parse_to_int(operatorId) + else: + raise ArgumentsRequired(self.id + ' createOrder() requires an operatorId in params or options, eg: exchange.options[\'operatorId\'] = 1234567890') + selfTradePrevention = None + selfTradePrevention, params = self.handle_option_and_params(params, 'createOrder', 'selfTradePrevention') + if selfTradePrevention is not None: + if selfTradePrevention == 'EXPIRE_BOTH': + request['selfTradePrevention'] = 'cancelBoth' + else: + request['selfTradePrevention'] = selfTradePrevention + return self.extend(request, params) + + async def create_order(self, symbol: Str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://docs.bitvavo.com/#tag/Trading-endpoints/paths/~1order/post + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float price: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the bitvavo api endpoint + :param str [params.timeInForce]: "GTC", "IOC", or "PO" + :param float [params.stopPrice]: Alias for triggerPrice + :param float [params.triggerPrice]: The price at which a trigger order is triggered at + :param bool [params.postOnly]: If True, the order will only be posted to the order book and not executed immediately + :param float [params.stopLossPrice]: The price at which a stop loss order is triggered at + :param float [params.takeProfitPrice]: The price at which a take profit order is triggered at + :param str [params.triggerType]: "price" + :param str [params.triggerReference]: "lastTrade", "bestBid", "bestAsk", "midPrice" Only for stop orders: Use self to determine which parameter will trigger the order + :param str [params.selfTradePrevention]: one of EXPIRE_BOTH, cancelOldest, cancelNewest or decrementAndCancel + :param bool [params.disableMarketProtection]: don't cancel if the next fill price is 10% worse than the best fill price + :param bool [params.responseRequired]: Set self to 'false' when only an acknowledgement of success or failure is required, self is faster. + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + request = self.create_order_request(symbol, type, side, amount, price, params) + response = await self.privatePostOrder(request) + # + # { + # "orderId":"dec6a640-5b4c-45bc-8d22-3b41c6716630", + # "market":"DOGE-EUR", + # "created":1654789135146, + # "updated":1654789135153, + # "status":"new", + # "side":"buy", + # "orderType":"stopLossLimit", + # "amount":"200", + # "amountRemaining":"200", + # "price":"0.07471", + # "triggerPrice":"0.0747", + # "triggerAmount":"0.0747", + # "triggerType":"price", + # "triggerReference":"lastTrade", + # "onHold":"14.98", + # "onHoldCurrency":"EUR", + # "filledAmount":"0", + # "filledAmountQuote":"0", + # "feePaid":"0", + # "feeCurrency":"EUR", + # "fills":[ # filled with market orders only + # { + # "id":"b0c86aa5-6ed3-4a2d-ba3a-be9a964220f4", + # "timestamp":1590505649245, + # "amount":"0.249825", + # "price":"183.49", + # "taker":true, + # "fee":"0.12038925", + # "feeCurrency":"EUR", + # "settled":true + # } + # ], + # "selfTradePrevention":"decrementAndCancel", + # "visible":true, + # "timeInForce":"GTC", + # "postOnly":false + # } + # + return self.parse_order(response, market) + + def edit_order_request(self, id: str, symbol, type, side, amount=None, price=None, params={}): + request: dict = {} + market = self.market(symbol) + amountRemaining = self.safe_number(params, 'amountRemaining') + triggerPrice = self.safe_string_n(params, ['triggerPrice', 'stopPrice', 'triggerAmount']) + params = self.omit(params, ['amountRemaining', 'triggerPrice', 'stopPrice', 'triggerAmount']) + if price is not None: + request['price'] = self.price_to_precision(symbol, price) + if amount is not None: + request['amount'] = self.amount_to_precision(symbol, amount) + if amountRemaining is not None: + request['amountRemaining'] = self.amount_to_precision(symbol, amountRemaining) + if triggerPrice is not None: + request['triggerAmount'] = self.price_to_precision(symbol, triggerPrice) + request = self.extend(request, params) + if self.is_empty(request): + raise ArgumentsRequired(self.id + ' editOrder() requires an amount argument, or a price argument, or non-empty params') + clientOrderId = self.safe_string(params, 'clientOrderId') + if clientOrderId is None: + request['orderId'] = id + operatorId = None + operatorId, params = self.handle_option_and_params(params, 'editOrder', 'operatorId') + if operatorId is not None: + request['operatorId'] = self.parse_to_int(operatorId) + else: + raise ArgumentsRequired(self.id + ' editOrder() requires an operatorId in params or options, eg: exchange.options[\'operatorId\'] = 1234567890') + request['market'] = market['id'] + return request + + async def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + """ + edit a trade order + + https://docs.bitvavo.com/#tag/Orders/paths/~1order/put + + :param str id: cancel order id + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float [amount]: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the bitvavo api endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + request = self.edit_order_request(id, symbol, type, side, amount, price, params) + response = await self.privatePutOrder(request) + return self.parse_order(response, market) + + def cancel_order_request(self, id: Str, symbol: Str = None, params={}): + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + clientOrderId = self.safe_string(params, 'clientOrderId') + if clientOrderId is None: + request['orderId'] = id + operatorId = None + operatorId, params = self.handle_option_and_params(params, 'cancelOrder', 'operatorId') + if operatorId is not None: + request['operatorId'] = self.parse_to_int(operatorId) + else: + raise ArgumentsRequired(self.id + ' cancelOrder() requires an operatorId in params or options, eg: exchange.options[\'operatorId\'] = 1234567890') + return self.extend(request, params) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + + https://docs.bitvavo.com/#tag/Orders/paths/~1order/delete + + cancels an open order + + https://docs.bitvavo.com/#tag/Trading-endpoints/paths/~1order/delete + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + request = self.cancel_order_request(id, symbol, params) + response = await self.privateDeleteOrder(request) + # + # { + # "orderId": "2e7ce7fc-44e2-4d80-a4a7-d079c4750b61" + # } + # + return self.parse_order(response, market) + + async def cancel_all_orders(self, symbol: Str = None, params={}): + """ + + https://docs.bitvavo.com/#tag/Orders/paths/~1orders/delete + + cancel all open orders + :param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['market'] = market['id'] + operatorId = None + operatorId, params = self.handle_option_and_params(params, 'cancelAllOrders', 'operatorId') + if operatorId is not None: + request['operatorId'] = self.parse_to_int(operatorId) + else: + raise ArgumentsRequired(self.id + ' canceAllOrders() requires an operatorId in params or options, eg: exchange.options[\'operatorId\'] = 1234567890') + response = await self.privateDeleteOrders(self.extend(request, params)) + # + # [ + # { + # "orderId": "1be6d0df-d5dc-4b53-a250-3376f3b393e6" + # } + # ] + # + return self.parse_orders(response, market) + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://docs.bitvavo.com/#tag/Trading-endpoints/paths/~1order/get + + :param str id: the order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrder() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + clientOrderId = self.safe_string(params, 'clientOrderId') + if clientOrderId is None: + request['orderId'] = id + response = await self.privateGetOrder(self.extend(request, params)) + # + # { + # "orderId":"af76d6ce-9f7c-4006-b715-bb5d430652d0", + # "market":"ETH-EUR", + # "created":1590505649241, + # "updated":1590505649241, + # "status":"filled", + # "side":"sell", + # "orderType":"market", + # "amount":"0.249825", + # "amountRemaining":"0", + # "onHold":"0", + # "onHoldCurrency":"ETH", + # "filledAmount":"0.249825", + # "filledAmountQuote":"45.84038925", + # "feePaid":"0.12038925", + # "feeCurrency":"EUR", + # "fills":[ + # { + # "id":"b0c86aa5-6ed3-4a2d-ba3a-be9a964220f4", + # "timestamp":1590505649245, + # "amount":"0.249825", + # "price":"183.49", + # "taker":true, + # "fee":"0.12038925", + # "feeCurrency":"EUR", + # "settled":true + # } + # ], + # "selfTradePrevention":"decrementAndCancel", + # "visible":false, + # "disableMarketProtection":false + # } + # + return self.parse_order(response, market) + + def fetch_orders_request(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + market = self.market(symbol) + request: dict = { + 'market': market['id'], + # "limit": 500, + # "start": since, + # "end": self.milliseconds(), + # "orderIdFrom": "af76d6ce-9f7c-4006-b715-bb5d430652d0", + # "orderIdTo": "af76d6ce-9f7c-4006-b715-bb5d430652d0", + } + if since is not None: + request['start'] = since + if limit is not None: + request['limit'] = limit # default 500, max 1000 + request, params = self.handle_until_option('end', request, params) + return self.extend(request, params) + + async def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + + https://docs.bitvavo.com/#tag/Trading-endpoints/paths/~1orders/get + + fetches information on multiple orders made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param int [params.until]: the latest time in ms to fetch entries for + :returns Order[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrders() requires a symbol argument') + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOrders', 'paginate') + if paginate: + return await self.fetch_paginated_call_dynamic('fetchOrders', symbol, since, limit, params) + market = self.market(symbol) + request = self.fetch_orders_request(symbol, since, limit, params) + response = await self.privateGetOrders(request) + # + # [ + # { + # "orderId":"af76d6ce-9f7c-4006-b715-bb5d430652d0", + # "market":"ETH-EUR", + # "created":1590505649241, + # "updated":1590505649241, + # "status":"filled", + # "side":"sell", + # "orderType":"market", + # "amount":"0.249825", + # "amountRemaining":"0", + # "onHold":"0", + # "onHoldCurrency":"ETH", + # "filledAmount":"0.249825", + # "filledAmountQuote":"45.84038925", + # "feePaid":"0.12038925", + # "feeCurrency":"EUR", + # "fills":[ + # { + # "id":"b0c86aa5-6ed3-4a2d-ba3a-be9a964220f4", + # "timestamp":1590505649245, + # "amount":"0.249825", + # "price":"183.49", + # "taker":true, + # "fee":"0.12038925", + # "feeCurrency":"EUR", + # "settled":true + # } + # ], + # "selfTradePrevention":"decrementAndCancel", + # "visible":false, + # "disableMarketProtection":false + # } + # ] + # + return self.parse_orders(response, market, since, limit) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + + https://docs.bitvavo.com/#tag/Trading-endpoints/paths/~1ordersOpen/get + + fetch all unfilled currently open orders + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + request: dict = { + # "market": market["id"], # rate limit 25 without a market, 1 with market specified + } + market = None + if symbol is not None: + market = self.market(symbol) + request['market'] = market['id'] + response = await self.privateGetOrdersOpen(self.extend(request, params)) + # + # [ + # { + # "orderId":"af76d6ce-9f7c-4006-b715-bb5d430652d0", + # "market":"ETH-EUR", + # "created":1590505649241, + # "updated":1590505649241, + # "status":"filled", + # "side":"sell", + # "orderType":"market", + # "amount":"0.249825", + # "amountRemaining":"0", + # "onHold":"0", + # "onHoldCurrency":"ETH", + # "filledAmount":"0.249825", + # "filledAmountQuote":"45.84038925", + # "feePaid":"0.12038925", + # "feeCurrency":"EUR", + # "fills":[ + # { + # "id":"b0c86aa5-6ed3-4a2d-ba3a-be9a964220f4", + # "timestamp":1590505649245, + # "amount":"0.249825", + # "price":"183.49", + # "taker":true, + # "fee":"0.12038925", + # "feeCurrency":"EUR", + # "settled":true + # } + # ], + # "selfTradePrevention":"decrementAndCancel", + # "visible":false, + # "disableMarketProtection":false + # } + # ] + # + return self.parse_orders(response, market, since, limit) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'new': 'open', + 'canceled': 'canceled', + 'canceledAuction': 'canceled', + 'canceledSelfTradePrevention': 'canceled', + 'canceledIOC': 'canceled', + 'canceledFOK': 'canceled', + 'canceledMarketProtection': 'canceled', + 'canceledPostOnly': 'canceled', + 'filled': 'closed', + 'partiallyFilled': 'open', + 'expired': 'canceled', + 'rejected': 'canceled', + 'awaitingTrigger': 'open', # https://github.com/ccxt/ccxt/issues/8489 + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # cancelOrder, cancelAllOrders + # + # { + # "orderId": "2e7ce7fc-44e2-4d80-a4a7-d079c4750b61" + # } + # + # createOrder, fetchOrder, fetchOpenOrders, fetchOrders, editOrder + # + # { + # "orderId":"af76d6ce-9f7c-4006-b715-bb5d430652d0", + # "market":"ETH-EUR", + # "created":1590505649241, + # "updated":1590505649241, + # "status":"filled", + # "side":"sell", + # "orderType":"market", + # "amount":"0.249825", + # "amountRemaining":"0", + # "price": "183.49", # limit orders only + # "onHold":"0", + # "onHoldCurrency":"ETH", + # "filledAmount":"0.249825", + # "filledAmountQuote":"45.84038925", + # "feePaid":"0.12038925", + # "feeCurrency":"EUR", + # "fills":[ + # { + # "id":"b0c86aa5-6ed3-4a2d-ba3a-be9a964220f4", + # "timestamp":1590505649245, + # "amount":"0.249825", + # "price":"183.49", + # "taker":true, + # "fee":"0.12038925", + # "feeCurrency":"EUR", + # "settled":true + # } + # ], + # "selfTradePrevention":"decrementAndCancel", + # "visible":false, + # "disableMarketProtection":false + # "timeInForce": "GTC", + # "postOnly": True, + # } + # + id = self.safe_string(order, 'orderId') + timestamp = self.safe_integer(order, 'created') + marketId = self.safe_string(order, 'market') + market = self.safe_market(marketId, market, '-') + symbol = market['symbol'] + status = self.parse_order_status(self.safe_string(order, 'status')) + side = self.safe_string(order, 'side') + type = self.safe_string(order, 'orderType') + price = self.safe_string(order, 'price') + amount = self.safe_string(order, 'amount') + remaining = self.safe_string(order, 'amountRemaining') + filled = self.safe_string(order, 'filledAmount') + cost = self.safe_string(order, 'filledAmountQuote') + if cost is None: + amountQuote = self.safe_string(order, 'amountQuote') + amountQuoteRemaining = self.safe_string(order, 'amountQuoteRemaining') + cost = Precise.string_sub(amountQuote, amountQuoteRemaining) + fee = None + feeCost = self.safe_number(order, 'feePaid') + if feeCost is not None: + feeCurrencyId = self.safe_string(order, 'feeCurrency') + feeCurrencyCode = self.safe_currency_code(feeCurrencyId) + fee = { + 'cost': feeCost, + 'currency': feeCurrencyCode, + } + rawTrades = self.safe_value(order, 'fills', []) + timeInForce = self.safe_string(order, 'timeInForce') + postOnly = self.safe_value(order, 'postOnly') + # https://github.com/ccxt/ccxt/issues/8489 + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'symbol': symbol, + 'type': type, + 'timeInForce': timeInForce, + 'postOnly': postOnly, + 'side': side, + 'price': price, + 'triggerPrice': self.safe_number(order, 'triggerPrice'), + 'amount': amount, + 'cost': cost, + 'average': None, + 'filled': filled, + 'remaining': remaining, + 'status': status, + 'fee': fee, + 'trades': rawTrades, + }, market) + + def fetch_my_trades_request(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + market = self.market(symbol) + request: dict = { + 'market': market['id'], + # "limit": 500, + # "start": since, + # "end": self.milliseconds(), + # "tradeIdFrom": "af76d6ce-9f7c-4006-b715-bb5d430652d0", + # "tradeIdTo": "af76d6ce-9f7c-4006-b715-bb5d430652d0", + } + if since is not None: + request['start'] = since + if limit is not None: + request['limit'] = limit # default 500, max 1000 + request, params = self.handle_until_option('end', request, params) + return self.extend(request, params) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + + https://docs.bitvavo.com/#tag/Trading-endpoints/paths/~1trades/get + + fetch all trades made by the user + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch entries for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Trade[]: a list of `trade structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMyTrades() requires a symbol argument') + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchMyTrades', 'paginate') + if paginate: + return await self.fetch_paginated_call_dynamic('fetchMyTrades', symbol, since, limit, params) + market = self.market(symbol) + request = self.fetch_my_trades_request(symbol, since, limit, params) + response = await self.privateGetTrades(request) + # + # [ + # { + # "id":"b0c86aa5-6ed3-4a2d-ba3a-be9a964220f4", + # "orderId":"af76d6ce-9f7c-4006-b715-bb5d430652d0", + # "timestamp":1590505649245, + # "market":"ETH-EUR", + # "side":"sell", + # "amount":"0.249825", + # "price":"183.49", + # "taker":true, + # "fee":"0.12038925", + # "feeCurrency":"EUR", + # "settled":true + # } + # ] + # + return self.parse_trades(response, market, since, limit) + + def withdraw_request(self, code: Str, amount, address, tag=None, params={}): + currency = self.currency(code) + request: dict = { + 'symbol': currency['id'], + 'amount': self.currency_to_precision(code, amount), + 'address': address, # address or IBAN + # 'internal': False, # transfer to another Bitvavo user address, no fees + # 'addWithdrawalFee': False, # True = add the fee on top, otherwise the fee is subtracted from the amount + } + if tag is not None: + request['paymentId'] = tag + return self.extend(request, params) + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.check_address(address) + await self.load_markets() + currency = self.currency(code) + request = self.withdraw_request(code, amount, address, tag, params) + response = await self.privatePostWithdrawal(request) + # + # { + # "success": True, + # "symbol": "BTC", + # "amount": "1.5" + # } + # + return self.parse_transaction(response, currency) + + def fetch_withdrawals_request(self, code: Str = None, since: Int = None, limit: Int = None, params={}): + request: dict = { + # 'symbol': currency['id'], + # 'limit': 500, # default 500, max 1000 + # 'start': since, + # 'end': self.milliseconds(), + } + currency = None + if code is not None: + currency = self.currency(code) + request['symbol'] = currency['id'] + if since is not None: + request['start'] = since + if limit is not None: + request['limit'] = limit # default 500, max 1000 + return self.extend(request, params) + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + + https://docs.bitvavo.com/#tag/Account/paths/~1withdrawalHistory/get + + fetch all withdrawals made from an account + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the bitvavo api endpoint + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + request = self.fetch_withdrawals_request(code, since, limit, params) + currency = None + if code is not None: + currency = self.currency(code) + response = await self.privateGetWithdrawalHistory(request) + # + # [ + # { + # "timestamp":1590531212000, + # "symbol":"ETH", + # "amount":"0.091", + # "fee":"0.009", + # "status":"awaiting_bitvavo_inspection", + # "address":"0xe42b309f1eE9F0cbf7f54CcF3bc2159eBfA6735b", + # "paymentId": "10002653", + # "txId": "927b3ea50c5bb52c6854152d305dfa1e27fc01d10464cf10825d96d69d235eb3", + # } + # ] + # + return self.parse_transactions(response, currency, since, limit, {'type': 'withdrawal'}) + + def fetch_deposits_request(self, code: Str = None, since: Int = None, limit: Int = None, params={}): + request: dict = { + # 'symbol': currency['id'], + # 'limit': 500, # default 500, max 1000 + # 'start': since, + # 'end': self.milliseconds(), + } + currency = None + if code is not None: + currency = self.currency(code) + request['symbol'] = currency['id'] + if since is not None: + request['start'] = since + if limit is not None: + request['limit'] = limit # default 500, max 1000 + return self.extend(request, params) + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + + https://docs.bitvavo.com/#tag/Account/paths/~1depositHistory/get + + fetch all deposits made to an account + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the bitvavo api endpoint + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + request = self.fetch_deposits_request(code, since, limit, params) + currency = None + if code is not None: + currency = self.currency(code) + response = await self.privateGetDepositHistory(request) + # + # [ + # { + # "timestamp":1590492401000, + # "symbol":"ETH", + # "amount":"0.249825", + # "fee":"0", + # "status":"completed", + # "txId":"0x5167b473fd37811f9ef22364c3d54726a859ef9d98934b3a1e11d7baa8d2c2e2" + # } + # ] + # + return self.parse_transactions(response, currency, since, limit, {'type': 'deposit'}) + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'awaiting_processing': 'pending', + 'awaiting_email_confirmation': 'pending', + 'awaiting_bitvavo_inspection': 'pending', + 'approved': 'pending', + 'sending': 'pending', + 'in_mempool': 'pending', + 'processed': 'pending', + 'completed': 'ok', + 'canceled': 'canceled', + } + return self.safe_string(statuses, status, status) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # withdraw + # + # { + # "success": True, + # "symbol": "BTC", + # "amount": "1.5" + # } + # + # fetchWithdrawals + # + # { + # "timestamp": 1542967486256, + # "symbol": "BTC", + # "amount": "0.99994", + # "address": "BitcoinAddress", + # "paymentId": "10002653", + # "txId": "927b3ea50c5bb52c6854152d305dfa1e27fc01d10464cf10825d96d69d235eb3", + # "fee": "0.00006", + # "status": "awaiting_processing" + # } + # + # fetchDeposits + # + # { + # "timestamp":1590492401000, + # "symbol":"ETH", + # "amount":"0.249825", + # "fee":"0", + # "status":"completed", + # "txId":"0x5167b473fd37811f9ef22364c3d54726a859ef9d98934b3a1e11d7baa8d2c2e2" + # } + # + id = None + timestamp = self.safe_integer(transaction, 'timestamp') + currencyId = self.safe_string(transaction, 'symbol') + code = self.safe_currency_code(currencyId, currency) + status = self.parse_transaction_status(self.safe_string(transaction, 'status')) + amount = self.safe_number(transaction, 'amount') + address = self.safe_string(transaction, 'address') + txid = self.safe_string(transaction, 'txId') + fee = None + feeCost = self.safe_number(transaction, 'fee') + if feeCost is not None: + fee = { + 'cost': feeCost, + 'currency': code, + } + type = None + if ('success' in transaction) or ('address' in transaction): + type = 'withdrawal' + else: + type = 'deposit' + tag = self.safe_string(transaction, 'paymentId') + return { + 'info': transaction, + 'id': id, + 'txid': txid, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'addressFrom': None, + 'address': address, + 'addressTo': address, + 'tagFrom': None, + 'tag': tag, + 'tagTo': tag, + 'type': type, + 'amount': amount, + 'currency': code, + 'status': status, + 'updated': None, + 'fee': fee, + 'network': None, + 'comment': None, + 'internal': None, + } + + def parse_deposit_withdraw_fee(self, fee, currency: Currency = None): + # + # { + # "symbol": "1INCH", + # "name": "1inch", + # "decimals": 8, + # "depositFee": "0", + # "depositConfirmations": 64, + # "depositStatus": "OK", + # "withdrawalFee": "6.1", + # "withdrawalMinAmount": "6.1", + # "withdrawalStatus": "OK", + # "networks": [ + # "ETH" + # ], + # "message": "" + # } + # + result: dict = { + 'info': fee, + 'withdraw': { + 'fee': self.safe_number(fee, 'withdrawalFee'), + 'percentage': False, + }, + 'deposit': { + 'fee': self.safe_number(fee, 'depositFee'), + 'percentage': False, + }, + 'networks': {}, + } + networks = self.safe_value(fee, 'networks') + networkId = self.safe_value(networks, 0) # Bitvavo currently only supports one network per currency + currencyCode = self.safe_string(currency, 'code') + if networkId == 'Mainnet': + networkId = currencyCode + networkCode = self.network_id_to_code(networkId, currencyCode) + result['networks'][networkCode] = { + 'deposit': result['deposit'], + 'withdraw': result['withdraw'], + } + return result + + async def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}): + """ + fetch deposit and withdraw fees + + https://docs.bitvavo.com/#tag/General/paths/~1assets/get + + :param str[]|None codes: list of unified currency codes + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `fee structures ` + """ + await self.load_markets() + response = await self.publicGetAssets(params) + # + # [ + # { + # "symbol": "1INCH", + # "name": "1inch", + # "decimals": 8, + # "depositFee": "0", + # "depositConfirmations": 64, + # "depositStatus": "OK", + # "withdrawalFee": "6.1", + # "withdrawalMinAmount": "6.1", + # "withdrawalStatus": "OK", + # "networks": [ + # "ETH" + # ], + # "message": "" + # }, + # ] + # + return self.parse_deposit_withdraw_fees(response, codes, 'symbol') + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + query = self.omit(params, self.extract_params(path)) + url = '/' + self.version + '/' + self.implode_params(path, params) + getOrDelete = (method == 'GET') or (method == 'DELETE') + if getOrDelete: + if query: + url += '?' + self.urlencode(query) + if api == 'private': + self.check_required_credentials() + payload = '' + if not getOrDelete: + if query: + body = self.json(query) + payload = body + timestamp = str(self.milliseconds()) + auth = timestamp + method + url + payload + signature = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256) + accessWindow = self.safe_string(self.options, 'BITVAVO-ACCESS-WINDOW', '10000') + headers = { + 'BITVAVO-ACCESS-KEY': self.apiKey, + 'BITVAVO-ACCESS-SIGNATURE': signature, + 'BITVAVO-ACCESS-TIMESTAMP': timestamp, + 'BITVAVO-ACCESS-WINDOW': accessWindow, + } + if not getOrDelete: + headers['Content-Type'] = 'application/json' + url = self.urls['api'][api] + url + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None # fallback to default error handler + # + # {"errorCode":308,"error":"The signature length is invalid(HMAC-SHA256 should return a 64 length hexadecimal string)."} + # {"errorCode":203,"error":"symbol parameter is required."} + # {"errorCode":205,"error":"symbol parameter is invalid."} + # + errorCode = self.safe_string(response, 'errorCode') + error = self.safe_string(response, 'error') + if errorCode is not None: + feedback = self.id + ' ' + body + self.throw_broadly_matched_exception(self.exceptions['broad'], error, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + raise ExchangeError(feedback) # unknown message + return None + + def calculate_rate_limiter_cost(self, api, method, path, params, config={}): + if ('noMarket' in config) and not ('market' in params): + return config['noMarket'] + return self.safe_value(config, 'cost', 1) diff --git a/ccxt/async_support/blockchaincom.py b/ccxt/async_support/blockchaincom.py new file mode 100644 index 0000000..1046f4a --- /dev/null +++ b/ccxt/async_support/blockchaincom.py @@ -0,0 +1,1219 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.blockchaincom import ImplicitAPI +from ccxt.base.types import Any, Balances, Currency, DepositAddress, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, TradingFees, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import OrderNotFound +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class blockchaincom(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(blockchaincom, self).describe(), { + 'id': 'blockchaincom', + 'secret': None, + 'name': 'Blockchain.com', + 'countries': ['LX'], + 'rateLimit': 500, # prev 1000 + 'version': 'v3', + 'pro': True, + 'has': { + 'CORS': False, + 'spot': True, + 'margin': None, # on exchange but not implemented in CCXT + 'swap': False, + 'future': False, + 'option': False, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'createOrder': True, + 'createStopLimitOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'fetchBalance': True, + 'fetchCanceledOrders': True, + 'fetchClosedOrders': True, + 'fetchCurrencies': False, + 'fetchDeposit': True, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchFundingHistory': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchIndexOHLCV': False, + 'fetchL2OrderBook': True, + 'fetchL3OrderBook': True, + 'fetchLedger': False, + 'fetchMarginMode': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMyTrades': True, + 'fetchOHLCV': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchPositionMode': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTrades': False, + 'fetchTradingFee': False, + 'fetchTradingFees': True, + 'fetchTransfer': False, + 'fetchTransfers': False, + 'fetchWithdrawal': True, + 'fetchWithdrawals': True, + 'fetchWithdrawalWhitelist': True, # fetches exchange specific benficiary-ids needed for withdrawals + 'transfer': False, + 'withdraw': True, + }, + 'timeframes': None, + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/975e3054-3399-4363-bcee-ec3c6d63d4e8', + 'test': { + 'public': 'https://testnet-api.delta.exchange', + 'private': 'https://testnet-api.delta.exchange', + }, + 'api': { + 'public': 'https://api.blockchain.com/v3/exchange', + 'private': 'https://api.blockchain.com/v3/exchange', + }, + 'www': 'https://blockchain.com', + 'doc': [ + 'https://api.blockchain.com/v3', + ], + 'fees': 'https://exchange.blockchain.com/fees', + }, + 'api': { + 'public': { + 'get': { + 'tickers': 1, # fetchTickers + 'tickers/{symbol}': 1, # fetchTicker + 'symbols': 1, # fetchMarkets + 'symbols/{symbol}': 1, # fetchMarket + 'l2/{symbol}': 1, # fetchL2OrderBook + 'l3/{symbol}': 1, # fetchL3OrderBook + }, + }, + 'private': { + 'get': { + 'fees': 1, # fetchFees + 'orders': 1, # fetchOpenOrders, fetchClosedOrders + 'orders/{orderId}': 1, # fetchOrder(id) + 'trades': 1, + 'fills': 1, # fetchMyTrades + 'deposits': 1, # fetchDeposits + 'deposits/{depositId}': 1, # fetchDeposit + 'accounts': 1, # fetchBalance + 'accounts/{account}/{currency}': 1, + 'whitelist': 1, # fetchWithdrawalWhitelist + 'whitelist/{currency}': 1, # fetchWithdrawalWhitelistByCurrency + 'withdrawals': 1, # fetchWithdrawalWhitelist + 'withdrawals/{withdrawalId}': 1, # fetchWithdrawalById + }, + 'post': { + 'orders': 1, # createOrder + 'deposits/{currency}': 1, # fetchDepositAddress by currency(only crypto supported) + 'withdrawals': 1, # withdraw + }, + 'delete': { + 'orders': 1, # cancelOrders + 'orders/{orderId}': 1, # cancelOrder + }, + }, + }, + 'fees': { + 'trading': { + 'feeSide': 'get', + 'tierBased': True, + 'percentage': True, + 'tiers': { + 'taker': [ + [self.parse_number('0'), self.parse_number('0.0045')], + [self.parse_number('10000'), self.parse_number('0.0035')], + [self.parse_number('50000'), self.parse_number('0.0018')], + [self.parse_number('100000'), self.parse_number('0.0018')], + [self.parse_number('500000'), self.parse_number('0.0018')], + [self.parse_number('1000000'), self.parse_number('0.0018')], + [self.parse_number('2500000'), self.parse_number('0.0018')], + [self.parse_number('5000000'), self.parse_number('0.0016')], + [self.parse_number('25000000'), self.parse_number('0.0014')], + [self.parse_number('100000000'), self.parse_number('0.0011')], + [self.parse_number('500000000'), self.parse_number('0.0008')], + [self.parse_number('1000000000'), self.parse_number('0.0006')], + ], + 'maker': [ + [self.parse_number('0'), self.parse_number('0.004')], + [self.parse_number('10000'), self.parse_number('0.0017')], + [self.parse_number('50000'), self.parse_number('0.0015')], + [self.parse_number('100000'), self.parse_number('0.0008')], + [self.parse_number('500000'), self.parse_number('0.0007')], + [self.parse_number('1000000'), self.parse_number('0.0006')], + [self.parse_number('2500000'), self.parse_number('0.0005')], + [self.parse_number('5000000'), self.parse_number('0.0004')], + [self.parse_number('25000000'), self.parse_number('0.0003')], + [self.parse_number('100000000'), self.parse_number('0.0002')], + [self.parse_number('500000000'), self.parse_number('0.0001')], + [self.parse_number('1000000000'), self.parse_number('0')], + ], + }, + }, + }, + 'requiredCredentials': { + 'apiKey': False, + 'secret': True, + }, + 'options': { + 'networks': { + 'ERC20': 'ETH', + 'TRC20': 'TRX', + 'ALGO': 'ALGO', + 'ADA': 'ADA', + 'AR': 'AR', + 'ATOM': 'ATOM', + 'AVAXC': 'AVAX', + 'BCH': 'BCH', + 'BSV': 'BSV', + 'BTC': 'BTC', + # 'BEP20': 'BNB', # todo + 'DCR': 'DCR', + 'DESO': 'DESO', + 'DASH': 'DASH', + 'CELO': 'CELO', + 'CHZ': 'CHZ', + 'MATIC': 'MATIC', + 'SOL': 'SOL', + 'DOGE': 'DOGE', + 'DOT': 'DOT', + 'EOS': 'EOS', + 'ETC': 'ETC', + 'FIL': 'FIL', + 'KAVA': 'KAVA', + 'LTC': 'LTC', + 'IOTA': 'MIOTA', + 'NEAR': 'NEAR', + 'STX': 'STX', + 'XLM': 'XLM', + 'XMR': 'XMR', + 'XRP': 'XRP', + 'XTZ': 'XTZ', + 'ZEC': 'ZEC', + 'ZIL': 'ZIL', + # 'THETA': 'THETA', # todo: possible TFUEL THETA FUEL is also same, but API might have a mistake + # todo: uncomment below after consensus + # 'MOBILECOIN': 'MOB', + # 'KIN': 'KIN', + # 'DIGITALGOLD': 'DGLD', + }, + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': False, + 'GTD': True, # todo implementation + }, + 'hedged': False, + 'leverage': False, + 'marketBuyRequiresPrice': False, + 'marketBuyByCost': False, + 'selfTradePrevention': False, + 'trailing': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 100000, # todo implementation + 'untilDays': 100000, # todo implementation + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'symbolRequired': False, + 'trailing': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 1000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, # todo implement + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 100000, + 'daysBackCanceled': 1, + 'untilDays': 100000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': None, # todo webapi + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'exact': { + '401': AuthenticationError, + '404': OrderNotFound, + }, + 'broad': {}, + }, + }) + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for blockchaincom + + https://api.blockchain.com/v3/#getsymbols + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + # + # "USDC-GBP": { + # "base_currency": "USDC", + # "base_currency_scale": 6, + # "counter_currency": "GBP", + # "counter_currency_scale": 2, + # "min_price_increment": 10000, + # "min_price_increment_scale": 8, + # "min_order_size": 500000000, + # "min_order_size_scale": 8, + # "max_order_size": 0, + # "max_order_size_scale": 8, + # "lot_size": 10000, + # "lot_size_scale": 8, + # "status": "open", + # "id": 68, + # "auction_price": 0, + # "auction_size": 0, + # "auction_time": "", + # "imbalance": 0 + # } + # + markets = await self.publicGetSymbols(params) + marketIds = list(markets.keys()) + result = [] + for i in range(0, len(marketIds)): + marketId = marketIds[i] + market = self.safe_value(markets, marketId) + baseId = self.safe_string(market, 'base_currency') + quoteId = self.safe_string(market, 'counter_currency') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + numericId = self.safe_number(market, 'id') + active = None + marketState = self.safe_string(market, 'status') + if marketState == 'open': + active = True + else: + active = False + # price precision + minPriceIncrementString = self.safe_string(market, 'min_price_increment') + minPriceIncrementScaleString = self.safe_string(market, 'min_price_increment_scale') + minPriceScalePrecisionString = self.parse_precision(minPriceIncrementScaleString) + pricePrecisionString = Precise.string_mul(minPriceIncrementString, minPriceScalePrecisionString) + # amount precision + lotSizeString = self.safe_string(market, 'lot_size') + lotSizeScaleString = self.safe_string(market, 'lot_size_scale') + lotSizeScalePrecisionString = self.parse_precision(lotSizeScaleString) + amountPrecisionString = Precise.string_mul(lotSizeString, lotSizeScalePrecisionString) + # minimum order size + minOrderSizeString = self.safe_string(market, 'min_order_size') + minOrderSizeScaleString = self.safe_string(market, 'min_order_size_scale') + minOrderSizeScalePrecisionString = self.parse_precision(minOrderSizeScaleString) + minOrderSizePreciseString = Precise.string_mul(minOrderSizeString, minOrderSizeScalePrecisionString) + minOrderSize = self.parse_number(minOrderSizePreciseString) + # maximum order size + maxOrderSize = None + maxOrderSize = self.safe_string(market, 'max_order_size') + if maxOrderSize != '0': + maxOrderSizeScaleString = self.safe_string(market, 'max_order_size_scale') + maxOrderSizeScalePrecisionString = self.parse_precision(maxOrderSizeScaleString) + maxOrderSizeString = Precise.string_mul(maxOrderSize, maxOrderSizeScalePrecisionString) + maxOrderSize = self.parse_number(maxOrderSizeString) + else: + maxOrderSize = None + result.append({ + 'info': market, + 'id': marketId, + 'numericId': numericId, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': active, + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(amountPrecisionString), + 'price': self.parse_number(pricePrecisionString), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': minOrderSize, + 'max': maxOrderSize, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + }) + return result + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://api.blockchain.com/v3/#getl3orderbook + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + return await self.fetch_l3_order_book(symbol, limit, params) + + async def fetch_l3_order_book(self, symbol: str, limit: Int = None, params={}): + """ + fetches level 3 information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://api.blockchain.com/v3/#getl3orderbook + + :param str symbol: unified market symbol + :param int [limit]: max number of orders to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order book structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['depth'] = limit + response = await self.publicGetL3Symbol(self.extend(request, params)) + return self.parse_order_book(response, market['symbol'], None, 'bids', 'asks', 'px', 'qty') + + async def fetch_l2_order_book(self, symbol: str, limit: Int = None, params={}): + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['depth'] = limit + response = await self.publicGetL2Symbol(self.extend(request, params)) + return self.parse_order_book(response, market['symbol'], None, 'bids', 'asks', 'px', 'qty') + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "symbol": "BTC-USD", + # "price_24h": 47791.86, + # "volume_24h": 362.88635738, + # "last_trade_price": 47587.75 + # } + # + marketId = self.safe_string(ticker, 'symbol') + symbol = self.safe_symbol(marketId, market, '-') + last = self.safe_string(ticker, 'last_trade_price') + baseVolume = self.safe_string(ticker, 'volume_24h') + open = self.safe_string(ticker, 'price_24h') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': None, + 'datetime': None, + 'high': None, + 'low': None, + 'bid': None, + 'bidVolume': None, + 'ask': None, + 'askVolume': None, + 'vwap': None, + 'open': open, + 'close': None, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': None, + 'info': ticker, + }, market) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://api.blockchain.com/v3/#gettickerbysymbol + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = await self.publicGetTickersSymbol(self.extend(request, params)) + return self.parse_ticker(response, market) + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://api.blockchain.com/v3/#gettickers + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + tickers = await self.publicGetTickers(params) + return self.parse_tickers(tickers, symbols) + + def parse_order_state(self, state): + states: dict = { + 'OPEN': 'open', + 'REJECTED': 'rejected', + 'FILLED': 'closed', + 'CANCELED': 'canceled', + 'PART_FILLED': 'open', + 'EXPIRED': 'expired', + } + return self.safe_string(states, state, state) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # { + # "clOrdId": "00001", + # "ordType": "MARKET", + # "ordStatus": "FILLED", + # "side": "BUY", + # "symbol": "USDC-USDT", + # "exOrdId": "281775861306290", + # "price": null, + # "text": "Fill", + # "lastShares": "30.0", + # "lastPx": "0.9999", + # "leavesQty": "0.0", + # "cumQty": "30.0", + # "avgPx": "0.9999", + # "timestamp": "1633940339619" + # } + # + clientOrderId = self.safe_string(order, 'clOrdId') + type = self.safe_string_lower(order, 'ordType') + statusId = self.safe_string(order, 'ordStatus') + state = self.parse_order_state(statusId) + side = self.safe_string_lower(order, 'side') + marketId = self.safe_string(order, 'symbol') + symbol = self.safe_symbol(marketId, market, '-') + exchangeOrderId = self.safe_string(order, 'exOrdId') + price = self.safe_string(order, 'price') if (type != 'market') else None + average = self.safe_number(order, 'avgPx') + timestamp = self.safe_integer(order, 'timestamp') + datetime = self.iso8601(timestamp) + filled = self.safe_string(order, 'cumQty') + remaining = self.safe_string(order, 'leavesQty') + result = self.safe_order({ + 'id': exchangeOrderId, + 'clientOrderId': clientOrderId, + 'datetime': datetime, + 'timestamp': timestamp, + 'lastTradeTimestamp': None, + 'status': state, + 'symbol': symbol, + 'type': type, + 'timeInForce': None, + 'side': side, + 'price': price, + 'average': average, + 'amount': None, + 'filled': filled, + 'remaining': remaining, + 'cost': None, + 'trades': [], + 'fees': [], + 'info': order, + }) + return result + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://api.blockchain.com/v3/#createorder + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + orderType = self.safe_string(params, 'ordType', type) + uppercaseOrderType = orderType.upper() + clientOrderId = self.safe_string_2(params, 'clientOrderId', 'clOrdId', self.uuid16()) + params = self.omit(params, ['ordType', 'clientOrderId', 'clOrdId']) + request: dict = { + # 'stopPx' : limit price + # 'timeInForce' : "GTC" for Good Till Cancel, "IOC" for Immediate or Cancel, "FOK" for Fill or Kill, "GTD" Good Till Date + # 'expireDate' : expiry date in the format YYYYMMDD + # 'minQty' : The minimum quantity required for an IOC fill + 'ordType': uppercaseOrderType, + 'symbol': market['id'], + 'side': side.upper(), + 'orderQty': self.amount_to_precision(symbol, amount), + 'clOrdId': clientOrderId, + } + triggerPrice = self.safe_value_n(params, ['triggerPrice', 'stopPx', 'stopPrice']) + params = self.omit(params, ['triggerPrice', 'stopPx', 'stopPrice']) + if uppercaseOrderType == 'STOP' or uppercaseOrderType == 'STOPLIMIT': + if triggerPrice is None: + raise ArgumentsRequired(self.id + ' createOrder() requires a stopPx or triggerPrice param for a ' + uppercaseOrderType + ' order') + if triggerPrice is not None: + if uppercaseOrderType == 'MARKET': + request['ordType'] = 'STOP' + elif uppercaseOrderType == 'LIMIT': + request['ordType'] = 'STOPLIMIT' + priceRequired = False + stopPriceRequired = False + if request['ordType'] == 'LIMIT' or request['ordType'] == 'STOPLIMIT': + priceRequired = True + if request['ordType'] == 'STOP' or request['ordType'] == 'STOPLIMIT': + stopPriceRequired = True + if priceRequired: + request['price'] = self.price_to_precision(symbol, price) + if stopPriceRequired: + request['stopPx'] = self.price_to_precision(symbol, triggerPrice) + response = await self.privatePostOrders(self.extend(request, params)) + return self.parse_order(response, market) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://api.blockchain.com/v3/#deleteorder + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + request: dict = { + 'orderId': id, + } + response = await self.privateDeleteOrdersOrderId(self.extend(request, params)) + return self.safe_order({ + 'id': id, + 'info': response, + }) + + async def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders + + https://api.blockchain.com/v3/#deleteallorders + + :param str symbol: unified market symbol of the market to cancel orders in, all markets are used if None, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an list of `order structures ` + """ + # cancels all open orders if no symbol specified + # cancels all open orders of specified symbol, if symbol is specified + await self.load_markets() + request: dict = { + # 'symbol': marketId, + } + if symbol is not None: + marketId = self.market_id(symbol) + request['symbol'] = marketId + response = await self.privateDeleteOrders(self.extend(request, params)) + # + # {} + # + return [ + self.safe_order({ + 'info': response, + }), + ] + + async def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://api.blockchain.com/v3/#getfees + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + await self.load_markets() + response = await self.privateGetFees(params) + # + # { + # "makerRate": "0.002", + # "takerRate": "0.004", + # "volumeInUSD": "0.0" + # } + # + makerFee = self.safe_number(response, 'makerRate') + takerFee = self.safe_number(response, 'takerRate') + result: dict = {} + for i in range(0, len(self.symbols)): + symbol = self.symbols[i] + result[symbol] = { + 'info': response, + 'symbol': symbol, + 'maker': makerFee, + 'taker': takerFee, + } + return result + + async def fetch_canceled_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches information on multiple canceled orders made by the user + + https://api.blockchain.com/v3/#getorders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: timestamp in ms of the earliest order, default is None + :param int [limit]: max number of orders to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `order structures ` + """ + state = 'CANCELED' + return await self.fetch_orders_by_state(state, symbol, since, limit, params) + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://api.blockchain.com/v3/#getorders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + state = 'FILLED' + return await self.fetch_orders_by_state(state, symbol, since, limit, params) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://api.blockchain.com/v3/#getorders + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + state = 'OPEN' + return await self.fetch_orders_by_state(state, symbol, since, limit, params) + + async def fetch_orders_by_state(self, state, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + await self.load_markets() + request: dict = { + # 'to': unix epoch ms + # 'from': unix epoch ms + 'status': state, + 'limit': 100, + } + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + response = await self.privateGetOrders(self.extend(request, params)) + return self.parse_orders(response, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # { + # "exOrdId":281685751028507, + # "tradeId":281685434947633, + # "execId":8847494003, + # "side":"BUY", + # "symbol":"AAVE-USDT", + # "price":405.34, + # "qty":0.1, + # "fee":0.162136, + # "timestamp":1634559249687 + # } + # + orderId = self.safe_string(trade, 'exOrdId') + tradeId = self.safe_string(trade, 'tradeId') + side = self.safe_string(trade, 'side').lower() + marketId = self.safe_string(trade, 'symbol') + priceString = self.safe_string(trade, 'price') + amountString = self.safe_string(trade, 'qty') + timestamp = self.safe_integer(trade, 'timestamp') + datetime = self.iso8601(timestamp) + market = self.safe_market(marketId, market, '-') + symbol = market['symbol'] + fee = None + feeCostString = self.safe_string(trade, 'fee') + if feeCostString is not None: + feeCurrency = market['quote'] + fee = {'cost': feeCostString, 'currency': feeCurrency} + return self.safe_trade({ + 'id': tradeId, + 'timestamp': timestamp, + 'datetime': datetime, + 'symbol': symbol, + 'order': orderId, + 'type': None, + 'side': side, + 'takerOrMaker': None, + 'price': priceString, + 'amount': amountString, + 'cost': None, + 'fee': fee, + 'info': trade, + }, market) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://api.blockchain.com/v3/#getfills + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + request: dict = {} + if limit is not None: + request['limit'] = limit + market = None + if symbol is not None: + request['symbol'] = self.market_id(symbol) + market = self.market(symbol) + trades = await self.privateGetFills(self.extend(request, params)) + return self.parse_trades(trades, market, since, limit, params) # need to define + + async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://api.blockchain.com/v3/#getdepositaddress + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + } + response = await self.privatePostDepositsCurrency(self.extend(request, params)) + rawAddress = self.safe_string(response, 'address') + tag = None + address = None + if rawAddress is not None: + addressParts = rawAddress.split(';') + # if a tag or memo is used it is separated by a colon in the 'address' value + tag = self.safe_string(addressParts, 0) + address = self.safe_string(addressParts, 1) + return { + 'info': response, + 'currency': currency['code'], + 'network': None, + 'address': address, + 'tag': tag, + } + + def parse_transaction_state(self, state): + states: dict = { + 'COMPLETED': 'ok', # + 'REJECTED': 'failed', + 'PENDING': 'pending', + 'FAILED': 'failed', + 'REFUNDED': 'refunded', + } + return self.safe_string(states, state, state) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # deposit + # + # { + # "depositId":"748e9180-be0d-4a80-e175-0156150efc95", + # "amount":0.009, + # "currency":"ETH", + # "address":"0xEC6B5929D454C8D9546d4221ace969E1810Fa92c", + # "state":"COMPLETED", + # "txHash":"582114562140e51a80b481c2dfebaf62b4ab9769b8ff54820bb67e34d4a3ab0c", + # "timestamp":1633697196241 + # } + # + # withdrawal + # + # { + # "amount":30.0, + # "currency":"USDT", + # "beneficiary":"cab00d11-6e7f-46b7-b453-2e8ef6f101fa", # blockchain specific id + # "withdrawalId":"99df5ef7-eab6-4033-be49-312930fbd1ea", + # "fee":34.005078, + # "state":"COMPLETED", + # "timestamp":1634218452549 + # } + # + type = None + id = None + amount = self.safe_number(transaction, 'amount') + timestamp = self.safe_integer(transaction, 'timestamp') + currencyId = self.safe_string(transaction, 'currency') + code = self.safe_currency_code(currencyId, currency) + state = self.safe_string(transaction, 'state') + if 'depositId' in transaction: + type = 'deposit' + id = self.safe_string(transaction, 'depositId') + elif 'withdrawalId' in transaction: + type = 'withdrawal' + id = self.safe_string(transaction, 'withdrawalId') + feeCost = self.safe_number(transaction, 'fee') if (type == 'withdrawal') else None + fee = None + if feeCost is not None: + fee = {'currency': code, 'cost': feeCost} + address = self.safe_string(transaction, 'address') + txid = self.safe_string(transaction, 'txhash') + return { + 'info': transaction, + 'id': id, + 'txid': txid, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': None, + 'addressFrom': None, + 'address': address, + 'addressTo': address, + 'tagFrom': None, + 'tag': None, + 'tagTo': None, + 'type': type, + 'amount': amount, + 'currency': code, + 'status': self.parse_transaction_state(state), # 'status': 'pending', # 'ok', 'failed', 'canceled', string + 'updated': None, + 'comment': None, + 'internal': None, + 'fee': fee, + } + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://api.blockchain.com/v3/#createwithdrawal + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'amount': amount, + 'currency': currency['id'], + 'beneficiary': address, + 'sendMax': False, + } + response = await self.privatePostWithdrawals(self.extend(request, params)) + # + # { + # "amount": "30.0", + # "currency": "USDT", + # "beneficiary": "adcd43fb-9ba6-41f7-8c0d-7013482cb88f", + # "withdrawalId": "99df5ef7-eab6-4033-be49-312930fbd1ea", + # "fee": "34.005078", + # "state": "PENDING", + # "timestamp": "1634218452595" + # }, + # + return self.parse_transaction(response, currency) + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://api.blockchain.com/v3/#getwithdrawals + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + request: dict = { + # 'from' : integer timestamp in ms + # 'to' : integer timestamp in ms + } + if since is not None: + request['from'] = since + currency = None + if code is not None: + currency = self.currency(code) + response = await self.privateGetWithdrawals(self.extend(request, params)) + return self.parse_transactions(response, currency, since, limit) + + async def fetch_withdrawal(self, id: str, code: Str = None, params={}): + """ + fetch data on a currency withdrawal via the withdrawal id + + https://api.blockchain.com/v3/#getwithdrawalbyid + + :param str id: withdrawal id + :param str code: not used by blockchaincom.fetchWithdrawal + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + await self.load_markets() + request: dict = { + 'withdrawalId': id, + } + response = await self.privateGetWithdrawalsWithdrawalId(self.extend(request, params)) + return self.parse_transaction(response) + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://api.blockchain.com/v3/#getdeposits + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + request: dict = { + # 'from' : integer timestamp in ms + # 'to' : integer timestap in ms + } + if since is not None: + request['from'] = since + currency = None + if code is not None: + currency = self.currency(code) + response = await self.privateGetDeposits(self.extend(request, params)) + return self.parse_transactions(response, currency, since, limit) + + async def fetch_deposit(self, id: str, code: Str = None, params={}): + """ + fetch information on a deposit + + https://api.blockchain.com/v3/#getdepositbyid + + :param str id: deposit id + :param str code: not used by blockchaincom fetchDeposit() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + await self.load_markets() + depositId = self.safe_string(params, 'depositId', id) + request: dict = { + 'depositId': depositId, + } + deposit = await self.privateGetDepositsDepositId(self.extend(request, params)) + return self.parse_transaction(deposit) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://api.blockchain.com/v3/#getaccounts + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + accountName = self.safe_string(params, 'account', 'primary') + params = self.omit(params, 'account') + request: dict = { + 'account': accountName, + } + response = await self.privateGetAccounts(self.extend(request, params)) + # + # { + # "primary": [ + # { + # "currency":"ETH", + # "balance":0.009, + # "available":0.009, + # "balance_local":30.82869, + # "available_local":30.82869, + # "rate":3425.41 + # }, + # ... + # ] + # } + # + balances = self.safe_value(response, accountName) + if balances is None: + raise ExchangeError(self.id + ' fetchBalance() could not find the "' + accountName + '" account') + result: dict = {'info': response} + for i in range(0, len(balances)): + entry = balances[i] + currencyId = self.safe_string(entry, 'currency') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(entry, 'available') + account['total'] = self.safe_string(entry, 'balance') + result[code] = account + return self.safe_balance(result) + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://api.blockchain.com/v3/#getorderbyid + + :param str id: the order id + :param str symbol: not used by blockchaincom fetchOrder + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + # note: only works with exchange-order-id + # does not work with clientOrderId + await self.load_markets() + request: dict = { + 'orderId': id, + } + response = await self.privateGetOrdersOrderId(self.extend(request, params)) + # + # { + # "exOrdId": 11111111, + # "clOrdId": "ABC", + # "ordType": "MARKET", + # "ordStatus": "FILLED", + # "side": "BUY", + # "price": 0.12345, + # "text": "string", + # "symbol": "BTC-USD", + # "lastShares": 0.5678, + # "lastPx": 3500.12, + # "leavesQty": 10, + # "cumQty": 0.123345, + # "avgPx": 345.33, + # "timestamp": 1592830770594 + # } + # + return self.parse_order(response) + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + requestPath = '/' + self.implode_params(path, params) + url = self.urls['api'][api] + requestPath + query = self.omit(params, self.extract_params(path)) + if api == 'public': + if query: + url += '?' + self.urlencode(query) + elif api == 'private': + self.check_required_credentials() + headers = { + 'X-API-Token': self.secret, + } + if (method == 'GET'): + if query: + url += '?' + self.urlencode(query) + else: + body = self.json(query) + headers['Content-Type'] = 'application/json' + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + # {"timestamp":"2021-10-21T15:13:58.837+00:00","status":404,"error":"Not Found","message":"","path":"/orders/505050" + if response is None: + return None + text = self.safe_string(response, 'text') + if text is not None: # if trade currency account is empty returns 200 with rejected order + if text == 'Insufficient Balance': + raise InsufficientFunds(self.id + ' ' + body) + errorCode = self.safe_string(response, 'status') + errorMessage = self.safe_string(response, 'error') + if code is not None: + feedback = self.id + ' ' + self.json(response) + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], errorMessage, feedback) + return None diff --git a/ccxt/async_support/blofin.py b/ccxt/async_support/blofin.py new file mode 100644 index 0000000..634aafd --- /dev/null +++ b/ccxt/async_support/blofin.py @@ -0,0 +1,2451 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.blofin import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currency, Int, LedgerEntry, Leverage, Leverages, MarginMode, Market, Num, Order, OrderBook, OrderRequest, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, Trade, TradingFeeInterface, Transaction, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class blofin(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(blofin, self).describe(), { + 'id': 'blofin', + 'name': 'BloFin', + 'countries': ['US'], + 'version': 'v1', + 'rateLimit': 100, + 'pro': True, + 'has': { + 'CORS': None, + 'spot': False, + 'margin': False, + 'swap': True, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowMargin': False, + 'cancelAllOrders': False, + 'cancelOrder': True, + 'cancelOrders': True, + 'closeAllPositions': False, + 'closePosition': True, + 'createDepositAddress': False, + 'createMarketBuyOrderWithCost': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createOrders': True, + 'createOrderWithTakeProfitAndStopLoss': True, + 'createPostOnlyOrder': False, + 'createReduceOnlyOrder': False, + 'createStopLimitOrder': False, + 'createStopLossOrder': True, + 'createStopMarketOrder': False, + 'createStopOrder': False, + 'createTakeProfitOrder': True, + 'createTriggerOrder': True, + 'editOrder': False, + 'fetchAccounts': False, + 'fetchBalance': True, + 'fetchBidsAsks': None, + 'fetchBorrowInterest': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchCanceledOrders': False, + 'fetchClosedOrder': False, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': False, + 'fetchDeposit': False, + 'fetchDepositAddress': False, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchDepositsWithdrawals': False, + 'fetchDepositWithdrawFee': 'emulated', + 'fetchDepositWithdrawFees': False, + 'fetchFundingHistory': True, + 'fetchFundingRate': True, + 'fetchFundingRateHistory': True, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchL3OrderBook': False, + 'fetchLedger': True, + 'fetchLedgerEntry': None, + 'fetchLeverage': True, + 'fetchLeverages': True, + 'fetchLeverageTiers': False, + 'fetchMarginMode': True, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenOrder': None, + 'fetchOpenOrders': True, + 'fetchOrder': None, + 'fetchOrderBook': True, + 'fetchOrderBooks': False, + 'fetchOrders': False, + 'fetchOrderTrades': True, + 'fetchPosition': True, + 'fetchPositionMode': True, + 'fetchPositions': True, + 'fetchPositionsForSymbol': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchStatus': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': False, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': False, + 'fetchTradingLimits': False, + 'fetchTransactionFee': False, + 'fetchTransactionFees': False, + 'fetchTransactions': False, + 'fetchTransfer': False, + 'fetchTransfers': False, + 'fetchUnderlyingAssets': False, + 'fetchVolatilityHistory': False, + 'fetchWithdrawal': False, + 'fetchWithdrawals': True, + 'fetchWithdrawalWhitelist': False, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'setLeverage': True, + 'setMargin': False, + 'setMarginMode': True, + 'setPositionMode': True, + 'signIn': False, + 'transfer': True, + 'withdraw': False, + }, + 'timeframes': { + '1m': '1m', + '3m': '3m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1H', + '2h': '2H', + '4h': '4H', + '6h': '6H', + '8h': '8H', + '12h': '12H', + '1d': '1D', + '3d': '3D', + '1w': '1W', + '1M': '1M', + }, + 'hostname': 'www.blofin.com', + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/518cdf80-f05d-4821-a3e3-d48ceb41d73b', + 'api': { + 'rest': 'https://openapi.blofin.com', + }, + 'test': { + 'rest': 'https://demo-trading-openapi.blofin.com', + }, + 'referral': { + 'url': 'https://blofin.com/register?referral_code=f79EsS', + 'discount': 0.05, + }, + 'www': 'https://www.blofin.com', + 'doc': 'https://blofin.com/docs', + }, + 'api': { + 'public': { + 'get': { + 'market/instruments': 1, + 'market/tickers': 1, + 'market/books': 1, + 'market/trades': 1, + 'market/candles': 1, + 'market/mark-price': 1, + 'market/funding-rate': 1, + 'market/funding-rate-history': 1, + }, + }, + 'private': { + 'get': { + 'asset/balances': 1, + 'trade/orders-pending': 1, + 'trade/fills-history': 1, + 'asset/deposit-history': 1, + 'asset/withdrawal-history': 1, + 'asset/bills': 1, + 'account/balance': 1, + 'account/positions': 1, + 'account/leverage-info': 1, + 'account/margin-mode': 1, + 'account/position-mode': 1, + 'account/batch-leverage-info': 1, + 'trade/orders-tpsl-pending': 1, + 'trade/orders-algo-pending': 1, + 'trade/orders-history': 1, + 'trade/orders-tpsl-history': 1, + 'trade/orders-algo-history': 1, # todo new + 'trade/order/price-range': 1, + 'user/query-apikey': 1, + 'affiliate/basic': 1, + 'copytrading/instruments': 1, + 'copytrading/account/balance': 1, + 'copytrading/account/positions-by-order': 1, + 'copytrading/account/positions-details-by-order': 1, + 'copytrading/account/positions-by-contract': 1, + 'copytrading/account/position-mode': 1, + 'copytrading/account/leverage-info': 1, + 'copytrading/trade/orders-pending': 1, + 'copytrading/trade/pending-tpsl-by-contract': 1, + 'copytrading/trade/position-history-by-order': 1, + 'copytrading/trade/orders-history': 1, + 'copytrading/trade/pending-tpsl-by-order': 1, + }, + 'post': { + 'account/set-margin-mode': 1, + 'account/set-position-mode': 1, + 'trade/order': 1, + 'trade/order-algo': 1, + 'trade/cancel-order': 1, + 'trade/cancel-algo': 1, + 'account/set-leverage': 1, + 'trade/batch-orders': 1, + 'trade/order-tpsl': 1, + 'trade/cancel-batch-orders': 1, + 'trade/cancel-tpsl': 1, + 'trade/close-position': 1, + 'asset/transfer': 1, + 'copytrading/account/set-position-mode': 1, + 'copytrading/account/set-leverage': 1, + 'copytrading/trade/place-order': 1, + 'copytrading/trade/cancel-order': 1, + 'copytrading/trade/place-tpsl-by-contract': 1, + 'copytrading/trade/cancel-tpsl-by-contract': 1, + 'copytrading/trade/place-tpsl-by-order': 1, + 'copytrading/trade/cancel-tpsl-by-order': 1, + 'copytrading/trade/close-position-by-order': 1, + 'copytrading/trade/close-position-by-contract': 1, + }, + }, + }, + 'fees': { + 'swap': { + 'taker': self.parse_number('0.00060'), + 'maker': self.parse_number('0.00020'), + }, + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + 'password': True, + }, + 'features': { + 'default': { + 'sandbox': False, + 'createOrder': { + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'leverage': False, + 'marketBuyRequiresPrice': False, + 'marketBuyByCost': False, + 'selfTradePrevention': False, + 'trailing': False, + 'iceberg': False, + }, + 'createOrders': { + 'max': 10, + }, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 100000, + 'untilDays': 100000, + 'symbolRequired': False, + }, + 'fetchOrder': None, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 100, + 'trigger': True, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 100000, + 'daysBackCanceled': 1, + 'untilDays': 100000, + 'trigger': True, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 1440, + }, + }, + 'spot': { + 'extends': 'default', + 'createOrder': { + 'marginMode': False, + 'triggerPrice': False, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'hedged': False, + }, + }, + 'forDerivatives': { + 'extends': 'default', + 'createOrder': { + 'marginMode': True, + 'triggerPrice': False, # todo + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': True, + 'takeProfitPrice': True, + 'attachedStopLossTakeProfit': { + 'triggerPriceType': None, + 'price': True, + }, + 'hedged': True, + }, + }, + 'swap': { + 'linear': { + 'extends': 'forDerivatives', + }, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'exceptions': { + 'exact': { + '400': BadRequest, # Body can not be empty + '401': AuthenticationError, # Invalid signature + '500': ExchangeError, # Internal Server Error + '404': BadRequest, # not found + '405': BadRequest, # Method Not Allowed + '406': BadRequest, # Not Acceptable + '429': RateLimitExceeded, # Too Many Requests + '152001': BadRequest, # Parameter {} cannot be empty + '152002': BadRequest, # Parameter {} error + '152003': BadRequest, # Either parameter {} or {} is required + '152004': BadRequest, # JSON syntax error + '152005': BadRequest, # Parameter error: wrong or empty + '152006': InvalidOrder, # Batch orders can be placed for up to 20 at once + '152007': InvalidOrder, # Batch orders can only be placed with the same instId and marginMode + '152008': InvalidOrder, # Only the same field is allowed for bulk cancellation of orders, orderId is preferred + '152009': InvalidOrder, # {} must be a combination of numbers, letters, or underscores, and the maximum length of characters is 32 + '150003': InvalidOrder, # clientId already exist + '150004': InvalidOrder, # Insufficient balance. please adjust the amount and try again + '542': InvalidOrder, # Exceeded the maximum order size limit + '102002': InvalidOrder, # Duplicate customized order ID + '102005': InvalidOrder, # Position had been closed + '102014': InvalidOrder, # Limit order exceeds maximum order size limit + '102015': InvalidOrder, # Market order exceeds maximum order size limit + '102022': InvalidOrder, # Failed to place order. You don’t have any positions of self contract. Turn off Reduce-only to continue. + '102037': InvalidOrder, # TP trigger price should be higher than the latest trading price + '102038': InvalidOrder, # SL trigger price should be lower than the latest trading price + '102039': InvalidOrder, # TP trigger price should be lower than the latest trading price + '102040': InvalidOrder, # SL trigger price should be higher than the latest trading price + '102047': InvalidOrder, # Stop loss trigger price should be higher than the order price + '102048': InvalidOrder, # stop loss trigger price must be higher than the best bid price + '102049': InvalidOrder, # Take profit trigger price should be lower than the order price + '102050': InvalidOrder, # stop loss trigger price must be lower than the best ask price + '102051': InvalidOrder, # stop loss trigger price should be lower than the order price + '102052': InvalidOrder, # take profit trigger price should be higher than the order price + '102053': InvalidOrder, # take profit trigger price should be lower than the best bid price + '102054': InvalidOrder, # take profit trigger price should be higher than the best ask price + '102055': InvalidOrder, # stop loss trigger price should be lower than the best ask price + '102064': BadRequest, # Buy price is not within the price limit(Minimum: 310.40; Maximum:1,629.40) + '102065': BadRequest, # Sell price is not within the price limit + '102068': BadRequest, # Cancel failed order has been filled, triggered, canceled or does not exist + '103013': ExchangeError, # Internal error; unable to process your request. Please try again. + 'Order failed. Insufficient USDT margin in account': InsufficientFunds, # Insufficient USDT margin in account + }, + 'broad': { + 'Internal Server Error': ExchangeNotAvailable, # {"code":500,"data":{},"detailMsg":"","error_code":"500","error_message":"Internal Server Error","msg":"Internal Server Error"} + 'server error': ExchangeNotAvailable, # {"code":500,"data":{},"detailMsg":"","error_code":"500","error_message":"server error 1236805249","msg":"server error 1236805249"} + }, + }, + 'httpExceptions': { + '429': ExchangeNotAvailable, # https://github.com/ccxt/ccxt/issues/9612 + }, + 'precisionMode': TICK_SIZE, + 'options': { + 'brokerId': 'ec6dd3a7dd982d0b', + 'accountsByType': { + 'swap': 'futures', + 'funding': 'funding', + 'future': 'futures', + 'copy_trading': 'copy_trading', + 'earn': 'earn', + 'spot': 'spot', + }, + 'accountsById': { + 'funding': 'funding', + 'futures': 'swap', + 'copy_trading': 'copy_trading', + 'earn': 'earn', + 'spot': 'spot', + }, + 'defaultNetwork': 'ERC20', + 'defaultNetworks': { + 'ETH': 'ERC20', + 'BTC': 'BTC', + 'USDT': 'TRC20', + }, + 'networks': { + 'BTC': 'Bitcoin', + 'BEP20': 'BSC', + 'ERC20': 'ERC20', + 'TRC20': 'TRC20', + }, + 'fetchOpenInterestHistory': { + 'timeframes': { + '5m': '5m', + '1h': '1H', + '8h': '8H', + '1d': '1D', + '5M': '5m', + '1H': '1H', + '8H': '8H', + '1D': '1D', + }, + }, + 'fetchOHLCV': { + # 'type': 'Candles', # Candles or HistoryCandles, IndexCandles, MarkPriceCandles + 'timezone': 'UTC', # UTC, HK + }, + 'fetchPositions': { + 'method': 'privateGetAccountPositions', # privateGetAccountPositions or privateGetAccountPositionsHistory + }, + 'createOrder': 'privatePostTradeOrder', # or 'privatePostTradeOrderTpsl' + 'createMarketBuyOrderRequiresPrice': False, + 'fetchMarkets': ['swap'], + 'defaultType': 'swap', + 'fetchLedger': { + 'method': 'privateGetAssetBills', + }, + 'fetchOpenOrders': { + 'method': 'privateGetTradeOrdersPending', + }, + 'cancelOrders': { + 'method': 'privatePostTradeCancelBatchOrders', + }, + 'fetchCanceledOrders': { + 'method': 'privateGetTradeOrdersHistory', # privateGetTradeOrdersTpslHistory + }, + 'fetchClosedOrders': { + 'method': 'privateGetTradeOrdersHistory', # privateGetTradeOrdersTpslHistory + }, + 'withdraw': { + # a funding password credential is required by the exchange for the + # withdraw call(not to be confused with the api password credential) + 'password': None, + 'pwd': None, # password or pwd both work + }, + 'exchangeType': { + 'spot': 'SPOT', + 'swap': 'SWAP', + 'SPOT': 'SPOT', + 'SWAP': 'SWAP', + }, + }, + }) + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for blofin + + https://blofin.com/docs#get-instruments + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = await self.publicGetMarketInstruments(params) + data = self.safe_list(response, 'data', []) + return self.parse_markets(data) + + def parse_market(self, market: dict) -> Market: + id = self.safe_string(market, 'instId') + type = self.safe_string_lower(market, 'instType') + spot = (type == 'spot') + future = (type == 'future') + swap = (type == 'swap') + option = (type == 'option') + contract = swap or future + baseId = self.safe_string(market, 'baseCurrency') + quoteId = self.safe_string(market, 'quoteCurrency') + settleId = self.safe_string(market, 'quoteCurrency') + settle = self.safe_currency_code(settleId) + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + symbol = base + '/' + quote + if swap: + symbol = symbol + ':' + settle + expiry = None + strikePrice = None + optionType = None + tickSize = self.safe_string(market, 'tickSize') + fees = self.safe_dict_2(self.fees, type, 'trading', {}) + taker = self.safe_number(fees, 'taker') + maker = self.safe_number(fees, 'maker') + maxLeverage = self.safe_string(market, 'maxLeverage', '100') + maxLeverage = Precise.string_max(maxLeverage, '1') + isActive = (self.safe_string(market, 'state') == 'live') + return self.safe_market_structure({ + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'baseId': baseId, + 'quoteId': quoteId, + 'settle': settle, + 'settleId': settleId, + 'type': type, + 'spot': spot, + 'option': option, + 'margin': spot and (Precise.string_gt(maxLeverage, '1')), + 'swap': swap, + 'future': future, + 'active': isActive, + 'taker': taker, + 'maker': maker, + 'contract': contract, + 'linear': (quoteId == settleId) if contract else None, + 'inverse': (baseId == settleId) if contract else None, + 'contractSize': self.safe_number(market, 'contractValue') if contract else None, + 'expiry': expiry, + 'expiryDatetime': expiry, + 'strike': strikePrice, + 'optionType': optionType, + 'created': self.safe_integer(market, 'listTime'), + 'precision': { + 'amount': self.safe_number(market, 'lotSize'), + 'price': self.parse_number(tickSize), + }, + 'limits': { + 'leverage': { + 'min': self.parse_number('1'), + 'max': self.parse_number(maxLeverage), + }, + 'amount': { + 'min': self.safe_number(market, 'minSize'), + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'info': market, + }) + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://blofin.com/docs#get-order-book + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + } + limit = 50 if (limit is None) else limit + if limit is not None: + request['size'] = limit # max 100 + response = await self.publicGetMarketBooks(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "asks": [ + # ["0.07228","4.211619","0","2"], # price, amount, liquidated orders, total open orders + # ["0.0723","299.880364","0","2"], + # ["0.07231","3.72832","0","1"], + # ], + # "bids": [ + # ["0.07221","18.5","0","1"], + # ["0.0722","18.5","0","1"], + # ["0.07219","0.505407","0","1"], + # ], + # "ts": "1621438475342" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + first = self.safe_dict(data, 0, {}) + timestamp = self.safe_integer(first, 'ts') + return self.parse_order_book(first, symbol, timestamp) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # response similar for REST & WS + # + # { + # instId: "ADA-USDT", + # ts: "1707736811486", + # last: "0.5315", + # lastSize: "4", + # askPrice: "0.5318", + # askSize: "248", + # bidPrice: "0.5315", + # bidSize: "63", + # open24h: "0.5555", + # high24h: "0.5563", + # low24h: "0.5315", + # volCurrency24h: "198560100", + # vol24h: "1985601", + # } + # + timestamp = self.safe_integer(ticker, 'ts') + marketId = self.safe_string(ticker, 'instId') + market = self.safe_market(marketId, market, '-') + symbol = market['symbol'] + last = self.safe_string(ticker, 'last') + open = self.safe_string(ticker, 'open24h') + spot = self.safe_bool(market, 'spot', False) + quoteVolume = self.safe_string(ticker, 'volCurrency24h') if spot else None + baseVolume = self.safe_string(ticker, 'vol24h') + high = self.safe_string(ticker, 'high24h') + low = self.safe_string(ticker, 'low24h') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': high, + 'low': low, + 'bid': self.safe_string(ticker, 'bidPrice'), + 'bidVolume': self.safe_string(ticker, 'bidSize'), + 'ask': self.safe_string(ticker, 'askPrice'), + 'askVolume': self.safe_string(ticker, 'askSize'), + 'vwap': None, + 'open': open, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'indexPrice': self.safe_string(ticker, 'indexPrice'), + 'markPrice': self.safe_string(ticker, 'markPrice'), + 'info': ticker, + }, market) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://blofin.com/docs#get-tickers + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + } + response = await self.publicGetMarketTickers(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + first = self.safe_dict(data, 0, {}) + return self.parse_ticker(first, market) + + async def fetch_mark_price(self, symbol: str, params={}) -> Ticker: + """ + fetches mark price for the market + + https://docs.blofin.com/index.html#get-mark-price + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.subType]: "linear" or "inverse" + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + market = self.market(symbol) + request = { + 'symbol': market['id'], + } + response = await self.publicGetMarketMarkPrice(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + first = self.safe_dict(data, 0, {}) + return self.parse_ticker(first, market) + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://blofin.com/docs#get-tickers + + :param str[] [symbols]: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + response = await self.publicGetMarketTickers(params) + tickers = self.safe_list(response, 'data', []) + return self.parse_tickers(tickers, symbols) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetch trades(response similar for REST & WS) + # + # { + # "tradeId": "3263934920", + # "instId": "LTC-USDT", + # "price": "67.87", + # "size": "1", + # "side": "buy", + # "ts": "1707232020854" + # } + # + # my trades + # { + # "instId": "LTC-USDT", + # "tradeId": "1440847", + # "orderId": "2075705202", + # "fillPrice": "67.850000000000000000", + # "fillSize": "1.000000000000000000", + # "fillPnl": "0.000000000000000000", + # "side": "buy", + # "positionSide": "net", + # "fee": "0.040710000000000000", + # "ts": "1707224678878", + # "brokerId": "" + # } + # + id = self.safe_string(trade, 'tradeId') + marketId = self.safe_string(trade, 'instId') + market = self.safe_market(marketId, market, '-') + symbol = market['symbol'] + timestamp = self.safe_integer(trade, 'ts') + price = self.safe_string_2(trade, 'price', 'fillPrice') + amount = self.safe_string_2(trade, 'size', 'fillSize') + side = self.safe_string(trade, 'side') + orderId = self.safe_string(trade, 'orderId') + feeCost = self.safe_string(trade, 'fee') + fee = None + if feeCost is not None: + fee = { + 'cost': feeCost, + 'currency': market['settle'], + } + return self.safe_trade({ + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'id': id, + 'order': orderId, + 'type': None, + 'takerOrMaker': None, + 'side': side, + 'price': price, + 'amount': amount, + 'cost': None, + 'fee': fee, + }, market) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://blofin.com/docs#get-trades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: *only applies to publicGetMarketHistoryTrades* default False, when True will automatically paginate by calling self endpoint multiple times + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchTrades', 'paginate') + if paginate: + return await self.fetch_paginated_call_cursor('fetchTrades', symbol, since, limit, params, 'tradeId', 'after', None, 100) + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + } + response = None + if limit is not None: + request['limit'] = limit # default 100 + method = None + method, params = self.handle_option_and_params(params, 'fetchTrades', 'method', 'publicGetMarketTrades') + if method == 'publicGetMarketTrades': + response = await self.publicGetMarketTrades(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + return self.parse_trades(data, market, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # [ + # "1678928760000", # timestamp + # "24341.4", # open + # "24344", # high + # "24313.2", # low + # "24323", # close + # "628", # contract volume + # "2.5819", # base volume + # "62800", # quote volume + # "0" # candlestick state + # ] + # + return [ + self.safe_integer(ohlcv, 0), + self.safe_number(ohlcv, 1), + self.safe_number(ohlcv, 2), + self.safe_number(ohlcv, 3), + self.safe_number(ohlcv, 4), + self.safe_number(ohlcv, 6), + ] + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://blofin.com/docs#get-candlesticks + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest candle to fetch + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate') + if paginate: + return await self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params, 100) + if limit is None: + limit = 100 # default 100, max 100 + request: dict = { + 'instId': market['id'], + 'bar': self.safe_string(self.timeframes, timeframe, timeframe), + 'limit': limit, + } + until = self.safe_integer(params, 'until') + if until is not None: + request['after'] = until + params = self.omit(params, 'until') + response = None + response = await self.publicGetMarketCandles(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + return self.parse_ohlcvs(data, market, timeframe, since, limit) + + async def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical funding rate prices + + https://blofin.com/docs#get-funding-rate-history + + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: timestamp in ms of the earliest funding rate to fetch + :param int [limit]: the maximum amount of `funding rate structures ` to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param int [params.until]: timestamp in ms of the latest funding rate to fetch + :returns dict[]: a list of `funding rate structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchFundingRateHistory() requires a symbol argument') + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchFundingRateHistory', 'paginate') + if paginate: + return await self.fetch_paginated_call_deterministic('fetchFundingRateHistory', symbol, since, limit, '8h', params, 100) + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + } + if since is not None: + request['before'] = max(since - 1, 0) + if limit is not None: + request['limit'] = limit + until = self.safe_integer(params, 'until') + if until is not None: + request['after'] = until + params = self.omit(params, 'until') + response = await self.publicGetMarketFundingRateHistory(self.extend(request, params)) + rates = [] + data = self.safe_list(response, 'data', []) + for i in range(0, len(data)): + rate = data[i] + timestamp = self.safe_integer(rate, 'fundingTime') + rates.append({ + 'info': rate, + 'symbol': market['symbol'], + 'fundingRate': self.safe_number(rate, 'fundingRate'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + sorted = self.sort_by(rates, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, market['symbol'], since, limit) + + def parse_funding_rate(self, contract, market: Market = None) -> FundingRate: + # + # { + # "fundingRate": "0.00027815", + # "fundingTime": "1634256000000", + # "instId": "BTC-USD-SWAP", + # } + # + marketId = self.safe_string(contract, 'instId') + symbol = self.safe_symbol(marketId, market) + fundingTime = self.safe_integer(contract, 'fundingTime') + # > The current interest is 0. + return { + 'info': contract, + 'symbol': symbol, + 'markPrice': None, + 'indexPrice': None, + 'interestRate': self.parse_number('0'), + 'estimatedSettlePrice': None, + 'timestamp': None, + 'datetime': None, + 'fundingRate': self.safe_number(contract, 'fundingRate'), + 'fundingTimestamp': fundingTime, + 'fundingDatetime': self.iso8601(fundingTime), + 'nextFundingRate': None, + 'nextFundingTimestamp': None, + 'nextFundingDatetime': None, + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': None, + } + + async def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate + + https://blofin.com/docs#get-funding-rate + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + await self.load_markets() + market = self.market(symbol) + if not market['swap']: + raise ExchangeError(self.id + ' fetchFundingRate() is only valid for swap markets') + request: dict = { + 'instId': market['id'], + } + response = await self.publicGetMarketFundingRate(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "fundingRate": "0.00027815", + # "fundingTime": "1634256000000", + # "instId": "BTC-USD-SWAP", + # } + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + entry = self.safe_dict(data, 0, {}) + return self.parse_funding_rate(entry, market) + + def parse_balance_by_type(self, response): + data = self.safe_list(response, 'data') + if (data is not None) and isinstance(data, list): + return self.parse_funding_balance(response) + else: + return self.parse_balance(response) + + def parse_balance(self, response): + # + # "data" similar for REST & WS + # + # { + # "code": "0", + # "msg": "success", + # "data": { + # "ts": "1697021343571", + # "totalEquity": "10011254.077985990315787910", + # "isolatedEquity": "861.763132108800000000", + # "details": [ + # { + # "currency": "USDT", + # "equity": "10014042.988958415234430699548", + # "balance": "10013119.885958415234430699", + # "ts": "1697021343571", + # "isolatedEquity": "862.003200000000000000048", + # "available": "9996399.4708691159703362725", + # "availableEquity": "9996399.4708691159703362725", + # "frozen": "15805.149672632597427761", + # "orderFrozen": "14920.994472632597427761", + # "equityUsd": "10011254.077985990315787910", + # "isolatedUnrealizedPnl": "-22.151999999999999999952", + # "bonus": "0" # present only in REST + # "unrealizedPnl": "0" # present only in WS + # } + # ] + # } + # } + # + result: dict = {'info': response} + data = self.safe_dict(response, 'data', {}) + timestamp = self.safe_integer(data, 'ts') + details = self.safe_list(data, 'details', []) + for i in range(0, len(details)): + balance = details[i] + currencyId = self.safe_string(balance, 'currency') + code = self.safe_currency_code(currencyId) + account = self.account() + # it may be incorrect to use total, free and used for swap accounts + eq = self.safe_string(balance, 'equity') + availEq = self.safe_string(balance, 'available') + if (eq is None) or (availEq is None): + account['free'] = self.safe_string(balance, 'availableEquity') + account['used'] = self.safe_string(balance, 'frozen') + else: + account['total'] = eq + account['free'] = availEq + result[code] = account + result['timestamp'] = timestamp + result['datetime'] = self.iso8601(timestamp) + return self.safe_balance(result) + + def parse_funding_balance(self, response): + # + # { + # "code": "0", + # "msg": "success", + # "data": [ + # { + # "currency": "USDT", + # "balance": "10012514.919418081548717298", + # "available": "9872132.414278782284622898", + # "frozen": "138556.471805965930761067", + # "bonus": "0" + # } + # ] + # } + # + result: dict = {'info': response} + data = self.safe_list(response, 'data', []) + for i in range(0, len(data)): + balance = data[i] + currencyId = self.safe_string(balance, 'currency') + code = self.safe_currency_code(currencyId) + account = self.account() + # it may be incorrect to use total, free and used for swap accounts + account['total'] = self.safe_string(balance, 'balance') + account['free'] = self.safe_string(balance, 'available') + account['used'] = self.safe_string(balance, 'frozen') + result[code] = account + return self.safe_balance(result) + + def parse_trading_fee(self, fee: dict, market: Market = None) -> TradingFeeInterface: + return { + 'info': fee, + 'symbol': self.safe_symbol(None, market), + # blofin returns the fees values opposed to other exchanges, so the sign needs to be flipped + 'maker': self.parse_number(Precise.string_neg(self.safe_string_2(fee, 'maker', 'makerU'))), + 'taker': self.parse_number(Precise.string_neg(self.safe_string_2(fee, 'taker', 'takerU'))), + 'percentage': None, + 'tierBased': None, + } + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://blofin.com/docs#get-balance + https://blofin.com/docs#get-futures-account-balance + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.accountType]: the type of account to fetch the balance for, either 'funding' or 'futures' or 'copy_trading' or 'earn' + :returns dict: a `balance structure ` + """ + await self.load_markets() + accountType = None + accountType, params = self.handle_option_and_params_2(params, 'fetchBalance', 'accountType', 'type') + request: dict = { + } + response = None + if accountType is not None and accountType != 'swap': + options = self.safe_dict(self.options, 'accountsByType', {}) + parsedAccountType = self.safe_string(options, accountType, accountType) + request['accountType'] = parsedAccountType + response = await self.privateGetAssetBalances(self.extend(request, params)) + else: + response = await self.privateGetAccountBalance(self.extend(request, params)) + return self.parse_balance_by_type(response) + + def create_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + 'side': side, + 'orderType': type, + 'size': self.amount_to_precision(symbol, amount), + 'brokerId': self.safe_string(self.options, 'brokerId', 'ec6dd3a7dd982d0b'), + } + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('createOrder', params, 'cross') + request['marginMode'] = marginMode + triggerPrice = self.safe_string(params, 'triggerPrice') + timeInForce = self.safe_string(params, 'timeInForce', 'GTC') + isHedged = self.safe_bool(params, 'hedged', False) + if isHedged: + request['positionSide'] = 'long' if (side == 'buy') else 'short' + isMarketOrder = type == 'market' + params = self.omit(params, ['timeInForce']) + ioc = (timeInForce == 'IOC') or (type == 'ioc') + marketIOC = (isMarketOrder and ioc) + if isMarketOrder or marketIOC: + request['orderType'] = 'market' + else: + key = 'orderPrice' if (triggerPrice is not None) else 'price' + request[key] = self.price_to_precision(symbol, price) + postOnly = False + postOnly, params = self.handle_post_only(isMarketOrder, type == 'post_only', params) + if postOnly: + request['type'] = 'post_only' + stopLoss = self.safe_dict(params, 'stopLoss') + takeProfit = self.safe_dict(params, 'takeProfit') + params = self.omit(params, ['stopLoss', 'takeProfit', 'hedged']) + isStopLoss = stopLoss is not None + isTakeProfit = takeProfit is not None + if isStopLoss or isTakeProfit: + if isStopLoss: + slTriggerPrice = self.safe_string_2(stopLoss, 'triggerPrice', 'stopPrice') + request['slTriggerPrice'] = self.price_to_precision(symbol, slTriggerPrice) + slOrderPrice = self.safe_string(stopLoss, 'price', '-1') + request['slOrderPrice'] = self.price_to_precision(symbol, slOrderPrice) + if isTakeProfit: + tpTriggerPrice = self.safe_string_2(takeProfit, 'triggerPrice', 'stopPrice') + request['tpTriggerPrice'] = self.price_to_precision(symbol, tpTriggerPrice) + tpPrice = self.safe_string(takeProfit, 'price', '-1') + request['tpOrderPrice'] = self.price_to_precision(symbol, tpPrice) + elif triggerPrice is not None: + request['orderType'] = 'trigger' + request['triggerPrice'] = self.price_to_precision(symbol, triggerPrice) + if isMarketOrder: + request['orderPrice'] = '-1' + return self.extend(request, params) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'canceled': 'canceled', + 'order_failed': 'canceled', + 'live': 'open', + 'partially_filled': 'open', + 'filled': 'closed', + 'effective': 'closed', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # response similar for REST & WS + # + # { + # "orderId": "2075628533", + # "clientOrderId": "", + # "instId": "LTC-USDT", + # "marginMode": "cross", + # "positionSide": "net", + # "side": "buy", + # "orderType": "market", + # "price": "0.000000000000000000", + # "size": "1.000000000000000000", + # "reduceOnly": "true", + # "leverage": "3", + # "state": "filled", + # "filledSize": "1.000000000000000000", + # "pnl": "-0.050000000000000000", + # "averagePrice": "68.110000000000000000", + # "fee": "0.040866000000000000", + # "createTime": "1706891359010", + # "updateTime": "1706891359098", + # "orderCategory": "normal", + # "tpTriggerPrice": null, + # "tpOrderPrice": null, + # "slTriggerPrice": null, + # "slOrderPrice": null, + # "cancelSource": "not_canceled", + # "cancelSourceReason": null, + # "brokerId": "ec6dd3a7dd982d0b" + # "filled_amount": "1.000000000000000000", # filledAmount in "ws" watchOrders + # "cancelSource": "", # only in WS + # "instType": "SWAP", # only in WS + # } + # + id = self.safe_string_n(order, ['tpslId', 'orderId', 'algoId']) + timestamp = self.safe_integer(order, 'createTime') + lastUpdateTimestamp = self.safe_integer(order, 'updateTime') + lastTradeTimestamp = self.safe_integer(order, 'fillTime') + side = self.safe_string(order, 'side') + type = self.safe_string(order, 'orderType') + postOnly = None + timeInForce = None + if type == 'post_only': + postOnly = True + type = 'limit' + elif type == 'fok': + timeInForce = 'FOK' + type = 'limit' + elif type == 'ioc': + timeInForce = 'IOC' + type = 'limit' + elif type == 'conditional': + type = 'trigger' + marketId = self.safe_string(order, 'instId') + market = self.safe_market(marketId, market) + symbol = self.safe_symbol(marketId, market, '-') + filled = self.safe_string(order, 'filledSize') + price = self.safe_string_n(order, ['px', 'price', 'orderPrice']) + average = self.safe_string(order, 'averagePrice') + status = self.parse_order_status(self.safe_string(order, 'state')) + feeCostString = self.safe_string(order, 'fee') + amount = self.safe_string(order, 'size') + leverage = self.safe_string(order, 'leverage', '1') + contractSize = self.safe_string(market, 'contractSize') + baseAmount = Precise.string_mul(contractSize, filled) + cost: Str = None + if average is not None: + cost = Precise.string_mul(average, baseAmount) + cost = Precise.string_div(cost, leverage) + # spot market buy: "sz" can refer either to base currency units or to quote currency units + fee = None + if feeCostString is not None: + feeCostSigned = Precise.string_abs(feeCostString) + feeCurrencyId = self.safe_string(order, 'feeCcy', 'USDT') + feeCurrencyCode = self.safe_currency_code(feeCurrencyId) + fee = { + 'cost': self.parse_number(feeCostSigned), + 'currency': feeCurrencyCode, + } + clientOrderId = self.safe_string(order, 'clientOrderId') + if (clientOrderId is not None) and (len(clientOrderId) < 1): + clientOrderId = None # fix empty clientOrderId string + stopLossTriggerPrice = self.safe_number(order, 'slTriggerPrice') + stopLossPrice = self.safe_number(order, 'slOrderPrice') + takeProfitTriggerPrice = self.safe_number(order, 'tpTriggerPrice') + takeProfitPrice = self.safe_number(order, 'tpOrderPrice') + reduceOnlyRaw = self.safe_string(order, 'reduceOnly') + reduceOnly = (reduceOnlyRaw == 'true') + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': lastTradeTimestamp, + 'lastUpdateTimestamp': lastUpdateTimestamp, + 'symbol': symbol, + 'type': type, + 'timeInForce': timeInForce, + 'postOnly': postOnly, + 'side': side, + 'price': price, + 'stopLossTriggerPrice': stopLossTriggerPrice, + 'takeProfitTriggerPrice': takeProfitTriggerPrice, + 'stopLossPrice': stopLossPrice, + 'takeProfitPrice': takeProfitPrice, + 'average': average, + 'cost': cost, + 'amount': amount, + 'filled': filled, + 'remaining': None, + 'status': status, + 'fee': fee, + 'trades': None, + 'reduceOnly': reduceOnly, + }, market) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}) -> Order: + """ + create a trade order + + https://blofin.com/docs#place-order + https://blofin.com/docs#place-tpsl-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' or 'post_only' or 'ioc' or 'fok' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.triggerPrice]: the trigger price for a trigger order + :param bool [params.reduceOnly]: a mark to reduce the position size for margin, swap and future orders + :param bool [params.postOnly]: True to place a post only order + :param str [params.marginMode]: 'cross' or 'isolated', default is 'cross' + :param float [params.stopLossPrice]: stop loss trigger price(will use privatePostTradeOrderTpsl) + :param float [params.takeProfitPrice]: take profit trigger price(will use privatePostTradeOrderTpsl) + :param str [params.positionSide]: *stopLossPrice/takeProfitPrice orders only* 'long' or 'short' or 'net' default is 'net' + :param boolean [params.hedged]: if True, the positionSide will be set to long/short instead of net, default is False + :param str [params.clientOrderId]: a unique id for the order + :param dict [params.takeProfit]: *takeProfit object in params* containing the triggerPrice at which the attached take profit order will be triggered + :param float [params.takeProfit.triggerPrice]: take profit trigger price + :param float [params.takeProfit.price]: take profit order price(if not provided the order will be a market order) + :param dict [params.stopLoss]: *stopLoss object in params* containing the triggerPrice at which the attached stop loss order will be triggered + :param float [params.stopLoss.triggerPrice]: stop loss trigger price + :param float [params.stopLoss.price]: stop loss order price(if not provided the order will be a market order) + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + tpsl = self.safe_bool(params, 'tpsl', False) + params = self.omit(params, 'tpsl') + method = None + method, params = self.handle_option_and_params(params, 'createOrder', 'method', 'privatePostTradeOrder') + isStopLossPriceDefined = self.safe_string(params, 'stopLossPrice') is not None + isTakeProfitPriceDefined = self.safe_string(params, 'takeProfitPrice') is not None + hasTriggerPrice = self.safe_string(params, 'triggerPrice') is not None + isType2Order = (isStopLossPriceDefined or isTakeProfitPriceDefined) + response = None + reduceOnly = self.safe_bool(params, 'reduceOnly') + if reduceOnly is not None: + params['reduceOnly'] = 'true' if reduceOnly else 'false' + isTpslOrder = tpsl or (method == 'privatePostTradeOrderTpsl') or isType2Order + isTriggerOrder = hasTriggerPrice or (method == 'privatePostTradeOrderAlgo') + if isTpslOrder: + tpslRequest = self.create_tpsl_order_request(symbol, type, side, amount, price, params) + response = await self.privatePostTradeOrderTpsl(tpslRequest) + elif isTriggerOrder: + triggerRequest = self.create_order_request(symbol, type, side, amount, price, params) + response = await self.privatePostTradeOrderAlgo(triggerRequest) + else: + request = self.create_order_request(symbol, type, side, amount, price, params) + response = await self.privatePostTradeOrder(request) + if isTpslOrder or isTriggerOrder: + dataDict = self.safe_dict(response, 'data', {}) + return self.parse_order(dataDict, market) + data = self.safe_list(response, 'data', []) + first = self.safe_dict(data, 0) + order = self.parse_order(first, market) + order['type'] = type + order['side'] = side + return order + + def create_tpsl_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + market = self.market(symbol) + positionSide = self.safe_string(params, 'positionSide', 'net') + request: dict = { + 'instId': market['id'], + 'side': side, + 'positionSide': positionSide, + 'brokerId': self.safe_string(self.options, 'brokerId', 'ec6dd3a7dd982d0b'), + } + if amount is not None: + request['size'] = self.amount_to_precision(symbol, amount) + marginMode = self.safe_string(params, 'marginMode', 'cross') # cross or isolated + if marginMode != 'cross' and marginMode != 'isolated': + raise BadRequest(self.id + ' createTpslOrder() requires a marginMode parameter that must be either cross or isolated') + stopLossPrice = self.safe_string(params, 'stopLossPrice') + takeProfitPrice = self.safe_string(params, 'takeProfitPrice') + if stopLossPrice is not None: + request['slTriggerPrice'] = self.price_to_precision(symbol, stopLossPrice) + if type == 'market': + request['slOrderPrice'] = '-1' + else: + request['slOrderPrice'] = self.price_to_precision(symbol, price) + elif takeProfitPrice is not None: + request['tpTriggerPrice'] = self.price_to_precision(symbol, takeProfitPrice) + if type == 'market': + request['tpOrderPrice'] = '-1' + else: + request['tpOrderPrice'] = self.price_to_precision(symbol, price) + request['marginMode'] = marginMode + params = self.omit(params, ['stopLossPrice', 'takeProfitPrice']) + return self.extend(request, params) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://blofin.com/docs#cancel-order + https://blofin.com/docs#cancel-tpsl-order + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: True if cancelling a trigger/conditional + :param boolean [params.tpsl]: True if cancelling a tpsl order + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + } + isTrigger = self.safe_bool_n(params, ['trigger'], False) + isTpsl = self.safe_bool_2(params, 'tpsl', 'TPSL', False) + clientOrderId = self.safe_string(params, 'clientOrderId') + if clientOrderId is not None: + request['clientOrderId'] = clientOrderId + else: + if not isTrigger and not isTpsl: + request['orderId'] = str(id) + elif isTpsl: + request['tpslId'] = str(id) + elif isTrigger: + request['algoId'] = str(id) + query = self.omit(params, ['orderId', 'clientOrderId', 'stop', 'trigger', 'tpsl']) + if isTpsl: + tpslResponse = await self.cancel_orders([id], symbol, params) + first = self.safe_dict(tpslResponse, 0) + return first + elif isTrigger: + triggerResponse = await self.privatePostTradeCancelAlgo(self.extend(request, query)) + triggerData = self.safe_dict(triggerResponse, 'data') + return self.parse_order(triggerData, market) + response = await self.privatePostTradeCancelOrder(self.extend(request, query)) + data = self.safe_list(response, 'data', []) + order = self.safe_dict(data, 0) + return self.parse_order(order, market) + + async def create_orders(self, orders: List[OrderRequest], params={}) -> List[Order]: + """ + create a list of trade orders + + https://blofin.com/docs#place-multiple-orders + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + ordersRequests = [] + for i in range(0, len(orders)): + rawOrder = orders[i] + marketId = self.safe_string(rawOrder, 'symbol') + type = self.safe_string(rawOrder, 'type') + side = self.safe_string(rawOrder, 'side') + amount = self.safe_value(rawOrder, 'amount') + price = self.safe_value(rawOrder, 'price') + orderParams = self.safe_dict(rawOrder, 'params', {}) + extendedParams = self.extend(orderParams, params) # the request does not accept extra params since it's a list, so we're extending each order with the common params + orderRequest = self.create_order_request(marketId, type, side, amount, price, extendedParams) + ordersRequests.append(orderRequest) + response = await self.privatePostTradeBatchOrders(ordersRequests) + data = self.safe_list(response, 'data', []) + return self.parse_orders(data) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + Fetch orders that are still open + + https://blofin.com/docs#get-active-orders + https://blofin.com/docs#get-active-tpsl-orders + https://docs.blofin.com/index.html#get-active-algo-orders + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.trigger]: True if fetching trigger or conditional orders + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOpenOrders', 'paginate') + if paginate: + return await self.fetch_paginated_call_dynamic('fetchOpenOrders', symbol, since, limit, params) + request: dict = { + } + market = None + if symbol is not None: + market = self.market(symbol) + request['instId'] = market['id'] + if limit is not None: + request['limit'] = limit # default 100, max 100 + isTrigger = self.safe_bool_n(params, ['stop', 'trigger'], False) + isTpSl = self.safe_bool_2(params, 'tpsl', 'TPSL', False) + method: Str = None + method, params = self.handle_option_and_params(params, 'fetchOpenOrders', 'method', 'privateGetTradeOrdersPending') + query = self.omit(params, ['method', 'stop', 'trigger', 'tpsl', 'TPSL']) + response = None + if isTpSl or (method == 'privateGetTradeOrdersTpslPending'): + response = await self.privateGetTradeOrdersTpslPending(self.extend(request, query)) + elif isTrigger or (method == 'privateGetTradeOrdersAlgoPending'): + request['orderType'] = 'trigger' + response = await self.privateGetTradeOrdersAlgoPending(self.extend(request, query)) + else: + response = await self.privateGetTradeOrdersPending(self.extend(request, query)) + data = self.safe_list(response, 'data', []) + return self.parse_orders(data, market, since, limit) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + fetch all trades made by the user + + https://blofin.com/docs#get-trade-history + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: Timestamp in ms of the latest time to retrieve trades for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchMyTrades', 'paginate') + if paginate: + return await self.fetch_paginated_call_dynamic('fetchMyTrades', symbol, since, limit, params) + request: dict = { + } + market = None + if symbol is not None: + market = self.market(symbol) + request['instId'] = market['id'] + request, params = self.handle_until_option('end', request, params) + if limit is not None: + request['limit'] = limit # default 100, max 100 + response = await self.privateGetTradeFillsHistory(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + return self.parse_trades(data, market, since, limit) + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://blofin.com/docs#get-deposite-history + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch entries for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchDeposits', 'paginate') + if paginate: + return await self.fetch_paginated_call_dynamic('fetchDeposits', code, since, limit, params) + request: dict = { + } + currency = None + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + if since is not None: + request['before'] = max(since - 1, 0) + if limit is not None: + request['limit'] = limit # default 100, max 100 + request, params = self.handle_until_option('after', request, params) + response = await self.privateGetAssetDepositHistory(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + return self.parse_transactions(data, currency, since, limit, params) + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://blofin.com/docs#get-withdraw-history + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch entries for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchWithdrawals', 'paginate') + if paginate: + return await self.fetch_paginated_call_dynamic('fetchWithdrawals', code, since, limit, params) + request: dict = { + } + currency = None + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + if since is not None: + request['before'] = max(since - 1, 0) + if limit is not None: + request['limit'] = limit # default 100, max 100 + request, params = self.handle_until_option('after', request, params) + response = await self.privateGetAssetWithdrawalHistory(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + return self.parse_transactions(data, currency, since, limit, params) + + async def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + + https://blofin.com/docs#get-funds-transfer-history + + :param str [code]: unified currency code, default is None + :param int [since]: timestamp in ms of the earliest ledger entry, default is None + :param int [limit]: max number of ledger entries to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' + :param int [params.until]: the latest time in ms to fetch entries for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict: a `ledger structure ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchLedger', 'paginate') + if paginate: + return await self.fetch_paginated_call_dynamic('fetchLedger', code, since, limit, params) + request: dict = { + } + if limit is not None: + request['limit'] = limit + currency = None + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + request, params = self.handle_until_option('end', request, params) + response = None + response = await self.privateGetAssetBills(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + return self.parse_ledger(data, currency, since, limit) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # + # fetchDeposits + # + # { + # "currency": "USDT", + # "chain": "TRC20", + # "address": "TGfJLtnsh3B9EqekFEBZ1nR14QanBUf5Bi", + # "txId": "892f4e0c32268b29b2e541ef30d32a30bbf10f902adcc4b1428319ed7c3758fd", + # "type": "0", + # "amount": "86.975843", + # "state": "1", + # "ts": "1703163304153", + # "tag": null, + # "confirm": "16", + # "depositId": "36c8e2a7ea184a219de72215a696acaf" + # } + # fetchWithdrawals + # { + # "currency": "USDT", + # "chain": "TRC20", + # "address": "TYgB3sVXHPEDQUu288EG1uMFh9Pk2swLgW", + # "txId": "1fd5ac52df414d7ea66194cadd9a5b4d2422c2b9720037f66d98207f9858fd96", + # "type": "0", + # "amount": "9", + # "fee": "1", + # "feeCurrency": "USDT", + # "state": "3", + # "clientId": null, + # "ts": "1707217439351", + # "tag": null, + # "memo": null, + # "withdrawId": "e0768698cfdf4aee8e54654c3775914b" + # } + # + type = None + id = None + withdrawalId = self.safe_string(transaction, 'withdrawId') + depositId = self.safe_string(transaction, 'depositId') + addressTo = self.safe_string(transaction, 'address') + address = addressTo + tagTo = self.safe_string(transaction, 'tag') + if withdrawalId is not None: + type = 'withdrawal' + id = withdrawalId + else: + id = depositId + type = 'deposit' + currencyId = self.safe_string(transaction, 'currency') + code = self.safe_currency_code(currencyId) + amount = self.safe_number(transaction, 'amount') + status = self.parse_transaction_status(self.safe_string(transaction, 'state')) + txid = self.safe_string(transaction, 'txId') + timestamp = self.safe_integer(transaction, 'ts') + feeCurrencyId = self.safe_string(transaction, 'feeCurrency') + feeCode = self.safe_currency_code(feeCurrencyId) + feeCost = self.safe_number(transaction, 'fee') + return { + 'info': transaction, + 'id': id, + 'currency': code, + 'amount': amount, + 'network': None, + 'addressFrom': None, + 'addressTo': addressTo, + 'address': address, + 'tagFrom': None, + 'tagTo': tagTo, + 'tag': tagTo, + 'status': status, + 'type': type, + 'updated': None, + 'txid': txid, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'internal': None, + 'comment': None, + 'fee': { + 'currency': feeCode, + 'cost': feeCost, + }, + } + + def parse_transaction_status(self, status: Str): + statuses: dict = { + '0': 'pending', + '1': 'ok', + '2': 'failed', + '3': 'pending', + } + return self.safe_string(statuses, status, status) + + def parse_ledger_entry_type(self, type): + types: dict = { + '1': 'transfer', # transfer + '2': 'trade', # trade + '3': 'trade', # delivery + '4': 'rebate', # auto token conversion + '5': 'trade', # liquidation + '6': 'transfer', # margin transfer + '7': 'trade', # interest deduction + '8': 'fee', # funding rate + '9': 'trade', # adl + '10': 'trade', # clawback + '11': 'trade', # system token conversion + } + return self.safe_string(types, type, type) + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + currencyId = self.safe_string(item, 'currency') + code = self.safe_currency_code(currencyId, currency) + currency = self.safe_currency(currencyId, currency) + timestamp = self.safe_integer(item, 'ts') + return self.safe_ledger_entry({ + 'info': item, + 'id': self.safe_string(item, 'transferId'), + 'direction': None, + 'account': None, + 'referenceId': self.safe_string(item, 'clientId'), + 'referenceAccount': None, + 'type': self.parse_ledger_entry_type(self.safe_string(item, 'type')), + 'currency': code, + 'amount': self.safe_number(item, 'amount'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'before': None, + 'after': None, + 'status': 'ok', + 'fee': None, + }, currency) + + def parse_ids(self, ids): + """ + @ignore + :param string[]|str ids: order ids + :returns str[]: list of order ids + """ + if isinstance(ids, str): + return ids.split(',') + else: + return ids + + async def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): + """ + cancel multiple orders + + https://blofin.com/docs#cancel-multiple-orders + + :param str[] ids: order ids + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: whether the order is a stop/trigger order + :returns dict: an list of `order structures ` + """ + # TODO : the original endpoint signature differs, according to that you can skip individual symbol and assign ids in batch. At self moment, `params` is not being used too. + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrders() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request = [] + options = self.safe_dict(self.options, 'cancelOrders', {}) + defaultMethod = self.safe_string(options, 'method', 'privatePostTradeCancelBatchOrders') + method = self.safe_string(params, 'method', defaultMethod) + clientOrderIds = self.parse_ids(self.safe_value(params, 'clientOrderId')) + tpslIds = self.parse_ids(self.safe_value(params, 'tpslId')) + trigger = self.safe_bool_n(params, ['stop', 'trigger', 'tpsl']) + if trigger: + method = 'privatePostTradeCancelTpsl' + if clientOrderIds is None: + ids = self.parse_ids(ids) + if tpslIds is not None: + for i in range(0, len(tpslIds)): + request.append({ + 'tpslId': tpslIds[i], + 'instId': market['id'], + }) + for i in range(0, len(ids)): + if trigger: + request.append({ + 'tpslId': ids[i], + 'instId': market['id'], + }) + else: + request.append({ + 'orderId': ids[i], + 'instId': market['id'], + }) + else: + for i in range(0, len(clientOrderIds)): + request.append({ + 'instId': market['id'], + 'clientOrderId': clientOrderIds[i], + }) + response = None + if method == 'privatePostTradeCancelTpsl': + response = await self.privatePostTradeCancelTpsl(request) # * dont self.extend with params, otherwise ARRAY will be turned into OBJECT + else: + response = await self.privatePostTradeCancelBatchOrders(request) # * dont self.extend with params, otherwise ARRAY will be turned into OBJECT + ordersData = self.safe_list(response, 'data', []) + return self.parse_orders(ordersData, market, None, None, params) + + async def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://blofin.com/docs#funds-transfer + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account to transfer from(funding, swap, copy_trading, earn) + :param str toAccount: account to transfer to(funding, swap, copy_trading, earn) + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transfer structure ` + """ + await self.load_markets() + currency = self.currency(code) + accountsByType = self.safe_dict(self.options, 'accountsByType', {}) + fromId = self.safe_string(accountsByType, fromAccount, fromAccount) + toId = self.safe_string(accountsByType, toAccount, toAccount) + request: dict = { + 'currency': currency['id'], + 'amount': self.currency_to_precision(code, amount), + 'fromAccount': fromId, + 'toAccount': toId, + } + response = await self.privatePostAssetTransfer(self.extend(request, params)) + data = self.safe_dict(response, 'data', {}) + return self.parse_transfer(data, currency) + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + id = self.safe_string(transfer, 'transferId') + return { + 'info': transfer, + 'id': id, + 'timestamp': None, + 'datetime': None, + 'currency': None, + 'amount': None, + 'fromAccount': None, + 'toAccount': None, + 'status': None, + } + + async def fetch_position(self, symbol: str, params={}) -> Position: + """ + fetch data on a single open contract trade position + + https://blofin.com/docs#get-positions + + :param str symbol: unified market symbol of the market the position is held in, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.instType]: MARGIN, SWAP, FUTURES, OPTION + :returns dict: a `position structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + } + response = await self.privateGetAccountPositions(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + position = self.safe_dict(data, 0) + if position is None: + return None + return self.parse_position(position, market) + + async def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch data on a single open contract trade position + + https://blofin.com/docs#get-positions + + :param str[] [symbols]: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.instType]: MARGIN, SWAP, FUTURES, OPTION + :returns dict: a `position structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + response = await self.privateGetAccountPositions(params) + data = self.safe_list(response, 'data', []) + result = self.parse_positions(data) + return self.filter_by_array_positions(result, 'symbol', symbols, False) + + def parse_position(self, position: dict, market: Market = None): + # + # response similar for REST & WS + # + # { + # instType: 'SWAP', + # instId: 'LTC-USDT', + # marginMode: 'cross', + # positionId: '644159', + # positionSide: 'net', + # positions: '1', + # availablePositions: '1', + # averagePrice: '68.16', + # unrealizedPnl: '0.80631223', + # unrealizedPnlRatio: '0.03548909463028169', + # leverage: '3', + # liquidationPrice: '10.116655172370356435', + # markPrice: '68.96', + # initialMargin: '22.988770743333333333', + # margin: '', # self field might not exist in rest response + # marginRatio: '152.523509620342499273', + # maintenanceMargin: '0.34483156115', + # adl: '4', + # createTime: '1707235776528', + # updateTime: '1707235776528' + # } + # + marketId = self.safe_string(position, 'instId') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + pos = self.safe_string(position, 'positions') + contractsAbs = Precise.string_abs(pos) + side = self.safe_string(position, 'positionSide') + hedged = side != 'net' + contracts = self.parse_number(contractsAbs) + if pos is not None: + if side == 'net': + if Precise.string_gt(pos, '0'): + side = 'long' + elif Precise.string_lt(pos, '0'): + side = 'short' + else: + side = None + contractSize = self.safe_number(market, 'contractSize') + contractSizeString = self.number_to_string(contractSize) + markPriceString = self.safe_string(position, 'markPrice') + notionalString = self.safe_string(position, 'notionalUsd') + if market['inverse']: + notionalString = Precise.string_div(Precise.string_mul(contractsAbs, contractSizeString), markPriceString) + notional = self.parse_number(notionalString) + marginMode = self.safe_string(position, 'marginMode') + initialMarginString = None + entryPriceString = self.safe_string(position, 'averagePrice') + unrealizedPnlString = self.safe_string(position, 'unrealizedPnl') + leverageString = self.safe_string(position, 'leverage') + initialMarginPercentage = None + collateralString = None + if marginMode == 'cross': + initialMarginString = self.safe_string(position, 'initialMargin') + collateralString = Precise.string_add(initialMarginString, unrealizedPnlString) + elif marginMode == 'isolated': + initialMarginPercentage = Precise.string_div('1', leverageString) + collateralString = self.safe_string(position, 'margin') + maintenanceMarginString = self.safe_string(position, 'maintenanceMargin') + maintenanceMargin = self.parse_number(maintenanceMarginString) + maintenanceMarginPercentageString = Precise.string_div(maintenanceMarginString, notionalString) + if initialMarginPercentage is None: + initialMarginPercentage = self.parse_number(Precise.string_div(initialMarginString, notionalString, 4)) + elif initialMarginString is None: + initialMarginString = Precise.string_mul(initialMarginPercentage, notionalString) + rounder = '0.00005' # round to closest 0.01% + maintenanceMarginPercentage = self.parse_number(Precise.string_div(Precise.string_add(maintenanceMarginPercentageString, rounder), '1', 4)) + liquidationPrice = self.safe_number(position, 'liquidationPrice') + percentageString = self.safe_string(position, 'unrealizedPnlRatio') + percentage = self.parse_number(Precise.string_mul(percentageString, '100')) + timestamp = self.safe_integer(position, 'updateTime') + marginRatio = self.parse_number(Precise.string_div(maintenanceMarginString, collateralString, 4)) + return self.safe_position({ + 'info': position, + 'id': None, + 'symbol': symbol, + 'notional': notional, + 'marginMode': marginMode, + 'liquidationPrice': liquidationPrice, + 'entryPrice': self.parse_number(entryPriceString), + 'unrealizedPnl': self.parse_number(unrealizedPnlString), + 'percentage': percentage, + 'contracts': contracts, + 'contractSize': contractSize, + 'markPrice': self.parse_number(markPriceString), + 'lastPrice': None, + 'side': side, + 'hedged': hedged, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastUpdateTimestamp': None, + 'maintenanceMargin': maintenanceMargin, + 'maintenanceMarginPercentage': maintenanceMarginPercentage, + 'collateral': self.parse_number(collateralString), + 'initialMargin': self.parse_number(initialMarginString), + 'initialMarginPercentage': self.parse_number(initialMarginPercentage), + 'leverage': self.parse_number(leverageString), + 'marginRatio': marginRatio, + 'stopLossPrice': None, + 'takeProfitPrice': None, + }) + + async def fetch_leverages(self, symbols: Strings = None, params={}) -> Leverages: + """ + fetch the set leverage for all contract markets + + https://docs.blofin.com/index.html#get-multiple-leverage + + :param str[] symbols: a list of unified market symbols, required on blofin + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' + :returns dict: a list of `leverage structures ` + """ + await self.load_markets() + if symbols is None: + raise ArgumentsRequired(self.id + ' fetchLeverages() requires a symbols argument') + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchLeverages', params) + if marginMode is None: + marginMode = self.safe_string(params, 'marginMode', 'cross') # cross marginMode + if (marginMode != 'cross') and (marginMode != 'isolated'): + raise BadRequest(self.id + ' fetchLeverages() requires a marginMode parameter that must be either cross or isolated') + symbols = self.market_symbols(symbols) + instIds = '' + for i in range(0, len(symbols)): + entry = symbols[i] + entryMarket = self.market(entry) + if i > 0: + instIds = instIds + ',' + entryMarket['id'] + else: + instIds = instIds + entryMarket['id'] + request: dict = { + 'instId': instIds, + 'marginMode': marginMode, + } + response = await self.privateGetAccountBatchLeverageInfo(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "success", + # "data": [ + # { + # "leverage": "3", + # "marginMode": "cross", + # "instId": "BTC-USDT" + # }, + # ] + # } + # + leverages = self.safe_list(response, 'data', []) + return self.parse_leverages(leverages, symbols, 'instId') + + async def fetch_leverage(self, symbol: str, params={}) -> Leverage: + """ + fetch the set leverage for a market + + https://docs.blofin.com/index.html#get-leverage + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' + :returns dict: a `leverage structure ` + """ + await self.load_markets() + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchLeverage', params) + if marginMode is None: + marginMode = self.safe_string(params, 'marginMode', 'cross') # cross marginMode + if (marginMode != 'cross') and (marginMode != 'isolated'): + raise BadRequest(self.id + ' fetchLeverage() requires a marginMode parameter that must be either cross or isolated') + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + 'marginMode': marginMode, + } + response = await self.privateGetAccountLeverageInfo(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "success", + # "data": { + # "leverage": "3", + # "marginMode": "cross", + # "instId": "BTC-USDT" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_leverage(data, market) + + def parse_leverage(self, leverage: dict, market: Market = None) -> Leverage: + marketId = self.safe_string(leverage, 'instId') + leverageValue = self.safe_integer(leverage, 'leverage') + return { + 'info': leverage, + 'symbol': self.safe_symbol(marketId, market), + 'marginMode': self.safe_string_lower(leverage, 'marginMode'), + 'longLeverage': leverageValue, + 'shortLeverage': leverageValue, + } + + async def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://blofin.com/docs#set-leverage + + :param int leverage: the rate of leverage + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' + :param str [params.positionSide]: 'long' or 'short' - required for hedged mode in isolated margin + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setLeverage() requires a symbol argument') + # WARNING: THIS WILL INCREASE LIQUIDATION PRICE FOR OPEN ISOLATED LONG POSITIONS + # AND DECREASE LIQUIDATION PRICE FOR OPEN ISOLATED SHORT POSITIONS + if (leverage < 1) or (leverage > 125): + raise BadRequest(self.id + ' setLeverage() leverage should be between 1 and 125') + await self.load_markets() + market = self.market(symbol) + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('setLeverage', params, 'cross') + if (marginMode != 'cross') and (marginMode != 'isolated'): + raise BadRequest(self.id + ' setLeverage() requires a marginMode parameter that must be either cross or isolated') + request: dict = { + 'leverage': leverage, + 'marginMode': marginMode, + 'instId': market['id'], + } + response = await self.privatePostAccountSetLeverage(self.extend(request, params)) + return response + + async def close_position(self, symbol: str, side: OrderSide = None, params={}) -> Order: + """ + closes open positions for a market + + https://blofin.com/docs#close-positions + + :param str symbol: Unified CCXT market symbol + :param str [side]: 'buy' or 'sell', leave in net mode + :param dict [params]: extra parameters specific to the blofin api endpoint + :param str [params.clientOrderId]: a unique identifier for the order + :param str [params.marginMode]: 'cross' or 'isolated', default is 'cross + :param str [params.code]: *required in the case of closing cross MARGIN position for Single-currency margin* margin currency + + EXCHANGE SPECIFIC PARAMETERS + :param boolean [params.autoCxl]: whether any pending orders for closing out needs to be automatically canceled when close position via a market order. False or True, the default is False + :param str [params.tag]: order tag a combination of case-sensitive alphanumerics, all numbers, or all letters of up to 16 characters + :returns dict[]: `A list of position structures ` + """ + await self.load_markets() + market = self.market(symbol) + clientOrderId = self.safe_string(params, 'clientOrderId') + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('closePosition', params, 'cross') + request: dict = { + 'instId': market['id'], + 'marginMode': marginMode, + } + if clientOrderId is not None: + request['clientOrderId'] = clientOrderId + response = await self.privatePostTradeClosePosition(self.extend(request, params)) + return self.safe_dict(response, 'data') + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://blofin.com/docs#get-order-history + https://blofin.com/docs#get-tpsl-order-history + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of orde structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.trigger]: True if fetching trigger or conditional orders + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchClosedOrders', 'paginate') + if paginate: + return await self.fetch_paginated_call_dynamic('fetchClosedOrders', symbol, since, limit, params) + request: dict = { + } + market = None + if symbol is not None: + market = self.market(symbol) + request['instId'] = market['id'] + if limit is not None: + request['limit'] = limit # default 100, max 100 + if since is not None: + request['begin'] = since + isTrigger = self.safe_bool_n(params, ['stop', 'trigger', 'tpsl', 'TPSL'], False) + method: Str = None + method, params = self.handle_option_and_params(params, 'fetchOpenOrders', 'method', 'privateGetTradeOrdersHistory') + query = self.omit(params, ['method', 'stop', 'trigger', 'tpsl', 'TPSL']) + response = None + if (isTrigger) or (method == 'privateGetTradeOrdersTpslHistory'): + response = await self.privateGetTradeOrdersTpslHistory(self.extend(request, query)) + else: + response = await self.privateGetTradeOrdersHistory(self.extend(request, query)) + data = self.safe_list(response, 'data', []) + return self.parse_orders(data, market, since, limit) + + async def fetch_margin_mode(self, symbol: str, params={}) -> MarginMode: + """ + fetches the margin mode of a trading pair + + https://docs.blofin.com/index.html#get-margin-mode + + :param str symbol: unified symbol of the market to fetch the margin mode for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin mode structure ` + """ + await self.load_markets() + market = self.market(symbol) + response = await self.privateGetAccountMarginMode(params) + # + # { + # "code": "0", + # "msg": "success", + # "data": { + # "marginMode": "cross" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_margin_mode(data, market) + + def parse_margin_mode(self, marginMode: dict, market: Market = None) -> MarginMode: + return { + 'info': marginMode, + 'symbol': self.safe_string(market, 'symbol'), + 'marginMode': self.safe_string(marginMode, 'marginMode'), + } + + async def set_margin_mode(self, marginMode: str, symbol: Str = None, params={}): + """ + set margin mode to 'cross' or 'isolated' + + https://docs.blofin.com/index.html#set-margin-mode + + :param str marginMode: 'cross' or 'isolated' + :param str [symbol]: unified market symbol(not used in blofin setMarginMode) + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + self.check_required_argument('setMarginMode', marginMode, 'marginMode', ['cross', 'isolated']) + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + request: dict = { + 'marginMode': marginMode, + } + response = await self.privatePostAccountSetMarginMode(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "success", + # "data": { + # "marginMode": "isolated" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_margin_mode(data, market) + + async def fetch_position_mode(self, symbol: Str = None, params={}): + """ + fetchs the position mode, hedged or one way + + https://docs.blofin.com/index.html#get-position-mode + + :param str [symbol]: unified symbol of the market to fetch the position mode for(not used in blofin fetchPositionMode) + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an object detailing whether the market is in hedged or one-way mode + """ + response = await self.privateGetAccountPositionMode(params) + data = self.safe_dict(response, 'data', {}) + positionMode = self.safe_string(data, 'positionMode') + # + # { + # "code": "0", + # "msg": "success", + # "data": { + # "positionMode": "long_short_mode" + # } + # } + # + return { + 'info': data, + 'hedged': positionMode == 'long_short_mode', + } + + async def set_position_mode(self, hedged: bool, symbol: Str = None, params={}): + """ + set hedged to True or False for a market + + https://docs.blofin.com/index.html#set-position-mode + + :param bool hedged: set to True to use hedged mode, False for one-way mode + :param str [symbol]: not used by blofin setPositionMode() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + request: dict = { + 'positionMode': 'long_short_mode' if hedged else 'net_mode', + } + # + # { + # "code": "0", + # "msg": "success", + # "data": { + # "positionMode": "net_mode" + # } + # } + # + return await self.privatePostAccountSetPositionMode(self.extend(request, params)) + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None # fallback to default error handler + # + # {"code":"152002","msg":"Parameter bar error."} + # + code = self.safe_string(response, 'code') + message = self.safe_string(response, 'msg') + feedback = self.id + ' ' + body + if code is not None and code != '0': + self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], code, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + raise ExchangeError(feedback) # unknown message + # + # { + # orderId: null, + # clientOrderId: '', + # msg: 'Order failed. Insufficient USDT margin in account', + # code: '103003' + # } + # + data = self.safe_list(response, 'data') + first = self.safe_dict(data, 0) + insideMsg = self.safe_string(first, 'msg') + insideCode = self.safe_string(first, 'code') + if insideCode is not None and insideCode != '0': + self.throw_exactly_matched_exception(self.exceptions['exact'], insideCode, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], insideMsg, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], insideMsg, feedback) + return None + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + request = '/api/' + self.version + '/' + self.implode_params(path, params) + query = self.omit(params, self.extract_params(path)) + url = self.implode_hostname(self.urls['api']['rest']) + request + # type = self.getPathAuthenticationType(path) + if api == 'public': + if not self.is_empty(query): + url += '?' + self.urlencode(query) + elif api == 'private': + self.check_required_credentials() + timestamp = str(self.milliseconds()) + headers = { + 'ACCESS-KEY': self.apiKey, + 'ACCESS-PASSPHRASE': self.password, + 'ACCESS-TIMESTAMP': timestamp, + 'ACCESS-NONCE': timestamp, + } + sign_body = '' + if method == 'GET': + if not self.is_empty(query): + urlencodedQuery = '?' + self.urlencode(query) + url += urlencodedQuery + request += urlencodedQuery + else: + if not self.is_empty(query): + body = self.json(query) + sign_body = body + headers['Content-Type'] = 'application/json' + auth = request + method + timestamp + timestamp + sign_body + signature = self.string_to_base64(self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256)) + headers['ACCESS-SIGN'] = signature + return {'url': url, 'method': method, 'body': body, 'headers': headers} diff --git a/ccxt/async_support/btcalpha.py b/ccxt/async_support/btcalpha.py new file mode 100644 index 0000000..b7d1e07 --- /dev/null +++ b/ccxt/async_support/btcalpha.py @@ -0,0 +1,1030 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.btcalpha import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currency, IndexType, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class btcalpha(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(btcalpha, self).describe(), { + 'id': 'btcalpha', + 'name': 'BTC-Alpha', + 'countries': ['US'], + 'version': 'v1', + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelOrder': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createDepositAddress': False, + 'createOrder': True, + 'createOrderWithTakeProfitAndStopLoss': False, + 'createOrderWithTakeProfitAndStopLossWs': False, + 'createPostOnlyOrder': False, + 'createReduceOnlyOrder': False, + 'createStopLimitOrder': False, + 'createStopMarketOrder': False, + 'createStopOrder': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': False, + 'fetchDeposit': False, + 'fetchDepositAddress': False, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchL2OrderBook': True, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': False, + 'fetchTransfer': False, + 'fetchTransfers': False, + 'fetchVolatilityHistory': False, + 'fetchWithdrawal': False, + 'fetchWithdrawals': True, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'repayMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'transfer': False, + 'withdraw': False, + }, + 'timeframes': { + '5m': '5', + '15m': '15', + '30m': '30', + '1h': '60', + '4h': '240', + '1d': 'D', + }, + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/dce49f3a-61e5-4ba0-a2fe-41d192fd0e5d', + 'api': { + 'rest': 'https://btc-alpha.com/api', + }, + 'www': 'https://btc-alpha.com', + 'doc': 'https://btc-alpha.github.io/api-docs', + 'fees': 'https://btc-alpha.com/fees/', + 'referral': 'https://btc-alpha.com/?r=123788', + }, + 'api': { + 'public': { + 'get': [ + 'currencies/', + 'pairs/', + 'orderbook/{pair_name}', + 'exchanges/', + 'charts/{pair}/{type}/chart/', + 'ticker/', + ], + }, + 'private': { + 'get': [ + 'wallets/', + 'orders/own/', + 'order/{id}/', + 'exchanges/own/', + 'deposits/', + 'withdraws/', + ], + 'post': [ + 'order/', + 'order-cancel/', + ], + }, + }, + 'fees': { + 'trading': { + 'maker': self.parse_number('0.002'), + 'taker': self.parse_number('0.002'), + }, + 'funding': { + 'withdraw': {}, + }, + }, + 'commonCurrencies': { + 'CBC': 'Cashbery', + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': False, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': False, + 'FOK': False, + 'PO': False, + 'GTD': False, + }, + 'hedged': False, + 'leverage': False, + 'marketBuyRequiresPrice': False, + 'marketBuyByCost': False, + 'selfTradePrevention': False, + 'trailing': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, + 'daysBack': None, + 'untilDays': None, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 2000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': 2000, + 'daysBack': None, + 'untilDays': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 2000, + 'daysBack': None, + 'daysBackCanceled': None, + 'untilDays': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 720, + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'exact': {}, + 'broad': { + 'Out of balance': InsufficientFunds, # {"date":1570599531.4814300537,"error":"Out of balance -9.99243661 BTC"} + }, + }, + }) + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for btcalpha + + https://btc-alpha.github.io/api-docs/#list-all-currencies + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = await self.publicGetPairs(params) + # + # [ + # { + # "name": "1INCH_USDT", + # "currency1": "1INCH", + # "currency2": "USDT", + # "price_precision": 4, + # "amount_precision": 2, + # "minimum_order_size": "0.01000000", + # "maximum_order_size": "900000.00000000", + # "minimum_order_value": "10.00000000", + # "liquidity_type": 10 + # }, + # ] + # + return self.parse_markets(response) + + def parse_market(self, market: dict) -> Market: + id = self.safe_string(market, 'name') + baseId = self.safe_string(market, 'currency1') + quoteId = self.safe_string(market, 'currency2') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + pricePrecision = self.safe_string(market, 'price_precision') + priceLimit = self.parse_precision(pricePrecision) + amountLimit = self.safe_string(market, 'minimum_order_size') + return { + 'id': id, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': True, + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(self.parse_precision(self.safe_string(market, 'amount_precision'))), + 'price': self.parse_number(self.parse_precision((pricePrecision))), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.parse_number(amountLimit), + 'max': self.safe_number(market, 'maximum_order_size'), + }, + 'price': { + 'min': self.parse_number(priceLimit), + 'max': None, + }, + 'cost': { + 'min': self.parse_number(Precise.string_mul(priceLimit, amountLimit)), + 'max': None, + }, + }, + 'created': None, + 'info': market, + } + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + + https://btc-alpha.github.io/api-docs/#tickers + + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + response = await self.publicGetTicker(params) + # + # [ + # { + # "timestamp": "1674658.445272", + # "pair": "BTC_USDT", + # "last": "22476.85", + # "diff": "458.96", + # "vol": "6660.847784", + # "high": "23106.08", + # "low": "22348.29", + # "buy": "22508.46", + # "sell": "22521.11" + # }, + # ... + # ] + # + return self.parse_tickers(response, symbols) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + + https://btc-alpha.github.io/api-docs/#tickers + + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + response = await self.publicGetTicker(self.extend(request, params)) + # + # { + # "timestamp": "1674658.445272", + # "pair": "BTC_USDT", + # "last": "22476.85", + # "diff": "458.96", + # "vol": "6660.847784", + # "high": "23106.08", + # "low": "22348.29", + # "buy": "22508.46", + # "sell": "22521.11" + # } + # + return self.parse_ticker(response, market) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "timestamp": "1674658.445272", + # "pair": "BTC_USDT", + # "last": "22476.85", + # "diff": "458.96", + # "vol": "6660.847784", + # "high": "23106.08", + # "low": "22348.29", + # "buy": "22508.46", + # "sell": "22521.11" + # } + # + timestampStr = self.safe_string(ticker, 'timestamp') + timestamp = int(Precise.string_mul(timestampStr, '1000000')) + marketId = self.safe_string(ticker, 'pair') + market = self.safe_market(marketId, market, '_') + last = self.safe_string(ticker, 'last') + return self.safe_ticker({ + 'info': ticker, + 'symbol': market['symbol'], + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': self.safe_string(ticker, 'buy'), + 'bidVolume': None, + 'ask': self.safe_string(ticker, 'sell'), + 'askVolume': None, + 'vwap': None, + 'open': None, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': self.safe_string(ticker, 'diff'), + 'percentage': None, + 'average': None, + 'baseVolume': None, + 'quoteVolume': self.safe_string(ticker, 'vol'), + }, market) + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + + https://btc-alpha.github.io/api-docs/#get-orderbook + + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair_name': market['id'], + } + if limit: + request['limit_sell'] = limit + request['limit_buy'] = limit + response = await self.publicGetOrderbookPairName(self.extend(request, params)) + return self.parse_order_book(response, market['symbol'], None, 'buy', 'sell', 'price', 'amount') + + def parse_bids_asks(self, bidasks, priceKey: IndexType = 0, amountKey: IndexType = 1, countOrIdKey: IndexType = 2): + result = [] + for i in range(0, len(bidasks)): + bidask = bidasks[i] + if bidask: + result.append(self.parse_bid_ask(bidask, priceKey, amountKey)) + return result + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(public) + # + # { + # "id": "202203440", + # "timestamp": "1637856276.264215", + # "pair": "AAVE_USDT", + # "price": "320.79900000", + # "amount": "0.05000000", + # "type": "buy" + # } + # + # fetchMyTrades(private) + # + # { + # "id": "202203440", + # "timestamp": "1637856276.264215", + # "pair": "AAVE_USDT", + # "price": "320.79900000", + # "amount": "0.05000000", + # "type": "buy", + # "my_side": "buy" + # } + # + marketId = self.safe_string(trade, 'pair') + market = self.safe_market(marketId, market, '_') + timestampRaw = self.safe_string(trade, 'timestamp') + timestamp = self.parse_to_int(Precise.string_mul(timestampRaw, '1000000')) + priceString = self.safe_string(trade, 'price') + amountString = self.safe_string(trade, 'amount') + id = self.safe_string(trade, 'id') + side = self.safe_string_2(trade, 'my_side', 'type') + return self.safe_trade({ + 'id': id, + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'order': id, + 'type': 'limit', + 'side': side, + 'takerOrMaker': None, + 'price': priceString, + 'amount': amountString, + 'cost': None, + 'fee': None, + }, market) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://btc-alpha.github.io/api-docs/#list-all-exchanges + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = None + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['pair'] = market['id'] + if limit is not None: + request['limit'] = limit + trades = await self.publicGetExchanges(self.extend(request, params)) + return self.parse_trades(trades, market, since, limit) + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://btc-alpha.github.io/api-docs/#list-own-deposits + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + currency = None + if code is not None: + currency = self.currency(code) + response = await self.privateGetDeposits(params) + # + # [ + # { + # "timestamp": 1485363039.18359, + # "id": 317, + # "currency": "BTC", + # "amount": 530.00000000 + # } + # ] + # + return self.parse_transactions(response, currency, since, limit, {'type': 'deposit'}) + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://btc-alpha.github.io/api-docs/#list-own-made-withdraws + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + currency = None + request: dict = {} + if code is not None: + currency = self.currency(code) + request['currency_id'] = currency['id'] + response = await self.privateGetWithdraws(self.extend(request, params)) + # + # [ + # { + # "id": 403, + # "timestamp": 1485363466.868539, + # "currency": "BTC", + # "amount": 0.53000000, + # "status": 20 + # } + # ] + # + return self.parse_transactions(response, currency, since, limit, {'type': 'withdrawal'}) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # deposit + # { + # "timestamp": 1485363039.18359, + # "id": 317, + # "currency": "BTC", + # "amount": 530.00000000 + # } + # + # withdrawal + # { + # "id": 403, + # "timestamp": 1485363466.868539, + # "currency": "BTC", + # "amount": 0.53000000, + # "status": 20 + # } + # + timestamp = self.safe_timestamp(transaction, 'timestamp') + currencyId = self.safe_string(transaction, 'currency') + statusId = self.safe_string(transaction, 'status') + return { + 'id': self.safe_string(transaction, 'id'), + 'info': transaction, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': None, + 'address': None, + 'addressTo': None, + 'addressFrom': None, + 'tag': None, + 'tagTo': None, + 'tagFrom': None, + 'currency': self.safe_currency_code(currencyId, currency), + 'amount': self.safe_number(transaction, 'amount'), + 'txid': None, + 'type': None, + 'status': self.parse_transaction_status(statusId), + 'comment': None, + 'internal': None, + 'fee': None, + 'updated': None, + } + + def parse_transaction_status(self, status: Str): + statuses: dict = { + '10': 'pending', # New + '20': 'pending', # Verified, waiting for approving + '30': 'ok', # Approved by moderator + '40': 'failed', # Refused by moderator. See your email for more details + '50': 'canceled', # Cancelled by user + } + return self.safe_string(statuses, status, status) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # { + # "time":1591296000, + # "open":0.024746, + # "close":0.024728, + # "low":0.024728, + # "high":0.024753, + # "volume":16.624 + # } + # + return [ + self.safe_timestamp(ohlcv, 'time'), + self.safe_number(ohlcv, 'open'), + self.safe_number(ohlcv, 'high'), + self.safe_number(ohlcv, 'low'), + self.safe_number(ohlcv, 'close'), + self.safe_number(ohlcv, 'volume'), + ] + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '5m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://btc-alpha.github.io/api-docs/#charts + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + 'type': self.safe_string(self.timeframes, timeframe, timeframe), + } + if limit is not None: + request['limit'] = limit + if since is not None: + request['since'] = self.parse_to_int(since / 1000) + response = await self.publicGetChartsPairTypeChart(self.extend(request, params)) + # + # [ + # {"time":1591296000,"open":0.024746,"close":0.024728,"low":0.024728,"high":0.024753,"volume":16.624}, + # {"time":1591295700,"open":0.024718,"close":0.02475,"low":0.024711,"high":0.02475,"volume":31.645}, + # {"time":1591295400,"open":0.024721,"close":0.024717,"low":0.024711,"high":0.02473,"volume":65.071} + # ] + # + return self.parse_ohlcvs(response, market, timeframe, since, limit) + + def parse_balance(self, response) -> Balances: + result: dict = {'info': response} + for i in range(0, len(response)): + balance = response[i] + currencyId = self.safe_string(balance, 'currency') + code = self.safe_currency_code(currencyId) + account = self.account() + account['used'] = self.safe_string(balance, 'reserve') + account['total'] = self.safe_string(balance, 'balance') + result[code] = account + return self.safe_balance(result) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://btc-alpha.github.io/api-docs/#list-own-wallets + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + response = await self.privateGetWallets(params) + return self.parse_balance(response) + + def parse_order_status(self, status: Str): + statuses: dict = { + '1': 'open', + '2': 'canceled', + '3': 'closed', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # fetchClosedOrders / fetchOrder + # { + # "id": "923763073", + # "date": "1635451090368", + # "type": "sell", + # "pair": "XRP_USDT", + # "price": "1.00000000", + # "amount": "0.00000000", + # "status": "3", + # "amount_filled": "10.00000000", + # "amount_original": "10.0" + # "trades": [], + # } + # + # createOrder + # { + # "success": True, + # "date": "1635451754.497541", + # "type": "sell", + # "oid": "923776755", + # "price": "1.0", + # "amount": "10.0", + # "amount_filled": "0.0", + # "amount_original": "10.0", + # "trades": [] + # } + # + marketId = self.safe_string(order, 'pair') + market = self.safe_market(marketId, market, '_') + symbol = market['symbol'] + success = self.safe_bool(order, 'success', False) + timestamp = None + if success: + timestamp = self.safe_timestamp(order, 'date') + else: + timestamp = self.safe_integer(order, 'date') + price = self.safe_string(order, 'price') + remaining = self.safe_string(order, 'amount') + filled = self.safe_string(order, 'amount_filled') + amount = self.safe_string(order, 'amount_original') + status = self.parse_order_status(self.safe_string(order, 'status')) + id = self.safe_string_n(order, ['oid', 'id', 'order']) + trades = self.safe_value(order, 'trades') + side = self.safe_string_2(order, 'my_side', 'type') + return self.safe_order({ + 'id': id, + 'clientOrderId': None, + 'datetime': self.iso8601(timestamp), + 'timestamp': timestamp, + 'status': status, + 'symbol': symbol, + 'type': 'limit', + 'timeInForce': None, + 'postOnly': None, + 'side': side, + 'price': price, + 'triggerPrice': None, + 'cost': None, + 'amount': amount, + 'filled': filled, + 'remaining': remaining, + 'trades': trades, + 'fee': None, + 'info': order, + 'lastTradeTimestamp': None, + 'average': None, + }, market) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + + https://btc-alpha.github.io/api-docs/#create-order + + create a trade order + :param str symbol: unified symbol of the market to create an order in + :param str type: 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + if type == 'market': + raise InvalidOrder(self.id + ' only limits orders are supported') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + 'type': side, + 'amount': amount, + 'price': self.price_to_precision(symbol, price), + } + response = await self.privatePostOrder(self.extend(request, params)) + if not response['success']: + raise InvalidOrder(self.id + ' ' + self.json(response)) + order = self.parse_order(response, market) + orderAmount = str(order['amount']) + amount = order['amount'] if Precise.string_gt(orderAmount, '0') else amount + order['amount'] = self.parse_number(amount) + return order + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + + https://btc-alpha.github.io/api-docs/#cancel-order + + cancels an open order + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + request: dict = { + 'order': id, + } + response = await self.privatePostOrderCancel(self.extend(request, params)) + # + # { + # "order": 63568 + # } + # + return self.parse_order(response) + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + + https://btc-alpha.github.io/api-docs/#retrieve-single-order + + fetches information on an order made by the user + :param str id: the order id + :param str symbol: not used by btcalpha fetchOrder + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + request: dict = { + 'id': id, + } + order = await self.privateGetOrderId(self.extend(request, params)) + return self.parse_order(order) + + async def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + + https://btc-alpha.github.io/api-docs/#list-own-orders + + fetches information on multiple orders made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['pair'] = market['id'] + if limit is not None: + request['limit'] = limit + orders = await self.privateGetOrdersOwn(self.extend(request, params)) + return self.parse_orders(orders, market, since, limit) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://btc-alpha.github.io/api-docs/#list-own-orders + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + request: dict = { + 'status': '1', + } + return await self.fetch_orders(symbol, since, limit, self.extend(request, params)) + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://btc-alpha.github.io/api-docs/#list-own-orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + request: dict = { + 'status': '3', + } + return await self.fetch_orders(symbol, since, limit, self.extend(request, params)) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://btc-alpha.github.io/api-docs/#list-own-exchanges + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['pair'] = market['id'] + if limit is not None: + request['limit'] = limit + trades = await self.privateGetExchangesOwn(self.extend(request, params)) + return self.parse_trades(trades, None, since, limit) + + def nonce(self): + return self.milliseconds() + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + query = self.urlencode(self.keysort(self.omit(params, self.extract_params(path)))) + url = self.urls['api']['rest'] + '/' + if path != 'charts/{pair}/{type}/chart/': + url += 'v1/' + url += self.implode_params(path, params) + headers = {'Accept': 'application/json'} + if api == 'public': + if len(query): + url += '?' + query + else: + self.check_required_credentials() + payload = self.apiKey + if method == 'POST': + headers['Content-Type'] = 'application/x-www-form-urlencoded' + body = query + payload += body + elif len(query): + url += '?' + query + headers['X-KEY'] = self.apiKey + headers['X-SIGN'] = self.hmac(self.encode(payload), self.encode(self.secret), hashlib.sha256) + headers['X-NONCE'] = str(self.nonce()) + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None # fallback to default error handler + # + # {"date":1570599531.4814300537,"error":"Out of balance -9.99243661 BTC"} + # + error = self.safe_string(response, 'error') + if error is not None: + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], error, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], error, feedback) + raise ExchangeError(feedback) # unknown error + return None diff --git a/ccxt/async_support/btcbox.py b/ccxt/async_support/btcbox.py new file mode 100644 index 0000000..7522461 --- /dev/null +++ b/ccxt/async_support/btcbox.py @@ -0,0 +1,814 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.btcbox import ImplicitAPI +import asyncio +import hashlib +import json +from ccxt.base.types import Any, Balances, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import DDoSProtection +from ccxt.base.errors import InvalidNonce +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class btcbox(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(btcbox, self).describe(), { + 'id': 'btcbox', + 'name': 'BtcBox', + 'countries': ['JP'], + 'rateLimit': 1000, + 'version': 'v1', + 'pro': False, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelOrder': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createOrder': True, + 'createOrderWithTakeProfitAndStopLoss': False, + 'createOrderWithTakeProfitAndStopLossWs': False, + 'createPostOnlyOrder': False, + 'createReduceOnlyOrder': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': False, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkOHLCV': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTrades': True, + 'fetchTransfer': False, + 'fetchTransfers': False, + 'fetchVolatilityHistory': False, + 'fetchWithdrawal': False, + 'fetchWithdrawals': False, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'repayMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'transfer': False, + 'withdraw': False, + 'ws': False, + }, + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/1e2cb499-8d0f-4f8f-9464-3c015cfbc76b', + 'api': { + 'rest': 'https://www.btcbox.co.jp/api', + }, + 'www': 'https://www.btcbox.co.jp/', + 'doc': 'https://blog.btcbox.jp/en/archives/8762', + 'fees': 'https://support.btcbox.co.jp/hc/en-us/articles/360001235694-Fees-introduction', + }, + 'api': { + 'public': { + 'get': [ + 'depth', + 'orders', + 'ticker', + 'tickers', + ], + }, + 'private': { + 'post': [ + 'balance', + 'trade_add', + 'trade_cancel', + 'trade_list', + 'trade_view', + 'wallet', + ], + }, + 'webApi': { + 'get': [ + 'ajax/coin/coinInfo', + ], + }, + }, + 'options': { + 'fetchMarkets': { + 'webApiEnable': True, # fetches from WEB + 'webApiRetries': 3, + }, + 'amountPrecision': '0.0001', # exchange has only few pairs and all of them + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': False, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': False, + 'FOK': False, + 'PO': False, + 'GTD': False, + }, + 'hedged': False, + 'leverage': False, + 'marketBuyRequiresPrice': False, + 'marketBuyByCost': False, + 'selfTradePrevention': False, + 'trailing': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': None, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 100, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': None, + 'untilDays': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchClosedOrders': None, + 'fetchOHLCV': None, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + '104': AuthenticationError, + '105': PermissionDenied, + '106': InvalidNonce, + '107': InvalidOrder, # price should be an integer + '200': InsufficientFunds, + '201': InvalidOrder, # amount too small + '202': InvalidOrder, # price should be [0 : 1000000] + '203': OrderNotFound, + '401': OrderNotFound, # cancel canceled, closed or non-existent order + '402': DDoSProtection, + }, + }) + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for ace + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + promise1 = self.publicGetTickers() + promise2 = self.fetch_web_endpoint('fetchMarkets', 'webApiGetAjaxCoinCoinInfo', True) + response1, response2 = await asyncio.gather(*[promise1, promise2]) + # + result2Data = self.safe_dict(response2, 'data', {}) + marketIds = list(response1.keys()) + markets = [] + for i in range(0, len(marketIds)): + marketId = marketIds[i] + symbolParts = marketId.split('_') + baseCurr = self.safe_string(symbolParts, 0) + quote = self.safe_string(symbolParts, 1) + quoteId = quote.lower() + id = baseCurr.lower() + res = response1[marketId] + symbol = baseCurr + '/' + quote + fee = self.parse_number('0.0005') if (id == 'BTC') else self.parse_number('0.0010') + details = self.safe_dict(result2Data, id, {}) + tradeDetails = self.safe_dict(details, 'trade', {}) + markets.append(self.safe_market_structure({ + 'id': id, + 'uppercaseId': None, + 'symbol': symbol, + 'base': baseCurr, + 'baseId': id, + 'quote': quote, + 'quoteId': quoteId, + 'settle': None, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'taker': fee, + 'maker': fee, + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + 'leverage': { + 'min': None, + 'max': None, + }, + }, + 'precision': { + 'price': self.parse_number(self.parse_precision(self.safe_string(tradeDetails, 'pricedecimal'))), + 'amount': None, + }, + 'active': self.safe_string(tradeDetails, 'enable') == '1', + 'created': None, + 'info': res, + })) + return markets + + def parse_market(self, market: dict) -> Market: + baseId = self.safe_string(market, 'base') + base = self.safe_currency_code(baseId) + quoteId = self.safe_string(market, 'quote') + quote = self.safe_currency_code(quoteId) + symbol = base + '/' + quote + return { + 'id': self.safe_string(market, 'symbol'), + 'uppercaseId': None, + 'symbol': symbol, + 'base': base, + 'baseId': baseId, + 'quote': quote, + 'quoteId': quoteId, + 'settle': None, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'limits': { + 'amount': { + 'min': self.safe_number(market, 'minLimitBaseAmount'), + 'max': self.safe_number(market, 'maxLimitBaseAmount'), + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + 'leverage': { + 'min': None, + 'max': None, + }, + }, + 'precision': { + 'price': self.parse_number(self.parse_precision(self.safe_string(market, 'quotePrecision'))), + 'amount': self.parse_number(self.parse_precision(self.safe_string(market, 'basePrecision'))), + }, + 'active': None, + 'created': None, + 'info': market, + } + + def parse_balance(self, response) -> Balances: + result: dict = {'info': response} + codes = list(self.currencies.keys()) + for i in range(0, len(codes)): + code = codes[i] + currency = self.currency(code) + currencyId = currency['id'] + free = currencyId + '_balance' + if free in response: + account = self.account() + used = currencyId + '_lock' + account['free'] = self.safe_string(response, free) + account['used'] = self.safe_string(response, used) + result[code] = account + return self.safe_balance(result) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://blog.btcbox.jp/en/archives/8762#toc13 + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + response = await self.privatePostBalance(params) + return self.parse_balance(response) + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://blog.btcbox.jp/en/archives/8762#toc6 + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = {} + numSymbols = len(self.symbols) + if numSymbols > 1: + request['coin'] = market['baseId'] + response = await self.publicGetDepth(self.extend(request, params)) + return self.parse_order_book(response, market['symbol']) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + symbol = self.safe_symbol(None, market) + last = self.safe_string(ticker, 'last') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': None, + 'datetime': None, + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': self.safe_string(ticker, 'buy'), + 'bidVolume': None, + 'ask': self.safe_string(ticker, 'sell'), + 'askVolume': None, + 'vwap': None, + 'open': None, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': self.safe_string(ticker, 'vol'), + 'quoteVolume': self.safe_string(ticker, 'volume'), + 'info': ticker, + }, market) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://blog.btcbox.jp/en/archives/8762#toc5 + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = {} + numSymbols = len(self.symbols) + if numSymbols > 1: + request['coin'] = market['baseId'] + response = await self.publicGetTicker(self.extend(request, params)) + return self.parse_ticker(response, market) + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + :param str[] [symbols]: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + response = await self.publicGetTickers(params) + return self.parse_tickers(response, symbols) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(public) + # + # { + # "date":"0", + # "price":3, + # "amount":0.1, + # "tid":"1", + # "type":"buy" + # } + # + timestamp = self.safe_timestamp(trade, 'date') + market = self.safe_market(None, market) + id = self.safe_string(trade, 'tid') + priceString = self.safe_string(trade, 'price') + amountString = self.safe_string(trade, 'amount') + type = None + side = self.safe_string(trade, 'type') + return self.safe_trade({ + 'info': trade, + 'id': id, + 'order': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'type': type, + 'side': side, + 'takerOrMaker': None, + 'price': priceString, + 'amount': amountString, + 'cost': None, + 'fee': None, + }, market) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://blog.btcbox.jp/en/archives/8762#toc7 + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = {} + numSymbols = len(self.symbols) + if numSymbols > 1: + request['coin'] = market['baseId'] + response = await self.publicGetOrders(self.extend(request, params)) + # + # [ + # { + # "date":"0", + # "price":3, + # "amount":0.1, + # "tid":"1", + # "type":"buy" + # }, + # ] + # + return self.parse_trades(response, market, since, limit) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://blog.btcbox.jp/en/archives/8762#toc18 + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'amount': amount, + 'price': price, + 'type': side, + 'coin': market['baseId'], + } + response = await self.privatePostTradeAdd(self.extend(request, params)) + # + # { + # "result":true, + # "id":"11" + # } + # + return self.parse_order(response, market) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://blog.btcbox.jp/en/archives/8762#toc17 + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + # a special case for btcbox – default symbol is BTC/JPY + if symbol is None: + symbol = 'BTC/JPY' + market = self.market(symbol) + request: dict = { + 'id': id, + 'coin': market['baseId'], + } + response = await self.privatePostTradeCancel(self.extend(request, params)) + # + # {"result":true, "id":"11"} + # + return self.parse_order(response, market) + + def parse_order_status(self, status: Str): + statuses: dict = { + # TODO: complete list + 'part': 'open', # partially or not at all executed + 'all': 'closed', # fully executed + 'cancelled': 'canceled', + 'closed': 'closed', # never encountered, seems to be bug in the doc + 'no': 'closed', # not clarified in the docs... + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # { + # "id":11, + # "datetime":"2014-10-21 10:47:20", + # "type":"sell", + # "price":42000, + # "amount_original":1.2, + # "amount_outstanding":1.2, + # "status":"closed", + # "trades":[] # no clarification of trade value structure of order endpoint + # } + # + id = self.safe_string(order, 'id') + datetimeString = self.safe_string(order, 'datetime') + timestamp = None + if datetimeString is not None: + timestamp = self.parse8601(order['datetime'] + '+09:00') # Tokyo time + amount = self.safe_string(order, 'amount_original') + remaining = self.safe_string(order, 'amount_outstanding') + price = self.safe_string(order, 'price') + # status is set by fetchOrder method only + status = self.parse_order_status(self.safe_string(order, 'status')) + # fetchOrders do not return status, use heuristic + if status is None: + if Precise.string_equals(remaining, '0'): + status = 'closed' + trades = None # todo: self.parse_trades(order['trades']) + market = self.safe_market(None, market) + side = self.safe_string(order, 'type') + return self.safe_order({ + 'id': id, + 'clientOrderId': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'amount': amount, + 'remaining': remaining, + 'filled': None, + 'side': side, + 'type': None, + 'timeInForce': None, + 'postOnly': None, + 'status': status, + 'symbol': market['symbol'], + 'price': price, + 'triggerPrice': None, + 'cost': None, + 'trades': trades, + 'fee': None, + 'info': order, + 'average': None, + }, market) + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://blog.btcbox.jp/en/archives/8762#toc16 + + :param str id: the order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + # a special case for btcbox – default symbol is BTC/JPY + if symbol is None: + symbol = 'BTC/JPY' + market = self.market(symbol) + request = self.extend({ + 'id': id, + 'coin': market['baseId'], + }, params) + response = await self.privatePostTradeView(self.extend(request, params)) + # + # { + # "id":11, + # "datetime":"2014-10-21 10:47:20", + # "type":"sell", + # "price":42000, + # "amount_original":1.2, + # "amount_outstanding":1.2, + # "status":"closed", + # "trades":[] + # } + # + return self.parse_order(response, market) + + async def fetch_orders_by_type(self, type, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + await self.load_markets() + # a special case for btcbox – default symbol is BTC/JPY + market = self.market(symbol) + request: dict = { + 'type': type, # 'open' or 'all' + 'coin': market['baseId'], + } + response = await self.privatePostTradeList(self.extend(request, params)) + # + # [ + # { + # "id":"7", + # "datetime":"2014-10-20 13:27:38", + # "type":"buy", + # "price":42750, + # "amount_original":0.235, + # "amount_outstanding":0.235 + # }, + # ] + # + orders = self.parse_orders(response, market, since, limit) + # status(open/closed/canceled) is None + # btcbox does not return status, but we know it's 'open' queried for open orders + if type == 'open': + for i in range(0, len(orders)): + orders[i]['status'] = 'open' + return orders + + async def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://blog.btcbox.jp/en/archives/8762#toc15 + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + return await self.fetch_orders_by_type('all', symbol, since, limit, params) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://blog.btcbox.jp/en/archives/8762#toc15 + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + return await self.fetch_orders_by_type('open', symbol, since, limit, params) + + def nonce(self): + return self.milliseconds() + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = self.urls['api']['rest'] + '/' + self.version + '/' + path + if api == 'public': + if params: + url += '?' + self.urlencode(params) + elif api == 'webApi': + url = self.urls['www'] + '/' + path + else: + self.check_required_credentials() + nonce = str(self.nonce()) + query = self.extend({ + 'key': self.apiKey, + 'nonce': nonce, + }, params) + request = self.urlencode(query) + secret = self.hash(self.encode(self.secret), 'md5') + query['signature'] = self.hmac(self.encode(request), self.encode(secret), hashlib.sha256) + body = self.urlencode(query) + headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + } + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None # resort to defaultErrorHandler + # typical error response: {"result":false,"code":"401"} + if httpCode >= 400: + return None # resort to defaultErrorHandler + result = self.safe_value(response, 'result') + if result is None or result is True: + return None # either public API(no error codes expected) or success + code = self.safe_value(response, 'code') + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions, code, feedback) + raise ExchangeError(feedback) # unknown message + + async def request(self, path, api='public', method='GET', params={}, headers=None, body=None, config={}): + response = await self.fetch2(path, api, method, params, headers, body, config) + if isinstance(response, str): + # sometimes the exchange returns whitespace prepended to json + response = self.strip(response) + if not self.is_json_encoded_object(response): + raise ExchangeError(self.id + ' ' + response) + response = json.loads(response) + return response diff --git a/ccxt/async_support/btcmarkets.py b/ccxt/async_support/btcmarkets.py new file mode 100644 index 0000000..e398af7 --- /dev/null +++ b/ccxt/async_support/btcmarkets.py @@ -0,0 +1,1377 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.btcmarkets import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currency, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Ticker, Trade, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class btcmarkets(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(btcmarkets, self).describe(), { + 'id': 'btcmarkets', + 'name': 'BTC Markets', + 'countries': ['AU'], # Australia + 'rateLimit': 1000, # market data cached for 1 second(trades cached for 2 seconds) + 'version': 'v3', + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelOrder': True, + 'cancelOrders': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createDepositAddress': False, + 'createOrder': True, + 'createOrderWithTakeProfitAndStopLoss': False, + 'createOrderWithTakeProfitAndStopLossWs': False, + 'createPostOnlyOrder': False, + 'createReduceOnlyOrder': False, + 'createTriggerOrder': True, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchClosedOrders': 'emulated', + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': False, + 'fetchDepositAddress': False, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchDepositsWithdrawals': True, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTransactions': 'emulated', + 'fetchVolatilityHistory': False, + 'fetchWithdrawals': True, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'repayMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'withdraw': True, + }, + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/8c8d6907-3873-4cc4-ad20-e22fba28247e', + 'api': { + 'public': 'https://api.btcmarkets.net', + 'private': 'https://api.btcmarkets.net', + }, + 'www': 'https://btcmarkets.net', + 'doc': [ + 'https://api.btcmarkets.net/doc/v3', + 'https://github.com/BTCMarkets/API', + ], + }, + 'api': { + 'public': { + 'get': [ + 'markets', + 'markets/{marketId}/ticker', + 'markets/{marketId}/trades', + 'markets/{marketId}/orderbook', + 'markets/{marketId}/candles', + 'markets/tickers', + 'markets/orderbooks', + 'time', + ], + }, + 'private': { + 'get': [ + 'orders', + 'orders/{id}', + 'batchorders/{ids}', + 'trades', + 'trades/{id}', + 'withdrawals', + 'withdrawals/{id}', + 'deposits', + 'deposits/{id}', + 'transfers', + 'transfers/{id}', + 'addresses', + 'withdrawal-fees', + 'assets', + 'accounts/me/trading-fees', + 'accounts/me/withdrawal-limits', + 'accounts/me/balances', + 'accounts/me/transactions', + 'reports/{id}', + ], + 'post': [ + 'orders', + 'batchorders', + 'withdrawals', + 'reports', + ], + 'delete': [ + 'orders', + 'orders/{id}', + 'batchorders/{ids}', + ], + 'put': [ + 'orders/{id}', + ], + }, + }, + 'timeframes': { + '1m': '1m', + '1h': '1h', + '1d': '1d', + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, # todo: check + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'leverage': False, + 'marketBuyRequiresPrice': False, + 'marketBuyByCost': False, + 'selfTradePrevention': True, # todo: check + 'trailing': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 100000, + 'untilDays': 100000, + 'symbolRequired': True, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 100, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 100000, + 'untilDays': 100000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 100000, + 'daysBackCanceled': 1, + 'untilDays': 100000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 1000, + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'exact': { + 'InsufficientFund': InsufficientFunds, + 'InvalidPrice': InvalidOrder, + 'InvalidAmount': InvalidOrder, + 'MissingArgument': BadRequest, + 'OrderAlreadyCancelled': InvalidOrder, + 'OrderNotFound': OrderNotFound, + 'OrderStatusIsFinal': InvalidOrder, + 'InvalidPaginationParameter': BadRequest, + }, + 'broad': { + }, + }, + 'fees': { + 'percentage': True, + 'tierBased': True, + 'maker': self.parse_number('-0.0005'), + 'taker': self.parse_number('0.0020'), + }, + 'options': { + 'fees': { + 'AUD': { + 'maker': self.parse_number('0.0085'), + 'taker': self.parse_number('0.0085'), + }, + }, + }, + }) + + async def fetch_transactions_with_method(self, method, code: Str = None, since: Int = None, limit: Int = None, params={}): + await self.load_markets() + request: dict = {} + if limit is not None: + request['limit'] = limit + if since is not None: + request['after'] = since + currency = None + if code is not None: + currency = self.currency(code) + response = await getattr(self, method)(self.extend(request, params)) + return self.parse_transactions(response, currency, since, limit) + + async def fetch_deposits_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch history of deposits and withdrawals + + https://docs.btcmarkets.net/v3/#tag/Fund-Management-APIs/paths/~1v3~1transfers/get + + :param str [code]: unified currency code for the currency of the deposit/withdrawals, default is None + :param int [since]: timestamp in ms of the earliest deposit/withdrawal, default is None + :param int [limit]: max number of deposit/withdrawals to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `transaction structure ` + """ + return await self.fetch_transactions_with_method('privateGetTransfers', code, since, limit, params) + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://docs.btcmarkets.net/v3/#tag/Fund-Management-APIs/paths/~1v3~1deposits/get + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + return await self.fetch_transactions_with_method('privateGetDeposits', code, since, limit, params) + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://docs.btcmarkets.net/v3/#tag/Fund-Management-APIs/paths/~1v3~1withdrawals/get + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + return await self.fetch_transactions_with_method('privateGetWithdrawals', code, since, limit, params) + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'Accepted': 'pending', + 'Pending Authorization': 'pending', + 'Complete': 'ok', + 'Cancelled': 'cancelled', + 'Failed': 'failed', + } + return self.safe_string(statuses, status, status) + + def parse_transaction_type(self, type): + statuses: dict = { + 'Withdraw': 'withdrawal', + 'Deposit': 'deposit', + } + return self.safe_string(statuses, type, type) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # { + # "id": "6500230339", + # "assetName": "XRP", + # "amount": "500", + # "type": "Deposit", + # "creationTime": "2020-07-27T07:52:08.640000Z", + # "status": "Complete", + # "description": "RIPPLE Deposit, XRP 500", + # "fee": "0", + # "lastUpdate": "2020-07-27T07:52:08.665000Z", + # "paymentDetail": { + # "txId": "lsjflsjdfljsd", + # "address": "kjasfkjsdf?dt=873874545" + # } + # } + # + # { + # "id": "500985282", + # "assetName": "BTC", + # "amount": "0.42570126", + # "type": "Withdraw", + # "creationTime": "2017-07-29T12:49:03.931000Z", + # "status": "Complete", + # "description": "BTC withdraw from [nick-btcmarkets@snowmonkey.co.uk] to Address: 1B9DsnSYQ54VMqFHVJYdGoLMCYzFwrQzsj amount: 0.42570126 fee: 0.00000000", + # "fee": "0.0005", + # "lastUpdate": "2017-07-29T12:52:20.676000Z", + # "paymentDetail": { + # "txId": "fkjdsfjsfljsdfl", + # "address": "a;daddjas;djas" + # } + # } + # + # { + # "id": "505102262", + # "assetName": "XRP", + # "amount": "979.836", + # "type": "Deposit", + # "creationTime": "2017-07-31T08:50:01.053000Z", + # "status": "Complete", + # "description": "Ripple Deposit, X 979.8360", + # "fee": "0", + # "lastUpdate": "2017-07-31T08:50:01.290000Z" + # } + # + timestamp = self.parse8601(self.safe_string(transaction, 'creationTime')) + lastUpdate = self.parse8601(self.safe_string(transaction, 'lastUpdate')) + type = self.parse_transaction_type(self.safe_string_lower(transaction, 'type')) + if type == 'withdraw': + type = 'withdrawal' + cryptoPaymentDetail = self.safe_dict(transaction, 'paymentDetail', {}) + txid = self.safe_string(cryptoPaymentDetail, 'txId') + address = self.safe_string(cryptoPaymentDetail, 'address') + tag = None + if address is not None: + addressParts = address.split('?dt=') + numParts = len(addressParts) + if numParts > 1: + address = addressParts[0] + tag = addressParts[1] + addressTo = address + tagTo = tag + addressFrom = None + tagFrom = None + fee = self.safe_string(transaction, 'fee') + status = self.parse_transaction_status(self.safe_string(transaction, 'status')) + currencyId = self.safe_string(transaction, 'assetName') + code = self.safe_currency_code(currencyId) + amount = self.safe_string(transaction, 'amount') + if fee: + amount = Precise.string_sub(amount, fee) + return { + 'id': self.safe_string(transaction, 'id'), + 'txid': txid, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': None, + 'address': address, + 'addressTo': addressTo, + 'addressFrom': addressFrom, + 'tag': tag, + 'tagTo': tagTo, + 'tagFrom': tagFrom, + 'type': type, + 'amount': self.parse_number(amount), + 'currency': code, + 'status': status, + 'updated': lastUpdate, + 'comment': self.safe_string(transaction, 'description'), + 'internal': None, + 'fee': { + 'currency': code, + 'cost': self.parse_number(fee), + 'rate': None, + }, + 'info': transaction, + } + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for btcmarkets + + https://docs.btcmarkets.net/v3/#tag/Market-Data-APIs/paths/~1v3~1markets/get + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = await self.publicGetMarkets(params) + # + # [ + # { + # "marketId":"COMP-AUD", + # "baseAssetName":"COMP", + # "quoteAssetName":"AUD", + # "minOrderAmount":"0.00006", + # "maxOrderAmount":"1000000", + # "amountDecimals":"8", + # "priceDecimals":"2", + # "status": "Online" + # } + # ] + # + return self.parse_markets(response) + + def parse_market(self, market: dict) -> Market: + baseId = self.safe_string(market, 'baseAssetName') + quoteId = self.safe_string(market, 'quoteAssetName') + id = self.safe_string(market, 'marketId') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + symbol = base + '/' + quote + fees = self.safe_value(self.safe_dict(self.options, 'fees', {}), quote, self.fees) + pricePrecision = self.parse_number(self.parse_precision(self.safe_string(market, 'priceDecimals'))) + minAmount = self.safe_number(market, 'minOrderAmount') + maxAmount = self.safe_number(market, 'maxOrderAmount') + status = self.safe_string(market, 'status') + minPrice = None + if quote == 'AUD': + minPrice = pricePrecision + return { + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': (status == 'Online'), + 'contract': False, + 'linear': None, + 'inverse': None, + 'taker': fees['taker'], + 'maker': fees['maker'], + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(self.parse_precision(self.safe_string(market, 'amountDecimals'))), + 'price': pricePrecision, + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': minAmount, + 'max': maxAmount, + }, + 'price': { + 'min': minPrice, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + 'info': market, + } + + async def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://docs.btcmarkets.net/v3/#tag/Misc-APIs/paths/~1v3~1time/get + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = await self.publicGetTime(params) + # + # { + # "timestamp": "2019-09-01T18:34:27.045000Z" + # } + # + return self.parse8601(self.safe_string(response, 'timestamp')) + + def parse_balance(self, response) -> Balances: + result: dict = {'info': response} + for i in range(0, len(response)): + balance = response[i] + currencyId = self.safe_string(balance, 'assetName') + code = self.safe_currency_code(currencyId) + account = self.account() + account['used'] = self.safe_string(balance, 'locked') + account['total'] = self.safe_string(balance, 'balance') + result[code] = account + return self.safe_balance(result) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://docs.btcmarkets.net/v3/#tag/Account-APIs/paths/~1v3~1accounts~1me~1balances/get + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + response = await self.privateGetAccountsMeBalances(params) + return self.parse_balance(response) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # [ + # "2020-09-12T18:30:00.000000Z", + # "14409.45", # open + # "14409.45", # high + # "14403.91", # low + # "14403.91", # close + # "0.01571701" # volume + # ] + # + return [ + self.parse8601(self.safe_string(ohlcv, 0)), + self.safe_number(ohlcv, 1), # open + self.safe_number(ohlcv, 2), # high + self.safe_number(ohlcv, 3), # low + self.safe_number(ohlcv, 4), # close + self.safe_number(ohlcv, 5), # volume + ] + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://docs.btcmarkets.net/v3/#tag/Market-Data-APIs/paths/~1v3~1markets~1{marketId}~1candles/get + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'marketId': market['id'], + 'timeWindow': self.safe_string(self.timeframes, timeframe, timeframe), + # 'from': self.iso8601(since), + # 'to': self.iso8601(self.milliseconds()), + # 'before': 1234567890123, + # 'after': 1234567890123, + # 'limit': limit, # default 10, max 200 + } + if since is not None: + request['from'] = self.iso8601(since) + if limit is not None: + request['limit'] = min(limit, 200) # default is 10, max 200 + response = await self.publicGetMarketsMarketIdCandles(self.extend(request, params)) + # + # [ + # ["2020-09-12T18:30:00.000000Z","14409.45","14409.45","14403.91","14403.91","0.01571701"], + # ["2020-09-12T18:21:00.000000Z","14409.45","14409.45","14409.45","14409.45","0.0035"], + # ["2020-09-12T18:03:00.000000Z","14361.37","14361.37","14361.37","14361.37","0.00345221"], + # ] + # + return self.parse_ohlcvs(response, market, timeframe, since, limit) + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://docs.btcmarkets.net/v3/#tag/Market-Data-APIs/paths/~1v3~1markets~1{marketId}~1orderbook/get + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'marketId': market['id'], + } + response = await self.publicGetMarketsMarketIdOrderbook(self.extend(request, params)) + # + # { + # "marketId":"BTC-AUD", + # "snapshotId":1599936148941000, + # "asks":[ + # ["14459.45","0.00456475"], + # ["14463.56","2"], + # ["14470.91","0.98"], + # ], + # "bids":[ + # ["14421.01","0.52"], + # ["14421","0.75"], + # ["14418","0.3521"], + # ] + # } + # + timestamp = self.safe_integer_product(response, 'snapshotId', 0.001) + orderbook = self.parse_order_book(response, symbol, timestamp) + orderbook['nonce'] = self.safe_integer(response, 'snapshotId') + return orderbook + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # fetchTicker + # + # { + # "marketId":"BAT-AUD", + # "bestBid":"0.3751", + # "bestAsk":"0.377", + # "lastPrice":"0.3769", + # "volume24h":"56192.97613335", + # "volumeQte24h":"21179.13270465", + # "price24h":"0.0119", + # "pricePct24h":"3.26", + # "low24h":"0.3611", + # "high24h":"0.3799", + # "timestamp":"2020-08-09T18:28:23.280000Z" + # } + # + marketId = self.safe_string(ticker, 'marketId') + market = self.safe_market(marketId, market, '-') + symbol = market['symbol'] + timestamp = self.parse8601(self.safe_string(ticker, 'timestamp')) + last = self.safe_string(ticker, 'lastPrice') + baseVolume = self.safe_string(ticker, 'volume24h') + quoteVolume = self.safe_string(ticker, 'volumeQte24h') + change = self.safe_string(ticker, 'price24h') + percentage = self.safe_string(ticker, 'pricePct24h') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high24h'), + 'low': self.safe_string(ticker, 'low'), + 'bid': self.safe_string(ticker, 'bestBid'), + 'bidVolume': None, + 'ask': self.safe_string(ticker, 'bestAsk'), + 'askVolume': None, + 'vwap': None, + 'open': None, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': change, + 'percentage': percentage, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'info': ticker, + }, market) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://docs.btcmarkets.net/v3/#tag/Market-Data-APIs/paths/~1v3~1markets~1{marketId}~1ticker/get + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'marketId': market['id'], + } + response = await self.publicGetMarketsMarketIdTicker(self.extend(request, params)) + # + # { + # "marketId":"BAT-AUD", + # "bestBid":"0.3751", + # "bestAsk":"0.377", + # "lastPrice":"0.3769", + # "volume24h":"56192.97613335", + # "volumeQte24h":"21179.13270465", + # "price24h":"0.0119", + # "pricePct24h":"3.26", + # "low24h":"0.3611", + # "high24h":"0.3799", + # "timestamp":"2020-08-09T18:28:23.280000Z" + # } + # + return self.parse_ticker(response, market) + + async def fetch_ticker_2(self, symbol: str, params={}): + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'id': market['id'], + } + response = await self.publicGetMarketsMarketIdTicker(self.extend(request, params)) + return self.parse_ticker(response, market) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # public fetchTrades + # + # { + # "id":"6191646611", + # "price":"539.98", + # "amount":"0.5", + # "timestamp":"2020-08-09T15:21:05.016000Z", + # "side":"Ask" + # } + # + # private fetchMyTrades + # + # { + # "id": "36014819", + # "marketId": "XRP-AUD", + # "timestamp": "2019-06-25T16:01:02.977000Z", + # "price": "0.67", + # "amount": "1.50533262", + # "side": "Ask", + # "fee": "0.00857285", + # "orderId": "3648306", + # "liquidityType": "Taker", + # "clientOrderId": "48" + # } + # + timestamp = self.parse8601(self.safe_string(trade, 'timestamp')) + marketId = self.safe_string(trade, 'marketId') + market = self.safe_market(marketId, market, '-') + feeCurrencyCode = market['quote'] if (market['quote'] == 'AUD') else market['base'] + side = self.safe_string(trade, 'side') + if side == 'Bid': + side = 'buy' + elif side == 'Ask': + side = 'sell' + id = self.safe_string(trade, 'id') + priceString = self.safe_string(trade, 'price') + amountString = self.safe_string(trade, 'amount') + orderId = self.safe_string(trade, 'orderId') + fee = None + feeCostString = self.safe_string(trade, 'fee') + if feeCostString is not None: + fee = { + 'cost': feeCostString, + 'currency': feeCurrencyCode, + } + takerOrMaker = self.safe_string_lower(trade, 'liquidityType') + return self.safe_trade({ + 'info': trade, + 'id': id, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'order': orderId, + 'symbol': market['symbol'], + 'type': None, + 'side': side, + 'price': priceString, + 'amount': amountString, + 'cost': None, + 'takerOrMaker': takerOrMaker, + 'fee': fee, + }, market) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://docs.btcmarkets.net/v3/#tag/Market-Data-APIs/paths/~1v3~1markets~1{marketId}~1trades/get + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + # 'since': 59868345231, + 'marketId': market['id'], + } + response = await self.publicGetMarketsMarketIdTrades(self.extend(request, params)) + # + # [ + # {"id":"6191646611","price":"539.98","amount":"0.5","timestamp":"2020-08-09T15:21:05.016000Z","side":"Ask"}, + # {"id":"6191646610","price":"539.99","amount":"0.5","timestamp":"2020-08-09T15:21:05.015000Z","side":"Ask"}, + # {"id":"6191646590","price":"540","amount":"0.00233785","timestamp":"2020-08-09T15:21:04.171000Z","side":"Bid"}, + # ] + # + return self.parse_trades(response, market, since, limit) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://docs.btcmarkets.net/v3/#tag/Order-Placement-APIs/paths/~1v3~1orders/post + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: the price at which a trigger order is triggered at + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'marketId': market['id'], + # 'price': self.price_to_precision(symbol, price), + 'amount': self.amount_to_precision(symbol, amount), + # 'type': 'Limit', # "Limit", "Market", "Stop Limit", "Stop", "Take Profit" + 'side': 'Bid' if (side == 'buy') else 'Ask', + # 'triggerPrice': self.price_to_precision(symbol, triggerPrice), # required for Stop, Stop Limit, Take Profit orders + # 'targetAmount': self.amount_to_precision(symbol, targetAmount), # target amount when a desired target outcome is required for order execution + # 'timeInForce': 'GTC', # GTC, FOK, IOC + # 'postOnly': False, # boolean if self is a post-only order + # 'selfTrade': 'A', # A = allow, P = prevent + # 'clientOrderId': self.uuid(), + } + lowercaseType = type.lower() + orderTypes = self.safe_value(self.options, 'orderTypes', { + 'limit': 'Limit', + 'market': 'Market', + 'stop': 'Stop', + 'stop limit': 'Stop Limit', + 'take profit': 'Take Profit', + }) + request['type'] = self.safe_string(orderTypes, lowercaseType, type) + priceIsRequired = False + triggerPriceIsRequired = False + if lowercaseType == 'limit': + priceIsRequired = True + # elif lowercaseType == 'market': + # ... + # } + elif lowercaseType == 'stop limit': + triggerPriceIsRequired = True + priceIsRequired = True + elif lowercaseType == 'take profit': + triggerPriceIsRequired = True + elif lowercaseType == 'stop': + triggerPriceIsRequired = True + if priceIsRequired: + if price is None: + raise ArgumentsRequired(self.id + ' createOrder() requires a price argument for a ' + type + 'order') + else: + request['price'] = self.price_to_precision(symbol, price) + if triggerPriceIsRequired: + triggerPrice = self.safe_number(params, 'triggerPrice') + params = self.omit(params, 'triggerPrice') + if triggerPrice is None: + raise ArgumentsRequired(self.id + ' createOrder() requires a triggerPrice parameter for a ' + type + 'order') + else: + request['triggerPrice'] = self.price_to_precision(symbol, triggerPrice) + clientOrderId = self.safe_string(params, 'clientOrderId') + if clientOrderId is not None: + request['clientOrderId'] = clientOrderId + params = self.omit(params, 'clientOrderId') + response = await self.privatePostOrders(self.extend(request, params)) + # + # { + # "orderId": "7524", + # "marketId": "BTC-AUD", + # "side": "Bid", + # "type": "Limit", + # "creationTime": "2019-08-30T11:08:21.956000Z", + # "price": "100.12", + # "amount": "1.034", + # "openAmount": "1.034", + # "status": "Accepted", + # "clientOrderId": "1234-5678", + # "timeInForce": "IOC", + # "postOnly": False, + # "selfTrade": "P", + # "triggerAmount": "105", + # "targetAmount": "1000" + # } + # + return self.parse_order(response, market) + + async def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): + """ + cancel multiple orders + + https://docs.btcmarkets.net/v3/#tag/Batch-Order-APIs/paths/~1v3~1batchorders~1{ids}/delete + + :param str[] ids: order ids + :param str symbol: not used by btcmarkets cancelOrders() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an list of `order structures ` + """ + await self.load_markets() + numericIds = [] + for i in range(0, len(ids)): + # numericIds[i] = int(ids[i]) + numericIds.append(int(ids[i])) + request: dict = { + 'ids': numericIds, + } + response = await self.privateDeleteBatchordersIds(self.extend(request, params)) + # + # { + # "cancelOrders": [ + # { + # "orderId": "414186", + # "clientOrderId": "6" + # }, + # ... + # ], + # "unprocessedRequests": [ + # { + # "code": "OrderAlreadyCancelled", + # "message": "order is already cancelled.", + # "requestId": "1" + # } + # ] + # } + # + cancelOrders = self.safe_list(response, 'cancelOrders', []) + unprocessedRequests = self.safe_list(response, 'unprocessedRequests', []) + orders = self.array_concat(cancelOrders, unprocessedRequests) + return self.parse_orders(orders) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://docs.btcmarkets.net/v3/#operation/cancelOrder + + :param str id: order id + :param str symbol: not used by btcmarket cancelOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + request: dict = { + 'id': id, + } + response = await self.privateDeleteOrdersId(self.extend(request, params)) + # + # { + # "orderId": "7524", + # "clientOrderId": "123-456" + # } + # + return self.parse_order(response) + + def calculate_fee(self, symbol, type, side, amount, price, takerOrMaker='taker', params={}): + """ + calculates the presumptive fee that would be charged for an order + :param str symbol: unified market symbol + :param str type: not used by btcmarkets.calculateFee + :param str side: not used by btcmarkets.calculateFee + :param float amount: how much you want to trade, in units of the base currency on most exchanges, or number of contracts + :param float price: the price for the order to be filled at, in units of the quote currency + :param str takerOrMaker: 'taker' or 'maker' + :param dict params: + :returns dict: contains the rate, the percentage multiplied to the order amount to obtain the fee amount, and cost, the total value of the fee in units of the quote currency, for the order + """ + market = self.markets[symbol] + currency = None + cost = None + if market['quote'] == 'AUD': + currency = market['quote'] + amountString = self.number_to_string(amount) + priceString = self.number_to_string(price) + otherUnitsAmount = Precise.string_mul(amountString, priceString) + cost = self.cost_to_precision(symbol, otherUnitsAmount) + else: + currency = market['base'] + cost = self.amount_to_precision(symbol, amount) + rate = market[takerOrMaker] + rateCost = Precise.string_mul(self.number_to_string(rate), cost) + return { + 'type': takerOrMaker, + 'currency': currency, + 'rate': rate, + 'cost': float(self.fee_to_precision(symbol, rateCost)), + } + + def parse_order_status(self, status: Str): + statuses: dict = { + 'Accepted': 'open', + 'Placed': 'open', + 'Partially Matched': 'open', + 'Fully Matched': 'closed', + 'Cancelled': 'canceled', + 'Partially Cancelled': 'canceled', + 'Failed': 'rejected', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder + # + # { + # "orderId": "7524", + # "marketId": "BTC-AUD", + # "side": "Bid", + # "type": "Limit", + # "creationTime": "2019-08-30T11:08:21.956000Z", + # "price": "100.12", + # "amount": "1.034", + # "openAmount": "1.034", + # "status": "Accepted", + # "clientOrderId": "1234-5678", + # "timeInForce": "IOC", + # "postOnly": False, + # "selfTrade": "P", + # "triggerAmount": "105", + # "targetAmount": "1000" + # } + # + timestamp = self.parse8601(self.safe_string(order, 'creationTime')) + marketId = self.safe_string(order, 'marketId') + market = self.safe_market(marketId, market, '-') + side = self.safe_string(order, 'side') + if side == 'Bid': + side = 'buy' + elif side == 'Ask': + side = 'sell' + type = self.safe_string_lower(order, 'type') + price = self.safe_string(order, 'price') + amount = self.safe_string(order, 'amount') + remaining = self.safe_string(order, 'openAmount') + status = self.parse_order_status(self.safe_string(order, 'status')) + id = self.safe_string(order, 'orderId') + clientOrderId = self.safe_string(order, 'clientOrderId') + timeInForce = self.safe_string(order, 'timeInForce') + postOnly = self.safe_bool(order, 'postOnly') + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'symbol': market['symbol'], + 'type': type, + 'timeInForce': timeInForce, + 'postOnly': postOnly, + 'side': side, + 'price': price, + 'triggerPrice': self.safe_number(order, 'triggerPrice'), + 'cost': None, + 'amount': amount, + 'filled': None, + 'remaining': remaining, + 'average': None, + 'status': status, + 'trades': None, + 'fee': None, + }, market) + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://docs.btcmarkets.net/v3/#operation/getOrderById + + :param str id: the order id + :param str symbol: not used by btcmarkets fetchOrder + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + request: dict = { + 'id': id, + } + response = await self.privateGetOrdersId(self.extend(request, params)) + return self.parse_order(response) + + async def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://docs.btcmarkets.net/v3/#operation/listOrders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + request: dict = { + 'status': 'all', + } + market = None + if symbol is not None: + market = self.market(symbol) + request['marketId'] = market['id'] + if since is not None: + request['after'] = since + if limit is not None: + request['limit'] = limit + response = await self.privateGetOrders(self.extend(request, params)) + return self.parse_orders(response, market, since, limit) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://docs.btcmarkets.net/v3/#operation/listOrders + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + request: dict = {'status': 'open'} + return await self.fetch_orders(symbol, since, limit, self.extend(request, params)) + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://docs.btcmarkets.net/v3/#operation/listOrders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + orders = await self.fetch_orders(symbol, since, limit, params) + return self.filter_by(orders, 'status', 'closed') + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://docs.btcmarkets.net/v3/#operation/getTrades + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['marketId'] = market['id'] + if since is not None: + request['after'] = since + if limit is not None: + request['limit'] = limit + response = await self.privateGetTrades(self.extend(request, params)) + # + # [ + # { + # "id": "36014819", + # "marketId": "XRP-AUD", + # "timestamp": "2019-06-25T16:01:02.977000Z", + # "price": "0.67", + # "amount": "1.50533262", + # "side": "Ask", + # "fee": "0.00857285", + # "orderId": "3648306", + # "liquidityType": "Taker", + # "clientOrderId": "48" + # }, + # { + # "id": "3568960", + # "marketId": "GNT-AUD", + # "timestamp": "2019-06-20T08:44:04.488000Z", + # "price": "0.1362", + # "amount": "0.85", + # "side": "Bid", + # "fee": "0.00098404", + # "orderId": "3543015", + # "liquidityType": "Maker" + # } + # ] + # + return self.parse_trades(response, market, since, limit) + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://docs.btcmarkets.net/v3/#tag/Fund-Management-APIs/paths/~1v3~1withdrawals/post + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency_id': currency['id'], + 'amount': self.currency_to_precision(code, amount), + } + if code != 'AUD': + self.check_address(address) + request['toAddress'] = address + if tag is not None: + request['toAddress'] = address + '?dt=' + tag + response = await self.privatePostWithdrawals(self.extend(request, params)) + # + # { + # "id": "4126657", + # "assetName": "XRP", + # "amount": "25", + # "type": "Withdraw", + # "creationTime": "2019-09-04T00:04:10.973000Z", + # "status": "Pending Authorization", + # "description": "XRP withdraw from [me@test.com] to Address: abc amount: 25 fee: 0", + # "fee": "0", + # "lastUpdate": "2019-09-04T00:04:11.018000Z", + # "paymentDetail": { + # "address": "abc" + # } + # } + # + return self.parse_transaction(response, currency) + + def nonce(self): + return self.milliseconds() + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + request = '/' + self.version + '/' + self.implode_params(path, params) + query = self.keysort(self.omit(params, self.extract_params(path))) + if api == 'private': + self.check_required_credentials() + nonce = str(self.nonce()) + secret = self.base64_to_binary(self.secret) + auth = method + request + nonce + if (method == 'GET') or (method == 'DELETE'): + if query: + request += '?' + self.urlencode(query) + else: + body = self.json(query) + auth += body + signature = self.hmac(self.encode(auth), secret, hashlib.sha512, 'base64') + headers = { + 'Accept': 'application/json', + 'Accept-Charset': 'UTF-8', + 'Content-Type': 'application/json', + 'BM-AUTH-APIKEY': self.apiKey, + 'BM-AUTH-TIMESTAMP': nonce, + 'BM-AUTH-SIGNATURE': signature, + } + elif api == 'public': + if query: + request += '?' + self.urlencode(query) + url = self.urls['api'][api] + request + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None # fallback to default error handler + # + # {"code":"UnAuthorized","message":"invalid access token"} + # {"code":"MarketNotFound","message":"invalid marketId"} + # + errorCode = self.safe_string(response, 'code') + message = self.safe_string(response, 'message') + if errorCode is not None: + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + raise ExchangeError(feedback) # unknown message + return None diff --git a/ccxt/async_support/btcturk.py b/ccxt/async_support/btcturk.py new file mode 100644 index 0000000..769ecc9 --- /dev/null +++ b/ccxt/async_support/btcturk.py @@ -0,0 +1,1035 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.btcturk import ImplicitAPI +import hashlib +import math +from ccxt.base.types import Any, Balances, Bool, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import BadRequest +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class btcturk(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(btcturk, self).describe(), { + 'id': 'btcturk', + 'name': 'BTCTurk', + 'countries': ['TR'], # Turkey + 'rateLimit': 100, + 'pro': False, + 'has': { + 'CORS': True, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelOrder': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createDepositAddress': False, + 'createOrder': True, + 'createOrderWithTakeProfitAndStopLoss': False, + 'createOrderWithTakeProfitAndStopLossWs': False, + 'createPostOnlyOrder': False, + 'createReduceOnlyOrder': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': False, + 'fetchDepositAddress': False, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTrades': True, + 'fetchVolatilityHistory': False, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'repayMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'ws': False, + }, + 'timeframes': { + '1m': 1, + '15m': 15, + '30m': 30, + '1h': 60, + '4h': 240, + '1d': '1 d', + '1w': '1 w', + '1y': '1 y', + }, + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/10e0a238-9f60-4b06-9dda-edfc7602f1d6', + 'api': { + 'public': 'https://api.btcturk.com/api/v2', + 'private': 'https://api.btcturk.com/api/v1', + 'graph': 'https://graph-api.btcturk.com/v1', + }, + 'www': 'https://www.btcturk.com', + 'doc': 'https://github.com/BTCTrader/broker-api-docs', + }, + 'api': { + 'public': { + 'get': { + 'orderbook': 1, + 'ticker': 0.1, + 'trades': 1, # ?last=COUNT(max 50) + 'ohlc': 1, + 'server/exchangeinfo': 1, + }, + }, + 'private': { + 'get': { + 'users/balances': 1, + 'openOrders': 1, + 'allOrders': 1, + 'users/transactions/trade': 1, + }, + 'post': { + 'users/transactions/crypto': 1, + 'users/transactions/fiat': 1, + 'order': 1, + 'cancelOrder': 1, + }, + 'delete': { + 'order': 1, + }, + }, + 'graph': { + 'get': { + 'ohlcs': 1, + 'klines/history': 1, + }, + }, + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': False, + 'FOK': False, + 'PO': False, + 'GTD': False, + }, + 'hedged': False, + 'leverage': False, + 'marketBuyRequiresPrice': False, + 'marketBuyByCost': False, + 'selfTradePrevention': False, + 'trailing': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 100000, + 'untilDays': 30, + 'symbolRequired': True, + }, + 'fetchOrder': None, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 100000, + 'untilDays': 30, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchClosedOrders': None, + 'fetchOHLCV': { + 'limit': None, + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'fees': { + 'trading': { + 'maker': self.parse_number('0.0005'), + 'taker': self.parse_number('0.0009'), + }, + }, + 'exceptions': { + 'exact': { + 'FAILED_ORDER_WITH_OPEN_ORDERS': InsufficientFunds, + 'FAILED_LIMIT_ORDER': InvalidOrder, + 'FAILED_MARKET_ORDER': InvalidOrder, + }, + }, + 'precisionMode': TICK_SIZE, + }) + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for btcturk + + https://docs.btcturk.com/public-endpoints/exchange-info + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = await self.publicGetServerExchangeinfo(params) + # + # { + # "data": { + # "timeZone": "UTC", + # "serverTime": "1618826678404", + # "symbols": [ + # { + # "id": "1", + # "name": "BTCTRY", + # "nameNormalized": "BTC_TRY", + # "status": "TRADING", + # "numerator": "BTC", + # "denominator": "TRY", + # "numeratorScale": "8", + # "denominatorScale": "2", + # "hasFraction": False, + # "filters": [ + # { + # "filterType": "PRICE_FILTER", + # "minPrice": "0.0000000000001", + # "maxPrice": "10000000", + # "tickSize": "10", + # "minExchangeValue": "99.92", + # "minAmount": null, + # "maxAmount": null + # } + # ], + # "orderMethods": [ + # "MARKET", + # "LIMIT", + # "STOP_MARKET", + # "STOP_LIMIT" + # ], + # "displayFormat": "#,###", + # "commissionFromNumerator": False, + # "order": "1000", + # "priceRounding": False + # }, + # ... + # }, + # ], + # } + # + data = self.safe_dict(response, 'data', {}) + markets = self.safe_list(data, 'symbols', []) + return self.parse_markets(markets) + + def parse_market(self, entry) -> Market: + id = self.safe_string(entry, 'name') + baseId = self.safe_string(entry, 'numerator') + quoteId = self.safe_string(entry, 'denominator') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + filters = self.safe_list(entry, 'filters', []) + minPrice = None + maxPrice = None + minAmount = None + maxAmount = None + minCost = None + for j in range(0, len(filters)): + filter = filters[j] + filterType = self.safe_string(filter, 'filterType') + if filterType == 'PRICE_FILTER': + minPrice = self.safe_number(filter, 'minPrice') + maxPrice = self.safe_number(filter, 'maxPrice') + minAmount = self.safe_number(filter, 'minAmount') + maxAmount = self.safe_number(filter, 'maxAmount') + minCost = self.safe_number(filter, 'minExchangeValue') + status = self.safe_string(entry, 'status') + return { + 'id': id, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': (status == 'TRADING'), + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(self.parse_precision(self.safe_string(entry, 'numeratorScale'))), + 'price': self.parse_number(self.parse_precision(self.safe_string(entry, 'denominatorScale'))), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': minAmount, + 'max': maxAmount, + }, + 'price': { + 'min': minPrice, + 'max': maxPrice, + }, + 'cost': { + 'min': minCost, + 'max': None, + }, + }, + 'created': None, + 'info': entry, + } + + def parse_balance(self, response) -> Balances: + data = self.safe_list(response, 'data', []) + result: dict = { + 'info': response, + 'timestamp': None, + 'datetime': None, + } + for i in range(0, len(data)): + entry = data[i] + currencyId = self.safe_string(entry, 'asset') + code = self.safe_currency_code(currencyId) + account = self.account() + account['total'] = self.safe_string(entry, 'balance') + account['free'] = self.safe_string(entry, 'free') + account['used'] = self.safe_string(entry, 'locked') + result[code] = account + return self.safe_balance(result) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://docs.btcturk.com/private-endpoints/account-balance + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + response = await self.privateGetUsersBalances(params) + # + # { + # "data": [ + # { + # "asset": "TRY", + # "assetname": "Türk Lirası", + # "balance": "0", + # "locked": "0", + # "free": "0", + # "orderFund": "0", + # "requestFund": "0", + # "precision": 2 + # } + # ] + # } + # + return self.parse_balance(response) + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://docs.btcturk.com/public-endpoints/orderbook + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pairSymbol': market['id'], + } + response = await self.publicGetOrderbook(self.extend(request, params)) + # { + # "data": { + # "timestamp": 1618827901241, + # "bids": [ + # [ + # "460263.00", + # "0.04244000" + # ] + # ] + # } + # } + data = self.safe_dict(response, 'data', {}) + timestamp = self.safe_integer(data, 'timestamp') + return self.parse_order_book(data, market['symbol'], timestamp, 'bids', 'asks', 0, 1) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "pair": "BTCTRY", + # "pairNormalized": "BTC_TRY", + # "timestamp": 1618826361234, + # "last": 462485, + # "high": 473976, + # "low": 444201, + # "bid": 461928, + # "ask": 462485, + # "open": 456915, + # "volume": 917.41368645, + # "average": 462868.29574589, + # "daily": 5570, + # "dailyPercent": 1.22, + # "denominatorSymbol": "TRY", + # "numeratorSymbol": "BTC", + # "order": 1000 + # } + # + marketId = self.safe_string(ticker, 'pair') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + timestamp = self.safe_integer(ticker, 'timestamp') + last = self.safe_string(ticker, 'last') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': self.safe_string(ticker, 'bid'), + 'bidVolume': None, + 'ask': self.safe_string(ticker, 'ask'), + 'askVolume': None, + 'vwap': None, + 'open': self.safe_string(ticker, 'open'), + 'close': last, + 'last': last, + 'previousClose': None, + 'change': self.safe_string(ticker, 'daily'), + 'percentage': self.safe_string(ticker, 'dailyPercent'), + 'average': self.safe_string(ticker, 'average'), + 'baseVolume': self.safe_string(ticker, 'volume'), + 'quoteVolume': None, + 'info': ticker, + }, market) + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://docs.btcturk.com/public-endpoints/ticker + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + response = await self.publicGetTicker(params) + tickers = self.safe_list(response, 'data') + return self.parse_tickers(tickers, symbols) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://docs.btcturk.com/public-endpoints/ticker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + tickers = await self.fetch_tickers([symbol], params) + return self.safe_value(tickers, symbol) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades + # { + # "pair": "BTCUSDT", + # "pairNormalized": "BTC_USDT", + # "numerator": "BTC", + # "denominator": "USDT", + # "date": "1618916879083", + # "tid": "637545136790672520", + # "price": "55774", + # "amount": "0.27917100", + # "side": "buy" + # } + # + # fetchMyTrades + # { + # "price": "56000", + # "numeratorSymbol": "BTC", + # "denominatorSymbol": "USDT", + # "orderType": "buy", + # "orderId": "2606935102", + # "id": "320874372", + # "timestamp": "1618916479593", + # "amount": "0.00020000", + # "fee": "0", + # "tax": "0" + # } + # + timestamp = self.safe_integer_2(trade, 'date', 'timestamp') + id = self.safe_string_2(trade, 'tid', 'id') + order = self.safe_string(trade, 'orderId') + priceString = self.safe_string(trade, 'price') + amountString = Precise.string_abs(self.safe_string(trade, 'amount')) + marketId = self.safe_string(trade, 'pair') + symbol = self.safe_symbol(marketId, market) + side = self.safe_string_2(trade, 'side', 'orderType') + fee = None + feeAmountString = self.safe_string(trade, 'fee') + if feeAmountString is not None: + feeCurrency = self.safe_string(trade, 'denominatorSymbol') + fee = { + 'cost': Precise.string_abs(feeAmountString), + 'currency': self.safe_currency_code(feeCurrency), + } + return self.safe_trade({ + 'info': trade, + 'id': id, + 'order': order, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'type': None, + 'side': side, + 'takerOrMaker': None, + 'price': priceString, + 'amount': amountString, + 'cost': None, + 'fee': fee, + }, market) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://docs.btcturk.com/public-endpoints/trades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + # maxCount = 50 + request: dict = { + 'pairSymbol': market['id'], + } + if limit is not None: + request['last'] = limit + response = await self.publicGetTrades(self.extend(request, params)) + # + # { + # "data": [ + # { + # "pair": "BTCTRY", + # "pairNormalized": "BTC_TRY", + # "numerator": "BTC", + # "denominator": "TRY", + # "date": 1618828421497, + # "tid": "637544252214980918", + # "price": "462585.00", + # "amount": "0.01618411", + # "side": "sell" + # } + # ] + # } + # + data = self.safe_list(response, 'data') + return self.parse_trades(data, market, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # { + # "timestamp": 1661990400, + # "high": 368388.0, + # "open": 368388.0, + # "low": 368388.0, + # "close": 368388.0, + # "volume": 0.00035208, + # } + # + return [ + self.safe_timestamp(ohlcv, 'timestamp'), + self.safe_number(ohlcv, 'open'), + self.safe_number(ohlcv, 'high'), + self.safe_number(ohlcv, 'low'), + self.safe_number(ohlcv, 'close'), + self.safe_number(ohlcv, 'volume'), + ] + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1h', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://docs.btcturk.com/public-endpoints/get-kline-data + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest candle to fetch + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'resolution': self.safe_value(self.timeframes, timeframe, timeframe), # allows the user to pass custom timeframes if needed + } + until = self.safe_integer(params, 'until', self.milliseconds()) + request['to'] = self.parse_to_int((until / 1000)) + if since is not None: + request['from'] = self.parse_to_int(since / 1000) + elif limit is None: # since will also be None + limit = 100 # default value + if limit is not None: + limit = min(limit, 11000) # max 11000 candles diapason can be covered + if timeframe == '1y': # difficult with leap years + raise BadRequest(self.id + ' fetchOHLCV() does not accept a limit parameter when timeframe == "1y"') + seconds = self.parse_timeframe(timeframe) + limitSeconds = seconds * (limit - 1) + if since is not None: + to = self.parse_to_int(since / 1000) + limitSeconds + request['to'] = min(request['to'], to) + else: + request['from'] = self.parse_to_int(0 / 1000) - limitSeconds + response = await self.graphGetKlinesHistory(self.extend(request, params)) + # + # { + # "s": "ok", + # "t": [ + # 1661990400, + # 1661990520, + # ... + # ], + # "h": [ + # 368388.0, + # 369090.0, + # ... + # ], + # "o": [ + # 368388.0, + # 368467.0, + # ... + # ], + # "l": [ + # 368388.0, + # 368467.0, + # ... + # ], + # "c": [ + # 368388.0, + # 369090.0, + # ... + # ], + # "v": [ + # 0.00035208, + # 0.2972395, + # ... + # ] + # } + # + return self.parse_ohlcvs(response, market, timeframe, since, limit) + + def parse_ohlcvs(self, ohlcvs, market=None, timeframe='1m', since: Int = None, limit: Int = None, tail: Bool = False): + results = [] + timestamp = self.safe_list(ohlcvs, 't', []) + high = self.safe_list(ohlcvs, 'h', []) + open = self.safe_list(ohlcvs, 'o', []) + low = self.safe_list(ohlcvs, 'l', []) + close = self.safe_list(ohlcvs, 'c', []) + volume = self.safe_list(ohlcvs, 'v', []) + for i in range(0, len(timestamp)): + ohlcv: dict = { + 'timestamp': self.safe_integer(timestamp, i), + 'high': self.safe_number(high, i), + 'open': self.safe_number(open, i), + 'low': self.safe_number(low, i), + 'close': self.safe_number(close, i), + 'volume': self.safe_number(volume, i), + } + results.append(self.parse_ohlcv(ohlcv, market)) + sorted = self.sort_by(results, 0) + return self.filter_by_since_limit(sorted, since, limit, 0, tail) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://docs.btcturk.com/private-endpoints/submit-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'orderType': side, + 'orderMethod': type, + 'pairSymbol': market['id'], + 'quantity': self.amount_to_precision(symbol, amount), + } + if type != 'market': + request['price'] = self.price_to_precision(symbol, price) + if 'clientOrderId' in params: + request['newClientOrderId'] = params['clientOrderId'] + elif not ('newClientOrderId' in params): + request['newClientOrderId'] = self.uuid() + response = await self.privatePostOrder(self.extend(request, params)) + data = self.safe_dict(response, 'data') + return self.parse_order(data, market) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://docs.btcturk.com/private-endpoints/cancel-order + + :param str id: order id + :param str symbol: not used by btcturk cancelOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + request: dict = { + 'id': id, + } + response = await self.privateDeleteOrder(self.extend(request, params)) + # + # { + # "success": True, + # "message": "SUCCESS", + # "code": 0 + # } + # + return self.safe_order({ + 'info': response, + }) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://docs.btcturk.com/private-endpoints/open-orders + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['pairSymbol'] = market['id'] + response = await self.privateGetOpenOrders(self.extend(request, params)) + data = self.safe_dict(response, 'data', {}) + bids = self.safe_list(data, 'bids', []) + asks = self.safe_list(data, 'asks', []) + return self.parse_orders(self.array_concat(bids, asks), market, since, limit) + + async def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://docs.btcturk.com/private-endpoints/all-orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pairSymbol': market['id'], + } + if limit is not None: + # default 100 max 1000 + request['last'] = limit + if since is not None: + request['startTime'] = int(math.floor(since / 1000)) + response = await self.privateGetAllOrders(self.extend(request, params)) + # { + # "data": [ + # { + # "id": "2606012912", + # "price": "55000", + # "amount": "0.0003", + # "quantity": "0.0003", + # "stopPrice": "0", + # "pairSymbol": "BTCUSDT", + # "pairSymbolNormalized": "BTC_USDT", + # "type": "buy", + # "method": "limit", + # "orderClientId": "2ed187bd-59a8-4875-a212-1b793963b85c", + # "time": "1618913189253", + # "updateTime": "1618913189253", + # "status": "Untouched", + # "leftAmount": "0.0003000000000000" + # } + # ] + # } + data = self.safe_list(response, 'data') + return self.parse_orders(data, market, since, limit) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'Untouched': 'open', + 'Partial': 'open', + 'Canceled': 'canceled', + 'Closed': 'closed', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # fetchOrders / fetchOpenOrders + # { + # "id": 2605984008, + # "price": "55000", + # "amount": "0.00050000", + # "quantity": "0.00050000", + # "stopPrice": "0", + # "pairSymbol": "BTCUSDT", + # "pairSymbolNormalized": "BTC_USDT", + # "type": "buy", + # "method": "limit", + # "orderClientId": "f479bdb6-0965-4f03-95b5-daeb7aa5a3a5", + # "time": 0, + # "updateTime": 1618913083543, + # "status": "Untouched", + # "leftAmount": "0.00050000" + # } + # + # createOrder + # { + # "id": "2606935102", + # "quantity": "0.0002", + # "price": "56000", + # "stopPrice": null, + # "newOrderClientId": "98e5c491-7ed9-462b-9666-93553180fb28", + # "type": "buy", + # "method": "limit", + # "pairSymbol": "BTCUSDT", + # "pairSymbolNormalized": "BTC_USDT", + # "datetime": "1618916479523" + # } + # + id = self.safe_string(order, 'id') + price = self.safe_string(order, 'price') + amountString = self.safe_string_2(order, 'amount', 'quantity') + amount = Precise.string_abs(amountString) + remaining = self.safe_string(order, 'leftAmount') + marketId = self.safe_string(order, 'pairSymbol') + symbol = self.safe_symbol(marketId, market) + side = self.safe_string(order, 'type') + type = self.safe_string(order, 'method') + clientOrderId = self.safe_string(order, 'orderClientId') + timestamp = self.safe_integer_2(order, 'updateTime', 'datetime') + rawStatus = self.safe_string(order, 'status') + status = self.parse_order_status(rawStatus) + return self.safe_order({ + 'info': order, + 'id': id, + 'price': price, + 'amount': amount, + 'remaining': remaining, + 'filled': None, + 'cost': None, + 'average': None, + 'status': status, + 'side': side, + 'type': type, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'fee': None, + }, market) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://docs.btcturk.com/private-endpoints/user-transactions + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + response = await self.privateGetUsersTransactionsTrade() + # + # { + # "data": [ + # { + # "price": "56000", + # "numeratorSymbol": "BTC", + # "denominatorSymbol": "USDT", + # "orderType": "buy", + # "orderId": "2606935102", + # "id": "320874372", + # "timestamp": "1618916479593", + # "amount": "0.00020000", + # "fee": "0", + # "tax": "0" + # } + # ], + # "success": True, + # "message": "SUCCESS", + # "code": "0" + # } + # + data = self.safe_list(response, 'data') + return self.parse_trades(data, market, since, limit) + + def nonce(self): + return self.milliseconds() + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + if self.id == 'btctrader': + raise ExchangeError(self.id + ' is an abstract base API for BTCExchange, BTCTurk') + url = self.urls['api'][api] + '/' + path + if (method == 'GET') or (method == 'DELETE'): + if params: + url += '?' + self.urlencode(params) + else: + body = self.json(params) + if api == 'private': + self.check_required_credentials() + nonce = str(self.nonce()) + secret = self.base64_to_binary(self.secret) + auth = self.apiKey + nonce + headers = { + 'X-PCK': self.apiKey, + 'X-Stamp': nonce, + 'X-Signature': self.hmac(self.encode(auth), secret, hashlib.sha256, 'base64'), + 'Content-Type': 'application/json', + } + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + errorCode = self.safe_string(response, 'code', '0') + message = self.safe_string(response, 'message') + output = body if (message is None) else message + self.throw_exactly_matched_exception(self.exceptions['exact'], message, self.id + ' ' + output) + if (errorCode != '0') and (errorCode != 'SUCCESS'): + raise ExchangeError(self.id + ' ' + output) + return None diff --git a/ccxt/async_support/bybit.py b/ccxt/async_support/bybit.py new file mode 100644 index 0000000..0a94536 --- /dev/null +++ b/ccxt/async_support/bybit.py @@ -0,0 +1,9021 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.bybit import ImplicitAPI +import asyncio +import hashlib +from ccxt.base.types import Any, Balances, BorrowInterest, Conversion, CrossBorrowRate, Currencies, Currency, DepositAddress, FundingHistory, Greeks, Int, LedgerEntry, Leverage, LeverageTier, LeverageTiers, Liquidation, LongShortRatio, Market, Num, Option, OptionChain, Order, OrderBook, OrderRequest, CancellationRequest, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, FundingRates, Trade, TradingFeeInterface, TradingFees, Transaction, MarketInterface, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import AccountSuspended +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import NoChange +from ccxt.base.errors import MarginModeAlreadySet +from ccxt.base.errors import ManualInteractionNeeded +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import NotSupported +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import InvalidNonce +from ccxt.base.errors import RequestTimeout +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class bybit(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(bybit, self).describe(), { + 'id': 'bybit', + 'name': 'Bybit', + 'countries': ['VG'], # British Virgin Islands + 'version': 'v5', + 'userAgent': None, + 'rateLimit': 20, + 'hostname': 'bybit.com', # bybit.com, bytick.com, bybit.nl, bybit.com.hk + 'pro': True, + 'certified': True, + 'has': { + 'CORS': True, + 'spot': True, + 'margin': True, + 'swap': True, + 'future': True, + 'option': True, + 'borrowCrossMargin': True, + 'cancelAllOrders': True, + 'cancelAllOrdersAfter': True, + 'cancelOrder': True, + 'cancelOrders': True, + 'cancelOrdersForSymbols': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createConvertTrade': True, + 'createMarketBuyOrderWithCost': True, + 'createMarketSellOrderWithCost': True, + 'createOrder': True, + 'createOrders': True, + 'createOrderWithTakeProfitAndStopLoss': True, + 'createPostOnlyOrder': True, + 'createReduceOnlyOrder': True, + 'createStopLimitOrder': True, + 'createStopLossOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'createTakeProfitOrder': True, + 'createTrailingAmountOrder': True, + 'createTriggerOrder': True, + 'editOrder': True, + 'editOrders': True, + 'fetchAllGreeks': True, + 'fetchBalance': True, + 'fetchBidsAsks': 'emulated', + 'fetchBorrowInterest': False, # temporarily disabled, doesn't work + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchCanceledAndClosedOrders': True, + 'fetchCanceledOrders': True, + 'fetchClosedOrder': True, + 'fetchClosedOrders': True, + 'fetchConvertCurrencies': True, + 'fetchConvertQuote': True, + 'fetchConvertTrade': True, + 'fetchConvertTradeHistory': True, + 'fetchCrossBorrowRate': True, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDeposit': False, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': True, + 'fetchDeposits': True, + 'fetchDepositWithdrawFee': 'emulated', + 'fetchDepositWithdrawFees': True, + 'fetchFundingHistory': True, + 'fetchFundingRate': 'emulated', # emulated in exchange + 'fetchFundingRateHistory': True, + 'fetchFundingRates': True, + 'fetchGreeks': True, + 'fetchIndexOHLCV': True, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchLedger': True, + 'fetchLeverage': True, + 'fetchLeverageTiers': True, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': True, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarketLeverageTiers': True, + 'fetchMarkets': True, + 'fetchMarkOHLCV': True, + 'fetchMyLiquidations': True, + 'fetchMySettlementHistory': True, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': True, + 'fetchOpenInterestHistory': True, + 'fetchOpenOrder': True, + 'fetchOpenOrders': True, + 'fetchOption': True, + 'fetchOptionChain': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': False, + 'fetchOrderTrades': True, + 'fetchPosition': True, + 'fetchPositionHistory': 'emulated', + 'fetchPositions': True, + 'fetchPositionsHistory': True, + 'fetchPremiumIndexOHLCV': True, + 'fetchSettlementHistory': True, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': True, + 'fetchTradingFees': True, + 'fetchTransactions': False, + 'fetchTransfers': True, + 'fetchUnderlyingAssets': False, + 'fetchVolatilityHistory': True, + 'fetchWithdrawals': True, + 'repayCrossMargin': True, + 'sandbox': True, + 'setLeverage': True, + 'setMarginMode': True, + 'setPositionMode': True, + 'transfer': True, + 'withdraw': True, + }, + 'timeframes': { + '1m': '1', + '3m': '3', + '5m': '5', + '15m': '15', + '30m': '30', + '1h': '60', + '2h': '120', + '4h': '240', + '6h': '360', + '12h': '720', + '1d': 'D', + '1w': 'W', + '1M': 'M', + }, + 'urls': { + 'test': { + 'spot': 'https://api-testnet.{hostname}', + 'futures': 'https://api-testnet.{hostname}', + 'v2': 'https://api-testnet.{hostname}', + 'public': 'https://api-testnet.{hostname}', + 'private': 'https://api-testnet.{hostname}', + }, + 'logo': 'https://github.com/user-attachments/assets/97a5d0b3-de10-423d-90e1-6620960025ed', + 'api': { + 'spot': 'https://api.{hostname}', + 'futures': 'https://api.{hostname}', + 'v2': 'https://api.{hostname}', + 'public': 'https://api.{hostname}', + 'private': 'https://api.{hostname}', + }, + 'demotrading': { + 'spot': 'https://api-demo.{hostname}', + 'futures': 'https://api-demo.{hostname}', + 'v2': 'https://api-demo.{hostname}', + 'public': 'https://api-demo.{hostname}', + 'private': 'https://api-demo.{hostname}', + }, + 'www': 'https://www.bybit.com', + 'doc': [ + 'https://bybit-exchange.github.io/docs/inverse/', + 'https://bybit-exchange.github.io/docs/linear/', + 'https://github.com/bybit-exchange', + ], + 'fees': 'https://help.bybit.com/hc/en-us/articles/360039261154', + 'referral': 'https://www.bybit.com/invite?ref=XDK12WP', + }, + 'api': { + 'public': { + 'get': { + # spot + 'spot/v3/public/symbols': 1, + 'spot/v3/public/quote/depth': 1, + 'spot/v3/public/quote/depth/merged': 1, + 'spot/v3/public/quote/trades': 1, + 'spot/v3/public/quote/kline': 1, + 'spot/v3/public/quote/ticker/24hr': 1, + 'spot/v3/public/quote/ticker/price': 1, + 'spot/v3/public/quote/ticker/bookTicker': 1, + 'spot/v3/public/server-time': 1, + 'spot/v3/public/infos': 1, + 'spot/v3/public/margin-product-infos': 1, + 'spot/v3/public/margin-ensure-tokens': 1, + # data + 'v3/public/time': 1, + 'contract/v3/public/copytrading/symbol/list': 1, + # derivative + 'derivatives/v3/public/order-book/L2': 1, + 'derivatives/v3/public/kline': 1, + 'derivatives/v3/public/tickers': 1, + 'derivatives/v3/public/instruments-info': 1, + 'derivatives/v3/public/mark-price-kline': 1, + 'derivatives/v3/public/index-price-kline': 1, + 'derivatives/v3/public/funding/history-funding-rate': 1, + 'derivatives/v3/public/risk-limit/list': 1, + 'derivatives/v3/public/delivery-price': 1, + 'derivatives/v3/public/recent-trade': 1, + 'derivatives/v3/public/open-interest': 1, + 'derivatives/v3/public/insurance': 1, + # v5 + 'v5/announcements/index': 5, # 10/s = 1000 / (20 * 5) + # market + 'v5/market/time': 5, + 'v5/market/kline': 5, + 'v5/market/mark-price-kline': 5, + 'v5/market/index-price-kline': 5, + 'v5/market/premium-index-price-kline': 5, + 'v5/market/instruments-info': 5, + 'v5/market/orderbook': 5, + 'v5/market/tickers': 5, + 'v5/market/funding/history': 5, + 'v5/market/recent-trade': 5, + 'v5/market/open-interest': 5, + 'v5/market/historical-volatility': 5, + 'v5/market/insurance': 5, + 'v5/market/risk-limit': 5, + 'v5/market/delivery-price': 5, + 'v5/market/account-ratio': 5, + # spot leverage token + 'v5/spot-lever-token/info': 5, + 'v5/spot-lever-token/reference': 5, + # spot margin trade + 'v5/spot-margin-trade/data': 5, + 'v5/spot-margin-trade/collateral': 5, + 'v5/spot-cross-margin-trade/data': 5, + 'v5/spot-cross-margin-trade/pledge-token': 5, + 'v5/spot-cross-margin-trade/borrow-token': 5, + # crypto loan + 'v5/crypto-loan/collateral-data': 5, + 'v5/crypto-loan/loanable-data': 5, + # institutional lending + 'v5/ins-loan/product-infos': 5, + 'v5/ins-loan/ensure-tokens-convert': 5, + # earn + 'v5/earn/product': 5, + }, + }, + 'private': { + 'get': { + 'v5/market/instruments-info': 5, + # Legacy inverse swap + 'v2/private/wallet/fund/records': 25, # 120 per minute = 2 per second => cost = 50 / 2 = 25 + # spot + 'spot/v3/private/order': 2.5, + 'spot/v3/private/open-orders': 2.5, + 'spot/v3/private/history-orders': 2.5, + 'spot/v3/private/my-trades': 2.5, + 'spot/v3/private/account': 2.5, + 'spot/v3/private/reference': 2.5, + 'spot/v3/private/record': 2.5, + 'spot/v3/private/cross-margin-orders': 10, + 'spot/v3/private/cross-margin-account': 10, + 'spot/v3/private/cross-margin-loan-info': 10, + 'spot/v3/private/cross-margin-repay-history': 10, + 'spot/v3/private/margin-loan-infos': 10, + 'spot/v3/private/margin-repaid-infos': 10, + 'spot/v3/private/margin-ltv': 10, + # account + 'asset/v3/private/transfer/inter-transfer/list/query': 50, # 60 per minute = 1 per second => cost = 50 / 1 = 50 + 'asset/v3/private/transfer/sub-member/list/query': 50, + 'asset/v3/private/transfer/sub-member-transfer/list/query': 50, + 'asset/v3/private/transfer/universal-transfer/list/query': 25, + 'asset/v3/private/coin-info/query': 25, # 2/s + 'asset/v3/private/deposit/address/query': 10, + 'contract/v3/private/copytrading/order/list': 30, # 100 req/min = 1000 / (20 * 30) = 1.66666666667/s + 'contract/v3/private/copytrading/position/list': 40, # 75 req/min = 1000 / (20 * 40) = 1.25/s + 'contract/v3/private/copytrading/wallet/balance': 25, # 120 req/min = 1000 / (20 * 25) = 2/s + 'contract/v3/private/position/limit-info': 25, # 120 per minute = 2 per second => cost = 50 / 2 = 25 + 'contract/v3/private/order/unfilled-orders': 1, + 'contract/v3/private/order/list': 1, + 'contract/v3/private/position/list': 1, + 'contract/v3/private/execution/list': 1, + 'contract/v3/private/position/closed-pnl': 1, + 'contract/v3/private/account/wallet/balance': 1, + 'contract/v3/private/account/fee-rate': 1, + 'contract/v3/private/account/wallet/fund-records': 1, + # derivative + 'unified/v3/private/order/unfilled-orders': 1, + 'unified/v3/private/order/list': 1, + 'unified/v3/private/position/list': 1, + 'unified/v3/private/execution/list': 1, + 'unified/v3/private/delivery-record': 1, + 'unified/v3/private/settlement-record': 1, + 'unified/v3/private/account/wallet/balance': 1, + 'unified/v3/private/account/transaction-log': 1, + 'unified/v3/private/account/borrow-history': 1, + 'unified/v3/private/account/borrow-rate': 1, + 'unified/v3/private/account/info': 1, + 'user/v3/private/frozen-sub-member': 10, # 5/s + 'user/v3/private/query-sub-members': 5, # 10/s + 'user/v3/private/query-api': 5, # 10/s + 'user/v3/private/get-member-type': 1, + 'asset/v3/private/transfer/transfer-coin/list/query': 50, + 'asset/v3/private/transfer/account-coin/balance/query': 50, + 'asset/v3/private/transfer/account-coins/balance/query': 25, + 'asset/v3/private/transfer/asset-info/query': 50, + 'asset/v3/public/deposit/allowed-deposit-list/query': 0.17, # 300/s + 'asset/v3/private/deposit/record/query': 10, + 'asset/v3/private/withdraw/record/query': 10, + # v5 + # trade + 'v5/order/realtime': 5, # 10/s => cost = 50 / 10 = 5 + 'v5/order/history': 5, # 10/s => cost = 50 / 10 = 5 + 'v5/order/spot-borrow-check': 1, # 50/s = 1000 / (20 * 1) + # position + 'v5/position/list': 5, # 10/s => cost = 50 / 10 = 5 + 'v5/execution/list': 5, # 10/s => cost = 50 / 10 = 5 + 'v5/position/closed-pnl': 5, # 10/s => cost = 50 / 10 = 5 + 'v5/position/move-history': 5, # 10/s => cost = 50 / 10 = 5 + # pre-upgrade + 'v5/pre-upgrade/order/history': 5, + 'v5/pre-upgrade/execution/list': 5, + 'v5/pre-upgrade/position/closed-pnl': 5, + 'v5/pre-upgrade/account/transaction-log': 5, + 'v5/pre-upgrade/asset/delivery-record': 5, + 'v5/pre-upgrade/asset/settlement-record': 5, + # account + 'v5/account/wallet-balance': 1, + 'v5/account/borrow-history': 1, + 'v5/account/instruments-info': 1, + 'v5/account/collateral-info': 1, + 'v5/asset/coin-greeks': 1, + 'v5/account/fee-rate': 10, # 5/s = 1000 / (20 * 10) + 'v5/account/info': 5, + 'v5/account/transaction-log': 1, + 'v5/account/contract-transaction-log': 1, + 'v5/account/smp-group': 1, + 'v5/account/mmp-state': 5, + 'v5/account/withdrawal': 5, + # asset + 'v5/asset/exchange/query-coin-list': 0.5, # 100/s => cost = 50 / 100 = 0.5 + 'v5/asset/exchange/convert-result-query': 0.5, # 100/s => cost = 50 / 100 = 0.5 + 'v5/asset/exchange/query-convert-history': 0.5, # 100/s => cost = 50 / 100 = 0.5 + 'v5/asset/exchange/order-record': 5, # 10/s => cost = 50 / 10 = 5 + 'v5/asset/delivery-record': 5, + 'v5/asset/settlement-record': 5, + 'v5/asset/transfer/query-asset-info': 50, # 1/s => cost = 50 / 1 = 50 + 'v5/asset/transfer/query-account-coins-balance': 25, # 2/s => cost = 50 / 2 = 25 + 'v5/asset/transfer/query-account-coin-balance': 50, # 1/s => cost = 50 / 1 = 50 + 'v5/asset/transfer/query-transfer-coin-list': 50, # 1/s => cost = 50 / 1 = 50 + 'v5/asset/transfer/query-inter-transfer-list': 50, # 1/s => cost = 50 / 1 = 50 + 'v5/asset/transfer/query-sub-member-list': 50, # 1/s => cost = 50 / 1 = 50 + 'v5/asset/transfer/query-universal-transfer-list': 25, # 2/s => cost = 50 / 2 = 25 + 'v5/asset/deposit/query-allowed-list': 5, + 'v5/asset/deposit/query-record': 10, # 5/s => cost = 50 / 5 = 10 + 'v5/asset/deposit/query-sub-member-record': 10, # 5/s => cost = 50 / 5 = 10 + 'v5/asset/deposit/query-internal-record': 5, + 'v5/asset/deposit/query-address': 10, # 5/s => cost = 50 / 5 = 10 + 'v5/asset/deposit/query-sub-member-address': 10, # 5/s => cost = 50 / 5 = 10 + 'v5/asset/coin/query-info': 28, # should be 25 but exceeds ratelimit unless the weight is 28 or higher + 'v5/asset/withdraw/query-address': 10, + 'v5/asset/withdraw/query-record': 10, # 5/s => cost = 50 / 5 = 10 + 'v5/asset/withdraw/withdrawable-amount': 5, + 'v5/asset/withdraw/vasp/list': 5, + # user + 'v5/user/query-sub-members': 5, # 10/s => cost = 50 / 10 = 5 + 'v5/user/query-api': 5, # 10/s => cost = 50 / 10 = 5 + 'v5/user/sub-apikeys': 5, + 'v5/user/get-member-type': 5, + 'v5/user/aff-customer-info': 5, + 'v5/user/del-submember': 5, + 'v5/user/submembers': 5, + # affilate + 'v5/affiliate/aff-user-list': 5, + # spot leverage token + 'v5/spot-lever-token/order-record': 1, # 50/s => cost = 50 / 50 = 1 + # spot margin trade + 'v5/spot-margin-trade/interest-rate-history': 5, + 'v5/spot-margin-trade/state': 5, + 'v5/spot-margin-trade/max-borrowable': 5, + 'v5/spot-margin-trade/position-tiers': 5, + 'v5/spot-margin-trade/coinstate': 5, + 'v5/spot-margin-trade/repayment-available-amount': 5, + 'v5/spot-cross-margin-trade/loan-info': 1, # 50/s => cost = 50 / 50 = 1 + 'v5/spot-cross-margin-trade/account': 1, # 50/s => cost = 50 / 50 = 1 + 'v5/spot-cross-margin-trade/orders': 1, # 50/s => cost = 50 / 50 = 1 + 'v5/spot-cross-margin-trade/repay-history': 1, # 50/s => cost = 50 / 50 = 1 + # crypto loan + 'v5/crypto-loan/borrowable-collateralisable-number': 5, + 'v5/crypto-loan/ongoing-orders': 5, + 'v5/crypto-loan/repayment-history': 5, + 'v5/crypto-loan/borrow-history': 5, + 'v5/crypto-loan/max-collateral-amount': 5, + 'v5/crypto-loan/adjustment-history': 5, + # institutional lending + 'v5/ins-loan/product-infos': 5, + 'v5/ins-loan/ensure-tokens-convert': 5, + 'v5/ins-loan/loan-order': 5, + 'v5/ins-loan/repaid-history': 5, + 'v5/ins-loan/ltv-convert': 5, + # c2c lending + 'v5/lending/info': 5, + 'v5/lending/history-order': 5, + 'v5/lending/account': 5, + # broker + 'v5/broker/earning-record': 5, # deprecated + 'v5/broker/earnings-info': 5, + 'v5/broker/account-info': 5, + 'v5/broker/asset/query-sub-member-deposit-record': 10, + # earn + 'v5/earn/product': 5, + 'v5/earn/order': 5, + 'v5/earn/position': 5, + 'v5/earn/yield': 5, + 'v5/earn/hourly-yield': 5, + }, + 'post': { + # spot + 'spot/v3/private/order': 2.5, + 'spot/v3/private/cancel-order': 2.5, + 'spot/v3/private/cancel-orders': 2.5, + 'spot/v3/private/cancel-orders-by-ids': 2.5, + 'spot/v3/private/purchase': 2.5, + 'spot/v3/private/redeem': 2.5, + 'spot/v3/private/cross-margin-loan': 10, + 'spot/v3/private/cross-margin-repay': 10, + # account + 'asset/v3/private/transfer/inter-transfer': 150, # 20 per minute = 0.333 per second => cost = 50 / 0.3333 = 150 + 'asset/v3/private/withdraw/create': 300, + 'asset/v3/private/withdraw/cancel': 50, + 'asset/v3/private/transfer/sub-member-transfer': 150, + 'asset/v3/private/transfer/transfer-sub-member-save': 150, + 'asset/v3/private/transfer/universal-transfer': 10, # 5/s + 'user/v3/private/create-sub-member': 10, # 5/s + 'user/v3/private/create-sub-api': 10, # 5/s + 'user/v3/private/update-api': 10, # 5/s + 'user/v3/private/delete-api': 10, # 5/s + 'user/v3/private/update-sub-api': 10, # 5/s + 'user/v3/private/delete-sub-api': 10, # 5/s + # contract + 'contract/v3/private/copytrading/order/create': 30, # 100 req/min = 1000 / (20 * 30) = 1.66666666667/s + 'contract/v3/private/copytrading/order/cancel': 30, + 'contract/v3/private/copytrading/order/close': 30, + 'contract/v3/private/copytrading/position/close': 40, # 75 req/min = 1000 / (20 * 40) = 1.25/s + 'contract/v3/private/copytrading/position/set-leverage': 40, + 'contract/v3/private/copytrading/wallet/transfer': 25, # 120 req/min = 1000 / (20 * 25) = 2/s + 'contract/v3/private/copytrading/order/trading-stop': 2.5, + 'contract/v3/private/order/create': 1, + 'contract/v3/private/order/cancel': 1, + 'contract/v3/private/order/cancel-all': 1, + 'contract/v3/private/order/replace': 1, + 'contract/v3/private/position/set-auto-add-margin': 1, + 'contract/v3/private/position/switch-isolated': 1, + 'contract/v3/private/position/switch-mode': 1, + 'contract/v3/private/position/switch-tpsl-mode': 1, + 'contract/v3/private/position/set-leverage': 1, + 'contract/v3/private/position/trading-stop': 1, + 'contract/v3/private/position/set-risk-limit': 1, + 'contract/v3/private/account/setMarginMode': 1, + # derivative + 'unified/v3/private/order/create': 30, # 100 req/min(shared) = 1000 / (20 * 30) = 1.66666666667/s + 'unified/v3/private/order/replace': 30, + 'unified/v3/private/order/cancel': 30, + 'unified/v3/private/order/create-batch': 30, + 'unified/v3/private/order/replace-batch': 30, + 'unified/v3/private/order/cancel-batch': 30, + 'unified/v3/private/order/cancel-all': 30, + 'unified/v3/private/position/set-leverage': 2.5, + 'unified/v3/private/position/tpsl/switch-mode': 2.5, + 'unified/v3/private/position/set-risk-limit': 2.5, + 'unified/v3/private/position/trading-stop': 2.5, + 'unified/v3/private/account/upgrade-unified-account': 2.5, + 'unified/v3/private/account/setMarginMode': 2.5, + # tax + 'fht/compliance/tax/v3/private/registertime': 50, + 'fht/compliance/tax/v3/private/create': 50, + 'fht/compliance/tax/v3/private/status': 50, + 'fht/compliance/tax/v3/private/url': 50, + # v5 + # trade + 'v5/order/create': 2.5, # 20/s = 1000 / (20 * 2.5) + 'v5/order/amend': 5, # 10/s => cost = 50 / 10 = 5 + 'v5/order/cancel': 2.5, + 'v5/order/cancel-all': 50, # 1/s = 1000 / (20 * 50) + 'v5/order/create-batch': 5, # 10/s => cost = 50 / 10 = 5 + 'v5/order/amend-batch': 5, # 10/s => cost = 50 / 10 = 5 + 'v5/order/cancel-batch': 5, # 10/s => cost = 50 / 10 = 5 + 'v5/order/disconnected-cancel-all': 5, + # position + 'v5/position/set-leverage': 5, # 10/s => cost = 50 / 10 = 5 + 'v5/position/switch-isolated': 5, + 'v5/position/set-tpsl-mode': 5, # 10/s => cost = 50 / 10 = 5 + 'v5/position/switch-mode': 5, + 'v5/position/set-risk-limit': 5, # 10/s => cost = 50 / 10 = 5 + 'v5/position/trading-stop': 5, # 10/s => cost = 50 / 10 = 5 + 'v5/position/set-auto-add-margin': 5, + 'v5/position/add-margin': 5, + 'v5/position/move-positions': 5, + 'v5/position/confirm-pending-mmr': 5, + # account + 'v5/account/upgrade-to-uta': 5, + 'v5/account/quick-repayment': 5, + 'v5/account/set-margin-mode': 5, + 'v5/account/set-hedging-mode': 5, + 'v5/account/mmp-modify': 5, + 'v5/account/mmp-reset': 5, + 'v5/account/borrow': 5, + 'v5/account/repay': 5, + 'v5/account/no-convert-repay': 5, + # asset + 'v5/asset/exchange/quote-apply': 1, # 50/s + 'v5/asset/exchange/convert-execute': 1, # 50/s + 'v5/asset/transfer/inter-transfer': 50, # 1/s => cost = 50 / 1 = 50 + 'v5/asset/transfer/save-transfer-sub-member': 150, # 1/3/s => cost = 50 / 1/3 = 150 + 'v5/asset/transfer/universal-transfer': 10, # 5/s => cost = 50 / 5 = 10 + 'v5/asset/deposit/deposit-to-account': 5, + 'v5/asset/withdraw/create': 50, # 1/s => cost = 50 / 1 = 50 + 'v5/asset/withdraw/cancel': 50, # 1/s => cost = 50 / 1 = 50 + # user + 'v5/user/create-sub-member': 10, # 5/s => cost = 50 / 5 = 10 + 'v5/user/create-sub-api': 10, # 5/s => cost = 50 / 5 = 10 + 'v5/user/frozen-sub-member': 10, # 5/s => cost = 50 / 5 = 10 + 'v5/user/update-api': 10, # 5/s => cost = 50 / 5 = 10 + 'v5/user/update-sub-api': 10, # 5/s => cost = 50 / 5 = 10 + 'v5/user/delete-api': 10, # 5/s => cost = 50 / 5 = 10 + 'v5/user/delete-sub-api': 10, # 5/s => cost = 50 / 5 = 10 + # spot leverage token + 'v5/spot-lever-token/purchase': 2.5, # 20/s => cost = 50 / 20 = 2.5 + 'v5/spot-lever-token/redeem': 2.5, # 20/s => cost = 50 / 20 = 2.5 + # spot margin trade + 'v5/spot-margin-trade/switch-mode': 5, + 'v5/spot-margin-trade/set-leverage': 5, + 'v5/spot-cross-margin-trade/loan': 2.5, # 20/s => cost = 50 / 20 = 2.5 + 'v5/spot-cross-margin-trade/repay': 2.5, # 20/s => cost = 50 / 20 = 2.5 + 'v5/spot-cross-margin-trade/switch': 2.5, # 20/s => cost = 50 / 20 = 2.5 + # crypto loan + 'v5/crypto-loan/borrow': 5, + 'v5/crypto-loan/repay': 5, + 'v5/crypto-loan/adjust-ltv': 5, + # institutional lending + 'v5/ins-loan/association-uid': 5, + # c2c lending + 'v5/lending/purchase': 5, + 'v5/lending/redeem': 5, + 'v5/lending/redeem-cancel': 5, + 'v5/account/set-collateral-switch': 5, + 'v5/account/set-collateral-switch-batch': 5, + # demo trading + 'v5/account/demo-apply-money': 5, + # broker + 'v5/broker/award/info': 5, + 'v5/broker/award/distribute-award': 5, + 'v5/broker/award/distribution-record': 5, + # earn + 'v5/earn/place-order': 5, + }, + }, + }, + 'httpExceptions': { + '403': RateLimitExceeded, # Forbidden -- You request too many times + }, + 'exceptions': { + # Uncodumented explanation of error strings: + # - oc_diff: order cost needed to place self order + # - new_oc: total order cost of open orders including the order you are trying to open + # - ob: order balance - the total cost of current open orders + # - ab: available balance + 'exact': { + '-10009': BadRequest, # {"ret_code":-10009,"ret_msg":"Invalid period!","result":null,"token":null} + '-1004': BadRequest, # {"ret_code":-1004,"ret_msg":"Missing required parameter \u0027symbol\u0027","ext_code":null,"ext_info":null,"result":null} + '-1021': BadRequest, # {"ret_code":-1021,"ret_msg":"Timestamp for self request is outside of the recvWindow.","ext_code":null,"ext_info":null,"result":null} + '-1103': BadRequest, # An unknown parameter was sent. + '-1140': InvalidOrder, # {"ret_code":-1140,"ret_msg":"Transaction amount lower than the minimum.","result":{},"ext_code":"","ext_info":null,"time_now":"1659204910.248576"} + '-1197': InvalidOrder, # {"ret_code":-1197,"ret_msg":"Your order quantity to buy is too large. The filled price may deviate significantly from the market price. Please try again","result":{},"ext_code":"","ext_info":null,"time_now":"1659204531.979680"} + '-2013': InvalidOrder, # {"ret_code":-2013,"ret_msg":"Order does not exist.","ext_code":null,"ext_info":null,"result":null} + '-2015': AuthenticationError, # Invalid API-key, IP, or permissions for action. + '-6017': BadRequest, # Repayment amount has exceeded the total liability + '-6025': BadRequest, # Amount to borrow cannot be lower than the min. amount to borrow(per transaction) + '-6029': BadRequest, # Amount to borrow has exceeded the user's estimated max amount to borrow + '5004': ExchangeError, # {"retCode":5004,"retMsg":"Server Timeout","result":null,"retExtInfo":{},"time":1667577060106} + '7001': BadRequest, # {"retCode":7001,"retMsg":"request params type error"} + '10001': BadRequest, # parameter error + '10002': InvalidNonce, # request expired, check your timestamp and recv_window + '10003': AuthenticationError, # Invalid apikey + '10004': AuthenticationError, # invalid sign + '10005': PermissionDenied, # permission denied for current apikey + '10006': RateLimitExceeded, # too many requests + '10007': AuthenticationError, # api_key not found in your request parameters + '10008': AccountSuspended, # User had been banned + '10009': AuthenticationError, # IP had been banned + '10010': PermissionDenied, # request ip mismatch + '10014': BadRequest, # Request is duplicate + '10016': ExchangeError, # {"retCode":10016,"retMsg":"System error. Please try again later."} + '10017': BadRequest, # request path not found or request method is invalid + '10018': RateLimitExceeded, # exceed ip rate limit + '10020': PermissionDenied, # {"retCode":10020,"retMsg":"your account is not a unified margin account, please update your account","result":null,"retExtInfo":null,"time":1664783731123} + '10024': PermissionDenied, # Compliance rules triggered + '10027': PermissionDenied, # Trading Banned + '10028': PermissionDenied, # The API can only be accessed by unified account users. + '10029': PermissionDenied, # The requested symbol is invalid, please check symbol whitelist + '12137': InvalidOrder, # {"retCode":12137,"retMsg":"Order quantity has too many decimals.","result":{},"retExtInfo":{},"time":1695900943033} + '12201': BadRequest, # {"retCode":12201,"retMsg":"Invalid orderCategory parameter.","result":{},"retExtInfo":null,"time":1666699391220} + '12141': BadRequest, # "retCode":12141,"retMsg":"Duplicate clientOrderId.","result":{},"retExtInfo":{},"time":1686134298989} + '100028': PermissionDenied, # The API cannot be accessed by unified account users. + '110001': OrderNotFound, # Order does not exist + '110003': InvalidOrder, # Order price is out of permissible range + '110004': InsufficientFunds, # Insufficient wallet balance + '110005': InvalidOrder, # position status + '110006': InsufficientFunds, # cannot afford estimated position_margin + '110007': InsufficientFunds, # {"retCode":110007,"retMsg":"ab not enough for new order","result":{},"retExtInfo":{},"time":1668838414793} + '110008': InvalidOrder, # Order has been finished or canceled + '110009': InvalidOrder, # The number of stop orders exceeds maximum limit allowed + '110010': InvalidOrder, # Order already cancelled + '110011': InvalidOrder, # Any adjustments made will trigger immediate liquidation + '110012': InsufficientFunds, # Available balance not enough + '110013': BadRequest, # Due to risk limit, cannot set leverage + '110014': InsufficientFunds, # Available balance not enough to add margin + '110015': BadRequest, # the position is in cross_margin + '110016': InvalidOrder, # Requested quantity of contracts exceeds risk limit, please adjust your risk limit level before trying again + '110017': InvalidOrder, # Reduce-only rule not satisfied + '110018': BadRequest, # userId illegal + '110019': InvalidOrder, # orderId illegal + '110020': InvalidOrder, # number of active orders greater than 500 + '110021': InvalidOrder, # Open Interest exceeded + '110022': InvalidOrder, # qty has been limited, cannot modify the order to add qty + '110023': InvalidOrder, # This contract only supports position reduction operation, please contact customer service for details + '110024': BadRequest, # You have an existing position, so position mode cannot be switched + '110025': NoChange, # Position mode is not modified + '110026': MarginModeAlreadySet, # Cross/isolated margin mode is not modified + '110027': NoChange, # Margin is not modified + '110028': BadRequest, # Open orders exist, so you cannot change position mode + '110029': BadRequest, # Hedge mode is not available for self symbol + '110030': InvalidOrder, # Duplicate orderId + '110031': InvalidOrder, # risk limit info does not exists + '110032': InvalidOrder, # Illegal order + '110033': InvalidOrder, # Margin cannot be set without open position + '110034': InvalidOrder, # There is no net position + '110035': InvalidOrder, # Cancel order is not completed before liquidation + '110036': InvalidOrder, # Cross margin mode is not allowed to change leverage + '110037': InvalidOrder, # User setting list does not have self symbol + '110038': InvalidOrder, # Portfolio margin mode is not allowed to change leverage + '110039': InvalidOrder, # Maintain margin rate is too high, which may trigger liquidation + '110040': InvalidOrder, # Order will trigger forced liquidation, please resubmit the order + '110041': InvalidOrder, # Skip liquidation is not allowed when a position or maker order exists + '110042': InvalidOrder, # Pre-delivery status can only reduce positions + '110043': BadRequest, # Set leverage not modified + '110044': InsufficientFunds, # Insufficient available margin + '110045': InsufficientFunds, # Insufficient wallet balance + '110046': BadRequest, # Any adjustments made will trigger immediate liquidation + '110047': BadRequest, # Risk limit cannot be adjusted due to insufficient available margin + '110048': BadRequest, # Risk limit cannot be adjusted current/expected position value held exceeds the revised risk limit + '110049': BadRequest, # Tick notes can only be numbers + '110050': BadRequest, # Coin is not in the range of selected + '110051': InsufficientFunds, # The user's available balance cannot cover the lowest price of the current market + '110052': InsufficientFunds, # User's available balance is insufficient to set a price + '110053': InsufficientFunds, # The user's available balance cannot cover the current market price and upper limit price + '110054': InvalidOrder, # This position has at least one take profit link order, so the take profit and stop loss mode cannot be switched + '110055': InvalidOrder, # This position has at least one stop loss link order, so the take profit and stop loss mode cannot be switched + '110056': InvalidOrder, # This position has at least one trailing stop link order, so the take profit and stop loss mode cannot be switched + '110057': InvalidOrder, # Conditional order or limit order contains TP/SL related params + '110058': InvalidOrder, # Insufficient number of remaining position size to set take profit and stop loss + '110059': InvalidOrder, # In the case of partial filled of the open order, it is not allowed to modify the take profit and stop loss settings of the open order + '110060': BadRequest, # Under full TP/SL mode, it is not allowed to modify TP/SL + '110061': BadRequest, # Under partial TP/SL mode, TP/SL set more than 20 + '110062': BadRequest, # Institution MMP profile not found. + '110063': ExchangeError, # Settlement in progress! xxx not available for trades. + '110064': InvalidOrder, # The number of contracts modified cannot be less than or equal to the filled quantity + '110065': PermissionDenied, # MMP hasn't yet been enabled for your account. Please contact your BD manager. + '110066': ExchangeError, # No trading is allowed at the current time + '110067': PermissionDenied, # unified account is not support + '110068': PermissionDenied, # Leveraged user trading is not allowed + '110069': PermissionDenied, # Do not allow OTC lending users to trade + '110070': InvalidOrder, # ETP symbols are not allowed to be traded + '110071': ExchangeError, # Sorry, we're revamping the Unified Margin Account! Currently, new upgrades are not supported. If you have any questions, please contact our 24/7 customer support. + '110072': InvalidOrder, # OrderLinkedID is duplicate + '110073': ExchangeError, # Set margin mode failed + '110092': InvalidOrder, # expect Rising, but trigger_price[XXXXX] <= current[XXXXX] + '110093': InvalidOrder, # expect Falling, but trigger_price[XXXXX] >= current[XXXXX] + '110094': InvalidOrder, # Order notional value below the lower limit + '130006': InvalidOrder, # {"ret_code":130006,"ret_msg":"The number of contracts exceeds maximum limit allowed: too large","ext_code":"","ext_info":"","result":null,"time_now":"1658397095.099030","rate_limit_status":99,"rate_limit_reset_ms":1658397095097,"rate_limit":100} + '130021': InsufficientFunds, # {"ret_code":130021,"ret_msg":"orderfix price failed for CannotAffordOrderCost.","ext_code":"","ext_info":"","result":null,"time_now":"1644588250.204878","rate_limit_status":98,"rate_limit_reset_ms":1644588250200,"rate_limit":100} | {"ret_code":130021,"ret_msg":"oc_diff[1707966351], new_oc[1707966351] with ob[....]+AB[....]","ext_code":"","ext_info":"","result":null,"time_now":"1658395300.872766","rate_limit_status":99,"rate_limit_reset_ms":1658395300855,"rate_limit":100} caused issues/9149#issuecomment-1146559498 + '130074': InvalidOrder, # {"ret_code":130074,"ret_msg":"expect Rising, but trigger_price[190000000] \u003c= current[211280000]??LastPrice","ext_code":"","ext_info":"","result":null,"time_now":"1655386638.067076","rate_limit_status":97,"rate_limit_reset_ms":1655386638065,"rate_limit":100} + '131001': InsufficientFunds, # {"retCode":131001,"retMsg":"the available balance is not sufficient to cover the handling fee","result":{},"retExtInfo":{},"time":1666892821245} + '131084': ExchangeError, # Withdraw failed because of Uta Upgrading + '131200': ExchangeError, # Service error + '131201': ExchangeError, # Internal error + '131202': BadRequest, # Invalid memberId + '131203': BadRequest, # Request parameter error + '131204': BadRequest, # Account info error + '131205': BadRequest, # Query transfer error + '131206': ExchangeError, # Fail to transfer + '131207': BadRequest, # Account not exist + '131208': ExchangeError, # Forbid transfer + '131209': BadRequest, # Get subMember relation error + '131210': BadRequest, # Amount accuracy error + '131211': BadRequest, # fromAccountType can't be the same + '131212': InsufficientFunds, # Insufficient balance + '131213': BadRequest, # TransferLTV check error + '131214': BadRequest, # TransferId exist + '131215': BadRequest, # Amount error + '131216': ExchangeError, # Query balance error + '131217': ExchangeError, # Risk check error + '131231': NotSupported, # Transfers into self account are not supported + '131232': NotSupported, # Transfers out self account are not supported + '131002': BadRequest, # Parameter error + '131003': ExchangeError, # Interal error + '131004': AuthenticationError, # KYC needed + '131085': InsufficientFunds, # Withdrawal amount is greater than your availale balance(the deplayed withdrawal is triggered) + '131086': BadRequest, # Withdrawal amount exceeds risk limit(the risk limit of margin trade is triggered) + '131088': BadRequest, # The withdrawal amount exceeds the remaining withdrawal limit of your identity verification level. The current available amount for withdrawal : %s + '131089': BadRequest, # User sensitive operation, withdrawal is prohibited within 24 hours + '131090': ExchangeError, # User withdraw has been banned + '131091': ExchangeError, # Blocked login status does not allow withdrawals + '131092': ExchangeError, # User status is abnormal + '131093': ExchangeError, # The withdrawal address is not in the whitelist + '131094': BadRequest, # UserId is not in the whitelist + '131095': BadRequest, # Withdrawl amount exceeds the 24 hour platform limit + '131096': BadRequest, # Withdraw amount does not satify the lower limit or upper limit + '131097': ExchangeError, # Withdrawal of self currency has been closed + '131098': ExchangeError, # Withdrawal currently is not availble from new address + '131099': ExchangeError, # Hot wallet status can cancel the withdraw + '140001': OrderNotFound, # Order does not exist + '140003': InvalidOrder, # Order price is out of permissible range + '140004': InsufficientFunds, # Insufficient wallet balance + '140005': InvalidOrder, # position status + '140006': InsufficientFunds, # cannot afford estimated position_margin + '140007': InsufficientFunds, # Insufficient available balance + '140008': InvalidOrder, # Order has been finished or canceled + '140009': InvalidOrder, # The number of stop orders exceeds maximum limit allowed + '140010': InvalidOrder, # Order already cancelled + '140011': InvalidOrder, # Any adjustments made will trigger immediate liquidation + '140012': InsufficientFunds, # Available balance not enough + '140013': BadRequest, # Due to risk limit, cannot set leverage + '140014': InsufficientFunds, # Available balance not enough to add margin + '140015': InvalidOrder, # the position is in cross_margin + '140016': InvalidOrder, # Requested quantity of contracts exceeds risk limit, please adjust your risk limit level before trying again + '140017': InvalidOrder, # Reduce-only rule not satisfied + '140018': BadRequest, # userId illegal + '140019': InvalidOrder, # orderId illegal + '140020': InvalidOrder, # number of active orders greater than 500 + '140021': InvalidOrder, # Open Interest exceeded + '140022': InvalidOrder, # qty has been limited, cannot modify the order to add qty + '140023': InvalidOrder, # This contract only supports position reduction operation, please contact customer service for details + '140024': BadRequest, # You have an existing position, so position mode cannot be switched + '140025': BadRequest, # Position mode is not modified + '140026': BadRequest, # Cross/isolated margin mode is not modified + '140027': BadRequest, # Margin is not modified + '140028': InvalidOrder, # Open orders exist, so you cannot change position mode + '140029': BadRequest, # Hedge mode is not available for self symbol + '140030': InvalidOrder, # Duplicate orderId + '140031': BadRequest, # risk limit info does not exists + '140032': InvalidOrder, # Illegal order + '140033': InvalidOrder, # Margin cannot be set without open position + '140034': InvalidOrder, # There is no net position + '140035': InvalidOrder, # Cancel order is not completed before liquidation + '140036': BadRequest, # Cross margin mode is not allowed to change leverage + '140037': InvalidOrder, # User setting list does not have self symbol + '140038': BadRequest, # Portfolio margin mode is not allowed to change leverage + '140039': BadRequest, # Maintain margin rate is too high, which may trigger liquidation + '140040': InvalidOrder, # Order will trigger forced liquidation, please resubmit the order + '140041': InvalidOrder, # Skip liquidation is not allowed when a position or maker order exists + '140042': InvalidOrder, # Pre-delivery status can only reduce positions + '140043': BadRequest, # Set leverage not modified + '140044': InsufficientFunds, # Insufficient available margin + '140045': InsufficientFunds, # Insufficient wallet balance + '140046': BadRequest, # Any adjustments made will trigger immediate liquidation + '140047': BadRequest, # Risk limit cannot be adjusted due to insufficient available margin + '140048': BadRequest, # Risk limit cannot be adjusted current/expected position value held exceeds the revised risk limit + '140049': BadRequest, # Tick notes can only be numbers + '140050': InvalidOrder, # Coin is not in the range of selected + '140051': InsufficientFunds, # The user's available balance cannot cover the lowest price of the current market + '140052': InsufficientFunds, # User's available balance is insufficient to set a price + '140053': InsufficientFunds, # The user's available balance cannot cover the current market price and upper limit price + '140054': InvalidOrder, # This position has at least one take profit link order, so the take profit and stop loss mode cannot be switched + '140055': InvalidOrder, # This position has at least one stop loss link order, so the take profit and stop loss mode cannot be switched + '140056': InvalidOrder, # This position has at least one trailing stop link order, so the take profit and stop loss mode cannot be switched + '140057': InvalidOrder, # Conditional order or limit order contains TP/SL related params + '140058': InvalidOrder, # Insufficient number of remaining position size to set take profit and stop loss + '140059': InvalidOrder, # In the case of partial filled of the open order, it is not allowed to modify the take profit and stop loss settings of the open order + '140060': BadRequest, # Under full TP/SL mode, it is not allowed to modify TP/SL + '140061': BadRequest, # Under partial TP/SL mode, TP/SL set more than 20 + '140062': BadRequest, # Institution MMP profile not found. + '140063': ExchangeError, # Settlement in progress! xxx not available for trades. + '140064': InvalidOrder, # The number of contracts modified cannot be less than or equal to the filled quantity + '140065': PermissionDenied, # MMP hasn't yet been enabled for your account. Please contact your BD manager. + '140066': ExchangeError, # No trading is allowed at the current time + '140067': PermissionDenied, # unified account is not support + '140068': PermissionDenied, # Leveraged user trading is not allowed + '140069': PermissionDenied, # Do not allow OTC lending users to trade + '140070': InvalidOrder, # ETP symbols are not allowed to be traded + '170001': ExchangeError, # Internal error. + '170005': InvalidOrder, # Too many new orders; current limit is %s orders per %s. + '170007': RequestTimeout, # Timeout waiting for response from backend server. + '170010': InvalidOrder, # Purchase failed: Exceed the maximum position limit of leveraged tokens, the current available limit is %s USDT + '170011': InvalidOrder, # "Purchase failed: Exceed the maximum position limit of innovation tokens, + '170019': InvalidOrder, # the current available limit is replaceKey0 USDT" + '170031': ExchangeError, # The feature has been suspended + '170032': ExchangeError, # Network error. Please try again later + '170033': InsufficientFunds, # margin Insufficient account balance + '170034': InsufficientFunds, # Liability over flow in spot leverage trade! + '170035': BadRequest, # Submitted to the system for processing! + '170036': BadRequest, # You haven't enabled Cross Margin Trading yet. To do so, please head to the PC trading site or the Bybit app + '170037': BadRequest, # Cross Margin Trading not yet supported by the selected coin + '170105': BadRequest, # Parameter '%s' was empty. + '170115': InvalidOrder, # Invalid timeInForce. + '170116': InvalidOrder, # Invalid orderType. + '170117': InvalidOrder, # Invalid side. + '170121': InvalidOrder, # Invalid symbol. + '170124': InvalidOrder, # Order amount too large. + '170130': BadRequest, # Data sent for paramter '%s' is not valid. + '170131': InsufficientFunds, # Balance insufficient + '170132': InvalidOrder, # Order price too high. + '170133': InvalidOrder, # Order price lower than the minimum. + '170134': InvalidOrder, # Order price decimal too long. + '170135': InvalidOrder, # Order quantity too large. + '170136': InvalidOrder, # Order quantity lower than the minimum. + '170137': InvalidOrder, # Order volume decimal too long + '170139': InvalidOrder, # Order has been filled. + '170140': InvalidOrder, # Transaction amount lower than the minimum. + '170141': InvalidOrder, # Duplicate clientOrderId + '170142': InvalidOrder, # Order has been canceled + '170143': InvalidOrder, # Cannot be found on order book + '170144': InvalidOrder, # Order has been locked + '170145': InvalidOrder, # This order type does not support cancellation + '170146': InvalidOrder, # Order creation timeout + '170147': InvalidOrder, # Order cancellation timeout + '170148': InvalidOrder, # Market order amount decimal too long + '170149': ExchangeError, # Create order failed + '170150': ExchangeError, # Cancel order failed + '170151': InvalidOrder, # The trading pair is not open yet + '170157': InvalidOrder, # The trading pair is not available for api trading + '170159': InvalidOrder, # Market Order is not supported within the first %s minutes of newly launched pairs due to risk control. + '170190': InvalidOrder, # Cancel order has been finished + '170191': InvalidOrder, # Can not cancel order, please try again later + '170192': InvalidOrder, # Order price cannot be higher than %s . + '170193': InvalidOrder, # Buy order price cannot be higher than %s. + '170194': InvalidOrder, # Sell order price cannot be lower than %s. + '170195': InvalidOrder, # Please note that your order may not be filled + '170196': InvalidOrder, # Please note that your order may not be filled + '170197': InvalidOrder, # Your order quantity to buy is too large. The filled price may deviate significantly from the market price. Please try again + '170198': InvalidOrder, # Your order quantity to sell is too large. The filled price may deviate significantly from the market price. Please try again + '170199': InvalidOrder, # Your order quantity to buy is too large. The filled price may deviate significantly from the nav. Please try again. + '170200': InvalidOrder, # Your order quantity to sell is too large. The filled price may deviate significantly from the nav. Please try again. + '170201': PermissionDenied, # Your account has been restricted for trades. If you have any questions, please email us at support@bybit.com + '170202': InvalidOrder, # Invalid orderFilter parameter. + '170203': InvalidOrder, # Please enter the TP/SL price. + '170204': InvalidOrder, # trigger price cannot be higher than 110% price. + '170206': InvalidOrder, # trigger price cannot be lower than 90% of qty. + '170210': InvalidOrder, # New order rejected. + '170213': OrderNotFound, # Order does not exist. + '170217': InvalidOrder, # Only LIMIT-MAKER order is supported for the current pair. + '170218': InvalidOrder, # The LIMIT-MAKER order is rejected due to invalid price. + '170221': BadRequest, # This coin does not exist. + '170222': RateLimitExceeded, # Too many hasattr(self, requests) time frame. + '170223': InsufficientFunds, # Your Spot Account with Institutional Lending triggers an alert or liquidation. + '170224': PermissionDenied, # You're not a user of the Innovation Zone. + '170226': InsufficientFunds, # Your Spot Account for Margin Trading is being liquidated. + '170227': ExchangeError, # This feature is not supported. + '170228': InvalidOrder, # The purchase amount of each order exceeds the estimated maximum purchase amount. + '170229': InvalidOrder, # The sell quantity per order exceeds the estimated maximum sell quantity. + '170234': ExchangeError, # System Error + '170241': ManualInteractionNeeded, # To proceed with trading, users must read through and confirm that they fully understand the project's risk disclosure document. + '175000': InvalidOrder, # The serialNum is already in use. + '175001': InvalidOrder, # Daily purchase limit has been exceeded. Please try again later. + '175002': InvalidOrder, # There's a large number of purchase orders. Please try again later. + '175003': InsufficientFunds, # Insufficient available balance. Please make a deposit and try again. + '175004': InvalidOrder, # Daily redemption limit has been exceeded. Please try again later. + '175005': InvalidOrder, # There's a large number of redemption orders. Please try again later. + '175006': InsufficientFunds, # Insufficient available balance. Please make a deposit and try again. + '175007': InvalidOrder, # Order not found. + '175008': InvalidOrder, # Purchase period hasn't started yet. + '175009': InvalidOrder, # Purchase amount has exceeded the upper limit. + '175010': PermissionDenied, # You haven't passed the quiz yet! To purchase and/or redeem an LT, please complete the quiz first. + '175012': InvalidOrder, # Redemption period hasn't started yet. + '175013': InvalidOrder, # Redemption amount has exceeded the upper limit. + '175014': InvalidOrder, # Purchase of the LT has been temporarily suspended. + '175015': InvalidOrder, # Redemption of the LT has been temporarily suspended. + '175016': InvalidOrder, # Invalid format. Please check the length and numeric precision. + '175017': InvalidOrder, # Failed to place order:Exceed the maximum position limit of leveraged tokens, the current available limit is XXXX USDT + '175027': ExchangeError, # Subscriptions and redemptions are temporarily unavailable while account upgrade is in progress + '176002': BadRequest, # Query user account info error + '176004': BadRequest, # Query order history start time exceeds end time + '176003': BadRequest, # Query user loan history error + '176006': BadRequest, # Repayment Failed + '176005': BadRequest, # Failed to borrow + '176008': BadRequest, # You haven't enabled Cross Margin Trading yet. To do so + '176007': BadRequest, # User not found + '176010': BadRequest, # Failed to locate the coins to borrow + '176009': BadRequest, # You haven't enabled Cross Margin Trading yet. To do so + '176012': BadRequest, # Pair not available + '176011': BadRequest, # Cross Margin Trading not yet supported by the selected coin + '176014': BadRequest, # Repeated repayment requests + '176013': BadRequest, # Cross Margin Trading not yet supported by the selected pair + '176015': InsufficientFunds, # Insufficient available balance + '176016': BadRequest, # No repayment required + '176017': BadRequest, # Repayment amount has exceeded the total liability + '176018': BadRequest, # Settlement in progress + '176019': BadRequest, # Liquidation in progress + '176020': BadRequest, # Failed to locate repayment history + '176021': BadRequest, # Repeated borrowing requests + '176022': BadRequest, # Coins to borrow not generally available yet + '176023': BadRequest, # Pair to borrow not generally available yet + '176024': BadRequest, # Invalid user status + '176025': BadRequest, # Amount to borrow cannot be lower than the min. amount to borrow(per transaction) + '176026': BadRequest, # Amount to borrow cannot be larger than the max. amount to borrow(per transaction) + '176027': BadRequest, # Amount to borrow cannot be higher than the max. amount to borrow per user + '176028': BadRequest, # Amount to borrow has exceeded Bybit's max. amount to borrow + '176029': BadRequest, # Amount to borrow has exceeded the user's estimated max. amount to borrow + '176030': BadRequest, # Query user loan info error + '176031': BadRequest, # Number of decimals has exceeded the maximum precision + '176034': BadRequest, # The leverage ratio is out of range + '176035': PermissionDenied, # Failed to close the leverage switch during liquidation + '176036': PermissionDenied, # Failed to adjust leverage switch during forced liquidation + '176037': PermissionDenied, # For non-unified transaction users, the operation failed + '176038': BadRequest, # The spot leverage is closed and the current operation is not allowed + '176039': BadRequest, # Borrowing, current operation is not allowed + '176040': BadRequest, # There is a spot leverage order, and the adjustment of the leverage switch failed! + '181000': BadRequest, # category is null + '181001': BadRequest, # category only support linear or option or spot. + '181002': InvalidOrder, # symbol is null. + '181003': InvalidOrder, # side is null. + '181004': InvalidOrder, # side only support Buy or Sell. + '182000': InvalidOrder, # symbol related quote price is null + '181017': BadRequest, # OrderStatus must be final status + '20001': OrderNotFound, # Order not exists + '20003': InvalidOrder, # missing parameter side + '20004': InvalidOrder, # invalid parameter side + '20005': InvalidOrder, # missing parameter symbol + '20006': InvalidOrder, # invalid parameter symbol + '20007': InvalidOrder, # missing parameter order_type + '20008': InvalidOrder, # invalid parameter order_type + '20009': InvalidOrder, # missing parameter qty + '20010': InvalidOrder, # qty must be greater than 0 + '20011': InvalidOrder, # qty must be an integer + '20012': InvalidOrder, # qty must be greater than zero and less than 1 million + '20013': InvalidOrder, # missing parameter price + '20014': InvalidOrder, # price must be greater than 0 + '20015': InvalidOrder, # missing parameter time_in_force + '20016': InvalidOrder, # invalid value for parameter time_in_force + '20017': InvalidOrder, # missing parameter order_id + '20018': InvalidOrder, # invalid date format + '20019': InvalidOrder, # missing parameter stop_px + '20020': InvalidOrder, # missing parameter base_price + '20021': InvalidOrder, # missing parameter stop_order_id + '20022': BadRequest, # missing parameter leverage + '20023': BadRequest, # leverage must be a number + '20031': BadRequest, # leverage must be greater than zero + '20070': BadRequest, # missing parameter margin + '20071': BadRequest, # margin must be greater than zero + '20084': BadRequest, # order_id or order_link_id is required + '30001': BadRequest, # order_link_id is repeated + '30003': InvalidOrder, # qty must be more than the minimum allowed + '30004': InvalidOrder, # qty must be less than the maximum allowed + '30005': InvalidOrder, # price exceeds maximum allowed + '30007': InvalidOrder, # price exceeds minimum allowed + '30008': InvalidOrder, # invalid order_type + '30009': ExchangeError, # no position found + '30010': InsufficientFunds, # insufficient wallet balance + '30011': PermissionDenied, # operation not allowed is undergoing liquidation + '30012': PermissionDenied, # operation not allowed is undergoing ADL + '30013': PermissionDenied, # position is in liq or adl status + '30014': InvalidOrder, # invalid closing order, qty should not greater than size + '30015': InvalidOrder, # invalid closing order, side should be opposite + '30016': ExchangeError, # TS and SL must be cancelled first while closing position + '30017': InvalidOrder, # estimated fill price cannot be lower than current Buy liq_price + '30018': InvalidOrder, # estimated fill price cannot be higher than current Sell liq_price + '30019': InvalidOrder, # cannot attach TP/SL params for non-zero position when placing non-opening position order + '30020': InvalidOrder, # position already has TP/SL params + '30021': InvalidOrder, # cannot afford estimated position_margin + '30022': InvalidOrder, # estimated buy liq_price cannot be higher than current mark_price + '30023': InvalidOrder, # estimated sell liq_price cannot be lower than current mark_price + '30024': InvalidOrder, # cannot set TP/SL/TS for zero-position + '30025': InvalidOrder, # trigger price should bigger than 10% of last price + '30026': InvalidOrder, # price too high + '30027': InvalidOrder, # price set for Take profit should be higher than Last Traded Price + '30028': InvalidOrder, # price set for Stop loss should be between Liquidation price and Last Traded Price + '30029': InvalidOrder, # price set for Stop loss should be between Last Traded Price and Liquidation price + '30030': InvalidOrder, # price set for Take profit should be lower than Last Traded Price + '30031': InsufficientFunds, # insufficient available balance for order cost + '30032': InvalidOrder, # order has been filled or cancelled + '30033': RateLimitExceeded, # The number of stop orders exceeds maximum limit allowed + '30034': OrderNotFound, # no order found + '30035': RateLimitExceeded, # too fast to cancel + '30036': ExchangeError, # the expected position value after order execution exceeds the current risk limit + '30037': InvalidOrder, # order already cancelled + '30041': ExchangeError, # no position found + '30042': InsufficientFunds, # insufficient wallet balance + '30043': InvalidOrder, # operation not allowed is undergoing liquidation + '30044': InvalidOrder, # operation not allowed is undergoing AD + '30045': InvalidOrder, # operation not allowed is not normal status + '30049': InsufficientFunds, # insufficient available balance + '30050': ExchangeError, # any adjustments made will trigger immediate liquidation + '30051': ExchangeError, # due to risk limit, cannot adjust leverage + '30052': ExchangeError, # leverage can not less than 1 + '30054': ExchangeError, # position margin is invalid + '30057': ExchangeError, # requested quantity of contracts exceeds risk limit + '30063': ExchangeError, # reduce-only rule not satisfied + '30067': InsufficientFunds, # insufficient available balance + '30068': ExchangeError, # exit value must be positive + '30074': InvalidOrder, # can't create the stop order, because you expect the order will be triggered when the LastPrice(or IndexPrice、 MarkPrice, determined by trigger_by) is raising to stop_px, but the LastPrice(or IndexPrice、 MarkPrice) is already equal to or greater than stop_px, please adjust base_price or stop_px + '30075': InvalidOrder, # can't create the stop order, because you expect the order will be triggered when the LastPrice(or IndexPrice、 MarkPrice, determined by trigger_by) is falling to stop_px, but the LastPrice(or IndexPrice、 MarkPrice) is already equal to or less than stop_px, please adjust base_price or stop_px + '30078': ExchangeError, # {"ret_code":30078,"ret_msg":"","ext_code":"","ext_info":"","result":null,"time_now":"1644853040.916000","rate_limit_status":73,"rate_limit_reset_ms":1644853040912,"rate_limit":75} + # '30084': BadRequest, # Isolated not modified, see handleErrors below + '33004': AuthenticationError, # apikey already expired + '34026': ExchangeError, # the limit is no change + '34036': BadRequest, # {"ret_code":34036,"ret_msg":"leverage not modified","ext_code":"","ext_info":"","result":null,"time_now":"1652376449.258918","rate_limit_status":74,"rate_limit_reset_ms":1652376449255,"rate_limit":75} + '35015': BadRequest, # {"ret_code":35015,"ret_msg":"Qty not in range","ext_code":"","ext_info":"","result":null,"time_now":"1652277215.821362","rate_limit_status":99,"rate_limit_reset_ms":1652277215819,"rate_limit":100} + '340099': ExchangeError, # Server error + '3400045': ExchangeError, # Set margin mode failed + '3100116': BadRequest, # {"retCode":3100116,"retMsg":"Order quantity below the lower limit 0.01.","result":null,"retExtMap":{"key0":"0.01"}} + '3100198': BadRequest, # {"retCode":3100198,"retMsg":"orderLinkId can not be empty.","result":null,"retExtMap":{}} + '3200300': InsufficientFunds, # {"retCode":3200300,"retMsg":"Insufficient margin balance.","result":null,"retExtMap":{}} + }, + 'broad': { + 'Not supported symbols': BadSymbol, # {"retCode":10001,"retMsg":"Not supported symbols","result":{},"retExtInfo":{},"time":1726147060461} + 'Request timeout': RequestTimeout, # {"retCode":10016,"retMsg":"Request timeout, please try again later","result":{},"retExtInfo":{},"time":1675307914985} + 'unknown orderInfo': OrderNotFound, # {"ret_code":-1,"ret_msg":"unknown orderInfo","ext_code":"","ext_info":"","result":null,"time_now":"1584030414.005545","rate_limit_status":99,"rate_limit_reset_ms":1584030414003,"rate_limit":100} + 'invalid api_key': AuthenticationError, # {"ret_code":10003,"ret_msg":"invalid api_key","ext_code":"","ext_info":"","result":null,"time_now":"1599547085.415797"} + # the below two issues are caused: issues/9149#issuecomment-1146559498, when response is such: {"ret_code":130021,"ret_msg":"oc_diff[1707966351], new_oc[1707966351] with ob[....]+AB[....]","ext_code":"","ext_info":"","result":null,"time_now":"1658395300.872766","rate_limit_status":99,"rate_limit_reset_ms":1658395300855,"rate_limit":100} + 'oc_diff': InsufficientFunds, + 'new_oc': InsufficientFunds, + 'openapi sign params error!': AuthenticationError, # {"retCode":10001,"retMsg":"empty value: apiTimestamp[] apiKey[] apiSignature[xxxxxxxxxxxxxxxxxxxxxxx]: openapi sign params error!","result":null,"retExtInfo":null,"time":1664789597123} + }, + }, + 'precisionMode': TICK_SIZE, + 'options': { + 'usePrivateInstrumentsInfo': False, + 'enableDemoTrading': False, + 'fetchMarkets': { + 'types': ['spot', 'linear', 'inverse', 'option'], + }, + 'enableUnifiedMargin': None, + 'enableUnifiedAccount': None, + 'unifiedMarginStatus': None, + 'createMarketBuyOrderRequiresPrice': False, # only True for classic accounts + 'createUnifiedMarginAccount': False, + 'defaultType': 'swap', # 'swap', 'future', 'option', 'spot' + 'defaultSubType': 'linear', # 'linear', 'inverse' + 'defaultSettle': 'USDT', # USDC for USDC settled markets + 'code': 'BTC', + 'recvWindow': 5 * 1000, # 5 sec default + 'timeDifference': 0, # the difference between system clock and exchange server clock + 'adjustForTimeDifference': False, # controls the adjustment logic upon instantiation + 'loadAllOptions': False, # load all possible option markets, adds signficant load time + 'loadExpiredOptions': False, # loads expired options, to load all possible expired options set loadAllOptions to True + 'brokerId': 'CCXT', + 'accountsByType': { + 'spot': 'SPOT', + 'margin': 'SPOT', + 'future': 'CONTRACT', + 'swap': 'CONTRACT', + 'option': 'OPTION', + 'investment': 'INVESTMENT', + 'unified': 'UNIFIED', + 'funding': 'FUND', + 'fund': 'FUND', + 'contract': 'CONTRACT', + }, + 'accountsById': { + 'SPOT': 'spot', + 'MARGIN': 'spot', + 'CONTRACT': 'contract', + 'OPTION': 'option', + 'INVESTMENT': 'investment', + 'UNIFIED': 'unified', + 'FUND': 'fund', + }, + 'networks': { + 'ERC20': 'ETH', + 'TRC20': 'TRX', + 'BEP20': 'BSC', + 'SOL': 'SOL', + 'ACA': 'ACA', + 'ADA': 'ADA', + 'ALGO': 'ALGO', + 'APT': 'APTOS', + 'AR': 'AR', + 'ARBONE': 'ARBI', + 'AVAXC': 'CAVAX', + 'AVAXX': 'XAVAX', + 'ATOM': 'ATOM', + 'BCH': 'BCH', + 'BEP2': 'BNB', + 'CHZ': 'CHZ', + 'DCR': 'DCR', + 'DGB': 'DGB', + 'DOGE': 'DOGE', + 'DOT': 'DOT', + 'EGLD': 'EGLD', + 'EOS': 'EOS', + 'ETC': 'ETC', + 'ETHF': 'ETHF', + 'ETHW': 'ETHW', + 'FIL': 'FIL', + 'STEP': 'FITFI', + 'FLOW': 'FLOW', + 'FTM': 'FTM', + 'GLMR': 'GLMR', + 'HBAR': 'HBAR', + 'HNT': 'HNT', + 'ICP': 'ICP', + 'ICX': 'ICX', + 'KDA': 'KDA', + 'KLAY': 'KLAY', + 'KMA': 'KMA', + 'KSM': 'KSM', + 'LTC': 'LTC', + # 'TERRA': 'LUNANEW', + # 'TERRACLASSIC': 'LUNA', + 'MATIC': 'MATIC', + 'MINA': 'MINA', + 'MOVR': 'MOVR', + 'NEAR': 'NEAR', + 'NEM': 'NEM', + 'OASYS': 'OAS', + 'OASIS': 'ROSE', + 'OMNI': 'OMNI', + 'ONE': 'ONE', + 'OPTIMISM': 'OP', + 'POKT': 'POKT', + 'QTUM': 'QTUM', + 'RVN': 'RVN', + 'SC': 'SC', + 'SCRT': 'SCRT', + 'STX': 'STX', + 'THETA': 'THETA', + 'TON': 'TON', + 'WAVES': 'WAVES', + 'WAX': 'WAXP', + 'XDC': 'XDC', + 'XEC': 'XEC', + 'XLM': 'XLM', + 'XRP': 'XRP', + 'XTZ': 'XTZ', + 'XYM': 'XYM', + 'ZEN': 'ZEN', + 'ZIL': 'ZIL', + 'ZKSYNC': 'ZKSYNC', + # todo: uncomment after consensus + # 'CADUCEUS': 'CMP', + # 'KON': 'KON', # konpay, "konchain" + # 'AURORA': 'AURORA', + # 'BITCOINGOLD': 'BTG', + }, + 'networksById': { + 'ETH': 'ERC20', + 'TRX': 'TRC20', + 'BSC': 'BEP20', + 'OMNI': 'OMNI', + 'SPL': 'SOL', + }, + 'defaultNetwork': 'ERC20', + 'defaultNetworks': { + 'USDT': 'TRC20', + }, + 'intervals': { + '5m': '5min', + '15m': '15min', + '30m': '30min', + '1h': '1h', + '4h': '4h', + '1d': '1d', + }, + 'useMarkPriceForPositionCollateral': False, # use mark price for position collateral + }, + 'features': { + 'default': { + 'sandbox': True, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerPriceType': { + 'last': True, + 'mark': True, + 'index': True, + }, + 'triggerDirection': True, + 'stopLossPrice': True, + 'takeProfitPrice': True, + 'attachedStopLossTakeProfit': { + 'triggerPriceType': { + 'last': True, + 'mark': True, + 'index': True, + }, + 'price': True, + }, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': True, + 'selfTradePrevention': True, # todo: implement + 'trailing': True, + 'iceberg': False, + 'leverage': False, + 'marketBuyRequiresPrice': False, + 'marketBuyByCost': True, + }, + 'createOrders': { + 'max': 10, + }, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 365 * 2, # 2 years + 'untilDays': 7, # days between start-end + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': True, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 50, + 'trigger': True, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 50, + 'daysBack': 365 * 2, # 2 years + 'daysBackCanceled': 1, + 'untilDays': 7, + 'trigger': True, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 1000, + }, + 'editOrders': { + 'max': 10, + }, + }, + 'spot': { + 'extends': 'default', + 'fetchCurrencies': { + 'private': True, + }, + 'createOrder': { + 'triggerPriceType': None, + 'triggerDirection': False, + 'attachedStopLossTakeProfit': { + 'triggerPriceType': None, + 'price': True, + }, + 'marketBuyRequiresPrice': True, + }, + }, + 'swap': { + 'linear': { + 'extends': 'default', + }, + 'inverse': { + 'extends': 'default', + }, + }, + 'future': { + 'linear': { + 'extends': 'default', + }, + 'inverse': { + 'extends': 'default', + }, + }, + }, + 'fees': { + 'trading': { + 'feeSide': 'get', + 'tierBased': True, + 'percentage': True, + 'taker': 0.00075, + 'maker': 0.0001, + }, + 'funding': { + 'tierBased': False, + 'percentage': False, + 'withdraw': {}, + 'deposit': {}, + }, + }, + }) + + def enable_demo_trading(self, enable: bool): + """ + enables or disables demo trading mode + + https://bybit-exchange.github.io/docs/v5/demo + + :param boolean [enable]: True if demo trading should be enabled, False otherwise + """ + if self.isSandboxModeEnabled: + raise NotSupported(self.id + ' demo trading does not support in sandbox environment') + # enable demo trading in bybit, see: https://bybit-exchange.github.io/docs/v5/demo + if enable: + self.urls['apiBackupDemoTrading'] = self.urls['api'] + self.urls['api'] = self.urls['demotrading'] + elif 'apiBackupDemoTrading' in self.urls: + self.urls['api'] = self.urls['apiBackupDemoTrading'] + newUrls = self.omit(self.urls, 'apiBackupDemoTrading') + self.urls = newUrls + self.options['enableDemoTrading'] = enable + + def nonce(self): + return self.milliseconds() - self.options['timeDifference'] + + def add_pagination_cursor_to_result(self, response): + result = self.safe_dict(response, 'result', {}) + data = self.safe_list_n(result, ['list', 'rows', 'data', 'dataList'], []) + paginationCursor = self.safe_string_2(result, 'nextPageCursor', 'cursor') + dataLength = len(data) + if (paginationCursor is not None) and (dataLength > 0): + first = data[0] + first['nextPageCursor'] = paginationCursor + data[0] = first + return data + + async def is_unified_enabled(self, params={}): + """ + + https://bybit-exchange.github.io/docs/v5/user/apikey-info#http-request + https://bybit-exchange.github.io/docs/v5/account/account-info + + returns [enableUnifiedMargin, enableUnifiedAccount] so the user can check if unified account is enabled + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns any: [enableUnifiedMargin, enableUnifiedAccount] + """ + # The API key of user id must own one of permissions will be allowed to call following API endpoints: + # SUB UID: "Account Transfer" + # MASTER UID: "Account Transfer", "Subaccount Transfer", "Withdrawal" + enableUnifiedMargin = self.safe_bool(self.options, 'enableUnifiedMargin') + enableUnifiedAccount = self.safe_bool(self.options, 'enableUnifiedAccount') + if enableUnifiedMargin is None or enableUnifiedAccount is None: + if self.options['enableDemoTrading']: + # info endpoint is not available in demo trading + # so we're assuming UTA is enabled + self.options['enableUnifiedMargin'] = False + self.options['enableUnifiedAccount'] = True + self.options['unifiedMarginStatus'] = 6 + return [self.options['enableUnifiedMargin'], self.options['enableUnifiedAccount']] + rawPromises = [self.privateGetV5UserQueryApi(params), self.privateGetV5AccountInfo(params)] + promises = await asyncio.gather(*rawPromises) + response = promises[0] + accountInfo = promises[1] + # + # { + # "retCode": 0, + # "retMsg": "", + # "result": { + # "id": "13770661", + # "note": "XXXXXX", + # "apiKey": "XXXXXX", + # "readOnly": 0, + # "secret": "", + # "permissions": { + # "ContractTrade": [...], + # "Spot": [...], + # "Wallet": [...], + # "Options": [...], + # "Derivatives": [...], + # "CopyTrading": [...], + # "BlockTrade": [...], + # "Exchange": [...], + # "NFT": [...], + # }, + # "ips": [...], + # "type": 1, + # "deadlineDay": 83, + # "expiredAt": "2023-05-15T03:21:05Z", + # "createdAt": "2022-10-16T02:24:40Z", + # "unified": 0, + # "uta": 0, + # "userID": 24600000, + # "inviterID": 0, + # "vipLevel": "No VIP", + # "mktMakerLevel": "0", + # "affiliateID": 0, + # "rsaPublicKey": "", + # "isMaster": False + # }, + # "retExtInfo": {}, + # "time": 1676891757649 + # } + # account info + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "marginMode": "REGULAR_MARGIN", + # "updatedTime": "1697078946000", + # "unifiedMarginStatus": 4, + # "dcpStatus": "OFF", + # "timeWindow": 10, + # "smpGroup": 0, + # "isMasterTrader": False, + # "spotHedgingStatus": "OFF" + # } + # } + # + result = self.safe_dict(response, 'result', {}) + accountResult = self.safe_dict(accountInfo, 'result', {}) + self.options['enableUnifiedMargin'] = self.safe_integer(result, 'unified') == 1 + self.options['enableUnifiedAccount'] = self.safe_integer(result, 'uta') == 1 + self.options['unifiedMarginStatus'] = self.safe_integer(accountResult, 'unifiedMarginStatus', 6) # default to uta 2.0 pro if not found + return [self.options['enableUnifiedMargin'], self.options['enableUnifiedAccount']] + + async def upgrade_unified_trade_account(self, params={}): + """ + upgrades the account to unified trade account *warning* self is irreversible + + https://bybit-exchange.github.io/docs/v5/account/upgrade-unified-account + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns any: nothing + """ + return await self.privatePostV5AccountUpgradeToUta(params) + + def create_expired_option_market(self, symbol: str): + # support expired option contracts + quote = None + settle = None + optionParts = symbol.split('-') + symbolBase = symbol.split('/') + base = None + expiry = None + if symbol.find('/') > -1: + base = self.safe_string(symbolBase, 0) + expiry = self.safe_string(optionParts, 1) + symbolQuoteAndSettle = self.safe_string(symbolBase, 1) + splitQuote = symbolQuoteAndSettle.split(':') + quoteAndSettle = self.safe_string(splitQuote, 0) + quote = quoteAndSettle + settle = quoteAndSettle + else: + base = self.safe_string(optionParts, 0) + expiry = self.convert_market_id_expire_date(self.safe_string(optionParts, 1)) + if symbol.endswith('-USDT'): + quote = 'USDT' + settle = 'USDT' + else: + quote = 'USDC' + settle = 'USDC' + strike = self.safe_string(optionParts, 2) + optionType = self.safe_string(optionParts, 3) + datetime = self.convert_expire_date(expiry) + timestamp = self.parse8601(datetime) + amountPrecision = None + pricePrecision = None + # hard coded amount and price precisions from fetchOptionMarkets + if base == 'BTC': + amountPrecision = self.parse_number('0.01') + pricePrecision = self.parse_number('5') + elif base == 'ETH': + amountPrecision = self.parse_number('0.1') + pricePrecision = self.parse_number('0.1') + elif base == 'SOL': + amountPrecision = self.parse_number('1') + pricePrecision = self.parse_number('0.01') + return { + 'id': base + '-' + self.convert_expire_date_to_market_id_date(expiry) + '-' + strike + '-' + optionType, + 'symbol': base + '/' + quote + ':' + settle + '-' + expiry + '-' + strike + '-' + optionType, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': base, + 'quoteId': quote, + 'settleId': settle, + 'active': False, + 'type': 'option', + 'linear': None, + 'inverse': None, + 'spot': False, + 'swap': False, + 'future': False, + 'option': True, + 'margin': False, + 'contract': True, + 'contractSize': self.parse_number('1'), + 'expiry': timestamp, + 'expiryDatetime': datetime, + 'optionType': 'call' if (optionType == 'C') else 'put', + 'strike': self.parse_number(strike), + 'precision': { + 'amount': amountPrecision, + 'price': pricePrecision, + }, + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'info': None, + } + + def safe_market(self, marketId: Str = None, market: Market = None, delimiter: Str = None, marketType: Str = None) -> MarketInterface: + isOption = (marketId is not None) and ((marketId.find('-C') > -1) or (marketId.find('-P') > -1)) + if isOption and not (marketId in self.markets_by_id): + # handle expired option contracts + return self.create_expired_option_market(marketId) + return super(bybit, self).safe_market(marketId, market, delimiter, marketType) + + def get_bybit_type(self, method, market, params={}): + type = None + type, params = self.handle_market_type_and_params(method, market, params) + subType = None + subType, params = self.handle_sub_type_and_params(method, market, params) + if type == 'option' or type == 'spot': + return [type, params] + return [subType, params] + + def get_amount(self, symbol: str, amount: float): + # some markets like options might not have the precision available + # and we shouldn't crash in those cases + market = self.market(symbol) + emptyPrecisionAmount = (market['precision']['amount'] is None) + amountString = self.number_to_string(amount) + if not emptyPrecisionAmount and (amountString != '0'): + return self.amount_to_precision(symbol, amount) + return amountString + + def get_price(self, symbol: str, price: str): + if price is None: + return price + market = self.market(symbol) + emptyPrecisionPrice = (market['precision']['price'] is None) + if not emptyPrecisionPrice: + return self.price_to_precision(symbol, price) + return price + + def get_cost(self, symbol: str, cost: str): + market = self.market(symbol) + emptyPrecisionPrice = (market['precision']['price'] is None) + if not emptyPrecisionPrice: + return self.cost_to_precision(symbol, cost) + return cost + + async def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://bybit-exchange.github.io/docs/v5/market/time + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = await self.publicGetV5MarketTime(params) + # + # { + # "retCode": "0", + # "retMsg": "OK", + # "result": { + # "timeSecond": "1666879482", + # "timeNano": "1666879482792685914" + # }, + # "retExtInfo": {}, + # "time": "1666879482792" + # } + # + return self.safe_integer(response, 'time') + + async def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://bybit-exchange.github.io/docs/v5/asset/coin-info + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + if not self.check_required_credentials(False): + return {} + if self.options['enableDemoTrading']: + return {} + response = await self.privateGetV5AssetCoinQueryInfo(params) + # + # { + # "retCode": 0, + # "retMsg": "", + # "result": { + # "rows": [ + # { + # "name": "BTC", + # "coin": "BTC", + # "remainAmount": "150", + # "chains": [ + # { + # "chainType": "BTC", + # "confirmation": "10000", + # "withdrawFee": "0.0005", + # "depositMin": "0.0005", + # "withdrawMin": "0.001", + # "chain": "BTC", + # "chainDeposit": "1", + # "chainWithdraw": "1", + # "minAccuracy": "8" + # } + # ] + # } + # ] + # }, + # "retExtInfo": {}, + # "time": 1672194582264 + # } + # + data = self.safe_dict(response, 'result', {}) + rows = self.safe_list(data, 'rows', []) + result: dict = {} + for i in range(0, len(rows)): + currency = rows[i] + currencyId = self.safe_string(currency, 'coin') + code = self.safe_currency_code(currencyId) + name = self.safe_string(currency, 'name') + chains = self.safe_list(currency, 'chains', []) + networks: dict = {} + for j in range(0, len(chains)): + chain = chains[j] + networkId = self.safe_string(chain, 'chain') + networkCode = self.network_id_to_code(networkId) + networks[networkCode] = { + 'info': chain, + 'id': networkId, + 'network': networkCode, + 'active': None, + 'deposit': self.safe_integer(chain, 'chainDeposit') == 1, + 'withdraw': self.safe_integer(chain, 'chainWithdraw') == 1, + 'fee': self.safe_number(chain, 'withdrawFee'), + 'precision': self.parse_number(self.parse_precision(self.safe_string(chain, 'minAccuracy'))), + 'limits': { + 'withdraw': { + 'min': self.safe_number(chain, 'withdrawMin'), + 'max': None, + }, + 'deposit': { + 'min': self.safe_number(chain, 'depositMin'), + 'max': None, + }, + }, + } + result[code] = self.safe_currency_structure({ + 'info': currency, + 'code': code, + 'id': currencyId, + 'name': name, + 'active': None, + 'deposit': None, + 'withdraw': None, + 'fee': None, + 'precision': None, + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + 'deposit': { + 'min': None, + 'max': None, + }, + }, + 'networks': networks, + 'type': 'crypto', # atm exchange api provides only cryptos + }) + return result + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for bybit + + https://bybit-exchange.github.io/docs/v5/market/instrument + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + if self.options['adjustForTimeDifference']: + await self.load_time_difference() + promisesUnresolved = [] + types = None + defaultTypes = ['spot', 'linear', 'inverse', 'option'] + fetchMarketsOptions = self.safe_dict(self.options, 'fetchMarkets') + if fetchMarketsOptions is not None: + types = self.safe_list(fetchMarketsOptions, 'types', defaultTypes) + else: + # for backward-compatibility + types = self.safe_list(self.options, 'fetchMarkets', defaultTypes) + for i in range(0, len(types)): + marketType = types[i] + if marketType == 'spot': + promisesUnresolved.append(self.fetch_spot_markets(params)) + elif marketType == 'linear': + promisesUnresolved.append(self.fetch_future_markets({'category': 'linear'})) + elif marketType == 'inverse': + promisesUnresolved.append(self.fetch_future_markets({'category': 'inverse'})) + elif marketType == 'option': + promisesUnresolved.append(self.fetch_option_markets({'baseCoin': 'BTC'})) + promisesUnresolved.append(self.fetch_option_markets({'baseCoin': 'ETH'})) + promisesUnresolved.append(self.fetch_option_markets({'baseCoin': 'SOL'})) + else: + raise ExchangeError(self.id + ' fetchMarkets() self.options fetchMarkets "' + marketType + '" is not a supported market type') + promises = await asyncio.gather(*promisesUnresolved) + spotMarkets = self.safe_list(promises, 0, []) + linearMarkets = self.safe_list(promises, 1, []) + inverseMarkets = self.safe_list(promises, 2, []) + btcOptionMarkets = self.safe_list(promises, 3, []) + ethOptionMarkets = self.safe_list(promises, 4, []) + solOptionMarkets = self.safe_list(promises, 5, []) + futureMarkets = self.array_concat(linearMarkets, inverseMarkets) + optionMarkets = self.array_concat(btcOptionMarkets, ethOptionMarkets) + optionMarkets = self.array_concat(optionMarkets, solOptionMarkets) + derivativeMarkets = self.array_concat(futureMarkets, optionMarkets) + return self.array_concat(spotMarkets, derivativeMarkets) + + async def fetch_spot_markets(self, params) -> List[Market]: + request: dict = { + 'category': 'spot', + } + usePrivateInstrumentsInfo = self.safe_bool(self.options, 'usePrivateInstrumentsInfo', False) + response: dict = None + if usePrivateInstrumentsInfo: + response = await self.privateGetV5MarketInstrumentsInfo(self.extend(request, params)) + else: + response = await self.publicGetV5MarketInstrumentsInfo(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "category": "spot", + # "list": [ + # { + # "symbol": "BTCUSDT", + # "baseCoin": "BTC", + # "quoteCoin": "USDT", + # "innovation": "0", + # "status": "Trading", + # "marginTrading": "both", + # "lotSizeFilter": { + # "basePrecision": "0.000001", + # "quotePrecision": "0.00000001", + # "minOrderQty": "0.00004", + # "maxOrderQty": "63.01197227", + # "minOrderAmt": "1", + # "maxOrderAmt": "100000" + # }, + # "priceFilter": { + # "tickSize": "0.01" + # } + # } + # ] + # }, + # "retExtInfo": {}, + # "time": 1672712468011 + # } + # + responseResult = self.safe_dict(response, 'result', {}) + markets = self.safe_list(responseResult, 'list', []) + result = [] + takerFee = self.parse_number('0.001') + makerFee = self.parse_number('0.001') + for i in range(0, len(markets)): + market = markets[i] + id = self.safe_string(market, 'symbol') + baseId = self.safe_string(market, 'baseCoin') + quoteId = self.safe_string(market, 'quoteCoin') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + symbol = base + '/' + quote + status = self.safe_string(market, 'status') + active = (status == 'Trading') + lotSizeFilter = self.safe_dict(market, 'lotSizeFilter') + priceFilter = self.safe_dict(market, 'priceFilter') + quotePrecision = self.safe_number(lotSizeFilter, 'quotePrecision') + marginTrading = self.safe_string(market, 'marginTrading', 'none') + allowsMargin = marginTrading != 'none' + result.append(self.safe_market_structure({ + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': allowsMargin, + 'swap': False, + 'future': False, + 'option': False, + 'active': active, + 'contract': False, + 'linear': None, + 'inverse': None, + 'taker': takerFee, + 'maker': makerFee, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.safe_number(lotSizeFilter, 'basePrecision'), + 'price': self.safe_number(priceFilter, 'tickSize', quotePrecision), + }, + 'limits': { + 'leverage': { + 'min': self.parse_number('1'), + 'max': None, + }, + 'amount': { + 'min': self.safe_number(lotSizeFilter, 'minOrderQty'), + 'max': self.safe_number(lotSizeFilter, 'maxOrderQty'), + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': self.safe_number(lotSizeFilter, 'minOrderAmt'), + 'max': self.safe_number(lotSizeFilter, 'maxOrderAmt'), + }, + }, + 'created': None, + 'info': market, + })) + return result + + async def fetch_future_markets(self, params) -> List[Market]: + params = self.extend(params) + params['limit'] = 1000 # minimize number of requests + preLaunchMarkets = [] + usePrivateInstrumentsInfo = self.safe_bool(self.options, 'usePrivateInstrumentsInfo', False) + response: dict = None + if usePrivateInstrumentsInfo: + response = await self.privateGetV5MarketInstrumentsInfo(params) + else: + linearPromises = [ + self.publicGetV5MarketInstrumentsInfo(params), + self.publicGetV5MarketInstrumentsInfo(self.extend(params, {'status': 'PreLaunch'})), + ] + promises = await asyncio.gather(*linearPromises) + response = self.safe_dict(promises, 0, {}) + preLaunchMarkets = self.safe_dict(promises, 1, {}) + data = self.safe_dict(response, 'result', {}) + markets = self.safe_list(data, 'list', []) + paginationCursor = self.safe_string(data, 'nextPageCursor') + if paginationCursor is not None: + while(paginationCursor is not None): + params['cursor'] = paginationCursor + responseInner: dict = None + if usePrivateInstrumentsInfo: + responseInner = await self.privateGetV5MarketInstrumentsInfo(params) + else: + responseInner = await self.publicGetV5MarketInstrumentsInfo(params) + dataNew = self.safe_dict(responseInner, 'result', {}) + rawMarkets = self.safe_list(dataNew, 'list', []) + rawMarketsLength = len(rawMarkets) + if rawMarketsLength == 0: + break + markets = self.array_concat(rawMarkets, markets) + paginationCursor = self.safe_string(dataNew, 'nextPageCursor') + # + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "category": "linear", + # "list": [ + # { + # "symbol": "BTCUSDT", + # "contractType": "LinearPerpetual", + # "status": "Trading", + # "baseCoin": "BTC", + # "quoteCoin": "USDT", + # "launchTime": "1585526400000", + # "deliveryTime": "0", + # "deliveryFeeRate": "", + # "priceScale": "2", + # "leverageFilter": { + # "minLeverage": "1", + # "maxLeverage": "100.00", + # "leverageStep": "0.01" + # }, + # "priceFilter": { + # "minPrice": "0.50", + # "maxPrice": "999999.00", + # "tickSize": "0.50" + # }, + # "lotSizeFilter": { + # "maxOrderQty": "100.000", + # "minOrderQty": "0.001", + # "qtyStep": "0.001", + # "postOnlyMaxOrderQty": "1000.000" + # }, + # "unifiedMarginTrade": True, + # "fundingInterval": 480, + # "settleCoin": "USDT" + # } + # ], + # "nextPageCursor": "" + # }, + # "retExtInfo": {}, + # "time": 1672712495660 + # } + # + preLaunchData = self.safe_dict(preLaunchMarkets, 'result', {}) + preLaunchMarketsList = self.safe_list(preLaunchData, 'list', []) + markets = self.array_concat(markets, preLaunchMarketsList) + result = [] + category = self.safe_string(data, 'category') + for i in range(0, len(markets)): + market = markets[i] + if category is None: + category = self.safe_string(market, 'category') + linear = (category == 'linear') + inverse = (category == 'inverse') + contractType = self.safe_string(market, 'contractType') + inverseFutures = (contractType == 'InverseFutures') + linearFutures = (contractType == 'LinearFutures') + linearPerpetual = (contractType == 'LinearPerpetual') + inversePerpetual = (contractType == 'InversePerpetual') + id = self.safe_string(market, 'symbol') + baseId = self.safe_string(market, 'baseCoin') + quoteId = self.safe_string(market, 'quoteCoin') + defaultSettledId = quoteId if linear else baseId + settleId = self.safe_string(market, 'settleCoin', defaultSettledId) + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + settle = None + if linearPerpetual and (settleId == 'USD'): + settle = 'USDC' + else: + settle = self.safe_currency_code(settleId) + symbol = base + '/' + quote + lotSizeFilter = self.safe_dict(market, 'lotSizeFilter', {}) + priceFilter = self.safe_dict(market, 'priceFilter', {}) + leverage = self.safe_dict(market, 'leverageFilter', {}) + status = self.safe_string(market, 'status') + swap = linearPerpetual or inversePerpetual + future = inverseFutures or linearFutures + type = None + if swap: + type = 'swap' + elif future: + type = 'future' + expiry = None + # some swaps have deliveryTime meaning delisting time + if not swap: + expiry = self.omit_zero(self.safe_string(market, 'deliveryTime')) + if expiry is not None: + expiry = int(expiry) + expiryDatetime = self.iso8601(expiry) + symbol = symbol + ':' + settle + if expiry is not None: + symbol = symbol + '-' + self.yymmdd(expiry) + contractSize = self.safe_number_2(lotSizeFilter, 'minTradingQty', 'minOrderQty') if inverse else self.parse_number('1') + result.append(self.safe_market_structure({ + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': type, + 'spot': False, + 'margin': None, + 'swap': swap, + 'future': future, + 'option': False, + 'active': (status == 'Trading'), + 'contract': True, + 'linear': linear, + 'inverse': inverse, + 'taker': self.safe_number(market, 'takerFee', self.parse_number('0.0006')), + 'maker': self.safe_number(market, 'makerFee', self.parse_number('0.0001')), + 'contractSize': contractSize, + 'expiry': expiry, + 'expiryDatetime': expiryDatetime, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.safe_number(lotSizeFilter, 'qtyStep'), + 'price': self.safe_number(priceFilter, 'tickSize'), + }, + 'limits': { + 'leverage': { + 'min': self.safe_number(leverage, 'minLeverage'), + 'max': self.safe_number(leverage, 'maxLeverage'), + }, + 'amount': { + 'min': self.safe_number_2(lotSizeFilter, 'minTradingQty', 'minOrderQty'), + 'max': self.safe_number_2(lotSizeFilter, 'maxTradingQty', 'maxOrderQty'), + }, + 'price': { + 'min': self.safe_number(priceFilter, 'minPrice'), + 'max': self.safe_number(priceFilter, 'maxPrice'), + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': self.safe_integer(market, 'launchTime'), + 'info': market, + })) + return result + + async def fetch_option_markets(self, params) -> List[Market]: + request: dict = { + 'category': 'option', + } + usePrivateInstrumentsInfo = self.safe_bool(self.options, 'usePrivateInstrumentsInfo', False) + response: dict = None + if usePrivateInstrumentsInfo: + response = await self.privateGetV5MarketInstrumentsInfo(self.extend(request, params)) + else: + response = await self.publicGetV5MarketInstrumentsInfo(self.extend(request, params)) + data = self.safe_dict(response, 'result', {}) + markets = self.safe_list(data, 'list', []) + if self.options['loadAllOptions']: + request['limit'] = 1000 + paginationCursor = self.safe_string(data, 'nextPageCursor') + if paginationCursor is not None: + while(paginationCursor is not None): + request['cursor'] = paginationCursor + responseInner: dict = None + if usePrivateInstrumentsInfo: + responseInner = await self.privateGetV5MarketInstrumentsInfo(self.extend(request, params)) + else: + responseInner = await self.publicGetV5MarketInstrumentsInfo(self.extend(request, params)) + dataNew = self.safe_dict(responseInner, 'result', {}) + rawMarkets = self.safe_list(dataNew, 'list', []) + rawMarketsLength = len(rawMarkets) + if rawMarketsLength == 0: + break + markets = self.array_concat(rawMarkets, markets) + paginationCursor = self.safe_string(dataNew, 'nextPageCursor') + # + # { + # "retCode": 0, + # "retMsg": "success", + # "result": { + # "category": "option", + # "nextPageCursor": "0%2C2", + # "list": [ + # { + # "symbol": "BTC-29DEC23-80000-C", + # "status": "Trading", + # "baseCoin": "BTC", + # "quoteCoin": "USD", + # "settleCoin": "USDC", + # "optionsType": "Call", + # "launchTime": "1688630400000", + # "deliveryTime": "1703836800000", + # "deliveryFeeRate": "0.00015", + # "priceFilter": { + # "minPrice": "5", + # "maxPrice": "10000000", + # "tickSize": "5" + # }, + # "lotSizeFilter": { + # "maxOrderQty": "500", + # "minOrderQty": "0.01", + # "qtyStep": "0.01" + # } + # }, + # ] + # }, + # "retExtInfo": {}, + # "time": 1688873094448 + # } + # + result = [] + for i in range(0, len(markets)): + market = markets[i] + id = self.safe_string(market, 'symbol') + baseId = self.safe_string(market, 'baseCoin') + quoteId = self.safe_string(market, 'quoteCoin') + settleId = self.safe_string(market, 'settleCoin') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + settle = self.safe_currency_code(settleId) + lotSizeFilter = self.safe_dict(market, 'lotSizeFilter', {}) + priceFilter = self.safe_dict(market, 'priceFilter', {}) + status = self.safe_string(market, 'status') + expiry = self.safe_integer(market, 'deliveryTime') + splitId = id.split('-') + strike = self.safe_string(splitId, 2) + optionLetter = self.safe_string(splitId, 3) + isActive = (status == 'Trading') + isInverse = base == settle + if isActive or (self.options['loadAllOptions']) or (self.options['loadExpiredOptions']): + result.append(self.safe_market_structure({ + 'id': id, + 'symbol': base + '/' + quote + ':' + settle + '-' + self.yymmdd(expiry) + '-' + strike + '-' + optionLetter, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': 'option', + 'subType': None, + 'spot': False, + 'margin': False, + 'swap': False, + 'future': False, + 'option': True, + 'active': isActive, + 'contract': True, + 'linear': not isInverse, + 'inverse': isInverse, + 'taker': self.safe_number(market, 'takerFee', self.parse_number('0.0006')), + 'maker': self.safe_number(market, 'makerFee', self.parse_number('0.0001')), + 'contractSize': self.parse_number('1'), + 'expiry': expiry, + 'expiryDatetime': self.iso8601(expiry), + 'strike': self.parse_number(strike), + 'optionType': self.safe_string_lower(market, 'optionsType'), + 'precision': { + 'amount': self.safe_number(lotSizeFilter, 'qtyStep'), + 'price': self.safe_number(priceFilter, 'tickSize'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(lotSizeFilter, 'minOrderQty'), + 'max': self.safe_number(lotSizeFilter, 'maxOrderQty'), + }, + 'price': { + 'min': self.safe_number(priceFilter, 'minPrice'), + 'max': self.safe_number(priceFilter, 'maxPrice'), + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': self.safe_integer(market, 'launchTime'), + 'info': market, + })) + return result + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # spot + # + # { + # "symbol": "BTCUSDT", + # "bid1Price": "20517.96", + # "bid1Size": "2", + # "ask1Price": "20527.77", + # "ask1Size": "1.862172", + # "lastPrice": "20533.13", + # "prevPrice24h": "20393.48", + # "price24hPcnt": "0.0068", + # "highPrice24h": "21128.12", + # "lowPrice24h": "20318.89", + # "turnover24h": "243765620.65899866", + # "volume24h": "11801.27771", + # "usdIndexPrice": "20784.12009279" + # } + # + # linear/inverse + # + # { + # "symbol": "BTCUSD", + # "lastPrice": "16597.00", + # "indexPrice": "16598.54", + # "markPrice": "16596.00", + # "prevPrice24h": "16464.50", + # "price24hPcnt": "0.008047", + # "highPrice24h": "30912.50", + # "lowPrice24h": "15700.00", + # "prevPrice1h": "16595.50", + # "openInterest": "373504107", + # "openInterestValue": "22505.67", + # "turnover24h": "2352.94950046", + # "volume24h": "49337318", + # "fundingRate": "-0.001034", + # "nextFundingTime": "1672387200000", + # "predictedDeliveryPrice": "", + # "basisRate": "", + # "deliveryFeeRate": "", + # "deliveryTime": "0", + # "ask1Size": "1", + # "bid1Price": "16596.00", + # "ask1Price": "16597.50", + # "bid1Size": "1" + # } + # + # option + # + # { + # "symbol": "BTC-30DEC22-18000-C", + # "bid1Price": "0", + # "bid1Size": "0", + # "bid1Iv": "0", + # "ask1Price": "435", + # "ask1Size": "0.66", + # "ask1Iv": "5", + # "lastPrice": "435", + # "highPrice24h": "435", + # "lowPrice24h": "165", + # "markPrice": "0.00000009", + # "indexPrice": "16600.55", + # "markIv": "0.7567", + # "underlyingPrice": "16590.42", + # "openInterest": "6.3", + # "turnover24h": "2482.73", + # "volume24h": "0.15", + # "totalVolume": "99", + # "totalTurnover": "1967653", + # "delta": "0.00000001", + # "gamma": "0.00000001", + # "vega": "0.00000004", + # "theta": "-0.00000152", + # "predictedDeliveryPrice": "0", + # "change24h": "86" + # } + # + isSpot = self.safe_string(ticker, 'openInterestValue') is None + timestamp = self.safe_integer(ticker, 'time') + marketId = self.safe_string(ticker, 'symbol') + type = 'spot' if isSpot else 'contract' + market = self.safe_market(marketId, market, None, type) + symbol = self.safe_symbol(marketId, market, None, type) + last = self.safe_string(ticker, 'lastPrice') + open = self.safe_string(ticker, 'prevPrice24h') + percentage = self.safe_string(ticker, 'price24hPcnt') + percentage = Precise.string_mul(percentage, '100') + quoteVolume = self.safe_string(ticker, 'turnover24h') + baseVolume = self.safe_string(ticker, 'volume24h') + bid = self.safe_string(ticker, 'bid1Price') + ask = self.safe_string(ticker, 'ask1Price') + high = self.safe_string(ticker, 'highPrice24h') + low = self.safe_string(ticker, 'lowPrice24h') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': high, + 'low': low, + 'bid': bid, + 'bidVolume': self.safe_string_2(ticker, 'bidSize', 'bid1Size'), + 'ask': ask, + 'askVolume': self.safe_string_2(ticker, 'askSize', 'ask1Size'), + 'vwap': None, + 'open': open, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': percentage, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'markPrice': self.safe_string(ticker, 'markPrice'), + 'indexPrice': self.safe_string(ticker, 'indexPrice'), + 'info': ticker, + }, market) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://bybit-exchange.github.io/docs/v5/market/tickers + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchTicker() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + # 'baseCoin': '', Base coin. For option only + # 'expDate': '', Expiry date. e.g., 25DEC22. For option only + } + category = None + category, params = self.get_bybit_type('fetchTicker', market, params) + request['category'] = category + response = await self.publicGetV5MarketTickers(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "category": "inverse", + # "list": [ + # { + # "symbol": "BTCUSD", + # "lastPrice": "16597.00", + # "indexPrice": "16598.54", + # "markPrice": "16596.00", + # "prevPrice24h": "16464.50", + # "price24hPcnt": "0.008047", + # "highPrice24h": "30912.50", + # "lowPrice24h": "15700.00", + # "prevPrice1h": "16595.50", + # "openInterest": "373504107", + # "openInterestValue": "22505.67", + # "turnover24h": "2352.94950046", + # "volume24h": "49337318", + # "fundingRate": "-0.001034", + # "nextFundingTime": "1672387200000", + # "predictedDeliveryPrice": "", + # "basisRate": "", + # "deliveryFeeRate": "", + # "deliveryTime": "0", + # "ask1Size": "1", + # "bid1Price": "16596.00", + # "ask1Price": "16597.50", + # "bid1Size": "1" + # } + # ] + # }, + # "retExtInfo": {}, + # "time": 1672376496682 + # } + # + result = self.safe_dict(response, 'result', {}) + tickers = self.safe_list(result, 'list', []) + rawTicker = self.safe_dict(tickers, 0) + return self.parse_ticker(rawTicker, market) + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://bybit-exchange.github.io/docs/v5/market/tickers + + :param str[] symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.subType]: *contract only* 'linear', 'inverse' + :param str [params.baseCoin]: *option only* base coin, default is 'BTC' + :returns dict: an array of `ticker structures ` + """ + await self.load_markets() + code = self.safe_string_n(params, ['code', 'currency', 'baseCoin']) + market = None + parsedSymbols = None + if symbols is not None: + parsedSymbols = [] + marketTypeInfo = self.handle_market_type_and_params('fetchTickers', None, params) + defaultType = marketTypeInfo[0] # don't omit here + # we can't use marketSymbols here due to the conflicing ids between markets + currentType = None + for i in range(0, len(symbols)): + symbol = symbols[i] + # using safeMarket here because if the user provides for instance BTCUSDT and "type": "spot" in params we should + # infer the market type from the type provided and not from the conflicting id(BTCUSDT might be swap or spot) + isExchangeSpecificSymbol = (symbol.find('/') == -1) + if isExchangeSpecificSymbol: + market = self.safe_market(symbol, None, None, defaultType) + else: + market = self.market(symbol) + if currentType is None: + currentType = market['type'] + elif market['type'] != currentType: + raise BadRequest(self.id + ' fetchTickers can only accept a list of symbols of the same type') + if market['option']: + if code is not None and code != market['base']: + raise BadRequest(self.id + ' fetchTickers the base currency must be the same for all symbols, self endpoint only supports one base currency at a time. Read more about it here: https://bybit-exchange.github.io/docs/v5/market/tickers') + if code is None: + code = market['base'] + params = self.omit(params, ['code', 'currency']) + parsedSymbols.append(market['symbol']) + request: dict = { + # 'symbol': market['id'], + # 'baseCoin': '', # Base coin. For option only + # 'expDate': '', # Expiry date. e.g., 25DEC22. For option only + } + category = None + category, params = self.get_bybit_type('fetchTickers', market, params) + request['category'] = category + if category == 'option': + request['category'] = 'option' + if code is None: + code = 'BTC' + request['baseCoin'] = code + response = await self.publicGetV5MarketTickers(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "category": "inverse", + # "list": [ + # { + # "symbol": "BTCUSD", + # "lastPrice": "16597.00", + # "indexPrice": "16598.54", + # "markPrice": "16596.00", + # "prevPrice24h": "16464.50", + # "price24hPcnt": "0.008047", + # "highPrice24h": "30912.50", + # "lowPrice24h": "15700.00", + # "prevPrice1h": "16595.50", + # "openInterest": "373504107", + # "openInterestValue": "22505.67", + # "turnover24h": "2352.94950046", + # "volume24h": "49337318", + # "fundingRate": "-0.001034", + # "nextFundingTime": "1672387200000", + # "predictedDeliveryPrice": "", + # "basisRate": "", + # "deliveryFeeRate": "", + # "deliveryTime": "0", + # "ask1Size": "1", + # "bid1Price": "16596.00", + # "ask1Price": "16597.50", + # "bid1Size": "1" + # } + # ] + # }, + # "retExtInfo": {}, + # "time": 1672376496682 + # } + # + result = self.safe_dict(response, 'result', {}) + tickerList = self.safe_list(result, 'list', []) + return self.parse_tickers(tickerList, parsedSymbols) + + async def fetch_bids_asks(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches the bid and ask price and volume for multiple markets + + https://bybit-exchange.github.io/docs/v5/market/tickers + + :param str[]|None symbols: unified symbols of the markets to fetch the bids and asks for, all markets are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.subType]: *contract only* 'linear', 'inverse' + :param str [params.baseCoin]: *option only* base coin, default is 'BTC' + :returns dict: a dictionary of `ticker structures ` + """ + return await self.fetch_tickers(symbols, params) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # [ + # "1621162800", + # "49592.43", + # "49644.91", + # "49342.37", + # "49349.42", + # "1451.59", + # "2.4343353100000003" + # ] + # + volumeIndex = 6 if (market['inverse']) else 5 + return [ + self.safe_integer(ohlcv, 0), + self.safe_number(ohlcv, 1), + self.safe_number(ohlcv, 2), + self.safe_number(ohlcv, 3), + self.safe_number(ohlcv, 4), + self.safe_number(ohlcv, volumeIndex), + ] + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://bybit-exchange.github.io/docs/v5/market/kline + https://bybit-exchange.github.io/docs/v5/market/mark-kline + https://bybit-exchange.github.io/docs/v5/market/index-kline + https://bybit-exchange.github.io/docs/v5/market/preimum-index-kline + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch orders for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOHLCV() requires a symbol argument') + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate') + if paginate: + return await self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params, 1000) + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is None: + limit = 200 # default is 200 when requested with `since` + if since is not None: + request['start'] = since + if limit is not None: + request['limit'] = limit # max 1000, default 1000 + request, params = self.handle_until_option('end', request, params) + request['interval'] = self.safe_string(self.timeframes, timeframe, timeframe) + response = None + if market['spot']: + request['category'] = 'spot' + response = await self.publicGetV5MarketKline(self.extend(request, params)) + else: + price = self.safe_string(params, 'price') + params = self.omit(params, 'price') + if market['linear']: + request['category'] = 'linear' + elif market['inverse']: + request['category'] = 'inverse' + else: + raise NotSupported(self.id + ' fetchOHLCV() is not supported for option markets') + if price == 'mark': + response = await self.publicGetV5MarketMarkPriceKline(self.extend(request, params)) + elif price == 'index': + response = await self.publicGetV5MarketIndexPriceKline(self.extend(request, params)) + elif price == 'premiumIndex': + response = await self.publicGetV5MarketPremiumIndexPriceKline(self.extend(request, params)) + else: + response = await self.publicGetV5MarketKline(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "symbol": "BTCUSD", + # "category": "inverse", + # "list": [ + # [ + # "1670608800000", + # "17071", + # "17073", + # "17027", + # "17055.5", + # "268611", + # "15.74462667" + # ], + # [ + # "1670605200000", + # "17071.5", + # "17071.5", + # "17061", + # "17071", + # "4177", + # "0.24469757" + # ], + # [ + # "1670601600000", + # "17086.5", + # "17088", + # "16978", + # "17071.5", + # "6356", + # "0.37288112" + # ] + # ] + # }, + # "retExtInfo": {}, + # "time": 1672025956592 + # } + # + result = self.safe_dict(response, 'result', {}) + ohlcvs = self.safe_list(result, 'list', []) + return self.parse_ohlcvs(ohlcvs, market, timeframe, since, limit) + + def parse_funding_rate(self, ticker, market: Market = None) -> FundingRate: + # + # { + # "symbol": "BTCUSDT", + # "bidPrice": "19255", + # "askPrice": "19255.5", + # "lastPrice": "19255.50", + # "lastTickDirection": "ZeroPlusTick", + # "prevPrice24h": "18634.50", + # "price24hPcnt": "0.033325", + # "highPrice24h": "19675.00", + # "lowPrice24h": "18610.00", + # "prevPrice1h": "19278.00", + # "markPrice": "19255.00", + # "indexPrice": "19260.68", + # "openInterest": "48069.549", + # "turnover24h": "4686694853.047006", + # "volume24h": "243730.252", + # "fundingRate": "0.0001", + # "nextFundingTime": "1663689600000", + # "predictedDeliveryPrice": "", + # "basisRate": "", + # "deliveryFeeRate": "", + # "deliveryTime": "0" + # } + # + timestamp = self.safe_integer(ticker, 'timestamp') # added artificially to avoid changing the signature + ticker = self.omit(ticker, 'timestamp') + marketId = self.safe_string(ticker, 'symbol') + symbol = self.safe_symbol(marketId, market, None, 'swap') + fundingRate = self.safe_number(ticker, 'fundingRate') + fundingTimestamp = self.safe_integer(ticker, 'nextFundingTime') + markPrice = self.safe_number(ticker, 'markPrice') + indexPrice = self.safe_number(ticker, 'indexPrice') + info = self.safe_dict(self.safe_market(marketId, market, None, 'swap'), 'info') + fundingInterval = self.safe_integer(info, 'fundingInterval') + intervalString = None + if fundingInterval is not None: + interval = self.parse_to_int(fundingInterval / 60) + intervalString = str(interval) + 'h' + return { + 'info': ticker, + 'symbol': symbol, + 'markPrice': markPrice, + 'indexPrice': indexPrice, + 'interestRate': None, + 'estimatedSettlePrice': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'fundingRate': fundingRate, + 'fundingTimestamp': fundingTimestamp, + 'fundingDatetime': self.iso8601(fundingTimestamp), + 'nextFundingRate': None, + 'nextFundingTimestamp': None, + 'nextFundingDatetime': None, + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': intervalString, + } + + async def fetch_funding_rates(self, symbols: Strings = None, params={}) -> FundingRates: + """ + fetches funding rates for multiple markets + + https://bybit-exchange.github.io/docs/v5/market/tickers + + :param str[] symbols: unified symbols of the markets to fetch the funding rates for, all market funding rates are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `funding rate structures ` + """ + await self.load_markets() + market = None + request: dict = {} + if symbols is not None: + symbols = self.market_symbols(symbols) + market = self.market(symbols[0]) + symbolsLength = len(symbols) + if symbolsLength == 1: + request['symbol'] = market['id'] + type = None + type, params = self.handle_market_type_and_params('fetchFundingRates', market, params) + if type != 'swap': + raise NotSupported(self.id + ' fetchFundingRates() does not support ' + type + ' markets') + else: + subType = None + subType, params = self.handle_sub_type_and_params('fetchFundingRates', market, params, 'linear') + request['category'] = subType + response = await self.publicGetV5MarketTickers(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "category": "linear", + # "list": [ + # { + # "symbol": "BTCUSDT", + # "bidPrice": "19255", + # "askPrice": "19255.5", + # "lastPrice": "19255.50", + # "lastTickDirection": "ZeroPlusTick", + # "prevPrice24h": "18634.50", + # "price24hPcnt": "0.033325", + # "highPrice24h": "19675.00", + # "lowPrice24h": "18610.00", + # "prevPrice1h": "19278.00", + # "markPrice": "19255.00", + # "indexPrice": "19260.68", + # "openInterest": "48069.549", + # "turnover24h": "4686694853.047006", + # "volume24h": "243730.252", + # "fundingRate": "0.0001", + # "nextFundingTime": "1663689600000", + # "predictedDeliveryPrice": "", + # "basisRate": "", + # "deliveryFeeRate": "", + # "deliveryTime": "0" + # } + # ] + # }, + # "retExtInfo": null, + # "time": 1663670053454 + # } + # + data = self.safe_dict(response, 'result', {}) + tickerList = self.safe_list(data, 'list', []) + timestamp = self.safe_integer(response, 'time') + for i in range(0, len(tickerList)): + tickerList[i]['timestamp'] = timestamp # will be removed inside the parser + return self.parse_funding_rates(tickerList, symbols) + + async def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical funding rate prices + + https://bybit-exchange.github.io/docs/v5/market/history-fund-rate + + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: timestamp in ms of the earliest funding rate to fetch + :param int [limit]: the maximum amount of `funding rate structures ` to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest funding rate + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `funding rate structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchFundingRateHistory() requires a symbol argument') + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchFundingRateHistory', 'paginate') + if paginate: + return await self.fetch_paginated_call_deterministic('fetchFundingRateHistory', symbol, since, limit, '8h', params, 200) + if limit is None: + limit = 200 + request: dict = { + # 'category': '', # Product type. linear,inverse + # 'symbol': '', # Symbol name + # 'startTime': 0, # The start timestamp(ms) + # 'endTime': 0, # The end timestamp(ms) + 'limit': limit, # Limit for data size per page. [1, 200]. Default: 200 + } + market = self.market(symbol) + symbol = market['symbol'] + request['symbol'] = market['id'] + type = None + type, params = self.get_bybit_type('fetchFundingRateHistory', market, params) + if type == 'spot' or type == 'option': + raise NotSupported(self.id + ' fetchFundingRateHistory() only support linear and inverse market') + request['category'] = type + if since is not None: + request['startTime'] = since + until = self.safe_integer(params, 'until') # unified in milliseconds + endTime = self.safe_integer(params, 'endTime', until) # exchange-specific in milliseconds + params = self.omit(params, ['endTime', 'until']) + if endTime is not None: + request['endTime'] = endTime + else: + if since is not None: + # end time is required when since is not empty + fundingInterval = 60 * 60 * 8 * 1000 + request['endTime'] = since + limit * fundingInterval + response = await self.publicGetV5MarketFundingHistory(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "category": "linear", + # "list": [ + # { + # "symbol": "ETHPERP", + # "fundingRate": "0.0001", + # "fundingRateTimestamp": "1672041600000" + # } + # ] + # }, + # "retExtInfo": {}, + # "time": 1672051897447 + # } + # + rates = [] + result = self.safe_dict(response, 'result') + resultList = self.safe_list(result, 'list') + for i in range(0, len(resultList)): + entry = resultList[i] + timestamp = self.safe_integer(entry, 'fundingRateTimestamp') + rates.append({ + 'info': entry, + 'symbol': self.safe_symbol(self.safe_string(entry, 'symbol'), None, None, 'swap'), + 'fundingRate': self.safe_number(entry, 'fundingRate'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + sorted = self.sort_by(rates, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, symbol, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # public https://bybit-exchange.github.io/docs/v5/market/recent-trade + # + # { + # "execId": "666042b4-50c6-58f3-bd9c-89b2088663ff", + # "symbol": "ETHUSD", + # "price": "1162.95", + # "size": "1", + # "side": "Sell", + # "time": "1669191277315", + # "isBlockTrade": False + # } + # + # private trades classic spot https://bybit-exchange.github.io/docs/v5/position/execution + # + # { + # "symbol": "QNTUSDT", + # "orderId": "1538686353240339712", + # "orderLinkId": "", + # "side": "Sell", + # "orderPrice": "", + # "orderQty": "", + # "leavesQty": "", + # "orderType": "Limit", + # "stopOrderType": "", + # "execFee": "0.040919", + # "execId": "2210000000097330907", + # "execPrice": "98.6", + # "execQty": "0.415", + # "execType": "", + # "execValue": "", + # "execTime": "1698161716634", + # "isMaker": True, + # "feeRate": "", + # "tradeIv": "", + # "markIv": "", + # "markPrice": "", + # "indexPrice": "", + # "underlyingPrice": "", + # "blockTradeId": "" + # } + # + # private trades unified https://bybit-exchange.github.io/docs/v5/position/execution + # + # { + # "symbol": "QNTUSDT", + # "orderType": "Limit", + # "underlyingPrice": "", + # "orderLinkId": "1549452573428424449", + # "orderId": "1549452573428424448", + # "stopOrderType": "", + # "execTime": "1699445151998", + # "feeRate": "0.00025", + # "tradeIv": "", + # "blockTradeId": "", + # "markPrice": "", + # "execPrice": "102.8", + # "markIv": "", + # "orderQty": "3.652", + # "orderPrice": "102.8", + # "execValue": "1.028", + # "closedSize": "", + # "execType": "Trade", + # "seq": "19157444346", + # "side": "Buy", + # "indexPrice": "", + # "leavesQty": "3.642", + # "isMaker": True, + # "execFee": "0.0000025", + # "execId": "2210000000101610464", + # "execQty": "0.01", + # "nextPageCursor": "267951%3A0%2C38567%3A0" + # }, + # + # private USDC settled trades + # + # { + # "symbol": "ETHPERP", + # "orderLinkId": "", + # "side": "Buy", + # "orderId": "aad0ee44-ce12-4112-aeee-b7829f6c3a26", + # "execFee": "0.0210", + # "feeRate": "0.000600", + # "blockTradeId": "", + # "tradeTime": "1669196417930", + # "execPrice": "1162.15", + # "lastLiquidityInd": "TAKER", + # "execValue": "34.8645", + # "execType": "Trade", + # "execQty": "0.030", + # "tradeId": "0e94eaf5-b08e-5505-b43f-7f1f30b1ca80" + # } + # + # watchMyTrades execution.fast + # + # { + # "category": "linear", + # "symbol": "ICPUSDT", + # "execId": "3510f361-0add-5c7b-a2e7-9679810944fc", + # "execPrice": "12.015", + # "execQty": "3000", + # "orderId": "443d63fa-b4c3-4297-b7b1-23bca88b04dc", + # "isMaker": False, + # "orderLinkId": "test-00001", + # "side": "Sell", + # "execTime": "1716800399334", + # "seq": 34771365464 + # } + # + # watchMyTrades execution + # + # { + # "category": "linear", + # "symbol": "BTCUSDT", + # "closedSize": "0", + # "execFee": "0.0679239", + # "execId": "135dbae5-cbed-5275-9290-3956bb2ed907", + # "execPrice": "123498", + # "execQty": "0.001", + # "execType": "Trade", + # "execValue": "123.498", + # "feeRate": "0.00055", + # "tradeIv": "", + # "markIv": "", + # "blockTradeId": "", + # "markPrice": "122392", + # "indexPrice": "", + # "underlyingPrice": "", + # "leavesQty": "0", + # "orderId": "aee7453a-a100-465f-857a-3db780e9329a", + # "orderLinkId": "", + # "orderPrice": "123615.9", + # "orderQty": "0.001", + # "orderType": "Market", + # "stopOrderType": "UNKNOWN", + # "side": "Buy", + # "execTime": "1757837580469", + # "isLeverage": "0", + # "isMaker": False, + # "seq": 9517074055, + # "marketUnit": "", + # "execPnl": "0", + # "createType": "CreateByUser", + # "extraFees": [], + # "feeCoin": "USDT" + # } + # + id = self.safe_string_n(trade, ['execId', 'id', 'tradeId']) + marketId = self.safe_string(trade, 'symbol') + marketType = 'contract' if ('createType' in trade) else 'spot' + category = self.safe_string(trade, 'category') + if category is not None: + marketType = 'spot' if (category == 'spot') else 'contract' + if market is not None: + marketType = market['type'] + market = self.safe_market(marketId, market, None, marketType) + symbol = market['symbol'] + amountString = self.safe_string_n(trade, ['execQty', 'orderQty', 'size']) + priceString = self.safe_string_n(trade, ['execPrice', 'orderPrice', 'price']) + costString = self.safe_string(trade, 'execValue') + timestamp = self.safe_integer_n(trade, ['time', 'execTime', 'tradeTime']) + side = self.safe_string_lower(trade, 'side') + if side is None: + isBuyer = self.safe_integer(trade, 'isBuyer') + if isBuyer is not None: + side = 'buy' if isBuyer else 'sell' + isMaker = self.safe_bool(trade, 'isMaker') + takerOrMaker = None + if isMaker is not None: + takerOrMaker = 'maker' if isMaker else 'taker' + else: + lastLiquidityInd = self.safe_string(trade, 'lastLiquidityInd') + if lastLiquidityInd == 'UNKNOWN': + lastLiquidityInd = None + if lastLiquidityInd is not None: + if (lastLiquidityInd == 'TAKER') or (lastLiquidityInd == 'MAKER'): + takerOrMaker = lastLiquidityInd.lower() + else: + takerOrMaker = 'maker' if (lastLiquidityInd == 'AddedLiquidity') else 'taker' + orderType = self.safe_string_lower(trade, 'orderType') + if orderType == 'unknown': + orderType = None + feeCostString = self.safe_string(trade, 'execFee') + fee = None + if feeCostString is not None: + feeRateString = self.safe_string(trade, 'feeRate') + feeCurrencyCode = None + if market['spot']: + if Precise.string_gt(feeCostString, '0'): + if side == 'buy': + feeCurrencyCode = market['base'] + else: + feeCurrencyCode = market['quote'] + else: + if side == 'buy': + feeCurrencyCode = market['quote'] + else: + feeCurrencyCode = market['base'] + else: + feeCurrencyCode = market['base'] if market['inverse'] else market['settle'] + fee = { + 'cost': feeCostString, + 'currency': self.safe_string(trade, 'feeCoin', feeCurrencyCode), + 'rate': feeRateString, + } + return self.safe_trade({ + 'id': id, + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'order': self.safe_string(trade, 'orderId'), + 'type': orderType, + 'side': side, + 'takerOrMaker': takerOrMaker, + 'price': priceString, + 'amount': amountString, + 'cost': costString, + 'fee': fee, + }, market) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://bybit-exchange.github.io/docs/v5/market/recent-trade + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: market type, ['swap', 'option', 'spot'] + :param str [params.subType]: market subType, ['linear', 'inverse'] + :returns Trade[]: a list of `trade structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchTrades() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + # 'baseCoin': '', # Base coin. For option only. If not passed, return BTC data by default + # 'optionType': 'Call', # Option type. Call or Put. For option only + } + if limit is not None: + # spot: [1,60], default: 60. + # others: [1,1000], default: 500 + request['limit'] = limit + type = None + type, params = self.get_bybit_type('fetchTrades', market, params) + request['category'] = type + response = await self.publicGetV5MarketRecentTrade(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "category": "spot", + # "list": [ + # { + # "execId": "2100000000007764263", + # "symbol": "BTCUSDT", + # "price": "16618.49", + # "size": "0.00012", + # "side": "Buy", + # "time": "1672052955758", + # "isBlockTrade": False + # } + # ] + # }, + # "retExtInfo": {}, + # "time": 1672053054358 + # } + # + result = self.safe_dict(response, 'result', {}) + trades = self.safe_list(result, 'list', []) + return self.parse_trades(trades, market, since, limit) + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://bybit-exchange.github.io/docs/v5/market/orderbook + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrderBook() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + defaultLimit = 25 + if market['spot']: + # limit: [1, 50]. Default: 1 + defaultLimit = 50 + request['category'] = 'spot' + else: + if market['option']: + # limit: [1, 25]. Default: 1 + request['category'] = 'option' + elif market['linear']: + # limit: [1, 500]. Default: 25 + request['category'] = 'linear' + elif market['inverse']: + # limit: [1, 500]. Default: 25 + request['category'] = 'inverse' + request['limit'] = limit if (limit is not None) else defaultLimit + response = await self.publicGetV5MarketOrderbook(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "s": "BTCUSDT", + # "a": [ + # [ + # "16638.64", + # "0.008479" + # ] + # ], + # "b": [ + # [ + # "16638.27", + # "0.305749" + # ] + # ], + # "ts": 1672765737733, + # "u": 5277055 + # }, + # "retExtInfo": {}, + # "time": 1672765737734 + # } + # + result = self.safe_dict(response, 'result', {}) + timestamp = self.safe_integer(result, 'ts') + return self.parse_order_book(result, symbol, timestamp, 'b', 'a') + + def parse_balance(self, response) -> Balances: + # + # cross + # { + # "retCode": 0, + # "retMsg": "success", + # "result": { + # "acctBalanceSum": "0.122995614474732872", + # "debtBalanceSum": "0.011734191124529754", + # "loanAccountList": [ + # { + # "free": "0.001143855", + # "interest": "0", + # "loan": "0", + # "locked": "0", + # "tokenId": "BTC", + # "total": "0.001143855" + # }, + # { + # "free": "200.00005568", + # "interest": "0.0008391", + # "loan": "200", + # "locked": "0", + # "tokenId": "USDT", + # "total": "200.00005568" + # }, + # ], + # "riskRate": "0.0954", + # "status": 1 + # }, + # "retExtInfo": {}, + # "time": 1669843584123 + # } + # + # funding + # { + # "retCode": 0, + # "retMsg": "success", + # "result": { + # "memberId": "533285", + # "accountType": "FUND", + # "balance": [ + # { + # "coin": "USDT", + # "transferBalance": "1010", + # "walletBalance": "1010", + # "bonus": "" + # }, + # { + # "coin": "USDC", + # "transferBalance": "0", + # "walletBalance": "0", + # "bonus": "" + # } + # ] + # }, + # "retExtInfo": {}, + # "time": 1675865290069 + # } + # + # spot & swap + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "list": [ + # { + # "totalEquity": "18070.32797922", + # "accountIMRate": "0.0101", + # "totalMarginBalance": "18070.32797922", + # "totalInitialMargin": "182.60183684", + # "accountType": "UNIFIED", + # "totalAvailableBalance": "17887.72614237", + # "accountMMRate": "0", + # "totalPerpUPL": "-0.11001349", + # "totalWalletBalance": "18070.43799271", + # "accountLTV": "0.017", + # "totalMaintenanceMargin": "0.38106773", + # "coin": [ + # { + # "availableToBorrow": "2.5", + # "bonus": "0", + # "accruedInterest": "0", + # "availableToWithdraw": "0.805994", + # "totalOrderIM": "0", + # "equity": "0.805994", + # "totalPositionMM": "0", + # "usdValue": "12920.95352538", + # "unrealisedPnl": "0", + # "borrowAmount": "0", + # "totalPositionIM": "0", + # "walletBalance": "0.805994", + # "cumRealisedPnl": "0", + # "coin": "BTC" + # } + # ] + # } + # ] + # }, + # "retExtInfo": {}, + # "time": 1672125441042 + # } + # + timestamp = self.safe_integer(response, 'time') + result: dict = { + 'info': response, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + responseResult = self.safe_dict(response, 'result', {}) + currencyList = self.safe_list_n(responseResult, ['loanAccountList', 'list', 'balance']) + if currencyList is None: + # usdc wallet + code = 'USDC' + account = self.account() + account['free'] = self.safe_string(responseResult, 'availableBalance') + account['total'] = self.safe_string(responseResult, 'walletBalance') + result[code] = account + else: + for i in range(0, len(currencyList)): + entry = currencyList[i] + accountType = self.safe_string(entry, 'accountType') + if accountType == 'UNIFIED' or accountType == 'CONTRACT' or accountType == 'SPOT': + coins = self.safe_list(entry, 'coin') + for j in range(0, len(coins)): + account = self.account() + coinEntry = coins[j] + loan = self.safe_string(coinEntry, 'borrowAmount') + interest = self.safe_string(coinEntry, 'accruedInterest') + if (loan is not None) and (interest is not None): + account['debt'] = Precise.string_add(loan, interest) + account['total'] = self.safe_string(coinEntry, 'walletBalance') + free = self.safe_string_2(coinEntry, 'availableToWithdraw', 'free') + if free is not None: + account['free'] = free + else: + locked = self.safe_string(coinEntry, 'locked', '0') + totalPositionIm = self.safe_string(coinEntry, 'totalPositionIM', '0') + totalOrderIm = self.safe_string(coinEntry, 'totalOrderIM', '0') + totalUsed = Precise.string_add(locked, totalPositionIm) + totalUsed = Precise.string_add(totalUsed, totalOrderIm) + account['used'] = totalUsed + # account['used'] = self.safe_string(coinEntry, 'locked') + currencyId = self.safe_string(coinEntry, 'coin') + code = self.safe_currency_code(currencyId) + result[code] = account + else: + account = self.account() + loan = self.safe_string(entry, 'loan') + interest = self.safe_string(entry, 'interest') + if (loan is not None) and (interest is not None): + account['debt'] = Precise.string_add(loan, interest) + account['total'] = self.safe_string_2(entry, 'total', 'walletBalance') + account['free'] = self.safe_string_n(entry, ['free', 'availableBalanceWithoutConvert', 'availableBalance', 'transferBalance']) + account['used'] = self.safe_string(entry, 'locked') + currencyId = self.safe_string_n(entry, ['tokenId', 'coin', 'currencyCoin']) + code = self.safe_currency_code(currencyId) + result[code] = account + return self.safe_balance(result) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://bybit-exchange.github.io/docs/v5/spot-margin-normal/account-info + https://bybit-exchange.github.io/docs/v5/asset/all-balance + https://bybit-exchange.github.io/docs/v5/account/wallet-balance + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: wallet type, ['spot', 'swap', 'funding'] + :returns dict: a `balance structure ` + """ + await self.load_markets() + request: dict = {} + enableUnifiedMargin, enableUnifiedAccount = await self.is_unified_enabled() + isUnifiedAccount = (enableUnifiedMargin or enableUnifiedAccount) + type = None + # don't use getBybitType here + type, params = self.handle_market_type_and_params('fetchBalance', None, params) + subType = None + subType, params = self.handle_sub_type_and_params('fetchBalance', None, params) + if (type == 'swap') or (type == 'future'): + type = subType + lowercaseRawType = type.lower() if (type is not None) else None + isSpot = (type == 'spot') + isLinear = (type == 'linear') + isInverse = (type == 'inverse') + isFunding = (lowercaseRawType == 'fund') or (lowercaseRawType == 'funding') + if isUnifiedAccount: + unifiedMarginStatus = self.safe_integer(self.options, 'unifiedMarginStatus', 6) + if unifiedMarginStatus < 5: + # it's not uta.20 where inverse are unified + if isInverse: + type = 'contract' + else: + type = 'unified' + else: + type = 'unified' # uta.20 where inverse are unified + else: + if isLinear or isInverse: + type = 'contract' + accountTypes = self.safe_dict(self.options, 'accountsByType', {}) + unifiedType = self.safe_string_upper(accountTypes, type, type) + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchBalance', params) + response = None + if isSpot and (marginMode is not None): + response = await self.privateGetV5SpotCrossMarginTradeAccount(self.extend(request, params)) + elif isFunding: + # use self endpoint only we have no other choice + # because it requires transfer permission + request['accountType'] = 'FUND' + response = await self.privateGetV5AssetTransferQueryAccountCoinsBalance(self.extend(request, params)) + else: + request['accountType'] = unifiedType + response = await self.privateGetV5AccountWalletBalance(self.extend(request, params)) + # + # cross + # { + # "retCode": 0, + # "retMsg": "success", + # "result": { + # "acctBalanceSum": "0.122995614474732872", + # "debtBalanceSum": "0.011734191124529754", + # "loanAccountList": [ + # { + # "free": "0.001143855", + # "interest": "0", + # "loan": "0", + # "locked": "0", + # "tokenId": "BTC", + # "total": "0.001143855" + # }, + # { + # "free": "200.00005568", + # "interest": "0.0008391", + # "loan": "200", + # "locked": "0", + # "tokenId": "USDT", + # "total": "200.00005568" + # }, + # ], + # "riskRate": "0.0954", + # "status": 1 + # }, + # "retExtInfo": {}, + # "time": 1669843584123 + # } + # + # funding + # { + # "retCode": 0, + # "retMsg": "success", + # "result": { + # "memberId": "533285", + # "accountType": "FUND", + # "balance": [ + # { + # "coin": "USDT", + # "transferBalance": "1010", + # "walletBalance": "1010", + # "bonus": "" + # }, + # { + # "coin": "USDC", + # "transferBalance": "0", + # "walletBalance": "0", + # "bonus": "" + # } + # ] + # }, + # "retExtInfo": {}, + # "time": 1675865290069 + # } + # + # spot & swap + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "list": [ + # { + # "totalEquity": "18070.32797922", + # "accountIMRate": "0.0101", + # "totalMarginBalance": "18070.32797922", + # "totalInitialMargin": "182.60183684", + # "accountType": "UNIFIED", + # "totalAvailableBalance": "17887.72614237", + # "accountMMRate": "0", + # "totalPerpUPL": "-0.11001349", + # "totalWalletBalance": "18070.43799271", + # "accountLTV": "0.017", + # "totalMaintenanceMargin": "0.38106773", + # "coin": [ + # { + # "availableToBorrow": "2.5", + # "bonus": "0", + # "accruedInterest": "0", + # "availableToWithdraw": "0.805994", + # "totalOrderIM": "0", + # "equity": "0.805994", + # "totalPositionMM": "0", + # "usdValue": "12920.95352538", + # "unrealisedPnl": "0", + # "borrowAmount": "0", + # "totalPositionIM": "0", + # "walletBalance": "0.805994", + # "cumRealisedPnl": "0", + # "coin": "BTC" + # } + # ] + # } + # ] + # }, + # "retExtInfo": {}, + # "time": 1672125441042 + # } + # + return self.parse_balance(response) + + def parse_order_status(self, status: Str): + statuses: dict = { + # v3 spot + 'NEW': 'open', + 'PARTIALLY_FILLED': 'open', + 'FILLED': 'closed', + 'CANCELED': 'canceled', + 'PENDING_CANCEL': 'open', + 'PENDING_NEW': 'open', + 'REJECTED': 'rejected', + 'PARTIALLY_FILLED_CANCELLED': 'closed', # context: https://github.com/ccxt/ccxt/issues/18685 + # v3 contract / unified margin / unified account + 'Created': 'open', + 'New': 'open', + 'Rejected': 'rejected', # order is triggered but failed upon being placed + 'PartiallyFilled': 'open', + 'PartiallyFilledCanceled': 'closed', # context: https://github.com/ccxt/ccxt/issues/18685 + 'Filled': 'closed', + 'PendingCancel': 'open', + 'Cancelled': 'canceled', + # below self line the status only pertains to conditional orders + 'Untriggered': 'open', + 'Deactivated': 'canceled', + 'Triggered': 'open', + 'Active': 'open', + } + return self.safe_string(statuses, status, status) + + def parse_time_in_force(self, timeInForce: Str): + timeInForces: dict = { + 'GoodTillCancel': 'GTC', + 'ImmediateOrCancel': 'IOC', + 'FillOrKill': 'FOK', + 'PostOnly': 'PO', + } + return self.safe_string(timeInForces, timeInForce, timeInForce) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # v1 for usdc normal account + # { + # "symbol": "BTCPERP", + # "orderType": "Market", + # "orderLinkId": "", + # "orderId": "36190ad3-de08-4b83-9ad3-56942f684b79", + # "cancelType": "UNKNOWN", + # "stopOrderType": "UNKNOWN", + # "orderStatus": "Filled", + # "updateTimeStamp": "1692769133267", + # "takeProfit": "0.0000", + # "cumExecValue": "259.6830", + # "createdAt": "1692769133261", + # "blockTradeId": "", + # "orderPnl": "", + # "price": "24674.7", + # "tpTriggerBy": "UNKNOWN", + # "timeInForce": "ImmediateOrCancel", + # "updatedAt": "1692769133267", + # "basePrice": "0.0", + # "realisedPnl": "0.0000", + # "side": "Sell", + # "triggerPrice": "0.0", + # "cumExecFee": "0.1429", + # "leavesQty": "0.000", + # "cashFlow": "", + # "slTriggerBy": "UNKNOWN", + # "iv": "", + # "closeOnTrigger": "UNKNOWN", + # "cumExecQty": "0.010", + # "reduceOnly": 0, + # "qty": "0.010", + # "stopLoss": "0.0000", + # "triggerBy": "UNKNOWN", + # "orderIM": "" + # } + # + # v5 + # { + # "symbol": "BTCUSDT", + # "orderType": "Market", + # "orderLinkId": "", + # "slLimitPrice": "0", + # "orderId": "f5f2d355-9a11-4af3-9b83-aa1d6ab6ddfe", + # "cancelType": "UNKNOWN", + # "avgPrice": "122529.9", + # "stopOrderType": "", + # "lastPriceOnCreated": "123747.9", + # "orderStatus": "Filled", + # "createType": "CreateByUser", + # "takeProfit": "", + # "cumExecValue": "122.5299", + # "tpslMode": "", + # "smpType": "None", + # "triggerDirection": 0, + # "blockTradeId": "", + # "cumFeeDetail": { + # "USDT": "0.06739145" + # }, + # "rejectReason": "EC_NoError", + # "isLeverage": "", + # "price": "120518", + # "orderIv": "", + # "createdTime": "1757837618905", + # "tpTriggerBy": "", + # "positionIdx": 0, + # "timeInForce": "IOC", + # "leavesValue": "0", + # "updatedTime": "1757837618909", + # "side": "Sell", + # "smpGroup": 0, + # "triggerPrice": "", + # "tpLimitPrice": "0", + # "cumExecFee": "0.06739145", + # "slTriggerBy": "", + # "leavesQty": "0", + # "closeOnTrigger": False, + # "slippageToleranceType": "UNKNOWN", + # "placeType": "", + # "cumExecQty": "0.001", + # "reduceOnly": True, + # "qty": "0.001", + # "stopLoss": "", + # "smpOrderId": "", + # "slippageTolerance": "0", + # "triggerBy": "", + # "extraFees": "" + # } + # + # createOrders failed order + # { + # "category": "linear", + # "symbol": "LTCUSDT", + # "orderId": '', + # "orderLinkId": '', + # "createAt": '', + # "code": "10001", + # "msg": "The number of contracts exceeds maximum limit allowed: too large" + # } + # + code = self.safe_string(order, 'code') + if code is not None: + if code != '0': + category = self.safe_string(order, 'category') + inferredMarketType = 'spot' if (category == 'spot') else 'contract' + return self.safe_order({ + 'info': order, + 'status': 'rejected', + 'id': self.safe_string(order, 'orderId'), + 'clientOrderId': self.safe_string(order, 'orderLinkId'), + 'symbol': self.safe_symbol(self.safe_string(order, 'symbol'), None, None, inferredMarketType), + }) + marketId = self.safe_string(order, 'symbol') + isContract = ('tpslMode' in order) + marketType = None + if market is not None: + marketType = market['type'] + else: + marketType = 'contract' if isContract else 'spot' + market = self.safe_market(marketId, market, None, marketType) + symbol = market['symbol'] + timestamp = self.safe_integer_2(order, 'createdTime', 'createdAt') + marketUnit = self.safe_string(order, 'marketUnit', 'baseCoin') + id = self.safe_string(order, 'orderId') + type = self.safe_string_lower(order, 'orderType') + price = self.safe_string(order, 'price') + amount: Str = None + cost: Str = None + if marketUnit == 'baseCoin': + amount = self.safe_string(order, 'qty') + cost = self.safe_string(order, 'cumExecValue') + else: + cost = self.safe_string(order, 'cumExecValue') + filled = self.safe_string(order, 'cumExecQty') + remaining = self.safe_string(order, 'leavesQty') + lastTradeTimestamp = self.safe_integer_2(order, 'updatedTime', 'updatedAt') + rawStatus = self.safe_string(order, 'orderStatus') + status = self.parse_order_status(rawStatus) + side = self.safe_string_lower(order, 'side') + fee = None + cumFeeDetail = self.safe_dict(order, 'cumFeeDetail', {}) + feeCoins = list(cumFeeDetail.keys()) + feeCoinId = self.safe_string(feeCoins, 0) + if feeCoinId is not None: + fee = { + 'cost': self.safe_number(cumFeeDetail, feeCoinId), + 'currency': feeCoinId, + } + clientOrderId = self.safe_string(order, 'orderLinkId') + if (clientOrderId is not None) and (len(clientOrderId) < 1): + clientOrderId = None + avgPrice = self.omit_zero(self.safe_string(order, 'avgPrice')) + rawTimeInForce = self.safe_string(order, 'timeInForce') + timeInForce = self.parse_time_in_force(rawTimeInForce) + triggerPrice = self.omit_zero(self.safe_string(order, 'triggerPrice')) + reduceOnly = self.safe_bool(order, 'reduceOnly') + takeProfitPrice = self.omit_zero(self.safe_string(order, 'takeProfit')) + stopLossPrice = self.omit_zero(self.safe_string(order, 'stopLoss')) + triggerDirection = self.safe_string(order, 'triggerDirection') + isAscending = (triggerDirection == '1') + isStopOrderType2 = (triggerPrice is not None) and reduceOnly + if (stopLossPrice is None) and isStopOrderType2: + # check if order is stop order type 2 - stopLossPrice + if isAscending and (side == 'buy'): + # stopLoss order against short position + stopLossPrice = triggerPrice + if not isAscending and (side == 'sell'): + # stopLoss order against a long position + stopLossPrice = triggerPrice + if (takeProfitPrice is None) and isStopOrderType2: + # check if order is stop order type 2 - takeProfitPrice + if isAscending and (side == 'sell'): + # takeprofit order against a long position + takeProfitPrice = triggerPrice + if not isAscending and (side == 'buy'): + # takeprofit order against a short position + takeProfitPrice = triggerPrice + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': lastTradeTimestamp, + 'lastUpdateTimestamp': lastTradeTimestamp, + 'symbol': symbol, + 'type': type, + 'timeInForce': timeInForce, + 'postOnly': None, + 'reduceOnly': self.safe_bool(order, 'reduceOnly'), + 'side': side, + 'price': price, + 'triggerPrice': triggerPrice, + 'takeProfitPrice': takeProfitPrice, + 'stopLossPrice': stopLossPrice, + 'amount': amount, + 'cost': cost, + 'average': avgPrice, + 'filled': filled, + 'remaining': remaining, + 'status': status, + 'fee': fee, + 'trades': None, + }, market) + + async def create_market_buy_order_with_cost(self, symbol: str, cost: float, params={}) -> Order: + """ + create a market buy order by providing the symbol and cost + + https://bybit-exchange.github.io/docs/v5/order/create-order + + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' createMarketBuyOrderWithCost() supports spot orders only') + req = { + 'cost': cost, + } + return await self.create_order(symbol, 'market', 'buy', -1, None, self.extend(req, params)) + + async def create_market_sell_order_with_cost(self, symbol: str, cost: float, params={}) -> Order: + """ + create a market sell order by providing the symbol and cost + + https://bybit-exchange.github.io/docs/v5/order/create-order + + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + types = await self.is_unified_enabled() + enableUnifiedAccount = types[1] + if not enableUnifiedAccount: + raise NotSupported(self.id + ' createMarketSellOrderWithCost() supports UTA accounts only') + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' createMarketSellOrderWithCost() supports spot orders only') + req = { + 'cost': cost, + } + return await self.create_order(symbol, 'market', 'sell', -1, None, self.extend(req, params)) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}) -> Order: + """ + create a trade order + + https://bybit-exchange.github.io/docs/v5/order/create-order + https://bybit-exchange.github.io/docs/v5/position/trading-stop + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.timeInForce]: "GTC", "IOC", "FOK" + :param bool [params.postOnly]: True or False whether the order is post-only + :param bool [params.reduceOnly]: True or False whether the order is reduce-only + :param str [params.positionIdx]: *contracts only* 0 for one-way mode, 1 buy side of hedged mode, 2 sell side of hedged mode + :param bool [params.hedged]: *contracts only* True for hedged mode, False for one way mode, default is False + :param int [params.isLeverage]: *unified spot only* False then spot trading True then margin trading + :param str [params.tpslMode]: *contract only* 'Full' or 'Partial' + :param str [params.mmp]: *option only* market maker protection + :param str [params.triggerDirection]: *contract only* the direction for trigger orders, 'ascending' or 'descending' + :param float [params.triggerPrice]: The price at which a trigger order is triggered at + :param float [params.stopLossPrice]: The price at which a stop loss order is triggered at + :param float [params.takeProfitPrice]: The price at which a take profit order is triggered at + :param dict [params.takeProfit]: *takeProfit object in params* containing the triggerPrice at which the attached take profit order will be triggered + :param float [params.takeProfit.triggerPrice]: take profit trigger price + :param dict [params.stopLoss]: *stopLoss object in params* containing the triggerPrice at which the attached stop loss order will be triggered + :param float [params.stopLoss.triggerPrice]: stop loss trigger price + :param str [params.trailingAmount]: the quote amount to trail away from the current market price + :param str [params.trailingTriggerPrice]: the price to trigger a trailing order, default uses the price argument + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + parts = await self.is_unified_enabled() + enableUnifiedAccount = parts[1] + trailingAmount = self.safe_string_2(params, 'trailingAmount', 'trailingStop') + stopLossPrice = self.safe_string(params, 'stopLossPrice') + takeProfitPrice = self.safe_string(params, 'takeProfitPrice') + isTrailingAmountOrder = trailingAmount is not None + isStopLoss = stopLossPrice is not None + isTakeProfit = takeProfitPrice is not None + orderRequest = self.create_order_request(symbol, type, side, amount, price, params, enableUnifiedAccount) + defaultMethod = None + if (isTrailingAmountOrder or isStopLoss or isTakeProfit) and not market['spot']: + defaultMethod = 'privatePostV5PositionTradingStop' + else: + defaultMethod = 'privatePostV5OrderCreate' + method = None + method, params = self.handle_option_and_params(params, 'createOrder', 'method', defaultMethod) + response = None + if method == 'privatePostV5PositionTradingStop': + response = await self.privatePostV5PositionTradingStop(orderRequest) + else: + response = await self.privatePostV5OrderCreate(orderRequest) # already extended inside createOrderRequest + # + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "orderId": "1321003749386327552", + # "orderLinkId": "spot-test-postonly" + # }, + # "retExtInfo": {}, + # "time": 1672211918471 + # } + # + order = self.safe_dict(response, 'result', {}) + return self.parse_order(order, market) + + def create_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}, isUTA=True): + market = self.market(symbol) + symbol = market['symbol'] + lowerCaseType = type.lower() + if (price is None) and (lowerCaseType == 'limit'): + raise ArgumentsRequired(self.id + ' createOrder requires a price argument for limit orders') + request: dict = { + 'symbol': market['id'], + # 'side': self.capitalize(side), + # 'orderType': self.capitalize(lowerCaseType), # limit or market + # 'timeInForce': 'GTC', # IOC, FOK, PostOnly + # 'takeProfit': 123.45, # take profit price, only take effect upon opening the position + # 'stopLoss': 123.45, # stop loss price, only take effect upon opening the position + # 'reduceOnly': False, # reduce only, required for linear orders + # when creating a closing order, bybit recommends a True value for + # closeOnTrigger to avoid failing due to insufficient available margin + # 'closeOnTrigger': False, required for linear orders + # 'orderLinkId': 'string', # unique client order id, max 36 characters + # 'triggerPrice': 123.46, # trigger price, required for conditional orders + # 'triggerBy': 'MarkPrice', # IndexPrice, MarkPrice, LastPrice + # 'tpTriggerby': 'MarkPrice', # IndexPrice, MarkPrice, LastPrice + # 'slTriggerBy': 'MarkPrice', # IndexPrice, MarkPrice, LastPrice + # 'mmp': False # market maker protection + # 'positionIdx': 0, # Position mode. Unified account has one-way mode only(0) + # 'triggerDirection': 1, # Conditional order param. Used to identify the expected direction of the conditional order. 1: triggered when market price rises to triggerPrice 2: triggered when market price falls to triggerPrice + # Valid for spot only. + # 'isLeverage': 0, # Whether to borrow. 0(default): False, 1: True + # 'orderFilter': 'Order' # Order,tpslOrder. If not passed, Order by default + # Valid for option only. + # 'orderIv': '0', # Implied volatility; parameters are passed according to the real value; for example, for 10%, 0.1 is passed + } + hedged = self.safe_bool(params, 'hedged', False) + reduceOnly = self.safe_bool(params, 'reduceOnly') + triggerPrice = self.safe_value_2(params, 'triggerPrice', 'stopPrice') + stopLossTriggerPrice = self.safe_value(params, 'stopLossPrice') + takeProfitTriggerPrice = self.safe_value(params, 'takeProfitPrice') + stopLoss = self.safe_value(params, 'stopLoss') + takeProfit = self.safe_value(params, 'takeProfit') + trailingTriggerPrice = self.safe_string_2(params, 'trailingTriggerPrice', 'activePrice', self.number_to_string(price)) + trailingAmount = self.safe_string_2(params, 'trailingAmount', 'trailingStop') + isTrailingAmountOrder = trailingAmount is not None + isTriggerOrder = triggerPrice is not None + isStopLossTriggerOrder = stopLossTriggerPrice is not None + isTakeProfitTriggerOrder = takeProfitTriggerPrice is not None + isStopLoss = stopLoss is not None + isTakeProfit = takeProfit is not None + isMarket = lowerCaseType == 'market' + isLimit = lowerCaseType == 'limit' + isBuy = side == 'buy' + defaultMethod = None + if (isTrailingAmountOrder or isStopLossTriggerOrder or isTakeProfitTriggerOrder) and not market['spot']: + defaultMethod = 'privatePostV5PositionTradingStop' + else: + defaultMethod = 'privatePostV5OrderCreate' + method = None + method, params = self.handle_option_and_params(params, 'createOrder', 'method', defaultMethod) + isAlternativeEndpoint = method == 'privatePostV5PositionTradingStop' + amountString = self.get_amount(symbol, amount) + priceString = self.get_price(symbol, self.number_to_string(price)) if (price is not None) else None + if isTrailingAmountOrder or isAlternativeEndpoint: + if isStopLoss or isTakeProfit or isTriggerOrder or market['spot']: + raise InvalidOrder(self.id + ' the API endpoint used only supports contract trailingAmount, stopLossPrice and takeProfitPrice orders') + if isStopLossTriggerOrder or isTakeProfitTriggerOrder: + tpslMode = self.safe_string(params, 'tpslMode', 'Partial') + isFullTpsl = tpslMode == 'Full' + isPartialTpsl = tpslMode == 'Partial' + if isLimit and isFullTpsl: + raise InvalidOrder(self.id + ' tpsl orders with "full" tpslMode only support "market" type') + request['tpslMode'] = tpslMode + if isStopLossTriggerOrder: + request['stopLoss'] = self.get_price(symbol, stopLossTriggerPrice) + if isPartialTpsl: + request['slSize'] = amountString + if isLimit: + request['slOrderType'] = 'Limit' + request['slLimitPrice'] = priceString + elif isTakeProfitTriggerOrder: + request['takeProfit'] = self.get_price(symbol, takeProfitTriggerPrice) + if isPartialTpsl: + request['tpSize'] = amountString + if isLimit: + request['tpOrderType'] = 'Limit' + request['tpLimitPrice'] = priceString + else: + request['side'] = self.capitalize(side) + request['orderType'] = self.capitalize(lowerCaseType) + timeInForce = self.safe_string_lower(params, 'timeInForce') # self is same specific param + postOnly = None + postOnly, params = self.handle_post_only(isMarket, timeInForce == 'postonly', params) + if postOnly: + request['timeInForce'] = 'PostOnly' + elif timeInForce == 'gtc': + request['timeInForce'] = 'GTC' + elif timeInForce == 'fok': + request['timeInForce'] = 'FOK' + elif timeInForce == 'ioc': + request['timeInForce'] = 'IOC' + if market['spot']: + # only works for spot market + if triggerPrice is not None: + request['orderFilter'] = 'StopOrder' + elif isStopLossTriggerOrder or isTakeProfitTriggerOrder: + request['orderFilter'] = 'tpslOrder' + clientOrderId = self.safe_string(params, 'clientOrderId') + if clientOrderId is not None: + request['orderLinkId'] = clientOrderId + elif market['option']: + # mandatory field for options + request['orderLinkId'] = self.uuid16() + if isLimit: + request['price'] = priceString + category = None + category, params = self.get_bybit_type('createOrderRequest', market, params) + request['category'] = category + cost = self.safe_string(params, 'cost') + params = self.omit(params, 'cost') + # if the cost is inferable, let's keep the old logic and ignore marketUnit, to minimize the impact of the changes + isMarketBuyAndCostInferable = (lowerCaseType == 'market') and (side == 'buy') and ((price is not None) or (cost is not None)) + isMarketOrder = lowerCaseType == 'market' + if market['spot'] and isMarketOrder and isUTA and not isMarketBuyAndCostInferable: + # UTA account can specify the cost of the order on both sides + if (cost is not None) or (price is not None): + request['marketUnit'] = 'quoteCoin' + orderCost = None + if cost is not None: + orderCost = cost + else: + quoteAmount = Precise.string_mul(amountString, priceString) + orderCost = quoteAmount + request['qty'] = self.get_cost(symbol, orderCost) + else: + request['marketUnit'] = 'baseCoin' + request['qty'] = amountString + elif market['spot'] and isMarketOrder and (side == 'buy'): + # classic accounts + # for market buy it requires the amount of quote currency to spend + createMarketBuyOrderRequiresPrice = True + createMarketBuyOrderRequiresPrice, params = self.handle_option_and_params(params, 'createOrder', 'createMarketBuyOrderRequiresPrice') + if createMarketBuyOrderRequiresPrice: + if (price is None) and (cost is None): + raise InvalidOrder(self.id + ' createOrder() requires the price argument for market buy orders to calculate the total cost to spend(amount * price), alternatively set the createMarketBuyOrderRequiresPrice option or param to False and pass the cost to spend in the amount argument') + else: + quoteAmount = Precise.string_mul(self.number_to_string(amount), priceString) + costRequest = cost if (cost is not None) else quoteAmount + request['qty'] = self.get_cost(symbol, costRequest) + else: + if cost is not None: + request['qty'] = self.get_cost(symbol, self.number_to_string(cost)) + elif price is not None: + request['qty'] = self.get_cost(symbol, Precise.string_mul(amountString, priceString)) + else: + request['qty'] = amountString + else: + if not isTrailingAmountOrder and not isAlternativeEndpoint: + request['qty'] = amountString + if isTrailingAmountOrder: + if trailingTriggerPrice is not None: + request['activePrice'] = self.get_price(symbol, trailingTriggerPrice) + request['trailingStop'] = trailingAmount + elif isTriggerOrder and not isAlternativeEndpoint: + triggerDirection = self.safe_string(params, 'triggerDirection') + params = self.omit(params, ['triggerPrice', 'stopPrice', 'triggerDirection']) + if market['spot']: + if triggerDirection is not None: + raise NotSupported(self.id + ' createOrder() : trigger order does not support triggerDirection for spot markets yet') + else: + if triggerDirection is None: + raise ArgumentsRequired(self.id + ' stop/trigger orders require a triggerDirection parameter, either "ascending" or "descending" to determine the direction of the trigger.') + isAsending = ((triggerDirection == 'ascending') or (triggerDirection == 'above') or (triggerDirection == '1')) + request['triggerDirection'] = 1 if isAsending else 2 + request['triggerPrice'] = self.get_price(symbol, triggerPrice) + elif (isStopLossTriggerOrder or isTakeProfitTriggerOrder) and not isAlternativeEndpoint: + if isBuy: + request['triggerDirection'] = 1 if isStopLossTriggerOrder else 2 + else: + request['triggerDirection'] = 2 if isStopLossTriggerOrder else 1 + triggerPrice = stopLossTriggerPrice if isStopLossTriggerOrder else takeProfitTriggerPrice + request['triggerPrice'] = self.get_price(symbol, triggerPrice) + request['reduceOnly'] = True + if (isStopLoss or isTakeProfit) and not isAlternativeEndpoint: + if isStopLoss: + slTriggerPrice = self.safe_value_2(stopLoss, 'triggerPrice', 'stopPrice', stopLoss) + request['stopLoss'] = self.get_price(symbol, slTriggerPrice) + slLimitPrice = self.safe_value(stopLoss, 'price') + if slLimitPrice is not None: + request['tpslMode'] = 'Partial' + request['slOrderType'] = 'Limit' + request['slLimitPrice'] = self.get_price(symbol, slLimitPrice) + else: + # for spot market, we need to add self + if market['spot']: + request['slOrderType'] = 'Market' + # for spot market, we need to add self + if market['spot'] and isMarketOrder: + raise InvalidOrder(self.id + ' createOrder(): attached stopLoss is not supported for spot market orders') + if isTakeProfit: + tpTriggerPrice = self.safe_value_2(takeProfit, 'triggerPrice', 'stopPrice', takeProfit) + request['takeProfit'] = self.get_price(symbol, tpTriggerPrice) + tpLimitPrice = self.safe_value(takeProfit, 'price') + if tpLimitPrice is not None: + request['tpslMode'] = 'Partial' + request['tpOrderType'] = 'Limit' + request['tpLimitPrice'] = self.get_price(symbol, tpLimitPrice) + else: + # for spot market, we need to add self + if market['spot']: + request['tpOrderType'] = 'Market' + # for spot market, we need to add self + if market['spot'] and isMarketOrder: + raise InvalidOrder(self.id + ' createOrder(): attached takeProfit is not supported for spot market orders') + if not market['spot'] and hedged: + if reduceOnly: + params = self.omit(params, 'reduceOnly') + side = 'sell' if (side == 'buy') else 'buy' + request['positionIdx'] = 1 if (side == 'buy') else 2 + params = self.omit(params, ['stopPrice', 'timeInForce', 'stopLossPrice', 'takeProfitPrice', 'postOnly', 'clientOrderId', 'triggerPrice', 'stopLoss', 'takeProfit', 'trailingAmount', 'trailingTriggerPrice', 'hedged', 'tpslMode']) + return self.extend(request, params) + + async def create_orders(self, orders: List[OrderRequest], params={}) -> List[Order]: + """ + create a list of trade orders + + https://bybit-exchange.github.io/docs/v5/order/batch-place + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + accounts = await self.is_unified_enabled() + isUta = accounts[1] + ordersRequests = [] + orderSymbols = [] + for i in range(0, len(orders)): + rawOrder = orders[i] + marketId = self.safe_string(rawOrder, 'symbol') + orderSymbols.append(marketId) + type = self.safe_string(rawOrder, 'type') + side = self.safe_string(rawOrder, 'side') + amount = self.safe_value(rawOrder, 'amount') + price = self.safe_value(rawOrder, 'price') + orderParams = self.safe_dict(rawOrder, 'params', {}) + orderRequest = self.create_order_request(marketId, type, side, amount, price, orderParams, isUta) + del orderRequest['category'] + ordersRequests.append(orderRequest) + symbols = self.market_symbols(orderSymbols, None, False, True, True) + market = self.market(symbols[0]) + unifiedMarginStatus = self.safe_integer(self.options, 'unifiedMarginStatus', 6) + category = None + category, params = self.get_bybit_type('createOrders', market, params) + if (category == 'inverse') and (unifiedMarginStatus < 5): + raise NotSupported(self.id + ' createOrders does not allow inverse orders for non UTA2.0 account') + request: dict = { + 'category': category, + 'request': ordersRequests, + } + response = await self.privatePostV5OrderCreateBatch(self.extend(request, params)) + result = self.safe_dict(response, 'result', {}) + data = self.safe_list(result, 'list', []) + retInfo = self.safe_dict(response, 'retExtInfo', {}) + codes = self.safe_list(retInfo, 'list', []) + # self.extend the error with the unsuccessful orders + for i in range(0, len(codes)): + code = codes[i] + retCode = self.safe_integer(code, 'code') + if retCode != 0: + data[i] = self.extend(data[i], code) + # + # { + # "retCode":0, + # "retMsg":"OK", + # "result":{ + # "list":[ + # { + # "category":"linear", + # "symbol":"LTCUSDT", + # "orderId":"", + # "orderLinkId":"", + # "createAt":"" + # }, + # { + # "category":"linear", + # "symbol":"LTCUSDT", + # "orderId":"3c9f65b6-01ad-4ac0-9741-df17e02a4223", + # "orderLinkId":"", + # "createAt":"1698075516029" + # } + # ] + # }, + # "retExtInfo":{ + # "list":[ + # { + # "code":10001, + # "msg":"The number of contracts exceeds maximum limit allowed: too large" + # }, + # { + # "code":0, + # "msg":"OK" + # } + # ] + # }, + # "time":1698075516029 + # } + # + return self.parse_orders(data) + + def edit_order_request(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'orderId': id, + # 'orderLinkId': 'string', # unique client order id, max 36 characters + # 'takeProfit': 123.45, # take profit price, only take effect upon opening the position + # 'stopLoss': 123.45, # stop loss price, only take effect upon opening the position + # 'triggerPrice': 123.45, # trigger price, required for conditional orders + # 'triggerBy': 'MarkPrice', # IndexPrice, MarkPrice, LastPrice + # 'tpTriggerby': 'MarkPrice', # IndexPrice, MarkPrice, LastPrice + # 'slTriggerBy': 'MarkPrice', # IndexPrice, MarkPrice, LastPrice + # Valid for option only. + # 'orderIv': '0', # Implied volatility; parameters are passed according to the real value; for example, for 10%, 0.1 is passed + } + category = None + category, params = self.get_bybit_type('editOrderRequest', market, params) + request['category'] = category + if amount is not None: + request['qty'] = self.get_amount(symbol, amount) + if price is not None: + request['price'] = self.get_price(symbol, self.number_to_string(price)) + triggerPrice = self.safe_string_2(params, 'triggerPrice', 'stopPrice') + stopLossTriggerPrice = self.safe_string(params, 'stopLossPrice') + takeProfitTriggerPrice = self.safe_string(params, 'takeProfitPrice') + stopLoss = self.safe_value(params, 'stopLoss') + takeProfit = self.safe_value(params, 'takeProfit') + isStopLossTriggerOrder = stopLossTriggerPrice is not None + isTakeProfitTriggerOrder = takeProfitTriggerPrice is not None + isStopLoss = stopLoss is not None + isTakeProfit = takeProfit is not None + if isStopLossTriggerOrder or isTakeProfitTriggerOrder: + triggerPrice = stopLossTriggerPrice if isStopLossTriggerOrder else takeProfitTriggerPrice + if triggerPrice is not None: + triggerPriceRequest = triggerPrice if (triggerPrice == '0') else self.get_price(symbol, triggerPrice) + request['triggerPrice'] = triggerPriceRequest + triggerBy = self.safe_string(params, 'triggerBy', 'LastPrice') + request['triggerBy'] = triggerBy + if isStopLoss or isTakeProfit: + if isStopLoss: + slTriggerPrice = self.safe_string_2(stopLoss, 'triggerPrice', 'stopPrice', stopLoss) + stopLossRequest = slTriggerPrice if (slTriggerPrice == '0') else self.get_price(symbol, slTriggerPrice) + request['stopLoss'] = stopLossRequest + slTriggerBy = self.safe_string(params, 'slTriggerBy', 'LastPrice') + request['slTriggerBy'] = slTriggerBy + if isTakeProfit: + tpTriggerPrice = self.safe_string_2(takeProfit, 'triggerPrice', 'stopPrice', takeProfit) + takeProfitRequest = tpTriggerPrice if (tpTriggerPrice == '0') else self.get_price(symbol, tpTriggerPrice) + request['takeProfit'] = takeProfitRequest + tpTriggerBy = self.safe_string(params, 'tpTriggerBy', 'LastPrice') + request['tpTriggerBy'] = tpTriggerBy + clientOrderId = self.safe_string(params, 'clientOrderId') + if clientOrderId is not None: + request['orderLinkId'] = clientOrderId + params = self.omit(params, ['stopPrice', 'stopLossPrice', 'takeProfitPrice', 'triggerPrice', 'clientOrderId', 'stopLoss', 'takeProfit']) + return request + + async def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}) -> Order: + """ + edit a trade order + + https://bybit-exchange.github.io/docs/v5/order/amend-order + https://bybit-exchange.github.io/docs/derivatives/unified/replace-order + https://bybit-exchange.github.io/docs/api-explorer/derivatives/trade/contract/replace-order + + :param str id: cancel order id + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float price: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: The price that a trigger order is triggered at + :param float [params.stopLossPrice]: The price that a stop loss order is triggered at + :param float [params.takeProfitPrice]: The price that a take profit order is triggered at + :param dict [params.takeProfit]: *takeProfit object in params* containing the triggerPrice that the attached take profit order will be triggered + :param float [params.takeProfit.triggerPrice]: take profit trigger price + :param dict [params.stopLoss]: *stopLoss object in params* containing the triggerPrice that the attached stop loss order will be triggered + :param float [params.stopLoss.triggerPrice]: stop loss trigger price + :param str [params.triggerBy]: 'IndexPrice', 'MarkPrice' or 'LastPrice', default is 'LastPrice', required if no initial value for triggerPrice + :param str [params.slTriggerBy]: 'IndexPrice', 'MarkPrice' or 'LastPrice', default is 'LastPrice', required if no initial value for stopLoss + :param str [params.tpTriggerby]: 'IndexPrice', 'MarkPrice' or 'LastPrice', default is 'LastPrice', required if no initial value for takeProfit + :returns dict: an `order structure ` + """ + await self.load_markets() + if symbol is None: + raise ArgumentsRequired(self.id + ' editOrder() requires a symbol argument') + request = self.edit_order_request(id, symbol, type, side, amount, price, params) + response = await self.privatePostV5OrderAmend(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "orderId": "c6f055d9-7f21-4079-913d-e6523a9cfffa", + # "orderLinkId": "linear-004" + # }, + # "retExtInfo": {}, + # "time": 1672217093461 + # } + # + result = self.safe_dict(response, 'result', {}) + return self.safe_order({ + 'info': response, + 'id': self.safe_string(result, 'orderId'), + }) + + async def edit_orders(self, orders: List[OrderRequest], params={}) -> List[Order]: + """ + edit a list of trade orders + + https://bybit-exchange.github.io/docs/v5/order/batch-amend + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + ordersRequests = [] + orderSymbols = [] + for i in range(0, len(orders)): + rawOrder = orders[i] + symbol = self.safe_string(rawOrder, 'symbol') + orderSymbols.append(symbol) + id = self.safe_string(rawOrder, 'id') + type = self.safe_string(rawOrder, 'type') + side = self.safe_string(rawOrder, 'side') + amount = self.safe_value(rawOrder, 'amount') + price = self.safe_value(rawOrder, 'price') + orderParams = self.safe_dict(rawOrder, 'params', {}) + orderRequest = self.edit_order_request(id, symbol, type, side, amount, price, orderParams) + del orderRequest['category'] + ordersRequests.append(orderRequest) + orderSymbols = self.market_symbols(orderSymbols, None, False, True, True) + market = self.market(orderSymbols[0]) + unifiedMarginStatus = self.safe_integer(self.options, 'unifiedMarginStatus', 6) + category = None + category, params = self.get_bybit_type('editOrders', market, params) + if (category == 'inverse') and (unifiedMarginStatus < 5): + raise NotSupported(self.id + ' editOrders does not allow inverse orders for non UTA2.0 account') + request: dict = { + 'category': category, + 'request': ordersRequests, + } + response = await self.privatePostV5OrderAmendBatch(self.extend(request, params)) + result = self.safe_dict(response, 'result', {}) + data = self.safe_list(result, 'list', []) + retInfo = self.safe_dict(response, 'retExtInfo', {}) + codes = self.safe_list(retInfo, 'list', []) + # self.extend the error with the unsuccessful orders + for i in range(0, len(codes)): + code = codes[i] + retCode = self.safe_integer(code, 'code') + if retCode != 0: + data[i] = self.extend(data[i], code) + # + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "list": [ + # { + # "category": "option", + # "symbol": "ETH-30DEC22-500-C", + # "orderId": "b551f227-7059-4fb5-a6a6-699c04dbd2f2", + # "orderLinkId": "" + # }, + # { + # "category": "option", + # "symbol": "ETH-30DEC22-700-C", + # "orderId": "fa6a595f-1a57-483f-b9d3-30e9c8235a52", + # "orderLinkId": "" + # } + # ] + # }, + # "retExtInfo": { + # "list": [ + # { + # "code": 0, + # "msg": "OK" + # }, + # { + # "code": 0, + # "msg": "OK" + # } + # ] + # }, + # "time": 1672222808060 + # } + # + return self.parse_orders(data) + + def cancel_order_request(self, id: str, symbol: Str = None, params={}): + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + # 'orderLinkId': 'string', + # 'orderId': id, + # conditional orders + # 'orderFilter': '', # Valid for spot only. Order,tpslOrder. If not passed, Order by default + } + if market['spot']: + # only works for spot market + isTrigger = self.safe_bool_2(params, 'stop', 'trigger', False) + params = self.omit(params, ['stop', 'trigger']) + request['orderFilter'] = 'StopOrder' if isTrigger else 'Order' + if id is not None: # The user can also use argument params["orderLinkId"] + request['orderId'] = id + category = None + category, params = self.get_bybit_type('cancelOrderRequest', market, params) + request['category'] = category + return self.extend(request, params) + + async def cancel_order(self, id: str, symbol: Str = None, params={}) -> Order: + """ + cancels an open order + + https://bybit-exchange.github.io/docs/v5/order/cancel-order + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: *spot only* whether the order is a trigger order + :param boolean [params.stop]: alias for trigger + :param str [params.orderFilter]: *spot only* 'Order' or 'StopOrder' or 'tpslOrder' + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + requestExtended = self.cancel_order_request(id, symbol, params) + response = await self.privatePostV5OrderCancel(requestExtended) + # + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "orderId": "c6f055d9-7f21-4079-913d-e6523a9cfffa", + # "orderLinkId": "linear-004" + # }, + # "retExtInfo": {}, + # "time": 1672217377164 + # } + # + result = self.safe_dict(response, 'result', {}) + return self.parse_order(result, market) + + async def cancel_orders(self, ids: List[str], symbol: Str = None, params={}) -> List[Order]: + """ + cancel multiple orders + + https://bybit-exchange.github.io/docs/v5/order/batch-cancel + + :param str[] ids: order ids + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str[] [params.clientOrderIds]: client order ids + :returns dict: an list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrders() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + types = await self.is_unified_enabled() + enableUnifiedAccount = types[1] + if not enableUnifiedAccount: + raise NotSupported(self.id + ' cancelOrders() supports UTA accounts only') + category = None + category, params = self.get_bybit_type('cancelOrders', market, params) + if category == 'inverse': + raise NotSupported(self.id + ' cancelOrders does not allow inverse orders') + ordersRequests = [] + clientOrderIds = self.safe_list_2(params, 'clientOrderIds', 'clientOids', []) + params = self.omit(params, ['clientOrderIds', 'clientOids']) + for i in range(0, len(clientOrderIds)): + ordersRequests.append({ + 'symbol': market['id'], + 'orderLinkId': self.safe_string(clientOrderIds, i), + }) + for i in range(0, len(ids)): + ordersRequests.append({ + 'symbol': market['id'], + 'orderId': self.safe_string(ids, i), + }) + request: dict = { + 'category': category, + 'request': ordersRequests, + } + response = await self.privatePostV5OrderCancelBatch(self.extend(request, params)) + # + # { + # "retCode": "0", + # "retMsg": "OK", + # "result": { + # "list": [ + # { + # "category": "spot", + # "symbol": "BTCUSDT", + # "orderId": "1636282505818800896", + # "orderLinkId": "1636282505818800897" + # }, + # { + # "category": "spot", + # "symbol": "BTCUSDT", + # "orderId": "1636282505818800898", + # "orderLinkId": "1636282505818800899" + # } + # ] + # }, + # "retExtInfo": { + # "list": [ + # { + # "code": "0", + # "msg": "OK" + # }, + # { + # "code": "0", + # "msg": "OK" + # } + # ] + # }, + # "time": "1709796158501" + # } + # + result = self.safe_dict(response, 'result', {}) + row = self.safe_list(result, 'list', []) + return self.parse_orders(row, market) + + async def cancel_all_orders_after(self, timeout: Int, params={}): + """ + dead man's switch, cancel all orders after the given timeout + + https://bybit-exchange.github.io/docs/v5/order/dcp + + :param number timeout: time in milliseconds + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.product]: OPTIONS, DERIVATIVES, SPOT, default is 'DERIVATIVES' + :returns dict: the api result + """ + await self.load_markets() + request: dict = { + 'timeWindow': self.parse_to_int(timeout / 1000), + } + type: Str = None + type, params = self.handle_market_type_and_params('cancelAllOrdersAfter', None, params, 'swap') + productMap = { + 'spot': 'SPOT', + 'swap': 'DERIVATIVES', + 'option': 'OPTIONS', + } + product = self.safe_string(productMap, type, type) + request['product'] = product + response = await self.privatePostV5OrderDisconnectedCancelAll(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "success" + # } + # + return response + + async def cancel_orders_for_symbols(self, orders: List[CancellationRequest], params={}): + """ + cancel multiple orders for multiple symbols + + https://bybit-exchange.github.io/docs/v5/order/batch-cancel + + :param CancellationRequest[] orders: list of order ids with symbol, example [{"id": "a", "symbol": "BTC/USDT"}, {"id": "b", "symbol": "ETH/USDT"}] + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an list of `order structures ` + """ + await self.load_markets() + types = await self.is_unified_enabled() + enableUnifiedAccount = types[1] + if not enableUnifiedAccount: + raise NotSupported(self.id + ' cancelOrdersForSymbols() supports UTA accounts only') + ordersRequests = [] + category = None + for i in range(0, len(orders)): + order = orders[i] + symbol = self.safe_string(order, 'symbol') + market = self.market(symbol) + currentCategory = None + currentCategory, params = self.get_bybit_type('cancelOrders', market, params) + if currentCategory == 'inverse': + raise NotSupported(self.id + ' cancelOrdersForSymbols does not allow inverse orders') + if (category is not None) and (category != currentCategory): + raise ExchangeError(self.id + ' cancelOrdersForSymbols requires all orders to be of the same category(linear, spot or option))') + category = currentCategory + id = self.safe_string(order, 'id') + clientOrderId = self.safe_string(order, 'clientOrderId') + idKey = 'orderId' + if clientOrderId is not None: + idKey = 'orderLinkId' + orderItem: dict = { + 'symbol': market['id'], + } + orderItem[idKey] = id if (idKey == 'orderId') else clientOrderId + ordersRequests.append(orderItem) + request: dict = { + 'category': category, + 'request': ordersRequests, + } + response = await self.privatePostV5OrderCancelBatch(self.extend(request, params)) + # + # { + # "retCode": "0", + # "retMsg": "OK", + # "result": { + # "list": [ + # { + # "category": "spot", + # "symbol": "BTCUSDT", + # "orderId": "1636282505818800896", + # "orderLinkId": "1636282505818800897" + # }, + # { + # "category": "spot", + # "symbol": "BTCUSDT", + # "orderId": "1636282505818800898", + # "orderLinkId": "1636282505818800899" + # } + # ] + # }, + # "retExtInfo": { + # "list": [ + # { + # "code": "0", + # "msg": "OK" + # }, + # { + # "code": "0", + # "msg": "OK" + # } + # ] + # }, + # "time": "1709796158501" + # } + # + result = self.safe_dict(response, 'result', {}) + row = self.safe_list(result, 'list', []) + return self.parse_orders(row, None) + + async def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders + + https://bybit-exchange.github.io/docs/v5/order/cancel-all + + :param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: True if trigger order + :param boolean [params.stop]: alias for trigger + :param str [params.type]: market type, ['swap', 'option', 'spot'] + :param str [params.subType]: market subType, ['linear', 'inverse'] + :param str [params.baseCoin]: Base coin. Supports linear, inverse & option + :param str [params.settleCoin]: Settle coin. Supports linear, inverse & option + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + enableUnifiedMargin, enableUnifiedAccount = await self.is_unified_enabled() + isUnifiedAccount = (enableUnifiedMargin or enableUnifiedAccount) + market = None + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + type = None + type, params = self.get_bybit_type('cancelAllOrders', market, params) + request['category'] = type + if (type == 'option') and not isUnifiedAccount: + raise NotSupported(self.id + ' cancelAllOrders() Normal Account not support ' + type + ' market') + if (type == 'linear') or (type == 'inverse'): + baseCoin = self.safe_string(params, 'baseCoin') + if symbol is None and baseCoin is None: + defaultSettle = self.safe_string(self.options, 'defaultSettle', 'USDT') + request['settleCoin'] = self.safe_string(params, 'settleCoin', defaultSettle) + isTrigger = self.safe_bool_2(params, 'stop', 'trigger', False) + params = self.omit(params, ['stop', 'trigger']) + if isTrigger: + request['orderFilter'] = 'StopOrder' + response = await self.privatePostV5OrderCancelAll(self.extend(request, params)) + # + # linear / inverse / option + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "list": [ + # { + # "orderId": "f6a73e1f-39b5-4dee-af21-1460b2e3b27c", + # "orderLinkId": "a001" + # } + # ] + # }, + # "retExtInfo": {}, + # "time": 1672219780463 + # } + # + # spot + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "success": "1" + # }, + # "retExtInfo": {}, + # "time": 1676962409398 + # } + # + result = self.safe_dict(response, 'result', {}) + orders = self.safe_list(result, 'list') + if not isinstance(orders, list): + return [self.safe_order({'info': response})] + return self.parse_orders(orders, market) + + async def fetch_order_classic(self, id: str, symbol: Str = None, params={}) -> Order: + """ + fetches information on an order made by the user *classic accounts only* + + https://bybit-exchange.github.io/docs/v5/order/order-list + + :param str id: the order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrder() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + if market['spot']: + raise NotSupported(self.id + ' fetchOrder() is not supported for spot markets') + request: dict = { + 'orderId': id, + } + result = await self.fetch_orders(symbol, None, None, self.extend(request, params)) + length = len(result) + if length == 0: + isTrigger = self.safe_bool_n(params, ['trigger', 'stop'], False) + extra = '' if isTrigger else ' If you are trying to fetch SL/TP conditional order, you might try setting params["trigger"] = True' + raise OrderNotFound('Order ' + str(id) + ' was not found.' + extra) + if length > 1: + raise InvalidOrder(self.id + ' returned more than one order') + return self.safe_value(result, 0) + + async def fetch_order(self, id: str, symbol: Str = None, params={}) -> Order: + """ +classic accounts only/ spot not supported* fetches information on an order made by the user *classic accounts only* + + https://bybit-exchange.github.io/docs/v5/order/order-list + + :param str id: the order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param dict [params.acknowledged]: to suppress the warning, set to True + :returns dict: An `order structure ` + """ + await self.load_markets() + enableUnifiedMargin, enableUnifiedAccount = await self.is_unified_enabled() + isUnifiedAccount = (enableUnifiedMargin or enableUnifiedAccount) + if not isUnifiedAccount: + return await self.fetch_order_classic(id, symbol, params) + acknowledge = False + acknowledge, params = self.handle_option_and_params(params, 'fetchOrder', 'acknowledged') + if not acknowledge: + raise ArgumentsRequired(self.id + ' fetchOrder() can only access an order if it is in last 500 orders(of any status) for your account. Set params["acknowledged"] = True to hide self warning. Alternatively, we suggest to use fetchOpenOrder or fetchClosedOrder') + market = self.market(symbol) + marketType = None + marketType, params = self.get_bybit_type('fetchOrder', market, params) + request: dict = { + 'symbol': market['id'], + 'orderId': id, + 'category': marketType, + } + isTrigger = None + isTrigger, params = self.handle_param_bool_2(params, 'trigger', 'stop', False) + if isTrigger: + request['orderFilter'] = 'StopOrder' + response = await self.privateGetV5OrderRealtime(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "nextPageCursor": "1321052653536515584%3A1672217748287%2C1321052653536515584%3A1672217748287", + # "category": "spot", + # "list": [ + # { + # "symbol": "ETHUSDT", + # "orderType": "Limit", + # "orderLinkId": "1672217748277652", + # "orderId": "1321052653536515584", + # "cancelType": "UNKNOWN", + # "avgPrice": "", + # "stopOrderType": "tpslOrder", + # "lastPriceOnCreated": "", + # "orderStatus": "Cancelled", + # "takeProfit": "", + # "cumExecValue": "0", + # "triggerDirection": 0, + # "isLeverage": "0", + # "rejectReason": "", + # "price": "1000", + # "orderIv": "", + # "createdTime": "1672217748287", + # "tpTriggerBy": "", + # "positionIdx": 0, + # "timeInForce": "GTC", + # "leavesValue": "500", + # "updatedTime": "1672217748287", + # "side": "Buy", + # "triggerPrice": "1500", + # "cumExecFee": "0", + # "leavesQty": "0", + # "slTriggerBy": "", + # "closeOnTrigger": False, + # "cumExecQty": "0", + # "reduceOnly": False, + # "qty": "0.5", + # "stopLoss": "", + # "triggerBy": "1192.5" + # } + # ] + # }, + # "retExtInfo": {}, + # "time": 1672219526294 + # } + # + result = self.safe_dict(response, 'result', {}) + innerList = self.safe_list(result, 'list', []) + if len(innerList) == 0: + extra = '' if isTrigger else ' If you are trying to fetch SL/TP conditional order, you might try setting params["trigger"] = True' + raise OrderNotFound('Order ' + str(id) + ' was not found.' + extra) + order = self.safe_dict(innerList, 0, {}) + return self.parse_order(order, market) + + async def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + res = await self.is_unified_enabled() + """ + *classic accounts only/ spot not supported* fetches information on multiple orders made by the user *classic accounts only/ spot not supported* + https://bybit-exchange.github.io/docs/v5/order/order-list + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: True if trigger order + :param boolean [params.stop]: alias for trigger + :param str [params.type]: market type, ['swap', 'option'] + :param str [params.subType]: market subType, ['linear', 'inverse'] + :param str [params.orderFilter]: 'Order' or 'StopOrder' or 'tpslOrder' + :param int [params.until]: the latest time in ms to fetch entries for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Order[]: a list of `order structures ` + """ + enableUnifiedAccount = self.safe_bool(res, 1) + if enableUnifiedAccount: + raise NotSupported(self.id + ' fetchOrders() is not supported after the 5/02 update for UTA accounts, please use fetchOpenOrders, fetchClosedOrders or fetchCanceledOrders') + return await self.fetch_orders_classic(symbol, since, limit, params) + + async def fetch_orders_classic(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user *classic accounts only* + + https://bybit-exchange.github.io/docs/v5/order/order-list + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: True if trigger order + :param boolean [params.stop]: alias for trigger + :param str [params.type]: market type, ['swap', 'option', 'spot'] + :param str [params.subType]: market subType, ['linear', 'inverse'] + :param str [params.orderFilter]: 'Order' or 'StopOrder' or 'tpslOrder' + :param int [params.until]: the latest time in ms to fetch entries for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOrders', 'paginate') + if paginate: + return await self.fetch_paginated_call_cursor('fetchOrders', symbol, since, limit, params, 'nextPageCursor', 'cursor', None, 50) + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + type = None + type, params = self.get_bybit_type('fetchOrders', market, params) + if type == 'spot': + raise NotSupported(self.id + ' fetchOrders() is not supported for spot markets') + request['category'] = type + isTrigger = self.safe_bool_n(params, ['trigger', 'stop'], False) + params = self.omit(params, ['trigger', 'stop']) + if isTrigger: + request['orderFilter'] = 'StopOrder' + if limit is not None: + request['limit'] = limit + if since is not None: + request['startTime'] = since + until = self.safe_integer(params, 'until') # unified in milliseconds + endTime = self.safe_integer(params, 'endTime', until) # exchange-specific in milliseconds + params = self.omit(params, ['endTime', 'until']) + if endTime is not None: + request['endTime'] = endTime + response = await self.privateGetV5OrderHistory(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "nextPageCursor": "03234de9-1332-41eb-b805-4a9f42c136a3%3A1672220109387%2C03234de9-1332-41eb-b805-4a9f42c136a3%3A1672220109387", + # "category": "linear", + # "list": [ + # { + # "symbol": "BTCUSDT", + # "orderType": "Limit", + # "orderLinkId": "test-001", + # "orderId": "03234de9-1332-41eb-b805-4a9f42c136a3", + # "cancelType": "CancelByUser", + # "avgPrice": "0", + # "stopOrderType": "UNKNOWN", + # "lastPriceOnCreated": "16656.5", + # "orderStatus": "Cancelled", + # "takeProfit": "", + # "cumExecValue": "0", + # "triggerDirection": 0, + # "blockTradeId": "", + # "rejectReason": "EC_PerCancelRequest", + # "isLeverage": "", + # "price": "18000", + # "orderIv": "", + # "createdTime": "1672220109387", + # "tpTriggerBy": "UNKNOWN", + # "positionIdx": 0, + # "timeInForce": "GoodTillCancel", + # "leavesValue": "0", + # "updatedTime": "1672220114123", + # "side": "Sell", + # "triggerPrice": "", + # "cumExecFee": "0", + # "slTriggerBy": "UNKNOWN", + # "leavesQty": "0", + # "closeOnTrigger": False, + # "cumExecQty": "0", + # "reduceOnly": False, + # "qty": "0.1", + # "stopLoss": "", + # "triggerBy": "UNKNOWN" + # } + # ] + # }, + # "retExtInfo": {}, + # "time": 1672221263862 + # } + # + data = self.add_pagination_cursor_to_result(response) + return self.parse_orders(data, market, since, limit) + + async def fetch_closed_order(self, id: str, symbol: Str = None, params={}) -> Order: + """ + fetches information on a closed order made by the user + + https://bybit-exchange.github.io/docs/v5/order/order-list + + :param str id: order id + :param str [symbol]: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: set to True for fetching a closed trigger order + :param boolean [params.stop]: alias for trigger + :param str [params.type]: market type, ['swap', 'option', 'spot'] + :param str [params.subType]: market subType, ['linear', 'inverse'] + :param str [params.orderFilter]: 'Order' or 'StopOrder' or 'tpslOrder' + :returns dict: an `order structure ` + """ + await self.load_markets() + request: dict = { + 'orderId': id, + } + result = await self.fetch_closed_orders(symbol, None, None, self.extend(request, params)) + length = len(result) + if length == 0: + isTrigger = self.safe_bool_n(params, ['trigger', 'stop'], False) + extra = '' if isTrigger else ' If you are trying to fetch SL/TP conditional order, you might try setting params["trigger"] = True' + raise OrderNotFound('Order ' + str(id) + ' was not found.' + extra) + if length > 1: + raise InvalidOrder(self.id + ' returned more than one order') + return self.safe_value(result, 0) + + async def fetch_open_order(self, id: str, symbol: Str = None, params={}) -> Order: + """ + fetches information on an open order made by the user + + https://bybit-exchange.github.io/docs/v5/order/open-order + + :param str id: order id + :param str [symbol]: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: set to True for fetching an open trigger order + :param boolean [params.stop]: alias for trigger + :param str [params.type]: market type, ['swap', 'option', 'spot'] + :param str [params.subType]: market subType, ['linear', 'inverse'] + :param str [params.baseCoin]: Base coin. Supports linear, inverse & option + :param str [params.settleCoin]: Settle coin. Supports linear, inverse & option + :param str [params.orderFilter]: 'Order' or 'StopOrder' or 'tpslOrder' + :returns dict: an `order structure ` + """ + await self.load_markets() + request: dict = { + 'orderId': id, + } + result = await self.fetch_open_orders(symbol, None, None, self.extend(request, params)) + length = len(result) + if length == 0: + isTrigger = self.safe_bool_n(params, ['trigger', 'stop'], False) + extra = '' if isTrigger else ' If you are trying to fetch SL/TP conditional order, you might try setting params["trigger"] = True' + raise OrderNotFound('Order ' + str(id) + ' was not found.' + extra) + if length > 1: + raise InvalidOrder(self.id + ' returned more than one order') + return self.safe_value(result, 0) + + async def fetch_canceled_and_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple canceled and closed orders made by the user + + https://bybit-exchange.github.io/docs/v5/order/order-list + + :param str [symbol]: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: set to True for fetching trigger orders + :param boolean [params.stop]: alias for trigger + :param str [params.type]: market type, ['swap', 'option', 'spot'] + :param str [params.subType]: market subType, ['linear', 'inverse'] + :param str [params.orderFilter]: 'Order' or 'StopOrder' or 'tpslOrder' + :param int [params.until]: the latest time in ms to fetch entries for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchCanceledAndClosedOrders', 'paginate') + if paginate: + return await self.fetch_paginated_call_cursor('fetchCanceledAndClosedOrders', symbol, since, limit, params, 'nextPageCursor', 'cursor', None, 50) + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + type = None + type, params = self.get_bybit_type('fetchCanceledAndClosedOrders', market, params) + request['category'] = type + isTrigger = self.safe_bool_n(params, ['trigger', 'stop'], False) + params = self.omit(params, ['trigger', 'stop']) + if isTrigger: + request['orderFilter'] = 'StopOrder' + if limit is not None: + request['limit'] = limit + if since is not None: + request['startTime'] = since + until = self.safe_integer(params, 'until') # unified in milliseconds + endTime = self.safe_integer(params, 'endTime', until) # exchange-specific in milliseconds + params = self.omit(params, ['endTime', 'until']) + if endTime is not None: + request['endTime'] = endTime + response = await self.privateGetV5OrderHistory(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "nextPageCursor": "f5f2d355-9a11-4af3-9b83-aa1d6ab6ddfe%3A1757837618905%2Caee7453a-a100-465f-857a-3db780e9329a%3A1757837580469", + # "category": "linear", + # "list": [ + # { + # "symbol": "BTCUSDT", + # "orderType": "Market", + # "orderLinkId": "", + # "slLimitPrice": "0", + # "orderId": "f5f2d355-9a11-4af3-9b83-aa1d6ab6ddfe", + # "cancelType": "UNKNOWN", + # "avgPrice": "122529.9", + # "stopOrderType": "", + # "lastPriceOnCreated": "123747.9", + # "orderStatus": "Filled", + # "createType": "CreateByUser", + # "takeProfit": "", + # "cumExecValue": "122.5299", + # "tpslMode": "", + # "smpType": "None", + # "triggerDirection": 0, + # "blockTradeId": "", + # "cumFeeDetail": { + # "USDT": "0.06739145" + # }, + # "rejectReason": "EC_NoError", + # "isLeverage": "", + # "price": "120518", + # "orderIv": "", + # "createdTime": "1757837618905", + # "tpTriggerBy": "", + # "positionIdx": 0, + # "timeInForce": "IOC", + # "leavesValue": "0", + # "updatedTime": "1757837618909", + # "side": "Sell", + # "smpGroup": 0, + # "triggerPrice": "", + # "tpLimitPrice": "0", + # "cumExecFee": "0.06739145", + # "slTriggerBy": "", + # "leavesQty": "0", + # "closeOnTrigger": False, + # "slippageToleranceType": "UNKNOWN", + # "placeType": "", + # "cumExecQty": "0.001", + # "reduceOnly": True, + # "qty": "0.001", + # "stopLoss": "", + # "smpOrderId": "", + # "slippageTolerance": "0", + # "triggerBy": "", + # "extraFees": "" + # }, + # ] + # }, + # "retExtInfo": {}, + # "time": 1758187806376 + # } + # + data = self.add_pagination_cursor_to_result(response) + return self.parse_orders(data, market, since, limit) + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://bybit-exchange.github.io/docs/v5/order/order-list + + :param str [symbol]: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: set to True for fetching closed trigger orders + :param boolean [params.stop]: alias for trigger + :param str [params.type]: market type, ['swap', 'option', 'spot'] + :param str [params.subType]: market subType, ['linear', 'inverse'] + :param str [params.orderFilter]: 'Order' or 'StopOrder' or 'tpslOrder' + :param int [params.until]: the latest time in ms to fetch entries for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + request: dict = { + 'orderStatus': 'Filled', + } + return await self.fetch_canceled_and_closed_orders(symbol, since, limit, self.extend(request, params)) + + async def fetch_canceled_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple canceled orders made by the user + + https://bybit-exchange.github.io/docs/v5/order/order-list + + :param str [symbol]: unified market symbol of the market orders were made in + :param int [since]: timestamp in ms of the earliest order, default is None + :param int [limit]: max number of orders to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: True if trigger order + :param boolean [params.stop]: alias for trigger + :param str [params.type]: market type, ['swap', 'option', 'spot'] + :param str [params.subType]: market subType, ['linear', 'inverse'] + :param str [params.orderFilter]: 'Order' or 'StopOrder' or 'tpslOrder' + :param int [params.until]: the latest time in ms to fetch entries for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict: a list of `order structures ` + """ + await self.load_markets() + request: dict = { + 'orderStatus': 'Cancelled', + } + return await self.fetch_canceled_and_closed_orders(symbol, since, limit, self.extend(request, params)) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://bybit-exchange.github.io/docs/v5/order/open-order + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: set to True for fetching open trigger orders + :param boolean [params.stop]: alias for trigger + :param str [params.type]: market type, ['swap', 'option', 'spot'] + :param str [params.subType]: market subType, ['linear', 'inverse'] + :param str [params.baseCoin]: Base coin. Supports linear, inverse & option + :param str [params.settleCoin]: Settle coin. Supports linear, inverse & option + :param str [params.orderFilter]: 'Order' or 'StopOrder' or 'tpslOrder' + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOpenOrders', 'paginate') + if paginate: + return await self.fetch_paginated_call_cursor('fetchOpenOrders', symbol, since, limit, params, 'nextPageCursor', 'cursor', None, 50) + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + type = None + type, params = self.get_bybit_type('fetchOpenOrders', market, params) + if type == 'linear' or type == 'inverse': + baseCoin = self.safe_string(params, 'baseCoin') + if symbol is None and baseCoin is None: + defaultSettle = self.safe_string(self.options, 'defaultSettle', 'USDT') + settleCoin = self.safe_string(params, 'settleCoin', defaultSettle) + request['settleCoin'] = settleCoin + request['category'] = type + isTrigger = self.safe_bool_2(params, 'stop', 'trigger', False) + params = self.omit(params, ['stop', 'trigger']) + if isTrigger: + request['orderFilter'] = 'StopOrder' + if limit is not None: + request['limit'] = limit + response = await self.privateGetV5OrderRealtime(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "nextPageCursor": "f5f2d355-9a11-4af3-9b83-aa1d6ab6ddfe%3A1757837618905%2Caee7453a-a100-465f-857a-3db780e9329a%3A1757837580469", + # "category": "linear", + # "list": [ + # { + # "symbol": "BTCUSDT", + # "orderType": "Market", + # "orderLinkId": "", + # "slLimitPrice": "0", + # "orderId": "f5f2d355-9a11-4af3-9b83-aa1d6ab6ddfe", + # "cancelType": "UNKNOWN", + # "avgPrice": "122529.9", + # "stopOrderType": "", + # "lastPriceOnCreated": "123747.9", + # "orderStatus": "Filled", + # "createType": "CreateByUser", + # "takeProfit": "", + # "cumExecValue": "122.5299", + # "tpslMode": "", + # "smpType": "None", + # "triggerDirection": 0, + # "blockTradeId": "", + # "cumFeeDetail": { + # "USDT": "0.06739145" + # }, + # "rejectReason": "EC_NoError", + # "isLeverage": "", + # "price": "120518", + # "orderIv": "", + # "createdTime": "1757837618905", + # "tpTriggerBy": "", + # "positionIdx": 0, + # "timeInForce": "IOC", + # "leavesValue": "0", + # "updatedTime": "1757837618909", + # "side": "Sell", + # "smpGroup": 0, + # "triggerPrice": "", + # "tpLimitPrice": "0", + # "cumExecFee": "0.06739145", + # "slTriggerBy": "", + # "leavesQty": "0", + # "closeOnTrigger": False, + # "slippageToleranceType": "UNKNOWN", + # "placeType": "", + # "cumExecQty": "0.001", + # "reduceOnly": True, + # "qty": "0.001", + # "stopLoss": "", + # "smpOrderId": "", + # "slippageTolerance": "0", + # "triggerBy": "", + # "extraFees": "" + # }, + # ] + # }, + # "retExtInfo": {}, + # "time": 1758187806376 + # } + # + data = self.add_pagination_cursor_to_result(response) + return self.parse_orders(data, market, since, limit) + + async def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + fetch all the trades made from a single order + + https://bybit-exchange.github.io/docs/v5/position/execution + + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + request: dict = {} + clientOrderId = self.safe_string_2(params, 'clientOrderId', 'orderLinkId') + if clientOrderId is not None: + request['orderLinkId'] = clientOrderId + else: + request['orderId'] = id + params = self.omit(params, ['clientOrderId', 'orderLinkId']) + return await self.fetch_my_trades(symbol, since, limit, self.extend(request, params)) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + fetch all trades made by the user + + https://bybit-exchange.github.io/docs/api-explorer/v5/position/execution + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: market type, ['swap', 'option', 'spot'] + :param str [params.subType]: market subType, ['linear', 'inverse'] + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchMyTrades', 'paginate') + if paginate: + return await self.fetch_paginated_call_cursor('fetchMyTrades', symbol, since, limit, params, 'nextPageCursor', 'cursor', None, 100) + request: dict = { + 'execType': 'Trade', + } + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + type = None + type, params = self.get_bybit_type('fetchMyTrades', market, params) + request['category'] = type + if limit is not None: + request['limit'] = limit + if since is not None: + request['startTime'] = since + request, params = self.handle_until_option('endTime', request, params) + response = await self.privateGetV5ExecutionList(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "nextPageCursor": "132766%3A2%2C132766%3A2", + # "category": "linear", + # "list": [ + # { + # "symbol": "ETHPERP", + # "orderType": "Market", + # "underlyingPrice": "", + # "orderLinkId": "", + # "side": "Buy", + # "indexPrice": "", + # "orderId": "8c065341-7b52-4ca9-ac2c-37e31ac55c94", + # "stopOrderType": "UNKNOWN", + # "leavesQty": "0", + # "execTime": "1672282722429", + # "isMaker": False, + # "execFee": "0.071409", + # "feeRate": "0.0006", + # "execId": "e0cbe81d-0f18-5866-9415-cf319b5dab3b", + # "tradeIv": "", + # "blockTradeId": "", + # "markPrice": "1183.54", + # "execPrice": "1190.15", + # "markIv": "", + # "orderQty": "0.1", + # "orderPrice": "1236.9", + # "execValue": "119.015", + # "execType": "Trade", + # "execQty": "0.1" + # } + # ] + # }, + # "retExtInfo": {}, + # "time": 1672283754510 + # } + # + trades = self.add_pagination_cursor_to_result(response) + return self.parse_trades(trades, market, since, limit) + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + # + # { + # "chainType": "ERC20", + # "addressDeposit": "0xf56297c6717c1d1c42c30324468ed50a9b7402ee", + # "tagDeposit": '', + # "chain": "ETH" + # } + # + address = self.safe_string(depositAddress, 'addressDeposit') + tag = self.safe_string(depositAddress, 'tagDeposit') + code = self.safe_string(currency, 'code') + self.check_address(address) + return { + 'info': depositAddress, + 'currency': code, + 'network': self.network_id_to_code(self.safe_string(depositAddress, 'chain'), code), + 'address': address, + 'tag': tag, + } + + async def fetch_deposit_addresses_by_network(self, code: str, params={}) -> List[DepositAddress]: + """ + fetch a dictionary of addresses for a currency, indexed by network + + https://bybit-exchange.github.io/docs/v5/asset/master-deposit-addr + + :param str code: unified currency code of the currency for the deposit address + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `address structures ` indexed by the network + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'coin': currency['id'], + } + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + if networkCode is not None: + request['chainType'] = self.network_code_to_id(networkCode, code) + response = await self.privateGetV5AssetDepositQueryAddress(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "success", + # "result": { + # "coin": "USDT", + # "chains": [ + # { + # "chainType": "ERC20", + # "addressDeposit": "0xd9e1cd77afa0e50b452a62fbb68a3340602286c3", + # "tagDeposit": "", + # "chain": "ETH" + # } + # ] + # }, + # "retExtInfo": {}, + # "time": 1672192792860 + # } + # + result = self.safe_dict(response, 'result', {}) + chains = self.safe_list(result, 'chains', []) + coin = self.safe_string(result, 'coin') + currency = self.currency(coin) + parsed = self.parse_deposit_addresses(chains, [currency['code']], False, { + 'currency': currency['code'], + }) + return self.index_by(parsed, 'network') + + async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://bybit-exchange.github.io/docs/v5/asset/master-deposit-addr + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + await self.load_markets() + currency = self.currency(code) + networkCode, paramsOmited = self.handle_network_code_and_params(params) + indexedAddresses = await self.fetch_deposit_addresses_by_network(code, paramsOmited) + selectedNetworkCode = self.select_network_code_from_unified_networks(currency['code'], networkCode, indexedAddresses) + return indexedAddresses[selectedNetworkCode] + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://bybit-exchange.github.io/docs/v5/asset/deposit-record + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for, default = 30 days before the current time + :param int [limit]: the maximum number of deposits structures to retrieve, default = 50, max = 50 + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch deposits for, default = 30 days after since + EXCHANGE SPECIFIC PARAMETERS + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param str [params.cursor]: used for pagination + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchDeposits', 'paginate') + if paginate: + return await self.fetch_paginated_call_cursor('fetchDeposits', code, since, limit, params, 'nextPageCursor', 'cursor', None, 50) + request: dict = { + # 'coin': currency['id'], + # 'limit': 20, # max 50 + # 'cursor': '', + } + currency = None + if code is not None: + currency = self.currency(code) + request['coin'] = currency['id'] + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + request, params = self.handle_until_option('endTime', request, params) + response = await self.privateGetV5AssetDepositQueryRecord(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "success", + # "result": { + # "rows": [ + # { + # "coin": "USDT", + # "chain": "ETH", + # "amount": "10000", + # "txID": "skip-notification-scene-test-amount-202212270944-533285-USDT", + # "status": 3, + # "toAddress": "test-amount-address", + # "tag": "", + # "depositFee": "", + # "successAt": "1672134274000", + # "confirmations": "10000", + # "txIndex": "", + # "blockHash": "" + # } + # ], + # "nextPageCursor": "eyJtaW5JRCI6MTA0NjA0MywibWF4SUQiOjEwNDYwNDN9" + # }, + # "retExtInfo": {}, + # "time": 1672191992512 + # } + # + data = self.add_pagination_cursor_to_result(response) + return self.parse_transactions(data, currency, since, limit) + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://bybit-exchange.github.io/docs/v5/asset/withdraw-record + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch entries for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchWithdrawals', 'paginate') + if paginate: + return await self.fetch_paginated_call_cursor('fetchWithdrawals', code, since, limit, params, 'nextPageCursor', 'cursor', None, 50) + request: dict = { + # 'coin': currency['id'], + # 'limit': 20, # max 50 + # 'cusor': '', + } + currency = None + if code is not None: + currency = self.currency(code) + request['coin'] = currency['id'] + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + request, params = self.handle_until_option('endTime', request, params) + response = await self.privateGetV5AssetWithdrawQueryRecord(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "success", + # "result": { + # "rows": [ + # { + # "coin": "USDT", + # "chain": "ETH", + # "amount": "77", + # "txID": "", + # "status": "SecurityCheck", + # "toAddress": "0x99ced129603abc771c0dabe935c326ff6c86645d", + # "tag": "", + # "withdrawFee": "10", + # "createTime": "1670922217000", + # "updateTime": "1670922217000", + # "withdrawId": "9976", + # "withdrawType": 0 + # }, + # { + # "coin": "USDT", + # "chain": "ETH", + # "amount": "26", + # "txID": "", + # "status": "success", + # "toAddress": "15638072681@163.com", + # "tag": "", + # "withdrawFee": "0", + # "createTime": "1669711121000", + # "updateTime": "1669711380000", + # "withdrawId": "9801", + # "withdrawType": 1 + # } + # ], + # "nextPageCursor": "eyJtaW5JRCI6OTgwMSwibWF4SUQiOjk5NzZ9" + # }, + # "retExtInfo": {}, + # "time": 1672194949928 + # } + # + data = self.add_pagination_cursor_to_result(response) + return self.parse_transactions(data, currency, since, limit) + + def parse_transaction_status(self, status: Str): + statuses: dict = { + # v3 deposit status + '0': 'unknown', + '1': 'pending', + '2': 'processing', + '3': 'ok', + '4': 'fail', + # v3 withdrawal status + 'SecurityCheck': 'pending', + 'Pending': 'pending', + 'success': 'ok', + 'CancelByUser': 'canceled', + 'Reject': 'rejected', + 'Fail': 'failed', + 'BlockchainConfirmed': 'ok', + } + return self.safe_string(statuses, status, status) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fetchWithdrawals + # + # { + # "coin": "USDT", + # "chain": "TRX", + # "amount": "12.34", + # "txID": "de5ea0a2f2e59dc9a714837dd3ddc6d5e151b56ec5d786d351c4f52336f80d3c", + # "status": "success", + # "toAddress": "TQdmFKUoe1Lk2iwZuwRJEHJreTUBoN3BAw", + # "tag": "", + # "withdrawFee": "0.5", + # "createTime": "1665144183000", + # "updateTime": "1665144256000", + # "withdrawId": "8839035" + # } + # + # fetchDeposits + # + # { + # "coin": "USDT", + # "chain": "TRX", + # "amount": "44", + # "txID": "0b038ea12fa1575e2d66693db3c346b700d4b28347afc39f80321cf089acc960", + # "status": "3", + # "toAddress": "TC6NCAC5WSVCCiaD3kWZXyW91ZKKhLm53b", + # "tag": "", + # "depositFee": "", + # "successAt": "1665142507000", + # "confirmations": "100", + # "txIndex": "0", + # "blockHash": "0000000002ac3b1064aee94bca1bd0b58c4c09c65813b084b87a2063d961129e" + # } + # + # withdraw + # + # { + # "id": "9377266" + # } + # + currencyId = self.safe_string(transaction, 'coin') + code = self.safe_currency_code(currencyId, currency) + timestamp = self.safe_integer_2(transaction, 'createTime', 'successAt') + updated = self.safe_integer(transaction, 'updateTime') + status = self.parse_transaction_status(self.safe_string(transaction, 'status')) + feeCost = self.safe_number_2(transaction, 'depositFee', 'withdrawFee') + type = 'deposit' if ('depositFee' in transaction) else 'withdrawal' + fee = None + if feeCost is not None: + fee = { + 'cost': feeCost, + 'currency': code, + } + toAddress = self.safe_string(transaction, 'toAddress') + return { + 'info': transaction, + 'id': self.safe_string_2(transaction, 'id', 'withdrawId'), + 'txid': self.safe_string(transaction, 'txID'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': self.network_id_to_code(self.safe_string(transaction, 'chain')), + 'address': None, + 'addressTo': toAddress, + 'addressFrom': None, + 'tag': self.safe_string(transaction, 'tag'), + 'tagTo': None, + 'tagFrom': None, + 'type': type, + 'amount': self.safe_number(transaction, 'amount'), + 'currency': code, + 'status': status, + 'updated': updated, + 'fee': fee, + 'internal': None, + 'comment': None, + } + + async def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + + https://bybit-exchange.github.io/docs/v5/account/transaction-log + https://bybit-exchange.github.io/docs/v5/account/contract-transaction-log + + :param str [code]: unified currency code, default is None + :param int [since]: timestamp in ms of the earliest ledger entry, default is None + :param int [limit]: max number of ledger entries to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param str [params.subType]: if inverse will use v5/account/contract-transaction-log + :returns dict: a `ledger structure ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchLedger', 'paginate') + if paginate: + return await self.fetch_paginated_call_cursor('fetchLedger', code, since, limit, params, 'nextPageCursor', 'cursor', None, 50) + request: dict = { + # 'coin': currency['id'], + # 'currency': currency['id'], # alias + # 'start_date': self.iso8601(since), + # 'end_date': self.iso8601(until), + # 'wallet_fund_type': 'Deposit', # Withdraw, RealisedPNL, Commission, Refund, Prize, ExchangeOrderWithdraw, ExchangeOrderDeposit + # 'page': 1, + # 'limit': 20, # max 50 + # v5 transaction log + # 'accountType': '', Account Type. UNIFIED + # 'category': '', Product type. spot,linear,option + # 'currency': '', Currency + # 'baseCoin': '', BaseCoin. e.g., BTC of BTCPERP + # 'type': '', Types of transaction logs + # 'startTime': 0, The start timestamp(ms) + # 'endTime': 0, The end timestamp(ms) + # 'limit': 0, Limit for data size per page. [1, 50]. Default: 20 + # 'cursor': '', Cursor. Used for pagination + } + enableUnified = await self.is_unified_enabled() + currency = None + currencyKey = 'coin' + if enableUnified[1]: + currencyKey = 'currency' + if since is not None: + request['startTime'] = since + else: + if since is not None: + request['start_date'] = self.yyyymmdd(since) + if code is not None: + currency = self.currency(code) + request[currencyKey] = currency['id'] + if limit is not None: + request['limit'] = limit + subType = None + subType, params = self.handle_sub_type_and_params('fetchLedger', None, params) + response = None + if enableUnified[1]: + unifiedMarginStatus = self.safe_integer(self.options, 'unifiedMarginStatus', 5) # 3/4 uta 1.0, 5/6 uta 2.0 + if subType == 'inverse' and (unifiedMarginStatus < 5): + response = await self.privateGetV5AccountContractTransactionLog(self.extend(request, params)) + else: + response = await self.privateGetV5AccountTransactionLog(self.extend(request, params)) + else: + response = await self.privateGetV5AccountContractTransactionLog(self.extend(request, params)) + # + # { + # "ret_code": 0, + # "ret_msg": "ok", + # "ext_code": "", + # "result": { + # "data": [ + # { + # "id": 234467, + # "user_id": 1, + # "coin": "BTC", + # "wallet_id": 27913, + # "type": "Realized P&L", + # "amount": "-0.00000006", + # "tx_id": "", + # "address": "BTCUSD", + # "wallet_balance": "0.03000330", + # "exec_time": "2019-12-09T00:00:25.000Z", + # "cross_seq": 0 + # } + # ] + # }, + # "ext_info": null, + # "time_now": "1577481867.115552", + # "rate_limit_status": 119, + # "rate_limit_reset_ms": 1577481867122, + # "rate_limit": 120 + # } + # + # v5 transaction log + # + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "nextPageCursor": "21963%3A1%2C14954%3A1", + # "list": [ + # { + # "symbol": "XRPUSDT", + # "side": "Buy", + # "funding": "-0.003676", + # "orderLinkId": "", + # "orderId": "1672128000-8-592324-1-2", + # "fee": "0.00000000", + # "change": "-0.003676", + # "cashFlow": "0", + # "transactionTime": "1672128000000", + # "type": "SETTLEMENT", + # "feeRate": "0.0001", + # "size": "100", + # "qty": "100", + # "cashBalance": "5086.55825002", + # "currency": "USDT", + # "category": "linear", + # "tradePrice": "0.3676", + # "tradeId": "534c0003-4bf7-486f-aa02-78cee36825e4" + # }, + # { + # "symbol": "XRPUSDT", + # "side": "Buy", + # "funding": "", + # "orderLinkId": "linear-order", + # "orderId": "592b7e41-78fd-42e2-9aa3-91e1835ef3e1", + # "fee": "0.01908720", + # "change": "-0.0190872", + # "cashFlow": "0", + # "transactionTime": "1672121182224", + # "type": "TRADE", + # "feeRate": "0.0006", + # "size": "100", + # "qty": "88", + # "cashBalance": "5086.56192602", + # "currency": "USDT", + # "category": "linear", + # "tradePrice": "0.3615", + # "tradeId": "5184f079-88ec-54c7-8774-5173cafd2b4e" + # }, + # { + # "symbol": "XRPUSDT", + # "side": "Buy", + # "funding": "", + # "orderLinkId": "linear-order", + # "orderId": "592b7e41-78fd-42e2-9aa3-91e1835ef3e1", + # "fee": "0.00260280", + # "change": "-0.0026028", + # "cashFlow": "0", + # "transactionTime": "1672121182224", + # "type": "TRADE", + # "feeRate": "0.0006", + # "size": "12", + # "qty": "12", + # "cashBalance": "5086.58101322", + # "currency": "USDT", + # "category": "linear", + # "tradePrice": "0.3615", + # "tradeId": "8569c10f-5061-5891-81c4-a54929847eb3" + # } + # ] + # }, + # "retExtInfo": {}, + # "time": 1672132481405 + # } + # + data = self.add_pagination_cursor_to_result(response) + return self.parse_ledger(data, currency, since, limit) + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + # + # { + # "id": 234467, + # "user_id": 1, + # "coin": "BTC", + # "wallet_id": 27913, + # "type": "Realized P&L", + # "amount": "-0.00000006", + # "tx_id": "", + # "address": "BTCUSD", + # "wallet_balance": "0.03000330", + # "exec_time": "2019-12-09T00:00:25.000Z", + # "cross_seq": 0 + # } + # + # { + # "symbol": "XRPUSDT", + # "side": "Buy", + # "funding": "", + # "orderLinkId": "linear-order", + # "orderId": "592b7e41-78fd-42e2-9aa3-91e1835ef3e1", + # "fee": "0.00260280", + # "change": "-0.0026028", + # "cashFlow": "0", + # "transactionTime": "1672121182224", + # "type": "TRADE", + # "feeRate": "0.0006", + # "size": "12", + # "qty": "12", + # "cashBalance": "5086.58101322", + # "currency": "USDT", + # "category": "linear", + # "tradePrice": "0.3615", + # "tradeId": "8569c10f-5061-5891-81c4-a54929847eb3" + # } + # + currencyId = self.safe_string_2(item, 'coin', 'currency') + code = self.safe_currency_code(currencyId, currency) + currency = self.safe_currency(currencyId, currency) + amountString = self.safe_string_2(item, 'amount', 'change') + afterString = self.safe_string_2(item, 'wallet_balance', 'cashBalance') + direction = 'out' if Precise.string_lt(amountString, '0') else 'in' + before = None + after = None + amount = None + if afterString is not None and amountString is not None: + difference = amountString if (direction == 'out') else Precise.string_neg(amountString) + before = self.parse_to_numeric(Precise.string_add(afterString, difference)) + after = self.parse_to_numeric(afterString) + amount = self.parse_to_numeric(Precise.string_abs(amountString)) + timestamp = self.parse8601(self.safe_string(item, 'exec_time')) + if timestamp is None: + timestamp = self.safe_integer(item, 'transactionTime') + return self.safe_ledger_entry({ + 'info': item, + 'id': self.safe_string(item, 'id'), + 'direction': direction, + 'account': self.safe_string(item, 'wallet_id'), + 'referenceId': self.safe_string(item, 'tx_id'), + 'referenceAccount': None, + 'type': self.parse_ledger_entry_type(self.safe_string(item, 'type')), + 'currency': code, + 'amount': amount, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'before': before, + 'after': after, + 'status': 'ok', + 'fee': { + 'currency': code, + 'cost': self.safe_number(item, 'fee'), + }, + }, currency) + + def parse_ledger_entry_type(self, type): + types: dict = { + 'Deposit': 'transaction', + 'Withdraw': 'transaction', + 'RealisedPNL': 'trade', + 'Commission': 'fee', + 'Refund': 'cashback', + 'Prize': 'prize', # ? + 'ExchangeOrderWithdraw': 'transaction', + 'ExchangeOrderDeposit': 'transaction', + # v5 + 'TRANSFER_IN': 'transaction', + 'TRANSFER_OUT': 'transaction', + 'TRADE': 'trade', + 'SETTLEMENT': 'trade', + 'DELIVERY': 'trade', + 'LIQUIDATION': 'trade', + 'BONUS': 'Prize', + 'FEE_REFUND': 'cashback', + 'INTEREST': 'transaction', + 'CURRENCY_BUY': 'trade', + 'CURRENCY_SELL': 'trade', + } + return self.safe_string(types, type, type) + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://bybit-exchange.github.io/docs/v5/asset/withdraw + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.accountType]: 'UTA', 'FUND', 'FUND,UTA', and 'SPOT(for classic accounts only) + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + accountType = None + accounts = await self.is_unified_enabled() + isUta = accounts[1] + accountType, params = self.handle_option_and_params(params, 'withdraw', 'accountType') + if accountType is None: + accountType = 'UTA' if isUta else 'SPOT' + await self.load_markets() + self.check_address(address) + currency = self.currency(code) + request: dict = { + 'coin': currency['id'], + 'amount': self.number_to_string(amount), + 'address': address, + 'timestamp': self.milliseconds(), + 'accountType': accountType, + } + if tag is not None: + request['tag'] = tag + networkCode, query = self.handle_network_code_and_params(params) + networkId = self.network_code_to_id(networkCode) + if networkId is not None: + request['chain'] = networkId.upper() + response = await self.privatePostV5AssetWithdrawCreate(self.extend(request, query)) + # + # { + # "retCode": "0", + # "retMsg": "success", + # "result": { + # "id": "9377266" + # }, + # "retExtInfo": {}, + # "time": "1666892894902" + # } + # + result = self.safe_dict(response, 'result', {}) + return self.parse_transaction(result, currency) + + async def fetch_position(self, symbol: str, params={}) -> Position: + """ + fetch data on a single open contract trade position + + https://bybit-exchange.github.io/docs/v5/position + + :param str symbol: unified market symbol of the market the position is held in, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `position structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchPosition() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = None + type = None + type, params = self.get_bybit_type('fetchPosition', market, params) + request['category'] = type + response = await self.privateGetV5PositionList(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "nextPageCursor": "updateAt%3D1672279322668", + # "category": "linear", + # "list": [ + # { + # "symbol": "XRPUSDT", + # "leverage": "10", + # "avgPrice": "0.3615", + # "liqPrice": "0.0001", + # "riskLimitValue": "200000", + # "takeProfit": "", + # "positionValue": "36.15", + # "tpslMode": "Full", + # "riskId": 41, + # "trailingStop": "0", + # "unrealisedPnl": "-1.83", + # "markPrice": "0.3432", + # "cumRealisedPnl": "0.48805876", + # "positionMM": "0.381021", + # "createdTime": "1672121182216", + # "positionIdx": 0, + # "positionIM": "3.634521", + # "updatedTime": "1672279322668", + # "side": "Buy", + # "bustPrice": "", + # "size": "100", + # "positionStatus": "Normal", + # "stopLoss": "", + # "tradeMode": 0 + # } + # ] + # }, + # "retExtInfo": {}, + # "time": 1672280219169 + # } + # + result = self.safe_dict(response, 'result', {}) + positions = self.safe_list_2(result, 'list', 'dataList', []) + timestamp = self.safe_integer(response, 'time') + first = self.safe_dict(positions, 0, {}) + position = self.parse_position(first, market) + position['timestamp'] = timestamp + position['datetime'] = self.iso8601(timestamp) + return position + + async def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://bybit-exchange.github.io/docs/v5/position + + :param str[] symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: market type, ['swap', 'option', 'spot'] + :param str [params.subType]: market subType, ['linear', 'inverse'] + :param str [params.baseCoin]: Base coin. Supports linear, inverse & option + :param str [params.settleCoin]: Settle coin. Supports linear, inverse & option + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times + :returns dict[]: a list of `position structure ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchPositions', 'paginate') + if paginate: + return await self.fetch_paginated_call_cursor('fetchPositions', symbols, None, None, params, 'nextPageCursor', 'cursor', None, 200) + symbol = None + if (symbols is not None) and isinstance(symbols, list): + symbolsLength = len(symbols) + if symbolsLength > 1: + raise ArgumentsRequired(self.id + ' fetchPositions() does not accept an array with more than one symbol') + elif symbolsLength == 1: + symbol = symbols[0] + symbols = self.market_symbols(symbols) + elif symbols is not None: + symbol = symbols + symbols = [self.symbol(symbol)] + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + request['symbol'] = market['id'] + type = None + type, params = self.get_bybit_type('fetchPositions', market, params) + if type == 'linear' or type == 'inverse': + baseCoin = self.safe_string(params, 'baseCoin') + if type == 'linear': + if symbol is None and baseCoin is None: + defaultSettle = self.safe_string(self.options, 'defaultSettle', 'USDT') + settleCoin = self.safe_string(params, 'settleCoin', defaultSettle) + request['settleCoin'] = settleCoin + else: + # inverse + if symbol is None and baseCoin is None: + request['category'] = 'inverse' + if self.safe_integer(params, 'limit') is None: + request['limit'] = 200 # max limit + params = self.omit(params, ['type']) + request['category'] = type + response = await self.privateGetV5PositionList(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "Success", + # "result": { + # "nextPageCursor": "0%3A1657711949945%2C0%3A1657711949945", + # "category": "linear", + # "list": [ + # { + # "symbol": "ETHUSDT", + # "leverage": "10", + # "updatedTime": 1657711949945, + # "side": "Buy", + # "positionValue": "536.92500000", + # "takeProfit": "", + # "tpslMode": "Full", + # "riskId": 11, + # "trailingStop": "", + # "entryPrice": "1073.85000000", + # "unrealisedPnl": "", + # "markPrice": "1080.65000000", + # "size": "0.5000", + # "positionStatus": "normal", + # "stopLoss": "", + # "cumRealisedPnl": "-0.32215500", + # "positionMM": "2.97456450", + # "createdTime": 1657711949928, + # "positionIdx": 0, + # "positionIM": "53.98243950" + # } + # ] + # }, + # "time": 1657713693182 + # } + # + positions = self.add_pagination_cursor_to_result(response) + results = [] + for i in range(0, len(positions)): + rawPosition = positions[i] + if ('data' in rawPosition) and ('is_valid' in rawPosition): + # futures only + rawPosition = self.safe_dict(rawPosition, 'data') + results.append(self.parse_position(rawPosition)) + return self.filter_by_array_positions(results, 'symbol', symbols, False) + + def parse_position(self, position: dict, market: Market = None) -> Position: + # + # linear swap + # + # { + # "positionIdx": 0, + # "riskId": "11", + # "symbol": "ETHUSDT", + # "side": "Buy", + # "size": "0.10", + # "positionValue": "119.845", + # "entryPrice": "1198.45", + # "tradeMode": 1, + # "autoAddMargin": 0, + # "leverage": "4.2", + # "positionBalance": "28.58931118", + # "liqPrice": "919.10", + # "bustPrice": "913.15", + # "takeProfit": "0.00", + # "stopLoss": "0.00", + # "trailingStop": "0.00", + # "unrealisedPnl": "0.083", + # "createdTime": "1669097244192", + # "updatedTime": "1669413126190", + # "tpSlMode": "Full", + # "riskLimitValue": "900000", + # "activePrice": "0.00" + # } + # + # usdc + # { + # "symbol":"BTCPERP", + # "leverage":"1.00", + # "occClosingFee":"0.0000", + # "liqPrice":"", + # "positionValue":"30.8100", + # "takeProfit":"0.0", + # "riskId":"10001", + # "trailingStop":"0.0000", + # "unrealisedPnl":"0.0000", + # "createdAt":"1652451795305", + # "markPrice":"30809.41", + # "cumRealisedPnl":"0.0000", + # "positionMM":"0.1541", + # "positionIM":"30.8100", + # "updatedAt":"1652451795305", + # "tpSLMode":"UNKNOWN", + # "side":"Buy", + # "bustPrice":"", + # "deleverageIndicator":"0", + # "entryPrice":"30810.0", + # "size":"0.001", + # "sessionRPL":"0.0000", + # "positionStatus":"NORMAL", + # "sessionUPL":"-0.0006", + # "stopLoss":"0.0", + # "orderMargin":"0.0000", + # "sessionAvgPrice":"30810.0" + # } + # + # unified margin + # + # { + # "symbol": "ETHUSDT", + # "leverage": "10", + # "updatedTime": 1657711949945, + # "side": "Buy", + # "positionValue": "536.92500000", + # "takeProfit": "", + # "tpslMode": "Full", + # "riskId": 11, + # "trailingStop": "", + # "entryPrice": "1073.85000000", + # "unrealisedPnl": "", + # "markPrice": "1080.65000000", + # "size": "0.5000", + # "positionStatus": "normal", + # "stopLoss": "", + # "cumRealisedPnl": "-0.32215500", + # "positionMM": "2.97456450", + # "createdTime": 1657711949928, + # "positionIdx": 0, + # "positionIM": "53.98243950" + # } + # + # unified account + # + # { + # "symbol": "XRPUSDT", + # "leverage": "10", + # "avgPrice": "0.3615", + # "liqPrice": "0.0001", + # "riskLimitValue": "200000", + # "takeProfit": "", + # "positionValue": "36.15", + # "tpslMode": "Full", + # "riskId": 41, + # "trailingStop": "0", + # "unrealisedPnl": "-1.83", + # "markPrice": "0.3432", + # "cumRealisedPnl": "0.48805876", + # "positionMM": "0.381021", + # "createdTime": "1672121182216", + # "positionIdx": 0, + # "positionIM": "3.634521", + # "updatedTime": "1672279322668", + # "side": "Buy", + # "bustPrice": "", + # "size": "100", + # "positionStatus": "Normal", + # "stopLoss": "", + # "tradeMode": 0 + # } + # + # fetchPositionsHistory + # + # { + # symbol: 'XRPUSDT', + # orderType: 'Market', + # leverage: '10', + # updatedTime: '1712717265572', + # side: 'Sell', + # orderId: '071749f3-a9fa-427b-b5ca-27b2f52b81de', + # closedPnl: '-0.00049568', + # avgEntryPrice: '0.6045', + # qty: '3', + # cumEntryValue: '1.8135', + # createdTime: '1712717265566', + # orderPrice: '0.5744', + # closedSize: '3', + # avgExitPrice: '0.605', + # execType: 'Trade', + # fillCount: '1', + # cumExitValue: '1.815' + # } + # + closedSize = self.safe_string(position, 'closedSize') + isHistory = (closedSize is not None) + contract = self.safe_string(position, 'symbol') + market = self.safe_market(contract, market, None, 'contract') + size = Precise.string_abs(self.safe_string_2(position, 'size', 'qty')) + side = self.safe_string(position, 'side') + if side is not None: + if side == 'Buy': + side = 'short' if isHistory else 'long' + elif side == 'Sell': + side = 'long' if isHistory else 'short' + else: + side = None + notional = self.safe_string_2(position, 'positionValue', 'cumExitValue') + unrealisedPnl = self.omit_zero(self.safe_string(position, 'unrealisedPnl')) + initialMarginString = self.safe_string_2(position, 'positionIM', 'cumEntryValue') + maintenanceMarginString = self.safe_string(position, 'positionMM') + timestamp = self.safe_integer_n(position, ['createdTime', 'createdAt']) + lastUpdateTimestamp = self.parse8601(self.safe_string(position, 'updated_at')) + if lastUpdateTimestamp is None: + lastUpdateTimestamp = self.safe_integer_n(position, ['updatedTime', 'updatedAt', 'updatedTime']) + tradeMode = self.safe_integer(position, 'tradeMode', 0) + marginMode = None + if (not self.options['enableUnifiedAccount']) or (self.options['enableUnifiedAccount'] and market['inverse']): + # tradeMode would work for classic and UTA(inverse) + if not isHistory: # cannot tell marginMode for fetchPositionsHistory, and closedSize will only be defined for fetchPositionsHistory response + marginMode = 'isolated' if (tradeMode == 1) else 'cross' + collateralString = self.safe_string(position, 'positionBalance') + entryPrice = self.omit_zero(self.safe_string_n(position, ['entryPrice', 'avgPrice', 'avgEntryPrice'])) + markPrice = self.safe_string(position, 'markPrice') + liquidationPrice = self.omit_zero(self.safe_string(position, 'liqPrice')) + leverage = self.safe_string(position, 'leverage') + if liquidationPrice is not None: + if market['settle'] == 'USDC': + # (Entry price - Liq price) * Contracts + Maintenance Margin + (unrealised pnl) = Collateral + price = markPrice if self.safe_bool(self.options, 'useMarkPriceForPositionCollateral', False) else entryPrice + difference = Precise.string_abs(Precise.string_sub(price, liquidationPrice)) + collateralString = Precise.string_add(Precise.string_add(Precise.string_mul(difference, size), maintenanceMarginString), unrealisedPnl) + else: + bustPrice = self.safe_string(position, 'bustPrice') + if market['linear']: + # derived from the following formulas + # (Entry price - Bust price) * Contracts = Collateral + # (Entry price - Liq price) * Contracts = Collateral - Maintenance Margin + # Maintenance Margin = (Bust price - Liq price) x Contracts + maintenanceMarginPriceDifference = Precise.string_abs(Precise.string_sub(liquidationPrice, bustPrice)) + maintenanceMarginString = Precise.string_mul(maintenanceMarginPriceDifference, size) + # Initial Margin = Contracts x Entry Price / Leverage + if (entryPrice is not None) and (initialMarginString is None): + initialMarginString = Precise.string_div(Precise.string_mul(size, entryPrice), leverage) + else: + # Contracts * (1 / Entry price - 1 / Bust price) = Collateral + # Contracts * (1 / Entry price - 1 / Liq price) = Collateral - Maintenance Margin + # Maintenance Margin = Contracts * (1 / Liq price - 1 / Bust price) + # Maintenance Margin = Contracts * (Bust price - Liq price) / (Liq price x Bust price) + difference = Precise.string_abs(Precise.string_sub(bustPrice, liquidationPrice)) + multiply = Precise.string_mul(bustPrice, liquidationPrice) + maintenanceMarginString = Precise.string_div(Precise.string_mul(size, difference), multiply) + # Initial Margin = Leverage x Contracts / EntryPrice + if (entryPrice is not None) and (initialMarginString is None): + initialMarginString = Precise.string_div(size, Precise.string_mul(entryPrice, leverage)) + maintenanceMarginPercentage = Precise.string_div(maintenanceMarginString, notional) + marginRatio = Precise.string_div(maintenanceMarginString, collateralString, 4) + positionIdx = self.safe_string(position, 'positionIdx') + hedged = (positionIdx is not None) and (positionIdx != '0') + return self.safe_position({ + 'info': position, + 'id': None, + 'symbol': market['symbol'], + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastUpdateTimestamp': lastUpdateTimestamp, + 'initialMargin': self.parse_number(initialMarginString), + 'initialMarginPercentage': self.parse_number(Precise.string_div(initialMarginString, notional)), + 'maintenanceMargin': self.parse_number(maintenanceMarginString), + 'maintenanceMarginPercentage': self.parse_number(maintenanceMarginPercentage), + 'entryPrice': self.parse_number(entryPrice), + 'notional': self.parse_number(notional), + 'leverage': self.parse_number(leverage), + 'unrealizedPnl': self.parse_number(unrealisedPnl), + 'realizedPnl': self.safe_number(position, 'closedPnl'), + 'contracts': self.parse_number(size), # in USD for inverse swaps + 'contractSize': self.safe_number(market, 'contractSize'), + 'marginRatio': self.parse_number(marginRatio), + 'liquidationPrice': self.parse_number(liquidationPrice), + 'markPrice': self.parse_number(markPrice), + 'lastPrice': self.safe_number(position, 'avgExitPrice'), + 'collateral': self.parse_number(collateralString), + 'marginMode': marginMode, + 'side': side, + 'percentage': None, + 'stopLossPrice': self.safe_number_2(position, 'stop_loss', 'stopLoss'), + 'takeProfitPrice': self.safe_number_2(position, 'take_profit', 'takeProfit'), + 'hedged': hedged, + }) + + async def fetch_leverage(self, symbol: str, params={}) -> Leverage: + """ + fetch the set leverage for a market + + https://bybit-exchange.github.io/docs/v5/position + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `leverage structure ` + """ + await self.load_markets() + market = self.market(symbol) + position = await self.fetch_position(symbol, params) + return self.parse_leverage(position, market) + + def parse_leverage(self, leverage: dict, market: Market = None) -> Leverage: + marketId = self.safe_string(leverage, 'symbol') + leverageValue = self.safe_integer(leverage, 'leverage') + return { + 'info': leverage, + 'symbol': self.safe_symbol(marketId, market), + 'marginMode': self.safe_string_lower(leverage, 'marginMode'), + 'longLeverage': leverageValue, + 'shortLeverage': leverageValue, + } + + async def set_margin_mode(self, marginMode: str, symbol: Str = None, params={}): + """ + set margin mode(account) or trade mode(symbol) + + https://bybit-exchange.github.io/docs/v5/account/set-margin-mode + https://bybit-exchange.github.io/docs/v5/position/cross-isolate + + :param str marginMode: account mode must be either [isolated, cross, portfolio], trade mode must be either [isolated, cross] + :param str symbol: unified market symbol of the market the position is held in, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.leverage]: the rate of leverage, is required if setting trade mode(symbol) + :returns dict: response from the exchange + """ + await self.load_markets() + enableUnifiedMargin, enableUnifiedAccount = await self.is_unified_enabled() + isUnifiedAccount = (enableUnifiedMargin or enableUnifiedAccount) + market = None + response = None + if isUnifiedAccount: + if marginMode == 'isolated': + marginMode = 'ISOLATED_MARGIN' + elif marginMode == 'cross': + marginMode = 'REGULAR_MARGIN' + elif marginMode == 'portfolio': + marginMode = 'PORTFOLIO_MARGIN' + else: + raise NotSupported(self.id + ' setMarginMode() marginMode must be either [isolated, cross, portfolio]') + request: dict = { + 'setMarginMode': marginMode, + } + response = await self.privatePostV5AccountSetMarginMode(self.extend(request, params)) + else: + if symbol is None: + raise ArgumentsRequired(self.id + ' setMarginMode() requires a symbol parameter for non unified account') + market = self.market(symbol) + isUsdcSettled = market['settle'] == 'USDC' + if isUsdcSettled: + if marginMode == 'cross': + marginMode = 'REGULAR_MARGIN' + elif marginMode == 'portfolio': + marginMode = 'PORTFOLIO_MARGIN' + else: + raise NotSupported(self.id + ' setMarginMode() for usdc market marginMode must be either [cross, portfolio]') + request: dict = { + 'setMarginMode': marginMode, + } + response = await self.privatePostV5AccountSetMarginMode(self.extend(request, params)) + else: + type = None + type, params = self.get_bybit_type('setPositionMode', market, params) + tradeMode = None + if marginMode == 'cross': + tradeMode = 0 + elif marginMode == 'isolated': + tradeMode = 1 + else: + raise NotSupported(self.id + ' setMarginMode() with symbol marginMode must be either [isolated, cross]') + sellLeverage = None + buyLeverage = None + leverage = self.safe_string(params, 'leverage') + if leverage is None: + sellLeverage = self.safe_string_2(params, 'sell_leverage', 'sellLeverage') + buyLeverage = self.safe_string_2(params, 'buy_leverage', 'buyLeverage') + if sellLeverage is None and buyLeverage is None: + raise ArgumentsRequired(self.id + ' setMarginMode() requires a leverage parameter or sell_leverage and buy_leverage parameters') + if buyLeverage is None: + buyLeverage = sellLeverage + if sellLeverage is None: + sellLeverage = buyLeverage + params = self.omit(params, ['buy_leverage', 'sell_leverage', 'sellLeverage', 'buyLeverage']) + else: + sellLeverage = leverage + buyLeverage = leverage + params = self.omit(params, 'leverage') + request: dict = { + 'category': type, + 'symbol': market['id'], + 'tradeMode': tradeMode, + 'buyLeverage': buyLeverage, + 'sellLeverage': sellLeverage, + } + response = await self.privatePostV5PositionSwitchIsolated(self.extend(request, params)) + return response + + async def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://bybit-exchange.github.io/docs/v5/position/leverage + + :param float leverage: the rate of leverage + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.buyLeverage]: leverage for buy side + :param str [params.sellLeverage]: leverage for sell side + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setLeverage() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + # WARNING: THIS WILL INCREASE LIQUIDATION PRICE FOR OPEN ISOLATED LONG POSITIONS + # AND DECREASE LIQUIDATION PRICE FOR OPEN ISOLATED SHORT POSITIONS + # engage in leverage setting + # we reuse the code here instead of having two methods + leverageString = self.number_to_string(leverage) + request: dict = { + 'symbol': market['id'], + 'buyLeverage': leverageString, + 'sellLeverage': leverageString, + } + request['buyLeverage'] = leverageString + request['sellLeverage'] = leverageString + if market['linear']: + request['category'] = 'linear' + elif market['inverse']: + request['category'] = 'inverse' + else: + raise NotSupported(self.id + ' setLeverage() only support linear and inverse market') + response = await self.privatePostV5PositionSetLeverage(self.extend(request, params)) + return response + + async def set_position_mode(self, hedged: bool, symbol: Str = None, params={}): + """ + set hedged to True or False for a market + + https://bybit-exchange.github.io/docs/v5/position/position-mode + + :param bool hedged: + :param str symbol: used for unified account with inverse market + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + mode = None + if hedged: + mode = 3 + else: + mode = 0 + request: dict = { + 'mode': mode, + } + if symbol is None: + request['coin'] = 'USDT' + else: + request['symbol'] = market['id'] + if symbol is not None: + request['category'] = 'linear' if market['linear'] else 'inverse' + else: + type = None + type, params = self.get_bybit_type('setPositionMode', market, params) + request['category'] = type + params = self.omit(params, 'type') + response = await self.privatePostV5PositionSwitchMode(self.extend(request, params)) + # + # v5 + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": {}, + # "retExtInfo": {}, + # "time": 1675249072814 + # } + return response + + async def fetch_derivatives_open_interest_history(self, symbol: str, timeframe='1h', since: Int = None, limit: Int = None, params={}): + await self.load_markets() + market = self.market(symbol) + subType = 'linear' if market['linear'] else 'inverse' + category = self.safe_string(params, 'category', subType) + intervals = self.safe_dict(self.options, 'intervals') + interval = self.safe_string(intervals, timeframe) # 5min,15min,30min,1h,4h,1d + if interval is None: + raise BadRequest(self.id + ' fetchOpenInterestHistory() cannot use the ' + timeframe + ' timeframe') + request: dict = { + 'symbol': market['id'], + 'intervalTime': interval, + 'category': category, + } + if since is not None: + request['startTime'] = since + until = self.safe_integer(params, 'until') # unified in milliseconds + params = self.omit(params, ['until']) + if until is not None: + request['endTime'] = until + if limit is not None: + request['limit'] = limit + response = await self.publicGetV5MarketOpenInterest(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "symbol": "BTCUSD", + # "category": "inverse", + # "list": [ + # { + # "openInterest": "461134384.00000000", + # "timestamp": "1669571400000" + # }, + # { + # "openInterest": "461134292.00000000", + # "timestamp": "1669571100000" + # } + # ], + # "nextPageCursor": "" + # }, + # "retExtInfo": {}, + # "time": 1672053548579 + # } + # + result = self.safe_dict(response, 'result', {}) + data = self.add_pagination_cursor_to_result(response) + id = self.safe_string(result, 'symbol') + market = self.safe_market(id, market, None, 'contract') + return self.parse_open_interests_history(data, market, since, limit) + + async def fetch_open_interest(self, symbol: str, params={}): + """ + Retrieves the open interest of a derivative trading pair + + https://bybit-exchange.github.io/docs/v5/market/open-interest + + :param str symbol: Unified CCXT market symbol + :param dict [params]: exchange specific parameters + :param str [params.interval]: 5m, 15m, 30m, 1h, 4h, 1d + :param str [params.category]: "linear" or "inverse" + :returns dict} an open interest structure{@link https://docs.ccxt.com/#/?id=open-interest-structure: + """ + await self.load_markets() + market = self.market(symbol) + if not market['contract']: + raise BadRequest(self.id + ' fetchOpenInterest() supports contract markets only') + timeframe = self.safe_string(params, 'interval', '1h') + intervals = self.safe_dict(self.options, 'intervals') + interval = self.safe_string(intervals, timeframe) # 5min,15min,30min,1h,4h,1d + if interval is None: + raise BadRequest(self.id + ' fetchOpenInterest() cannot use the ' + timeframe + ' timeframe') + subType = 'linear' if market['linear'] else 'inverse' + category = self.safe_string(params, 'category', subType) + request: dict = { + 'symbol': market['id'], + 'intervalTime': interval, + 'category': category, + } + response = await self.publicGetV5MarketOpenInterest(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "symbol": "BTCUSD", + # "category": "inverse", + # "list": [ + # { + # "openInterest": "461134384.00000000", + # "timestamp": "1669571400000" + # }, + # { + # "openInterest": "461134292.00000000", + # "timestamp": "1669571100000" + # } + # ], + # "nextPageCursor": "" + # }, + # "retExtInfo": {}, + # "time": 1672053548579 + # } + # + result = self.safe_dict(response, 'result', {}) + id = self.safe_string(result, 'symbol') + market = self.safe_market(id, market, None, 'contract') + data = self.add_pagination_cursor_to_result(response) + return self.parse_open_interest(data[0], market) + + async def fetch_open_interest_history(self, symbol: str, timeframe='1h', since: Int = None, limit: Int = None, params={}): + """ + Gets the total amount of unsettled contracts. In other words, the total number of contracts held in open positions + + https://bybit-exchange.github.io/docs/v5/market/open-interest + + :param str symbol: Unified market symbol + :param str timeframe: "5m", 15m, 30m, 1h, 4h, 1d + :param int [since]: Not used by Bybit + :param int [limit]: The number of open interest structures to return. Max 200, default 50 + :param dict [params]: Exchange specific parameters + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns: An array of open interest structures + """ + if timeframe == '1m': + raise BadRequest(self.id + ' fetchOpenInterestHistory cannot use the 1m timeframe') + await self.load_markets() + paginate = self.safe_bool(params, 'paginate') + if paginate: + params = self.omit(params, 'paginate') + params['timeframe'] = timeframe + return await self.fetch_paginated_call_cursor('fetchOpenInterestHistory', symbol, since, limit, params, 'nextPageCursor', 'cursor', None, 200) + market = self.market(symbol) + if market['spot'] or market['option']: + raise BadRequest(self.id + ' fetchOpenInterestHistory() symbol does not support market ' + symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['limit'] = limit + return await self.fetch_derivatives_open_interest_history(symbol, timeframe, since, limit, params) + + def parse_open_interest(self, interest, market: Market = None): + # + # { + # "openInterest": 64757.62400000, + # "timestamp": 1665784800000, + # } + # + timestamp = self.safe_integer(interest, 'timestamp') + openInterest = self.safe_number_2(interest, 'open_interest', 'openInterest') + # the openInterest is in the base asset for linear and quote asset for inverse + amount = openInterest if market['linear'] else None + value = openInterest if market['inverse'] else None + return self.safe_open_interest({ + 'symbol': market['symbol'], + 'openInterestAmount': amount, + 'openInterestValue': value, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'info': interest, + }, market) + + async def fetch_cross_borrow_rate(self, code: str, params={}) -> CrossBorrowRate: + """ + fetch the rate of interest to borrow a currency for margin trading + + https://bybit-exchange.github.io/docs/zh-TW/v5/spot-margin-normal/interest-quota + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `borrow rate structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'coin': currency['id'], + } + response = await self.privateGetV5SpotCrossMarginTradeLoanInfo(self.extend(request, params)) + # + # { + # "retCode": "0", + # "retMsg": "success", + # "result": { + # "coin": "USDT", + # "interestRate": "0.000107000000", + # "loanAbleAmount": "", + # "maxLoanAmount": "79999.999" + # }, + # "retExtInfo": null, + # "time": "1666734490778" + # } + # + timestamp = self.safe_integer(response, 'time') + data = self.safe_dict(response, 'result', {}) + data['timestamp'] = timestamp + return self.parse_borrow_rate(data, currency) + + def parse_borrow_rate(self, info, currency: Currency = None): + # + # { + # "coin": "USDT", + # "interestRate": "0.000107000000", + # "loanAbleAmount": "", + # "maxLoanAmount": "79999.999", + # "timestamp": 1666734490778 + # } + # + # fetchBorrowRateHistory + # { + # "timestamp": 1721469600000, + # "currency": "USDC", + # "hourlyBorrowRate": "0.000014621596", + # "vipLevel": "No VIP" + # } + # + timestamp = self.safe_integer(info, 'timestamp') + currencyId = self.safe_string_2(info, 'coin', 'currency') + hourlyBorrowRate = self.safe_number(info, 'hourlyBorrowRate') + period = 3600000 if (hourlyBorrowRate is not None) else 86400000 # 1h or 1d + return { + 'currency': self.safe_currency_code(currencyId, currency), + 'rate': self.safe_number(info, 'interestRate', hourlyBorrowRate), + 'period': period, # Daily + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'info': info, + } + + async def fetch_borrow_interest(self, code: Str = None, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[BorrowInterest]: + """ + fetch the interest owed by the user for borrowing currency for margin trading + + https://bybit-exchange.github.io/docs/zh-TW/v5/spot-margin-normal/account-info + + :param str code: unified currency code + :param str symbol: unified market symbol when fetch interest in isolated markets + :param number [since]: the earliest time in ms to fetch borrrow interest for + :param number [limit]: the maximum number of structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `borrow interest structures ` + """ + await self.load_markets() + request: dict = {} + response = await self.privateGetV5SpotCrossMarginTradeAccount(self.extend(request, params)) + # + # { + # "ret_code": 0, + # "ret_msg": "", + # "ext_code": null, + # "ext_info": null, + # "result": { + # "status": "1", + # "riskRate": "0", + # "acctBalanceSum": "0.000486213817680857", + # "debtBalanceSum": "0", + # "loanAccountList": [ + # { + # "tokenId": "BTC", + # "total": "0.00048621", + # "locked": "0", + # "loan": "0", + # "interest": "0", + # "free": "0.00048621" + # }, + # ... + # ] + # } + # } + # + data = self.safe_dict(response, 'result', {}) + rows = self.safe_list(data, 'loanAccountList', []) + interest = self.parse_borrow_interests(rows, None) + return self.filter_by_currency_since_limit(interest, code, since, limit) + + async def fetch_borrow_rate_history(self, code: str, since: Int = None, limit: Int = None, params={}): + """ + retrieves a history of a currencies borrow interest rate at specific time slots + + https://bybit-exchange.github.io/docs/v5/spot-margin-uta/historical-interest + + :param str code: unified currency code + :param int [since]: timestamp for the earliest borrow rate + :param int [limit]: the maximum number of `borrow rate structures ` to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch entries for + :returns dict[]: an array of `borrow rate structures ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + } + if since is None: + since = self.milliseconds() - 86400000 * 30 # last 30 days + request['startTime'] = since + endTime = self.safe_integer_2(params, 'until', 'endTime') + params = self.omit(params, ['until']) + if endTime is None: + endTime = since + 86400000 * 30 # since + 30 days + request['endTime'] = endTime + response = await self.privateGetV5SpotMarginTradeInterestRateHistory(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "list": [ + # { + # "timestamp": 1721469600000, + # "currency": "USDC", + # "hourlyBorrowRate": "0.000014621596", + # "vipLevel": "No VIP" + # } + # ] + # }, + # "retExtInfo": "{}", + # "time": 1721899048991 + # } + # + data = self.safe_dict(response, 'result') + rows = self.safe_list(data, 'list', []) + return self.parse_borrow_rate_history(rows, code, since, limit) + + def parse_borrow_interest(self, info: dict, market: Market = None) -> BorrowInterest: + # + # { + # "tokenId": "BTC", + # "total": "0.00048621", + # "locked": "0", + # "loan": "0", + # "interest": "0", + # "free": "0.00048621" + # }, + # + return { + 'info': info, + 'symbol': None, + 'currency': self.safe_currency_code(self.safe_string(info, 'tokenId')), + 'interest': self.safe_number(info, 'interest'), + 'interestRate': None, + 'amountBorrowed': self.safe_number(info, 'loan'), + 'marginMode': 'cross', + 'timestamp': None, + 'datetime': None, + } + + async def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://bybit-exchange.github.io/docs/v5/asset/create-inter-transfer + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account to transfer from + :param str toAccount: account to transfer to + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.transferId]: UUID, which is unique across the platform + :returns dict: a `transfer structure ` + """ + await self.load_markets() + transferId = self.safe_string(params, 'transferId', self.uuid()) + accountTypes = self.safe_dict(self.options, 'accountsByType', {}) + fromId = self.safe_string(accountTypes, fromAccount, fromAccount) + toId = self.safe_string(accountTypes, toAccount, toAccount) + currency = self.currency(code) + amountToPrecision = self.currency_to_precision(code, amount) + request: dict = { + 'transferId': transferId, + 'fromAccountType': fromId, + 'toAccountType': toId, + 'coin': currency['id'], + 'amount': amountToPrecision, + } + response = await self.privatePostV5AssetTransferInterTransfer(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "success", + # "result": { + # "transferId": "4244af44-f3b0-4cf6-a743-b56560e987bc" + # }, + # "retExtInfo": {}, + # "time": 1666875857205 + # } + # + timestamp = self.safe_integer(response, 'time') + transfer = self.safe_dict(response, 'result', {}) + statusRaw = self.safe_string_n(response, ['retCode', 'retMsg']) + status = self.parse_transfer_status(statusRaw) + return self.extend(self.parse_transfer(transfer, currency), { + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'amount': self.parse_number(amountToPrecision), + 'fromAccount': fromAccount, + 'toAccount': toAccount, + 'status': status, + }) + + async def fetch_transfers(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[TransferEntry]: + """ + fetch a history of internal transfers made on an account + + https://bybit-exchange.github.io/docs/v5/asset/inter-transfer-list + + :param str code: unified currency code of the currency transferred + :param int [since]: the earliest time in ms to fetch transfers for + :param int [limit]: the maximum number of transfer structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch entries for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `transfer structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchTransfers', 'paginate') + if paginate: + return await self.fetch_paginated_call_cursor('fetchTransfers', code, since, limit, params, 'nextPageCursor', 'cursor', None, 50) + currency = None + request: dict = {} + if code is not None: + currency = self.safe_currency(code) + request['coin'] = currency['id'] + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + request, params = self.handle_until_option('endTime', request, params) + response = await self.privateGetV5AssetTransferQueryInterTransferList(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "success", + # "result": { + # "list": [ + # { + # "transferId": "selfTransfer_a1091cc7-9364-4b74-8de1-18f02c6f2d5c", + # "coin": "USDT", + # "amount": "5000", + # "fromAccountType": "SPOT", + # "toAccountType": "UNIFIED", + # "timestamp": "1667283263000", + # "status": "SUCCESS" + # } + # ], + # "nextPageCursor": "eyJtaW5JRCI6MTM1ODQ2OCwibWF4SUQiOjEzNTg0Njh9" + # }, + # "retExtInfo": {}, + # "time": 1670988271677 + # } + # + data = self.add_pagination_cursor_to_result(response) + return self.parse_transfers(data, currency, since, limit) + + async def borrow_cross_margin(self, code: str, amount: float, params={}): + """ + create a loan to borrow margin + + https://bybit-exchange.github.io/docs/v5/spot-margin-normal/borrow + + :param str code: unified currency code of the currency to borrow + :param float amount: the amount to borrow + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin loan structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'coin': currency['id'], + 'qty': self.currency_to_precision(code, amount), + } + response = await self.privatePostV5SpotCrossMarginTradeLoan(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "success", + # "result": { + # "transactId": "14143" + # }, + # "retExtInfo": null, + # "time": 1662617848970 + # } + # + result = self.safe_dict(response, 'result', {}) + transaction = self.parse_margin_loan(result, currency) + return self.extend(transaction, { + 'symbol': None, + 'amount': amount, + }) + + async def repay_cross_margin(self, code: str, amount, params={}): + """ + repay borrowed margin and interest + + https://bybit-exchange.github.io/docs/v5/spot-margin-normal/repay + + :param str code: unified currency code of the currency to repay + :param float amount: the amount to repay + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin loan structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'coin': currency['id'], + 'qty': self.number_to_string(amount), + } + response = await self.privatePostV5SpotCrossMarginTradeRepay(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "success", + # "result": { + # "repayId": "12128" + # }, + # "retExtInfo": null, + # "time": 1662618298452 + # } + # + result = self.safe_dict(response, 'result', {}) + transaction = self.parse_margin_loan(result, currency) + return self.extend(transaction, { + 'symbol': None, + 'amount': amount, + }) + + def parse_margin_loan(self, info, currency: Currency = None) -> dict: + # + # borrowCrossMargin + # + # { + # "transactId": "14143" + # } + # + # repayCrossMargin + # + # { + # "repayId": "12128" + # } + # + return { + 'id': self.safe_string_2(info, 'transactId', 'repayId'), + 'currency': self.safe_string(currency, 'code'), + 'amount': None, + 'symbol': None, + 'timestamp': None, + 'datetime': None, + 'info': info, + } + + def parse_transfer_status(self, status: Str) -> Str: + statuses: dict = { + '0': 'ok', + 'OK': 'ok', + 'SUCCESS': 'ok', + } + return self.safe_string(statuses, status, status) + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + # + # transfer + # + # { + # "transferId": "22c2bc11-ed5b-49a4-8647-c4e0f5f6f2b2" + # } + # + # fetchTransfers + # + # { + # "transferId": "e9c421c4-b010-4b16-abd6-106179f27702", + # "coin": "USDT", + # "amount": "8", + # "fromAccountType": "FUND", + # "toAccountType": "SPOT", + # "timestamp": "1666879426000", + # "status": "SUCCESS" + # } + # + currencyId = self.safe_string(transfer, 'coin') + timestamp = self.safe_integer(transfer, 'timestamp') + fromAccountId = self.safe_string(transfer, 'fromAccountType') + toAccountId = self.safe_string(transfer, 'toAccountType') + accountIds = self.safe_dict(self.options, 'accountsById', {}) + fromAccount = self.safe_string(accountIds, fromAccountId, fromAccountId) + toAccount = self.safe_string(accountIds, toAccountId, toAccountId) + return { + 'info': transfer, + 'id': self.safe_string(transfer, 'transferId'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'currency': self.safe_currency_code(currencyId, currency), + 'amount': self.safe_number(transfer, 'amount'), + 'fromAccount': fromAccount, + 'toAccount': toAccount, + 'status': self.parse_transfer_status(self.safe_string(transfer, 'status')), + } + + async def fetch_derivatives_market_leverage_tiers(self, symbol: str, params={}) -> List[LeverageTier]: + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if market['linear']: + request['category'] = 'linear' + elif market['inverse']: + request['category'] = 'inverse' + response = await self.publicGetV5MarketRiskLimit(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "category": "inverse", + # "list": [ + # { + # "id": 1, + # "symbol": "BTCUSD", + # "riskLimitValue": "150", + # "maintenanceMargin": "0.5", + # "initialMargin": "1", + # "isLowestRisk": 1, + # "maxLeverage": "100.00" + # }, + # .... + # ] + # }, + # "retExtInfo": {}, + # "time": 1672054488010 + # } + # + result = self.safe_dict(response, 'result') + tiers = self.safe_list(result, 'list') + return self.parse_market_leverage_tiers(tiers, market) + + async def fetch_market_leverage_tiers(self, symbol: str, params={}) -> List[LeverageTier]: + """ + retrieve information on the maximum leverage, and maintenance margin for trades of varying trade sizes for a single market + + https://bybit-exchange.github.io/docs/v5/market/risk-limit + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `leverage tiers structure ` + """ + await self.load_markets() + request: dict = {} + market = None + market = self.market(symbol) + if market['spot'] or market['option']: + raise BadRequest(self.id + ' fetchMarketLeverageTiers() symbol does not support market ' + symbol) + request['symbol'] = market['id'] + return await self.fetch_derivatives_market_leverage_tiers(symbol, params) + + def parse_trading_fee(self, fee: dict, market: Market = None) -> TradingFeeInterface: + # + # { + # "symbol": "ETHUSDT", + # "makerFeeRate": 0.001, + # "takerFeeRate": 0.001 + # } + # + marketId = self.safe_string(fee, 'symbol') + defaultType = market['type'] if (market is not None) else 'contract' + symbol = self.safe_symbol(marketId, market, None, defaultType) + return { + 'info': fee, + 'symbol': symbol, + 'maker': self.safe_number(fee, 'makerFeeRate'), + 'taker': self.safe_number(fee, 'takerFeeRate'), + 'percentage': None, + 'tierBased': None, + } + + async def fetch_trading_fee(self, symbol: str, params={}) -> TradingFeeInterface: + """ + fetch the trading fees for a market + + https://bybit-exchange.github.io/docs/v5/account/fee-rate + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `fee structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + category = None + category, params = self.get_bybit_type('fetchTradingFee', market, params) + request['category'] = category + response = await self.privateGetV5AccountFeeRate(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "list": [ + # { + # "symbol": "ETHUSDT", + # "takerFeeRate": "0.0006", + # "makerFeeRate": "0.0001" + # } + # ] + # }, + # "retExtInfo": {}, + # "time": 1676360412576 + # } + # + result = self.safe_dict(response, 'result', {}) + fees = self.safe_list(result, 'list', []) + first = self.safe_dict(fees, 0, {}) + return self.parse_trading_fee(first, market) + + async def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://bybit-exchange.github.io/docs/v5/account/fee-rate + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: market type, ['swap', 'option', 'spot'] + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + await self.load_markets() + type = None + type, params = self.handle_option_and_params(params, 'fetchTradingFees', 'type', 'future') + if type == 'spot': + raise NotSupported(self.id + ' fetchTradingFees() is not supported for spot market') + response = await self.privateGetV5AccountFeeRate(params) + # + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "list": [ + # { + # "symbol": "ETHUSDT", + # "takerFeeRate": "0.0006", + # "makerFeeRate": "0.0001" + # } + # ] + # }, + # "retExtInfo": {}, + # "time": 1676360412576 + # } + # + fees = self.safe_dict(response, 'result', {}) + fees = self.safe_list(fees, 'list', []) + result: dict = {} + for i in range(0, len(fees)): + fee = self.parse_trading_fee(fees[i]) + symbol = fee['symbol'] + result[symbol] = fee + return result + + def parse_deposit_withdraw_fee(self, fee, currency: Currency = None) -> Any: + # + # { + # "name": "BTC", + # "coin": "BTC", + # "remainAmount": "150", + # "chains": [ + # { + # "chainType": "BTC", + # "confirmation": "10000", + # "withdrawFee": "0.0005", + # "depositMin": "0.0005", + # "withdrawMin": "0.001", + # "chain": "BTC", + # "chainDeposit": "1", + # "chainWithdraw": "1", + # "minAccuracy": "8" + # } + # ] + # } + # + chains = self.safe_list(fee, 'chains', []) + chainsLength = len(chains) + result: dict = { + 'info': fee, + 'withdraw': { + 'fee': None, + 'percentage': None, + }, + 'deposit': { + 'fee': None, + 'percentage': None, + }, + 'networks': {}, + } + if chainsLength != 0: + for i in range(0, chainsLength): + chain = chains[i] + networkId = self.safe_string(chain, 'chain') + currencyCode = self.safe_string(currency, 'code') + networkCode = self.network_id_to_code(networkId, currencyCode) + result['networks'][networkCode] = { + 'deposit': {'fee': None, 'percentage': None}, + 'withdraw': {'fee': self.safe_number(chain, 'withdrawFee'), 'percentage': False}, + } + if chainsLength == 1: + result['withdraw']['fee'] = self.safe_number(chain, 'withdrawFee') + result['withdraw']['percentage'] = False + return result + + async def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}): + """ + fetch deposit and withdraw fees + + https://bybit-exchange.github.io/docs/v5/asset/coin-info + + :param str[] codes: list of unified currency codes + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `fee structures ` + """ + self.check_required_credentials() + await self.load_markets() + response = await self.privateGetV5AssetCoinQueryInfo(params) + # + # { + # "retCode": 0, + # "retMsg": "", + # "result": { + # "rows": [ + # { + # "name": "BTC", + # "coin": "BTC", + # "remainAmount": "150", + # "chains": [ + # { + # "chainType": "BTC", + # "confirmation": "10000", + # "withdrawFee": "0.0005", + # "depositMin": "0.0005", + # "withdrawMin": "0.001", + # "chain": "BTC", + # "chainDeposit": "1", + # "chainWithdraw": "1", + # "minAccuracy": "8" + # } + # ] + # } + # ] + # }, + # "retExtInfo": {}, + # "time": 1672194582264 + # } + # + data = self.safe_dict(response, 'result', {}) + rows = self.safe_list(data, 'rows', []) + return self.parse_deposit_withdraw_fees(rows, codes, 'coin') + + async def fetch_settlement_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical settlement records + + https://bybit-exchange.github.io/docs/v5/market/delivery-price + + :param str symbol: unified market symbol of the settlement history + :param int [since]: timestamp in ms + :param int [limit]: number of records + :param dict [params]: exchange specific params + :param str [params.type]: market type, ['swap', 'option', 'spot'] + :param str [params.subType]: market subType, ['linear', 'inverse'] + :returns dict[]: a list of [settlement history objects] + """ + await self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + type = None + type, params = self.get_bybit_type('fetchSettlementHistory', market, params) + if type == 'spot': + raise NotSupported(self.id + ' fetchSettlementHistory() is not supported for spot market') + request['category'] = type + if limit is not None: + request['limit'] = limit + response = await self.publicGetV5MarketDeliveryPrice(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "success", + # "result": { + # "category": "option", + # "nextPageCursor": "0%2C3", + # "list": [ + # { + # "symbol": "SOL-27JUN23-20-C", + # "deliveryPrice": "16.62258889", + # "deliveryTime": "1687852800000" + # }, + # ] + # }, + # "retExtInfo": {}, + # "time": 1689043527231 + # } + # + result = self.safe_dict(response, 'result', {}) + data = self.safe_list(result, 'list', []) + settlements = self.parse_settlements(data, market) + sorted = self.sort_by(settlements, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, market['symbol'], since, limit) + + async def fetch_my_settlement_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical settlement records of the user + + https://bybit-exchange.github.io/docs/v5/asset/delivery + + :param str symbol: unified market symbol of the settlement history + :param int [since]: timestamp in ms + :param int [limit]: number of records + :param dict [params]: exchange specific params + :param str [params.type]: market type, ['swap', 'option', 'spot'] + :param str [params.subType]: market subType, ['linear', 'inverse'] + :returns dict[]: a list of [settlement history objects] + """ + await self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + type = None + type, params = self.get_bybit_type('fetchMySettlementHistory', market, params) + if type == 'spot': + raise NotSupported(self.id + ' fetchMySettlementHistory() is not supported for spot market') + request['category'] = type + if limit is not None: + request['limit'] = limit + response = await self.privateGetV5AssetDeliveryRecord(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "success", + # "result": { + # "category": "option", + # "nextPageCursor": "0%2C3", + # "list": [ + # { + # "symbol": "SOL-27JUN23-20-C", + # "deliveryPrice": "16.62258889", + # "deliveryTime": "1687852800000", + # "side": "Buy", + # "strike": "20", + # "fee": "0.00000000", + # "position": "0.01", + # "deliveryRpl": "3.5" + # }, + # ] + # }, + # "retExtInfo": {}, + # "time": 1689043527231 + # } + # + result = self.safe_dict(response, 'result', {}) + data = self.safe_list(result, 'list', []) + settlements = self.parse_settlements(data, market) + sorted = self.sort_by(settlements, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, market['symbol'], since, limit) + + def parse_settlement(self, settlement, market): + # + # fetchSettlementHistory + # + # { + # "symbol": "SOL-27JUN23-20-C", + # "deliveryPrice": "16.62258889", + # "deliveryTime": "1687852800000" + # } + # + # fetchMySettlementHistory + # + # { + # "symbol": "SOL-27JUN23-20-C", + # "deliveryPrice": "16.62258889", + # "deliveryTime": "1687852800000", + # "side": "Buy", + # "strike": "20", + # "fee": "0.00000000", + # "position": "0.01", + # "deliveryRpl": "3.5" + # } + # + timestamp = self.safe_integer(settlement, 'deliveryTime') + marketId = self.safe_string(settlement, 'symbol') + return { + 'info': settlement, + 'symbol': self.safe_symbol(marketId, market), + 'price': self.safe_number(settlement, 'deliveryPrice'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + + def parse_settlements(self, settlements, market): + # + # fetchSettlementHistory + # + # [ + # { + # "symbol": "SOL-27JUN23-20-C", + # "deliveryPrice": "16.62258889", + # "deliveryTime": "1687852800000" + # } + # ] + # + # fetchMySettlementHistory + # + # [ + # { + # "symbol": "SOL-27JUN23-20-C", + # "deliveryPrice": "16.62258889", + # "deliveryTime": "1687852800000", + # "side": "Buy", + # "strike": "20", + # "fee": "0.00000000", + # "position": "0.01", + # "deliveryRpl": "3.5" + # } + # ] + # + result = [] + for i in range(0, len(settlements)): + result.append(self.parse_settlement(settlements[i], market)) + return result + + async def fetch_volatility_history(self, code: str, params={}): + """ + fetch the historical volatility of an option market based on an underlying asset + + https://bybit-exchange.github.io/docs/v5/market/iv + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.period]: the period in days to fetch the volatility for: 7,14,21,30,60,90,180,270 + :returns dict[]: a list of `volatility history objects ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'category': 'option', + 'baseCoin': currency['id'], + } + response = await self.publicGetV5MarketHistoricalVolatility(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "SUCCESS", + # "category": "option", + # "result": [ + # { + # "period": 7, + # "value": "0.23854072", + # "time": "1690574400000" + # } + # ] + # } + # + volatility = self.safe_list(response, 'result', []) + return self.parse_volatility_history(volatility) + + def parse_volatility_history(self, volatility): + # + # { + # "period": 7, + # "value": "0.23854072", + # "time": "1690574400000" + # } + # + result = [] + for i in range(0, len(volatility)): + entry = volatility[i] + timestamp = self.safe_integer(entry, 'time') + result.append({ + 'info': volatility, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'volatility': self.safe_number(entry, 'value'), + }) + return result + + async def fetch_greeks(self, symbol: str, params={}) -> Greeks: + """ + fetches an option contracts greeks, financial metrics used to measure the factors that affect the price of an options contract + + https://bybit-exchange.github.io/docs/api-explorer/v5/market/tickers + + :param str symbol: unified symbol of the market to fetch greeks for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `greeks structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'category': 'option', + } + response = await self.publicGetV5MarketTickers(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "SUCCESS", + # "result": { + # "category": "option", + # "list": [ + # { + # "symbol": "BTC-26JAN24-39000-C", + # "bid1Price": "3205", + # "bid1Size": "7.1", + # "bid1Iv": "0.5478", + # "ask1Price": "3315", + # "ask1Size": "1.98", + # "ask1Iv": "0.5638", + # "lastPrice": "3230", + # "highPrice24h": "3255", + # "lowPrice24h": "3200", + # "markPrice": "3273.02263032", + # "indexPrice": "36790.96", + # "markIv": "0.5577", + # "underlyingPrice": "37649.67254894", + # "openInterest": "19.67", + # "turnover24h": "170140.33875912", + # "volume24h": "4.56", + # "totalVolume": "22", + # "totalTurnover": "789305", + # "delta": "0.49640971", + # "gamma": "0.00004131", + # "vega": "69.08651675", + # "theta": "-24.9443226", + # "predictedDeliveryPrice": "0", + # "change24h": "0.18532111" + # } + # ] + # }, + # "retExtInfo": {}, + # "time": 1699584008326 + # } + # + timestamp = self.safe_integer(response, 'time') + result = self.safe_dict(response, 'result', {}) + data = self.safe_list(result, 'list', []) + greeks = self.parse_greeks(data[0], market) + return self.extend(greeks, { + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + + async def fetch_all_greeks(self, symbols: Strings = None, params={}) -> List[Greeks]: + """ + fetches all option contracts greeks, financial metrics used to measure the factors that affect the price of an options contract + + https://bybit-exchange.github.io/docs/api-explorer/v5/market/tickers + + :param str[] [symbols]: unified symbols of the markets to fetch greeks for, all markets are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.baseCoin]: the baseCoin of the symbol, default is BTC + :returns dict: a `greeks structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, True, True, True) + baseCoin = self.safe_string(params, 'baseCoin', 'BTC') + request: dict = { + 'category': 'option', + 'baseCoin': baseCoin, + } + market = None + if symbols is not None: + symbolsLength = len(symbols) + if symbolsLength == 1: + market = self.market(symbols[0]) + request['symbol'] = market['id'] + response = await self.publicGetV5MarketTickers(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "SUCCESS", + # "result": { + # "category": "option", + # "list": [ + # { + # "symbol": "BTC-26JAN24-39000-C", + # "bid1Price": "3205", + # "bid1Size": "7.1", + # "bid1Iv": "0.5478", + # "ask1Price": "3315", + # "ask1Size": "1.98", + # "ask1Iv": "0.5638", + # "lastPrice": "3230", + # "highPrice24h": "3255", + # "lowPrice24h": "3200", + # "markPrice": "3273.02263032", + # "indexPrice": "36790.96", + # "markIv": "0.5577", + # "underlyingPrice": "37649.67254894", + # "openInterest": "19.67", + # "turnover24h": "170140.33875912", + # "volume24h": "4.56", + # "totalVolume": "22", + # "totalTurnover": "789305", + # "delta": "0.49640971", + # "gamma": "0.00004131", + # "vega": "69.08651675", + # "theta": "-24.9443226", + # "predictedDeliveryPrice": "0", + # "change24h": "0.18532111" + # } + # ] + # }, + # "retExtInfo": {}, + # "time": 1699584008326 + # } + # + result = self.safe_dict(response, 'result', {}) + data = self.safe_list(result, 'list', []) + return self.parse_all_greeks(data, symbols) + + def parse_greeks(self, greeks: dict, market: Market = None) -> Greeks: + # + # { + # "symbol": "BTC-26JAN24-39000-C", + # "bid1Price": "3205", + # "bid1Size": "7.1", + # "bid1Iv": "0.5478", + # "ask1Price": "3315", + # "ask1Size": "1.98", + # "ask1Iv": "0.5638", + # "lastPrice": "3230", + # "highPrice24h": "3255", + # "lowPrice24h": "3200", + # "markPrice": "3273.02263032", + # "indexPrice": "36790.96", + # "markIv": "0.5577", + # "underlyingPrice": "37649.67254894", + # "openInterest": "19.67", + # "turnover24h": "170140.33875912", + # "volume24h": "4.56", + # "totalVolume": "22", + # "totalTurnover": "789305", + # "delta": "0.49640971", + # "gamma": "0.00004131", + # "vega": "69.08651675", + # "theta": "-24.9443226", + # "predictedDeliveryPrice": "0", + # "change24h": "0.18532111" + # } + # + marketId = self.safe_string(greeks, 'symbol') + symbol = self.safe_symbol(marketId, market) + return { + 'symbol': symbol, + 'timestamp': None, + 'datetime': None, + 'delta': self.safe_number(greeks, 'delta'), + 'gamma': self.safe_number(greeks, 'gamma'), + 'theta': self.safe_number(greeks, 'theta'), + 'vega': self.safe_number(greeks, 'vega'), + 'rho': None, + 'bidSize': self.safe_number(greeks, 'bid1Size'), + 'askSize': self.safe_number(greeks, 'ask1Size'), + 'bidImpliedVolatility': self.safe_number(greeks, 'bid1Iv'), + 'askImpliedVolatility': self.safe_number(greeks, 'ask1Iv'), + 'markImpliedVolatility': self.safe_number(greeks, 'markIv'), + 'bidPrice': self.safe_number(greeks, 'bid1Price'), + 'askPrice': self.safe_number(greeks, 'ask1Price'), + 'markPrice': self.safe_number(greeks, 'markPrice'), + 'lastPrice': self.safe_number(greeks, 'lastPrice'), + 'underlyingPrice': self.safe_number(greeks, 'underlyingPrice'), + 'info': greeks, + } + + async def fetch_my_liquidations(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Liquidation]: + """ + retrieves the users liquidated positions + + https://bybit-exchange.github.io/docs/api-explorer/v5/position/execution + + :param str [symbol]: unified CCXT market symbol + :param int [since]: the earliest time in ms to fetch liquidations for + :param int [limit]: the maximum number of liquidation structures to retrieve + :param dict [params]: exchange specific parameters for the exchange API endpoint + :param str [params.type]: market type, ['swap', 'option', 'spot'] + :param str [params.subType]: market subType, ['linear', 'inverse'] + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict: an array of `liquidation structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchMyLiquidations', 'paginate') + if paginate: + return await self.fetch_paginated_call_cursor('fetchMyLiquidations', symbol, since, limit, params, 'nextPageCursor', 'cursor', None, 100) + request: dict = { + 'execType': 'BustTrade', + } + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + type = None + type, params = self.get_bybit_type('fetchMyLiquidations', market, params) + request['category'] = type + if limit is not None: + request['limit'] = limit + if since is not None: + request['startTime'] = since + request, params = self.handle_until_option('endTime', request, params) + response = await self.privateGetV5ExecutionList(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "nextPageCursor": "132766%3A2%2C132766%3A2", + # "category": "linear", + # "list": [ + # { + # "symbol": "ETHPERP", + # "orderType": "Market", + # "underlyingPrice": "", + # "orderLinkId": "", + # "side": "Buy", + # "indexPrice": "", + # "orderId": "8c065341-7b52-4ca9-ac2c-37e31ac55c94", + # "stopOrderType": "UNKNOWN", + # "leavesQty": "0", + # "execTime": "1672282722429", + # "isMaker": False, + # "execFee": "0.071409", + # "feeRate": "0.0006", + # "execId": "e0cbe81d-0f18-5866-9415-cf319b5dab3b", + # "tradeIv": "", + # "blockTradeId": "", + # "markPrice": "1183.54", + # "execPrice": "1190.15", + # "markIv": "", + # "orderQty": "0.1", + # "orderPrice": "1236.9", + # "execValue": "119.015", + # "execType": "Trade", + # "execQty": "0.1" + # } + # ] + # }, + # "retExtInfo": {}, + # "time": 1672283754510 + # } + # + liquidations = self.add_pagination_cursor_to_result(response) + return self.parse_liquidations(liquidations, market, since, limit) + + def parse_liquidation(self, liquidation, market: Market = None) -> Liquidation: + # + # { + # "symbol": "ETHPERP", + # "orderType": "Market", + # "underlyingPrice": "", + # "orderLinkId": "", + # "side": "Buy", + # "indexPrice": "", + # "orderId": "8c065341-7b52-4ca9-ac2c-37e31ac55c94", + # "stopOrderType": "UNKNOWN", + # "leavesQty": "0", + # "execTime": "1672282722429", + # "isMaker": False, + # "execFee": "0.071409", + # "feeRate": "0.0006", + # "execId": "e0cbe81d-0f18-5866-9415-cf319b5dab3b", + # "tradeIv": "", + # "blockTradeId": "", + # "markPrice": "1183.54", + # "execPrice": "1190.15", + # "markIv": "", + # "orderQty": "0.1", + # "orderPrice": "1236.9", + # "execValue": "119.015", + # "execType": "Trade", + # "execQty": "0.1" + # } + # + marketId = self.safe_string(liquidation, 'symbol') + timestamp = self.safe_integer(liquidation, 'execTime') + contractsString = self.safe_string(liquidation, 'execQty') + contractSizeString = self.safe_string(market, 'contractSize') + priceString = self.safe_string(liquidation, 'execPrice') + baseValueString = Precise.string_mul(contractsString, contractSizeString) + quoteValueString = Precise.string_mul(baseValueString, priceString) + return self.safe_liquidation({ + 'info': liquidation, + 'symbol': self.safe_symbol(marketId, market, None, 'contract'), + 'contracts': self.parse_number(contractsString), + 'contractSize': self.parse_number(contractSizeString), + 'price': self.parse_number(priceString), + 'baseValue': self.parse_number(baseValueString), + 'quoteValue': self.parse_number(quoteValueString), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + + async def get_leverage_tiers_paginated(self, symbol: Str = None, params={}): + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + paginate = False + paginate, params = self.handle_option_and_params(params, 'getLeverageTiersPaginated', 'paginate') + if paginate: + return await self.fetch_paginated_call_cursor('getLeverageTiersPaginated', symbol, None, None, params, 'nextPageCursor', 'cursor', None, 100) + subType = None + subType, params = self.handle_sub_type_and_params('getLeverageTiersPaginated', market, params, 'linear') + request: dict = { + 'category': subType, + } + response = await self.publicGetV5MarketRiskLimit(self.extend(request, params)) + result = self.add_pagination_cursor_to_result(response) + first = self.safe_dict(result, 0) + total = len(result) + lastIndex = total - 1 + last = self.safe_dict(result, lastIndex) + cursorValue = self.safe_string(first, 'nextPageCursor') + last['info'] = { + 'nextPageCursor': cursorValue, + } + result[lastIndex] = last + return result + + async def fetch_leverage_tiers(self, symbols: Strings = None, params={}) -> LeverageTiers: + """ + retrieve information on the maximum leverage, for different trade sizes + + https://bybit-exchange.github.io/docs/v5/market/risk-limit + + :param str[] [symbols]: a list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.subType]: market subType, ['linear', 'inverse'], default is 'linear' + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict: a dictionary of `leverage tiers structures `, indexed by market symbols + """ + await self.load_markets() + market = None + symbol = None + if symbols is not None: + market = self.market(symbols[0]) + if market['spot']: + raise NotSupported(self.id + ' fetchLeverageTiers() is not supported for spot market') + symbol = market['symbol'] + data = await self.get_leverage_tiers_paginated(symbol, self.extend({'paginate': True, 'paginationCalls': 50}, params)) + symbols = self.market_symbols(symbols) + return self.parse_leverage_tiers(data, symbols, 'symbol') + + def parse_leverage_tiers(self, response, symbols: Strings = None, marketIdKey=None) -> LeverageTiers: + # + # [ + # { + # "id": 1, + # "symbol": "BTCUSD", + # "riskLimitValue": "150", + # "maintenanceMargin": "0.5", + # "initialMargin": "1", + # "isLowestRisk": 1, + # "maxLeverage": "100.00" + # } + # ] + # + tiers: dict = {} + marketIds = self.market_ids(symbols) + filteredResults = self.filter_by_array(response, marketIdKey, marketIds, False) + grouped = self.group_by(filteredResults, marketIdKey) + keys = list(grouped.keys()) + for i in range(0, len(keys)): + marketId = keys[i] + entry = grouped[marketId] + for j in range(0, len(entry)): + id = self.safe_integer(entry[j], 'id') + entry[j]['id'] = id + market = self.safe_market(marketId, None, None, 'contract') + symbol = market['symbol'] + tiers[symbol] = self.parse_market_leverage_tiers(self.sort_by(entry, 'id'), market) + return tiers + + def parse_market_leverage_tiers(self, info, market: Market = None) -> List[LeverageTier]: + # + # [ + # { + # "id": 1, + # "symbol": "BTCUSD", + # "riskLimitValue": "150", + # "maintenanceMargin": "0.5", + # "initialMargin": "1", + # "isLowestRisk": 1, + # "maxLeverage": "100.00" + # } + # ] + # + tiers = [] + for i in range(0, len(info)): + tier = info[i] + marketId = self.safe_string(info, 'symbol') + market = self.safe_market(marketId) + minNotional = self.parse_number('0') + if i != 0: + minNotional = self.safe_number(info[i - 1], 'riskLimitValue') + tiers.append({ + 'tier': self.safe_integer(tier, 'id'), + 'symbol': self.safe_symbol(marketId, market), + 'currency': market['settle'], + 'minNotional': minNotional, + 'maxNotional': self.safe_number(tier, 'riskLimitValue'), + 'maintenanceMarginRate': self.safe_number(tier, 'maintenanceMargin'), + 'maxLeverage': self.safe_number(tier, 'maxLeverage'), + 'info': tier, + }) + return tiers + + async def fetch_funding_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[FundingHistory]: + """ + fetch the history of funding payments paid and received on self account + + https://bybit-exchange.github.io/docs/api-explorer/v5/position/execution + + :param str [symbol]: unified market symbol + :param int [since]: the earliest time in ms to fetch funding history for + :param int [limit]: the maximum number of funding history structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict: a `funding history structure ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchFundingHistory', 'paginate') + if paginate: + return await self.fetch_paginated_call_cursor('fetchFundingHistory', symbol, since, limit, params, 'nextPageCursor', 'cursor', None, 100) + request: dict = { + 'execType': 'Funding', + } + market: Market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + type = None + type, params = self.get_bybit_type('fetchFundingHistory', market, params) + request['category'] = type + if symbol is not None: + request['symbol'] = market['id'] + if since is not None: + request['startTime'] = since + if limit is not None: + request['size'] = limit + else: + request['size'] = 100 + request, params = self.handle_until_option('endTime', request, params) + response = await self.privateGetV5ExecutionList(self.extend(request, params)) + fundings = self.add_pagination_cursor_to_result(response) + return self.parse_incomes(fundings, market, since, limit) + + def parse_income(self, income, market: Market = None) -> object: + # + # { + # "symbol": "XMRUSDT", + # "orderType": "UNKNOWN", + # "underlyingPrice": "", + # "orderLinkId": "", + # "orderId": "a11e5fe2-1dbf-4bab-a9b2-af80a14efc5d", + # "stopOrderType": "UNKNOWN", + # "execTime": "1710950400000", + # "feeCurrency": "", + # "createType": "", + # "feeRate": "-0.000761", + # "tradeIv": "", + # "blockTradeId": "", + # "markPrice": "136.79", + # "execPrice": "137.11", + # "markIv": "", + # "orderQty": "0", + # "orderPrice": "0", + # "execValue": "134.3678", + # "closedSize": "0", + # "execType": "Funding", + # "seq": "28097658790", + # "side": "Sell", + # "indexPrice": "", + # "leavesQty": "0", + # "isMaker": False, + # "execFee": "-0.10232512", + # "execId": "8d1ef156-4ec6-4445-9a6c-1c0c24dbd046", + # "marketUnit": "", + # "execQty": "0.98", + # "nextPageCursor": "5774437%3A0%2C5771289%3A0" + # } + # + marketId = self.safe_string(income, 'symbol') + market = self.safe_market(marketId, market, None, 'contract') + code = 'USDT' + if market['inverse']: + code = market['quote'] + timestamp = self.safe_integer(income, 'execTime') + return { + 'info': income, + 'symbol': self.safe_symbol(marketId, market, '-', 'swap'), + 'code': code, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': self.safe_string(income, 'execId'), + 'amount': self.safe_number(income, 'execFee'), + 'rate': self.safe_number(income, 'feeRate'), + } + + async def fetch_option(self, symbol: str, params={}) -> Option: + """ + fetches option data that is commonly found in an option chain + + https://bybit-exchange.github.io/docs/v5/market/tickers + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `option chain structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'category': 'option', + 'symbol': market['id'], + } + response = await self.publicGetV5MarketTickers(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "SUCCESS", + # "result": { + # "category": "option", + # "list": [ + # { + # "symbol": "BTC-27DEC24-55000-P", + # "bid1Price": "0", + # "bid1Size": "0", + # "bid1Iv": "0", + # "ask1Price": "0", + # "ask1Size": "0", + # "ask1Iv": "0", + # "lastPrice": "10980", + # "highPrice24h": "0", + # "lowPrice24h": "0", + # "markPrice": "11814.66756236", + # "indexPrice": "63838.92", + # "markIv": "0.8866", + # "underlyingPrice": "71690.55303594", + # "openInterest": "0.01", + # "turnover24h": "0", + # "volume24h": "0", + # "totalVolume": "2", + # "totalTurnover": "78719", + # "delta": "-0.23284954", + # "gamma": "0.0000055", + # "vega": "191.70757975", + # "theta": "-30.43617927", + # "predictedDeliveryPrice": "0", + # "change24h": "0" + # } + # ] + # }, + # "retExtInfo": {}, + # "time": 1711162003672 + # } + # + result = self.safe_dict(response, 'result', {}) + resultList = self.safe_list(result, 'list', []) + chain = self.safe_dict(resultList, 0, {}) + return self.parse_option(chain, None, market) + + async def fetch_option_chain(self, code: str, params={}) -> OptionChain: + """ + fetches data for an underlying asset that is commonly found in an option chain + + https://bybit-exchange.github.io/docs/v5/market/tickers + + :param str code: base currency to fetch an option chain for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `option chain structures ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'category': 'option', + 'baseCoin': currency['id'], + } + response = await self.publicGetV5MarketTickers(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "SUCCESS", + # "result": { + # "category": "option", + # "list": [ + # { + # "symbol": "BTC-27DEC24-55000-P", + # "bid1Price": "0", + # "bid1Size": "0", + # "bid1Iv": "0", + # "ask1Price": "0", + # "ask1Size": "0", + # "ask1Iv": "0", + # "lastPrice": "10980", + # "highPrice24h": "0", + # "lowPrice24h": "0", + # "markPrice": "11814.66756236", + # "indexPrice": "63838.92", + # "markIv": "0.8866", + # "underlyingPrice": "71690.55303594", + # "openInterest": "0.01", + # "turnover24h": "0", + # "volume24h": "0", + # "totalVolume": "2", + # "totalTurnover": "78719", + # "delta": "-0.23284954", + # "gamma": "0.0000055", + # "vega": "191.70757975", + # "theta": "-30.43617927", + # "predictedDeliveryPrice": "0", + # "change24h": "0" + # }, + # ] + # }, + # "retExtInfo": {}, + # "time": 1711162003672 + # } + # + result = self.safe_dict(response, 'result', {}) + resultList = self.safe_list(result, 'list', []) + return self.parse_option_chain(resultList, None, 'symbol') + + def parse_option(self, chain: dict, currency: Currency = None, market: Market = None) -> Option: + # + # { + # "symbol": "BTC-27DEC24-55000-P", + # "bid1Price": "0", + # "bid1Size": "0", + # "bid1Iv": "0", + # "ask1Price": "0", + # "ask1Size": "0", + # "ask1Iv": "0", + # "lastPrice": "10980", + # "highPrice24h": "0", + # "lowPrice24h": "0", + # "markPrice": "11814.66756236", + # "indexPrice": "63838.92", + # "markIv": "0.8866", + # "underlyingPrice": "71690.55303594", + # "openInterest": "0.01", + # "turnover24h": "0", + # "volume24h": "0", + # "totalVolume": "2", + # "totalTurnover": "78719", + # "delta": "-0.23284954", + # "gamma": "0.0000055", + # "vega": "191.70757975", + # "theta": "-30.43617927", + # "predictedDeliveryPrice": "0", + # "change24h": "0" + # } + # + marketId = self.safe_string(chain, 'symbol') + market = self.safe_market(marketId, market) + return { + 'info': chain, + 'currency': None, + 'symbol': market['symbol'], + 'timestamp': None, + 'datetime': None, + 'impliedVolatility': self.safe_number(chain, 'markIv'), + 'openInterest': self.safe_number(chain, 'openInterest'), + 'bidPrice': self.safe_number(chain, 'bid1Price'), + 'askPrice': self.safe_number(chain, 'ask1Price'), + 'midPrice': None, + 'markPrice': self.safe_number(chain, 'markPrice'), + 'lastPrice': self.safe_number(chain, 'lastPrice'), + 'underlyingPrice': self.safe_number(chain, 'underlyingPrice'), + 'change': self.safe_number(chain, 'change24h'), + 'percentage': None, + 'baseVolume': self.safe_number(chain, 'totalVolume'), + 'quoteVolume': None, + } + + async def fetch_positions_history(self, symbols: Strings = None, since: Int = None, limit: Int = None, params={}) -> List[Position]: + """ + fetches historical positions + + https://bybit-exchange.github.io/docs/v5/position/close-pnl + + :param str[] symbols: a list of unified market symbols + :param int [since]: timestamp in ms of the earliest position to fetch, params["until"] - since <= 7 days + :param int [limit]: the maximum amount of records to fetch, default=50, max=100 + :param dict params: extra parameters specific to the exchange api endpoint + :param int [params.until]: timestamp in ms of the latest position to fetch, params["until"] - since <= 7 days + :param str [params.subType]: 'linear' or 'inverse' + :returns dict[]: a list of `position structures ` + """ + await self.load_markets() + market = None + subType = None + symbolsLength = 0 + if symbols is not None: + symbolsLength = len(symbols) + if symbolsLength > 0: + market = self.market(symbols[0]) + until = self.safe_integer(params, 'until') + subType, params = self.handle_sub_type_and_params('fetchPositionsHistory', market, params, 'linear') + params = self.omit(params, 'until') + request: dict = { + 'category': subType, + } + if (symbols is not None) and (symbolsLength == 1): + request['symbol'] = market['id'] + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + if until is not None: + request['endTime'] = until + response = await self.privateGetV5PositionClosedPnl(self.extend(request, params)) + # + # { + # retCode: '0', + # retMsg: 'OK', + # result: { + # nextPageCursor: '071749f3-a9fa-427b-b5ca-27b2f52b81de%3A1712717265566520788%2C071749f3-a9fa-427b-b5ca-27b2f52b81de%3A1712717265566520788', + # category: 'linear', + # list: [ + # { + # symbol: 'XRPUSDT', + # orderType: 'Market', + # leverage: '10', + # updatedTime: '1712717265572', + # side: 'Sell', + # orderId: '071749f3-a9fa-427b-b5ca-27b2f52b81de', + # closedPnl: '-0.00049568', + # avgEntryPrice: '0.6045', + # qty: '3', + # cumEntryValue: '1.8135', + # createdTime: '1712717265566', + # orderPrice: '0.5744', + # closedSize: '3', + # avgExitPrice: '0.605', + # execType: 'Trade', + # fillCount: '1', + # cumExitValue: '1.815' + # } + # ] + # }, + # retExtInfo: {}, + # time: '1712717286073' + # } + # + result = self.safe_dict(response, 'result') + rawPositions = self.safe_list(result, 'list') + positions = self.parse_positions(rawPositions, symbols, params) + return self.filter_by_since_limit(positions, since, limit) + + async def fetch_convert_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies that can be converted + + https://bybit-exchange.github.io/docs/v5/asset/convert/convert-coin-list + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.accountType]: eb_convert_uta, eb_convert_spot, eb_convert_funding, eb_convert_inverse, or eb_convert_contract + :returns dict: an associative dictionary of currencies + """ + await self.load_markets() + accountType = None + enableUnifiedMargin, enableUnifiedAccount = await self.is_unified_enabled() + isUnifiedAccount = (enableUnifiedMargin or enableUnifiedAccount) + accountTypeDefault = 'eb_convert_uta' if isUnifiedAccount else 'eb_convert_spot' + accountType, params = self.handle_option_and_params(params, 'fetchConvertCurrencies', 'accountType', accountTypeDefault) + request: dict = { + 'accountType': accountType, + } + response = await self.privateGetV5AssetExchangeQueryCoinList(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "ok", + # "result": { + # "coins": [ + # { + # "coin": "MATIC", + # "fullName": "MATIC", + # "icon": "https://s1.bycsi.com/app/assets/token/0552ae79c535c3095fa18f7b377dd2e9.svg", + # "iconNight": "https://t1.bycsi.com/app/assets/token/f59301aef2d6ac2165c4c4603e672fb4.svg", + # "accuracyLength": 8, + # "coinType": "crypto", + # "balance": "0", + # "uBalance": "0", + # "timePeriod": 0, + # "singleFromMinLimit": "1.1", + # "singleFromMaxLimit": "20001", + # "singleToMinLimit": "0", + # "singleToMaxLimit": "0", + # "dailyFromMinLimit": "0", + # "dailyFromMaxLimit": "0", + # "dailyToMinLimit": "0", + # "dailyToMaxLimit": "0", + # "disableFrom": False, + # "disableTo": False + # }, + # ] + # }, + # "retExtInfo": {}, + # "time": 1727256416250 + # } + # + result: dict = {} + data = self.safe_dict(response, 'result', {}) + coins = self.safe_list(data, 'coins', []) + for i in range(0, len(coins)): + entry = coins[i] + id = self.safe_string(entry, 'coin') + disableFrom = self.safe_bool(entry, 'disableFrom') + disableTo = self.safe_bool(entry, 'disableTo') + inactive = (disableFrom or disableTo) + code = self.safe_currency_code(id) + result[code] = { + 'info': entry, + 'id': id, + 'code': code, + 'networks': None, + 'type': self.safe_string(entry, 'coinType'), + 'name': self.safe_string(entry, 'fullName'), + 'active': not inactive, + 'deposit': None, + 'withdraw': self.safe_number(entry, 'balance'), + 'fee': None, + 'precision': None, + 'limits': { + 'amount': { + 'min': self.safe_number(entry, 'singleFromMinLimit'), + 'max': self.safe_number(entry, 'singleFromMaxLimit'), + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + 'deposit': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + } + return result + + async def fetch_convert_quote(self, fromCode: str, toCode: str, amount: Num = None, params={}) -> Conversion: + """ + fetch a quote for converting from one currency to another + + https://bybit-exchange.github.io/docs/v5/asset/convert/apply-quote + + :param str fromCode: the currency that you want to sell and convert from + :param str toCode: the currency that you want to buy and convert into + :param float [amount]: how much you want to trade in units of the from currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.accountType]: eb_convert_uta, eb_convert_spot, eb_convert_funding, eb_convert_inverse, or eb_convert_contract + :returns dict: a `conversion structure ` + """ + await self.load_markets() + accountType = None + enableUnifiedMargin, enableUnifiedAccount = await self.is_unified_enabled() + isUnifiedAccount = (enableUnifiedMargin or enableUnifiedAccount) + accountTypeDefault = 'eb_convert_uta' if isUnifiedAccount else 'eb_convert_spot' + accountType, params = self.handle_option_and_params(params, 'fetchConvertQuote', 'accountType', accountTypeDefault) + request: dict = { + 'fromCoin': fromCode, + 'toCoin': toCode, + 'requestAmount': self.number_to_string(amount), + 'requestCoin': fromCode, + 'accountType': accountType, + } + response = await self.privatePostV5AssetExchangeQuoteApply(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "ok", + # "result": { + # "quoteTxId": "1010020692439481682687668224", + # "exchangeRate": "0.000015330836780000", + # "fromCoin": "USDT", + # "fromCoinType": "crypto", + # "toCoin": "BTC", + # "toCoinType": "crypto", + # "fromAmount": "10", + # "toAmount": "0.000153308367800000", + # "expiredTime": "1727257413353", + # "requestId": "" + # }, + # "retExtInfo": {}, + # "time": 1727257398375 + # } + # + data = self.safe_dict(response, 'result', {}) + fromCurrencyId = self.safe_string(data, 'fromCoin', fromCode) + fromCurrency = self.currency(fromCurrencyId) + toCurrencyId = self.safe_string(data, 'toCoin', toCode) + toCurrency = self.currency(toCurrencyId) + return self.parse_conversion(data, fromCurrency, toCurrency) + + async def create_convert_trade(self, id: str, fromCode: str, toCode: str, amount: Num = None, params={}) -> Conversion: + """ + convert from one currency to another + + https://bybit-exchange.github.io/docs/v5/asset/convert/confirm-quote + + :param str id: the id of the trade that you want to make + :param str fromCode: the currency that you want to sell and convert from + :param str toCode: the currency that you want to buy and convert into + :param float amount: how much you want to trade in units of the from currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `conversion structure ` + """ + await self.load_markets() + request: dict = { + 'quoteTxId': id, + } + response = await self.privatePostV5AssetExchangeConvertExecute(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "ok", + # "result": { + # "exchangeStatus": "processing", + # "quoteTxId": "1010020692439483803499737088" + # }, + # "retExtInfo": {}, + # "time": 1727257904969 + # } + # + data = self.safe_dict(response, 'result', {}) + return self.parse_conversion(data) + + async def fetch_convert_trade(self, id: str, code: Str = None, params={}) -> Conversion: + """ + fetch the data for a conversion trade + + https://bybit-exchange.github.io/docs/v5/asset/convert/get-convert-result + + :param str id: the id of the trade that you want to fetch + :param str [code]: the unified currency code of the conversion trade + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.accountType]: eb_convert_uta, eb_convert_spot, eb_convert_funding, eb_convert_inverse, or eb_convert_contract + :returns dict: a `conversion structure ` + """ + await self.load_markets() + accountType = None + enableUnifiedMargin, enableUnifiedAccount = await self.is_unified_enabled() + isUnifiedAccount = (enableUnifiedMargin or enableUnifiedAccount) + accountTypeDefault = 'eb_convert_uta' if isUnifiedAccount else 'eb_convert_spot' + accountType, params = self.handle_option_and_params(params, 'fetchConvertQuote', 'accountType', accountTypeDefault) + request: dict = { + 'quoteTxId': id, + 'accountType': accountType, + } + response = await self.privateGetV5AssetExchangeConvertResultQuery(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "ok", + # "result": { + # "result": { + # "accountType": "eb_convert_uta", + # "exchangeTxId": "1010020692439483803499737088", + # "userId": "100406395", + # "fromCoin": "USDT", + # "fromCoinType": "crypto", + # "fromAmount": "10", + # "toCoin": "BTC", + # "toCoinType": "crypto", + # "toAmount": "0.00015344889", + # "exchangeStatus": "success", + # "extInfo": {}, + # "convertRate": "0.000015344889", + # "createdAt": "1727257904726" + # } + # }, + # "retExtInfo": {}, + # "time": 1727258257216 + # } + # + data = self.safe_dict(response, 'result', {}) + result = self.safe_dict(data, 'result', {}) + fromCurrencyId = self.safe_string(result, 'fromCoin') + toCurrencyId = self.safe_string(result, 'toCoin') + fromCurrency = None + toCurrency = None + if fromCurrencyId is not None: + fromCurrency = self.currency(fromCurrencyId) + if toCurrencyId is not None: + toCurrency = self.currency(toCurrencyId) + return self.parse_conversion(result, fromCurrency, toCurrency) + + async def fetch_convert_trade_history(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Conversion]: + """ + fetch the users history of conversion trades + + https://bybit-exchange.github.io/docs/v5/asset/convert/get-convert-history + + :param str [code]: the unified currency code + :param int [since]: the earliest time in ms to fetch conversions for + :param int [limit]: the maximum number of conversion structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.accountType]: eb_convert_uta, eb_convert_spot, eb_convert_funding, eb_convert_inverse, or eb_convert_contract + :returns dict[]: a list of `conversion structures ` + """ + await self.load_markets() + request: dict = {} + if limit is not None: + request['limit'] = limit + response = await self.privateGetV5AssetExchangeQueryConvertHistory(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "ok", + # "result": { + # "list": [ + # { + # "accountType": "eb_convert_uta", + # "exchangeTxId": "1010020692439483803499737088", + # "userId": "100406395", + # "fromCoin": "USDT", + # "fromCoinType": "crypto", + # "fromAmount": "10", + # "toCoin": "BTC", + # "toCoinType": "crypto", + # "toAmount": "0.00015344889", + # "exchangeStatus": "success", + # "extInfo": {}, + # "convertRate": "0.000015344889", + # "createdAt": "1727257904726" + # } + # ] + # }, + # "retExtInfo": {}, + # "time": 1727258761874 + # } + # + data = self.safe_dict(response, 'result', {}) + dataList = self.safe_list(data, 'list', []) + return self.parse_conversions(dataList, code, 'fromCoin', 'toCoin', since, limit) + + def parse_conversion(self, conversion: dict, fromCurrency: Currency = None, toCurrency: Currency = None) -> Conversion: + # + # fetchConvertQuote + # + # { + # "quoteTxId": "1010020692439481682687668224", + # "exchangeRate": "0.000015330836780000", + # "fromCoin": "USDT", + # "fromCoinType": "crypto", + # "toCoin": "BTC", + # "toCoinType": "crypto", + # "fromAmount": "10", + # "toAmount": "0.000153308367800000", + # "expiredTime": "1727257413353", + # "requestId": "" + # } + # + # createConvertTrade + # + # { + # "exchangeStatus": "processing", + # "quoteTxId": "1010020692439483803499737088" + # } + # + # fetchConvertTrade, fetchConvertTradeHistory + # + # { + # "accountType": "eb_convert_uta", + # "exchangeTxId": "1010020692439483803499737088", + # "userId": "100406395", + # "fromCoin": "USDT", + # "fromCoinType": "crypto", + # "fromAmount": "10", + # "toCoin": "BTC", + # "toCoinType": "crypto", + # "toAmount": "0.00015344889", + # "exchangeStatus": "success", + # "extInfo": {}, + # "convertRate": "0.000015344889", + # "createdAt": "1727257904726" + # } + # + timestamp = self.safe_integer_2(conversion, 'expiredTime', 'createdAt') + fromCoin = self.safe_string(conversion, 'fromCoin') + fromCode = self.safe_currency_code(fromCoin, fromCurrency) + to = self.safe_string(conversion, 'toCoin') + toCode = self.safe_currency_code(to, toCurrency) + return { + 'info': conversion, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': self.safe_string_2(conversion, 'quoteTxId', 'exchangeTxId'), + 'fromCurrency': fromCode, + 'fromAmount': self.safe_number(conversion, 'fromAmount'), + 'toCurrency': toCode, + 'toAmount': self.safe_number(conversion, 'toAmount'), + 'price': None, + 'fee': None, + } + + async def fetch_long_short_ratio_history(self, symbol: Str = None, timeframe: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LongShortRatio]: + """ + fetches the long short ratio history for a unified market symbol + + https://bybit-exchange.github.io/docs/v5/market/long-short-ratio + + :param str symbol: unified symbol of the market to fetch the long short ratio for + :param str [timeframe]: the period for the ratio, default is 24 hours + :param int [since]: the earliest time in ms to fetch ratios for + :param int [limit]: the maximum number of long short ratio structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of `long short ratio structures ` + """ + await self.load_markets() + market = self.market(symbol) + type = None + type, params = self.get_bybit_type('fetchLongShortRatioHistory', market, params) + if type == 'spot' or type == 'option': + raise NotSupported(self.id + ' fetchLongShortRatioHistory() only support linear and inverse markets') + if timeframe is None: + timeframe = '1d' + request: dict = { + 'symbol': market['id'], + 'period': timeframe, + 'category': type, + } + if limit is not None: + request['limit'] = limit + response = await self.publicGetV5MarketAccountRatio(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "list": [ + # { + # "symbol": "BTCUSDT", + # "buyRatio": "0.5707", + # "sellRatio": "0.4293", + # "timestamp": "1729123200000" + # }, + # ] + # }, + # "retExtInfo": {}, + # "time": 1729147842516 + # } + # + result = self.safe_dict(response, 'result', {}) + data = self.safe_list(result, 'list', []) + return self.parse_long_short_ratio_history(data, market) + + def parse_long_short_ratio(self, info: dict, market: Market = None) -> LongShortRatio: + # + # { + # "symbol": "BTCUSDT", + # "buyRatio": "0.5707", + # "sellRatio": "0.4293", + # "timestamp": "1729123200000" + # } + # + marketId = self.safe_string(info, 'symbol') + timestamp = self.safe_integer_omit_zero(info, 'timestamp') + longString = self.safe_string(info, 'buyRatio') + shortString = self.safe_string(info, 'sellRatio') + return { + 'info': info, + 'symbol': self.safe_symbol(marketId, market, None, 'contract'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'timeframe': None, + 'longShortRatio': self.parse_to_numeric(Precise.string_div(longString, shortString)), + } + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = self.implode_hostname(self.urls['api'][api]) + '/' + path + if api == 'public': + if params: + url += '?' + self.rawencode(params) + elif api == 'private': + self.check_required_credentials() + isOpenapi = url.find('openapi') >= 0 + isV3UnifiedMargin = url.find('unified/v3') >= 0 + isV3Contract = url.find('contract/v3') >= 0 + isV5UnifiedAccount = url.find('v5') >= 0 + timestamp = str(self.nonce()) + if isOpenapi: + if params: + body = self.json(params) + else: + # self fix for PHP is required otherwise it generates + # '[]' on empty arrays even when forced to use objects + body = '{}' + payload = timestamp + self.apiKey + body + signature = self.hmac(self.encode(payload), self.encode(self.secret), hashlib.sha256, 'hex') + headers = { + 'Content-Type': 'application/json', + 'X-BAPI-API-KEY': self.apiKey, + 'X-BAPI-TIMESTAMP': timestamp, + 'X-BAPI-SIGN': signature, + } + elif isV3UnifiedMargin or isV3Contract or isV5UnifiedAccount: + headers = { + 'Content-Type': 'application/json', + 'X-BAPI-API-KEY': self.apiKey, + 'X-BAPI-TIMESTAMP': timestamp, + 'X-BAPI-RECV-WINDOW': str(self.options['recvWindow']), + } + if isV3UnifiedMargin or isV3Contract: + headers['X-BAPI-SIGN-TYPE'] = '2' + query = self.extend({}, params) + queryEncoded = self.rawencode(query) + auth_base = str(timestamp) + self.apiKey + str(self.options['recvWindow']) + authFull = None + if method == 'POST': + body = self.json(query) + authFull = auth_base + body + else: + authFull = auth_base + queryEncoded + url += '?' + queryEncoded + signature = None + if self.secret.find('PRIVATE KEY') > -1: + signature = self.rsa(authFull, self.secret, 'sha256') + else: + signature = self.hmac(self.encode(authFull), self.encode(self.secret), hashlib.sha256) + headers['X-BAPI-SIGN'] = signature + else: + query = self.extend(params, { + 'api_key': self.apiKey, + 'recv_window': self.options['recvWindow'], + 'timestamp': timestamp, + }) + sortedQuery = self.keysort(query) + auth = self.rawencode(sortedQuery, True) + signature = None + if self.secret.find('PRIVATE KEY') > -1: + signature = self.rsa(auth, self.secret, 'sha256') + else: + signature = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256) + if method == 'POST': + isSpot = url.find('spot') >= 0 + extendedQuery = self.extend(query, { + 'sign': signature, + }) + if isSpot: + body = self.urlencode(extendedQuery) + headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + } + else: + body = self.json(extendedQuery) + headers = { + 'Content-Type': 'application/json', + } + else: + url += '?' + self.rawencode(sortedQuery, True) + url += '&sign=' + signature + if method == 'POST': + brokerId = self.safe_string(self.options, 'brokerId') + if brokerId is not None: + headers['Referer'] = brokerId + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if not response: + return None # fallback to default error handler + # + # { + # "ret_code": 10001, + # "ret_msg": "ReadMapCB: expect {or n, but found \u0000, error " + + # "found in #0 byte of ...||..., bigger context " + + # "...||...", + # "ext_code": '', + # "ext_info": '', + # "result": null, + # "time_now": "1583934106.590436" + # } + # + # { + # "retCode":10001, + # "retMsg":"symbol params err", + # "result":{"symbol":"","bid":"","bidIv":"","bidSize":"","ask":"","askIv":"","askSize":"","lastPrice":"","openInterest":"","indexPrice":"","markPrice":"","markPriceIv":"","change24h":"","high24h":"","low24h":"","volume24h":"","turnover24h":"","totalVolume":"","totalTurnover":"","fundingRate":"","predictedFundingRate":"","nextFundingTime":"","countdownHour":"0","predictedDeliveryPrice":"","underlyingPrice":"","delta":"","gamma":"","vega":"","theta":""} + # } + # + errorCode = self.safe_string_2(response, 'ret_code', 'retCode') + if errorCode != '0': + if errorCode == '30084': + # not an error + # https://github.com/ccxt/ccxt/issues/11268 + # https://github.com/ccxt/ccxt/pull/11624 + # POST https://api.bybit.com/v2/private/position/switch-isolated 200 OK + # {"ret_code":30084,"ret_msg":"Isolated not modified","ext_code":"","ext_info":"","result":null,"time_now":"1642005219.937988","rate_limit_status":73,"rate_limit_reset_ms":1642005219894,"rate_limit":75} + return None + feedback = None + if errorCode == '10005' and url.find('order') < 0: + feedback = self.id + ' private api uses /user/v3/private/query-api to check if you have a unified account. The API key of user id must own one of permissions: "Account Transfer", "Subaccount Transfer", "Withdrawal" ' + body + else: + feedback = self.id + ' ' + body + if body.find('Withdraw address chain or destination tag are not equal') > -1: + feedback = feedback + '; You might also need to ensure the address is whitelisted' + self.throw_broadly_matched_exception(self.exceptions['broad'], body, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + raise ExchangeError(feedback) # unknown message + return None diff --git a/ccxt/async_support/cex.py b/ccxt/async_support/cex.py new file mode 100644 index 0000000..7c35b6b --- /dev/null +++ b/ccxt/async_support/cex.py @@ -0,0 +1,1742 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.cex import ImplicitAPI +import asyncio +import hashlib +from ccxt.base.types import Account, Any, Balances, Currencies, Currency, DepositAddress, Int, LedgerEntry, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, TradingFeeInterface, TradingFees, Transaction, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import NullResponse +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class cex(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(cex, self).describe(), { + 'id': 'cex', + 'name': 'CEX.IO', + 'countries': ['GB', 'EU', 'CY', 'RU'], + 'rateLimit': 300, # 200 req/min + 'pro': True, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, # has, but not through api + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createOrder': True, + 'createOrderWithTakeProfitAndStopLoss': False, + 'createOrderWithTakeProfitAndStopLossWs': False, + 'createPostOnlyOrder': False, + 'createReduceOnlyOrder': False, + 'createStopOrder': True, + 'createTriggerOrder': True, + 'fetchAccounts': True, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchClosedOrder': True, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': True, + 'fetchDepositsWithdrawals': True, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLedger': True, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrder': True, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrderBook': True, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFees': True, + 'fetchVolatilityHistory': False, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'repayMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'transfer': True, + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/27766442-8ddc33b0-5ed8-11e7-8b98-f786aef0f3c9.jpg', + 'api': { + 'public': 'https://trade.cex.io/api/spot/rest-public', + 'private': 'https://trade.cex.io/api/spot/rest', + }, + 'www': 'https://cex.io', + 'doc': 'https://trade.cex.io/docs/', + 'fees': [ + 'https://cex.io/fee-schedule', + 'https://cex.io/limits-commissions', + ], + 'referral': 'https://cex.io/r/0/up105393824/0/', + }, + 'api': { + 'public': { + 'get': {}, + 'post': { + 'get_server_time': 1, + 'get_pairs_info': 1, + 'get_currencies_info': 1, + 'get_processing_info': 10, + 'get_ticker': 1, + 'get_trade_history': 1, + 'get_order_book': 1, + 'get_candles': 1, + }, + }, + 'private': { + 'get': {}, + 'post': { + 'get_my_current_fee': 5, + 'get_fee_strategy': 1, + 'get_my_volume': 5, + 'do_create_account': 1, + 'get_my_account_status_v3': 5, + 'get_my_wallet_balance': 5, + 'get_my_orders': 5, + 'do_my_new_order': 1, + 'do_cancel_my_order': 1, + 'do_cancel_all_orders': 5, + 'get_order_book': 1, + 'get_candles': 1, + 'get_trade_history': 1, + 'get_my_transaction_history': 1, + 'get_my_funding_history': 5, + 'do_my_internal_transfer': 1, + 'get_processing_info': 10, + 'get_deposit_address': 5, + 'do_deposit_funds_from_wallet': 1, + 'do_withdrawal_funds_to_wallet': 1, + }, + }, + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': False, # todo check + 'GTD': True, + }, + 'hedged': False, + 'leverage': False, + 'marketBuyRequiresPrice': False, + 'marketBuyByCost': True, # todo check + 'selfTradePrevention': False, + 'trailing': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': None, + 'fetchOrder': None, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 1000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 100000, + 'daysBackCanceled': 1, + 'untilDays': 100000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 1000, + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'exact': {}, + 'broad': { + 'You have negative balance on following accounts': InsufficientFunds, + 'Mandatory parameter side should be one of BUY,SELL': BadRequest, + 'API orders from Main account are not allowed': BadRequest, + 'check failed': BadRequest, + 'Insufficient funds': InsufficientFunds, + 'Get deposit address for main account is not allowed': PermissionDenied, + 'Market Trigger orders are not allowed': BadRequest, # for some reason, triggerPrice does not work for market orders + 'key not passed or incorrect': AuthenticationError, + }, + }, + 'timeframes': { + '1m': '1m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1h', + '2h': '2h', + '4h': '4h', + '1d': '1d', + }, + 'options': { + 'networks': { + 'BTC': 'bitcoin', + 'ERC20': 'ERC20', + 'BSC20': 'binancesmartchain', + 'DOGE': 'dogecoin', + 'ALGO': 'algorand', + 'XLM': 'stellar', + 'ATOM': 'cosmos', + 'LTC': 'litecoin', + 'XRP': 'ripple', + 'FTM': 'fantom', + 'MINA': 'mina', + 'THETA': 'theta', + 'XTZ': 'tezos', + 'TIA': 'celestia', + 'CRONOS': 'cronos', # CRC20 + 'MATIC': 'polygon', + 'TON': 'ton', + 'TRC20': 'tron', + 'SOLANA': 'solana', + 'SGB': 'songbird', + 'DYDX': 'dydx', + 'DASH': 'dash', + 'ZIL': 'zilliqa', + 'EOS': 'eos', + 'AVALANCHEC': 'avalanche', + 'ETHPOW': 'ethereumpow', + 'NEAR': 'near', + 'ARB': 'arbitrum', + 'DOT': 'polkadot', + 'OPT': 'optimism', + 'INJ': 'injective', + 'ADA': 'cardano', + 'ONT': 'ontology', + 'ICP': 'icp', + 'KAVA': 'kava', + 'KSM': 'kusama', + 'SEI': 'sei', + # 'OSM': 'osmosis', + 'NEO': 'neo', + 'NEO3': 'neo3', + # 'TERRAOLD': 'terra', # tbd + # 'TERRA': 'terra2', # tbd + # 'EVER': 'everscale', # tbd + 'XDC': 'xdc', + }, + }, + }) + + async def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://trade.cex.io/docs/#rest-public-api-calls-currencies-info + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + promises = [] + promises.append(self.publicPostGetCurrenciesInfo(params)) + # + # { + # "ok": "ok", + # "data": [ + # { + # "currency": "ZAP", + # "fiat": False, + # "precision": "8", + # "walletPrecision": "6", + # "walletDeposit": True, + # "walletWithdrawal": True + # }, + # ... + # + promises.append(self.publicPostGetProcessingInfo(params)) + # + # { + # "ok": "ok", + # "data": { + # "ADA": { + # "name": "Cardano", + # "blockchains": { + # "cardano": { + # "type": "coin", + # "deposit": "enabled", + # "minDeposit": "1", + # "withdrawal": "enabled", + # "minWithdrawal": "5", + # "withdrawalFee": "1", + # "withdrawalFeePercent": "0", + # "depositConfirmations": "15" + # } + # } + # }, + # ... + # + responses = await asyncio.gather(*promises) + dataCurrencies = self.safe_list(responses[0], 'data', []) + dataNetworks = self.safe_dict(responses[1], 'data', {}) + currenciesIndexed = self.index_by(dataCurrencies, 'currency') + data = self.deep_extend(currenciesIndexed, dataNetworks) + return self.parse_currencies(self.to_array(data)) + + def parse_currency(self, rawCurrency: dict) -> Currency: + id = self.safe_string(rawCurrency, 'currency') + code = self.safe_currency_code(id) + type = 'fiat' if self.safe_bool(rawCurrency, 'fiat') else 'crypto' + currencyPrecision = self.parse_number(self.parse_precision(self.safe_string(rawCurrency, 'precision'))) + networks: dict = {} + rawNetworks = self.safe_dict(rawCurrency, 'blockchains', {}) + keys = list(rawNetworks.keys()) + for j in range(0, len(keys)): + networkId = keys[j] + rawNetwork = rawNetworks[networkId] + networkCode = self.network_id_to_code(networkId) + deposit = self.safe_string(rawNetwork, 'deposit') == 'enabled' + withdraw = self.safe_string(rawNetwork, 'withdrawal') == 'enabled' + networks[networkCode] = { + 'id': networkId, + 'network': networkCode, + 'margin': None, + 'deposit': deposit, + 'withdraw': withdraw, + 'active': None, + 'fee': self.safe_number(rawNetwork, 'withdrawalFee'), + 'precision': currencyPrecision, + 'limits': { + 'deposit': { + 'min': self.safe_number(rawNetwork, 'minDeposit'), + 'max': None, + }, + 'withdraw': { + 'min': self.safe_number(rawNetwork, 'minWithdrawal'), + 'max': None, + }, + }, + 'info': rawNetwork, + } + return self.safe_currency_structure({ + 'id': id, + 'code': code, + 'name': None, + 'type': type, + 'active': None, + 'deposit': self.safe_bool(rawCurrency, 'walletDeposit'), + 'withdraw': self.safe_bool(rawCurrency, 'walletWithdrawal'), + 'fee': None, + 'precision': currencyPrecision, + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + }, + 'networks': networks, + 'info': rawCurrency, + }) + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for ace + + https://trade.cex.io/docs/#rest-public-api-calls-pairs-info + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = await self.publicPostGetPairsInfo(params) + # + # { + # "ok": "ok", + # "data": [ + # { + # "base": "AI", + # "quote": "USD", + # "baseMin": "30", + # "baseMax": "2516000", + # "baseLotSize": "0.000001", + # "quoteMin": "10", + # "quoteMax": "1000000", + # "quoteLotSize": "0.01000000", + # "basePrecision": "6", + # "quotePrecision": "8", + # "pricePrecision": "4", + # "minPrice": "0.0377", + # "maxPrice": "19.5000" + # }, + # ... + # + data = self.safe_list(response, 'data', []) + return self.parse_markets(data) + + def parse_market(self, market: dict) -> Market: + baseId = self.safe_string(market, 'base') + base = self.safe_currency_code(baseId) + quoteId = self.safe_string(market, 'quote') + quote = self.safe_currency_code(quoteId) + id = base + '-' + quote # not actual id, but for self exchange we can use self abbreviation, because e.g. tickers have hyphen in between + symbol = base + '/' + quote + return self.safe_market_structure({ + 'id': id, + 'symbol': symbol, + 'base': base, + 'baseId': baseId, + 'quote': quote, + 'quoteId': quoteId, + 'settle': None, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'limits': { + 'amount': { + 'min': self.safe_number(market, 'baseMin'), + 'max': self.safe_number(market, 'baseMax'), + }, + 'price': { + 'min': self.safe_number(market, 'minPrice'), + 'max': self.safe_number(market, 'maxPrice'), + }, + 'cost': { + 'min': self.safe_number(market, 'quoteMin'), + 'max': self.safe_number(market, 'quoteMax'), + }, + 'leverage': { + 'min': None, + 'max': None, + }, + }, + 'precision': { + 'amount': self.safe_string(market, 'baseLotSize'), + 'price': self.parse_number(self.parse_precision(self.safe_string(market, 'pricePrecision'))), + # 'cost': self.parse_number(self.parse_precision(self.safe_string(market, 'quoteLotSize'))), # buggy, doesn't reflect their documentation + 'base': self.parse_number(self.parse_precision(self.safe_string(market, 'basePrecision'))), + 'quote': self.parse_number(self.parse_precision(self.safe_string(market, 'quotePrecision'))), + }, + 'active': None, + 'created': None, + 'info': market, + }) + + async def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = await self.publicPostGetServerTime(params) + # + # { + # "ok": "ok", + # "data": { + # "timestamp": "1728472063472", + # "ISODate": "2024-10-09T11:07:43.472Z" + # } + # } + # + data = self.safe_dict(response, 'data') + timestamp = self.safe_integer(data, 'timestamp') + return timestamp + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://trade.cex.io/docs/#rest-public-api-calls-ticker + + :param str symbol: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + response = await self.fetch_tickers([symbol], params) + return self.safe_dict(response, symbol, {}) + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://trade.cex.io/docs/#rest-public-api-calls-ticker + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + request = {} + if symbols is not None: + request['pairs'] = self.market_ids(symbols) + response = await self.publicPostGetTicker(self.extend(request, params)) + # + # { + # "ok": "ok", + # "data": { + # "AI-USD": { + # "bestBid": "0.3917", + # "bestAsk": "0.3949", + # "bestBidChange": "0.0035", + # "bestBidChangePercentage": "0.90", + # "bestAskChange": "0.0038", + # "bestAskChangePercentage": "0.97", + # "low": "0.3787", + # "high": "0.3925", + # "volume30d": "2945.722277", + # "lastTradeDateISO": "2024-10-11T06:18:42.077Z", + # "volume": "120.736000", + # "quoteVolume": "46.65654070", + # "lastTradeVolume": "67.914000", + # "volumeUSD": "46.65", + # "last": "0.3949", + # "lastTradePrice": "0.3925", + # "priceChange": "0.0038", + # "priceChangePercentage": "0.97" + # }, + # ... + # + data = self.safe_dict(response, 'data', {}) + return self.parse_tickers(data, symbols) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + marketId = self.safe_string(ticker, 'id') + symbol = self.safe_symbol(marketId, market) + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': None, + 'datetime': None, + 'high': self.safe_number(ticker, 'high'), + 'low': self.safe_number(ticker, 'low'), + 'bid': self.safe_number(ticker, 'bestBid'), + 'bidVolume': None, + 'ask': self.safe_number(ticker, 'bestAsk'), + 'askVolume': None, + 'vwap': None, + 'open': None, + 'close': self.safe_string(ticker, 'last'), # last indicative price per api docs(difference also seen here: https://github.com/ccxt/ccxt/actions/runs/14593899575/job/40935513901?pr=25767#step:11:456 ) + 'previousClose': None, + 'change': self.safe_number(ticker, 'priceChange'), + 'percentage': self.safe_number(ticker, 'priceChangePercentage'), + 'average': None, + 'baseVolume': self.safe_string(ticker, 'volume'), + 'quoteVolume': self.safe_string(ticker, 'quoteVolume'), + 'info': ticker, + }, market) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://trade.cex.io/docs/#rest-public-api-calls-trade-history + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest entry + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + if since is not None: + request['fromDateISO'] = self.iso8601(since) + until = None + until, params = self.handle_param_integer_2(params, 'until', 'till') + if until is not None: + request['toDateISO'] = self.iso8601(until) + if limit is not None: + request['pageSize'] = min(limit, 10000) # has a bug, still returns more trades + response = await self.publicPostGetTradeHistory(self.extend(request, params)) + # + # { + # "ok": "ok", + # "data": { + # "pageSize": "10", + # "trades": [ + # { + # "tradeId": "1728630559823-0", + # "dateISO": "2024-10-11T07:09:19.823Z", + # "side": "SELL", + # "price": "60879.5", + # "amount": "0.00165962" + # }, + # ... followed by older trades + # + data = self.safe_dict(response, 'data', {}) + trades = self.safe_list(data, 'trades', []) + return self.parse_trades(trades, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # public fetchTrades + # + # { + # "tradeId": "1728630559823-0", + # "dateISO": "2024-10-11T07:09:19.823Z", + # "side": "SELL", + # "price": "60879.5", + # "amount": "0.00165962" + # }, + # + dateStr = self.safe_string(trade, 'dateISO') + timestamp = self.parse8601(dateStr) + market = self.safe_market(None, market) + return self.safe_trade({ + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'id': self.safe_string(trade, 'tradeId'), + 'order': None, + 'type': None, + 'takerOrMaker': None, + 'side': self.safe_string_lower(trade, 'side'), + 'price': self.safe_string(trade, 'price'), + 'amount': self.safe_string(trade, 'amount'), + 'cost': None, + 'fee': None, + }, market) + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://trade.cex.io/docs/#rest-public-api-calls-order-book + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + response = await self.publicPostGetOrderBook(self.extend(request, params)) + # + # { + # "ok": "ok", + # "data": { + # "timestamp": "1728636922648", + # "currency1": "BTC", + # "currency2": "USDT", + # "bids": [ + # [ + # "60694.1", + # "13.12849761" + # ], + # [ + # "60694.0", + # "0.71829244" + # ], + # ... + # + orderBook = self.safe_dict(response, 'data', {}) + timestamp = self.safe_integer(orderBook, 'timestamp') + return self.parse_order_book(orderBook, market['symbol'], timestamp) + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://trade.cex.io/docs/#rest-public-api-calls-candles + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest entry + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + dataType = None + dataType, params = self.handle_option_and_params(params, 'fetchOHLCV', 'dataType') + if dataType is None: + raise ArgumentsRequired(self.id + ' fetchOHLCV requires a parameter "dataType" to be either "bestBid" or "bestAsk"') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + 'resolution': self.timeframes[timeframe], + 'dataType': dataType, + } + if since is not None: + request['fromISO'] = self.iso8601(since) + until = None + until, params = self.handle_param_integer_2(params, 'until', 'till') + if until is not None: + request['toISO'] = self.iso8601(until) + elif since is None: + # exchange still requires that we provide one of them + request['toISO'] = self.iso8601(self.milliseconds()) + if since is not None and until is not None and limit is not None: + raise ArgumentsRequired(self.id + ' fetchOHLCV does not support fetching candles with both a limit and since/until') + elif (since is not None or until is not None) and limit is None: + raise ArgumentsRequired(self.id + ' fetchOHLCV requires a limit parameter when fetching candles with since or until') + if limit is not None: + request['limit'] = limit + response = await self.publicPostGetCandles(self.extend(request, params)) + # + # { + # "ok": "ok", + # "data": [ + # { + # "timestamp": "1728643320000", + # "open": "61061", + # "high": "61095.1", + # "low": "61048.5", + # "close": "61087.8", + # "volume": "0", + # "resolution": "1m", + # "isClosed": True, + # "timestampISO": "2024-10-11T10:42:00.000Z" + # }, + # ... + # + data = self.safe_list(response, 'data', []) + return self.parse_ohlcvs(data, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + return [ + self.safe_integer(ohlcv, 'timestamp'), + self.safe_number(ohlcv, 'open'), + self.safe_number(ohlcv, 'high'), + self.safe_number(ohlcv, 'low'), + self.safe_number(ohlcv, 'close'), + self.safe_number(ohlcv, 'volume'), + ] + + async def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://trade.cex.io/docs/#rest-public-api-calls-candles + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + await self.load_markets() + response = await self.privatePostGetMyCurrentFee(params) + # + # { + # "ok": "ok", + # "data": { + # "tradingFee": { + # "AI-USD": { + # "percent": "0.25" + # }, + # ... + # + data = self.safe_dict(response, 'data', {}) + fees = self.safe_dict(data, 'tradingFee', {}) + return self.parse_trading_fees(fees, True) + + def parse_trading_fees(self, response, useKeyAsId=False) -> TradingFees: + result: dict = {} + keys = list(response.keys()) + for i in range(0, len(keys)): + key = keys[i] + market = None + if useKeyAsId: + market = self.safe_market(key) + parsed = self.parse_trading_fee(response[key], market) + result[parsed['symbol']] = parsed + for i in range(0, len(self.symbols)): + symbol = self.symbols[i] + if not (symbol in result): + market = self.market(symbol) + result[symbol] = self.parse_trading_fee(response, market) + return result + + def parse_trading_fee(self, fee: dict, market: Market = None) -> TradingFeeInterface: + return { + 'info': fee, + 'symbol': self.safe_string(market, 'symbol'), + 'maker': self.safe_number(fee, 'percent'), + 'taker': self.safe_number(fee, 'percent'), + 'percentage': None, + 'tierBased': None, + } + + async def fetch_accounts(self, params={}) -> List[Account]: + await self.load_markets() + response = await self.privatePostGetMyAccountStatusV3(params) + # + # { + # "ok": "ok", + # "data": { + # "convertedCurrency": "USD", + # "balancesPerAccounts": { + # "": { + # "AI": { + # "balance": "0.000000", + # "balanceOnHold": "0.000000" + # }, + # "USDT": { + # "balance": "0.00000000", + # "balanceOnHold": "0.00000000" + # } + # } + # } + # } + # } + # + data = self.safe_dict(response, 'data', {}) + balances = self.safe_dict(data, 'balancesPerAccounts', {}) + arrays = self.to_array(balances) + return self.parse_accounts(arrays, params) + + def parse_account(self, account: dict) -> Account: + return { + 'id': None, + 'type': None, + 'code': None, + 'info': account, + } + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://trade.cex.io/docs/#rest-private-api-calls-account-status-v3 + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param dict [params.method]: 'privatePostGetMyWalletBalance' or 'privatePostGetMyAccountStatusV3' + :param dict [params.account]: in case 'privatePostGetMyAccountStatusV3' is chosen, self can specify the account name(default is empty string) + :returns dict: a `balance structure ` + """ + accountName = None + accountName, params = self.handle_param_string(params, 'account', '') # default is empty string + method = None + method, params = self.handle_param_string(params, 'method', 'privatePostGetMyWalletBalance') + accountBalance = None + if method == 'privatePostGetMyAccountStatusV3': + response = await self.privatePostGetMyAccountStatusV3(params) + # + # { + # "ok": "ok", + # "data": { + # "convertedCurrency": "USD", + # "balancesPerAccounts": { + # "": { + # "AI": { + # "balance": "0.000000", + # "balanceOnHold": "0.000000" + # }, + # .... + # + data = self.safe_dict(response, 'data', {}) + balances = self.safe_dict(data, 'balancesPerAccounts', {}) + accountBalance = self.safe_dict(balances, accountName, {}) + else: + response = await self.privatePostGetMyWalletBalance(params) + # + # { + # "ok": "ok", + # "data": { + # "AI": { + # "balance": "25.606429" + # }, + # "USDT": { + # "balance": "7.935449" + # }, + # ... + # + accountBalance = self.safe_dict(response, 'data', {}) + return self.parse_balance(accountBalance) + + def parse_balance(self, response) -> Balances: + result: dict = { + 'info': response, + } + keys = list(response.keys()) + for i in range(0, len(keys)): + key = keys[i] + balance = self.safe_dict(response, key, {}) + code = self.safe_currency_code(key) + account: dict = { + 'used': self.safe_string(balance, 'balanceOnHold'), + 'total': self.safe_string(balance, 'balance'), + } + result[code] = account + return self.safe_balance(result) + + async def fetch_orders_by_status(self, status: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://trade.cex.io/docs/#rest-private-api-calls-orders + + :param str status: order status to fetch for + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest entry + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + request: dict = {} + isClosedOrders = (status == 'closed') + if isClosedOrders: + request['archived'] = True + market = None + if symbol is not None: + market = self.market(symbol) + request['pair'] = market['id'] + if limit is not None: + request['pageSize'] = limit + if since is not None: + request['serverCreateTimestampFrom'] = since + elif isClosedOrders: + # exchange requires a `since` parameter for closed orders, so set default to allowed 365 + request['serverCreateTimestampFrom'] = self.milliseconds() - 364 * 24 * 60 * 60 * 1000 + until = None + until, params = self.handle_param_integer_2(params, 'until', 'till') + if until is not None: + request['serverCreateTimestampTo'] = until + response = await self.privatePostGetMyOrders(self.extend(request, params)) + # + # if called without `pair` + # + # { + # "ok": "ok", + # "data": [ + # { + # "orderId": "1313003", + # "clientOrderId": "037F0AFEB93A", + # "clientId": "up421412345", + # "accountId": null, + # "status": "FILLED", + # "statusIsFinal": True, + # "currency1": "AI", + # "currency2": "USDT", + # "side": "BUY", + # "orderType": "Market", + # "timeInForce": "IOC", + # "comment": null, + # "rejectCode": null, + # "rejectReason": null, + # "initialOnHoldAmountCcy1": null, + # "initialOnHoldAmountCcy2": "10.23456700", + # "executedAmountCcy1": "25.606429", + # "executedAmountCcy2": "10.20904439", + # "requestedAmountCcy1": null, + # "requestedAmountCcy2": "10.20904439", + # "originalAmountCcy2": "10.23456700", + # "feeAmount": "0.02552261", + # "feeCurrency": "USDT", + # "price": null, + # "averagePrice": "0.3986", + # "clientCreateTimestamp": "1728474625320", + # "serverCreateTimestamp": "1728474624956", + # "lastUpdateTimestamp": "1728474628015", + # "expireTime": null, + # "effectiveTime": null + # }, + # ... + # + data = self.safe_list(response, 'data', []) + return self.parse_orders(data, market, since, limit) + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + + https://trade.cex.io/docs/#rest-private-api-calls-orders + + fetches information on multiple canceled orders made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: timestamp in ms of the earliest order, default is None + :param int [limit]: max number of orders to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `order structures ` + """ + return await self.fetch_orders_by_status('closed', symbol, since, limit, params) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + + https://trade.cex.io/docs/#rest-private-api-calls-orders + + fetches information on multiple canceled orders made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: timestamp in ms of the earliest order, default is None + :param int [limit]: max number of orders to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `order structures ` + """ + return await self.fetch_orders_by_status('open', symbol, since, limit, params) + + async def fetch_open_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an open order made by the user + + https://trade.cex.io/docs/#rest-private-api-calls-orders + + :param str id: order id + :param str [symbol]: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + request: dict = { + 'orderId': int(id), + } + result = await self.fetch_open_orders(symbol, None, None, self.extend(request, params)) + return result[0] + + async def fetch_closed_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an closed order made by the user + + https://trade.cex.io/docs/#rest-private-api-calls-orders + + :param str id: order id + :param str [symbol]: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + request: dict = { + 'orderId': int(id), + } + result = await self.fetch_closed_orders(symbol, None, None, self.extend(request, params)) + return result[0] + + def parse_order_status(self, status: Str): + statuses: dict = { + 'PENDING_NEW': 'open', + 'NEW': 'open', + 'PARTIALLY_FILLED': 'open', + 'FILLED': 'closed', + 'EXPIRED': 'expired', + 'REJECTED': 'rejected', + 'PENDING_CANCEL': 'canceling', + 'CANCELLED': 'canceled', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # "orderId": "1313003", + # "clientOrderId": "037F0AFEB93A", + # "clientId": "up421412345", + # "accountId": null, + # "status": "FILLED", + # "statusIsFinal": True, + # "currency1": "AI", + # "currency2": "USDT", + # "side": "BUY", + # "orderType": "Market", + # "timeInForce": "IOC", + # "comment": null, + # "rejectCode": null, + # "rejectReason": null, + # "initialOnHoldAmountCcy1": null, + # "initialOnHoldAmountCcy2": "10.23456700", + # "executedAmountCcy1": "25.606429", + # "executedAmountCcy2": "10.20904439", + # "requestedAmountCcy1": null, + # "requestedAmountCcy2": "10.20904439", + # "originalAmountCcy2": "10.23456700", + # "feeAmount": "0.02552261", + # "feeCurrency": "USDT", + # "price": null, + # "averagePrice": "0.3986", + # "clientCreateTimestamp": "1728474625320", + # "serverCreateTimestamp": "1728474624956", + # "lastUpdateTimestamp": "1728474628015", + # "expireTime": null, + # "effectiveTime": null + # + currency1 = self.safe_string(order, 'currency1') + currency2 = self.safe_string(order, 'currency2') + marketId = None + if currency1 is not None and currency2 is not None: + marketId = currency1 + '-' + currency2 + market = self.safe_market(marketId, market) + symbol = market['symbol'] + status = self.parse_order_status(self.safe_string(order, 'status')) + fee = {} + feeAmount = self.safe_number(order, 'feeAmount') + if feeAmount is not None: + currencyId = self.safe_string(order, 'feeCurrency') + feeCode = self.safe_currency_code(currencyId) + fee['currency'] = feeCode + fee['cost'] = feeAmount + timestamp = self.safe_integer(order, 'serverCreateTimestamp') + requestedBase = self.safe_number(order, 'requestedAmountCcy1') + executedBase = self.safe_number(order, 'executedAmountCcy1') + # requestedQuote = self.safe_number(order, 'requestedAmountCcy2') + executedQuote = self.safe_number(order, 'executedAmountCcy2') + return self.safe_order({ + 'id': self.safe_string(order, 'orderId'), + 'clientOrderId': self.safe_string(order, 'clientOrderId'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastUpdateTimestamp': self.safe_integer(order, 'lastUpdateTimestamp'), + 'lastTradeTimestamp': None, + 'symbol': symbol, + 'type': self.safe_string_lower(order, 'orderType'), + 'timeInForce': self.safe_string(order, 'timeInForce'), + 'postOnly': None, + 'side': self.safe_string_lower(order, 'side'), + 'price': self.safe_number(order, 'price'), + 'triggerPrice': self.safe_number(order, 'stopPrice'), + 'amount': requestedBase, + 'cost': executedQuote, + 'average': self.safe_number(order, 'averagePrice'), + 'filled': executedBase, + 'remaining': None, + 'status': status, + 'fee': fee, + 'trades': None, + 'info': order, + }, market) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://trade.cex.io/docs/#rest-private-api-calls-new-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.accountId]: account-id to use(default is empty string) + :param float [params.triggerPrice]: the price at which a trigger order is triggered at + :returns dict: an `order structure ` + """ + accountId = None + accountId, params = self.handle_option_and_params(params, 'createOrder', 'accountId') + if accountId is None: + raise ArgumentsRequired(self.id + ' createOrder() : API trading is now allowed from main account, set params["accountId"] or .options["createOrder"]["accountId"] to the name of your sub-account') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'clientOrderId': self.uuid(), + 'currency1': market['baseId'], + 'currency2': market['quoteId'], + 'accountId': accountId, + 'orderType': self.capitalize(type.lower()), + 'side': side.upper(), + 'timestamp': self.milliseconds(), + 'amountCcy1': self.amount_to_precision(symbol, amount), + } + timeInForce = None + timeInForce, params = self.handle_option_and_params(params, 'createOrder', 'timeInForce', 'GTC') + if type == 'limit': + request['price'] = self.price_to_precision(symbol, price) + request['timeInForce'] = timeInForce + triggerPrice = None + triggerPrice, params = self.handle_param_string(params, 'triggerPrice') + if triggerPrice is not None: + request['type'] = 'Stop Limit' + request['stopPrice'] = triggerPrice + response = await self.privatePostDoMyNewOrder(self.extend(request, params)) + # + # on success + # + # { + # "ok": "ok", + # "data": { + # "messageType": "executionReport", + # "clientId": "up132245425", + # "orderId": "1318485", + # "clientOrderId": "b5b6cd40-154c-4c1c-bd51-4a442f3d50b9", + # "accountId": "sub1", + # "status": "FILLED", + # "currency1": "LTC", + # "currency2": "USDT", + # "side": "BUY", + # "executedAmountCcy1": "0.23000000", + # "executedAmountCcy2": "15.09030000", + # "requestedAmountCcy1": "0.23000000", + # "requestedAmountCcy2": null, + # "orderType": "Market", + # "timeInForce": null, + # "comment": null, + # "executionType": "Trade", + # "executionId": "1726747124624_101_41116", + # "transactTime": "2024-10-15T15:08:12.794Z", + # "expireTime": null, + # "effectiveTime": null, + # "averagePrice": "65.61", + # "lastQuantity": "0.23000000", + # "lastAmountCcy1": "0.23000000", + # "lastAmountCcy2": "15.09030000", + # "lastPrice": "65.61", + # "feeAmount": "0.03772575", + # "feeCurrency": "USDT", + # "clientCreateTimestamp": "1729004892014", + # "serverCreateTimestamp": "1729004891628", + # "lastUpdateTimestamp": "1729004892786" + # } + # } + # + # on failure, there are extra fields + # + # "status": "REJECTED", + # "requestedAmountCcy1": null, + # "orderRejectReason": "{\\" code \\ ":405,\\" reason \\ ":\\" Either AmountCcy1(OrderQty)or AmountCcy2(CashOrderQty)should be specified for market order not both \\ "}", + # "rejectCode": 405, + # "rejectReason": "Either AmountCcy1(OrderQty) or AmountCcy2(CashOrderQty) should be specified for market order not both", + # + data = self.safe_dict(response, 'data') + return self.parse_order(data, market) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://trade.cex.io/docs/#rest-private-api-calls-cancel-order + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + request: dict = { + 'orderId': int(id), + 'cancelRequestId': 'c_' + str((self.milliseconds())), + 'timestamp': self.milliseconds(), + } + response = await self.privatePostDoCancelMyOrder(self.extend(request, params)) + # + # {"ok":"ok","data":{}} + # + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data) + + async def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders in a market + + https://trade.cex.io/docs/#rest-private-api-calls-cancel-all-orders + + :param str symbol: alpaca cancelAllOrders cannot setting symbol, it will cancel all open orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + response = await self.privatePostDoCancelAllOrders(params) + # + # { + # "ok": "ok", + # "data": { + # "clientOrderIds": [ + # "3AF77B67109F" + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + ids = self.safe_list(data, 'clientOrderIds', []) + orders = [] + for i in range(0, len(ids)): + id = ids[i] + orders.append({'clientOrderId': id}) + return self.parse_orders(orders) + + async def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + + https://trade.cex.io/docs/#rest-private-api-calls-transaction-history + + :param str [code]: unified currency code + :param int [since]: timestamp in ms of the earliest ledger entry + :param int [limit]: max number of ledger entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest ledger entry + :returns dict: a `ledger structure ` + """ + await self.load_markets() + currency = None + request: dict = {} + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + if since is not None: + request['dateFrom'] = since + if limit is not None: + request['pageSize'] = limit + until = None + until, params = self.handle_param_integer_2(params, 'until', 'till') + if until is not None: + request['dateTo'] = until + response = await self.privatePostGetMyTransactionHistory(self.extend(request, params)) + # + # { + # "ok": "ok", + # "data": [ + # { + # "transactionId": "30367722", + # "timestamp": "2024-10-14T14:08:49.987Z", + # "accountId": "", + # "type": "withdraw", + # "amount": "-12.39060600", + # "details": "Withdraw fundingId=1235039 clientId=up421412345 walletTxId=76337154166", + # "currency": "USDT" + # }, + # ... + # + data = self.safe_list(response, 'data', []) + return self.parse_ledger(data, currency, since, limit) + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + amount = self.safe_string(item, 'amount') + direction = None + if Precise.string_le(amount, '0'): + direction = 'out' + amount = Precise.string_mul('-1', amount) + else: + direction = 'in' + currencyId = self.safe_string(item, 'currency') + currency = self.safe_currency(currencyId, currency) + code = self.safe_currency_code(currencyId, currency) + timestampString = self.safe_string(item, 'timestamp') + timestamp = self.parse8601(timestampString) + type = self.safe_string(item, 'type') + return self.safe_ledger_entry({ + 'info': item, + 'id': self.safe_string(item, 'transactionId'), + 'direction': direction, + 'account': self.safe_string(item, 'accountId', ''), + 'referenceAccount': None, + 'referenceId': None, + 'type': self.parse_ledger_entry_type(type), + 'currency': code, + 'amount': self.parse_number(amount), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'before': None, + 'after': None, + 'status': None, + 'fee': None, + }, currency) + + def parse_ledger_entry_type(self, type): + ledgerType: dict = { + 'deposit': 'deposit', + 'withdraw': 'withdrawal', + 'commission': 'fee', + } + return self.safe_string(ledgerType, type, type) + + async def fetch_deposits_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch history of deposits and withdrawals + + https://trade.cex.io/docs/#rest-private-api-calls-funding-history + + :param str [code]: unified currency code for the currency of the deposit/withdrawals, default is None + :param int [since]: timestamp in ms of the earliest deposit/withdrawal, default is None + :param int [limit]: max number of deposit/withdrawals to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `transaction structure ` + """ + await self.load_markets() + request: dict = {} + currency = None + if code is not None: + currency = self.currency(code) + if since is not None: + request['dateFrom'] = since + if limit is not None: + request['pageSize'] = limit + until = None + until, params = self.handle_param_integer_2(params, 'until', 'till') + if until is not None: + request['dateTo'] = until + response = await self.privatePostGetMyFundingHistory(self.extend(request, params)) + # + # { + # "ok": "ok", + # "data": [ + # { + # "clientId": "up421412345", + # "accountId": "", + # "currency": "USDT", + # "direction": "withdraw", + # "amount": "12.39060600", + # "commissionAmount": "0.00000000", + # "status": "approved", + # "updatedAt": "2024-10-14T14:08:50.013Z", + # "txId": "30367718", + # "details": {} + # }, + # ... + # + data = self.safe_list(response, 'data', []) + return self.parse_transactions(data, currency, since, limit) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + currencyId = self.safe_string(transaction, 'currency') + direction = self.safe_string(transaction, 'direction') + type = 'withdrawal' if (direction == 'withdraw') else 'deposit' + code = self.safe_currency_code(currencyId, currency) + updatedAt = self.safe_string(transaction, 'updatedAt') + timestamp = self.parse8601(updatedAt) + return { + 'info': transaction, + 'id': self.safe_string(transaction, 'txId'), + 'txid': None, + 'type': type, + 'currency': code, + 'network': None, + 'amount': self.safe_number(transaction, 'amount'), + 'status': self.parse_transaction_status(self.safe_string(transaction, 'status')), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'address': None, + 'addressFrom': None, + 'addressTo': None, + 'tag': None, + 'tagFrom': None, + 'tagTo': None, + 'updated': None, + 'comment': None, + 'fee': { + 'currency': code, + 'cost': self.safe_number(transaction, 'commissionAmount'), + }, + 'internal': None, + } + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'rejected': 'rejected', + 'pending': 'pending', + 'approved': 'ok', + } + return self.safe_string(statuses, status, status) + + async def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://trade.cex.io/docs/#rest-private-api-calls-internal-transfer + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: 'SPOT', 'FUND', or 'CONTRACT' + :param str toAccount: 'SPOT', 'FUND', or 'CONTRACT' + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transfer structure ` + """ + transfer = None + if toAccount != '' and fromAccount != '': + transfer = await self.transfer_between_sub_accounts(code, amount, fromAccount, toAccount, params) + else: + transfer = await self.transfer_between_main_and_sub_account(code, amount, fromAccount, toAccount, params) + fillResponseFromRequest = self.handle_option('transfer', 'fillResponseFromRequest', True) + if fillResponseFromRequest: + transfer['fromAccount'] = fromAccount + transfer['toAccount'] = toAccount + return transfer + + async def transfer_between_main_and_sub_account(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + await self.load_markets() + currency = self.currency(code) + fromMain = (fromAccount == '') + targetAccount = toAccount if fromMain else fromAccount + guid = self.safe_string(params, 'guid', self.uuid()) + request: dict = { + 'currency': currency['id'], + 'amount': self.currency_to_precision(code, amount), + 'accountId': targetAccount, + 'clientTxId': guid, + } + response = None + if fromMain: + response = await self.privatePostDoDepositFundsFromWallet(self.extend(request, params)) + else: + response = await self.privatePostDoWithdrawalFundsToWallet(self.extend(request, params)) + # both endpoints return the same structure, the only difference is that + # the "accountId" is filled with the "subAccount" + # + # { + # "ok": "ok", + # "data": { + # "accountId": "sub1", + # "clientTxId": "27ba8284-67cf-4386-9ec7-80b3871abd45", + # "currency": "USDT", + # "status": "approved" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_transfer(data, currency) + + async def transfer_between_sub_accounts(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + 'amount': self.currency_to_precision(code, amount), + 'fromAccountId': fromAccount, + 'toAccountId': toAccount, + } + response = await self.privatePostDoMyInternalTransfer(self.extend(request, params)) + # + # { + # "ok": "ok", + # "data": { + # "transactionId": "30225415" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_transfer(data, currency) + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + # + # transferBetweenSubAccounts + # + # { + # "ok": "ok", + # "data": { + # "transactionId": "30225415" + # } + # } + # + # transfer between main/sub + # + # { + # "ok": "ok", + # "data": { + # "accountId": "sub1", + # "clientTxId": "27ba8284-67cf-4386-9ec7-80b3871abd45", + # "currency": "USDT", + # "status": "approved" + # } + # } + # + currencyId = self.safe_string(transfer, 'currency') + currencyCode = self.safe_currency_code(currencyId, currency) + return { + 'info': transfer, + 'id': self.safe_string_2(transfer, 'transactionId', 'clientTxId'), + 'timestamp': None, + 'datetime': None, + 'currency': currencyCode, + 'amount': None, + 'fromAccount': None, + 'toAccount': None, + 'status': self.parse_transaction_status(self.safe_string(transfer, 'status')), + } + + async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://trade.cex.io/docs/#rest-private-api-calls-deposit-address + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.accountId]: account-id(default to empty string) to refer to(at self moment, only sub-accounts allowed by exchange) + :returns dict: an `address structure ` + """ + accountId = None + accountId, params = self.handle_option_and_params(params, 'createOrder', 'accountId') + if accountId is None: + raise ArgumentsRequired(self.id + ' fetchDepositAddress() : main account is not allowed to fetch deposit address from api, set params["accountId"] or .options["createOrder"]["accountId"] to the name of your sub-account') + await self.load_markets() + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + currency = self.currency(code) + request: dict = { + 'accountId': accountId, + 'currency': currency['id'], # documentation is wrong about self param + 'blockchain': self.network_code_to_id(networkCode), + } + response = await self.privatePostGetDepositAddress(self.extend(request, params)) + # + # { + # "ok": "ok", + # "data": { + # "address": "TCr..................1AE", + # "accountId": "sub1", + # "currency": "USDT", + # "blockchain": "tron" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_deposit_address(data, currency) + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + address = self.safe_string(depositAddress, 'address') + currencyId = self.safe_string(depositAddress, 'currency') + currency = self.safe_currency(currencyId, currency) + self.check_address(address) + return { + 'info': depositAddress, + 'currency': currency['code'], + 'network': self.network_id_to_code(self.safe_string(depositAddress, 'blockchain')), + 'address': address, + 'tag': None, + } + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = self.urls['api'][api] + '/' + self.implode_params(path, params) + query = self.omit(params, self.extract_params(path)) + if api == 'public': + if method == 'GET': + if query: + url += '?' + self.urlencode(query) + else: + body = self.json(query) + headers = { + 'Content-Type': 'application/json', + } + else: + self.check_required_credentials() + seconds = str(self.seconds()) + body = self.json(query) + auth = path + seconds + body + signature = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256, 'base64') + headers = { + 'Content-Type': 'application/json', + 'X-AGGR-KEY': self.apiKey, + 'X-AGGR-TIMESTAMP': seconds, + 'X-AGGR-SIGNATURE': signature, + } + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + # in some cases, like from createOrder, exchange returns nested escaped JSON string: + # {"ok":"ok","data":{"messageType":"executionReport", "orderRejectReason":"{\"code\":405}"}} + # and because of `.parseJson` bug, we need extra fix + if response is None: + if body is None: + raise NullResponse(self.id + ' returned empty response') + elif body[0] == '{': + fixed = self.fix_stringified_json_members(body) + response = self.parse_json(fixed) + else: + raise NullResponse(self.id + ' returned unparsed response: ' + body) + error = self.safe_string(response, 'error') + if error is not None: + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], error, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], error, feedback) + raise ExchangeError(feedback) + # check errors in order-engine(the responses are not standard, so we parse here) + if url.find('do_my_new_order') >= 0: + data = self.safe_dict(response, 'data', {}) + rejectReason = self.safe_string(data, 'rejectReason') + if rejectReason is not None: + self.throw_broadly_matched_exception(self.exceptions['broad'], rejectReason, rejectReason) + raise ExchangeError(self.id + ' createOrder() ' + rejectReason) + return None diff --git a/ccxt/async_support/coinbase.py b/ccxt/async_support/coinbase.py new file mode 100644 index 0000000..7e633d7 --- /dev/null +++ b/ccxt/async_support/coinbase.py @@ -0,0 +1,5002 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.coinbase import ImplicitAPI +import asyncio +import hashlib +from ccxt.base.types import Account, Any, Balances, Conversion, Currencies, Currency, DepositAddress, Int, LedgerEntry, Market, Num, Order, OrderBook, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, Trade, TradingFees, Transaction, MarketInterface +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import NotSupported +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import InvalidNonce +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class coinbase(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(coinbase, self).describe(), { + 'id': 'coinbase', + 'name': 'Coinbase Advanced', + 'countries': ['US'], + 'pro': True, + 'certified': False, + # rate-limits: + # ADVANCED API: https://docs.cloud.coinbase.com/advanced-trade/docs/rest-api-rate-limits + # - max 30 req/second for private data, 10 req/s for public data + # DATA API : https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/rate-limiting + # - max 10000 req/hour(to prevent userland mistakes we apply ~3 req/second RL per call + 'rateLimit': 34, + 'version': 'v2', + 'userAgent': self.userAgents['chrome'], + 'headers': { + 'CB-VERSION': '2018-05-30', + }, + 'has': { + 'CORS': True, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelOrder': True, + 'cancelOrders': True, + 'closeAllPositions': False, + 'closePosition': True, + 'createConvertTrade': True, + 'createDepositAddress': True, + 'createLimitBuyOrder': True, + 'createLimitSellOrder': True, + 'createMarketBuyOrder': True, + 'createMarketBuyOrderWithCost': True, + 'createMarketOrderWithCost': False, + 'createMarketSellOrder': True, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createOrderWithTakeProfitAndStopLoss': False, + 'createOrderWithTakeProfitAndStopLossWs': False, + 'createPostOnlyOrder': True, + 'createReduceOnlyOrder': False, + 'createStopLimitOrder': True, + 'createStopMarketOrder': False, + 'createStopOrder': True, + 'deposit': True, + 'editOrder': True, + 'fetchAccounts': True, + 'fetchBalance': True, + 'fetchBidsAsks': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchCanceledOrders': True, + 'fetchClosedOrders': True, + 'fetchConvertQuote': True, + 'fetchConvertTrade': True, + 'fetchConvertTradeHistory': False, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDeposit': True, + 'fetchDepositAddress': 'emulated', + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': True, + 'fetchDepositMethodId': True, + 'fetchDepositMethodIds': True, + 'fetchDeposits': True, + 'fetchDepositsWithdrawals': True, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchL2OrderBook': False, + 'fetchLedger': True, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrices': False, + 'fetchMyBuys': True, + 'fetchMyLiquidations': False, + 'fetchMySells': True, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchPosition': True, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': True, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': 'emulated', + 'fetchTradingFees': True, + 'fetchVolatilityHistory': False, + 'fetchWithdrawals': True, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'repayMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'withdraw': True, + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/40811661-b6eceae2-653a-11e8-829e-10bfadb078cf.jpg', + 'api': { + 'rest': 'https://api.coinbase.com', + }, + 'www': 'https://www.coinbase.com', + 'doc': [ + 'https://developers.coinbase.com/api/v2', + 'https://docs.cloud.coinbase.com/advanced-trade/docs/welcome', + ], + 'fees': [ + 'https://support.coinbase.com/customer/portal/articles/2109597-buy-sell-bank-transfer-fees', + 'https://www.coinbase.com/advanced-fees', + ], + 'referral': 'https://www.coinbase.com/join/58cbe25a355148797479dbd2', + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + }, + 'api': { + 'v2': { + 'public': { + 'get': { + 'currencies': 10.6, + 'currencies/crypto': 10.6, + 'time': 10.6, + 'exchange-rates': 10.6, + 'users/{user_id}': 10.6, + 'prices/{symbol}/buy': 10.6, + 'prices/{symbol}/sell': 10.6, + 'prices/{symbol}/spot': 10.6, + }, + }, + 'private': { + 'get': { + 'accounts': 10.6, + 'accounts/{account_id}': 10.6, + 'accounts/{account_id}/addresses': 10.6, + 'accounts/{account_id}/addresses/{address_id}': 10.6, + 'accounts/{account_id}/addresses/{address_id}/transactions': 10.6, + 'accounts/{account_id}/transactions': 10.6, + 'accounts/{account_id}/transactions/{transaction_id}': 10.6, + 'accounts/{account_id}/buys': 10.6, + 'accounts/{account_id}/buys/{buy_id}': 10.6, + 'accounts/{account_id}/sells': 10.6, + 'accounts/{account_id}/sells/{sell_id}': 10.6, + 'accounts/{account_id}/deposits': 10.6, + 'accounts/{account_id}/deposits/{deposit_id}': 10.6, + 'accounts/{account_id}/withdrawals': 10.6, + 'accounts/{account_id}/withdrawals/{withdrawal_id}': 10.6, + 'payment-methods': 10.6, + 'payment-methods/{payment_method_id}': 10.6, + 'user': 10.6, + 'user/auth': 10.6, + }, + 'post': { + 'accounts': 10.6, + 'accounts/{account_id}/primary': 10.6, + 'accounts/{account_id}/addresses': 10.6, + 'accounts/{account_id}/transactions': 10.6, + 'accounts/{account_id}/transactions/{transaction_id}/complete': 10.6, + 'accounts/{account_id}/transactions/{transaction_id}/resend': 10.6, + 'accounts/{account_id}/buys': 10.6, + 'accounts/{account_id}/buys/{buy_id}/commit': 10.6, + 'accounts/{account_id}/sells': 10.6, + 'accounts/{account_id}/sells/{sell_id}/commit': 10.6, + 'accounts/{account_id}/deposits': 10.6, + 'accounts/{account_id}/deposits/{deposit_id}/commit': 10.6, + 'accounts/{account_id}/withdrawals': 10.6, + 'accounts/{account_id}/withdrawals/{withdrawal_id}/commit': 10.6, + }, + 'put': { + 'accounts/{account_id}': 10.6, + 'user': 10.6, + }, + 'delete': { + 'accounts/{id}': 10.6, + 'accounts/{account_id}/transactions/{transaction_id}': 10.6, + }, + }, + }, + 'v3': { + 'public': { + 'get': { + 'brokerage/time': 3, + 'brokerage/market/product_book': 3, + 'brokerage/market/products': 3, + 'brokerage/market/products/{product_id}': 3, + 'brokerage/market/products/{product_id}/candles': 3, + 'brokerage/market/products/{product_id}/ticker': 3, + }, + }, + 'private': { + 'get': { + 'brokerage/accounts': 1, + 'brokerage/accounts/{account_uuid}': 1, + 'brokerage/orders/historical/batch': 1, + 'brokerage/orders/historical/fills': 1, + 'brokerage/orders/historical/{order_id}': 1, + 'brokerage/products': 3, + 'brokerage/products/{product_id}': 3, + 'brokerage/products/{product_id}/candles': 3, + 'brokerage/products/{product_id}/ticker': 3, + 'brokerage/best_bid_ask': 3, + 'brokerage/product_book': 3, + 'brokerage/transaction_summary': 3, + 'brokerage/portfolios': 1, + 'brokerage/portfolios/{portfolio_uuid}': 1, + 'brokerage/convert/trade/{trade_id}': 1, + 'brokerage/cfm/balance_summary': 1, + 'brokerage/cfm/positions': 1, + 'brokerage/cfm/positions/{product_id}': 1, + 'brokerage/cfm/sweeps': 1, + 'brokerage/intx/portfolio/{portfolio_uuid}': 1, + 'brokerage/intx/positions/{portfolio_uuid}': 1, + 'brokerage/intx/positions/{portfolio_uuid}/{symbol}': 1, + 'brokerage/payment_methods': 1, + 'brokerage/payment_methods/{payment_method_id}': 1, + 'brokerage/key_permissions': 1, + }, + 'post': { + 'brokerage/orders': 1, + 'brokerage/orders/batch_cancel': 1, + 'brokerage/orders/edit': 1, + 'brokerage/orders/edit_preview': 1, + 'brokerage/orders/preview': 1, + 'brokerage/portfolios': 1, + 'brokerage/portfolios/move_funds': 1, + 'brokerage/convert/quote': 1, + 'brokerage/convert/trade/{trade_id}': 1, + 'brokerage/cfm/sweeps/schedule': 1, + 'brokerage/intx/allocate': 1, + # futures + 'brokerage/orders/close_position': 1, + }, + 'put': { + 'brokerage/portfolios/{portfolio_uuid}': 1, + }, + 'delete': { + 'brokerage/portfolios/{portfolio_uuid}': 1, + 'brokerage/cfm/sweeps': 1, + }, + }, + }, + }, + 'fees': { + 'trading': { + 'taker': self.parse_number('0.012'), + 'maker': self.parse_number('0.006'), # {"pricing_tier":"Advanced 1","usd_from":"0","usd_to":"1000","taker_fee_rate":"0.012","maker_fee_rate":"0.006","aop_from":"","aop_to":""} + 'tierBased': True, + 'percentage': True, + 'tiers': { + 'taker': [ + [self.parse_number('0'), self.parse_number('0.006')], + [self.parse_number('10000'), self.parse_number('0.004')], + [self.parse_number('50000'), self.parse_number('0.0025')], + [self.parse_number('100000'), self.parse_number('0.002')], + [self.parse_number('1000000'), self.parse_number('0.0018')], + [self.parse_number('15000000'), self.parse_number('0.0016')], + [self.parse_number('75000000'), self.parse_number('0.0012')], + [self.parse_number('250000000'), self.parse_number('0.0008')], + [self.parse_number('400000000'), self.parse_number('0.0005')], + ], + 'maker': [ + [self.parse_number('0'), self.parse_number('0.004')], + [self.parse_number('10000'), self.parse_number('0.0025')], + [self.parse_number('50000'), self.parse_number('0.0015')], + [self.parse_number('100000'), self.parse_number('0.001')], + [self.parse_number('1000000'), self.parse_number('0.0008')], + [self.parse_number('15000000'), self.parse_number('0.0006')], + [self.parse_number('75000000'), self.parse_number('0.0003')], + [self.parse_number('250000000'), self.parse_number('0.0')], + [self.parse_number('400000000'), self.parse_number('0.0')], + ], + }, + }, + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'exact': { + 'two_factor_required': AuthenticationError, # 402 When sending money over 2fa limit + 'param_required': ExchangeError, # 400 Missing parameter + 'validation_error': ExchangeError, # 400 Unable to validate POST/PUT + 'invalid_request': ExchangeError, # 400 Invalid request + 'personal_details_required': AuthenticationError, # 400 User’s personal detail required to complete self request + 'identity_verification_required': AuthenticationError, # 400 Identity verification is required to complete self request + 'jumio_verification_required': AuthenticationError, # 400 Document verification is required to complete self request + 'jumio_face_match_verification_required': AuthenticationError, # 400 Document verification including face match is required to complete self request + 'unverified_email': AuthenticationError, # 400 User has not verified their email + 'authentication_error': AuthenticationError, # 401 Invalid auth(generic) + 'invalid_authentication_method': AuthenticationError, # 401 API access is blocked for deleted users. + 'invalid_token': AuthenticationError, # 401 Invalid Oauth token + 'revoked_token': AuthenticationError, # 401 Revoked Oauth token + 'expired_token': AuthenticationError, # 401 Expired Oauth token + 'invalid_scope': AuthenticationError, # 403 User hasn’t authenticated necessary scope + 'not_found': ExchangeError, # 404 Resource not found + 'rate_limit_exceeded': RateLimitExceeded, # 429 Rate limit exceeded + 'internal_server_error': ExchangeError, # 500 Internal server error + 'UNSUPPORTED_ORDER_CONFIGURATION': BadRequest, + 'INSUFFICIENT_FUND': InsufficientFunds, + 'PERMISSION_DENIED': PermissionDenied, + 'INVALID_ARGUMENT': BadRequest, + 'PREVIEW_STOP_PRICE_ABOVE_LAST_TRADE_PRICE': InvalidOrder, + 'PREVIEW_INSUFFICIENT_FUND': InsufficientFunds, + }, + 'broad': { + 'Insufficient balance in source account': InsufficientFunds, + 'request timestamp expired': InvalidNonce, # {"errors":[{"id":"authentication_error","message":"request timestamp expired"}]} + 'order with self orderID was not found': OrderNotFound, # {"error":"unknown","error_details":"order with self orderID was not found","message":"order with self orderID was not found"} + }, + }, + 'timeframes': { + '1m': 'ONE_MINUTE', + '5m': 'FIVE_MINUTE', + '15m': 'FIFTEEN_MINUTE', + '30m': 'THIRTY_MINUTE', + '1h': 'ONE_HOUR', + '2h': 'TWO_HOUR', + '6h': 'SIX_HOUR', + '1d': 'ONE_DAY', + }, + 'commonCurrencies': { + 'CGLD': 'CELO', + }, + 'options': { + 'usePrivate': False, + 'brokerId': 'ccxt', + 'stablePairs': ['BUSD-USD', 'CBETH-ETH', 'DAI-USD', 'GUSD-USD', 'GYEN-USD', 'PAX-USD', 'PAX-USDT', 'USDC-EUR', 'USDC-GBP', 'USDT-EUR', 'USDT-GBP', 'USDT-USD', 'USDT-USDC', 'WBTC-BTC'], + 'fetchCurrencies': { + 'expires': 5000, + }, + 'accounts': [ + 'wallet', + 'fiat', + # 'vault', + ], + 'v3Accounts': [ + 'ACCOUNT_TYPE_CRYPTO', + 'ACCOUNT_TYPE_FIAT', + ], + 'networks': { + 'ERC20': 'ethereum', + 'XLM': 'stellar', + }, + 'createMarketBuyOrderRequiresPrice': True, + 'advanced': True, # set to True if using any v3 endpoints from the advanced trade API + 'fetchMarkets': 'fetchMarketsV3', # 'fetchMarketsV3' or 'fetchMarketsV2' + 'timeDifference': 0, # the difference between system clock and exchange server clock + 'adjustForTimeDifference': False, # controls the adjustment logic upon instantiation + 'fetchTicker': 'fetchTickerV3', # 'fetchTickerV3' or 'fetchTickerV2' + 'fetchTickers': 'fetchTickersV3', # 'fetchTickersV3' or 'fetchTickersV2' + 'fetchAccounts': 'fetchAccountsV3', # 'fetchAccountsV3' or 'fetchAccountsV2' + 'fetchBalance': 'v2PrivateGetAccounts', # 'v2PrivateGetAccounts' or 'v3PrivateGetBrokerageAccounts' + 'fetchTime': 'v2PublicGetTime', # 'v2PublicGetTime' or 'v3PublicGetBrokerageTime' + 'user_native_currency': 'USD', # needed to get fees for v3 + }, + 'features': { + 'default': { + 'sandbox': False, + 'createOrder': { + 'marginMode': True, + 'triggerPrice': True, + 'triggerPriceType': None, + 'triggerDirection': True, + 'stopLossPrice': True, + 'takeProfitPrice': True, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': True, + }, + 'hedged': False, + 'trailing': False, + 'leverage': True, # todo implement + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': True, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 3000, + 'daysBack': None, + 'untilDays': 10000, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': None, + 'daysBack': None, + 'untilDays': 10000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': None, + 'daysBack': None, + 'daysBackCanceled': None, + 'untilDays': 10000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 300, + }, + }, + 'spot': { + 'extends': 'default', + }, + 'swap': { + 'linear': { + 'extends': 'default', + }, + 'inverse': None, + }, + 'future': { + 'linear': { + 'extends': 'default', + }, + 'inverse': None, + }, + }, + }) + + async def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/api-time#http-request + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.method]: 'v2PublicGetTime' or 'v3PublicGetBrokerageTime' default is 'v2PublicGetTime' + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + defaultMethod = self.safe_string(self.options, 'fetchTime', 'v2PublicGetTime') + method = self.safe_string(params, 'method', defaultMethod) + params = self.omit(params, 'method') + response = None + if method == 'v2PublicGetTime': + response = await self.v2PublicGetTime(params) + # + # { + # "data": { + # "epoch": 1589295679, + # "iso": "2020-05-12T15:01:19Z" + # } + # } + # + response = self.safe_dict(response, 'data', {}) + else: + response = await self.v3PublicGetBrokerageTime(params) + # + # { + # "iso": "2024-02-27T03:37:14Z", + # "epochSeconds": "1709005034", + # "epochMillis": "1709005034333" + # } + # + return self.safe_timestamp_2(response, 'epoch', 'epochSeconds') + + async def fetch_accounts(self, params={}) -> List[Account]: + """ + fetch all the accounts associated with a profile + + https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_getaccounts + https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/api-accounts#list-accounts + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict: a dictionary of `account structures ` indexed by the account type + """ + method = self.safe_string(self.options, 'fetchAccounts', 'fetchAccountsV3') + if method == 'fetchAccountsV3': + return await self.fetch_accounts_v3(params) + return await self.fetch_accounts_v2(params) + + async def fetch_accounts_v2(self, params={}) -> List[Account]: + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchAccounts', 'paginate') + if paginate: + return await self.fetch_paginated_call_cursor('fetchAccounts', None, None, None, params, 'next_starting_after', 'starting_after', None, 100) + request: dict = { + 'limit': 100, + } + response = await self.v2PrivateGetAccounts(self.extend(request, params)) + # + # { + # "pagination": { + # "ending_before": null, + # "starting_after": null, + # "previous_ending_before": null, + # "next_starting_after": null, + # "limit": 244, + # "order": "desc", + # "previous_uri": null, + # "next_uri": null + # }, + # "data": [ + # { + # "id": "XLM", + # "name": "XLM Wallet", + # "primary": False, + # "type": "wallet", + # "currency": { + # "code": "XLM", + # "name": "Stellar Lumens", + # "color": "#000000", + # "sort_index": 127, + # "exponent": 7, + # "type": "crypto", + # "address_regex": "^G[A-Z2-7]{55}$", + # "asset_id": "13b83335-5ede-595b-821e-5bcdfa80560f", + # "destination_tag_name": "XLM Memo ID", + # "destination_tag_regex": "^[-~]{1,28}$" + # }, + # "balance": { + # "amount": "0.0000000", + # "currency": "XLM" + # }, + # "created_at": null, + # "updated_at": null, + # "resource": "account", + # "resource_path": "/v2/accounts/XLM", + # "allow_deposits": True, + # "allow_withdrawals": True + # }, + # ] + # } + # + data = self.safe_list(response, 'data', []) + pagination = self.safe_dict(response, 'pagination', {}) + cursor = self.safe_string(pagination, 'next_starting_after') + accounts = self.safe_list(response, 'data', []) + length = len(accounts) + lastIndex = length - 1 + last = self.safe_dict(accounts, lastIndex) + if (cursor is not None) and (cursor != ''): + last['next_starting_after'] = cursor + accounts[lastIndex] = last + return self.parse_accounts(data, params) + + async def fetch_accounts_v3(self, params={}) -> List[Account]: + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchAccounts', 'paginate') + if paginate: + return await self.fetch_paginated_call_cursor('fetchAccounts', None, None, None, params, 'cursor', 'cursor', None, 250) + request: dict = { + 'limit': 250, + } + response = await self.v3PrivateGetBrokerageAccounts(self.extend(request, params)) + # + # { + # "accounts": [ + # { + # "uuid": "11111111-1111-1111-1111-111111111111", + # "name": "USDC Wallet", + # "currency": "USDC", + # "available_balance": { + # "value": "0.0000000000000000", + # "currency": "USDC" + # }, + # "default": True, + # "active": True, + # "created_at": "2023-01-04T06:20:06.456Z", + # "updated_at": "2023-01-04T06:20:07.181Z", + # "deleted_at": null, + # "type": "ACCOUNT_TYPE_CRYPTO", + # "ready": False, + # "hold": { + # "value": "0.0000000000000000", + # "currency": "USDC" + # } + # }, + # ... + # ], + # "has_next": False, + # "cursor": "", + # "size": 9 + # } + # + accounts = self.safe_list(response, 'accounts', []) + length = len(accounts) + lastIndex = length - 1 + last = self.safe_dict(accounts, lastIndex) + cursor = self.safe_string(response, 'cursor') + if (cursor is not None) and (cursor != ''): + last['cursor'] = cursor + accounts[lastIndex] = last + return self.parse_accounts(accounts, params) + + async def fetch_portfolios(self, params={}) -> List[Account]: + """ + fetch all the portfolios + + https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_getportfolios + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `account structures ` indexed by the account type + """ + response = await self.v3PrivateGetBrokeragePortfolios(params) + portfolios = self.safe_list(response, 'portfolios', []) + result = [] + for i in range(0, len(portfolios)): + portfolio = portfolios[i] + result.append({ + 'id': self.safe_string(portfolio, 'uuid'), + 'type': self.safe_string(portfolio, 'type'), + 'code': None, + 'info': portfolio, + }) + return result + + def parse_account(self, account): + # + # fetchAccountsV2 + # + # { + # "id": "XLM", + # "name": "XLM Wallet", + # "primary": False, + # "type": "wallet", + # "currency": { + # "code": "XLM", + # "name": "Stellar Lumens", + # "color": "#000000", + # "sort_index": 127, + # "exponent": 7, + # "type": "crypto", + # "address_regex": "^G[A-Z2-7]{55}$", + # "asset_id": "13b83335-5ede-595b-821e-5bcdfa80560f", + # "destination_tag_name": "XLM Memo ID", + # "destination_tag_regex": "^[-~]{1,28}$" + # }, + # "balance": { + # "amount": "0.0000000", + # "currency": "XLM" + # }, + # "created_at": null, + # "updated_at": null, + # "resource": "account", + # "resource_path": "/v2/accounts/XLM", + # "allow_deposits": True, + # "allow_withdrawals": True + # } + # + # fetchAccountsV3 + # + # { + # "uuid": "11111111-1111-1111-1111-111111111111", + # "name": "USDC Wallet", + # "currency": "USDC", + # "available_balance": { + # "value": "0.0000000000000000", + # "currency": "USDC" + # }, + # "default": True, + # "active": True, + # "created_at": "2023-01-04T06:20:06.456Z", + # "updated_at": "2023-01-04T06:20:07.181Z", + # "deleted_at": null, + # "type": "ACCOUNT_TYPE_CRYPTO", + # "ready": False, + # "hold": { + # "value": "0.0000000000000000", + # "currency": "USDC" + # } + # } + # + active = self.safe_bool(account, 'active') + currencyIdV3 = self.safe_string(account, 'currency') + currency = self.safe_dict(account, 'currency', {}) + currencyId = self.safe_string(currency, 'code', currencyIdV3) + typeV3 = self.safe_string(account, 'name') + typeV2 = self.safe_string(account, 'type') + parts = typeV3.split(' ') + return { + 'id': self.safe_string_2(account, 'id', 'uuid'), + 'type': self.safe_string_lower(parts, 1) if (active is not None) else typeV2, + 'code': self.safe_currency_code(currencyId), + 'info': account, + } + + async def create_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + create a currency deposit address + + https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/api-addresses#create-address + + :param str code: unified currency code of the currency for the deposit address + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + accountId = self.safe_string(params, 'account_id') + params = self.omit(params, 'account_id') + if accountId is None: + await self.load_accounts() + for i in range(0, len(self.accounts)): + account = self.accounts[i] + if account['code'] == code and account['type'] == 'wallet': + accountId = account['id'] + break + if accountId is None: + raise ExchangeError(self.id + ' createDepositAddress() could not find the account with matching currency code ' + code + ', specify an `account_id` extra param to target specific wallet') + request: dict = { + 'account_id': accountId, + } + response = await self.v2PrivatePostAccountsAccountIdAddresses(self.extend(request, params)) + # + # { + # "data": { + # "id": "05b1ebbf-9438-5dd4-b297-2ddedc98d0e4", + # "address": "coinbasebase", + # "address_info": { + # "address": "coinbasebase", + # "destination_tag": "287594668" + # }, + # "name": null, + # "created_at": "2019-07-01T14:39:29Z", + # "updated_at": "2019-07-01T14:39:29Z", + # "network": "eosio", + # "uri_scheme": "eosio", + # "resource": "address", + # "resource_path": "/v2/accounts/14cfc769-e852-52f3-b831-711c104d194c/addresses/05b1ebbf-9438-5dd4-b297-2ddedc98d0e4", + # "warnings": [ + # { + # "title": "Only send EOS(EOS) to self address", + # "details": "Sending any other cryptocurrency will result in permanent loss.", + # "image_url": "https://dynamic-assets.coinbase.com/deaca3d47b10ed4a91a872e9618706eec34081127762d88f2476ac8e99ada4b48525a9565cf2206d18c04053f278f693434af4d4629ca084a9d01b7a286a7e26/asset_icons/1f8489bb280fb0a0fd643c1161312ba49655040e9aaaced5f9ad3eeaf868eadc.png" + # }, + # { + # "title": "Both an address and EOS memo are required to receive EOS", + # "details": "If you send funds without an EOS memo or with an incorrect EOS memo, your funds cannot be credited to your account.", + # "image_url": "https://www.coinbase.com/assets/receive-warning-2f3269d83547a7748fb39d6e0c1c393aee26669bfea6b9f12718094a1abff155.png" + # } + # ], + # "warning_title": "Only send EOS(EOS) to self address", + # "warning_details": "Sending any other cryptocurrency will result in permanent loss.", + # "destination_tag": "287594668", + # "deposit_uri": "eosio:coinbasebase?dt=287594668", + # "callback_url": null + # } + # } + # + data = self.safe_dict(response, 'data', {}) + tag = self.safe_string(data, 'destination_tag') + address = self.safe_string(data, 'address') + return { + 'currency': code, + 'tag': tag, + 'address': address, + 'network': None, + 'info': response, + } + + async def fetch_my_sells(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + @ignore + fetch sells + + https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/api-sells#list-sells + + :param str symbol: not used by coinbase fetchMySells() + :param int [since]: timestamp in ms of the earliest sell, default is None + :param int [limit]: max number of sells to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `list of order structures ` + """ + # v2 did't have an endpoint for all historical trades + request = self.prepare_account_request(limit, params) + await self.load_markets() + query = self.omit(params, ['account_id', 'accountId']) + sells = await self.v2PrivateGetAccountsAccountIdSells(self.extend(request, query)) + return self.parse_trades(sells['data'], None, since, limit) + + async def fetch_my_buys(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + @ignore + fetch buys + + https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/api-buys#list-buys + + :param str symbol: not used by coinbase fetchMyBuys() + :param int [since]: timestamp in ms of the earliest buy, default is None + :param int [limit]: max number of buys to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `order structures ` + """ + # v2 did't have an endpoint for all historical trades + request = self.prepare_account_request(limit, params) + await self.load_markets() + query = self.omit(params, ['account_id', 'accountId']) + buys = await self.v2PrivateGetAccountsAccountIdBuys(self.extend(request, query)) + return self.parse_trades(buys['data'], None, since, limit) + + async def fetch_transactions_with_method(self, method, code: Str = None, since: Int = None, limit: Int = None, params={}): + request = None + request, params = await self.prepare_account_request_with_currency_code(code, limit, params) + await self.load_markets() + response = await getattr(self, method)(self.extend(request, params)) + return self.parse_transactions(response['data'], None, since, limit) + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + Fetch all withdrawals made from an account. Won't return crypto withdrawals. Use fetchLedger for those. + + https://docs.cdp.coinbase.com/coinbase-app/docs/api-withdrawals#list-withdrawals + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.currencyType]: "fiat" or "crypto" + :returns dict[]: a list of `transaction structures ` + """ + currencyType = None + currencyType, params = self.handle_option_and_params(params, 'fetchWithdrawals', 'currencyType') + if currencyType == 'crypto': + results = await self.fetch_transactions_with_method('v2PrivateGetAccountsAccountIdTransactions', code, since, limit, params) + return self.filter_by_array(results, 'type', 'withdrawal', False) + return await self.fetch_transactions_with_method('v2PrivateGetAccountsAccountIdWithdrawals', code, since, limit, params) + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + Fetch all fiat deposits made to an account. Won't return crypto deposits or staking rewards. Use fetchLedger for those. + + https://docs.cdp.coinbase.com/coinbase-app/docs/api-deposits#list-deposits + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.currencyType]: "fiat" or "crypto" + :returns dict[]: a list of `transaction structures ` + """ + currencyType = None + currencyType, params = self.handle_option_and_params(params, 'fetchWithdrawals', 'currencyType') + if currencyType == 'crypto': + results = await self.fetch_transactions_with_method('v2PrivateGetAccountsAccountIdTransactions', code, since, limit, params) + return self.filter_by_array(results, 'type', 'deposit', False) + return await self.fetch_transactions_with_method('v2PrivateGetAccountsAccountIdDeposits', code, since, limit, params) + + async def fetch_deposits_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch history of deposits and withdrawals + + https://docs.cdp.coinbase.com/coinbase-app/docs/api-transactions + + :param str [code]: unified currency code for the currency of the deposit/withdrawals, default is None + :param int [since]: timestamp in ms of the earliest deposit/withdrawal, default is None + :param int [limit]: max number of deposit/withdrawals to return, default = 50, Min: 1, Max: 100 + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `transaction structure ` + """ + await self.load_markets() + results = await self.fetch_transactions_with_method('v2PrivateGetAccountsAccountIdTransactions', code, since, limit, params) + return self.filter_by_array(results, 'type', ['deposit', 'withdrawal'], False) + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'created': 'pending', + 'completed': 'ok', + 'canceled': 'canceled', + } + return self.safe_string(statuses, status, status) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fiat deposit + # + # { + # "id": "f34c19f3-b730-5e3d-9f72", + # "status": "completed", + # "payment_method": { + # "id": "a022b31d-f9c7-5043-98f2", + # "resource": "payment_method", + # "resource_path": "/v2/payment-methods/a022b31d-f9c7-5043-98f2" + # }, + # "transaction": { + # "id": "04ed4113-3732-5b0c-af86-b1d2146977d0", + # "resource": "transaction", + # "resource_path": "/v2/accounts/91cd2d36-3a91-55b6-a5d4-0124cf105483/transactions/04ed4113-3732-5b0c-af86" + # }, + # "user_reference": "2VTYTH", + # "created_at": "2017-02-09T07:01:18Z", + # "updated_at": "2017-02-09T07:01:26Z", + # "resource": "deposit", + # "resource_path": "/v2/accounts/91cd2d36-3a91-55b6-a5d4-0124cf105483/deposits/f34c19f3-b730-5e3d-9f72", + # "committed": True, + # "payout_at": "2017-02-12T07:01:17Z", + # "instant": False, + # "fee": {"amount": "0.00", "currency": "EUR"}, + # "amount": {"amount": "114.02", "currency": "EUR"}, + # "subtotal": {"amount": "114.02", "currency": "EUR"}, + # "hold_until": null, + # "hold_days": 0, + # "hold_business_days": 0, + # "next_step": null + # } + # + # fiat_withdrawal + # + # { + # "id": "cfcc3b4a-eeb6-5e8c-8058", + # "status": "completed", + # "payment_method": { + # "id": "8b94cfa4-f7fd-5a12-a76a", + # "resource": "payment_method", + # "resource_path": "/v2/payment-methods/8b94cfa4-f7fd-5a12-a76a" + # }, + # "transaction": { + # "id": "fcc2550b-5104-5f83-a444", + # "resource": "transaction", + # "resource_path": "/v2/accounts/91cd2d36-3a91-55b6-a5d4-0124cf105483/transactions/fcc2550b-5104-5f83-a444" + # }, + # "user_reference": "MEUGK", + # "created_at": "2018-07-26T08:55:12Z", + # "updated_at": "2018-07-26T08:58:18Z", + # "resource": "withdrawal", + # "resource_path": "/v2/accounts/91cd2d36-3a91-55b6-a5d4-0124cf105483/withdrawals/cfcc3b4a-eeb6-5e8c-8058", + # "committed": True, + # "payout_at": "2018-07-31T08:55:12Z", + # "instant": False, + # "fee": {"amount": "0.15", "currency": "EUR"}, + # "amount": {"amount": "13130.69", "currency": "EUR"}, + # "subtotal": {"amount": "13130.84", "currency": "EUR"}, + # "idem": "e549dee5-63ed-4e79-8a96", + # "next_step": null + # } + # + # withdraw + # + # { + # "id": "a1794ecf-5693-55fa-70cf-ef731748ed82", + # "type": "send", + # "status": "pending", + # "amount": { + # "amount": "-14.008308", + # "currency": "USDC" + # }, + # "native_amount": { + # "amount": "-18.74", + # "currency": "CAD" + # }, + # "description": null, + # "created_at": "2024-01-12T01:27:31Z", + # "updated_at": "2024-01-12T01:27:31Z", + # "resource": "transaction", + # "resource_path": "/v2/accounts/a34bgfad-ed67-538b-bffc-730c98c10da0/transactions/a1794ecf-5693-55fa-70cf-ef731748ed82", + # "instant_exchange": False, + # "network": { + # "status": "pending", + # "status_description": "Pending(est. less than 10 minutes)", + # "transaction_fee": { + # "amount": "4.008308", + # "currency": "USDC" + # }, + # "transaction_amount": { + # "amount": "10.000000", + # "currency": "USDC" + # }, + # "confirmations": 0 + # }, + # "to": { + # "resource": "ethereum_address", + # "address": "0x9...", + # "currency": "USDC", + # "address_info": { + # "address": "0x9..." + # } + # }, + # "idem": "748d8591-dg9a-7831-a45b-crd61dg78762", + # "details": { + # "title": "Sent USDC", + # "subtitle": "To USDC address on Ethereum network", + # "header": "Sent 14.008308 USDC($18.74)", + # "health": "warning" + # }, + # "hide_native_amount": False + # } + # + # + # crypto deposit & withdrawal(using `/transactions` endpoint) + # { + # "amount": { + # "amount": "0.00014200",(negative for withdrawal) + # "currency": "BTC" + # }, + # "created_at": "2024-03-29T15:48:30Z", + # "id": "0031a605-241d-514d-a97b-d4b99f3225d3", + # "idem": "092a979b-017e-4403-940a-2ca57811f442", # field present only in case of withdrawal + # "native_amount": { + # "amount": "9.85",(negative for withdrawal) + # "currency": "USD" + # }, + # "network": { + # "status": "pending", # if status is `off_blockchain` then no more other fields are hasattr(self, present) object + # "hash": "5jYuvrNsvX2DZoMnzGYzVpYxJLfYu4GSK3xetG1H5LHrSovsuFCFYdFMwNRoiht3s6fBk92MM8QLLnz65xuEFTrE", + # "network_name": "solana", + # "transaction_fee": { + # "amount": "0.000100000", + # "currency": "SOL" + # } + # }, + # "resource": "transaction", + # "resource_path": "/v2/accounts/dc504b1c-248e-5b68-a3b0-b991f7fa84e6/transactions/0031a605-241d-514d-a97b-d4b99f3225d3", + # "status": "completed", + # "type": "send", + # "from": { # in some cases, field might be present for deposit + # "id": "7fd10cd7-b091-5cee-ba41-c29e49a7cccf", + # "name": "Coinbase", + # "resource": "user" + # }, + # "to": { # field only present for withdrawal + # "address": "5HA12BNthAvBwNYARYf9y5MqqCpB4qhCNFCs1Qw48ACE", + # "resource": "address" + # }, + # "description": "C3 - One Time BTC Credit . Reference Case # 123.", # in some cases, field might be present for deposit + # } + # + transactionType = self.safe_string(transaction, 'type') + amountAndCurrencyObject = None + feeObject = None + network = self.safe_dict(transaction, 'network', {}) + if transactionType == 'send': + amountAndCurrencyObject = self.safe_dict(network, 'transaction_amount') + feeObject = self.safe_dict(network, 'transaction_fee', {}) + else: + amountAndCurrencyObject = self.safe_dict(transaction, 'subtotal') + feeObject = self.safe_dict(transaction, 'fee', {}) + if amountAndCurrencyObject is None: + amountAndCurrencyObject = self.safe_dict(transaction, 'amount') + amountString = self.safe_string(amountAndCurrencyObject, 'amount') + amountStringAbs = Precise.string_abs(amountString) + status = self.parse_transaction_status(self.safe_string(transaction, 'status')) + if status is None: + committed = self.safe_bool(transaction, 'committed') + status = 'ok' if committed else 'pending' + id = self.safe_string(transaction, 'id') + currencyId = self.safe_string(amountAndCurrencyObject, 'currency') + feeCurrencyId = self.safe_string(feeObject, 'currency') + datetime = self.safe_string(transaction, 'created_at') + resource = self.safe_string(transaction, 'resource') + type = resource + if not self.in_array(type, ['deposit', 'withdrawal']): + if Precise.string_gt(amountString, '0'): + type = 'deposit' + elif Precise.string_lt(amountString, '0'): + type = 'withdrawal' + toObject = self.safe_dict(transaction, 'to') + addressTo = self.safe_string(toObject, 'address') + networkId = self.safe_string(network, 'network_name') + return { + 'info': transaction, + 'id': id, + 'txid': self.safe_string(network, 'hash', id), + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + 'network': self.network_id_to_code(networkId), + 'address': addressTo, + 'addressTo': addressTo, + 'addressFrom': None, + 'tag': None, + 'tagTo': None, + 'tagFrom': None, + 'type': type, + 'amount': self.parse_number(amountStringAbs), + 'currency': self.safe_currency_code(currencyId, currency), + 'status': status, + 'updated': self.parse8601(self.safe_string(transaction, 'updated_at')), + 'fee': { + 'cost': self.safe_number(feeObject, 'amount'), + 'currency': self.safe_currency_code(feeCurrencyId), + }, + } + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchMyBuys, fetchMySells + # + # { + # "id": "67e0eaec-07d7-54c4-a72c-2e92826897df", + # "status": "completed", + # "payment_method": { + # "id": "83562370-3e5c-51db-87da-752af5ab9559", + # "resource": "payment_method", + # "resource_path": "/v2/payment-methods/83562370-3e5c-51db-87da-752af5ab9559" + # }, + # "transaction": { + # "id": "441b9494-b3f0-5b98-b9b0-4d82c21c252a", + # "resource": "transaction", + # "resource_path": "/v2/accounts/2bbf394c-193b-5b2a-9155-3b4732659ede/transactions/441b9494-b3f0-5b98-b9b0-4d82c21c252a" + # }, + # "amount": {"amount": "1.00000000", "currency": "BTC"}, + # "total": {"amount": "10.25", "currency": "USD"}, + # "subtotal": {"amount": "10.10", "currency": "USD"}, + # "created_at": "2015-01-31T20:49:02Z", + # "updated_at": "2015-02-11T16:54:02-08:00", + # "resource": "buy", + # "resource_path": "/v2/accounts/2bbf394c-193b-5b2a-9155-3b4732659ede/buys/67e0eaec-07d7-54c4-a72c-2e92826897df", + # "committed": True, + # "instant": False, + # "fee": {"amount": "0.15", "currency": "USD"}, + # "payout_at": "2015-02-18T16:54:00-08:00" + # } + # + # fetchTrades + # + # { + # "trade_id": "10092327", + # "product_id": "BTC-USDT", + # "price": "17488.12", + # "size": "0.0000623", + # "time": "2023-01-11T00:52:37.557001Z", + # "side": "BUY", + # "bid": "", + # "ask": "" + # } + # + # fetchMyTrades + # + # { + # "entry_id": "b88b82cc89e326a2778874795102cbafd08dd979a2a7a3c69603fc4c23c2e010", + # "trade_id": "cdc39e45-bbd3-44ec-bf02-61742dfb16a1", + # "order_id": "813a53c5-3e39-47bb-863d-2faf685d22d8", + # "trade_time": "2023-01-18T01:37:38.091377090Z", + # "trade_type": "FILL", + # "price": "21220.64", + # "size": "0.0046830664333996", + # "commission": "0.0000280983986004", + # "product_id": "BTC-USDT", + # "sequence_timestamp": "2023-01-18T01:37:38.092520Z", + # "liquidity_indicator": "UNKNOWN_LIQUIDITY_INDICATOR", + # "size_in_quote": True, + # "user_id": "1111111-1111-1111-1111-111111111111", + # "side": "BUY" + # } + # + symbol = None + totalObject = self.safe_dict(trade, 'total', {}) + amountObject = self.safe_dict(trade, 'amount', {}) + subtotalObject = self.safe_dict(trade, 'subtotal', {}) + feeObject = self.safe_dict(trade, 'fee', {}) + marketId = self.safe_string(trade, 'product_id') + market = self.safe_market(marketId, market, '-') + if market is not None: + symbol = market['symbol'] + else: + baseId = self.safe_string(amountObject, 'currency') + quoteId = self.safe_string(totalObject, 'currency') + if (baseId is not None) and (quoteId is not None): + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + symbol = base + '/' + quote + sizeInQuote = self.safe_bool(trade, 'size_in_quote') + v3Price = self.safe_string(trade, 'price') + v3Cost = None + v3Amount = self.safe_string(trade, 'size') + if sizeInQuote: + # calculate base size + v3Cost = v3Amount + v3Amount = Precise.string_div(v3Amount, v3Price) + v3FeeCost = self.safe_string(trade, 'commission') + amountString = self.safe_string(amountObject, 'amount', v3Amount) + costString = self.safe_string(subtotalObject, 'amount', v3Cost) + priceString = None + cost = None + if (costString is not None) and (amountString is not None): + priceString = Precise.string_div(costString, amountString) + else: + priceString = v3Price + if (priceString is not None) and (amountString is not None): + cost = Precise.string_mul(priceString, amountString) + else: + cost = costString + feeCurrencyId = self.safe_string(feeObject, 'currency') + feeCost = self.safe_number(feeObject, 'amount', self.parse_number(v3FeeCost)) + if (feeCurrencyId is None) and (market is not None) and (feeCost is not None): + feeCurrencyId = market['quote'] + datetime = self.safe_string_n(trade, ['created_at', 'trade_time', 'time']) + side = self.safe_string_lower_2(trade, 'resource', 'side') + takerOrMaker = self.safe_string_lower(trade, 'liquidity_indicator') + return self.safe_trade({ + 'info': trade, + 'id': self.safe_string_2(trade, 'id', 'trade_id'), + 'order': self.safe_string(trade, 'order_id'), + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + 'symbol': symbol, + 'type': None, + 'side': None if (side == 'unknown_order_side') else side, + 'takerOrMaker': None if (takerOrMaker == 'unknown_liquidity_indicator') else takerOrMaker, + 'price': priceString, + 'amount': amountString, + 'cost': cost, + 'fee': { + 'cost': feeCost, + 'currency': self.safe_currency_code(feeCurrencyId), + }, + }) + + async def fetch_markets(self, params={}) -> List[Market]: + """ + + https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_getpublicproducts + https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/api-currencies#get-fiat-currencies + https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/api-exchange-rates#get-exchange-rates + + retrieves data on all markets for coinbase + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.usePrivate]: use private endpoint for fetching markets + :returns dict[]: an array of objects representing market data + """ + if self.options['adjustForTimeDifference']: + await self.load_time_difference() + method = self.safe_string(self.options, 'fetchMarkets', 'fetchMarketsV3') + if method == 'fetchMarketsV3': + return await self.fetch_markets_v3(params) + return await self.fetch_markets_v2(params) + + async def fetch_markets_v2(self, params={}) -> List[Market]: + response = await self.fetch_currencies_from_cache(params) + currencies = self.safe_dict(response, 'currencies', {}) + exchangeRates = self.safe_dict(response, 'exchangeRates', {}) + data = self.safe_list(currencies, 'data', []) + dataById = self.index_by(data, 'id') + rates = self.safe_dict(self.safe_dict(exchangeRates, 'data', {}), 'rates', {}) + baseIds = list(rates.keys()) + result = [] + for i in range(0, len(baseIds)): + baseId = baseIds[i] + base = self.safe_currency_code(baseId) + type = 'fiat' if (baseId in dataById) else 'crypto' + # https://github.com/ccxt/ccxt/issues/6066 + if type == 'crypto': + for j in range(0, len(data)): + quoteCurrency = data[j] + quoteId = self.safe_string(quoteCurrency, 'id') + quote = self.safe_currency_code(quoteId) + result.append(self.safe_market_structure({ + 'id': baseId + '-' + quoteId, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': None, + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': None, + 'price': None, + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': None, + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': self.safe_number(quoteCurrency, 'min_size'), + 'max': None, + }, + }, + 'info': quoteCurrency, + })) + return result + + async def fetch_markets_v3(self, params={}) -> List[Market]: + usePrivate = False + usePrivate, params = self.handle_option_and_params(params, 'fetchMarkets', 'usePrivate', False) + spotUnresolvedPromises = [] + if usePrivate: + spotUnresolvedPromises.append(self.v3PrivateGetBrokerageProducts(params)) + else: + spotUnresolvedPromises.append(self.v3PublicGetBrokerageMarketProducts(params)) + # + # { + # products: [ + # { + # product_id: 'BTC-USD', + # price: '67060', + # price_percentage_change_24h: '3.30054960636883', + # volume_24h: '10967.87426597', + # volume_percentage_change_24h: '141.73048325503036', + # base_increment: '0.00000001', + # quote_increment: '0.01', + # quote_min_size: '1', + # quote_max_size: '150000000', + # base_min_size: '0.00000001', + # base_max_size: '3400', + # base_name: 'Bitcoin', + # quote_name: 'US Dollar', + # watched: False, + # is_disabled: False, + # new: False, + # status: 'online', + # cancel_only: False, + # limit_only: False, + # post_only: False, + # trading_disabled: False, + # auction_mode: False, + # product_type: 'SPOT', + # quote_currency_id: 'USD', + # base_currency_id: 'BTC', + # fcm_trading_session_details: null, + # mid_market_price: '', + # alias: '', + # alias_to: ['BTC-USDC'], + # base_display_symbol: 'BTC', + # quote_display_symbol: 'USD', + # view_only: False, + # price_increment: '0.01', + # display_name: 'BTC-USD', + # product_venue: 'CBE' + # }, + # ... + # ], + # num_products: '646' + # } + # + if self.check_required_credentials(False): + spotUnresolvedPromises.append(self.v3PrivateGetBrokerageTransactionSummary(params)) + # + # { + # total_volume: '9.995989116664404', + # total_fees: '0.07996791093331522', + # fee_tier: { + # pricing_tier: 'Advanced 1', + # usd_from: '0', + # usd_to: '1000', + # taker_fee_rate: '0.008', + # maker_fee_rate: '0.006', + # aop_from: '', + # aop_to: '' + # }, + # margin_rate: null, + # goods_and_services_tax: null, + # advanced_trade_only_volume: '9.995989116664404', + # advanced_trade_only_fees: '0.07996791093331522', + # coinbase_pro_volume: '0', + # coinbase_pro_fees: '0', + # total_balance: '', + # has_promo_fee: False + # } + # + unresolvedContractPromises = [] + try: + unresolvedContractPromises = [ + self.v3PublicGetBrokerageMarketProducts(self.extend(params, {'product_type': 'FUTURE'})), + self.v3PublicGetBrokerageMarketProducts(self.extend(params, {'product_type': 'FUTURE', 'contract_expiry_type': 'PERPETUAL'})), + ] + except Exception as e: + unresolvedContractPromises = [] # the sync version of ccxt won't have the promise.all line so the request is made here. Some users can't access perpetual products + promises = await asyncio.gather(*spotUnresolvedPromises) + contractPromises = None + try: + contractPromises = await asyncio.gather(*unresolvedContractPromises) # some users don't have access to contracts + except Exception as e: + contractPromises = [] + spot = self.safe_dict(promises, 0, {}) + fees = self.safe_dict(promises, 1, {}) + expiringFutures = self.safe_dict(contractPromises, 0, {}) + perpetualFutures = self.safe_dict(contractPromises, 1, {}) + expiringFees = self.safe_dict(contractPromises, 0, {}) + perpetualFees = self.safe_dict(contractPromises, 1, {}) + # + # { + # "total_volume": 0, + # "total_fees": 0, + # "fee_tier": { + # "pricing_tier": "", + # "usd_from": "0", + # "usd_to": "10000", + # "taker_fee_rate": "0.006", + # "maker_fee_rate": "0.004" + # }, + # "margin_rate": null, + # "goods_and_services_tax": null, + # "advanced_trade_only_volume": 0, + # "advanced_trade_only_fees": 0, + # "coinbase_pro_volume": 0, + # "coinbase_pro_fees": 0 + # } + # + feeTier = self.safe_dict(fees, 'fee_tier', {}) + expiringFeeTier = self.safe_dict(expiringFees, 'fee_tier', {}) # fee tier null? + perpetualFeeTier = self.safe_dict(perpetualFees, 'fee_tier', {}) # fee tier null? + data = self.safe_list(spot, 'products', []) + result = [] + for i in range(0, len(data)): + result.append(self.parse_spot_market(data[i], feeTier)) + futureData = self.safe_list(expiringFutures, 'products', []) + for i in range(0, len(futureData)): + result.append(self.parse_contract_market(futureData[i], expiringFeeTier)) + perpetualData = self.safe_list(perpetualFutures, 'products', []) + for i in range(0, len(perpetualData)): + result.append(self.parse_contract_market(perpetualData[i], perpetualFeeTier)) + newMarkets = [] + for i in range(0, len(result)): + market = result[i] + info = self.safe_value(market, 'info', {}) + realMarketIds = self.safe_list(info, 'alias_to', []) + length = len(realMarketIds) + if length > 0: + market['alias'] = realMarketIds[0] + else: + market['alias'] = None + newMarkets.append(market) + return newMarkets + + def parse_spot_market(self, market, feeTier) -> MarketInterface: + # + # { + # "product_id": "TONE-USD", + # "price": "0.01523", + # "price_percentage_change_24h": "1.94109772423025", + # "volume_24h": "19773129", + # "volume_percentage_change_24h": "437.0170530929949", + # "base_increment": "1", + # "quote_increment": "0.00001", + # "quote_min_size": "1", + # "quote_max_size": "10000000", + # "base_min_size": "26.7187147229469674", + # "base_max_size": "267187147.2294696735908216", + # "base_name": "TE-FOOD", + # "quote_name": "US Dollar", + # "watched": False, + # "is_disabled": False, + # "new": False, + # "status": "online", + # "cancel_only": False, + # "limit_only": False, + # "post_only": False, + # "trading_disabled": False, + # "auction_mode": False, + # "product_type": "SPOT", + # "quote_currency_id": "USD", + # "base_currency_id": "TONE", + # "fcm_trading_session_details": null, + # "mid_market_price": "" + # } + # + id = self.safe_string(market, 'product_id') + baseId = self.safe_string(market, 'base_currency_id') + quoteId = self.safe_string(market, 'quote_currency_id') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + marketType = self.safe_string_lower(market, 'product_type') + tradingDisabled = self.safe_bool(market, 'trading_disabled') + stablePairs = self.safe_list(self.options, 'stablePairs', []) + defaultTakerFee = self.safe_number(self.fees['trading'], 'taker') + defaultMakerFee = self.safe_number(self.fees['trading'], 'maker') + takerFee = 0.00001 if self.in_array(id, stablePairs) else self.safe_number(feeTier, 'taker_fee_rate', defaultTakerFee) + makerFee = 0.0 if self.in_array(id, stablePairs) else self.safe_number(feeTier, 'maker_fee_rate', defaultMakerFee) + return self.safe_market_structure({ + 'id': id, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': marketType, + 'spot': (marketType == 'spot'), + 'margin': None, + 'swap': False, + 'future': False, + 'option': False, + 'active': not tradingDisabled, + 'contract': False, + 'linear': None, + 'inverse': None, + 'taker': takerFee, + 'maker': makerFee, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.safe_number(market, 'base_increment'), + 'price': self.safe_number_2(market, 'price_increment', 'quote_increment'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(market, 'base_min_size'), + 'max': self.safe_number(market, 'base_max_size'), + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': self.safe_number(market, 'quote_min_size'), + 'max': self.safe_number(market, 'quote_max_size'), + }, + }, + 'created': None, + 'info': market, + }) + + def parse_contract_market(self, market, feeTier) -> MarketInterface: + # expiring + # + # { + # "product_id":"BIT-26APR24-CDE", + # "price":"71145", + # "price_percentage_change_24h":"-2.36722931247427", + # "volume_24h":"108549", + # "volume_percentage_change_24h":"155.78255337197794", + # "base_increment":"1", + # "quote_increment":"0.01", + # "quote_min_size":"0", + # "quote_max_size":"100000000", + # "base_min_size":"1", + # "base_max_size":"100000000", + # "base_name":"", + # "quote_name":"US Dollar", + # "watched":false, + # "is_disabled":false, + # "new":false, + # "status":"", + # "cancel_only":false, + # "limit_only":false, + # "post_only":false, + # "trading_disabled":false, + # "auction_mode":false, + # "product_type":"FUTURE", + # "quote_currency_id":"USD", + # "base_currency_id":"", + # "fcm_trading_session_details":{ + # "is_session_open":true, + # "open_time":"2024-04-08T22:00:00Z", + # "close_time":"2024-04-09T21:00:00Z" + # }, + # "mid_market_price":"71105", + # "alias":"", + # "alias_to":[ + # ], + # "base_display_symbol":"", + # "quote_display_symbol":"USD", + # "view_only":false, + # "price_increment":"5", + # "display_name":"BTC 26 APR 24", + # "product_venue":"FCM", + # "future_product_details":{ + # "venue":"cde", + # "contract_code":"BIT", + # "contract_expiry":"2024-04-26T15:00:00Z", + # "contract_size":"0.01", + # "contract_root_unit":"BTC", + # "group_description":"Nano Bitcoin Futures", + # "contract_expiry_timezone":"Europe/London", + # "group_short_description":"Nano BTC", + # "risk_managed_by":"MANAGED_BY_FCM", + # "contract_expiry_type":"EXPIRING", + # "contract_display_name":"BTC 26 APR 24" + # } + # } + # + # perpetual + # + # { + # "product_id":"ETH-PERP-INTX", + # "price":"3630.98", + # "price_percentage_change_24h":"0.65142426292038", + # "volume_24h":"114020.1501", + # "volume_percentage_change_24h":"63.33650787154869", + # "base_increment":"0.0001", + # "quote_increment":"0.01", + # "quote_min_size":"10", + # "quote_max_size":"50000000", + # "base_min_size":"0.0001", + # "base_max_size":"50000", + # "base_name":"", + # "quote_name":"USDC", + # "watched":false, + # "is_disabled":false, + # "new":false, + # "status":"", + # "cancel_only":false, + # "limit_only":false, + # "post_only":false, + # "trading_disabled":false, + # "auction_mode":false, + # "product_type":"FUTURE", + # "quote_currency_id":"USDC", + # "base_currency_id":"", + # "fcm_trading_session_details":null, + # "mid_market_price":"3630.975", + # "alias":"", + # "alias_to":[], + # "base_display_symbol":"", + # "quote_display_symbol":"USDC", + # "view_only":false, + # "price_increment":"0.01", + # "display_name":"ETH PERP", + # "product_venue":"INTX", + # "future_product_details":{ + # "venue":"", + # "contract_code":"ETH", + # "contract_expiry":null, + # "contract_size":"1", + # "contract_root_unit":"ETH", + # "group_description":"", + # "contract_expiry_timezone":"", + # "group_short_description":"", + # "risk_managed_by":"MANAGED_BY_VENUE", + # "contract_expiry_type":"PERPETUAL", + # "perpetual_details":{ + # "open_interest":"0", + # "funding_rate":"0.000016", + # "funding_time":"2024-04-09T09:00:00.000008Z", + # "max_leverage":"10" + # }, + # "contract_display_name":"ETH PERPETUAL" + # } + # } + # + id = self.safe_string(market, 'product_id') + futureProductDetails = self.safe_dict(market, 'future_product_details', {}) + contractExpiryType = self.safe_string(futureProductDetails, 'contract_expiry_type') + contractSize = self.safe_number(futureProductDetails, 'contract_size') + contractExpire = self.safe_string(futureProductDetails, 'contract_expiry') + expireTimestamp = self.parse8601(contractExpire) + expireDateTime = self.iso8601(expireTimestamp) + isSwap = (contractExpiryType == 'PERPETUAL') + baseId = self.safe_string(futureProductDetails, 'contract_root_unit') + quoteId = self.safe_string(market, 'quote_currency_id') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + tradingDisabled = self.safe_bool(market, 'is_disabled') + symbol = base + '/' + quote + type = None + if isSwap: + type = 'swap' + symbol = symbol + ':' + quote + else: + type = 'future' + symbol = symbol + ':' + quote + '-' + self.yymmdd(expireTimestamp) + takerFeeRate = self.safe_number(feeTier, 'taker_fee_rate') + makerFeeRate = self.safe_number(feeTier, 'maker_fee_rate') + taker = takerFeeRate if takerFeeRate else self.parse_number('0.06') + maker = makerFeeRate if makerFeeRate else self.parse_number('0.04') + return self.safe_market_structure({ + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': quote, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': quoteId, + 'type': type, + 'spot': False, + 'margin': False, + 'swap': isSwap, + 'future': not isSwap, + 'option': False, + 'active': not tradingDisabled, + 'contract': True, + 'linear': True, + 'inverse': False, + 'taker': taker, + 'maker': maker, + 'contractSize': contractSize, + 'expiry': expireTimestamp, + 'expiryDatetime': expireDateTime, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.safe_number(market, 'base_increment'), + 'price': self.safe_number_2(market, 'price_increment', 'quote_increment'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(market, 'base_min_size'), + 'max': self.safe_number(market, 'base_max_size'), + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': self.safe_number(market, 'quote_min_size'), + 'max': self.safe_number(market, 'quote_max_size'), + }, + }, + 'created': None, + 'info': market, + }) + + async def fetch_currencies_from_cache(self, params={}): + options = self.safe_dict(self.options, 'fetchCurrencies', {}) + timestamp = self.safe_integer(options, 'timestamp') + expires = self.safe_integer(options, 'expires', 1000) + now = self.milliseconds() + if (timestamp is None) or ((now - timestamp) > expires): + promises = [ + self.v2PublicGetCurrencies(params), + self.v2PublicGetCurrenciesCrypto(params), + ] + promisesResult = await asyncio.gather(*promises) + fiatResponse = self.safe_dict(promisesResult, 0, {}) + # + # [ + # "data": { + # id: 'IMP', + # name: 'Isle of Man Pound', + # min_size: '0.01' + # }, + # ... + # ] + # + cryptoResponse = self.safe_dict(promisesResult, 1, {}) + # + # { + # asset_id: '9476e3be-b731-47fa-82be-347fabc573d9', + # code: 'AERO', + # name: 'Aerodrome Finance', + # color: '#0433FF', + # sort_index: '340', + # exponent: '8', + # type: 'crypto', + # address_regex: '^(?:0x)?[0-9a-fA-F]{40}$' + # } + # + fiatData = self.safe_list(fiatResponse, 'data', []) + cryptoData = self.safe_list(cryptoResponse, 'data', []) + exchangeRates = await self.v2PublicGetExchangeRates(params) + self.options['fetchCurrencies'] = self.extend(options, { + 'currencies': self.array_concat(fiatData, cryptoData), + 'exchangeRates': exchangeRates, + 'timestamp': now, + }) + return self.safe_dict(self.options, 'fetchCurrencies', {}) + + async def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/api-currencies#get-fiat-currencies + https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/api-exchange-rates#get-exchange-rates + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + promises = [ + self.v2PublicGetCurrencies(params), + self.v2PublicGetCurrenciesCrypto(params), + self.v2PublicGetExchangeRates(params), + ] + promisesResult = await asyncio.gather(*promises) + fiatResponse = self.safe_dict(promisesResult, 0, {}) + # + # [ + # "data": [ + # { + # id: 'IMP', + # name: 'Isle of Man Pound', + # min_size: '0.01' + # }, + # ... + # + cryptoResponse = self.safe_dict(promisesResult, 1, {}) + # + # [ + # "data": [ + # { + # asset_id: '9476e3be-b731-47fa-82be-347fabc573d9', + # code: 'AERO', + # name: 'Aerodrome Finance', + # color: '#0433FF', + # sort_index: '340', + # exponent: '8', + # type: 'crypto', + # address_regex: '^(?:0x)?[0-9a-fA-F]{40}$' + # }, + # ... + # + ratesResponse = self.safe_dict(promisesResult, 2, {}) + fiatData = self.safe_list(fiatResponse, 'data', []) + cryptoData = self.safe_list(cryptoResponse, 'data', []) + ratesData = self.safe_dict(ratesResponse, 'data', {}) + rates = self.safe_dict(ratesData, 'rates', {}) + ratesIds = list(rates.keys()) + currencies = self.array_concat(fiatData, cryptoData) + result: dict = {} + networks: dict = {} + networksById: dict = {} + for i in range(0, len(currencies)): + currency = currencies[i] + assetId = self.safe_string(currency, 'asset_id') + id = self.safe_string_2(currency, 'id', 'code') + code = self.safe_currency_code(id) + name = self.safe_string(currency, 'name') + self.options['networks'][code] = name.lower() + self.options['networksById'][code] = name.lower() + type = 'crypto' if (assetId is not None) else 'fiat' + result[code] = self.safe_currency_structure({ + 'info': currency, + 'id': id, + 'code': code, + 'type': type, + 'name': name, + 'active': True, + 'deposit': None, + 'withdraw': None, + 'fee': None, + 'precision': None, + 'networks': {}, # todo + 'limits': { + 'amount': { + 'min': self.safe_number(currency, 'min_size'), + 'max': None, + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + }, + }) + if assetId is not None: + lowerCaseName = name.lower() + networks[code] = lowerCaseName + networksById[lowerCaseName] = code + # we have to add other currencies here( https://discord.com/channels/1220414409550336183/1220464770239430761/1372215891940479098 ) + for i in range(0, len(ratesIds)): + currencyId = ratesIds[i] + code = self.safe_currency_code(currencyId) + if not (code in result): + result[code] = self.safe_currency_structure({ + 'info': {}, + 'id': currencyId, + 'code': code, + 'type': 'crypto', + 'networks': {}, # todo + }) + self.options['networks'] = self.extend(networks, self.options['networks']) + self.options['networksById'] = self.extend(networksById, self.options['networksById']) + return result + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_getproducts + https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/api-exchange-rates#get-exchange-rates + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.usePrivate]: use private endpoint for fetching tickers + :returns dict: a dictionary of `ticker structures ` + """ + method = self.safe_string(self.options, 'fetchTickers', 'fetchTickersV3') + if method == 'fetchTickersV3': + return await self.fetch_tickers_v3(symbols, params) + return await self.fetch_tickers_v2(symbols, params) + + async def fetch_tickers_v2(self, symbols: Strings = None, params={}) -> Tickers: + await self.load_markets() + symbols = self.market_symbols(symbols) + request: dict = { + # 'currency': 'USD', + } + response = await self.v2PublicGetExchangeRates(self.extend(request, params)) + # + # { + # "data":{ + # "currency":"USD", + # "rates":{ + # "AED":"3.6731", + # "AFN":"103.163942", + # "ALL":"106.973038", + # } + # } + # } + # + data = self.safe_dict(response, 'data', {}) + rates = self.safe_dict(data, 'rates', {}) + quoteId = self.safe_string(data, 'currency') + result: dict = {} + baseIds = list(rates.keys()) + delimiter = '-' + for i in range(0, len(baseIds)): + baseId = baseIds[i] + marketId = baseId + delimiter + quoteId + market = self.safe_market(marketId, None, delimiter) + symbol = market['symbol'] + result[symbol] = self.parse_ticker(rates[baseId], market) + return self.filter_by_array_tickers(result, 'symbol', symbols) + + async def fetch_tickers_v3(self, symbols: Strings = None, params={}) -> Tickers: + await self.load_markets() + symbols = self.market_symbols(symbols) + request: dict = {} + if symbols is not None: + request['product_ids'] = self.market_ids(symbols) + marketType = None + marketType, params = self.handle_market_type_and_params('fetchTickers', self.get_market_from_symbols(symbols), params, 'default') + if marketType is not None and marketType != 'default': + request['product_type'] = 'FUTURE' if (marketType == 'swap') else 'SPOT' + response = None + usePrivate = False + usePrivate, params = self.handle_option_and_params(params, 'fetchTickers', 'usePrivate', False) + if usePrivate: + response = await self.v3PrivateGetBrokerageProducts(self.extend(request, params)) + else: + response = await self.v3PublicGetBrokerageMarketProducts(self.extend(request, params)) + # + # { + # "products": [ + # { + # "product_id": "TONE-USD", + # "price": "0.01523", + # "price_percentage_change_24h": "1.94109772423025", + # "volume_24h": "19773129", + # "volume_percentage_change_24h": "437.0170530929949", + # "base_increment": "1", + # "quote_increment": "0.00001", + # "quote_min_size": "1", + # "quote_max_size": "10000000", + # "base_min_size": "26.7187147229469674", + # "base_max_size": "267187147.2294696735908216", + # "base_name": "TE-FOOD", + # "quote_name": "US Dollar", + # "watched": False, + # "is_disabled": False, + # "new": False, + # "status": "online", + # "cancel_only": False, + # "limit_only": False, + # "post_only": False, + # "trading_disabled": False, + # "auction_mode": False, + # "product_type": "SPOT", + # "quote_currency_id": "USD", + # "base_currency_id": "TONE", + # "fcm_trading_session_details": null, + # "mid_market_price": "" + # }, + # ... + # ], + # "num_products": 549 + # } + # + data = self.safe_list(response, 'products', []) + result: dict = {} + for i in range(0, len(data)): + entry = data[i] + marketId = self.safe_string(entry, 'product_id') + market = self.safe_market(marketId, None, '-') + symbol = market['symbol'] + result[symbol] = self.parse_ticker(entry, market) + return self.filter_by_array_tickers(result, 'symbol', symbols) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_getmarkettrades + https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/api-prices#get-spot-price + https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/api-prices#get-buy-price + https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/api-prices#get-sell-price + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.usePrivate]: whether to use the private endpoint for fetching the ticker + :returns dict: a `ticker structure ` + """ + method = self.safe_string(self.options, 'fetchTicker', 'fetchTickerV3') + if method == 'fetchTickerV3': + return await self.fetch_ticker_v3(symbol, params) + return await self.fetch_ticker_v2(symbol, params) + + async def fetch_ticker_v2(self, symbol: str, params={}): + await self.load_markets() + market = self.market(symbol) + request = self.extend({ + 'symbol': market['id'], + }, params) + spot = await self.v2PublicGetPricesSymbolSpot(request) + # + # {"data":{"base":"BTC","currency":"USD","amount":"48691.23"}} + # + ask = await self.v2PublicGetPricesSymbolBuy(request) + # + # {"data":{"base":"BTC","currency":"USD","amount":"48691.23"}} + # + bid = await self.v2PublicGetPricesSymbolSell(request) + # + # {"data":{"base":"BTC","currency":"USD","amount":"48691.23"}} + # + spotData = self.safe_dict(spot, 'data', {}) + askData = self.safe_dict(ask, 'data', {}) + bidData = self.safe_dict(bid, 'data', {}) + bidAskLast: dict = { + 'bid': self.safe_number(bidData, 'amount'), + 'ask': self.safe_number(askData, 'amount'), + 'price': self.safe_number(spotData, 'amount'), + } + return self.parse_ticker(bidAskLast, market) + + async def fetch_ticker_v3(self, symbol: str, params={}): + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'product_id': market['id'], + 'limit': 1, + } + usePrivate = False + usePrivate, params = self.handle_option_and_params(params, 'fetchTicker', 'usePrivate', False) + response = None + if usePrivate: + response = await self.v3PrivateGetBrokerageProductsProductIdTicker(self.extend(request, params)) + else: + response = await self.v3PublicGetBrokerageMarketProductsProductIdTicker(self.extend(request, params)) + # + # { + # "trades": [ + # { + # "trade_id": "518078013", + # "product_id": "BTC-USD", + # "price": "28208.1", + # "size": "0.00659179", + # "time": "2023-04-04T23:05:34.492746Z", + # "side": "BUY", + # "bid": "", + # "ask": "" + # } + # ], + # "best_bid": "28208.61", + # "best_ask": "28208.62" + # } + # + data = self.safe_list(response, 'trades', []) + ticker = self.parse_ticker(data[0], market) + ticker['bid'] = self.safe_number(response, 'best_bid') + ticker['ask'] = self.safe_number(response, 'best_ask') + return ticker + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # fetchTickerV2 + # + # { + # "bid": 20713.37, + # "ask": 20924.65, + # "price": 20809.83 + # } + # + # fetchTickerV3 + # + # { + # "trade_id": "10209805", + # "product_id": "BTC-USDT", + # "price": "19381.27", + # "size": "0.1", + # "time": "2023-01-13T20:35:41.865970Z", + # "side": "BUY", + # "bid": "", + # "ask": "" + # } + # + # fetchTickersV2 + # + # "48691.23" + # + # fetchTickersV3 + # + # [ + # { + # "product_id": "ETH-USD", + # "price": "4471.59", + # "price_percentage_change_24h": "0.14243387238731", + # "volume_24h": "87329.92990204", + # "volume_percentage_change_24h": "-60.7789801794578", + # "base_increment": "0.00000001", + # "quote_increment": "0.01", + # "quote_min_size": "1", + # "quote_max_size": "150000000", + # "base_min_size": "0.00000001", + # "base_max_size": "42000", + # "base_name": "Ethereum", + # "quote_name": "US Dollar", + # "watched": False, + # "is_disabled": False, + # "new": False, + # "status": "online", + # "cancel_only": False, + # "limit_only": False, + # "post_only": False, + # "trading_disabled": False, + # "auction_mode": False, + # "product_type": "SPOT", + # "quote_currency_id": "USD", + # "base_currency_id": "ETH", + # "fcm_trading_session_details": null, + # "mid_market_price": "", + # "alias": "", + # "alias_to": ["ETH-USDC"], + # "base_display_symbol": "ETH", + # "quote_display_symbol": "USD", + # "view_only": False, + # "price_increment": "0.01", + # "display_name": "ETH-USD", + # "product_venue": "CBE", + # "approximate_quote_24h_volume": "390503641.25", + # "new_at": "2023-01-01T00:00:00Z" + # }, + # ... + # ] + # + # fetchBidsAsks + # + # { + # "product_id": "TRAC-EUR", + # "bids": [ + # { + # "price": "0.2384", + # "size": "386.1" + # } + # ], + # "asks": [ + # { + # "price": "0.2406", + # "size": "672" + # } + # ], + # "time": "2023-06-30T07:15:24.656044Z" + # } + # + bid = self.safe_number(ticker, 'bid') + ask = self.safe_number(ticker, 'ask') + bidVolume = None + askVolume = None + if ('bids' in ticker): + bids = self.safe_list(ticker, 'bids', []) + asks = self.safe_list(ticker, 'asks', []) + firstBid = self.safe_dict(bids, 0, {}) + firstAsk = self.safe_dict(asks, 0, {}) + bid = self.safe_number(firstBid, 'price') + bidVolume = self.safe_number(firstBid, 'size') + ask = self.safe_number(firstAsk, 'price') + askVolume = self.safe_number(firstAsk, 'size') + marketId = self.safe_string(ticker, 'product_id') + market = self.safe_market(marketId, market) + last = self.safe_number(ticker, 'price') + datetime = self.safe_string(ticker, 'time') + return self.safe_ticker({ + 'symbol': market['symbol'], + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + 'bid': bid, + 'ask': ask, + 'last': last, + 'high': None, + 'low': None, + 'bidVolume': bidVolume, + 'askVolume': askVolume, + 'vwap': None, + 'open': None, + 'close': last, + 'previousClose': None, + 'change': None, + 'percentage': self.safe_number(ticker, 'price_percentage_change_24h'), + 'average': None, + 'baseVolume': self.safe_number(ticker, 'volume_24h'), + 'quoteVolume': self.safe_number(ticker, 'approximate_quote_24h_volume'), + 'info': ticker, + }, market) + + def parse_custom_balance(self, response, params={}): + balances = self.safe_list_2(response, 'data', 'accounts', []) + accounts = self.safe_list(params, 'type', self.options['accounts']) + v3Accounts = self.safe_list(params, 'type', self.options['v3Accounts']) + result: dict = {'info': response} + for b in range(0, len(balances)): + balance = balances[b] + type = self.safe_string(balance, 'type') + if self.in_array(type, accounts): + value = self.safe_dict(balance, 'balance') + if value is not None: + currencyId = self.safe_string(value, 'currency') + code = self.safe_currency_code(currencyId) + total = self.safe_string(value, 'amount') + free = total + account = self.safe_dict(result, code) + if account is None: + account = self.account() + account['free'] = free + account['total'] = total + else: + account['free'] = Precise.string_add(account['free'], total) + account['total'] = Precise.string_add(account['total'], total) + result[code] = account + elif self.in_array(type, v3Accounts): + available = self.safe_dict(balance, 'available_balance') + hold = self.safe_dict(balance, 'hold') + if available is not None and hold is not None: + currencyId = self.safe_string(available, 'currency') + code = self.safe_currency_code(currencyId) + used = self.safe_string(hold, 'value') + free = self.safe_string(available, 'value') + total = Precise.string_add(used, free) + account = self.safe_dict(result, code) + if account is None: + account = self.account() + account['free'] = free + account['used'] = used + account['total'] = total + else: + account['free'] = Precise.string_add(account['free'], free) + account['used'] = Precise.string_add(account['used'], used) + account['total'] = Precise.string_add(account['total'], total) + result[code] = account + return self.safe_balance(result) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_getaccounts + https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/api-accounts#list-accounts + https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_getfcmbalancesummary + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.v3]: default False, set True to use v3 api endpoint + :param str [params.type]: "spot"(default) or "swap" or "future" + :param int [params.limit]: default 250, maximum number of accounts to return + :returns dict: a `balance structure ` + """ + await self.load_markets() + request: dict = {} + response = None + isV3 = self.safe_bool(params, 'v3', False) + params = self.omit(params, ['v3']) + marketType = None + marketType, params = self.handle_market_type_and_params('fetchBalance', None, params) + method = self.safe_string(self.options, 'fetchBalance', 'v3PrivateGetBrokerageAccounts') + if marketType == 'future': + response = await self.v3PrivateGetBrokerageCfmBalanceSummary(self.extend(request, params)) + elif (isV3) or (method == 'v3PrivateGetBrokerageAccounts'): + request['limit'] = 250 + response = await self.v3PrivateGetBrokerageAccounts(self.extend(request, params)) + else: + request['limit'] = 250 + response = await self.v2PrivateGetAccounts(self.extend(request, params)) + # + # v2PrivateGetAccounts + # { + # "pagination":{ + # "ending_before":null, + # "starting_after":null, + # "previous_ending_before":null, + # "next_starting_after":"6b17acd6-2e68-5eb0-9f45-72d67cef578a", + # "limit":100, + # "order":"desc", + # "previous_uri":null, + # "next_uri":"/v2/accounts?limit=100\u0026starting_after=6b17acd6-2e68-5eb0-9f45-72d67cef578b" + # }, + # "data":[ + # { + # "id":"94ad58bc-0f15-5309-b35a-a4c86d7bad60", + # "name":"MINA Wallet", + # "primary":false, + # "type":"wallet", + # "currency":{ + # "code":"MINA", + # "name":"Mina", + # "color":"#EA6B48", + # "sort_index":397, + # "exponent":9, + # "type":"crypto", + # "address_regex":"^(B62)[A-Za-z0-9]{52}$", + # "asset_id":"a4ffc575-942c-5e26-b70c-cb3befdd4229", + # "slug":"mina" + # }, + # "balance":{"amount":"0.000000000","currency":"MINA"}, + # "created_at":"2022-03-25T00:36:16Z", + # "updated_at":"2022-03-25T00:36:16Z", + # "resource":"account", + # "resource_path":"/v2/accounts/94ad58bc-0f15-5309-b35a-a4c86d7bad60", + # "allow_deposits":true, + # "allow_withdrawals":true + # }, + # ] + # } + # + # v3PrivateGetBrokerageAccounts + # { + # "accounts": [ + # { + # "uuid": "11111111-1111-1111-1111-111111111111", + # "name": "USDC Wallet", + # "currency": "USDC", + # "available_balance": { + # "value": "0.0000000000000000", + # "currency": "USDC" + # }, + # "default": True, + # "active": True, + # "created_at": "2023-01-04T06:20:06.456Z", + # "updated_at": "2023-01-04T06:20:07.181Z", + # "deleted_at": null, + # "type": "ACCOUNT_TYPE_CRYPTO", + # "ready": False, + # "hold": { + # "value": "0.0000000000000000", + # "currency": "USDC" + # } + # }, + # ... + # ], + # "has_next": False, + # "cursor": "", + # "size": 9 + # } + # + params['type'] = marketType + return self.parse_custom_balance(response, params) + + async def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + Fetch the history of changes, i.e. actions done by the user or operations that altered the balance. Will return staking rewards, and crypto deposits or withdrawals. + + https://docs.cdp.coinbase.com/coinbase-app/docs/api-transactions#list-transactions + + :param str [code]: unified currency code, default is None + :param int [since]: timestamp in ms of the earliest ledger entry, default is None + :param int [limit]: max number of ledger entries to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict: a `ledger structure ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchLedger', 'paginate') + if paginate: + return await self.fetch_paginated_call_cursor('fetchLedger', code, since, limit, params, 'next_starting_after', 'starting_after', None, 100) + currency = None + if code is not None: + currency = self.currency(code) + request = None + request, params = await self.prepare_account_request_with_currency_code(code, limit, params) + # for pagination use parameter 'starting_after' + # the value for the next page can be obtained from the result of the previous call in the 'pagination' field + # eg: instance.last_http_response -> pagination.next_starting_after + response = await self.v2PrivateGetAccountsAccountIdTransactions(self.extend(request, params)) + ledger = self.parse_ledger(response['data'], currency, since, limit) + length = len(ledger) + if length == 0: + return ledger + lastIndex = length - 1 + last = self.safe_dict(ledger, lastIndex) + pagination = self.safe_dict(response, 'pagination', {}) + cursor = self.safe_string(pagination, 'next_starting_after') + if (cursor is not None) and (cursor != ''): + last['info']['next_starting_after'] = cursor + ledger[lastIndex] = last + return ledger + + def parse_ledger_entry_status(self, status): + types: dict = { + 'completed': 'ok', + } + return self.safe_string(types, status, status) + + def parse_ledger_entry_type(self, type): + types: dict = { + 'buy': 'trade', + 'sell': 'trade', + 'fiat_deposit': 'transaction', + 'fiat_withdrawal': 'transaction', + 'exchange_deposit': 'transaction', # fiat withdrawal(from coinbase to coinbasepro) + 'exchange_withdrawal': 'transaction', # fiat deposit(to coinbase from coinbasepro) + 'send': 'transaction', # crypto deposit OR withdrawal + 'pro_deposit': 'transaction', # crypto withdrawal(from coinbase to coinbasepro) + 'pro_withdrawal': 'transaction', # crypto deposit(to coinbase from coinbasepro) + } + return self.safe_string(types, type, type) + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + # + # crypto deposit transaction + # + # { + # "id": "34e4816b-4c8c-5323-a01c-35a9fa26e490", + # "type": "send", + # "status": "completed", + # "amount": {amount: "28.31976528", currency: "BCH"}, + # "native_amount": {amount: "2799.65", currency: "GBP"}, + # "description": null, + # "created_at": "2019-02-28T12:35:20Z", + # "updated_at": "2019-02-28T12:43:24Z", + # "resource": "transaction", + # "resource_path": "/v2/accounts/c01d7364-edd7-5f3a-bd1d-de53d4cbb25e/transactions/34e4816b-4c8c-5323-a01c-35a9fa26e490", + # "instant_exchange": False, + # "network": { + # "status": "confirmed", + # "hash": "56222d865dae83774fccb2efbd9829cf08c75c94ce135bfe4276f3fb46d49701", + # "transaction_url": "https://bch.btc.com/56222d865dae83774fccb2efbd9829cf08c75c94ce135bfe4276f3fb46d49701" + # }, + # "from": {resource: "bitcoin_cash_network", currency: "BCH"}, + # "details": {title: 'Received Bitcoin Cash', subtitle: "From Bitcoin Cash address"} + # } + # + # crypto withdrawal transaction + # + # { + # "id": "459aad99-2c41-5698-ac71-b6b81a05196c", + # "type": "send", + # "status": "completed", + # "amount": {amount: "-0.36775642", currency: "BTC"}, + # "native_amount": {amount: "-1111.65", currency: "GBP"}, + # "description": null, + # "created_at": "2019-03-20T08:37:07Z", + # "updated_at": "2019-03-20T08:49:33Z", + # "resource": "transaction", + # "resource_path": "/v2/accounts/c6afbd34-4bd0-501e-8616-4862c193cd84/transactions/459aad99-2c41-5698-ac71-b6b81a05196c", + # "instant_exchange": False, + # "network": { + # "status": "confirmed", + # "hash": "2732bbcf35c69217c47b36dce64933d103895277fe25738ffb9284092701e05b", + # "transaction_url": "https://blockchain.info/tx/2732bbcf35c69217c47b36dce64933d103895277fe25738ffb9284092701e05b", + # "transaction_fee": {amount: "0.00000000", currency: "BTC"}, + # "transaction_amount": {amount: "0.36775642", currency: "BTC"}, + # "confirmations": 15682 + # }, + # "to": { + # "resource": "bitcoin_address", + # "address": "1AHnhqbvbYx3rnZx8uC7NbFZaTe4tafFHX", + # "currency": "BTC", + # "address_info": {address: "1AHnhqbvbYx3rnZx8uC7NbFZaTe4tafFHX"} + # }, + # "idem": "da0a2f14-a2af-4c5a-a37e-d4484caf582bsend", + # "application": { + # "id": "5756ab6e-836b-553b-8950-5e389451225d", + # "resource": "application", + # "resource_path": "/v2/applications/5756ab6e-836b-553b-8950-5e389451225d" + # }, + # "details": {title: 'Sent Bitcoin', subtitle: "To Bitcoin address"} + # } + # + # withdrawal transaction from coinbase to coinbasepro + # + # { + # "id": "5b1b9fb8-5007-5393-b923-02903b973fdc", + # "type": "pro_deposit", + # "status": "completed", + # "amount": {amount: "-0.00001111", currency: "BCH"}, + # "native_amount": {amount: "0.00", currency: "GBP"}, + # "description": null, + # "created_at": "2019-02-28T13:31:58Z", + # "updated_at": "2019-02-28T13:31:58Z", + # "resource": "transaction", + # "resource_path": "/v2/accounts/c01d7364-edd7-5f3a-bd1d-de53d4cbb25e/transactions/5b1b9fb8-5007-5393-b923-02903b973fdc", + # "instant_exchange": False, + # "application": { + # "id": "5756ab6e-836b-553b-8950-5e389451225d", + # "resource": "application", + # "resource_path": "/v2/applications/5756ab6e-836b-553b-8950-5e389451225d" + # }, + # "details": {title: 'Transferred Bitcoin Cash', subtitle: "To Coinbase Pro"} + # } + # + # withdrawal transaction from coinbase to gdax + # + # { + # "id": "badb7313-a9d3-5c07-abd0-00f8b44199b1", + # "type": "exchange_deposit", + # "status": "completed", + # "amount": {amount: "-0.43704149", currency: "BCH"}, + # "native_amount": {amount: "-51.90", currency: "GBP"}, + # "description": null, + # "created_at": "2019-03-19T10:30:40Z", + # "updated_at": "2019-03-19T10:30:40Z", + # "resource": "transaction", + # "resource_path": "/v2/accounts/c01d7364-edd7-5f3a-bd1d-de53d4cbb25e/transactions/badb7313-a9d3-5c07-abd0-00f8b44199b1", + # "instant_exchange": False, + # "details": {title: 'Transferred Bitcoin Cash', subtitle: "To GDAX"} + # } + # + # deposit transaction from gdax to coinbase + # + # { + # "id": "9c4b642c-8688-58bf-8962-13cef64097de", + # "type": "exchange_withdrawal", + # "status": "completed", + # "amount": {amount: "0.57729420", currency: "BTC"}, + # "native_amount": {amount: "4418.72", currency: "GBP"}, + # "description": null, + # "created_at": "2018-02-17T11:33:33Z", + # "updated_at": "2018-02-17T11:33:33Z", + # "resource": "transaction", + # "resource_path": "/v2/accounts/c6afbd34-4bd0-501e-8616-4862c193cd84/transactions/9c4b642c-8688-58bf-8962-13cef64097de", + # "instant_exchange": False, + # "details": {title: 'Transferred Bitcoin', subtitle: "From GDAX"} + # } + # + # deposit transaction from coinbasepro to coinbase + # + # { + # "id": "8d6dd0b9-3416-568a-889d-8f112fae9e81", + # "type": "pro_withdrawal", + # "status": "completed", + # "amount": {amount: "0.40555386", currency: "BTC"}, + # "native_amount": {amount: "1140.27", currency: "GBP"}, + # "description": null, + # "created_at": "2019-03-04T19:41:58Z", + # "updated_at": "2019-03-04T19:41:58Z", + # "resource": "transaction", + # "resource_path": "/v2/accounts/c6afbd34-4bd0-501e-8616-4862c193cd84/transactions/8d6dd0b9-3416-568a-889d-8f112fae9e81", + # "instant_exchange": False, + # "application": { + # "id": "5756ab6e-836b-553b-8950-5e389451225d", + # "resource": "application", + # "resource_path": "/v2/applications/5756ab6e-836b-553b-8950-5e389451225d" + # }, + # "details": {title: 'Transferred Bitcoin', subtitle: "From Coinbase Pro"} + # } + # + # sell trade + # + # { + # "id": "a9409207-df64-585b-97ab-a50780d2149e", + # "type": "sell", + # "status": "completed", + # "amount": {amount: "-9.09922880", currency: "BTC"}, + # "native_amount": {amount: "-7285.73", currency: "GBP"}, + # "description": null, + # "created_at": "2017-03-27T15:38:34Z", + # "updated_at": "2017-03-27T15:38:34Z", + # "resource": "transaction", + # "resource_path": "/v2/accounts/c6afbd34-4bd0-501e-8616-4862c193cd84/transactions/a9409207-df64-585b-97ab-a50780d2149e", + # "instant_exchange": False, + # "sell": { + # "id": "e3550b4d-8ae6-5de3-95fe-1fb01ba83051", + # "resource": "sell", + # "resource_path": "/v2/accounts/c6afbd34-4bd0-501e-8616-4862c193cd84/sells/e3550b4d-8ae6-5de3-95fe-1fb01ba83051" + # }, + # "details": { + # "title": "Sold Bitcoin", + # "subtitle": "Using EUR Wallet", + # "payment_method_name": "EUR Wallet" + # } + # } + # + # buy trade + # + # { + # "id": "63eeed67-9396-5912-86e9-73c4f10fe147", + # "type": "buy", + # "status": "completed", + # "amount": {amount: "2.39605772", currency: "ETH"}, + # "native_amount": {amount: "98.31", currency: "GBP"}, + # "description": null, + # "created_at": "2017-03-27T09:07:56Z", + # "updated_at": "2017-03-27T09:07:57Z", + # "resource": "transaction", + # "resource_path": "/v2/accounts/8902f85d-4a69-5d74-82fe-8e390201bda7/transactions/63eeed67-9396-5912-86e9-73c4f10fe147", + # "instant_exchange": False, + # "buy": { + # "id": "20b25b36-76c6-5353-aa57-b06a29a39d82", + # "resource": "buy", + # "resource_path": "/v2/accounts/8902f85d-4a69-5d74-82fe-8e390201bda7/buys/20b25b36-76c6-5353-aa57-b06a29a39d82" + # }, + # "details": { + # "title": "Bought Ethereum", + # "subtitle": "Using EUR Wallet", + # "payment_method_name": "EUR Wallet" + # } + # } + # + # fiat deposit transaction + # + # { + # "id": "04ed4113-3732-5b0c-af86-b1d2146977d0", + # "type": "fiat_deposit", + # "status": "completed", + # "amount": {amount: "114.02", currency: "EUR"}, + # "native_amount": {amount: "97.23", currency: "GBP"}, + # "description": null, + # "created_at": "2017-02-09T07:01:21Z", + # "updated_at": "2017-02-09T07:01:22Z", + # "resource": "transaction", + # "resource_path": "/v2/accounts/91cd2d36-3a91-55b6-a5d4-0124cf105483/transactions/04ed4113-3732-5b0c-af86-b1d2146977d0", + # "instant_exchange": False, + # "fiat_deposit": { + # "id": "f34c19f3-b730-5e3d-9f72-96520448677a", + # "resource": "fiat_deposit", + # "resource_path": "/v2/accounts/91cd2d36-3a91-55b6-a5d4-0124cf105483/deposits/f34c19f3-b730-5e3d-9f72-96520448677a" + # }, + # "details": { + # "title": "Deposited funds", + # "subtitle": "From SEPA Transfer(GB47 BARC 20..., reference CBADVI)", + # "payment_method_name": "SEPA Transfer(GB47 BARC 20..., reference CBADVI)" + # } + # } + # + # fiat withdrawal transaction + # + # { + # "id": "957d98e2-f80e-5e2f-a28e-02945aa93079", + # "type": "fiat_withdrawal", + # "status": "completed", + # "amount": {amount: "-11000.00", currency: "EUR"}, + # "native_amount": {amount: "-9698.22", currency: "GBP"}, + # "description": null, + # "created_at": "2017-12-06T13:19:19Z", + # "updated_at": "2017-12-06T13:19:19Z", + # "resource": "transaction", + # "resource_path": "/v2/accounts/91cd2d36-3a91-55b6-a5d4-0124cf105483/transactions/957d98e2-f80e-5e2f-a28e-02945aa93079", + # "instant_exchange": False, + # "fiat_withdrawal": { + # "id": "f4bf1fd9-ab3b-5de7-906d-ed3e23f7a4e7", + # "resource": "fiat_withdrawal", + # "resource_path": "/v2/accounts/91cd2d36-3a91-55b6-a5d4-0124cf105483/withdrawals/f4bf1fd9-ab3b-5de7-906d-ed3e23f7a4e7" + # }, + # "details": { + # "title": "Withdrew funds", + # "subtitle": "To HSBC BANK PLC(GB74 MIDL...)", + # "payment_method_name": "HSBC BANK PLC(GB74 MIDL...)" + # } + # } + # + amountInfo = self.safe_dict(item, 'amount', {}) + amount = self.safe_string(amountInfo, 'amount') + direction = None + if Precise.string_lt(amount, '0'): + direction = 'out' + amount = Precise.string_neg(amount) + else: + direction = 'in' + currencyId = self.safe_string(amountInfo, 'currency') + code = self.safe_currency_code(currencyId, currency) + currency = self.safe_currency(currencyId, currency) + # + # the address and txid do not belong to the unified ledger structure + # + # address = None + # if item['to']: + # address = self.safe_string(item['to'], 'address') + # } + # txid = None + # + fee = None + networkInfo = self.safe_dict(item, 'network', {}) + # txid = network['hash'] # txid does not belong to the unified ledger structure + feeInfo = self.safe_dict(networkInfo, 'transaction_fee') + if feeInfo is not None: + feeCurrencyId = self.safe_string(feeInfo, 'currency') + feeCurrencyCode = self.safe_currency_code(feeCurrencyId, currency) + feeAmount = self.safe_number(feeInfo, 'amount') + fee = { + 'cost': feeAmount, + 'currency': feeCurrencyCode, + } + timestamp = self.parse8601(self.safe_string(item, 'created_at')) + id = self.safe_string(item, 'id') + type = self.parse_ledger_entry_type(self.safe_string(item, 'type')) + status = self.parse_ledger_entry_status(self.safe_string(item, 'status')) + path = self.safe_string(item, 'resource_path') + accountId = None + if path is not None: + parts = path.split('/') + numParts = len(parts) + if numParts > 3: + accountId = parts[3] + return self.safe_ledger_entry({ + 'info': item, + 'id': id, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'direction': direction, + 'account': accountId, + 'referenceId': None, + 'referenceAccount': None, + 'type': type, + 'currency': code, + 'amount': self.parse_number(amount), + 'before': None, + 'after': None, + 'status': status, + 'fee': fee, + }, currency) + + async def find_account_id(self, code, params={}): + await self.load_markets() + await self.load_accounts(False, params) + for i in range(0, len(self.accounts)): + account = self.accounts[i] + if account['code'] == code: + return account['id'] + return None + + def prepare_account_request(self, limit: Int = None, params={}): + accountId = self.safe_string_2(params, 'account_id', 'accountId') + if accountId is None: + raise ArgumentsRequired(self.id + ' prepareAccountRequest() method requires an account_id(or accountId) parameter') + request: dict = { + 'account_id': accountId, + } + if limit is not None: + request['limit'] = limit + return request + + async def prepare_account_request_with_currency_code(self, code: Str = None, limit: Int = None, params={}): + accountId = self.safe_string_2(params, 'account_id', 'accountId') + params = self.omit(params, ['account_id', 'accountId']) + if accountId is None: + if code is None: + raise ArgumentsRequired(self.id + ' prepareAccountRequestWithCurrencyCode() method requires an account_id(or accountId) parameter OR a currency code argument') + accountId = await self.find_account_id(code, params) + if accountId is None: + raise ExchangeError(self.id + ' prepareAccountRequestWithCurrencyCode() could not find account id for ' + code + '. You might try to generate the deposit address in the website for that coin first.') + request: dict = { + 'account_id': accountId, + } + if limit is not None: + request['limit'] = limit + return [request, params] + + async def create_market_buy_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market buy order by providing the symbol and cost + + https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_postorder + + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' createMarketBuyOrderWithCost() supports spot orders only') + params['createMarketBuyOrderRequiresPrice'] = False + return await self.create_order(symbol, 'market', 'buy', cost, None, params) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_postorder + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency, quote currency for 'market' 'buy' orders + :param float [price]: the price to fulfill the order, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.stopPrice]: price to trigger stop orders + :param float [params.triggerPrice]: price to trigger stop orders + :param float [params.stopLossPrice]: price to trigger stop-loss orders + :param float [params.takeProfitPrice]: price to trigger take-profit orders + :param bool [params.postOnly]: True or False + :param str [params.timeInForce]: 'GTC', 'IOC', 'GTD' or 'PO', 'FOK' + :param str [params.stop_direction]: 'UNKNOWN_STOP_DIRECTION', 'STOP_DIRECTION_STOP_UP', 'STOP_DIRECTION_STOP_DOWN' the direction the stopPrice is triggered from + :param str [params.end_time]: '2023-05-25T17:01:05.092Z' for 'GTD' orders + :param float [params.cost]: *spot market buy only* the quote quantity that can be used alternative for the amount + :param boolean [params.preview]: default to False, wether to use the test/preview endpoint or not + :param float [params.leverage]: default to 1, the leverage to use for the order + :param str [params.marginMode]: 'cross' or 'isolated' + :param str [params.retail_portfolio_id]: portfolio uid + :param boolean [params.is_max]: Used in conjunction with tradable_balance to indicate the user wants to use their entire tradable balance + :param str [params.tradable_balance]: amount of tradable balance + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + id = self.safe_string(self.options, 'brokerId', 'ccxt') + request: dict = { + 'client_order_id': id + '-' + self.uuid(), + 'product_id': market['id'], + 'side': side.upper(), + } + triggerPrice = self.safe_number_n(params, ['stopPrice', 'stop_price', 'triggerPrice']) + stopLossPrice = self.safe_number(params, 'stopLossPrice') + takeProfitPrice = self.safe_number(params, 'takeProfitPrice') + isStop = triggerPrice is not None + isStopLoss = stopLossPrice is not None + isTakeProfit = takeProfitPrice is not None + timeInForce = self.safe_string(params, 'timeInForce') + postOnly = True if (timeInForce == 'PO') else self.safe_bool_2(params, 'postOnly', 'post_only', False) + endTime = self.safe_string(params, 'end_time') + stopDirection = self.safe_string(params, 'stop_direction') + if type == 'limit': + if isStop: + if stopDirection is None: + stopDirection = 'STOP_DIRECTION_STOP_DOWN' if (side == 'buy') else 'STOP_DIRECTION_STOP_UP' + if (timeInForce == 'GTD') or (endTime is not None): + if endTime is None: + raise ExchangeError(self.id + ' createOrder() requires an end_time parameter for a GTD order') + request['order_configuration'] = { + 'stop_limit_stop_limit_gtd': { + 'base_size': self.amount_to_precision(symbol, amount), + 'limit_price': self.price_to_precision(symbol, price), + 'stop_price': self.price_to_precision(symbol, triggerPrice), + 'stop_direction': stopDirection, + 'end_time': endTime, + }, + } + else: + request['order_configuration'] = { + 'stop_limit_stop_limit_gtc': { + 'base_size': self.amount_to_precision(symbol, amount), + 'limit_price': self.price_to_precision(symbol, price), + 'stop_price': self.price_to_precision(symbol, triggerPrice), + 'stop_direction': stopDirection, + }, + } + elif isStopLoss or isTakeProfit: + tpslPrice = None + if isStopLoss: + if stopDirection is None: + stopDirection = 'STOP_DIRECTION_STOP_UP' if (side == 'buy') else 'STOP_DIRECTION_STOP_DOWN' + tpslPrice = self.price_to_precision(symbol, stopLossPrice) + else: + if stopDirection is None: + stopDirection = 'STOP_DIRECTION_STOP_DOWN' if (side == 'buy') else 'STOP_DIRECTION_STOP_UP' + tpslPrice = self.price_to_precision(symbol, takeProfitPrice) + request['order_configuration'] = { + 'stop_limit_stop_limit_gtc': { + 'base_size': self.amount_to_precision(symbol, amount), + 'limit_price': self.price_to_precision(symbol, price), + 'stop_price': tpslPrice, + 'stop_direction': stopDirection, + }, + } + else: + if (timeInForce == 'GTD') or (endTime is not None): + if endTime is None: + raise ExchangeError(self.id + ' createOrder() requires an end_time parameter for a GTD order') + request['order_configuration'] = { + 'limit_limit_gtd': { + 'base_size': self.amount_to_precision(symbol, amount), + 'limit_price': self.price_to_precision(symbol, price), + 'end_time': endTime, + 'post_only': postOnly, + }, + } + elif timeInForce == 'IOC': + request['order_configuration'] = { + 'sor_limit_ioc': { + 'base_size': self.amount_to_precision(symbol, amount), + 'limit_price': self.price_to_precision(symbol, price), + }, + } + elif timeInForce == 'FOK': + request['order_configuration'] = { + 'limit_limit_fok': { + 'base_size': self.amount_to_precision(symbol, amount), + 'limit_price': self.price_to_precision(symbol, price), + }, + } + else: + request['order_configuration'] = { + 'limit_limit_gtc': { + 'base_size': self.amount_to_precision(symbol, amount), + 'limit_price': self.price_to_precision(symbol, price), + 'post_only': postOnly, + }, + } + else: + if isStop or isStopLoss or isTakeProfit: + raise NotSupported(self.id + ' createOrder() only stop limit orders are supported') + if market['spot'] and (side == 'buy'): + total = None + createMarketBuyOrderRequiresPrice = True + createMarketBuyOrderRequiresPrice, params = self.handle_option_and_params(params, 'createOrder', 'createMarketBuyOrderRequiresPrice', True) + cost = self.safe_number(params, 'cost') + params = self.omit(params, 'cost') + if cost is not None: + total = self.cost_to_precision(symbol, cost) + elif createMarketBuyOrderRequiresPrice: + if price is None: + raise InvalidOrder(self.id + ' createOrder() requires a price argument for market buy orders on spot markets to calculate the total amount to spend(amount * price), alternatively set the createMarketBuyOrderRequiresPrice option or param to False and pass the cost to spend in the amount argument') + else: + amountString = self.number_to_string(amount) + priceString = self.number_to_string(price) + costRequest = Precise.string_mul(amountString, priceString) + total = self.cost_to_precision(symbol, costRequest) + else: + total = self.cost_to_precision(symbol, amount) + request['order_configuration'] = { + 'market_market_ioc': { + 'quote_size': total, + }, + } + else: + request['order_configuration'] = { + 'market_market_ioc': { + 'base_size': self.amount_to_precision(symbol, amount), + }, + } + marginMode = self.safe_string(params, 'marginMode') + if marginMode is not None: + if marginMode == 'isolated': + request['margin_type'] = 'ISOLATED' + elif marginMode == 'cross': + request['margin_type'] = 'CROSS' + params = self.omit(params, ['timeInForce', 'triggerPrice', 'stopLossPrice', 'takeProfitPrice', 'stopPrice', 'stop_price', 'stopDirection', 'stop_direction', 'clientOrderId', 'postOnly', 'post_only', 'end_time', 'marginMode']) + preview = self.safe_bool_2(params, 'preview', 'test', False) + response = None + if preview: + params = self.omit(params, ['preview', 'test']) + request = self.omit(request, 'client_order_id') + response = await self.v3PrivatePostBrokerageOrdersPreview(self.extend(request, params)) + else: + response = await self.v3PrivatePostBrokerageOrders(self.extend(request, params)) + # + # successful order + # + # { + # "success": True, + # "failure_reason": "UNKNOWN_FAILURE_REASON", + # "order_id": "52cfe5e2-0b29-4c19-a245-a6a773de5030", + # "success_response": { + # "order_id": "52cfe5e2-0b29-4c19-a245-a6a773de5030", + # "product_id": "LTC-BTC", + # "side": "SELL", + # "client_order_id": "4d760580-6fca-4094-a70b-ebcca8626288" + # }, + # "order_configuration": null + # } + # + # failed order + # + # { + # "success": False, + # "failure_reason": "UNKNOWN_FAILURE_REASON", + # "order_id": "", + # "error_response": { + # "error": "UNSUPPORTED_ORDER_CONFIGURATION", + # "message": "source is not enabled for trading", + # "error_details": "", + # "new_order_failure_reason": "UNSUPPORTED_ORDER_CONFIGURATION" + # }, + # "order_configuration": { + # "limit_limit_gtc": { + # "base_size": "100", + # "limit_price": "40000", + # "post_only": False + # } + # } + # } + # + success = self.safe_bool(response, 'success') + if success is not True: + errorResponse = self.safe_dict(response, 'error_response') + errorTitle = self.safe_string(errorResponse, 'error') + errorMessage = self.safe_string(errorResponse, 'message') + if errorResponse is not None: + self.throw_exactly_matched_exception(self.exceptions['exact'], errorTitle, errorMessage) + self.throw_broadly_matched_exception(self.exceptions['broad'], errorTitle, errorMessage) + raise ExchangeError(errorMessage) + data = self.safe_dict(response, 'success_response', {}) + return self.parse_order(data, market) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder + # + # { + # "order_id": "52cfe5e2-0b29-4c19-a245-a6a773de5030", + # "product_id": "LTC-BTC", + # "side": "SELL", + # "client_order_id": "4d760580-6fca-4094-a70b-ebcca8626288" + # } + # + # cancelOrder, cancelOrders + # + # { + # "success": True, + # "failure_reason": "UNKNOWN_CANCEL_FAILURE_REASON", + # "order_id": "bb8851a3-4fda-4a2c-aa06-9048db0e0f0d" + # } + # + # fetchOrder, fetchOrders, fetchOpenOrders, fetchClosedOrders, fetchCanceledOrders + # + # { + # "order_id": "9bc1eb3b-5b46-4b71-9628-ae2ed0cca75b", + # "product_id": "LTC-BTC", + # "user_id": "1111111-1111-1111-1111-111111111111", + # "order_configuration": { + # "limit_limit_gtc": { + # "base_size": "0.2", + # "limit_price": "0.006", + # "post_only": False + # }, + # "stop_limit_stop_limit_gtc": { + # "base_size": "48.54", + # "limit_price": "6.998", + # "stop_price": "7.0687", + # "stop_direction": "STOP_DIRECTION_STOP_DOWN" + # } + # }, + # "side": "SELL", + # "client_order_id": "e5fe8482-05bb-428f-ad4d-dbc8ce39239c", + # "status": "OPEN", + # "time_in_force": "GOOD_UNTIL_CANCELLED", + # "created_time": "2023-01-16T23:37:23.947030Z", + # "completion_percentage": "0", + # "filled_size": "0", + # "average_filled_price": "0", + # "fee": "", + # "number_of_fills": "0", + # "filled_value": "0", + # "pending_cancel": False, + # "size_in_quote": False, + # "total_fees": "0", + # "size_inclusive_of_fees": False, + # "total_value_after_fees": "0", + # "trigger_status": "INVALID_ORDER_TYPE", + # "order_type": "LIMIT", + # "reject_reason": "REJECT_REASON_UNSPECIFIED", + # "settled": False, + # "product_type": "SPOT", + # "reject_message": "", + # "cancel_message": "" + # } + # + marketId = self.safe_string(order, 'product_id') + symbol = self.safe_symbol(marketId, market, '-') + if symbol is not None: + market = self.safe_market(symbol, market) + orderConfiguration = self.safe_dict(order, 'order_configuration', {}) + limitGTC = self.safe_dict(orderConfiguration, 'limit_limit_gtc') + limitGTD = self.safe_dict(orderConfiguration, 'limit_limit_gtd') + limitIOC = self.safe_dict(orderConfiguration, 'sor_limit_ioc') + stopLimitGTC = self.safe_dict(orderConfiguration, 'stop_limit_stop_limit_gtc') + stopLimitGTD = self.safe_dict(orderConfiguration, 'stop_limit_stop_limit_gtd') + marketIOC = self.safe_dict(orderConfiguration, 'market_market_ioc') + isLimit = ((limitGTC is not None) or (limitGTD is not None) or (limitIOC is not None)) + isStop = ((stopLimitGTC is not None) or (stopLimitGTD is not None)) + price = None + amount = None + postOnly = None + triggerPrice = None + if isLimit: + target = None + if limitGTC is not None: + target = limitGTC + elif limitGTD is not None: + target = limitGTD + else: + target = limitIOC + price = self.safe_string(target, 'limit_price') + amount = self.safe_string(target, 'base_size') + postOnly = self.safe_bool(target, 'post_only') + elif isStop: + stopTarget = stopLimitGTC if (stopLimitGTC is not None) else stopLimitGTD + price = self.safe_string(stopTarget, 'limit_price') + amount = self.safe_string(stopTarget, 'base_size') + postOnly = self.safe_bool(stopTarget, 'post_only') + triggerPrice = self.safe_string(stopTarget, 'stop_price') + else: + amount = self.safe_string(marketIOC, 'base_size') + datetime = self.safe_string(order, 'created_time') + totalFees = self.safe_string(order, 'total_fees') + currencyFee = None + if (totalFees is not None) and (market is not None): + currencyFee = market['quote'] + return self.safe_order({ + 'info': order, + 'id': self.safe_string(order, 'order_id'), + 'clientOrderId': self.safe_string(order, 'client_order_id'), + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + 'lastTradeTimestamp': None, + 'symbol': symbol, + 'type': self.parse_order_type(self.safe_string(order, 'order_type')), + 'timeInForce': self.parse_time_in_force(self.safe_string(order, 'time_in_force')), + 'postOnly': postOnly, + 'side': self.safe_string_lower(order, 'side'), + 'price': price, + 'triggerPrice': triggerPrice, + 'amount': amount, + 'filled': self.safe_string(order, 'filled_size'), + 'remaining': None, + 'cost': None, + 'average': self.safe_string(order, 'average_filled_price'), + 'status': self.parse_order_status(self.safe_string(order, 'status')), + 'fee': { + 'cost': self.safe_string(order, 'total_fees'), + 'currency': currencyFee, + }, + 'trades': None, + }, market) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'OPEN': 'open', + 'FILLED': 'closed', + 'CANCELLED': 'canceled', + 'EXPIRED': 'canceled', + 'FAILED': 'canceled', + 'UNKNOWN_ORDER_STATUS': None, + } + return self.safe_string(statuses, status, status) + + def parse_order_type(self, type: Str): + if type == 'UNKNOWN_ORDER_TYPE': + return None + types: dict = { + 'MARKET': 'market', + 'LIMIT': 'limit', + 'STOP': 'limit', + 'STOP_LIMIT': 'limit', + } + return self.safe_string(types, type, type) + + def parse_time_in_force(self, timeInForce: Str): + timeInForces: dict = { + 'GOOD_UNTIL_CANCELLED': 'GTC', + 'GOOD_UNTIL_DATE_TIME': 'GTD', + 'IMMEDIATE_OR_CANCEL': 'IOC', + 'FILL_OR_KILL': 'FOK', + 'UNKNOWN_TIME_IN_FORCE': None, + } + return self.safe_string(timeInForces, timeInForce, timeInForce) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_cancelorders + + :param str id: order id + :param str symbol: not used by coinbase cancelOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + orders = await self.cancel_orders([id], symbol, params) + return self.safe_dict(orders, 0, {}) + + async def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): + """ + cancel multiple orders + + https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_cancelorders + + :param str[] ids: order ids + :param str symbol: not used by coinbase cancelOrders() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `order structures ` + """ + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + request: dict = { + 'order_ids': ids, + } + response = await self.v3PrivatePostBrokerageOrdersBatchCancel(self.extend(request, params)) + # + # { + # "results": [ + # { + # "success": True, + # "failure_reason": "UNKNOWN_CANCEL_FAILURE_REASON", + # "order_id": "bb8851a3-4fda-4a2c-aa06-9048db0e0f0d" + # } + # ] + # } + # + orders = self.safe_list(response, 'results', []) + for i in range(0, len(orders)): + success = self.safe_bool(orders[i], 'success') + if success is not True: + raise BadRequest(self.id + ' cancelOrders() has failed, check your arguments and parameters') + return self.parse_orders(orders, market) + + async def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + """ + edit a trade order + + https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_editorder + + :param str id: cancel order id + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.preview]: default to False, wether to use the test/preview endpoint or not + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'order_id': id, + } + if amount is not None: + request['size'] = self.amount_to_precision(symbol, amount) + if price is not None: + request['price'] = self.price_to_precision(symbol, price) + preview = self.safe_bool_2(params, 'preview', 'test', False) + response = None + if preview: + params = self.omit(params, ['preview', 'test']) + response = await self.v3PrivatePostBrokerageOrdersEditPreview(self.extend(request, params)) + else: + response = await self.v3PrivatePostBrokerageOrdersEdit(self.extend(request, params)) + # + # { + # "success": True, + # "errors": { + # "edit_failure_reason": "UNKNOWN_EDIT_ORDER_FAILURE_REASON", + # "preview_failure_reason": "UNKNOWN_PREVIEW_FAILURE_REASON" + # } + # } + # + return self.parse_order(response, market) + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_gethistoricalorder + + :param str id: the order id + :param str symbol: unified market symbol that the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + request: dict = { + 'order_id': id, + } + response = await self.v3PrivateGetBrokerageOrdersHistoricalOrderId(self.extend(request, params)) + # + # { + # "order": { + # "order_id": "9bc1eb3b-5b46-4b71-9628-ae2ed0cca75b", + # "product_id": "LTC-BTC", + # "user_id": "1111111-1111-1111-1111-111111111111", + # "order_configuration": { + # "limit_limit_gtc": { + # "base_size": "0.2", + # "limit_price": "0.006", + # "post_only": False + # } + # }, + # "side": "SELL", + # "client_order_id": "e5fe8482-05bb-428f-ad4d-dbc8ce39239c", + # "status": "OPEN", + # "time_in_force": "GOOD_UNTIL_CANCELLED", + # "created_time": "2023-01-16T23:37:23.947030Z", + # "completion_percentage": "0", + # "filled_size": "0", + # "average_filled_price": "0", + # "fee": "", + # "number_of_fills": "0", + # "filled_value": "0", + # "pending_cancel": False, + # "size_in_quote": False, + # "total_fees": "0", + # "size_inclusive_of_fees": False, + # "total_value_after_fees": "0", + # "trigger_status": "INVALID_ORDER_TYPE", + # "order_type": "LIMIT", + # "reject_reason": "REJECT_REASON_UNSPECIFIED", + # "settled": False, + # "product_type": "SPOT", + # "reject_message": "", + # "cancel_message": "" + # } + # } + # + order = self.safe_dict(response, 'order', {}) + return self.parse_order(order, market) + + async def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = 100, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_gethistoricalorders + + :param str symbol: unified market symbol that the orders were made in + :param int [since]: the earliest time in ms to fetch orders + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch trades for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOrders', 'paginate') + if paginate: + return await self.fetch_paginated_call_cursor('fetchOrders', symbol, since, limit, params, 'cursor', 'cursor', None, 1000) + market = None + if symbol is not None: + market = self.market(symbol) + request: dict = {} + if market is not None: + request['product_id'] = market['id'] + if limit is not None: + request['limit'] = limit + if since is not None: + request['start_date'] = self.iso8601(since) + until = self.safe_integer_n(params, ['until']) + if until is not None: + params = self.omit(params, ['until']) + request['end_date'] = self.iso8601(until) + response = await self.v3PrivateGetBrokerageOrdersHistoricalBatch(self.extend(request, params)) + # + # { + # "orders": [ + # { + # "order_id": "813a53c5-3e39-47bb-863d-2faf685d22d8", + # "product_id": "BTC-USDT", + # "user_id": "1111111-1111-1111-1111-111111111111", + # "order_configuration": { + # "market_market_ioc": { + # "quote_size": "6.36" + # } + # }, + # "side": "BUY", + # "client_order_id": "18eb9947-db49-4874-8e7b-39b8fe5f4317", + # "status": "FILLED", + # "time_in_force": "IMMEDIATE_OR_CANCEL", + # "created_time": "2023-01-18T01:37:37.975552Z", + # "completion_percentage": "100", + # "filled_size": "0.000297920684505", + # "average_filled_price": "21220.6399999973697697", + # "fee": "", + # "number_of_fills": "2", + # "filled_value": "6.3220675944333996", + # "pending_cancel": False, + # "size_in_quote": True, + # "total_fees": "0.0379324055666004", + # "size_inclusive_of_fees": True, + # "total_value_after_fees": "6.36", + # "trigger_status": "INVALID_ORDER_TYPE", + # "order_type": "MARKET", + # "reject_reason": "REJECT_REASON_UNSPECIFIED", + # "settled": True, + # "product_type": "SPOT", + # "reject_message": "", + # "cancel_message": "Internal error" + # }, + # ], + # "sequence": "0", + # "has_next": False, + # "cursor": "" + # } + # + orders = self.safe_list(response, 'orders', []) + first = self.safe_dict(orders, 0) + cursor = self.safe_string(response, 'cursor') + if (cursor is not None) and (cursor != ''): + first['cursor'] = cursor + orders[0] = first + return self.parse_orders(orders, market, since, limit) + + async def fetch_orders_by_status(self, status, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + request: dict = { + 'order_status': status, + } + if market is not None: + request['product_id'] = market['id'] + if limit is None: + limit = 100 + request['limit'] = limit + if since is not None: + request['start_date'] = self.iso8601(since) + until = self.safe_integer_n(params, ['until']) + if until is not None: + params = self.omit(params, ['until']) + request['end_date'] = self.iso8601(until) + response = await self.v3PrivateGetBrokerageOrdersHistoricalBatch(self.extend(request, params)) + # + # { + # "orders": [ + # { + # "order_id": "813a53c5-3e39-47bb-863d-2faf685d22d8", + # "product_id": "BTC-USDT", + # "user_id": "1111111-1111-1111-1111-111111111111", + # "order_configuration": { + # "market_market_ioc": { + # "quote_size": "6.36" + # } + # }, + # "side": "BUY", + # "client_order_id": "18eb9947-db49-4874-8e7b-39b8fe5f4317", + # "status": "FILLED", + # "time_in_force": "IMMEDIATE_OR_CANCEL", + # "created_time": "2023-01-18T01:37:37.975552Z", + # "completion_percentage": "100", + # "filled_size": "0.000297920684505", + # "average_filled_price": "21220.6399999973697697", + # "fee": "", + # "number_of_fills": "2", + # "filled_value": "6.3220675944333996", + # "pending_cancel": False, + # "size_in_quote": True, + # "total_fees": "0.0379324055666004", + # "size_inclusive_of_fees": True, + # "total_value_after_fees": "6.36", + # "trigger_status": "INVALID_ORDER_TYPE", + # "order_type": "MARKET", + # "reject_reason": "REJECT_REASON_UNSPECIFIED", + # "settled": True, + # "product_type": "SPOT", + # "reject_message": "", + # "cancel_message": "Internal error" + # }, + # ], + # "sequence": "0", + # "has_next": False, + # "cursor": "" + # } + # + orders = self.safe_list(response, 'orders', []) + first = self.safe_dict(orders, 0) + cursor = self.safe_string(response, 'cursor') + if (cursor is not None) and (cursor != ''): + first['cursor'] = cursor + orders[0] = first + return self.parse_orders(orders, market, since, limit) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on all currently open orders + + https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_gethistoricalorders + + :param str symbol: unified market symbol of the orders + :param int [since]: timestamp in ms of the earliest order, default is None + :param int [limit]: the maximum number of open order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param int [params.until]: the latest time in ms to fetch trades for + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOpenOrders', 'paginate') + if paginate: + return await self.fetch_paginated_call_cursor('fetchOpenOrders', symbol, since, limit, params, 'cursor', 'cursor', None, 100) + return await self.fetch_orders_by_status('OPEN', symbol, since, limit, params) + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_gethistoricalorders + + :param str symbol: unified market symbol of the orders + :param int [since]: timestamp in ms of the earliest order, default is None + :param int [limit]: the maximum number of closed order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param int [params.until]: the latest time in ms to fetch trades for + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchClosedOrders', 'paginate') + if paginate: + return await self.fetch_paginated_call_cursor('fetchClosedOrders', symbol, since, limit, params, 'cursor', 'cursor', None, 100) + return await self.fetch_orders_by_status('FILLED', symbol, since, limit, params) + + async def fetch_canceled_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches information on multiple canceled orders made by the user + + https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_gethistoricalorders + + :param str symbol: unified market symbol of the orders + :param int [since]: timestamp in ms of the earliest order, default is None + :param int [limit]: the maximum number of canceled order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `order structures ` + """ + return await self.fetch_orders_by_status('CANCELLED', symbol, since, limit, params) + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_getpubliccandles + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch, not used by coinbase + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch trades for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param boolean [params.usePrivate]: default False, when True will use the private endpoint to fetch the candles + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + maxLimit = 300 + limit = maxLimit if (limit is None) else min(limit, maxLimit) + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate', False) + if paginate: + return await self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params, maxLimit - 1) + market = self.market(symbol) + request: dict = { + 'product_id': market['id'], + 'granularity': self.safe_string(self.timeframes, timeframe, timeframe), + } + until = self.safe_integer_n(params, ['until', 'end']) + params = self.omit(params, ['until']) + duration = self.parse_timeframe(timeframe) + requestedDuration = limit * duration + sinceString = None + if since is not None: + sinceString = self.number_to_string(self.parse_to_int(since / 1000)) + else: + now = str(self.seconds()) + sinceString = Precise.string_sub(now, str(requestedDuration)) + request['start'] = sinceString + if until is not None: + request['end'] = self.number_to_string(self.parse_to_int(until / 1000)) + else: + # 300 candles max + request['end'] = Precise.string_add(sinceString, str(requestedDuration)) + response = None + usePrivate = False + usePrivate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'usePrivate', False) + if usePrivate: + response = await self.v3PrivateGetBrokerageProductsProductIdCandles(self.extend(request, params)) + else: + response = await self.v3PublicGetBrokerageMarketProductsProductIdCandles(self.extend(request, params)) + # + # { + # "candles": [ + # { + # "start": "1673391780", + # "low": "17414.36", + # "high": "17417.99", + # "open": "17417.74", + # "close": "17417.38", + # "volume": "1.87780853" + # }, + # ] + # } + # + candles = self.safe_list(response, 'candles', []) + return self.parse_ohlcvs(candles, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # [ + # { + # "start": "1673391780", + # "low": "17414.36", + # "high": "17417.99", + # "open": "17417.74", + # "close": "17417.38", + # "volume": "1.87780853" + # }, + # ] + # + return [ + self.safe_timestamp(ohlcv, 'start'), + self.safe_number(ohlcv, 'open'), + self.safe_number(ohlcv, 'high'), + self.safe_number(ohlcv, 'low'), + self.safe_number(ohlcv, 'close'), + self.safe_number(ohlcv, 'volume'), + ] + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_getpublicmarkettrades + + :param str symbol: unified market symbol of the trades + :param int [since]: not used by coinbase fetchTrades + :param int [limit]: the maximum number of trade structures to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.usePrivate]: default False, when True will use the private endpoint to fetch the trades + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'product_id': market['id'], + } + if since is not None: + request['start'] = self.number_to_string(self.parse_to_int(since / 1000)) + if limit is not None: + request['limit'] = min(limit, 1000) + until = None + until, params = self.handle_option_and_params(params, 'fetchTrades', 'until') + if until is not None: + request['end'] = self.number_to_string(self.parse_to_int(until / 1000)) + elif since is not None: + raise ArgumentsRequired(self.id + ' fetchTrades() requires a `until` parameter when you use `since` argument') + response = None + usePrivate = False + usePrivate, params = self.handle_option_and_params(params, 'fetchTrades', 'usePrivate', False) + if usePrivate: + response = await self.v3PrivateGetBrokerageProductsProductIdTicker(self.extend(request, params)) + else: + response = await self.v3PublicGetBrokerageMarketProductsProductIdTicker(self.extend(request, params)) + # + # { + # "trades": [ + # { + # "trade_id": "10092327", + # "product_id": "BTC-USDT", + # "price": "17488.12", + # "size": "0.0000623", + # "time": "2023-01-11T00:52:37.557001Z", + # "side": "BUY", + # "bid": "", + # "ask": "" + # }, + # ] + # } + # + trades = self.safe_list(response, 'trades', []) + return self.parse_trades(trades, market, since, limit) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_getfills + + :param str symbol: unified market symbol of the trades + :param int [since]: timestamp in ms of the earliest order, default is None + :param int [limit]: the maximum number of trade structures to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch trades for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchMyTrades', 'paginate') + if paginate: + return await self.fetch_paginated_call_cursor('fetchMyTrades', symbol, since, limit, params, 'cursor', 'cursor', None, 250) + market = None + if symbol is not None: + market = self.market(symbol) + request: dict = {} + if market is not None: + request['product_id'] = market['id'] + if limit is not None: + request['limit'] = limit + if since is not None: + request['start_sequence_timestamp'] = self.iso8601(since) + until = self.safe_integer_n(params, ['until']) + if until is not None: + params = self.omit(params, ['until']) + request['end_sequence_timestamp'] = self.iso8601(until) + response = await self.v3PrivateGetBrokerageOrdersHistoricalFills(self.extend(request, params)) + # + # { + # "fills": [ + # { + # "entry_id": "b88b82cc89e326a2778874795102cbafd08dd979a2a7a3c69603fc4c23c2e010", + # "trade_id": "cdc39e45-bbd3-44ec-bf02-61742dfb16a1", + # "order_id": "813a53c5-3e39-47bb-863d-2faf685d22d8", + # "trade_time": "2023-01-18T01:37:38.091377090Z", + # "trade_type": "FILL", + # "price": "21220.64", + # "size": "0.0046830664333996", + # "commission": "0.0000280983986004", + # "product_id": "BTC-USDT", + # "sequence_timestamp": "2023-01-18T01:37:38.092520Z", + # "liquidity_indicator": "UNKNOWN_LIQUIDITY_INDICATOR", + # "size_in_quote": True, + # "user_id": "1111111-1111-1111-1111-111111111111", + # "side": "BUY" + # }, + # ], + # "cursor": "" + # } + # + trades = self.safe_list(response, 'fills', []) + first = self.safe_dict(trades, 0) + cursor = self.safe_string(response, 'cursor') + if (cursor is not None) and (cursor != ''): + first['cursor'] = cursor + trades[0] = first + return self.parse_trades(trades, market, since, limit) + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_getpublicproductbook + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.usePrivate]: default False, when True will use the private endpoint to fetch the order book + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'product_id': market['id'], + } + if limit is not None: + request['limit'] = limit + response = None + usePrivate = False + usePrivate, params = self.handle_option_and_params(params, 'fetchOrderBook', 'usePrivate', False) + if usePrivate: + response = await self.v3PrivateGetBrokerageProductBook(self.extend(request, params)) + else: + response = await self.v3PublicGetBrokerageMarketProductBook(self.extend(request, params)) + # + # { + # "pricebook": { + # "product_id": "BTC-USDT", + # "bids": [ + # { + # "price": "30757.85", + # "size": "0.115" + # }, + # ], + # "asks": [ + # { + # "price": "30759.07", + # "size": "0.04877659" + # }, + # ], + # "time": "2023-06-30T04:02:40.533606Z" + # } + # } + # + data = self.safe_dict(response, 'pricebook', {}) + time = self.safe_string(data, 'time') + timestamp = self.parse8601(time) + return self.parse_order_book(data, symbol, timestamp, 'bids', 'asks', 'price', 'size') + + async def fetch_bids_asks(self, symbols: Strings = None, params={}): + """ + fetches the bid and ask price and volume for multiple markets + + https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_getbestbidask + + :param str[] [symbols]: unified symbols of the markets to fetch the bids and asks for, all markets are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + request: dict = {} + if symbols is not None: + request['product_ids'] = self.market_ids(symbols) + response = await self.v3PrivateGetBrokerageBestBidAsk(self.extend(request, params)) + # + # { + # "pricebooks": [ + # { + # "product_id": "TRAC-EUR", + # "bids": [ + # { + # "price": "0.2384", + # "size": "386.1" + # } + # ], + # "asks": [ + # { + # "price": "0.2406", + # "size": "672" + # } + # ], + # "time": "2023-06-30T07:15:24.656044Z" + # }, + # ] + # } + # + tickers = self.safe_list(response, 'pricebooks', []) + return self.parse_tickers(tickers, symbols) + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/api-transactions#send-money + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str [tag]: an optional tag for the withdrawal + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.check_address(address) + await self.load_markets() + currency = self.currency(code) + accountId = self.safe_string_2(params, 'account_id', 'accountId') + params = self.omit(params, ['account_id', 'accountId']) + if accountId is None: + if code is None: + raise ArgumentsRequired(self.id + ' withdraw() requires an account_id(or accountId) parameter OR a currency code argument') + accountId = await self.find_account_id(code, params) + if accountId is None: + raise ExchangeError(self.id + ' withdraw() could not find account id for ' + code) + request: dict = { + 'account_id': accountId, + 'type': 'send', + 'to': address, + 'amount': amount, + 'currency': currency['id'], + } + if tag is not None: + request['destination_tag'] = tag + response = await self.v2PrivatePostAccountsAccountIdTransactions(self.extend(request, params)) + # + # { + # "data": { + # "id": "a1794ecf-5693-55fa-70cf-ef731748ed82", + # "type": "send", + # "status": "pending", + # "amount": { + # "amount": "-14.008308", + # "currency": "USDC" + # }, + # "native_amount": { + # "amount": "-18.74", + # "currency": "CAD" + # }, + # "description": null, + # "created_at": "2024-01-12T01:27:31Z", + # "updated_at": "2024-01-12T01:27:31Z", + # "resource": "transaction", + # "resource_path": "/v2/accounts/a34bgfad-ed67-538b-bffc-730c98c10da0/transactions/a1794ecf-5693-55fa-70cf-ef731748ed82", + # "instant_exchange": False, + # "network": { + # "status": "pending", + # "status_description": "Pending(est. less than 10 minutes)", + # "transaction_fee": { + # "amount": "4.008308", + # "currency": "USDC" + # }, + # "transaction_amount": { + # "amount": "10.000000", + # "currency": "USDC" + # }, + # "confirmations": 0 + # }, + # "to": { + # "resource": "ethereum_address", + # "address": "0x9...", + # "currency": "USDC", + # "address_info": { + # "address": "0x9..." + # } + # }, + # "idem": "748d8591-dg9a-7831-a45b-crd61dg78762", + # "details": { + # "title": "Sent USDC", + # "subtitle": "To USDC address on Ethereum network", + # "header": "Sent 14.008308 USDC($18.74)", + # "health": "warning" + # }, + # "hide_native_amount": False + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_transaction(data, currency) + + async def fetch_deposit_addresses_by_network(self, code: str, params={}) -> List[DepositAddress]: + """ + fetch the deposit address for a currency associated with self account + + https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_postcoinbaseaccountaddresses + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + await self.load_markets() + currency = self.currency(code) + request = None + request, params = await self.prepare_account_request_with_currency_code(currency['code'], None, params) + response = await self.v2PrivateGetAccountsAccountIdAddresses(self.extend(request, params)) + # + # { + # pagination: { + # ending_before: null, + # starting_after: null, + # previous_ending_before: null, + # next_starting_after: null, + # limit: '25', + # order: 'desc', + # previous_uri: null, + # next_uri: null + # }, + # data: [ + # { + # id: '64ceb5f1-5fa2-5310-a4ff-9fd46271003d', + # address: '5xjPKeAXpnhA2kHyinvdVeui6RXVdEa3B2J3SCAwiKnk', + # address_info: {address: '5xjPKeAXpnhA2kHyinvdVeui6RXVdEa3B2J3SCAwiKnk'}, + # name: null, + # created_at: '2023-05-29T21:12:12Z', + # updated_at: '2023-05-29T21:12:12Z', + # network: 'solana', + # uri_scheme: 'solana', + # resource: 'address', + # resource_path: '/v2/accounts/a7b3d387-bfb8-5ce7-b8da-1f507e81cf25/addresses/64ceb5f1-5fa2-5310-a4ff-9fd46271003d', + # warnings: [ + # { + # type: 'correct_address_warning', + # title: 'This is an ERC20 USDC address.', + # details: 'Only send ERC20 USD Coin(USDC) to self address.', + # image_url: 'https://www.coinbase.com/assets/addresses/global-receive-warning-a3d91807e61c717e5a38d270965003dcc025ca8a3cea40ec3d7835b7c86087fa.png', + # options: [{text: 'I understand', style: 'primary', id: 'dismiss'}] + # } + # ], + # qr_code_image_url: 'https://static-assets.coinbase.com/p2p/l2/asset_network_combinations/v5/usdc-solana.png', + # address_label: 'USDC address(Solana)', + # default_receive: True, + # deposit_uri: 'solana:5xjPKeAXpnhA2kHyinvdVeui6RXVdEa3B2J3SCAwiKnk?spl-token=EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', + # callback_url: null, + # share_address_copy: { + # line1: '5xjPKeAXpnhA2kHyinvdVeui6RXVdEa3B2J3SCAwiKnk', + # line2: 'This address can only receive USDC-SPL from Solana network. Don’t send USDC from other networks, other SPL tokens or NFTs, or it may result in a loss of funds.' + # }, + # receive_subtitle: 'ERC-20', + # inline_warning: { + # text: 'This address can only receive USDC-SPL from Solana network. Don’t send USDC from other networks, other SPL tokens or NFTs, or it may result in a loss of funds.', + # tooltip: { + # title: 'USDC(Solana)', + # subtitle: 'This address can only receive USDC-SPL from Solana network.' + # } + # } + # }, + # ... + # ] + # } + # + data = self.safe_list(response, 'data', []) + addressStructures = self.parse_deposit_addresses(data, None, False) + return self.index_by(addressStructures, 'network') + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + # + # { + # id: '64ceb5f1-5fa2-5310-a4ff-9fd46271003d', + # address: '5xjPKeAXpnhA2kHyinvdVeui6RXVdEa3B2J3SCAwiKnk', + # address_info: { + # address: 'GCF74576I7AQ56SLMKBQAP255EGUOWCRVII3S44KEXVNJEOIFVBDMXVL', + # destination_tag: '3722061866' + # }, + # name: null, + # created_at: '2023-05-29T21:12:12Z', + # updated_at: '2023-05-29T21:12:12Z', + # network: 'solana', + # uri_scheme: 'solana', + # resource: 'address', + # resource_path: '/v2/accounts/a7b3d387-bfb8-5ce7-b8da-1f507e81cf25/addresses/64ceb5f1-5fa2-5310-a4ff-9fd46271003d', + # warnings: [ + # { + # type: 'correct_address_warning', + # title: 'This is an ERC20 USDC address.', + # details: 'Only send ERC20 USD Coin(USDC) to self address.', + # image_url: 'https://www.coinbase.com/assets/addresses/global-receive-warning-a3d91807e61c717e5a38d270965003dcc025ca8a3cea40ec3d7835b7c86087fa.png', + # options: [{text: 'I understand', style: 'primary', id: 'dismiss'}] + # } + # ], + # qr_code_image_url: 'https://static-assets.coinbase.com/p2p/l2/asset_network_combinations/v5/usdc-solana.png', + # address_label: 'USDC address(Solana)', + # default_receive: True, + # deposit_uri: 'solana:5xjPKeAXpnhA2kHyinvdVeui6RXVdEa3B2J3SCAwiKnk?spl-token=EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', + # callback_url: null, + # share_address_copy: { + # line1: '5xjPKeAXpnhA2kHyinvdVeui6RXVdEa3B2J3SCAwiKnk', + # line2: 'This address can only receive USDC-SPL from Solana network. Don’t send USDC from other networks, other SPL tokens or NFTs, or it may result in a loss of funds.' + # }, + # receive_subtitle: 'ERC-20', + # inline_warning: { + # text: 'This address can only receive USDC-SPL from Solana network. Don’t send USDC from other networks, other SPL tokens or NFTs, or it may result in a loss of funds.', + # tooltip: { + # title: 'USDC(Solana)', + # subtitle: 'This address can only receive USDC-SPL from Solana network.' + # } + # } + # } + # + address = self.safe_string(depositAddress, 'address') + self.check_address(address) + networkId = self.safe_string(depositAddress, 'network') + code = self.safe_currency_code(None, currency) + addressLabel = self.safe_string(depositAddress, 'address_label') + currencyId = None + if addressLabel is not None: + splitAddressLabel = addressLabel.split(' ') + currencyId = self.safe_string(splitAddressLabel, 0) + addressInfo = self.safe_dict(depositAddress, 'address_info') + return { + 'info': depositAddress, + 'currency': self.safe_currency_code(currencyId, currency), + 'network': self.network_id_to_code(networkId, code), + 'address': address, + 'tag': self.safe_string(addressInfo, 'destination_tag'), + } + + async def deposit(self, code: str, amount: float, id: str, params={}): + """ + make a deposit + + https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/api-deposits#deposit-funds + + :param str code: unified currency code + :param float amount: the amount to deposit + :param str id: the payment method id to be used for the deposit, can be retrieved from v2PrivateGetPaymentMethods + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.accountId]: the id of the account to deposit into + :returns dict: a `transaction structure ` + """ + await self.load_markets() + accountId = self.safe_string_2(params, 'account_id', 'accountId') + params = self.omit(params, ['account_id', 'accountId']) + if accountId is None: + if code is None: + raise ArgumentsRequired(self.id + ' deposit() requires an account_id(or accountId) parameter OR a currency code argument') + accountId = await self.find_account_id(code, params) + if accountId is None: + raise ExchangeError(self.id + ' deposit() could not find account id for ' + code) + request: dict = { + 'account_id': accountId, + 'amount': self.number_to_string(amount), + 'currency': code.upper(), # need to use code in case depositing USD etc. + 'payment_method': id, + 'commit': True, # otheriwse the deposit does not go through + } + response = await self.v2PrivatePostAccountsAccountIdDeposits(self.extend(request, params)) + # + # { + # "data": { + # "id": "67e0eaec-07d7-54c4-a72c-2e92826897df", + # "status": "created", + # "payment_method": { + # "id": "83562370-3e5c-51db-87da-752af5ab9559", + # "resource": "payment_method", + # "resource_path": "/v2/payment-methods/83562370-3e5c-51db-87da-752af5ab9559" + # }, + # "transaction": { + # "id": "441b9494-b3f0-5b98-b9b0-4d82c21c252a", + # "resource": "transaction", + # "resource_path": "/v2/accounts/2bbf394c-193b-5b2a-9155-3b4732659ede/transactions/441b9494-b3f0-5b98-b9b0-4d82c21c252a" + # }, + # "amount": { + # "amount": "10.00", + # "currency": "USD" + # }, + # "subtotal": { + # "amount": "10.00", + # "currency": "USD" + # }, + # "created_at": "2015-01-31T20:49:02Z", + # "updated_at": "2015-02-11T16:54:02-08:00", + # "resource": "deposit", + # "resource_path": "/v2/accounts/2bbf394c-193b-5b2a-9155-3b4732659ede/deposits/67e0eaec-07d7-54c4-a72c-2e92826897df", + # "committed": True, + # "fee": { + # "amount": "0.00", + # "currency": "USD" + # }, + # "payout_at": "2015-02-18T16:54:00-08:00" + # } + # } + # + # https://github.com/ccxt/ccxt/issues/25484 + data = self.safe_dict_2(response, 'data', 'transfer', {}) + return self.parse_transaction(data) + + async def fetch_deposit(self, id: str, code: Str = None, params={}): + """ + fetch information on a deposit, fiat only, for crypto transactions use fetchLedger + + https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/api-deposits#show-deposit + + :param str id: deposit id + :param str [code]: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.accountId]: the id of the account that the funds were deposited into + :returns dict: a `transaction structure ` + """ + await self.load_markets() + accountId = self.safe_string_2(params, 'account_id', 'accountId') + params = self.omit(params, ['account_id', 'accountId']) + if accountId is None: + if code is None: + raise ArgumentsRequired(self.id + ' fetchDeposit() requires an account_id(or accountId) parameter OR a currency code argument') + accountId = await self.find_account_id(code, params) + if accountId is None: + raise ExchangeError(self.id + ' fetchDeposit() could not find account id for ' + code) + request: dict = { + 'account_id': accountId, + 'deposit_id': id, + } + response = await self.v2PrivateGetAccountsAccountIdDepositsDepositId(self.extend(request, params)) + # + # { + # "data": { + # "id": "67e0eaec-07d7-54c4-a72c-2e92826897df", + # "status": "completed", + # "payment_method": { + # "id": "83562370-3e5c-51db-87da-752af5ab9559", + # "resource": "payment_method", + # "resource_path": "/v2/payment-methods/83562370-3e5c-51db-87da-752af5ab9559" + # }, + # "transaction": { + # "id": "441b9494-b3f0-5b98-b9b0-4d82c21c252a", + # "resource": "transaction", + # "resource_path": "/v2/accounts/2bbf394c-193b-5b2a-9155-3b4732659ede/transactions/441b9494-b3f0-5b98-b9b0-4d82c21c252a" + # }, + # "amount": { + # "amount": "10.00", + # "currency": "USD" + # }, + # "subtotal": { + # "amount": "10.00", + # "currency": "USD" + # }, + # "created_at": "2015-01-31T20:49:02Z", + # "updated_at": "2015-02-11T16:54:02-08:00", + # "resource": "deposit", + # "resource_path": "/v2/accounts/2bbf394c-193b-5b2a-9155-3b4732659ede/deposits/67e0eaec-07d7-54c4-a72c-2e92826897df", + # "committed": True, + # "fee": { + # "amount": "0.00", + # "currency": "USD" + # }, + # "payout_at": "2015-02-18T16:54:00-08:00" + # } + # } + # + # https://github.com/ccxt/ccxt/issues/25484 + data = self.safe_dict_2(response, 'data', 'transfer', {}) + return self.parse_transaction(data) + + async def fetch_deposit_method_ids(self, params={}): + """ + fetch the deposit id for a fiat currency associated with self account + + https://docs.cdp.coinbase.com/advanced-trade/reference/retailbrokerageapi_getpaymentmethods + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an array of `deposit id structures ` + """ + await self.load_markets() + response = await self.v3PrivateGetBrokeragePaymentMethods(params) + # + # { + # "payment_methods": [ + # { + # "id": "21b39a5d-f7b46876fb2e", + # "type": "COINBASE_FIAT_ACCOUNT", + # "name": "CAD Wallet", + # "currency": "CAD", + # "verified": True, + # "allow_buy": False, + # "allow_sell": True, + # "allow_deposit": False, + # "allow_withdraw": False, + # "created_at": "2023-06-29T19:58:46Z", + # "updated_at": "2023-10-30T20:25:01Z" + # } + # ] + # } + # + result = self.safe_list(response, 'payment_methods', []) + return self.parse_deposit_method_ids(result) + + async def fetch_deposit_method_id(self, id: str, params={}): + """ + fetch the deposit id for a fiat currency associated with self account + + https://docs.cdp.coinbase.com/advanced-trade/reference/retailbrokerageapi_getpaymentmethod + + :param str id: the deposit payment method id + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `deposit id structure ` + """ + await self.load_markets() + request: dict = { + 'payment_method_id': id, + } + response = await self.v3PrivateGetBrokeragePaymentMethodsPaymentMethodId(self.extend(request, params)) + # + # { + # "payment_method": { + # "id": "21b39a5d-f7b46876fb2e", + # "type": "COINBASE_FIAT_ACCOUNT", + # "name": "CAD Wallet", + # "currency": "CAD", + # "verified": True, + # "allow_buy": False, + # "allow_sell": True, + # "allow_deposit": False, + # "allow_withdraw": False, + # "created_at": "2023-06-29T19:58:46Z", + # "updated_at": "2023-10-30T20:25:01Z" + # } + # } + # + result = self.safe_dict(response, 'payment_method', {}) + return self.parse_deposit_method_id(result) + + def parse_deposit_method_ids(self, ids, params={}): + result = [] + for i in range(0, len(ids)): + id = self.extend(self.parse_deposit_method_id(ids[i]), params) + result.append(id) + return result + + def parse_deposit_method_id(self, depositId): + return { + 'info': depositId, + 'id': self.safe_string(depositId, 'id'), + 'currency': self.safe_string(depositId, 'currency'), + 'verified': self.safe_bool(depositId, 'verified'), + 'tag': self.safe_string(depositId, 'name'), + } + + async def fetch_convert_quote(self, fromCode: str, toCode: str, amount: Num = None, params={}) -> Conversion: + """ + fetch a quote for converting from one currency to another + + https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_createconvertquote + + :param str fromCode: the currency that you want to sell and convert from + :param str toCode: the currency that you want to buy and convert into + :param float [amount]: how much you want to trade in units of the from currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :param dict [params.trade_incentive_metadata]: an object to fill in user incentive data + :param str [params.trade_incentive_metadata.user_incentive_id]: the id of the incentive + :param str [params.trade_incentive_metadata.code_val]: the code value of the incentive + :returns dict: a `conversion structure ` + """ + await self.load_markets() + request: dict = { + 'from_account': fromCode, + 'to_account': toCode, + 'amount': self.number_to_string(amount), + } + response = await self.v3PrivatePostBrokerageConvertQuote(self.extend(request, params)) + data = self.safe_dict(response, 'trade', {}) + return self.parse_conversion(data) + + async def create_convert_trade(self, id: str, fromCode: str, toCode: str, amount: Num = None, params={}) -> Conversion: + """ + convert from one currency to another + + https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_commitconverttrade + + :param str id: the id of the trade that you want to make + :param str fromCode: the currency that you want to sell and convert from + :param str toCode: the currency that you want to buy and convert into + :param float [amount]: how much you want to trade in units of the from currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `conversion structure ` + """ + await self.load_markets() + request: dict = { + 'trade_id': id, + 'from_account': fromCode, + 'to_account': toCode, + } + response = await self.v3PrivatePostBrokerageConvertTradeTradeId(self.extend(request, params)) + data = self.safe_dict(response, 'trade', {}) + return self.parse_conversion(data) + + async def fetch_convert_trade(self, id: str, code: Str = None, params={}) -> Conversion: + """ + fetch the data for a conversion trade + + https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_getconverttrade + + :param str id: the id of the trade that you want to commit + :param str code: the unified currency code that was converted from + :param dict [params]: extra parameters specific to the exchange API endpoint + :param strng params['toCode']: the unified currency code that was converted into + :returns dict: a `conversion structure ` + """ + await self.load_markets() + if code is None: + raise ArgumentsRequired(self.id + ' fetchConvertTrade() requires a code argument') + toCode = self.safe_string(params, 'toCode') + if toCode is None: + raise ArgumentsRequired(self.id + ' fetchConvertTrade() requires a toCode parameter') + params = self.omit(params, 'toCode') + request: dict = { + 'trade_id': id, + 'from_account': code, + 'to_account': toCode, + } + response = await self.v3PrivateGetBrokerageConvertTradeTradeId(self.extend(request, params)) + data = self.safe_dict(response, 'trade', {}) + return self.parse_conversion(data) + + def parse_conversion(self, conversion: dict, fromCurrency: Currency = None, toCurrency: Currency = None) -> Conversion: + fromCoin = self.safe_string(conversion, 'source_currency') + fromCode = self.safe_currency_code(fromCoin, fromCurrency) + to = self.safe_string(conversion, 'target_currency') + toCode = self.safe_currency_code(to, toCurrency) + fromAmountStructure = self.safe_dict(conversion, 'user_entered_amount') + feeStructure = self.safe_dict(conversion, 'total_fee') + feeAmountStructure = self.safe_dict(feeStructure, 'amount') + return { + 'info': conversion, + 'timestamp': None, + 'datetime': None, + 'id': self.safe_string(conversion, 'id'), + 'fromCurrency': fromCode, + 'fromAmount': self.safe_number(fromAmountStructure, 'value'), + 'toCurrency': toCode, + 'toAmount': None, + 'price': None, + 'fee': self.safe_number(feeAmountStructure, 'value'), + } + + async def close_position(self, symbol: str, side: OrderSide = None, params={}) -> Order: + """ + *futures only* closes open positions for a market + + https://docs.cdp.coinbase.com/coinbase-app/trade/reference/retailbrokerageapi_closeposition + + :param str symbol: Unified CCXT market symbol + :param str [side]: not used by coinbase + :param dict [params]: extra parameters specific to the coinbase api endpoint + @param {str} params.clientOrderId *mandatory* the client order id of the position to close + :param float [params.size]: the size of the position to close, optional + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + clientOrderId = self.safe_string_2(params, 'client_order_id', 'clientOrderId') + params = self.omit(params, 'clientOrderId') + request: dict = { + 'product_id': market['id'], + } + if clientOrderId is None: + raise ArgumentsRequired(self.id + ' closePosition() requires a clientOrderId parameter') + request['client_order_id'] = clientOrderId + response = await self.v3PrivatePostBrokerageOrdersClosePosition(self.extend(request, params)) + order = self.safe_dict(response, 'success_response', {}) + return self.parse_order(order) + + async def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_getfcmpositions + https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_getintxpositions + + :param str[] [symbols]: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.portfolio]: the portfolio UUID to fetch positions for + :returns dict[]: a list of `position structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + market = None + if symbols is not None: + market = self.market(symbols[0]) + type = None + type, params = self.handle_market_type_and_params('fetchPositions', market, params) + response = None + if type == 'future': + response = await self.v3PrivateGetBrokerageCfmPositions(params) + else: + portfolio = None + portfolio, params = self.handle_option_and_params(params, 'fetchPositions', 'portfolio') + if portfolio is None: + raise ArgumentsRequired(self.id + ' fetchPositions() requires a "portfolio" value in params(eg: dbcb91e7-2bc9-515), or set.options["portfolio"]. You can get a list of portfolios with fetchPortfolios()') + request: dict = { + 'portfolio_uuid': portfolio, + } + response = await self.v3PrivateGetBrokerageIntxPositionsPortfolioUuid(self.extend(request, params)) + positions = self.safe_list(response, 'positions', []) + return self.parse_positions(positions, symbols) + + async def fetch_position(self, symbol: str, params={}): + """ + fetch data on a single open contract trade position + + https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_getintxposition + https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_getfcmposition + + :param str symbol: unified market symbol of the market the position is held in, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.product_id]: *futures only* the product id of the position to fetch, required for futures markets only + :param str [params.portfolio]: *perpetual/swaps only* the portfolio UUID to fetch the position for, required for perpetual/swaps markets only + :returns dict: a `position structure ` + """ + await self.load_markets() + market = self.market(symbol) + response = None + if market['future']: + productId = self.safe_string(market, 'product_id') + if productId is None: + raise ArgumentsRequired(self.id + ' fetchPosition() requires a "product_id" in params') + futureRequest: dict = { + 'product_id': productId, + } + response = await self.v3PrivateGetBrokerageCfmPositionsProductId(self.extend(futureRequest, params)) + else: + portfolio = None + portfolio, params = self.handle_option_and_params(params, 'fetchPositions', 'portfolio') + if portfolio is None: + raise ArgumentsRequired(self.id + ' fetchPosition() requires a "portfolio" value in params(eg: dbcb91e7-2bc9-515), or set.options["portfolio"]. You can get a list of portfolios with fetchPortfolios()') + request: dict = { + 'symbol': market['id'], + 'portfolio_uuid': portfolio, + } + response = await self.v3PrivateGetBrokerageIntxPositionsPortfolioUuidSymbol(self.extend(request, params)) + position = self.safe_dict(response, 'position', {}) + return self.parse_position(position, market) + + def parse_position(self, position: dict, market: Market = None): + # + # { + # "product_id": "1r4njf84-0-0", + # "product_uuid": "cd34c18b-3665-4ed8-9305-3db277c49fc5", + # "symbol": "ADA-PERP-INTX", + # "vwap": { + # "value": "0.6171", + # "currency": "USDC" + # }, + # "position_side": "POSITION_SIDE_LONG", + # "net_size": "20", + # "buy_order_size": "0", + # "sell_order_size": "0", + # "im_contribution": "0.1", + # "unrealized_pnl": { + # "value": "0.074", + # "currency": "USDC" + # }, + # "mark_price": { + # "value": "0.6208", + # "currency": "USDC" + # }, + # "liquidation_price": { + # "value": "0", + # "currency": "USDC" + # }, + # "leverage": "1", + # "im_notional": { + # "value": "12.342", + # "currency": "USDC" + # }, + # "mm_notional": { + # "value": "0.814572", + # "currency": "USDC" + # }, + # "position_notional": { + # "value": "12.342", + # "currency": "USDC" + # }, + # "margin_type": "MARGIN_TYPE_CROSS", + # "liquidation_buffer": "19.677828", + # "liquidation_percentage": "4689.3506", + # "portfolio_summary": { + # "portfolio_uuid": "018ebd63-1f6d-7c8e-ada9-0761c5a2235f", + # "collateral": "20.4184", + # "position_notional": "12.342", + # "open_position_notional": "12.342", + # "pending_fees": "0", + # "borrow": "0", + # "accrued_interest": "0", + # "rolling_debt": "0", + # "portfolio_initial_margin": "0.1", + # "portfolio_im_notional": { + # "value": "12.342", + # "currency": "USDC" + # }, + # "portfolio_maintenance_margin": "0.066", + # "portfolio_mm_notional": { + # "value": "0.814572", + # "currency": "USDC" + # }, + # "liquidation_percentage": "4689.3506", + # "liquidation_buffer": "19.677828", + # "margin_type": "MARGIN_TYPE_CROSS", + # "margin_flags": "PORTFOLIO_MARGIN_FLAGS_UNSPECIFIED", + # "liquidation_status": "PORTFOLIO_LIQUIDATION_STATUS_NOT_LIQUIDATING", + # "unrealized_pnl": { + # "value": "0.074", + # "currency": "USDC" + # }, + # "buying_power": { + # "value": "8.1504", + # "currency": "USDC" + # }, + # "total_balance": { + # "value": "20.4924", + # "currency": "USDC" + # }, + # "max_withdrawal": { + # "value": "8.0764", + # "currency": "USDC" + # } + # }, + # "entry_vwap": { + # "value": "0.6091", + # "currency": "USDC" + # } + # } + # + marketId = self.safe_string(position, 'symbol', '') + market = self.safe_market(marketId, market) + rawMargin = self.safe_string(position, 'margin_type') + marginMode = None + if rawMargin is not None: + marginMode = 'cross' if (rawMargin == 'MARGIN_TYPE_CROSS') else 'isolated' + notionalObject = self.safe_dict(position, 'position_notional', {}) + positionSide = self.safe_string(position, 'position_side') + side = 'long' if (positionSide == 'POSITION_SIDE_LONG') else 'short' + unrealizedPNLObject = self.safe_dict(position, 'unrealized_pnl', {}) + liquidationPriceObject = self.safe_dict(position, 'liquidation_price', {}) + liquidationPrice = self.safe_number(liquidationPriceObject, 'value') + vwapObject = self.safe_dict(position, 'vwap', {}) + summaryObject = self.safe_dict(position, 'portfolio_summary', {}) + return self.safe_position({ + 'info': position, + 'id': self.safe_string(position, 'product_id'), + 'symbol': self.safe_symbol(marketId, market), + 'notional': self.safe_number(notionalObject, 'value'), + 'marginMode': marginMode, + 'liquidationPrice': liquidationPrice, + 'entryPrice': self.safe_number(vwapObject, 'value'), + 'unrealizedPnl': self.safe_number(unrealizedPNLObject, 'value'), + 'realizedPnl': None, + 'percentage': None, + 'contracts': self.safe_number(position, 'net_size'), + 'contractSize': market['contractSize'], + 'markPrice': None, + 'lastPrice': None, + 'side': side, + 'hedged': None, + 'timestamp': None, + 'datetime': None, + 'lastUpdateTimestamp': None, + 'maintenanceMargin': None, + 'maintenanceMarginPercentage': None, + 'collateral': self.safe_number(summaryObject, 'collateral'), + 'initialMargin': None, + 'initialMarginPercentage': None, + 'leverage': self.safe_number(position, 'leverage'), + 'marginRatio': None, + 'stopLossPrice': None, + 'takeProfitPrice': None, + }) + + async def fetch_trading_fees(self, params={}) -> TradingFees: + """ + + https://docs.cdp.coinbase.com/advanced-trade/reference/retailbrokerageapi_gettransactionsummary/ + + fetch the trading fees for multiple markets + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: 'spot' or 'swap' + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + await self.load_markets() + type = None + type, params = self.handle_market_type_and_params('fetchTradingFees', None, params) + isSpot = (type == 'spot') + productType = 'SPOT' if isSpot else 'FUTURE' + request: dict = { + 'product_type': productType, + } + response = await self.v3PrivateGetBrokerageTransactionSummary(self.extend(request, params)) + # + # { + # total_volume: '0', + # total_fees: '0', + # fee_tier: { + # pricing_tier: 'Advanced 1', + # usd_from: '0', + # usd_to: '1000', + # taker_fee_rate: '0.008', + # maker_fee_rate: '0.006', + # aop_from: '', + # aop_to: '' + # }, + # margin_rate: null, + # goods_and_services_tax: null, + # advanced_trade_only_volume: '0', + # advanced_trade_only_fees: '0', + # coinbase_pro_volume: '0', + # coinbase_pro_fees: '0', + # total_balance: '', + # has_promo_fee: False + # } + # + data = self.safe_dict(response, 'fee_tier', {}) + taker_fee = self.safe_number(data, 'taker_fee_rate') + marker_fee = self.safe_number(data, 'maker_fee_rate') + result: dict = {} + for i in range(0, len(self.symbols)): + symbol = self.symbols[i] + market = self.market(symbol) + if (isSpot and market['spot']) or (not isSpot and not market['spot']): + result[symbol] = { + 'info': response, + 'symbol': symbol, + 'maker': taker_fee, + 'taker': marker_fee, + 'percentage': True, + } + return result + + async def fetch_portfolio_details(self, portfolioUuid: str, params={}) -> List[Any]: + """ + Fetch details for a specific portfolio by UUID + + https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_getportfolios + + :param str portfolioUuid: The unique identifier of the portfolio to fetch + :param Dict [params]: Extra parameters specific to the exchange API endpoint + :returns any[]: An account structure + """ + await self.load_markets() + request = { + 'portfolio_uuid': portfolioUuid, + } + response = await self.v3PrivateGetBrokeragePortfoliosPortfolioUuid(self.extend(request, params)) + result = self.parse_portfolio_details(response) + return result + + def parse_portfolio_details(self, portfolioData: dict): + breakdown = portfolioData['breakdown'] + portfolioInfo = self.safe_dict(breakdown, 'portfolio', {}) + portfolioName = self.safe_string(portfolioInfo, 'name', 'Unknown') + portfolioUuid = self.safe_string(portfolioInfo, 'uuid', '') + spotPositions = self.safe_list(breakdown, 'spot_positions', []) + parsedPositions = [] + for i in range(0, len(spotPositions)): + position: dict = spotPositions[i] + currencyCode = self.safe_string(position, 'asset', 'Unknown') + availableBalanceStr = self.safe_string(position, 'available_to_trade_fiat', '0') + availableBalance = self.parse_number(availableBalanceStr) + totalBalanceFiatStr = self.safe_string(position, 'total_balance_fiat', '0') + totalBalanceFiat = self.parse_number(totalBalanceFiatStr) + holdAmount = totalBalanceFiat - availableBalance + costBasisDict = self.safe_dict(position, 'cost_basis', {}) + costBasisStr = self.safe_string(costBasisDict, 'value', '0') + averageEntryPriceDict = self.safe_dict(position, 'average_entry_price', {}) + averageEntryPriceStr = self.safe_string(averageEntryPriceDict, 'value', '0') + positionData: dict = { + 'currency': currencyCode, + 'available_balance': availableBalance, + 'hold_amount': holdAmount > holdAmount if 0 else 0, + 'wallet_name': portfolioName, + 'account_id': portfolioUuid, + 'account_uuid': self.safe_string(position, 'account_uuid', ''), + 'total_balance_fiat': totalBalanceFiat, + 'total_balance_crypto': self.parse_number(self.safe_string(position, 'total_balance_crypto', '0')), + 'available_to_trade_fiat': self.parse_number(self.safe_string(position, 'available_to_trade_fiat', '0')), + 'available_to_trade_crypto': self.parse_number(self.safe_string(position, 'available_to_trade_crypto', '0')), + 'available_to_transfer_fiat': self.parse_number(self.safe_string(position, 'available_to_transfer_fiat', '0')), + 'available_to_transfer_crypto': self.parse_number(self.safe_string(position, 'available_to_trade_crypto', '0')), + 'allocation': self.parse_number(self.safe_string(position, 'allocation', '0')), + 'cost_basis': self.parse_number(costBasisStr), + 'cost_basis_currency': self.safe_string(costBasisDict, 'currency', 'USD'), + 'is_cash': self.safe_bool(position, 'is_cash', False), + 'average_entry_price': self.parse_number(averageEntryPriceStr), + 'average_entry_price_currency': self.safe_string(averageEntryPriceDict, 'currency', 'USD'), + 'asset_uuid': self.safe_string(position, 'asset_uuid', ''), + 'unrealized_pnl': self.parse_number(self.safe_string(position, 'unrealized_pnl', '0')), + 'asset_color': self.safe_string(position, 'asset_color', ''), + 'account_type': self.safe_string(position, 'account_type', ''), + } + parsedPositions.append(positionData) + return parsedPositions + + def create_auth_token(self, seconds: Int, method: Str = None, url: Str = None, useEddsa=False): + # v1 https://docs.cdp.coinbase.com/api-reference/authentication#php-2 + # v2 https://docs.cdp.coinbase.com/api-reference/v2/authentication + uri = None + if url is not None: + uri = method + ' ' + url.replace('https://', '') + quesPos = uri.find('?') + # Due to we use mb_strpos, quesPos could be False in php. In that case, the quesPos >= 0 is True + # Also it's not possible that the question mark is first character, only check > 0 here. + if quesPos > 0: + uri = uri[0:quesPos] + # self.eddsa{"sub":"d2efa49a-369c-43d7-a60e-ae26e28853c2","iss":"cdp","aud":["cdp_service"],"uris":["GET api.coinbase.com/api/v3/brokerage/transaction_summary"]} + nonce = self.random_bytes(16) + aud = 'cdp_service' if useEddsa else 'retail_rest_api_proxy' + iss = 'cdp' if useEddsa else 'coinbase-cloud' + request: dict = { + 'aud': [aud], + 'iss': iss, + 'nbf': seconds, + 'exp': seconds + 120, + 'sub': self.apiKey, + 'iat': seconds, + } + if uri is not None: + if not useEddsa: + request['uri'] = uri + else: + request['uris'] = [uri] + if useEddsa: + byteArray = self.base64_to_binary(self.secret) + seed = self.array_slice(byteArray, 0, 32) + return self.jwt(request, seed, 'sha256', False, {'kid': self.apiKey, 'nonce': nonce, 'alg': 'EdDSA'}) + else: + # self.ecdsawith p256 + return self.jwt(request, self.encode(self.secret), 'sha256', False, {'kid': self.apiKey, 'nonce': nonce, 'alg': 'ES256'}) + + def nonce(self): + return self.milliseconds() - self.options['timeDifference'] + + def sign(self, path, api=[], method='GET', params={}, headers=None, body=None): + version = api[0] + signed = api[1] == 'private' + isV3 = version == 'v3' + pathPart = 'api/v3' if (isV3) else 'v2' + fullPath = '/' + pathPart + '/' + self.implode_params(path, params) + query = self.omit(params, self.extract_params(path)) + savedPath = fullPath + if method == 'GET': + if query: + fullPath += '?' + self.urlencode_with_array_repeat(query) + url = self.urls['api']['rest'] + fullPath + if signed: + authorization = self.safe_string(self.headers, 'Authorization') + authorizationString = None + if authorization is not None: + authorizationString = authorization + elif self.token and not self.check_required_credentials(False): + authorizationString = 'Bearer ' + self.token + else: + self.check_required_credentials() + seconds = self.seconds() + payload = '' + if method != 'GET': + if query: + body = self.json(query) + payload = body + else: + if not isV3: + if query: + payload += '?' + self.urlencode(query) + # v3: 'GET' doesn't need payload in the signature. inside url is enough + # https://docs.cloud.coinbase.com/advanced-trade/docs/auth#example-request + # v2: 'GET' require payload in the signature + # https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/api-key-authentication + isCloudAPiKey = (self.apiKey.find('organizations/') >= 0) or (self.secret.startswith('-----BEGIN')) + # using the size might be fragile, so we add an option to force v2 cloud api key if needed + isV2CloudAPiKey = len(self.secret) == 88 or self.safe_bool(self.options, 'v2CloudAPiKey', False) or self.secret.endswith('=') + if isCloudAPiKey or isV2CloudAPiKey: + if isCloudAPiKey and self.apiKey.startswith('-----BEGIN'): + raise ArgumentsRequired(self.id + ' apiKey should contain the name(eg: organizations/3b910e93....) and not the public key') + # # it may not work for v2 + # uri = method + ' ' + url.replace('https://', '') + # quesPos = uri.find('?') + # # Due to we use mb_strpos, quesPos could be False in php. In that case, the quesPos >= 0 is True + # # Also it's not possible that the question mark is first character, only check > 0 here. + # if quesPos > 0: + # uri = uri[0:quesPos] + # } + # nonce = self.random_bytes(16) + # request: Dict = { + # 'aud': ['retail_rest_api_proxy'], + # 'iss': 'coinbase-cloud', + # 'nbf': seconds, + # 'exp': seconds + 120, + # 'sub': self.apiKey, + # 'uri': uri, + # 'iat': seconds, + # } + token = self.create_auth_token(seconds, method, url, isV2CloudAPiKey) + # token = self.jwt(request, self.encode(self.secret), 'sha256', False, {'kid': self.apiKey, 'nonce': nonce, 'alg': 'ES256'}) + authorizationString = 'Bearer ' + token + else: + nonce = self.nonce() + timestamp = self.parse_to_int(nonce / 1000) + timestampString = str(timestamp) + auth = timestampString + method + savedPath + payload + signature = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256) + headers = { + 'CB-ACCESS-KEY': self.apiKey, + 'CB-ACCESS-SIGN': signature, + 'CB-ACCESS-TIMESTAMP': timestampString, + 'Content-Type': 'application/json', + } + if authorizationString is not None: + headers = { + 'Authorization': authorizationString, + 'Content-Type': 'application/json', + } + if method != 'GET': + if query: + body = self.json(query) + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None # fallback to default error handler + feedback = self.id + ' ' + body + # + # {"error": "invalid_request", "error_description": "The request is missing a required parameter, includes an unsupported parameter value, or is otherwise malformed."} + # + # or + # + # { + # "errors": [ + # { + # "id": "not_found", + # "message": "Not found" + # } + # ] + # } + # or + # { + # "success": False, + # "error_response": { + # "error": "UNKNOWN_FAILURE_REASON", + # "message": "", + # "error_details": "", + # "preview_failure_reason": "PREVIEW_STOP_PRICE_ABOVE_LAST_TRADE_PRICE" + # }, + # "order_configuration": { + # "stop_limit_stop_limit_gtc": { + # "base_size": "0.0001", + # "limit_price": "2000", + # "stop_price": "2005", + # "stop_direction": "STOP_DIRECTION_STOP_DOWN", + # "reduce_only": False + # } + # } + # } + # + errorCode = self.safe_string(response, 'error') + if errorCode is not None: + errorMessage = self.safe_string_2(response, 'error_description', 'error') + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], errorMessage, feedback) + raise ExchangeError(feedback) + errorResponse = self.safe_dict(response, 'error_response') + if errorResponse is not None: + errorMessageInner = self.safe_string_2(errorResponse, 'preview_failure_reason', 'preview_failure_reason') + self.throw_exactly_matched_exception(self.exceptions['exact'], errorMessageInner, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], errorMessageInner, feedback) + raise ExchangeError(feedback) + errors = self.safe_list(response, 'errors') + if errors is not None: + if isinstance(errors, list): + numErrors = len(errors) + if numErrors > 0: + errorCode = self.safe_string(errors[0], 'id') + errorMessage = self.safe_string(errors[0], 'message') + if errorCode is not None: + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], errorMessage, feedback) + raise ExchangeError(feedback) + advancedTrade = self.options['advanced'] + if not ('data' in response) and (not advancedTrade): + raise ExchangeError(self.id + ' failed due to a malformed response ' + self.json(response)) + return None diff --git a/ccxt/async_support/coinbaseadvanced.py b/ccxt/async_support/coinbaseadvanced.py new file mode 100644 index 0000000..7c575d4 --- /dev/null +++ b/ccxt/async_support/coinbaseadvanced.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.coinbase import coinbase +from ccxt.abstract.coinbaseadvanced import ImplicitAPI +from ccxt.base.types import Any + + +class coinbaseadvanced(coinbase, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(coinbaseadvanced, self).describe(), { + 'id': 'coinbaseadvanced', + 'name': 'Coinbase Advanced', + 'alias': True, + }) diff --git a/ccxt/async_support/coinbaseexchange.py b/ccxt/async_support/coinbaseexchange.py new file mode 100644 index 0000000..d1d257e --- /dev/null +++ b/ccxt/async_support/coinbaseexchange.py @@ -0,0 +1,2048 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.coinbaseexchange import ImplicitAPI +import hashlib +from ccxt.base.types import Account, Any, Balances, Currencies, Currency, DepositAddress, Int, LedgerEntry, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, TradingFees, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidAddress +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import OnMaintenance +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class coinbaseexchange(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(coinbaseexchange, self).describe(), { + 'id': 'coinbaseexchange', + 'name': 'Coinbase Exchange', + 'countries': ['US'], + 'rateLimit': 100, + 'userAgent': self.userAgents['chrome'], + 'pro': True, + 'has': { + 'CORS': True, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createDepositAddress': True, + 'createOrder': True, + 'createOrderWithTakeProfitAndStopLoss': False, + 'createOrderWithTakeProfitAndStopLossWs': False, + 'createPostOnlyOrder': False, + 'createReduceOnlyOrder': False, + 'createStopLimitOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'fetchAccounts': True, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': False, # the exchange does not have self method, only createDepositAddress, see https://github.com/ccxt/ccxt/pull/7405 + 'fetchDeposits': True, + 'fetchDepositsWithdrawals': True, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLedger': True, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchOrderTrades': True, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': True, + 'fetchTransactions': 'emulated', + 'fetchVolatilityHistory': False, + 'fetchWithdrawals': True, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'repayMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'withdraw': True, + }, + 'timeframes': { + '1m': 60, + '5m': 300, + '15m': 900, + '1h': 3600, + '6h': 21600, + '1d': 86400, + }, + 'hostname': 'exchange.coinbase.com', + 'urls': { + 'test': { + 'public': 'https://api-public.sandbox.exchange.coinbase.com', + 'private': 'https://api-public.sandbox.exchange.coinbase.com', + }, + 'logo': 'https://github.com/ccxt/ccxt/assets/43336371/34a65553-88aa-4a38-a714-064bd228b97e', + 'api': { + 'public': 'https://api.{hostname}', + 'private': 'https://api.{hostname}', + }, + 'www': 'https://coinbase.com/', + 'doc': 'https://docs.cloud.coinbase.com/exchange/docs/', + 'fees': [ + 'https://docs.pro.coinbase.com/#fees', + 'https://support.pro.coinbase.com/customer/en/portal/articles/2945310-fees', + ], + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + 'password': True, + }, + 'api': { + 'public': { + 'get': [ + 'currencies', + 'products', + 'products/{id}', + 'products/{id}/book', + 'products/{id}/candles', + 'products/{id}/stats', + 'products/{id}/ticker', + 'products/{id}/trades', + 'time', + 'products/spark-lines', # experimental, + 'products/volume-summary', + ], + }, + 'private': { + 'get': [ + 'address-book', + 'accounts', + 'accounts/{id}', + 'accounts/{id}/holds', + 'accounts/{id}/ledger', + 'accounts/{id}/transfers', + 'coinbase-accounts', + 'fills', + 'funding', + 'fees', + 'margin/profile_information', + 'margin/buying_power', + 'margin/withdrawal_power', + 'margin/withdrawal_power_all', + 'margin/exit_plan', + 'margin/liquidation_history', + 'margin/position_refresh_amounts', + 'margin/status', + 'oracle', + 'orders', + 'orders/{id}', + 'orders/client:{client_oid}', + 'otc/orders', + 'payment-methods', + 'position', + 'profiles', + 'profiles/{id}', + 'reports/{report_id}', + 'transfers', + 'transfers/{transfer_id}', + 'users/self/exchange-limits', + 'users/self/hold-balances', + 'users/self/trailing-volume', + 'withdrawals/fee-estimate', + 'conversions/{conversion_id}', + 'conversions', + 'conversions/fees', + 'loans/lending-overview', + 'loans/lending-overview-xm', + 'loans/loan-preview', + 'loans/loan-preview-xm', + 'loans/repayment-preview', + 'loans/repayment-preview-xm', + 'loans/interest/{loan_id}', + 'loans/interest/history/{loan_id}', + 'loans/interest', + 'loans/assets', + 'loans', + ], + 'post': [ + 'conversions', + 'deposits/coinbase-account', + 'deposits/payment-method', + 'coinbase-accounts/{id}/addresses', + 'funding/repay', + 'orders', + 'position/close', + 'profiles/margin-transfer', + 'profiles/transfer', + 'reports', + 'withdrawals/coinbase', + 'withdrawals/coinbase-account', + 'withdrawals/crypto', + 'withdrawals/payment-method', + 'loans/open', + 'loans/repay-interest', + 'loans/repay-principal', + ], + 'delete': [ + 'orders', + 'orders/client:{client_oid}', + 'orders/{id}', + ], + }, + }, + 'commonCurrencies': { + 'CGLD': 'CELO', + }, + 'precisionMode': TICK_SIZE, + 'fees': { + 'trading': { + 'tierBased': True, # complicated tier system per coin + 'percentage': True, + 'maker': self.parse_number('0.004'), # highest fee of all tiers + 'taker': self.parse_number('0.006'), # highest fee of all tiers + }, + 'funding': { + 'tierBased': False, + 'percentage': False, + 'withdraw': { + 'BCH': 0, + 'BTC': 0, + 'LTC': 0, + 'ETH': 0, + 'EUR': 0.15, + 'USD': 25, + }, + 'deposit': { + 'BCH': 0, + 'BTC': 0, + 'LTC': 0, + 'ETH': 0, + 'EUR': 0.15, + 'USD': 10, + }, + }, + }, + 'features': { + 'default': { + 'sandbox': True, + 'createOrder': { + 'marginMode': True, + 'triggerPrice': True, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': True, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': False, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': False, + 'iceberg': True, # todo: implement + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 100000, + 'untilDays': 100000, + 'symbolRequired': True, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 100, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 100000, + 'untilDays': 100000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 100000, + 'daysBackCanceled': 1, + 'untilDays': 100000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 300, + }, + }, + 'spot': { + 'extends': 'default', + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'options': { + 'networks': { + 'BTC': 'bitcoin', + # LIGHTNING unsupported + 'ETH': 'ethereum', + # TRON unsupported + 'SOL': 'solana', + # BSC unsupported + 'ARBONE': 'arbitrum', + 'AVAXC': 'avacchain', + 'MATIC': 'polygon', + 'BASE': 'base', + 'SUI': 'sui', + 'OP': 'optimism', + 'NEAR': 'near', + # CRONOS unsupported + # GNO unsupported + 'APT': 'aptos', + # SCROLL unsupported + 'KAVA': 'kava', + # TAIKO unsupported + # BOB unsupported + # LINEA unsupported + 'BLAST': 'blast', + 'XLM': 'stellar', + # RSK unsupported + 'SEI': 'sei', + # TON unsupported + # MANTLE unsupported + 'ADA': 'cardano', + # HYPE unsupported + 'CORE': 'coredao', + 'ALGO': 'algorand', + # RUNE unsupported + 'OSMO': 'osmosis', + # XIN unsupported + 'CELO': 'celo', + 'HBAR': 'hedera', + # FTM unsupported + # WEMIX unsupported + 'ZKSYNC': 'zksync', + # KLAY unsupported + # HT unsupported + # FSN unsupported + # EOS unsupported, eosio? + # ACA unsupported + 'STX': 'stacks', + 'XTZ': 'tezos', + # NEO unsupported + # METIS unsupported + # TLOS unsupported + 'EGLD': 'elrond', + # ASTR unsupported + # CFX unsupported + # GLMR unsupported + # CANTO unsupported + # SCRT unsupported + 'LTC': 'litecoin', + # AURORA unsupported + # ONG unsupported + 'ATOM': 'cosmos', + # CHZ unsupported + 'FIL': 'filecoin', + 'DOT': 'polkadot', + 'DOGE': 'dogecoin', + # BRC20 unsupported + 'XRP': 'ripple', + # XMR unsupported + 'DASH': 'dash', + # akash, aleo, axelar, bitcoincash, berachain, deso, ethereumclassic, unichain, flow, flare, dfinity, story,kusama, mina, ronin, oasis, bittensor, celestia, noble, vara, vechain, zcash, horizen, zetachain + }, + }, + 'exceptions': { + 'exact': { + 'Insufficient funds': InsufficientFunds, + 'NotFound': OrderNotFound, + 'Invalid API Key': AuthenticationError, + 'invalid signature': AuthenticationError, + 'Invalid Passphrase': AuthenticationError, + 'Invalid order id': InvalidOrder, + 'Private rate limit exceeded': RateLimitExceeded, + 'Trading pair not available': PermissionDenied, + 'Product not found': InvalidOrder, + }, + 'broad': { + 'Order already done': OrderNotFound, + 'order not found': OrderNotFound, + 'price too small': InvalidOrder, + 'price too precise': InvalidOrder, + 'under maintenance': OnMaintenance, + 'size is too small': InvalidOrder, + 'Cancel only mode': OnMaintenance, # https://github.com/ccxt/ccxt/issues/7690 + }, + }, + }) + + async def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getcurrencies + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = await self.publicGetCurrencies(params) + # + # { + # "id": "USDT", + # "name": "Tether", + # "min_size": "0.000001", + # "status": "online", + # "message": "", + # "max_precision": "0.000001", + # "convertible_to": [], + # "details": { + # "type": "crypto", + # "symbol": null, + # "network_confirmations": 14, + # "sort_order": 0, + # "crypto_address_link": "https://etherscan.io/token/0xdac17f958d2ee523a2206206994597c13d831ec7?a={{address}}", + # "crypto_transaction_link": "https://etherscan.io/tx/0x{{txId}}", + # "push_payment_methods": [], + # "group_types": [], + # "display_name": null, + # "processing_time_seconds": null, + # "min_withdrawal_amount": 0.000001, + # "max_withdrawal_amount": 20000000 + # }, + # "default_network": "ethereum", + # "supported_networks": [ + # { + # "id": "ethereum", + # "name": "Ethereum", + # "status": "online", + # "contract_address": "0xdac17f958d2ee523a2206206994597c13d831ec7", + # "crypto_address_link": "https://etherscan.io/token/0xdac17f958d2ee523a2206206994597c13d831ec7?a={{address}}", + # "crypto_transaction_link": "https://etherscan.io/tx/0x{{txId}}", + # "min_withdrawal_amount": 0.000001, + # "max_withdrawal_amount": 20000000, + # "network_confirmations": 14, + # "processing_time_seconds": null + # } + # ], + # "display_name": "USDT" + # } + # + result: dict = {} + for i in range(0, len(response)): + currency = response[i] + id = self.safe_string(currency, 'id') + name = self.safe_string(currency, 'name') + code = self.safe_currency_code(id) + details = self.safe_dict(currency, 'details', {}) + networks: dict = {} + supportedNetworks = self.safe_list(currency, 'supported_networks', []) + for j in range(0, len(supportedNetworks)): + network = supportedNetworks[j] + networkId = self.safe_string(network, 'id') + networkCode = self.network_id_to_code(networkId) + networks[networkCode] = { + 'id': networkId, + 'name': self.safe_string(network, 'name'), + 'network': networkCode, + 'active': self.safe_string(network, 'status') == 'online', + 'withdraw': None, + 'deposit': None, + 'fee': None, + 'precision': None, + 'limits': { + 'withdraw': { + 'min': self.safe_number(network, 'min_withdrawal_amount'), + 'max': self.safe_number(network, 'max_withdrawal_amount'), + }, + }, + 'contract': self.safe_string(network, 'contract_address'), + 'info': network, + } + result[code] = self.safe_currency_structure({ + 'id': id, + 'code': code, + 'info': currency, + 'type': self.safe_string(details, 'type'), + 'name': name, + 'active': self.safe_string(currency, 'status') == 'online', + 'deposit': None, + 'withdraw': None, + 'fee': None, + 'precision': self.safe_number(currency, 'max_precision'), + 'limits': { + 'amount': { + 'min': self.safe_number(details, 'min_size'), + 'max': None, + }, + 'withdraw': { + 'min': self.safe_number(details, 'min_withdrawal_amount'), + 'max': self.safe_number(details, 'max_withdrawal_amount'), + }, + }, + 'networks': networks, + }) + return result + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for coinbaseexchange + + https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getproducts + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = await self.publicGetProducts(params) + # + # [ + # { + # "id": "BTCAUCTION-USD", + # "base_currency": "BTC", + # "quote_currency": "USD", + # "base_min_size": "0.000016", + # "base_max_size": "1500", + # "quote_increment": "0.01", + # "base_increment": "0.00000001", + # "display_name": "BTCAUCTION/USD", + # "min_market_funds": "1", + # "max_market_funds": "20000000", + # "margin_enabled": False, + # "fx_stablecoin": False, + # "max_slippage_percentage": "0.02000000", + # "post_only": False, + # "limit_only": False, + # "cancel_only": True, + # "trading_disabled": False, + # "status": "online", + # "status_message": '', + # "auction_mode": False + # }, + # { + # "id": "BTC-USD", + # "base_currency": "BTC", + # "quote_currency": "USD", + # "base_min_size": "0.000016", + # "base_max_size": "1500", + # "quote_increment": "0.01", + # "base_increment": "0.00000001", + # "display_name": "BTC/USD", + # "min_market_funds": "1", + # "max_market_funds": "20000000", + # "margin_enabled": False, + # "fx_stablecoin": False, + # "max_slippage_percentage": "0.02000000", + # "post_only": False, + # "limit_only": False, + # "cancel_only": False, + # "trading_disabled": False, + # "status": "online", + # "status_message": '', + # "auction_mode": False + # } + # ] + # + result = [] + for i in range(0, len(response)): + market = response[i] + id = self.safe_string(market, 'id') + baseId, quoteId = id.split('-') + # BTCAUCTION-USD vs BTC-USD conflict workaround, see the output sample above + # baseId = self.safe_string(market, 'base_currency') + # quoteId = self.safe_string(market, 'quote_currency') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + status = self.safe_string(market, 'status') + result.append(self.extend(self.fees['trading'], { + 'id': id, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': self.safe_value(market, 'margin_enabled'), + 'swap': False, + 'future': False, + 'option': False, + 'active': (status == 'online'), + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.safe_number(market, 'base_increment'), + 'price': self.safe_number(market, 'quote_increment'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': None, + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': self.safe_number(market, 'min_market_funds'), + 'max': None, + }, + }, + 'created': None, + 'info': market, + })) + return result + + async def fetch_accounts(self, params={}) -> List[Account]: + """ + fetch all the accounts associated with a profile + + https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getaccounts + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `account structures ` indexed by the account type + """ + await self.load_markets() + response = await self.privateGetAccounts(params) + # + # [ + # { + # "id": "4aac9c60-cbda-4396-9da4-4aa71e95fba0", + # "currency": "BTC", + # "balance": "0.0000000000000000", + # "available": "0", + # "hold": "0.0000000000000000", + # "profile_id": "b709263e-f42a-4c7d-949a-a95c83d065da" + # }, + # { + # "id": "f75fa69a-1ad1-4a80-bd61-ee7faa6135a3", + # "currency": "USDC", + # "balance": "0.0000000000000000", + # "available": "0", + # "hold": "0.0000000000000000", + # "profile_id": "b709263e-f42a-4c7d-949a-a95c83d065da" + # }, + # ] + # + return self.parse_accounts(response, params) + + def parse_account(self, account): + # + # { + # "id": "4aac9c60-cbda-4396-9da4-4aa71e95fba0", + # "currency": "BTC", + # "balance": "0.0000000000000000", + # "available": "0", + # "hold": "0.0000000000000000", + # "profile_id": "b709263e-f42a-4c7d-949a-a95c83d065da" + # } + # + currencyId = self.safe_string(account, 'currency') + return { + 'id': self.safe_string(account, 'id'), + 'type': None, + 'code': self.safe_currency_code(currencyId), + 'info': account, + } + + def parse_balance(self, response) -> Balances: + result: dict = {'info': response} + for i in range(0, len(response)): + balance = response[i] + currencyId = self.safe_string(balance, 'currency') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(balance, 'available') + account['used'] = self.safe_string(balance, 'hold') + account['total'] = self.safe_string(balance, 'balance') + result[code] = account + return self.safe_balance(result) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getaccounts + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + response = await self.privateGetAccounts(params) + return self.parse_balance(response) + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + + https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getproductbook + + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + # level 1 - only the best bid and ask + # level 2 - top 50 bids and asks(aggregated) + # level 3 - full order book(non aggregated) + request: dict = { + 'id': self.market_id(symbol), + 'level': 2, # 1 best bidask, 2 aggregated, 3 full + } + response = await self.publicGetProductsIdBook(self.extend(request, params)) + # + # { + # "sequence":1924393896, + # "bids":[ + # ["0.01825","24.34811287",2], + # ["0.01824","72.5463",3], + # ["0.01823","424.54298049",6], + # ], + # "asks":[ + # ["0.01826","171.10414904",4], + # ["0.01827","22.60427028",1], + # ["0.01828","397.46018784",7], + # ] + # } + # + orderbook = self.parse_order_book(response, symbol) + orderbook['nonce'] = self.safe_integer(response, 'sequence') + return orderbook + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # fetchTickers + # + # [ + # 1639472400, # timestamp + # 4.26, # low + # 4.38, # high + # 4.35, # open + # 4.27 # close + # ] + # + # fetchTicker + # + # publicGetProductsIdTicker + # + # { + # "trade_id":843439, + # "price":"0.997999", + # "size":"80.29769", + # "time":"2020-01-28T02:13:33.012523Z", + # "bid":"0.997094", + # "ask":"0.998", + # "volume":"1903188.03750000" + # } + # + # publicGetProductsIdStats + # + # { + # "open": "34.19000000", + # "high": "95.70000000", + # "low": "7.06000000", + # "volume": "2.41000000" + # } + # + timestamp = None + bid = None + ask = None + last = None + high = None + low = None + open = None + volume = None + symbol = None if (market is None) else market['symbol'] + if isinstance(ticker, list): + last = self.safe_string(ticker, 4) + timestamp = self.milliseconds() + else: + timestamp = self.parse8601(self.safe_value(ticker, 'time')) + bid = self.safe_string(ticker, 'bid') + ask = self.safe_string(ticker, 'ask') + high = self.safe_string(ticker, 'high') + low = self.safe_string(ticker, 'low') + open = self.safe_string(ticker, 'open') + last = self.safe_string_2(ticker, 'price', 'last') + volume = self.safe_string(ticker, 'volume') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': high, + 'low': low, + 'bid': bid, + 'bidVolume': None, + 'ask': ask, + 'askVolume': None, + 'vwap': None, + 'open': open, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': volume, + 'quoteVolume': None, + 'info': ticker, + }, market) + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getproduct + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + request: dict = {} + response = await self.publicGetProductsSparkLines(self.extend(request, params)) + # + # { + # YYY-USD: [ + # [ + # 1639472400, # timestamp + # 4.26, # low + # 4.38, # high + # 4.35, # open + # 4.27 # close + # ], + # [ + # 1639468800, + # 4.31, + # 4.45, + # 4.35, + # 4.35 + # ], + # ] + # } + # + result: dict = {} + marketIds = list(response.keys()) + delimiter = '-' + for i in range(0, len(marketIds)): + marketId = marketIds[i] + entry = self.safe_value(response, marketId, []) + first = self.safe_value(entry, 0, []) + market = self.safe_market(marketId, None, delimiter) + symbol = market['symbol'] + result[symbol] = self.parse_ticker(first, market) + return self.filter_by_array_tickers(result, 'symbol', symbols) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + + https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getproductticker + + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'id': market['id'], + } + # publicGetProductsIdTicker or publicGetProductsIdStats + method = self.safe_string(self.options, 'fetchTickerMethod', 'publicGetProductsIdTicker') + response = await getattr(self, method)(self.extend(request, params)) + # + # publicGetProductsIdTicker + # + # { + # "trade_id":843439, + # "price":"0.997999", + # "size":"80.29769", + # "time":"2020-01-28T02:13:33.012523Z", + # "bid":"0.997094", + # "ask":"0.998", + # "volume":"1903188.03750000" + # } + # + # publicGetProductsIdStats + # + # { + # "open": "34.19000000", + # "high": "95.70000000", + # "low": "7.06000000", + # "volume": "2.41000000" + # } + # + return self.parse_ticker(response, market) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # { + # "type": "match", + # "trade_id": 82047307, + # "maker_order_id": "0f358725-2134-435e-be11-753912a326e0", + # "taker_order_id": "252b7002-87a3-425c-ac73-f5b9e23f3caf", + # "order_id": "d50ec984-77a8-460a-b958-66f114b0de9b", + # "side": "sell", + # "size": "0.00513192", + # "price": "9314.78", + # "product_id": "BTC-USD", + # "profile_id": "6244401d-c078-40d9-b305-7ad3551bc3b0", + # "sequence": 12038915443, + # "time": "2020-01-31T20:03:41.158814Z" + # "created_at": "2014-11-07T22:19:28.578544Z", + # "liquidity": "T", + # "fee": "0.00025", + # "settled": True, + # "usd_volume": "0.0924556000000000", + # "user_id": "595eb864313c2b02ddf2937d" + # } + # + timestamp = self.parse8601(self.safe_string_2(trade, 'time', 'created_at')) + marketId = self.safe_string(trade, 'product_id') + market = self.safe_market(marketId, market, '-') + feeRate = None + takerOrMaker = None + cost = None + feeCurrencyId = self.safe_string_lower(market, 'quoteId') + if feeCurrencyId is not None: + costField = feeCurrencyId + '_value' + cost = self.safe_string(trade, costField) + liquidity = self.safe_string(trade, 'liquidity') + if liquidity is not None: + takerOrMaker = 'taker' if (liquidity == 'T') else 'maker' + feeRate = self.safe_string(market, takerOrMaker) + feeCost = self.safe_string_2(trade, 'fill_fees', 'fee') + fee = { + 'cost': feeCost, + 'currency': market['quote'], + 'rate': feeRate, + } + id = self.safe_string(trade, 'trade_id') + side = 'sell' if (trade['side'] == 'buy') else 'buy' + orderId = self.safe_string(trade, 'order_id') + # Coinbase Pro returns inverted side to fetchMyTrades vs fetchTrades + makerOrderId = self.safe_string(trade, 'maker_order_id') + takerOrderId = self.safe_string(trade, 'taker_order_id') + if (orderId is not None) or ((makerOrderId is not None) and (takerOrderId is not None)): + side = 'buy' if (trade['side'] == 'buy') else 'sell' + price = self.safe_string(trade, 'price') + amount = self.safe_string(trade, 'size') + return self.safe_trade({ + 'id': id, + 'order': orderId, + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'type': None, + 'takerOrMaker': takerOrMaker, + 'side': side, + 'price': price, + 'amount': amount, + 'fee': fee, + 'cost': cost, + }, market) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + + https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getfills + + fetch all trades made by the user + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch trades for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Trade[]: a list of `trade structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMyTrades() requires a symbol argument') + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchMyTrades', 'paginate') + if paginate: + return await self.fetch_paginated_call_dynamic('fetchMyTrades', symbol, since, limit, params, 100) + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'product_id': market['id'], + } + if limit is not None: + request['limit'] = limit + if since is not None: + request['start_date'] = self.iso8601(since) + until = self.safe_value_2(params, 'until', 'end_date') + if until is not None: + params = self.omit(params, ['until']) + request['end_date'] = self.iso8601(until) + response = await self.privateGetFills(self.extend(request, params)) + return self.parse_trades(response, market, since, limit) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + + https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getproducttrades + + get the list of most recent trades for a particular symbol + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'id': market['id'], # fixes issue #2 + } + if limit is not None: + request['limit'] = limit # default 100 + response = await self.publicGetProductsIdTrades(self.extend(request, params)) + # + # [ + # { + # "trade_id": "15035219", + # "side": "sell", + # "size": "0.27426731", + # "price": "25820.42000000", + # "time": "2023-09-10T13:47:41.447577Z" + # }, + # ] + # + return self.parse_trades(response, market, since, limit) + + async def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getfees + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + await self.load_markets() + response = await self.privateGetFees(params) + # + # { + # "maker_fee_rate": "0.0050", + # "taker_fee_rate": "0.0050", + # "usd_volume": "43806.92" + # } + # + maker = self.safe_number(response, 'maker_fee_rate') + taker = self.safe_number(response, 'taker_fee_rate') + result: dict = {} + for i in range(0, len(self.symbols)): + symbol = self.symbols[i] + result[symbol] = { + 'info': response, + 'symbol': symbol, + 'maker': maker, + 'taker': taker, + 'percentage': True, + 'tierBased': True, + } + return result + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # [ + # 1591514160, + # 0.02507, + # 0.02507, + # 0.02507, + # 0.02507, + # 0.02816506 + # ] + # + return [ + self.safe_timestamp(ohlcv, 0), + self.safe_number(ohlcv, 3), + self.safe_number(ohlcv, 2), + self.safe_number(ohlcv, 1), + self.safe_number(ohlcv, 4), + self.safe_number(ohlcv, 5), + ] + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + + https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getproductcandles + + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch trades for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate', False) + if paginate: + return await self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params, 300) + market = self.market(symbol) + parsedTimeframe = self.safe_integer(self.timeframes, timeframe) + request: dict = { + 'id': market['id'], + } + if parsedTimeframe is not None: + request['granularity'] = parsedTimeframe + else: + request['granularity'] = timeframe + until = self.safe_value_2(params, 'until', 'end') + params = self.omit(params, ['until']) + if since is not None: + request['start'] = self.iso8601(since) + if limit is None: + # https://docs.pro.coinbase.com/#get-historic-rates + limit = 300 # max = 300 + else: + limit = min(300, limit) + if until is None: + parsedTimeframeMilliseconds = parsedTimeframe * 1000 + if self.is_round_number(since % parsedTimeframeMilliseconds): + request['end'] = self.iso8601(self.sum((limit - 1) * parsedTimeframeMilliseconds, since)) + else: + request['end'] = self.iso8601(self.sum(limit * parsedTimeframeMilliseconds, since)) + else: + request['end'] = self.iso8601(until) + response = await self.publicGetProductsIdCandles(self.extend(request, params)) + # + # [ + # [1591514160,0.02507,0.02507,0.02507,0.02507,0.02816506], + # [1591514100,0.02507,0.02507,0.02507,0.02507,1.63830323], + # [1591514040,0.02505,0.02507,0.02505,0.02507,0.19918178] + # ] + # + return self.parse_ohlcvs(response, market, timeframe, since, limit) + + async def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = await self.publicGetTime(params) + # + # { + # "iso":"2020-05-12T08:00:51.504Z", + # "epoch":1589270451.504 + # } + # + return self.safe_timestamp(response, 'epoch') + + def parse_order_status(self, status: Str): + statuses: dict = { + 'pending': 'open', + 'active': 'open', + 'open': 'open', + 'done': 'closed', + 'canceled': 'canceled', + 'canceling': 'open', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder + # + # { + # "id": "d0c5340b-6d6c-49d9-b567-48c4bfca13d2", + # "price": "0.10000000", + # "size": "0.01000000", + # "product_id": "BTC-USD", + # "side": "buy", + # "stp": "dc", + # "type": "limit", + # "time_in_force": "GTC", + # "post_only": False, + # "created_at": "2016-12-08T20:02:28.53864Z", + # "fill_fees": "0.0000000000000000", + # "filled_size": "0.00000000", + # "executed_value": "0.0000000000000000", + # "status": "pending", + # "settled": False + # } + # + timestamp = self.parse8601(self.safe_string(order, 'created_at')) + marketId = self.safe_string(order, 'product_id') + market = self.safe_market(marketId, market, '-') + status = self.parse_order_status(self.safe_string(order, 'status')) + doneReason = self.safe_string(order, 'done_reason') + if (status == 'closed') and (doneReason == 'canceled'): + status = 'canceled' + price = self.safe_string(order, 'price') + filled = self.safe_string(order, 'filled_size') + amount = self.safe_string(order, 'size', filled) + cost = self.safe_string(order, 'executed_value') + feeCost = self.safe_number(order, 'fill_fees') + fee = None + if feeCost is not None: + fee = { + 'cost': feeCost, + 'currency': market['quote'], + 'rate': None, + } + id = self.safe_string(order, 'id') + type = self.safe_string(order, 'type') + side = self.safe_string(order, 'side') + timeInForce = self.safe_string(order, 'time_in_force') + postOnly = self.safe_value(order, 'post_only') + triggerPrice = self.safe_number(order, 'stop_price') + clientOrderId = self.safe_string(order, 'client_oid') + return self.safe_order({ + 'id': id, + 'clientOrderId': clientOrderId, + 'info': order, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'status': status, + 'symbol': market['symbol'], + 'type': type, + 'timeInForce': timeInForce, + 'postOnly': postOnly, + 'side': side, + 'price': price, + 'triggerPrice': triggerPrice, + 'cost': cost, + 'amount': amount, + 'filled': filled, + 'remaining': None, + 'fee': fee, + 'average': None, + 'trades': None, + }, market) + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + + https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getorder + + fetches information on an order made by the user + :param str id: the order id + :param str symbol: not used by coinbaseexchange fetchOrder + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + request: dict = {} + clientOrderId = self.safe_string_2(params, 'clientOrderId', 'client_oid') + method = None + if clientOrderId is None: + method = 'privateGetOrdersId' + request['id'] = id + else: + method = 'privateGetOrdersClientClientOid' + request['client_oid'] = clientOrderId + params = self.omit(params, ['clientOrderId', 'client_oid']) + response = await getattr(self, method)(self.extend(request, params)) + return self.parse_order(response) + + async def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all the trades made from a single order + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + request: dict = { + 'order_id': id, + } + response = await self.privateGetFills(self.extend(request, params)) + return self.parse_trades(response, market, since, limit) + + async def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + + https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getorders + + fetches information on multiple orders made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch open orders for + :returns Order[]: a list of `order structures ` + """ + request: dict = { + 'status': 'all', + } + return await self.fetch_open_orders(symbol, since, limit, self.extend(request, params)) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + + https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getorders + + fetch all unfilled currently open orders + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch open orders for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOpenOrders', 'paginate') + if paginate: + return await self.fetch_paginated_call_dynamic('fetchOpenOrders', symbol, since, limit, params, 100) + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['product_id'] = market['id'] + if limit is not None: + request['limit'] = limit # default 100 + if since is not None: + request['start_date'] = self.iso8601(since) + until = self.safe_value_2(params, 'until', 'end_date') + if until is not None: + params = self.omit(params, ['until']) + request['end_date'] = self.iso8601(until) + response = await self.privateGetOrders(self.extend(request, params)) + return self.parse_orders(response, market, since, limit) + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + + https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getorders + + fetches information on multiple closed orders made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch open orders for + :returns Order[]: a list of `order structures ` + """ + request: dict = { + 'status': 'done', + } + return await self.fetch_open_orders(symbol, since, limit, self.extend(request, params)) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + + https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_postorders + + create a trade order + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + # common params -------------------------------------------------- + # 'client_oid': clientOrderId, + 'type': type, + 'side': side, + 'product_id': market['id'], + # 'size': self.amount_to_precision(symbol, amount), + # 'stp': 'dc', # self-trade prevention, dc = decrease and cancel, co = cancel oldest, cn = cancel newest, cb = cancel both + # 'stop': 'loss', # "loss" = stop loss below price, "entry" = take profit above price + # 'stop_price': self.price_to_precision(symbol, price), + # limit order params --------------------------------------------- + # 'price': self.price_to_precision(symbol, price), + # 'size': self.amount_to_precision(symbol, amount), + # 'time_in_force': 'GTC', # GTC, GTT, IOC, or FOK + # 'cancel_after' [optional]* min, hour, day, requires time_in_force to be GTT + # 'post_only': False, # invalid when time_in_force is IOC or FOK + # market order params -------------------------------------------- + # 'size': self.amount_to_precision(symbol, amount), + # 'funds': self.cost_to_precision(symbol, amount), + } + clientOrderId = self.safe_string_2(params, 'clientOrderId', 'client_oid') + if clientOrderId is not None: + request['client_oid'] = clientOrderId + triggerPrice = self.safe_number_n(params, ['stopPrice', 'stop_price', 'triggerPrice']) + if triggerPrice is not None: + request['stop_price'] = self.price_to_precision(symbol, triggerPrice) + timeInForce = self.safe_string_2(params, 'timeInForce', 'time_in_force') + if timeInForce is not None: + request['time_in_force'] = timeInForce + postOnly = self.safe_value_2(params, 'postOnly', 'post_only', False) + if postOnly: + request['post_only'] = True + params = self.omit(params, ['timeInForce', 'time_in_force', 'stopPrice', 'stop_price', 'clientOrderId', 'client_oid', 'postOnly', 'post_only', 'triggerPrice']) + if type == 'limit': + request['price'] = self.price_to_precision(symbol, price) + request['size'] = self.amount_to_precision(symbol, amount) + elif type == 'market': + cost = self.safe_number_2(params, 'cost', 'funds') + if cost is None: + if price is not None: + cost = amount * price + else: + params = self.omit(params, ['cost', 'funds']) + if cost is not None: + request['funds'] = self.cost_to_precision(symbol, cost) + else: + request['size'] = self.amount_to_precision(symbol, amount) + response = await self.privatePostOrders(self.extend(request, params)) + # + # { + # "id": "d0c5340b-6d6c-49d9-b567-48c4bfca13d2", + # "price": "0.10000000", + # "size": "0.01000000", + # "product_id": "BTC-USD", + # "side": "buy", + # "stp": "dc", + # "type": "limit", + # "time_in_force": "GTC", + # "post_only": False, + # "created_at": "2016-12-08T20:02:28.53864Z", + # "fill_fees": "0.0000000000000000", + # "filled_size": "0.00000000", + # "executed_value": "0.0000000000000000", + # "status": "pending", + # "settled": False + # } + # + return self.parse_order(response, market) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + + https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_deleteorder + + cancels an open order + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + request: dict = { + # 'product_id': market['id'], # the request will be more performant if you include it + } + clientOrderId = self.safe_string_2(params, 'clientOrderId', 'client_oid') + method = None + if clientOrderId is None: + method = 'privateDeleteOrdersId' + request['id'] = id + else: + method = 'privateDeleteOrdersClientClientOid' + request['client_oid'] = clientOrderId + params = self.omit(params, ['clientOrderId', 'client_oid']) + market = None + if symbol is not None: + market = self.market(symbol) + request['product_id'] = market['symbol'] # the request will be more performant if you include it + response = await getattr(self, method)(self.extend(request, params)) + return self.safe_order({'info': response}) + + async def cancel_all_orders(self, symbol: Str = None, params={}): + """ + + https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_deleteorders + + cancel all open orders + :param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['product_id'] = market['symbol'] # the request will be more performant if you include it + response = await self.privateDeleteOrders(self.extend(request, params)) + return [self.safe_order({'info': response})] + + async def fetch_payment_methods(self, params={}): + return await self.privateGetPaymentMethods(params) + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_postwithdrawpaymentmethod + https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_postwithdrawcoinbaseaccount + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.check_address(address) + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + 'amount': amount, + } + method = 'privatePostWithdrawals' + if 'payment_method_id' in params: + method += 'PaymentMethod' + elif 'coinbase_account_id' in params: + method += 'CoinbaseAccount' + else: + method += 'Crypto' + request['crypto_address'] = address + if tag is not None: + request['destination_tag'] = tag + response = await getattr(self, method)(self.extend(request, params)) + if not response: + raise ExchangeError(self.id + ' withdraw() error: ' + self.json(response)) + return self.parse_transaction(response, currency) + + def parse_ledger_entry_type(self, type): + types: dict = { + 'transfer': 'transfer', # Funds moved between portfolios + 'match': 'trade', # Funds moved result of a trade + 'fee': 'fee', # Fee result of a trade + 'rebate': 'rebate', # Fee rebate + 'conversion': 'trade', # Funds converted between fiat currency and a stablecoin + } + return self.safe_string(types, type, type) + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + # { + # "id": "12087495079", + # "amount": "-0.0100000000000000", + # "balance": "0.0645419900000000", + # "created_at": "2021-10-28T17:14:32.593168Z", + # "type": "transfer", + # "details": { + # "from": "2f74edf7-1440-4586-86dc-ae58c5693691", + # "profile_transfer_id": "3ef093ad-2482-40d1-8ede-2f89cff5099e", + # "to": "dda99503-4980-4b60-9549-0b770ee51336" + # } + # }, + # { + # "id": "11740725774", + # "amount": "-1.7565669701255000", + # "balance": "0.0016490047745000", + # "created_at": "2021-10-22T03:47:34.764122Z", + # "type": "fee", + # "details": { + # "order_id": "ad06abf4-95ab-432a-a1d8-059ef572e296", + # "product_id": "ETH-DAI", + # "trade_id": "1740617" + # } + # } + id = self.safe_string(item, 'id') + amountString = self.safe_string(item, 'amount') + direction = None + afterString = self.safe_string(item, 'balance') + beforeString = Precise.string_sub(afterString, amountString) + if Precise.string_lt(amountString, '0'): + direction = 'out' + amountString = Precise.string_abs(amountString) + else: + direction = 'in' + amount = self.parse_number(amountString) + after = self.parse_number(afterString) + before = self.parse_number(beforeString) + timestamp = self.parse8601(self.safe_value(item, 'created_at')) + type = self.parse_ledger_entry_type(self.safe_string(item, 'type')) + code = self.safe_currency_code(None, currency) + details = self.safe_value(item, 'details', {}) + account = None + referenceAccount = None + referenceId = None + if type == 'transfer': + account = self.safe_string(details, 'from') + referenceAccount = self.safe_string(details, 'to') + referenceId = self.safe_string(details, 'profile_transfer_id') + else: + referenceId = self.safe_string(details, 'order_id') + status = 'ok' + return self.safe_ledger_entry({ + 'info': item, + 'id': id, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'direction': direction, + 'account': account, + 'referenceAccount': referenceAccount, + 'referenceId': referenceId, + 'type': type, + 'currency': code, + 'amount': amount, + 'before': before, + 'after': after, + 'status': status, + 'fee': None, + }, currency) + + async def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + + https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getaccountledger + + :param str code: unified currency code, default is None + :param int [since]: timestamp in ms of the earliest ledger entry, default is None + :param int [limit]: max number of ledger entries to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch trades for + :returns dict: a `ledger structure ` + """ + # https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getaccountledger + if code is None: + raise ArgumentsRequired(self.id + ' fetchLedger() requires a code param') + await self.load_markets() + await self.load_accounts() + currency = self.currency(code) + accountsByCurrencyCode = self.index_by(self.accounts, 'code') + account = self.safe_value(accountsByCurrencyCode, code) + if account is None: + raise ExchangeError(self.id + ' fetchLedger() could not find account id for ' + code) + request: dict = { + 'id': account['id'], + # 'start_date': self.iso8601(since), + # 'end_date': self.iso8601(self.milliseconds()), + # 'before': 'cursor', # sets start cursor to before date + # 'after': 'cursor', # sets end cursor to after date + # 'limit': limit, # default 100 + # 'profile_id': 'string' + } + if since is not None: + request['start_date'] = self.iso8601(since) + if limit is not None: + request['limit'] = limit # default 100 + until = self.safe_value_2(params, 'until', 'end_date') + if until is not None: + params = self.omit(params, ['until']) + request['end_date'] = self.iso8601(until) + response = await self.privateGetAccountsIdLedger(self.extend(request, params)) + for i in range(0, len(response)): + response[i]['currency'] = code + return self.parse_ledger(response, currency, since, limit) + + async def fetch_deposits_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch history of deposits and withdrawals + + https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_gettransfers + https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getaccounttransfers + + :param str [code]: unified currency code for the currency of the deposit/withdrawals, default is None + :param int [since]: timestamp in ms of the earliest deposit/withdrawal, default is None + :param int [limit]: max number of deposit/withdrawals to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.id]: account id, when defined, the endpoint used is '/accounts/{account_id}/transfers/' instead of '/transfers/' + :returns dict: a list of `transaction structure ` + """ + await self.load_markets() + await self.load_accounts() + currency = None + id = self.safe_string(params, 'id') # account id + if id is None: + if code is not None: + currency = self.currency(code) + accountsByCurrencyCode = self.index_by(self.accounts, 'code') + account = self.safe_value(accountsByCurrencyCode, code) + if account is None: + raise ExchangeError(self.id + ' fetchDepositsWithdrawals() could not find account id for ' + code) + id = account['id'] + request: dict = {} + if id is not None: + request['id'] = id + if limit is not None: + request['limit'] = limit + response = None + if id is None: + response = await self.privateGetTransfers(self.extend(request, params)) + # + # [ + # { + # "id": "bee6fd7c-afb2-4e47-8298-671d09997d16", + # "type": "deposit", + # "created_at": "2022-12-21 00:48:45.477503+00", + # "completed_at": null, + # "account_id": "sal3802-36bd-46be-a7b8-alsjf383sldak", + # "user_id": "6382048209f92as392039dlks2", + # "amount": "0.01000000", + # "details": { + # "network": "litecoin", + # "crypto_address": "MKemtnCFUYKsNWaf5EMYMpwSszcXWFDtTY", + # "coinbase_account_id": "fl2b6925-f6ba-403n-jj03-40fl435n430f", + # "coinbase_transaction_id": "63a25bb13cb5cf0001d2cf17", # withdrawals only + # "crypto_transaction_hash": "752f35570736341e2a253f7041a34cf1e196fc56128c900fd03d99da899d94c1", + # "tx_service_transaction_id": "1873249104", + # "coinbase_payment_method_id": "" + # }, + # "canceled_at": null, + # "processed_at": null, + # "user_nonce": null, + # "idem": "5e3201b0-e390-5k3k-a913-c32932049242", + # "profile_id": "k3k302a8-c4dk-4f49-9d39-3203923wpk39", + # "currency": "LTC" + # } + # ] + # + for i in range(0, len(response)): + account_id = self.safe_string(response[i], 'account_id') + account = self.safe_value(self.accountsById, account_id) + codeInner = self.safe_string(account, 'code') + response[i]['currency'] = codeInner + else: + response = await self.privateGetAccountsIdTransfers(self.extend(request, params)) + # + # [ + # { + # "id": "bee6fd7c-afb2-4e47-8298-671d09997d16", + # "type": "deposit", + # "created_at": "2022-12-21 00:48:45.477503+00", + # "completed_at": null, + # "amount": "0.01000000", + # "details": { + # "network": "litecoin", + # "crypto_address": "MKemtnCFUYKsNWaf5EMYMpwSszcXWFDtTY", + # "coinbase_account_id": "fl2b6925-f6ba-403n-jj03-40fl435n430f", + # "coinbase_transaction_id": "63a25bb13cb5cf0001d2cf17", # withdrawals only + # "crypto_transaction_hash": "752f35570736341e2a253f7041a34cf1e196fc56128c900fd03d99da899d94c1", + # "tx_service_transaction_id": "1873249104", + # "coinbase_payment_method_id": "" + # }, + # "canceled_at": null, + # "processed_at": null, + # "user_nonce": null, + # "idem": "5e3201b0-e390-5k3k-a913-c32932049242", + # "profile_id": "k3k302a8-c4dk-4f49-9d39-3203923wpk39", + # "currency": "LTC" + # } + # ] + # + for i in range(0, len(response)): + response[i]['currency'] = code + return self.parse_transactions(response, currency, since, limit) + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_gettransfers + https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getaccounttransfers + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + return await self.fetch_deposits_withdrawals(code, since, limit, self.extend({'type': 'deposit'}, params)) + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_gettransfers + https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getaccounttransfers + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + return await self.fetch_deposits_withdrawals(code, since, limit, self.extend({'type': 'withdraw'}, params)) + + def parse_transaction_status(self, transaction): + canceled = self.safe_value(transaction, 'canceled_at') + if canceled: + return 'canceled' + processed = self.safe_value(transaction, 'processed_at') + completed = self.safe_value(transaction, 'completed_at') + if completed: + return 'ok' + elif processed and not completed: + return 'failed' + else: + return 'pending' + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # privateGetTransfers + # + # [ + # { + # "id": "bee6fd7c-afb2-4e47-8298-671d09997d16", + # "type": "deposit", + # "created_at": "2022-12-21 00:48:45.477503+00", + # "completed_at": null, + # "account_id": "sal3802-36bd-46be-a7b8-alsjf383sldak", # only from privateGetTransfers + # "user_id": "6382048209f92as392039dlks2", # only from privateGetTransfers + # "amount": "0.01000000", + # "details": { + # "network": "litecoin", + # "crypto_address": "MKemtnCFUYKsNWaf5EMYMpwSszcXWFDtTY", + # "coinbase_account_id": "fl2b6925-f6ba-403n-jj03-40fl435n430f", + # "coinbase_transaction_id": "63a25bb13cb5cf0001d2cf17", # withdrawals only + # "crypto_transaction_hash": "752f35570736341e2a253f7041a34cf1e196fc56128c900fd03d99da899d94c1", + # "tx_service_transaction_id": "1873249104", + # "coinbase_payment_method_id": "" + # }, + # "canceled_at": null, + # "processed_at": null, + # "user_nonce": null, + # "idem": "5e3201b0-e390-5k3k-a913-c32932049242", + # "profile_id": "k3k302a8-c4dk-4f49-9d39-3203923wpk39", + # "currency": "LTC" + # } + # ] + # + details = self.safe_value(transaction, 'details', {}) + timestamp = self.parse8601(self.safe_string(transaction, 'created_at')) + currencyId = self.safe_string(transaction, 'currency') + code = self.safe_currency_code(currencyId, currency) + amount = self.safe_number(transaction, 'amount') + type = self.safe_string(transaction, 'type') + address = self.safe_string(details, 'crypto_address') + address = self.safe_string(transaction, 'crypto_address', address) + fee = { + 'currency': None, + 'cost': None, + 'rate': None, + } + if type == 'withdraw': + type = 'withdrawal' + address = self.safe_string(details, 'sent_to_address', address) + feeCost = self.safe_number(details, 'fee') + if feeCost is not None: + if amount is not None: + amount -= feeCost + fee['cost'] = feeCost + fee['currency'] = code + networkId = self.safe_string(details, 'network') + return { + 'info': transaction, + 'id': self.safe_string(transaction, 'id'), + 'txid': self.safe_string(details, 'crypto_transaction_hash'), + 'type': type, + 'currency': code, + 'network': self.network_id_to_code(networkId), + 'amount': amount, + 'status': self.parse_transaction_status(transaction), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'address': address, + 'addressFrom': None, + 'addressTo': self.safe_string(details, 'crypto_address'), + 'tag': self.safe_string(details, 'destination_tag'), + 'tagFrom': None, + 'tagTo': None, + 'updated': self.parse8601(self.safe_string(transaction, 'processed_at')), + 'comment': None, + 'internal': False, + 'fee': fee, + } + + async def create_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + create a currency deposit address + + https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_postcoinbaseaccountaddresses + + :param str code: unified currency code of the currency for the deposit address + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + await self.load_markets() + currency = self.currency(code) + accounts = self.safe_value(self.options, 'coinbaseAccounts') + if accounts is None: + accounts = await self.privateGetCoinbaseAccounts() + self.options['coinbaseAccounts'] = accounts # cache it + self.options['coinbaseAccountsByCurrencyId'] = self.index_by(accounts, 'currency') + currencyId = currency['id'] + account = self.safe_value(self.options['coinbaseAccountsByCurrencyId'], currencyId) + if account is None: + # eslint-disable-next-line quotes + raise InvalidAddress(self.id + " createDepositAddress() could not find currency code " + code + " with id = " + currencyId + " in self.options['coinbaseAccountsByCurrencyId']") + request: dict = { + 'id': account['id'], + } + response = await self.privatePostCoinbaseAccountsIdAddresses(self.extend(request, params)) + address = self.safe_string(response, 'address') + tag = self.safe_string(response, 'destination_tag') + return { + 'currency': code, + 'address': self.check_address(address), + 'network': None, + 'tag': tag, + 'info': response, + } + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + request = '/' + self.implode_params(path, params) + query = self.omit(params, self.extract_params(path)) + if method == 'GET': + if query: + request += '?' + self.urlencode(query) + url = self.implode_hostname(self.urls['api'][api]) + request + if api == 'private': + self.check_required_credentials() + nonce = str(self.nonce()) + payload = '' + if method != 'GET': + if query: + body = self.json(query) + payload = body + what = nonce + method + request + payload + secret = None + try: + secret = self.base64_to_binary(self.secret) + except Exception as e: + raise AuthenticationError(self.id + ' sign() invalid base64 secret') + signature = self.hmac(self.encode(what), secret, hashlib.sha256, 'base64') + headers = { + 'CB-ACCESS-KEY': self.apiKey, + 'CB-ACCESS-SIGN': signature, + 'CB-ACCESS-TIMESTAMP': nonce, + 'CB-ACCESS-PASSPHRASE': self.password, + 'Content-Type': 'application/json', + } + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if (code == 400) or (code == 404): + if body[0] == '{': + message = self.safe_string(response, 'message') + feedback = self.id + ' ' + message + self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + raise ExchangeError(feedback) # unknown message + raise ExchangeError(self.id + ' ' + body) + return None + + async def request(self, path, api='public', method='GET', params={}, headers=None, body=None, config={}): + response = await self.fetch2(path, api, method, params, headers, body, config) + if not isinstance(response, str): + if 'message' in response: + raise ExchangeError(self.id + ' ' + self.json(response)) + return response diff --git a/ccxt/async_support/coinbaseinternational.py b/ccxt/async_support/coinbaseinternational.py new file mode 100644 index 0000000..f0f4626 --- /dev/null +++ b/ccxt/async_support/coinbaseinternational.py @@ -0,0 +1,2254 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.coinbaseinternational import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currencies, Currency, DepositAddress, Int, MarginModification, Market, Order, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, Trade, Transaction, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import DuplicateOrderId +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class coinbaseinternational(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(coinbaseinternational, self).describe(), { + 'id': 'coinbaseinternational', + 'name': 'Coinbase International', + 'countries': ['US'], + 'certified': False, + 'pro': True, + 'rateLimit': 100, # 10 requests per second + 'version': 'v1', + 'userAgent': self.userAgents['chrome'], + 'headers': { + 'CB-VERSION': '2018-05-30', + }, + 'has': { + 'CORS': True, + 'spot': True, + 'margin': True, + 'swap': True, + 'future': True, + 'option': False, + 'addMargin': False, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'cancelOrders': False, + 'closeAllPositions': False, + 'closePosition': False, + 'createDepositAddress': True, + 'createLimitBuyOrder': True, + 'createLimitSellOrder': True, + 'createMarketBuyOrder': True, + 'createMarketBuyOrderWithCost': False, + 'createMarketOrderWithCost': False, + 'createMarketSellOrder': True, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createPostOnlyOrder': True, + 'createReduceOnlyOrder': False, + 'createStopLimitOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'editOrder': True, + 'fetchAccounts': True, + 'fetchBalance': True, + 'fetchBidsAsks': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchCanceledOrders': False, + 'fetchClosedOrders': False, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDeposits': True, + 'fetchFundingHistory': True, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': True, + 'fetchFundingRates': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchL2OrderBook': False, + 'fetchLedger': False, + 'fetchLeverage': False, + 'fetchLeverageTiers': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMyBuys': True, + 'fetchMySells': True, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterestHistory': False, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': False, + 'fetchOrders': False, + 'fetchPosition': True, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': True, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': False, + 'fetchTrades': False, + 'fetchTradingFee': False, + 'fetchTradingFees': False, + 'fetchTransfers': True, + 'fetchWithdrawals': True, + 'reduceMargin': False, + 'sandbox': True, + 'setLeverage': False, + 'setMargin': True, + 'setMarginMode': False, + 'setPositionMode': False, + 'withdraw': True, + }, + 'urls': { + 'logo': 'https://github.com/ccxt/ccxt/assets/43336371/866ae638-6ab5-4ebf-ab2c-cdcce9545625', + 'api': { + 'rest': 'https://api.international.coinbase.com/api', + }, + 'test': { + 'rest': 'https://api-n5e1.coinbase.com/api', + }, + 'www': 'https://international.coinbase.com', + 'doc': [ + 'https://docs.cloud.coinbase.com/intx/docs', + ], + 'fees': [ + 'https://help.coinbase.com/en/international-exchange/trading-deposits-withdrawals/international-exchange-fees', + ], + 'referral': '', + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + 'password': True, + }, + 'api': { + 'v1': { + 'public': { + 'get': [ + 'assets', + 'assets/{assets}', + 'assets/{asset}/networks', + 'instruments', + 'instruments/{instrument}', + 'instruments/{instrument}/quote', + 'instruments/{instrument}/funding', + 'instruments/{instrument}/candles', + ], + }, + 'private': { + 'get': [ + 'orders', + 'orders/{id}', + 'portfolios', + 'portfolios/{portfolio}', + 'portfolios/{portfolio}/detail', + 'portfolios/{portfolio}/summary', + 'portfolios/{portfolio}/balances', + 'portfolios/{portfolio}/balances/{asset}', + 'portfolios/{portfolio}/positions', + 'portfolios/{portfolio}/positions/{instrument}', + 'portfolios/fills', + 'portfolios/{portfolio}/fills', + 'transfers', + 'transfers/{transfer_uuid}', + ], + 'post': [ + 'orders', + 'portfolios', + 'portfolios/margin', + 'portfolios/transfer', + 'transfers/withdraw', + 'transfers/address', + 'transfers/create-counterparty-id', + 'transfers/validate-counterparty-id', + 'transfers/withdraw/counterparty', + ], + 'put': [ + 'orders/{id}', + 'portfolios/{portfolio}', + ], + 'delete': [ + 'orders', + 'orders/{id}', + ], + }, + }, + }, + 'fees': { + 'trading': { + 'taker': self.parse_number('0.004'), + 'maker': self.parse_number('0.002'), + 'tierBased': True, + 'percentage': True, + 'tiers': { + 'taker': [ + [self.parse_number('0'), self.parse_number('0.004')], + [self.parse_number('1000000'), self.parse_number('0.004')], + [self.parse_number('5000000'), self.parse_number('0.0035')], + [self.parse_number('10000000'), self.parse_number('0.0035')], + [self.parse_number('50000000'), self.parse_number('0.003')], + [self.parse_number('250000000'), self.parse_number('0.0025')], + ], + 'maker': [ + [self.parse_number('0'), self.parse_number('0.002')], + [self.parse_number('1000000'), self.parse_number('0.0016')], + [self.parse_number('5000000'), self.parse_number('0.001')], + [self.parse_number('10000000'), self.parse_number('0.0008')], + [self.parse_number('50000000'), self.parse_number('0.0005')], + [self.parse_number('250000000'), self.parse_number('0')], + ], + }, + }, + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'exact': {}, + 'broad': { + 'DUPLICATE_CLIENT_ORDER_ID': DuplicateOrderId, + 'Order rejected': InvalidOrder, + 'market orders must be IoC': InvalidOrder, + 'tif is required': InvalidOrder, + 'Invalid replace order request': InvalidOrder, + 'Unauthorized': PermissionDenied, + 'invalid result_limit': BadRequest, + 'is a required field': BadRequest, + 'Not Found': BadRequest, + 'ip not allowed': AuthenticationError, + }, + }, + 'timeframes': { + '1m': 'ONE_MINUTE', + '5m': 'FIVE_MINUTE', + '15m': 'FIFTEEN_MINUTE', + '30m': 'THIRTY_MINUTE', + '1h': 'ONE_HOUR', + '2h': 'TWO_HOUR', + '6h': 'SIX_HOUR', + '1d': 'ONE_DAY', + }, + 'options': { + 'brokerId': 'nfqkvdjp', + 'portfolio': '', # default portfolio id + 'withdraw': { + 'method': 'v1PrivatePostTransfersWithdraw', # use v1PrivatePostTransfersWithdrawCounterparty for counterparty withdrawals + }, + 'networksById': { + 'ethereum': 'ETH', + 'arbitrum': 'ARBITRUM', + 'avacchain': 'AVAX', + 'optimism': 'OPTIMISM', + 'polygon': 'MATIC', + 'solana': 'SOL', + 'bitcoin': 'BTC', + }, + }, + 'features': { + 'default': { + 'sandbox': True, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerPriceType': None, + 'triggerDirection': True, + 'stopLossPrice': False, # todo implementation + 'takeProfitPrice': False, # todo implementation + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': True, + 'GTC': True, # has 30 days max + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': False, + 'marketBuyRequiresPrice': True, + 'selfTradePrevention': True, # todo: implement + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, + 'daysBack': None, + 'untilDays': 10000, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 100, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': None, + 'fetchOHLCV': { + 'limit': 300, + }, + }, + 'spot': { + 'extends': 'default', + }, + 'swap': { + 'linear': { + 'extends': 'default', + }, + 'inverse': { + 'extends': 'default', + }, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + }) + + async def handle_portfolio_and_params(self, methodName: str, params={}): + portfolio = None + portfolio, params = self.handle_option_and_params(params, methodName, 'portfolio') + if (portfolio is not None) and (portfolio != ''): + return [portfolio, params] + defaultPortfolio = self.safe_string(self.options, 'portfolio') + if (defaultPortfolio is not None) and (defaultPortfolio != ''): + return [defaultPortfolio, params] + accounts = await self.fetch_accounts() + for i in range(0, len(accounts)): + account = accounts[i] + info = self.safe_dict(account, 'info', {}) + if self.safe_bool(info, 'is_default'): + portfolioId = self.safe_string(info, 'portfolio_id') + self.options['portfolio'] = portfolioId + return [portfolioId, params] + raise ArgumentsRequired(self.id + ' ' + methodName + '() requires a portfolio parameter or set the default portfolio with self.options["portfolio"]') + + async def handle_network_id_and_params(self, currencyCode: str, methodName: str, params): + networkId = None + networkId, params = self.handle_option_and_params(params, methodName, 'network_arn_id') + if networkId is None: + await self.load_currency_networks(currencyCode) + networks = self.currencies[currencyCode]['networks'] + network = self.safe_string_2(params, 'networkCode', 'network') + if network is None: + # find default network + if self.is_empty(networks): + raise BadRequest(self.id + ' createDepositAddress network not found for currency ' + currencyCode + ' please specify networkId in params') + defaultNetwork = self.find_default_network(networks) + networkId = defaultNetwork['id'] + else: + networkId = self.network_code_to_id(network, currencyCode) + return [networkId, params] + + async def fetch_accounts(self, params={}): + """ + fetch all the accounts associated with a profile + + https://docs.cloud.coinbase.com/intx/reference/getportfolios + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `account structures ` indexed by the account type + """ + await self.load_markets() + response = await self.v1PrivateGetPortfolios(params) + # + # [ + # { + # "portfolio_id":"1ap32qsc-1-0", + # "portfolio_uuid":"028d7f6c-b92c-7361-8b7e-2932711e5a22", + # "name":"CCXT Portfolio 030624-17:16", + # "user_uuid":"e6cf46b6-a32f-5fa7-addb-3324d4526fbd", + # "maker_fee_rate":"0", + # "taker_fee_rate":"0.0002", + # "trading_lock":false, + # "borrow_disabled":false, + # "is_lsp":false, + # "is_default":true, + # "cross_collateral_enabled":false + # } + # ] + # + return self.parse_accounts(response, params) + + def parse_account(self, account): + # + # { + # "portfolio_id":"1ap32qsc-1-0", + # "portfolio_uuid":"028d7f6c-b92c-7361-8b7e-2932711e5a22", + # "name":"CCXT Portfolio 030624-17:16", + # "user_uuid":"e6cf46b6-a32f-5fa7-addb-3324d4526fbd", + # "maker_fee_rate":"0", + # "taker_fee_rate":"0.0002", + # "trading_lock":false, + # "borrow_disabled":false, + # "is_lsp":false, + # "is_default":true, + # "cross_collateral_enabled":false + # } + # + return { + 'id': self.safe_string_2(account, 'portfolio_id', 'portfolio_uuid'), + 'type': None, + 'code': None, + 'info': account, + } + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = 100, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://docs.cdp.coinbase.com/intx/reference/getinstrumentcandles + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch, default 100 max 10000 + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + :param int [params.until]: timestamp in ms of the latest candle to fetch + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate') + if paginate: + return await self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params, 10000) + market = self.market(symbol) + request: dict = { + 'instrument': market['id'], + 'granularity': self.safe_string(self.timeframes, timeframe, timeframe), + } + if since is not None: + request['start'] = self.iso8601(since) + else: + raise ArgumentsRequired(self.id + ' fetchOHLCV() requires a since argument') + unitl = self.safe_integer(params, 'until') + if unitl is not None: + params = self.omit(params, 'until') + request['end'] = self.iso8601(unitl) + response = await self.v1PublicGetInstrumentsInstrumentCandles(self.extend(request, params)) + # + # { + # "aggregations": [ + # { + # "start": "2024-04-23T00:00:00Z", + # "open": "62884.4", + # "high": "64710.6", + # "low": "62884.4", + # "close": "63508.4", + # "volume": "3253.9983" + # } + # ] + # } + # + candles = self.safe_list(response, 'aggregations', []) + return self.parse_ohlcvs(candles, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # { + # "start": "2024-04-23T00:00:00Z", + # "open": "62884.4", + # "high": "64710.6", + # "low": "62884.4", + # "close": "63508.4", + # "volume": "3253.9983" + # } + # + return [ + self.parse8601(self.safe_string_2(ohlcv, 'start', 'time')), + self.safe_number(ohlcv, 'open'), + self.safe_number(ohlcv, 'high'), + self.safe_number(ohlcv, 'low'), + self.safe_number(ohlcv, 'close'), + self.safe_number(ohlcv, 'volume'), + ] + + async def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical funding rate prices + + https://docs.cloud.coinbase.com/intx/reference/getinstrumentfunding + + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: timestamp in ms of the earliest funding rate to fetch + :param int [limit]: the maximum amount of `funding rate structures ` to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `funding rate structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchFundingRateHistory() requires a symbol argument') + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchFundingRateHistory', 'paginate') + maxEntriesPerRequest = None + maxEntriesPerRequest, params = self.handle_option_and_params(params, 'fetchFundingRateHistory', 'maxEntriesPerRequest', 100) + pageKey = 'ccxtPageKey' + if paginate: + return await self.fetch_paginated_call_incremental('fetchFundingRateHistory', symbol, since, limit, params, pageKey, maxEntriesPerRequest) + market = self.market(symbol) + page = self.safe_integer(params, pageKey, 1) - 1 + request: dict = { + 'instrument': market['id'], + 'result_offset': self.safe_integer_2(params, 'offset', 'result_offset', page * maxEntriesPerRequest), + } + if limit is not None: + request['result_limit'] = limit + response = await self.v1PublicGetInstrumentsInstrumentFunding(self.extend(request, params)) + # + # { + # "pagination":{ + # "result_limit":"25", + # "result_offset":"0" + # }, + # "results":[ + # { + # "instrument_id":"149264167780483072", + # "funding_rate":"0.000011", + # "mark_price":"47388.1", + # "event_time":"2024-02-10T16:00:00Z" + # }, + # ... + # ] + # } + # + rawRates = self.safe_list(response, 'results', []) + return self.parse_funding_rate_histories(rawRates, market, since, limit) + + def parse_funding_rate_history(self, info, market: Market = None): + return self.parse_funding_rate(info, market) + + def parse_funding_rate(self, contract, market: Market = None): + # + # { + # "instrument_id":"149264167780483072", + # "funding_rate":"0.000011", + # "mark_price":"47388.1", + # "event_time":"2024-02-10T16:00:00Z" + # } + # + fundingDatetime = self.safe_string_2(contract, 'event_time', 'time') + return { + 'info': contract, + 'symbol': self.safe_symbol(None, market), + 'markPrice': self.safe_number(contract, 'mark_price'), + 'indexPrice': None, + 'interestRate': None, + 'estimatedSettlePrice': None, + 'timestamp': self.parse8601(fundingDatetime), + 'datetime': fundingDatetime, + 'fundingRate': self.safe_number(contract, 'funding_rate'), + 'fundingTimestamp': self.parse8601(fundingDatetime), + 'fundingDatetime': fundingDatetime, + 'nextFundingRate': None, + 'nextFundingTimestamp': None, + 'nextFundingDatetime': None, + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + } + + async def fetch_funding_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch the history of funding payments paid and received on self account + + https://docs.cdp.coinbase.com/intx/reference/gettransfers + + :param str [symbol]: unified market symbol + :param int [since]: the earliest time in ms to fetch funding history for + :param int [limit]: the maximum number of funding history structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding history structure ` + """ + await self.load_markets() + request: dict = { + 'type': 'FUNDING', + } + market: Market = None + if symbol is not None: + market = self.market(symbol) + portfolios = None + portfolios, params = self.handle_option_and_params(params, 'fetchFundingHistory', 'portfolios') + if portfolios is not None: + request['portfolios'] = portfolios + if since is not None: + request['time_from'] = self.iso8601(since) + if limit is not None: + request['result_limit'] = limit + else: + request['result_limit'] = 100 + response = await self.v1PrivateGetTransfers(self.extend(request, params)) + fundings = self.safe_list(response, 'results', []) + return self.parse_incomes(fundings, market, since, limit) + + def parse_income(self, income, market: Market = None): + # + # { + # "amount":"0.0008", + # "asset":"USDC", + # "created_at":"2024-02-22T16:00:00Z", + # "from_portfolio":{ + # "id":"13yuk1fs-1-0", + # "name":"Eng Test Portfolio - 2", + # "uuid":"018712f2-5ff9-7de3-9010-xxxxxxxxx" + # }, + # "instrument_id":"149264164756389888", + # "instrument_symbol":"ETH-PERP", + # "position_id":"1xy4v51m-1-2", + # "status":"PROCESSED", + # "to_portfolio":{ + # "name":"CB_FUND" + # }, + # "transfer_type":"FUNDING", + # "transfer_uuid":"a6b708df-2c44-32c5-bb98-xxxxxxxxxx", + # "updated_at":"2024-02-22T16:00:00Z" + # } + # + marketId = self.safe_string(income, 'symbol') + market = self.safe_market(marketId, market, None, 'contract') + datetime = self.safe_integer(income, 'created_at') + timestamp = self.parse8601(datetime) + currencyId = self.safe_string(income, 'asset') + code = self.safe_currency_code(currencyId) + return { + 'info': income, + 'symbol': market['symbol'], + 'code': code, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': self.safe_string(income, 'transfer_uuid'), + 'amount': self.safe_number(income, 'amount'), + 'rate': None, + } + + async def fetch_transfers(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[TransferEntry]: + """ + fetch a history of internal transfers made on an account + + https://docs.cdp.coinbase.com/intx/reference/gettransfers + + :param str code: unified currency code of the currency transferred + :param int [since]: the earliest time in ms to fetch transfers for + :param int [limit]: the maximum number of transfers structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transfer structures ` + """ + await self.load_markets() + request: dict = { + 'type': 'INTERNAL', + } + currency = None + if code is not None: + currency = self.currency(code) + portfolios = None + portfolios, params = self.handle_option_and_params(params, 'fetchTransfers', 'portfolios') + if portfolios is not None: + request['portfolios'] = portfolios + if since is not None: + request['time_from'] = self.iso8601(since) + if limit is not None: + request['result_limit'] = limit + else: + request['result_limit'] = 100 + response = await self.v1PrivateGetTransfers(self.extend(request, params)) + transfers = self.safe_list(response, 'results', []) + return self.parse_transfers(transfers, currency, since, limit) + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + # + # { + # "amount":"0.0008", + # "asset":"USDC", + # "created_at":"2024-02-22T16:00:00Z", + # "from_portfolio":{ + # "id":"13yuk1fs-1-0", + # "name":"Eng Test Portfolio - 2", + # "uuid":"018712f2-5ff9-7de3-9010-xxxxxxxxx" + # }, + # "instrument_id":"149264164756389888", + # "instrument_symbol":"ETH-PERP", + # "position_id":"1xy4v51m-1-2", + # "status":"PROCESSED", + # "to_portfolio":{ + # "name":"CB_FUND" + # }, + # "transfer_type":"FUNDING", + # "transfer_uuid":"a6b708df-2c44-32c5-bb98-xxxxxxxxxx", + # "updated_at":"2024-02-22T16:00:00Z" + # } + # + datetime = self.safe_integer(transfer, 'created_at') + timestamp = self.parse8601(datetime) + currencyId = self.safe_string(transfer, 'asset') + code = self.safe_currency_code(currencyId) + fromPorfolio = self.safe_dict(transfer, 'from_portfolio', {}) + fromId = self.safe_string(fromPorfolio, 'id') + toPorfolio = self.safe_dict(transfer, 'to_portfolio', {}) + toId = self.safe_string(toPorfolio, 'id') + return { + 'info': transfer, + 'id': self.safe_string(transfer, 'transfer_uuid'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'currency': code, + 'amount': self.safe_number(transfer, 'amount'), + 'fromAccount': fromId, + 'toAccount': toId, + 'status': self.parse_transfer_status(self.safe_string(transfer, 'status')), + } + + def parse_transfer_status(self, status: Str) -> Str: + statuses: dict = { + 'FAILED': 'failed', + 'PROCESSED': 'ok', + 'NEW': 'pending', + 'STARTED': 'pending', + } + return self.safe_string(statuses, status, status) + + async def create_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + create a currency deposit address + + https://docs.cloud.coinbase.com/intx/reference/createaddress + https://docs.cloud.coinbase.com/intx/reference/createcounterpartyid + + :param str code: unified currency code of the currency for the deposit address + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.network_arn_id]: Identifies the blockchain network(e.g., networks/ethereum-mainnet/assets/313ef8a9-ae5a-5f2f-8a56-572c0e2a4d5a) if not provided will pick default + :param str [params.network]: unified network code to identify the blockchain network + :returns dict: an `address structure ` + """ + await self.load_markets() + method = None + method, params = self.handle_option_and_params(params, 'createDepositAddress', 'method', 'v1PrivatePostTransfersAddress') + portfolio = None + portfolio, params = await self.handle_portfolio_and_params('createDepositAddress', params) + request: dict = { + 'portfolio': portfolio, + } + if method == 'v1PrivatePostTransfersAddress': + currency = self.currency(code) + request['asset'] = currency['id'] + networkId = None + networkId, params = await self.handle_network_id_and_params(code, 'createDepositAddress', params) + request['network_arn_id'] = networkId + response = await getattr(self, method)(self.extend(request, params)) + # + # v1PrivatePostTransfersAddress + # { + # address: "3LkwYscRyh6tUR1XTqXSJQoJnK7ucC1F4n", + # network_arn_id: "networks/bitcoin-mainnet/assets/6ecc0dcc-10a2-500e-b315-a3b9abae19ce", + # destination_tag: "", + # } + # v1PrivatePostTransfersCreateCounterpartyId + # { + # "portfolio_uuid":"018e0a8b-6b6b-70e0-9689-1e7926c2c8bc", + # "counterparty_id":"CB2ZPUCZBE" + # } + # + tag = self.safe_string(response, 'destination_tag') + address = self.safe_string_2(response, 'address', 'counterparty_id') + return { + 'currency': code, + 'tag': tag, + 'address': address, + 'network': None, + 'info': response, + } + + def find_default_network(self, networks): + networksArray = self.to_array(networks) + for i in range(0, len(networksArray)): + info = networksArray[i]['info'] + is_default = self.safe_bool(info, 'is_default', False) + if is_default is True: + return networksArray[i] + return networksArray[0] + + async def load_currency_networks(self, code, params={}): + currency = self.currency(code) + networks = self.safe_dict(currency, 'networks') + if networks is not None: + return False + request: dict = { + 'asset': currency['id'], + } + rawNetworks = await self.v1PublicGetAssetsAssetNetworks(request) + # + # [ + # { + # "asset_id":"1", + # "asset_uuid":"2b92315d-eab7-5bef-84fa-089a131333f5", + # "asset_name":"USDC", + # "network_arn_id":"networks/ethereum-mainnet/assets/9bc140b4-69c3-5fc9-bd0d-b041bcf40039", + # "min_withdrawal_amt":"1", + # "max_withdrawal_amt":"100000000", + # "network_confirms":35, + # "processing_time":485, + # "is_default":true, + # "network_name":"ethereum", + # "display_name":"Ethereum" + # }, + # .... + # ] + # + currency['networks'] = self.parse_networks(rawNetworks) + return True + + def parse_networks(self, networks, params={}): + result: dict = {} + for i in range(0, len(networks)): + network = self.extend(self.parse_network(networks[i]), params) + result[network['network']] = network + return result + + def parse_network(self, network, params={}): + # + # { + # "asset_id":"1", + # "asset_uuid":"2b92315d-eab7-5bef-84fa-089a131333f5", + # "asset_name":"USDC", + # "network_arn_id":"networks/ethereum-mainnet/assets/9bc140b4-69c3-5fc9-bd0d-b041bcf40039", + # "min_withdrawal_amt":"1", + # "max_withdrawal_amt":"100000000", + # "network_confirms":35, + # "processing_time":485, + # "is_default":true, + # "network_name":"ethereum", + # "display_name":"Ethereum" + # } + # + currencyId = self.safe_string(network, 'asset_name') + currencyCode = self.safe_currency_code(currencyId) + networkId = self.safe_string(network, 'network_arn_id') + networkIdForCode = self.safe_string_n(network, ['network_name', 'display_name', 'network_arn_id'], '') + return self.safe_network({ + 'info': network, + 'id': networkId, + 'name': self.safe_string(network, 'display_name'), + 'network': self.network_id_to_code(networkIdForCode, currencyCode), + 'active': None, + 'deposit': None, + 'withdraw': None, + 'precision': None, + 'fee': None, + 'limits': { + 'withdraw': { + 'min': self.safe_number(network, 'min_withdrawal_amt'), + 'max': self.safe_number(network, 'max_withdrawal_amt'), + }, + 'deposit': { + 'min': None, + 'max': None, + }, + }, + }) + + async def set_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + Either adds or reduces margin in order to set the margin to a specific value + + https://docs.cloud.coinbase.com/intx/reference/setportfoliomarginoverride + + :param str symbol: unified market symbol of the market to set margin in + :param float amount: the amount to set the margin to + :param dict [params]: parameters specific to the exchange API endpoint + :returns dict: A `margin structure ` + """ + portfolio = None + portfolio, params = await self.handle_portfolio_and_params('setMargin', params) + if symbol is not None: + raise BadRequest(self.id + ' setMargin() only allows setting margin to full portfolio') + request: dict = { + 'portfolio': portfolio, + 'margin_override': amount, + } + return await self.v1PrivatePostPortfoliosMargin(self.extend(request, params)) + + async def fetch_deposits_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch history of deposits and withdrawals + + https://docs.cloud.coinbase.com/intx/reference/gettransfers + + :param str [code]: unified currency code for the currency of the deposit/withdrawals, default is None + :param int [since]: timestamp in ms of the earliest deposit/withdrawal, default is None + :param int [limit]: max number of deposit/withdrawals to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.portfolios]: Identifies the portfolios by UUID(e.g., 892e8c7c-e979-4cad-b61b-55a197932cf1) or portfolio ID(e.g., 5189861793641175). Can provide single or multiple portfolios to filter by or fetches transfers for all portfolios if none are provided. + :param int [params.until]: Only find transfers updated before self time. Use timestamp format + :param str [params.status]: The current status of transfer. Possible values: [PROCESSED, NEW, FAILED, STARTED] + :param str [params.type]: The type of transfer Possible values: [DEPOSIT, WITHDRAW, REBATE, STIPEND, INTERNAL, FUNDING] + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict: a list of `transaction structures ` + """ + await self.load_markets() + paginate = None + paginate, params = self.handle_option_and_params(params, 'fetchDepositsWithdrawals', 'paginate') + maxEntriesPerRequest = None + maxEntriesPerRequest, params = self.handle_option_and_params(params, 'fetchDepositsWithdrawals', 'maxEntriesPerRequest', 100) + pageKey = 'ccxtPageKey' + if paginate: + return await self.fetch_paginated_call_incremental('fetchDepositsWithdrawals', code, since, limit, params, pageKey, maxEntriesPerRequest) + page = self.safe_integer(params, pageKey, 1) - 1 + request: dict = { + 'result_offset': self.safe_integer_2(params, 'offset', 'result_offset', page * maxEntriesPerRequest), + } + if since is not None: + request['time_from'] = self.iso8601(since) + if limit is not None: + newLimit = min(limit, 100) + request['result_limit'] = newLimit + portfolios = None + portfolios, params = self.handle_option_and_params(params, 'fetchDepositsWithdrawals', 'portfolios') + if portfolios is not None: + request['portfolios'] = portfolios + until = None + until, params = self.handle_option_and_params(params, 'fetchDepositsWithdrawals', 'until') + if until is not None: + request['time_to'] = self.iso8601(until) + response = await self.v1PrivateGetTransfers(self.extend(request, params)) + # + # { + # "pagination":{ + # "result_limit":25, + # "result_offset":0 + # }, + # "results":[ + # { + # "transfer_uuid":"8e471d77-4208-45a8-9e5b-f3bd8a2c1fc3", + # "transfer_type":"WITHDRAW", + # "amount":"1.000000", + # "asset":"USDC", + # "status":"PROCESSED", + # "network_name":"ethereum", + # "created_at":"2024-03-14T02:32:18.497795Z", + # "updated_at":"2024-03-14T02:35:38.514588Z", + # "from_portfolio":{ + # "id":"1yun54bb-1-6", + # "uuid":"018e0a8b-6b6b-70e0-9689-1e7926c2c8bc", + # "name":"fungus technology o?Portfolio" + # }, + # "to_address":"0xcdcE79F820BE9d6C5033db5c31d1AE3A8c2399bB" + # } + # ] + # } + # + rawTransactions = self.safe_list(response, 'results', []) + return self.parse_transactions(rawTransactions) + + async def fetch_position(self, symbol: str, params={}): + """ + + https://docs.cloud.coinbase.com/intx/reference/getportfolioposition + + fetch data on an open position + :param str symbol: unified market symbol of the market the position is held in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `position structure ` + """ + await self.load_markets() + symbol = self.symbol(symbol) + portfolio = None + portfolio, params = await self.handle_portfolio_and_params('fetchPosition', params) + request: dict = { + 'portfolio': portfolio, + 'instrument': self.market_id(symbol), + } + position = await self.v1PrivateGetPortfoliosPortfolioPositionsInstrument(self.extend(request, params)) + # + # { + # "symbol":"BTC-PERP", + # "instrument_id":"114jqr89-0-0", + # "instrument_uuid":"b3469e0b-222c-4f8a-9f68-1f9e44d7e5e0", + # "vwap":"52482.3", + # "net_size":"0", + # "buy_order_size":"0.001", + # "sell_order_size":"0", + # "im_contribution":"0.2", + # "unrealized_pnl":"0", + # "mark_price":"52406.8", + # "entry_vwap":"52472.9" + # } + # + return self.parse_position(position) + + def parse_position(self, position: dict, market: Market = None): + # + # { + # "symbol":"BTC-PERP", + # "instrument_id":"114jqr89-0-0", + # "instrument_uuid":"b3469e0b-222c-4f8a-9f68-1f9e44d7e5e0", + # "vwap":"52482.3", + # "net_size":"0", + # "buy_order_size":"0.001", + # "sell_order_size":"0", + # "im_contribution":"0.2", + # "unrealized_pnl":"0", + # "mark_price":"52406.8", + # "entry_vwap":"52472.9" + # } + # + marketId = self.safe_string(position, 'symbol') + quantity = self.safe_string(position, 'net_size') + market = self.safe_market(marketId, market, '-') + side = 'long' + if Precise.string_le(quantity, '0'): + side = 'short' + quantity = Precise.string_mul('-1', quantity) + return self.safe_position({ + 'info': position, + 'id': self.safe_string(position, 'id'), + 'symbol': market['symbol'], + 'entryPrice': None, + 'markPrice': self.safe_number(position, 'mark_price'), + 'notional': None, + 'collateral': None, + 'unrealizedPnl': self.safe_number(position, 'unrealized_pnl'), + 'side': side, + 'contracts': self.parse_number(quantity), + 'contractSize': self.safe_number(market, 'contractSize'), + 'timestamp': None, + 'datetime': None, + 'hedged': None, + 'maintenanceMargin': None, + 'maintenanceMarginPercentage': None, + 'initialMargin': self.safe_number(position, 'im_contribution'), + 'initialMarginPercentage': None, + 'leverage': None, + 'liquidationPrice': None, + 'marginRatio': None, + 'marginMode': None, + 'percentage': None, + }) + + async def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + + https://docs.cloud.coinbase.com/intx/reference/getportfoliopositions + + fetch all open positions + :param str[] [symbols]: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + await self.load_markets() + portfolio = None + portfolio, params = await self.handle_portfolio_and_params('fetchPositions', params) + request: dict = { + 'portfolio': portfolio, + } + response = await self.v1PrivateGetPortfoliosPortfolioPositions(self.extend(request, params)) + # + # [ + # { + # "symbol":"BTC-PERP", + # "instrument_id":"114jqr89-0-0", + # "instrument_uuid":"b3469e0b-222c-4f8a-9f68-1f9e44d7e5e0", + # "vwap":"52482.3", + # "net_size":"0", + # "buy_order_size":"0.001", + # "sell_order_size":"0", + # "im_contribution":"0.2", + # "unrealized_pnl":"0", + # "mark_price":"52406.8", + # "entry_vwap":"52472.9" + # } + # ] + # + positions = self.parse_positions(response) + if self.is_empty(symbols): + return positions + symbols = self.market_symbols(symbols) + return self.filter_by_array_positions(positions, 'symbol', symbols, False) + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://docs.cloud.coinbase.com/intx/reference/gettransfers + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.portfolios]: Identifies the portfolios by UUID(e.g., 892e8c7c-e979-4cad-b61b-55a197932cf1) or portfolio ID(e.g., 5189861793641175). Can provide single or multiple portfolios to filter by or fetches transfers for all portfolios if none are provided. + :param int [params.until]: Only find transfers updated before self time. Use timestamp format + :param str [params.status]: The current status of transfer. Possible values: [PROCESSED, NEW, FAILED, STARTED] + :param str [params.type]: The type of transfer Possible values: [DEPOSIT, WITHDRAW, REBATE, STIPEND, INTERNAL, FUNDING] + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + params['type'] = 'WITHDRAW' + return await self.fetch_deposits_withdrawals(code, since, limit, params) + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.portfolios]: Identifies the portfolios by UUID(e.g., 892e8c7c-e979-4cad-b61b-55a197932cf1) or portfolio ID(e.g., 5189861793641175). Can provide single or multiple portfolios to filter by or fetches transfers for all portfolios if none are provided. + :param int [params.until]: Only find transfers updated before self time. Use timestamp format + :param str [params.status]: The current status of transfer. Possible values: [PROCESSED, NEW, FAILED, STARTED] + :param str [params.type]: The type of transfer Possible values: [DEPOSIT, WITHDRAW, REBATE, STIPEND, INTERNAL, FUNDING] + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + params['type'] = 'DEPOSIT' + return await self.fetch_deposits_withdrawals(code, since, limit, params) + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'PROCESSED': 'ok', + 'NEW': 'pending', + 'STARTED': 'pending', + 'FAILED': 'canceled', + } + return self.safe_string(statuses, status, status) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # { + # "idem":"8e471d77-4208-45a8-9e5b-f3bd8a2c1fc3" + # } + # transactionType = self.safe_string(transaction, 'type') + datetime = self.safe_string(transaction, 'updated_at') + fromPorfolio = self.safe_dict(transaction, 'from_portfolio', {}) + addressFrom = self.safe_string_n(transaction, ['from_address', 'from_cb_account', self.safe_string_n(fromPorfolio, ['id', 'uuid', 'name']), 'from_counterparty_id']) + toPorfolio = self.safe_dict(transaction, 'from_portfolio', {}) + addressTo = self.safe_string_n(transaction, ['to_address', 'to_cb_account', self.safe_string_n(toPorfolio, ['id', 'uuid', 'name']), 'to_counterparty_id']) + return { + 'info': transaction, + 'id': self.safe_string(transaction, 'transfer_uuid'), + 'txid': self.safe_string(transaction, 'transaction_uuid'), + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + 'network': self.network_id_to_code(self.safe_string(transaction, 'network_name')), + 'address': None, # TODO check if withdraw or deposit and populate + 'addressTo': addressTo, + 'addressFrom': addressFrom, + 'tag': None, + 'tagTo': None, + 'tagFrom': None, + 'type': self.safe_string(transaction, 'resource'), + 'amount': self.safe_number(transaction, 'amount'), + 'currency': self.safe_currency_code(self.safe_string(transaction, 'asset'), currency), + 'status': self.parse_transaction_status(self.safe_string(transaction, 'status')), + 'updated': self.parse8601(datetime), + 'fee': { + 'cost': None, + 'currency': None, + }, + } + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # { + # "portfolio_id":"1wp37qsc-1-0", + # "portfolio_uuid":"018d7f6c-b92c-7361-8b7e-2932711e5a22", + # "portfolio_name":"CCXT Portfolio 020624-17:16", + # "fill_id":"1xbfy19y-1-184", + # "exec_id":"280841526207070392", + # "order_id":"1xbfv8yw-1-0", + # "instrument_id":"114jqr89-0-0", + # "instrument_uuid":"b3469e0b-222c-4f8a-9f68-1f9e44d7e5e0", + # "symbol":"BTC-PERP", + # "match_id":"280841526207053840", + # "fill_price":"52500", + # "fill_qty":"0.01", + # "client_id":"1x59ctku-1-1", + # "client_order_id":"ccxt3e4e2a5f-4a89-", + # "order_qty":"0.01", + # "limit_price":"52500", + # "total_filled":"0.01", + # "filled_vwap":"52500", + # "expire_time":"", + # "stop_price":"", + # "side":"BUY", + # "tif":"GTC", + # "stp_mode":"BOTH", + # "flags":"", + # "fee":"0.105", + # "fee_asset":"USDC", + # "order_status":"DONE", + # "event_time":"2024-02-15T00:43:57.631Z" + # } + # + marketId = self.safe_string(trade, 'symbol') + datetime = self.safe_string(trade, 'event_time') + return self.safe_trade({ + 'info': trade, + 'id': self.safe_string_2(trade, 'fill_id', 'exec_id'), + 'order': self.safe_string(trade, 'order_id'), + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + 'symbol': self.safe_symbol(marketId, market), + 'type': None, + 'side': self.safe_string_lower(trade, 'side'), + 'takerOrMaker': None, + 'price': self.safe_number(trade, 'fill_price'), + 'amount': self.safe_number(trade, 'fill_qty'), + 'cost': None, + 'fee': { + 'cost': self.safe_number(trade, 'fee'), + 'currency': self.safe_currency_code(self.safe_string(trade, 'fee_asset')), + }, + }) + + async def fetch_markets(self, params={}) -> List[Market]: + """ + + https://docs.cloud.coinbase.com/intx/reference/getinstruments + + retrieves data on all markets for coinbaseinternational + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = await self.v1PublicGetInstruments(params) + # + # [ + # { + # "instrument_id":"149264164756389888", + # "instrument_uuid":"e9360798-6a10-45d6-af05-67c30eb91e2d", + # "symbol":"ETH-PERP", + # "type":"PERP", + # "base_asset_id":"118059611793145856", + # "base_asset_uuid":"d85dce9b-5b73-5c3c-8978-522ce1d1c1b4", + # "base_asset_name":"ETH", + # "quote_asset_id":"1", + # "quote_asset_uuid":"2b92315d-eab7-5bef-84fa-089a131333f5", + # "quote_asset_name":"USDC", + # "base_increment":"0.0001", + # "quote_increment":"0.01", + # "price_band_percent":"0.02", + # "market_order_percent":"0.0075", + # "qty_24hr":"44434.8131", + # "notional_24hr":"110943454.279785", + # "avg_daily_qty":"1099171.6025", + # "avg_daily_notional":"2637240145.456987", + # "previous_day_qty":"78909.3939", + # "open_interest":"1270.749", + # "position_limit_qty":"1831.9527", + # "position_limit_adq_pct":"0.05", + # "replacement_cost":"0.23", + # "base_imf":"0.1", + # "min_notional_value":"10", + # "funding_interval":"3600000000000", + # "trading_state":"TRADING", + # "quote":{ + # "best_bid_price":"2490.8", + # "best_bid_size":"9.0515", + # "best_ask_price":"2490.81", + # "best_ask_size":"4.8486", + # "trade_price":"2490.39", + # "trade_qty":"0.9508", + # "index_price":"2490.5", + # "mark_price":"2490.8", + # "settlement_price":"2490.81", + # "limit_up":"2615.42", + # "limit_down":"2366.34", + # "predicted_funding":"0.000009", + # "timestamp":"2024-02-10T16:07:39.454Z" + # } + # }, + # ... + # ] + # + return self.parse_markets(response) + + def parse_market(self, market: dict) -> Market: + # + # { + # "instrument_id":"149264164756389888", + # "instrument_uuid":"e9360798-6a10-45d6-af05-67c30eb91e2d", + # "symbol":"ETH-PERP", + # "type":"PERP", + # "base_asset_id":"118059611793145856", + # "base_asset_uuid":"d85dce9b-5b73-5c3c-8978-522ce1d1c1b4", + # "base_asset_name":"ETH", + # "quote_asset_id":"1", + # "quote_asset_uuid":"2b92315d-eab7-5bef-84fa-089a131333f5", + # "quote_asset_name":"USDC", + # "base_increment":"0.0001", + # "quote_increment":"0.01", + # "price_band_percent":"0.02", + # "market_order_percent":"0.0075", + # "qty_24hr":"44434.8131", + # "notional_24hr":"110943454.279785", + # "avg_daily_qty":"1099171.6025", + # "avg_daily_notional":"2637240145.456987", + # "previous_day_qty":"78909.3939", + # "open_interest":"1270.749", + # "position_limit_qty":"1831.9527", + # "position_limit_adq_pct":"0.05", + # "replacement_cost":"0.23", + # "base_imf":"0.1", + # "min_notional_value":"10", + # "funding_interval":"3600000000000", + # "trading_state":"TRADING", + # "quote":{ + # "best_bid_price":"2490.8", + # "best_bid_size":"9.0515", + # "best_ask_price":"2490.81", + # "best_ask_size":"4.8486", + # "trade_price":"2490.39", + # "trade_qty":"0.9508", + # "index_price":"2490.5", + # "mark_price":"2490.8", + # "settlement_price":"2490.81", + # "limit_up":"2615.42", + # "limit_down":"2366.34", + # "predicted_funding":"0.000009", + # "timestamp":"2024-02-10T16:07:39.454Z" + # } + # } + # + marketId = self.safe_string(market, 'symbol') + baseId = self.safe_string(market, 'base_asset_name') + quoteId = self.safe_string(market, 'quote_asset_name') + typeId = self.safe_string(market, 'type') # 'SPOT', 'PERP' + isSpot = (typeId == 'SPOT') + fees = self.fees + symbol = baseId + '/' + quoteId + settleId = None + if not isSpot: + settleId = quoteId + symbol += ':' + quoteId + return { + 'id': marketId, + 'lowercaseId': marketId.lower(), + 'symbol': symbol, + 'base': baseId, + 'quote': quoteId, + 'settle': settleId if settleId else None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId if settleId else None, + 'type': 'spot' if isSpot else 'swap', + 'spot': isSpot, + 'margin': False, + 'swap': not isSpot, + 'future': False, + 'option': False, + 'active': self.safe_string(market, 'trading_state') == 'TRADING', + 'contract': not isSpot, + 'linear': None if isSpot else (settleId == quoteId), + 'inverse': None if isSpot else (settleId != quoteId), + 'taker': fees['trading']['taker'], + 'maker': fees['trading']['maker'], + 'contractSize': None if isSpot else 1, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.safe_number(market, 'base_increment'), + 'price': self.safe_number(market, 'quote_increment'), + 'cost': self.safe_number(market, 'quote_increment'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': self.safe_number(market, 'base_imf'), + }, + 'amount': { + 'min': None, + 'max': None if isSpot else self.safe_number(market, 'position_limit_qty'), + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': self.safe_number(market, 'min_notional_value'), + 'max': None, + }, + }, + 'info': market, + 'created': None, + } + + async def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://docs.cloud.coinbase.com/intx/reference/getassets + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + currencies = await self.v1PublicGetAssets(params) + # + # [ + # { + # "asset_id":"1", + # "asset_uuid":"2b92315d-eab7-5bef-84fa-089a131333f5", + # "asset_name":"USDC", + # "status":"ACTIVE", + # "collateral_weight":1.0, + # "supported_networks_enabled":true + # }, + # ... + # ] + # + return self.parse_currencies(currencies) + + def parse_currency(self, currency: dict) -> Currency: + # + # { + # "asset_id":"1", + # "asset_uuid":"2b92315d-eab7-5bef-84fa-089a131333f5", + # "asset_name":"USDC", + # "status":"ACTIVE", + # "collateral_weight":1.0, + # "supported_networks_enabled":true + # } + # + id = self.safe_string(currency, 'asset_name') + code = self.safe_currency_code(id) + statusId = self.safe_string(currency, 'status') + return self.safe_currency_structure({ + 'id': id, + 'name': code, + 'code': code, + 'precision': None, + 'info': currency, + 'active': (statusId == 'ACTIVE'), + 'deposit': None, + 'withdraw': None, + 'networks': None, + 'fee': None, + 'fees': None, + 'limits': self.limits, + }) + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://docs.cloud.coinbase.com/intx/reference/getinstruments + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + instruments = await self.v1PublicGetInstruments(params) + tickers: dict = {} + for i in range(0, len(instruments)): + instrument = instruments[i] + marketId = self.safe_string(instrument, 'symbol') + symbol = self.safe_symbol(marketId) + quote = self.safe_dict(instrument, 'quote', {}) + tickers[symbol] = self.parse_ticker(quote, self.safe_market(marketId)) + return self.filter_by_array(tickers, 'symbol', symbols, True) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://docs.cloud.coinbase.com/intx/reference/getinstrumentquote + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'instrument': self.market_id(symbol), + } + ticker = await self.v1PublicGetInstrumentsInstrumentQuote(self.extend(request, params)) + return self.parse_ticker(ticker, market) + + def parse_ticker(self, ticker: object, market: Market = None) -> Ticker: + # + # { + # "best_bid_price":"2490.8", + # "best_bid_size":"9.0515", + # "best_ask_price":"2490.81", + # "best_ask_size":"4.8486", + # "trade_price":"2490.39", + # "trade_qty":"0.9508", + # "index_price":"2490.5", + # "mark_price":"2490.8", + # "settlement_price":"2490.81", + # "limit_up":"2615.42", + # "limit_down":"2366.34", + # "predicted_funding":"0.000009", + # "timestamp":"2024-02-10T16:07:39.454Z" + # } + # + datetime = self.safe_string(ticker, 'timestamp') + return self.safe_ticker({ + 'info': ticker, + 'symbol': self.safe_symbol(None, market), + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + 'bid': self.safe_number(ticker, 'best_bid_price'), + 'bidVolume': self.safe_number(ticker, 'best_bid_size'), + 'ask': self.safe_number(ticker, 'best_ask_price'), + 'askVolume': self.safe_number(ticker, 'best_ask_size'), + 'high': None, + 'low': None, + 'open': None, + 'close': None, + 'last': None, + 'change': None, + 'percentage': None, + 'average': None, + 'vwap': None, + 'baseVolume': None, + 'quoteVolume': None, + 'previousClose': None, + 'markPrice': self.safe_number(ticker, 'mark_price'), + 'indexPrice': self.safe_number(ticker, 'index_price'), + }) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://docs.cloud.coinbase.com/intx/reference/getportfoliobalances + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.v3]: default False, set True to use v3 api endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + portfolio = None + portfolio, params = await self.handle_portfolio_and_params('fetchBalance', params) + request: dict = { + 'portfolio': portfolio, + } + balances = await self.v1PrivateGetPortfoliosPortfolioBalances(self.extend(request, params)) + # + # [ + # { + # "asset_id":"0-0-1", + # "asset_name":"USDC", + # "asset_uuid":"2b92315d-eab7-5bef-84fa-089a131333f5", + # "quantity":"500000.0000000000", + # "hold":"0", + # "hold_available_for_collateral":"0", + # "transfer_hold":"0", + # "collateral_value":"500000.0", + # "max_withdraw_amount":"500000.0000000000", + # "loan":"0", + # "loan_collateral_requirement":"0.0" + # } + # ] + # + return self.parse_balance(balances) + + def parse_balance(self, response) -> Balances: + # + # { + # "asset_id":"0-0-1", + # "asset_name":"USDC", + # "asset_uuid":"2b92315d-eab7-5bef-84fa-089a131333f5", + # "quantity":"500000.0000000000", + # "hold":"0", + # "hold_available_for_collateral":"0", + # "transfer_hold":"0", + # "collateral_value":"500000.0", + # "max_withdraw_amount":"500000.0000000000", + # "loan":"0", + # "loan_collateral_requirement":"0.0" + # } + # + result: dict = { + 'info': response, + } + for i in range(0, len(response)): + rawBalance = response[i] + currencyId = self.safe_string(rawBalance, 'asset_name') + code = self.safe_currency_code(currencyId) + account = self.account() + account['total'] = self.safe_string(rawBalance, 'quantity') + account['used'] = self.safe_string(rawBalance, 'hold') + result[code] = account + return self.safe_balance(result) + + async def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + Transfer an amount of asset from one portfolio to another. + + https://docs.cloud.coinbase.com/intx/reference/createportfolioassettransfer + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account to transfer from + :param str toAccount: account to transfer to + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transfer structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'asset': currency['id'], + 'ammount': amount, + 'from': fromAccount, + 'to': toAccount, + } + response = await self.v1PrivatePostPortfoliosTransfer(self.extend(request, params)) + success = self.safe_bool(response, 'success') + return { + 'info': response, + 'id': None, + 'timestamp': None, + 'datetime': None, + 'currency': code, + 'amount': amount, + 'fromAccount': fromAccount, + 'toAccount': toAccount, + 'status': 'ok' if success else 'failed', + } + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: float = None, params={}): + """ + create a trade order + + https://docs.cloud.coinbase.com/intx/reference/createorder + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency, quote currency for 'market' 'buy' orders + :param float [price]: the price to fulfill the order, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.stopPrice]: alias for triggerPrice + :param float [params.triggerPrice]: price to trigger stop orders + :param float [params.stopLossPrice]: price to trigger stop-loss orders + :param bool [params.postOnly]: True or False + :param str [params.tif]: 'GTC', 'IOC', 'GTD' default is 'GTC' for limit orders and 'IOC' for market orders + :param str [params.expire_time]: The expiration time required for orders with the time in force set to GTT. Must not go beyond 30 days of the current time. Uses ISO-8601 format(e.g., 2023-03-16T23:59:53Z) + :param str [params.stp_mode]: Possible values: [NONE, AGGRESSING, BOTH] Specifies the behavior for self match handling. None disables the functionality, new cancels the newest order, and both cancels both orders. + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + typeId = type.upper() + triggerPrice = self.safe_number_n(params, ['triggerPrice', 'stopPrice', 'stop_price']) + clientOrderIdprefix = self.safe_string(self.options, 'brokerId', 'nfqkvdjp') + clientOrderId = clientOrderIdprefix + '-' + self.uuid() + clientOrderId = clientOrderId[0:17] + request: dict = { + 'client_order_id': clientOrderId, + 'side': side.upper(), + 'instrument': market['id'], + 'size': self.amount_to_precision(market['symbol'], amount), + } + if triggerPrice is not None: + if type == 'limit': + typeId = 'STOP_LIMIT' + else: + typeId = 'STOP' + request['stop_price'] = triggerPrice + request['type'] = typeId + if type == 'limit': + if price is None: + raise InvalidOrder(self.id + ' createOrder() requires a price parameter for a limit order types') + request['price'] = price + portfolio = None + portfolio, params = await self.handle_portfolio_and_params('createOrder', params) + if portfolio is not None: + request['portfolio'] = portfolio + postOnly = self.safe_bool_2(params, 'postOnly', 'post_only') + tif = self.safe_string_2(params, 'tif', 'timeInForce') + # market orders must be IOC + if typeId == 'MARKET': + if tif is not None and tif != 'IOC': + raise InvalidOrder(self.id + ' createOrder() market orders must have tif set to "IOC"') + tif = 'IOC' + else: + tif = 'GTC' if (tif is None) else tif + if postOnly is not None: + request['post_only'] = postOnly + request['tif'] = tif + params = self.omit(params, ['client_order_id', 'user', 'postOnly', 'timeInForce']) + response = await self.v1PrivatePostOrders(self.extend(request, params)) + # + # { + # "order_id":"1x96skvg-1-0", + # "client_order_id":"ccxt", + # "side":"BUY", + # "instrument_id":"114jqr89-0-0", + # "instrument_uuid":"b3469e0b-222c-4f8a-9f68-1f9e44d7e5e0", + # "symbol":"BTC-PERP", + # "portfolio_id":"1wp37qsc-1-0", + # "portfolio_uuid":"018d7f6c-b92c-7361-8b7e-2932711e5a22", + # "type":"LIMIT", + # "price":"10000", + # "size":"0.001", + # "tif":"GTC", + # "stp_mode":"BOTH", + # "event_type":"NEW", + # "order_status":"WORKING", + # "leaves_qty":"0.001", + # "exec_qty":"0", + # "avg_price":"0", + # "fee":"0" + # } + # + return self.parse_order(response, market) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # { + # "order_id":"1x96skvg-1-0", + # "client_order_id":"ccxt", + # "side":"BUY", + # "instrument_id":"114jqr89-0-0", + # "instrument_uuid":"b3469e0b-222c-4f8a-9f68-1f9e44d7e5e0", + # "symbol":"BTC-PERP", + # "portfolio_id":"1wp37qsc-1-0", + # "portfolio_uuid":"018d7f6c-b92c-7361-8b7e-2932711e5a22", + # "type":"LIMIT", + # "price":"10000", + # "size":"0.001", + # "tif":"GTC", + # "stp_mode":"BOTH", + # "event_type":"NEW", + # "order_status":"WORKING", + # "leaves_qty":"0.001", + # "exec_qty":"0", + # "avg_price":"0", + # "fee":"0" + # } + # + marketId = self.safe_string(order, 'symbol') + feeCost = self.safe_number(order, 'fee') + fee = None + if feeCost is not None: + fee = { + 'cost': feeCost, + } + datetime = self.safe_string_2(order, 'submit_time', 'event_time') + return self.safe_order({ + 'info': order, + 'id': self.safe_string(order, 'order_id'), + 'clientOrderId': self.safe_string(order, 'client_order_id'), + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + 'lastTradeTimestamp': None, + 'symbol': self.safe_symbol(marketId, market), + 'type': self.parse_order_type(self.safe_string(order, 'type')), + 'timeInForce': self.safe_string(order, 'tif'), + 'postOnly': None, + 'side': self.safe_string_lower(order, 'side'), + 'price': self.safe_string(order, 'price'), + 'triggerPrice': self.safe_string(order, 'stop_price'), + 'amount': self.safe_string(order, 'size'), + 'filled': self.safe_string(order, 'exec_qty'), + 'remaining': self.safe_string(order, 'leaves_qty'), + 'cost': None, + 'average': self.safe_string(order, 'avg_price'), + 'status': self.parse_order_status(self.safe_string(order, 'order_status')), + 'fee': fee, + 'trades': None, + }, market) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'NEW': 'open', + 'PARTIAL_FILLED': 'open', + 'FILLED': 'closed', + 'CANCELED': 'canceled', + 'REPLACED': 'canceled', + 'PENDING_CANCEL': 'open', + 'REJECTED': 'rejected', + 'PENDING_NEW': 'open', + 'EXPIRED': 'expired', + 'PENDING_REPLACE': 'open', + } + return self.safe_string(statuses, status, status) + + def parse_order_type(self, type: Str): + if type == 'UNKNOWN_ORDER_TYPE': + return None + types: dict = { + 'MARKET': 'market', + 'LIMIT': 'limit', + 'STOP': 'limit', + 'STOP_LIMIT': 'limit', + } + return self.safe_string(types, type, type) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://docs.cloud.coinbase.com/intx/reference/cancelorder + + :param str id: order id + :param str symbol: not used by coinbaseinternational cancelOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + portfolio = None + portfolio, params = await self.handle_portfolio_and_params('cancelOrder', params) + request: dict = { + 'portfolio': portfolio, + 'id': id, + } + market = None + if symbol is not None: + market = self.market(symbol) + orders = await self.v1PrivateDeleteOrdersId(self.extend(request, params)) + # + # { + # "order_id":"1x96skvg-1-0", + # "client_order_id":"ccxt", + # "side":"BUY", + # "instrument_id":"114jqr89-0-0", + # "instrument_uuid":"b3469e0b-222c-4f8a-9f68-1f9e44d7e5e0", + # "symbol":"BTC-PERP", + # "portfolio_id":"1wp37qsc-1-0", + # "portfolio_uuid":"018d7f6c-b92c-7361-8b7e-2932711e5a22", + # "type":"LIMIT", + # "price":"10000", + # "size":"0.001", + # "tif":"GTC", + # "stp_mode":"BOTH", + # "event_type":"CANCELED", + # "order_status":"DONE", + # "leaves_qty":"0.001", + # "exec_qty":"0", + # "avg_price":"0", + # "fee":"0" + # } + # + return self.parse_order(orders, market) + + async def cancel_all_orders(self, symbol: str = None, params={}): + """ + cancel all open orders + :param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + portfolio = None + portfolio, params = await self.handle_portfolio_and_params('cancelAllOrders', params) + request: dict = { + 'portfolio': portfolio, + } + market = None + if symbol: + market = self.market(symbol) + request['instrument'] = market['id'] + orders = await self.v1PrivateDeleteOrders(self.extend(request, params)) + return self.parse_orders(orders, market) + + async def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: float = None, price: float = None, params={}): + """ + edit a trade order + + https://docs.cloud.coinbase.com/intx/reference/modifyorder + + :param str id: cancel order id + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str params['clientOrderId']: client order id + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'id': id, + } + portfolio = None + portfolio, params = await self.handle_portfolio_and_params('editOrder', params) + if portfolio is not None: + request['portfolio'] = portfolio + if amount is not None: + request['size'] = self.amount_to_precision(symbol, amount) + if price is not None: + request['price'] = self.price_to_precision(symbol, price) + triggerPrice = self.safe_number_n(params, ['stopPrice', 'stop_price', 'triggerPrice']) + if triggerPrice is not None: + request['stop_price'] = triggerPrice + clientOrderId = self.safe_string_2(params, 'client_order_id', 'clientOrderId') + if clientOrderId is None: + raise BadRequest(self.id + ' editOrder() requires a clientOrderId parameter') + request['client_order_id'] = clientOrderId + order = await self.v1PrivatePutOrdersId(self.extend(request, params)) + return self.parse_order(order, market) + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://docs.cloud.coinbase.com/intx/reference/modifyorder + + :param str id: the order id + :param str symbol: unified market symbol that the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + portfolio = None + portfolio, params = await self.handle_portfolio_and_params('fetchOrder', params) + request: dict = { + 'id': id, + 'portfolio': portfolio, + } + order = await self.v1PrivateGetOrdersId(self.extend(request, params)) + # + # { + # "order_id":"1x96skvg-1-0", + # "client_order_id":"ccxt", + # "side":"BUY", + # "instrument_id":"114jqr89-0-0", + # "instrument_uuid":"b3469e0b-222c-4f8a-9f68-1f9e44d7e5e0", + # "symbol":"BTC-PERP", + # "portfolio_id":"1wp37qsc-1-0", + # "portfolio_uuid":"018d7f6c-b92c-7361-8b7e-2932711e5a22", + # "type":"LIMIT", + # "price":"10000", + # "size":"0.001", + # "tif":"GTC", + # "stp_mode":"BOTH", + # "event_type":"NEW", + # "event_time":"2024-02-14T03:25:14Z", + # "submit_time":"2024-02-14T03:25:13.999Z", + # "order_status":"WORKING", + # "leaves_qty":"0.001", + # "exec_qty":"0", + # "avg_price":"0", + # "fee":"0" + # } + # + return self.parse_order(order, market) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on all currently open orders + + https://docs.cloud.coinbase.com/intx/reference/getorders + + :param str symbol: unified market symbol of the orders + :param int [since]: timestamp in ms of the earliest order, default is None + :param int [limit]: the maximum number of open order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param int [params.offset]: offset + :param str [params.event_type]: The most recent type of event that happened to the order. Allowed values: NEW, TRADE, REPLACED + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + portfolio = None + portfolio, params = await self.handle_portfolio_and_params('fetchOpenOrders', params) + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOpenOrders', 'paginate') + maxEntriesPerRequest = None + maxEntriesPerRequest, params = self.handle_option_and_params(params, 'fetchOpenOrders', 'maxEntriesPerRequest', 100) + pageKey = 'ccxtPageKey' + if paginate: + return await self.fetch_paginated_call_incremental('fetchOpenOrders', symbol, since, limit, params, pageKey, maxEntriesPerRequest) + page = self.safe_integer(params, pageKey, 1) - 1 + request: dict = { + 'portfolio': portfolio, + 'result_offset': self.safe_integer_2(params, 'offset', 'result_offset', page * maxEntriesPerRequest), + } + market = None + if symbol: + market = self.market(symbol) + request['instrument'] = symbol + if limit is not None: + if limit > 100: + raise BadRequest(self.id + ' fetchOpenOrders() maximum limit is 100') + request['result_limit'] = limit + if since is not None: + request['ref_datetime'] = self.iso8601(since) + response = await self.v1PrivateGetOrders(self.extend(request, params)) + # + # { + # "pagination":{ + # "result_limit":25, + # "result_offset":0 + # }, + # "results":[ + # { + # "order_id":"1y4cm6b4-1-0", + # "client_order_id":"ccxtd0dd4b5d-8e5f-", + # "side":"SELL", + # "instrument_id":"114jqr89-0-0", + # "instrument_uuid":"b3469e0b-222c-4f8a-9f68-1f9e44d7e5e0", + # "symbol":"BTC-PERP", + # "portfolio_id":"1wp37qsc-1-0", + # "portfolio_uuid":"018d7f6c-b92c-7361-8b7e-2932711e5a22", + # "type":"LIMIT", + # "price":"54000", + # "size":"0.01", + # "tif":"GTC", + # "stp_mode":"BOTH", + # "event_type":"NEW", + # "event_time":"2024-02-24T16:46:37.413Z", + # "submit_time":"2024-02-24T16:46:37.412Z", + # "order_status":"WORKING", + # "leaves_qty":"0.01", + # "exec_qty":"0", + # "avg_price":"0", + # "fee":"0" + # }, + # ... + # ] + # } + # + rawOrders = self.safe_list(response, 'results', []) + return self.parse_orders(rawOrders, market, since, limit) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://docs.cloud.coinbase.com/intx/reference/getmultiportfoliofills + + :param str symbol: unified market symbol of the trades + :param int [since]: timestamp in ms of the earliest order, default is None + :param int [limit]: the maximum number of trade structures to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch trades for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchMyTrades', 'paginate') + pageKey = 'ccxtPageKey' + maxEntriesPerRequest = None + maxEntriesPerRequest, params = self.handle_option_and_params(params, 'fetchMyTrades', 'maxEntriesPerRequest', 100) + if paginate: + return await self.fetch_paginated_call_incremental('fetchMyTrades', symbol, since, limit, params, pageKey, maxEntriesPerRequest) + market = None + if symbol is not None: + market = self.market(symbol) + page = self.safe_integer(params, pageKey, 1) - 1 + request: dict = { + 'result_offset': self.safe_integer_2(params, 'offset', 'result_offset', page * maxEntriesPerRequest), + } + if limit is not None: + if limit > 100: + raise BadRequest(self.id + ' fetchMyTrades() maximum limit is 100. Consider setting paginate to True to fetch more trades.') + request['result_limit'] = limit + if since is not None: + request['time_from'] = self.iso8601(since) + until = self.safe_string_n(params, ['until']) + if until is not None: + params = self.omit(params, ['until']) + request['ref_datetime'] = self.iso8601(until) + response = await self.v1PrivateGetPortfoliosFills(self.extend(request, params)) + # + # { + # "pagination":{ + # "result_limit":25, + # "result_offset":0 + # }, + # "results":[ + # { + # "portfolio_id":"1wp37qsc-1-0", + # "portfolio_uuid":"018d7f6c-b92c-7361-8b7e-2932711e5a22", + # "portfolio_name":"CCXT Portfolio 020624-17:16", + # "fill_id":"1xbfy19y-1-184", + # "exec_id":"280841526207070392", + # "order_id":"1xbfv8yw-1-0", + # "instrument_id":"114jqr89-0-0", + # "instrument_uuid":"b3469e0b-222c-4f8a-9f68-1f9e44d7e5e0", + # "symbol":"BTC-PERP", + # "match_id":"280841526207053840", + # "fill_price":"52500", + # "fill_qty":"0.01", + # "client_id":"1x59ctku-1-1", + # "client_order_id":"ccxt3e4e2a5f-4a89-", + # "order_qty":"0.01", + # "limit_price":"52500", + # "total_filled":"0.01", + # "filled_vwap":"52500", + # "expire_time":"", + # "stop_price":"", + # "side":"BUY", + # "tif":"GTC", + # "stp_mode":"BOTH", + # "flags":"", + # "fee":"0.105", + # "fee_asset":"USDC", + # "order_status":"DONE", + # "event_time":"2024-02-15T00:43:57.631Z" + # }, + # ] + # } + # + trades = self.safe_list(response, 'results', []) + return self.parse_trades(trades, market, since, limit) + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://docs.cloud.coinbase.com/intx/reference/withdraw + https://docs.cloud.coinbase.com/intx/reference/counterpartywithdraw + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str [tag]: an optional tag for the withdrawal + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.add_network_fee_to_total]: if True, deducts network fee from the portfolio, otherwise deduct fee from the withdrawal + :param str [params.network_arn_id]: Identifies the blockchain network(e.g., networks/ethereum-mainnet/assets/313ef8a9-ae5a-5f2f-8a56-572c0e2a4d5a) + :param str [params.nonce]: a unique integer representing the withdrawal request + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.check_address(address) + await self.load_markets() + currency = self.currency(code) + portfolio = None + portfolio, params = await self.handle_portfolio_and_params('withdraw', params) + method = None + method, params = self.handle_option_and_params(params, 'withdraw', 'method', 'v1PrivatePostTransfersWithdraw') + networkId = None + networkId, params = await self.handle_network_id_and_params(code, 'withdraw', params) + request: dict = { + 'portfolio': portfolio, + 'type': 'send', + 'asset': currency['id'], + 'address': address, + 'amount': amount, + 'currency': currency['id'], + 'network_arn_id': networkId, + 'nonce': self.nonce(), + } + response = await getattr(self, method)(self.extend(request, params)) + # + # { + # "idem":"8e471d77-4208-45a8-9e5b-f3bd8a2c1fc3" + # } + # + return self.parse_transaction(response, currency) + + def safe_network(self, network): + withdrawEnabled = self.safe_bool(network, 'withdraw') + depositEnabled = self.safe_bool(network, 'deposit') + limits = self.safe_dict(network, 'limits') + withdraw = self.safe_dict(limits, 'withdraw') + withdrawMax = self.safe_number(withdraw, 'max') + deposit = self.safe_dict(limits, 'deposit') + depositMax = self.safe_number(deposit, 'max') + if withdrawEnabled is None and withdrawMax is not None: + withdrawEnabled = (withdrawMax > 0) + if depositEnabled is None and depositMax is not None: + depositEnabled = (depositMax > 0) + networkId = self.safe_string(network, 'id') + isEnabled = (withdrawEnabled and depositEnabled) + return { + 'info': network['info'], + 'id': networkId, + 'name': self.safe_string(network, 'name'), + 'network': self.safe_string(network, 'network'), + 'active': self.safe_bool(network, 'active', isEnabled), + 'deposit': depositEnabled, + 'withdraw': withdrawEnabled, + 'fee': self.safe_number(network, 'fee'), + 'precision': self.safe_number(network, 'precision'), + 'limits': { + 'withdraw': { + 'min': self.safe_number(withdraw, 'min'), + 'max': withdrawMax, + }, + 'deposit': { + 'min': self.safe_number(deposit, 'min'), + 'max': depositMax, + }, + }, + } + + def sign(self, path, api=[], method='GET', params={}, headers=None, body=None): + version = api[0] + signed = api[1] == 'private' + fullPath = '/' + version + '/' + self.implode_params(path, params) + query = self.omit(params, self.extract_params(path)) + savedPath = '/api' + fullPath + if method == 'GET' or method == 'DELETE': + if query: + fullPath += '?' + self.urlencode_with_array_repeat(query) + url = self.urls['api']['rest'] + fullPath + if signed: + self.check_required_credentials() + nonce = str(self.nonce()) + payload = '' + if method != 'GET': + if query: + body = self.json(query) + payload = body + auth = nonce + method + savedPath + payload + signature = self.hmac(self.encode(auth), self.base64_to_binary(self.secret), hashlib.sha256, 'base64') + headers = { + 'CB-ACCESS-TIMESTAMP': nonce, + 'CB-ACCESS-SIGN': signature, + 'CB-ACCESS-PASSPHRASE': self.password, + 'CB-ACCESS-KEY': self.apiKey, + } + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + # + # { + # "title":"io.javalin.http.BadRequestResponse: Order rejected(DUPLICATE_CLIENT_ORDER_ID - duplicate client order id detected)", + # "status":400 + # } + # + if response is None: + return None # fallback to default error handler + feedback = self.id + ' ' + body + errMsg = self.safe_string(response, 'title') + if errMsg is not None: + self.throw_exactly_matched_exception(self.exceptions['exact'], errMsg, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], errMsg, feedback) + raise ExchangeError(feedback) + return None diff --git a/ccxt/async_support/coincatch.py b/ccxt/async_support/coincatch.py new file mode 100644 index 0000000..e75e8d8 --- /dev/null +++ b/ccxt/async_support/coincatch.py @@ -0,0 +1,5178 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.coincatch import ImplicitAPI +import hashlib +import math +import json +from ccxt.base.types import Any, Balances, Bool, Currencies, Currency, DepositAddress, Int, LedgerEntry, Leverage, MarginMode, MarginModification, Market, Num, Order, OrderBook, OrderRequest, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, Trade, Transaction, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import NotSupported +from ccxt.base.errors import DDoSProtection +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import OnMaintenance +from ccxt.base.errors import InvalidNonce +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class coincatch(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(coincatch, self).describe(), { + 'id': 'coincatch', + 'name': 'CoinCatch', + 'countries': ['VG'], # British Virgin Islands + 'rateLimit': 50, # 20 times per second + 'version': 'v1', + 'certified': False, + 'pro': True, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': True, + 'future': False, + 'option': False, + 'addMargin': True, + 'cancelAllOrders': True, + 'cancelAllOrdersAfter': False, + 'cancelOrder': True, + 'cancelOrders': True, + 'cancelWithdraw': False, + 'closePosition': False, + 'createConvertTrade': False, + 'createDepositAddress': False, + 'createLimitBuyOrder': True, + 'createLimitSellOrder': True, + 'createMarketBuyOrder': True, + 'createMarketBuyOrderWithCost': True, + 'createMarketOrder': True, + 'createMarketOrderWithCost': False, + 'createMarketSellOrder': True, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createOrders': True, + 'createOrderWithTakeProfitAndStopLoss': True, + 'createPostOnlyOrder': True, + 'createReduceOnlyOrder': True, + 'createStopLimitOrder': True, + 'createStopLossOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'createTakeProfitOrder': True, + 'createTrailingAmountOrder': False, + 'createTrailingPercentOrder': False, + 'createTriggerOrder': True, + 'fetchAccounts': False, + 'fetchBalance': True, + 'fetchCanceledAndClosedOrders': True, + 'fetchCanceledOrders': False, + 'fetchClosedOrder': False, + 'fetchClosedOrders': False, + 'fetchConvertCurrencies': False, + 'fetchConvertQuote': False, + 'fetchConvertTrade': False, + 'fetchConvertTradeHistory': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': True, + 'fetchDeposits': True, + 'fetchDepositsWithdrawals': False, + 'fetchDepositWithdrawFees': True, + 'fetchFundingHistory': False, + 'fetchFundingRate': True, + 'fetchFundingRateHistory': True, + 'fetchFundingRates': False, + 'fetchIndexOHLCV': False, + 'fetchLedger': True, + 'fetchLeverage': True, + 'fetchLeverageTiers': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': True, + 'fetchMarkets': True, + 'fetchMarkOHLCV': True, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterestHistory': False, + 'fetchOpenOrder': False, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': False, + 'fetchOrderTrades': True, + 'fetchPosition': True, + 'fetchPositionHistory': False, + 'fetchPositionMode': True, + 'fetchPositions': True, + 'fetchPositionsForSymbol': True, + 'fetchPositionsHistory': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchStatus': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': False, + 'fetchTransactions': False, + 'fetchTransfers': False, + 'fetchWithdrawals': True, + 'reduceMargin': True, + 'sandbox': False, + 'setLeverage': True, + 'setMargin': False, + 'setMarginMode': True, + 'setPositionMode': True, + 'transfer': False, + 'withdraw': True, + }, + 'timeframes': { + '1m': '1m', + '3m': '3m', + '5m': '5m', + '15': '15m', + '30': '30m', + '1h': '1H', + '2h': '2H', + '4h': '4H', + '6h': '6H', + '12h': '12H', + '1d': '1D', + '3d': '3D', + '1w': '1W', + '1M': '1M', + }, + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/3d49065f-f05d-4573-88a2-1b5201ec6ff3', + 'api': { + 'public': 'https://api.coincatch.com', + 'private': 'https://api.coincatch.com', + }, + 'www': 'https://www.coincatch.com/', + 'doc': 'https://coincatch.github.io/github.io/en/', + 'fees': 'https://www.coincatch.com/en/rate/', + 'referral': { + 'url': 'https://partner.coincatch.cc/bg/92hy70391729607848548', + 'discount': 0.1, + }, + }, + 'api': { + 'public': { + 'get': { + 'api/spot/v1/public/time': 1, # done + 'api/spot/v1/public/currencies': 20 / 3, # done + 'api/spot/v1/market/ticker': 1, # done + 'api/spot/v1/market/tickers': 1, # done + 'api/spot/v1/market/fills': 2, # not used + 'api/spot/v1/market/fills-history': 2, # done + 'api/spot/v1/market/candles': 1, # done + 'api/spot/v1/market/history-candles': 1, # not used + 'api/spot/v1/market/depth': 1, # not used + 'api/spot/v1/market/merge-depth': 1, # done + 'api/mix/v1/market/contracts': 1, # done + 'api/mix/v1/market/merge-depth': 1, # done + 'api/mix/v1/market/depth': 1, # not used + 'api/mix/v1/market/ticker': 1, # done + 'api/mix/v1/market/tickers': 1, # done + 'api/mix/v1/market/fills': 1, # not used + 'api/mix/v1/market/fills-history': 1, # done + 'api/mix/v1/market/candles': 1, # done + 'pi/mix/v1/market/index': 1, + 'api/mix/v1/market/funding-time': 1, + 'api/mix/v1/market/history-fundRate': 1, # done + 'api/mix/v1/market/current-fundRate': 1, # done + 'api/mix/v1/market/open-interest': 1, + 'api/mix/v1/market/mark-price': 1, + 'api/mix/v1/market/symbol-leverage': 1, # done + 'api/mix/v1/market/queryPositionLever': 1, + }, + }, + 'private': { + 'get': { + 'api/spot/v1/wallet/deposit-address': 4, # done + 'pi/spot/v1/wallet/withdrawal-list': 1, # not used + 'api/spot/v1/wallet/withdrawal-list-v2': 1, # done but should be checked + 'api/spot/v1/wallet/deposit-list': 1, # done + 'api/spot/v1/account/getInfo': 1, + 'api/spot/v1/account/assets': 2, # done + 'api/spot/v1/account/transferRecords': 1, + 'api/mix/v1/account/account': 2, # done + 'api/mix/v1/account/accounts': 2, # done + 'api/mix/v1/position/singlePosition-v2': 2, # done + 'api/mix/v1/position/allPosition-v2': 4, # done + 'api/mix/v1/account/accountBill': 2, + 'api/mix/v1/account/accountBusinessBill': 4, + 'api/mix/v1/order/current': 1, # done + 'api/mix/v1/order/marginCoinCurrent': 1, # done + 'api/mix/v1/order/history': 2, # done + 'api/mix/v1/order/historyProductType': 4, # done + 'api/mix/v1/order/detail': 2, # done + 'api/mix/v1/order/fills': 2, # done + 'api/mix/v1/order/allFills': 2, # done + 'api/mix/v1/plan/currentPlan': 1, # done + 'api/mix/v1/plan/historyPlan': 2, # done + }, + 'post': { + 'api/spot/v1/wallet/transfer-v2': 4, # done + 'api/spot/v1/wallet/withdrawal-v2': 4, # done but should be checked + 'api/spot/v1/wallet/withdrawal-inner-v2': 1, + 'api/spot/v1/account/bills': 2, # done + 'api/spot/v1/trade/orders': 2, # done + 'api/spot/v1/trade/batch-orders': {'cost': 4, 'step': 10}, # done + 'api/spot/v1/trade/cancel-order': 1, # not used + 'api/spot/v1/trade/cancel-order-v2': 2, # done + 'api/spot/v1/trade/cancel-symbol-order': 2, # done + 'api/spot/v1/trade/cancel-batch-orders': 1, # not used + 'api/spot/v1/trade/cancel-batch-orders-v2': 1, # done + 'api/spot/v1/trade/orderInfo': 1, # done + 'api/spot/v1/trade/open-orders': 1, # done + 'api/spot/v1/trade/history': 1, # done + 'api/spot/v1/trade/fills': 1, # done + 'api/spot/v1/plan/placePlan': 1, # done + 'api/spot/v1/plan/modifyPlan': 1, # done + 'api/spot/v1/plan/cancelPlan': 1, # done + 'api/spot/v1/plan/currentPlan': 1, # done + 'api/spot/v1/plan/historyPlan': 1, # done + 'api/spot/v1/plan/batchCancelPlan': 2, # done + 'api/mix/v1/account/open-count': 1, + 'api/mix/v1/account/setLeverage': 4, # done + 'api/mix/v1/account/setMargin': 4, # done + 'api/mix/v1/account/setMarginMode': 4, # done + 'api/mix/v1/account/setPositionMode': 4, # done + 'api/mix/v1/order/placeOrder': 2, # done + 'api/mix/v1/order/batch-orders': {'cost': 4, 'step': 10}, # done + 'api/mix/v1/order/cancel-order': 2, # done + 'api/mix/v1/order/cancel-batch-orders': 2, # done + 'api/mix/v1/order/cancel-symbol-orders': 2, # done + 'api/mix/v1/order/cancel-all-orders': 2, # done + 'api/mix/v1/plan/placePlan': 2, # done + 'api/mix/v1/plan/modifyPlan': 2, + 'api/mix/v1/plan/modifyPlanPreset': 2, + 'api/mix/v1/plan/placeTPSL': 2, # done + 'api/mix/v1/plan/placeTrailStop': 2, # not used + 'api/mix/v1/plan/placePositionsTPSL': 2, # not used + 'api/mix/v1/plan/modifyTPSLPlan': 2, + 'api/mix/v1/plan/cancelPlan': 2, # done + 'api/mix/v1/plan/cancelSymbolPlan': 2, # done + 'api/mix/v1/plan/cancelAllPlan': 2, # done + }, + }, + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + 'password': True, + }, + 'fees': { + 'trading': { + 'spot': { + 'tierBased': False, + 'percentage': True, + 'feeSide': 'get', + 'maker': self.parse_number('0.001'), + 'taker': self.parse_number('0.001'), + }, + }, + }, + 'options': { + 'brokerId': '47cfy', + 'createMarketBuyOrderRequiresPrice': True, # for spot orders only + 'timeframes': { + 'spot': { + '1m': '1min', + '5m': '5min', + '15m': '15min', + '30m': '30min', + '1h': '1h', + '4h': '4h', + '6h': '6h', + '12h': '12h', + '1d': '1day', + '3d': '3day', + '1w': '1week', + '1M': '1M', + }, + 'swap': { + '1m': '1m', + '3m': '3m', + '5m': '5m', + '15': '15m', + '30': '30m', + '1h': '1H', + '2h': '2H', + '4h': '4H', + '6h': '6H', + '12h': '12H', + '1d': '1D', + '3d': '3D', + '1w': '1W', + '1M': '1M', + }, + }, + 'currencyIdsListForParseMarket': None, + 'broker': '', + 'networks': { + 'BTC': 'BITCOIN', + 'ERC20': 'ERC20', + 'TRC20': 'TRC20', + 'BEP20': 'BEP20', + 'ARB': 'ArbitrumOne', + 'OPTIMISM': 'Optimism', + 'LTC': 'LTC', + 'BCH': 'BCH', + 'ETC': 'ETC', + 'SOL': 'SOL', + 'NEO3': 'NEO3', + 'STX': 'stacks', + 'EGLD': 'Elrond', + 'NEAR': 'NEARProtocol', + 'ACA': 'AcalaToken', + 'KLAY': 'Klaytn', + 'FTM': 'Fantom', + 'TERRA': 'Terra', + 'WAVES': 'WAVES', + 'TAO': 'TAO', + 'SUI': 'SUI', + 'SEI': 'SEI', + 'RUNE': 'THORChain', + 'ZIL': 'ZIL', + 'SXP': 'Solar', + 'FET': 'FET', + 'AVAX': 'C-Chain', + 'XRP': 'XRP', + 'EOS': 'EOS', + 'DOGE': 'DOGECOIN', + 'CAP20': 'CAP20', + 'MATIC': 'Polygon', + 'CSPR': 'CSPR', + 'GLMR': 'Moonbeam', + 'MINA': 'MINA', + 'CFX': 'CFX', + 'STRAT': 'StratisEVM', + 'TIA': 'Celestia', + 'ChilizChain': 'ChilizChain', + 'APT': 'Aptos', + 'ONT': 'Ontology', + 'ICP': 'ICP', + 'ADA': 'Cardano', + 'FIL': 'FIL', + 'CELO': 'CELO', + 'DOT': 'DOT', + 'XLM': 'StellarLumens', + 'ATOM': 'ATOM', + 'CRO': 'CronosChain', + }, + 'networksById': { + 'TRC20': 'TRC20', + 'TRX(TRC20)': 'TRC20', + 'ArbitrumOne': 'ARB', # todo check + 'THORChain': 'RUNE', # todo check + 'Solar': 'SXP', # todo check + 'C-Chain': 'AVAX', # todo check + 'CAP20': 'CAP20', # todo check + 'CFXeSpace': 'CFX', # todo check + 'CFX': 'CFX', + 'StratisEVM': 'STRAT', # todo check + 'ChilizChain': 'ChilizChain', # todo check + 'StellarLumens': 'XLM', # todo check + 'CronosChain': 'CRO', # todo check + 'Optimism': 'Optimism', + }, + }, + 'features': { + 'default': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerPriceType': { + 'last': True, + 'mark': True, + 'index': False, + }, + 'triggerDirection': False, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': { + 'max': 50, + }, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 500, + 'daysBack': 100000, # todo implement + 'untilDays': 100000, # todo implement + 'symbolRequired': True, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 100, + 'trigger': True, + 'trailing': False, + 'marketType': True, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': None, # todo implement + 'fetchOHLCV': { + 'limit': 1000, + }, + }, + 'spot': { + 'extends': 'default', + }, + 'forDerivatives': { + 'extends': 'default', + 'createOrder': { + # todo check + 'attachedStopLossTakeProfit': { + 'triggerPriceType': None, + 'price': False, + }, + }, + 'fetchMyTrades': { + 'limit': 100, + }, + }, + 'swap': { + 'linear': { + 'extends': 'forDerivatives', + }, + 'inverse': { + 'extends': 'forDerivatives', + }, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'commonCurrencies': {}, + 'exceptions': { + 'exact': { + '22001': OrderNotFound, # No order to cancel + '429': DDoSProtection, # Request is too frequent + '40001': AuthenticationError, # The request header "ACCESS_KEY" cannot be empty + '40002': AuthenticationError, # The request header "ACCESS_SIGN" cannot be empty + '40003': AuthenticationError, # The request header "ACCESS_TIMESTAMP" cannot be empty + '40005': InvalidNonce, # Invalid ACCESS_TIMESTAMP + '40006': AuthenticationError, # Invalid ACCESS_KEY + '40007': BadRequest, # Invalid Content_Type,please use“application/json”format + '40008': InvalidNonce, # Request timestamp expired + '40009': AuthenticationError, # api verification failed + '40011': AuthenticationError, # The request header "ACCESS_PASSPHRASE" cannot be empty + '40012': AuthenticationError, # apikey/passphrase is incorrect + '40013': ExchangeError, # User has been frozen + '40014': PermissionDenied, # Incorrect permissions + '40015': ExchangeError, # System error + '40016': PermissionDenied, # The user must bind a mobile phone or Google authenticator + '40017': ExchangeError, # Parameter verification failed + '40018': PermissionDenied, # Illegal IP request + '40019': BadRequest, # Parameter {0} cannot be empty + '40020': BadRequest, # Parameter orderIds or clientOids error + '40034': BadRequest, # Parameter {0} does not exist + '400172': BadRequest, # symbol cannot be empty + '40912': BadRequest, # Batch processing orders can only process up to 50 + '40913': BadRequest, # orderId or clientOrderId must be passed one + '40102': BadRequest, # The contract configuration does not exist, please check the parameters + '40200': OnMaintenance, # Server upgrade, please try again later + '40305': BadRequest, # client_oid length is not greater than 40, and cannot be Martian characters + '40409': ExchangeError, # wrong format + '40704': ExchangeError, # Only check the data of the last three months + '40724': BadRequest, # Parameter is empty + '40725': ExchangeError, # spot service return an error + '40762': InsufficientFunds, # The order amount exceeds the balance + '40774': BadRequest, # The order type for unilateral position must also be the unilateral position type. + '40808': BadRequest, # Parameter verification exception {0} + '43001': OrderNotFound, # The order does not exist + '43002': InvalidOrder, # Pending order failed + '43004': OrderNotFound, # There is no order to cancel + '43005': RateLimitExceeded, # Exceeded the maximum order limit of transaction volume + '43006': BadRequest, # The order quantity is less than the minimum transaction quantity + '43007': BadRequest, # The order quantity is greater than the maximum transaction quantity + '43008': BadRequest, # The current order price cannot be less than {0} + '43009': BadRequest, # The current commission price exceeds the limit {0} + '43010': BadRequest, # The transaction amount cannot be less than {0} + '43011': BadRequest, # The current order price cannot be less than {0} + '43012': InsufficientFunds, # {"code":"43012","msg":"Insufficient balance","requestTime":1729327822139,"data":null} + '43117': InsufficientFunds, # Exceeds the maximum amount that can be transferred + '43118': BadRequest, # clientOrderId duplicate + '43122': BadRequest, # The purchase limit of self currency is {0}, and there is still {1} left + '45006': InsufficientFunds, # Insufficient position + '45110': BadRequest, # less than the minimum amount {0} {1} + # {"code":"40913","msg":"orderId or clientOrderId must be passed one","requestTime":1726160988275,"data":null} + }, + 'broad': {}, + }, + 'precisionMode': TICK_SIZE, + }) + + def calculate_rate_limiter_cost(self, api, method, path, params, config={}): + step = self.safe_integer(config, 'step') + cost = self.safe_integer(config, 'cost', 1) + orders = self.safe_list_2(params, 'orderList', 'orderDataList', []) + ordersLength = len(orders) + if (step is not None) and (ordersLength > step): + numberOfSteps = int(math.ceil(ordersLength / step)) + return cost * numberOfSteps + else: + return cost + + async def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://coincatch.github.io/github.io/en/spot/#get-server-time + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = await self.publicGetApiSpotV1PublicTime(params) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1725046822028, + # "data": "1725046822028" + # } + # + return self.safe_integer(response, 'data') + + async def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://coincatch.github.io/github.io/en/spot/#get-coin-list + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = await self.publicGetApiSpotV1PublicCurrencies(params) + data = self.safe_list(response, 'data', []) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1725102364202, + # "data": [ + # { + # "coinId": "1", + # "coinName": "BTC", + # "transfer": "true", + # "chains": [ + # { + # "chainId": "10", + # "chain": "BITCOIN", + # "needTag": "false", + # "withdrawable": "true", + # "rechargeable": "true", + # "withdrawFee": "0.0005", + # "extraWithDrawFee": "0", + # "depositConfirm": "1", + # "withdrawConfirm": "1", + # "minDepositAmount": "0.00001", + # "minWithdrawAmount": "0.001", + # "browserUrl": "https://blockchair.com/bitcoin/transaction/" + # } + # ] + # }, + # ... + # ] + # } + # + result: dict = {} + currenciesIds = [] + for i in range(0, len(data)): + currecy = data[i] + currencyId = self.safe_string(currecy, 'coinName') + currenciesIds.append(currencyId) + code = self.safe_currency_code(currencyId) + networks = self.safe_list(currecy, 'chains') + parsedNetworks: dict = {} + for j in range(0, len(networks)): + network = networks[j] + networkId = self.safe_string(network, 'chain') + networkCode = self.network_id_to_code(networkId) + parsedNetworks[networkCode] = { + 'id': networkId, + 'network': networkCode, + 'limits': { + 'deposit': { + 'min': self.safe_number(network, 'minDepositAmount'), + 'max': None, + }, + 'withdraw': { + 'min': self.safe_number(network, 'minWithdrawAmount'), + 'max': None, + }, + }, + 'active': None, + 'deposit': self.safe_string(network, 'rechargeable') == 'true', + 'withdraw': self.safe_string(network, 'withdrawable') == 'true', + 'fee': self.safe_number(network, 'withdrawFee'), + 'precision': None, + 'info': network, + } + result[code] = self.safe_currency_structure({ + 'id': currencyId, + 'numericId': self.safe_integer(currecy, 'coinId'), + 'code': code, + 'precision': None, + 'type': None, + 'name': None, + 'active': None, + 'deposit': None, + 'withdraw': None, + 'fee': None, + 'limits': { + 'deposit': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + }, + 'networks': parsedNetworks, + 'info': currecy, + }) + if self.safe_list(self.options, 'currencyIdsListForParseMarket') is None: + self.options['currencyIdsListForParseMarket'] = currenciesIds + return result + + async def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}): + """ + fetch deposit and withdraw fees + + https://coincatch.github.io/github.io/en/spot/#get-coin-list + + :param str[] [codes]: list of unified currency codes + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `fee structures ` + """ + await self.load_markets() + response = await self.publicGetApiSpotV1PublicCurrencies(params) + data = self.safe_list(response, 'data', []) + return self.parse_deposit_withdraw_fees(data, codes, 'coinName') + + def parse_deposit_withdraw_fee(self, fee, currency: Currency = None): + # + # { + # "coinId":"1", + # "coinName":"BTC", + # "transfer":"true", + # "chains":[ + # { + # "chain":null, + # "needTag":"false", + # "withdrawable":"true", + # "rechargeAble":"true", + # "withdrawFee":"0.005", + # "depositConfirm":"1", + # "withdrawConfirm":"1", + # "minDepositAmount":"0.001", + # "minWithdrawAmount":"0.001", + # "browserUrl":"https://blockchair.com/bitcoin/testnet/transaction/" + # } + # ] + # } + # + chains = self.safe_list(fee, 'chains', []) + chainsLength = len(chains) + result: dict = { + 'info': fee, + 'withdraw': { + 'fee': None, + 'percentage': None, + }, + 'deposit': { + 'fee': None, + 'percentage': None, + }, + 'networks': {}, + } + for i in range(0, chainsLength): + chain = chains[i] + networkId = self.safe_string(chain, 'chain') + currencyCode = self.safe_string(currency, 'code') + networkCode = self.network_id_to_code(networkId, currencyCode) + result['networks'][networkCode] = { + 'deposit': {'fee': None, 'percentage': None}, + 'withdraw': {'fee': self.safe_number(chain, 'withdrawFee'), 'percentage': False}, + } + if chainsLength == 1: + result['withdraw']['fee'] = self.safe_number(chain, 'withdrawFee') + result['withdraw']['percentage'] = False + return result + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for the exchange + + https://coincatch.github.io/github.io/en/spot/#get-all-tickers + https://coincatch.github.io/github.io/en/mix/#get-all-symbols + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = await self.publicGetApiSpotV1MarketTickers(params) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1725114040155, + # "data": [ + # { + # "symbol": "BTCUSDT", + # "high24h": "59461.34", + # "low24h": "57723.23", + # "close": "59056.02", + # "quoteVol": "18240112.23368", + # "baseVol": "309.05564", + # "usdtVol": "18240112.2336744", + # "ts": "1725114038951", + # "buyOne": "59055.85", + # "sellOne": "59057.45", + # "bidSz": "0.0139", + # "askSz": "0.0139", + # "openUtc0": "59126.71", + # "changeUtc": "-0.0012", + # "change": "0.01662" + # }, + # ... + # ] + # } + # + if self.safe_list(self.options, 'currencyIdsListForParseMarket') is None: + await self.fetch_currencies() + spotMarkets = self.safe_list(response, 'data', []) + request: dict = {} + productType: Str = None + productType, params = self.handle_option_and_params(params, 'fetchMarkets', 'productType', productType) + swapMarkets = [] + request['productType'] = 'umcbl' + response = await self.publicGetApiMixV1MarketContracts(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1725297439225, + # "data": [ + # { + # "symbol": "BTCUSDT_UMCBL", + # "makerFeeRate": "0.0002", + # "takerFeeRate": "0.0006", + # "feeRateUpRatio": "0.005", + # "openCostUpRatio": "0.01", + # "quoteCoin": "USDT", + # "baseCoin": "BTC", + # "buyLimitPriceRatio": "0.01", + # "sellLimitPriceRatio": "0.01", + # "supportMarginCoins": ["USDT"], + # "minTradeNum": "0.001", + # "priceEndStep": "1", + # "volumePlace": "3", + # "pricePlace": "1", + # "sizeMultiplier": "0.001", + # "symbolType": "perpetual", + # "symbolStatus": "normal", + # "offTime": "-1", + # "limitOpenTime": "-1", + # "maintainTime": "", + # "symbolName": "BTCUSDT", + # "minTradeUSDT": null, + # "maxPositionNum": null, + # "maxOrderNum": null + # } + # ] + # } + # + swapUMCBL = self.safe_list(response, 'data', []) + request['productType'] = 'dmcbl' + response = await self.publicGetApiMixV1MarketContracts(self.extend(request, params)) + # + # { + # "code":"00000", + # "msg":"success", + # "requestTime":1725297439646, + # "data":[ + # { + # "symbol":"BTCUSD_DMCBL", + # "makerFeeRate":"0.0002", + # "takerFeeRate":"0.0006", + # "feeRateUpRatio":"0.005", + # "openCostUpRatio":"0.01", + # "quoteCoin":"USD", + # "baseCoin":"BTC", + # "buyLimitPriceRatio":"0.01", + # "sellLimitPriceRatio":"0.01", + # "supportMarginCoins":[ + # "BTC", + # "ETH" + # ], + # "minTradeNum":"0.001", + # "priceEndStep":"1", + # "volumePlace":"3", + # "pricePlace":"1", + # "sizeMultiplier":"0.001", + # "symbolType":"perpetual", + # "symbolStatus":"normal", + # "offTime":"-1", + # "limitOpenTime":"-1", + # "maintainTime":"", + # "symbolName":"BTCUSD", + # "minTradeUSDT":null, + # "maxPositionNum":null, + # "maxOrderNum":null + # } + # ] + # } + swapDMCBL = self.safe_list(response, 'data', []) + swapDMCBLExtended = [] + for i in range(0, len(swapDMCBL)): + market = swapDMCBL[i] + supportMarginCoins = self.safe_list(market, 'supportMarginCoins', []) + for j in range(0, len(supportMarginCoins)): + settle = supportMarginCoins[j] + obj = { + 'supportMarginCoins': [settle], + } + swapDMCBLExtended.append(self.extend(market, obj)) + swapMarkets = self.array_concat(swapUMCBL, swapDMCBLExtended) + markets = self.array_concat(spotMarkets, swapMarkets) + return self.parse_markets(markets) + + def parse_market(self, market: dict) -> Market: + # + # spot + # { + # "symbol": "BTCUSDT", + # "high24h": "59461.34", + # "low24h": "57723.23", + # "close": "59056.02", + # "quoteVol": "18240112.23368", + # "baseVol": "309.05564", + # "usdtVol": "18240112.2336744", + # "ts": "1725114038951", + # "buyOne": "59055.85", + # "sellOne": "59057.45", + # "bidSz": "0.0139", + # "askSz": "0.0139", + # "openUtc0": "59126.71", + # "changeUtc": "-0.0012", + # "change": "0.01662" + # }, + # + # swap + # { + # "symbol": "BTCUSDT_UMCBL", + # "makerFeeRate": "0.0002", + # "takerFeeRate": "0.0006", + # "feeRateUpRatio": "0.005", + # "openCostUpRatio": "0.01", + # "quoteCoin": "USDT", + # "baseCoin": "BTC", + # "buyLimitPriceRatio": "0.01", + # "sellLimitPriceRatio": "0.01", + # "supportMarginCoins": ["USDT"], + # "minTradeNum": "0.001", + # "priceEndStep": "1", + # "volumePlace": "3", + # "pricePlace": "1", + # "sizeMultiplier": "0.001", + # "symbolType": "perpetual", + # "symbolStatus": "normal", + # "offTime": "-1", + # "limitOpenTime": "-1", + # "maintainTime": "", + # "symbolName": "BTCUSDT", + # "minTradeUSDT": null, + # "maxPositionNum": null, + # "maxOrderNum": null + # } + # + marketId = self.safe_string(market, 'symbol') + tradingFees = self.safe_dict(self.fees, 'trading') + fees = self.safe_dict(tradingFees, 'spot') + baseId = self.safe_string(market, 'baseCoin') + quoteId = self.safe_string(market, 'quoteCoin') + settleId: Str = None + suffix = '' + settle: Str = None + type = 'spot' + isLinear: Bool = None + isInverse: Bool = None + subType: Str = None + isSpot = baseId is None # for now spot markets have no properties baseCoin and quoteCoin + if isSpot: + parsedMarketId = self.parse_spot_market_id(marketId) + baseId = self.safe_string(parsedMarketId, 'baseId') + quoteId = self.safe_string(parsedMarketId, 'quoteId') + marketId += '_SPBL' # spot markets should have current suffix + else: + type = 'swap' + fees['taker'] = self.safe_number(market, 'takerFeeRate') + fees['maker'] = self.safe_number(market, 'makerFeeRate') + supportMarginCoins = self.safe_list(market, 'supportMarginCoins', []) + settleId = self.safe_string(supportMarginCoins, 0) + settle = self.safe_currency_code(settleId) + suffix = ':' + settle + isLinear = quoteId == settleId # todo check + isInverse = baseId == settleId # todo check + if isLinear: + subType = 'linear' + elif isInverse: + subType = 'inverse' + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + symbol = base + '/' + quote + suffix + symbolStatus = self.safe_string(market, 'symbolStatus') + active = (symbolStatus == 'normal') if symbolStatus else None + volumePlace = self.safe_string(market, 'volumePlace') + amountPrecisionString = self.parse_precision(volumePlace) + pricePlace = self.safe_string(market, 'pricePlace') + priceEndStep = self.safe_string(market, 'priceEndStep') + pricePrecisionString = Precise.string_mul(self.parse_precision(pricePlace), priceEndStep) + return self.safe_market_structure({ + 'id': marketId, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'baseId': baseId, + 'quoteId': quoteId, + 'active': active, + 'type': type, + 'subType': subType, + 'spot': isSpot, + 'margin': False if isSpot else None, + 'swap': not isSpot, + 'future': False, + 'option': False, + 'contract': not isSpot, + 'settle': settle, + 'settleId': settleId, + 'contractSize': self.safe_number(market, 'sizeMultiplier'), + 'linear': isLinear, + 'inverse': isInverse, + 'taker': self.safe_number(fees, 'taker'), + 'maker': self.safe_number(fees, 'maker'), + 'percentage': self.safe_bool(fees, 'percentage'), + 'tierBased': self.safe_bool(fees, 'tierBased'), + 'feeSide': self.safe_string(fees, 'feeSide'), + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(amountPrecisionString), + 'price': self.parse_number(pricePrecisionString), + }, + 'limits': { + 'amount': { + 'min': self.safe_number(market, 'minTradeNum'), + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'leverage': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + 'info': market, + }) + + def parse_spot_market_id(self, marketId): + baseId = None + quoteId = None + currencyIds = self.safe_list(self.options, 'currencyIdsListForParseMarket', []) + for i in range(0, len(currencyIds)): + currencyId = currencyIds[i] + entryIndex = marketId.find(currencyId) + if entryIndex > -1: + restId = marketId.replace(currencyId, '') + if entryIndex == 0: + baseId = currencyId + quoteId = restId + else: + baseId = restId + quoteId = currencyId + break + result: dict = { + 'baseId': baseId, + 'quoteId': quoteId, + } + return result + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://coincatch.github.io/github.io/en/spot/#get-single-ticker + https://coincatch.github.io/github.io/en/mix/#get-single-symbol-ticker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = None + if market['spot']: + response = await self.publicGetApiSpotV1MarketTicker(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1725132487751, + # "data": { + # "symbol": "ETHUSDT", + # "high24h": "2533.76", + # "low24h": "2492.72", + # "close": "2499.76", + # "quoteVol": "21457850.7442", + # "baseVol": "8517.1869", + # "usdtVol": "21457850.744163", + # "ts": "1725132487476", + # "buyOne": "2499.75", + # "sellOne": "2499.76", + # "bidSz": "0.5311", + # "askSz": "4.5806", + # "openUtc0": "2525.69", + # "changeUtc": "-0.01027", + # "change": "-0.00772" + # } + # } + # + elif market['swap']: + response = await self.publicGetApiMixV1MarketTicker(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1725316687174, + # "data": { + # "symbol": "ETHUSDT_UMCBL", + # "last": "2540.6", + # "bestAsk": "2540.71", + # "bestBid": "2540.38", + # "bidSz": "12.1", + # "askSz": "20", + # "high24h": "2563.91", + # "low24h": "2398.3", + # "timestamp": "1725316687177", + # "priceChangePercent": "0.01134", + # "baseVolume": "706928.96", + # "quoteVolume": "1756401737.8766", + # "usdtVolume": "1756401737.8766", + # "openUtc": "2424.49", + # "chgUtc": "0.04789", + # "indexPrice": "2541.977142", + # "fundingRate": "0.00006", + # "holdingAmount": "144688.49", + # "deliveryStartTime": null, + # "deliveryTime": null, + # "deliveryStatus": "normal" + # } + # } + # + else: + raise NotSupported(self.id + ' ' + 'fetchTicker() is not supported for ' + market['type'] + ' type of markets') + data = self.safe_dict(response, 'data', {}) + return self.parse_ticker(data, market) + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://coincatch.github.io/github.io/en/spot/#get-all-tickers + https://coincatch.github.io/github.io/en/mix/#get-all-symbol-ticker + + :param str[] [symbols]: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: 'spot' or 'swap'(default 'spot') + :param str [params.productType]: 'umcbl' or 'dmcbl'(default 'umcbl') - USDT perpetual contract or Universal margin perpetual contract + :returns dict: a dictionary of `ticker structures ` + """ + methodName = 'fetchTickers' + await self.load_markets() + symbols = self.market_symbols(symbols, None, True, True) + market = self.get_market_from_symbols(symbols) + marketType = 'spot' + marketType, params = self.handle_market_type_and_params(methodName, market, params, marketType) + response = None + if marketType == 'spot': + response = await self.publicGetApiSpotV1MarketTickers(params) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1725114040155, + # "data": [ + # { + # "symbol": "BTCUSDT", + # "high24h": "59461.34", + # "low24h": "57723.23", + # "close": "59056.02", + # "quoteVol": "18240112.23368", + # "baseVol": "309.05564", + # "usdtVol": "18240112.2336744", + # "ts": "1725114038951", + # "buyOne": "59055.85", + # "sellOne": "59057.45", + # "bidSz": "0.0139", + # "askSz": "0.0139", + # "openUtc0": "59126.71", + # "changeUtc": "-0.0012", + # "change": "0.01662" + # }, + # ... + # ] + # } + # + elif marketType == 'swap': + productType = 'umcbl' + productType, params = self.handle_option_and_params(params, methodName, 'productType', productType) + request: dict = { + 'productType': productType, + } + response = await self.publicGetApiMixV1MarketTickers(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1725320291340, + # "data": [ + # { + # "symbol": "BTCUSDT_UMCBL", + # "last": "59110.5", + # "bestAsk": "59113.2", + # "bestBid": "59109.5", + # "bidSz": "1.932", + # "askSz": "0.458", + # "high24h": "59393.5", + # "low24h": "57088.5", + # "timestamp": "1725320291347", + # "priceChangePercent": "0.01046", + # "baseVolume": "59667.001", + # "quoteVolume": "3472522256.9927", + # "usdtVolume": "3472522256.9927", + # "openUtc": "57263", + # "chgUtc": "0.03231", + # "indexPrice": "59151.25442", + # "fundingRate": "0.00007", + # "holdingAmount": "25995.377", + # "deliveryStartTime": null, + # "deliveryTime": null, + # "deliveryStatus": "normal"} + # }, + # ... + # ] + # } + # + else: + raise NotSupported(self.id + ' ' + methodName + '() is not supported for ' + marketType + ' type of markets') + data = self.safe_list(response, 'data', []) + return self.parse_tickers(data, symbols) + + def parse_ticker(self, ticker, market: Market = None) -> Ticker: + # + # spot + # { + # "symbol": "BTCUSDT", + # "high24h": "59461.34", + # "low24h": "57723.23", + # "close": "59056.02", + # "quoteVol": "18240112.23368", + # "baseVol": "309.05564", + # "usdtVol": "18240112.2336744", + # "ts": "1725114038951", + # "buyOne": "59055.85", + # "sellOne": "59057.45", + # "bidSz": "0.0139", + # "askSz": "0.0139", + # "openUtc0": "59126.71", + # "changeUtc": "-0.0012", + # "change": "0.01662" + # } + # + # swap + # { + # "symbol": "ETHUSDT_UMCBL", + # "last": "2540.6", + # "bestAsk": "2540.71", + # "bestBid": "2540.38", + # "bidSz": "12.1", + # "askSz": "20", + # "high24h": "2563.91", + # "low24h": "2398.3", + # "timestamp": "1725316687177", + # "priceChangePercent": "0.01134", + # "baseVolume": "706928.96", + # "quoteVolume": "1756401737.8766", + # "usdtVolume": "1756401737.8766", + # "openUtc": "2424.49", + # "chgUtc": "0.04789", + # "indexPrice": "2541.977142", + # "fundingRate": "0.00006", + # "holdingAmount": "144688.49", + # "deliveryStartTime": null, + # "deliveryTime": null, + # "deliveryStatus": "normal" + # } + # + timestamp = self.safe_integer_2(ticker, 'ts', 'timestamp') + marketId = self.safe_string(ticker, 'symbol', '') + if marketId.find('_') < 0: + marketId += '_SPBL' # spot markets from tickers endpoints have no suffix specific for market id + market = self.safe_market_custom(marketId, market) + last = self.safe_string_2(ticker, 'close', 'last') + return self.safe_ticker({ + 'symbol': market['symbol'], + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high24h'), + 'low': self.safe_string(ticker, 'low24h'), + 'bid': self.safe_string_2(ticker, 'buyOne', 'bestBid'), + 'bidVolume': self.safe_string(ticker, 'bidSz'), + 'ask': self.safe_string_2(ticker, 'sellOne', 'bestAsk'), + 'askVolume': self.safe_string(ticker, 'askSz'), + 'vwap': None, + 'open': self.safe_string_2(ticker, 'openUtc0', 'openUtc'), + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': Precise.string_mul(self.safe_string_2(ticker, 'changeUtc', 'chgUtc'), '100'), + 'average': None, + 'baseVolume': self.safe_string_2(ticker, 'baseVol', 'baseVolume'), + 'quoteVolume': self.safe_string_2(ticker, 'quoteVol', 'quoteVolume'), + 'indexPrice': self.safe_string(ticker, 'indexPrice'), + 'markPrice': None, + 'info': ticker, + }, market) + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://coincatch.github.io/github.io/en/spot/#get-merged-depth-data + https://coincatch.github.io/github.io/en/mix/#get-merged-depth-data + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return(maximum and default value is 100) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.precision]: 'scale0'(default), 'scale1', 'scale2' or 'scale3' - price accuracy, according to the selected accuracy step size to return the cumulative depth + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + methodName = 'fetchOrderBook' + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['limit'] = limit + precision: Str = None + precision, params = self.handle_option_and_params(params, methodName, 'precision') + if precision is not None: + request['precision'] = precision + response = None + if market['spot']: + response = await self.publicGetApiSpotV1MarketMergeDepth(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1725137170814, + # "data": { + # "asks": [[2507.07, 0.4248]], + # "bids": [[2507.05, 0.1198]], + # "ts": "1725137170850", + # "scale": "0.01", + # "precision": "scale0", + # "isMaxPrecision": "NO" + # } + # } + # + elif market['swap']: + response = await self.publicGetApiMixV1MarketMergeDepth(self.extend(request, params)) + else: + raise NotSupported(self.id + ' ' + methodName + '() is not supported for ' + market['type'] + ' type of markets') + data = self.safe_dict(response, 'data', {}) + timestamp = self.safe_integer(data, 'ts') + return self.parse_order_book(data, symbol, timestamp, 'bids', 'asks') + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + + https://coincatch.github.io/github.io/en/spot/#get-candle-data + https://coincatch.github.io/github.io/en/mix/#get-candle-data + + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch(default 100) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest candle to fetch + :param str [params.price]: "mark" for mark price candles + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + methodName = 'fetchOHLCV' + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + until: Int = None + until, params = self.handle_option_and_params(params, methodName, 'until') + marketType = market['type'] + timeframes = self.options['timeframes'][marketType] + encodedTimeframe = self.safe_string(timeframes, timeframe, timeframe) + maxLimit = 1000 + requestedLimit = limit + if (since is not None) or (until is not None): + requestedLimit = maxLimit # the exchange returns only last limit candles, so we have to fetch max limit if since or until are provided + if requestedLimit is not None: + request['limit'] = requestedLimit + response = None + if market['spot']: + request['period'] = encodedTimeframe + if since is not None: + request['after'] = since + if until is not None: + request['before'] = until + response = await self.publicGetApiSpotV1MarketCandles(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1725142465742, + # "data": [ + # { + # "open": "2518.6", + # "high": "2519.19", + # "low": "2518.42", + # "close": "2518.86", + # "quoteVol": "17193.239401", + # "baseVol": "6.8259", + # "usdtVol": "17193.239401", + # "ts": "1725142200000" + # }, + # ... + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_ohlcvs(data, market, timeframe, since, limit) + elif market['swap']: + request['granularity'] = encodedTimeframe + if until is None: + until = self.milliseconds() + if since is None: + duration = self.parse_timeframe(timeframe) + since = until - (duration * maxLimit * 1000) + request['startTime'] = since # since and until are mandatory for swap + request['endTime'] = until + priceType: Str = None + priceType, params = self.handle_option_and_params(params, methodName, 'price') + if priceType == 'mark': + request['kLineType'] = 'market mark index' + response = await self.publicGetApiMixV1MarketCandles(self.extend(request, params)) + # + # [ + # [ + # "1725379020000", + # "57614", + # "57636", + # "57614", + # "57633", + # "28.725", + # "1655346.493" + # ], + # ... + # ] + # + return self.parse_ohlcvs(response, market, timeframe, since, limit) + else: + raise NotSupported(self.id + ' ' + methodName + '() is not supported for ' + market['type'] + ' type of markets') + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + return [ + self.safe_integer_2(ohlcv, 'ts', 0), + self.safe_number_2(ohlcv, 'open', 1), + self.safe_number_2(ohlcv, 'high', 2), + self.safe_number_2(ohlcv, 'low', 3), + self.safe_number_2(ohlcv, 'close', 4), + self.safe_number_2(ohlcv, 'baseVol', 5), + ] + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://coincatch.github.io/github.io/en/spot/#get-recent-trades + https://coincatch.github.io/github.io/en/mix/#get-fills + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest entry to fetch + :returns Trade[]: a list of `trade structures ` + """ + methodName = 'fetchTrades' + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + until: Int = None + until, params = self.handle_option_and_params(params, methodName, 'until') + maxLimit = 1000 + requestLimit = limit + if (since is not None) or (until is not None): + requestLimit = maxLimit + if since is not None: + request['startTime'] = since + if until is not None: + request['endTime'] = until + if requestLimit is not None: + request['limit'] = requestLimit + response = None + if market['spot']: + response = await self.publicGetApiSpotV1MarketFillsHistory(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1725198410976, + # "data": [ + # { + # "symbol": "ETHUSDT_SPBL", + # "tradeId": "1214135619719827457", + # "side": "buy", + # "fillPrice": "2458.62", + # "fillQuantity": "0.4756", + # "fillTime": "1725198409967" + # } + # ] + # } + # + elif market['swap']: + response = await self.publicGetApiMixV1MarketFillsHistory(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1725389251975, + # "data": [ + # { + # "tradeId": "1214936067582234782", + # "price": "57998.5", + # "size": "1.918", + # "side": "Sell", + # "timestamp": "1725389251000", + # "symbol": "BTCUSDT_UMCBL" + # }, + # ... + # ] + # } + # + else: + raise NotSupported(self.id + ' ' + methodName + '() is not supported for ' + market['type'] + ' type of markets') + data = self.safe_list(response, 'data', []) + return self.parse_trades(data, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades spot + # { + # "symbol": "ETHUSDT_SPBL", + # "tradeId": "1214135619719827457", + # "side": "Buy", + # "fillPrice": "2458.62", + # "fillQuantity": "0.4756", + # "fillTime": "1725198409967" + # } + # + # fetchTrades swap + # { + # "tradeId": "1214936067582234782", + # "price": "57998.5", + # "size": "1.918", + # "side": "Sell", + # "timestamp": "1725389251000", + # "symbol": "BTCUSDT_UMCBL" + # } + # + # fetchMyTrades spot + # { + # "accountId": "1002820815393", + # "symbol": "ETHUSDT_SPBL", + # "orderId": "1217143186968068096", + # "fillId": "1217143193356505089", + # "orderType": "market", + # "side": "buy", + # "fillPrice": "2340.55", + # "fillQuantity": "0.0042", + # "fillTotalAmount": "9.83031", + # "feeCcy": "ETH", + # "fees": "-0.0000042", + # "takerMakerFlag": "taker", + # "cTime": "1725915471400" + # } + # + # fetchMyTrades swap + # { + # "tradeId": "1225467075440189441", + # "symbol": "ETHUSD_DMCBL", + # "orderId": "1225467075288719360", + # "price": "2362.03", + # "sizeQty": "0.1", + # "fee": "-0.00005996", + # "side": "burst_close_long", + # "fillAmount": "236.203", + # "profit": "-0.0083359", + # "enterPointSource": "SYS", + # "tradeSide": "burst_close_long", + # "holdMode": "double_hold", + # "takerMakerFlag": "taker", + # "cTime": "1727900039539" + # } + # + marketId = self.safe_string(trade, 'symbol') + market = self.safe_market_custom(marketId, market) + timestamp = self.safe_integer_n(trade, ['fillTime', 'timestamp', 'cTime']) + fees = self.safe_string_2(trade, 'fees', 'fee') + feeCost: Str = None + if fees is not None: + feeCost = Precise.string_abs(fees) + feeCurrency = self.safe_string(trade, 'feeCcy') + if (feeCurrency is None) and (market['settle'] is not None): + feeCurrency = market['settle'] + side = self.safe_string_lower_2(trade, 'tradeSide', 'side') + return self.safe_trade({ + 'id': self.safe_string_2(trade, 'tradeId', 'fillId'), + 'order': self.safe_string(trade, 'orderId'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'type': self.safe_string(trade, 'orderType'), + 'side': self.parse_order_side(side), + 'takerOrMaker': self.safe_string(trade, 'takerMakerFlag'), + 'price': self.safe_string_2(trade, 'fillPrice', 'price'), + 'amount': self.safe_string_n(trade, ['fillQuantity', 'size', 'sizeQty']), + 'cost': self.safe_string_2(trade, 'fillTotalAmount', 'fillAmount'), + 'fee': { + 'cost': feeCost, + 'currency': feeCurrency, + }, + 'info': trade, + }, market) + + async def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate + + https://coincatch.github.io/github.io/en/mix/#get-current-funding-rate + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + await self.load_markets() + market = self.market(symbol) + marketId = market['id'] + parts = marketId.split('_') + request: dict = { + 'symbol': marketId, + 'productType': self.safe_string(parts, 1), + } + response = await self.publicGetApiMixV1MarketCurrentFundRate(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1725402130395, + # "data": { + # "symbol": "BTCUSDT_UMCBL", + # "fundingRate": "0.000043" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_funding_rate(data, market) + + def parse_funding_rate(self, contract, market: Market = None): + marketId = self.safe_string(contract, 'symbol') + market = self.safe_market_custom(marketId, market) + fundingRate = self.safe_number(contract, 'fundingRate') + return { + 'info': contract, + 'symbol': market['symbol'], + 'markPrice': None, + 'indexPrice': None, + 'interestRate': None, + 'estimatedSettlePrice': None, + 'timestamp': None, + 'datetime': None, + 'fundingRate': fundingRate, + 'fundingTimestamp': None, + 'fundingDatetime': None, + 'nextFundingRate': None, + 'nextFundingTimestamp': None, + 'nextFundingDatetime': None, + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + } + + def handle_option_params_and_request(self, params: object, methodName: str, optionName: str, request: object, requestProperty: str, defaultValue=None): + option, paramsOmited = self.handle_option_and_params(params, methodName, optionName, defaultValue) + if option is not None: + request[requestProperty] = option + return [request, paramsOmited] + + async def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical funding rate prices + + https://coincatch.github.io/github.io/en/mix/#get-history-funding-rate + + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: timestamp in ms of the earliest funding rate to fetch + :param int [limit]: the maximum amount of entries to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.pageNo]: the page number to fetch + :param bool [params.nextPage]: whether to query the next page(default False) + :returns dict[]: a list of `funding rate structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchFundingRateHistory() requires a symbol argument') + await self.load_markets() + maxEntriesPerRequest = 100 + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + requestedLimit = limit + if since is not None: + requestedLimit = maxEntriesPerRequest + if requestedLimit is not None: + request['pageSize'] = requestedLimit + response = await self.publicGetApiMixV1MarketHistoryFundRate(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1725455810888, + # "data": [ + # { + # "symbol": "BTCUSD", + # "fundingRate": "0.000635", + # "settleTime": "1724889600000" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + rates = [] + for i in range(0, len(data)): + entry = data[i] + timestamp = self.safe_integer(entry, 'settleTime') + rates.append({ + 'info': entry, + 'symbol': self.safe_symbol(self.safe_string(entry, 'symbol'), market, None, 'swap'), + 'fundingRate': self.safe_number(entry, 'fundingRate'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + sorted = self.sort_by(rates, 'timestamp') + return self.filter_by_since_limit(sorted, since, limit) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://coincatch.github.io/github.io/en/spot/#get-account-assets + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: 'spot' or 'swap' - the type of the market to fetch balance for(default 'spot') + :param str [params.productType]: *swap only* 'umcbl' or 'dmcbl'(default 'umcbl') + :returns dict: a `balance structure ` + """ + await self.load_markets() + methodName = 'fetchBalance' + marketType = None + marketType, params = self.handle_market_type_and_params(methodName, None, params) + response = None + if marketType == 'spot': + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1725202685986, + # "data": [ + # { + # "coinId": 2, + # "coinName": "USDT", + # "available": "99.20000000", + # "frozen": "0.00000000", + # "lock": "0.00000000", + # "uTime": "1724938746000" + # } + # ] + # } + # + response = await self.privateGetApiSpotV1AccountAssets(params) + elif marketType == 'swap': + productType = 'umcbl' + productType, params = self.handle_option_and_params(params, methodName, 'productType', productType) + request: dict = { + 'productType': productType, + } + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1726666298135, + # "data": [ + # { + # "marginCoin": "USDT", + # "locked": "0", + # "available": "60", + # "crossMaxAvailable": "60", + # "fixedMaxAvailable": "60", + # "maxTransferOut": "60", + # "equity": "60", + # "usdtEquity": "60", + # "btcEquity": "0.001002360626", + # "crossRiskRate": "0", + # "unrealizedPL": "0", + # "bonus": "0", + # "crossedUnrealizedPL": null, + # "isolatedUnrealizedPL": null + # } + # ] + # } + # + response = await self.privateGetApiMixV1AccountAccounts(self.extend(request, params)) + else: + raise NotSupported(self.id + ' ' + methodName + '() is not supported for ' + marketType + ' type of markets') + data = self.safe_list(response, 'data', []) + return self.parse_balance(data) + + def parse_balance(self, balances) -> Balances: + # + # spot + # [ + # { + # "coinId": 2, + # "coinName": "USDT", + # "available": "99.20000000", + # "frozen": "0.00000000", + # "lock": "0.00000000", + # "uTime": "1724938746000" + # } + # ] + # + # swap + # [ + # { + # "marginCoin": "USDT", + # "locked": "0", + # "available": "60", + # "crossMaxAvailable": "60", + # "fixedMaxAvailable": "60", + # "maxTransferOut": "60", + # "equity": "60", + # "usdtEquity": "60", + # "btcEquity": "0.001002360626", + # "crossRiskRate": "0", + # "unrealizedPL": "0", + # "bonus": "0", + # "crossedUnrealizedPL": null, + # "isolatedUnrealizedPL": null + # } + # ] + # + result: dict = { + 'info': balances, + } + for i in range(0, len(balances)): + balanceEntry = self.safe_dict(balances, i, {}) + currencyId = self.safe_string_2(balanceEntry, 'coinName', 'marginCoin') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(balanceEntry, 'available') + locked = self.safe_string_2(balanceEntry, 'lock', 'locked') + frozen = self.safe_string(balanceEntry, 'frozen', '0') + account['used'] = Precise.string_add(locked, frozen) + account['total'] = self.safe_string(balanceEntry, 'equity') + result[code] = account + return self.safe_balance(result) + + async def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://coincatch.github.io/github.io/en/spot/#transfer + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: 'spot' or 'swap' or 'mix_usdt' or 'mix_usd' - account to transfer from + :param str toAccount: 'spot' or 'swap' or 'mix_usdt' or 'mix_usd' - account to transfer to + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.clientOrderId]: a unique id for the transfer + :returns dict: a `transfer structure ` + """ + await self.load_markets() + currency = self.currency(code) + if fromAccount == 'swap': + if code == 'USDT': + fromAccount = 'mix_usdt' + else: + fromAccount = 'mix_usd' + if toAccount == 'swap': + if code == 'USDT': + toAccount = 'mix_usdt' + else: + toAccount = 'mix_usd' + request: dict = { + 'coin': currency['id'], + 'amount': self.currency_to_precision(code, amount), + 'fromType': fromAccount, + 'toType': toAccount, + } + clientOrderId: Str = None + clientOrderId, params = self.handle_option_and_params(params, 'transfer', 'clientOrderId') + if clientOrderId is not None: + request['clientOid'] = clientOrderId + response = await self.privatePostApiSpotV1WalletTransferV2(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1726664727436, + # "data": { + # "transferId": "1220285801129066496", + # "clientOrderId": null + # } + # } + # + return self.parse_transfer(response, currency) + + def parse_transfer(self, transfer, currency: Currency = None): + msg = self.safe_string(transfer, 'msg') + status: Str = None + if msg == 'success': + status = 'ok' + data = self.safe_dict(transfer, 'data', {}) + return { + 'id': self.safe_string(data, 'transferId'), + 'timestamp': None, + 'datetime': None, + 'currency': self.safe_currency_code(None, currency), + 'amount': None, + 'fromAccount': None, + 'toAccount': None, + 'status': status, + 'info': transfer, + } + + async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://coincatch.github.io/github.io/en/spot/#get-coin-address + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.network]: network for fetch deposit address + :returns dict: an `address structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'coin': currency['id'], + } + networkCode: Str = None + networkCode, params = self.handle_network_code_and_params(params) + if networkCode is None: + networkCode = self.default_network_code(code) + if networkCode is None: + raise ArgumentsRequired(self.id + ' fetchDepositAddress() requires a network parameter or a default network code') + request['chain'] = self.network_code_to_id(networkCode, code) + response = await self.privateGetApiSpotV1WalletDepositAddress(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1725210515143, + # "data": { + # "coin": "USDT", + # "address": "TKTUt7qiTaMgnTwZXjE3ZBkPB6LKhLPJyZ", + # "chain": "TRC20", + # "tag": null, + # "url": "https://tronscan.org/#/transaction/" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + depositAddress = self.parse_deposit_address(data, currency) + return depositAddress + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + # + # { + # "coin": "USDT", + # "address": "TKTUt7qiTaMgnTwZXjE3ZBkPB6LKhLPJyZ", + # "chain": "TRC20", + # "tag": null, + # "url": "https://tronscan.org/#/transaction/" + # } + # + address = self.safe_string(depositAddress, 'address') + self.check_address(address) + networkId = self.safe_string(depositAddress, 'chain') + network = self.safe_string(self.options['networksById'], networkId, networkId) + tag = self.safe_string(depositAddress, 'tag') + return { + 'currency': currency['code'], + 'address': address, + 'tag': tag, + 'network': network, + 'info': depositAddress, + } + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://coincatch.github.io/github.io/en/spot/#get-deposit-list + + :param str code: unified currency code of the currency transferred + :param int [since]: the earliest time in ms to fetch transfers for(default 24 hours ago) + :param int [limit]: the maximum number of transfer structures to retrieve(not used by exchange) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch transfers for(default time now) + :param int [params.pageNo]: pageNo default 1 + :param int [params.pageSize]: pageSize(default 20, max 100) + :returns dict[]: a list of `transfer structures ` + """ + methodName = 'fetchDeposits' + await self.load_markets() + request: dict = {} + currency: Currency = None + if code is not None: + currency = self.currency(code) + request['coin'] = currency['id'] + if since is not None: + request['startTime'] = since + until: Int = None + until, params = self.handle_option_and_params(params, methodName, 'until') + if until is not None: + request['endTime'] = until + response = await self.privateGetApiSpotV1WalletDepositList(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1725205525239, + # "data": [ + # { + # "id": "1213046466852196352", + # "txId": "824246b030cd84d56400661303547f43a1d9fef66cf968628dd5112f362053ff", + # "coin": "USDT", + # "type": "deposit", + # "amount": "99.20000000", + # "status": "success", + # "toAddress": "TKTUt7qiTaMgnTwZXjE3ZBkPB6LKhLPJyZ", + # "fee": null, + # "chain": "TRX(TRC20)", + # "confirm": null, + # "clientOid": null, + # "tag": null, + # "fromAddress": null, + # "dest": "on_chain", + # "cTime": "1724938735688", + # "uTime": "1724938746015" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_transactions(data, currency, since, limit) + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://coincatch.github.io/github.io/en/spot/#get-withdraw-list-v2 + + :param str code: unified currency code of the currency transferred + :param int [since]: the earliest time in ms to fetch transfers for(default 24 hours ago) + :param int [limit]: the maximum number of transfer structures to retrieve(default 50, max 200) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch transfers for(default time now) + :param str [params.clientOid]: clientOid + :param str [params.orderId]: The response orderId + :param str [params.idLessThan]: Requests the content on the page before self ID(older data), the value input should be the orderId of the corresponding interface. + :returns dict[]: a list of `transaction structures ` + """ + methodName = 'fetchWithdrawals' + await self.load_markets() + request: dict = {} + currency: Currency = None + if code is not None: + currency = self.currency(code) + request['coin'] = currency['id'] + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + until: Int = None + until, params = self.handle_option_and_params(params, methodName, 'until') + if until is not None: + request['endTime'] = until + response = await self.privateGetApiSpotV1WalletWithdrawalListV2(self.extend(request, params)) + # todo add after withdrawal + # + data = self.safe_list(response, 'data', []) + return self.parse_transactions(data, currency, since, limit) + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://coincatch.github.io/github.io/en/spot/#withdraw + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str [tag]: + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str params['network']: network for withdraw(mandatory) + :param str [params.remark]: remark + :param str [params.clientOid]: custom id + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'coin': currency['id'], + 'address': address, + 'amount': amount, + } + if tag is not None: + request['tag'] = tag + networkCode: Str = None + networkCode, params = self.handle_network_code_and_params(params) + if networkCode is not None: + request['chain'] = self.network_code_to_id(networkCode) + response = await self.privatePostApiSpotV1WalletWithdrawalV2(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "data": { + # "orderId":888291686266343424", + # "clientOrderId":"123" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_transaction(data, currency) + + def parse_transaction(self, transaction, currency: Currency = None) -> Transaction: + # + # fetchDeposits + # + # { + # "id": "1213046466852196352", + # "txId": "824246b030cd84d56400661303547f43a1d9fef66cf968628dd5112f362053ff", + # "coin": "USDT", + # "type": "deposit", + # "amount": "99.20000000", + # "status": "success", + # "toAddress": "TKTUt7qiTaMgnTwZXjE3ZBkPB6LKhLPJyZ", + # "fee": null, + # "chain": "TRX(TRC20)", + # "confirm": null, + # "clientOid": null, + # "tag": null, + # "fromAddress": null, + # "dest": "on_chain", + # "cTime": "1724938735688", + # "uTime": "1724938746015" + # } + # + # withdraw + # + # { + # "code": "00000", + # "msg": "success", + # "data": { + # "orderId":888291686266343424", + # "clientOrderId":"123" + # } + # } + # + status = self.safe_string(transaction, 'status') + if status == 'success': + status = 'ok' + txid = self.safe_string(transaction, 'txId') + coin = self.safe_string(transaction, 'coin') + code = self.safe_currency_code(coin, currency) + timestamp = self.safe_integer(transaction, 'cTime') + amount = self.safe_number(transaction, 'amount') + networkId = self.safe_string(transaction, 'chain') + network = self.safe_string(self.options['networksById'], networkId, networkId) + addressTo = self.safe_string(transaction, 'toAddress') + addressFrom = self.safe_string(transaction, 'fromAddress') + tag = self.safe_string(transaction, 'tag') + type = self.safe_string(transaction, 'type') + feeCost = self.safe_number(transaction, 'fee') + fee = None + if feeCost is not None: + fee = { + 'cost': feeCost, + 'currency': code, + } + return { + 'info': transaction, + 'id': self.safe_string_2(transaction, 'id', 'orderId'), + 'txid': txid, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': network, + 'address': None, + 'addressTo': addressTo, + 'addressFrom': addressFrom, + 'tag': tag, + 'tagTo': None, + 'tagFrom': None, + 'type': type, + 'amount': amount, + 'currency': code, + 'status': status, + 'updated': None, + 'internal': None, + 'comment': None, + 'fee': fee, + } + + async def create_market_buy_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market buy order by providing the symbol and cost + + https://coincatch.github.io/github.io/en/spot/#place-order + + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + methodName = 'createMarketBuyOrderWithCost' + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' ' + methodName + '() supports spot orders only') + params['methodName'] = methodName + params['createMarketBuyOrderRequiresPrice'] = False + return await self.create_order(symbol, 'market', 'buy', cost, None, params) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}) -> Order: + """ + create a trade order + + https://coincatch.github.io/github.io/en/spot/#place-order + https://coincatch.github.io/github.io/en/spot/#place-plan-order + https://coincatch.github.io/github.io/en/mix/#place-order + https://coincatch.github.io/github.io/en/mix/#place-plan-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' or 'LIMIT_MAKER' for spot, 'market' or 'limit' or 'STOP' for swap + :param str side: 'buy' or 'sell' + :param float amount: how much of you want to trade in units of the base currency + :param float [price]: the price that the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.hedged]: *swap markets only* must be set to True if position mode is hedged(default False) + :param float [params.cost]: *spot market buy only* the quote quantity that can be used alternative for the amount + :param float [params.triggerPrice]: the price that the order is to be triggered + :param bool [params.postOnly]: if True, the order will only be posted to the order book and not executed immediately + :param str [params.timeInForce]: 'GTC', 'IOC', 'FOK' or 'PO' + :param str [params.clientOrderId]: a unique id for the order - is mandatory for swap + :returns dict: an `order structure ` + """ + await self.load_markets() + params['methodName'] = self.safe_string(params, 'methodName', 'createOrder') + market = self.market(symbol) + if market['spot']: + return await self.create_spot_order(symbol, type, side, amount, price, params) + elif market['swap']: + return await self.create_swap_order(symbol, type, side, amount, price, params) + else: + raise NotSupported(self.id + ' createOrder() is not supported for ' + market['type'] + ' type of markets') + + async def create_spot_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}) -> Order: + """ + create a trade order on spot market + + https://coincatch.github.io/github.io/en/spot/#place-order + https://coincatch.github.io/github.io/en/spot/#place-plan-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of you want to trade in units of the base currency + :param float [price]: the price that the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.cost]: *market buy only* the quote quantity that can be used alternative for the amount + :param float [params.triggerPrice]: the price that the order is to be triggered at + :param bool [params.postOnly]: if True, the order will only be posted to the order book and not executed immediately + :param str [params.timeInForce]: 'GTC', 'IOC', 'FOK' or 'PO' + :param str [params.clientOrderId]: a unique id for the order(max length 40) + :returns dict: an `order structure ` + """ + await self.load_markets() + params['methodName'] = self.safe_string(params, 'methodName', 'createSpotOrder') + request: dict = self.create_spot_order_request(symbol, type, side, amount, price, params) + isPlanOrer = self.safe_string(request, 'triggerPrice') is not None + response = None + if isPlanOrer: + response = await self.privatePostApiSpotV1PlanPlacePlan(request) + else: + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1725915469815, + # "data": { + # "orderId": "1217143186968068096", + # "clientOrderId": "8fa3eb89-2377-4519-a199-35d5db9ed262" + # } + # } + # + response = await self.privatePostApiSpotV1TradeOrders(request) + data = self.safe_dict(response, 'data', {}) + market = self.market(symbol) + return self.parse_order(data, market) + + def create_spot_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}) -> dict: + """ + @ignore + helper function to build request + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of you want to trade in units of the base currency + :param float [price]: the price that the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: the price that the order is to be triggered at + :param float [params.cost]: *market buy only* the quote quantity that can be used alternative for the amount + :param bool [params.postOnly]: if True, the order will only be posted to the order book and not executed immediately + :param str [params.timeInForce]: 'GTC', 'IOC', 'FOK' or 'PO'(default 'GTC') + :param str [params.clientOrderId]: a unique id for the order(max length 40) + :returns dict: request to be sent to the exchange + """ + methodName = 'createSpotOrderRequest' + # spot market info has no presicion so we do not use it + methodName, params = self.handle_param_string(params, 'methodName', methodName) + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'side': side, + 'orderType': type, + } + isMarketOrder = (type == 'market') + timeInForceAndParams = self.handle_time_in_force_and_post_only(methodName, params, isMarketOrder) + params = timeInForceAndParams['params'] + timeInForce = timeInForceAndParams['timeInForce'] + cost: Str = None + cost, params = self.handle_param_string(params, 'cost') + triggerPrice: Str = None + triggerPrice, params = self.handle_param_string(params, 'triggerPrice') + isMarketBuy = isMarketOrder and (side == 'buy') + if (not isMarketBuy) and (cost is not None): + raise NotSupported(self.id + ' ' + methodName + ' supports cost parameter for market buy orders only') + if isMarketBuy: + costAndParams = self.handle_requires_price_and_cost(methodName, params, price, amount, cost) + cost = costAndParams['cost'] + params = costAndParams['params'] + if triggerPrice is None: + if type == 'limit': + request['price'] = price # spot markets have no precision + request['quantity'] = cost if isMarketBuy else self.number_to_string(amount) # spot markets have no precision + request['force'] = timeInForce if timeInForce else 'normal' # the exchange requres force but accepts any value + else: + request['triggerPrice'] = triggerPrice # spot markets have no precision + if timeInForce is not None: + request['timeInForceValue'] = timeInForce + clientOrderId: Str = None + clientOrderId, params = self.handle_param_string(params, 'clientOrderId') + if clientOrderId is not None: + request['clientOid'] = clientOrderId + if type == 'limit': + request['executePrice'] = price # spot markets have no precision + triggerType: Str = None + if isMarketOrder: + triggerType = 'market_price' + else: + triggerType = 'fill_price' + request['triggerType'] = triggerType + # tood check placeType + request['size'] = cost if isMarketOrder else self.number_to_string(amount) # spot markets have no precision + return self.extend(request, params) + + def handle_requires_price_and_cost(self, methodName: str, params: dict = {}, price: Num = None, amount: Num = None, cost: Str = None, side: str = 'buy'): + optionName = 'createMarket' + self.capitalize(side) + 'OrderRequiresPrice' + requiresPrice = True + requiresPrice, params = self.handle_option_and_params(params, methodName, optionName, True) + amountString: Str = None + if amount is not None: + amountString = self.number_to_string(amount) + priceString: Str = None + if price is not None: + priceString = self.number_to_string(price) + if requiresPrice: + if (price is None) and (cost is None): + raise InvalidOrder(self.id + ' ' + methodName + '() requires the price argument for market ' + side + ' orders to calculate the total cost to spend(amount * price), alternatively set the ' + optionName + ' option or param to False and pass the cost to spend in the amount argument') + elif cost is None: + cost = Precise.string_mul(amountString, priceString) + else: + cost = cost if cost else amountString + result: dict = { + 'cost': cost, + 'params': params, + } + return result + + def handle_time_in_force_and_post_only(self, methodName: str, params: dict = {}, isMarketOrder: Bool = False): + timeInForce: Str = None + timeInForce, params = self.handle_option_and_params(params, methodName, 'timeInForce') + postOnly = False + postOnly, params = self.handle_post_only(isMarketOrder, timeInForce == 'post_only', params) + if postOnly: + timeInForce = 'PO' + timeInForce = self.encode_time_in_force(timeInForce) + result: dict = { + 'timeInForce': timeInForce, + 'params': params, + } + return result + + async def create_swap_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}) -> Order: + """ + create a trade order on swap market + + https://coincatch.github.io/github.io/en/mix/#place-order + https://coincatch.github.io/github.io/en/mix/#place-plan-order + https://coincatch.github.io/github.io/en/mix/#place-stop-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of you want to trade in units of the base currency + :param float [price]: the price that the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.hedged]: must be set to True if position mode is hedged(default False) + :param bool [params.postOnly]: *non-trigger orders only* if True, the order will only be posted to the order book and not executed immediately + :param bool [params.reduceOnly]: True or False whether the order is reduce only + :param str [params.timeInForce]: *non-trigger orders only* 'GTC', 'FOK', 'IOC' or 'PO' + :param str [params.clientOrderId]: a unique id for the order + :param float [params.triggerPrice]: the price that the order is to be triggered at + :param float [params.stopLossPrice]: The price at which a stop loss order is triggered at + :param float [params.takeProfitPrice]: The price at which a take profit order is triggered at + :param dict [params.takeProfit]: *takeProfit object in params* containing the triggerPrice at which the attached take profit order will be triggered(perpetual swap markets only) + :param float [params.takeProfit.triggerPrice]: take profit trigger price + :param dict [params.stopLoss]: *stopLoss object in params* containing the triggerPrice at which the attached stop loss order will be triggered(perpetual swap markets only) + :param float [params.stopLoss.triggerPrice]: stop loss trigger price + :returns dict: an `order structure ` + """ + params['methodName'] = self.safe_string(params, 'methodName', 'createSwapOrder') + await self.load_markets() + market = self.market(symbol) + request = self.create_swap_order_request(symbol, type, side, amount, price, params) + endpointType = self.safe_string(request, 'endpointType') + request = self.omit(request, 'endpointType') + response = None + if endpointType == 'trigger': + response = await self.privatePostApiMixV1PlanPlacePlan(request) + elif endpointType == 'tpsl': + response = await self.privatePostApiMixV1PlanPlaceTPSL(request) + else: # standard + response = await self.privatePostApiMixV1OrderPlaceOrder(request) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1727977301979, + # "data": + # { + # "clientOid": "1225791137701519360", + # "orderId": "1225791137697325056" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data, market) + + def create_swap_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}) -> dict: + """ + @ignore + helper function to build request + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of you want to trade in units of the base currency + :param float [price]: the price that the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.hedged]: must be set to True if position mode is hedged(default False) + :param bool [params.postOnly]: *non-trigger orders only* if True, the order will only be posted to the order book and not executed immediately + :param bool [params.reduceOnly]: True or False whether the order is reduce only + :param str [params.timeInForce]: *non-trigger orders only* 'GTC', 'FOK', 'IOC' or 'PO' + :param str [params.clientOrderId]: a unique id for the order + :param float [params.triggerPrice]: the price that the order is to be triggered at + :param float [params.stopLossPrice]: The price at which a stop loss order is triggered at + :param float [params.takeProfitPrice]: The price at which a take profit order is triggered at + :param dict [params.takeProfit]: *takeProfit object in params* containing the triggerPrice at which the attached take profit order will be triggered(perpetual swap markets only) + :param float [params.takeProfit.triggerPrice]: take profit trigger price + :param dict [params.stopLoss]: *stopLoss object in params* containing the triggerPrice at which the attached stop loss order will be triggered(perpetual swap markets only) + :param float [params.stopLoss.triggerPrice]: stop loss trigger price + :returns dict: request to be sent to the exchange + """ + methodName = 'createSwapOrderRequest' + methodName, params = self.handle_param_string(params, 'methodName', methodName) + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'marginCoin': market['settleId'], + 'size': self.amount_to_precision(symbol, amount), + } + request, params = self.handle_option_params_and_request(params, methodName, 'clientOrderId', request, 'clientOid') + isMarketOrder = (type == 'market') + params = self.handle_trigger_stop_loss_and_take_profit(symbol, side, type, price, methodName, params) + endpointType = self.safe_string(params, 'endpointType') + if (endpointType is None) or (endpointType == 'standard'): + timeInForceAndParams = self.handle_time_in_force_and_post_only(methodName, params, isMarketOrder) # only for non-trigger orders + params = timeInForceAndParams['params'] + timeInForce = timeInForceAndParams['timeInForce'] + if timeInForce is not None: + request['timeInForceValue'] = timeInForce + if price is not None: + request['price'] = self.price_to_precision(symbol, price) + if (endpointType != 'tpsl'): + request['orderType'] = type + sideIsExchangeSpecific = False + hedged = False + if (side == 'buy_single') or (side == 'sell_single') or (side == 'open_long') or (side == 'open_short') or (side == 'close_long') or (side == 'close_short'): + sideIsExchangeSpecific = True + if (side != 'buy_single') and (side != 'sell_single'): + hedged = True + if not sideIsExchangeSpecific: + hedged, params = self.handle_option_and_params(params, methodName, 'hedged', hedged) + # hedged and non-hedged orders have different side values and reduceOnly handling + reduceOnly = self.safe_bool(params, 'reduceOnly') + if hedged: + if (reduceOnly is not None) and reduceOnly: + if side == 'buy': + side = 'close_short' + elif side == 'sell': + side = 'close_long' + else: + if side == 'buy': + side = 'open_long' + elif side == 'sell': + side = 'open_short' + else: + side = side.lower() + '_single' + if hedged: + params = self.omit(params, 'reduceOnly') + request['side'] = side + return self.extend(request, params) + + def handle_trigger_stop_loss_and_take_profit(self, symbol, side, type, price, methodName='createOrder', params={}): + request: dict = {} + endpointType = 'standard' # standard, trigger, tpsl, trailing - to define the endpoint to use + stopLossPrice = self.safe_string(params, 'stopLossPrice') + takeProfitPrice = self.safe_string(params, 'takeProfitPrice') + requestTriggerPrice: Str = None + takeProfitParams = self.safe_dict(params, 'takeProfit') + stopLossParams = self.safe_dict(params, 'stopLoss') + triggerPrice = self.safe_string_2(params, 'triggerPrice', 'stopPrice') + isTrigger = (triggerPrice is not None) + trailingPercent = self.safe_string(params, 'trailingPercent') + trailingTriggerPrice = self.safe_string(params, 'trailingTriggerPrice') + hasTPPrice = (takeProfitPrice is not None) + hasSLPrice = (stopLossPrice is not None) + hasTPParams = (takeProfitParams is not None) + if hasTPParams and not hasTPPrice: + takeProfitPrice = self.safe_string(takeProfitParams, 'triggerPrice') + hasTPPrice = (takeProfitPrice is not None) + hasSLParams = (stopLossParams is not None) + if hasSLParams and not hasSLPrice: + stopLossPrice = self.safe_string(stopLossParams, 'triggerPrice') + hasSLPrice = (stopLossPrice is not None) + hasBothTPAndSL = hasTPPrice and hasSLPrice + isTrailingPercentOrder = (trailingPercent is not None) + isMarketOrder = (type == 'market') + # handle with triggerPrice stopLossPrice and takeProfitPrice + if hasBothTPAndSL or isTrigger or (methodName == 'createOrderWithTakeProfitAndStopLoss'): + if isTrigger: + if isMarketOrder: + request['triggerType'] = 'market_price' + else: + request['triggerType'] = 'fill_price' + request['executePrice'] = self.price_to_precision(symbol, price) + request['triggerPrice'] = self.price_to_precision(symbol, triggerPrice) + endpointType = 'trigger' # if order also has triggerPrice we use endpoint for trigger orders + if methodName == 'createOrders': + endpointType = None # we do not provide endpointType for createOrders + if hasTPPrice: + request['presetTakeProfitPrice'] = takeProfitPrice + if hasSLPrice: + request['presetStopLossPrice'] = stopLossPrice + elif hasTPPrice or hasSLPrice or isTrailingPercentOrder: + if not isMarketOrder: + raise NotSupported(self.id + ' ' + methodName + '() supports does not support ' + type + ' type of stop loss and take profit orders(only market type is supported for stop loss and take profit orders). To create a market order with stop loss or take profit attached use createOrderWithTakeProfitAndStopLoss()') + endpointType = 'tpsl' # if order has only one of the two we use endpoint for tpsl orders + holdSide = 'long' + if side == 'buy': + holdSide = 'short' + request['holdSide'] = holdSide + if isTrailingPercentOrder: + if trailingTriggerPrice is None: + raise ArgumentsRequired(self.id + ' ' + methodName + '() requires the trailingTriggerPrice parameter for trailing stop orders') + requestTriggerPrice = trailingTriggerPrice + request['rangeRate'] = trailingPercent + request['planType'] = 'moving_plan' + elif hasTPPrice: # take profit + requestTriggerPrice = takeProfitPrice + request['planType'] = 'profit_plan' + else: # stop loss + requestTriggerPrice = stopLossPrice + request['planType'] = 'loss_plan' + request['triggerPrice'] = self.price_to_precision(symbol, requestTriggerPrice) + if endpointType is not None: + request['endpointType'] = endpointType + params = self.omit(params, ['stopLoss', 'takeProfit', 'stopLossPrice', 'takeProfitPrice', 'triggerPrice', 'stopPrice', 'trailingPercent', 'trailingTriggerPrice']) + return self.extend(request, params) + + async def create_order_with_take_profit_and_stop_loss(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, takeProfit: Num = None, stopLoss: Num = None, params={}) -> Order: + """ + *swap markets only* create an order with a stop loss or take profit attached(type 3) + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency or the number of contracts + :param float [price]: the price to fulfill the order, in units of the quote currency, ignored in market orders + :param float [takeProfit]: the take profit price, in units of the quote currency + :param float [stopLoss]: the stop loss price, in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + methodName = 'createOrderWithTakeProfitAndStopLoss' + await self.load_markets() + market = self.market(symbol) + if not market['swap']: + raise NotSupported(self.id + ' ' + methodName + '() is supported for swap markets only') + params['methodName'] = methodName + return super(coincatch, self).create_order_with_take_profit_and_stop_loss(symbol, type, side, amount, price, takeProfit, stopLoss, params) + + def encode_time_in_force(self, timeInForce: Str) -> Str: + timeInForceMap = { + 'GTC': 'normal', + 'IOC': 'iok', + 'FOK': 'fok', + 'PO': 'post_only', + } + return self.safe_string(timeInForceMap, timeInForce, timeInForce) + + async def create_orders(self, orders: List[OrderRequest], params={}): + """ + create a list of trade orders(all orders should be of the same symbol) + + https://coincatch.github.io/github.io/en/spot/#batch-order + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params(max 50 entries) + :param dict [params]: extra parameters specific to the api endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + # same symbol for all orders + methodName = 'createOrders' + params['methodName'] = methodName + ordersRequests = [] + symbols = [] + for i in range(0, len(orders)): + rawOrder = orders[i] + symbol = self.safe_string(rawOrder, 'symbol') + symbols.append(symbol) + type = self.safe_string(rawOrder, 'type') + side = self.safe_string(rawOrder, 'side') + amount = self.safe_number(rawOrder, 'amount') + price = self.safe_number(rawOrder, 'price') + orderParams = self.safe_dict(rawOrder, 'params', {}) + orderRequest = self.create_order_request(symbol, type, side, amount, price, orderParams) + triggerPrice = self.safe_string(orderParams, 'triggerPrice') + if triggerPrice is not None: + raise NotSupported(self.id + ' ' + methodName + '() does not support trigger orders') + clientOrderId = self.safe_string(orderRequest, 'clientOrderId') + if clientOrderId is None: + orderRequest['clientOrderId'] = self.uuid() # both spot and swap endpoints require clientOrderId + ordersRequests.append(orderRequest) + symbols = self.unique(symbols) + symbolsLength = len(symbols) + if symbolsLength != 1: + raise BadRequest(self.id + ' createOrders() requires all orders to be of the same symbol') + ordersSymbol = self.safe_string(symbols, 0) + market = self.market(ordersSymbol) + request: dict = { + 'symbol': market['id'], + } + marketType = market['type'] + response = None + responseOrders = None + propertyName: Str = None + if marketType == 'spot': + request['orderList'] = ordersRequests + response = await self.privatePostApiSpotV1TradeBatchOrders(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1726160718706, + # "data": { + # "resultList": [ + # { + # "orderId": "1218171835238367232", + # "clientOrderId": "28759338-ca10-42dd-8ac3-5183785ef60b" + # } + # ], + # "failure": [ + # { + # "orderId": "", + # "clientOrderId": "ee2e67c9-47fc-4311-9cc1-737ec408d509", + # "errorMsg": "The order price of eth_usdt cannot be less than 5.00% of the current price", + # "errorCode": "43008" + # }, + # { + # "orderId": "", + # "clientOrderId": "1af2defa-0c2d-4bb5-acb7-6feb6a86787a", + # "errorMsg": "less than the minimum amount 1 USDT", + # "errorCode": "45110" + # } + # ] + # } + # } + # + propertyName = 'resultList' + elif market['swap']: + request['marginCoin'] = market['settleId'] + request['orderDataList'] = ordersRequests + response = await self.privatePostApiMixV1OrderBatchOrders(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1729100084017, + # "data": { + # "orderInfo": [ + # { + # "orderId": "1230500426827522049", + # "clientOid": "1230500426898825216" + # } + # ], + # "failure": [ + # { + # "orderId": "", + # "clientOid": null, + # "errorMsg": "The order price exceeds the maximum price limit: 2,642.53", + # "errorCode": "22047" + # } + # ], + # "result": True + # } + # } + # + propertyName = 'orderInfo' + else: + raise NotSupported(self.id + ' ' + methodName + '() is not supported for ' + marketType + ' type of markets') + data = self.safe_dict(response, 'data', {}) + responseOrders = self.safe_list(data, propertyName, []) + return self.parse_orders(responseOrders) + + def create_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}) -> dict: + methodName = self.safe_string(params, 'methodName', 'createOrderRequest') + params['methodName'] = methodName + market = self.market(symbol) + if market['spot']: + return self.create_spot_order_request(symbol, type, side, amount, price, params) + elif market['swap']: + return self.create_swap_order_request(symbol, type, side, amount, price, params) + else: + raise NotSupported(self.id + ' ' + methodName + '() is not supported for ' + market['type'] + ' type of markets') + + async def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + """ + edit a trade trigger, stop-looss or take-profit order + + https://coincatch.github.io/github.io/en/spot/#modify-plan-order + + :param str id: order id + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + methodName = 'editOrder' + # only trigger, stop-looss or take-profit orders can be edited + params['methodName'] = methodName + await self.load_markets() + market = self.market(symbol) + if market['spot']: + return await self.edit_spot_order(id, symbol, type, side, amount, price, params) + else: + # todo return await self.editSwapOrder(id, symbol, type, side, amount, price, params) + raise NotSupported(self.id + ' ' + methodName + '() is not supported for ' + market['type'] + ' type of markets') + + async def edit_spot_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + @ignore + edit a trade order + + https://coincatch.github.io/github.io/en/spot/#modify-plan-order + + :param str id: order id + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.clientOrderId]: a unique id for the order that can be used alternative for the id + :param str params['triggerPrice']: *mandatory* the price that the order is to be triggered at + :param float [params.cost]: *market buy only* the quote quantity that can be used alternative for the amount + :returns dict: an `order structure ` + """ + await self.load_markets() + methodName = 'editSpotOrder' + methodName, params = self.handle_param_string(params, 'methodName', methodName) + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' editSpotOrder() does not support ' + market['type'] + ' orders') + request: dict = { + 'orderType': type, + } + clientOrderId = self.safe_string(params, 'clientOrderId') + if clientOrderId is not None: + request['clientOid'] = clientOrderId + elif id is None: + raise BadRequest(self.id + ' ' + methodName + '() requires id or clientOrderId') + else: + request['orderId'] = id + cost: Str = None + cost, params = self.handle_param_string(params, 'cost') + isMarketBuy = (type == 'market') and (side == 'buy') + if (not isMarketBuy) and (cost is not None): + raise NotSupported(self.id + ' ' + methodName + '() supports cost parameter for market buy orders only') + if amount is not None: + if isMarketBuy: + costAndParams = self.handle_requires_price_and_cost(methodName, params, price, amount, cost) + cost = costAndParams['cost'] + params = costAndParams['params'] + else: + request['size'] = self.number_to_string(amount) # spot markets have no precision + if cost is not None: + request['size'] = cost # spot markets have no precision + if (type == 'limit') and (price is not None): + request['price'] = price # spot markets have no precision + response = await self.privatePostApiSpotV1PlanModifyPlan(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1668136575920, + # "data": { + # "orderId": "974792060738441216", + # "clientOrderId": "974792554995224576" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data, market) + + async def fetch_order(self, id: str, symbol: Str = None, params={}) -> Order: + """ + fetches information on an order made by the user(non-trigger orders only) + + https://coincatch.github.io/github.io/en/spot/#get-order-details + https://coincatch.github.io/github.io/en/mix/#get-order-details + + :param str id: the order id + :param str symbol: unified symbol of the market the order was made in(is mandatory for swap) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: 'spot' or 'swap' - the type of the market to fetch entry for(default 'spot') + :param str [params.clientOrderId]: a unique id for the order that can be used alternative for the id + :returns dict: An `order structure ` + """ + methodName = 'fetchOrder' + # for non-trigger orders only + await self.load_markets() + request: dict = {} + clientOrderId = self.safe_string(params, 'clientOrderId') + if clientOrderId is None: + request['orderId'] = id + market: Market = None + if symbol is not None: + market = self.market(symbol) + marketType = 'spot' + marketType, params = self.handle_market_type_and_params(methodName, market, params, marketType) + response = None + order: dict = None + if marketType == 'spot': + # user could query cancelled/filled order details within 24 hours, After 24 hours should use fetchOrders + response = await self.privatePostApiSpotV1TradeOrderInfo(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1725918004434, + # "data": [ + # { + # "accountId": "1002820815393", + # "symbol": "ETHUSDT_SPBL", + # "orderId": "1217143186968068096", + # "clientOrderId": "8fa3eb89-2377-4519-a199-35d5db9ed262", + # "price": "0", + # "quantity": "10.0000000000000000", + # "orderType": "market", + # "side": "buy", + # "status": "full_fill", + # "fillPrice": "2340.5500000000000000", + # "fillQuantity": "0.0042000000000000", + # "fillTotalAmount": "9.8303100000000000", + # "enterPointSource": "API", + # "feeDetail": "{ + # \"ETH\": { + # \"deduction\": False, + # \"feeCoinCode\": \"ETH\", + # \"totalDeductionFee\": 0, + # \"totalFee\": -0.0000042000000000}, + # \"newFees\": { + # \"c\": 0, + # \"d\": 0, + # \"deduction\": False, + # \"r\": -0.0000042, + # \"t\": -0.0000042, + # \"totalDeductionFee\": 0 + # } + # }", + # "orderSource": "market", + # "cTime": "1725915469877" + # } + # ] + # } + # + data = self.safe_list(response, 'data') + if data is None: + response = json.loads(response) # the response from closed orders is not a standard JSON + data = self.safe_list(response, 'data', []) + order = self.safe_dict(data, 0, {}) + elif marketType == 'swap': + if market is None: + raise ArgumentsRequired(self.id + ' ' + methodName + '() requires a symbol argument for ' + marketType + ' type of markets') + request['symbol'] = market['id'] + if clientOrderId is not None: + params = self.omit(params, 'clientOrderId') + request['clientOid'] = clientOrderId + response = await self.privateGetApiMixV1OrderDetail(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1727981421364, + # "data": { + # "symbol": "ETHUSDT_UMCBL", + # "size": 0.01, + # "orderId": "1225791137697325056", + # "clientOid": "1225791137701519360", + # "filledQty": 0.01, + # "fee": -0.01398864, + # "price": null, + # "priceAvg": 2331.44, + # "state": "filled", + # "side": "close_long", + # "timeInForce": "normal", + # "totalProfits": -2.23680000, + # "posSide": "long", + # "marginCoin": "USDT", + # "filledAmount": 23.3144, + # "orderType": "market", + # "leverage": "5", + # "marginMode": "crossed", + # "reduceOnly": True, + # "enterPointSource": "API", + # "tradeSide": "close_long", + # "holdMode": "double_hold", + # "orderSource": "market", + # "cTime": "1727977302003", + # "uTime": "1727977303604" + # } + # } + # + order = self.safe_dict(response, 'data', {}) + else: + raise NotSupported(self.id + ' ' + methodName + '() is not supported for ' + marketType + ' type of markets') + return self.parse_order(order, market) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://coincatch.github.io/github.io/en/spot/#get-order-list + https://coincatch.github.io/github.io/en/spot/#get-current-plan-orders + https://coincatch.github.io/github.io/en/mix/#get-open-order + https://coincatch.github.io/github.io/en/mix/#get-all-open-order + https://coincatch.github.io/github.io/en/mix/#get-plan-order-tpsl-list + + :param str [symbol]: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: True if fetching trigger orders(default False) + :param str [params.type]: 'spot' or 'swap' - the type of the market to fetch entries for(default 'spot') + :param str [params.productType]: *swap only* 'umcbl' or 'dmcbl' - the product type of the market to fetch entries for(default 'umcbl') + :param str [params.marginCoin]: *swap only* the margin coin of the market to fetch entries for + :param str [params.isPlan]: *swap trigger only* 'plan' or 'profit_loss'('plan'(default) for trigger(plan) orders, 'profit_loss' for stop-loss and take-profit orders) + :returns Order[]: a list of `order structures ` + """ + methodName = 'fetchOpenOrders' + await self.load_markets() + market: Market = None + if symbol is not None: + market = self.market(symbol) + marketType = 'spot' + marketType, params = self.handle_market_type_and_params(methodName, market, params, marketType) + params['methodName'] = methodName + if marketType == 'spot': + return await self.fetch_open_spot_orders(symbol, since, limit, params) + elif marketType == 'swap': + return await self.fetch_open_swap_orders(symbol, since, limit, params) + else: + raise NotSupported(self.id + ' ' + methodName + '() is not supported for ' + marketType + ' type of markets') + + async def fetch_open_spot_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + @ignore + fetch all unfilled currently open orders for spot markets + + https://coincatch.github.io/github.io/en/spot/#get-order-list + https://coincatch.github.io/github.io/en/spot/#get-current-plan-orders + + :param str [symbol]: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: True if fetching trigger orders(default False) + :param str [params.lastEndId]: *for trigger orders only* the last order id to fetch entries after + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + methodName = 'fetchOpenSpotOrders' + methodName, params = self.handle_param_string(params, 'methodName', methodName) + request: dict = {} + market: Market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + isTrigger = False + isTrigger, params = self.handle_option_and_params_2(params, methodName, 'trigger', 'stop', isTrigger) + result = None + if isTrigger: + if symbol is None: + raise ArgumentsRequired(self.id + ' ' + methodName + '() requires a symbol argument for trigger orders') + if limit is not None: + request['pageSize'] = limit + response = await self.privatePostApiSpotV1PlanCurrentPlan(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1728664710749, + # "data": { + # "nextFlag": False, + # "endId": 1228661660806787072, + # "orderList": [ + # { + # "orderId": "1228669617606991872", + # "clientOid": "1228669617573437440", + # "symbol": "ETHUSDT_SPBL", + # "size": "50", + # "executePrice": "0", + # "triggerPrice": "4000", + # "status": "not_trigger", + # "orderType": "market", + # "side": "sell", + # "triggerType": "fill_price", + # "enterPointSource": "API", + # "placeType": null, + # "cTime": "1728663585092", + # "uTime": null + # }, + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + result = self.safe_list(data, 'orderList', []) + else: + response = await self.privatePostApiSpotV1TradeOpenOrders(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1725965783430, + # "data": [ + # { + # "accountId": "1002820815393", + # "symbol": "ETHUSDT_SPBL", + # "orderId": "1217347655911653376", + # "clientOrderId": "c57c07d1-bd00-4167-95e2-9b22a55fbc28", + # "price": "2000.0000000000000000", + # "quantity": "0.0010000000000000", + # "orderType": "limit", + # "side": "buy", + # "status": "new", + # "fillPrice": "0", + # "fillQuantity": "0.0000000000000000", + # "fillTotalAmount": "0.0000000000000000", + # "enterPointSource": "API", + # "feeDetail": "", + # "orderSource": "normal", + # "cTime": "1725964219072" + # }, + # ... + # ] + # } + # + result = self.safe_list(response, 'data', []) + return self.parse_orders(result, market, since, limit) + + async def fetch_open_swap_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + @ignore + fetch all unfilled currently open orders for swap markets + + https://coincatch.github.io/github.io/en/mix/#get-open-order + https://coincatch.github.io/github.io/en/mix/#get-all-open-order + https://coincatch.github.io/github.io/en/mix/#get-plan-order-tpsl-list + + :param str [symbol]: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: True if fetching trigger orders(default False) + :param str [params.isPlan]: 'plan' or 'profit_loss'('plan'(default) for trigger(plan) orders, 'profit_loss' for stop-loss and take-profit orders) + :param str [params.productType]: 'umcbl' or 'dmcbl' - the product type of the market to fetch entries for(default 'umcbl') + :param str [params.marginCoin]: the margin coin of the market to fetch entries for + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + methodName = 'fetchOpenSwapOrders' + methodName, params = self.handle_param_string(params, 'methodName', methodName) + isTrigger = False + isTrigger, params = self.handle_option_and_params_2(params, methodName, 'trigger', 'stop', isTrigger) + plan: Str = None + plan, params = self.handle_option_and_params(params, methodName, 'isPlan', plan) + productType = self.handle_option(methodName, 'productType') + market: Market = None + response = None + if symbol is not None: + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if (isTrigger) or (plan is not None): # the same endpoint is used for trigger and stop-loss/take-profit orders + if productType is not None: + request['productType'] = productType + if plan is not None: + request['isPlan'] = plan # current param is used to define the type of the orders to fetch(trigger or stop-loss/take-profit) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1729168682690, + # "data": [ + # { + # "orderId": "1230779428914049025", + # "clientOid": "1230779428914049024", + # "symbol": "ETHUSDT_UMCBL", + # "marginCoin": "USDT", + # "size": "0.01", + # "executePrice": "1000", + # "triggerPrice": "1200", + # "status": "not_trigger", + # "orderType": "limit", + # "planType": "normal_plan", + # "side": "buy_single", + # "triggerType": "fill_price", + # "presetTakeProfitPrice": "4000", + # "presetTakeLossPrice": "900", + # "rangeRate": "", + # "enterPointSource": "API", + # "tradeSide": "buy_single", + # "holdMode": "single_hold", + # "reduceOnly": False, + # "cTime": "1729166603306", + # "uTime": null + # } + # ] + # } + # + response = await self.privateGetApiMixV1PlanCurrentPlan(self.extend(request, params)) + else: + response = await self.privateGetApiMixV1OrderCurrent(self.extend(request, params)) + elif isTrigger: + raise ArgumentsRequired(self.id + ' ' + methodName + '() requires a symbol argument for swap trigger orders') + else: + if productType is None: + productType = 'umcbl' + request: dict = { + 'productType': productType, # is mandatory for current endpoint(all open non-trigger orders) + } + marginCoin: Str = None + marginCoin = self.handle_option(methodName, 'marginCoin', marginCoin) + if marginCoin is not None: + request['marginCoin'] = marginCoin + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1728127869097, + # "data": [ + # { + # "symbol": "ETHUSDT_UMCBL", + # "size": 0.02, + # "orderId": "1226422495431974913", + # "clientOid": "1226422495457140736", + # "filledQty": 0.00, + # "fee": 0E-8, + # "price": 500.00, + # "state": "new", + # "side": "buy_single", + # "timeInForce": "normal", + # "totalProfits": 0E-8, + # "posSide": "long", + # "marginCoin": "USDT", + # "filledAmount": 0.0000, + # "orderType": "limit", + # "leverage": "5", + # "marginMode": "crossed", + # "reduceOnly": False, + # "enterPointSource": "API", + # "tradeSide": "buy_single", + # "holdMode": "single_hold", + # "orderSource": "normal", + # "cTime": "1728127829422", + # "uTime": "1728127830980" + # } + # ] + # } + # + response = await self.privateGetApiMixV1OrderMarginCoinCurrent(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + return self.parse_orders(data, market, since, limit) + + async def fetch_canceled_and_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple canceled and closed orders made by the user + + https://coincatch.github.io/github.io/en/spot/#get-order-list + https://coincatch.github.io/github.io/en/spot/#get-history-plan-orders + https://coincatch.github.io/github.io/en/mix/#get-history-orders + https://coincatch.github.io/github.io/en/mix/#get-producttype-history-orders + https://coincatch.github.io/github.io/en/mix/#get-history-plan-orders-tpsl + + :param str symbol: *is mandatory* unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch orders for + :param boolean [params.trigger]: True if fetching trigger orders(default False) + :param str [params.isPlan]: *swap only* 'plan' or 'profit_loss'('plan'(default) for trigger(plan) orders, 'profit_loss' for stop-loss and take-profit orders) + :param str [params.type]: 'spot' or 'swap' - the type of the market to fetch entries for(default 'spot') + :param str [params.productType]: *swap only* 'umcbl' or 'dmcbl' - the product type of the market to fetch entries for(default 'umcbl') + :returns Order[]: a list of `order structures ` + """ + methodName = 'fetchCanceledAndClosedOrders' + await self.load_markets() + market: Market = None + if symbol is not None: + market = self.market(symbol) + marketType = 'spot' + marketType, params = self.handle_market_type_and_params(methodName, market, params, marketType) + params['methodName'] = methodName + if marketType == 'spot': + return await self.fetch_canceled_and_closed_spot_orders(symbol, since, limit, params) + elif marketType == 'swap': + return await self.fetch_canceled_and_closed_swap_orders(symbol, since, limit, params) + else: + raise NotSupported(self.id + ' ' + methodName + '() is not supported for ' + marketType + ' type of markets') + + async def fetch_canceled_and_closed_spot_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + @ignore + fetches information on multiple canceled and closed orders made by the user on spot markets + + https://coincatch.github.io/github.io/en/spot/#get-order-history + https://coincatch.github.io/github.io/en/spot/#get-history-plan-orders + + :param str symbol: *is mandatory* unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: *for trigger orders only* the latest time in ms to fetch orders for + :param boolean [params.trigger]: True if fetching trigger orders(default False) + :param str [params.lastEndId]: *for trigger orders only* the last order id to fetch entries after + :returns Order[]: a list of `order structures ` + """ + methodName = 'fetchCanceledAndClosedSpotOrders' + methodName, params = self.handle_param_string(params, 'methodName', methodName) + if symbol is None: + raise ArgumentsRequired(self.id + ' ' + methodName + '() requires a symbol argument for spot markets') + maxLimit = 500 + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + requestLimit = limit + isTrigger = False + isTrigger, params = self.handle_option_and_params_2(params, methodName, 'trigger', 'stop', isTrigger) + result = None + if isTrigger: + until: Int = None + until, params = self.handle_option_and_params(params, methodName, 'until', until) + # now = self.milliseconds() + requestSince = since + interval = 90 * 24 * 60 * 60 * 1000 # startTime and endTime interval cannot be greater than 90 days + now = self.milliseconds() + # both since and until are required for trigger orders + if (until is None) and (requestSince is None): + requestSince = now - interval + until = now + elif until is not None: + requestSince = until - interval + else: # if since is defined + until = since + interval + request['startTime'] = requestSince + request['endTime'] = until + if requestLimit is None: + requestLimit = maxLimit + request['pageSize'] = requestLimit + response = await self.privatePostApiSpotV1PlanHistoryPlan(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1728668998002, + # "data": { + # "nextFlag": False, + # "endId": 1228669617606991872, + # "orderList": [ + # { + # "orderId": "1228669617606991872", + # "clientOid": "1228669617573437440", + # "symbol": "ETHUSDT_SPBL", + # "size": "50", + # "executePrice": "0", + # "triggerPrice": "4000", + # "status": "cancel", + # "orderType": "market", + # "side": "sell", + # "triggerType": "fill_price", + # "enterPointSource": "API", + # "placeType": null, + # "cTime": "1728663585092", + # "uTime": "1728666719223" + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + result = self.safe_list(data, 'orderList', []) + else: + if since is not None: + request['after'] = since + requestLimit = maxLimit + if requestLimit is not None: + request['limit'] = requestLimit + response = await self.privatePostApiSpotV1TradeHistory(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1725963777690, + # "data": [ + # { + # "accountId": "1002820815393", + # "symbol": "ETHUSDT_SPBL", + # "orderId": "1217143186968068096", + # "clientOrderId": "8fa3eb89-2377-4519-a199-35d5db9ed262", + # "price": "0", + # "quantity": "10.0000000000000000", + # "orderType": "market", + # "side": "buy", + # "status": "full_fill", + # "fillPrice": "2340.5500000000000000", + # "fillQuantity": "0.0042000000000000", + # "fillTotalAmount": "9.8303100000000000", + # "enterPointSource": "API", + # "feeDetail": "{ + # \"ETH\": { + # \"deduction\": False, + # \"feeCoinCode\": \"ETH\", + # \"totalDeductionFee\": 0, + # \"totalFee\": -0.0000042000000000 + # }, + # \"newFees\": { + # \"c\": 0, + # \"d\": 0, + # \"deduction\": False, + # \"r\": -0.0000042, + # \"t\": -0.0000042, + # \"totalDeductionFee\": 0 + # } + # }", + # "orderSource": "market", + # "cTime": "1725915469877" + # }, + # ... + # ] + # } + # + parsedResponse = json.loads(response) # the response is not a standard JSON + result = self.safe_list(parsedResponse, 'data', []) + return self.parse_orders(result, market, since, limit) + + async def fetch_canceled_and_closed_swap_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + @ignore + fetches information on multiple canceled and closed orders made by the user on swap markets + + https://coincatch.github.io/github.io/en/mix/#get-history-orders + https://coincatch.github.io/github.io/en/mix/#get-producttype-history-orders + https://coincatch.github.io/github.io/en/mix/#get-history-plan-orders-tpsl + + :param str [symbol]: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch orders for + :param boolean [params.trigger]: True if fetching trigger orders(default False) + :param str [params.isPlan]: *swap only* 'plan' or 'profit_loss'('plan'(default) for trigger(plan) orders, 'profit_loss' for stop-loss and take-profit orders) + :param str [params.productType]: *swap only* 'umcbl' or 'dmcbl' - the product type of the market to fetch entries for(default 'umcbl') + :returns Order[]: a list of `order structures ` + """ + methodName = 'fetchCanceledAndClosedSwapOrders' + methodName, params = self.handle_param_string(params, 'methodName', methodName) + requestSince = since + until: Int = None + until, params = self.handle_option_and_params(params, methodName, 'until', until) + now = self.milliseconds() + # since and until are mandatory + # they should be within 90 days interval + interval = 90 * 24 * 60 * 60 * 1000 + if (until is None) and (requestSince is None): + requestSince = now - interval + until = now + elif until is not None: + requestSince = until - interval + else: # if since is defined + until = since + interval + request: dict = { + 'startTime': requestSince, + 'endTime': until, + } + if limit is not None: + request['pageSize'] = limit + market: Market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + productType = self.handle_option(methodName, 'productType') + isTrigger = False + isTrigger, params = self.handle_option_and_params_2(params, methodName, 'trigger', 'stop', isTrigger) + plan: Str = None + plan, params = self.handle_option_and_params(params, methodName, 'isPlan', plan) + response = None + result = None + if (isTrigger) or (plan is not None): + if plan is not None: + request['isPlan'] = plan + if productType is not None: + request['productType'] = productType + response = await self.privateGetApiMixV1PlanHistoryPlan(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1729174716526, + # "data": [ + # { + # "orderId": "1230763430987104257", + # "clientOid": "1230763431003881472", + # "executeOrderId": "", + # "symbol": "ETHUSDT_UMCBL", + # "marginCoin": "USDT", + # "size": "0.03", + # "executePrice": "0", + # "triggerPrice": "2000", + # "status": "cancel", + # "orderType": "market", + # "planType": "loss_plan", + # "side": "sell_single", + # "triggerType": "fill_price", + # "presetTakeProfitPrice": "0", + # "presetTakeLossPrice": "0", + # "rangeRate": null, + # "enterPointSource": "SYS", + # "tradeSide": "sell_single", + # "holdMode": "single_hold", + # "reduceOnly": True, + # "executeTime": "1729173770776", + # "executeSize": "0", + # "cTime": "1729162789103", + # "uTime": "1729173770776" + # } + # ] + # } + # + result = self.safe_list(response, 'data', []) + else: + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + response = await self.privateGetApiMixV1OrderHistory(self.extend(request, params)) + else: + if productType is None: + productType = 'umcbl' # is mandatory for current endpoint + request['productType'] = productType + response = await self.privateGetApiMixV1OrderHistoryProductType(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1728129807637, + # "data": { + # "nextFlag": False, + # "endId": "1221413696648339457", + # "orderList": [ + # { + # "symbol": "ETHUSD_DMCBL", + # "size": 0.1, + # "orderId": "1225467075288719360", + # "clientOid": "1225467075288719361", + # "filledQty": 0.1, + # "fee": -0.00005996, + # "price": null, + # "priceAvg": 2362.03, + # "state": "filled", + # "side": "burst_close_long", + # "timeInForce": "normal", + # "totalProfits": -0.00833590, + # "posSide": "long", + # "marginCoin": "ETH", + # "filledAmount": 236.20300000, + # "orderType": "market", + # "leverage": "12", + # "marginMode": "fixed", + # "reduceOnly": True, + # "enterPointSource": "SYS", + # "tradeSide": "burst_close_long", + # "holdMode": "double_hold", + # "orderSource": "market", + # "cTime": "1727900039503", + # "uTime": "1727900039576" + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + result = self.safe_list(data, 'orderList', []) + return self.parse_orders(result, market) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://coincatch.github.io/github.io/en/spot/#cancel-order-v2 + https://coincatch.github.io/github.io/en/spot/#cancel-plan-order + https://coincatch.github.io/github.io/en/mix/#cancel-order + https://coincatch.github.io/github.io/en/mix/#cancel-plan-order-tpsl + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.clientOrderId]: a unique id for the order that can be used alternative for the id + :param bool [params.trigger]: True for canceling a trigger order(default False) + :param bool [params.stop]: *swap only* an alternative for trigger param + :param str [params.planType]: *swap trigger only* the type of the plan order to cancel: 'profit_plan' - profit order, 'loss_plan' - loss order, 'normal_plan' - plan order, 'pos_profit' - position profit, 'pos_loss' - position loss, 'moving_plan' - Trailing TP/SL, 'track_plan' - Trailing Stop + :returns dict: An `order structure ` + """ + methodName = 'cancelOrder' + if symbol is None: + raise ArgumentsRequired(self.id + ' ' + methodName + '() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = {} + clientOrderId: Str = None + clientOrderId, params = self.handle_param_string(params, 'clientOrderId') + if (id is None) and (clientOrderId is None): + raise ArgumentsRequired(self.id + ' ' + methodName + '() requires an id argument or clientOrderId parameter') + if clientOrderId is not None: + request['clientOid'] = clientOrderId + else: + request['orderId'] = id + marketType = market['type'] + trigger = False + trigger, params = self.handle_option_and_params_2(params, methodName, 'trigger', 'stop', trigger) + response = None + if not trigger or (marketType != 'spot'): + request['symbol'] = market['id'] + if marketType == 'spot': + if trigger: + response = await self.privatePostApiSpotV1PlanCancelPlan(self.extend(request, params)) + else: + response = await self.privatePostApiSpotV1TradeCancelOrderV2(self.extend(request, params)) + elif marketType == 'swap': + planType: Str = None + planType, params = self.handle_option_and_params(params, methodName, 'planType', planType) + request['marginCoin'] = market['settleId'] + if (trigger) or (planType is not None): + if planType is None: + raise ArgumentsRequired(self.id + ' ' + methodName + '() requires a planType parameter for swap trigger orders("profit_plan" - profit order, "loss_plan" - loss order, "normal_plan" - plan order, "pos_profit" - position profit, "pos_loss" - position loss, "moving_plan" - Trailing TP/SL, "track_plan" - Trailing Stop)') + request['planType'] = planType + response = await self.privatePostApiMixV1PlanCancelPlan(self.extend(request, params)) + else: + response = await self.privatePostApiMixV1OrderCancelOrder(self.extend(request, params)) + else: + raise NotSupported(self.id + ' ' + methodName + '() is not supported for ' + marketType + ' type of markets') + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data, market) + + async def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancels all open orders + + https://coincatch.github.io/github.io/en/spot/#cancel-all-orders + https://coincatch.github.io/github.io/en/spot/#batch-cancel-plan-orders + https://coincatch.github.io/github.io/en/mix/#batch-cancel-order + https://coincatch.github.io/github.io/en/mix/#cancel-order-by-symbol + https://coincatch.github.io/github.io/en/mix/#cancel-plan-order-tpsl-by-symbol + https://coincatch.github.io/github.io/en/mix/#cancel-all-trigger-order-tpsl + + :param str [symbol]: unified symbol of the market the orders were made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: 'spot' or 'swap' - the type of the market to cancel orders for(default 'spot') + :param bool [params.trigger]: True for canceling a trigger orders(default False) + :param str [params.productType]: *swap only(if symbol is not provided* 'umcbl' or 'dmcbl' - the product type of the market to cancel orders for(default 'umcbl') + :param str [params.marginCoin]: *mandatory for swap non-trigger dmcb(if symbol is not provided)* the margin coin of the market to cancel orders for + :param str [params.planType]: *swap trigger only* the type of the plan order to cancel: 'profit_plan' - profit order, 'loss_plan' - loss order, 'normal_plan' - plan order, 'pos_profit' - position profit, 'pos_loss' - position loss, 'moving_plan' - Trailing TP/SL, 'track_plan' - Trailing Stop + :returns dict: response from the exchange + """ + methodName = 'cancelAllOrders' + await self.load_markets() + market: Market = None + if symbol is not None: + market = self.market(symbol) + request: dict = {} + marketType = 'spot' + marketType, params = self.handle_market_type_and_params(methodName, market, params, marketType) + trigger = False + trigger, params = self.handle_option_and_params_2(params, methodName, 'trigger', 'stop', trigger) + response = None + if marketType == 'spot': + if trigger: + if symbol is not None: + request['symbols'] = [market['id']] + response = await self.privatePostApiSpotV1PlanBatchCancelPlan(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1728670464735, + # "data": [ + # { + # "orderId": "1228661660806787072", + # "clientOid": "1228661660752261120", + # "result": True + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_orders(data, market) + else: + if symbol is None: + raise ArgumentsRequired(self.id + ' ' + methodName + '() requires a symbol argument for spot non-trigger orders') + request['symbol'] = market['id'] + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1725989560461, + # "data": "ETHUSDT_SPBL" + # } + # + response = await self.privatePostApiSpotV1TradeCancelSymbolOrder(self.extend(request, params)) + elif marketType == 'swap': + productType = 'umcbl' + if symbol is not None: + request['symbol'] = market['id'] + else: + productType = self.handle_option(methodName, 'productType', productType) + request['productType'] = productType # we need either symbol or productType + planType: Str = None + planType, params = self.handle_option_and_params(params, methodName, 'planType', planType) + if (trigger) or (planType is not None): # if trigger or stop-loss/take-profit orders + if planType is None: + raise ArgumentsRequired(self.id + ' ' + methodName + '() requires a planType parameter for swap trigger orders("profit_plan" - profit order, "loss_plan" - loss order, "normal_plan" - plan order, "pos_profit" - position profit, "pos_loss" - position loss, "moving_plan" - Trailing TP/SL, "track_plan" - Trailing Stop)') + request['planType'] = planType + if symbol is not None: + response = await self.privatePostApiMixV1PlanCancelSymbolPlan(self.extend(request, params)) + else: + response = await self.privatePostApiMixV1PlanCancelAllPlan(self.extend(request, params)) + elif symbol is not None: # if non-trigger orders and symbol is provided + request['marginCoin'] = market['settleId'] + response = await self.privatePostApiMixV1OrderCancelSymbolOrders(self.extend(request, params)) + else: # if non-trigger orders and symbol is not provided + marginCoin: Str = None + if productType == 'umcbl': + marginCoin = 'USDT' + else: + marginCoin, params = self.handle_option_and_params(params, methodName, 'marginCoin', marginCoin) + if marginCoin is None: + raise ArgumentsRequired(self.id + ' ' + methodName + '() requires a marginCoin parameter for dmcbl product type') + request['marginCoin'] = marginCoin + response = await self.privatePostApiMixV1OrderCancelAllOrders(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1729104940774, + # "data": { + # "result": True, + # "order_ids": ["1230500426827522049"], + # "client_order_ids": ["1230500426898825216"], + # "fail_infos": [] + # } + # } + # + result = self.get_result_from_batch_canceling_swap_orders(response) + return self.parse_orders(result, market) + else: + raise NotSupported(self.id + ' ' + methodName + '() is not supported for ' + marketType + ' type of markets') + order = self.safe_order(response) + order['info'] = response + return [order] + + async def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): + """ + cancel multiple non-trigger orders + + https://coincatch.github.io/github.io/en/spot/#cancel-order-in-batch-v2-single-instruments + + :param str[] ids: order ids + :param str symbol: *is mandatory* unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str[] [params.clientOrderIds]: client order ids + :returns dict: an list of `order structures ` + """ + methodName = 'cancelOrders' + # only non-trigger and not tp/sl orders can be canceled via cancelOrders + if symbol is None: + raise ArgumentsRequired(self.id + ' ' + methodName + '() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request = { + 'symbol': market['id'], + } + marketType = market['type'] + clientOrderIds = self.safe_list(params, 'clientOrderIds') + if clientOrderIds is not None: + request['clientOids'] = clientOrderIds + params = self.omit(params, 'clientOrderIds') + elif ids is None: + raise ArgumentsRequired(self.id + ' ' + methodName + '() requires either ids argument or clientOrderIds parameter') + else: + request['orderIds'] = ids + response = None + result = None + if marketType == 'spot': + response = await self.privatePostApiSpotV1TradeCancelBatchOrdersV2(self.extend(request)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1726491486352, + # "data": { + # "resultList": [ + # { + # "orderId": "1219555778395160576", + # "clientOrderId": "e229d70a-bb16-4633-a45c-d7f4d3b5d2cf" + # } + # ], + # "failure": [ + # { + # "orderId": "123124124", + # "clientOrderId": null, + # "errorMsg": "The order does not exist", + # "errorCode": "43001" + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + result = self.safe_list(data, 'resultList', []) + elif marketType == 'swap': + request['marginCoin'] = market['settleId'] + response = await self.privatePostApiMixV1OrderCancelBatchOrders(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1729101962321, + # "data": { + # "result": True, + # "symbol": "ETHUSDT_UMCBL", + # "order_ids": ["1226441551501418496", "1230506854262857729"], + # "client_order_ids": [], + # "fail_infos": [] + # } + # } + # + result = self.get_result_from_batch_canceling_swap_orders(response) + else: + raise NotSupported(self.id + ' ' + methodName + '() is not supported for ' + marketType + ' type of markets') + return self.parse_orders(result, market) + + def get_result_from_batch_canceling_swap_orders(self, response): + data = self.safe_dict(response, 'data', {}) + result = [] + orderIds = self.safe_value(data, 'order_ids', []) + for i in range(0, len(orderIds)): + orderId = orderIds[i] + resultItem = { + 'orderId': orderId, + } + result.append(resultItem) + return result + + def parse_order(self, order, market=None) -> Order: + # + # createOrder spot + # { + # "orderId": "1217143186968068096", + # "clientOrderId": "8fa3eb89-2377-4519-a199-35d5db9ed262" + # } + # + # createOrder swap + # { + # "clientOid": "1225791137701519360", + # "orderId": "1225791137697325056" + # } + # + # privatePostApiSpotV1TradeOrderInfo, privatePostApiSpotV1TradeHistory + # { + # "accountId": "1002820815393", + # "symbol": "ETHUSDT_SPBL", + # "orderId": "1217143186968068096", + # "clientOrderId": "8fa3eb89-2377-4519-a199-35d5db9ed262", + # "price": "0", + # "quantity": "10.0000000000000000", + # "orderType": "market", + # "side": "buy", + # "status": "full_fill", + # "fillPrice": "2340.5500000000000000", + # "fillQuantity": "0.0042000000000000", + # "fillTotalAmount": "9.8303100000000000", + # "enterPointSource": "API", + # "feeDetail": "{ + # \"ETH\": { + # \"deduction\": False, + # \"feeCoinCode\": \"ETH\", + # \"totalDeductionFee\": 0, + # \"totalFee\": -0.0000042000000000}, + # \"newFees\": { + # \"c\": 0, + # \"d\": 0, + # \"deduction\": False, + # \"r\": -0.0000042, + # \"t\": -0.0000042, + # \"totalDeductionFee\": 0 + # } + # }", + # "orderSource": "market", + # "cTime": "1725915469877" + # } + # + # privatePostApiMixV1OrderDetail, privateGetApiMixV1OrderMarginCoinCurrent + # { + # "symbol": "ETHUSDT_UMCBL", + # "size": 0.01, + # "orderId": "1225791137697325056", + # "clientOid": "1225791137701519360", + # "filledQty": 0.01, + # "fee": -0.01398864, + # "price": null, + # "priceAvg": 2331.44, + # "state": "filled", + # "side": "close_long", + # "timeInForce": "normal", + # "totalProfits": -2.23680000, + # "posSide": "long", + # "marginCoin": "USDT", + # "filledAmount": 23.3144, + # "orderType": "market", + # "leverage": "5", + # "marginMode": "crossed", + # "reduceOnly": True, + # "enterPointSource": "API", + # "tradeSide": "close_long", + # "holdMode": "double_hold", + # "orderSource": "market", + # "cTime": "1727977302003", + # "uTime": "1727977303604" + # } + # + # privatePostApiSpotV1TradeOpenOrders + # { + # "accountId": "1002820815393", + # "symbol": "ETHUSDT_SPBL", + # "orderId": "1217347655911653376", + # "clientOrderId": "c57c07d1-bd00-4167-95e2-9b22a55fbc28", + # "price": "2000.0000000000000000", + # "quantity": "0.0010000000000000", + # "orderType": "limit", + # "side": "buy", + # "status": "new", + # "fillPrice": "0", + # "fillQuantity": "0.0000000000000000", + # "fillTotalAmount": "0.0000000000000000", + # "enterPointSource": "API", + # "feeDetail": "", + # "orderSource": "normal", + # "cTime": "1725964219072" + # } + # + # privatePostApiSpotV1PlanCurrentPlan, privatePostApiSpotV1PlanHistoryPlan + # { + # "orderId": "1228669617606991872", + # "clientOid": "1228669617573437440", + # "symbol": "ETHUSDT_SPBL", + # "size": "50", + # "executePrice": "0", + # "triggerPrice": "4000", + # "status": "not_trigger", + # "orderType": "market", + # "side": "sell", + # "triggerType": "fill_price", + # "enterPointSource": "API", + # "placeType": null, + # "cTime": "1728663585092", + # "uTime": null + # } + # + # privateGetApiMixV1PlanCurrentPlan + # { + # "orderId": "1230779428914049025", + # "clientOid": "1230779428914049024", + # "symbol": "ETHUSDT_UMCBL", + # "marginCoin": "USDT", + # "size": "0.01", + # "executePrice": "1000", + # "triggerPrice": "1200", + # "status": "not_trigger", + # "orderType": "limit", + # "planType": "normal_plan", + # "side": "buy_single", + # "triggerType": "fill_price", + # "presetTakeProfitPrice": "4000", + # "presetTakeLossPrice": "900", + # "rangeRate": "", + # "enterPointSource": "API", + # "tradeSide": "buy_single", + # "holdMode": "single_hold", + # "reduceOnly": False, + # "cTime": "1729166603306", + # "uTime": null + # } + # + marketId = self.safe_string(order, 'symbol') + marginCoin = self.safe_string(order, 'marginCoin') + market = self.safe_market_custom(marketId, market, marginCoin) + timestamp = self.safe_integer(order, 'cTime') + price = self.omit_zero(self.safe_string_2(order, 'price', 'executePrice')) # price is zero for market orders + priceAvg = self.omit_zero(self.safe_string(order, 'priceAvg')) + if price is None: + price = priceAvg + type = self.safe_string(order, 'orderType') + side = self.parse_order_side(self.safe_string_lower(order, 'side')) + amount = self.safe_string_2(order, 'quantity', 'size') + isTrigger = self.safe_string(order, 'triggerType') is not None + isMarketBuy = (type == 'market') and (side == 'buy') + if (market['spot']) and (isMarketBuy) and (not isTrigger): + amount = None # cost instead of amount is returned for market buy spot non-trigger orders + status = self.safe_string_2(order, 'status', 'state') + feeDetailString = self.safe_string(order, 'feeDetail') + fees = None + feeCurrency: Str = None + feeCost: Str = None + if feeDetailString is not None: + fees = self.parse_fee_detail_string(feeDetailString) + else: + feeCurrency = self.safe_currency_code(marginCoin) if marginCoin else None + feeCost = Precise.string_abs(self.safe_string(order, 'fee')) + timeInForce = self.parse_order_time_in_force(self.safe_string_lower(order, 'timeInForce')) + postOnly: Bool = None + if timeInForce is not None: + postOnly = timeInForce == 'PO' + triggerPrice = self.omit_zero(self.safe_string(order, 'triggerPrice')) + takeProfitPrice = self.omit_zero(self.safe_string(order, 'presetTakeProfitPrice')) + stopLossPrice = self.omit_zero(self.safe_string_2(order, 'presetTakeProfitPrice', 'presetTakeLossPrice')) + planType = self.safe_string(order, 'planType') + if planType == 'loss_plan': + stopLossPrice = triggerPrice + elif planType == 'profit_plan': + takeProfitPrice = triggerPrice + return self.safe_order({ + 'id': self.safe_string(order, 'orderId'), + 'clientOrderId': self.safe_string_2(order, 'clientOrderId', 'clientOid'), + 'datetime': self.iso8601(timestamp), + 'timestamp': timestamp, + 'lastTradeTimestamp': None, + 'lastUpdateTimestamp': self.safe_integer(order, 'uTime'), + 'status': self.parse_order_status(status), + 'symbol': market['symbol'], + 'type': type, + 'timeInForce': timeInForce, + 'side': side, + 'price': price, + 'average': priceAvg if priceAvg else self.safe_string(order, 'fillPrice'), + 'amount': amount, + 'filled': self.safe_string_2(order, 'fillQuantity', 'filledQty'), + 'remaining': None, + 'triggerPrice': triggerPrice, + 'takeProfitPrice': takeProfitPrice, + 'stopLossPrice': stopLossPrice, + 'cost': self.safe_string_2(order, 'fillTotalAmount', 'filledAmount'), + 'trades': None, + 'fee': { + 'currency': feeCurrency, + 'cost': feeCost, + }, + 'fees': fees, + 'reduceOnly': self.safe_bool(order, 'reduceOnly'), + 'postOnly': postOnly, + 'info': order, + }, market) + + def parse_order_status(self, status: Str) -> Str: + satuses = { + 'not_trigger': 'open', + 'init': 'open', + 'new': 'open', + 'partially_filled': 'open', + 'full_fill': 'closed', + 'filled': 'closed', + 'cancel': 'canceled', + 'canceled': 'canceled', + 'cancelled': 'canceled', + } + return self.safe_string(satuses, status, status) + + def parse_order_side(self, side: Str) -> Str: + sides = { + 'buy': 'buy', + 'sell': 'sell', + 'open_long': 'buy', + 'open_short': 'sell', + 'close_long': 'sell', + 'close_short': 'buy', + 'reduce_close_long': 'sell', + 'reduce_close_short': 'buy', + 'offset_close_long': 'sell', + 'offset_close_short': 'buy', + 'burst_close_long': 'sell', + 'burst_close_short': 'buy', + 'delivery_close_long': 'sell', + 'delivery_close_short': 'buy', + 'buy_single': 'buy', + 'sell_single': 'sell', + } + return self.safe_string(sides, side, side) + + def parse_order_time_in_force(self, timeInForce: Str) -> Str: + timeInForces = { + 'normal': 'GTC', + 'post_only': 'PO', + 'iok': 'IOC', + 'fok': 'FOK', + } + return self.safe_string(timeInForces, timeInForce, timeInForce) + + def parse_fee_detail_string(self, feeDetailString: Str): + result = [] + feeDetail = self.parse_json(feeDetailString) + if feeDetail: + keys = list(feeDetail.keys()) + for i in range(0, len(keys)): + currencyId = self.safe_string(keys, i) + if currencyId in self.currencies_by_id: + currency = self.safe_currency_code(currencyId) + feeEntry = self.safe_dict(feeDetail, currencyId, {}) + amount = Precise.string_abs(self.safe_string(feeEntry, 'totalFee')) + result.append({ + 'currency': currency, + 'amount': amount, + }) + return result + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + fetch all trades made by the user + + https://coincatch.github.io/github.io/en/spot/#get-transaction-details + https://coincatch.github.io/github.io/en/mix/#get-order-fill-detail + https://coincatch.github.io/github.io/en/mix/#get-producttype-order-fill-detail + + :param str symbol: *is mandatory* unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: *swap markets only* the latest time in ms to fetch trades for, only supports the last 30 days timeframe + :param str [params.lastEndId]: *swap markets only* query the data after self tradeId + :returns Trade[]: a list of `trade structures ` + """ + methodName = 'fetchMyTrades' + methodName, params = self.handle_param_string(params, 'methodName', methodName) + await self.load_markets() + market: Market = None + marketType = 'spot' + request: dict = {} + if symbol is not None: + market = self.market(symbol) + marketType = market['type'] + request['symbol'] = market['id'] + else: + marketType, params = self.handle_market_type_and_params(methodName, market, params, marketType) + if marketType == 'spot': + raise ArgumentsRequired(self.id + ' ' + methodName + '() requires a symbol argument for spot markets') + response = None + requestLimit = limit + if marketType == 'spot': + maxSpotLimit = 500 + if since is not None: + requestLimit = maxSpotLimit + if requestLimit is not None: + request['limit'] = requestLimit + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1725968747299, + # "data": [ + # { + # "accountId": "1002820815393", + # "symbol": "ETHUSDT_SPBL", + # "orderId": "1217143186968068096", + # "fillId": "1217143193356505089", + # "orderType": "market", + # "side": "buy", + # "fillPrice": "2340.55", + # "fillQuantity": "0.0042", + # "fillTotalAmount": "9.83031", + # "feeCcy": "ETH", + # "fees": "-0.0000042", + # "takerMakerFlag": "taker", + # "cTime": "1725915471400" + # }, + # ... + # ] + # } + # + response = await self.privatePostApiSpotV1TradeFills(self.extend(request, params)) + elif marketType == 'swap': + if since is not None: + params['startTime'] = since + else: + params['startTime'] = 0 # mandatory + until: Int = None + until, params = self.handle_option_and_params(params, methodName, 'until') + if until is not None: + request['endTime'] = until + else: + request['endTime'] = self.milliseconds() # mandatory + if symbol is not None: + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1728306590704, + # "data": [ + # { + # "tradeId": "1221355735285014530", + # "symbol": "ETHUSDT_UMCBL", + # "orderId": "1221355728716259329", + # "price": "2555.12", + # "sizeQty": "0.01", + # "fee": "-0.01533072", + # "side": "open_long", + # "fillAmount": "25.5512", + # "profit": "0", + # "enterPointSource": "API", + # "tradeSide": "open_long", + # "holdMode": "double_hold", + # "takerMakerFlag": "taker", + # "cTime": "1726919819661" + # } + # ] + # } + # + response = await self.privateGetApiMixV1OrderFills(self.extend(request, params)) + else: + productType = 'umcbl' + productType = self.handle_option(methodName, 'productType', productType) + request['productType'] = productType + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1728306372044, + # "data": [ + # { + # "tradeId": "1225467075440189441", + # "symbol": "ETHUSD_DMCBL", + # "orderId": "1225467075288719360", + # "price": "2362.03", + # "sizeQty": "0.1", + # "fee": "-0.00005996", + # "side": "burst_close_long", + # "fillAmount": "236.203", + # "profit": "-0.0083359", + # "enterPointSource": "SYS", + # "tradeSide": "burst_close_long", + # "holdMode": "double_hold", + # "takerMakerFlag": "taker", + # "cTime": "1727900039539" + # }, + # ... + # ] + # } + # + response = await self.privateGetApiMixV1OrderAllFills(self.extend(request, params)) + else: + raise NotSupported(self.id + ' ' + methodName + '() is not supported for ' + marketType + ' type of markets') + data = self.safe_list(response, 'data', []) + return self.parse_trades(data, market, since, limit) + + async def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + fetch all the trades made from a single order + + https://coincatch.github.io/github.io/en/spot/#get-transaction-details + + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + methodName = 'fetchOrderTrades' + if symbol is None: + raise ArgumentsRequired(self.id + ' ' + methodName + '() requires a symbol argument') + request: dict = { + 'orderId': id, + 'methodName': methodName, + } + return await self.fetch_my_trades(symbol, since, limit, self.extend(request, params)) + + async def fetch_margin_mode(self, symbol: str, params={}) -> MarginMode: + """ + fetches the margin mode of the trading pair + + https://coincatch.github.io/github.io/en/mix/#get-single-account + + :param str symbol: unified symbol of the market to fetch the margin mode for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin mode structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'marginCoin': market['settleId'], + } + response = await self.privateGetApiMixV1AccountAccount(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1726669633799, + # "data": { + # "marginCoin": "ETH", + # "locked": "0", + # "available": "0.01", + # "crossMaxAvailable": "0.01", + # "fixedMaxAvailable": "0.01", + # "maxTransferOut": "0.01", + # "equity": "0.01", + # "usdtEquity": "22.97657025", + # "btcEquity": "0.000386195288", + # "crossRiskRate": "0", + # "crossMarginLeverage": 100, + # "fixedLongLeverage": 100, + # "fixedShortLeverage": 100, + # "marginMode": "crossed", + # "holdMode": "double_hold", + # "unrealizedPL": "0", + # "bonus": "0", + # "crossedUnrealizedPL": "0", + # "isolatedUnrealizedPL": "" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_margin_mode(data, market) + + def parse_margin_mode(self, marginMode: dict, market=None) -> MarginMode: + marginType = self.safe_string_lower(marginMode, 'marginMode') + return { + 'info': marginMode, + 'symbol': self.safe_symbol(None, market), + 'marginMode': self.parse_margin_mode_type(marginType), + } + + def parse_margin_mode_type(self, type: str) -> str: + types: dict = { + 'crossed': 'cross', + 'fixed': 'isolated', + } + return self.safe_string(types, type, type) + + async def set_margin_mode(self, marginMode: str, symbol: Str = None, params={}): + """ + set margin mode to 'cross' or 'isolated' + + https://coincatch.github.io/github.io/en/mix/#change-margin-mode + + :param str marginMode: 'cross' or 'isolated' + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setMarginMode() requires a symbol argument') + marginMode = marginMode.lower() + await self.load_markets() + market = self.market(symbol) + if market['type'] != 'swap': + raise NotSupported(self.id + ' setMarginMode() is not supported for ' + market['type'] + ' type of markets') + request: dict = { + 'symbol': market['id'], + 'marginCoin': market['settleId'], + 'marginMode': self.encode_margin_mode_type(marginMode), + } + response = await self.privatePostApiMixV1AccountSetMarginMode(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1726670096099, + # "data": { + # "symbol": "ETHUSD_DMCBL", + # "marginCoin": "ETH", + # "longLeverage": 10, + # "shortLeverage": 10, + # "crossMarginLeverage": null, + # "marginMode": "fixed" + # } + # } + # + return response + + def encode_margin_mode_type(self, type: str) -> str: + types: dict = { + 'cross': 'crossed', + 'isolated': 'fixed', + } + return self.safe_string(types, type, type) + + async def fetch_position_mode(self, symbol: Str = None, params={}): + """ + fetchs the position mode, hedged or one way + + https://coincatch.github.io/github.io/en/mix/#get-single-account + + :param str symbol: unified symbol of the market to fetch entry for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an object detailing whether the market is in hedged or one-way mode + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchPositionMode() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + if market['type'] != 'swap': + raise NotSupported(self.id + ' fetchPositionMode() is not supported for ' + market['type'] + ' type of markets') + request: dict = { + 'symbol': market['id'], + 'marginCoin': market['settleId'], + } + response = await self.privateGetApiMixV1AccountAccount(self.extend(request, params)) # same endpoint + data = self.safe_dict(response, 'data', {}) + holdMode = self.safe_string(data, 'holdMode') + return { + 'info': response, + 'hedged': holdMode == 'double_hold', + } + + async def set_position_mode(self, hedged: bool, symbol: Str = None, params={}): + """ + set hedged to True or False for a market + + https://bingx-api.github.io/docs/#/en-us/swapV2/trade-api.html#Set%20Position%20Mode + + :param bool hedged: set to True to use dualSidePosition + :param str symbol: unified symbol of the market to fetch entry for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.productType]: 'umcbl' or 'dmcbl'(default 'umcbl' if symbol is not provided) + :returns dict: response from the exchange + """ + methodName = 'setPositionMode' + defaultProductType = 'umcbl' + await self.load_markets() + productType = self.safe_string(params, 'productType') + if productType is None: + if symbol is not None: + market = self.market(symbol) + if market['type'] != 'swap': + raise NotSupported(self.id + ' setPositionMode() is not supported for ' + market['type'] + ' type of markets') + marketId = market['id'] + parts = marketId.split('_') + productType = self.safe_string_lower(parts, 1, productType) + else: + productType = self.handle_option(methodName, 'productType', defaultProductType) + request: dict = { + 'productType': productType, + 'holdMode': 'double_hold' if hedged else 'single_hold', + } + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1726677135005, + # "data": { + # "marginCoin": "ETH", + # "dualSidePosition": False + # } + # } + # + return await self.privatePostApiMixV1AccountSetPositionMode(self.extend(request, params)) + + async def fetch_leverage(self, symbol: str, params={}) -> Leverage: + """ + fetch the set leverage for a market + + https://coincatch.github.io/github.io/en/mix/#get-single-account + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `leverage structure ` + """ + await self.load_markets() + market = self.market(symbol) + if market['type'] != 'swap': + raise NotSupported(self.id + ' fetchLeverage() is not supported for ' + market['type'] + ' type of markets') + request: dict = { + 'symbol': market['id'], + 'marginCoin': market['settleId'], + } + response = await self.privateGetApiMixV1AccountAccount(self.extend(request, params)) # same endpoint + data = self.safe_dict(response, 'data', {}) + return self.parse_leverage(data, market) + + async def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://hashkeyglobal-apidoc.readme.io/reference/change-futures-leverage-trade + + :param float leverage: the rate of leverage + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.side]: *for isolated margin mode with hedged position mode only* 'long' or 'short' + :returns dict: response from the exchange + """ + methodName = 'setLeverage' + if symbol is None: + raise ArgumentsRequired(self.id + ' ' + methodName + '() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + if market['type'] != 'swap': + raise NotSupported(self.id + ' ' + methodName + '() is not supported for ' + market['type'] + ' type of markets') + request: dict = { + 'symbol': market['id'], + 'marginCoin': market['settleId'], + 'leverage': leverage, + } + side: Str = None + side, params = self.handle_option_and_params(params, methodName, 'side') + if side is not None: + request['holdSide'] = side + response = await self.privatePostApiMixV1AccountSetLeverage(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1726680486657, + # "data": { + # "symbol": "ETHUSD_DMCBL", + # "marginCoin": "ETH", + # "longLeverage": 2, + # "shortLeverage": 2, + # "crossMarginLeverage": 2, + # "marginMode": "crossed" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_leverage(data, market) + + def parse_leverage(self, leverage: dict, market: Market = None) -> Leverage: + # + # fetchLeverage + # { + # "marginCoin": "ETH", + # "locked": "0", + # "available": "0.01", + # "crossMaxAvailable": "0.01", + # "fixedMaxAvailable": "0.01", + # "maxTransferOut": "0.01", + # "equity": "0.01", + # "usdtEquity": "22.97657025", + # "btcEquity": "0.000386195288", + # "crossRiskRate": "0", + # "crossMarginLeverage": 100, + # "fixedLongLeverage": 100, + # "fixedShortLeverage": 100, + # "marginMode": "crossed", + # "holdMode": "double_hold", + # "unrealizedPL": "0", + # "bonus": "0", + # "crossedUnrealizedPL": "0", + # "isolatedUnrealizedPL": "" + # } + # + # setLeverage + # { + # "symbol": "ETHUSD_DMCBL", + # "marginCoin": "ETH", + # "longLeverage": 2, + # "shortLeverage": 2, + # "crossMarginLeverage": 2, + # "marginMode": "crossed" + # } + # + marketId = self.safe_string(leverage, 'symbol') + market = self.safe_market_custom(marketId, market) + marginMode = self.parse_margin_mode_type(self.safe_string_lower(leverage, 'marginMode')) + longLeverage = self.safe_integer_2(leverage, 'fixedLongLeverage', 'longLeverage') + shortLeverage = self.safe_integer_2(leverage, 'fixedShortLeverage', 'shortLeverage') + crossMarginLeverage = self.safe_integer(leverage, 'crossMarginLeverage') + if marginMode == 'cross': + longLeverage = crossMarginLeverage + shortLeverage = crossMarginLeverage + return { + 'info': leverage, + 'symbol': market['symbol'], + 'marginMode': marginMode, + 'longLeverage': longLeverage, + 'shortLeverage': shortLeverage, + } + + async def modify_margin_helper(self, symbol: str, amount, type, params={}) -> MarginModification: + methodName = 'modifyMarginHelper' + methodName, params = self.handle_param_string(params, 'methodName', methodName) + await self.load_markets() + market = self.market(symbol) + if market['type'] != 'swap': + raise NotSupported(self.id + ' ' + methodName + '() is not supported for ' + market['type'] + ' type of markets') + amount = self.amount_to_precision(symbol, amount) + request: dict = { + 'symbol': market['id'], + 'marginCoin': market['settleId'], + 'amount': amount, # positive value for adding margin, negative for reducing + } + side: Str = None + side, params = self.handle_option_and_params(params, methodName, 'side') + if side is not None: + request['holdSide'] = side + response = await self.privatePostApiMixV1AccountSetMargin(self.extend(request, params)) + # todo check response + # always returns error + # addMargin - "code":"45006","msg":"Insufficient position","requestTime":1729162281543,"data":null + # reduceMargin - "code":"40800","msg":"Insufficient amount of margin","requestTime":1729162362718,"data":null + if type == 'reduce': + amount = Precise.string_abs(amount) + return self.extend(self.parse_margin_modification(response, market), { + 'amount': self.parse_number(amount), + 'type': type, + }) + + def parse_margin_modification(self, data: dict, market: Market = None) -> MarginModification: + # + # + msg = self.safe_string(data, 'msg') + status = 'ok' if (msg == 'success') else 'failed' + return { + 'info': data, + 'symbol': market['symbol'], + 'type': None, + 'marginMode': None, + 'amount': None, + 'total': None, + 'code': market['quote'], + 'status': status, + 'timestamp': None, + 'datetime': None, + } + + async def reduce_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + remove margin from a position + + https://coincatch.github.io/github.io/en/mix/#change-margin + + :param str symbol: unified market symbol + :param float amount: the amount of margin to remove + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.side]: *for isolated margin mode with hedged position mode only* 'long' or 'short' + :returns dict: a `margin structure ` + """ + params['methodName'] = 'reduceMargin' + return await self.modify_margin_helper(symbol, -amount, 'reduce', params) + + async def add_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + add margin + + https://coincatch.github.io/github.io/en/mix/#change-margin + + :param str symbol: unified market symbol + :param float amount: amount of margin to add + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.side]: *for isolated margin mode with hedged position mode only* 'long' or 'short' + :returns dict: a `margin structure ` + """ + params['methodName'] = 'addMargin' + return await self.modify_margin_helper(symbol, amount, 'add', params) + + async def fetch_position(self, symbol: str, params={}) -> Position: + """ + fetch data on a single open contract trade position + + https://coincatch.github.io/github.io/en/mix/#get-symbol-position + + :param str symbol: unified market symbol of the market the position is held in, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + @param {str} [params.side] 'long' or 'short' *for non-hedged position mode only* (default 'long') + :returns dict: a `position structure ` + """ + methodName = 'fetchPosition' + side = 'long' + side, params = self.handle_option_and_params(params, methodName, 'side') + positions = await self.fetch_positions_for_symbol(symbol, params) + arrayLength = len(positions) + if arrayLength > 1: + for i in range(0, len(positions)): + position = positions[i] + if position['side'] == side: + return position + return self.safe_dict(positions, 0, {}) + + async def fetch_positions_for_symbol(self, symbol: str, params={}) -> List[Position]: + """ + fetch open positions for a single market + + https://coincatch.github.io/github.io/en/mix/#get-symbol-position + + fetch all open positions for specific symbol + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'marginCoin': market['settleId'], + } + response = await self.privateGetApiMixV1PositionSinglePositionV2(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1726926959041, + # "data": [ + # { + # "marginCoin": "USDT", + # "symbol": "ETHUSDT_UMCBL", + # "holdSide": "long", + # "openDelegateCount": "0", + # "margin": "2.55512", + # "available": "0.01", + # "locked": "0", + # "total": "0.01", + # "leverage": 10, + # "achievedProfits": "0", + # "averageOpenPrice": "2555.12", + # "marginMode": "crossed", + # "holdMode": "double_hold", + # "unrealizedPL": "0.1371", + # "liquidationPrice": "-3433.328491", + # "keepMarginRate": "0.0033", + # "marketPrice": "2568.83", + # "marginRatio": "0.001666357648", + # "autoMargin": "off", + # "cTime": "1726919819686" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_positions(data, [symbol]) + + async def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://coincatch.github.io/github.io/en/mix/#get-all-position + + :param str[] [symbols]: list of unified market symbols(all symbols must belong to the same product type) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.productType]: 'umcbl' or 'dmcbl'(default 'umcbl' if symbols are not provided) + :param str [params.marginCoin]: the settle currency of the positions, needs to match the productType + :returns dict[]: a list of `position structure ` + """ + methodName = 'fetchPositions' + await self.load_markets() + productType = 'umcbl' + if symbols is not None: + marketIds = self.market_ids(symbols) + productTypes = [] + for i in range(0, len(marketIds)): + marketId = marketIds[i] + parts = marketId.split('_') + marketProductType = self.safe_string(parts, 1) + productTypes.append(marketProductType) + productTypes = self.unique(productTypes) + arrayLength = len(productTypes) + if arrayLength > 1: + raise BadSymbol(self.id + ' ' + methodName + '() requires all symbols to belong to the same product type(umcbl or dmcbl)') + else: + productType = productTypes[0] + else: + productType, params = self.handle_option_and_params(params, methodName, 'productType', productType) + request: dict = { + 'productType': productType, + } + if productType == 'dmcbl': + marginCoin: Str = None + marginCoin, params = self.handle_option_and_params(params, methodName, 'marginCoin') + if marginCoin is not None: + currency = self.currency(marginCoin) + request['marginCoin'] = currency['id'] + response = await self.privateGetApiMixV1PositionAllPositionV2(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1726933132054, + # "data": [ + # { + # "marginCoin": "USDT", + # "symbol": "ETHUSDT_UMCBL", + # "holdSide": "long", + # "openDelegateCount": "0", + # "margin": "2.55512", + # "available": "0.01", + # "locked": "0", + # "total": "0.01", + # "leverage": 10, + # "achievedProfits": "0", + # "averageOpenPrice": "2555.12", + # "marginMode": "crossed", + # "holdMode": "double_hold", + # "unrealizedPL": "0.0093", + # "liquidationPrice": "-3433.378333", + # "keepMarginRate": "0.0033", + # "marketPrice": "2556.05", + # "marginRatio": "0.001661599511", + # "autoMargin": "off", + # "cTime": "1726919819686", + # "uTime": "1726919819686" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_positions(data, symbols) + + def parse_position(self, position: dict, market: Market = None): + # + # { + # "marginCoin": "USDT", + # "symbol": "ETHUSDT_UMCBL", + # "holdSide": "long", + # "openDelegateCount": "0", + # "margin": "2.55512", + # "available": "0.01", + # "locked": "0", + # "total": "0.01", + # "leverage": 10, + # "achievedProfits": "0", + # "averageOpenPrice": "2555.12", + # "marginMode": "crossed", + # "holdMode": "double_hold", + # "unrealizedPL": "0.0093", + # "liquidationPrice": "-3433.378333", + # "keepMarginRate": "0.0033", + # "marketPrice": "2556.05", + # "marginRatio": "0.001661599511", + # "autoMargin": "off", + # "cTime": "1726919819686", + # "uTime": "1726919819686" + # } + # + marketId = self.safe_string(position, 'symbol') + settleId = self.safe_string(position, 'marginCoin') + market = self.safe_market_custom(marketId, market, settleId) + timestamp = self.safe_integer(position, 'cTime') + marginMode = self.safe_string(position, 'marginMode') + isHedged: Bool = None + holdMode = self.safe_string(position, 'holdMode') + if holdMode == 'double_hold': + isHedged = True + elif holdMode == 'single_hold': + isHedged = False + margin = self.safe_number(position, 'margin') + keepMarginRate = self.safe_string(position, 'keepMarginRate') + return self.safe_position({ + 'symbol': market['symbol'], + 'id': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'contracts': self.safe_number(position, 'total'), # todo check + 'contractSize': None, + 'side': self.safe_string_lower(position, 'holdSide'), + 'notional': margin, # todo check + 'leverage': self.safe_integer(position, 'leverage'), + 'unrealizedPnl': self.safe_number(position, 'unrealizedPL'), + 'realizedPnl': self.safe_number(position, 'achievedProfits'), + 'collateral': None, # todo check + 'entryPrice': self.safe_number(position, 'averageOpenPrice'), + 'markPrice': self.safe_number(position, 'marketPrice'), + 'liquidationPrice': self.safe_number(position, 'liquidationPrice'), + 'marginMode': self.parse_margin_mode_type(marginMode), + 'hedged': isHedged, + 'maintenanceMargin': None, # todo check + 'maintenanceMarginPercentage': self.parse_number(Precise.string_mul(keepMarginRate, '100')), # todo check + 'initialMargin': margin, # todo check + 'initialMarginPercentage': None, + 'marginRatio': self.safe_number(position, 'marginRatio'), + 'lastUpdateTimestamp': self.safe_integer(position, 'uTime'), + 'lastPrice': None, + 'stopLossPrice': None, + 'takeProfitPrice': None, + 'percentage': None, + 'info': position, + }) + + def safe_market_custom(self, marketId: Str, market: Market = None, settleId: Str = None) -> Market: + try: + market = self.safe_market(marketId, market) + except Exception as e: + # dmcbl markets have the same id and market type but different settleId + # so we need to resolve the market by settleId + marketsWithCurrentId = self.safe_list(self.markets_by_id, marketId, []) + if settleId is None: + market = marketsWithCurrentId[0] # if settleId is not provided, return the first market with the current id + else: + for i in range(0, len(marketsWithCurrentId)): + marketWithCurrentId = marketsWithCurrentId[i] + if marketWithCurrentId['settleId'] == settleId: + market = marketWithCurrentId + break + return market + + async def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch the history of changes, actions done by the user or operations that altered balance of the user + + https://coincatch.github.io/github.io/en/spot/#get-bills + https://coincatch.github.io/github.io/en/mix/#get-business-account-bill + + :param str [code]: unified currency code + :param int [since]: timestamp in ms of the earliest ledger entry, default is None + :param int [limit]: max number of ledger entrys to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: *swap only* the latest time in ms to fetch entries for + :param str [params.type]: 'spot' or 'swap'(default 'spot') + :param str [params.after]: *spot only* billId, return the data less than self billId + :param str [params.before]: *spot only* billId, return the data greater than or equals to self billId + :param str [params.groupType]: *spot only* + :param str [params.bizType]: *spot only* + :param str [params.productType]: *swap only* 'umcbl' or 'dmcbl'(default 'umcbl' or 'dmcbl' if code is provided and code is not equal to 'USDT') + :param str [params.business]: *swap only* + :param str [params.lastEndId]: *swap only* + :param bool [params.next]: *swap only* + :returns dict: a `ledger structure ` + """ + methodName = 'fetchLedger' + await self.load_markets() + request: dict = {} + marketType = 'spot' + marketType, params = self.handle_market_type_and_params(methodName, None, params, marketType) + result = None + currency = None + if code is not None: + currency = self.currency(code) + if marketType == 'spot': + if currency is not None: + numericId = self.safe_string(currency, 'numericId') + request['coinId'] = numericId + if limit is not None: + request['limit'] = limit + response = await self.privatePostApiSpotV1AccountBills(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1727964749515, + # "data": [ + # { + # "billId": "1220289012519190529", + # "coinId": 2, + # "coinName": "USDT", + # "groupType": "transfer", + # "bizType": "Transfer out", + # "quantity": "-40.00000000", + # "balance": "4.43878673", + # "fees": "0.00000000", + # "cTime": "1726665493092" + # }, + # ... + # ] + # } + # + result = self.safe_list(response, 'data', []) + elif marketType == 'swap': + if since is not None: + request['startTime'] = since + else: + request['startTime'] = 0 # is mandatory + until: Int = None + until, params = self.handle_option_and_params(params, methodName, 'until') + if until is not None: + request['endTime'] = until + else: + request['endTime'] = self.milliseconds() # is mandatory + if limit is not None: + request['pageSize'] = limit + productType = 'umcbl' + if code is None: + productType = self.handle_option(methodName, 'productType', productType) + elif code == 'USDT': + productType = 'umcbl' + else: + productType = 'dmcbl' + productType, params = self.handle_param_string(params, 'productType', productType) + request['productType'] = productType + response = await self.privateGetApiMixV1AccountAccountBusinessBill(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1727971607663, + # "data": { + # "result": [ + # { + # "id": "1225766556446064640", + # "symbol": null, + # "marginCoin": "ETH", + # "amount": "-0.0016", + # "fee": "0", + # "feeByCoupon": "", + # "feeCoin": "ETH", + # "business": "trans_to_exchange", + # "cTime": "1727971441425" + # }, + # { + # "id": "1225467081664061441", + # "symbol": "ETHUSD_DMCBL", + # "marginCoin": "ETH", + # "amount": "-0.00052885", + # "fee": "0", + # "feeByCoupon": "", + # "feeCoin": "ETH", + # "business": "risk_captital_user_transfer", + # "cTime": "1727900041024" + # }, + # { + # "id": "1225467075440189441", + # "symbol": "ETHUSD_DMCBL", + # "marginCoin": "ETH", + # "amount": "-0.0083359", + # "fee": "-0.00005996", + # "feeByCoupon": "", + # "feeCoin": "ETH", + # "business": "burst_long_loss_query", + # "cTime": "1727900039576" + # }, + # { + # "id": "1221416895715303426", + # "symbol": "ETHUSD_DMCBL", + # "marginCoin": "ETH", + # "amount": "0.00004756", + # "fee": "0", + # "feeByCoupon": "", + # "feeCoin": "ETH", + # "business": "contract_settle_fee", + # "cTime": "1726934401444" + # }, + # { + # "id": "1221413703233871873", + # "symbol": "ETHUSD_DMCBL", + # "marginCoin": "ETH", + # "amount": "0", + # "fee": "-0.00005996", + # "feeByCoupon": "", + # "feeCoin": "ETH", + # "business": "open_long", + # "cTime": "1726933640336" + # }, + # { + # "id": "1220288640761122816", + # "symbol": null, + # "marginCoin": "ETH", + # "amount": "0.01", + # "fee": "0", + # "feeByCoupon": "", + # "feeCoin": "ETH", + # "business": "trans_from_exchange", + # "cTime": "1726665404563" + # } + # ], + # "lastEndId": "1220288641021337600", + # "nextFlag": False, + # "preFlag": False + # } + # } + # + data = self.safe_dict(response, 'data', {}) + result = self.safe_list(data, 'result', []) + else: + raise NotSupported(self.id + ' ' + methodName + '() does not support market type ' + marketType) + return self.parse_ledger(result, currency, since, limit) + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + # + # spot + # { + # "billId": "1220289012519190529", + # "coinId": 2, + # "coinName": "USDT", + # "groupType": "transfer", + # "bizType": "Transfer out", + # "quantity": "-40.00000000", + # "balance": "4.43878673", + # "fees": "0.00000000", + # "cTime": "1726665493092" + # } + # + # swap + # { + # "id": "1220288640761122816", + # "symbol": null, + # "marginCoin": "ETH", + # "amount": "0.01", + # "fee": "0", + # "feeByCoupon": "", + # "feeCoin": "ETH", + # "business": "trans_from_exchange", + # "cTime": "1726665404563" + # } + # + timestamp = self.safe_integer(item, 'cTime') + settleId = self.safe_string_2(item, 'coinName', 'marginCoin') + market: Market = None + marketId = self.safe_string(item, 'symbol') + market = self.safe_market_custom(marketId, market, settleId) + amountString = self.safe_string_2(item, 'quantity', 'amount') + direction = 'in' + if Precise.string_lt(amountString, '0'): + direction = 'out' + amountString = Precise.string_mul(amountString, '-1') + fee = { + 'cost': Precise.string_abs(self.safe_string_2(item, 'fee', 'fees')), + 'currency': self.safe_string(item, 'feeCoin'), + } + return self.safe_ledger_entry({ + 'id': self.safe_string_2(item, 'billId', 'id'), + 'info': item, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'account': None, + 'direction': direction, + 'referenceId': None, + 'referenceAccount': None, + 'type': self.parse_ledger_entry_type(self.safe_string_lower_2(item, 'bizType', 'business')), + 'currency': self.safe_currency_code(settleId, currency), + 'symbol': market['symbol'], + 'amount': amountString, + 'before': None, + 'after': self.safe_string(item, 'balance'), + 'status': 'ok', + 'fee': fee, + }, currency) + + def parse_ledger_entry_type(self, type: str) -> str: + types = { + 'deposit': 'deposit', + 'withdraw': 'withdrawal', + 'buy': 'trade', + 'sell': 'trade', + 'deduction of handling fee': 'fee', # todo check + 'transfer-in': 'transfer', + 'transfer in': 'transfer', + 'transfer out': 'transfer', + 'rebate rewards': 'rebate', # todo check + 'airdrop rewards': 'rebate', # todo check + 'usdt contract rewards': 'rebate', # todo check + 'mix contract rewards': 'rebate', # todo check + 'system lock': 'system lock', + 'user lock': 'user lock', + 'open_long': 'trade', + 'open_short': 'trade', + 'close_long': 'trade', + 'close_short': 'trade', + 'trans_from_exchange': 'transfer', + 'trans_to_exchange': 'transfer', + 'contract_settle_fee': 'fee', # todo check sometimes it is positive, sometimes negative + 'burst_long_loss_query': 'trade', # todo check + 'burst_short_loss_query': 'trade', # todo check + } + return self.safe_string(types, type, type) + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if not response: + return None # fallback to default error handler + message = self.safe_string(response, 'msg') + feedback = self.id + ' ' + body + messageCode = self.safe_string(response, 'code') + success = (message == 'success') or (message is None) + if url.find('batch') >= 0: # createOrders, cancelOrders + data = self.safe_dict(response, 'data', {}) + failure = self.safe_list_2(data, 'failure', 'fail_infos', []) + if not self.is_empty(failure): + success = False + firstEntry = self.safe_dict(failure, 0, {}) + messageCode = self.safe_string(firstEntry, 'errorCode') + message = self.safe_string(firstEntry, 'errorMsg') + if not success: + self.throw_exactly_matched_exception(self.exceptions['exact'], messageCode, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + raise ExchangeError(feedback) # unknown message + return None + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + endpoint = '/' + path + if method == 'GET': + query = self.urlencode(params) + if len(query) != 0: + endpoint += '?' + query + if api == 'private': + self.check_required_credentials() + timestamp = self.number_to_string(self.milliseconds()) + suffix = '' + if method != 'GET': + body = self.json(params) + suffix = body + payload = timestamp + method + endpoint + suffix + signature = self.hmac(self.encode(payload), self.encode(self.secret), hashlib.sha256, 'base64') + headers = { + 'ACCESS-KEY': self.apiKey, + 'ACCESS-SIGN': signature, + 'ACCESS-TIMESTAMP': timestamp, + 'ACCESS-PASSPHRASE': self.password, + 'Content-Type': 'application/json', + 'X-CHANNEL-API-CODE': self.safe_string(self.options, 'brokerId', '47cfy'), + } + url = self.urls['api'][api] + endpoint + return {'url': url, 'method': method, 'body': body, 'headers': headers} diff --git a/ccxt/async_support/coincheck.py b/ccxt/async_support/coincheck.py new file mode 100644 index 0000000..dbaf827 --- /dev/null +++ b/ccxt/async_support/coincheck.py @@ -0,0 +1,931 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.coincheck import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currency, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Ticker, Trade, TradingFees, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadSymbol +from ccxt.base.decimal_to_precision import TICK_SIZE + + +class coincheck(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(coincheck, self).describe(), { + 'id': 'coincheck', + 'name': 'coincheck', + 'countries': ['JP', 'ID'], + 'rateLimit': 1500, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelOrder': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createOrder': True, + 'createOrderWithTakeProfitAndStopLoss': False, + 'createOrderWithTakeProfitAndStopLossWs': False, + 'createPostOnlyOrder': False, + 'createReduceOnlyOrder': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchDeposits': True, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkOHLCV': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrderBook': True, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': True, + 'fetchVolatilityHistory': False, + 'fetchWithdrawals': True, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'repayMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'ws': True, + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/51840849/87182088-1d6d6380-c2ec-11ea-9c64-8ab9f9b289f5.jpg', + 'api': { + 'rest': 'https://coincheck.com/api', + }, + 'www': 'https://coincheck.com', + 'doc': 'https://coincheck.com/documents/exchange/api', + 'fees': [ + 'https://coincheck.com/exchange/fee', + 'https://coincheck.com/info/fee', + ], + }, + 'api': { + 'public': { + 'get': [ + 'exchange/orders/rate', + 'order_books', + 'rate/{pair}', + 'ticker', + 'trades', + ], + }, + 'private': { + 'get': [ + 'accounts', + 'accounts/balance', + 'accounts/leverage_balance', + 'bank_accounts', + 'deposit_money', + 'exchange/orders/opens', + 'exchange/orders/transactions', + 'exchange/orders/transactions_pagination', + 'exchange/leverage/positions', + 'lending/borrows/matches', + 'send_money', + 'withdraws', + ], + 'post': [ + 'bank_accounts', + 'deposit_money/{id}/fast', + 'exchange/orders', + 'exchange/transfers/to_leverage', + 'exchange/transfers/from_leverage', + 'lending/borrows', + 'lending/borrows/{id}/repay', + 'send_money', + 'withdraws', + ], + 'delete': [ + 'bank_accounts/{id}', + 'exchange/orders/{id}', + 'withdraws/{id}', + ], + }, + }, + 'markets': { + 'BTC/JPY': self.safe_market_structure({'id': 'btc_jpy', 'symbol': 'BTC/JPY', 'base': 'BTC', 'quote': 'JPY', 'baseId': 'btc', 'quoteId': 'jpy', 'type': 'spot', 'spot': True}), # the only real pair + # 'ETH/JPY': {'id': 'eth_jpy', 'symbol': 'ETH/JPY', 'base': 'ETH', 'quote': 'JPY', 'baseId': 'eth', 'quoteId': 'jpy'}, + 'ETC/JPY': self.safe_market_structure({'id': 'etc_jpy', 'symbol': 'ETC/JPY', 'base': 'ETC', 'quote': 'JPY', 'baseId': 'etc', 'quoteId': 'jpy', 'type': 'spot', 'spot': True}), + # 'DAO/JPY': {'id': 'dao_jpy', 'symbol': 'DAO/JPY', 'base': 'DAO', 'quote': 'JPY', 'baseId': 'dao', 'quoteId': 'jpy'}, + # 'LSK/JPY': {'id': 'lsk_jpy', 'symbol': 'LSK/JPY', 'base': 'LSK', 'quote': 'JPY', 'baseId': 'lsk', 'quoteId': 'jpy'}, + 'FCT/JPY': self.safe_market_structure({'id': 'fct_jpy', 'symbol': 'FCT/JPY', 'base': 'FCT', 'quote': 'JPY', 'baseId': 'fct', 'quoteId': 'jpy', 'type': 'spot', 'spot': True}), + 'MONA/JPY': self.safe_market_structure({'id': 'mona_jpy', 'symbol': 'MONA/JPY', 'base': 'MONA', 'quote': 'JPY', 'baseId': 'mona', 'quoteId': 'jpy', 'type': 'spot', 'spot': True}), + # 'XMR/JPY': {'id': 'xmr_jpy', 'symbol': 'XMR/JPY', 'base': 'XMR', 'quote': 'JPY', 'baseId': 'xmr', 'quoteId': 'jpy'}, + # 'REP/JPY': {'id': 'rep_jpy', 'symbol': 'REP/JPY', 'base': 'REP', 'quote': 'JPY', 'baseId': 'rep', 'quoteId': 'jpy'}, + # 'XRP/JPY': {'id': 'xrp_jpy', 'symbol': 'XRP/JPY', 'base': 'XRP', 'quote': 'JPY', 'baseId': 'xrp', 'quoteId': 'jpy'}, + # 'ZEC/JPY': {'id': 'zec_jpy', 'symbol': 'ZEC/JPY', 'base': 'ZEC', 'quote': 'JPY', 'baseId': 'zec', 'quoteId': 'jpy'}, + # 'XEM/JPY': {'id': 'xem_jpy', 'symbol': 'XEM/JPY', 'base': 'XEM', 'quote': 'JPY', 'baseId': 'xem', 'quoteId': 'jpy'}, + # 'LTC/JPY': {'id': 'ltc_jpy', 'symbol': 'LTC/JPY', 'base': 'LTC', 'quote': 'JPY', 'baseId': 'ltc', 'quoteId': 'jpy'}, + # 'DASH/JPY': {'id': 'dash_jpy', 'symbol': 'DASH/JPY', 'base': 'DASH', 'quote': 'JPY', 'baseId': 'dash', 'quoteId': 'jpy'}, + # 'ETH/BTC': {'id': 'eth_btc', 'symbol': 'ETH/BTC', 'base': 'ETH', 'quote': 'BTC', 'baseId': 'eth', 'quoteId': 'btc'}, + 'ETC/BTC': self.safe_market_structure({'id': 'etc_btc', 'symbol': 'ETC/BTC', 'base': 'ETC', 'quote': 'BTC', 'baseId': 'etc', 'quoteId': 'btc', 'type': 'spot', 'spot': True}), + # 'LSK/BTC': {'id': 'lsk_btc', 'symbol': 'LSK/BTC', 'base': 'LSK', 'quote': 'BTC', 'baseId': 'lsk', 'quoteId': 'btc'}, + # 'FCT/BTC': {'id': 'fct_btc', 'symbol': 'FCT/BTC', 'base': 'FCT', 'quote': 'BTC', 'baseId': 'fct', 'quoteId': 'btc'}, + # 'XMR/BTC': {'id': 'xmr_btc', 'symbol': 'XMR/BTC', 'base': 'XMR', 'quote': 'BTC', 'baseId': 'xmr', 'quoteId': 'btc'}, + # 'REP/BTC': {'id': 'rep_btc', 'symbol': 'REP/BTC', 'base': 'REP', 'quote': 'BTC', 'baseId': 'rep', 'quoteId': 'btc'}, + # 'XRP/BTC': {'id': 'xrp_btc', 'symbol': 'XRP/BTC', 'base': 'XRP', 'quote': 'BTC', 'baseId': 'xrp', 'quoteId': 'btc'}, + # 'ZEC/BTC': {'id': 'zec_btc', 'symbol': 'ZEC/BTC', 'base': 'ZEC', 'quote': 'BTC', 'baseId': 'zec', 'quoteId': 'btc'}, + # 'XEM/BTC': {'id': 'xem_btc', 'symbol': 'XEM/BTC', 'base': 'XEM', 'quote': 'BTC', 'baseId': 'xem', 'quoteId': 'btc'}, + # 'LTC/BTC': {'id': 'ltc_btc', 'symbol': 'LTC/BTC', 'base': 'LTC', 'quote': 'BTC', 'baseId': 'ltc', 'quoteId': 'btc'}, + # 'DASH/BTC': {'id': 'dash_btc', 'symbol': 'DASH/BTC', 'base': 'DASH', 'quote': 'BTC', 'baseId': 'dash', 'quoteId': 'btc'}, + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': False, # todo + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': False, + 'FOK': False, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': False, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': None, + 'daysBack': None, + 'untilDays': None, + 'symbolRequired': True, + }, + 'fetchOrder': None, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': None, + 'fetchOHLCV': None, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'fees': { + 'trading': { + 'tierBased': False, + 'percentage': True, + 'maker': self.parse_number('0'), + 'taker': self.parse_number('0'), + }, + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'exact': { + 'disabled API Key': AuthenticationError, # {"success":false,"error":"disabled API Key"}' + 'invalid authentication': AuthenticationError, # {"success":false,"error":"invalid authentication"} + }, + 'broad': {}, + }, + }) + + def parse_balance(self, response) -> Balances: + result: dict = {'info': response} + codes = list(self.currencies.keys()) + for i in range(0, len(codes)): + code = codes[i] + currency = self.currency(code) + currencyId = currency['id'] + if currencyId in response: + account = self.account() + reserved = currencyId + '_reserved' + account['free'] = self.safe_string(response, currencyId) + account['used'] = self.safe_string(response, reserved) + result[code] = account + return self.safe_balance(result) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://coincheck.com/documents/exchange/api#order-transactions-pagination + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + response = await self.privateGetAccountsBalance(params) + return self.parse_balance(response) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://coincheck.com/documents/exchange/api#order-opens + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + # Only BTC/JPY is meaningful + market = None + if symbol is not None: + market = self.market(symbol) + response = await self.privateGetExchangeOrdersOpens(params) + rawOrders = self.safe_value(response, 'orders', []) + parsedOrders = self.parse_orders(rawOrders, market, since, limit) + result = [] + for i in range(0, len(parsedOrders)): + result.append(self.extend(parsedOrders[i], {'status': 'open'})) + return result + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # fetchOpenOrders + # + # { id: 202835, + # "order_type": "buy", + # "rate": 26890, + # "pair": "btc_jpy", + # "pending_amount": "0.5527", + # "pending_market_buy_amount": null, + # "stop_loss_rate": null, + # "created_at": "2015-01-10T05:55:38.000Z"} + # + # todo: add formats for fetchOrder, fetchClosedOrders here + # + id = self.safe_string(order, 'id') + side = self.safe_string(order, 'order_type') + timestamp = self.parse8601(self.safe_string(order, 'created_at')) + amount = self.safe_string(order, 'pending_amount') + remaining = self.safe_string(order, 'pending_amount') + price = self.safe_string(order, 'rate') + status = None + marketId = self.safe_string(order, 'pair') + symbol = self.safe_symbol(marketId, market, '_') + return self.safe_order({ + 'id': id, + 'clientOrderId': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'amount': amount, + 'remaining': remaining, + 'filled': None, + 'side': side, + 'type': None, + 'timeInForce': None, + 'postOnly': None, + 'status': status, + 'symbol': symbol, + 'price': price, + 'triggerPrice': None, + 'cost': None, + 'fee': None, + 'info': order, + 'average': None, + 'trades': None, + }, market) + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://coincheck.com/documents/exchange/api#order-book + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + response = await self.publicGetOrderBooks(self.extend(request, params)) + return self.parse_order_book(response, market['symbol']) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "last":4192632.0, + # "bid":4192496.0, + # "ask":4193749.0, + # "high":4332000.0, + # "low":4101047.0, + # "volume":2313.43191762, + # "timestamp":1643374115 + # } + # + symbol = self.safe_symbol(None, market) + timestamp = self.safe_timestamp(ticker, 'timestamp') + last = self.safe_string(ticker, 'last') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': self.safe_string(ticker, 'bid'), + 'bidVolume': None, + 'ask': self.safe_string(ticker, 'ask'), + 'askVolume': None, + 'vwap': None, + 'open': None, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': self.safe_string(ticker, 'volume'), + 'quoteVolume': None, + 'info': ticker, + }, market) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://coincheck.com/documents/exchange/api#ticker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + if symbol != 'BTC/JPY': + raise BadSymbol(self.id + ' fetchTicker() supports BTC/JPY only') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + ticker = await self.publicGetTicker(self.extend(request, params)) + # + # { + # "last":4192632.0, + # "bid":4192496.0, + # "ask":4193749.0, + # "high":4332000.0, + # "low":4101047.0, + # "volume":2313.43191762, + # "timestamp":1643374115 + # } + # + return self.parse_ticker(ticker, market) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(public) + # + # { + # "id": "206849494", + # "amount": "0.01", + # "rate": "5598346.0", + # "pair": "btc_jpy", + # "order_type": "sell", + # "created_at": "2021-12-08T14:10:33.000Z" + # } + # + # fetchMyTrades(private) - example from docs + # + # { + # "id": 38, + # "order_id": 49, + # "created_at": "2015-11-18T07:02:21.000Z", + # "funds": { + # "btc": "0.1", + # "jpy": "-4096.135" + # }, + # "pair": "btc_jpy", + # "rate": "40900.0", + # "fee_currency": "JPY", + # "fee": "6.135", + # "liquidity": "T", + # "side": "buy" + # } + # + timestamp = self.parse8601(self.safe_string(trade, 'created_at')) + id = self.safe_string(trade, 'id') + priceString = self.safe_string(trade, 'rate') + marketId = self.safe_string(trade, 'pair') + market = self.safe_market(marketId, market, '_') + baseId = market['baseId'] + quoteId = market['quoteId'] + symbol = market['symbol'] + takerOrMaker = None + amountString = None + costString = None + side = None + fee = None + orderId = None + if 'liquidity' in trade: + if self.safe_string(trade, 'liquidity') == 'T': + takerOrMaker = 'taker' + elif self.safe_string(trade, 'liquidity') == 'M': + takerOrMaker = 'maker' + funds = self.safe_value(trade, 'funds', {}) + amountString = self.safe_string(funds, baseId) + costString = self.safe_string(funds, quoteId) + fee = { + 'currency': self.safe_string(trade, 'fee_currency'), + 'cost': self.safe_string(trade, 'fee'), + } + side = self.safe_string(trade, 'side') + orderId = self.safe_string(trade, 'order_id') + else: + amountString = self.safe_string(trade, 'amount') + side = self.safe_string(trade, 'order_type') + return self.safe_trade({ + 'id': id, + 'info': trade, + 'datetime': self.iso8601(timestamp), + 'timestamp': timestamp, + 'symbol': symbol, + 'type': None, + 'side': side, + 'order': orderId, + 'takerOrMaker': takerOrMaker, + 'price': priceString, + 'amount': amountString, + 'cost': costString, + 'fee': fee, + }, market) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://coincheck.com/documents/exchange/api#order-transactions-pagination + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = {} + if limit is not None: + request['limit'] = limit + response = await self.privateGetExchangeOrdersTransactionsPagination(self.extend(request, params)) + # + # { + # "success": True, + # "data": [ + # { + # "id": 38, + # "order_id": 49, + # "created_at": "2015-11-18T07:02:21.000Z", + # "funds": { + # "btc": "0.1", + # "jpy": "-4096.135" + # }, + # "pair": "btc_jpy", + # "rate": "40900.0", + # "fee_currency": "JPY", + # "fee": "6.135", + # "liquidity": "T", + # "side": "buy" + # }, + # ] + # } + # + transactions = self.safe_list(response, 'data', []) + return self.parse_trades(transactions, market, since, limit) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://coincheck.com/documents/exchange/api#public-trades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + if limit is not None: + request['limit'] = limit + response = await self.publicGetTrades(self.extend(request, params)) + # + # { + # "id": "206849494", + # "amount": "0.01", + # "rate": "5598346.0", + # "pair": "btc_jpy", + # "order_type": "sell", + # "created_at": "2021-12-08T14:10:33.000Z" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_trades(data, market, since, limit) + + async def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://coincheck.com/documents/exchange/api#account-info + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + await self.load_markets() + response = await self.privateGetAccounts(params) + # + # { + # "success": True, + # "id": "7487995", + # "email": "some@email.com", + # "identity_status": "identity_pending", + # "bitcoin_address": null, + # "lending_leverage": "4", + # "taker_fee": "0.0", + # "maker_fee": "0.0", + # "exchange_fees": { + # "btc_jpy": {taker_fee: '0.0', maker_fee: "0.0"}, + # "etc_jpy": {taker_fee: '0.0', maker_fee: "0.0"}, + # "fct_jpy": {taker_fee: '0.0', maker_fee: "0.0"}, + # "mona_jpy": {taker_fee: '0.0', maker_fee: "0.0"}, + # "plt_jpy": {taker_fee: '0.0', maker_fee: "0.0"} + # } + # } + # + fees = self.safe_value(response, 'exchange_fees', {}) + result: dict = {} + for i in range(0, len(self.symbols)): + symbol = self.symbols[i] + market = self.market(symbol) + fee = self.safe_value(fees, market['id'], {}) + result[symbol] = { + 'info': fee, + 'symbol': symbol, + 'maker': self.safe_number(fee, 'maker_fee'), + 'taker': self.safe_number(fee, 'taker_fee'), + 'percentage': True, + 'tierBased': False, + } + return result + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://coincheck.com/documents/exchange/api#order-new + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + if type == 'market': + request['order_type'] = type + '_' + side + if side == 'sell': + request['amount'] = amount + else: + cost = self.safe_number(params, 'cost') + params = self.omit(params, 'cost') + if cost is not None: + raise ArgumentsRequired(self.id + ' createOrder() : you should use "cost" parameter instead of "amount" argument to create market buy orders') + request['market_buy_amount'] = cost + else: + request['order_type'] = side + request['rate'] = price + request['amount'] = amount + response = await self.privatePostExchangeOrders(self.extend(request, params)) + id = self.safe_string(response, 'id') + return self.safe_order({ + 'id': id, + 'info': response, + }, market) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://coincheck.com/documents/exchange/api#order-cancel + + :param str id: order id + :param str symbol: not used by coincheck cancelOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + request: dict = { + 'id': id, + } + response = await self.privateDeleteExchangeOrdersId(self.extend(request, params)) + # + # { + # "success": True, + # "id": 12345 + # } + # + return self.parse_order(response) + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://coincheck.com/documents/exchange/api#account-deposits + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + currency = None + request: dict = {} + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + if limit is not None: + request['limit'] = limit + response = await self.privateGetDepositMoney(self.extend(request, params)) + # { + # "success": True, + # "deposits": [ + # { + # "id": 2, + # "amount": "0.05", + # "currency": "BTC", + # "address": "13PhzoK8me3u5nHzzFD85qT9RqEWR9M4Ty", + # "status": "confirmed", + # "confirmed_at": "2015-06-13T08:29:18.000Z", + # "created_at": "2015-06-13T08:22:18.000Z" + # }, + # { + # "id": 1, + # "amount": "0.01", + # "currency": "BTC", + # "address": "13PhzoK8me3u5nHzzFD85qT9RqEWR9M4Ty", + # "status": "received", + # "confirmed_at": "2015-06-13T08:21:18.000Z", + # "created_at": "2015-06-13T08:21:18.000Z" + # } + # ] + # } + data = self.safe_list(response, 'deposits', []) + return self.parse_transactions(data, currency, since, limit, {'type': 'deposit'}) + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://coincheck.com/documents/exchange/api#withdraws + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + currency = None + if code is not None: + currency = self.currency(code) + request: dict = {} + if limit is not None: + request['limit'] = limit + response = await self.privateGetWithdraws(self.extend(request, params)) + # { + # "success": True, + # "pagination": { + # "limit": 25, + # "order": "desc", + # "starting_after": null, + # "ending_before": null + # }, + # "data": [ + # { + # "id": 398, + # "status": "finished", + # "amount": "242742.0", + # "currency": "JPY", + # "created_at": "2014-12-04T15:00:00.000Z", + # "bank_account_id": 243, + # "fee": "400.0", + # "is_fast": True + # } + # ] + # } + data = self.safe_list(response, 'data', []) + return self.parse_transactions(data, currency, since, limit, {'type': 'withdrawal'}) + + def parse_transaction_status(self, status: Str): + statuses: dict = { + # withdrawals + 'pending': 'pending', + 'processing': 'pending', + 'finished': 'ok', + 'canceled': 'canceled', + # deposits + 'confirmed': 'pending', + 'received': 'ok', + } + return self.safe_string(statuses, status, status) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fetchDeposits + # + # { + # "id": 2, + # "amount": "0.05", + # "currency": "BTC", + # "address": "13PhzoK8me3u5nHzzFD85qT9RqEWR9M4Ty", + # "status": "confirmed", + # "confirmed_at": "2015-06-13T08:29:18.000Z", + # "created_at": "2015-06-13T08:22:18.000Z" + # } + # + # fetchWithdrawals + # + # { + # "id": 398, + # "status": "finished", + # "amount": "242742.0", + # "currency": "JPY", + # "created_at": "2014-12-04T15:00:00.000Z", + # "bank_account_id": 243, + # "fee": "400.0", + # "is_fast": True + # } + # + id = self.safe_string(transaction, 'id') + timestamp = self.parse8601(self.safe_string(transaction, 'created_at')) + address = self.safe_string(transaction, 'address') + amount = self.safe_number(transaction, 'amount') + currencyId = self.safe_string(transaction, 'currency') + code = self.safe_currency_code(currencyId, currency) + status = self.parse_transaction_status(self.safe_string(transaction, 'status')) + updated = self.parse8601(self.safe_string(transaction, 'confirmed_at')) + fee = None + feeCost = self.safe_number(transaction, 'fee') + if feeCost is not None: + fee = { + 'cost': feeCost, + 'currency': code, + } + return { + 'info': transaction, + 'id': id, + 'txid': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': None, + 'address': address, + 'addressTo': address, + 'addressFrom': None, + 'tag': None, + 'tagTo': None, + 'tagFrom': None, + 'type': None, + 'amount': amount, + 'currency': code, + 'status': status, + 'updated': updated, + 'comment': None, + 'internal': None, + 'fee': fee, + } + + def nonce(self): + return self.milliseconds() + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = self.urls['api']['rest'] + '/' + self.implode_params(path, params) + query = self.omit(params, self.extract_params(path)) + if api == 'public': + if query: + url += '?' + self.urlencode(query) + else: + self.check_required_credentials() + nonce = str(self.nonce()) + queryString = '' + if method == 'GET': + if query: + url += '?' + self.urlencode(self.keysort(query)) + else: + if query: + body = self.urlencode(self.keysort(query)) + queryString = body + auth = nonce + url + queryString + headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + 'ACCESS-KEY': self.apiKey, + 'ACCESS-NONCE': nonce, + 'ACCESS-SIGNATURE': self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256), + } + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None + # + # {"success":false,"error":"disabled API Key"}' + # {"success":false,"error":"invalid authentication"} + # + success = self.safe_bool(response, 'success', True) + if not success: + error = self.safe_string(response, 'error') + feedback = self.id + ' ' + self.json(response) + self.throw_exactly_matched_exception(self.exceptions['exact'], error, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], body, feedback) + raise ExchangeError(self.id + ' ' + self.json(response)) + return None diff --git a/ccxt/async_support/coinex.py b/ccxt/async_support/coinex.py new file mode 100644 index 0000000..c590d61 --- /dev/null +++ b/ccxt/async_support/coinex.py @@ -0,0 +1,5900 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.coinex import ImplicitAPI +import asyncio +from ccxt.base.types import Any, Balances, BorrowInterest, Currencies, Currency, DepositAddress, Int, IsolatedBorrowRate, Leverage, LeverageTier, LeverageTiers, MarginModification, Market, Num, Order, OrderRequest, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, FundingRates, Trade, TradingFeeInterface, TradingFees, Transaction, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import AccountSuspended +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import NotSupported +from ccxt.base.errors import OperationFailed +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.errors import RequestTimeout +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class coinex(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(coinex, self).describe(), { + 'id': 'coinex', + 'name': 'CoinEx', + 'version': 'v2', + 'countries': ['CN'], + # IP ratelimit is 400 requests per second + # rateLimit = 1000ms / 400 = 2.5 + # 200 per 2 seconds => 100 per second => weight = 4 + # 120 per 2 seconds => 60 per second => weight = 6.667 + # 80 per 2 seconds => 40 per second => weight = 10 + # 60 per 2 seconds => 30 per second => weight = 13.334 + # 40 per 2 seconds => 20 per second => weight = 20 + # 20 per 2 seconds => 10 per second => weight = 40 + # v1 is per 2 seconds and v2 is per 1 second + 'rateLimit': 2.5, + 'pro': True, + 'certified': True, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': True, + 'swap': True, + 'future': False, + 'option': False, + 'addMargin': True, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': True, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'cancelOrders': True, + 'closeAllPositions': False, + 'closePosition': True, + 'createDepositAddress': True, + 'createMarketBuyOrderWithCost': True, + 'createMarketOrderWithCost': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createOrders': True, + 'createReduceOnlyOrder': True, + 'createStopLossOrder': True, + 'createStopOrder': True, + 'createTakeProfitOrder': True, + 'createTriggerOrder': True, + 'editOrder': True, + 'fetchBalance': True, + 'fetchBorrowInterest': True, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchDepositWithdrawFee': True, + 'fetchDepositWithdrawFees': True, + 'fetchFundingHistory': True, + 'fetchFundingInterval': True, + 'fetchFundingIntervals': False, + 'fetchFundingRate': True, + 'fetchFundingRateHistory': True, + 'fetchFundingRates': True, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': True, + 'fetchIsolatedBorrowRates': False, + 'fetchLeverage': True, + 'fetchLeverages': False, + 'fetchLeverageTiers': True, + 'fetchMarginAdjustmentHistory': True, + 'fetchMarketLeverageTiers': 'emulated', + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchPosition': True, + 'fetchPositionHistory': True, + 'fetchPositions': True, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': True, + 'fetchTradingFees': True, + 'fetchTransfer': False, + 'fetchTransfers': True, + 'fetchWithdrawal': False, + 'fetchWithdrawals': True, + 'reduceMargin': True, + 'repayCrossMargin': False, + 'repayIsolatedMargin': True, + 'setLeverage': True, + 'setMarginMode': True, + 'setPositionMode': False, + 'transfer': True, + 'withdraw': True, + }, + 'timeframes': { + '1m': '1min', + '3m': '3min', + '5m': '5min', + '15m': '15min', + '30m': '30min', + '1h': '1hour', + '2h': '2hour', + '4h': '4hour', + '6h': '6hour', + '12h': '12hour', + '1d': '1day', + '3d': '3day', + '1w': '1week', + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/51840849/87182089-1e05fa00-c2ec-11ea-8da9-cc73b45abbbc.jpg', + 'api': { + 'public': 'https://api.coinex.com', + 'private': 'https://api.coinex.com', + 'perpetualPublic': 'https://api.coinex.com/perpetual', + 'perpetualPrivate': 'https://api.coinex.com/perpetual', + }, + 'www': 'https://www.coinex.com', + 'doc': 'https://docs.coinex.com/api/v2', + 'fees': 'https://www.coinex.com/fees', + 'referral': 'https://www.coinex.com/register?refer_code=yw5fz', + }, + 'api': { + 'v1': { + 'public': { + 'get': { + 'amm/market': 1, + 'common/currency/rate': 1, + 'common/asset/config': 1, + 'common/maintain/info': 1, + 'common/temp-maintain/info': 1, + 'margin/market': 1, + 'market/info': 1, + 'market/list': 1, + 'market/ticker': 1, + 'market/ticker/all': 1, + 'market/depth': 1, + 'market/deals': 1, + 'market/kline': 1, + 'market/detail': 1, + }, + }, + 'private': { + 'get': { + 'account/amm/balance': 40, + 'account/investment/balance': 40, + 'account/balance/history': 40, + 'account/market/fee': 40, + 'balance/coin/deposit': 40, + 'balance/coin/withdraw': 40, + 'balance/info': 40, + 'balance/deposit/address/{coin_type}': 40, + 'contract/transfer/history': 40, + 'credit/info': 40, + 'credit/balance': 40, + 'investment/transfer/history': 40, + 'margin/account': 1, + 'margin/config': 1, + 'margin/loan/history': 40, + 'margin/transfer/history': 40, + 'order/deals': 40, + 'order/finished': 40, + 'order/pending': 8, + 'order/status': 8, + 'order/status/batch': 8, + 'order/user/deals': 40, + 'order/stop/finished': 40, + 'order/stop/pending': 8, + 'order/user/trade/fee': 1, + 'order/market/trade/info': 1, + 'sub_account/balance': 1, + 'sub_account/transfer/history': 40, + 'sub_account/auth/api': 40, + 'sub_account/auth/api/{user_auth_id}': 40, + }, + 'post': { + 'balance/coin/withdraw': 40, + 'contract/balance/transfer': 40, + 'margin/flat': 40, + 'margin/loan': 40, + 'margin/transfer': 40, + 'order/limit/batch': 40, + 'order/ioc': 13.334, + 'order/limit': 13.334, + 'order/market': 13.334, + 'order/modify': 13.334, + 'order/stop/limit': 13.334, + 'order/stop/market': 13.334, + 'order/stop/modify': 13.334, + 'sub_account/transfer': 40, + 'sub_account/register': 1, + 'sub_account/unfrozen': 40, + 'sub_account/frozen': 40, + 'sub_account/auth/api': 40, + }, + 'put': { + 'balance/deposit/address/{coin_type}': 40, + 'sub_account/unfrozen': 40, + 'sub_account/frozen': 40, + 'sub_account/auth/api/{user_auth_id}': 40, + 'v1/account/settings': 40, + }, + 'delete': { + 'balance/coin/withdraw': 40, + 'order/pending/batch': 40, + 'order/pending': 13.334, + 'order/stop/pending': 40, + 'order/stop/pending/{id}': 13.334, + 'order/pending/by_client_id': 40, + 'order/stop/pending/by_client_id': 40, + 'sub_account/auth/api/{user_auth_id}': 40, + 'sub_account/authorize/{id}': 40, + }, + }, + 'perpetualPublic': { + 'get': { + 'ping': 1, + 'time': 1, + 'market/list': 1, + 'market/limit_config': 1, + 'market/ticker': 1, + 'market/ticker/all': 1, + 'market/depth': 1, + 'market/deals': 1, + 'market/funding_history': 1, + 'market/kline': 1, + }, + }, + 'perpetualPrivate': { + 'get': { + 'market/user_deals': 1, + 'asset/query': 40, + 'order/pending': 8, + 'order/finished': 40, + 'order/stop_finished': 40, + 'order/stop_pending': 8, + 'order/status': 8, + 'order/stop_status': 8, + 'position/finished': 40, + 'position/pending': 40, + 'position/funding': 40, + 'position/adl_history': 40, + 'market/preference': 40, + 'position/margin_history': 40, + 'position/settle_history': 40, + }, + 'post': { + 'market/adjust_leverage': 1, + 'market/position_expect': 1, + 'order/put_limit': 20, + 'order/put_market': 20, + 'order/put_stop_limit': 20, + 'order/put_stop_market': 20, + 'order/modify': 20, + 'order/modify_stop': 20, + 'order/cancel': 20, + 'order/cancel_all': 40, + 'order/cancel_batch': 40, + 'order/cancel_stop': 20, + 'order/cancel_stop_all': 40, + 'order/close_limit': 20, + 'order/close_market': 20, + 'position/adjust_margin': 20, + 'position/stop_loss': 20, + 'position/take_profit': 20, + 'position/market_close': 20, + 'order/cancel/by_client_id': 20, + 'order/cancel_stop/by_client_id': 20, + 'market/preference': 20, + }, + }, + }, + 'v2': { + 'public': { + 'get': { + 'maintain/info': 1, + 'ping': 1, + 'time': 1, + 'spot/market': 1, + 'spot/ticker': 1, + 'spot/depth': 1, + 'spot/deals': 1, + 'spot/kline': 1, + 'spot/index': 1, + 'futures/market': 1, + 'futures/ticker': 1, + 'futures/depth': 1, + 'futures/deals': 1, + 'futures/kline': 1, + 'futures/index': 1, + 'futures/funding-rate': 1, + 'futures/funding-rate-history': 1, + 'futures/position-level': 1, + 'futures/liquidation-history': 1, + 'futures/basis-history': 1, + 'assets/deposit-withdraw-config': 1, + 'assets/all-deposit-withdraw-config': 1, + }, + }, + 'private': { + 'get': { + 'account/subs': 1, + 'account/subs/api-detail': 40, + 'account/subs/info': 1, + 'account/subs/api': 40, + 'account/subs/transfer-history': 40, + 'account/subs/spot-balance': 1, + 'account/trade-fee-rate': 40, + 'assets/spot/balance': 40, + 'assets/futures/balance': 40, + 'assets/margin/balance': 1, + 'assets/financial/balance': 40, + 'assets/amm/liquidity': 40, + 'assets/credit/info': 40, + 'assets/margin/borrow-history': 40, + 'assets/margin/interest-limit': 1, + 'assets/deposit-address': 40, + 'assets/deposit-history': 40, + 'assets/withdraw': 40, + 'assets/transfer-history': 40, + 'spot/order-status': 8, + 'spot/batch-order-status': 8, + 'spot/pending-order': 8, + 'spot/finished-order': 40, + 'spot/pending-stop-order': 8, + 'spot/finished-stop-order': 40, + 'spot/user-deals': 40, + 'spot/order-deals': 40, + 'futures/order-status': 8, + 'futures/batch-order-status': 1, + 'futures/pending-order': 8, + 'futures/finished-order': 40, + 'futures/pending-stop-order': 8, + 'futures/finished-stop-order': 40, + 'futures/user-deals': 1, + 'futures/order-deals': 1, + 'futures/pending-position': 40, + 'futures/finished-position': 1, + 'futures/position-margin-history': 1, + 'futures/position-funding-history': 40, + 'futures/position-adl-history': 1, + 'futures/position-settle-history': 1, + }, + 'post': { + 'account/subs': 40, + 'account/subs/frozen': 40, + 'account/subs/unfrozen': 40, + 'account/subs/api': 40, + 'account/subs/edit-api': 40, + 'account/subs/delete-api': 40, + 'account/subs/transfer': 40, + 'account/settings': 40, + 'assets/margin/borrow': 40, + 'assets/margin/repay': 40, + 'assets/renewal-deposit-address': 40, + 'assets/withdraw': 40, + 'assets/cancel-withdraw': 40, + 'assets/transfer': 40, + 'assets/amm/add-liquidity': 1, + 'assets/amm/remove-liquidity': 1, + 'spot/order': 13.334, + 'spot/stop-order': 13.334, + 'spot/batch-order': 40, + 'spot/batch-stop-order': 1, + 'spot/modify-order': 13.334, + 'spot/modify-stop-order': 13.334, + 'spot/cancel-all-order': 1, + 'spot/cancel-order': 6.667, + 'spot/cancel-stop-order': 6.667, + 'spot/cancel-batch-order': 10, + 'spot/cancel-batch-stop-order': 10, + 'spot/cancel-order-by-client-id': 1, + 'spot/cancel-stop-order-by-client-id': 1, + 'futures/order': 20, + 'futures/stop-order': 20, + 'futures/batch-order': 1, + 'futures/batch-stop-order': 1, + 'futures/modify-order': 20, + 'futures/modify-stop-order': 20, + 'futures/cancel-all-order': 1, + 'futures/cancel-order': 10, + 'futures/cancel-stop-order': 10, + 'futures/cancel-batch-order': 20, + 'futures/cancel-batch-stop-order': 20, + 'futures/cancel-order-by-client-id': 1, + 'futures/cancel-stop-order-by-client-id': 1, + 'futures/close-position': 20, + 'futures/adjust-position-margin': 20, + 'futures/adjust-position-leverage': 20, + 'futures/set-position-stop-loss': 20, + 'futures/set-position-take-profit': 20, + }, + }, + }, + }, + 'fees': { + 'trading': { + 'maker': 0.001, + 'taker': 0.001, + }, + 'funding': { + 'withdraw': { + 'BCH': 0.0, + 'BTC': 0.001, + 'LTC': 0.001, + 'ETH': 0.001, + 'ZEC': 0.0001, + 'DASH': 0.0001, + }, + }, + }, + 'limits': { + 'amount': { + 'min': 0.001, + 'max': None, + }, + }, + 'options': { + 'brokerId': 'x-167673045', + 'createMarketBuyOrderRequiresPrice': True, + 'defaultType': 'spot', # spot, swap, margin + 'defaultSubType': 'linear', # linear, inverse + 'fetchDepositAddress': { + 'fillResponseFromRequest': True, + }, + 'accountsByType': { + 'spot': 'SPOT', + 'margin': 'MARGIN', + 'swap': 'FUTURES', + }, + 'accountsById': { + 'SPOT': 'spot', + 'MARGIN': 'margin', + 'FUTURES': 'swap', + }, + 'networks': { + 'BTC': 'BTC', + 'BEP20': 'BSC', + 'TRC20': 'TRC20', + 'ERC20': 'ERC20', + 'BRC20': 'BRC20', + 'SOL': 'SOL', + 'TON': 'TON', + 'BSV': 'BSV', + 'AVAXC': 'AVA_C', + 'AVAXX': 'AVA', + 'SUI': 'SUI', + 'ACA': 'ACA', + 'CHZ': 'CHILIZ', + 'ADA': 'ADA', + 'ARB': 'ARBITRUM', + 'ARBNOVA': 'ARBITRUM_NOVA', + 'OP': 'OPTIMISM', + 'APT': 'APTOS', + 'ATOM': 'ATOM', + 'FTM': 'FTM', + 'BCH': 'BCH', + 'ASTR': 'ASTR', + 'LTC': 'LTC', + 'MATIC': 'MATIC', + 'CRONOS': 'CRONOS', + 'DASH': 'DASH', + 'DOT': 'DOT', + 'ETC': 'ETC', + 'ETHW': 'ETHPOW', + 'FIL': 'FIL', + 'ZIL': 'ZIL', + 'DOGE': 'DOGE', + 'TIA': 'CELESTIA', + 'SEI': 'SEI', + 'XRP': 'XRP', + 'XMR': 'XMR', + # CSC, AE, BASE, AIPG, AKASH, POLKADOTASSETHUB ?, ALEO, STX, ALGO, ALPH, BLAST, AR, ARCH, ARDR, ARK, ARRR, MANTA, NTRN, LUNA, AURORA, AVAIL, ASC20, AVA, AYA, AZERO, BAN, BAND, BB, RUNES, BEAM, BELLSCOIN, BITCI, NEAR, AGORIC, BLOCX, BNC, BOBA, BRISE, KRC20, CANTO, CAPS, CCD, CELO, CFX, CHI, CKB, CLORE, CLV, CORE, CSPR, CTXC, DAG, DCR, DERO, DESO, DEFI, DGB, DNX, DOCK, DOGECHAIN, DYDX, DYMENSION, EGLD, ELA, ELF, ENJIN, EOSIO, ERG, ETN_SC, EVMOS, EWC, SGB, FACT, FB, FET, FIO, FIRO, NEO3, FLOW, FLARE, FLUX, LINEA, FREN, FSN, FB_BRC20, GLMR, GRIN, GRS, HACASH, HBAR, HERB, HIVE, MAPO, HMND, HNS, ZKSYNC, HTR, HUAHUA, MERLIN, ICP, ICX, INJ, IOST, IOTA, IOTX, IRIS, IRON, ONE, JOYSTREAM, KAI, KAR, KAS, KAVA, KCN, KDA, KLAY, KLY, KMD, KSM, KUB, KUJIRA, LAT, LBC, LUNC, LUKSO, MARS, METIS, MINA, MANTLE, MOB, MODE, MONA, MOVR, MTL, NEOX, NEXA, NIBI, NIMIQ, NMC, ONOMY, NRG, WAVES, NULS, OAS, OCTA, OLT, ONT, OORT, ORAI, OSMO, P3D, COMPOSABLE, PIVX, RON, POKT, POLYMESH, PRE_MARKET, PYI, QKC, QTUM, QUBIC, RSK, ROSE, ROUTE, RTM, THORCHAIN, RVN, RADIANT, SAGA, SALVIUM, SATOX, SC, SCP, _NULL, SCRT, SDN, RGBPP, SELF, SMH, SPACE, STARGAZE, STC, STEEM, STRATISEVM, STRD, STARKNET, SXP, SYS, TAIKO, TAO, TARA, TENET, THETA, TT, VENOM, VECHAIN, TOMO, VITE, VLX, VSYS, VTC, WAN, WAXP, WEMIX, XCH, XDC, XEC, XELIS, NEM, XHV, XLM, XNA, NANO, XPLA, XPR, XPRT, XRD, XTZ, XVG, XYM, ZANO, ZEC, ZEN, ZEPH, ZETA + }, + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': True, + 'triggerPrice': True, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': True, + 'selfTradePrevention': True, # todo: implement + 'iceberg': True, # todo implement + }, + 'createOrders': { + 'max': 5, + }, + 'fetchMyTrades': { + 'marginMode': True, + 'limit': 1000, + 'daysBack': None, + 'untilDays': 100000, + 'symbolRequired': True, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOpenOrders': { + 'marginMode': True, + 'limit': 1000, + 'trigger': True, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': { + 'marginMode': True, + 'limit': 1000, + 'daysBack': None, + 'daysBackCanceled': None, + 'untilDays': None, + 'trigger': True, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 1000, + }, + }, + 'forDerivatives': { + 'extends': 'spot', + 'createOrder': { + 'marginMode': True, + 'stopLossPrice': True, + 'takeProfitPrice': True, + }, + 'fetchOpenOrders': { + 'marginMode': False, + }, + 'fetchClosedOrders': { + 'marginMode': False, + }, + }, + 'swap': { + 'linear': { + 'extends': 'forDerivatives', + }, + 'inverse': { + 'extends': 'forDerivatives', + }, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'commonCurrencies': { + 'ACM': 'Actinium', + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'exact': { + # https://github.com/coinexcom/coinex_exchange_api/wiki/013error_code + '23': PermissionDenied, # IP Prohibited + '24': AuthenticationError, + '25': AuthenticationError, + '34': AuthenticationError, # Access id is expires + '35': ExchangeNotAvailable, # Service unavailable + '36': RequestTimeout, # Service timeout + '213': RateLimitExceeded, # Too many requests + '107': InsufficientFunds, + '158': PermissionDenied, # {"code":158,"data":{},"message":"API permission is not allowed"} + '600': OrderNotFound, + '601': InvalidOrder, + '602': InvalidOrder, + '606': InvalidOrder, + '3008': RequestTimeout, # Service busy, please try again later. + '3109': InsufficientFunds, # {"code":3109,"data":{},"message":"balance not enough"} + '3127': InvalidOrder, # The order quantity is below the minimum requirement. Please adjust the order quantity. + '3600': OrderNotFound, # {"code":3600,"data":{},"message":"Order not found"} + '3606': InvalidOrder, # The price difference between the order price and the latest price is too large. Please adjust the order amount accordingly. + '3610': ExchangeError, # Order cancellation prohibited during the Call Auction period. + '3612': InvalidOrder, # The est. ask price is lower than the current bottom ask price. Please reduce the amount. + '3613': InvalidOrder, # The est. bid price is higher than the current top bid price. Please reduce the amount. + '3614': InvalidOrder, # The deviation between your est. filled price and the index price. Please reduce the amount. + '3615': InvalidOrder, # The deviation between your order price and the index price is too high. Please adjust your order price and try again. + '3616': InvalidOrder, # The order price exceeds the current top bid price. Please adjust the order price and try again. + '3617': InvalidOrder, # The order price exceeds the current bottom ask price. Please adjust the order price and try again. + '3618': InvalidOrder, # The deviation between your order price and the index price is too high. Please adjust your order price and try again. + '3619': InvalidOrder, # The deviation between your order price and the trigger price is too high. Please adjust your order price and try again. + '3620': InvalidOrder, # Market order submission is temporarily unavailable due to insufficient depth in the current market + '3621': InvalidOrder, # This order can't be completely executed and has been canceled. + '3622': InvalidOrder, # This order can't be set Only and has been canceled. + '3627': InvalidOrder, # The current market depth is low, please reduce your order amount and try again. + '3628': InvalidOrder, # The current market depth is low, please reduce your order amount and try again. + '3629': InvalidOrder, # The current market depth is low, please reduce your order amount and try again. + '3632': InvalidOrder, # The order price exceeds the current top bid price. Please adjust the order price and try again. + '3633': InvalidOrder, # The order price exceeds the current bottom ask price. Please adjust the order price and try again. + '3634': InvalidOrder, # The deviation between your est. filled price and the index price is too high. Please reduce the amount and try again. + '3635': InvalidOrder, # The deviation between your est. filled price and the index price is too high. Please reduce the amount and try again. + '4001': ExchangeNotAvailable, # Service unavailable, please try again later. + '4002': RequestTimeout, # Service request timed out, please try again later. + '4003': ExchangeError, # Internal error, please contact customer service for help. + '4004': BadRequest, # Parameter error, please check whether the request parameters are abnormal. + '4005': AuthenticationError, # Abnormal access_id, please check whether the value passed by X-COINEX-KEY is normal. + '4006': AuthenticationError, # Signature verification failed, please check the signature according to the documentation instructions. + '4007': PermissionDenied, # IP address prohibited, please check whether the whitelist or export IP is normal. + '4008': AuthenticationError, # Abnormal X-COIN-SIGN value, please check. + '4009': ExchangeError, # Abnormal request method, please check. + '4010': ExchangeError, # Expired request, please try again later. + '4011': PermissionDenied, # User prohibited from accessing, please contact customer service for help. + '4017': ExchangeError, # Signature expired, please try again later. + '4115': AccountSuspended, # User prohibited from trading, please contact customer service for help. + '4117': BadSymbol, # Trading hasattr(self, prohibited) market, please try again later. + '4123': RateLimitExceeded, # Rate limit triggered. Please adjust your strategy and reduce the request rate. + '4130': ExchangeError, # Futures trading prohibited, please try again later. + '4158': ExchangeError, # Trading prohibited, please try again later. + '4213': RateLimitExceeded, # The request is too frequent, please try again later. + '4512': PermissionDenied, # Insufficient sub-account permissions, please check. + }, + 'broad': { + 'ip not allow visit': PermissionDenied, + 'service too busy': ExchangeNotAvailable, + 'Service is not available during funding fee settlement': OperationFailed, + }, + }, + }) + + async def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://docs.coinex.com/api/v2/assets/deposit-withdrawal/http/list-all-deposit-withdrawal-config + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = await self.v2PublicGetAssetsAllDepositWithdrawConfig(params) + # + # { + # "code": 0, + # "data": [ + # { + # "asset": { + # "ccy": "CET", + # "deposit_enabled": True, + # "withdraw_enabled": True, + # "inter_transfer_enabled": True, + # "is_st": False + # }, + # "chains": [ + # { + # "chain": "CSC", + # "min_deposit_amount": "0.8", + # "min_withdraw_amount": "8", + # "deposit_enabled": True, + # "withdraw_enabled": True, + # "deposit_delay_minutes": 0, + # "safe_confirmations": 10, + # "irreversible_confirmations": 20, + # "deflation_rate": "0", + # "withdrawal_fee": "0.026", + # "withdrawal_precision": 8, + # "memo": "", + # "is_memo_required_for_deposit": False, + # "explorer_asset_url": "" + # }, + # ] + # } + # ], + # "message": "OK" + # } + # + data = self.safe_list(response, 'data', []) + result: dict = {} + for i in range(0, len(data)): + coin = data[i] + asset = self.safe_dict(coin, 'asset', {}) + chains = self.safe_list(coin, 'chains', []) + currencyId = self.safe_string(asset, 'ccy') + if currencyId is None: + continue # coinex returns empty structures for some reason + code = self.safe_currency_code(currencyId) + canDeposit = self.safe_bool(asset, 'deposit_enabled') + canWithdraw = self.safe_bool(asset, 'withdraw_enabled') + firstChain = self.safe_dict(chains, 0, {}) + firstPrecisionString = self.parse_precision(self.safe_string(firstChain, 'withdrawal_precision')) + networks: dict = {} + for j in range(0, len(chains)): + chain = chains[j] + networkId = self.safe_string(chain, 'chain') + networkCode = self.network_id_to_code(networkId, code) + if networkId is None: + continue + precisionString = self.parse_precision(self.safe_string(chain, 'withdrawal_precision')) + feeString = self.safe_string(chain, 'withdrawal_fee') + minNetworkDepositString = self.safe_string(chain, 'min_deposit_amount') + minNetworkWithdrawString = self.safe_string(chain, 'min_withdraw_amount') + canDepositChain = self.safe_bool(chain, 'deposit_enabled') + canWithdrawChain = self.safe_bool(chain, 'withdraw_enabled') + network: dict = { + 'id': networkId, + 'network': networkCode, + 'name': None, + 'active': canDepositChain and canWithdrawChain, + 'deposit': canDepositChain, + 'withdraw': canWithdrawChain, + 'fee': self.parse_number(feeString), + 'precision': self.parse_number(precisionString), + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'deposit': { + 'min': self.parse_number(minNetworkDepositString), + 'max': None, + }, + 'withdraw': { + 'min': self.parse_number(minNetworkWithdrawString), + 'max': None, + }, + }, + 'info': chain, + } + networks[networkCode] = network + result[code] = self.safe_currency_structure({ + 'id': currencyId, + 'code': code, + 'name': None, + 'active': canDeposit and canWithdraw, + 'deposit': canDeposit, + 'withdraw': canWithdraw, + 'fee': None, + 'precision': self.parse_number(firstPrecisionString), + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'deposit': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + }, + 'networks': networks, + 'type': 'crypto', + 'info': coin, + }) + return result + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for coinex + + https://docs.coinex.com/api/v2/spot/market/http/list-market + https://docs.coinex.com/api/v2/futures/market/http/list-market + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + promisesUnresolved = [ + self.fetch_spot_markets(params), + self.fetch_contract_markets(params), + ] + promises = await asyncio.gather(*promisesUnresolved) + spotMarkets = promises[0] + swapMarkets = promises[1] + return self.array_concat(spotMarkets, swapMarkets) + + async def fetch_spot_markets(self, params) -> List[Market]: + response = await self.v2PublicGetSpotMarket(params) + # + # { + # "code": 0, + # "data": [ + # { + # "market": "BTCUSDT", + # "taker_fee_rate": "0.002", + # "maker_fee_rate": "0.002", + # "min_amount": "0.0005", + # "base_ccy": "BTC", + # "quote_ccy": "USDT", + # "base_ccy_precision": 8, + # "quote_ccy_precision": 2, + # "is_amm_available": True, + # "is_margin_available": True, + # "is_pre_trading_available": True, + # "is_api_trading_available": True + # } + # ], + # "message": "OK" + # } + # + markets = self.safe_list(response, 'data', []) + result = [] + for i in range(0, len(markets)): + market = markets[i] + id = self.safe_string(market, 'market') + baseId = self.safe_string(market, 'base_ccy') + quoteId = self.safe_string(market, 'quote_ccy') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + symbol = base + '/' + quote + result.append({ + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': self.safe_bool(market, 'is_margin_available'), + 'swap': False, + 'future': False, + 'option': False, + 'active': self.safe_bool(market, 'is_api_trading_available'), + 'contract': False, + 'linear': None, + 'inverse': None, + 'taker': self.safe_number(market, 'taker_fee_rate'), + 'maker': self.safe_number(market, 'maker_fee_rate'), + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(self.parse_precision(self.safe_string(market, 'base_ccy_precision'))), + 'price': self.parse_number(self.parse_precision(self.safe_string(market, 'quote_ccy_precision'))), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(market, 'min_amount'), + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + 'info': market, + }) + return result + + async def fetch_contract_markets(self, params): + response = await self.v2PublicGetFuturesMarket(params) + # + # { + # "code": 0, + # "data": [ + # { + # "base_ccy": "BTC", + # "base_ccy_precision": 8, + # "contract_type": "inverse", + # "leverage": ["1","2","3","5","8","10","15","20","30","50","100"], + # "maker_fee_rate": "0", + # "market": "BTCUSD", + # "min_amount": "10", + # "open_interest_volume": "2566879", + # "quote_ccy": "USD", + # "quote_ccy_precision": 2, + # "taker_fee_rate": "0" + # }, + # ], + # "message": "OK" + # } + # + markets = self.safe_list(response, 'data', []) + result = [] + for i in range(0, len(markets)): + entry = markets[i] + fees = self.fees + leverages = self.safe_list(entry, 'leverage', []) + subType = self.safe_string(entry, 'contract_type') + linear = (subType == 'linear') + inverse = (subType == 'inverse') + id = self.safe_string(entry, 'market') + baseId = self.safe_string(entry, 'base_ccy') + quoteId = self.safe_string(entry, 'quote_ccy') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + settleId = 'USDT' if (subType == 'linear') else baseId + settle = self.safe_currency_code(settleId) + symbol = base + '/' + quote + ':' + settle + leveragesLength = len(leverages) + result.append({ + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': 'swap', + 'spot': False, + 'margin': False, + 'swap': True, + 'future': False, + 'option': False, + 'active': None, + 'contract': True, + 'linear': linear, + 'inverse': inverse, + 'taker': fees['trading']['taker'], + 'maker': fees['trading']['maker'], + 'contractSize': self.parse_number('1'), + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(self.parse_precision(self.safe_string(entry, 'base_ccy_precision'))), + 'price': self.parse_number(self.parse_precision(self.safe_string(entry, 'quote_ccy_precision'))), + }, + 'limits': { + 'leverage': { + 'min': self.safe_number(leverages, 0), + 'max': self.safe_number(leverages, leveragesLength - 1), + }, + 'amount': { + 'min': self.safe_number(entry, 'min_amount'), + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + 'info': entry, + }) + return result + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # Spot fetchTicker, fetchTickers + # + # { + # "close": "62393.47", + # "high": "64106.41", + # "last": "62393.47", + # "low": "59650.01", + # "market": "BTCUSDT", + # "open": "61616.15", + # "period": 86400, + # "value": "28711273.4065667262", + # "volume": "461.76557205", + # "volume_buy": "11.41506354", + # "volume_sell": "7.3240169" + # } + # + # Swap fetchTicker, fetchTickers + # + # { + # "close": "62480.08", + # "high": "64100", + # "index_price": "62443.05", + # "last": "62480.08", + # "low": "59600", + # "mark_price": "62443.05", + # "market": "BTCUSDT", + # "open": "61679.98", + # "period": 86400, + # "value": "180226025.69791713065326633165", + # "volume": "2900.2218", + # "volume_buy": "7.3847", + # "volume_sell": "6.1249" + # } + # + marketType = 'swap' if ('mark_price' in ticker) else 'spot' + marketId = self.safe_string(ticker, 'market') + symbol = self.safe_symbol(marketId, market, None, marketType) + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': None, + 'datetime': None, + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': None, + 'bidVolume': self.safe_string(ticker, 'volume_buy'), + 'ask': None, + 'askVolume': self.safe_string(ticker, 'volume_sell'), + 'vwap': None, + 'open': self.safe_string(ticker, 'open'), + 'close': self.safe_string(ticker, 'close'), + 'last': self.safe_string(ticker, 'last'), + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': self.safe_string(ticker, 'volume'), + 'quoteVolume': None, + 'markPrice': self.safe_string(ticker, 'mark_price'), + 'indexPrice': self.safe_string(ticker, 'index_price'), + 'info': ticker, + }, market) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://docs.coinex.com/api/v2/spot/market/http/list-market-ticker + https://docs.coinex.com/api/v2/futures/market/http/list-market-ticker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + response = None + if market['swap']: + response = await self.v2PublicGetFuturesTicker(self.extend(request, params)) + else: + response = await self.v2PublicGetSpotTicker(self.extend(request, params)) + # + # Spot + # + # { + # "code": 0, + # "data": [ + # { + # "close": "62393.47", + # "high": "64106.41", + # "last": "62393.47", + # "low": "59650.01", + # "market": "BTCUSDT", + # "open": "61616.15", + # "period": 86400, + # "value": "28711273.4065667262", + # "volume": "461.76557205", + # "volume_buy": "11.41506354", + # "volume_sell": "7.3240169" + # } + # ], + # "message": "OK" + # } + # + # Swap + # + # { + # "code": 0, + # "data": [ + # { + # "close": "62480.08", + # "high": "64100", + # "index_price": "62443.05", + # "last": "62480.08", + # "low": "59600", + # "mark_price": "62443.05", + # "market": "BTCUSDT", + # "open": "61679.98", + # "period": 86400, + # "value": "180226025.69791713065326633165", + # "volume": "2900.2218", + # "volume_buy": "7.3847", + # "volume_sell": "6.1249" + # } + # ], + # "message": "OK" + # } + # + data = self.safe_list(response, 'data', []) + result = self.safe_dict(data, 0, {}) + return self.parse_ticker(result, market) + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://docs.coinex.com/api/v2/spot/market/http/list-market-ticker + https://docs.coinex.com/api/v2/futures/market/http/list-market-ticker + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + market = None + if symbols is not None: + symbol = self.safe_value(symbols, 0) + market = self.market(symbol) + marketType, query = self.handle_market_type_and_params('fetchTickers', market, params) + response = None + if marketType == 'swap': + response = await self.v2PublicGetFuturesTicker(query) + else: + response = await self.v2PublicGetSpotTicker(query) + # + # Spot + # + # { + # "code": 0, + # "data": [ + # { + # "close": "62393.47", + # "high": "64106.41", + # "last": "62393.47", + # "low": "59650.01", + # "market": "BTCUSDT", + # "open": "61616.15", + # "period": 86400, + # "value": "28711273.4065667262", + # "volume": "461.76557205", + # "volume_buy": "11.41506354", + # "volume_sell": "7.3240169" + # } + # ], + # "message": "OK" + # } + # + # Swap + # + # { + # "code": 0, + # "data": [ + # { + # "close": "62480.08", + # "high": "64100", + # "index_price": "62443.05", + # "last": "62480.08", + # "low": "59600", + # "mark_price": "62443.05", + # "market": "BTCUSDT", + # "open": "61679.98", + # "period": 86400, + # "value": "180226025.69791713065326633165", + # "volume": "2900.2218", + # "volume_buy": "7.3847", + # "volume_sell": "6.1249" + # } + # ], + # "message": "OK" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_tickers(data, symbols) + + async def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://docs.coinex.com/api/v2/common/http/time + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = await self.v2PublicGetTime(params) + # + # { + # "code": 0, + # "data": { + # "timestamp": 1711699867777 + # }, + # "message": "OK" + # } + # + data = self.safe_dict(response, 'data', {}) + return self.safe_integer(data, 'timestamp') + + async def fetch_order_book(self, symbol: str, limit: Int = 20, params={}): + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://docs.coinex.com/api/v2/spot/market/http/list-market-depth + https://docs.coinex.com/api/v2/futures/market/http/list-market-depth + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + if limit is None: + limit = 20 # default + request: dict = { + 'market': market['id'], + 'limit': limit, + 'interval': '0', + } + response = None + if market['swap']: + response = await self.v2PublicGetFuturesDepth(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "depth": { + # "asks": [ + # ["70851.94", "0.2119"], + # ["70851.95", "0.0004"], + # ["70851.96", "0.0004"] + # ], + # "bids": [ + # ["70851.93", "1.0314"], + # ["70850.93", "0.0021"], + # ["70850.42", "0.0306"] + # ], + # "checksum": 2956436260, + # "last": "70851.94", + # "updated_at": 1712824003252 + # }, + # "is_full": True, + # "market": "BTCUSDT" + # }, + # "message": "OK" + # } + # + else: + response = await self.v2PublicGetSpotDepth(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "depth": { + # "asks": [ + # ["70875.31", "0.28670282"], + # ["70875.32", "0.31008114"], + # ["70875.42", "0.05876653"] + # ], + # "bids": [ + # ["70855.3", "0.00632222"], + # ["70855.29", "0.36216834"], + # ["70855.17", "0.10166802"] + # ], + # "checksum": 2313816665, + # "last": "70857.19", + # "updated_at": 1712823790987 + # }, + # "is_full": True, + # "market": "BTCUSDT" + # }, + # "message": "OK" + # } + # + data = self.safe_dict(response, 'data', {}) + depth = self.safe_dict(data, 'depth', {}) + timestamp = self.safe_integer(depth, 'updated_at') + return self.parse_order_book(depth, symbol, timestamp) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # Spot and Swap fetchTrades(public) + # + # { + # "amount": "0.00049432", + # "created_at": 1713849825667, + # "deal_id": 4137517302, + # "price": "66251", + # "side": "buy" + # } + # + # Spot and Margin fetchMyTrades(private) + # + # { + # "amount": "0.00010087", + # "created_at": 1714618087585, + # "deal_id": 4161200602, + # "margin_market": "", + # "market": "BTCUSDT", + # "order_id": 117654919342, + # "price": "57464.04", + # "side": "sell" + # } + # + # Swap fetchMyTrades(private) + # + # { + # "deal_id": 1180222387, + # "created_at": 1714119054558, + # "market": "BTCUSDT", + # "side": "buy", + # "order_id": 136915589622, + # "price": "64376", + # "amount": "0.0001", + # "role": "taker", + # "fee": "0.0299", + # "fee_ccy": "USDT" + # } + # + timestamp = self.safe_integer(trade, 'created_at') + defaultType = self.safe_string(self.options, 'defaultType') + if market is not None: + defaultType = market['type'] + marketId = self.safe_string(trade, 'market') + market = self.safe_market(marketId, market, None, defaultType) + feeCostString = self.safe_string(trade, 'fee') + fee = None + if feeCostString is not None: + feeCurrencyId = self.safe_string(trade, 'fee_ccy') + feeCurrencyCode = self.safe_currency_code(feeCurrencyId) + fee = { + 'cost': feeCostString, + 'currency': feeCurrencyCode, + } + return self.safe_trade({ + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'id': self.safe_string(trade, 'deal_id'), + 'order': self.safe_string(trade, 'order_id'), + 'type': None, + 'side': self.safe_string(trade, 'side'), + 'takerOrMaker': self.safe_string(trade, 'role'), + 'price': self.safe_string(trade, 'price'), + 'amount': self.safe_string(trade, 'amount'), + 'cost': self.safe_string(trade, 'deal_money'), + 'fee': fee, + }, market) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of the most recent trades for a particular symbol + + https://docs.coinex.com/api/v2/spot/market/http/list-market-deals + https://docs.coinex.com/api/v2/futures/market/http/list-market-deals + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + # 'last_id': 0, + } + if limit is not None: + request['limit'] = limit + response = None + if market['swap']: + response = await self.v2PublicGetFuturesDeals(self.extend(request, params)) + else: + response = await self.v2PublicGetSpotDeals(self.extend(request, params)) + # + # Spot and Swap + # + # { + # "code": 0, + # "data": [ + # { + # "amount": "0.00049432", + # "created_at": 1713849825667, + # "deal_id": 4137517302, + # "price": "66251", + # "side": "buy" + # }, + # ], + # "message": "OK" + # } + # + return self.parse_trades(response['data'], market, since, limit) + + async def fetch_trading_fee(self, symbol: str, params={}) -> TradingFeeInterface: + """ + fetch the trading fees for a market + + https://docs.coinex.com/api/v2/spot/market/http/list-market + https://docs.coinex.com/api/v2/futures/market/http/list-market + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `fee structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + response = None + if market['spot']: + response = await self.v2PublicGetSpotMarket(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "base_ccy": "BTC", + # "base_ccy_precision": 8, + # "is_amm_available": False, + # "is_margin_available": True, + # "maker_fee_rate": "0.002", + # "market": "BTCUSDT", + # "min_amount": "0.0001", + # "quote_ccy": "USDT", + # "quote_ccy_precision": 2, + # "taker_fee_rate": "0.002" + # } + # ], + # "message": "OK" + # } + # + else: + response = await self.v2PublicGetFuturesMarket(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "base_ccy": "BTC", + # "base_ccy_precision": 8, + # "contract_type": "linear", + # "leverage": ["1","2","3","5","8","10","15","20","30","50","100"], + # "maker_fee_rate": "0", + # "market": "BTCUSDT", + # "min_amount": "0.0001", + # "open_interest_volume": "185.7498", + # "quote_ccy": "USDT", + # "quote_ccy_precision": 2, + # "taker_fee_rate": "0" + # } + # ], + # "message": "OK" + # } + # + data = self.safe_list(response, 'data', []) + result = self.safe_dict(data, 0, {}) + return self.parse_trading_fee(result, market) + + async def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://docs.coinex.com/api/v2/spot/market/http/list-market + https://docs.coinex.com/api/v2/futures/market/http/list-market + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + await self.load_markets() + type = None + type, params = self.handle_market_type_and_params('fetchTradingFees', None, params) + response = None + if type == 'swap': + response = await self.v2PublicGetFuturesMarket(params) + # + # { + # "code": 0, + # "data": [ + # { + # "base_ccy": "BTC", + # "base_ccy_precision": 8, + # "contract_type": "linear", + # "leverage": ["1","2","3","5","8","10","15","20","30","50","100"], + # "maker_fee_rate": "0", + # "market": "BTCUSDT", + # "min_amount": "0.0001", + # "open_interest_volume": "185.7498", + # "quote_ccy": "USDT", + # "quote_ccy_precision": 2, + # "taker_fee_rate": "0" + # } + # ], + # "message": "OK" + # } + # + else: + response = await self.v2PublicGetSpotMarket(params) + # + # { + # "code": 0, + # "data": [ + # { + # "base_ccy": "BTC", + # "base_ccy_precision": 8, + # "is_amm_available": False, + # "is_margin_available": True, + # "maker_fee_rate": "0.002", + # "market": "BTCUSDT", + # "min_amount": "0.0001", + # "quote_ccy": "USDT", + # "quote_ccy_precision": 2, + # "taker_fee_rate": "0.002" + # }, + # ], + # "message": "OK" + # } + # + data = self.safe_list(response, 'data', []) + result: dict = {} + for i in range(0, len(data)): + entry = data[i] + marketId = self.safe_string(entry, 'market') + market = self.safe_market(marketId, None, None, type) + symbol = market['symbol'] + result[symbol] = self.parse_trading_fee(entry, market) + return result + + def parse_trading_fee(self, fee: dict, market: Market = None) -> TradingFeeInterface: + marketId = self.safe_value(fee, 'market') + symbol = self.safe_symbol(marketId, market) + return { + 'info': fee, + 'symbol': symbol, + 'maker': self.safe_number(fee, 'maker_fee_rate'), + 'taker': self.safe_number(fee, 'taker_fee_rate'), + 'percentage': True, + 'tierBased': True, + } + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # { + # "close": "66999.95", + # "created_at": 1713934620000, + # "high": "66999.95", + # "low": "66988.53", + # "market": "BTCUSDT", + # "open": "66988.53", + # "value": "0.1572393", # base volume + # "volume": "10533.2501364336" # quote volume + # } + # + return [ + self.safe_integer(ohlcv, 'created_at'), + self.safe_number(ohlcv, 'open'), + self.safe_number(ohlcv, 'high'), + self.safe_number(ohlcv, 'low'), + self.safe_number(ohlcv, 'close'), + self.safe_number(ohlcv, 'value'), + ] + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://docs.coinex.com/api/v2/spot/market/http/list-market-kline + https://docs.coinex.com/api/v2/futures/market/http/list-market-kline + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + 'period': self.safe_string(self.timeframes, timeframe, timeframe), + } + if limit is not None: + request['limit'] = limit + response = None + if market['swap']: + response = await self.v2PublicGetFuturesKline(self.extend(request, params)) + else: + response = await self.v2PublicGetSpotKline(self.extend(request, params)) + # + # Spot and Swap + # + # { + # "code": 0, + # "data": [ + # { + # "close": "66999.95", + # "created_at": 1713934620000, + # "high": "66999.95", + # "low": "66988.53", + # "market": "BTCUSDT", + # "open": "66988.53", + # "value": "0.1572393", + # "volume": "10533.2501364336" + # }, + # ], + # "message": "OK" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_ohlcvs(data, market, timeframe, since, limit) + + async def fetch_margin_balance(self, params={}): + await self.load_markets() + response = await self.v2PrivateGetAssetsMarginBalance(params) + # + # { + # "data": [ + # { + # "margin_account": "BTCUSDT", + # "base_ccy": "BTC", + # "quote_ccy": "USDT", + # "available": { + # "base_ccy": "0.00000026", + # "quote_ccy": "0" + # }, + # "frozen": { + # "base_ccy": "0", + # "quote_ccy": "0" + # }, + # "repaid": { + # "base_ccy": "0", + # "quote_ccy": "0" + # }, + # "interest": { + # "base_ccy": "0", + # "quote_ccy": "0" + # }, + # "rik_rate": "", + # "liq_price": "" + # }, + # ], + # "code": 0, + # "message": "OK" + # } + # + result: dict = {'info': response} + balances = self.safe_list(response, 'data', []) + for i in range(0, len(balances)): + entry = balances[i] + free = self.safe_dict(entry, 'available', {}) + used = self.safe_dict(entry, 'frozen', {}) + loan = self.safe_dict(entry, 'repaid', {}) + interest = self.safe_dict(entry, 'interest', {}) + baseAccount = self.account() + baseCurrencyId = self.safe_string(entry, 'base_ccy') + baseCurrencyCode = self.safe_currency_code(baseCurrencyId) + baseAccount['free'] = self.safe_string(free, 'base_ccy') + baseAccount['used'] = self.safe_string(used, 'base_ccy') + baseDebt = self.safe_string(loan, 'base_ccy') + baseInterest = self.safe_string(interest, 'base_ccy') + baseAccount['debt'] = Precise.string_add(baseDebt, baseInterest) + result[baseCurrencyCode] = baseAccount + return self.safe_balance(result) + + async def fetch_spot_balance(self, params={}): + await self.load_markets() + response = await self.v2PrivateGetAssetsSpotBalance(params) + # + # { + # "code": 0, + # "data": [ + # { + # "available": "0.00000046", + # "ccy": "USDT", + # "frozen": "0" + # } + # ], + # "message": "OK" + # } + # + result: dict = {'info': response} + balances = self.safe_list(response, 'data', []) + for i in range(0, len(balances)): + entry = balances[i] + currencyId = self.safe_string(entry, 'ccy') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(entry, 'available') + account['used'] = self.safe_string(entry, 'frozen') + result[code] = account + return self.safe_balance(result) + + async def fetch_swap_balance(self, params={}): + await self.load_markets() + response = await self.v2PrivateGetAssetsFuturesBalance(params) + # + # { + # "code": 0, + # "data": [ + # { + # "available": "0.00000046", + # "ccy": "USDT", + # "frozen": "0", + # "margin": "0", + # "transferrable": "0.00000046", + # "unrealized_pnl": "0" + # } + # ], + # "message": "OK" + # } + # + result: dict = {'info': response} + balances = self.safe_list(response, 'data', []) + for i in range(0, len(balances)): + entry = balances[i] + currencyId = self.safe_string(entry, 'ccy') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(entry, 'available') + account['used'] = self.safe_string(entry, 'frozen') + result[code] = account + return self.safe_balance(result) + + async def fetch_financial_balance(self, params={}): + await self.load_markets() + response = await self.v2PrivateGetAssetsFinancialBalance(params) + # + # { + # "code": 0, + # "data": [ + # { + # "available": "0.00000046", + # "ccy": "USDT", + # "frozen": "0" + # } + # ], + # "message": "OK" + # } + # + result: dict = {'info': response} + balances = self.safe_list(response, 'data', []) + for i in range(0, len(balances)): + entry = balances[i] + currencyId = self.safe_string(entry, 'ccy') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(entry, 'available') + account['used'] = self.safe_string(entry, 'frozen') + result[code] = account + return self.safe_balance(result) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://docs.coinex.com/api/v2/assets/balance/http/get-spot-balance # spot + https://docs.coinex.com/api/v2/assets/balance/http/get-futures-balance # swap + https://docs.coinex.com/api/v2/assets/balance/http/get-marigin-balance # margin + https://docs.coinex.com/api/v2/assets/balance/http/get-financial-balance # financial + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: 'margin', 'swap', 'financial', or 'spot' + :returns dict: a `balance structure ` + """ + marketType = None + marketType, params = self.handle_market_type_and_params('fetchBalance', None, params) + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchBalance', params) + isMargin = (marginMode is not None) or (marketType == 'margin') + if marketType == 'swap': + return await self.fetch_swap_balance(params) + elif marketType == 'financial': + return await self.fetch_financial_balance(params) + elif isMargin: + return await self.fetch_margin_balance(params) + else: + return await self.fetch_spot_balance(params) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'rejected': 'rejected', + 'open': 'open', + 'not_deal': 'open', + 'part_deal': 'open', + 'done': 'closed', + 'cancel': 'canceled', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # Spot and Margin createOrder, createOrders, editOrder, cancelOrders, cancelOrder, fetchOpenOrders + # + # { + # "amount": "0.0001", + # "base_fee": "0", + # "ccy": "BTC", + # "client_id": "x-167673045-a0a3c6461459a801", + # "created_at": 1714114386250, + # "discount_fee": "0", + # "filled_amount": "0", + # "filled_value": "0", + # "last_fill_amount": "0", + # "last_fill_price": "0", + # "maker_fee_rate": "0.002", + # "market": "BTCUSDT", + # "market_type": "SPOT", + # "order_id": 117178743547, + # "price": "61000", + # "quote_fee": "0", + # "side": "buy", + # "taker_fee_rate": "0.002", + # "type": "limit", + # "unfilled_amount": "0.0001", + # "updated_at": 1714114386250 + # } + # + # Spot and Margin fetchClosedOrders + # + # { + # "order_id": 117180532345, + # "market": "BTCUSDT", + # "market_type": "SPOT", + # "side": "sell", + # "type": "market", + # "ccy": "BTC", + # "amount": "0.00015484", + # "price": "0", + # "client_id": "", + # "created_at": 1714116494219, + # "updated_at": 0, + # "base_fee": "0", + # "quote_fee": "0.0199931699632", + # "discount_fee": "0", + # "maker_fee_rate": "0", + # "taker_fee_rate": "0.002", + # "unfilled_amount": "0", + # "filled_amount": "0.00015484", + # "filled_value": "9.9965849816" + # } + # + # Spot, Margin and Swap trigger createOrder, createOrders, editOrder + # + # { + # "stop_id": 117180138153 + # } + # + # Swap createOrder, createOrders, editOrder, cancelOrders, cancelOrder, fetchOpenOrders, fetchClosedOrders, closePosition + # + # { + # "amount": "0.0001", + # "client_id": "x-167673045-1471b81d747080a0", + # "created_at": 1714116769986, + # "fee": "0", + # "fee_ccy": "USDT", + # "filled_amount": "0", + # "filled_value": "0", + # "last_filled_amount": "0", + # "last_filled_price": "0", + # "maker_fee_rate": "0.0003", + # "market": "BTCUSDT", + # "market_type": "FUTURES", + # "order_id": 136913377780, + # "price": "61000.42", + # "realized_pnl": "0", + # "side": "buy", + # "taker_fee_rate": "0.0005", + # "type": "limit", + # "unfilled_amount": "0.0001", + # "updated_at": 1714116769986 + # } + # + # Swap stopLossPrice and takeProfitPrice createOrder + # + # { + # "adl_level": 1, + # "ath_margin_size": "2.14586666", + # "ath_position_amount": "0.0001", + # "avg_entry_price": "64376", + # "bkr_price": "0", + # "close_avbl": "0.0001", + # "cml_position_value": "6.4376", + # "created_at": 1714119054558, + # "leverage": "3", + # "liq_price": "0", + # "maintenance_margin_rate": "0.005", + # "maintenance_margin_value": "0.03218632", + # "margin_avbl": "2.14586666", + # "margin_mode": "cross", + # "market": "BTCUSDT", + # "market_type": "FUTURES", + # "max_position_value": "6.4376", + # "open_interest": "0.0001", + # "position_id": 303884204, + # "position_margin_rate": "3.10624785634397912265", + # "realized_pnl": "-0.0032188", + # "settle_price": "64376", + # "settle_value": "6.4376", + # "side": "long", + # "stop_loss_price": "62000", + # "stop_loss_type": "latest_price", + # "take_profit_price": "0", + # "take_profit_type": "", + # "unrealized_pnl": "0", + # "updated_at": 1714119054559 + # } + # + # Swap fetchOrder + # + # { + # "amount": "0.0001", + # "client_id": "x-167673045-da5f31dcd478a829", + # "created_at": 1714460987164, + # "fee": "0", + # "fee_ccy": "USDT", + # "filled_amount": "0", + # "filled_value": "0", + # "last_filled_amount": "0", + # "last_filled_price": "0", + # "maker_fee_rate": "0.0003", + # "market": "BTCUSDT", + # "market_type": "FUTURES", + # "order_id": 137319868771, + # "price": "61000", + # "realized_pnl": "0", + # "side": "buy", + # "status": "open", + # "taker_fee_rate": "0.0005", + # "type": "limit", + # "unfilled_amount": "0.0001", + # "updated_at": 1714460987164 + # } + # + # Spot and Margin fetchOrder + # + # { + # "amount": "0.0001", + # "base_fee": "0", + # "ccy": "BTC", + # "client_id": "x-167673045-da918d6724e3af81", + # "created_at": 1714461638958, + # "discount_fee": "0", + # "filled_amount": "0", + # "filled_value": "0", + # "last_fill_amount": "0", + # "last_fill_price": "0", + # "maker_fee_rate": "0.002", + # "market": "BTCUSDT", + # "market_type": "SPOT", + # "order_id": 117492012985, + # "price": "61000", + # "quote_fee": "0", + # "side": "buy", + # "status": "open", + # "taker_fee_rate": "0.002", + # "type": "limit", + # "unfilled_amount": "0.0001", + # "updated_at": 1714461638958 + # } + # + # Swap trigger fetchOpenOrders, fetchClosedOrders - Spot and Swap trigger cancelOrders, cancelOrder + # + # { + # "amount": "0.0001", + # "client_id": "x-167673045-a7d7714c6478acf6", + # "created_at": 1714187923820, + # "market": "BTCUSDT", + # "market_type": "FUTURES", + # "price": "61000", + # "side": "buy", + # "stop_id": 136984426097, + # "trigger_direction": "higher", + # "trigger_price": "62000", + # "trigger_price_type": "latest_price", + # "type": "limit", + # "updated_at": 1714187974363 + # } + # + # Spot and Margin trigger fetchOpenOrders, fetchClosedOrders + # + # { + # "stop_id": 117586439530, + # "market": "BTCUSDT", + # "market_type": "SPOT", + # "ccy": "BTC", + # "side": "buy", + # "type": "limit", + # "amount": "0.0001", + # "price": "51000", + # "trigger_price": "52000", + # "trigger_direction": "higher", + # "trigger_price_type": "mark_price", + # "client_id": "x-167673045-df61777094c69312", + # "created_at": 1714551237335, + # "updated_at": 1714551237335 + # } + # + rawStatus = self.safe_string(order, 'status') + timestamp = self.safe_integer(order, 'created_at') + updatedTimestamp = self.safe_integer(order, 'updated_at') + if updatedTimestamp == 0: + updatedTimestamp = timestamp + marketId = self.safe_string(order, 'market') + defaultType = self.safe_string(self.options, 'defaultType') + orderType = self.safe_string_lower(order, 'market_type', defaultType) + if orderType == 'futures': + orderType = 'swap' + marketType = 'swap' if (orderType == 'swap') else 'spot' + market = self.safe_market(marketId, market, None, marketType) + feeCurrencyId = self.safe_string(order, 'fee_ccy') + feeCurrency = self.safe_currency_code(feeCurrencyId) + if feeCurrency is None: + feeCurrency = market['quote'] + side = self.safe_string(order, 'side') + if side == 'long': + side = 'buy' + elif side == 'short': + side = 'sell' + clientOrderId = self.safe_string(order, 'client_id') + if clientOrderId == '': + clientOrderId = None + return self.safe_order({ + 'id': self.safe_string_n(order, ['position_id', 'order_id', 'stop_id']), + 'clientOrderId': clientOrderId, + 'datetime': self.iso8601(timestamp), + 'timestamp': timestamp, + 'lastTradeTimestamp': updatedTimestamp, + 'status': self.parse_order_status(rawStatus), + 'symbol': market['symbol'], + 'type': self.safe_string(order, 'type'), + 'timeInForce': None, + 'postOnly': None, + 'reduceOnly': None, + 'side': side, + 'price': self.safe_string(order, 'price'), + 'triggerPrice': self.safe_string(order, 'trigger_price'), + 'takeProfitPrice': self.safe_number(order, 'take_profit_price'), + 'stopLossPrice': self.safe_number(order, 'stop_loss_price'), + 'cost': self.safe_string(order, 'filled_value'), + 'average': self.safe_string(order, 'avg_entry_price'), + 'amount': self.safe_string(order, 'amount'), + 'filled': self.safe_string(order, 'filled_amount'), + 'remaining': self.safe_string(order, 'unfilled_amount'), + 'trades': None, + 'fee': { + 'currency': feeCurrency, + 'cost': self.safe_string_2(order, 'quote_fee', 'fee'), + }, + 'info': order, + }, market) + + async def create_market_buy_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market buy order by providing the symbol and cost + + https://viabtc.github.io/coinex_api_en_doc/spot/#docsspot003_trade003_market_order + https://docs.coinex.com/api/v2/spot/order/http/put-order + + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' createMarketBuyOrderWithCost() supports spot orders only') + params['createMarketBuyOrderRequiresPrice'] = False + return await self.create_order(symbol, 'market', 'buy', cost, None, params) + + def create_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + market = self.market(symbol) + swap = market['swap'] + clientOrderId = self.safe_string_2(params, 'client_id', 'clientOrderId') + triggerPrice = self.safe_string_2(params, 'stopPrice', 'triggerPrice') + stopLossPrice = self.safe_string(params, 'stopLossPrice') + takeProfitPrice = self.safe_string(params, 'takeProfitPrice') + option = self.safe_string(params, 'option') + isMarketOrder = type == 'market' + postOnly = self.is_post_only(isMarketOrder, option == 'maker_only', params) + timeInForceRaw = self.safe_string_upper(params, 'timeInForce') + reduceOnly = self.safe_bool(params, 'reduceOnly') + if reduceOnly: + if not market['swap']: + raise InvalidOrder(self.id + ' createOrder() does not support reduceOnly for ' + market['type'] + ' orders, reduceOnly orders are supported for swap markets only') + request: dict = { + 'market': market['id'], + } + if clientOrderId is None: + defaultId = 'x-167673045' + brokerId = self.safe_string(self.options, 'brokerId', defaultId) + request['client_id'] = brokerId + '-' + self.uuid16() + else: + request['client_id'] = clientOrderId + if (stopLossPrice is None) and (takeProfitPrice is None): + if not reduceOnly: + request['side'] = side + requestType = type + if postOnly: + requestType = 'maker_only' + elif timeInForceRaw is not None: + if timeInForceRaw == 'IOC': + requestType = 'ioc' + elif timeInForceRaw == 'FOK': + requestType = 'fok' + if not isMarketOrder: + request['price'] = self.price_to_precision(symbol, price) + request['type'] = requestType + if swap: + request['market_type'] = 'FUTURES' + if stopLossPrice or takeProfitPrice: + if stopLossPrice: + request['stop_loss_price'] = self.price_to_precision(symbol, stopLossPrice) + request['stop_loss_type'] = self.safe_string(params, 'stop_type', 'latest_price') + elif takeProfitPrice: + request['take_profit_price'] = self.price_to_precision(symbol, takeProfitPrice) + request['take_profit_type'] = self.safe_string(params, 'stop_type', 'latest_price') + else: + request['amount'] = self.amount_to_precision(symbol, amount) + if triggerPrice is not None: + request['trigger_price'] = self.price_to_precision(symbol, triggerPrice) + request['trigger_price_type'] = self.safe_string(params, 'stop_type', 'latest_price') + else: + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('createOrder', params) + if marginMode is not None: + request['market_type'] = 'MARGIN' + else: + request['market_type'] = 'SPOT' + if (type == 'market') and (side == 'buy'): + createMarketBuyOrderRequiresPrice = True + createMarketBuyOrderRequiresPrice, params = self.handle_option_and_params(params, 'createOrder', 'createMarketBuyOrderRequiresPrice', True) + cost = self.safe_number(params, 'cost') + params = self.omit(params, 'cost') + if createMarketBuyOrderRequiresPrice: + if (price is None) and (cost is None): + raise InvalidOrder(self.id + ' createOrder() requires the price argument for market buy orders to calculate the total cost to spend(amount * price), alternatively set the createMarketBuyOrderRequiresPrice option or param to False and pass the cost to spend in the amount argument') + else: + amountString = self.number_to_string(amount) + priceString = self.number_to_string(price) + quoteAmount = self.parse_to_numeric(Precise.string_mul(amountString, priceString)) + costRequest = cost if (cost is not None) else quoteAmount + request['amount'] = self.cost_to_precision(symbol, costRequest) + else: + request['amount'] = self.cost_to_precision(symbol, amount) + else: + request['amount'] = self.amount_to_precision(symbol, amount) + if triggerPrice is not None: + request['trigger_price'] = self.price_to_precision(symbol, triggerPrice) + params = self.omit(params, ['reduceOnly', 'timeInForce', 'postOnly', 'stopPrice', 'triggerPrice', 'stopLossPrice', 'takeProfitPrice']) + return self.extend(request, params) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://docs.coinex.com/api/v2/spot/order/http/put-order + https://docs.coinex.com/api/v2/spot/order/http/put-stop-order + https://docs.coinex.com/api/v2/futures/order/http/put-order + https://docs.coinex.com/api/v2/futures/order/http/put-stop-order + https://docs.coinex.com/api/v2/futures/position/http/close-position + https://docs.coinex.com/api/v2/futures/position/http/set-position-stop-loss + https://docs.coinex.com/api/v2/futures/position/http/set-position-take-profit + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: price to trigger stop orders + :param float [params.stopLossPrice]: price to trigger stop loss orders + :param float [params.takeProfitPrice]: price to trigger take profit orders + :param str [params.timeInForce]: 'GTC', 'IOC', 'FOK', 'PO' + :param boolean [params.postOnly]: set to True if you wish to make a post only order + :param boolean [params.reduceOnly]: *contract only* indicates if self order is to reduce the size of a position + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + reduceOnly = self.safe_bool(params, 'reduceOnly') + triggerPrice = self.safe_string_2(params, 'stopPrice', 'triggerPrice') + stopLossTriggerPrice = self.safe_string(params, 'stopLossPrice') + takeProfitTriggerPrice = self.safe_string(params, 'takeProfitPrice') + isTriggerOrder = triggerPrice is not None + isStopLossTriggerOrder = stopLossTriggerPrice is not None + isTakeProfitTriggerOrder = takeProfitTriggerPrice is not None + isStopLossOrTakeProfitTrigger = isStopLossTriggerOrder or isTakeProfitTriggerOrder + request = self.create_order_request(symbol, type, side, amount, price, params) + response = None + if market['spot']: + if isTriggerOrder: + response = await self.v2PrivatePostSpotStopOrder(request) + # + # { + # "code": 0, + # "data": { + # "stop_id": 117180138153 + # }, + # "message": "OK" + # } + # + else: + response = await self.v2PrivatePostSpotOrder(request) + # + # { + # "code": 0, + # "data": { + # "amount": "0.0001", + # "base_fee": "0", + # "ccy": "BTC", + # "client_id": "x-167673045-a0a3c6461459a801", + # "created_at": 1714114386250, + # "discount_fee": "0", + # "filled_amount": "0", + # "filled_value": "0", + # "last_fill_amount": "0", + # "last_fill_price": "0", + # "maker_fee_rate": "0.002", + # "market": "BTCUSDT", + # "market_type": "SPOT", + # "order_id": 117178743547, + # "price": "61000", + # "quote_fee": "0", + # "side": "buy", + # "taker_fee_rate": "0.002", + # "type": "limit", + # "unfilled_amount": "0.0001", + # "updated_at": 1714114386250 + # }, + # "message": "OK" + # } + # + else: + if isTriggerOrder: + response = await self.v2PrivatePostFuturesStopOrder(request) + # + # { + # "code": 0, + # "data": { + # "stop_id": 136915460994 + # }, + # "message": "OK" + # } + # + elif isStopLossOrTakeProfitTrigger: + if isStopLossTriggerOrder: + response = await self.v2PrivatePostFuturesSetPositionStopLoss(request) + # + # { + # "code": 0, + # "data": { + # "adl_level": 1, + # "ath_margin_size": "2.14586666", + # "ath_position_amount": "0.0001", + # "avg_entry_price": "64376", + # "bkr_price": "0", + # "close_avbl": "0.0001", + # "cml_position_value": "6.4376", + # "created_at": 1714119054558, + # "leverage": "3", + # "liq_price": "0", + # "maintenance_margin_rate": "0.005", + # "maintenance_margin_value": "0.03218632", + # "margin_avbl": "2.14586666", + # "margin_mode": "cross", + # "market": "BTCUSDT", + # "market_type": "FUTURES", + # "max_position_value": "6.4376", + # "open_interest": "0.0001", + # "position_id": 303884204, + # "position_margin_rate": "3.10624785634397912265", + # "realized_pnl": "-0.0032188", + # "settle_price": "64376", + # "settle_value": "6.4376", + # "side": "long", + # "stop_loss_price": "62000", + # "stop_loss_type": "latest_price", + # "take_profit_price": "0", + # "take_profit_type": "", + # "unrealized_pnl": "0", + # "updated_at": 1714119054559 + # }, + # "message": "OK" + # } + # + elif isTakeProfitTriggerOrder: + response = await self.v2PrivatePostFuturesSetPositionTakeProfit(request) + # + # { + # "code": 0, + # "data": { + # "adl_level": 1, + # "ath_margin_size": "2.14586666", + # "ath_position_amount": "0.0001", + # "avg_entry_price": "64376", + # "bkr_price": "0", + # "close_avbl": "0.0001", + # "cml_position_value": "6.4376", + # "created_at": 1714119054558, + # "leverage": "3", + # "liq_price": "0", + # "maintenance_margin_rate": "0.005", + # "maintenance_margin_value": "0.03218632", + # "margin_avbl": "2.14586666", + # "margin_mode": "cross", + # "market": "BTCUSDT", + # "market_type": "FUTURES", + # "max_position_value": "6.4376", + # "open_interest": "0.0001", + # "position_id": 303884204, + # "position_margin_rate": "3.10624785634397912265", + # "realized_pnl": "-0.0032188", + # "settle_price": "64376", + # "settle_value": "6.4376", + # "side": "long", + # "stop_loss_price": "62000", + # "stop_loss_type": "latest_price", + # "take_profit_price": "70000", + # "take_profit_type": "latest_price", + # "unrealized_pnl": "0", + # "updated_at": 1714119054559 + # }, + # "message": "OK" + # } + # + else: + if reduceOnly: + response = await self.v2PrivatePostFuturesClosePosition(request) + # + # { + # "code": 0, + # "data": { + # "amount": "0.0001", + # "client_id": "x-167673045-4f264600c432ac06", + # "created_at": 1714119323764, + # "fee": "0.003221", + # "fee_ccy": "USDT", + # "filled_amount": "0.0001", + # "filled_value": "6.442017", + # "last_filled_amount": "0.0001", + # "last_filled_price": "64420.17", + # "maker_fee_rate": "0", + # "market": "BTCUSDT", + # "market_type": "FUTURES", + # "order_id": 136915813578, + # "price": "0", + # "realized_pnl": "0.004417", + # "side": "sell", + # "taker_fee_rate": "0.0005", + # "type": "market", + # "unfilled_amount": "0", + # "updated_at": 1714119323764 + # }, + # "message": "OK" + # } + # + else: + response = await self.v2PrivatePostFuturesOrder(request) + # + # { + # "code": 0, + # "data": { + # "amount": "0.0001", + # "client_id": "x-167673045-1471b81d747080a0", + # "created_at": 1714116769986, + # "fee": "0", + # "fee_ccy": "USDT", + # "filled_amount": "0", + # "filled_value": "0", + # "last_filled_amount": "0", + # "last_filled_price": "0", + # "maker_fee_rate": "0.0003", + # "market": "BTCUSDT", + # "market_type": "FUTURES", + # "order_id": 136913377780, + # "price": "61000.42", + # "realized_pnl": "0", + # "side": "buy", + # "taker_fee_rate": "0.0005", + # "type": "limit", + # "unfilled_amount": "0.0001", + # "updated_at": 1714116769986 + # }, + # "message": "OK" + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data, market) + + async def create_orders(self, orders: List[OrderRequest], params={}) -> List[Order]: + """ + create a list of trade orders(all orders should be of the same symbol) + + https://docs.coinex.com/api/v2/spot/order/http/put-multi-order + https://docs.coinex.com/api/v2/spot/order/http/put-multi-stop-order + https://docs.coinex.com/api/v2/futures/order/http/put-multi-order + https://docs.coinex.com/api/v2/futures/order/http/put-multi-stop-order + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the api endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + ordersRequests = [] + symbol = None + reduceOnly = False + isTriggerOrder = False + isStopLossOrTakeProfitTrigger = False + for i in range(0, len(orders)): + rawOrder = orders[i] + marketId = self.safe_string(rawOrder, 'symbol') + if symbol is None: + symbol = marketId + else: + if symbol != marketId: + raise BadRequest(self.id + ' createOrders() requires all orders to have the same symbol') + type = self.safe_string(rawOrder, 'type') + side = self.safe_string(rawOrder, 'side') + amount = self.safe_value(rawOrder, 'amount') + price = self.safe_value(rawOrder, 'price') + orderParams = self.safe_value(rawOrder, 'params', {}) + if type != 'limit': + raise NotSupported(self.id + ' createOrders() does not support ' + type + ' orders, only limit orders are accepted') + reduceOnly = self.safe_value(orderParams, 'reduceOnly') + triggerPrice = self.safe_number_2(orderParams, 'stopPrice', 'triggerPrice') + stopLossTriggerPrice = self.safe_number(orderParams, 'stopLossPrice') + takeProfitTriggerPrice = self.safe_number(orderParams, 'takeProfitPrice') + isTriggerOrder = triggerPrice is not None + isStopLossTriggerOrder = stopLossTriggerPrice is not None + isTakeProfitTriggerOrder = takeProfitTriggerPrice is not None + isStopLossOrTakeProfitTrigger = isStopLossTriggerOrder or isTakeProfitTriggerOrder + orderRequest = self.create_order_request(marketId, type, side, amount, price, orderParams) + ordersRequests.append(orderRequest) + market = self.market(symbol) + request: dict = { + 'market': market['id'], + 'orders': ordersRequests, + } + response = None + if market['spot']: + if isTriggerOrder: + response = await self.v2PrivatePostSpotBatchStopOrder(request) + # + # { + # "code": 0, + # "data": [ + # { + # "code": 0, + # "data": { + # "stop_id": 117186257510 + # }, + # "message": "OK" + # }, + # ], + # "message": "OK" + # } + # + else: + response = await self.v2PrivatePostSpotBatchOrder(request) + # + # { + # "code": 0, + # "data": [ + # { + # "amount": "0.0001", + # "base_fee": "0", + # "ccy": "BTC", + # "client_id": "x-167673045-f3651372049dab0d", + # "created_at": 1714121403450, + # "discount_fee": "0", + # "filled_amount": "0", + # "filled_value": "0", + # "last_fill_amount": "0", + # "last_fill_price": "0", + # "maker_fee_rate": "0.002", + # "market": "BTCUSDT", + # "market_type": "SPOT", + # "order_id": 117185362233, + # "price": "61000", + # "quote_fee": "0", + # "side": "buy", + # "taker_fee_rate": "0.002", + # "type": "limit", + # "unfilled_amount": "0.0001", + # "updated_at": 1714121403450 + # }, + # { + # "code": 3109, + # "data": null, + # "message": "balance not enough" + # } + # ], + # "message": "OK" + # } + # + else: + if isTriggerOrder: + response = await self.v2PrivatePostFuturesBatchStopOrder(request) + # + # { + # "code": 0, + # "data": [ + # { + # "code": 0, + # "data": { + # "stop_id": 136919625994 + # }, + # "message": "OK" + # }, + # ], + # "message": "OK" + # } + # + elif isStopLossOrTakeProfitTrigger: + raise NotSupported(self.id + ' createOrders() does not support stopLossPrice or takeProfitPrice orders') + else: + if reduceOnly: + raise NotSupported(self.id + ' createOrders() does not support reduceOnly orders') + else: + response = await self.v2PrivatePostFuturesBatchOrder(request) + # + # { + # "code": 0, + # "data": [ + # { + # "code": 0, + # "data": { + # "amount": "0.0001", + # "client_id": "x-167673045-2cb7436f3462a654", + # "created_at": 1714122832493, + # "fee": "0", + # "fee_ccy": "USDT", + # "filled_amount": "0", + # "filled_value": "0", + # "last_filled_amount": "0", + # "last_filled_price": "0", + # "maker_fee_rate": "0.0003", + # "market": "BTCUSDT", + # "market_type": "FUTURES", + # "order_id": 136918835063, + # "price": "61000", + # "realized_pnl": "0", + # "side": "buy", + # "taker_fee_rate": "0.0005", + # "type": "limit", + # "unfilled_amount": "0.0001", + # "updated_at": 1714122832493 + # }, + # "message": "OK" + # }, + # ], + # "message": "OK" + # } + # + data = self.safe_list(response, 'data', []) + results = [] + for i in range(0, len(data)): + entry = data[i] + status = None + code = self.safe_integer(entry, 'code') + if code is not None: + if code != 0: + status = 'rejected' + else: + status = 'open' + innerData = self.safe_dict(entry, 'data', {}) + order = None + if market['spot'] and not isTriggerOrder: + entry['status'] = status + order = self.parse_order(entry, market) + else: + innerData['status'] = status + order = self.parse_order(innerData, market) + results.append(order) + return results + + async def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): + """ + cancel multiple orders + + https://docs.coinex.com/api/v2/spot/order/http/cancel-batch-order + https://docs.coinex.com/api/v2/spot/order/http/cancel-batch-stop-order + https://docs.coinex.com/api/v2/futures/order/http/cancel-batch-order + https://docs.coinex.com/api/v2/futures/order/http/cancel-batch-stop-order + + :param str[] ids: order ids + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: set to True for canceling stop orders + :returns dict: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrders() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + trigger = self.safe_bool_2(params, 'stop', 'trigger') + params = self.omit(params, ['stop', 'trigger']) + response = None + requestIds = [] + for i in range(0, len(ids)): + requestIds.append(int(ids[i])) + if trigger: + request['stop_ids'] = requestIds + else: + request['order_ids'] = requestIds + if market['spot']: + if trigger: + response = await self.v2PrivatePostSpotCancelBatchStopOrder(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "code": 0, + # "data": { + # "amount": "0.0001", + # "ccy": "BTC", + # "client_id": "x-167673045-8e33d6f4a4bcb022", + # "created_at": 1714188827291, + # "market": "BTCUSDT", + # "market_type": "SPOT", + # "price": "61000", + # "side": "buy", + # "stop_id": 117248845854, + # "trigger_direction": "higher", + # "trigger_price": "62000", + # "trigger_price_type": "mark_price", + # "type": "limit", + # "updated_at": 1714188827291 + # }, + # "message": "OK" + # }, + # ], + # "message": "OK" + # } + # + else: + response = await self.v2PrivatePostSpotCancelBatchOrder(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "code": 0, + # "data": { + # "amount": "0.0001", + # "base_fee": "0", + # "ccy": "BTC", + # "client_id": "x-167673045-c1cc78e5b42d8c4e", + # "created_at": 1714188449497, + # "discount_fee": "0", + # "filled_amount": "0", + # "filled_value": "0", + # "last_fill_amount": "0", + # "last_fill_price": "0", + # "maker_fee_rate": "0.002", + # "market": "BTCUSDT", + # "market_type": "SPOT", + # "order_id": 117248494358, + # "price": "60000", + # "quote_fee": "0", + # "side": "buy", + # "taker_fee_rate": "0.002", + # "type": "limit", + # "unfilled_amount": "0.0001", + # "updated_at": 1714188449497 + # }, + # "message": "" + # }, + # ], + # "message": "OK" + # } + # + else: + request['market_type'] = 'FUTURES' + if trigger: + response = await self.v2PrivatePostFuturesCancelBatchStopOrder(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "code": 0, + # "data": { + # "amount": "0.0001", + # "client_id": "x-167673045-a7d7714c6478acf6", + # "created_at": 1714187923820, + # "market": "BTCUSDT", + # "market_type": "FUTURES", + # "price": "61000", + # "side": "buy", + # "stop_id": 136984426097, + # "trigger_direction": "higher", + # "trigger_price": "62000", + # "trigger_price_type": "latest_price", + # "type": "limit", + # "updated_at": 1714187974363 + # }, + # "message": "" + # }, + # ], + # "message": "OK" + # } + # + else: + response = await self.v2PrivatePostFuturesCancelBatchOrder(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "code": 0, + # "data": { + # "amount": "0.0001", + # "client_id": "x-167673045-9f80fde284339a72", + # "created_at": 1714187491784, + # "fee": "0", + # "fee_ccy": "USDT", + # "filled_amount": "0", + # "filled_value": "0", + # "last_filled_amount": "0", + # "last_filled_price": "0", + # "maker_fee_rate": "0.0003", + # "market": "BTCUSDT", + # "market_type": "FUTURES", + # "order_id": 136983851788, + # "price": "61000", + # "realized_pnl": "0", + # "side": "buy", + # "taker_fee_rate": "0.0005", + # "type": "limit", + # "unfilled_amount": "0.0001", + # "updated_at": 1714187567079 + # }, + # "message": "" + # }, + # ], + # "message": "OK" + # } + # + data = self.safe_list(response, 'data', []) + results = [] + for i in range(0, len(data)): + entry = data[i] + item = self.safe_dict(entry, 'data', {}) + order = self.parse_order(item, market) + results.append(order) + return results + + async def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + """ + edit a trade order + + https://docs.coinex.com/api/v2/spot/order/http/edit-order + https://docs.coinex.com/api/v2/spot/order/http/edit-stop-order + https://docs.coinex.com/api/v2/futures/order/http/edit-order + https://docs.coinex.com/api/v2/futures/order/http/edit-stop-order + + :param str id: order id + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of the currency you want to trade in units of the base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: the price to trigger stop orders + :returns dict: an `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' editOrder() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + if amount is not None: + request['amount'] = self.amount_to_precision(symbol, amount) + if price is not None: + request['price'] = self.price_to_precision(symbol, price) + response = None + triggerPrice = self.safe_string_n(params, ['stopPrice', 'triggerPrice', 'trigger_price']) + params = self.omit(params, ['stopPrice', 'triggerPrice']) + isTriggerOrder = triggerPrice is not None + if isTriggerOrder: + request['trigger_price'] = self.price_to_precision(symbol, triggerPrice) + request['stop_id'] = self.parse_to_numeric(id) + else: + request['order_id'] = self.parse_to_numeric(id) + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('editOrder', params) + if market['spot']: + if marginMode is not None: + request['market_type'] = 'MARGIN' + else: + request['market_type'] = 'SPOT' + if isTriggerOrder: + response = await self.v2PrivatePostSpotModifyStopOrder(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "stop_id": 117337235167 + # }, + # "message": "OK" + # } + # + else: + response = await self.v2PrivatePostSpotModifyOrder(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "amount": "0.0001", + # "base_fee": "0", + # "ccy": "BTC", + # "client_id": "x-167673045-87eb2bebf42882d8", + # "created_at": 1714290302047, + # "discount_fee": "0", + # "filled_amount": "0", + # "filled_value": "0", + # "last_fill_amount": "0", + # "last_fill_price": "0", + # "maker_fee_rate": "0.002", + # "market": "BTCUSDT", + # "market_type": "SPOT", + # "order_id": 117336922195, + # "price": "61000", + # "quote_fee": "0", + # "side": "buy", + # "status": "open", + # "taker_fee_rate": "0.002", + # "type": "limit", + # "unfilled_amount": "0.0001", + # "updated_at": 1714290191141 + # }, + # "message": "OK" + # } + # + else: + request['market_type'] = 'FUTURES' + if isTriggerOrder: + response = await self.v2PrivatePostFuturesModifyStopOrder(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "stop_id": 137091875605 + # }, + # "message": "OK" + # } + # + else: + response = await self.v2PrivatePostFuturesModifyOrder(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "amount": "0.0001", + # "client_id": "x-167673045-3f2d09191462b207", + # "created_at": 1714290927630, + # "fee": "0", + # "fee_ccy": "USDT", + # "filled_amount": "0", + # "filled_value": "0", + # "last_filled_amount": "0", + # "last_filled_price": "0", + # "maker_fee_rate": "0.0003", + # "market": "BTCUSDT", + # "market_type": "FUTURES", + # "order_id": 137091566717, + # "price": "61000", + # "realized_pnl": "0", + # "side": "buy", + # "taker_fee_rate": "0.0005", + # "type": "limit", + # "unfilled_amount": "0.0001", + # "updated_at": 1714290927630 + # }, + # "message": "OK" + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data, market) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://docs.coinex.com/api/v2/spot/order/http/cancel-order + https://docs.coinex.com/api/v2/spot/order/http/cancel-stop-order + https://docs.coinex.com/api/v2/spot/order/http/cancel-order-by-client-id + https://docs.coinex.com/api/v2/spot/order/http/cancel-stop-order-by-client-id + https://docs.coinex.com/api/v2/futures/order/http/cancel-order + https://docs.coinex.com/api/v2/futures/order/http/cancel-stop-order + https://docs.coinex.com/api/v2/futures/order/http/cancel-order-by-client-id + https://docs.coinex.com/api/v2/futures/order/http/cancel-stop-order-by-client-id + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.clientOrderId]: client order id, defaults to id if not passed + :param boolean [params.trigger]: set to True for canceling a trigger order + :returns dict: an `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + isTriggerOrder = self.safe_bool_2(params, 'stop', 'trigger') + swap = market['swap'] + request: dict = { + 'market': market['id'], + } + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('cancelOrder', params) + if swap: + request['market_type'] = 'FUTURES' + else: + if marginMode is not None: + request['market_type'] = 'MARGIN' + else: + request['market_type'] = 'SPOT' + clientOrderId = self.safe_string_2(params, 'client_id', 'clientOrderId') + params = self.omit(params, ['stop', 'trigger', 'clientOrderId']) + response = None + if clientOrderId is not None: + request['client_id'] = clientOrderId + if isTriggerOrder: + if swap: + response = await self.v2PrivatePostFuturesCancelStopOrderByClientId(self.extend(request, params)) + # { + # "code": 0, + # "data": [ + # { + # "code": 0, + # "data": { + # "amount": "0.0001", + # "client_id": "client01", + # "created_at": 1714368624473, + # "market": "BTCUSDT", + # "market_type": "FUTURES", + # "price": "61000", + # "side": "buy", + # "stop_id": 137175823891, + # "trigger_direction": "higher", + # "trigger_price": "61500", + # "trigger_price_type": "latest_price", + # "type": "limit", + # "updated_at": 1714368661444 + # }, + # "message": "" + # } + # ], + # "message": "OK" + # } + else: + response = await self.v2PrivatePostSpotCancelStopOrderByClientId(self.extend(request, params)) + # { + # "code" :0, + # "data": [ + # { + # "code": 0, + # "data": { + # "amount": "0.0001", + # "ccy": "BTC", + # "client_id": "client01", + # "created_at": 1714366950279, + # "market": "BTCUSDT", + # "market_type": "SPOT", + # "price": "61000", + # "side": "buy", + # "stop_id": 117402512706, + # "trigger_direction": "higher", + # "trigger_price": "61500", + # "trigger_price_type": "mark_price", + # "type": "limit", + # "updated_at": 1714366950279 + # }, + # "message": "OK" + # } + # ], + # "message": "OK" + # } + else: + if swap: + response = await self.v2PrivatePostFuturesCancelOrderByClientId(self.extend(request, params)) + # { + # "code": 0, + # "data": [ + # { + # "code": 0, + # "data": { + # "amount": "0.0001", + # "client_id": "x-167673045-bf60e24bb437a3df", + # "created_at": 1714368416437, + # "fee": "0", + # "fee_ccy": "USDT", + # "filled_amount": "0", + # "filled_value": "0", + # "last_filled_amount": "0", + # "last_filled_price": "0", + # "maker_fee_rate": "0.0003", + # "market": "BTCUSDT", + # "market_type": "FUTURES", + # "order_id": 137175616437, + # "price": "61000", + # "realized_pnl": "0", + # "side": "buy", + # "taker_fee_rate": "0.0005", + # "type": "limit", + # "unfilled_amount": "0.0001", + # "updated_at": 1714368507174 + # }, + # "message": "" + # } + # ], + # "message": "OK" + # } + else: + response = await self.v2PrivatePostSpotCancelOrderByClientId(self.extend(request, params)) + # { + # "code": 0, + # "data": [ + # { + # "code": 0, + # "data": { + # "amount": "0.0001", + # "base_fee": "0", + # "ccy": "BTC", + # "client_id": "x-167673045-d49eaca5f412afc8", + # "created_at": 1714366502807, + # "discount_fee": "0", + # "filled_amount": "0", + # "filled_value": "0", + # "last_fill_amount": "0", + # "last_fill_price": "0", + # "maker_fee_rate": "0.002", + # "market": "BTCUSDT", + # "market_type": "SPOT", + # "order_id": 117402157490, + # "price": "61000", + # "quote_fee": "0", + # "side": "buy", + # "taker_fee_rate": "0.002", + # "type": "limit", + # "unfilled_amount": "0.0001", + # "updated_at": 1714366502807 + # }, + # "message": "OK" + # } + # ], + # "message": "OK" + # } + else: + if isTriggerOrder: + request['stop_id'] = self.parse_to_numeric(id) + if swap: + response = await self.v2PrivatePostFuturesCancelStopOrder(self.extend(request, params)) + # { + # "code": 0, + # "data": { + # "amount": "0.0001", + # "ccy": "BTC", + # "client_id": "x-167673045-f21ecfd7542abf1f", + # "created_at": 1714366177334, + # "market": "BTCUSDT", + # "market_type": "SPOT", + # "price": "61000", + # "side": "buy", + # "stop_id": 117401897954, + # "trigger_direction": "higher", + # "trigger_price": "61500", + # "trigger_price_type": "mark_price", + # "type": "limit", + # "updated_at": 1714366177334 + # }, + # "message": "OK" + # } + else: + response = await self.v2PrivatePostSpotCancelStopOrder(self.extend(request, params)) + # { + # "code": 0, + # "data": { + # "amount": "0.0001", + # "ccy": "BTC", + # "client_id": "x-167673045-f21ecfd7542abf1f", + # "created_at": 1714366177334, + # "market": "BTCUSDT", + # "market_type": "SPOT", + # "price": "61000", + # "side": "buy", + # "stop_id": 117401897954, + # "trigger_direction": "higher", + # "trigger_price": "61500", + # "trigger_price_type": "mark_price", + # "type": "limit", + # "updated_at": 1714366177334 + # }, + # "message": "OK" + # } + else: + request['order_id'] = self.parse_to_numeric(id) + if swap: + response = await self.v2PrivatePostFuturesCancelOrder(self.extend(request, params)) + # { + # "code": 0, + # "data": { + # "amount": "0.0001", + # "client_id": "x-167673045-7f14381c74a98a85", + # "created_at": 1714367342024, + # "fee": "0", + # "fee_ccy": "USDT", + # "filled_amount": "0", + # "filled_value": "0", + # "last_filled_amount": "0", + # "last_filled_price": "0", + # "maker_fee_rate": "0.0003", + # "market": "BTCUSDT", + # "market_type": "FUTURES", + # "order_id": 137174472136, + # "price": "61000", + # "realized_pnl": "0", + # "side": "buy", + # "taker_fee_rate": "0.0005", + # "type": "limit", + # "unfilled_amount": "0.0001", + # "updated_at": 1714367515978 + # }, + # "message": "OK" + # } + else: + response = await self.v2PrivatePostSpotCancelOrder(self.extend(request, params)) + # { + # "code": 0, + # "data": { + # "amount": "0.0001", + # "base_fee": "0", + # "ccy": "BTC", + # "client_id": "x-167673045-86fbe37b54a2aea3", + # "created_at": 1714365277437, + # "discount_fee": "0", + # "filled_amount": "0", + # "filled_value": "0", + # "last_fill_amount": "0", + # "last_fill_price": "0", + # "maker_fee_rate": "0.002", + # "market": "BTCUSDT", + # "market_type": "SPOT", + # "order_id": 117401168172, + # "price": "61000", + # "quote_fee": "0", + # "side": "buy", + # "taker_fee_rate": "0.002", + # "type": "limit", + # "unfilled_amount": "0.0001", + # "updated_at": 1714365277437 + # }, + # "message": "OK" + # } + data = None + if clientOrderId is not None: + rows = self.safe_list(response, 'data', []) + data = self.safe_dict(rows[0], 'data', {}) + else: + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data, market) + + async def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders in a market + + https://docs.coinex.com/api/v2/spot/order/http/cancel-all-order + https://docs.coinex.com/api/v2/futures/order/http/cancel-all-order + + :param str symbol: unified market symbol of the market to cancel orders in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' for canceling spot margin orders + :returns dict[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelAllOrders() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + response = None + if market['swap']: + request['market_type'] = 'FUTURES' + response = await self.v2PrivatePostFuturesCancelAllOrder(self.extend(request, params)) + # + # {"code":0,"data":{},"message":"OK"} + # + else: + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('cancelAllOrders', params) + if marginMode is not None: + request['market_type'] = 'MARGIN' + else: + request['market_type'] = 'SPOT' + response = await self.v2PrivatePostSpotCancelAllOrder(self.extend(request, params)) + # + # {"code":0,"data":{},"message":"OK"} + # + return [ + self.safe_order({ + 'info': response, + }), + ] + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://docs.coinex.com/api/v2/spot/order/http/get-order-status + https://docs.coinex.com/api/v2/futures/order/http/get-order-status + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrder() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + 'order_id': self.parse_to_numeric(id), + } + response = None + if market['swap']: + response = await self.v2PrivateGetFuturesOrderStatus(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "amount": "0.0001", + # "client_id": "x-167673045-da5f31dcd478a829", + # "created_at": 1714460987164, + # "fee": "0", + # "fee_ccy": "USDT", + # "filled_amount": "0", + # "filled_value": "0", + # "last_filled_amount": "0", + # "last_filled_price": "0", + # "maker_fee_rate": "0.0003", + # "market": "BTCUSDT", + # "market_type": "FUTURES", + # "order_id": 137319868771, + # "price": "61000", + # "realized_pnl": "0", + # "side": "buy", + # "status": "open", + # "taker_fee_rate": "0.0005", + # "type": "limit", + # "unfilled_amount": "0.0001", + # "updated_at": 1714460987164 + # }, + # "message": "OK" + # } + # + else: + response = await self.v2PrivateGetSpotOrderStatus(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "amount": "0.0001", + # "base_fee": "0", + # "ccy": "BTC", + # "client_id": "x-167673045-da918d6724e3af81", + # "created_at": 1714461638958, + # "discount_fee": "0", + # "filled_amount": "0", + # "filled_value": "0", + # "last_fill_amount": "0", + # "last_fill_price": "0", + # "maker_fee_rate": "0.002", + # "market": "BTCUSDT", + # "market_type": "SPOT", + # "order_id": 117492012985, + # "price": "61000", + # "quote_fee": "0", + # "side": "buy", + # "status": "open", + # "taker_fee_rate": "0.002", + # "type": "limit", + # "unfilled_amount": "0.0001", + # "updated_at": 1714461638958 + # }, + # "message": "OK" + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data, market) + + async def fetch_orders_by_status(self, status, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch a list of orders + + https://docs.coinex.com/api/v2/spot/order/http/list-finished-order + https://docs.coinex.com/api/v2/spot/order/http/list-finished-stop-order + https://docs.coinex.com/api/v2/futures/order/http/list-finished-order + https://docs.coinex.com/api/v2/futures/order/http/list-finished-stop-order + + :param str status: order status to fetch for + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: set to True for fetching trigger orders + :param str [params.marginMode]: 'cross' or 'isolated' for fetching spot margin orders + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['market'] = market['id'] + if limit is not None: + request['limit'] = limit + trigger = self.safe_bool_2(params, 'stop', 'trigger') + params = self.omit(params, ['stop', 'trigger']) + marketType = None + marketType, params = self.handle_market_type_and_params('fetchOrdersByStatus', market, params) + response = None + isClosed = (status == 'finished') or (status == 'closed') + isOpen = (status == 'pending') or (status == 'open') + if marketType == 'swap': + request['market_type'] = 'FUTURES' + if isClosed: + if trigger: + response = await self.v2PrivateGetFuturesFinishedStopOrder(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "stop_id": 52431158859, + # "market": "BTCUSDT", + # "market_type": "FUTURES", + # "side": "sell", + # "type": "market", + # "amount": "0.0005", + # "price": "20599.64", + # "client_id": "", + # "created_at": 1667547909856, + # "updated_at": 1667547909856, + # "trigger_price": "20599.64", + # "trigger_price_type": "latest_price", + # "trigger_direction": "" + # }, + # ], + # "message": "OK", + # "pagination": { + # "has_next": False + # } + # } + # + else: + response = await self.v2PrivateGetFuturesFinishedOrder(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "order_id": 136915813578, + # "market": "BTCUSDT", + # "market_type": "FUTURES", + # "side": "sell", + # "type": "market", + # "amount": "0.0001", + # "price": "0", + # "client_id": "x-167673045-4f264600c432ac06", + # "created_at": 1714119323764, + # "updated_at": 1714119323764, + # "unfilled_amount": "0", + # "filled_amount": "0.0001", + # "filled_value": "6.442017", + # "fee": "0.003221", + # "fee_ccy": "USDT", + # "maker_fee_rate": "0", + # "taker_fee_rate": "0.0005" + # }, + # ], + # "message": "OK", + # "pagination": { + # "has_next": False + # } + # } + # + elif isOpen: + if trigger: + response = await self.v2PrivateGetFuturesPendingStopOrder(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "stop_id": 137481469849, + # "market": "BTCUSDT", + # "market_type": "FUTURES", + # "side": "buy", + # "type": "limit", + # "amount": "0.0001", + # "price": "51000", + # "client_id": "x-167673045-2b932341949fa2a1", + # "created_at": 1714552257876, + # "updated_at": 1714552257876, + # "trigger_price": "52000", + # "trigger_price_type": "latest_price", + # "trigger_direction": "higher" + # } + # ], + # "message": "OK", + # "pagination": { + # "total": 1, + # "has_next": False + # } + # } + # + else: + response = await self.v2PrivateGetFuturesPendingOrder(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "order_id": 137480580906, + # "market": "BTCUSDT", + # "market_type": "FUTURES", + # "side": "buy", + # "type": "limit", + # "amount": "0.0001", + # "price": "51000", + # "client_id": "", + # "created_at": 1714551877569, + # "updated_at": 1714551877569, + # "unfilled_amount": "0.0001", + # "filled_amount": "0", + # "filled_value": "0", + # "fee": "0", + # "fee_ccy": "USDT", + # "maker_fee_rate": "0.0003", + # "taker_fee_rate": "0.0005", + # "last_filled_amount": "0", + # "last_filled_price": "0", + # "realized_pnl": "0" + # } + # ], + # "message": "OK", + # "pagination": { + # "total": 1, + # "has_next": False + # } + # } + # + else: + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchOrdersByStatus', params) + if marginMode is not None: + request['market_type'] = 'MARGIN' + else: + request['market_type'] = 'SPOT' + if isClosed: + if trigger: + response = await self.v2PrivateGetSpotFinishedStopOrder(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "stop_id": 117654881420, + # "market": "BTCUSDT", + # "market_type": "SPOT", + # "ccy": "USDT", + # "side": "buy", + # "type": "market", + # "amount": "5.83325524", + # "price": "0", + # "trigger_price": "57418", + # "trigger_direction": "lower", + # "trigger_price_type": "mark_price", + # "client_id": "", + # "created_at": 1714618050597, + # "updated_at": 0 + # } + # ], + # "message": "OK", + # "pagination": { + # "has_next": False + # } + # } + # + else: + response = await self.v2PrivateGetSpotFinishedOrder(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "order_id": 117180532345, + # "market": "BTCUSDT", + # "market_type": "SPOT", + # "side": "sell", + # "type": "market", + # "ccy": "BTC", + # "amount": "0.00015484", + # "price": "0", + # "client_id": "", + # "created_at": 1714116494219, + # "updated_at": 0, + # "base_fee": "0", + # "quote_fee": "0.0199931699632", + # "discount_fee": "0", + # "maker_fee_rate": "0", + # "taker_fee_rate": "0.002", + # "unfilled_amount": "0", + # "filled_amount": "0.00015484", + # "filled_value": "9.9965849816" + # }, + # ], + # "message": "OK", + # "pagination": { + # "has_next": False + # } + # } + # + elif status == 'pending': + if trigger: + response = await self.v2PrivateGetSpotPendingStopOrder(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "stop_id": 117586439530, + # "market": "BTCUSDT", + # "market_type": "SPOT", + # "ccy": "BTC", + # "side": "buy", + # "type": "limit", + # "amount": "0.0001", + # "price": "51000", + # "trigger_price": "52000", + # "trigger_direction": "higher", + # "trigger_price_type": "mark_price", + # "client_id": "x-167673045-df61777094c69312", + # "created_at": 1714551237335, + # "updated_at": 1714551237335 + # } + # ], + # "message": "OK", + # "pagination": { + # "total": 1, + # "has_next": False + # } + # } + # + else: + response = await self.v2PrivateGetSpotPendingOrder(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "order_id": 117585921297, + # "market": "BTCUSDT", + # "market_type": "SPOT", + # "side": "buy", + # "type": "limit", + # "ccy": "BTC", + # "amount": "0.00011793", + # "price": "52000", + # "client_id": "", + # "created_at": 1714550707486, + # "updated_at": 1714550707486, + # "base_fee": "0", + # "quote_fee": "0", + # "discount_fee": "0", + # "maker_fee_rate": "0.002", + # "taker_fee_rate": "0.002", + # "last_fill_amount": "0", + # "last_fill_price": "0", + # "unfilled_amount": "0.00011793", + # "filled_amount": "0", + # "filled_value": "0" + # } + # ], + # "message": "OK", + # "pagination": { + # "total": 1, + # "has_next": False + # } + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_orders(data, market, since, limit) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://docs.coinex.com/api/v2/spot/order/http/list-pending-order + https://docs.coinex.com/api/v2/spot/order/http/list-pending-stop-order + https://docs.coinex.com/api/v2/futures/order/http/list-pending-order + https://docs.coinex.com/api/v2/futures/order/http/list-pending-stop-order + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: set to True for fetching trigger orders + :param str [params.marginMode]: 'cross' or 'isolated' for fetching spot margin orders + :returns Order[]: a list of `order structures ` + """ + openOrders = await self.fetch_orders_by_status('pending', symbol, since, limit, params) + for i in range(0, len(openOrders)): + openOrders[i]['status'] = 'open' + return openOrders + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://docs.coinex.com/api/v2/spot/order/http/list-finished-order + https://docs.coinex.com/api/v2/spot/order/http/list-finished-stop-order + https://docs.coinex.com/api/v2/futures/order/http/list-finished-order + https://docs.coinex.com/api/v2/futures/order/http/list-finished-stop-order + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: set to True for fetching trigger orders + :param str [params.marginMode]: 'cross' or 'isolated' for fetching spot margin orders + :returns Order[]: a list of `order structures ` + """ + return await self.fetch_orders_by_status('finished', symbol, since, limit, params) + + async def create_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + create a currency deposit address + + https://docs.coinex.com/api/v2/assets/deposit-withdrawal/http/update-deposit-address + + :param str code: unified currency code of the currency for the deposit address + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.network]: the blockchain network to create a deposit address on + :returns dict: an `address structure ` + """ + await self.load_markets() + currency = self.currency(code) + network = self.safe_string_2(params, 'chain', 'network') + if network is None: + raise ArgumentsRequired(self.id + ' createDepositAddress() requires a network parameter') + params = self.omit(params, 'network') + request: dict = { + 'ccy': currency['id'], + 'chain': self.network_code_to_id(network, currency['code']), + } + response = await self.v2PrivatePostAssetsRenewalDepositAddress(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "address": "0x321bd6479355142334f45653ad5d8b76105a1234", + # "memo": "" + # }, + # "message": "OK" + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_deposit_address(data, currency) + + async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://docs.coinex.com/api/v2/assets/deposit-withdrawal/http/get-deposit-address + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.network]: the blockchain network to create a deposit address on + :returns dict: an `address structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'ccy': currency['id'], + } + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + if networkCode is None: + raise ArgumentsRequired(self.id + ' fetchDepositAddress() requires a "network" parameter') + request['chain'] = self.network_code_to_id(networkCode) # required for on-chain, not required for inter-user transfer + response = await self.v2PrivateGetAssetsDepositAddress(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "address": "0x321bd6479355142334f45653ad5d8b76105a1234", + # "memo": "" + # }, + # "message": "OK" + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_deposit_address(data, currency) + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + # + # { + # "address": "1P1JqozxioQwaqPwgMAQdNDYNyaVSqgARq", + # "memo": "" + # } + # + coinAddress = self.safe_string(depositAddress, 'address') + parts = coinAddress.split(':') + address = None + tag = None + partsLength = len(parts) + if partsLength > 1 and parts[0] != 'cfx': + address = parts[0] + tag = parts[1] + else: + address = coinAddress + return { + 'info': depositAddress, + 'currency': self.safe_currency_code(None, currency), + 'network': None, + 'address': address, + 'tag': self.safe_string(depositAddress, 'memo', tag), + } + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://docs.coinex.com/api/v2/spot/deal/http/list-user-deals + https://docs.coinex.com/api/v2/futures/deal/http/list-user-deals + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trade structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest trades + :param str [params.side]: the side of the trades, either 'buy' or 'sell', required for swap + :returns Trade[]: a list of `trade structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMyTrades() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + if limit is not None: + request['limit'] = limit + if since is not None: + request['start_time'] = since + request, params = self.handle_until_option('end_time', request, params) + response = None + if market['swap']: + request['market_type'] = 'FUTURES' + response = await self.v2PrivateGetFuturesUserDeals(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "deal_id": 1180222387, + # "created_at": 1714119054558, + # "market": "BTCUSDT", + # "side": "buy", + # "order_id": 136915589622, + # "price": "64376", + # "amount": "0.0001" + # } + # ], + # "message": "OK", + # "pagination": { + # "has_next": True + # } + # } + # + else: + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchMyTrades', params) + if marginMode is not None: + request['market_type'] = 'MARGIN' + else: + request['market_type'] = 'SPOT' + response = await self.v2PrivateGetSpotUserDeals(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "amount": "0.00010087", + # "created_at": 1714618087585, + # "deal_id": 4161200602, + # "margin_market": "", + # "market": "BTCUSDT", + # "order_id": 117654919342, + # "price": "57464.04", + # "side": "sell" + # } + # ], + # "message": "OK", + # "pagination": { + # "has_next": True + # } + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_trades(data, market, since, limit) + + async def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://docs.coinex.com/api/v2/futures/position/http/list-pending-position + https://docs.coinex.com/api/v2/futures/position/http/list-finished-position + + :param str[] [symbols]: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.method]: the method to use 'v2PrivateGetFuturesPendingPosition' or 'v2PrivateGetFuturesFinishedPosition' default is 'v2PrivateGetFuturesPendingPosition' + :returns dict[]: a list of `position structure ` + """ + await self.load_markets() + defaultMethod = None + defaultMethod, params = self.handle_option_and_params(params, 'fetchPositions', 'method', 'v2PrivateGetFuturesPendingPosition') + symbols = self.market_symbols(symbols) + request: dict = { + 'market_type': 'FUTURES', + } + market = None + if symbols is not None: + symbol = None + if isinstance(symbols, list): + symbolsLength = len(symbols) + if symbolsLength > 1: + raise BadRequest(self.id + ' fetchPositions() symbols argument cannot contain more than 1 symbol') + symbol = symbols[0] + else: + symbol = symbols + market = self.market(symbol) + request['market'] = market['id'] + response = None + if defaultMethod == 'v2PrivateGetFuturesPendingPosition': + response = await self.v2PrivateGetFuturesPendingPosition(self.extend(request, params)) + else: + response = await self.v2PrivateGetFuturesFinishedPosition(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "position_id": 305891033, + # "market": "BTCUSDT", + # "market_type": "FUTURES", + # "side": "long", + # "margin_mode": "cross", + # "open_interest": "0.0001", + # "close_avbl": "0.0001", + # "ath_position_amount": "0.0001", + # "unrealized_pnl": "0", + # "realized_pnl": "-0.00311684", + # "avg_entry_price": "62336.8", + # "cml_position_value": "6.23368", + # "max_position_value": "6.23368", + # "created_at": 1715152208041, + # "updated_at": 1715152208041, + # "take_profit_price": "0", + # "stop_loss_price": "0", + # "take_profit_type": "", + # "stop_loss_type": "", + # "settle_price": "62336.8", + # "settle_value": "6.23368", + # "leverage": "3", + # "margin_avbl": "2.07789333", + # "ath_margin_size": "2.07789333", + # "position_margin_rate": "2.40545879023305655728", + # "maintenance_margin_rate": "0.005", + # "maintenance_margin_value": "0.03118094", + # "liq_price": "0", + # "bkr_price": "0", + # "adl_level": 1 + # } + # ], + # "message": "OK", + # "pagination": { + # "has_next": False + # } + # } + # + position = self.safe_list(response, 'data', []) + result = [] + for i in range(0, len(position)): + result.append(self.parse_position(position[i], market)) + return self.filter_by_array_positions(result, 'symbol', symbols, False) + + async def fetch_position(self, symbol: str, params={}): + """ + fetch data on a single open contract trade position + + https://docs.coinex.com/api/v2/futures/position/http/list-pending-position + + :param str symbol: unified market symbol of the market the position is held in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `position structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'market_type': 'FUTURES', + 'market': market['id'], + } + response = await self.v2PrivateGetFuturesPendingPosition(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "position_id": 305891033, + # "market": "BTCUSDT", + # "market_type": "FUTURES", + # "side": "long", + # "margin_mode": "cross", + # "open_interest": "0.0001", + # "close_avbl": "0.0001", + # "ath_position_amount": "0.0001", + # "unrealized_pnl": "0", + # "realized_pnl": "-0.00311684", + # "avg_entry_price": "62336.8", + # "cml_position_value": "6.23368", + # "max_position_value": "6.23368", + # "created_at": 1715152208041, + # "updated_at": 1715152208041, + # "take_profit_price": "0", + # "stop_loss_price": "0", + # "take_profit_type": "", + # "stop_loss_type": "", + # "settle_price": "62336.8", + # "settle_value": "6.23368", + # "leverage": "3", + # "margin_avbl": "2.07789333", + # "ath_margin_size": "2.07789333", + # "position_margin_rate": "2.40545879023305655728", + # "maintenance_margin_rate": "0.005", + # "maintenance_margin_value": "0.03118094", + # "liq_price": "0", + # "bkr_price": "0", + # "adl_level": 1 + # } + # ], + # "message": "OK", + # "pagination": { + # "has_next": False + # } + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_position(data[0], market) + + def parse_position(self, position: dict, market: Market = None): + # + # { + # "position_id": 305891033, + # "market": "BTCUSDT", + # "market_type": "FUTURES", + # "side": "long", + # "margin_mode": "cross", + # "open_interest": "0.0001", + # "close_avbl": "0.0001", + # "ath_position_amount": "0.0001", + # "unrealized_pnl": "0", + # "realized_pnl": "-0.00311684", + # "avg_entry_price": "62336.8", + # "cml_position_value": "6.23368", + # "max_position_value": "6.23368", + # "created_at": 1715152208041, + # "updated_at": 1715152208041, + # "take_profit_price": "0", + # "stop_loss_price": "0", + # "take_profit_type": "", + # "stop_loss_type": "", + # "settle_price": "62336.8", + # "settle_value": "6.23368", + # "leverage": "3", + # "margin_avbl": "2.07789333", + # "ath_margin_size": "2.07789333", + # "position_margin_rate": "2.40545879023305655728", + # "maintenance_margin_rate": "0.005", + # "maintenance_margin_value": "0.03118094", + # "liq_price": "0", + # "bkr_price": "0", + # "adl_level": 1 + # } + # + marketId = self.safe_string(position, 'market') + market = self.safe_market(marketId, market, None, 'swap') + timestamp = self.safe_integer(position, 'created_at') + return self.safe_position({ + 'info': position, + 'id': self.safe_integer(position, 'position_id'), + 'symbol': market['symbol'], + 'notional': self.safe_number(position, 'settle_value'), + 'marginMode': self.safe_string(position, 'margin_mode'), + 'liquidationPrice': self.safe_number(position, 'liq_price'), + 'entryPrice': self.safe_number(position, 'avg_entry_price'), + 'unrealizedPnl': self.safe_number(position, 'unrealized_pnl'), + 'realizedPnl': self.safe_number(position, 'realized_pnl'), + 'percentage': None, + 'contracts': self.safe_number(position, 'close_avbl'), + 'contractSize': self.safe_number(market, 'contractSize'), + 'markPrice': None, + 'lastPrice': None, + 'side': self.safe_string(position, 'side'), + 'hedged': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastUpdateTimestamp': self.safe_integer(position, 'updated_at'), + 'maintenanceMargin': self.safe_number(position, 'maintenance_margin_value'), + 'maintenanceMarginPercentage': self.safe_number(position, 'maintenance_margin_rate'), + 'collateral': self.safe_number(position, 'margin_avbl'), + 'initialMargin': None, + 'initialMarginPercentage': None, + 'leverage': self.safe_number(position, 'leverage'), + 'marginRatio': self.safe_number(position, 'position_margin_rate'), + 'stopLossPrice': self.omit_zero(self.safe_string(position, 'stop_loss_price')), + 'takeProfitPrice': self.omit_zero(self.safe_string(position, 'take_profit_price')), + }) + + async def set_margin_mode(self, marginMode: str, symbol: Str = None, params={}): + """ + set margin mode to 'cross' or 'isolated' + + https://docs.coinex.com/api/v2/futures/position/http/adjust-position-leverage + + :param str marginMode: 'cross' or 'isolated' + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int params['leverage']: the rate of leverage + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setMarginMode() requires a symbol argument') + marginMode = marginMode.lower() + if marginMode != 'isolated' and marginMode != 'cross': + raise BadRequest(self.id + ' setMarginMode() marginMode argument should be isolated or cross') + await self.load_markets() + market = self.market(symbol) + if market['type'] != 'swap': + raise BadSymbol(self.id + ' setMarginMode() supports swap contracts only') + leverage = self.safe_integer(params, 'leverage') + maxLeverage = self.safe_integer(market['limits']['leverage'], 'max', 100) + if leverage is None: + raise ArgumentsRequired(self.id + ' setMarginMode() requires a leverage parameter') + if (leverage < 1) or (leverage > maxLeverage): + raise BadRequest(self.id + ' setMarginMode() leverage should be between 1 and ' + str(maxLeverage) + ' for ' + symbol) + request: dict = { + 'market': market['id'], + 'market_type': 'FUTURES', + 'margin_mode': marginMode, + 'leverage': leverage, + } + return await self.v2PrivatePostFuturesAdjustPositionLeverage(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "leverage": 1, + # "margin_mode": "isolated" + # }, + # "message": "OK" + # } + # + + async def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + + https://docs.coinex.com/api/v2/futures/position/http/adjust-position-leverage + + set the level of leverage for a market + :param float leverage: the rate of leverage + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated'(default is 'cross') + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setLeverage() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + if not market['swap']: + raise BadSymbol(self.id + ' setLeverage() supports swap contracts only') + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('setLeverage', params, 'cross') + minLeverage = self.safe_integer(market['limits']['leverage'], 'min', 1) + maxLeverage = self.safe_integer(market['limits']['leverage'], 'max', 100) + if (leverage < minLeverage) or (leverage > maxLeverage): + raise BadRequest(self.id + ' setLeverage() leverage should be between ' + str(minLeverage) + ' and ' + str(maxLeverage) + ' for ' + symbol) + request: dict = { + 'market': market['id'], + 'market_type': 'FUTURES', + 'margin_mode': marginMode, + 'leverage': leverage, + } + return await self.v2PrivatePostFuturesAdjustPositionLeverage(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "leverage": 1, + # "margin_mode": "isolated" + # }, + # "message": "OK" + # } + # + + async def fetch_leverage_tiers(self, symbols: Strings = None, params={}) -> LeverageTiers: + """ + retrieve information on the maximum leverage, and maintenance margin for trades of varying trade sizes + + https://docs.coinex.com/api/v2/futures/market/http/list-market-position-level + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `leverage tiers structures `, indexed by market symbols + """ + await self.load_markets() + request: dict = {} + if symbols is not None: + marketIds = self.market_ids(symbols) + request['market'] = ','.join(marketIds) + response = await self.v2PublicGetFuturesPositionLevel(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "level": [ + # { + # "amount": "20001", + # "leverage": "20", + # "maintenance_margin_rate": "0.02", + # "min_initial_margin_rate": "0.05" + # }, + # { + # "amount": "50001", + # "leverage": "10", + # "maintenance_margin_rate": "0.04", + # "min_initial_margin_rate": "0.1" + # }, + # ], + # "market": "MINAUSDT" + # }, + # ], + # "message": "OK" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_leverage_tiers(data, symbols, 'market') + + def parse_market_leverage_tiers(self, info, market: Market = None) -> List[LeverageTier]: + tiers = [] + brackets = self.safe_list(info, 'level', []) + minNotional = 0 + for i in range(0, len(brackets)): + tier = brackets[i] + marketId = self.safe_string(info, 'market') + market = self.safe_market(marketId, market, None, 'swap') + maxNotional = self.safe_number(tier, 'amount') + tiers.append({ + 'tier': self.sum(i, 1), + 'symbol': self.safe_symbol(marketId, market, None, 'swap'), + 'currency': market['base'] if market['linear'] else market['quote'], + 'minNotional': minNotional, + 'maxNotional': maxNotional, + 'maintenanceMarginRate': self.safe_number(tier, 'maintenance_margin_rate'), + 'maxLeverage': self.safe_integer(tier, 'leverage'), + 'info': tier, + }) + minNotional = maxNotional + return tiers + + async def modify_margin_helper(self, symbol: str, amount, addOrReduce, params={}): + await self.load_markets() + market = self.market(symbol) + rawAmount = self.amount_to_precision(symbol, amount) + requestAmount = rawAmount + if addOrReduce == 'reduce': + requestAmount = Precise.string_neg(rawAmount) + request: dict = { + 'market': market['id'], + 'market_type': 'FUTURES', + 'amount': requestAmount, + } + response = await self.v2PrivatePostFuturesAdjustPositionMargin(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "adl_level": 1, + # "ath_margin_size": "2.034928", + # "ath_position_amount": "0.0001", + # "avg_entry_price": "61047.84", + # "bkr_price": "30698.5600000000000004142", + # "close_avbl": "0.0001", + # "cml_position_value": "6.104784", + # "created_at": 1715488472908, + # "leverage": "3", + # "liq_price": "30852.82412060301507579316", + # "maintenance_margin_rate": "0.005", + # "maintenance_margin_value": "0.03051465", + # "margin_avbl": "3.034928", + # "margin_mode": "isolated", + # "market": "BTCUSDT", + # "market_type": "FUTURES", + # "max_position_value": "6.104784", + # "open_interest": "0.0001", + # "position_id": 306458800, + # "position_margin_rate": "0.49713929272518077625", + # "realized_pnl": "-0.003052392", + # "settle_price": "61047.84", + # "settle_value": "6.104784", + # "side": "long", + # "stop_loss_price": "0", + # "stop_loss_type": "", + # "take_profit_price": "0", + # "take_profit_type": "", + # "unrealized_pnl": "0", + # "updated_at": 1715488805563 + # }, + # "message": "OK" + # } + # + data = self.safe_dict(response, 'data') + status = self.safe_string_lower(response, 'message') + type = 'reduce' if (addOrReduce == 'reduce') else 'add' + return self.extend(self.parse_margin_modification(data, market), { + 'type': type, + 'amount': self.parse_number(amount), + 'status': status, + }) + + def parse_margin_modification(self, data: dict, market: Market = None) -> MarginModification: + # + # addMargin/reduceMargin + # + # { + # "adl_level": 1, + # "ath_margin_size": "2.034928", + # "ath_position_amount": "0.0001", + # "avg_entry_price": "61047.84", + # "bkr_price": "30698.5600000000000004142", + # "close_avbl": "0.0001", + # "cml_position_value": "6.104784", + # "created_at": 1715488472908, + # "leverage": "3", + # "liq_price": "30852.82412060301507579316", + # "maintenance_margin_rate": "0.005", + # "maintenance_margin_value": "0.03051465", + # "margin_avbl": "3.034928", + # "margin_mode": "isolated", + # "market": "BTCUSDT", + # "market_type": "FUTURES", + # "max_position_value": "6.104784", + # "open_interest": "0.0001", + # "position_id": 306458800, + # "position_margin_rate": "0.49713929272518077625", + # "realized_pnl": "-0.003052392", + # "settle_price": "61047.84", + # "settle_value": "6.104784", + # "side": "long", + # "stop_loss_price": "0", + # "stop_loss_type": "", + # "take_profit_price": "0", + # "take_profit_type": "", + # "unrealized_pnl": "0", + # "updated_at": 1715488805563 + # } + # + # fetchMarginAdjustmentHistory + # + # { + # "bkr_pirce": "24698.56000000000000005224", + # "created_at": 1715489978697, + # "leverage": "3", + # "liq_price": "24822.67336683417085432386", + # "margin_avbl": "3.634928", + # "margin_change": "-1.5", + # "margin_mode": "isolated", + # "market": "BTCUSDT", + # "market_type": "FUTURES", + # "open_interest": "0.0001", + # "position_id": 306458800, + # "settle_price": "61047.84" + # } + # + marketId = self.safe_string(data, 'market') + timestamp = self.safe_integer_2(data, 'updated_at', 'created_at') + change = self.safe_string(data, 'margin_change') + return { + 'info': data, + 'symbol': self.safe_symbol(marketId, market, None, 'swap'), + 'type': None, + 'marginMode': 'isolated', + 'amount': self.parse_number(Precise.string_abs(change)), + 'total': self.safe_number(data, 'margin_avbl'), + 'code': market['quote'], + 'status': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + + async def add_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + add margin + + https://docs.coinex.com/api/v2/futures/position/http/adjust-position-margin + + :param str symbol: unified market symbol + :param float amount: amount of margin to add + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin structure ` + """ + return await self.modify_margin_helper(symbol, amount, 'add', params) + + async def reduce_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + remove margin from a position + + https://docs.coinex.com/api/v2/futures/position/http/adjust-position-margin + + :param str symbol: unified market symbol + :param float amount: the amount of margin to remove + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin structure ` + """ + return await self.modify_margin_helper(symbol, amount, 'reduce', params) + + async def fetch_funding_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch the history of funding fee payments paid and received on self account + + https://docs.coinex.com/api/v2/futures/position/http/list-position-funding-history + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch funding history for + :param int [limit]: the maximum number of funding history structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding history structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchFundingHistory() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + 'market_type': 'FUTURES', + } + request, params = self.handle_until_option('end_time', request, params) + if since is not None: + request['start_time'] = since + if limit is not None: + request['limit'] = limit + response = await self.v2PrivateGetFuturesPositionFundingHistory(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "ccy": "USDT", + # "created_at": 1715673620183, + # "funding_rate": "0", + # "funding_value": "0", + # "market": "BTCUSDT", + # "market_type": "FUTURES", + # "position_id": 306458800, + # "side": "long" + # }, + # ], + # "message": "OK", + # "pagination": { + # "has_next": True + # } + # } + # + data = self.safe_list(response, 'data', []) + result = [] + for i in range(0, len(data)): + entry = data[i] + timestamp = self.safe_integer(entry, 'created_at') + currencyId = self.safe_string(entry, 'ccy') + code = self.safe_currency_code(currencyId) + result.append({ + 'info': entry, + 'symbol': symbol, + 'code': code, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': self.safe_number(entry, 'position_id'), + 'amount': self.safe_number(entry, 'funding_value'), + }) + return result + + async def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate + + https://docs.coinex.com/api/v2/futures/market/http/list-market-funding-rate + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + await self.load_markets() + market = self.market(symbol) + if not market['swap']: + raise BadSymbol(self.id + ' fetchFundingRate() supports swap contracts only') + request: dict = { + 'market': market['id'], + } + response = await self.v2PublicGetFuturesFundingRate(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "latest_funding_rate": "0", + # "latest_funding_time": 1715731200000, + # "mark_price": "61602.22", + # "market": "BTCUSDT", + # "max_funding_rate": "0.00375", + # "min_funding_rate": "-0.00375", + # "next_funding_rate": "0.00021074", + # "next_funding_time": 1715760000000 + # } + # ], + # "message": "OK" + # } + # + data = self.safe_list(response, 'data', []) + first = self.safe_dict(data, 0, {}) + return self.parse_funding_rate(first, market) + + async def fetch_funding_interval(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate interval + + https://docs.coinex.com/api/v2/futures/market/http/list-market-funding-rate + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + return await self.fetch_funding_rate(symbol, params) + + def parse_funding_rate(self, contract, market: Market = None) -> FundingRate: + # + # fetchFundingRate, fetchFundingRates, fetchFundingInterval + # + # { + # "latest_funding_rate": "0", + # "latest_funding_time": 1715731200000, + # "mark_price": "61602.22", + # "market": "BTCUSDT", + # "max_funding_rate": "0.00375", + # "min_funding_rate": "-0.00375", + # "next_funding_rate": "0.00021074", + # "next_funding_time": 1715760000000 + # } + # + currentFundingTimestamp = self.safe_integer(contract, 'latest_funding_time') + futureFundingTimestamp = self.safe_integer(contract, 'next_funding_time') + fundingTimeString = self.safe_string(contract, 'latest_funding_time') + nextFundingTimeString = self.safe_string(contract, 'next_funding_time') + millisecondsInterval = Precise.string_sub(nextFundingTimeString, fundingTimeString) + marketId = self.safe_string(contract, 'market') + return { + 'info': contract, + 'symbol': self.safe_symbol(marketId, market, None, 'swap'), + 'markPrice': self.safe_number(contract, 'mark_price'), + 'indexPrice': None, + 'interestRate': None, + 'estimatedSettlePrice': None, + 'timestamp': None, + 'datetime': None, + 'fundingRate': self.safe_number(contract, 'latest_funding_rate'), + 'fundingTimestamp': currentFundingTimestamp, + 'fundingDatetime': self.iso8601(currentFundingTimestamp), + 'nextFundingRate': self.safe_number(contract, 'next_funding_rate'), + 'nextFundingTimestamp': futureFundingTimestamp, + 'nextFundingDatetime': self.iso8601(futureFundingTimestamp), + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': self.parse_funding_interval(millisecondsInterval), + } + + def parse_funding_interval(self, interval): + intervals: dict = { + '3600000': '1h', + '14400000': '4h', + '28800000': '8h', + '57600000': '16h', + '86400000': '24h', + } + return self.safe_string(intervals, interval, interval) + + async def fetch_funding_rates(self, symbols: Strings = None, params={}) -> FundingRates: + """ + fetch the current funding rates for multiple markets + + https://docs.coinex.com/api/v2/futures/market/http/list-market-funding-rate + + :param str[] symbols: unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of `funding rate structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + request: dict = {} + market = None + if symbols is not None: + symbol = self.safe_value(symbols, 0) + market = self.market(symbol) + if not market['swap']: + raise BadSymbol(self.id + ' fetchFundingRates() supports swap contracts only') + marketIds = self.market_ids(symbols) + request['market'] = ','.join(marketIds) + response = await self.v2PublicGetFuturesFundingRate(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "latest_funding_rate": "0", + # "latest_funding_time": 1715731200000, + # "mark_price": "61602.22", + # "market": "BTCUSDT", + # "max_funding_rate": "0.00375", + # "min_funding_rate": "-0.00375", + # "next_funding_rate": "0.00021074", + # "next_funding_time": 1715760000000 + # } + # ], + # "message": "OK" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_funding_rates(data, symbols) + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://docs.coinex.com/api/v2/assets/deposit-withdrawal/http/withdrawal + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str [tag]: memo + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.network]: unified network code + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.check_address(address) + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'ccy': currency['id'], + 'to_address': address, # must be authorized, inter-user transfer by a registered mobile phone number or an email address is supported + 'amount': self.currency_to_precision(code, amount), # the actual amount without fees, https://www.coinex.com/fees + } + if tag is not None: + request['memo'] = tag + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + if networkCode is not None: + request['chain'] = self.network_code_to_id(networkCode) # required for on-chain, not required for inter-user transfer + response = await self.v2PrivatePostAssetsWithdraw(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "withdraw_id": 31193755, + # "created_at": 1716874165038, + # "withdraw_method": "ON_CHAIN", + # "ccy": "USDT", + # "amount": "17.3", + # "actual_amount": "15", + # "chain": "TRC20", + # "tx_fee": "2.3", + # "fee_asset": "USDT", + # "fee_amount": "2.3", + # "to_address": "TY5vq3MT6b5cQVAHWHtpGyPg1ERcQgi3UN", + # "memo": "", + # "tx_id": "", + # "confirmations": 0, + # "explorer_address_url": "https://tronscan.org/#/address/TY5vq3MT6b5cQVAHWHtpGyPg1ERcQgi3UN", + # "explorer_tx_url": "https://tronscan.org/#/transaction/", + # "remark": "", + # "status": "audit_required" + # }, + # "message": "OK" + # } + # + transaction = self.safe_dict(response, 'data', {}) + return self.parse_transaction(transaction, currency) + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'audit': 'pending', + 'pass': 'pending', + 'audit_required': 'pending', + 'processing': 'pending', + 'confirming': 'pending', + 'not_pass': 'failed', + 'cancel': 'canceled', + 'finish': 'ok', + 'finished': 'ok', + 'fail': 'failed', + } + return self.safe_string(statuses, status, status) + + async def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical funding rate prices + + https://docs.coinex.com/api/v2/futures/market/http/list-market-funding-rate-history + + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: timestamp in ms of the earliest funding rate to fetch + :param int [limit]: the maximum amount of `funding rate structures ` to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param int [params.until]: timestamp in ms of the latest funding rate + :returns dict[]: a list of `funding rate structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchFundingRateHistory() requires a symbol argument') + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchFundingRateHistory', 'paginate') + if paginate: + return await self.fetch_paginated_call_deterministic('fetchFundingRateHistory', symbol, since, limit, '8h', params, 1000) + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + if since is not None: + request['start_time'] = since + if limit is not None: + request['limit'] = limit + request, params = self.handle_until_option('end_time', request, params) + response = await self.v2PublicGetFuturesFundingRateHistory(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "actual_funding_rate": "0", + # "funding_time": 1715731221761, + # "market": "BTCUSDT", + # "theoretical_funding_rate": "0" + # }, + # ], + # "message": "OK", + # "pagination": { + # "has_next": True + # } + # } + # + data = self.safe_list(response, 'data', []) + rates = [] + for i in range(0, len(data)): + entry = data[i] + marketId = self.safe_string(entry, 'market') + symbolInner = self.safe_symbol(marketId, market, None, 'swap') + timestamp = self.safe_integer(entry, 'funding_time') + rates.append({ + 'info': entry, + 'symbol': symbolInner, + 'fundingRate': self.safe_number(entry, 'actual_funding_rate'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + sorted = self.sort_by(rates, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, market['symbol'], since, limit) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fetchDeposits + # + # { + # "deposit_id": 5173806, + # "created_at": 1714021652557, + # "tx_id": "d9f47d2550397c635cb89a8963118f8fe78ef048bc8b6f0caaeaa7dc6", + # "tx_id_display": "", + # "ccy": "USDT", + # "chain": "TRC20", + # "deposit_method": "ON_CHAIN", + # "amount": "30", + # "actual_amount": "", + # "to_address": "TYewD2pVWDUwfNr9A", + # "confirmations": 20, + # "status": "FINISHED", + # "tx_explorer_url": "https://tronscan.org/#/transaction", + # "to_addr_explorer_url": "https://tronscan.org/#/address", + # "remark": "" + # } + # + # fetchWithdrawals and withdraw + # + # { + # "withdraw_id": 259364, + # "created_at": 1701323541548, + # "withdraw_method": "ON_CHAIN", + # "ccy": "USDT", + # "amount": "23.845744", + # "actual_amount": "22.445744", + # "chain": "TRC20", + # "tx_fee": "1.4", + # "fee_asset": "USDT", + # "fee_amount": "1.4", + # "to_address": "T8t5i2454dhdhnnnGdi49vMbihvY", + # "memo": "", + # "tx_id": "1237623941964de9954ed2e36640228d78765c1026", + # "confirmations": 18, + # "explorer_address_url": "https://tronscan.org/#/address", + # "explorer_tx_url": "https://tronscan.org/#/transaction", + # "remark": "", + # "status": "finished" + # } + # + address = self.safe_string(transaction, 'to_address') + tag = self.safe_string(transaction, 'memo') + if tag is not None: + if len(tag) < 1: + tag = None + remark = self.safe_string(transaction, 'remark') + if remark is not None: + if len(remark) < 1: + remark = None + txid = self.safe_string(transaction, 'tx_id') + if txid is not None: + if len(txid) < 1: + txid = None + currencyId = self.safe_string(transaction, 'ccy') + code = self.safe_currency_code(currencyId, currency) + timestamp = self.safe_integer(transaction, 'created_at') + type = 'withdrawal' if ('withdraw_id' in transaction) else 'deposit' + networkId = self.safe_string(transaction, 'chain') + feeCost = self.safe_string(transaction, 'tx_fee') + transferMethod = self.safe_string_lower_2(transaction, 'withdraw_method', 'deposit_method') + internal = transferMethod == 'local' + amount = self.safe_number(transaction, 'actual_amount') + if amount is None: + amount = self.safe_number(transaction, 'amount') + if type == 'deposit': + feeCost = '0' + feeCurrencyId = self.safe_string(transaction, 'fee_asset') + fee = { + 'cost': self.parse_number(feeCost), + 'currency': self.safe_currency_code(feeCurrencyId), + } + return { + 'info': transaction, + 'id': self.safe_string_2(transaction, 'withdraw_id', 'deposit_id'), + 'txid': txid, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': self.network_id_to_code(networkId), + 'address': address, + 'addressTo': address, + 'addressFrom': None, + 'tag': tag, + 'tagTo': None, + 'tagFrom': None, + 'type': type, + 'amount': amount, + 'currency': code, + 'status': self.parse_transaction_status(self.safe_string(transaction, 'status')), + 'updated': None, + 'fee': fee, + 'comment': remark, + 'internal': internal, + } + + async def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://docs.coinex.com/api/v2/assets/transfer/http/transfer + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account to transfer from + :param str toAccount: account to transfer to + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.symbol]: unified ccxt symbol, required when either the fromAccount or toAccount is margin + :returns dict: a `transfer structure ` + """ + await self.load_markets() + currency = self.currency(code) + amountToPrecision = self.currency_to_precision(code, amount) + accountsByType = self.safe_dict(self.options, 'accountsByType', {}) + fromId = self.safe_string(accountsByType, fromAccount, fromAccount) + toId = self.safe_string(accountsByType, toAccount, toAccount) + request: dict = { + 'ccy': currency['id'], + 'amount': amountToPrecision, + 'from_account_type': fromId, + 'to_account_type': toId, + } + if (fromAccount == 'margin') or (toAccount == 'margin'): + symbol = self.safe_string(params, 'symbol') + if symbol is None: + raise ArgumentsRequired(self.id + ' transfer() the symbol parameter must be defined for a margin account') + params = self.omit(params, 'symbol') + request['market'] = self.market_id(symbol) + if (fromAccount != 'spot') and (toAccount != 'spot'): + raise BadRequest(self.id + ' transfer() can only be between spot and swap, or spot and margin, either the fromAccount or toAccount must be spot') + response = await self.v2PrivatePostAssetsTransfer(self.extend(request, params)) + # + # { + # "code": 0, + # "data": {}, + # "message": "OK" + # } + # + return self.extend(self.parse_transfer(response, currency), { + 'amount': self.parse_number(amountToPrecision), + 'fromAccount': fromAccount, + 'toAccount': toAccount, + }) + + def parse_transfer_status(self, status): + statuses: dict = { + '0': 'ok', + 'SUCCESS': 'ok', + 'OK': 'ok', + 'finished': 'ok', + 'FINISHED': 'ok', + } + return self.safe_string(statuses, status, status) + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + timestamp = self.safe_integer(transfer, 'created_at') + currencyId = self.safe_string(transfer, 'ccy') + fromId = self.safe_string(transfer, 'from_account_type') + toId = self.safe_string(transfer, 'to_account_type') + accountsById = self.safe_value(self.options, 'accountsById', {}) + return { + 'id': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'currency': self.safe_currency_code(currencyId, currency), + 'amount': self.safe_number(transfer, 'amount'), + 'fromAccount': self.safe_string(accountsById, fromId, fromId), + 'toAccount': self.safe_string(accountsById, toId, toId), + 'status': self.parse_transfer_status(self.safe_string_2(transfer, 'code', 'status')), + } + + async def fetch_transfers(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[TransferEntry]: + """ + fetch a history of internal transfers made on an account + + https://docs.coinex.com/api/v2/assets/transfer/http/list-transfer-history + + :param str code: unified currency code of the currency transferred + :param int [since]: the earliest time in ms to fetch transfers for + :param int [limit]: the maximum number of transfer structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' for fetching transfers to and from your margin account + :returns dict[]: a list of `transfer structures ` + """ + await self.load_markets() + if code is None: + raise ArgumentsRequired(self.id + ' fetchTransfers() requires a code argument') + currency = self.currency(code) + request: dict = { + 'ccy': currency['id'], + } + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchTransfers', params) + if marginMode is not None: + request['transfer_type'] = 'MARGIN' + else: + request['transfer_type'] = 'FUTURES' + if since is not None: + request['start_time'] = since + if limit is not None: + request['limit'] = limit + request, params = self.handle_until_option('end_time', request, params) + response = await self.v2PrivateGetAssetsTransferHistory(self.extend(request, params)) + # + # { + # "data": [ + # { + # "created_at": 1715848480646, + # "from_account_type": "SPOT", + # "to_account_type": "FUTURES", + # "ccy": "USDT", + # "amount": "10", + # "status": "finished" + # }, + # ], + # "pagination": { + # "total": 8, + # "has_next": False + # }, + # "code": 0, + # "message": "OK" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_transfers(data, currency, since, limit) + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://docs.coinex.com/api/v2/assets/deposit-withdrawal/http/list-withdrawal-history + + :param str [code]: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawal structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + request: dict = {} + currency = None + if code is not None: + currency = self.currency(code) + request['ccy'] = currency['id'] + if limit is not None: + request['limit'] = limit + response = await self.v2PrivateGetAssetsWithdraw(self.extend(request, params)) + # + # { + # "data": [ + # { + # "withdraw_id": 259364, + # "created_at": 1701323541548, + # "withdraw_method": "ON_CHAIN", + # "ccy": "USDT", + # "amount": "23.845744", + # "actual_amount": "22.445744", + # "chain": "TRC20", + # "tx_fee": "1.4", + # "fee_asset": "USDT", + # "fee_amount": "1.4", + # "to_address": "T8t5i2454dhdhnnnGdi49vMbihvY", + # "memo": "", + # "tx_id": "1237623941964de9954ed2e36640228d78765c1026", + # "confirmations": 18, + # "explorer_address_url": "https://tronscan.org/#/address", + # "explorer_tx_url": "https://tronscan.org/#/transaction", + # "remark": "", + # "status": "finished" + # }, + # ], + # "pagination": { + # "total": 9, + # "has_next": True + # }, + # "code": 0, + # "message": "OK" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_transactions(data, currency, since, limit) + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://docs.coinex.com/api/v2/assets/deposit-withdrawal/http/list-deposit-history + + :param str [code]: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposit structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + request: dict = {} + currency = None + if code is not None: + currency = self.currency(code) + request['ccy'] = currency['id'] + if limit is not None: + request['limit'] = limit + response = await self.v2PrivateGetAssetsDepositHistory(self.extend(request, params)) + # + # { + # "data": [ + # { + # "deposit_id": 5173806, + # "created_at": 1714021652557, + # "tx_id": "d9f47d2550397c635cb89a8963118f8fe78ef048bc8b6f0caaeaa7dc6", + # "tx_id_display": "", + # "ccy": "USDT", + # "chain": "TRC20", + # "deposit_method": "ON_CHAIN", + # "amount": "30", + # "actual_amount": "", + # "to_address": "TYewD2pVWDUwfNr9A", + # "confirmations": 20, + # "status": "FINISHED", + # "tx_explorer_url": "https://tronscan.org/#/transaction", + # "to_addr_explorer_url": "https://tronscan.org/#/address", + # "remark": "" + # }, + # ], + # "paginatation": { + # "total": 8, + # "has_next": True + # }, + # "code": 0, + # "message": "OK" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_transactions(data, currency, since, limit) + + def parse_isolated_borrow_rate(self, info: dict, market: Market = None) -> IsolatedBorrowRate: + # + # { + # "market": "BTCUSDT", + # "ccy": "USDT", + # "leverage": 10, + # "min_amount": "60", + # "max_amount": "500000", + # "daily_interest_rate": "0.001" + # } + # + marketId = self.safe_string(info, 'market') + market = self.safe_market(marketId, market, None, 'spot') + currency = self.safe_string(info, 'ccy') + rate = self.safe_number(info, 'daily_interest_rate') + baseRate = None + quoteRate = None + if currency == market['baseId']: + baseRate = rate + elif currency == market['quoteId']: + quoteRate = rate + return { + 'symbol': market['symbol'], + 'base': market['base'], + 'baseRate': baseRate, + 'quote': market['quote'], + 'quoteRate': quoteRate, + 'period': 86400000, + 'timestamp': None, + 'datetime': None, + 'info': info, + } + + async def fetch_isolated_borrow_rate(self, symbol: str, params={}) -> IsolatedBorrowRate: + """ + fetch the rate of interest to borrow a currency for margin trading + + https://docs.coinex.com/api/v2/assets/loan-flat/http/list-margin-interest-limit + + :param str symbol: unified symbol of the market to fetch the borrow rate for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str params['code']: unified currency code + :returns dict: an `isolated borrow rate structure ` + """ + await self.load_markets() + code = self.safe_string(params, 'code') + if code is None: + raise ArgumentsRequired(self.id + ' fetchIsolatedBorrowRate() requires a code parameter') + params = self.omit(params, 'code') + currency = self.currency(code) + market = self.market(symbol) + request: dict = { + 'market': market['id'], + 'ccy': currency['id'], + } + response = await self.v2PrivateGetAssetsMarginInterestLimit(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "market": "BTCUSDT", + # "ccy": "USDT", + # "leverage": 10, + # "min_amount": "60", + # "max_amount": "500000", + # "daily_interest_rate": "0.001" + # }, + # "message": "OK" + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_isolated_borrow_rate(data, market) + + async def fetch_borrow_interest(self, code: Str = None, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[BorrowInterest]: + """ + fetch the interest owed by the user for borrowing currency for margin trading + + https://docs.coinex.com/api/v2/assets/loan-flat/http/list-margin-borrow-history + + :param str [code]: unified currency code + :param str [symbol]: unified market symbol when fetch interest in isolated markets + :param int [since]: the earliest time in ms to fetch borrrow interest for + :param int [limit]: the maximum number of structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `borrow interest structures ` + """ + await self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['market'] = market['id'] + if limit is not None: + request['limit'] = limit + response = await self.v2PrivateGetAssetsMarginBorrowHistory(self.extend(request, params)) + # + # { + # "data": [ + # { + # "borrow_id": 2642934, + # "created_at": 1654761016000, + # "market": "BTCUSDT", + # "ccy": "USDT", + # "daily_interest_rate": "0.001", + # "expired_at": 1655625016000, + # "borrow_amount": "100", + # "to_repaied_amount": "0", + # "is_auto_renew": False, + # "status": "finish" + # }, + # ], + # "pagination": { + # "total": 4, + # "has_next": True + # }, + # "code": 0, + # "message": "OK" + # } + # + rows = self.safe_value(response, 'data', []) + interest = self.parse_borrow_interests(rows, market) + return self.filter_by_currency_since_limit(interest, code, since, limit) + + def parse_borrow_interest(self, info: dict, market: Market = None) -> BorrowInterest: + # + # { + # "borrow_id": 2642934, + # "created_at": 1654761016000, + # "market": "BTCUSDT", + # "ccy": "USDT", + # "daily_interest_rate": "0.001", + # "expired_at": 1655625016000, + # "borrow_amount": "100", + # "to_repaied_amount": "0", + # "is_auto_renew": False, + # "status": "finish" + # } + # + marketId = self.safe_string(info, 'market') + market = self.safe_market(marketId, market, None, 'spot') + timestamp = self.safe_integer(info, 'expired_at') + return { + 'info': info, + 'symbol': market['symbol'], + 'currency': self.safe_currency_code(self.safe_string(info, 'ccy')), + 'interest': self.safe_number(info, 'to_repaied_amount'), + 'interestRate': self.safe_number(info, 'daily_interest_rate'), + 'amountBorrowed': self.safe_number(info, 'borrow_amount'), + 'marginMode': 'isolated', + 'timestamp': timestamp, # expiry time + 'datetime': self.iso8601(timestamp), + } + + async def borrow_isolated_margin(self, symbol: str, code: str, amount: float, params={}): + """ + create a loan to borrow margin + + https://docs.coinex.com/api/v2/assets/loan-flat/http/margin-borrow + + :param str symbol: unified market symbol, required for coinex + :param str code: unified currency code of the currency to borrow + :param float amount: the amount to borrow + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.isAutoRenew]: whether to renew the margin loan automatically or not, default is False + :returns dict: a `margin loan structure ` + """ + await self.load_markets() + market = self.market(symbol) + currency = self.currency(code) + isAutoRenew = self.safe_bool_2(params, 'isAutoRenew', 'is_auto_renew', False) + params = self.omit(params, 'isAutoRenew') + request: dict = { + 'market': market['id'], + 'ccy': currency['id'], + 'borrow_amount': self.currency_to_precision(code, amount), + 'is_auto_renew': isAutoRenew, + } + response = await self.v2PrivatePostAssetsMarginBorrow(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "borrow_id": 13784021, + # "market": "BTCUSDT", + # "ccy": "USDT", + # "daily_interest_rate": "0.001", + # "expired_at": 1717299948340, + # "borrow_amount": "60", + # "to_repaied_amount": "60.0025", + # "status": "loan" + # }, + # "message": "OK" + # } + # + data = self.safe_dict(response, 'data', {}) + transaction = self.parse_margin_loan(data, currency) + return self.extend(transaction, { + 'amount': amount, + 'symbol': symbol, + }) + + async def repay_isolated_margin(self, symbol: str, code: str, amount, params={}): + """ + repay borrowed margin and interest + + https://docs.coinex.com/api/v2/assets/loan-flat/http/margin-repay + + :param str symbol: unified market symbol, required for coinex + :param str code: unified currency code of the currency to repay + :param float amount: the amount to repay + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.borrow_id]: extra parameter that is not required + :returns dict: a `margin loan structure ` + """ + await self.load_markets() + market = self.market(symbol) + currency = self.currency(code) + request: dict = { + 'market': market['id'], + 'ccy': currency['id'], + 'amount': self.currency_to_precision(code, amount), + } + response = await self.v2PrivatePostAssetsMarginRepay(self.extend(request, params)) + # + # { + # "code": 0, + # "data": {}, + # "message": "OK" + # } + # + data = self.safe_dict(response, 'data', {}) + transaction = self.parse_margin_loan(data, currency) + return self.extend(transaction, { + 'amount': amount, + 'symbol': symbol, + }) + + def parse_margin_loan(self, info, currency: Currency = None): + # + # { + # "borrow_id": 13784021, + # "market": "BTCUSDT", + # "ccy": "USDT", + # "daily_interest_rate": "0.001", + # "expired_at": 1717299948340, + # "borrow_amount": "60", + # "to_repaied_amount": "60.0025", + # "status": "loan" + # } + # + currencyId = self.safe_string(info, 'ccy') + marketId = self.safe_string(info, 'market') + timestamp = self.safe_integer(info, 'expired_at') + return { + 'id': self.safe_integer(info, 'borrow_id'), + 'currency': self.safe_currency_code(currencyId, currency), + 'amount': self.safe_string(info, 'borrow_amount'), + 'symbol': self.safe_symbol(marketId, None, None, 'spot'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'info': info, + } + + async def fetch_deposit_withdraw_fee(self, code: str, params={}): + """ + fetch the fee for deposits and withdrawals + + https://docs.coinex.com/api/v2/assets/deposit-withdrawal/http/get-deposit-withdrawal-config + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `fee structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'ccy': currency['id'], + } + response = await self.v2PublicGetAssetsDepositWithdrawConfig(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "asset": { + # "ccy": "USDT", + # "deposit_enabled": True, + # "withdraw_enabled": True, + # "inter_transfer_enabled": True, + # "is_st": False + # }, + # "chains": [ + # { + # "chain": "TRC20", + # "min_deposit_amount": "2.4", + # "min_withdraw_amount": "2.4", + # "deposit_enabled": True, + # "withdraw_enabled": True, + # "deposit_delay_minutes": 0, + # "safe_confirmations": 10, + # "irreversible_confirmations": 20, + # "deflation_rate": "0", + # "withdrawal_fee": "2.4", + # "withdrawal_precision": 6, + # "memo": "", + # "is_memo_required_for_deposit": False, + # "explorer_asset_url": "https://tronscan.org/#/token20/TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t" + # }, + # ] + # }, + # "message": "OK" + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_deposit_withdraw_fee(data, currency) + + async def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}): + """ + fetch the fees for deposits and withdrawals + + https://docs.coinex.com/api/v2/assets/deposit-withdrawal/http/list-all-deposit-withdrawal-config + + @param codes + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` + """ + await self.load_markets() + response = await self.v2PublicGetAssetsAllDepositWithdrawConfig(params) + # + # { + # "code": 0, + # "data": [ + # { + # "asset": { + # "ccy": "CET", + # "deposit_enabled": True, + # "withdraw_enabled": True, + # "inter_transfer_enabled": True, + # "is_st": False + # }, + # "chains": [ + # { + # "chain": "CSC", + # "min_deposit_amount": "0.8", + # "min_withdraw_amount": "8", + # "deposit_enabled": True, + # "withdraw_enabled": True, + # "deposit_delay_minutes": 0, + # "safe_confirmations": 10, + # "irreversible_confirmations": 20, + # "deflation_rate": "0", + # "withdrawal_fee": "0.026", + # "withdrawal_precision": 8, + # "memo": "", + # "is_memo_required_for_deposit": False, + # "explorer_asset_url": "" + # }, + # ] + # } + # ], + # "message": "OK" + # } + # + data = self.safe_list(response, 'data', []) + result: dict = {} + for i in range(0, len(data)): + item = data[i] + asset = self.safe_dict(item, 'asset', {}) + currencyId = self.safe_string(asset, 'ccy') + if currencyId is None: + continue + code = self.safe_currency_code(currencyId) + if codes is None or self.in_array(code, codes): + result[code] = self.parse_deposit_withdraw_fee(item) + return result + + def parse_deposit_withdraw_fee(self, fee, currency: Currency = None): + # + # { + # "asset": { + # "ccy": "USDT", + # "deposit_enabled": True, + # "withdraw_enabled": True, + # "inter_transfer_enabled": True, + # "is_st": False + # }, + # "chains": [ + # { + # "chain": "TRC20", + # "min_deposit_amount": "2.4", + # "min_withdraw_amount": "2.4", + # "deposit_enabled": True, + # "withdraw_enabled": True, + # "deposit_delay_minutes": 0, + # "safe_confirmations": 10, + # "irreversible_confirmations": 20, + # "deflation_rate": "0", + # "withdrawal_fee": "2.4", + # "withdrawal_precision": 6, + # "memo": "", + # "is_memo_required_for_deposit": False, + # "explorer_asset_url": "https://tronscan.org/#/token20/TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t" + # }, + # ] + # } + # + result: dict = { + 'info': fee, + 'withdraw': { + 'fee': None, + 'percentage': None, + }, + 'deposit': { + 'fee': None, + 'percentage': None, + }, + 'networks': {}, + } + chains = self.safe_list(fee, 'chains', []) + asset = self.safe_dict(fee, 'asset', {}) + for i in range(0, len(chains)): + entry = chains[i] + isWithdrawEnabled = self.safe_bool(entry, 'withdraw_enabled') + if isWithdrawEnabled: + result['withdraw']['fee'] = self.safe_number(entry, 'withdrawal_fee') + result['withdraw']['percentage'] = False + networkId = self.safe_string(entry, 'chain') + if networkId: + networkCode = self.network_id_to_code(networkId, self.safe_string(asset, 'ccy')) + result['networks'][networkCode] = { + 'withdraw': { + 'fee': self.safe_number(entry, 'withdrawal_fee'), + 'percentage': False, + }, + 'deposit': { + 'fee': None, + 'percentage': None, + }, + } + return result + + async def fetch_leverage(self, symbol: str, params={}) -> Leverage: + """ + fetch the set leverage for a market + + https://docs.coinex.com/api/v2/assets/loan-flat/http/list-margin-interest-limit + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str params['code']: unified currency code + :returns dict: a `leverage structure ` + """ + await self.load_markets() + code = self.safe_string(params, 'code') + if code is None: + raise ArgumentsRequired(self.id + ' fetchLeverage() requires a code parameter') + params = self.omit(params, 'code') + currency = self.currency(code) + market = self.market(symbol) + request: dict = { + 'market': market['id'], + 'ccy': currency['id'], + } + response = await self.v2PrivateGetAssetsMarginInterestLimit(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "market": "BTCUSDT", + # "ccy": "USDT", + # "leverage": 10, + # "min_amount": "50", + # "max_amount": "500000", + # "daily_interest_rate": "0.001" + # }, + # "message": "OK" + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_leverage(data, market) + + def parse_leverage(self, leverage: dict, market: Market = None) -> Leverage: + # + # { + # "market": "BTCUSDT", + # "ccy": "USDT", + # "leverage": 10, + # "min_amount": "50", + # "max_amount": "500000", + # "daily_interest_rate": "0.001" + # } + # + marketId = self.safe_string(leverage, 'market') + leverageValue = self.safe_integer(leverage, 'leverage') + return { + 'info': leverage, + 'symbol': self.safe_symbol(marketId, market, None, 'spot'), + 'marginMode': 'isolated', + 'longLeverage': leverageValue, + 'shortLeverage': leverageValue, + } + + async def fetch_position_history(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Position]: + """ + fetches historical positions + + https://docs.coinex.com/api/v2/futures/position/http/list-finished-position + + :param str symbol: unified contract symbol + :param int [since]: the earliest time in ms to fetch positions for + :param int [limit]: the maximum amount of records to fetch, default is 10 + :param dict [params]: extra parameters specific to the exchange api endpoint + :param int [params.until]: the latest time in ms to fetch positions for + :returns dict[]: a list of `position structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'market_type': 'FUTURES', + 'market': market['id'], + } + if limit is not None: + request['limit'] = limit + if since is not None: + request['start_time'] = since + request, params = self.handle_until_option('end_time', request, params) + response = await self.v2PrivateGetFuturesFinishedPosition(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "position_id": 305891033, + # "market": "BTCUSDT", + # "market_type": "FUTURES", + # "side": "long", + # "margin_mode": "cross", + # "open_interest": "0.0001", + # "close_avbl": "0.0001", + # "ath_position_amount": "0.0001", + # "unrealized_pnl": "0", + # "realized_pnl": "-0.00311684", + # "avg_entry_price": "62336.8", + # "cml_position_value": "6.23368", + # "max_position_value": "6.23368", + # "created_at": 1715152208041, + # "updated_at": 1715152208041, + # "take_profit_price": "0", + # "stop_loss_price": "0", + # "take_profit_type": "", + # "stop_loss_type": "", + # "settle_price": "62336.8", + # "settle_value": "6.23368", + # "leverage": "3", + # "margin_avbl": "2.07789333", + # "ath_margin_size": "2.07789333", + # "position_margin_rate": "2.40545879023305655728", + # "maintenance_margin_rate": "0.005", + # "maintenance_margin_value": "0.03118094", + # "liq_price": "0", + # "bkr_price": "0", + # "adl_level": 1 + # } + # ], + # "message": "OK", + # "pagination": { + # "has_next": False + # } + # } + # + records = self.safe_list(response, 'data', []) + positions = self.parse_positions(records) + return self.filter_by_symbol_since_limit(positions, symbol, since, limit) + + async def close_position(self, symbol: str, side: OrderSide = None, params={}) -> Order: + """ + closes an open position for a market + + https://docs.coinex.com/api/v2/futures/position/http/close-position + + :param str symbol: unified CCXT market symbol + :param str [side]: buy or sell, not used by coinex + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str params['type']: required by coinex, one of: limit, market, maker_only, ioc or fok, default is *market* + :param str [params.price]: the price to fulfill the order, ignored in market orders + :param str [params.amount]: the amount to trade in units of the base currency + :param str [params.clientOrderId]: the client id of the order + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + type = self.safe_string(params, 'type', 'market') + request: dict = { + 'market': market['id'], + 'market_type': 'FUTURES', + 'type': type, + } + clientOrderId = self.safe_string_2(params, 'client_id', 'clientOrderId') + if clientOrderId is not None: + request['client_id'] = clientOrderId + params = self.omit(params, 'clientOrderId') + response = await self.v2PrivatePostFuturesClosePosition(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "amount": "0.0001", + # "client_id": "", + # "created_at": 1729666043969, + # "fee": "0.00335858", + # "fee_ccy": "USDT", + # "filled_amount": "0.0001", + # "filled_value": "6.717179", + # "last_filled_amount": "0.0001", + # "last_filled_price": "67171.79", + # "maker_fee_rate": "0", + # "market": "BTCUSDT", + # "market_type": "FUTURES", + # "order_id": 155477479761, + # "price": "0", + # "realized_pnl": "-0.001823", + # "side": "sell", + # "taker_fee_rate": "0.0005", + # "type": "market", + # "unfilled_amount": "0", + # "updated_at": 1729666043969 + # }, + # "message": "OK" + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data, market) + + def handle_margin_mode_and_params(self, methodName, params={}, defaultValue=None): + """ + @ignore + marginMode specified by params["marginMode"], self.options["marginMode"], self.options["defaultMarginMode"], params["margin"] = True or self.options["defaultType"] = 'margin' + :param dict params: extra parameters specific to the exchange api endpoint + :returns Array: the marginMode in lowercase + """ + defaultType = self.safe_string(self.options, 'defaultType') + isMargin = self.safe_bool(params, 'margin', False) + marginMode = None + marginMode, params = super(coinex, self).handle_margin_mode_and_params(methodName, params, defaultValue) + if marginMode is None: + if (defaultType == 'margin') or (isMargin is True): + marginMode = 'isolated' + return [marginMode, params] + + def nonce(self): + return self.milliseconds() + + def sign(self, path, api=[], method='GET', params={}, headers=None, body=None): + path = self.implode_params(path, params) + version = api[0] + requestUrl = api[1] + url = self.urls['api'][requestUrl] + '/' + version + '/' + path + query = self.omit(params, self.extract_params(path)) + nonce = str(self.nonce()) + if method == 'POST': + parts = path.split('/') + firstPart = self.safe_string(parts, 0, '') + numParts = len(parts) + lastPart = self.safe_string(parts, numParts - 1, '') + lastWords = lastPart.split('_') + numWords = len(lastWords) + lastWord = self.safe_string(lastWords, numWords - 1, '') + if (firstPart == 'order') and (lastWord == 'limit' or lastWord == 'market'): + # inject in implicit API calls + # POST /order/limit - Place limit orders + # POST /order/market - Place market orders + # POST /order/stop/limit - Place stop limit orders + # POST /order/stop/market - Place stop market orders + # POST /perpetual/v1/order/put_limit - Place limit orders + # POST /perpetual/v1/order/put_market - Place market orders + # POST /perpetual/v1/order/put_stop_limit - Place stop limit orders + # POST /perpetual/v1/order/put_stop_market - Place stop market orders + clientOrderId = self.safe_string(params, 'client_id') + if clientOrderId is None: + defaultId = 'x-167673045' + brokerId = self.safe_value(self.options, 'brokerId', defaultId) + query['client_id'] = brokerId + '_' + self.uuid16() + if requestUrl == 'perpetualPrivate': + self.check_required_credentials() + query = self.extend({ + 'access_id': self.apiKey, + 'timestamp': nonce, + }, query) + query = self.keysort(query) + urlencoded = self.rawencode(query) + signature = self.hash(self.encode(urlencoded + '&secret_key=' + self.secret), 'sha256') + headers = { + 'Authorization': signature.lower(), + 'AccessId': self.apiKey, + } + if (method == 'GET') or (method == 'PUT'): + url += '?' + urlencoded + else: + headers['Content-Type'] = 'application/x-www-form-urlencoded' + body = urlencoded + elif requestUrl == 'public' or requestUrl == 'perpetualPublic': + if query: + url += '?' + self.urlencode(query) + else: + if version == 'v1': + self.check_required_credentials() + query = self.extend({ + 'access_id': self.apiKey, + 'tonce': nonce, + }, query) + query = self.keysort(query) + urlencoded = self.rawencode(query) + signature = self.hash(self.encode(urlencoded + '&secret_key=' + self.secret), 'md5') + headers = { + 'Authorization': signature.upper(), + 'Content-Type': 'application/json', + } + if (method == 'GET') or (method == 'DELETE') or (method == 'PUT'): + url += '?' + urlencoded + else: + body = self.json(query) + elif version == 'v2': + self.check_required_credentials() + query = self.keysort(query) + urlencoded = self.rawencode(query) + preparedString = method + '/' + version + '/' + path + if method == 'POST': + body = self.json(query) + preparedString += body + elif urlencoded: + preparedString += '?' + urlencoded + preparedString += nonce + self.secret + signature = self.hash(self.encode(preparedString), 'sha256') + headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'X-COINEX-KEY': self.apiKey, + 'X-COINEX-SIGN': signature, + 'X-COINEX-TIMESTAMP': nonce, + } + if method != 'POST': + if urlencoded: + url += '?' + urlencoded + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None + code = self.safe_string(response, 'code') + data = self.safe_value(response, 'data') + message = self.safe_string(response, 'message') + if (code != '0') or ((message != 'Success') and (message != 'Succeeded') and (message.lower() != 'ok') and not data): + feedback = self.id + ' ' + message + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], code, feedback) + raise ExchangeError(feedback) + return None + + async def fetch_margin_adjustment_history(self, symbol: Str = None, type: Str = None, since: Num = None, limit: Num = None, params={}) -> List[MarginModification]: + """ + fetches the history of margin added or reduced from contract isolated positions + + https://docs.coinex.com/api/v2/futures/position/http/list-position-margin-history + + :param str symbol: unified market symbol + :param str [type]: not used by coinex fetchMarginAdjustmentHistory + :param int [since]: timestamp in ms of the earliest change to fetch + :param int [limit]: the maximum amount of changes to fetch, default is 10 + :param dict params: extra parameters specific to the exchange api endpoint + :param int [params.until]: timestamp in ms of the latest change to fetch + :param int [params.positionId]: the id of the position that you want to retrieve margin adjustment history for + :returns dict[]: a list of `margin structures ` + """ + await self.load_markets() + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMarginAdjustmentHistory() requires a symbol argument') + positionId = self.safe_integer_2(params, 'positionId', 'position_id') + params = self.omit(params, 'positionId') + if positionId is None: + raise ArgumentsRequired(self.id + ' fetchMarginAdjustmentHistory() requires a positionId parameter') + market = self.market(symbol) + request: dict = { + 'market': market['id'], + 'market_type': 'FUTURES', + 'position_id': positionId, + } + request, params = self.handle_until_option('end_time', request, params) + if since is not None: + request['start_time'] = since + if limit is not None: + request['limit'] = limit + response = await self.v2PrivateGetFuturesPositionMarginHistory(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "bkr_pirce": "24698.56000000000000005224", + # "created_at": 1715489978697, + # "leverage": "3", + # "liq_price": "24822.67336683417085432386", + # "margin_avbl": "3.634928", + # "margin_change": "-1.5", + # "margin_mode": "isolated", + # "market": "BTCUSDT", + # "market_type": "FUTURES", + # "open_interest": "0.0001", + # "position_id": 306458800, + # "settle_price": "61047.84" + # }, + # ], + # "message": "OK", + # "pagination": { + # "has_next": True + # } + # } + # + data = self.safe_list(response, 'data', []) + modifications = self.parse_margin_modifications(data, None, 'market', 'swap') + return self.filter_by_symbol_since_limit(modifications, symbol, since, limit) diff --git a/ccxt/async_support/coinmate.py b/ccxt/async_support/coinmate.py new file mode 100644 index 0000000..b636337 --- /dev/null +++ b/ccxt/async_support/coinmate.py @@ -0,0 +1,1193 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.coinmate import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currency, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, TradingFeeInterface, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class coinmate(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(coinmate, self).describe(), { + 'id': 'coinmate', + 'name': 'CoinMate', + 'countries': ['GB', 'CZ', 'EU'], # UK, Czech Republic + 'rateLimit': 600, + 'has': { + 'CORS': True, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelOrder': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createOrder': True, + 'createOrderWithTakeProfitAndStopLoss': False, + 'createOrderWithTakeProfitAndStopLossWs': False, + 'createPostOnlyOrder': False, + 'createReduceOnlyOrder': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchDepositsWithdrawals': True, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTrades': True, + 'fetchTradingFee': True, + 'fetchTradingFees': False, + 'fetchTransactions': 'emulated', + 'fetchVolatilityHistory': False, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'repayMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'transfer': False, + 'withdraw': True, + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/51840849/87460806-1c9f3f00-c616-11ea-8c46-a77018a8f3f4.jpg', + 'api': { + 'rest': 'https://coinmate.io/api', + }, + 'www': 'https://coinmate.io', + 'fees': 'https://coinmate.io/fees', + 'doc': [ + 'https://coinmate.docs.apiary.io', + 'https://coinmate.io/developers', + ], + 'referral': 'https://coinmate.io?referral=YTFkM1RsOWFObVpmY1ZjMGREQmpTRnBsWjJJNVp3PT0', + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + 'uid': True, + }, + 'api': { + 'public': { + 'get': [ + 'orderBook', + 'ticker', + 'tickerAll', + 'products', + 'transactions', + 'tradingPairs', + ], + }, + 'private': { + 'post': [ + 'balances', + 'bitcoinCashWithdrawal', + 'bitcoinCashDepositAddresses', + 'bitcoinDepositAddresses', + 'bitcoinWithdrawal', + 'bitcoinWithdrawalFees', + 'buyInstant', + 'buyLimit', + 'cancelOrder', + 'cancelOrderWithInfo', + 'createVoucher', + 'dashDepositAddresses', + 'dashWithdrawal', + 'ethereumWithdrawal', + 'ethereumDepositAddresses', + 'litecoinWithdrawal', + 'litecoinDepositAddresses', + 'openOrders', + 'order', + 'orderHistory', + 'orderById', + 'pusherAuth', + 'redeemVoucher', + 'replaceByBuyLimit', + 'replaceByBuyInstant', + 'replaceBySellLimit', + 'replaceBySellInstant', + 'rippleDepositAddresses', + 'rippleWithdrawal', + 'sellInstant', + 'sellLimit', + 'transactionHistory', + 'traderFees', + 'tradeHistory', + 'transfer', + 'transferHistory', + 'unconfirmedBitcoinDeposits', + 'unconfirmedBitcoinCashDeposits', + 'unconfirmedDashDeposits', + 'unconfirmedEthereumDeposits', + 'unconfirmedLitecoinDeposits', + 'unconfirmedRippleDeposits', + 'cancelAllOpenOrders', + 'withdrawVirtualCurrency', + 'virtualCurrencyDepositAddresses', + 'unconfirmedVirtualCurrencyDeposits', + 'adaWithdrawal', + 'adaDepositAddresses', + 'unconfirmedAdaDeposits', + 'solWithdrawal', + 'solDepositAddresses', + 'unconfirmedSolDeposits', + ], + }, + }, + 'fees': { + 'trading': { + 'tierBased': True, + 'percentage': True, + 'taker': self.parse_number('0.006'), + 'maker': self.parse_number('0.004'), + 'tiers': { + 'taker': [ + [self.parse_number('0'), self.parse_number('0.006')], + [self.parse_number('10000'), self.parse_number('0.003')], + [self.parse_number('100000'), self.parse_number('0.0023')], + [self.parse_number('250000'), self.parse_number('0.0021')], + [self.parse_number('500000'), self.parse_number('0.0018')], + [self.parse_number('1000000'), self.parse_number('0.0015')], + [self.parse_number('3000000'), self.parse_number('0.0012')], + [self.parse_number('15000000'), self.parse_number('0.001')], + ], + 'maker': [ + [self.parse_number('0'), self.parse_number('0.004')], + [self.parse_number('10000'), self.parse_number('0.002')], + [self.parse_number('100000'), self.parse_number('0.0012')], + [self.parse_number('250000'), self.parse_number('0.0009')], + [self.parse_number('500000'), self.parse_number('0.0005')], + [self.parse_number('1000000'), self.parse_number('0.0003')], + [self.parse_number('3000000'), self.parse_number('0.0002')], + [self.parse_number('15000000'), self.parse_number('-0.0004')], + ], + }, + }, + }, + 'options': { + 'withdraw': { + 'fillResponsefromRequest': True, + 'methods': { + 'BTC': 'privatePostBitcoinWithdrawal', + 'LTC': 'privatePostLitecoinWithdrawal', + 'BCH': 'privatePostBitcoinCashWithdrawal', + 'ETH': 'privatePostEthereumWithdrawal', + 'XRP': 'privatePostRippleWithdrawal', + 'DASH': 'privatePostDashWithdrawal', + 'DAI': 'privatePostDaiWithdrawal', + 'ADA': 'privatePostAdaWithdrawal', + 'SOL': 'privatePostSolWithdrawal', + }, + }, + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, # todo implement + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': False, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': True, # todo implement + 'leverage': False, + 'marketBuyByCost': False, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': False, + 'iceberg': True, # todo + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 100000, + 'untilDays': 100000, # todo implement + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': None, + 'untilDays': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchClosedOrders': None, + 'fetchOHLCV': None, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'exceptions': { + 'exact': { + 'No order with given ID': OrderNotFound, + }, + 'broad': { + 'Not enough account balance available': InsufficientFunds, + 'Incorrect order ID': InvalidOrder, + 'Minimum Order Size ': InvalidOrder, + 'max allowed precision': InvalidOrder, # {"error":true,"errorMessage":"USDT_EUR - max allowed precision is 4 decimal places","data":null} + 'TOO MANY REQUESTS': RateLimitExceeded, + 'Access denied.': AuthenticationError, # {"error":true,"errorMessage":"Access denied.","data":null} + }, + }, + 'precisionMode': TICK_SIZE, + }) + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for coinmate + + https://coinmate.docs.apiary.io/#reference/trading-pairs/get-trading-pairs/get + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = await self.publicGetTradingPairs(params) + # + # { + # "error":false, + # "errorMessage":null, + # "data": [ + # { + # "name":"BTC_EUR", + # "firstCurrency":"BTC", + # "secondCurrency":"EUR", + # "priceDecimals":2, + # "lotDecimals":8, + # "minAmount":0.0002, + # "tradesWebSocketChannelId":"trades-BTC_EUR", + # "orderBookWebSocketChannelId":"order_book-BTC_EUR", + # "tradeStatisticsWebSocketChannelId":"statistics-BTC_EUR" + # }, + # ] + # } + # + data = self.safe_value(response, 'data', []) + result = [] + for i in range(0, len(data)): + market = data[i] + id = self.safe_string(market, 'name') + baseId = self.safe_string(market, 'firstCurrency') + quoteId = self.safe_string(market, 'secondCurrency') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + symbol = base + '/' + quote + result.append({ + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': None, + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(self.parse_precision(self.safe_string(market, 'lotDecimals'))), + 'price': self.parse_number(self.parse_precision(self.safe_string(market, 'priceDecimals'))), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(market, 'minAmount'), + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + 'info': market, + }) + return result + + def parse_balance(self, response) -> Balances: + balances = self.safe_value(response, 'data', {}) + result: dict = {'info': response} + currencyIds = list(balances.keys()) + for i in range(0, len(currencyIds)): + currencyId = currencyIds[i] + code = self.safe_currency_code(currencyId) + balance = self.safe_value(balances, currencyId) + account = self.account() + account['free'] = self.safe_string(balance, 'available') + account['used'] = self.safe_string(balance, 'reserved') + account['total'] = self.safe_string(balance, 'balance') + result[code] = account + return self.safe_balance(result) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://coinmate.docs.apiary.io/#reference/balance/get-balances/post + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + response = await self.privatePostBalances(params) + return self.parse_balance(response) + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://coinmate.docs.apiary.io/#reference/order-book/get-order-book/get + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'currencyPair': market['id'], + 'groupByPriceLimit': 'False', + } + response = await self.publicGetOrderBook(self.extend(request, params)) + orderbook = response['data'] + timestamp = self.safe_timestamp(orderbook, 'timestamp') + return self.parse_order_book(orderbook, market['symbol'], timestamp, 'bids', 'asks', 'price', 'amount') + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://coinmate.docs.apiary.io/#reference/ticker/get-ticker/get + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'currencyPair': market['id'], + } + response = await self.publicGetTicker(self.extend(request, params)) + # + # { + # "error": False, + # "errorMessage": null, + # "data": { + # "last": 0.55105, + # "high": 0.56439, + # "low": 0.54358, + # "amount": 37038.993381, + # "bid": 0.54595, + # "ask": 0.55324, + # "change": 3.03659243, + # "open": 0.53481, + # "timestamp": 1708074779 + # } + # } + # + data = self.safe_dict(response, 'data') + return self.parse_ticker(data, market) + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://coinmate.docs.apiary.io/#reference/ticker/get-ticker-all/get + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + response = await self.publicGetTickerAll(params) + # + # { + # "error": False, + # "errorMessage": null, + # "data": { + # "LTC_BTC": { + # "last": "0.001337", + # "high": "0.001348", + # "low": "0.001332", + # "amount": "34.75472959", + # "bid": "0.001348", + # "ask": "0.001356", + # "change": "-0.74239050", + # "open": "0.001347", + # "timestamp": "1708074485" + # } + # } + # } + # + data = self.safe_value(response, 'data', {}) + keys = list(data.keys()) + result: dict = {} + for i in range(0, len(keys)): + market = self.market(keys[i]) + ticker = self.parse_ticker(self.safe_value(data, keys[i]), market) + result[market['symbol']] = ticker + return self.filter_by_array_tickers(result, 'symbol', symbols) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "last": "0.001337", + # "high": "0.001348", + # "low": "0.001332", + # "amount": "34.75472959", + # "bid": "0.001348", + # "ask": "0.001356", + # "change": "-0.74239050", + # "open": "0.001347", + # "timestamp": "1708074485" + # } + # + timestamp = self.safe_timestamp(ticker, 'timestamp') + last = self.safe_number(ticker, 'last') + return self.safe_ticker({ + 'symbol': market['symbol'], + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_number(ticker, 'high'), + 'low': self.safe_number(ticker, 'low'), + 'bid': self.safe_number(ticker, 'bid'), + 'bidVolume': None, + 'ask': self.safe_number(ticker, 'ask'), + 'vwap': None, + 'askVolume': None, + 'open': None, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': self.safe_number(ticker, 'amount'), + 'quoteVolume': None, + 'info': ticker, + }, market) + + async def fetch_deposits_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch history of deposits and withdrawals + + https://coinmate.docs.apiary.io/#reference/transfers/get-transfer-history/post + + :param str [code]: unified currency code for the currency of the deposit/withdrawals, default is None + :param int [since]: timestamp in ms of the earliest deposit/withdrawal, default is None + :param int [limit]: max number of deposit/withdrawals to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `transaction structure ` + """ + await self.load_markets() + request: dict = { + 'limit': 1000, + } + if limit is not None: + request['limit'] = limit + if since is not None: + request['timestampFrom'] = since + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + response = await self.privatePostTransferHistory(self.extend(request, params)) + items = response['data'] + return self.parse_transactions(items, None, since, limit) + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'COMPLETED': 'ok', + 'WAITING': 'pending', + 'SENT': 'pending', + 'CREATED': 'pending', + 'OK': 'ok', + 'NEW': 'pending', + 'CANCELED': 'canceled', + } + return self.safe_string(statuses, status, status) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # deposits + # + # { + # "transactionId": 1862815, + # "timestamp": 1516803982388, + # "amountCurrency": "LTC", + # "amount": 1, + # "fee": 0, + # "walletType": "LTC", + # "transferType": "DEPOSIT", + # "transferStatus": "COMPLETED", + # "txid": + # "ccb9255dfa874e6c28f1a64179769164025329d65e5201849c2400abd6bce245", + # "destination": "LQrtSKA6LnhcwRrEuiborQJnjFF56xqsFn", + # "destinationTag": null + # } + # + # withdrawals + # + # { + # "transactionId": 2140966, + # "timestamp": 1519314282976, + # "amountCurrency": "EUR", + # "amount": 8421.7228, + # "fee": 16.8772, + # "walletType": "BANK_WIRE", + # "transferType": "WITHDRAWAL", + # "transferStatus": "COMPLETED", + # "txid": null, + # "destination": null, + # "destinationTag": null + # } + # + # withdraw + # + # { + # "id": 2132583, + # } + # + timestamp = self.safe_integer(transaction, 'timestamp') + currencyId = self.safe_string(transaction, 'amountCurrency') + code = self.safe_currency_code(currencyId, currency) + return { + 'info': transaction, + 'id': self.safe_string_2(transaction, 'transactionId', 'id'), + 'txid': self.safe_string(transaction, 'txid'), + 'type': self.safe_string_lower(transaction, 'transferType'), + 'currency': code, + 'network': self.safe_string(transaction, 'walletType'), + 'amount': self.safe_number(transaction, 'amount'), + 'status': self.parse_transaction_status(self.safe_string(transaction, 'transferStatus')), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'address': self.safe_string(transaction, 'destination'), + 'addressFrom': None, + 'addressTo': None, + 'tag': self.safe_string(transaction, 'destinationTag'), + 'tagFrom': None, + 'tagTo': None, + 'updated': None, + 'comment': None, + 'internal': None, + 'fee': { + 'cost': self.safe_number(transaction, 'fee'), + 'currency': code, + 'rate': None, + }, + } + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://coinmate.docs.apiary.io/#reference/bitcoin-withdrawal-and-deposit/withdraw-bitcoins/post + https://coinmate.docs.apiary.io/#reference/litecoin-withdrawal-and-deposit/withdraw-litecoins/post + https://coinmate.docs.apiary.io/#reference/ethereum-withdrawal-and-deposit/withdraw-ethereum/post + https://coinmate.docs.apiary.io/#reference/ripple-withdrawal-and-deposit/withdraw-ripple/post + https://coinmate.docs.apiary.io/#reference/cardano-withdrawal-and-deposit/withdraw-cardano/post + https://coinmate.docs.apiary.io/#reference/solana-withdrawal-and-deposit/withdraw-solana/post + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.check_address(address) + await self.load_markets() + currency = self.currency(code) + withdrawOptions = self.safe_value(self.options, 'withdraw', {}) + methods = self.safe_value(withdrawOptions, 'methods', {}) + method = self.safe_string(methods, code) + if method is None: + allowedCurrencies = list(methods.keys()) + raise ExchangeError(self.id + ' withdraw() only allows withdrawing the following currencies: ' + ', '.join(allowedCurrencies)) + request: dict = { + 'amount': self.currency_to_precision(code, amount), + 'address': address, + } + if tag is not None: + request['destinationTag'] = tag + response = await getattr(self, method)(self.extend(request, params)) + # + # { + # "error": False, + # "errorMessage": null, + # "data": { + # "id": "9e0a37fc-4ab4-4b9d-b9e7-c9c8f7c4c8e0" + # } + # } + # + data = self.safe_value(response, 'data') + transaction = self.parse_transaction(data, currency) + fillResponseFromRequest = self.safe_bool(withdrawOptions, 'fillResponseFromRequest', True) + if fillResponseFromRequest: + transaction['amount'] = amount + transaction['currency'] = code + transaction['address'] = address + transaction['tag'] = tag + transaction['type'] = 'withdrawal' + transaction['status'] = 'pending' + return transaction + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://coinmate.docs.apiary.io/#reference/trade-history/get-trade-history/post + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + if limit is None: + limit = 1000 + request: dict = { + 'limit': limit, + } + if symbol is not None: + market = self.market(symbol) + request['currencyPair'] = market['id'] + if since is not None: + request['timestampFrom'] = since + response = await self.privatePostTradeHistory(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + return self.parse_trades(data, None, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchMyTrades(private) + # + # { + # "transactionId": 2671819, + # "createdTimestamp": 1529649127605, + # "currencyPair": "LTC_BTC", + # "type": "BUY", + # "orderType": "LIMIT", + # "orderId": 101810227, + # "amount": 0.01, + # "price": 0.01406, + # "fee": 0, + # "feeType": "MAKER" + # } + # + # fetchTrades(public) + # + # { + # "timestamp":1561598833416, + # "transactionId":"4156303", + # "price":10950.41, + # "amount":0.004, + # "currencyPair":"BTC_EUR", + # "tradeType":"BUY" + # } + # + marketId = self.safe_string(trade, 'currencyPair') + market = self.safe_market(marketId, market, '_') + priceString = self.safe_string(trade, 'price') + amountString = self.safe_string(trade, 'amount') + side = self.safe_string_lower_2(trade, 'type', 'tradeType') + type = self.safe_string_lower(trade, 'orderType') + orderId = self.safe_string(trade, 'orderId') + id = self.safe_string(trade, 'transactionId') + timestamp = self.safe_integer_2(trade, 'timestamp', 'createdTimestamp') + fee = None + feeCostString = self.safe_string(trade, 'fee') + if feeCostString is not None: + fee = { + 'cost': feeCostString, + 'currency': market['quote'], + } + takerOrMaker = self.safe_string(trade, 'feeType') + takerOrMaker = 'maker' if (takerOrMaker == 'MAKER') else 'taker' + return self.safe_trade({ + 'id': id, + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'type': type, + 'side': side, + 'order': orderId, + 'takerOrMaker': takerOrMaker, + 'price': priceString, + 'amount': amountString, + 'cost': None, + 'fee': fee, + }, market) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://coinmate.docs.apiary.io/#reference/transactions/transactions/get + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'currencyPair': market['id'], + 'minutesIntoHistory': 10, + } + response = await self.publicGetTransactions(self.extend(request, params)) + # + # { + # "error":false, + # "errorMessage":null, + # "data":[ + # { + # "timestamp":1561598833416, + # "transactionId":"4156303", + # "price":10950.41, + # "amount":0.004, + # "currencyPair":"BTC_EUR", + # "tradeType":"BUY" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_trades(data, market, since, limit) + + async def fetch_trading_fee(self, symbol: str, params={}) -> TradingFeeInterface: + """ + fetch the trading fees for a market + + https://coinmate.docs.apiary.io/#reference/trader-fees/get-trading-fees/post + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `fee structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'currencyPair': market['id'], + } + response = await self.privatePostTraderFees(self.extend(request, params)) + # + # { + # "error": False, + # "errorMessage": null, + # "data": {maker: '0.3', taker: "0.35", timestamp: "1646253217815"} + # } + # + data = self.safe_value(response, 'data', {}) + makerString = self.safe_string(data, 'maker') + takerString = self.safe_string(data, 'taker') + maker = self.parse_number(Precise.string_div(makerString, '100')) + taker = self.parse_number(Precise.string_div(takerString, '100')) + return { + 'info': data, + 'symbol': market['symbol'], + 'maker': maker, + 'taker': taker, + 'percentage': True, + 'tierBased': True, + } + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://coinmate.docs.apiary.io/#reference/order/get-open-orders/post + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + response = await self.privatePostOpenOrders(self.extend({}, params)) + extension: dict = {'status': 'open'} + return self.parse_orders(response['data'], None, since, limit, extension) + + async def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://coinmate.docs.apiary.io/#reference/order/order-history/post + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrders() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'currencyPair': market['id'], + } + # offset param that appears in other parts of the API doesn't appear to be supported here + if limit is not None: + request['limit'] = limit + response = await self.privatePostOrderHistory(self.extend(request, params)) + return self.parse_orders(response['data'], market, since, limit) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'FILLED': 'closed', + 'CANCELLED': 'canceled', + 'PARTIALLY_FILLED': 'open', + 'OPEN': 'open', + } + return self.safe_string(statuses, status, status) + + def parse_order_type(self, type: Str): + types: dict = { + 'LIMIT': 'limit', + 'MARKET': 'market', + } + return self.safe_string(types, type, type) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # limit sell + # + # { + # "id": 781246605, + # "timestamp": 1584480015133, + # "trailingUpdatedTimestamp": null, + # "type": "SELL", + # "currencyPair": "ETH_BTC", + # "price": 0.0345, + # "amount": 0.01, + # "stopPrice": null, + # "originalStopPrice": null, + # "marketPriceAtLastUpdate": null, + # "marketPriceAtOrderCreation": null, + # "orderTradeType": "LIMIT", + # "hidden": False, + # "trailing": False, + # "clientOrderId": null + # } + # + # limit buy + # + # { + # "id": 67527001, + # "timestamp": 1517931722613, + # "trailingUpdatedTimestamp": null, + # "type": "BUY", + # "price": 5897.24, + # "remainingAmount": 0.002367, + # "originalAmount": 0.1, + # "stopPrice": null, + # "originalStopPrice": null, + # "marketPriceAtLastUpdate": null, + # "marketPriceAtOrderCreation": null, + # "status": "CANCELLED", + # "orderTradeType": "LIMIT", + # "hidden": False, + # "avgPrice": null, + # "trailing": False, + # } + # + # cancelOrder + # + # { + # "success": True, + # "remainingAmount": 0.1 + # } + # + id = self.safe_string(order, 'id') + timestamp = self.safe_integer(order, 'timestamp') + side = self.safe_string_lower(order, 'type') + priceString = self.safe_string(order, 'price') + amountString = self.safe_string(order, 'originalAmount') + remainingString = self.safe_string_2(order, 'remainingAmount', 'amount') + status = self.parse_order_status(self.safe_string(order, 'status')) + type = self.parse_order_type(self.safe_string(order, 'orderTradeType')) + averageString = self.safe_string(order, 'avgPrice') + marketId = self.safe_string(order, 'currencyPair') + symbol = self.safe_symbol(marketId, market, '_') + clientOrderId = self.safe_string(order, 'clientOrderId') + return self.safe_order({ + 'id': id, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'symbol': symbol, + 'type': type, + 'timeInForce': None, + 'postOnly': None, + 'side': side, + 'price': priceString, + 'triggerPrice': self.safe_number(order, 'stopPrice'), + 'amount': amountString, + 'cost': None, + 'average': averageString, + 'filled': None, + 'remaining': remainingString, + 'status': status, + 'trades': None, + 'info': order, + 'fee': None, + }, market) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://coinmate.docs.apiary.io/#reference/order/buy-limit-order/post + https://coinmate.docs.apiary.io/#reference/order/sell-limit-order/post + https://coinmate.docs.apiary.io/#reference/order/buy-instant-order/post + https://coinmate.docs.apiary.io/#reference/order/sell-instant-order/post + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + method = 'privatePost' + self.capitalize(side) + market = self.market(symbol) + request: dict = { + 'currencyPair': market['id'], + } + if type == 'market': + if side == 'buy': + request['total'] = self.amount_to_precision(symbol, amount) # amount in fiat + else: + request['amount'] = self.amount_to_precision(symbol, amount) # amount in fiat + method += 'Instant' + else: + request['amount'] = self.amount_to_precision(symbol, amount) # amount in crypto + request['price'] = self.price_to_precision(symbol, price) + method += self.capitalize(type) + response = await getattr(self, method)(self.extend(request, params)) + id = self.safe_string(response, 'data') + return self.safe_order({ + 'info': response, + 'id': id, + }, market) + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://coinmate.docs.apiary.io/#reference/order/get-order-by-orderid/post + https://coinmate.docs.apiary.io/#reference/order/get-order-by-clientorderid/post + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + request: dict = { + 'orderId': id, + } + market = None + if symbol: + market = self.market(symbol) + response = await self.privatePostOrderById(self.extend(request, params)) + data = self.safe_dict(response, 'data') + return self.parse_order(data, market) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://coinmate.docs.apiary.io/#reference/order/cancel-order/post + + :param str id: order id + :param str symbol: not used by coinmate cancelOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + # {"error":false,"errorMessage":null,"data":{"success":true,"remainingAmount":0.01}} + request: dict = {'orderId': id} + response = await self.privatePostCancelOrderWithInfo(self.extend(request, params)) + # + # { + # "error": False, + # "errorMessage": null, + # "data": { + # "success": True, + # "remainingAmount": 0.1 + # } + # } + # + data = self.safe_dict(response, 'data') + return self.parse_order(data) + + def nonce(self): + return self.milliseconds() + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = self.urls['api']['rest'] + '/' + path + if api == 'public': + if params: + url += '?' + self.urlencode(params) + else: + self.check_required_credentials() + nonce = str(self.nonce()) + auth = nonce + self.uid + self.apiKey + signature = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256) + body = self.urlencode(self.extend({ + 'clientId': self.uid, + 'nonce': nonce, + 'publicKey': self.apiKey, + 'signature': signature.upper(), + }, params)) + headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + } + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None # fallback to default error handler + # + # {"error":true,"errorMessage":"Api internal error","data":null} + # {"error":true,"errorMessage":"Access denied.","data":null} + # + errorMessage = self.safe_string(response, 'errorMessage') + if errorMessage is not None: + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], errorMessage, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], errorMessage, feedback) + raise ExchangeError(feedback) # unknown message + return None diff --git a/ccxt/async_support/coinmetro.py b/ccxt/async_support/coinmetro.py new file mode 100644 index 0000000..15d96cc --- /dev/null +++ b/ccxt/async_support/coinmetro.py @@ -0,0 +1,1946 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.coinmetro import ImplicitAPI +import asyncio +from ccxt.base.types import Any, Balances, Currencies, Currency, IndexType, Int, LedgerEntry, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class coinmetro(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(coinmetro, self).describe(), { + 'id': 'coinmetro', + 'name': 'Coinmetro', + 'countries': ['EE'], # Republic of Estonia + 'version': 'v1', + 'rateLimit': 200, # 1 request per 200 ms, 20 per minute, 300 per hour, 1k per day + 'certified': False, + 'pro': False, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': True, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': True, + 'borrowIsolatedMargin': False, + 'cancelAllOrders': False, + 'cancelOrder': True, + 'cancelOrders': False, + 'closeAllPositions': False, + 'closePosition': True, + 'createDepositAddress': False, + 'createOrder': True, + 'createPostOnlyOrder': False, + 'createReduceOnlyOrder': False, + 'createStopLimitOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'deposit': False, + 'editOrder': False, + 'fetchAccounts': False, + 'fetchBalance': True, + 'fetchBidsAsks': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchCanceledAndClosedOrders': True, + 'fetchCanceledOrders': False, + 'fetchClosedOrder': False, + 'fetchClosedOrders': False, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDeposit': False, + 'fetchDepositAddress': False, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': False, + 'fetchDepositsWithdrawals': False, + 'fetchDepositWithdrawFee': False, + 'fetchDepositWithdrawFees': False, + 'fetchFundingHistory': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchL3OrderBook': False, + 'fetchLedger': True, + 'fetchLeverage': False, + 'fetchLeverageTiers': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterestHistory': False, + 'fetchOpenOrder': False, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrderBooks': False, + 'fetchOrders': False, + 'fetchOrderTrades': False, + 'fetchPosition': False, + 'fetchPositions': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchStatus': False, + 'fetchTicker': False, + 'fetchTickers': True, + 'fetchTime': False, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': False, + 'fetchTradingLimits': False, + 'fetchTransactionFee': False, + 'fetchTransactionFees': False, + 'fetchTransactions': False, + 'fetchTransfers': False, + 'fetchWithdrawal': False, + 'fetchWithdrawals': False, + 'fetchWithdrawalWhitelist': False, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'sandbox': True, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'signIn': False, + 'transfer': False, + 'withdraw': False, + 'ws': False, + }, + 'timeframes': { + '1m': '60000', + '5m': '300000', + '30m': '1800000', + '4h': '14400000', + '1d': '86400000', + }, + 'urls': { + 'logo': 'https://github.com/ccxt/ccxt/assets/43336371/e86f87ec-6ba3-4410-962b-f7988c5db539', + 'api': { + 'public': 'https://api.coinmetro.com', + 'private': 'https://api.coinmetro.com', + }, + 'test': { + 'public': 'https://api.coinmetro.com/open', + 'private': 'https://api.coinmetro.com/open', + }, + 'www': 'https://coinmetro.com/', + 'doc': [ + 'https://documenter.getpostman.com/view/3653795/SVfWN6KS', + ], + 'fees': 'https://help.coinmetro.com/hc/en-gb/articles/6844007317789-What-are-the-fees-on-Coinmetro-', + 'referral': 'https://go.coinmetro.com/?ref=crypto24', + }, + 'api': { + 'public': { + 'get': { + 'demo/temp': 1, + 'exchange/candles/{pair}/{timeframe}/{from}/{to}': 3, + 'exchange/prices': 1, + 'exchange/ticks/{pair}/{from}': 3, + 'assets': 1, + 'markets': 1, + 'exchange/book/{pair}': 3, + 'exchange/bookUpdates/{pair}/{from}': 1, # not unified + }, + }, + 'private': { + 'get': { + 'users/balances': 1, + 'users/wallets': 1, + 'users/wallets/history/{since}': 1.67, + 'exchange/orders/status/{orderID}': 1, + 'exchange/orders/active': 1, + 'exchange/orders/history/{since}': 1.67, + 'exchange/fills/{since}': 1.67, + 'exchange/margin': 1, # not unified + }, + 'post': { + 'jwt': 1, # not unified + 'jwtDevice': 1, # not unified + 'devices': 1, # not unified + 'jwt-read-only': 1, # not unified + 'exchange/orders/create': 1, + 'exchange/orders/modify/{orderID}': 1, # not unified + 'exchange/swap': 1, # not unified + 'exchange/swap/confirm/{swapId}': 1, # not unified + 'exchange/orders/close/{orderID}': 1, + 'exchange/orders/hedge': 1, # not unified + }, + 'put': { + 'jwt': 1, # not unified + 'exchange/orders/cancel/{orderID}': 1, + 'users/margin/collateral': 1, + 'users/margin/primary/{currency}': 1, # not unified + }, + }, + }, + 'requiredCredentials': { + 'apiKey': False, + 'secret': False, + 'uid': True, + 'token': True, + }, + 'fees': { + 'trading': { + 'feeSide': 'get', + 'tierBased': False, + 'percentage': True, + 'taker': self.parse_number('0.001'), + 'maker': self.parse_number('0'), + }, + }, + 'precisionMode': TICK_SIZE, + # exchange-specific options + 'options': { + 'currenciesByIdForParseMarket': None, + 'currencyIdsListForParseMarket': ['QRDO'], + }, + 'features': { + 'spot': { + 'sandbox': True, + 'createOrder': { + 'marginMode': True, # todo implement + 'triggerPrice': True, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': { + 'triggerPriceType': None, + 'price': False, + }, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': False, + 'GTD': True, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': False, + 'iceberg': True, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': None, + 'daysBack': 100000, + 'untilDays': None, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': None, + 'daysBack': 100000, + 'untilDays': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchClosedOrders': None, + 'fetchOHLCV': { + 'limit': 1000, + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'exceptions': { + # https://trade-docs.coinmetro.co/?javascript--nodejs#message-codes + 'exact': { + 'Both buyingCurrency and sellingCurrency are required': InvalidOrder, # 422 - "Both buyingCurrency and sellingCurrency are required" + 'One and only one of buyingQty and sellingQty is required': InvalidOrder, # 422 - "One and only one of buyingQty and sellingQty is required" + 'Invalid buyingCurrency': InvalidOrder, # 422 - "Invalid buyingCurrency" + 'Invalid \'from\'': BadRequest, # 422 Unprocessable Entity {"message":"Invalid 'from'"} + 'Invalid sellingCurrency': InvalidOrder, # 422 - "Invalid sellingCurrency" + 'Invalid buyingQty': InvalidOrder, # 422 - "Invalid buyingQty" + 'Invalid sellingQty': InvalidOrder, # 422 - "Invalid sellingQty" + 'Insufficient balance': InsufficientFunds, # 422 - "Insufficient balance" + 'Expiration date is in the past or too near in the future': InvalidOrder, # 422 Unprocessable Entity {"message":"Expiration date is in the past or too near in the future"} + 'Forbidden': PermissionDenied, # 403 Forbidden {"message":"Forbidden"} + 'Order Not Found': OrderNotFound, # 404 Not Found {"message":"Order Not Found"} + 'since must be a millisecond timestamp': BadRequest, # 422 Unprocessable Entity {"message":"since must be a millisecond timestamp"} + 'This pair is disabled on margin': BadSymbol, # 422 Unprocessable Entity {"message":"This pair is disabled on margin"} + }, + 'broad': { + 'accessing from a new IP': PermissionDenied, # 403 Forbidden {"message":"You're accessing from a new IP. Please check your email."} + 'available to allocate': InsufficientFunds, # 403 Forbidden {"message":"Insufficient EUR available to allocate"} + 'At least': BadRequest, # 422 Unprocessable Entity {"message":"At least 5 EUR per operation"} + 'collateral is not allowed': BadRequest, # 422 Unprocessable Entity {"message":"DOGE collateral is not allowed"} + 'Insufficient liquidity': InvalidOrder, # 503 Service Unavailable {"message":"Insufficient liquidity to fill the FOK order completely."} + 'Insufficient order size': InvalidOrder, # 422 Unprocessable Entity {"message":"Insufficient order size - min 0.002 ETH"} + 'Invalid quantity': InvalidOrder, # 422 Unprocessable Entity {"message":"Invalid quantity!"} + 'Invalid Stop Loss': InvalidOrder, # 422 Unprocessable Entity {"message":"Invalid Stop Loss!"} + 'Invalid stop price!': InvalidOrder, # 422 Unprocessable Entity {"message":"Invalid stop price!"} + 'Not enough balance': InsufficientFunds, # 422 Unprocessable Entity {"message":"Not enough balance!"} + 'Not enough margin': InsufficientFunds, # Unprocessable Entity {"message":"Not enough margin!"} + 'orderType missing': BadRequest, # 422 Unprocessable Entity {"message":"orderType missing!"} + 'Server Timeout': ExchangeError, # 503 Service Unavailable {"message":"Server Timeout!"} + 'Time in force has to be IOC or FOK for market orders': InvalidOrder, # 422 Unprocessable Entity {"message":"Time in force has to be IOC or FOK for market orders!"} + 'Too many attempts': RateLimitExceeded, # 429 Too Many Requests {"message":"Too many attempts. Try again in 3 seconds"} + }, + }, + }) + + async def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://documenter.getpostman.com/view/3653795/SVfWN6KS#d5876d43-a3fe-4479-8c58-24d0f044edfb + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = await self.publicGetAssets(params) + # + # [ + # { + # "symbol": "BTC", + # "name": "Bitcoin", + # "color": "#FFA500", + # "type": "coin", + # "canDeposit": True, + # "canWithdraw": True, + # "canTrade": True, + # "notabeneDecimals": 8, + # "canMarket": True, + # "maxSwap": 10000, + # "digits": 6, + # "multiplier": 1000000, + # "bookDigits": 8, + # "bookMultiplier": 100000000, + # "sentimentData": { + # "sentiment": 51.59555555555555, + # "interest": 1.127511216044664 + # }, + # "minQty": 0.0001 + # }, + # { + # "symbol": "EUR", + # "name": "Euro", + # "color": "#1246FF", + # "type": "fiat", + # "canDeposit": True, + # "canWithdraw": True, + # "canTrade": True, + # "canMarket": True, + # "maxSwap": 10000, + # "digits": 2, + # "multiplier": 100, + # "bookDigits": 3, + # "bookMultiplier": 1000, + # "minQty": 5 + # } + # ... + # ] + # + result: dict = {} + for i in range(0, len(response)): + currency = response[i] + id = self.safe_string(currency, 'symbol') + code = self.safe_currency_code(id) + typeRaw = self.safe_string(currency, 'type') + type = None + if typeRaw == 'coin' or typeRaw == 'token' or typeRaw == 'erc20': + type = 'crypto' + elif typeRaw == 'fiat': + type = 'fiat' + precisionDigits = self.safe_string_2(currency, 'digits', 'notabeneDecimals') + if code == 'RENDER': + # RENDER is an exception(with broken info) + precisionDigits = '4' + result[code] = self.safe_currency_structure({ + 'id': id, + 'code': code, + 'name': code, + 'type': type, + 'info': currency, + 'active': self.safe_bool(currency, 'canTrade'), + 'deposit': self.safe_bool(currency, 'canDeposit'), + 'withdraw': self.safe_bool(currency, 'canWithdraw'), + 'fee': None, + 'precision': self.parse_number(self.parse_precision(precisionDigits)), + 'limits': { + 'amount': { + 'min': self.safe_number(currency, 'minQty'), + 'max': None, + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + }, + 'networks': {}, + }) + if self.safe_value(self.options, 'currenciesByIdForParseMarket') is None: + currenciesById = self.index_by(result, 'id') + self.options['currenciesByIdForParseMarket'] = currenciesById + currentCurrencyIdsList = self.safe_list(self.options, 'currencyIdsListForParseMarket', []) + currencyIdsList = list(currenciesById.keys()) + for i in range(0, len(currencyIdsList)): + currentCurrencyIdsList.append(currencyIdsList[i]) + self.options['currencyIdsListForParseMarket'] = currentCurrencyIdsList + return result + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for coinmetro + + https://documenter.getpostman.com/view/3653795/SVfWN6KS#9fd18008-338e-4863-b07d-722878a46832 + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + promises = [] + promises.append(self.publicGetMarkets(params)) + if self.safe_value(self.options, 'currenciesByIdForParseMarket') is None: + promises.append(self.fetch_currencies()) + responses = await asyncio.gather(*promises) + response = responses[0] + # + # [ + # { + # "pair": "YFIEUR", + # "precision": 5, + # "margin": False + # }, + # { + # "pair": "BTCEUR", + # "precision": 2, + # "margin": True + # }, + # ... + # ] + # + result = [] + for i in range(0, len(response)): + market = self.parse_market(response[i]) + # there are several broken(unavailable info) markets + if market['base'] is None or market['quote'] is None: + continue + result.append(market) + return result + + def parse_market(self, market: dict) -> Market: + id = self.safe_string(market, 'pair') + parsedMarketId = self.parse_market_id(id) + baseId = self.safe_string(parsedMarketId, 'baseId') + quoteId = self.safe_string(parsedMarketId, 'quoteId') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + basePrecisionAndLimits = self.parse_market_precision_and_limits(baseId) + quotePrecisionAndLimits = self.parse_market_precision_and_limits(quoteId) + margin = self.safe_bool(market, 'margin', False) + tradingFees = self.safe_value(self.fees, 'trading', {}) + return self.safe_market_structure({ + 'id': id, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': margin, + 'swap': False, + 'future': False, + 'option': False, + 'active': True, + 'contract': False, + 'linear': None, + 'inverse': None, + 'taker': self.safe_number(tradingFees, 'taker'), + 'maker': self.safe_number(tradingFees, 'maker'), + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': basePrecisionAndLimits['precision'], + 'price': self.parse_number(self.parse_precision(self.safe_string(market, 'precision'))), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': basePrecisionAndLimits['minLimit'], + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': quotePrecisionAndLimits['minLimit'], + 'max': None, + }, + }, + 'created': None, + 'info': market, + }) + + def parse_market_id(self, marketId): + baseId = None + quoteId = None + currencyIds = self.safe_value(self.options, 'currencyIdsListForParseMarket', []) + # Bubble sort by length(longest first) + currencyIdsLength = len(currencyIds) + for i in range(0, currencyIdsLength): + for j in range(0, currencyIdsLength - i - 1): + a = currencyIds[j] + b = currencyIds[j + 1] + if len(a) < len(b): + currencyIds[j] = b + currencyIds[j + 1] = a + for i in range(0, len(currencyIds)): + currencyId = currencyIds[i] + entryIndex = marketId.find(currencyId) + if entryIndex == 0: + restId = marketId.replace(currencyId, '') + if self.in_array(restId, currencyIds): + if entryIndex == 0: + baseId = currencyId + quoteId = restId + else: + baseId = restId + quoteId = currencyId + break + if baseId is None or quoteId is None: + # https://github.com/ccxt/ccxt/issues/26820 + if marketId.endswith('USDT'): + baseId = marketId.replace('USDT', '') + quoteId = 'USDT' + if marketId.endswith('USD'): + baseId = marketId.replace('USD', '') + quoteId = 'USD' + result: dict = { + 'baseId': baseId, + 'quoteId': quoteId, + } + return result + + def parse_market_precision_and_limits(self, currencyId): + currencies = self.safe_value(self.options, 'currenciesByIdForParseMarket', {}) + currency = self.safe_value(currencies, currencyId, {}) + limits = self.safe_value(currency, 'limits', {}) + amountLimits = self.safe_value(limits, 'amount', {}) + minLimit = self.safe_number(amountLimits, 'min') + result: dict = { + 'precision': self.safe_number(currency, 'precision'), + 'minLimit': minLimit, + } + return result + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://documenter.getpostman.com/view/3653795/SVfWN6KS#13cfb5bc-7bfb-4847-85e1-e0f35dfb3573 + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch entries for + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + 'timeframe': self.safe_string(self.timeframes, timeframe, timeframe), + } + until = None + if since is not None: + request['from'] = since + if limit is not None: + duration = self.parse_timeframe(timeframe) * 1000 + until = self.sum(since, duration * (limit)) + else: + request['from'] = ':from' # self endpoint doesn't accept empty from and to params(setting them into the value described in the documentation) + until = self.safe_integer(params, 'until', until) + if until is not None: + params = self.omit(params, ['until']) + request['to'] = until + else: + request['to'] = ':to' + response = await self.publicGetExchangeCandlesPairTimeframeFromTo(self.extend(request, params)) + # + # { + # "candleHistory": [ + # { + # "pair": "ETHUSDT", + # "timeframe": 86400000, + # "timestamp": 1697673600000, + # "c": 1567.4409353098604, + # "h": 1566.7514068472303, + # "l": 1549.4563666936847, + # "o": 1563.4490341395904, + # "v": 0 + # }, + # { + # "pair": "ETHUSDT", + # "timeframe": 86400000, + # "timestamp": 1697760000000, + # "c": 1603.7831363339324, + # "h": 1625.0356823666407, + # "l": 1565.4629390011505, + # "o": 1566.8387619426028, + # "v": 0 + # }, + # ... + # ] + # } + # + candleHistory = self.safe_list(response, 'candleHistory', []) + return self.parse_ohlcvs(candleHistory, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + return [ + self.safe_integer(ohlcv, 'timestamp'), + self.safe_number(ohlcv, 'o'), + self.safe_number(ohlcv, 'h'), + self.safe_number(ohlcv, 'l'), + self.safe_number(ohlcv, 'c'), + self.safe_number(ohlcv, 'v'), + ] + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://documenter.getpostman.com/view/3653795/SVfWN6KS#6ee5d698-06da-4570-8c84-914185e05065 + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch(default 200, max 500) + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + if since is not None: + request['from'] = since + else: + # self endpoint accepts empty from param + request['from'] = '' + response = await self.publicGetExchangeTicksPairFrom(self.extend(request, params)) + # + # { + # "tickHistory": [ + # { + # "pair": "ETHUSDT", + # "price": 2077.5623, + # "qty": 0.002888, + # "timestamp": 1700684689420, + # "seqNum": 10644554718 + # }, + # { + # "pair": "ETHUSDT", + # "price": 2078.3848, + # "qty": 0.003368, + # "timestamp": 1700684738410, + # "seqNum": 10644559561 + # }, + # { + # "pair": "ETHUSDT", + # "price": 2077.1513, + # "qty": 0.00337, + # "timestamp": 1700684816853, + # "seqNum": 10644567113 + # }, + # ... + # ] + # } + # + tickHistory = self.safe_list(response, 'tickHistory', []) + return self.parse_trades(tickHistory, market, since, limit) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://documenter.getpostman.com/view/3653795/SVfWN6KS#4d48ae69-8ee2-44d1-a268-71f84e557b7b + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve(default 500, max 1000) + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + request: dict = {} + if since is not None: + request['since'] = since + else: + # the exchange requires a value for the since param + request['since'] = 0 + response = await self.privateGetExchangeFillsSince(self.extend(request, params)) + # + # [ + # { + # "pair": "ETHUSDC", + # "seqNumber": 10873722343, + # "timestamp": 1702570610747, + # "qty": 0.002, + # "price": 2282, + # "side": "buy", + # "orderID": "65671262d93d9525ac009e36170257061073952c6423a8c5b4d6c" + # }, + # ... + # ] + # + return self.parse_trades(response, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades + # { + # "pair": "ETHUSDT", + # "price": 2077.1513, + # "qty": 0.00337, + # "timestamp": 1700684816853, + # "seqNum": 10644567113 + # }, + # + # fetchMyTrades + # { + # "pair": "ETHUSDC", + # "seqNumber": 10873722343, + # "timestamp": 1702570610747, + # "qty": 0.002, + # "price": 2282, + # "side": "buy", + # "orderID": "65671262d93d9525ac009e36170257061073952c6423a8c5b4d6c" + # } + # + # fetchOrders + # { + # "_id": "657b31d360a9542449381bdc", + # "seqNumber": 10873722343, + # "timestamp": 1702570610747, + # "qty": 0.002, + # "price": 2282, + # "side": "buy" + # } + # + # { + # "pair":"ETHUSDC", + # "seqNumber":"10873722343", + # "timestamp":"1702570610747", + # "qty":"0.002", + # "price":"2282", + # "side":"buy", + # "orderID":"65671262d93d9525ac009e36170257061073952c6423a8c5b4d6c", + # "userID":"65671262d93d9525ac009e36" + # } + # + marketId = self.safe_string_2(trade, 'symbol', 'pair') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + id = self.safe_string_n(trade, ['_id', 'seqNum', 'seqNumber']) + timestamp = self.safe_integer(trade, 'timestamp') + priceString = self.safe_string(trade, 'price') + amountString = self.safe_string(trade, 'qty') + order = self.safe_string(trade, 'orderID') + side = self.safe_string(trade, 'side') + return self.safe_trade({ + 'id': id, + 'order': order, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'type': None, + 'side': side, + 'takerOrMaker': None, + 'price': priceString, + 'amount': amountString, + 'cost': None, + 'fee': None, + 'info': trade, + }, market) + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://documenter.getpostman.com/view/3653795/SVfWN6KS#26ad80d7-8c46-41b5-9208-386f439a8b87 + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return(default 100, max 200) + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + response = await self.publicGetExchangeBookPair(self.extend(request, params)) + # + # { + # "book": { + # "pair": "ETHUSDT", + # "seqNumber": 10800409239, + # "ask": { + # "2354.2861": 3.75, + # "2354.3138": 19, + # "2354.7538": 80, + # "2355.5430": 260, + # "2356.4611": 950, + # "2361.7150": 1500, + # "206194.0000": 0.01 + # }, + # "bid": { + # "2352.6339": 3.75, + # "2352.6002": 19, + # "2352.2402": 80, + # "2351.4582": 260, + # "2349.3111": 950, + # "2343.8601": 1500, + # "1.0000": 5 + # }, + # "checksum": 2108177337 + # } + # } + # + book = self.safe_value(response, 'book', {}) + rawBids = self.safe_value(book, 'bid', {}) + rawAsks = self.safe_value(book, 'ask', {}) + rawOrderbook: dict = { + 'bids': rawBids, + 'asks': rawAsks, + } + orderbook = self.parse_order_book(rawOrderbook, symbol) + orderbook['nonce'] = self.safe_integer(book, 'seqNumber') + return orderbook + + def parse_bids_asks(self, bidasks, priceKey: IndexType = 0, amountKey: IndexType = 1, countOrIdKey: IndexType = 2): + prices = list(bidasks.keys()) + result = [] + for i in range(0, len(prices)): + priceString = self.safe_string(prices, i) + price = self.safe_number(prices, i) + volume = self.safe_number(bidasks, priceString) + result.append([price, volume]) + return result + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://documenter.getpostman.com/view/3653795/SVfWN6KS#6ecd1cd1-f162-45a3-8b3b-de690332a485 + + :param str[] [symbols]: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + response = await self.publicGetExchangePrices(params) + # + # { + # "latestPrices": [ + # { + # "pair": "PERPEUR", + # "timestamp": 1702549840393, + # "price": 0.7899997816001223, + # "qty": 1e-12, + # "ask": 0.8, + # "bid": 0.7799995632002446 + # }, + # { + # "pair": "PERPUSD", + # "timestamp": 1702549841973, + # "price": 0.8615317721366659, + # "qty": 1e-12, + # "ask": 0.8742333599999257, + # "bid": 0.8490376365388491 + # }, + # ... + # ], + # "24hInfo": [ + # { + # "delta": 0.25396444229149906, + # "h": 0.78999978160012, + # "l": 0.630001740844, + # "v": 54.910000002833996, + # "pair": "PERPEUR", + # "sentimentData": { + # "sentiment": 36.71333333333333, + # "interest": 0.47430830039525695 + # } + # }, + # { + # "delta": 0.26915154078134096, + # "h": 0.86220315458898, + # "l": 0.67866757035154, + # "v": 2.835000000000001e-9, + # "pair": "PERPUSD", + # "sentimentData": { + # "sentiment": 36.71333333333333, + # "interest": 0.47430830039525695 + # } + # }, + # ... + # ] + # } + # + latestPrices = self.safe_value(response, 'latestPrices', []) + twentyFourHInfos = self.safe_value(response, '24hInfo', []) + tickersObject: dict = {} + # merging info from two lists into one + for i in range(0, len(latestPrices)): + latestPrice = latestPrices[i] + marketId = self.safe_string(latestPrice, 'pair') + if marketId is not None: + tickersObject[marketId] = latestPrice + for i in range(0, len(twentyFourHInfos)): + twentyFourHInfo = twentyFourHInfos[i] + marketId = self.safe_string(twentyFourHInfo, 'pair') + if marketId is not None: + latestPrice = self.safe_value(tickersObject, marketId, {}) + tickersObject[marketId] = self.extend(twentyFourHInfo, latestPrice) + tickers = list(tickersObject.values()) + return self.parse_tickers(tickers, symbols) + + async def fetch_bids_asks(self, symbols: Strings = None, params={}): + """ + fetches the bid and ask price and volume for multiple markets + + https://documenter.getpostman.com/view/3653795/SVfWN6KS#6ecd1cd1-f162-45a3-8b3b-de690332a485 + + :param str[] [symbols]: unified symbols of the markets to fetch the bids and asks for, all markets are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + response = await self.publicGetExchangePrices(params) + latestPrices = self.safe_list(response, 'latestPrices', []) + return self.parse_tickers(latestPrices, symbols) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "pair": "PERPUSD", + # "timestamp": 1702549841973, + # "price": 0.8615317721366659, + # "qty": 1e-12, + # "ask": 0.8742333599999257, + # "bid": 0.8490376365388491 + # "delta": 0.26915154078134096, + # "h": 0.86220315458898, + # "l": 0.67866757035154, + # "v": 2.835000000000001e-9, + # "sentimentData": { + # "sentiment": 36.71333333333333, + # "interest": 0.47430830039525695 + # } + # } + # + marketId = self.safe_string(ticker, 'pair') + market = self.safe_market(marketId, market) + timestamp = self.safe_integer(ticker, 'timestamp') + bid = self.safe_string(ticker, 'bid') + ask = self.safe_string(ticker, 'ask') + high = self.safe_string(ticker, 'h') + low = self.safe_string(ticker, 'l') + last = self.safe_string(ticker, 'price') + baseVolume = self.safe_string(ticker, 'v') + delta = self.safe_string(ticker, 'delta') + percentage = Precise.string_mul(delta, '100') + return self.safe_ticker({ + 'symbol': market['symbol'], + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'open': None, + 'high': high, + 'low': low, + 'close': None, + 'last': last, + 'bid': bid, + 'bidVolume': None, + 'ask': ask, + 'askVolume': None, + 'vwap': None, + 'previousClose': None, + 'change': None, + 'percentage': percentage, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': None, + 'info': ticker, + }, market) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://documenter.getpostman.com/view/3653795/SVfWN6KS#741a1dcc-7307-40d0-acca-28d003d1506a + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + response = await self.privateGetUsersWallets(params) + list = self.safe_list(response, 'list', []) + return self.parse_balance(list) + + def parse_balance(self, balances) -> Balances: + # + # [ + # { + # "xcmLocks": [], + # "xcmLockAmounts": [], + # "refList": [], + # "balanceHistory": [], + # "_id": "5fecd3c998e75c2e4d63f7c3", + # "currency": "BTC", + # "label": "BTC", + # "userId": "5fecd3c97fbfed1521db23bd", + # "__v": 0, + # "balance": 0.5, + # "createdAt": "2020-12-30T19:23:53.646Z", + # "disabled": False, + # "updatedAt": "2020-12-30T19:23:53.653Z", + # "reserved": 0, + # "id": "5fecd3c998e75c2e4d63f7c3" + # }, + # ... + # ] + # + result: dict = { + 'info': balances, + } + for i in range(0, len(balances)): + balanceEntry = self.safe_dict(balances, i, {}) + currencyId = self.safe_string(balanceEntry, 'currency') + code = self.safe_currency_code(currencyId) + account = self.account() + account['total'] = self.safe_string(balanceEntry, 'balance') + account['used'] = self.safe_string(balanceEntry, 'reserved') + result[code] = account + return self.safe_balance(result) + + async def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + + https://documenter.getpostman.com/view/3653795/SVfWN6KS#4e7831f7-a0e7-4c3e-9336-1d0e5dcb15cf + + :param str [code]: unified currency code, default is None + :param int [since]: timestamp in ms of the earliest ledger entry, default is None + :param int [limit]: max number of ledger entries to return(default 200, max 500) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch entries for + :returns dict: a `ledger structure ` + """ + await self.load_markets() + request: dict = {} + if since is not None: + request['since'] = since + else: + # self endpoint accepts empty since param + request['since'] = '' + currency = None + if code is not None: + currency = self.currency(code) + response = await self.privateGetUsersWalletsHistorySince(self.extend(request, params)) + # + # { + # "list": [ + # { + # "currency": "USDC", + # "label": "USDC", + # "userId": "65671262d93d9525ac009e36", + # "balance": 0, + # "disabled": False, + # "balanceHistory": [ + # { + # "description": "Deposit - 657973a9b6eadf0f33d70100", + # "JSONdata": { + # "fees": 0, + # "notes": "Via Crypto", + # "txHash": "0x2e4875185b0f312d8e24b2d26d46bf9877db798b608ad2ff97b2b8bc7d8134e5", + # "last4Digits": null, + # "IBAN": null, + # "alternativeChain": "polygon", + # "referenceId": "657973a9b6eadf0f33d70100", + # "status": "completed", + # "tracked": True + # }, + # "amount": 99, + # "timestamp": "2023-12-13T09:04:51.270Z", + # "amountEUR": 91.79310117335974 + # }, + # { + # "description": "Order 65671262d93d9525ac009e36170257061073952c6423a8c5b4d6c SeqNum 10873722342", + # "JSONdata": { + # "price": "2282.00 ETH/USDC", + # "fees": 0, + # "notes": "Order 3a8c5b4d6c" + # }, + # "amount": -4.564, + # "timestamp": "2023-12-14T16:16:50.760Z", + # "amountEUR": -4.150043849187587 + # }, + # ... + # ] + # }, + # { + # "currency": "ETH", + # "label": "ETH", + # "userId": "65671262d93d9525ac009e36", + # "balance": 0, + # "disabled": False, + # "balanceHistory": [ + # { + # "description": "Order 65671262d93d9525ac009e36170257061073952c6423a8c5b4d6c SeqNum 10873722342", + # "JSONdata": { + # "price": "2282.00 ETH/USDC", + # "fees": 0.000002, + # "notes": "Order 3a8c5b4d6c" + # }, + # "amount": 0.001998, + # "timestamp": "2023-12-14T16:16:50.761Z", + # "amountEUR": 4.144849415806856 + # }, + # ... + # ] + # }, + # { + # "currency": "DOGE", + # "label": "DOGE", + # "userId": "65671262d93d9525ac009e36", + # "balance": 0, + # "disabled": False, + # "balanceHistory": [ + # { + # "description": "Order 65671262d93d9525ac009e361702905785319b5d9016dc20736034d13ca6a - Swap", + # "JSONdata": { + # "swap": True, + # "subtype": "swap", + # "fees": 0, + # "price": "0.0905469 DOGE/USDC", + # "notes": "Swap 034d13ca6a" + # }, + # "amount": 70, + # "timestamp": "2023-12-18T13:23:05.836Z", + # "amountEUR": 5.643627624549227 + # } + # ] + # }, + # ... + # ] + # } + # + ledgerByCurrencies = self.safe_value(response, 'list', []) + ledger = [] + for i in range(0, len(ledgerByCurrencies)): + currencyLedger = ledgerByCurrencies[i] + currencyId = self.safe_string(currencyLedger, 'currency') + balanceHistory = self.safe_value(currencyLedger, 'balanceHistory', []) + for j in range(0, len(balanceHistory)): + rawLedgerEntry = balanceHistory[j] + rawLedgerEntry['currencyId'] = currencyId + ledger.append(rawLedgerEntry) + return self.parse_ledger(ledger, currency, since, limit) + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + datetime = self.safe_string(item, 'timestamp') + currencyId = self.safe_string(item, 'currencyId') + item = self.omit(item, 'currencyId') + currency = self.safe_currency(currencyId, currency) + description = self.safe_string(item, 'description', '') + type, referenceId = self.parse_ledger_entry_description(description) + JSONdata = self.safe_value(item, 'JSONdata', {}) + feeCost = self.safe_string(JSONdata, 'fees') + fee = { + 'cost': feeCost, + 'currency': None, + } + amount = self.safe_string(item, 'amount') + direction = None + if amount is not None: + if Precise.string_lt(amount, '0'): + direction = 'out' + amount = Precise.string_abs(amount) + elif Precise.string_gt(amount, '0'): + direction = 'in' + return self.safe_ledger_entry({ + 'info': item, + 'id': None, + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + 'direction': direction, + 'account': None, + 'referenceId': referenceId, + 'referenceAccount': None, + 'type': type, + 'currency': currency, + 'amount': amount, + 'before': None, + 'after': None, + 'status': None, + 'fee': fee, + }, currency) + + def parse_ledger_entry_description(self, description): + descriptionArray = [] + if description is not None: + descriptionArray = description.split(' ') + type = None + referenceId = None + length = len(descriptionArray) + if length > 1: + type = self.parse_ledger_entry_type(descriptionArray[0]) + if descriptionArray[1] != '-': + referenceId = descriptionArray[1] + else: + referenceId = self.safe_string(descriptionArray, 2) + return [type, referenceId] + + def parse_ledger_entry_type(self, type): + types: dict = { + 'Deposit': 'transaction', + 'Withdraw': 'transaction', + 'Order': 'trade', + } + return self.safe_string(types, type, type) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://documenter.getpostman.com/view/3653795/SVfWN6KS#a4895a1d-3f50-40ae-8231-6962ef06c771 + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.cost]: the quote quantity that can be used alternative for the amount in market orders + :param str [params.timeInForce]: "GTC", "IOC", "FOK", "GTD" + :param number [params.expirationTime]: timestamp in millisecond, for GTD orders only + :param float [params.triggerPrice]: the price at which a trigger order is triggered at + :param float [params.stopLossPrice]: *margin only* The price at which a stop loss order is triggered at + :param float [params.takeProfitPrice]: *margin only* The price at which a take profit order is triggered at + :param bool [params.margin]: True for creating a margin order + :param str [params.fillStyle]: fill style of the limit order: "sell" fulfills selling quantity "buy" fulfills buying quantity "base" fulfills base currency quantity "quote" fulfills quote currency quantity + :param str [params.clientOrderId]: client's comment + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + } + request['orderType'] = type + formattedAmount = None + if amount is not None: + formattedAmount = self.amount_to_precision(symbol, amount) + cost = self.safe_value(params, 'cost') + params = self.omit(params, 'cost') + if type == 'limit': + if (price is None) and (cost is None): + raise ArgumentsRequired(self.id + ' createOrder() requires a price or params.cost argument for a ' + type + ' order') + elif (price is not None) and (amount is not None): + costString = Precise.string_mul(self.number_to_string(price), self.number_to_string(formattedAmount)) + cost = self.parse_to_numeric(costString) + precisedCost = None + if cost is not None: + precisedCost = self.cost_to_precision(symbol, cost) + if side == 'sell': + request = self.handle_create_order_side(market['baseId'], market['quoteId'], formattedAmount, precisedCost, request) + elif side == 'buy': + request = self.handle_create_order_side(market['quoteId'], market['baseId'], precisedCost, formattedAmount, request) + timeInForce = self.safe_value(params, 'timeInForce') + if timeInForce is not None: + params = self.omit(params, 'timeInForce') + request['timeInForce'] = self.encode_order_time_in_force(timeInForce) + triggerPrice = self.safe_string_2(params, 'triggerPrice', 'stopPrice') + if triggerPrice is not None: + params = self.omit(params, ['triggerPrice']) + request['stopPrice'] = self.price_to_precision(symbol, triggerPrice) + userData = self.safe_value(params, 'userData', {}) + comment = self.safe_string_2(params, 'clientOrderId', 'comment') + if comment is not None: + params = self.omit(params, ['clientOrderId']) + userData['comment'] = comment + stopLossPrice = self.safe_string(params, 'stopLossPrice') + if stopLossPrice is not None: + params = self.omit(params, 'stopLossPrice') + userData['stopLoss'] = self.price_to_precision(symbol, stopLossPrice) + takeProfitPrice = self.safe_string(params, 'takeProfitPrice') + if takeProfitPrice is not None: + params = self.omit(params, 'takeProfitPrice') + userData['takeProfit'] = self.price_to_precision(symbol, takeProfitPrice) + if not self.is_empty(userData): + request['userData'] = userData + response = await self.privatePostExchangeOrdersCreate(self.extend(request, params)) + # + # { + # "userID": "65671262d93d9525ac009e36", + # "orderID": "65671262d93d9525ac009e36170257448481749b7ee2893bafec2", + # "orderType": "market", + # "buyingCurrency": "ETH", + # "sellingCurrency": "USDC", + # "buyingQty": 0.002, + # "timeInForce": 4, + # "boughtQty": 0.002, + # "soldQty": 4.587, + # "creationTime": 1702574484829, + # "seqNumber": 10874285330, + # "firstFillTime": 1702574484831, + # "lastFillTime": 1702574484831, + # "fills": [ + # { + # "seqNumber": 10874285329, + # "timestamp": 1702574484831, + # "qty": 0.002, + # "price": 2293.5, + # "side": "buy" + # } + # ], + # "completionTime": 1702574484831, + # "takerQty": 0.002 + # } + # + return self.parse_order(response, market) + + def handle_create_order_side(self, sellingCurrency, buyingCurrency, sellingQty, buyingQty, request={}): + request['sellingCurrency'] = sellingCurrency + request['buyingCurrency'] = buyingCurrency + if sellingQty is not None: + request['sellingQty'] = sellingQty + if buyingQty is not None: + request['buyingQty'] = buyingQty + return request + + def encode_order_time_in_force(self, timeInForce): + timeInForceTypes: dict = { + 'GTC': 1, + 'IOC': 2, + 'GTD': 3, + 'FOK': 4, + } + return self.safe_value(timeInForceTypes, timeInForce, timeInForce) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://documenter.getpostman.com/view/3653795/SVfWN6KS#eaea86da-16ca-4c56-9f00-5b1cb2ad89f8 + https://documenter.getpostman.com/view/3653795/SVfWN6KS#47f913fb-8cab-49f4-bc78-d980e6ced316 + + :param str id: order id + :param str symbol: not used by coinmetro cancelOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.margin]: True for cancelling a margin order + :returns dict: An `order structure ` + """ + await self.load_markets() + request: dict = { + 'orderID': id, + } + marginMode = None + params, params = self.handle_margin_mode_and_params('cancelOrder', params) + isMargin = self.safe_bool(params, 'margin', False) + params = self.omit(params, 'margin') + response = None + if isMargin or (marginMode is not None): + response = await self.privatePostExchangeOrdersCloseOrderID(self.extend(request, params)) + else: + response = await self.privatePutExchangeOrdersCancelOrderID(self.extend(request, params)) + # + # { + # "userID": "65671262d93d9525ac009e36", + # "orderID": "65671262d93d9525ac009e3617026635256739c996fe17d7cd5d4", + # "orderType": "limit", + # "buyingCurrency": "ETH", + # "sellingCurrency": "USDC", + # "fillStyle": "sell", + # "orderPlatform": "trade-v3", + # "timeInForce": 1, + # "buyingQty": 0.005655, + # "sellingQty": 11.31, + # "boughtQty": 0, + # "soldQty": 0, + # "creationTime": 1702663525713, + # "seqNumber": 10915220048, + # "completionTime": 1702928369053 + # } + # + return self.parse_order(response) + + async def close_position(self, symbol: str, side: OrderSide = None, params={}): + """ + closes an open position + + https://documenter.getpostman.com/view/3653795/SVfWN6KS#47f913fb-8cab-49f4-bc78-d980e6ced316 + + :param str symbol: not used by coinmetro closePosition() + :param str [side]: not used by coinmetro closePosition() + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.orderID]: order id + :param number [params.fraction]: fraction of order to close, between 0 and 1(defaults to 1) + :returns dict: An `order structure ` + """ + await self.load_markets() + orderId = self.safe_string(params, 'orderId') + if orderId is None: + raise ArgumentsRequired(self.id + ' closePosition() requires a orderId parameter') + request: dict = { + 'orderID': orderId, + } + response = await self.privatePostExchangeOrdersCloseOrderID(self.extend(request, params)) + # + # { + # "userID": "65671262d93d9525ac009e36", + # "orderID": "65671262d93d9525ac009e3617030152811996e5b352556d3d7d8_CL", + # "orderType": "market", + # "buyingCurrency": "ETH", + # "sellingCurrency": "EUR", + # "margin": True, + # "buyingQty": 0.03, + # "timeInForce": 4, + # "boughtQty": 0.03, + # "soldQty": 59.375, + # "creationTime": 1703015488482, + # "seqNumber": 10925321179, + # "firstFillTime": 1703015488483, + # "lastFillTime": 1703015488483, + # "fills": [ + # { + # "seqNumber": 10925321178, + # "timestamp": 1703015488483, + # "qty": 0.03, + # "price": 1979.1666666666667, + # "side": "buy" + # } + # ], + # "completionTime": 1703015488483, + # "takerQty": 0.03 + # } + # + return self.parse_order(response) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://documenter.getpostman.com/view/3653795/SVfWN6KS#518afd7a-4338-439c-a651-d4fdaa964138 + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + response = await self.privateGetExchangeOrdersActive(params) + orders = self.parse_orders(response, market, since, limit) + for i in range(0, len(orders)): + order = orders[i] + order['status'] = 'open' + return orders + + async def fetch_canceled_and_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple canceled and closed orders made by the user + + https://documenter.getpostman.com/view/3653795/SVfWN6KS#4d48ae69-8ee2-44d1-a268-71f84e557b7b + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + request: dict = {} + if since is not None: + request['since'] = since + response = await self.privateGetExchangeOrdersHistorySince(self.extend(request, params)) + return self.parse_orders(response, market, since, limit) + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://documenter.getpostman.com/view/3653795/SVfWN6KS#95bbed87-db1c-47a7-a03e-aa247e91d5a6 + + :param int|str id: order id + :param str symbol: not used by coinmetro fetchOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + request: dict = { + 'orderID': id, + } + response = await self.privateGetExchangeOrdersStatusOrderID(self.extend(request, params)) + # + # { + # "_id": "657b4e6d60a954244939ac6f", + # "userID": "65671262d93d9525ac009e36", + # "orderID": "65671262d93d9525ac009e361702576531985b78465468b9cc544", + # "orderType": "market", + # "buyingCurrency": "ETH", + # "sellingCurrency": "USDC", + # "buyingQty": 0.004, + # "timeInForce": 4, + # "boughtQty": 0.004, + # "soldQty": 9.236, + # "creationTime": 1702576531995, + # "seqNumber": 10874644062, + # "firstFillTime": 1702576531995, + # "lastFillTime": 1702576531995, + # "fills": [ + # { + # "_id": "657b4e6d60a954244939ac70", + # "seqNumber": 10874644061, + # "timestamp": 1702576531995, + # "qty": 0.004, + # "price": 2309, + # "side": "buy" + # } + # ], + # "completionTime": 1702576531995, + # "takerQty": 0.004, + # "fees": 0.000004, + # "isAncillary": False, + # "margin": False, + # "trade": False, + # "canceled": False + # } + # + return self.parse_order(response) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder market + # { + # "userID": "65671262d93d9525ac009e36", + # "orderID": "65671262d93d9525ac009e36170257448481749b7ee2893bafec2", + # "orderType": "market", + # "buyingCurrency": "ETH", + # "sellingCurrency": "USDC", + # "buyingQty": 0.002, + # "timeInForce": 4, + # "boughtQty": 0.002, + # "soldQty": 4.587, + # "creationTime": 1702574484829, + # "seqNumber": 10874285330, + # "firstFillTime": 1702574484831, + # "lastFillTime": 1702574484831, + # "fills": [ + # { + # "seqNumber": 10874285329, + # "timestamp": 1702574484831, + # "qty": 0.002, + # "price": 2293.5, + # "side": "buy" + # } + # ], + # "completionTime": 1702574484831, + # "takerQty": 0.002 + # } + # + # createOrder limit + # { + # "userID": "65671262d93d9525ac009e36", + # "orderID": "65671262d93d9525ac009e3617026635256739c996fe17d7cd5d4", + # "orderType": "limit", + # "buyingCurrency": "ETH", + # "sellingCurrency": "USDC", + # "fillStyle": "sell", + # "orderPlatform": "trade-v3", + # "timeInForce": 1, + # "buyingQty": 0.005655, + # "sellingQty": 11.31, + # "boughtQty": 0, + # "soldQty": 0, + # "creationTime": 1702663525713, + # "seqNumber": 10885528683, + # "fees": 0, + # "fills": [], + # "isAncillary": False, + # "margin": False, + # "trade": False + # } + # + # fetchOrders market + # { + # "userID": "65671262d93d9525ac009e36", + # "orderID": "65671262d93d9525ac009e36170257061073952c6423a8c5b4d6c", + # "orderType": "market", + # "buyingCurrency": "ETH", + # "sellingCurrency": "USDC", + # "buyingQty": 0.002, + # "timeInForce": 4, + # "boughtQty": 0.002, + # "soldQty": 4.564, + # "creationTime": 1702570610746, + # "seqNumber": 10873722344, + # "firstFillTime": 1702570610747, + # "lastFillTime": 1702570610747, + # "fills": [ + # { + # "_id": "657b31d360a9542449381bdc", + # "seqNumber": 10873722343, + # "timestamp": 1702570610747, + # "qty": 0.002, + # "price": 2282, + # "side": "buy" + # } + # ], + # "completionTime": 1702570610747, + # "takerQty": 0.002, + # "fees": 0.000002, + # "isAncillary": False, + # "margin": False, + # "trade": False, + # "canceled": False, + # "__v": 0 + # } + # + # fetchOrders margin + # { + # "userData": { + # "takeProfit": 1700, + # "stopLoss": 2100 + # }, + # "_id": "658201d060a95424499394a2", + # "seqNumber": 10925300213, + # "orderType": "limit", + # "buyingCurrency": "EUR", + # "sellingCurrency": "ETH", + # "userID": "65671262d93d9525ac009e36", + # "closedQty": 0.03, + # "sellingQty": 0.03, + # "buyingQty": 58.8, + # "creationTime": 1703015281205, + # "margin": True, + # "timeInForce": 1, + # "boughtQty": 59.31, + # "orderID": "65671262d93d9525ac009e3617030152811996e5b352556d3d7d8", + # "lastFillTime": 1703015281206, + # "soldQty": 0.03, + # "closedTime": 1703015488488, + # "closedVal": 59.375, + # "trade": True, + # "takerQty": 59.31, + # "firstFillTime": 1703015281206, + # "completionTime": 1703015281206, + # "fills": [ + # { + # "_id": "658201d060a95424499394a3", + # "seqNumber": 10925300212, + # "side": "sell", + # "price": 1977, + # "qty": 0.03, + # "timestamp": 1703015281206 + # }, + # { + # "_id": "658201d060a95424499394a4", + # "seqNumber": 10925321178, + # "timestamp": 1703015488483, + # "qty": 0.03, + # "price": 1979.1666666666667, + # "side": "buy" + # } + # ], + # "fees": 0.11875000200000001, + # "settledQtys": { + # "ETH": -0.000092842104710025 + # }, + # "isAncillary": False, + # "canceled": False + # } + # + # fetchOrder + # { + # "_id": "657b4e6d60a954244939ac6f", + # "userID": "65671262d93d9525ac009e36", + # "orderID": "65671262d93d9525ac009e361702576531985b78465468b9cc544", + # "orderType": "market", + # "buyingCurrency": "ETH", + # "sellingCurrency": "USDC", + # "buyingQty": 0.004, + # "timeInForce": 4, + # "boughtQty": 0.004, + # "soldQty": 9.236, + # "creationTime": 1702576531995, + # "seqNumber": 10874644062, + # "firstFillTime": 1702576531995, + # "lastFillTime": 1702576531995, + # "fills": [ + # { + # "_id": "657b4e6d60a954244939ac70", + # "seqNumber": 10874644061, + # "timestamp": 1702576531995, + # "qty": 0.004, + # "price": 2309, + # "side": "buy" + # } + # ], + # "completionTime": 1702576531995, + # "takerQty": 0.004, + # "fees": 0.000004, + # "isAncillary": False, + # "margin": False, + # "trade": False, + # "canceled": False + # } + # + timestamp = self.safe_integer(order, 'creationTime') + isCanceled = self.safe_value(order, 'canceled') + status = None + if isCanceled is True: + if timestamp is None: + timestamp = self.safe_integer(order, 'completionTime') # market orders with bad price gain IOC - we mark them as 'rejected'? + status = 'rejected' # these orders don't have the 'creationTime` param and have 'canceled': True + else: + status = 'canceled' + else: + status = self.safe_string(order, 'status') + order = self.omit(order, 'status') # we mark orders from fetchOpenOrders with param 'status': 'open' + type = self.safe_string(order, 'orderType') + buyingQty = self.safe_string(order, 'buyingQty') + sellingQty = self.safe_string(order, 'sellingQty') + boughtQty = self.safe_string(order, 'boughtQty') + soldQty = self.safe_string(order, 'soldQty') + if type == 'market': + if (buyingQty is None) and (boughtQty is not None) and (boughtQty != '0'): + buyingQty = boughtQty + if (sellingQty is None) and (soldQty is not None) and (soldQty != '0'): + sellingQty = soldQty + buyingCurrencyId = self.safe_string(order, 'buyingCurrency', '') + sellingCurrencyId = self.safe_string(order, 'sellingCurrency', '') + byuingIdPlusSellingId = buyingCurrencyId + sellingCurrencyId + sellingIdPlusBuyingId = sellingCurrencyId + buyingCurrencyId + side = None + marketId = None + baseAmount = buyingQty + quoteAmount = buyingQty + filled = None + cost = None + feeInBaseOrQuote = None + marketsById = self.index_by(self.markets, 'id') + if self.safe_value(marketsById, byuingIdPlusSellingId) is not None: + side = 'buy' + marketId = byuingIdPlusSellingId + quoteAmount = sellingQty + filled = boughtQty + cost = soldQty + feeInBaseOrQuote = 'base' + elif self.safe_value(marketsById, sellingIdPlusBuyingId) is not None: + side = 'sell' + marketId = sellingIdPlusBuyingId + baseAmount = sellingQty + filled = soldQty + cost = boughtQty + feeInBaseOrQuote = 'quote' + price = None + if (baseAmount is not None) and (quoteAmount is not None): + price = Precise.string_div(quoteAmount, baseAmount) + market = self.safe_market(marketId, market) + fee = None + feeCost = self.safe_string(order, 'fees') + if (feeCost is not None) and (feeInBaseOrQuote is not None): + fee = { + 'currency': market[feeInBaseOrQuote], + 'cost': feeCost, + 'rate': None, + } + trades = self.safe_value(order, 'fills', []) + userData = self.safe_value(order, 'userData', {}) + clientOrderId = self.safe_string(userData, 'comment') + takeProfitPrice = self.safe_string(userData, 'takeProfit') + stopLossPrice = self.safe_string(userData, 'stopLoss') + return self.safe_order({ + 'id': self.safe_string(order, 'orderID'), + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': self.safe_integer(order, 'lastFillTime'), + 'status': status, + 'symbol': market['symbol'], + 'type': type, + 'timeInForce': self.parse_order_time_in_force(self.safe_integer(order, 'timeInForce')), + 'side': side, + 'price': price, + 'triggerPrice': self.safe_string(order, 'stopPrice'), + 'takeProfitPrice': takeProfitPrice, + 'stopLossPrice': stopLossPrice, + 'average': None, + 'amount': baseAmount, + 'cost': cost, + 'filled': filled, + 'remaining': None, + 'fee': fee, + 'fees': None, + 'trades': trades, + 'info': order, + }, market) + + def parse_order_time_in_force(self, timeInForce): + timeInForceTypes = [ + None, + 'GTC', + 'IOC', + 'GTD', + 'FOK', + ] + return self.safe_value(timeInForceTypes, timeInForce, timeInForce) + + async def borrow_cross_margin(self, code: str, amount: float, params={}): + """ + create a loan to borrow margin + + https://documenter.getpostman.com/view/3653795/SVfWN6KS#5b90b3b9-e5db-4d07-ac9d-d680a06fd110 + + :param str code: unified currency code of the currency to borrow + :param float amount: the amount to borrow + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin loan structure ` + """ + await self.load_markets() + currency = self.currency(code) + currencyId = currency['id'] + request: dict = {} + request[currencyId] = self.currency_to_precision(code, amount) + response = await self.privatePutUsersMarginCollateral(self.extend(request, params)) + # + # {"message": "OK"} + # + result = self.safe_value(response, 'result', {}) + transaction = self.parse_margin_loan(result, currency) + return self.extend(transaction, { + 'amount': amount, + }) + + def parse_margin_loan(self, info, currency: Currency = None): + currencyId = self.safe_string(info, 'coin') + return { + 'id': None, + 'currency': self.safe_currency_code(currencyId, currency), + 'amount': None, + 'symbol': None, + 'timestamp': None, + 'datetime': None, + 'info': info, + } + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + request = self.omit(params, self.extract_params(path)) + endpoint = '/' + self.implode_params(path, params) + url = self.urls['api'][api] + endpoint + query = self.urlencode(request) + if headers is None: + headers = {} + headers['CCXT'] = 'true' + if api == 'private': + if (self.uid is None) and (self.apiKey is not None): + self.uid = self.apiKey + if (self.token is None) and (self.secret is not None): + self.token = self.secret + if url == 'https://api.coinmetro.com/jwt': # handle with headers for login endpoint + headers['X-Device-Id'] = 'bypass' + if self.twofa is not None: + headers['X-OTP'] = self.twofa + elif url == 'https://api.coinmetro.com/jwtDevice': # handle with headers for long lived token login endpoint + headers['X-Device-Id'] = self.uid + if self.twofa is not None: + headers['X-OTP'] = self.twofa + else: + headers['Authorization'] = 'Bearer ' + self.token + if not url.startswith('https://api.coinmetro.com/open'): # if not sandbox endpoint + self.check_required_credentials() + headers['X-Device-Id'] = self.uid + if (method == 'POST') or (method == 'PUT'): + headers['Content-Type'] = 'application/x-www-form-urlencoded' + body = self.urlencode(request) + elif len(query) != 0: + url += '?' + query + while(url.endswith('/')): + url = url[0:len(url) - 1] + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None + if (code != 200) and (code != 201) and (code != 202): + feedback = self.id + ' ' + body + message = self.safe_string(response, 'message') + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) + raise ExchangeError(feedback) + return None diff --git a/ccxt/async_support/coinone.py b/ccxt/async_support/coinone.py new file mode 100644 index 0000000..7b4fbfe --- /dev/null +++ b/ccxt/async_support/coinone.py @@ -0,0 +1,1234 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.coinone import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currencies, DepositAddress, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import OnMaintenance +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class coinone(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(coinone, self).describe(), { + 'id': 'coinone', + 'name': 'CoinOne', + 'countries': ['KR'], # Korea + 'rateLimit': 50, + 'version': 'v2', + 'pro': False, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelOrder': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createMarketOrder': False, + 'createOrder': True, + 'createOrderWithTakeProfitAndStopLoss': False, + 'createOrderWithTakeProfitAndStopLossWs': False, + 'createPostOnlyOrder': False, + 'createReduceOnlyOrder': False, + 'createStopLimitOrder': False, + 'createStopMarketOrder': False, + 'createStopOrder': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchClosedOrders': False, # the endpoint that should return closed orders actually returns trades, https://github.com/ccxt/ccxt/pull/7067 + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': False, + 'fetchDepositAddresses': True, + 'fetchDepositAddressesByNetwork': False, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTrades': True, + 'fetchVolatilityHistory': False, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'repayMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'ws': True, + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/38003300-adc12fba-323f-11e8-8525-725f53c4a659.jpg', + 'api': { + 'rest': 'https://api.coinone.co.kr', + 'v2Public': 'https://api.coinone.co.kr/public/v2', + 'v2Private': 'https://api.coinone.co.kr/v2', + 'v2_1Private': 'https://api.coinone.co.kr/v2.1', + }, + 'www': 'https://coinone.co.kr', + 'doc': 'https://doc.coinone.co.kr', + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + }, + 'api': { + 'public': { + 'get': [ + 'orderbook', + 'ticker', + 'ticker_utc', + 'trades', + ], + }, + 'v2Public': { + 'get': [ + 'range_units', + 'markets/{quote_currency}', + 'markets/{quote_currency}/{target_currency}', + 'orderbook/{quote_currency}/{target_currency}', + 'trades/{quote_currency}/{target_currency}', + 'ticker_new/{quote_currency}', + 'ticker_new/{quote_currency}/{target_currency}', + 'ticker_utc_new/{quote_currency}', + 'ticker_utc_new/{quote_currency}/{target_currency}', + 'currencies', + 'currencies/{currency}', + 'chart/{quote_currency}/{target_currency}', + ], + }, + 'private': { + 'post': [ + 'account/deposit_address', + 'account/btc_deposit_address', + 'account/balance', + 'account/daily_balance', + 'account/user_info', + 'account/virtual_account', + 'order/cancel_all', + 'order/cancel', + 'order/limit_buy', + 'order/limit_sell', + 'order/complete_orders', + 'order/limit_orders', + 'order/order_info', + 'transaction/auth_number', + 'transaction/history', + 'transaction/krw/history', + 'transaction/btc', + 'transaction/coin', + ], + }, + 'v2Private': { + 'post': [ + 'account/balance', + 'account/deposit_address', + 'account/user_info', + 'account/virtual_account', + 'order/cancel', + 'order/limit_buy', + 'order/limit_sell', + 'order/limit_orders', + 'order/complete_orders', + 'order/query_order', + 'transaction/auth_number', + 'transaction/btc', + 'transaction/history', + 'transaction/krw/history', + ], + }, + 'v2_1Private': { + 'post': [ + 'account/balance/all', + 'account/balance', + 'account/trade_fee', + 'account/trade_fee/{quote_currency}/{target_currency}', + 'order/limit', + 'order/cancel', + 'order/cancel/all', + 'order/open_orders', + 'order/open_orders/all', + 'order/complete_orders', + 'order/complete_orders/all', + 'order/info', + 'transaction/krw/history', + 'transaction/coin/history', + 'transaction/coin/withdrawal/limit', + ], + }, + }, + 'fees': { + 'trading': { + 'tierBased': False, + 'percentage': True, + 'taker': 0.002, + 'maker': 0.002, + }, + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': False, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': False, + 'FOK': False, + 'PO': False, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': False, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, # todo implement + 'daysBack': 100000, # todo implement + 'untilDays': 100000, # todo implement + 'symbolRequired': True, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOrders': None, + 'fetchClosedOrders': None, # todo implement + 'fetchOHLCV': None, # todo implement + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + '104': OrderNotFound, + '107': BadRequest, + '108': BadSymbol, + '405': OnMaintenance, + }, + 'commonCurrencies': { + 'SOC': 'Soda Coin', + }, + }) + + async def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://docs.coinone.co.kr/reference/currencies + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = await self.v2PublicGetCurrencies(params) + # + # { + # "result": "success", + # "error_code": "0", + # "server_time": 1701054555578, + # "currencies": [ + # { + # "name": "Polygon", + # "symbol": "MATIC", + # "deposit_status": "normal", + # "withdraw_status": "normal", + # "deposit_confirm_count": 150, + # "max_precision": 8, + # "deposit_fee": "0.0", + # "withdrawal_min_amount": "1.0", + # "withdrawal_fee": "3.0" + # } + # ] + # } + # + result: dict = {} + currencies = self.safe_list(response, 'currencies', []) + for i in range(0, len(currencies)): + entry = currencies[i] + id = self.safe_string(entry, 'symbol') + code = self.safe_currency_code(id) + isWithdrawEnabled = self.safe_string(entry, 'withdraw_status', '') == 'normal' + isDepositEnabled = self.safe_string(entry, 'deposit_status', '') == 'normal' + type = 'crypto' if (code != 'KRW') else 'fiat' + result[code] = self.safe_currency_structure({ + 'id': id, + 'code': code, + 'info': entry, + 'name': self.safe_string(entry, 'name'), + 'active': None, + 'deposit': isDepositEnabled, + 'withdraw': isWithdrawEnabled, + 'fee': self.safe_number(entry, 'withdrawal_fee'), + 'precision': self.parse_number(self.parse_precision(self.safe_string(entry, 'max_precision'))), + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': self.safe_number(entry, 'withdrawal_min_amount'), + 'max': None, + }, + }, + 'networks': {}, + 'type': type, + }) + return result + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for coinone + + https://docs.coinone.co.kr/v1.0/reference/tickers + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + request: dict = { + 'quote_currency': 'KRW', + } + response = await self.v2PublicGetTickerNewQuoteCurrency(request) + # + # { + # "result": "success", + # "error_code": "0", + # "server_time": 1701067923060, + # "tickers": [ + # { + # "quote_currency": "krw", + # "target_currency": "stg", + # "timestamp": 1701067920001, + # "high": "667.5", + # "low": "667.5", + # "first": "667.5", + # "last": "667.5", + # "quote_volume": "0.0", + # "target_volume": "0.0", + # "best_asks": [ + # { + # "price": "777.0", + # "qty": "73.9098" + # } + # ], + # "best_bids": [ + # { + # "price": "690.8", + # "qty": "40.7768" + # } + # ], + # "id": "1701067920001001" + # } + # ] + # } + # + tickers = self.safe_list(response, 'tickers', []) + result = [] + for i in range(0, len(tickers)): + entry = self.safe_value(tickers, i) + id = self.safe_string(entry, 'id') + baseId = self.safe_string_upper(entry, 'target_currency') + quoteId = self.safe_string_upper(entry, 'quote_currency') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + result.append({ + 'id': id, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': None, + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number('1e-4'), + 'price': self.parse_number('1e-4'), + 'cost': self.parse_number('1e-8'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': None, + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + 'info': entry, + }) + return result + + def parse_balance(self, response) -> Balances: + result: dict = {'info': response} + balances = self.omit(response, [ + 'errorCode', + 'result', + 'normalWallets', + ]) + currencyIds = list(balances.keys()) + for i in range(0, len(currencyIds)): + currencyId = currencyIds[i] + balance = balances[currencyId] + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(balance, 'avail') + account['total'] = self.safe_string(balance, 'balance') + result[code] = account + return self.safe_balance(result) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://docs.coinone.co.kr/v1.0/reference/v21 + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + response = await self.v2PrivatePostAccountBalance(params) + return self.parse_balance(response) + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://docs.coinone.co.kr/v1.0/reference/orderbook + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'quote_currency': market['quote'], + 'target_currency': market['base'], + } + if limit is not None: + request['size'] = limit # only support 5, 10, 15, 16 + response = await self.v2PublicGetOrderbookQuoteCurrencyTargetCurrency(self.extend(request, params)) + # + # { + # "result": "success", + # "error_code": "0", + # "timestamp": 1701071108673, + # "id": "1701071108673001", + # "quote_currency": "KRW", + # "target_currency": "BTC", + # "order_book_unit": "0.0", + # "bids": [ + # { + # "price": "50048000", + # "qty": "0.01080229" + # } + # ], + # "asks": [ + # { + # "price": "50058000", + # "qty": "0.00272592" + # } + # ] + # } + # + timestamp = self.safe_integer(response, 'timestamp') + return self.parse_order_book(response, market['symbol'], timestamp, 'bids', 'asks', 'price', 'qty') + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://docs.coinone.co.kr/v1.0/reference/tickers + https://docs.coinone.co.kr/v1.0/reference/ticker + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + request: dict = { + 'quote_currency': 'KRW', + } + market = None + response = None + if symbols is not None: + first = self.safe_string(symbols, 0) + market = self.market(first) + request['quote_currency'] = market['quote'] + request['target_currency'] = market['base'] + response = await self.v2PublicGetTickerNewQuoteCurrencyTargetCurrency(self.extend(request, params)) + else: + response = await self.v2PublicGetTickerNewQuoteCurrency(self.extend(request, params)) + # + # { + # "result": "success", + # "error_code": "0", + # "server_time": 1701073358487, + # "tickers": [ + # { + # "quote_currency": "krw", + # "target_currency": "btc", + # "timestamp": 1701073357818, + # "high": "50543000.0", + # "low": "49945000.0", + # "first": "50487000.0", + # "last": "50062000.0", + # "quote_volume": "11349804285.3859", + # "target_volume": "226.07268994", + # "best_asks": [ + # { + # "price": "50081000.0", + # "qty": "0.18471358" + # } + # ], + # "best_bids": [ + # { + # "price": "50062000.0", + # "qty": "0.04213455" + # } + # ], + # "id": "1701073357818001" + # } + # ] + # } + # + data = self.safe_list(response, 'tickers', []) + return self.parse_tickers(data, symbols) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://docs.coinone.co.kr/v1.0/reference/ticker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'quote_currency': market['quote'], + 'target_currency': market['base'], + } + response = await self.v2PublicGetTickerNewQuoteCurrencyTargetCurrency(self.extend(request, params)) + # + # { + # "result": "success", + # "error_code": "0", + # "server_time": 1701073358487, + # "tickers": [ + # { + # "quote_currency": "krw", + # "target_currency": "btc", + # "timestamp": 1701073357818, + # "high": "50543000.0", + # "low": "49945000.0", + # "first": "50487000.0", + # "last": "50062000.0", + # "quote_volume": "11349804285.3859", + # "target_volume": "226.07268994", + # "best_asks": [ + # { + # "price": "50081000.0", + # "qty": "0.18471358" + # } + # ], + # "best_bids": [ + # { + # "price": "50062000.0", + # "qty": "0.04213455" + # } + # ], + # "id": "1701073357818001" + # } + # ] + # } + # + data = self.safe_list(response, 'tickers', []) + ticker = self.safe_dict(data, 0, {}) + return self.parse_ticker(ticker, market) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "quote_currency": "krw", + # "target_currency": "btc", + # "timestamp": 1701073357818, + # "high": "50543000.0", + # "low": "49945000.0", + # "first": "50487000.0", + # "last": "50062000.0", + # "quote_volume": "11349804285.3859", + # "target_volume": "226.07268994", + # "best_asks": [ + # { + # "price": "50081000.0", + # "qty": "0.18471358" + # } + # ], + # "best_bids": [ + # { + # "price": "50062000.0", + # "qty": "0.04213455" + # } + # ], + # "id": "1701073357818001" + # } + # + timestamp = self.safe_integer(ticker, 'timestamp') + last = self.safe_string(ticker, 'last') + asks = self.safe_list(ticker, 'best_asks', []) + bids = self.safe_list(ticker, 'best_bids', []) + baseId = self.safe_string(ticker, 'target_currency') + quoteId = self.safe_string(ticker, 'quote_currency') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + return self.safe_ticker({ + 'symbol': base + '/' + quote, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': self.safe_string(bids, 'price'), + 'bidVolume': self.safe_string(bids, 'qty'), + 'ask': self.safe_string(asks, 'price'), + 'askVolume': self.safe_string(asks, 'qty'), + 'vwap': None, + 'open': self.safe_string(ticker, 'first'), + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': self.safe_string(ticker, 'target_volume'), + 'quoteVolume': self.safe_string(ticker, 'quote_volume'), + 'info': ticker, + }, market) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(public) + # + # { + # "id": "1701075265708001", + # "timestamp": 1701075265708, + # "price": "50020000", + # "qty": "0.00155177", + # "is_seller_maker": False + # } + # + # fetchMyTrades(private) + # + # { + # "timestamp": "1416561032", + # "price": "419000.0", + # "type": "bid", + # "qty": "0.001", + # "feeRate": "-0.0015", + # "fee": "-0.0000015", + # "orderId": "E84A1AC2-8088-4FA0-B093-A3BCDB9B3C85" + # } + # + timestamp = self.safe_integer(trade, 'timestamp') + market = self.safe_market(None, market) + isSellerMaker = self.safe_bool(trade, 'is_seller_maker') + side = None + if isSellerMaker is not None: + side = 'sell' if isSellerMaker else 'buy' + priceString = self.safe_string(trade, 'price') + amountString = self.safe_string(trade, 'qty') + orderId = self.safe_string(trade, 'orderId') + feeCostString = self.safe_string(trade, 'fee') + fee = None + if feeCostString is not None: + feeCostString = Precise.string_abs(feeCostString) + feeRateString = self.safe_string(trade, 'feeRate') + feeRateString = Precise.string_abs(feeRateString) + feeCurrencyCode = market['quote'] if (side == 'sell') else market['base'] + fee = { + 'cost': feeCostString, + 'currency': feeCurrencyCode, + 'rate': feeRateString, + } + return self.safe_trade({ + 'id': self.safe_string(trade, 'id'), + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'order': orderId, + 'symbol': market['symbol'], + 'type': None, + 'side': side, + 'takerOrMaker': None, + 'price': priceString, + 'amount': amountString, + 'cost': None, + 'fee': fee, + }, market) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://docs.coinone.co.kr/v1.0/reference/recent-completed-orders + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'quote_currency': market['quote'], + 'target_currency': market['base'], + } + if limit is not None: + request['size'] = min(limit, 200) + response = await self.v2PublicGetTradesQuoteCurrencyTargetCurrency(self.extend(request, params)) + # + # { + # "result": "success", + # "error_code": "0", + # "server_time": 1701075315771, + # "quote_currency": "KRW", + # "target_currency": "BTC", + # "transactions": [ + # { + # "id": "1701075265708001", + # "timestamp": 1701075265708, + # "price": "50020000", + # "qty": "0.00155177", + # "is_seller_maker": False + # } + # ] + # } + # + data = self.safe_list(response, 'transactions', []) + return self.parse_trades(data, market, since, limit) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://doc.coinone.co.kr/#tag/Order-V2/operation/v2_order_limit_buy + https://doc.coinone.co.kr/#tag/Order-V2/operation/v2_order_limit_sell + + :param str symbol: unified symbol of the market to create an order in + :param str type: must be 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + if type != 'limit': + raise ExchangeError(self.id + ' createOrder() allows limit orders only') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'price': price, + 'currency': market['id'], + 'qty': amount, + } + method = 'privatePostOrder' + self.capitalize(type) + self.capitalize(side) + response = await getattr(self, method)(self.extend(request, params)) + # + # { + # "result": "success", + # "errorCode": "0", + # "orderId": "8a82c561-40b4-4cb3-9bc0-9ac9ffc1d63b" + # } + # + return self.parse_order(response, market) + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrder() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'order_id': id, + 'currency': market['id'], + } + response = await self.v2PrivatePostOrderQueryOrder(self.extend(request, params)) + # + # { + # "result": "success", + # "errorCode": "0", + # "orderId": "0e3019f2-1e4d-11e9-9ec7-00e04c3600d7", + # "baseCurrency": "KRW", + # "targetCurrency": "BTC", + # "price": "10011000.0", + # "originalQty": "3.0", + # "executedQty": "0.62", + # "canceledQty": "1.125", + # "remainQty": "1.255", + # "status": "partially_filled", + # "side": "bid", + # "orderedAt": 1499340941, + # "updatedAt": 1499341142, + # "feeRate": "0.002", + # "fee": "0.00124", + # "averageExecutedPrice": "10011000.0" + # } + # + return self.parse_order(response, market) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'live': 'open', + 'partially_filled': 'open', + 'partially_canceled': 'open', + 'filled': 'closed', + 'canceled': 'canceled', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder + # + # { + # "result": "success", + # "errorCode": "0", + # "orderId": "8a82c561-40b4-4cb3-9bc0-9ac9ffc1d63b" + # } + # + # fetchOrder + # + # { + # "result": "success", + # "errorCode": "0", + # "orderId": "0e3019f2-1e4d-11e9-9ec7-00e04c3600d7", + # "baseCurrency": "KRW", + # "targetCurrency": "BTC", + # "price": "10011000.0", + # "originalQty": "3.0", + # "executedQty": "0.62", + # "canceledQty": "1.125", + # "remainQty": "1.255", + # "status": "partially_filled", + # "side": "bid", + # "orderedAt": 1499340941, + # "updatedAt": 1499341142, + # "feeRate": "0.002", + # "fee": "0.00124", + # "averageExecutedPrice": "10011000.0" + # } + # + # fetchOpenOrders + # + # { + # "index": "0", + # "orderId": "68665943-1eb5-4e4b-9d76-845fc54f5489", + # "timestamp": "1449037367", + # "price": "444000.0", + # "qty": "0.3456", + # "type": "ask", + # "feeRate": "-0.0015" + # } + # + id = self.safe_string(order, 'orderId') + baseId = self.safe_string(order, 'baseCurrency') + quoteId = self.safe_string(order, 'targetCurrency') + base = None + quote = None + if baseId is not None: + base = self.safe_currency_code(baseId) + if quoteId is not None: + quote = self.safe_currency_code(quoteId) + symbol = None + if (base is not None) and (quote is not None): + symbol = base + '/' + quote + market = self.safe_market(symbol, market, '/') + timestamp = self.safe_timestamp_2(order, 'timestamp', 'updatedAt') + side = self.safe_string_2(order, 'type', 'side') + if side == 'ask': + side = 'sell' + elif side == 'bid': + side = 'buy' + remainingString = self.safe_string(order, 'remainQty') + amountString = self.safe_string_2(order, 'originalQty', 'qty') + status = self.safe_string(order, 'status') + # https://github.com/ccxt/ccxt/pull/7067 + if status == 'live': + if (remainingString is not None) and (amountString is not None): + isLessThan = Precise.string_lt(remainingString, amountString) + if isLessThan: + status = 'canceled' + status = self.parse_order_status(status) + fee = None + feeCostString = self.safe_string(order, 'fee') + if feeCostString is not None: + feeCurrencyCode = quote if (side == 'sell') else base + fee = { + 'cost': feeCostString, + 'rate': self.safe_string(order, 'feeRate'), + 'currency': feeCurrencyCode, + } + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'symbol': symbol, + 'type': 'limit', + 'timeInForce': None, + 'postOnly': None, + 'side': side, + 'price': self.safe_string(order, 'price'), + 'triggerPrice': None, + 'cost': None, + 'average': self.safe_string(order, 'averageExecutedPrice'), + 'amount': amountString, + 'filled': self.safe_string(order, 'executedQty'), + 'remaining': remainingString, + 'status': status, + 'fee': fee, + 'trades': None, + }, market) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + # The returned amount might not be same ordered amount. If an order is partially filled, the returned amount means the remaining amount. + # For the same reason, the returned amount and remaining are always same, and the returned filled and cost are always zero. + if symbol is None: + raise ExchangeError(self.id + ' fetchOpenOrders() allows fetching closed orders with a specific symbol') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'currency': market['id'], + } + response = await self.privatePostOrderLimitOrders(self.extend(request, params)) + # + # { + # "result": "success", + # "errorCode": "0", + # "limitOrders": [ + # { + # "index": "0", + # "orderId": "68665943-1eb5-4e4b-9d76-845fc54f5489", + # "timestamp": "1449037367", + # "price": "444000.0", + # "qty": "0.3456", + # "type": "ask", + # "feeRate": "-0.0015" + # } + # ] + # } + # + limitOrders = self.safe_list(response, 'limitOrders', []) + return self.parse_orders(limitOrders, market, since, limit) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMyTrades() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'currency': market['id'], + } + response = await self.v2PrivatePostOrderCompleteOrders(self.extend(request, params)) + # + # despite the name of the endpoint it returns trades which may have a duplicate orderId + # https://github.com/ccxt/ccxt/pull/7067 + # + # { + # "result": "success", + # "errorCode": "0", + # "completeOrders": [ + # { + # "timestamp": "1416561032", + # "price": "419000.0", + # "type": "bid", + # "qty": "0.001", + # "feeRate": "-0.0015", + # "fee": "-0.0000015", + # "orderId": "E84A1AC2-8088-4FA0-B093-A3BCDB9B3C85" + # } + # ] + # } + # + completeOrders = self.safe_list(response, 'completeOrders', []) + return self.parse_trades(completeOrders, market, since, limit) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + if symbol is None: + # eslint-disable-next-line quotes + raise ArgumentsRequired(self.id + " cancelOrder() requires a symbol argument. To cancel the order, pass a symbol argument and {'price': 12345, 'qty': 1.2345, 'is_ask': 0} in the params argument of cancelOrder.") + price = self.safe_number(params, 'price') + qty = self.safe_number(params, 'qty') + isAsk = self.safe_integer(params, 'is_ask') + if (price is None) or (qty is None) or (isAsk is None): + # eslint-disable-next-line quotes + raise ArgumentsRequired(self.id + " cancelOrder() requires {'price': 12345, 'qty': 1.2345, 'is_ask': 0} in the params argument.") + await self.load_markets() + request: dict = { + 'order_id': id, + 'price': price, + 'qty': qty, + 'is_ask': isAsk, + 'currency': self.market_id(symbol), + } + response = await self.v2PrivatePostOrderCancel(self.extend(request, params)) + # + # { + # "result": "success", + # "errorCode": "0" + # } + # + return self.safe_order(response) + + async def fetch_deposit_addresses(self, codes: Strings = None, params={}) -> List[DepositAddress]: + """ + fetch deposit addresses for multiple currencies and chain types + :param str[]|None codes: list of unified currency codes, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `address structures ` + """ + await self.load_markets() + response = await self.v2PrivatePostAccountDepositAddress(params) + # + # { + # "result": "success", + # "errorCode": "0", + # "walletAddress": { + # "matic": null, + # "btc": "mnobqu4i6qMCJWDpf5UimRmr8JCvZ8FLcN", + # "xrp": null, + # "xrp_tag": "-1", + # "kava": null, + # "kava_memo": null, + # } + # } + # + walletAddress = self.safe_dict(response, 'walletAddress', {}) + keys = list(walletAddress.keys()) + result: dict = {} + for i in range(0, len(keys)): + key = keys[i] + value = walletAddress[key] + if (not value) or (value == '-1'): + continue + parts = key.split('_') + currencyId = self.safe_value(parts, 0) + secondPart = self.safe_value(parts, 1) + code = self.safe_currency_code(currencyId) + depositAddress = self.safe_value(result, code) + if depositAddress is None: + depositAddress = { + 'info': value, + 'currency': code, + 'network': None, + 'address': None, + 'tag': None, + } + address = self.safe_string(depositAddress, 'address', value) + self.check_address(address) + depositAddress['address'] = address + depositAddress['info'] = address + if (secondPart == 'tag' or secondPart == 'memo'): + depositAddress['tag'] = value + depositAddress['info'] = [address, value] + result[code] = depositAddress + return result + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + request = self.implode_params(path, params) + query = self.omit(params, self.extract_params(path)) + url = self.urls['api']['rest'] + '/' + if api == 'v2Public': + url = self.urls['api']['v2Public'] + '/' + api = 'public' + elif api == 'v2Private': + url = self.urls['api']['v2Private'] + '/' + elif api == 'v2_1Private': + url = self.urls['api']['v2_1Private'] + '/' + if api == 'public': + url += request + if query: + url += '?' + self.urlencode(query) + else: + self.check_required_credentials() + url += request + nonce = str(self.nonce()) + json = self.json(self.extend({ + 'access_token': self.apiKey, + 'nonce': nonce, + }, params)) + payload = self.string_to_base64(json) + body = payload + secret = self.secret.upper() + signature = self.hmac(self.encode(payload), self.encode(secret), hashlib.sha512) + headers = { + 'Content-Type': 'application/json', + 'X-COINONE-PAYLOAD': payload, + 'X-COINONE-SIGNATURE': signature, + } + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None # fallback to default error handler + # + # {"result":"error","error_code":"107","error_msg":"Parameter value is wrong"} + # {"result":"error","error_code":"108","error_msg":"Unknown CryptoCurrency"} + # + errorCode = self.safe_string(response, 'error_code') + if errorCode is not None and errorCode != '0': + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions, errorCode, feedback) + raise ExchangeError(feedback) # unknown message + return None diff --git a/ccxt/async_support/coinsph.py b/ccxt/async_support/coinsph.py new file mode 100644 index 0000000..17f7095 --- /dev/null +++ b/ccxt/async_support/coinsph.py @@ -0,0 +1,2128 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.coinsph import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currencies, Currency, DepositAddress, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, TradingFeeInterface, TradingFees, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidAddress +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import OrderImmediatelyFillable +from ccxt.base.errors import DuplicateOrderId +from ccxt.base.errors import NotSupported +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.errors import BadResponse +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class coinsph(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(coinsph, self).describe(), { + 'id': 'coinsph', + 'name': 'Coins.ph', + 'countries': ['PH'], # Philippines + 'version': 'v1', + 'rateLimit': 50, # 1200 per minute + 'certified': False, + 'pro': False, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'cancelOrders': False, + 'closeAllPositions': False, + 'closePosition': False, + 'createDepositAddress': False, + 'createMarketBuyOrderWithCost': True, + 'createMarketOrderWithCost': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createOrderWithTakeProfitAndStopLoss': False, + 'createOrderWithTakeProfitAndStopLossWs': False, + 'createPostOnlyOrder': False, + 'createReduceOnlyOrder': False, + 'createStopLimitOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'deposit': True, + 'editOrder': False, + 'fetchAccounts': False, + 'fetchBalance': True, + 'fetchBidsAsks': False, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchCanceledOrders': False, + 'fetchClosedOrder': False, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDeposit': None, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchDepositWithdrawFee': False, + 'fetchDepositWithdrawFees': False, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchL3OrderBook': False, + 'fetchLedger': False, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrder': None, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrderBooks': False, + 'fetchOrders': False, + 'fetchOrderTrades': True, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchStatus': True, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': True, + 'fetchTradingFees': True, + 'fetchTradingLimits': False, + 'fetchTransactionFee': False, + 'fetchTransactionFees': False, + 'fetchTransactions': False, + 'fetchTransfers': False, + 'fetchVolatilityHistory': False, + 'fetchWithdrawal': None, + 'fetchWithdrawals': True, + 'fetchWithdrawalWhitelist': False, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'repayMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'signIn': False, + 'transfer': False, + 'withdraw': True, + 'ws': False, + }, + 'timeframes': { + '1m': '1m', + '3m': '3m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1h', + '2h': '2h', + '4h': '4h', + '6h': '6h', + '8h': '8h', + '12h': '12h', + '1d': '1d', + '3d': '3d', + '1w': '1w', + '1M': '1M', + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/225719995-48ab2026-4ddb-496c-9da7-0d7566617c9b.jpg', + 'api': { + 'public': 'https://api.pro.coins.ph', + 'private': 'https://api.pro.coins.ph', + }, + 'www': 'https://coins.ph/', + 'doc': [ + 'https://coins-docs.github.io/rest-api', + ], + 'fees': 'https://support.coins.ph/hc/en-us/sections/4407198694681-Limits-Fees', + }, + 'api': { + 'public': { + 'get': { + 'openapi/v1/ping': 1, + 'openapi/v1/time': 1, + # cost 1 if 'symbol' param defined(one market symbol) or if 'symbols' param is a list of 1-20 market symbols + # cost 20 if 'symbols' param is a list of 21-100 market symbols + # cost 40 if 'symbols' param is a list of 101 or more market symbols or if both 'symbol' and 'symbols' params are omited + 'openapi/quote/v1/ticker/24hr': {'cost': 1, 'noSymbolAndNoSymbols': 40, 'byNumberOfSymbols': [[101, 40], [21, 20], [0, 1]]}, + # cost 1 if 'symbol' param defined(one market symbol) + # cost 2 if 'symbols' param is a list of 1 or more market symbols or if both 'symbol' and 'symbols' params are omited + 'openapi/quote/v1/ticker/price': {'cost': 1, 'noSymbol': 2}, + # cost 1 if 'symbol' param defined(one market symbol) + # cost 2 if 'symbols' param is a list of 1 or more market symbols or if both 'symbol' and 'symbols' params are omited + 'openapi/quote/v1/ticker/bookTicker': {'cost': 1, 'noSymbol': 2}, + 'openapi/v1/exchangeInfo': 10, + # cost 1 if limit <= 100; 5 if limit > 100. + 'openapi/quote/v1/depth': {'cost': 1, 'byLimit': [[101, 5], [0, 1]]}, + 'openapi/quote/v1/klines': 1, # default limit 500; max 1000. + 'openapi/quote/v1/trades': 1, # default limit 500; max 1000. if limit <=0 or > 1000 then return 1000 + 'openapi/v1/pairs': 1, + 'openapi/quote/v1/avgPrice': 1, + }, + }, + 'private': { + 'get': { + 'openapi/wallet/v1/config/getall': 10, + 'openapi/wallet/v1/deposit/address': 10, + 'openapi/wallet/v1/deposit/history': 1, + 'openapi/wallet/v1/withdraw/history': 1, + 'openapi/v1/account': 10, + # cost 3 for a single symbol; 40 when the symbol parameter is omitted + 'openapi/v1/openOrders': {'cost': 3, 'noSymbol': 40}, + 'openapi/v1/asset/tradeFee': 1, + 'openapi/v1/order': 2, + # cost 10 with symbol, 40 when the symbol parameter is omitted + 'openapi/v1/historyOrders': {'cost': 10, 'noSymbol': 40}, + 'openapi/v1/myTrades': 10, + 'openapi/v1/capital/deposit/history': 1, + 'openapi/v1/capital/withdraw/history': 1, + 'openapi/v3/payment-request/get-payment-request': 1, + 'merchant-api/v1/get-invoices': 1, + 'openapi/account/v3/crypto-accounts': 1, + 'openapi/transfer/v3/transfers/{id}': 1, + }, + 'post': { + 'openapi/wallet/v1/withdraw/apply': 600, + 'openapi/v1/order/test': 1, + 'openapi/v1/order': 1, + 'openapi/v1/capital/withdraw/apply': 1, + 'openapi/v1/capital/deposit/apply': 1, + 'openapi/v3/payment-request/payment-requests': 1, + 'openapi/v3/payment-request/delete-payment-request': 1, + 'openapi/v3/payment-request/payment-request-reminder': 1, + 'openapi/v1/userDataStream': 1, + 'merchant-api/v1/invoices': 1, + 'merchant-api/v1/invoices-cancel': 1, + 'openapi/convert/v1/get-supported-trading-pairs': 1, + 'openapi/convert/v1/get-quote': 1, + 'openapi/convert/v1/accpet-quote': 1, + 'openapi/fiat/v1/support-channel': 1, + 'openapi/fiat/v1/cash-out': 1, + 'openapi/fiat/v1/history': 1, + 'openapi/migration/v4/sellorder': 1, + 'openapi/migration/v4/validate-field': 1, + 'openapi/transfer/v3/transfers': 1, + }, + 'delete': { + 'openapi/v1/order': 1, + 'openapi/v1/openOrders': 1, + 'openapi/v1/userDataStream': 1, + }, + }, + }, + 'fees': { + # todo: zero fees for USDT, ETH and BTC markets till 2023-04-02 + 'trading': { + 'feeSide': 'get', + 'tierBased': True, + 'percentage': True, + 'maker': self.parse_number('0.0025'), + 'taker': self.parse_number('0.003'), + 'tiers': { + 'taker': [ + [self.parse_number('0'), self.parse_number('0.003')], + [self.parse_number('500000'), self.parse_number('0.0027')], + [self.parse_number('1000000'), self.parse_number('0.0024')], + [self.parse_number('2500000'), self.parse_number('0.002')], + [self.parse_number('5000000'), self.parse_number('0.0018')], + [self.parse_number('10000000'), self.parse_number('0.0015')], + [self.parse_number('100000000'), self.parse_number('0.0012')], + [self.parse_number('500000000'), self.parse_number('0.0009')], + [self.parse_number('1000000000'), self.parse_number('0.0007')], + [self.parse_number('2500000000'), self.parse_number('0.0005')], + ], + 'maker': [ + [self.parse_number('0'), self.parse_number('0.0025')], + [self.parse_number('500000'), self.parse_number('0.0022')], + [self.parse_number('1000000'), self.parse_number('0.0018')], + [self.parse_number('2500000'), self.parse_number('0.0015')], + [self.parse_number('5000000'), self.parse_number('0.0012')], + [self.parse_number('10000000'), self.parse_number('0.001')], + [self.parse_number('100000000'), self.parse_number('0.0008')], + [self.parse_number('500000000'), self.parse_number('0.0007')], + [self.parse_number('1000000000'), self.parse_number('0.0006')], + [self.parse_number('2500000000'), self.parse_number('0.0005')], + ], + }, + }, + }, + 'precisionMode': TICK_SIZE, + # exchange-specific options + 'options': { + 'createMarketBuyOrderRequiresPrice': True, # True or False + 'withdraw': { + 'warning': False, + }, + 'deposit': { + 'warning': False, + }, + 'createOrder': { + 'timeInForce': 'GTC', # FOK, IOC + 'newOrderRespType': { + 'market': 'FULL', # FULL, RESULT. ACK + 'limit': 'FULL', # we change it from 'ACK' by default to 'FULL' + }, + }, + 'fetchTicker': { + 'method': 'publicGetOpenapiQuoteV1Ticker24hr', # publicGetOpenapiQuoteV1TickerPrice, publicGetOpenapiQuoteV1TickerBookTicker + }, + 'fetchTickers': { + 'method': 'publicGetOpenapiQuoteV1Ticker24hr', # publicGetOpenapiQuoteV1TickerPrice, publicGetOpenapiQuoteV1TickerBookTicker + }, + 'networks': { + # all networks: 'ETH', 'TRX', 'BSC', 'ARBITRUM', 'RON', 'BTC', 'XRP' + # you can call api privateGetOpenapiWalletV1ConfigGetall to check which network is supported for the currency + 'TRC20': 'TRX', + 'ERC20': 'ETH', + 'BEP20': 'BSC', + 'ARB': 'ARBITRUM', + }, + }, + 'features': { + 'spot': { + 'sandbox': False, + 'fetchCurrencies': { + 'private': True, + }, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': False, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': True, # todo implement + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 100000, + 'untilDays': 100000, # todo implement + 'symbolRequired': True, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 100000, + 'daysBackCanceled': 1, + 'untilDays': 100000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOHLCV': { + 'limit': 1000, + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + # https://coins-docs.github.io/errors/ + 'exceptions': { + 'exact': { + '-1000': BadRequest, # An unknown error occured while processing the request. + '-1001': BadRequest, # {"code":-1001,"msg":"Internal error."} + '-1002': AuthenticationError, # You are not authorized to execute self request. Request need API Key included in . We suggest that API Key be included in any request. + '-1003': RateLimitExceeded, # Too many requests; please use the websocket for live updates. Too many requests; current limit is %s requests per minute. Please use the websocket for live updates to avoid polling the API. Way too many requests; IP banned until %s. Please use the websocket for live updates to avoid bans. + '-1004': InvalidOrder, # {"code":-1004,"msg":"Missing required parameter \u0027symbol\u0027"} + '-1006': BadResponse, # An unexpected response was received from the message bus. Execution status unknown. OPEN API server find some exception in execute request .Please report to Customer service. + '-1007': BadResponse, # Timeout waiting for response from backend server. Send status unknown; execution status unknown. + '-1014': InvalidOrder, # Unsupported order combination. + '-1015': RateLimitExceeded, # Reach the rate limit .Please slow down your request speed. Too many new orders. Too many new orders; current limit is %s orders per %s. + '-1016': NotSupported, # This service is no longer available. + '-1020': NotSupported, # This operation is not supported. + '-1021': BadRequest, # {"code":-1021,"msg":"Timestamp for self request is outside of the recvWindow."} + '-1022': BadRequest, # {"code":-1022,"msg":"Signature for self request is not valid."} + '-1023': AuthenticationError, # Please set IP whitelist before using API. + '-1024': BadRequest, # {"code":-1024,"msg":"recvWindow is not valid."} + '-1025': BadRequest, # {"code":-1025,"msg":"recvWindow cannot be greater than 60000"} + '-1030': ExchangeError, # Business error. + '-1100': BadRequest, # Illegal characters found in a parameter. Illegal characters found in parameter ‘%s’; legal range is ‘%s’. + '-1101': BadRequest, # Too many parameters sent for self endpoint. Too many parameters; expected ‘%s’ and received ‘%s’. Duplicate values for a parameter detected. + '-1102': BadRequest, # A mandatory parameter was not sent, was empty/null, or malformed. Mandatory parameter ‘%s’ was not sent, was empty/null, or malformed. Param ‘%s’ or ‘%s’ must be sent, but both were empty/null! + '-1103': BadRequest, # An unknown parameter was sent. In BHEx Open Api , each request requires at least one parameter. {Timestamp}. + '-1104': BadRequest, # Not all sent parameters were read. Not all sent parameters were read; read ‘%s’ parameter(s) but was sent ‘%s’. + '-1105': BadRequest, # {"code":-1105,"msg":"Parameter \u0027orderId and origClientOrderId\u0027 is empty."} + '-1106': BadRequest, # A parameter was sent when not required. Parameter ‘%s’ sent when not required. + '-1111': BadRequest, # Precision is over the maximum defined for self asset. + '-1112': BadResponse, # No orders on book for symbol. + '-1114': BadRequest, # TimeInForce parameter sent when not required. + '-1115': InvalidOrder, # {"code":-1115,"msg":"Invalid timeInForce."} + '-1116': InvalidOrder, # {"code":-1116,"msg":"Invalid orderType."} + '-1117': InvalidOrder, # {"code":-1117,"msg":"Invalid side."} + '-1118': InvalidOrder, # New client order ID was empty. + '-1119': InvalidOrder, # Original client order ID was empty. + '-1120': BadRequest, # Invalid interval. + '-1121': BadSymbol, # Invalid symbol. + '-1122': InvalidOrder, # Invalid newOrderRespType. + '-1125': BadRequest, # This listenKey does not exist. + '-1127': BadRequest, # Lookup interval is too big. More than %s hours between startTime and endTime. + '-1128': BadRequest, # Combination of optional parameters invalid. + '-1130': BadRequest, # Invalid data sent for a parameter. Data sent for paramter ‘%s’ is not valid. + '-1131': InsufficientFunds, # {"code":-1131,"msg":"Balance insufficient "} + '-1132': InvalidOrder, # Order price too high. + '-1133': InvalidOrder, # Order price lower than the minimum,please check general broker info. + '-1134': InvalidOrder, # Order price decimal too long,please check general broker info. + '-1135': InvalidOrder, # Order quantity too large. + '-1136': InvalidOrder, # Order quantity lower than the minimum. + '-1137': InvalidOrder, # Order quantity decimal too long. + '-1138': InvalidOrder, # Order price exceeds permissible range. + '-1139': InvalidOrder, # Order has been filled. + '-1140': InvalidOrder, # {"code":-1140,"msg":"Transaction amount lower than the minimum."} + '-1141': DuplicateOrderId, # {"code":-1141,"msg":"Duplicate clientOrderId"} + '-1142': InvalidOrder, # {"code":-1142,"msg":"Order has been canceled"} + '-1143': OrderNotFound, # Cannot be found on order book + '-1144': InvalidOrder, # Order has been locked + '-1145': InvalidOrder, # This order type does not support cancellation + '-1146': InvalidOrder, # Order creation timeout + '-1147': InvalidOrder, # Order cancellation timeout + '-1148': InvalidOrder, # Market order amount decimal too long + '-1149': InvalidOrder, # Create order failed + '-1150': InvalidOrder, # Cancel order failed + '-1151': BadSymbol, # The trading pair is not open yet + '-1152': NotSupported, # Coming soon + '-1153': AuthenticationError, # User not exist + '-1154': BadRequest, # Invalid price type + '-1155': BadRequest, # Invalid position side + '-1156': InvalidOrder, # Order quantity invalid + '-1157': BadSymbol, # The trading pair is not available for api trading + '-1158': InvalidOrder, # create limit maker order failed + '-1159': InvalidOrder, # {"code":-1159,"msg":"STOP_LOSS/TAKE_PROFIT order is not allowed to trade immediately"} + '-1160': BadRequest, # Modify futures margin error + '-1161': BadRequest, # Reduce margin forbidden + '-2010': InvalidOrder, # {"code":-2010,"msg":"New order rejected."} + '-2013': OrderNotFound, # {"code":-2013,"msg":"Order does not exist."} + '-2011': BadRequest, # CANCEL_REJECTED + '-2014': BadRequest, # API-key format invalid. + '-2015': AuthenticationError, # {"code":-2015,"msg":"Invalid API-key, IP, or permissions for action."} + '-2016': BadResponse, # No trading window could be found for the symbol. Try ticker/24hrs instead + '-3126': InvalidOrder, # {"code":-3126,"msg":"Order price lower than 72005.93415"} + '-3127': InvalidOrder, # {"code":-3127,"msg":"Order price higher than 1523.192"} + '-4001': BadRequest, # {"code":-4001,"msg":"start time must less than end time"} + '-100011': BadSymbol, # {"code":-100011,"msg":"Not supported symbols"} + '-100012': BadSymbol, # {"code":-100012,"msg":"Parameter symbol [str] missing!"} + '-30008': InsufficientFunds, # {"code":-30008,"msg":"withdraw balance insufficient"} + '-30036': InsufficientFunds, # {"code":-30036,"msg":"Available balance not enough!"} + '403': ExchangeNotAvailable, + }, + 'broad': { + 'Unknown order sent': OrderNotFound, # The order(by either orderId, clOrdId, origClOrdId) could not be found + 'Duplicate order sent': DuplicateOrderId, # The clOrdId is already in use + 'Market is closed': BadSymbol, # The symbol is not trading + 'Account has insufficient balance for requested action': InsufficientFunds, # Not enough funds to complete the action + 'Market orders are not supported for self symbol': BadSymbol, # MARKET is not enabled on the symbol + 'Iceberg orders are not supported for self symbol': BadSymbol, # icebergQty is not enabled on the symbol + 'Stop loss orders are not supported for self symbol': BadSymbol, # STOP_LOSS is not enabled on the symbol + 'Stop loss limit orders are not supported for self symbol': BadSymbol, # STOP_LOSS_LIMIT is not enabled on the symbol + 'Take profit orders are not supported for self symbol': BadSymbol, # TAKE_PROFIT is not enabled on the symbol + 'Take profit limit orders are not supported for self symbol': BadSymbol, # TAKE_PROFIT_LIMIT is not enabled on the symbol + 'Price* QTY is zero or less': BadRequest, # price* quantity is too low + 'IcebergQty exceeds QTY': BadRequest, # icebergQty must be less than the order quantity + 'This action disabled is on self account': PermissionDenied, # Contact customer support; some actions have been disabled on the account. + 'Unsupported order combination': InvalidOrder, # The orderType, timeInForce, stopPrice, and or icebergQty combination isn’t allowed. + 'Order would trigger immediately': InvalidOrder, # The order’s stop price is not valid when compared to the last traded price. + 'Cancel order is invalid. Check origClOrdId and orderId': InvalidOrder, # No origClOrdId or orderId was sent in. + 'Order would immediately match and take': OrderImmediatelyFillable, # LIMIT_MAKER order type would immediately match and trade, and not be a pure maker order. + 'PRICE_FILTER': InvalidOrder, # price is too high, too low, and or not following the tick size rule for the symbol. + 'LOT_SIZE': InvalidOrder, # quantity is too high, too low, and or not following the step size rule for the symbol. + 'MIN_NOTIONAL': InvalidOrder, # price* quantity is too low to be a valid order for the symbol. + 'MAX_NUM_ORDERS': InvalidOrder, # Account has too many open orders on the symbol. + 'MAX_ALGO_ORDERS': InvalidOrder, # Account has too many open stop loss and or take profit orders on the symbol. + 'BROKER_MAX_NUM_ORDERS': InvalidOrder, # Account has too many open orders on the broker. + 'BROKER_MAX_ALGO_ORDERS': InvalidOrder, # Account has too many open stop loss and or take profit orders on the broker. + 'ICEBERG_PARTS': BadRequest, # Iceberg order would break into too many parts; icebergQty is too small. + }, + }, + }) + + async def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://docs.coins.ph/rest-api/#all-coins-information-user_data + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + if not self.check_required_credentials(False): + return {} + response = await self.privateGetOpenapiWalletV1ConfigGetall(params) + # + # [ + # { + # "coin": "PHP", + # "name": "PHP", + # "depositAllEnable": False, + # "withdrawAllEnable": False, + # "free": "0", + # "locked": "0", + # "transferPrecision": "2", + # "transferMinQuantity": "0", + # "networkList": [], + # "legalMoney": True + # }, + # { + # "coin": "USDT", + # "name": "USDT", + # "depositAllEnable": True, + # "withdrawAllEnable": True, + # "free": "0", + # "locked": "0", + # "transferPrecision": "8", + # "transferMinQuantity": "0", + # "networkList": [ + # { + # "addressRegex": "^0x[0-9a-fA-F]{40}$", + # "memoRegex": " ", + # "network": "ETH", + # "name": "Ethereum(ERC20)", + # "depositEnable": True, + # "minConfirm": "12", + # "unLockConfirm": "-1", + # "withdrawDesc": "", + # "withdrawEnable": True, + # "withdrawFee": "6", + # "withdrawIntegerMultiple": "0.000001", + # "withdrawMax": "500000", + # "withdrawMin": "10", + # "sameAddress": False + # }, + # { + # "addressRegex": "^T[0-9a-zA-Z]{33}$", + # "memoRegex": "", + # "network": "TRX", + # "name": "TRON", + # "depositEnable": True, + # "minConfirm": "19", + # "unLockConfirm": "-1", + # "withdrawDesc": "", + # "withdrawEnable": True, + # "withdrawFee": "3", + # "withdrawIntegerMultiple": "0.000001", + # "withdrawMax": "1000000", + # "withdrawMin": "20", + # "sameAddress": False + # } + # ], + # "legalMoney": False + # } + # ] + # + result: dict = {} + for i in range(0, len(response)): + entry = response[i] + id = self.safe_string(entry, 'coin') + code = self.safe_currency_code(id) + isFiat = self.safe_bool(entry, 'isLegalMoney') + networkList = self.safe_list(entry, 'networkList', []) + networks: dict = {} + for j in range(0, len(networkList)): + networkItem = networkList[j] + network = self.safe_string(networkItem, 'network') + networkCode = self.network_id_to_code(network) + networks[networkCode] = { + 'info': networkItem, + 'id': network, + 'network': networkCode, + 'active': None, + 'deposit': self.safe_bool(networkItem, 'depositEnable'), + 'withdraw': self.safe_bool(networkItem, 'withdrawEnable'), + 'fee': self.safe_number(networkItem, 'withdrawFee'), + 'precision': self.safe_number(networkItem, 'withdrawIntegerMultiple'), + 'limits': { + 'withdraw': { + 'min': self.safe_number(networkItem, 'withdrawMin'), + 'max': self.safe_number(networkItem, 'withdrawMax'), + }, + 'deposit': { + 'min': None, + 'max': None, + }, + }, + } + result[code] = self.safe_currency_structure({ + 'id': id, + 'name': self.safe_string(entry, 'name'), + 'code': code, + 'type': 'fiat' if isFiat else 'crypto', + 'precision': self.parse_number(self.parse_precision(self.safe_string(entry, 'transferPrecision'))), + 'info': entry, + 'active': None, + 'deposit': self.safe_bool(entry, 'depositAllEnable'), + 'withdraw': self.safe_bool(entry, 'withdrawAllEnable'), + 'networks': networks, + 'fee': None, + 'fees': None, + 'limits': {}, + }) + return result + + def calculate_rate_limiter_cost(self, api, method, path, params, config={}): + if ('noSymbol' in config) and not ('symbol' in params): + return config['noSymbol'] + elif ('noSymbolAndNoSymbols' in config) and not ('symbol' in params) and not ('symbols' in params): + return config['noSymbolAndNoSymbols'] + elif ('byNumberOfSymbols' in config) and ('symbols' in params): + symbols = params['symbols'] + symbolsAmount = len(symbols) + byNumberOfSymbols = config['byNumberOfSymbols'] + for i in range(0, len(byNumberOfSymbols)): + entry = byNumberOfSymbols[i] + if symbolsAmount >= entry[0]: + return entry[1] + elif ('byLimit' in config) and ('limit' in params): + limit = params['limit'] + byLimit = config['byLimit'] + for i in range(0, len(byLimit)): + entry = byLimit[i] + if limit >= entry[0]: + return entry[1] + return self.safe_value(config, 'cost', 1) + + async def fetch_status(self, params={}): + """ + the latest known information on the availability of the exchange API + + https://coins-docs.github.io/rest-api/#test-connectivity + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `status structure ` + """ + response = await self.publicGetOpenapiV1Ping(params) + return { + 'status': 'ok', # if there's no Errors, status = 'ok' + 'updated': None, + 'eta': None, + 'url': None, + 'info': response, + } + + async def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://coins-docs.github.io/rest-api/#check-server-time + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = await self.publicGetOpenapiV1Time(params) + # + # {"serverTime":1677705408268} + # + return self.safe_integer(response, 'serverTime') + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for coinsph + + https://coins-docs.github.io/rest-api/#exchange-information + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = await self.publicGetOpenapiV1ExchangeInfo(params) + # + # { + # "timezone": "UTC", + # "serverTime": "1677449496897", + # "exchangeFilters": [], + # "symbols": [ + # { + # "symbol": "XRPPHP", + # "status": "TRADING", + # "baseAsset": "XRP", + # "baseAssetPrecision": "2", + # "quoteAsset": "PHP", + # "quoteAssetPrecision": "4", + # "orderTypes": [ + # "LIMIT", + # "MARKET", + # "LIMIT_MAKER", + # "STOP_LOSS_LIMIT", + # "STOP_LOSS", + # "TAKE_PROFIT_LIMIT", + # "TAKE_PROFIT" + # ], + # "filters": [ + # { + # "minPrice": "0.01", + # "maxPrice": "99999999.00000000", + # "tickSize": "0.01", + # "filterType": "PRICE_FILTER" + # }, + # { + # "minQty": "0.01", + # "maxQty": "99999999999.00000000", + # "stepSize": "0.01", + # "filterType": "LOT_SIZE" + # }, + # {minNotional: "50", filterType: "NOTIONAL"}, + # {minNotional: "50", filterType: "MIN_NOTIONAL"}, + # { + # "priceUp": "99999999", + # "priceDown": "0.01", + # "filterType": "STATIC_PRICE_RANGE" + # }, + # { + # "multiplierUp": "1.1", + # "multiplierDown": "0.9", + # "filterType": "PERCENT_PRICE_INDEX" + # }, + # { + # "multiplierUp": "1.1", + # "multiplierDown": "0.9", + # "filterType": "PERCENT_PRICE_ORDER_SIZE" + # }, + # {maxNumOrders: "200", filterType: "MAX_NUM_ORDERS"}, + # {maxNumAlgoOrders: "5", filterType: "MAX_NUM_ALGO_ORDERS"} + # ] + # }, + # ] + # } + # + markets = self.safe_list(response, 'symbols', []) + result = [] + for i in range(0, len(markets)): + market = markets[i] + id = self.safe_string(market, 'symbol') + baseId = self.safe_string(market, 'baseAsset') + quoteId = self.safe_string(market, 'quoteAsset') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + limits = self.index_by(self.safe_list(market, 'filters', []), 'filterType') + amountLimits = self.safe_value(limits, 'LOT_SIZE', {}) + priceLimits = self.safe_value(limits, 'PRICE_FILTER', {}) + costLimits = self.safe_value(limits, 'NOTIONAL', {}) + result.append({ + 'id': id, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': self.safe_string_lower(market, 'status') == 'trading', + 'contract': False, + 'linear': None, + 'inverse': None, + 'taker': None, + 'maker': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(self.safe_string(amountLimits, 'stepSize')), + 'price': self.parse_number(self.safe_string(priceLimits, 'tickSize')), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.parse_number(self.safe_string(amountLimits, 'minQty')), + 'max': self.parse_number(self.safe_string(amountLimits, 'maxQty')), + }, + 'price': { + 'min': self.parse_number(self.safe_string(priceLimits, 'minPrice')), + 'max': self.parse_number(self.safe_string(priceLimits, 'maxPrice')), + }, + 'cost': { + 'min': self.parse_number(self.safe_string(costLimits, 'minNotional')), + 'max': None, + }, + }, + 'created': None, + 'info': market, + }) + self.set_markets(result) + return result + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://coins-docs.github.io/rest-api/#24hr-ticker-price-change-statistics + https://coins-docs.github.io/rest-api/#symbol-price-ticker + https://coins-docs.github.io/rest-api/#symbol-order-book-ticker + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + request: dict = {} + if symbols is not None: + ids = [] + for i in range(0, len(symbols)): + market = self.market(symbols[i]) + id = market['id'] + ids.append(id) + request['symbols'] = ids + defaultMethod = 'publicGetOpenapiQuoteV1Ticker24hr' + options = self.safe_dict(self.options, 'fetchTickers', {}) + method = self.safe_string(options, 'method', defaultMethod) + tickers = None + if method == 'publicGetOpenapiQuoteV1TickerPrice': + tickers = await self.publicGetOpenapiQuoteV1TickerPrice(self.extend(request, params)) + elif method == 'publicGetOpenapiQuoteV1TickerBookTicker': + tickers = await self.publicGetOpenapiQuoteV1TickerBookTicker(self.extend(request, params)) + else: + tickers = await self.publicGetOpenapiQuoteV1Ticker24hr(self.extend(request, params)) + return self.parse_tickers(tickers, symbols, params) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://coins-docs.github.io/rest-api/#24hr-ticker-price-change-statistics + https://coins-docs.github.io/rest-api/#symbol-price-ticker + https://coins-docs.github.io/rest-api/#symbol-order-book-ticker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + defaultMethod = 'publicGetOpenapiQuoteV1Ticker24hr' + options = self.safe_dict(self.options, 'fetchTicker', {}) + method = self.safe_string(options, 'method', defaultMethod) + ticker = None + if method == 'publicGetOpenapiQuoteV1TickerPrice': + ticker = await self.publicGetOpenapiQuoteV1TickerPrice(self.extend(request, params)) + elif method == 'publicGetOpenapiQuoteV1TickerBookTicker': + ticker = await self.publicGetOpenapiQuoteV1TickerBookTicker(self.extend(request, params)) + else: + ticker = await self.publicGetOpenapiQuoteV1Ticker24hr(self.extend(request, params)) + return self.parse_ticker(ticker, market) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # publicGetOpenapiQuoteV1Ticker24hr + # { + # "symbol": "ETHUSDT", + # "priceChange": "41.440000000000000000", + # "priceChangePercent": "0.0259", + # "weightedAvgPrice": "1631.169825783972125436", + # "prevClosePrice": "1601.520000000000000000", + # "lastPrice": "1642.96", + # "lastQty": "0.000001000000000000", + # "bidPrice": "1638.790000000000000000", + # "bidQty": "0.280075000000000000", + # "askPrice": "1647.340000000000000000", + # "askQty": "0.165183000000000000", + # "openPrice": "1601.52", + # "highPrice": "1648.28", + # "lowPrice": "1601.52", + # "volume": "0.000287", + # "quoteVolume": "0.46814574", + # "openTime": "1677417000000", + # "closeTime": "1677503415200", + # "firstId": "1364680572697591809", + # "lastId": "1365389809203560449", + # "count": "100" + # } + # + # publicGetOpenapiQuoteV1TickerPrice + # {"symbol": "ETHUSDT", "price": "1599.68"} + # + # publicGetOpenapiQuoteV1TickerBookTicker + # { + # "symbol": "ETHUSDT", + # "bidPrice": "1596.57", + # "bidQty": "0.246405", + # "askPrice": "1605.12", + # "askQty": "0.242681" + # } + # + marketId = self.safe_string(ticker, 'symbol') + market = self.safe_market(marketId, market) + timestamp = self.safe_integer(ticker, 'closeTime') + bid = self.safe_string(ticker, 'bidPrice') + ask = self.safe_string(ticker, 'askPrice') + bidVolume = self.safe_string(ticker, 'bidQty') + askVolume = self.safe_string(ticker, 'askQty') + baseVolume = self.safe_string(ticker, 'volume') + quoteVolume = self.safe_string(ticker, 'quoteVolume') + open = self.safe_string(ticker, 'openPrice') + high = self.safe_string(ticker, 'highPrice') + low = self.safe_string(ticker, 'lowPrice') + prevClose = self.safe_string(ticker, 'prevClosePrice') + vwap = self.safe_string(ticker, 'weightedAvgPrice') + changeValue = self.safe_string(ticker, 'priceChange') + changePcnt = self.safe_string(ticker, 'priceChangePercent') + changePcnt = Precise.string_mul(changePcnt, '100') + return self.safe_ticker({ + 'symbol': market['symbol'], + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'open': open, + 'high': high, + 'low': low, + 'close': self.safe_string_2(ticker, 'lastPrice', 'price'), + 'bid': bid, + 'bidVolume': bidVolume, + 'ask': ask, + 'askVolume': askVolume, + 'vwap': vwap, + 'previousClose': prevClose, + 'change': changeValue, + 'percentage': changePcnt, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'info': ticker, + }, market) + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://coins-docs.github.io/rest-api/#order-book + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return(default 100, max 200) + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['limit'] = limit + response = await self.publicGetOpenapiQuoteV1Depth(self.extend(request, params)) + # + # { + # "lastUpdateId": "1667022157000699400", + # "bids": [ + # ['1651.810000000000000000', '0.214556000000000000'], + # ['1651.730000000000000000', '0.257343000000000000'], + # ], + # "asks": [ + # ['1660.510000000000000000', '0.299092000000000000'], + # ['1660.600000000000000000', '0.253667000000000000'], + # ] + # } + # + orderbook = self.parse_order_book(response, symbol) + orderbook['nonce'] = self.safe_integer(response, 'lastUpdateId') + return orderbook + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://coins-docs.github.io/rest-api/#klinecandlestick-data + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch(default 500, max 1000) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest candle to fetch + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + interval = self.safe_string(self.timeframes, timeframe) + until = self.safe_integer(params, 'until') + request: dict = { + 'symbol': market['id'], + 'interval': interval, + } + if limit is None: + limit = 1000 + if since is not None: + request['startTime'] = since + # since work properly only when it is "younger" than last "limit" candle + if until is not None: + request['endTime'] = until + else: + duration = self.parse_timeframe(timeframe) * 1000 + endTimeByLimit = self.sum(since, duration * (limit - 1)) + now = self.milliseconds() + request['endTime'] = min(endTimeByLimit, now) + elif until is not None: + request['endTime'] = until + # since work properly only when it is "younger" than last "limit" candle + duration = self.parse_timeframe(timeframe) * 1000 + request['startTime'] = until - (duration * (limit - 1)) + request['limit'] = limit + params = self.omit(params, 'until') + response = await self.publicGetOpenapiQuoteV1Klines(self.extend(request, params)) + # + # [ + # [ + # 1499040000000, # Open time + # "0.01634790", # Open + # "0.80000000", # High + # "0.01575800", # Low + # "0.01577100", # Close + # "148976.11427815", # Volume + # 1499644799999, # Close time + # "2434.19055334", # Quote asset volume + # 308, # Number of trades + # "1756.87402397", # Taker buy base asset volume + # "28.46694368" # Taker buy quote asset volume + # ] + # ] + # + return self.parse_ohlcvs(response, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + return [ + self.safe_integer(ohlcv, 0), + self.safe_number(ohlcv, 1), + self.safe_number(ohlcv, 2), + self.safe_number(ohlcv, 3), + self.safe_number(ohlcv, 4), + self.safe_number(ohlcv, 5), + ] + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://coins-docs.github.io/rest-api/#recent-trades-list + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch(default 500, max 1000) + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if since is not None: + # since work properly only when it is "younger" than last 'limit' trade + request['limit'] = 1000 + else: + if limit is not None: + request['limit'] = limit + response = await self.publicGetOpenapiQuoteV1Trades(self.extend(request, params)) + # + # [ + # { + # "price": "89685.8", + # "id": "1365561108437680129", + # "qty": "0.000004", + # "quoteQty": "0.000004000000000000", + # "time": "1677523569575", + # "isBuyerMaker": False, + # "isBestMatch": True + # }, + # ] + # + return self.parse_trades(response, market, since, limit) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://coins-docs.github.io/rest-api/#account-trade-list-user_data + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve(default 500, max 1000) + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMyTrades() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if since is not None: + request['startTime'] = since + # since work properly only when it is "younger" than last 'limit' trade + request['limit'] = 1000 + elif limit is not None: + request['limit'] = limit + response = await self.privateGetOpenapiV1MyTrades(self.extend(request, params)) + return self.parse_trades(response, market, since, limit) + + async def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all the trades made from a single order + + https://coins-docs.github.io/rest-api/#account-trade-list-user_data + + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrderTrades() requires a symbol argument') + request: dict = { + 'orderId': id, + } + return await self.fetch_my_trades(symbol, since, limit, self.extend(request, params)) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades + # { + # "price": "89685.8", + # "id": "1365561108437680129", + # "qty": "0.000004", + # "quoteQty": "0.000004000000000000", # warning: report to exchange - self is not quote quantity, self is base quantity + # "time": "1677523569575", + # "isBuyerMaker": False, + # "isBestMatch": True + # }, + # + # fetchMyTrades + # { + # "symbol": "ETHUSDT", + # "id": 1375426310524125185, + # "orderId": 1375426310415879614, + # "price": "1580.91", + # "qty": "0.01", + # "quoteQty": "15.8091", + # "commission": "0", + # "commissionAsset": "USDT", + # "time": 1678699593307, + # "isBuyer": False, + # "isMaker":false, + # "isBestMatch":false + # } + # + # createOrder + # { + # "price": "1579.51", + # "qty": "0.001899", + # "commission": "0", + # "commissionAsset": "ETH", + # "tradeId":1375445992035598337 + # } + # + marketId = self.safe_string(trade, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + id = self.safe_string_2(trade, 'id', 'tradeId') + orderId = self.safe_string(trade, 'orderId') + timestamp = self.safe_integer(trade, 'time') + priceString = self.safe_string(trade, 'price') + amountString = self.safe_string(trade, 'qty') + type = None + fee = None + feeCost = self.safe_string(trade, 'commission') + if feeCost is not None: + feeCurrencyId = self.safe_string(trade, 'commissionAsset') + fee = { + 'cost': feeCost, + 'currency': self.safe_currency_code(feeCurrencyId), + } + isBuyer = self.safe_bool_2(trade, 'isBuyer', 'isBuyerMaker', None) + side = None + if isBuyer is not None: + side = 'buy' if (isBuyer is True) else 'sell' + isMaker = self.safe_string_2(trade, 'isMaker', None) + takerOrMaker = None + if isMaker is not None: + takerOrMaker = 'maker' if (isMaker == 'true') else 'taker' + costString = None + if orderId is not None: + costString = self.safe_string(trade, 'quoteQty') + return self.safe_trade({ + 'id': id, + 'order': orderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'type': type, + 'side': side, + 'takerOrMaker': takerOrMaker, + 'price': priceString, + 'amount': amountString, + 'cost': costString, + 'fee': fee, + 'info': trade, + }, market) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://coins-docs.github.io/rest-api/#accept-the-quote + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + response = await self.privateGetOpenapiV1Account(params) + # + # { + # "accountType": "SPOT", + # "balances": [ + # { + # "asset": "BTC", + # "free": "4723846.89208129", + # "locked": "0.00000000" + # }, + # { + # "asset": "LTC", + # "free": "4763368.68006011", + # "locked": "0.00000000" + # } + # ], + # "canDeposit": True, + # "canTrade": True, + # "canWithdraw": True, + # "updateTime": "1677430932528" + # } + # + return self.parse_balance(response) + + def parse_balance(self, response) -> Balances: + balances = self.safe_list(response, 'balances', []) + result: dict = { + 'info': response, + 'timestamp': None, + 'datetime': None, + } + for i in range(0, len(balances)): + balance = balances[i] + currencyId = self.safe_string(balance, 'asset') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(balance, 'free') + account['used'] = self.safe_string(balance, 'locked') + result[code] = account + return self.safe_balance(result) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://coins-docs.github.io/rest-api/#new-order--trade + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market', 'limit', 'stop_loss', 'take_profit', 'stop_loss_limit', 'take_profit_limit' or 'limit_maker' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.cost]: the quote quantity that can be used alternative for the amount for market buy orders + :param bool [params.test]: set to True to test an order, no order will be created but the request will be validated + :returns dict: an `order structure ` + """ + # todo: add test order low priority + await self.load_markets() + market = self.market(symbol) + testOrder = self.safe_bool(params, 'test', False) + params = self.omit(params, 'test') + orderType = self.safe_string(params, 'type', type) + orderType = self.encode_order_type(orderType) + params = self.omit(params, 'type') + orderSide = self.encode_order_side(side) + request: dict = { + 'symbol': market['id'], + 'type': orderType, + 'side': orderSide, + } + options = self.safe_value(self.options, 'createOrder', {}) + newOrderRespType = self.safe_value(options, 'newOrderRespType', {}) + # if limit order + if orderType == 'LIMIT' or orderType == 'STOP_LOSS_LIMIT' or orderType == 'TAKE_PROFIT_LIMIT' or orderType == 'LIMIT_MAKER': + if price is None: + raise ArgumentsRequired(self.id + ' createOrder() requires a price argument for a ' + type + ' order') + newOrderRespType = self.safe_string(newOrderRespType, 'limit', 'FULL') + request['price'] = self.price_to_precision(symbol, price) + request['quantity'] = self.amount_to_precision(symbol, amount) + if orderType != 'LIMIT_MAKER': + request['timeInForce'] = self.safe_string(options, 'timeInForce', 'GTC') + # if market order + elif orderType == 'MARKET' or orderType == 'STOP_LOSS' or orderType == 'TAKE_PROFIT': + newOrderRespType = self.safe_string(newOrderRespType, 'market', 'FULL') + if orderSide == 'SELL': + request['quantity'] = self.amount_to_precision(symbol, amount) + elif orderSide == 'BUY': + quoteAmount = None + createMarketBuyOrderRequiresPrice = True + createMarketBuyOrderRequiresPrice, params = self.handle_option_and_params(params, 'createOrder', 'createMarketBuyOrderRequiresPrice', True) + cost = self.safe_number_2(params, 'cost', 'quoteOrderQty') + params = self.omit(params, 'cost') + if cost is not None: + quoteAmount = self.cost_to_precision(symbol, cost) + elif createMarketBuyOrderRequiresPrice: + if price is None: + raise InvalidOrder(self.id + ' createOrder() requires the price argument for market buy orders to calculate the total cost to spend(amount * price), alternatively set the createMarketBuyOrderRequiresPrice option or param to False and pass the cost to spend in the amount argument') + else: + amountString = self.number_to_string(amount) + priceString = self.number_to_string(price) + costRequest = Precise.string_mul(amountString, priceString) + quoteAmount = self.cost_to_precision(symbol, costRequest) + else: + quoteAmount = self.cost_to_precision(symbol, amount) + request['quoteOrderQty'] = quoteAmount + if orderType == 'STOP_LOSS' or orderType == 'STOP_LOSS_LIMIT' or orderType == 'TAKE_PROFIT' or orderType == 'TAKE_PROFIT_LIMIT': + triggerPrice = self.safe_string_2(params, 'triggerPrice', 'stopPrice') + if triggerPrice is None: + raise InvalidOrder(self.id + ' createOrder() requires a triggerPrice or stopPrice param for stop_loss, take_profit, stop_loss_limit, and take_profit_limit orders') + request['stopPrice'] = self.price_to_precision(symbol, triggerPrice) + request['newOrderRespType'] = newOrderRespType + params = self.omit(params, 'price', 'stopPrice', 'triggerPrice', 'quantity', 'quoteOrderQty') + response = None + if testOrder: + response = await self.privatePostOpenapiV1OrderTest(self.extend(request, params)) + else: + response = await self.privatePostOpenapiV1Order(self.extend(request, params)) + # + # { + # "symbol": "ETHUSDT", + # "orderId": "1375407140139731486", + # "clientOrderId": "1375407140139733169", + # "transactTime": "1678697308023", + # "price": "1600", + # "origQty": "0.02", + # "executedQty": "0.02", + # "cummulativeQuoteQty": "31.9284", + # "status": "FILLED", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "side": "BUY", + # "stopPrice": "0", + # "origQuoteOrderQty": "0", + # "fills": [ + # { + # "price": "1596.42", + # "qty": "0.02", + # "commission": "0", + # "commissionAsset": "ETH", + # "tradeId": "1375407140281532417" + # } + # ] + # }, + # + return self.parse_order(response, market) + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://coins-docs.github.io/rest-api/#query-order-user_data + + :param int|str id: order id + :param str symbol: not used by coinsph fetchOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + request: dict = {} + clientOrderId = self.safe_value_2(params, 'origClientOrderId', 'clientOrderId') + if clientOrderId is not None: + request['origClientOrderId'] = clientOrderId + else: + request['orderId'] = id + params = self.omit(params, ['clientOrderId', 'origClientOrderId']) + response = await self.privateGetOpenapiV1Order(self.extend(request, params)) + return self.parse_order(response) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://coins-docs.github.io/rest-api/#current-open-orders-user_data + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + market = None + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + response = await self.privateGetOpenapiV1OpenOrders(self.extend(request, params)) + return self.parse_orders(response, market, since, limit) + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://coins-docs.github.io/rest-api/#history-orders-user_data + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve(default 500, max 1000) + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchClosedOrders() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if since is not None: + request['startTime'] = since + # since work properly only when it is "younger" than last 'limit' order + request['limit'] = 1000 + elif limit is not None: + request['limit'] = limit + response = await self.privateGetOpenapiV1HistoryOrders(self.extend(request, params)) + return self.parse_orders(response, market, since, limit) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://coins-docs.github.io/rest-api/#cancel-order-trade + + :param str id: order id + :param str symbol: not used by coinsph cancelOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + request: dict = {} + clientOrderId = self.safe_value_2(params, 'origClientOrderId', 'clientOrderId') + if clientOrderId is not None: + request['origClientOrderId'] = clientOrderId + else: + request['orderId'] = id + params = self.omit(params, ['clientOrderId', 'origClientOrderId']) + response = await self.privateDeleteOpenapiV1Order(self.extend(request, params)) + return self.parse_order(response) + + async def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel open orders of market + + https://coins-docs.github.io/rest-api/#cancel-all-open-orders-on-a-symbol-trade + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelAllOrders() requires a symbol argument') + await self.load_markets() + market = None + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + response = await self.privateDeleteOpenapiV1OpenOrders(self.extend(request, params)) + return self.parse_orders(response, market) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder POST /openapi/v1/order + # { + # "symbol": "ETHUSDT", + # "orderId": 1375445991893797391, + # "clientOrderId": "1375445991893799115", + # "transactTime": 1678701939513, + # "price": "0", + # "origQty": "0", + # "executedQty": "0.001899", + # "cummulativeQuoteQty": "2.99948949", + # "status": "FILLED", + # "timeInForce": "GTC", + # "type": "MARKET", + # "side": "BUY", + # "stopPrice": "0", + # "origQuoteOrderQty": "3", + # "fills": [ + # { + # "price": "1579.51", + # "qty": "0.001899", + # "commission": "0", + # "commissionAsset": "ETH", + # "tradeId":1375445992035598337 + # } + # ] + # } + # + # fetchOrder GET /openapi/v1/order + # fetchOpenOrders GET /openapi/v1/openOrders + # fetchClosedOrders GET /openapi/v1/historyOrders + # cancelAllOrders DELETE /openapi/v1/openOrders + # { + # "symbol": "DOGEPHP", + # "orderId":1375465375097982423, + # "clientOrderId": "1375465375098001241", + # "price": "0", + # "origQty": "0", + # "executedQty": "13", + # "cummulativeQuoteQty": "49.621", + # "status": "FILLED", + # "timeInForce": "GTC", + # "type": "MARKET", + # "side": "BUY", + # "stopPrice": "0", + # "time":1678704250171, + # "updateTime":1678704250256, + # "isWorking":false, + # "origQuoteOrderQty": "50" + # } + # + # cancelOrder DELETE /openapi/v1/order + # { + # "symbol": "ETHPHP", + # "orderId":1375609441915774332, + # "clientOrderId": "1375609441915899557", + # "price": "96000", + # "origQty": "0.001", + # "executedQty": "0", + # "cummulativeQuoteQty": "0", + # "status": "CANCELED", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "side": "SELL", + # "stopPrice": "0", + # "origQuoteOrderQty": "0" + # } + # + id = self.safe_string(order, 'orderId') + marketId = self.safe_string(order, 'symbol') + market = self.safe_market(marketId, market) + timestamp = self.safe_integer_2(order, 'time', 'transactTime') + trades = self.safe_value(order, 'fills', None) + triggerPrice = self.safe_string(order, 'stopPrice') + if Precise.string_eq(triggerPrice, '0'): + triggerPrice = None + return self.safe_order({ + 'id': id, + 'clientOrderId': self.safe_string(order, 'clientOrderId'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'status': self.parse_order_status(self.safe_string(order, 'status')), + 'symbol': market['symbol'], + 'type': self.parse_order_type(self.safe_string(order, 'type')), + 'timeInForce': self.parse_order_time_in_force(self.safe_string(order, 'timeInForce')), + 'side': self.parse_order_side(self.safe_string(order, 'side')), + 'price': self.safe_string(order, 'price'), + 'triggerPrice': triggerPrice, + 'average': None, + 'amount': self.safe_string(order, 'origQty'), + 'cost': self.safe_string(order, 'cummulativeQuoteQty'), + 'filled': self.safe_string(order, 'executedQty'), + 'remaining': None, + 'fee': None, + 'fees': None, + 'trades': trades, + 'info': order, + }, market) + + def parse_order_side(self, status): + statuses: dict = { + 'BUY': 'buy', + 'SELL': 'sell', + } + return self.safe_string(statuses, status, status) + + def encode_order_side(self, status): + statuses: dict = { + 'buy': 'BUY', + 'sell': 'SELL', + } + return self.safe_string(statuses, status, status) + + def parse_order_type(self, status): + statuses: dict = { + 'MARKET': 'market', + 'LIMIT': 'limit', + 'LIMIT_MAKER': 'limit', + 'STOP_LOSS': 'market', + 'STOP_LOSS_LIMIT': 'limit', + 'TAKE_PROFIT': 'market', + 'TAKE_PROFIT_LIMIT': 'limit', + } + return self.safe_string(statuses, status, status) + + def encode_order_type(self, status): + statuses: dict = { + 'market': 'MARKET', + 'limit': 'LIMIT', + 'limit_maker': 'LIMIT_MAKER', + 'stop_loss': 'STOP_LOSS', + 'stop_loss_limit': 'STOP_LOSS_LIMIT', + 'take_profit': 'TAKE_PROFIT', + 'take_profit_limit': 'TAKE_PROFIT_LIMIT', + } + return self.safe_string(statuses, status, status) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'NEW': 'open', + 'FILLED': 'closed', + 'CANCELED': 'canceled', + 'PARTIALLY_FILLED': 'open', + 'PARTIALLY_CANCELED': 'canceled', + 'REJECTED': 'rejected', + } + return self.safe_string(statuses, status, status) + + def parse_order_time_in_force(self, status): + statuses: dict = { + 'GTC': 'GTC', + 'FOK': 'FOK', + 'IOC': 'IOC', + } + return self.safe_string(statuses, status, status) + + async def fetch_trading_fee(self, symbol: str, params={}) -> TradingFeeInterface: + """ + fetch the trading fees for a market + + https://coins-docs.github.io/rest-api/#trade-fee-user_data + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `fee structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = await self.privateGetOpenapiV1AssetTradeFee(self.extend(request, params)) + # + # [ + # { + # "symbol": "ETHUSDT", + # "makerCommission": "0.0025", + # "takerCommission": "0.003" + # } + # ] + # + tradingFee = self.safe_dict(response, 0, {}) + return self.parse_trading_fee(tradingFee, market) + + async def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://coins-docs.github.io/rest-api/#trade-fee-user_data + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + await self.load_markets() + response = await self.privateGetOpenapiV1AssetTradeFee(params) + # + # [ + # { + # "symbol": "ETHPHP", + # "makerCommission": "0.0025", + # "takerCommission": "0.003" + # }, + # { + # "symbol": "UNIPHP", + # "makerCommission": "0.0025", + # "takerCommission": "0.003" + # }, + # ] + # + result: dict = {} + for i in range(0, len(response)): + fee = self.parse_trading_fee(response[i]) + symbol = fee['symbol'] + result[symbol] = fee + return result + + def parse_trading_fee(self, fee: dict, market: Market = None) -> TradingFeeInterface: + # + # { + # "symbol": "ETHUSDT", + # "makerCommission": "0.0025", + # "takerCommission": "0.003" + # } + # + marketId = self.safe_string(fee, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + return { + 'info': fee, + 'symbol': symbol, + 'maker': self.safe_number(fee, 'makerCommission'), + 'taker': self.safe_number(fee, 'takerCommission'), + 'percentage': None, + 'tierBased': None, + } + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal to coins_ph account + + https://coins-docs.github.io/rest-api/#withdrawuser_data + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: not used by coinsph withdraw() + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + options = self.safe_value(self.options, 'withdraw') + warning = self.safe_bool(options, 'warning', True) + if warning: + raise InvalidAddress(self.id + " withdraw() makes a withdrawals only to coins_ph account, add .options['withdraw']['warning'] = False to make a withdrawal to your coins_ph account") + networkCode = self.safe_string(params, 'network') + networkId = self.network_code_to_id(networkCode, code) + if networkId is None: + raise BadRequest(self.id + ' withdraw() require network parameter') + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'coin': currency['id'], + 'amount': self.number_to_string(amount), + 'network': networkId, + 'address': address, + } + if tag is not None: + request['withdrawOrderId'] = tag + params = self.omit(params, 'network') + response = await self.privatePostOpenapiWalletV1WithdrawApply(self.extend(request, params)) + return self.parse_transaction(response, currency) + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://coins-docs.github.io/rest-api/#deposit-history-user_data + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + # todo: returns an empty array - find out why + await self.load_markets() + currency = None + request: dict = {} + if code is not None: + currency = self.currency(code) + request['coin'] = currency['id'] + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + response = await self.privateGetOpenapiWalletV1DepositHistory(self.extend(request, params)) + # + # [ + # { + # "id": "d_769800519366885376", + # "amount": "0.001", + # "coin": "BNB", + # "network": "BNB", + # "status": 0, + # "address": "bnb136ns6lfw4zs5hg4n85vdthaad7hq5m4gtkgf23", + # "addressTag": "101764890", + # "txId": "98A3EA560C6B3336D348B6C83F0F95ECE4F1F5919E94BD006E5BF3BF264FACFC", + # "insertTime": 1661493146000, + # "confirmNo": 10, + # }, + # { + # "id": "d_769754833590042625", + # "amount":"0.5", + # "coin":"IOTA", + # "network":"IOTA", + # "status":1, + # "address":"SIZ9VLMHWATXKV99LH99CIGFJFUMLEHGWVZVNNZXRJJVWBPHYWPPBOSDORZ9EQSHCZAMPVAPGFYQAUUV9DROOXJLNW", + # "addressTag":"", + # "txId":"ESBFVQUTPIWQNJSPXFNHNYHSQNTGKRVKPRABQWTAXCDWOAKDKYWPTVG9BGXNVNKTLEJGESAVXIKIZ9999", + # "insertTime":1599620082000, + # "confirmNo": 20, + # } + # ] + # + return self.parse_transactions(response, currency, since, limit) + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://coins-docs.github.io/rest-api/#withdraw-history-user_data + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + # todo: returns an empty array - find out why + await self.load_markets() + currency = None + request: dict = {} + if code is not None: + currency = self.currency(code) + request['coin'] = currency['id'] + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + response = await self.privateGetOpenapiWalletV1WithdrawHistory(self.extend(request, params)) + # + # [ + # { + # "id": "459890698271244288", + # "amount": "0.01", + # "transactionFee": "0", + # "coin": "ETH", + # "status": 1, + # "address": "0x386AE30AE2dA293987B5d51ddD03AEb70b21001F", + # "addressTag": "", + # "txId": "0x4ae2fed36a90aada978fc31c38488e8b60d7435cfe0b4daed842456b4771fcf7", + # "applyTime": 1673601139000, + # "network": "ETH", + # "withdrawOrderId": "thomas123", + # "info": "", + # "confirmNo": 100 + # }, + # { + # "id": "451899190746456064", + # "amount": "0.00063", + # "transactionFee": "0.00037", + # "coin": "ETH", + # "status": 1, + # "address": "0x386AE30AE2dA293987B5d51ddD03AEb70b21001F", + # "addressTag": "", + # "txId": "0x62690ca4f9d6a8868c258e2ce613805af614d9354dda7b39779c57b2e4da0260", + # "applyTime": 1671695815000, + # "network": "ETH", + # "withdrawOrderId": "", + # "info": "", + # "confirmNo": 100 + # } + # ] + # + return self.parse_transactions(response, currency, since, limit) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fetchDeposits + # { + # "coin": "PHP", + # "address": "Internal Transfer", + # "addressTag": "Internal Transfer", + # "amount": "0.02", + # "id": "31312321312312312312322", + # "network": "Internal", + # "transferType": "0", + # "status": 3, + # "confirmTimes": "", + # "unlockConfirm": "", + # "txId": "Internal Transfer", + # "insertTime": 1657623798000, + # "depositOrderId": "the deposit id which created by client" + # } + # + # fetchWithdrawals + # { + # "coin": "BTC", + # "address": "Internal Transfer", + # "amount": "0.1", + # "id": "1201515362324421632", + # "withdrawOrderId": null, + # "network": "Internal", + # "transferType": "0", + # "status": 0, + # "transactionFee": "0", + # "confirmNo": 0, + # "info": "{}", + # "txId": "Internal Transfer", + # "applyTime": 1657967792000 + # } + # + # todo: self is in progress + id = self.safe_string(transaction, 'id') + address = self.safe_string(transaction, 'address') + tag = self.safe_string(transaction, 'addressTag') + if tag is not None: + if len(tag) < 1: + tag = None + txid = self.safe_string(transaction, 'txId') + currencyId = self.safe_string(transaction, 'coin') + code = self.safe_currency_code(currencyId, currency) + timestamp = None + timestamp = self.safe_integer_2(transaction, 'insertTime', 'applyTime') + updated = None + type = None + withdrawOrderId = self.safe_string(transaction, 'withdrawOrderId') + depositOrderId = self.safe_string(transaction, 'depositOrderId') + if withdrawOrderId is not None: + type = 'withdrawal' + elif depositOrderId is not None: + type = 'deposit' + status = self.parse_transaction_status(self.safe_string(transaction, 'status')) + amount = self.safe_number(transaction, 'amount') + feeCost = self.safe_number(transaction, 'transactionFee') + fee = None + if feeCost is not None: + fee = {'currency': code, 'cost': feeCost} + network = self.safe_string(transaction, 'network') + internal = network == 'Internal' + return { + 'info': transaction, + 'id': id, + 'txid': txid, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': network, + 'address': address, + 'addressTo': address, + 'addressFrom': None, + 'tag': tag, + 'tagTo': tag, + 'tagFrom': None, + 'type': type, + 'amount': amount, + 'currency': code, + 'status': status, + 'updated': updated, + 'internal': internal, + 'comment': None, + 'fee': fee, + } + + def parse_transaction_status(self, status: Str): + statuses: dict = { + '0': 'pending', + '1': 'ok', + '2': 'failed', + '3': 'pending', + } + return self.safe_string(statuses, status, status) + + async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://coins-docs.github.io/rest-api/#deposit-address-user_data + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.network]: network for fetch deposit address + :returns dict: an `address structure ` + """ + networkCode = self.safe_string(params, 'network') + networkId = self.network_code_to_id(networkCode, code) + if networkId is None: + raise BadRequest(self.id + ' fetchDepositAddress() require network parameter') + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'coin': currency['id'], + 'network': networkId, + } + params = self.omit(params, 'network') + response = await self.privateGetOpenapiWalletV1DepositAddress(self.extend(request, params)) + # + # { + # "coin": "ETH", + # "address": "0xfe98628173830bf79c59f04585ce41f7de168784", + # "addressTag": "" + # } + # + return self.parse_deposit_address(response, currency) + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + # + # { + # "coin": "ETH", + # "address": "0xfe98628173830bf79c59f04585ce41f7de168784", + # "addressTag": "" + # } + # + currencyId = self.safe_string(depositAddress, 'coin') + parsedCurrency = self.safe_currency_code(currencyId, currency) + return { + 'info': depositAddress, + 'currency': parsedCurrency, + 'network': None, + 'address': self.safe_string(depositAddress, 'address'), + 'tag': self.safe_string(depositAddress, 'addressTag'), + } + + def url_encode_query(self, query={}): + encodedArrayParams = '' + keys = list(query.keys()) + for i in range(0, len(keys)): + key = keys[i] + if isinstance(query[key], list): + if i != 0: + encodedArrayParams += '&' + innerArray = query[key] + query = self.omit(query, key) + encodedArrayParam = self.parse_array_param(innerArray, key) + encodedArrayParams += encodedArrayParam + encodedQuery = self.urlencode(query) + if len(encodedQuery) != 0: + return encodedQuery + '&' + encodedArrayParams + else: + return encodedArrayParams + + def parse_array_param(self, array, key): + stringifiedArray = self.json(array) + stringifiedArray = stringifiedArray.replace('[', '%5B') + stringifiedArray = stringifiedArray.replace(']', '%5D') + urlEncodedParam = key + '=' + stringifiedArray + return urlEncodedParam + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = self.urls['api'][api] + query = self.omit(params, self.extract_params(path)) + endpoint = self.implode_params(path, params) + url = url + '/' + endpoint + if api == 'private': + self.check_required_credentials() + query['timestamp'] = self.milliseconds() + recvWindow = self.safe_integer(query, 'recvWindow') + if recvWindow is None: + defaultRecvWindow = self.safe_integer(self.options, 'recvWindow') + if defaultRecvWindow is not None: + query['recvWindow'] = defaultRecvWindow + query = self.url_encode_query(query) + signature = self.hmac(self.encode(query), self.encode(self.secret), hashlib.sha256) + url = url + '?' + query + '&signature=' + signature + headers = { + 'X-COINS-APIKEY': self.apiKey, + } + else: + query = self.url_encode_query(query) + if len(query) != 0: + url += '?' + query + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None + responseCode = self.safe_string(response, 'code', None) + if (responseCode is not None) and (responseCode != '200') and (responseCode != '0'): + feedback = self.id + ' ' + body + self.throw_broadly_matched_exception(self.exceptions['broad'], body, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], responseCode, feedback) + raise ExchangeError(feedback) + return None diff --git a/ccxt/async_support/coinspot.py b/ccxt/async_support/coinspot.py new file mode 100644 index 0000000..09124ee --- /dev/null +++ b/ccxt/async_support/coinspot.py @@ -0,0 +1,633 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.coinspot import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Int, Market, Num, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class coinspot(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(coinspot, self).describe(), { + 'id': 'coinspot', + 'name': 'CoinSpot', + 'countries': ['AU'], # Australia + 'rateLimit': 1000, + 'pro': False, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelOrder': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createMarketOrder': False, + 'createOrder': True, + 'createOrderWithTakeProfitAndStopLoss': False, + 'createOrderWithTakeProfitAndStopLossWs': False, + 'createPostOnlyOrder': False, + 'createReduceOnlyOrder': False, + 'createStopLimitOrder': False, + 'createStopMarketOrder': False, + 'createStopOrder': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkOHLCV': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrderBook': True, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': False, + 'fetchVolatilityHistory': False, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'repayMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'ws': False, + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/28208429-3cacdf9a-6896-11e7-854e-4c79a772a30f.jpg', + 'api': { + 'public': 'https://www.coinspot.com.au/pubapi', + 'private': 'https://www.coinspot.com.au/api', + }, + 'www': 'https://www.coinspot.com.au', + 'doc': 'https://www.coinspot.com.au/api', + 'referral': 'https://www.coinspot.com.au/register?code=PJURCU', + }, + 'api': { + 'public': { + 'get': [ + 'latest', + ], + }, + 'private': { + 'post': [ + 'orders', + 'orders/history', + 'my/coin/deposit', + 'my/coin/send', + 'quote/buy', + 'quote/sell', + 'my/balances', + 'my/orders', + 'my/buy', + 'my/sell', + 'my/buy/cancel', + 'my/sell/cancel', + 'ro/my/balances', + 'ro/my/balances/{cointype}', + 'ro/my/deposits', + 'ro/my/withdrawals', + 'ro/my/transactions', + 'ro/my/transactions/{cointype}', + 'ro/my/transactions/open', + 'ro/my/transactions/{cointype}/open', + 'ro/my/sendreceive', + 'ro/my/affiliatepayments', + 'ro/my/referralpayments', + ], + }, + }, + 'markets': { + 'ADA/AUD': self.safe_market_structure({'id': 'ada', 'symbol': 'ADA/AUD', 'base': 'ADA', 'quote': 'AUD', 'baseId': 'ada', 'quoteId': 'aud', 'type': 'spot', 'spot': True}), + 'BTC/AUD': self.safe_market_structure({'id': 'btc', 'symbol': 'BTC/AUD', 'base': 'BTC', 'quote': 'AUD', 'baseId': 'btc', 'quoteId': 'aud', 'type': 'spot', 'spot': True}), + 'ETH/AUD': self.safe_market_structure({'id': 'eth', 'symbol': 'ETH/AUD', 'base': 'ETH', 'quote': 'AUD', 'baseId': 'eth', 'quoteId': 'aud', 'type': 'spot', 'spot': True}), + 'XRP/AUD': self.safe_market_structure({'id': 'xrp', 'symbol': 'XRP/AUD', 'base': 'XRP', 'quote': 'AUD', 'baseId': 'xrp', 'quoteId': 'aud', 'type': 'spot', 'spot': True}), + 'LTC/AUD': self.safe_market_structure({'id': 'ltc', 'symbol': 'LTC/AUD', 'base': 'LTC', 'quote': 'AUD', 'baseId': 'ltc', 'quoteId': 'aud', 'type': 'spot', 'spot': True}), + 'DOGE/AUD': self.safe_market_structure({'id': 'doge', 'symbol': 'DOGE/AUD', 'base': 'DOGE', 'quote': 'AUD', 'baseId': 'doge', 'quoteId': 'aud', 'type': 'spot', 'spot': True}), + 'RFOX/AUD': self.safe_market_structure({'id': 'rfox', 'symbol': 'RFOX/AUD', 'base': 'RFOX', 'quote': 'AUD', 'baseId': 'rfox', 'quoteId': 'aud', 'type': 'spot', 'spot': True}), + 'POWR/AUD': self.safe_market_structure({'id': 'powr', 'symbol': 'POWR/AUD', 'base': 'POWR', 'quote': 'AUD', 'baseId': 'powr', 'quoteId': 'aud', 'type': 'spot', 'spot': True}), + 'NEO/AUD': self.safe_market_structure({'id': 'neo', 'symbol': 'NEO/AUD', 'base': 'NEO', 'quote': 'AUD', 'baseId': 'neo', 'quoteId': 'aud', 'type': 'spot', 'spot': True}), + 'TRX/AUD': self.safe_market_structure({'id': 'trx', 'symbol': 'TRX/AUD', 'base': 'TRX', 'quote': 'AUD', 'baseId': 'trx', 'quoteId': 'aud', 'type': 'spot', 'spot': True}), + 'EOS/AUD': self.safe_market_structure({'id': 'eos', 'symbol': 'EOS/AUD', 'base': 'EOS', 'quote': 'AUD', 'baseId': 'eos', 'quoteId': 'aud', 'type': 'spot', 'spot': True}), + 'XLM/AUD': self.safe_market_structure({'id': 'xlm', 'symbol': 'XLM/AUD', 'base': 'XLM', 'quote': 'AUD', 'baseId': 'xlm', 'quoteId': 'aud', 'type': 'spot', 'spot': True}), + 'RHOC/AUD': self.safe_market_structure({'id': 'rhoc', 'symbol': 'RHOC/AUD', 'base': 'RHOC', 'quote': 'AUD', 'baseId': 'rhoc', 'quoteId': 'aud', 'type': 'spot', 'spot': True}), + 'GAS/AUD': self.safe_market_structure({'id': 'gas', 'symbol': 'GAS/AUD', 'base': 'GAS', 'quote': 'AUD', 'baseId': 'gas', 'quoteId': 'aud', 'type': 'spot', 'spot': True}), + }, + 'commonCurrencies': { + 'DRK': 'DASH', + }, + 'options': { + 'fetchBalance': 'private_post_my_balances', + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': False, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': False, + 'FOK': False, + 'PO': False, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': False, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': None, + 'daysBack': 100000, + 'untilDays': 100000, # todo implement + 'symbolRequired': False, + }, + 'fetchOrder': None, + 'fetchOpenOrders': None, # todo implement + 'fetchOrders': None, + 'fetchClosedOrders': None, # todo implement + 'fetchOHLCV': None, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'precisionMode': TICK_SIZE, + }) + + def parse_balance(self, response) -> Balances: + result: dict = {'info': response} + balances = self.safe_value_2(response, 'balance', 'balances') + if isinstance(balances, list): + for i in range(0, len(balances)): + currencies = balances[i] + currencyIds = list(currencies.keys()) + for j in range(0, len(currencyIds)): + currencyId = currencyIds[j] + balance = currencies[currencyId] + code = self.safe_currency_code(currencyId) + account = self.account() + account['total'] = self.safe_string(balance, 'balance') + result[code] = account + else: + currencyIds = list(balances.keys()) + for i in range(0, len(currencyIds)): + currencyId = currencyIds[i] + code = self.safe_currency_code(currencyId) + account = self.account() + account['total'] = self.safe_string(balances, currencyId) + result[code] = account + return self.safe_balance(result) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://www.coinspot.com.au/api#listmybalance + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + method = self.safe_string(self.options, 'fetchBalance', 'private_post_my_balances') + response = await getattr(self, method)(params) + # + # read-write api keys + # + # ... + # + # read-only api keys + # + # { + # "status":"ok", + # "balances":[ + # { + # "LTC":{"balance":0.1,"audbalance":16.59,"rate":165.95} + # } + # ] + # } + # + return self.parse_balance(response) + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://www.coinspot.com.au/api#listopenorders + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'cointype': market['id'], + } + orderbook = await self.privatePostOrders(self.extend(request, params)) + return self.parse_order_book(orderbook, market['symbol'], None, 'buyorders', 'sellorders', 'rate', 'amount') + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "btc":{ + # "bid":"51970", + # "ask":"53000", + # "last":"52806.47" + # } + # } + # + symbol = self.safe_symbol(None, market) + last = self.safe_string(ticker, 'last') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': None, + 'datetime': None, + 'high': None, + 'low': None, + 'bid': self.safe_string(ticker, 'bid'), + 'bidVolume': None, + 'ask': self.safe_string(ticker, 'ask'), + 'askVolume': None, + 'vwap': None, + 'open': None, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': None, + 'quoteVolume': None, + 'info': ticker, + }, market) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://www.coinspot.com.au/api#latestprices + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + response = await self.publicGetLatest(params) + id = market['id'] + id = id.lower() + prices = self.safe_dict(response, 'prices', {}) + # + # { + # "status":"ok", + # "prices":{ + # "btc":{ + # "bid":"52732.47000022", + # "ask":"53268.0699976", + # "last":"53284.03" + # } + # } + # } + # + ticker = self.safe_dict(prices, id) + return self.parse_ticker(ticker, market) + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://www.coinspot.com.au/api#latestprices + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + response = await self.publicGetLatest(params) + # + # { + # "status": "ok", + # "prices": { + # "btc": { + # "bid": "25050", + # "ask": "25370", + # "last": "25234" + # }, + # "ltc": { + # "bid": "79.39192993", + # "ask": "87.98", + # "last": "87.95" + # } + # } + # } + # + result: dict = {} + prices = self.safe_dict(response, 'prices', {}) + ids = list(prices.keys()) + for i in range(0, len(ids)): + id = ids[i] + market = self.safe_market(id) + if market['spot']: + symbol = market['symbol'] + ticker = prices[id] + result[symbol] = self.parse_ticker(ticker, market) + return self.filter_by_array_tickers(result, 'symbol', symbols) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://www.coinspot.com.au/api#orderhistory + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'cointype': market['id'], + } + response = await self.privatePostOrdersHistory(self.extend(request, params)) + # + # { + # "status":"ok", + # "orders":[ + # {"amount":0.00102091,"rate":21549.09999991,"total":21.99969168,"coin":"BTC","solddate":1604890646143,"market":"BTC/AUD"}, + # ], + # } + # + trades = self.safe_list(response, 'orders', []) + return self.parse_trades(trades, market, since, limit) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://www.coinspot.com.au/api#rotransaction + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + if since is not None: + request['startdate'] = self.yyyymmdd(since) + response = await self.privatePostRoMyTransactions(self.extend(request, params)) + # { + # "status": "ok", + # "buyorders": [ + # { + # "otc": False, + # "market": "ALGO/AUD", + # "amount": 386.95197925, + # "created": "2022-10-20T09:56:44.502Z", + # "audfeeExGst": 1.80018002, + # "audGst": 0.180018, + # "audtotal": 200 + # }, + # ], + # "sellorders": [ + # { + # "otc": False, + # "market": "SOLO/ALGO", + # "amount": 154.52345614, + # "total": 115.78858204658796, + # "created": "2022-04-16T09:36:43.698Z", + # "audfeeExGst": 1.08995731, + # "audGst": 0.10899573, + # "audtotal": 118.7 + # }, + # ] + # } + buyTrades = self.safe_list(response, 'buyorders', []) + for i in range(0, len(buyTrades)): + buyTrades[i]['side'] = 'buy' + sellTrades = self.safe_list(response, 'sellorders', []) + for i in range(0, len(sellTrades)): + sellTrades[i]['side'] = 'sell' + trades = self.array_concat(buyTrades, sellTrades) + return self.parse_trades(trades, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # public fetchTrades + # + # { + # "amount":0.00102091, + # "rate":21549.09999991, + # "total":21.99969168, + # "coin":"BTC", + # "solddate":1604890646143, + # "market":"BTC/AUD" + # } + # + # private fetchMyTrades + # { + # "otc": False, + # "market": "ALGO/AUD", + # "amount": 386.95197925, + # "created": "2022-10-20T09:56:44.502Z", + # "audfeeExGst": 1.80018002, + # "audGst": 0.180018, + # "audtotal": 200, + # "total": 200, + # "side": "buy", + # "price": 0.5168600000125209 + # } + timestamp = None + priceString = None + fee = None + audTotal = self.safe_string(trade, 'audtotal') + costString = self.safe_string(trade, 'total', audTotal) + side = self.safe_string(trade, 'side') + amountString = self.safe_string(trade, 'amount') + marketId = self.safe_string(trade, 'market') + symbol = self.safe_symbol(marketId, market, '/') + solddate = self.safe_integer(trade, 'solddate') + if solddate is not None: + priceString = self.safe_string(trade, 'rate') + timestamp = solddate + else: + priceString = Precise.string_div(costString, amountString) + createdString = self.safe_string(trade, 'created') + timestamp = self.parse8601(createdString) + audfeeExGst = self.safe_string(trade, 'audfeeExGst') + audGst = self.safe_string(trade, 'audGst') + # The transaction fee which consumers pay is inclusive of GST by default + feeCost = Precise.string_add(audfeeExGst, audGst) + feeCurrencyId = 'AUD' + fee = { + 'cost': self.parse_number(feeCost), + 'currency': self.safe_currency_code(feeCurrencyId), + } + return self.safe_trade({ + 'info': trade, + 'id': None, + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'order': None, + 'type': None, + 'side': side, + 'takerOrMaker': None, + 'price': self.parse_number(priceString), + 'amount': self.parse_number(amountString), + 'cost': self.parse_number(costString), + 'fee': fee, + }, market) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://www.coinspot.com.au/api#placebuyorder + + :param str symbol: unified symbol of the market to create an order in + :param str type: must be 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + method = 'privatePostMy' + self.capitalize(side) + if type == 'market': + raise ExchangeError(self.id + ' createOrder() allows limit orders only') + market = self.market(symbol) + request: dict = { + 'cointype': market['id'], + 'amount': amount, + 'rate': price, + } + response = await getattr(self, method)(self.extend(request, params)) + return self.parse_order(response) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://www.coinspot.com.au/api#cancelbuyorder + https://www.coinspot.com.au/api#cancelsellorder + + :param str id: order id + :param str symbol: not used by coinspot cancelOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + side = self.safe_string(params, 'side') + if side != 'buy' and side != 'sell': + raise ArgumentsRequired(self.id + ' cancelOrder() requires a side parameter, "buy" or "sell"') + params = self.omit(params, 'side') + request: dict = { + 'id': id, + } + response = None + if side == 'buy': + response = await self.privatePostMyBuyCancel(self.extend(request, params)) + else: + response = await self.privatePostMySellCancel(self.extend(request, params)) + # + # status - ok, error + # + return self.safe_order({ + 'info': response, + }) + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = self.urls['api'][api] + '/' + path + if api == 'private': + self.check_required_credentials() + nonce = self.nonce() + body = self.json(self.extend({'nonce': nonce}, params)) + headers = { + 'Content-Type': 'application/json', + 'key': self.apiKey, + 'sign': self.hmac(self.encode(body), self.encode(self.secret), hashlib.sha512), + } + return {'url': url, 'method': method, 'body': body, 'headers': headers} diff --git a/ccxt/async_support/cryptocom.py b/ccxt/async_support/cryptocom.py new file mode 100644 index 0000000..fa2d1e0 --- /dev/null +++ b/ccxt/async_support/cryptocom.py @@ -0,0 +1,3379 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.cryptocom import ImplicitAPI +import hashlib +import math +from ccxt.base.types import Account, Any, Balances, Currencies, Currency, DepositAddress, Int, LedgerEntry, Market, Num, Order, OrderBook, OrderRequest, CancellationRequest, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, Trade, TradingFeeInterface, TradingFees, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import AccountNotEnabled +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import NotSupported +from ccxt.base.errors import DDoSProtection +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import OnMaintenance +from ccxt.base.errors import InvalidNonce +from ccxt.base.errors import RequestTimeout +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class cryptocom(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(cryptocom, self).describe(), { + 'id': 'cryptocom', + 'name': 'Crypto.com', + 'countries': ['MT'], + 'version': 'v2', + 'rateLimit': 10, # 100 requests per second + 'certified': True, + 'pro': True, + 'has': { + 'CORS': False, + 'spot': True, + 'margin': True, + 'swap': True, + 'future': True, + 'option': True, + 'addMargin': False, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'cancelOrders': True, + 'cancelOrdersForSymbols': True, + 'closeAllPositions': False, + 'closePosition': True, + 'createMarketBuyOrderWithCost': False, + 'createMarketOrderWithCost': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createOrders': True, + 'createStopOrder': True, + 'createTriggerOrder': True, + 'editOrder': True, + 'fetchAccounts': True, + 'fetchBalance': True, + 'fetchBidsAsks': False, + 'fetchBorrowInterest': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchClosedOrders': 'emulated', + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': True, + 'fetchDeposits': True, + 'fetchDepositsWithdrawals': False, + 'fetchDepositWithdrawFee': 'emulated', + 'fetchDepositWithdrawFees': True, + 'fetchFundingHistory': False, + 'fetchFundingRate': True, + 'fetchFundingRateHistory': True, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchLedger': True, + 'fetchLeverage': False, + 'fetchLeverageTiers': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchPosition': True, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': True, + 'fetchPositionsHistory': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': True, + 'fetchStatus': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': False, + 'fetchTrades': True, + 'fetchTradingFee': True, + 'fetchTradingFees': True, + 'fetchTransactionFees': False, + 'fetchTransactions': False, + 'fetchTransfers': False, + 'fetchUnderlyingAssets': False, + 'fetchVolatilityHistory': False, + 'fetchWithdrawals': True, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'sandbox': True, + 'setLeverage': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'transfer': False, + 'withdraw': True, + }, + 'timeframes': { + '1m': '1m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1h', + '4h': '4h', + '6h': '6h', + '12h': '12h', + '1d': '1D', + '1w': '7D', + '2w': '14D', + '1M': '1M', + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/147792121-38ed5e36-c229-48d6-b49a-48d05fc19ed4.jpeg', + 'test': { + 'v1': 'https://uat-api.3ona.co/exchange/v1', + 'v2': 'https://uat-api.3ona.co/v2', + 'derivatives': 'https://uat-api.3ona.co/v2', + }, + 'api': { + 'base': 'https://api.crypto.com', + 'v1': 'https://api.crypto.com/exchange/v1', + 'v2': 'https://api.crypto.com/v2', + 'derivatives': 'https://deriv-api.crypto.com/v1', + }, + 'www': 'https://crypto.com/', + 'referral': { + 'url': 'https://crypto.com/exch/kdacthrnxt', + 'discount': 0.75, + }, + 'doc': [ + 'https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html', + 'https://exchange-docs.crypto.com/spot/index.html', + 'https://exchange-docs.crypto.com/derivatives/index.html', + ], + 'fees': 'https://crypto.com/exchange/document/fees-limits', + }, + 'api': { + 'base': { + 'public': { + 'get': { + 'v1/public/get-announcements': 1, # no description of rate limit + }, + }, + }, + 'v1': { + 'public': { + 'get': { + 'public/auth': 10 / 3, + 'public/get-instruments': 10 / 3, + 'public/get-book': 1, + 'public/get-candlestick': 1, + 'public/get-trades': 1, + 'public/get-tickers': 1, + 'public/get-valuations': 1, + 'public/get-expired-settlement-price': 10 / 3, + 'public/get-insurance': 1, + 'public/get-risk-parameters': 1, + }, + 'post': { + 'public/staking/get-conversion-rate': 2, + }, + }, + 'private': { + 'post': { + 'private/set-cancel-on-disconnect': 10 / 3, + 'private/get-cancel-on-disconnect': 10 / 3, + 'private/user-balance': 10 / 3, + 'private/user-balance-history': 10 / 3, + 'private/get-positions': 10 / 3, + 'private/create-order': 2 / 3, + 'private/amend-order': 4 / 3, # no description of rate limit + 'private/create-order-list': 10 / 3, + 'private/cancel-order': 2 / 3, + 'private/cancel-order-list': 10 / 3, + 'private/cancel-all-orders': 2 / 3, + 'private/close-position': 10 / 3, + 'private/get-order-history': 100, + 'private/get-open-orders': 10 / 3, + 'private/get-order-detail': 1 / 3, + 'private/get-trades': 100, + 'private/change-account-leverage': 10 / 3, + 'private/get-transactions': 10 / 3, + 'private/create-subaccount-transfer': 10 / 3, + 'private/get-subaccount-balances': 10 / 3, + 'private/get-order-list': 10 / 3, + 'private/create-withdrawal': 10 / 3, + 'private/get-currency-networks': 10 / 3, + 'private/get-deposit-address': 10 / 3, + 'private/get-accounts': 10 / 3, + 'private/get-withdrawal-history': 10 / 3, + 'private/get-deposit-history': 10 / 3, + 'private/get-fee-rate': 2, + 'private/get-instrument-fee-rate': 2, + 'private/staking/stake': 2, + 'private/staking/unstake': 2, + 'private/staking/get-staking-position': 2, + 'private/staking/get-staking-instruments': 2, + 'private/staking/get-open-stake': 2, + 'private/staking/get-stake-history': 2, + 'private/staking/get-reward-history': 2, + 'private/staking/convert': 2, + 'private/staking/get-open-convert': 2, + 'private/staking/get-convert-history': 2, + }, + }, + }, + 'v2': { + 'public': { + 'get': { + 'public/auth': 1, + 'public/get-instruments': 1, + 'public/get-book': 1, + 'public/get-candlestick': 1, + 'public/get-ticker': 1, + 'public/get-trades': 1, + 'public/margin/get-transfer-currencies': 1, + 'public/margin/get-load-currenices': 1, + 'public/respond-heartbeat': 1, + }, + }, + 'private': { + 'post': { + 'private/set-cancel-on-disconnect': 10 / 3, + 'private/get-cancel-on-disconnect': 10 / 3, + 'private/create-withdrawal': 10 / 3, + 'private/get-withdrawal-history': 10 / 3, + 'private/get-currency-networks': 10 / 3, + 'private/get-deposit-history': 10 / 3, + 'private/get-deposit-address': 10 / 3, + 'private/export/create-export-request': 10 / 3, + 'private/export/get-export-requests': 10 / 3, + 'private/export/download-export-output': 10 / 3, + 'private/get-account-summary': 10 / 3, + 'private/create-order': 2 / 3, + 'private/cancel-order': 2 / 3, + 'private/cancel-all-orders': 2 / 3, + 'private/create-order-list': 10 / 3, + 'private/get-order-history': 10 / 3, + 'private/get-open-orders': 10 / 3, + 'private/get-order-detail': 1 / 3, + 'private/get-trades': 100, + 'private/get-accounts': 10 / 3, + 'private/get-subaccount-balances': 10 / 3, + 'private/create-subaccount-transfer': 10 / 3, + 'private/otc/get-otc-user': 10 / 3, + 'private/otc/get-instruments': 10 / 3, + 'private/otc/request-quote': 100, + 'private/otc/accept-quote': 100, + 'private/otc/get-quote-history': 10 / 3, + 'private/otc/get-trade-history': 10 / 3, + 'private/otc/create-order': 10 / 3, + }, + }, + }, + 'derivatives': { + 'public': { + 'get': { + 'public/auth': 10 / 3, + 'public/get-instruments': 10 / 3, + 'public/get-book': 1, + 'public/get-candlestick': 1, + 'public/get-trades': 1, + 'public/get-tickers': 1, + 'public/get-valuations': 1, + 'public/get-expired-settlement-price': 10 / 3, + 'public/get-insurance': 1, + }, + }, + 'private': { + 'post': { + 'private/set-cancel-on-disconnect': 10 / 3, + 'private/get-cancel-on-disconnect': 10 / 3, + 'private/user-balance': 10 / 3, + 'private/user-balance-history': 10 / 3, + 'private/get-positions': 10 / 3, + 'private/create-order': 2 / 3, + 'private/create-order-list': 10 / 3, + 'private/cancel-order': 2 / 3, + 'private/cancel-order-list': 10 / 3, + 'private/cancel-all-orders': 2 / 3, + 'private/close-position': 10 / 3, + 'private/convert-collateral': 10 / 3, + 'private/get-order-history': 100, + 'private/get-open-orders': 10 / 3, + 'private/get-order-detail': 1 / 3, + 'private/get-trades': 100, + 'private/change-account-leverage': 10 / 3, + 'private/get-transactions': 10 / 3, + 'private/create-subaccount-transfer': 10 / 3, + 'private/get-subaccount-balances': 10 / 3, + 'private/get-order-list': 10 / 3, + }, + }, + }, + }, + 'fees': { + 'trading': { + 'maker': self.parse_number('0.0025'), + 'taker': self.parse_number('0.005'), + 'tiers': { + 'maker': [ + [self.parse_number('0'), self.parse_number('0.0025')], + [self.parse_number('10000'), self.parse_number('0.002')], + [self.parse_number('50000'), self.parse_number('0.0015')], + [self.parse_number('250000'), self.parse_number('0.001')], + [self.parse_number('500000'), self.parse_number('0.0008')], + [self.parse_number('2500000'), self.parse_number('0.00065')], + [self.parse_number('10000000'), self.parse_number('0')], + [self.parse_number('25000000'), self.parse_number('0')], + [self.parse_number('100000000'), self.parse_number('0')], + [self.parse_number('250000000'), self.parse_number('0')], + [self.parse_number('500000000'), self.parse_number('0')], + ], + 'taker': [ + [self.parse_number('0'), self.parse_number('0.005')], + [self.parse_number('10000'), self.parse_number('0.004')], + [self.parse_number('50000'), self.parse_number('0.0025')], + [self.parse_number('250000'), self.parse_number('0.002')], + [self.parse_number('500000'), self.parse_number('0.0018')], + [self.parse_number('2500000'), self.parse_number('0.001')], + [self.parse_number('10000000'), self.parse_number('0.0005')], + [self.parse_number('25000000'), self.parse_number('0.0004')], + [self.parse_number('100000000'), self.parse_number('0.00035')], + [self.parse_number('250000000'), self.parse_number('0.00031')], + [self.parse_number('500000000'), self.parse_number('0.00025')], + ], + }, + }, + }, + 'options': { + 'defaultType': 'spot', + 'accountsById': { + 'funding': 'SPOT', + 'spot': 'SPOT', + 'margin': 'MARGIN', + 'derivatives': 'DERIVATIVES', + 'swap': 'DERIVATIVES', + 'future': 'DERIVATIVES', + }, + 'networks': { + 'BEP20': 'BSC', + 'ERC20': 'ETH', + 'TRC20': 'TRON', + }, + 'broker': 'CCXT', + }, + 'features': { + 'default': { + 'sandbox': True, + 'createOrder': { + 'marginMode': True, + 'triggerPrice': True, + # todo: implementation fix + 'triggerPriceType': { + 'last': True, + 'mark': True, + 'index': True, + }, + 'triggerDirection': False, + 'stopLossPrice': True, + 'takeProfitPrice': True, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'selfTradePrevention': True, # todo: implement + 'trailing': False, + 'iceberg': False, + 'leverage': False, + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': True, + }, + 'createOrders': { + 'max': 10, + }, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, + 'daysBack': None, + 'untilDays': 1, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': True, + 'limit': 100, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': None, + 'untilDays': 1, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': None, + 'daysBackCanceled': None, + 'untilDays': 1, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 300, + }, + }, + 'spot': { + 'extends': 'default', + 'fetchCurrencies': { + 'private': True, + }, + }, + 'swap': { + 'linear': { + 'extends': 'default', + }, + 'inverse': { + 'extends': 'default', + }, + }, + 'future': { + 'linear': { + 'extends': 'default', + }, + 'inverse': { + 'extends': 'default', + }, + }, + }, + # https://exchange-docs.crypto.com/spot/index.html#response-and-reason-codes + 'commonCurrencies': { + 'USD_STABLE_COIN': 'USDC', + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'exact': { + '219': InvalidOrder, + '306': InsufficientFunds, # {"id" : 1753xxx, "method" : "private/amend-order", "code" : 306, "message" : "INSUFFICIENT_AVAILABLE_BALANCE", "result" : {"client_oid" : "1753xxx", "order_id" : "6530xxx"}} + '314': InvalidOrder, # {"id" : 1700xxx, "method" : "private/create-order", "code" : 314, "message" : "EXCEEDS_MAX_ORDER_SIZE", "result" : {"client_oid" : "1700xxx", "order_id" : "6530xxx"}} + '325': InvalidOrder, # {"id" : 1741xxx, "method" : "private/create-order", "code" : 325, "message" : "EXCEED_DAILY_VOL_LIMIT", "result" : {"client_oid" : "1741xxx", "order_id" : "6530xxx"}} + '415': InvalidOrder, # {"id" : 1741xxx, "method" : "private/create-order", "code" : 415, "message" : "BELOW_MIN_ORDER_SIZE", "result" : {"client_oid" : "1741xxx", "order_id" : "6530xxx"}} + '10001': ExchangeError, + '10002': PermissionDenied, + '10003': PermissionDenied, + '10004': BadRequest, + '10005': PermissionDenied, + '10006': DDoSProtection, + '10007': InvalidNonce, + '10008': BadRequest, + '10009': BadRequest, + '20001': BadRequest, + '20002': InsufficientFunds, + '20005': AccountNotEnabled, # {"id":"123xxx","method":"private/margin/xxx","code":"20005","message":"ACCOUNT_NOT_FOUND"} + '30003': BadSymbol, + '30004': BadRequest, + '30005': BadRequest, + '30006': InvalidOrder, + '30007': InvalidOrder, + '30008': InvalidOrder, + '30009': InvalidOrder, + '30010': BadRequest, + '30013': InvalidOrder, + '30014': InvalidOrder, + '30016': InvalidOrder, + '30017': InvalidOrder, + '30023': InvalidOrder, + '30024': InvalidOrder, + '30025': InvalidOrder, + '40001': BadRequest, + '40002': BadRequest, + '40003': BadRequest, + '40004': BadRequest, + '40005': BadRequest, + '40006': BadRequest, + '40007': BadRequest, + '40101': AuthenticationError, + '40102': InvalidNonce, # Nonce value differs by more than 60 seconds from server + '40103': AuthenticationError, # IP address not whitelisted + '40104': AuthenticationError, # Disallowed based on user tier + '40107': BadRequest, # Session subscription limit has been exceeded + '40401': OrderNotFound, + '40801': RequestTimeout, + '42901': RateLimitExceeded, + '43005': InvalidOrder, # Rejected POST_ONLY create-order request(normally happened when exec_inst contains POST_ONLY but time_in_force is NOT GOOD_TILL_CANCEL) + '43003': InvalidOrder, # FOK order has not been filled and cancelled + '43004': InvalidOrder, # IOC order has not been filled and cancelled + '43012': BadRequest, # Canceled due to Self Trade Prevention + '50001': ExchangeError, + '9010001': OnMaintenance, # {"code":9010001,"message":"SYSTEM_MAINTENANCE","details":"Crypto.com Exchange is currently under maintenance. Please refer to https://status.crypto.com for more details."} + }, + 'broad': {}, + }, + }) + + async def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#private-get-currency-networks + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + # self endpoint requires authentication + if not self.check_required_credentials(False): + return {} + skipFetchCurrencies = False + skipFetchCurrencies, params = self.handle_option_and_params(params, 'fetchCurrencies', 'skipFetchCurrencies', False) + if skipFetchCurrencies: + # sub-accounts can't access self endpoint + return {} + response = {} + try: + response = await self.v1PrivatePostPrivateGetCurrencyNetworks(params) + except Exception as e: + if isinstance(e, ExchangeError): + # sub-accounts can't access self endpoint + # {"code":"10001","msg":"SYS_ERROR"} + return {} + raise e + # do nothing + # sub-accounts can't access self endpoint + # + # { + # "id": "1747502328559", + # "method": "private/get-currency-networks", + # "code": "0", + # "result": { + # "update_time": "1747502281000", + # "currency_map": { + # "USDT": { + # "full_name": "Tether USD", + # "default_network": "ETH", + # "network_list": [ + # { + # "network_id": "ETH", + # "withdrawal_fee": "10.00000000", + # "withdraw_enabled": True, + # "min_withdrawal_amount": "20.0", + # "deposit_enabled": True, + # "confirmation_required": "32" + # }, + # { + # "network_id": "CRONOS", + # "withdrawal_fee": "0.18000000", + # "withdraw_enabled": True, + # "min_withdrawal_amount": "0.35", + # "deposit_enabled": True, + # "confirmation_required": "15" + # }, + # { + # "network_id": "SOL", + # "withdrawal_fee": "5.31000000", + # "withdraw_enabled": True, + # "min_withdrawal_amount": "10.62", + # "deposit_enabled": True, + # "confirmation_required": "1" + # } + # ] + # } + # } + # } + # } + # + resultData = self.safe_dict(response, 'result', {}) + currencyMap = self.safe_dict(resultData, 'currency_map', {}) + keys = list(currencyMap.keys()) + result: dict = {} + for i in range(0, len(keys)): + key = keys[i] + currency = currencyMap[key] + id = key + code = self.safe_currency_code(id) + networks: dict = {} + chains = self.safe_list(currency, 'network_list', []) + for j in range(0, len(chains)): + chain = chains[j] + networkId = self.safe_string(chain, 'network_id') + network = self.network_id_to_code(networkId) + networks[network] = { + 'info': chain, + 'id': networkId, + 'network': network, + 'active': None, + 'deposit': self.safe_bool(chain, 'deposit_enabled', False), + 'withdraw': self.safe_bool(chain, 'withdraw_enabled', False), + 'fee': self.safe_number(chain, 'withdrawal_fee'), + 'precision': None, + 'limits': { + 'withdraw': { + 'min': self.safe_number(chain, 'min_withdrawal_amount'), + 'max': None, + }, + }, + } + result[code] = self.safe_currency_structure({ + 'info': currency, + 'id': id, + 'code': code, + 'name': self.safe_string(currency, 'full_name'), + 'active': None, + 'deposit': None, + 'withdraw': None, + 'fee': None, + 'precision': None, + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + }, + 'type': 'crypto', # only crypto now + 'networks': networks, + }) + return result + + async def fetch_markets(self, params={}) -> List[Market]: + """ + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#public-get-instruments + + retrieves data on all markets for cryptocom + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = await self.v1PublicGetPublicGetInstruments(params) + # + # { + # "id": 1, + # "method": "public/get-instruments", + # "code": 0, + # "result": { + # "data": [ + # { + # "symbol": "BTC_USDT", + # "inst_type": "CCY_PAIR", + # "display_name": "BTC/USDT", + # "base_ccy": "BTC", + # "quote_ccy": "USDT", + # "quote_decimals": 2, + # "quantity_decimals": 5, + # "price_tick_size": "0.01", + # "qty_tick_size": "0.00001", + # "max_leverage": "50", + # "tradable": True, + # "expiry_timestamp_ms": 0, + # "beta_product": False, + # "margin_buy_enabled": False, + # "margin_sell_enabled": True + # }, + # { + # "symbol": "RUNEUSD-PERP", + # "inst_type": "PERPETUAL_SWAP", + # "display_name": "RUNEUSD Perpetual", + # "base_ccy": "RUNE", + # "quote_ccy": "USD", + # "quote_decimals": 3, + # "quantity_decimals": 1, + # "price_tick_size": "0.001", + # "qty_tick_size": "0.1", + # "max_leverage": "50", + # "tradable": True, + # "expiry_timestamp_ms": 0, + # "beta_product": False, + # "underlying_symbol": "RUNEUSD-INDEX", + # "contract_size": "1", + # "margin_buy_enabled": False, + # "margin_sell_enabled": False + # }, + # { + # "symbol": "ETHUSD-230825", + # "inst_type": "FUTURE", + # "display_name": "ETHUSD Futures 20230825", + # "base_ccy": "ETH", + # "quote_ccy": "USD", + # "quote_decimals": 2, + # "quantity_decimals": 4, + # "price_tick_size": "0.01", + # "qty_tick_size": "0.0001", + # "max_leverage": "100", + # "tradable": True, + # "expiry_timestamp_ms": 1692950400000, + # "beta_product": False, + # "underlying_symbol": "ETHUSD-INDEX", + # "contract_size": "1", + # "margin_buy_enabled": False, + # "margin_sell_enabled": False + # }, + # { + # "symbol": "BTCUSD-230630-CW30000", + # "inst_type": "WARRANT", + # "display_name": "BTCUSD-230630-CW30000", + # "base_ccy": "BTC", + # "quote_ccy": "USD", + # "quote_decimals": 3, + # "quantity_decimals": 0, + # "price_tick_size": "0.001", + # "qty_tick_size": "10", + # "max_leverage": "50", + # "tradable": True, + # "expiry_timestamp_ms": 1688112000000, + # "beta_product": False, + # "underlying_symbol": "BTCUSD-INDEX", + # "put_call": "CALL", + # "strike": "30000", + # "contract_size": "0.0001", + # "margin_buy_enabled": False, + # "margin_sell_enabled": False + # }, + # ] + # } + # } + # + resultResponse = self.safe_dict(response, 'result', {}) + data = self.safe_list(resultResponse, 'data', []) + result = [] + for i in range(0, len(data)): + market = data[i] + inst_type = self.safe_string(market, 'inst_type') + spot = inst_type == 'CCY_PAIR' + swap = inst_type == 'PERPETUAL_SWAP' + future = inst_type == 'FUTURE' + option = inst_type == 'WARRANT' + baseId = self.safe_string(market, 'base_ccy') + quoteId = self.safe_string(market, 'quote_ccy') + settleId = None if spot else quoteId + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + settle = None if spot else self.safe_currency_code(settleId) + optionType = self.safe_string_lower(market, 'put_call') + strike = self.safe_string(market, 'strike') + marginBuyEnabled = self.safe_bool(market, 'margin_buy_enabled') + marginSellEnabled = self.safe_bool(market, 'margin_sell_enabled') + expiryString = self.omit_zero(self.safe_string(market, 'expiry_timestamp_ms')) + expiry = int(expiryString) if (expiryString is not None) else None + symbol = base + '/' + quote + type = None + contract = None + if inst_type == 'CCY_PAIR': + type = 'spot' + contract = False + elif inst_type == 'PERPETUAL_SWAP': + type = 'swap' + symbol = symbol + ':' + quote + contract = True + elif inst_type == 'FUTURE': + type = 'future' + symbol = symbol + ':' + quote + '-' + self.yymmdd(expiry) + contract = True + elif inst_type == 'WARRANT': + type = 'option' + symbolOptionType = 'C' if (optionType == 'call') else 'P' + symbol = symbol + ':' + quote + '-' + self.yymmdd(expiry) + '-' + strike + '-' + symbolOptionType + contract = True + result.append({ + 'id': self.safe_string(market, 'symbol'), + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': type, + 'spot': spot, + 'margin': ((marginBuyEnabled) or (marginSellEnabled)), + 'swap': swap, + 'future': future, + 'option': option, + 'active': self.safe_bool(market, 'tradable'), + 'contract': contract, + 'linear': True if (contract) else None, + 'inverse': False if (contract) else None, + 'contractSize': self.safe_number(market, 'contract_size'), + 'expiry': expiry, + 'expiryDatetime': self.iso8601(expiry), + 'strike': self.parse_number(strike), + 'optionType': optionType, + 'precision': { + 'price': self.parse_number(self.safe_string(market, 'price_tick_size')), + 'amount': self.parse_number(self.safe_string(market, 'qty_tick_size')), + }, + 'limits': { + 'leverage': { + 'min': self.parse_number('1'), + 'max': self.safe_number(market, 'max_leverage'), + }, + 'amount': { + 'min': None, + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + 'info': market, + }) + return result + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#public-get-tickers + https://exchange-docs.crypto.com/derivatives/index.html#public-get-tickers + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + market = None + request: dict = {} + if symbols is not None: + symbol = None + if isinstance(symbols, list): + symbolsLength = len(symbols) + if symbolsLength > 1: + raise BadRequest(self.id + ' fetchTickers() symbols argument cannot contain more than 1 symbol') + symbol = symbols[0] + else: + symbol = symbols + market = self.market(symbol) + request['instrument_name'] = market['id'] + response = await self.v1PublicGetPublicGetTickers(self.extend(request, params)) + # + # { + # "id": -1, + # "method": "public/get-tickers", + # "code": 0, + # "result": { + # "data": [ + # { + # "i": "AVAXUSD-PERP", + # "h": "13.209", + # "l": "12.148", + # "a": "13.209", + # "v": "1109.8", + # "vv": "14017.33", + # "c": "0.0732", + # "b": "13.210", + # "k": "13.230", + # "oi": "10888.9", + # "t": 1687402657575 + # }, + # ] + # } + # } + # + result = self.safe_dict(response, 'result', {}) + data = self.safe_list(result, 'data', []) + return self.parse_tickers(data, symbols) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#public-get-tickers + + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbol = self.symbol(symbol) + tickers = await self.fetch_tickers([symbol], params) + return self.safe_value(tickers, symbol) + + async def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#private-get-order-history + + :param str symbol: unified market symbol of the market the orders were made in + :param int [since]: the earliest time in ms to fetch orders for, max date range is one day + :param int [limit]: the maximum number of order structures to retrieve, default 100 max 100 + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms for the ending date filter, default is the current time + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOrders', 'paginate') + if paginate: + return await self.fetch_paginated_call_dynamic('fetchOrders', symbol, since, limit, params) + market = None + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['instrument_name'] = market['id'] + if since is not None: + request['start_time'] = since + if limit is not None: + request['limit'] = limit + until = self.safe_integer(params, 'until') + params = self.omit(params, ['until']) + if until is not None: + request['end_time'] = until + response = await self.v1PrivatePostPrivateGetOrderHistory(self.extend(request, params)) + # + # { + # "id": 1686881486183, + # "method": "private/get-order-history", + # "code": 0, + # "result": { + # "data": [ + # { + # "account_id": "ce075bef-1234-4321-bd6g-ff9007252e63", + # "order_id": "6142909895014042762", + # "client_oid": "4e918597-1234-4321-8201-a7577e1e1d91", + # "order_type": "MARKET", + # "time_in_force": "GOOD_TILL_CANCEL", + # "side": "SELL", + # "exec_inst": [], + # "quantity": "0.00024", + # "order_value": "5.7054672", + # "maker_fee_rate": "0", + # "taker_fee_rate": "0", + # "avg_price": "25023.97", + # "trigger_price": "0", + # "ref_price": "0", + # "ref_price_type": "NULL_VAL", + # "cumulative_quantity": "0.00024", + # "cumulative_value": "6.0057528", + # "cumulative_fee": "0.001501438200", + # "status": "FILLED", + # "update_user_id": "ce075bef-1234-4321-bd6g-ff9007252e63", + # "order_date": "2023-06-15", + # "instrument_name": "BTC_USD", + # "fee_instrument_name": "USD", + # "create_time": 1686805465891, + # "create_time_ns": "1686805465891812578", + # "update_time": 1686805465891 + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'result', {}) + orders = self.safe_list(data, 'data', []) + return self.parse_orders(orders, market, since, limit) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get a list of the most recent trades for a particular symbol + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#public-get-trades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch, maximum date range is one day + :param int [limit]: the maximum number of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms for the ending date filter, default is the current time + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchTrades', 'paginate') + if paginate: + return await self.fetch_paginated_call_dynamic('fetchTrades', symbol, since, limit, params) + market = self.market(symbol) + request: dict = { + 'instrument_name': market['id'], + } + if since is not None: + request['start_ts'] = since + if limit is not None: + request['count'] = limit + until = self.safe_integer(params, 'until') + params = self.omit(params, ['until']) + if until is not None: + request['end_ts'] = until + response = await self.v1PublicGetPublicGetTrades(self.extend(request, params)) + # + # { + # "id": -1, + # "method": "public/get-trades", + # "code": 0, + # "result": { + # "data": [ + # { + # "s": "sell", + # "p": "26386.00", + # "q": "0.00453", + # "t": 1686944282062, + # "tn" : 1704476468851524373, + # "d": "4611686018455979970", + # "i": "BTC_USD" + # }, + # ] + # } + # } + # + result = self.safe_dict(response, 'result', {}) + trades = self.safe_list(result, 'data', []) + return self.parse_trades(trades, market, since, limit) + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#public-get-candlestick + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms for the ending date filter, default is the current time + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate', False) + if paginate: + return await self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params, 300) + market = self.market(symbol) + request: dict = { + 'instrument_name': market['id'], + 'timeframe': self.safe_string(self.timeframes, timeframe, timeframe), + } + if limit is not None: + if limit > 300: + limit = 300 + request['count'] = limit + now = self.microseconds() + duration = self.parse_timeframe(timeframe) + until = self.safe_integer(params, 'until', now) + params = self.omit(params, ['until']) + if since is not None: + request['start_ts'] = since - duration * 1000 + if limit is not None: + request['end_ts'] = self.sum(since, duration * limit * 1000) + else: + request['end_ts'] = until + else: + request['end_ts'] = until + response = await self.v1PublicGetPublicGetCandlestick(self.extend(request, params)) + # + # { + # "id": -1, + # "method": "public/get-candlestick", + # "code": 0, + # "result": { + # "interval": "1m", + # "data": [ + # { + # "o": "26949.89", + # "h": "26957.64", + # "l": "26948.24", + # "c": "26950.00", + # "v": "0.0670", + # "t": 1687237080000 + # }, + # ], + # "instrument_name": "BTC_USD" + # } + # } + # + result = self.safe_dict(response, 'result', {}) + data = self.safe_list(result, 'data', []) + return self.parse_ohlcvs(data, market, timeframe, since, limit) + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#public-get-book + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the number of order book entries to return, max 50 + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'instrument_name': market['id'], + } + if limit: + request['depth'] = min(limit, 50) # max 50 + response = await self.v1PublicGetPublicGetBook(self.extend(request, params)) + # + # { + # "id": -1, + # "method": "public/get-book", + # "code": 0, + # "result": { + # "depth": 3, + # "data": [ + # { + # "bids": [["30025.00", "0.00004", "1"], ["30020.15", "0.02498", "1"], ["30020.00", "0.00004", "1"]], + # "asks": [["30025.01", "0.04090", "1"], ["30025.70", "0.01000", "1"], ["30026.94", "0.02681", "1"]], + # "t": 1687491287380 + # } + # ], + # "instrument_name": "BTC_USD" + # } + # } + # + result = self.safe_dict(response, 'result', {}) + data = self.safe_list(result, 'data', []) + orderBook = self.safe_value(data, 0) + timestamp = self.safe_integer(orderBook, 't') + return self.parse_order_book(orderBook, symbol, timestamp) + + def parse_balance(self, response) -> Balances: + responseResult = self.safe_dict(response, 'result', {}) + data = self.safe_list(responseResult, 'data', []) + positionBalances = self.safe_value(data[0], 'position_balances', []) + result: dict = {'info': response} + for i in range(0, len(positionBalances)): + balance = positionBalances[i] + currencyId = self.safe_string(balance, 'instrument_name') + code = self.safe_currency_code(currencyId) + account = self.account() + account['total'] = self.safe_string(balance, 'quantity') + account['used'] = self.safe_string(balance, 'reserved_qty') + result[code] = account + return self.safe_balance(result) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#private-user-balance + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + response = await self.v1PrivatePostPrivateUserBalance(params) + # + # { + # "id": 1687300499018, + # "method": "private/user-balance", + # "code": 0, + # "result": { + # "data": [ + # { + # "total_available_balance": "5.84684368", + # "total_margin_balance": "5.84684368", + # "total_initial_margin": "0", + # "total_maintenance_margin": "0", + # "total_position_cost": "0", + # "total_cash_balance": "6.44412101", + # "total_collateral_value": "5.846843685", + # "total_session_unrealized_pnl": "0", + # "instrument_name": "USD", + # "total_session_realized_pnl": "0", + # "position_balances": [ + # { + # "quantity": "0.0002119875", + # "reserved_qty": "0", + # "collateral_weight": "0.9", + # "collateral_amount": "5.37549592", + # "market_value": "5.97277325", + # "max_withdrawal_balance": "0.00021198", + # "instrument_name": "BTC", + # "hourly_interest_rate": "0" + # }, + # ], + # "total_effective_leverage": "0", + # "position_limit": "3000000", + # "used_position_limit": "0", + # "total_borrow": "0", + # "margin_score": "0", + # "is_liquidating": False, + # "has_risk": False, + # "terminatable": True + # } + # ] + # } + # } + # + return self.parse_balance(response) + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#private-get-order-detail + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + request: dict = { + 'order_id': id, + } + response = await self.v1PrivatePostPrivateGetOrderDetail(self.extend(request, params)) + # + # { + # "id": 1686872583882, + # "method": "private/get-order-detail", + # "code": 0, + # "result": { + # "account_id": "ae075bef-1234-4321-bd6g-bb9007252a63", + # "order_id": "6142909895025252686", + # "client_oid": "CCXT_c2d2152cc32d40a3ae7fbf", + # "order_type": "LIMIT", + # "time_in_force": "GOOD_TILL_CANCEL", + # "side": "BUY", + # "exec_inst": [], + # "quantity": "0.00020", + # "limit_price": "20000.00", + # "order_value": "4", + # "avg_price": "0", + # "trigger_price": "0", + # "ref_price": "0", + # "cumulative_quantity": "0", + # "cumulative_value": "0", + # "cumulative_fee": "0", + # "status": "ACTIVE", + # "update_user_id": "ae075bef-1234-4321-bd6g-bb9007252a63", + # "order_date": "2023-06-15", + # "instrument_name": "BTC_USD", + # "fee_instrument_name": "BTC", + # "create_time": 1686870220684, + # "create_time_ns": "1686870220684239675", + # "update_time": 1686870220684 + # } + # } + # + order = self.safe_dict(response, 'result', {}) + return self.parse_order(order, market) + + def create_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + market = self.market(symbol) + uppercaseType = type.upper() + request: dict = { + 'instrument_name': market['id'], + 'side': side.upper(), + 'quantity': self.amount_to_precision(symbol, amount), + } + if (uppercaseType == 'LIMIT') or (uppercaseType == 'STOP_LIMIT') or (uppercaseType == 'TAKE_PROFIT_LIMIT'): + request['price'] = self.price_to_precision(symbol, price) + broker = self.safe_string(self.options, 'broker', 'CCXT') + request['broker_id'] = broker + marketType = None + marginMode = None + marketType, params = self.handle_market_type_and_params('createOrder', market, params) + marginMode, params = self.custom_handle_margin_mode_and_params('createOrder', params) + if (marketType == 'margin') or (marginMode is not None): + request['spot_margin'] = 'MARGIN' + elif marketType == 'spot': + request['spot_margin'] = 'SPOT' + timeInForce = self.safe_string_upper_2(params, 'timeInForce', 'time_in_force') + if timeInForce is not None: + if timeInForce == 'GTC': + request['time_in_force'] = 'GOOD_TILL_CANCEL' + elif timeInForce == 'IOC': + request['time_in_force'] = 'IMMEDIATE_OR_CANCEL' + elif timeInForce == 'FOK': + request['time_in_force'] = 'FILL_OR_KILL' + else: + request['time_in_force'] = timeInForce + postOnly = self.safe_bool(params, 'postOnly', False) + if (postOnly) or (timeInForce == 'PO'): + request['exec_inst'] = ['POST_ONLY'] + request['time_in_force'] = 'GOOD_TILL_CANCEL' + triggerPrice = self.safe_string_n(params, ['stopPrice', 'triggerPrice', 'ref_price']) + stopLossPrice = self.safe_number(params, 'stopLossPrice') + takeProfitPrice = self.safe_number(params, 'takeProfitPrice') + isTrigger = (triggerPrice is not None) + isStopLossTrigger = (stopLossPrice is not None) + isTakeProfitTrigger = (takeProfitPrice is not None) + if isTrigger: + request['ref_price'] = self.price_to_precision(symbol, triggerPrice) + priceString = self.number_to_string(price) + if (uppercaseType == 'LIMIT') or (uppercaseType == 'STOP_LIMIT') or (uppercaseType == 'TAKE_PROFIT_LIMIT'): + if side == 'buy': + if Precise.string_lt(priceString, triggerPrice): + request['type'] = 'TAKE_PROFIT_LIMIT' + else: + request['type'] = 'STOP_LIMIT' + else: + if Precise.string_lt(priceString, triggerPrice): + request['type'] = 'STOP_LIMIT' + else: + request['type'] = 'TAKE_PROFIT_LIMIT' + else: + if side == 'buy': + if Precise.string_lt(priceString, triggerPrice): + request['type'] = 'TAKE_PROFIT' + else: + request['type'] = 'STOP_LOSS' + else: + if Precise.string_lt(priceString, triggerPrice): + request['type'] = 'STOP_LOSS' + else: + request['type'] = 'TAKE_PROFIT' + elif isStopLossTrigger: + if (uppercaseType == 'LIMIT') or (uppercaseType == 'STOP_LIMIT'): + request['type'] = 'STOP_LIMIT' + else: + request['type'] = 'STOP_LOSS' + request['ref_price'] = self.price_to_precision(symbol, stopLossPrice) + elif isTakeProfitTrigger: + if (uppercaseType == 'LIMIT') or (uppercaseType == 'TAKE_PROFIT_LIMIT'): + request['type'] = 'TAKE_PROFIT_LIMIT' + else: + request['type'] = 'TAKE_PROFIT' + request['ref_price'] = self.price_to_precision(symbol, takeProfitPrice) + else: + request['type'] = uppercaseType + params = self.omit(params, ['postOnly', 'clientOrderId', 'timeInForce', 'stopPrice', 'triggerPrice', 'stopLossPrice', 'takeProfitPrice']) + return self.extend(request, params) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#private-create-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market', 'limit', 'stop_loss', 'stop_limit', 'take_profit', 'take_profit_limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.timeInForce]: 'GTC', 'IOC', 'FOK' or 'PO' + :param str [params.ref_price_type]: 'MARK_PRICE', 'INDEX_PRICE', 'LAST_PRICE' which trigger price type to use, default is MARK_PRICE + :param float [params.triggerPrice]: price to trigger a trigger order + :param float [params.stopLossPrice]: price to trigger a stop-loss trigger order + :param float [params.takeProfitPrice]: price to trigger a take-profit trigger order + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + request = self.create_order_request(symbol, type, side, amount, price, params) + response = await self.v1PrivatePostPrivateCreateOrder(request) + # + # { + # "id": 1686804664362, + # "method": "private/create-order", + # "code" : 0, + # "result": { + # "order_id": "6540219377766741832", + # "client_oid": "CCXT_d6ef7c3db6c1495aa8b757" + # } + # } + # + result = self.safe_dict(response, 'result', {}) + return self.parse_order(result, market) + + async def create_orders(self, orders: List[OrderRequest], params={}): + """ + create a list of trade orders + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#private-create-order-list-list + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#private-create-order-list-oco + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + ordersRequests = [] + for i in range(0, len(orders)): + rawOrder = orders[i] + marketId = self.safe_string(rawOrder, 'symbol') + type = self.safe_string(rawOrder, 'type') + side = self.safe_string(rawOrder, 'side') + amount = self.safe_value(rawOrder, 'amount') + price = self.safe_value(rawOrder, 'price') + orderParams = self.safe_dict(rawOrder, 'params', {}) + orderRequest = self.create_advanced_order_request(marketId, type, side, amount, price, orderParams) + ordersRequests.append(orderRequest) + contigency = self.safe_string(params, 'contingency_type', 'LIST') + request: dict = { + 'contingency_type': contigency, # or OCO + 'order_list': ordersRequests, + } + response = await self.v1PrivatePostPrivateCreateOrderList(self.extend(request, params)) + # + # { + # "id": 12, + # "method": "private/create-order-list", + # "code": 10001, + # "result": { + # "result_list": [ + # { + # "index": 0, + # "code": 0, + # "order_id": "2015106383706015873", + # "client_oid": "my_order_0001" + # }, + # { + # "index": 1, + # "code": 20007, + # "message": "INVALID_REQUEST", + # "client_oid": "my_order_0002" + # } + # ] + # } + # } + # + # { + # "id" : 1698068111133, + # "method" : "private/create-order-list", + # "code" : 0, + # "result" : [{ + # "code" : 0, + # "index" : 0, + # "client_oid" : "1698068111133_0", + # "order_id" : "6142909896519488206" + # }, { + # "code" : 306, + # "index" : 1, + # "client_oid" : "1698068111133_1", + # "message" : "INSUFFICIENT_AVAILABLE_BALANCE", + # "order_id" : "6142909896519488207" + # }] + # } + # + result = self.safe_value(response, 'result', []) + listId = self.safe_string(result, 'list_id') + if listId is not None: + ocoOrders = [{'order_id': listId}] + return self.parse_orders(ocoOrders) + return self.parse_orders(result) + + def create_advanced_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + # differs slightly from createOrderRequest + # since the advanced order endpoint requires a different set of parameters + # namely here we don't support ref_price or spot_margin + # and market-buy orders need to send notional instead of quantity + market = self.market(symbol) + uppercaseType = type.upper() + request: dict = { + 'instrument_name': market['id'], + 'side': side.upper(), + } + if (uppercaseType == 'LIMIT') or (uppercaseType == 'STOP_LIMIT') or (uppercaseType == 'TAKE_PROFIT_LIMIT'): + request['price'] = self.price_to_precision(symbol, price) + broker = self.safe_string(self.options, 'broker', 'CCXT') + request['broker_id'] = broker + timeInForce = self.safe_string_upper_2(params, 'timeInForce', 'time_in_force') + if timeInForce is not None: + if timeInForce == 'GTC': + request['time_in_force'] = 'GOOD_TILL_CANCEL' + elif timeInForce == 'IOC': + request['time_in_force'] = 'IMMEDIATE_OR_CANCEL' + elif timeInForce == 'FOK': + request['time_in_force'] = 'FILL_OR_KILL' + else: + request['time_in_force'] = timeInForce + postOnly = self.safe_bool(params, 'postOnly', False) + if (postOnly) or (timeInForce == 'PO'): + request['exec_inst'] = ['POST_ONLY'] + request['time_in_force'] = 'GOOD_TILL_CANCEL' + triggerPrice = self.safe_string_n(params, ['stopPrice', 'triggerPrice', 'ref_price']) + stopLossPrice = self.safe_number(params, 'stopLossPrice') + takeProfitPrice = self.safe_number(params, 'takeProfitPrice') + isTrigger = (triggerPrice is not None) + isStopLossTrigger = (stopLossPrice is not None) + isTakeProfitTrigger = (takeProfitPrice is not None) + if isTrigger: + priceString = self.number_to_string(price) + if (uppercaseType == 'LIMIT') or (uppercaseType == 'STOP_LIMIT') or (uppercaseType == 'TAKE_PROFIT_LIMIT'): + if side == 'buy': + if Precise.string_lt(priceString, triggerPrice): + request['type'] = 'TAKE_PROFIT_LIMIT' + else: + request['type'] = 'STOP_LIMIT' + else: + if Precise.string_lt(priceString, triggerPrice): + request['type'] = 'STOP_LIMIT' + else: + request['type'] = 'TAKE_PROFIT_LIMIT' + else: + if side == 'buy': + if Precise.string_lt(priceString, triggerPrice): + request['type'] = 'TAKE_PROFIT' + else: + request['type'] = 'STOP_LOSS' + else: + if Precise.string_lt(priceString, triggerPrice): + request['type'] = 'STOP_LOSS' + else: + request['type'] = 'TAKE_PROFIT' + elif isStopLossTrigger: + if (uppercaseType == 'LIMIT') or (uppercaseType == 'STOP_LIMIT'): + request['type'] = 'STOP_LIMIT' + else: + request['type'] = 'STOP_LOSS' + elif isTakeProfitTrigger: + if (uppercaseType == 'LIMIT') or (uppercaseType == 'TAKE_PROFIT_LIMIT'): + request['type'] = 'TAKE_PROFIT_LIMIT' + else: + request['type'] = 'TAKE_PROFIT' + else: + request['type'] = uppercaseType + if (side == 'buy') and ((uppercaseType == 'MARKET') or (uppercaseType == 'STOP_LOSS') or (uppercaseType == 'TAKE_PROFIT')): + # use createmarketBuy logic here + quoteAmount = None + createMarketBuyOrderRequiresPrice = True + createMarketBuyOrderRequiresPrice, params = self.handle_option_and_params(params, 'createOrder', 'createMarketBuyOrderRequiresPrice', True) + cost = self.safe_number_2(params, 'cost', 'notional') + params = self.omit(params, 'cost') + if cost is not None: + quoteAmount = self.cost_to_precision(symbol, cost) + elif createMarketBuyOrderRequiresPrice: + if price is None: + raise InvalidOrder(self.id + ' createOrder() requires the price argument for market buy orders to calculate the total cost to spend(amount * price), alternatively set the createMarketBuyOrderRequiresPrice option or param to False and pass the cost to spend(quote quantity) in the amount argument') + else: + amountString = self.number_to_string(amount) + priceString = self.number_to_string(price) + costRequest = Precise.string_mul(amountString, priceString) + quoteAmount = self.cost_to_precision(symbol, costRequest) + else: + quoteAmount = self.cost_to_precision(symbol, amount) + request['notional'] = quoteAmount + else: + request['quantity'] = self.amount_to_precision(symbol, amount) + params = self.omit(params, ['postOnly', 'clientOrderId', 'timeInForce', 'stopPrice', 'triggerPrice', 'stopLossPrice', 'takeProfitPrice']) + return self.extend(request, params) + + async def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + """ + edit a trade order + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#private-amend-order + + :param str id: order id + :param str symbol: unified market symbol of the order to edit + :param str [type]: not used by cryptocom editOrder + :param str [side]: not used by cryptocom editOrder + :param float amount:(mandatory) how much of the currency you want to trade in units of the base currency + :param float price:(mandatory) the price for the order, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.clientOrderId]: the original client order id of the order to edit, required if id is not provided + :returns dict: an `order structure ` + """ + await self.load_markets() + request = self.edit_order_request(id, symbol, amount, price, params) + response = await self.v1PrivatePostPrivateAmendOrder(request) + result = self.safe_dict(response, 'result', {}) + return self.parse_order(result) + + def edit_order_request(self, id: str, symbol: str, amount: float, price: Num = None, params={}): + request: dict = {} + if id is not None: + request['order_id'] = id + else: + originalClientOrderId = self.safe_string_2(params, 'orig_client_oid', 'clientOrderId') + if originalClientOrderId is None: + raise ArgumentsRequired(self.id + ' editOrder() requires an id argument or orig_client_oid parameter') + else: + request['orig_client_oid'] = originalClientOrderId + params = self.omit(params, ['orig_client_oid', 'clientOrderId']) + if (amount is None) or (price is None): + raise ArgumentsRequired(self.id + ' editOrder() requires both amount and price arguments. If you do not want to change the amount or price, you should pass the original values') + request['new_quantity'] = self.amount_to_precision(symbol, amount) + request['new_price'] = self.price_to_precision(symbol, price) + return self.extend(request, params) + + async def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#private-cancel-all-orders + + :param str symbol: unified market symbol of the orders to cancel + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict} Returns exchange raw message{@link https://docs.ccxt.com/#/?id=order-structure: + """ + await self.load_markets() + market = None + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['instrument_name'] = market['id'] + response = await self.v1PrivatePostPrivateCancelAllOrders(self.extend(request, params)) + return [self.safe_order({'info': response})] + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#private-cancel-order + + :param str id: the order id of the order to cancel + :param str [symbol]: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + request: dict = { + 'order_id': id, + } + response = await self.v1PrivatePostPrivateCancelOrder(self.extend(request, params)) + # + # { + # "id": 1686882846638, + # "method": "private/cancel-order", + # "code": 0, + # "message": "NO_ERROR", + # "result": { + # "client_oid": "CCXT_c2d2152cc32d40a3ae7fbf", + # "order_id": "6142909895025252686" + # } + # } + # + result = self.safe_dict(response, 'result', {}) + return self.parse_order(result, market) + + async def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): + """ + cancel multiple orders + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#private-cancel-order-list-list + + :param str[] ids: order ids + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrders() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + orderRequests = [] + for i in range(0, len(ids)): + id = ids[i] + order: dict = { + 'instrument_name': market['id'], + 'order_id': str(id), + } + orderRequests.append(order) + request: dict = { + 'contingency_type': 'LIST', + 'order_list': orderRequests, + } + response = await self.v1PrivatePostPrivateCancelOrderList(self.extend(request, params)) + result = self.safe_list(response, 'result', []) + return self.parse_orders(result, market, None, None, params) + + async def cancel_orders_for_symbols(self, orders: List[CancellationRequest], params={}): + """ + cancel multiple orders for multiple symbols + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#private-cancel-order-list-list + + :param CancellationRequest[] orders: each order should contain the parameters required by cancelOrder namely id and symbol, example [{"id": "a", "symbol": "BTC/USDT"}, {"id": "b", "symbol": "ETH/USDT"}] + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an list of `order structures ` + """ + await self.load_markets() + orderRequests = [] + for i in range(0, len(orders)): + order = orders[i] + id = self.safe_string(order, 'id') + symbol = self.safe_string(order, 'symbol') + market = self.market(symbol) + orderItem: dict = { + 'instrument_name': market['id'], + 'order_id': str(id), + } + orderRequests.append(orderItem) + request: dict = { + 'contingency_type': 'LIST', + 'order_list': orderRequests, + } + response = await self.v1PrivatePostPrivateCancelOrderList(self.extend(request, params)) + result = self.safe_list(response, 'result', []) + return self.parse_orders(result, None, None, None, params) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#private-get-open-orders + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + market = None + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['instrument_name'] = market['id'] + response = await self.v1PrivatePostPrivateGetOpenOrders(self.extend(request, params)) + # + # { + # "id": 1686806134961, + # "method": "private/get-open-orders", + # "code": 0, + # "result": { + # "data": [ + # { + # "account_id": "ce075bef-1234-4321-bd6g-ff9007252e63", + # "order_id": "6530219477767564494", + # "client_oid": "CCXT_7ce730f0388441df9bc218", + # "order_type": "LIMIT", + # "time_in_force": "GOOD_TILL_CANCEL", + # "side": "BUY", + # "exec_inst": [], + # "quantity": "0.00020", + # "limit_price": "20000.00", + # "order_value": "4", + # "avg_price": "0", + # "trigger_price": "0", + # "ref_price": "0", + # "cumulative_quantity": "0", + # "cumulative_value": "0", + # "cumulative_fee": "0", + # "status": "ACTIVE", + # "update_user_id": "ce075bef-1234-4321-bd6g-gg9007252e63", + # "order_date": "2023-06-15", + # "instrument_name": "BTC_USD", + # "fee_instrument_name": "BTC", + # "create_time": 1686806053992, + # "create_time_ns": "1686806053992921880", + # "update_time": 1686806053993 + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'result', {}) + orders = self.safe_list(data, 'data', []) + return self.parse_orders(orders, market, since, limit) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#private-get-trades + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for, maximum date range is one day + :param int [limit]: the maximum number of trade structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms for the ending date filter, default is the current time + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchMyTrades', 'paginate') + if paginate: + return await self.fetch_paginated_call_dynamic('fetchMyTrades', symbol, since, limit, params, 100) + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['instrument_name'] = market['id'] + if since is not None: + request['start_time'] = since + if limit is not None: + request['limit'] = limit + until = self.safe_integer(params, 'until') + params = self.omit(params, ['until']) + if until is not None: + request['end_time'] = until + response = await self.v1PrivatePostPrivateGetTrades(self.extend(request, params)) + # + # { + # "id": 1686942003520, + # "method": "private/get-trades", + # "code": 0, + # "result": { + # "data": [ + # { + # "account_id": "ds075abc-1234-4321-bd6g-ff9007252r63", + # "event_date": "2023-06-16", + # "journal_type": "TRADING", + # "side": "BUY", + # "instrument_name": "BTC_USD", + # "fees": "-0.0000000525", + # "trade_id": "6142909898247428343", + # "trade_match_id": "4611686018455978480", + # "create_time": 1686941992887, + # "traded_price": "26347.16", + # "traded_quantity": "0.00021", + # "fee_instrument_name": "BTC", + # "client_oid": "d1c70a60-810e-4c92-b2a0-72b931cb31e0", + # "taker_side": "TAKER", + # "order_id": "6142909895036331486", + # "create_time_ns": "1686941992887207066" + # } + # ] + # } + # } + # + result = self.safe_dict(response, 'result', {}) + trades = self.safe_list(result, 'data', []) + return self.parse_trades(trades, market, since, limit) + + def parse_address(self, addressString): + address = None + tag = None + rawTag = None + if addressString.find('?') > 0: + address, rawTag = addressString.split('?') + splitted = rawTag.split('=') + tag = splitted[1] + else: + address = addressString + return [address, tag] + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#private-create-withdrawal + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + await self.load_markets() + currency = self.safe_currency(code) # for instance, USDC is not inferred from markets but it's still available + request: dict = { + 'currency': currency['id'], + 'amount': amount, + 'address': address, + } + if tag is not None: + request['address_tag'] = tag + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + networkId = self.network_code_to_id(networkCode) + if networkId is not None: + request['network_id'] = networkId + response = await self.v1PrivatePostPrivateCreateWithdrawal(self.extend(request, params)) + # + # { + # "id":-1, + # "method":"private/create-withdrawal", + # "code":0, + # "result": { + # "id": 2220, + # "amount": 1, + # "fee": 0.0004, + # "symbol": "BTC", + # "address": "2NBqqD5GRJ8wHy1PYyCXTe9ke5226FhavBf", + # "client_wid": "my_withdrawal_002", + # "create_time":1607063412000 + # } + # } + # + result = self.safe_dict(response, 'result') + return self.parse_transaction(result, currency) + + async def fetch_deposit_addresses_by_network(self, code: str, params={}) -> List[DepositAddress]: + """ + fetch a dictionary of addresses for a currency, indexed by network + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#private-get-deposit-address + + :param str code: unified currency code of the currency for the deposit address + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `address structures ` indexed by the network + """ + await self.load_markets() + currency = self.safe_currency(code) + request: dict = { + 'currency': currency['id'], + } + response = await self.v1PrivatePostPrivateGetDepositAddress(self.extend(request, params)) + # + # { + # "id": 1234555011221, + # "method": "private/get-deposit-address", + # "code": 0, + # "result": { + # "deposit_address_list": [ + # { + # "currency": "BTC", + # "create_time": 1686730755000, + # "id": "3737377", + # "address": "3N9afggxTSmJ3H4jaMQuWyEiLBzZdAbK6d", + # "status":"1", + # "network": "BTC" + # }, + # ] + # } + # } + # + data = self.safe_dict(response, 'result', {}) + addresses = self.safe_list(data, 'deposit_address_list', []) + addressesLength = len(addresses) + if addressesLength == 0: + raise ExchangeError(self.id + ' fetchDepositAddressesByNetwork() generating address...') + result: dict = {} + for i in range(0, addressesLength): + value = self.safe_dict(addresses, i) + addressString = self.safe_string(value, 'address') + currencyId = self.safe_string(value, 'currency') + responseCode = self.safe_currency_code(currencyId) + address, tag = self.parse_address(addressString) + self.check_address(address) + networkId = self.safe_string(value, 'network') + network = self.network_id_to_code(networkId, responseCode) + result[network] = { + 'info': value, + 'currency': responseCode, + 'network': network, + 'address': address, + 'tag': tag, + } + return result + + async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#private-get-deposit-address + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + network = self.safe_string_upper(params, 'network') + params = self.omit(params, ['network']) + depositAddresses = await self.fetch_deposit_addresses_by_network(code, params) + if network in depositAddresses: + return depositAddresses[network] + else: + keys = list(depositAddresses.keys()) + return depositAddresses[keys[0]] + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#private-get-deposit-history + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms for the ending date filter, default is the current time + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + currency = None + request: dict = {} + if code is not None: + currency = self.safe_currency(code) + request['currency'] = currency['id'] + if since is not None: + # 90 days date range + request['start_ts'] = since + if limit is not None: + request['page_size'] = limit + until = self.safe_integer(params, 'until') + params = self.omit(params, ['until']) + if until is not None: + request['end_ts'] = until + response = await self.v1PrivatePostPrivateGetDepositHistory(self.extend(request, params)) + # + # { + # "id": 1688701375714, + # "method": "private/get-deposit-history", + # "code": 0, + # "result": { + # "deposit_list": [ + # { + # "currency": "BTC", + # "fee": 0, + # "create_time": 1688023659000, + # "id": "6201135", + # "update_time": 1688178509000, + # "amount": 0.00114571, + # "address": "1234fggxTSmJ3H4jaMQuWyEiLBzZdAbK6d", + # "status": "1", + # "txid": "f0ae4202b76eb999c301eccdde44dc639bee42d1fdd5974105286ca3393f6065/2" + # }, + # ] + # } + # } + # + data = self.safe_dict(response, 'result', {}) + depositList = self.safe_list(data, 'deposit_list', []) + return self.parse_transactions(depositList, currency, since, limit) + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#private-get-withdrawal-history + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms for the ending date filter, default is the current time + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + currency = None + request: dict = {} + if code is not None: + currency = self.safe_currency(code) + request['currency'] = currency['id'] + if since is not None: + # 90 days date range + request['start_ts'] = since + if limit is not None: + request['page_size'] = limit + until = self.safe_integer(params, 'until') + params = self.omit(params, ['until']) + if until is not None: + request['end_ts'] = until + response = await self.v1PrivatePostPrivateGetWithdrawalHistory(self.extend(request, params)) + # + # { + # "id": 1688613879534, + # "method": "private/get-withdrawal-history", + # "code": 0, + # "result": { + # "withdrawal_list": [ + # { + # "currency": "BTC", + # "client_wid": "", + # "fee": 0.0005, + # "create_time": 1688613850000, + # "id": "5275977", + # "update_time": 1688613850000, + # "amount": 0.0005, + # "address": "1234NMEWbiF8ZkwUMxmfzMxi2A1MQ44bMn", + # "status": "1", + # "txid": "", + # "network_id": "BTC" + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'result', {}) + withdrawalList = self.safe_list(data, 'withdrawal_list', []) + return self.parse_transactions(withdrawalList, currency, since, limit) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # fetchTicker + # + # { + # "i": "BTC_USD", + # "h": "30821.45", + # "l": "28685.11", + # "a": "30446.00", + # "v": "1767.8734", + # "vv": "52436726.42", + # "c": "0.0583", + # "b": "30442.00", + # "k": "30447.66", + # "t": 1687403045415 + # } + # + # fetchTickers + # + # { + # "i": "AVAXUSD-PERP", + # "h": "13.209", + # "l": "12.148", + # "a": "13.209", + # "v": "1109.8", + # "vv": "14017.33", + # "c": "0.0732", + # "b": "13.210", + # "k": "13.230", + # "oi": "10888.9", + # "t": 1687402657575 + # } + # + timestamp = self.safe_integer(ticker, 't') + marketId = self.safe_string(ticker, 'i') + market = self.safe_market(marketId, market, '_') + quote = self.safe_string(market, 'quote') + last = self.safe_string(ticker, 'a') + return self.safe_ticker({ + 'symbol': market['symbol'], + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_number(ticker, 'h'), + 'low': self.safe_number(ticker, 'l'), + 'bid': self.safe_number(ticker, 'b'), + 'bidVolume': None, + 'ask': self.safe_number(ticker, 'k'), + 'askVolume': None, + 'vwap': None, + 'open': None, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': self.safe_string(ticker, 'c'), + 'average': None, + 'baseVolume': self.safe_string(ticker, 'v'), + 'quoteVolume': self.safe_string(ticker, 'vv') if (quote == 'USD') else None, + 'info': ticker, + }, market) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades + # + # { + # "s": "sell", + # "p": "26386.00", + # "q": "0.00453", + # "tn": 1686944282062, + # "tn": 1704476468851524373, + # "d": "4611686018455979970", + # "i": "BTC_USD" + # } + # + # fetchMyTrades + # + # { + # "account_id": "ds075abc-1234-4321-bd6g-ff9007252r63", + # "event_date": "2023-06-16", + # "journal_type": "TRADING", + # "side": "BUY", + # "instrument_name": "BTC_USD", + # "fees": "-0.0000000525", + # "trade_id": "6142909898247428343", + # "trade_match_id": "4611686018455978480", + # "create_time": 1686941992887, + # "traded_price": "26347.16", + # "traded_quantity": "0.00021", + # "fee_instrument_name": "BTC", + # "client_oid": "d1c70a60-1234-4c92-b2a0-72b931cb31e0", + # "taker_side": "TAKER", + # "order_id": "6142909895036331486", + # "create_time_ns": "1686941992887207066" + # } + # + timestamp = self.safe_integer_2(trade, 't', 'create_time') + marketId = self.safe_string_2(trade, 'i', 'instrument_name') + market = self.safe_market(marketId, market, '_') + feeCurrency = self.safe_string(trade, 'fee_instrument_name') + feeCostString = self.safe_string(trade, 'fees') + return self.safe_trade({ + 'info': trade, + 'id': self.safe_string_2(trade, 'd', 'trade_id'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'order': self.safe_string(trade, 'order_id'), + 'side': self.safe_string_lower_2(trade, 's', 'side'), + 'takerOrMaker': self.safe_string_lower(trade, 'taker_side'), + 'price': self.safe_number_2(trade, 'p', 'traded_price'), + 'amount': self.safe_number_2(trade, 'q', 'traded_quantity'), + 'cost': None, + 'type': None, + 'fee': { + 'currency': self.safe_currency_code(feeCurrency), + 'cost': self.parse_number(Precise.string_neg(feeCostString)), + }, + }, market) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # { + # "o": "26949.89", + # "h": "26957.64", + # "l": "26948.24", + # "c": "26950.00", + # "v": "0.0670", + # "t": 1687237080000 + # } + # + return [ + self.safe_integer(ohlcv, 't'), + self.safe_number(ohlcv, 'o'), + self.safe_number(ohlcv, 'h'), + self.safe_number(ohlcv, 'l'), + self.safe_number(ohlcv, 'c'), + self.safe_number(ohlcv, 'v'), + ] + + def parse_order_status(self, status: Str): + statuses: dict = { + 'ACTIVE': 'open', + 'CANCELED': 'canceled', + 'FILLED': 'closed', + 'REJECTED': 'rejected', + 'EXPIRED': 'expired', + } + return self.safe_string(statuses, status, status) + + def parse_time_in_force(self, timeInForce: Str): + timeInForces: dict = { + 'GOOD_TILL_CANCEL': 'GTC', + 'IMMEDIATE_OR_CANCEL': 'IOC', + 'FILL_OR_KILL': 'FOK', + } + return self.safe_string(timeInForces, timeInForce, timeInForce) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder, cancelOrder + # + # { + # "order_id": "6540219377766741832", + # "client_oid": "CCXT_d6ef7c3db6c1495aa8b757" + # } + # + # fetchOpenOrders, fetchOrder, fetchOrders + # + # { + # "account_id": "ce075bef-1234-4321-bd6g-ff9007252e63", + # "order_id": "6530219477767564494", + # "client_oid": "CCXT_7ce730f0388441df9bc218", + # "order_type": "LIMIT", + # "time_in_force": "GOOD_TILL_CANCEL", + # "side": "BUY", + # "exec_inst": [], + # "quantity": "0.00020", + # "limit_price": "20000.00", + # "order_value": "4", + # "avg_price": "0", + # "trigger_price": "0", + # "ref_price": "0", + # "cumulative_quantity": "0", + # "cumulative_value": "0", + # "cumulative_fee": "0", + # "status": "ACTIVE", + # "update_user_id": "ce075bef-1234-4321-bd6g-gg9007252e63", + # "order_date": "2023-06-15", + # "instrument_name": "BTC_USD", + # "fee_instrument_name": "BTC", + # "create_time": 1686806053992, + # "create_time_ns": "1686806053992921880", + # "update_time": 1686806053993 + # } + # + # createOrders + # { + # "code" : 306, + # "index" : 1, + # "client_oid" : "1698068111133_1", + # "message" : "INSUFFICIENT_AVAILABLE_BALANCE", + # "order_id" : "6142909896519488207" + # } + # + code = self.safe_integer(order, 'code') + if (code is not None) and (code != 0): + return self.safe_order({ + 'id': self.safe_string(order, 'order_id'), + 'clientOrderId': self.safe_string(order, 'client_oid'), + 'info': order, + 'status': 'rejected', + }) + created = self.safe_integer(order, 'create_time') + marketId = self.safe_string(order, 'instrument_name') + symbol = self.safe_symbol(marketId, market) + execInst = self.safe_value(order, 'exec_inst') + postOnly = None + if execInst is not None: + postOnly = False + for i in range(0, len(execInst)): + inst = execInst[i] + if inst == 'POST_ONLY': + postOnly = True + break + feeCurrency = self.safe_string(order, 'fee_instrument_name') + return self.safe_order({ + 'info': order, + 'id': self.safe_string(order, 'order_id'), + 'clientOrderId': self.safe_string(order, 'client_oid'), + 'timestamp': created, + 'datetime': self.iso8601(created), + 'lastTradeTimestamp': self.safe_integer(order, 'update_time'), + 'status': self.parse_order_status(self.safe_string(order, 'status')), + 'symbol': symbol, + 'type': self.safe_string_lower(order, 'order_type'), + 'timeInForce': self.parse_time_in_force(self.safe_string(order, 'time_in_force')), + 'postOnly': postOnly, + 'side': self.safe_string_lower(order, 'side'), + 'price': self.safe_number(order, 'limit_price'), + 'amount': self.safe_number(order, 'quantity'), + 'filled': self.safe_number(order, 'cumulative_quantity'), + 'remaining': None, + 'average': self.safe_number(order, 'avg_price'), + 'cost': self.safe_number(order, 'cumulative_value'), + 'fee': { + 'currency': self.safe_currency_code(feeCurrency), + 'cost': self.safe_number(order, 'cumulative_fee'), + }, + 'trades': [], + }, market) + + def parse_deposit_status(self, status): + statuses: dict = { + '0': 'pending', + '1': 'ok', + '2': 'failed', + '3': 'pending', + } + return self.safe_string(statuses, status, status) + + def parse_withdrawal_status(self, status): + statuses: dict = { + '0': 'pending', + '1': 'pending', + '2': 'failed', + '3': 'pending', + '4': 'failed', + '5': 'ok', + '6': 'canceled', + } + return self.safe_string(statuses, status, status) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fetchDeposits + # + # { + # "currency": "BTC", + # "fee": 0, + # "create_time": 1688023659000, + # "id": "6201135", + # "update_time": 1688178509000, + # "amount": 0.00114571, + # "address": "1234fggxTSmJ3H4jaMQuWyEiLBzZdAbK6d", + # "status": "1", + # "txid": "f0ae4202b76eb999c301eccdde44dc639bee42d1fdd5974105286ca3393f6065/2" + # } + # + # fetchWithdrawals + # + # { + # "currency": "BTC", + # "client_wid": "", + # "fee": 0.0005, + # "create_time": 1688613850000, + # "id": "5775977", + # "update_time": 1688613850000, + # "amount": 0.0005, + # "address": "1234NMEWbiF8ZkwUMxmfzMxi2A1MQ44bMn", + # "status": "1", + # "txid": "", + # "network_id": "BTC" + # } + # + # withdraw + # + # { + # "id": 2220, + # "amount": 1, + # "fee": 0.0004, + # "symbol": "BTC", + # "address": "2NBqqD5GRJ8wHy1PYyCXTe9ke5226FhavBf", + # "client_wid": "my_withdrawal_002", + # "create_time":1607063412000 + # } + # + type = None + rawStatus = self.safe_string(transaction, 'status') + status = None + if 'client_wid' in transaction: + type = 'withdrawal' + status = self.parse_withdrawal_status(rawStatus) + else: + type = 'deposit' + status = self.parse_deposit_status(rawStatus) + addressString = self.safe_string(transaction, 'address') + address, tag = self.parse_address(addressString) + currencyId = self.safe_string(transaction, 'currency') + code = self.safe_currency_code(currencyId, currency) + timestamp = self.safe_integer(transaction, 'create_time') + feeCost = self.safe_number(transaction, 'fee') + fee = None + if feeCost is not None: + fee = {'currency': code, 'cost': feeCost} + return { + 'info': transaction, + 'id': self.safe_string(transaction, 'id'), + 'txid': self.safe_string(transaction, 'txid'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': None, + 'address': address, + 'addressTo': address, + 'addressFrom': None, + 'tag': tag, + 'tagTo': tag, + 'tagFrom': None, + 'type': type, + 'amount': self.safe_number(transaction, 'amount'), + 'currency': code, + 'status': status, + 'updated': self.safe_integer(transaction, 'update_time'), + 'internal': None, + 'comment': self.safe_string(transaction, 'client_wid'), + 'fee': fee, + } + + def custom_handle_margin_mode_and_params(self, methodName, params={}): + """ + @ignore + marginMode specified by params["marginMode"], self.options["marginMode"], self.options["defaultMarginMode"], params["margin"] = True or self.options["defaultType"] = 'margin' + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Array: the marginMode in lowercase + """ + defaultType = self.safe_string(self.options, 'defaultType') + isMargin = self.safe_bool(params, 'margin', False) + params = self.omit(params, 'margin') + marginMode = None + marginMode, params = self.handle_margin_mode_and_params(methodName, params) + if marginMode is not None: + if marginMode != 'cross': + raise NotSupported(self.id + ' only cross margin is supported') + else: + if (defaultType == 'margin') or (isMargin is True): + marginMode = 'cross' + return [marginMode, params] + + def parse_deposit_withdraw_fee(self, fee, currency: Currency = None): + # + # { + # "full_name": "Alchemix", + # "default_network": "ETH", + # "network_list": [ + # { + # "network_id": "ETH", + # "withdrawal_fee": "0.25000000", + # "withdraw_enabled": True, + # "min_withdrawal_amount": "0.5", + # "deposit_enabled": True, + # "confirmation_required": "0" + # } + # ] + # } + # + networkList = self.safe_list(fee, 'network_list', []) + networkListLength = len(networkList) + result: dict = { + 'info': fee, + 'withdraw': { + 'fee': None, + 'percentage': None, + }, + 'deposit': { + 'fee': None, + 'percentage': None, + }, + 'networks': {}, + } + if networkList is not None: + for i in range(0, networkListLength): + networkInfo = networkList[i] + networkId = self.safe_string(networkInfo, 'network_id') + currencyCode = self.safe_string(currency, 'code') + networkCode = self.network_id_to_code(networkId, currencyCode) + result['networks'][networkCode] = { + 'deposit': {'fee': None, 'percentage': None}, + 'withdraw': {'fee': self.safe_number(networkInfo, 'withdrawal_fee'), 'percentage': False}, + } + if networkListLength == 1: + result['withdraw']['fee'] = self.safe_number(networkInfo, 'withdrawal_fee') + result['withdraw']['percentage'] = False + return result + + async def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}): + """ + fetch deposit and withdraw fees + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#private-get-currency-networks + + :param str[]|None codes: list of unified currency codes + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `fee structures ` + """ + await self.load_markets() + response = await self.v1PrivatePostPrivateGetCurrencyNetworks(params) + data = self.safe_value(response, 'result') + currencyMap = self.safe_list(data, 'currency_map') + return self.parse_deposit_withdraw_fees(currencyMap, codes, 'full_name') + + async def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#private-get-transactions + + :param str [code]: unified currency code + :param int [since]: timestamp in ms of the earliest ledger entry + :param int [limit]: max number of ledger entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms for the ending date filter, default is the current time + :returns dict: a `ledger structure ` + """ + await self.load_markets() + request: dict = {} + currency = None + if code is not None: + currency = self.safe_currency(code) + if since is not None: + request['start_time'] = since + if limit is not None: + request['limit'] = limit + until = self.safe_integer(params, 'until') + params = self.omit(params, ['until']) + if until is not None: + request['end_time'] = until + response = await self.v1PrivatePostPrivateGetTransactions(self.extend(request, params)) + # + # { + # "id": 1686813195698, + # "method": "private/get-transactions", + # "code": 0, + # "result": { + # "data": [ + # { + # "account_id": "ce075cef-1234-4321-bd6e-gf9007351e64", + # "event_date": "2023-06-15", + # "journal_type": "TRADING", + # "journal_id": "6530219460124075091", + # "transaction_qty": "6.0091224", + # "transaction_cost": "6.0091224", + # "realized_pnl": "0", + # "order_id": "6530219477766741833", + # "trade_id": "6530219495775954765", + # "trade_match_id": "4611686018455865176", + # "event_timestamp_ms": 1686804665013, + # "event_timestamp_ns": "1686804665013642422", + # "client_oid": "CCXT_d6ea7c5db6c1495aa8b758", + # "taker_side": "", + # "side": "BUY", + # "instrument_name": "USD" + # }, + # ] + # } + # } + # + result = self.safe_dict(response, 'result', {}) + ledger = self.safe_list(result, 'data', []) + return self.parse_ledger(ledger, currency, since, limit) + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + # + # { + # "account_id": "ce075cef-1234-4321-bd6e-gf9007351e64", + # "event_date": "2023-06-15", + # "journal_type": "TRADING", + # "journal_id": "6530219460124075091", + # "transaction_qty": "6.0091224", + # "transaction_cost": "6.0091224", + # "realized_pnl": "0", + # "order_id": "6530219477766741833", + # "trade_id": "6530219495775954765", + # "trade_match_id": "4611686018455865176", + # "event_timestamp_ms": 1686804665013, + # "event_timestamp_ns": "1686804665013642422", + # "client_oid": "CCXT_d6ea7c5db6c1495aa8b758", + # "taker_side": "", + # "side": "BUY", + # "instrument_name": "USD" + # } + # + timestamp = self.safe_integer(item, 'event_timestamp_ms') + currencyId = self.safe_string(item, 'instrument_name') + code = self.safe_currency_code(currencyId, currency) + currency = self.safe_currency(currencyId, currency) + amount = self.safe_string(item, 'transaction_qty') + direction = None + if Precise.string_lt(amount, '0'): + direction = 'out' + amount = Precise.string_abs(amount) + else: + direction = 'in' + return self.safe_ledger_entry({ + 'info': item, + 'id': self.safe_string(item, 'order_id'), + 'direction': direction, + 'account': self.safe_string(item, 'account_id'), + 'referenceId': self.safe_string(item, 'trade_id'), + 'referenceAccount': self.safe_string(item, 'trade_match_id'), + 'type': self.parse_ledger_entry_type(self.safe_string(item, 'journal_type')), + 'currency': code, + 'amount': self.parse_number(amount), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'before': None, + 'after': None, + 'status': None, + 'fee': { + 'currency': None, + 'cost': None, + }, + }, currency) + + def parse_ledger_entry_type(self, type): + ledgerType: dict = { + 'TRADING': 'trade', + 'TRADE_FEE': 'fee', + 'WITHDRAW_FEE': 'fee', + 'WITHDRAW': 'withdrawal', + 'DEPOSIT': 'deposit', + 'ROLLBACK_WITHDRAW': 'rollback', + 'ROLLBACK_DEPOSIT': 'rollback', + 'FUNDING': 'fee', + 'REALIZED_PNL': 'trade', + 'INSURANCE_FUND': 'insurance', + 'SOCIALIZED_LOSS': 'trade', + 'LIQUIDATION_FEE': 'fee', + 'SESSION_RESET': 'reset', + 'ADJUSTMENT': 'adjustment', + 'SESSION_SETTLE': 'settlement', + 'UNCOVERED_LOSS': 'trade', + 'ADMIN_ADJUSTMENT': 'adjustment', + 'DELIST': 'delist', + 'SETTLEMENT_FEE': 'fee', + 'AUTO_CONVERSION': 'conversion', + 'MANUAL_CONVERSION': 'conversion', + } + return self.safe_string(ledgerType, type, type) + + async def fetch_accounts(self, params={}) -> List[Account]: + """ + fetch all the accounts associated with a profile + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#private-get-accounts + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `account structures ` indexed by the account type + """ + await self.load_markets() + response = await self.v1PrivatePostPrivateGetAccounts(params) + # + # { + # "id": 1234567894321, + # "method": "private/get-accounts", + # "code": 0, + # "result": { + # "master_account": { + # "uuid": "a1234abc-1234-4321-q5r7-b1ab0a0b12b", + # "user_uuid": "a1234abc-1234-4321-q5r7-b1ab0a0b12b", + # "enabled": True, + # "tradable": True, + # "name": "YOUR_NAME", + # "country_code": "CAN", + # "phone_country_code": "CAN", + # "incorp_country_code": "", + # "margin_access": "DEFAULT", + # "derivatives_access": "DEFAULT", + # "create_time": 1656445188000, + # "update_time": 1660794567262, + # "two_fa_enabled": True, + # "kyc_level": "ADVANCED", + # "suspended": False, + # "terminated": False, + # "spot_enabled": False, + # "margin_enabled": False, + # "derivatives_enabled": False + # }, + # "sub_account_list": [] + # } + # } + # + result = self.safe_dict(response, 'result', {}) + masterAccount = self.safe_dict(result, 'master_account', {}) + accounts = self.safe_list(result, 'sub_account_list', []) + accounts.append(masterAccount) + return self.parse_accounts(accounts, params) + + def parse_account(self, account): + # + # { + # "uuid": "a1234abc-1234-4321-q5r7-b1ab0a0b12b", + # "user_uuid": "a1234abc-1234-4321-q5r7-b1ab0a0b12b", + # "master_account_uuid": "a1234abc-1234-4321-q5r7-b1ab0a0b12b", + # "label": "FORMER_MASTER_MARGIN", + # "enabled": True, + # "tradable": True, + # "name": "YOUR_NAME", + # "country_code": "YOUR_COUNTRY_CODE", + # "incorp_country_code": "", + # "margin_access": "DEFAULT", + # "derivatives_access": "DEFAULT", + # "create_time": 1656481992000, + # "update_time": 1667272884594, + # "two_fa_enabled": False, + # "kyc_level": "ADVANCED", + # "suspended": False, + # "terminated": False, + # "spot_enabled": False, + # "margin_enabled": False, + # "derivatives_enabled": False, + # "system_label": "FORMER_MASTER_MARGIN" + # } + # + return { + 'id': self.safe_string(account, 'uuid'), + 'type': self.safe_string(account, 'label'), + 'code': None, + 'info': account, + } + + async def fetch_settlement_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical settlement records + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#public-get-expired-settlement-price + + :param str symbol: unified market symbol of the settlement history + :param int [since]: timestamp in ms + :param int [limit]: number of records + :param dict [params]: exchange specific params + :param int [params.type]: 'future', 'option' + :returns dict[]: a list of `settlement history objects ` + """ + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + type = None + type, params = self.handle_market_type_and_params('fetchSettlementHistory', market, params) + self.check_required_argument('fetchSettlementHistory', type, 'type', ['future', 'option', 'WARRANT', 'FUTURE']) + if type == 'option': + type = 'WARRANT' + request: dict = { + 'instrument_type': type.upper(), + } + response = await self.v1PublicGetPublicGetExpiredSettlementPrice(self.extend(request, params)) + # + # { + # "id": -1, + # "method": "public/get-expired-settlement-price", + # "code": 0, + # "result": { + # "data": [ + # { + # "i": "BTCUSD-230526", + # "x": 1685088000000, + # "v": "26464.1", + # "t": 1685087999500 + # } + # ] + # } + # } + # + result = self.safe_dict(response, 'result', {}) + data = self.safe_list(result, 'data', []) + settlements = self.parse_settlements(data, market) + sorted = self.sort_by(settlements, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, symbol, since, limit) + + def parse_settlement(self, settlement, market): + # + # { + # "i": "BTCUSD-230526", + # "x": 1685088000000, + # "v": "26464.1", + # "t": 1685087999500 + # } + # + timestamp = self.safe_integer(settlement, 'x') + marketId = self.safe_string(settlement, 'i') + return { + 'info': settlement, + 'symbol': self.safe_symbol(marketId, market), + 'price': self.safe_number(settlement, 'v'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + + def parse_settlements(self, settlements, market): + # + # [ + # { + # "i": "BTCUSD-230526", + # "x": 1685088000000, + # "v": "26464.1", + # "t": 1685087999500 + # } + # ] + # + result = [] + for i in range(0, len(settlements)): + result.append(self.parse_settlement(settlements[i], market)) + return result + + async def fetch_funding_rate(self, symbol: str, params={}): + """ + fetches historical funding rates + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#public-get-valuations + + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `funding rate structures ` + """ + await self.load_markets() + market = self.market(symbol) + if not market['swap']: + raise BadSymbol(self.id + ' fetchFundingRate() supports swap contracts only') + request: dict = { + 'instrument_name': market['id'], + 'valuation_type': 'estimated_funding_rate', + 'count': 1, + } + response = await self.v1PublicGetPublicGetValuations(self.extend(request, params)) + # + # { + # "id": -1, + # "method": "public/get-valuations", + # "code": 0, + # "result": { + # "data": [ + # { + # "v": "-0.000001884", + # "t": 1687892400000 + # }, + # ], + # "instrument_name": "BTCUSD-PERP" + # } + # } + # + result = self.safe_dict(response, 'result', {}) + data = self.safe_list(result, 'data', []) + entry = self.safe_dict(data, 0, {}) + return self.parse_funding_rate(entry, market) + + def parse_funding_rate(self, contract, market: Market = None) -> FundingRate: + # + # { + # "v": "-0.000001884", + # "t": 1687892400000 + # }, + # + timestamp = self.safe_integer(contract, 't') + fundingTimestamp = None + if timestamp is not None: + fundingTimestamp = int(math.ceil(timestamp / 3600000)) * 3600000 # end of the next hour + return { + 'info': contract, + 'symbol': self.safe_symbol(None, market), + 'markPrice': None, + 'indexPrice': None, + 'interestRate': None, + 'estimatedSettlePrice': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'fundingRate': self.safe_number(contract, 'v'), + 'fundingTimestamp': fundingTimestamp, + 'fundingDatetime': self.iso8601(fundingTimestamp), + 'nextFundingRate': None, + 'nextFundingTimestamp': None, + 'nextFundingDatetime': None, + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': '1h', + } + + async def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical funding rates + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#public-get-valuations + + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: timestamp in ms of the earliest funding rate to fetch + :param int [limit]: the maximum amount of [funding rate structures] to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms for the ending date filter, default is the current time + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `funding rate structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchFundingRateHistory() requires a symbol argument') + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchFundingRateHistory', 'paginate') + if paginate: + return await self.fetch_paginated_call_deterministic('fetchFundingRateHistory', symbol, since, limit, '8h', params) + market = self.market(symbol) + if not market['swap']: + raise BadSymbol(self.id + ' fetchFundingRateHistory() supports swap contracts only') + request: dict = { + 'instrument_name': market['id'], + 'valuation_type': 'funding_hist', + } + if since is not None: + request['start_ts'] = since + if limit is not None: + request['count'] = limit + until = self.safe_integer(params, 'until') + params = self.omit(params, ['until']) + if until is not None: + request['end_ts'] = until + response = await self.v1PublicGetPublicGetValuations(self.extend(request, params)) + # + # { + # "id": -1, + # "method": "public/get-valuations", + # "code": 0, + # "result": { + # "data": [ + # { + # "v": "-0.000001884", + # "t": 1687892400000 + # }, + # ], + # "instrument_name": "BTCUSD-PERP" + # } + # } + # + result = self.safe_dict(response, 'result', {}) + data = self.safe_list(result, 'data', []) + marketId = self.safe_string(result, 'instrument_name') + rates = [] + for i in range(0, len(data)): + entry = data[i] + timestamp = self.safe_integer(entry, 't') + rates.append({ + 'info': entry, + 'symbol': self.safe_symbol(marketId, market), + 'fundingRate': self.safe_number(entry, 'v'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + sorted = self.sort_by(rates, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, market['symbol'], since, limit) + + async def fetch_position(self, symbol: str, params={}): + """ + fetch data on a single open contract trade position + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#private-get-positions + + :param str symbol: unified market symbol of the market the position is held in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `position structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'instrument_name': market['id'], + } + response = await self.v1PrivatePostPrivateGetPositions(self.extend(request, params)) + # + # { + # "id": 1688015952050, + # "method": "private/get-positions", + # "code": 0, + # "result": { + # "data": [ + # { + # "account_id": "ce075bef-b600-4277-bd6e-ff9007251e63", + # "quantity": "0.0001", + # "cost": "3.02392", + # "open_pos_cost": "3.02392", + # "open_position_pnl": "-0.0010281328", + # "session_pnl": "-0.0010281328", + # "update_timestamp_ms": 1688015919091, + # "instrument_name": "BTCUSD-PERP", + # "type": "PERPETUAL_SWAP" + # } + # ] + # } + # } + # + result = self.safe_dict(response, 'result', {}) + data = self.safe_list(result, 'data', []) + return self.parse_position(self.safe_dict(data, 0), market) + + async def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#private-get-positions + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + request: dict = {} + market = None + if symbols is not None: + symbol = None + if isinstance(symbols, list): + symbolsLength = len(symbols) + if symbolsLength > 1: + raise BadRequest(self.id + ' fetchPositions() symbols argument cannot contain more than 1 symbol') + symbol = symbols[0] + else: + symbol = symbols + market = self.market(symbol) + request['instrument_name'] = market['id'] + response = await self.v1PrivatePostPrivateGetPositions(self.extend(request, params)) + # + # { + # "id": 1688015952050, + # "method": "private/get-positions", + # "code": 0, + # "result": { + # "data": [ + # { + # "account_id": "ce075bef-b600-4277-bd6e-ff9007251e63", + # "quantity": "0.0001", + # "cost": "3.02392", + # "open_pos_cost": "3.02392", + # "open_position_pnl": "-0.0010281328", + # "session_pnl": "-0.0010281328", + # "update_timestamp_ms": 1688015919091, + # "instrument_name": "BTCUSD-PERP", + # "type": "PERPETUAL_SWAP" + # } + # ] + # } + # } + # + responseResult = self.safe_dict(response, 'result', {}) + positions = self.safe_list(responseResult, 'data', []) + result = [] + for i in range(0, len(positions)): + entry = positions[i] + marketId = self.safe_string(entry, 'instrument_name') + marketInner = self.safe_market(marketId, None, None, 'contract') + result.append(self.parse_position(entry, marketInner)) + return self.filter_by_array_positions(result, 'symbol', None, False) + + def parse_position(self, position: dict, market: Market = None): + # + # { + # "account_id": "ce075bef-b600-4277-bd6e-ff9007251e63", + # "quantity": "0.0001", + # "cost": "3.02392", + # "open_pos_cost": "3.02392", + # "open_position_pnl": "-0.0010281328", + # "session_pnl": "-0.0010281328", + # "update_timestamp_ms": 1688015919091, + # "instrument_name": "BTCUSD-PERP", + # "type": "PERPETUAL_SWAP" + # } + # + marketId = self.safe_string(position, 'instrument_name') + market = self.safe_market(marketId, market, None, 'contract') + symbol = self.safe_symbol(marketId, market, None, 'contract') + timestamp = self.safe_integer(position, 'update_timestamp_ms') + amount = self.safe_string(position, 'quantity') + return self.safe_position({ + 'info': position, + 'id': None, + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'hedged': None, + 'side': 'buy' if Precise.string_gt(amount, '0') else 'sell', + 'contracts': Precise.string_abs(amount), + 'contractSize': market['contractSize'], + 'entryPrice': None, + 'markPrice': None, + 'notional': None, + 'leverage': None, + 'collateral': self.safe_number(position, 'open_pos_cost'), + 'initialMargin': self.safe_number(position, 'cost'), + 'maintenanceMargin': None, + 'initialMarginPercentage': None, + 'maintenanceMarginPercentage': None, + 'unrealizedPnl': self.safe_number(position, 'open_position_pnl'), + 'liquidationPrice': None, + 'marginMode': None, + 'percentage': None, + 'marginRatio': None, + 'stopLossPrice': None, + 'takeProfitPrice': None, + }) + + def nonce(self): + return self.milliseconds() + + def params_to_string(self, object, level): + maxLevel = 3 + if level >= maxLevel: + return str(object) + if isinstance(object, str): + return object + returnString = '' + paramsKeys = None + if isinstance(object, list): + paramsKeys = object + else: + sorted = self.keysort(object) + paramsKeys = list(sorted.keys()) + for i in range(0, len(paramsKeys)): + key = paramsKeys[i] + returnString += key + value = object[key] + if value == 'None': + returnString += 'None' + elif isinstance(value, list): + for j in range(0, len(value)): + returnString += self.params_to_string(value[j], level + 1) + else: + returnString += str(value) + return returnString + + async def close_position(self, symbol: str, side: OrderSide = None, params={}) -> Order: + """ + closes open positions for a market + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#private-close-position + + :param str symbol: Unified CCXT market symbol + :param str [side]: not used by cryptocom.closePositions + :param dict [params]: extra parameters specific to the okx api endpoint + + EXCHANGE SPECIFIC PARAMETERS + :param str [params.type]: LIMIT or MARKET + :param number [params.price]: for limit orders only + :returns dict[]: `A list of position structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'instrument_name': market['id'], + 'type': 'MARKET', + } + type = self.safe_string_upper(params, 'type') + price = self.safe_string(params, 'price') + if type is not None: + request['type'] = type + if price is not None: + request['price'] = self.price_to_precision(market['symbol'], price) + response = await self.v1PrivatePostPrivateClosePosition(self.extend(request, params)) + # + # { + # "id" : 1700830813298, + # "method" : "private/close-position", + # "code" : 0, + # "result" : { + # "client_oid" : "179a909d-5614-655b-0d0e-9e85c9a25c85", + # "order_id" : "6142909897021751347" + # } + # } + # + result = self.safe_dict(response, 'result') + return self.parse_order(result, market) + + async def fetch_trading_fee(self, symbol: str, params={}) -> TradingFeeInterface: + """ + fetch the trading fees for a market + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#private-get-instrument-fee-rate + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `fee structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'instrument_name': market['id'], + } + response = await self.v1PrivatePostPrivateGetInstrumentFeeRate(self.extend(request, params)) + # + # { + # "id": 1, + # "code": 0, + # "method": "private/staking/unstake", + # "result": { + # "staking_id": "1", + # "instrument_name": "SOL.staked", + # "status": "NEW", + # "quantity": "1", + # "underlying_inst_name": "SOL", + # "reason": "NO_ERROR" + # } + # } + # + data = self.safe_dict(response, 'result', {}) + return self.parse_trading_fee(data, market) + + async def fetch_trading_fees(self, params={}) -> TradingFees: + """ + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#private-get-fee-rate + + fetch the trading fees for multiple markets + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + await self.load_markets() + response = await self.v1PrivatePostPrivateGetFeeRate(params) + # + # { + # "id": 1, + # "method": "/private/get-fee-rate", + # "code": 0, + # "result": { + # "spot_tier": "3", + # "deriv_tier": "3", + # "effective_spot_maker_rate_bps": "6.5", + # "effective_spot_taker_rate_bps": "6.9", + # "effective_deriv_maker_rate_bps": "1.1", + # "effective_deriv_taker_rate_bps": "3" + # } + # } + # + result = self.safe_dict(response, 'result', {}) + return self.parse_trading_fees(result) + + def parse_trading_fees(self, response): + # + # { + # "spot_tier": "3", + # "deriv_tier": "3", + # "effective_spot_maker_rate_bps": "6.5", + # "effective_spot_taker_rate_bps": "6.9", + # "effective_deriv_maker_rate_bps": "1.1", + # "effective_deriv_taker_rate_bps": "3" + # } + # + result: dict = {} + result['info'] = response + for i in range(0, len(self.symbols)): + symbol = self.symbols[i] + market = self.market(symbol) + isSwap = market['swap'] + takerFeeKey = 'effective_deriv_taker_rate_bps' if isSwap else 'effective_spot_taker_rate_bps' + makerFeeKey = 'effective_deriv_maker_rate_bps' if isSwap else 'effective_spot_maker_rate_bps' + tradingFee = { + 'info': response, + 'symbol': symbol, + 'maker': self.parse_number(Precise.string_div(self.safe_string(response, makerFeeKey), '10000')), + 'taker': self.parse_number(Precise.string_div(self.safe_string(response, takerFeeKey), '10000')), + 'percentage': None, + 'tierBased': None, + } + result[symbol] = tradingFee + return result + + def parse_trading_fee(self, fee: dict, market: Market = None) -> TradingFeeInterface: + # + # { + # "instrument_name": "BTC_USD", + # "effective_maker_rate_bps": "6.5", + # "effective_taker_rate_bps": "6.9" + # } + # + marketId = self.safe_string(fee, 'instrument_name') + symbol = self.safe_symbol(marketId, market) + return { + 'info': fee, + 'symbol': symbol, + 'maker': self.parse_number(Precise.string_div(self.safe_string(fee, 'effective_maker_rate_bps'), '10000')), + 'taker': self.parse_number(Precise.string_div(self.safe_string(fee, 'effective_taker_rate_bps'), '10000')), + 'percentage': None, + 'tierBased': None, + } + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + type = self.safe_string(api, 0) + access = self.safe_string(api, 1) + url = self.urls['api'][type] + '/' + path + query = self.omit(params, self.extract_params(path)) + if access == 'public': + if query: + url += '?' + self.urlencode(query) + else: + self.check_required_credentials() + nonce = str(self.nonce()) + requestParams = self.extend({}, params) + paramsKeys = list(requestParams.keys()) + strSortKey = self.params_to_string(requestParams, 0) + payload = path + nonce + self.apiKey + strSortKey + nonce + signature = self.hmac(self.encode(payload), self.encode(self.secret), hashlib.sha256) + paramsKeysLength = len(paramsKeys) + body = self.json({ + 'id': nonce, + 'method': path, + 'params': params, + 'api_key': self.apiKey, + 'sig': signature, + 'nonce': nonce, + }) + # fix issue https://github.com/ccxt/ccxt/issues/11179 + # php always encodes dictionaries + # if an array is empty, php will put it in square brackets + # python and js will put it in curly brackets + # the code below checks and replaces those brackets in empty requests + if paramsKeysLength == 0: + paramsString = '{}' + arrayString = '[]' + body = body.replace(arrayString, paramsString) + headers = { + 'Content-Type': 'application/json', + } + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + errorCode = self.safe_string(response, 'code') + if errorCode != '0': + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + raise ExchangeError(self.id + ' ' + body) + return None diff --git a/ccxt/async_support/cryptomus.py b/ccxt/async_support/cryptomus.py new file mode 100644 index 0000000..b98d23e --- /dev/null +++ b/ccxt/async_support/cryptomus.py @@ -0,0 +1,1137 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.cryptomus import ImplicitAPI +from ccxt.base.types import Any, Balances, Currencies, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, TradingFees +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class cryptomus(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(cryptomus, self).describe(), { + 'id': 'cryptomus', + 'name': 'Cryptomus', + 'countries': ['CA'], + 'rateLimit': 100, # todo check + 'version': 'v2', + 'certified': False, + 'pro': False, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelAllOrders': False, + 'cancelAllOrdersAfter': False, + 'cancelOrder': True, + 'cancelOrders': False, + 'cancelWithdraw': False, + 'closeAllPositions': False, + 'closePosition': False, + 'createConvertTrade': False, + 'createDepositAddress': False, + 'createMarketBuyOrderWithCost': False, + 'createMarketOrder': False, + 'createMarketOrderWithCost': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createOrderWithTakeProfitAndStopLoss': False, + 'createOrderWithTakeProfitAndStopLossWs': False, + 'createPostOnlyOrder': False, + 'createReduceOnlyOrder': False, + 'createStopLimitOrder': False, + 'createStopLossOrder': False, + 'createStopMarketOrder': False, + 'createStopOrder': False, + 'createTakeProfitOrder': False, + 'createTrailingAmountOrder': False, + 'createTrailingPercentOrder': False, + 'createTriggerOrder': False, + 'fetchAccounts': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchCanceledAndClosedOrders': True, + 'fetchCanceledOrders': False, + 'fetchClosedOrder': False, + 'fetchClosedOrders': False, + 'fetchConvertCurrencies': False, + 'fetchConvertQuote': False, + 'fetchConvertTrade': False, + 'fetchConvertTradeHistory': False, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': False, + 'fetchDeposits': False, + 'fetchDepositsWithdrawals': False, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLedger': False, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': False, + 'fetchOHLCV': False, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrder': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': False, + 'fetchOrderTrades': False, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchStatus': False, + 'fetchTicker': False, + 'fetchTickers': True, + 'fetchTime': False, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': True, + 'fetchTransactions': False, + 'fetchTransfers': False, + 'fetchVolatilityHistory': False, + 'fetchWithdrawals': False, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'repayMargin': False, + 'sandbox': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'transfer': False, + 'withdraw': False, + }, + 'timeframes': {}, + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/8e0b1c48-7c01-4177-9224-f1b01d89d7e7', + 'api': { + 'public': 'https://api.cryptomus.com', + 'private': 'https://api.cryptomus.com', + }, + 'www': 'https://cryptomus.com', + 'doc': 'https://doc.cryptomus.com/personal', + 'fees': 'https://cryptomus.com/tariffs', # todo check + 'referral': 'https://app.cryptomus.com/signup/?ref=JRP4yj', # todo + }, + 'api': { + 'public': { + 'get': { + 'v2/user-api/exchange/markets': 1, # done + 'v2/user-api/exchange/market/price': 1, # not used + 'v1/exchange/market/assets': 1, # done + 'v1/exchange/market/order-book/{currencyPair}': 1, # done + 'v1/exchange/market/tickers': 1, # done + 'v1/exchange/market/trades/{currencyPair}': 1, # done + }, + }, + 'private': { + 'get': { + 'v2/user-api/exchange/orders': 1, # done + 'v2/user-api/exchange/orders/history': 1, # done + 'v2/user-api/exchange/account/balance': 1, # done + 'v2/user-api/exchange/account/tariffs': 1, # done + 'v2/user-api/payment/services': 1, + 'v2/user-api/payout/services': 1, + 'v2/user-api/transaction/list': 1, + }, + 'post': { + 'v2/user-api/exchange/orders': 1, # done + 'v2/user-api/exchange/orders/market': 1, # done + }, + 'delete': { + 'v2/user-api/exchange/orders/{orderId}': 1, # done + }, + }, + }, + 'fees': { + 'trading': { + 'percentage': True, + 'feeSide': 'get', + 'maker': self.parse_number('0.02'), + 'taker': self.parse_number('0.02'), + }, + }, + 'options': { + 'createMarketBuyOrderRequiresPrice': True, + 'networks': { + 'BEP20': 'bsc', + 'DASH': 'dash', + 'POLYGON': 'polygon', + 'ARB': 'arbitrum', + 'SOL': 'sol', + 'TON': 'ton', + 'ERC20': 'eth', + 'TRC20': 'tron', + 'LTC': 'ltc', + 'XMR': 'xmr', + 'BCH': 'bch', + 'DOGE': 'doge', + 'AVAX': 'avalanche', + 'BTC': 'btc', + 'RUB': 'rub', + }, + 'networksById': { + 'bsc': 'BEP20', + 'dash': 'DASH', + 'polygon': 'POLYGON', + 'arbitrum': 'ARB', + 'sol': 'SOL', + 'ton': 'TON', + 'eth': 'ERC20', + 'tron': 'TRC20', + 'ltc': 'LTC', + 'xmr': 'XMR', + 'bch': 'BCH', + 'doge': 'DOGE', + 'avalanche': 'AVAX', + 'btc': 'BTC', + 'rub': 'RUB', + }, + 'fetchOrderBook': { + 'level': 0, # 0, 1, 2, 4 or 5 + }, + }, + 'commonCurrencies': {}, + 'exceptions': { + 'exact': { + '500': ExchangeError, + '6': InsufficientFunds, # {"code":6,"message":"Insufficient funds."} + 'Insufficient funds.': InsufficientFunds, + 'Minimum amount 15 USDT': InvalidOrder, + # {"code":500,"message":"Server error."} + # {"message":"Minimum amount 15 USDT","state":1} + # {"message":"Insufficient funds. USDT wallet balance is 35.21617400.","state":1} + }, + 'broad': {}, + }, + 'precisionMode': TICK_SIZE, + 'requiredCredentials': { + 'apiKey': False, + 'uid': True, + }, + 'features': {}, + }) + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for the exchange + + https://doc.cryptomus.com/personal/market-cap/tickers + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = await self.publicGetV2UserApiExchangeMarkets(params) + # + # { + # "result": [ + # { + # "id": "01JHN5EFT64YC4HR9KCGM5M65D", + # "symbol": "POL_USDT", + # "baseCurrency": "POL", + # "quoteCurrency": "USDT", + # "baseMinSize": "1.00000000", + # "quoteMinSize": "5.00000000", + # "baseMaxSize": "50000.00000000", + # "quoteMaxSize": "10000000000.00000000", + # "basePrec": "1", + # "quotePrec": "4" + # }, + # ... + # ] + # } + # + result = self.safe_list(response, 'result', []) + return self.parse_markets(result) + + def parse_market(self, market: dict) -> Market: + # + # { + # "id": "01JHN5EFT64YC4HR9KCGM5M65D", + # "symbol": "POL_USDT", + # "baseCurrency": "POL", + # "quoteCurrency": "USDT", + # "baseMinSize": "1.00000000", + # "quoteMinSize": "5.00000000", + # "baseMaxSize": "50000.00000000", + # "quoteMaxSize": "10000000000.00000000", + # "basePrec": "1", + # "quotePrec": "4" + # } + # + marketId = self.safe_string(market, 'symbol') + parts = marketId.split('_') + baseId = parts[0] + quoteId = parts[1] + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + fees = self.safe_dict(self.fees, 'trading') + return self.safe_market_structure({ + 'id': marketId, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'baseId': baseId, + 'quoteId': quoteId, + 'active': True, + 'type': 'spot', + 'subType': None, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'contract': False, + 'settle': None, + 'settleId': None, + 'contractSize': None, + 'linear': None, + 'inverse': None, + 'taker': self.safe_number(fees, 'taker'), + 'maker': self.safe_number(fees, 'maker'), + 'percentage': self.safe_bool(fees, 'percentage'), + 'tierBased': None, + 'feeSide': self.safe_string(fees, 'feeSide'), + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(self.parse_precision(self.safe_string(market, 'quotePrec'))), + 'price': self.parse_number(self.parse_precision(self.safe_string(market, 'basePrec'))), + }, + 'limits': { + 'amount': { + 'min': self.safe_number(market, 'quoteMinSize'), + 'max': self.safe_number(market, 'quoteMaxSize'), + }, + 'price': { + 'min': self.safe_number(market, 'baseMinSize'), + 'max': self.safe_number(market, 'baseMaxSize'), + }, + 'leverage': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + 'info': market, + }) + + async def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://doc.cryptomus.com/personal/market-cap/assets + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = await self.publicGetV1ExchangeMarketAssets(params) + # + # { + # 'state': '0', + # 'result': [ + # { + # 'currency_code': 'USDC', + # 'network_code': 'bsc', + # 'can_withdraw': True, + # 'can_deposit': True, + # 'min_withdraw': '1.00000000', + # 'max_withdraw': '10000000.00000000', + # 'max_deposit': '10000000.00000000', + # 'min_deposit': '1.00000000' + # }, + # ... + # ] + # } + # + coins = self.safe_list(response, 'result') + groupedById = self.group_by(coins, 'currency_code') + keys = list(groupedById.keys()) + result: dict = {} + for i in range(0, len(keys)): + id = keys[i] + code = self.safe_currency_code(id) + networks = {} + networkEntries = groupedById[id] + for j in range(0, len(networkEntries)): + networkEntry = networkEntries[j] + networkId = self.safe_string(networkEntry, 'network_code') + networkCode = self.network_id_to_code(networkId) + networks[networkCode] = { + 'id': networkId, + 'network': networkCode, + 'limits': { + 'withdraw': { + 'min': self.safe_number(networkEntry, 'min_withdraw'), + 'max': self.safe_number(networkEntry, 'max_withdraw'), + }, + 'deposit': { + 'min': self.safe_number(networkEntry, 'min_deposit'), + 'max': self.safe_number(networkEntry, 'max_deposit'), + }, + }, + 'active': None, + 'deposit': self.safe_bool(networkEntry, 'can_withdraw'), + 'withdraw': self.safe_bool(networkEntry, 'can_deposit'), + 'fee': None, + 'precision': None, + 'info': networkEntry, + } + result[code] = self.safe_currency_structure({ + 'id': id, + 'code': code, + 'networks': networks, + 'info': networkEntries, + }) + return result + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://doc.cryptomus.com/personal/market-cap/tickers + + :param str[] [symbols]: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + response = await self.publicGetV1ExchangeMarketTickers(params) + # + # { + # "data": [ + # { + # "currency_pair": "MATIC_USDT", + # "last_price": "0.342", + # "base_volume": "1676.84092771", + # "quote_volume": "573.48033609043" + # }, + # ... + # } + # + data = self.safe_list(response, 'data') + return self.parse_tickers(data, symbols) + + def parse_ticker(self, ticker, market: Market = None) -> Ticker: + # + # { + # "currency_pair": "XMR_USDT", + # "last_price": "158.04829772", + # "base_volume": "0.35185785", + # "quote_volume": "55.523761128544" + # } + # + marketId = self.safe_string(ticker, 'currency_pair') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + last = self.safe_string(ticker, 'last_price') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': None, + 'datetime': None, + 'high': None, + 'low': None, + 'bid': None, + 'bidVolume': None, + 'ask': None, + 'askVolume': None, + 'vwap': None, + 'open': None, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': self.safe_string(ticker, 'base_volume'), + 'quoteVolume': self.safe_string(ticker, 'quote_volume'), + 'info': ticker, + }, market) + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://doc.cryptomus.com/personal/market-cap/orderbook + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.level]: 0 or 1 or 2 or 3 or 4 or 5 - the level of volume + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'currencyPair': market['id'], + } + level = 0 + level, params = self.handle_option_and_params(params, 'fetchOrderBook', 'level', level) + request['level'] = level + response = await self.publicGetV1ExchangeMarketOrderBookCurrencyPair(self.extend(request, params)) + # + # { + # "data": { + # "timestamp": "1730138702", + # "bids": [ + # { + # "price": "2250.00", + # "quantity": "1.00000" + # } + # ], + # "asks": [ + # { + # "price": "2428.69", + # "quantity": "0.16470" + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + timestamp = self.safe_timestamp(data, 'timestamp') + return self.parse_order_book(data, symbol, timestamp, 'bids', 'asks', 'price', 'quantity') + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://doc.cryptomus.com/personal/market-cap/trades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch(maximum value is 100) + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'currencyPair': market['id'], + } + response = await self.publicGetV1ExchangeMarketTradesCurrencyPair(self.extend(request, params)) + # + # { + # "data": [ + # { + # "trade_id": "01J829C3RAXHXHR09HABGQ1YAT", + # "price": "2315.6320500000000000", + # "base_volume": "21.9839623057260000", + # "quote_volume": "0.0094937200000000", + # "timestamp": 1726653796, + # "type": "sell" + # } + # ] + # } + # + data = self.safe_list(response, 'data') + return self.parse_trades(data, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # { + # "trade_id": "01J017Q6B3JGHZRP9D2NZHVKFX", + # "price": "59498.63487492", + # "base_volume": "94.00784310", + # "quote_volume": "0.00158000", + # "timestamp": 1718028573, + # "type": "sell" + # } + # + timestamp = self.safe_timestamp(trade, 'timestamp') + return self.safe_trade({ + 'id': self.safe_string(trade, 'trade_id'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'side': self.safe_string(trade, 'type'), + 'price': self.safe_string(trade, 'price'), + 'amount': self.safe_string(trade, 'quote_volume'), # quote_volume is amount + 'cost': self.safe_string(trade, 'base_volume'), # base_volume is cost + 'takerOrMaker': None, + 'type': None, + 'order': None, + 'fee': { + 'currency': None, + 'cost': None, + }, + 'info': trade, + }, market) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://doc.cryptomus.com/personal/converts/balance + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + request: dict = {} + response = await self.privateGetV2UserApiExchangeAccountBalance(self.extend(request, params)) + # + # { + # "result": [ + # { + # "ticker": "AVAX", + # "available": "0.00000000", + # "held": "0.00000000" + # } + # ] + # } + # + result = self.safe_list(response, 'result', []) + return self.parse_balance(result) + + def parse_balance(self, balance) -> Balances: + # + # { + # "ticker": "AVAX", + # "available": "0.00000000", + # "held": "0.00000000" + # } + # + result: dict = { + 'info': balance, + } + for i in range(0, len(balance)): + balanceEntry = balance[i] + currencyId = self.safe_string(balanceEntry, 'ticker') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(balanceEntry, 'available') + account['used'] = self.safe_string(balanceEntry, 'held') + result[code] = account + return self.safe_balance(result) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}) -> Order: + """ + create a trade order + + https://doc.cryptomus.com/personal/exchange/market-order-creation + https://doc.cryptomus.com/personal/exchange/limit-order-creation + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' or for spot + :param str side: 'buy' or 'sell' + :param float amount: how much of you want to trade in units of the base currency + :param float [price]: the price that the order is to be fulfilled, in units of the quote currency, ignored in market orders(only for limit orders) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.cost]: *market buy only* the quote quantity that can be used alternative for the amount + :param str [params.clientOrderId]: a unique identifier for the order(optional) + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + 'direction': side, + 'tag': 'ccxt', + } + clientOrderId = self.safe_string(params, 'clientOrderId') + if clientOrderId is not None: + params = self.omit(params, 'clientOrderId') + request['client_order_id'] = clientOrderId + sideBuy = side == 'buy' + amountToString = self.number_to_string(amount) + priceToString = self.number_to_string(price) + cost = None + cost, params = self.handle_param_string(params, 'cost') + response = None + if type == 'market': + if sideBuy: + createMarketBuyOrderRequiresPrice = True + createMarketBuyOrderRequiresPrice, params = self.handle_option_and_params(params, 'createOrder', 'createMarketBuyOrderRequiresPrice', True) + if createMarketBuyOrderRequiresPrice: + if (price is None) and (cost is None): + raise InvalidOrder(self.id + ' createOrder() requires the price argument for market buy orders to calculate the total cost to spend(amount * price), alternatively set the createMarketBuyOrderRequiresPrice option of param to False and pass the cost to spend in the amount argument') + elif cost is None: + cost = Precise.string_mul(amountToString, priceToString) + else: + cost = cost if cost else amountToString + request['value'] = cost + else: + request['quantity'] = amountToString + response = await self.privatePostV2UserApiExchangeOrdersMarket(self.extend(request, params)) + elif type == 'limit': + if price is None: + raise ArgumentsRequired(self.id + ' createOrder() requires a price parameter for a ' + type + ' order') + request['quantity'] = amountToString + request['price'] = price + response = await self.privatePostV2UserApiExchangeOrders(self.extend(request, params)) + else: + raise ArgumentsRequired(self.id + ' createOrder() requires a type parameter(limit or market)') + # + # { + # "order_id": "01JEXAFCCC5ZVJPZAAHHDKQBNG" + # } + # + return self.parse_order(response, market) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open limit order + + https://doc.cryptomus.com/personal/exchange/limit-order-cancellation + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in(not used in cryptomus) + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + request: dict = {} + request['orderId'] = id + response = await self.privateDeleteV2UserApiExchangeOrdersOrderId(self.extend(request, params)) + # + # { + # "success": True + # } + # + return self.safe_order({'info': response}) + + async def fetch_canceled_and_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://doc.cryptomus.com/personal/exchange/history-of-completed-orders + + :param str symbol: unified market symbol of the market orders were made in(not used in cryptomus) + :param int [since]: the earliest time in ms to fetch orders for(not used in cryptomus) + :param int [limit]: the maximum number of order structures to retrieve(not used in cryptomus) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.direction]: order direction 'buy' or 'sell' + :param str [params.order_id]: order id + :param str [params.client_order_id]: client order id + :param str [params.limit]: A special parameter that sets the maximum number of records the request will return + :param str [params.offset]: A special parameter that sets the number of records from the beginning of the list + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['market'] = market['id'] + if limit is not None: + request['limit'] = limit + response = await self.privateGetV2UserApiExchangeOrdersHistory(self.extend(request, params)) + # + # { + # "result": [ + # { + # "id": "01JEXAPY04JDFBVFC2D23BCKMK", + # "type": "market", + # "direction": "sell", + # "symbol": "TRX_USDT", + # "quantity": "67.5400000000000000", + # "filledQuantity": "67.5400000000000000", + # "filledValue": "20.0053480000000000", + # "state": "completed", + # "internalState": "filled", + # "createdAt": "2024-12-12 11:40:19", + # "finishedAt": "2024-12-12 11:40:21", + # "deal": { + # "id": "01JEXAPZ9C9TWENPFZJASZ1YD2", + # "state": "completed", + # "createdAt": "2024-12-12 11:40:21", + # "completedAt": "2024-12-12 11:40:21", + # "averageFilledPrice": "0.2962000000000000", + # "transactions": [ + # { + # "id": "01JEXAPZ9C9TWENPFZJASZ1YD3", + # "tradeRole": "taker", + # "filledPrice": "0.2962000000000000", + # "filledQuantity": "67.5400000000000000", + # "filledValue": "20.0053480000000000", + # "fee": "0.0000000000000000", + # "feeCurrency": "USDT", + # "committedAt": "2024-12-12 11:40:21" + # } + # ] + # } + # }, + # ... + # ] + # } + # + result = self.safe_list(response, 'result', []) + orders = [] + for i in range(0, len(result)): + order = result[i] + orders.append(self.parse_order(order, market)) + return orders + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://doc.cryptomus.com/personal/exchange/list-of-active-orders + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for(not used in cryptomus) + :param int [limit]: the maximum number of open orders structures to retrieve(not used in cryptomus) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.direction]: order direction 'buy' or 'sell' + :param str [params.order_id]: order id + :param str [params.client_order_id]: client order id + :param str [params.limit]: A special parameter that sets the maximum number of records the request will return + :param str [params.offset]: A special parameter that sets the number of records from the beginning of the list + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + request: dict = { + } + if market is not None: + request['market'] = market['id'] + response = await self.privateGetV2UserApiExchangeOrders(self.extend(request, params)) + # + # { + # "result": [ + # { + # "id": "01JFFG72CBRDP68K179KC9DSTG", + # "direction": "sell", + # "symbol": "BTC_USDT", + # "price": "102.0130000000000000", + # "quantity": "0.0005000000000000", + # "value": "0.0510065000000000", + # "filledQuantity": "0.0000000000000000", + # "filledValue": "0.0000000000000000", + # "createdAt": "2024-12-19 09:02:51", + # "clientOrderId": "987654321", + # "stopLossPrice": "101.12" + # }, + # ... + # ] + # } + result = self.safe_list(response, 'result', []) + return self.parse_orders(result, market, None, None) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder + # { + # "order_id": "01JEXAFCCC5ZVJPZAAHHDKQBNG" + # } + # + # fetchOrders + # { + # "id": "01JEXAPY04JDFBVFC2D23BCKMK", + # "type": "market", + # "direction": "sell", + # "symbol": "TRX_USDT", + # "quantity": "67.5400000000000000", + # "filledQuantity": "67.5400000000000000", + # "filledValue": "20.0053480000000000", + # "state": "completed", + # "internalState": "filled", + # "createdAt": "2024-12-12 11:40:19", + # "finishedAt": "2024-12-12 11:40:21", + # "deal": { + # "id": "01JEXAPZ9C9TWENPFZJASZ1YD2", + # "state": "completed", + # "createdAt": "2024-12-12 11:40:21", + # "completedAt": "2024-12-12 11:40:21", + # "averageFilledPrice": "0.2962000000000000", + # "transactions": [ + # { + # "id": "01JEXAPZ9C9TWENPFZJASZ1YD3", + # "tradeRole": "taker", + # "filledPrice": "0.2962000000000000", + # "filledQuantity": "67.5400000000000000", + # "filledValue": "20.0053480000000000", + # "fee": "0.0000000000000000", + # "feeCurrency": "USDT", + # "committedAt": "2024-12-12 11:40:21" + # } + # ] + # } + # }, + # ... + # + # fetchOpenOrders + # { + # "id": "01JFFG72CBRDP68K179KC9DSTG", + # "direction": "sell", + # "symbol": "BTC_USDT", + # "price": "102.0130000000000000", + # "quantity": "0.0005000000000000", + # "value": "0.0510065000000000", + # "filledQuantity": "0.0000000000000000", + # "filledValue": "0.0000000000000000", + # "createdAt": "2024-12-19 09:02:51", + # "clientOrderId": "987654321", + # "stopLossPrice": "101.12" + # } + # + id = self.safe_string_2(order, 'order_id', 'id') + marketId = self.safe_string(order, 'symbol') + market = self.safe_market(marketId, market) + dateTime = self.safe_string(order, 'createdAt') + timestamp = self.parse8601(dateTime) + deal = self.safe_dict(order, 'deal', {}) + averageFilledPrice = self.safe_number(deal, 'averageFilledPrice') + type = self.safe_string(order, 'type') + side = self.safe_string(order, 'direction') + price = self.safe_number(order, 'price') + transaction = self.safe_list(deal, 'transactions', []) + fee = None + firstTx = self.safe_dict(transaction, 0) + feeCurrency = self.safe_string(firstTx, 'feeCurrency') + if feeCurrency is not None: + fee = { + 'currency': self.safe_currency_code(feeCurrency), + 'cost': self.safe_number(firstTx, 'fee'), + } + if price is None: + price = self.safe_number(firstTx, 'filledPrice') + amount = self.safe_number(order, 'quantity') + cost = self.safe_number(order, 'value') + status = self.parse_order_status(self.safe_string(order, 'state')) + clientOrderId = self.safe_string(order, 'clientOrderId') + return self.safe_order({ + 'id': id, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'symbol': market['symbol'], + 'type': type, + 'timeInForce': None, + 'postOnly': None, + 'side': side, + 'price': price, + 'stopPrice': self.safe_string(order, 'stopLossPrice'), + 'triggerPrice': self.safe_string(order, 'stopLossPrice'), + 'amount': amount, + 'cost': cost, + 'average': averageFilledPrice, + 'filled': self.safe_string(order, 'filledQuantity'), + 'remaining': None, + 'status': status, + 'fee': fee, + 'trades': None, + 'info': order, + }, market) + + def parse_order_status(self, status: Str = None) -> Str: + statuses = { + 'active': 'open', + 'completed': 'closed', + 'partially_completed': 'open', + 'cancelled': 'canceled', + 'expired': 'expired', + 'failed': 'failed', + } + return self.safe_string(statuses, status, status) + + async def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://trade-docs.coinlist.co/?javascript--nodejs#list-fees + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + response = await self.privateGetV2UserApiExchangeAccountTariffs(params) + # + # { + # result: { + # equivalent_currency_code: 'USD', + # current_tariff_step: { + # step: '0', + # from_turnover: '0.00000000', + # maker_percent: '0.08', + # taker_percent: '0.1' + # }, + # tariff_steps: [ + # { + # step: '0', + # from_turnover: '0.00000000', + # maker_percent: '0.08', + # taker_percent: '0.1' + # }, + # { + # step: '1', + # from_turnover: '100001.00000000', + # maker_percent: '0.06', + # taker_percent: '0.095' + # }, + # { + # step: '2', + # from_turnover: '250001.00000000', + # maker_percent: '0.055', + # taker_percent: '0.085' + # }, + # { + # step: '3', + # from_turnover: '500001.00000000', + # maker_percent: '0.05', + # taker_percent: '0.075' + # }, + # { + # step: '4', + # from_turnover: '2500001.00000000', + # maker_percent: '0.04', + # taker_percent: '0.07' + # } + # ], + # daily_turnover: '0.00000000', + # monthly_turnover: '77.52062617', + # circulation_funds: '25.48900443' + # } + # } + # + data = self.safe_dict(response, 'result', {}) + currentFeeTier = self.safe_dict(data, 'current_tariff_step', {}) + makerFee = self.safe_string(currentFeeTier, 'maker_percent') + takerFee = self.safe_string(currentFeeTier, 'taker_percent') + makerFee = Precise.string_div(makerFee, '100') + takerFee = Precise.string_div(takerFee, '100') + feeTiers = self.safe_list(data, 'tariff_steps', []) + result: dict = {} + tiers = self.parse_fee_tiers(feeTiers) + for i in range(0, len(self.symbols)): + symbol = self.symbols[i] + result[symbol] = { + 'info': response, + 'symbol': symbol, + 'maker': self.parse_number(makerFee), + 'taker': self.parse_number(takerFee), + 'percentage': True, + 'tierBased': True, + 'tiers': tiers, + } + return result + + def parse_fee_tiers(self, feeTiers, market: Market = None): + takerFees = [] + makerFees = [] + for i in range(0, len(feeTiers)): + tier = feeTiers[i] + turnover = self.safe_number(tier, 'from_turnover') + taker = self.safe_string(tier, 'taker_percent') + maker = self.safe_string(tier, 'maker_percent') + maker = Precise.string_div(maker, '100') + taker = Precise.string_div(taker, '100') + makerFees.append([turnover, self.parse_number(maker)]) + takerFees.append([turnover, self.parse_number(taker)]) + return { + 'maker': makerFees, + 'taker': takerFees, + } + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + endpoint = self.implode_params(path, params) + params = self.omit(params, self.extract_params(path)) + url = self.urls['api'][api] + '/' + endpoint + if api == 'private': + self.check_required_credentials() + jsonParams = '' + headers = { + 'userId': self.uid, + } + if method != 'GET': + body = self.json(params) + jsonParams = body + headers['Content-Type'] = 'application/json' + else: + query = self.urlencode(params) + if len(query) != 0: + url += '?' + query + jsonParamsBase64 = self.string_to_base64(jsonParams) + stringToSign = jsonParamsBase64 + self.secret + signature = self.hash(self.encode(stringToSign), 'md5') + headers['sign'] = signature + else: + query = self.urlencode(params) + if len(query) != 0: + url += '?' + query + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None + if 'code' in response: + code = self.safe_string(response, 'code') + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], code, feedback) + raise ExchangeError(feedback) + elif 'message' in response: + # + # {"message":"Minimum amount 15 USDT","state":1} + # + message = self.safe_string(response, 'message') + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + raise ExchangeError(feedback) # unknown message + return None diff --git a/ccxt/async_support/deepcoin.py b/ccxt/async_support/deepcoin.py new file mode 100644 index 0000000..07fc561 --- /dev/null +++ b/ccxt/async_support/deepcoin.py @@ -0,0 +1,2859 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.deepcoin import ImplicitAPI +import asyncio +import hashlib +from ccxt.base.types import Any, Balances, Currency, DepositAddress, Int, LedgerEntry, Market, Num, Order, OrderBook, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, FundingRates, Trade, Transaction, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import NotSupported +from ccxt.base.errors import NullResponse +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class deepcoin(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(deepcoin, self).describe(), { + 'id': 'deepcoin', + 'name': 'DeepCoin', + 'countries': ['SG'], # Singapore + 'rateLimit': 200, # 5 times per second + 'version': 'v1', + 'certified': False, + 'pro': True, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': True, + 'swap': True, + 'future': False, + 'option': False, + 'addMargin': False, + 'cancelAllOrders': True, + 'cancelAllOrdersAfter': False, + 'cancelOrder': True, + 'cancelOrders': True, + 'cancelWithdraw': False, + 'closePosition': True, + 'createConvertTrade': False, + 'createDepositAddress': False, + 'createLimitBuyOrder': True, + 'createLimitOrder': True, + 'createLimitSellOrder': True, + 'createMarketBuyOrder': True, + 'createMarketBuyOrderWithCost': True, + 'createMarketOrder': True, + 'createMarketOrderWithCost': True, + 'createMarketSellOrder': True, + 'createMarketSellOrderWithCost': True, + 'createOrder': True, + 'createOrders': False, + 'createOrderWithTakeProfitAndStopLoss': True, + 'createPostOnlyOrder': True, + 'createReduceOnlyOrder': True, + 'createStopLossOrder': False, + 'createTakeProfitOrder': False, + 'createTrailingAmountOrder': False, + 'createTrailingPercentOrder': False, + 'createTriggerOrder': True, + 'editOrder': True, + 'fetchAccounts': False, + 'fetchBalance': True, + 'fetchCanceledAndClosedOrders': True, + 'fetchCanceledOrders': True, + 'fetchClosedOrder': True, + 'fetchClosedOrders': True, + 'fetchConvertCurrencies': False, + 'fetchConvertQuote': False, + 'fetchConvertTrade': False, + 'fetchConvertTradeHistory': False, + 'fetchCurrencies': False, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': True, + 'fetchDeposits': True, + 'fetchDepositsWithdrawals': False, + 'fetchDepositWithdrawFees': False, + 'fetchFundingHistory': False, + 'fetchFundingRate': True, + 'fetchFundingRateHistory': True, + 'fetchFundingRates': True, + 'fetchIndexOHLCV': True, + 'fetchLedger': True, + 'fetchLeverage': False, + 'fetchLeverageTiers': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': True, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenOrder': True, + 'fetchOpenOrders': True, + 'fetchOrder': False, + 'fetchOrderBook': True, + 'fetchOrders': False, + 'fetchOrderTrades': True, + 'fetchPosition': True, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': True, + 'fetchPositionsForSymbol': True, + 'fetchPositionsHistory': True, + 'fetchPremiumIndexOHLCV': False, + 'fetchStatus': False, + 'fetchTicker': False, + 'fetchTickers': True, + 'fetchTime': False, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': False, + 'fetchTransactions': False, + 'fetchTransfers': False, + 'fetchWithdrawals': True, + 'reduceMargin': False, + 'sandbox': False, + 'setLeverage': True, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'transfer': True, + 'withdraw': False, + }, + 'timeframes': { + '1m': '1m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1H', + '4h': '4H', + '12h': '12H', + '1d': '1D', + '1w': '1W', + '1M': '1M', + '1y': '1Y', + }, + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/671bd35c-770e-4935-9070-f8fb114f79c4', + 'api': { + 'public': 'https://api.deepcoin.com', + 'private': 'https://api.deepcoin.com', + }, + 'www': 'https://www.deepcoin.com/', + 'doc': 'https://www.deepcoin.com/docs', + 'referral': { + 'url': 'https://s.deepcoin.com/UzkyODgy', + 'discount': 0.1, + }, + }, + 'api': { + 'public': { + 'get': { + 'deepcoin/market/books': 1, + 'deepcoin/market/candles': 1, + 'deepcoin/market/instruments': 1, + 'deepcoin/market/tickers': 1, + 'deepcoin/market/index-candles': 1, + 'deepcoin/market/trades': 1, + 'deepcoin/market/mark-price-candles': 1, + 'deepcoin/market/step-margin': 5, + }, + }, + 'private': { + 'get': { + 'deepcoin/account/balances': 5, + 'deepcoin/account/bills': 5, + 'deepcoin/account/positions': 5, + 'deepcoin/trade/fills': 5, + 'deepcoin/trade/orderByID': 5, + 'deepcoin/trade/finishOrderByID': 5, + 'deepcoin/trade/orders-history': 5, + 'deepcoin/trade/v2/orders-pending': 5, + 'deepcoin/trade/funding-rate': 5, + 'deepcoin/trade/fund-rate/current-funding-rate': 5, + 'deepcoin/trade/fund-rate/history': 5, + 'deepcoin/trade/trigger-orders-pending': 5, + 'deepcoin/trade/trigger-orders-history': 5, + 'deepcoin/copytrading/support-contracts': 5, + 'deepcoin/copytrading/leader-position': 5, + 'deepcoin/copytrading/estimate-profit': 5, + 'deepcoin/copytrading/history-profit': 5, + 'deepcoin/copytrading/follower-rank': 5, + 'deepcoin/internal-transfer/support': 5, + 'deepcoin/internal-transfer/history-order': 5, + 'deepcoin/rebate/config': 5, + 'deepcoin/agents/users': 5, + 'deepcoin/agents/users/rebate-list': 5, + 'deepcoin/agents/users/rebates': 5, + 'deepcoin/asset/deposit-list': 5, + 'deepcoin/asset/withdraw-list': 5, + 'deepcoin/asset/recharge-chain-list': 5, + 'deepcoin/listenkey/acquire': 5, + 'deepcoin/listenkey/extend': 5, + }, + 'post': { + 'deepcoin/account/set-leverage': 5, + 'deepcoin/trade/order': 5, + 'deepcoin/trade/replace-order': 5, + 'deepcoin/trade/cancel-order': 5, + 'deepcoin/trade/batch-cancel-order': 5, + 'deepcoin/trade/cancel-trigger-order': 1 / 6, + 'deepcoin/trade/swap/cancel-all': 5, + 'deepcoin/trade/trigger-order': 5, + 'deepcoin/trade/batch-close-position': 5, + 'deepcoin/trade/replace-order-sltp': 5, + 'deepcoin/trade/close-position-by-ids': 5, + 'deepcoin/copytrading/leader-settings': 5, + 'deepcoin/copytrading/set-contracts': 5, + 'deepcoin/internal-transfer': 5, + 'deepcoin/rebate/config': 5, + 'deepcoin/asset/transfer': 5, + }, + }, + }, + 'fees': { + 'trading': { + 'taker': self.parse_number('0.0015'), + 'maker': self.parse_number('0.0010'), + }, + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': True, + 'triggerPrice': True, + 'triggerPriceType': { + 'last': True, + 'mark': False, + 'index': False, + }, + 'triggerDirection': False, + 'stopLossPrice': True, + 'takeProfitPrice': True, + 'attachedStopLossTakeProfit': { + 'triggerPriceType': { + 'last': False, + 'mark': False, + 'index': False, + }, + 'price': True, + }, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': True, + 'trailing': False, + 'marketBuyRequiresPrice': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 60, + 'untilDays': None, + 'symbolRequired': True, + }, + 'fetchOrder': None, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 100, + 'trigger': True, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOrders': None, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': None, + 'daysBackCanceled': None, + 'untilDays': None, + 'trigger': True, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOHLCV': { + 'limit': 300, + }, + }, + 'swap': { + 'linear': { + 'extends': 'spot', + }, + 'inverse': { + 'extends': 'spot', + }, + }, + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + 'password': True, + }, + 'precisionMode': TICK_SIZE, + 'options': { + 'recvWindow': 5000, + 'defaultNetworks': { + 'ETH': 'ERC20', + 'USDT': 'TRC20', + 'USDC': 'ERC20', + }, + 'networks': { + 'ERC20': 'ERC20', + 'TRC20': 'TRC20', + 'ARB': 'ARBITRUM', + 'BSC': 'BSC(BEP20)', + 'SOL': 'SOL', + 'BTC': 'Bitcoin', + 'ADA': 'Cardano', + }, + 'networksById': { + }, + 'fetchMarkets': { + 'types': ['spot', 'swap'], # spot, swap, + }, + 'timeInForce': { + 'GTC': 'GTC', # Good Till Cancel + 'IOC': 'IOC', # Immediate Or Cancel + 'PO': 'PO', # Post Only + }, + 'exchangeType': { + 'spot': 'SPOT', + 'swap': 'SWAP', + 'SPOT': 'SPOT', + 'SWAP': 'SWAP', + }, + 'accountsByType': { + 'spot': 1, + 'fund': 2, + 'rebate': 3, + 'inverse': 5, + 'linear': 7, + 'demo': 10, + }, + }, + 'commonCurrencies': {}, + 'exceptions': { + 'exact': { + '24': OrderNotFound, # {"code":"0","msg":"","data":{"ordId":"","clOrdId":"","sCode":"24","sMsg":"OrderNotFound:1"}} + '31': InsufficientFunds, # {"code":"0","msg":"","data":{"ordId":"","clOrdId":"","tag":"","sCode":"31","sMsg":"NotEnoughPositionToClose:Position=0"}} + '36': InsufficientFunds, # {"code":"0","msg":"","data":{"ordId":"","clOrdId":"","tag":"","sCode":"36","sMsg":"InsufficientMoney:-0.000004"}} + '44': BadRequest, # {"code":"0","msg":"","data":{"ordId":"","clOrdId":"","tag":"","sCode":"44","sMsg":"VolumeNotOnTick"}} + '49': InvalidOrder, # {"code":"0","msg":"","data":{"ordId":"","clOrdId":"","tag":"","sCode":"49","sMsg":"PriceOutOfUpperLimit:Price\u003eUpperLimitPrice[0.28422]"}} + '194': InvalidOrder, # {"code":"0","msg":"","data":{"ordId":"","clOrdId":"","tag":"","sCode":"194","sMsg":"LessThanMinVolume"}} + '195': InvalidOrder, # {"code":"0","msg":"","data":{"ordId":"","clOrdId":"","tag":"","sCode":"195","sMsg":"PositionLessThanMinVolume"}} + '199': BadRequest, # {"code":"0","msg":"","data":{"instId":"","lever":"","mgnMode":"","mrgPosition":"","sCode":"199","sMsg":"LeverageTooHigh:Amount[10000.0]\u003eLeverage[75.1880]"}} + '100010': InsufficientFunds, # {"code":"0","msg":"","data":{"retCode":100010,"retMsg":"Balance is insufficient, please deposit first.","retData":{}}} + 'unsupportedAction': BadRequest, + 'localIDNotExist': BadRequest, + }, + 'broad': { + 'no available': NotSupported, # orderbook does not exist: ETHUSD_0.1, no available orderbook data + 'field is required': ArgumentsRequired, # {"code":"51","msg":"The productGroup field is required","data":null} + 'not in acceptable range': BadRequest, # {"code":"51","msg":"The instType value `spot` is not in acceptable range: SPOT,SWAP","data":null} + 'subscription cluster does not "exist"': BadRequest, + 'must be equal or lesser than': BadRequest, # {"code":"51","msg":"The Size value `100` must be equal or lesser than 50","data":null} + }, + }, + }) + + def handle_market_type_and_params(self, methodName: str, market: Market = None, params={}, defaultValue=None) -> Any: + instType = self.safe_string(params, 'instType') + params = self.omit(params, 'instType') + type = self.safe_string(params, 'type') + if (type is None) and (instType is not None): + params = self.extend(params, {'type': instType}) + return super(deepcoin, self).handle_market_type_and_params(methodName, market, params, defaultValue) + + def convert_to_instrument_type(self, type): + exchangeTypes = self.safe_dict(self.options, 'exchangeType', {}) + return self.safe_string(exchangeTypes, type, type) + + async def fetch_markets(self, params={}) -> List[Market]: + """ + + https://www.deepcoin.com/docs/DeepCoinMarket/getBaseInfo + + retrieves data on all markets for okcoin + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + types = ['spot', 'swap'] + fetchMarketsOption = self.safe_dict(self.options, 'fetchMarkets') + if fetchMarketsOption is not None: + types = self.safe_list(fetchMarketsOption, 'types', types) + else: + types = self.safe_list(self.options, 'fetchMarkets', types) # backward-support + promises = [] + result = [] + for i in range(0, len(types)): + promises.append(self.fetch_markets_by_type(types[i], params)) + promises = await asyncio.gather(*promises) + for i in range(0, len(promises)): + result = self.array_concat(result, promises[i]) + return result + + async def fetch_markets_by_type(self, type, params={}): + request: dict = { + 'instType': self.convert_to_instrument_type(type), + } + response = await self.publicGetDeepcoinMarketInstruments(self.extend(request, params)) + # + # spot + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "instType": "SPOT", + # "instId": "A-USDT", + # "uly": "", + # "baseCcy": "A", + # "quoteCcy": "USDT", + # "ctVal": "1", + # "ctValCcy": "", + # "listTime": "0", + # "lever": "1", + # "tickSz": "0.0001", + # "lotSz": "0.001", + # "minSz": "0.5", + # "ctType": "", + # "alias": "", + # "state": "live", + # "maxLmtSz": "7692307", + # "maxMktSz": "7692307" + # } + # ] + # } + # + dataResponse = self.safe_list(response, 'data', []) + return self.parse_markets(dataResponse) + + def parse_market(self, market: dict) -> Market: + # + # spot markets + # + # { + # "instType": "SPOT", + # "instId": "A-USDT", + # "uly": "", + # "baseCcy": "A", + # "quoteCcy": "USDT", + # "ctVal": "1", + # "ctValCcy": "", + # "listTime": "0", + # "lever": "1", + # "tickSz": "0.0001", + # "lotSz": "0.001", + # "minSz": "0.5", + # "ctType": "", + # "alias": "", + # "state": "live", + # "maxLmtSz": "7692307", + # "maxMktSz": "7692307" + # } + # + # swap markets + # + # { + # "instType": "SWAP", + # "instId": "ZORA-USDT-SWAP", + # "uly": "", + # "baseCcy": "ZORA", + # "quoteCcy": "USDT", + # "ctVal": "1", + # "ctValCcy": "", + # "listTime": "0", + # "lever": "20", + # "tickSz": "0.00001", + # "lotSz": "1", + # "minSz": "1685", + # "ctType": "", + # "alias": "", + # "state": "live", + # "maxLmtSz": "10000000", + # "maxMktSz": "10000000" + # } + # + id = self.safe_string(market, 'instId') + type = self.safe_string_lower(market, 'instType') + spot = (type == 'spot') + swap = (type == 'swap') + baseId = self.safe_string(market, 'baseCcy') + quoteId = self.safe_string(market, 'quoteCcy', '') + settleId = None + settle = None + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + symbol = base + '/' + quote + isLinear = None + if swap: + isLinear = (quoteId != 'USD') + settleId = quoteId if isLinear else baseId + settle = self.safe_currency_code(settleId) + symbol = symbol + ':' + settle + fees = self.safe_dict_2(self.fees, type, 'trading', {}) + maxLeverage = self.safe_string(market, 'lever', '1') + maxLeverage = Precise.string_max(maxLeverage, '1') + maxMarketSize = self.safe_string(market, 'maxMktSz') + maxLimitSize = self.safe_string(market, 'maxLmtSz') + maxAmount = self.parse_number(Precise.string_max(maxMarketSize, maxLimitSize)) + state = self.safe_string(market, 'state') + return self.extend(fees, { + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': type, + 'spot': spot, + 'margin': spot and (Precise.string_gt(maxLeverage, '1')), + 'swap': swap, + 'future': False, + 'option': False, + 'active': state == 'live', + 'contract': swap, + 'linear': isLinear, + 'inverse': (not isLinear) if swap else None, + 'contractSize': self.safe_number(market, 'ctVal') if swap else None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'created': None, + 'precision': { + 'amount': self.safe_number(market, 'lotSz'), + 'price': self.safe_number(market, 'tickSz'), + }, + 'limits': { + 'leverage': { + 'min': self.parse_number('1'), + 'max': self.parse_number(maxLeverage), + }, + 'amount': { + 'min': self.safe_number(market, 'minSz'), + 'max': maxAmount, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'info': market, + }) + + def set_markets(self, markets, currencies=None): + markets = super(deepcoin, self).set_markets(markets, currencies) + symbols = list(markets.keys()) + for i in range(0, len(symbols)): + symbol = symbols[i] + market = markets[symbol] + if market['swap']: + additionalId = market['baseId'] + market['quoteId'] + self.markets_by_id[additionalId] = [market] # some endpoints return swap market id+quote + return self.markets + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://www.deepcoin.com/docs/DeepCoinMarket/marketBooks + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + if limit is None: + limit = 400 + request: dict = { + 'instId': market['id'], + 'sz': limit, + } + response = await self.publicGetDeepcoinMarketBooks(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "", + # "data": { + # "bids": [ + # ["3732.21", "99.6"], + # ["3732.2", "54.7"] + # ], + # "asks": [ + # ["3732.22", "85.1"], + # ["3732.23", "49.4"] + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_order_book(data, symbol, None, 'bids', 'asks', 0, 1) + + async def fetch_ohlcv(self, symbol: str, timeframe='1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://www.deepcoin.com/docs/DeepCoinMarket/getKlineData + https://www.deepcoin.com/docs/DeepCoinMarket/getIndexKlineData + https://www.deepcoin.com/docs/DeepCoinMarket/getMarkKlineData + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest candle to fetch + :param str [params.price]: "mark" or "index" for mark price and index price candles + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + maxLimit = 300 + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate', False) + if paginate: + params = self.extend(params, {'calculateUntil': True}) + return await self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params, maxLimit) + market = self.market(symbol) + price = self.safe_string(params, 'price') + params = self.omit(params, 'price') + bar = self.safe_string(self.timeframes, timeframe, timeframe) + request: dict = { + 'instId': market['id'], + 'bar': bar, + } + if limit is not None: + request['limit'] = limit + until = self.safe_integer(params, 'until') + if until is not None: + request['after'] = until + params = self.omit(params, 'until') + calculateUntil = self.safe_bool(params, 'calculateUntil', False) + if calculateUntil: + params = self.omit(params, 'calculateUntil') + if since is not None: + # the exchange do not have a since param for self endpoint + # we canlculate until(after) for correct pagination + duration = self.parse_timeframe(timeframe) + numberOfCandles = maxLimit if (limit is None) else limit + endTime = since + (duration * numberOfCandles) * 1000 + if until is not None: + endTime = min(endTime, until) + now = self.milliseconds() + request['after'] = min(endTime, now) + response = None + if price == 'mark': + response = await self.publicGetDeepcoinMarketMarkPriceCandles(self.extend(request, params)) + elif price == 'index': + response = await self.publicGetDeepcoinMarketIndexCandles(self.extend(request, params)) + else: + response = await self.publicGetDeepcoinMarketCandles(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "", + # "data":[ + # [ + # "1760221800000", + # "3739.08", + # "3741.95", + # "3737.75", + # "3740.1", + # "2849", + # "1065583.744" + # ], + # [ + # "1760221740000", + # "3742.36", + # "3743.01", + # "3736.83", + # "3739.08", + # "2723", + # "1018290.723" + # ] + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_ohlcvs(data, market, timeframe, since, limit) + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://www.deepcoin.com/docs/DeepCoinMarket/getMarketTickers + + :param str[] [symbols]: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + market = self.get_market_from_symbols(symbols) + marketType = None + marketType, params = self.handle_market_type_and_params('fetchTickers', market, params) + request: dict = { + 'instType': self.convert_to_instrument_type(marketType), + } + response = await self.publicGetDeepcoinMarketTickers(self.extend(request, params)) + tickers = self.safe_list(response, 'data', []) + return self.parse_tickers(tickers, symbols) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "instType": "SWAP", + # "instId": "BTC-USD-SWAP", + # "last": "114113.3", + # "lastSz": "", + # "askPx": "114113.5", + # "askSz": "56280", + # "bidPx": "114113.2", + # "bidSz": "63220", + # "open24h": "113214.7", + # "high24h": "116039.2", + # "low24h": "113214.7", + # "volCcy24h": "73.31475724", + # "vol24h": "8406739", + # "sodUtc0": "", + # "sodUtc8": "", + # "ts": "1760367816000" + # } + # + timestamp = self.safe_integer(ticker, 'ts') + marketId = self.safe_string(ticker, 'instId') + market = self.safe_market(marketId, market, '-') + symbol = market['symbol'] + last = self.safe_string(ticker, 'last') + open = self.safe_string(ticker, 'open24h') + quoteVolume = self.safe_string(ticker, 'volCcy24h') + baseVolume = self.safe_string(ticker, 'vol24h') + if market['swap'] and market['inverse']: + temp = baseVolume + baseVolume = quoteVolume + quoteVolume = temp + high = self.safe_string(ticker, 'high24h') + low = self.safe_string(ticker, 'low24h') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': high, + 'low': low, + 'bid': self.safe_string(ticker, 'bidPx'), + 'bidVolume': self.safe_string(ticker, 'bidSz'), + 'ask': self.safe_string(ticker, 'askPx'), + 'askVolume': self.safe_string(ticker, 'askSz'), + 'vwap': None, + 'open': open, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'markPrice': None, + 'indexPrice': None, + 'info': ticker, + }, market) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://www.deepcoin.com/docs/DeepCoinMarket/getTrades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch(default 100, max 500) + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + } + if limit is not None: + request['limit'] = limit # default 100, max 500 + productGroup = self.get_product_group_from_market(market) + request['productGroup'] = productGroup + response = await self.publicGetDeepcoinMarketTrades(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + return self.parse_trades(data, market, since, limit) + + def get_product_group_from_market(self, market: Market) -> str: + productGroup = 'Spot' + if market['swap']: + if market['linear']: + productGroup = 'SwapU' + else: + productGroup = 'Swap' + return productGroup + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # public fetchTrades + # + # { + # "instId": "ETH-USDT", + # "tradeId": "1001056388761321", + # "px": "4095.66", + # "sz": "0.01311251", + # "side": "sell", + # "ts": "1760367870000" + # } + # + # private fetchMyTrades + # { + # "instType": "SPOT", + # "instId": "ETH-USDT", + # "tradeId": "1001056429613610", + # "ordId": "1001435238208686", + # "clOrdId": "", + # "billId": "10010564296136101", + # "tag": "", + # "fillPx": "3791.15", + # "fillSz": "0.004", + # "side": "sell", + # "posSide": "", + # "execType": "", + # "feeCcy": "USDT", + # "fee": "0.0151646", + # "ts": "1760704540000" + # } + # + marketId = self.safe_string(trade, 'instId') + market = self.safe_market(marketId, market) + timestamp = self.safe_integer(trade, 'ts') + side = self.safe_string(trade, 'side') + execType = self.safe_string(trade, 'execType') + fee = None + feeCost = self.safe_string(trade, 'fee') + if feeCost is not None: + feeCurrencyId = self.safe_string(trade, 'feeCcy') + feeCurrencyCode = self.safe_currency_code(feeCurrencyId) + fee = { + 'cost': feeCost, + 'currency': feeCurrencyCode, + } + return self.safe_trade({ + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'id': self.safe_string(trade, 'tradeId'), + 'order': self.safe_string(trade, 'ordId'), + 'type': None, + 'takerOrMaker': self.parse_taker_or_maker(execType), + 'side': side, + 'price': self.safe_string_2(trade, 'fillPx', 'px'), + 'amount': self.safe_string_2(trade, 'fillSz', 'sz'), + 'cost': None, + 'fee': fee, + }, market) + + def parse_taker_or_maker(self, execType: Str): + types = { + 'T': 'taker', + 'M': 'maker', + } + return self.safe_string(types, execType, execType) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://www.deepcoin.com/docs/DeepCoinAccount/getAccountBalance + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: "spot" or "swap", the market type for the balance + :returns dict: a `balance structure ` + """ + await self.load_markets() + marketType = 'spot' + marketType, params = self.handle_market_type_and_params('fetchBalance', None, params, marketType) + request: dict = { + 'instType': self.convert_to_instrument_type(marketType), + } + response = await self.privateGetDeepcoinAccountBalances(self.extend(request, params)) + return self.parse_balance(response) + + def parse_balance(self, response) -> Balances: + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "ccy": "USDT", + # "bal": "74", + # "frozenBal": "0", + # "availBal": "74" + # } + # ] + # } + # + result: dict = { + 'info': response, + 'timestamp': None, + 'datetime': None, + } + balances = self.safe_list(response, 'data', []) + for i in range(0, len(balances)): + balance = balances[i] + symbol = self.safe_string(balance, 'ccy') + code = self.safe_currency_code(symbol) + account = self.account() + account['total'] = self.safe_string(balance, 'bal') + account['used'] = self.safe_string(balance, 'frozenBal') + account['free'] = self.safe_string(balance, 'availBal') + result[code] = account + return self.safe_balance(result) + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://www.deepcoin.com/docs/assets/deposit + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch entries for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchDeposits', 'paginate', False) + if paginate: + return await self.fetch_paginated_call_cursor('fetchDeposits', code, since, limit, params, 'code', None, 1, 50) + request: dict = {} + currency = None + if code is not None: + currency = self.currency(code) + request['coin'] = currency['id'] + if since is not None: + request['startTime'] = since + if limit is not None: + request['size'] = limit + until = self.safe_integer(params, 'until') + if until is not None: + request['endTime'] = until + params = self.omit(params, 'until') + response = await self.privateGetDeepcoinAssetDepositList(self.extend(request, params)) + data = self.safe_dict(response, 'data', {}) + items = self.safe_list(data, 'data', []) + transactionParams: dict = { + 'type': 'deposit', + } + return self.parse_transactions(items, currency, since, limit, transactionParams) + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://www.deepcoin.com/docs/assets/withdraw + + :param str code: unified currency code of the currency transferred + :param int [since]: the earliest time in ms to fetch transfers for(default 24 hours ago) + :param int [limit]: the maximum number of transfer structures to retrieve(default 50, max 200) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch transfers for(default time now) + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchDeposits', 'paginate', False) + if paginate: + return await self.fetch_paginated_call_cursor('fetchDeposits', code, since, limit, params, 'code', None, 1, 50) + request: dict = {} + currency = None + if code is not None: + currency = self.currency(code) + request['coin'] = currency['id'] + if since is not None: + request['startTime'] = since + if limit is not None: + request['size'] = limit + until = self.safe_integer(params, 'until') + if until is not None: + request['endTime'] = until + params = self.omit(params, 'until') + response = await self.privateGetDeepcoinAssetWithdrawList(self.extend(request, params)) + data = self.safe_dict(response, 'data', {}) + items = self.safe_list(data, 'data', []) + transactionParams: dict = { + 'type': 'withdrawal', + } + return self.parse_transactions(items, currency, since, limit, transactionParams) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fetchDeposits + # { + # "createTime": 1760368656, + # "txHash": "03fe3244d89e794586222413c61779380da9e9fe5baaa253c38d01a4199a3499", + # "chainName": "TRC20", + # "amount": "149", + # "coin": "USDT", + # "status": "succeed" + # } + # + txid = self.safe_string(transaction, 'txHash') + currencyId = self.safe_string(transaction, 'coin') + code = self.safe_currency_code(currencyId, currency) + amount = self.safe_number(transaction, 'amount') + timestamp = self.safe_timestamp(transaction, 'createTime') + networkId = self.safe_string(transaction, 'chainName') + network = self.network_id_to_code(networkId) + status = self.parse_transaction_status(self.safe_string(transaction, 'status')) + return { + 'info': transaction, + 'id': None, + 'currency': code, + 'amount': amount, + 'network': network, + 'addressFrom': None, + 'addressTo': None, + 'address': self.safe_string(transaction, 'address'), + 'tagFrom': None, + 'tagTo': None, + 'tag': None, + 'status': status, + 'type': None, + 'updated': None, + 'txid': txid, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'internal': None, + 'comment': None, + 'fee': { + 'currency': None, + 'cost': None, + }, + } + + def parse_transaction_status(self, status: Str) -> Str: + statuses: dict = { + 'confirming': 'pending', + 'succeed': 'ok', + } + return self.safe_string(statuses, status, status) + + async def fetch_deposit_addresses(self, codes: Strings = None, params={}) -> List[DepositAddress]: + """ + fetch deposit addresses for multiple currencies and chain types + + https://www.deepcoin.com/docs/assets/chainlist + + :param str[]|None codes: list of unified currency codes, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `address structures ` + """ + await self.load_markets() + if codes is None: + raise ArgumentsRequired(self.id + ' fetchDepositAddresses requires a list with one currency code') + length = len(codes) + if length != 1: + raise NotSupported(self.id + ' fetchDepositAddresses requires a list with one currency code') + code = codes[0] + currency = self.currency(code) + request: dict = { + 'currency_id': currency['id'], + 'lang': 'en', + } + response = await self.privateGetDeepcoinAssetRechargeChainList(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "", + # "data": { + # "list": [ + # { + # "chain": "TRC20", + # "state": 1, + # "remind": "Only support deposits and withdrawals via TRC20 network. If you send it via other address by mistake, it will not be credited and will result in the permanent loss of your deposit.", + # "inNotice": "", + # "actLogo": "", + # "address": "TNJYDW9Bk87VwfA6s7FtxURLEMHesQbYgF", + # "hasMemo": False, + # "memo": "", + # "estimatedTime": 1, + # "fastConfig": { + # "fastLimitNum": 0, + # "fastBlock": 10, + # "realBlock": 1 + # } + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + list = self.safe_list(data, 'list', []) + additionalParams: dict = { + 'currency': code, + } + return self.parse_deposit_addresses(list, codes, False, additionalParams) + + async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://www.deepcoin.com/docs/assets/chainlist + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.network]: unified network code for deposit chain + :returns dict: an `address structure ` + """ + await self.load_markets() + network = self.safe_string(params, 'network') + defaultNetworks = self.safe_dict(self.options, 'defaultNetworks', {}) + defaultNetwork = self.safe_string(defaultNetworks, code) + network = network if network else defaultNetwork + if network is not None: + params = self.omit(params, 'network') + addressess = await self.fetch_deposit_addresses([code], params) + length = len(addressess) + address = self.safe_dict(addressess, 0, {}) + if (network is not None) and (length > 1): + for i in range(0, length): + entry = addressess[i] + if entry['network'] == network: + address = entry + return address + + def parse_deposit_address(self, response, currency: Currency = None) -> DepositAddress: + # + # { + # "chain": "TRC20", + # "state": 1, + # "remind": "Only support deposits and withdrawals via TRC20 network. If you send it via other address by mistake, it will not be credited and will result in the permanent loss of your deposit.", + # "inNotice": "", + # "actLogo": "", + # "address": "TNJYDW9Bk87VwfA6s7FtxURLEMHesQbYgF", + # "hasMemo": False, + # "memo": "", + # "estimatedTime": 1, + # "fastConfig": { + # "fastLimitNum": 0, + # "fastBlock": 10, + # "realBlock": 1 + # } + # } + # + chain = self.safe_string(response, 'chain') + address = self.safe_string(response, 'address') + self.check_address(address) + return { + 'info': response, + 'currency': None, + 'network': self.network_id_to_code(chain), + 'address': address, + 'tag': self.safe_string(response, 'memo'), + } + + async def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + + https://www.deepcoin.com/docs/DeepCoinAccount/getAccountBills + + :param str [code]: unified currency code + :param int [since]: timestamp in ms of the earliest ledger entry + :param int [limit]: max number of ledger entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest ledger entry + :param str [params.type]: 'spot' or 'swap', the market type for the ledger(default 'spot') + :returns dict[]: a list of `ledger structures ` + """ + await self.load_markets() + marketType = 'spot' + marketType, params = self.handle_market_type_and_params('fetchLedger', None, params, marketType) + request: dict = { + 'instType': self.convert_to_instrument_type(marketType), + } + currency = None + if code is not None: + currency = self.currency(code) + request['ccy'] = currency['id'] + if since is not None: + request['after'] = since + if limit is not None: + request['limit'] = limit + until = self.safe_integer(params, 'until') + if until is not None: + request['before'] = until + params = self.omit(params, 'until') + response = await self.privateGetDeepcoinAccountBills(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "billId": "1001044652247714", + # "ccy": "USDT", + # "clientId": "", + # "balChg": "-0.03543537", + # "bal": "72.41881427", + # "type": "5", + # "ts": "1761047448000" + # }, + # { + # "billId": "1001044652258368", + # "ccy": "DOGE", + # "clientId": "", + # "balChg": "76", + # "bal": "76", + # "type": "2", + # "ts": "1761051006000" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_ledger(data, currency, since, limit) + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + # + # { + # "billId": "1001044652247714", + # "ccy": "USDT", + # "clientId": "", + # "balChg": "-0.03543537", + # "bal": "72.41881427", + # "type": "5", + # "ts": "1761047448000" + # } + # + timestamp = self.safe_integer(item, 'ts') + change = self.safe_string(item, 'balChg') + amount = Precise.string_abs(change) + direction = 'out' if Precise.string_lt(change, '0') else 'in' + currencyId = self.safe_string(item, 'ccy') + currency = self.safe_currency(currencyId, currency) + type = self.safe_string(item, 'type') + return self.safe_ledger_entry({ + 'info': item, + 'id': self.safe_string(item, 'billId'), + 'direction': direction, + 'account': None, + 'referenceAccount': None, + 'referenceId': None, + 'type': self.parse_ledger_entry_type(type), + 'currency': currency['code'], + 'amount': amount, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'before': None, + 'after': self.safe_string(item, 'bal'), + 'status': None, + 'fee': None, + }, currency) + + def parse_ledger_entry_type(self, type): + ledgerType: dict = { + '1': 'trade', + '2': 'trade', + '3': 'transfer', + '4': 'transfer', + '5': 'fee', + } + return self.safe_string(ledgerType, type, type) + + async def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://www.deepcoin.com/docs/assets/transfer + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account to transfer from('spot', 'inverse', 'linear', 'fund', 'rebate' or 'demo') + :param str toAccount: account to transfer to('spot', 'inverse', 'linear', 'fund', 'rebate' or 'demo') + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.userId]: user id + :returns dict: a `transfer structure ` + """ + userId = None + userId, params = self.handle_option_and_params(params, 'transfer', 'userId') + userId = userId if userId else self.safe_string(params, 'uid') + if userId is None: + raise ArgumentsRequired(self.id + ' transfer() requires a userId parameter') + await self.load_markets() + currency = self.currency(code) + accountsByType = self.safe_dict(self.options, 'accountsByType', {}) + fromId = self.safe_string(accountsByType, fromAccount, fromAccount) + toId = self.safe_string(accountsByType, toAccount, toAccount) + request: dict = { + 'currency_id': currency['id'], + 'amount': self.currency_to_precision(code, amount), + 'from_id': fromId, + 'to_id': toId, + 'uid': userId, + } + response = await self.privatePostDeepcoinAssetTransfer(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "", + # "data": { + # "retCode": 0, + # "retMsg": "", + # "retData": {} + # } + # } + # + data = self.safe_dict(response, 'data', {}) + transfer = self.parse_transfer(data, currency) + transferOptions = self.safe_dict(self.options, 'transfer', {}) + fillResponseFromRequest = self.safe_bool(transferOptions, 'fillResponseFromRequest', True) + if fillResponseFromRequest: + transfer['fromAccount'] = fromAccount + transfer['toAccount'] = toAccount + transfer['amount'] = amount + return transfer + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + # + # { + # "retCode": 0, + # "retMsg": "", + # "retData": {} + # } + # + status = self.safe_string(transfer, 'retCode') + currencyCode = self.safe_currency_code(None, currency) + return { + 'info': transfer, + 'id': None, + 'timestamp': None, + 'datetime': None, + 'currency': currencyCode, + 'amount': None, + 'fromAccount': None, + 'toAccount': None, + 'status': self.parse_transfer_status(status), + } + + def parse_transfer_status(self, status: Str) -> Str: + if status == '0': + return 'ok' + return 'failed' + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://www.deepcoin.com/docs/DeepCoinTrade/order + https://www.deepcoin.com/docs/DeepCoinTrade/triggerOrder + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.clientOrderId]: a unique id for the order + :param str [params.timeInForce]: *non trigger orders only* 'GTC'(Good Till Cancel), 'IOC'(Immediate Or Cancel) or 'PO'(Post Only) + :param bool [params.postOnly]: *non trigger orders only* True to place a post only order + :param bool [params.reduceOnly]: *non trigger orders only* a mark to reduce the position size for margin, swap and future orders + :param float [params.triggerPrice]: the price a trigger order is triggered at + :param float [params.stopLoss.triggerPrice]: the price that a stop loss order is triggered at + :param float [params.takeProfit.triggerPrice]: the price that a take profit order is triggered at + :param str [params.positionSide]: if position mode is one-way: set to 'net', if position mode is hedge-mode: set to 'long' or 'short' + :param bool [params.hedged]: *swap only* True for hedged mode, False for one way mode + :param str [params.marginMode]: *swap only*'cross' or 'isolated', the default is 'cash' for spot and 'cross' for swap + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + triggerPrice = self.safe_string(params, 'triggerPrice') + request = self.create_order_request(symbol, type, side, amount, price, params) + response = None + if triggerPrice is not None: + # trigger orders + response = await self.privatePostDeepcoinTradeTriggerOrder(request) + else: + # regular orders + # + # { + # "code": "0", + # "msg": "", + # "data": { + # "ordId": "1001434570213727", + # "clOrdId": "", + # "tag": "", + # "sCode": "0", + # "sMsg": "" + # } + # } + # + response = await self.privatePostDeepcoinTradeOrder(request) + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data, market) + + def create_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + @ignore + helper function to build request + """ + market = self.market(symbol) + triggerPrice = self.safe_string(params, 'triggerPrice') + # isTriggerOrder = (triggerPrice is not None) or self.safe_string_2(params, 'stopLossPrice', 'takeProfitPrice') is not None + isTriggerOrder = (triggerPrice is not None) + cost = self.safe_string(params, 'cost') + if cost is not None: + if not market['spot'] or (triggerPrice is not None): + raise BadRequest(self.id + ' createOrder() accepts a cost parameter for spot non-trigger market orders only') + if isTriggerOrder: + return self.create_trigger_order_request(symbol, type, side, amount, price, params) + else: + return self.create_regular_order_request(symbol, type, side, amount, price, params) + + def create_regular_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + @ignore + helper function to build request + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.cost]: *spot only* the cost of the order in units of the quote currency, for market orders only + :param str [params.clientOrderId]: a unique id for the order + :param str [params.timeInForce]: 'GTC'(Good Till Cancel), 'IOC'(Immediate Or Cancel) or 'PO'(Post Only) + :param bool [params.postOnly]: True to place a post only order + :param bool [params.reduceOnly]: a mark to reduce the position size for margin and swap orders + :param float [params.stopLossPrice]: the price that a stop loss order is triggered at + :param float [params.takeProfitPrice]: the price that a take profit order is triggered at + :param str [params.marginMode]: *swap only* 'cross' or 'isolated', the default is 'cash' for spot and 'cross' for swap + :param str [params.mrgPosition]: *swap only* 'merge' or 'split', the default is 'merge' + """ + market = self.market(symbol) + orderType = type + orderType, params = self.handle_type_post_only_and_time_in_force(type, params) + request: dict = { + 'instId': market['id'], + # 'tdMode': 'cash', # 'cash' for spot, 'cross' or 'isolated' for swap + # 'ccy': currency['id'], # only applicable to cross MARGIN orders in single-currency margin + # 'clOrdId': clientOrderId, + 'side': side, + 'ordType': orderType, + # 'sz': amount or cost + # 'px': price, # limit orders only + # 'reduceOnly': False, # a mark to reduce the position size for margin and swap orders + # 'tgtCcy': 'base_ccy', # spot only 'base_ccy' or 'quote_ccy', the default is 'base_ccy' for spot orders + # 'tpTriggerPx': takeProfitPrice, # take profit trigger price + # 'slTriggerPx': stopLossPrice, # stop loss trigger price + # 'posSide': 'long', # swap only 'long' or 'short' + # 'mrgPosition': 'merge', # swap only 'merge' or 'split' + # 'closePosId': 'id', # swap only position ID to close, required in split mode + } + clientOrderId = self.safe_string(params, 'clientOrderId') + if clientOrderId is not None: + request['clOrdId'] = clientOrderId + params = self.omit(params, 'clientOrderId') + stopLoss = self.safe_dict(params, 'stopLoss', {}) + stopLossPrice = self.safe_string(stopLoss, 'triggerPrice') + if stopLossPrice is not None: + params = self.omit(params, ['stopLoss']) + request['slTriggerPx'] = self.price_to_precision(symbol, stopLossPrice) + takeProfit = self.safe_dict(params, 'takeProfit', {}) + takeProfitPrice = self.safe_string(takeProfit, 'triggerPrice') + if takeProfitPrice is not None: + params = self.omit(params, ['takeProfit']) + request['tpTriggerPx'] = self.price_to_precision(symbol, takeProfitPrice) + isMarketOrder = (type == 'market') + if price is not None: + if isMarketOrder: + raise BadRequest(self.id + ' createOrder() does not require a price argument for market orders') + request['px'] = self.price_to_precision(symbol, price) + elif not isMarketOrder: + raise BadRequest(self.id + ' createOrder() requires a price argument for limit orders') + if market['spot']: + cost = self.safe_string(params, 'cost') + if cost is not None: + if not isMarketOrder: + raise BadRequest(self.id + ' createOrder() accepts a cost parameter for spot market orders only') + params = self.omit(params, 'cost') + request['sz'] = self.cost_to_precision(symbol, cost) + request['tgtCcy'] = 'quote_ccy' + else: + request['sz'] = self.amount_to_precision(symbol, amount) + request['tgtCcy'] = 'base_ccy' + request['side'] = side + request['tdMode'] = 'cash' + else: + request['sz'] = self.amount_to_precision(symbol, amount) + marginMode = 'cross' + marginMode, params = self.handle_margin_mode_and_params('createOrder', params, marginMode) + request['tdMode'] = marginMode + mrgPosition = 'merge' + mrgPosition, params = self.handle_option_and_params(params, 'createOrder', 'mrgPosition', mrgPosition) + request['mrgPosition'] = mrgPosition + posSide: Str = None + reduceOnly = self.safe_bool(params, 'reduceOnly', False) + if reduceOnly: + if side == 'buy': + posSide = 'short' + elif side == 'sell': + posSide = 'long' + else: + if side == 'buy': + posSide = 'long' + elif side == 'sell': + posSide = 'short' + request['posSide'] = posSide + return self.extend(request, params) + + def create_trigger_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + @ignore + helper function to build request + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.reduceOnly]: a mark to reduce the position size for margin orders + :param str [params.marginMode]: *swap only* 'cross' or 'isolated', the default is 'cash' for spot and 'cross' for swap + """ + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + 'productGroup': self.capitalize(market['type']), + 'sz': self.amount_to_precision(symbol, amount), + 'side': side, + # 'posSide': 'long', # 'long' or 'short' - required when product type is SWAP + # 'price': price, + # 'isCrossMargin': 1, # 1 for cross margin, 0 for isolated margin + 'orderType': type, + # 'triggerPrice': triggerPrice, + # 'mrgPosition': 'merge', # 'merge' or 'split', the default is 'merge' - required when product type is SWAP + # 'tdMode': 'cash', # 'cash' for spot, 'cross' or 'isolated' for swap + } + triggerPrice = self.safe_string(params, 'triggerPrice') + # takeProfitPrice = self.safe_string(params, 'takeProfitPrice') + # stopLossPrice = self.safe_string(params, 'stopLossPrice') + # isTpOrSlOrder = (takeProfitPrice is not None) or (stopLossPrice is not None) + # if isTpOrSlOrder: + # if takeProfitPrice is not None: + # request['triggerPrice'] = self.price_to_precision(symbol, takeProfitPrice) + # else: + # request['triggerPrice'] = self.price_to_precision(symbol, stopLossPrice) + # } + # else: + request['triggerPrice'] = self.price_to_precision(symbol, triggerPrice) + # } + if price is not None: + request['price'] = self.price_to_precision(symbol, price) + elif type == 'limit': + raise ArgumentsRequired(self.id + ' createOrder() requires a price argument for limit trigger orders') + marginMode = 'cross' + marginMode, params = self.handle_margin_mode_and_params('createOrder', params, marginMode) + isCrossMargin = 1 + if marginMode == 'isolated': + isCrossMargin = 0 + reduceOnly = self.safe_bool(params, 'reduceOnly', False) + params = self.omit(params, 'reduceOnly') + request['isCrossMargin'] = isCrossMargin + request['tdMode'] = marginMode + if market['swap']: + if reduceOnly: + if side == 'buy': + request['posSide'] = 'short' + elif side == 'sell': + request['posSide'] = 'long' + else: + if side == 'buy': + request['posSide'] = 'long' + elif side == 'sell': + request['posSide'] = 'short' + mrgPosition = 'merge' + mrgPosition, params = self.handle_option_and_params(params, 'createOrder', 'mrgPosition', mrgPosition) + request['mrgPosition'] = mrgPosition + return self.extend(request, params) + + def handle_type_post_only_and_time_in_force(self, type: OrderType, params): + postOnly = False + postOnly, params = self.handle_post_only(type == 'market', type == 'post_only', params) + if postOnly: + type = 'post_only' + timeInForce = self.handle_time_in_force(params) + params = self.omit(params, 'timeInForce') + if (timeInForce is not None) and (timeInForce == 'IOC'): + type = 'ioc' + return [type, params] + + async def create_market_order_with_cost(self, symbol: str, side: OrderSide, cost: float, params={}): + """ + create a market order by providing the symbol, side and cost + :param str symbol: unified symbol of the market to create an order in + :param str side: 'buy' or 'sell' + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + params = self.extend(params, {'cost': cost}) + return await self.create_order(symbol, 'market', side, 0, None, params) + + async def create_market_buy_order_with_cost(self, symbol: str, cost: float, params={}) -> Order: + """ + create a market buy order by providing the symbol and cost + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + params = self.extend(params, {'cost': cost}) + return await self.create_order(symbol, 'market', 'buy', 0, None, params) + + async def create_market_sell_order_with_cost(self, symbol: str, cost: float, params={}) -> Order: + """ + create a market sell order by providing the symbol and cost + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + params = self.extend(params, {'cost': cost}) + return await self.create_order(symbol, 'market', 'sell', 0, None, params) + + async def fetch_closed_order(self, id: str, symbol: Str = None, params={}) -> Order: + """ + fetches information on a closed order made by the user + + https://www.deepcoin.com/docs/DeepCoinTrade/finishOrderByID + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchClosedOrder() requires a symbol argument') + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + 'ordId': id, + } + response = await self.privateGetDeepcoinTradeFinishOrderByID(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "instType": "SPOT", + # "instId": "ETH-USDT", + # "tgtCcy": "", + # "ccy": "", + # "ordId": "1001434573319675", + # "clOrdId": "", + # "tag": "", + # "px": "4056.620000000000", + # "sz": "0.004000", + # "pnl": "0.000000", + # "ordType": "market", + # "side": "buy", + # "posSide": "", + # "tdMode": "cash", + # "accFillSz": "0.004000", + # "fillPx": "", + # "tradeId": "", + # "fillSz": "0.004000", + # "fillTime": "1760619119000", + # "avgPx": "", + # "state": "filled", + # "lever": "1.000000", + # "tpTriggerPx": "", + # "tpTriggerPxType": "", + # "tpOrdPx": "", + # "slTriggerPx": "", + # "slTriggerPxType": "", + # "slOrdPx": "", + # "feeCcy": "USDT", + # "fee": "0.000004", + # "rebateCcy": "", + # "source": "", + # "rebate": "", + # "category": "normal", + # "uTime": "1760619119000", + # "cTime": "1760619119000" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + entry = self.safe_dict(data, 0, {}) + return self.parse_order(entry, market) + + async def fetch_open_order(self, id: str, symbol: Str = None, params={}) -> Order: + """ + fetch an open order by it's id + + https://www.deepcoin.com/docs/DeepCoinTrade/orderByID + + :param str id: order id + :param str symbol: unified market symbol, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchClosedOrder() requires a symbol argument') + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + 'ordId': id, + } + response = await self.privateGetDeepcoinTradeOrderByID(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + length = len(data) + if length == 0: + return None + entry = self.safe_dict(data, 0, {}) + return self.parse_order(entry, market) + + async def fetch_canceled_and_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + + https://www.deepcoin.com/docs/DeepCoinTrade/ordersHistory + https://www.deepcoin.com/docs/DeepCoinTrade/triggerOrdersHistory + + fetches information on multiple canceled and closed orders made by the user + :param str [symbol]: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.trigger]: whether to fetch trigger/algo orders(default False) + :param str [params.type]: *non trigger orders only* 'spot' or 'swap', the market type for the orders + :param str [params.state]: *non trigger orders only* 'canceled' or 'filled', the order state to filter by + :param str [params.OrderType]: *trigger orders only* 'limit' or 'market' + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchCanceledAndClosedOrders', 'paginate') + if paginate: + return await self.fetch_paginated_call_dynamic('fetchCanceledAndClosedOrders', symbol, since, limit, params) + trigger = self.safe_bool(params, 'trigger', False) + methodName = 'fetchCanceledAndClosedOrders' + methodName, params = self.handle_param_string(params, 'methodName', methodName) + market: Market = None + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['instId'] = market['id'] + marketType = 'spot' + marketType, params = self.handle_market_type_and_params(methodName, market, params, marketType) + request['instType'] = self.convert_to_instrument_type(marketType) + if limit is not None: + request['limit'] = limit # default 100 + response = None + if trigger: + if methodName != 'fetchCanceledAndClosedOrders': + raise BadRequest(self.id + ' ' + methodName + '() does not support trigger orders') + if market is None: + raise ArgumentsRequired(self.id + ' fetchCanceledAndClosedOrders() requires a symbol argument for trigger orders') + params = self.omit(params, 'trigger') + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "instType": "SWAP", + # "instId": "DOGE-USDT-SWAP", + # "ordId": "1001110510915416", + # "px": "0", + # "sz": "76", + # "triggerPx": "0", + # "triggerPxType": "last", + # "ordType": "TPSL", + # "side": "sell", + # "posSide": "long", + # "tdMode": "cross", + # "lever": "2", + # "triggerTime": "0", + # "uTime": "1761059366000", + # "cTime": "1761059218", + # "errorCode": "0", + # "errorMsg": "" + # } + # ] + # } + # + response = await self.privateGetDeepcoinTradeTriggerOrdersHistory(self.extend(request, params)) + else: + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "instType": "SPOT", + # "instId": "ETH-USDT", + # "tgtCcy": "", + # "ccy": "", + # "ordId": "1001434573319675", + # "clOrdId": "", + # "tag": "", + # "px": "4056.620000000000", + # "sz": "0.004000", + # "pnl": "0.000000", + # "ordType": "market", + # "side": "buy", + # "posSide": "", + # "tdMode": "cash", + # "accFillSz": "0.004000", + # "fillPx": "", + # "tradeId": "", + # "fillSz": "0.004000", + # "fillTime": "1760619119000", + # "avgPx": "", + # "state": "filled", + # "lever": "1.000000", + # "tpTriggerPx": "", + # "tpTriggerPxType": "", + # "tpOrdPx": "", + # "slTriggerPx": "", + # "slTriggerPxType": "", + # "slOrdPx": "", + # "feeCcy": "USDT", + # "fee": "0.000004", + # "rebateCcy": "", + # "source": "", + # "rebate": "", + # "category": "normal", + # "uTime": "1760619119000", + # "cTime": "1760619119000" + # } + # ] + # } + # + response = await self.privateGetDeepcoinTradeOrdersHistory(self.extend(request, params)) + # todo handle with since, until and pagination + data = self.safe_list(response, 'data', []) + return self.parse_orders(data, market, since, limit) + + async def fetch_canceled_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple canceled orders made by the user + + https://www.deepcoin.com/docs/DeepCoinTrade/ordersHistory + + :param str symbol: unified market symbol of the market the orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: 'spot' or 'swap', the market type for the orders + :returns dict[]: a list of `order structures ` + """ + methodName = 'fetchCanceledOrders' + params = self.extend(params, {'methodName': methodName}) + params = self.extend(params, {'state': 'canceled'}) + return await self.fetch_canceled_and_closed_orders(symbol, since, limit, params) + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://www.deepcoin.com/docs/DeepCoinTrade/ordersHistory + + :param str symbol: unified market symbol of the market the orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: 'spot' or 'swap', the market type for the orders + :returns dict[]: a list of `order structures ` + """ + methodName = 'fetchClosedOrders' + params = self.extend(params, {'methodName': methodName}) + params = self.extend(params, {'state': 'filled'}) + return await self.fetch_canceled_and_closed_orders(symbol, since, limit, params) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://www.deepcoin.com/docs/DeepCoinTrade/ordersPendingV2 + https://www.deepcoin.com/docs/DeepCoinTrade/triggerOrdersPending + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.trigger]: whether to fetch trigger/algo orders(default False) + :param int [params.index]: *non trigger orders only* pagination index, default is 1 + :param str [params.orderType]: *trigger orders only* 'limit' or 'market' + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOpenOrders() requires a symbol argument') + market = self.market(symbol) + index = self.safe_integer(params, 'index', 1) # todo add pagination handling + request: dict = { + 'instId': market['id'], + } + if limit is not None: + request['limit'] = limit + trigger = self.safe_bool(params, 'trigger', False) + response = None + if trigger: + params = self.omit(params, 'trigger') + request['instType'] = self.convert_to_instrument_type(market['type']) + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "instType": "SPOT", + # "instId": "DOGE-USDT", + # "ordId": "1001442305797142", + # "triggerPx": "0.01", + # "ordPx": "0.01", + # "sz": "20", + # "ordType": "", + # "side": "buy", + # "posSide": "", + # "tdMode": "cash", + # "triggerOrderType": "Conditional", + # "triggerPxType": "last", + # "lever": "", + # "slPrice": "", + # "slTriggerPrice": "", + # "tpPrice": "", + # "tpTriggerPrice": "", + # "closeSLTriggerPrice": "", + # "closeTPTriggerPrice": "", + # "cTime": "1761814167000", + # "uTime": "1761814167000" + # } + # ] + # } + # + response = await self.privateGetDeepcoinTradeTriggerOrdersPending(self.extend(request, params)) + else: + request['index'] = index + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "instType": "SPOT", + # "instId": "ETH-USDT", + # "tgtCcy": "", + # "ccy": "", + # "ordId": "1001435158096314", + # "clOrdId": "", + # "tag": "", + # "px": "1000.000000000000", + # "sz": "0.004000", + # "pnl": "0.000000", + # "ordType": "limit", + # "side": "buy", + # "posSide": "", + # "tdMode": "cash", + # "accFillSz": "0.000000", + # "fillPx": "", + # "tradeId": "", + # "fillSz": "0.000000", + # "fillTime": "1760695267000", + # "avgPx": "", + # "state": "live", + # "lever": "1", + # "tpTriggerPx": "", + # "tpTriggerPxType": "", + # "tpOrdPx": "", + # "slTriggerPx": "", + # "slTriggerPxType": "", + # "slOrdPx": "", + # "feeCcy": "USDT", + # "fee": "0.000000", + # "rebateCcy": "", + # "source": "", + # "rebate": "", + # "category": "normal", + # "uTime": "1760695267000", + # "cTime": "1760695267000" + # } + # ] + # } + # + response = await self.privateGetDeepcoinTradeV2OrdersPending(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + return self.parse_orders(data, market, since, limit, {'status': 'open'}) + + async def cancel_order(self, id: str, symbol: Str = None, params={}) -> Order: + """ + cancels an open order + + https://www.deepcoin.com/docs/DeepCoinTrade/cancelOrder + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.trigger]: whether the order is a trigger/algo order(default False) + :returns dict: An `order structure ` + """ + await self.load_markets() + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + 'ordId': id, + } + response = None + trigger = self.safe_bool(params, 'trigger', False) + if trigger: + params = self.omit(params, 'trigger') + response = await self.privatePostDeepcoinTradeCancelTriggerOrder(self.extend(request, params)) + else: + response = await self.privatePostDeepcoinTradeCancelOrder(self.extend(request, params)) + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data, market) + + async def cancel_all_orders(self, symbol: Str = None, params={}) -> List[Order]: + """ + cancel all open orders in a market + + https://www.deepcoin.com/docs/DeepCoinTrade/cancelAllOrder + + :param str symbol: unified market symbol of the market to cancel orders in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: *swap only* 'cross' or 'isolated', the default is 'cash' for spot and 'cross' for swap + :param bool [params.merged]: *swap only* True for merged positions, False for split positions(default True) + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelAllOrders() requires a symbol argument') + market = self.market(symbol) + if market['spot']: + raise NotSupported(self.id + ' cancelAllOrders() is not supported for spot markets') + productGroup = self.get_product_group_from_market(market) + marginMode = self.safe_string(params, 'marginMode') + encodedMarginMode = 1 + if marginMode is not None: + params = self.omit(params, 'marginMode') + if marginMode == 'isolated': + encodedMarginMode = 0 + merged = True + merged, params = self.handle_option_and_params(params, 'cancelAllOrders', 'merged', merged) + request: dict = { + 'InstrumentID': market['id'], + 'ProductGroup': productGroup, + 'IsCrossMargin': encodedMarginMode, + 'IsMergeMode': 1 if merged else 0, + } + response = await self.privatePostDeepcoinTradeSwapCancelAll(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + return self.parse_orders(data, market) + + async def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + """ + edit a trade order + + https://www.deepcoin.com/docs/DeepCoinTrade/replaceOrder + https://www.deepcoin.com/docs/DeepCoinTrade/replaceTPSL + + :param str id: cancel order id + :param str [symbol]: unified symbol of the market to create an order in(not used in deepcoin editOrder) + :param str [type]: 'market' or 'limit'(not used in deepcoin editOrder) + :param str [side]: 'buy' or 'sell'(not used in deepcoin editOrder) + :param float [amount]: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.stopLossPrice]: the price that a stop loss order is triggered at + :param float [params.takeProfitPrice]: the price that a take profit order is triggered at + :returns dict: an `order structure ` + """ + await self.load_markets() + request: dict = { + 'OrderSysID': id, + } + market: Market = None + if symbol is not None: + market = self.market(symbol) + if market['spot']: + raise NotSupported(self.id + ' editOrder() is not supported for spot markets') + symbol = market['symbol'] + stopLossPrice = self.safe_number(params, 'stopLossPrice') + takeProfitPrice = self.safe_number(params, 'takeProfitPrice') + isTPSL = (stopLossPrice is not None) or (takeProfitPrice is not None) + response = None + if isTPSL: + if (price is not None) or (amount is not None): + raise BadRequest(self.id + ' editOrder() with stopLossPrice or takeProfitPrice cannot have price or amount. Either use stopLossPrice/takeProfitPrice or price/amount to edit order.') + if stopLossPrice is not None: + request['slTriggerPx'] = self.price_to_precision(symbol, stopLossPrice) if symbol else self.number_to_string(stopLossPrice) + if takeProfitPrice is not None: + request['tpTriggerPx'] = self.price_to_precision(symbol, takeProfitPrice) if symbol else self.number_to_string(takeProfitPrice) + params = self.omit(params, ['stopLossPrice', 'takeProfitPrice']) + response = await self.privatePostDeepcoinTradeReplaceOrderSltp(self.extend(request, params)) + else: + if price is not None: + if symbol is not None: + request['price'] = self.price_to_precision(symbol, price) + else: + request['price'] = self.number_to_string(price) + if amount is not None: + if symbol is not None: + request['volume'] = self.amount_to_precision(symbol, amount) + else: + request['volume'] = self.number_to_string(amount) + response = await self.privatePostDeepcoinTradeReplaceOrder(self.extend(request, params)) + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data) + + async def cancel_orders(self, ids: List[str], symbol: Str = None, params={}) -> List[Order]: + """ + cancel multiple orders + :param str[] ids: order ids + :param str [symbol]: unified market symbol, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an list of `order structures ` + """ + await self.load_markets() + market: Market = None + if symbol is not None: + market = self.market(symbol) + if market['spot']: + raise NotSupported(self.id + ' cancelOrders() is not supported for spot markets') + request: dict = { + 'OrderSysIDs': ids, + } + response = await self.privatePostDeepcoinTradeBatchCancelOrder(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + return self.parse_orders(data, market) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # regular order + # { + # "instType": "SPOT", + # "instId": "ETH-USDT", + # "tgtCcy": "", + # "ccy": "", + # "ordId": "1001434573319675", + # "clOrdId": "", + # "tag": "", + # "px": "4056.620000000000", + # "sz": "0.004000", + # "pnl": "0.000000", + # "ordType": "market", + # "side": "buy", + # "posSide": "", + # "tdMode": "cash", + # "accFillSz": "0.004000", + # "fillPx": "", + # "tradeId": "", + # "fillSz": "0.004000", + # "fillTime": "1760619119000", + # "avgPx": "", + # "state": "filled", + # "lever": "1.000000", + # "tpTriggerPx": "", + # "tpTriggerPxType": "", + # "tpOrdPx": "", + # "slTriggerPx": "", + # "slTriggerPxType": "", + # "slOrdPx": "", + # "feeCcy": "USDT", + # "fee": "0.000004", + # "rebateCcy": "", + # "source": "", + # "rebate": "", + # "category": "normal", + # "uTime": "1760619119000", + # "cTime": "1760619119000" + # } + # + # trigger order + # { + # "instType": "SPOT", + # "instId": "DOGE-USDT", + # "ordId": "1001442305797142", + # "triggerPx": "0.01", + # "ordPx": "0.01", + # "sz": "20", + # "ordType": "", + # "side": "buy", + # "posSide": "", + # "tdMode": "cash", + # "triggerOrderType": "Conditional", + # "triggerPxType": "last", + # "lever": "", + # "slPrice": "", + # "slTriggerPrice": "", + # "tpPrice": "", + # "tpTriggerPrice": "", + # "closeSLTriggerPrice": "", + # "closeTPTriggerPrice": "", + # "cTime": "1761814167000", + # "uTime": "1761814167000" + # } + # + marketId = self.safe_string(order, 'instId') + market = self.safe_market(marketId, market) + timestamp = self.safe_integer(order, 'cTime') + timestampString = self.safe_string(order, 'cTime', '') + if len(timestampString) < 13: + timestamp = self.safe_timestamp(order, 'cTime') + state = self.safe_string(order, 'state') + orderType = self.safe_string(order, 'ordType') + average = self.safe_string(order, 'avgPx') + if average == '': + average = None + feeCurrencyId = self.safe_string(order, 'feeCcy') + fee = None + if feeCurrencyId is not None: + feeCost = self.safe_string(order, 'fee') + fee = { + 'cost': self.parse_number(feeCost), + 'currency': self.safe_currency_code(feeCurrencyId), + } + return self.safe_order({ + 'id': self.safe_string(order, 'ordId'), + 'clientOrderId': self.safe_string(order, 'clOrdId'), + 'datetime': self.iso8601(timestamp), + 'timestamp': timestamp, + 'lastTradeTimestamp': None, + 'lastUpdateTimestamp': self.safe_integer(order, 'uTime'), + 'status': self.parse_order_status(state), + 'symbol': market['symbol'], + 'type': self.parse_order_type(orderType), + 'timeInForce': self.parse_order_time_in_force(orderType), + 'side': self.safe_string(order, 'side'), + 'price': self.safe_string_2(order, 'px', 'ordPx'), + 'average': average, + 'amount': self.safe_string(order, 'sz'), + 'filled': self.safe_string(order, 'accFillSz'), + 'remaining': None, + 'triggerPrice': self.omit_zero(self.safe_string(order, 'triggerPx')), + 'takeProfitPrice': self.safe_string_2(order, 'tpTriggerPx', 'tpTriggerPrice'), + 'stopLossPrice': self.safe_string_2(order, 'slTriggerPx', 'slTriggerPrice'), + 'cost': None, + 'trades': None, + 'fee': fee, + 'reduceOnly': None, + 'postOnly': (orderType == 'post_only') if orderType else None, + 'info': order, + }, market) + + def parse_order_status(self, status: Str) -> Str: + statuses = { + 'live': 'open', + 'filled': 'closed', + 'canceled': 'canceled', + 'partially_filled': 'open', + } + return self.safe_string(statuses, status, status) + + def parse_order_type(self, type: Str) -> Str: + types = { + 'limit': 'limit', + 'market': 'market', + 'post_only': 'limit', + 'ioc': 'market', + 'TPSL': 'market', + } + return self.safe_string(types, type, type) + + def parse_order_time_in_force(self, type: Str) -> Str: + timeInForces = { + 'post_only': 'PO', + 'ioc': 'IOC', + 'limit': 'GTC', + 'market': 'GTC', + } + return self.safe_string(timeInForces, type, type) + + async def fetch_positions_for_symbol(self, symbol: str, params={}) -> List[Position]: + """ + fetch open positions for a single market + + https://www.deepcoin.com/docs/DeepCoinAccount/accountPositions + + fetch all open positions for specific symbol + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + await self.load_markets() + market = self.market(symbol) + instrumentType = self.convert_to_instrument_type(market['type']) + request: dict = { + 'instType': instrumentType, + 'instId': market['id'], + } + response = await self.privateGetDeepcoinAccountPositions(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + return self.parse_positions(data, [market['symbol']]) + + async def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://www.deepcoin.com/docs/DeepCoinAccount/accountPositions + + :param str[] [symbols]: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, True, True) + marketType = 'swap' + market: Market = None + if symbols is not None: + firstSymbol = self.safe_string(symbols, 0) + market = self.market(firstSymbol) + marketType, params = self.handle_market_type_and_params('fetchPositions', market, params, marketType) + instrumentType = self.convert_to_instrument_type(marketType) + request: dict = { + 'instType': instrumentType, + } + response = await self.privateGetDeepcoinAccountPositions(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "instType": "SWAP", + # "mgnMode": "cross", + # "instId": "DOGE-USDT-SWAP", + # "posId": "1001110099878275", + # "posSide": "long", + # "pos": "20", + # "avgPx": "0.18408", + # "lever": "75", + # "liqPx": "0.00001", + # "useMargin": "0.049088", + # "mrgPosition": "merge", + # "ccy": "USDT", + # "uTime": "1760709419000", + # "cTime": "1760709419000" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_positions(data, symbols) + + def parse_position(self, position: dict, market: Market = None) -> Position: + # + # { + # "instType": "SWAP", + # "mgnMode": "cross", + # "instId": "DOGE-USDT-SWAP", + # "posId": "1001110099878275", + # "posSide": "long", + # "pos": "20", + # "avgPx": "0.18408", + # "lever": "75", + # "liqPx": "0.00001", + # "useMargin": "0.049088", + # "mrgPosition": "merge", + # "ccy": "USDT", + # "uTime": "1760709419000", + # "cTime": "1760709419000" + # } + # + marketId = self.safe_string(position, 'instId') + market = self.safe_market(marketId, market) + timestamp = self.safe_integer(position, 'cTime') + return self.safe_position({ + 'symbol': market['symbol'], + 'id': self.safe_string(position, 'posId'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'contracts': self.safe_string(position, 'pos'), + 'contractSize': None, + 'side': self.safe_string(position, 'posSide'), + 'notional': None, + 'leverage': self.omit_zero(self.safe_string(position, 'lever')), + 'unrealizedPnl': None, + 'realizedPnl': None, + 'collateral': None, + 'entryPrice': self.safe_string(position, 'avgPx'), + 'markPrice': None, + 'liquidationPrice': self.safe_string(position, 'liqPx'), + 'marginMode': self.safe_string(position, 'mgnMode'), + 'hedged': True, + 'maintenanceMargin': self.safe_string(position, 'useMargin'), + 'maintenanceMarginPercentage': None, + 'initialMargin': None, + 'initialMarginPercentage': None, + 'marginRatio': None, + 'lastUpdateTimestamp': self.safe_integer(position, 'uTime'), + 'lastPrice': None, + 'stopLossPrice': None, + 'takeProfitPrice': None, + 'percentage': None, + 'info': position, + }) + + async def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://www.deepcoin.com/docs/DeepCoinAccount/accountSetLeverage + + :param float leverage: the rate of leverage + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated'(default is cross) + :param str [params.mrgPosition]: 'merge' or 'split', default is merge + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setLeverage() requires a symbol argument') + # WARNING: THIS WILL INCREASE LIQUIDATION PRICE FOR OPEN ISOLATED LONG POSITIONS + # AND DECREASE LIQUIDATION PRICE FOR OPEN ISOLATED SHORT POSITIONS + if leverage < 1: + raise BadRequest(self.id + ' setLeverage() leverage should be minimum 1') + await self.load_markets() + market = self.market(symbol) + marginMode = 'cross' + marginMode, params = self.handle_margin_mode_and_params('setLeverage', params, marginMode) + if (marginMode != 'cross') and (marginMode != 'isolated'): + raise BadRequest(self.id + ' setLeverage() requires a marginMode parameter that must be either cross or isolated') + mrgPosition = 'merge' + mrgPosition, params = self.handle_option_and_params(params, 'setLeverage', 'mrgPosition', mrgPosition) + if mrgPosition != 'merge' and mrgPosition != 'split': + raise BadRequest(self.id + ' setLeverage() mrgPosition parameter must be either merge or split') + request: dict = { + 'lever': leverage, + 'mgnMode': marginMode, + 'instId': market['id'], + 'mrgPosition': mrgPosition, + } + response = await self.privatePostDeepcoinAccountSetLeverage(self.extend(request, params)) + # + # { + # code: '0', + # msg: '', + # data: { + # instId: 'ETH-USDT-SWAP', + # lever: '2', + # mgnMode: 'cross', + # mrgPosition: 'merge', + # sCode: '0', + # sMsg: '' + # } + # } + # + return response + + async def fetch_funding_rates(self, symbols: Strings = None, params={}) -> FundingRates: + """ + fetch the funding rate for multiple markets + + https://www.deepcoin.com/docs/DeepCoinTrade/currentFundRate + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.subType]: "linear" or "inverse" + :returns dict[]: a list of `funding rate structures `, indexed by market symbols + """ + await self.load_markets() + symbols = self.market_symbols(symbols, 'swap', True, True, True) + subType = 'linear' + firstMarket: Market = None + if symbols is not None: + firstSymbol = self.safe_string(symbols, 0) + firstMarket = self.market(firstSymbol) + subType, params = self.handle_sub_type_and_params('fetchFundingRates', firstMarket, params, subType) + instType = 'SwapU' + if subType == 'inverse': + instType = 'Swap' + elif subType != 'linear': + raise BadRequest(self.id + ' fetchFundingRates() subType parameter must be either linear or inverse') + request: dict = { + 'instType': instType, + } + response = await self.privateGetDeepcoinTradeFundRateCurrentFundingRate(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "", + # "data": { + # "current_fund_rates": [ + # { + # "instrumentId": "SPKUSDT", + # "fundingRate": 0.00005 + # }, + # { + # "instrumentId": "LAUNCHCOINUSDT", + # "fundingRate": 0.00005 + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + rates = self.safe_list(data, 'current_fund_rates', []) + return self.parse_funding_rates(rates, symbols) + + async def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate + + https://www.deepcoin.com/docs/DeepCoinTrade/currentFundRate + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + await self.load_markets() + market = self.market(symbol) + if not market['swap']: + raise ExchangeError(self.id + ' fetchFundingRate() is only valid for swap markets') + request: dict = { + 'instId': market['id'], + 'instType': self.get_product_group_from_market(market), + } + response = await self.privateGetDeepcoinTradeFundRateCurrentFundingRate(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "", + # "data": { + # "current_fund_rates": [ + # { + # "instrumentId": "ETHUSDT", + # "fundingRate": 0.0000402356250176 + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + rates = self.safe_list(data, 'current_fund_rates', []) + entry = self.safe_dict(rates, 0, {}) + return self.parse_funding_rate(entry, market) + + def parse_funding_rate(self, contract, market: Market = None) -> FundingRate: + # + # { + # "instrumentId": "ETHUSDT", + # "fundingRate": 0.0000402356250176 + # } + # + marketId = self.safe_string_2(contract, 'instrumentId', 'instrumentID') + symbol = self.safe_symbol(marketId, market) + return { + 'info': contract, + 'symbol': symbol, + 'markPrice': None, + 'indexPrice': None, + 'interestRate': None, + 'estimatedSettlePrice': None, + 'timestamp': None, + 'datetime': None, + 'fundingRate': self.safe_number(contract, 'fundingRate'), + 'fundingTimestamp': None, + 'fundingDatetime': None, + 'nextFundingRate': None, + 'nextFundingTimestamp': None, + 'nextFundingDatetime': None, + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': None, + } + + async def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical funding rate prices + + https://www.deepcoin.com/docs/DeepCoinTrade/fundingRateHistory + + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: timestamp in ms of the earliest funding rate to fetch + :param int [limit]: the maximum amount of `funding rate structures ` to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.page]: pagination page number + :returns dict[]: a list of `funding rate structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchFundingRateHistory() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + } + if limit is not None: + request['size'] = limit # default 20, max 100 + response = await self.privateGetDeepcoinTradeFundRateHistory(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "", + # "data": { + # "rows": [ + # { + # "instrumentID": "ETHUSD", + # "rate": "0.00046493", + # "CreateTime": 1760860800, + # "ratePeriodSec": 0 + # }, + # { + # "instrumentID": "ETHUSD", + # "rate": "0.00047949", + # "CreateTime": 1760832000, + # "ratePeriodSec": 0 + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + rows = self.safe_list(data, 'rows', []) + return self.parse_funding_rate_histories(rows, market, since, limit) + + def parse_funding_rate_history(self, info, market: Market = None): + # + # { + # "instrumentID": "ETHUSD", + # "rate": "0.00047949", + # "CreateTime": 1760832000, + # "ratePeriodSec": 0 + # } + # + timestamp = self.safe_timestamp(info, 'CreateTime') + instrumentID = self.safe_string_2(info, 'instrumentID', 'instrumentId') + market = self.safe_market(instrumentID, market, None, 'swap') + return { + 'info': info, + 'symbol': market['symbol'], + 'fundingRate': self.safe_number(info, 'rate'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://www.deepcoin.com/docs/DeepCoinTrade/tradeFills + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest trade to fetch + :param str [params.type]: 'spot' or 'swap', the market type for the trades(default is 'spot') + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchMyTrades', 'paginate') + if paginate: + return await self.fetch_paginated_call_dynamic('fetchMyTrades', symbol, since, limit, params) + market: Market = None + if symbol is not None: + market = self.market(symbol) + marketType = 'spot' + marketType, params = self.handle_market_type_and_params('fetchMyTrades', market, params, marketType) + request: dict = { + 'instType': self.convert_to_instrument_type(marketType), + } + if market is not None: + request['instId'] = market['id'] + if since is not None: + request['begin'] = since + if limit is not None: + request['limit'] = limit # default 100, max 100 + until = self.safe_integer(params, 'until') + if until is not None: + params = self.omit(params, 'until') + request['end'] = until + response = await self.privateGetDeepcoinTradeFills(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "instType": "SPOT", + # "instId": "ETH-USDT", + # "tradeId": "1001056429613610", + # "ordId": "1001435238208686", + # "clOrdId": "", + # "billId": "10010564296136101", + # "tag": "", + # "fillPx": "3791.15", + # "fillSz": "0.004", + # "side": "sell", + # "posSide": "", + # "execType": "", + # "feeCcy": "USDT", + # "fee": "0.0151646", + # "ts": "1760704540000" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_trades(data, market, since, limit) + + async def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all the trades made from a single order + + https://www.deepcoin.com/docs/DeepCoinTrade/tradeFills + + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: 'spot' or 'swap', the market type for the trades + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + marketType = self.safe_string(params, 'type') + if symbol is None and marketType is None: + raise ArgumentsRequired(self.id + ' fetchOrderTrades requires a symbol argument or a market type in the params') + params = self.extend({'ordId': id}, params) + return await self.fetch_my_trades(symbol, since, limit, params) + + async def close_position(self, symbol: str, side: OrderSide = None, params={}) -> Order: + """ + closes open positions for a market + + https://www.deepcoin.com/docs/DeepCoinTrade/batchClosePosition + https://www.deepcoin.com/docs/DeepCoinTrade/closePositionByIds + + :param str symbol: Unified CCXT market symbol + :param str [side]: not used by deepcoin + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str|None [params.positionId]: the id of the position you would like to close + :param str[]|None [params.positionIds]: list of position ids to close(for batch closing) + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + productGroup = self.get_product_group_from_market(market) + positionId = self.safe_string(params, 'positionId') + positionIds = self.safe_list(params, 'positionIds') + request: dict = { + 'instId': market['id'], + 'productGroup': productGroup, + } + response = None + if positionId is None and positionIds is None: + response = await self.privatePostDeepcoinTradeBatchClosePosition(self.extend(request, params)) + else: + if positionId is not None: + params = self.omit(params, 'positionId') + request['positionIds'] = [positionId] + response = await self.privatePostDeepcoinTradeClosePositionByIds(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + return self.parse_order(data, market) + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + requestPath = path + if method == 'GET': + query = self.urlencode(params) + if len(query): + requestPath += '?' + query + url = self.urls['api'][api] + '/' + requestPath + if api == 'private': + self.check_required_credentials() + timestamp = self.milliseconds() + dateTime = self.iso8601(timestamp) + payload = dateTime + method + '/' + requestPath + headers = { + 'DC-ACCESS-KEY': self.apiKey, + 'DC-ACCESS-TIMESTAMP': dateTime, + 'DC-ACCESS-PASSPHRASE': self.password, + 'appid': '200103', + } + if method != 'GET': + body = self.json(params) + headers['Content-Type'] = 'application/json' + payload += body + signature = self.hmac(self.encode(payload), self.encode(self.secret), hashlib.sha256, 'base64') + headers['DC-ACCESS-SIGN'] = signature + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + data = self.safe_dict(response, 'data', {}) + msg = self.safe_string(response, 'msg') + messageCode = self.safe_string(response, 'code') + sCode = self.safe_string(data, 'sCode') + sMsg = self.safe_string(data, 'sMsg') + errorCode = self.safe_string(data, 'errorCode') + if (msg is not None) and (msg == '') and (sMsg is not None): + msg = sMsg + errorList = self.safe_list(data, 'errorList') + if errorList is not None: + for i in range(0, len(errorList)): + entry = self.safe_dict(errorList, i, {}) + errorCode = self.safe_string(entry, 'errorCode') + feedback = self.id + ' ' + body + if (sCode is None) and (errorCode is not None): + sCode = errorCode + retCode = self.safe_string(data, 'retCode') + if (sCode is None) and (retCode is not None): + sCode = retCode + if (code != 200) or (messageCode != '0') or (sCode is not None and sCode != '0'): + self.throw_exactly_matched_exception(self.exceptions['exact'], messageCode, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], sCode, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], msg, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], msg, feedback) + raise ExchangeError(feedback) + else: + list = self.safe_list(data, 'list', []) + if ('list' in data) and (list is None): + raise NullResponse(feedback) + return None diff --git a/ccxt/async_support/defx.py b/ccxt/async_support/defx.py new file mode 100644 index 0000000..c2d21e7 --- /dev/null +++ b/ccxt/async_support/defx.py @@ -0,0 +1,2072 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.defx import ImplicitAPI +import asyncio +import hashlib +from ccxt.base.types import Any, Balances, Currency, Int, LedgerEntry, Leverage, Market, Num, Order, OrderBook, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, Trade, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import NotSupported +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class defx(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(defx, self).describe(), { + 'id': 'defx', + 'name': 'Defx X', + # 'countries': [''], + 'rateLimit': 100, + 'version': 'v1', + 'certified': False, + 'pro': False, + 'hostname': 'defx.com', + 'dex': True, + 'has': { + 'CORS': None, + 'spot': False, + 'margin': False, + 'swap': True, + 'future': False, + 'option': False, + 'addMargin': True, + 'cancelAllOrders': True, + 'cancelAllOrdersAfter': False, + 'cancelOrder': True, + 'cancelWithdraw': False, + 'closeAllPositions': True, + 'closePosition': True, + 'createConvertTrade': False, + 'createDepositAddress': False, + 'createMarketBuyOrderWithCost': False, + 'createMarketOrder': False, + 'createMarketOrderWithCost': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createOrderWithTakeProfitAndStopLoss': True, + 'createReduceOnlyOrder': True, + 'createStopLimitOrder': False, + 'createStopLossOrder': False, + 'createStopMarketOrder': False, + 'createStopOrder': False, + 'createTakeProfitOrder': True, + 'createTrailingAmountOrder': False, + 'createTrailingPercentOrder': False, + 'createTriggerOrder': True, + 'fetchAccounts': False, + 'fetchBalance': True, + 'fetchCanceledOrders': True, + 'fetchClosedOrder': False, + 'fetchClosedOrders': True, + 'fetchConvertCurrencies': False, + 'fetchConvertQuote': False, + 'fetchConvertTrade': False, + 'fetchConvertTradeHistory': False, + 'fetchCurrencies': False, + 'fetchDepositAddress': False, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': False, + 'fetchDepositsWithdrawals': False, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': True, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchIndexOHLCV': False, + 'fetchLedger': True, + 'fetchLeverage': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrice': False, + 'fetchMarkPrices': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterestHistory': False, + 'fetchOpenOrder': False, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchOrderTrades': False, + 'fetchPosition': True, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': True, + 'fetchPositionsHistory': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchStatus': True, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': False, + 'fetchTransactions': False, + 'fetchTransfers': False, + 'fetchWithdrawals': False, + 'reduceMargin': False, + 'sandbox': True, + 'setLeverage': True, + 'setMargin': False, + 'setPositionMode': False, + 'transfer': False, + 'withdraw': True, + }, + 'timeframes': { + '1m': '1m', + '3m': '3m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1h', + '2h': '2h', + '4h': '4h', + '12h': '12h', + '1d': '1d', + '1w': '1w', + '1M': '1M', + }, + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/4e92bace-d7a9-45ea-92be-122168dc87e4', + 'api': { + 'public': 'https://api.{hostname}', + 'private': 'https://api.{hostname}', + }, + 'test': { + 'public': 'https://api.testnet.{hostname}', + 'private': 'https://api.testnet.{hostname}', + }, + 'www': 'https://defx.com/home', + 'doc': [ + 'https://docs.defx.com/docs', + 'https://api-docs.defx.com/', + ], + 'fees': [ + '', + ], + 'referral': { + 'url': 'https://app.defx.com/join/6I2CZ7', + }, + }, + 'api': { + 'v1': { + 'public': { + 'get': { + 'healthcheck/ping': 1, + 'symbols/{symbol}/ohlc': 1, + 'symbols/{symbol}/trades': 1, + 'symbols/{symbol}/prices': 1, + 'symbols/{symbol}/ticker/24hr': 1, + 'symbols/{symbol}/depth/{level}/{slab}': 1, + 'ticker/24HrAgg': 1, + 'c/markets': 1, + 'c/markets/metadata': 1, + 'analytics/market/stats/newUsers': 1, + 'analytics/market/stats/tvl': 1, + 'analytics/market/stats/volumeByInstrument': 1, + 'analytics/market/stats/liquidation': 1, + 'analytics/market/stats/totalVolume': 1, + 'analytics/market/stats/openInterest': 1, + 'analytics/market/stats/totalTrades': 1, + 'analytics/market/stats/basis': 1, + 'analytics/market/stats/insuranceFund': 1, + 'analytics/market/stats/longAndShortRatio': 1, + 'analytics/market/stats/fundingRate': 1, + 'analytics/market/overview': 1, + 'explorer/search': 1, + 'explorer/transactions': 1, + 'explorer/blocks': 1, + }, + }, + 'private': { + 'get': { + 'api/order/{orderId}': 1, + 'api/orders': 1, + 'api/orders/oco/{parentOrderId}': 1, + 'api/trades': 1, + 'api/position/active': 1, + 'api/users/metadata/leverage': 1, + 'api/users/metadata/feeMultiplier': 1, + 'api/users/metadata/slippage': 1, + 'api/users/referral': 1, + 'api/users/apikeys': 1, + 'connection-signature-message/evm': 1, + 'api/users/profile/wallets': 1, + 'api/notifications': 1, + 'api/wallet/balance': 1, + 'api/wallet/transactions': 1, + 'api/analytics/user/overview': 1, + 'api/analytics/user/pnl': 1, + 'api/analytics/points/overview': 1, + 'api/analytics/points/history': 1, + }, + 'post': { + 'api/order': 1, + 'api/position/oco': 1, + 'api/users/socket/listenKeys': 1, + 'api/users/metadata/leverage': 1, + 'api/users/metadata/feeMultiplier': 1, + 'api/users/metadata/slippage': 1, + 'api/users/referral/recordReferralSignup': 1, + 'api/users/apikeys': 1, + 'api/users/profile/wallets': 1, + 'api/transfers/withdrawal': 1, + 'api/transfers/bridge/withdrawal': 1, + }, + 'put': { + 'api/position/updatePositionMargin': 1, + 'api/users/socket/listenKeys/{listenKey}': 1, + 'api/users/apikeys/{accessKey}/status': 1, + 'api/users/referral': 1, + }, + 'patch': { + 'api/users/apikeys/{accessKey}': 1, + }, + 'delete': { + 'api/orders/allOpen': 1, + 'api/order/{orderId}': 1, + 'api/position/{positionId}': 1, + 'api/position/all': 1, + 'api/users/socket/listenKeys/{listenKey}': 1, + 'api/users/apikeys/{accessKey}': 1, + }, + }, + }, + }, + 'fees': { + 'trading': { + 'tierBased': True, + 'percentage': True, + 'maker': self.parse_number('0.0002'), + 'taker': self.parse_number('0.0005'), + }, + }, + 'options': { + 'sandboxMode': False, + }, + 'features': { + 'spot': None, + 'forDerivatives': { + 'sandbox': True, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + # todo implement + 'triggerPriceType': { + 'last': True, + 'mark': True, + 'index': False, + }, + 'triggerDirection': False, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'selfTradePrevention': False, + 'trailing': False, + 'iceberg': False, + 'leverage': False, + 'marketBuyByCost': False, + 'marketBuyRequiresPrice': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': None, + 'untilDays': None, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': True, + 'limit': 100, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': 500, + 'daysBack': 100000, + 'untilDays': 100000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 500, + 'daysBack': 100000, + 'daysBackCanceled': 1, + 'untilDays': 100000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 1000, + }, + }, + 'swap': { + 'linear': { + 'extends': 'forDerivatives', + }, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'commonCurrencies': {}, + 'exceptions': { + 'exact': { + '404': BadRequest, # {"errorCode":404,"errorMessage":"Not Found"} + 'missing_auth_signature': AuthenticationError, # {"msg":"Missing auth signature","code":"missing_auth_signature"} + 'order_rejected': InvalidOrder, # {"success":false,"err":{"msg":"Order has already been rejected","code":"order_rejected"}} + 'invalid_order_id': InvalidOrder, # {"success":false,"err":{"msg":"Invalid order id","code":"invalid_order_id"}} + 'filter_lotsize_maxqty': InvalidOrder, # {"errorCode":"filter_lotsize_maxqty","errorMessage":"LOT_SIZE filter failed, quantity more than maxQty","errorData":{"maxQty":"5000.00"}} + 'filter_notional_min': InvalidOrder, # {"errorCode":"filter_notional_min","errorMessage":"NOTIONAL filter failed, Notional value of quote asset less than minNotional","errorData":{"minNotional":"100.00000000"}} + 'failed_index_price_up_multiplier_filter': InvalidOrder, # {"errorCode":"failed_index_price_up_multiplier_filter","errorMessage":"failed_index_price_up_multiplier_filter","errorData":{"maxPrice":"307.81241042"}} + 'no_open_orders': InvalidOrder, # {"errorMessage":"No open orders found","errorCode":"no_open_orders"} + 'active_position_not_found': InvalidOrder, # {"errorCode":"active_position_not_found","errorMessage":"Active position not found"} + 'position_inactive': InvalidOrder, # {"errorCode":"position_inactive","errorMessage":"Position is already inactive"} + 'invalid_position_id': InvalidOrder, # {"errorCode":"invalid_position_id","errorMessage":"Position id is invalid"} + 'Internal server error': ExchangeError, # {"msg":"Internal server error","code":"internal_server_error"} + }, + 'broad': { + 'Bad Request': BadRequest, # {"errorMessage":"Bad Request","data":[{"param":"symbol","message":"\"symbol\" must be one of [ETH_USDC, BTC_USDC, BNB_USDC, SOL_USDC, DOGE_USDC, TON_USDC, AVAX_USDC, WIF_USDC, KPEPE_USDC, KSHIB_USDC, KBONK_USDC, MOODENG_USDC, POPCAT_USDC, MOTHER_USDC]"}]} + }, + }, + 'precisionMode': TICK_SIZE, + }) + + async def fetch_status(self, params={}): + """ + the latest known information on the availability of the exchange API + + https://api-docs.defx.com/#4b03bb3b-a0fa-4dfb-b96c-237bde0ce9e6 + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `status structure ` + """ + response = await self.v1PublicGetHealthcheckPing(params) + # + # { + # "success": True, + # "t": 1709705048323, + # "v": "0.0.7", + # "msg": "A programmer’s wife tells him, “While you’re at the grocery store, buy some eggs.” He never comes back." + # } + # + status = None + success = self.safe_bool(response, 'success') + if success: + status = 'ok' + else: + status = 'error' + return { + 'status': status, + 'updated': None, + 'eta': None, + 'url': None, + 'info': response, + } + + async def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://api-docs.defx.com/#4b03bb3b-a0fa-4dfb-b96c-237bde0ce9e6 + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = await self.v1PublicGetHealthcheckPing(params) + # + # { + # "success": True, + # "t": 1709705048323, + # "v": "0.0.7", + # "msg": "A programmer’s wife tells him, “While you’re at the grocery store, buy some eggs.” He never comes back." + # } + # + return self.safe_integer(response, 't') + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for defx + + https://api-docs.defx.com/#73cce0c8-f842-4891-9145-01bb6d61324d + https://api-docs.defx.com/#24fd4e5b-840e-451e-99e0-7fea47c7f371 + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + request = { + 'type': 'perps', + } + promises = [ + self.v1PublicGetCMarkets(self.extend(request, params)), + self.v1PublicGetCMarketsMetadata(self.extend(request, params)), + ] + responses = await asyncio.gather(*promises) + # + # { + # "data": [ + # { + # "market": "DOGE_USDC", + # "candleWindows": [ + # "1m", + # "3m", + # "5m", + # "15m", + # "30m", + # "1h", + # "2h", + # "4h", + # "12h", + # "1d", + # "1w", + # "1M" + # ], + # "depthSlabs": [ + # "0.00001", + # "0.00005", + # "0.0001", + # "0.001", + # "0.01" + # ], + # "filters": [ + # { + # "filterType": "LOT_SIZE", + # "minQty": "1.00000", + # "maxQty": "1500000.00000", + # "stepSize": "1.00000" + # }, + # { + # "filterType": "MARKET_LOT_SIZE", + # "minQty": "1.00000", + # "maxQty": "750000.00000", + # "stepSize": "1.00000" + # }, + # { + # "filterType": "PRICE_FILTER", + # "minPrice": "0.00244000", + # "maxPrice": "30.00000000", + # "tickSize": "0.00001" + # }, + # { + # "filterType": "NOTIONAL", + # "minNotional": "100.00000000" + # }, + # { + # "filterType": "PERCENT_PRICE_BY_SIDE", + # "bidMultiplierUp": "1.5", + # "bidMultiplierDown": "0.5", + # "askMultiplierUp": "1.5", + # "askMultiplierDown": "0.5" + # }, + # { + # "filterType": "INDEX_PRICE_FILTER", + # "multiplierUp": "1.3", + # "multiplierDown": "0.7" + # } + # ], + # "cappedLeverage": "25", + # "maintenanceMarginTiers": [ + # { + # "tier": "1", + # "minMaintenanceMargin": "0", + # "maxMaintenanceMargin": "2500", + # "leverage": "25" + # }, + # { + # "tier": "2", + # "minMaintenanceMargin": "2500", + # "maxMaintenanceMargin": "12500", + # "leverage": "20" + # }, + # { + # "tier": "3", + # "minMaintenanceMargin": "12500", + # "maxMaintenanceMargin": "25000", + # "leverage": "15" + # }, + # { + # "tier": "4", + # "minMaintenanceMargin": "25000", + # "maxMaintenanceMargin": "50000", + # "leverage": "10" + # }, + # { + # "tier": "5", + # "minMaintenanceMargin": "50000", + # "maxMaintenanceMargin": "75000", + # "leverage": "8" + # }, + # { + # "tier": "6", + # "minMaintenanceMargin": "75000", + # "maxMaintenanceMargin": "125000", + # "leverage": "7" + # }, + # { + # "tier": "7", + # "minMaintenanceMargin": "125000", + # "maxMaintenanceMargin": "187500", + # "leverage": "5" + # }, + # { + # "tier": "8", + # "minMaintenanceMargin": "187500", + # "maxMaintenanceMargin": "250000", + # "leverage": "3" + # }, + # { + # "tier": "9", + # "minMaintenanceMargin": "250000", + # "maxMaintenanceMargin": "375000", + # "leverage": "2" + # }, + # { + # "tier": "10", + # "minMaintenanceMargin": "375000", + # "maxMaintenanceMargin": "500000", + # "leverage": "1" + # } + # ], + # "fees": { + # "maker": "0.08", + # "taker": "0.1" + # } + # }, + # ] + # } + # + activeMarkets = self.safe_list(responses[0], 'data') + activeMarketsByType = self.index_by(activeMarkets, 'market') + marketMetadatas = self.safe_list(responses[1], 'data') + for i in range(0, len(marketMetadatas)): + marketId = marketMetadatas[i]['market'] + status = None + if marketId in activeMarketsByType: + status = activeMarketsByType[marketId]['status'] + marketMetadatas[i]['status'] = status + return self.parse_markets(marketMetadatas) + + def parse_market(self, market: dict) -> Market: + marketId = self.safe_string(market, 'market') + parts = marketId.split('_') + baseId = self.safe_string(parts, 0) + quoteId = self.safe_string(parts, 1) + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + symbol = base + '/' + quote + ':' + quote + filters = self.safe_list(market, 'filters', []) + fees = self.safe_dict(market, 'fees', {}) + filtersByType = self.index_by(filters, 'filterType') + priceFilter = self.safe_dict(filtersByType, 'PRICE_FILTER', {}) + lotFilter = self.safe_dict(filtersByType, 'LOT_SIZE', {}) + marketLotFilter = self.safe_dict(filtersByType, 'MARKET_LOT_SIZE', {}) + notionalFilter = self.safe_dict(filtersByType, 'NOTIONAL', {}) + return { + 'id': marketId, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': quote, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': quoteId, + 'type': 'swap', + 'spot': False, + 'margin': False, + 'swap': True, + 'future': False, + 'option': False, + 'active': self.safe_string(market, 'status', '') == 'active', + 'contract': True, + 'linear': True, + 'inverse': False, + 'taker': self.safe_number(fees, 'taker'), + 'maker': self.safe_number(fees, 'maker'), + 'contractSize': self.parse_number('1'), + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.safe_number(lotFilter, 'stepSize'), + 'price': self.safe_number(priceFilter, 'tickSize'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': self.safe_number(market, 'cappedLeverage'), + }, + 'amount': { + 'min': self.safe_number(lotFilter, 'minQty'), + 'max': self.safe_number(lotFilter, 'maxQty'), + }, + 'price': { + 'min': self.safe_number(priceFilter, 'minPrice'), + 'max': self.safe_number(priceFilter, 'maxPrice'), + }, + 'cost': { + 'min': self.safe_number(notionalFilter, 'minNotional'), + 'max': None, + }, + 'market': { + 'min': self.safe_number(marketLotFilter, 'minQty'), + 'max': self.safe_number(marketLotFilter, 'maxQty'), + }, + }, + 'created': None, + 'info': market, + } + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://api-docs.defx.com/#fe6f81d0-2f3a-4eee-976f-c8fc8f4c5d56 + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = await self.v1PublicGetSymbolsSymbolTicker24hr(self.extend(request, params)) + # + # { + # "symbol": "BTC_USDC", + # "priceChange": "0", + # "priceChangePercent": "0", + # "weightedAvgPrice": "0", + # "lastPrice": "2.00", + # "lastQty": "10.000", + # "bestBidPrice": "1646.00", + # "bestBidQty": "10.000", + # "bestAskPrice": "1646.00", + # "bestAskQty": "10.000", + # "openPrice": "0.00", + # "highPrice": "0.00", + # "lowPrice": "0.00", + # "volume": "0.000", + # "quoteVolume": "0.00", + # "openTime": 1700142658697, + # "closeTime": 1700142658697, + # "openInterestBase": "1.000", + # "openInterestQuote": "0.43112300" + # } + # + return self.parse_ticker(response, market) + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://api-docs.defx.com/#8c61cfbd-40d9-410e-b014-f5b36eba51d1 + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + market = None + if symbols is not None: + symbols = self.market_symbols(symbols) + firstSymbol = self.safe_string(symbols, 0) + if firstSymbol is not None: + market = self.market(firstSymbol) + type = None + type, params = self.handle_market_type_and_params('fetchTickers', market, params) + if type == 'spot': + raise NotSupported(self.id + ' fetchTickers() is not supported for ' + type + ' markets') + response = await self.v1PublicGetTicker24HrAgg(params) + # + # { + # "ETH_USDC": { + # "priceChange": "0", + # "priceChangePercent": "0", + # "openPrice": "1646.15", + # "highPrice": "1646.15", + # "lowPrice": "1646.15", + # "lastPrice": "1646.15", + # "quoteVolume": "13.17", + # "volume": "0.008", + # "markPrice": "1645.15" + # } + # } + # + return self.parse_tickers(response, symbols) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # fetchTicker + # + # { + # "symbol": "BTC_USDC", + # "priceChange": "0", + # "priceChangePercent": "0", + # "weightedAvgPrice": "0", + # "lastPrice": "2.00", + # "lastQty": "10.000", + # "bestBidPrice": "1646.00", + # "bestBidQty": "10.000", + # "bestAskPrice": "1646.00", + # "bestAskQty": "10.000", + # "openPrice": "0.00", + # "highPrice": "0.00", + # "lowPrice": "0.00", + # "volume": "0.000", + # "quoteVolume": "0.00", + # "openTime": 1700142658697, + # "closeTime": 1700142658697, + # "openInterestBase": "1.000", + # "openInterestQuote": "0.43112300" + # } + # + # fetchTickers + # + # "ETH_USDC": { + # "priceChange": "0", + # "priceChangePercent": "0", + # "openPrice": "1646.15", + # "highPrice": "1646.15", + # "lowPrice": "1646.15", + # "lastPrice": "1646.15", + # "quoteVolume": "13.17", + # "volume": "0.008", + # "markPrice": "1645.15" + # } + # + # fetchMarkPrice + # + # { + # "markPrice": "100.00", + # "indexPrice": "100.00", + # "ltp": "101.34", + # "movingFundingRate": "0.08", + # "payoutFundingRate": "-0.03", + # "nextFundingPayout": 1711555532146 + # } + # + marketId = self.safe_string(ticker, 'symbol') + if marketId is not None: + market = self.market(marketId) + symbol = market['symbol'] + open = self.safe_string(ticker, 'openPrice') + high = self.safe_string(ticker, 'highPrice') + low = self.safe_string(ticker, 'lowPrice') + close = self.safe_string(ticker, 'lastPrice') + quoteVolume = self.safe_string(ticker, 'quoteVolume') + baseVolume = self.safe_string(ticker, 'volume') + percentage = self.safe_string(ticker, 'priceChangePercent') + change = self.safe_string(ticker, 'priceChange') + ts = self.safe_integer(ticker, 'closeTime') + if ts == 0: + ts = None + datetime = self.iso8601(ts) + bid = self.safe_string(ticker, 'bestBidPrice') + bidVolume = self.safe_string(ticker, 'bestBidQty') + ask = self.safe_string(ticker, 'bestAskPrice') + askVolume = self.safe_string(ticker, 'bestAskQty') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': ts, + 'datetime': datetime, + 'high': high, + 'low': low, + 'bid': bid, + 'bidVolume': bidVolume, + 'ask': ask, + 'askVolume': askVolume, + 'vwap': None, + 'open': open, + 'close': close, + 'last': None, + 'previousClose': None, + 'change': change, + 'percentage': percentage, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'markPrice': self.safe_string(ticker, 'markPrice'), + 'indexPrice': self.safe_string(ticker, 'indexPrice'), + 'info': ticker, + }, market) + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + + https://api-docs.defx.com/#54b71951-1472-4670-b5af-4c2dc41e73d0 + + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: max=1000, max=100 when since is defined and is less than(now - (999 * (timeframe in ms))) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch orders for + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + maxLimit = 1000 + if limit is None: + limit = maxLimit + limit = min(maxLimit, limit) + request: dict = { + 'symbol': market['id'], + 'interval': self.safe_string(self.timeframes, timeframe, timeframe), + 'limit': limit, + } + until = self.safe_integer_2(params, 'until', 'till') + params = self.omit(params, ['until', 'till']) + request['endTime'] = self.milliseconds() if (until is None) else until + if since is None: + request['startTime'] = 0 + else: + request['startTime'] = since + if until is None: + timeframeInSeconds = self.parse_timeframe(timeframe) + timeframeInMilliseconds = timeframeInSeconds * 1000 + totalTimeframeInMilliseconds = limit * timeframeInMilliseconds + request['endTime'] = self.sum(since, totalTimeframeInMilliseconds) + response = await self.v1PublicGetSymbolsSymbolOhlc(self.extend(request, params)) + # + # [ + # { + # "symbol": "BTC_USDC", + # "open": "0.00", + # "high": "0.00", + # "low": "0.00", + # "close": "0.00", + # "volume": "0.000", + # "quoteAssetVolume": "0.00", + # "takerBuyAssetVolume": "0.000", + # "takerBuyQuoteAssetVolume": "0.00", + # "numberOfTrades": 0, + # "start": 1702453663894, + # "end": 1702453663894, + # "isClosed": True + # } + # ] + # + return self.parse_ohlcvs(response, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # example response in fetchOHLCV + return [ + self.safe_integer(ohlcv, 'start'), + self.safe_number(ohlcv, 'open'), + self.safe_number(ohlcv, 'high'), + self.safe_number(ohlcv, 'low'), + self.safe_number(ohlcv, 'close'), + self.safe_number(ohlcv, 'volume'), + ] + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://api-docs.defx.com/#5865452f-ea32-4f13-bfbc-03af5f5574fd + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + maxLimit = 50 + if limit is None: + limit = maxLimit + limit = min(maxLimit, limit) + request: dict = { + 'symbol': market['id'], + 'limit': limit, + } + response = await self.v1PublicGetSymbolsSymbolTrades(self.extend(request, params)) + # + # [ + # { + # "buyerMaker": "false", + # "price": "2.0000", + # "qty": "10.0000", + # "symbol": "BTC_USDC", + # "timestamp": "1702453663894" + # } + # ] + # + return self.parse_trades(response, market, since, limit) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + fetch all trades made by the user + + https://api-docs.defx.com/#06b5b33c-2fc6-48de-896c-fc316f5871a7 + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['symbols'] = market['id'] + if limit is not None: + maxLimit = 100 + limit = min(maxLimit, limit) + request['pageSize'] = limit + response = await self.v1PrivateGetApiTrades(self.extend(request, params)) + # + # { + # "data": [ + # { + # "id": "0192f665-c05b-7ba0-a080-8b6c99083489", + # "orderId": "757730811259651728", + # "time": "2024-11-04T08:58:36.474Z", + # "symbol": "SOL_USDC", + # "side": "SELL", + # "price": "160.43600000", + # "qty": "1.00", + # "fee": "0.08823980", + # "role": "TAKER", + # "pnl": "0.00000000" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_trades(data, None, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades + # { + # "buyerMaker": "false", + # "price": "2.0000", + # "qty": "10.0000", + # "symbol": "BTC_USDC", + # "timestamp": "1702453663894" + # } + # + # fetchMyTrades + # { + # "id": "0192f665-c05b-7ba0-a080-8b6c99083489", + # "orderId": "757730811259651728", + # "time": "2024-11-04T08:58:36.474Z", + # "symbol": "SOL_USDC", + # "side": "SELL", + # "price": "160.43600000", + # "qty": "1.00", + # "fee": "0.08823980", + # "role": "TAKER", + # "pnl": "0.00000000" + # } + # + time = self.safe_string(trade, 'time') + timestamp = self.safe_integer(trade, 'timestamp', self.parse8601(time)) + marketId = self.safe_string(trade, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + price = self.safe_string(trade, 'price') + amount = self.safe_string(trade, 'qty') + id = self.safe_string(trade, 'id') + oid = self.safe_string(trade, 'orderId') + takerOrMaker = self.safe_string_lower(trade, 'role') + buyerMaker = self.safe_bool(trade, 'buyerMaker') + side = self.safe_string_lower(trade, 'side') + if buyerMaker is not None: + if buyerMaker: + side = 'sell' + else: + side = 'buy' + return self.safe_trade({ + 'id': id, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'side': side, + 'price': price, + 'amount': amount, + 'cost': None, + 'order': oid, + 'takerOrMaker': takerOrMaker, + 'type': None, + 'fee': { + 'cost': self.safe_string(trade, 'fee'), + 'currency': 'USDC', + }, + 'info': trade, + }, market) + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://api-docs.defx.com/#6c1a2971-8325-4e7d-9962-e0bfcaacf9c4 + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.slab]: slab from market.info.depthSlabs + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + if limit is None: + limit = 10 # limit must be one of [5, 10, 20] + marketInfo = self.safe_dict(market, 'info', {}) + slab = self.safe_list(marketInfo, 'depthSlabs', []) + request: dict = { + 'symbol': market['id'], + 'level': limit, + 'slab': self.safe_string(slab, 0), + } + response = await self.v1PublicGetSymbolsSymbolDepthLevelSlab(self.extend(request, params)) + # + # { + # "symbol": "ETH_USDC", + # "level": "5", + # "slab": "1", + # "lastTradeTimestamp": "1708313446812", + # "timestamp": "1708313446812", + # "bids": [ + # { + # "price": "1646.16", + # "qty": "0.001" + # } + # ], + # "asks": [ + # { + # "price": "1646.16", + # "qty": "0.001" + # } + # ] + # } + # + timestamp = self.safe_integer(response, 'timestamp') + return self.parse_order_book(response, symbol, timestamp, 'bids', 'asks', 'price', 'qty') + + async def fetch_mark_price(self, symbol: str, params={}) -> Ticker: + """ + fetches mark price for the market + + https://api-docs.defx.com/#12168192-4e7b-4458-a001-e8b80961f0b7 + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.subType]: "linear" or "inverse" + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + market = self.market(symbol) + request = { + 'symbol': market['id'], + } + response = await self.v1PublicGetSymbolsSymbolPrices(self.extend(request, params)) + # + # { + # "markPrice": "100.00", + # "indexPrice": "100.00", + # "ltp": "101.34", + # "movingFundingRate": "0.08", + # "payoutFundingRate": "-0.03", + # "nextFundingPayout": 1711555532146 + # } + # + return self.parse_ticker(response, market) + + async def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate + + https://api-docs.defx.com/#12168192-4e7b-4458-a001-e8b80961f0b7 + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + await self.load_markets() + market = self.market(symbol) + request = { + 'symbol': market['id'], + } + response = await self.v1PublicGetSymbolsSymbolPrices(self.extend(request, params)) + # + # { + # "markPrice": "100.00", + # "indexPrice": "100.00", + # "ltp": "101.34", + # "movingFundingRate": "0.08", + # "payoutFundingRate": "-0.03", + # "nextFundingPayout": 1711555532146 + # } + # + return self.parse_funding_rate(response, market) + + def parse_funding_rate(self, contract, market: Market = None) -> FundingRate: + # + # { + # "markPrice": "100.00", + # "indexPrice": "100.00", + # "ltp": "101.34", + # "movingFundingRate": "0.08", + # "payoutFundingRate": "-0.03", + # "nextFundingPayout": 1711555532146 + # } + # + markPrice = self.safe_number(contract, 'markPrice') + indexPrice = self.safe_number(contract, 'indexPrice') + fundingRateRaw = self.safe_string(contract, 'payoutFundingRate') + fundingRate = Precise.string_div(fundingRateRaw, '100') + fundingTime = self.safe_integer(contract, 'nextFundingPayout') + return { + 'info': contract, + 'symbol': market['symbol'], + 'markPrice': markPrice, + 'indexPrice': indexPrice, + 'interestRate': None, + 'estimatedSettlePrice': None, + 'timestamp': None, + 'datetime': None, + 'fundingRate': self.parse_number(fundingRate), + 'fundingTimestamp': fundingTime, + 'fundingDatetime': self.iso8601(fundingTime), + 'nextFundingRate': None, + 'nextFundingTimestamp': None, + 'nextFundingDatetime': None, + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': None, + } + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://api-docs.defx.com/#26414338-14f7-40a1-b246-f8ea8571493f + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + response = await self.v1PrivateGetApiWalletBalance(params) + # + # { + # "assets": [ + # { + # "asset": "USDC", + # "balance": "0.000" + # } + # ] + # } + # + data = self.safe_list(response, 'assets') + return self.parse_balance(data) + + def parse_balance(self, balances) -> Balances: + result: dict = { + 'info': balances, + } + for i in range(0, len(balances)): + balance = balances[i] + code = self.safe_currency_code(self.safe_string(balance, 'asset')) + account = self.account() + account['total'] = self.safe_string(balance, 'balance') + result[code] = account + return self.safe_balance(result) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://api-docs.defx.com/#ba222d88-8856-4d3c-87a9-7cec07bb2622 + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: The price a trigger order is triggered at + :param str [params.reduceOnly]: for swap and future reduceOnly is a string 'true' or 'false' that cant be sent with close position set to True or in hedge mode. For spot margin and option reduceOnly is a boolean. + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + reduceOnly = self.safe_bool_2(params, 'reduceOnly', 'reduce_only') + params = self.omit(params, ['reduceOnly', 'reduce_only']) + orderType = type.upper() + orderSide = side.upper() + request: dict = { + 'symbol': market['id'], + 'side': orderSide, + 'type': orderType, + } + takeProfitPrice = self.safe_string(params, 'takeProfitPrice') + triggerPrice = self.safe_string_2(params, 'stopPrice', 'triggerPrice') + isMarket = orderType == 'MARKET' + isLimit = orderType == 'LIMIT' + timeInForce = self.safe_string_upper(params, 'timeInForce') + if timeInForce is not None: + # GTC, IOC, FOK, AON + request['timeInForce'] = timeInForce + else: + if isLimit: + request['timeInForce'] = 'GTC' + if reduceOnly: + request['reduceOnly'] = reduceOnly + clientOrderId = self.safe_string(params, 'clientOrderId') + if clientOrderId is not None: + request['newClientOrderId'] = clientOrderId + if triggerPrice is not None or takeProfitPrice is not None: + request['workingType'] = 'MARK_PRICE' + if takeProfitPrice is not None: + request['stopPrice'] = self.price_to_precision(symbol, takeProfitPrice) + if isMarket: + request['type'] = 'TAKE_PROFIT_MARKET' + else: + request['type'] = 'TAKE_PROFIT_LIMIT' + else: + request['stopPrice'] = self.price_to_precision(symbol, triggerPrice) + if isMarket: + request['type'] = 'STOP_MARKET' + else: + request['type'] = 'STOP_LIMIT' + if isLimit and price is not None: + request['price'] = self.price_to_precision(symbol, price) + request['quantity'] = self.amount_to_precision(symbol, amount) + params = self.omit(params, ['clOrdID', 'clientOrderId', 'client_order_id', 'postOnly', 'timeInForce', 'stopPrice', 'triggerPrice', 'takeProfitPrice']) + response = await self.v1PrivatePostApiOrder(self.extend(request, params)) + # + # { + # "success": True, + # "data": { + # "orderId": "", + # "clientOrderId": "", + # "cumulativeQty": "", + # "cumulativeQuote": "", + # "executedQty": "", + # "avgPrice": "", + # "origQty": "", + # "price": "", + # "reduceOnly": True, + # "side": "", + # "status": "", + # "symbol": "", + # "timeInForce": "", + # "type": "", + # "workingType": "" + # } + # } + # + data = self.safe_dict(response, 'data') + return self.parse_order(data, market) + + def parse_order_status(self, status: Str): + if status is not None: + statuses: dict = { + 'NEW': 'open', + 'OPEN': 'open', + 'CANCELLED': 'canceled', + 'REJECTED': 'rejected', + 'FILLED': 'closed', + } + return self.safe_string(statuses, status, status) + return status + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # { + # "orderId": "746472647227344528", + # "createdAt": "2024-10-25T16:49:31.077Z", + # "updatedAt": "2024-10-25T16:49:31.378Z", + # "clientOrderId": "0192c495-49c3-71ee-b3d3-7442a2090807", + # "reduceOnly": False, + # "side": "SELL", + # "status": "FILLED", + # "symbol": "SOL_USDC", + # "timeInForce": "GTC", + # "type": "MARKET", + # "origQty": "0.80", + # "executedQty": "0.80", + # "cumulativeQuote": "137.87440000", + # "avgPrice": "172.34300000", + # "totalPnL": "0.00000000", + # "totalFee": "0.07583092", + # "workingType": null, + # "postOnly": False, + # "linkedOrderParentType": null, + # "isTriggered": False, + # "slippagePercentage": "5" + # } + # + orderId = self.safe_string(order, 'orderId') + clientOrderId = self.safe_string(order, 'clientOrderId') + marketId = self.safe_string(order, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + price = self.safe_string(order, 'price') + amount = self.safe_string(order, 'origQty') + orderType = self.safe_string_lower(order, 'type') + status = self.safe_string(order, 'status') + side = self.safe_string_lower(order, 'side') + filled = self.omit_zero(self.safe_string(order, 'executedQty')) + average = self.omit_zero(self.safe_string(order, 'avgPrice')) + timeInForce = self.safe_string_lower(order, 'timeInForce') + takeProfitPrice: Str = None + triggerPrice: Str = None + if orderType is not None: + if orderType.find('take_profit') >= 0: + takeProfitPrice = self.safe_string(order, 'stopPrice') + else: + triggerPrice = self.safe_string(order, 'stopPrice') + timestamp = self.parse8601(self.safe_string(order, 'createdAt')) + lastTradeTimestamp = self.parse8601(self.safe_string(order, 'updatedAt')) + return self.safe_order({ + 'id': orderId, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': lastTradeTimestamp, + 'lastUpdateTimestamp': lastTradeTimestamp, + 'status': self.parse_order_status(status), + 'symbol': symbol, + 'type': orderType, + 'timeInForce': timeInForce, + 'postOnly': self.safe_bool(order, 'postOnly'), + 'reduceOnly': self.safe_bool(order, 'reduceOnly'), + 'side': side, + 'price': price, + 'triggerPrice': triggerPrice, + 'takeProfitPrice': takeProfitPrice, + 'stopLossPrice': None, + 'average': average, + 'amount': amount, + 'filled': filled, + 'remaining': None, + 'cost': None, + 'trades': None, + 'fee': { + 'cost': self.safe_string(order, 'totalFee'), + 'currency': 'USDC', + }, + 'info': order, + }, market) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + + https://api-docs.defx.com/#09186f23-f8d1-4993-acf4-9974d8a6ddb0 + + cancels an open order + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + request: dict = { + 'orderId': id, + 'idType': 'orderId', + } + clientOrderId = self.safe_string_n(params, ['clOrdID', 'clientOrderId', 'client_order_id']) + isByClientOrder = clientOrderId is not None + if isByClientOrder: + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + market = self.market(symbol) + request['orderId'] = clientOrderId + request['idType'] = 'clientOrderId' + request['symbol'] = market['id'] + params = self.omit(params, ['clOrdID', 'clientOrderId', 'client_order_id']) + response = await self.v1PrivateDeleteApiOrderOrderId(self.extend(request, params)) + # + # { + # "success": True + # } + # + extendParams: dict = {'symbol': symbol} + if isByClientOrder: + extendParams['clientOrderId'] = clientOrderId + else: + extendParams['id'] = id + return self.extend(self.parse_order(response), extendParams) + + async def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders + + https://api-docs.defx.com/#db5531da-3692-4a53-841f-6ad6495f823a + + :param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbols': [market['id']], + } + response = await self.v1PrivateDeleteApiOrdersAllOpen(self.extend(request, params)) + # + # { + # "data": { + # "msg": "The operation of cancel all open order is done." + # } + # } + # + return [self.safe_order({'info': response})] + + async def fetch_position(self, symbol: str, params={}): + """ + fetch data on a single open contract trade position + + https://api-docs.defx.com/#d89dbb86-9aba-4f59-ac5d-a97ff25ea80e + + :param str symbol: unified market symbol of the market the position is held in, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `position structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchPosition() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = await self.v1PrivateGetApiPositionActive(self.extend(request, params)) + # + # { + # "data": [ + # { + # "positionId": "0192c495-4a68-70ee-9081-9d368bd16dfc", + # "symbol": "SOL_USDC", + # "positionSide": "SHORT", + # "entryPrice": "172.34300000", + # "quantity": "0.80", + # "marginAmount": "20.11561173", + # "marginAsset": "USDC", + # "pnl": "0.00000000" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + first = self.safe_dict(data, 0, {}) + return self.parse_position(first, market) + + async def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://api-docs.defx.com/#d89dbb86-9aba-4f59-ac5d-a97ff25ea80e + + :param str[] [symbols]: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + await self.load_markets() + response = await self.v1PrivateGetApiPositionActive(params) + # + # { + # "data": [ + # { + # "positionId": "0192c495-4a68-70ee-9081-9d368bd16dfc", + # "symbol": "SOL_USDC", + # "positionSide": "SHORT", + # "entryPrice": "172.34300000", + # "quantity": "0.80", + # "marginAmount": "20.11561173", + # "marginAsset": "USDC", + # "pnl": "0.00000000" + # } + # ] + # } + # + positions = self.safe_list(response, 'data', []) + return self.parse_positions(positions, symbols) + + def parse_position(self, position: dict, market: Market = None): + # + # { + # "positionId": "0192c495-4a68-70ee-9081-9d368bd16dfc", + # "symbol": "SOL_USDC", + # "positionSide": "SHORT", + # "entryPrice": "172.34300000", + # "quantity": "0.80", + # "marginAmount": "20.11561173", + # "marginAsset": "USDC", + # "pnl": "0.00000000" + # } + # + marketId = self.safe_string(position, 'symbol') + market = self.safe_market(marketId, market) + size = Precise.string_abs(self.safe_string(position, 'quantity')) + side = self.safe_string_lower(position, 'positionSide') + unrealisedPnl = self.omit_zero(self.safe_string(position, 'pnl')) + entryPrice = self.omit_zero(self.safe_string(position, 'entryPrice')) + initialMargin = self.safe_string(position, 'marginAmount') + return self.safe_position({ + 'info': position, + 'id': self.safe_string(position, 'positionId'), + 'symbol': market['symbol'], + 'timestamp': None, + 'datetime': None, + 'lastUpdateTimestamp': None, + 'initialMargin': self.parse_number(initialMargin), + 'initialMarginPercentage': None, + 'maintenanceMargin': None, + 'maintenanceMarginPercentage': None, + 'entryPrice': self.parse_number(entryPrice), + 'notional': None, + 'leverage': None, + 'unrealizedPnl': self.parse_number(unrealisedPnl), + 'realizedPnl': None, + 'contracts': self.parse_number(size), + 'contractSize': self.safe_number(market, 'contractSize'), + 'marginRatio': None, + 'liquidationPrice': None, + 'markPrice': None, + 'lastPrice': None, + 'collateral': None, + 'marginMode': None, + 'side': side, + 'percentage': None, + 'stopLossPrice': None, + 'takeProfitPrice': None, + 'hedged': None, + }) + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://api-docs.defx.com/#44f82dd5-26b3-4e1f-b4aa-88ceddd65237 + + :param str id: the order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + request: dict = { + 'orderId': id, + 'idType': 'orderId', + } + clientOrderId = self.safe_string_n(params, ['clOrdID', 'clientOrderId', 'client_order_id']) + params = self.omit(params, ['clOrdID', 'clientOrderId', 'client_order_id']) + if clientOrderId is not None: + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrder() requires a symbol argument') + market = self.market(symbol) + request['orderId'] = clientOrderId + request['idType'] = 'clientOrderId' + request['symbol'] = market['id'] + response = await self.v1PrivateGetApiOrderOrderId(self.extend(request, params)) + # + # { + # "success": True, + # "data": { + # "orderId": "555068654076559792", + # "createdAt": "2024-05-08T05:45:42.148Z", + # "updatedAt": "2024-05-08T05:45:42.166Z", + # "clientOrderId": "dummyClientOrderId", + # "reduceOnly": False, + # "side": "SELL", + # "status": "REJECTED", + # "symbol": "BTC_USDC", + # "timeInForce": "GTC", + # "type": "TAKE_PROFIT_MARKET", + # "origQty": "1.000", + # "executedQty": "0.000", + # "cumulativeQuote": "0.00", + # "avgPrice": "0.00", + # "stopPrice": "65000.00", + # "totalPnL": "0.00", + # "workingType": "MARK_PRICE", + # "postOnly": False + # } + # } + # + data = self.safe_dict(response, 'data') + return self.parse_order(data) + + async def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://api-docs.defx.com/#ab200038-8acb-4170-b05e-4fcb4cc13751 + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch orders for + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['symbols'] = market['id'] + until = self.safe_integer(params, 'until') + if until is not None: + params = self.omit(params, 'until') + request['end'] = self.iso8601(until) + if since is not None: + request['start'] = self.iso8601(since) + if limit is not None: + maxLimit = 100 + limit = min(maxLimit, limit) + request['pageSize'] = limit + response = await self.v1PrivateGetApiOrders(self.extend(request, params)) + # + # { + # "data": [ + # { + # "orderId": "746472647227344528", + # "createdAt": "2024-10-25T16:49:31.077Z", + # "updatedAt": "2024-10-25T16:49:31.378Z", + # "clientOrderId": "0192c495-49c3-71ee-b3d3-7442a2090807", + # "reduceOnly": False, + # "side": "SELL", + # "status": "FILLED", + # "symbol": "SOL_USDC", + # "timeInForce": "GTC", + # "type": "MARKET", + # "origQty": "0.80", + # "executedQty": "0.80", + # "cumulativeQuote": "137.87440000", + # "avgPrice": "172.34300000", + # "totalPnL": "0.00000000", + # "totalFee": "0.07583092", + # "workingType": null, + # "postOnly": False, + # "linkedOrderParentType": null, + # "isTriggered": False, + # "slippagePercentage": 5 + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_orders(data, None, since, limit) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://api-docs.defx.com/#ab200038-8acb-4170-b05e-4fcb4cc13751 + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch orders for + :returns Order[]: a list of `order structures ` + """ + req = { + 'statuses': 'OPEN', + } + return await self.fetch_orders(symbol, since, limit, self.extend(req, params)) + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://api-docs.defx.com/#ab200038-8acb-4170-b05e-4fcb4cc13751 + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch orders for + :returns Order[]: a list of `order structures ` + """ + req = { + 'statuses': 'FILLED', + } + return await self.fetch_orders(symbol, since, limit, self.extend(req, params)) + + async def fetch_canceled_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple canceled orders made by the user + + https://api-docs.defx.com/#ab200038-8acb-4170-b05e-4fcb4cc13751 + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch orders for + :returns Order[]: a list of `order structures ` + """ + req = { + 'statuses': 'CANCELED', + } + return await self.fetch_orders(symbol, since, limit, self.extend(req, params)) + + async def close_position(self, symbol: str, side: OrderSide = None, params={}) -> Order: + """ + closes an open position for a market + + https://api-docs.defx.com/#b2c08074-c4d9-4e50-b637-0d6c498fa29e + + :param str symbol: unified CCXT market symbol + :param str [side]: one-way mode: 'buy' or 'sell', hedge-mode: 'long' or 'short' + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.positionId]: the position id you want to close + :param str [params.type]: 'MARKET' or 'LIMIT' + :param str [params.quantity]: how much of currency you want to trade in units of base currency + :param str [params.price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :returns dict: An `order structure ` + """ + await self.load_markets() + positionId = self.safe_string(params, 'positionId') + if positionId is None: + raise ArgumentsRequired(self.id + ' closePosition() requires a positionId') + type = self.safe_string_upper(params, 'type') + if type is None: + raise ArgumentsRequired(self.id + ' closePosition() requires a type') + quantity = self.safe_string(params, 'quantity') + if quantity is None: + raise ArgumentsRequired(self.id + ' closePosition() requires a quantity') + request: dict = { + 'positionId': positionId, + 'type': type, + 'quantity': quantity, + } + if type != 'MARKET': + price = self.safe_string(params, 'price') + if price is None: + raise ArgumentsRequired(self.id + ' closePosition() requires a price') + request['price'] = price + params = self.omit(params, ['positionId', 'type', 'quantity', 'price']) + response = await self.v1PrivateDeleteApiPositionPositionId(self.extend(request, params)) + # + # {} + # + return response + + async def close_all_positions(self, params={}) -> List[Position]: + """ + closes all open positions for a market type + + https://api-docs.defx.com/#d6f63b43-100e-47a9-998c-8b6c0c72d204 + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: A list of `position structures ` + """ + await self.load_markets() + response = await self.v1PrivateDeleteApiPositionAll(params) + # + # { + # "data": [ + # { + # "positionId": "d6ca1a27-28ad-47ae-b244-0bda5ac37b2b", + # "success": True + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_positions(data, None, params) + + async def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + + https://api-docs.defx.com/#38cc8974-794f-48c0-b959-db045a0ee565 + + :param str [code]: unified currency code + :param int [since]: timestamp in ms of the earliest ledger entry + :param int [limit]: max number of ledger entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest ledger entry + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict: a `ledger structure ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchLedger', 'paginate') + if paginate: + return await self.fetch_paginated_call_dynamic('fetchLedger', code, since, limit, params) + request: dict = {} + if since is not None: + request['start'] = since + else: + request['start'] = 0 + until = self.safe_integer(params, 'until') + if until is not None: + params = self.omit(params, 'until') + request['end'] = until + else: + request['end'] = self.milliseconds() + response = await self.v1PrivateGetApiWalletTransactions(self.extend(request, params)) + data = self.safe_list(response, 'transactions', []) + return self.parse_ledger(data, None, since, limit) + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + # + # { + # "id": "01JCSZS6H5VQND3GF5P98SJ29C", + # "timestamp": 1731744012054, + # "type": "FundingFee", + # "amount": "0.02189287", + # "asset": "USDC", + # "operation": "CREDIT" + # } + # + amount = self.safe_string(item, 'amount') + currencyId = self.safe_string(item, 'asset') + code = self.safe_currency_code(currencyId, currency) + currency = self.safe_currency(currencyId, currency) + timestamp = self.safe_integer(item, 'timestamp') + type = self.safe_string(item, 'type') + return self.safe_ledger_entry({ + 'info': item, + 'id': self.safe_string(item, 'id'), + 'direction': None, + 'account': None, + 'referenceAccount': None, + 'referenceId': None, + 'type': self.parse_ledger_entry_type(type), + 'currency': code, + 'amount': self.parse_number(amount), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'before': None, + 'after': None, + 'status': None, + 'fee': None, + }, currency) + + def parse_ledger_entry_type(self, type): + ledgerType: dict = { + 'FundingFee': 'fee', + 'FeeRebate': 'fee', + 'FeeKickback': 'fee', + 'RealizedPnl': 'trade', + 'LiquidationClearance': 'trade', + 'Transfer': 'transfer', + 'ReferralPayout': 'referral', + 'Commission': 'commission', + } + return self.safe_string(ledgerType, type, type) + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://api-docs.defx.com/#2600f503-63ed-4672-b8f6-69ea5f03203b + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'amount': self.currency_to_precision(code, amount), + 'asset': currency['id'], + # 'network': 'ARB_SEPOLIA', + # 'chainId': '421614', + } + response = await self.v1PrivatePostApiTransfersBridgeWithdrawal(self.extend(request, params)) + # + # { + # "transactionId": "0x301e5851e5aefa733abfbc8b30817ca3b61601e0ddf1df8c59656fb888b0bc9c" + # } + # + return self.parse_transaction(response, currency) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # withdraw + # + # { + # "transactionId": "0x301e5851e5aefa733abfbc8b30817ca3b61601e0ddf1df8c59656fb888b0bc9c" + # } + # + txid = self.safe_string(transaction, 'transactionId') + return { + 'info': transaction, + 'id': None, + 'txid': txid, + 'timestamp': None, + 'datetime': None, + 'network': None, + 'address': None, + 'addressTo': None, + 'addressFrom': None, + 'tag': None, + 'tagTo': None, + 'tagFrom': None, + 'type': None, + 'amount': None, + 'currency': self.safe_currency_code(None, currency), + 'status': None, + 'updated': None, + 'internal': None, + 'comment': None, + 'fee': None, + } + + async def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://api-docs.defx.com/#4cb4ecc4-6c61-4194-8353-be67faaf7ca7 + + :param float leverage: the rate of leverage + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setLeverage() requires a symbol argument') + await self.load_markets() + request: dict = { + 'leverage': self.number_to_string(leverage), + } + market = self.market(symbol) + request['symbol'] = market['id'] + response = await self.v1PrivatePostApiUsersMetadataLeverage(self.extend(request, params)) + # + # { + # "success": True, + # "data": { + # "leverage": "11", + # "symbol": "BTC_USDC" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_leverage(data, market) + + def parse_leverage(self, leverage: dict, market: Market = None) -> Leverage: + # + # "data": { + # "leverage": "11", + # "symbol": "BTC_USDC" + # } + # + marketId = self.safe_string(leverage, 'symbol') + leverageValue = self.safe_integer(leverage, 'leverage') + return { + 'info': leverage, + 'symbol': self.safe_symbol(marketId, market), + 'marginMode': None, + 'longLeverage': leverageValue, + 'shortLeverage': leverageValue, + } + + def nonce(self): + return self.milliseconds() + + def sign(self, path, section='public', method='GET', params={}, headers=None, body=None): + version = section[0] + access = section[1] + pathWithParams = self.implode_params(path, params) + url = self.implode_hostname(self.urls['api'][access]) + url += '/' + version + '/' + params = self.omit(params, self.extract_params(path)) + params = self.keysort(params) + if access == 'public': + url += 'open/' + pathWithParams + if params: + url += '?' + self.rawencode(params) + else: + self.check_required_credentials() + headers = {'X-DEFX-SOURCE': 'ccxt'} + url += 'auth/' + pathWithParams + nonce = str(self.milliseconds()) + payload = nonce + if method == 'GET' or path == 'api/order/{orderId}': + payload += self.rawencode(params) + if params: + url += '?' + self.rawencode(params) + else: + if params is not None: + body = self.json(params) + payload += body + headers['Content-Type'] = 'application/json' + signature = self.hmac(self.encode(payload), self.encode(self.secret), hashlib.sha256) + headers['X-DEFX-APIKEY'] = self.apiKey + headers['X-DEFX-TIMESTAMP'] = nonce + headers['X-DEFX-SIGNATURE'] = signature + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if not response: + return None # fallback to default error handler + # {"errorCode":404,"errorMessage":"Not Found"} + # {"msg":"Missing auth signature","code":"missing_auth_signature"} + # {"success":false,"err":{"msg":"Invalid order id","code":"invalid_order_id"}} + success = self.safe_bool(response, 'success') + err = self.safe_dict(response, 'err', response) + errorCode = self.safe_string_2(err, 'errorCode', 'code') + if not success: + feedback = self.id + ' ' + self.json(response) + self.throw_broadly_matched_exception(self.exceptions['broad'], body, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + return None + + def default_network_code_for_currency(self, code): + currencyItem = self.currency(code) + networks = currencyItem['networks'] + networkKeys = list(networks.keys()) + for i in range(0, len(networkKeys)): + network = networkKeys[i] + if network == 'ETH': + return network + # if it was not returned according to above options, then return the first network of currency + return self.safe_value(networkKeys, 0) + + def set_sandbox_mode(self, enable: bool): + super(defx, self).set_sandbox_mode(enable) + self.options['sandboxMode'] = enable diff --git a/ccxt/async_support/delta.py b/ccxt/async_support/delta.py new file mode 100644 index 0000000..8354086 --- /dev/null +++ b/ccxt/async_support/delta.py @@ -0,0 +1,3665 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.delta import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currencies, Currency, DepositAddress, Greeks, Int, LedgerEntry, Leverage, MarginMode, MarginModification, Market, Num, Option, Order, OrderBook, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, FundingRates, Trade, MarketInterface +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class delta(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(delta, self).describe(), { + 'id': 'delta', + 'name': 'Delta Exchange', + 'countries': ['VC'], # Saint Vincent and the Grenadines + 'rateLimit': 300, + 'version': 'v2', + # new metainfo interface + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': True, + 'future': False, + 'option': True, + 'addMargin': True, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'closeAllPositions': True, + 'closePosition': False, + 'createOrder': True, + 'createReduceOnlyOrder': True, + 'editOrder': True, + 'fetchBalance': True, + 'fetchClosedOrders': True, + 'fetchCurrencies': True, + 'fetchDeposit': None, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': None, + 'fetchFundingHistory': False, + 'fetchFundingRate': True, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': True, + 'fetchGreeks': True, + 'fetchIndexOHLCV': True, + 'fetchLedger': True, + 'fetchLeverage': True, + 'fetchLeverageTiers': False, # An infinite number of tiers, see examples/js/delta-maintenance-margin-rate-max-leverage.js + 'fetchMarginMode': True, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': True, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': True, + 'fetchOpenOrders': True, + 'fetchOption': True, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchPosition': True, + 'fetchPositionMode': False, + 'fetchPositions': True, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': True, + 'fetchStatus': True, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTransfer': None, + 'fetchTransfers': None, + 'fetchUnderlyingAssets': False, + 'fetchVolatilityHistory': False, + 'fetchWithdrawal': None, + 'fetchWithdrawals': None, + 'reduceMargin': True, + 'setLeverage': True, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'transfer': False, + 'withdraw': False, + }, + 'timeframes': { + '1m': '1m', + '3m': '3m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1h', + '2h': '2h', + '4h': '4h', + '6h': '6h', + '1d': '1d', + '7d': '7d', + '1w': '1w', + '2w': '2w', + '1M': '30d', + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/99450025-3be60a00-2931-11eb-9302-f4fd8d8589aa.jpg', + 'test': { + 'public': 'https://testnet-api.delta.exchange', + 'private': 'https://testnet-api.delta.exchange', + }, + 'api': { + 'public': 'https://api.delta.exchange', + 'private': 'https://api.delta.exchange', + }, + 'www': 'https://www.delta.exchange', + 'doc': [ + 'https://docs.delta.exchange', + ], + 'fees': 'https://www.delta.exchange/fees', + 'referral': 'https://www.delta.exchange/app/signup/?code=IULYNB', + }, + 'api': { + 'public': { + 'get': [ + 'assets', + 'indices', + 'products', + 'products/{symbol}', + 'tickers', + 'tickers/{symbol}', + 'l2orderbook/{symbol}', + 'trades/{symbol}', + 'stats', + 'history/candles', + 'history/sparklines', + 'settings', + ], + }, + 'private': { + 'get': [ + 'orders', + 'orders/{order_id}', + 'orders/client_order_id/{client_oid}', + 'products/{product_id}/orders/leverage', + 'positions/margined', + 'positions', + 'orders/history', + 'fills', + 'fills/history/download/csv', + 'wallet/balances', + 'wallet/transactions', + 'wallet/transactions/download', + 'wallets/sub_accounts_transfer_history', + 'users/trading_preferences', + 'sub_accounts', + 'profile', + 'heartbeat', + 'deposits/address', + ], + 'post': [ + 'orders', + 'orders/bracket', + 'orders/batch', + 'products/{product_id}/orders/leverage', + 'positions/change_margin', + 'positions/close_all', + 'wallets/sub_account_balance_transfer', + 'heartbeat/create', + 'heartbeat', + 'orders/cancel_after', + 'orders/leverage', + ], + 'put': [ + 'orders', + 'orders/bracket', + 'orders/batch', + 'positions/auto_topup', + 'users/update_mmp', + 'users/reset_mmp', + ], + 'delete': [ + 'orders', + 'orders/all', + 'orders/batch', + ], + }, + }, + 'fees': { + 'trading': { + 'tierBased': True, + 'percentage': True, + 'taker': self.parse_number('0.0015'), + 'maker': self.parse_number('0.0010'), + 'tiers': { + 'taker': [ + [self.parse_number('0'), self.parse_number('0.0015')], + [self.parse_number('100'), self.parse_number('0.0013')], + [self.parse_number('250'), self.parse_number('0.0013')], + [self.parse_number('1000'), self.parse_number('0.001')], + [self.parse_number('5000'), self.parse_number('0.0009')], + [self.parse_number('10000'), self.parse_number('0.00075')], + [self.parse_number('20000'), self.parse_number('0.00065')], + ], + 'maker': [ + [self.parse_number('0'), self.parse_number('0.001')], + [self.parse_number('100'), self.parse_number('0.001')], + [self.parse_number('250'), self.parse_number('0.0009')], + [self.parse_number('1000'), self.parse_number('0.00075')], + [self.parse_number('5000'), self.parse_number('0.0006')], + [self.parse_number('10000'), self.parse_number('0.0005')], + [self.parse_number('20000'), self.parse_number('0.0005')], + ], + }, + }, + }, + 'userAgent': self.userAgents['chrome39'], # needed for C# + 'options': { + 'networks': { + 'TRC20': 'TRC20(TRON)', + 'BEP20': 'BEP20(BSC)', + }, + }, + 'features': { + 'default': { + 'sandbox': True, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, # todo implement + # todo implement + 'triggerPriceType': { + 'last': True, + 'mark': True, + 'index': True, + }, + 'triggerDirection': False, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': { + 'triggerPriceType': None, + 'price': True, + }, + # todo implementation + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'selfTradePrevention': False, + 'trailing': False, # todo: implement + 'iceberg': False, + 'leverage': False, + 'marketBuyByCost': False, + 'marketBuyRequiresPrice': False, + }, + 'createOrders': None, # todo: implement + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, # todo: revise + 'daysBack': 100000, + 'untilDays': 100000, + 'symbolRequired': False, + }, + 'fetchOrder': None, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 100, # todo: revise + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 500, + 'daysBack': 100000, + 'daysBackCanceled': 1, + 'untilDays': 100000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 2000, # todo: recheck + }, + }, + 'spot': { + 'extends': 'default', + }, + 'swap': { + 'linear': { + 'extends': 'default', + }, + 'inverse': { + 'extends': 'default', + }, + }, + 'future': { + 'linear': { + 'extends': 'default', + }, + 'inverse': { + 'extends': 'default', + }, + }, + }, + 'precisionMode': TICK_SIZE, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + }, + 'exceptions': { + 'exact': { + # Margin required to place order with selected leverage and quantity is insufficient. + 'insufficient_margin': InsufficientFunds, # {"error":{"code":"insufficient_margin","context":{"available_balance":"0.000000000000000000","required_additional_balance":"1.618626000000000000000000000"}},"success":false} + 'order_size_exceed_available': InvalidOrder, # The order book doesn't have sufficient liquidity, hence the order couldnt be filled, for example, ioc orders + 'risk_limits_breached': BadRequest, # orders couldn't be placed will breach allowed risk limits. + 'invalid_contract': BadSymbol, # The contract/product is either doesn't exist or has already expired. + 'immediate_liquidation': InvalidOrder, # Order will cause immediate liquidation. + 'out_of_bankruptcy': InvalidOrder, # Order prices are out of position bankruptcy limits. + 'self_matching_disrupted_post_only': InvalidOrder, # Self matching is not allowed during auction. + 'immediate_execution_post_only': InvalidOrder, # orders couldn't be placed includes post only orders which will be immediately executed + 'bad_schema': BadRequest, # {"error":{"code":"bad_schema","context":{"schema_errors":[{"code":"validation_error","message":"id is required","param":""}]}},"success":false} + 'invalid_api_key': AuthenticationError, # {"success":false,"error":{"code":"invalid_api_key"}} + 'invalid_signature': AuthenticationError, # {"success":false,"error":{"code":"invalid_signature"}} + 'open_order_not_found': OrderNotFound, # {"error":{"code":"open_order_not_found"},"success":false} + 'unavailable': ExchangeNotAvailable, # {"error":{"code":"unavailable"},"success":false} + }, + 'broad': { + }, + }, + }) + + def create_expired_option_market(self, symbol: str): + # support expired option contracts + quote = 'USDT' + optionParts = symbol.split('-') + symbolBase = symbol.split('/') + base = None + expiry = None + optionType = None + if symbol.find('/') > -1: + base = self.safe_string(symbolBase, 0) + expiry = self.safe_string(optionParts, 1) + optionType = self.safe_string(optionParts, 3) + else: + base = self.safe_string(optionParts, 1) + expiry = self.safe_string(optionParts, 3) + optionType = self.safe_string(optionParts, 0) + if expiry is not None: + expiry = expiry[4:] + expiry[2:4] + expiry[0:2] + settle = quote + strike = self.safe_string(optionParts, 2) + datetime = self.convert_expire_date(expiry) + timestamp = self.parse8601(datetime) + return { + 'id': optionType + '-' + base + '-' + strike + '-' + expiry, + 'symbol': base + '/' + quote + ':' + settle + '-' + expiry + '-' + strike + '-' + optionType, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': base, + 'quoteId': quote, + 'settleId': settle, + 'active': False, + 'type': 'option', + 'linear': None, + 'inverse': None, + 'spot': False, + 'swap': False, + 'future': False, + 'option': True, + 'margin': False, + 'contract': True, + 'contractSize': self.parse_number('1'), + 'expiry': timestamp, + 'expiryDatetime': datetime, + 'optionType': 'call' if (optionType == 'C') else 'put', + 'strike': self.parse_number(strike), + 'precision': { + 'amount': None, + 'price': None, + }, + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'info': None, + } + + def safe_market(self, marketId: Str = None, market: Market = None, delimiter: Str = None, marketType: Str = None) -> MarketInterface: + isOption = (marketId is not None) and ((marketId.endswith('-C')) or (marketId.endswith('-P')) or (marketId.startswith('C-')) or (marketId.startswith('P-'))) + if isOption and not (marketId in self.markets_by_id): + # handle expired option contracts + return self.create_expired_option_market(marketId) + return super(delta, self).safe_market(marketId, market, delimiter, marketType) + + async def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = await self.publicGetSettings(params) + # full response sample under `fetchStatus` + result = self.safe_dict(response, 'result', {}) + return self.safe_integer_product(result, 'server_time', 0.001) + + async def fetch_status(self, params={}): + """ + the latest known information on the availability of the exchange API + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `status structure ` + """ + response = await self.publicGetSettings(params) + # + # { + # "result": { + # "deto_liquidity_mining_daily_reward": "40775", + # "deto_msp": "1.0", + # "deto_staking_daily_reward": "23764.08", + # "enabled_wallets": [ + # "BTC", + # ... + # ], + # "portfolio_margin_params": { + # "enabled_portfolios": { + # ".DEAVAXUSDT": { + # "asset_id": 5, + # "futures_contingency_margin_percent": "1", + # "interest_rate": "0", + # "maintenance_margin_multiplier": "0.8", + # "max_price_shock": "20", + # "max_short_notional_limit": "2000", + # "options_contingency_margin_percent": "1", + # "options_discount_range": "10", + # "options_liq_band_range_percentage": "25", + # "settling_asset": "USDT", + # "sort_priority": 5, + # "underlying_asset": "AVAX", + # "volatility_down_shock": "30", + # "volatility_up_shock": "45" + # }, + # ... + # }, + # "portfolio_enabled_contracts": [ + # "futures", + # "perpetual_futures", + # "call_options", + # "put_options" + # ] + # }, + # "server_time": 1650640673500273, + # "trade_farming_daily_reward": "100000", + # "circulating_supply": "140000000", + # "circulating_supply_update_time": "1636752800", + # "deto_referral_mining_daily_reward": "0", + # "deto_total_reward_pool": "100000000", + # "deto_trade_mining_daily_reward": "0", + # "kyc_deposit_limit": "20", + # "kyc_withdrawal_limit": "10000", + # "maintenance_start_time": "1650387600000000", + # "msp_deto_commission_percent": "25", + # "under_maintenance": "false" + # }, + # "success": True + # } + # + result = self.safe_dict(response, 'result', {}) + underMaintenance = self.safe_string(result, 'under_maintenance') + status = 'maintenance' if (underMaintenance == 'true') else 'ok' + updated = self.safe_integer_product(result, 'server_time', 0.001, self.milliseconds()) + return { + 'status': status, + 'updated': updated, + 'eta': None, + 'url': None, + 'info': response, + } + + async def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://docs.delta.exchange/#get-list-of-all-assets + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = await self.publicGetAssets(params) + # + # { + # "result": [ + # { + # "base_withdrawal_fee": "0.005000000000000000", + # "id": "1", + # "interest_credit": False, + # "interest_slabs": null, + # "kyc_deposit_limit": "0.000000000000000000", + # "kyc_withdrawal_limit": "0.000000000000000000", + # "min_withdrawal_amount": "0.010000000000000000", + # "minimum_precision": "4", + # "name": "Ethereum", + # "networks": [ + # { + # "allowed_deposit_groups": null, + # "base_withdrawal_fee": "0.0025", + # "deposit_status": "enabled", + # "memo_required": False, + # "min_deposit_amount": "0.000050000000000000", + # "min_withdrawal_amount": "0.010000000000000000", + # "minimum_deposit_confirmations": "12", + # "network": "ERC20", + # "variable_withdrawal_fee": "0", + # "withdrawal_status": "enabled" + # }, + # { + # "allowed_deposit_groups": null, + # "base_withdrawal_fee": "0.0001", + # "deposit_status": "enabled", + # "memo_required": False, + # "min_deposit_amount": "0.000050000000000000", + # "min_withdrawal_amount": "0.000300000000000000", + # "minimum_deposit_confirmations": "15", + # "network": "BEP20(BSC)", + # "variable_withdrawal_fee": "0", + # "withdrawal_status": "enabled" + # } + # ], + # "precision": "18", + # "sort_priority": "3", + # "symbol": "ETH", + # "variable_withdrawal_fee": "0.000000000000000000" + # }, + # ], + # "success":true + # } + # + currencies = self.safe_list(response, 'result', []) + result: dict = {} + for i in range(0, len(currencies)): + currency = currencies[i] + id = self.safe_string(currency, 'symbol') + numericId = self.safe_integer(currency, 'id') + code = self.safe_currency_code(id) + chains = self.safe_list(currency, 'networks', []) + networks = {} + for j in range(0, len(chains)): + chain = chains[j] + networkId = self.safe_string(chain, 'network') + networkCode = self.network_id_to_code(networkId) + networks[networkCode] = { + 'id': networkId, + 'network': networkCode, + 'name': self.safe_string(chain, 'name'), + 'info': chain, + 'active': self.safe_string(chain, 'status') == 'enabled', + 'deposit': self.safe_string(chain, 'deposit_status') == 'enabled', + 'withdraw': self.safe_string(chain, 'withdrawal_status') == 'enabled', + 'fee': self.safe_number(chain, 'base_withdrawal_fee'), + 'limits': { + 'deposit': { + 'min': self.safe_number(chain, 'min_deposit_amount'), + 'max': None, + }, + 'withdraw': { + 'min': self.safe_number(chain, 'min_withdrawal_amount'), + 'max': None, + }, + }, + } + result[code] = self.safe_currency_structure({ + 'id': id, + 'numericId': numericId, + 'code': code, + 'name': self.safe_string(currency, 'name'), + 'info': currency, # the original payload + 'active': None, + 'deposit': self.safe_string(currency, 'deposit_status') == 'enabled', + 'withdraw': self.safe_string(currency, 'withdrawal_status') == 'enabled', + 'fee': self.safe_number(currency, 'base_withdrawal_fee'), + 'precision': self.parse_number(self.parse_precision(self.safe_string(currency, 'precision'))), + 'limits': { + 'amount': {'min': None, 'max': None}, + 'withdraw': { + 'min': self.safe_number(currency, 'min_withdrawal_amount'), + 'max': None, + }, + }, + 'networks': networks, + 'type': 'crypto', + }) + return result + + async def load_markets(self, reload=False, params={}): + markets = await super(delta, self).load_markets(reload, params) + currenciesByNumericId = self.safe_dict(self.options, 'currenciesByNumericId') + if (currenciesByNumericId is None) or reload: + self.options['currenciesByNumericId'] = self.index_by_stringified_numeric_id(self.currencies) + marketsByNumericId = self.safe_dict(self.options, 'marketsByNumericId') + if (marketsByNumericId is None) or reload: + self.options['marketsByNumericId'] = self.index_by_stringified_numeric_id(self.markets) + return markets + + def index_by_stringified_numeric_id(self, input): + result: dict = {} + if input is None: + return None + keys = list(input.keys()) + for i in range(0, len(keys)): + key = keys[i] + item = input[key] + numericIdString = self.safe_string(item, 'numericId') + if numericIdString is None: + continue + result[numericIdString] = item + return result + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for delta + + https://docs.delta.exchange/#get-list-of-products + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = await self.publicGetProducts(params) + # + # { + # "meta":{"after":null, "before":null, "limit":100, "total_count":81}, + # "result":[ + # # the below response represents item from perpetual market + # { + # "annualized_funding":"5.475000000000000000", + # "is_quanto":false, + # "ui_config":{ + # "default_trading_view_candle":"15", + # "leverage_slider_values":[1,3,5,10,25,50], + # "price_clubbing_values":[0.001,0.005,0.05,0.1,0.5,1,5], + # "show_bracket_orders":false, + # "sort_priority":29, + # "tags":[] + # }, + # "basis_factor_max_limit":"0.15", + # "symbol":"P-LINK-D-151120", + # "id":1584, + # "default_leverage":"5.000000000000000000", + # "maker_commission_rate":"0.0005", + # "contract_unit_currency":"LINK", + # "strike_price":"12.507948", + # "settling_asset":{ + # # asset structure + # }, + # "auction_start_time":null, + # "auction_finish_time":null, + # "settlement_time":"2020-11-15T12:00:00Z", + # "launch_time":"2020-11-14T11:55:05Z", + # "spot_index":{ + # # index structure + # }, + # "trading_status":"operational", + # "tick_size":"0.001", + # "position_size_limit":100000, + # "notional_type":"vanilla", # vanilla, inverse + # "price_band":"0.4", + # "barrier_price":null, + # "description":"Daily LINK PUT options quoted in USDT and settled in USDT", + # "insurance_fund_margin_contribution":"1", + # "quoting_asset":{ + # # asset structure + # }, + # "liquidation_penalty_factor":"0.2", + # "product_specs":{"max_volatility":3,"min_volatility":0.3,"spot_price_band":"0.40"}, + # "initial_margin_scaling_factor":"0.0001", + # "underlying_asset":{ + # # asset structure + # }, + # "state":"live", + # "contract_value":"1", + # "initial_margin":"2", + # "impact_size":5000, + # "settlement_price":null, + # "contract_type":"put_options", # put_options, call_options, move_options, perpetual_futures, interest_rate_swaps, futures, spreads + # "taker_commission_rate":"0.0005", + # "maintenance_margin":"1", + # "short_description":"LINK Daily PUT Options", + # "maintenance_margin_scaling_factor":"0.00005", + # "funding_method":"mark_price", + # "max_leverage_notional":"20000" + # }, + # # the below response represents item from spot market + # { + # "position_size_limit": 10000000, + # "settlement_price": null, + # "funding_method": "mark_price", + # "settling_asset": null, + # "impact_size": 10, + # "id": 32258, + # "auction_finish_time": null, + # "description": "Solana tether spot market", + # "trading_status": "operational", + # "tick_size": "0.01", + # "liquidation_penalty_factor": "1", + # "spot_index": { + # "config": {"quoting_asset": "USDT", "service_id": 8, "underlying_asset": "SOL"}, + # "constituent_exchanges": [ + # {"exchange": "binance", "health_interval": 60, "health_priority": 1, "weight": 1}, + # {"exchange": "huobi", "health_interval": 60, "health_priority": 2, "weight": 1} + # ], + # "constituent_indices": null, + # "description": "Solana index from binance and huobi", + # "health_interval": 300, + # "id": 105, + # "impact_size": "40.000000000000000000", + # "index_type": "spot_pair", + # "is_composite": False, + # "price_method": "ltp", + # "quoting_asset_id": 5, + # "symbol": ".DESOLUSDT", + # "tick_size": "0.000100000000000000", + # "underlying_asset_id": 66 + # }, + # "contract_type": "spot", + # "launch_time": "2022-02-03T10:18:11Z", + # "symbol": "SOL_USDT", + # "disruption_reason": null, + # "settlement_time": null, + # "insurance_fund_margin_contribution": "1", + # "is_quanto": False, + # "maintenance_margin": "5", + # "taker_commission_rate": "0.0005", + # "auction_start_time": null, + # "max_leverage_notional": "10000000", + # "state": "live", + # "annualized_funding": "0", + # "notional_type": "vanilla", + # "price_band": "100", + # "product_specs": {"kyc_required": False, "max_order_size": 2000, "min_order_size": 0.01, "quoting_precision": 4, "underlying_precision": 2}, + # "default_leverage": "1.000000000000000000", + # "initial_margin": "10", + # "maintenance_margin_scaling_factor": "1", + # "ui_config": { + # "default_trading_view_candle": "1d", + # "leverage_slider_values": [], + # "price_clubbing_values": [0.01, 0.05, 0.1, 0.5, 1, 2.5, 5], + # "show_bracket_orders": False, + # "sort_priority": 2, + # "tags": [] + # }, + # "basis_factor_max_limit": "10000", + # "contract_unit_currency": "SOL", + # "strike_price": null, + # "quoting_asset": { + # "base_withdrawal_fee": "10.000000000000000000", + # "deposit_status": "enabled", + # "id": 5, + # "interest_credit": False, + # "interest_slabs": null, + # "kyc_deposit_limit": "100000.000000000000000000", + # "kyc_withdrawal_limit": "10000.000000000000000000", + # "min_withdrawal_amount": "30.000000000000000000", + # "minimum_precision": 2, + # "name": "Tether", + # "networks": [ + # {"base_withdrawal_fee": "25", "deposit_status": "enabled", "memo_required": False, "network": "ERC20", "variable_withdrawal_fee": "0", "withdrawal_status": "enabled"}, + # {"base_withdrawal_fee": "1", "deposit_status": "enabled", "memo_required": False, "network": "BEP20(BSC)", "variable_withdrawal_fee": "0", "withdrawal_status": "enabled"}, + # {"base_withdrawal_fee": "1", "deposit_status": "disabled", "memo_required": False, "network": "TRC20(TRON)", "variable_withdrawal_fee": "0", "withdrawal_status": "disabled"} + # ], + # "precision": 8, + # "sort_priority": 1, + # "symbol": "USDT", + # "variable_withdrawal_fee": "0.000000000000000000", + # "withdrawal_status": "enabled" + # }, + # "maker_commission_rate": "0.0005", + # "initial_margin_scaling_factor": "2", + # "underlying_asset": { + # "base_withdrawal_fee": "0.000000000000000000", + # "deposit_status": "enabled", + # "id": 66, + # "interest_credit": False, + # "interest_slabs": null, + # "kyc_deposit_limit": "0.000000000000000000", + # "kyc_withdrawal_limit": "0.000000000000000000", + # "min_withdrawal_amount": "0.020000000000000000", + # "minimum_precision": 4, + # "name": "Solana", + # "networks": [ + # {"base_withdrawal_fee": "0.01", "deposit_status": "enabled", "memo_required": False, "network": "SOLANA", "variable_withdrawal_fee": "0", "withdrawal_status": "enabled"}, + # {"base_withdrawal_fee": "0.01", "deposit_status": "enabled", "memo_required": False, "network": "BEP20(BSC)", "variable_withdrawal_fee": "0", "withdrawal_status": "enabled"} + # ], + # "precision": 8, + # "sort_priority": 7, + # "symbol": "SOL", + # "variable_withdrawal_fee": "0.000000000000000000", + # "withdrawal_status": "enabled" + # }, + # "barrier_price": null, + # "contract_value": "1", + # "short_description": "SOL-USDT spot market" + # }, + # ], + # "success":true + # } + # + markets = self.safe_list(response, 'result', []) + result = [] + for i in range(0, len(markets)): + market = markets[i] + type = self.safe_string(market, 'contract_type') + if type == 'options_combos': + continue + # settlingAsset = self.safe_value(market, 'settling_asset', {}) + quotingAsset = self.safe_dict(market, 'quoting_asset', {}) + underlyingAsset = self.safe_dict(market, 'underlying_asset', {}) + settlingAsset = self.safe_dict(market, 'settling_asset') + productSpecs = self.safe_dict(market, 'product_specs', {}) + baseId = self.safe_string(underlyingAsset, 'symbol') + quoteId = self.safe_string(quotingAsset, 'symbol') + settleId = self.safe_string(settlingAsset, 'symbol') + id = self.safe_string(market, 'symbol') + numericId = self.safe_integer(market, 'id') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + settle = self.safe_currency_code(settleId) + callOptions = (type == 'call_options') + putOptions = (type == 'put_options') + moveOptions = (type == 'move_options') + spot = (type == 'spot') + swap = (type == 'perpetual_futures') + future = (type == 'futures') + option = (callOptions or putOptions or moveOptions) + strike = self.safe_string(market, 'strike_price') + expiryDatetime = self.safe_string(market, 'settlement_time') + expiry = self.parse8601(expiryDatetime) + contractSize = self.safe_number(market, 'contract_value') + amountPrecision = None + if spot: + amountPrecision = self.parse_number(self.parse_precision(self.safe_string(productSpecs, 'underlying_precision'))) # seems inverse of 'impact_size' + else: + # other markets(swap, futures, move, spread, irs) seem to use the step of '1' contract + amountPrecision = self.parse_number('1') + linear = (settle == quote) + optionType = None + symbol = base + '/' + quote + if swap or future or option: + symbol = symbol + ':' + settle + if future or option: + symbol = symbol + '-' + self.yymmdd(expiry) + if option: + type = 'option' + letter = 'C' + optionType = 'call' + if putOptions: + letter = 'P' + optionType = 'put' + elif moveOptions: + letter = 'M' + optionType = 'move' + symbol = symbol + '-' + strike + '-' + letter + else: + type = 'future' + else: + type = 'swap' + state = self.safe_string(market, 'state') + result.append({ + 'id': id, + 'numericId': numericId, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': type, + 'spot': spot, + 'margin': None if spot else False, + 'swap': swap, + 'future': future, + 'option': option, + 'active': (state == 'live'), + 'contract': not spot, + 'linear': None if spot else linear, + 'inverse': None if spot else not linear, + 'taker': self.safe_number(market, 'taker_commission_rate'), + 'maker': self.safe_number(market, 'maker_commission_rate'), + 'contractSize': None if spot else contractSize, + 'expiry': expiry, + 'expiryDatetime': self.iso8601(expiry), # do not use raw expiry string + 'strike': self.parse_number(strike), + 'optionType': optionType, + 'precision': { + 'amount': amountPrecision, + 'price': self.safe_number(market, 'tick_size'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.parse_number('1'), + 'max': self.safe_number(market, 'position_size_limit'), + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': self.safe_number(market, 'min_size'), + 'max': None, + }, + }, + 'created': self.parse8601(self.safe_string(market, 'launch_time')), + 'info': market, + }) + return result + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # spot: fetchTicker, fetchTickers + # + # { + # "close": 30634.0, + # "contract_type": "spot", + # "greeks": null, + # "high": 30780.0, + # "low": 30340.5, + # "mark_price": "48000", + # "oi": "0.0000", + # "oi_change_usd_6h": "0.0000", + # "oi_contracts": "0", + # "oi_value": "0.0000", + # "oi_value_symbol": "BTC", + # "oi_value_usd": "0.0000", + # "open": 30464.0, + # "price_band": null, + # "product_id": 8320, + # "quotes": {}, + # "size": 2.6816639999999996, + # "spot_price": "30637.91465121", + # "symbol": "BTC_USDT", + # "timestamp": 1689139767621299, + # "turnover": 2.6816639999999996, + # "turnover_symbol": "BTC", + # "turnover_usd": 81896.45613400004, + # "volume": 2.6816639999999996 + # } + # + # swap: fetchTicker, fetchTickers + # + # { + # "close": 30600.5, + # "contract_type": "perpetual_futures", + # "funding_rate": "0.00602961", + # "greeks": null, + # "high": 30803.0, + # "low": 30265.5, + # "mark_basis": "-0.45601594", + # "mark_price": "30600.10481568", + # "oi": "469.9190", + # "oi_change_usd_6h": "2226314.9900", + # "oi_contracts": "469919", + # "oi_value": "469.9190", + # "oi_value_symbol": "BTC", + # "oi_value_usd": "14385640.6802", + # "open": 30458.5, + # "price_band": { + # "lower_limit": "29067.08312627", + # "upper_limit": "32126.77608693" + # }, + # "product_id": 139, + # "quotes": { + # "ask_iv": null, + # "ask_size": "965", + # "best_ask": "30600.5", + # "best_bid": "30599.5", + # "bid_iv": null, + # "bid_size": "196", + # "impact_mid_price": null, + # "mark_iv": "-0.44931641" + # }, + # "size": 1226303, + # "spot_price": "30612.85362773", + # "symbol": "BTCUSDT", + # "timestamp": 1689136597460456, + # "turnover": 37392218.45999999, + # "turnover_symbol": "USDT", + # "turnover_usd": 37392218.45999999, + # "volume": 1226.3029999999485 + # } + # + # option: fetchTicker, fetchTickers + # + # { + # "contract_type": "call_options", + # "greeks": { + # "delta": "0.60873994", + # "gamma": "0.00014854", + # "rho": "7.71808010", + # "spot": "30598.49040622", + # "theta": "-30.44743017", + # "vega": "24.83508248" + # }, + # "mark_price": "1347.74819696", + # "mark_vol": "0.39966303", + # "oi": "2.7810", + # "oi_change_usd_6h": "0.0000", + # "oi_contracts": "2781", + # "oi_value": "2.7810", + # "oi_value_symbol": "BTC", + # "oi_value_usd": "85127.4337", + # "price_band": { + # "lower_limit": "91.27423497", + # "upper_limit": "7846.19454697" + # }, + # "product_id": 107150, + # "quotes": { + # "ask_iv": "0.41023239", + # "ask_size": "2397", + # "best_ask": "1374", + # "best_bid": "1322", + # "bid_iv": "0.38929375", + # "bid_size": "3995", + # "impact_mid_price": null, + # "mark_iv": "0.39965618" + # }, + # "spot_price": "30598.43379314", + # "strike_price": "30000", + # "symbol": "C-BTC-30000-280723", + # "timestamp": 1689136932893181, + # "turnover_symbol": "USDT" + # } + # + timestamp = self.safe_integer_product(ticker, 'timestamp', 0.001) + marketId = self.safe_string(ticker, 'symbol') + symbol = self.safe_symbol(marketId, market) + last = self.safe_string(ticker, 'close') + quotes = self.safe_dict(ticker, 'quotes', {}) + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_number(ticker, 'high'), + 'low': self.safe_number(ticker, 'low'), + 'bid': self.safe_number(quotes, 'best_bid'), + 'bidVolume': self.safe_number(quotes, 'bid_size'), + 'ask': self.safe_number(quotes, 'best_ask'), + 'askVolume': self.safe_number(quotes, 'ask_size'), + 'vwap': None, + 'open': self.safe_string(ticker, 'open'), + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': self.safe_number(ticker, 'volume'), + 'quoteVolume': self.safe_number(ticker, 'turnover'), + 'markPrice': self.safe_number(ticker, 'mark_price'), + 'indexPrice': self.safe_number(ticker, 'spot_price'), + 'info': ticker, + }, market) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://docs.delta.exchange/#get-ticker-for-a-product-by-symbol + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = await self.publicGetTickersSymbol(self.extend(request, params)) + # + # spot + # + # { + # "result": { + # "close": 30634.0, + # "contract_type": "spot", + # "greeks": null, + # "high": 30780.0, + # "low": 30340.5, + # "mark_price": "48000", + # "oi": "0.0000", + # "oi_change_usd_6h": "0.0000", + # "oi_contracts": "0", + # "oi_value": "0.0000", + # "oi_value_symbol": "BTC", + # "oi_value_usd": "0.0000", + # "open": 30464.0, + # "price_band": null, + # "product_id": 8320, + # "quotes": {}, + # "size": 2.6816639999999996, + # "spot_price": "30637.91465121", + # "symbol": "BTC_USDT", + # "timestamp": 1689139767621299, + # "turnover": 2.6816639999999996, + # "turnover_symbol": "BTC", + # "turnover_usd": 81896.45613400004, + # "volume": 2.6816639999999996 + # }, + # "success": True + # } + # + # swap + # + # { + # "result": { + # "close": 30600.5, + # "contract_type": "perpetual_futures", + # "funding_rate": "0.00602961", + # "greeks": null, + # "high": 30803.0, + # "low": 30265.5, + # "mark_basis": "-0.45601594", + # "mark_price": "30600.10481568", + # "oi": "469.9190", + # "oi_change_usd_6h": "2226314.9900", + # "oi_contracts": "469919", + # "oi_value": "469.9190", + # "oi_value_symbol": "BTC", + # "oi_value_usd": "14385640.6802", + # "open": 30458.5, + # "price_band": { + # "lower_limit": "29067.08312627", + # "upper_limit": "32126.77608693" + # }, + # "product_id": 139, + # "quotes": { + # "ask_iv": null, + # "ask_size": "965", + # "best_ask": "30600.5", + # "best_bid": "30599.5", + # "bid_iv": null, + # "bid_size": "196", + # "impact_mid_price": null, + # "mark_iv": "-0.44931641" + # }, + # "size": 1226303, + # "spot_price": "30612.85362773", + # "symbol": "BTCUSDT", + # "timestamp": 1689136597460456, + # "turnover": 37392218.45999999, + # "turnover_symbol": "USDT", + # "turnover_usd": 37392218.45999999, + # "volume": 1226.3029999999485 + # }, + # "success": True + # } + # + # option + # + # { + # "result": { + # "contract_type": "call_options", + # "greeks": { + # "delta": "0.60873994", + # "gamma": "0.00014854", + # "rho": "7.71808010", + # "spot": "30598.49040622", + # "theta": "-30.44743017", + # "vega": "24.83508248" + # }, + # "mark_price": "1347.74819696", + # "mark_vol": "0.39966303", + # "oi": "2.7810", + # "oi_change_usd_6h": "0.0000", + # "oi_contracts": "2781", + # "oi_value": "2.7810", + # "oi_value_symbol": "BTC", + # "oi_value_usd": "85127.4337", + # "price_band": { + # "lower_limit": "91.27423497", + # "upper_limit": "7846.19454697" + # }, + # "product_id": 107150, + # "quotes": { + # "ask_iv": "0.41023239", + # "ask_size": "2397", + # "best_ask": "1374", + # "best_bid": "1322", + # "bid_iv": "0.38929375", + # "bid_size": "3995", + # "impact_mid_price": null, + # "mark_iv": "0.39965618" + # }, + # "spot_price": "30598.43379314", + # "strike_price": "30000", + # "symbol": "C-BTC-30000-280723", + # "timestamp": 1689136932893181, + # "turnover_symbol": "USDT" + # }, + # "success": True + # } + # + result = self.safe_dict(response, 'result', {}) + return self.parse_ticker(result, market) + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://docs.delta.exchange/#get-tickers-for-products + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + response = await self.publicGetTickers(params) + # + # spot + # + # { + # "result": [ + # { + # "close": 30634.0, + # "contract_type": "spot", + # "greeks": null, + # "high": 30780.0, + # "low": 30340.5, + # "mark_price": "48000", + # "oi": "0.0000", + # "oi_change_usd_6h": "0.0000", + # "oi_contracts": "0", + # "oi_value": "0.0000", + # "oi_value_symbol": "BTC", + # "oi_value_usd": "0.0000", + # "open": 30464.0, + # "price_band": null, + # "product_id": 8320, + # "quotes": {}, + # "size": 2.6816639999999996, + # "spot_price": "30637.91465121", + # "symbol": "BTC_USDT", + # "timestamp": 1689139767621299, + # "turnover": 2.6816639999999996, + # "turnover_symbol": "BTC", + # "turnover_usd": 81896.45613400004, + # "volume": 2.6816639999999996 + # }, + # ], + # "success":true + # } + # + # swap + # + # { + # "result": [ + # { + # "close": 30600.5, + # "contract_type": "perpetual_futures", + # "funding_rate": "0.00602961", + # "greeks": null, + # "high": 30803.0, + # "low": 30265.5, + # "mark_basis": "-0.45601594", + # "mark_price": "30600.10481568", + # "oi": "469.9190", + # "oi_change_usd_6h": "2226314.9900", + # "oi_contracts": "469919", + # "oi_value": "469.9190", + # "oi_value_symbol": "BTC", + # "oi_value_usd": "14385640.6802", + # "open": 30458.5, + # "price_band": { + # "lower_limit": "29067.08312627", + # "upper_limit": "32126.77608693" + # }, + # "product_id": 139, + # "quotes": { + # "ask_iv": null, + # "ask_size": "965", + # "best_ask": "30600.5", + # "best_bid": "30599.5", + # "bid_iv": null, + # "bid_size": "196", + # "impact_mid_price": null, + # "mark_iv": "-0.44931641" + # }, + # "size": 1226303, + # "spot_price": "30612.85362773", + # "symbol": "BTCUSDT", + # "timestamp": 1689136597460456, + # "turnover": 37392218.45999999, + # "turnover_symbol": "USDT", + # "turnover_usd": 37392218.45999999, + # "volume": 1226.3029999999485 + # }, + # ], + # "success":true + # } + # + # option + # + # { + # "result": [ + # { + # "contract_type": "call_options", + # "greeks": { + # "delta": "0.60873994", + # "gamma": "0.00014854", + # "rho": "7.71808010", + # "spot": "30598.49040622", + # "theta": "-30.44743017", + # "vega": "24.83508248" + # }, + # "mark_price": "1347.74819696", + # "mark_vol": "0.39966303", + # "oi": "2.7810", + # "oi_change_usd_6h": "0.0000", + # "oi_contracts": "2781", + # "oi_value": "2.7810", + # "oi_value_symbol": "BTC", + # "oi_value_usd": "85127.4337", + # "price_band": { + # "lower_limit": "91.27423497", + # "upper_limit": "7846.19454697" + # }, + # "product_id": 107150, + # "quotes": { + # "ask_iv": "0.41023239", + # "ask_size": "2397", + # "best_ask": "1374", + # "best_bid": "1322", + # "bid_iv": "0.38929375", + # "bid_size": "3995", + # "impact_mid_price": null, + # "mark_iv": "0.39965618" + # }, + # "spot_price": "30598.43379314", + # "strike_price": "30000", + # "symbol": "C-BTC-30000-280723", + # "timestamp": 1689136932893181, + # "turnover_symbol": "USDT" + # }, + # ], + # "success":true + # } + # + tickers = self.safe_list(response, 'result', []) + result: dict = {} + for i in range(0, len(tickers)): + ticker = self.parse_ticker(tickers[i]) + symbol = ticker['symbol'] + result[symbol] = ticker + return self.filter_by_array_tickers(result, 'symbol', symbols) + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://docs.delta.exchange/#get-l2-orderbook + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['depth'] = limit + response = await self.publicGetL2orderbookSymbol(self.extend(request, params)) + # + # { + # "result":{ + # "buy":[ + # {"price":"15814.0","size":912}, + # {"price":"15813.5","size":1279}, + # {"price":"15813.0","size":1634}, + # ], + # "sell":[ + # {"price":"15814.5","size":625}, + # {"price":"15815.0","size":982}, + # {"price":"15815.5","size":1328}, + # ], + # "symbol":"BTCUSDT" + # }, + # "success":true + # } + # + result = self.safe_dict(response, 'result', {}) + return self.parse_order_book(result, market['symbol'], None, 'buy', 'sell', 'price', 'size') + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # public fetchTrades + # + # { + # "buyer_role":"maker", + # "price":"15896.5", + # "seller_role":"taker", + # "size":241, + # "symbol":"BTCUSDT", + # "timestamp":1605376684714595 + # } + # + # private fetchMyTrades + # + # { + # "commission":"0.008335000000000000", + # "created_at":"2020-11-16T19:07:19Z", + # "fill_type":"normal", + # "id":"e7ff05c233a74245b72381f8dd91d1ce", + # "meta_data":{ + # "effective_commission_rate":"0.0005", + # "order_price":"16249", + # "order_size":1, + # "order_type":"market_order", + # "order_unfilled_size":0, + # "trading_fee_credits_used":"0" + # }, + # "order_id":"152999629", + # "price":"16669", + # "product":{ + # "contract_type":"perpetual_futures", + # "contract_unit_currency":"BTC", + # "contract_value":"0.001", + # "id":139, + # "notional_type":"vanilla", + # "quoting_asset":{"minimum_precision":2,"precision":6,"symbol":"USDT"}, + # "settling_asset":{"minimum_precision":2,"precision":6,"symbol":"USDT"}, + # "symbol":"BTCUSDT", + # "tick_size":"0.5", + # "underlying_asset":{"minimum_precision":4,"precision":8,"symbol":"BTC"} + # }, + # "product_id":139, + # "role":"taker", + # "side":"sell", + # "size":1 + # } + # + id = self.safe_string(trade, 'id') + orderId = self.safe_string(trade, 'order_id') + timestamp = self.parse8601(self.safe_string(trade, 'created_at')) + timestamp = self.safe_integer_product(trade, 'timestamp', 0.001, timestamp) + priceString = self.safe_string(trade, 'price') + amountString = self.safe_string(trade, 'size') + product = self.safe_dict(trade, 'product', {}) + marketId = self.safe_string(product, 'symbol') + symbol = self.safe_symbol(marketId, market) + sellerRole = self.safe_string(trade, 'seller_role') + side = self.safe_string(trade, 'side') + if side is None: + if sellerRole == 'taker': + side = 'sell' + elif sellerRole == 'maker': + side = 'buy' + takerOrMaker = self.safe_string(trade, 'role') + metaData = self.safe_dict(trade, 'meta_data', {}) + type = self.safe_string(metaData, 'order_type') + if type is not None: + type = type.replace('_order', '') + feeCostString = self.safe_string(trade, 'commission') + fee = None + if feeCostString is not None: + settlingAsset = self.safe_dict(product, 'settling_asset', {}) + feeCurrencyId = self.safe_string(settlingAsset, 'symbol') + feeCurrencyCode = self.safe_currency_code(feeCurrencyId) + fee = { + 'cost': feeCostString, + 'currency': feeCurrencyCode, + } + return self.safe_trade({ + 'id': id, + 'order': orderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'type': type, + 'side': side, + 'price': priceString, + 'amount': amountString, + 'cost': None, + 'takerOrMaker': takerOrMaker, + 'fee': fee, + 'info': trade, + }, market) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://docs.delta.exchange/#get-public-trades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = await self.publicGetTradesSymbol(self.extend(request, params)) + # + # { + # "result":[ + # { + # "buyer_role":"maker", + # "price":"15896.5", + # "seller_role":"taker", + # "size":241, + # "symbol":"BTCUSDT", + # "timestamp":1605376684714595 + # } + # ], + # "success":true + # } + # + result = self.safe_list(response, 'result', []) + return self.parse_trades(result, market, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # { + # "time":1605393120, + # "open":15989, + # "high":15989, + # "low":15987.5, + # "close":15987.5, + # "volume":565 + # } + # + return [ + self.safe_timestamp(ohlcv, 'time'), + self.safe_number(ohlcv, 'open'), + self.safe_number(ohlcv, 'high'), + self.safe_number(ohlcv, 'low'), + self.safe_number(ohlcv, 'close'), + self.safe_number(ohlcv, 'volume'), + ] + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://docs.delta.exchange/#delta-exchange-api-v2-historical-ohlc-candles-sparklines + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.until]: timestamp in ms of the latest candle to fetch + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'resolution': self.safe_string(self.timeframes, timeframe, timeframe), + } + duration = self.parse_timeframe(timeframe) + limit = limit if limit else 2000 # max 2000 + until = self.safe_integer_product(params, 'until', 0.001) + untilIsDefined = (until is not None) + if untilIsDefined: + until = self.parse_to_int(until) + if since is None: + end = until if untilIsDefined else self.seconds() + request['end'] = end + request['start'] = end - limit * duration + else: + start = self.parse_to_int(since / 1000) + request['start'] = start + request['end'] = until if untilIsDefined else self.sum(start, limit * duration) + price = self.safe_string(params, 'price') + if price == 'mark': + request['symbol'] = 'MARK:' + market['id'] + elif price == 'index': + request['symbol'] = market['info']['spot_index']['symbol'] + else: + request['symbol'] = market['id'] + params = self.omit(params, ['price', 'until']) + response = await self.publicGetHistoryCandles(self.extend(request, params)) + # + # { + # "success":true, + # "result":[ + # {"time":1605393120,"open":15989,"high":15989,"low":15987.5,"close":15987.5,"volume":565}, + # {"time":1605393180,"open":15966,"high":15966,"low":15959,"close":15959,"volume":24}, + # {"time":1605393300,"open":15973,"high":15973,"low":15973,"close":15973,"volume":1288}, + # ] + # } + # + result = self.safe_list(response, 'result', []) + return self.parse_ohlcvs(result, market, timeframe, since, limit) + + def parse_balance(self, response) -> Balances: + balances = self.safe_list(response, 'result', []) + result: dict = {'info': response} + currenciesByNumericId = self.safe_dict(self.options, 'currenciesByNumericId', {}) + for i in range(0, len(balances)): + balance = balances[i] + currencyId = self.safe_string(balance, 'asset_id') + currency = self.safe_dict(currenciesByNumericId, currencyId) + code = currencyId if (currency is None) else currency['code'] + account = self.account() + account['total'] = self.safe_string(balance, 'balance') + account['free'] = self.safe_string(balance, 'available_balance') + result[code] = account + return self.safe_balance(result) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://docs.delta.exchange/#get-wallet-balances + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + response = await self.privateGetWalletBalances(params) + # + # { + # "result":[ + # { + # "asset_id":1, + # "available_balance":"0", + # "balance":"0", + # "commission":"0", + # "id":154883, + # "interest_credit":"0", + # "order_margin":"0", + # "pending_referral_bonus":"0", + # "pending_trading_fee_credit":"0", + # "position_margin":"0", + # "trading_fee_credit":"0", + # "user_id":22142 + # }, + # ], + # "success":true + # } + # + return self.parse_balance(response) + + async def fetch_position(self, symbol: str, params={}): + """ + fetch data on a single open contract trade position + + https://docs.delta.exchange/#get-position + + :param str symbol: unified market symbol of the market the position is held in, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `position structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'product_id': market['numericId'], + } + response = await self.privateGetPositions(self.extend(request, params)) + # + # { + # "result":{ + # "entry_price":null, + # "size":0, + # "timestamp":1605454074268079 + # }, + # "success":true + # } + # + result = self.safe_dict(response, 'result', {}) + return self.parse_position(result, market) + + async def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://docs.delta.exchange/#get-margined-positions + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + await self.load_markets() + response = await self.privateGetPositionsMargined(params) + # + # { + # "success": True, + # "result": [ + # { + # "user_id": 0, + # "size": 0, + # "entry_price": "string", + # "margin": "string", + # "liquidation_price": "string", + # "bankruptcy_price": "string", + # "adl_level": 0, + # "product_id": 0, + # "product_symbol": "string", + # "commission": "string", + # "realized_pnl": "string", + # "realized_funding": "string" + # } + # ] + # } + # + result = self.safe_list(response, 'result', []) + return self.parse_positions(result, symbols) + + def parse_position(self, position: dict, market: Market = None): + # + # fetchPosition + # + # { + # "entry_price":null, + # "size":0, + # "timestamp":1605454074268079 + # } + # + # + # fetchPositions + # + # { + # "user_id": 0, + # "size": 0, + # "entry_price": "string", + # "margin": "string", + # "liquidation_price": "string", + # "bankruptcy_price": "string", + # "adl_level": 0, + # "product_id": 0, + # "product_symbol": "string", + # "commission": "string", + # "realized_pnl": "string", + # "realized_funding": "string" + # } + # + marketId = self.safe_string(position, 'product_symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + timestamp = self.safe_integer_product(position, 'timestamp', 0.001) + sizeString = self.safe_string(position, 'size') + side = None + if sizeString is not None: + if Precise.string_gt(sizeString, '0'): + side = 'buy' + elif Precise.string_lt(sizeString, '0'): + side = 'sell' + return self.safe_position({ + 'info': position, + 'id': None, + 'symbol': symbol, + 'notional': None, + 'marginMode': None, + 'liquidationPrice': self.safe_number(position, 'liquidation_price'), + 'entryPrice': self.safe_number(position, 'entry_price'), + 'unrealizedPnl': None, # todo - realized_pnl ? + 'percentage': None, + 'contracts': self.parse_number(sizeString), + 'contractSize': self.safe_number(market, 'contractSize'), + 'markPrice': None, + 'side': side, + 'hedged': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'maintenanceMargin': None, + 'maintenanceMarginPercentage': None, + 'collateral': None, + 'initialMargin': None, + 'initialMarginPercentage': None, + 'leverage': None, + 'marginRatio': None, + 'stopLossPrice': None, + 'takeProfitPrice': None, + }) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'open': 'open', + 'pending': 'open', + 'closed': 'closed', + 'cancelled': 'canceled', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder, cancelOrder, editOrder, fetchOpenOrders, fetchClosedOrders + # + # { + # "average_fill_price":null, + # "bracket_order":null, + # "bracket_stop_loss_limit_price":null, + # "bracket_stop_loss_price":null, + # "bracket_take_profit_limit_price":null, + # "bracket_take_profit_price":null, + # "bracket_trail_amount":null, + # "cancellation_reason":null, + # "client_order_id":null, + # "close_on_trigger":"false", + # "commission":"0", + # "created_at":"2020-11-16T02:38:26Z", + # "id":152870626, + # "limit_price":"10000", + # "meta_data":{"source":"api"}, + # "order_type":"limit_order", + # "paid_commission":"0", + # "product_id":139, + # "reduce_only":false, + # "side":"buy", + # "size":0, + # "state":"open", + # "stop_order_type":null, + # "stop_price":null, + # "stop_trigger_method":"mark_price", + # "time_in_force":"gtc", + # "trail_amount":null, + # "unfilled_size":0, + # "user_id":22142 + # } + # + # fetchOrder + # + # { + # "id": 123, + # "user_id": 453671, + # "size": 10, + # "unfilled_size": 2, + # "side": "buy", + # "order_type": "limit_order", + # "limit_price": "59000", + # "stop_order_type": "stop_loss_order", + # "stop_price": "55000", + # "paid_commission": "0.5432", + # "commission": "0.5432", + # "reduce_only": False, + # "client_order_id": "my_signal_34521712", + # "state": "open", + # "created_at": "1725865012000000", + # "product_id": 27, + # "product_symbol": "BTCUSD" + # } + # + id = self.safe_string(order, 'id') + clientOrderId = self.safe_string(order, 'client_order_id') + createdAt = self.safe_string(order, 'created_at') + timestamp = None + if createdAt is not None: + if createdAt.find('-') >= 0: + timestamp = self.parse8601(createdAt) + else: + timestamp = self.safe_integer_product(order, 'created_at', 0.001) + marketId = self.safe_string(order, 'product_id') + marketsByNumericId = self.safe_dict(self.options, 'marketsByNumericId', {}) + market = self.safe_value(marketsByNumericId, marketId, market) + symbol = marketId if (market is None) else market['symbol'] + status = self.parse_order_status(self.safe_string(order, 'state')) + side = self.safe_string(order, 'side') + type = self.safe_string(order, 'order_type') + if type is not None: + type = type.replace('_order', '') + price = self.safe_string(order, 'limit_price') + amount = self.safe_string(order, 'size') + remaining = self.safe_string(order, 'unfilled_size') + average = self.safe_string(order, 'average_fill_price') + fee = None + feeCostString = self.safe_string(order, 'paid_commission') + if feeCostString is not None: + feeCurrencyCode = None + if market is not None: + settlingAsset = self.safe_dict(market['info'], 'settling_asset', {}) + feeCurrencyId = self.safe_string(settlingAsset, 'symbol') + feeCurrencyCode = self.safe_currency_code(feeCurrencyId) + fee = { + 'cost': feeCostString, + 'currency': feeCurrencyCode, + } + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'symbol': symbol, + 'type': type, + 'side': side, + 'price': price, + 'amount': amount, + 'cost': None, + 'average': average, + 'filled': None, + 'remaining': remaining, + 'status': status, + 'fee': fee, + 'trades': None, + }, market) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://docs.delta.exchange/#place-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.reduceOnly]: *contract only* indicates if self order is to reduce the size of a position + :returns dict: an `order structure ` + """ + await self.load_markets() + orderType = type + '_order' + market = self.market(symbol) + request: dict = { + 'product_id': market['numericId'], + # 'limit_price': self.price_to_precision(market['symbol'], price), + 'size': self.amount_to_precision(market['symbol'], amount), + 'side': side, + 'order_type': orderType, + # 'client_order_id': 'string', + # 'time_in_force': 'gtc', # gtc, ioc, fok + # 'post_only': 'false', # 'true', + # 'reduce_only': 'false', # 'true', + } + if type == 'limit': + request['limit_price'] = self.price_to_precision(market['symbol'], price) + clientOrderId = self.safe_string_2(params, 'clientOrderId', 'client_order_id') + params = self.omit(params, ['clientOrderId', 'client_order_id']) + if clientOrderId is not None: + request['client_order_id'] = clientOrderId + reduceOnly = self.safe_bool(params, 'reduceOnly') + if reduceOnly: + request['reduce_only'] = reduceOnly + params = self.omit(params, 'reduceOnly') + response = await self.privatePostOrders(self.extend(request, params)) + # + # { + # "result":{ + # "average_fill_price":null, + # "bracket_order":null, + # "bracket_stop_loss_limit_price":null, + # "bracket_stop_loss_price":null, + # "bracket_take_profit_limit_price":null, + # "bracket_take_profit_price":null, + # "bracket_trail_amount":null, + # "cancellation_reason":null, + # "client_order_id":null, + # "close_on_trigger":"false", + # "commission":"0", + # "created_at":"2020-11-16T02:38:26Z", + # "id":152870626, + # "limit_price":"10000", + # "meta_data":{"source":"api"}, + # "order_type":"limit_order", + # "paid_commission":"0", + # "product_id":139, + # "reduce_only":false, + # "side":"buy", + # "size":0, + # "state":"open", + # "stop_order_type":null, + # "stop_price":null, + # "stop_trigger_method":"mark_price", + # "time_in_force":"gtc", + # "trail_amount":null, + # "unfilled_size":0, + # "user_id":22142 + # }, + # "success":true + # } + # + result = self.safe_dict(response, 'result', {}) + return self.parse_order(result, market) + + async def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + """ + edit a trade order + + https://docs.delta.exchange/#edit-order + + :param str id: order id + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of the currency you want to trade in units of the base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'id': int(id), + 'product_id': market['numericId'], + # "limit_price": self.price_to_precision(symbol, price), + # "size": self.amount_to_precision(symbol, amount), + } + if amount is not None: + request['size'] = int(self.amount_to_precision(symbol, amount)) + if price is not None: + request['limit_price'] = self.price_to_precision(symbol, price) + response = await self.privatePutOrders(self.extend(request, params)) + # + # { + # "success": True, + # "result": { + # "id": "ashb1212", + # "product_id": 27, + # "limit_price": "9200", + # "side": "buy", + # "size": 100, + # "unfilled_size": 50, + # "user_id": 1, + # "order_type": "limit_order", + # "state": "open", + # "created_at": "..." + # } + # } + # + result = self.safe_dict(response, 'result') + return self.parse_order(result, market) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://docs.delta.exchange/#cancel-order + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'id': int(id), + 'product_id': market['numericId'], + } + response = await self.privateDeleteOrders(self.extend(request, params)) + # + # { + # "result":{ + # "average_fill_price":null, + # "bracket_order":null, + # "bracket_stop_loss_limit_price":null, + # "bracket_stop_loss_price":null, + # "bracket_take_profit_limit_price":null, + # "bracket_take_profit_price":null, + # "bracket_trail_amount":null, + # "cancellation_reason":"cancelled_by_user", + # "client_order_id":null, + # "close_on_trigger":"false", + # "commission":"0", + # "created_at":"2020-11-16T02:38:26Z", + # "id":152870626, + # "limit_price":"10000", + # "meta_data":{"source":"api"}, + # "order_type":"limit_order", + # "paid_commission":"0", + # "product_id":139, + # "reduce_only":false, + # "side":"buy", + # "size":0, + # "state":"cancelled", + # "stop_order_type":null, + # "stop_price":null, + # "stop_trigger_method":"mark_price", + # "time_in_force":"gtc", + # "trail_amount":null, + # "unfilled_size":0, + # "user_id":22142 + # }, + # "success":true + # } + # + result = self.safe_dict(response, 'result') + return self.parse_order(result, market) + + async def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders in a market + + https://docs.delta.exchange/#cancel-all-open-orders + + :param str symbol: unified market symbol of the market to cancel orders in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelAllOrders() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'product_id': market['numericId'], + # 'cancel_limit_orders': 'true', + # 'cancel_stop_orders': 'true', + } + response = self.privateDeleteOrdersAll(self.extend(request, params)) + # + # { + # "result":{}, + # "success":true + # } + # + return [ + self.safe_order({ + 'info': response, + }), + ] + + async def fetch_order(self, id: str, symbol: Str = None, params={}) -> Order: + """ + fetches information on an order made by the user + + https://docs.delta.exchange/#get-order-by-id + https://docs.delta.exchange/#get-order-by-client-oid + + :param str id: the order id + :param str [symbol]: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.clientOrderId]: client order id of the order + :returns dict: an `order structure ` + """ + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + clientOrderId = self.safe_string_n(params, ['clientOrderId', 'client_oid', 'clientOid']) + params = self.omit(params, ['clientOrderId', 'client_oid', 'clientOid']) + request: dict = {} + response = None + if clientOrderId is not None: + request['client_oid'] = clientOrderId + response = await self.privateGetOrdersClientOrderIdClientOid(self.extend(request, params)) + else: + request['order_id'] = id + response = await self.privateGetOrdersOrderId(self.extend(request, params)) + # + # { + # "success": True, + # "result": { + # "id": 123, + # "user_id": 453671, + # "size": 10, + # "unfilled_size": 2, + # "side": "buy", + # "order_type": "limit_order", + # "limit_price": "59000", + # "stop_order_type": "stop_loss_order", + # "stop_price": "55000", + # "paid_commission": "0.5432", + # "commission": "0.5432", + # "reduce_only": False, + # "client_order_id": "my_signal_34521712", + # "state": "open", + # "created_at": "1725865012000000", + # "product_id": 27, + # "product_symbol": "BTCUSD" + # } + # } + # + result = self.safe_dict(response, 'result', {}) + return self.parse_order(result, market) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://docs.delta.exchange/#get-active-orders + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + return await self.fetch_orders_with_method('privateGetOrders', symbol, since, limit, params) + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://docs.delta.exchange/#get-order-history-cancelled-and-closed + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + return await self.fetch_orders_with_method('privateGetOrdersHistory', symbol, since, limit, params) + + async def fetch_orders_with_method(self, method, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + await self.load_markets() + request: dict = { + # 'product_ids': market['id'], # comma-separated + # 'contract_types': types, # comma-separated, futures, perpetual_futures, call_options, put_options, interest_rate_swaps, move_options, spreads + # 'order_types': types, # comma-separated, market, limit, stop_market, stop_limit, all_stop + # 'start_time': since * 1000, + # 'end_time': self.microseconds(), + # 'after', # after cursor for pagination + # 'before', # before cursor for pagination + # 'page_size': limit, # number of records per page + } + market = None + if symbol is not None: + market = self.market(symbol) + request['product_ids'] = market['numericId'] # accepts a comma-separated list of ids + if since is not None: + request['start_time'] = str(since) + '000' + if limit is not None: + request['page_size'] = limit + response = None + if method == 'privateGetOrders': + response = await self.privateGetOrders(self.extend(request, params)) + elif method == 'privateGetOrdersHistory': + response = await self.privateGetOrdersHistory(self.extend(request, params)) + # + # { + # "success": True, + # "result": [ + # { + # "id": "ashb1212", + # "product_id": 27, + # "limit_price": "9200", + # "side": "buy", + # "size": 100, + # "unfilled_size": 50, + # "user_id": 1, + # "order_type": "limit_order", + # "state": "open", + # "created_at": "..." + # } + # ], + # "meta": { + # "after": "string", + # "before": "string" + # } + # } + # + result = self.safe_list(response, 'result', []) + return self.parse_orders(result, market, since, limit) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://docs.delta.exchange/#get-user-fills-by-filters + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + request: dict = { + # 'product_ids': market['id'], # comma-separated + # 'contract_types': types, # comma-separated, futures, perpetual_futures, call_options, put_options, interest_rate_swaps, move_options, spreads + # 'start_time': since * 1000, + # 'end_time': self.microseconds(), + # 'after', # after cursor for pagination + # 'before', # before cursor for pagination + # 'page_size': limit, # number of records per page + } + market = None + if symbol is not None: + market = self.market(symbol) + request['product_ids'] = market['numericId'] # accepts a comma-separated list of ids + if since is not None: + request['start_time'] = str(since) + '000' + if limit is not None: + request['page_size'] = limit + response = await self.privateGetFills(self.extend(request, params)) + # + # { + # "meta":{ + # "after":null, + # "before":null, + # "limit":10, + # "total_count":2 + # }, + # "result":[ + # { + # "commission":"0.008335000000000000", + # "created_at":"2020-11-16T19:07:19Z", + # "fill_type":"normal", + # "id":"e7ff05c233a74245b72381f8dd91d1ce", + # "meta_data":{ + # "effective_commission_rate":"0.0005", + # "order_price":"16249", + # "order_size":1, + # "order_type":"market_order", + # "order_unfilled_size":0, + # "trading_fee_credits_used":"0" + # }, + # "order_id":"152999629", + # "price":"16669", + # "product":{ + # "contract_type":"perpetual_futures", + # "contract_unit_currency":"BTC", + # "contract_value":"0.001", + # "id":139, + # "notional_type":"vanilla", + # "quoting_asset":{"minimum_precision":2,"precision":6,"symbol":"USDT"}, + # "settling_asset":{"minimum_precision":2,"precision":6,"symbol":"USDT"}, + # "symbol":"BTCUSDT", + # "tick_size":"0.5", + # "underlying_asset":{"minimum_precision":4,"precision":8,"symbol":"BTC"} + # }, + # "product_id":139, + # "role":"taker", + # "side":"sell", + # "size":1 + # } + # ], + # "success":true + # } + # + result = self.safe_list(response, 'result', []) + return self.parse_trades(result, market, since, limit) + + async def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + + https://docs.delta.exchange/#get-wallet-transactions + + :param str [code]: unified currency code, default is None + :param int [since]: timestamp in ms of the earliest ledger entry, default is None + :param int [limit]: max number of ledger entries to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ledger structure ` + """ + await self.load_markets() + request: dict = { + # 'asset_id': currency['numericId'], + # 'end_time': self.seconds(), + # 'after': 'string', # after cursor for pagination + # 'before': 'string', # before cursor for pagination + # 'page_size': limit, + } + currency = None + if code is not None: + currency = self.currency(code) + request['asset_id'] = currency['numericId'] + if limit is not None: + request['page_size'] = limit + response = await self.privateGetWalletTransactions(self.extend(request, params)) + # + # { + # "meta":{"after":null,"before":null,"limit":10,"total_count":1}, + # "result":[ + # { + # "amount":"29.889184", + # "asset_id":5, + # "balance":"29.889184", + # "created_at":"2020-11-15T21:25:01Z", + # "meta_data":{ + # "deposit_id":3884, + # "transaction_id":"0x41a60174849828530abb5008e98fc63c9b598288743ec4ba9620bcce900a3b8d" + # }, + # "transaction_type":"deposit", + # "user_id":22142, + # "uuid":"70bb5679da3c4637884e2dc63efaa846" + # } + # ], + # "success":true + # } + # + result = self.safe_list(response, 'result', []) + return self.parse_ledger(result, currency, since, limit) + + def parse_ledger_entry_type(self, type): + types: dict = { + 'pnl': 'pnl', + 'deposit': 'transaction', + 'withdrawal': 'transaction', + 'commission': 'fee', + 'conversion': 'trade', + # 'perpetual_futures_funding': 'perpetual_futures_funding', + # 'withdrawal_cancellation': 'withdrawal_cancellation', + 'referral_bonus': 'referral', + 'commission_rebate': 'rebate', + # 'promo_credit': 'promo_credit', + } + return self.safe_string(types, type, type) + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + # + # { + # "amount":"29.889184", + # "asset_id":5, + # "balance":"29.889184", + # "created_at":"2020-11-15T21:25:01Z", + # "meta_data":{ + # "deposit_id":3884, + # "transaction_id":"0x41a60174849828530abb5008e98fc63c9b598288743ec4ba9620bcce900a3b8d" + # }, + # "transaction_type":"deposit", + # "user_id":22142, + # "uuid":"70bb5679da3c4637884e2dc63efaa846" + # } + # + id = self.safe_string(item, 'uuid') + direction = None + account = None + metaData = self.safe_dict(item, 'meta_data', {}) + referenceId = self.safe_string(metaData, 'transaction_id') + referenceAccount = None + type = self.safe_string(item, 'transaction_type') + if (type == 'deposit') or (type == 'commission_rebate') or (type == 'referral_bonus') or (type == 'pnl') or (type == 'withdrawal_cancellation') or (type == 'promo_credit'): + direction = 'in' + elif (type == 'withdrawal') or (type == 'commission') or (type == 'conversion') or (type == 'perpetual_futures_funding'): + direction = 'out' + type = self.parse_ledger_entry_type(type) + currencyId = self.safe_string(item, 'asset_id') + currenciesByNumericId = self.safe_dict(self.options, 'currenciesByNumericId') + currency = self.safe_value(currenciesByNumericId, currencyId, currency) + code = None if (currency is None) else currency['code'] + amount = self.safe_string(item, 'amount') + timestamp = self.parse8601(self.safe_string(item, 'created_at')) + after = self.safe_string(item, 'balance') + before = Precise.string_max('0', Precise.string_sub(after, amount)) + status = 'ok' + return self.safe_ledger_entry({ + 'info': item, + 'id': id, + 'direction': direction, + 'account': account, + 'referenceId': referenceId, + 'referenceAccount': referenceAccount, + 'type': type, + 'currency': code, + 'amount': self.parse_number(amount), + 'before': self.parse_number(before), + 'after': self.parse_number(after), + 'status': status, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'fee': None, + }, currency) + + async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.network]: unified network code + :returns dict: an `address structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'asset_symbol': currency['id'], + } + networkCode = self.safe_string_upper(params, 'network') + if networkCode is not None: + request['network'] = self.network_code_to_id(networkCode, code) + params = self.omit(params, 'network') + response = await self.privateGetDepositsAddress(self.extend(request, params)) + # + # { + # "success": True, + # "result": { + # "id": 1915615, + # "user_id": 27854758, + # "address": "TXYB4GdKsXKEWbeSNPsmGZu4ZVCkhVh1Zz", + # "memo": "", + # "status": "active", + # "updated_at": "2023-01-12T06:03:46.000Z", + # "created_at": "2023-01-12T06:03:46.000Z", + # "asset_symbol": "USDT", + # "network": "TRC20(TRON)", + # "custodian": "fireblocks" + # } + # } + # + result = self.safe_dict(response, 'result', {}) + return self.parse_deposit_address(result, currency) + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + # + # { + # "id": 1915615, + # "user_id": 27854758, + # "address": "TXYB4GdKsXKEWbeSNPsmGZu4ZVCkhVh1Zz", + # "memo": "", + # "status": "active", + # "updated_at": "2023-01-12T06:03:46.000Z", + # "created_at": "2023-01-12T06:03:46.000Z", + # "asset_symbol": "USDT", + # "network": "TRC20(TRON)", + # "custodian": "fireblocks" + # } + # + address = self.safe_string(depositAddress, 'address') + marketId = self.safe_string(depositAddress, 'asset_symbol') + networkId = self.safe_string(depositAddress, 'network') + self.check_address(address) + return { + 'info': depositAddress, + 'currency': self.safe_currency_code(marketId, currency), + 'network': self.network_id_to_code(networkId), + 'address': address, + 'tag': self.safe_string(depositAddress, 'memo'), + } + + async def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate + + https://docs.delta.exchange/#get-ticker-for-a-product-by-symbol + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + await self.load_markets() + market = self.market(symbol) + if not market['swap']: + raise BadSymbol(self.id + ' fetchFundingRate() supports swap contracts only') + request: dict = { + 'symbol': market['id'], + } + response = await self.publicGetTickersSymbol(self.extend(request, params)) + # + # { + # "result": { + # "close": 30600.5, + # "contract_type": "perpetual_futures", + # "funding_rate": "0.00602961", + # "greeks": null, + # "high": 30803.0, + # "low": 30265.5, + # "mark_basis": "-0.45601594", + # "mark_price": "30600.10481568", + # "oi": "469.9190", + # "oi_change_usd_6h": "2226314.9900", + # "oi_contracts": "469919", + # "oi_value": "469.9190", + # "oi_value_symbol": "BTC", + # "oi_value_usd": "14385640.6802", + # "open": 30458.5, + # "price_band": { + # "lower_limit": "29067.08312627", + # "upper_limit": "32126.77608693" + # }, + # "product_id": 139, + # "quotes": { + # "ask_iv": null, + # "ask_size": "965", + # "best_ask": "30600.5", + # "best_bid": "30599.5", + # "bid_iv": null, + # "bid_size": "196", + # "impact_mid_price": null, + # "mark_iv": "-0.44931641" + # }, + # "size": 1226303, + # "spot_price": "30612.85362773", + # "symbol": "BTCUSDT", + # "timestamp": 1689136597460456, + # "turnover": 37392218.45999999, + # "turnover_symbol": "USDT", + # "turnover_usd": 37392218.45999999, + # "volume": 1226.3029999999485 + # }, + # "success": True + # } + # + result = self.safe_dict(response, 'result', {}) + return self.parse_funding_rate(result, market) + + async def fetch_funding_rates(self, symbols: Strings = None, params={}) -> FundingRates: + """ + fetch the funding rate for multiple markets + + https://docs.delta.exchange/#get-tickers-for-products + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `funding rate structures `, indexed by market symbols + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + request: dict = { + 'contract_types': 'perpetual_futures', + } + response = await self.publicGetTickers(self.extend(request, params)) + # + # { + # "result": [ + # { + # "close": 30600.5, + # "contract_type": "perpetual_futures", + # "funding_rate": "0.00602961", + # "greeks": null, + # "high": 30803.0, + # "low": 30265.5, + # "mark_basis": "-0.45601594", + # "mark_price": "30600.10481568", + # "oi": "469.9190", + # "oi_change_usd_6h": "2226314.9900", + # "oi_contracts": "469919", + # "oi_value": "469.9190", + # "oi_value_symbol": "BTC", + # "oi_value_usd": "14385640.6802", + # "open": 30458.5, + # "price_band": { + # "lower_limit": "29067.08312627", + # "upper_limit": "32126.77608693" + # }, + # "product_id": 139, + # "quotes": { + # "ask_iv": null, + # "ask_size": "965", + # "best_ask": "30600.5", + # "best_bid": "30599.5", + # "bid_iv": null, + # "bid_size": "196", + # "impact_mid_price": null, + # "mark_iv": "-0.44931641" + # }, + # "size": 1226303, + # "spot_price": "30612.85362773", + # "symbol": "BTCUSDT", + # "timestamp": 1689136597460456, + # "turnover": 37392218.45999999, + # "turnover_symbol": "USDT", + # "turnover_usd": 37392218.45999999, + # "volume": 1226.3029999999485 + # }, + # ], + # "success":true + # } + # + rates = self.safe_list(response, 'result', []) + return self.parse_funding_rates(rates, symbols) + + def parse_funding_rate(self, contract, market: Market = None) -> FundingRate: + # + # { + # "close": 30600.5, + # "contract_type": "perpetual_futures", + # "funding_rate": "0.00602961", + # "greeks": null, + # "high": 30803.0, + # "low": 30265.5, + # "mark_basis": "-0.45601594", + # "mark_price": "30600.10481568", + # "oi": "469.9190", + # "oi_change_usd_6h": "2226314.9900", + # "oi_contracts": "469919", + # "oi_value": "469.9190", + # "oi_value_symbol": "BTC", + # "oi_value_usd": "14385640.6802", + # "open": 30458.5, + # "price_band": { + # "lower_limit": "29067.08312627", + # "upper_limit": "32126.77608693" + # }, + # "product_id": 139, + # "quotes": { + # "ask_iv": null, + # "ask_size": "965", + # "best_ask": "30600.5", + # "best_bid": "30599.5", + # "bid_iv": null, + # "bid_size": "196", + # "impact_mid_price": null, + # "mark_iv": "-0.44931641" + # }, + # "size": 1226303, + # "spot_price": "30612.85362773", + # "symbol": "BTCUSDT", + # "timestamp": 1689136597460456, + # "turnover": 37392218.45999999, + # "turnover_symbol": "USDT", + # "turnover_usd": 37392218.45999999, + # "volume": 1226.3029999999485 + # } + # + timestamp = self.safe_integer_product(contract, 'timestamp', 0.001) + marketId = self.safe_string(contract, 'symbol') + fundingRateString = self.safe_string(contract, 'funding_rate') + fundingRate = Precise.string_div(fundingRateString, '100') + return { + 'info': contract, + 'symbol': self.safe_symbol(marketId, market), + 'markPrice': self.safe_number(contract, 'mark_price'), + 'indexPrice': self.safe_number(contract, 'spot_price'), + 'interestRate': None, + 'estimatedSettlePrice': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'fundingRate': self.parse_number(fundingRate), + 'fundingTimestamp': None, + 'fundingDatetime': None, + 'nextFundingRate': None, + 'nextFundingTimestamp': None, + 'nextFundingDatetime': None, + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': None, + } + + async def add_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + add margin + + https://docs.delta.exchange/#add-remove-position-margin + + :param str symbol: unified market symbol + :param float amount: amount of margin to add + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin structure ` + """ + return await self.modify_margin_helper(symbol, amount, 'add', params) + + async def reduce_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + remove margin from a position + + https://docs.delta.exchange/#add-remove-position-margin + + :param str symbol: unified market symbol + :param float amount: the amount of margin to remove + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin structure ` + """ + return await self.modify_margin_helper(symbol, amount, 'reduce', params) + + async def modify_margin_helper(self, symbol: str, amount, type, params={}) -> MarginModification: + await self.load_markets() + market = self.market(symbol) + amount = str(amount) + if type == 'reduce': + amount = Precise.string_mul(amount, '-1') + request: dict = { + 'product_id': market['numericId'], + 'delta_margin': amount, + } + response = await self.privatePostPositionsChangeMargin(self.extend(request, params)) + # + # { + # "result": { + # "auto_topup": False, + # "bankruptcy_price": "24934.12", + # "commission": "0.01197072", + # "created_at": "2023-07-20T03:49:09.159401Z", + # "entry_price": "29926.8", + # "liquidation_price": "25083.754", + # "margin": "4.99268", + # "margin_mode": "isolated", + # "product_id": 84, + # "product_symbol": "BTCUSDT", + # "realized_cashflow": "0", + # "realized_funding": "0", + # "realized_pnl": "0", + # "size": 1, + # "updated_at": "2023-07-20T03:49:09.159401Z", + # "user_id": 30084879 + # }, + # "success": True + # } + # + result = self.safe_dict(response, 'result', {}) + return self.parse_margin_modification(result, market) + + def parse_margin_modification(self, data: dict, market: Market = None) -> MarginModification: + # + # { + # "auto_topup": False, + # "bankruptcy_price": "24934.12", + # "commission": "0.01197072", + # "created_at": "2023-07-20T03:49:09.159401Z", + # "entry_price": "29926.8", + # "liquidation_price": "25083.754", + # "margin": "4.99268", + # "margin_mode": "isolated", + # "product_id": 84, + # "product_symbol": "BTCUSDT", + # "realized_cashflow": "0", + # "realized_funding": "0", + # "realized_pnl": "0", + # "size": 1, + # "updated_at": "2023-07-20T03:49:09.159401Z", + # "user_id": 30084879 + # } + # + marketId = self.safe_string(data, 'product_symbol') + market = self.safe_market(marketId, market) + return { + 'info': data, + 'symbol': market['symbol'], + 'type': None, + 'marginMode': 'isolated', + 'amount': None, + 'total': self.safe_number(data, 'margin'), + 'code': None, + 'status': None, + 'timestamp': None, + 'datetime': None, + } + + async def fetch_open_interest(self, symbol: str, params={}): + """ + retrieves the open interest of a derivative market + + https://docs.delta.exchange/#get-ticker-for-a-product-by-symbol + + :param str symbol: unified market symbol + :param dict [params]: exchange specific parameters + :returns dict} an open interest structure{@link https://docs.ccxt.com/#/?id=open-interest-structure: + """ + await self.load_markets() + market = self.market(symbol) + if not market['contract']: + raise BadRequest(self.id + ' fetchOpenInterest() supports contract markets only') + request: dict = { + 'symbol': market['id'], + } + response = await self.publicGetTickersSymbol(self.extend(request, params)) + # + # { + # "result": { + # "close": 894.0, + # "contract_type": "call_options", + # "greeks": { + # "delta": "0.67324861", + # "gamma": "0.00022178", + # "rho": "4.34638266", + # "spot": "30178.53195697", + # "theta": "-35.64972577", + # "vega": "16.34381277" + # }, + # "high": 946.0, + # "low": 893.0, + # "mark_price": "1037.07582681", + # "mark_vol": "0.35899491", + # "oi": "0.0910", + # "oi_change_usd_6h": "-90.5500", + # "oi_contracts": "91", + # "oi_value": "0.0910", + # "oi_value_symbol": "BTC", + # "oi_value_usd": "2746.3549", + # "open": 946.0, + # "price_band": { + # "lower_limit": "133.37794509", + # "upper_limit": "5663.66930164" + # }, + # "product_id": 116171, + # "quotes": { + # "ask_iv": "0.36932389", + # "ask_size": "1321", + # "best_ask": "1054", + # "best_bid": "1020", + # "bid_iv": "0.34851914", + # "bid_size": "2202", + # "impact_mid_price": null, + # "mark_iv": "0.35896335" + # }, + # "size": 152, + # "spot_price": "30178.53195697", + # "strike_price": "29500", + # "symbol": "C-BTC-29500-280723", + # "timestamp": 1689834695286094, + # "turnover": 4546.601744940001, + # "turnover_symbol": "USDT", + # "turnover_usd": 4546.601744940001, + # "volume": 0.15200000000000002 + # }, + # "success": True + # } + # + result = self.safe_dict(response, 'result', {}) + return self.parse_open_interest(result, market) + + def parse_open_interest(self, interest, market: Market = None): + # + # { + # "close": 894.0, + # "contract_type": "call_options", + # "greeks": { + # "delta": "0.67324861", + # "gamma": "0.00022178", + # "rho": "4.34638266", + # "spot": "30178.53195697", + # "theta": "-35.64972577", + # "vega": "16.34381277" + # }, + # "high": 946.0, + # "low": 893.0, + # "mark_price": "1037.07582681", + # "mark_vol": "0.35899491", + # "oi": "0.0910", + # "oi_change_usd_6h": "-90.5500", + # "oi_contracts": "91", + # "oi_value": "0.0910", + # "oi_value_symbol": "BTC", + # "oi_value_usd": "2746.3549", + # "open": 946.0, + # "price_band": { + # "lower_limit": "133.37794509", + # "upper_limit": "5663.66930164" + # }, + # "product_id": 116171, + # "quotes": { + # "ask_iv": "0.36932389", + # "ask_size": "1321", + # "best_ask": "1054", + # "best_bid": "1020", + # "bid_iv": "0.34851914", + # "bid_size": "2202", + # "impact_mid_price": null, + # "mark_iv": "0.35896335" + # }, + # "size": 152, + # "spot_price": "30178.53195697", + # "strike_price": "29500", + # "symbol": "C-BTC-29500-280723", + # "timestamp": 1689834695286094, + # "turnover": 4546.601744940001, + # "turnover_symbol": "USDT", + # "turnover_usd": 4546.601744940001, + # "volume": 0.15200000000000002 + # } + # + timestamp = self.safe_integer_product(interest, 'timestamp', 0.001) + marketId = self.safe_string(interest, 'symbol') + return self.safe_open_interest({ + 'symbol': self.safe_symbol(marketId, market), + 'baseVolume': self.safe_number(interest, 'oi_value'), + 'quoteVolume': self.safe_number(interest, 'oi_value_usd'), + 'openInterestAmount': self.safe_number(interest, 'oi_contracts'), + 'openInterestValue': self.safe_number(interest, 'oi'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'info': interest, + }, market) + + async def fetch_leverage(self, symbol: str, params={}) -> Leverage: + """ + fetch the set leverage for a market + + https://docs.delta.exchange/#get-order-leverage + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `leverage structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'product_id': market['numericId'], + } + response = await self.privateGetProductsProductIdOrdersLeverage(self.extend(request, params)) + # + # { + # "result": { + # "index_symbol": null, + # "leverage": "10", + # "margin_mode": "isolated", + # "order_margin": "0", + # "product_id": 84, + # "user_id": 30084879 + # }, + # "success": True + # } + # + result = self.safe_dict(response, 'result', {}) + return self.parse_leverage(result, market) + + def parse_leverage(self, leverage: dict, market: Market = None) -> Leverage: + marketId = self.safe_string(leverage, 'index_symbol') + leverageValue = self.safe_integer(leverage, 'leverage') + return { + 'info': leverage, + 'symbol': self.safe_symbol(marketId, market), + 'marginMode': self.safe_string_lower(leverage, 'margin_mode'), + 'longLeverage': leverageValue, + 'shortLeverage': leverageValue, + } + + async def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://docs.delta.exchange/#change-order-leverage + + :param float leverage: the rate of leverage + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setLeverage() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'product_id': market['numericId'], + 'leverage': leverage, + } + # + # { + # "result": { + # "leverage": "20", + # "margin_mode": "isolated", + # "order_margin": "0", + # "product_id": 84 + # }, + # "success": True + # } + # + return await self.privatePostProductsProductIdOrdersLeverage(self.extend(request, params)) + + async def fetch_settlement_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical settlement records + + https://docs.delta.exchange/#get-product-settlement-prices + + :param str symbol: unified market symbol of the settlement history + :param int [since]: timestamp in ms + :param int [limit]: number of records + :param dict [params]: exchange specific params + :returns dict[]: a list of `settlement history objects ` + """ + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + request: dict = { + 'states': 'expired', + } + if limit is not None: + request['page_size'] = limit + response = await self.publicGetProducts(self.extend(request, params)) + # + # { + # "result": [ + # { + # "contract_value": "0.001", + # "basis_factor_max_limit": "10.95", + # "maker_commission_rate": "0.0003", + # "launch_time": "2023-07-19T04:30:03Z", + # "trading_status": "operational", + # "product_specs": { + # "backup_vol_expiry_time": 31536000, + # "max_deviation_from_external_vol": 0.75, + # "max_lower_deviation_from_external_vol": 0.75, + # "max_upper_deviation_from_external_vol": 0.5, + # "max_volatility": 3, + # "min_volatility": 0.1, + # "premium_commission_rate": 0.1, + # "settlement_index_price": "29993.536675710806", + # "vol_calculation_method": "orderbook", + # "vol_expiry_time": 31536000 + # }, + # "description": "BTC call option expiring on 19-7-2023", + # "settlement_price": "0", + # "disruption_reason": null, + # "settling_asset": {}, + # "initial_margin": "1", + # "tick_size": "0.1", + # "maintenance_margin": "0.5", + # "id": 117542, + # "notional_type": "vanilla", + # "ui_config": {}, + # "contract_unit_currency": "BTC", + # "symbol": "C-BTC-30900-190723", + # "insurance_fund_margin_contribution": "1", + # "price_band": "2", + # "annualized_funding": "10.95", + # "impact_size": 200, + # "contract_type": "call_options", + # "position_size_limit": 255633, + # "max_leverage_notional": "200000", + # "initial_margin_scaling_factor": "0.000002", + # "strike_price": "30900", + # "is_quanto": False, + # "settlement_time": "2023-07-19T12:00:00Z", + # "liquidation_penalty_factor": "0.5", + # "funding_method": "mark_price", + # "taker_commission_rate": "0.0003", + # "default_leverage": "100.000000000000000000", + # "state": "expired", + # "auction_start_time": null, + # "short_description": "BTC Call", + # "quoting_asset": {}, + # "maintenance_margin_scaling_factor":"0.000002" + # } + # ], + # "success": True + # } + # + result = self.safe_list(response, 'result', []) + settlements = self.parse_settlements(result, market) + sorted = self.sort_by(settlements, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, market['symbol'], since, limit) + + def parse_settlement(self, settlement, market): + # + # { + # "contract_value": "0.001", + # "basis_factor_max_limit": "10.95", + # "maker_commission_rate": "0.0003", + # "launch_time": "2023-07-19T04:30:03Z", + # "trading_status": "operational", + # "product_specs": { + # "backup_vol_expiry_time": 31536000, + # "max_deviation_from_external_vol": 0.75, + # "max_lower_deviation_from_external_vol": 0.75, + # "max_upper_deviation_from_external_vol": 0.5, + # "max_volatility": 3, + # "min_volatility": 0.1, + # "premium_commission_rate": 0.1, + # "settlement_index_price": "29993.536675710806", + # "vol_calculation_method": "orderbook", + # "vol_expiry_time": 31536000 + # }, + # "description": "BTC call option expiring on 19-7-2023", + # "settlement_price": "0", + # "disruption_reason": null, + # "settling_asset": {}, + # "initial_margin": "1", + # "tick_size": "0.1", + # "maintenance_margin": "0.5", + # "id": 117542, + # "notional_type": "vanilla", + # "ui_config": {}, + # "contract_unit_currency": "BTC", + # "symbol": "C-BTC-30900-190723", + # "insurance_fund_margin_contribution": "1", + # "price_band": "2", + # "annualized_funding": "10.95", + # "impact_size": 200, + # "contract_type": "call_options", + # "position_size_limit": 255633, + # "max_leverage_notional": "200000", + # "initial_margin_scaling_factor": "0.000002", + # "strike_price": "30900", + # "is_quanto": False, + # "settlement_time": "2023-07-19T12:00:00Z", + # "liquidation_penalty_factor": "0.5", + # "funding_method": "mark_price", + # "taker_commission_rate": "0.0003", + # "default_leverage": "100.000000000000000000", + # "state": "expired", + # "auction_start_time": null, + # "short_description": "BTC Call", + # "quoting_asset": {}, + # "maintenance_margin_scaling_factor":"0.000002" + # } + # + datetime = self.safe_string(settlement, 'settlement_time') + marketId = self.safe_string(settlement, 'symbol') + return { + 'info': settlement, + 'symbol': self.safe_symbol(marketId, market), + 'price': self.safe_number(settlement, 'settlement_price'), + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + } + + def parse_settlements(self, settlements, market): + result = [] + for i in range(0, len(settlements)): + result.append(self.parse_settlement(settlements[i], market)) + return result + + async def fetch_greeks(self, symbol: str, params={}) -> Greeks: + """ + fetches an option contracts greeks, financial metrics used to measure the factors that affect the price of an options contract + + https://docs.delta.exchange/#get-ticker-for-a-product-by-symbol + + :param str symbol: unified symbol of the market to fetch greeks for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `greeks structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = await self.publicGetTickersSymbol(self.extend(request, params)) + # + # { + # "result": { + # "close": 6793.0, + # "contract_type": "call_options", + # "greeks": { + # "delta": "0.94739174", + # "gamma": "0.00002206", + # "rho": "11.00890725", + # "spot": "36839.58124652", + # "theta": "-18.18365310", + # "vega": "7.85209698" + # }, + # "high": 7556.0, + # "low": 6793.0, + # "mark_price": "6955.70698909", + # "mark_vol": "0.66916863", + # "oi": "1.8980", + # "oi_change_usd_6h": "110.4600", + # "oi_contracts": "1898", + # "oi_value": "1.8980", + # "oi_value_symbol": "BTC", + # "oi_value_usd": "69940.7319", + # "open": 7.2e3, + # "price_band": { + # "lower_limit": "5533.89814767", + # "upper_limit": "11691.37688371" + # }, + # "product_id": 129508, + # "quotes": { + # "ask_iv": "0.90180438", + # "ask_size": "1898", + # "best_ask": "7210", + # "best_bid": "6913", + # "bid_iv": "0.60881706", + # "bid_size": "3163", + # "impact_mid_price": null, + # "mark_iv": "0.66973549" + # }, + # "size": 5, + # "spot_price": "36839.58153868", + # "strike_price": "30000", + # "symbol": "C-BTC-30000-241123", + # "timestamp": 1699584998504530, + # "turnover": 184.41206804, + # "turnover_symbol": "USDT", + # "turnover_usd": 184.41206804, + # "volume": 0.005 + # }, + # "success": True + # } + # + result = self.safe_dict(response, 'result', {}) + return self.parse_greeks(result, market) + + def parse_greeks(self, greeks: dict, market: Market = None) -> Greeks: + # + # { + # "close": 6793.0, + # "contract_type": "call_options", + # "greeks": { + # "delta": "0.94739174", + # "gamma": "0.00002206", + # "rho": "11.00890725", + # "spot": "36839.58124652", + # "theta": "-18.18365310", + # "vega": "7.85209698" + # }, + # "high": 7556.0, + # "low": 6793.0, + # "mark_price": "6955.70698909", + # "mark_vol": "0.66916863", + # "oi": "1.8980", + # "oi_change_usd_6h": "110.4600", + # "oi_contracts": "1898", + # "oi_value": "1.8980", + # "oi_value_symbol": "BTC", + # "oi_value_usd": "69940.7319", + # "open": 7.2e3, + # "price_band": { + # "lower_limit": "5533.89814767", + # "upper_limit": "11691.37688371" + # }, + # "product_id": 129508, + # "quotes": { + # "ask_iv": "0.90180438", + # "ask_size": "1898", + # "best_ask": "7210", + # "best_bid": "6913", + # "bid_iv": "0.60881706", + # "bid_size": "3163", + # "impact_mid_price": null, + # "mark_iv": "0.66973549" + # }, + # "size": 5, + # "spot_price": "36839.58153868", + # "strike_price": "30000", + # "symbol": "C-BTC-30000-241123", + # "timestamp": 1699584998504530, + # "turnover": 184.41206804, + # "turnover_symbol": "USDT", + # "turnover_usd": 184.41206804, + # "volume": 0.005 + # } + # + timestamp = self.safe_integer_product(greeks, 'timestamp', 0.001) + marketId = self.safe_string(greeks, 'symbol') + symbol = self.safe_symbol(marketId, market) + stats = self.safe_dict(greeks, 'greeks', {}) + quotes = self.safe_dict(greeks, 'quotes', {}) + return { + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'delta': self.safe_number(stats, 'delta'), + 'gamma': self.safe_number(stats, 'gamma'), + 'theta': self.safe_number(stats, 'theta'), + 'vega': self.safe_number(stats, 'vega'), + 'rho': self.safe_number(stats, 'rho'), + 'bidSize': self.safe_number(quotes, 'bid_size'), + 'askSize': self.safe_number(quotes, 'ask_size'), + 'bidImpliedVolatility': self.safe_number(quotes, 'bid_iv'), + 'askImpliedVolatility': self.safe_number(quotes, 'ask_iv'), + 'markImpliedVolatility': self.safe_number(quotes, 'mark_iv'), + 'bidPrice': self.safe_number(quotes, 'best_bid'), + 'askPrice': self.safe_number(quotes, 'best_ask'), + 'markPrice': self.safe_number(greeks, 'mark_price'), + 'lastPrice': None, + 'underlyingPrice': self.safe_number(greeks, 'spot_price'), + 'info': greeks, + } + + async def close_all_positions(self, params={}) -> List[Position]: + """ + closes all open positions for a market type + + https://docs.delta.exchange/#close-all-positions + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.user_id]: the users id + :returns dict[]: A list of `position structures ` + """ + await self.load_markets() + request: dict = { + 'close_all_portfolio': True, + 'close_all_isolated': True, + # 'user_id': 12345, + } + response = await self.privatePostPositionsCloseAll(self.extend(request, params)) + # + # {"result":{},"success":true} + # + position = self.parse_position(self.safe_dict(response, 'result', {})) + return [position] + + async def fetch_margin_mode(self, symbol: str, params={}) -> MarginMode: + """ + fetches the margin mode of a trading pair + + https://docs.delta.exchange/#get-user + + :param str symbol: unified symbol of the market to fetch the margin mode for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin mode structure ` + """ + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + response = await self.privateGetProfile(params) + # + # { + # "result": { + # "is_password_set": True, + # "kyc_expiry_date": null, + # "phishing_code": "12345", + # "preferences": { + # "favorites": [] + # }, + # "is_kyc_provisioned": False, + # "country": "Canada", + # "margin_mode": "isolated", + # "mfa_updated_at": "2023-07-19T01:04:43Z", + # "last_name": "", + # "oauth_apple_active": False, + # "pf_index_symbol": null, + # "proof_of_identity_status": "approved", + # "dob": null, + # "email": "abc_123@gmail.com", + # "force_change_password": False, + # "nick_name": "still-breeze-123", + # "oauth_google_active": False, + # "phone_verification_status": "verified", + # "id": 12345678, + # "last_seen": null, + # "is_withdrawal_enabled": True, + # "force_change_mfa": False, + # "enable_bots": False, + # "kyc_verified_on": null, + # "created_at": "2023-07-19T01:02:32Z", + # "withdrawal_blocked_till": null, + # "proof_of_address_status": "approved", + # "is_password_change_blocked": False, + # "is_mfa_enabled": True, + # "is_kyc_done": True, + # "oauth": null, + # "account_name": "Main", + # "sub_account_permissions": null, + # "phone_number": null, + # "tracking_info": { + # "ga_cid": "1234.4321", + # "is_kyc_gtm_tracked": True, + # "sub_account_config": { + # "cross": 2, + # "isolated": 2, + # "portfolio": 2 + # } + # }, + # "first_name": "", + # "phone_verified_on": null, + # "seen_intro": False, + # "password_updated_at": null, + # "is_login_enabled": True, + # "registration_date": "2023-07-19T01:02:32Z", + # "permissions": {}, + # "max_sub_accounts_limit": 2, + # "country_calling_code": null, + # "is_sub_account": False, + # "is_kyc_refresh_required": False + # }, + # "success": True + # } + # + result = self.safe_dict(response, 'result', {}) + return self.parse_margin_mode(result, market) + + def parse_margin_mode(self, marginMode: dict, market=None) -> MarginMode: + symbol = None + if market is not None: + symbol = market['symbol'] + return { + 'info': marginMode, + 'symbol': symbol, + 'marginMode': self.safe_string(marginMode, 'margin_mode'), + } + + async def fetch_option(self, symbol: str, params={}) -> Option: + """ + fetches option data that is commonly found in an option chain + + https://docs.delta.exchange/#get-ticker-for-a-product-by-symbol + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `option chain structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = await self.publicGetTickersSymbol(self.extend(request, params)) + # + # { + # "result": { + # "close": 6793.0, + # "contract_type": "call_options", + # "greeks": { + # "delta": "0.94739174", + # "gamma": "0.00002206", + # "rho": "11.00890725", + # "spot": "36839.58124652", + # "theta": "-18.18365310", + # "vega": "7.85209698" + # }, + # "high": 7556.0, + # "low": 6793.0, + # "mark_price": "6955.70698909", + # "mark_vol": "0.66916863", + # "oi": "1.8980", + # "oi_change_usd_6h": "110.4600", + # "oi_contracts": "1898", + # "oi_value": "1.8980", + # "oi_value_symbol": "BTC", + # "oi_value_usd": "69940.7319", + # "open": 7.2e3, + # "price_band": { + # "lower_limit": "5533.89814767", + # "upper_limit": "11691.37688371" + # }, + # "product_id": 129508, + # "quotes": { + # "ask_iv": "0.90180438", + # "ask_size": "1898", + # "best_ask": "7210", + # "best_bid": "6913", + # "bid_iv": "0.60881706", + # "bid_size": "3163", + # "impact_mid_price": null, + # "mark_iv": "0.66973549" + # }, + # "size": 5, + # "spot_price": "36839.58153868", + # "strike_price": "30000", + # "symbol": "C-BTC-30000-241123", + # "timestamp": 1699584998504530, + # "turnover": 184.41206804, + # "turnover_symbol": "USDT", + # "turnover_usd": 184.41206804, + # "volume": 0.005 + # }, + # "success": True + # } + # + result = self.safe_dict(response, 'result', {}) + return self.parse_option(result, None, market) + + def parse_option(self, chain: dict, currency: Currency = None, market: Market = None) -> Option: + # + # { + # "close": 6793.0, + # "contract_type": "call_options", + # "greeks": { + # "delta": "0.94739174", + # "gamma": "0.00002206", + # "rho": "11.00890725", + # "spot": "36839.58124652", + # "theta": "-18.18365310", + # "vega": "7.85209698" + # }, + # "high": 7556.0, + # "low": 6793.0, + # "mark_price": "6955.70698909", + # "mark_vol": "0.66916863", + # "oi": "1.8980", + # "oi_change_usd_6h": "110.4600", + # "oi_contracts": "1898", + # "oi_value": "1.8980", + # "oi_value_symbol": "BTC", + # "oi_value_usd": "69940.7319", + # "open": 7.2e3, + # "price_band": { + # "lower_limit": "5533.89814767", + # "upper_limit": "11691.37688371" + # }, + # "product_id": 129508, + # "quotes": { + # "ask_iv": "0.90180438", + # "ask_size": "1898", + # "best_ask": "7210", + # "best_bid": "6913", + # "bid_iv": "0.60881706", + # "bid_size": "3163", + # "impact_mid_price": null, + # "mark_iv": "0.66973549" + # }, + # "size": 5, + # "spot_price": "36839.58153868", + # "strike_price": "30000", + # "symbol": "C-BTC-30000-241123", + # "timestamp": 1699584998504530, + # "turnover": 184.41206804, + # "turnover_symbol": "USDT", + # "turnover_usd": 184.41206804, + # "volume": 0.005 + # } + # + marketId = self.safe_string(chain, 'symbol') + market = self.safe_market(marketId, market) + quotes = self.safe_dict(chain, 'quotes', {}) + timestamp = self.safe_integer_product(chain, 'timestamp', 0.001) + return { + 'info': chain, + 'currency': None, + 'symbol': market['symbol'], + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'impliedVolatility': self.safe_number(quotes, 'mark_iv'), + 'openInterest': self.safe_number(chain, 'oi'), + 'bidPrice': self.safe_number(quotes, 'best_bid'), + 'askPrice': self.safe_number(quotes, 'best_ask'), + 'midPrice': self.safe_number(quotes, 'impact_mid_price'), + 'markPrice': self.safe_number(chain, 'mark_price'), + 'lastPrice': None, + 'underlyingPrice': self.safe_number(chain, 'spot_price'), + 'change': None, + 'percentage': None, + 'baseVolume': self.safe_number(chain, 'volume'), + 'quoteVolume': None, + } + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + requestPath = '/' + self.version + '/' + self.implode_params(path, params) + url = self.urls['api'][api] + requestPath + query = self.omit(params, self.extract_params(path)) + if api == 'public': + if query: + url += '?' + self.urlencode(query) + elif api == 'private': + self.check_required_credentials() + timestamp = str(self.seconds()) + headers = { + 'api-key': self.apiKey, + 'timestamp': timestamp, + } + auth = method + timestamp + requestPath + if method == 'GET': + if query: + queryString = '?' + self.urlencode(query) + auth += queryString + url += queryString + else: + body = self.json(query) + auth += body + headers['Content-Type'] = 'application/json' + signature = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256) + headers['signature'] = signature + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None + # + # {"error":{"code":"insufficient_margin","context":{"available_balance":"0.000000000000000000","required_additional_balance":"1.618626000000000000000000000"}},"success":false} + # + error = self.safe_dict(response, 'error', {}) + errorCode = self.safe_string(error, 'code') + if errorCode is not None: + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], errorCode, feedback) + raise ExchangeError(feedback) # unknown message + return None diff --git a/ccxt/async_support/deribit.py b/ccxt/async_support/deribit.py new file mode 100644 index 0000000..babc125 --- /dev/null +++ b/ccxt/async_support/deribit.py @@ -0,0 +1,3666 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.deribit import ImplicitAPI +import hashlib +from ccxt.base.types import Account, Any, Balances, Currencies, Currency, DepositAddress, Greeks, Int, Market, Num, Option, OptionChain, Order, OrderBook, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, Trade, TradingFees, Transaction, MarketInterface, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidAddress +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import NotSupported +from ccxt.base.errors import DDoSProtection +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.errors import OnMaintenance +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class deribit(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(deribit, self).describe(), { + 'id': 'deribit', + 'name': 'Deribit', + 'countries': ['NL'], # Netherlands + 'version': 'v2', + 'userAgent': None, + # 20 requests per second for non-matching-engine endpoints, 1000ms / 20 = 50ms between requests + # 5 requests per second for matching-engine endpoints, cost = (1000ms / rateLimit) / 5 = 4 + 'rateLimit': 50, + 'pro': True, + 'has': { + 'CORS': True, + 'spot': False, + 'margin': False, + 'swap': True, + 'future': True, + 'option': True, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'cancelOrders': False, + 'createDepositAddress': True, + 'createOrder': True, + 'createReduceOnlyOrder': True, + 'createStopLimitOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'createTrailingAmountOrder': True, + 'editOrder': True, + 'fetchAccounts': True, + 'fetchBalance': True, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDeposit': False, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchDepositWithdrawFees': True, + 'fetchFundingRate': True, + 'fetchFundingRateHistory': True, + 'fetchGreeks': True, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': True, + 'fetchMarginMode': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMyLiquidations': True, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenOrders': True, + 'fetchOption': True, + 'fetchOptionChain': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': False, + 'fetchOrderTrades': True, + 'fetchPosition': True, + 'fetchPositionMode': False, + 'fetchPositions': True, + 'fetchPremiumIndexOHLCV': False, + 'fetchStatus': True, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': True, + 'fetchTransactions': False, + 'fetchTransfer': False, + 'fetchTransfers': True, + 'fetchUnderlyingAssets': False, + 'fetchVolatilityHistory': True, + 'fetchWithdrawal': False, + 'fetchWithdrawals': True, + 'sandbox': True, + 'transfer': True, + 'withdraw': True, + }, + 'timeframes': { + '1m': '1', + '3m': '3', + '5m': '5', + '10m': '10', + '15m': '15', + '30m': '30', + '1h': '60', + '2h': '120', + '3h': '180', + '6h': '360', + '12h': '720', + '1d': '1D', + }, + 'urls': { + 'test': { + 'rest': 'https://test.deribit.com', + }, + 'logo': 'https://user-images.githubusercontent.com/1294454/41933112-9e2dd65a-798b-11e8-8440-5bab2959fcb8.jpg', + 'api': { + 'rest': 'https://www.deribit.com', + }, + 'www': 'https://www.deribit.com', + 'doc': [ + 'https://docs.deribit.com/v2', + 'https://github.com/deribit', + ], + 'fees': 'https://www.deribit.com/pages/information/fees', + 'referral': { + 'url': 'https://www.deribit.com/reg-1189.4038', + 'discount': 0.1, + }, + }, + 'api': { + 'public': { + 'get': { + # Authentication + 'auth': 1, + 'exchange_token': 1, + 'fork_token': 1, + # Session management + 'set_heartbeat': 1, + 'disable_heartbeat': 1, + # Supporting + 'get_time': 1, + 'hello': 1, + 'status': 1, + 'test': 1, + # Subscription management + 'subscribe': 1, + 'unsubscribe': 1, + 'unsubscribe_all': 1, + # Account management + 'get_announcements': 1, + # Market data + 'get_book_summary_by_currency': 1, + 'get_book_summary_by_instrument': 1, + 'get_contract_size': 1, + 'get_currencies': 1, + 'get_delivery_prices': 1, + 'get_funding_chart_data': 1, + 'get_funding_rate_history': 1, + 'get_funding_rate_value': 1, + 'get_historical_volatility': 1, + 'get_index': 1, + 'get_index_price': 1, + 'get_index_price_names': 1, + 'get_instrument': 1, + 'get_instruments': 1, + 'get_last_settlements_by_currency': 1, + 'get_last_settlements_by_instrument': 1, + 'get_last_trades_by_currency': 1, + 'get_last_trades_by_currency_and_time': 1, + 'get_last_trades_by_instrument': 1, + 'get_last_trades_by_instrument_and_time': 1, + 'get_mark_price_history': 1, + 'get_order_book': 1, + 'get_trade_volumes': 1, + 'get_tradingview_chart_data': 1, + 'get_volatility_index_data': 1, + 'ticker': 1, + }, + }, + 'private': { + 'get': { + # Authentication + 'logout': 1, + # Session management + 'enable_cancel_on_disconnect': 1, + 'disable_cancel_on_disconnect': 1, + 'get_cancel_on_disconnect': 1, + # Subscription management + 'subscribe': 1, + 'unsubscribe': 1, + 'unsubscribe_all': 1, + # Account management + 'change_api_key_name': 1, + 'change_scope_in_api_key': 1, + 'change_subaccount_name': 1, + 'create_api_key': 1, + 'create_subaccount': 1, + 'disable_api_key': 1, + 'disable_tfa_for_subaccount': 1, + 'enable_affiliate_program': 1, + 'enable_api_key': 1, + 'get_access_log': 1, + 'get_account_summary': 1, + 'get_account_summaries': 1, + 'get_affiliate_program_info': 1, + 'get_email_language': 1, + 'get_new_announcements': 1, + 'get_portfolio_margins': 1, + 'get_position': 1, + 'get_positions': 1, + 'get_subaccounts': 1, + 'get_subaccounts_details': 1, + 'get_transaction_log': 1, + 'list_api_keys': 1, + 'remove_api_key': 1, + 'remove_subaccount': 1, + 'reset_api_key': 1, + 'set_announcement_as_read': 1, + 'set_api_key_as_default': 1, + 'set_email_for_subaccount': 1, + 'set_email_language': 1, + 'set_password_for_subaccount': 1, + 'toggle_notifications_from_subaccount': 1, + 'toggle_subaccount_login': 1, + # Block Trade + 'execute_block_trade': 4, + 'get_block_trade': 1, + 'get_last_block_trades_by_currency': 1, + 'invalidate_block_trade_signature': 1, + 'verify_block_trade': 4, + # Trading + 'buy': 4, + 'sell': 4, + 'edit': 4, + 'edit_by_label': 4, + 'cancel': 4, + 'cancel_all': 4, + 'cancel_all_by_currency': 4, + 'cancel_all_by_instrument': 4, + 'cancel_by_label': 4, + 'close_position': 4, + 'get_margins': 1, + 'get_mmp_config': 1, + 'get_open_orders_by_currency': 1, + 'get_open_orders_by_instrument': 1, + 'get_order_history_by_currency': 1, + 'get_order_history_by_instrument': 1, + 'get_order_margin_by_ids': 1, + 'get_order_state': 1, + 'get_stop_order_history': 1, # deprecated + 'get_trigger_order_history': 1, + 'get_user_trades_by_currency': 1, + 'get_user_trades_by_currency_and_time': 1, + 'get_user_trades_by_instrument': 1, + 'get_user_trades_by_instrument_and_time': 1, + 'get_user_trades_by_order': 1, + 'reset_mmp': 1, + 'set_mmp_config': 1, + 'get_settlement_history_by_instrument': 1, + 'get_settlement_history_by_currency': 1, + # Wallet + 'cancel_transfer_by_id': 1, + 'cancel_withdrawal': 1, + 'create_deposit_address': 1, + 'get_current_deposit_address': 1, + 'get_deposits': 1, + 'get_transfers': 1, + 'get_withdrawals': 1, + 'submit_transfer_to_subaccount': 1, + 'submit_transfer_to_user': 1, + 'withdraw': 1, + }, + }, + }, + 'features': { + 'default': { + 'sandbox': True, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, # todo + # todo implement + 'triggerPriceType': { + 'last': True, + 'mark': True, + 'index': True, + }, + 'triggerDirection': False, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': True, + }, + 'hedged': False, + 'selfTradePrevention': False, + 'trailing': True, # todo + 'leverage': False, + 'marketBuyByCost': True, # todo + 'marketBuyRequiresPrice': False, + 'iceberg': True, # todo + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, # todo: revise + 'daysBack': 100000, + 'untilDays': 100000, + 'symbolRequired': True, # todo + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, # todo + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, # todo + }, + 'fetchOrders': None, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 100000, + 'daysBackCanceled': 1, + 'untilDays': 100000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, # todo + }, + 'fetchOHLCV': { + 'limit': 1000, # todo: recheck + }, + }, + 'spot': { + 'extends': 'default', + }, + 'swap': { + 'linear': { + 'extends': 'default', + }, + 'inverse': { + 'extends': 'default', + }, + }, + 'future': { + 'linear': { + 'extends': 'default', + }, + 'inverse': { + 'extends': 'default', + }, + }, + }, + 'exceptions': { + # 0 or absent Success, No error. + '9999': PermissionDenied, # 'api_not_enabled' User didn't enable API for the Account. + '10000': AuthenticationError, # 'authorization_required' Authorization issue, invalid or absent signature etc. + '10001': ExchangeError, # 'error' Some general failure, no public information available. + '10002': InvalidOrder, # 'qty_too_low' Order quantity is too low. + '10003': InvalidOrder, # 'order_overlap' Rejection, order overlap is found and self-trading is not enabled. + '10004': OrderNotFound, # 'order_not_found' Attempt to operate with order that can't be found by specified id. + '10005': InvalidOrder, # 'price_too_low ' Price is too low, defines current limit for the operation. + '10006': InvalidOrder, # 'price_too_low4idx ' Price is too low for current index, defines current bottom limit for the operation. + '10007': InvalidOrder, # 'price_too_high ' Price is too high, defines current up limit for the operation. + '10008': InvalidOrder, # 'price_too_high4idx ' Price is too high for current index, defines current up limit for the operation. + '10009': InsufficientFunds, # 'not_enough_funds' Account has not enough funds for the operation. + '10010': OrderNotFound, # 'already_closed' Attempt of doing something with closed order. + '10011': InvalidOrder, # 'price_not_allowed' This price is not allowed for some reason. + '10012': InvalidOrder, # 'book_closed' Operation for instrument which order book had been closed. + '10013': PermissionDenied, # 'pme_max_total_open_orders ' Total limit of open orders has been exceeded, it is applicable for PME users. + '10014': PermissionDenied, # 'pme_max_future_open_orders ' Limit of count of futures' open orders has been exceeded, it is applicable for PME users. + '10015': PermissionDenied, # 'pme_max_option_open_orders ' Limit of count of options' open orders has been exceeded, it is applicable for PME users. + '10016': PermissionDenied, # 'pme_max_future_open_orders_size ' Limit of size for futures has been exceeded, it is applicable for PME users. + '10017': PermissionDenied, # 'pme_max_option_open_orders_size ' Limit of size for options has been exceeded, it is applicable for PME users. + '10018': PermissionDenied, # 'non_pme_max_future_position_size ' Limit of size for futures has been exceeded, it is applicable for non-PME users. + '10019': PermissionDenied, # 'locked_by_admin' Trading is temporary locked by admin. + '10020': ExchangeError, # 'invalid_or_unsupported_instrument' Instrument name is not valid. + '10021': InvalidOrder, # 'invalid_amount' Amount is not valid. + '10022': InvalidOrder, # 'invalid_quantity' quantity was not recognized valid number(for API v1). + '10023': InvalidOrder, # 'invalid_price' price was not recognized valid number. + '10024': InvalidOrder, # 'invalid_max_show' max_show parameter was not recognized valid number. + '10025': InvalidOrder, # 'invalid_order_id' Order id is missing or its format was not recognized. + '10026': InvalidOrder, # 'price_precision_exceeded' Extra precision of the price is not supported. + '10027': InvalidOrder, # 'non_integer_contract_amount' Futures contract amount was not recognized. + '10028': DDoSProtection, # 'too_many_requests' Allowed request rate has been exceeded. + '10029': OrderNotFound, # 'not_owner_of_order' Attempt to operate with not own order. + '10030': ExchangeError, # 'must_be_websocket_request' REST request where Websocket is expected. + '10031': ExchangeError, # 'invalid_args_for_instrument' Some of arguments are not recognized. + '10032': InvalidOrder, # 'whole_cost_too_low' Total cost is too low. + '10033': NotSupported, # 'not_implemented' Method is not implemented yet. + '10034': InvalidOrder, # 'stop_price_too_high' Stop price is too high. + '10035': InvalidOrder, # 'stop_price_too_low' Stop price is too low. + '10036': InvalidOrder, # 'invalid_max_show_amount' Max Show Amount is not valid. + '10040': ExchangeNotAvailable, # 'retry' Request can't be processed right now and should be retried. + '10041': OnMaintenance, # 'settlement_in_progress' Settlement is in progress. Every day at settlement time for several seconds, the system calculates user profits and updates balances. That time trading is paused for several seconds till the calculation is completed. + '10043': InvalidOrder, # 'price_wrong_tick' Price has to be rounded to a certain tick size. + '10044': InvalidOrder, # 'stop_price_wrong_tick' Stop Price has to be rounded to a certain tick size. + '10045': InvalidOrder, # 'can_not_cancel_liquidation_order' Liquidation order can't be canceled. + '10046': InvalidOrder, # 'can_not_edit_liquidation_order' Liquidation order can't be edited. + '10047': DDoSProtection, # 'matching_engine_queue_full' Reached limit of pending Matching Engine requests for user. + '10048': ExchangeError, # 'not_on_self_server' The requested operation is not available on self server. + '11008': InvalidOrder, # 'already_filled' This request is not allowed in regards to the filled order. + '11029': BadRequest, # 'invalid_arguments' Some invalid input has been detected. + '11030': ExchangeError, # 'other_reject ' Some rejects which are not considered often, more info may be specified in . + '11031': ExchangeError, # 'other_error ' Some errors which are not considered often, more info may be specified in . + '11035': DDoSProtection, # 'no_more_stops ' Allowed amount of stop orders has been exceeded. + '11036': InvalidOrder, # 'invalid_stoppx_for_index_or_last' Invalid StopPx(too high or too low) current index or market. + '11037': BadRequest, # 'outdated_instrument_for_IV_order' Instrument already not available for trading. + '11038': InvalidOrder, # 'no_adv_for_futures' Advanced orders are not available for futures. + '11039': InvalidOrder, # 'no_adv_postonly' Advanced post-only orders are not supported yet. + '11041': InvalidOrder, # 'not_adv_order' Advanced order properties can't be set if the order is not advanced. + '11042': PermissionDenied, # 'permission_denied' Permission for the operation has been denied. + '11043': BadRequest, # 'bad_argument' Bad argument has been passed. + '11044': InvalidOrder, # 'not_open_order' Attempt to do open order operations with the not open order. + '11045': BadRequest, # 'invalid_event' Event name has not been recognized. + '11046': BadRequest, # 'outdated_instrument' At several minutes to instrument expiration, corresponding advanced implied volatility orders are not allowed. + '11047': BadRequest, # 'unsupported_arg_combination' The specified combination of arguments is not supported. + '11048': ExchangeError, # 'wrong_max_show_for_option' Wrong Max Show for options. + '11049': BadRequest, # 'bad_arguments' Several bad arguments have been passed. + '11050': BadRequest, # 'bad_request' Request has not been parsed properly. + '11051': OnMaintenance, # 'system_maintenance' System is under maintenance. + '11052': ExchangeError, # 'subscribe_error_unsubscribed' Subscription error. However, subscription may fail without self error, please check list of subscribed channels returned, channels can be not subscribed due to wrong input or lack of permissions. + '11053': ExchangeError, # 'transfer_not_found' Specified transfer is not found. + '11090': InvalidAddress, # 'invalid_addr' Invalid address. + '11091': InvalidAddress, # 'invalid_transfer_address' Invalid addres for the transfer. + '11092': InvalidAddress, # 'address_already_exist' The address already exists. + '11093': DDoSProtection, # 'max_addr_count_exceeded' Limit of allowed addresses has been reached. + '11094': ExchangeError, # 'internal_server_error' Some unhandled error on server. Please report to admin. The details of the request will help to locate the problem. + '11095': ExchangeError, # 'disabled_deposit_address_creation' Deposit address creation has been disabled by admin. + '11096': ExchangeError, # 'address_belongs_to_user' Withdrawal instead of transfer. + '12000': AuthenticationError, # 'bad_tfa' Wrong TFA code + '12001': DDoSProtection, # 'too_many_subaccounts' Limit of subbacounts is reached. + '12002': ExchangeError, # 'wrong_subaccount_name' The input is not allowed of subaccount. + '12998': AuthenticationError, # 'tfa_over_limit' The number of failed TFA attempts is limited. + '12003': AuthenticationError, # 'login_over_limit' The number of failed login attempts is limited. + '12004': AuthenticationError, # 'registration_over_limit' The number of registration requests is limited. + '12005': AuthenticationError, # 'country_is_banned' The country is banned(possibly via IP check). + '12100': ExchangeError, # 'transfer_not_allowed' Transfer is not allowed. Possible wrong direction or other mistake. + '12999': AuthenticationError, # 'tfa_used' TFA code is correct but it is already used. Please, use next code. + '13000': AuthenticationError, # 'invalid_login' Login name is invalid(not allowed or it contains wrong characters). + '13001': AuthenticationError, # 'account_not_activated' Account must be activated. + '13002': PermissionDenied, # 'account_blocked' Account is blocked by admin. + '13003': AuthenticationError, # 'tfa_required' This action requires TFA authentication. + '13004': AuthenticationError, # 'invalid_credentials' Invalid credentials has been used. + '13005': AuthenticationError, # 'pwd_match_error' Password confirmation error. + '13006': AuthenticationError, # 'security_error' Invalid Security Code. + '13007': AuthenticationError, # 'user_not_found' User's security code has been changed or wrong. + '13008': ExchangeError, # 'request_failed' Request failed because of invalid input or internal failure. + '13009': AuthenticationError, # 'unauthorized' Wrong or expired authorization token or bad signature. For example, please check scope of the token, 'connection' scope can't be reused for other connections. + '13010': BadRequest, # 'value_required' Invalid input, missing value. + '13011': BadRequest, # 'value_too_short' Input is too short. + '13012': PermissionDenied, # 'unavailable_in_subaccount' Subaccount restrictions. + '13013': BadRequest, # 'invalid_phone_number' Unsupported or invalid phone number. + '13014': BadRequest, # 'cannot_send_sms' SMS sending failed -- phone number is wrong. + '13015': BadRequest, # 'invalid_sms_code' Invalid SMS code. + '13016': BadRequest, # 'invalid_input' Invalid input. + '13017': ExchangeError, # 'subscription_failed' Subscription hailed, invalid subscription parameters. + '13018': ExchangeError, # 'invalid_content_type' Invalid content type of the request. + '13019': ExchangeError, # 'orderbook_closed' Closed, expired order book. + '13020': ExchangeError, # 'not_found' Instrument is not found, invalid instrument name. + '13021': PermissionDenied, # 'forbidden' Not enough permissions to execute the request, forbidden. + '13025': ExchangeError, # 'method_switched_off_by_admin' API method temporarily switched off by administrator. + '-32602': BadRequest, # 'Invalid params' see JSON-RPC spec. + '-32601': BadRequest, # 'Method not found' see JSON-RPC spec. + '-32700': BadRequest, # 'Parse error' see JSON-RPC spec. + '-32000': BadRequest, # 'Missing params' see JSON-RPC spec. + '11054': InvalidOrder, # 'post_only_reject' post order would be filled immediately + }, + 'precisionMode': TICK_SIZE, + 'options': { + 'code': 'BTC', + 'fetchBalance': { + 'code': 'BTC', + }, + 'transfer': { + 'method': 'privateGetSubmitTransferToSubaccount', # or 'privateGetSubmitTransferToUser' + }, + }, + }) + + def create_expired_option_market(self, symbol: str): + # support expired option contracts + quote = 'USD' + settle = None + optionParts = symbol.split('-') + symbolBase = symbol.split('/') + base = None + expiry = None + if symbol.find('/') > -1: + base = self.safe_string(symbolBase, 0) + expiry = self.safe_string(optionParts, 1) + if symbol.find('USDC') > -1: + base = base + '_USDC' + else: + base = self.safe_string(optionParts, 0) + expiry = self.convert_market_id_expire_date(self.safe_string(optionParts, 1)) + if symbol.find('USDC') > -1: + quote = 'USDC' + settle = 'USDC' + else: + settle = base + splitBase = base + if base.find('_') > -1: + splitSymbol = base.split('_') + splitBase = self.safe_string(splitSymbol, 0) + strike = self.safe_string(optionParts, 2) + optionType = self.safe_string(optionParts, 3) + datetime = self.convert_expire_date(expiry) + timestamp = self.parse8601(datetime) + return { + 'id': base + '-' + self.convert_expire_date_to_market_id_date(expiry) + '-' + strike + '-' + optionType, + 'symbol': splitBase + '/' + quote + ':' + settle + '-' + expiry + '-' + strike + '-' + optionType, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': base, + 'quoteId': quote, + 'settleId': settle, + 'active': False, + 'type': 'option', + 'linear': None, + 'inverse': None, + 'spot': False, + 'swap': False, + 'future': False, + 'option': True, + 'margin': False, + 'contract': True, + 'contractSize': None, + 'expiry': timestamp, + 'expiryDatetime': datetime, + 'optionType': 'call' if (optionType == 'C') else 'put', + 'strike': self.parse_number(strike), + 'precision': { + 'amount': None, + 'price': None, + }, + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'info': None, + } + + def safe_market(self, marketId: Str = None, market: Market = None, delimiter: Str = None, marketType: Str = None) -> MarketInterface: + isOption = (marketId is not None) and ((marketId.endswith('-C')) or (marketId.endswith('-P'))) + if isOption and not (marketId in self.markets_by_id): + # handle expired option contracts + return self.create_expired_option_market(marketId) + return super(deribit, self).safe_market(marketId, market, delimiter, marketType) + + async def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://docs.deribit.com/#public-get_time + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = await self.publicGetGetTime(params) + # + # { + # "jsonrpc": "2.0", + # "result": 1583922446019, + # "usIn": 1583922446019955, + # "usOut": 1583922446019956, + # "usDiff": 1, + # "testnet": False + # } + # + return self.safe_integer(response, 'result') + + async def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://docs.deribit.com/#public-get_currencies + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = await self.publicGetGetCurrencies(params) + # + # { + # "jsonrpc": "2.0", + # "result": [ + # { + # "currency": "XRP", + # "network_fee": "1.5e-5", + # "min_withdrawal_fee": "0.0001", + # "apr": "0.0", + # "withdrawal_fee": "0.0001", + # "network_currency": "XRP", + # "coin_type": "XRP", + # "withdrawal_priorities": [], + # "min_confirmations": "1", + # "currency_long": "XRP", + # "in_cross_collateral_pool": False + # }, + # ], + # "usIn": "1760110326693923", + # "usOut": "1760110326944891", + # "usDiff": "250968", + # "testnet": False + # } + # + data = self.safe_list(response, 'result', []) + result: dict = {} + for i in range(0, len(data)): + currency = data[i] + currencyId = self.safe_string(currency, 'currency') + code = self.safe_currency_code(currencyId) + result[code] = self.safe_currency_structure({ + 'info': currency, + 'code': code, + 'id': currencyId, + 'name': self.safe_string(currency, 'currency_long'), + 'active': None, + 'deposit': None, + 'withdraw': None, + 'type': 'crypto', + 'fee': self.safe_number(currency, 'withdrawal_fee'), + 'precision': None, + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + 'deposit': { + 'min': None, + 'max': None, + }, + }, + 'networks': None, + }) + return result + + def code_from_options(self, methodName, params={}): + defaultCode = self.safe_value(self.options, 'code', 'BTC') + options = self.safe_value(self.options, methodName, {}) + code = self.safe_value(options, 'code', defaultCode) + return self.safe_value(params, 'code', code) + + async def fetch_status(self, params={}): + """ + the latest known information on the availability of the exchange API + + https://docs.deribit.com/#public-status + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `status structure ` + """ + response = await self.publicGetStatus(params) + # + # { + # "jsonrpc": "2.0", + # "result": { + # "locked": "false" # True, partial, False + # }, + # "usIn": 1650641690226788, + # "usOut": 1650641690226836, + # "usDiff": 48, + # "testnet": False + # } + # + result = self.safe_value(response, 'result') + locked = self.safe_string(result, 'locked') + updateTime = self.safe_integer_product(response, 'usIn', 0.001, self.milliseconds()) + return { + 'status': 'ok' if (locked == 'false') else 'maintenance', + 'updated': updateTime, + 'eta': None, + 'url': None, + 'info': response, + } + + async def fetch_accounts(self, params={}) -> List[Account]: + """ + fetch all the accounts associated with a profile + + https://docs.deribit.com/#private-get_subaccounts + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `account structures ` indexed by the account type + """ + await self.load_markets() + response = await self.privateGetGetSubaccounts(params) + # + # { + # "jsonrpc": "2.0", + # "result": [{ + # "username": "someusername", + # "type": "main", + # "system_name": "someusername", + # "security_keys_enabled": False, + # "security_keys_assignments": [], + # "receive_notifications": False, + # "login_enabled": True, + # "is_password": True, + # "id": "238216", + # "email": "pablo@abcdef.com" + # }, + # { + # "username": "someusername_1", + # "type": "subaccount", + # "system_name": "someusername_1", + # "security_keys_enabled": False, + # "security_keys_assignments": [], + # "receive_notifications": False, + # "login_enabled": False, + # "is_password": False, + # "id": "245499", + # "email": "pablo@abcdef.com" + # } + # ], + # "usIn": "1652736468292006", + # "usOut": "1652736468292377", + # "usDiff": "371", + # "testnet": False + # } + # + result = self.safe_value(response, 'result', []) + return self.parse_accounts(result) + + def parse_account(self, account): + # + # { + # "username": "someusername_1", + # "type": "subaccount", + # "system_name": "someusername_1", + # "security_keys_enabled": False, + # "security_keys_assignments": [], + # "receive_notifications": False, + # "login_enabled": False, + # "is_password": False, + # "id": "245499", + # "email": "pablo@abcdef.com" + # } + # + return { + 'info': account, + 'id': self.safe_string(account, 'id'), + 'type': self.safe_string(account, 'type'), + 'code': None, + } + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for deribit + + https://docs.deribit.com/#public-get_currencies + https://docs.deribit.com/#public-get_instruments + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + instrumentsResponses = [] + result = [] + parsedMarkets: dict = {} + fetchAllMarkets = None + fetchAllMarkets, params = self.handle_option_and_params(params, 'fetchMarkets', 'fetchAllMarkets', True) + if fetchAllMarkets: + instrumentsResponse = await self.publicGetGetInstruments(params) + instrumentsResponses.append(instrumentsResponse) + else: + currenciesResponse = await self.publicGetGetCurrencies(params) + # + # { + # "jsonrpc": "2.0", + # "result": [ + # { + # "withdrawal_priorities": [ + # {value: 0.15, name: "very_low"}, + # {value: 1.5, name: "very_high"}, + # ], + # "withdrawal_fee": 0.0005, + # "min_withdrawal_fee": 0.0005, + # "min_confirmations": 1, + # "fee_precision": 4, + # "currency_long": "Bitcoin", + # "currency": "BTC", + # "coin_type": "BITCOIN" + # } + # ], + # "usIn": 1583761588590479, + # "usOut": 1583761588590544, + # "usDiff": 65, + # "testnet": False + # } + # + currenciesResult = self.safe_value(currenciesResponse, 'result', []) + for i in range(0, len(currenciesResult)): + currencyId = self.safe_string(currenciesResult[i], 'currency') + request: dict = { + 'currency': currencyId, + } + instrumentsResponse = await self.publicGetGetInstruments(self.extend(request, params)) + # + # { + # "jsonrpc":"2.0", + # "result":[ + # { + # "tick_size":0.0005, + # "taker_commission":0.0003, + # "strike":52000.0, + # "settlement_period":"month", + # "settlement_currency":"BTC", + # "quote_currency":"BTC", + # "option_type":"put", # put, call + # "min_trade_amount":0.1, + # "maker_commission":0.0003, + # "kind":"option", + # "is_active":true, + # "instrument_name":"BTC-24JUN22-52000-P", + # "expiration_timestamp":1656057600000, + # "creation_timestamp":1648199543000, + # "counter_currency":"USD", + # "contract_size":1.0, + # "block_trade_commission":0.0003, + # "base_currency":"BTC" + # }, + # { + # "tick_size":0.5, + # "taker_commission":0.0005, + # "settlement_period":"month", # month, week + # "settlement_currency":"BTC", + # "quote_currency":"USD", + # "min_trade_amount":10.0, + # "max_liquidation_commission":0.0075, + # "max_leverage":50, + # "maker_commission":0.0, + # "kind":"future", + # "is_active":true, + # "instrument_name":"BTC-27MAY22", + # "future_type":"reversed", + # "expiration_timestamp":1653638400000, + # "creation_timestamp":1648195209000, + # "counter_currency":"USD", + # "contract_size":10.0, + # "block_trade_commission":0.0001, + # "base_currency":"BTC" + # }, + # { + # "tick_size":0.5, + # "taker_commission":0.0005, + # "settlement_period":"perpetual", + # "settlement_currency":"BTC", + # "quote_currency":"USD", + # "min_trade_amount":10.0, + # "max_liquidation_commission":0.0075, + # "max_leverage":50, + # "maker_commission":0.0, + # "kind":"future", + # "is_active":true, + # "instrument_name":"BTC-PERPETUAL", + # "future_type":"reversed", + # "expiration_timestamp":32503708800000, + # "creation_timestamp":1534242287000, + # "counter_currency":"USD", + # "contract_size":10.0, + # "block_trade_commission":0.0001, + # "base_currency":"BTC" + # }, + # ], + # "usIn":1648691472831791, + # "usOut":1648691472831896, + # "usDiff":105, + # "testnet":false + # } + # + instrumentsResponses.append(instrumentsResponse) + for i in range(0, len(instrumentsResponses)): + instrumentsResult = self.safe_value(instrumentsResponses[i], 'result', []) + for k in range(0, len(instrumentsResult)): + market = instrumentsResult[k] + kind = self.safe_string(market, 'kind') + isSpot = (kind == 'spot') + id = self.safe_string(market, 'instrument_name') + baseId = self.safe_string(market, 'base_currency') + quoteId = self.safe_string(market, 'counter_currency') + settleId = self.safe_string(market, 'settlement_currency') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + settle = self.safe_currency_code(settleId) + settlementPeriod = self.safe_value(market, 'settlement_period') + swap = (settlementPeriod == 'perpetual') + future = not swap and (kind.find('future') >= 0) + option = (kind.find('option') >= 0) + isComboMarket = kind.find('combo') >= 0 + expiry = self.safe_integer(market, 'expiration_timestamp') + strike = None + optionType = None + symbol = id + type = 'swap' + if future: + type = 'future' + elif option: + type = 'option' + elif isSpot: + type = 'spot' + inverse = None + linear = None + if isSpot: + symbol = base + '/' + quote + elif not isComboMarket: + symbol = base + '/' + quote + ':' + settle + if option or future: + symbol = symbol + '-' + self.yymmdd(expiry, '') + if option: + strike = self.safe_number(market, 'strike') + optionType = self.safe_string(market, 'option_type') + letter = 'C' if (optionType == 'call') else 'P' + symbol = symbol + '-' + self.number_to_string(strike) + '-' + letter + inverse = (quote != settle) + linear = (settle == quote) + parsedMarketValue = self.safe_value(parsedMarkets, symbol) + if parsedMarketValue: + continue + parsedMarkets[symbol] = True + minTradeAmount = self.safe_number(market, 'min_trade_amount') + tickSize = self.safe_number(market, 'tick_size') + result.append({ + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': type, + 'spot': isSpot, + 'margin': False, + 'swap': swap, + 'future': future, + 'option': option, + 'active': self.safe_value(market, 'is_active'), + 'contract': not isSpot, + 'linear': linear, + 'inverse': inverse, + 'taker': self.safe_number(market, 'taker_commission'), + 'maker': self.safe_number(market, 'maker_commission'), + 'contractSize': self.safe_number(market, 'contract_size'), + 'expiry': expiry, + 'expiryDatetime': self.iso8601(expiry), + 'strike': strike, + 'optionType': optionType, + 'precision': { + 'amount': minTradeAmount, + 'price': tickSize, + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': minTradeAmount, + 'max': None, + }, + 'price': { + 'min': tickSize, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': self.safe_integer(market, 'creation_timestamp'), + 'info': market, + }) + return result + + def parse_balance(self, balance) -> Balances: + result: dict = { + 'info': balance, + } + summaries = [] + if 'summaries' in balance: + summaries = self.safe_list(balance, 'summaries') + else: + summaries = [balance] + for i in range(0, len(summaries)): + data = summaries[i] + currencyId = self.safe_string(data, 'currency') + currencyCode = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(data, 'available_funds') + account['used'] = self.safe_string(data, 'maintenance_margin') + account['total'] = self.safe_string(data, 'equity') + result[currencyCode] = account + return self.safe_balance(result) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://docs.deribit.com/#private-get_account_summary + https://docs.deribit.com/#private-get_account_summaries + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.code]: unified currency code of the currency for the balance, if defined 'privateGetGetAccountSummary' will be used, otherwise 'privateGetGetAccountSummaries' will be used + :returns dict: a `balance structure ` + """ + await self.load_markets() + code = self.safe_string(params, 'code') + params = self.omit(params, 'code') + request: dict = { + } + if code is not None: + request['currency'] = self.currency_id(code) + response = None + if code is None: + response = await self.privateGetGetAccountSummaries(params) + else: + response = await self.privateGetGetAccountSummary(self.extend(request, params)) + # + # { + # "jsonrpc": "2.0", + # "result": { + # "total_pl": 0, + # "session_upl": 0, + # "session_rpl": 0, + # "session_funding": 0, + # "portfolio_margining_enabled": False, + # "options_vega": 0, + # "options_theta": 0, + # "options_session_upl": 0, + # "options_session_rpl": 0, + # "options_pl": 0, + # "options_gamma": 0, + # "options_delta": 0, + # "margin_balance": 0.00062359, + # "maintenance_margin": 0, + # "limits": { + # "non_matching_engine_burst": 300, + # "non_matching_engine": 200, + # "matching_engine_burst": 20, + # "matching_engine": 2 + # }, + # "initial_margin": 0, + # "futures_session_upl": 0, + # "futures_session_rpl": 0, + # "futures_pl": 0, + # "equity": 0.00062359, + # "deposit_address": "13tUtNsJSZa1F5GeCmwBywVrymHpZispzw", + # "delta_total": 0, + # "currency": "BTC", + # "balance": 0.00062359, + # "available_withdrawal_funds": 0.00062359, + # "available_funds": 0.00062359 + # }, + # "usIn": 1583775838115975, + # "usOut": 1583775838116520, + # "usDiff": 545, + # "testnet": False + # } + # + result = self.safe_dict(response, 'result', {}) + return self.parse_balance(result) + + async def create_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + create a currency deposit address + + https://docs.deribit.com/#private-create_deposit_address + + :param str code: unified currency code of the currency for the deposit address + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + } + response = await self.privateGetCreateDepositAddress(self.extend(request, params)) + # + # { + # "jsonrpc": "2.0", + # "id": 7538, + # "result": { + # "address": "2N8udZGBc1hLRCFsU9kGwMPpmYUwMFTuCwB", + # "creation_timestamp": 1550575165170, + # "currency": "BTC", + # "type": "deposit" + # } + # } + # + result = self.safe_value(response, 'result', {}) + address = self.safe_string(result, 'address') + self.check_address(address) + return { + 'currency': code, + 'address': address, + 'tag': None, + 'network': None, + 'info': response, + } + + async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://docs.deribit.com/#private-get_current_deposit_address + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + } + response = await self.privateGetGetCurrentDepositAddress(self.extend(request, params)) + # + # { + # "jsonrpc": "2.0", + # "result": { + # "type": "deposit", + # "status": "ready", + # "requires_confirmation": True, + # "currency": "BTC", + # "creation_timestamp": 1514694684651, + # "address": "13tUtNsJSZa1F5GeCmwBywVrymHpZispzw" + # }, + # "usIn": 1583785137274288, + # "usOut": 1583785137274454, + # "usDiff": 166, + # "testnet": False + # } + # + result = self.safe_value(response, 'result', {}) + address = self.safe_string(result, 'address') + self.check_address(address) + return { + 'info': response, + 'currency': code, + 'network': None, + 'address': address, + 'tag': None, + } + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # fetchTicker /public/ticker + # + # { + # "timestamp": 1583778859480, + # "stats": {volume: 60627.57263769, low: 7631.5, high: 8311.5}, + # "state": "open", + # "settlement_price": 7903.21, + # "open_interest": 111543850, + # "min_price": 7634, + # "max_price": 7866.51, + # "mark_price": 7750.02, + # "last_price": 7750.5, + # "instrument_name": "BTC-PERPETUAL", + # "index_price": 7748.01, + # "funding_8h": 0.0000026, + # "current_funding": 0, + # "best_bid_price": 7750, + # "best_bid_amount": 19470, + # "best_ask_price": 7750.5, + # "best_ask_amount": 343280 + # } + # + # fetchTicker /public/get_book_summary_by_instrument + # fetchTickers /public/get_book_summary_by_currency + # + # { + # "volume": 124.1, + # "underlying_price": 7856.445926872601, + # "underlying_index": "SYN.BTC-10MAR20", + # "quote_currency": "USD", + # "open_interest": 121.8, + # "mid_price": 0.01975, + # "mark_price": 0.01984559, + # "low": 0.0095, + # "last": 0.0205, + # "interest_rate": 0, + # "instrument_name": "BTC-10MAR20-7750-C", + # "high": 0.0295, + # "estimated_delivery_price": 7856.29, + # "creation_timestamp": 1583783678366, + # "bid_price": 0.0185, + # "base_currency": "BTC", + # "ask_price": 0.021 + # }, + # + timestamp = self.safe_integer_2(ticker, 'timestamp', 'creation_timestamp') + marketId = self.safe_string(ticker, 'instrument_name') + symbol = self.safe_symbol(marketId, market) + last = self.safe_string_2(ticker, 'last_price', 'last') + stats = self.safe_value(ticker, 'stats', ticker) + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string_2(stats, 'high', 'max_price'), + 'low': self.safe_string_2(stats, 'low', 'min_price'), + 'bid': self.safe_string_2(ticker, 'best_bid_price', 'bid_price'), + 'bidVolume': self.safe_string(ticker, 'best_bid_amount'), + 'ask': self.safe_string_2(ticker, 'best_ask_price', 'ask_price'), + 'askVolume': self.safe_string(ticker, 'best_ask_amount'), + 'vwap': None, + 'open': None, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': None, + 'quoteVolume': self.safe_string(stats, 'volume'), + 'markPrice': self.safe_string(ticker, 'mark_price'), + 'indexPrice': self.safe_string(ticker, 'index_price'), + 'info': ticker, + }, market) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://docs.deribit.com/#public-ticker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'instrument_name': market['id'], + } + response = await self.publicGetTicker(self.extend(request, params)) + # + # { + # "jsonrpc": "2.0", + # "result": { + # "timestamp": 1583778859480, + # "stats": {volume: 60627.57263769, low: 7631.5, high: 8311.5}, + # "state": "open", + # "settlement_price": 7903.21, + # "open_interest": 111543850, + # "min_price": 7634, + # "max_price": 7866.51, + # "mark_price": 7750.02, + # "last_price": 7750.5, + # "instrument_name": "BTC-PERPETUAL", + # "index_price": 7748.01, + # "funding_8h": 0.0000026, + # "current_funding": 0, + # "best_bid_price": 7750, + # "best_bid_amount": 19470, + # "best_ask_price": 7750.5, + # "best_ask_amount": 343280 + # }, + # "usIn": 1583778859483941, + # "usOut": 1583778859484075, + # "usDiff": 134, + # "testnet": False + # } + # + result = self.safe_dict(response, 'result') + return self.parse_ticker(result, market) + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://docs.deribit.com/#public-get_book_summary_by_currency + + :param str[] [symbols]: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.code]: *required* the currency code to fetch the tickers for, eg. 'BTC', 'ETH' + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + code = self.safe_string_2(params, 'code', 'currency') + type = None + params = self.omit(params, ['code']) + if symbols is not None: + for i in range(0, len(symbols)): + market = self.market(symbols[i]) + if code is not None and code != market['base']: + raise BadRequest(self.id + ' fetchTickers the base currency must be the same for all symbols, self endpoint only supports one base currency at a time. Read more about it here: https://docs.deribit.com/#public-get_book_summary_by_currency') + if code is None: + code = market['base'] + type = market['type'] + if code is None: + raise ArgumentsRequired(self.id + ' fetchTickers requires a currency/code(eg: BTC/ETH/USDT) parameter to fetch tickers for') + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + } + if type is not None: + requestType = None + if type == 'spot': + requestType = 'spot' + elif type == 'future' or (type == 'contract'): + requestType = 'future' + elif type == 'option': + requestType = 'option' + if requestType is not None: + request['kind'] = requestType + response = await self.publicGetGetBookSummaryByCurrency(self.extend(request, params)) + # + # { + # "jsonrpc": "2.0", + # "result": [ + # { + # "volume": 124.1, + # "underlying_price": 7856.445926872601, + # "underlying_index": "SYN.BTC-10MAR20", + # "quote_currency": "USD", + # "open_interest": 121.8, + # "mid_price": 0.01975, + # "mark_price": 0.01984559, + # "low": 0.0095, + # "last": 0.0205, + # "interest_rate": 0, + # "instrument_name": "BTC-10MAR20-7750-C", + # "high": 0.0295, + # "estimated_delivery_price": 7856.29, + # "creation_timestamp": 1583783678366, + # "bid_price": 0.0185, + # "base_currency": "BTC", + # "ask_price": 0.021 + # }, + # ], + # "usIn": 1583783678361966, + # "usOut": 1583783678372069, + # "usDiff": 10103, + # "testnet": False + # } + # + result = self.safe_list(response, 'result', []) + tickers: dict = {} + for i in range(0, len(result)): + ticker = self.parse_ticker(result[i]) + symbol = ticker['symbol'] + tickers[symbol] = ticker + return self.filter_by_array_tickers(tickers, 'symbol', symbols) + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://docs.deribit.com/#public-get_tradingview_chart_data + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: whether to paginate the results, set to False by default + :param int [params.until]: the latest time in ms to fetch ohlcv for + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate') + if paginate: + return await self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params, 5000) + market = self.market(symbol) + request: dict = { + 'instrument_name': market['id'], + 'resolution': self.safe_string(self.timeframes, timeframe, timeframe), + } + duration = self.parse_timeframe(timeframe) + now = self.milliseconds() + if since is None: + if limit is None: + limit = 1000 # at max, it provides 5000 bars, but we set generous default here + request['start_timestamp'] = now - (limit - 1) * duration * 1000 + request['end_timestamp'] = now + else: + since = max(since - 1, 0) + request['start_timestamp'] = since + if limit is None: + request['end_timestamp'] = now + else: + request['end_timestamp'] = self.sum(since, limit * duration * 1000) + until = self.safe_integer(params, 'until') + if until is not None: + params = self.omit(params, 'until') + request['end_timestamp'] = until + response = await self.publicGetGetTradingviewChartData(self.extend(request, params)) + # + # { + # "jsonrpc": "2.0", + # "result": { + # "volume": [3.6680847969999992, 22.682721123, 3.011587939, 0], + # "ticks": [1583916960000, 1583917020000, 1583917080000, 1583917140000], + # "status": "ok", + # "open": [7834, 7839, 7833.5, 7833], + # "low": [7834, 7833.5, 7832.5, 7833], + # "high": [7839.5, 7839, 7833.5, 7833], + # "cost": [28740, 177740, 23590, 0], + # "close": [7839.5, 7833.5, 7833, 7833] + # }, + # "usIn": 1583917166709801, + # "usOut": 1583917166710175, + # "usDiff": 374, + # "testnet": False + # } + # + result = self.safe_value(response, 'result', {}) + ohlcvs = self.convert_trading_view_to_ohlcv(result, 'ticks', 'open', 'high', 'low', 'close', 'volume', True) + return self.parse_ohlcvs(ohlcvs, market, timeframe, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(public) + # + # { + # "trade_seq":132564271, + # "trade_id":"195402220", + # "timestamp":1639684927932, + # "tick_direction":0, + # "price":47946.5, + # "mark_price":47944.13, + # "instrument_name":"BTC-PERPETUAL", + # "index_price":47925.45, + # "direction":"buy", + # "amount":580.0 + # } + # + # + # fetchMyTrades, fetchOrderTrades(private) + # + # { + # "trade_seq": 3, + # "trade_id": "ETH-34066", + # "timestamp": 1550219814585, + # "tick_direction": 1, + # "state": "open", + # "self_trade": False, + # "reduce_only": False, + # "price": 0.04, + # "post_only": False, + # "order_type": "limit", + # "order_id": "ETH-334607", + # "matching_id": null, + # "liquidity": "M", + # "iv": 56.83, + # "instrument_name": "ETH-22FEB19-120-C", + # "index_price": 121.37, + # "fee_currency": "ETH", + # "fee": 0.0011, + # "direction": "buy", + # "amount": 11 + # } + # + id = self.safe_string(trade, 'trade_id') + marketId = self.safe_string(trade, 'instrument_name') + symbol = self.safe_symbol(marketId, market) + timestamp = self.safe_integer(trade, 'timestamp') + side = self.safe_string(trade, 'direction') + priceString = self.safe_string(trade, 'price') + market = self.safe_market(marketId, market) + # Amount for inverse perpetual and futures is in USD which in ccxt is the cost + # For options amount and linear is in corresponding cryptocurrency contracts, e.g., BTC or ETH + amount = self.safe_string(trade, 'amount') + cost = Precise.string_mul(amount, priceString) + if market['inverse']: + cost = Precise.string_div(amount, priceString) + liquidity = self.safe_string(trade, 'liquidity') + takerOrMaker = None + if liquidity is not None: + # M = maker, T = taker, MT = both + takerOrMaker = 'maker' if (liquidity == 'M') else 'taker' + feeCostString = self.safe_string(trade, 'fee') + fee = None + if feeCostString is not None: + feeCurrencyId = self.safe_string(trade, 'fee_currency') + feeCurrencyCode = self.safe_currency_code(feeCurrencyId) + fee = { + 'cost': feeCostString, + 'currency': feeCurrencyCode, + } + return self.safe_trade({ + 'id': id, + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'order': self.safe_string(trade, 'order_id'), + 'type': self.safe_string(trade, 'order_type'), + 'side': side, + 'takerOrMaker': takerOrMaker, + 'price': priceString, + 'amount': amount, + 'cost': cost, + 'fee': fee, + }, market) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + + https://docs.deribit.com/#public-get_last_trades_by_instrument + https://docs.deribit.com/#public-get_last_trades_by_instrument_and_time + + get the list of most recent trades for a particular symbol. + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch trades for + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'instrument_name': market['id'], + 'include_old': True, + } + if since is not None: + request['start_timestamp'] = since + if limit is not None: + request['count'] = min(limit, 1000) # default 10 + until = self.safe_integer_2(params, 'until', 'end_timestamp') + if until is not None: + params = self.omit(params, ['until']) + request['end_timestamp'] = until + response = None + if (since is None) and not ('end_timestamp' in request): + response = await self.publicGetGetLastTradesByInstrument(self.extend(request, params)) + else: + response = await self.publicGetGetLastTradesByInstrumentAndTime(self.extend(request, params)) + # + # { + # "jsonrpc":"2.0", + # "result": { + # "trades": [ + # { + # "trade_seq":132564271, + # "trade_id":"195402220", + # "timestamp":1639684927932, + # "tick_direction":0, + # "price":47946.5, + # "mark_price":47944.13, + # "instrument_name":"BTC-PERPETUAL", + # "index_price":47925.45, + # "direction":"buy","amount":580.0 + # } + # ], + # "has_more":true + # }, + # "usIn":1639684931934671, + # "usOut":1639684931935337, + # "usDiff":666, + # "testnet":false + # } + # + result = self.safe_value(response, 'result', {}) + trades = self.safe_list(result, 'trades', []) + return self.parse_trades(trades, market, since, limit) + + async def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://docs.deribit.com/#private-get_account_summary + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + await self.load_markets() + code = self.code_from_options('fetchTradingFees', params) + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + 'extended': True, + } + response = await self.privateGetGetAccountSummary(self.extend(request, params)) + # + # { + # "jsonrpc": "2.0", + # "result": { + # "total_pl": 0, + # "session_upl": 0, + # "session_rpl": 0, + # "session_funding": 0, + # "portfolio_margining_enabled": False, + # "options_vega": 0, + # "options_theta": 0, + # "options_session_upl": 0, + # "options_session_rpl": 0, + # "options_pl": 0, + # "options_gamma": 0, + # "options_delta": 0, + # "margin_balance": 0.00062359, + # "maintenance_margin": 0, + # "limits": { + # "non_matching_engine_burst": 300, + # "non_matching_engine": 200, + # "matching_engine_burst": 20, + # "matching_engine": 2 + # }, + # "initial_margin": 0, + # "futures_session_upl": 0, + # "futures_session_rpl": 0, + # "futures_pl": 0, + # "equity": 0.00062359, + # "deposit_address": "13tUtNsJSZa1F5GeCmwBywVrymHpZispzw", + # "delta_total": 0, + # "currency": "BTC", + # "balance": 0.00062359, + # "available_withdrawal_funds": 0.00062359, + # "available_funds": 0.00062359, + # "fees": [ + # "currency": '', + # "instrument_type": "perpetual", + # "fee_type": "relative", + # "maker_fee": 0, + # "taker_fee": 0, + # ], + # }, + # "usIn": 1583775838115975, + # "usOut": 1583775838116520, + # "usDiff": 545, + # "testnet": False + # } + # + result = self.safe_value(response, 'result', {}) + fees = self.safe_value(result, 'fees', []) + perpetualFee: dict = {} + futureFee: dict = {} + optionFee: dict = {} + for i in range(0, len(fees)): + fee = fees[i] + instrumentType = self.safe_string(fee, 'instrument_type') + if instrumentType == 'future': + futureFee = { + 'info': fee, + 'maker': self.safe_number(fee, 'maker_fee'), + 'taker': self.safe_number(fee, 'taker_fee'), + } + elif instrumentType == 'perpetual': + perpetualFee = { + 'info': fee, + 'maker': self.safe_number(fee, 'maker_fee'), + 'taker': self.safe_number(fee, 'taker_fee'), + } + elif instrumentType == 'option': + optionFee = { + 'info': fee, + 'maker': self.safe_number(fee, 'maker_fee'), + 'taker': self.safe_number(fee, 'taker_fee'), + } + parsedFees: dict = {} + for i in range(0, len(self.symbols)): + symbol = self.symbols[i] + market = self.market(symbol) + fee: dict = { + 'info': market, + 'symbol': symbol, + 'percentage': True, + 'tierBased': True, + 'maker': market['maker'], + 'taker': market['taker'], + } + if market['swap']: + fee = self.extend(fee, perpetualFee) + elif market['future']: + fee = self.extend(fee, futureFee) + elif market['option']: + fee = self.extend(fee, optionFee) + parsedFees[symbol] = fee + return parsedFees + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://docs.deribit.com/#public-get_order_book + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'instrument_name': market['id'], + } + if limit is not None: + request['depth'] = limit + response = await self.publicGetGetOrderBook(self.extend(request, params)) + # + # { + # "jsonrpc": "2.0", + # "result": { + # "timestamp": 1583781354740, + # "stats": {volume: 61249.66735634, low: 7631.5, high: 8311.5}, + # "state": "open", + # "settlement_price": 7903.21, + # "open_interest": 111536690, + # "min_price": 7695.13, + # "max_price": 7929.49, + # "mark_price": 7813.06, + # "last_price": 7814.5, + # "instrument_name": "BTC-PERPETUAL", + # "index_price": 7810.12, + # "funding_8h": 0.0000031, + # "current_funding": 0, + # "change_id": 17538025952, + # "bids": [ + # [7814, 351820], + # [7813.5, 207490], + # [7813, 32160], + # ], + # "best_bid_price": 7814, + # "best_bid_amount": 351820, + # "best_ask_price": 7814.5, + # "best_ask_amount": 11880, + # "asks": [ + # [7814.5, 11880], + # [7815, 18100], + # [7815.5, 2640], + # ], + # }, + # "usIn": 1583781354745804, + # "usOut": 1583781354745932, + # "usDiff": 128, + # "testnet": False + # } + # + result = self.safe_value(response, 'result', {}) + timestamp = self.safe_integer(result, 'timestamp') + nonce = self.safe_integer(result, 'change_id') + orderbook = self.parse_order_book(result, market['symbol'], timestamp) + orderbook['nonce'] = nonce + return orderbook + + def parse_order_status(self, status: Str): + statuses: dict = { + 'open': 'open', + 'cancelled': 'canceled', + 'filled': 'closed', + 'rejected': 'rejected', + 'untriggered': 'open', + } + return self.safe_string(statuses, status, status) + + def parse_time_in_force(self, timeInForce: Str): + timeInForces: dict = { + 'good_til_cancelled': 'GTC', + 'fill_or_kill': 'FOK', + 'immediate_or_cancel': 'IOC', + } + return self.safe_string(timeInForces, timeInForce, timeInForce) + + def parse_order_type(self, orderType): + orderTypes: dict = { + 'stop_limit': 'limit', + 'take_limit': 'limit', + 'stop_market': 'market', + 'take_market': 'market', + } + return self.safe_string(orderTypes, orderType, orderType) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder + # + # { + # "time_in_force": "good_til_cancelled", + # "reduce_only": False, + # "profit_loss": 0, + # "price": "market_price", + # "post_only": False, + # "order_type": "market", + # "order_state": "filled", + # "order_id": "ETH-349249", + # "max_show": 40, + # "last_update_timestamp": 1550657341322, + # "label": "market0000234", + # "is_liquidation": False, + # "instrument_name": "ETH-PERPETUAL", + # "filled_amount": 40, + # "direction": "buy", + # "creation_timestamp": 1550657341322, + # "commission": 0.000139, + # "average_price": 143.81, + # "api": True, + # "amount": 40, + # "trades": [], # injected by createOrder + # } + # + marketId = self.safe_string(order, 'instrument_name') + market = self.safe_market(marketId, market) + timestamp = self.safe_integer(order, 'creation_timestamp') + lastUpdate = self.safe_integer(order, 'last_update_timestamp') + id = self.safe_string(order, 'order_id') + priceString = self.safe_string(order, 'price') + if priceString == 'market_price': + priceString = None + averageString = self.safe_string(order, 'average_price') + # Inverse contracts amount is in USD which in ccxt is the cost + # For options and Linear contracts amount is in corresponding cryptocurrency, e.g., BTC or ETH + filledString = self.safe_string(order, 'filled_amount') + amount = self.safe_string(order, 'amount') + cost = Precise.string_mul(filledString, averageString) + if self.safe_bool(market, 'inverse'): + if averageString != '0': + cost = Precise.string_div(amount, averageString) + lastTradeTimestamp = None + if filledString is not None: + isFilledPositive = Precise.string_gt(filledString, '0') + if isFilledPositive: + lastTradeTimestamp = lastUpdate + status = self.parse_order_status(self.safe_string(order, 'order_state')) + side = self.safe_string_lower(order, 'direction') + feeCostString = self.safe_string(order, 'commission') + fee = None + if feeCostString is not None: + feeCostString = Precise.string_abs(feeCostString) + fee = { + 'cost': feeCostString, + 'currency': market['base'], + } + rawType = self.safe_string(order, 'order_type') + type = self.parse_order_type(rawType) + # injected in createOrder + trades = self.safe_value(order, 'trades') + timeInForce = self.parse_time_in_force(self.safe_string(order, 'time_in_force')) + postOnly = self.safe_value(order, 'post_only') + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': lastTradeTimestamp, + 'symbol': market['symbol'], + 'type': type, + 'timeInForce': timeInForce, + 'postOnly': postOnly, + 'side': side, + 'price': priceString, + 'triggerPrice': self.safe_value(order, 'stop_price'), + 'amount': amount, + 'cost': cost, + 'average': averageString, + 'filled': filledString, + 'remaining': None, + 'status': status, + 'fee': fee, + 'trades': trades, + }, market) + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://docs.deribit.com/#private-get_order_state + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + request: dict = { + 'order_id': id, + } + market = None + if symbol is not None: + market = self.market(symbol) + response = await self.privateGetGetOrderState(self.extend(request, params)) + # + # { + # "jsonrpc": "2.0", + # "id": 4316, + # "result": { + # "time_in_force": "good_til_cancelled", + # "reduce_only": False, + # "profit_loss": 0.051134, + # "price": 118.94, + # "post_only": False, + # "order_type": "limit", + # "order_state": "filled", + # "order_id": "ETH-331562", + # "max_show": 37, + # "last_update_timestamp": 1550219810944, + # "label": "", + # "is_liquidation": False, + # "instrument_name": "ETH-PERPETUAL", + # "filled_amount": 37, + # "direction": "sell", + # "creation_timestamp": 1550219749176, + # "commission": 0.000031, + # "average_price": 118.94, + # "api": False, + # "amount": 37 + # } + # } + # + result = self.safe_dict(response, 'result') + return self.parse_order(result, market) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://docs.deribit.com/#private-buy + https://docs.deribit.com/#private-sell + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency. For perpetual and inverse futures the amount is in USD units. For options it is in the underlying assets base currency. + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.trigger]: the trigger type 'index_price', 'mark_price', or 'last_price', default is 'last_price' + :param float [params.trailingAmount]: the quote amount to trail away from the current market price + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'instrument_name': market['id'], + 'amount': self.amount_to_precision(symbol, amount), + 'type': type, # limit, stop_limit, market, stop_market, default is limit + # 'label': 'string', # user-defined label for the order(maximum 64 characters) + # 'price': self.price_to_precision(symbol, 123.45), # only for limit and stop_limit orders + # 'time_in_force' : 'good_til_cancelled', # fill_or_kill, immediate_or_cancel + # 'max_show': 123.45, # max amount within an order to be shown to other customers, 0 for invisible order + # 'post_only': False, # if the new price would cause the order to be filled immediately(as taker), the price will be changed to be just below the spread. + # 'reject_post_only': False, # if True the order is put to order book unmodified or request is rejected + # 'reduce_only': False, # if True, the order is intended to only reduce a current position + # 'stop_price': False, # stop price, required for stop_limit orders + # 'trigger': 'index_price', # mark_price, last_price, required for stop_limit orders + # 'advanced': 'usd', # 'implv', advanced option order type, options only + } + trigger = self.safe_string(params, 'trigger', 'last_price') + timeInForce = self.safe_string_upper(params, 'timeInForce') + reduceOnly = self.safe_value_2(params, 'reduceOnly', 'reduce_only') + # only stop loss sell orders are allowed when price crossed from above + stopLossPrice = self.safe_value(params, 'stopLossPrice') + # only take profit buy orders are allowed when price crossed from below + takeProfitPrice = self.safe_value(params, 'takeProfitPrice') + trailingAmount = self.safe_string_2(params, 'trailingAmount', 'trigger_offset') + isTrailingAmountOrder = trailingAmount is not None + isStopLimit = type == 'stop_limit' + isStopMarket = type == 'stop_market' + isTakeLimit = type == 'take_limit' + isTakeMarket = type == 'take_market' + isStopLossOrder = isStopLimit or isStopMarket or (stopLossPrice is not None) + isTakeProfitOrder = isTakeLimit or isTakeMarket or (takeProfitPrice is not None) + if isStopLossOrder and isTakeProfitOrder: + raise InvalidOrder(self.id + ' createOrder() only allows one of stopLossPrice or takeProfitPrice to be specified') + isStopOrder = isStopLossOrder or isTakeProfitOrder + isLimitOrder = (type == 'limit') or isStopLimit or isTakeLimit + isMarketOrder = (type == 'market') or isStopMarket or isTakeMarket + exchangeSpecificPostOnly = self.safe_value(params, 'post_only') + postOnly = self.is_post_only(isMarketOrder, exchangeSpecificPostOnly, params) + if isLimitOrder: + request['type'] = 'limit' + request['price'] = self.price_to_precision(symbol, price) + else: + request['type'] = 'market' + if isTrailingAmountOrder: + request['trigger'] = trigger + request['type'] = 'trailing_stop' + request['trigger_offset'] = self.parse_to_numeric(trailingAmount) + elif isStopOrder: + triggerPrice = stopLossPrice if (stopLossPrice is not None) else takeProfitPrice + request['trigger_price'] = self.price_to_precision(symbol, triggerPrice) + request['trigger'] = trigger + if isStopLossOrder: + if isMarketOrder: + # stop_market(sell only) + request['type'] = 'stop_market' + else: + # stop_limit(sell only) + request['type'] = 'stop_limit' + else: + if isMarketOrder: + # take_market(buy only) + request['type'] = 'take_market' + else: + # take_limit(buy only) + request['type'] = 'take_limit' + if reduceOnly: + request['reduce_only'] = True + if postOnly: + request['post_only'] = True + request['reject_post_only'] = True + if timeInForce is not None: + if timeInForce == 'GTC': + request['time_in_force'] = 'good_til_cancelled' + if timeInForce == 'IOC': + request['time_in_force'] = 'immediate_or_cancel' + if timeInForce == 'FOK': + request['time_in_force'] = 'fill_or_kill' + params = self.omit(params, ['timeInForce', 'stopLossPrice', 'takeProfitPrice', 'postOnly', 'reduceOnly', 'trailingAmount']) + response = None + if self.capitalize(side) == 'Buy': + response = await self.privateGetBuy(self.extend(request, params)) + else: + response = await self.privateGetSell(self.extend(request, params)) + # + # { + # "jsonrpc": "2.0", + # "id": 5275, + # "result": { + # "trades": [ + # { + # "trade_seq": 14151, + # "trade_id": "ETH-37435", + # "timestamp": 1550657341322, + # "tick_direction": 2, + # "state": "closed", + # "self_trade": False, + # "price": 143.81, + # "order_type": "market", + # "order_id": "ETH-349249", + # "matching_id": null, + # "liquidity": "T", + # "label": "market0000234", + # "instrument_name": "ETH-PERPETUAL", + # "index_price": 143.73, + # "fee_currency": "ETH", + # "fee": 0.000139, + # "direction": "buy", + # "amount": 40 + # } + # ], + # "order": { + # "time_in_force": "good_til_cancelled", + # "reduce_only": False, + # "profit_loss": 0, + # "price": "market_price", + # "post_only": False, + # "order_type": "market", + # "order_state": "filled", + # "order_id": "ETH-349249", + # "max_show": 40, + # "last_update_timestamp": 1550657341322, + # "label": "market0000234", + # "is_liquidation": False, + # "instrument_name": "ETH-PERPETUAL", + # "filled_amount": 40, + # "direction": "buy", + # "creation_timestamp": 1550657341322, + # "commission": 0.000139, + # "average_price": 143.81, + # "api": True, + # "amount": 40 + # } + # } + # } + # + result = self.safe_value(response, 'result', {}) + order = self.safe_value(result, 'order') + trades = self.safe_value(result, 'trades', []) + order['trades'] = trades + return self.parse_order(order, market) + + async def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + """ + edit a trade order + + https://docs.deribit.com/#private-edit + + :param str id: edit order id + :param str [symbol]: unified symbol of the market to edit an order in + :param str [type]: 'market' or 'limit' + :param str [side]: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency. For perpetual and inverse futures the amount is in USD units. For options it is in the underlying assets base currency. + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.trailingAmount]: the quote amount to trail away from the current market price + :returns dict: an `order structure ` + """ + if amount is None: + raise ArgumentsRequired(self.id + ' editOrder() requires an amount argument') + await self.load_markets() + request: dict = { + 'order_id': id, + 'amount': self.amount_to_precision(symbol, amount), + # 'post_only': False, # if the new price would cause the order to be filled immediately(as taker), the price will be changed to be just below the spread. + # 'reject_post_only': False, # if True the order is put to order book unmodified or request is rejected + # 'reduce_only': False, # if True, the order is intended to only reduce a current position + # 'stop_price': False, # stop price, required for stop_limit orders + # 'advanced': 'usd', # 'implv', advanced option order type, options only + } + if price is not None: + request['price'] = self.price_to_precision(symbol, price) + trailingAmount = self.safe_string_2(params, 'trailingAmount', 'trigger_offset') + isTrailingAmountOrder = trailingAmount is not None + if isTrailingAmountOrder: + request['trigger_offset'] = self.parse_to_numeric(trailingAmount) + params = self.omit(params, 'trigger_offset') + response = await self.privateGetEdit(self.extend(request, params)) + result = self.safe_value(response, 'result', {}) + order = self.safe_value(result, 'order') + trades = self.safe_value(result, 'trades', []) + order['trades'] = trades + return self.parse_order(order) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://docs.deribit.com/#private-cancel + + :param str id: order id + :param str symbol: not used by deribit cancelOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + request: dict = { + 'order_id': id, + } + response = await self.privateGetCancel(self.extend(request, params)) + result = self.safe_dict(response, 'result', {}) + return self.parse_order(result) + + async def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders + + https://docs.deribit.com/#private-cancel_all + https://docs.deribit.com/#private-cancel_all_by_instrument + + :param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + request: dict = {} + response = None + if symbol is None: + response = await self.privateGetCancelAll(self.extend(request, params)) + else: + market = self.market(symbol) + request['instrument_name'] = market['id'] + response = await self.privateGetCancelAllByInstrument(self.extend(request, params)) + # + # { + # jsonrpc: '2.0', + # result: '1', + # usIn: '1720508354127369', + # usOut: '1720508354133603', + # usDiff: '6234', + # testnet: True + # } + # + return [ + self.safe_order({ + 'info': response, + }), + ] + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://docs.deribit.com/#private-get_open_orders_by_currency + https://docs.deribit.com/#private-get_open_orders_by_instrument + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + request: dict = {} + market = None + response = None + if symbol is None: + code = self.code_from_options('fetchOpenOrders', params) + currency = self.currency(code) + request['currency'] = currency['id'] + response = await self.privateGetGetOpenOrdersByCurrency(self.extend(request, params)) + else: + market = self.market(symbol) + request['instrument_name'] = market['id'] + response = await self.privateGetGetOpenOrdersByInstrument(self.extend(request, params)) + result = self.safe_list(response, 'result', []) + return self.parse_orders(result, market, since, limit) + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://docs.deribit.com/#private-get_order_history_by_currency + https://docs.deribit.com/#private-get_order_history_by_instrument + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + request: dict = {} + market = None + response = None + if limit is not None: + request['count'] = limit + else: + request['count'] = 1000 # max value + if symbol is None: + code = self.code_from_options('fetchClosedOrders', params) + currency = self.currency(code) + request['currency'] = currency['id'] + response = await self.privateGetGetOrderHistoryByCurrency(self.extend(request, params)) + else: + market = self.market(symbol) + request['instrument_name'] = market['id'] + response = await self.privateGetGetOrderHistoryByInstrument(self.extend(request, params)) + result = self.safe_list(response, 'result', []) + return self.parse_orders(result, market, since, limit) + + async def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all the trades made from a single order + + https://docs.deribit.com/#private-get_user_trades_by_order + + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + request: dict = { + 'order_id': id, + } + response = await self.privateGetGetUserTradesByOrder(self.extend(request, params)) + # + # { + # "jsonrpc": "2.0", + # "id": 9367, + # "result": { + # "trades": [ + # { + # "trade_seq": 3, + # "trade_id": "ETH-34066", + # "timestamp": 1550219814585, + # "tick_direction": 1, + # "state": "open", + # "self_trade": False, + # "reduce_only": False, + # "price": 0.04, + # "post_only": False, + # "order_type": "limit", + # "order_id": "ETH-334607", + # "matching_id": null, + # "liquidity": "M", + # "iv": 56.83, + # "instrument_name": "ETH-22FEB19-120-C", + # "index_price": 121.37, + # "fee_currency": "ETH", + # "fee": 0.0011, + # "direction": "buy", + # "amount": 11 + # }, + # ], + # "has_more": True + # } + # } + # + result = self.safe_list(response, 'result', []) + return self.parse_trades(result, None, since, limit) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://docs.deribit.com/#private-get_user_trades_by_currency + https://docs.deribit.com/#private-get_user_trades_by_currency_and_time + https://docs.deribit.com/#private-get_user_trades_by_instrument + https://docs.deribit.com/#private-get_user_trades_by_instrument_and_time + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + request: dict = { + 'include_old': True, + } + market = None + if limit is not None: + request['count'] = limit # default 10 + response = None + if symbol is None: + code = self.code_from_options('fetchMyTrades', params) + currency = self.currency(code) + request['currency'] = currency['id'] + if since is None: + response = await self.privateGetGetUserTradesByCurrency(self.extend(request, params)) + else: + request['start_timestamp'] = since + response = await self.privateGetGetUserTradesByCurrencyAndTime(self.extend(request, params)) + else: + market = self.market(symbol) + request['instrument_name'] = market['id'] + if since is None: + response = await self.privateGetGetUserTradesByInstrument(self.extend(request, params)) + else: + request['start_timestamp'] = since + response = await self.privateGetGetUserTradesByInstrumentAndTime(self.extend(request, params)) + # + # { + # "jsonrpc": "2.0", + # "id": 9367, + # "result": { + # "trades": [ + # { + # "trade_seq": 3, + # "trade_id": "ETH-34066", + # "timestamp": 1550219814585, + # "tick_direction": 1, + # "state": "open", + # "self_trade": False, + # "reduce_only": False, + # "price": 0.04, + # "post_only": False, + # "order_type": "limit", + # "order_id": "ETH-334607", + # "matching_id": null, + # "liquidity": "M", + # "iv": 56.83, + # "instrument_name": "ETH-22FEB19-120-C", + # "index_price": 121.37, + # "fee_currency": "ETH", + # "fee": 0.0011, + # "direction": "buy", + # "amount": 11 + # }, + # ], + # "has_more": True + # } + # } + # + result = self.safe_value(response, 'result', {}) + trades = self.safe_list(result, 'trades', []) + return self.parse_trades(trades, market, since, limit) + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://docs.deribit.com/#private-get_deposits + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + if code is None: + raise ArgumentsRequired(self.id + ' fetchDeposits() requires a currency code argument') + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + } + if limit is not None: + request['count'] = limit + response = await self.privateGetGetDeposits(self.extend(request, params)) + # + # { + # "jsonrpc": "2.0", + # "id": 5611, + # "result": { + # "count": 1, + # "data": [ + # { + # "address": "2N35qDKDY22zmJq9eSyiAerMD4enJ1xx6ax", + # "amount": 5, + # "currency": "BTC", + # "received_timestamp": 1549295017670, + # "state": "completed", + # "transaction_id": "230669110fdaf0a0dbcdc079b6b8b43d5af29cc73683835b9bc6b3406c065fda", + # "updated_timestamp": 1549295130159 + # } + # ] + # } + # } + # + result = self.safe_value(response, 'result', {}) + data = self.safe_list(result, 'data', []) + return self.parse_transactions(data, currency, since, limit, params) + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://docs.deribit.com/#private-get_withdrawals + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + if code is None: + raise ArgumentsRequired(self.id + ' fetchWithdrawals() requires a currency code argument') + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + } + if limit is not None: + request['count'] = limit + response = await self.privateGetGetWithdrawals(self.extend(request, params)) + # + # { + # "jsonrpc": "2.0", + # "id": 2745, + # "result": { + # "count": 1, + # "data": [ + # { + # "address": "2NBqqD5GRJ8wHy1PYyCXTe9ke5226FhavBz", + # "amount": 0.5, + # "confirmed_timestamp": null, + # "created_timestamp": 1550571443070, + # "currency": "BTC", + # "fee": 0.0001, + # "id": 1, + # "priority": 0.15, + # "state": "unconfirmed", + # "transaction_id": null, + # "updated_timestamp": 1550571443070 + # } + # ] + # } + # } + # + result = self.safe_value(response, 'result', {}) + data = self.safe_list(result, 'data', []) + return self.parse_transactions(data, currency, since, limit, params) + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'completed': 'ok', + 'unconfirmed': 'pending', + } + return self.safe_string(statuses, status, status) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fetchWithdrawals + # + # { + # "address": "2NBqqD5GRJ8wHy1PYyCXTe9ke5226FhavBz", + # "amount": 0.5, + # "confirmed_timestamp": null, + # "created_timestamp": 1550571443070, + # "currency": "BTC", + # "fee": 0.0001, + # "id": 1, + # "priority": 0.15, + # "state": "unconfirmed", + # "transaction_id": null, + # "updated_timestamp": 1550571443070 + # } + # + # fetchDeposits + # + # { + # "address": "2N35qDKDY22zmJq9eSyiAerMD4enJ1xx6ax", + # "amount": 5, + # "currency": "BTC", + # "received_timestamp": 1549295017670, + # "state": "completed", + # "transaction_id": "230669110fdaf0a0dbcdc079b6b8b43d5af29cc73683835b9bc6b3406c065fda", + # "updated_timestamp": 1549295130159 + # } + # + currencyId = self.safe_string(transaction, 'currency') + code = self.safe_currency_code(currencyId, currency) + timestamp = self.safe_integer_2(transaction, 'created_timestamp', 'received_timestamp') + updated = self.safe_integer(transaction, 'updated_timestamp') + status = self.parse_transaction_status(self.safe_string(transaction, 'state')) + address = self.safe_string(transaction, 'address') + feeCost = self.safe_number(transaction, 'fee') + type = 'deposit' + fee = None + if feeCost is not None: + type = 'withdrawal' + fee = { + 'cost': feeCost, + 'currency': code, + } + return { + 'info': transaction, + 'id': self.safe_string(transaction, 'id'), + 'txid': self.safe_string(transaction, 'transaction_id'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'address': address, + 'addressTo': address, + 'addressFrom': None, + 'tag': None, + 'tagTo': None, + 'tagFrom': None, + 'type': type, + 'amount': self.safe_number(transaction, 'amount'), + 'currency': code, + 'status': status, + 'updated': updated, + 'network': None, + 'internal': None, + 'comment': None, + 'fee': fee, + } + + def parse_position(self, position: dict, market: Market = None): + # + # { + # "jsonrpc": "2.0", + # "id": 404, + # "result": { + # "average_price": 0, + # "delta": 0, + # "direction": "buy", + # "estimated_liquidation_price": 0, + # "floating_profit_loss": 0, + # "index_price": 3555.86, + # "initial_margin": 0, + # "instrument_name": "BTC-PERPETUAL", + # "leverage": 100, + # "kind": "future", + # "maintenance_margin": 0, + # "mark_price": 3556.62, + # "open_orders_margin": 0.000165889, + # "realized_profit_loss": 0, + # "settlement_price": 3555.44, + # "size": 0, + # "size_currency": 0, + # "total_profit_loss": 0 + # } + # } + # + contract = self.safe_string(position, 'instrument_name') + market = self.safe_market(contract, market) + side = self.safe_string(position, 'direction') + side = 'long' if (side == 'buy') else 'short' + unrealizedPnl = self.safe_string(position, 'floating_profit_loss') + initialMarginString = self.safe_string(position, 'initial_margin') + notionalString = self.safe_string(position, 'size_currency') + notionalStringAbs = Precise.string_abs(notionalString) + maintenanceMarginString = self.safe_string(position, 'maintenance_margin') + return self.safe_position({ + 'info': position, + 'id': None, + 'symbol': self.safe_string(market, 'symbol'), + 'timestamp': None, + 'datetime': None, + 'lastUpdateTimestamp': None, + 'initialMargin': self.parse_number(initialMarginString), + 'initialMarginPercentage': self.parse_number(Precise.string_mul(Precise.string_div(initialMarginString, notionalStringAbs), '100')), + 'maintenanceMargin': self.parse_number(maintenanceMarginString), + 'maintenanceMarginPercentage': self.parse_number(Precise.string_mul(Precise.string_div(maintenanceMarginString, notionalStringAbs), '100')), + 'entryPrice': self.safe_number(position, 'average_price'), + 'notional': self.parse_number(notionalStringAbs), + 'leverage': self.safe_integer(position, 'leverage'), + 'unrealizedPnl': self.parse_number(unrealizedPnl), + 'realizedPnl': self.safe_number(position, 'realized_profit_loss'), + 'contracts': self.safe_number(position, 'size'), + 'contractSize': self.safe_number(position, 'contractSize'), + 'marginRatio': None, + 'liquidationPrice': self.safe_number(position, 'estimated_liquidation_price'), + 'markPrice': self.safe_number(position, 'mark_price'), + 'lastPrice': None, + 'collateral': None, + 'marginMode': None, + 'side': side, + 'percentage': None, + 'hedged': None, + 'stopLossPrice': None, + 'takeProfitPrice': None, + }) + + async def fetch_position(self, symbol: str, params={}): + """ + fetch data on a single open contract trade position + + https://docs.deribit.com/#private-get_position + + :param str symbol: unified market symbol of the market the position is held in, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `position structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'instrument_name': market['id'], + } + response = await self.privateGetGetPosition(self.extend(request, params)) + # + # { + # "jsonrpc": "2.0", + # "id": 404, + # "result": { + # "average_price": 0, + # "delta": 0, + # "direction": "buy", + # "estimated_liquidation_price": 0, + # "floating_profit_loss": 0, + # "index_price": 3555.86, + # "initial_margin": 0, + # "instrument_name": "BTC-PERPETUAL", + # "leverage": 100, + # "kind": "future", + # "maintenance_margin": 0, + # "mark_price": 3556.62, + # "open_orders_margin": 0.000165889, + # "realized_profit_loss": 0, + # "settlement_price": 3555.44, + # "size": 0, + # "size_currency": 0, + # "total_profit_loss": 0 + # } + # } + # + result = self.safe_dict(response, 'result') + return self.parse_position(result) + + async def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://docs.deribit.com/#private-get_positions + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.currency]: currency code filter for positions + :param str [params.kind]: market type filter for positions 'future', 'option', 'spot', 'future_combo' or 'option_combo' + :param int [params.subaccount_id]: the user id for the subaccount + :returns dict[]: a list of `position structure ` + """ + await self.load_markets() + code = self.safe_string(params, 'currency') + request: dict = {} + if code is not None: + params = self.omit(params, 'currency') + currency = self.currency(code) + request['currency'] = currency['id'] + response = await self.privateGetGetPositions(self.extend(request, params)) + # + # { + # "jsonrpc": "2.0", + # "id": 2236, + # "result": [ + # { + # "average_price": 7440.18, + # "delta": 0.006687487, + # "direction": "buy", + # "estimated_liquidation_price": 1.74, + # "floating_profit_loss": 0, + # "index_price": 7466.79, + # "initial_margin": 0.000197283, + # "instrument_name": "BTC-PERPETUAL", + # "kind": "future", + # "leverage": 34, + # "maintenance_margin": 0.000143783, + # "mark_price": 7476.65, + # "open_orders_margin": 0.000197288, + # "realized_funding": -1e-8, + # "realized_profit_loss": -9e-9, + # "settlement_price": 7476.65, + # "size": 50, + # "size_currency": 0.006687487, + # "total_profit_loss": 0.000032781 + # }, + # ] + # } + # + result = self.safe_list(response, 'result') + return self.parse_positions(result, symbols) + + async def fetch_volatility_history(self, code: str, params={}): + """ + fetch the historical volatility of an option market based on an underlying asset + + https://docs.deribit.com/#public-get_historical_volatility + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `volatility history objects ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + } + response = await self.publicGetGetHistoricalVolatility(self.extend(request, params)) + # + # { + # "jsonrpc": "2.0", + # "result": [ + # [1640142000000,63.828320460740585], + # [1640142000000,63.828320460740585], + # [1640145600000,64.03821964123213] + # ], + # "usIn": 1641515379467734, + # "usOut": 1641515379468095, + # "usDiff": 361, + # "testnet": False + # } + # + return self.parse_volatility_history(response) + + def parse_volatility_history(self, volatility): + # + # { + # "jsonrpc": "2.0", + # "result": [ + # [1640142000000,63.828320460740585], + # [1640142000000,63.828320460740585], + # [1640145600000,64.03821964123213] + # ], + # "usIn": 1641515379467734, + # "usOut": 1641515379468095, + # "usDiff": 361, + # "testnet": False + # } + # + volatilityResult = self.safe_value(volatility, 'result', []) + result = [] + for i in range(0, len(volatilityResult)): + timestamp = self.safe_integer(volatilityResult[i], 0) + volatilityObj = self.safe_number(volatilityResult[i], 1) + result.append({ + 'info': volatilityObj, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'volatility': volatilityObj, + }) + return result + + async def fetch_transfers(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[TransferEntry]: + """ + fetch a history of internal transfers made on an account + + https://docs.deribit.com/#private-get_transfers + + :param str code: unified currency code of the currency transferred + :param int [since]: the earliest time in ms to fetch transfers for + :param int [limit]: the maximum number of transfers structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transfer structures ` + """ + if code is None: + raise ArgumentsRequired(self.id + ' fetchTransfers() requires a currency code argument') + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + } + if limit is not None: + request['count'] = limit + response = await self.privateGetGetTransfers(self.extend(request, params)) + # + # { + # "jsonrpc": "2.0", + # "id": 7606, + # "result": { + # "count": 2, + # "data": [ + # { + # "amount": 0.2, + # "created_timestamp": 1550579457727, + # "currency": "BTC", + # "direction": "payment", + # "id": 2, + # "other_side": "2MzyQc5Tkik61kJbEpJV5D5H9VfWHZK9Sgy", + # "state": "prepared", + # "type": "user", + # "updated_timestamp": 1550579457727 + # }, + # { + # "amount": 0.3, + # "created_timestamp": 1550579255800, + # "currency": "BTC", + # "direction": "payment", + # "id": 1, + # "other_side": "new_user_1_1", + # "state": "confirmed", + # "type": "subaccount", + # "updated_timestamp": 1550579255800 + # } + # ] + # } + # } + # + result = self.safe_value(response, 'result', {}) + transfers = self.safe_list(result, 'data', []) + return self.parse_transfers(transfers, currency, since, limit, params) + + async def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://docs.deribit.com/#private-submit_transfer_to_user + https://docs.deribit.com/#private-submit_transfer_to_subaccount + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account to transfer from + :param str toAccount: account to transfer to + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transfer structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'amount': amount, + 'currency': currency['id'], + 'destination': toAccount, + } + method = self.safe_string(params, 'method') + params = self.omit(params, 'method') + if method is None: + transferOptions = self.safe_value(self.options, 'transfer', {}) + method = self.safe_string(transferOptions, 'method', 'privateGetSubmitTransferToSubaccount') + response = None + if method == 'privateGetSubmitTransferToUser': + response = await self.privateGetSubmitTransferToUser(self.extend(request, params)) + else: + response = await self.privateGetSubmitTransferToSubaccount(self.extend(request, params)) + # + # { + # "jsonrpc": "2.0", + # "id": 9421, + # "result": { + # "updated_timestamp": 1550232862350, + # "type": "user", + # "state": "prepared", + # "other_side": "0x4aa0753d798d668056920094d65321a8e8913e26", + # "id": 3, + # "direction": "payment", + # "currency": "ETH", + # "created_timestamp": 1550232862350, + # "amount": 13.456 + # } + # } + # + result = self.safe_dict(response, 'result', {}) + return self.parse_transfer(result, currency) + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + # + # { + # "updated_timestamp": 1550232862350, + # "type": "user", + # "state": "prepared", + # "other_side": "0x4aa0753d798d668056920094d65321a8e8913e26", + # "id": 3, + # "direction": "payment", + # "currency": "ETH", + # "created_timestamp": 1550232862350, + # "amount": 13.456 + # } + # + timestamp = self.safe_timestamp(transfer, 'created_timestamp') + status = self.safe_string(transfer, 'state') + account = self.safe_string(transfer, 'other_side') + direction = self.safe_string(transfer, 'direction') + currencyId = self.safe_string(transfer, 'currency') + return { + 'info': transfer, + 'id': self.safe_string(transfer, 'id'), + 'status': self.parse_transfer_status(status), + 'amount': self.safe_number(transfer, 'amount'), + 'currency': self.safe_currency_code(currencyId, currency), + 'fromAccount': direction != account if 'payment' else None, + 'toAccount': direction == account if 'payment' else None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + + def parse_transfer_status(self, status: Str) -> Str: + statuses: dict = { + 'prepared': 'pending', + 'confirmed': 'ok', + 'cancelled': 'cancelled', + 'waiting_for_admin': 'pending', + } + return self.safe_string(statuses, status, status) + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://docs.deribit.com/#private-withdraw + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.check_address(address) + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + 'address': address, # must be in the address book + 'amount': amount, + # 'priority': 'high', # low, mid, high, very_high, extreme_high, insane + # 'tfa': '123456', # if enabled + } + if self.twofa is not None: + request['tfa'] = self.totp(self.twofa) + response = await self.privateGetWithdraw(self.extend(request, params)) + return self.parse_transaction(response, currency) + + def parse_deposit_withdraw_fee(self, fee, currency: Currency = None): + # + # { + # "withdrawal_priorities": [], + # "withdrawal_fee": 0.01457324, + # "min_withdrawal_fee": 0.000001, + # "min_confirmations": 1, + # "fee_precision": 8, + # "currency_long": "Solana", + # "currency": "SOL", + # "coin_type": "SOL" + # } + # + return { + 'info': fee, + 'withdraw': { + 'fee': self.safe_number(fee, 'withdrawal_fee'), + 'percentage': False, + }, + 'deposit': { + 'fee': None, + 'percentage': None, + }, + 'networks': {}, + } + + async def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}): + """ + fetch deposit and withdraw fees + + https://docs.deribit.com/#public-get_currencies + + :param str[]|None codes: list of unified currency codes + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `fee structures ` + """ + await self.load_markets() + response = await self.publicGetGetCurrencies(params) + # + # { + # "jsonrpc": "2.0", + # "result": [ + # { + # "withdrawal_priorities": [], + # "withdrawal_fee": 0.01457324, + # "min_withdrawal_fee": 0.000001, + # "min_confirmations": 1, + # "fee_precision": 8, + # "currency_long": "Solana", + # "currency": "SOL", + # "coin_type": "SOL" + # }, + # ... + # ], + # "usIn": 1688652701456124, + # "usOut": 1688652701456390, + # "usDiff": 266, + # "testnet": True + # } + # + data = self.safe_list(response, 'result', []) + return self.parse_deposit_withdraw_fees(data, codes, 'currency') + + async def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate + + https://docs.deribit.com/#public-get_funding_rate_value + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.start_timestamp]: fetch funding rate starting from self timestamp + :param int [params.end_timestamp]: fetch funding rate ending at self timestamp + :returns dict: a `funding rate structure ` + """ + await self.load_markets() + market = self.market(symbol) + time = self.milliseconds() + request: dict = { + 'instrument_name': market['id'], + 'start_timestamp': time - (8 * 60 * 60 * 1000), # 8h ago, + 'end_timestamp': time, + } + response = await self.publicGetGetFundingRateValue(self.extend(request, params)) + # + # { + # "jsonrpc":"2.0", + # "result":"0", + # "usIn":"1691161645596519", + # "usOut":"1691161645597149", + # "usDiff":"630", + # "testnet":false + # } + # + return self.parse_funding_rate(response, market) + + async def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch the current funding rate + + https://docs.deribit.com/#public-get_funding_rate_history + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch funding rate history for + :param int [limit]: the maximum number of entries to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: fetch funding rate ending at self timestamp + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict: a `funding rate structure ` + """ + await self.load_markets() + market = self.market(symbol) + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchFundingRateHistory', 'paginate') + maxEntriesPerRequest = 744 # seems exchange returns max 744 items per request + eachItemDuration = '1h' + if paginate: + # fix for: https://github.com/ccxt/ccxt/issues/25040 + return await self.fetch_paginated_call_deterministic('fetchFundingRateHistory', symbol, since, limit, eachItemDuration, self.extend(params, {'isDeribitPaginationCall': True}), maxEntriesPerRequest) + duration = self.parse_timeframe(eachItemDuration) * 1000 + time = self.milliseconds() + month = 30 * 24 * 60 * 60 * 1000 + if since is None: + since = time - month + else: + time = since + month + request: dict = { + 'instrument_name': market['id'], + 'start_timestamp': since - 1, + } + until = self.safe_integer_2(params, 'until', 'end_timestamp') + if until is not None: + params = self.omit(params, ['until']) + request['end_timestamp'] = until + else: + request['end_timestamp'] = time + if 'isDeribitPaginationCall' in params: + params = self.omit(params, 'isDeribitPaginationCall') + maxUntil = self.sum(since, limit * duration) + request['end_timestamp'] = min(request['end_timestamp'], maxUntil) + response = await self.publicGetGetFundingRateHistory(self.extend(request, params)) + # + # { + # "jsonrpc": "2.0", + # "id": 7617, + # "result": [ + # { + # "timestamp": 1569891600000, + # "index_price": 8222.87, + # "prev_index_price": 8305.72, + # "interest_8h": -0.00009234260068476106, + # "interest_1h": -4.739622041017375e-7 + # } + # ] + # } + # + rates = [] + result = self.safe_value(response, 'result', []) + for i in range(0, len(result)): + fr = result[i] + rate = self.parse_funding_rate(fr, market) + rates.append(rate) + return self.filter_by_symbol_since_limit(rates, symbol, since, limit) + + def parse_funding_rate(self, contract, market: Market = None) -> FundingRate: + # + # { + # "jsonrpc":"2.0", + # "result":"0", + # "usIn":"1691161645596519", + # "usOut":"1691161645597149", + # "usDiff":"630", + # "testnet":false + # } + # history + # { + # "timestamp": 1569891600000, + # "index_price": 8222.87, + # "prev_index_price": 8305.72, + # "interest_8h": -0.00009234260068476106, + # "interest_1h": -4.739622041017375e-7 + # } + # + timestamp = self.safe_integer(contract, 'timestamp') + datetime = self.iso8601(timestamp) + result = self.safe_number_2(contract, 'result', 'interest_8h') + return { + 'info': contract, + 'symbol': self.safe_symbol(None, market), + 'markPrice': None, + 'indexPrice': self.safe_number(contract, 'index_price'), + 'interestRate': None, + 'estimatedSettlePrice': None, + 'timestamp': timestamp, + 'datetime': datetime, + 'fundingRate': result, + 'fundingTimestamp': None, + 'fundingDatetime': None, + 'nextFundingRate': None, + 'nextFundingTimestamp': None, + 'nextFundingDatetime': None, + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': '8h', + } + + async def fetch_liquidations(self, symbol: str, since: Int = None, limit: Int = None, params={}): + """ + retrieves the public liquidations of a trading pair + + https://docs.deribit.com/#public-get_last_settlements_by_currency + + :param str symbol: unified CCXT market symbol + :param int [since]: the earliest time in ms to fetch liquidations for + :param int [limit]: the maximum number of liquidation structures to retrieve + :param dict [params]: exchange specific parameters for the deribit api endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict: an array of `liquidation structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchLiquidations', 'paginate') + if paginate: + return await self.fetch_paginated_call_cursor('fetchLiquidations', symbol, since, limit, params, 'continuation', 'continuation', None) + market = self.market(symbol) + if market['spot']: + raise NotSupported(self.id + ' fetchLiquidations() does not support ' + market['type'] + ' markets') + request: dict = { + 'instrument_name': market['id'], + 'type': 'bankruptcy', + } + if since is not None: + request['search_start_timestamp'] = since + if limit is not None: + request['count'] = limit + response = await self.publicGetGetLastSettlementsByInstrument(self.extend(request, params)) + # + # { + # "jsonrpc": "2.0", + # "result": { + # "settlements": [ + # { + # "type": "bankruptcy", + # "timestamp": 1696579200041, + # "funded": 10000.0, + # "session_bankrupcy": 10000.0 + # "session_profit_loss": 112951.68715857354, + # "session_tax": 0.15, + # "session_tax_rate": 0.0015, + # "socialized": 0.001, + # }, + # ], + # "continuation": "5dHzoGyD8Hs8KURoUhfgXgHpJTA5oyapoudSmNeAfEftqRbjNE6jNNUpo2oCu1khnZL9ao" + # }, + # "usIn": 1696652052254890, + # "usOut": 1696652052255733, + # "usDiff": 843, + # "testnet": False + # } + # + result = self.safe_value(response, 'result', {}) + cursor = self.safe_string(result, 'continuation') + settlements = self.safe_value(result, 'settlements', []) + settlementsWithCursor = self.add_pagination_cursor_to_result(cursor, settlements) + return self.parse_liquidations(settlementsWithCursor, market, since, limit) + + def add_pagination_cursor_to_result(self, cursor, data): + if cursor is not None: + dataLength = len(data) + if dataLength > 0: + first = data[0] + last = data[dataLength - 1] + first['continuation'] = cursor + last['continuation'] = cursor + data[0] = first + data[dataLength - 1] = last + return data + + async def fetch_my_liquidations(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + retrieves the users liquidated positions + + https://docs.deribit.com/#private-get_settlement_history_by_instrument + + :param str symbol: unified CCXT market symbol + :param int [since]: the earliest time in ms to fetch liquidations for + :param int [limit]: the maximum number of liquidation structures to retrieve + :param dict [params]: exchange specific parameters for the deribit api endpoint + :returns dict: an array of `liquidation structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMyLiquidations() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + if market['spot']: + raise NotSupported(self.id + ' fetchMyLiquidations() does not support ' + market['type'] + ' markets') + request: dict = { + 'instrument_name': market['id'], + 'type': 'bankruptcy', + } + if since is not None: + request['search_start_timestamp'] = since + if limit is not None: + request['count'] = limit + response = await self.privateGetGetSettlementHistoryByInstrument(self.extend(request, params)) + # + # { + # "jsonrpc": "2.0", + # "result": { + # "settlements": [ + # { + # "type": "bankruptcy", + # "timestamp": 1696579200041, + # "funded": 10000.0, + # "session_bankrupcy": 10000.0 + # "session_profit_loss": 112951.68715857354, + # "session_tax": 0.15, + # "session_tax_rate": 0.0015, + # "socialized": 0.001, + # }, + # ], + # "continuation": "5dHzoGyD8Hs8KURoUhfgXgHpJTA5oyapoudSmNeAfEftqRbjNE6jNNUpo2oCu1khnZL9ao" + # }, + # "usIn": 1696652052254890, + # "usOut": 1696652052255733, + # "usDiff": 843, + # "testnet": False + # } + # + result = self.safe_value(response, 'result', {}) + settlements = self.safe_list(result, 'settlements', []) + return self.parse_liquidations(settlements, market, since, limit) + + def parse_liquidation(self, liquidation, market: Market = None): + # + # { + # "type": "bankruptcy", + # "timestamp": 1696579200041, + # "funded": 1, + # "session_bankrupcy": 0.001, + # "session_profit_loss": 0.001, + # "session_tax": 0.0015, + # "session_tax_rate": 0.0015, + # "socialized": 0.001, + # } + # + timestamp = self.safe_integer(liquidation, 'timestamp') + return self.safe_liquidation({ + 'info': liquidation, + 'symbol': self.safe_symbol(None, market), + 'contracts': None, + 'contractSize': self.safe_number(market, 'contractSize'), + 'price': None, + 'baseValue': self.safe_number(liquidation, 'session_bankrupcy'), + 'quoteValue': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + + async def fetch_greeks(self, symbol: str, params={}) -> Greeks: + """ + fetches an option contracts greeks, financial metrics used to measure the factors that affect the price of an options contract + + https://docs.deribit.com/#public-ticker + + :param str symbol: unified symbol of the market to fetch greeks for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `greeks structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'instrument_name': market['id'], + } + response = await self.publicGetTicker(self.extend(request, params)) + # + # { + # "jsonrpc": "2.0", + # "result": { + # "estimated_delivery_price": 36552.72, + # "best_bid_amount": 0.2, + # "best_ask_amount": 9.1, + # "interest_rate": 0.0, + # "best_bid_price": 0.214, + # "best_ask_price": 0.219, + # "open_interest": 368.8, + # "settlement_price": 0.22103022, + # "last_price": 0.215, + # "bid_iv": 60.51, + # "ask_iv": 61.88, + # "mark_iv": 61.27, + # "underlying_index": "BTC-27SEP24", + # "underlying_price": 38992.71, + # "min_price": 0.1515, + # "max_price": 0.326, + # "mark_price": 0.2168, + # "instrument_name": "BTC-27SEP24-40000-C", + # "index_price": 36552.72, + # "greeks": { + # "rho": 130.63998, + # "theta": -13.48784, + # "vega": 141.90146, + # "gamma": 0.00002, + # "delta": 0.59621 + # }, + # "stats": { + # "volume_usd": 100453.9, + # "volume": 12.0, + # "price_change": -2.2727, + # "low": 0.2065, + # "high": 0.238 + # }, + # "state": "open", + # "timestamp": 1699578548021 + # }, + # "usIn": 1699578548308414, + # "usOut": 1699578548308606, + # "usDiff": 192, + # "testnet": False + # } + # + result = self.safe_value(response, 'result', {}) + return self.parse_greeks(result, market) + + def parse_greeks(self, greeks: dict, market: Market = None) -> Greeks: + # + # { + # "estimated_delivery_price": 36552.72, + # "best_bid_amount": 0.2, + # "best_ask_amount": 9.1, + # "interest_rate": 0.0, + # "best_bid_price": 0.214, + # "best_ask_price": 0.219, + # "open_interest": 368.8, + # "settlement_price": 0.22103022, + # "last_price": 0.215, + # "bid_iv": 60.51, + # "ask_iv": 61.88, + # "mark_iv": 61.27, + # "underlying_index": "BTC-27SEP24", + # "underlying_price": 38992.71, + # "min_price": 0.1515, + # "max_price": 0.326, + # "mark_price": 0.2168, + # "instrument_name": "BTC-27SEP24-40000-C", + # "index_price": 36552.72, + # "greeks": { + # "rho": 130.63998, + # "theta": -13.48784, + # "vega": 141.90146, + # "gamma": 0.00002, + # "delta": 0.59621 + # }, + # "stats": { + # "volume_usd": 100453.9, + # "volume": 12.0, + # "price_change": -2.2727, + # "low": 0.2065, + # "high": 0.238 + # }, + # "state": "open", + # "timestamp": 1699578548021 + # } + # + timestamp = self.safe_integer(greeks, 'timestamp') + marketId = self.safe_string(greeks, 'instrument_name') + symbol = self.safe_symbol(marketId, market) + stats = self.safe_value(greeks, 'greeks', {}) + return { + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'delta': self.safe_number(stats, 'delta'), + 'gamma': self.safe_number(stats, 'gamma'), + 'theta': self.safe_number(stats, 'theta'), + 'vega': self.safe_number(stats, 'vega'), + 'rho': self.safe_number(stats, 'rho'), + 'bidSize': self.safe_number(greeks, 'best_bid_amount'), + 'askSize': self.safe_number(greeks, 'best_ask_amount'), + 'bidImpliedVolatility': self.safe_number(greeks, 'bid_iv'), + 'askImpliedVolatility': self.safe_number(greeks, 'ask_iv'), + 'markImpliedVolatility': self.safe_number(greeks, 'mark_iv'), + 'bidPrice': self.safe_number(greeks, 'best_bid_price'), + 'askPrice': self.safe_number(greeks, 'best_ask_price'), + 'markPrice': self.safe_number(greeks, 'mark_price'), + 'lastPrice': self.safe_number(greeks, 'last_price'), + 'underlyingPrice': self.safe_number(greeks, 'underlying_price'), + 'info': greeks, + } + + async def fetch_option(self, symbol: str, params={}) -> Option: + """ + fetches option data that is commonly found in an option chain + + https://docs.deribit.com/#public-get_book_summary_by_instrument + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `option chain structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'instrument_name': market['id'], + } + response = await self.publicGetGetBookSummaryByInstrument(self.extend(request, params)) + # + # { + # "jsonrpc": "2.0", + # "result": [ + # { + # "mid_price": 0.04025, + # "volume_usd": 11045.12, + # "quote_currency": "BTC", + # "estimated_delivery_price": 65444.72, + # "creation_timestamp": 1711100949273, + # "base_currency": "BTC", + # "underlying_index": "BTC-27DEC24", + # "underlying_price": 73742.14, + # "volume": 4.0, + # "interest_rate": 0.0, + # "price_change": -6.9767, + # "open_interest": 274.2, + # "ask_price": 0.042, + # "bid_price": 0.0385, + # "instrument_name": "BTC-27DEC24-240000-C", + # "mark_price": 0.04007735, + # "last": 0.04, + # "low": 0.04, + # "high": 0.043 + # } + # ], + # "usIn": 1711100949273223, + # "usOut": 1711100949273580, + # "usDiff": 357, + # "testnet": False + # } + # + result = self.safe_list(response, 'result', []) + chain = self.safe_dict(result, 0, {}) + return self.parse_option(chain, None, market) + + async def fetch_option_chain(self, code: str, params={}) -> OptionChain: + """ + fetches data for an underlying asset that is commonly found in an option chain + + https://docs.deribit.com/#public-get_book_summary_by_currency + + :param str code: base currency to fetch an option chain for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `option chain structures ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + 'kind': 'option', + } + response = await self.publicGetGetBookSummaryByCurrency(self.extend(request, params)) + # + # { + # "jsonrpc": "2.0", + # "result": [ + # { + # "mid_price": 0.4075, + # "volume_usd": 2836.83, + # "quote_currency": "BTC", + # "estimated_delivery_price": 65479.26, + # "creation_timestamp": 1711101594477, + # "base_currency": "BTC", + # "underlying_index": "BTC-28JUN24", + # "underlying_price": 68827.27, + # "volume": 0.1, + # "interest_rate": 0.0, + # "price_change": 0.0, + # "open_interest": 364.1, + # "ask_price": 0.411, + # "bid_price": 0.404, + # "instrument_name": "BTC-28JUN24-42000-C", + # "mark_price": 0.40752052, + # "last": 0.423, + # "low": 0.423, + # "high": 0.423 + # } + # ], + # "usIn": 1711101594456388, + # "usOut": 1711101594484065, + # "usDiff": 27677, + # "testnet": False + # } + # + result = self.safe_list(response, 'result', []) + return self.parse_option_chain(result, 'base_currency', 'instrument_name') + + def parse_option(self, chain: dict, currency: Currency = None, market: Market = None) -> Option: + # + # { + # "mid_price": 0.04025, + # "volume_usd": 11045.12, + # "quote_currency": "BTC", + # "estimated_delivery_price": 65444.72, + # "creation_timestamp": 1711100949273, + # "base_currency": "BTC", + # "underlying_index": "BTC-27DEC24", + # "underlying_price": 73742.14, + # "volume": 4.0, + # "interest_rate": 0.0, + # "price_change": -6.9767, + # "open_interest": 274.2, + # "ask_price": 0.042, + # "bid_price": 0.0385, + # "instrument_name": "BTC-27DEC24-240000-C", + # "mark_price": 0.04007735, + # "last": 0.04, + # "low": 0.04, + # "high": 0.043 + # } + # + marketId = self.safe_string(chain, 'instrument_name') + market = self.safe_market(marketId, market) + currencyId = self.safe_string(chain, 'base_currency') + code = self.safe_currency_code(currencyId, currency) + timestamp = self.safe_integer(chain, 'timestamp') + return { + 'info': chain, + 'currency': code, + 'symbol': market['symbol'], + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'impliedVolatility': None, + 'openInterest': self.safe_number(chain, 'open_interest'), + 'bidPrice': self.safe_number(chain, 'bid_price'), + 'askPrice': self.safe_number(chain, 'ask_price'), + 'midPrice': self.safe_number(chain, 'mid_price'), + 'markPrice': self.safe_number(chain, 'mark_price'), + 'lastPrice': self.safe_number(chain, 'last'), + 'underlyingPrice': self.safe_number(chain, 'underlying_price'), + 'change': None, + 'percentage': self.safe_number(chain, 'price_change'), + 'baseVolume': self.safe_number(chain, 'volume'), + 'quoteVolume': self.safe_number(chain, 'volume_usd'), + } + + def nonce(self): + return self.milliseconds() + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + request = '/' + 'api/' + self.version + '/' + api + '/' + path + if api == 'public': + if params: + request += '?' + self.urlencode(params) + if api == 'private': + self.check_required_credentials() + nonce = str(self.nonce()) + timestamp = str(self.milliseconds()) + requestBody = '' + if params: + request += '?' + self.urlencode(params) + requestData = method + "\n" + request + "\n" + requestBody + "\n" # eslint-disable-line quotes + auth = timestamp + "\n" + nonce + "\n" + requestData # eslint-disable-line quotes + signature = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256) + headers = { + 'Authorization': 'deri-hmac-sha256 id=' + self.apiKey + ',ts=' + timestamp + ',sig=' + signature + ',' + 'nonce=' + nonce, + } + url = self.urls['api']['rest'] + request + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if not response: + return None # fallback to default error handler + # + # { + # "jsonrpc": "2.0", + # "error": { + # "message": "Invalid params", + # "data": {reason: "invalid currency", param: "currency"}, + # "code": -32602 + # }, + # "testnet": False, + # "usIn": 1583763842150374, + # "usOut": 1583763842150410, + # "usDiff": 36 + # } + # + error = self.safe_value(response, 'error') + if error is not None: + errorCode = self.safe_string(error, 'code') + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions, errorCode, feedback) + raise ExchangeError(feedback) # unknown message + return None diff --git a/ccxt/async_support/derive.py b/ccxt/async_support/derive.py new file mode 100644 index 0000000..18df024 --- /dev/null +++ b/ccxt/async_support/derive.py @@ -0,0 +1,2572 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.derive import ImplicitAPI +import asyncio +from ccxt.base.types import Any, Balances, Bool, Currencies, Currency, Int, Market, MarketType, Num, Order, OrderSide, OrderType, Position, Str, Strings, Ticker, FundingRate, Trade, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class derive(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(derive, self).describe(), { + 'id': 'derive', + 'name': 'derive', + 'countries': [], + 'version': 'v1', + 'rateLimit': 50, + 'certified': False, + 'pro': True, + 'dex': True, + 'has': { + 'CORS': None, + 'spot': False, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'cancelAllOrders': True, + 'cancelAllOrdersAfter': False, + 'cancelOrder': True, + 'cancelOrders': False, + 'cancelOrdersForSymbols': False, + 'closeAllPositions': False, + 'closePosition': False, + 'createMarketBuyOrderWithCost': False, + 'createMarketOrderWithCost': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createOrders': False, + 'createReduceOnlyOrder': False, + 'createStopOrder': False, + 'createTriggerOrder': False, + 'editOrder': True, + 'fetchAccounts': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchCanceledAndClosedOrders': False, + 'fetchCanceledOrders': True, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': False, + 'fetchDepositAddresses': False, + 'fetchDeposits': True, + 'fetchDepositWithdrawFee': 'emulated', + 'fetchDepositWithdrawFees': False, + 'fetchFundingHistory': True, + 'fetchFundingRate': True, + 'fetchFundingRateHistory': True, + 'fetchFundingRates': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchLedger': True, + 'fetchLeverage': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchMarginMode': None, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMyLiquidations': False, + 'fetchMyTrades': True, + 'fetchOHLCV': False, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOrder': False, + 'fetchOrderBook': False, + 'fetchOrders': True, + 'fetchOrderTrades': True, + 'fetchPosition': False, + 'fetchPositionMode': False, + 'fetchPositions': True, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchTicker': True, + 'fetchTickers': False, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': False, + 'fetchTransfer': False, + 'fetchTransfers': False, + 'fetchWithdrawal': False, + 'fetchWithdrawals': True, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'sandbox': True, + 'setLeverage': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'transfer': False, + 'withdraw': False, + }, + 'timeframes': { + '1m': '1m', + '3m': '3m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1h', + '2h': '2h', + '4h': '4h', + '8h': '8h', + '12h': '12h', + '1d': '1d', + '3d': '3d', + '1w': '1w', + '1M': '1M', + }, + 'hostname': 'derive.xyz', + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/f835b95f-033a-43dd-b6bb-24e698fc498c', + 'api': { + 'public': 'https://api.lyra.finance/public', + 'private': 'https://api.lyra.finance/private', + }, + 'test': { + 'public': 'https://api-demo.lyra.finance/public', + 'private': 'https://api-demo.lyra.finance/private', + }, + 'www': 'https://www.derive.xyz/', + 'doc': 'https://docs.derive.xyz/docs/', + 'fees': 'https://docs.derive.xyz/reference/fees-1/', + 'referral': 'https://www.derive.xyz/invite/3VB0B', + }, + 'api': { + 'public': { + 'get': [ + 'get_all_currencies', + ], + 'post': [ + 'build_register_session_key_tx', + 'register_session_key', + 'deregister_session_key', + 'login', + 'statistics', + 'get_all_currencies', + 'get_currency', + 'get_instrument', + 'get_all_instruments', + 'get_instruments', + 'get_ticker', + 'get_latest_signed_feeds', + 'get_option_settlement_prices', + 'get_spot_feed_history', + 'get_spot_feed_history_candles', + 'get_funding_rate_history', + 'get_trade_history', + 'get_option_settlement_history', + 'get_liquidation_history', + 'get_interest_rate_history', + 'get_transaction', + 'get_margin', + 'margin_watch', + 'validate_invite_code', + 'get_points', + 'get_all_points', + 'get_points_leaderboard', + 'get_descendant_tree', + 'get_tree_roots', + 'get_swell_percent_points', + 'get_vault_assets', + 'get_etherfi_effective_balances', + 'get_kelp_effective_balances', + 'get_bridge_balances', + 'get_ethena_participants', + 'get_vault_share', + 'get_vault_statistics', + 'get_vault_balances', + 'estimate_integrator_points', + 'create_subaccount_debug', + 'deposit_debug', + 'withdraw_debug', + 'send_quote_debug', + 'execute_quote_debug', + 'get_invite_code', + 'register_invite', + 'get_time', + 'get_live_incidents', + 'get_maker_programs', + 'get_maker_program_scores', + ], + }, + 'private': { + 'post': [ + 'get_account', + 'create_subaccount', + 'get_subaccount', + 'get_subaccounts', + 'get_all_portfolios', + 'change_subaccount_label', + 'get_notificationsv', + 'update_notifications', + 'deposit', + 'withdraw', + 'transfer_erc20', + 'transfer_position', + 'transfer_positions', + 'order', + 'replace', + 'order_debug', + 'get_order', + 'get_orders', + 'get_open_orders', + 'cancel', + 'cancel_by_label', + 'cancel_by_nonce', + 'cancel_by_instrument', + 'cancel_all', + 'cancel_trigger_order', + 'get_order_history', + 'get_trade_history', + 'get_deposit_history', + 'get_withdrawal_history', + 'send_rfq', + 'cancel_rfq', + 'cancel_batch_rfqs', + 'get_rfqs', + 'poll_rfqs', + 'send_quote', + 'cancel_quote', + 'cancel_batch_quotes', + 'get_quotes', + 'poll_quotes', + 'execute_quote', + 'rfq_get_best_quote', + 'get_margin', + 'get_collaterals', + 'get_positions', + 'get_option_settlement_history', + 'get_subaccount_value_history', + 'expired_and_cancelled_history', + 'get_funding_history', + 'get_interest_history', + 'get_erc20_transfer_history', + 'get_liquidation_history', + 'liquidate', + 'get_liquidator_history', + 'session_keys', + 'edit_session_key', + 'register_scoped_session_key', + 'get_mmp_config', + 'set_mmp_config', + 'reset_mmp', + 'set_cancel_on_disconnect', + 'get_invite_code', + 'register_invite', + ], + }, + }, + 'fees': { + }, + 'requiredCredentials': { + 'apiKey': False, + 'secret': False, + 'walletAddress': True, + 'privateKey': True, + }, + 'exceptions': { + 'exact': { + '-32000': RateLimitExceeded, # Rate limit exceeded + '-32100': RateLimitExceeded, # Number of concurrent websocket clients limit exceeded + '-32700': BadRequest, # Parse error + '-32600': BadRequest, # Invalid Request + '-32601': BadRequest, # Method not found + '-32602': InvalidOrder, # {"id":"55e66a3d-6a4e-4a36-a23d-5cf8a91ef478","error":{"code":"","message":"Invalid params"}} + '-32603': InvalidOrder, # {"code":"-32603","message":"Internal error","data":"SubAccount matching query does not exist."} + '9000': InvalidOrder, # Order confirmation timeout + '10000': BadRequest, # Manager not found + '10001': BadRequest, # Asset is not an ERC20 token + '10002': BadRequest, # Sender and recipient wallet do not match + '10003': BadRequest, # Sender and recipient subaccount IDs are the same + '10004': InvalidOrder, # Multiple currencies not supported + '10005': BadRequest, # Maximum number of subaccounts per wallet reached + '10006': BadRequest, # Maximum number of session keys per wallet reached + '10007': BadRequest, # Maximum number of assets per subaccount reached + '10008': BadRequest, # Maximum number of expiries per subaccount reached + '10009': BadRequest, # Recipient subaccount ID of the transfer cannot be 0 + '10010': InvalidOrder, # PMRM only supports USDC asset collateral. Cannot trade spot markets. + '10011': InsufficientFunds, # ERC20 allowance is insufficient + '10012': InsufficientFunds, # ERC20 balance is less than transfer amount + '10013': ExchangeError, # There is a pending deposit for self asset + '10014': ExchangeError, # There is a pending withdrawal for self asset + '11000': InsufficientFunds, # Insufficient funds + '11002': InvalidOrder, # Order rejected from queue + '11003': InvalidOrder, # Already cancelled + '11004': InvalidOrder, # Already filled + '11005': InvalidOrder, # Already expired + '11006': OrderNotFound, # {"code":"11006","message":"Does not exist","data":"Open order with id: 804018f3-b092-40a3-a933-b29574fa1ff8 does not exist."} + '11007': InvalidOrder, # Self-crossing disallowed + '11008': InvalidOrder, # Post-only reject + '11009': InvalidOrder, # Zero liquidity for market or IOC/FOK order + '11010': InvalidOrder, # Post-only invalid order type + '11011': InvalidOrder, # {"code":11011,"message":"Invalid signature expiry","data":"Order must expire in 300 sec or more"} + '11012': InvalidOrder, # {"code":"11012","message":"Invalid amount","data":"Amount must be a multiple of 0.01"} + '11013': InvalidOrder, # {"code":"11013","message":"Invalid limit price","data":{"limit":"10000","bandwidth":"92530"}} + '11014': InvalidOrder, # Fill-or-kill not filled + '11015': InvalidOrder, # MMP frozen + '11016': InvalidOrder, # Already consumed + '11017': InvalidOrder, # Non unique nonce + '11018': InvalidOrder, # Invalid nonce date + '11019': InvalidOrder, # Open orders limit exceeded + '11020': InsufficientFunds, # Negative ERC20 balance + '11021': InvalidOrder, # Instrument is not live + '11022': InvalidOrder, # Reject timestamp exceeded + '11023': InvalidOrder, # {"code":"11023","message":"Max fee order param is too low","data":"signed max_fee must be >= 194.420835871999983091712000000000000000"} + '11024': InvalidOrder, # {"code":11024,"message":"Reduce only not supported with self time in force"} + '11025': InvalidOrder, # Reduce only reject + '11026': BadRequest, # Transfer reject + '11027': InvalidOrder, # Subaccount undergoing liquidation + '11028': InvalidOrder, # Replaced order filled amount does not match expected state. + '11050': InvalidOrder, # Trigger order was cancelled between the time worker sent order and engine processed order + '11051': InvalidOrder, # {"code":"11051","message":"Trigger price must be higher than the current price for stop orders and vice versa for take orders","data":"Trigger price 9000.0 must be < or > current price 102671.2 depending on trigger type and direction."} + '11052': InvalidOrder, # Trigger order limit exceeded(separate limit from regular orders) + '11053': InvalidOrder, # Index and last-trade trigger price types not supported yet + '11054': InvalidOrder, # {"code":"11054","message":"Trigger orders cannot replace or be replaced"} + '11055': InvalidOrder, # Market order limit_price is unfillable at the given trigger price + '11100': InvalidOrder, # Leg instruments are not unique + '11101': InvalidOrder, # RFQ not found + '11102': InvalidOrder, # Quote not found + '11103': InvalidOrder, # Quote leg does not match RFQ leg + '11104': InvalidOrder, # Requested quote or RFQ is not open + '11105': InvalidOrder, # Requested quote ID references a different RFQ ID + '11106': InvalidOrder, # Invalid RFQ counterparty + '11107': InvalidOrder, # Quote maker total cost too high + '11200': InvalidOrder, # Auction not ongoing + '11201': InvalidOrder, # Open orders not allowed + '11202': InvalidOrder, # Price limit exceeded + '11203': InvalidOrder, # Last trade ID mismatch + '12000': InvalidOrder, # Asset not found + '12001': InvalidOrder, # Instrument not found + '12002': BadRequest, # Currency not found + '12003': BadRequest, # USDC does not have asset caps per manager + '13000': BadRequest, # Invalid channels + '14000': BadRequest, # {"code": 14000, "message": "Account not found"} + '14001': InvalidOrder, # {"code": 14001, "message": "Subaccount not found"} + '14002': BadRequest, # Subaccount was withdrawn + '14008': BadRequest, # Cannot reduce expiry using registerSessionKey RPC route + '14009': BadRequest, # Session key expiry must be > utc_now + 10 min + '14010': BadRequest, # Session key already registered for self account + '14011': BadRequest, # Session key already registered with another account + '14012': BadRequest, # Address must be checksummed + '14013': BadRequest, # str is not a valid ethereum address + '14014': InvalidOrder, # {"code":"14014","message":"Signature invalid for message or transaction","data":"Signature does not match data"} + '14015': BadRequest, # Transaction count for given wallet does not match provided nonce + '14016': BadRequest, # The provided signed raw transaction contains function name that does not match the expected function name + '14017': BadRequest, # The provided signed raw transaction contains contract address that does not match the expected contract address + '14018': BadRequest, # The provided signed raw transaction contains function params that do not match any expected function params + '14019': BadRequest, # The provided signed raw transaction contains function param values that do not match the expected values + '14020': BadRequest, # The X-LyraWallet header does not match the requested subaccount_id or wallet + '14021': BadRequest, # The X-LyraWallet header not provided + '14022': AuthenticationError, # Subscription to a private channel failed + '14023': InvalidOrder, # {"code":"14023","message":"Signer in on-chain related request is not wallet owner or registered session key","data":"Session key does not belong to wallet"} + '14024': BadRequest, # Chain ID must match the current roll up chain id + '14025': BadRequest, # The private request is missing a wallet or subaccount_id param + '14026': BadRequest, # Session key not found + '14027': AuthenticationError, # Unauthorized maker + '14028': BadRequest, # Cross currency RFQ not supported + '14029': AuthenticationError, # Session key IP not whitelisted + '14030': BadRequest, # Session key expired + '14031': AuthenticationError, # Unauthorized key scope + '14032': BadRequest, # Scope should not be changed + '16000': AuthenticationError, # You are in a restricted region that violates our terms of service. + '16001': AuthenticationError, # Account is disabled due to compliance violations, please contact support to enable it. + '16100': AuthenticationError, # Sentinel authorization is invalid + '17000': BadRequest, # This accoount does not have a shareable invite code + '17001': BadRequest, # Invalid invite code + '17002': BadRequest, # Invite code already registered for self account + '17003': BadRequest, # Invite code has no remaining uses + '17004': BadRequest, # Requirement for successful invite registration not met + '17005': BadRequest, # Account must register with a valid invite code to be elligible for points + '17006': BadRequest, # Point program does not exist + '17007': BadRequest, # Invalid leaderboard page number + '18000': BadRequest, # Invalid block number + '18001': BadRequest, # Failed to estimate block number. Please try again later. + '18002': BadRequest, # The provided smart contract owner does not match the wallet in LightAccountFactory.getAddress() + '18003': BadRequest, # Vault ERC20 asset does not exist + '18004': BadRequest, # Vault ERC20 pool does not exist + '18005': BadRequest, # Must add asset to pool before getting balances + '18006': BadRequest, # Invalid Swell season. Swell seasons are in the form 'swell_season_X'. + '18007': BadRequest, # Vault not found + '19000': BadRequest, # Maker program not found + }, + 'broad': { + }, + }, + 'precisionMode': TICK_SIZE, + 'commonCurrencies': { + }, + 'options': { + 'deriveWalletAddress': '', # a derive wallet address "0x"-prefixed hexstring + 'id': '0x0ad42b8e602c2d3d475ae52d678cf63d84ab2749', + }, + }) + + def set_sandbox_mode(self, enable: bool): + super(derive, self).set_sandbox_mode(enable) + self.options['sandboxMode'] = enable + + async def fetch_time(self, params={}): + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://docs.derive.xyz/reference/post_public-get-time + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = await self.publicPostGetTime(params) + # + # { + # "result": 1735846536758, + # "id": "f1c03d21-f886-4c5a-9a9d-33dd06f180f0" + # } + # + return self.safe_integer(response, 'result') + + async def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://docs.derive.xyz/reference/post_public-get-all-currencies + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + result: dict = {} + tokenResponse = await self.publicGetGetAllCurrencies(params) + # + # { + # "result": [ + # { + # "currency": "SEI", + # "instrument_types": [ + # "perp" + # ], + # "protocol_asset_addresses": { + # "perp": "0x7225889B75fd34C68eA3098dAE04D50553C09840", + # "option": null, + # "spot": null, + # "underlying_erc20": null + # }, + # "managers": [ + # { + # "address": "0x28c9ddF9A3B29c2E6a561c1BC520954e5A33de5D", + # "margin_type": "SM", + # "currency": null + # } + # ], + # "srm_im_discount": "0", + # "srm_mm_discount": "0", + # "pm2_collateral_discounts": [], + # "borrow_apy": "0", + # "supply_apy": "0", + # "total_borrow": "0", + # "total_supply": "0", + # "asset_cap_and_supply_per_manager": { + # "perp": { + # "SM": [ + # { + # "current_open_interest": "0", + # "interest_cap": "2000000", + # "manager_currency": null + # } + # ] + # }, + # "option": {}, + # "erc20": {} + # }, + # "market_type": "SRM_PERP_ONLY", + # "spot_price": "0.2193542905042081", + # "spot_price_24h": "0.238381655533635830" + # }, + # "id": "7e07fe1d-0ab4-4d2b-9e22-b65ce9e232dc" + # } + # + currencies = self.safe_list(tokenResponse, 'result', []) + for i in range(0, len(currencies)): + currency = currencies[i] + currencyId = self.safe_string(currency, 'currency') + code = self.safe_currency_code(currencyId) + result[code] = self.safe_currency_structure({ + 'id': currencyId, + 'name': None, + 'code': code, + 'precision': None, + 'active': None, + 'fee': None, + 'networks': None, + 'deposit': None, + 'withdraw': None, + 'limits': { + 'deposit': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + }, + 'info': currency, + }) + return result + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for bybit + + https://docs.derive.xyz/reference/post_public-get-all-instruments + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + spotMarketsPromise = self.fetch_spot_markets(params) + swapMarketsPromise = self.fetch_swap_markets(params) + optionMarketsPromise = self.fetch_option_markets(params) + spotMarkets, swapMarkets, optionMarkets = await asyncio.gather(*[spotMarketsPromise, swapMarketsPromise, optionMarketsPromise]) + # + # { + # "result": { + # "instruments": [ + # { + # "instrument_type": "perp", + # "instrument_name": "BTC-PERP", + # "scheduled_activation": 1701840228, + # "scheduled_deactivation": 9223372036854776000, + # "is_active": True, + # "tick_size": "0.1", + # "minimum_amount": "0.01", + # "maximum_amount": "10000", + # "amount_step": "0.001", + # "mark_price_fee_rate_cap": "0", + # "maker_fee_rate": "0.00005", + # "taker_fee_rate": "0.0003", + # "base_fee": "0.1", + # "base_currency": "BTC", + # "quote_currency": "USD", + # "option_details": null, + # "perp_details": { + # "index": "BTC-USD", + # "max_rate_per_hour": "0.004", + # "min_rate_per_hour": "-0.004", + # "static_interest_rate": "0.0000125", + # "aggregate_funding": "10538.574363381759146829", + # "funding_rate": "0.0000125" + # }, + # "erc20_details": null, + # "base_asset_address": "0xDBa83C0C654DB1cd914FA2710bA743e925B53086", + # "base_asset_sub_id": "0", + # "pro_rata_fraction": "0", + # "fifo_min_allocation": "0", + # "pro_rata_amount_step": "0.1" + # } + # ], + # "pagination": { + # "num_pages": 1, + # "count": 1 + # } + # }, + # "id": "a06bc0b2-8e78-4536-a21f-f785f225b5a5" + # } + # + result = self.array_concat(spotMarkets, swapMarkets) + result = self.array_concat(result, optionMarkets) + return result + + async def fetch_spot_markets(self, params={}) -> List[Market]: + request: dict = { + 'expired': False, + 'instrument_type': 'erc20', + } + response = await self.publicPostGetAllInstruments(self.extend(request, params)) + result = self.safe_dict(response, 'result', {}) + data = self.safe_list(result, 'instruments', []) + return self.parse_markets(data) + + async def fetch_swap_markets(self, params={}) -> List[Market]: + request: dict = { + 'expired': False, + 'instrument_type': 'perp', + } + response = await self.publicPostGetAllInstruments(self.extend(request, params)) + result = self.safe_dict(response, 'result', {}) + data = self.safe_list(result, 'instruments', []) + return self.parse_markets(data) + + async def fetch_option_markets(self, params={}) -> List[Market]: + request: dict = { + 'expired': False, + 'instrument_type': 'option', + } + response = await self.publicPostGetAllInstruments(self.extend(request, params)) + result = self.safe_dict(response, 'result', {}) + data = self.safe_list(result, 'instruments', []) + return self.parse_markets(data) + + def parse_market(self, market: dict) -> Market: + type = self.safe_string(market, 'instrument_type') + marketType: MarketType + spot = False + margin = True + swap = False + option = False + linear: Bool = None + inverse: Bool = None + baseId = self.safe_string(market, 'base_currency') + quoteId = self.safe_string(market, 'quote_currency') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + marketId = self.safe_string(market, 'instrument_name') + symbol = base + '/' + quote + settleId: Str = None + settle: Str = None + expiry: Num = None + strike: Num = None + optionType: Str = None + optionLetter: Str = None + if type == 'erc20': + spot = True + marketType = 'spot' + elif type == 'perp': + margin = False + settleId = 'USDC' + settle = self.safe_currency_code(settleId) + symbol = base + '/' + quote + ':' + settle + swap = True + linear = True + inverse = False + marketType = 'swap' + elif type == 'option': + settleId = 'USDC' + settle = self.safe_currency_code(settleId) + margin = False + option = True + marketType = 'option' + optionDetails = self.safe_dict(market, 'option_details') + expiry = self.safe_timestamp(optionDetails, 'expiry') + strike = self.safe_integer(optionDetails, 'strike') + optionLetter = self.safe_string(optionDetails, 'option_type') + symbol = base + '/' + quote + ':' + settle + '-' + self.yymmdd(expiry) + '-' + self.number_to_string(strike) + '-' + optionLetter + if optionLetter == 'P': + optionType = 'put' + else: + optionType = 'call' + linear = True + inverse = False + return self.safe_market_structure({ + 'id': marketId, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': marketType, + 'spot': spot, + 'margin': margin, + 'swap': swap, + 'future': False, + 'option': option, + 'active': self.safe_bool(market, 'is_active'), + 'contract': (swap or option), + 'linear': linear, + 'inverse': inverse, + 'contractSize': None if (spot) else 1, + 'expiry': expiry, + 'expiryDatetime': self.iso8601(expiry), + 'taker': self.safe_number(market, 'taker_fee_rate'), + 'maker': self.safe_number(market, 'maker_fee_rate'), + 'strike': strike, + 'optionType': optionType, + 'precision': { + 'amount': self.safe_number(market, 'amount_step'), + 'price': self.safe_number(market, 'tick_size'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(market, 'minimum_amount'), + 'max': self.safe_number(market, 'maximum_amount'), + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + 'info': market, + }) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://docs.derive.xyz/reference/post_public-get-ticker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'instrument_name': market['id'], + } + response = await self.publicPostGetTicker(self.extend(request, params)) + # + # spot + # + # { + # "result": { + # "instrument_type": "perp", + # "instrument_name": "BTC-PERP", + # "scheduled_activation": 1701840228, + # "scheduled_deactivation": 9223372036854776000, + # "is_active": True, + # "tick_size": "0.1", + # "minimum_amount": "0.01", + # "maximum_amount": "10000", + # "amount_step": "0.001", + # "mark_price_fee_rate_cap": "0", + # "maker_fee_rate": "0.00005", + # "taker_fee_rate": "0.0003", + # "base_fee": "0.1", + # "base_currency": "BTC", + # "quote_currency": "USD", + # "option_details": null, + # "perp_details": { + # "index": "BTC-USD", + # "max_rate_per_hour": "0.004", + # "min_rate_per_hour": "-0.004", + # "static_interest_rate": "0.0000125", + # "aggregate_funding": "10512.580833189805742522", + # "funding_rate": "-0.000022223906766867" + # }, + # "erc20_details": null, + # "base_asset_address": "0xDBa83C0C654DB1cd914FA2710bA743e925B53086", + # "base_asset_sub_id": "0", + # "pro_rata_fraction": "0", + # "fifo_min_allocation": "0", + # "pro_rata_amount_step": "0.1", + # "best_ask_amount": "0.012", + # "best_ask_price": "99567.9", + # "best_bid_amount": "0.129", + # "best_bid_price": "99554.5", + # "five_percent_bid_depth": "11.208", + # "five_percent_ask_depth": "11.42", + # "option_pricing": null, + # "index_price": "99577.2", + # "mark_price": "99543.642926357933902181684970855712890625", + # "stats": { + # "contract_volume": "464.712", + # "num_trades": "10681", + # "open_interest": "72.804739389481989861", + # "high": "99519.1", + # "low": "97254.1", + # "percent_change": "0.0128", + # "usd_change": "1258.1" + # }, + # "timestamp": 1736140984000, + # "min_price": "97591.2", + # "max_price": "101535.1" + # }, + # "id": "bbd7c271-c2be-48f7-b93a-26cf6d4cb79f" + # } + # + data = self.safe_dict(response, 'result', {}) + return self.parse_ticker(data, market) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "instrument_type": "perp", + # "instrument_name": "BTC-PERP", + # "scheduled_activation": 1701840228, + # "scheduled_deactivation": 9223372036854776000, + # "is_active": True, + # "tick_size": "0.1", + # "minimum_amount": "0.01", + # "maximum_amount": "10000", + # "amount_step": "0.001", + # "mark_price_fee_rate_cap": "0", + # "maker_fee_rate": "0.00005", + # "taker_fee_rate": "0.0003", + # "base_fee": "0.1", + # "base_currency": "BTC", + # "quote_currency": "USD", + # "option_details": null, + # "perp_details": { + # "index": "BTC-USD", + # "max_rate_per_hour": "0.004", + # "min_rate_per_hour": "-0.004", + # "static_interest_rate": "0.0000125", + # "aggregate_funding": "10512.580833189805742522", + # "funding_rate": "-0.000022223906766867" + # }, + # "erc20_details": null, + # "base_asset_address": "0xDBa83C0C654DB1cd914FA2710bA743e925B53086", + # "base_asset_sub_id": "0", + # "pro_rata_fraction": "0", + # "fifo_min_allocation": "0", + # "pro_rata_amount_step": "0.1", + # "best_ask_amount": "0.012", + # "best_ask_price": "99567.9", + # "best_bid_amount": "0.129", + # "best_bid_price": "99554.5", + # "five_percent_bid_depth": "11.208", + # "five_percent_ask_depth": "11.42", + # "option_pricing": null, + # "index_price": "99577.2", + # "mark_price": "99543.642926357933902181684970855712890625", + # "stats": { + # "contract_volume": "464.712", + # "num_trades": "10681", + # "open_interest": "72.804739389481989861", + # "high": "99519.1", + # "low": "97254.1", + # "percent_change": "0.0128", + # "usd_change": "1258.1" + # }, + # "timestamp": 1736140984000, + # "min_price": "97591.2", + # "max_price": "101535.1" + # } + # + marketId = self.safe_string(ticker, 'instrument_name') + timestamp = self.safe_integer_omit_zero(ticker, 'timestamp') + symbol = self.safe_symbol(marketId, market) + stats = self.safe_dict(ticker, 'stats') + change = self.safe_string(stats, 'percent_change') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(stats, 'high'), + 'low': self.safe_string(stats, 'low'), + 'bid': self.safe_string(ticker, 'best_bid_price'), + 'bidVolume': self.safe_string(ticker, 'best_bid_amount'), + 'ask': self.safe_string(ticker, 'best_ask_price'), + 'askVolume': self.safe_string(ticker, 'best_ask_amount'), + 'vwap': None, + 'open': None, + 'close': None, + 'last': None, + 'previousClose': None, + 'change': change, + 'percentage': Precise.string_mul(change, '100'), + 'average': None, + 'baseVolume': None, + 'quoteVolume': None, + 'indexPrice': self.safe_string(ticker, 'index_price'), + 'markPrice': self.safe_string(ticker, 'mark_price'), + 'info': ticker, + }, market) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://docs.derive.xyz/reference/post_public-get-trade-history + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch trades for + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['instrument_name'] = market['id'] + if limit is not None: + if limit > 1000: + limit = 1000 + request['page_size'] = limit # default 100, max 1000 + if since is not None: + request['from_timestamp'] = since + until = self.safe_integer(params, 'until') + params = self.omit(params, ['until']) + if until is not None: + request['to_timestamp'] = until + response = await self.publicPostGetTradeHistory(self.extend(request, params)) + # + # { + # "result": { + # "trades": [ + # { + # "trade_id": "9dbc88b0-f0c4-4439-9cc1-4e6409d4eafb", + # "instrument_name": "BTC-PERP", + # "timestamp": 1736153910930, + # "trade_price": "98995.3", + # "trade_amount": "0.033", + # "mark_price": "98990.875914388161618263", + # "index_price": "99038.050611100001501184", + # "direction": "sell", + # "quote_id": null, + # "wallet": "0x88B6BB87fbFac92a34F8155aaA35c87B5b166fA9", + # "subaccount_id": 8250, + # "tx_status": "settled", + # "tx_hash": "0x020bd735b312f867f17f8cc254946d87cfe9f2c8ff3605035d8129082eb73723", + # "trade_fee": "0.980476701049890015", + # "liquidity_role": "taker", + # "realized_pnl": "-2.92952402688793509", + # "realized_pnl_excl_fees": "-1.949047325838045075" + # } + # ], + # "pagination": { + # "num_pages": 598196, + # "count": 598196 + # } + # }, + # "id": "b8539544-6975-4497-8163-5e51a38e4aa7" + # } + # + result = self.safe_dict(response, 'result', {}) + data = self.safe_list(result, 'trades', []) + return self.parse_trades(data, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # { + # "subaccount_id": 130837, + # "order_id": "30c48194-8d48-43ac-ad00-0d5ba29eddc9", + # "instrument_name": "BTC-PERP", + # "direction": "sell", + # "label": "test1234", + # "quote_id": null, + # "trade_id": "f8a30740-488c-4c2d-905d-e17057bafde1", + # "timestamp": 1738065303708, + # "mark_price": "102740.137375457314192317", + # "index_price": "102741.553409299981533184", + # "trade_price": "102700.6", + # "trade_amount": "0.01", + # "liquidity_role": "taker", + # "realized_pnl": "0", + # "realized_pnl_excl_fees": "0", + # "is_transfer": False, + # "tx_status": "settled", + # "trade_fee": "1.127415534092999815", + # "tx_hash": "0xc55df1f07330faf86579bd8a6385391fbe9e73089301149d8550e9d29c9ead74", + # "transaction_id": "e18b9426-3fa5-41bb-99d3-8b54fb4d51bb" + # } + # + marketId = self.safe_string(trade, 'instrument_name') + symbol = self.safe_symbol(marketId, market) + timestamp = self.safe_integer(trade, 'timestamp') + fee = { + 'currency': 'USDC', + 'cost': self.safe_string(trade, 'trade_fee'), + } + return self.safe_trade({ + 'info': trade, + 'id': self.safe_string(trade, 'trade_id'), + 'order': self.safe_string(trade, 'order_id'), + 'symbol': symbol, + 'side': self.safe_string_lower(trade, 'direction'), + 'type': None, + 'takerOrMaker': self.safe_string(trade, 'liquidity_role'), + 'price': self.safe_string(trade, 'trade_price'), + 'amount': self.safe_string(trade, 'trade_amount'), + 'cost': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'fee': fee, + }, market) + + async def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical funding rate prices + + https://docs.derive.xyz/reference/post_public-get-funding-rate-history + + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: timestamp in ms of the earliest funding rate to fetch + :param int [limit]: the maximum amount of funding rate structures to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `funding rate structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'instrument_name': market['id'], + } + if since is not None: + request['start_timestamp'] = since + until = self.safe_integer(params, 'until') + params = self.omit(params, ['until']) + if until is not None: + request['to_timestamp'] = until + response = await self.publicPostGetFundingRateHistory(self.extend(request, params)) + # + # { + # "result": { + # "funding_rate_history": [ + # { + # "timestamp": 1736215200000, + # "funding_rate": "-0.000020014" + # } + # ] + # }, + # "id": "3200ab8d-0080-42f0-8517-c13e3d9201d8" + # } + # + result = self.safe_dict(response, 'result', {}) + data = self.safe_list(result, 'funding_rate_history', []) + rates = [] + for i in range(0, len(data)): + entry = data[i] + timestamp = self.safe_integer(entry, 'timestamp') + rates.append({ + 'info': entry, + 'symbol': market['symbol'], + 'fundingRate': self.safe_number(entry, 'funding_rate'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + sorted = self.sort_by(rates, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, market['symbol'], since, limit) + + async def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate + + https://docs.derive.xyz/reference/post_public-get-funding-rate-history + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + response = await self.fetch_funding_rate_history(symbol, None, 1, params) + # + # [ + # { + # "info": { + # "timestamp": 1736157600000, + # "funding_rate": "-0.000008872" + # }, + # "symbol": "BTC/USD:USDC", + # "fundingRate": -0.000008872, + # "timestamp": 1736157600000, + # "datetime": "2025-01-06T10:00:00.000Z" + # } + # ] + # + data = self.safe_dict(response, 0) + return self.parse_funding_rate(data) + + def parse_funding_rate(self, contract, market: Market = None) -> FundingRate: + symbol = self.safe_string(contract, 'symbol') + fundingTimestamp = self.safe_integer(contract, 'timestamp') + return { + 'info': contract, + 'symbol': symbol, + 'markPrice': None, + 'indexPrice': None, + 'interestRate': None, + 'estimatedSettlePrice': None, + 'timestamp': None, + 'datetime': None, + 'fundingRate': self.safe_number(contract, 'fundingRate'), + 'fundingTimestamp': fundingTimestamp, + 'fundingDatetime': self.iso8601(fundingTimestamp), + 'nextFundingRate': None, + 'nextFundingTimestamp': None, + 'nextFundingDatetime': None, + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': None, + } + + def hash_order_message(self, order): + accountHash = self.hash(self.eth_abi_encode([ + 'bytes32', 'uint256', 'uint256', 'address', 'bytes32', 'uint256', 'address', 'address', + ], order), 'keccak', 'binary') + sandboxMode = self.safe_bool(self.options, 'sandboxMode', False) + DOMAIN_SEPARATOR = '9bcf4dc06df5d8bf23af818d5716491b995020f377d3b7b64c29ed14e3dd1105' if (sandboxMode) else 'd96e5f90797da7ec8dc4e276260c7f3f87fedf68775fbe1ef116e996fc60441b' + binaryDomainSeparator = self.base16_to_binary(DOMAIN_SEPARATOR) + prefix = self.base16_to_binary('1901') + return self.hash(self.binary_concat(prefix, binaryDomainSeparator, accountHash), 'keccak', 'hex') + + def sign_order(self, order, privateKey): + hashOrder = self.hash_order_message(order) + return self.sign_hash(hashOrder[-64:], privateKey[-64:]) + + def hash_message(self, message): + binaryMessage = self.encode(message) + binaryMessageLength = self.binary_length(binaryMessage) + x19 = self.base16_to_binary('19') + newline = self.base16_to_binary('0a') + prefix = self.binary_concat(x19, self.encode('Ethereum Signed Message:'), newline, self.encode(self.number_to_string(binaryMessageLength))) + return '0x' + self.hash(self.binary_concat(prefix, binaryMessage), 'keccak', 'hex') + + def sign_hash(self, hash, privateKey): + self.check_required_credentials() + signature = self.ecdsa(hash[-64:], privateKey[-64:], 'secp256k1', None) + r = signature['r'] + s = signature['s'] + v = self.int_to_base16(self.sum(27, signature['v'])) + return '0x' + r.rjust(64, '0') + s.rjust(64, '0') + v + + def sign_message(self, message, privateKey): + return self.sign_hash(self.hash_message(message), privateKey[-64:]) + + def parse_units(self, num: str, dec='1000000000000000000'): + return Precise.string_mul(num, dec) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://docs.derive.xyz/reference/post_private-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.subaccount_id]: *required* the subaccount id + :param float [params.triggerPrice]: The price a trigger order is triggered at + :param dict [params.takeProfit]: *takeProfit object in params* containing the triggerPrice at which the attached take profit order will be triggered(perpetual swap markets only) + :param float [params.takeProfit.triggerPrice]: take profit trigger price + :param dict [params.stopLoss]: *stopLoss object in params* containing the triggerPrice at which the attached stop loss order will be triggered(perpetual swap markets only) + :param float [params.stopLoss.triggerPrice]: stop loss trigger price + :param float [params.max_fee]: *required* the maximum fee you are willing to pay for the order + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + if price is None: + raise ArgumentsRequired(self.id + ' createOrder() requires a price argument') + subaccountId = None + subaccountId, params = self.handle_derive_subaccount_id('createOrder', params) + test = self.safe_bool(params, 'test', False) + reduceOnly = self.safe_bool_2(params, 'reduceOnly', 'reduce_only') + timeInForce = self.safe_string_lower_2(params, 'timeInForce', 'time_in_force') + postOnly = self.safe_bool(params, 'postOnly') + orderType = type.lower() + orderSide = side.lower() + nonce = self.milliseconds() + # Order signature expiry must be between 2592000 and 7776000 sec from now + signatureExpiry = self.safe_integer(params, 'signature_expiry_sec', self.seconds() + 7776000) + ACTION_TYPEHASH = self.base16_to_binary('4d7a9f27c403ff9c0f19bce61d76d82f9aa29f8d6d4b0c5474607d9770d1af17') + sandboxMode = self.safe_bool(self.options, 'sandboxMode', False) + TRADE_MODULE_ADDRESS = '0x87F2863866D85E3192a35A73b388BD625D83f2be' if (sandboxMode) else '0xB8D20c2B7a1Ad2EE33Bc50eF10876eD3035b5e7b' + priceString = self.number_to_string(price) + maxFee = None + maxFee, params = self.handle_option_and_params(params, 'createOrder', 'max_fee') + if maxFee is None: + raise ArgumentsRequired(self.id + ' createOrder() requires a max_fee argument in params') + maxFeeString = self.number_to_string(maxFee) + amountString = self.number_to_string(amount) + tradeModuleDataHash = self.hash(self.eth_abi_encode([ + 'address', 'uint', 'int', 'int', 'uint', 'uint', 'bool', + ], [ + market['info']['base_asset_address'], + self.parse_to_numeric(market['info']['base_asset_sub_id']), + self.convert_to_big_int(self.parse_units(priceString)), + self.convert_to_big_int(self.parse_units(self.amount_to_precision(symbol, amountString))), + self.convert_to_big_int(self.parse_units(maxFeeString)), + subaccountId, + orderSide == 'buy', + ]), 'keccak', 'binary') + deriveWalletAddress = None + deriveWalletAddress, params = self.handle_derive_wallet_address('createOrder', params) + signature = self.sign_order([ + ACTION_TYPEHASH, + subaccountId, + nonce, + TRADE_MODULE_ADDRESS, + tradeModuleDataHash, + signatureExpiry, + deriveWalletAddress, + self.walletAddress, + ], self.privateKey) + request: dict = { + 'instrument_name': market['id'], + 'direction': orderSide, + 'order_type': orderType, + 'nonce': nonce, + 'amount': amountString, + 'limit_price': priceString, + 'max_fee': maxFeeString, + 'subaccount_id': subaccountId, + 'signature_expiry_sec': signatureExpiry, + 'referral_code': self.safe_string(self.options, 'id', '0x0ad42b8e602c2d3d475ae52d678cf63d84ab2749'), + 'signer': self.walletAddress, + } + if reduceOnly is not None: + request['reduce_only'] = reduceOnly + if reduceOnly and postOnly: + raise InvalidOrder(self.id + ' cannot use reduce only with post only time in force') + if postOnly is not None: + request['time_in_force'] = 'post_only' + elif timeInForce is not None: + request['time_in_force'] = timeInForce + stopLoss = self.safe_value(params, 'stopLoss') + takeProfit = self.safe_value(params, 'takeProfit') + triggerPriceType = self.safe_string(params, 'trigger_price_type', 'mark') + if stopLoss is not None: + stopLossPrice = self.safe_string(stopLoss, 'triggerPrice', stopLoss) + request['trigger_price'] = stopLossPrice + request['trigger_type'] = 'stoploss' + request['trigger_price_type'] = triggerPriceType + elif takeProfit is not None: + takeProfitPrice = self.safe_string(takeProfit, 'triggerPrice', takeProfit) + request['trigger_price'] = takeProfitPrice + request['trigger_type'] = 'takeprofit' + request['trigger_price_type'] = triggerPriceType + clientOrderId = self.safe_string(params, 'clientOrderId') + if clientOrderId is not None: + request['label'] = clientOrderId + request['signature'] = signature + params = self.omit(params, ['reduceOnly', 'reduce_only', 'timeInForce', 'time_in_force', 'postOnly', 'test', 'clientOrderId', 'stopPrice', 'triggerPrice', 'trigger_price', 'stopLoss', 'takeProfit', 'trigger_price_type']) + response = None + if test: + response = await self.privatePostOrderDebug(self.extend(request, params)) + else: + response = await self.privatePostOrder(self.extend(request, params)) + # + # { + # "result": { + # "raw_data": { + # "subaccount_id": 130837, + # "nonce": 1736923517552, + # "module": "0x87F2863866D85E3192a35A73b388BD625D83f2be", + # "expiry": 86400, + # "owner": "0x108b9aF9279a525b8A8AeAbE7AC2bA925Bc50075", + # "signer": "0x108b9aF9279a525b8A8AeAbE7AC2bA925Bc50075", + # "signature": "0xaa4f42b2f3da33c668fa703ea872d4c3a6b55aca66025b5119e3bebb6679fe2e2794638db51dcace21fc39a498047835994f07eb59f311bb956ce057e66793d1c", + # "data": { + # "asset": "0xAFB6Bb95cd70D5367e2C39e9dbEb422B9815339D", + # "sub_id": 0, + # "limit_price": "10000", + # "desired_amount": "0.001", + # "worst_fee": "0", + # "recipient_id": 130837, + # "is_bid": True, + # "trade_id": "" + # } + # }, + # "encoded_data": "0x000000000000000000000000afb6bb95cd70d5367e2c39e9dbeb422b9815339d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000021e19e0c9bab240000000000000000000000000000000000000000000000000000000038d7ea4c680000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001ff150000000000000000000000000000000000000000000000000000000000000001", + # "encoded_data_hashed": "0xe88fb416bc54dba2d288988f1a82fee40fd792ed555b3471b5f6b4b810d279b4", + # "action_hash": "0x273a0befb3751fa991edc7ed73582456c3b50ae964d458c8f472e932fb6a0069", + # "typed_data_hash": "0x123e2d2f3d5b2473b4e260f51c6459d6bf904e5db8f042a3ea63be8d55329ce9" + # }, + # "id": "f851c8c4-dddf-4b77-93cf-aeddd0966f29" + # } + # { + # "result": { + # "order": { + # "subaccount_id": 130837, + # "order_id": "96349ebb-7d46-43ae-81c7-7ab390444293", + # "instrument_name": "BTC-PERP", + # "direction": "buy", + # "label": "", + # "quote_id": null, + # "creation_timestamp": 1737467576257, + # "last_update_timestamp": 1737467576257, + # "limit_price": "10000", + # "amount": "0.01", + # "filled_amount": "0", + # "average_price": "0", + # "order_fee": "0", + # "order_type": "limit", + # "time_in_force": "gtc", + # "order_status": "open", + # "max_fee": "210", + # "signature_expiry_sec": 1737468175989, + # "nonce": 1737467575989, + # "signer": "0x30CB7B06AdD6749BbE146A6827502B8f2a79269A", + # "signature": "0xd1ca49df1fa06bd805bb59b132ff6c0de29bf973a3e01705abe0a01cc956e4945ed9eb99ab68f3df4c037908113cac5a5bfc3a954a0b7103cdab285962fa6a51c", + # "cancel_reason": "", + # "mmp": False, + # "is_transfer": False, + # "replaced_order_id": null, + # "trigger_type": null, + # "trigger_price_type": null, + # "trigger_price": null, + # "trigger_reject_message": null + # }, + # "trades": [] + # }, + # "id": "397087fa-0125-42af-bfc3-f66166f9fb55" + # } + # + result = self.safe_dict(response, 'result') + rawOrder = self.safe_dict(result, 'raw_data') + if rawOrder is None: + rawOrder = self.safe_dict(result, 'order') + order = self.parse_order(rawOrder, market) + order['type'] = type + return order + + async def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + """ + edit a trade order + + https://docs.derive.xyz/reference/post_private-replace + + :param str id: order id + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.subaccount_id]: *required* the subaccount id + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + subaccountId = None + subaccountId, params = self.handle_derive_subaccount_id('editOrder', params) + reduceOnly = self.safe_bool_2(params, 'reduceOnly', 'reduce_only') + timeInForce = self.safe_string_lower_2(params, 'timeInForce', 'time_in_force') + postOnly = self.safe_bool(params, 'postOnly') + orderType = type.lower() + orderSide = side.lower() + nonce = self.milliseconds() + signatureExpiry = self.safe_number(params, 'signature_expiry_sec', self.seconds() + 7776000) + # TODO: subaccount id / trade module address + ACTION_TYPEHASH = self.base16_to_binary('4d7a9f27c403ff9c0f19bce61d76d82f9aa29f8d6d4b0c5474607d9770d1af17') + sandboxMode = self.safe_bool(self.options, 'sandboxMode', False) + TRADE_MODULE_ADDRESS = '0x87F2863866D85E3192a35A73b388BD625D83f2be' if (sandboxMode) else '0xB8D20c2B7a1Ad2EE33Bc50eF10876eD3035b5e7b' + priceString = self.number_to_string(price) + maxFeeString = self.safe_string(params, 'max_fee', '0') + amountString = self.number_to_string(amount) + tradeModuleDataHash = self.hash(self.eth_abi_encode([ + 'address', 'uint', 'int', 'int', 'uint', 'uint', 'bool', + ], [ + market['info']['base_asset_address'], + self.parse_to_numeric(market['info']['base_asset_sub_id']), + self.convert_to_big_int(self.parse_units(priceString)), + self.convert_to_big_int(self.parse_units(self.amount_to_precision(symbol, amountString))), + self.convert_to_big_int(self.parse_units(maxFeeString)), + subaccountId, + orderSide == 'buy', + ]), 'keccak', 'binary') + deriveWalletAddress = None + deriveWalletAddress, params = self.handle_derive_wallet_address('editOrder', params) + signature = self.sign_order([ + ACTION_TYPEHASH, + subaccountId, + nonce, + TRADE_MODULE_ADDRESS, + tradeModuleDataHash, + signatureExpiry, + deriveWalletAddress, + self.walletAddress, + ], self.privateKey) + request: dict = { + 'instrument_name': market['id'], + 'order_id_to_cancel': id, + 'direction': orderSide, + 'order_type': orderType, + 'nonce': nonce, + 'amount': amountString, + 'limit_price': priceString, + 'max_fee': maxFeeString, + 'subaccount_id': subaccountId, + 'signature_expiry_sec': signatureExpiry, + 'signer': self.walletAddress, + } + if reduceOnly is not None: + request['reduce_only'] = reduceOnly + if reduceOnly and postOnly: + raise InvalidOrder(self.id + ' cannot use reduce only with post only time in force') + if postOnly is not None: + request['time_in_force'] = 'post_only' + elif timeInForce is not None: + request['time_in_force'] = timeInForce + clientOrderId = self.safe_string(params, 'clientOrderId') + if clientOrderId is not None: + request['label'] = clientOrderId + request['signature'] = signature + params = self.omit(params, ['reduceOnly', 'reduce_only', 'timeInForce', 'time_in_force', 'postOnly', 'clientOrderId']) + response = await self.privatePostReplace(self.extend(request, params)) + # + # { + # "result": + # { + # "cancelled_order": + # { + # "subaccount_id": 130837, + # "order_id": "c2337704-f1af-437d-91c8-dddb9d6bac59", + # "instrument_name": "BTC-PERP", + # "direction": "buy", + # "label": "test1234", + # "quote_id": null, + # "creation_timestamp": 1737539743959, + # "last_update_timestamp": 1737539764234, + # "limit_price": "10000", + # "amount": "0.01", + # "filled_amount": "0", + # "average_price": "0", + # "order_fee": "0", + # "order_type": "limit", + # "time_in_force": "post_only", + # "order_status": "cancelled", + # "max_fee": "211", + # "signature_expiry_sec": 1737540343631, + # "nonce": 1737539743631, + # "signer": "0x30CB7B06AdD6749BbE146A6827502B8f2a79269A", + # "signature": "0xdb669e18f407a3efa816b79c0dd3bac1c651d4dbf3caad4db67678ce9b81c76378d787a08143a30707eb0827ce4626640767c9f174358df1b90611bd6d1391711b", + # "cancel_reason": "user_request", + # "mmp": False, + # "is_transfer": False, + # "replaced_order_id": null, + # "trigger_type": null, + # "trigger_price_type": null, + # "trigger_price": null, + # "trigger_reject_message": null, + # }, + # "order": + # { + # "subaccount_id": 130837, + # "order_id": "97af0902-813f-4892-a54b-797e5689db05", + # "instrument_name": "BTC-PERP", + # "direction": "buy", + # "label": "test1234", + # "quote_id": null, + # "creation_timestamp": 1737539764154, + # "last_update_timestamp": 1737539764154, + # "limit_price": "10000", + # "amount": "0.01", + # "filled_amount": "0", + # "average_price": "0", + # "order_fee": "0", + # "order_type": "limit", + # "time_in_force": "post_only", + # "order_status": "open", + # "max_fee": "211", + # "signature_expiry_sec": 1737540363890, + # "nonce": 1737539763890, + # "signer": "0x30CB7B06AdD6749BbE146A6827502B8f2a79269A", + # "signature": "0xef2c459ab4797cbbd7d97b47678ff172542af009bac912bf53e7879cf92eb1aa6b1a6cf40bf0928684f5394942fb424cc2db71eac0eaf7226a72480034332f291c", + # "cancel_reason": "", + # "mmp": False, + # "is_transfer": False, + # "replaced_order_id": "c2337704-f1af-437d-91c8-dddb9d6bac59", + # "trigger_type": null, + # "trigger_price_type": null, + # "trigger_price": null, + # "trigger_reject_message": null, + # }, + # "trades": [], + # "create_order_error": null, + # }, + # "id": "fb19e991-15f6-4c80-a20c-917e762a1a38", + # } + # + result = self.safe_dict(response, 'result') + rawOrder = self.safe_dict(result, 'order') + order = self.parse_order(rawOrder, market) + return order + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + + https://docs.derive.xyz/reference/post_private-cancel + + cancels an open order + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: whether the order is a trigger/algo order + :param str [params.subaccount_id]: *required* the subaccount id + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + await self.load_markets() + market: Market = self.market(symbol) + isTrigger = self.safe_bool_2(params, 'trigger', 'stop', False) + subaccountId = None + subaccountId, params = self.handle_derive_subaccount_id('cancelOrder', params) + params = self.omit(params, ['trigger', 'stop']) + request: dict = { + 'instrument_name': market['id'], + 'subaccount_id': subaccountId, + } + clientOrderIdUnified = self.safe_string(params, 'clientOrderId') + clientOrderIdExchangeSpecific = self.safe_string(params, 'label', clientOrderIdUnified) + isByClientOrder = clientOrderIdExchangeSpecific is not None + response = None + if isByClientOrder: + request['label'] = clientOrderIdExchangeSpecific + params = self.omit(params, ['clientOrderId', 'label']) + response = await self.privatePostCancelByLabel(self.extend(request, params)) + else: + request['order_id'] = id + if isTrigger: + response = await self.privatePostCancelTriggerOrder(self.extend(request, params)) + else: + response = await self.privatePostCancel(self.extend(request, params)) + # + # { + # "result": { + # "subaccount_id": 130837, + # "order_id": "de4f30b6-0dcb-4df6-9222-c1a27f1ad80d", + # "instrument_name": "BTC-PERP", + # "direction": "buy", + # "label": "test1234", + # "quote_id": null, + # "creation_timestamp": 1737540100989, + # "last_update_timestamp": 1737540574696, + # "limit_price": "10000", + # "amount": "0.01", + # "filled_amount": "0", + # "average_price": "0", + # "order_fee": "0", + # "order_type": "limit", + # "time_in_force": "post_only", + # "order_status": "cancelled", + # "max_fee": "211", + # "signature_expiry_sec": 1737540700726, + # "nonce": 1737540100726, + # "signer": "0x30CB7B06AdD6749BbE146A6827502B8f2a79269A", + # "signature": "0x9cd1a6e32a0699929e4e090c08c548366b1353701ec56e02d5cdf37fc89bd19b7b29e00e57e8383bb6336d73019027a7e2a4364f40859e7a949115024c7f199a1b", + # "cancel_reason": "user_request", + # "mmp": False, + # "is_transfer": False, + # "replaced_order_id": "4ccc89ba-3c3d-4047-8900-0aa5fb4ef706", + # "trigger_type": null, + # "trigger_price_type": null, + # "trigger_price": null, + # "trigger_reject_message": null + # }, + # "id": "cef61e2a-cb13-4779-8e6b-535361981fad" + # } + # + # { + # "result": { + # "cancelled_orders": 1 + # }, + # "id": "674e075e-1e8a-4a47-99ff-75efbdd2370f" + # } + # + extendParams: dict = {'symbol': symbol} + order = self.safe_dict(response, 'result') + if isByClientOrder: + extendParams['client_order_id'] = clientOrderIdExchangeSpecific + return self.extend(self.parse_order(order, market), extendParams) + + async def cancel_all_orders(self, symbol: Str = None, params={}): + """ + + https://docs.derive.xyz/reference/post_private-cancel-by-instrument + https://docs.derive.xyz/reference/post_private-cancel-all + + cancel all open orders in a market + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.subaccount_id]: *required* the subaccount id + :returns dict: an list of `order structures ` + """ + await self.load_markets() + market: Market = None + if symbol is not None: + market = self.market(symbol) + subaccountId = None + subaccountId, params = self.handle_derive_subaccount_id('cancelAllOrders', params) + request: dict = { + 'subaccount_id': subaccountId, + } + response = None + if market is not None: + request['instrument_name'] = market['id'] + response = await self.privatePostCancelByInstrument(self.extend(request, params)) + else: + response = await self.privatePostCancelAll(self.extend(request, params)) + # + # { + # "result": { + # "cancelled_orders": 0 + # }, + # "id": "9d633799-2098-4559-b547-605bb6f4d8f4" + # } + # + # { + # "id": "45548646-c74f-4ca2-9de4-551e6de49afa", + # "result": "ok" + # } + # + return [self.safe_order({'info': response})] + + async def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://docs.derive.xyz/reference/post_private-get-orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: set to True if you want to fetch orders with pagination + :param boolean [params.trigger]: whether the order is a trigger/algo order + :param str [params.subaccount_id]: *required* the subaccount id + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOrders', 'paginate') + if paginate: + return await self.fetch_paginated_call_incremental('fetchOrders', symbol, since, limit, params, 'page', 500) + isTrigger = self.safe_bool_2(params, 'trigger', 'stop', False) + params = self.omit(params, ['trigger', 'stop']) + subaccountId = None + subaccountId, params = self.handle_derive_subaccount_id('fetchOrders', params) + request: dict = { + 'subaccount_id': subaccountId, + } + market: Market = None + if symbol is not None: + market = self.market(symbol) + request['instrument_name'] = market['id'] + if limit is not None: + request['page_size'] = limit + else: + request['page_size'] = 500 + if isTrigger: + request['status'] = 'untriggered' + response = await self.privatePostGetOrders(self.extend(request, params)) + # + # { + # "result": { + # "subaccount_id": 130837, + # "orders": [ + # { + # "subaccount_id": 130837, + # "order_id": "63a80cb8-387b-472b-a838-71cd9513c365", + # "instrument_name": "BTC-PERP", + # "direction": "buy", + # "label": "test1234", + # "quote_id": null, + # "creation_timestamp": 1737551053207, + # "last_update_timestamp": 1737551053207, + # "limit_price": "10000", + # "amount": "0.01", + # "filled_amount": "0", + # "average_price": "0", + # "order_fee": "0", + # "order_type": "limit", + # "time_in_force": "post_only", + # "order_status": "open", + # "max_fee": "211", + # "signature_expiry_sec": 1737551652765, + # "nonce": 1737551052765, + # "signer": "0x30CB7B06AdD6749BbE146A6827502B8f2a79269A", + # "signature": "0x35535ccb1bcad509ecc435c79e966174db6403fc9aeee1e237d08a941014c57b59279dfe4be39e081f9921a53eaad59cb2a151d9f52f2d05fc47e6280254952e1c", + # "cancel_reason": "", + # "mmp": False, + # "is_transfer": False, + # "replaced_order_id": null, + # "trigger_type": null, + # "trigger_price_type": null, + # "trigger_price": null, + # "trigger_reject_message": null + # } + # ], + # "pagination": { + # "num_pages": 1, + # "count": 1 + # } + # }, + # "id": "e5a88d4f-7ac7-40cd-aec9-e0e8152b8b92" + # } + # + data = self.safe_value(response, 'result') + page = self.safe_integer(params, 'page') + if page is not None: + pagination = self.safe_dict(data, 'pagination') + currentPage = self.safe_integer(pagination, 'num_pages') + if page > currentPage: + return [] + orders = self.safe_list(data, 'orders') + return self.parse_orders(orders, market, since, limit) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://docs.derive.xyz/reference/post_private-get-orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: set to True if you want to fetch orders with pagination + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + extendedParams = self.extend(params, {'status': 'open'}) + return await self.fetch_orders(symbol, since, limit, extendedParams) + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://docs.derive.xyz/reference/post_private-get-orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: set to True if you want to fetch orders with pagination + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + extendedParams = self.extend(params, {'status': 'filled'}) + return await self.fetch_orders(symbol, since, limit, extendedParams) + + async def fetch_canceled_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches information on multiple canceled orders made by the user + + https://docs.derive.xyz/reference/post_private-get-orders + + :param str symbol: unified market symbol of the market the orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + extendedParams = self.extend(params, {'status': 'cancelled'}) + return await self.fetch_orders(symbol, since, limit, extendedParams) + + def parse_time_in_force(self, timeInForce: Str): + timeInForces: dict = { + 'ioc': 'IOC', + 'fok': 'FOK', + 'gtc': 'GTC', + 'post_only': 'PO', + } + return self.safe_string(timeInForces, timeInForce, None) + + def parse_order_status(self, status: Str): + if status is not None: + statuses: dict = { + 'open': 'open', + 'untriggered': 'open', + 'filled': 'closed', + 'cancelled': 'canceled', + 'expired': 'rejected', + } + return self.safe_string(statuses, status, status) + return status + + def parse_order(self, rawOrder: dict, market: Market = None) -> Order: + # + # { + # "subaccount_id": 130837, + # "nonce": 1736923517552, + # "module": "0x87F2863866D85E3192a35A73b388BD625D83f2be", + # "expiry": 86400, + # "owner": "0x108b9aF9279a525b8A8AeAbE7AC2bA925Bc50075", + # "signer": "0x108b9aF9279a525b8A8AeAbE7AC2bA925Bc50075", + # "signature": "0xaa4f42b2f3da33c668fa703ea872d4c3a6b55aca66025b5119e3bebb6679fe2e2794638db51dcace21fc39a498047835994f07eb59f311bb956ce057e66793d1c", + # "data": { + # "asset": "0xAFB6Bb95cd70D5367e2C39e9dbEb422B9815339D", + # "sub_id": 0, + # "limit_price": "10000", + # "desired_amount": "0.001", + # "worst_fee": "0", + # "recipient_id": 130837, + # "is_bid": True, + # "trade_id": "" + # } + # } + # { + # "subaccount_id": 130837, + # "order_id": "96349ebb-7d46-43ae-81c7-7ab390444293", + # "instrument_name": "BTC-PERP", + # "direction": "buy", + # "label": "", + # "quote_id": null, + # "creation_timestamp": 1737467576257, + # "last_update_timestamp": 1737467576257, + # "limit_price": "10000", + # "amount": "0.01", + # "filled_amount": "0", + # "average_price": "0", + # "order_fee": "0", + # "order_type": "limit", + # "time_in_force": "gtc", + # "order_status": "open", + # "max_fee": "210", + # "signature_expiry_sec": 1737468175989, + # "nonce": 1737467575989, + # "signer": "0x30CB7B06AdD6749BbE146A6827502B8f2a79269A", + # "signature": "0xd1ca49df1fa06bd805bb59b132ff6c0de29bf973a3e01705abe0a01cc956e4945ed9eb99ab68f3df4c037908113cac5a5bfc3a954a0b7103cdab285962fa6a51c", + # "cancel_reason": "", + # "mmp": False, + # "is_transfer": False, + # "replaced_order_id": null, + # "trigger_type": null, + # "trigger_price_type": null, + # "trigger_price": null, + # "trigger_reject_message": null + # } + order = self.safe_dict(rawOrder, 'data') + if order is None: + order = rawOrder + timestamp = self.safe_integer_2(rawOrder, 'creation_timestamp', 'nonce') + orderId = self.safe_string(order, 'order_id') + marketId = self.safe_string(order, 'instrument_name') + if marketId is not None: + market = self.safe_market(marketId, market) + symbol = market['symbol'] + price = self.safe_string(order, 'limit_price') + average = self.safe_string(order, 'average_price') + amount = self.safe_string(order, 'desired_amount') + filled = self.safe_string(order, 'filled_amount') + fee = self.safe_string(order, 'order_fee') + orderType = self.safe_string_lower(order, 'order_type') + isBid = self.safe_bool(order, 'is_bid') + side = self.safe_string(order, 'direction') + if side is None: + if isBid: + side = 'buy' + else: + side = 'sell' + triggerType = self.safe_string(order, 'trigger_type') + stopLossPrice = None + takeProfitPrice = None + triggerPrice = None + if triggerType is not None: + triggerPrice = self.safe_string(order, 'trigger_price') + if triggerType == 'stoploss': + stopLossPrice = triggerPrice + else: + takeProfitPrice = triggerPrice + lastUpdateTimestamp = self.safe_integer(rawOrder, 'last_update_timestamp') + status = self.safe_string(order, 'order_status') + timeInForce = self.safe_string(order, 'time_in_force') + return self.safe_order({ + 'id': orderId, + 'clientOrderId': self.safe_string(order, 'label'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'lastUpdateTimestamp': lastUpdateTimestamp, + 'status': self.parse_order_status(status), + 'symbol': symbol, + 'type': orderType, + 'timeInForce': self.parse_time_in_force(timeInForce), + 'postOnly': None, # handled in safeOrder + 'reduceOnly': self.safe_bool(order, 'reduce_only'), + 'side': side, + 'price': price, + 'triggerPrice': triggerPrice, + 'takeProfitPrice': takeProfitPrice, + 'stopLossPrice': stopLossPrice, + 'average': average, + 'amount': amount, + 'filled': filled, + 'remaining': None, + 'cost': None, + 'trades': None, + 'fee': { + 'cost': fee, + 'currency': 'USDC', + }, + 'info': order, + }, market) + + async def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all the trades made from a single order + + https://docs.derive.xyz/reference/post_private-get-trade-history + + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.subaccount_id]: *required* the subaccount id + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + subaccountId = None + subaccountId, params = self.handle_derive_subaccount_id('fetchOrderTrades', params) + request: dict = { + 'order_id': id, + 'subaccount_id': subaccountId, + } + market: Market = None + if symbol is not None: + market = self.market(symbol) + request['instrument_name'] = market['id'] + if limit is not None: + request['page_size'] = limit + if since is not None: + request['from_timestamp'] = since + response = await self.privatePostGetTradeHistory(self.extend(request, params)) + # + # { + # "result": { + # "subaccount_id": 130837, + # "trades": [ + # { + # "subaccount_id": 130837, + # "order_id": "30c48194-8d48-43ac-ad00-0d5ba29eddc9", + # "instrument_name": "BTC-PERP", + # "direction": "sell", + # "label": "test1234", + # "quote_id": null, + # "trade_id": "f8a30740-488c-4c2d-905d-e17057bafde1", + # "timestamp": 1738065303708, + # "mark_price": "102740.137375457314192317", + # "index_price": "102741.553409299981533184", + # "trade_price": "102700.6", + # "trade_amount": "0.01", + # "liquidity_role": "taker", + # "realized_pnl": "0", + # "realized_pnl_excl_fees": "0", + # "is_transfer": False, + # "tx_status": "settled", + # "trade_fee": "1.127415534092999815", + # "tx_hash": "0xc55df1f07330faf86579bd8a6385391fbe9e73089301149d8550e9d29c9ead74", + # "transaction_id": "e18b9426-3fa5-41bb-99d3-8b54fb4d51bb" + # } + # ], + # "pagination": { + # "num_pages": 1, + # "count": 1 + # } + # }, + # "id": "a16f798c-a121-44e2-b77e-c38a063f8a99" + # } + # + result = self.safe_dict(response, 'result', {}) + trades = self.safe_list(result, 'trades', []) + return self.parse_trades(trades, market, since, limit, params) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://docs.derive.xyz/reference/post_private-get-trade-history + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: set to True if you want to fetch trades with pagination + :param str [params.subaccount_id]: *required* the subaccount id + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchMyTrades', 'paginate') + if paginate: + return await self.fetch_paginated_call_incremental('fetchMyTrades', symbol, since, limit, params, 'page', 500) + subaccountId = None + subaccountId, params = self.handle_derive_subaccount_id('fetchMyTrades', params) + request: dict = { + 'subaccount_id': subaccountId, + } + market: Market = None + if symbol is not None: + market = self.market(symbol) + request['instrument_name'] = market['id'] + if limit is not None: + request['page_size'] = limit + if since is not None: + request['from_timestamp'] = since + response = await self.privatePostGetTradeHistory(self.extend(request, params)) + # + # { + # "result": { + # "subaccount_id": 130837, + # "trades": [ + # { + # "subaccount_id": 130837, + # "order_id": "30c48194-8d48-43ac-ad00-0d5ba29eddc9", + # "instrument_name": "BTC-PERP", + # "direction": "sell", + # "label": "test1234", + # "quote_id": null, + # "trade_id": "f8a30740-488c-4c2d-905d-e17057bafde1", + # "timestamp": 1738065303708, + # "mark_price": "102740.137375457314192317", + # "index_price": "102741.553409299981533184", + # "trade_price": "102700.6", + # "trade_amount": "0.01", + # "liquidity_role": "taker", + # "realized_pnl": "0", + # "realized_pnl_excl_fees": "0", + # "is_transfer": False, + # "tx_status": "settled", + # "trade_fee": "1.127415534092999815", + # "tx_hash": "0xc55df1f07330faf86579bd8a6385391fbe9e73089301149d8550e9d29c9ead74", + # "transaction_id": "e18b9426-3fa5-41bb-99d3-8b54fb4d51bb" + # } + # ], + # "pagination": { + # "num_pages": 1, + # "count": 1 + # } + # }, + # "id": "a16f798c-a121-44e2-b77e-c38a063f8a99" + # } + # + result = self.safe_dict(response, 'result', {}) + page = self.safe_integer(params, 'page') + if page is not None: + pagination = self.safe_dict(result, 'pagination') + currentPage = self.safe_integer(pagination, 'num_pages') + if page > currentPage: + return [] + trades = self.safe_list(result, 'trades', []) + return self.parse_trades(trades, market, since, limit, params) + + async def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://docs.derive.xyz/reference/post_private-get-positions + + :param str[] [symbols]: not used by kraken fetchPositions() + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.subaccount_id]: *required* the subaccount id + :returns dict[]: a list of `position structure ` + """ + await self.load_markets() + subaccountId = None + subaccountId, params = self.handle_derive_subaccount_id('fetchPositions', params) + request: dict = { + 'subaccount_id': subaccountId, + } + params = self.omit(params, ['subaccount_id']) + response = await self.privatePostGetPositions(self.extend(request, params)) + # + # { + # "result": { + # "subaccount_id": 130837, + # "positions": [ + # { + # "instrument_type": "perp", + # "instrument_name": "BTC-PERP", + # "amount": "-0.02", + # "average_price": "102632.9105389869500088", + # "realized_pnl": "0", + # "unrealized_pnl": "-2.6455959784245548835819950103759765625", + # "total_fees": "2.255789220260999824", + # "average_price_excl_fees": "102745.7", + # "realized_pnl_excl_fees": "0", + # "unrealized_pnl_excl_fees": "-0.3898067581635550595819950103759765625", + # "net_settlements": "-4.032902047219498639", + # "cumulative_funding": "-0.004677736347850093", + # "pending_funding": "0", + # "mark_price": "102765.190337908177752979099750518798828125", + # "index_price": "102767.657193800017641472", + # "delta": "1", + # "gamma": "0", + # "vega": "0", + # "theta": "0", + # "mark_value": "1.38730606879471451975405216217041015625", + # "maintenance_margin": "-101.37788426911356509663164615631103515625", + # "initial_margin": "-132.2074413704858670826070010662078857421875", + # "open_orders_margin": "264.116085900726830004714429378509521484375", + # "leverage": "8.6954476205089299495699106539379941746377322586618", + # "liquidation_price": "109125.705451984322280623018741607666015625", + # "creation_timestamp": 1738065303840 + # } + # ] + # }, + # "id": "167350f1-d9fc-41d4-9797-1c78f83fda8e" + # } + # + result = self.safe_dict(response, 'result', {}) + positions = self.safe_list(result, 'positions', []) + return self.parse_positions(positions, symbols) + + def parse_position(self, position: dict, market: Market = None): + # + # { + # "instrument_type": "perp", + # "instrument_name": "BTC-PERP", + # "amount": "-0.02", + # "average_price": "102632.9105389869500088", + # "realized_pnl": "0", + # "unrealized_pnl": "-2.6455959784245548835819950103759765625", + # "total_fees": "2.255789220260999824", + # "average_price_excl_fees": "102745.7", + # "realized_pnl_excl_fees": "0", + # "unrealized_pnl_excl_fees": "-0.3898067581635550595819950103759765625", + # "net_settlements": "-4.032902047219498639", + # "cumulative_funding": "-0.004677736347850093", + # "pending_funding": "0", + # "mark_price": "102765.190337908177752979099750518798828125", + # "index_price": "102767.657193800017641472", + # "delta": "1", + # "gamma": "0", + # "vega": "0", + # "theta": "0", + # "mark_value": "1.38730606879471451975405216217041015625", + # "maintenance_margin": "-101.37788426911356509663164615631103515625", + # "initial_margin": "-132.2074413704858670826070010662078857421875", + # "open_orders_margin": "264.116085900726830004714429378509521484375", + # "leverage": "8.6954476205089299495699106539379941746377322586618", + # "liquidation_price": "109125.705451984322280623018741607666015625", + # "creation_timestamp": 1738065303840 + # } + # + contract = self.safe_string(position, 'instrument_name') + market = self.safe_market(contract, market) + size = self.safe_string(position, 'amount') + side: Str = None + if Precise.string_gt(size, '0'): + side = 'long' + else: + side = 'short' + contractSize = self.safe_string(market, 'contractSize') + markPrice = self.safe_string(position, 'mark_price') + timestamp = self.safe_integer(position, 'creation_timestamp') + unrealisedPnl = self.safe_string(position, 'unrealized_pnl') + size = Precise.string_abs(size) + notional = Precise.string_mul(size, markPrice) + return self.safe_position({ + 'info': position, + 'id': None, + 'symbol': self.safe_string(market, 'symbol'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastUpdateTimestamp': None, + 'initialMargin': self.safe_string(position, 'initial_margin'), + 'initialMarginPercentage': None, + 'maintenanceMargin': self.safe_string(position, 'maintenance_margin'), + 'maintenanceMarginPercentage': None, + 'entryPrice': None, + 'notional': self.parse_number(notional), + 'leverage': self.safe_number(position, 'leverage'), + 'unrealizedPnl': self.parse_number(unrealisedPnl), + 'contracts': self.parse_number(size), + 'contractSize': self.parse_number(contractSize), + 'marginRatio': None, + 'liquidationPrice': self.safe_number(position, 'liquidation_price'), + 'markPrice': self.parse_number(markPrice), + 'lastPrice': None, + 'collateral': None, + 'marginMode': None, + 'side': side, + 'percentage': None, + 'hedged': None, + 'stopLossPrice': None, + 'takeProfitPrice': None, + }) + + async def fetch_funding_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch the history of funding payments paid and received on self account + + https://docs.derive.xyz/reference/post_private-get-funding-history + + :param str [symbol]: unified market symbol + :param int [since]: the earliest time in ms to fetch funding history for + :param int [limit]: the maximum number of funding history structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict: a `funding history structure ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchFundingHistory', 'paginate') + if paginate: + return await self.fetch_paginated_call_incremental('fetchFundingHistory', symbol, since, limit, params, 'page', 500) + subaccountId = None + subaccountId, params = self.handle_derive_subaccount_id('fetchFundingHistory', params) + request: dict = { + 'subaccount_id': subaccountId, + } + market: Market = None + if symbol is not None: + market = self.market(symbol) + request['instrument_name'] = market['id'] + if since is not None: + request['start_timestamp'] = since + if limit is not None: + request['page_size'] = limit + response = await self.privatePostGetFundingHistory(self.extend(request, params)) + # + # { + # "result": { + # "events": [ + # { + # "instrument_name": "BTC-PERP", + # "timestamp": 1738066618272, + # "funding": "-0.004677736347850093", + # "pnl": "-0.944081615774632967" + # }, + # { + # "instrument_name": "BTC-PERP", + # "timestamp": 1738066617964, + # "funding": "0", + # "pnl": "-0.437556413479249408" + # }, + # { + # "instrument_name": "BTC-PERP", + # "timestamp": 1738065307565, + # "funding": "0", + # "pnl": "-0.39547479770461644" + # } + # ], + # "pagination": { + # "num_pages": 1, + # "count": 3 + # } + # }, + # "id": "524b817f-2108-467f-8795-511066f4acec" + # } + # + result = self.safe_dict(response, 'result', {}) + page = self.safe_integer(params, 'page') + if page is not None: + pagination = self.safe_dict(result, 'pagination') + currentPage = self.safe_integer(pagination, 'num_pages') + if page > currentPage: + return [] + events = self.safe_list(result, 'events', []) + return self.parse_incomes(events, market, since, limit) + + def parse_income(self, income, market: Market = None): + # + # { + # "instrument_name": "BTC-PERP", + # "timestamp": 1738065307565, + # "funding": "0", + # "pnl": "-0.39547479770461644" + # } + # + marketId = self.safe_string(income, 'instrument_name') + symbol = self.safe_symbol(marketId, market) + rate = self.safe_string(income, 'funding') + code = self.safe_currency_code('USDC') + timestamp = self.safe_integer(income, 'timestamp') + return { + 'info': income, + 'symbol': symbol, + 'code': code, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': None, + 'amount': None, + 'rate': rate, + } + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://docs.derive.xyz/reference/post_private-get-all-portfolios + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + deriveWalletAddress = None + deriveWalletAddress, params = self.handle_derive_wallet_address('fetchBalance', params) + request = { + 'wallet': deriveWalletAddress, + } + response = await self.privatePostGetAllPortfolios(self.extend(request, params)) + # + # { + # "result": [{ + # "subaccount_id": 130837, + # "label": "", + # "currency": "all", + # "margin_type": "SM", + # "is_under_liquidation": False, + # "positions_value": "0", + # "collaterals_value": "318.0760325000001103035174310207366943359375", + # "subaccount_value": "318.0760325000001103035174310207366943359375", + # "positions_maintenance_margin": "0", + # "positions_initial_margin": "0", + # "collaterals_maintenance_margin": "238.557024375000082727638073265552520751953125", + # "collaterals_initial_margin": "190.845619500000083235136116854846477508544921875", + # "maintenance_margin": "238.557024375000082727638073265552520751953125", + # "initial_margin": "190.845619500000083235136116854846477508544921875", + # "open_orders_margin": "0", + # "projected_margin_change": "0", + # "open_orders": [], + # "positions": [], + # "collaterals": [ + # { + # "asset_type": "erc20", + # "asset_name": "ETH", + # "currency": "ETH", + # "amount": "0.1", + # "mark_price": "3180.760325000000438272", + # "mark_value": "318.0760325000001103035174310207366943359375", + # "cumulative_interest": "0", + # "pending_interest": "0", + # "initial_margin": "190.845619500000083235136116854846477508544921875", + # "maintenance_margin": "238.557024375000082727638073265552520751953125", + # "realized_pnl": "0", + # "average_price": "3184.891931", + # "unrealized_pnl": "-0.413161", + # "total_fees": "0", + # "average_price_excl_fees": "3184.891931", + # "realized_pnl_excl_fees": "0", + # "unrealized_pnl_excl_fees": "-0.413161", + # "open_orders_margin": "0", + # "creation_timestamp": 1736860533493 + # } + # ] + # }], + # "id": "27b9a64e-3379-4ce6-a126-9fb941c4a970" + # } + # + result = self.safe_list(response, 'result') + return self.parse_balance(result) + + def parse_balance(self, response) -> Balances: + result: dict = { + 'info': response, + } + for i in range(0, len(response)): + subaccount = response[i] + collaterals = self.safe_list(subaccount, 'collaterals', []) + for j in range(0, len(collaterals)): + balance = collaterals[j] + code = self.safe_currency_code(self.safe_string(balance, 'currency')) + account = self.safe_dict(result, code) + if account is None: + account = self.account() + account['total'] = self.safe_string(balance, 'amount') + else: + amount = self.safe_string(balance, 'amount') + account['total'] = Precise.string_add(account['total'], amount) + result[code] = account + return self.safe_balance(result) + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://docs.derive.xyz/reference/post_private-get-deposit-history + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.subaccount_id]: *required* the subaccount id + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + subaccountId = None + subaccountId, params = self.handle_derive_subaccount_id('fetchDeposits', params) + request: dict = { + 'subaccount_id': subaccountId, + } + if since is not None: + request['start_timestamp'] = since + response = await self.privatePostGetDepositHistory(self.extend(request, params)) + # + # { + # "result": { + # "events": [ + # { + # "timestamp": 1736860533599, + # "transaction_id": "f2069395-ec00-49f5-925a-87202a5d240f", + # "asset": "ETH", + # "amount": "0.1", + # "tx_status": "settled", + # "tx_hash": "0xeda21a315c59302a19c42049b4cef05a10b685302b6cc3edbaf49102d91166d4", + # "error_log": {} + # } + # ] + # }, + # "id": "ceebc730-22ab-40cd-9941-33ceb2a74389" + # } + # + currency = self.safe_currency(code) + result = self.safe_dict(response, 'result', {}) + events = self.safe_list(result, 'events') + return self.parse_transactions(events, currency, since, limit, params) + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://docs.derive.xyz/reference/post_private-get-withdrawal-history + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.subaccount_id]: *required* the subaccount id + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + subaccountId = None + subaccountId, params = self.handle_derive_subaccount_id('fetchWithdrawals', params) + request: dict = { + 'subaccount_id': subaccountId, + } + if since is not None: + request['start_timestamp'] = since + response = await self.privatePostGetWithdrawalHistory(self.extend(request, params)) + # + # { + # "result": { + # "events": [ + # { + # "timestamp": 1736860533599, + # "transaction_id": "f2069395-ec00-49f5-925a-87202a5d240f", + # "asset": "ETH", + # "amount": "0.1", + # "tx_status": "settled", + # "tx_hash": "0xeda21a315c59302a19c42049b4cef05a10b685302b6cc3edbaf49102d91166d4", + # "error_log": {} + # } + # ] + # }, + # "id": "ceebc730-22ab-40cd-9941-33ceb2a74389" + # } + # + currency = self.safe_currency(code) + result = self.safe_dict(response, 'result', {}) + events = self.safe_list(result, 'events') + return self.parse_transactions(events, currency, since, limit, params) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # { + # "timestamp": 1736860533599, + # "transaction_id": "f2069395-ec00-49f5-925a-87202a5d240f", + # "asset": "ETH", + # "amount": "0.1", + # "tx_status": "settled", + # "tx_hash": "0xeda21a315c59302a19c42049b4cef05a10b685302b6cc3edbaf49102d91166d4", + # "error_log": {} + # } + # + code = self.safe_string(transaction, 'asset') + timestamp = self.safe_integer(transaction, 'timestamp') + txId = self.safe_string(transaction, 'tx_hash') + if txId == '0x0': + txId = None + return { + 'info': transaction, + 'id': None, + 'txid': txId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'address': None, + 'addressFrom': None, + 'addressTo': None, + 'tag': None, + 'tagFrom': None, + 'tagTo': None, + 'type': None, + 'amount': self.safe_number(transaction, 'amount'), + 'currency': code, + 'status': self.parse_transaction_status(self.safe_string(transaction, 'tx_status')), + 'updated': None, + 'comment': None, + 'internal': None, + 'fee': None, + 'network': None, + } + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'settled': 'ok', + 'reverted': 'failed', + } + return self.safe_string(statuses, status, status) + + def handle_derive_subaccount_id(self, methodName: str, params: dict): + derivesubAccountId = None + derivesubAccountId, params = self.handle_option_and_params(params, methodName, 'subaccount_id') + if (derivesubAccountId is not None) and (derivesubAccountId != ''): + self.options['subaccount_id'] = derivesubAccountId # saving in options + return [derivesubAccountId, params] + optionsWallet = self.safe_string(self.options, 'subaccount_id') + if optionsWallet is not None: + return [optionsWallet, params] + raise ArgumentsRequired(self.id + ' ' + methodName + '() requires a subaccount_id parameter inside \'params\' or exchange.options[\'subaccount_id\']=ID.') + + def handle_derive_wallet_address(self, methodName: str, params: dict): + deriveWalletAddress = None + deriveWalletAddress, params = self.handle_option_and_params(params, methodName, 'deriveWalletAddress') + if (deriveWalletAddress is not None) and (deriveWalletAddress != ''): + self.options['deriveWalletAddress'] = deriveWalletAddress # saving in options + return [deriveWalletAddress, params] + optionsWallet = self.safe_string(self.options, 'deriveWalletAddress') + if optionsWallet is not None: + return [optionsWallet, params] + raise ArgumentsRequired(self.id + ' ' + methodName + '() requires a deriveWalletAddress parameter inside \'params\' or exchange.options[\'deriveWalletAddress\'] = ADDRESS, the address can find in HOME => Developers tab.') + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if not response: + return None # fallback to default error handler + error = self.safe_dict(response, 'error') + if error is not None: + errorCode = self.safe_string(error, 'code') + feedback = self.id + ' ' + self.json(response) + self.throw_broadly_matched_exception(self.exceptions['broad'], body, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + raise ExchangeError(feedback) + return None + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = self.urls['api'][api] + '/' + path + if method == 'POST': + headers = { + 'Content-Type': 'application/json', + } + if api == 'private': + now = str(self.milliseconds()) + signature = self.sign_message(now, self.privateKey) + headers['X-LyraWallet'] = self.safe_string(self.options, 'deriveWalletAddress') + headers['X-LyraTimestamp'] = now + headers['X-LyraSignature'] = signature + body = self.json(params) + return {'url': url, 'method': method, 'body': body, 'headers': headers} diff --git a/ccxt/async_support/digifinex.py b/ccxt/async_support/digifinex.py new file mode 100644 index 0000000..e96bc6d --- /dev/null +++ b/ccxt/async_support/digifinex.py @@ -0,0 +1,4176 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.digifinex import ImplicitAPI +import asyncio +import hashlib +import json +from ccxt.base.types import Any, Balances, BorrowInterest, CrossBorrowRate, CrossBorrowRates, Currencies, Currency, DepositAddress, Int, LedgerEntry, LeverageTier, LeverageTiers, MarginModification, Market, Num, Order, OrderBook, OrderRequest, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, Trade, TradingFeeInterface, Transaction, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import AccountSuspended +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidAddress +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import NotSupported +from ccxt.base.errors import NetworkError +from ccxt.base.errors import DDoSProtection +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import InvalidNonce +from ccxt.base.errors import BadResponse +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class digifinex(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(digifinex, self).describe(), { + 'id': 'digifinex', + 'name': 'DigiFinex', + 'countries': ['SG'], + 'version': 'v3', + 'rateLimit': 900, # 300 for posts + 'has': { + 'CORS': None, + 'spot': True, + 'margin': True, + 'swap': True, + 'future': False, + 'option': False, + 'addMargin': True, + 'cancelOrder': True, + 'cancelOrders': True, + 'createMarketBuyOrderWithCost': True, + 'createMarketOrderWithCost': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createOrders': True, + 'createPostOnlyOrder': True, + 'createReduceOnlyOrder': True, + 'createStopLimitOrder': False, + 'createStopMarketOrder': False, + 'createStopOrder': False, + 'fetchBalance': True, + 'fetchBorrowInterest': True, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchCrossBorrowRate': True, + 'fetchCrossBorrowRates': True, + 'fetchCurrencies': True, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchDepositWithdrawFee': 'emulated', + 'fetchDepositWithdrawFees': True, + 'fetchFundingHistory': True, + 'fetchFundingInterval': True, + 'fetchFundingIntervals': False, + 'fetchFundingRate': True, + 'fetchFundingRateHistory': True, + 'fetchFundingRates': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchLedger': True, + 'fetchLeverage': False, + 'fetchLeverageTiers': True, + 'fetchMarginMode': False, + 'fetchMarketLeverageTiers': True, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchPosition': True, + 'fetchPositionMode': False, + 'fetchPositions': True, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchStatus': True, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': True, + 'fetchTradingFees': False, + 'fetchTransfers': True, + 'fetchWithdrawals': True, + 'reduceMargin': True, + 'setLeverage': True, + 'setMargin': False, + 'setMarginMode': True, + 'setPositionMode': False, + 'transfer': True, + 'withdraw': True, + }, + 'timeframes': { + '1m': '1', + '5m': '5', + '15m': '15', + '30m': '30', + '1h': '60', + '4h': '240', + '12h': '720', + '1d': '1D', + '1w': '1W', + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/51840849/87443315-01283a00-c5fe-11ea-8628-c2a0feaf07ac.jpg', + 'api': { + 'rest': 'https://openapi.digifinex.com', + }, + 'www': 'https://www.digifinex.com', + 'doc': [ + 'https://docs.digifinex.com', + ], + 'fees': 'https://digifinex.zendesk.com/hc/en-us/articles/360000328422-Fee-Structure-on-DigiFinex', + 'referral': 'https://www.digifinex.com/en-ww/from/DhOzBg?channelCode=ljaUPp', + }, + 'api': { + 'public': { + 'spot': { + 'get': [ + '{market}/symbols', + 'kline', + 'margin/currencies', + 'margin/symbols', + 'markets', + 'order_book', + 'ping', + 'spot/symbols', + 'time', + 'trades', + 'trades/symbols', + 'ticker', + 'currencies', + ], + }, + 'swap': { + 'get': [ + 'public/api_weight', + 'public/candles', + 'public/candles_history', + 'public/depth', + 'public/funding_rate', + 'public/funding_rate_history', + 'public/instrument', + 'public/instruments', + 'public/ticker', + 'public/tickers', + 'public/time', + 'public/trades', + ], + }, + }, + 'private': { + 'spot': { + 'get': [ + '{market}/financelog', + '{market}/mytrades', + '{market}/order', + '{market}/order/detail', + '{market}/order/current', + '{market}/order/history', + 'margin/assets', + 'margin/financelog', + 'margin/mytrades', + 'margin/order', + 'margin/order/current', + 'margin/order/history', + 'margin/positions', + 'otc/financelog', + 'spot/assets', + 'spot/financelog', + 'spot/mytrades', + 'spot/order', + 'spot/order/current', + 'spot/order/history', + 'deposit/address', + 'deposit/history', + 'withdraw/history', + ], + 'post': [ + '{market}/order/cancel', + '{market}/order/new', + '{market}/order/batch_new', + 'margin/order/cancel', + 'margin/order/new', + 'margin/position/close', + 'spot/order/cancel', + 'spot/order/new', + 'transfer', + 'withdraw/new', + 'withdraw/cancel', + ], + }, + 'swap': { + 'get': [ + 'account/balance', + 'account/positions', + 'account/finance_record', + 'account/trading_fee_rate', + 'account/transfer_record', + 'account/funding_fee', + 'trade/history_orders', + 'trade/history_trades', + 'trade/open_orders', + 'trade/order_info', + ], + 'post': [ + 'account/transfer', + 'account/leverage', + 'account/position_mode', + 'account/position_margin', + 'trade/batch_cancel_order', + 'trade/batch_order', + 'trade/cancel_order', + 'trade/order_place', + 'follow/sponsor_order', + 'follow/close_order', + 'follow/cancel_order', + 'follow/user_center_current', + 'follow/user_center_history', + 'follow/expert_current_open_order', + 'follow/add_algo', + 'follow/cancel_algo', + 'follow/account_available', + 'follow/plan_task', + 'follow/instrument_list', + ], + }, + }, + }, + 'features': { + 'default': { + 'sandbox': False, + 'createOrder': { + 'marginMode': True, + 'triggerPrice': False, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': False, + 'FOK': False, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'selfTradePrevention': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': False, + 'marketBuyRequiresPrice': False, + 'iceberg': False, + }, + 'createOrders': { + 'max': 10, + }, + 'fetchMyTrades': { + 'marginMode': True, + 'limit': 500, + 'daysBack': 100000, # todo + 'untilDays': 30, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': True, + 'trigger': False, + 'trailing': False, + 'marketType': True, + 'symbolRequired': True, + }, + 'fetchOpenOrders': { + 'marginMode': True, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': True, + 'limit': 100, + 'daysBack': 100000, # todo + 'untilDays': 30, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchClosedOrders': None, + 'fetchOHLCV': { + 'limit': 500, + }, + }, + 'spot': { + 'extends': 'default', + }, + 'forDerivatives': { + 'extends': 'default', + 'createOrders': { + 'max': 20, + 'marginMode': False, + }, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 100000, # todo + 'untilDays': 100000, # todo + }, + 'fetchOrder': { + 'marginMode': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 100, + }, + 'fetchOrders': { + 'marginMode': False, + 'daysBack': 100000, # todo + }, + 'fetchOHLCV': { + 'limit': 100, + }, + }, + 'swap': { + 'linear': { + 'extends': 'forDerivatives', + }, + 'inverse': { + 'extends': 'forDerivatives', + }, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'fees': { + 'trading': { + 'tierBased': True, + 'percentage': True, + 'maker': self.parse_number('0.002'), + 'taker': self.parse_number('0.002'), + }, + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'exact': { + '10001': [BadRequest, "Wrong request method, please check it's a GET ot POST request"], + '10002': [AuthenticationError, 'Invalid ApiKey'], + '10003': [AuthenticationError, "Sign doesn't match"], + '10004': [BadRequest, 'Illegal request parameters'], + '10005': [DDoSProtection, 'Request frequency exceeds the limit'], + '10006': [PermissionDenied, 'Unauthorized to execute self request'], + '10007': [PermissionDenied, 'IP address Unauthorized'], + '10008': [InvalidNonce, 'Timestamp for self request is invalid, timestamp must within 1 minute'], + '10009': [NetworkError, 'Unexist endpoint, please check endpoint URL'], + '10011': [AccountSuspended, 'ApiKey expired. Please go to client side to re-create an ApiKey'], + '20001': [PermissionDenied, 'Trade is not open for self trading pair'], + '20002': [PermissionDenied, 'Trade of self trading pair is suspended'], + '20003': [InvalidOrder, 'Invalid price or amount'], + '20007': [InvalidOrder, 'Price precision error'], + '20008': [InvalidOrder, 'Amount precision error'], + '20009': [InvalidOrder, 'Amount is less than the minimum requirement'], + '20010': [InvalidOrder, 'Cash Amount is less than the minimum requirement'], + '20011': [InsufficientFunds, 'Insufficient balance'], + '20012': [BadRequest, 'Invalid trade type, valid value: buy/sell)'], + '20013': [InvalidOrder, 'No order info found'], + '20014': [BadRequest, 'Invalid date, Valid format: 2018-07-25)'], + '20015': [BadRequest, 'Date exceeds the limit'], + '20018': [PermissionDenied, 'Your trading rights have been banned by the system'], + '20019': [BadSymbol, 'Wrong trading pair symbol. Correct format:"usdt_btc". Quote asset is in the front'], + '20020': [DDoSProtection, "You have violated the API operation trading rules and temporarily forbid trading. At present, we have certain restrictions on the user's transaction rate and withdrawal rate."], + '50000': [ExchangeError, 'Exception error'], + '20021': [BadRequest, 'Invalid currency'], + '20022': [BadRequest, 'The ending timestamp must be larger than the starting timestamp'], + '20023': [BadRequest, 'Invalid transfer type'], + '20024': [BadRequest, 'Invalid amount'], + '20025': [BadRequest, 'This currency is not transferable at the moment'], + '20026': [InsufficientFunds, 'Transfer amount exceed your balance'], + '20027': [PermissionDenied, 'Abnormal account status'], + '20028': [PermissionDenied, 'Blacklist for transfer'], + '20029': [PermissionDenied, 'Transfer amount exceed your daily limit'], + '20030': [BadRequest, 'You have no position on self trading pair'], + '20032': [PermissionDenied, 'Withdrawal limited'], + '20033': [BadRequest, 'Wrong Withdrawal ID'], + '20034': [PermissionDenied, 'Withdrawal service of self crypto has been closed'], + '20035': [PermissionDenied, 'Withdrawal limit'], + '20036': [ExchangeError, 'Withdrawal cancellation failed'], + '20037': [InvalidAddress, 'The withdrawal address, Tag or chain type is not included in the withdrawal management list'], + '20038': [InvalidAddress, 'The withdrawal address is not on the white list'], + '20039': [ExchangeError, "Can't be canceled in current status"], + '20040': [RateLimitExceeded, 'Withdraw too frequently; limitation: 3 times a minute, 100 times a day'], + '20041': [PermissionDenied, 'Beyond the daily withdrawal limit'], + '20042': [BadSymbol, 'Current trading pair does not support API trading'], + '400002': [BadRequest, 'Invalid Parameter'], + }, + 'broad': { + }, + }, + 'options': { + 'defaultType': 'spot', + 'types': ['spot', 'margin', 'otc'], + 'createMarketBuyOrderRequiresPrice': True, + 'accountsByType': { + 'spot': '1', + 'margin': '2', + 'OTC': '3', + }, + 'networks': { + 'ARBITRUM': 'Arbitrum', + 'AVALANCEC': 'AVAX-CCHAIN', + 'AVALANCEX': 'AVAX-XCHAIN', + 'BEP20': 'BEP20', + 'BSC': 'BEP20', + 'CARDANO': 'Cardano', + 'CELO': 'Celo', + 'CHILIZ': 'Chiliz', + 'COSMOS': 'COSMOS', + 'CRC20': 'Crypto.com', + 'CRONOS': 'Crypto.com', + 'DOGECOIN': 'DogeChain', + 'ERC20': 'ERC20', + 'ETH': 'ERC20', + 'ETHW': 'ETHW', + 'IOTA': 'MIOTA', + 'KLAYTN': 'KLAY', + 'MATIC': 'Polygon', + 'METIS': 'MetisDAO', + 'MOONBEAM': 'GLMR', + 'MOONRIVER': 'Moonriver', + 'OPTIMISM': 'OPETH', + 'POLYGON': 'Polygon', + 'RIPPLE': 'XRP', + 'SOLANA': 'SOL', # SOL & SPL + 'STELLAR': 'Stella', # XLM + 'TERRACLASSIC': 'TerraClassic', + 'TERRA': 'Terra', + 'TON': 'Ton', + 'TRC20': 'TRC20', + 'TRON': 'TRC20', + 'TRX': 'TRC20', + 'VECHAIN': 'Vechain', # VET + }, + 'networksById': { + 'TRC20': 'TRC20', + 'TRX': 'TRC20', + 'BEP20': 'BEP20', + 'BSC': 'BEP20', + 'ERC20': 'ERC20', + 'ETH': 'ERC20', + 'Polygon': 'POLYGON', + 'Crypto.com': 'CRONOS', + }, + }, + 'commonCurrencies': { + 'BHT': 'Black House Test', + 'EPS': 'Epanus', + 'FREE': 'FreeRossDAO', + 'MBN': 'Mobilian Coin', + 'TEL': 'TEL666', + }, + }) + + async def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = await self.publicSpotGetCurrencies(params) + # + # { + # "data":[ + # { + # "deposit_status":1, + # "min_deposit_amount":10, + # "withdraw_fee_rate":0, + # "min_withdraw_amount":10, + # "min_withdraw_fee":5, + # "currency":"USDT", + # "withdraw_fee_currency":"USDT", + # "withdraw_status":0, + # "chain":"OMNI" + # }, + # { + # "deposit_status":1, + # "min_deposit_amount":10, + # "withdraw_fee_rate":0, + # "min_withdraw_amount":10, + # "min_withdraw_fee":3, + # "currency":"USDT", + # "withdraw_fee_currency":"USDT", + # "withdraw_status":1, + # "chain":"ERC20" + # }, + # { + # "deposit_status":0, + # "min_deposit_amount":0, + # "withdraw_fee_rate":0, + # "min_withdraw_amount":0, + # "min_withdraw_fee":0, + # "currency":"DGF13", + # "withdraw_fee_currency":"DGF13", + # "withdraw_status":0, + # "chain":"" + # }, + # ], + # "code":200 + # } + # + data = self.safe_list(response, 'data', []) + groupedById = self.group_by(data, 'currency') + keys = list(groupedById.keys()) + result: dict = {} + for i in range(0, len(keys)): + id = keys[i] + networkEntries = groupedById[id] + code = self.safe_currency_code(id) + networks = {} + for j in range(0, len(networkEntries)): + networkEntry = networkEntries[j] + networkId = self.safe_string_2(networkEntry, 'chain', 'currency') + networkCode = self.network_id_to_code(networkId) + networks[networkCode] = { + 'id': networkId, + 'network': networkCode, + 'active': None, + 'deposit': self.safe_integer(networkEntry, 'deposit_status') == 1, + 'withdraw': self.safe_integer(networkEntry, 'withdraw_status') == 1, + 'fee': self.safe_number(networkEntry, 'min_withdraw_fee'), + 'precision': None, + 'limits': { + 'withdraw': { + 'min': self.safe_number(networkEntry, 'min_withdraw_amount'), + 'max': None, + }, + 'deposit': { + 'min': self.safe_number(networkEntry, 'min_deposit_amount'), + 'max': None, + }, + }, + 'info': networkEntry, + } + result[code] = self.safe_currency_structure({ + 'id': id, + 'code': code, + 'info': networkEntries, + 'networks': networks, + }) + return result + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for digifinex + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + options = self.safe_value(self.options, 'fetchMarkets', {}) + method = self.safe_string(options, 'method', 'fetch_markets_v2') + if method == 'fetch_markets_v2': + return await self.fetch_markets_v2(params) + return await self.fetch_markets_v1(params) + + async def fetch_markets_v2(self, params={}): + defaultType = self.safe_string(self.options, 'defaultType') + marginMode, query = self.handle_margin_mode_and_params('fetchMarketsV2', params) + promisesRaw = [] + if marginMode is not None: + promisesRaw.append(self.publicSpotGetMarginSymbols(query)) + else: + promisesRaw.append(self.publicSpotGetTradesSymbols(query)) + promisesRaw.append(self.publicSwapGetPublicInstruments(params)) + promises = await asyncio.gather(*promisesRaw) + spotMarkets = promises[0] + swapMarkets = promises[1] + # + # spot and margin + # + # { + # "symbol_list":[ + # { + # "order_types":["LIMIT","MARKET"], + # "quote_asset":"USDT", + # "minimum_value":2, + # "amount_precision":4, + # "status":"TRADING", + # "minimum_amount":0.0001, + # "symbol":"BTC_USDT", + # "is_allow":1, + # "zone":"MAIN", + # "base_asset":"BTC", + # "price_precision":2 + # } + # ], + # "code":0 + # } + # + # swap + # + # { + # "code": 0, + # "data": [ + # { + # "instrument_id": "BTCUSDTPERP", + # "type": "REAL", + # "contract_type": "PERPETUAL", + # "base_currency": "BTC", + # "quote_currency": "USDT", + # "clear_currency": "USDT", + # "contract_value": "0.001", + # "contract_value_currency": "BTC", + # "is_inverse": False, + # "is_trading": True, + # "status": "ONLINE", + # "price_precision": 4, + # "tick_size": "0.0001", + # "min_order_amount": 1, + # "open_max_limits": [ + # { + # "leverage": "50", + # "max_limit": "1000000" + # } + # ] + # }, + # ] + # } + # + spotData = self.safe_value(spotMarkets, 'symbol_list', []) + swapData = self.safe_value(swapMarkets, 'data', []) + response = self.array_concat(spotData, swapData) + result = [] + for i in range(0, len(response)): + market = response[i] + id = self.safe_string_2(market, 'symbol', 'instrument_id') + baseId = self.safe_string_2(market, 'base_asset', 'base_currency') + quoteId = self.safe_string_2(market, 'quote_asset', 'quote_currency') + settleId = self.safe_string(market, 'clear_currency') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + settle = self.safe_currency_code(settleId) + # + # The status is documented in the exchange API docs: + # TRADING, HALT(delisted), BREAK(trading paused) + # https://docs.digifinex.vip/en-ww/v3/#/public/spot/symbols + # However, all spot markets actually have status == 'HALT' + # despite that they appear to be active on the exchange website. + # Apparently, we can't trust self status. + # status = self.safe_string(market, 'status') + # active = (status == 'TRADING') + # + isAllowed = self.safe_integer(market, 'is_allow', 1) + type = 'margin' if (defaultType == 'margin') else 'spot' + spot = settle is None + swap = not spot + margin = True if (marginMode is not None) else None + symbol = base + '/' + quote + isInverse = None + isLinear = None + if swap: + type = 'swap' + symbol = base + '/' + quote + ':' + settle + isInverse = self.safe_value(market, 'is_inverse') + isLinear = True if (not isInverse) else False + isTrading = self.safe_value(market, 'isTrading') + if isTrading: + isAllowed = 1 + result.append({ + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': type, + 'spot': spot, + 'margin': margin, + 'swap': swap, + 'future': False, + 'option': False, + 'active': True if isAllowed else False, + 'contract': swap, + 'linear': isLinear, + 'inverse': isInverse, + 'contractSize': self.safe_number(market, 'contract_value'), + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(self.parse_precision(self.safe_string(market, 'amount_precision'))), + 'price': self.parse_number(self.parse_precision(self.safe_string(market, 'price_precision'))), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number_2(market, 'minimum_amount', 'min_order_amount'), + 'max': None, + }, + 'price': { + 'min': self.safe_number(market, 'tick_size'), + 'max': None, + }, + 'cost': { + 'min': self.safe_number(market, 'minimum_value'), + 'max': None, + }, + }, + 'created': None, + 'info': market, + }) + return result + + async def fetch_markets_v1(self, params={}): + response = await self.publicSpotGetMarkets(params) + # + # { + # "data": [ + # { + # "volume_precision":4, + # "price_precision":2, + # "market":"btc_usdt", + # "min_amount":2, + # "min_volume":0.0001 + # }, + # ], + # "date":1564507456, + # "code":0 + # } + # + markets = self.safe_value(response, 'data', []) + result = [] + for i in range(0, len(markets)): + market = markets[i] + id = self.safe_string(market, 'market') + baseId, quoteId = id.split('_') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + result.append({ + 'id': id, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': None, + 'swap': False, + 'future': False, + 'option': False, + 'active': None, + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'price': self.parse_number(self.parse_precision(self.safe_string(market, 'price_precision'))), + 'amount': self.parse_number(self.parse_precision(self.safe_string(market, 'volume_precision'))), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(market, 'min_volume'), + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': self.safe_number(market, 'min_amount'), + 'max': None, + }, + }, + 'info': market, + }) + return result + + def parse_balance(self, response) -> Balances: + # + # spot and margin + # + # { + # "currency": "BTC", + # "free": 4723846.89208129, + # "total": 0 + # } + # + # swap + # + # { + # "equity": "0", + # "currency": "BTC", + # "margin": "0", + # "frozen_margin": "0", + # "frozen_money": "0", + # "margin_ratio": "0", + # "realized_pnl": "0", + # "avail_balance": "0", + # "unrealized_pnl": "0", + # "time_stamp": 1661487402396 + # } + # + result: dict = {'info': response} + for i in range(0, len(response)): + balance = response[i] + currencyId = self.safe_string(balance, 'currency') + code = self.safe_currency_code(currencyId) + account = self.account() + free = self.safe_string_2(balance, 'free', 'avail_balance') + total = self.safe_string_2(balance, 'total', 'equity') + account['free'] = free + account['used'] = Precise.string_sub(total, free) + account['total'] = total + result[code] = account + return self.safe_balance(result) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://docs.digifinex.com/en-ww/spot/v3/rest.html#spot-account-assets + https://docs.digifinex.com/en-ww/spot/v3/rest.html#margin-assets + https://docs.digifinex.com/en-ww/swap/v2/rest.html#accountbalance + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + marketType = None + marketType, params = self.handle_market_type_and_params('fetchBalance', None, params) + marginMode, query = self.handle_margin_mode_and_params('fetchBalance', params) + response = None + if marginMode is not None or marketType == 'margin': + marketType = 'margin' + response = await self.privateSpotGetMarginAssets(query) + elif marketType == 'spot': + response = await self.privateSpotGetSpotAssets(query) + elif marketType == 'swap': + response = await self.privateSwapGetAccountBalance(query) + else: + raise NotSupported(self.id + ' fetchBalance() not support self market type') + # + # spot and margin + # + # { + # "code": 0, + # "list": [ + # { + # "currency": "BTC", + # "free": 4723846.89208129, + # "total": 0 + # }, + # ... + # ] + # } + # + # swap + # + # { + # "code": 0, + # "data": [ + # { + # "equity": "0", + # "currency": "BTC", + # "margin": "0", + # "frozen_margin": "0", + # "frozen_money": "0", + # "margin_ratio": "0", + # "realized_pnl": "0", + # "avail_balance": "0", + # "unrealized_pnl": "0", + # "time_stamp": 1661487402396 + # }, + # ... + # ] + # } + # + balanceRequest = 'data' if (marketType == 'swap') else 'list' + balances = self.safe_value(response, balanceRequest, []) + return self.parse_balance(balances) + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://docs.digifinex.com/en-ww/spot/v3/rest.html#get-orderbook + https://docs.digifinex.com/en-ww/swap/v2/rest.html#orderbook + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + marketType, query = self.handle_market_type_and_params('fetchOrderBook', market, params) + request: dict = {} + if limit is not None: + request['limit'] = limit + response = None + if marketType == 'swap': + request['instrument_id'] = market['id'] + response = await self.publicSwapGetPublicDepth(self.extend(request, query)) + else: + request['symbol'] = market['id'] + response = await self.publicSpotGetOrderBook(self.extend(request, query)) + # + # spot + # + # { + # "bids": [ + # [9605.77,0.0016], + # [9605.46,0.0003], + # [9602.04,0.0127], + # ], + # "asks": [ + # [9627.22,0.025803], + # [9627.12,0.168543], + # [9626.52,0.0011529], + # ], + # "date":1564509499, + # "code":0 + # } + # + # swap + # + # { + # "code": 0, + # "data": { + # "instrument_id": "BTCUSDTPERP", + # "timestamp": 1667975290425, + # "asks": [ + # ["18384.7",3492], + # ["18402.7",5000], + # ["18406.7",5000], + # ], + # "bids": [ + # ["18366.2",4395], + # ["18364.3",3070], + # ["18359.4",5000], + # ] + # } + # } + # + timestamp = None + orderBook = None + if marketType == 'swap': + orderBook = self.safe_value(response, 'data', {}) + timestamp = self.safe_integer(orderBook, 'timestamp') + else: + orderBook = response + timestamp = self.safe_timestamp(response, 'date') + return self.parse_order_book(orderBook, market['symbol'], timestamp) + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://docs.digifinex.com/en-ww/spot/v3/rest.html#ticker-price + https://docs.digifinex.com/en-ww/swap/v2/rest.html#tickers + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + first = self.safe_string(symbols, 0) + market = None + if first is not None: + market = self.market(first) + type = None + type, params = self.handle_market_type_and_params('fetchTickers', market, params) + request: dict = {} + response = None + if type == 'swap': + response = await self.publicSwapGetPublicTickers(self.extend(request, params)) + else: + response = await self.publicSpotGetTicker(self.extend(request, params)) + # + # spot + # + # { + # "ticker": [{ + # "vol": 40717.4461, + # "change": -1.91, + # "base_vol": 392447999.65374, + # "sell": 9592.23, + # "last": 9592.22, + # "symbol": "btc_usdt", + # "low": 9476.24, + # "buy": 9592.03, + # "high": 9793.87 + # }], + # "date": 1589874294, + # "code": 0 + # } + # + # swap + # + # { + # "code": 0, + # "data": [ + # { + # "instrument_id": "SUSHIUSDTPERP", + # "index_price": "1.1297", + # "mark_price": "1.1289", + # "max_buy_price": "1.1856", + # "min_sell_price": "1.0726", + # "best_bid": "1.1278", + # "best_bid_size": "500", + # "best_ask": "1.1302", + # "best_ask_size": "471", + # "high_24h": "1.2064", + # "open_24h": "1.1938", + # "low_24h": "1.1239", + # "last": "1.1302", + # "last_qty": "29", + # "volume_24h": "4946163", + # "price_change_percent": "-0.053275255486681085", + # "open_interest": "-", + # "timestamp": 1663222782100 + # }, + # ... + # ] + # } + # + result: dict = {} + tickers = self.safe_value_2(response, 'ticker', 'data', []) + date = self.safe_integer(response, 'date') + for i in range(0, len(tickers)): + rawTicker = self.extend({ + 'date': date, + }, tickers[i]) + ticker = self.parse_ticker(rawTicker) + symbol = ticker['symbol'] + result[symbol] = ticker + return self.filter_by_array_tickers(result, 'symbol', symbols) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://docs.digifinex.com/en-ww/spot/v3/rest.html#ticker-price + https://docs.digifinex.com/en-ww/swap/v2/rest.html#ticker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = {} + response = None + if market['swap']: + request['instrument_id'] = market['id'] + response = await self.publicSwapGetPublicTicker(self.extend(request, params)) + else: + request['symbol'] = market['id'] + response = await self.publicSpotGetTicker(self.extend(request, params)) + # + # spot + # + # { + # "ticker": [{ + # "vol": 40717.4461, + # "change": -1.91, + # "base_vol": 392447999.65374, + # "sell": 9592.23, + # "last": 9592.22, + # "symbol": "btc_usdt", + # "low": 9476.24, + # "buy": 9592.03, + # "high": 9793.87 + # }], + # "date": 1589874294, + # "code": 0 + # } + # + # swap + # + # { + # "code": 0, + # "data": { + # "instrument_id": "BTCUSDTPERP", + # "index_price": "20141.9967", + # "mark_price": "20139.3404", + # "max_buy_price": "21146.4838", + # "min_sell_price": "19132.2725", + # "best_bid": "20140.0998", + # "best_bid_size": "3116", + # "best_ask": "20140.0999", + # "best_ask_size": "9004", + # "high_24h": "20410.6496", + # "open_24h": "20308.6998", + # "low_24h": "19600", + # "last": "20140.0999", + # "last_qty": "2", + # "volume_24h": "49382816", + # "price_change_percent": "-0.008301855936636448", + # "open_interest": "-", + # "timestamp": 1663221614998 + # } + # } + # + date = self.safe_integer(response, 'date') + tickers = self.safe_value(response, 'ticker', []) + data = self.safe_value(response, 'data', {}) + firstTicker = self.safe_value(tickers, 0, {}) + result = None + if market['swap']: + result = data + else: + result = self.extend({'date': date}, firstTicker) + return self.parse_ticker(result, market) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # spot: fetchTicker, fetchTickers + # + # { + # "last":0.021957, + # "symbol": "btc_usdt", + # "base_vol":2249.3521732227, + # "change":-0.6, + # "vol":102443.5111, + # "sell":0.021978, + # "low":0.021791, + # "buy":0.021946, + # "high":0.022266, + # "date"1564518452, # injected from fetchTicker/fetchTickers + # } + # + # swap: fetchTicker, fetchTickers + # + # { + # "instrument_id": "BTCUSDTPERP", + # "index_price": "20141.9967", + # "mark_price": "20139.3404", + # "max_buy_price": "21146.4838", + # "min_sell_price": "19132.2725", + # "best_bid": "20140.0998", + # "best_bid_size": "3116", + # "best_ask": "20140.0999", + # "best_ask_size": "9004", + # "high_24h": "20410.6496", + # "open_24h": "20308.6998", + # "low_24h": "19600", + # "last": "20140.0999", + # "last_qty": "2", + # "volume_24h": "49382816", + # "price_change_percent": "-0.008301855936636448", + # "open_interest": "-", + # "timestamp": 1663221614998 + # } + # + indexPrice = self.safe_number(ticker, 'index_price') + marketType = 'contract' if (indexPrice is not None) else 'spot' + marketId = self.safe_string_upper_2(ticker, 'symbol', 'instrument_id') + symbol = self.safe_symbol(marketId, market, None, marketType) + market = self.safe_market(marketId, market, None, marketType) + timestamp = self.safe_timestamp(ticker, 'date') + if market['swap']: + timestamp = self.safe_integer(ticker, 'timestamp') + last = self.safe_string(ticker, 'last') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string_2(ticker, 'high', 'high_24h'), + 'low': self.safe_string_2(ticker, 'low', 'low_24h'), + 'bid': self.safe_string_2(ticker, 'buy', 'best_bid'), + 'bidVolume': self.safe_string(ticker, 'best_bid_size'), + 'ask': self.safe_string_2(ticker, 'sell', 'best_ask'), + 'askVolume': self.safe_string(ticker, 'best_ask_size'), + 'vwap': None, + 'open': self.safe_string(ticker, 'open_24h'), + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': self.safe_string_2(ticker, 'change', 'price_change_percent'), + 'average': None, + 'baseVolume': self.safe_string_2(ticker, 'vol', 'volume_24h'), + 'quoteVolume': self.safe_string(ticker, 'base_vol'), + 'markPrice': self.safe_string(ticker, 'mark_price'), + 'indexPrice': indexPrice, + 'info': ticker, + }, market) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # spot: fetchTrades + # + # { + # "date":1564520003, + # "id":1596149203, + # "amount":0.7073, + # "type":"buy", + # "price":0.02193, + # } + # + # swap: fetchTrades + # + # { + # "instrument_id": "BTCUSDTPERP", + # "trade_id": "1595190773677035521", + # "direction": "4", + # "volume": "4", + # "price": "16188.3", + # "trade_time": 1669158092314 + # } + # + # spot: fetchMyTrades + # + # { + # "symbol": "BTC_USDT", + # "order_id": "6707cbdcda0edfaa7f4ab509e4cbf966", + # "id": 28457, + # "price": 0.1, + # "amount": 0, + # "fee": 0.096, + # "fee_currency": "USDT", + # "timestamp": 1499865549, + # "side": "buy", # or "side": "sell_market" + # "is_maker": True + # } + # + # swap: fetchMyTrades + # + # { + # "trade_id": "1590136768424841218", + # "instrument_id": "BTCUSDTPERP", + # "order_id": "1590136768156405760", + # "type": 1, + # "order_type": 8, + # "price": "18514.5", + # "size": "1", + # "fee": "0.00925725", + # "close_profit": "0", + # "leverage": "20", + # "trade_type": 0, + # "match_role": 1, + # "trade_time": 1667953123562 + # } + # + id = self.safe_string_2(trade, 'id', 'trade_id') + orderId = self.safe_string(trade, 'order_id') + priceString = self.safe_string(trade, 'price') + amountString = self.safe_string_n(trade, ['amount', 'volume', 'size']) + marketId = self.safe_string_upper_2(trade, 'symbol', 'instrument_id') + symbol = self.safe_symbol(marketId, market) + if market is None: + market = self.safe_market(marketId) + timestamp = self.safe_timestamp_2(trade, 'date', 'timestamp') + side = self.safe_string_2(trade, 'type', 'side') + type = None + takerOrMaker = None + if market['type'] == 'swap': + timestamp = self.safe_integer(trade, 'trade_time') + orderType = self.safe_string(trade, 'order_type') + tradeRole = self.safe_string(trade, 'match_role') + direction = self.safe_string(trade, 'direction') + if orderType is not None: + type = 'limit' if (orderType == '0') else None + if tradeRole == '1': + takerOrMaker = 'taker' + elif tradeRole == '2': + takerOrMaker = 'maker' + else: + takerOrMaker = None + if (side == '1') or (direction == '1'): + # side = 'open long' + side = 'buy' + elif (side == '2') or (direction == '2'): + # side = 'open short' + side = 'sell' + elif (side == '3') or (direction == '3'): + # side = 'close long' + side = 'sell' + elif (side == '4') or (direction == '4'): + # side = 'close short' + side = 'buy' + else: + parts = side.split('_') + side = self.safe_string(parts, 0) + type = self.safe_string(parts, 1) + if type is None: + type = 'limit' + isMaker = self.safe_value(trade, 'is_maker') + takerOrMaker = 'maker' if isMaker else 'taker' + fee = None + feeCostString = self.safe_string(trade, 'fee') + if feeCostString is not None: + feeCurrencyId = self.safe_string(trade, 'fee_currency') + feeCurrencyCode = None + if feeCurrencyId is not None: + feeCurrencyCode = self.safe_currency_code(feeCurrencyId) + fee = { + 'cost': feeCostString, + 'currency': feeCurrencyCode, + } + return self.safe_trade({ + 'id': id, + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'type': type, + 'order': orderId, + 'side': side, + 'price': priceString, + 'amount': amountString, + 'cost': None, + 'takerOrMaker': takerOrMaker, + 'fee': fee, + }, market) + + async def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = await self.publicSpotGetTime(params) + # + # { + # "server_time": 1589873762, + # "code": 0 + # } + # + return self.safe_timestamp(response, 'server_time') + + async def fetch_status(self, params={}): + """ + the latest known information on the availability of the exchange API + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `status structure ` + """ + response = await self.publicSpotGetPing(params) + # + # { + # "msg": "pong", + # "code": 0 + # } + # + code = self.safe_integer(response, 'code') + status = 'ok' if (code == 0) else 'maintenance' + return { + 'status': status, + 'updated': None, + 'eta': None, + 'url': None, + 'info': response, + } + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://docs.digifinex.com/en-ww/spot/v3/rest.html#get-recent-trades + https://docs.digifinex.com/en-ww/swap/v2/rest.html#recenttrades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = {} + if limit is not None: + request['limit'] = min(limit, 100) if market['swap'] else limit + response = None + if market['swap']: + request['instrument_id'] = market['id'] + response = await self.publicSwapGetPublicTrades(self.extend(request, params)) + else: + request['symbol'] = market['id'] + response = await self.publicSpotGetTrades(self.extend(request, params)) + # + # spot + # + # { + # "data":[ + # { + # "date":1564520003, + # "id":1596149203, + # "amount":0.7073, + # "type":"buy", + # "price":0.02193, + # }, + # { + # "date":1564520002, + # "id":1596149165, + # "amount":0.3232, + # "type":"sell", + # "price":0.021927, + # }, + # ], + # "code": 0, + # "date": 1564520003, + # } + # + # swap + # + # { + # "code": 0, + # "data": [ + # { + # "instrument_id": "BTCUSDTPERP", + # "trade_id": "1595190773677035521", + # "direction": "4", + # "volume": "4", + # "price": "16188.3", + # "trade_time": 1669158092314 + # }, + # ... + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_trades(data, market, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # [ + # 1556712900, + # 2205.899, + # 0.029967, + # 0.02997, + # 0.029871, + # 0.029927 + # ] + # + if market['swap']: + return [ + self.safe_integer(ohlcv, 0), + self.safe_number(ohlcv, 1), # open + self.safe_number(ohlcv, 2), # high + self.safe_number(ohlcv, 3), # low + self.safe_number(ohlcv, 4), # close + self.safe_number(ohlcv, 5), # volume + ] + else: + return [ + self.safe_timestamp(ohlcv, 0), + self.safe_number(ohlcv, 5), # open + self.safe_number(ohlcv, 3), # high + self.safe_number(ohlcv, 4), # low + self.safe_number(ohlcv, 2), # close + self.safe_number(ohlcv, 1), # volume + ] + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://docs.digifinex.com/en-ww/spot/v3/rest.html#get-candles-data + https://docs.digifinex.com/en-ww/swap/v2/rest.html#recentcandle + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest candle to fetch + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + request: dict = {} + response = None + if market['swap']: + request['instrument_id'] = market['id'] + request['granularity'] = timeframe + if limit is not None: + request['limit'] = min(limit, 100) + response = await self.publicSwapGetPublicCandles(self.extend(request, params)) + else: + until = self.safe_integer(params, 'until') + request['symbol'] = market['id'] + request['period'] = self.safe_string(self.timeframes, timeframe, timeframe) + startTime = since + duration = self.parse_timeframe(timeframe) + if startTime is None: + if (limit is not None) or (until is not None): + endTime = until if (until is not None) else self.milliseconds() + startLimit = limit if (limit is not None) else 200 + startTime = endTime - (startLimit * duration * 1000) + if startTime is not None: + startTime = self.parse_to_int(startTime / 1000) + request['start_time'] = startTime + if (limit is not None) or (until is not None): + if until is not None: + endByUntil = self.parse_to_int(until / 1000) + if limit is not None: + endByLimit = self.sum(startTime, limit * duration) + request['end_time'] = min(endByLimit, endByUntil) + else: + request['end_time'] = endByUntil + else: + request['end_time'] = self.sum(startTime, limit * duration) + params = self.omit(params, 'until') + response = await self.publicSpotGetKline(self.extend(request, params)) + # + # spot + # + # { + # "code":0, + # "data":[ + # [1556712900,2205.899,0.029967,0.02997,0.029871,0.029927], + # [1556713800,1912.9174,0.029992,0.030014,0.029955,0.02996], + # [1556714700,1556.4795,0.029974,0.030019,0.029969,0.02999], + # ] + # } + # + # swap + # + # { + # "code": 0, + # "data": { + # "instrument_id": "BTCUSDTPERP", + # "granularity": "1m", + # "candles": [ + # [1588089660000,"6900","6900","6900","6900","0","0"], + # [1588089720000,"6900","6900","6900","6900","0","0"], + # [1588089780000,"6900","6900","6900","6900","0","0"], + # ] + # } + # } + # + candles = None + if market['swap']: + data = self.safe_value(response, 'data', {}) + candles = self.safe_value(data, 'candles', []) + else: + candles = self.safe_value(response, 'data', []) + return self.parse_ohlcvs(candles, market, timeframe, since, limit) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://docs.digifinex.com/en-ww/spot/v3/rest.html#create-new-order + https://docs.digifinex.com/en-ww/swap/v2/rest.html#orderplace + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency, spot market orders use the quote currency, swap requires the number of contracts + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.timeInForce]: "GTC", "IOC", "FOK", or "PO" + :param bool [params.postOnly]: True or False + :param bool [params.reduceOnly]: True or False + :param str [params.marginMode]: 'cross' or 'isolated', for spot margin trading + :param float [params.cost]: *spot market buy only* the quote quantity that can be used alternative for the amount + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + marginResult = self.handle_margin_mode_and_params('createOrder', params) + marginMode = marginResult[0] + request = self.create_order_request(symbol, type, side, amount, price, params) + response = None + if market['swap']: + response = await self.privateSwapPostTradeOrderPlace(request) + else: + if marginMode is not None: + response = await self.privateSpotPostMarginOrderNew(request) + else: + response = await self.privateSpotPostSpotOrderNew(request) + # + # spot and margin + # + # { + # "code": 0, + # "order_id": "198361cecdc65f9c8c9bb2fa68faec40" + # } + # + # swap + # + # { + # "code": 0, + # "data": "1590873693003714560" + # } + # + order = self.parse_order(response, market) + order['symbol'] = market['symbol'] + order['type'] = type + order['side'] = side + order['amount'] = amount + order['price'] = price + return order + + async def create_orders(self, orders: List[OrderRequest], params={}): + """ + create a list of trade orders(all orders should be of the same symbol) + + https://docs.digifinex.com/en-ww/spot/v3/rest.html#create-multiple-order + https://docs.digifinex.com/en-ww/swap/v2/rest.html#batchorder + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + ordersRequests = [] + symbol = None + marginMode = None + for i in range(0, len(orders)): + rawOrder = orders[i] + marketId = self.safe_string(rawOrder, 'symbol') + if symbol is None: + symbol = marketId + else: + if symbol != marketId: + raise BadRequest(self.id + ' createOrders() requires all orders to have the same symbol') + type = self.safe_string(rawOrder, 'type') + side = self.safe_string(rawOrder, 'side') + amount = self.safe_value(rawOrder, 'amount') + price = self.safe_value(rawOrder, 'price') + orderParams = self.safe_value(rawOrder, 'params', {}) + marginResult = self.handle_margin_mode_and_params('createOrders', orderParams) + currentMarginMode = marginResult[0] + if currentMarginMode is not None: + if marginMode is None: + marginMode = currentMarginMode + else: + if marginMode != currentMarginMode: + raise BadRequest(self.id + ' createOrders() requires all orders to have the same margin mode(isolated or cross)') + orderRequest = self.create_order_request(marketId, type, side, amount, price, orderParams) + ordersRequests.append(orderRequest) + market = self.market(symbol) + request: dict = {} + response = None + if market['swap']: + response = await self.privateSwapPostTradeBatchOrder(ordersRequests) + else: + request['market'] = 'margin' if (marginMode is not None) else 'spot' + request['symbol'] = market['id'] + request['list'] = self.json(ordersRequests) + response = await self.privateSpotPostMarketOrderBatchNew(request) + # + # spot + # + # { + # "code": 0, + # "order_ids": [ + # "064290fbe2d26e7b28d7e6c0a5cf70a5", + # "24c8f9b73d81e4d9d8d7e3280281c258" + # ] + # } + # + # swap + # + # { + # "code": 0, + # "data": [ + # "1720297963537829888", + # "1720297963537829889" + # ] + # } + # + data = [] + if market['swap']: + data = self.safe_value(response, 'data', []) + else: + data = self.safe_value(response, 'order_ids', []) + result = [] + for i in range(0, len(orders)): + rawOrder = orders[i] + individualOrder: dict = {} + individualOrder['order_id'] = data[i] + individualOrder['instrument_id'] = market['id'] + individualOrder['amount'] = self.safe_number(rawOrder, 'amount') + individualOrder['price'] = self.safe_number(rawOrder, 'price') + result.append(individualOrder) + return self.parse_orders(result, market) + + def create_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + @ignore + helper function to build request + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency, spot market orders use the quote currency, swap requires the number of contracts + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: request to be sent to the exchange + """ + market = self.market(symbol) + marketType = None + marginMode = None + marketType, params = self.handle_market_type_and_params('createOrderRequest', market, params) + marginMode, params = self.handle_margin_mode_and_params('createOrderRequest', params) + if marginMode is not None: + marketType = 'margin' + request: dict = {} + swap = (marketType == 'swap') + isMarketOrder = (type == 'market') + isLimitOrder = (type == 'limit') + marketIdRequest = 'instrument_id' if swap else 'symbol' + request[marketIdRequest] = market['id'] + postOnly = self.is_post_only(isMarketOrder, False, params) + postOnlyParsed = None + if swap: + reduceOnly = self.safe_bool(params, 'reduceOnly', False) + timeInForce = self.safe_string(params, 'timeInForce') + orderType = None + if side == 'buy': + requestType = 4 if (reduceOnly) else 1 + request['type'] = requestType + else: + requestType = 3 if (reduceOnly) else 2 + request['type'] = requestType + if isLimitOrder: + orderType = 0 + if timeInForce == 'FOK': + orderType = 15 if isMarketOrder else 9 + elif timeInForce == 'IOC': + orderType = 13 if isMarketOrder else 4 + elif (timeInForce == 'GTC') or (isMarketOrder): + orderType = 14 + elif timeInForce == 'PO': + postOnly = True + if price is not None: + request['price'] = self.price_to_precision(symbol, price) + request['order_type'] = orderType + request['size'] = amount # swap orders require the amount to be the number of contracts + params = self.omit(params, ['reduceOnly', 'timeInForce']) + else: + postOnlyParsed = 1 if (postOnly is True) else 2 + request['market'] = marketType + suffix = '' + if type == 'market': + suffix = '_market' + else: + request['price'] = self.price_to_precision(symbol, price) + request['type'] = side + suffix + # limit orders require the amount in the base currency, market orders require the amount in the quote currency + quantity = None + createMarketBuyOrderRequiresPrice = True + createMarketBuyOrderRequiresPrice, params = self.handle_option_and_params(params, 'createOrderRequest', 'createMarketBuyOrderRequiresPrice', True) + if isMarketOrder and (side == 'buy'): + cost = self.safe_number(params, 'cost') + params = self.omit(params, 'cost') + if cost is not None: + quantity = self.cost_to_precision(symbol, cost) + elif createMarketBuyOrderRequiresPrice: + if price is None: + raise InvalidOrder(self.id + ' createOrder() requires a price argument for market buy orders on spot markets to calculate the total amount to spend(amount * price), alternatively set the createMarketBuyOrderRequiresPrice option or param to False and pass the cost to spend in the amount argument') + else: + amountString = self.number_to_string(amount) + priceString = self.number_to_string(price) + costRequest = self.parse_number(Precise.string_mul(amountString, priceString)) + quantity = self.cost_to_precision(symbol, costRequest) + else: + quantity = self.cost_to_precision(symbol, amount) + else: + quantity = self.amount_to_precision(symbol, amount) + request['amount'] = quantity + if postOnly: + if postOnlyParsed: + request['post_only'] = postOnlyParsed + else: + request['post_only'] = postOnly + params = self.omit(params, ['postOnly']) + return self.extend(request, params) + + async def create_market_buy_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market buy order by providing the symbol and cost + + https://docs.digifinex.com/en-ww/spot/v3/rest.html#create-new-order + + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' createMarketBuyOrderWithCost() supports spot orders only') + params['createMarketBuyOrderRequiresPrice'] = False + return await self.create_order(symbol, 'market', 'buy', cost, None, params) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://docs.digifinex.com/en-ww/spot/v3/rest.html#cancel-order + https://docs.digifinex.com/en-ww/swap/v2/rest.html#cancelorder + + :param str id: order id + :param str symbol: not used by digifinex cancelOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + id = str(id) + marketType = None + marketType, params = self.handle_market_type_and_params('cancelOrder', market, params) + request: dict = { + 'order_id': id, + } + if marketType == 'swap': + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + request['instrument_id'] = market['id'] + else: + request['market'] = marketType + marginMode, query = self.handle_margin_mode_and_params('cancelOrder', params) + response = None + if marginMode is not None or marketType == 'margin': + marketType = 'margin' + response = await self.privateSpotPostMarginOrderCancel(self.extend(request, query)) + elif marketType == 'spot': + response = await self.privateSpotPostSpotOrderCancel(self.extend(request, query)) + elif marketType == 'swap': + response = await self.privateSwapPostTradeCancelOrder(self.extend(request, query)) + else: + raise NotSupported(self.id + ' cancelOrder() not support self market type') + # + # spot and margin + # + # { + # "code": 0, + # "success": [ + # "198361cecdc65f9c8c9bb2fa68faec40", + # "3fb0d98e51c18954f10d439a9cf57de0" + # ], + # "error": [ + # "78a7104e3c65cc0c5a212a53e76d0205" + # ] + # } + # + # swap + # + # { + # "code": 0, + # "data": "1590923061186531328" + # } + # + if (marketType == 'spot') or (marketType == 'margin'): + canceledOrders = self.safe_value(response, 'success', []) + numCanceledOrders = len(canceledOrders) + if numCanceledOrders != 1: + raise OrderNotFound(self.id + ' cancelOrder() ' + id + ' not found') + orders = self.parse_cancel_orders(response) + return self.safe_dict(orders, 0) + else: + return self.safe_order({ + 'info': response, + 'orderId': self.safe_string(response, 'data'), + }) + + def parse_cancel_orders(self, response): + success = self.safe_list(response, 'success') + error = self.safe_list(response, 'error') + result = [] + for i in range(0, len(success)): + order = success[i] + result.append(self.safe_order({ + 'info': order, + 'id': order, + 'status': 'canceled', + })) + for i in range(0, len(error)): + order = error[i] + result.append(self.safe_order({ + 'info': order, + 'id': self.safe_string_2(order, 'order-id', 'order_id'), + 'status': 'failed', + 'clientOrderId': self.safe_string(order, 'client-order-id'), + })) + return result + + async def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): + """ + cancel multiple orders + :param str[] ids: order ids + :param str symbol: not used by digifinex cancelOrders() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an list of `order structures ` + """ + await self.load_markets() + defaultType = self.safe_string(self.options, 'defaultType', 'spot') + orderType = self.safe_string(params, 'type', defaultType) + params = self.omit(params, 'type') + request: dict = { + 'market': orderType, + 'order_id': ','.join(ids), + } + response = await self.privateSpotPostSpotOrderCancel(self.extend(request, params)) + # + # { + # "code": 0, + # "success": [ + # "198361cecdc65f9c8c9bb2fa68faec40", + # "3fb0d98e51c18954f10d439a9cf57de0" + # ], + # "error": [ + # "78a7104e3c65cc0c5a212a53e76d0205" + # ] + # } + # + return self.parse_cancel_orders(response) + + def parse_order_status(self, status: Str): + statuses: dict = { + '0': 'open', + '1': 'open', # partially filled + '2': 'closed', + '3': 'canceled', + '4': 'canceled', # partially filled and canceled + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # spot: createOrder + # + # { + # "code": 0, + # "order_id": "198361cecdc65f9c8c9bb2fa68faec40" + # } + # + # swap: createOrder + # + # { + # "code": 0, + # "data": "1590873693003714560" + # } + # + # spot and swap: createOrders + # + # { + # "order_id": "d64d92a5e0a120f792f385485bc3d95b", + # "instrument_id": "BTC_USDT", + # "amount": 0.0001, + # "price": 27000 + # } + # + # spot: fetchOrder, fetchOpenOrders, fetchOrders + # + # { + # "symbol": "BTC_USDT", + # "order_id": "dd3164b333a4afa9d5730bb87f6db8b3", + # "created_date": 1562303547, + # "finished_date": 0, + # "price": 0.1, + # "amount": 1, + # "cash_amount": 1, + # "executed_amount": 0, + # "avg_price": 0, + # "status": 1, + # "type": "buy", + # "kind": "margin" + # } + # + # swap: fetchOrder, fetchOpenOrders, fetchOrders + # + # { + # "order_id": "1590898207657824256", + # "instrument_id": "BTCUSDTPERP", + # "margin_mode": "crossed", + # "contract_val": "0.001", + # "type": 1, + # "order_type": 0, + # "price": "14000", + # "size": "6", + # "filled_qty": "0", + # "price_avg": "0", + # "fee": "0", + # "state": 0, + # "leverage": "20", + # "turnover": "0", + # "has_stop": 0, + # "insert_time": 1668134664828, + # "time_stamp": 1668134664828 + # } + # + timestamp = None + lastTradeTimestamp = None + timeInForce = None + type = None + side = self.safe_string(order, 'type') + marketId = self.safe_string_2(order, 'symbol', 'instrument_id') + symbol = self.safe_symbol(marketId, market) + market = self.market(symbol) + if market['type'] == 'swap': + orderType = self.safe_integer(order, 'order_type') + if orderType is not None: + if (orderType == 9) or (orderType == 10) or (orderType == 11) or (orderType == 12) or (orderType == 15): + timeInForce = 'FOK' + elif (orderType == 1) or (orderType == 2) or (orderType == 3) or (orderType == 4) or (orderType == 13): + timeInForce = 'IOC' + elif (orderType == 6) or (orderType == 7) or (orderType == 8) or (orderType == 14): + timeInForce = 'GTC' + if (orderType == 0) or (orderType == 1) or (orderType == 4) or (orderType == 5) or (orderType == 9) or (orderType == 10): + type = 'limit' + else: + type = 'market' + if side == '1': + side = 'open long' + elif side == '2': + side = 'open short' + elif side == '3': + side = 'close long' + elif side == '4': + side = 'close short' + timestamp = self.safe_integer(order, 'insert_time') + lastTradeTimestamp = self.safe_integer(order, 'time_stamp') + else: + timestamp = self.safe_timestamp(order, 'created_date') + lastTradeTimestamp = self.safe_timestamp(order, 'finished_date') + if side is not None: + parts = side.split('_') + numParts = len(parts) + if numParts > 1: + side = parts[0] + type = parts[1] + else: + type = 'limit' + return self.safe_order({ + 'info': order, + 'id': self.safe_string_2(order, 'order_id', 'data'), + 'clientOrderId': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': lastTradeTimestamp, + 'symbol': symbol, + 'type': type, + 'timeInForce': timeInForce, + 'postOnly': None, + 'side': side, + 'price': self.safe_number(order, 'price'), + 'triggerPrice': None, + 'amount': self.safe_number_2(order, 'amount', 'size'), + 'filled': self.safe_number_2(order, 'executed_amount', 'filled_qty'), + 'remaining': None, + 'cost': None, + 'average': self.safe_number_2(order, 'avg_price', 'price_avg'), + 'status': self.parse_order_status(self.safe_string_2(order, 'status', 'state')), + 'fee': { + 'cost': self.safe_number(order, 'fee'), + }, + 'trades': None, + }, market) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://docs.digifinex.com/en-ww/spot/v3/rest.html#current-active-orders + https://docs.digifinex.com/en-ww/swap/v2/rest.html#openorder + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + marketType = None + marketType, params = self.handle_market_type_and_params('fetchOpenOrders', market, params) + marginMode, query = self.handle_margin_mode_and_params('fetchOpenOrders', params) + request: dict = {} + swap = (marketType == 'swap') + if swap: + if since is not None: + request['start_timestamp'] = since + if limit is not None: + request['limit'] = limit + else: + request['market'] = marketType + if market is not None: + marketIdRequest = 'instrument_id' if swap else 'symbol' + request[marketIdRequest] = market['id'] + response = None + if marginMode is not None or marketType == 'margin': + marketType = 'margin' + response = await self.privateSpotGetMarginOrderCurrent(self.extend(request, query)) + elif marketType == 'spot': + response = await self.privateSpotGetSpotOrderCurrent(self.extend(request, query)) + elif marketType == 'swap': + response = await self.privateSwapGetTradeOpenOrders(self.extend(request, query)) + else: + raise NotSupported(self.id + ' fetchOpenOrders() not support self market type') + # + # spot and margin + # + # { + # "code": 0, + # "data": [ + # { + # "symbol": "BTC_USDT", + # "order_id": "dd3164b333a4afa9d5730bb87f6db8b3", + # "created_date": 1562303547, + # "finished_date": 0, + # "price": 0.1, + # "amount": 1, + # "cash_amount": 1, + # "executed_amount": 0, + # "avg_price": 0, + # "status": 1, + # "type": "buy", + # "kind": "margin" + # } + # ] + # } + # + # swap + # + # { + # "code": 0, + # "data": [ + # { + # "order_id": "1590898207657824256", + # "instrument_id": "BTCUSDTPERP", + # "margin_mode": "crossed", + # "contract_val": "0.001", + # "type": 1, + # "order_type": 0, + # "price": "14000", + # "size": "6", + # "filled_qty": "0", + # "price_avg": "0", + # "fee": "0", + # "state": 0, + # "leverage": "20", + # "turnover": "0", + # "has_stop": 0, + # "insert_time": 1668134664828, + # "time_stamp": 1668134664828 + # }, + # ... + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_orders(data, market, since, limit) + + async def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://docs.digifinex.com/en-ww/spot/v3/rest.html#get-all-orders-including-history-orders + https://docs.digifinex.com/en-ww/swap/v2/rest.html#historyorder + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + marketType = None + marketType, params = self.handle_market_type_and_params('fetchOrders', market, params) + marginMode, query = self.handle_margin_mode_and_params('fetchOrders', params) + request: dict = {} + if marketType == 'swap': + if since is not None: + request['start_timestamp'] = since + else: + request['market'] = marketType + if since is not None: + request['start_time'] = self.parse_to_int(since / 1000) # default 3 days from now, max 30 days + if market is not None: + marketIdRequest = 'instrument_id' if (marketType == 'swap') else 'symbol' + request[marketIdRequest] = market['id'] + if limit is not None: + request['limit'] = limit + response = None + if marginMode is not None or marketType == 'margin': + marketType = 'margin' + response = await self.privateSpotGetMarginOrderHistory(self.extend(request, query)) + elif marketType == 'spot': + response = await self.privateSpotGetSpotOrderHistory(self.extend(request, query)) + elif marketType == 'swap': + response = await self.privateSwapGetTradeHistoryOrders(self.extend(request, query)) + else: + raise NotSupported(self.id + ' fetchOrders() not support self market type') + # + # spot and margin + # + # { + # "code": 0, + # "data": [ + # { + # "symbol": "BTC_USDT", + # "order_id": "dd3164b333a4afa9d5730bb87f6db8b3", + # "created_date": 1562303547, + # "finished_date": 0, + # "price": 0.1, + # "amount": 1, + # "cash_amount": 1, + # "executed_amount": 0, + # "avg_price": 0, + # "status": 1, + # "type": "buy", + # "kind": "margin" + # } + # ] + # } + # + # swap + # + # { + # "code": 0, + # "data": [ + # { + # "order_id": "1590136768156405760", + # "instrument_id": "BTCUSDTPERP", + # "margin_mode": "crossed", + # "contract_val": "0.001", + # "type": 1, + # "order_type": 8, + # "price": "18660.2", + # "size": "1", + # "filled_qty": "1", + # "price_avg": "18514.5", + # "fee": "0.00925725", + # "state": 2, + # "leverage": "20", + # "turnover": "18.5145", + # "has_stop": 0, + # "insert_time": 1667953123526, + # "time_stamp": 1667953123596 + # }, + # ... + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_orders(data, market, since, limit) + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://docs.digifinex.com/en-ww/spot/v3/rest.html#get-order-status + https://docs.digifinex.com/en-ww/swap/v2/rest.html#orderinfo + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + marketType = None + marketType, params = self.handle_market_type_and_params('fetchOrder', market, params) + marginMode, query = self.handle_margin_mode_and_params('fetchOrder', params) + request: dict = { + 'order_id': id, + } + if marketType == 'swap': + if market is not None: + request['instrument_id'] = market['id'] + else: + request['market'] = marketType + response = None + if (marginMode is not None) or (marketType == 'margin'): + marketType = 'margin' + response = await self.privateSpotGetMarginOrder(self.extend(request, query)) + elif marketType == 'spot': + response = await self.privateSpotGetSpotOrder(self.extend(request, query)) + elif marketType == 'swap': + response = await self.privateSwapGetTradeOrderInfo(self.extend(request, query)) + else: + raise NotSupported(self.id + ' fetchOrder() not support self market type') + # + # spot and margin + # + # { + # "code": 0, + # "data": [ + # { + # "symbol": "BTC_USDT", + # "order_id": "dd3164b333a4afa9d5730bb87f6db8b3", + # "created_date": 1562303547, + # "finished_date": 0, + # "price": 0.1, + # "amount": 1, + # "cash_amount": 1, + # "executed_amount": 0, + # "avg_price": 0, + # "status": 1, + # "type": "buy", + # "kind": "margin" + # } + # ] + # } + # + # swap + # + # { + # "code": 0, + # "data": { + # "order_id": "1590923061186531328", + # "instrument_id": "ETHUSDTPERP", + # "margin_mode": "crossed", + # "contract_val": "0.01", + # "type": 1, + # "order_type": 0, + # "price": "900", + # "size": "6", + # "filled_qty": "0", + # "price_avg": "0", + # "fee": "0", + # "state": 0, + # "leverage": "20", + # "turnover": "0", + # "has_stop": 0, + # "insert_time": 1668140590372, + # "time_stamp": 1668140590372 + # } + # } + # + data = self.safe_value(response, 'data') + order = data if (marketType == 'swap') else self.safe_value(data, 0) + if order is None: + raise OrderNotFound(self.id + ' fetchOrder() order ' + str(id) + ' not found') + return self.parse_order(order, market) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://docs.digifinex.com/en-ww/spot/v3/rest.html#customer-39-s-trades + https://docs.digifinex.com/en-ww/swap/v2/rest.html#historytrade + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = None + request: dict = {} + if symbol is not None: + market = self.market(symbol) + marketType = None + marketType, params = self.handle_market_type_and_params('fetchMyTrades', market, params) + marginMode, query = self.handle_margin_mode_and_params('fetchMyTrades', params) + if marketType == 'swap': + if since is not None: + request['start_timestamp'] = since + else: + request['market'] = marketType + if since is not None: + request['start_time'] = self.parse_to_int(since / 1000) # default 3 days from now, max 30 days + marketIdRequest = 'instrument_id' if (marketType == 'swap') else 'symbol' + if symbol is not None: + request[marketIdRequest] = market['id'] + if limit is not None: + request['limit'] = limit + response = None + if marginMode is not None or marketType == 'margin': + marketType = 'margin' + response = await self.privateSpotGetMarginMytrades(self.extend(request, query)) + elif marketType == 'spot': + response = await self.privateSpotGetSpotMytrades(self.extend(request, query)) + elif marketType == 'swap': + response = await self.privateSwapGetTradeHistoryTrades(self.extend(request, query)) + else: + raise NotSupported(self.id + ' fetchMyTrades() not support self market type') + # + # spot and margin + # + # { + # "list":[ + # { + # "timestamp":1639506068, + # "is_maker":false, + # "id":"8975951332", + # "amount":31.83, + # "side":"sell_market", + # "symbol":"DOGE_USDT", + # "fee_currency":"USDT", + # "fee":0.01163774826 + # ,"order_id":"32b169792f4a7a19e5907dc29fc123d4", + # "price":0.182811 + # } + # ], + # "code": 0 + # } + # + # swap + # + # { + # "code": 0, + # "data": [ + # { + # "trade_id": "1590136768424841218", + # "instrument_id": "BTCUSDTPERP", + # "order_id": "1590136768156405760", + # "type": 1, + # "order_type": 8, + # "price": "18514.5", + # "size": "1", + # "fee": "0.00925725", + # "close_profit": "0", + # "leverage": "20", + # "trade_type": 0, + # "match_role": 1, + # "trade_time": 1667953123562 + # }, + # ... + # ] + # } + # + responseRequest = 'data' if (marketType == 'swap') else 'list' + data = self.safe_list(response, responseRequest, []) + return self.parse_trades(data, market, since, limit) + + def parse_ledger_entry_type(self, type): + types: dict = {} + return self.safe_string(types, type, type) + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + # + # spot and margin + # + # { + # "currency_mark": "BTC", + # "type": 100234, + # "num": -10, + # "balance": 0.1, + # "time": 1546272000 + # } + # + # swap + # + # { + # "currency": "USDT", + # "finance_type": 17, + # "change": "-3.01", + # "timestamp": 1650809432000 + # } + # + type = self.parse_ledger_entry_type(self.safe_string_2(item, 'type', 'finance_type')) + currencyId = self.safe_string_2(item, 'currency_mark', 'currency') + code = self.safe_currency_code(currencyId, currency) + currency = self.safe_currency(currencyId, currency) + amount = self.safe_number_2(item, 'num', 'change') + after = self.safe_number(item, 'balance') + timestamp = self.safe_timestamp(item, 'time') + if timestamp is None: + timestamp = self.safe_integer(item, 'timestamp') + return self.safe_ledger_entry({ + 'info': item, + 'id': None, + 'direction': None, + 'account': None, + 'referenceId': None, + 'referenceAccount': None, + 'type': type, + 'currency': code, + 'amount': amount, + 'before': None, + 'after': after, + 'status': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'fee': None, + }, currency) + + async def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + + https://docs.digifinex.com/en-ww/spot/v3/rest.html#spot-margin-otc-financial-logs + https://docs.digifinex.com/en-ww/swap/v2/rest.html#bills + + :param str [code]: unified currency code, default is None + :param int [since]: timestamp in ms of the earliest ledger entry, default is None + :param int [limit]: max number of ledger entries to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ledger structure ` + """ + await self.load_markets() + request: dict = {} + marketType = None + marketType, params = self.handle_market_type_and_params('fetchLedger', None, params) + marginMode, query = self.handle_margin_mode_and_params('fetchLedger', params) + if marketType == 'swap': + if since is not None: + request['start_timestamp'] = since + else: + request['market'] = marketType + if since is not None: + request['start_time'] = self.parse_to_int(since / 1000) # default 3 days from now, max 30 days + currencyIdRequest = 'currency' if (marketType == 'swap') else 'currency_mark' + currency = None + if code is not None: + currency = self.currency(code) + request[currencyIdRequest] = currency['id'] + if limit is not None: + request['limit'] = limit + response = None + if marginMode is not None or marketType == 'margin': + marketType = 'margin' + response = await self.privateSpotGetMarginFinancelog(self.extend(request, query)) + elif marketType == 'spot': + response = await self.privateSpotGetSpotFinancelog(self.extend(request, query)) + elif marketType == 'swap': + response = await self.privateSwapGetAccountFinanceRecord(self.extend(request, query)) + else: + raise NotSupported(self.id + ' fetchLedger() not support self market type') + # + # spot and margin + # + # { + # "code": 0, + # "data": { + # "total": 521, + # "finance": [ + # { + # "currency_mark": "BTC", + # "type": 100234, + # "num": 28457, + # "balance": 0.1, + # "time": 1546272000 + # } + # ] + # } + # } + # + # swap + # + # { + # "code": 0, + # "data": [ + # { + # "currency": "USDT", + # "finance_type": 17, + # "change": "3.01", + # "timestamp": 1650809432000 + # }, + # ] + # } + # + ledger = None + if marketType == 'swap': + ledger = self.safe_value(response, 'data', []) + else: + data = self.safe_value(response, 'data', {}) + ledger = self.safe_value(data, 'finance', []) + return self.parse_ledger(ledger, currency, since, limit) + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + # + # { + # "addressTag":"", + # "address":"0xf1104d9f8624f89775a3e9d480fc0e75a8ef4373", + # "currency":"USDT", + # "chain":"ERC20" + # } + # + address = self.safe_string(depositAddress, 'address') + tag = self.safe_string(depositAddress, 'addressTag') + currencyId = self.safe_string_upper(depositAddress, 'currency') + code = self.safe_currency_code(currencyId) + return { + 'info': depositAddress, + 'currency': code, + 'network': None, + 'address': address, + 'tag': tag, + } + + async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + } + response = await self.privateSpotGetDepositAddress(self.extend(request, params)) + # + # { + # "data":[ + # { + # "addressTag":"", + # "address":"0xf1104d9f8624f89775a3e9d480fc0e75a8ef4373", + # "currency":"USDT", + # "chain":"ERC20" + # } + # ], + # "code":200 + # } + # + data = self.safe_value(response, 'data', []) + addresses = self.parse_deposit_addresses(data, [currency['code']]) + address = self.safe_value(addresses, code) + if address is None: + raise InvalidAddress(self.id + ' fetchDepositAddress() did not return an address for ' + code + ' - create the deposit address in the user settings on the exchange website first.') + return address + + async def fetch_transactions_by_type(self, type, code: Str = None, since: Int = None, limit: Int = None, params={}): + await self.load_markets() + currency = None + request: dict = { + # 'currency': currency['id'], + # 'from': 'fromId', # When direct is' prev ', from is 1, returning from old to new ascending, when direct is' next ', from is the ID of the most recent record, returned from the old descending order + # 'size': 100, # default 100, max 500 + # 'direct': 'prev', # "prev" ascending, "next" descending + } + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + if limit is not None: + request['size'] = min(500, limit) + response = None + if type == 'deposit': + response = await self.privateSpotGetDepositHistory(self.extend(request, params)) + else: + response = await self.privateSpotGetWithdrawHistory(self.extend(request, params)) + # + # { + # "code": 200, + # "data": [ + # { + # "id": 1171, + # "currency": "xrp", + # "hash": "ed03094b84eafbe4bc16e7ef766ee959885ee5bcb265872baaa9c64e1cf86c2b", + # "chain": "", + # "amount": 7.457467, + # "address": "rae93V8d2mdoUQHwBDBdM4NHCMehRJAsbm", + # "memo": "100040", + # "fee": 0, + # "state": "safe", + # "created_date": "2020-04-20 11:23:00", + # "finished_date": "2020-04-20 13:23:00" + # }, + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_transactions(data, currency, since, limit, {'type': type}) + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + return await self.fetch_transactions_by_type('deposit', code, since, limit, params) + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + return await self.fetch_transactions_by_type('withdrawal', code, since, limit, params) + + def parse_transaction_status(self, status: Str): + # deposit state includes: 1(in deposit), 2(to be confirmed), 3(successfully deposited), 4(stopped) + # withdrawal state includes: 1(application in progress), 2(to be confirmed), 3(completed), 4(rejected) + statuses: dict = { + '1': 'pending', # in Progress + '2': 'pending', # to be confirmed + '3': 'ok', # Completed + '4': 'failed', # Rejected + } + return self.safe_string(statuses, status, status) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # withdraw + # + # { + # "code": 200, + # "withdraw_id": 700 + # } + # + # fetchDeposits, fetchWithdrawals + # + # { + # "id": 1171, + # "currency": "xrp", + # "hash": "ed03094b84eafbe4bc16e7ef766ee959885ee5bcb265872baaa9c64e1cf86c2b", + # "chain": "", + # "amount": 7.457467, + # "address": "rae93V8d2mdoUQHwBDBdM4NHCMehRJAsbm", + # "memo": "100040", + # "fee": 0, + # "state": "safe", + # "created_date": "2020-04-20 11:23:00", + # "finished_date": "2020-04-20 13:23:00" + # } + # + id = self.safe_string_2(transaction, 'id', 'withdraw_id') + address = self.safe_string(transaction, 'address') + tag = self.safe_string(transaction, 'memo') + txid = self.safe_string(transaction, 'hash') + currencyId = self.safe_string_upper(transaction, 'currency') + code = self.safe_currency_code(currencyId, currency) + timestamp = self.parse8601(self.safe_string(transaction, 'created_date')) + updated = self.parse8601(self.safe_string(transaction, 'finished_date')) + status = self.parse_transaction_status(self.safe_string(transaction, 'state')) + amount = self.safe_number(transaction, 'amount') + feeCost = self.safe_number(transaction, 'fee') + fee = None + if feeCost is not None: + fee = {'currency': code, 'cost': feeCost} + network = self.safe_string(transaction, 'chain') + return { + 'info': transaction, + 'id': id, + 'txid': txid, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': network, + 'address': address, + 'addressTo': address, + 'addressFrom': None, + 'tag': tag, + 'tagTo': tag, + 'tagFrom': None, + 'type': None, + 'amount': amount, + 'currency': code, + 'status': status, + 'updated': updated, + 'internal': None, + 'comment': None, + 'fee': fee, + } + + def parse_transfer_status(self, status: Str) -> Str: + statuses: dict = { + '0': 'ok', + } + return self.safe_string(statuses, status, status) + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + # + # transfer between spot, margin and OTC + # + # { + # "code": 0 + # } + # + # transfer between spot and swap + # + # { + # "code": 0, + # "data": { + # "type": 2, + # "currency": "USDT", + # "transfer_amount": "5" + # } + # } + # + # fetchTransfers + # + # { + # "transfer_id": 130524, + # "type": 1, + # "currency": "USDT", + # "amount": "24", + # "timestamp": 1666505659000 + # } + # + fromAccount = None + toAccount = None + data = self.safe_dict(transfer, 'data', transfer) + type = self.safe_integer(data, 'type') + if type == 1: + fromAccount = 'spot' + toAccount = 'swap' + elif type == 2: + fromAccount = 'swap' + toAccount = 'spot' + timestamp = self.safe_integer(transfer, 'timestamp') + return { + 'info': transfer, + 'id': self.safe_string(transfer, 'transfer_id'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'currency': self.safe_currency_code(self.safe_string(data, 'currency'), currency), + 'amount': self.safe_number_2(data, 'amount', 'transfer_amount'), + 'fromAccount': fromAccount, + 'toAccount': toAccount, + 'status': self.parse_transfer_status(self.safe_string(transfer, 'code')), + } + + async def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://docs.digifinex.com/en-ww/spot/v3/rest.html#transfer-assets-among-accounts + https://docs.digifinex.com/en-ww/swap/v2/rest.html#accounttransfer + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: 'spot', 'swap', 'margin', 'OTC' - account to transfer from + :param str toAccount: 'spot', 'swap', 'margin', 'OTC' - account to transfer to + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transfer structure ` + """ + await self.load_markets() + currency = self.currency(code) + currencyId = currency['id'] + accountsByType = self.safe_value(self.options, 'accountsByType', {}) + fromId = self.safe_string(accountsByType, fromAccount, fromAccount) + toId = self.safe_string(accountsByType, toAccount, toAccount) + request = {} + fromSwap = (fromAccount == 'swap') + toSwap = (toAccount == 'swap') + response = None + amountString = self.currency_to_precision(code, amount) + if fromSwap or toSwap: + if (fromId != '1') and (toId != '1'): + raise ExchangeError(self.id + ' transfer() supports transferring between spot and swap, spot and margin, spot and OTC only') + request['type'] = 1 if toSwap else 2 # 1 = spot to swap, 2 = swap to spot + request['currency'] = currencyId + request['transfer_amount'] = amountString + # + # { + # "code": 0, + # "data": { + # "type": 2, + # "currency": "USDT", + # "transfer_amount": "5" + # } + # } + # + response = await self.privateSwapPostAccountTransfer(self.extend(request, params)) + else: + request['currency_mark'] = currencyId + request['num'] = amountString + request['from'] = fromId # 1 = SPOT, 2 = MARGIN, 3 = OTC + request['to'] = toId # 1 = SPOT, 2 = MARGIN, 3 = OTC + # + # { + # "code": 0 + # } + # + response = await self.privateSpotPostTransfer(self.extend(request, params)) + return self.parse_transfer(response, currency) + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.check_address(address) + await self.load_markets() + currency = self.currency(code) + request: dict = { + # 'chain': 'ERC20', 'OMNI', 'TRC20', # required for USDT + 'address': address, + 'amount': self.currency_to_precision(code, amount), + 'currency': currency['id'], + } + if tag is not None: + request['memo'] = tag + response = await self.privateSpotPostWithdrawNew(self.extend(request, params)) + # + # { + # "code": 200, + # "withdraw_id": 700 + # } + # + return self.parse_transaction(response, currency) + + async def fetch_borrow_interest(self, code: Str = None, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[BorrowInterest]: + await self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + response = await self.privateSpotGetMarginPositions(self.extend(request, params)) + # + # { + # "margin": "45.71246418952618", + # "code": 0, + # "margin_rate": "7.141978570340037", + # "positions": [ + # { + # "amount": 0.0006103, + # "side": "go_long", + # "entry_price": 31428.72, + # "liquidation_rate": 0.3, + # "liquidation_price": 10225.335481159, + # "unrealized_roe": -0.0076885829266987, + # "symbol": "BTC_USDT", + # "unrealized_pnl": -0.049158102631999, + # "leverage_ratio": 3 + # } + # ], + # "unrealized_pnl": "-0.049158102631998504" + # } + # + rows = self.safe_value(response, 'positions') + interest = self.parse_borrow_interests(rows, market) + return self.filter_by_currency_since_limit(interest, code, since, limit) + + def parse_borrow_interest(self, info: dict, market: Market = None) -> BorrowInterest: + # + # { + # "amount": 0.0006103, + # "side": "go_long", + # "entry_price": 31428.72, + # "liquidation_rate": 0.3, + # "liquidation_price": 10225.335481159, + # "unrealized_roe": -0.0076885829266987, + # "symbol": "BTC_USDT", + # "unrealized_pnl": -0.049158102631999, + # "leverage_ratio": 3 + # } + # + marketId = self.safe_string(info, 'symbol') + amountString = self.safe_string(info, 'amount') + leverageString = self.safe_string(info, 'leverage_ratio') + amountInvested = Precise.string_div(amountString, leverageString) + amountBorrowed = Precise.string_sub(amountString, amountInvested) + currency = None if (market is None) else market['base'] + symbol = self.safe_symbol(marketId, market) + return { + 'info': info, + 'symbol': symbol, + 'currency': currency, + 'interest': None, + 'interestRate': 0.001, # all interest rates on digifinex are 0.1% + 'amountBorrowed': self.parse_number(amountBorrowed), + 'marginMode': None, + 'timestamp': None, + 'datetime': None, + } + + async def fetch_cross_borrow_rate(self, code: str, params={}) -> CrossBorrowRate: + """ + fetch the rate of interest to borrow a currency for margin trading + + https://docs.digifinex.com/en-ww/spot/v3/rest.html#margin-assets + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `borrow rate structure ` + """ + await self.load_markets() + request: dict = {} + response = await self.privateSpotGetMarginAssets(self.extend(request, params)) + # + # { + # "list": [ + # { + # "valuation_rate": 1, + # "total": 1.92012186174, + # "free": 1.92012186174, + # "currency": "USDT" + # }, + # ], + # "total": 45.133305540922, + # "code": 0, + # "unrealized_pnl": 0, + # "free": 45.133305540922, + # "equity": 45.133305540922 + # } + # + data = self.safe_value(response, 'list', []) + result = [] + for i in range(0, len(data)): + entry = data[i] + if self.safe_string(entry, 'currency') == code: + result = entry + currency = self.currency(code) + return self.parse_borrow_rate(result, currency) + + async def fetch_cross_borrow_rates(self, params={}) -> CrossBorrowRates: + """ + fetch the borrow interest rates of all currencies + + https://docs.digifinex.com/en-ww/spot/v3/rest.html#margin-assets + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `borrow rate structures ` + """ + await self.load_markets() + response = await self.privateSpotGetMarginAssets(params) + # + # { + # "list": [ + # { + # "valuation_rate": 1, + # "total": 1.92012186174, + # "free": 1.92012186174, + # "currency": "USDT" + # }, + # ], + # "total": 45.133305540922, + # "code": 0, + # "unrealized_pnl": 0, + # "free": 45.133305540922, + # "equity": 45.133305540922 + # } + # + result = self.safe_value(response, 'list', []) + return self.parse_borrow_rates(result, 'currency') + + def parse_borrow_rate(self, info, currency: Currency = None): + # + # { + # "valuation_rate": 1, + # "total": 1.92012186174, + # "free": 1.92012186174, + # "currency": "USDT" + # } + # + timestamp = self.milliseconds() + currencyId = self.safe_string(info, 'currency') + return { + 'currency': self.safe_currency_code(currencyId, currency), + 'rate': 0.001, # all interest rates on digifinex are 0.1% + 'period': 86400000, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'info': info, + } + + def parse_borrow_rates(self, info, codeKey): + # + # { + # "valuation_rate": 1, + # "total": 1.92012186174, + # "free": 1.92012186174, + # "currency": "USDT" + # }, + # + result: dict = {} + for i in range(0, len(info)): + item = info[i] + currency = self.safe_string(item, codeKey) + code = self.safe_currency_code(currency) + borrowRate = self.parse_borrow_rate(item) + result[code] = borrowRate + return result + + async def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate + + https://docs.digifinex.com/en-ww/swap/v2/rest.html#currentfundingrate + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + await self.load_markets() + market = self.market(symbol) + if not market['swap']: + raise BadSymbol(self.id + ' fetchFundingRate() supports swap contracts only') + request: dict = { + 'instrument_id': market['id'], + } + response = await self.publicSwapGetPublicFundingRate(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "instrument_id": "BTCUSDTPERP", + # "funding_rate": "-0.00012", + # "funding_time": 1662710400000, + # "next_funding_rate": "0.0001049907085171607", + # "next_funding_time": 1662739200000 + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_funding_rate(data, market) + + async def fetch_funding_interval(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate interval + + https://docs.digifinex.com/en-ww/swap/v2/rest.html#currentfundingrate + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + return await self.fetch_funding_rate(symbol, params) + + def parse_funding_rate(self, contract, market: Market = None) -> FundingRate: + # + # { + # "instrument_id": "BTCUSDTPERP", + # "funding_rate": "-0.00012", + # "funding_time": 1662710400000, + # "next_funding_rate": "0.0001049907085171607", + # "next_funding_time": 1662739200000 + # } + # + marketId = self.safe_string(contract, 'instrument_id') + timestamp = self.safe_integer(contract, 'funding_time') + nextTimestamp = self.safe_integer(contract, 'next_funding_time') + fundingTimeString = self.safe_string(contract, 'funding_time') + nextFundingTimeString = self.safe_string(contract, 'next_funding_time') + millisecondsInterval = Precise.string_sub(nextFundingTimeString, fundingTimeString) + return { + 'info': contract, + 'symbol': self.safe_symbol(marketId, market), + 'markPrice': None, + 'indexPrice': None, + 'interestRate': None, + 'estimatedSettlePrice': None, + 'timestamp': None, + 'datetime': None, + 'fundingRate': self.safe_number(contract, 'funding_rate'), + 'fundingTimestamp': timestamp, + 'fundingDatetime': self.iso8601(timestamp), + 'nextFundingRate': self.safe_number(contract, 'next_funding_rate'), + 'nextFundingTimestamp': nextTimestamp, + 'nextFundingDatetime': self.iso8601(nextTimestamp), + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': self.parse_funding_interval(millisecondsInterval), + } + + def parse_funding_interval(self, interval): + intervals: dict = { + '3600000': '1h', + '14400000': '4h', + '28800000': '8h', + '57600000': '16h', + '86400000': '24h', + } + return self.safe_string(intervals, interval, interval) + + async def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical funding rate prices + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: timestamp in ms of the earliest funding rate to fetch + :param int [limit]: the maximum amount of `funding rate structures ` to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `funding rate structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchFundingRateHistory() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + if not market['swap']: + raise BadSymbol(self.id + ' fetchFundingRateHistory() supports swap contracts only') + request: dict = { + 'instrument_id': market['id'], + } + if since is not None: + request['start_timestamp'] = since + if limit is not None: + request['limit'] = limit + response = await self.publicSwapGetPublicFundingRateHistory(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "instrument_id": "BTCUSDTPERP", + # "funding_rates": [ + # { + # "rate": "-0.00375", + # "time": 1607673600000 + # }, + # ... + # ] + # } + # } + # + data = self.safe_value(response, 'data', {}) + result = self.safe_value(data, 'funding_rates', []) + rates = [] + for i in range(0, len(result)): + entry = result[i] + marketId = self.safe_string(data, 'instrument_id') + symbolInner = self.safe_symbol(marketId) + timestamp = self.safe_integer(entry, 'time') + rates.append({ + 'info': entry, + 'symbol': symbolInner, + 'fundingRate': self.safe_number(entry, 'rate'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + sorted = self.sort_by(rates, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, symbol, since, limit) + + async def fetch_trading_fee(self, symbol: str, params={}) -> TradingFeeInterface: + """ + fetch the trading fees for a market + + https://docs.digifinex.com/en-ww/swap/v2/rest.html#tradingfee + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `fee structure ` + """ + await self.load_markets() + market = self.market(symbol) + if not market['swap']: + raise BadRequest(self.id + ' fetchTradingFee() supports swap markets only') + request: dict = { + 'instrument_id': market['id'], + } + response = await self.privateSwapGetAccountTradingFeeRate(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "instrument_id": "BTCUSDTPERP", + # "taker_fee_rate": "0.0005", + # "maker_fee_rate": "0.0003" + # } + # } + # + data = self.safe_value(response, 'data', {}) + return self.parse_trading_fee(data, market) + + def parse_trading_fee(self, fee: dict, market: Market = None) -> TradingFeeInterface: + # + # { + # "instrument_id": "BTCUSDTPERP", + # "taker_fee_rate": "0.0005", + # "maker_fee_rate": "0.0003" + # } + # + marketId = self.safe_string(fee, 'instrument_id') + symbol = self.safe_symbol(marketId, market) + return { + 'info': fee, + 'symbol': symbol, + 'maker': self.safe_number(fee, 'maker_fee_rate'), + 'taker': self.safe_number(fee, 'taker_fee_rate'), + 'percentage': None, + 'tierBased': None, + } + + async def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://docs.digifinex.com/en-ww/spot/v3/rest.html#margin-positions + https://docs.digifinex.com/en-ww/swap/v2/rest.html#positions + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + request: dict = {} + market = None + marketType = None + if symbols is not None: + symbol = None + if isinstance(symbols, list): + symbolsLength = len(symbols) + if symbolsLength > 1: + raise BadRequest(self.id + ' fetchPositions() symbols argument cannot contain more than 1 symbol') + symbol = symbols[0] + else: + symbol = symbols + market = self.market(symbol) + marketType, params = self.handle_market_type_and_params('fetchPositions', market, params) + marginMode, query = self.handle_margin_mode_and_params('fetchPositions', params) + if marginMode is not None: + marketType = 'margin' + if market is not None: + marketIdRequest = 'instrument_id' if (marketType == 'swap') else 'symbol' + request[marketIdRequest] = market['id'] + response = None + if marketType == 'spot' or marketType == 'margin': + response = await self.privateSpotGetMarginPositions(self.extend(request, query)) + elif marketType == 'swap': + response = await self.privateSwapGetAccountPositions(self.extend(request, query)) + else: + raise NotSupported(self.id + ' fetchPositions() not support self market type') + # + # swap + # + # { + # "code": 0, + # "data": [ + # { + # "instrument_id": "BTCUSDTPERP", + # "margin_mode": "crossed", + # "avail_position": "1", + # "avg_cost": "18369.3", + # "last": "18404.7", + # "leverage": "20", + # "liquidation_price": "451.12820512820264", + # "maint_margin_ratio": "0.005", + # "margin": "0.918465", + # "position": "1", + # "realized_pnl": "0", + # "unrealized_pnl": "0.03410000000000224", + # "unrealized_pnl_rate": "0.03712716325608732", + # "side": "long", + # "open_outstanding": "0", + # "risk_score": "0.495049504950495", + # "margin_ratio": "0.4029464788983229", + # "timestamp": 1667960497145 + # }, + # ... + # ] + # } + # + # margin + # + # { + # "margin": "77.71534772983289", + # "code": 0, + # "margin_rate": "10.284503769497306", + # "positions": [ + # { + # "amount": 0.0010605, + # "side": "go_long", + # "entry_price": 18321.39, + # "liquidation_rate": 0.3, + # "liquidation_price": -52754.371758471, + # "unrealized_roe": -0.002784390267332, + # "symbol": "BTC_USDT", + # "unrealized_pnl": -0.010820048189999, + # "leverage_ratio": 5 + # }, + # ... + # ], + # "unrealized_pnl": "-0.10681600018999979" + # } + # + positionRequest = 'data' if (marketType == 'swap') else 'positions' + positions = self.safe_value(response, positionRequest, []) + result = [] + for i in range(0, len(positions)): + result.append(self.parse_position(positions[i], market)) + return self.filter_by_array_positions(result, 'symbol', symbols, False) + + async def fetch_position(self, symbol: str, params={}): + """ + + https://docs.digifinex.com/en-ww/spot/v3/rest.html#margin-positions + https://docs.digifinex.com/en-ww/swap/v2/rest.html#positions + + fetch data on a single open contract trade position + :param str symbol: unified market symbol of the market the position is held in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `position structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = {} + marketType = None + marketType, params = self.handle_market_type_and_params('fetchPosition', market, params) + marginMode, query = self.handle_margin_mode_and_params('fetchPosition', params) + if marginMode is not None: + marketType = 'margin' + marketIdRequest = 'instrument_id' if (marketType == 'swap') else 'symbol' + request[marketIdRequest] = market['id'] + response = None + if marketType == 'spot' or marketType == 'margin': + response = await self.privateSpotGetMarginPositions(self.extend(request, query)) + elif marketType == 'swap': + response = await self.privateSwapGetAccountPositions(self.extend(request, query)) + else: + raise NotSupported(self.id + ' fetchPosition() not support self market type') + # + # swap + # + # { + # "code": 0, + # "data": [ + # { + # "instrument_id": "BTCUSDTPERP", + # "margin_mode": "crossed", + # "avail_position": "1", + # "avg_cost": "18369.3", + # "last": "18388.9", + # "leverage": "20", + # "liquidation_price": "383.38712921065553", + # "maint_margin_ratio": "0.005", + # "margin": "0.918465", + # "position": "1", + # "realized_pnl": "0", + # "unrealized_pnl": "0.021100000000004115", + # "unrealized_pnl_rate": "0.02297311274790451", + # "side": "long", + # "open_outstanding": "0", + # "risk_score": "0.4901960784313725", + # "margin_ratio": "0.40486964045976204", + # "timestamp": 1667960241758 + # } + # ] + # } + # + # margin + # + # { + # "margin": "77.71534772983289", + # "code": 0, + # "margin_rate": "10.284503769497306", + # "positions": [ + # { + # "amount": 0.0010605, + # "side": "go_long", + # "entry_price": 18321.39, + # "liquidation_rate": 0.3, + # "liquidation_price": -52754.371758471, + # "unrealized_roe": -0.002784390267332, + # "symbol": "BTC_USDT", + # "unrealized_pnl": -0.010820048189999, + # "leverage_ratio": 5 + # } + # ], + # "unrealized_pnl": "-0.10681600018999979" + # } + # + dataRequest = 'data' if (marketType == 'swap') else 'positions' + data = self.safe_value(response, dataRequest, []) + position = self.parse_position(data[0], market) + if marketType == 'swap': + return position + else: + position['collateral'] = self.safe_number(response, 'margin') + position['marginRatio'] = self.safe_number(response, 'margin_rate') + return position + + def parse_position(self, position: dict, market: Market = None): + # + # swap + # + # { + # "instrument_id": "BTCUSDTPERP", + # "margin_mode": "crossed", + # "avail_position": "1", + # "avg_cost": "18369.3", + # "last": "18388.9", + # "leverage": "20", + # "liquidation_price": "383.38712921065553", + # "maint_margin_ratio": "0.005", + # "margin": "0.918465", + # "position": "1", + # "realized_pnl": "0", + # "unrealized_pnl": "0.021100000000004115", + # "unrealized_pnl_rate": "0.02297311274790451", + # "side": "long", + # "open_outstanding": "0", + # "risk_score": "0.4901960784313725", + # "margin_ratio": "0.40486964045976204", + # "timestamp": 1667960241758 + # } + # + # margin + # + # { + # "amount": 0.0010605, + # "side": "go_long", + # "entry_price": 18321.39, + # "liquidation_rate": 0.3, + # "liquidation_price": -52754.371758471, + # "unrealized_roe": -0.002784390267332, + # "symbol": "BTC_USDT", + # "unrealized_pnl": -0.010820048189999, + # "leverage_ratio": 5 + # } + # + marketId = self.safe_string_2(position, 'instrument_id', 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + marginMode = self.safe_string(position, 'margin_mode') + if marginMode is not None: + marginMode = 'cross' if (marginMode == 'crossed') else 'isolated' + else: + marginMode = 'crossed' + timestamp = self.safe_integer(position, 'timestamp') + side = self.safe_string(position, 'side') + if side == 'go_long': + side = 'long' + elif side == 'go_short': + side = 'short' + return self.safe_position({ + 'info': position, + 'id': None, + 'symbol': symbol, + 'notional': self.safe_number(position, 'amount'), + 'marginMode': marginMode, + 'liquidationPrice': self.safe_number(position, 'liquidation_price'), + 'entryPrice': self.safe_number_2(position, 'avg_cost', 'entry_price'), + 'unrealizedPnl': self.safe_number(position, 'unrealized_pnl'), + 'contracts': self.safe_number(position, 'avail_position'), + 'contractSize': self.safe_number(market, 'contractSize'), + 'markPrice': self.safe_number(position, 'last'), + 'side': side, + 'hedged': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'maintenanceMargin': self.safe_number(position, 'margin'), + 'maintenanceMarginPercentage': self.safe_number(position, 'maint_margin_ratio'), + 'collateral': None, + 'initialMargin': None, + 'initialMarginPercentage': None, + 'leverage': self.safe_number_2(position, 'leverage', 'leverage_ratio'), + 'marginRatio': self.safe_number(position, 'margin_ratio'), + 'percentage': None, + 'stopLossPrice': None, + 'takeProfitPrice': None, + }) + + async def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://docs.digifinex.com/en-ww/swap/v2/rest.html#setleverage + + :param float leverage: the rate of leverage + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: either 'cross' or 'isolated', default is cross + :param str [params.side]: either 'long' or 'short', required for isolated markets only + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setLeverage() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + if market['type'] != 'swap': + raise BadSymbol(self.id + ' setLeverage() supports swap contracts only') + if (leverage < 1) or (leverage > 100): + raise BadRequest(self.id + ' leverage should be between 1 and 100') + request: dict = { + 'instrument_id': market['id'], + 'leverage': leverage, + } + defaultMarginMode = self.safe_string_2(self.options, 'marginMode', 'defaultMarginMode') + marginMode = self.safe_string_lower_2(params, 'marginMode', 'defaultMarginMode', defaultMarginMode) + if marginMode is not None: + marginMode = 'crossed' if (marginMode == 'cross') else 'isolated' + request['margin_mode'] = marginMode + params = self.omit(params, ['marginMode', 'defaultMarginMode']) + if marginMode == 'isolated': + side = self.safe_string(params, 'side') + if side is not None: + request['side'] = side + params = self.omit(params, 'side') + else: + self.check_required_argument('setLeverage', side, 'side', ['long', 'short']) + return await self.privateSwapPostAccountLeverage(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "instrument_id": "BTCUSDTPERP", + # "leverage": 30, + # "margin_mode": "crossed", + # "side": "both" + # } + # } + # + + async def fetch_transfers(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[TransferEntry]: + """ + fetch the transfer history, only transfers between spot and swap accounts are supported + + https://docs.digifinex.com/en-ww/swap/v2/rest.html#transferrecord + + :param str code: unified currency code of the currency transferred + :param int [since]: the earliest time in ms to fetch transfers for + :param int [limit]: the maximum number of transfers to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transfer structures ` + """ + await self.load_markets() + currency = None + request: dict = {} + if code is not None: + currency = self.safe_currency_code(code) + request['currency'] = currency['id'] + if since is not None: + request['start_timestamp'] = since + if limit is not None: + request['limit'] = limit # default 20 max 100 + response = await self.privateSwapGetAccountTransferRecord(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "transfer_id": 130524, + # "type": 1, + # "currency": "USDT", + # "amount": "24", + # "timestamp": 1666505659000 + # }, + # ... + # ] + # } + # + transfers = self.safe_list(response, 'data', []) + return self.parse_transfers(transfers, currency, since, limit) + + async def fetch_leverage_tiers(self, symbols: Strings = None, params={}) -> LeverageTiers: + """ + + https://docs.digifinex.com/en-ww/swap/v2/rest.html#instruments + + retrieve information on the maximum leverage, for different trade sizes + :param str[]|None symbols: a list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `leverage tiers structures `, indexed by market symbols + """ + await self.load_markets() + response = await self.publicSwapGetPublicInstruments(params) + # + # { + # "code": 0, + # "data": [ + # { + # "instrument_id": "BTCUSDTPERP", + # "type": "REAL", + # "contract_type": "PERPETUAL", + # "base_currency": "BTC", + # "quote_currency": "USDT", + # "clear_currency": "USDT", + # "contract_value": "0.001", + # "contract_value_currency": "BTC", + # "is_inverse": False, + # "is_trading": True, + # "status": "ONLINE", + # "price_precision": 1, + # "tick_size": "0.1", + # "min_order_amount": 1, + # "open_max_limits": [ + # { + # "leverage": "50", + # "max_limit": "1000000" + # }, + # ] + # }, + # ] + # } + # + data = self.safe_value(response, 'data', []) + symbols = self.market_symbols(symbols) + return self.parse_leverage_tiers(data, symbols, 'instrument_id') + + async def fetch_market_leverage_tiers(self, symbol: str, params={}) -> List[LeverageTier]: + """ + retrieve information on the maximum leverage, for different trade sizes for a single market + + https://docs.digifinex.com/en-ww/swap/v2/rest.html#instrument + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `leverage tiers structure ` + """ + await self.load_markets() + market = self.market(symbol) + if not market['swap']: + raise BadRequest(self.id + ' fetchMarketLeverageTiers() supports swap markets only') + request: dict = { + 'instrument_id': market['id'], + } + response = await self.publicSwapGetPublicInstrument(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "instrument_id": "BTCUSDTPERP", + # "type": "REAL", + # "contract_type": "PERPETUAL", + # "base_currency": "BTC", + # "quote_currency": "USDT", + # "clear_currency": "USDT", + # "contract_value": "0.001", + # "contract_value_currency": "BTC", + # "is_inverse": False, + # "is_trading": True, + # "status": "ONLINE", + # "price_precision": 1, + # "tick_size": "0.1", + # "min_order_amount": 1, + # "open_max_limits": [ + # { + # "leverage": "50", + # "max_limit": "1000000" + # } + # ] + # } + # } + # + data = self.safe_value(response, 'data', {}) + return self.parse_market_leverage_tiers(data, market) + + def parse_market_leverage_tiers(self, info, market: Market = None) -> List[LeverageTier]: + # + # { + # "instrument_id": "BTCUSDTPERP", + # "type": "REAL", + # "contract_type": "PERPETUAL", + # "base_currency": "BTC", + # "quote_currency": "USDT", + # "clear_currency": "USDT", + # "contract_value": "0.001", + # "contract_value_currency": "BTC", + # "is_inverse": False, + # "is_trading": True, + # "status": "ONLINE", + # "price_precision": 1, + # "tick_size": "0.1", + # "min_order_amount": 1, + # "open_max_limits": [ + # { + # "leverage": "50", + # "max_limit": "1000000" + # } + # ] + # } + # + tiers = [] + brackets = self.safe_value(info, 'open_max_limits', {}) + for i in range(0, len(brackets)): + tier = brackets[i] + marketId = self.safe_string(info, 'instrument_id') + market = self.safe_market(marketId, market) + tiers.append({ + 'tier': self.sum(i, 1), + 'symbol': self.safe_symbol(marketId, market, None, 'swap'), + 'currency': market['settle'], + 'minNotional': None, + 'maxNotional': self.safe_number(tier, 'max_limit'), + 'maintenanceMarginRate': None, + 'maxLeverage': self.safe_number(tier, 'leverage'), + 'info': tier, + }) + return tiers + + def handle_margin_mode_and_params(self, methodName, params={}, defaultValue=None): + """ + @ignore + marginMode specified by params["marginMode"], self.options["marginMode"], self.options["defaultMarginMode"], params["margin"] = True or self.options["defaultType"] = 'margin' + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Array: the marginMode in lowercase + """ + defaultType = self.safe_string(self.options, 'defaultType') + isMargin = self.safe_bool(params, 'margin', False) + marginMode = None + marginMode, params = super(digifinex, self).handle_margin_mode_and_params(methodName, params, defaultValue) + if marginMode is not None: + if marginMode != 'cross': + raise NotSupported(self.id + ' only cross margin is supported') + else: + if (defaultType == 'margin') or (isMargin is True): + marginMode = 'cross' + return [marginMode, params] + + async def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}): + """ + fetch deposit and withdraw fees + + https://docs.digifinex.com/en-ww/spot/v3/rest.html#get-currency-deposit-and-withdrawal-information + + :param str[]|None codes: not used by fetchDepositWithdrawFees() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `fee structures ` + """ + await self.load_markets() + response = await self.publicSpotGetCurrencies(params) + # + # { + # "data": [ + # { + # "deposit_status": 0, + # "min_withdraw_fee": 5, + # "withdraw_fee_currency": "USDT", + # "chain": "OMNI", + # "withdraw_fee_rate": 0, + # "min_withdraw_amount": 10, + # "currency": "USDT", + # "withdraw_status": 0, + # "min_deposit_amount": 10 + # }, + # { + # "deposit_status": 1, + # "min_withdraw_fee": 5, + # "withdraw_fee_currency": "USDT", + # "chain": "ERC20", + # "withdraw_fee_rate": 0, + # "min_withdraw_amount": 10, + # "currency": "USDT", + # "withdraw_status": 1, + # "min_deposit_amount": 10 + # }, + # ], + # "code": 200, + # } + # + data = self.safe_list(response, 'data') + return self.parse_deposit_withdraw_fees(data, codes) + + def parse_deposit_withdraw_fees(self, response, codes=None, currencyIdKey=None): + # + # [ + # { + # "deposit_status": 0, + # "min_withdraw_fee": 5, + # "withdraw_fee_currency": "USDT", + # "chain": "OMNI", + # "withdraw_fee_rate": 0, + # "min_withdraw_amount": 10, + # "currency": "USDT", + # "withdraw_status": 0, + # "min_deposit_amount": 10 + # }, + # { + # "deposit_status": 1, + # "min_withdraw_fee": 5, + # "withdraw_fee_currency": "USDT", + # "chain": "ERC20", + # "withdraw_fee_rate": 0, + # "min_withdraw_amount": 10, + # "currency": "USDT", + # "withdraw_status": 1, + # "min_deposit_amount": 10 + # }, + # ] + # + depositWithdrawFees: dict = {} + codes = self.market_codes(codes) + for i in range(0, len(response)): + entry = response[i] + currencyId = self.safe_string(entry, 'currency') + code = self.safe_currency_code(currencyId) + if (codes is None) or (self.in_array(code, codes)): + depositWithdrawFee = self.safe_value(depositWithdrawFees, code) + if depositWithdrawFee is None: + depositWithdrawFees[code] = self.deposit_withdraw_fee({}) + depositWithdrawFees[code]['info'] = [] + depositWithdrawInfo = depositWithdrawFees[code]['info'] + depositWithdrawInfo.append(entry) + networkId = self.safe_string(entry, 'chain') + withdrawFee = self.safe_value(entry, 'min_withdraw_fee') + withdrawResult: dict = { + 'fee': withdrawFee, + 'percentage': False if (withdrawFee is not None) else None, + } + depositResult: dict = { + 'fee': None, + 'percentage': None, + } + if networkId is not None: + networkCode = self.network_id_to_code(networkId) + depositWithdrawFees[code]['networks'][networkCode] = { + 'withdraw': withdrawResult, + 'deposit': depositResult, + } + else: + depositWithdrawFees[code]['withdraw'] = withdrawResult + depositWithdrawFees[code]['deposit'] = depositResult + depositWithdrawCodes = list(depositWithdrawFees.keys()) + for i in range(0, len(depositWithdrawCodes)): + code = depositWithdrawCodes[i] + currency = self.currency(code) + depositWithdrawFees[code] = self.assign_default_deposit_withdraw_fees(depositWithdrawFees[code], currency) + return depositWithdrawFees + + async def add_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + add margin to a position + + https://docs.digifinex.com/en-ww/swap/v2/rest.html#positionmargin + + :param str symbol: unified market symbol + :param float amount: amount of margin to add + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str params['side']: the position side: 'long' or 'short' + :returns dict: a `margin structure ` + """ + side = self.safe_string(params, 'side') + self.check_required_argument('addMargin', side, 'side', ['long', 'short']) + return await self.modify_margin_helper(symbol, amount, 1, params) + + async def reduce_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + remove margin from a position + + https://docs.digifinex.com/en-ww/swap/v2/rest.html#positionmargin + + :param str symbol: unified market symbol + :param float amount: the amount of margin to remove + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str params['side']: the position side: 'long' or 'short' + :returns dict: a `margin structure ` + """ + side = self.safe_string(params, 'side') + self.check_required_argument('reduceMargin', side, 'side', ['long', 'short']) + return await self.modify_margin_helper(symbol, amount, 2, params) + + async def modify_margin_helper(self, symbol: str, amount, type, params={}) -> MarginModification: + await self.load_markets() + side = self.safe_string(params, 'side') + market = self.market(symbol) + request: dict = { + 'instrument_id': market['id'], + 'amount': self.number_to_string(amount), + 'type': type, + 'side': side, + } + response = await self.privateSwapPostAccountPositionMargin(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "instrument_id": "BTCUSDTPERP", + # "side": "long", + # "type": 1, + # "amount": "3.6834" + # } + # } + # + code = self.safe_integer(response, 'code') + status = 'ok' if (code == 0) else 'failed' + data = self.safe_value(response, 'data', {}) + return self.extend(self.parse_margin_modification(data, market), { + 'status': status, + }) + + def parse_margin_modification(self, data: dict, market: Market = None) -> MarginModification: + # + # { + # "instrument_id": "BTCUSDTPERP", + # "side": "long", + # "type": 1, + # "amount": "3.6834" + # } + # + marketId = self.safe_string(data, 'instrument_id') + rawType = self.safe_integer(data, 'type') + return { + 'info': data, + 'symbol': self.safe_symbol(marketId, market, None, 'swap'), + 'type': 'add' if (rawType == 1) else 'reduce', + 'marginMode': 'isolated', + 'amount': self.safe_number(data, 'amount'), + 'total': None, + 'code': market['settle'], + 'status': None, + 'timestamp': None, + 'datetime': None, + } + + async def fetch_funding_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch the history of funding payments paid and received on self account + + https://docs.digifinex.com/en-ww/swap/v2/rest.html#funding-fee + + :param str [symbol]: unified market symbol + :param int [since]: the earliest time in ms to fetch funding history for + :param int [limit]: the maximum number of funding history structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest funding payment + :returns dict: a `funding history structure ` + """ + await self.load_markets() + request: dict = {} + request, params = self.handle_until_option('end_timestamp', request, params) + market = None + if symbol is not None: + market = self.market(symbol) + request['instrument_id'] = market['id'] + if limit is not None: + request['limit'] = limit + if since is not None: + request['start_timestamp'] = since + response = await self.privateSwapGetAccountFundingFee(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "instrument_id": "BTCUSDTPERP", + # "currency": "USDT", + # "amount": "-0.000342814", + # "timestamp": 1698768009440 + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_incomes(data, market, since, limit) + + def parse_income(self, income, market: Market = None): + # + # { + # "instrument_id": "BTCUSDTPERP", + # "currency": "USDT", + # "amount": "-0.000342814", + # "timestamp": 1698768009440 + # } + # + marketId = self.safe_string(income, 'instrument_id') + currencyId = self.safe_string(income, 'currency') + timestamp = self.safe_integer(income, 'timestamp') + return { + 'info': income, + 'symbol': self.safe_symbol(marketId, market, None, 'swap'), + 'code': self.safe_currency_code(currencyId), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': None, + 'amount': self.safe_number(income, 'amount'), + } + + async def set_margin_mode(self, marginMode: str, symbol: Str = None, params={}): + """ + set margin mode to 'cross' or 'isolated' + + https://docs.digifinex.com/en-ww/swap/v2/rest.html#positionmode + + :param str marginMode: 'cross' or 'isolated' + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setMarginMode() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + marginMode = marginMode.lower() + if marginMode == 'cross': + marginMode = 'crossed' + request: dict = { + 'instrument_id': market['id'], + 'margin_mode': marginMode, + } + return await self.privateSwapPostAccountPositionMode(self.extend(request, params)) + + def sign(self, path, api=[], method='GET', params={}, headers=None, body=None): + signed = api[0] == 'private' + endpoint = api[1] + pathPart = '/v3' if (endpoint == 'spot') else '/swap/v2' + request = '/' + self.implode_params(path, params) + payload = pathPart + request + url = self.urls['api']['rest'] + payload + query = self.omit(params, self.extract_params(path)) + urlencoded = None + if signed and (pathPart == '/swap/v2') and (method == 'POST'): + urlencoded = json.dumps(params) + else: + urlencoded = self.urlencode(self.keysort(query)) + if signed: + auth = None + nonce = None + if pathPart == '/swap/v2': + nonce = str(self.milliseconds()) + auth = nonce + method + payload + if method == 'GET': + if urlencoded: + auth += '?' + urlencoded + elif method == 'POST': + auth += urlencoded + else: + nonce = str(self.nonce()) + auth = urlencoded + signature = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256) + if method == 'GET': + if urlencoded: + url += '?' + urlencoded + elif method == 'POST': + headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + } + if urlencoded: + body = urlencoded + headers = { + 'ACCESS-KEY': self.apiKey, + 'ACCESS-SIGN': signature, + 'ACCESS-TIMESTAMP': nonce, + } + else: + if urlencoded: + url += '?' + urlencoded + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, statusCode: int, statusText: str, url: str, method: str, responseHeaders: dict, responseBody, response, requestHeaders, requestBody): + if not response: + return None # fall back to default error handler + code = self.safe_string(response, 'code') + if (code == '0') or (code == '200'): + return None # no error + feedback = self.id + ' ' + responseBody + if code is None: + raise BadResponse(feedback) + unknownError = [ExchangeError, feedback] + ExceptionClass, message = self.safe_value(self.exceptions['exact'], code, unknownError) + raise ExceptionClass(message) diff --git a/ccxt/async_support/exmo.py b/ccxt/async_support/exmo.py new file mode 100644 index 0000000..b17d114 --- /dev/null +++ b/ccxt/async_support/exmo.py @@ -0,0 +1,2675 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.exmo import ImplicitAPI +import asyncio +import hashlib +from ccxt.base.types import Any, Balances, Currencies, Currency, DepositAddress, Int, MarginModification, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, OrderBooks, Trade, TradingFees, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import OnMaintenance +from ccxt.base.errors import InvalidNonce +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class exmo(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(exmo, self).describe(), { + 'id': 'exmo', + 'name': 'EXMO', + 'countries': ['LT'], # Lithuania + 'rateLimit': 100, # 10 requests per 1 second + 'version': 'v1.1', + 'has': { + 'CORS': None, + 'spot': True, + 'margin': True, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': True, + 'cancelOrder': True, + 'cancelOrders': False, + 'createDepositAddress': False, + 'createMarketBuyOrder': True, + 'createMarketBuyOrderWithCost': True, + 'createMarketOrderWithCost': True, + 'createOrder': True, + 'createStopLimitOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'editOrder': True, # margin only + 'fetchAccounts': False, + 'fetchBalance': True, + 'fetchCanceledOrders': True, + 'fetchCurrencies': True, + 'fetchDeposit': True, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchDepositsWithdrawals': True, + 'fetchDepositWithdrawFee': 'emulated', + 'fetchDepositWithdrawFees': True, + 'fetchFundingHistory': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchIndexOHLCV': False, + 'fetchMarginMode': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterestHistory': False, + 'fetchOpenOrders': True, + 'fetchOrder': 'emulated', + 'fetchOrderBook': True, + 'fetchOrderBooks': True, + 'fetchOrderTrades': True, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': True, + 'fetchTransactionFees': True, + 'fetchTransactions': 'emulated', + 'fetchTransfer': False, + 'fetchTransfers': False, + 'fetchWithdrawal': True, + 'fetchWithdrawals': True, + 'reduceMargin': True, + 'setMargin': False, + 'transfer': False, + 'withdraw': True, + }, + 'timeframes': { + '1m': '1', + '5m': '5', + '15m': '15', + '30m': '30', + '45m': '45', + '1h': '60', + '2h': '120', + '3h': '180', + '4h': '240', + '1d': 'D', + '1w': 'W', + '1M': 'M', + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/27766491-1b0ea956-5eda-11e7-9225-40d67b481b8d.jpg', + 'api': { + 'public': 'https://api.exmo.com', + 'private': 'https://api.exmo.com', + 'web': 'https://exmo.me', + }, + 'www': 'https://exmo.me', + 'referral': 'https://exmo.me/?ref=131685', + 'doc': [ + 'https://exmo.me/en/api_doc?ref=131685', + ], + 'fees': 'https://exmo.com/en/docs/fees', + }, + 'api': { + 'web': { + 'get': [ + 'ctrl/feesAndLimits', + 'en/docs/fees', + ], + }, + 'public': { + 'get': [ + 'currency', + 'currency/list/extended', + 'order_book', + 'pair_settings', + 'ticker', + 'trades', + 'candles_history', + 'required_amount', + 'payments/providers/crypto/list', + ], + }, + 'private': { + 'post': [ + 'user_info', + 'order_create', + 'order_cancel', + 'stop_market_order_create', + 'stop_market_order_cancel', + 'user_open_orders', + 'user_trades', + 'user_cancelled_orders', + 'order_trades', + 'deposit_address', + 'withdraw_crypt', + 'withdraw_get_txid', + 'excode_create', + 'excode_load', + 'code_check', + 'wallet_history', + 'wallet_operations', + 'margin/user/order/create', + 'margin/user/order/update', + 'margin/user/order/cancel', + 'margin/user/position/close', + 'margin/user/position/margin_add', + 'margin/user/position/margin_remove', + 'margin/currency/list', + 'margin/pair/list', + 'margin/settings', + 'margin/funding/list', + 'margin/user/info', + 'margin/user/order/list', + 'margin/user/order/history', + 'margin/user/order/trades', + 'margin/user/order/max_quantity', + 'margin/user/position/list', + 'margin/user/position/margin_remove_info', + 'margin/user/position/margin_add_info', + 'margin/user/wallet/list', + 'margin/user/wallet/history', + 'margin/user/trade/list', + 'margin/trades', + 'margin/liquidation/feed', + ], + }, + }, + 'fees': { + 'trading': { + 'feeSide': 'get', + 'tierBased': True, + 'percentage': True, + 'maker': self.parse_number('0.004'), + 'taker': self.parse_number('0.004'), + }, + 'transaction': { + 'tierBased': False, + 'percentage': False, # fixed transaction fees for crypto, see fetchDepositWithdrawFees below + }, + }, + 'options': { + 'networks': { + 'ETH': 'ERC20', + 'TRX': 'TRC20', + }, + 'fetchTradingFees': { + 'method': 'fetchPrivateTradingFees', # or 'fetchPublicTradingFees' + }, + 'margin': { + 'fillResponseFromRequest': True, + }, + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': True, # todo revise + 'triggerPrice': True, # todo: endpoint lacks other features + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': True, + }, + 'hedged': False, + 'selfTradePrevention': False, + 'trailing': False, + 'leverage': True, + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': True, + 'limit': 100, + 'daysBack': None, + 'untilDays': None, + 'symbolRequired': True, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': None, + 'fetchOHLCV': { + 'limit': 1000, # todo, not in request + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'commonCurrencies': { + 'GMT': 'GMT Token', + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'exact': { + '140333': InvalidOrder, # {"error":{"code":140333,"msg":"The number of characters after the point in the price exceeds the maximum number '8\u003e6'"}} + '140434': BadRequest, + '40005': AuthenticationError, # Authorization error, incorrect signature + '40009': InvalidNonce, # + '40015': ExchangeError, # API function do not exist + '40016': OnMaintenance, # {"result":false,"error":"Error 40016: Maintenance work in progress"} + '40017': AuthenticationError, # Wrong API Key + '40032': PermissionDenied, # {"result":false,"error":"Error 40032: Access is denied for self API key"} + '40033': PermissionDenied, # {"result":false,"error":"Error 40033: Access is denied, self resources are temporarily blocked to user"} + '40034': RateLimitExceeded, # {"result":false,"error":"Error 40034: Access is denied, rate limit is exceeded"} + '50052': InsufficientFunds, + '50054': InsufficientFunds, + '50304': OrderNotFound, # "Order was not found '123456789'"(fetching order trades for an order that does not have trades yet) + '50173': OrderNotFound, # "Order with id X was not found."(cancelling non-existent, closed and cancelled order) + '50277': InvalidOrder, + '50319': InvalidOrder, # Price by order is less than permissible minimum for self pair + '50321': InvalidOrder, # Price by order is more than permissible maximum for self pair + '50381': InvalidOrder, # {"result":false,"error":"Error 50381: More than 2 decimal places are not permitted for pair BTC_USD"} + }, + 'broad': { + 'range period is too long': BadRequest, + 'invalid syntax': BadRequest, + 'API rate limit exceeded': RateLimitExceeded, # {"result":false,"error":"API rate limit exceeded for x.x.x.x. Retry after 60 sec.","history":[],"begin":1579392000,"end":1579478400} + }, + }, + }) + + async def modify_margin_helper(self, symbol: str, amount, type, params={}): + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'position_id': market['id'], + 'quantity': amount, + } + response = None + if type == 'add': + response = await self.privatePostMarginUserPositionMarginAdd(self.extend(request, params)) + elif type == 'reduce': + response = await self.privatePostMarginUserPositionMarginRemove(self.extend(request, params)) + # + # {} + # + margin = self.parse_margin_modification(response, market) + options = self.safe_value(self.options, 'margin', {}) + fillResponseFromRequest = self.safe_bool(options, 'fillResponseFromRequest', True) + if fillResponseFromRequest: + margin['type'] = type + margin['amount'] = amount + return margin + + def parse_margin_modification(self, data: dict, market: Market = None) -> MarginModification: + # + # {} + # + return { + 'info': data, + 'symbol': self.safe_symbol(None, market), + 'type': None, + 'marginMode': 'isolated', + 'amount': None, + 'total': None, + 'code': self.safe_value(market, 'quote'), + 'status': 'ok', + 'timestamp': None, + 'datetime': None, + } + + async def reduce_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + remove margin from a position + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#eebf9f25-0289-4946-9482-89872c738449 + + :param str symbol: unified market symbol + :param float amount: the amount of margin to remove + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin structure ` + """ + return await self.modify_margin_helper(symbol, amount, 'reduce', params) + + async def add_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + add margin + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#143ef808-79ca-4e49-9e79-a60ea4d8c0e3 + + :param str symbol: unified market symbol + :param float amount: amount of margin to add + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin structure ` + """ + return await self.modify_margin_helper(symbol, amount, 'add', params) + + async def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#90927062-256c-4b03-900f-2b99131f9a54 + https://documenter.getpostman.com/view/10287440/SzYXWKPi#7de7e75c-5833-45a8-b937-c2276d235aaa + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + options = self.safe_value(self.options, 'fetchTradingFees', {}) + defaultMethod = self.safe_string(options, 'method', 'fetchPrivateTradingFees') + method = self.safe_string(params, 'method', defaultMethod) + params = self.omit(params, 'method') + if method == 'fetchPrivateTradingFees': + return await self.fetch_private_trading_fees(params) + else: + return await self.fetch_public_trading_fees(params) + + async def fetch_private_trading_fees(self, params={}): + await self.load_markets() + response = await self.privatePostMarginPairList(params) + # + # { + # "pairs": [{ + # "name": "EXM_USD", + # "buy_price": "0.02728391", + # "sell_price": "0.0276", + # "last_trade_price": "0.0276", + # "ticker_updated": "1646956050056696046", + # "is_fair_price": True, + # "max_price_precision": "8", + # "min_order_quantity": "1", + # "max_order_quantity": "50000", + # "min_order_price": "0.00000001", + # "max_order_price": "1000", + # "max_position_quantity": "50000", + # "trade_taker_fee": "0.05", + # "trade_maker_fee": "0", + # "liquidation_fee": "0.5", + # "max_leverage": "3", + # "default_leverage": "3", + # "liquidation_level": "5", + # "margin_call_level": "7.5", + # "position": "1", + # "updated": "1638976144797807397" + # } + # ... + # ] + # } + # + pairs = self.safe_value(response, 'pairs', []) + result: dict = {} + for i in range(0, len(pairs)): + pair = pairs[i] + marketId = self.safe_string(pair, 'name') + symbol = self.safe_symbol(marketId, None, '_') + makerString = self.safe_string(pair, 'trade_maker_fee') + takerString = self.safe_string(pair, 'trade_taker_fee') + maker = self.parse_number(Precise.string_div(makerString, '100')) + taker = self.parse_number(Precise.string_div(takerString, '100')) + result[symbol] = { + 'info': pair, + 'symbol': symbol, + 'maker': maker, + 'taker': taker, + 'percentage': True, + 'tierBased': True, + } + return result + + async def fetch_public_trading_fees(self, params={}): + await self.load_markets() + response = await self.publicGetPairSettings(params) + # + # { + # "BTC_USD": { + # "min_quantity": "0.00002", + # "max_quantity": "1000", + # "min_price": "1", + # "max_price": "150000", + # "max_amount": "500000", + # "min_amount": "1", + # "price_precision": "2", + # "commission_taker_percent": "0.3", + # "commission_maker_percent": "0.3" + # }, + # } + # + result: dict = {} + for i in range(0, len(self.symbols)): + symbol = self.symbols[i] + market = self.market(symbol) + fee = self.safe_value(response, market['id'], {}) + makerString = self.safe_string(fee, 'commission_maker_percent') + takerString = self.safe_string(fee, 'commission_taker_percent') + maker = self.parse_number(Precise.string_div(makerString, '100')) + taker = self.parse_number(Precise.string_div(takerString, '100')) + result[symbol] = { + 'info': fee, + 'symbol': symbol, + 'maker': maker, + 'taker': taker, + 'percentage': True, + 'tierBased': True, + } + return result + + def parse_fixed_float_value(self, input): + if (input is None) or (input == '-'): + return None + if input == '': + return 0 + isPercentage = (input.find('%') >= 0) + parts = input.split(' ') + value = parts[0].replace('%', '') + result = float(value) + if (result > 0) and isPercentage: + raise ExchangeError(self.id + ' parseFixedFloatValue() detected an unsupported non-zero percentage-based fee ' + input) + return result + + async def fetch_transaction_fees(self, codes: Strings = None, params={}): + """ + @deprecated + please use fetchDepositWithdrawFees instead + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#4190035d-24b1-453d-833b-37e0a52f88e2 + + :param str[]|None codes: list of unified currency codes + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `transaction fees structures ` + """ + await self.load_markets() + cryptoList = await self.publicGetPaymentsProvidersCryptoList(params) + # + # { + # "BTC":[ + # {"type":"deposit", "name":"BTC", "currency_name":"BTC", "min":"0.001", "max":"0", "enabled":true,"comment":"Minimum deposit amount is 0.001 BTC. We do not support BSC and BEP20 network, please consider self when sending funds", "commission_desc":"0%", "currency_confirmations":1}, + # {"type":"withdraw", "name":"BTC", "currency_name":"BTC", "min":"0.001", "max":"350", "enabled":true,"comment":"Do not withdraw directly to the Crowdfunding or ICO address account will not be credited with tokens from such sales.", "commission_desc":"0.0005 BTC", "currency_confirmations":6} + # ], + # "ETH":[ + # {"type":"withdraw", "name":"ETH", "currency_name":"ETH", "min":"0.01", "max":"500", "enabled":true,"comment":"Do not withdraw directly to the Crowdfunding or ICO address account will not be credited with tokens from such sales.", "commission_desc":"0.004 ETH", "currency_confirmations":4}, + # {"type":"deposit", "name":"ETH", "currency_name":"ETH", "min":"0.01", "max":"0", "enabled":true,"comment":"Minimum deposit amount is 0.01 ETH. We do not support BSC and BEP20 network, please consider self when sending funds", "commission_desc":"0%", "currency_confirmations":1} + # ], + # "USDT":[ + # {"type":"deposit", "name":"USDT(OMNI)", "currency_name":"USDT", "min":"10", "max":"0", "enabled":false,"comment":"Minimum deposit amount is 10 USDT", "commission_desc":"0%", "currency_confirmations":2}, + # {"type":"withdraw", "name":"USDT(OMNI)", "currency_name":"USDT", "min":"10", "max":"100000", "enabled":false,"comment":"Do not withdraw directly to the Crowdfunding or ICO address account will not be credited with tokens from such sales.", "commission_desc":"5 USDT", "currency_confirmations":6}, + # {"type":"deposit", "name":"USDT(ERC20)", "currency_name":"USDT", "min":"10", "max":"0", "enabled":true,"comment":"Minimum deposit amount is 10 USDT", "commission_desc":"0%", "currency_confirmations":2}, + # { + # "type":"withdraw", + # "name":"USDT(ERC20)", + # "currency_name":"USDT", + # "min":"55", + # "max":"200000", + # "enabled":true, + # "comment":"Caution! Do not withdraw directly to a crowdfund or ICO address, account will not be credited with tokens from such sales. Recommendation: Due to the high load of ERC20 network, using TRC20 address for withdrawal is recommended.", + # "commission_desc":"10 USDT", + # "currency_confirmations":6 + # }, + # {"type":"deposit", "name":"USDT(TRC20)", "currency_name":"USDT", "min":"10", "max":"100000", "enabled":true,"comment":"Minimum deposit amount is 10 USDT. Only TRON main network supported", "commission_desc":"0%", "currency_confirmations":2}, + # {"type":"withdraw", "name":"USDT(TRC20)", "currency_name":"USDT", "min":"10", "max":"150000", "enabled":true,"comment":"Caution! Do not withdraw directly to a crowdfund or ICO address, account will not be credited with tokens from such sales. Only TRON main network supported.", "commission_desc":"1 USDT", "currency_confirmations":6} + # ], + # "XLM":[ + # {"type":"deposit", "name":"XLM", "currency_name":"XLM", "min":"1", "max":"1000000", "enabled":true,"comment":"Attention! A deposit without memo(invoice) will not be credited. Minimum deposit amount is 1 XLM. We do not support BSC and BEP20 network, please consider self when sending funds", "commission_desc":"0%", "currency_confirmations":1}, + # {"type":"withdraw", "name":"XLM", "currency_name":"XLM", "min":"21", "max":"1000000", "enabled":true,"comment":"Caution! Do not withdraw directly to a crowdfund or ICO address, account will not be credited with tokens from such sales.", "commission_desc":"0.01 XLM", "currency_confirmations":1} + # ], + # } + # + result: dict = {} + cryptoListKeys = list(cryptoList.keys()) + for i in range(0, len(cryptoListKeys)): + code = cryptoListKeys[i] + if codes is not None and not self.in_array(code, codes): + continue + result[code] = { + 'deposit': None, + 'withdraw': None, + } + currency = self.currency(code) + currencyId = self.safe_string(currency, 'id') + providers = self.safe_value(cryptoList, currencyId, []) + for j in range(0, len(providers)): + provider = providers[j] + typeInner = self.safe_string(provider, 'type') + commissionDesc = self.safe_string(provider, 'commission_desc') + fee = self.parse_fixed_float_value(commissionDesc) + result[code][typeInner] = fee + result[code]['info'] = providers + # cache them for later use + self.options['transactionFees'] = result + return result + + async def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}): + """ + fetch deposit and withdraw fees + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#4190035d-24b1-453d-833b-37e0a52f88e2 + + :param str[]|None codes: list of unified currency codes + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `transaction fees structures ` + """ + await self.load_markets() + response = await self.publicGetPaymentsProvidersCryptoList(params) + # + # { + # "USDT": [ + # { + # "type": "deposit", # or "withdraw" + # "name": "USDT(ERC20)", + # "currency_name": "USDT", + # "min": "10", + # "max": "0", + # "enabled": True, + # "comment": "Minimum deposit amount is 10 USDT", + # "commission_desc": "0%", + # "currency_confirmations": 2 + # }, + # ... + # ], + # ... + # } + # + result = self.parse_deposit_withdraw_fees(response, codes) + # cache them for later use + self.options['transactionFees'] = result + return result + + def parse_deposit_withdraw_fee(self, fee, currency: Currency = None): + # + # [ + # { + # "type": "deposit", # or "withdraw" + # "name": "BTC", + # "currency_name": "BTC", + # "min": "0.001", + # "max": "0", + # "enabled": True, + # "comment": "Minimum deposit amount is 0.001 BTC. We do not support BSC and BEP20 network, please consider self when sending funds", + # "commission_desc": "0%", + # "currency_confirmations": 1 + # }, + # ... + # ] + # + result = self.deposit_withdraw_fee(fee) + for i in range(0, len(fee)): + provider = fee[i] + type = self.safe_string(provider, 'type') + networkId = self.safe_string(provider, 'name') + networkCode = self.network_id_to_code(networkId, self.safe_string(currency, 'code')) + commissionDesc = self.safe_string(provider, 'commission_desc') + splitCommissionDesc = [] + percentage = None + if commissionDesc is not None: + splitCommissionDesc = commissionDesc.split('%') + splitCommissionDescLength = len(splitCommissionDesc) + percentage = splitCommissionDescLength >= 2 + network = self.safe_value(result['networks'], networkCode) + if network is None: + result['networks'][networkCode] = { + 'withdraw': { + 'fee': None, + 'percentage': None, + }, + 'deposit': { + 'fee': None, + 'percentage': None, + }, + } + result['networks'][networkCode][type] = { + 'fee': self.parse_fixed_float_value(self.safe_string(splitCommissionDesc, 0)), + 'percentage': percentage, + } + return self.assign_default_deposit_withdraw_fees(result) + + async def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#7cdf0ca8-9ff6-4cf3-aa33-bcec83155c49 + https://documenter.getpostman.com/view/10287440/SzYXWKPi#4190035d-24b1-453d-833b-37e0a52f88e2 + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + promises = [] + # + promises.append(self.publicGetCurrencyListExtended(params)) + # + # [ + # {"name":"VLX","description":"Velas"}, + # {"name":"RUB","description":"Russian Ruble"}, + # {"name":"BTC","description":"Bitcoin"}, + # {"name":"USD","description":"US Dollar"} + # ] + # + promises.append(self.publicGetPaymentsProvidersCryptoList(params)) + # + # { + # "BTC":[ + # {"type":"deposit", "name":"BTC", "currency_name":"BTC", "min":"0.001", "max":"0", "enabled":true,"comment":"Minimum deposit amount is 0.001 BTC. We do not support BSC and BEP20 network, please consider self when sending funds", "commission_desc":"0%", "currency_confirmations":1}, + # {"type":"withdraw", "name":"BTC", "currency_name":"BTC", "min":"0.001", "max":"350", "enabled":true,"comment":"Do not withdraw directly to the Crowdfunding or ICO address account will not be credited with tokens from such sales.", "commission_desc":"0.0005 BTC", "currency_confirmations":6} + # ], + # "ETH":[ + # {"type":"withdraw", "name":"ETH", "currency_name":"ETH", "min":"0.01", "max":"500", "enabled":true,"comment":"Do not withdraw directly to the Crowdfunding or ICO address account will not be credited with tokens from such sales.", "commission_desc":"0.004 ETH", "currency_confirmations":4}, + # {"type":"deposit", "name":"ETH", "currency_name":"ETH", "min":"0.01", "max":"0", "enabled":true,"comment":"Minimum deposit amount is 0.01 ETH. We do not support BSC and BEP20 network, please consider self when sending funds", "commission_desc":"0%", "currency_confirmations":1} + # ], + # "USDT":[ + # {"type":"deposit", "name":"USDT(OMNI)", "currency_name":"USDT", "min":"10", "max":"0", "enabled":false,"comment":"Minimum deposit amount is 10 USDT", "commission_desc":"0%", "currency_confirmations":2}, + # {"type":"withdraw", "name":"USDT(OMNI)", "currency_name":"USDT", "min":"10", "max":"100000", "enabled":false,"comment":"Do not withdraw directly to the Crowdfunding or ICO address account will not be credited with tokens from such sales.", "commission_desc":"5 USDT", "currency_confirmations":6}, + # {"type":"deposit", "name":"USDT(ERC20)", "currency_name":"USDT", "min":"10", "max":"0", "enabled":true,"comment":"Minimum deposit amount is 10 USDT", "commission_desc":"0%", "currency_confirmations":2}, + # {"type":"withdraw", "name":"USDT(ERC20)", "currency_name":"USDT", "min":"55", "max":"200000", "enabled":true, "comment":"Caution! Do not withdraw directly to a crowdfund or ICO address, account will not be credited with tokens from such sales. Recommendation: Due to the high load of ERC20 network, using TRC20 address for withdrawal is recommended.", "commission_desc":"10 USDT", "currency_confirmations":6}, + # {"type":"deposit", "name":"USDT(TRC20)", "currency_name":"USDT", "min":"10", "max":"100000", "enabled":true,"comment":"Minimum deposit amount is 10 USDT. Only TRON main network supported", "commission_desc":"0%", "currency_confirmations":2}, + # {"type":"withdraw", "name":"USDT(TRC20)", "currency_name":"USDT", "min":"10", "max":"150000", "enabled":true,"comment":"Caution! Do not withdraw directly to a crowdfund or ICO address, account will not be credited with tokens from such sales. Only TRON main network supported.", "commission_desc":"1 USDT", "currency_confirmations":6} + # ], + # "XLM":[ + # {"type":"deposit", "name":"XLM", "currency_name":"XLM", "min":"1", "max":"1000000", "enabled":true,"comment":"Attention! A deposit without memo(invoice) will not be credited. Minimum deposit amount is 1 XLM. We do not support BSC and BEP20 network, please consider self when sending funds", "commission_desc":"0%", "currency_confirmations":1}, + # {"type":"withdraw", "name":"XLM", "currency_name":"XLM", "min":"21", "max":"1000000", "enabled":true,"comment":"Caution! Do not withdraw directly to a crowdfund or ICO address, account will not be credited with tokens from such sales.", "commission_desc":"0.01 XLM", "currency_confirmations":1} + # ], + # } + # + responses = await asyncio.gather(*promises) + currencyList = responses[0] + cryptoList = responses[1] + result: dict = {} + for i in range(0, len(currencyList)): + currency = currencyList[i] + currencyId = self.safe_string(currency, 'name') + code = self.safe_currency_code(currencyId) + type = 'crypto' + networks = {} + providers = self.safe_list(cryptoList, currencyId) + if providers is None: + type = 'fiat' + else: + for j in range(0, len(providers)): + provider = providers[j] + name = self.safe_string(provider, 'name') + # get network-id by removing extra things + networkId = name.replace(currencyId + ' ', '') + networkId = networkId.replace('(', '') + replaceChar = ')' # transpiler trick + networkId = networkId.replace(replaceChar, '') + networkCode = self.network_id_to_code(networkId) + if not (networkCode in networks): + networks[networkCode] = { + 'id': networkId, + 'network': networkCode, + 'active': None, + 'deposit': None, + 'withdraw': None, + 'fee': None, + 'limits': { + 'withdraw': { + 'min': None, + 'max': None, + }, + 'deposit': { + 'min': None, + 'max': None, + }, + }, + 'info': [], # set, because of multiple network sub-entries + } + typeInner = self.safe_string(provider, 'type') + minValue = self.safe_string(provider, 'min') + maxValue = self.safe_string(provider, 'max') + activeProvider = self.safe_bool(provider, 'enabled') + networkEntry = networks[networkCode] + if typeInner == 'deposit': + networkEntry['deposit'] = activeProvider + networkEntry['limits']['deposit']['min'] = minValue + networkEntry['limits']['deposit']['max'] = maxValue + elif typeInner == 'withdraw': + networkEntry['withdraw'] = activeProvider + networkEntry['limits']['withdraw']['min'] = minValue + networkEntry['limits']['withdraw']['max'] = maxValue + info = self.safe_list(networkEntry, 'info') + info.append(provider) + networkEntry['info'] = info + networks[networkCode] = networkEntry + result[code] = self.safe_currency_structure({ + 'id': currencyId, + 'code': code, + 'name': self.safe_string(currency, 'description'), + 'type': type, + 'active': None, + 'deposit': None, + 'withdraw': None, + 'fee': None, + 'precision': self.parse_number('1e-8'), + 'limits': { + 'withdraw': { + 'min': None, + 'max': None, + }, + 'deposit': { + 'min': None, + 'max': None, + }, + }, + 'info': { + 'currency': currency, + 'providers': providers, + }, + 'networks': networks, + }) + return result + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for exmo + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#7de7e75c-5833-45a8-b937-c2276d235aaa + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + promises = [] + promises.append(self.publicGetPairSettings(params)) + # + # { + # "BTC_USD":{ + # "min_quantity":"0.0001", + # "max_quantity":"1000", + # "min_price":"1", + # "max_price":"30000", + # "max_amount":"500000", + # "min_amount":"1", + # "price_precision":8, + # "commission_taker_percent":"0.4", + # "commission_maker_percent":"0.4" + # }, + # } + # + marginPairsDict: dict = {} + fetchMargin = self.check_required_credentials(False) + if fetchMargin: + promises.append(self.privatePostMarginPairList(params)) + # + # { + # "pairs": [ + # { + # "buy_price": "55978.85", + # "default_leverage": "3", + # "is_fair_price": True, + # "last_trade_price": "55999.23", + # "liquidation_fee": "2", + # "liquidation_level": "10", + # "margin_call_level": "15", + # "max_leverage": "3", + # "max_order_price": "150000", + # "max_order_quantity": "1", + # "max_position_quantity": "1", + # "max_price_precision": 2, + # "min_order_price": "1", + # "min_order_quantity": "0.00002", + # "name": "BTC_USD", + # "position": 1, + # "sell_price": "55985.51", + # "ticker_updated": "1619019818936107989", + # "trade_maker_fee": "0", + # "trade_taker_fee": "0.05", + # "updated": "1619008608955599013" + # } + # ] + # } + # + responses = await asyncio.gather(*promises) + spotResponse = responses[0] + if fetchMargin: + marginPairs = responses[1] + pairs = self.safe_list(marginPairs, 'pairs') + marginPairsDict = self.index_by(pairs, 'name') + keys = list(spotResponse.keys()) + result = [] + for i in range(0, len(keys)): + id = keys[i] + market = spotResponse[id] + marginMarket = self.safe_dict(marginPairsDict, id) + symbol = id.replace('_', '/') + baseId, quoteId = symbol.split('/') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + takerString = self.safe_string(market, 'commission_taker_percent') + makerString = self.safe_string(market, 'commission_maker_percent') + maxQuantity = self.safe_string(market, 'max_quantity') + marginMaxQuantity = self.safe_string(marginMarket, 'max_order_quantity') + result.append({ + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': marginMarket is not None, + 'swap': False, + 'future': False, + 'option': False, + 'active': None, + 'contract': False, + 'linear': None, + 'inverse': None, + 'taker': self.parse_number(Precise.string_div(takerString, '100')), + 'maker': self.parse_number(Precise.string_div(makerString, '100')), + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number('1e-8'), + 'price': self.parse_number(self.parse_precision(self.safe_string(market, 'price_precision'))), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': self.safe_number(market, 'leverage'), + }, + 'amount': { + 'min': self.safe_number(market, 'min_quantity'), + 'max': self.parse_number(Precise.string_max(maxQuantity, marginMaxQuantity)), + }, + 'price': { + 'min': self.safe_number(market, 'min_price'), + 'max': self.safe_number(market, 'max_price'), + }, + 'cost': { + 'min': self.safe_number(market, 'min_amount'), + 'max': self.safe_number(market, 'max_amount'), + }, + }, + 'created': None, + 'info': market, + }) + return result + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#65eeb949-74e5-4631-9184-c38387fe53e8 + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest candle to fetch + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + until = self.safe_integer_product(params, 'until', 0.001) + untilIsDefined = (until is not None) + request: dict = { + 'symbol': market['id'], + 'resolution': self.safe_string(self.timeframes, timeframe, timeframe), + } + maxLimit = 3000 + duration = self.parse_timeframe(timeframe) + now = self.parse_to_int(self.milliseconds() / 1000) + if since is None: + to = min(until, now) if untilIsDefined else now + if limit is None: + limit = 1000 # cap default at generous amount + else: + limit = min(limit, maxLimit) + request['from'] = to - (limit * duration) - 1 + request['to'] = to + else: + request['from'] = self.parse_to_int(since / 1000) + if untilIsDefined: + request['to'] = min(until, now) + else: + if limit is None: + limit = maxLimit + else: + limit = min(limit, maxLimit) + to = self.sum(since, limit * duration) + request['to'] = min(to, now) + params = self.omit(params, 'until') + response = await self.publicGetCandlesHistory(self.extend(request, params)) + # + # { + # "candles":[ + # {"t":1584057600000,"o":0.02235144,"c":0.02400233,"h":0.025171,"l":0.02221,"v":5988.34031761}, + # {"t":1584144000000,"o":0.0240373,"c":0.02367413,"h":0.024399,"l":0.0235,"v":2027.82522329}, + # {"t":1584230400000,"o":0.02363458,"c":0.02319242,"h":0.0237948,"l":0.02223196,"v":1707.96944997}, + # ] + # } + # + candles = self.safe_list(response, 'candles', []) + return self.parse_ohlcvs(candles, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # { + # "t":1584057600000, + # "o":0.02235144, + # "c":0.02400233, + # "h":0.025171, + # "l":0.02221, + # "v":5988.34031761 + # } + # + return [ + self.safe_integer(ohlcv, 't'), + self.safe_number(ohlcv, 'o'), + self.safe_number(ohlcv, 'h'), + self.safe_number(ohlcv, 'l'), + self.safe_number(ohlcv, 'c'), + self.safe_number(ohlcv, 'v'), + ] + + def parse_balance(self, response) -> Balances: + result: dict = {'info': response} + wallets = self.safe_value(response, 'wallets') + if wallets is not None: + currencyIds = list(wallets.keys()) + for i in range(0, len(currencyIds)): + currencyId = currencyIds[i] + item = wallets[currencyId] + currency = self.safe_currency_code(currencyId) + account = self.account() + account['used'] = self.safe_string(item, 'used') + account['free'] = self.safe_string(item, 'free') + account['total'] = self.safe_string(item, 'balance') + result[currency] = account + else: + free = self.safe_value(response, 'balances', {}) + used = self.safe_value(response, 'reserved', {}) + currencyIds = list(free.keys()) + for i in range(0, len(currencyIds)): + currencyId = currencyIds[i] + code = self.safe_currency_code(currencyId) + account = self.account() + if currencyId in free: + account['free'] = self.safe_string(free, currencyId) + if currencyId in used: + account['used'] = self.safe_string(used, currencyId) + result[code] = account + return self.safe_balance(result) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#59c5160f-27a1-4d9a-8cfb-7979c7ffaac6 + https://documenter.getpostman.com/view/10287440/SzYXWKPi#c8388df7-1f9f-4d41-81c4-5a387d171dc6 + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: *isolated* fetches the isolated margin balance + :returns dict: a `balance structure ` + """ + await self.load_markets() + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchBalance', params) + if marginMode == 'cross': + raise BadRequest(self.id + ' does not support cross margin') + response = None + if marginMode == 'isolated': + response = await self.privatePostMarginUserWalletList(params) + # + # { + # "wallets": { + # "USD": { + # "balance": "1000", + # "free": "600", + # "used": "400" + # } + # } + # } + # + else: + response = await self.privatePostUserInfo(params) + # + # { + # "uid":131685, + # "server_date":1628999600, + # "balances":{ + # "EXM":"0", + # "USD":"0", + # "EUR":"0", + # "GBP":"0", + # }, + # } + # + return self.parse_balance(response) + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#c60c51a8-e683-4f45-a000-820723d37871 + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + if limit is not None: + request['limit'] = limit + response = await self.publicGetOrderBook(self.extend(request, params)) + result = self.safe_dict(response, market['id']) + return self.parse_order_book(result, market['symbol'], None, 'bid', 'ask') + + async def fetch_order_books(self, symbols: Strings = None, limit: Int = None, params={}) -> OrderBooks: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data for multiple markets + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#c60c51a8-e683-4f45-a000-820723d37871 + + :param str[]|None symbols: list of unified market symbols, all symbols fetched if None, default is None + :param int [limit]: max number of entries per orderbook to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `order book structures ` indexed by market symbol + """ + await self.load_markets() + ids = None + if symbols is None: + ids = ','.join(self.ids) + # max URL length is 2083 symbols, including http schema, hostname, tld, etc... + if len(ids) > 2048: + numIds = len(self.ids) + raise ExchangeError(self.id + ' fetchOrderBooks() has ' + str(numIds) + ' symbols exceeding max URL length, you are required to specify a list of symbols in the first argument to fetchOrderBooks') + else: + ids = self.market_ids(symbols) + ids = ','.join(ids) + request: dict = { + 'pair': ids, + } + if limit is not None: + request['limit'] = limit + response = await self.publicGetOrderBook(self.extend(request, params)) + result: dict = {} + marketIds = list(response.keys()) + for i in range(0, len(marketIds)): + marketId = marketIds[i] + symbol = self.safe_symbol(marketId) + result[symbol] = self.parse_order_book(response[marketId], symbol, None, 'bid', 'ask') + return result + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "buy_price":"0.00002996", + # "sell_price":"0.00003002", + # "last_trade":"0.00002992", + # "high":"0.00003028", + # "low":"0.00002935", + # "avg":"0.00002963", + # "vol":"1196546.3163222", + # "vol_curr":"35.80066578", + # "updated":1642291733 + # } + # + timestamp = self.safe_timestamp(ticker, 'updated') + market = self.safe_market(None, market) + last = self.safe_string(ticker, 'last_trade') + return self.safe_ticker({ + 'symbol': market['symbol'], + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': self.safe_string(ticker, 'buy_price'), + 'bidVolume': None, + 'ask': self.safe_string(ticker, 'sell_price'), + 'askVolume': None, + 'vwap': None, + 'open': None, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': self.safe_string(ticker, 'avg'), + 'baseVolume': self.safe_string(ticker, 'vol'), + 'quoteVolume': self.safe_string(ticker, 'vol_curr'), + 'info': ticker, + }, market) + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#4c8e6459-3503-4361-b012-c34bb9f7e385 + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + response = await self.publicGetTicker(params) + # + # { + # "ADA_BTC":{ + # "buy_price":"0.00002996", + # "sell_price":"0.00003002", + # "last_trade":"0.00002992", + # "high":"0.00003028", + # "low":"0.00002935", + # "avg":"0.00002963", + # "vol":"1196546.3163222", + # "vol_curr":"35.80066578", + # "updated":1642291733 + # } + # } + # + result: dict = {} + marketIds = list(response.keys()) + for i in range(0, len(marketIds)): + marketId = marketIds[i] + market = self.safe_market(marketId, None, '_') + symbol = market['symbol'] + ticker = self.safe_value(response, marketId) + result[symbol] = self.parse_ticker(ticker, market) + return self.filter_by_array_tickers(result, 'symbol', symbols) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#4c8e6459-3503-4361-b012-c34bb9f7e385 + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + response = await self.publicGetTicker(params) + market = self.market(symbol) + return self.parse_ticker(response[market['id']], market) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(public) + # + # { + # "trade_id":165087520, + # "date":1587470005, + # "type":"buy", + # "quantity":"1.004", + # "price":"0.02491461", + # "amount":"0.02501426" + # }, + # + # fetchMyTrades, fetchOrderTrades + # + # { + # "trade_id": 3, + # "date": 1435488248, + # "type": "buy", + # "pair": "BTC_USD", + # "order_id": 12345, + # "quantity": 1, + # "price": 100, + # "amount": 100, + # "exec_type": "taker", + # "commission_amount": "0.02", + # "commission_currency": "BTC", + # "commission_percent": "0.2" + # } + # + # fetchMyTrades(margin) + # + # { + # "trade_id": "692861757015952517", + # "trade_dt": "1693951853197811824", + # "trade_type": "buy", + # "pair": "ADA_USDT", + # "quantity": "1.96607879", + # "price": "0.2568", + # "amount": "0.50488903" + # } + # + timestamp = self.safe_timestamp(trade, 'date') + id = self.safe_string(trade, 'trade_id') + orderId = self.safe_string(trade, 'order_id') + priceString = self.safe_string(trade, 'price') + amountString = self.safe_string(trade, 'quantity') + costString = self.safe_string(trade, 'amount') + side = self.safe_string_2(trade, 'type', 'trade_type') + type = None + marketId = self.safe_string(trade, 'pair') + market = self.safe_market(marketId, market, '_') + symbol = market['symbol'] + isMaker = self.safe_value(trade, 'is_maker') + takerOrMakerDefault = None + if isMaker is not None: + takerOrMakerDefault = 'maker' if isMaker else 'taker' + takerOrMaker = self.safe_string(trade, 'exec_type', takerOrMakerDefault) + fee = None + feeCostString = self.safe_string(trade, 'commission_amount') + if feeCostString is not None: + feeCurrencyId = self.safe_string(trade, 'commission_currency') + feeCurrencyCode = self.safe_currency_code(feeCurrencyId) + feeRateString = self.safe_string(trade, 'commission_percent') + if feeRateString is not None: + feeRateString = Precise.string_div(feeRateString, '1000', 18) + fee = { + 'cost': feeCostString, + 'currency': feeCurrencyCode, + 'rate': feeRateString, + } + return self.safe_trade({ + 'id': id, + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'order': orderId, + 'type': type, + 'side': side, + 'takerOrMaker': takerOrMaker, + 'price': priceString, + 'amount': amountString, + 'cost': costString, + 'fee': fee, + }, market) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#5a5a9c0d-cf17-47f6-9d62-6d4404ebd5ac + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + response = await self.publicGetTrades(self.extend(request, params)) + # + # { + # "ETH_BTC":[ + # { + # "trade_id":165087520, + # "date":1587470005, + # "type":"buy", + # "quantity":"1.004", + # "price":"0.02491461", + # "amount":"0.02501426" + # }, + # { + # "trade_id":165087369, + # "date":1587469938, + # "type":"buy", + # "quantity":"0.94", + # "price":"0.02492348", + # "amount":"0.02342807" + # } + # ] + # } + # + data = self.safe_list(response, market['id'], []) + return self.parse_trades(data, market, since, limit) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#b8d8d9af-4f46-46a1-939b-ad261d79f452 # spot + https://documenter.getpostman.com/view/10287440/SzYXWKPi#f4b1aaf8-399f-403b-ab5e-4926d967a106 # margin + + :param str symbol: a symbol is required but it can be a single string, or a non-empty array + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: *required for margin orders* the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + + EXCHANGE SPECIFIC PARAMETERS + :param int [params.offset]: last deal offset, default = 0 + :returns Trade[]: a list of `trade structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMyTrades() requires a symbol argument') + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchMyTrades', params) + if marginMode == 'cross': + raise BadRequest(self.id + ' only isolated margin is supported') + await self.load_markets() + market = self.market(symbol) + pair = market['id'] + isSpot = marginMode != 'isolated' + if limit is None: + limit = 100 + request: dict = {} + if isSpot: + request['pair'] = pair + else: + request['pair_name'] = pair + if limit is not None: + request['limit'] = limit + offset = self.safe_integer(params, 'offset', 0) + request['offset'] = offset + response = None + if isSpot: + response = await self.privatePostUserTrades(self.extend(request, params)) + # + # { + # "BTC_USD": [ + # { + # "trade_id": 20056872, + # "client_id": 100500, + # "date": 1435488248, + # "type": "buy", + # "pair": "BTC_USD", + # "quantity": "1", + # "price": "100", + # "amount": "100", + # "order_id": 7, + # "parent_order_id": 117684023830293, + # "exec_type": "taker", + # "commission_amount": "0.02", + # "commission_currency": "BTC", + # "commission_percent": "0.2" + # } + # ], + # ... + # } + # + else: + responseFromExchange = await self.privatePostMarginTrades(self.extend(request, params)) + # + # { + # "trades": { + # "ADA_USDT": [ + # { + # "trade_id": "692861757015952517", + # "trade_dt": "1693951853197811824", + # "trade_type": "buy", + # "pair": "ADA_USDT", + # "quantity": "1.96607879", + # "price": "0.2568", + # "amount": "0.50488903" + # }, + # ] + # ... + # } + # } + # + response = self.safe_value(responseFromExchange, 'trades') + result = [] + marketIdsInner = list(response.keys()) + for i in range(0, len(marketIdsInner)): + marketId = marketIdsInner[i] + resultMarket = self.safe_market(marketId, None, '_') + items = response[marketId] + trades = self.parse_trades(items, resultMarket, since, limit) + result = self.array_concat(result, trades) + return self.filter_by_since_limit(result, since, limit) + + async def create_market_order_with_cost(self, symbol: str, side: OrderSide, cost: float, params={}): + """ + create a market order by providing the symbol, side and cost + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#80daa469-ec59-4d0a-b229-6a311d8dd1cd + + :param str symbol: unified symbol of the market to create an order in + :param str side: 'buy' or 'sell' + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + params = self.extend(params, {'cost': cost}) + return await self.create_order(symbol, 'market', side, cost, None, params) + + async def create_market_buy_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market buy order by providing the symbol and cost + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#80daa469-ec59-4d0a-b229-6a311d8dd1cd + + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + params = self.extend(params, {'cost': cost}) + return await self.create_order(symbol, 'market', 'buy', cost, None, params) + + async def create_market_sell_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market sell order by providing the symbol and cost + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#80daa469-ec59-4d0a-b229-6a311d8dd1cd + + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + params = self.extend(params, {'cost': cost}) + return await self.create_order(symbol, 'market', 'sell', cost, None, params) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#80daa469-ec59-4d0a-b229-6a311d8dd1cd + https://documenter.getpostman.com/view/10287440/SzYXWKPi#de6f4321-eeac-468c-87f7-c4ad7062e265 # stop market + https://documenter.getpostman.com/view/10287440/SzYXWKPi#3561b86c-9ff1-436e-8e68-ac926b7eb523 # margin + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: the price at which a trigger order is triggered at + :param str [params.timeInForce]: *spot only* 'fok', 'ioc' or 'post_only' + :param boolean [params.postOnly]: *spot only* True for post only orders + :param float [params.cost]: *spot only* *market orders only* the cost of the order in the quote currency for market orders + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + isMarket = (type == 'market') and (price is None) + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('createOrder', params) + if marginMode == 'cross': + raise BadRequest(self.id + ' only supports isolated margin') + isSpot = (marginMode != 'isolated') + triggerPrice = self.safe_string_n(params, ['triggerPrice', 'stopPrice', 'stop_price']) + cost = self.safe_string(params, 'cost') + request: dict = { + 'pair': market['id'], + # 'leverage': 2, + # 'quantity': self.amount_to_precision(market['symbol'], amount), + # spot - buy, sell, market_buy, market_sell, market_buy_total, market_sell_total + # margin - limit_buy, limit_sell, market_buy, market_sell, stop_buy, stop_sell, stop_limit_buy, stop_limit_sell, trailing_stop_buy, trailing_stop_sell + # 'stop_price': self.price_to_precision(symbol, stopPrice), + # 'distance': 0, # distance for trailing stop orders + # 'expire': 0, # expiration timestamp in UTC timezone for the order, unless expire is 0 + # 'client_id': 123, # optional, must be a positive integer + # 'comment': '', # up to 50 latin symbols, whitespaces, underscores + } + if cost is None: + request['quantity'] = self.amount_to_precision(market['symbol'], amount) + else: + request['quantity'] = self.cost_to_precision(market['symbol'], cost) + clientOrderId = self.safe_value_2(params, 'client_id', 'clientOrderId') + if clientOrderId is not None: + clientOrderId = self.safe_integer_2(params, 'client_id', 'clientOrderId') + if clientOrderId is None: + raise BadRequest(self.id + ' createOrder() client order id must be an integer / numeric literal') + else: + request['client_id'] = clientOrderId + leverage = self.safe_number(params, 'leverage') + if not isSpot and (leverage is None): + raise ArgumentsRequired(self.id + ' createOrder requires an extra param params["leverage"] for margin orders') + params = self.omit(params, ['stopPrice', 'stop_price', 'triggerPrice', 'timeInForce', 'client_id', 'clientOrderId', 'cost']) + if price is not None: + request['price'] = self.price_to_precision(market['symbol'], price) + response = None + if isSpot: + if triggerPrice is not None: + if type == 'limit': + raise BadRequest(self.id + ' createOrder() cannot create stop limit orders for spot, only stop market') + else: + request['type'] = side + request['trigger_price'] = self.price_to_precision(symbol, triggerPrice) + response = await self.privatePostStopMarketOrderCreate(self.extend(request, params)) + else: + execType = self.safe_string(params, 'exec_type') + isPostOnly = None + isPostOnly, params = self.handle_post_only(type == 'market', execType == 'post_only', params) + timeInForce = self.safe_string(params, 'timeInForce') + request['price'] = 0 if isMarket else self.price_to_precision(market['symbol'], price) + if type == 'limit': + request['type'] = side + elif type == 'market': + marketSuffix = '_total' if (cost is not None) else '' + request['type'] = 'market_' + side + marketSuffix + if isPostOnly: + request['exec_type'] = 'post_only' + elif timeInForce is not None: + request['exec_type'] = timeInForce + response = await self.privatePostOrderCreate(self.extend(request, params)) + else: + if triggerPrice is not None: + request['stop_price'] = self.price_to_precision(symbol, triggerPrice) + if type == 'limit': + request['type'] = 'stop_limit_' + side + elif type == 'market': + request['type'] = 'stop_' + side + else: + request['type'] = type + else: + if type == 'limit' or type == 'market': + request['type'] = type + '_' + side + else: + request['type'] = type + response = await self.privatePostMarginUserOrderCreate(self.extend(request, params)) + return self.parse_order(response, market) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#1f710d4b-75bc-4b65-ad68-006f863a3f26 + https://documenter.getpostman.com/view/10287440/SzYXWKPi#a4d0aae8-28f7-41ac-94fd-c4030130453d # stop market + https://documenter.getpostman.com/view/10287440/SzYXWKPi#705dfec5-2b35-4667-862b-faf54eca6209 # margin + + :param str id: order id + :param str symbol: not used by exmo cancelOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: True to cancel a trigger order + :param str [params.marginMode]: set to 'cross' or 'isolated' to cancel a margin order + :returns dict: An `order structure ` + """ + await self.load_markets() + request: dict = {} + trigger = self.safe_value_2(params, 'trigger', 'stop') + params = self.omit(params, ['trigger', 'stop']) + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('cancelOrder', params) + if marginMode == 'cross': + raise BadRequest(self.id + ' only supports isolated margin') + response = None + if (marginMode == 'isolated'): + request['order_id'] = id + response = await self.privatePostMarginUserOrderCancel(self.extend(request, params)) + # + # {} + # + else: + if trigger: + request['parent_order_id'] = id + response = await self.privatePostStopMarketOrderCancel(self.extend(request, params)) + # + # {} + # + else: + request['order_id'] = id + response = await self.privatePostOrderCancel(self.extend(request, params)) + # + # { + # "error": '', + # "result": True + # } + # + return self.parse_order(response) + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + *spot only* fetches information on an order made by the user + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#cf27781e-28e5-4b39-a52d-3110f5d22459 # spot + + :param str id: order id + :param str symbol: not used by exmo fetchOrder + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + request: dict = { + 'order_id': str(id), + } + response = await self.privatePostOrderTrades(self.extend(request, params)) + # + # { + # "type": "buy", + # "in_currency": "BTC", + # "in_amount": "1", + # "out_currency": "USD", + # "out_amount": "100", + # "trades": [ + # { + # "trade_id": 3, + # "date": 1435488248, + # "type": "buy", + # "pair": "BTC_USD", + # "order_id": 12345, + # "quantity": 1, + # "price": 100, + # "amount": 100 + # } + # ] + # } + # + order = self.parse_order(response) + order['id'] = str(id) + return order + + async def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all the trades made from a single order + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#cf27781e-28e5-4b39-a52d-3110f5d22459 # spot + https://documenter.getpostman.com/view/10287440/SzYXWKPi#00810661-9119-46c5-aec5-55abe9cb42c7 # margin + + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: set to "isolated" to fetch trades for a margin order + :returns dict[]: a list of `trade structures ` + """ + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchOrderTrades', params) + if marginMode == 'cross': + raise BadRequest(self.id + ' only supports isolated margin') + market = None + if symbol is not None: + market = self.market(symbol) + request: dict = { + 'order_id': str(id), + } + response = None + if marginMode == 'isolated': + response = await self.privatePostMarginUserOrderTrades(self.extend(request, params)) + # + # { + # "trades": [ + # { + # "is_maker": False, + # "order_id": "123", + # "pair": "BTC_USD", + # "price": "54122.25", + # "quantity": "0.00069994", + # "trade_dt": "1619069561718824428", + # "trade_id": "692842802860135010", + # "type": "sell" + # } + # ] + # } + # + else: + response = await self.privatePostOrderTrades(self.extend(request, params)) + # + # { + # "type": "buy", + # "in_currency": "BTC", + # "in_amount": "1", + # "out_currency": "USD", + # "out_amount": "100", + # "trades": [ + # { + # "trade_id": 3, + # "date": 1435488248, + # "type": "buy", + # "pair": "BTC_USD", + # "order_id": 12345, + # "quantity": 1, + # "price": 100, + # "amount": 100, + # "exec_type": "taker", + # "commission_amount": "0.02", + # "commission_currency": "BTC", + # "commission_percent": "0.2" + # } + # ] + # } + # + trades = self.safe_list(response, 'trades') + return self.parse_trades(trades, market, since, limit) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#0e135370-daa4-4689-8acd-b6876dee9ba1 # spot open orders + https://documenter.getpostman.com/view/10287440/SzYXWKPi#a7cfd4f0-476e-4675-b33f-22a46902f245 # margin + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: set to "isolated" for margin orders + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchOpenOrders', params) + isMargin = ((marginMode == 'cross') or (marginMode == 'isolated')) + response = None + orders = [] + if isMargin: + response = await self.privatePostMarginUserOrderList(params) + # + # { + # "orders": [ + # { + # "client_id": "0", + # "comment": "", + # "created": "1619068707985325495", + # "distance": "0", + # "expire": 0, + # "funding_currency": "BTC", + # "funding_quantity": "0.01", + # "funding_rate": "0.02", + # "leverage": "2", + # "order_id": "123", + # "pair": "BTC_USD", + # "previous_type": "limit_sell", + # "price": "58000", + # "quantity": "0.01", + # "src": 0, + # "stop_price": "0", + # "trigger_price": "58000", + # "type": "limit_sell", + # "updated": 1619068707989411800 + # } + # ] + # } + # + params = self.extend(params, { + 'status': 'open', + }) + responseOrders = self.safe_value(response, 'orders') + orders = self.parse_orders(responseOrders, market, since, limit, params) + else: + response = await self.privatePostUserOpenOrders(params) + # + # { + # "USDT_USD": [ + # { + # "parent_order_id": "507061384740151010", + # "client_id": "100500", + # "created": "1589547391", + # "type": "stop_market_buy", + # "pair": "USDT_USD", + # "quantity": "1", + # "trigger_price": "5", + # "amount": "5" + # } + # ], + # ... + # } + # + marketIds = list(response.keys()) + for i in range(0, len(marketIds)): + marketId = marketIds[i] + marketInner = self.safe_market(marketId) + params = self.extend(params, { + 'status': 'open', + }) + parsedOrders = self.parse_orders(response[marketId], marketInner, since, limit, params) + orders = self.array_concat(orders, parsedOrders) + return orders + + def parse_status(self, status): + if status is None: + return None + statuses: dict = { + 'cancel_started': 'canceled', + } + if status.find('cancel') >= 0: + status = 'canceled' + return self.safe_string(statuses, status, status) + + def parse_side(self, orderType): + side: dict = { + 'limit_buy': 'buy', + 'limit_sell': 'sell', + 'market_buy': 'buy', + 'market_sell': 'sell', + 'stop_buy': 'buy', + 'stop_sell': 'sell', + 'stop_limit_buy': 'buy', + 'stop_limit_sell': 'sell', + 'trailing_stop_buy': 'buy', + 'trailing_stop_sell': 'sell', + 'stop_market_sell': 'sell', + 'stop_market_buy': 'buy', + 'buy': 'buy', + 'sell': 'sell', + } + return self.safe_string(side, orderType, orderType) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # fetchOrders, fetchOpenOrders, fetchClosedOrders, fetchCanceledOrders + # + # { + # "order_id": "14", + # "created": "1435517311", + # "type": "buy", + # "pair": "BTC_USD", + # "price": "100", + # "quantity": "1", + # "amount": "100" + # } + # + # fetchOrder + # + # { + # "type": "buy", + # "in_currency": "BTC", + # "in_amount": "1", + # "out_currency": "USD", + # "out_amount": "100", + # "trades": [ + # { + # "trade_id": 3, + # "date": 1435488248, + # "type": "buy", + # "pair": "BTC_USD", + # "order_id": 12345, + # "quantity": 1, + # "price": 100, + # "amount": 100 + # } + # ] + # } + # + # Margin fetchOpenOrders + # + # { + # "client_id": "0", + # "comment": "", + # "created": "1619068707985325495", + # "distance": "0", + # "expire": 0, + # "funding_currency": "BTC", + # "funding_quantity": "0.01", + # "funding_rate": "0.02", + # "leverage": "2", + # "order_id": "123", + # "pair": "BTC_USD", + # "previous_type": "limit_sell", + # "price": "58000", + # "quantity": "0.01", + # "src": 0, + # "stop_price": "0", + # "trigger_price": "58000", + # "type": "limit_sell", + # "updated": 1619068707989411800 + # } + # + # Margin fetchClosedOrders + # + # { + # "distance": "0", + # "event_id": "692842802860022508", + # "event_time": "1619069531190173720", + # "event_type": "OrderCancelStarted", + # "order_id": "123", + # "order_status": "cancel_started", + # "order_type": "limit_sell", + # "pair": "BTC_USD", + # "price": "54115", + # "quantity": "0.001", + # "stop_price": "0", + # "trade_id": "0", + # "trade_price": "0", + # "trade_quantity": "0", + # "trade_type": "" + # }, + # + id = self.safe_string_2(order, 'order_id', 'parent_order_id') + eventTime = self.safe_integer_product_2(order, 'event_time', 'created', 0.000001) + timestamp = self.safe_timestamp(order, 'created', eventTime) + orderType = self.safe_string_2(order, 'type', 'order_type') + side = self.parse_side(orderType) + marketId = None + if 'pair' in order: + marketId = order['pair'] + elif ('in_currency' in order) and ('out_currency' in order): + if side == 'buy': + marketId = order['in_currency'] + '_' + order['out_currency'] + else: + marketId = order['out_currency'] + '_' + order['in_currency'] + market = self.safe_market(marketId, market) + symbol = market['symbol'] + amount = self.safe_string(order, 'quantity') + if amount is None: + amountField = 'in_amount' if (side == 'buy') else 'out_amount' + amount = self.safe_string(order, amountField) + price = self.safe_string(order, 'price') + cost = self.safe_string(order, 'amount') + transactions = self.safe_value(order, 'trades', []) + clientOrderId = self.safe_integer(order, 'client_id') + triggerPrice = self.safe_string(order, 'stop_price') + if triggerPrice == '0': + triggerPrice = None + type = None + if (orderType != 'buy') and (orderType != 'sell'): + type = orderType + return self.safe_order({ + 'id': id, + 'clientOrderId': clientOrderId, + 'datetime': self.iso8601(timestamp), + 'timestamp': timestamp, + 'lastTradeTimestamp': self.safe_integer_product(order, 'updated', 0.000001), + 'status': self.parse_status(self.safe_string(order, 'order_status')), + 'symbol': symbol, + 'type': type, + 'timeInForce': None, + 'postOnly': None, + 'side': side, + 'price': price, + 'triggerPrice': triggerPrice, + 'cost': cost, + 'amount': amount, + 'filled': None, + 'remaining': None, + 'average': None, + 'trades': transactions, + 'fee': None, + 'info': order, + }, market) + + async def fetch_canceled_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches information on multiple canceled orders made by the user + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#1d2524dd-ae6d-403a-a067-77b50d13fbe5 # margin + https://documenter.getpostman.com/view/10287440/SzYXWKPi#a51be1d0-af5f-44e4-99d7-f7b04c6067d0 # spot canceled orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: timestamp in ms of the earliest order, default is None + :param int [limit]: max number of orders to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: set to "isolated" for margin orders + :returns dict: a list of `order structures ` + """ + await self.load_markets() + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchOrders', params) + if marginMode == 'cross': + raise BadRequest(self.id + ' only supports isolated margin') + if limit is None: + limit = 100 + isSpot = (marginMode != 'isolated') + if symbol is not None: + marketInner = self.market(symbol) + symbol = marketInner['symbol'] + request: dict = { + 'limit': limit, + } + request['offset'] = limit if (since is not None) else 0 + request['limit'] = limit + market = None + if symbol is not None: + market = self.market(symbol) + response = None + if isSpot: + response = await self.privatePostUserCancelledOrders(self.extend(request, params)) + # + # [ + # { + # "order_id": "27056153840", + # "client_id": "0", + # "created": "1653428646", + # "type": "buy", + # "pair": "BTC_USDT", + # "quantity": "0.1", + # "price": "10", + # "amount": "1" + # } + # ] + # + params = self.extend(params, { + 'status': 'canceled', + }) + return self.parse_orders(response, market, since, limit, params) + else: + responseSwap = await self.privatePostMarginUserOrderHistory(self.extend(request, params)) + # + # { + # "items": [ + # { + # "event_id": "692862104574106858", + # "event_time": "1694116400173489405", + # "event_type": "OrderCancelStarted", + # "order_id": "692862104561289319", + # "order_type": "stop_limit_sell", + # "order_status": "cancel_started", + # "trade_id": "0", + # "trade_type":"", + # "trade_quantity": "0", + # "trade_price": "0", + # "pair": "ADA_USDT", + # "quantity": "12", + # "price": "0.23", + # "stop_price": "0.22", + # "distance": "0" + # } + # ... + # ] + # } + # + items = self.safe_value(responseSwap, 'items') + orders = self.parse_orders(items, market, since, limit, params) + result = [] + for i in range(0, len(orders)): + order = orders[i] + if order['status'] == 'canceled': + result.append(order) + return result + + async def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + """ + *margin only* edit a trade order + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#f27ee040-c75f-4b59-b608-d05bd45b7899 # margin + + :param str id: order id + :param str symbol: unified CCXT market symbol + :param str type: not used by exmo editOrder + :param str side: not used by exmo editOrder + :param float [amount]: how much of the currency you want to trade in units of the base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: stop price for stop-market and stop-limit orders + :param str params['marginMode']: must be set to isolated + + EXCHANGE SPECIFIC PARAMETERS + :param int [params.distance]: distance for trailing stop orders + :param int [params.expire]: expiration timestamp in UTC timezone for the order. order will not be expired if expire is 0 + :param str [params.comment]: optional comment for order. up to 50 latin symbols, whitespaces, underscores + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('editOrder', params) + if marginMode != 'isolated': + raise BadRequest(self.id + ' editOrder() can only be used for isolated margin orders') + triggerPrice = self.safe_number_n(params, ['triggerPrice', 'stopPrice', 'stop_price']) + params = self.omit(params, ['triggerPrice', 'stopPrice']) + request: dict = { + 'order_id': id, # id of the open order + } + if amount is not None: + request['quantity'] = amount + if price is not None: + request['price'] = self.price_to_precision(market['symbol'], price) + if triggerPrice is not None: + request['stop_price'] = self.price_to_precision(market['symbol'], triggerPrice) + response = await self.privatePostMarginUserOrderUpdate(self.extend(request, params)) + return self.parse_order(response) + + async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#c8f9ced9-7ab6-4383-a6a4-bc54469ba60e + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + await self.load_markets() + response = await self.privatePostDepositAddress(params) + # + # { + # "TRX":"TBnwrf4ZdoYXE3C8L2KMs7YPSL3fg6q6V9", + # "USDTTRC20":"TBnwrf4ZdoYXE3C8L2KMs7YPSL3fg6q6V9" + # } + # + depositAddress = self.safe_string(response, code) + address = None + tag = None + if depositAddress: + addressAndTag = depositAddress.split(',') + address = addressAndTag[0] + numParts = len(addressAndTag) + if numParts > 1: + tag = addressAndTag[1] + self.check_address(address) + return { + 'info': response, + 'currency': code, + 'network': None, + 'address': address, + 'tag': tag, + } + + def get_market_from_trades(self, trades): + tradesBySymbol = self.index_by(trades, 'pair') + symbols = list(tradesBySymbol.keys()) + numSymbols = len(symbols) + if numSymbols == 1: + return self.markets[symbols[0]] + return None + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#3ab9c34d-ad58-4f87-9c57-2e2ea88a8325 + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'amount': amount, + 'currency': currency['id'], + 'address': address, + } + if tag is not None: + request['invoice'] = tag + networks = self.safe_value(self.options, 'networks', {}) + network = self.safe_string_upper(params, 'network') # self line allows the user to specify either ERC20 or ETH + network = self.safe_string(networks, network, network) # handle ERC20>ETH alias + if network is not None: + request['transport'] = network + params = self.omit(params, 'network') + response = await self.privatePostWithdrawCrypt(self.extend(request, params)) + return self.parse_transaction(response, currency) + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'transferred': 'ok', + 'paid': 'ok', + 'pending': 'pending', + 'processing': 'pending', + 'verifying': 'pending', + } + return self.safe_string(statuses, status, status) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fetchDepositsWithdrawals + # + # { + # "dt": 1461841192, + # "type": "deposit", + # "curr": "RUB", + # "status": "processing", + # "provider": "Qiwi(LA) [12345]", + # "amount": "1", + # "account": "", + # "txid": "ec46f784ad976fd7f7539089d1a129fe46...", + # } + # + # fetchWithdrawals + # + # { + # "operation_id": 47412538520634344, + # "created": 1573760013, + # "updated": 1573760013, + # "type": "withdraw", + # "currency": "DOGE", + # "status": "Paid", + # "amount": "300", + # "provider": "DOGE", + # "commission": "0", + # "account": "DOGE: DBVy8pF1f8yxaCVEHqHeR7kkcHecLQ8nRS", + # "order_id": 69670170, + # "provider_type": "crypto", + # "crypto_address": "DBVy8pF1f8yxaCVEHqHeR7kkcHecLQ8nRS", + # "card_number": "", + # "wallet_address": "", + # "email": "", + # "phone": "", + # "extra": { + # "txid": "f2b66259ae1580f371d38dd27e31a23fff8c04122b65ee3ab5a3f612d579c792", + # "confirmations": null, + # "excode": "", + # "invoice": "" + # }, + # "error": "" + # } + # + # withdraw + # + # { + # "result": True, + # "error": "", + # "task_id": 11775077 + # } + # + timestamp = self.safe_timestamp_2(transaction, 'dt', 'created') + amountString = self.safe_string(transaction, 'amount') + if amountString is not None: + amountString = Precise.string_abs(amountString) + txid = self.safe_string(transaction, 'txid') + if txid is None: + extra = self.safe_value(transaction, 'extra', {}) + extraTxid = self.safe_string(extra, 'txid') + if extraTxid != '': + txid = extraTxid + type = self.safe_string(transaction, 'type') + currencyId = self.safe_string_2(transaction, 'curr', 'currency') + code = self.safe_currency_code(currencyId, currency) + address = None + comment = None + account = self.safe_string(transaction, 'account') + if type == 'deposit': + comment = account + elif type == 'withdrawal': + address = account + if address is not None: + parts = address.split(':') + numParts = len(parts) + if numParts == 2: + address = self.safe_string(parts, 1) + address = address.replace(' ', '') + fee = { + 'currency': None, + 'cost': None, + 'rate': None, + } + # fixed funding fees only(for now) + if not self.fees['transaction']['percentage']: + key = 'withdraw' if (type == 'withdrawal') else 'deposit' + feeCost = self.safe_string(transaction, 'commission') + if feeCost is None: + transactionFees = self.safe_value(self.options, 'transactionFees', {}) + codeFees = self.safe_value(transactionFees, code, {}) + feeCost = self.safe_string(codeFees, key) + # users don't pay for cashbacks, no fees for that + provider = self.safe_string(transaction, 'provider') + if provider == 'cashback': + feeCost = '0' + if feeCost is not None: + # withdrawal amount includes the fee + if type == 'withdrawal': + amountString = Precise.string_sub(amountString, feeCost) + fee['cost'] = self.parse_number(feeCost) + fee['currency'] = code + return { + 'info': transaction, + 'id': self.safe_string_2(transaction, 'order_id', 'task_id'), + 'txid': txid, + 'type': type, + 'currency': code, + 'network': self.safe_string(transaction, 'provider'), + 'amount': self.parse_number(amountString), + 'status': self.parse_transaction_status(self.safe_string_lower(transaction, 'status')), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'address': address, + 'addressFrom': None, + 'addressTo': address, + 'tag': None, + 'tagFrom': None, + 'tagTo': None, + 'updated': self.safe_timestamp(transaction, 'updated'), + 'comment': comment, + 'internal': None, + 'fee': fee, + } + + async def fetch_deposits_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch history of deposits and withdrawals + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#31e69a33-4849-4e6a-b4b4-6d574238f6a7 + + :param str [code]: unified currency code for the currency of the deposit/withdrawals, default is None + :param int [since]: timestamp in ms of the earliest deposit/withdrawal, default is None + :param int [limit]: max number of deposit/withdrawals to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `transaction structure ` + """ + await self.load_markets() + request: dict = {} + if since is not None: + request['date'] = self.parse_to_int(since / 1000) + currency = None + if code is not None: + currency = self.currency(code) + response = await self.privatePostWalletHistory(self.extend(request, params)) + # + # { + # "result": True, + # "error": "", + # "begin": "1493942400", + # "end": "1494028800", + # "history": [ + # { + # "dt": 1461841192, + # "type": "deposit", + # "curr": "RUB", + # "status": "processing", + # "provider": "Qiwi(LA) [12345]", + # "amount": "1", + # "account": "", + # "txid": "ec46f784ad976fd7f7539089d1a129fe46...", + # }, + # { + # "dt": 1463414785, + # "type": "withdrawal", + # "curr": "USD", + # "status": "paid", + # "provider": "EXCODE", + # "amount": "-1", + # "account": "EX-CODE_19371_USDda...", + # "txid": "", + # }, + # ], + # } + # + return self.parse_transactions(response['history'], currency, since, limit) + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#97f1becd-7aad-4e0e-babe-7bbe09e33706 + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + currency = None + request: dict = { + 'type': 'withdraw', + } + if limit is not None: + request['limit'] = limit # default: 100, maximum: 100 + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + response = await self.privatePostWalletOperations(self.extend(request, params)) + # + # { + # "items": [ + # { + # "operation_id": 47412538520634344, + # "created": 1573760013, + # "updated": 1573760013, + # "type": "withdraw", + # "currency": "DOGE", + # "status": "Paid", + # "amount": "300", + # "provider": "DOGE", + # "commission": "0", + # "account": "DOGE: DBVy8pF1f8yxaCVEHqHeR7kkcHecLQ8nRS", + # "order_id": 69670170, + # "extra": { + # "txid": "f2b66259ae1580f371d38dd27e31a23fff8c04122b65ee3ab5a3f612d579c792", + # "excode": "", + # "invoice": "" + # }, + # "error": "" + # }, + # ], + # "count": 23 + # } + # + items = self.safe_list(response, 'items', []) + return self.parse_transactions(items, currency, since, limit) + + async def fetch_withdrawal(self, id: str, code: Str = None, params={}): + """ + fetch data on a currency withdrawal via the withdrawal id + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#97f1becd-7aad-4e0e-babe-7bbe09e33706 + + :param str id: withdrawal id + :param str code: unified currency code of the currency withdrawn, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + await self.load_markets() + currency = None + request: dict = { + 'order_id': id, + 'type': 'withdraw', + } + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + response = await self.privatePostWalletOperations(self.extend(request, params)) + # + # { + # "items": [ + # { + # "operation_id": 47412538520634344, + # "created": 1573760013, + # "updated": 1573760013, + # "type": "deposit", + # "currency": "DOGE", + # "status": "Paid", + # "amount": "300", + # "provider": "DOGE", + # "commission": "0", + # "account": "DOGE: DBVy8pF1f8yxaCVEHqHeR7kkcHecLQ8nRS", + # "order_id": 69670170, + # "extra": { + # "txid": "f2b66259ae1580f371d38dd27e31a23fff8c04122b65ee3ab5a3f612d579c792", + # "excode": "", + # "invoice": "" + # }, + # "error": "" + # }, + # ], + # "count": 23 + # } + # + items = self.safe_value(response, 'items', []) + first = self.safe_dict(items, 0, {}) + return self.parse_transaction(first, currency) + + async def fetch_deposit(self, id: str, code: Str = None, params={}): + """ + fetch information on a deposit + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#97f1becd-7aad-4e0e-babe-7bbe09e33706 + + :param str id: deposit id + :param str code: unified currency code, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + await self.load_markets() + currency = None + request: dict = { + 'order_id': id, + 'type': 'deposit', + } + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + response = await self.privatePostWalletOperations(self.extend(request, params)) + # + # { + # "items": [ + # { + # "operation_id": 47412538520634344, + # "created": 1573760013, + # "updated": 1573760013, + # "type": "deposit", + # "currency": "DOGE", + # "status": "Paid", + # "amount": "300", + # "provider": "DOGE", + # "commission": "0", + # "account": "DOGE: DBVy8pF1f8yxaCVEHqHeR7kkcHecLQ8nRS", + # "order_id": 69670170, + # "extra": { + # "txid": "f2b66259ae1580f371d38dd27e31a23fff8c04122b65ee3ab5a3f612d579c792", + # "excode": "", + # "invoice": "" + # }, + # "error": "" + # }, + # ], + # "count": 23 + # } + # + items = self.safe_value(response, 'items', []) + first = self.safe_dict(items, 0, {}) + return self.parse_transaction(first, currency) + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#97f1becd-7aad-4e0e-babe-7bbe09e33706 + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + currency = None + request: dict = { + 'type': 'deposit', + } + if limit is not None: + request['limit'] = limit # default: 100, maximum: 100 + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + response = await self.privatePostWalletOperations(self.extend(request, params)) + # + # { + # "items": [ + # { + # "operation_id": 47412538520634344, + # "created": 1573760013, + # "updated": 1573760013, + # "type": "deposit", + # "currency": "DOGE", + # "status": "Paid", + # "amount": "300", + # "provider": "DOGE", + # "commission": "0", + # "account": "DOGE: DBVy8pF1f8yxaCVEHqHeR7kkcHecLQ8nRS", + # "order_id": 69670170, + # "extra": { + # "txid": "f2b66259ae1580f371d38dd27e31a23fff8c04122b65ee3ab5a3f612d579c792", + # "excode": "", + # "invoice": "" + # }, + # "error": "" + # }, + # ], + # "count": 23 + # } + # + items = self.safe_list(response, 'items', []) + return self.parse_transactions(items, currency, since, limit) + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = self.urls['api'][api] + '/' + if api != 'web': + url += self.version + '/' + url += path + if (api == 'public') or (api == 'web'): + if params: + url += '?' + self.urlencode(params) + elif api == 'private': + self.check_required_credentials() + nonce = self.nonce() + body = self.urlencode(self.extend({'nonce': nonce}, params)) + headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Key': self.apiKey, + 'Sign': self.hmac(self.encode(body), self.encode(self.secret), hashlib.sha512), + } + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def nonce(self): + return self.milliseconds() + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None # fallback to default error handler + if ('error' in response) and not ('result' in response): + # error: { + # "code": "140434", + # "msg": "Your margin balance is not sufficient to place the order for '5 TON'. Please top up your margin wallet by "2.5 USDT"." + # } + # + errorCode = self.safe_value(response, 'error', {}) + messageError = self.safe_string(errorCode, 'msg') + code = self.safe_string(errorCode, 'code') + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], code, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], messageError, feedback) + raise ExchangeError(feedback) + if ('result' in response) or ('errmsg' in response): + # + # {"result":false,"error":"Error 50052: Insufficient funds"} + # {"s":"error","errmsg":"strconv.ParseInt: parsing \"\": invalid syntax"} + # + success = self.safe_bool(response, 'result', False) + if isinstance(success, str): + if (success == 'true') or (success == '1'): + success = True + else: + success = False + if not success: + code = None + message = self.safe_string_2(response, 'error', 'errmsg') + errorParts = message.split(':') + numParts = len(errorParts) + if numParts > 1: + errorSubParts = errorParts[0].split(' ') + numSubParts = len(errorSubParts) + code = errorSubParts[1] if (numSubParts > 1) else errorSubParts[0] + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], code, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + raise ExchangeError(feedback) + return None diff --git a/ccxt/async_support/fmfwio.py b/ccxt/async_support/fmfwio.py new file mode 100644 index 0000000..37dd8cf --- /dev/null +++ b/ccxt/async_support/fmfwio.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.hitbtc import hitbtc +from ccxt.abstract.fmfwio import ImplicitAPI +from ccxt.base.types import Any + + +class fmfwio(hitbtc, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(fmfwio, self).describe(), { + 'id': 'fmfwio', + 'name': 'FMFW.io', + 'countries': ['KN'], + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/159177712-b685b40c-5269-4cea-ac83-f7894c49525d.jpg', + 'api': { + 'public': 'https://api.fmfw.io/api/3', + 'private': 'https://api.fmfw.io/api/3', + }, + 'www': 'https://fmfw.io', + 'doc': 'https://api.fmfw.io/', + 'fees': 'https://fmfw.io/fees-and-limits', + 'referral': 'https://fmfw.io/referral/da948b21d6c92d69', + }, + 'fees': { + 'trading': { + 'maker': self.parse_number('0.005'), + 'taker': self.parse_number('0.005'), + }, + }, + }) diff --git a/ccxt/async_support/foxbit.py b/ccxt/async_support/foxbit.py new file mode 100644 index 0000000..1b68cd3 --- /dev/null +++ b/ccxt/async_support/foxbit.py @@ -0,0 +1,1935 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.foxbit import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currencies, Currency, DepositAddress, Int, Market, Num, Order, OrderBook, OrderRequest, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, TradingFeeInterface, TradingFees, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import AccountSuspended +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.errors import OnMaintenance +from ccxt.base.decimal_to_precision import DECIMAL_PLACES +from ccxt.base.precise import Precise + + +class foxbit(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(foxbit, self).describe(), { + 'id': 'foxbit', + 'name': 'Foxbit', + 'countries': ['pt-BR'], + # 300 requests per 10 seconds = 30 requests per second + # rateLimit = 1000 ms / 30 requests ~= 33.334 + 'rateLimit': 33.334, + 'version': '1', + 'comment': 'Foxbit Exchange', + 'certified': False, + 'pro': False, + 'has': { + 'CORS': True, + 'spot': True, + 'margin': None, + 'swap': None, + 'future': None, + 'option': None, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'createLimitBuyOrder': True, + 'createLimitSellOrder': True, + 'createMarketBuyOrder': True, + 'createMarketSellOrder': True, + 'createOrder': True, + 'fecthOrderBook': True, + 'fetchBalance': True, + 'fetchCanceledOrders': True, + 'fetchClosedOrders': True, + 'fetchCurrencies': True, + 'fetchDepositAddress': True, + 'fetchDeposits': True, + 'fetchL2OrderBook': True, + 'fetchLedger': True, + 'fetchMarkets': True, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrders': True, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTrades': True, + 'fetchTradingFee': True, + 'fetchTradingFees': True, + 'fetchTransactions': True, + 'fetchWithdrawals': True, + 'loadMarkets': True, + 'sandbox': False, + 'withdraw': True, + 'ws': False, + }, + 'timeframes': { + '1m': '1m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1h', + '2h': '2h', + '4h': '4h', + '6h': '6h', + '12h': '12h', + '1d': '1d', + '1w': '1w', + '2w': '2w', + '1M': '1M', + }, + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/1f8faca2-ae2f-4222-b33e-5671e7d873dd', + 'api': { + 'public': 'https://api.foxbit.com.br', + 'private': 'https://api.foxbit.com.br', + 'status': 'https://metadata-v2.foxbit.com.br/api', + }, + 'www': 'https://app.foxbit.com.br', + 'doc': [ + 'https://docs.foxbit.com.br', + ], + }, + 'precisionMode': DECIMAL_PLACES, + 'exceptions': { + 'exact': { + # https://docs.foxbit.com.br/rest/v3/#tag/API-Codes/Errors + '400': BadRequest, # Bad request. An unknown error occurred while processing request parameters. + '429': RateLimitExceeded, # Too many requests. Request limit exceeded. Try again later. + '404': BadRequest, # Resource not found. A resource was not found while processing the request. + '500': ExchangeError, # Internal server error. An unknown error occurred while processing the request. + '2001': AuthenticationError, # Authentication error. Error authenticating request. + '2002': AuthenticationError, # Invalid signature. The signature for self request is not valid. + '2003': AuthenticationError, # Invalid access key. Access key missing, invalid or not found. + '2004': BadRequest, # Invalid timestamp. Invalid or missing timestamp. + '2005': PermissionDenied, # IP not allowed. The IP address {IP_ADDR} isn't on the trusted list for self API key. + '3001': PermissionDenied, # Permission denied. Permission denied for self request. + '3002': PermissionDenied, # KYC required. A greater level of KYC verification is required to proceed with self request. + '3003': AccountSuspended, # Member disabled. This member is disabled. Please get in touch with our support for more information. + '4001': BadRequest, # Validation error. A validation error occurred. + '4002': InsufficientFunds, # Insufficient funds. Insufficient funds to proceed with self request. + '4003': InvalidOrder, # Quantity below the minimum allowed. Quantity below the minimum allowed to proceed with self request. + '4004': BadSymbol, # Invalid symbol. The market or asset symbol is invalid or was not found. + '4005': BadRequest, # Invalid idempotent. Characters allowed are "a-z", "0-9", "_" or "-", and 36 at max. We recommend UUID v4 in lowercase. + '4007': ExchangeError, # Locked error. There was an error in your allocated balance, please contact us. + '4008': InvalidOrder, # Cannot submit order. The order cannot be created. + '4009': PermissionDenied, # Invalid level. The sub-member does not have the required level to create the transaction. + '4011': RateLimitExceeded, # Too many open orders. You have reached the limit of open orders per market/side. + '4012': ExchangeError, # Too many simultaneous account operations. We are currently unable to process your balance change due to simultaneous operations on your account. Please retry shortly. + '5001': ExchangeNotAvailable, # Service unavailable. The requested resource is currently unavailable. Try again later. + '5002': OnMaintenance, # Service under maintenance. The requested resource is currently under maintenance. Try again later. + '5003': OnMaintenance, # Market under maintenance. The market is under maintenance. Try again later. + '5004': InvalidOrder, # Market is not deep enough. The market is not deep enough to complete your request. + '5005': InvalidOrder, # Price out of range from market. The order price is out of range from market to complete your request. + '5006': InvalidOrder, # Significant price deviation detected, exceeding acceptable limits. The order price is exceeding acceptable limits from market to complete your request. + }, + 'broad': { + # todo: add details messages that can be usefull here, like when market is not found + }, + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + }, + 'api': { + 'v3': { + 'public': { + 'get': { + 'currencies': 5, # 6 requests per second + 'markets': 5, # 6 requests per second + 'markets/ticker/24hr': 60, # 1 request per 2 seconds + 'markets/{market}/orderbook': 6, # 10 requests per 2 seconds + 'markets/{market}/candlesticks': 12, # 5 requests per 2 seconds + 'markets/{market}/trades/history': 12, # 5 requests per 2 seconds + 'markets/{market}/ticker/24hr': 15, # 4 requests per 2 seconds + }, + }, + 'private': { + 'get': { + 'accounts': 2, # 15 requests per second + 'accounts/{symbol}/transactions': 60, # 1 requests per 2 seconds + 'orders': 2, # 30 requests per 2 seconds + 'orders/by-order-id/{id}': 2, # 30 requests per 2 seconds + 'trades': 6, # 5 orders per second + 'deposits/address': 10, # 3 requests per second + 'deposits': 10, # 3 requests per second + 'withdrawals': 10, # 3 requests per second + 'me/fees/trading': 60, # 1 requests per 2 seconds + }, + 'post': { + 'orders': 2, # 30 requests per 2 seconds + 'orders/batch': 7.5, # 8 requests per 2 seconds + 'orders/cancel-replace': 3, # 20 requests per 2 seconds + 'withdrawals': 10, # 3 requests per second + }, + 'put': { + 'orders/cancel': 2, # 30 requests per 2 seconds + }, + }, + }, + 'status': { + 'public': { + 'get': { + 'status': 30, # 1 request per second + }, + }, + }, + }, + 'fees': { + 'trading': { + 'feeSide': 'get', + 'tierBased': False, + 'percentage': True, + 'taker': self.parse_number('0.005'), + 'maker': self.parse_number('0.0025'), + }, + }, + 'options': { + 'sandboxMode': False, + 'networksById': { + 'algorand': 'ALGO', + 'arbitrum': 'ARBITRUM', + 'avalanchecchain': 'AVAX', + 'bitcoin': 'BTC', + 'bitcoincash': 'BCH', + 'bsc': 'BEP20', + 'cardano': 'ADA', + 'cosmos': 'ATOM', + 'dogecoin': 'DOGE', + 'erc20': 'ETH', + 'hedera': 'HBAR', + 'litecoin': 'LTC', + 'near': 'NEAR', + 'optimism': 'OPTIMISM', + 'polkadot': 'DOT', + 'polygon': 'MATIC', + 'ripple': 'XRP', + 'solana': 'SOL', + 'stacks': 'STX', + 'stellar': 'XLM', + 'tezos': 'XTZ', + 'trc20': 'TRC20', + }, + 'networks': { + 'ALGO': 'algorand', + 'ARBITRUM': 'arbitrum', + 'AVAX': 'avalanchecchain', + 'BTC': 'bitcoin', + 'BCH': 'bitcoincash', + 'BEP20': 'bsc', + 'ADA': 'cardano', + 'ATOM': 'cosmos', + 'DOGE': 'dogecoin', + 'ETH': 'erc20', + 'HBAR': 'hedera', + 'LTC': 'litecoin', + 'NEAR': 'near', + 'OPTIMISM': 'optimism', + 'DOT': 'polkadot', + 'MATIC': 'polygon', + 'XRP': 'ripple', + 'SOL': 'solana', + 'STX': 'stacks', + 'XLM': 'stellar', + 'XTZ': 'tezos', + 'TRC20': 'trc20', + }, + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerPriceType': { + 'last': True, # foxbit default trigger price type is last, no params will change it + 'mark': False, + 'index': False, + }, + 'triggerDirection': False, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'GTC': True, + 'FOK': True, + 'IOC': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'leverage': False, + 'marketBuyByCost': False, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': { + 'expire_maker': True, # foxbit prevents self trading by default, no params can change self + 'expire_taker': True, # foxbit prevents self trading by default, no params can change self + 'expire_both': True, # foxbit prevents self trading by default, no params can change self + 'none': True, # foxbit prevents self trading by default, no params can change self + }, + 'trailing': False, + 'icebergAmount': False, + }, + 'createOrders': { + 'max': 5, + }, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 90, + 'untilDays': 10000, # high value just to keep clear that there is no range limit, just the limit of the page size + 'symbolRequired': True, + }, + 'fetchOrder': { + 'marginMode': False, + 'limit': 1, + 'daysBack': 90, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 90, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': True, + 'limit': 100, + 'daysBack': 90, + 'untilDays': 10000, # high value just to keep clear that there is no range limit, just the limit of the page size + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchClosedOrders': { + 'marginMode': True, + 'limit': 100, + 'daysBack': 90, + 'daysBackCanceled': 90, + 'untilDays': 10000, # high value just to keep clear that there is no range limit, just the limit of the page size + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 500, + }, + }, + }, + }) + + async def fetch_currencies(self, params={}) -> Currencies: + response = await self.v3PublicGetCurrencies(params) + # { + # "data": [ + # { + # "symbol": "btc", + # "name": "Bitcoin", + # "type": "CRYPTO", + # "precision": 8, + # "deposit_info": { + # "min_to_confirm": "1", + # "min_amount": "0.0001" + # }, + # "withdraw_info": { + # "enabled": True, + # "min_amount": "0.0001", + # "fee": "0.0001" + # }, + # "category": { + # "code": "cripto", + # "name": "Cripto" + # }, + # "networks": [ + # { + # "name": "Bitcoin", + # "code": "btc", + # "deposit_info": { + # status: "ENABLED", + # }, + # "withdraw_info": { + # "status": "ENABLED", + # "fee": "0.0001", + # }, + # "has_destination_tag": False + # } + # ] + # } + # ] + # } + data = self.safe_list(response, 'data', []) + result: dict = {} + for i in range(0, len(data)): + currency = data[i] + precision = self.safe_integer(currency, 'precision') + currencyId = self.safe_string(currency, 'symbol') + name = self.safe_string(currency, 'name') + code = self.safe_currency_code(currencyId) + depositInfo = self.safe_dict(currency, 'deposit_info') + withdrawInfo = self.safe_dict(currency, 'withdraw_info') + networks = self.safe_list(currency, 'networks', []) + type = self.safe_string_lower(currency, 'type') + parsedNetworks: dict = {} + for j in range(0, len(networks)): + network = networks[j] + networkId = self.safe_string(network, 'code') + networkCode = self.network_id_to_code(networkId, code) + networkWithdrawInfo = self.safe_dict(network, 'withdraw_info') + networkDepositInfo = self.safe_dict(network, 'deposit_info') + isWithdrawEnabled = self.safe_string(networkWithdrawInfo, 'status') == 'ENABLED' + isDepositEnabled = self.safe_string(networkDepositInfo, 'status') == 'ENABLED' + parsedNetworks[networkCode] = { + 'info': currency, + 'id': networkId, + 'network': networkCode, + 'name': self.safe_string(network, 'name'), + 'deposit': isDepositEnabled, + 'withdraw': isWithdrawEnabled, + 'active': True, + 'precision': precision, + 'fee': self.safe_number(networkWithdrawInfo, 'fee'), + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'deposit': { + 'min': self.safe_number(depositInfo, 'min_amount'), + 'max': None, + }, + 'withdraw': { + 'min': self.safe_number(withdrawInfo, 'min_amount'), + 'max': None, + }, + }, + } + if self.safe_dict(result, code) is None: + result[code] = self.safe_currency_structure({ + 'id': currencyId, + 'code': code, + 'info': currency, + 'name': name, + 'active': True, + 'type': type, + 'deposit': self.safe_bool(depositInfo, 'enabled', False), + 'withdraw': self.safe_bool(withdrawInfo, 'enabled', False), + 'fee': self.safe_number(withdrawInfo, 'fee'), + 'precision': precision, + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'deposit': { + 'min': self.safe_number(depositInfo, 'min_amount'), + 'max': None, + }, + 'withdraw': { + 'min': self.safe_number(withdrawInfo, 'min_amount'), + 'max': None, + }, + }, + 'networks': parsedNetworks, + }) + return result + + async def fetch_markets(self, params={}) -> List[Market]: + """ + Retrieves data on all markets for foxbit. + + https://docs.foxbit.com.br/rest/v3/#tag/Market-Data/operation/MarketsController_index + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = await self.v3PublicGetMarkets(params) + # { + # "data": [ + # { + # "symbol": "btcbrl", + # "quantity_min": "0.00000236", + # "quantity_increment": "0.00000001", + # "quantity_precision": 8, + # "price_min": "0.0001", + # "price_increment": "0.0001", + # "price_precision": 4, + # "default_fees": { + # "maker": "0.001", + # "taker": "0.001" + # }, + # "base": { + # "symbol": "btc", + # "name": "Bitcoin", + # "type": "CRYPTO", + # "precision": 8, + # "category": { + # "code": "cripto", + # "name": "Cripto" + # }, + # "deposit_info": { + # "min_to_confirm": "1", + # "min_amount": "0.0001", + # "enabled": True + # }, + # "withdraw_info": { + # "enabled": True, + # "min_amount": "0.0001", + # "fee": "0.0001" + # }, + # "networks": [ + # { + # "name": "Bitcoin", + # "code": "bitcoin", + # "deposit_info": { + # "status": "ENABLED" + # }, + # "withdraw_info": { + # "status": "ENABLED", + # "fee": "0.0001" + # }, + # "has_destination_tag": False + # } + # ], + # "default_network_code": "bitcoin" + # }, + # "quote": { + # "symbol": "btc", + # "name": "Bitcoin", + # "type": "CRYPTO", + # "precision": 8, + # "category": { + # "code": "cripto", + # "name": "Cripto" + # }, + # "deposit_info": { + # "min_to_confirm": "1", + # "min_amount": "0.0001", + # "enabled": True + # }, + # "withdraw_info": { + # "enabled": True, + # "min_amount": "0.0001", + # "fee": "0.0001" + # }, + # "networks": [ + # { + # "name": "Bitcoin", + # "code": "bitcoin", + # "deposit_info": { + # "status": "ENABLED" + # }, + # "withdraw_info": { + # "status": "ENABLED", + # "fee": "0.0001" + # }, + # "has_destination_tag": False + # } + # ], + # "default_network_code": "bitcoin" + # }, + # "order_type": [ + # "LIMIT", + # "MARKET", + # "INSTANT", + # "STOP_LIMIT", + # "STOP_MARKET" + # ] + # } + # ] + # } + markets = self.safe_list(response, 'data', []) + return self.parse_markets(markets) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + Get last 24 hours ticker information, in real-time, for given market. + + https://docs.foxbit.com.br/rest/v3/#tag/Market-Data/operation/MarketsController_ticker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + response = await self.v3PublicGetMarketsMarketTicker24hr(self.extend(request, params)) + # { + # "data": [ + # { + # "market_symbol": "btcbrl", + # "last_trade": { + # "price": "358504.69340000", + # "volume": "0.00027893", + # "date": "2024-01-01T00:00:00.000Z" + # }, + # "rolling_24h": { + # "price_change": "3211.87290000", + # "price_change_percent": "0.90400726", + # "volume": "20.03206866", + # "trades_count": "4376", + # "open": "355292.82050000", + # "high": "362999.99990000", + # "low": "355002.88880000" + # }, + # "best": { + # "ask": { + # "price": "358504.69340000", + # "volume": "0.00027893" + # }, + # "bid": { + # "price": "358504.69340000", + # "volume": "0.00027893" + # } + # } + # } + # ] + # } + data = self.safe_list(response, 'data', []) + result = self.safe_dict(data, 0, {}) + return self.parse_ticker(result, market) + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + Retrieve the ticker data of all markets. + + https://docs.foxbit.com.br/rest/v3/#tag/Market-Data/operation/MarketsController_tickers + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + response = await self.v3PublicGetMarketsTicker24hr(params) + # { + # "data": [ + # { + # "market_symbol": "btcbrl", + # "last_trade": { + # "price": "358504.69340000", + # "volume": "0.00027893", + # "date": "2024-01-01T00:00:00.000Z" + # }, + # "rolling_24h": { + # "price_change": "3211.87290000", + # "price_change_percent": "0.90400726", + # "volume": "20.03206866", + # "trades_count": "4376", + # "open": "355292.82050000", + # "high": "362999.99990000", + # "low": "355002.88880000" + # }, + # } + # ] + # } + data = self.safe_list(response, 'data', []) + return self.parse_tickers(data, symbols) + + async def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://docs.foxbit.com.br/rest/v3/#tag/Member-Info/operation/MembersController_listTradingFees + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + await self.load_markets() + response = await self.v3PrivateGetMeFeesTrading(params) + # [ + # { + # "market_symbol": "btcbrl", + # "maker": "0.0025", + # "taker": "0.005" + # } + # ] + data = self.safe_list(response, 'data', []) + result = {} + for i in range(0, len(data)): + entry = data[i] + marketId = self.safe_string(entry, 'market_symbol') + market = self.safe_market(marketId) + symbol = market['symbol'] + result[symbol] = self.parse_trading_fee(entry, market) + return result + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + Exports a copy of the order book of a specific market. + + https://docs.foxbit.com.br/rest/v3/#tag/Market-Data/operation/MarketsController_findOrderbook + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return, the maximum is 100 + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + defaultLimit = 20 + request: dict = { + 'market': market['id'], + 'depth': defaultLimit if (limit is None) else limit, + } + response = await self.v3PublicGetMarketsMarketOrderbook(self.extend(request, params)) + # { + # "sequence_id": 1234567890, + # "timestamp": 1713187921336, + # "bids": [ + # [ + # "3.00000000", + # "300.00000000" + # ], + # [ + # "1.70000000", + # "310.00000000" + # ] + # ], + # "asks": [ + # [ + # "3.00000000", + # "300.00000000" + # ], + # [ + # "2.00000000", + # "321.00000000" + # ] + # ] + # } + timestamp = self.safe_integer(response, 'timestamp') + return self.parse_order_book(response, symbol, timestamp) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + Retrieve the trades of a specific market. + + https://docs.foxbit.com.br/rest/v3/#tag/Market-Data/operation/MarketsController_publicTrades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + if limit is not None: + request['page_size'] = limit + if limit > 200: + request['page_size'] = 200 + # [ + # { + # "id": 1, + # "price": "329248.74700000", + # "volume": "0.00100000", + # "taker_side": "BUY", + # "created_at": "2024-01-01T00:00:00Z" + # } + # ] + response = await self.v3PublicGetMarketsMarketTradesHistory(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + return self.parse_trades(data, market, since, limit) + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + Fetch historical candlestick data containing the open, high, low, and close price, and the volume of a market. + + https://docs.foxbit.com.br/rest/v3/#tag/Market-Data/operation/MarketsController_findCandlesticks + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + interval = self.safe_string(self.timeframes, timeframe, timeframe) + request: dict = { + 'market': market['id'], + 'interval': interval, + } + if since is not None: + request['start_time'] = self.iso8601(since) + if limit is not None: + request['limit'] = limit + if limit > 500: + request['limit'] = 500 + response = await self.v3PublicGetMarketsMarketCandlesticks(self.extend(request, params)) + # [ + # [ + # "1692918000000", # timestamp + # "127772.05150000", # open + # "128467.99980000", # high + # "127750.01000000", # low + # "128353.99990000", # close + # "1692918060000", # close timestamp + # "0.17080431", # base volume + # "21866.35948786", # quote volume + # 66, # number of trades + # "0.12073605", # taker buy base volume + # "15466.34096391" # taker buy quote volume + # ] + # ] + return self.parse_ohlcvs(response, market, interval, since, limit) + + async def fetch_balance(self, params={}) -> Balances: + """ + Query for balance and get the amount of funds available for trading or funds locked in orders. + + https://docs.foxbit.com.br/rest/v3/#tag/Account/operation/AccountsController_all + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + response = await self.v3PrivateGetAccounts(params) + # { + # "data": [ + # { + # "currency_symbol": "btc", + # "balance": "10000.0", + # "balance_available": "9000.0", + # "balance_locked": "1000.0" + # } + # ] + # } + accounts = self.safe_list(response, 'data', []) + result: dict = { + 'info': response, + } + for i in range(0, len(accounts)): + account = accounts[i] + currencyId = self.safe_string(account, 'currency_symbol') + currencyCode = self.safe_currency_code(currencyId) + total = self.safe_string(account, 'balance') + used = self.safe_string(account, 'balance_locked') + free = self.safe_string(account, 'balance_available') + balanceObj = { + 'free': free, + 'used': used, + 'total': total, + } + result[currencyCode] = balanceObj + return self.safe_balance(result) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + Fetch all unfilled currently open orders. + + https://docs.foxbit.com.br/rest/v3/#tag/Trading/operation/OrdersController_listOrders + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + return await self.fetch_orders_by_status('ACTIVE', symbol, since, limit, params) + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + Fetch all currently closed orders. + + https://docs.foxbit.com.br/rest/v3/#tag/Trading/operation/OrdersController_listOrders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + return await self.fetch_orders_by_status('FILLED', symbol, since, limit, params) + + async def fetch_canceled_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + return await self.fetch_orders_by_status('CANCELED', symbol, since, limit, params) + + async def fetch_orders_by_status(self, status: Str, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + await self.load_markets() + market = None + request: dict = { + 'state': status, + } + if symbol is not None: + market = self.market(symbol) + request['market_symbol'] = market['id'] + if since is not None: + request['start_time'] = self.iso8601(since) + if limit is not None: + request['page_size'] = limit + if limit > 100: + request['page_size'] = 100 + response = await self.v3PrivateGetOrders(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + return self.parse_orders(data) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}) -> Order: + """ + Create an order with the specified characteristics + + https://docs.foxbit.com.br/rest/v3/#tag/Trading/operation/OrdersController_create + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market', 'limit', 'stop_market', 'stop_limit', 'instant' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency + :param float [price]: the price at which the order is to be fullfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.timeInForce]: "GTC", "FOK", "IOC", "PO" + :param float [params.triggerPrice]: The time in force for the order. One of GTC, FOK, IOC, PO. See .features or foxbit's doc to see more details. + :param bool [params.postOnly]: True or False whether the order is post-only + :param str [params.clientOrderId]: a unique identifier for the order + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + type = type.upper() + if type != 'LIMIT' and type != 'MARKET' and type != 'STOP_MARKET' and type != 'STOP_LIMIT' and type != 'INSTANT': + raise InvalidOrder('Invalid order type: ' + type + '. Must be one of: limit, market, stop_market, stop_limit, instant.') + timeInForce = self.safe_string_upper(params, 'timeInForce') + postOnly = self.safe_bool(params, 'postOnly', False) + triggerPrice = self.safe_number(params, 'triggerPrice') + request: dict = { + 'market_symbol': market['id'], + 'side': side.upper(), + 'type': type, + } + if type == 'STOP_MARKET' or type == 'STOP_LIMIT': + if triggerPrice is None: + raise InvalidOrder('Invalid order type: ' + type + '. Must have triggerPrice.') + if timeInForce is not None: + if timeInForce == 'PO': + request['post_only'] = True + else: + request['time_in_force'] = timeInForce + if postOnly: + request['post_only'] = True + if triggerPrice is not None: + request['stop_price'] = self.price_to_precision(symbol, triggerPrice) + if type == 'INSTANT': + request['amount'] = self.price_to_precision(symbol, amount) + else: + request['quantity'] = self.amount_to_precision(symbol, amount) + if type == 'LIMIT' or type == 'STOP_LIMIT': + request['price'] = self.price_to_precision(symbol, price) + clientOrderId = self.safe_string(params, 'clientOrderId') + if clientOrderId is not None: + request['client_order_id'] = clientOrderId + params = self.omit(params, ['timeInForce', 'postOnly', 'triggerPrice', 'clientOrderId']) + response = await self.v3PrivatePostOrders(self.extend(request, params)) + # { + # "id": 1234567890, + # "sn": "OKMAKSDHRVVREK", + # "client_order_id": "451637946501" + # } + return self.parse_order(response, market) + + async def create_orders(self, orders: List[OrderRequest], params={}): + """ + create a list of trade orders + + https://docs.foxbit.com.br/rest/v3/#tag/Trading/operation/createBatch + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + ordersRequests = [] + for i in range(0, len(orders)): + order = self.safe_dict(orders, i) + symbol = self.safe_string(order, 'symbol') + market = self.market(symbol) + type = self.safe_string_upper(order, 'type') + orderParams = self.safe_dict(order, 'params', {}) + if type != 'LIMIT' and type != 'MARKET' and type != 'STOP_MARKET' and type != 'STOP_LIMIT' and type != 'INSTANT': + raise InvalidOrder('Invalid order type: ' + type + '. Must be one of: limit, market, stop_market, stop_limit, instant.') + timeInForce = self.safe_string_upper(orderParams, 'timeInForce') + postOnly = self.safe_bool(orderParams, 'postOnly', False) + triggerPrice = self.safe_number(orderParams, 'triggerPrice') + request: dict = { + 'market_symbol': market['id'], + 'side': self.safe_string_upper(order, 'side'), + 'type': type, + } + if type == 'STOP_MARKET' or type == 'STOP_LIMIT': + if triggerPrice is None: + raise InvalidOrder('Invalid order type: ' + type + '. Must have triggerPrice.') + if timeInForce is not None: + if timeInForce == 'PO': + request['post_only'] = True + else: + request['time_in_force'] = timeInForce + del orderParams['timeInForce'] + if postOnly: + request['post_only'] = True + del orderParams['postOnly'] + if triggerPrice is not None: + request['stop_price'] = self.price_to_precision(symbol, triggerPrice) + del orderParams['triggerPrice'] + if type == 'INSTANT': + request['amount'] = self.price_to_precision(symbol, self.safe_string(order, 'amount')) + else: + request['quantity'] = self.amount_to_precision(symbol, self.safe_string(order, 'amount')) + if type == 'LIMIT' or type == 'STOP_LIMIT': + request['price'] = self.price_to_precision(symbol, self.safe_string(order, 'price')) + ordersRequests.append(self.extend(request, orderParams)) + createOrdersRequest = {'data': ordersRequests} + response = await self.v3PrivatePostOrdersBatch(self.extend(createOrdersRequest, params)) + # { + # "data": [ + # { + # "side": "BUY", + # "type": "LIMIT", + # "market_symbol": "btcbrl", + # "client_order_id": "451637946501", + # "remark": "A remarkable note for the order.", + # "quantity": "0.42", + # "price": "250000.0", + # "post_only": True, + # "time_in_force": "GTC" + # } + # ] + # } + data = self.safe_list(response, 'data', []) + return self.parse_orders(data) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + Cancel open orders. + + https://docs.foxbit.com.br/rest/v3/#tag/Trading/operation/OrdersController_cancel + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + request: dict = { + 'id': self.parse_number(id), + 'type': 'ID', + } + response = await self.v3PrivatePutOrdersCancel(self.extend(request, params)) + # { + # "data": [ + # { + # "sn": "OKMAKSDHRVVREK", + # "id": 123456789 + # } + # ] + # } + data = self.safe_list(response, 'data', []) + result = self.safe_dict(data, 0, {}) + return self.parse_order(result) + + async def cancel_all_orders(self, symbol: Str = None, params={}): + """ + Cancel all open orders or all open orders for a specific market. + + https://docs.foxbit.com.br/rest/v3/#tag/Trading/operation/OrdersController_cancel + + :param str symbol: unified market symbol of the market to cancel orders in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + request: dict = { + 'type': 'ALL', + } + if symbol is not None: + market = self.market(symbol) + request['type'] = 'MARKET' + request['market_symbol'] = market['id'] + response = await self.v3PrivatePutOrdersCancel(self.extend(request, params)) + # { + # "data": [ + # { + # "sn": "OKMAKSDHRVVREK", + # "id": 123456789 + # } + # ] + # } + return [self.safe_order({ + 'info': response, + })] + + async def fetch_order(self, id: str, symbol: Str = None, params={}) -> Order: + """ + Get an order by ID. + + https://docs.foxbit.com.br/rest/v3/#tag/Trading/operation/OrdersController_findByOrderId + + @param id + :param str symbol: it is not used in the foxbit API + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + request: dict = { + 'id': id, + } + response = await self.v3PrivateGetOrdersByOrderIdId(self.extend(request, params)) + # { + # "id": "1234567890", + # "sn": "OKMAKSDHRVVREK", + # "client_order_id": "451637946501", + # "market_symbol": "btcbrl", + # "side": "BUY", + # "type": "LIMIT", + # "state": "ACTIVE", + # "price": "290000.0", + # "price_avg": "295333.3333", + # "quantity": "0.42", + # "quantity_executed": "0.41", + # "instant_amount": "290.0", + # "instant_amount_executed": "290.0", + # "created_at": "2021-02-15T22:06:32.999Z", + # "trades_count": "2", + # "remark": "A remarkable note for the order.", + # "funds_received": "290.0" + # } + return self.parse_order(response, None) + + async def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://docs.foxbit.com.br/rest/v3/#tag/Trading/operation/OrdersController_listOrders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.state]: Enum: ACTIVE, CANCELED, FILLED, PARTIALLY_CANCELED, PARTIALLY_FILLED + :param str [params.side]: Enum: BUY, SELL + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + market = None + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['market_symbol'] = market['id'] + if since is not None: + request['start_time'] = self.iso8601(since) + if limit is not None: + request['page_size'] = limit + if limit > 100: + request['page_size'] = 100 + response = await self.v3PrivateGetOrders(self.extend(request, params)) + # { + # "data": [ + # { + # "id": "1234567890", + # "sn": "OKMAKSDHRVVREK", + # "client_order_id": "451637946501", + # "market_symbol": "btcbrl", + # "side": "BUY", + # "type": "LIMIT", + # "state": "ACTIVE", + # "price": "290000.0", + # "price_avg": "295333.3333", + # "quantity": "0.42", + # "quantity_executed": "0.41", + # "instant_amount": "290.0", + # "instant_amount_executed": "290.0", + # "created_at": "2021-02-15T22:06:32.999Z", + # "trades_count": "2", + # "remark": "A remarkable note for the order.", + # "funds_received": "290.0" + # } + # ] + # } + list = self.safe_list(response, 'data', []) + return self.parse_orders(list, market, since, limit) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + Trade history queries will only have data available for the last 3 months, in descending order(most recents trades first). + + https://docs.foxbit.com.br/rest/v3/#tag/Trading/operation/TradesController_all + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trade structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMyTrades() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request = { + 'market_symbol': market['id'], + } + if since is not None: + request['start_time'] = self.iso8601(since) + if limit is not None: + request['page_size'] = limit + if limit > 100: + request['page_size'] = 100 + response = await self.v3PrivateGetTrades(self.extend(request, params)) + # { + # "data": [ + # "id": 1234567890, + # "sn": "TC5JZVW2LLJ3IW", + # "order_id": 1234567890, + # "market_symbol": "btcbrl", + # "side": "BUY", + # "price": "290000.0", + # "quantity": "1.0", + # "fee": "0.01", + # "fee_currency_symbol": "btc", + # "created_at": "2021-02-15T22:06:32.999Z" + # ] + # } + data = self.safe_list(response, 'data', []) + return self.parse_trades(data, market, since, limit) + + async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + Fetch the deposit address for a currency associated with self account. + + https://docs.foxbit.com.br/rest/v3/#tag/Deposit/operation/DepositsController_depositAddress + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.networkCode]: the blockchain network to create a deposit address on + :returns dict: an `address structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency_symbol': currency['id'], + } + networkCode, paramsOmited = self.handle_network_code_and_params(params) + if networkCode is not None: + request['network_code'] = self.network_code_to_id(networkCode, code) + response = await self.v3PrivateGetDepositsAddress(self.extend(request, paramsOmited)) + # { + # "currency_symbol": "btc", + # "address": "2N9sS8LgrY19rvcCWDmE1ou1tTVmqk4KQAB", + # "message": "Address was retrieved successfully", + # "destination_tag": "string", + # "network": { + # "name": "Bitcoin Network", + # "code": "btc" + # } + # } + return self.parse_deposit_address(response, currency) + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + Fetch all deposits made to an account. + + https://docs.foxbit.com.br/rest/v3/#tag/Deposit/operation/DepositsController_listOrders + + :param str [code]: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposit structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + request: dict = {} + currency = None + if code is not None: + currency = self.currency(code) + if limit is not None: + request['page_size'] = limit + if limit > 100: + request['page_size'] = 100 + if since is not None: + request['start_time'] = self.iso8601(since) + response = await self.v3PrivateGetDeposits(self.extend(request, params)) + # { + # "data": [ + # { + # "sn": "OKMAKSDHRVVREK", + # "state": "ACCEPTED", + # "currency_symbol": "btc", + # "amount": "1.0", + # "fee": "0.1", + # "created_at": "2022-02-18T22:06:32.999Z", + # "details_crypto": { + # "transaction_id": "e20f035387020c5d5ea18ad53244f09f3", + # "receiving_address": "2N2rTrnKEFcyJjEJqvVjgWZ3bKvKT7Aij61" + # } + # } + # ] + # } + data = self.safe_list(response, 'data', []) + return self.parse_transactions(data, currency, since, limit) + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + Fetch all withdrawals made from an account. + + https://docs.foxbit.com.br/rest/v3/#tag/Withdrawal/operation/WithdrawalsController_listWithdrawals + + :param str [code]: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawal structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + request: dict = {} + currency = None + if code is not None: + currency = self.currency(code) + if limit is not None: + request['page_size'] = limit + if limit > 100: + request['page_size'] = 100 + if since is not None: + request['start_time'] = self.iso8601(since) + response = await self.v3PrivateGetWithdrawals(self.extend(request, params)) + # { + # "data": [ + # { + # "sn": "OKMAKSDHRVVREK", + # "state": "ACCEPTED", + # "rejection_reason": "monthly_limit_exceeded", + # "currency_symbol": "btc", + # "amount": "1.0", + # "fee": "0.1", + # "created_at": "2022-02-18T22:06:32.999Z", + # "details_crypto": { + # "transaction_id": "e20f035387020c5d5ea18ad53244f09f3", + # "destination_address": "2N2rTrnKEFcyJjEJqvVjgWZ3bKvKT7Aij61" + # }, + # "details_fiat": { + # "bank": { + # "code": "1", + # "branch": { + # "number": "1234567890", + # "digit": "1" + # }, + # "account": { + # "number": "1234567890", + # "digit": "1", + # "type": "CHECK" + # } + # } + # } + # } + # ] + # } + data = self.safe_list(response, 'data', []) + return self.parse_transactions(data, currency, since, limit) + + async def fetch_transactions(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + Fetch all transactions(deposits and withdrawals) made from an account. + + https://docs.foxbit.com.br/rest/v3/#tag/Withdrawal/operation/WithdrawalsController_listWithdrawals + https://docs.foxbit.com.br/rest/v3/#tag/Deposit/operation/DepositsController_listOrders + + :param str [code]: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawal structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + withdrawals = await self.fetch_withdrawals(code, since, limit, params) + deposits = await self.fetch_deposits(code, since, limit, params) + allTransactions = self.array_concat(withdrawals, deposits) + result = self.sort_by(allTransactions, 'timestamp') + return result + + async def fetch_status(self, params={}): + """ + The latest known information on the availability of the exchange API. + + https://status.foxbit.com/ + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `status structure ` + """ + response = await self.statusPublicGetStatus(params) + # { + # "data": { + # "id": 1, + # "attributes": { + # "status": "NORMAL", + # "createdAt": "2023-05-17T18:37:05.934Z", + # "updatedAt": "2024-04-17T02:33:50.945Z", + # "publishedAt": "2023-05-17T18:37:07.653Z", + # "locale": "pt-BR" + # } + # }, + # "meta": { + # } + # } + data = self.safe_dict(response, 'data', {}) + attributes = self.safe_dict(data, 'attributes', {}) + statusRaw = self.safe_string(attributes, 'status') + statusMap = { + 'NORMAL': 'ok', + 'UNDER_MAINTENANCE': 'maintenance', + } + return { + 'status': self.safe_string(statusMap, statusRaw, statusRaw), + 'updated': self.safe_string(attributes, 'updatedAt'), + 'eta': None, + 'url': None, + 'info': response, + } + + async def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}) -> Order: + """ + Simultaneously cancel an existing order and create a new one. + + https://docs.foxbit.com.br/rest/v3/#tag/Trading/operation/OrdersController_cancelReplace + + :param str id: order id + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of the currency you want to trade in units of the base currency + :param float [price]: the price at which the order is to be fullfilled, in units of the quote currency, ignored in market orders, used on stop market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' editOrder() requires a symbol argument') + type = type.upper() + if type != 'LIMIT' and type != 'MARKET' and type != 'STOP_MARKET' and type != 'INSTANT': + raise InvalidOrder('Invalid order type: ' + type + '. Must be one of: LIMIT, MARKET, STOP_MARKET, INSTANT.') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'mode': 'ALLOW_FAILURE', + 'cancel': { + 'type': 'ID', + 'id': self.parse_number(id), + }, + 'create': { + 'type': type, + 'side': side.upper(), + 'market_symbol': market['id'], + }, + } + if type == 'LIMIT' or type == 'MARKET': + request['create']['quantity'] = self.amount_to_precision(symbol, amount) + if type == 'LIMIT': + request['create']['price'] = self.price_to_precision(symbol, price) + if type == 'STOP_MARKET': + request['create']['stop_price'] = self.price_to_precision(symbol, price) + request['create']['quantity'] = self.amount_to_precision(symbol, amount) + if type == 'INSTANT': + request['create']['amount'] = self.price_to_precision(symbol, amount) + response = await self.v3PrivatePostOrdersCancelReplace(self.extend(request, params)) + # { + # "cancel": { + # "id": 123456789 + # }, + # "create": { + # "id": 1234567890, + # "client_order_id": "451637946501" + # } + # } + return self.parse_order(response['create'], market) + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + Make a withdrawal. + + https://docs.foxbit.com.br/rest/v3/#tag/Withdrawal/operation/WithdrawalsController_createWithdrawal + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency_symbol': currency['id'], + 'amount': self.number_to_string(amount), + 'destination_address': address, + } + if tag is not None: + request['destination_tag'] = tag + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + if networkCode is not None: + request['network_code'] = self.network_code_to_id(networkCode) + response = await self.v3PrivatePostWithdrawals(self.extend(request, params)) + # { + # "amount": "1", + # "currency_symbol": "xrp", + # "network_code": "ripple", + # "destination_address": "0x1234567890123456789012345678", + # "destination_tag": "123456" + # } + return self.parse_transaction(response) + + async def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch the history of changes, actions done by the user or operations that altered balance of the user + + https://docs.foxbit.com.br/rest/v3/#tag/Account/operation/AccountsController_getTransactions + + :param str code: unified currency code, default is None + :param int [since]: timestamp in ms of the earliest ledger entry, default is None + :param int [limit]: max number of ledger entrys to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ledger structure ` + """ + await self.load_markets() + request: dict = {} + if code is None: + raise ArgumentsRequired(self.id + ' fetchLedger() requires a code argument') + if limit is not None: + request['page_size'] = limit + if limit > 100: + request['page_size'] = 100 + if since is not None: + request['start_time'] = self.iso8601(since) + currency = self.currency(code) + request['symbol'] = currency['id'] + response = await self.v3PrivateGetAccountsSymbolTransactions(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + return self.parse_ledger(data, currency, since, limit) + + def parse_market(self, market: dict) -> Market: + id = self.safe_string(market, 'symbol') + baseAssets = self.safe_dict(market, 'base') + baseId = self.safe_string(baseAssets, 'symbol') + quoteAssets = self.safe_dict(market, 'quote') + quoteId = self.safe_string(quoteAssets, 'symbol') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + symbol = base + '/' + quote + fees = self.safe_dict(market, 'default_fees') + return self.safe_market_structure({ + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'baseId': baseId, + 'quoteId': quoteId, + 'active': True, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'future': False, + 'swap': False, + 'option': False, + 'contract': False, + 'settle': None, + 'settleId': None, + 'contractSize': None, + 'linear': None, + 'inverse': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'taker': self.safe_number(fees, 'taker'), + 'maker': self.safe_number(fees, 'maker'), + 'percentage': True, + 'tierBased': False, + 'feeSide': 'get', + 'precision': { + 'price': self.safe_integer(quoteAssets, 'precision'), + 'amount': self.safe_integer(baseAssets, 'precision'), + 'cost': self.safe_integer(quoteAssets, 'precision'), + }, + 'limits': { + 'amount': { + 'min': self.safe_number(market, 'quantity_min'), + 'max': None, + }, + 'price': { + 'min': self.safe_number(market, 'price_min'), + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + 'leverage': { + 'min': None, + 'max': None, + }, + }, + 'info': market, + }) + + def parse_trading_fee(self, entry: dict, market: Market = None) -> TradingFeeInterface: + return { + 'info': entry, + 'symbol': market['symbol'], + 'maker': self.safe_number(entry, 'maker'), + 'taker': self.safe_number(entry, 'taker'), + 'percentage': True, + 'tierBased': True, + } + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + marketId = self.safe_string(ticker, 'market_symbol') + symbol = self.safe_symbol(marketId, market, None, 'spot') + rolling_24h = ticker['rolling_24h'] + best = self.safe_dict(ticker, 'best') + bestAsk = self.safe_dict(best, 'ask') + bestBid = self.safe_dict(best, 'bid') + lastTrade = ticker['last_trade'] + lastPrice = self.safe_string(lastTrade, 'price') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': self.parse_date(self.safe_string(lastTrade, 'date')), + 'datetime': self.iso8601(self.parse_date(self.safe_string(lastTrade, 'date'))), + 'high': self.safe_number(rolling_24h, 'high'), + 'low': self.safe_number(rolling_24h, 'low'), + 'bid': self.safe_number(bestBid, 'price'), + 'bidVolume': self.safe_number(bestBid, 'volume'), + 'ask': self.safe_number(bestAsk, 'price'), + 'askVolume': self.safe_number(bestAsk, 'volume'), + 'vwap': None, + 'open': self.safe_number(rolling_24h, 'open'), + 'close': lastPrice, + 'last': lastPrice, + 'previousClose': None, + 'change': self.safe_string(rolling_24h, 'price_change'), + 'percentage': self.safe_string(rolling_24h, 'price_change_percent'), + 'average': None, + 'baseVolume': self.safe_string(rolling_24h, 'volume'), + 'quoteVolume': self.safe_string(rolling_24h, 'quote_volume'), + 'info': ticker, + }, market) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + return [ + self.safe_integer(ohlcv, 0), + self.safe_number(ohlcv, 1), + self.safe_number(ohlcv, 2), + self.safe_number(ohlcv, 3), + self.safe_number(ohlcv, 4), + self.safe_number(ohlcv, 6), + ] + + def parse_trade(self, trade, market=None) -> Trade: + timestamp = self.parse_date(self.safe_string(trade, 'created_at')) + price = self.safe_string(trade, 'price') + amount = self.safe_string(trade, 'volume', self.safe_string(trade, 'quantity')) + privateSideField = self.safe_string_lower(trade, 'side') + side = self.safe_string_lower(trade, 'taker_side', privateSideField) + cost = Precise.string_mul(price, amount) + fee = { + 'currency': self.safe_symbol(self.safe_string(trade, 'fee_currency_symbol')), + 'cost': self.safe_number(trade, 'fee'), + 'rate': None, + } + return self.safe_trade({ + 'id': self.safe_string(trade, 'id'), + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'order': None, + 'type': None, + 'side': side, + 'takerOrMaker': None, + 'price': price, + 'amount': amount, + 'cost': cost, + 'fee': fee, + }, market) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'PARTIALLY_CANCELED': 'open', + 'ACTIVE': 'open', + 'PARTIALLY_FILLED': 'open', + 'FILLED': 'closed', + 'PENDING_CANCEL': 'canceled', + 'CANCELED': 'canceled', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order, market=None) -> Order: + symbol = self.safe_string(order, 'market_symbol') + if market is None and symbol is not None: + market = self.market(symbol) + if market is not None: + symbol = market['symbol'] + timestamp = self.parse_date(self.safe_string(order, 'created_at')) + price = self.safe_string(order, 'price') + filled = self.safe_string(order, 'quantity_executed') + remaining = self.safe_string(order, 'quantity') + # TODO: validate logic of amount here, should self be calculated? + amount = None + if remaining is not None and filled is not None: + amount = Precise.string_add(remaining, filled) + cost = self.safe_string(order, 'funds_received') + if not cost: + priceAverage = self.safe_string(order, 'price_avg') + priceToCalculate = self.safe_string(order, 'price', priceAverage) + cost = Precise.string_mul(priceToCalculate, amount) + side = self.safe_string_lower(order, 'side') + feeCurrency = self.safe_string_upper(market, 'quoteId') + if side == 'buy': + feeCurrency = self.safe_string_upper(market, 'baseId') + return self.safe_order({ + 'id': self.safe_string(order, 'id'), + 'info': order, + 'clientOrderId': self.safe_string(order, 'client_order_id'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'status': self.parse_order_status(self.safe_string(order, 'state')), + 'symbol': self.safe_string(market, 'symbol'), + 'type': self.safe_string(order, 'type'), + 'timeInForce': self.safe_string(order, 'time_in_force'), + 'postOnly': self.safe_bool(order, 'post_only'), + 'reduceOnly': None, + 'side': side, + 'price': self.parse_number(price), + 'triggerPrice': self.safe_number(order, 'stop_price'), + 'takeProfitPrice': None, + 'stopLossPrice': None, + 'cost': self.parse_number(cost), + 'average': self.safe_number(order, 'price_avg'), + 'amount': self.parse_number(amount), + 'filled': self.parse_number(filled), + 'remaining': self.parse_number(remaining), + 'trades': None, + 'fee': { + 'currency': feeCurrency, + 'cost': self.safe_number(order, 'fee_paid'), + }, + }) + + def parse_deposit_address(self, depositAddress, currency: Currency = None): + network = self.safe_dict(depositAddress, 'network') + networkId = self.safe_string(network, 'code') + currencyCode = self.safe_currency_code(None, currency) + unifiedNetwork = self.network_id_to_code(networkId, currencyCode) + return { + 'address': self.safe_string(depositAddress, 'address'), + 'tag': self.safe_string(depositAddress, 'tag'), + 'currency': currencyCode, + 'network': unifiedNetwork, + 'info': depositAddress, + } + + def parse_transaction_status(self, status: Str): + statuses: dict = { + # BOTH + 'SUBMITTING': 'pending', + 'SUBMITTED': 'pending', + 'REJECTED': 'failed', + # DEPOSIT-SPECIFIC + 'CANCELLED': 'canceled', + 'ACCEPTED': 'ok', + 'WARNING': 'pending', + 'UNBLOCKED': 'pending', + 'BLOCKED': 'pending', + # WITHDRAWAL-SPECIFIC + 'PROCESSING': 'pending', + 'CANCELED': 'canceled', + 'FAILED': 'failed', + 'DONE': 'ok', + } + return self.safe_string(statuses, status, status) + + def parse_transaction(self, transaction, currency: Currency = None, since: Int = None, limit: Int = None) -> Transaction: + cryptoDetails = self.safe_dict(transaction, 'details_crypto') + address = self.safe_string_2(cryptoDetails, 'receiving_address', 'destination_address') + sn = self.safe_string(transaction, 'sn') + type = 'withdrawal' + if sn is not None and sn[0] == 'D': + type = 'deposit' + fee = self.safe_string(transaction, 'fee', '0') + amount = self.safe_string(transaction, 'amount') + currencySymbol = self.safe_string(transaction, 'currency_symbol') + actualAmount = amount + currencyCode = self.safe_currency_code(currencySymbol) + status = self.parse_transaction_status(self.safe_string(transaction, 'state')) + created_at = self.safe_string(transaction, 'created_at') + timestamp = self.parse_date(created_at) + datetime = self.iso8601(timestamp) + if fee is not None and amount is not None: + # actualAmount = amount - fee + actualAmount = Precise.string_sub(amount, fee) + feeRate = Precise.string_div(fee, actualAmount) + feeObj = { + 'cost': self.parse_number(fee), + 'currency': currencyCode, + 'rate': self.parse_number(feeRate), + } + return { + 'info': transaction, + 'id': self.safe_string(transaction, 'sn'), + 'txid': self.safe_string(cryptoDetails, 'transaction_id'), + 'timestamp': timestamp, + 'datetime': datetime, + 'network': self.safe_string(transaction, 'network_code'), + 'address': address, + 'addressTo': address, + 'addressFrom': None, + 'tag': self.safe_string(transaction, 'destination_tag'), + 'tagTo': self.safe_string(transaction, 'destination_tag'), + 'tagFrom': None, + 'type': type, + 'amount': self.parse_number(amount), + 'currency': currencyCode, + 'status': status, + 'updated': None, + 'fee': feeObj, + 'comment': None, + 'internal': None, + } + + def parse_ledger_entry_type(self, type): + types: dict = { + 'DEPOSITING': 'transaction', + 'WITHDRAWING': 'transaction', + 'TRADING': 'trade', + 'INTERNAL_TRANSFERING': 'transfer', + 'OTHERS': 'transaction', + } + return self.safe_string(types, type, type) + + def parse_ledger_entry(self, item: dict, currency: Currency = None): + # { + # "uuid": "f8e9f2d6-3c1e-4f2d-8f8e-9f2d6c1e4f2d", + # "amount": "0.0001", + # "balance": "0.0002", + # "created_at": "2021-07-01T12:00:00Z", + # "currency_symbol": "btc", + # "fee": "0.0001", + # "locked": "0.0001", + # "locked_amount": "0.0001", + # "reason_type": "DEPOSITING" + # } + id = self.safe_string(item, 'uuid') + createdAt = self.safe_string(item, 'created_at') + timestamp = self.parse8601(createdAt) + reasonType = self.safe_string(item, 'reason_type') + type = self.parse_ledger_entry_type(reasonType) + exchangeSymbol = self.safe_string(item, 'currency_symbol') + currencySymbol = self.safe_currency_code(exchangeSymbol) + direction = 'in' + amount = self.safe_number(item, 'amount') + realAmount = amount + balance = self.safe_number(item, 'balance') + fee = { + 'cost': self.safe_number(item, 'fee'), + 'currency': currencySymbol, + } + if amount < 0: + direction = 'out' + realAmount = amount * -1 + return { + 'id': id, + 'info': item, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'direction': direction, + 'account': None, + 'referenceId': None, + 'referenceAccount': None, + 'type': type, + 'currency': currencySymbol, + 'amount': realAmount, + 'before': balance - amount, + 'after': balance, + 'status': 'ok', + 'fee': fee, + } + + def sign(self, path, api=[], method='GET', params={}, headers=None, body=None): + version = api[0] + urlPath = api[1] + fullPath = '/rest/' + version + '/' + self.implode_params(path, params) + if version == 'status': + fullPath = '/status' + urlPath = 'status' + url = self.urls['api'][urlPath] + fullPath + params = self.omit(params, self.extract_params(path)) + timestamp = self.milliseconds() + query = '' + signatureQuery = '' + if method == 'GET': + paramKeys = list(params.keys()) + paramKeysLength = len(paramKeys) + if paramKeysLength > 0: + query = self.urlencode(params) + url += '?' + query + for i in range(0, len(paramKeys)): + key = paramKeys[i] + value = self.safe_string(params, key) + if value is not None: + signatureQuery += key + '=' + value + if i < paramKeysLength - 1: + signatureQuery += '&' + if method == 'POST' or method == 'PUT': + body = self.json(params) + bodyToSignature = '' + if body is not None: + bodyToSignature = body + headers = { + 'Content-Type': 'application/json', + } + if urlPath == 'private': + self.check_required_credentials() + preHash = self.number_to_string(timestamp) + method + fullPath + signatureQuery + bodyToSignature + signature = self.hmac(self.encode(preHash), self.encode(self.secret), hashlib.sha256, 'hex') + headers['X-FB-ACCESS-KEY'] = self.apiKey + headers['X-FB-ACCESS-TIMESTAMP'] = self.number_to_string(timestamp) + headers['X-FB-ACCESS-SIGNATURE'] = signature + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None + error = self.safe_dict(response, 'error') + code = self.safe_string(error, 'code') + details = self.safe_list(error, 'details') + message = self.safe_string(error, 'message') + detailsString = '' + if details: + for i in range(0, len(details)): + detailsString = detailsString + details[i] + ' ' + if error is not None: + feedback = self.id + ' ' + message + ' details: ' + detailsString + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], detailsString, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], code, feedback) + raise ExchangeError(feedback) + return None diff --git a/ccxt/async_support/gate.py b/ccxt/async_support/gate.py new file mode 100644 index 0000000..3cfa288 --- /dev/null +++ b/ccxt/async_support/gate.py @@ -0,0 +1,7869 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.gate import ImplicitAPI +import asyncio +import hashlib +from ccxt.base.types import Any, Balances, BorrowInterest, Bool, Currencies, Currency, DepositAddress, FundingHistory, Greeks, Int, LedgerEntry, Leverage, Leverages, LeverageTier, LeverageTiers, MarginModification, Market, Num, Option, OptionChain, Order, OrderBook, OrderRequest, CancellationRequest, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, FundingRates, Trade, TradingFeeInterface, TradingFees, Transaction, MarketInterface, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import AccountNotEnabled +from ccxt.base.errors import AccountSuspended +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import OrderImmediatelyFillable +from ccxt.base.errors import NotSupported +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class gate(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(gate, self).describe(), { + 'id': 'gate', + 'name': 'Gate', + 'countries': ['KR'], + 'rateLimit': 50, # 200 requests per 10 second or 50ms + 'version': 'v4', + 'certified': True, + 'pro': True, + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/64f988c5-07b6-4652-b5c1-679a6bf67c85', + 'doc': 'https://www.gate.com/docs/developers/apiv4/en/', + 'www': 'https://gate.com', + 'api': { + 'public': { + 'wallet': 'https://api.gateio.ws/api/v4', + 'futures': 'https://api.gateio.ws/api/v4', + 'margin': 'https://api.gateio.ws/api/v4', + 'delivery': 'https://api.gateio.ws/api/v4', + 'spot': 'https://api.gateio.ws/api/v4', + 'options': 'https://api.gateio.ws/api/v4', + 'sub_accounts': 'https://api.gateio.ws/api/v4', + 'earn': 'https://api.gateio.ws/api/v4', + }, + 'private': { + 'withdrawals': 'https://api.gateio.ws/api/v4', + 'wallet': 'https://api.gateio.ws/api/v4', + 'futures': 'https://api.gateio.ws/api/v4', + 'margin': 'https://api.gateio.ws/api/v4', + 'delivery': 'https://api.gateio.ws/api/v4', + 'spot': 'https://api.gateio.ws/api/v4', + 'options': 'https://api.gateio.ws/api/v4', + 'subAccounts': 'https://api.gateio.ws/api/v4', + 'unified': 'https://api.gateio.ws/api/v4', + 'rebate': 'https://api.gateio.ws/api/v4', + 'earn': 'https://api.gateio.ws/api/v4', + 'account': 'https://api.gateio.ws/api/v4', + 'loan': 'https://api.gateio.ws/api/v4', + }, + }, + 'test': { + 'public': { + 'futures': 'https://api-testnet.gateapi.io/api/v4', + 'delivery': 'https://api-testnet.gateapi.io/api/v4', + 'options': 'https://api-testnet.gateapi.io/api/v4', + 'spot': 'https://api-testnet.gateapi.io/api/v4', + 'wallet': 'https://api-testnet.gateapi.io/api/v4', + 'margin': 'https://api-testnet.gateapi.io/api/v4', + 'sub_accounts': 'https://api-testnet.gateapi.io/api/v4', + 'account': 'https://api-testnet.gateapi.io/api/v4', + }, + 'private': { + 'futures': 'https://api-testnet.gateapi.io/api/v4', + 'delivery': 'https://api-testnet.gateapi.io/api/v4', + 'options': 'https://api-testnet.gateapi.io/api/v4', + 'spot': 'https://api-testnet.gateapi.io/api/v4', + 'wallet': 'https://api-testnet.gateapi.io/api/v4', + 'margin': 'https://api-testnet.gateapi.io/api/v4', + 'sub_accounts': 'https://api-testnet.gateapi.io/api/v4', + 'account': 'https://api-testnet.gateapi.io/api/v4', + }, + }, + 'referral': { + 'url': 'https://www.gate.com/share/CCXTGATE', + 'discount': 0.2, + }, + }, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': True, + 'swap': True, + 'future': True, + 'option': True, + 'addMargin': True, + 'borrowCrossMargin': True, + 'borrowIsolatedMargin': True, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'cancelOrders': True, + 'cancelOrdersForSymbols': True, + 'createMarketBuyOrderWithCost': True, + 'createMarketOrder': True, + 'createMarketOrderWithCost': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createOrders': True, + 'createPostOnlyOrder': True, + 'createReduceOnlyOrder': True, + 'createStopLimitOrder': True, + 'createStopLossOrder': True, + 'createStopMarketOrder': False, + 'createStopOrder': True, + 'createTakeProfitOrder': True, + 'createTriggerOrder': True, + 'editOrder': True, + 'fetchBalance': True, + 'fetchBorrowInterest': True, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': True, + 'fetchDeposits': True, + 'fetchDepositWithdrawFee': 'emulated', + 'fetchDepositWithdrawFees': True, + 'fetchFundingHistory': True, + 'fetchFundingRate': True, + 'fetchFundingRateHistory': True, + 'fetchFundingRates': True, + 'fetchGreeks': True, + 'fetchIndexOHLCV': True, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchLedger': True, + 'fetchLeverage': True, + 'fetchLeverages': True, + 'fetchLeverageTiers': True, + 'fetchLiquidations': True, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarketLeverageTiers': True, + 'fetchMarkets': True, + 'fetchMarkOHLCV': True, + 'fetchMyLiquidations': True, + 'fetchMySettlementHistory': True, + 'fetchMyTrades': True, + 'fetchNetworkDepositAddress': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': True, + 'fetchOpenOrders': True, + 'fetchOption': True, + 'fetchOptionChain': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchPosition': True, + 'fetchPositionHistory': 'emulated', + 'fetchPositionMode': False, + 'fetchPositions': True, + 'fetchPositionsHistory': True, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': True, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': True, + 'fetchTradingFees': True, + 'fetchTransactionFees': True, + 'fetchUnderlyingAssets': True, + 'fetchVolatilityHistory': False, + 'fetchWithdrawals': True, + 'reduceMargin': True, + 'repayCrossMargin': True, + 'repayIsolatedMargin': True, + 'sandbox': True, + 'setLeverage': True, + 'setMarginMode': False, + 'setPositionMode': True, + 'signIn': False, + 'transfer': True, + 'withdraw': True, + }, + 'api': { + 'public': { + # All public endpoints 200r/10s per endpoint + 'wallet': { + 'get': { + 'currency_chains': 1, + }, + }, + 'unified': { + 'get': { + 'currencies': 1, + 'history_loan_rate': 1, + }, + }, + 'spot': { + 'get': { + 'currencies': 1, + 'currencies/{currency}': 1, + 'currency_pairs': 1, + 'currency_pairs/{currency_pair}': 1, + 'tickers': 1, + 'order_book': 1, + 'trades': 1, + 'candlesticks': 1, + 'time': 1, + 'insurance_history': 1, + }, + }, + 'margin': { + 'get': { + 'uni/currency_pairs': 1, + 'uni/currency_pairs/{currency_pair}': 1, + 'loan_margin_tiers': 1, + 'currency_pairs': 1, # deprecated + 'currency_pairs/{currency_pair}': 1, # deprecated + 'funding_book': 1, # deprecated + 'cross/currencies': 1, # deprecated + 'cross/currencies/{currency}': 1, # deprecated + }, + }, + 'flash_swap': { + 'get': { + 'currency_pairs': 1, + 'currencies': 1, # deprecated + }, + }, + 'futures': { + 'get': { + '{settle}/contracts': 1, + '{settle}/contracts/{contract}': 1, + '{settle}/order_book': 1, + '{settle}/trades': 1, + '{settle}/candlesticks': 1, + '{settle}/premium_index': 1, + '{settle}/tickers': 1, + '{settle}/funding_rate': 1, + '{settle}/insurance': 1, + '{settle}/contract_stats': 1, + '{settle}/index_constituents/{index}': 1, + '{settle}/liq_orders': 1, + '{settle}/risk_limit_tiers': 1, + }, + }, + 'delivery': { + 'get': { + '{settle}/contracts': 1, + '{settle}/contracts/{contract}': 1, + '{settle}/order_book': 1, + '{settle}/trades': 1, + '{settle}/candlesticks': 1, + '{settle}/tickers': 1, + '{settle}/insurance': 1, + '{settle}/risk_limit_tiers': 1, + }, + }, + 'options': { + 'get': { + 'underlyings': 1, + 'expirations': 1, + 'contracts': 1, + 'contracts/{contract}': 1, + 'settlements': 1, + 'settlements/{contract}': 1, + 'order_book': 1, + 'tickers': 1, + 'underlying/tickers/{underlying}': 1, + 'candlesticks': 1, + 'underlying/candlesticks': 1, + 'trades': 1, + }, + }, + 'earn': { + 'get': { + 'uni/currencies': 1, + 'uni/currencies/{currency}': 1, + 'dual/investment_plan': 1, + 'structured/products': 1, + }, + }, + 'loan': { + 'get': { + 'collateral/currencies': 1, + 'multi_collateral/currencies': 1, + 'multi_collateral/ltv': 1, + 'multi_collateral/fixed_rate': 1, + 'multi_collateral/current_rate': 1, + }, + }, + }, + 'private': { + # private endpoints default is 150r/10s per endpoint + 'withdrawals': { + 'post': { + 'withdrawals': 20, # 1r/s cost = 20 / 1 = 20 + 'push': 1, + }, + 'delete': { + 'withdrawals/{withdrawal_id}': 1, + }, + }, + 'wallet': { + 'get': { + 'deposit_address': 1, + 'withdrawals': 1, + 'deposits': 1, + 'sub_account_transfers': 1, + 'order_status': 1, + 'withdraw_status': 1, + 'sub_account_balances': 2.5, + 'sub_account_margin_balances': 2.5, + 'sub_account_futures_balances': 2.5, + 'sub_account_cross_margin_balances': 2.5, + 'saved_address': 1, + 'fee': 1, + 'total_balance': 2.5, + 'small_balance': 1, + 'small_balance_history': 1, + 'push': 1, + }, + 'post': { + 'transfers': 2.5, # 8r/s cost = 20 / 8 = 2.5 + 'sub_account_transfers': 2.5, + 'sub_account_to_sub_account': 2.5, + 'small_balance': 1, + }, + }, + 'subAccounts': { + 'get': { + 'sub_accounts': 2.5, + 'sub_accounts/{user_id}': 2.5, + 'sub_accounts/{user_id}/keys': 2.5, + 'sub_accounts/{user_id}/keys/{key}': 2.5, + }, + 'post': { + 'sub_accounts': 2.5, + 'sub_accounts/{user_id}/keys': 2.5, + 'sub_accounts/{user_id}/lock': 2.5, + 'sub_accounts/{user_id}/unlock': 2.5, + }, + 'put': { + 'sub_accounts/{user_id}/keys/{key}': 2.5, + }, + 'delete': { + 'sub_accounts/{user_id}/keys/{key}': 2.5, + }, + }, + 'unified': { + 'get': { + 'accounts': 20 / 15, + 'borrowable': 20 / 15, + 'transferable': 20 / 15, + 'transferables': 20 / 15, + 'batch_borrowable': 20 / 15, + 'loans': 20 / 15, + 'loan_records': 20 / 15, + 'interest_records': 20 / 15, + 'risk_units': 20 / 15, + 'unified_mode': 20 / 15, + 'estimate_rate': 20 / 15, + 'currency_discount_tiers': 20 / 15, + 'loan_margin_tiers': 20 / 15, + 'leverage/user_currency_config': 20 / 15, + 'leverage/user_currency_setting': 20 / 15, + 'account_mode': 20 / 15, # deprecated + }, + 'post': { + 'loans': 200 / 15, # 15r/10s cost = 20 / 1.5 = 13.33 + 'portfolio_calculator': 20 / 15, + 'leverage/user_currency_setting': 20 / 15, + 'collateral_currencies': 20 / 15, + 'account_mode': 20 / 15, # deprecated + }, + 'put': { + 'unified_mode': 20 / 15, + }, + }, + 'spot': { + # default is 200r/10s + 'get': { + 'fee': 1, + 'batch_fee': 1, + 'accounts': 1, + 'account_book': 1, + 'open_orders': 1, + 'orders': 1, + 'orders/{order_id}': 1, + 'my_trades': 1, + 'price_orders': 1, + 'price_orders/{order_id}': 1, + }, + 'post': { + 'batch_orders': 0.4, + 'cross_liquidate_orders': 1, + 'orders': 0.4, + 'cancel_batch_orders': 20 / 75, + 'countdown_cancel_all': 20 / 75, + 'amend_batch_orders': 0.4, + 'price_orders': 0.4, + }, + 'delete': { + 'orders': 20 / 75, + 'orders/{order_id}': 20 / 75, + 'price_orders': 20 / 75, + 'price_orders/{order_id}': 20 / 75, + }, + 'patch': { + 'orders/{order_id}': 0.4, + }, + }, + 'margin': { + 'get': { + 'accounts': 20 / 15, + 'account_book': 20 / 15, + 'funding_accounts': 20 / 15, + 'auto_repay': 20 / 15, + 'transferable': 20 / 15, + 'uni/estimate_rate': 20 / 15, + 'uni/loans': 20 / 15, + 'uni/loan_records': 20 / 15, + 'uni/interest_records': 20 / 15, + 'uni/borrowable': 20 / 15, + 'user/loan_margin_tiers': 20 / 15, + 'user/account': 20 / 15, + 'loans': 20 / 15, # deprecated + 'loans/{loan_id}': 20 / 15, # deprecated + 'loans/{loan_id}/repayment': 20 / 15, # deprecated + 'loan_records': 20 / 15, # deprecated + 'loan_records/{loan_record_id}': 20 / 15, # deprecated + 'borrowable': 20 / 15, # deprecated + 'cross/accounts': 20 / 15, # deprecated + 'cross/account_book': 20 / 15, # deprecated + 'cross/loans': 20 / 15, # deprecated + 'cross/loans/{loan_id}': 20 / 15, # deprecated + 'cross/repayments': 20 / 15, # deprecated + 'cross/interest_records': 20 / 15, # deprecated + 'cross/transferable': 20 / 15, # deprecated + 'cross/estimate_rate': 20 / 15, # deprecated + 'cross/borrowable': 20 / 15, # deprecated + }, + 'post': { + 'auto_repay': 20 / 15, + 'uni/loans': 20 / 15, + 'leverage/user_market_setting': 20 / 15, + 'loans': 20 / 15, # deprecated + 'merged_loans': 20 / 15, # deprecated + 'loans/{loan_id}/repayment': 20 / 15, # deprecated + 'cross/loans': 20 / 15, # deprecated + 'cross/repayments': 20 / 15, # deprecated + }, + 'patch': { + 'loans/{loan_id}': 20 / 15, # deprecated + 'loan_records/{loan_record_id}': 20 / 15, # deprecated + }, + 'delete': { + 'loans/{loan_id}': 20 / 15, # deprecated + }, + }, + 'flash_swap': { + 'get': { + 'orders': 1, + 'orders/{order_id}': 1, + }, + 'post': { + 'orders': 1, + 'orders/preview': 1, + }, + }, + 'futures': { + 'get': { + '{settle}/accounts': 1, + '{settle}/account_book': 1, + '{settle}/positions': 1, + '{settle}/positions/{contract}': 1, + '{settle}/dual_comp/positions/{contract}': 1, + '{settle}/orders': 1, + '{settle}/orders_timerange': 1, + '{settle}/orders/{order_id}': 1, + '{settle}/my_trades': 1, + '{settle}/my_trades_timerange': 1, + '{settle}/position_close': 1, + '{settle}/liquidates': 1, + '{settle}/auto_deleverages': 1, + '{settle}/fee': 1, + '{settle}/risk_limit_table': 1, + '{settle}/price_orders': 1, + '{settle}/price_orders/{order_id}': 1, + }, + 'post': { + '{settle}/positions/{contract}/margin': 1, + '{settle}/positions/{contract}/leverage': 1, + '{settle}/positions/{contract}/risk_limit': 1, + '{settle}/positions/cross_mode': 1, + '{settle}/dual_comp/positions/cross_mode': 1, + '{settle}/dual_mode': 1, + '{settle}/dual_comp/positions/{contract}/margin': 1, + '{settle}/dual_comp/positions/{contract}/leverage': 1, + '{settle}/dual_comp/positions/{contract}/risk_limit': 1, + '{settle}/orders': 0.4, + '{settle}/batch_orders': 0.4, + '{settle}/countdown_cancel_all': 0.4, + '{settle}/batch_cancel_orders': 0.4, + '{settle}/batch_amend_orders': 0.4, + '{settle}/bbo_orders': 0.4, + '{settle}/price_orders': 0.4, + }, + 'put': { + '{settle}/orders/{order_id}': 1, + }, + 'delete': { + '{settle}/orders': 20 / 75, + '{settle}/orders/{order_id}': 20 / 75, + '{settle}/price_orders': 20 / 75, + '{settle}/price_orders/{order_id}': 20 / 75, + }, + }, + 'delivery': { + 'get': { + '{settle}/accounts': 20 / 15, + '{settle}/account_book': 20 / 15, + '{settle}/positions': 20 / 15, + '{settle}/positions/{contract}': 20 / 15, + '{settle}/orders': 20 / 15, + '{settle}/orders/{order_id}': 20 / 15, + '{settle}/my_trades': 20 / 15, + '{settle}/position_close': 20 / 15, + '{settle}/liquidates': 20 / 15, + '{settle}/settlements': 20 / 15, + '{settle}/price_orders': 20 / 15, + '{settle}/price_orders/{order_id}': 20 / 15, + }, + 'post': { + '{settle}/positions/{contract}/margin': 20 / 15, + '{settle}/positions/{contract}/leverage': 20 / 15, + '{settle}/positions/{contract}/risk_limit': 20 / 15, + '{settle}/orders': 20 / 15, + '{settle}/price_orders': 20 / 15, + }, + 'delete': { + '{settle}/orders': 20 / 15, + '{settle}/orders/{order_id}': 20 / 15, + '{settle}/price_orders': 20 / 15, + '{settle}/price_orders/{order_id}': 20 / 15, + }, + }, + 'options': { + 'get': { + 'my_settlements': 20 / 15, + 'accounts': 20 / 15, + 'account_book': 20 / 15, + 'positions': 20 / 15, + 'positions/{contract}': 20 / 15, + 'position_close': 20 / 15, + 'orders': 20 / 15, + 'orders/{order_id}': 20 / 15, + 'my_trades': 20 / 15, + 'mmp': 20 / 15, + }, + 'post': { + 'orders': 20 / 15, + 'countdown_cancel_all': 20 / 15, + 'mmp': 20 / 15, + 'mmp/reset': 20 / 15, + }, + 'delete': { + 'orders': 20 / 15, + 'orders/{order_id}': 20 / 15, + }, + }, + 'earn': { + 'get': { + 'uni/lends': 20 / 15, + 'uni/lend_records': 20 / 15, + 'uni/interests/{currency}': 20 / 15, + 'uni/interest_records': 20 / 15, + 'uni/interest_status/{currency}': 20 / 15, + 'uni/chart': 20 / 15, + 'uni/rate': 20 / 15, + 'staking/eth2/rate_records': 20 / 15, + 'dual/orders': 20 / 15, + 'structured/orders': 20 / 15, + 'staking/coins': 20 / 15, + 'staking/order_list': 20 / 15, + 'staking/award_list': 20 / 15, + 'staking/assets': 20 / 15, + 'uni/currencies': 20 / 15, # deprecated + 'uni/currencies/{currency}': 20 / 15, # deprecated + }, + 'post': { + 'uni/lends': 20 / 15, + 'staking/eth2/swap': 20 / 15, + 'dual/orders': 20 / 15, + 'structured/orders': 20 / 15, + 'staking/swap': 20 / 15, + }, + 'put': { + 'uni/interest_reinvest': 20 / 15, # deprecated + }, + 'patch': { + 'uni/lends': 20 / 15, + }, + }, + 'loan': { + 'get': { + 'collateral/orders': 20 / 15, + 'collateral/orders/{order_id}': 20 / 15, + 'collateral/repay_records': 20 / 15, + 'collateral/collaterals': 20 / 15, + 'collateral/total_amount': 20 / 15, + 'collateral/ltv': 20 / 15, + 'multi_collateral/orders': 20 / 15, + 'multi_collateral/orders/{order_id}': 20 / 15, + 'multi_collateral/repay': 20 / 15, + 'multi_collateral/mortgage': 20 / 15, + 'multi_collateral/currency_quota': 20 / 15, + 'collateral/currencies': 20 / 15, # deprecated + 'multi_collateral/currencies': 20 / 15, # deprecated + 'multi_collateral/ltv': 20 / 15, # deprecated + 'multi_collateral/fixed_rate': 20 / 15, # deprecated + 'multi_collateral/current_rate': 20 / 15, # deprecated + }, + 'post': { + 'collateral/orders': 20 / 15, + 'collateral/repay': 20 / 15, + 'collateral/collaterals': 20 / 15, + 'multi_collateral/orders': 20 / 15, + 'multi_collateral/repay': 20 / 15, + 'multi_collateral/mortgage': 20 / 15, + }, + }, + 'account': { + 'get': { + 'detail': 20 / 15, + 'main_keys': 20 / 15, + 'rate_limit': 20 / 15, + 'stp_groups': 20 / 15, + 'stp_groups/{stp_id}/users': 20 / 15, + 'stp_groups/debit_fee': 20 / 15, + 'debit_fee': 20 / 15, + }, + 'post': { + 'stp_groups': 20 / 15, + 'stp_groups/{stp_id}/users': 20 / 15, + 'debit_fee': 20 / 15, + }, + 'delete': { + 'stp_groups/{stp_id}/users': 20 / 15, + }, + }, + 'rebate': { + 'get': { + 'agency/transaction_history': 20 / 15, + 'agency/commission_history': 20 / 15, + 'partner/transaction_history': 20 / 15, + 'partner/commission_history': 20 / 15, + 'partner/sub_list': 20 / 15, + 'broker/commission_history': 20 / 15, + 'broker/transaction_history': 20 / 15, + 'user/info': 20 / 15, + 'user/sub_relation': 20 / 15, + }, + }, + }, + }, + 'timeframes': { + '10s': '10s', + '1m': '1m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1h', + '2h': '2h', + '4h': '4h', + '8h': '8h', + '1d': '1d', + '7d': '7d', + '1w': '7d', + }, + # copied from gatev2 + 'commonCurrencies': { + 'ORT': 'XREATORS', + 'ASS': 'ASSF', + '88MPH': 'MPH', + 'AXIS': 'AXISDEFI', + 'BIFI': 'BITCOINFILE', + 'BOX': 'DEFIBOX', + 'BYN': 'BEYONDFI', + 'EGG': 'GOOSEFINANCE', + 'GTC': 'GAMECOM', # conflict with Gitcoin and Gastrocoin + 'GTC_HT': 'GAMECOM_HT', + 'GTC_BSC': 'GAMECOM_BSC', + 'HIT': 'HITCHAIN', + 'MM': 'MILLION', # conflict with MilliMeter + 'MPH': 'MORPHER', # conflict with 88MPH + 'POINT': 'GATEPOINT', + 'RAI': 'RAIREFLEXINDEX', # conflict with RAI Finance + 'RED': 'RedLang', + 'SBTC': 'SUPERBITCOIN', + 'TNC': 'TRINITYNETWORKCREDIT', + 'VAI': 'VAIOT', + 'TRAC': 'TRACO', # conflict with OriginTrail(TRAC) + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + }, + 'headers': { + 'X-Gate-Channel-Id': 'ccxt', + }, + 'options': { + 'timeDifference': 0, # the difference between system clock and exchange clock + 'adjustForTimeDifference': False, # controls the adjustment logic upon instantiation + 'sandboxMode': False, + 'unifiedAccount': None, + 'createOrder': { + 'expiration': 86400, # for conditional orders + }, + 'createMarketBuyOrderRequiresPrice': True, + 'networks': { + 'BTC': 'BTC', + 'BRC20': 'BTCBRC', # for eg: ORDI, RATS, ... + 'ETH': 'ETH', + 'ERC20': 'ETH', + 'TRX': 'TRX', + 'TRC20': 'TRX', + 'HECO': 'HT', + 'HRC20': 'HT', + 'BSC': 'BSC', + 'BEP20': 'BSC', + 'SOL': 'SOL', + 'MATIC': 'MATIC', + 'OPTIMISM': 'OPETH', + 'ADA': 'ADA', # CARDANO + 'AVAXC': 'AVAX_C', + 'NEAR': 'NEAR', + 'ARBONE': 'ARBEVM', + 'BASE': 'BASEEVM', + 'SUI': 'SUI', + 'CRONOS': 'CRO', + 'CRO': 'CRO', + 'APT': 'APT', + 'SCROLL': 'SCROLLETH', + 'TAIKO': 'TAIKOETH', + 'HYPE': 'HYPE', + 'ALGO': 'ALGO', + # KAVA: ['KAVA', 'KAVAEVM'] + # SEI: ['SEI', 'SEIEVM'] + 'LINEA': 'LINEAETH', + 'BLAST': 'BLASTETH', + 'XLM': 'XLM', + 'RSK': 'RBTC', + 'TON': 'TON', + 'MNT': 'MNT', + # 'RUNE': 'BTCRUNES', probably, cant verify atm + 'CELO': 'CELO', + 'HBAR': 'HBAR', + # 'FTM': SONIC REBRAND, todo + 'ZKSERA': 'ZKSERA', + 'KLAY': 'KLAY', + 'EOS': 'EOS', + 'ACA': 'ACA', + # TLOS: ['TLOS', 'TLOSEVM'] + # ASTR: ['ASTR', 'ASTREVM'] + # CFX: ['CFX', 'CFXEVM'] + 'XTZ': 'XTZ', + 'EGLD': 'EGLD', + 'GLMR': 'GLMR', + 'AURORA': 'AURORAEVM', + # others + 'KON': 'KONET', + 'GATECHAIN': 'GTEVM', + 'KUSAMA': 'KSMSM', + 'OKC': 'OKT', + 'POLKADOT': 'DOTSM', # todo: DOT for main DOT + 'LUNA': 'LUNC', + }, + 'networksById': { + 'OPETH': 'OP', + 'ETH': 'ERC20', # for GOlang + 'ERC20': 'ERC20', + 'TRX': 'TRC20', + 'TRC20': 'TRC20', + 'HT': 'HRC20', + 'HECO': 'HRC20', + 'BSC': 'BEP20', + 'BEP20': 'BEP20', + 'POLYGON': 'MATIC', + 'POL': 'MATIC', + }, + 'timeInForce': { + 'GTC': 'gtc', + 'IOC': 'ioc', + 'PO': 'poc', + 'POC': 'poc', + 'FOK': 'fok', + }, + 'accountsByType': { + 'funding': 'spot', + 'spot': 'spot', + 'margin': 'margin', + 'cross_margin': 'cross_margin', + 'cross': 'cross_margin', + 'isolated': 'margin', + 'swap': 'futures', + 'future': 'delivery', + 'futures': 'futures', + 'delivery': 'delivery', + 'option': 'options', + 'options': 'options', + }, + 'fetchMarkets': { + 'types': ['spot', 'swap', 'future', 'option'], + }, + 'swap': { + 'fetchMarkets': { + 'settlementCurrencies': ['usdt', 'btc'], + }, + }, + 'future': { + 'fetchMarkets': { + 'settlementCurrencies': ['usdt'], + }, + }, + }, + 'features': { + 'default': { + 'sandbox': True, + 'createOrder': { + 'marginMode': True, + 'triggerPrice': True, + 'triggerDirection': True, # todo: implementation edit needed + 'triggerPriceType': None, + 'stopLossPrice': True, + 'takeProfitPrice': True, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'iceberg': True, # todo implement + 'selfTradePrevention': True, # todo implement + 'leverage': False, + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': True, + }, + 'createOrders': { + 'max': 40, # NOTE! max 10 per symbol + }, + 'fetchMyTrades': { + 'marginMode': True, + 'limit': 1000, + 'daysBack': None, + 'untilDays': 30, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': True, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOpenOrders': { + 'marginMode': True, + 'trigger': True, + 'trailing': False, + 'limit': 100, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': { + 'marginMode': True, + 'trigger': True, + 'trailing': False, + 'limit': 100, + 'untilDays': 30, + 'daysBack': None, + 'daysBackCanceled': None, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 1000, + }, + }, + 'spot': { + 'extends': 'default', + }, + 'forDerivatives': { + 'extends': 'spot', + 'createOrder': { + 'marginMode': False, + 'triggerPriceType': { + 'last': True, + 'mark': True, + 'index': True, + }, + }, + 'createOrders': { + 'max': 10, + }, + 'fetchMyTrades': { + 'marginMode': False, + 'untilDays': None, + }, + 'fetchOpenOrders': { + 'marginMode': False, + }, + 'fetchClosedOrders': { + 'marginMode': False, + 'untilDays': None, + 'limit': 1000, + }, + 'fetchOHLCV': { + 'limit': 1999, + }, + }, + 'swap': { + 'linear': { + 'extends': 'forDerivatives', + }, + 'inverse': { + 'extends': 'forDerivatives', + }, + }, + 'future': { + 'linear': { + 'extends': 'forDerivatives', + }, + 'inverse': { + 'extends': 'forDerivatives', + }, + }, + }, + 'precisionMode': TICK_SIZE, + 'fees': { + 'trading': { + 'tierBased': True, + 'feeSide': 'get', + 'percentage': True, + 'maker': self.parse_number('0.002'), + 'taker': self.parse_number('0.002'), + 'tiers': { + # volume is in BTC + 'maker': [ + [self.parse_number('0'), self.parse_number('0.002')], + [self.parse_number('1.5'), self.parse_number('0.00185')], + [self.parse_number('3'), self.parse_number('0.00175')], + [self.parse_number('6'), self.parse_number('0.00165')], + [self.parse_number('12.5'), self.parse_number('0.00155')], + [self.parse_number('25'), self.parse_number('0.00145')], + [self.parse_number('75'), self.parse_number('0.00135')], + [self.parse_number('200'), self.parse_number('0.00125')], + [self.parse_number('500'), self.parse_number('0.00115')], + [self.parse_number('1250'), self.parse_number('0.00105')], + [self.parse_number('2500'), self.parse_number('0.00095')], + [self.parse_number('3000'), self.parse_number('0.00085')], + [self.parse_number('6000'), self.parse_number('0.00075')], + [self.parse_number('11000'), self.parse_number('0.00065')], + [self.parse_number('20000'), self.parse_number('0.00055')], + [self.parse_number('40000'), self.parse_number('0.00055')], + [self.parse_number('75000'), self.parse_number('0.00055')], + ], + 'taker': [ + [self.parse_number('0'), self.parse_number('0.002')], + [self.parse_number('1.5'), self.parse_number('0.00195')], + [self.parse_number('3'), self.parse_number('0.00185')], + [self.parse_number('6'), self.parse_number('0.00175')], + [self.parse_number('12.5'), self.parse_number('0.00165')], + [self.parse_number('25'), self.parse_number('0.00155')], + [self.parse_number('75'), self.parse_number('0.00145')], + [self.parse_number('200'), self.parse_number('0.00135')], + [self.parse_number('500'), self.parse_number('0.00125')], + [self.parse_number('1250'), self.parse_number('0.00115')], + [self.parse_number('2500'), self.parse_number('0.00105')], + [self.parse_number('3000'), self.parse_number('0.00095')], + [self.parse_number('6000'), self.parse_number('0.00085')], + [self.parse_number('11000'), self.parse_number('0.00075')], + [self.parse_number('20000'), self.parse_number('0.00065')], + [self.parse_number('40000'), self.parse_number('0.00065')], + [self.parse_number('75000'), self.parse_number('0.00065')], + ], + }, + }, + 'swap': { + 'tierBased': True, + 'feeSide': 'base', + 'percentage': True, + 'maker': self.parse_number('0.0'), + 'taker': self.parse_number('0.0005'), + 'tiers': { + 'maker': [ + [self.parse_number('0'), self.parse_number('0.0000')], + [self.parse_number('1.5'), self.parse_number('-0.00005')], + [self.parse_number('3'), self.parse_number('-0.00005')], + [self.parse_number('6'), self.parse_number('-0.00005')], + [self.parse_number('12.5'), self.parse_number('-0.00005')], + [self.parse_number('25'), self.parse_number('-0.00005')], + [self.parse_number('75'), self.parse_number('-0.00005')], + [self.parse_number('200'), self.parse_number('-0.00005')], + [self.parse_number('500'), self.parse_number('-0.00005')], + [self.parse_number('1250'), self.parse_number('-0.00005')], + [self.parse_number('2500'), self.parse_number('-0.00005')], + [self.parse_number('3000'), self.parse_number('-0.00008')], + [self.parse_number('6000'), self.parse_number('-0.01000')], + [self.parse_number('11000'), self.parse_number('-0.01002')], + [self.parse_number('20000'), self.parse_number('-0.01005')], + [self.parse_number('40000'), self.parse_number('-0.02000')], + [self.parse_number('75000'), self.parse_number('-0.02005')], + ], + 'taker': [ + [self.parse_number('0'), self.parse_number('0.00050')], + [self.parse_number('1.5'), self.parse_number('0.00048')], + [self.parse_number('3'), self.parse_number('0.00046')], + [self.parse_number('6'), self.parse_number('0.00044')], + [self.parse_number('12.5'), self.parse_number('0.00042')], + [self.parse_number('25'), self.parse_number('0.00040')], + [self.parse_number('75'), self.parse_number('0.00038')], + [self.parse_number('200'), self.parse_number('0.00036')], + [self.parse_number('500'), self.parse_number('0.00034')], + [self.parse_number('1250'), self.parse_number('0.00032')], + [self.parse_number('2500'), self.parse_number('0.00030')], + [self.parse_number('3000'), self.parse_number('0.00030')], + [self.parse_number('6000'), self.parse_number('0.00030')], + [self.parse_number('11000'), self.parse_number('0.00030')], + [self.parse_number('20000'), self.parse_number('0.00030')], + [self.parse_number('40000'), self.parse_number('0.00030')], + [self.parse_number('75000'), self.parse_number('0.00030')], + ], + }, + }, + }, + # https://www.gate.com/docs/developers/apiv4/en/#label-list + 'exceptions': { + 'exact': { + 'INVALID_PARAM_VALUE': BadRequest, + 'INVALID_PROTOCOL': BadRequest, + 'INVALID_ARGUMENT': BadRequest, + 'INVALID_REQUEST_BODY': BadRequest, + 'MISSING_REQUIRED_PARAM': ArgumentsRequired, + 'BAD_REQUEST': BadRequest, + 'INVALID_CONTENT_TYPE': BadRequest, + 'NOT_ACCEPTABLE': BadRequest, + 'METHOD_NOT_ALLOWED': BadRequest, + 'NOT_FOUND': ExchangeError, + 'AUTHENTICATION_FAILED': AuthenticationError, + 'INVALID_CREDENTIALS': AuthenticationError, + 'INVALID_KEY': AuthenticationError, + 'IP_FORBIDDEN': AuthenticationError, + 'READ_ONLY': PermissionDenied, + 'INVALID_SIGNATURE': AuthenticationError, + 'MISSING_REQUIRED_HEADER': AuthenticationError, + 'REQUEST_EXPIRED': AuthenticationError, + 'ACCOUNT_LOCKED': AccountSuspended, + 'FORBIDDEN': PermissionDenied, + 'SUB_ACCOUNT_NOT_FOUND': ExchangeError, + 'SUB_ACCOUNT_LOCKED': AccountSuspended, + 'MARGIN_BALANCE_EXCEPTION': ExchangeError, + 'MARGIN_TRANSFER_FAILED': ExchangeError, + 'TOO_MUCH_FUTURES_AVAILABLE': ExchangeError, + 'FUTURES_BALANCE_NOT_ENOUGH': InsufficientFunds, + 'ACCOUNT_EXCEPTION': ExchangeError, + 'SUB_ACCOUNT_TRANSFER_FAILED': ExchangeError, + 'ADDRESS_NOT_USED': ExchangeError, + 'TOO_FAST': RateLimitExceeded, + 'WITHDRAWAL_OVER_LIMIT': ExchangeError, + 'API_WITHDRAW_DISABLED': ExchangeNotAvailable, + 'INVALID_WITHDRAW_ID': ExchangeError, + 'INVALID_WITHDRAW_CANCEL_STATUS': ExchangeError, + 'INVALID_PRECISION': InvalidOrder, + 'INVALID_CURRENCY': BadSymbol, + 'INVALID_CURRENCY_PAIR': BadSymbol, + 'POC_FILL_IMMEDIATELY': OrderImmediatelyFillable, # {"label":"POC_FILL_IMMEDIATELY","message":"Order would match and take immediately so its cancelled"} + 'ORDER_NOT_FOUND': OrderNotFound, + 'CLIENT_ID_NOT_FOUND': OrderNotFound, + 'ORDER_CLOSED': InvalidOrder, + 'ORDER_CANCELLED': InvalidOrder, + 'QUANTITY_NOT_ENOUGH': InvalidOrder, + 'BALANCE_NOT_ENOUGH': InsufficientFunds, + 'MARGIN_NOT_SUPPORTED': InvalidOrder, + 'MARGIN_BALANCE_NOT_ENOUGH': InsufficientFunds, + 'AMOUNT_TOO_LITTLE': InvalidOrder, + 'AMOUNT_TOO_MUCH': InvalidOrder, + 'REPEATED_CREATION': InvalidOrder, + 'LOAN_NOT_FOUND': OrderNotFound, + 'LOAN_RECORD_NOT_FOUND': OrderNotFound, + 'NO_MATCHED_LOAN': ExchangeError, + 'NOT_MERGEABLE': ExchangeError, + 'NO_CHANGE': ExchangeError, + 'REPAY_TOO_MUCH': ExchangeError, + 'TOO_MANY_CURRENCY_PAIRS': InvalidOrder, + 'TOO_MANY_ORDERS': InvalidOrder, + 'TOO_MANY_REQUESTS': RateLimitExceeded, + 'MIXED_ACCOUNT_TYPE': InvalidOrder, + 'AUTO_BORROW_TOO_MUCH': ExchangeError, + 'TRADE_RESTRICTED': InsufficientFunds, + 'USER_NOT_FOUND': AccountNotEnabled, + 'CONTRACT_NO_COUNTER': ExchangeError, + 'CONTRACT_NOT_FOUND': BadSymbol, + 'RISK_LIMIT_EXCEEDED': ExchangeError, + 'INSUFFICIENT_AVAILABLE': InsufficientFunds, + 'LIQUIDATE_IMMEDIATELY': InvalidOrder, + 'LEVERAGE_TOO_HIGH': InvalidOrder, + 'LEVERAGE_TOO_LOW': InvalidOrder, + 'ORDER_NOT_OWNED': ExchangeError, + 'ORDER_FINISHED': ExchangeError, + 'POSITION_CROSS_MARGIN': ExchangeError, + 'POSITION_IN_LIQUIDATION': ExchangeError, + 'POSITION_IN_CLOSE': ExchangeError, + 'POSITION_EMPTY': InvalidOrder, + 'REMOVE_TOO_MUCH': ExchangeError, + 'RISK_LIMIT_NOT_MULTIPLE': ExchangeError, + 'RISK_LIMIT_TOO_HIGH': ExchangeError, + 'RISK_LIMIT_TOO_lOW': ExchangeError, + 'PRICE_TOO_DEVIATED': InvalidOrder, + 'SIZE_TOO_LARGE': InvalidOrder, + 'SIZE_TOO_SMALL': InvalidOrder, + 'PRICE_OVER_LIQUIDATION': InvalidOrder, + 'PRICE_OVER_BANKRUPT': InvalidOrder, + 'ORDER_POC_IMMEDIATE': OrderImmediatelyFillable, # {"label":"ORDER_POC_IMMEDIATE","detail":"order price 1700 while counter price 1793.55"} + 'INCREASE_POSITION': InvalidOrder, + 'CONTRACT_IN_DELISTING': ExchangeError, + 'INTERNAL': ExchangeNotAvailable, + 'SERVER_ERROR': ExchangeNotAvailable, + 'TOO_BUSY': ExchangeNotAvailable, + 'CROSS_ACCOUNT_NOT_FOUND': ExchangeError, + 'RISK_LIMIT_TOO_LOW': BadRequest, # {"label":"RISK_LIMIT_TOO_LOW","detail":"limit 1000000"} + 'AUTO_TRIGGER_PRICE_LESS_LAST': InvalidOrder, # {"label":"AUTO_TRIGGER_PRICE_LESS_LAST","message":"invalid argument: Trigger.Price must < last_price"} + 'AUTO_TRIGGER_PRICE_GREATE_LAST': InvalidOrder, # {"label":"AUTO_TRIGGER_PRICE_GREATE_LAST","message":"invalid argument: Trigger.Price must > last_price"} + 'POSITION_HOLDING': BadRequest, + 'USER_LOAN_EXCEEDED': BadRequest, # {"label":"USER_LOAN_EXCEEDED","message":"Max loan amount per user would be exceeded"} + }, + 'broad': {}, + }, + }) + + def set_sandbox_mode(self, enable: bool): + super(gate, self).set_sandbox_mode(enable) + self.options['sandboxMode'] = enable + + async def load_unified_status(self, params={}): + """ + :param dict [params]: extra parameters specific to the exchange API endpoint + returns unifiedAccount so the user can check if the unified account is enabled + + https://www.gate.com/docs/developers/apiv4/#get-account-detail + + :returns boolean: True or False if the enabled unified account is enabled or not and sets the unifiedAccount option if it is None + """ + unifiedAccount = self.safe_bool(self.options, 'unifiedAccount') + if unifiedAccount is None: + try: + # + # { + # "user_id": 10406147, + # "ip_whitelist": [], + # "currency_pairs": [], + # "key": { + # "mode": 1 + # }, + # "tier": 0, + # "tier_expire_time": "0001-01-01T00:00:00Z", + # "copy_trading_role": 0 + # } + # + response = await self.privateAccountGetDetail(params) + result = self.safe_dict(response, 'key', {}) + self.options['unifiedAccount'] = self.safe_integer(result, 'mode') == 2 + except Exception as e: + # if the request fails, the unifiedAccount is disabled + self.options['unifiedAccount'] = False + return self.options['unifiedAccount'] + + async def upgrade_unified_trade_account(self, params={}): + return await self.privateUnifiedPutUnifiedMode(params) + + async def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://www.gate.com/docs/developers/apiv4/en/#get-server-current-time + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = await self.publicSpotGetTime(params) + # + # { + # "server_time": 1731447921098 + # } + # + return self.safe_integer(response, 'server_time') + + def create_expired_option_market(self, symbol: str): + # support expired option contracts + quote = 'USDT' + settle = quote + optionParts = symbol.split('-') + symbolBase = symbol.split('/') + marketIdBase = symbol.split('_') + base = None + expiry = self.safe_string(optionParts, 1) + if symbol.find('/') > -1: + base = self.safe_string(symbolBase, 0) + else: + base = self.safe_string(marketIdBase, 0) + expiry = expiry[2:8] # convert 20230728 to 230728 + strike = self.safe_string(optionParts, 2) + optionType = self.safe_string(optionParts, 3) + datetime = self.convert_expire_date(expiry) + timestamp = self.parse8601(datetime) + return { + 'id': base + '_' + quote + '-' + '20' + expiry + '-' + strike + '-' + optionType, + 'symbol': base + '/' + quote + ':' + settle + '-' + expiry + '-' + strike + '-' + optionType, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': base, + 'quoteId': quote, + 'settleId': settle, + 'active': False, + 'type': 'option', + 'linear': None, + 'inverse': None, + 'spot': False, + 'swap': False, + 'future': False, + 'option': True, + 'margin': False, + 'contract': True, + 'contractSize': self.parse_number('1'), + 'expiry': timestamp, + 'expiryDatetime': datetime, + 'optionType': 'call' if (optionType == 'C') else 'put', + 'strike': self.parse_number(strike), + 'precision': { + 'amount': self.parse_number('1'), + 'price': None, + }, + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'info': None, + } + + def safe_market(self, marketId: Str = None, market: Market = None, delimiter: Str = None, marketType: Str = None) -> MarketInterface: + isOption = (marketId is not None) and ((marketId.find('-C') > -1) or (marketId.find('-P') > -1)) + if isOption and not (marketId in self.markets_by_id): + # handle expired option contracts + return self.create_expired_option_market(marketId) + return super(gate, self).safe_market(marketId, market, delimiter, marketType) + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for gate + + https://www.gate.com/docs/developers/apiv4/en/#list-all-currency-pairs-supported # spot + https://www.gate.com/docs/developers/apiv4/en/#list-all-supported-currency-pairs-supported-in-margin-trading # margin + https://www.gate.com/docs/developers/apiv4/en/#list-all-futures-contracts # swap + https://www.gate.com/docs/developers/apiv4/en/#list-all-futures-contracts-2 # future + https://www.gate.com/docs/developers/apiv4/en/#list-all-the-contracts-with-specified-underlying-and-expiration-time # option + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + if self.options['adjustForTimeDifference']: + await self.load_time_difference() + if self.check_required_credentials(False): + await self.load_unified_status() + rawPromises = [] + fetchMarketsOptions = self.safe_dict(self.options, 'fetchMarkets') + types = self.safe_list(fetchMarketsOptions, 'types', ['spot', 'swap', 'future', 'option']) + for i in range(0, len(types)): + marketType = types[i] + if marketType == 'spot': + # if not sandboxMode: + # gate doesn't have a sandbox for spot markets + rawPromises.append(self.fetch_spot_markets(params)) + # } + elif marketType == 'swap': + rawPromises.append(self.fetch_swap_markets(params)) + elif marketType == 'future': + rawPromises.append(self.fetch_future_markets(params)) + elif marketType == 'option': + rawPromises.append(self.fetch_option_markets(params)) + results = await asyncio.gather(*rawPromises) + return self.arrays_concat(results) + + async def fetch_spot_markets(self, params={}): + marginPromise = self.publicMarginGetCurrencyPairs(params) + spotMarketsPromise = self.publicSpotGetCurrencyPairs(params) + marginResponse, spotMarketsResponse = await asyncio.gather(*[marginPromise, spotMarketsPromise]) + marginMarkets = self.index_by(marginResponse, 'id') + # + # Spot + # + # [ + # { + # "id": "QTUM_ETH", + # "base": "QTUM", + # "base_name": "Quantum", + # "quote": "ETH", + # "quote_name": "Ethereum", + # "fee": "0.2", + # "min_base_amount": "0.01", + # "min_quote_amount": "0.001", + # "max_quote_amount": "50000", + # "amount_precision": 3, + # "precision": 6, + # "trade_status": "tradable", + # "sell_start": 1607313600, + # "buy_start": 1700492400, + # "type": "normal", + # "trade_url": "https://www.gate.com/trade/QTUM_ETH", + # } + # + # Margin + # + # [ + # { + # "id": "ETH_USDT", + # "base": "ETH", + # "quote": "USDT", + # "leverage": 3, + # "min_base_amount": "0.01", + # "min_quote_amount": "100", + # "max_quote_amount": "1000000" + # } + # ] + # + result = [] + for i in range(0, len(spotMarketsResponse)): + spotMarket = spotMarketsResponse[i] + id = self.safe_string(spotMarket, 'id') + marginMarket = self.safe_value(marginMarkets, id) + market = self.deep_extend(marginMarket, spotMarket) + baseId, quoteId = id.split('_') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + takerPercent = self.safe_string(market, 'fee') + makerPercent = self.safe_string(market, 'maker_fee_rate', takerPercent) + amountPrecision = self.parse_number(self.parse_precision(self.safe_string(market, 'amount_precision'))) + tradeStatus = self.safe_string(market, 'trade_status') + leverage = self.safe_number(market, 'leverage') + margin = leverage is not None + buyStart = self.safe_integer_product(spotMarket, 'buy_start', 1000) # buy_start is the trading start time, while sell_start is offline orders start time + createdTs = buyStart if (buyStart != 0) else None + result.append({ + 'id': id, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': margin, + 'swap': False, + 'future': False, + 'option': False, + 'active': (tradeStatus == 'tradable'), + 'contract': False, + 'linear': None, + 'inverse': None, + # Fee is in %, so divide by 100 + 'taker': self.parse_number(Precise.string_div(takerPercent, '100')), + 'maker': self.parse_number(Precise.string_div(makerPercent, '100')), + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': amountPrecision, + 'price': self.parse_number(self.parse_precision(self.safe_string(market, 'precision'))), + }, + 'limits': { + 'leverage': { + 'min': self.parse_number('1'), + 'max': self.safe_number(market, 'leverage', 1), + }, + 'amount': { + 'min': self.safe_number(spotMarket, 'min_base_amount', amountPrecision), + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': self.safe_number(market, 'min_quote_amount'), + 'max': self.safe_number(market, 'max_quote_amount') if margin else None, + }, + }, + 'created': createdTs, + 'info': market, + }) + return result + + async def fetch_swap_markets(self, params={}): + result = [] + swapSettlementCurrencies = self.get_settlement_currencies('swap', 'fetchMarkets') + if self.options['sandboxMode']: + swapSettlementCurrencies = ['usdt'] # gate sandbox only has usdt-margined swaps + for c in range(0, len(swapSettlementCurrencies)): + settleId = swapSettlementCurrencies[c] + request: dict = { + 'settle': settleId, + } + response = await self.publicFuturesGetSettleContracts(self.extend(request, params)) + for i in range(0, len(response)): + parsedMarket = self.parse_contract_market(response[i], settleId) + result.append(parsedMarket) + return result + + async def fetch_future_markets(self, params={}): + if self.options['sandboxMode']: + return [] # right now sandbox does not have inverse swaps + result = [] + futureSettlementCurrencies = self.get_settlement_currencies('future', 'fetchMarkets') + for c in range(0, len(futureSettlementCurrencies)): + settleId = futureSettlementCurrencies[c] + request: dict = { + 'settle': settleId, + } + response = await self.publicDeliveryGetSettleContracts(self.extend(request, params)) + for i in range(0, len(response)): + parsedMarket = self.parse_contract_market(response[i], settleId) + result.append(parsedMarket) + return result + + def parse_contract_market(self, market, settleId): + # + # Perpetual swap + # + # { + # "name": "BTC_USDT", + # "type": "direct", + # "quanto_multiplier": "0.0001", + # "ref_discount_rate": "0", + # "order_price_deviate": "0.5", + # "maintenance_rate": "0.005", + # "mark_type": "index", + # "last_price": "38026", + # "mark_price": "37985.6", + # "index_price": "37954.92", + # "funding_rate_indicative": "0.000219", + # "mark_price_round": "0.01", + # "funding_offset": 0, + # "in_delisting": False, + # "risk_limit_base": "1000000", + # "interest_rate": "0.0003", + # "order_price_round": "0.1", + # "order_size_min": 1, + # "ref_rebate_rate": "0.2", + # "funding_interval": 28800, + # "risk_limit_step": "1000000", + # "leverage_min": "1", + # "leverage_max": "100", + # "risk_limit_max": "8000000", + # "maker_fee_rate": "-0.00025", # not actual value for regular users + # "taker_fee_rate": "0.00075", # not actual value for regular users + # "funding_rate": "0.002053", + # "order_size_max": 1000000, + # "funding_next_apply": 1610035200, + # "short_users": 977, + # "config_change_time": 1609899548, + # "create_time": 1609800048, + # "trade_size": 28530850594, + # "position_size": 5223816, + # "long_users": 455, + # "funding_impact_value": "60000", + # "orders_limit": 50, + # "trade_id": 10851092, + # "orderbook_id": 2129638396 + # } + # + # Delivery Futures + # + # { + # "name": "BTC_USDT_20200814", + # "underlying": "BTC_USDT", + # "cycle": "WEEKLY", + # "type": "direct", + # "quanto_multiplier": "0.0001", + # "mark_type": "index", + # "last_price": "9017", + # "mark_price": "9019", + # "index_price": "9005.3", + # "basis_rate": "0.185095", + # "basis_value": "13.7", + # "basis_impact_value": "100000", + # "settle_price": "0", + # "settle_price_interval": 60, + # "settle_price_duration": 1800, + # "settle_fee_rate": "0.0015", + # "expire_time": 1593763200, + # "order_price_round": "0.1", + # "mark_price_round": "0.1", + # "leverage_min": "1", + # "leverage_max": "100", + # "maintenance_rate": "1000000", + # "risk_limit_base": "140.726652109199", + # "risk_limit_step": "1000000", + # "risk_limit_max": "8000000", + # "maker_fee_rate": "-0.00025", # not actual value for regular users + # "taker_fee_rate": "0.00075", # not actual value for regular users + # "ref_discount_rate": "0", + # "ref_rebate_rate": "0.2", + # "order_price_deviate": "0.5", + # "order_size_min": 1, + # "order_size_max": 1000000, + # "orders_limit": 50, + # "orderbook_id": 63, + # "trade_id": 26, + # "trade_size": 435, + # "position_size": 130, + # "config_change_time": 1593158867, + # "in_delisting": False + # } + # + id = self.safe_string(market, 'name') + parts = id.split('_') + baseId = self.safe_string(parts, 0) + quoteId = self.safe_string(parts, 1) + date = self.safe_string(parts, 2) + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + settle = self.safe_currency_code(settleId) + expiry = self.safe_timestamp(market, 'expire_time') + symbol = '' + marketType = 'swap' + if date is not None: + symbol = base + '/' + quote + ':' + settle + '-' + self.yymmdd(expiry, '') + marketType = 'future' + else: + symbol = base + '/' + quote + ':' + settle + priceDeviate = self.safe_string(market, 'order_price_deviate') + markPrice = self.safe_string(market, 'mark_price') + minMultiplier = Precise.string_sub('1', priceDeviate) + maxMultiplier = Precise.string_add('1', priceDeviate) + minPrice = Precise.string_mul(minMultiplier, markPrice) + maxPrice = Precise.string_mul(maxMultiplier, markPrice) + isLinear = quote == settle + contractSize = self.safe_string(market, 'quanto_multiplier') + # exception only for one market: https://api.gateio.ws/api/v4/futures/btc/contracts + if contractSize == '0': + contractSize = '1' # 1 USD in WEB: https://i.imgur.com/MBBUI04.png + return { + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': marketType, + 'spot': False, + 'margin': False, + 'swap': marketType == 'swap', + 'future': marketType == 'future', + 'option': marketType == 'option', + 'active': True, + 'contract': True, + 'linear': isLinear, + 'inverse': not isLinear, + 'taker': None, + 'maker': None, + 'contractSize': self.parse_number(contractSize), + 'expiry': expiry, + 'expiryDatetime': self.iso8601(expiry), + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number('1'), # all contracts have self step size + 'price': self.safe_number(market, 'order_price_round'), + }, + 'limits': { + 'leverage': { + 'min': self.safe_number(market, 'leverage_min'), + 'max': self.safe_number(market, 'leverage_max'), + }, + 'amount': { + 'min': self.safe_number(market, 'order_size_min'), + 'max': self.safe_number(market, 'order_size_max'), + }, + 'price': { + 'min': self.parse_number(minPrice), + 'max': self.parse_number(maxPrice), + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': self.safe_integer_product(market, 'create_time', 1000), + 'info': market, + } + + async def fetch_option_markets(self, params={}): + result = [] + underlyings = await self.fetch_option_underlyings() + for i in range(0, len(underlyings)): + underlying = underlyings[i] + query = self.extend({}, params) + query['underlying'] = underlying + response = await self.publicOptionsGetContracts(query) + # + # [ + # { + # "orders_limit": "50", + # "order_size_max": "100000", + # "mark_price_round": "0.1", + # "order_size_min": "1", + # "position_limit": "1000000", + # "orderbook_id": "575967", + # "order_price_deviate": "0.9", + # "is_call": True, # True means Call False means Put + # "last_price": "93.9", + # "bid1_size": "0", + # "bid1_price": "0", + # "taker_fee_rate": "0.0004", + # "underlying": "BTC_USDT", + # "create_time": "1646381188", + # "price_limit_fee_rate": "0.1", + # "maker_fee_rate": "0.0004", + # "trade_id": "727", + # "order_price_round": "0.1", + # "settle_fee_rate": "0.0001", + # "trade_size": "1982", + # "ref_rebate_rate": "0", + # "name": "BTC_USDT-20220311-44000-C", + # "underlying_price": "39194.26", + # "strike_price": "44000", + # "multiplier": "0.0001", + # "ask1_price": "0", + # "ref_discount_rate": "0", + # "expiration_time": "1646985600", + # "mark_price": "12.15", + # "position_size": "4", + # "ask1_size": "0", + # "tag": "WEEK" + # } + # ] + # + for j in range(0, len(response)): + market = response[j] + id = self.safe_string(market, 'name') + parts = underlying.split('_') + baseId = self.safe_string(parts, 0) + quoteId = self.safe_string(parts, 1) + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + symbol = base + '/' + quote + expiry = self.safe_timestamp(market, 'expiration_time') + strike = self.safe_string(market, 'strike_price') + isCall = self.safe_value(market, 'is_call') + optionLetter = 'C' if isCall else 'P' + optionType = 'call' if isCall else 'put' + symbol = symbol + ':' + quote + '-' + self.yymmdd(expiry) + '-' + strike + '-' + optionLetter + priceDeviate = self.safe_string(market, 'order_price_deviate') + markPrice = self.safe_string(market, 'mark_price') + minMultiplier = Precise.string_sub('1', priceDeviate) + maxMultiplier = Precise.string_add('1', priceDeviate) + minPrice = Precise.string_mul(minMultiplier, markPrice) + maxPrice = Precise.string_mul(maxMultiplier, markPrice) + result.append({ + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': quote, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': quoteId, + 'type': 'option', + 'spot': False, + 'margin': False, + 'swap': False, + 'future': False, + 'option': True, + 'active': True, + 'contract': True, + 'linear': True, + 'inverse': False, + 'taker': None, + 'maker': None, + 'contractSize': self.parse_number('1'), + 'expiry': expiry, + 'expiryDatetime': self.iso8601(expiry), + 'strike': self.parse_number(strike), + 'optionType': optionType, + 'precision': { + 'amount': self.parse_number('1'), # all options have self step size + 'price': self.safe_number(market, 'order_price_round'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(market, 'order_size_min'), + 'max': self.safe_number(market, 'order_size_max'), + }, + 'price': { + 'min': self.parse_number(minPrice), + 'max': self.parse_number(maxPrice), + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': self.safe_timestamp(market, 'create_time'), + 'info': market, + }) + return result + + async def fetch_option_underlyings(self): + underlyingsResponse = await self.publicOptionsGetUnderlyings() + # + # [ + # { + # "index_time": "1646915796", + # "name": "BTC_USDT", + # "index_price": "39142.73" + # } + # ] + # + underlyings = [] + for i in range(0, len(underlyingsResponse)): + underlying = underlyingsResponse[i] + name = self.safe_string(underlying, 'name') + if name is not None: + underlyings.append(name) + return underlyings + + def prepare_request(self, market=None, type=None, params={}): + """ + @ignore + Fills request params contract, settle, currency_pair, market and account where applicable + :param dict market: CCXT market, required when type is None + :param str type: 'spot', 'swap', or 'future', required when market is None + :param dict [params]: request parameters + :returns: the api request object, and the new params object with non-needed parameters removed + """ + # * Do not call for multi spot order methods like cancelAllOrders and fetchOpenOrders. Use multiOrderSpotPrepareRequest instead + request: dict = {} + if market is not None: + if market['contract']: + request['contract'] = market['id'] + if not market['option']: + request['settle'] = market['settleId'] + else: + request['currency_pair'] = market['id'] + else: + swap = type == 'swap' + future = type == 'future' + if swap or future: + defaultSettle = 'usdt' if swap else 'btc' + settle = self.safe_string_lower(params, 'settle', defaultSettle) + params = self.omit(params, 'settle') + request['settle'] = settle + return [request, params] + + def spot_order_prepare_request(self, market=None, trigger=False, params={}): + """ + @ignore + Fills request params currency_pair, market and account where applicable for spot order methods like fetchOpenOrders, cancelAllOrders + :param dict market: CCXT market + :param bool trigger: True if for a trigger order + :param dict [params]: request parameters + :returns: the api request object, and the new params object with non-needed parameters removed + """ + marginMode, query = self.get_margin_mode(trigger, params) + request: dict = {} + if not trigger: + if market is None: + raise ArgumentsRequired(self.id + ' spotOrderPrepareRequest() requires a market argument for non-trigger orders') + request['account'] = marginMode + request['currency_pair'] = market['id'] # Should always be set for non-trigger + return [request, query] + + def multi_order_spot_prepare_request(self, market=None, trigger=False, params={}): + """ + @ignore + Fills request params currency_pair, market and account where applicable for spot order methods like fetchOpenOrders, cancelAllOrders + :param dict market: CCXT market + :param bool trigger: True if for a trigger order + :param dict [params]: request parameters + :returns: the api request object, and the new params object with non-needed parameters removed + """ + marginMode, query = self.get_margin_mode(trigger, params) + request: dict = { + 'account': marginMode, + } + if market is not None: + if trigger: + # gate spot and margin trigger orders use the term market instead of currency_pair, and normal instead of spot. Neither parameter is used when fetching/cancelling a single order. They are used for creating a single trigger order, but createOrder does not call self method + request['market'] = market['id'] + else: + request['currency_pair'] = market['id'] + return [request, query] + + def get_margin_mode(self, trigger, params): + """ + @ignore + Gets the margin type for self api call + :param bool trigger: True if for a trigger order + :param dict [params]: Request params + :returns: The marginMode and the updated request params with marginMode removed, marginMode value is the value that can be read by the "account" property specified in gates api docs + """ + defaultMarginMode = self.safe_string_lower_2(self.options, 'defaultMarginMode', 'marginMode', 'spot') # 'margin' is isolated margin on gate's api + marginMode = self.safe_string_lower_2(params, 'marginMode', 'account', defaultMarginMode) + params = self.omit(params, ['marginMode', 'account']) + if marginMode == 'cross': + marginMode = 'cross_margin' + elif marginMode == 'isolated': + marginMode = 'margin' + elif marginMode == '': + marginMode = 'spot' + if trigger: + if marginMode == 'spot': + # gate spot trigger orders use the term normal instead of spot + marginMode = 'normal' + if marginMode == 'cross_margin': + raise BadRequest(self.id + ' getMarginMode() does not support trigger orders for cross margin') + isUnifiedAccount = False + isUnifiedAccount, params = self.handle_option_and_params(params, 'getMarginMode', 'unifiedAccount') + if isUnifiedAccount: + marginMode = 'unified' + return [marginMode, params] + + def get_settlement_currencies(self, type, method): + options = self.safe_value(self.options, type, {}) # ['BTC', 'USDT'] unified codes + fetchMarketsContractOptions = self.safe_value(options, method, {}) + defaultSettle = ['usdt'] if (type == 'swap') else ['btc'] + return self.safe_value(fetchMarketsContractOptions, 'settlementCurrencies', defaultSettle) + + async def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://www.gate.com/docs/developers/apiv4/en/#list-all-currencies-details + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + # sandbox/testnet only supports future markets + apiBackup = self.safe_value(self.urls, 'apiBackup') + if apiBackup is not None: + return {} + response = await self.publicSpotGetCurrencies(params) + # + # [ + # { + # "currency": "USDT", + # "name": "Tether", + # "delisted": False, + # "withdraw_disabled": False, + # "withdraw_delayed": False, + # "deposit_disabled": False, + # "trade_disabled": False, + # "fixed_rate": "", + # "chain": "ETH", + # "chains": [ + # { + # "name": "ETH", + # "addr": "0xdAC17F958D2ee523a2206206994597C13D831ec7", + # "withdraw_disabled": False, + # "withdraw_delayed": False, + # "deposit_disabled": False + # }, + # { + # "name": "ARBEVM", + # "addr": "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", + # "withdraw_disabled": False, + # "withdraw_delayed": False, + # "deposit_disabled": False + # }, + # { + # "name": "BSC", + # "addr": "0x55d398326f99059fF775485246999027B3197955", + # "withdraw_disabled": False, + # "withdraw_delayed": False, + # "deposit_disabled": False + # }, + # ] + # }, + # ] + # + indexedCurrencies = self.index_by(response, 'currency') + result: dict = {} + for i in range(0, len(response)): + entry = response[i] + currencyId = self.safe_string(entry, 'currency') + code = self.safe_currency_code(currencyId) + # check leveraged tokens(e.g. BTC3S, ETH5L) + type = 'leveraged' if self.is_leveraged_currency(currencyId, True, indexedCurrencies) else 'crypto' + chains = self.safe_list(entry, 'chains', []) + networks = {} + for j in range(0, len(chains)): + chain = chains[j] + networkId = self.safe_string(chain, 'name') + networkCode = self.network_id_to_code(networkId) + networks[networkCode] = { + 'info': chain, + 'id': networkId, + 'network': networkCode, + 'active': None, + 'deposit': not self.safe_bool(chain, 'deposit_disabled'), + 'withdraw': not self.safe_bool(chain, 'withdraw_disabled'), + 'fee': None, + 'precision': self.parse_number('0.0001'), # temporary safe default, because no value provided from API, + 'limits': { + 'deposit': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + }, + } + result[code] = self.safe_currency_structure({ + 'id': currencyId, + 'code': code, + 'name': self.safe_string(entry, 'name'), + 'type': type, + 'active': not self.safe_bool(entry, 'delisted'), + 'deposit': not self.safe_bool(entry, 'deposit_disabled'), + 'withdraw': not self.safe_bool(entry, 'withdraw_disabled'), + 'fee': None, + 'networks': networks, + 'precision': self.parse_number('0.0001'), + 'info': entry, + }) + return result + + async def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate + + https://www.gate.com/docs/developers/apiv4/en/#get-a-single-contract + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + await self.load_markets() + market = self.market(symbol) + if not market['swap']: + raise BadSymbol(self.id + ' fetchFundingRate() supports swap contracts only') + request, query = self.prepare_request(market, None, params) + response = await self.publicFuturesGetSettleContractsContract(self.extend(request, query)) + # + # [ + # { + # "name": "BTC_USDT", + # "type": "direct", + # "quanto_multiplier": "0.0001", + # "ref_discount_rate": "0", + # "order_price_deviate": "0.5", + # "maintenance_rate": "0.005", + # "mark_type": "index", + # "last_price": "38026", + # "mark_price": "37985.6", + # "index_price": "37954.92", + # "funding_rate_indicative": "0.000219", + # "mark_price_round": "0.01", + # "funding_offset": 0, + # "in_delisting": False, + # "risk_limit_base": "1000000", + # "interest_rate": "0.0003", + # "order_price_round": "0.1", + # "order_size_min": 1, + # "ref_rebate_rate": "0.2", + # "funding_interval": 28800, + # "risk_limit_step": "1000000", + # "leverage_min": "1", + # "leverage_max": "100", + # "risk_limit_max": "8000000", + # "maker_fee_rate": "-0.00025", + # "taker_fee_rate": "0.00075", + # "funding_rate": "0.002053", + # "order_size_max": 1000000, + # "funding_next_apply": 1610035200, + # "short_users": 977, + # "config_change_time": 1609899548, + # "trade_size": 28530850594, + # "position_size": 5223816, + # "long_users": 455, + # "funding_impact_value": "60000", + # "orders_limit": 50, + # "trade_id": 10851092, + # "orderbook_id": 2129638396 + # } + # ] + # + return self.parse_funding_rate(response) + + async def fetch_funding_rates(self, symbols: Strings = None, params={}) -> FundingRates: + """ + fetch the funding rate for multiple markets + + https://www.gate.com/docs/developers/apiv4/en/#list-all-futures-contracts + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `funding rate structures `, indexed by market symbols + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + market = None + if symbols is not None: + firstSymbol = self.safe_string(symbols, 0) + market = self.market(firstSymbol) + request, query = self.prepare_request(market, 'swap', params) + response = await self.publicFuturesGetSettleContracts(self.extend(request, query)) + # + # [ + # { + # "name": "BTC_USDT", + # "type": "direct", + # "quanto_multiplier": "0.0001", + # "ref_discount_rate": "0", + # "order_price_deviate": "0.5", + # "maintenance_rate": "0.005", + # "mark_type": "index", + # "last_price": "38026", + # "mark_price": "37985.6", + # "index_price": "37954.92", + # "funding_rate_indicative": "0.000219", + # "mark_price_round": "0.01", + # "funding_offset": 0, + # "in_delisting": False, + # "risk_limit_base": "1000000", + # "interest_rate": "0.0003", + # "order_price_round": "0.1", + # "order_size_min": 1, + # "ref_rebate_rate": "0.2", + # "funding_interval": 28800, + # "risk_limit_step": "1000000", + # "leverage_min": "1", + # "leverage_max": "100", + # "risk_limit_max": "8000000", + # "maker_fee_rate": "-0.00025", + # "taker_fee_rate": "0.00075", + # "funding_rate": "0.002053", + # "order_size_max": 1000000, + # "funding_next_apply": 1610035200, + # "short_users": 977, + # "config_change_time": 1609899548, + # "trade_size": 28530850594, + # "position_size": 5223816, + # "long_users": 455, + # "funding_impact_value": "60000", + # "orders_limit": 50, + # "trade_id": 10851092, + # "orderbook_id": 2129638396 + # } + # ] + # + return self.parse_funding_rates(response, symbols) + + def parse_funding_rate(self, contract, market: Market = None) -> FundingRate: + # + # { + # "name": "BTC_USDT", + # "type": "direct", + # "quanto_multiplier": "0.0001", + # "ref_discount_rate": "0", + # "order_price_deviate": "0.5", + # "maintenance_rate": "0.005", + # "mark_type": "index", + # "last_price": "38026", + # "mark_price": "37985.6", + # "index_price": "37954.92", + # "funding_rate_indicative": "0.000219", + # "mark_price_round": "0.01", + # "funding_offset": 0, + # "in_delisting": False, + # "risk_limit_base": "1000000", + # "interest_rate": "0.0003", + # "order_price_round": "0.1", + # "order_size_min": 1, + # "ref_rebate_rate": "0.2", + # "funding_interval": 28800, + # "risk_limit_step": "1000000", + # "leverage_min": "1", + # "leverage_max": "100", + # "risk_limit_max": "8000000", + # "maker_fee_rate": "-0.00025", + # "taker_fee_rate": "0.00075", + # "funding_rate": "0.002053", + # "order_size_max": 1000000, + # "funding_next_apply": 1610035200, + # "short_users": 977, + # "config_change_time": 1609899548, + # "trade_size": 28530850594, + # "position_size": 5223816, + # "long_users": 455, + # "funding_impact_value": "60000", + # "orders_limit": 50, + # "trade_id": 10851092, + # "orderbook_id": 2129638396 + # } + # + marketId = self.safe_string(contract, 'name') + symbol = self.safe_symbol(marketId, market, '_', 'swap') + markPrice = self.safe_number(contract, 'mark_price') + indexPrice = self.safe_number(contract, 'index_price') + interestRate = self.safe_number(contract, 'interest_rate') + fundingRate = self.safe_number(contract, 'funding_rate') + fundingTime = self.safe_timestamp(contract, 'funding_next_apply') + fundingRateIndicative = self.safe_number(contract, 'funding_rate_indicative') + fundingInterval = Precise.string_mul('1000', self.safe_string(contract, 'funding_interval')) + return { + 'info': contract, + 'symbol': symbol, + 'markPrice': markPrice, + 'indexPrice': indexPrice, + 'interestRate': interestRate, + 'estimatedSettlePrice': None, + 'timestamp': None, + 'datetime': None, + 'fundingRate': fundingRate, + 'fundingTimestamp': fundingTime, + 'fundingDatetime': self.iso8601(fundingTime), + 'nextFundingRate': fundingRateIndicative, + 'nextFundingTimestamp': None, + 'nextFundingDatetime': None, + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': self.parse_funding_interval(fundingInterval), + } + + def parse_funding_interval(self, interval): + intervals: dict = { + '3600000': '1h', + '14400000': '4h', + '28800000': '8h', + '57600000': '16h', + '86400000': '24h', + } + return self.safe_string(intervals, interval, interval) + + async def fetch_network_deposit_address(self, code: str, params={}): + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], # todo: currencies have network-junctions + } + response = await self.privateWalletGetDepositAddress(self.extend(request, params)) + addresses = self.safe_value(response, 'multichain_addresses') + currencyId = self.safe_string(response, 'currency') + code = self.safe_currency_code(currencyId) + result: dict = {} + for i in range(0, len(addresses)): + entry = addresses[i] + # + # { + # "chain": "ETH", + # "address": "0x359a697945E79C7e17b634675BD73B33324E9408", + # "payment_id": "", + # "payment_name": "", + # "obtain_failed": "0" + # } + # + obtainFailed = self.safe_integer(entry, 'obtain_failed') + if obtainFailed: + continue + network = self.safe_string(entry, 'chain') + address = self.safe_string(entry, 'address') + tag = self.safe_string(entry, 'payment_id') + result[network] = { + 'info': entry, + 'code': code, # kept here for backward-compatibility, but will be removed soon + 'currency': code, + 'address': address, + 'tag': tag, + } + return result + + async def fetch_deposit_addresses_by_network(self, code: str, params={}) -> List[DepositAddress]: + """ + fetch a dictionary of addresses for a currency, indexed by network + :param str code: unified currency code of the currency for the deposit address + :param dict [params]: extra parameters specific to the api endpoint + :returns dict: a dictionary of `address structures ` indexed by the network + """ + await self.load_markets() + currency = self.currency(code) + request = { + 'currency': currency['id'], + } + response = await self.privateWalletGetDepositAddress(self.extend(request, params)) + chains = self.safe_value(response, 'multichain_addresses', []) + currencyId = self.safe_string(response, 'currency') + currency = self.safe_currency(currencyId, currency) + parsed = self.parse_deposit_addresses(chains, None, False) + return self.index_by(parsed, 'network') + + async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://www.gate.com/docs/developers/apiv4/en/#generate-currency-deposit-address + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.network]: unified network code(not used directly by gate.com but used by ccxt to filter the response) + :returns dict: an `address structure ` + """ + await self.load_markets() + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + chainsIndexedById = await self.fetch_deposit_addresses_by_network(code, params) + selectedNetworkIdOrCode = self.select_network_code_from_unified_networks(code, networkCode, chainsIndexedById) + return chainsIndexedById[selectedNetworkIdOrCode] + + def parse_deposit_address(self, depositAddress, currency=None): + # + # { + # chain: "BTC", + # address: "1Nxu.......Ys", + # payment_id: "", + # payment_name: "", + # obtain_failed: "0", + # } + # + address = self.safe_string(depositAddress, 'address') + self.check_address(address) + return { + 'info': depositAddress, + 'currency': self.safe_string(currency, 'code'), + 'address': address, + 'tag': self.safe_string(depositAddress, 'payment_id'), + 'network': self.network_id_to_code(self.safe_string(depositAddress, 'chain')), + } + + async def fetch_trading_fee(self, symbol: str, params={}) -> TradingFeeInterface: + """ + fetch the trading fees for a market + + https://www.gate.com/docs/developers/apiv4/en/#retrieve-personal-trading-fee + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `fee structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'currency_pair': market['id'], + } + response = await self.privateWalletGetFee(self.extend(request, params)) + # + # { + # "user_id": 1486602, + # "taker_fee": "0.002", + # "maker_fee": "0.002", + # "gt_discount": True, + # "gt_taker_fee": "0.0015", + # "gt_maker_fee": "0.0015", + # "loan_fee": "0.18", + # "point_type": "0", + # "futures_taker_fee": "0.0005", + # "futures_maker_fee": "0" + # } + # + return self.parse_trading_fee(response, market) + + async def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://www.gate.com/docs/developers/apiv4/en/#retrieve-personal-trading-fee + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + await self.load_markets() + response = await self.privateWalletGetFee(params) + # + # { + # "user_id": 1486602, + # "taker_fee": "0.002", + # "maker_fee": "0.002", + # "gt_discount": True, + # "gt_taker_fee": "0.0015", + # "gt_maker_fee": "0.0015", + # "loan_fee": "0.18", + # "point_type": "0", + # "futures_taker_fee": "0.0005", + # "futures_maker_fee": "0" + # } + # + return self.parse_trading_fees(response) + + def parse_trading_fees(self, response): + result: dict = {} + for i in range(0, len(self.symbols)): + symbol = self.symbols[i] + market = self.market(symbol) + result[symbol] = self.parse_trading_fee(response, market) + return result + + def parse_trading_fee(self, info, market: Market = None): + # + # { + # "user_id": 1486602, + # "taker_fee": "0.002", + # "maker_fee": "0.002", + # "gt_discount": True, + # "gt_taker_fee": "0.0015", + # "gt_maker_fee": "0.0015", + # "loan_fee": "0.18", + # "point_type": "0", + # "futures_taker_fee": "0.0005", + # "futures_maker_fee": "0" + # } + # + gtDiscount = self.safe_value(info, 'gt_discount') + taker = 'gt_taker_fee' if gtDiscount else 'taker_fee' + maker = 'gt_maker_fee' if gtDiscount else 'maker_fee' + contract = self.safe_value(market, 'contract') + takerKey = 'futures_taker_fee' if contract else taker + makerKey = 'futures_maker_fee' if contract else maker + return { + 'info': info, + 'symbol': self.safe_string(market, 'symbol'), + 'maker': self.safe_number(info, makerKey), + 'taker': self.safe_number(info, takerKey), + 'percentage': None, + 'tierBased': None, + } + + async def fetch_transaction_fees(self, codes: Strings = None, params={}): + """ + @deprecated + please use fetchDepositWithdrawFees instead + + https://www.gate.com/docs/developers/apiv4/en/#retrieve-withdrawal-status + + :param str[]|None codes: list of unified currency codes + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `fee structures ` + """ + await self.load_markets() + response = await self.privateWalletGetWithdrawStatus(params) + # + # { + # "currency": "MTN", + # "name": "Medicalchain", + # "name_cn": "Medicalchain", + # "deposit": "0", + # "withdraw_percent": "0%", + # "withdraw_fix": "900", + # "withdraw_day_limit": "500000", + # "withdraw_day_limit_remain": "500000", + # "withdraw_amount_mini": "900.1", + # "withdraw_eachtime_limit": "90000000000", + # "withdraw_fix_on_chains": { + # "ETH": "900" + # } + # } + # + result: dict = {} + withdrawFees = {} + for i in range(0, len(response)): + withdrawFees = {} + entry = response[i] + currencyId = self.safe_string(entry, 'currency') + code = self.safe_currency_code(currencyId) + if (codes is not None) and not self.in_array(code, codes): + continue + withdrawFixOnChains = self.safe_value(entry, 'withdraw_fix_on_chains') + if withdrawFixOnChains is None: + withdrawFees = self.safe_number(entry, 'withdraw_fix') + else: + networkIds = list(withdrawFixOnChains.keys()) + for j in range(0, len(networkIds)): + networkId = networkIds[j] + networkCode = self.network_id_to_code(networkId) + withdrawFees[networkCode] = self.parse_number(withdrawFixOnChains[networkId]) + result[code] = { + 'withdraw': withdrawFees, + 'deposit': None, + 'info': entry, + } + return result + + async def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}): + """ + fetch deposit and withdraw fees + + https://www.gate.com/docs/developers/apiv4/en/#retrieve-withdrawal-status + + :param str[]|None codes: list of unified currency codes + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `fee structures ` + """ + await self.load_markets() + response = await self.privateWalletGetWithdrawStatus(params) + # + # [ + # { + # "currency": "MTN", + # "name": "Medicalchain", + # "name_cn": "Medicalchain", + # "deposit": "0", + # "withdraw_percent": "0%", + # "withdraw_fix": "900", + # "withdraw_day_limit": "500000", + # "withdraw_day_limit_remain": "500000", + # "withdraw_amount_mini": "900.1", + # "withdraw_eachtime_limit": "90000000000", + # "withdraw_fix_on_chains": { + # "ETH": "900" + # } + # } + # ] + # + return self.parse_deposit_withdraw_fees(response, codes, 'currency') + + def parse_deposit_withdraw_fee(self, fee, currency: Currency = None): + # + # { + # "currency": "MTN", + # "name": "Medicalchain", + # "name_cn": "Medicalchain", + # "deposit": "0", + # "withdraw_percent": "0%", + # "withdraw_fix": "900", + # "withdraw_day_limit": "500000", + # "withdraw_day_limit_remain": "500000", + # "withdraw_amount_mini": "900.1", + # "withdraw_eachtime_limit": "90000000000", + # "withdraw_fix_on_chains": { + # "ETH": "900" + # } + # } + # + withdrawFixOnChains = self.safe_value(fee, 'withdraw_fix_on_chains') + result: dict = { + 'info': fee, + 'withdraw': { + 'fee': self.safe_number(fee, 'withdraw_fix'), + 'percentage': False, + }, + 'deposit': { + 'fee': self.safe_number(fee, 'deposit'), + 'percentage': False, + }, + 'networks': {}, + } + if withdrawFixOnChains is not None: + chainKeys = list(withdrawFixOnChains.keys()) + for i in range(0, len(chainKeys)): + chainKey = chainKeys[i] + networkCode = self.network_id_to_code(chainKey, self.safe_string(fee, 'currency')) + result['networks'][networkCode] = { + 'withdraw': { + 'fee': self.parse_number(withdrawFixOnChains[chainKey]), + 'percentage': False, + }, + 'deposit': { + 'fee': None, + 'percentage': None, + }, + } + return result + + async def fetch_funding_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch the history of funding payments paid and received on self account + + https://www.gate.com/docs/developers/apiv4/en/#query-account-book-2 + https://www.gate.com/docs/developers/apiv4/en/#query-account-book-3 + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch funding history for + :param int [limit]: the maximum number of funding history structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding history structure ` + """ + await self.load_markets() + # defaultType = 'future' + market = None + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + type, query = self.handle_market_type_and_params('fetchFundingHistory', market, params) + request, requestParams = self.prepare_request(market, type, query) + request['type'] = 'fund' # 'dnw' 'pnl' 'fee' 'refr' 'fund' 'point_dnw' 'point_fee' 'point_refr' + if since is not None: + # from should be integer + request['from'] = self.parse_to_int(since / 1000) + if limit is not None: + request['limit'] = limit + response = None + if type == 'swap': + response = await self.privateFuturesGetSettleAccountBook(self.extend(request, requestParams)) + elif type == 'future': + response = await self.privateDeliveryGetSettleAccountBook(self.extend(request, requestParams)) + else: + raise NotSupported(self.id + ' fetchFundingHistory() only support swap & future market type') + # + # [ + # { + # "time": 1646899200, + # "change": "-0.027722", + # "balance": "11.653120591841", + # "text": "XRP_USDT", + # "type": "fund" + # }, + # ... + # ] + # + return self.parse_funding_histories(response, symbol, since, limit) + + def parse_funding_histories(self, response, symbol, since, limit) -> List[FundingHistory]: + result = [] + for i in range(0, len(response)): + entry = response[i] + funding = self.parse_funding_history(entry) + result.append(funding) + sorted = self.sort_by(result, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, symbol, since, limit) + + def parse_funding_history(self, info, market: Market = None): + # + # { + # "time": 1646899200, + # "change": "-0.027722", + # "balance": "11.653120591841", + # "text": "XRP_USDT", + # "type": "fund" + # } + # + timestamp = self.safe_timestamp(info, 'time') + marketId = self.safe_string(info, 'text') + market = self.safe_market(marketId, market, '_', 'swap') + return { + 'info': info, + 'symbol': self.safe_string(market, 'symbol'), + 'code': self.safe_string(market, 'settle'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': None, + 'amount': self.safe_number(info, 'change'), + } + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://www.gate.com/docs/developers/apiv4/en/#retrieve-order-book + https://www.gate.com/docs/developers/apiv4/en/#futures-order-book + https://www.gate.com/docs/developers/apiv4/en/#futures-order-book-2 + https://www.gate.com/docs/developers/apiv4/en/#options-order-book + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + # + # request: Dict = { + # 'currency_pair': market['id'], + # 'interval': '0', # depth, 0 means no aggregation is applied, default to 0 + # 'limit': limit, # maximum number of order depth data in asks or bids + # 'with_id': True, # return order book ID + # } + # + request, query = self.prepare_request(market, market['type'], params) + if limit is not None: + if market['spot']: + limit = min(limit, 1000) + else: + limit = min(limit, 300) + request['limit'] = limit + request['with_id'] = True + response = None + if market['spot'] or market['margin']: + response = await self.publicSpotGetOrderBook(self.extend(request, query)) + elif market['swap']: + response = await self.publicFuturesGetSettleOrderBook(self.extend(request, query)) + elif market['future']: + response = await self.publicDeliveryGetSettleOrderBook(self.extend(request, query)) + elif market['option']: + response = await self.publicOptionsGetOrderBook(self.extend(request, query)) + else: + raise NotSupported(self.id + ' fetchOrderBook() not support self market type') + # + # spot + # + # { + # "id": 6358770031 + # "current": 1634345973275, + # "update": 1634345973271, + # "asks": [ + # ["2.2241","12449.827"], + # ["2.2242","200"], + # ["2.2244","826.931"], + # ["2.2248","3876.107"], + # ["2.225","2377.252"], + # ["2.22509","439.484"], + # ["2.2251","1489.313"], + # ["2.2253","714.582"], + # ["2.2254","1349.784"], + # ["2.2256","234.701"]], + # "bids": [ + # ["2.2236","32.465"], + # ["2.2232","243.983"], + # ["2.2231","32.207"], + # ["2.223","449.827"], + # ["2.2228","7.918"], + # ["2.2227","12703.482"], + # ["2.2226","143.033"], + # ["2.2225","143.027"], + # ["2.2224","1369.352"], + # ["2.2223","756.063"] + # ] + # } + # + # swap, future and option + # + # { + # "id": 6358770031 + # "current": 1634350208.745, + # "asks": [ + # {"s": 24909, "p": "61264.8"}, + # {"s": 81, "p": "61266.6"}, + # {"s": 2000, "p": "61267.6"}, + # {"s": 490, "p": "61270.2"}, + # {"s": 12, "p": "61270.4"}, + # {"s": 11782, "p": "61273.2"}, + # {"s": 14666, "p": "61273.3"}, + # {"s": 22541, "p": "61273.4"}, + # {"s": 33, "p": "61273.6"}, + # {"s": 11980, "p": "61274.5"} + # ], + # "bids": [ + # {"s": 41844, "p": "61264.7"}, + # {"s": 13783, "p": "61263.3"}, + # {"s": 1143, "p": "61259.8"}, + # {"s": 81, "p": "61258.7"}, + # {"s": 2471, "p": "61257.8"}, + # {"s": 2471, "p": "61257.7"}, + # {"s": 2471, "p": "61256.5"}, + # {"s": 3, "p": "61254.2"}, + # {"s": 114, "p": "61252.4"}, + # {"s": 14372, "p": "61248.6"} + # ], + # "update": 1634350208.724 + # } + # + timestamp = self.safe_integer(response, 'current') + if not market['spot']: + timestamp = timestamp * 1000 + priceKey = 0 if market['spot'] else 'p' + amountKey = 1 if market['spot'] else 's' + nonce = self.safe_integer(response, 'id') + result = self.parse_order_book(response, symbol, timestamp, 'bids', 'asks', priceKey, amountKey) + result['nonce'] = nonce + return result + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://www.gate.com/docs/developers/apiv4/en/#get-details-of-a-specifc-order + https://www.gate.com/docs/developers/apiv4/en/#list-futures-tickers + https://www.gate.com/docs/developers/apiv4/en/#list-futures-tickers-2 + https://www.gate.com/docs/developers/apiv4/en/#list-tickers-of-options-contracts + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request, query = self.prepare_request(market, None, params) + response = None + if market['spot'] or market['margin']: + response = await self.publicSpotGetTickers(self.extend(request, query)) + elif market['swap']: + response = await self.publicFuturesGetSettleTickers(self.extend(request, query)) + elif market['future']: + response = await self.publicDeliveryGetSettleTickers(self.extend(request, query)) + elif market['option']: + marketId = market['id'] + optionParts = marketId.split('-') + request['underlying'] = self.safe_string(optionParts, 0) + response = await self.publicOptionsGetTickers(self.extend(request, query)) + else: + raise NotSupported(self.id + ' fetchTicker() not support self market type') + ticker = None + if market['option']: + for i in range(0, len(response)): + entry = response[i] + if entry['name'] == market['id']: + ticker = entry + break + else: + ticker = self.safe_value(response, 0) + return self.parse_ticker(ticker, market) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # SPOT + # + # { + # "currency_pair": "KFC_USDT", + # "last": "7.255", + # "lowest_ask": "7.298", + # "highest_bid": "7.218", + # "change_percentage": "-1.18", + # "base_volume": "1219.053687865", + # "quote_volume": "8807.40299875455", + # "high_24h": "7.262", + # "low_24h": "7.095" + # } + # + # LINEAR/DELIVERY + # + # { + # "contract": "BTC_USDT", + # "last": "6432", + # "low_24h": "6278", + # "high_24h": "6790", + # "change_percentage": "4.43", + # "total_size": "32323904", + # "volume_24h": "184040233284", + # "volume_24h_btc": "28613220", + # "volume_24h_usd": "184040233284", + # "volume_24h_base": "28613220", + # "volume_24h_quote": "184040233284", + # "volume_24h_settle": "28613220", + # "mark_price": "6534", + # "funding_rate": "0.0001", + # "funding_rate_indicative": "0.0001", + # "index_price": "6531" + # } + # + # bookTicker + # { + # "t": 1671363004228, + # "u": 9793320464, + # "s": "BTC_USDT", + # "b": "16716.8", # best bid price + # "B": "0.0134", # best bid size + # "a": "16716.9", # best ask price + # "A": "0.0353" # best ask size + # } + # + # option + # + # { + # "vega": "0.00002", + # "leverage": "12.277188268663", + # "ask_iv": "0", + # "delta": "-0.99999", + # "last_price": "0", + # "theta": "-0.00661", + # "bid1_price": "1096", + # "mark_iv": "0.7799", + # "name": "BTC_USDT-20230608-28500-P", + # "bid_iv": "0", + # "ask1_price": "2935", + # "mark_price": "2147.3", + # "position_size": 0, + # "bid1_size": 12, + # "ask1_size": -14, + # "gamma": "0" + # } + # + marketId = self.safe_string_n(ticker, ['currency_pair', 'contract', 'name']) + marketType = 'contract' if ('mark_price' in ticker) else 'spot' + symbol = self.safe_symbol(marketId, market, '_', marketType) + last = self.safe_string_2(ticker, 'last', 'last_price') + ask = self.safe_string_n(ticker, ['lowest_ask', 'a', 'ask1_price']) + bid = self.safe_string_n(ticker, ['highest_bid', 'b', 'bid1_price']) + high = self.safe_string(ticker, 'high_24h') + low = self.safe_string(ticker, 'low_24h') + bidVolume = self.safe_string_2(ticker, 'B', 'bid1_size') + askVolume = self.safe_string_2(ticker, 'A', 'ask1_size') + timestamp = self.safe_integer(ticker, 't') + baseVolume = self.safe_string_2(ticker, 'base_volume', 'volume_24h_base') + if baseVolume == 'nan': + baseVolume = '0' + quoteVolume = self.safe_string_2(ticker, 'quote_volume', 'volume_24h_quote') + if quoteVolume == 'nan': + quoteVolume = '0' + percentage = self.safe_string(ticker, 'change_percentage') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': high, + 'low': low, + 'bid': bid, + 'bidVolume': bidVolume, + 'ask': ask, + 'askVolume': askVolume, + 'vwap': None, + 'open': None, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': percentage, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'markPrice': self.safe_string(ticker, 'mark_price'), + 'indexPrice': self.safe_string(ticker, 'index_price'), + 'info': ticker, + }, market) + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://www.gate.com/docs/developers/apiv4/en/#get-details-of-a-specifc-order + https://www.gate.com/docs/developers/apiv4/en/#list-futures-tickers + https://www.gate.com/docs/developers/apiv4/en/#list-futures-tickers-2 + https://www.gate.com/docs/developers/apiv4/en/#list-tickers-of-options-contracts + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + first = self.safe_string(symbols, 0) + market = None + if first is not None: + market = self.market(first) + type, query = self.handle_market_type_and_params('fetchTickers', market, params) + request, requestParams = self.prepare_request(None, type, query) + response = None + request['timezone'] = 'utc0' # default to utc + if type == 'spot' or type == 'margin': + response = await self.publicSpotGetTickers(self.extend(request, requestParams)) + elif type == 'swap': + response = await self.publicFuturesGetSettleTickers(self.extend(request, requestParams)) + elif type == 'future': + response = await self.publicDeliveryGetSettleTickers(self.extend(request, requestParams)) + elif type == 'option': + self.check_required_argument('fetchTickers', symbols, 'symbols') + marketId = market['id'] + optionParts = marketId.split('-') + request['underlying'] = self.safe_string(optionParts, 0) + response = await self.publicOptionsGetTickers(self.extend(request, requestParams)) + else: + raise NotSupported(self.id + ' fetchTickers() not support self market type, provide symbols or set params["defaultType"] to one from spot/margin/swap/future/option') + return self.parse_tickers(response, symbols) + + def parse_balance_helper(self, entry): + account = self.account() + account['used'] = self.safe_string_2(entry, 'freeze', 'locked') + account['free'] = self.safe_string(entry, 'available') + account['total'] = self.safe_string(entry, 'total') + if 'borrowed' in entry: + account['debt'] = self.safe_string(entry, 'borrowed') + return account + + async def fetch_balance(self, params={}) -> Balances: + """ + + https://www.gate.com/docs/developers/apiv4/en/#margin-account-list + https://www.gate.com/docs/developers/apiv4/en/#get-unified-account-information + https://www.gate.com/docs/developers/apiv4/en/#list-spot-trading-accounts + https://www.gate.com/docs/developers/apiv4/en/#get-futures-account + https://www.gate.com/docs/developers/apiv4/en/#get-futures-account-2 + https://www.gate.com/docs/developers/apiv4/en/#query-account-information + + :param dict [params]: exchange specific parameters + :param str [params.type]: spot, margin, swap or future, if not provided self.options['defaultType'] is used + :param str [params.settle]: 'btc' or 'usdt' - settle currency for perpetual swap and future - default="usdt" for swap and "btc" for future + :param str [params.marginMode]: 'cross' or 'isolated' - marginMode for margin trading if not provided self.options['defaultMarginMode'] is used + :param str [params.symbol]: margin only - unified ccxt symbol + :param boolean [params.unifiedAccount]: default False, set to True for fetching the unified account balance + :returns dict: a `balance structure ` + """ + await self.load_markets() + await self.load_unified_status() + symbol = self.safe_string(params, 'symbol') + params = self.omit(params, 'symbol') + isUnifiedAccount = False + isUnifiedAccount, params = self.handle_option_and_params(params, 'fetchBalance', 'unifiedAccount') + type, query = self.handle_market_type_and_params('fetchBalance', None, params) + request, requestParams = self.prepare_request(None, type, query) + marginMode, requestQuery = self.get_margin_mode(False, requestParams) + if symbol is not None: + market = self.market(symbol) + request['currency_pair'] = market['id'] + response = None + if isUnifiedAccount: + response = await self.privateUnifiedGetAccounts(self.extend(request, params)) + elif type == 'spot': + if marginMode == 'spot': + response = await self.privateSpotGetAccounts(self.extend(request, requestQuery)) + elif marginMode == 'margin': + response = await self.privateMarginGetAccounts(self.extend(request, requestQuery)) + elif marginMode == 'cross_margin': + response = await self.privateMarginGetCrossAccounts(self.extend(request, requestQuery)) + else: + raise NotSupported(self.id + ' fetchBalance() not support self marginMode') + elif type == 'funding': + response = await self.privateMarginGetFundingAccounts(self.extend(request, requestQuery)) + elif type == 'swap': + response = await self.privateFuturesGetSettleAccounts(self.extend(request, requestQuery)) + elif type == 'future': + response = await self.privateDeliveryGetSettleAccounts(self.extend(request, requestQuery)) + elif type == 'option': + response = await self.privateOptionsGetAccounts(self.extend(request, requestQuery)) + else: + raise NotSupported(self.id + ' fetchBalance() not support self market type') + contract = ((type == 'swap') or (type == 'future') or (type == 'option')) + if contract: + response = [response] + # + # Spot / margin funding + # + # [ + # { + # "currency": "DBC", + # "available": "0", + # "locked": "0" + # "lent": "0", # margin funding only + # "total_lent": "0" # margin funding only + # }, + # ... + # ] + # + # Margin + # + # [ + # { + # "currency_pair": "DOGE_USDT", + # "locked": False, + # "risk": "9999.99", + # "base": { + # "currency": "DOGE", + # "available": "0", + # "locked": "0", + # "borrowed": "0", + # "interest": "0" + # }, + # "quote": { + # "currency": "USDT", + # "available": "0.73402", + # "locked": "0", + # "borrowed": "0", + # "interest": "0" + # } + # }, + # ... + # ] + # + # Cross margin + # + # { + # "user_id": 10406147, + # "locked": False, + # "balances": { + # "USDT": { + # "available": "1", + # "freeze": "0", + # "borrowed": "0", + # "interest": "0" + # } + # }, + # "total": "1", + # "borrowed": "0", + # "interest": "0", + # "risk": "9999.99" + # } + # + # Perpetual Swap + # + # { + # "order_margin": "0", + # "point": "0", + # "bonus": "0", + # "history": { + # "dnw": "2.1321", + # "pnl": "11.5351", + # "refr": "0", + # "point_fee": "0", + # "fund": "-0.32340576684", + # "bonus_dnw": "0", + # "point_refr": "0", + # "bonus_offset": "0", + # "fee": "-0.20132775", + # "point_dnw": "0", + # }, + # "unrealised_pnl": "13.315100000006", + # "total": "12.51345151332", + # "available": "0", + # "in_dual_mode": False, + # "currency": "USDT", + # "position_margin": "12.51345151332", + # "user": "6333333", + # } + # + # Delivery Future + # + # { + # "order_margin": "0", + # "point": "0", + # "history": { + # "dnw": "1", + # "pnl": "0", + # "refr": "0", + # "point_fee": "0", + # "point_dnw": "0", + # "settle": "0", + # "settle_fee": "0", + # "point_refr": "0", + # "fee": "0", + # }, + # "unrealised_pnl": "0", + # "total": "1", + # "available": "1", + # "currency": "USDT", + # "position_margin": "0", + # "user": "6333333", + # } + # + # option + # + # { + # "order_margin": "0", + # "bid_order_margin": "0", + # "init_margin": "0", + # "history": { + # "dnw": "32", + # "set": "0", + # "point_fee": "0", + # "point_dnw": "0", + # "prem": "0", + # "point_refr": "0", + # "insur": "0", + # "fee": "0", + # "refr": "0" + # }, + # "total": "32", + # "available": "32", + # "liq_triggered": False, + # "maint_margin": "0", + # "ask_order_margin": "0", + # "point": "0", + # "position_notional_limit": "2000000", + # "unrealised_pnl": "0", + # "equity": "32", + # "user": 5691076, + # "currency": "USDT", + # "short_enabled": False, + # "orders_limit": 10 + # } + # + # unified + # + # { + # "user_id": 10001, + # "locked": False, + # "balances": { + # "ETH": { + # "available": "0", + # "freeze": "0", + # "borrowed": "0.075393666654", + # "negative_liab": "0", + # "futures_pos_liab": "0", + # "equity": "1016.1", + # "total_freeze": "0", + # "total_liab": "0" + # }, + # "POINT": { + # "available": "9999999999.017023138734", + # "freeze": "0", + # "borrowed": "0", + # "negative_liab": "0", + # "futures_pos_liab": "0", + # "equity": "12016.1", + # "total_freeze": "0", + # "total_liab": "0" + # }, + # "USDT": { + # "available": "0.00000062023", + # "freeze": "0", + # "borrowed": "0", + # "negative_liab": "0", + # "futures_pos_liab": "0", + # "equity": "16.1", + # "total_freeze": "0", + # "total_liab": "0" + # } + # }, + # "total": "230.94621713", + # "borrowed": "161.66395521", + # "total_initial_margin": "1025.0524665088", + # "total_margin_balance": "3382495.944473949183", + # "total_maintenance_margin": "205.01049330176", + # "total_initial_margin_rate": "3299.827135672679", + # "total_maintenance_margin_rate": "16499.135678363399", + # "total_available_margin": "3381470.892007440383", + # "unified_account_total": "3381470.892007440383", + # "unified_account_total_liab": "0", + # "unified_account_total_equity": "100016.1", + # "leverage": "2" + # } + # + result: dict = { + 'info': response, + } + isolated = marginMode == 'margin' and type == 'spot' + data = response + if 'balances' in data: # True for cross_margin and unified + flatBalances = [] + balances = self.safe_value(data, 'balances', []) + # inject currency and create an artificial balance object + # so it can follow the existent flow + keys = list(balances.keys()) + for i in range(0, len(keys)): + currencyId = keys[i] + content = balances[currencyId] + content['currency'] = currencyId + flatBalances.append(content) + data = flatBalances + for i in range(0, len(data)): + entry = data[i] + if isolated: + marketId = self.safe_string(entry, 'currency_pair') + symbolInner = self.safe_symbol(marketId, None, '_', 'margin') + base = self.safe_value(entry, 'base', {}) + quote = self.safe_value(entry, 'quote', {}) + baseCode = self.safe_currency_code(self.safe_string(base, 'currency')) + quoteCode = self.safe_currency_code(self.safe_string(quote, 'currency')) + subResult: dict = {} + subResult[baseCode] = self.parse_balance_helper(base) + subResult[quoteCode] = self.parse_balance_helper(quote) + result[symbolInner] = self.safe_balance(subResult) + else: + code = self.safe_currency_code(self.safe_string(entry, 'currency')) + result[code] = self.parse_balance_helper(entry) + returnResult = result if isolated else self.safe_balance(result) + return returnResult + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://www.gate.com/docs/developers/apiv4/en/#market-candlesticks # spot + https://www.gate.com/docs/developers/apiv4/en/#get-futures-candlesticks # swap + https://www.gate.com/docs/developers/apiv4/en/#market-candlesticks # future + https://www.gate.com/docs/developers/apiv4/en/#get-options-candlesticks # option + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch, limit is conflicted with since and params["until"], If either since and params["until"] is specified, request will be rejected + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.price]: "mark" or "index" for mark price and index price candles + :param int [params.until]: timestamp in ms of the latest candle to fetch + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns int[][]: A list of candles ordered, open, high, low, close, volume(units in quote currency) + """ + await self.load_markets() + market = self.market(symbol) + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate') + if paginate: + return await self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params, 1000) + if market['option']: + return await self.fetch_option_ohlcv(symbol, timeframe, since, limit, params) + price = self.safe_string(params, 'price') + request: dict = {} + request, params = self.prepare_request(market, None, params) + request['interval'] = self.safe_string(self.timeframes, timeframe, timeframe) + maxLimit = 1999 if market['contract'] else 1000 + limit = maxLimit if (limit is None) else min(limit, maxLimit) + until = self.safe_integer(params, 'until') + if until is not None: + until = self.parse_to_int(until / 1000) + params = self.omit(params, 'until') + if since is not None: + duration = self.parse_timeframe(timeframe) + request['from'] = self.parse_to_int(since / 1000) + distance = (limit - 1) * duration + toTimestamp = self.sum(request['from'], distance) + currentTimestamp = self.seconds() + to = min(toTimestamp, currentTimestamp) + if until is not None: + request['to'] = min(to, until) + else: + request['to'] = to + else: + if until is not None: + request['to'] = until + request['limit'] = limit + response = None + if market['contract']: + isMark = (price == 'mark') + isIndex = (price == 'index') + if isMark or isIndex: + request['contract'] = price + '_' + market['id'] + params = self.omit(params, 'price') + if market['future']: + response = await self.publicDeliveryGetSettleCandlesticks(self.extend(request, params)) + elif market['swap']: + response = await self.publicFuturesGetSettleCandlesticks(self.extend(request, params)) + else: + response = await self.publicSpotGetCandlesticks(self.extend(request, params)) + return self.parse_ohlcvs(response, market, timeframe, since, limit) + + async def fetch_option_ohlcv(self, symbol: str, timeframe='1m', since: Int = None, limit: Int = None, params={}): + # separated option logic because the from, to and limit parameters weren't functioning + await self.load_markets() + market = self.market(symbol) + request: dict = {} + request, params = self.prepare_request(market, None, params) + request['interval'] = self.safe_string(self.timeframes, timeframe, timeframe) + response = await self.publicOptionsGetCandlesticks(self.extend(request, params)) + return self.parse_ohlcvs(response, market, timeframe, since, limit) + + async def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical funding rate prices + + https://www.gate.com/docs/developers/apiv4/en/#funding-rate-history + + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: timestamp in ms of the earliest funding rate to fetch + :param int [limit]: the maximum amount of `funding rate structures ` to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest funding rate to fetch + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `funding rate structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchFundingRateHistory() requires a symbol argument') + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchFundingRateHistory', 'paginate') + if paginate: + return await self.fetch_paginated_call_deterministic('fetchFundingRateHistory', symbol, since, limit, '8h', params) + market = self.market(symbol) + if not market['swap']: + raise BadSymbol(self.id + ' fetchFundingRateHistory() supports swap contracts only') + request: dict = {} + request, params = self.prepare_request(market, None, params) + if limit is not None: + request['limit'] = limit + if since is not None: + request['from'] = self.parse_to_int(since / 1000) + until = self.safe_integer(params, 'until') + if until is not None: + params = self.omit(params, 'until') + request['to'] = self.parse_to_int(until / 1000) + response = await self.publicFuturesGetSettleFundingRate(self.extend(request, params)) + # + # { + # "r": "0.00063521", + # "t": "1621267200000", + # } + # + rates = [] + for i in range(0, len(response)): + entry = response[i] + timestamp = self.safe_timestamp(entry, 't') + rates.append({ + 'info': entry, + 'symbol': symbol, + 'fundingRate': self.safe_number(entry, 'r'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + sorted = self.sort_by(rates, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, market['symbol'], since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # Spot market candles + # + # [ + # "1660957920", # timestamp + # "6227.070147198573", # quote volume + # "0.0000133485", # close + # "0.0000133615", # high + # "0.0000133347", # low + # "0.0000133468", # open + # "466641934.99" # base volume + # ] + # + # + # Swap, Future, Option, Mark and Index price candles + # + # { + # "t":1632873600, # Unix timestamp in seconds + # "o": "41025", # Open price + # "h": "41882.17", # Highest price + # "c": "41776.92", # Close price + # "l": "40783.94" # Lowest price + # } + # + if isinstance(ohlcv, list): + return [ + self.safe_timestamp(ohlcv, 0), # unix timestamp in seconds + self.safe_number(ohlcv, 5), # open price + self.safe_number(ohlcv, 3), # highest price + self.safe_number(ohlcv, 4), # lowest price + self.safe_number(ohlcv, 2), # close price + self.safe_number(ohlcv, 6), # trading volume + ] + else: + # Swap, Future, Option, Mark and Index price candles + return [ + self.safe_timestamp(ohlcv, 't'), # unix timestamp in seconds + self.safe_number(ohlcv, 'o'), # open price + self.safe_number(ohlcv, 'h'), # highest price + self.safe_number(ohlcv, 'l'), # lowest price + self.safe_number(ohlcv, 'c'), # close price + self.safe_number(ohlcv, 'v'), # trading volume, None for mark or index price + ] + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://www.gate.com/docs/developers/apiv4/en/#retrieve-market-trades + https://www.gate.com/docs/developers/apiv4/en/#futures-trading-history + https://www.gate.com/docs/developers/apiv4/en/#futures-trading-history-2 + https://www.gate.com/docs/developers/apiv4/en/#options-trade-history + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest trade to fetch + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchTrades', 'paginate') + if paginate: + return await self.fetch_paginated_call_dynamic('fetchTrades', symbol, since, limit, params) + market = self.market(symbol) + # + # spot + # + # request: Dict = { + # 'currency_pair': market['id'], + # 'limit': limit, # maximum number of records to be returned in a single list + # 'last_id': 'id', # specify list staring point using the id of last record in previous list-query results + # 'reverse': False, # True to retrieve records where id is smaller than the specified last_id, False to retrieve records where id is larger than the specified last_id + # } + # + # swap, future + # + # request: Dict = { + # 'settle': market['settleId'], + # 'contract': market['id'], + # 'limit': limit, # maximum number of records to be returned in a single list + # 'last_id': 'id', # specify list staring point using the id of last record in previous list-query results + # 'from': since / 1000), # starting time in seconds, if not specified, to and limit will be used to limit response items + # 'to': self.seconds(), # end time in seconds, default to current time + # } + # + request, query = self.prepare_request(market, None, params) + until = self.safe_integer_2(params, 'to', 'until') + if until is not None: + params = self.omit(params, ['until']) + request['to'] = self.parse_to_int(until / 1000) + if limit is not None: + request['limit'] = min(limit, 1000) # default 100, max 1000 + if since is not None and (market['contract']): + request['from'] = self.parse_to_int(since / 1000) + response = None + if market['type'] == 'spot' or market['type'] == 'margin': + response = await self.publicSpotGetTrades(self.extend(request, query)) + elif market['swap']: + response = await self.publicFuturesGetSettleTrades(self.extend(request, query)) + elif market['future']: + response = await self.publicDeliveryGetSettleTrades(self.extend(request, query)) + elif market['type'] == 'option': + response = await self.publicOptionsGetTrades(self.extend(request, query)) + else: + raise NotSupported(self.id + ' fetchTrades() not support self market type.') + # + # spot + # + # [ + # { + # "id": "1852958144", + # "create_time": "1634673259", + # "create_time_ms": "1634673259378.105000", + # "currency_pair": "ADA_USDT", + # "side": "sell", + # "amount": "307.078", + # "price": "2.104", + # } + # ] + # + # perpetual swap + # + # [ + # { + # "size": "2", + # "id": "2522911", + # "create_time_ms": "1634673380.182", + # "create_time": "1634673380.182", + # "contract": "ADA_USDT", + # "price": "2.10486", + # } + # ] + # + # option + # + # [ + # { + # "size": -5, + # "id": 25, + # "create_time": 1682378573, + # "contract": "ETH_USDT-20230526-2000-P", + # "price": "209.1" + # } + # ] + # + return self.parse_trades(response, market, since, limit) + + async def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all the trades made from a single order + + https://www.gate.com/docs/developers/apiv4/en/#list-personal-trading-history + https://www.gate.com/docs/developers/apiv4/en/#list-personal-trading-history-2 + https://www.gate.com/docs/developers/apiv4/en/#list-personal-trading-history-3 + https://www.gate.com/docs/developers/apiv4/en/#list-personal-trading-history-4 + + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrderTrades() requires a symbol argument') + await self.load_markets() + # + # [ + # { + # "id":"3711449544", + # "create_time":"1655486040", + # "create_time_ms":"1655486040177.599900", + # "currency_pair":"SHIB_USDT", + # "side":"buy", + # "role":"taker", + # "amount":"1360039", + # "price":"0.0000081084", + # "order_id":"169717399644", + # "fee":"2720.078", + # "fee_currency":"SHIB", + # "point_fee":"0", + # "gt_fee":"0" + # } + # ] + # + response = await self.fetch_my_trades(symbol, since, limit, {'order_id': id}) + return response + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + Fetch personal trading history + + https://www.gate.com/docs/developers/apiv4/en/#list-personal-trading-history + https://www.gate.com/docs/developers/apiv4/en/#list-personal-trading-history-2 + https://www.gate.com/docs/developers/apiv4/en/#list-personal-trading-history-3 + https://www.gate.com/docs/developers/apiv4/en/#list-personal-trading-history-4 + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' - marginMode for margin trading if not provided self.options['defaultMarginMode'] is used + :param str [params.type]: 'spot', 'swap', or 'future', if not provided self.options['defaultMarginMode'] is used + :param int [params.until]: The latest timestamp, in ms, that fetched trades were made + :param int [params.page]: *spot only* Page number + :param str [params.order_id]: *spot only* Filter trades with specified order ID. symbol is also required if self field is present + :param str [params.order]: *contract only* Futures order ID, return related data only if specified + :param int [params.offset]: *contract only* list offset, starting from 0 + :param str [params.last_id]: *contract only* specify list staring point using the id of last record in previous list-query results + :param int [params.count_total]: *contract only* whether to return total number matched, default to 0(no return) + :param bool [params.unifiedAccount]: set to True for fetching trades in a unified account + :param bool [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + await self.load_unified_status() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchMyTrades', 'paginate') + if paginate: + return await self.fetch_paginated_call_dynamic('fetchMyTrades', symbol, since, limit, params) + type = None + marginMode = None + request: dict = {} + market = self.market(symbol) if (symbol is not None) else None + until = self.safe_integer(params, 'until') + params = self.omit(params, ['until']) + type, params = self.handle_market_type_and_params('fetchMyTrades', market, params) + contract = (type == 'swap') or (type == 'future') or (type == 'option') + if contract: + request, params = self.prepare_request(market, type, params) + if type == 'option': + params = self.omit(params, 'order_id') + else: + if market is not None: + request['currency_pair'] = market['id'] # Should always be set for non-trigger + marginMode, params = self.get_margin_mode(False, params) + request['account'] = marginMode + if limit is not None: + request['limit'] = limit # default 100, max 1000 + if since is not None: + request['from'] = self.parse_to_int(since / 1000) + if until is not None: + request['to'] = self.parse_to_int(until / 1000) + response = None + if type == 'spot' or type == 'margin': + response = await self.privateSpotGetMyTrades(self.extend(request, params)) + elif type == 'swap': + response = await self.privateFuturesGetSettleMyTradesTimerange(self.extend(request, params)) + elif type == 'future': + response = await self.privateDeliveryGetSettleMyTrades(self.extend(request, params)) + elif type == 'option': + response = await self.privateOptionsGetMyTrades(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchMyTrades() not support self market type.') + # + # spot + # + # [ + # { + # "id": "2876130500", + # "create_time": "1645464610", + # "create_time_ms": "1645464610777.399200", + # "currency_pair": "DOGE_USDT", + # "side": "sell", + # "role": "taker", + # "amount": "10.97", + # "price": "0.137384", + # "order_id": "125924049993", + # "fee": "0.00301420496", + # "fee_currency": "USDT", + # "point_fee": "0", + # "gt_fee": "0" + # } + # ] + # + # perpetual swap + # + # [ + # { + # "size": -5, + # "order_id": "130264979823", + # "id": 26884791, + # "role": "taker", + # "create_time": 1645465199.5472, + # "contract": "DOGE_USDT", + # "price": "0.136888" + # } + # ] + # + # future + # + # [ + # { + # "id": 121234231, + # "create_time": 1514764800.123, + # "contract": "BTC_USDT", + # "order_id": "21893289839", + # "size": 100, + # "price": "100.123", + # "role": "taker" + # } + # ] + # + # option + # + # [ + # { + # "underlying_price": "26817.84", + # "size": -1, + # "contract": "BTC_USDT-20230602-26500-C", + # "id": 16, + # "role": "taker", + # "create_time": 1685594770, + # "order_id": 2611026125, + # "price": "333" + # } + # ] + # + return self.parse_trades(response, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # public + # + # spot: + # { + # "id": "1334253759", + # "create_time": "1626342738", + # "create_time_ms": "1626342738331.497000", + # "currency_pair": "BTC_USDT", + # "side": "sell", + # "amount": "0.0022", + # "price": "32452.16" + # } + # + # swap: + # + # { + # "id": "442288327", + # "contract": "BTC_USDT", + # "create_time": "1739814676.707", + # "create_time_ms": "1739814676.707", + # "size": "-105", + # "price": "95594.8" + # } + # + # + # public ws + # + # { + # "id": 221994511, + # "time": 1580311438.618647, + # "price": "9309", + # "amount": "0.0019", + # "type": "sell" + # } + # + # spot rest + # + # { + # "id": "2876130500", + # "create_time": "1645464610", + # "create_time_ms": "1645464610777.399200", + # "currency_pair": "DOGE_USDT", + # "side": "sell", + # "role": "taker", + # "amount": "10.97", + # "price": "0.137384", + # "order_id": "125924049993", + # "fee": "0.00301420496", + # "fee_currency": "USDT", + # "point_fee": "1.1", + # "gt_fee":"2.2" + # } + # + # perpetual swap rest + # + # { + # "size": -5, + # "order_id": "130264979823", + # "id": 26884791, + # "role": "taker", + # "create_time": 1645465199.5472, + # "contract": "DOGE_USDT", + # "price": "0.136888" + # } + # + # future rest + # + # { + # "id": 121234231, + # "create_time": 1514764800.123, + # "contract": "BTC_USDT", + # "order_id": "21893289839", + # "size": 100, + # "price": "100.123", + # "role": "taker" + # } + # + # fetchTrades: option + # + # { + # "size": -5, + # "id": 25, + # "create_time": 1682378573, + # "contract": "ETH_USDT-20230526-2000-P", + # "price": "209.1" + # } + # + # fetchMyTrades: option + # + # { + # "underlying_price": "26817.84", + # "size": -1, + # "contract": "BTC_USDT-20230602-26500-C", + # "id": 16, + # "role": "taker", + # "create_time": 1685594770, + # "order_id": 2611026125, + # "price": "333" + # } + # + id = self.safe_string_2(trade, 'id', 'trade_id') + timestamp: Int = None + msString = self.safe_string(trade, 'create_time_ms') + if msString is not None: + msString = Precise.string_mul(msString, '1000') + msString = msString[0:13] + timestamp = self.parse_to_int(msString) + else: + timestamp = self.safe_timestamp_2(trade, 'time', 'create_time') + marketId = self.safe_string_2(trade, 'currency_pair', 'contract') + marketType = 'contract' if ('contract' in trade) else 'spot' + market = self.safe_market(marketId, market, '_', marketType) + amountString = self.safe_string_2(trade, 'amount', 'size') + priceString = self.safe_string(trade, 'price') + contractSide = 'sell' if Precise.string_lt(amountString, '0') else 'buy' + amountString = Precise.string_abs(amountString) + side = self.safe_string_2(trade, 'side', 'type', contractSide) + orderId = self.safe_string(trade, 'order_id') + feeAmount = self.safe_string(trade, 'fee') + gtFee = self.omit_zero(self.safe_string(trade, 'gt_fee')) + pointFee = self.omit_zero(self.safe_string(trade, 'point_fee')) + fees = [] + if feeAmount is not None: + feeCurrencyId = self.safe_string(trade, 'fee_currency') + feeCurrencyCode = self.safe_currency_code(feeCurrencyId) + if feeCurrencyCode is None: + feeCurrencyCode = self.safe_string(market, 'settle') + fees.append({ + 'cost': feeAmount, + 'currency': feeCurrencyCode, + }) + if gtFee is not None: + fees.append({ + 'cost': gtFee, + 'currency': 'GT', + }) + if pointFee is not None: + fees.append({ + 'cost': pointFee, + 'currency': 'GatePoint', + }) + takerOrMaker = self.safe_string(trade, 'role') + return self.safe_trade({ + 'info': trade, + 'id': id, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'order': orderId, + 'type': None, + 'side': side, + 'takerOrMaker': takerOrMaker, + 'price': priceString, + 'amount': amountString, + 'cost': None, + 'fee': None, + 'fees': fees, + }, market) + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://www.gate.com/docs/developers/apiv4/en/#retrieve-deposit-records + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: end time in ms + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchDeposits', 'paginate') + if paginate: + return await self.fetch_paginated_call_dynamic('fetchDeposits', code, since, limit, params) + request: dict = {} + currency = None + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] # todo: currencies have network-junctions + if limit is not None: + request['limit'] = limit + if since is not None: + start = self.parse_to_int(since / 1000) + request['from'] = start + request['to'] = self.sum(start, 30 * 24 * 60 * 60) + request, params = self.handle_until_option('to', request, params, 0.001) + response = await self.privateWalletGetDeposits(self.extend(request, params)) + return self.parse_transactions(response, currency) + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://www.gate.com/docs/developers/apiv4/en/#retrieve-withdrawal-records + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: end time in ms + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchWithdrawals', 'paginate') + if paginate: + return await self.fetch_paginated_call_dynamic('fetchWithdrawals', code, since, limit, params) + request: dict = {} + currency = None + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] # todo: currencies have network-junctions + if limit is not None: + request['limit'] = limit + if since is not None: + start = self.parse_to_int(since / 1000) + request['from'] = start + request['to'] = self.sum(start, 30 * 24 * 60 * 60) + request, params = self.handle_until_option('to', request, params, 0.001) + response = await self.privateWalletGetWithdrawals(self.extend(request, params)) + return self.parse_transactions(response, currency) + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://www.gate.com/docs/developers/apiv4/en/#withdraw + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.check_address(address) + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], # todo: currencies have network-junctions + 'address': address, + 'amount': self.currency_to_precision(code, amount), + } + if tag is not None: + request['memo'] = tag + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + if networkCode is not None: + request['chain'] = self.network_code_to_id(networkCode) + response = await self.privateWithdrawalsPostWithdrawals(self.extend(request, params)) + # + # { + # "id": "w13389675", + # "currency": "USDT", + # "amount": "50", + # "address": "TUu2rLFrmzUodiWfYki7QCNtv1akL682p1", + # "memo": null + # } + # + return self.parse_transaction(response, currency) + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'PEND': 'pending', + 'REQUEST': 'pending', + 'DMOVE': 'pending', + 'MANUAL': 'pending', + 'VERIFY': 'pending', + 'PROCES': 'pending', + 'EXTPEND': 'pending', + 'SPLITPEND': 'pending', + 'CANCEL': 'canceled', + 'FAIL': 'failed', + 'INVALID': 'failed', + 'DONE': 'ok', + 'BCODE': 'ok', # GateCode withdrawal + } + return self.safe_string(statuses, status, status) + + def parse_transaction_type(self, type): + types: dict = { + 'd': 'deposit', + 'w': 'withdrawal', + } + return self.safe_string(types, type, type) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fetchDeposits + # + # { + # "id": "d33361395", + # "currency": "USDT_TRX", + # "address": "TErdnxenuLtXfnMafLbfappYdHtnXQ5U4z", + # "amount": "100", + # "txid": "ae9374de34e558562fe18cbb1bf9ab4d9eb8aa7669d65541c9fa2a532c1474a0", + # "timestamp": "1626345819", + # "status": "DONE", + # "memo": "" + # } + # + # withdraw + # + # { + # "id":"w64413318", + # "currency":"usdt", + # "amount":"10150", + # "address":"0x0ab891497116f7f5532a4c2f4f7b1784488628e1", + # "memo":null, + # "status":"REQUEST", + # "chain":"eth", + # "withdraw_order_id":"", + # "fee_amount":"4.15000000" + # } + # + # fetchWithdrawals + # + # { + # "id": "210496", + # "timestamp": "1542000000", + # "withdraw_order_id": "order_123456", + # "currency": "USDT", + # "address": "1HkxtBAMrA3tP5ENnYY2CZortjZvFDH5Cs", + # "txid": "128988928203223323290", + # "block_number": "41575382", + # "amount": "222.61", + # "fee": "0.01", + # "memo": "", + # "status": "DONE", + # "chain": "TRX" + # } + # + # { + # "id": "w13389675", + # "currency": "USDT", + # "amount": "50", + # "address": "TUu2rLFrmzUodiWfYki7QCNtv1akL682p1", + # "memo": null + # } + # + # { + # "currency":"usdt", + # "address":"0x01c0A9b7b4CdE774AF0f3E47CB4f1c2CCdBa0806", + # "amount":"1880", + # "chain":"eth" + # } + # + id = self.safe_string(transaction, 'id') + type = None + amountString = self.safe_string(transaction, 'amount') + if id is not None: + if id[0] == 'b': + # GateCode handling + type = 'deposit' if Precise.string_gt(amountString, '0') else 'withdrawal' + amountString = Precise.string_abs(amountString) + else: + type = self.parse_transaction_type(id[0]) + feeCostString = self.safe_string_2(transaction, 'fee', 'fee_amount') + if type == 'withdrawal': + amountString = Precise.string_sub(amountString, feeCostString) + networkId = self.safe_string_upper(transaction, 'chain') + currencyId = self.safe_string(transaction, 'currency') + code = self.safe_currency_code(currencyId) + txid = self.safe_string(transaction, 'txid') + rawStatus = self.safe_string(transaction, 'status') + status = self.parse_transaction_status(rawStatus) + address = self.safe_string(transaction, 'address') + tag = self.safe_string(transaction, 'memo') + timestamp = self.safe_timestamp(transaction, 'timestamp') + return { + 'info': transaction, + 'id': id, + 'txid': txid, + 'currency': code, + 'amount': self.parse_number(amountString), + 'network': self.network_id_to_code(networkId), + 'address': address, + 'addressTo': None, + 'addressFrom': None, + 'tag': tag, + 'tagTo': None, + 'tagFrom': None, + 'status': status, + 'type': type, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'updated': None, + 'internal': None, + 'comment': None, + 'fee': { + 'currency': code, + 'cost': self.parse_number(feeCostString), + }, + } + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + Create an order on the exchange + + https://www.gate.com/docs/developers/apiv4/en/#create-an-order + https://www.gate.com/docs/developers/apiv4/en/#create-a-price-triggered-order + https://www.gate.com/docs/developers/apiv4/en/#create-a-futures-order + https://www.gate.com/docs/developers/apiv4/en/#create-a-price-triggered-order-2 + https://www.gate.com/docs/developers/apiv4/en/#create-a-futures-order-2 + https://www.gate.com/docs/developers/apiv4/en/#create-a-price-triggered-order-3 + https://www.gate.com/docs/developers/apiv4/en/#create-an-options-order + + :param str symbol: Unified CCXT market symbol + :param str type: 'limit' or 'market' *"market" is contract only* + :param str side: 'buy' or 'sell' + :param float amount: the amount of currency to trade + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: The price at which a trigger order is triggered at + :param str [params.timeInForce]: "GTC", "IOC", or "PO" + :param float [params.stopLossPrice]: The price at which a stop loss order is triggered at + :param float [params.takeProfitPrice]: The price at which a take profit order is triggered at + :param str [params.marginMode]: 'cross' or 'isolated' - marginMode for margin trading if not provided self.options['defaultMarginMode'] is used + :param int [params.iceberg]: Amount to display for the iceberg order, Null or 0 for normal orders, Set to -1 to hide the order completely + :param str [params.text]: User defined information + :param str [params.account]: *spot and margin only* "spot", "margin" or "cross_margin" + :param bool [params.auto_borrow]: *margin only* Used in margin or cross margin trading to allow automatic loan of insufficient amount if balance is not enough + :param str [params.settle]: *contract only* Unified Currency Code for settle currency + :param bool [params.reduceOnly]: *contract only* Indicates if self order is to reduce the size of a position + :param bool [params.close]: *contract only* Set to close the position, with size set to 0 + :param bool [params.auto_size]: *contract only* Set side to close dual-mode position, close_long closes the long side, while close_short the short one, size also needs to be set to 0 + :param int [params.price_type]: *contract only* 0 latest deal price, 1 mark price, 2 index price + :param float [params.cost]: *spot market buy only* the quote quantity that can be used alternative for the amount + :param bool [params.unifiedAccount]: set to True for creating an order in the unified account + :returns dict|None: `An order structure ` + """ + await self.load_markets() + await self.load_unified_status() + market = self.market(symbol) + trigger = self.safe_value(params, 'trigger') + triggerPrice = self.safe_value_2(params, 'triggerPrice', 'stopPrice') + stopLossPrice = self.safe_value(params, 'stopLossPrice', triggerPrice) + takeProfitPrice = self.safe_value(params, 'takeProfitPrice') + isStopLossOrder = stopLossPrice is not None + isTakeProfitOrder = takeProfitPrice is not None + isTpsl = isStopLossOrder or isTakeProfitOrder + nonTriggerOrder = not isTpsl and (trigger is None) + orderRequest = self.create_order_request(symbol, type, side, amount, price, params) + response = None + if market['spot'] or market['margin']: + if nonTriggerOrder: + response = await self.privateSpotPostOrders(orderRequest) + else: + response = await self.privateSpotPostPriceOrders(orderRequest) + elif market['swap']: + if nonTriggerOrder: + response = await self.privateFuturesPostSettleOrders(orderRequest) + else: + response = await self.privateFuturesPostSettlePriceOrders(orderRequest) + elif market['future']: + if nonTriggerOrder: + response = await self.privateDeliveryPostSettleOrders(orderRequest) + else: + response = await self.privateDeliveryPostSettlePriceOrders(orderRequest) + else: + response = await self.privateOptionsPostOrders(orderRequest) + # response = await getattr(self, method)(self.deep_extend(request, params)) + # + # spot + # + # { + # "id": "95282841887", + # "text": "apiv4", + # "create_time": "1637383156", + # "update_time": "1637383156", + # "create_time_ms": 1637383156017, + # "update_time_ms": 1637383156017, + # "status": "open", + # "currency_pair": "ETH_USDT", + # "type": "limit", + # "account": "spot", + # "side": "buy", + # "amount": "0.01", + # "price": "3500", + # "time_in_force": "gtc", + # "iceberg": "0", + # "left": "0.01", + # "fill_price": "0", + # "filled_total": "0", + # "fee": "0", + # "fee_currency": "ETH", + # "point_fee": "0", + # "gt_fee": "0", + # "gt_discount": False, + # "rebated_fee": "0", + # "rebated_fee_currency": "USDT" + # } + # + # spot conditional + # + # {"id": 5891843} + # + # futures, perpetual swaps and options + # + # { + # "id": 95938572327, + # "contract": "ETH_USDT", + # "mkfr": "0", + # "tkfr": "0.0005", + # "tif": "gtc", + # "is_reduce_only": False, + # "create_time": 1637384600.08, + # "price": "3000", + # "size": 1, + # "refr": "0", + # "left": 1, + # "text": "api", + # "fill_price": "0", + # "user": 2436035, + # "status": "open", + # "is_liq": False, + # "refu": 0, + # "is_close": False, + # "iceberg": 0 + # } + # + # futures and perpetual swaps conditionals + # + # {"id": 7615567} + # + return self.parse_order(response, market) + + def create_orders_request(self, orders: List[OrderRequest], params={}): + ordersRequests = [] + orderSymbols = [] + ordersLength = len(orders) + if ordersLength == 0: + raise BadRequest(self.id + ' createOrders() requires at least one order') + if ordersLength > 10: + raise BadRequest(self.id + ' createOrders() accepts a maximum of 10 orders at a time') + for i in range(0, len(orders)): + rawOrder = orders[i] + marketId = self.safe_string(rawOrder, 'symbol') + orderSymbols.append(marketId) + type = self.safe_string(rawOrder, 'type') + side = self.safe_string(rawOrder, 'side') + amount = self.safe_value(rawOrder, 'amount') + price = self.safe_value(rawOrder, 'price') + orderParams = self.safe_value(rawOrder, 'params', {}) + extendedParams = self.extend(orderParams, params) # the request does not accept extra params since it's a list, so we're extending each order with the common params + triggerValue = self.safe_value_n(orderParams, ['triggerPrice', 'stopPrice', 'takeProfitPrice', 'stopLossPrice']) + if triggerValue is not None: + raise NotSupported(self.id + ' createOrders() does not support advanced order properties(stopPrice, takeProfitPrice, stopLossPrice)') + extendedParams['textIsRequired'] = True # the exchange requires a text parameter for each order here + orderRequest = self.create_order_request(marketId, type, side, amount, price, extendedParams) + ordersRequests.append(orderRequest) + symbols = self.market_symbols(orderSymbols, None, False, True, True) + market = self.market(symbols[0]) + if market['future'] or market['option']: + raise NotSupported(self.id + ' createOrders() does not support futures or options markets') + return ordersRequests + + async def create_orders(self, orders: List[OrderRequest], params={}): + """ + create a list of trade orders + + https://www.gate.com/docs/developers/apiv4/en/#get-a-single-order-2 + https://www.gate.com/docs/developers/apiv4/en/#create-a-batch-of-orders + https://www.gate.com/docs/developers/apiv4/en/#create-a-batch-of-futures-orders + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + await self.load_unified_status() + ordersRequests = self.create_orders_request(orders, params) + firstOrder = orders[0] + market = self.market(firstOrder['symbol']) + response = None + if market['spot']: + response = await self.privateSpotPostBatchOrders(ordersRequests) + elif market['swap']: + response = await self.privateFuturesPostSettleBatchOrders(ordersRequests) + return self.parse_orders(response) + + def create_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + market = self.market(symbol) + contract = market['contract'] + trigger = self.safe_value(params, 'trigger') + triggerPrice = self.safe_value_2(params, 'triggerPrice', 'stopPrice') + stopLossPrice = self.safe_value(params, 'stopLossPrice', triggerPrice) + takeProfitPrice = self.safe_value(params, 'takeProfitPrice') + isStopLossOrder = stopLossPrice is not None + isTakeProfitOrder = takeProfitPrice is not None + isTpsl = isStopLossOrder or isTakeProfitOrder + if isStopLossOrder and isTakeProfitOrder: + raise ExchangeError(self.id + ' createOrder() stopLossPrice and takeProfitPrice cannot both be defined') + reduceOnly = self.safe_value(params, 'reduceOnly') + exchangeSpecificTimeInForce = self.safe_string_lower_n(params, ['timeInForce', 'tif', 'time_in_force']) + postOnly = None + postOnly, params = self.handle_post_only(type == 'market', exchangeSpecificTimeInForce == 'poc', params) + timeInForce = self.handle_time_in_force(params) + if postOnly: + timeInForce = 'poc' + # we only omit the unified params here + # self is because the other params will get extended into the request + params = self.omit(params, ['stopPrice', 'triggerPrice', 'stopLossPrice', 'takeProfitPrice', 'reduceOnly', 'timeInForce', 'postOnly']) + isLimitOrder = (type == 'limit') + isMarketOrder = (type == 'market') + if isLimitOrder and price is None: + raise ArgumentsRequired(self.id + ' createOrder() requires a price argument for ' + type + ' orders') + if isMarketOrder: + if (timeInForce == 'poc') or (timeInForce == 'gtc'): + raise ExchangeError(self.id + ' createOrder() timeInForce for market order can only be "FOK" or "IOC"') + else: + if timeInForce is None: + defaultTif = self.safe_string(self.options, 'defaultTimeInForce', 'IOC') + exchangeSpecificTif = self.safe_string(self.options['timeInForce'], defaultTif, 'ioc') + timeInForce = exchangeSpecificTif + if contract: + price = 0 + if contract: + isClose = self.safe_value(params, 'close') + if isClose: + amount = 0 + else: + amountToPrecision = self.amount_to_precision(symbol, amount) + signedAmount = Precise.string_neg(amountToPrecision) if (side == 'sell') else amountToPrecision + amount = int(signedAmount) + request = None + nonTriggerOrder = not isTpsl and (trigger is None) + if nonTriggerOrder: + if contract: + # contract order + request = { + 'contract': market['id'], # filled in prepareRequest above + 'size': amount, # int64, positive = bid, negative = ask + # 'iceberg': 0, # int64, display size for iceberg order, 0 for non-iceberg, note that you will have to pay the taker fee for the hidden size + # 'close': False, # True to close the position, with size set to 0 + # 'reduce_only': False, # St to be reduce-only order + # 'tif': 'gtc', # gtc, ioc, poc PendingOrCancelled == postOnly order + # 'text': clientOrderId, # 't-abcdef1234567890', + # 'auto_size': '', # close_long, close_short, note size also needs to be set to 0 + } + if not market['option']: + request['settle'] = market['settleId'] # filled in prepareRequest above + if isMarketOrder: + request['price'] = '0' # set to 0 for market orders + else: + request['price'] = '0' if (price == 0) else self.price_to_precision(symbol, price) + if reduceOnly is not None: + request['reduce_only'] = reduceOnly + if timeInForce is not None: + request['tif'] = timeInForce + else: + marginMode = None + marginMode, params = self.get_margin_mode(False, params) + # spot order + request = { + # 'text': clientOrderId, # 't-abcdef1234567890', + 'currency_pair': market['id'], # filled in prepareRequest above + 'type': type, + 'account': marginMode, # spot, margin, cross_margin, unified + 'side': side, + # 'time_in_force': 'gtc', # gtc, ioc, poc PendingOrCancelled == postOnly order + # 'iceberg': 0, # amount to display for the iceberg order, null or 0 for normal orders, set to -1 to hide the order completely + # 'auto_borrow': False, # used in margin or cross margin trading to allow automatic loan of insufficient amount if balance is not enough + # 'auto_repay': False, # automatic repayment for automatic borrow loan generated by cross margin order, diabled by default + } + if isMarketOrder and (side == 'buy'): + quoteAmount = None + createMarketBuyOrderRequiresPrice = True + createMarketBuyOrderRequiresPrice, params = self.handle_option_and_params(params, 'createOrder', 'createMarketBuyOrderRequiresPrice', True) + cost = self.safe_number(params, 'cost') + params = self.omit(params, 'cost') + if cost is not None: + quoteAmount = self.cost_to_precision(symbol, cost) + elif createMarketBuyOrderRequiresPrice: + if price is None: + raise InvalidOrder(self.id + ' createOrder() requires the price argument for market buy orders to calculate the total cost to spend(amount * price), alternatively set the createMarketBuyOrderRequiresPrice option or param to False and pass the cost to spend(quote quantity) in the amount argument') + else: + amountString = self.number_to_string(amount) + priceString = self.number_to_string(price) + costRequest = Precise.string_mul(amountString, priceString) + quoteAmount = self.cost_to_precision(symbol, costRequest) + else: + quoteAmount = self.cost_to_precision(symbol, amount) + request['amount'] = quoteAmount + else: + request['amount'] = self.amount_to_precision(symbol, amount) + if isLimitOrder: + request['price'] = self.price_to_precision(symbol, price) + if timeInForce is not None: + request['time_in_force'] = timeInForce + clientOrderId = self.safe_string_2(params, 'text', 'clientOrderId') + textIsRequired = self.safe_bool(params, 'textIsRequired', False) + if clientOrderId is not None: + # user-defined, must follow the rules if not empty + # prefixed with t- + # no longer than 28 bytes without t- prefix + # can only include 0-9, A-Z, a-z, underscores(_), hyphens(-) or dots(.) + if len(clientOrderId) > 28: + raise BadRequest(self.id + ' createOrder() clientOrderId or text param must be up to 28 characters') + params = self.omit(params, ['text', 'clientOrderId', 'textIsRequired']) + if clientOrderId[0] != 't': + clientOrderId = 't-' + clientOrderId + request['text'] = clientOrderId + else: + if textIsRequired: + # batchOrders requires text in the request + request['text'] = 't-' + self.uuid16() + else: + if market['option']: + raise NotSupported(self.id + ' createOrder() conditional option orders are not supported') + if contract: + # contract conditional order + request = { + 'initial': { + 'contract': market['id'], + 'size': amount, # positive = buy, negative = sell, set to 0 to close the position + # 'price': '0' if (price == 0) else self.price_to_precision(symbol, price), # set to 0 to use market price + # 'close': False, # set to True if trying to close the position + # 'tif': 'gtc', # gtc, ioc, if using market price, only ioc is supported + # 'text': clientOrderId, # web, api, app + # 'reduce_only': False, + }, + 'settle': market['settleId'], + } + if type == 'market': + request['initial']['price'] = '0' + else: + request['initial']['price'] = '0' if (price == 0) else self.price_to_precision(symbol, price) + if trigger is None: + rule = None + triggerOrderPrice = None + if isStopLossOrder: + # we trigger orders be aliases for stopLoss orders because + # gateio doesn't accept conventional trigger orders for spot markets + rule = 1 if (side == 'buy') else 2 + triggerOrderPrice = self.price_to_precision(symbol, stopLossPrice) + elif isTakeProfitOrder: + rule = 2 if (side == 'buy') else 1 + triggerOrderPrice = self.price_to_precision(symbol, takeProfitPrice) + priceType = self.safe_integer(params, 'price_type', 0) + if priceType < 0 or priceType > 2: + raise BadRequest(self.id + ' createOrder() price_type should be 0 latest deal price, 1 mark price, 2 index price') + params = self.omit(params, ['price_type']) + request['trigger'] = { + # 'strategy_type': 0, # 0 = by price, 1 = by price gap, only 0 is supported currently + 'price_type': priceType, # 0 latest deal price, 1 mark price, 2 index price + 'price': self.price_to_precision(symbol, triggerOrderPrice), # price or gap + 'rule': rule, # 1 means price_type >= price, 2 means price_type <= price + # 'expiration': expiration, how many seconds to wait for the condition to be triggered before cancelling the order + } + if reduceOnly is not None: + request['initial']['reduce_only'] = reduceOnly + if timeInForce is not None: + request['initial']['tif'] = timeInForce + else: + # spot conditional order + options = self.safe_value(self.options, 'createOrder', {}) + marginMode = None + marginMode, params = self.get_margin_mode(True, params) + if timeInForce is None: + timeInForce = 'gtc' + request = { + 'put': { + 'type': type, + 'side': side, + 'price': self.price_to_precision(symbol, price), + 'amount': self.amount_to_precision(symbol, amount), + 'account': marginMode, + 'time_in_force': timeInForce, # gtc, ioc(ioc is for taker only, so shouldnt't be in conditional order) + }, + 'market': market['id'], + } + if trigger is None: + defaultExpiration = self.safe_integer(options, 'expiration') + expiration = self.safe_integer(params, 'expiration', defaultExpiration) + rule = None + triggerOrderPrice = None + if isStopLossOrder: + # we trigger orders be aliases for stopLoss orders because + # gateio doesn't accept conventional trigger orders for spot markets + rule = '>=' if (side == 'buy') else '<=' + triggerOrderPrice = self.price_to_precision(symbol, stopLossPrice) + elif isTakeProfitOrder: + rule = '<=' if (side == 'buy') else '>=' + triggerOrderPrice = self.price_to_precision(symbol, takeProfitPrice) + request['trigger'] = { + 'price': self.price_to_precision(symbol, triggerOrderPrice), + 'rule': rule, # >= triggered when market price larger than or equal to price field, <= triggered when market price less than or equal to price field + 'expiration': expiration, # required, how long(in seconds) to wait for the condition to be triggered before cancelling the order + } + return self.extend(request, params) + + async def create_market_buy_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market buy order by providing the symbol and cost + + https://www.gate.com/docs/developers/apiv4/en/#create-an-order + + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.unifiedAccount]: set to True for creating a unified account order + :returns dict: an `order structure ` + """ + await self.load_markets() + await self.load_unified_status() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' createMarketBuyOrderWithCost() supports spot orders only') + params['createMarketBuyOrderRequiresPrice'] = False + return await self.create_order(symbol, 'market', 'buy', cost, None, params) + + def edit_order_request(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + market = self.market(symbol) + marketType = None + marketType, params = self.handle_market_type_and_params('editOrder', market, params) + account = self.convert_type_to_account(marketType) + isUnifiedAccount = False + isUnifiedAccount, params = self.handle_option_and_params(params, 'editOrder', 'unifiedAccount') + if isUnifiedAccount: + account = 'unified' + isLimitOrder = (type == 'limit') + if account == 'spot': + if not isLimitOrder: + # exchange doesn't have market orders for spot + raise InvalidOrder(self.id + ' editOrder() does not support ' + type + ' orders for ' + marketType + ' markets') + request: dict = { + 'order_id': str(id), + 'currency_pair': market['id'], + 'account': account, + } + if amount is not None: + if market['spot']: + request['amount'] = self.amount_to_precision(symbol, amount) + else: + if side == 'sell': + request['size'] = self.parse_to_numeric(Precise.string_neg(self.amount_to_precision(symbol, amount))) + else: + request['size'] = self.parse_to_numeric(self.amount_to_precision(symbol, amount)) + if price is not None: + request['price'] = self.price_to_precision(symbol, price) + if not market['spot']: + request['settle'] = market['settleId'] + return self.extend(request, params) + + async def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + """ + edit a trade order, gate currently only supports the modification of the price or amount fields + + https://www.gate.com/docs/developers/apiv4/en/#amend-an-order + https://www.gate.com/docs/developers/apiv4/en/#amend-an-order-2 + + :param str id: order id + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of the currency you want to trade in units of the base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.unifiedAccount]: set to True for editing an order in a unified account + :returns dict: an `order structure ` + """ + await self.load_markets() + await self.load_unified_status() + market = self.market(symbol) + extendedRequest = self.edit_order_request(id, symbol, type, side, amount, price, params) + response = None + if market['spot']: + response = await self.privateSpotPatchOrdersOrderId(extendedRequest) + else: + response = await self.privateFuturesPutSettleOrdersOrderId(extendedRequest) + # + # { + # "id": "243233276443", + # "text": "apiv4", + # "create_time": "1670908873", + # "update_time": "1670914102", + # "create_time_ms": 1670908873077, + # "update_time_ms": 1670914102241, + # "status": "open", + # "currency_pair": "ADA_USDT", + # "type": "limit", + # "account": "spot", + # "side": "sell", + # "amount": "10", + # "price": "0.6", + # "time_in_force": "gtc", + # "iceberg": "0", + # "left": "10", + # "fill_price": "0", + # "filled_total": "0", + # "fee": "0", + # "fee_currency": "USDT", + # "point_fee": "0", + # "gt_fee": "0", + # "gt_maker_fee": "0", + # "gt_taker_fee": "0", + # "gt_discount": False, + # "rebated_fee": "0", + # "rebated_fee_currency": "ADA" + # } + # + return self.parse_order(response, market) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'open': 'open', + '_new': 'open', + 'filled': 'closed', + 'cancelled': 'canceled', + 'liquidated': 'closed', + 'ioc': 'canceled', + 'failed': 'canceled', + 'expired': 'canceled', + 'finished': 'closed', + 'finish': 'closed', + 'succeeded': 'closed', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # SPOT + # createOrder/cancelOrder/fetchOrder/editOrder + # + # { + # "id": "62364648575", + # "text": "apiv4", + # "create_time": "1626354834", + # "update_time": "1626354834", + # "create_time_ms": "1626354833544", + # "update_time_ms": "1626354833544", + # "status": "open", + # "currency_pair": "BTC_USDT", + # "type": "limit", + # "account": "spot", + # "side": "buy", + # "amount": "0.0001", + # "price": "30000", + # "time_in_force": "gtc", + # "iceberg": "0", + # "left": "0.0001", + # "fill_price": "0", + # "filled_total": "0", + # "fee": "0", + # "fee_currency": "BTC", + # "point_fee": "0", + # "gt_fee": "0", + # "gt_discount": True, + # "rebated_fee": "0", + # "rebated_fee_currency": "USDT" + # } + # + # SPOT TRIGGER ORDERS + # createOrder + # + # { + # "id": 12604556 + # } + # + # fetchOrder/cancelOrder + # + # { + # "market": "ADA_USDT", + # "user": 6392049, + # "trigger": { + # "price": "1.08", # stopPrice + # "rule": "\u003e=", + # "expiration": 86400 + # }, + # "put": { + # "type": "limit", + # "side": "buy", + # "price": "1.08", # order price + # "amount": "1.00000000000000000000", + # "account": "normal", + # "time_in_force": "gtc" + # }, + # "id": 71639298, + # "ctime": 1643945985, + # "status": "open" + # } + # + # FUTURE, SWAP AND OPTION + # createOrder/cancelOrder/fetchOrder + # + # { + # "id": 123028481731, + # "contract": "ADA_USDT", + # "mkfr": "-0.00005", + # "tkfr": "0.00048", + # "tif": "ioc", + # "is_reduce_only": False, + # "create_time": 1643950262.68, + # "finish_time": 1643950262.68, + # "price": "0", + # "size": 1, + # "refr": "0", + # "left":0, + # "text": "api", + # "fill_price": "1.05273", + # "user":6329238, + # "finish_as": "filled", + # "status": "finished", + # "is_liq": False, + # "refu":0, + # "is_close": False, + # "iceberg": 0 + # } + # + # TRIGGER ORDERS(FUTURE AND SWAP) + # createOrder + # + # { + # "id": 12604556 + # } + # + # fetchOrder/cancelOrder + # + # { + # "user": 6320300, + # "trigger": { + # "strategy_type": 0, + # "price_type": 0, + # "price": "1.03", # stopPrice + # "rule": 2, + # "expiration": 0 + # }, + # "initial": { + # "contract": "ADA_USDT", + # "size": -1, + # "price": "1.02", + # "tif": "gtc", + # "text": "", + # "iceberg": 0, + # "is_close": False, + # "is_reduce_only": False, + # "auto_size": "" + # }, + # "id": 126393906, + # "trade_id": 0, + # "status": "open", + # "reason": "", + # "create_time": 1643953482, + # "finish_time": 1643953482, + # "is_stop_order": False, + # "stop_trigger": { + # "rule": 0, + # "trigger_price": "", + # "order_price": "" + # }, + # "me_order_id": 0, + # "order_type": "" + # } + # + # { + # "text": "t-d18baf9ac44d82e2", + # "succeeded": False, + # "label": "BALANCE_NOT_ENOUGH", + # "message": "Not enough balance" + # } + # + # {"user_id":10406147,"id":"id","succeeded":false,"message":"INVALID_PROTOCOL","label":"INVALID_PROTOCOL"} + # + succeeded = self.safe_bool(order, 'succeeded', True) + if not succeeded: + # cancelOrders response + return self.safe_order({ + 'clientOrderId': self.safe_string(order, 'text'), + 'info': order, + 'status': 'rejected', + 'id': self.safe_string(order, 'id'), + }) + put = self.safe_value_2(order, 'put', 'initial', {}) + trigger = self.safe_value(order, 'trigger', {}) + contract = self.safe_string(put, 'contract') + type = self.safe_string(put, 'type') + timeInForce = self.safe_string_upper_2(put, 'time_in_force', 'tif') + amount = self.safe_string_2(put, 'amount', 'size') + side = self.safe_string(put, 'side') + price = self.safe_string(put, 'price') + contract = self.safe_string(order, 'contract', contract) + type = self.safe_string(order, 'type', type) + timeInForce = self.safe_string_upper_2(order, 'time_in_force', 'tif', timeInForce) + if timeInForce == 'POC': + timeInForce = 'PO' + postOnly = (timeInForce == 'PO') + amount = self.safe_string_2(order, 'amount', 'size', amount) + side = self.safe_string(order, 'side', side) + price = self.safe_string(order, 'price', price) + remainingString = self.safe_string(order, 'left') + cost = self.safe_string(order, 'filled_total') + triggerPrice = self.safe_number(trigger, 'price') + average = self.safe_number_2(order, 'avg_deal_price', 'fill_price') + if triggerPrice: + remainingString = amount + cost = '0' + if contract: + isMarketOrder = Precise.string_equals(price, '0') and (timeInForce == 'IOC') + type = 'market' if isMarketOrder else 'limit' + side = 'buy' if Precise.string_gt(amount, '0') else 'sell' + rawStatus = self.safe_string_n(order, ['finish_as', 'status', 'open']) + timestamp = self.safe_integer(order, 'create_time_ms') + if timestamp is None: + timestamp = self.safe_timestamp_2(order, 'create_time', 'ctime') + lastTradeTimestamp = self.safe_integer(order, 'update_time_ms') + if lastTradeTimestamp is None: + lastTradeTimestamp = self.safe_timestamp_2(order, 'update_time', 'finish_time') + marketType = 'contract' + if ('currency_pair' in order) or ('market' in order): + marketType = 'spot' + exchangeSymbol = self.safe_string_2(order, 'currency_pair', 'market', contract) + symbol = self.safe_symbol(exchangeSymbol, market, '_', marketType) + # Everything below self(above return) is related to fees + fees = [] + gtFee = self.safe_string(order, 'gt_fee') + if gtFee is not None: + fees.append({ + 'currency': 'GT', + 'cost': gtFee, + }) + fee = self.safe_string(order, 'fee') + if fee is not None: + fees.append({ + 'currency': self.safe_currency_code(self.safe_string(order, 'fee_currency')), + 'cost': fee, + }) + rebate = self.safe_string(order, 'rebated_fee') + if rebate is not None: + fees.append({ + 'currency': self.safe_currency_code(self.safe_string(order, 'rebated_fee_currency')), + 'cost': Precise.string_neg(rebate), + }) + numFeeCurrencies = len(fees) + multipleFeeCurrencies = numFeeCurrencies > 1 + status = self.parse_order_status(rawStatus) + remaining = Precise.string_abs(remainingString) + # handle spot market buy + account = self.safe_string(order, 'account') # using self instead of market type because of the conflicting ids + if account == 'spot': + averageString = self.safe_string(order, 'avg_deal_price') + average = self.parse_number(averageString) + if (type == 'market') and (side == 'buy'): + remaining = Precise.string_div(remainingString, averageString) + price = None # arrives + cost = amount + amount = Precise.string_div(amount, averageString) + return self.safe_order({ + 'id': self.safe_string(order, 'id'), + 'clientOrderId': self.safe_string(order, 'text'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': lastTradeTimestamp, + 'status': status, + 'symbol': symbol, + 'type': type, + 'timeInForce': timeInForce, + 'postOnly': postOnly, + 'reduceOnly': self.safe_value(order, 'is_reduce_only'), + 'side': side, + 'price': price, + 'triggerPrice': triggerPrice, + 'average': average, + 'amount': Precise.string_abs(amount), + 'cost': Precise.string_abs(cost), + 'filled': None, + 'remaining': remaining, + 'fee': None if multipleFeeCurrencies else self.safe_value(fees, 0), + 'fees': fees if multipleFeeCurrencies else [], + 'trades': None, + 'info': order, + }, market) + + def fetch_order_request(self, id: str, symbol: Str = None, params={}): + market = None if (symbol is None) else self.market(symbol) + trigger = self.safe_bool_n(params, ['trigger', 'is_stop_order', 'stop'], False) + params = self.omit(params, ['is_stop_order', 'stop', 'trigger']) + clientOrderId = self.safe_string_2(params, 'text', 'clientOrderId') + orderId = id + if clientOrderId is not None: + params = self.omit(params, ['text', 'clientOrderId']) + if clientOrderId[0] != 't': + clientOrderId = 't-' + clientOrderId + orderId = clientOrderId + type, query = self.handle_market_type_and_params('fetchOrder', market, params) + contract = (type == 'swap') or (type == 'future') or (type == 'option') + request, requestParams = self.prepare_request(market, type, query) if contract else self.spot_order_prepare_request(market, trigger, query) + request['order_id'] = str(orderId) + return [request, requestParams] + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + Retrieves information on an order + + https://www.gate.com/docs/developers/apiv4/en/#get-a-single-order + https://www.gate.com/docs/developers/apiv4/en/#get-a-single-order-2 + https://www.gate.com/docs/developers/apiv4/en/#get-a-single-order-3 + https://www.gate.com/docs/developers/apiv4/en/#get-a-single-order-4 + + :param str id: Order id + :param str symbol: Unified market symbol, *required for spot and margin* + :param dict [params]: Parameters specified by the exchange api + :param bool [params.trigger]: True if the order being fetched is a trigger order + :param str [params.marginMode]: 'cross' or 'isolated' - marginMode for margin trading if not provided self.options['defaultMarginMode'] is used + :param str [params.type]: 'spot', 'swap', or 'future', if not provided self.options['defaultMarginMode'] is used + :param str [params.settle]: 'btc' or 'usdt' - settle currency for perpetual swap and future - market settle currency is used if symbol is not None, default="usdt" for swap and "btc" for future + :param bool [params.unifiedAccount]: set to True for fetching a unified account order + :returns: An `order structure ` + """ + await self.load_markets() + await self.load_unified_status() + market = None if (symbol is None) else self.market(symbol) + result = self.handle_market_type_and_params('fetchOrder', market, params) + type = self.safe_string(result, 0) + trigger = self.safe_bool_n(params, ['trigger', 'is_stop_order', 'stop'], False) + request, requestParams = self.fetch_order_request(id, symbol, params) + response = None + if type == 'spot' or type == 'margin': + if trigger: + response = await self.privateSpotGetPriceOrdersOrderId(self.extend(request, requestParams)) + else: + response = await self.privateSpotGetOrdersOrderId(self.extend(request, requestParams)) + elif type == 'swap': + if trigger: + response = await self.privateFuturesGetSettlePriceOrdersOrderId(self.extend(request, requestParams)) + else: + response = await self.privateFuturesGetSettleOrdersOrderId(self.extend(request, requestParams)) + elif type == 'future': + if trigger: + response = await self.privateDeliveryGetSettlePriceOrdersOrderId(self.extend(request, requestParams)) + else: + response = await self.privateDeliveryGetSettleOrdersOrderId(self.extend(request, requestParams)) + elif type == 'option': + response = await self.privateOptionsGetOrdersOrderId(self.extend(request, requestParams)) + else: + raise NotSupported(self.id + ' fetchOrder() not support self market type') + return self.parse_order(response, market) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://www.gate.com/docs/developers/apiv4/en/#list-all-open-orders + https://www.gate.com/docs/developers/apiv4/en/#retrieve-running-auto-order-list + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.trigger]: True for fetching trigger orders + :param str [params.type]: spot, margin, swap or future, if not provided self.options['defaultType'] is used + :param str [params.marginMode]: 'cross' or 'isolated' - marginMode for type='margin', if not provided self.options['defaultMarginMode'] is used + :param bool [params.unifiedAccount]: set to True for fetching unified account orders + :returns Order[]: a list of `order structures ` + """ + return await self.fetch_orders_by_status('open', symbol, since, limit, params) + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://www.gate.com/docs/developers/apiv4/en/#list-orders + https://www.gate.com/docs/developers/apiv4/en/#retrieve-running-auto-order-list + https://www.gate.com/docs/developers/apiv4/en/#list-futures-orders + https://www.gate.com/docs/developers/apiv4/en/#list-all-auto-orders + https://www.gate.com/docs/developers/apiv4/en/#list-futures-orders-2 + https://www.gate.com/docs/developers/apiv4/en/#list-all-auto-orders-2 + https://www.gate.com/docs/developers/apiv4/en/#list-options-orders + https://www.gate.com/docs/developers/apiv4/en/#list-futures-orders-by-time-range + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.trigger]: True for fetching trigger orders + :param str [params.type]: spot, swap or future, if not provided self.options['defaultType'] is used + :param str [params.marginMode]: 'cross' or 'isolated' - marginMode for margin trading if not provided self.options['defaultMarginMode'] is used + :param boolean [params.historical]: *swap only* True for using historical endpoint + :param bool [params.unifiedAccount]: set to True for fetching unified account orders + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + await self.load_unified_status() + until = self.safe_integer(params, 'until') + market = None + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + res = self.handle_market_type_and_params('fetchClosedOrders', market, params) + type = self.safe_string(res, 0) + useHistorical = False + useHistorical, params = self.handle_option_and_params(params, 'fetchClosedOrders', 'historical', False) + if not useHistorical and ((since is None and until is None) or (type != 'swap')): + return await self.fetch_orders_by_status('finished', symbol, since, limit, params) + params = self.omit(params, 'type') + request = {} + request, params = self.prepare_request(market, type, params) + if since is not None: + request['from'] = self.parse_to_int(since / 1000) + if until is not None: + params = self.omit(params, 'until') + request['to'] = self.parse_to_int(until / 1000) + if limit is not None: + request['limit'] = limit + response = await self.privateFuturesGetSettleOrdersTimerange(self.extend(request, params)) + return self.parse_orders(response, market, since, limit) + + def prepare_orders_by_status_request(self, status, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + market = None + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + trigger: Bool = None + trigger, params = self.handle_param_bool_2(params, 'trigger', 'stop') + type: Str = None + type, params = self.handle_market_type_and_params('fetchOrdersByStatus', market, params) + spot = (type == 'spot') or (type == 'margin') + request: dict = {} + request, params = self.multi_order_spot_prepare_request(market, trigger, params) if spot else self.prepare_request(market, type, params) + if spot and trigger: + request = self.omit(request, 'account') + if status == 'closed': + status = 'finished' + request['status'] = status + if limit is not None: + request['limit'] = limit + if spot: + if since is not None: + request['from'] = self.parse_to_int(since / 1000) + until = self.safe_integer(params, 'until') + if until is not None: + params = self.omit(params, 'until') + request['to'] = self.parse_to_int(until / 1000) + lastId, finalParams = self.handle_param_string_2(params, 'lastId', 'last_id') + if lastId is not None: + request['last_id'] = lastId + return [request, finalParams] + + async def fetch_orders_by_status(self, status, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + await self.load_markets() + await self.load_unified_status() + market = None + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + # don't omit here, omits done in prepareOrdersByStatusRequest + trigger: Bool = self.safe_bool_2(params, 'trigger', 'stop') + res = self.handle_market_type_and_params('fetchOrdersByStatus', market, params) + type = self.safe_string(res, 0) + request, requestParams = self.prepare_orders_by_status_request(status, symbol, since, limit, params) + spot = (type == 'spot') or (type == 'margin') + openStatus = (status == 'open') + openSpotOrders = spot and openStatus and not trigger + response = None + if spot: + if not trigger: + if openStatus: + response = await self.privateSpotGetOpenOrders(self.extend(request, requestParams)) + else: + response = await self.privateSpotGetOrders(self.extend(request, requestParams)) + else: + response = await self.privateSpotGetPriceOrders(self.extend(request, requestParams)) + elif type == 'swap': + if trigger: + response = await self.privateFuturesGetSettlePriceOrders(self.extend(request, requestParams)) + else: + response = await self.privateFuturesGetSettleOrders(self.extend(request, requestParams)) + elif type == 'future': + if trigger: + response = await self.privateDeliveryGetSettlePriceOrders(self.extend(request, requestParams)) + else: + response = await self.privateDeliveryGetSettleOrders(self.extend(request, requestParams)) + elif type == 'option': + response = await self.privateOptionsGetOrders(self.extend(request, requestParams)) + else: + raise NotSupported(self.id + ' fetchOrders() not support self market type') + # + # spot open orders + # + # [ + # { + # "currency_pair": "ADA_USDT", + # "total": 2, + # "orders": [ + # { + # "id": "155498539874", + # "text": "apiv4", + # "create_time": "1652406843", + # "update_time": "1652406843", + # "create_time_ms": 1652406843295, + # "update_time_ms": 1652406843295, + # "status": "open", + # "currency_pair": "ADA_USDT", + # "type": "limit", + # "account": "spot", + # "side": "buy", + # "amount": "3", + # "price": "0.35", + # "time_in_force": "gtc", + # "iceberg": "0", + # "left": "3", + # "fill_price": "0", + # "filled_total": "0", + # "fee": "0", + # "fee_currency": "ADA", + # "point_fee": "0", + # "gt_fee": "0", + # "gt_discount": False, + # "rebated_fee": "0", + # "rebated_fee_currency": "USDT" + # }, + # ... + # ] + # }, + # ... + # ] + # + # spot + # + # [ + # { + # "id": "8834234273", + # "text": "3", + # "create_time": "1635406193", + # "update_time": "1635406193", + # "create_time_ms": 1635406193361, + # "update_time_ms": 1635406193361, + # "status": "closed", + # "currency_pair": "BTC_USDT", + # "type": "limit", + # "account": "spot", # margin for margin orders + # "side": "sell", + # "amount": "0.0002", + # "price": "58904.01", + # "time_in_force": "gtc", + # "iceberg": "0", + # "left": "0.0000", + # "fill_price": "11.790516", + # "filled_total": "11.790516", + # "fee": "0.023581032", + # "fee_currency": "USDT", + # "point_fee": "0", + # "gt_fee": "0", + # "gt_discount": False, + # "rebated_fee_currency": "BTC" + # } + # ] + # + # spot trigger + # + # [ + # { + # "market": "ADA_USDT", + # "user": 10406147, + # "trigger": { + # "price": "0.65", + # "rule": "\u003c=", + # "expiration": 86400 + # }, + # "put": { + # "type": "limit", + # "side": "sell", + # "price": "0.65", + # "amount": "2.00000000000000000000", + # "account": "normal", # margin for margin orders + # "time_in_force": "gtc" + # }, + # "id": 8449909, + # "ctime": 1652188982, + # "status": "open" + # } + # ] + # + # swap + # + # [ + # { + # "status": "finished", + # "size": -1, + # "left": 0, + # "id": 82750739203, + # "is_liq": False, + # "is_close": False, + # "contract": "BTC_USDT", + # "text": "web", + # "fill_price": "60721.3", + # "finish_as": "filled", + # "iceberg": 0, + # "tif": "ioc", + # "is_reduce_only": True, + # "create_time": 1635403475.412, + # "finish_time": 1635403475.4127, + # "price": "0" + # } + # ] + # + # option + # + # [ + # { + # "id": 2593450699, + # "contract": "BTC_USDT-20230601-27500-C", + # "mkfr": "0.0003", + # "tkfr": "0.0003", + # "tif": "gtc", + # "is_reduce_only": False, + # "create_time": 1685503873, + # "price": "200", + # "size": 1, + # "refr": "0", + # "left": 1, + # "text": "api", + # "fill_price": "0", + # "user": 5691076, + # "status": "open", + # "is_liq": False, + # "refu": 0, + # "is_close": False, + # "iceberg": 0 + # } + # ] + # + result = response + if openSpotOrders: + result = [] + for i in range(0, len(response)): + ordersInner = self.safe_value(response[i], 'orders') + result = self.array_concat(result, ordersInner) + orders = self.parse_orders(result, market, since, limit) + return self.filter_by_symbol_since_limit(orders, symbol, since, limit) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + Cancels an open order + + https://www.gate.com/docs/developers/apiv4/en/#cancel-a-single-order + https://www.gate.com/docs/developers/apiv4/en/#cancel-a-single-order-2 + https://www.gate.com/docs/developers/apiv4/en/#cancel-a-single-order-3 + https://www.gate.com/docs/developers/apiv4/en/#cancel-a-single-order-4 + + :param str id: Order id + :param str symbol: Unified market symbol + :param dict [params]: Parameters specified by the exchange api + :param bool [params.trigger]: True if the order to be cancelled is a trigger order + :param bool [params.unifiedAccount]: set to True for canceling unified account orders + :returns: An `order structure ` + """ + await self.load_markets() + await self.load_unified_status() + market = None if (symbol is None) else self.market(symbol) + trigger = self.safe_bool_n(params, ['is_stop_order', 'stop', 'trigger'], False) + params = self.omit(params, ['is_stop_order', 'stop', 'trigger']) + type, query = self.handle_market_type_and_params('cancelOrder', market, params) + request, requestParams = self.spot_order_prepare_request(market, trigger, query) if (type == 'spot' or type == 'margin') else self.prepare_request(market, type, query) + request['order_id'] = id + response = None + if type == 'spot' or type == 'margin': + if trigger: + response = await self.privateSpotDeletePriceOrdersOrderId(self.extend(request, requestParams)) + else: + response = await self.privateSpotDeleteOrdersOrderId(self.extend(request, requestParams)) + elif type == 'swap': + if trigger: + response = await self.privateFuturesDeleteSettlePriceOrdersOrderId(self.extend(request, requestParams)) + else: + response = await self.privateFuturesDeleteSettleOrdersOrderId(self.extend(request, requestParams)) + elif type == 'future': + if trigger: + response = await self.privateDeliveryDeleteSettlePriceOrdersOrderId(self.extend(request, requestParams)) + else: + response = await self.privateDeliveryDeleteSettleOrdersOrderId(self.extend(request, requestParams)) + elif type == 'option': + response = await self.privateOptionsDeleteOrdersOrderId(self.extend(request, requestParams)) + else: + raise NotSupported(self.id + ' cancelOrder() not support self market type') + # + # spot + # + # { + # "id": "95282841887", + # "text": "apiv4", + # "create_time": "1637383156", + # "update_time": "1637383235", + # "create_time_ms": 1637383156017, + # "update_time_ms": 1637383235085, + # "status": "cancelled", + # "currency_pair": "ETH_USDT", + # "type": "limit", + # "account": "spot", + # "side": "buy", + # "amount": "0.01", + # "price": "3500", + # "time_in_force": "gtc", + # "iceberg": "0", + # "left": "0.01", + # "fill_price": "0", + # "filled_total": "0", + # "fee": "0", + # "fee_currency": "ETH", + # "point_fee": "0", + # "gt_fee": "0", + # "gt_discount": False, + # "rebated_fee": "0", + # "rebated_fee_currency": "USDT" + # } + # + # spot conditional + # + # { + # "market": "ETH_USDT", + # "user": 2436035, + # "trigger": { + # "price": "3500", + # "rule": "\u003c=", + # "expiration": 86400 + # }, + # "put": { + # "type": "limit", + # "side": "buy", + # "price": "3500", + # "amount": "0.01000000000000000000", + # "account": "normal", + # "time_in_force": "gtc" + # }, + # "id": 5891843, + # "ctime": 1637382379, + # "ftime": 1637382673, + # "status": "canceled" + # } + # + # swap, future and option + # + # { + # "id": "82241928192", + # "contract": "BTC_USDT", + # "mkfr": "0", + # "tkfr": "0.0005", + # "tif": "gtc", + # "is_reduce_only": False, + # "create_time": "1635196145.06", + # "finish_time": "1635196233.396", + # "price": "61000", + # "size": "4", + # "refr": "0", + # "left": "4", + # "text": "web", + # "fill_price": "0", + # "user": "6693577", + # "finish_as": "cancelled", + # "status": "finished", + # "is_liq": False, + # "refu": "0", + # "is_close": False, + # "iceberg": "0", + # } + # + return self.parse_order(response, market) + + async def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): + """ + cancel multiple orders + + https://www.gate.com/docs/developers/apiv4/en/#cancel-a-batch-of-orders-with-an-id-list + https://www.gate.com/docs/developers/apiv4/en/#cancel-a-batch-of-orders-with-an-id-list-2 + + :param str[] ids: order ids + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.unifiedAccount]: set to True for canceling unified account orders + :returns dict: an list of `order structures ` + """ + await self.load_markets() + await self.load_unified_status() + market = None + if symbol is not None: + market = self.market(symbol) + type = None + defaultSettle = 'usdt' if (market is None) else market['settle'] + settle = self.safe_string_lower(params, 'settle', defaultSettle) + type, params = self.handle_market_type_and_params('cancelOrders', market, params) + isSpot = (type == 'spot') + if isSpot and (symbol is None): + raise ArgumentsRequired(self.id + ' cancelOrders requires a symbol argument for spot markets') + if isSpot: + ordersRequests = [] + for i in range(0, len(ids)): + id = ids[i] + orderItem: dict = { + 'id': id, + 'symbol': symbol, + } + ordersRequests.append(orderItem) + return await self.cancel_orders_for_symbols(ordersRequests, params) + request = { + 'settle': settle, + } + finalList = [request] # hacky but needs to be done here + for i in range(0, len(ids)): + finalList.append(ids[i]) + response = await self.privateFuturesPostSettleBatchCancelOrders(finalList) + return self.parse_orders(response) + + async def cancel_orders_for_symbols(self, orders: List[CancellationRequest], params={}): + """ + cancel multiple orders for multiple symbols + + https://www.gate.com/docs/developers/apiv4/en/#cancel-a-batch-of-orders-with-an-id-list + + :param CancellationRequest[] orders: list of order ids with symbol, example [{"id": "a", "symbol": "BTC/USDT"}, {"id": "b", "symbol": "ETH/USDT"}] + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str[] [params.clientOrderIds]: client order ids + :param bool [params.unifiedAccount]: set to True for canceling unified account orders + :returns dict: an list of `order structures ` + """ + await self.load_markets() + await self.load_unified_status() + ordersRequests = [] + for i in range(0, len(orders)): + order = orders[i] + symbol = self.safe_string(order, 'symbol') + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' cancelOrdersForSymbols() supports only spot markets') + id = self.safe_string(order, 'id') + orderItem: dict = { + 'id': id, + 'currency_pair': market['id'], + } + ordersRequests.append(orderItem) + response = await self.privateSpotPostCancelBatchOrders(ordersRequests) + # + # [ + # { + # "currency_pair": "BTC_USDT", + # "id": "123456" + # } + # ] + # + return self.parse_orders(response) + + async def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders + + https://www.gate.com/docs/developers/apiv4/en/#cancel-all-open-orders-in-specified-currency-pair + https://www.gate.com/docs/developers/apiv4/en/#cancel-all-open-orders-matched + https://www.gate.com/docs/developers/apiv4/en/#cancel-all-open-orders-matched-2 + https://www.gate.com/docs/developers/apiv4/en/#cancel-all-open-orders-matched-3 + + :param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.unifiedAccount]: set to True for canceling unified account orders + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + await self.load_unified_status() + market = None if (symbol is None) else self.market(symbol) + trigger = self.safe_bool_2(params, 'stop', 'trigger') + params = self.omit(params, ['stop', 'trigger']) + type, query = self.handle_market_type_and_params('cancelAllOrders', market, params) + request, requestParams = self.multi_order_spot_prepare_request(market, trigger, query) if (type == 'spot') else self.prepare_request(market, type, query) + response = None + if type == 'spot' or type == 'margin': + if trigger: + response = await self.privateSpotDeletePriceOrders(self.extend(request, requestParams)) + else: + response = await self.privateSpotDeleteOrders(self.extend(request, requestParams)) + elif type == 'swap': + if trigger: + response = await self.privateFuturesDeleteSettlePriceOrders(self.extend(request, requestParams)) + else: + response = await self.privateFuturesDeleteSettleOrders(self.extend(request, requestParams)) + elif type == 'future': + if trigger: + response = await self.privateDeliveryDeleteSettlePriceOrders(self.extend(request, requestParams)) + else: + response = await self.privateDeliveryDeleteSettleOrders(self.extend(request, requestParams)) + elif type == 'option': + response = await self.privateOptionsDeleteOrders(self.extend(request, requestParams)) + else: + raise NotSupported(self.id + ' cancelAllOrders() not support self market type') + # + # [ + # { + # "id": 139797004085, + # "contract": "ADA_USDT", + # "mkfr": "0", + # "tkfr": "0.0005", + # "tif": "gtc", + # "is_reduce_only": False, + # "create_time": 1647911169.343, + # "finish_time": 1647911226.849, + # "price": "0.8", + # "size": 1, + # "refr": "0.3", + # "left": 1, + # "text": "api", + # "fill_price": "0", + # "user": 6693577, + # "finish_as": "cancelled", + # "status": "finished", + # "is_liq": False, + # "refu": 2436035, + # "is_close": False, + # "iceberg": 0 + # } + # ... + # ] + # + return self.parse_orders(response, market) + + async def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://www.gate.com/docs/developers/apiv4/en/#transfer-between-trading-accounts + + :param str code: unified currency code for currency being transferred + :param float amount: the amount of currency to transfer + :param str fromAccount: the account to transfer currency from + :param str toAccount: the account to transfer currency to + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.symbol]: Unified market symbol *required for type == margin* + :returns: A `transfer structure ` + """ + await self.load_markets() + currency = self.currency(code) + fromId = self.convert_type_to_account(fromAccount) + toId = self.convert_type_to_account(toAccount) + truncated = self.currency_to_precision(code, amount) + request: dict = { + 'currency': currency['id'], # todo: currencies have network-junctions + 'amount': truncated, + } + if not (fromId in self.options['accountsByType']): + request['from'] = 'margin' + request['currency_pair'] = fromId + else: + request['from'] = fromId + if not (toId in self.options['accountsByType']): + request['to'] = 'margin' + request['currency_pair'] = toId + else: + request['to'] = toId + if fromId == 'margin' or toId == 'margin': + symbol = self.safe_string_2(params, 'symbol', 'currency_pair') + if symbol is None: + raise ArgumentsRequired(self.id + ' transfer requires params["symbol"] for isolated margin transfers') + market = self.market(symbol) + request['currency_pair'] = market['id'] + params = self.omit(params, 'symbol') + if (toId == 'futures') or (toId == 'delivery') or (fromId == 'futures') or (fromId == 'delivery'): + request['settle'] = currency['id'] # todo: currencies have network-junctions + response = await self.privateWalletPostTransfers(self.extend(request, params)) + # + # according to the docs(however actual response seems to be an empty string '') + # + # { + # "currency": "BTC", + # "from": "spot", + # "to": "margin", + # "amount": "1", + # "currency_pair": "BTC_USDT" + # } + # + return self.parse_transfer(response, currency) + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + # + # { + # "currency": "BTC", + # "from": "spot", + # "to": "margin", + # "amount": "1", + # "currency_pair": "BTC_USDT" + # } + # + return { + 'id': self.safe_string(transfer, 'tx_id'), + 'timestamp': None, + 'datetime': None, + 'currency': self.safe_currency_code(None, currency), + 'amount': None, + 'fromAccount': None, + 'toAccount': None, + 'status': None, + 'info': transfer, + } + + async def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://www.gate.com/docs/developers/apiv4/en/#update-position-leverage + https://www.gate.com/docs/developers/apiv4/en/#update-position-leverage-2 + + :param float leverage: the rate of leverage + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setLeverage() requires a symbol argument') + # WARNING: THIS WILL INCREASE LIQUIDATION PRICE FOR OPEN ISOLATED LONG POSITIONS + # AND DECREASE LIQUIDATION PRICE FOR OPEN ISOLATED SHORT POSITIONS + if (leverage < 0) or (leverage > 100): + raise BadRequest(self.id + ' setLeverage() leverage should be between 1 and 100') + await self.load_markets() + market = self.market(symbol) + request, query = self.prepare_request(market, None, params) + defaultMarginMode = self.safe_string_2(self.options, 'marginMode', 'defaultMarginMode') + crossLeverageLimit = self.safe_string(query, 'cross_leverage_limit') + marginMode = self.safe_string(query, 'marginMode', defaultMarginMode) + stringifiedMargin = self.number_to_string(leverage) + if crossLeverageLimit is not None: + marginMode = 'cross' + stringifiedMargin = crossLeverageLimit + if marginMode == 'cross' or marginMode == 'cross_margin': + request['cross_leverage_limit'] = stringifiedMargin + request['leverage'] = '0' + else: + request['leverage'] = stringifiedMargin + response = None + if market['swap']: + response = await self.privateFuturesPostSettlePositionsContractLeverage(self.extend(request, query)) + elif market['future']: + response = await self.privateDeliveryPostSettlePositionsContractLeverage(self.extend(request, query)) + else: + raise NotSupported(self.id + ' setLeverage() not support self market type') + # + # { + # "value": "0", + # "leverage": "5", + # "mode": "single", + # "realised_point": "0", + # "contract": "BTC_USDT", + # "entry_price": "0", + # "mark_price": "62035.86", + # "history_point": "0", + # "realised_pnl": "0", + # "close_order": null, + # "size": 0, + # "cross_leverage_limit": "0", + # "pending_orders": 0, + # "adl_ranking": 6, + # "maintenance_rate": "0.005", + # "unrealised_pnl": "0", + # "user": 2436035, + # "leverage_max": "100", + # "history_pnl": "0", + # "risk_limit": "1000000", + # "margin": "0", + # "last_close_pnl": "0", + # "liq_price": "0" + # } + # + return response + + def parse_position(self, position: dict, market: Market = None): + # + # swap and future + # + # { + # "value": "4.60516", + # "leverage": "0", + # "mode": "single", + # "realised_point": "0", + # "contract": "BTC_USDT", + # "entry_price": "46030.3", + # "mark_price": "46051.6", + # "history_point": "0", + # "realised_pnl": "-0.002301515", + # "close_order": null, + # "size": 1, + # "cross_leverage_limit": "0", + # "pending_orders": 0, + # "adl_ranking": 5, + # "maintenance_rate": "0.004", + # "unrealised_pnl": "0.00213", + # "user": 5691076, + # "leverage_max": "125", + # "history_pnl": "0", + # "risk_limit": "1000000", + # "margin": "8.997698485", + # "last_close_pnl": "0", + # "liq_price": "0", + # "update_time": 1705034246, + # "update_id": 1, + # "initial_margin": "0", + # "maintenance_margin": "0", + # "open_time": 1705034246, + # "trade_max_size": "0" + # } + # + # option + # + # { + # "close_order": null, + # "size": 1, + # "vega": "5.29756", + # "theta": "-98.98917", + # "gamma": "0.00056", + # "delta": "0.68691", + # "contract": "BTC_USDT-20230602-26500-C", + # "entry_price": "529", + # "unrealised_pnl": "-1.0131", + # "user": 5691076, + # "mark_price": "427.69", + # "underlying_price": "26810.2", + # "underlying": "BTC_USDT", + # "realised_pnl": "-0.08042877", + # "mark_iv": "0.4224", + # "pending_orders": 0 + # } + # + # fetchPositionsHistory(swap and future) + # + # { + # "contract": "SLERF_USDT", # Futures contract + # "text": "web", # Text of close order + # "long_price": "0.766306", # When 'side' is 'long,' it indicates the opening average price; when 'side' is 'short,' it indicates the closing average price. + # "pnl": "-23.41702352", # PNL + # "pnl_pnl": "-22.7187", # Position P/L + # "pnl_fee": "-0.06527125", # Transaction Fees + # "pnl_fund": "-0.63305227", # Funding Fees + # "accum_size": "100", + # "time": 1711279263, # Position close time + # "short_price": "0.539119", # When 'side' is 'long,' it indicates the opening average price; when 'side' is 'short,' it indicates the closing average price + # "side": "long", # Position side, long or short + # "max_size": "100", # Max Trade Size + # "first_open_time": 1711037985 # First Open Time + # } + # + contract = self.safe_string(position, 'contract') + market = self.safe_market(contract, market, '_', 'contract') + size = self.safe_string_2(position, 'size', 'accum_size') + side = self.safe_string(position, 'side') + if side is None: + if Precise.string_gt(size, '0'): + side = 'long' + elif Precise.string_lt(size, '0'): + side = 'short' + maintenanceRate = self.safe_string(position, 'maintenance_rate') + notional = self.safe_string(position, 'value') + leverage = self.safe_string(position, 'leverage') + marginMode = None + if leverage is not None: + if leverage == '0': + marginMode = 'cross' + else: + marginMode = 'isolated' + # Initial Position Margin = ( Position Value / Leverage ) + Close Position Fee + # *The default leverage under the full position is the highest leverage in the market. + # *Trading fee is charged Fee Rate(0.075%). + feePaid = self.safe_string(position, 'pnl_fee') + initialMarginString = None + if feePaid is None: + takerFee = '0.00075' + feePaid = Precise.string_mul(takerFee, notional) + initialMarginString = Precise.string_add(Precise.string_div(notional, leverage), feePaid) + timestamp = self.safe_timestamp_2(position, 'open_time', 'first_open_time') + if timestamp == 0: + timestamp = None + return self.safe_position({ + 'info': position, + 'id': None, + 'symbol': self.safe_string(market, 'symbol'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastUpdateTimestamp': self.safe_timestamp_2(position, 'update_time', 'time'), + 'initialMargin': self.parse_number(initialMarginString), + 'initialMarginPercentage': self.parse_number(Precise.string_div(initialMarginString, notional)), + 'maintenanceMargin': self.parse_number(Precise.string_mul(maintenanceRate, notional)), + 'maintenanceMarginPercentage': self.parse_number(maintenanceRate), + 'entryPrice': self.safe_number(position, 'entry_price'), + 'notional': self.parse_number(notional), + 'leverage': self.safe_number(position, 'leverage'), + 'unrealizedPnl': self.safe_number(position, 'unrealised_pnl'), + 'realizedPnl': self.safe_number_2(position, 'realised_pnl', 'pnl'), + 'contracts': self.parse_number(Precise.string_abs(size)), + 'contractSize': self.safe_number(market, 'contractSize'), + 'marginRatio': None, + 'liquidationPrice': self.safe_number(position, 'liq_price'), + 'markPrice': self.safe_number(position, 'mark_price'), + 'lastPrice': None, + 'collateral': self.safe_number(position, 'margin'), + 'marginMode': marginMode, + 'side': side, + 'percentage': None, + 'stopLossPrice': None, + 'takeProfitPrice': None, + }) + + async def fetch_position(self, symbol: str, params={}): + """ + fetch data on an open contract position + + https://www.gate.com/docs/developers/apiv4/en/#get-single-position + https://www.gate.com/docs/developers/apiv4/en/#get-single-position-2 + https://www.gate.com/docs/developers/apiv4/en/#get-specified-contract-position + + :param str symbol: unified market symbol of the market the position is held in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `position structure ` + """ + await self.load_markets() + market = self.market(symbol) + if not market['contract']: + raise BadRequest(self.id + ' fetchPosition() supports contract markets only') + request: dict = {} + request, params = self.prepare_request(market, market['type'], params) + extendedRequest = self.extend(request, params) + response = None + if market['swap']: + response = await self.privateFuturesGetSettlePositionsContract(extendedRequest) + elif market['future']: + response = await self.privateDeliveryGetSettlePositionsContract(extendedRequest) + elif market['type'] == 'option': + response = await self.privateOptionsGetPositionsContract(extendedRequest) + # + # swap and future + # + # { + # "value": "4.60516", + # "leverage": "0", + # "mode": "single", + # "realised_point": "0", + # "contract": "BTC_USDT", + # "entry_price": "46030.3", + # "mark_price": "46051.6", + # "history_point": "0", + # "realised_pnl": "-0.002301515", + # "close_order": null, + # "size": 1, + # "cross_leverage_limit": "0", + # "pending_orders": 0, + # "adl_ranking": 5, + # "maintenance_rate": "0.004", + # "unrealised_pnl": "0.00213", + # "user": 5691076, + # "leverage_max": "125", + # "history_pnl": "0", + # "risk_limit": "1000000", + # "margin": "8.997698485", + # "last_close_pnl": "0", + # "liq_price": "0", + # "update_time": 1705034246, + # "update_id": 1, + # "initial_margin": "0", + # "maintenance_margin": "0", + # "open_time": 1705034246, + # "trade_max_size": "0" + # } + # + # option + # + # { + # "close_order": null, + # "size": 1, + # "vega": "5.29756", + # "theta": "-98.98917", + # "gamma": "0.00056", + # "delta": "0.68691", + # "contract": "BTC_USDT-20230602-26500-C", + # "entry_price": "529", + # "unrealised_pnl": "-1.0131", + # "user": 5691076, + # "mark_price": "427.69", + # "underlying_price": "26810.2", + # "underlying": "BTC_USDT", + # "realised_pnl": "-0.08042877", + # "mark_iv": "0.4224", + # "pending_orders": 0 + # } + # + return self.parse_position(response, market) + + async def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://www.gate.com/docs/developers/apiv4/en/#list-all-positions-of-a-user + https://www.gate.com/docs/developers/apiv4/en/#list-all-positions-of-a-user-2 + https://www.gate.com/docs/developers/apiv4/en/#list-user-s-positions-of-specified-underlying + + :param str[]|None symbols: Not used by gate, but parsed internally by CCXT + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.settle]: 'btc' or 'usdt' - settle currency for perpetual swap and future - default="usdt" for swap and "btc" for future + :param str [params.type]: swap, future or option, if not provided self.options['defaultType'] is used + :returns dict[]: a list of `position structure ` + """ + await self.load_markets() + market = None + symbols = self.market_symbols(symbols, None, True, True, True) + if symbols is not None: + symbolsLength = len(symbols) + if symbolsLength > 0: + market = self.market(symbols[0]) + type = None + request: dict = {} + type, params = self.handle_market_type_and_params('fetchPositions', market, params) + if (type is None) or (type == 'spot'): + type = 'swap' # default to swap + if type == 'option': + if symbols is not None: + marketId = market['id'] + optionParts = marketId.split('-') + request['underlying'] = self.safe_string(optionParts, 0) + else: + request, params = self.prepare_request(None, type, params) + response = None + if type == 'swap': + response = await self.privateFuturesGetSettlePositions(self.extend(request, params)) + elif type == 'future': + response = await self.privateDeliveryGetSettlePositions(self.extend(request, params)) + elif type == 'option': + response = await self.privateOptionsGetPositions(self.extend(request, params)) + # + # swap and future + # + # [ + # { + # "value": "4.602828", + # "leverage": "0", + # "mode": "single", + # "realised_point": "0", + # "contract": "BTC_USDT", + # "entry_price": "46030.3", + # "mark_price": "46028.28", + # "history_point": "0", + # "realised_pnl": "-0.002301515", + # "close_order": null, + # "size": 1, + # "cross_leverage_limit": "0", + # "pending_orders": 0, + # "adl_ranking": 5, + # "maintenance_rate": "0.004", + # "unrealised_pnl": "-0.000202", + # "user": 5691076, + # "leverage_max": "125", + # "history_pnl": "0", + # "risk_limit": "1000000", + # "margin": "8.997698485", + # "last_close_pnl": "0", + # "liq_price": "0", + # "update_time": 1705034246, + # "update_id": 1, + # "initial_margin": "0", + # "maintenance_margin": "0", + # "open_time": 1705034246, + # "trade_max_size": "0" + # } + # ] + # + # option + # + # [ + # { + # "close_order": null, + # "size": 0, + # "vega": "0.01907", + # "theta": "-3.04888", + # "gamma": "0.00001", + # "delta": "0.0011", + # "contract": "BTC_USDT-20230601-27500-C", + # "entry_price": "0", + # "unrealised_pnl": "0", + # "user": 5691076, + # "mark_price": "0.07", + # "underlying_price": "26817.27", + # "underlying": "BTC_USDT", + # "realised_pnl": "0", + # "mark_iv": "0.4339", + # "pending_orders": 0 + # } + # ] + # + return self.parse_positions(response, symbols) + + async def fetch_leverage_tiers(self, symbols: Strings = None, params={}) -> LeverageTiers: + """ + retrieve information on the maximum leverage, and maintenance margin for trades of varying trade sizes + + https://www.gate.com/docs/developers/apiv4/en/#list-all-futures-contracts + https://www.gate.com/docs/developers/apiv4/en/#list-all-futures-contracts-2 + + :param str[] [symbols]: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `leverage tiers structures `, indexed by market symbols + """ + await self.load_markets() + type, query = self.handle_market_type_and_params('fetchLeverageTiers', None, params) + request, requestParams = self.prepare_request(None, type, query) + if type != 'future' and type != 'swap': + raise BadRequest(self.id + ' fetchLeverageTiers only supports swap and future') + response = None + if type == 'swap': + response = await self.publicFuturesGetSettleContracts(self.extend(request, requestParams)) + elif type == 'future': + response = await self.publicDeliveryGetSettleContracts(self.extend(request, requestParams)) + else: + raise NotSupported(self.id + ' fetchLeverageTiers() not support self market type') + # + # Perpetual swap + # + # [ + # { + # "name": "BTC_USDT", + # "type": "direct", + # "quanto_multiplier": "0.0001", + # "ref_discount_rate": "0", + # "order_price_deviate": "0.5", + # "maintenance_rate": "0.005", + # "mark_type": "index", + # "last_price": "38026", + # "mark_price": "37985.6", + # "index_price": "37954.92", + # "funding_rate_indicative": "0.000219", + # "mark_price_round": "0.01", + # "funding_offset": 0, + # "in_delisting": False, + # "risk_limit_base": "1000000", + # "interest_rate": "0.0003", + # "order_price_round": "0.1", + # "order_size_min": 1, + # "ref_rebate_rate": "0.2", + # "funding_interval": 28800, + # "risk_limit_step": "1000000", + # "leverage_min": "1", + # "leverage_max": "100", + # "risk_limit_max": "8000000", + # "maker_fee_rate": "-0.00025", + # "taker_fee_rate": "0.00075", + # "funding_rate": "0.002053", + # "order_size_max": 1000000, + # "funding_next_apply": 1610035200, + # "short_users": 977, + # "config_change_time": 1609899548, + # "trade_size": 28530850594, + # "position_size": 5223816, + # "long_users": 455, + # "funding_impact_value": "60000", + # "orders_limit": 50, + # "trade_id": 10851092, + # "orderbook_id": 2129638396 + # } + # ] + # + # Delivery Futures + # + # [ + # { + # "name": "BTC_USDT_20200814", + # "underlying": "BTC_USDT", + # "cycle": "WEEKLY", + # "type": "direct", + # "quanto_multiplier": "0.0001", + # "mark_type": "index", + # "last_price": "9017", + # "mark_price": "9019", + # "index_price": "9005.3", + # "basis_rate": "0.185095", + # "basis_value": "13.7", + # "basis_impact_value": "100000", + # "settle_price": "0", + # "settle_price_interval": 60, + # "settle_price_duration": 1800, + # "settle_fee_rate": "0.0015", + # "expire_time": 1593763200, + # "order_price_round": "0.1", + # "mark_price_round": "0.1", + # "leverage_min": "1", + # "leverage_max": "100", + # "maintenance_rate": "1000000", + # "risk_limit_base": "140.726652109199", + # "risk_limit_step": "1000000", + # "risk_limit_max": "8000000", + # "maker_fee_rate": "-0.00025", + # "taker_fee_rate": "0.00075", + # "ref_discount_rate": "0", + # "ref_rebate_rate": "0.2", + # "order_price_deviate": "0.5", + # "order_size_min": 1, + # "order_size_max": 1000000, + # "orders_limit": 50, + # "orderbook_id": 63, + # "trade_id": 26, + # "trade_size": 435, + # "position_size": 130, + # "config_change_time": 1593158867, + # "in_delisting": False + # } + # ] + # + return self.parse_leverage_tiers(response, symbols, 'name') + + async def fetch_market_leverage_tiers(self, symbol: str, params={}) -> List[LeverageTier]: + """ + retrieve information on the maximum leverage, and maintenance margin for trades of varying trade sizes for a single market + + https://www.gate.com/docs/developers/apiv4/en/#list-risk-limit-tiers + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `leverage tiers structure ` + """ + await self.load_markets() + market = self.market(symbol) + type, query = self.handle_market_type_and_params('fetchMarketLeverageTiers', market, params) + request, requestParams = self.prepare_request(market, type, query) + if type != 'future' and type != 'swap': + raise BadRequest(self.id + ' fetchMarketLeverageTiers only supports swap and future') + response = await self.publicFuturesGetSettleRiskLimitTiers(self.extend(request, requestParams)) + # + # [ + # { + # "maintenance_rate": "0.004", + # "tier": 1, + # "initial_rate": "0.008", + # "leverage_max": "125", + # "risk_limit": "1000000" + # } + # ] + # + return self.parse_market_leverage_tiers(response, market) + + def parse_emulated_leverage_tiers(self, info, market=None) -> List[LeverageTier]: + marketId = self.safe_string(info, 'name') + maintenanceMarginUnit = self.safe_string(info, 'maintenance_rate') # '0.005', + leverageMax = self.safe_string(info, 'leverage_max') # '100', + riskLimitStep = self.safe_string(info, 'risk_limit_step') # '1000000', + riskLimitMax = self.safe_string(info, 'risk_limit_max') # '16000000', + initialMarginUnit = Precise.string_div('1', leverageMax) + maintenanceMarginRate = maintenanceMarginUnit + initialMarginRatio = initialMarginUnit + floor = '0' + tiers = [] + while(Precise.string_lt(floor, riskLimitMax)): + cap = Precise.string_add(floor, riskLimitStep) + tiers.append({ + 'tier': self.parse_number(Precise.string_div(cap, riskLimitStep)), + 'symbol': self.safe_symbol(marketId, market, None, 'contract'), + 'currency': self.safe_string(market, 'settle'), + 'minNotional': self.parse_number(floor), + 'maxNotional': self.parse_number(cap), + 'maintenanceMarginRate': self.parse_number(maintenanceMarginRate), + 'maxLeverage': self.parse_number(Precise.string_div('1', initialMarginRatio)), + 'info': info, + }) + maintenanceMarginRate = Precise.string_add(maintenanceMarginRate, maintenanceMarginUnit) + initialMarginRatio = Precise.string_add(initialMarginRatio, initialMarginUnit) + floor = cap + return tiers + + def parse_market_leverage_tiers(self, info, market: Market = None) -> List[LeverageTier]: + # + # [ + # { + # "maintenance_rate": "0.004", + # "tier": 1, + # "initial_rate": "0.008", + # "leverage_max": "125", + # "risk_limit": "1000000" + # } + # ] + # + if not isinstance(info, list): + return self.parse_emulated_leverage_tiers(info, market) + minNotional = 0 + tiers = [] + for i in range(0, len(info)): + item = info[i] + maxNotional = self.safe_number(item, 'risk_limit') + tiers.append({ + 'tier': self.sum(i, 1), + 'symbol': market['symbol'], + 'currency': market['base'], + 'minNotional': minNotional, + 'maxNotional': maxNotional, + 'maintenanceMarginRate': self.safe_number(item, 'maintenance_rate'), + 'maxLeverage': self.safe_number(item, 'leverage_max'), + 'info': item, + }) + minNotional = maxNotional + return tiers + + async def repay_isolated_margin(self, symbol: str, code: str, amount, params={}): + """ + repay borrowed margin and interest + + https://www.gate.com/docs/apiv4/en/#repay-a-loan + + :param str symbol: unified market symbol + :param str code: unified currency code of the currency to repay + :param float amount: the amount to repay + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.mode]: 'all' or 'partial' payment mode, extra parameter required for isolated margin + :param str [params.id]: '34267567' loan id, extra parameter required for isolated margin + :returns dict: a `margin loan structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'].upper(), # todo: currencies have network-junctions + 'amount': self.currency_to_precision(code, amount), + } + market = self.market(symbol) + request['currency_pair'] = market['id'] + request['type'] = 'repay' + response = await self.privateMarginPostUniLoans(self.extend(request, params)) + # + # empty response + # + return self.parse_margin_loan(response, currency) + + async def repay_cross_margin(self, code: str, amount, params={}): + """ + repay cross margin borrowed margin and interest + + https://www.gate.com/docs/developers/apiv4/en/#cross-margin-repayments + https://www.gate.com/docs/developers/apiv4/en/#borrow-or-repay + + :param str code: unified currency code of the currency to repay + :param float amount: the amount to repay + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.mode]: 'all' or 'partial' payment mode, extra parameter required for isolated margin + :param str [params.id]: '34267567' loan id, extra parameter required for isolated margin + :param boolean [params.unifiedAccount]: set to True for repaying in the unified account + :returns dict: a `margin loan structure ` + """ + await self.load_markets() + await self.load_unified_status() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'].upper(), # todo: currencies have network-junctions + 'amount': self.currency_to_precision(code, amount), + } + isUnifiedAccount = False + isUnifiedAccount, params = self.handle_option_and_params(params, 'repayCrossMargin', 'unifiedAccount') + response = None + if isUnifiedAccount: + request['type'] = 'repay' + response = await self.privateUnifiedPostLoans(self.extend(request, params)) + else: + response = await self.privateMarginPostCrossRepayments(self.extend(request, params)) + response = self.safe_dict(response, 0) + # + # [ + # { + # "id": "17", + # "create_time": 1620381696159, + # "update_time": 1620381696159, + # "currency": "EOS", + # "amount": "110.553635", + # "text": "web", + # "status": 2, + # "repaid": "110.506649705159", + # "repaid_interest": "0.046985294841", + # "unpaid_interest": "0.0000074393366667" + # } + # ] + # + return self.parse_margin_loan(response, currency) + + async def borrow_isolated_margin(self, symbol: str, code: str, amount: float, params={}): + """ + create a loan to borrow margin + + https://www.gate.com/docs/developers/apiv4/en/#marginuni + + :param str symbol: unified market symbol, required for isolated margin + :param str code: unified currency code of the currency to borrow + :param float amount: the amount to borrow + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.rate]: '0.0002' or '0.002' extra parameter required for isolated margin + :returns dict: a `margin loan structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'].upper(), # todo: currencies have network-junctions + 'amount': self.currency_to_precision(code, amount), + } + response = None + market = self.market(symbol) + request['currency_pair'] = market['id'] + request['type'] = 'borrow' + response = await self.privateMarginPostUniLoans(self.extend(request, params)) + # + # { + # "id": "34267567", + # "create_time": "1656394778", + # "expire_time": "1657258778", + # "status": "loaned", + # "side": "borrow", + # "currency": "USDT", + # "rate": "0.0002", + # "amount": "100", + # "days": 10, + # "auto_renew": False, + # "currency_pair": "LTC_USDT", + # "left": "0", + # "repaid": "0", + # "paid_interest": "0", + # "unpaid_interest": "0.003333333333" + # } + # + return self.parse_margin_loan(response, currency) + + async def borrow_cross_margin(self, code: str, amount: float, params={}): + """ + create a loan to borrow margin + + https://www.gate.com/docs/apiv4/en/#create-a-cross-margin-borrow-loan + https://www.gate.com/docs/developers/apiv4/en/#borrow-or-repay + + :param str code: unified currency code of the currency to borrow + :param float amount: the amount to borrow + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.rate]: '0.0002' or '0.002' extra parameter required for isolated margin + :param boolean [params.unifiedAccount]: set to True for borrowing in the unified account + :returns dict: a `margin loan structure ` + """ + await self.load_markets() + await self.load_unified_status() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'].upper(), # todo: currencies have network-junctions + 'amount': self.currency_to_precision(code, amount), + } + isUnifiedAccount = False + isUnifiedAccount, params = self.handle_option_and_params(params, 'borrowCrossMargin', 'unifiedAccount') + response = None + if isUnifiedAccount: + request['type'] = 'borrow' + response = await self.privateUnifiedPostLoans(self.extend(request, params)) + else: + response = await self.privateMarginPostCrossLoans(self.extend(request, params)) + # + # { + # "id": "17", + # "create_time": 1620381696159, + # "update_time": 1620381696159, + # "currency": "EOS", + # "amount": "110.553635", + # "text": "web", + # "status": 2, + # "repaid": "110.506649705159", + # "repaid_interest": "0.046985294841", + # "unpaid_interest": "0.0000074393366667" + # } + # + return self.parse_margin_loan(response, currency) + + def parse_margin_loan(self, info, currency: Currency = None): + # + # Cross + # + # { + # "id": "17", + # "create_time": 1620381696159, + # "update_time": 1620381696159, + # "currency": "EOS", + # "amount": "110.553635", + # "text": "web", + # "status": 2, + # "repaid": "110.506649705159", + # "repaid_interest": "0.046985294841", + # "unpaid_interest": "0.0000074393366667" + # } + # + # Isolated + # + # { + # "id": "34267567", + # "create_time": "1656394778", + # "expire_time": "1657258778", + # "status": "loaned", + # "side": "borrow", + # "currency": "USDT", + # "rate": "0.0002", + # "amount": "100", + # "days": 10, + # "auto_renew": False, + # "currency_pair": "LTC_USDT", + # "left": "0", + # "repaid": "0", + # "paid_interest": "0", + # "unpaid_interest": "0.003333333333" + # } + # + marginMode = self.safe_string_2(self.options, 'defaultMarginMode', 'marginMode', 'cross') + timestamp = self.safe_integer(info, 'create_time') + if marginMode == 'isolated': + timestamp = self.safe_timestamp(info, 'create_time') + currencyId = self.safe_string(info, 'currency') + marketId = self.safe_string(info, 'currency_pair') + return { + 'id': self.safe_integer(info, 'id'), + 'currency': self.safe_currency_code(currencyId, currency), + 'amount': self.safe_number(info, 'amount'), + 'symbol': self.safe_symbol(marketId, None, '_', 'margin'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'info': info, + } + + async def fetch_borrow_interest(self, code: Str = None, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[BorrowInterest]: + """ + fetch the interest owed by the user for borrowing currency for margin trading + + https://www.gate.com/docs/developers/apiv4/en/#list-interest-records + https://www.gate.com/docs/developers/apiv4/en/#interest-records-for-the-cross-margin-account + https://www.gate.com/docs/developers/apiv4/en/#list-interest-records-2 + + :param str [code]: unified currency code + :param str [symbol]: unified market symbol when fetching interest in isolated markets + :param int [since]: the earliest time in ms to fetch borrow interest for + :param int [limit]: the maximum number of structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.unifiedAccount]: set to True for fetching borrow interest in the unified account + :returns dict[]: a list of `borrow interest structures ` + """ + await self.load_markets() + await self.load_unified_status() + isUnifiedAccount = False + isUnifiedAccount, params = self.handle_option_and_params(params, 'fetchBorrowInterest', 'unifiedAccount') + request: dict = {} + request, params = self.handle_until_option('to', request, params) + currency = None + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + market = None + if symbol is not None: + market = self.market(symbol) + if since is not None: + request['from'] = since + if limit is not None: + request['limit'] = limit + response = None + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchBorrowInterest', params, 'cross') + if isUnifiedAccount: + response = await self.privateUnifiedGetInterestRecords(self.extend(request, params)) + elif marginMode == 'isolated': + if market is not None: + request['currency_pair'] = market['id'] + response = await self.privateMarginGetUniInterestRecords(self.extend(request, params)) + elif marginMode == 'cross': + response = await self.privateMarginGetCrossInterestRecords(self.extend(request, params)) + interest = self.parse_borrow_interests(response, market) + return self.filter_by_currency_since_limit(interest, code, since, limit) + + def parse_borrow_interest(self, info: dict, market: Market = None) -> BorrowInterest: + marketId = self.safe_string(info, 'currency_pair') + market = self.safe_market(marketId, market) + marginMode = 'isolated' if (marketId is not None) else 'cross' + timestamp = self.safe_integer(info, 'create_time') + return { + 'info': info, + 'symbol': self.safe_string(market, 'symbol'), + 'currency': self.safe_currency_code(self.safe_string(info, 'currency')), + 'interest': self.safe_number(info, 'interest'), + 'interestRate': self.safe_number(info, 'actual_rate'), + 'amountBorrowed': None, + 'marginMode': marginMode, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + + def nonce(self): + return self.milliseconds() - self.options['timeDifference'] + + def sign(self, path, api=[], method='GET', params={}, headers=None, body=None): + authentication = api[0] # public, private + type = api[1] # spot, margin, future, delivery + query = self.omit(params, self.extract_params(path)) + containsSettle = path.find('settle') > -1 + if containsSettle and path.endswith('batch_cancel_orders'): # weird check to prevent $settle in php and converting {settle} to array(settle) + # special case where we need to extract the settle from the path + # but the body is an array of strings + settle = self.safe_dict(params, 0) + path = self.implode_params(path, settle) + # remove the first element from params + newParams = [] + anyParams = params + for i in range(1, len(anyParams)): + newParams.append(params[i]) + params = newParams + query = newParams + elif isinstance(params, list): + # endpoints like createOrders use an array instead of an object + # so we infer the settle from one of the elements + # they have to be all the same so relying on the first one is fine + first = self.safe_value(params, 0, {}) + path = self.implode_params(path, first) + else: + path = self.implode_params(path, params) + endPart = '' if (path == '') else ('/' + path) + entirePath = '/' + type + endPart + if (type == 'subAccounts') or (type == 'withdrawals'): + entirePath = endPart + url = self.urls['api'][authentication][type] + if url is None: + raise NotSupported(self.id + ' does not have a testnet for the ' + type + ' market type.') + url += entirePath + if authentication == 'public': + if query: + url += '?' + self.urlencode(query) + else: + self.check_required_credentials() + queryString = '' + requiresURLEncoding = False + if ((type == 'futures') or (type == 'delivery')) and method == 'POST': + pathParts = path.split('/') + secondPart = self.safe_string(pathParts, 1, '') + requiresURLEncoding = (secondPart.find('dual') >= 0) or (secondPart.find('positions') >= 0) + if (method == 'GET') or (method == 'DELETE') or requiresURLEncoding or (method == 'PATCH'): + if query: + queryString = self.urlencode(query) + # https://github.com/ccxt/ccxt/issues/25570 + if queryString.find('currencies=') >= 0 and queryString.find('%2C') >= 0: + queryString = queryString.replace('%2C', ',') + url += '?' + queryString + if method == 'PATCH': + body = self.json(query) + else: + urlQueryParams = self.safe_value(query, 'query', {}) + if urlQueryParams: + queryString = self.urlencode(urlQueryParams) + url += '?' + queryString + query = self.omit(query, 'query') + body = self.json(query) + bodyPayload = '' if (body is None) else body + bodySignature = self.hash(self.encode(bodyPayload), 'sha512') + nonce = self.nonce() + timestamp = self.parse_to_int(nonce / 1000) + timestampString = str(timestamp) + signaturePath = '/api/' + self.version + entirePath + payloadArray = [method.upper(), signaturePath, queryString, bodySignature, timestampString] + # eslint-disable-next-line quotes + payload = "\n".join(payloadArray) + signature = self.hmac(self.encode(payload), self.encode(self.secret), hashlib.sha512) + headers = { + 'KEY': self.apiKey, + 'Timestamp': timestampString, + 'SIGN': signature, + 'Content-Type': 'application/json', + } + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + async def modify_margin_helper(self, symbol: str, amount, params={}): + await self.load_markets() + market = self.market(symbol) + request, query = self.prepare_request(market, None, params) + request['change'] = self.number_to_string(amount) + response = None + if market['swap']: + response = await self.privateFuturesPostSettlePositionsContractMargin(self.extend(request, query)) + elif market['future']: + response = await self.privateDeliveryPostSettlePositionsContractMargin(self.extend(request, query)) + else: + raise NotSupported(self.id + ' modifyMarginHelper() not support self market type') + return self.parse_margin_modification(response, market) + + def parse_margin_modification(self, data: dict, market: Market = None) -> MarginModification: + # + # { + # "value": "11.9257", + # "leverage": "5", + # "mode": "single", + # "realised_point": "0", + # "contract": "ETH_USDT", + # "entry_price": "1203.45", + # "mark_price": "1192.57", + # "history_point": "0", + # "realised_pnl": "-0.00577656", + # "close_order": null, + # "size": "1", + # "cross_leverage_limit": "0", + # "pending_orders": "0", + # "adl_ranking": "5", + # "maintenance_rate": "0.005", + # "unrealised_pnl": "-0.1088", + # "user": "1486602", + # "leverage_max": "100", + # "history_pnl": "0", + # "risk_limit": "1000000", + # "margin": "5.415925875", + # "last_close_pnl": "0", + # "liq_price": "665.69" + # } + # + contract = self.safe_string(data, 'contract') + market = self.safe_market(contract, market, '_', 'contract') + total = self.safe_number(data, 'margin') + return { + 'info': data, + 'symbol': market['symbol'], + 'type': None, + 'marginMode': 'isolated', + 'amount': None, + 'total': total, + 'code': self.safe_value(market, 'quote'), + 'status': 'ok', + 'timestamp': None, + 'datetime': None, + } + + async def reduce_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + remove margin from a position + + https://www.gate.com/docs/developers/apiv4/en/#update-position-margin + https://www.gate.com/docs/developers/apiv4/en/#update-position-margin-2 + + :param str symbol: unified market symbol + :param float amount: the amount of margin to remove + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin structure ` + """ + return await self.modify_margin_helper(symbol, -amount, params) + + async def add_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + add margin + + https://www.gate.com/docs/developers/apiv4/en/#update-position-margin + https://www.gate.com/docs/developers/apiv4/en/#update-position-margin-2 + + :param str symbol: unified market symbol + :param float amount: amount of margin to add + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin structure ` + """ + return await self.modify_margin_helper(symbol, amount, params) + + async def fetch_open_interest_history(self, symbol: str, timeframe='5m', since: Int = None, limit: Int = None, params={}): + """ + Retrieves the open interest of a currency + + https://www.gate.com/docs/developers/apiv4/en/#futures-stats + + :param str symbol: Unified CCXT market symbol + :param str timeframe: "5m", "15m", "30m", "1h", "4h", "1d" + :param int [since]: the time(ms) of the earliest record to retrieve unix timestamp + :param int [limit]: default 30 + :param dict [params]: exchange specific parameters + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict} an open interest structure{@link https://docs.ccxt.com/#/?id=open-interest-structure: + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOpenInterestHistory', 'paginate', False) + if paginate: + return await self.fetch_paginated_call_deterministic('fetchOpenInterestHistory', symbol, since, limit, timeframe, params, 100) + market = self.market(symbol) + if not market['swap']: + raise BadRequest(self.id + ' fetchOpenInterest() supports swap markets only') + request: dict = { + 'contract': market['id'], + 'settle': market['settleId'], + 'interval': self.safe_string(self.timeframes, timeframe, timeframe), + } + if limit is not None: + request['limit'] = limit + if since is not None: + request['from'] = since + response = await self.publicFuturesGetSettleContractStats(self.extend(request, params)) + # + # [ + # { + # "long_liq_size": "0", + # "short_liq_size": "0", + # "short_liq_usd": "0", + # "lsr_account": "3.2808988764045", + # "mark_price": "0.34619", + # "top_lsr_size": "0", + # "time": "1674057000", + # "short_liq_amount": "0", + # "long_liq_amount": "0", + # "open_interest_usd": "9872386.7775", + # "top_lsr_account": "0", + # "open_interest": "2851725", + # "long_liq_usd": "0", + # "lsr_taker": "9.3765153315902" + # }, + # ... + # ] + # + return self.parse_open_interests_history(response, market, since, limit) + + def parse_open_interest(self, interest, market: Market = None): + # + # { + # "long_liq_size": "0", + # "short_liq_size": "0", + # "short_liq_usd": "0", + # "lsr_account": "3.2808988764045", + # "mark_price": "0.34619", + # "top_lsr_size": "0", + # "time": "1674057000", + # "short_liq_amount": "0", + # "long_liq_amount": "0", + # "open_interest_usd": "9872386.7775", + # "top_lsr_account": "0", + # "open_interest": "2851725", + # "long_liq_usd": "0", + # "lsr_taker": "9.3765153315902" + # } + # + timestamp = self.safe_timestamp(interest, 'time') + return { + 'symbol': self.safe_string(market, 'symbol'), + 'openInterestAmount': self.safe_number(interest, 'open_interest'), + 'openInterestValue': self.safe_number(interest, 'open_interest_usd'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'info': interest, + } + + async def fetch_settlement_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical settlement records + + https://www.gate.com/docs/developers/apiv4/en/#list-settlement-history-2 + + :param str symbol: unified market symbol of the settlement history, required on gate + :param int [since]: timestamp in ms + :param int [limit]: number of records + :param dict [params]: exchange specific params + :returns dict[]: a list of `settlement history objects ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchSettlementHistory() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + type = None + type, params = self.handle_market_type_and_params('fetchSettlementHistory', market, params) + if type != 'option': + raise NotSupported(self.id + ' fetchSettlementHistory() supports option markets only') + marketId = market['id'] + optionParts = marketId.split('-') + request: dict = { + 'underlying': self.safe_string(optionParts, 0), + } + if since is not None: + request['from'] = since + if limit is not None: + request['limit'] = limit + response = await self.publicOptionsGetSettlements(self.extend(request, params)) + # + # [ + # { + # "time": 1685952000, + # "profit": "18.266806892718", + # "settle_price": "26826.68068927182", + # "fee": "0.040240021034", + # "contract": "BTC_USDT-20230605-25000-C", + # "strike_price": "25000" + # } + # ] + # + settlements = self.parse_settlements(response, market) + sorted = self.sort_by(settlements, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, symbol, since, limit) + + async def fetch_my_settlement_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical settlement records of the user + + https://www.gate.com/docs/developers/apiv4/en/#list-my-options-settlements + + :param str symbol: unified market symbol of the settlement history + :param int [since]: timestamp in ms + :param int [limit]: number of records + :param dict [params]: exchange specific params + :returns dict[]: a list of [settlement history objects] + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMySettlementHistory() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + type = None + type, params = self.handle_market_type_and_params('fetchMySettlementHistory', market, params) + if type != 'option': + raise NotSupported(self.id + ' fetchMySettlementHistory() supports option markets only') + marketId = market['id'] + optionParts = marketId.split('-') + request: dict = { + 'underlying': self.safe_string(optionParts, 0), + 'contract': marketId, + } + if since is not None: + request['from'] = since + if limit is not None: + request['limit'] = limit + response = await self.privateOptionsGetMySettlements(self.extend(request, params)) + # + # [ + # { + # "size": -1, + # "settle_profit": "0", + # "contract": "BTC_USDT-20220624-26000-C", + # "strike_price": "26000", + # "time": 1656057600, + # "settle_price": "20917.461281337048", + # "underlying": "BTC_USDT", + # "realised_pnl": "-0.00116042", + # "fee": "0" + # } + # ] + # + result = self.safe_value(response, 'result', {}) + data = self.safe_value(result, 'list', []) + settlements = self.parse_settlements(data, market) + sorted = self.sort_by(settlements, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, market['symbol'], since, limit) + + def parse_settlement(self, settlement, market): + # + # fetchSettlementHistory + # + # { + # "time": 1685952000, + # "profit": "18.266806892718", + # "settle_price": "26826.68068927182", + # "fee": "0.040240021034", + # "contract": "BTC_USDT-20230605-25000-C", + # "strike_price": "25000" + # } + # + # fetchMySettlementHistory + # + # { + # "size": -1, + # "settle_profit": "0", + # "contract": "BTC_USDT-20220624-26000-C", + # "strike_price": "26000", + # "time": 1656057600, + # "settle_price": "20917.461281337048", + # "underlying": "BTC_USDT", + # "realised_pnl": "-0.00116042", + # "fee": "0" + # } + # + timestamp = self.safe_timestamp(settlement, 'time') + marketId = self.safe_string(settlement, 'contract') + return { + 'info': settlement, + 'symbol': self.safe_symbol(marketId, market), + 'price': self.safe_number(settlement, 'settle_price'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + + def parse_settlements(self, settlements, market): + # + # fetchSettlementHistory + # + # [ + # { + # "time": 1685952000, + # "profit": "18.266806892718", + # "settle_price": "26826.68068927182", + # "fee": "0.040240021034", + # "contract": "BTC_USDT-20230605-25000-C", + # "strike_price": "25000" + # } + # ] + # + # fetchMySettlementHistory + # + # [ + # { + # "size": -1, + # "settle_profit": "0", + # "contract": "BTC_USDT-20220624-26000-C", + # "strike_price": "26000", + # "time": 1656057600, + # "settle_price": "20917.461281337048", + # "underlying": "BTC_USDT", + # "realised_pnl": "-0.00116042", + # "fee": "0" + # } + # ] + # + result = [] + for i in range(0, len(settlements)): + result.append(self.parse_settlement(settlements[i], market)) + return result + + async def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + + https://www.gate.com/docs/developers/apiv4/en/#query-account-book + https://www.gate.com/docs/developers/apiv4/en/#list-margin-account-balance-change-history + https://www.gate.com/docs/developers/apiv4/en/#query-account-book-2 + https://www.gate.com/docs/developers/apiv4/en/#query-account-book-3 + https://www.gate.com/docs/developers/apiv4/en/#list-account-changing-history + + :param str [code]: unified currency code + :param int [since]: timestamp in ms of the earliest ledger entry + :param int [limit]: max number of ledger entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: end time in ms + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict: a `ledger structure ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchLedger', 'paginate') + if paginate: + return await self.fetch_paginated_call_dynamic('fetchLedger', code, since, limit, params) + type = None + currency = None + response = None + request: dict = {} + type, params = self.handle_market_type_and_params('fetchLedger', None, params) + if (type == 'spot') or (type == 'margin'): + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] # todo: currencies have network-junctions + if (type == 'swap') or (type == 'future'): + defaultSettle = 'usdt' if (type == 'swap') else 'btc' + settle = self.safe_string_lower(params, 'settle', defaultSettle) + params = self.omit(params, 'settle') + request['settle'] = settle + if since is not None: + request['from'] = since + if limit is not None: + request['limit'] = limit + request, params = self.handle_until_option('to', request, params) + if type == 'spot': + response = await self.privateSpotGetAccountBook(self.extend(request, params)) + elif type == 'margin': + response = await self.privateMarginGetAccountBook(self.extend(request, params)) + elif type == 'swap': + response = await self.privateFuturesGetSettleAccountBook(self.extend(request, params)) + elif type == 'future': + response = await self.privateDeliveryGetSettleAccountBook(self.extend(request, params)) + elif type == 'option': + response = await self.privateOptionsGetAccountBook(self.extend(request, params)) + # + # spot + # + # [ + # { + # "id": "123456", + # "time": 1547633726123, + # "currency": "BTC", + # "change": "1.03", + # "balance": "4.59316525194", + # "type": "margin_in" + # } + # ] + # + # margin + # + # [ + # { + # "id": "123456", + # "time": "1547633726", + # "time_ms": 1547633726123, + # "currency": "BTC", + # "currency_pair": "BTC_USDT", + # "change": "1.03", + # "balance": "4.59316525194" + # } + # ] + # + # swap and future + # + # [ + # { + # "time": 1682294400.123456, + # "change": "0.000010152188", + # "balance": "4.59316525194", + # "text": "ETH_USD:6086261", + # "type": "fee" + # } + # ] + # + # option + # + # [ + # { + # "time": 1685594770, + # "change": "3.33", + # "balance": "29.87911771", + # "text": "BTC_USDT-20230602-26500-C:2611026125", + # "type": "prem" + # } + # ] + # + return self.parse_ledger(response, currency, since, limit) + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + # + # spot + # + # { + # "id": "123456", + # "time": 1547633726123, + # "currency": "BTC", + # "change": "1.03", + # "balance": "4.59316525194", + # "type": "margin_in" + # } + # + # margin + # + # { + # "id": "123456", + # "time": "1547633726", + # "time_ms": 1547633726123, + # "currency": "BTC", + # "currency_pair": "BTC_USDT", + # "change": "1.03", + # "balance": "4.59316525194" + # } + # + # swap and future + # + # { + # "time": 1682294400.123456, + # "change": "0.000010152188", + # "balance": "4.59316525194", + # "text": "ETH_USD:6086261", + # "type": "fee" + # } + # + # option + # + # { + # "time": 1685594770, + # "change": "3.33", + # "balance": "29.87911771", + # "text": "BTC_USDT-20230602-26500-C:2611026125", + # "type": "prem" + # } + # + direction = None + amount = self.safe_string(item, 'change') + if Precise.string_lt(amount, '0'): + direction = 'out' + amount = Precise.string_abs(amount) + else: + direction = 'in' + currencyId = self.safe_string(item, 'currency') + currency = self.safe_currency(currencyId, currency) + type = self.safe_string(item, 'type') + rawTimestamp = self.safe_string(item, 'time') + timestamp = None + if len(rawTimestamp) > 10: + timestamp = int(rawTimestamp) + else: + timestamp = int(rawTimestamp) * 1000 + balanceString = self.safe_string(item, 'balance') + changeString = self.safe_string(item, 'change') + before = self.parse_number(Precise.string_sub(balanceString, changeString)) + return self.safe_ledger_entry({ + 'info': item, + 'id': self.safe_string(item, 'id'), + 'direction': direction, + 'account': None, + 'referenceAccount': None, + 'referenceId': None, + 'type': self.parse_ledger_entry_type(type), + 'currency': self.safe_currency_code(currencyId, currency), + 'amount': self.parse_number(amount), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'before': before, + 'after': self.safe_number(item, 'balance'), + 'status': None, + 'fee': None, + }, currency) + + def parse_ledger_entry_type(self, type): + ledgerType: dict = { + 'deposit': 'deposit', + 'withdraw': 'withdrawal', + 'sub_account_transfer': 'transfer', + 'margin_in': 'transfer', + 'margin_out': 'transfer', + 'margin_funding_in': 'transfer', + 'margin_funding_out': 'transfer', + 'cross_margin_in': 'transfer', + 'cross_margin_out': 'transfer', + 'copy_trading_in': 'transfer', + 'copy_trading_out': 'transfer', + 'quant_in': 'transfer', + 'quant_out': 'transfer', + 'futures_in': 'transfer', + 'futures_out': 'transfer', + 'delivery_in': 'transfer', + 'delivery_out': 'transfer', + 'new_order': 'trade', + 'order_fill': 'trade', + 'referral_fee': 'rebate', + 'order_fee': 'fee', + 'interest': 'interest', + 'lend': 'loan', + 'redeem': 'loan', + 'profit': 'interest', + 'flash_swap_buy': 'trade', + 'flash_swap_sell': 'trade', + 'unknown': 'unknown', + 'set': 'settlement', + 'prem': 'trade', + 'point_refr': 'rebate', + 'point_fee': 'fee', + 'point_dnw': 'deposit/withdraw', + 'fund': 'fee', + 'refr': 'rebate', + 'fee': 'fee', + 'pnl': 'trade', + 'dnw': 'deposit/withdraw', + } + return self.safe_string(ledgerType, type, type) + + async def set_position_mode(self, hedged: bool, symbol: Str = None, params={}): + """ + set dual/hedged mode to True or False for a swap market, make sure all positions are closed and no orders are open before setting dual mode + + https://www.gate.com/docs/developers/apiv4/en/#enable-or-disable-dual-mode + + :param bool hedged: set to True to enable dual mode + :param str|None symbol: if passed, dual mode is set for all markets with the same settle currency + :param dict params: extra parameters specific to the exchange API endpoint + :param str params['settle']: settle currency + :returns dict: response from the exchange + """ + market = self.market(symbol) if (symbol is not None) else None + request, query = self.prepare_request(market, 'swap', params) + request['dual_mode'] = hedged + return await self.privateFuturesPostSettleDualMode(self.extend(request, query)) + + async def fetch_underlying_assets(self, params={}): + """ + fetches the market ids of underlying assets for a specific contract market type + + https://www.gate.com/docs/developers/apiv4/en/#list-all-underlyings + + :param dict [params]: exchange specific params + :param str [params.type]: the contract market type, 'option', 'swap' or 'future', the default is 'option' + :returns dict[]: a list of `underlying assets ` + """ + await self.load_markets() + marketType = None + marketType, params = self.handle_market_type_and_params('fetchUnderlyingAssets', None, params) + if (marketType is None) or (marketType == 'spot'): + marketType = 'option' + if marketType != 'option': + raise NotSupported(self.id + ' fetchUnderlyingAssets() supports option markets only') + response = await self.publicOptionsGetUnderlyings(params) + # + # [ + # { + # "index_time": "1646915796", + # "name": "BTC_USDT", + # "index_price": "39142.73" + # } + # ] + # + underlyings = [] + for i in range(0, len(response)): + underlying = response[i] + name = self.safe_string(underlying, 'name') + if name is not None: + underlyings.append(name) + return underlyings + + async def fetch_liquidations(self, symbol: str, since: Int = None, limit: Int = None, params={}): + """ + retrieves the public liquidations of a trading pair + + https://www.gate.com/docs/developers/apiv4/en/#retrieve-liquidation-history + + :param str symbol: unified CCXT market symbol + :param int [since]: the earliest time in ms to fetch liquidations for + :param int [limit]: the maximum number of liquidation structures to retrieve + :param dict [params]: exchange specific parameters for the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest liquidation + :returns dict: an array of `liquidation structures ` + """ + await self.load_markets() + market = self.market(symbol) + if not market['swap']: + raise NotSupported(self.id + ' fetchLiquidations() supports swap markets only') + request: dict = { + 'settle': market['settleId'], + 'contract': market['id'], + } + if since is not None: + request['from'] = since + if limit is not None: + request['limit'] = limit + request, params = self.handle_until_option('to', request, params) + response = await self.publicFuturesGetSettleLiqOrders(self.extend(request, params)) + # + # [ + # { + # "contract": "BTC_USDT", + # "left": 0, + # "size": -165, + # "fill_price": "28070", + # "order_price": "28225", + # "time": 1696736132 + # }, + # ] + # + return self.parse_liquidations(response, market, since, limit) + + async def fetch_my_liquidations(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + retrieves the users liquidated positions + + https://www.gate.com/docs/developers/apiv4/en/#list-liquidation-history + https://www.gate.com/docs/developers/apiv4/en/#list-liquidation-history-2 + https://www.gate.com/docs/developers/apiv4/en/#list-user-s-liquidation-history-of-specified-underlying + + :param str symbol: unified CCXT market symbol + :param int [since]: the earliest time in ms to fetch liquidations for + :param int [limit]: the maximum number of liquidation structures to retrieve + :param dict [params]: exchange specific parameters for the exchange API endpoint + :returns dict: an array of `liquidation structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMyLiquidations() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'contract': market['id'], + } + response = None + if (market['swap']) or (market['future']): + if limit is not None: + request['limit'] = limit + request['settle'] = market['settleId'] + elif market['option']: + marketId = market['id'] + optionParts = marketId.split('-') + request['underlying'] = self.safe_string(optionParts, 0) + if market['swap']: + response = await self.privateFuturesGetSettleLiquidates(self.extend(request, params)) + elif market['future']: + response = await self.privateDeliveryGetSettleLiquidates(self.extend(request, params)) + elif market['option']: + response = await self.privateOptionsGetPositionClose(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchMyLiquidations() does not support ' + market['type'] + ' orders') + # + # swap and future + # + # [ + # { + # "time": 1548654951, + # "contract": "BTC_USDT", + # "size": 600, + # "leverage": "25", + # "margin": "0.006705256878", + # "entry_price": "3536.123", + # "liq_price": "3421.54", + # "mark_price": "3420.27", + # "order_id": 317393847, + # "order_price": "3405", + # "fill_price": "3424", + # "left": 0 + # } + # ] + # + # option + # + # [ + # { + # "time": 1631764800, + # "pnl": "-42914.291", + # "settle_size": "-10001", + # "side": "short", + # "contract": "BTC_USDT-20210916-5000-C", + # "text": "settled" + # } + # ] + # + return self.parse_liquidations(response, market, since, limit) + + def parse_liquidation(self, liquidation, market: Market = None): + # + # fetchLiquidations + # + # { + # "contract": "BTC_USDT", + # "left": 0, + # "size": -165, + # "fill_price": "28070", + # "order_price": "28225", + # "time": 1696736132 + # } + # + # swap and future: fetchMyLiquidations + # + # { + # "time": 1548654951, + # "contract": "BTC_USDT", + # "size": 600, + # "leverage": "25", + # "margin": "0.006705256878", + # "entry_price": "3536.123", + # "liq_price": "3421.54", + # "mark_price": "3420.27", + # "order_id": 317393847, + # "order_price": "3405", + # "fill_price": "3424", + # "left": 0 + # } + # + # option: fetchMyLiquidations + # + # { + # "time": 1631764800, + # "pnl": "-42914.291", + # "settle_size": "-10001", + # "side": "short", + # "contract": "BTC_USDT-20210916-5000-C", + # "text": "settled" + # } + # + marketId = self.safe_string(liquidation, 'contract') + timestamp = self.safe_timestamp(liquidation, 'time') + size = self.safe_string_2(liquidation, 'size', 'settle_size') + left = self.safe_string(liquidation, 'left', '0') + contractsString = Precise.string_abs(Precise.string_sub(size, left)) + contractSizeString = self.safe_string(market, 'contractSize') + priceString = self.safe_string_2(liquidation, 'liq_price', 'fill_price') + baseValueString = Precise.string_mul(contractsString, contractSizeString) + quoteValueString = self.safe_string(liquidation, 'pnl') + if quoteValueString is None: + quoteValueString = Precise.string_mul(baseValueString, priceString) + # --- derive side --- + # 1) options payload has explicit 'side': 'long' | 'short' + optPos = self.safe_string_lower(liquidation, 'side') + side: Str = None + if optPos == 'long': + side = 'buy' + elif optPos == 'short': + side = 'sell' + else: + if size is not None: # 2) futures/perpetual(and fallback for options): infer from size + if Precise.string_gt(size, '0'): + side = 'buy' + elif Precise.string_lt(size, '0'): + side = 'sell' + return self.safe_liquidation({ + 'info': liquidation, + 'symbol': self.safe_symbol(marketId, market), + 'contracts': self.parse_number(contractsString), + 'contractSize': self.parse_number(contractSizeString), + 'price': self.parse_number(priceString), + 'side': side, + 'baseValue': self.parse_number(baseValueString), + 'quoteValue': self.parse_number(Precise.string_abs(quoteValueString)), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + + async def fetch_greeks(self, symbol: str, params={}) -> Greeks: + """ + fetches an option contracts greeks, financial metrics used to measure the factors that affect the price of an options contract + + https://www.gate.com/docs/developers/apiv4/en/#list-tickers-of-options-contracts + + :param str symbol: unified symbol of the market to fetch greeks for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `greeks structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'underlying': market['info']['underlying'], + } + response = await self.publicOptionsGetTickers(self.extend(request, params)) + # + # [ + # { + # "vega": "1.78992", + # "leverage": "6.2096777055417", + # "ask_iv": "0.6245", + # "delta": "-0.69397", + # "last_price": "0", + # "theta": "-2.5723", + # "bid1_price": "222.9", + # "mark_iv": "0.5909", + # "name": "ETH_USDT-20231201-2300-P", + # "bid_iv": "0.5065", + # "ask1_price": "243.6", + # "mark_price": "236.57", + # "position_size": 0, + # "bid1_size": 368, + # "ask1_size": -335, + # "gamma": "0.00116" + # }, + # ] + # + marketId = market['id'] + for i in range(0, len(response)): + entry = response[i] + entryMarketId = self.safe_string(entry, 'name') + if entryMarketId == marketId: + return self.parse_greeks(entry, market) + return None + + def parse_greeks(self, greeks: dict, market: Market = None) -> Greeks: + # + # { + # "vega": "1.78992", + # "leverage": "6.2096777055417", + # "ask_iv": "0.6245", + # "delta": "-0.69397", + # "last_price": "0", + # "theta": "-2.5723", + # "bid1_price": "222.9", + # "mark_iv": "0.5909", + # "name": "ETH_USDT-20231201-2300-P", + # "bid_iv": "0.5065", + # "ask1_price": "243.6", + # "mark_price": "236.57", + # "position_size": 0, + # "bid1_size": 368, + # "ask1_size": -335, + # "gamma": "0.00116" + # } + # + marketId = self.safe_string(greeks, 'name') + symbol = self.safe_symbol(marketId, market) + return { + 'symbol': symbol, + 'timestamp': None, + 'datetime': None, + 'delta': self.safe_number(greeks, 'delta'), + 'gamma': self.safe_number(greeks, 'gamma'), + 'theta': self.safe_number(greeks, 'theta'), + 'vega': self.safe_number(greeks, 'vega'), + 'rho': None, + 'bidSize': self.safe_number(greeks, 'bid1_size'), + 'askSize': self.safe_number(greeks, 'ask1_size'), + 'bidImpliedVolatility': self.safe_number(greeks, 'bid_iv'), + 'askImpliedVolatility': self.safe_number(greeks, 'ask_iv'), + 'markImpliedVolatility': self.safe_number(greeks, 'mark_iv'), + 'bidPrice': self.safe_number(greeks, 'bid1_price'), + 'askPrice': self.safe_number(greeks, 'ask1_price'), + 'markPrice': self.safe_number(greeks, 'mark_price'), + 'lastPrice': self.safe_number(greeks, 'last_price'), + 'underlyingPrice': self.parse_number(market['info']['underlying_price']), + 'info': greeks, + } + + async def close_position(self, symbol: str, side: OrderSide = None, params={}) -> Order: + """ + closes open positions for a market + + https://www.gate.com/docs/developers/apiv4/en/#create-a-futures-order + https://www.gate.com/docs/developers/apiv4/en/#create-a-futures-order-2 + https://www.gate.com/docs/developers/apiv4/en/#create-an-options-order + + :param str symbol: Unified CCXT market symbol + :param str side: 'buy' or 'sell' + :param dict [params]: extra parameters specific to the okx api endpoint + :returns dict[]: `A list of position structures ` + """ + request: dict = { + 'close': True, + } + params = self.extend(request, params) + if side is None: + side = '' # side is not used but needs to be present, otherwise crashes in php + return await self.create_order(symbol, 'market', side, 0, None, params) + + async def fetch_leverage(self, symbol: str, params={}) -> Leverage: + """ + fetch the set leverage for a market + + https://www.gate.com/docs/developers/apiv4/en/#get-unified-account-information + https://www.gate.com/docs/developers/apiv4/en/#get-detail-of-lending-market + https://www.gate.com/docs/developers/apiv4/en/#query-one-single-margin-currency-pair-deprecated + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.unified]: default False, set to True for fetching the unified accounts leverage + :returns dict: a `leverage structure ` + """ + await self.load_markets() + market = None + if symbol is not None: + # unified account does not require a symbol + market = self.market(symbol) + request: dict = {} + response = None + isUnified = self.safe_bool(params, 'unified') + params = self.omit(params, 'unified') + if market['spot']: + request['currency_pair'] = market['id'] + if isUnified: + response = await self.publicMarginGetUniCurrencyPairsCurrencyPair(self.extend(request, params)) + # + # { + # "currency_pair": "BTC_USDT", + # "base_min_borrow_amount": "0.0001", + # "quote_min_borrow_amount": "1", + # "leverage": "10" + # } + # + else: + response = await self.publicMarginGetCurrencyPairsCurrencyPair(self.extend(request, params)) + # + # { + # "id": "BTC_USDT", + # "base": "BTC", + # "quote": "USDT", + # "leverage": 10, + # "min_base_amount": "0.0001", + # "min_quote_amount": "1", + # "max_quote_amount": "40000000", + # "status": 1 + # } + # + elif isUnified: + response = await self.privateUnifiedGetAccounts(self.extend(request, params)) + # + # { + # "user_id": 10001, + # "locked": False, + # "balances": { + # "ETH": { + # "available": "0", + # "freeze": "0", + # "borrowed": "0.075393666654", + # "negative_liab": "0", + # "futures_pos_liab": "0", + # "equity": "1016.1", + # "total_freeze": "0", + # "total_liab": "0" + # }, + # "POINT": { + # "available": "9999999999.017023138734", + # "freeze": "0", + # "borrowed": "0", + # "negative_liab": "0", + # "futures_pos_liab": "0", + # "equity": "12016.1", + # "total_freeze": "0", + # "total_liab": "0" + # }, + # "USDT": { + # "available": "0.00000062023", + # "freeze": "0", + # "borrowed": "0", + # "negative_liab": "0", + # "futures_pos_liab": "0", + # "equity": "16.1", + # "total_freeze": "0", + # "total_liab": "0" + # } + # }, + # "total": "230.94621713", + # "borrowed": "161.66395521", + # "total_initial_margin": "1025.0524665088", + # "total_margin_balance": "3382495.944473949183", + # "total_maintenance_margin": "205.01049330176", + # "total_initial_margin_rate": "3299.827135672679", + # "total_maintenance_margin_rate": "16499.135678363399", + # "total_available_margin": "3381470.892007440383", + # "unified_account_total": "3381470.892007440383", + # "unified_account_total_liab": "0", + # "unified_account_total_equity": "100016.1", + # "leverage": "2" + # } + # + else: + raise NotSupported(self.id + ' fetchLeverage() does not support ' + market['type'] + ' markets') + return self.parse_leverage(response, market) + + async def fetch_leverages(self, symbols: Strings = None, params={}) -> Leverages: + """ + fetch the set leverage for all leverage markets, only spot margin is supported on gate + + https://www.gate.com/docs/developers/apiv4/en/#list-lending-markets + https://www.gate.com/docs/developers/apiv4/en/#list-all-supported-currency-pairs-supported-in-margin-trading-deprecated + + :param str[] symbols: a list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.unified]: default False, set to True for fetching unified account leverages + :returns dict: a list of `leverage structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + response = None + isUnified = self.safe_bool(params, 'unified') + params = self.omit(params, 'unified') + marketIdRequest = 'id' + if isUnified: + marketIdRequest = 'currency_pair' + response = await self.publicMarginGetUniCurrencyPairs(params) + # + # [ + # { + # "currency_pair": "1INCH_USDT", + # "base_min_borrow_amount": "8", + # "quote_min_borrow_amount": "1", + # "leverage": "3" + # }, + # ] + # + else: + response = await self.publicMarginGetCurrencyPairs(params) + # + # [ + # { + # "id": "1CAT_USDT", + # "base": "1CAT", + # "quote": "USDT", + # "leverage": 3, + # "min_base_amount": "71", + # "min_quote_amount": "1", + # "max_quote_amount": "10000", + # "status": 1 + # }, + # ] + # + return self.parse_leverages(response, symbols, marketIdRequest, 'spot') + + def parse_leverage(self, leverage: dict, market: Market = None) -> Leverage: + marketId = self.safe_string_2(leverage, 'currency_pair', 'id') + leverageValue = self.safe_integer(leverage, 'leverage') + return { + 'info': leverage, + 'symbol': self.safe_symbol(marketId, market, '_', 'spot'), + 'marginMode': None, + 'longLeverage': leverageValue, + 'shortLeverage': leverageValue, + } + + async def fetch_option(self, symbol: str, params={}) -> Option: + """ + fetches option data that is commonly found in an option chain + + https://www.gate.com/docs/developers/apiv4/en/#query-specified-contract-detail + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `option chain structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'contract': market['id'], + } + response = await self.publicOptionsGetContractsContract(self.extend(request, params)) + # + # { + # "is_active": True, + # "mark_price_round": "0.01", + # "settle_fee_rate": "0.00015", + # "bid1_size": 30, + # "taker_fee_rate": "0.0003", + # "price_limit_fee_rate": "0.1", + # "order_price_round": "0.1", + # "tag": "month", + # "ref_rebate_rate": "0", + # "name": "ETH_USDT-20240628-4500-C", + # "strike_price": "4500", + # "ask1_price": "280.5", + # "ref_discount_rate": "0", + # "order_price_deviate": "0.2", + # "ask1_size": -19, + # "mark_price_down": "155.45", + # "orderbook_id": 11724695, + # "is_call": True, + # "last_price": "188.7", + # "mark_price": "274.26", + # "underlying": "ETH_USDT", + # "create_time": 1688024882, + # "settle_limit_fee_rate": "0.1", + # "orders_limit": 10, + # "mark_price_up": "403.83", + # "position_size": 80, + # "order_size_max": 10000, + # "position_limit": 100000, + # "multiplier": "0.01", + # "order_size_min": 1, + # "trade_size": 229, + # "underlying_price": "3326.6", + # "maker_fee_rate": "0.0003", + # "expiration_time": 1719561600, + # "trade_id": 15, + # "bid1_price": "269.3" + # } + # + return self.parse_option(response, None, market) + + async def fetch_option_chain(self, code: str, params={}) -> OptionChain: + """ + fetches data for an underlying asset that is commonly found in an option chain + + https://www.gate.com/docs/developers/apiv4/en/#list-all-the-contracts-with-specified-underlying-and-expiration-time + + :param str code: base currency to fetch an option chain for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.underlying]: the underlying asset, can be obtained from fetchUnderlyingAssets() + :param int [params.expiration]: unix timestamp of the expiration time + :returns dict: a list of `option chain structures ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'underlying': currency['code'] + '_USDT', # todo: currency['id'].upper() & network junctions + } + response = await self.publicOptionsGetContracts(self.extend(request, params)) + # + # [ + # { + # "is_active": True, + # "mark_price_round": "0.1", + # "settle_fee_rate": "0.00015", + # "bid1_size": 434, + # "taker_fee_rate": "0.0003", + # "price_limit_fee_rate": "0.1", + # "order_price_round": "1", + # "tag": "day", + # "ref_rebate_rate": "0", + # "name": "BTC_USDT-20240324-63500-P", + # "strike_price": "63500", + # "ask1_price": "387", + # "ref_discount_rate": "0", + # "order_price_deviate": "0.15", + # "ask1_size": -454, + # "mark_price_down": "124.3", + # "orderbook_id": 29600, + # "is_call": False, + # "last_price": "0", + # "mark_price": "366.6", + # "underlying": "BTC_USDT", + # "create_time": 1711118829, + # "settle_limit_fee_rate": "0.1", + # "orders_limit": 10, + # "mark_price_up": "630", + # "position_size": 0, + # "order_size_max": 10000, + # "position_limit": 10000, + # "multiplier": "0.01", + # "order_size_min": 1, + # "trade_size": 0, + # "underlying_price": "64084.65", + # "maker_fee_rate": "0.0003", + # "expiration_time": 1711267200, + # "trade_id": 0, + # "bid1_price": "307" + # }, + # ] + # + return self.parse_option_chain(response, None, 'name') + + def parse_option(self, chain: dict, currency: Currency = None, market: Market = None) -> Option: + # + # { + # "is_active": True, + # "mark_price_round": "0.1", + # "settle_fee_rate": "0.00015", + # "bid1_size": 434, + # "taker_fee_rate": "0.0003", + # "price_limit_fee_rate": "0.1", + # "order_price_round": "1", + # "tag": "day", + # "ref_rebate_rate": "0", + # "name": "BTC_USDT-20240324-63500-P", + # "strike_price": "63500", + # "ask1_price": "387", + # "ref_discount_rate": "0", + # "order_price_deviate": "0.15", + # "ask1_size": -454, + # "mark_price_down": "124.3", + # "orderbook_id": 29600, + # "is_call": False, + # "last_price": "0", + # "mark_price": "366.6", + # "underlying": "BTC_USDT", + # "create_time": 1711118829, + # "settle_limit_fee_rate": "0.1", + # "orders_limit": 10, + # "mark_price_up": "630", + # "position_size": 0, + # "order_size_max": 10000, + # "position_limit": 10000, + # "multiplier": "0.01", + # "order_size_min": 1, + # "trade_size": 0, + # "underlying_price": "64084.65", + # "maker_fee_rate": "0.0003", + # "expiration_time": 1711267200, + # "trade_id": 0, + # "bid1_price": "307" + # } + # + marketId = self.safe_string(chain, 'name') + market = self.safe_market(marketId, market) + timestamp = self.safe_timestamp(chain, 'create_time') + return { + 'info': chain, + 'currency': None, + 'symbol': market['symbol'], + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'impliedVolatility': None, + 'openInterest': None, + 'bidPrice': self.safe_number(chain, 'bid1_price'), + 'askPrice': self.safe_number(chain, 'ask1_price'), + 'midPrice': None, + 'markPrice': self.safe_number(chain, 'mark_price'), + 'lastPrice': self.safe_number(chain, 'last_price'), + 'underlyingPrice': self.safe_number(chain, 'underlying_price'), + 'change': None, + 'percentage': None, + 'baseVolume': None, + 'quoteVolume': None, + } + + async def fetch_positions_history(self, symbols: Strings = None, since: Int = None, limit: Int = None, params={}) -> List[Position]: + """ + fetches historical positions + + https://www.gate.com/docs/developers/apiv4/#list-position-close-history + https://www.gate.com/docs/developers/apiv4/#list-position-close-history-2 + + :param str[] symbols: unified conract symbols, must all have the same settle currency and the same market type + :param int [since]: the earliest time in ms to fetch positions for + :param int [limit]: the maximum amount of records to fetch, default=1000 + :param dict params: extra parameters specific to the exchange api endpoint + :param int [params.until]: the latest time in ms to fetch positions for + + EXCHANGE SPECIFIC PARAMETERS + :param int [params.offset]: list offset, starting from 0 + :param str [params.side]: long or short + :param str [params.pnl]: query profit or loss + :returns dict[]: a list of `position structures ` + """ + await self.load_markets() + market = None + if symbols is not None: + symbolsLength = len(symbols) + if symbolsLength == 1: + market = self.market(symbols[0]) + marketType = None + marketType, params = self.handle_market_type_and_params('fetchPositionsHistory', market, params, 'swap') + until = self.safe_integer(params, 'until') + params = self.omit(params, 'until') + request: dict = {} + request, params = self.prepare_request(market, marketType, params) + if limit is not None: + request['limit'] = limit + if since is not None: + request['from'] = self.parse_to_int(since / 1000) + if until is not None: + request['to'] = self.parse_to_int(until / 1000) + response = None + if marketType == 'swap': + response = await self.privateFuturesGetSettlePositionClose(self.extend(request, params)) + elif marketType == 'future': + response = await self.privateDeliveryGetSettlePositionClose(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchPositionsHistory() does not support markets of type ' + marketType) + # + # [ + # { + # "contract": "SLERF_USDT", + # "text": "web", + # "long_price": "0.766306", + # "pnl": "-23.41702352", + # "pnl_pnl": "-22.7187", + # "pnl_fee": "-0.06527125", + # "pnl_fund": "-0.63305227", + # "accum_size": "100", + # "time": 1711279263, + # "short_price": "0.539119", + # "side": "long", + # "max_size": "100", + # "first_open_time": 1711037985 + # }, + # ... + # ] + # + return self.parse_positions(response, symbols, params) + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None + # + # {"label": "ORDER_NOT_FOUND", "message": "Order not found"} + # {"label": "INVALID_PARAM_VALUE", "message": "invalid argument: status"} + # {"label": "INVALID_PARAM_VALUE", "message": "invalid argument: Trigger.rule"} + # {"label": "INVALID_PARAM_VALUE", "message": "invalid argument: trigger.expiration invalid range"} + # {"label": "INVALID_ARGUMENT", "detail": "invalid size"} + # {"user_id":10406147,"id":"id","succeeded":false,"message":"INVALID_PROTOCOL","label":"INVALID_PROTOCOL"} + # + label = self.safe_string(response, 'label') + if label is not None: + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], label, feedback) + raise ExchangeError(feedback) + return None diff --git a/ccxt/async_support/gateio.py b/ccxt/async_support/gateio.py new file mode 100644 index 0000000..88d6302 --- /dev/null +++ b/ccxt/async_support/gateio.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.gate import gate +from ccxt.abstract.gateio import ImplicitAPI +from ccxt.base.types import Any + + +class gateio(gate, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(gateio, self).describe(), { + 'id': 'gateio', + 'alias': True, + }) diff --git a/ccxt/async_support/gemini.py b/ccxt/async_support/gemini.py new file mode 100644 index 0000000..62769cf --- /dev/null +++ b/ccxt/async_support/gemini.py @@ -0,0 +1,1934 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.gemini import ImplicitAPI +import asyncio +import hashlib +from ccxt.base.types import Any, Balances, Currencies, Currency, DepositAddress, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, TradingFees, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import NotSupported +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.errors import OnMaintenance +from ccxt.base.errors import InvalidNonce +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class gemini(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(gemini, self).describe(), { + 'id': 'gemini', + 'name': 'Gemini', + 'countries': ['US'], + # 600 requests a minute = 10 requests per second => 1000ms / 10 = 100ms between requests(private endpoints) + # 120 requests a minute = 2 requests per second =>( 1000ms / rateLimit ) / 2 = 5(public endpoints) + 'rateLimit': 100, + 'version': 'v1', + 'pro': True, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': True, + 'future': False, + 'option': False, + 'addMargin': False, + 'cancelOrder': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createDepositAddress': True, + 'createMarketOrder': False, + 'createOrder': True, + 'createReduceOnlyOrder': False, + 'fetchBalance': True, + 'fetchBidsAsks': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchClosedOrders': False, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': True, + 'fetchDepositsWithdrawals': True, + 'fetchFundingHistory': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchLeverage': False, + 'fetchLeverageTiers': False, + 'fetchMarginMode': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterestHistory': False, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': False, + 'fetchPosition': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': True, + 'fetchTransactions': 'emulated', + 'postOnly': True, + 'reduceMargin': False, + 'sandbox': True, + 'setLeverage': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'withdraw': True, + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/27816857-ce7be644-6096-11e7-82d6-3c257263229c.jpg', + 'api': { + 'public': 'https://api.gemini.com', + 'private': 'https://api.gemini.com', + 'web': 'https://docs.gemini.com', + 'webExchange': 'https://exchange.gemini.com', + }, + 'www': 'https://gemini.com/', + 'doc': [ + 'https://docs.gemini.com/rest-api', + 'https://docs.sandbox.gemini.com', + ], + 'test': { + 'public': 'https://api.sandbox.gemini.com', + 'private': 'https://api.sandbox.gemini.com', + # use the True doc instead of the sandbox doc + # since they differ in parsing + # https://github.com/ccxt/ccxt/issues/7874 + # https://github.com/ccxt/ccxt/issues/7894 + 'web': 'https://docs.gemini.com', + 'webExchange': 'https://exchange.gemini.com', + }, + 'fees': [ + 'https://gemini.com/api-fee-schedule', + 'https://gemini.com/trading-fees', + 'https://gemini.com/transfer-fees', + ], + }, + 'api': { + 'webExchange': { + 'get': [ + '', + ], + }, + 'web': { + 'get': [ + 'rest-api', + ], + }, + 'public': { + 'get': { + 'v1/symbols': 5, + 'v1/symbols/details/{symbol}': 5, + 'v1/staking/rates': 5, + 'v1/pubticker/{symbol}': 5, + 'v2/ticker/{symbol}': 5, + 'v2/candles/{symbol}/{timeframe}': 5, + 'v1/trades/{symbol}': 5, + 'v1/auction/{symbol}': 5, + 'v1/auction/{symbol}/history': 5, + 'v1/pricefeed': 5, + 'v1/book/{symbol}': 5, + 'v1/earn/rates': 5, + }, + }, + 'private': { + 'post': { + 'v1/staking/unstake': 1, + 'v1/staking/stake': 1, + 'v1/staking/rewards': 1, + 'v1/staking/history': 1, + 'v1/order/new': 1, + 'v1/order/cancel': 1, + 'v1/wrap/{symbol}': 1, + 'v1/order/cancel/session': 1, + 'v1/order/cancel/all': 1, + 'v1/order/status': 1, + 'v1/orders': 1, + 'v1/mytrades': 1, + 'v1/notionalvolume': 1, + 'v1/tradevolume': 1, + 'v1/clearing/new': 1, + 'v1/clearing/status': 1, + 'v1/clearing/cancel': 1, + 'v1/clearing/confirm': 1, + 'v1/balances': 1, + 'v1/balances/staking': 1, + 'v1/notionalbalances/{currency}': 1, + 'v1/transfers': 1, + 'v1/addresses/{network}': 1, + 'v1/deposit/{network}/newAddress': 1, + 'v1/deposit/{currency}/newAddress': 1, + 'v1/withdraw/{currency}': 1, + 'v1/account/transfer/{currency}': 1, + 'v1/payments/addbank': 1, + 'v1/payments/methods': 1, + 'v1/payments/sen/withdraw': 1, + 'v1/balances/earn': 1, + 'v1/earn/interest': 1, + 'v1/earn/history': 1, + 'v1/approvedAddresses/{network}/request': 1, + 'v1/approvedAddresses/account/{network}': 1, + 'v1/approvedAddresses/{network}/remove': 1, + 'v1/account': 1, + 'v1/account/create': 1, + 'v1/account/list': 1, + 'v1/heartbeat': 1, + 'v1/roles': 1, + }, + }, + }, + 'precisionMode': TICK_SIZE, + 'fees': { + 'trading': { + 'taker': 0.004, + 'maker': 0.002, + }, + }, + 'httpExceptions': { + '400': BadRequest, # Auction not open or paused, ineligible timing, market not open, or the request was malformed, in the case of a private API request, missing or malformed Gemini private API authentication headers + '403': PermissionDenied, # The API key is missing the role necessary to access self private API endpoint + '404': OrderNotFound, # Unknown API entry point or Order not found + '406': InsufficientFunds, # Insufficient Funds + '429': RateLimitExceeded, # Rate Limiting was applied + '500': ExchangeError, # The server encountered an error + '502': ExchangeNotAvailable, # Technical issues are preventing the request from being satisfied + '503': OnMaintenance, # The exchange is down for maintenance + }, + 'timeframes': { + '1m': '1m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1hr', + '6h': '6hr', + '1d': '1day', + }, + 'exceptions': { + 'exact': { + 'AuctionNotOpen': BadRequest, # Failed to place an auction-only order because there is no current auction open for self symbol + 'ClientOrderIdTooLong': BadRequest, # The Client Order ID must be under 100 characters + 'ClientOrderIdMustBeString': BadRequest, # The Client Order ID must be a string + 'ConflictingOptions': BadRequest, # New orders using a combination of order execution options are not supported + 'EndpointMismatch': BadRequest, # The request was submitted to an endpoint different than the one in the payload + 'EndpointNotFound': BadRequest, # No endpoint was specified + 'IneligibleTiming': BadRequest, # Failed to place an auction order for the current auction on self symbol because the timing is not eligible, new orders may only be placed before the auction begins. + 'InsufficientFunds': InsufficientFunds, # The order was rejected because of insufficient funds + 'InvalidJson': BadRequest, # The JSON provided is invalid + 'InvalidNonce': InvalidNonce, # The nonce was not greater than the previously used nonce, or was not present + 'InvalidApiKey': AuthenticationError, # Invalid API key + 'InvalidOrderType': InvalidOrder, # An unknown order type was provided + 'InvalidPrice': InvalidOrder, # For new orders, the price was invalid + 'InvalidQuantity': InvalidOrder, # A negative or otherwise invalid quantity was specified + 'InvalidSide': InvalidOrder, # For new orders, and invalid side was specified + 'InvalidSignature': AuthenticationError, # The signature did not match the expected signature + 'InvalidSymbol': BadRequest, # An invalid symbol was specified + 'InvalidTimestampInPayload': BadRequest, # The JSON payload contained a timestamp parameter with an unsupported value. + 'Maintenance': OnMaintenance, # The system is down for maintenance + 'MarketNotOpen': InvalidOrder, # The order was rejected because the market is not accepting new orders + 'MissingApikeyHeader': AuthenticationError, # The X-GEMINI-APIKEY header was missing + 'MissingOrderField': InvalidOrder, # A required order_id field was not specified + 'MissingRole': AuthenticationError, # The API key used to access self endpoint does not have the required role assigned to it + 'MissingPayloadHeader': AuthenticationError, # The X-GEMINI-PAYLOAD header was missing + 'MissingSignatureHeader': AuthenticationError, # The X-GEMINI-SIGNATURE header was missing + 'NoSSL': AuthenticationError, # You must use HTTPS to access the API + 'OptionsMustBeArray': BadRequest, # The options parameter must be an array. + 'OrderNotFound': OrderNotFound, # The order specified was not found + 'RateLimit': RateLimitExceeded, # Requests were made too frequently. See Rate Limits below. + 'System': ExchangeError, # We are experiencing technical issues + 'UnsupportedOption': BadRequest, # This order execution option is not supported. + }, + 'broad': { + 'The Gemini Exchange is currently undergoing maintenance.': OnMaintenance, # The Gemini Exchange is currently undergoing maintenance. Please check https://status.gemini.com/ for more information. + 'We are investigating technical issues with the Gemini Exchange.': ExchangeNotAvailable, # We are investigating technical issues with the Gemini Exchange. Please check https://status.gemini.com/ for more information. + 'Internal Server Error': ExchangeNotAvailable, + }, + }, + 'options': { + 'fetchMarketsMethod': 'fetch_markets_from_api', # fetch_markets_from_api, fetch_markets_from_web + 'fetchMarketFromWebRetries': 10, + 'fetchMarketsFromAPI': { + 'fetchDetailsForAllSymbols': False, + 'quoteCurrencies': ['USDT', 'GUSD', 'USD', 'DAI', 'EUR', 'GBP', 'SGD', 'BTC', 'ETH', 'LTC', 'BCH', 'SOL', 'USDC'], + }, + 'fetchMarkets': { + 'webApiEnable': True, # fetches from WEB + 'webApiRetries': 10, + }, + 'fetchUsdtMarkets': ['btcusdt', 'ethusdt'], # self is only used if markets-fetch is set from "web"; keep self list updated(not available trough web api) + 'fetchCurrencies': { + 'webApiEnable': True, # fetches from WEB + 'webApiRetries': 5, + 'webApiMuteFailure': True, + }, + 'fetchTickerMethod': 'fetchTickerV1', # fetchTickerV1, fetchTickerV2, fetchTickerV1AndV2 + 'networks': { + 'BTC': 'bitcoin', + 'ERC20': 'ethereum', + 'BCH': 'bitcoincash', + 'LTC': 'litecoin', + 'ZEC': 'zcash', + 'FIL': 'filecoin', + 'DOGE': 'dogecoin', + 'XTZ': 'tezos', + 'AVAXX': 'avalanche', + 'SOL': 'solana', + 'ATOM': 'cosmos', + 'DOT': 'polkadot', + }, + 'nonce': 'milliseconds', # if getting a Network 400 error change to seconds, + 'conflictingMarkets': { + 'paxgusd': { + 'base': 'PAXG', + 'quote': 'USD', + }, + }, + 'brokenPairs': ['efilusd', 'maticrlusd', 'maticusdc', 'eurusdc', 'maticgusd', 'maticusd', 'efilfil', 'eurusd'], + }, + 'features': { + 'default': { + 'sandbox': True, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 500, + 'daysBack': None, + 'untilDays': None, + 'symbolRequired': True, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': None, # todo: implement + 'fetchOHLCV': { + 'limit': None, + }, + }, + 'spot': { + 'extends': 'default', + }, + 'swap': { + 'linear': { + 'extends': 'default', + }, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + }) + + async def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + :param dict [params]: extra parameters specific to the endpoint + :returns dict: an associative dictionary of currencies + """ + return await self.fetch_currencies_from_web(params) + + async def fetch_currencies_from_web(self, params={}): + """ + @ignore + fetches all available currencies on an exchange + :param dict [params]: extra parameters specific to the endpoint + :returns dict: an associative dictionary of currencies + """ + data = await self.fetch_web_endpoint('fetchCurrencies', 'webExchangeGet', True, '="currencyData">', '') + if data is None: + return {} + # + # { + # "tradingPairs": [['BTCUSD', 2, 8, '0.00001', 10, True], ...], + # "currencies": [ + # ["ORCA", "Orca", 204, 6, 0, 6, 8, False, null, "solana"], #, precisions seem to be the 5th index + # ["ATOM", "Cosmos", 44, 6, 0, 6, 8, False, null, "cosmos"], + # ["ETH", "Ether", 2, 6, 0, 18, 8, False, null, "ethereum"], + # ["GBP", "Pound Sterling", 22, 2, 2, 2, 2, True, "£", null], + # ... + # ], + # "networks": [ + # ["solana", "SOL", "Solana"], + # ["zcash", "ZEC", "Zcash"], + # ["tezos", "XTZ", "Tezos"], + # ["cosmos", "ATOM", "Cosmos"], + # ["ethereum", "ETH", "Ethereum"], + # ... + # ] + # } + # + result: dict = {} + self.options['tradingPairs'] = self.safe_list(data, 'tradingPairs') + currenciesArray = self.safe_value(data, 'currencies', []) + for i in range(0, len(currenciesArray)): + currency = currenciesArray[i] + id = self.safe_string(currency, 0) + code = self.safe_currency_code(id) + type = 'fiat' if self.safe_string(currency, 7) else 'crypto' + precision = self.parse_number(self.parse_precision(self.safe_string(currency, 5))) + networks: dict = {} + networkId = self.safe_string(currency, 9) + networkCode = None + if networkId is not None: + networkCode = self.network_id_to_code(networkId) + networks[networkCode] = { + 'info': currency, + 'id': networkId, + 'network': networkCode, + 'active': None, + 'deposit': None, + 'withdraw': None, + 'fee': None, + 'precision': precision, + 'limits': { + 'deposit': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + }, + } + result[code] = self.safe_currency_structure({ + 'info': currency, + 'id': id, + 'code': code, + 'name': self.safe_string(currency, 1), + 'active': None, + 'deposit': None, + 'withdraw': None, + 'fee': None, + 'type': type, + 'precision': precision, + 'limits': { + 'deposit': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + }, + 'networks': networks, + }) + return result + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for gemini + + https://docs.gemini.com/rest-api/#symbols + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + method = self.safe_value(self.options, 'fetchMarketsMethod', 'fetch_markets_from_api') + if method == 'fetch_markets_from_web': + promises = [] + promises.append(self.fetch_markets_from_web(params)) # get usd markets + promises.append(self.fetch_usdt_markets(params)) # get usdt markets + promisesResult = await asyncio.gather(*promises) + return self.array_concat(promisesResult[0], promisesResult[1]) + return await self.fetch_markets_from_api(params) + + async def fetch_markets_from_web(self, params={}): + data = await self.fetch_web_endpoint('fetchMarkets', 'webGetRestApi', False, '

Symbols and minimums

') + error = self.id + ' fetchMarketsFromWeb() the API doc HTML markup has changed, breaking the parser of order limits and precision info for markets.' + tables = data.split('tbody>') + numTables = len(tables) + if numTables < 2: + raise NotSupported(error) + rows = tables[1].split("\n\n") # eslint-disable-line quotes + numRows = len(rows) + if numRows < 2: + raise NotSupported(error) + result = [] + # skip the first element(empty string) + for i in range(1, numRows): + row = rows[i] + cells = row.split("\n") # eslint-disable-line quotes + numCells = len(cells) + if numCells < 5: + raise NotSupported(error) + # [ + # 'btcusd', # currency + # '0.00001 BTC(1e-5)', # min order size + # '0.00000001 BTC(1e-8)', # tick size + # '0.01 USD', # quote currency price increment + # '' + # ] + marketId = cells[0].replace('', '') + marketId = marketId.replace('*', '') + # base = self.safe_currency_code(baseId) + minAmountString = cells[1].replace('', '') + minAmountParts = minAmountString.split(' ') + minAmount = self.safe_number(minAmountParts, 0) + amountPrecisionString = cells[2].replace('', '') + amountPrecisionParts = amountPrecisionString.split(' ') + idLength = len(marketId) - 0 + startingIndex = idLength - 3 + pricePrecisionString = cells[3].replace('', '') + pricePrecisionParts = pricePrecisionString.split(' ') + quoteId = self.safe_string_lower(pricePrecisionParts, 1, marketId[startingIndex:idLength]) + baseId = self.safe_string_lower(amountPrecisionParts, 1, marketId.replace(quoteId, '')) + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + result.append({ + 'id': marketId, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': None, + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.safe_number(amountPrecisionParts, 0), + 'price': self.safe_number(pricePrecisionParts, 0), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': minAmount, + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + 'info': row, + }) + return result + + def parse_market_active(self, status): + statuses: dict = { + 'open': True, + 'closed': False, + 'cancel_only': True, + 'post_only': True, + 'limit_only': True, + } + if status is None: + return True # below + return self.safe_bool(statuses, status, True) + + async def fetch_usdt_markets(self, params={}): + # these markets can't be scrapped and fetchMarketsFrom api does an extra call + # to load market ids which we don't need here + if 'test' in self.urls: + return [] # sandbox does not have usdt markets + fetchUsdtMarkets = self.safe_value(self.options, 'fetchUsdtMarkets', []) + result = [] + for i in range(0, len(fetchUsdtMarkets)): + marketId = fetchUsdtMarkets[i] + request: dict = { + 'symbol': marketId, + } + # don't use Promise.all here, for some reason the exchange can't handle it and crashes + rawResponse = await self.publicGetV1SymbolsDetailsSymbol(self.extend(request, params)) + result.append(self.parse_market(rawResponse)) + return result + + async def fetch_markets_from_api(self, params={}): + marketIdsRaw = await self.publicGetV1Symbols(params) + # + # [ + # "btcusd", + # "linkusd", + # ... + # ] + # + result = [] + options = self.safe_dict(self.options, 'fetchMarketsFromAPI', {}) + brokenPairs = self.safe_list(self.options, 'brokenPairs', []) + marketIds = [] + for i in range(0, len(marketIdsRaw)): + if not self.in_array(marketIdsRaw[i], brokenPairs): + marketIds.append(marketIdsRaw[i]) + if self.safe_bool(options, 'fetchDetailsForAllSymbols', False): + promises = [] + for i in range(0, len(marketIds)): + marketId = marketIds[i] + request: dict = { + 'symbol': marketId, + } + promises.append(self.publicGetV1SymbolsDetailsSymbol(self.extend(request, params))) + # + # { + # "symbol": "BTCUSD", + # "base_currency": "BTC", + # "quote_currency": "USD", + # "tick_size": 1E-8, + # "quote_increment": 0.01, + # "min_order_size": "0.00001", + # "status": "open", + # "wrap_enabled": False + # } + # + responses = await asyncio.gather(*promises) + for i in range(0, len(responses)): + result.append(self.parse_market(responses[i])) + else: + # use trading-pairs info, if it was fetched + tradingPairs = self.safe_list(self.options, 'tradingPairs') + if tradingPairs is not None: + indexedTradingPairs = self.index_by(tradingPairs, 0) + for i in range(0, len(marketIds)): + marketId = marketIds[i] + pairInfo = self.safe_list(indexedTradingPairs, marketId.upper()) + if pairInfo is not None and not self.in_array(marketId, brokenPairs): + result.append(self.parse_market(pairInfo)) + else: + for i in range(0, len(marketIds)): + if not self.in_array(marketIds[i], brokenPairs): + result.append(self.parse_market(marketIds[i])) + return result + + def parse_market(self, response) -> Market: + # + # response might be: + # + # btcusd + # + # or + # + # [ + # 'BTCUSD', # symbol + # 2, # tick precision(priceTickDecimalPlaces) + # 8, # amount precision(quantityTickDecimalPlaces) + # '0.00001', # quantityMinimum + # 10, # quantityRoundDecimalPlaces + # True # minimumsAreInclusive + # ], + # + # or + # + # { + # "symbol": "BTCUSD", # perpetuals have 'PERP' suffix, i.e. DOGEUSDPERP + # "base_currency": "BTC", + # "quote_currency": "USD", + # "tick_size": 1E-8, + # "quote_increment": 0.01, + # "min_order_size": "0.00001", + # "status": "open", + # "wrap_enabled": False + # "product_type": "swap", # only in perps + # "contract_type": "linear", # only in perps + # "contract_price_currency": "GUSD" + # } + # + marketId = None + baseId = None + quoteId = None + settleId = None + tickSize = None + amountPrecision = None + minSize = None + status = None + swap = False + contractSize = None + linear = None + inverse = None + isString = (isinstance(response, str)) + isArray = (isinstance(response, list)) + if not isString and not isArray: + marketId = self.safe_string_lower(response, 'symbol') + amountPrecision = self.safe_number(response, 'tick_size') # right, exchange has an imperfect naming and self turns out to be an amount-precision + tickSize = self.safe_number(response, 'quote_increment') # self is tick-size actually + minSize = self.safe_number(response, 'min_order_size') + status = self.parse_market_active(self.safe_string(response, 'status')) + baseId = self.safe_string(response, 'base_currency') + quoteId = self.safe_string(response, 'quote_currency') + settleId = self.safe_string(response, 'contract_price_currency') + else: + # if no detailed API was called, then parse either string or array + if isString: + marketId = response + else: + marketId = self.safe_string_lower(response, 0) + tickSize = self.parse_number(self.parse_precision(self.safe_string(response, 1))) # priceTickDecimalPlaces + amountPrecision = self.parse_number(self.parse_precision(self.safe_string(response, 2))) # quantityTickDecimalPlaces + minSize = self.safe_number(response, 3) # quantityMinimum + marketIdUpper = marketId.upper() + isPerp = (marketIdUpper.find('PERP') >= 0) + marketIdWithoutPerp = marketIdUpper.replace('PERP', '') + conflictingMarkets = self.safe_dict(self.options, 'conflictingMarkets', {}) + lowerCaseId = marketIdWithoutPerp.lower() + if lowerCaseId in conflictingMarkets: + conflictingMarket = conflictingMarkets[lowerCaseId] + baseId = conflictingMarket['base'] + quoteId = conflictingMarket['quote'] + if isPerp: + settleId = conflictingMarket['quote'] + else: + quoteCurrencies = self.handle_option('fetchMarketsFromAPI', 'quoteCurrencies', []) + for i in range(0, len(quoteCurrencies)): + quoteCurrency = quoteCurrencies[i] + if marketIdWithoutPerp.endswith(quoteCurrency): + quoteLength = self.parse_to_int(-1 * len(quoteCurrency)) + baseId = marketIdWithoutPerp[0:quoteLength] + quoteId = quoteCurrency + if isPerp: + settleId = quoteCurrency # always same + break + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + settle = self.safe_currency_code(settleId) + symbol = base + '/' + quote + if settleId is not None: + symbol = symbol + ':' + settle + swap = True + contractSize = tickSize # always same + linear = True # always linear + inverse = False + type = 'swap' if swap else 'spot' + return { + 'id': marketId, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': type, + 'spot': not swap, + 'margin': False, + 'swap': swap, + 'future': False, + 'option': False, + 'active': status, + 'contract': swap, + 'linear': linear, + 'inverse': inverse, + 'contractSize': contractSize, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'price': tickSize, + 'amount': amountPrecision, + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': minSize, + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + 'info': response, + } + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://docs.gemini.com/rest-api/#current-order-book + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['limit_bids'] = limit + request['limit_asks'] = limit + response = await self.publicGetV1BookSymbol(self.extend(request, params)) + return self.parse_order_book(response, market['symbol'], None, 'bids', 'asks', 'price', 'amount') + + async def fetch_ticker_v1(self, symbol: str, params={}): + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = await self.publicGetV1PubtickerSymbol(self.extend(request, params)) + # + # { + # "bid":"9117.95", + # "ask":"9117.96", + # "volume":{ + # "BTC":"1615.46861748", + # "USD":"14727307.57545006088", + # "timestamp":1594982700000 + # }, + # "last":"9115.23" + # } + # + return self.parse_ticker(response, market) + + async def fetch_ticker_v2(self, symbol: str, params={}): + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = await self.publicGetV2TickerSymbol(self.extend(request, params)) + # + # { + # "symbol":"BTCUSD", + # "open":"9080.58", + # "high":"9184.53", + # "low":"9063.56", + # "close":"9116.08", + # # Hourly prices descending for past 24 hours + # "changes":["9117.33","9105.69","9106.23","9120.35","9098.57","9114.53","9113.55","9128.01","9113.63","9133.49","9133.49","9137.75","9126.73","9103.91","9119.33","9123.04","9124.44","9117.57","9114.22","9102.33","9076.67","9074.72","9074.97","9092.05"], + # "bid":"9115.86", + # "ask":"9115.87" + # } + # + return self.parse_ticker(response, market) + + async def fetch_ticker_v1_and_v2(self, symbol: str, params={}): + tickerPromiseA = self.fetch_ticker_v1(symbol, params) + tickerPromiseB = self.fetch_ticker_v2(symbol, params) + tickerA, tickerB = await asyncio.gather(*[tickerPromiseA, tickerPromiseB]) + return self.deep_extend(tickerA, { + 'open': tickerB['open'], + 'high': tickerB['high'], + 'low': tickerB['low'], + 'change': tickerB['change'], + 'percentage': tickerB['percentage'], + 'average': tickerB['average'], + 'info': tickerB['info'], + }) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://docs.gemini.com/rest-api/#ticker + https://docs.gemini.com/rest-api/#ticker-v2 + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param dict [params.fetchTickerMethod]: 'fetchTickerV2', 'fetchTickerV1' or 'fetchTickerV1AndV2' - 'fetchTickerV1' for original ccxt.gemini.fetchTicker - 'fetchTickerV1AndV2' for 2 api calls to get the result of both fetchTicker methods - default = 'fetchTickerV1' + :returns dict: a `ticker structure ` + """ + method = self.safe_value(self.options, 'fetchTickerMethod', 'fetchTickerV1') + if method == 'fetchTickerV1': + return await self.fetch_ticker_v1(symbol, params) + if method == 'fetchTickerV2': + return await self.fetch_ticker_v2(symbol, params) + return await self.fetch_ticker_v1_and_v2(symbol, params) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # fetchTickers + # + # { + # "pair": "BATUSD", + # "price": "0.20687", + # "percentChange24h": "0.0146" + # } + # + # fetchTickerV1 + # + # { + # "bid":"9117.95", + # "ask":"9117.96", + # "volume":{ + # "BTC":"1615.46861748", + # "USD":"14727307.57545006088", + # "timestamp":1594982700000 + # }, + # "last":"9115.23" + # } + # + # fetchTickerV2 + # + # { + # "symbol":"BTCUSD", + # "open":"9080.58", + # "high":"9184.53", + # "low":"9063.56", + # "close":"9116.08", + # # Hourly prices descending for past 24 hours + # "changes":["9117.33","9105.69","9106.23","9120.35","9098.57","9114.53","9113.55","9128.01","9113.63","9133.49","9133.49","9137.75","9126.73","9103.91","9119.33","9123.04","9124.44","9117.57","9114.22","9102.33","9076.67","9074.72","9074.97","9092.05"], + # "bid":"9115.86", + # "ask":"9115.87" + # } + # + volume = self.safe_value(ticker, 'volume', {}) + timestamp = self.safe_integer(volume, 'timestamp') + symbol = None + marketId = self.safe_string_lower(ticker, 'pair') + market = self.safe_market(marketId, market) + baseId = None + quoteId = None + base = None + quote = None + if (marketId is not None) and (market is None): + idLength = len(marketId) - 0 + if idLength == 7: + baseId = marketId[0:4] + quoteId = marketId[4:7] + else: + baseId = marketId[0:3] + quoteId = marketId[3:6] + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + symbol = base + '/' + quote + if (symbol is None) and (market is not None): + symbol = market['symbol'] + baseId = self.safe_string_upper(market, 'baseId') + quoteId = self.safe_string_upper(market, 'quoteId') + price = self.safe_string(ticker, 'price') + last = self.safe_string_2(ticker, 'last', 'close', price) + percentage = self.safe_string(ticker, 'percentChange24h') + open = self.safe_string(ticker, 'open') + baseVolume = self.safe_string(volume, baseId) + quoteVolume = self.safe_string(volume, quoteId) + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': self.safe_string(ticker, 'bid'), + 'bidVolume': None, + 'ask': self.safe_string(ticker, 'ask'), + 'askVolume': None, + 'vwap': None, + 'open': open, + 'close': last, + 'last': last, + 'previousClose': None, # previous day close + 'change': None, + 'percentage': percentage, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'info': ticker, + }, market) + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://docs.gemini.com/rest-api/#price-feed + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + response = await self.publicGetV1Pricefeed(params) + # + # [ + # { + # "pair": "BATUSD", + # "price": "0.20687", + # "percentChange24h": "0.0146" + # }, + # { + # "pair": "LINKETH", + # "price": "0.018", + # "percentChange24h": "0.0000" + # }, + # ] + # + result = self.parse_tickers(response, symbols) + brokenPairs = self.safe_list(self.options, 'brokenPairs', []) + return self.remove_keys_from_dict(result, brokenPairs) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # public fetchTrades + # + # { + # "timestamp":1601617445, + # "timestampms":1601617445144, + # "tid":14122489752, + # "price":"0.46476", + # "amount":"28.407209", + # "exchange":"gemini", + # "type":"buy" + # } + # + # private fetchTrades + # + # { + # "price":"3900.00", + # "amount":"0.00996", + # "timestamp":1638891173, + # "timestampms":1638891173518, + # "type":"Sell", + # "aggressor":false, + # "fee_currency":"EUR", + # "fee_amount":"0.00", + # "tid":73621746145, + # "order_id":"73621746059", + # "exchange":"gemini", + # "is_auction_fill":false, + # "is_clearing_fill":false, + # "symbol":"ETHEUR", + # "client_order_id":"1638891171610" + # } + # + timestamp = self.safe_integer(trade, 'timestampms') + id = self.safe_string(trade, 'tid') + orderId = self.safe_string(trade, 'order_id') + feeCurrencyId = self.safe_string(trade, 'fee_currency') + feeCurrencyCode = self.safe_currency_code(feeCurrencyId) + fee = { + 'cost': self.safe_string(trade, 'fee_amount'), + 'currency': feeCurrencyCode, + } + priceString = self.safe_string(trade, 'price') + amountString = self.safe_string(trade, 'amount') + side = self.safe_string_lower(trade, 'type') + symbol = self.safe_symbol(None, market) + return self.safe_trade({ + 'id': id, + 'order': orderId, + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'type': None, + 'side': side, + 'takerOrMaker': None, + 'price': priceString, + 'cost': None, + 'amount': amountString, + 'fee': fee, + }, market) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://docs.gemini.com/rest-api/#trade-history + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['limit_trades'] = min(limit, 500) + if since is not None: + request['timestamp'] = since + response = await self.publicGetV1TradesSymbol(self.extend(request, params)) + # + # [ + # { + # "timestamp":1601617445, + # "timestampms":1601617445144, + # "tid":14122489752, + # "price":"0.46476", + # "amount":"28.407209", + # "exchange":"gemini", + # "type":"buy" + # }, + # ] + # + return self.parse_trades(response, market, since, limit) + + def parse_balance(self, response) -> Balances: + result: dict = {'info': response} + for i in range(0, len(response)): + balance = response[i] + currencyId = self.safe_string(balance, 'currency') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(balance, 'available') + account['total'] = self.safe_string(balance, 'amount') + result[code] = account + return self.safe_balance(result) + + async def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://docs.gemini.com/rest-api/#get-notional-volume + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + await self.load_markets() + response = await self.privatePostV1Notionalvolume(params) + # + # { + # "web_maker_fee_bps": 25, + # "web_taker_fee_bps": 35, + # "web_auction_fee_bps": 25, + # "api_maker_fee_bps": 10, + # "api_taker_fee_bps": 35, + # "api_auction_fee_bps": 20, + # "fix_maker_fee_bps": 10, + # "fix_taker_fee_bps": 35, + # "fix_auction_fee_bps": 20, + # "block_maker_fee_bps": 0, + # "block_taker_fee_bps": 50, + # "notional_30d_volume": 150.00, + # "last_updated_ms": 1551371446000, + # "date": "2019-02-28", + # "notional_1d_volume": [ + # { + # "date": "2019-02-22", + # "notional_volume": 75.00 + # }, + # { + # "date": "2019-02-14", + # "notional_volume": 75.00 + # } + # ] + # } + # + makerBps = self.safe_string(response, 'api_maker_fee_bps') + takerBps = self.safe_string(response, 'api_taker_fee_bps') + makerString = Precise.string_div(makerBps, '10000') + takerString = Precise.string_div(takerBps, '10000') + maker = self.parse_number(makerString) + taker = self.parse_number(takerString) + result: dict = {} + for i in range(0, len(self.symbols)): + symbol = self.symbols[i] + result[symbol] = { + 'info': response, + 'symbol': symbol, + 'maker': maker, + 'taker': taker, + 'percentage': True, + 'tierBased': True, + } + return result + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://docs.gemini.com/rest-api/#get-available-balances + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + response = await self.privatePostV1Balances(params) + return self.parse_balance(response) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder(private) + # + # { + # "order_id":"106027397702", + # "id":"106027397702", + # "symbol":"etheur", + # "exchange":"gemini", + # "avg_execution_price":"2877.48", + # "side":"sell", + # "type":"exchange limit", + # "timestamp":"1650398122", + # "timestampms":1650398122308, + # "is_live":false, + # "is_cancelled":false, + # "is_hidden":false, + # "was_forced":false, + # "executed_amount":"0.014434", + # "client_order_id":"1650398121695", + # "options":[], + # "price":"2800.00", + # "original_amount":"0.014434", + # "remaining_amount":"0" + # } + # + # fetchOrder(private) + # + # { + # "order_id":"106028543717", + # "id":"106028543717", + # "symbol":"etheur", + # "exchange":"gemini", + # "avg_execution_price":"0.00", + # "side":"buy", + # "type":"exchange limit", + # "timestamp":"1650398446", + # "timestampms":1650398446375, + # "is_live":true, + # "is_cancelled":false, + # "is_hidden":false, + # "was_forced":false, + # "executed_amount":"0", + # "client_order_id":"1650398445709", + # "options":[], + # "price":"2000.00", + # "original_amount":"0.01", + # "remaining_amount":"0.01" + # } + # + # fetchOpenOrders(private) + # + # { + # "order_id":"106028543717", + # "id":"106028543717", + # "symbol":"etheur", + # "exchange":"gemini", + # "avg_execution_price":"0.00", + # "side":"buy", + # "type":"exchange limit", + # "timestamp":"1650398446", + # "timestampms":1650398446375, + # "is_live":true, + # "is_cancelled":false, + # "is_hidden":false, + # "was_forced":false, + # "executed_amount":"0", + # "client_order_id":"1650398445709", + # "options":[], + # "price":"2000.00", + # "original_amount":"0.01", + # "remaining_amount":"0.01" + # } + # + # cancelOrder(private) + # + # { + # "order_id":"106028543717", + # "id":"106028543717", + # "symbol":"etheur", + # "exchange":"gemini", + # "avg_execution_price":"0.00", + # "side":"buy", + # "type":"exchange limit", + # "timestamp":"1650398446", + # "timestampms":1650398446375, + # "is_live":false, + # "is_cancelled":true, + # "is_hidden":false, + # "was_forced":false, + # "executed_amount":"0", + # "client_order_id":"1650398445709", + # "reason":"Requested", + # "options":[], + # "price":"2000.00", + # "original_amount":"0.01", + # "remaining_amount":"0.01" + # } + # + timestamp = self.safe_integer(order, 'timestampms') + amount = self.safe_string(order, 'original_amount') + remaining = self.safe_string(order, 'remaining_amount') + filled = self.safe_string(order, 'executed_amount') + status = 'closed' + if order['is_live']: + status = 'open' + if order['is_cancelled']: + status = 'canceled' + price = self.safe_string(order, 'price') + average = self.safe_string(order, 'avg_execution_price') + type = self.safe_string(order, 'type') + if type == 'exchange limit': + type = 'limit' + elif type == 'market buy' or type == 'market sell': + type = 'market' + else: + type = order['type'] + fee = None + marketId = self.safe_string(order, 'symbol') + symbol = self.safe_symbol(marketId, market) + id = self.safe_string(order, 'order_id') + side = self.safe_string_lower(order, 'side') + clientOrderId = self.safe_string(order, 'client_order_id') + optionsArray = self.safe_value(order, 'options', []) + option = self.safe_string(optionsArray, 0) + timeInForce = 'GTC' + postOnly = False + if option is not None: + if option == 'immediate-or-cancel': + timeInForce = 'IOC' + elif option == 'fill-or-kill': + timeInForce = 'FOK' + elif option == 'maker-or-cancel': + timeInForce = 'PO' + postOnly = True + return self.safe_order({ + 'id': id, + 'clientOrderId': clientOrderId, + 'info': order, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'status': status, + 'symbol': symbol, + 'type': type, + 'timeInForce': timeInForce, # default set to GTC + 'postOnly': postOnly, + 'side': side, + 'price': price, + 'triggerPrice': None, + 'average': average, + 'cost': None, + 'amount': amount, + 'filled': filled, + 'remaining': remaining, + 'fee': fee, + 'trades': None, + }, market) + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://docs.gemini.com/rest-api/#order-status + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + request: dict = { + 'order_id': id, + } + response = await self.privatePostV1OrderStatus(self.extend(request, params)) + # + # { + # "order_id":"106028543717", + # "id":"106028543717", + # "symbol":"etheur", + # "exchange":"gemini", + # "avg_execution_price":"0.00", + # "side":"buy", + # "type":"exchange limit", + # "timestamp":"1650398446", + # "timestampms":1650398446375, + # "is_live":true, + # "is_cancelled":false, + # "is_hidden":false, + # "was_forced":false, + # "executed_amount":"0", + # "client_order_id":"1650398445701", + # "options":[], + # "price":"2000.00", + # "original_amount":"0.01", + # "remaining_amount":"0.01" + # } + # + return self.parse_order(response) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://docs.gemini.com/rest-api/#get-active-orders + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + response = await self.privatePostV1Orders(params) + # + # [ + # { + # "order_id":"106028543717", + # "id":"106028543717", + # "symbol":"etheur", + # "exchange":"gemini", + # "avg_execution_price":"0.00", + # "side":"buy", + # "type":"exchange limit", + # "timestamp":"1650398446", + # "timestampms":1650398446375, + # "is_live":true, + # "is_cancelled":false, + # "is_hidden":false, + # "was_forced":false, + # "executed_amount":"0", + # "client_order_id":"1650398445709", + # "options":[], + # "price":"2000.00", + # "original_amount":"0.01", + # "remaining_amount":"0.01" + # } + # ] + # + market = None + if symbol is not None: + market = self.market(symbol) # throws on non-existent symbol + return self.parse_orders(response, market, since, limit) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://docs.gemini.com/rest-api/#new-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: must be 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + if type != 'limit': + raise ExchangeError(self.id + ' createOrder() allows limit orders only') + clientOrderId = self.safe_string_2(params, 'clientOrderId', 'client_order_id') + params = self.omit(params, ['clientOrderId', 'client_order_id']) + if clientOrderId is None: + clientOrderId = str(self.milliseconds()) + market = self.market(symbol) + amountString = self.amount_to_precision(symbol, amount) + priceString = self.price_to_precision(symbol, price) + request: dict = { + 'client_order_id': clientOrderId, + 'symbol': market['id'], + 'amount': amountString, + 'price': priceString, + 'side': side, + 'type': 'exchange limit', # gemini allows limit orders only + # 'options': [], one of: maker-or-cancel, immediate-or-cancel, fill-or-kill, auction-only, indication-of-interest + } + type = self.safe_string(params, 'type', type) + params = self.omit(params, 'type') + triggerPrice = self.safe_string_n(params, ['triggerPrice', 'stop_price', 'stopPrice']) + params = self.omit(params, ['triggerPrice', 'stop_price', 'stopPrice', 'type']) + if type == 'stopLimit': + raise ArgumentsRequired(self.id + ' createOrder() requires a triggerPrice parameter or a stop_price parameter for ' + type + ' orders') + if triggerPrice is not None: + request['stop_price'] = self.price_to_precision(symbol, triggerPrice) + request['type'] = 'exchange stop limit' + else: + # No options can be applied to stop-limit orders at self time. + timeInForce = self.safe_string(params, 'timeInForce') + params = self.omit(params, 'timeInForce') + if timeInForce is not None: + if (timeInForce == 'IOC') or (timeInForce == 'immediate-or-cancel'): + request['options'] = ['immediate-or-cancel'] + elif (timeInForce == 'FOK') or (timeInForce == 'fill-or-kill'): + request['options'] = ['fill-or-kill'] + elif timeInForce == 'PO': + request['options'] = ['maker-or-cancel'] + postOnly = self.safe_bool(params, 'postOnly', False) + params = self.omit(params, 'postOnly') + if postOnly: + request['options'] = ['maker-or-cancel'] + # allowing override for auction-only and indication-of-interest order options + options = self.safe_string(params, 'options') + if options is not None: + request['options'] = [options] + response = await self.privatePostV1OrderNew(self.extend(request, params)) + # + # { + # "order_id":"106027397702", + # "id":"106027397702", + # "symbol":"etheur", + # "exchange":"gemini", + # "avg_execution_price":"2877.48", + # "side":"sell", + # "type":"exchange limit", + # "timestamp":"1650398122", + # "timestampms":1650398122308, + # "is_live":false, + # "is_cancelled":false, + # "is_hidden":false, + # "was_forced":false, + # "executed_amount":"0.014434", + # "client_order_id":"1650398121695", + # "options":[], + # "price":"2800.00", + # "original_amount":"0.014434", + # "remaining_amount":"0" + # } + # + return self.parse_order(response) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://docs.gemini.com/rest-api/#cancel-order + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + request: dict = { + 'order_id': id, + } + response = await self.privatePostV1OrderCancel(self.extend(request, params)) + # + # { + # "order_id":"106028543717", + # "id":"106028543717", + # "symbol":"etheur", + # "exchange":"gemini", + # "avg_execution_price":"0.00", + # "side":"buy", + # "type":"exchange limit", + # "timestamp":"1650398446", + # "timestampms":1650398446375, + # "is_live":false, + # "is_cancelled":true, + # "is_hidden":false, + # "was_forced":false, + # "executed_amount":"0", + # "client_order_id":"1650398445709", + # "reason":"Requested", + # "options":[], + # "price":"2000.00", + # "original_amount":"0.01", + # "remaining_amount":"0.01" + # } + # + return self.parse_order(response) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://docs.gemini.com/rest-api/#get-past-trades + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMyTrades() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['limit_trades'] = limit + if since is not None: + request['timestamp'] = self.parse_to_int(since / 1000) + response = await self.privatePostV1Mytrades(self.extend(request, params)) + return self.parse_trades(response, market, since, limit) + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://docs.gemini.com/rest-api/#withdraw-crypto-funds + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.check_address(address) + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + 'amount': amount, + 'address': address, + } + response = await self.privatePostV1WithdrawCurrency(self.extend(request, params)) + # + # for BTC + # { + # "address":"mi98Z9brJ3TgaKsmvXatuRahbFRUFKRUdR", + # "amount":"1", + # "withdrawalId":"02176a83-a6b1-4202-9b85-1c1c92dd25c4", + # "message":"You have requested a transfer of 1 BTC to mi98Z9brJ3TgaKsmvXatuRahbFRUFKRUdR. This withdrawal will be sent to the blockchain within the next 60 seconds." + # } + # + # for ETH + # { + # "address":"0xA63123350Acc8F5ee1b1fBd1A6717135e82dBd28", + # "amount":"2.34567", + # "txHash":"0x28267179f92926d85c5516bqc063b2631935573d8915258e95d9572eedcc8cc" + # } + # + # for error(other variations of error messages are also expected) + # { + # "result":"error", + # "reason":"CryptoAddressWhitelistsNotEnabled", + # "message":"Cryptocurrency withdrawal address whitelists are not enabled for account 24. Please contact support@gemini.com for information on setting up a withdrawal address whitelist." + # } + # + result = self.safe_string(response, 'result') + if result == 'error': + raise ExchangeError(self.id + ' withdraw() failed: ' + self.json(response)) + return self.parse_transaction(response, currency) + + def nonce(self): + nonceMethod = self.safe_string(self.options, 'nonce', 'milliseconds') + if nonceMethod == 'milliseconds': + return self.milliseconds() + return self.seconds() + + async def fetch_deposits_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch history of deposits and withdrawals + + https://docs.gemini.com/rest-api/#transfers + + :param str [code]: unified currency code for the currency of the deposit/withdrawals, default is None + :param int [since]: timestamp in ms of the earliest deposit/withdrawal, default is None + :param int [limit]: max number of deposit/withdrawals to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `transaction structure ` + """ + await self.load_markets() + request: dict = {} + if limit is not None: + request['limit_transfers'] = limit + if since is not None: + request['timestamp'] = since + response = await self.privatePostV1Transfers(self.extend(request, params)) + return self.parse_transactions(response) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # withdraw + # + # for BTC + # { + # "address":"mi98Z9brJ3TgaKsmvXatuRahbFRUFKRUdR", + # "amount":"1", + # "withdrawalId":"02176a83-a6b1-4202-9b85-1c1c92dd25c4", + # "message":"You have requested a transfer of 1 BTC to mi98Z9brJ3TgaKsmvXatuRahbFRUFKRUdR. This withdrawal will be sent to the blockchain within the next 60 seconds." + # } + # + # for ETH + # { + # "address":"0xA63123350Acc8F5ee1b1fBd1A6717135e82dBd28", + # "amount":"2.34567", + # "txHash":"0x28267179f92926d85c5516bqc063b2631935573d8915258e95d9572eedcc8cc" + # } + # + timestamp = self.safe_integer(transaction, 'timestampms') + currencyId = self.safe_string(transaction, 'currency') + code = self.safe_currency_code(currencyId, currency) + address = self.safe_string(transaction, 'destination') + type = self.safe_string_lower(transaction, 'type') + # if status field is available, then it's complete + statusRaw = self.safe_string(transaction, 'status') + fee = None + feeAmount = self.safe_number(transaction, 'feeAmount') + if feeAmount is not None: + fee = { + 'cost': feeAmount, + 'currency': code, + } + return { + 'info': transaction, + 'id': self.safe_string_2(transaction, 'eid', 'withdrawalId'), + 'txid': self.safe_string(transaction, 'txHash'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': None, + 'address': address, + 'addressTo': None, + 'addressFrom': None, + 'tag': None, # or is it defined? + 'tagTo': None, + 'tagFrom': None, + 'type': type, # direction of the transaction,('deposit' | 'withdraw') + 'amount': self.safe_number(transaction, 'amount'), + 'currency': code, + 'status': self.parse_transaction_status(statusRaw), + 'updated': None, + 'internal': None, + 'comment': self.safe_string(transaction, 'message'), + 'fee': fee, + } + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'Advanced': 'ok', + 'Complete': 'ok', + } + return self.safe_string(statuses, status, status) + + def parse_deposit_address(self, depositAddress, currency: Currency = None): + # + # { + # "address": "0xed6494Fe7c1E56d1bd6136e89268C51E32d9708B", + # "timestamp": "1636813923098", + # "addressVersion": "eV1" } + # } + # + address = self.safe_string(depositAddress, 'address') + code = self.safe_currency_code(None, currency) + return { + 'currency': code, + 'network': None, + 'address': address, + 'tag': None, + 'info': depositAddress, + } + + async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + + https://docs.gemini.com/rest-api/#get-deposit-addresses + + fetch the deposit address for a currency associated with self account + :param str code: unified currency code + :param dict [params]: extra parameters specific to the endpoint + :param str [params.network]: *required* The chain of currency + :returns dict: an `address structure ` + """ + await self.load_markets() + groupedByNetwork = await self.fetch_deposit_addresses_by_network(code, params) + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + networkGroup = self.index_by(self.safe_value(groupedByNetwork, networkCode), 'currency') + return self.safe_value(networkGroup, code) + + async def fetch_deposit_addresses_by_network(self, code: str, params={}) -> List[DepositAddress]: + """ + fetch a dictionary of addresses for a currency, indexed by network + + https://docs.gemini.com/rest-api/#get-deposit-addresses + + :param str code: unified currency code of the currency for the deposit address + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.network]: *required* The chain of currency + :returns dict: a dictionary of `address structures ` indexed by the network + """ + await self.load_markets() + currency = self.currency(code) + code = currency['code'] + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + if networkCode is None: + raise ArgumentsRequired(self.id + ' fetchDepositAddresses() requires a network parameter') + networkId = self.network_code_to_id(networkCode) + request: dict = { + 'network': networkId, + } + response = await self.privatePostV1AddressesNetwork(self.extend(request, params)) + results = self.parse_deposit_addresses(response, [code], False, {'network': networkCode, 'currency': code}) + return self.group_by(results, 'network') + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = '/' + self.implode_params(path, params) + query = self.omit(params, self.extract_params(path)) + if api == 'private': + self.check_required_credentials() + apiKey = self.apiKey + if apiKey.find('account') < 0: + raise AuthenticationError(self.id + ' sign() requires an account-key, master-keys are not-supported') + nonce = str(self.nonce()) + request = self.extend({ + 'request': url, + 'nonce': nonce, + }, query) + payload = self.json(request) + payload = self.string_to_base64(payload) + signature = self.hmac(self.encode(payload), self.encode(self.secret), hashlib.sha384) + headers = { + 'Content-Type': 'text/plain', + 'X-GEMINI-APIKEY': self.apiKey, + 'X-GEMINI-PAYLOAD': payload, + 'X-GEMINI-SIGNATURE': signature, + } + else: + if query: + url += '?' + self.urlencode(query) + url = self.urls['api'][api] + url + if (method == 'POST') or (method == 'DELETE'): + body = self.json(query) + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + if isinstance(body, str): + feedback = self.id + ' ' + body + self.throw_broadly_matched_exception(self.exceptions['broad'], body, feedback) + return None # fallback to default error handler + # + # { + # "result": "error", + # "reason": "BadNonce", + # "message": "Out-of-sequence nonce <1234> precedes previously used nonce <2345>" + # } + # + result = self.safe_string(response, 'result') + if result == 'error': + reasonInner = self.safe_string(response, 'reason') + message = self.safe_string(response, 'message') + feedback = self.id + ' ' + message + self.throw_exactly_matched_exception(self.exceptions['exact'], reasonInner, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + raise ExchangeError(feedback) # unknown message + return None + + async def create_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + create a currency deposit address + + https://docs.gemini.com/rest-api/#new-deposit-address + + :param str code: unified currency code of the currency for the deposit address + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + } + response = await self.privatePostV1DepositCurrencyNewAddress(self.extend(request, params)) + address = self.safe_string(response, 'address') + self.check_address(address) + return { + 'currency': code, + 'address': address, + 'tag': None, + 'network': None, + 'info': response, + } + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://docs.gemini.com/rest-api/#candles + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + timeframeId = self.safe_string(self.timeframes, timeframe, timeframe) + request: dict = { + 'timeframe': timeframeId, + 'symbol': market['id'], + } + response = await self.publicGetV2CandlesSymbolTimeframe(self.extend(request, params)) + # + # [ + # [1591515000000,0.02509,0.02509,0.02509,0.02509,0], + # [1591514700000,0.02503,0.02509,0.02503,0.02509,44.6405], + # [1591514400000,0.02503,0.02503,0.02503,0.02503,0], + # ] + # + return self.parse_ohlcvs(response, market, timeframe, since, limit) diff --git a/ccxt/async_support/hashkey.py b/ccxt/async_support/hashkey.py new file mode 100644 index 0000000..6a7e21d --- /dev/null +++ b/ccxt/async_support/hashkey.py @@ -0,0 +1,4193 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.hashkey import ImplicitAPI +import hashlib +from ccxt.base.types import Account, Any, Balances, Bool, Currencies, Currency, DepositAddress, Int, LastPrice, LastPrices, LedgerEntry, Leverage, LeverageTier, LeverageTiers, Market, Num, Order, OrderBook, OrderRequest, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, FundingRates, Trade, TradingFeeInterface, TradingFees, Transaction, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import AccountNotEnabled +from ccxt.base.errors import AccountSuspended +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import OperationRejected +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidAddress +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import OrderImmediatelyFillable +from ccxt.base.errors import OrderNotFillable +from ccxt.base.errors import DuplicateOrderId +from ccxt.base.errors import ContractUnavailable +from ccxt.base.errors import NotSupported +from ccxt.base.errors import OperationFailed +from ccxt.base.errors import DDoSProtection +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.errors import InvalidNonce +from ccxt.base.errors import RequestTimeout +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class hashkey(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(hashkey, self).describe(), { + 'id': 'hashkey', + 'name': 'HashKey Global', + 'countries': ['BM'], # Bermuda + 'rateLimit': 100, + 'version': 'v1', + 'certified': True, + 'pro': True, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelAllOrders': True, + 'cancelAllOrdersAfter': False, + 'cancelOrder': True, + 'cancelOrders': True, + 'cancelWithdraw': False, + 'closeAllPositions': False, + 'closePosition': False, + 'createConvertTrade': False, + 'createDepositAddress': False, + 'createMarketBuyOrderWithCost': True, + 'createMarketOrder': True, + 'createMarketOrderWithCost': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createOrderWithTakeProfitAndStopLoss': False, + 'createReduceOnlyOrder': True, + 'createStopLimitOrder': True, + 'createStopLossOrder': False, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'createTakeProfitOrder': False, + 'createTrailingAmountOrder': False, + 'createTrailingPercentOrder': False, + 'createTriggerOrder': True, + 'fetchAccounts': True, + 'fetchAllGreeks': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchCanceledAndClosedOrders': True, + 'fetchCanceledOrders': True, + 'fetchClosedOrder': True, + 'fetchClosedOrders': False, + 'fetchConvertCurrencies': False, + 'fetchConvertQuote': False, + 'fetchConvertTrade': False, + 'fetchConvertTradeHistory': False, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchDepositsWithdrawals': False, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': True, + 'fetchFundingRateHistory': True, + 'fetchFundingRates': True, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLedger': True, + 'fetchLeverage': True, + 'fetchLeverages': False, + 'fetchLeverageTiers': True, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': 'emulated', + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrice': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrder': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': False, + 'fetchOrderTrades': False, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': True, + 'fetchPositionsForSymbol': True, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchStatus': True, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': True, # emulated for spot markets + 'fetchTradingFees': True, # for spot markets only + 'fetchTransactions': False, + 'fetchTransfers': False, + 'fetchUnderlyingAssets': False, + 'fetchVolatilityHistory': False, + 'fetchWithdrawals': True, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'sandbox': False, + 'setLeverage': True, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'transfer': True, + 'withdraw': True, + }, + 'timeframes': { + '1m': '1m', + '3m': '3m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1h', + '2h': '2h', + '4h': '4h', + '6h': '6h', + '8h': '8h', + '12h': '12h', + '1d': '1d', + '1w': '1w', + '1M': '1M', + }, + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/6dd6127b-cc19-4a13-9b29-a98d81f80e98', + 'api': { + 'public': 'https://api-glb.hashkey.com', + 'private': 'https://api-glb.hashkey.com', + }, + 'test': { + 'public': 'https://api-glb.sim.hashkeydev.com', + 'private': 'https://api-glb.sim.hashkeydev.com', + }, + 'www': 'https://global.hashkey.com/', + 'doc': 'https://hashkeyglobal-apidoc.readme.io/', + 'fees': 'https://support.global.hashkey.com/hc/en-us/articles/13199900083612-HashKey-Global-Fee-Structure', + 'referral': 'https://global.hashkey.com/en-US/register/invite?invite_code=82FQUN', + }, + 'api': { + 'public': { + 'get': { + 'api/v1/exchangeInfo': 5, + 'quote/v1/depth': 1, + 'quote/v1/trades': 1, + 'quote/v1/klines': 1, + 'quote/v1/ticker/24hr': 1, + 'quote/v1/ticker/price': 1, + 'quote/v1/ticker/bookTicker': 1, # not unified + 'quote/v1/depth/merged': 1, + 'quote/v1/markPrice': 1, + 'quote/v1/index': 1, + 'api/v1/futures/fundingRate': 1, + 'api/v1/futures/historyFundingRate': 1, + 'api/v1/ping': 1, + 'api/v1/time': 1, + }, + }, + 'private': { + 'get': { + 'api/v1/spot/order': 1, + 'api/v1/spot/openOrders': 1, + 'api/v1/spot/tradeOrders': 5, + 'api/v1/futures/leverage': 1, + 'api/v1/futures/order': 1, + 'api/v1/futures/openOrders': 1, + 'api/v1/futures/userTrades': 1, + 'api/v1/futures/positions': 1, + 'api/v1/futures/historyOrders': 1, + 'api/v1/futures/balance': 1, + 'api/v1/futures/liquidationAssignStatus': 1, + 'api/v1/futures/riskLimit': 1, + 'api/v1/futures/commissionRate': 1, + 'api/v1/futures/getBestOrder': 1, + 'api/v1/account/vipInfo': 1, + 'api/v1/account': 1, + 'api/v1/account/trades': 5, + 'api/v1/account/type': 5, + 'api/v1/account/checkApiKey': 1, + 'api/v1/account/balanceFlow': 5, + 'api/v1/spot/subAccount/openOrders': 1, + 'api/v1/spot/subAccount/tradeOrders': 1, + 'api/v1/subAccount/trades': 1, + 'api/v1/futures/subAccount/openOrders': 1, + 'api/v1/futures/subAccount/historyOrders': 1, + 'api/v1/futures/subAccount/userTrades': 1, + 'api/v1/account/deposit/address': 1, + 'api/v1/account/depositOrders': 1, + 'api/v1/account/withdrawOrders': 1, + }, + 'post': { + 'api/v1/userDataStream': 1, + 'api/v1/spot/orderTest': 1, + 'api/v1/spot/order': 1, + 'api/v1.1/spot/order': 1, + 'api/v1/spot/batchOrders': 5, + 'api/v1/futures/leverage': 1, + 'api/v1/futures/order': 1, + 'api/v1/futures/position/trading-stop': 3, + 'api/v1/futures/batchOrders': 5, + 'api/v1/account/assetTransfer': 1, + 'api/v1/account/authAddress': 1, + 'api/v1/account/withdraw': 1, + }, + 'put': { + 'api/v1/userDataStream': 1, + }, + 'delete': { + 'api/v1/spot/order': 1, + 'api/v1/spot/openOrders': 5, + 'api/v1/spot/cancelOrderByIds': 5, + 'api/v1/futures/order': 1, + 'api/v1/futures/batchOrders': 1, + 'api/v1/futures/cancelOrderByIds': 1, + 'api/v1/userDataStream': 1, + }, + }, + }, + 'fees': { + 'trading': { + 'spot': { + 'tierBased': True, + 'percentage': True, + 'feeSide': 'get', + 'maker': self.parse_number('0.0012'), + 'taker': self.parse_number('0.0012'), + 'tiers': { + 'maker': [ + [self.parse_number('0'), self.parse_number('0.0012')], + [self.parse_number('1000000'), self.parse_number('0.00080')], + [self.parse_number('5000000'), self.parse_number('0.00070')], + [self.parse_number('10000000'), self.parse_number('0.00060')], + [self.parse_number('50000000'), self.parse_number('0.00040')], + [self.parse_number('200000000'), self.parse_number('0.00030')], + [self.parse_number('400000000'), self.parse_number('0.00010')], + [self.parse_number('800000000'), self.parse_number('0.00')], + ], + 'taker': [ + [self.parse_number('0'), self.parse_number('0.0012')], + [self.parse_number('1000000'), self.parse_number('0.00090')], + [self.parse_number('5000000'), self.parse_number('0.00085')], + [self.parse_number('10000000'), self.parse_number('0.00075')], + [self.parse_number('50000000'), self.parse_number('0.00065')], + [self.parse_number('200000000'), self.parse_number('0.00045')], + [self.parse_number('400000000'), self.parse_number('0.00040')], + [self.parse_number('800000000'), self.parse_number('0.00035')], + ], + }, + }, + 'swap': { + 'tierBased': True, + 'percentage': True, + 'feeSide': 'get', + 'maker': self.parse_number('0.00025'), + 'taker': self.parse_number('0.00060'), + 'tiers': { + 'maker': [ + [self.parse_number('0'), self.parse_number('0.00025')], + [self.parse_number('1000000'), self.parse_number('0.00016')], + [self.parse_number('5000000'), self.parse_number('0.00014')], + [self.parse_number('10000000'), self.parse_number('0.00012')], + [self.parse_number('50000000'), self.parse_number('0.000080')], + [self.parse_number('200000000'), self.parse_number('0.000060')], + [self.parse_number('400000000'), self.parse_number('0.000020')], + [self.parse_number('800000000'), self.parse_number('0.00')], + ], + 'taker': [ + [self.parse_number('0'), self.parse_number('0.00060')], + [self.parse_number('1000000'), self.parse_number('0.00050')], + [self.parse_number('5000000'), self.parse_number('0.00045')], + [self.parse_number('10000000'), self.parse_number('0.00040')], + [self.parse_number('50000000'), self.parse_number('0.00035')], + [self.parse_number('200000000'), self.parse_number('0.00030')], + [self.parse_number('400000000'), self.parse_number('0.00025')], + [self.parse_number('800000000'), self.parse_number('0.00020')], + ], + }, + }, + }, + }, + 'options': { + 'broker': '10000700011', + 'recvWindow': None, + 'sandboxMode': False, + 'networks': { + 'BTC': 'BTC', + 'ERC20': 'ETH', + 'AVAX': 'AvalancheC', + 'SOL': 'Solana', + 'MATIC': 'Polygon', + 'ATOM': 'Cosmos', + 'DOT': 'Polkadot', + 'LTC': 'LTC', + 'OPTIMISM': 'Optimism', + 'ARB': 'Arbitrum', + 'DOGE': 'Dogecoin', + 'TRC20': 'Tron', + 'ZKSYNC': 'zkSync', + 'TON': 'TON', + 'KLAYTN': 'Klaytn', + 'MERLINCHAIN': 'Merlin Chain', + }, + 'networksById': { + 'BTC': 'BTC', + 'Bitcoin': 'BTC', + 'ETH': 'ERC20', + 'ERC20': 'ERC20', + 'AvalancheC': 'AVAX', + 'AVAX C-Chain': 'AVAX', + 'Solana': 'SOL', + 'Cosmos': 'ATOM', + 'Arbitrum': 'ARB', + 'Polygon': 'MATIC', + 'Optimism': 'OPTIMISM', + 'Polkadot': 'DOT', + 'LTC': 'LTC', + 'Litecoin': 'LTC', + 'Dogecoin': 'DOGE', + 'Merlin Chain': 'MERLINCHAIN', + 'zkSync': 'ZKSYNC', + 'TRC20': 'TRC20', + 'Tron': 'TRC20', + 'TON': 'TON', + 'BSC(BEP20)': 'BSC', + 'Klaytn': 'KLAYTN', + }, + 'defaultNetwork': 'ERC20', + }, + 'features': { + 'default': { + 'sandbox': True, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': False, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': True, # todo fix + 'selfTradePrevention': True, # todo implement + 'iceberg': False, + }, + 'createOrders': { + 'max': 20, + }, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 30, + 'untilDays': 30, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 1000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': None, # todo + 'fetchOHLCV': { + 'limit': 1000, + }, + }, + 'spot': { + 'extends': 'default', + }, + 'forDerivatives': { + 'extends': 'default', + 'createOrder': { + 'triggerPrice': True, + 'selfTradePrevention': True, + }, + 'fetchOpenOrders': { + 'trigger': True, + 'limit': 500, + }, + }, + 'swap': { + 'linear': { + 'extends': 'forDerivatives', + }, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'commonCurrencies': {}, + 'exceptions': { + 'exact': { + '0001': BadRequest, # Required field '%s' missing or invalid. + '0002': AuthenticationError, # Incorrect signature + '0003': RateLimitExceeded, # Rate limit exceeded + '0102': AuthenticationError, # Invalid APIKey + '0103': AuthenticationError, # APIKey expired + '0104': PermissionDenied, # The accountId defined is not permissible + '0201': ExchangeError, # Instrument not found + '0202': PermissionDenied, # Invalid IP + '0206': BadRequest, # Unsupported order type + '0207': BadRequest, # Invalid price + '0209': BadRequest, # Invalid price precision + '0210': BadRequest, # Price outside of allowed range + '0211': OrderNotFound, # Order not found + '0401': InsufficientFunds, # Insufficient asset + '0402': BadRequest, # Invalid asset + '-1000': ExchangeError, # An unknown error occurred while processing the request + '-1001': ExchangeError, # Internal error + '-100010': BadSymbol, # Invalid Symbols! + '-100012': BadSymbol, # Parameter symbol [str] missing! + '-1002': AuthenticationError, # Unauthorized operation + '-1004': BadRequest, # Bad request + '-1005': PermissionDenied, # No permission + '-1006': ExchangeError, # Execution status unknown + '-1007': RequestTimeout, # Timeout waiting for response from server + '-1014': InvalidOrder, # Unsupported order combination + '-1015': InvalidOrder, # Too many new orders + '-1020': OperationRejected, # Unsupported operation + '-1021': InvalidNonce, # Timestamp for self request is outside of the recvWindow + '-1024': BadRequest, # Duplicate request + '-1101': ExchangeNotAvailable, # Feature has been offline + '-1115': InvalidOrder, # Invalid timeInForce + '-1117': InvalidOrder, # Invalid order side + '-1123': InvalidOrder, # Invalid client order id + '-1124': InvalidOrder, # Invalid price + '-1126': InvalidOrder, # Invalid quantity + '-1129': BadRequest, # Invalid parameters, quantity and amount are not allowed to be sent at the same time. + '-1130': BadRequest, # Illegal parameter '%s' + '-1132': BadRequest, # Order price greater than the maximum + '-1133': BadRequest, # Order price lower than the minimum + '-1135': BadRequest, # Order quantity greater than the maximum + '-1136': BadRequest, # Order quantity lower than the minimum + '-1138': InvalidOrder, # Order has been partially cancelled + '-1137': InvalidOrder, # Order quantity precision too large + '-1139': OrderImmediatelyFillable, # Order has been filled + '-1140': InvalidOrder, # Order amount lower than the minimum + '-1141': DuplicateOrderId, # Duplicate order + '-1142': OrderNotFillable, # Order has been cancelled + '-1143': OrderNotFound, # Order not found on order book + '-1144': OperationRejected, # Order has been locked + '-1145': NotSupported, # Cancellation on self order type not supported + '-1146': RequestTimeout, # Order creation timeout + '-1147': RequestTimeout, # Order cancellation timeout + '-1148': InvalidOrder, # Order amount precision too large + '-1149': OperationRejected, # Order creation failed + '-1150': OperationFailed, # Order cancellation failed + '-1151': OperationRejected, # The trading pair is not open yet + '-1152': AccountNotEnabled, # User does not exist + '-1153': InvalidOrder, # Invalid price type + '-1154': InvalidOrder, # Invalid position side + '-1155': OperationRejected, # The trading pair is not available for api trading + '-1156': OperationFailed, # Limit maker order creation failed + '-1157': OperationFailed, # Modify futures margin failed + '-1158': OperationFailed, # Reduce margin is forbidden + '-1159': AccountNotEnabled, # Finance account already exists + '-1160': AccountNotEnabled, # Account does not exist + '-1161': OperationFailed, # Balance transfer failed + '-1162': ContractUnavailable, # Unsupport contract address + '-1163': InvalidAddress, # Illegal withdrawal address + '-1164': OperationFailed, # Withdraw failed + '-1165': ArgumentsRequired, # Withdrawal amount cannot be null + '-1166': OperationRejected, # Withdrawal amount exceeds the daily limit + '-1167': BadRequest, # Withdrawal amount less than the minimum + '-1168': BadRequest, # Illegal withdrawal amount + '-1169': PermissionDenied, # Withdraw not allowed + '-1170': PermissionDenied, # Deposit not allowed + '-1171': PermissionDenied, # Withdrawal address not in whitelist + '-1172': BadRequest, # Invalid from account id + '-1173': BadRequest, # Invalid to account i + '-1174': PermissionDenied, # Transfer not allowed between the same account + '-1175': BadRequest, # Invalid fiat deposit status + '-1176': BadRequest, # Invalid fiat withdrawal status + '-1177': InvalidOrder, # Invalid fiat order type + '-1178': AccountNotEnabled, # Brokerage account does not exist + '-1179': AccountSuspended, # Address owner is not True + '-1181': ExchangeError, # System error + '-1193': OperationRejected, # Order creation count exceeds the limit + '-1194': OperationRejected, # Market order creation forbidden + '-1195': BadRequest, # Market order long position cannot exceed %s above the market price + '-1196': BadRequest, # Market order short position cannot be below %s of the market price + '-1200': BadRequest, # Order buy quantity too small + '-1201': BadRequest, # Order buy quantity too large + '-1202': BadRequest, # Order sell quantity too small + '-1203': BadRequest, # Order sell quantity too large + '-1204': BadRequest, # From account must be a main account + '-1205': AccountNotEnabled, # Account not authorized + '-1206': BadRequest, # Order amount greater than the maximum + '-1207': BadRequest, # The status of deposit is invalid + '-1208': BadRequest, # The orderType of fiat is invalid + '-1209': BadRequest, # The status of withdraw is invalid + '-2001': ExchangeNotAvailable, # Platform is yet to open trading + '-2002': OperationFailed, # The number of open orders exceeds the limit 300 + '-2003': OperationFailed, # Position size cannot meet target leverage + '-2004': OperationFailed, # Adjust leverage fail + '-2005': RequestTimeout, # Adjust leverage timeout + '-2010': OperationRejected, # New order rejected + '-2011': OperationRejected, # Order cancellation rejected + '-2016': OperationRejected, # API key creation exceeds the limit + '-2017': OperationRejected, # Open orders exceeds the limit of the trading pair + '-2018': OperationRejected, # Trade user creation exceeds the limit + '-2019': PermissionDenied, # Trader and omnibus user not allowed to login app + '-2020': PermissionDenied, # Not allowed to trade self trading pair + '-2021': PermissionDenied, # Not allowed to trade self trading pair + '-2022': OperationRejected, # Order batch size exceeds the limit + '-2023': AuthenticationError, # Need to pass KYC verification + '-2024': AccountNotEnabled, # Fiat account does not exist + '-2025': AccountNotEnabled, # Custody account not exist + '-2026': BadRequest, # Invalid type + '-2027': OperationRejected, # Exceed maximum time range of 30 days + '-2028': OperationRejected, # The search is limited to data within the last one month + '-2029': OperationRejected, # The search is limited to data within the last three months + '-2030': InsufficientFunds, # Insufficient margin + '-2031': NotSupported, # Leverage reduction is not supported in Isolated Margin Mode with open positions + '-2032': OperationRejected, # After the transaction, your %s position will account for %s of the total position, which poses concentration risk. Do you want to continue with the transaction? + '-2033': OperationFailed, # Order creation failed. Please verify if the order parameters comply with the trading rules + '-2034': InsufficientFunds, # Trade account holding limit is zero + '-2035': OperationRejected, # The sub account has been frozen and cannot transfer + '-2036': NotSupported, # We do not support queries for records exceeding 30 days + '-2037': ExchangeError, # Position and order data error + '-2038': InsufficientFunds, # Insufficient margin + '-2039': NotSupported, # Leverage reduction is not supported in Isolated Margin Mode with open positions + '-2040': ExchangeNotAvailable, # There is a request being processed. Please try again later + '-2041': BadRequest, # Token does not exist + '-2042': OperationRejected, # You have passed the trade limit, please pay attention to the risks + '-2043': OperationRejected, # Maximum allowed leverage reached, please lower your leverage + '-2044': BadRequest, # This order price is unreasonable to exceed(or be lower than) the liquidation price + '-2045': BadRequest, # Price too low, please order again! + '-2046': BadRequest, # Price too high, please order again! + '-2048': BadRequest, # Exceed the maximum number of conditional orders of %s + '-2049': BadRequest, # Create stop order buy price too big + '-2050': BadRequest, # Create stop order sell price too small + '-2051': OperationRejected, # Create order rejected + '-2052': OperationRejected, # Create stop profit-loss plan order reject + '-2053': OperationRejected, # Position not enough + '-2054': BadRequest, # Invalid long stop profit price + '-2055': BadRequest, # Invalid long stop loss price + '-2056': BadRequest, # Invalid short stop profit price + '-2057': BadRequest, # Invalid short stop loss price + '-3117': PermissionDenied, # Invalid permission + '-3143': PermissionDenied, # According to KYC and risk assessment, your trading account has exceeded the limit. + '-3144': PermissionDenied, # Currently, your trading account has exceeded its limit and is temporarily unable to perform transfers + '-3145': DDoSProtection, # Please DO NOT submit request too frequently + '-4001': BadRequest, # Invalid asset + '-4002': BadRequest, # Withdrawal amount less than Minimum Withdrawal Amount + '-4003': InsufficientFunds, # Insufficient Balance + '-4004': BadRequest, # Invalid bank account number + '-4005': BadRequest, # Assets are not listed + '-4006': AccountNotEnabled, # KYC is not certified + '-4007': NotSupported, # Withdrawal channels are not supported + '-4008': AccountNotEnabled, # This currency does not support self customer type + '-4009': PermissionDenied, # No withdrawal permission + '-4010': PermissionDenied, # Withdrawals on the same day exceed the maximum limit for a single day + '-4011': ExchangeError, # System error + '-4012': ExchangeError, # Parameter error + '-4013': OperationFailed, # Withdraw repeatly + }, + 'broad': {}, + }, + 'precisionMode': TICK_SIZE, + }) + + async def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://hashkeyglobal-apidoc.readme.io/reference/check-server-time + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = await self.publicGetApiV1Time(params) + # + # { + # "serverTime": 1721661553214 + # } + # + return self.safe_integer(response, 'serverTime') + + async def fetch_status(self, params={}): + """ + the latest known information on the availability of the exchange API + + https://hashkeyglobal-apidoc.readme.io/reference/test-connectivity + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `status structure ` + """ + response = await self.publicGetApiV1Ping(params) + # + # {} + # + return { + 'status': 'ok', + 'updated': None, + 'eta': None, + 'url': None, + 'info': response, + } + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for the exchange + + https://hashkeyglobal-apidoc.readme.io/reference/exchangeinfo + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.symbol]: the id of the market to fetch + :returns dict[]: an array of objects representing market data + """ + request: dict = {} + response = await self.publicGetApiV1ExchangeInfo(self.extend(request, params)) + # + # { + # "timezone": "UTC", + # "serverTime": "1721661653952", + # "brokerFilters": [], + # "symbols": [ + # { + # "symbol": "BTCUSDT", + # "symbolName": "BTCUSDT", + # "status": "TRADING", + # "baseAsset": "BTC", + # "baseAssetName": "BTC", + # "baseAssetPrecision": "0.00001", + # "quoteAsset": "USDT", + # "quoteAssetName": "USDT", + # "quotePrecision": "0.0000001", + # "retailAllowed": True, + # "piAllowed": True, + # "corporateAllowed": True, + # "omnibusAllowed": True, + # "icebergAllowed": False, + # "isAggregate": False, + # "allowMargin": False, + # "filters": [ + # { + # "minPrice": "0.01", + # "maxPrice": "100000.00000000", + # "tickSize": "0.01", + # "filterType": "PRICE_FILTER" + # }, + # { + # "minQty": "0.00001", + # "maxQty": "8", + # "stepSize": "0.00001", + # "marketOrderMinQty": "0.00001", + # "marketOrderMaxQty": "4", + # "filterType": "LOT_SIZE" + # }, + # { + # "minNotional": "1", + # "filterType": "MIN_NOTIONAL" + # }, + # { + # "minAmount": "1", + # "maxAmount": "400000", + # "minBuyPrice": "0", + # "marketOrderMinAmount": "1", + # "marketOrderMaxAmount": "200000", + # "filterType": "TRADE_AMOUNT" + # }, + # { + # "maxSellPrice": "0", + # "buyPriceUpRate": "0.1", + # "sellPriceDownRate": "0.1", + # "filterType": "LIMIT_TRADING" + # }, + # { + # "buyPriceUpRate": "0.1", + # "sellPriceDownRate": "0.1", + # "filterType": "MARKET_TRADING" + # }, + # { + # "noAllowMarketStartTime": "1710485700000", + # "noAllowMarketEndTime": "1710486000000", + # "limitOrderStartTime": "0", + # "limitOrderEndTime": "0", + # "limitMinPrice": "0", + # "limitMaxPrice": "0", + # "filterType": "OPEN_QUOTE" + # } + # ] + # } + # ], + # "options": [], + # "contracts": [ + # { + # "filters": [ + # { + # "minPrice": "0.1", + # "maxPrice": "100000.00000000", + # "tickSize": "0.1", + # "filterType": "PRICE_FILTER" + # }, + # { + # "minQty": "0.001", + # "maxQty": "10", + # "stepSize": "0.001", + # "marketOrderMinQty": "0", + # "marketOrderMaxQty": "0", + # "filterType": "LOT_SIZE" + # }, + # { + # "minNotional": "0", + # "filterType": "MIN_NOTIONAL" + # }, + # { + # "maxSellPrice": "999999", + # "buyPriceUpRate": "0.05", + # "sellPriceDownRate": "0.05", + # "maxEntrustNum": 200, + # "maxConditionNum": 200, + # "filterType": "LIMIT_TRADING" + # }, + # { + # "buyPriceUpRate": "0.05", + # "sellPriceDownRate": "0.05", + # "filterType": "MARKET_TRADING" + # }, + # { + # "noAllowMarketStartTime": "0", + # "noAllowMarketEndTime": "0", + # "limitOrderStartTime": "0", + # "limitOrderEndTime": "0", + # "limitMinPrice": "0", + # "limitMaxPrice": "0", + # "filterType": "OPEN_QUOTE" + # } + # ], + # "exchangeId": "301", + # "symbol": "BTCUSDT-PERPETUAL", + # "symbolName": "BTCUSDT-PERPETUAL", + # "status": "TRADING", + # "baseAsset": "BTCUSDT-PERPETUAL", + # "baseAssetPrecision": "0.001", + # "quoteAsset": "USDT", + # "quoteAssetPrecision": "0.1", + # "icebergAllowed": False, + # "inverse": False, + # "index": "USDT", + # "marginToken": "USDT", + # "marginPrecision": "0.0001", + # "contractMultiplier": "0.001", + # "underlying": "BTC", + # "riskLimits": [ + # { + # "riskLimitId": "200000722", + # "quantity": "1000.00", + # "initialMargin": "0.10", + # "maintMargin": "0.005", + # "isWhite": False + # }, + # { + # "riskLimitId": "200000723", + # "quantity": "2000.00", + # "initialMargin": "0.10", + # "maintMargin": "0.01", + # "isWhite": False + # } + # ] + # } + # ], + # "coins": [ + # { + # "orgId": "9001", + # "coinId": "BTC", + # "coinName": "BTC", + # "coinFullName": "Bitcoin", + # "allowWithdraw": True, + # "allowDeposit": True, + # "tokenType": "CHAIN_TOKEN", + # "chainTypes": [ + # { + # "chainType": "Bitcoin", + # "withdrawFee": "0", + # "minWithdrawQuantity": "0.002", + # "maxWithdrawQuantity": "0", + # "minDepositQuantity": "0.0005", + # "allowDeposit": True, + # "allowWithdraw": True + # } + # ] + # } + # ] + # } + # + spotMarkets = self.safe_list(response, 'symbols', []) + swapMarkets = self.safe_list(response, 'contracts', []) + markets = self.array_concat(spotMarkets, swapMarkets) + if self.is_empty(markets): + markets = [response] # if user provides params.symbol the exchange returns a single object insted of list of objects + return self.parse_markets(markets) + + def parse_market(self, market: dict) -> Market: + # spot + # { + # "symbol": "BTCUSDT", + # "symbolName": "BTCUSDT", + # "status": "TRADING", + # "baseAsset": "BTC", + # "baseAssetName": "BTC", + # "baseAssetPrecision": "0.00001", + # "quoteAsset": "USDT", + # "quoteAssetName": "USDT", + # "quotePrecision": "0.0000001", + # "retailAllowed": True, + # "piAllowed": True, + # "corporateAllowed": True, + # "omnibusAllowed": True, + # "icebergAllowed": False, + # "isAggregate": False, + # "allowMargin": False, + # "filters": [ + # { + # "minPrice": "0.01", + # "maxPrice": "100000.00000000", + # "tickSize": "0.01", + # "filterType": "PRICE_FILTER" + # }, + # { + # "minQty": "0.00001", + # "maxQty": "8", + # "stepSize": "0.00001", + # "marketOrderMinQty": "0.00001", + # "marketOrderMaxQty": "4", + # "filterType": "LOT_SIZE" + # }, + # { + # "minNotional": "1", + # "filterType": "MIN_NOTIONAL" + # }, + # { + # "minAmount": "1", + # "maxAmount": "400000", + # "minBuyPrice": "0", + # "marketOrderMinAmount": "1", + # "marketOrderMaxAmount": "200000", + # "filterType": "TRADE_AMOUNT" + # }, + # { + # "maxSellPrice": "0", + # "buyPriceUpRate": "0.1", + # "sellPriceDownRate": "0.1", + # "filterType": "LIMIT_TRADING" + # }, + # { + # "buyPriceUpRate": "0.1", + # "sellPriceDownRate": "0.1", + # "filterType": "MARKET_TRADING" + # }, + # { + # "noAllowMarketStartTime": "1710485700000", + # "noAllowMarketEndTime": "1710486000000", + # "limitOrderStartTime": "0", + # "limitOrderEndTime": "0", + # "limitMinPrice": "0", + # "limitMaxPrice": "0", + # "filterType": "OPEN_QUOTE" + # } + # ] + # } + # + # swap + # { + # "filters": [ + # { + # "minPrice": "0.1", + # "maxPrice": "100000.00000000", + # "tickSize": "0.1", + # "filterType": "PRICE_FILTER" + # }, + # { + # "minQty": "0.001", + # "maxQty": "10", + # "stepSize": "0.001", + # "marketOrderMinQty": "0", + # "marketOrderMaxQty": "0", + # "filterType": "LOT_SIZE" + # }, + # { + # "minNotional": "0", + # "filterType": "MIN_NOTIONAL" + # }, + # { + # "maxSellPrice": "999999", + # "buyPriceUpRate": "0.05", + # "sellPriceDownRate": "0.05", + # "maxEntrustNum": 200, + # "maxConditionNum": 200, + # "filterType": "LIMIT_TRADING" + # }, + # { + # "buyPriceUpRate": "0.05", + # "sellPriceDownRate": "0.05", + # "filterType": "MARKET_TRADING" + # }, + # { + # "noAllowMarketStartTime": "0", + # "noAllowMarketEndTime": "0", + # "limitOrderStartTime": "0", + # "limitOrderEndTime": "0", + # "limitMinPrice": "0", + # "limitMaxPrice": "0", + # "filterType": "OPEN_QUOTE" + # } + # ], + # "exchangeId": "301", + # "symbol": "BTCUSDT-PERPETUAL", + # "symbolName": "BTCUSDT-PERPETUAL", + # "status": "TRADING", + # "baseAsset": "BTCUSDT-PERPETUAL", + # "baseAssetPrecision": "0.001", + # "quoteAsset": "USDT", + # "quoteAssetPrecision": "0.1", + # "icebergAllowed": False, + # "inverse": False, + # "index": "USDT", + # "marginToken": "USDT", + # "marginPrecision": "0.0001", + # "contractMultiplier": "0.001", + # "underlying": "BTC", + # "riskLimits": [ + # { + # "riskLimitId": "200000722", + # "quantity": "1000.00", + # "initialMargin": "0.10", + # "maintMargin": "0.005", + # "isWhite": False + # }, + # { + # "riskLimitId": "200000723", + # "quantity": "2000.00", + # "initialMargin": "0.10", + # "maintMargin": "0.01", + # "isWhite": False + # } + # ] + # } + # + marketId = self.safe_string(market, 'symbol') + quoteId = self.safe_string(market, 'quoteAsset') + quote = self.safe_currency_code(quoteId) + settleId = self.safe_string(market, 'marginToken') + settle = self.safe_currency_code(settleId) + baseId = self.safe_string(market, 'baseAsset') + marketType = 'spot' + isSpot = True + isSwap = False + suffix = '' + parts = marketId.split('-') + secondPart = self.safe_string(parts, 1) + if secondPart == 'PERPETUAL': + marketType = 'swap' + isSpot = False + isSwap = True + baseId = self.safe_string(market, 'underlying') + suffix += ':' + settleId + base = self.safe_currency_code(baseId) + symbol = base + '/' + quote + suffix + status = self.safe_string(market, 'status') + active = status == 'TRADING' + isLinear: Bool = None + subType = None + isInverse = self.safe_bool(market, 'inverse') + if isInverse is not None: + if isInverse: + isLinear = False + subType = 'inverse' + else: + isLinear = True + subType = 'linear' + filtersList = self.safe_list(market, 'filters', []) + filters = self.index_by(filtersList, 'filterType') + priceFilter = self.safe_dict(filters, 'PRICE_FILTER', {}) + amountFilter = self.safe_dict(filters, 'LOT_SIZE', {}) + costFilter = self.safe_dict(filters, 'MIN_NOTIONAL', {}) + minCostString = self.omit_zero(self.safe_string(costFilter, 'min_notional')) + contractSizeString = self.safe_string(market, 'contractMultiplier') + amountPrecisionString = self.safe_string(amountFilter, 'stepSize') + amountMinLimitString = self.safe_string(amountFilter, 'minQty') + amountMaxLimitString = self.safe_string(amountFilter, 'maxQty') + minLeverage: Int = None + maxLeverage: Int = None + if isSwap: + amountPrecisionString = Precise.string_div(amountPrecisionString, contractSizeString) + amountMinLimitString = Precise.string_div(amountMinLimitString, contractSizeString) + amountMaxLimitString = Precise.string_div(amountMaxLimitString, contractSizeString) + riskLimits = self.safe_list(market, 'riskLimits') + if riskLimits is not None: + first = self.safe_dict(riskLimits, 0) + arrayLength = len(riskLimits) + last = self.safe_dict(riskLimits, arrayLength - 1) + minInitialMargin = self.safe_string(first, 'initialMargin') + maxInitialMargin = self.safe_string(last, 'initialMargin') + if Precise.string_gt(minInitialMargin, maxInitialMargin): + minInitialMargin, maxInitialMargin = [maxInitialMargin, minInitialMargin] + minLeverage = self.parse_to_int(Precise.string_div('1', maxInitialMargin)) + maxLeverage = self.parse_to_int(Precise.string_div('1', minInitialMargin)) + tradingFees = self.safe_dict(self.fees, 'trading') + fees = self.safe_dict(tradingFees, 'spot') if isSpot else self.safe_dict(tradingFees, 'swap') + return self.safe_market_structure({ + 'id': marketId, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'baseId': baseId, + 'quoteId': quoteId, + 'active': active, + 'type': marketType, + 'subType': subType, + 'spot': isSpot, + 'margin': self.safe_bool(market, 'allowMargin'), + 'swap': isSwap, + 'future': False, + 'option': False, + 'contract': isSwap, + 'settle': settle, + 'settleId': settleId, + 'contractSize': self.parse_number(contractSizeString), + 'linear': isLinear, + 'inverse': isInverse, + 'taker': self.safe_number(fees, 'taker'), + 'maker': self.safe_number(fees, 'maker'), + 'percentage': self.safe_bool(fees, 'percentage'), + 'tierBased': self.safe_bool(fees, 'tierBased'), + 'feeSide': self.safe_string(fees, 'feeSide'), + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(amountPrecisionString), + 'price': self.safe_number(priceFilter, 'tickSize'), + }, + 'limits': { + 'amount': { + 'min': self.parse_number(amountMinLimitString), + 'max': self.parse_number(amountMaxLimitString), + }, + 'price': { + 'min': self.safe_number(priceFilter, 'minPrice'), + 'max': self.safe_number(priceFilter, 'maxPrice'), + }, + 'leverage': { + 'min': minLeverage, + 'max': maxLeverage, + }, + 'cost': { + 'min': self.parse_number(minCostString), + 'max': None, + }, + }, + 'created': None, + 'info': market, + }) + + async def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://hashkeyglobal-apidoc.readme.io/reference/exchangeinfo + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = await self.publicGetApiV1ExchangeInfo(params) + coins = self.safe_list(response, 'coins') + # + # { + # ... + # "coins": [ + # { + # "orgId": "9001", + # "coinId": "BTC", + # "coinName": "BTC", + # "coinFullName": "Bitcoin", + # "allowWithdraw": True, + # "allowDeposit": True, + # "tokenType": "CHAIN_TOKEN", + # "chainTypes": [ + # { + # "chainType": "Bitcoin", + # "withdrawFee": "0", + # "minWithdrawQuantity": "0.002", + # "maxWithdrawQuantity": "0", + # "minDepositQuantity": "0.0005", + # "allowDeposit": True, + # "allowWithdraw": True + # } + # ] + # } + # ] + # } + # + result: dict = {} + for i in range(0, len(coins)): + currecy = coins[i] + currencyId = self.safe_string(currecy, 'coinId') + code = self.safe_currency_code(currencyId) + networks = self.safe_list(currecy, 'chainTypes') + parsedNetworks: dict = {} + for j in range(0, len(networks)): + network = networks[j] + networkId = self.safe_string(network, 'chainType') + networkCode = self.network_code_to_id(networkId) + parsedNetworks[networkCode] = { + 'id': networkId, + 'network': networkCode, + 'limits': { + 'withdraw': { + 'min': self.safe_number(network, 'minWithdrawQuantity'), + 'max': self.parse_number(self.omit_zero(self.safe_string(network, 'maxWithdrawQuantity'))), + }, + 'deposit': { + 'min': self.safe_number(network, 'minDepositQuantity'), + 'max': None, + }, + }, + 'active': None, + 'deposit': self.safe_bool(network, 'allowDeposit'), + 'withdraw': self.safe_bool(network, 'allowWithdraw'), + 'fee': self.safe_number(network, 'withdrawFee'), + 'precision': None, + 'info': network, + } + rawType = self.safe_string(currecy, 'tokenType') + type = 'fiat' if (rawType == 'REAL_MONEY') else 'crypto' + result[code] = self.safe_currency_structure({ + 'id': currencyId, + 'code': code, + 'precision': None, + 'type': type, + 'name': self.safe_string(currecy, 'coinFullName'), + 'active': None, + 'deposit': self.safe_bool(currecy, 'allowDeposit'), + 'withdraw': self.safe_bool(currecy, 'allowWithdraw'), + 'fee': None, + 'limits': { + 'deposit': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + }, + 'networks': parsedNetworks, + 'info': currecy, + }) + return result + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://hashkeyglobal-apidoc.readme.io/reference/get-order-book + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return(maximum value is 200) + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['limit'] = limit + response = await self.publicGetQuoteV1Depth(self.extend(request, params)) + # + # { + # "t": 1721681436393, + # "b": [ + # ["67902.49", "0.00112"], + # ["67901.08", "0.01014"] + # ... + # ], + # "a": [ + # ["67905.99", "0.87134"], + # ["67906", "0.57361"] + # ... + # ] + # } + # + timestamp = self.safe_integer(response, 't') + return self.parse_order_book(response, symbol, timestamp, 'b', 'a') + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://hashkeyglobal-apidoc.readme.io/reference/get-recent-trade-list + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch(maximum value is 100) + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['limit'] = limit + response = await self.publicGetQuoteV1Trades(self.extend(request, params)) + # + # [ + # { + # "t": 1721682745779, + # "p": "67835.99", + # "q": "0.00017", + # "ibm": True + # }, + # ... + # ] + # + return self.parse_trades(response, market, since, limit) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://hashkeyglobal-apidoc.readme.io/reference/get-account-trade-list + https://hashkeyglobal-apidoc.readme.io/reference/query-futures-trades + https://hashkeyglobal-apidoc.readme.io/reference/get-sub-account-user + + :param str symbol: *is mandatory for swap markets* unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum amount of trades to fetch(default 200, max 500) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: 'spot' or 'swap' - the type of the market to fetch trades for(default 'spot') + :param int [params.until]: the latest time in ms to fetch trades for, only supports the last 30 days timeframe + :param str [params.fromId]: srarting trade id + :param str [params.toId]: ending trade id + :param str [params.clientOrderId]: *spot markets only* filter trades by orderId + :param str [params.accountId]: account id to fetch the orders from + :returns Trade[]: a list of `trade structures ` + """ + methodName = 'fetchMyTrades' + await self.load_markets() + request: dict = {} + market: Market = None + if symbol is not None: + market = self.market(symbol) + marketType = 'spot' + marketType, params = self.handle_market_type_and_params(methodName, market, params) + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + until: Int = None + until, params = self.handle_option_and_params(params, methodName, 'until') + if until is not None: + request['endTime'] = until + accountId: Str = None + accountId, params = self.handle_option_and_params(params, methodName, 'accountId') + response = None + if marketType == 'spot': + if market is not None: + request['symbol'] = market['id'] + if accountId is not None: + request['accountId'] = accountId + response = await self.privateGetApiV1AccountTrades(self.extend(request, params)) + # + # [ + # { + # "id": "1739352552862964736", + # "clientOrderId": "1722082982086472", + # "ticketId": "1739352552795029504", + # "symbol": "ETHUSDT", + # "symbolName": "ETHUSDT", + # "orderId": "1739352552762301440", + # "matchOrderId": "0", + # "price": "3289.96", + # "qty": "0.001", + # "commission": "0.0000012", + # "commissionAsset": "ETH", + # "time": "1722082982097", + # "isBuyer": True, + # "isMaker": False, + # "fee": { + # "feeCoinId": "ETH", + # "feeCoinName": "ETH", + # "fee": "0.0000012" + # }, + # "feeCoinId": "ETH", + # "feeAmount": "0.0000012", + # "makerRebate": "0" + # }, + # ... + # ] + # + elif marketType == 'swap': + if symbol is None: + raise ArgumentsRequired(self.id + ' ' + methodName + '() requires a symbol argument for swap markets') + request['symbol'] = market['id'] + if accountId is not None: + request['subAccountId'] = accountId + response = await self.privateGetApiV1FuturesSubAccountUserTrades(self.extend(request, params)) + else: + response = await self.privateGetApiV1FuturesUserTrades(self.extend(request, params)) + # + # [ + # { + # "time": "1722429951648", + # "tradeId": "1742263144691139328", + # "orderId": "1742263144028363776", + # "symbol": "ETHUSDT-PERPETUAL", + # "price": "3327.54", + # "quantity": "4", + # "commissionAsset": "USDT", + # "commission": "0.00798609", + # "makerRebate": "0", + # "type": "LIMIT", + # "side": "BUY_OPEN", + # "realizedPnl": "0", + # "isMarker": False + # } + # ] + # + else: + raise NotSupported(self.id + ' ' + methodName + '() is not supported for ' + marketType + ' type of markets') + return self.parse_trades(response, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades + # + # { + # "t": 1721682745779, + # "p": "67835.99", + # "q": "0.00017", + # "ibm": True + # } + # + # fetchMyTrades spot + # + # { + # "id": "1739352552862964736", + # "clientOrderId": "1722082982086472", + # "ticketId": "1739352552795029504", + # "symbol": "ETHUSDT", + # "symbolName": "ETHUSDT", + # "orderId": "1739352552762301440", + # "matchOrderId": "0", + # "price": "3289.96", + # "qty": "0.001", + # "commission": "0.0000012", + # "commissionAsset": "ETH", + # "time": "1722082982097", + # "isBuyer": True, + # "isMaker": False, + # "fee": { + # "feeCoinId": "ETH", + # "feeCoinName": "ETH", + # "fee": "0.0000012" + # }, + # "feeCoinId": "ETH", + # "feeAmount": "0.0000012", + # "makerRebate": "0" + # } + # + # fetchMyTrades swap + # { + # "time": "1722429951648", + # "tradeId": "1742263144691139328", + # "orderId": "1742263144028363776", + # "symbol": "ETHUSDT-PERPETUAL", + # "price": "3327.54", + # "quantity": "4", + # "commissionAsset": "USDT", + # "commission": "0.00798609", + # "makerRebate": "0", + # "type": "LIMIT", + # "side": "BUY_OPEN", + # "realizedPnl": "0", + # "isMarker": False + # } + timestamp = self.safe_integer_2(trade, 't', 'time') + marketId = self.safe_string(trade, 'symbol') + market = self.safe_market(marketId, market) + side = self.safe_string_lower(trade, 'side') # swap trades have side param + if side is not None: + side = self.safe_string(side.split('_'), 0) + isBuyer = self.safe_bool(trade, 'isBuyer') + if isBuyer is not None: + side = 'buy' if isBuyer else 'sell' + takerOrMaker = None + isMaker = self.safe_bool_n(trade, ['isMaker', 'isMarker']) + if isMaker is not None: + takerOrMaker = 'maker' if isMaker else 'taker' + isBuyerMaker = self.safe_bool(trade, 'ibm') + # if public trade + if isBuyerMaker is not None: + takerOrMaker = 'taker' + side = 'sell' if isBuyerMaker else 'buy' + feeCost = self.safe_string(trade, 'commission') + feeCurrncyId = self.safe_string(trade, 'commissionAsset') + feeInfo = self.safe_dict(trade, 'fee') + fee = None + if feeInfo is not None: + feeCost = self.safe_string(feeInfo, 'fee') + feeCurrncyId = self.safe_string(feeInfo, 'feeCoinId') + if feeCost is not None: + fee = { + 'cost': self.parse_number(feeCost), + 'currency': self.safe_currency_code(feeCurrncyId), + } + return self.safe_trade({ + 'id': self.safe_string_2(trade, 'id', 'tradeId'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'side': side, + 'price': self.safe_string_2(trade, 'p', 'price'), + 'amount': self.safe_string_n(trade, ['q', 'qty', 'quantity']), + 'cost': None, + 'takerOrMaker': takerOrMaker, + 'type': None, + 'order': self.safe_string(trade, 'orderId'), + 'fee': fee, + 'info': trade, + }, market) + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + + https://hashkeyglobal-apidoc.readme.io/reference/get-kline + + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest candle to fetch + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + methodName = 'fetchOHLCV' + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, methodName, 'paginate') + if paginate: + return await self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params, 1000) + market = self.market(symbol) + timeframe = self.safe_string(self.timeframes, timeframe, timeframe) + request: dict = { + 'symbol': market['id'], + 'interval': timeframe, + } + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + until: Int = None + until, params = self.handle_option_and_params(params, methodName, 'until') + if until is not None: + request['endTime'] = until + response = await self.publicGetQuoteV1Klines(self.extend(request, params)) + # + # [ + # [ + # 1721684280000, + # "67832.49", + # "67862.5", + # "67832.49", + # "67861.44", + # "0.01122",0, + # "761.2763533",68, + # "0.00561", + # "380.640643" + # ], + # ... + # ] + # + return self.parse_ohlcvs(response, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # [ + # 1721684280000, + # "67832.49", + # "67862.5", + # "67832.49", + # "67861.44", + # "0.01122",0, + # "761.2763533",68, + # "0.00561", + # "380.640643" + # ] + # + return [ + self.safe_integer(ohlcv, 0), + self.safe_number(ohlcv, 1), + self.safe_number(ohlcv, 2), + self.safe_number(ohlcv, 3), + self.safe_number(ohlcv, 4), + self.safe_number(ohlcv, 5), + ] + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://hashkeyglobal-apidoc.readme.io/reference/get-24hr-ticker-price-change + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = await self.publicGetQuoteV1Ticker24hr(self.extend(request, params)) + # + # [ + # { + # "t": 1721685896846, + # "s": "BTCUSDT-PERPETUAL", + # "c": "67756.7", + # "h": "68479.9", + # "l": "66594.3", + # "o": "68279.7", + # "b": "67756.6", + # "a": "67756.7", + # "v": "1604722", + # "qv": "108827258.7761" + # } + # ] + # + ticker = self.safe_dict(response, 0, {}) + return self.parse_ticker(ticker, market) + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://hashkeyglobal-apidoc.readme.io/reference/get-24hr-ticker-price-change + + :param str[] [symbols]: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + response = await self.publicGetQuoteV1Ticker24hr(params) + return self.parse_tickers(response, symbols) + + def parse_ticker(self, ticker, market: Market = None) -> Ticker: + # + # { + # "t": 1721685896846, + # "s": "BTCUSDT-PERPETUAL", + # "c": "67756.7", + # "h": "68479.9", + # "l": "66594.3", + # "o": "68279.7", + # "b": "67756.6", + # "a": "67756.7", + # "v": "1604722", + # "qv": "108827258.7761" + # } + # + timestamp = self.safe_integer(ticker, 't') + marketId = self.safe_string(ticker, 's') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + last = self.safe_string(ticker, 'c') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'h'), + 'low': self.safe_string(ticker, 'l'), + 'bid': self.safe_string(ticker, 'b'), + 'bidVolume': None, + 'ask': self.safe_string(ticker, 'a'), + 'askVolume': None, + 'vwap': None, + 'open': self.safe_string(ticker, 'o'), + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': self.safe_string(ticker, 'v'), + 'quoteVolume': self.safe_string(ticker, 'qv'), + 'info': ticker, + }, market) + + async def fetch_last_prices(self, symbols: Strings = None, params={}) -> LastPrices: + """ + fetches the last price for multiple markets + + https://hashkeyglobal-apidoc.readme.io/reference/get-symbol-price-ticker + + :param str[] [symbols]: unified symbols of the markets to fetch the last prices + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.symbol]: the id of the market to fetch last price for + :returns dict: a dictionary of lastprices structures + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + request: dict = {} + response = await self.publicGetQuoteV1TickerPrice(self.extend(request, params)) + # + # [ + # { + # "s": "BTCUSDT-PERPETUAL", + # "p": "64871" + # }, + # ... + # ] + # + return self.parse_last_prices(response, symbols) + + def parse_last_price(self, entry, market: Market = None) -> LastPrice: + marketId = self.safe_string(entry, 's') + market = self.safe_market(marketId, market) + return { + 'symbol': market['symbol'], + 'timestamp': None, + 'datetime': None, + 'price': self.safe_number(entry, 'p'), + 'side': None, + 'info': entry, + } + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://hashkeyglobal-apidoc.readme.io/reference/get-account-information + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.accountId]: account ID, for Master Key only + :param str [params.type]: 'spot' or 'swap' - the type of the market to fetch balance for(default 'spot') + :returns dict: a `balance structure ` + """ + await self.load_markets() + request: dict = {} + methodName = 'fetchBalance' + marketType = 'spot' + marketType, params = self.handle_market_type_and_params(methodName, None, params, marketType) + if marketType == 'swap': + response = await self.privateGetApiV1FuturesBalance(params) + # + # [ + # { + # "balance": "30.63364672", + # "availableBalance": "28.85635534", + # "positionMargin": "4.3421", + # "orderMargin": "0", + # "asset": "USDT", + # "crossUnRealizedPnl": "2.5649" + # } + # ] + # + balance = self.safe_dict(response, 0, {}) + return self.parse_swap_balance(balance) + elif marketType == 'spot': + response = await self.privateGetApiV1Account(self.extend(request, params)) + # + # { + # "balances": [ + # { + # "asset":"USDT", + # "assetId":"USDT", + # "assetName":"USDT", + # "total":"40", + # "free":"40", + # "locked":"0" + # }, + # ... + # ], + # "userId": "1732885739572845312" + # } + # + return self.parse_balance(response) + else: + raise NotSupported(self.id + ' ' + methodName + '() is not supported for ' + marketType + ' type of markets') + + def parse_balance(self, balance) -> Balances: + # + # { + # "balances": [ + # { + # "asset":"USDT", + # "assetId":"USDT", + # "assetName":"USDT", + # "total":"40", + # "free":"40", + # "locked":"0" + # }, + # ... + # ], + # "userId": "1732885739572845312" + # } + # + result: dict = { + 'info': balance, + } + balances = self.safe_list(balance, 'balances', []) + for i in range(0, len(balances)): + balanceEntry = balances[i] + currencyId = self.safe_string(balanceEntry, 'asset') + code = self.safe_currency_code(currencyId) + account = self.account() + account['total'] = self.safe_string(balanceEntry, 'total') + account['free'] = self.safe_string(balanceEntry, 'free') + account['used'] = self.safe_string(balanceEntry, 'locked') + result[code] = account + return self.safe_balance(result) + + def parse_swap_balance(self, balance) -> Balances: + # + # { + # "balance": "30.63364672", + # "availableBalance": "28.85635534", + # "positionMargin": "4.3421", + # "orderMargin": "0", + # "asset": "USDT", + # "crossUnRealizedPnl": "2.5649" + # } + # + currencyId = self.safe_string(balance, 'asset') + code = self.safe_currency_code(currencyId) + account = self.account() + account['total'] = self.safe_string(balance, 'balance') + positionMargin = self.safe_string(balance, 'positionMargin') + orderMargin = self.safe_string(balance, 'orderMargin') + account['used'] = Precise.string_add(positionMargin, orderMargin) + result: dict = { + 'info': balance, + } + result[code] = account + return self.safe_balance(result) + + async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://hashkeyglobal-apidoc.readme.io/reference/get-deposit-address + + :param str code: unified currency code(default is 'USDT') + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.network]: network for fetch deposit address(default is 'ETH') + :returns dict: an `address structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'coin': currency['id'], + } + networkCode: Str = None + networkCode, params = self.handle_network_code_and_params(params) + if networkCode is None: + networkCode = self.default_network_code(code) + request['chainType'] = self.network_code_to_id(networkCode, code) + response = await self.privateGetApiV1AccountDepositAddress(self.extend(request, params)) + # + # { + # "canDeposit": True, + # "address": "0x61AAd7F763e2C7fF1CC996918740F67f9dC8BF4e", + # "addressExt": "", + # "minQuantity": "1", + # "needAddressTag": False, + # "requiredConfirmTimes": 64, + # "canWithdrawConfirmTimes": 64, + # "coinType": "ERC20_TOKEN" + # } + # + depositAddress = self.parse_deposit_address(response, currency) + depositAddress['network'] = networkCode + return depositAddress + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + # + # { + # "canDeposit": True, + # "address": "0x61AAd7F763e2C7fF1CC996918740F67f9dC8BF4e", + # "addressExt": "", + # "minQuantity": "1", + # "needAddressTag": False, + # "requiredConfirmTimes": 64, + # "canWithdrawConfirmTimes": 64, + # "coinType": "ERC20_TOKEN" + # } + # + address = self.safe_string(depositAddress, 'address') + self.check_address(address) + tag = self.safe_string(depositAddress, 'addressExt') + if tag == '': + tag = None + return { + 'info': depositAddress, + 'currency': currency['code'], + 'network': None, + 'address': address, + 'tag': tag, + } + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://hashkeyglobal-apidoc.readme.io/reference/get-deposit-history + + :param str code: unified currency code of the currency transferred + :param int [since]: the earliest time in ms to fetch transfers for(default 24 hours ago) + :param int [limit]: the maximum number of transfer structures to retrieve(default 50, max 200) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch transfers for(default time now) + :param int [params.fromId]: starting ID(To be released) + :returns dict[]: a list of `transfer structures ` + """ + methodName = 'fetchDeposits' + await self.load_markets() + request: dict = {} + currency: Currency = None + if code is not None: + currency = self.currency(code) + request['coin'] = currency['id'] + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + until: Int = None + until, params = self.handle_option_and_params(params, methodName, 'until') + if until is not None: + request['endTime'] = until + response = await self.privateGetApiV1AccountDepositOrders(self.extend(request, params)) + # + # [ + # { + # "time": "1721641082163", + # "coin": "TRXUSDT", + # "coinName": "TRXUSDT", + # "address": "TBA6CypYJizwA9XdC7Ubgc5F1bxrQ7SqPt", + # "quantity": "86.00000000000000000000", + # "status": 4, + # "statusCode": "4", + # "txId": "0970c14da4d7412295fa7b21c03a08da319e746a0d59ef14462a74183d118da4" + # } + # ] + # + return self.parse_transactions(response, currency, since, limit, {'type': 'deposit'}) + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://hashkeyglobal-apidoc.readme.io/reference/withdrawal-records + + :param str code: unified currency code of the currency transferred + :param int [since]: the earliest time in ms to fetch transfers for(default 24 hours ago) + :param int [limit]: the maximum number of transfer structures to retrieve(default 50, max 200) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch transfers for(default time now) + :returns dict[]: a list of `transaction structures ` + """ + methodName = 'fetchWithdrawals' + await self.load_markets() + request: dict = {} + currency: Currency = None + if code is not None: + currency = self.currency(code) + request['coin'] = currency['id'] + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + until: Int = None + until, params = self.handle_option_and_params(params, methodName, 'until') + if until is not None: + request['endTime'] = until + response = await self.privateGetApiV1AccountWithdrawOrders(self.extend(request, params)) + # + # [ + # { + # "time": "1723545505366", + # "id": "W611267400947572736", + # "coin": "USDT", + # "coinId": "USDT", + # "coinName": "USDT", + # "address": "TQbkBMnWnJNGTAUpFS4kvv4NRLzUAnGAes", + # "quantity": "2.00000000", + # "arriveQuantity": "2.00000000", + # "txId": "f83f94e7d2e81fbec98c66c25d6615872cc2d426145629b6cf22e5e0a0753715", + # "addressUrl": "TQbkBMnWnJNGTAUpFS4kvv4NRLzUAnGAes", + # "feeCoinId": "USDT", + # "feeCoinName": "USDT", + # "fee": "1.00000000", + # "remark": "", + # "platform": "" + # } + # ] + # + return self.parse_transactions(response, currency, since, limit, {'type': 'withdrawal'}) + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://hashkeyglobal-apidoc.readme.io/reference/withdraw + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.network]: network for withdraw + :param str [params.clientOrderId]: client order id + :param str [params.platform]: the platform to withdraw to(hashkey, HashKey HK) + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'coin': currency['id'], + 'address': address, + 'quantity': amount, + } + if tag is not None: + request['addressExt'] = tag + networkCode: Str = None + networkCode, params = self.handle_network_code_and_params(params) + if networkCode is not None: + request['chainType'] = self.network_code_to_id(networkCode) + response = await self.privatePostApiV1AccountWithdraw(self.extend(request, params)) + # + # { + # "success": True, + # "id": "0", + # "orderId": "W611267400947572736", + # "accountId": "1732885739589466115" + # } + # + return self.parse_transaction(response, currency) + + def parse_transaction(self, transaction, currency: Currency = None) -> Transaction: + # + # fetchDeposits + # { + # "time": "1721641082163", + # "coin": "TRXUSDT", # todo how to parse it? + # "coinName": "TRXUSDT", + # "address": "TBA6CypYJizwA9XdC7Ubgc5F1bxrQ7SqPt", + # "quantity": "86.00000000000000000000", + # "status": 4, + # "statusCode": "4", + # "txId": "0970c14da4d7412295fa7b21c03a08da319e746a0d59ef14462a74183d118da4" + # } + # + # fetchWithdrawals + # { + # "time": "1723545505366", + # "id": "W611267400947572736", + # "coin": "USDT", + # "coinId": "USDT", + # "coinName": "USDT", + # "address": "TQbkBMnWnJNGTAUpFS4kvv4NRLzUAnGAes", + # "quantity": "2.00000000", + # "arriveQuantity": "2.00000000", + # "txId": "f83f94e7d2e81fbec98c66c25d6615872cc2d426145629b6cf22e5e0a0753715", + # "addressUrl": "TQbkBMnWnJNGTAUpFS4kvv4NRLzUAnGAes", + # "feeCoinId": "USDT", + # "feeCoinName": "USDT", + # "fee": "1.00000000", + # "remark": "", + # "platform": "" + # } + # + # withdraw + # { + # "success": True, + # "id": "0", + # "orderId": "W611267400947572736", + # "accountId": "1732885739589466115" + # } + # + id = self.safe_string_2(transaction, 'id', 'orderId') + address = self.safe_string(transaction, 'address') + status = self.safe_string(transaction, 'status') # for fetchDeposits + if status is None: + success = self.safe_bool(transaction, 'success', False) # for withdraw + if success: + status = 'ok' + else: + addressUrl = self.safe_string(transaction, 'addressUrl') # for fetchWithdrawals + if addressUrl is not None: + status = 'ok' + txid = self.safe_string(transaction, 'txId') + coin = self.safe_string(transaction, 'coin') + code = self.safe_currency_code(coin, currency) + timestamp = self.safe_integer(transaction, 'time') + amount = self.safe_number(transaction, 'quantity') + feeCost = self.safe_number(transaction, 'fee') + fee = None + if feeCost is not None: + fee = { + 'cost': feeCost, + 'currency': code, + } + return { + 'info': transaction, + 'id': id, + 'txid': txid, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': None, + 'address': address, + 'addressTo': None, + 'addressFrom': None, + 'tag': None, + 'tagTo': None, + 'tagFrom': None, + 'type': None, + 'amount': amount, + 'currency': code, + 'status': self.parse_transaction_status(status), + 'updated': None, + 'internal': None, + 'comment': None, + 'fee': fee, + } + + def parse_transaction_status(self, status): + statuses: dict = { + '1': 'pending', + '2': 'pending', + '3': 'failed', + '4': 'ok', + '5': 'pending', + '6': 'ok', + '7': 'failed', + '8': 'cancelled', + '9': 'failed', + '10': 'failed', + 'successful': 'ok', + 'success': 'ok', + } + return self.safe_string(statuses, status, status) + + async def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://hashkeyglobal-apidoc.readme.io/reference/new-account-transfer + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account id to transfer from + :param str toAccount: account id to transfer to + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.clientOrderId]: a unique id for the transfer + :param str [params.remark]: a note for the transfer + :returns dict: a `transfer structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'coin': currency['id'], + 'quantity': self.currency_to_precision(code, amount), + 'fromAccountId': fromAccount, + 'toAccountId': toAccount, + } + response = await self.privatePostApiV1AccountAssetTransfer(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1722260230773, + # "clientOrderId": "", + # "orderId": "1740839420695806720" + # } + # + return self.parse_transfer(response, currency) + + def parse_transfer(self, transfer, currency: Currency = None): + timestamp = self.safe_integer(transfer, 'timestamp') + currencyId = self.safe_string(currency, 'id') + status: Str = None + success = self.safe_bool(transfer, 'success', False) + if success: + status = 'ok' + return { + 'id': self.safe_string(transfer, 'orderId'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'currency': self.safe_currency_code(currencyId, currency), + 'amount': None, + 'fromAccount': None, + 'toAccount': None, + 'status': status, + 'info': transfer, + } + + async def fetch_accounts(self, params={}) -> List[Account]: + """ + fetch all the accounts associated with a profile + + https://hashkeyglobal-apidoc.readme.io/reference/query-sub-account + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `account structures ` indexed by the account type + """ + await self.load_markets() + response = await self.privateGetApiV1AccountType(params) + # + # [ + # { + # "accountId": "1732885739589466112", + # "accountLabel": "Main Trading Account", + # "accountType": 1, + # "accountIndex": 0 + # }, + # ... + # ] + # + return self.parse_accounts(response, params) + + def parse_account(self, account): + accountLabel = self.safe_string(account, 'accountLabel') + label = '' + if accountLabel == 'Main Trading Account' or accountLabel == 'Main Future Account': + label = 'main' + elif accountLabel == 'Sub Main Trading Account' or accountLabel == 'Sub Main Future Account': + label = 'sub' + accountType = self.parse_account_type(self.safe_string(account, 'accountType')) + type = label + ' ' + accountType + return { + 'id': self.safe_string(account, 'accountId'), + 'type': type, + 'code': None, + 'info': account, + } + + def parse_account_type(self, type): + types: dict = { + '1': 'spot account', + '3': 'swap account', + '5': 'custody account', + '6': 'fiat account', + } + return self.safe_string(types, type, type) + + def encode_account_type(self, type): + types = { + 'spot': '1', + 'swap': '3', + 'custody': '5', + } + return self.safe_integer(types, type, type) + + def encode_flow_type(self, type): + types = { + 'trade': '1', + 'fee': '3', + 'transfer': '51', + 'deposit': '900', + 'withdraw': '904', + } + return self.safe_integer(types, type, type) + + async def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + + https://hashkeyglobal-apidoc.readme.io/reference/get-account-transaction-list + + :param str [code]: unified currency code, default is None(not used) + :param int [since]: timestamp in ms of the earliest ledger entry, default is None + :param int [limit]: max number of ledger entries to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch entries for + :param int [params.flowType]: trade, fee, transfer, deposit, withdrawal + :param int [params.accountType]: spot, swap, custody + :returns dict: a `ledger structure ` + """ + methodName = 'fetchLedger' + if since is None: + raise ArgumentsRequired(self.id + ' ' + methodName + '() requires a since argument') + until: Int = None + until, params = self.handle_option_and_params(params, methodName, 'until') + if until is None: + raise ArgumentsRequired(self.id + ' ' + methodName + '() requires an until argument') + await self.load_markets() + currency = self.currency(code) + request = {} + request['startTime'] = since + if limit is not None: + request['limit'] = limit + request['endTime'] = until + flowType = None + flowType, params = self.handle_option_and_params(params, methodName, 'flowType') + if flowType is not None: + request['flowType'] = self.encode_flow_type(flowType) + accountType = None + accountType, params = self.handle_option_and_params(params, methodName, 'accountType') + if accountType is not None: + request['accountType'] = self.encode_account_type(accountType) + response = await self.privateGetApiV1AccountBalanceFlow(self.extend(request, params)) + # + # [ + # { + # "id": "1740844413612065537", + # "accountId": "1732885739589466112", + # "coin": "USDT", + # "coinId": "USDT", + # "coinName": "USDT", + # "flowTypeValue": 51, + # "flowType": "USER_ACCOUNT_TRANSFER", + # "flowName": "", + # "change": "-1", + # "total": "8.015680088", + # "created": "1722260825765" + # }, + # ... + # ] + # + return self.parse_ledger(response, currency, since, limit) + + def parse_ledger_entry_type(self, type): + types: dict = { + '1': 'trade', # transfer + '2': 'fee', # trade + '51': 'transfer', + '900': 'deposit', + '904': 'withdraw', + } + return self.safe_string(types, type, type) + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + # + # { + # "id": "1740844413612065537", + # "accountId": "1732885739589466112", + # "coin": "USDT", + # "coinId": "USDT", + # "coinName": "USDT", + # "flowTypeValue": 51, + # "flowType": "USER_ACCOUNT_TRANSFER", + # "flowName": "", + # "change": "-1", + # "total": "8.015680088", + # "created": "1722260825765" + # } + # + id = self.safe_string(item, 'id') + account = self.safe_string(item, 'accountId') + timestamp = self.safe_integer(item, 'created') + type = self.parse_ledger_entry_type(self.safe_string(item, 'flowTypeValue')) + currencyId = self.safe_string(item, 'coin') + code = self.safe_currency_code(currencyId, currency) + currency = self.safe_currency(currencyId, currency) + amountString = self.safe_string(item, 'change') + amount = self.parse_number(amountString) + direction = 'in' + if amountString.find('-') >= 0: + direction = 'out' + afterString = self.safe_string(item, 'total') + after = self.parse_number(afterString) + status = 'ok' + return self.safe_ledger_entry({ + 'info': item, + 'id': id, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'account': account, + 'direction': direction, + 'referenceId': None, + 'referenceAccount': None, + 'type': type, + 'currency': code, + 'symbol': None, + 'amount': amount, + 'before': None, + 'after': after, + 'status': status, + 'fee': None, + }, currency) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}) -> Order: + """ + create a trade order + + https://hashkeyglobal-apidoc.readme.io/reference/test-new-order + https://hashkeyglobal-apidoc.readme.io/reference/create-order + https://hashkeyglobal-apidoc.readme.io/reference/create-new-futures-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' or 'LIMIT_MAKER' for spot, 'market' or 'limit' or 'STOP' for swap + :param str side: 'buy' or 'sell' + :param float amount: how much of you want to trade in units of the base currency + :param float [price]: the price that the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.cost]: *spot market buy only* the quote quantity that can be used alternative for the amount + :param boolean [params.test]: *spot markets only* whether to use the test endpoint or not, default is False + :param bool [params.postOnly]: if True, the order will only be posted to the order book and not executed immediately + :param str [params.timeInForce]: "GTC" or "IOC" or "PO" for spot, 'GTC' or 'FOK' or 'IOC' or 'LIMIT_MAKER' or 'PO' for swap + :param str [params.clientOrderId]: a unique id for the order - is mandatory for swap + :param float [params.triggerPrice]: *swap markets only* The price at which a trigger order is triggered at + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + if market['spot']: + return await self.create_spot_order(symbol, type, side, amount, price, params) + elif market['swap']: + return await self.create_swap_order(symbol, type, side, amount, price, params) + else: + raise NotSupported(self.id + ' createOrder() is not supported for ' + market['type'] + ' type of markets') + + async def create_market_buy_order_with_cost(self, symbol: str, cost: float, params={}) -> Order: + """ + create a market buy order by providing the symbol and cost + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' createMarketBuyOrderWithCost() is supported for spot markets only') + req = { + 'cost': cost, + } + return await self.create_order(symbol, 'market', 'buy', cost, None, self.extend(req, params)) + + async def create_spot_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}) -> Order: + """ + create a trade order on spot market + + https://hashkeyglobal-apidoc.readme.io/reference/test-new-order + https://hashkeyglobal-apidoc.readme.io/reference/create-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' or 'LIMIT_MAKER' + :param str side: 'buy' or 'sell' + :param float amount: how much of you want to trade in units of the base currency + :param float [price]: the price that the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.cost]: *market buy only* the quote quantity that can be used alternative for the amount + :param bool [params.test]: whether to use the test endpoint or not, default is False + :param bool [params.postOnly]: if True, the order will only be posted to the order book and not executed immediately + :param str [params.timeInForce]: 'GTC', 'IOC', or 'PO' + :param str [params.clientOrderId]: a unique id for the order + :returns dict: an `order structure ` + """ + triggerPrice = self.safe_string_2(params, 'stopPrice', 'triggerPrice') + if triggerPrice is not None: + raise NotSupported(self.id + ' trigger orders are not supported for spot markets') + await self.load_markets() + market = self.market(symbol) + isMarketBuy = (type == 'market') and (side == 'buy') + cost = self.safe_string(params, 'cost') + if (not isMarketBuy) and (cost is not None): + raise NotSupported(self.id + ' createOrder() supports cost parameter for spot market buy orders only') + request: dict = self.create_spot_order_request(symbol, type, side, amount, price, params) + response: dict = {} + test = self.safe_bool(params, 'test') + if test: + params = self.omit(params, 'test') + response = await self.privatePostApiV1SpotOrderTest(request) + elif isMarketBuy and (cost is None): + response = await self.privatePostApiV11SpotOrder(request) # the endpoint for market buy orders by amount + # + # { + # "accountId": "1732885739589466112", + # "symbol": "ETHUSDT", + # "symbolName": "ETHUSDT", + # "clientOrderId": "1722005792096557", + # "orderId": "1738705036219839744", + # "transactTime": "1722005792106", + # "price": "0", + # "origQty": "0.006", + # "executedQty": "0.0059", + # "status": "FILLED", + # "timeInForce": "IOC", + # "type": "MARKET", + # "side": "BUY", + # "reqAmount": "0", + # "concentration": "" + # } + # + else: + response = await self.privatePostApiV1SpotOrder(request) # the endpoint for market buy orders by cost and other orders + # + # market buy + # { + # "accountId": "1732885739589466112", + # "symbol": "ETHUSDT", + # "symbolName": "ETHUSDT", + # "clientOrderId": "1722004623170558", + # "orderId": "1738695230608169984", + # "transactTime": "1722004623186", + # "price": "0", + # "origQty": "0", + # "executedQty": "0.0061", + # "status": "FILLED", + # "timeInForce": "IOC", + # "type": "MARKET", + # "side": "BUY", + # "reqAmount": "20", + # "concentration": "" + # } + # + # market sell + # { + # "accountId": "1732885739589466112", + # "symbol": "ETHUSDT", + # "symbolName": "ETHUSDT", + # "clientOrderId": "1722005654516362", + # "orderId": "1738703882140316928", + # "transactTime": "1722005654529", + # "price": "0", + # "origQty": "0.006", + # "executedQty": "0.006", + # "status": "FILLED", + # "timeInForce": "IOC", + # "type": "MARKET", + # "side": "SELL", + # "reqAmount": "0", + # "concentration": "" + # } + # + # limit + # { + # "accountId": "1732885739589466112", + # "symbol": "ETHUSDT", + # "symbolName": "ETHUSDT", + # "clientOrderId": "1722006209978370", + # "orderId": "1738708541676585728", + # "transactTime": "1722006209989", + # "price": "5000", + # "origQty": "0.005", + # "executedQty": "0", + # "status": "NEW", + # "timeInForce": "GTC", + # "type": "LIMIT_MAKER", + # "side": "SELL", + # "reqAmount": "0", + # "concentration": "" + # } + # + return self.parse_order(response, market) + + def create_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}) -> dict: + market = self.market(symbol) + if market['spot']: + return self.create_spot_order_request(symbol, type, side, amount, price, params) + elif market['swap']: + return self.create_swap_order_request(symbol, type, side, amount, price, params) + else: + raise NotSupported(self.id + ' ' + 'createOrderRequest() is not supported for ' + market['type'] + ' type of markets') + + def create_spot_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}) -> dict: + """ + @ignore + helper function to build request + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' or 'LIMIT_MAKER' + :param str side: 'buy' or 'sell' + :param float amount: how much of you want to trade in units of the base currency + :param float [price]: the price that the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.cost]: *market buy only* the quote quantity that can be used alternative for the amount + :param bool [params.postOnly]: if True, the order will only be posted to the order book and not executed immediately + :param str [params.timeInForce]: "GTC", "IOC", or "PO" + :param str [params.clientOrderId]: a unique id for the order + :returns dict: request to be sent to the exchange + """ + market = self.market(symbol) + type = type.upper() + request: dict = { + 'symbol': market['id'], + 'side': side.upper(), + 'type': type, + } + if amount is not None: + request['quantity'] = self.amount_to_precision(symbol, amount) + cost: Str = None + cost, params = self.handle_param_string(params, 'cost') + if cost is not None: + request['quantity'] = self.cost_to_precision(symbol, cost) + if price is not None: + request['price'] = self.price_to_precision(symbol, price) + isMarketOrder = type == 'MARKET' + postOnly = False + postOnly, params = self.handle_post_only(isMarketOrder, type == 'LIMIT_MAKER', params) + if postOnly and (type == 'LIMIT'): + request['type'] = 'LIMIT_MAKER' + clientOrderId: Str = None + clientOrderId, params = self.handle_param_string(params, 'clientOrderId') + if clientOrderId is not None: + params['newClientOrderId'] = clientOrderId + return self.extend(request, params) + + def create_swap_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}) -> dict: + """ + @ignore + helper function to build request + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' or 'STOP' + :param str side: 'buy' or 'sell' + :param float amount: how much of you want to trade in units of the base currency + :param float [price]: the price that the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.postOnly]: if True, the order will only be posted to the order book and not executed immediately + :param bool [params.reduceOnly]: True or False whether the order is reduce only + :param float [params.triggerPrice]: The price at which a trigger order is triggered at + :param str [params.timeInForce]: 'GTC', 'FOK', 'IOC', 'LIMIT_MAKER' or 'PO' + :param str [params.clientOrderId]: a unique id for the order + :returns dict: request to be sent to the exchange + """ + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'type': 'LIMIT', + 'quantity': self.amount_to_precision(symbol, amount), + } + isMarketOrder = type == 'market' + if isMarketOrder: + request['priceType'] = 'MARKET' + if price is not None: + request['price'] = self.price_to_precision(symbol, price) + request['priceType'] = 'INPUT' + reduceOnly = False + reduceOnly, params = self.handle_param_bool(params, 'reduceOnly', reduceOnly) + suffix = '_OPEN' + if reduceOnly: + suffix = '_CLOSE' + request['side'] = side.upper() + suffix + timeInForce: Str = None + timeInForce, params = self.handle_param_string(params, 'timeInForce') + postOnly = False + postOnly, params = self.handle_post_only(isMarketOrder, timeInForce == 'LIMIT_MAKER', params) + if postOnly: + timeInForce = 'LIMIT_MAKER' + if timeInForce is not None: + request['timeInForce'] = timeInForce + clientOrderId = self.safe_string(params, 'clientOrderId') + if clientOrderId is None: + request['clientOrderId'] = self.uuid() + triggerPrice = self.safe_string(params, 'triggerPrice') + if triggerPrice is not None: + request['stopPrice'] = self.price_to_precision(symbol, triggerPrice) + request['type'] = 'STOP' + params = self.omit(params, 'triggerPrice') + return self.extend(request, params) + + async def create_swap_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}) -> Order: + """ + create a trade order on swap market + + https://hashkeyglobal-apidoc.readme.io/reference/create-new-futures-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' or 'STOP' + :param str side: 'buy' or 'sell' + :param float amount: how much of you want to trade in units of the base currency + :param float [price]: the price that the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.postOnly]: if True, the order will only be posted to the order book and not executed immediately + :param bool [params.reduceOnly]: True or False whether the order is reduce only + :param float [params.triggerPrice]: The price at which a trigger order is triggered at + :param str [params.timeInForce]: 'GTC', 'FOK', 'IOC', 'LIMIT_MAKER' or 'PO' + :param str [params.clientOrderId]: a unique id for the order + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + request = self.create_swap_order_request(symbol, type, side, amount, price, params) + response = await self.privatePostApiV1FuturesOrder(self.extend(request, params)) + # + # { + # "time": "1722429951611", + # "updateTime": "1722429951648", + # "orderId": "1742263144028363776", + # "clientOrderId": "1722429950315", + # "symbol": "ETHUSDT-PERPETUAL", + # "price": "3460.62", + # "leverage": "5", + # "origQty": "10", + # "executedQty": "10", + # "avgPrice": "0", + # "marginLocked": "6.9212", + # "type": "LIMIT", + # "side": "BUY_OPEN", + # "timeInForce": "IOC", + # "status": "FILLED", + # "priceType": "MARKET", + # "contractMultiplier": "0.00100000" + # } + # + return self.parse_order(response, market) + + async def create_orders(self, orders: List[OrderRequest], params={}): + """ + create a list of trade orders(all orders should be of the same symbol) + + https://hashkeyglobal-apidoc.readme.io/reference/create-multiple-orders + https://hashkeyglobal-apidoc.readme.io/reference/batch-create-new-futures-order + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the api endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + ordersRequests = [] + for i in range(0, len(orders)): + rawOrder = orders[i] + symbol = self.safe_string(rawOrder, 'symbol') + type = self.safe_string(rawOrder, 'type') + side = self.safe_string(rawOrder, 'side') + amount = self.safe_number(rawOrder, 'amount') + price = self.safe_number(rawOrder, 'price') + orderParams = self.safe_dict(rawOrder, 'params', {}) + orderRequest = self.create_order_request(symbol, type, side, amount, price, orderParams) + clientOrderId = self.safe_string(orderRequest, 'clientOrderId') + if clientOrderId is None: + orderRequest['clientOrderId'] = self.uuid() # both spot and swap endpoints require clientOrderId + ordersRequests.append(orderRequest) + firstOrder = ordersRequests[0] + firstSymbol = self.safe_string(firstOrder, 'symbol') + market = self.market(firstSymbol) + request: dict = { + 'orders': ordersRequests, + } + response = None + if market['spot']: + response = await self.privatePostApiV1SpotBatchOrders(self.extend(request, params)) + # + # { + # "code": 0, + # "result": [ + # { + # "code": "0000", + # "order": { + # "accountId": "1732885739589466112", + # "symbol": "ETHUSDT", + # "symbolName": "ETHUSDT", + # "clientOrderId": "1722701490163000", + # "orderId": "1744540984757258752", + # "transactTime": "1722701491385", + # "price": "1500", + # "origQty": "0.001", + # "executedQty": "0", + # "status": "NEW", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "side": "BUY", + # "reqAmount": "0" + # } + # } + # ], + # "concentration": "" + # } + # + elif market['swap']: + response = await self.privatePostApiV1FuturesBatchOrders(self.extend(request, params)) + # + # { + # "code": "0000", + # "result": [ + # { + # "code": "0000", + # "order": { + # "time": "1722704251911", + # "updateTime": "1722704251918", + # "orderId": "1744564141727808768", + # "clientOrderId": "1722704250648000", + # "symbol": "ETHUSDT-PERPETUAL", + # "price": "1500", + # "leverage": "4", + # "origQty": "1", + # "executedQty": "0", + # "avgPrice": "0", + # "marginLocked": "0.375", + # "type": "LIMIT", + # "side": "BUY_OPEN", + # "timeInForce": "GTC", + # "status": "NEW", + # "priceType": "INPUT", + # "isLiquidationOrder": False, + # "indexPrice": "0", + # "liquidationType": "" + # } + # }, + # { + # "code": "0207", + # "msg": "Create limit order sell price too low" + # } + # ] + # } + # + else: + raise NotSupported(self.id + ' ' + 'createOrderRequest() is not supported for ' + market['type'] + ' type of markets') + result = self.safe_list(response, 'result', []) + responseOrders = [] + for i in range(0, len(result)): + responseEntry = self.safe_dict(result, i, {}) + responseOrder = self.safe_dict(responseEntry, 'order', {}) + responseOrders.append(responseOrder) + return self.parse_orders(responseOrders) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://hashkeyglobal-apidoc.readme.io/reference/cancel-order + https://hashkeyglobal-apidoc.readme.io/reference/cancel-futures-order + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: 'spot' or 'swap' - the type of the market to fetch entry for(default 'spot') + :param str [params.clientOrderId]: a unique id for the order that can be used alternative for the id + :param bool [params.trigger]: *swap markets only* True for canceling a trigger order(default False) + :param bool [params.stop]: *swap markets only* an alternative for trigger param + :returns dict: An `order structure ` + """ + methodName = 'cancelOrder' + self.check_type_param(methodName, params) + await self.load_markets() + request: dict = {} + clientOrderId = self.safe_string(params, 'clientOrderId') + if clientOrderId is None: + request['orderId'] = id + market: Market = None + if symbol is not None: + market = self.market(symbol) + marketType = 'spot' + marketType, params = self.handle_market_type_and_params(methodName, market, params, marketType) + response = None + if marketType == 'spot': + response = await self.privateDeleteApiV1SpotOrder(self.extend(request, params)) + # + # { + # "accountId": "1732885739589466112", + # "symbol": "ETHUSDT", + # "clientOrderId": "1722006209978370", + # "orderId": "1738708541676585728", + # "transactTime": "1722006209989", + # "price": "5000", + # "origQty": "0.005", + # "executedQty": "0", + # "status": "NEW", + # "timeInForce": "GTC", + # "type": "LIMIT_MAKER", + # "side": "SELL" + # } + # + elif marketType == 'swap': + isTrigger = False + isTrigger, params = self.handle_trigger_option_and_params(params, methodName, isTrigger) + if isTrigger: + request['type'] = 'STOP' + else: + request['type'] = 'LIMIT' + if market is not None: + request['symbol'] = market['id'] + response = await self.privateDeleteApiV1FuturesOrder(self.extend(request, params)) + # + # { + # "time": "1722432302919", + # "updateTime": "1722432302925", + # "orderId": "1742282868229463040", + # "clientOrderId": "1722432301670", + # "symbol": "ETHUSDT-PERPETUAL", + # "price": "4000", + # "leverage": "5", + # "origQty": "10", + # "executedQty": "0", + # "avgPrice": "0", + # "marginLocked": "0", + # "type": "LIMIT_MAKER", + # "side": "SELL_CLOSE", + # "timeInForce": "GTC", + # "status": "NEW", + # "priceType": "INPUT", + # "isLiquidationOrder": False, + # "indexPrice": "0", + # "liquidationType": "" + # } + # + else: + raise NotSupported(self.id + ' ' + methodName + '() is not supported for ' + marketType + ' type of markets') + return self.parse_order(response) + + async def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders + + https://hashkeyglobal-apidoc.readme.io/reference/cancel-all-open-orders + https://hashkeyglobal-apidoc.readme.io/reference/batch-cancel-futures-order + + :param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.side]: 'buy' or 'sell' + :returns dict: response from exchange + """ + # Does not cancel trigger orders. For canceling trigger order use cancelOrder() or cancelOrders() + methodName = 'cancelAllOrders' + if symbol is None: + raise ArgumentsRequired(self.id + ' ' + methodName + '() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + side = self.safe_string(params, 'side') + if side is not None: + request['side'] = side + response = None + if market['spot']: + response = await self.privateDeleteApiV1SpotOpenOrders(self.extend(request, params)) + # + # {"success": True} + # + elif market['swap']: + response = await self.privateDeleteApiV1FuturesBatchOrders(self.extend(request, params)) + # + # {"message": "success", "timestamp": "1723127222198", "code": "0000"} + # + else: + raise NotSupported(self.id + ' ' + methodName + '() is not supported for ' + market['type'] + ' type of markets') + order = self.safe_order(response) + order['info'] = response + return [order] + + async def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): + """ + cancel multiple orders + + https://hashkeyglobal-apidoc.readme.io/reference/cancel-multiple-orders + https://hashkeyglobal-apidoc.readme.io/reference/batch-cancel-futures-order-by-order-id + + :param str[] ids: order ids + :param str [symbol]: unified market symbol(not used by hashkey) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: 'spot' or 'swap' - the type of the market to fetch entry for(default 'spot') + :returns dict: an list of `order structures ` + """ + methodName = 'cancelOrders' + await self.load_markets() + request = {} + orderIds = ','.join(ids) + request['ids'] = orderIds + market: Market = None + if symbol is not None: + market = self.market(symbol) + marketType = 'spot' + marketType, params = self.handle_market_type_and_params(methodName, market, params, marketType) + response = None + if marketType == 'spot': + response = await self.privateDeleteApiV1SpotCancelOrderByIds(self.extend(request)) + # + # { + # "code": "0000", + # "result": [] + # } + # + elif marketType == 'swap': + response = self.privateDeleteApiV1FuturesCancelOrderByIds(self.extend(request)) + else: + raise NotSupported(self.id + ' ' + methodName + '() is not supported for ' + marketType + ' type of markets') + order = self.safe_order(response) + order['info'] = response + return [order] + + async def fetch_order(self, id: str, symbol: Str = None, params={}) -> Order: + """ + fetches information on an order made by the user + + https://hashkeyglobal-apidoc.readme.io/reference/query-order + https://hashkeyglobal-apidoc.readme.io/reference/get-futures-order + + :param str id: the order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: 'spot' or 'swap' - the type of the market to fetch entry for(default 'spot') + :param str [params.clientOrderId]: a unique id for the order that can be used alternative for the id + :param str [params.accountId]: *spot markets only* account id to fetch the order from + :param bool [params.trigger]: *swap markets only* True for fetching a trigger order(default False) + :param bool [params.stop]: *swap markets only* an alternative for trigger param + :returns dict: An `order structure ` + """ + methodName = 'fetchOrder' + self.check_type_param(methodName, params) + await self.load_markets() + request: dict = {} + clientOrderId: Str = None + clientOrderId, params = self.handle_param_string(params, 'clientOrderId') + if clientOrderId is None: + request['orderId'] = id + market: Market = None + if symbol is not None: + market = self.market(symbol) + marketType = 'spot' + marketType, params = self.handle_market_type_and_params(methodName, market, params, marketType) + response = None + if marketType == 'spot': + if clientOrderId is not None: + request['origClientOrderId'] = clientOrderId + response = await self.privateGetApiV1SpotOrder(self.extend(request, params)) + # + # { + # "accountId": "1732885739589466112", + # "exchangeId": "301", + # "symbol": "ETHUSDT", + # "symbolName": "ETHUSDT", + # "clientOrderId": "1722004623170558", + # "orderId": "1738695230608169984", + # "price": "0", + # "origQty": "0", + # "executedQty": "0.0061", + # "cummulativeQuoteQty": "19.736489", + # "cumulativeQuoteQty": "19.736489", + # "avgPrice": "3235.49", + # "status": "FILLED", + # "timeInForce": "IOC", + # "type": "MARKET", + # "side": "BUY", + # "stopPrice": "0.0", + # "icebergQty": "0.0", + # "time": "1722004623186", + # "updateTime": "1722004623406", + # "isWorking": True, + # "reqAmount": "20", + # "feeCoin": "", + # "feeAmount": "0", + # "sumFeeAmount": "0" + # } + # + elif marketType == 'swap': + isTrigger = False + isTrigger, params = self.handle_trigger_option_and_params(params, methodName, isTrigger) + if isTrigger: + request['type'] = 'STOP' + response = await self.privateGetApiV1FuturesOrder(self.extend(request, params)) + # + # { + # "time": "1722429951611", + # "updateTime": "1722429951700", + # "orderId": "1742263144028363776", + # "clientOrderId": "1722429950315", + # "symbol": "ETHUSDT-PERPETUAL", + # "price": "3460.62", + # "leverage": "5", + # "origQty": "10", + # "executedQty": "10", + # "avgPrice": "3327.52", + # "marginLocked": "0", + # "type": "LIMIT", + # "side": "BUY_OPEN", + # "timeInForce": "IOC", + # "status": "FILLED", + # "priceType": "MARKET", + # "isLiquidationOrder": False, + # "indexPrice": "0", + # "liquidationType": "" + # } + # + else: + raise NotSupported(self.id + ' ' + methodName + '() is not supported for ' + marketType + ' type of markets') + return self.parse_order(response) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://hashkeyglobal-apidoc.readme.io/reference/get-current-open-orders + https://hashkeyglobal-apidoc.readme.io/reference/get-sub-account-open-orders + https://hashkeyglobal-apidoc.readme.io/reference/sub + https://hashkeyglobal-apidoc.readme.io/reference/query-open-futures-orders + + :param str [symbol]: unified market symbol of the market orders were made in - is mandatory for swap markets + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve - default 500, maximum 1000 + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: 'spot' or 'swap' - the type of the market to fetch entries for(default 'spot') + :param str [params.orderId]: *spot markets only* the id of the order to fetch + :param str [params.side]: *spot markets only* 'buy' or 'sell' - the side of the orders to fetch + :param str [params.fromOrderId]: *swap markets only* the id of the order to start from + :param bool [params.trigger]: *swap markets only* True for fetching trigger orders(default False) + :param bool [params.stop]: *swap markets only* an alternative for trigger param + :param str [params.accountId]: account id to fetch the orders from + :returns Order[]: a list of `order structures ` + """ + methodName = 'fetchOpenOrders' + self.check_type_param(methodName, params) + await self.load_markets() + market: Market = None + if symbol is not None: + market = self.market(symbol) + marketType = 'spot' + marketType, params = self.handle_market_type_and_params(methodName, market, params, marketType) + params = self.extend({'methodName': methodName}, params) + if marketType == 'spot': + return await self.fetch_open_spot_orders(symbol, since, limit, params) + elif marketType == 'swap': + return await self.fetch_open_swap_orders(symbol, since, limit, params) + else: + raise NotSupported(self.id + ' ' + methodName + '() is not supported for ' + marketType + ' type of markets') + + async def fetch_open_spot_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + @ignore + fetch all unfilled currently open orders for spot markets + + https://hashkeyglobal-apidoc.readme.io/reference/get-current-open-orders + https://hashkeyglobal-apidoc.readme.io/reference/sub + + :param str [symbol]: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve - default 500, maximum 1000 + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.orderId]: the id of the order to fetch + :param str [params.side]: 'buy' or 'sell' - the side of the orders to fetch + :param str [params.accountId]: account id to fetch the orders from + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + methodName = 'fetchOpenSpotOrders' + methodName, params = self.handle_param_string(params, 'methodName', methodName) + market: Market = None + request: dict = {} + response = None + accountId: Str = None + accountId, params = self.handle_option_and_params(params, methodName, 'accountId') + if accountId is not None: + request['subAccountId'] = accountId + response = await self.privateGetApiV1SpotSubAccountOpenOrders(self.extend(request, params)) + else: + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if limit is not None: + request['limit'] = limit + response = await self.privateGetApiV1SpotOpenOrders(self.extend(request, params)) + # + # [ + # { + # "accountId": "1732885739589466112", + # "exchangeId": "301", + # "symbol": "ETHUSDT", + # "symbolName": "ETHUSDT", + # "clientOrderId": "1", + # "orderId": "1739491435386897152", + # "price": "2000", + # "origQty": "0.001", + # "executedQty": "0", + # "cummulativeQuoteQty": "0", + # "cumulativeQuoteQty": "0", + # "avgPrice": "0", + # "status": "NEW", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "side": "BUY", + # "stopPrice": "0.0", + # "icebergQty": "0.0", + # "time": "1722099538193", + # "updateTime": "1722099538197", + # "isWorking": True, + # "reqAmount": "0" + # } + # ] + # + return self.parse_orders(response, market, since, limit) + + async def fetch_open_swap_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + @ignore + fetch all unfilled currently open orders for swap markets + + https://hashkeyglobal-apidoc.readme.io/reference/query-open-futures-orders + https://hashkeyglobal-apidoc.readme.io/reference/get-sub-account-open-orders + + :param str symbol: *is mandatory* unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve - maximum 500 + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.fromOrderId]: the id of the order to start from + :param bool [params.trigger]: True for fetching trigger orders(default False) + :param bool [params.stop]: an alternative for trigger param + :param str [params.accountId]: account id to fetch the orders from + :returns Order[]: a list of `order structures ` + """ + methodName = 'fetchOpenSwapOrders' + methodName, params = self.handle_param_string(params, 'methodName', methodName) + if symbol is None: + raise ArgumentsRequired(self.id + ' ' + methodName + '() requires a symbol argument for swap market orders') + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + isTrigger = False + isTrigger, params = self.handle_trigger_option_and_params(params, methodName, isTrigger) + if isTrigger: + request['type'] = 'STOP' + else: + request['type'] = 'LIMIT' + if limit is not None: + request['limit'] = limit + response = None + accountId: Str = None + accountId, params = self.handle_option_and_params(params, methodName, 'accountId') + if accountId is not None: + request['subAccountId'] = accountId + response = await self.privateGetApiV1FuturesSubAccountOpenOrders(self.extend(request, params)) + else: + response = await self.privateGetApiV1FuturesOpenOrders(self.extend(request, params)) + # 'LIMIT' + # [ + # { + # "time": "1722432302919", + # "updateTime": "1722432302925", + # "orderId": "1742282868229463040", + # "clientOrderId": "1722432301670", + # "symbol": "ETHUSDT-PERPETUAL", + # "price": "4000", + # "leverage": "5", + # "origQty": "10", + # "executedQty": "0", + # "avgPrice": "0", + # "marginLocked": "0", + # "type": "LIMIT_MAKER", + # "side": "SELL_CLOSE", + # "timeInForce": "GTC", + # "status": "NEW", + # "priceType": "INPUT", + # "isLiquidationOrder": False, + # "indexPrice": "0", + # "liquidationType": "" + # } + # ] + # + # 'STOP' + # [ + # { + # "time": "1722433095688", + # "updateTime": "1722433095688", + # "orderId": "1742289518466225664", + # "accountId": "1735619524953226496", + # "clientOrderId": "1722433094438", + # "symbol": "ETHUSDT-PERPETUAL", + # "price": "3700", + # "leverage": "0", + # "origQty": "10", + # "type": "STOP", + # "side": "SELL_CLOSE", + # "status": "ORDER_NEW", + # "stopPrice": "3600" + # } + # ] + return self.parse_orders(response, market, since, limit) + + async def fetch_canceled_and_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple canceled and closed orders made by the user + + https://hashkeyglobal-apidoc.readme.io/reference/get-all-orders + https://hashkeyglobal-apidoc.readme.io/reference/query-futures-history-orders + https://hashkeyglobal-apidoc.readme.io/reference/get-sub-account-history-orders + + :param str symbol: *is mandatory for swap markets* unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve - default 500, maximum 1000 + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch entries for - only supports the last 90 days timeframe + :param str [params.type]: 'spot' or 'swap' - the type of the market to fetch entries for(default 'spot') + :param str [params.orderId]: *spot markets only* the id of the order to fetch + :param str [params.side]: *spot markets only* 'buy' or 'sell' - the side of the orders to fetch + :param str [params.fromOrderId]: *swap markets only* the id of the order to start from + :param bool [params.trigger]: *swap markets only* the id of the order to start from True for fetching trigger orders(default False) + :param bool [params.stop]: *swap markets only* the id of the order to start from an alternative for trigger param + :param str [params.accountId]: account id to fetch the orders from + :returns Order[]: a list of `order structures ` + """ + methodName = 'fetchCanceledAndClosedOrders' + self.check_type_param(methodName, params) + await self.load_markets() + request: dict = {} + if limit is not None: + request['limit'] = limit + if since is not None: + request['startTime'] = since + until: Int = None + until, params = self.handle_option_and_params(params, methodName, 'until') + if until is not None: + request['endTime'] = until + accountId: Str = None + accountId, params = self.handle_option_and_params(params, methodName, 'accountId') + market: Market = None + if symbol is not None: + market = self.market(symbol) + marketType = 'spot' + marketType, params = self.handle_market_type_and_params(methodName, market, params, marketType) + response = None + if marketType == 'spot': + if market is not None: + request['symbol'] = market['id'] + if accountId is not None: + request['accountId'] = accountId + response = await self.privateGetApiV1SpotTradeOrders(self.extend(request, params)) + # + # [ + # { + # "accountId": "1732885739589466112", + # "exchangeId": "301", + # "symbol": "ETHUSDT", + # "symbolName": "ETHUSDT", + # "clientOrderId": "1722082982086472", + # "orderId": "1739352552762301440", + # "price": "0", + # "origQty": "0.001", + # "executedQty": "0.001", + # "cummulativeQuoteQty": "3.28996", + # "cumulativeQuoteQty": "3.28996", + # "avgPrice": "3289.96", + # "status": "FILLED", + # "timeInForce": "IOC", + # "type": "MARKET", + # "side": "BUY", + # "stopPrice": "0.0", + # "icebergQty": "0.0", + # "time": "1722082982093", + # "updateTime": "1722082982097", + # "isWorking": True, + # "reqAmount": "0" + # }, + # ... + # ] + # + elif marketType == 'swap': + if symbol is None: + raise ArgumentsRequired(self.id + ' ' + methodName + '() requires a symbol argument for swap markets') + request['symbol'] = market['id'] + isTrigger = False + isTrigger, params = self.handle_trigger_option_and_params(params, methodName, isTrigger) + if isTrigger: + request['type'] = 'STOP' + else: + request['type'] = 'LIMIT' + if accountId is not None: + request['subAccountId'] = accountId + response = await self.privateGetApiV1FuturesSubAccountHistoryOrders(self.extend(request, params)) + else: + response = await self.privateGetApiV1FuturesHistoryOrders(self.extend(request, params)) + # + # [ + # { + # "time": "1722429951611", + # "updateTime": "1722429951700", + # "orderId": "1742263144028363776", + # "clientOrderId": "1722429950315", + # "symbol": "ETHUSDT-PERPETUAL", + # "price": "3460.62", + # "leverage": "5", + # "origQty": "10", + # "executedQty": "10", + # "avgPrice": "3327.52", + # "marginLocked": "0", + # "type": "LIMIT", + # "side": "BUY_OPEN", + # "timeInForce": "IOC", + # "status": "FILLED", + # "priceType": "MARKET", + # "isLiquidationOrder": False, + # "indexPrice": "0", + # "liquidationType": "" + # } + # ] + # + else: + raise NotSupported(self.id + ' ' + methodName + '() is not supported for ' + marketType + ' type of markets') + return self.parse_orders(response, market, since, limit) + + def check_type_param(self, methodName, params): + # some hashkey endpoints have a type param for swap markets that defines the type of an order + # type param is reserved in ccxt for defining the type of the market + # current method warns user if he provides the exchange specific value in type parameter + paramsType = self.safe_string(params, 'type') + if (paramsType is not None) and (paramsType != 'spot') and (paramsType != 'swap'): + raise BadRequest(self.id + ' ' + methodName + '() type parameter can not be "' + paramsType + '". It should define the type of the market("spot" or "swap"). To define the type of an order use the trigger parameter(True for trigger orders)') + + def handle_trigger_option_and_params(self, params: object, methodName: str, defaultValue=None): + isTrigger = defaultValue + isTrigger, params = self.handle_option_and_params_2(params, methodName, 'stop', 'trigger', isTrigger) + return [isTrigger, params] + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder spot + # { + # "accountId": "1732885739589466112", + # "symbol": "ETHUSDT", + # "symbolName": "ETHUSDT", + # "clientOrderId": "1722004623170558", + # "orderId": "1738695230608169984", + # "transactTime": "1722004623186", + # "price": "0", + # "origQty": "0", + # "executedQty": "0.0061", + # "status": "FILLED", + # "timeInForce": "IOC", + # "type": "MARKET", + # "side": "BUY", + # "reqAmount": "20", + # "concentration": "" + # } + # + # fetchOrder spot + # { + # "accountId": "1732885739589466112", + # "exchangeId": "301", + # "symbol": "ETHUSDT", + # "symbolName": "ETHUSDT", + # "clientOrderId": "1722004623170558", + # "orderId": "1738695230608169984", + # "price": "0", + # "origQty": "0", + # "executedQty": "0.0061", + # "cummulativeQuoteQty": "19.736489", + # "cumulativeQuoteQty": "19.736489", + # "avgPrice": "3235.49", + # "status": "FILLED", + # "timeInForce": "IOC", + # "type": "MARKET", + # "side": "BUY", + # "stopPrice": "0.0", + # "icebergQty": "0.0", + # "time": "1722004623186", + # "updateTime": "1722004623406", + # "isWorking": True, + # "reqAmount": "20", + # "feeCoin": "", + # "feeAmount": "0", + # "sumFeeAmount": "0" + # } + # + # cancelOrder + # { + # "accountId": "1732885739589466112", + # "symbol": "ETHUSDT", + # "clientOrderId": "1722006209978370", + # "orderId": "1738708541676585728", + # "transactTime": "1722006209989", + # "price": "5000", + # "origQty": "0.005", + # "executedQty": "0", + # "status": "NEW", + # "timeInForce": "GTC", + # "type": "LIMIT_MAKER", + # "side": "SELL" + # } + # + # createOrder swap + # { + # "time": "1722429951611", + # "updateTime": "1722429951648", + # "orderId": "1742263144028363776", + # "clientOrderId": "1722429950315", + # "symbol": "ETHUSDT-PERPETUAL", + # "price": "3460.62", + # "leverage": "5", + # "origQty": "10", + # "executedQty": "10", + # "avgPrice": "0", + # "marginLocked": "6.9212", + # "type": "LIMIT", + # "side": "BUY_OPEN", + # "timeInForce": "IOC", + # "status": "FILLED", + # "priceType": "MARKET", + # "contractMultiplier": "0.00100000" + # } + # + # fetchOrder swap + # { + # "time": "1722429951611", + # "updateTime": "1722429951700", + # "orderId": "1742263144028363776", + # "clientOrderId": "1722429950315", + # "symbol": "ETHUSDT-PERPETUAL", + # "price": "3460.62", + # "leverage": "5", + # "origQty": "10", + # "executedQty": "10", + # "avgPrice": "3327.52", + # "marginLocked": "0", + # "type": "LIMIT", + # "side": "BUY_OPEN", + # "timeInForce": "IOC", + # "status": "FILLED", + # "priceType": "MARKET", + # "isLiquidationOrder": False, + # "indexPrice": "0", + # "liquidationType": "" + # } + # + marketId = self.safe_string(order, 'symbol') + market = self.safe_market(marketId, market) + timestamp = self.safe_integer_2(order, 'transactTime', 'time') + status = self.safe_string(order, 'status') + type = self.safe_string(order, 'type') + priceType = self.safe_string(order, 'priceType') + if priceType == 'MARKET': + type = 'market' + price = self.omit_zero(self.safe_string(order, 'price')) + if type == 'STOP': + if price is None: + type = 'market' + else: + type = 'limit' + timeInForce = self.safe_string(order, 'timeInForce') + postOnly: Bool = None + type, timeInForce, postOnly = self.parse_order_type_time_in_force_and_post_only(type, timeInForce) + average = self.omit_zero(self.safe_string(order, 'avgPrice')) + if price is None: + price = average + side = self.safe_string_lower(order, 'side') + reduceOnly: Bool = None + side, reduceOnly = self.parse_order_side_and_reduce_only(side) + feeCurrncyId = self.safe_string(order, 'feeCoin') + if feeCurrncyId == '': + feeCurrncyId = None + return self.safe_order({ + 'id': self.safe_string(order, 'orderId'), + 'clientOrderId': self.safe_string(order, 'clientOrderId'), + 'datetime': self.iso8601(timestamp), + 'timestamp': timestamp, + 'lastTradeTimestamp': None, + 'lastUpdateTimestamp': self.safe_integer(order, 'updateTime'), + 'status': self.parse_order_status(status), + 'symbol': market['symbol'], + 'type': type, + 'timeInForce': timeInForce, + 'side': side, + 'price': price, + 'average': average, + 'amount': self.omit_zero(self.safe_string(order, 'origQty')), + 'filled': self.safe_string(order, 'executedQty'), + 'remaining': None, + 'triggerPrice': self.omit_zero(self.safe_string(order, 'stopPrice')), + 'takeProfitPrice': None, + 'stopLossPrice': None, + 'cost': self.omit_zero(self.safe_string_2(order, 'cumulativeQuoteQty', 'cummulativeQuoteQty')), + 'trades': None, + 'fee': { + 'currency': self.safe_currency_code(feeCurrncyId), + 'amount': self.omit_zero(self.safe_string(order, 'feeAmount')), + }, + 'reduceOnly': reduceOnly, + 'postOnly': postOnly, + 'info': order, + }, market) + + def parse_order_side_and_reduce_only(self, unparsed): + parts = unparsed.split('_') + side = parts[0] + reduceOnly: Bool = None + secondPart = self.safe_string(parts, 1) + if secondPart is not None: + if secondPart == 'open': + reduceOnly = False + elif (secondPart == 'close'): + reduceOnly = True + return [side, reduceOnly] + + def parse_order_status(self, status): + statuses = { + 'NEW': 'open', + 'PARTIALLY_FILLED': 'open', + 'PARTIALLY_CANCELED': 'canceled', + 'FILLED': 'closed', + 'CANCELED': 'canceled', + 'ORDER_CANCELED': 'canceled', + 'PENDING_CANCEL': 'canceled', + 'REJECTED': 'rejected', + 'ORDER_NEW': 'open', + } + return self.safe_string(statuses, status, status) + + def parse_order_type_time_in_force_and_post_only(self, type, timeInForce): + postOnly: Bool = None + if type == 'LIMIT_MAKER': + postOnly = True + elif (timeInForce == 'LIMIT_MAKER') or (timeInForce == 'MAKER'): + postOnly = True + timeInForce = 'PO' + type = self.parse_order_type(type) + return [type, timeInForce, postOnly] + + def parse_order_type(self, type): + types = { + 'MARKET': 'market', + 'LIMIT': 'limit', + 'LIMIT_MAKER': 'limit', + 'MARKET_OF_BASE': 'market', + } + return self.safe_string(types, type, type) + + async def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate + + https://hashkeyglobal-apidoc.readme.io/reference/get-futures-funding-rate + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'timestamp': self.milliseconds(), + } + response = await self.publicGetApiV1FuturesFundingRate(self.extend(request, params)) + # + # [ + # {"symbol": "ETHUSDT-PERPETUAL", "rate": "0.0001", "nextSettleTime": "1722297600000"} + # ] + # + rate = self.safe_dict(response, 0, {}) + return self.parse_funding_rate(rate, market) + + async def fetch_funding_rates(self, symbols: Strings = None, params={}) -> FundingRates: + """ + fetch the funding rate for multiple markets + + https://hashkeyglobal-apidoc.readme.io/reference/get-futures-funding-rate + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `funding rate structures `, indexed by market symbols + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + request: dict = { + 'timestamp': self.milliseconds(), + } + response = await self.publicGetApiV1FuturesFundingRate(self.extend(request, params)) + # + # [ + # {"symbol": "BTCUSDT-PERPETUAL", "rate": "0.0001", "nextSettleTime": "1722297600000"}, + # {"symbol": "ETHUSDT-PERPETUAL", "rate": "0.0001", "nextSettleTime": "1722297600000"} + # ] + # + return self.parse_funding_rates(response, symbols) + + def parse_funding_rate(self, contract, market: Market = None) -> FundingRate: + # + # { + # "symbol": "ETHUSDT-PERPETUAL", + # "rate": "0.0001", + # "nextSettleTime": "1722297600000" + # } + # + marketId = self.safe_string(contract, 'symbol') + market = self.safe_market(marketId, market, None, 'swap') + fundingRate = self.safe_number(contract, 'rate') + fundingTimestamp = self.safe_integer(contract, 'nextSettleTime') + return { + 'info': contract, + 'symbol': market['symbol'], + 'markPrice': None, + 'indexPrice': None, + 'interestRate': None, + 'estimatedSettlePrice': None, + 'timestamp': None, + 'datetime': None, + 'fundingRate': fundingRate, + 'fundingTimestamp': None, + 'fundingDatetime': None, + 'nextFundingRate': None, + 'nextFundingTimestamp': fundingTimestamp, + 'nextFundingDatetime': self.iso8601(fundingTimestamp), + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': None, + } + + async def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical funding rate prices + + https://hashkeyglobal-apidoc.readme.io/reference/get-futures-history-funding-rate + + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: timestamp in ms of the earliest funding rate to fetch + :param int [limit]: the maximum amount of `funding rate structures ` to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.fromId]: the id of the entry to start from + :param int [params.endId]: the id of the entry to end with + :returns dict[]: a list of `funding rate structures ` + """ + await self.load_markets() + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchFundingRateHistory() requires a symbol argument') + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['limit'] = limit + response = await self.publicGetApiV1FuturesHistoryFundingRate(self.extend(request, params)) + # + # [ + # { + # "id": "10698", + # "symbol": "ETHUSDT-PERPETUAL", + # "settleTime": "1722268800000", + # "settleRate": "0.0001" + # }, + # ... + # ] + # + rates = [] + for i in range(0, len(response)): + entry = response[i] + timestamp = self.safe_integer(entry, 'settleTime') + rates.append({ + 'info': entry, + 'symbol': self.safe_symbol(self.safe_string(entry, 'symbol'), market, None, 'swap'), + 'fundingRate': self.safe_number(entry, 'settleRate'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + sorted = self.sort_by(rates, 'timestamp') + return self.filter_by_since_limit(sorted, since, limit) + + async def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch open positions for a market + + https://hashkeyglobal-apidoc.readme.io/reference/get-futures-positions + + fetch all open positions + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.side]: 'LONG' or 'SHORT' - the direction of the position(if not provided, positions for both sides will be returned) + :returns dict[]: a list of `position structure ` + """ + methodName = 'fetchPositions' + if (symbols is None): + raise ArgumentsRequired(self.id + ' ' + methodName + '() requires a symbol argument with one single market symbol') + else: + symbolsLength = len(symbols) + if symbolsLength != 1: + raise NotSupported(self.id + ' ' + methodName + '() is supported for a symbol argument with one single market symbol only') + await self.load_markets() + return await self.fetch_positions_for_symbol(symbols[0], self.extend({'methodName': 'fetchPositions'}, params)) + + async def fetch_positions_for_symbol(self, symbol: str, params={}) -> List[Position]: + """ + fetch open positions for a single market + + https://hashkeyglobal-apidoc.readme.io/reference/get-futures-positions + + fetch all open positions for specific symbol + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.side]: 'LONG' or 'SHORT' - the direction of the position(if not provided, positions for both sides will be returned) + :returns dict[]: a list of `position structure ` + """ + await self.load_markets() + market = self.market(symbol) + methodName = 'fetchPosition' + methodName, params = self.handle_param_string(params, 'methodName', methodName) + if not market['swap']: + raise NotSupported(self.id + ' ' + methodName + '() supports swap markets only') + request: dict = { + 'symbol': market['id'], + } + response = await self.privateGetApiV1FuturesPositions(self.extend(request, params)) + # + # [ + # { + # "symbol": "ETHUSDT-PERPETUAL", + # "side": "LONG", + # "avgPrice": "3327.52", + # "position": "10", + # "available": "0", + # "leverage": "5", + # "lastPrice": "3324.44", + # "positionValue": "33.2752", + # "liquidationPrice": "-953.83", + # "margin": "6.9012", + # "marginRate": "", + # "unrealizedPnL": "-0.0288", + # "profitRate": "-0.0041", + # "realizedPnL": "-0.0199", + # "minMargin": "0.2173" + # } + # ] + # + return self.parse_positions(response, [symbol]) + + def parse_position(self, position: dict, market: Market = None): + marketId = self.safe_string(position, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + return self.safe_position({ + 'symbol': symbol, + 'id': None, + 'timestamp': None, + 'datetime': None, + 'contracts': self.safe_number(position, 'position'), + 'contractSize': None, + 'side': self.safe_string_lower(position, 'side'), + 'notional': self.safe_number(position, 'positionValue'), + 'leverage': self.safe_integer(position, 'leverage'), + 'unrealizedPnl': self.safe_number(position, 'unrealizedPnL'), + 'realizedPnl': self.safe_number(position, 'realizedPnL'), + 'collateral': None, + 'entryPrice': self.safe_number(position, 'avgPrice'), + 'markPrice': None, + 'liquidationPrice': self.safe_number(position, 'liquidationPrice'), + 'marginMode': 'cross', + 'hedged': True, + 'maintenanceMargin': self.safe_number(position, 'minMargin'), + 'maintenanceMarginPercentage': None, + 'initialMargin': self.safe_number(position, 'margin'), + 'initialMarginPercentage': None, + 'marginRatio': None, + 'lastUpdateTimestamp': None, + 'lastPrice': self.safe_number(position, 'lastPrice'), + 'stopLossPrice': None, + 'takeProfitPrice': None, + 'percentage': None, + 'info': position, + }) + + async def fetch_leverage(self, symbol: str, params={}) -> Leverage: + """ + fetch the set leverage for a market + + https://hashkeyglobal-apidoc.readme.io/reference/query-futures-leverage-trade + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `leverage structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = await self.privateGetApiV1FuturesLeverage(self.extend(request, params)) + # + # [ + # { + # "symbolId": "ETHUSDT-PERPETUAL", + # "leverage": "5", + # "marginType": "CROSS" + # } + # ] + # + leverage = self.safe_dict(response, 0, {}) + return self.parse_leverage(leverage, market) + + def parse_leverage(self, leverage: dict, market: Market = None) -> Leverage: + marginMode = self.safe_string_lower(leverage, 'marginType') + leverageValue = self.safe_number(leverage, 'leverage') + return { + 'info': leverage, + 'symbol': market['symbol'], + 'marginMode': marginMode, + 'longLeverage': leverageValue, + 'shortLeverage': leverageValue, + } + + async def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://hashkeyglobal-apidoc.readme.io/reference/change-futures-leverage-trade + + :param float leverage: the rate of leverage + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setLeverage() requires a symbol argument') + await self.load_markets() + request: dict = { + 'leverage': leverage, + } + market = self.market(symbol) + request['symbol'] = market['id'] + response = await self.privatePostApiV1FuturesLeverage(self.extend(request, params)) + # + # { + # "code": "0000", + # "symbolId": "ETHUSDT-PERPETUAL", + # "leverage": "3" + # } + # + return self.parse_leverage(response, market) + + async def fetch_leverage_tiers(self, symbols: Strings = None, params={}) -> LeverageTiers: + """ + retrieve information on the maximum leverage, and maintenance margin for trades of varying trade sizes + + https://hashkeyglobal-apidoc.readme.io/reference/exchangeinfo + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `leverage tiers structures `, indexed by market symbols + """ + await self.load_markets() + response = await self.publicGetApiV1ExchangeInfo(params) + # response is the same fetchMarkets() + data = self.safe_list(response, 'contracts', []) + symbols = self.market_symbols(symbols) + return self.parse_leverage_tiers(data, symbols, 'symbol') + + def parse_market_leverage_tiers(self, info, market: Market = None) -> List[LeverageTier]: + # + # { + # "filters": [ + # { + # "minPrice": "0.1", + # "maxPrice": "100000.00000000", + # "tickSize": "0.1", + # "filterType": "PRICE_FILTER" + # }, + # { + # "minQty": "0.001", + # "maxQty": "10", + # "stepSize": "0.001", + # "marketOrderMinQty": "0", + # "marketOrderMaxQty": "0", + # "filterType": "LOT_SIZE" + # }, + # { + # "minNotional": "0", + # "filterType": "MIN_NOTIONAL" + # }, + # { + # "maxSellPrice": "999999", + # "buyPriceUpRate": "0.05", + # "sellPriceDownRate": "0.05", + # "maxEntrustNum": 200, + # "maxConditionNum": 200, + # "filterType": "LIMIT_TRADING" + # }, + # { + # "buyPriceUpRate": "0.05", + # "sellPriceDownRate": "0.05", + # "filterType": "MARKET_TRADING" + # }, + # { + # "noAllowMarketStartTime": "0", + # "noAllowMarketEndTime": "0", + # "limitOrderStartTime": "0", + # "limitOrderEndTime": "0", + # "limitMinPrice": "0", + # "limitMaxPrice": "0", + # "filterType": "OPEN_QUOTE" + # } + # ], + # "exchangeId": "301", + # "symbol": "BTCUSDT-PERPETUAL", + # "symbolName": "BTCUSDT-PERPETUAL", + # "status": "TRADING", + # "baseAsset": "BTCUSDT-PERPETUAL", + # "baseAssetPrecision": "0.001", + # "quoteAsset": "USDT", + # "quoteAssetPrecision": "0.1", + # "icebergAllowed": False, + # "inverse": False, + # "index": "USDT", + # "marginToken": "USDT", + # "marginPrecision": "0.0001", + # "contractMultiplier": "0.001", + # "underlying": "BTC", + # "riskLimits": [ + # { + # "riskLimitId": "200000722", + # "quantity": "1000.00", + # "initialMargin": "0.10", + # "maintMargin": "0.005", + # "isWhite": False + # }, + # { + # "riskLimitId": "200000723", + # "quantity": "2000.00", + # "initialMargin": "0.10", + # "maintMargin": "0.01", + # "isWhite": False + # } + # ] + # } + # + riskLimits = self.safe_list(info, 'riskLimits', []) + marketId = self.safe_string(info, 'symbol') + market = self.safe_market(marketId, market) + tiers = [] + for i in range(0, len(riskLimits)): + tier = riskLimits[i] + initialMarginRate = self.safe_string(tier, 'initialMargin') + tiers.append({ + 'tier': self.sum(i, 1), + 'symbol': self.safe_symbol(marketId, market), + 'currency': market['settle'], + 'minNotional': None, + 'maxNotional': self.safe_number(tier, 'quantity'), + 'maintenanceMarginRate': self.safe_number(tier, 'maintMargin'), + 'maxLeverage': self.parse_number(Precise.string_div('1', initialMarginRate)), + 'info': tier, + }) + return tiers + + async def fetch_trading_fee(self, symbol: str, params={}) -> TradingFeeInterface: + """ + fetch the trading fees for a market + + https://developers.binance.com/docs/wallet/asset/trade-fee # spot + https://hashkeyglobal-apidoc.readme.io/reference/get-futures-commission-rate-request-weight # swap + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `fee structure ` + """ + await self.load_markets() + market = self.market(symbol) + methodName = 'fetchTradingFee' + response = None + if market['spot']: + response = await self.fetch_trading_fees(params) + return self.safe_dict(response, symbol) + elif market['swap']: + response = await self.privateGetApiV1FuturesCommissionRate(self.extend({'symbol': market['id']}, params)) + return self.parse_trading_fee(response, market) + # + # { + # "openMakerFee": "0.00025", + # "openTakerFee": "0.0006", + # "closeMakerFee": "0.00025", + # "closeTakerFee": "0.0006" + # } + # + else: + raise NotSupported(self.id + ' ' + methodName + '() is not supported for ' + market['type'] + ' type of markets') + + async def fetch_trading_fees(self, params={}) -> TradingFees: + """ + *for spot markets only* fetch the trading fees for multiple markets + + https://developers.binance.com/docs/wallet/asset/trade-fee + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + await self.load_markets() + response = await self.privateGetApiV1AccountVipInfo(params) + # + # { + # "code": 0, + # "vipLevel": "0", + # "tradeVol30Day": "67", + # "totalAssetBal": "0", + # "data": [ + # { + # "symbol": "UXLINKUSDT", + # "productType": "Token-Token", + # "buyMakerFeeCurrency": "UXLINK", + # "buyTakerFeeCurrency": "UXLINK", + # "sellMakerFeeCurrency": "USDT", + # "sellTakerFeeCurrency": "USDT", + # "actualMakerRate": "0.0012", + # "actualTakerRate": "0.0012" + # }, + # ... + # ], + # "updateTimestamp": "1722320137809" + # } + # + data = self.safe_list(response, 'data', []) + result: dict = {} + for i in range(0, len(data)): + fee = self.safe_dict(data, i, {}) + parsedFee = self.parse_trading_fee(fee) + result[parsedFee['symbol']] = parsedFee + return result + + def parse_trading_fee(self, fee: dict, market: Market = None) -> TradingFeeInterface: + # + # spot + # { + # "symbol": "UXLINKUSDT", + # "productType": "Token-Token", + # "buyMakerFeeCurrency": "UXLINK", + # "buyTakerFeeCurrency": "UXLINK", + # "sellMakerFeeCurrency": "USDT", + # "sellTakerFeeCurrency": "USDT", + # "actualMakerRate": "0.0012", + # "actualTakerRate": "0.0012" + # } + # + # swap + # { + # "openMakerFee": "0.00025", + # "openTakerFee": "0.0006", + # "closeMakerFee": "0.00025", + # "closeTakerFee": "0.0006" + # } + # + marketId = self.safe_string(fee, 'symbol') + market = self.safe_market(marketId, market) + return { + 'info': fee, + 'symbol': market['symbol'], + 'maker': self.safe_number_2(fee, 'openMakerFee', 'actualMakerRate'), + 'taker': self.safe_number_2(fee, 'openTakerFee', 'actualTakerRate'), + 'percentage': True, + 'tierBased': True, + } + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = self.urls['api'][api] + '/' + path + query: Str = None + if api == 'private': + self.check_required_credentials() + timestamp = self.milliseconds() + additionalParams = { + 'timestamp': timestamp, + } + recvWindow = self.safe_integer(self.options, 'recvWindow') + if recvWindow is not None: + additionalParams['recvWindow'] = recvWindow + headers = { + 'X-HK-APIKEY': self.apiKey, + 'Content-Type': 'application/x-www-form-urlencoded', + } + signature: Str = None + if (method == 'POST') and ((path == 'api/v1/spot/batchOrders') or (path == 'api/v1/futures/batchOrders')): + headers['Content-Type'] = 'application/json' + body = self.json(self.safe_list(params, 'orders')) + signature = self.hmac(self.encode(self.custom_urlencode(additionalParams)), self.encode(self.secret), hashlib.sha256) + query = self.custom_urlencode(self.extend(additionalParams, {'signature': signature})) + url += '?' + query + else: + totalParams = self.extend(additionalParams, params) + signature = self.hmac(self.encode(self.custom_urlencode(totalParams)), self.encode(self.secret), hashlib.sha256) + totalParams['signature'] = signature + query = self.custom_urlencode(totalParams) + if method == 'GET': + url += '?' + query + else: + body = query + headers['INPUT-SOURCE'] = self.safe_string(self.options, 'broker', '10000700011') + headers['broker_sign'] = signature + else: + query = self.urlencode(params) + if len(query) != 0: + url += '?' + query + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def custom_urlencode(self, params: dict = {}) -> Str: + result = self.urlencode(params) + result = result.replace('%2C', ',') + return result + + def handle_errors(self, code, reason, url, method, headers, body, response, requestHeaders, requestBody): + if response is None: + return None + errorInArray = False + responseCodeString = self.safe_string(response, 'code', None) + responseCodeInteger = self.safe_integer(response, 'code', None) # some codes in response are returned as '0000' others + if responseCodeInteger == 0: + result = self.safe_list(response, 'result', []) # for batch methods + for i in range(0, len(result)): + entry = self.safe_dict(result, i) + entryCodeInteger = self.safe_integer(entry, 'code') + if entryCodeInteger != 0: + errorInArray = True + responseCodeString = self.safe_string(entry, 'code') + if (code != 200) or errorInArray: + feedback = self.id + ' ' + body + self.throw_broadly_matched_exception(self.exceptions['broad'], responseCodeString, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], responseCodeString, feedback) + raise ExchangeError(feedback) + return None diff --git a/ccxt/async_support/hibachi.py b/ccxt/async_support/hibachi.py new file mode 100644 index 0000000..8c7b3dd --- /dev/null +++ b/ccxt/async_support/hibachi.py @@ -0,0 +1,2073 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.hibachi import ImplicitAPI +import asyncio +import hashlib +from ccxt.base.types import Any, Balances, Currencies, Currency, DepositAddress, Int, LedgerEntry, Market, Num, Order, OrderBook, OrderRequest, OrderSide, OrderType, Position, Str, Strings, Ticker, FundingRate, Trade, TradingFees, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import BadRequest +from ccxt.base.errors import OrderNotFound +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class hibachi(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(hibachi, self).describe(), { + 'id': 'hibachi', + 'name': 'Hibachi', + 'countries': ['US'], + 'rateLimit': 100, + 'userAgent': self.userAgents['chrome'], + 'certified': False, + 'pro': False, + 'dex': True, + 'has': { + 'CORS': None, + 'spot': False, + 'margin': False, + 'swap': True, + 'future': False, + 'option': False, + 'addMargin': False, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'cancelOrders': True, + 'cancelWithdraw': False, + 'closeAllPositions': False, + 'closePosition': False, + 'createConvertTrade': False, + 'createDepositAddress': False, + 'createMarketBuyOrderWithCost': False, + 'createMarketOrder': False, + 'createMarketOrderWithCost': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createOrders': True, + 'createOrderWithTakeProfitAndStopLoss': False, + 'createReduceOnlyOrder': False, + 'createStopLimitOrder': False, + 'createStopLossOrder': False, + 'createStopMarketOrder': False, + 'createStopOrder': False, + 'createTakeProfitOrder': False, + 'createTrailingAmountOrder': False, + 'createTrailingPercentOrder': False, + 'createTriggerOrder': False, + 'editOrder': True, + 'editOrders': True, + 'fetchAccounts': False, + 'fetchBalance': True, + 'fetchCanceledOrders': False, + 'fetchClosedOrder': False, + 'fetchClosedOrders': False, + 'fetchConvertCurrencies': False, + 'fetchConvertQuote': False, + 'fetchCurrencies': False, + 'fetchDepositAddress': True, + 'fetchDeposits': True, + 'fetchDepositsWithdrawals': False, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': True, + 'fetchFundingRateHistory': True, + 'fetchFundingRates': False, + 'fetchIndexOHLCV': False, + 'fetchLedger': True, + 'fetchLeverage': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarkets': True, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': True, + 'fetchOpenInterestHistory': False, + 'fetchOpenOrder': False, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': False, + 'fetchOrderTrades': False, + 'fetchPosition': False, + 'fetchPositionMode': False, + 'fetchPositions': True, + 'fetchPremiumIndexOHLCV': False, + 'fetchStatus': False, + 'fetchTicker': True, + 'fetchTickers': False, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': True, + 'fetchTradingLimits': False, + 'fetchTransactions': 'emulated', + 'fetchTransfers': False, + 'fetchWithdrawals': True, + 'reduceMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setPositionMode': False, + 'transfer': False, + 'withdraw': True, + }, + 'timeframes': { + '1m': '1min', + '5m': '5min', + '15m': '15min', + '1h': '1h', + '4h': '4h', + '1d': '1d', + '1w': '1w', + }, + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/7301bbb1-4f27-4167-8a55-75f74b14e973', + 'api': { + 'public': 'https://data-api.hibachi.xyz', + 'private': 'https://api.hibachi.xyz', + }, + 'www': 'https://www.hibachi.xyz/', + 'referral': { + 'url': 'hibachi.xyz/r/ZBL2YFWIHU', + }, + }, + 'api': { + 'public': { + 'get': { + 'market/exchange-info': 1, + 'market/data/trades': 1, + 'market/data/prices': 1, + 'market/data/stats': 1, + 'market/data/klines': 1, + 'market/data/orderbook': 1, + 'market/data/open-interest': 1, + 'market/data/funding-rates': 1, + 'exchange/utc-timestamp': 1, + }, + }, + 'private': { + 'get': { + 'capital/deposit-info': 1, + 'capital/history': 1, + 'trade/account/trading_history': 1, + 'trade/account/info': 1, + 'trade/order': 1, + 'trade/account/trades': 1, + 'trade/orders': 1, + }, + 'put': { + 'trade/order': 1, + }, + 'delete': { + 'trade/order': 1, + 'trade/orders': 1, + }, + 'post': { + 'trade/order': 1, + 'trade/orders': 1, + 'capital/withdraw': 1, + }, + }, + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': False, + 'accountId': True, + 'privateKey': True, + }, + 'fees': { + 'trading': { + 'tierBased': False, + 'percentage': True, + 'maker': self.parse_number('0.00015'), + 'taker': self.parse_number('0.00045'), + }, + }, + 'currencies': self.hardcoded_currencies(), + 'options': { + }, + 'features': { + 'default': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerPriceType': None, + 'triggerDirection': None, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': False, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': False, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': None, + 'daysBack': None, + 'untilDays': None, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': None, + 'fetchOHLCV': { + 'limit': None, + }, + }, + 'swap': { + 'linear': { + 'extends': 'default', + }, + 'inverse': None, + }, + 'future': { + 'linear': { + 'extends': 'default', + }, + 'inverse': None, + }, + }, + 'commonCurrencies': {}, + 'exceptions': { + 'exact': { + '2': BadRequest, # {"errorCode":2,"message":"Invalid signature: Failed to verify signature"} + '3': OrderNotFound, # {"errorCode":3,"message":"Not found: order ID 33","status":"failed"} + '4': BadRequest, # {"errorCode":4,"message":"Missing accountId","status":"failed"} + }, + 'broad': { + }, + }, + 'precisionMode': TICK_SIZE, + }) + + def get_account_id(self): + self.check_required_credentials() + id = self.parse_to_int(self.accountId) + return id + + def parse_market(self, market: dict) -> Market: + marketId = self.safe_string(market, 'symbol') + numericId = self.safe_number(market, 'id') + marketType = 'swap' + baseId = self.safe_string(market, 'underlyingSymbol') + quoteId = self.safe_string(market, 'settlementSymbol') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + settleId: Str = self.safe_string(market, 'settlementSymbol') + settle: Str = self.safe_currency_code(settleId) + symbol = base + '/' + quote + ':' + settle + created = self.safe_integer_product(market, 'marketCreationTimestamp', 1000) + return { + 'id': marketId, + 'numericId': numericId, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': marketType, + 'spot': False, + 'margin': False, + 'swap': True, + 'future': False, + 'option': False, + 'active': self.safe_string(market, 'status') == 'LIVE', + 'contract': True, + 'linear': True, + 'inverse': False, + 'contractSize': self.parse_number('1'), + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(self.parse_precision(self.safe_string(market, 'underlyingDecimals'))), + 'price': self.parse_number(self.safe_list(market, 'orderbookGranularities')[0]) / 10000.0, + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': None, + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': self.safe_number(market, 'minNotional'), + 'max': None, + }, + }, + 'created': created, + 'info': market, + } + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for hibachi + + https://api-doc.hibachi.xyz/#183981da-8df5-40a0-a155-da15015dd536 + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = await self.publicGetMarketExchangeInfo(params) + # { + # "displayName": "ETH/USDT Perps", + # "id": 1, + # "maintenanceFactorForPositions": "0.030000", + # "marketCloseTimestamp": null, + # "marketOpenTimestamp": null, + # "minNotional": "1", + # "minOrderSize": "0.000000001", + # "orderbookGranularities": [ + # "0.01", + # "0.1", + # "1", + # "10" + # ], + # "riskFactorForOrders": "0.066667", + # "riskFactorForPositions": "0.030000", + # "settlementDecimals": 6, + # "settlementSymbol": "USDT", + # "status": "LIVE", + # "stepSize": "0.000000001", + # "symbol": "ETH/USDT-P", + # "tickSize": "0.000001", + # "underlyingDecimals": 9, + # "underlyingSymbol": "ETH" + # }, + rows = self.safe_list(response, 'futureContracts') + return self.parse_markets(rows) + + def hardcoded_currencies(self) -> Currencies: + # Hibachi only supports USDT on Arbitrum at self time + # We don't have an API endpoint to expose self information yet + result: dict = {} + networks: dict = {} + networkId = 'ARBITRUM' + networks[networkId] = { + 'id': networkId, + 'network': networkId, + 'limits': { + 'withdraw': { + 'min': None, + 'max': None, + }, + 'deposit': { + 'min': None, + 'max': None, + }, + }, + 'active': None, + 'deposit': None, + 'withdraw': None, + 'info': {}, + } + code = self.safe_currency_code('USDT') + result[code] = self.safe_currency_structure({ + 'id': 'USDT', + 'name': 'USDT', + 'type': 'fiat', + 'code': code, + 'precision': self.parse_number('0.000001'), + 'active': True, + 'fee': None, + 'networks': networks, + 'deposit': True, + 'withdraw': True, + 'limits': { + 'deposit': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + }, + 'info': {}, + }) + return result + + def parse_balance(self, response) -> Balances: + result: dict = { + 'info': response, + } + # Hibachi only supports USDT on Arbitrum at self time + code = self.safe_currency_code('USDT') + account = self.account() + account['total'] = self.safe_string(response, 'balance') + account['free'] = self.safe_string(response, 'maximalWithdraw') + result[code] = account + return self.safe_balance(result) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://api-doc.hibachi.xyz/#69aafedb-8274-4e21-bbaf-91dace8b8f31 + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + request: dict = { + 'accountId': self.get_account_id(), + } + response = await self.privateGetTradeAccountInfo(self.extend(request, params)) + # + # { + # assets: [{quantity: '3.000000', symbol: 'USDT'}], + # balance: '3.000000', + # maximalWithdraw: '3.000000', + # numFreeTransfersRemaining: '100', + # positions: [], + # totalOrderNotional: '0.000000', + # totalPositionNotional: '0.000000', + # totalUnrealizedFundingPnl: '0.000000', + # totalUnrealizedPnl: '0.000000', + # totalUnrealizedTradingPnl: '0.000000', + # tradeMakerFeeRate: '0.00000000', + # tradeTakerFeeRate: '0.00020000' + # } + # + return self.parse_balance(response) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + prices = self.safe_dict(ticker, 'prices') + stats = self.safe_dict(ticker, 'stats') + bid = self.safe_number(prices, 'bidPrice') + ask = self.safe_number(prices, 'askPrice') + last = self.safe_number(prices, 'tradePrice') + high = self.safe_number(stats, 'high24h') + low = self.safe_number(stats, 'low24h') + volume = self.safe_number(stats, 'volume24h') + return self.safe_ticker({ + 'symbol': self.safe_symbol(None, market), + 'timestamp': None, + 'datetime': None, + 'bid': bid, + 'ask': ask, + 'last': last, + 'high': high, + 'low': low, + 'bidVolume': None, + 'askVolume': None, + 'vwap': None, + 'open': None, + 'close': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': None, + 'quoteVolume': volume, + 'info': ticker, + }, market) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # public fetchTrades: + # { + # "price": "3512.431902", + # "quantity": "1.414780098", + # "takerSide": "Buy", + # "timestamp": 1712692147 + # } + # + # private fetchMyTrades: + # { + # "askAccountId": 221, + # "askOrderId": 589168494921909200, + # "bidAccountId": 132, + # "bidOrderId": 589168494829895700, + # "fee": "0.000477", + # "id": 199511136, + # "orderType": "MARKET", + # "price": "119257.90000", + # "quantity": "0.0000200000", + # "realizedPnl": "-0.000352", + # "side": "Sell", + # "symbol": "BTC/USDT-P", + # "timestamp": 1752543391 + # } + marketId = self.safe_string(trade, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + id = self.safe_string(trade, 'id') + price = self.safe_string(trade, 'price') + amount = self.safe_string(trade, 'quantity') + timestamp = self.safe_integer_product(trade, 'timestamp', 1000) + cost = Precise.string_mul(price, amount) + side = None + fee = None + orderType = None + orderId = None + takerOrMaker = None + if id is None: + # public trades + side = self.safe_string_lower(trade, 'takerSide') + takerOrMaker = 'taker' + else: + # private trades + side = self.safe_string_lower(trade, 'side') + fee = {'cost': self.safe_string(trade, 'fee'), 'currency': 'USDT'} + orderType = self.safe_string_lower(trade, 'orderType') + if side == 'buy': + orderId = self.safe_string(trade, 'bidOrderId') + else: + orderId = self.safe_string(trade, 'askOrderId') + return self.safe_trade({ + 'id': id, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'side': side, + 'price': price, + 'amount': amount, + 'cost': cost, + 'order': orderId, + 'takerOrMaker': takerOrMaker, + 'type': orderType, + 'fee': fee, + 'info': trade, + }, market) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://api-doc.hibachi.xyz/#86a53bc1-d3bb-4b93-8a11-7034d4698caa + + :param str symbol: unified market symbol + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch(maximum value is 100) + :param dict [params]: extra parameters specific to the hibachi api endpoint + :returns dict[]: a list of recent [trade structures] + """ + await self.load_markets() + market = self.market(symbol) + request = { + 'symbol': market['id'], + } + response = await self.publicGetMarketDataTrades(self.extend(request, params)) + # + # { + # "trades": [ + # { + # "price": "111091.38352", + # "quantity": "0.0090090093", + # "takerSide": "Buy", + # "timestamp": 1752095479 + # }, + # ] + # } + # + trades = self.safe_list(response, 'trades', []) + return self.parse_trades(trades, market) + + async def fetch_ticker(self, symbol: Str, params={}) -> Ticker: + """ + + https://api-doc.hibachi.xyz/#4abb30c4-e5c7-4b0f-9ade-790111dbfa47 + + fetches a price ticker and the related information for the past 24h + :param str symbol: unified symbol of the market + :param dict [params]: extra parameters specific to the hibachi api endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + rawPromises = [ + self.publicGetMarketDataPrices(self.extend(request, params)), + self.publicGetMarketDataStats(self.extend(request, params)), + ] + promises = await asyncio.gather(*rawPromises) + pricesResponse = promises[0] + # { + # "askPrice": "3514.650296", + # "bidPrice": "3513.596112", + # "fundingRateEstimation": { + # "estimatedFundingRate": "0.000001", + # "nextFundingTimestamp": 1712707200 + # }, + # "markPrice": "3514.288858", + # "spotPrice": "3514.715000", + # "symbol": "ETH/USDT-P", + # "tradePrice": "2372.746570" + # } + statsResponse = promises[1] + # { + # "high24h": "3819.507827", + # "low24h": "3754.474162", + # "symbol": "ETH/USDT-P", + # "volume24h": "23554.858590416" + # } + ticker = { + 'prices': pricesResponse, + 'stats': statsResponse, + } + return self.parse_ticker(ticker, market) + + def parse_order_status(self, status: str) -> str: + statuses: dict = { + 'PENDING': 'open', + 'CHILD_PENDING': 'open', + 'SCHEDULED_TWAP': 'open', + 'PLACED': 'open', + 'PARTIALLY_FILLED': 'open', + 'FILLED': 'closed', + 'CANCELLED': 'canceled', + 'REJECTED': 'rejected', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + marketId = self.safe_string(order, 'symbol') + market = self.safe_market(marketId, market) + status = self.safe_string(order, 'status') + type = self.safe_string_lower(order, 'orderType') + price = self.safe_string(order, 'price') + rawSide = self.safe_string(order, 'side') + side = None + if rawSide == 'BID': + side = 'buy' + elif rawSide == 'ASK': + side = 'sell' + amount = self.safe_string(order, 'totalQuantity') + remaining = self.safe_string(order, 'availableQuantity') + totalQuantity = self.safe_string(order, 'totalQuantity') + availableQuantity = self.safe_string(order, 'availableQuantity') + filled = None + if totalQuantity is not None and availableQuantity is not None: + filled = Precise.string_sub(totalQuantity, availableQuantity) + timeInForce = 'GTC' + orderFlags = self.safe_value(order, 'orderFlags') + postOnly = False + reduceOnly = False + if orderFlags == 'POST_ONLY': + timeInForce = 'PO' + postOnly = True + elif orderFlags == 'IOC': + timeInForce = 'IOC' + elif orderFlags == 'REDUCE_ONLY': + reduceOnly = True + return self.safe_order({ + 'info': order, + 'id': self.safe_string(order, 'orderId'), + 'clientOrderId': None, + 'datetime': None, + 'timestamp': None, + 'lastTradeTimestamp': None, + 'lastUpdateTimestamp': None, + 'status': self.parse_order_status(status), + 'symbol': market['symbol'], + 'type': type, + 'timeInForce': timeInForce, + 'side': side, + 'price': price, + 'average': None, + 'amount': amount, + 'filled': filled, + 'remaining': remaining, + 'cost': None, + 'trades': None, + 'fee': None, + 'reduceOnly': reduceOnly, + 'postOnly': postOnly, + 'triggerPrice': self.safe_number(order, 'triggerPrice'), + }, market) + + async def fetch_order(self, id: str, symbol: Str = None, params={}) -> Order: + """ + fetches information on an order made by the user + + https://api-doc.hibachi.xyz/#096a8854-b918-4de8-8731-b2a28d26b96d + + :param str id: the order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + request: dict = { + 'orderId': id, + 'accountId': self.get_account_id(), + } + response = await self.privateGetTradeOrder(self.extend(request, params)) + return self.parse_order(response, market) + + async def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fee + @param params extra parameters + :returns dict: a map of market symbols to `fee structures ` + """ + await self.load_markets() + request: dict = { + 'accountId': self.get_account_id(), + } + response = await self.privateGetTradeAccountInfo(self.extend(request, params)) + # { + # "tradeMakerFeeRate": "0.00000000", + # "tradeTakerFeeRate": "0.00020000" + # }, + makerFeeRate = self.safe_number(response, 'tradeMakerFeeRate') + takerFeeRate = self.safe_number(response, 'tradeTakerFeeRate') + result: dict = {} + for i in range(0, len(self.symbols)): + symbol = self.symbols[i] + result[symbol] = { + 'info': response, + 'symbol': symbol, + 'maker': makerFeeRate, + 'taker': takerFeeRate, + 'percentage': True, + } + return result + + def order_message(self, market, nonce: float, feeRate: float, type: OrderType, side: OrderSide, amount: float, price: Num = None): + sideInternal = 0 + if side == 'sell': + sideInternal = 0 + elif side == 'buy': + sideInternal = 1 + # Converting them to internal representation: + # - Quantity: Internal = External * (10^underlyingDecimals) + # - Price: Internal = External * (2^32) * (10^(settlementDecimals-underlyingDecimals)) + # - FeeRate: Internal = External * (10^8) + amountStr = self.amount_to_precision(self.safe_string(market, 'symbol'), amount) + feeRateStr = self.number_to_string(feeRate) + info = self.safe_dict(market, 'info') + underlying = '1e' + self.safe_string(info, 'underlyingDecimals') + settlement = '1e' + self.safe_string(info, 'settlementDecimals') + one = '1' + feeRateFactor = '100000000' # 10^8 + priceFactor = '4294967296' # 2^32 + quantityInternal = Precise.string_div(Precise.string_mul(amountStr, underlying), one, 0) + feeRateInternal = Precise.string_div(Precise.string_mul(feeRateStr, feeRateFactor), one, 0) + # Encoding + nonce16 = self.int_to_base16(nonce) + noncePadded = nonce16.rjust(16, '0') + encodedNonce = self.base16_to_binary(noncePadded) + numericId = self.int_to_base16(self.safe_integer(market, 'numericId')) + numericIdPadded = numericId.rjust(8, '0') + encodedMarketId = self.base16_to_binary(numericIdPadded) + quantity16 = self.int_to_base16(self.parse_to_int(quantityInternal)) + quantityPadded = quantity16.rjust(16, '0') + encodedQuantity = self.base16_to_binary(quantityPadded) + sideInternal16 = self.int_to_base16(sideInternal) + sidePadded = sideInternal16.rjust(8, '0') + encodedSide = self.base16_to_binary(sidePadded) + feeRateInternal16 = self.int_to_base16(self.parse_to_int(feeRateInternal)) + feeRatePadded = feeRateInternal16.rjust(16, '0') + encodedFeeRate = self.base16_to_binary(feeRatePadded) + encodedPrice = self.binary_concat() + if type == 'limit': + priceStr = self.price_to_precision(self.safe_string(market, 'symbol'), price) + priceInternal = Precise.string_div(Precise.string_div(Precise.string_mul(Precise.string_mul(priceStr, priceFactor), settlement), underlying), one, 0) + price16 = self.int_to_base16(self.parse_to_int(priceInternal)) + pricePadded = price16.rjust(16, '0') + encodedPrice = self.base16_to_binary(pricePadded) + message = self.binary_concat(encodedNonce, encodedMarketId, encodedQuantity, encodedSide, encodedPrice, encodedFeeRate) + return message + + def create_order_request(self, nonce: float, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + market = self.market(symbol) + feeRate = max(self.safe_number(market, 'taker', self.safe_number(self.options, 'defaultTakerFee', 0.00045)), self.safe_number(market, 'maker', self.safe_number(self.options, 'defaultMakerFee', 0.00015))) + sideInternal = '' + if side == 'sell': + sideInternal = 'ASK' + elif side == 'buy': + sideInternal = 'BID' + priceInternal = '' + if price: + priceInternal = self.price_to_precision(symbol, price) + message = self.order_message(market, nonce, feeRate, type, side, amount, price) + signature = self.sign_message(message, self.privateKey) + request = { + 'symbol': self.safe_string(market, 'id'), + 'nonce': nonce, + 'side': sideInternal, + 'orderType': type.upper(), + 'quantity': self.amount_to_precision(symbol, amount), + 'price': priceInternal, + 'signature': signature, + 'maxFeesPercent': self.number_to_string(feeRate), + } + postOnly = self.is_post_only(type.upper() == 'MARKET', None, params) + reduceOnly = self.safe_bool_2(params, 'reduceOnly', 'reduce_only') + timeInForce = self.safe_string_lower(params, 'timeInForce') + triggerPrice = self.safe_string_2(params, 'triggerPrice', 'stopPrice') + if postOnly: + request['orderFlags'] = 'POST_ONLY' + elif timeInForce == 'ioc': + request['orderFlags'] = 'IOC' + elif reduceOnly: + request['orderFlags'] = 'REDUCE_ONLY' + if triggerPrice is not None: + request['triggerPrice'] = triggerPrice + params = self.omit(params, ['reduceOnly', 'reduce_only', 'postOnly', 'timeInForce', 'stopPrice', 'triggerPrice']) + return self.extend(request, params) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://api-doc.hibachi.xyz/#00f6d5ad-5275-41cb-a1a8-19ed5d142124 + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + nonce = self.nonce() + request = self.create_order_request(nonce, symbol, type, side, amount, price, params) + request['accountId'] = self.get_account_id() + response = await self.privatePostTradeOrder(request) + # + # { + # "orderId": "578721673790138368" + # } + # + return self.safe_order({ + 'id': self.safe_string(response, 'orderId'), + 'status': 'pending', + }) + + async def create_orders(self, orders: List[OrderRequest], params={}) -> List[Order]: + """ + *contract only* create a list of trade orders + + https://api-doc.hibachi.xyz/#c2840b9b-f02c-44ed-937d-dc2819f135b4 + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + nonce = self.nonce() + requestOrders = [] + for i in range(0, len(orders)): + rawOrder = orders[i] + symbol = self.safe_string(rawOrder, 'symbol') + type = self.safe_string(rawOrder, 'type') + side = self.safe_string(rawOrder, 'side') + amount = self.safe_value(rawOrder, 'amount') + price = self.safe_value(rawOrder, 'price') + orderParams = self.safe_dict(rawOrder, 'params', {}) + orderRequest = self.create_order_request(nonce + i, symbol, type, side, amount, price, orderParams) + orderRequest['action'] = 'place' + requestOrders.append(orderRequest) + request: dict = { + 'accountId': self.get_account_id(), + 'orders': requestOrders, + } + response = await self.privatePostTradeOrders(self.extend(request, params)) + # + # {"orders": [{nonce: '1754349993908', orderId: '589642085255349248'}]} + # + ret = [] + responseOrders = self.safe_list(response, 'orders') + for i in range(0, len(responseOrders)): + responseOrder = responseOrders[i] + ret.append(self.safe_order({ + 'info': responseOrder, + 'id': self.safe_string(responseOrder, 'orderId'), + 'status': 'pending', + })) + return ret + + def edit_order_request(self, nonce: float, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + market = self.market(symbol) + feeRate = max(self.safe_number(market, 'taker'), self.safe_number(market, 'maker')) + message = self.order_message(market, nonce, feeRate, type, side, amount, price) + signature = self.sign_message(message, self.privateKey) + request = { + 'orderId': id, + 'nonce': nonce, + 'updatedQuantity': self.amount_to_precision(symbol, amount), + 'updatedPrice': self.price_to_precision(symbol, price), + 'maxFeesPercent': self.number_to_string(feeRate), + 'signature': signature, + } + return self.extend(request, params) + + async def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + """ + edit a limit order that is not matched + + https://api-doc.hibachi.xyz/#94d2cdaf-1c71-440f-a981-da1112824810 + + :param str id: order id + :param str symbol: unified symbol of the market to create an order in + :param str type: must be 'limit' + :param str side: 'buy' or 'sell', should stay the same with original side + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + nonce = self.nonce() + request = self.edit_order_request(nonce, id, symbol, type, side, amount, price, params) + request['accountId'] = self.get_account_id() + await self.privatePutTradeOrder(request) + # At self time the response body is empty. A 200 response means the update request is accepted and sent to process + # + # {} + # + return self.safe_order({ + 'id': id, + 'status': 'pending', + }) + + async def edit_orders(self, orders: List[OrderRequest], params={}) -> List[Order]: + """ + edit a list of trade orders + + https://api-doc.hibachi.xyz/#c2840b9b-f02c-44ed-937d-dc2819f135b4 + + :param Array orders: list of orders to edit, each object should contain the parameters required by editOrder, namely id, symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + nonce = self.nonce() + requestOrders = [] + for i in range(0, len(orders)): + rawOrder = orders[i] + id = self.safe_string(rawOrder, 'id') + symbol = self.safe_string(rawOrder, 'symbol') + type = self.safe_string(rawOrder, 'type') + side = self.safe_string(rawOrder, 'side') + amount = self.safe_value(rawOrder, 'amount') + price = self.safe_value(rawOrder, 'price') + orderParams = self.safe_dict(rawOrder, 'params', {}) + orderRequest = self.edit_order_request(nonce + i, id, symbol, type, side, amount, price, orderParams) + orderRequest['action'] = 'modify' + requestOrders.append(orderRequest) + request: dict = { + 'accountId': self.get_account_id(), + 'orders': requestOrders, + } + response = await self.privatePostTradeOrders(self.extend(request, params)) + # + # {"orders": [{"orderId": "589636801329628160"}]} + # + ret = [] + responseOrders = self.safe_list(response, 'orders') + for i in range(0, len(responseOrders)): + responseOrder = responseOrders[i] + ret.append(self.safe_order({ + 'info': responseOrder, + 'id': self.safe_string(responseOrder, 'orderId'), + 'status': 'pending', + })) + return ret + + def cancel_order_request(self, id: str): + bigid = self.convert_to_big_int(id) + idbase16 = self.int_to_base16(bigid) + idPadded = idbase16.rjust(16, '0') + message = self.base16_to_binary(idPadded) + signature = self.sign_message(message, self.privateKey) + return { + 'orderId': id, + 'signature': signature, + } + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + + https://api-doc.hibachi.xyz/#e99c4f48-e610-4b7c-b7f6-1b4bb7af0271 + + cancels an open order + :param str id: order id + :param str symbol: is unused + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + request: dict = self.cancel_order_request(id) + request['accountId'] = self.get_account_id() + response = await self.privateDeleteTradeOrder(self.extend(request, params)) + # At self time the response body is empty. A 200 response means the cancel request is accepted and sent to cancel + # + # {} + # + return self.safe_order({ + 'info': response, + 'id': id, + 'status': 'canceled', + }) + + async def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): + """ + cancel multiple orders + + https://api-doc.hibachi.xyz/#c2840b9b-f02c-44ed-937d-dc2819f135b4 + + :param str[] ids: order ids + :param str [symbol]: unified market symbol, unused + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an list of `order structures ` + """ + orders = [] + for i in range(0, len(ids)): + orderRequest = self.cancel_order_request(ids[i]) + orderRequest['action'] = 'cancel' + orders.append(orderRequest) + request: dict = { + 'accountId': self.get_account_id(), + 'orders': orders, + } + response = await self.privatePostTradeOrders(self.extend(request, params)) + # + # {"orders": [{"orderId": "589636801329628160"}]} + # + ret = [] + responseOrders = self.safe_list(response, 'orders') + for i in range(0, len(responseOrders)): + responseOrder = responseOrders[i] + ret.append(self.safe_order({ + 'info': responseOrder, + 'id': self.safe_string(responseOrder, 'orderId'), + 'status': 'canceled', + })) + return ret + + async def cancel_all_orders(self, symbol: Str = None, params={}): + """ + + https://api-doc.hibachi.xyz/#8ed24695-016e-49b2-a72d-7511ca921fee + + cancel all open orders in a market + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an list of `order structures ` + """ + await self.load_markets() + nonce = self.nonce() + nonce16 = self.int_to_base16(nonce) + noncePadded = nonce16.rjust(16, '0') + message = self.base16_to_binary(noncePadded) + signature = self.sign_message(message, self.privateKey) + request: dict = { + 'accountId': self.get_account_id(), + 'nonce': nonce, + 'signature': signature, + } + if symbol is not None: + market = self.market(symbol) + request['contractId'] = self.safe_integer(market, 'numericId') + response = await self.privateDeleteTradeOrders(self.extend(request, params)) + # At self time the response body is empty. A 200 response means the cancel request is accepted and sent to process + # + # {} + # + return [ + self.safe_order({ + 'info': response, + }), + ] + + def encode_withdraw_message(self, amount: float, maxFees: float, address: str): + # Converting them to internal representation: + # - Quantity: Internal = External * (10^6) + # - maxFees: Internal = External * (10^6) + # We only have USDT currency time + USDTAssetId = 1 + USDTFactor = '1000000' + amountStr = self.number_to_string(amount) + maxFeesStr = self.number_to_string(maxFees) + one = '1' + quantityInternal = Precise.string_div(Precise.string_mul(amountStr, USDTFactor), one, 0) + maxFeesInternal = Precise.string_div(Precise.string_mul(maxFeesStr, USDTFactor), one, 0) + # Encoding + usdtAsset16 = self.int_to_base16(USDTAssetId) + usdtAssetPadded = usdtAsset16.rjust(8, '0') + encodedAssetId = self.base16_to_binary(usdtAssetPadded) + quantity16 = self.int_to_base16(self.parse_to_int(quantityInternal)) + quantityPadded = quantity16.rjust(16, '0') + encodedQuantity = self.base16_to_binary(quantityPadded) + maxFees16 = self.int_to_base16(self.parse_to_int(maxFeesInternal)) + maxFeesPadded = maxFees16.rjust(16, '0') + encodedMaxFees = self.base16_to_binary(maxFeesPadded) + encodedAddress = self.base16_to_binary(address) + message = self.binary_concat(encodedAssetId, encodedQuantity, encodedMaxFees, encodedAddress) + return message + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://api-doc.hibachi.xyz/#6421625d-3e45-45fa-be9b-d2a0e780c090 + + :param str code: unified currency code, only support USDT + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + withdrawAddress = address[-40:] + # Get the withdraw fees + exchangeInfo = await self.publicGetMarketExchangeInfo(params) + # { + # "feeConfig": { + # "depositFees": "0.004518", + # "tradeMakerFeeRate": "0.00000000", + # "tradeTakerFeeRate": "0.00020000", + # "transferFeeRate": "0.00010000", + # "withdrawalFees": "0.012050" + # }, + # } + feeConfig = self.safe_dict(exchangeInfo, 'feeConfig') + maxFees = self.safe_number(feeConfig, 'withdrawalFees') + # Generate the signature + message = self.encode_withdraw_message(amount, maxFees, withdrawAddress) + signature = self.sign_message(message, self.privateKey) + request = { + 'accountId': self.get_account_id(), + 'coin': 'USDT', + 'network': 'ARBITRUM', + 'withdrawAddress': withdrawAddress, + 'selfWithdrawal': False, + 'quantity': self.number_to_string(amount), + 'maxFees': self.number_to_string(maxFees), + 'signature': signature, + } + await self.privatePostCapitalWithdraw(self.extend(request, params)) + # At self time the response body is empty. A 200 response means the withdraw request is accepted and sent to process + # + # {} + # + return { + 'info': None, + 'id': None, + 'txid': None, + 'timestamp': self.milliseconds(), + 'datetime': None, + 'address': None, + 'addressFrom': None, + 'addressTo': withdrawAddress, + 'tag': None, + 'tagFrom': None, + 'tagTo': None, + 'type': 'withdrawal', + 'amount': amount, + 'currency': code, + 'status': 'pending', + 'fee': {'currency': 'USDT', 'cost': maxFees}, + 'network': 'ARBITRUM', + 'updated': None, + 'comment': None, + 'internal': None, + } + + def nonce(self): + return self.milliseconds() + + def sign_message(self, message, privateKey): + if len(privateKey) == 44: + # For Exchange Managed account, the key length is 44 and we use HMAC to sign the message + return self.hmac(message, self.encode(privateKey), hashlib.sha256, 'hex') + else: + # For Trustless account, the key length is 66 including '0x' and we use ECDSA to sign the message + hash = self.hash(message, 'sha256', 'hex') + signature = self.ecdsa(hash[-64:], privateKey[-64:], 'secp256k1', None) + r = signature['r'] + s = signature['s'] + v = self.int_to_base16(signature['v']) + return r.rjust(64, '0') + s.rjust(64, '0') + v.rjust(2, '0') + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches the state of the open orders on the orderbook + + https://api-doc.hibachi.xyz/#4abb30c4-e5c7-4b0f-9ade-790111dbfa47 + + :param str symbol: unified symbol of the market + :param int [limit]: currently unused + :param dict [params]: extra parameters to be passed -- see documentation link above + :returns dict: A dictionary containg `orderbook information ` + """ + await self.load_markets() + market: Market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = await self.publicGetMarketDataOrderbook(self.extend(request, params)) + formattedResponse = {} + formattedResponse['ask'] = self.safe_list(self.safe_dict(response, 'ask'), 'levels') + formattedResponse['bid'] = self.safe_list(self.safe_dict(response, 'bid'), 'levels') + # { + # "ask": { + # "endPrice": "3512.63", + # "levels": [ + # { + # "price": "3511.93", + # "quantity": "0.284772482" + # }, + # { + # "price": "3512.28", + # "quantity": "0.569544964" + # }, + # { + # "price": "3512.63", + # "quantity": "0.854317446" + # } + # ], + # "startPrice": "3511.93" + # }, + # "bid": { + # "endPrice": "3510.87", + # "levels": [ + # { + # "price": "3515.39", + # "quantity": "2.345153070" + # }, + # { + # "price": "3511.22", + # "quantity": "0.284772482" + # }, + # { + # "price": "3510.87", + # "quantity": "0.569544964" + # } + # ], + # "startPrice": "3515.39" + # } + # } + return self.parse_order_book(formattedResponse, symbol, self.milliseconds(), 'bid', 'ask', 'price', 'quantity') + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + + https://api-doc.hibachi.xyz/#0adbf143-189f-40e0-afdc-88af4cba3c79 + + fetch all trades made by the user + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market: Market = None + if symbol is not None: + market = self.market(symbol) + request = {'accountId': self.get_account_id()} + response = await self.privateGetTradeAccountTrades(self.extend(request, params)) + # + # { + # "trades": [ + # { + # "askAccountId": 221, + # "askOrderId": 589168494921909200, + # "bidAccountId": 132, + # "bidOrderId": 589168494829895700, + # "fee": "0.000477", + # "id": 199511136, + # "orderType": "MARKET", + # "price": "119257.90000", + # "quantity": "0.0000200000", + # "realizedPnl": "-0.000352", + # "side": "Sell", + # "symbol": "BTC/USDT-P", + # "timestamp": 1752543391 + # } + # ] + # } + # + trades = self.safe_list(response, 'trades') + return self.parse_trades(trades, market, since, limit, params) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # [ + # { + # "close": "3704.751036", + # "high": "3716.530378", + # "interval": "1h", + # "low": "3699.627883", + # "open": "3716.406894", + # "timestamp": 1712628000, + # "volumeNotional": "1637355.846362" + # } + # ] + # + return [ + self.safe_integer_product(ohlcv, 'timestamp', 1000), + self.safe_number(ohlcv, 'open'), + self.safe_number(ohlcv, 'high'), + self.safe_number(ohlcv, 'low'), + self.safe_number(ohlcv, 'close'), + self.safe_number(ohlcv, 'volumeNotional'), + ] + + async def fetch_open_orders(self, symbol: str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches all current open orders + + https://api-doc.hibachi.xyz/#3243f8a0-086c-44c5-ab8a-71bbb7bab403 + + :param str [symbol]: unified market symbol to filter by + :param int [since]: milisecond timestamp of the earliest order + :param int [limit]: the maximum number of open orders to return + :param dict [params]: extra parameters + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + request = { + 'accountId': self.get_account_id(), + } + response = await self.privateGetTradeOrders(self.extend(request, params)) + # [ + # { + # "accountId": 12452, + # "availableQuantity": "0.0000230769", + # "contractId": 2, + # "creationTime": 1752684501, + # "orderId": "589205486123876352", + # "orderType": "LIMIT", + # "price": "130000.00000", + # "side": "ASK", + # "status": "PLACED", + # "symbol": "BTC/USDT-P", + # "totalQuantity": "0.0000230769" + # }, + # { + # "accountId": 12452, + # "availableQuantity": "1.234000000", + # "contractId": 1, + # "creationTime": 1752240682, + # "orderId": "589089141754429441", + # "orderType": "LIMIT", + # "price": "1.234000", + # "side": "BID", + # "status": "PLACED", + # "symbol": "ETH/USDT-P", + # "totalQuantity": "1.234000000" + # } + # ] + return self.parse_orders(response, market, since, limit) + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + + https://api-doc.hibachi.xyz/#4f0eacec-c61e-4d51-afb3-23c51c2c6bac + + fetches historical candlestick data containing the close, high, low, open prices, interval and the volumeNotional + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest candle to fetch + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + timeframe = self.safe_string(self.timeframes, timeframe, timeframe) + request: dict = { + 'symbol': market['id'], + 'interval': timeframe, + } + if since is not None: + request['fromMs'] = since + until: Int = None + until, params = self.handle_option_and_params(params, 'fetchOHLCV', 'until') + if until is not None: + request['toMs'] = until + response = await self.publicGetMarketDataKlines(self.extend(request, params)) + # + # [ + # { + # "close": "3704.751036", + # "high": "3716.530378", + # "interval": "1h", + # "low": "3699.627883", + # "open": "3716.406894", + # "timestamp": 1712628000, + # "volumeNotional": "1637355.846362" + # } + # ] + # + klines = self.safe_list(response, 'klines', []) + return self.parse_ohlcvs(klines, market, timeframe, since, limit) + + async def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://api-doc.hibachi.xyz/#69aafedb-8274-4e21-bbaf-91dace8b8f31 + + :param str[] [symbols]: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + request: dict = { + 'accountId': self.get_account_id(), + } + response = await self.privateGetTradeAccountInfo(self.extend(request, params)) + # + # { + # "assets": [ + # { + # "quantity": "14.130626", + # "symbol": "USDT" + # } + # ], + # "balance": "14.186087", + # "maximalWithdraw": "4.152340", + # "numFreeTransfersRemaining": 96, + # "positions": [ + # { + # "direction": "Short", + # "entryNotional": "10.302213", + # "notionalValue": "10.225008", + # "quantity": "0.004310550", + # "symbol": "ETH/USDT-P", + # "unrealizedFundingPnl": "0.000000", + # "unrealizedTradingPnl": "0.077204" + # }, + # { + # "direction": "Short", + # "entryNotional": "2.000016", + # "notionalValue": "1.999390", + # "quantity": "0.0000328410", + # "symbol": "BTC/USDT-P", + # "unrealizedFundingPnl": "0.000000", + # "unrealizedTradingPnl": "0.000625" + # }, + # { + # "direction": "Short", + # "entryNotional": "2.000015", + # "notionalValue": "2.022384", + # "quantity": "0.01470600", + # "symbol": "SOL/USDT-P", + # "unrealizedFundingPnl": "0.000000", + # "unrealizedTradingPnl": "-0.022369" + # } + # ], + # } + # + data = self.safe_list(response, 'positions', []) + return self.parse_positions(data, symbols) + + def parse_position(self, position: dict, market: Market = None): + # + # { + # "direction": "Short", + # "entryNotional": "10.302213", + # "notionalValue": "10.225008", + # "quantity": "0.004310550", + # "symbol": "ETH/USDT-P", + # "unrealizedFundingPnl": "0.000000", + # "unrealizedTradingPnl": "0.077204" + # } + # + marketId = self.safe_string(position, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + side = self.safe_string_lower(position, 'direction') + quantity = self.safe_string(position, 'quantity') + unrealizedFunding = self.safe_string(position, 'unrealizedFundingPnl', '0') + unrealizedTrading = self.safe_string(position, 'unrealizedTradingPnl', '0') + unrealizedPnl = Precise.string_add(unrealizedFunding, unrealizedTrading) + return self.safe_position({ + 'info': position, + 'id': None, + 'symbol': symbol, + 'entryPrice': self.safe_string(position, 'average_entry_price'), + 'markPrice': None, + 'notional': self.safe_string(position, 'notionalValue'), + 'collateral': None, + 'unrealizedPnl': unrealizedPnl, + 'side': side, + 'contracts': self.parse_number(quantity), + 'contractSize': None, + 'timestamp': None, + 'datetime': None, + 'hedged': None, + 'maintenanceMargin': None, + 'maintenanceMarginPercentage': None, + 'initialMargin': None, + 'initialMarginPercentage': None, + 'leverage': None, + 'liquidationPrice': None, + 'marginRatio': None, + 'marginMode': None, + 'percentage': None, + }) + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + endpoint = '/' + self.implode_params(path, params) + url = self.urls['api'][api] + endpoint + headers = {'Hibachi-Client': 'HibachiCCXT/unversioned'} + if method == 'GET': + request = self.omit(params, self.extract_params(path)) + query = self.urlencode(request) + if len(query) != 0: + url += '?' + query + if method == 'POST' or method == 'PUT' or method == 'DELETE': + headers['Content-Type'] = 'application/json' + body = self.json(params) + if api == 'private': + self.check_required_credentials() + headers['Authorization'] = self.apiKey + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None # fallback to default error handler + if 'status' in response: + # + # {"errorCode":4,"message":"Invalid input: Invalid quantity: 0","status":"failed"} + # + status = self.safe_string(response, 'status') + if status == 'failed': + code = self.safe_string(response, 'errorCode') + feedback = self.id + ' ' + body + self.throw_broadly_matched_exception(self.exceptions['broad'], body, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], code, feedback) + message = self.safe_string(response, 'message') + self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) + raise ExchangeError(feedback) + return None + + def parse_transaction_type(self, type): + types: dict = { + 'deposit': 'transaction', + 'withdrawal': 'transaction', + 'transfer-in': 'transfer', + 'transfer-out': 'transfer', + } + return self.safe_string(types, type, type) + + def parse_transaction_status(self, status): + statuses: dict = { + 'pending': 'pending', + 'claimable': 'pending', + 'completed': 'ok', + 'failed': 'canceled', + } + return self.safe_string(statuses, status, status) + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + transactionType = self.safe_string(item, 'transactionType') + timestamp = None + type = None + direction = None + amount = None + fee = None + referenceId = None + referenceAccount = None + status = None + if transactionType is None: + # response from TradeAccountTradingHistory + timestamp = self.safe_integer_product(item, 'timestamp', 1000) + type = 'trade' + amountStr = self.safe_string(item, 'realizedPnl') + if Precise.string_lt(amountStr, '0'): + direction = 'out' + amountStr = Precise.string_neg(amountStr) + else: + direction = 'in' + amount = self.parse_number(amountStr) + fee = {'currency': 'USDT', 'cost': self.safe_number(item, 'fee')} + status = 'ok' + else: + # response from CapitalHistory + timestamp = self.safe_integer_product(item, 'timestampSec', 1000) + amount = self.safe_number(item, 'quantity') + direction = 'in' if (transactionType == 'deposit' or transactionType == 'transfer-in') else 'out' + type = self.parse_transaction_type(transactionType) + status = self.parse_transaction_status(self.safe_string(item, 'status')) + if transactionType == 'transfer-in': + referenceAccount = self.safe_string(item, 'srcAccountId') + elif transactionType == 'transfer-out': + referenceAccount = self.safe_string(item, 'receivingAccountId') + referenceId = self.safe_string(item, 'transactionHash') + return self.safe_ledger_entry({ + 'id': self.safe_string(item, 'id'), + 'currency': self.currency('USDT'), + 'account': self.number_to_string(self.accountId), + 'referenceAccount': referenceAccount, + 'referenceId': referenceId, + 'status': status, + 'amount': amount, + 'before': None, + 'after': None, + 'fee': fee, + 'direction': direction, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'type': type, + 'info': item, + }, currency) + + async def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + + https://api-doc.hibachi.xyz/#35125e3f-d154-4bfd-8276-a48bb1c62020 + + :param str [code]: unified currency code, default is None + :param int [since]: timestamp in ms of the earliest ledger entry, default is None + :param int [limit]: max number of ledger entries to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ledger structure ` + """ + await self.load_markets() + currency = self.currency('USDT') + request = {'accountId': self.get_account_id()} + rawPromises = [ + self.privateGetCapitalHistory(self.extend(request, params)), + self.privateGetTradeAccountTradingHistory(self.extend(request, params)), + ] + promises = await asyncio.gather(*rawPromises) + responseCapitalHistory = promises[0] + # + # { + # "transactions": [ + # { + # "assetId": 1, + # "blockNumber": 358396669, + # "chain": "Arbitrum", + # "etaTsSec": null, + # "id": 358396669, + # "quantity": "0.999500", + # "status": "pending", + # "timestampSec": 1752692872, + # "token": "USDT", + # "transactionHash": "0x408e48881e0ba77d8638e3fe57bc06bdec513ddaa8b672e0aefa7e22e2f18b5e", + # "transactionType": "deposit" + # }, + # { + # "assetId": 1, + # "etaTsSec": null, + # "id": 13116, + # "instantWithdrawalChain": null, + # "instantWithdrawalToken": null, + # "isInstantWithdrawal": False, + # "quantity": "0.040000", + # "status": "completed", + # "timestampSec": 1752542708, + # "transactionHash": "0xe89cf90b2408d1a273dc9427654145def102d9449e5e2cfc10690ccffc3d7e28", + # "transactionType": "withdrawal", + # "withdrawalAddress": "0x23625d5fc6a6e32638d908eb4c3a3415e5121f76" + # }, + # { + # "assetId": 1, + # "id": 167, + # "quantity": "10.000000", + # "srcAccountId": 175, + # "srcAddress": "0xc2f77ce029438a3fdfe68ddee25991a9fb985a86", + # "status": "completed", + # "timestampSec": 1732224729, + # "transactionType": "transfer-in" + # }, + # { + # "assetId": 1, + # "id": 170, + # "quantity": "10.000000", + # "receivingAccountId": 175, + # "receivingAddress": "0xc2f77ce029438a3fdfe68ddee25991a9fb985a86", + # "status": "completed", + # "timestampSec": 1732225631, + # "transactionType": "transfer-out" + # }, + # ] + # } + # + rowsCapitalHistory = self.safe_list(responseCapitalHistory, 'transactions') + responseTradingHistory = promises[1] + # + # { + # "tradingHistory": [ + # { + # "eventType": "MARKET", + # "fee": "0.000008", + # "priceOrFundingRate": "119687.82481", + # "quantity": "0.0000003727", + # "realizedPnl": "0.004634", + # "side": "Sell", + # "symbol": "BTC/USDT-P", + # "timestamp": 1752522571 + # }, + # { + # "eventType": "FundingEvent", + # "fee": "0", + # "priceOrFundingRate": "0.000203", + # "quantity": "0.0000003727", + # "realizedPnl": "-0.000009067899008751979", + # "side": "Long", + # "symbol": "BTC/USDT-P", + # "timestamp": 1752508800 + # }, + # ] + # } + # + rowsTradingHistory = self.safe_list(responseTradingHistory, 'tradingHistory') + rows = self.array_concat(rowsCapitalHistory, rowsTradingHistory) + return self.parse_ledger(rows, currency, since, limit, params) + + async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch deposit address for given currency and chain. currently, we have a single EVM address across multiple EVM chains. Note: This method is currently only supported for trustless accounts + :param str code: unified currency code + :param dict [params]: extra parameters for API + :param str [params.publicKey]: your public key, you can get it from UI after creating API key + :returns dict: an `address structure ` + """ + request = { + 'publicKey': self.safe_string(params, 'publicKey'), + 'accountId': self.get_account_id(), + } + response = await self.privateGetCapitalDepositInfo(self.extend(request, params)) + # { + # "depositAddressEvm": "0x0b95d90b9345dadf1460bd38b9f4bb0d2f4ed788" + # } + return { + 'info': response, + 'currency': 'USDT', + 'network': 'ARBITRUM', + 'address': self.safe_string(response, 'depositAddressEvm'), + 'tag': None, + } + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + timestamp = self.safe_integer_product(transaction, 'timestampSec', 1000) + address = self.safe_string(transaction, 'withdrawalAddress') + transactionType = self.safe_string(transaction, 'transactionType') + if transactionType != 'deposit' and transactionType != 'withdrawal': + transactionType = self.parse_transaction_type(transactionType) + return { + 'info': transaction, + 'id': self.safe_string(transaction, 'id'), + 'txid': self.safe_string(transaction, 'transactionHash'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': 'ARBITRUM', # Currently the exchange only exists on Arbitrum, + 'address': address, + 'addressTo': address, + 'addressFrom': None, + 'tag': None, + 'tagTo': None, + 'tagFrom': None, + 'type': transactionType, + 'amount': self.safe_number(transaction, 'quantity'), + 'currency': 'USDT', + 'status': self.parse_transaction_status(self.safe_string(transaction, 'status')), + 'updated': None, + 'internal': None, + 'comment': None, + 'fee': None, + } + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch deposits made to account + + https://api-doc.hibachi.xyz/#35125e3f-d154-4bfd-8276-a48bb1c62020 + + :param str [code]: unified currency code + :param int [since]: filter by earliest timestamp(ms) + :param int [limit]: maximum number of deposits to be returned + :param dict [params]: extra parameters to be passed to API + :returns dict[]: a list of `transaction structures ` + """ + currency = self.safe_currency(code) + request = { + 'accountId': self.get_account_id(), + } + response = await self.privateGetCapitalHistory(self.extend(request, params)) + # { + # "transactions": [ + # { + # "assetId": 1, + # "blockNumber": 0, + # "chain": null, + # "etaTsSec": 1752758789, + # "id": 42688, + # "quantity": "6.130000", + # "status": "completed", + # "timestampSec": 1752758788, + # "token": null, + # "transactionHash": "0x8dcd7bd1155b5624fb5e38a1365888f712ec633a57434340e05080c70b0e3bba", + # "transactionType": "deposit" + # }, + # { + # "assetId": 1, + # "etaTsSec": null, + # "id": 12993, + # "instantWithdrawalChain": null, + # "instantWithdrawalToken": null, + # "isInstantWithdrawal": False, + # "quantity": "0.111930", + # "status": "completed", + # "timestampSec": 1752387891, + # "transactionHash": "0x32ab5fe5b90f6d753bab83523ebc8465eb9daef54580e13cb9ff031d400c5620", + # "transactionType": "withdrawal", + # "withdrawalAddress": "0x43f15ef2ef2ab5e61e987ee3d652a5872aea8a6c" + # }, + # ] + # } + transactions = self.safe_list(response, 'transactions') + deposits = [] + for i in range(0, len(transactions)): + transaction = transactions[i] + if self.safe_string(transaction, 'transactionType') == 'deposit': + deposits.append(transaction) + return self.parse_transactions(deposits, currency, since, limit, params) + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch withdrawals made from account + + https://api-doc.hibachi.xyz/#35125e3f-d154-4bfd-8276-a48bb1c62020 + + :param str [code]: unified currency code + :param int [since]: filter by earliest timestamp(ms) + :param int [limit]: maximum number of deposits to be returned + :param dict [params]: extra parameters to be passed to API + :returns dict[]: a list of `transaction structures ` + """ + currency = self.safe_currency(code) + request = { + 'accountId': self.get_account_id(), + } + response = await self.privateGetCapitalHistory(self.extend(request, params)) + # { + # "transactions": [ + # { + # "assetId": 1, + # "blockNumber": 0, + # "chain": null, + # "etaTsSec": 1752758789, + # "id": 42688, + # "quantity": "6.130000", + # "status": "completed", + # "timestampSec": 1752758788, + # "token": null, + # "transactionHash": "0x8dcd7bd1155b5624fb5e38a1365888f712ec633a57434340e05080c70b0e3bba", + # "transactionType": "deposit" + # }, + # { + # "assetId": 1, + # "etaTsSec": null, + # "id": 12993, + # "instantWithdrawalChain": null, + # "instantWithdrawalToken": null, + # "isInstantWithdrawal": False, + # "quantity": "0.111930", + # "status": "completed", + # "timestampSec": 1752387891, + # "transactionHash": "0x32ab5fe5b90f6d753bab83523ebc8465eb9daef54580e13cb9ff031d400c5620", + # "transactionType": "withdrawal", + # "withdrawalAddress": "0x43f15ef2ef2ab5e61e987ee3d652a5872aea8a6c" + # }, + # ] + # } + transactions = self.safe_list(response, 'transactions') + withdrawals = [] + for i in range(0, len(transactions)): + transaction = transactions[i] + if self.safe_string(transaction, 'transactionType') == 'withdrawal': + withdrawals.append(transaction) + return self.parse_transactions(withdrawals, currency, since, limit, params) + + async def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + http://api-doc.hibachi.xyz/#b5c6a3bc-243d-4d35-b6d4-a74c92495434 + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = await self.publicGetExchangeUtcTimestamp(params) + # + # {"timestampMs":1754077574040} + # + return self.safe_integer(response, 'timestampMs') + + async def fetch_open_interest(self, symbol: str, params={}): + """ + retrieves the open interest of a contract trading pair + + https://api-doc.hibachi.xyz/#bc34e8ae-e094-4802-8d56-3efe3a7bad49 + + :param str symbol: unified CCXT market symbol + :param dict [params]: exchange specific parameters + :returns dict} an open interest structure{@link https://docs.ccxt.com/#/?id=open-interest-structure: + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = await self.publicGetMarketDataOpenInterest(self.extend(request, params)) + # + # {"totalQuantity" : "2.3299770166"} + # + timestamp = self.milliseconds() + return self.safe_open_interest({ + 'symbol': symbol, + 'openInterestAmount': self.safe_string(response, 'totalQuantity'), + 'openInterestValue': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'info': response, + }, market) + + async def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate + + https://api-doc.hibachi.xyz/#bca696ca-b9b2-4072-8864-5d6b8c09807e + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = await self.publicGetMarketDataPrices(self.extend(request, params)) + # + # { + # "askPrice": "3514.650296", + # "bidPrice": "3513.596112", + # "fundingRateEstimation": { + # "estimatedFundingRate": "0.000001", + # "nextFundingTimestamp": 1712707200 + # }, + # "markPrice": "3514.288858", + # "spotPrice": "3514.715000", + # "symbol": "ETH/USDT-P", + # "tradePrice": "2372.746570" + # } + # + funding = self.safe_dict(response, 'fundingRateEstimation', {}) + timestamp = self.milliseconds() + nextFundingTimestamp = self.safe_integer_product(funding, 'nextFundingTimestamp', 1000) + return { + 'info': funding, + 'symbol': market['symbol'], + 'markPrice': None, + 'indexPrice': None, + 'interestRate': self.parse_number('0'), + 'estimatedSettlePrice': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'fundingRate': self.safe_number(funding, 'estimatedFundingRate'), + 'fundingTimestamp': nextFundingTimestamp, + 'fundingDatetime': self.iso8601(nextFundingTimestamp), + 'nextFundingRate': None, + 'nextFundingTimestamp': None, + 'nextFundingDatetime': None, + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': '8h', + } + + async def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical funding rate prices + + https://api-doc.hibachi.xyz/#4abb30c4-e5c7-4b0f-9ade-790111dbfa47 + + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: timestamp in ms of the earliest funding rate to fetch + :param int [limit]: the maximum amount of `funding rate structures ` to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `funding rate structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = await self.publicGetMarketDataFundingRates(self.extend(request, params)) + # + # { + # "data": [ + # { + # "contractId": 2, + # "fundingTimestamp": 1753488000, + # "fundingRate": "0.000137", + # "indexPrice": "117623.65010" + # } + # ] + # } + # + data = self.safe_list(response, 'data') + rates = [] + for i in range(0, len(data)): + entry = data[i] + timestamp = self.safe_integer_product(entry, 'fundingTimestamp', 1000) + rates.append({ + 'info': entry, + 'symbol': symbol, + 'fundingRate': self.safe_number(entry, 'fundingRate'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + sorted = self.sort_by(rates, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, symbol, since, limit) diff --git a/ccxt/async_support/hitbtc.py b/ccxt/async_support/hitbtc.py new file mode 100644 index 0000000..b98c4d0 --- /dev/null +++ b/ccxt/async_support/hitbtc.py @@ -0,0 +1,3651 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.hitbtc import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currencies, Currency, DepositAddress, Int, Leverage, MarginMode, MarginModes, MarginModification, Market, Num, Order, OrderBook, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, FundingRates, OrderBooks, Trade, TradingFeeInterface, TradingFees, Transaction, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import AccountSuspended +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import NotSupported +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.errors import OnMaintenance +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class hitbtc(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(hitbtc, self).describe(), { + 'id': 'hitbtc', + 'name': 'HitBTC', + 'countries': ['HK'], + # 300 requests per second => 1000ms / 300 = 3.333(Trading: placing, replacing, deleting) + # 30 requests per second =>( 1000ms / rateLimit ) / 30 = cost = 10(Market Data and other Public Requests) + # 20 requests per second =>( 1000ms / rateLimit ) / 20 = cost = 15(All Other) + 'rateLimit': 3.333, # TODO: optimize https://api.hitbtc.com/#rate-limiting + 'version': '3', + 'has': { + 'CORS': False, + 'spot': True, + 'margin': True, + 'swap': True, + 'future': False, + 'option': False, + 'addMargin': True, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'closePosition': False, + 'createDepositAddress': True, + 'createOrder': True, + 'createPostOnlyOrder': True, + 'createReduceOnlyOrder': True, + 'createStopLimitOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'editOrder': True, + 'fetchAccounts': False, + 'fetchBalance': True, + 'fetchBorrowRateHistories': None, + 'fetchBorrowRateHistory': None, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchDepositsWithdrawals': True, + 'fetchDepositWithdrawFee': 'emulated', + 'fetchDepositWithdrawFees': True, + 'fetchFundingHistory': None, + 'fetchFundingRate': True, + 'fetchFundingRateHistory': True, + 'fetchFundingRates': True, + 'fetchGreeks': False, + 'fetchIndexOHLCV': True, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchLeverage': True, + 'fetchLeverageTiers': None, + 'fetchLiquidations': False, + 'fetchMarginMode': 'emulated', + 'fetchMarginModes': True, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': True, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': True, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': True, + 'fetchOpenOrder': True, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrderBooks': True, + 'fetchOrders': False, + 'fetchOrderTrades': True, + 'fetchPosition': True, + 'fetchPositions': True, + 'fetchPremiumIndexOHLCV': True, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTrades': True, + 'fetchTradingFee': True, + 'fetchTradingFees': True, + 'fetchTransactions': 'emulated', + 'fetchVolatilityHistory': False, + 'fetchWithdrawals': True, + 'reduceMargin': True, + 'sandbox': True, + 'setLeverage': True, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'transfer': True, + 'withdraw': True, + }, + 'precisionMode': TICK_SIZE, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/27766555-8eaec20e-5edc-11e7-9c5b-6dc69fc42f5e.jpg', + 'test': { + 'public': 'https://api.demo.hitbtc.com/api/3', + 'private': 'https://api.demo.hitbtc.com/api/3', + }, + 'api': { + 'public': 'https://api.hitbtc.com/api/3', + 'private': 'https://api.hitbtc.com/api/3', + }, + 'www': 'https://hitbtc.com', + 'referral': 'https://hitbtc.com/?ref_id=5a5d39a65d466', + 'doc': [ + 'https://api.hitbtc.com', + 'https://github.com/hitbtc-com/hitbtc-api/blob/master/APIv2.md', + ], + 'fees': [ + 'https://hitbtc.com/fees-and-limits', + 'https://support.hitbtc.com/hc/en-us/articles/115005148605-Fees-and-limits', + ], + }, + 'api': { + 'public': { + 'get': { + 'public/currency': 10, + 'public/currency/{currency}': 10, + 'public/symbol': 10, + 'public/symbol/{symbol}': 10, + 'public/ticker': 10, + 'public/ticker/{symbol}': 10, + 'public/price/rate': 10, + 'public/price/history': 10, + 'public/price/ticker': 10, + 'public/price/ticker/{symbol}': 10, + 'public/trades': 10, + 'public/trades/{symbol}': 10, + 'public/orderbook': 10, + 'public/orderbook/{symbol}': 10, + 'public/candles': 10, + 'public/candles/{symbol}': 10, + 'public/converted/candles': 10, + 'public/converted/candles/{symbol}': 10, + 'public/futures/info': 10, + 'public/futures/info/{symbol}': 10, + 'public/futures/history/funding': 10, + 'public/futures/history/funding/{symbol}': 10, + 'public/futures/candles/index_price': 10, + 'public/futures/candles/index_price/{symbol}': 10, + 'public/futures/candles/mark_price': 10, + 'public/futures/candles/mark_price/{symbol}': 10, + 'public/futures/candles/premium_index': 10, + 'public/futures/candles/premium_index/{symbol}': 10, + 'public/futures/candles/open_interest': 10, + 'public/futures/candles/open_interest/{symbol}': 10, + }, + }, + 'private': { + 'get': { + 'spot/balance': 15, + 'spot/balance/{currency}': 15, + 'spot/order': 1, + 'spot/order/{client_order_id}': 1, + 'spot/fee': 15, + 'spot/fee/{symbol}': 15, + 'spot/history/order': 15, + 'spot/history/trade': 15, + 'margin/account': 1, + 'margin/account/isolated/{symbol}': 1, + 'margin/account/cross/{currency}': 1, + 'margin/order': 1, + 'margin/order/{client_order_id}': 1, + 'margin/config': 15, + 'margin/history/order': 15, + 'margin/history/trade': 15, + 'margin/history/positions': 15, + 'margin/history/clearing': 15, + 'futures/balance': 15, + 'futures/balance/{currency}': 15, + 'futures/account': 1, + 'futures/account/isolated/{symbol}': 1, + 'futures/order': 1, + 'futures/order/{client_order_id}': 1, + 'futures/config': 15, + 'futures/fee': 15, + 'futures/fee/{symbol}': 15, + 'futures/history/order': 15, + 'futures/history/trade': 15, + 'futures/history/positions': 15, + 'futures/history/clearing': 15, + 'wallet/balance': 30, + 'wallet/balance/{currency}': 30, + 'wallet/crypto/address': 30, + 'wallet/crypto/address/recent-deposit': 30, + 'wallet/crypto/address/recent-withdraw': 30, + 'wallet/crypto/address/check-mine': 30, + 'wallet/transactions': 30, + 'wallet/transactions/{tx_id}': 30, + 'wallet/crypto/fee/estimate': 30, + 'wallet/airdrops': 30, + 'wallet/amount-locks': 30, + 'sub-account': 15, + 'sub-account/acl': 15, + 'sub-account/balance/{subAccID}': 15, + 'sub-account/crypto/address/{subAccID}/{currency}': 15, + }, + 'post': { + 'spot/order': 1, + 'spot/order/list': 1, + 'margin/order': 1, + 'margin/order/list': 1, + 'futures/order': 1, + 'futures/order/list': 1, + 'wallet/crypto/address': 30, + 'wallet/crypto/withdraw': 30, + 'wallet/convert': 30, + 'wallet/transfer': 30, + 'wallet/internal/withdraw': 30, + 'wallet/crypto/check-offchain-available': 30, + 'wallet/crypto/fees/estimate': 30, + 'wallet/airdrops/{id}/claim': 30, + 'sub-account/freeze': 15, + 'sub-account/activate': 15, + 'sub-account/transfer': 15, + 'sub-account/acl': 15, + }, + 'patch': { + 'spot/order/{client_order_id}': 1, + 'margin/order/{client_order_id}': 1, + 'futures/order/{client_order_id}': 1, + }, + 'delete': { + 'spot/order': 1, + 'spot/order/{client_order_id}': 1, + 'margin/position': 1, + 'margin/position/isolated/{symbol}': 1, + 'margin/order': 1, + 'margin/order/{client_order_id}': 1, + 'futures/position': 1, + 'futures/position/{margin_mode}/{symbol}': 1, + 'futures/order': 1, + 'futures/order/{client_order_id}': 1, + 'wallet/crypto/withdraw/{id}': 30, + }, + 'put': { + 'margin/account/isolated/{symbol}': 1, + 'futures/account/isolated/{symbol}': 1, + 'wallet/crypto/withdraw/{id}': 30, + }, + }, + }, + 'fees': { + 'trading': { + 'tierBased': True, + 'percentage': True, + 'taker': self.parse_number('0.0009'), + 'maker': self.parse_number('0.0009'), + 'tiers': { + 'maker': [ + [self.parse_number('0'), self.parse_number('0.0009')], + [self.parse_number('10'), self.parse_number('0.0007')], + [self.parse_number('100'), self.parse_number('0.0006')], + [self.parse_number('500'), self.parse_number('0.0005')], + [self.parse_number('1000'), self.parse_number('0.0003')], + [self.parse_number('5000'), self.parse_number('0.0002')], + [self.parse_number('10000'), self.parse_number('0.0001')], + [self.parse_number('20000'), self.parse_number('0')], + [self.parse_number('50000'), self.parse_number('-0.0001')], + [self.parse_number('100000'), self.parse_number('-0.0001')], + ], + 'taker': [ + [self.parse_number('0'), self.parse_number('0.0009')], + [self.parse_number('10'), self.parse_number('0.0008')], + [self.parse_number('100'), self.parse_number('0.0007')], + [self.parse_number('500'), self.parse_number('0.0007')], + [self.parse_number('1000'), self.parse_number('0.0006')], + [self.parse_number('5000'), self.parse_number('0.0006')], + [self.parse_number('10000'), self.parse_number('0.0005')], + [self.parse_number('20000'), self.parse_number('0.0004')], + [self.parse_number('50000'), self.parse_number('0.0003')], + [self.parse_number('100000'), self.parse_number('0.0002')], + ], + }, + }, + }, + 'features': { + 'default': { + 'sandbox': True, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': True, + }, + 'hedged': False, + 'selfTradePrevention': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': False, + 'marketBuyRequiresPrice': False, + 'iceberg': True, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': True, + 'limit': 1000, + 'daysBack': 100000, + 'untilDays': 100000, + 'symbolRequired': False, + 'marketType': True, + }, + 'fetchOrder': { + 'marginMode': True, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + 'marketType': True, + }, + 'fetchOpenOrders': { + 'marginMode': True, + 'limit': 1000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + 'marketType': True, + }, + 'fetchOrders': None, + 'fetchClosedOrders': { + 'marginMode': True, + 'limit': 1000, + 'daysBack': 100000, # todo + 'daysBackCanceled': 1, # todo + 'untilDays': 100000, # todo + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + 'marketType': True, + }, + 'fetchOHLCV': { + 'limit': 1000, + }, + }, + 'spot': { + 'extends': 'default', + }, + 'forDerivatives': { + 'extends': 'default', + 'createOrder': { + 'marginMode': True, + }, + 'fetchOrder': { + 'marginMode': False, + }, + 'fetchMyTrades': { + 'marginMode': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + }, + 'fetchClosedOrders': { + 'marginMode': False, + }, + }, + 'swap': { + 'linear': { + 'extends': 'forDerivatives', + }, + 'inverse': { + 'extends': 'forDerivatives', + }, + }, + 'future': { + 'linear': { + 'extends': 'forDerivatives', + }, + 'inverse': { + 'extends': 'forDerivatives', + }, + }, + }, + 'timeframes': { + '1m': 'M1', + '3m': 'M3', + '5m': 'M5', + '15m': 'M15', + '30m': 'M30', # default + '1h': 'H1', + '4h': 'H4', + '1d': 'D1', + '1w': 'D7', + '1M': '1M', + }, + 'exceptions': { + 'exact': { + '429': RateLimitExceeded, + '500': ExchangeError, + '503': ExchangeNotAvailable, + '504': ExchangeNotAvailable, + '600': PermissionDenied, + '800': ExchangeError, + '1002': AuthenticationError, + '1003': PermissionDenied, + '1004': AuthenticationError, + '1005': AuthenticationError, + '2001': BadSymbol, + '2002': BadRequest, + '2003': BadRequest, + '2010': BadRequest, + '2011': BadRequest, + '2012': BadRequest, + '2020': BadRequest, + '2022': BadRequest, + '2024': InvalidOrder, # Invalid margin mode. + '10001': BadRequest, + '10021': AccountSuspended, + '10022': BadRequest, + '20001': InsufficientFunds, + '20002': OrderNotFound, + '20003': ExchangeError, + '20004': ExchangeError, + '20005': ExchangeError, + '20006': ExchangeError, + '20007': ExchangeError, + '20008': InvalidOrder, + '20009': InvalidOrder, + '20010': OnMaintenance, + '20011': ExchangeError, + '20012': ExchangeError, + '20014': ExchangeError, + '20016': ExchangeError, + '20018': ExchangeError, # Withdrawals are unavailable due to the current configuration. Any of: - internal withdrawals are disabled; - in-chain withdrawals are disabled. + '20031': ExchangeError, + '20032': ExchangeError, + '20033': ExchangeError, + '20034': ExchangeError, + '20040': ExchangeError, + '20041': ExchangeError, + '20042': ExchangeError, + '20043': ExchangeError, + '20044': PermissionDenied, + '20045': InvalidOrder, + '20047': InvalidOrder, # Order placing exceeds the central counterparty balance limit. + '20048': InvalidOrder, # Provided Time-In-Force instruction is invalid or the combination of the instruction and the order type is not allowed. + '20049': InvalidOrder, # Provided order type is invalid. + '20080': ExchangeError, + '21001': ExchangeError, + '21003': AccountSuspended, + '21004': AccountSuspended, + '22004': ExchangeError, # User is not found. + '22008': ExchangeError, # Gateway timeout exceeded. + }, + 'broad': {}, + }, + 'options': { + 'defaultNetwork': 'ERC20', + 'defaultNetworks': { + 'ETH': 'ETH', + 'USDT': 'TRC20', + }, + 'networks': { + # mainnet network ids are in lowercase for BTC & ETH + 'BTC': 'btc', + 'OMNI': 'BTC', + 'ETH': 'eth', + 'ERC20': 'ETH', + 'ETC': 'ETC', + 'BEP20': 'BSC', + 'TRC20': 'TRX', + # '': 'UGT', + 'NEAR': 'NEAR', + # '': 'LWF', + 'DGB': 'DGB', + # '': 'YOYOW', + 'AE': 'AE', + # 'BCHABC': 'BCHABC', + # '': 'BCI', + # 'BYTECOIN': 'bcn', + 'AR': 'AR', + # '': 'HPC', + 'ADA': 'ADA', + # 'BELDEX': 'BDX', + # 'ARDOR': 'ARDR', + # 'NEBLIO': 'NEBL', + # '': 'DIM', + 'CHZ': 'CHZ', + # '': 'BET', + # '': '8BT', + 'ABBC': 'ABBC', + # '': 'ABTC', + # 'ACHAIN': 'ACT', + # '': 'ADK', + # '': 'AEON', + 'ALGO': 'ALGO', + # 'AMBROSUS': 'AMB', + # '': 'APL', + 'APT': 'APT', + # '': 'ARK', + # 'PIRATECHAIN': 'ARRR', + # '': 'ASP', + # '': 'ATB', + 'ATOM': 'ATOM', + 'AVAXC': 'AVAC', + 'AVAXX': 'AVAX', + # '': 'AYA', + # '': 'B2G', + # '': 'B2X', + # '': 'BANANO', + # '': 'BCCF', + 'BSV': 'BCHSV', + 'BEP2': 'BNB', + # 'BOSON': 'BOS', + # '': 'BRL', # brazilian real + # '': 'BST', + # 'BITCOINADDITION': 'BTCADD', + # '': 'BTCP', + # 'SUPERBTC': 'SBTC', + # 'BITCOINVAULT': 'BTCV', + # 'BITCOINGOLD': 'BTG', + # 'BITCOINDIAMOND': 'BCD', + # 'BITCONNECT': 'BCC', + # '': 'BTM', + # 'BITSHARES_OLD': 'BTS', + # '': 'BTX', + # '': 'BWI', + 'CELO': 'CELO', + # '': 'CENNZ', + # '': 'CHX', + 'CKB': 'CKB', + # 'CALLISTO': 'CLO', + # '': 'CLR', + # '': 'CNX', + # '': 'CRS', + # '': 'CSOV', + 'CTXC': 'CTXC', + # '': 'CURE', + # 'CONSTELLATION': 'DAG', + # '': 'DAPS', + 'DASH': 'DASH', + # '': 'DBIX', + 'DCR': 'DCR', + # '': 'DCT', + # '': 'DDR', + # '': 'DNA', + 'DOGE': 'doge', + # 'POLKADOT': 'DOT', + # '': 'NEWDOT', POLKADOT NEW + # '': 'dsh', + # '': 'ECA', + # '': 'ECOIN', + # '': 'EEX', + 'EGLD': 'EGLD', + # '': 'ELE', + # 'ELECTRONEUM': 'Electroneum', + # '': 'ELM', + # '': 'EMC', + 'EOS': 'EOS', + # 'AERGO': 'ERG', + 'ETHW': 'ETHW', + # 'ETHERLITE': 'ETL', + # '': 'ETP', # metaverse etp + # '': 'EUNO', + 'EVER': 'EVER', + # '': 'EXP', + # '': 'fcn', + 'FET': 'FET', + 'FIL': 'FIL', + # '': 'FIRO', + 'FLOW': 'FLOW', + # '': 'G999', + # '': 'GAME', + # '': 'GASP', + # '': 'GBX', + # '': 'GHOST', + # '': 'GLEEC', + 'GLMR': 'GLMR', + # '': 'GMD', + # '': 'GRAPH', + 'GRIN': 'GRIN', + 'HBAR': 'HBAR', + # '': 'HDG', + 'HIVE': 'HIVE', + # 'HARBOR': 'HRB', + # '': 'HSR', + # '': 'HTML', + 'HYDRA': 'HYDRA', + 'ICP': 'ICP', + 'ICX': 'ICX', + # '': 'IML', + 'IOST': 'IOST', + 'IOTA': 'IOTA', + 'IOTX': 'IOTX', + # '': 'IQ', + 'KAVA': 'KAVA', + 'KLAY': 'KIM', + 'KOMODO': 'KMD', + # '': 'KRM', + 'KSM': 'KSM', + # '': 'LAVA', + # 'LITECOINCASH': 'LCC', + 'LSK': 'LSK', + # '': 'LOC', + 'LTC': 'ltc', + # '': 'LTNM', + # 'TERRACLASSIC': 'LUNA', + # 'TERRA': 'LUNANEW', + # '': 'MAN', + # '': 'MESH', + 'MINA': 'MINA', + # '': 'MNX', + # 'MOBILECOIN': 'MOB', + 'MOVR': 'MOVR', + # '': 'MPK', + # '': 'MRV', + 'NANO': 'NANO', + # '': 'NAV', + 'NEO': 'NEO', + # 'NIMIQ': 'NIM', + # '': 'NJBC', + # '': 'NKN', + # '': 'NLC2', + # '': 'NOF', + # 'ENERGI': 'NRG', + # '': 'nxt', + # '': 'ODN', + 'ONE': 'ONE', + # 'ONTOLOGYGAS': 'ONG', + 'ONT': 'ONT', + 'OPTIMISM': 'OP', + # '': 'PAD', + # '': 'PART', + # '': 'PBKX', + # '': 'PLC', + 'PLCU': 'PLCU', + # '': 'PLI', + # '': 'POA', + 'MATIC': 'POLYGON', + # '': 'PPC', + # '': 'PQT', + # '': 'PROC', + # 'PASTEL': 'PSL', + # '': 'qcn', + 'QTUM': 'QTUM', + # '': 'RCOIN', + 'REI': 'REI', + # '': 'RIF', + # '': 'ROOTS', + 'OASIS': 'ROSE', + # '': 'RPX', + # '': 'RUB', + 'RVN': 'RVN', + # '': 'SBD', + 'SC': 'SC', + 'SCRT': 'SCRT', + # '': 'SLX', + # 'SMARTMESH': 'SMART', + # '': 'SMT', + # '': 'SNM', + 'SOL': 'SOL', + # '': 'SRX', + # '': 'STAK', + 'STEEM': 'STEEM', + # 'STRATIS': 'STRAT', + # '': 'TCN', + # '': 'TENT', + 'THETA': 'Theta', + # '': 'TIV', + # '': 'TNC', + # 'TON': 'TONCOIN', + 'TRUE': 'TRUE', + # '': 'TRY', # turkish lira + # '': 'UNO', + # '': 'USNOTA', + # '': 'VEO', + 'VET': 'VET', + # '': 'VITAE', + # 'VELAS': 'VLX', + 'VSYS': 'VSYS', + # '': 'VTC', + 'WAVES': 'WAVES', + 'WAX': 'WAX', + # '': 'WEALTH', + # 'WALTONCHAIN': 'WTC', + # '': 'WTT', + 'XCH': 'XCH', + # '': 'XDC', # xinfin? + # '': 'xdn', + # '': 'XDNCO', + # '': 'XDNICCO', + 'XEC': 'XEC', + 'NEM': 'XEM', + # 'HAVEN': 'XHV', + # '': 'XLC', + 'XLM': 'XLM', + # '': 'XMO', + 'XMR': 'xmr', + # 'MONEROCLASSIC': 'XMC', + # '': 'XNS', + # '': 'XPRM', + # '': 'XRC', + 'XRD': 'XRD', + 'XRP': 'XRP', + 'XTZ': 'XTZ', + 'XVG': 'XVG', + 'XYM': 'XYM', + 'ZEC': 'ZEC', + 'ZEN': 'ZEN', + 'ZIL': 'ZIL', + # '': 'ZYN', + }, + 'accountsByType': { + 'spot': 'spot', + 'funding': 'wallet', + 'swap': 'derivatives', + 'future': 'derivatives', + }, + 'withdraw': { + 'includeFee': False, + }, + }, + 'commonCurrencies': { + 'AUTO': 'Cube', + 'BCC': 'BCC', # initial symbol for Bitcoin Cash, now inactive + 'BDP': 'BidiPass', + 'BET': 'DAO.Casino', + 'BIT': 'BitRewards', + 'BOX': 'BOX Token', + 'CPT': 'Cryptaur', # conflict with CPT = Contents Protocol https://github.com/ccxt/ccxt/issues/4920 and https://github.com/ccxt/ccxt/issues/6081 + 'GET': 'Themis', + 'GMT': 'GMT Token', + 'HSR': 'HC', + 'IQ': 'IQ.Cash', + 'LNC': 'LinkerCoin', + 'PLA': 'PlayChip', + 'PNT': 'Penta', + 'SBTC': 'Super Bitcoin', + 'STEPN': 'GMT', + 'STX': 'STOX', + 'TV': 'Tokenville', + 'XMT': 'MTL', + 'XPNT': 'PNT', + }, + }) + + def nonce(self): + return self.milliseconds() + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for hitbtc + + https://api.hitbtc.com/#symbols + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = await self.publicGetPublicSymbol(params) + # + # { + # "AAVEUSDT_PERP":{ + # "type":"futures", + # "expiry":null, + # "underlying":"AAVE", + # "base_currency":null, + # "quote_currency":"USDT", + # "quantity_increment":"0.01", + # "tick_size":"0.001", + # "take_rate":"0.0005", + # "make_rate":"0.0002", + # "fee_currency":"USDT", + # "margin_trading":true, + # "max_initial_leverage":"50.00" + # }, + # "MANAUSDT":{ + # "type":"spot", + # "base_currency":"MANA", + # "quote_currency":"USDT", + # "quantity_increment":"1", + # "tick_size":"0.0000001", + # "take_rate":"0.0025", + # "make_rate":"0.001", + # "fee_currency":"USDT", + # "margin_trading":true, + # "max_initial_leverage":"5.00" + # }, + # } + # + result = [] + ids = list(response.keys()) + for i in range(0, len(ids)): + id = ids[i] + if id.endswith('_BQX'): + continue # seems like an invalid symbol and if we try to access it individually we get: {"timestamp":"2023-09-02T14:38:20.351Z","error":{"description":"Try get /public/symbol, to get list of all available symbols.","code":2001,"message":"No such symbol: EOSUSD_BQX"},"path":"/api/3/public/symbol/EOSUSD_BQX","requestId":"e1e9fce6-16374591"} + market = self.safe_value(response, id) + marketType = self.safe_string(market, 'type') + expiry = self.safe_integer(market, 'expiry') + contract = (marketType == 'futures') + spot = (marketType == 'spot') + marginTrading = self.safe_bool(market, 'margin_trading', False) + margin = spot and marginTrading + future = (expiry is not None) + swap = (contract and not future) + option = False + baseId = self.safe_string_2(market, 'base_currency', 'underlying') + quoteId = self.safe_string(market, 'quote_currency') + feeCurrencyId = self.safe_string(market, 'fee_currency') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + feeCurrency = self.safe_currency_code(feeCurrencyId) + settleId = None + settle = None + symbol = base + '/' + quote + type = 'spot' + contractSize = None + linear = None + inverse = None + if contract: + contractSize = self.parse_number('1') + settleId = feeCurrencyId + settle = self.safe_currency_code(settleId) + linear = ((quote is not None) and (quote == settle)) + inverse = not linear + symbol = symbol + ':' + settle + if future: + symbol = symbol + '-' + expiry + type = 'future' + else: + type = 'swap' + lotString = self.safe_string(market, 'quantity_increment') + stepString = self.safe_string(market, 'tick_size') + lot = self.parse_number(lotString) + step = self.parse_number(stepString) + result.append({ + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': type, + 'spot': spot, + 'margin': margin, + 'swap': swap, + 'future': future, + 'option': option, + 'active': True, + 'contract': contract, + 'linear': linear, + 'inverse': inverse, + 'taker': self.safe_number(market, 'take_rate'), + 'maker': self.safe_number(market, 'make_rate'), + 'contractSize': contractSize, + 'expiry': expiry, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'feeCurrency': feeCurrency, + 'precision': { + 'amount': lot, + 'price': step, + }, + 'limits': { + 'leverage': { + 'min': self.parse_number('1'), + 'max': self.safe_number(market, 'max_initial_leverage', 1), + }, + 'amount': { + 'min': lot, + 'max': None, + }, + 'price': { + 'min': step, + 'max': None, + }, + 'cost': { + 'min': self.parse_number(Precise.string_mul(lotString, stepString)), + 'max': None, + }, + }, + 'created': None, + 'info': market, + }) + return result + + async def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://api.hitbtc.com/#currencies + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = await self.publicGetPublicCurrency(params) + # + # { + # "DFC": { + # "full_name": "DeFiScale", + # "crypto": True, + # "payin_enabled": False, + # "payout_enabled": True, + # "transfer_enabled": False, + # "transfer_to_wallet_enabled": True, + # "transfer_to_exchange_enabled": False, + # "sign": "D", + # "crypto_payment_id_name": "", + # "crypto_explorer": "https://etherscan.io/tx/{tx}", + # "precision_transfer": "0.00000001", + # "delisted": False, + # "networks": [ + # { + # "code": "ETH", + # "network_name": "Ethereum", + # "network": "ETH", + # "protocol": "ERC-20", + # "default": True, + # "is_ens_available": True, + # "payin_enabled": True, + # "payout_enabled": True, + # "precision_payout": "0.000000000000000001", + # "payout_fee": "277000.0000000000", + # "payout_is_payment_id": False, + # "payin_payment_id": False, + # "payin_confirmations": "2", + # "contract_address": "0x1b2a76da77d03b7fc21189d9838f55bd849014af", + # "crypto_payment_id_name": "", + # "crypto_explorer": "https://etherscan.io/tx/{tx}", + # "is_multichain": True, + # "asset_id": { + # "contract_address": "0x1b2a76da77d03b7fc21189d9838f55bd849014af" + # } + # } + # ] + # }, + # } + # + result: dict = {} + currencies = list(response.keys()) + for i in range(0, len(currencies)): + currencyId = currencies[i] + code = self.safe_currency_code(currencyId) + entry = response[currencyId] + rawNetworks = self.safe_list(entry, 'networks', []) + networks: dict = {} + for j in range(0, len(rawNetworks)): + rawNetwork = rawNetworks[j] + networkId = self.safe_string_2(rawNetwork, 'protocol', 'network') + networkCode = self.network_id_to_code(networkId) + networkCode = networkCode.upper() if (networkCode is not None) else code # is white label, ensure we safeguard from possible bugs + networks[networkCode] = { + 'info': rawNetwork, + 'id': networkId, + 'network': networkCode, + 'active': None, + 'fee': self.safe_number(rawNetwork, 'payout_fee'), + 'deposit': self.safe_bool(rawNetwork, 'payin_enabled'), + 'withdraw': self.safe_bool(rawNetwork, 'payout_enabled'), + 'precision': self.safe_number(rawNetwork, 'precision_payout'), + 'limits': { + 'withdraw': { + 'min': None, + 'max': None, + }, + }, + } + result[code] = self.safe_currency_structure({ + 'info': entry, + 'code': code, + 'id': currencyId, + 'precision': self.safe_number(entry, 'precision_transfer'), + 'name': self.safe_string(entry, 'full_name'), + 'active': not self.safe_bool(entry, 'delisted'), + 'deposit': self.safe_bool(entry, 'payin_enabled'), + 'withdraw': self.safe_bool(entry, 'payout_enabled'), + 'networks': networks, + 'fee': None, + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + }, + 'type': None, # 'crypto' field emits incorrect values + }) + return result + + async def create_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + create a currency deposit address + + https://api.hitbtc.com/#generate-deposit-crypto-address + + :param str code: unified currency code of the currency for the deposit address + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + } + network = self.safe_string_upper(params, 'network') + if (network is not None) and (code == 'USDT'): + networks = self.safe_value(self.options, 'networks') + parsedNetwork = self.safe_string(networks, network) + if parsedNetwork is not None: + request['currency'] = parsedNetwork + params = self.omit(params, 'network') + response = await self.privatePostWalletCryptoAddress(self.extend(request, params)) + # + # {"currency":"ETH","address":"0xd0d9aea60c41988c3e68417e2616065617b7afd3"} + # + currencyId = self.safe_string(response, 'currency') + return { + 'currency': self.safe_currency_code(currencyId), + 'address': self.safe_string(response, 'address'), + 'tag': self.safe_string(response, 'payment_id'), + 'network': None, + 'info': response, + } + + async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://api.hitbtc.com/#get-deposit-crypto-address + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + } + network = self.safe_string_upper(params, 'network') + if (network is not None) and (code == 'USDT'): + networks = self.safe_value(self.options, 'networks') + parsedNetwork = self.safe_string(networks, network) + if parsedNetwork is not None: + request['currency'] = parsedNetwork + params = self.omit(params, 'network') + response = await self.privateGetWalletCryptoAddress(self.extend(request, params)) + # + # [{"currency":"ETH","address":"0xd0d9aea60c41988c3e68417e2616065617b7afd3"}] + # + firstAddress = self.safe_value(response, 0) + address = self.safe_string(firstAddress, 'address') + currencyId = self.safe_string(firstAddress, 'currency') + tag = self.safe_string(firstAddress, 'payment_id') + parsedCode = self.safe_currency_code(currencyId) + return { + 'info': response, + 'currency': parsedCode, + 'network': None, + 'address': address, + 'tag': tag, + } + + def parse_balance(self, response) -> Balances: + result: dict = {'info': response} + for i in range(0, len(response)): + entry = response[i] + currencyId = self.safe_string(entry, 'currency') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(entry, 'available') + account['used'] = self.safe_string(entry, 'reserved') + result[code] = account + return self.safe_balance(result) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://api.hitbtc.com/#wallet-balance + https://api.hitbtc.com/#get-spot-trading-balance + https://api.hitbtc.com/#get-trading-balance + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + type = self.safe_string_lower(params, 'type', 'spot') + params = self.omit(params, ['type']) + accountsByType = self.safe_value(self.options, 'accountsByType', {}) + account = self.safe_string(accountsByType, type, type) + response = None + if account == 'wallet': + response = await self.privateGetWalletBalance(params) + elif account == 'spot': + response = await self.privateGetSpotBalance(params) + elif account == 'derivatives': + response = await self.privateGetFuturesBalance(params) + else: + keys = list(accountsByType.keys()) + raise BadRequest(self.id + ' fetchBalance() type parameter must be one of ' + ', '.join(keys)) + # + # [ + # { + # "currency": "PAXG", + # "available": "0", + # "reserved": "0", + # "reserved_margin": "0", + # }, + # ... + # ] + # + return self.parse_balance(response) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://api.hitbtc.com/#tickers + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = await self.publicGetPublicTickerSymbol(self.extend(request, params)) + # + # { + # "ask": "0.020572", + # "bid": "0.020566", + # "last": "0.020574", + # "low": "0.020388", + # "high": "0.021084", + # "open": "0.020913", + # "volume": "138444.3666", + # "volume_quote": "2853.6874972480", + # "timestamp": "2021-06-02T17:52:36.731Z" + # } + # + return self.parse_ticker(response, market) + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://api.hitbtc.com/#tickers + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + request: dict = {} + if symbols is not None: + marketIds = self.market_ids(symbols) + delimited = ','.join(marketIds) + request['symbols'] = delimited + response = await self.publicGetPublicTicker(self.extend(request, params)) + # + # { + # "BTCUSDT": { + # "ask": "63049.06", + # "bid": "63046.41", + # "last": "63048.36", + # "low": "62010.00", + # "high": "66657.99", + # "open": "64839.75", + # "volume": "15272.13278", + # "volume_quote": "976312127.6277998", + # "timestamp": "2021-10-22T04:25:47.573Z" + # } + # } + # + result: dict = {} + keys = list(response.keys()) + for i in range(0, len(keys)): + marketId = keys[i] + market = self.safe_market(marketId) + symbol = market['symbol'] + entry = response[marketId] + result[symbol] = self.parse_ticker(entry, market) + return self.filter_by_array_tickers(result, 'symbol', symbols) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "ask": "62756.01", + # "bid": "62754.09", + # "last": "62755.87", + # "low": "62010.00", + # "high": "66657.99", + # "open": "65089.27", + # "volume": "16719.50366", + # "volume_quote": "1063422878.8156828", + # "timestamp": "2021-10-22T07:29:14.585Z" + # } + # + timestamp = self.parse8601(ticker['timestamp']) + symbol = self.safe_symbol(None, market) + baseVolume = self.safe_string(ticker, 'volume') + quoteVolume = self.safe_string(ticker, 'volume_quote') + open = self.safe_string(ticker, 'open') + last = self.safe_string(ticker, 'last') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': self.safe_string(ticker, 'bid'), + 'bidVolume': None, + 'ask': self.safe_string(ticker, 'ask'), + 'askVolume': None, + 'vwap': None, + 'open': open, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'info': ticker, + }, market) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://api.hitbtc.com/#trades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = None + request: dict = {} + if limit is not None: + request['limit'] = min(limit, 1000) + if since is not None: + request['from'] = since + response = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + response = await self.publicGetPublicTradesSymbol(self.extend(request, params)) + else: + response = await self.publicGetPublicTrades(self.extend(request, params)) + if symbol is not None: + return self.parse_trades(response, market) + trades = [] + marketIds = list(response.keys()) + for i in range(0, len(marketIds)): + marketId = marketIds[i] + marketInner = self.market(marketId) + rawTrades = response[marketId] + parsed = self.parse_trades(rawTrades, marketInner) + trades = self.array_concat(trades, parsed) + return trades + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://api.hitbtc.com/#spot-trades-history + https://api.hitbtc.com/#futures-trades-history + https://api.hitbtc.com/#margin-trades-history + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' only 'isolated' is supported + :param bool [params.margin]: True for fetching margin trades + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = None + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if limit is not None: + request['limit'] = limit + if since is not None: + request['from'] = since + marketType = None + marginMode = None + response = None + marketType, params = self.handle_market_type_and_params('fetchMyTrades', market, params) + marginMode, params = self.handle_margin_mode_and_params('fetchMyTrades', params) + params = self.omit(params, ['marginMode', 'margin']) + if marginMode is not None: + response = await self.privateGetMarginHistoryTrade(self.extend(request, params)) + else: + if marketType == 'spot': + response = await self.privateGetSpotHistoryTrade(self.extend(request, params)) + elif marketType == 'swap': + response = await self.privateGetFuturesHistoryTrade(self.extend(request, params)) + elif marketType == 'margin': + response = await self.privateGetMarginHistoryTrade(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchMyTrades() not support self market type') + return self.parse_trades(response, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # createOrder(market) + # + # { + # "id": "1569252895", + # "position_id": "0", + # "quantity": "10", + # "price": "0.03919424", + # "fee": "0.000979856000", + # "timestamp": "2022-01-25T19:38:36.153Z", + # "taker": True + # } + # + # fetchTrades + # + # { + # "id": 974786185, + # "price": "0.032462", + # "qty": "0.3673", + # "side": "buy", + # "timestamp": "2020-10-16T12:57:39.846Z" + # } + # + # fetchMyTrades spot + # + # { + # "id": 277210397, + # "clientOrderId": "6e102f3e7f3f4e04aeeb1cdc95592f1a", + # "orderId": 28102855393, + # "symbol": "ETHBTC", + # "side": "sell", + # "quantity": "0.002", + # "price": "0.073365", + # "fee": "0.000000147", + # "timestamp": "2018-04-28T18:39:55.345Z", + # "taker": True + # } + # + # fetchMyTrades swap and margin + # + # { + # "id": 4718564, + # "order_id": 58730811958, + # "client_order_id": "475c47d97f867f09726186eb22b4c3d4", + # "symbol": "BTCUSDT_PERP", + # "side": "sell", + # "quantity": "0.0001", + # "price": "41118.51", + # "fee": "0.002055925500", + # "timestamp": "2022-03-17T05:23:17.795Z", + # "taker": True, + # "position_id": 2350122, + # "pnl": "0.002255000000", + # "liquidation": False + # } + # + timestamp = self.parse8601(trade['timestamp']) + marketId = self.safe_string(trade, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + fee = None + feeCostString = self.safe_string(trade, 'fee') + taker = self.safe_value(trade, 'taker') + takerOrMaker: str + if taker is not None: + takerOrMaker = 'taker' if taker else 'maker' + else: + takerOrMaker = 'taker' # the only case when `taker` field is missing, is public fetchTrades and it must be taker + if feeCostString is not None: + info = self.safe_value(market, 'info', {}) + feeCurrency = self.safe_string(info, 'fee_currency') + feeCurrencyCode = self.safe_currency_code(feeCurrency) + fee = { + 'cost': feeCostString, + 'currency': feeCurrencyCode, + } + # we use clientOrderId order id with self exchange intentionally + # because most of their endpoints will require clientOrderId + # explained here: https://github.com/ccxt/ccxt/issues/5674 + orderId = self.safe_string_2(trade, 'clientOrderId', 'client_order_id') + priceString = self.safe_string(trade, 'price') + amountString = self.safe_string_2(trade, 'quantity', 'qty') + side = self.safe_string(trade, 'side') + id = self.safe_string(trade, 'id') + return self.safe_trade({ + 'info': trade, + 'id': id, + 'order': orderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'type': None, + 'side': side, + 'takerOrMaker': takerOrMaker, + 'price': priceString, + 'amount': amountString, + 'cost': None, + 'fee': fee, + }, market) + + async def fetch_transactions_helper(self, types, code, since, limit, params): + await self.load_markets() + request: dict = { + 'types': types, + } + currency = None + if code is not None: + currency = self.currency(code) + request['currencies'] = currency['id'] + if since is not None: + request['from'] = self.iso8601(since) + if limit is not None: + request['limit'] = limit + response = await self.privateGetWalletTransactions(self.extend(request, params)) + # + # [ + # { + # "id": "101609495", + # "created_at": "2018-03-06T22:05:06.507Z", + # "updated_at": "2018-03-06T22:11:45.03Z", + # "status": "SUCCESS", + # "type": "DEPOSIT", + # "subtype": "BLOCKCHAIN", + # "native": { + # "tx_id": "e20b0965-4024-44d0-b63f-7fb8996a6706", + # "index": "881652766", + # "currency": "ETH", + # "amount": "0.01418088", + # "hash": "d95dbbff3f9234114f1211ab0ba2a94f03f394866fd5749d74a1edab80e6c5d3", + # "address": "0xd9259302c32c0a0295d86a39185c9e14f6ba0a0d", + # "confirmations": "20", + # "senders": [ + # "0x243bec9256c9a3469da22103891465b47583d9f1" + # ] + # } + # } + # ] + # + return self.parse_transactions(response, currency, since, limit, params) + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'CREATED': 'pending', + 'PENDING': 'pending', + 'FAILED': 'failed', + 'ROLLED_BACK': 'failed', + 'SUCCESS': 'ok', + } + return self.safe_string(statuses, status, status) + + def parse_transaction_type(self, type): + types: dict = { + 'DEPOSIT': 'deposit', + 'WITHDRAW': 'withdrawal', + } + return self.safe_string(types, type, type) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # transaction + # + # { + # "id": "101609495", + # "created_at": "2018-03-06T22:05:06.507Z", + # "updated_at": "2018-03-06T22:11:45.03Z", + # "status": "SUCCESS", + # "type": "DEPOSIT", # DEPOSIT, WITHDRAW, .. + # "subtype": "BLOCKCHAIN", + # "native": { + # "tx_id": "e20b0965-4024-44d0-b63f-7fb8996a6706", + # "index": "881652766", + # "currency": "ETH", + # "amount": "0.01418088", + # "hash": "d95dbbff3f9234114f1211ab0ba2a94f03f394866fd5749d74a1edab80e6c5d3", + # "address": "0xd9259302c32c0a0295d86a39185c9e14f6ba0a0d", + # "confirmations": "20", + # "senders": [ + # "0x243bec9256c9a3469da22103891465b47583d9f1" + # ], + # "fee": "1.22" # only for WITHDRAW + # } + # }, + # "operation_id": "084cfcd5-06b9-4826-882e-fdb75ec3625d", # only for WITHDRAW + # "commit_risk": {} + # withdraw + # + # { + # "id":"084cfcd5-06b9-4826-882e-fdb75ec3625d" + # } + # + id = self.safe_string_2(transaction, 'operation_id', 'id') + timestamp = self.parse8601(self.safe_string(transaction, 'created_at')) + updated = self.parse8601(self.safe_string(transaction, 'updated_at')) + type = self.parse_transaction_type(self.safe_string(transaction, 'type')) + status = self.parse_transaction_status(self.safe_string(transaction, 'status')) + native = self.safe_value(transaction, 'native', {}) + currencyId = self.safe_string(native, 'currency') + code = self.safe_currency_code(currencyId) + txhash = self.safe_string(native, 'hash') + address = self.safe_string(native, 'address') + addressTo = address + tag = self.safe_string(native, 'payment_id') + tagTo = tag + sender = self.safe_value(native, 'senders') + addressFrom = self.safe_string(sender, 0) + amount = self.safe_number(native, 'amount') + subType = self.safe_string(transaction, 'subtype') + internal = subType == 'OFFCHAIN' + # https://api.hitbtc.com/#check-if-offchain-is-available + fee = { + 'currency': None, + 'cost': None, + 'rate': None, + } + feeCost = self.safe_number(native, 'fee') + if feeCost is not None: + fee['currency'] = code + fee['cost'] = feeCost + return { + 'info': transaction, + 'id': id, + 'txid': txhash, + 'type': type, + 'currency': code, + 'network': None, + 'amount': amount, + 'status': status, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'address': address, + 'addressFrom': addressFrom, + 'addressTo': addressTo, + 'tag': tag, + 'tagFrom': None, + 'tagTo': tagTo, + 'updated': updated, + 'comment': None, + 'internal': internal, + 'fee': fee, + } + + async def fetch_deposits_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch history of deposits and withdrawals + + https://api.hitbtc.com/#get-transactions-history + + :param str [code]: unified currency code for the currency of the deposit/withdrawals, default is None + :param int [since]: timestamp in ms of the earliest deposit/withdrawal, default is None + :param int [limit]: max number of deposit/withdrawals to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `transaction structure ` + """ + return await self.fetch_transactions_helper('DEPOSIT,WITHDRAW', code, since, limit, params) + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://api.hitbtc.com/#get-transactions-history + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + return await self.fetch_transactions_helper('DEPOSIT', code, since, limit, params) + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://api.hitbtc.com/#get-transactions-history + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + return await self.fetch_transactions_helper('WITHDRAW', code, since, limit, params) + + async def fetch_order_books(self, symbols: Strings = None, limit: Int = None, params={}) -> OrderBooks: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data for multiple markets + + https://api.hitbtc.com/#order-books + + :param str[] [symbols]: list of unified market symbols, all symbols fetched if None, default is None + :param int [limit]: max number of entries per orderbook to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `order book structures ` indexed by market symbol + """ + await self.load_markets() + request: dict = {} + if symbols is not None: + marketIdsInner = self.market_ids(symbols) + request['symbols'] = ','.join(marketIdsInner) + if limit is not None: + request['depth'] = limit + response = await self.publicGetPublicOrderbook(self.extend(request, params)) + result: dict = {} + marketIds = list(response.keys()) + for i in range(0, len(marketIds)): + marketId = marketIds[i] + orderbook = response[marketId] + symbol = self.safe_symbol(marketId) + timestamp = self.parse8601(self.safe_string(orderbook, 'timestamp')) + result[symbol] = self.parse_order_book(response[marketId], symbol, timestamp, 'bid', 'ask') + return result + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://api.hitbtc.com/#order-books + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['depth'] = limit + response = await self.publicGetPublicOrderbookSymbol(self.extend(request, params)) + timestamp = self.parse8601(self.safe_string(response, 'timestamp')) + return self.parse_order_book(response, symbol, timestamp, 'bid', 'ask') + + def parse_trading_fee(self, fee: dict, market: Market = None) -> TradingFeeInterface: + # + # { + # "symbol":"ARVUSDT", # returned from fetchTradingFees only + # "take_rate":"0.0009", + # "make_rate":"0.0009" + # } + # + taker = self.safe_number(fee, 'take_rate') + maker = self.safe_number(fee, 'make_rate') + marketId = self.safe_string(fee, 'symbol') + symbol = self.safe_symbol(marketId, market) + return { + 'info': fee, + 'symbol': symbol, + 'taker': taker, + 'maker': maker, + 'percentage': None, + 'tierBased': None, + } + + async def fetch_trading_fee(self, symbol: str, params={}) -> TradingFeeInterface: + """ + fetch the trading fees for a market + + https://api.hitbtc.com/#get-trading-commission + https://api.hitbtc.com/#get-trading-commission-2 + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `fee structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = None + if market['type'] == 'spot': + response = await self.privateGetSpotFeeSymbol(self.extend(request, params)) + elif market['type'] == 'swap': + response = await self.privateGetFuturesFeeSymbol(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchTradingFee() not support self market type') + # + # { + # "take_rate":"0.0009", + # "make_rate":"0.0009" + # } + # + return self.parse_trading_fee(response, market) + + async def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://api.hitbtc.com/#get-all-trading-commissions + https://api.hitbtc.com/#get-all-trading-commissions-2 + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + await self.load_markets() + marketType, query = self.handle_market_type_and_params('fetchTradingFees', None, params) + response = None + if marketType == 'spot': + response = await self.privateGetSpotFee(query) + elif marketType == 'swap': + response = await self.privateGetFuturesFee(query) + else: + raise NotSupported(self.id + ' fetchTradingFees() not support self market type') + # + # [ + # { + # "symbol":"ARVUSDT", + # "take_rate":"0.0009", + # "make_rate":"0.0009" + # } + # ] + # + result: dict = {} + for i in range(0, len(response)): + fee = self.parse_trading_fee(response[i]) + symbol = fee['symbol'] + result[symbol] = fee + return result + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://api.hitbtc.com/#candles + https://api.hitbtc.com/#futures-index-price-candles + https://api.hitbtc.com/#futures-mark-price-candles + https://api.hitbtc.com/#futures-premium-index-candles + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest funding rate + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate') + if paginate: + return await self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params, 1000) + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'period': self.safe_string(self.timeframes, timeframe, timeframe), + } + if since is not None: + request['from'] = self.iso8601(since) + request, params = self.handle_until_option('until', request, params) + if limit is not None: + request['limit'] = min(limit, 1000) + price = self.safe_string(params, 'price') + params = self.omit(params, 'price') + response = None + if price == 'mark': + response = await self.publicGetPublicFuturesCandlesMarkPriceSymbol(self.extend(request, params)) + elif price == 'index': + response = await self.publicGetPublicFuturesCandlesIndexPriceSymbol(self.extend(request, params)) + elif price == 'premiumIndex': + response = await self.publicGetPublicFuturesCandlesPremiumIndexSymbol(self.extend(request, params)) + else: + response = await self.publicGetPublicCandlesSymbol(self.extend(request, params)) + # + # Spot and Swap + # + # [ + # { + # "timestamp": "2021-10-25T07:38:00.000Z", + # "open": "4173.391", + # "close": "4170.923", + # "min": "4170.923", + # "max": "4173.986", + # "volume": "0.1879", + # "volume_quote": "784.2517846" + # } + # ] + # + # Mark, Index and Premium Index + # + # [ + # { + # "timestamp": "2022-04-01T01:28:00.000Z", + # "open": "45146.39", + # "close": "45219.43", + # "min": "45146.39", + # "max": "45219.43" + # }, + # ] + # + return self.parse_ohlcvs(response, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # Spot and Swap + # + # { + # "timestamp":"2015-08-20T19:01:00.000Z", + # "open":"0.006", + # "close":"0.006", + # "min":"0.006", + # "max":"0.006", + # "volume":"0.003", + # "volume_quote":"0.000018" + # } + # + # Mark, Index and Premium Index + # + # { + # "timestamp": "2022-04-01T01:28:00.000Z", + # "open": "45146.39", + # "close": "45219.43", + # "min": "45146.39", + # "max": "45219.43" + # }, + # + return [ + self.parse8601(self.safe_string(ohlcv, 'timestamp')), + self.safe_number(ohlcv, 'open'), + self.safe_number(ohlcv, 'max'), + self.safe_number(ohlcv, 'min'), + self.safe_number(ohlcv, 'close'), + self.safe_number(ohlcv, 'volume'), + ] + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://api.hitbtc.com/#spot-orders-history + https://api.hitbtc.com/#futures-orders-history + https://api.hitbtc.com/#margin-orders-history + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' only 'isolated' is supported + :param bool [params.margin]: True for fetching margin orders + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + market = None + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if since is not None: + request['from'] = self.iso8601(since) + if limit is not None: + request['limit'] = limit + marketType = None + marginMode = None + marketType, params = self.handle_market_type_and_params('fetchClosedOrders', market, params) + marginMode, params = self.handle_margin_mode_and_params('fetchClosedOrders', params) + params = self.omit(params, ['marginMode', 'margin']) + response = None + if marginMode is not None: + response = await self.privateGetMarginHistoryOrder(self.extend(request, params)) + else: + if marketType == 'spot': + response = await self.privateGetSpotHistoryOrder(self.extend(request, params)) + elif marketType == 'swap': + response = await self.privateGetFuturesHistoryOrder(self.extend(request, params)) + elif marketType == 'margin': + response = await self.privateGetMarginHistoryOrder(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchClosedOrders() not support self market type') + parsed = self.parse_orders(response, market, since, limit) + return self.filter_by_array(parsed, 'status', ['closed', 'canceled'], False) + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://api.hitbtc.com/#spot-orders-history + https://api.hitbtc.com/#futures-orders-history + https://api.hitbtc.com/#margin-orders-history + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' only 'isolated' is supported + :param bool [params.margin]: True for fetching a margin order + :returns dict: An `order structure ` + """ + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + request: dict = { + 'client_order_id': id, + } + marketType = None + marginMode = None + marketType, params = self.handle_market_type_and_params('fetchOrder', market, params) + marginMode, params = self.handle_margin_mode_and_params('fetchOrder', params) + params = self.omit(params, ['marginMode', 'margin']) + response = None + if marginMode is not None: + response = await self.privateGetMarginHistoryOrder(self.extend(request, params)) + else: + if marketType == 'spot': + response = await self.privateGetSpotHistoryOrder(self.extend(request, params)) + elif marketType == 'swap': + response = await self.privateGetFuturesHistoryOrder(self.extend(request, params)) + elif marketType == 'margin': + response = await self.privateGetMarginHistoryOrder(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchOrder() not support self market type') + # + # [ + # { + # "id": "685965182082", + # "client_order_id": "B3CBm9uGg9oYQlw96bBSEt38-6gbgBO0", + # "symbol": "BTCUSDT", + # "side": "buy", + # "status": "new", + # "type": "limit", + # "time_in_force": "GTC", + # "quantity": "0.00010", + # "quantity_cumulative": "0", + # "price": "50000.00", + # "price_average": "0", + # "created_at": "2021-10-26T11:40:09.287Z", + # "updated_at": "2021-10-26T11:40:09.287Z" + # } + # ] + # + order = self.safe_dict(response, 0) + return self.parse_order(order, market) + + async def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all the trades made from a single order + + https://api.hitbtc.com/#spot-trades-history + https://api.hitbtc.com/#futures-trades-history + https://api.hitbtc.com/#margin-trades-history + + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' only 'isolated' is supported + :param bool [params.margin]: True for fetching margin trades + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + request: dict = { + 'order_id': id, # exchange assigned order id to the client order id + } + marketType = None + marginMode = None + marketType, params = self.handle_market_type_and_params('fetchOrderTrades', market, params) + marginMode, params = self.handle_margin_mode_and_params('fetchOrderTrades', params) + params = self.omit(params, ['marginMode', 'margin']) + response = None + if marginMode is not None: + response = await self.privateGetMarginHistoryTrade(self.extend(request, params)) + else: + if marketType == 'spot': + response = await self.privateGetSpotHistoryTrade(self.extend(request, params)) + elif marketType == 'swap': + response = await self.privateGetFuturesHistoryTrade(self.extend(request, params)) + elif marketType == 'margin': + response = await self.privateGetMarginHistoryTrade(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchOrderTrades() not support self market type') + # + # Spot + # + # [ + # { + # "id": 1393448977, + # "order_id": 653496804534, + # "client_order_id": "065f6f0ff9d54547848454182263d7b4", + # "symbol": "DICEETH", + # "side": "buy", + # "quantity": "1.4", + # "price": "0.00261455", + # "fee": "0.000003294333", + # "timestamp": "2021-09-19T05:35:56.601Z", + # "taker": True + # } + # ] + # + # Swap and Margin + # + # [ + # { + # "id": 4718551, + # "order_id": 58730748700, + # "client_order_id": "dcbcd8549e3445ee922665946002ef67", + # "symbol": "BTCUSDT_PERP", + # "side": "buy", + # "quantity": "0.0001", + # "price": "41095.96", + # "fee": "0.002054798000", + # "timestamp": "2022-03-17T05:23:02.217Z", + # "taker": True, + # "position_id": 2350122, + # "pnl": "0", + # "liquidation": False + # } + # ] + # + return self.parse_trades(response, market, since, limit) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://api.hitbtc.com/#get-all-active-spot-orders + https://api.hitbtc.com/#get-active-futures-orders + https://api.hitbtc.com/#get-active-margin-orders + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' only 'isolated' is supported + :param bool [params.margin]: True for fetching open margin orders + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + market = None + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + marketType = None + marginMode = None + marketType, params = self.handle_market_type_and_params('fetchOpenOrders', market, params) + marginMode, params = self.handle_margin_mode_and_params('fetchOpenOrders', params) + params = self.omit(params, ['marginMode', 'margin']) + response = None + if marginMode is not None: + response = await self.privateGetMarginOrder(self.extend(request, params)) + else: + if marketType == 'spot': + response = await self.privateGetSpotOrder(self.extend(request, params)) + elif marketType == 'swap': + response = await self.privateGetFuturesOrder(self.extend(request, params)) + elif marketType == 'margin': + response = await self.privateGetMarginOrder(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchOpenOrders() not support self market type') + # + # [ + # { + # "id": "488953123149", + # "client_order_id": "103ad305301e4c3590045b13de15b36e", + # "symbol": "BTCUSDT", + # "side": "buy", + # "status": "new", + # "type": "limit", + # "time_in_force": "GTC", + # "quantity": "0.00001", + # "quantity_cumulative": "0", + # "price": "0.01", + # "post_only": False, + # "created_at": "2021-04-13T13:06:16.567Z", + # "updated_at": "2021-04-13T13:06:16.567Z" + # } + # ] + # + return self.parse_orders(response, market, since, limit) + + async def fetch_open_order(self, id: str, symbol: Str = None, params={}): + """ + fetch an open order by it's id + + https://api.hitbtc.com/#get-active-spot-order + https://api.hitbtc.com/#get-active-futures-order + https://api.hitbtc.com/#get-active-margin-order + + :param str id: order id + :param str symbol: unified market symbol, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' only 'isolated' is supported + :param bool [params.margin]: True for fetching an open margin order + :returns dict: an `order structure ` + """ + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + request: dict = { + 'client_order_id': id, + } + marketType = None + marginMode = None + marketType, params = self.handle_market_type_and_params('fetchOpenOrder', market, params) + marginMode, params = self.handle_margin_mode_and_params('fetchOpenOrder', params) + params = self.omit(params, ['marginMode', 'margin']) + response = None + if marginMode is not None: + response = await self.privateGetMarginOrderClientOrderId(self.extend(request, params)) + else: + if marketType == 'spot': + response = await self.privateGetSpotOrderClientOrderId(self.extend(request, params)) + elif marketType == 'swap': + response = await self.privateGetFuturesOrderClientOrderId(self.extend(request, params)) + elif marketType == 'margin': + response = await self.privateGetMarginOrderClientOrderId(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchOpenOrder() not support self market type') + return self.parse_order(response, market) + + async def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders + + https://api.hitbtc.com/#cancel-all-spot-orders + https://api.hitbtc.com/#cancel-futures-orders + https://api.hitbtc.com/#cancel-all-margin-orders + + :param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' only 'isolated' is supported + :param bool [params.margin]: True for canceling margin orders + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + market = None + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + marketType = None + marginMode = None + marketType, params = self.handle_market_type_and_params('cancelAllOrders', market, params) + marginMode, params = self.handle_margin_mode_and_params('cancelAllOrders', params) + params = self.omit(params, ['marginMode', 'margin']) + response = None + if marginMode is not None: + response = await self.privateDeleteMarginOrder(self.extend(request, params)) + else: + if marketType == 'spot': + response = await self.privateDeleteSpotOrder(self.extend(request, params)) + elif marketType == 'swap': + response = await self.privateDeleteFuturesOrder(self.extend(request, params)) + elif marketType == 'margin': + response = await self.privateDeleteMarginOrder(self.extend(request, params)) + else: + raise NotSupported(self.id + ' cancelAllOrders() not support self market type') + return self.parse_orders(response, market) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://api.hitbtc.com/#cancel-spot-order + https://api.hitbtc.com/#cancel-futures-order + https://api.hitbtc.com/#cancel-margin-order + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' only 'isolated' is supported + :param bool [params.margin]: True for canceling a margin order + :returns dict: An `order structure ` + """ + await self.load_markets() + market = None + request: dict = { + 'client_order_id': id, + } + if symbol is not None: + market = self.market(symbol) + marketType = None + marginMode = None + marketType, params = self.handle_market_type_and_params('cancelOrder', market, params) + marginMode, params = self.handle_margin_mode_and_params('cancelOrder', params) + params = self.omit(params, ['marginMode', 'margin']) + response = None + if marginMode is not None: + response = await self.privateDeleteMarginOrderClientOrderId(self.extend(request, params)) + else: + if marketType == 'spot': + response = await self.privateDeleteSpotOrderClientOrderId(self.extend(request, params)) + elif marketType == 'swap': + response = await self.privateDeleteFuturesOrderClientOrderId(self.extend(request, params)) + elif marketType == 'margin': + response = await self.privateDeleteMarginOrderClientOrderId(self.extend(request, params)) + else: + raise NotSupported(self.id + ' cancelOrder() not support self market type') + return self.parse_order(response, market) + + async def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + await self.load_markets() + market = None + request: dict = { + 'client_order_id': id, + 'quantity': self.amount_to_precision(symbol, amount), + } + if (type == 'limit') or (type == 'stopLimit'): + if price is None: + raise ExchangeError(self.id + ' editOrder() limit order requires price') + request['price'] = self.price_to_precision(symbol, price) + if symbol is not None: + market = self.market(symbol) + marketType = None + marginMode = None + marketType, params = self.handle_market_type_and_params('editOrder', market, params) + marginMode, params = self.handle_margin_mode_and_params('editOrder', params) + params = self.omit(params, ['marginMode', 'margin']) + response = None + if marginMode is not None: + response = await self.privatePatchMarginOrderClientOrderId(self.extend(request, params)) + else: + if marketType == 'spot': + response = await self.privatePatchSpotOrderClientOrderId(self.extend(request, params)) + elif marketType == 'swap': + response = await self.privatePatchFuturesOrderClientOrderId(self.extend(request, params)) + elif marketType == 'margin': + response = await self.privatePatchMarginOrderClientOrderId(self.extend(request, params)) + else: + raise NotSupported(self.id + ' editOrder() not support self market type') + return self.parse_order(response, market) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://api.hitbtc.com/#create-new-spot-order + https://api.hitbtc.com/#create-margin-order + https://api.hitbtc.com/#create-futures-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' only 'isolated' is supported for spot-margin, swap supports both, default is 'cross' + :param bool [params.margin]: True for creating a margin order + :param float [params.triggerPrice]: The price at which a trigger order is triggered at + :param bool [params.postOnly]: if True, the order will only be posted to the order book and not executed immediately + :param str [params.timeInForce]: "GTC", "IOC", "FOK", "Day", "GTD" + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + request = None + marketType = None + marketType, params = self.handle_market_type_and_params('createOrder', market, params) + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('createOrder', params) + request, params = self.create_order_request(market, marketType, type, side, amount, price, marginMode, params) + response = None + if marketType == 'swap': + response = await self.privatePostFuturesOrder(self.extend(request, params)) + elif (marketType == 'margin') or (marginMode is not None): + response = await self.privatePostMarginOrder(self.extend(request, params)) + else: + response = await self.privatePostSpotOrder(self.extend(request, params)) + return self.parse_order(response, market) + + def create_order_request(self, market: object, marketType: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, marginMode: Str = None, params={}): + isLimit = (type == 'limit') + reduceOnly = self.safe_value(params, 'reduceOnly') + timeInForce = self.safe_string(params, 'timeInForce') + triggerPrice = self.safe_number_n(params, ['triggerPrice', 'stopPrice', 'stop_price']) + isPostOnly = self.is_post_only(type == 'market', None, params) + request: dict = { + 'type': type, + 'side': side, + 'quantity': self.amount_to_precision(market['symbol'], amount), + 'symbol': market['id'], + # 'client_order_id': 'r42gdPjNMZN-H_xs8RKl2wljg_dfgdg4', # Optional + # 'time_in_force': 'GTC', # Optional GTC, IOC, FOK, Day, GTD + # 'price': self.price_to_precision(symbol, price), # Required if type is limit, stopLimit, or takeProfitLimit + # 'stop_price': self.safe_number(params, 'stop_price'), # Required if type is stopLimit, stopMarket, takeProfitLimit, takeProfitMarket + # 'expire_time': '2021-06-15T17:01:05.092Z', # Required if timeInForce is GTD + # 'strict_validate': False, + # 'post_only': False, # Optional + # 'reduce_only': False, # Optional + # 'display_quantity': '0', # Optional + # 'take_rate': 0.001, # Optional + # 'make_rate': 0.001, # Optional + } + if reduceOnly is not None: + if (market['type'] != 'swap') and (market['type'] != 'margin'): + raise InvalidOrder(self.id + ' createOrder() does not support reduce_only for ' + market['type'] + ' orders, reduce_only orders are supported for swap and margin markets only') + if reduceOnly is True: + request['reduce_only'] = reduceOnly + if isPostOnly: + request['post_only'] = True + if timeInForce is not None: + request['time_in_force'] = timeInForce + if isLimit or (type == 'stopLimit') or (type == 'takeProfitLimit'): + if price is None: + raise ExchangeError(self.id + ' createOrder() requires a price argument for limit orders') + request['price'] = self.price_to_precision(market['symbol'], price) + if (timeInForce == 'GTD'): + expireTime = self.safe_string(params, 'expire_time') + if expireTime is None: + raise ExchangeError(self.id + ' createOrder() requires an expire_time parameter for a GTD order') + if triggerPrice is not None: + request['stop_price'] = self.price_to_precision(market['symbol'], triggerPrice) + if isLimit: + request['type'] = 'stopLimit' + elif type == 'market': + request['type'] = 'stopMarket' + elif (type == 'stopLimit') or (type == 'stopMarket') or (type == 'takeProfitLimit') or (type == 'takeProfitMarket'): + raise ExchangeError(self.id + ' createOrder() requires a triggerPrice parameter for stop-loss and take-profit orders') + params = self.omit(params, ['triggerPrice', 'timeInForce', 'stopPrice', 'stop_price', 'reduceOnly', 'postOnly']) + if marketType == 'swap': + # set default margin mode to cross + if marginMode is None: + marginMode = 'cross' + request['margin_mode'] = marginMode + return [request, params] + + def parse_order_status(self, status: Str): + statuses: dict = { + 'new': 'open', + 'suspended': 'open', + 'partiallyFilled': 'open', + 'filled': 'closed', + 'canceled': 'canceled', + 'expired': 'failed', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # limit + # { + # "id": 488953123149, + # "client_order_id": "103ad305301e4c3590045b13de15b36e", + # "symbol": "BTCUSDT", + # "side": "buy", + # "status": "new", + # "type": "limit", + # "time_in_force": "GTC", + # "quantity": "0.00001", + # "quantity_cumulative": "0", + # "price": "0.01", + # "price_average": "0.01", + # "post_only": False, + # "created_at": "2021-04-13T13:06:16.567Z", + # "updated_at": "2021-04-13T13:06:16.567Z" + # } + # + # market + # { + # "id": "685877626834", + # "client_order_id": "Yshl7G-EjaREyXQYaGbsmdtVbW-nzQwu", + # "symbol": "BTCUSDT", + # "side": "buy", + # "status": "filled", + # "type": "market", + # "time_in_force": "GTC", + # "quantity": "0.00010", + # "quantity_cumulative": "0.00010", + # "post_only": False, + # "created_at": "2021-10-26T08:55:55.1Z", + # "updated_at": "2021-10-26T08:55:55.1Z", + # "trades": [ + # { + # "id": "1437229630", + # "position_id": "0", + # "quantity": "0.00010", + # "price": "62884.78", + # "fee": "0.005659630200", + # "timestamp": "2021-10-26T08:55:55.1Z", + # "taker": True + # } + # ] + # } + # + # swap and margin + # + # { + # "id": 58418961892, + # "client_order_id": "r42gdPjNMZN-H_xs8RKl2wljg_dfgdg4", + # "symbol": "BTCUSDT_PERP", + # "side": "buy", + # "status": "new", + # "type": "limit", + # "time_in_force": "GTC", + # "quantity": "0.0005", + # "quantity_cumulative": "0", + # "price": "30000.00", + # "post_only": False, + # "reduce_only": False, + # "created_at": "2022-03-16T08:16:53.039Z", + # "updated_at": "2022-03-16T08:16:53.039Z" + # } + # + id = self.safe_string(order, 'client_order_id') + # we use clientOrderId order id with self exchange intentionally + # because most of their endpoints will require clientOrderId + # explained here: https://github.com/ccxt/ccxt/issues/5674 + side = self.safe_string(order, 'side') + type = self.safe_string(order, 'type') + amount = self.safe_string(order, 'quantity') + price = self.safe_string(order, 'price') + average = self.safe_string(order, 'price_average') + created = self.safe_string(order, 'created_at') + timestamp = self.parse8601(created) + updated = self.safe_string(order, 'updated_at') + lastTradeTimestamp = None + if updated != created: + lastTradeTimestamp = self.parse8601(updated) + filled = self.safe_string(order, 'quantity_cumulative') + status = self.parse_order_status(self.safe_string(order, 'status')) + marketId = self.safe_string(order, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + postOnly = self.safe_value(order, 'post_only') + timeInForce = self.safe_string(order, 'time_in_force') + rawTrades = self.safe_value(order, 'trades') + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': id, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': lastTradeTimestamp, + 'lastUpdateTimestamp': lastTradeTimestamp, + 'symbol': symbol, + 'price': price, + 'amount': amount, + 'type': type, + 'side': side, + 'timeInForce': timeInForce, + 'postOnly': postOnly, + 'reduceOnly': self.safe_value(order, 'reduce_only'), + 'filled': filled, + 'remaining': None, + 'cost': None, + 'status': status, + 'average': average, + 'trades': rawTrades, + 'fee': None, + 'triggerPrice': self.safe_string(order, 'stop_price'), + 'takeProfitPrice': None, + 'stopLossPrice': None, + }, market) + + async def fetch_margin_modes(self, symbols: List[Str] = None, params={}) -> MarginModes: + """ + fetches margin mode of the user + + https://api.hitbtc.com/#get-margin-position-parameters + https://api.hitbtc.com/#get-futures-position-parameters + + :param str[] symbols: unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `margin mode structures ` + """ + await self.load_markets() + market = None + if symbols is not None: + symbols = self.market_symbols(symbols) + market = self.market(symbols[0]) + marketType = None + marketType, params = self.handle_market_type_and_params('fetchMarginMode', market, params) + response = None + if marketType == 'margin': + response = await self.privateGetMarginConfig(params) + # + # { + # "config": [{ + # "symbol": "BTCUSD", + # "margin_call_leverage_mul": "1.50", + # "liquidation_leverage_mul": "2.00", + # "max_initial_leverage": "10.00", + # "margin_mode": "Isolated", + # "force_close_fee": "0.05", + # "enabled": True, + # "active": True, + # "limit_base": "50000.00", + # "limit_power": "2.2", + # "unlimited_threshold": "10.0" + # }] + # } + # + elif marketType == 'swap': + response = await self.privateGetFuturesConfig(params) + # + # { + # "config": [{ + # "symbol": "BTCUSD_PERP", + # "margin_call_leverage_mul": "1.20", + # "liquidation_leverage_mul": "2.00", + # "max_initial_leverage": "100.00", + # "margin_mode": "Isolated", + # "force_close_fee": "0.001", + # "enabled": True, + # "active": False, + # "limit_base": "5000000.000000000000", + # "limit_power": "1.25", + # "unlimited_threshold": "2.00" + # }] + # } + # + else: + raise BadSymbol(self.id + ' fetchMarginModes() supports swap contracts and margin only') + config = self.safe_list(response, 'config', []) + return self.parse_margin_modes(config, symbols, 'symbol') + + def parse_margin_mode(self, marginMode: dict, market=None) -> MarginMode: + marketId = self.safe_string(marginMode, 'symbol') + return { + 'info': marginMode, + 'symbol': self.safe_symbol(marketId, market), + 'marginMode': self.safe_string_lower(marginMode, 'margin_mode'), + } + + async def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://api.hitbtc.com/#transfer-between-wallet-and-exchange + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account to transfer from + :param str toAccount: account to transfer to + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transfer structure ` + """ + # account can be "spot", "wallet", or "derivatives" + await self.load_markets() + currency = self.currency(code) + requestAmount = self.currency_to_precision(code, amount) + accountsByType = self.safe_value(self.options, 'accountsByType', {}) + fromAccount = fromAccount.lower() + toAccount = toAccount.lower() + fromId = self.safe_string(accountsByType, fromAccount, fromAccount) + toId = self.safe_string(accountsByType, toAccount, toAccount) + if fromId == toId: + raise BadRequest(self.id + ' transfer() fromAccount and toAccount arguments cannot be the same account') + request: dict = { + 'currency': currency['id'], + 'amount': requestAmount, + 'source': fromId, + 'destination': toId, + } + response = await self.privatePostWalletTransfer(self.extend(request, params)) + # + # [ + # "2db6ebab-fb26-4537-9ef8-1a689472d236" + # ] + # + return self.parse_transfer(response, currency) + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + # + # transfer + # + # [ + # "2db6ebab-fb26-4537-9ef8-1a689472d236" + # ] + # + return { + 'id': self.safe_string(transfer, 0), + 'timestamp': None, + 'datetime': None, + 'currency': self.safe_currency_code(None, currency), + 'amount': None, + 'fromAccount': None, + 'toAccount': None, + 'status': None, + 'info': transfer, + } + + async def convert_currency_network(self, code: str, amount, fromNetwork, toNetwork, params): + await self.load_markets() + if code != 'USDT': + raise ExchangeError(self.id + ' convertCurrencyNetwork() only supports USDT currently') + networks = self.safe_value(self.options, 'networks', {}) + fromNetwork = fromNetwork.upper() + toNetwork = toNetwork.upper() + fromNetwork = self.safe_string(networks, fromNetwork) # handle ETH>ERC20 alias + toNetwork = self.safe_string(networks, toNetwork) # handle ETH>ERC20 alias + if fromNetwork == toNetwork: + raise BadRequest(self.id + ' convertCurrencyNetwork() fromNetwork cannot be the same') + if (fromNetwork is None) or (toNetwork is None): + keys = list(networks.keys()) + raise ArgumentsRequired(self.id + ' convertCurrencyNetwork() requires a fromNetwork parameter and a toNetwork parameter, supported networks are ' + ', '.join(keys)) + request: dict = { + 'from_currency': fromNetwork, + 'to_currency': toNetwork, + 'amount': self.currency_to_precision(code, amount), + } + response = await self.privatePostWalletConvert(self.extend(request, params)) + # {"result":["587a1868-e62d-4d8e-b27c-dbdb2ee96149","e168df74-c041-41f2-b76c-e43e4fed5bc7"]} + return { + 'info': response, + } + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://api.hitbtc.com/#withdraw-crypto + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + await self.load_markets() + self.check_address(address) + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + 'amount': amount, + 'address': address, + } + if tag is not None: + request['payment_id'] = tag + networks = self.safe_value(self.options, 'networks', {}) + network = self.safe_string_upper(params, 'network') + if (network is not None) and (code == 'USDT'): + parsedNetwork = self.safe_string(networks, network) + if parsedNetwork is not None: + request['network_code'] = parsedNetwork + params = self.omit(params, 'network') + withdrawOptions = self.safe_value(self.options, 'withdraw', {}) + includeFee = self.safe_bool(withdrawOptions, 'includeFee', False) + if includeFee: + request['include_fee'] = True + response = await self.privatePostWalletCryptoWithdraw(self.extend(request, params)) + # + # { + # "id":"084cfcd5-06b9-4826-882e-fdb75ec3625d" + # } + # + return self.parse_transaction(response, currency) + + async def fetch_funding_rates(self, symbols: Strings = None, params={}) -> FundingRates: + """ + fetches funding rates for multiple markets + + https://api.hitbtc.com/#futures-info + + :param str[] symbols: unified symbols of the markets to fetch the funding rates for, all market funding rates are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `funding rate structures ` + """ + await self.load_markets() + market = None + request: dict = {} + if symbols is not None: + symbols = self.market_symbols(symbols) + market = self.market(symbols[0]) + queryMarketIds = self.market_ids(symbols) + request['symbols'] = ','.join(queryMarketIds) + type = None + type, params = self.handle_market_type_and_params('fetchFundingRates', market, params) + if type != 'swap': + raise NotSupported(self.id + ' fetchFundingRates() does not support ' + type + ' markets') + response = await self.publicGetPublicFuturesInfo(self.extend(request, params)) + # + # { + # "BTCUSDT_PERP": { + # "contract_type": "perpetual", + # "mark_price": "30897.68", + # "index_price": "30895.29", + # "funding_rate": "0.0001", + # "open_interest": "93.7128", + # "next_funding_time": "2021-07-21T16:00:00.000Z", + # "indicative_funding_rate": "0.0001", + # "premium_index": "0.000047541807127312", + # "avg_premium_index": "0.000087063368020112", + # "interest_rate": "0.0001", + # "timestamp": "2021-07-21T09:48:37.235Z" + # } + # } + # + marketIds = list(response.keys()) + fundingRates: dict = {} + for i in range(0, len(marketIds)): + marketId = self.safe_string(marketIds, i) + rawFundingRate = self.safe_value(response, marketId) + marketInner = self.market(marketId) + symbol = marketInner['symbol'] + fundingRate = self.parse_funding_rate(rawFundingRate, marketInner) + fundingRates[symbol] = fundingRate + return self.filter_by_array(fundingRates, 'symbol', symbols) + + async def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + + https://api.hitbtc.com/#funding-history + + fetches historical funding rate prices + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: timestamp in ms of the earliest funding rate to fetch + :param int [limit]: the maximum amount of `funding rate structures ` to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest funding rate + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `funding rate structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchFundingRateHistory', 'paginate') + if paginate: + return await self.fetch_paginated_call_deterministic('fetchFundingRateHistory', symbol, since, limit, '8h', params, 1000) + market = None + request: dict = { + # all arguments are optional + # 'symbols': Comma separated list of symbol codes, + # 'sort': 'DESC' or 'ASC' + # 'from': 'Datetime or Number', + # 'until': 'Datetime or Number', + # 'limit': 100, + # 'offset': 0, + } + request, params = self.handle_until_option('until', request, params) + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + request['symbols'] = market['id'] + if since is not None: + request['from'] = since + if limit is not None: + request['limit'] = limit + response = await self.publicGetPublicFuturesHistoryFunding(self.extend(request, params)) + # + # { + # "BTCUSDT_PERP": [ + # { + # "timestamp": "2021-07-29T16:00:00.271Z", + # "funding_rate": "0.0001", + # "avg_premium_index": "0.000061858585213222", + # "next_funding_time": "2021-07-30T00:00:00.000Z", + # "interest_rate": "0.0001" + # }, + # ... + # ], + # ... + # } + # + contracts = list(response.keys()) + rates = [] + for i in range(0, len(contracts)): + marketId = contracts[i] + marketInner = self.safe_market(marketId) + fundingRateData = response[marketId] + for j in range(0, len(fundingRateData)): + entry = fundingRateData[j] + symbolInner = self.safe_symbol(marketInner['symbol']) + fundingRate = self.safe_number(entry, 'funding_rate') + datetime = self.safe_string(entry, 'timestamp') + rates.append({ + 'info': entry, + 'symbol': symbolInner, + 'fundingRate': fundingRate, + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + }) + sorted = self.sort_by(rates, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, symbol, since, limit) + + async def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://api.hitbtc.com/#get-futures-margin-accounts + https://api.hitbtc.com/#get-all-margin-accounts + + :param str[]|None symbols: not used by hitbtc fetchPositions() + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' only 'isolated' is supported, defaults to spot-margin endpoint if self is set + :param bool [params.margin]: True for fetching spot-margin positions + :returns dict[]: a list of `position structure ` + """ + await self.load_markets() + request: dict = {} + marketType = None + marginMode = None + marketType, params = self.handle_market_type_and_params('fetchPositions', None, params) + if marketType == 'spot': + marketType = 'swap' + marginMode, params = self.handle_margin_mode_and_params('fetchPositions', params) + params = self.omit(params, ['marginMode', 'margin']) + response = None + if marginMode is not None: + response = await self.privateGetMarginAccount(self.extend(request, params)) + else: + if marketType == 'swap': + response = await self.privateGetFuturesAccount(self.extend(request, params)) + elif marketType == 'margin': + response = await self.privateGetMarginAccount(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchPositions() not support self market type') + # + # [ + # { + # "symbol": "ETHUSDT_PERP", + # "type": "isolated", + # "leverage": "10.00", + # "created_at": "2022-03-19T07:54:35.24Z", + # "updated_at": "2022-03-19T07:54:58.922Z", + # currencies": [ + # { + # "code": "USDT", + # "margin_balance": "7.478100643043", + # "reserved_orders": "0", + # "reserved_positions": "0.303530761300" + # } + # ], + # "positions": [ + # { + # "id": 2470568, + # "symbol": "ETHUSDT_PERP", + # "quantity": "0.001", + # "price_entry": "2927.509", + # "price_margin_call": "0", + # "price_liquidation": "0", + # "pnl": "0", + # "created_at": "2022-03-19T07:54:35.24Z", + # "updated_at": "2022-03-19T07:54:58.922Z" + # } + # ] + # }, + # ] + # + result = [] + for i in range(0, len(response)): + result.append(self.parse_position(response[i])) + return result + + async def fetch_position(self, symbol: str, params={}): + """ + fetch data on a single open contract trade position + + https://api.hitbtc.com/#get-futures-margin-account + https://api.hitbtc.com/#get-isolated-margin-account + + :param str symbol: unified market symbol of the market the position is held in, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' only 'isolated' is supported, defaults to spot-margin endpoint if self is set + :param bool [params.margin]: True for fetching a spot-margin position + :returns dict: a `position structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + marketType = None + marginMode = None + marketType, params = self.handle_market_type_and_params('fetchPosition', None, params) + marginMode, params = self.handle_margin_mode_and_params('fetchPosition', params) + params = self.omit(params, ['marginMode', 'margin']) + response = None + if marginMode is not None: + response = await self.privateGetMarginAccountIsolatedSymbol(self.extend(request, params)) + else: + if marketType == 'swap': + response = await self.privateGetFuturesAccountIsolatedSymbol(self.extend(request, params)) + elif marketType == 'margin': + response = await self.privateGetMarginAccountIsolatedSymbol(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchPosition() not support self market type') + # + # [ + # { + # "symbol": "ETHUSDT_PERP", + # "type": "isolated", + # "leverage": "10.00", + # "created_at": "2022-03-19T07:54:35.24Z", + # "updated_at": "2022-03-19T07:54:58.922Z", + # currencies": [ + # { + # "code": "USDT", + # "margin_balance": "7.478100643043", + # "reserved_orders": "0", + # "reserved_positions": "0.303530761300" + # } + # ], + # "positions": [ + # { + # "id": 2470568, + # "symbol": "ETHUSDT_PERP", + # "quantity": "0.001", + # "price_entry": "2927.509", + # "price_margin_call": "0", + # "price_liquidation": "0", + # "pnl": "0", + # "created_at": "2022-03-19T07:54:35.24Z", + # "updated_at": "2022-03-19T07:54:58.922Z" + # } + # ] + # }, + # ] + # + return self.parse_position(response, market) + + def parse_position(self, position: dict, market: Market = None): + # + # [ + # { + # "symbol": "ETHUSDT_PERP", + # "type": "isolated", + # "leverage": "10.00", + # "created_at": "2022-03-19T07:54:35.24Z", + # "updated_at": "2022-03-19T07:54:58.922Z", + # currencies": [ + # { + # "code": "USDT", + # "margin_balance": "7.478100643043", + # "reserved_orders": "0", + # "reserved_positions": "0.303530761300" + # } + # ], + # "positions": [ + # { + # "id": 2470568, + # "symbol": "ETHUSDT_PERP", + # "quantity": "0.001", + # "price_entry": "2927.509", + # "price_margin_call": "0", + # "price_liquidation": "0", + # "pnl": "0", + # "created_at": "2022-03-19T07:54:35.24Z", + # "updated_at": "2022-03-19T07:54:58.922Z" + # } + # ] + # }, + # ] + # + marginMode = self.safe_string(position, 'type') + leverage = self.safe_number(position, 'leverage') + datetime = self.safe_string(position, 'updated_at') + positions = self.safe_value(position, 'positions', []) + liquidationPrice = None + entryPrice = None + contracts = None + for i in range(0, len(positions)): + entry = positions[i] + liquidationPrice = self.safe_number(entry, 'price_liquidation') + entryPrice = self.safe_number(entry, 'price_entry') + contracts = self.safe_number(entry, 'quantity') + currencies = self.safe_value(position, 'currencies', []) + collateral = None + for i in range(0, len(currencies)): + entry = currencies[i] + collateral = self.safe_number(entry, 'margin_balance') + marketId = self.safe_string(position, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + return self.safe_position({ + 'info': position, + 'id': None, + 'symbol': symbol, + 'notional': None, + 'marginMode': marginMode, + 'marginType': marginMode, + 'liquidationPrice': liquidationPrice, + 'entryPrice': entryPrice, + 'unrealizedPnl': None, + 'percentage': None, + 'contracts': contracts, + 'contractSize': None, + 'markPrice': None, + 'lastPrice': None, + 'side': None, + 'hedged': None, + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + 'lastUpdateTimestamp': None, + 'maintenanceMargin': None, + 'maintenanceMarginPercentage': None, + 'collateral': collateral, + 'initialMargin': None, + 'initialMarginPercentage': None, + 'leverage': leverage, + 'marginRatio': None, + 'stopLossPrice': None, + 'takeProfitPrice': None, + }) + + def parse_open_interest(self, interest, market: Market = None): + # + # { + # "contract_type": "perpetual", + # "mark_price": "42307.43", + # "index_price": "42303.27", + # "funding_rate": "0.0001", + # "open_interest": "30.9826", + # "next_funding_time": "2022-03-22T16:00:00.000Z", + # "indicative_funding_rate": "0.0001", + # "premium_index": "0", + # "avg_premium_index": "0.000029587712038098", + # "interest_rate": "0.0001", + # "timestamp": "2022-03-22T08:08:26.687Z" + # } + # + datetime = self.safe_string(interest, 'timestamp') + value = self.safe_number(interest, 'open_interest') + return self.safe_open_interest({ + 'symbol': self.safe_symbol(None, market), + 'openInterestAmount': None, + 'openInterestValue': value, + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + 'info': interest, + }, market) + + async def fetch_open_interests(self, symbols: Strings = None, params={}): + """ + Retrieves the open interest for a list of symbols + + https://api.hitbtc.com/#futures-info + + :param str[] [symbols]: a list of unified CCXT market symbols + :param dict [params]: exchange specific parameters + :returns dict[]: a list of `open interest structures ` + """ + await self.load_markets() + request: dict = {} + symbols = self.market_symbols(symbols) + marketIds = None + if symbols is not None: + marketIds = self.market_ids(symbols) + request['symbols'] = ','.join(marketIds) + response = await self.publicGetPublicFuturesInfo(self.extend(request, params)) + # + # { + # "BTCUSDT_PERP": { + # "contract_type": "perpetual", + # "mark_price": "97291.83", + # "index_price": "97298.61", + # "funding_rate": "-0.000183473092423284", + # "open_interest": "94.1503", + # "next_funding_time": "2024-12-20T08:00:00.000Z", + # "indicative_funding_rate": "-0.00027495203277752", + # "premium_index": "-0.000789474900583786", + # "avg_premium_index": "-0.000683473092423284", + # "interest_rate": "0.0001", + # "timestamp": "2024-12-20T04:57:33.693Z" + # } + # } + # + results = [] + markets = list(response.keys()) + for i in range(0, len(markets)): + marketId = markets[i] + marketInner = self.safe_market(marketId) + results.append(self.parse_open_interest(response[marketId], marketInner)) + return self.filter_by_array(results, 'symbol', symbols) + + async def fetch_open_interest(self, symbol: str, params={}): + """ + Retrieves the open interest of a derivative trading pair + + https://api.hitbtc.com/#futures-info + + :param str symbol: Unified CCXT market symbol + :param dict [params]: exchange specific parameters + :returns dict} an open interest structure{@link https://docs.ccxt.com/#/?id=interest-history-structure: + """ + await self.load_markets() + market = self.market(symbol) + if not market['swap']: + raise BadSymbol(self.id + ' fetchOpenInterest() supports swap contracts only') + request: dict = { + 'symbol': market['id'], + } + response = await self.publicGetPublicFuturesInfoSymbol(self.extend(request, params)) + # + # { + # "contract_type": "perpetual", + # "mark_price": "42307.43", + # "index_price": "42303.27", + # "funding_rate": "0.0001", + # "open_interest": "30.9826", + # "next_funding_time": "2022-03-22T16:00:00.000Z", + # "indicative_funding_rate": "0.0001", + # "premium_index": "0", + # "avg_premium_index": "0.000029587712038098", + # "interest_rate": "0.0001", + # "timestamp": "2022-03-22T08:08:26.687Z" + # } + # + return self.parse_open_interest(response, market) + + async def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate + + https://api.hitbtc.com/#futures-info + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + await self.load_markets() + market = self.market(symbol) + if not market['swap']: + raise BadSymbol(self.id + ' fetchFundingRate() supports swap contracts only') + request: dict = { + 'symbol': market['id'], + } + response = await self.publicGetPublicFuturesInfoSymbol(self.extend(request, params)) + # + # { + # "contract_type": "perpetual", + # "mark_price": "42307.43", + # "index_price": "42303.27", + # "funding_rate": "0.0001", + # "open_interest": "30.9826", + # "next_funding_time": "2022-03-22T16:00:00.000Z", + # "indicative_funding_rate": "0.0001", + # "premium_index": "0", + # "avg_premium_index": "0.000029587712038098", + # "interest_rate": "0.0001", + # "timestamp": "2022-03-22T08:08:26.687Z" + # } + # + return self.parse_funding_rate(response, market) + + def parse_funding_rate(self, contract, market: Market = None) -> FundingRate: + # + # { + # "contract_type": "perpetual", + # "mark_price": "42307.43", + # "index_price": "42303.27", + # "funding_rate": "0.0001", + # "open_interest": "30.9826", + # "next_funding_time": "2022-03-22T16:00:00.000Z", + # "indicative_funding_rate": "0.0001", + # "premium_index": "0", + # "avg_premium_index": "0.000029587712038098", + # "interest_rate": "0.0001", + # "timestamp": "2022-03-22T08:08:26.687Z" + # } + # + fundingDateTime = self.safe_string(contract, 'next_funding_time') + datetime = self.safe_string(contract, 'timestamp') + return { + 'info': contract, + 'symbol': self.safe_symbol(None, market), + 'markPrice': self.safe_number(contract, 'mark_price'), + 'indexPrice': self.safe_number(contract, 'index_price'), + 'interestRate': self.safe_number(contract, 'interest_rate'), + 'estimatedSettlePrice': None, + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + 'fundingRate': self.safe_number(contract, 'funding_rate'), + 'fundingTimestamp': self.parse8601(fundingDateTime), + 'fundingDatetime': fundingDateTime, + 'nextFundingRate': self.safe_number(contract, 'indicative_funding_rate'), + 'nextFundingTimestamp': None, + 'nextFundingDatetime': None, + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': None, + } + + async def modify_margin_helper(self, symbol: str, amount, type, params={}) -> MarginModification: + await self.load_markets() + market = self.market(symbol) + leverage = self.safe_string(params, 'leverage') + if market['swap']: + if leverage is None: + raise ArgumentsRequired(self.id + ' modifyMarginHelper() requires a leverage parameter for swap markets') + stringAmount = self.number_to_string(amount) + if stringAmount != '0': + amount = self.amount_to_precision(symbol, stringAmount) + else: + amount = '0' + request: dict = { + 'symbol': market['id'], # swap and margin + 'margin_balance': amount, # swap and margin + # "leverage": "10", # swap only required + # "strict_validate": False, # swap and margin + } + if leverage is not None: + request['leverage'] = leverage + marketType = None + marginMode = None + marketType, params = self.handle_market_type_and_params('modifyMarginHelper', market, params) + marginMode, params = self.handle_margin_mode_and_params('modifyMarginHelper', params) + response = None + if marketType == 'swap': + response = await self.privatePutFuturesAccountIsolatedSymbol(self.extend(request, params)) + elif (marketType == 'margin') or (marketType == 'spot') or (marginMode == 'isolated'): + response = await self.privatePutMarginAccountIsolatedSymbol(self.extend(request, params)) + else: + raise NotSupported(self.id + ' modifyMarginHelper() not support self market type') + # + # { + # "symbol": "BTCUSDT_PERP", + # "type": "isolated", + # "leverage": "8.00", + # "created_at": "2022-03-30T23:34:27.161Z", + # "updated_at": "2022-03-30T23:34:27.161Z", + # "currencies": [ + # { + # "code": "USDT", + # "margin_balance": "7.000000000000", + # "reserved_orders": "0", + # "reserved_positions": "0" + # } + # ], + # "positions": null + # } + # + return self.extend(self.parse_margin_modification(response, market), { + 'amount': self.parse_number(amount), + 'type': type, + }) + + def parse_margin_modification(self, data: dict, market: Market = None) -> MarginModification: + # + # addMargin/reduceMargin + # + # { + # "symbol": "BTCUSDT_PERP", + # "type": "isolated", + # "leverage": "8.00", + # "created_at": "2022-03-30T23:34:27.161Z", + # "updated_at": "2022-03-30T23:34:27.161Z", + # "currencies": [ + # { + # "code": "USDT", + # "margin_balance": "7.000000000000", + # "reserved_orders": "0", + # "reserved_positions": "0" + # } + # ], + # "positions": null + # } + # + currencies = self.safe_value(data, 'currencies', []) + currencyInfo = self.safe_value(currencies, 0) + datetime = self.safe_string(data, 'updated_at') + return { + 'info': data, + 'symbol': market['symbol'], + 'type': None, + 'marginMode': 'isolated', + 'amount': None, + 'total': None, + 'code': self.safe_string(currencyInfo, 'code'), + 'status': None, + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + } + + async def reduce_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + remove margin from a position + + https://api.hitbtc.com/#create-update-margin-account-2 + https://api.hitbtc.com/#create-update-margin-account + + :param str symbol: unified market symbol + :param float amount: the amount of margin to remove + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' only 'isolated' is supported, defaults to the spot-margin endpoint if self is set + :param bool [params.margin]: True for reducing spot-margin + :returns dict: a `margin structure ` + """ + if self.number_to_string(amount) != '0': + raise BadRequest(self.id + ' reduceMargin() on hitbtc requires the amount to be 0 and that will remove the entire margin amount') + return await self.modify_margin_helper(symbol, amount, 'reduce', params) + + async def add_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + add margin + + https://api.hitbtc.com/#create-update-margin-account-2 + https://api.hitbtc.com/#create-update-margin-account + + :param str symbol: unified market symbol + :param float amount: amount of margin to add + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' only 'isolated' is supported, defaults to the spot-margin endpoint if self is set + :param bool [params.margin]: True for adding spot-margin + :returns dict: a `margin structure ` + """ + return await self.modify_margin_helper(symbol, amount, 'add', params) + + async def fetch_leverage(self, symbol: str, params={}) -> Leverage: + """ + fetch the set leverage for a market + + https://api.hitbtc.com/#get-futures-margin-account + https://api.hitbtc.com/#get-isolated-margin-account + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' only 'isolated' is supported, defaults to the spot-margin endpoint if self is set + :param bool [params.margin]: True for fetching spot-margin leverage + :returns dict: a `leverage structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchLeverage', params) + params = self.omit(params, ['marginMode', 'margin']) + response = None + if marginMode is not None: + response = await self.privateGetMarginAccountIsolatedSymbol(self.extend(request, params)) + else: + if market['type'] == 'spot': + response = await self.privateGetMarginAccountIsolatedSymbol(self.extend(request, params)) + elif market['type'] == 'swap': + response = await self.privateGetFuturesAccountIsolatedSymbol(self.extend(request, params)) + elif market['type'] == 'margin': + response = await self.privateGetMarginAccountIsolatedSymbol(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchLeverage() not support self market type') + # + # { + # "symbol": "BTCUSDT", + # "type": "isolated", + # "leverage": "12.00", + # "created_at": "2022-03-29T22:31:29.067Z", + # "updated_at": "2022-03-30T00:00:00.125Z", + # "currencies": [ + # { + # "code": "USDT", + # "margin_balance": "20.824360374174", + # "reserved_orders": "0", + # "reserved_positions": "0.973330435000" + # } + # ], + # "positions": [ + # { + # "id": 631301, + # "symbol": "BTCUSDT", + # "quantity": "0.00022", + # "price_entry": "47425.57", + # "price_margin_call": "", + # "price_liquidation": "0", + # "pnl": "0", + # "created_at": "2022-03-29T22:31:29.067Z", + # "updated_at": "2022-03-30T00:00:00.125Z" + # } + # ] + # } + # + return self.parse_leverage(response, market) + + def parse_leverage(self, leverage: dict, market: Market = None) -> Leverage: + marketId = self.safe_string(leverage, 'symbol') + leverageValue = self.safe_integer(leverage, 'leverage') + return { + 'info': leverage, + 'symbol': self.safe_symbol(marketId, market), + 'marginMode': self.safe_string_lower(leverage, 'type'), + 'longLeverage': leverageValue, + 'shortLeverage': leverageValue, + } + + async def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://api.hitbtc.com/#create-update-margin-account-2 + + :param float leverage: the rate of leverage + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setLeverage() requires a symbol argument') + await self.load_markets() + if params['margin_balance'] is None: + raise ArgumentsRequired(self.id + ' setLeverage() requires a margin_balance parameter that will transfer margin to the specified trading pair') + market = self.market(symbol) + amount = self.safe_number(params, 'margin_balance') + maxLeverage = self.safe_integer(market['limits']['leverage'], 'max', 50) + if market['type'] != 'swap': + raise BadSymbol(self.id + ' setLeverage() supports swap contracts only') + if (leverage < 1) or (leverage > maxLeverage): + raise BadRequest(self.id + ' setLeverage() leverage should be between 1 and ' + str(maxLeverage) + ' for ' + symbol) + request: dict = { + 'symbol': market['id'], + 'leverage': str(leverage), + 'margin_balance': self.amount_to_precision(symbol, amount), + # 'strict_validate': False, + } + return await self.privatePutFuturesAccountIsolatedSymbol(self.extend(request, params)) + + async def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}): + """ + fetch deposit and withdraw fees + + https://api.hitbtc.com/#currencies + + :param str[]|None codes: list of unified currency codes + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `fees structures ` + """ + await self.load_markets() + response = await self.publicGetPublicCurrency(params) + # + # { + # "WEALTH": { + # "full_name": "ConnectWealth", + # "payin_enabled": False, + # "payout_enabled": False, + # "transfer_enabled": True, + # "precision_transfer": "0.001", + # "networks": [ + # { + # "network": "ETH", + # "protocol": "ERC20", + # "default": True, + # "payin_enabled": False, + # "payout_enabled": False, + # "precision_payout": "0.001", + # "payout_fee": "0.016800000000", + # "payout_is_payment_id": False, + # "payin_payment_id": False, + # "payin_confirmations": "2" + # } + # ] + # } + # } + # + return self.parse_deposit_withdraw_fees(response, codes) + + def parse_deposit_withdraw_fee(self, fee, currency: Currency = None): + # + # { + # "full_name": "ConnectWealth", + # "payin_enabled": False, + # "payout_enabled": False, + # "transfer_enabled": True, + # "precision_transfer": "0.001", + # "networks": [ + # { + # "network": "ETH", + # "protocol": "ERC20", + # "default": True, + # "payin_enabled": False, + # "payout_enabled": False, + # "precision_payout": "0.001", + # "payout_fee": "0.016800000000", + # "payout_is_payment_id": False, + # "payin_payment_id": False, + # "payin_confirmations": "2" + # } + # ] + # } + # + networks = self.safe_value(fee, 'networks', []) + result = self.deposit_withdraw_fee(fee) + for j in range(0, len(networks)): + networkEntry = networks[j] + networkId = self.safe_string(networkEntry, 'network') + networkCode = self.network_id_to_code(networkId) + networkCode = networkCode.upper() if (networkCode is not None) else None + withdrawFee = self.safe_number(networkEntry, 'payout_fee') + isDefault = self.safe_value(networkEntry, 'default') + withdrawResult: dict = { + 'fee': withdrawFee, + 'percentage': False if (withdrawFee is not None) else None, + } + if isDefault is True: + result['withdraw'] = withdrawResult + result['networks'][networkCode] = { + 'withdraw': withdrawResult, + 'deposit': { + 'fee': None, + 'percentage': None, + }, + } + return result + + async def close_position(self, symbol: str, side: OrderSide = None, params={}) -> Order: + """ + closes open positions for a market + + https://api.hitbtc.com/#close-all-futures-margin-positions + + :param str symbol: unified ccxt market symbol + :param str side: 'buy' or 'sell' + :param dict [params]: extra parameters specific to the okx api endpoint + :param str [params.symbol]: *required* unified market symbol + :param str [params.marginMode]: 'cross' or 'isolated', default is 'cross' + :returns dict: An `order structure ` + """ + await self.load_markets() + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('closePosition', params, 'cross') + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'margin_mode': marginMode, + } + response = await self.privateDeleteFuturesPositionMarginModeSymbol(self.extend(request, params)) + # + # { + # "id":"202471640", + # "symbol":"TRXUSDT_PERP", + # "margin_mode":"Cross", + # "leverage":"1.00", + # "quantity":"0", + # "price_entry":"0", + # "price_margin_call":"0", + # "price_liquidation":"0", + # "pnl":"0.001234100000", + # "created_at":"2023-10-29T14:46:13.235Z", + # "updated_at":"2023-12-19T09:34:40.014Z" + # } + # + return self.parse_order(response, market) + + def handle_margin_mode_and_params(self, methodName, params={}, defaultValue=None): + """ + @ignore + marginMode specified by params["marginMode"], self.options["marginMode"], self.options["defaultMarginMode"], params["margin"] = True or self.options["defaultType"] = 'margin' + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Array: the marginMode in lowercase + """ + defaultType = self.safe_string(self.options, 'defaultType') + isMargin = self.safe_bool(params, 'margin', False) + marginMode = None + marginMode, params = super(hitbtc, self).handle_margin_mode_and_params(methodName, params, defaultValue) + if marginMode is None: + if (defaultType == 'margin') or (isMargin is True): + marginMode = 'isolated' + return [marginMode, params] + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + # + # { + # "error": { + # "code": 20001, + # "message": "Insufficient funds", + # "description": "Check that the funds are sufficient, given commissions" + # } + # } + # + # { + # "error": { + # "code": "600", + # "message": "Action not allowed" + # } + # } + # + error = self.safe_value(response, 'error') + errorCode = self.safe_string(error, 'code') + if errorCode is not None: + feedback = self.id + ' ' + body + message = self.safe_string_2(error, 'message', 'description') + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + raise ExchangeError(feedback) + return None + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + query = self.omit(params, self.extract_params(path)) + implodedPath = self.implode_params(path, params) + url = self.urls['api'][api] + '/' + implodedPath + getRequest = None + keys = list(query.keys()) + queryLength = len(keys) + headers = { + 'Content-Type': 'application/json', + } + if method == 'GET': + if queryLength: + getRequest = '?' + self.urlencode(query) + url = url + getRequest + else: + body = self.json(params) + if api == 'private': + self.check_required_credentials() + timestamp = str(self.nonce()) + payload = [method, '/api/3/' + implodedPath] + if method == 'GET': + if getRequest is not None: + payload.append(getRequest) + else: + payload.append(body) + payload.append(timestamp) + payloadString = ''.join(payload) + signature = self.hmac(self.encode(payloadString), self.encode(self.secret), hashlib.sha256, 'hex') + secondPayload = self.apiKey + ':' + signature + ':' + timestamp + encoded = self.string_to_base64(secondPayload) + headers['Authorization'] = 'HS256 ' + encoded + return {'url': url, 'method': method, 'body': body, 'headers': headers} diff --git a/ccxt/async_support/hollaex.py b/ccxt/async_support/hollaex.py new file mode 100644 index 0000000..1049837 --- /dev/null +++ b/ccxt/async_support/hollaex.py @@ -0,0 +1,2001 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.hollaex import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currencies, Currency, DepositAddress, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, OrderBooks, Trade, TradingFees, Transaction +from typing import List +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import OrderImmediatelyFillable +from ccxt.base.errors import NetworkError +from ccxt.base.errors import InvalidNonce +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class hollaex(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(hollaex, self).describe(), { + 'id': 'hollaex', + 'name': 'HollaEx', + 'countries': ['KR'], + # 4 requests per second => 1000ms / 4 = 250 ms between requests + 'rateLimit': 250, + 'version': 'v2', + 'pro': True, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': None, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'createLimitBuyOrder': True, + 'createLimitSellOrder': True, + 'createMarketBuyOrder': True, + 'createMarketSellOrder': True, + 'createOrder': True, + 'createPostOnlyOrder': True, + 'createReduceOnlyOrder': False, + 'createStopLimitOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'fetchBalance': True, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': 'emulated', + 'fetchDepositAddresses': True, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchFundingHistory': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchLeverage': False, + 'fetchMarginMode': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterestHistory': False, + 'fetchOpenOrder': True, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrderBooks': True, + 'fetchOrders': True, + 'fetchPosition': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': True, + 'fetchTransactions': False, + 'fetchTransfer': False, + 'fetchTransfers': False, + 'fetchWithdrawal': True, + 'fetchWithdrawals': True, + 'reduceMargin': False, + 'sandbox': True, + 'setLeverage': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'transfer': False, + 'withdraw': True, + }, + 'timeframes': { + '1m': '1m', + '5m': '5m', + '15m': '15m', + '1h': '1h', + '4h': '4h', + '1d': '1d', + '1w': '1w', + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/75841031-ca375180-5ddd-11ea-8417-b975674c23cb.jpg', + 'test': { + 'rest': 'https://api.sandbox.hollaex.com', + }, + 'api': { + 'rest': 'https://api.hollaex.com', + }, + 'www': 'https://hollaex.com', + 'doc': 'https://apidocs.hollaex.com', + 'referral': 'https://pro.hollaex.com/signup?affiliation_code=QSWA6G', + }, + 'precisionMode': TICK_SIZE, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + }, + 'api': { + 'public': { + 'get': { + 'health': 1, + 'constants': 1, + 'kit': 1, + 'tiers': 1, + 'ticker': 1, + 'tickers': 1, + 'orderbook': 1, + 'orderbooks': 1, + 'trades': 1, + 'chart': 1, + 'charts': 1, + 'minicharts': 1, + 'oracle/prices': 1, + 'quick-trade': 1, + # TradingView + 'udf/config': 1, + 'udf/history': 1, + 'udf/symbols': 1, + }, + }, + 'private': { + 'get': { + 'user': 1, + 'user/balance': 1, + 'user/deposits': 1, + 'user/withdrawals': 1, + 'user/withdrawal/fee': 1, + 'user/trades': 1, + 'orders': 1, + 'order': 1, + }, + 'post': { + 'user/withdrawal': 1, + 'order': 1, + }, + 'delete': { + 'order/all': 1, + 'order': 1, + }, + }, + }, + 'features': { + 'spot': { + 'sandbox': True, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': False, + 'FOK': False, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'selfTradePrevention': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': False, + 'marketBuyRequiresPrice': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 100000, + 'untilDays': 100000, # todo implement + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 100, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 100000, # todo + 'untilDays': 100000, # todo + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 100000, # todo + 'daysBackCanceled': 1, # todo + 'untilDays': 100000, # todo + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 1000, # todo: no limit in request + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'fees': { + 'trading': { + 'tierBased': True, + 'percentage': True, + 'taker': 0.001, + 'maker': 0.001, + }, + }, + 'exceptions': { + 'broad': { + 'API request is expired': InvalidNonce, + 'Invalid token': AuthenticationError, + 'Order not found': OrderNotFound, + 'Insufficient balance': InsufficientFunds, + 'Error 1001 - Order rejected. Order could not be submitted order was set to a post only order.': OrderImmediatelyFillable, + }, + 'exact': { + '400': BadRequest, + '403': AuthenticationError, + '404': BadRequest, + '405': BadRequest, + '410': BadRequest, + '429': BadRequest, + '500': NetworkError, + '503': NetworkError, + }, + }, + 'options': { + # how many seconds before the authenticated request expires + 'api-expires': self.parse_to_int(self.timeout / 1000), + 'networks': { + 'BTC': 'btc', + 'ETH': 'eth', + 'ERC20': 'eth', + 'TRX': 'trx', + 'TRC20': 'trx', + 'XRP': 'xrp', + 'XLM': 'xlm', + 'BNB': 'bnb', + 'MATIC': 'matic', + }, + 'networksById': { + 'eth': 'ERC20', + 'ETH': 'ERC20', + 'ERC20': 'ERC20', + 'trx': 'TRC20', + 'TRX': 'TRC20', + 'TRC20': 'TRC20', + }, + }, + }) + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for hollaex + + https://apidocs.hollaex.com/#constants + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = await self.publicGetConstants(params) + # + # { + # "coins": { + # "xmr": { + # "id": 7, + # "fullname": "Monero", + # "symbol": "xmr", + # "active": True, + # "allow_deposit": True, + # "allow_withdrawal": True, + # "withdrawal_fee": 0.02, + # "min": 0.001, + # "max": 100000, + # "increment_unit": 0.001, + # "deposit_limits": {'1': 0, '2': 0, '3': 0, '4': 0, "5": 0, "6": 0}, + # "withdrawal_limits": {'1': 10, '2': 15, '3': 100, '4': 100, '5': 200, '6': 300, '7': 350, '8': 400, "9": 500, "10": -1}, + # "created_at": "2019-12-09T07:14:02.720Z", + # "updated_at": "2020-01-16T12:12:53.162Z" + # }, + # # ... + # }, + # "pairs": { + # "btc-usdt": { + # "id": 2, + # "name": "btc-usdt", + # "pair_base": "btc", + # "pair_2": "usdt", + # "taker_fees": {'1': 0.3, '2': 0.25, '3': 0.2, '4': 0.18, '5': 0.1, '6': 0.09, '7': 0.08, '8': 0.06, "9": 0.04, "10": 0}, + # "maker_fees": {'1': 0.1, '2': 0.08, '3': 0.05, '4': 0.03, '5': 0, '6': 0, '7': 0, '8': 0, "9": 0, "10": 0}, + # "min_size": 0.0001, + # "max_size": 1000, + # "min_price": 100, + # "max_price": 100000, + # "increment_size": 0.0001, + # "increment_price": 0.05, + # "active": True, + # "created_at": "2019-12-09T07:15:54.537Z", + # "updated_at": "2019-12-09T07:15:54.537Z" + # }, + # }, + # "config": {tiers: 10}, + # "status": True + # } + # + pairs = self.safe_value(response, 'pairs', {}) + keys = list(pairs.keys()) + result = [] + for i in range(0, len(keys)): + key = keys[i] + market = pairs[key] + baseId = self.safe_string(market, 'pair_base') + quoteId = self.safe_string(market, 'pair_2') + base = self.common_currency_code(baseId.upper()) + quote = self.common_currency_code(quoteId.upper()) + result.append({ + 'id': self.safe_string(market, 'name'), + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': self.safe_value(market, 'active'), + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.safe_number(market, 'increment_size'), + 'price': self.safe_number(market, 'increment_price'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(market, 'min_size'), + 'max': self.safe_number(market, 'max_size'), + }, + 'price': { + 'min': self.safe_number(market, 'min_price'), + 'max': self.safe_number(market, 'max_price'), + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': self.parse8601(self.safe_string(market, 'created_at')), + 'info': market, + }) + return result + + async def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://apidocs.hollaex.com/#constants + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = await self.publicGetConstants(params) + # + # { + # "coins": { + # "usdt": { + # "id": "6", + # "fullname": "USD Tether", + # "symbol": "usdt", + # "active": True, + # "verified": True, + # "allow_deposit": True, + # "allow_withdrawal": True, + # "withdrawal_fee": "20", + # "min": "1", + # "max": "10000000", + # "increment_unit": "0.0001", + # "logo": "https://hollaex-resources.s3.ap-southeast-1.amazonaws.com/icons/usdt.svg", + # "code": "usdt", + # "is_public": True, + # "meta": { + # "color": "#27a17a", + # "website": "https://tether.to", + # "explorer": "https://blockchair.com/tether", + # "decimal_points": "6" + # }, + # "estimated_price": "1", + # "description": "

Tether(USDT) is a stablecoin pegged 1:1 to the US dollar. It is a digital currency that aims to maintain its value while allowing for fast and secure transfer of funds. It was the first stablecoin, and is the most widely used due stablecoin due to its stability and low volatility compared to other cryptocurrencies. It was launched in 2014 by Tether Limited.

", + # "type": "blockchain", + # "network": "eth,trx,bnb,matic", + # "standard": "", + # "issuer": "HollaEx", + # "withdrawal_fees": { + # "bnb": { + # "value": "0.8", + # "active": True, + # "symbol": "usdt" + # }, + # "eth": { + # "value": "1.5", + # "active": True, + # "symbol": "usdt" + # }, + # "trx": { + # "value": "4", + # "active": True, + # "symbol": "usdt" + # }, + # "matic": { + # "value": "0.3", + # "active": True, + # "symbol": "usdt" + # } + # }, + # "display_name": null, + # "deposit_fees": null, + # "is_risky": False, + # "market_cap": "144568098696.29", + # "category": "stable", + # "created_at": "2019-08-09T10:45:43.367Z", + # "updated_at": "2025-03-25T17:12:37.970Z", + # "created_by": "168", + # "owner_id": "1" + # }, + # }, + # "network":"https://api.hollaex.network" + # } + # + coins = self.safe_dict(response, 'coins', {}) + keys = list(coins.keys()) + result: dict = {} + for i in range(0, len(keys)): + key = keys[i] + currency = coins[key] + id = self.safe_string(currency, 'symbol') + code = self.safe_currency_code(id) + withdrawalLimits = self.safe_list(currency, 'withdrawal_limits', []) + rawType = self.safe_string(currency, 'type') + type = 'crypto' if (rawType == 'blockchain') else 'other' + rawNetworks = self.safe_dict(currency, 'withdrawal_fees', {}) + networks = {} + networkIds = list(rawNetworks.keys()) + for j in range(0, len(networkIds)): + networkId = networkIds[j] + networkEntry = self.safe_dict(rawNetworks, networkId) + networkCode = self.network_id_to_code(networkId) + networks[networkCode] = { + 'id': networkId, + 'network': networkCode, + 'active': self.safe_bool(networkEntry, 'active'), + 'deposit': None, + 'withdraw': None, + 'fee': self.safe_number(networkEntry, 'value'), + 'precision': None, + 'limits': { + 'withdraw': { + 'min': None, + 'max': None, + }, + }, + 'info': networkEntry, + } + result[code] = self.safe_currency_structure({ + 'id': id, + 'numericId': self.safe_integer(currency, 'id'), + 'code': code, + 'info': currency, + 'name': self.safe_string(currency, 'fullname'), + 'active': self.safe_bool(currency, 'active'), + 'deposit': self.safe_bool(currency, 'allow_deposit'), + 'withdraw': self.safe_bool(currency, 'allow_withdrawal'), + 'fee': self.safe_number(currency, 'withdrawal_fee'), + 'precision': self.safe_number(currency, 'increment_unit'), + 'limits': { + 'amount': { + 'min': self.safe_number(currency, 'min'), + 'max': self.safe_number(currency, 'max'), + }, + 'withdraw': { + 'min': None, + 'max': self.safe_value(withdrawalLimits, 0), + }, + }, + 'networks': networks, + 'type': type, + }) + return result + + async def fetch_order_books(self, symbols: Strings = None, limit: Int = None, params={}) -> OrderBooks: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data for multiple markets + + https://apidocs.hollaex.com/#orderbooks + + :param str[]|None symbols: not used by hollaex fetchOrderBooks() + :param int [limit]: not used by hollaex fetchOrderBooks() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `order book structures ` indexed by market symbol + """ + await self.load_markets() + response = await self.publicGetOrderbooks(params) + result: dict = {} + marketIds = list(response.keys()) + for i in range(0, len(marketIds)): + marketId = marketIds[i] + orderbook = response[marketId] + symbol = self.safe_symbol(marketId, None, '-') + timestamp = self.parse8601(self.safe_string(orderbook, 'timestamp')) + result[symbol] = self.parse_order_book(response[marketId], symbol, timestamp) + return result + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://apidocs.hollaex.com/#orderbook + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = await self.publicGetOrderbook(self.extend(request, params)) + # + # { + # "btc-usdt": { + # "bids": [ + # [8836.4, 1.022], + # [8800, 0.0668], + # [8797.75, 0.2398], + # ], + # "asks": [ + # [8839.35, 1.5334], + # [8852.6, 0.0579], + # [8860.45, 0.1815], + # ], + # "timestamp": "2020-03-03T02:27:25.147Z" + # }, + # "eth-usdt": {}, + # # ... + # } + # + orderbook = self.safe_value(response, market['id']) + timestamp = self.parse8601(self.safe_string(orderbook, 'timestamp')) + return self.parse_order_book(orderbook, market['symbol'], timestamp) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://apidocs.hollaex.com/#ticker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = await self.publicGetTicker(self.extend(request, params)) + # + # { + # "open": 8615.55, + # "close": 8841.05, + # "high": 8921.1, + # "low": 8607, + # "last": 8841.05, + # "volume": 20.2802, + # "timestamp": "2020-03-03T03:11:18.964Z" + # } + # + return self.parse_ticker(response, market) + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://apidocs.hollaex.com/#tickers + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + response = await self.publicGetTickers(params) + # + # { + # "bch-usdt": { + # "time": "2020-03-02T04:29:45.011Z", + # "open": 341.65, + # "close":337.9, + # "high":341.65, + # "low":337.3, + # "last":337.9, + # "volume":0.054, + # "symbol":"bch-usdt" + # }, + # # ... + # } + # + return self.parse_tickers(response, symbols) + + def parse_tickers(self, tickers, symbols: Strings = None, params={}) -> Tickers: + result: dict = {} + keys = list(tickers.keys()) + for i in range(0, len(keys)): + key = keys[i] + ticker = tickers[key] + marketId = self.safe_string(ticker, 'symbol', key) + market = self.safe_market(marketId, None, '-') + symbol = market['symbol'] + result[symbol] = self.extend(self.parse_ticker(ticker, market), params) + return self.filter_by_array_tickers(result, 'symbol', symbols) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # fetchTicker + # + # { + # "open": 8615.55, + # "close": 8841.05, + # "high": 8921.1, + # "low": 8607, + # "last": 8841.05, + # "volume": 20.2802, + # "timestamp": "2020-03-03T03:11:18.964Z", + # } + # + # fetchTickers + # + # { + # "time": "2020-03-02T04:29:45.011Z", + # "open": 341.65, + # "close": 337.9, + # "high": 341.65, + # "low": 337.3, + # "last": 337.9, + # "volume": 0.054, + # "symbol": "bch-usdt" + # } + # + marketId = self.safe_string(ticker, 'symbol') + market = self.safe_market(marketId, market, '-') + symbol = market['symbol'] + timestamp = self.parse8601(self.safe_string_2(ticker, 'time', 'timestamp')) + close = self.safe_string(ticker, 'close') + return self.safe_ticker({ + 'symbol': symbol, + 'info': ticker, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': None, + 'bidVolume': None, + 'ask': None, + 'askVolume': None, + 'vwap': None, + 'open': self.safe_string(ticker, 'open'), + 'close': close, + 'last': self.safe_string(ticker, 'last', close), + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': self.safe_string(ticker, 'volume'), + 'quoteVolume': None, + }, market) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://apidocs.hollaex.com/#trades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = await self.publicGetTrades(self.extend(request, params)) + # + # { + # "btc-usdt": [ + # { + # "size": 0.5, + # "price": 8830, + # "side": "buy", + # "timestamp": "2020-03-03T04:44:33.034Z" + # }, + # # ... + # ] + # } + # + trades = self.safe_list(response, market['id'], []) + return self.parse_trades(trades, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(public) + # + # { + # "size": 0.5, + # "price": 8830, + # "side": "buy", + # "timestamp": "2020-03-03T04:44:33.034Z" + # } + # + # fetchMyTrades(private) + # { + # "side":"sell", + # "symbol":"doge-usdt", + # "size":70, + # "price":0.147411, + # "timestamp":"2022-01-26T17:53:34.650Z", + # "order_id":"cba78ecb-4187-4da2-9d2f-c259aa693b5a", + # "fee":0.01031877, + # "fee_coin":"usdt" + # } + # + marketId = self.safe_string(trade, 'symbol') + market = self.safe_market(marketId, market, '-') + symbol = market['symbol'] + datetime = self.safe_string(trade, 'timestamp') + timestamp = self.parse8601(datetime) + side = self.safe_string(trade, 'side') + orderId = self.safe_string(trade, 'order_id') + priceString = self.safe_string(trade, 'price') + amountString = self.safe_string(trade, 'size') + feeCostString = self.safe_string(trade, 'fee') + feeCoin = self.safe_string(trade, 'fee_coin') + fee = None + if feeCostString is not None: + fee = { + 'cost': feeCostString, + 'currency': self.safe_currency_code(feeCoin), + } + return self.safe_trade({ + 'info': trade, + 'id': None, + 'timestamp': timestamp, + 'datetime': datetime, + 'symbol': symbol, + 'order': orderId, + 'type': None, + 'side': side, + 'takerOrMaker': None, + 'price': priceString, + 'amount': amountString, + 'cost': None, + 'fee': fee, + }, market) + + async def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://apidocs.hollaex.com/#tiers + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + await self.load_markets() + response = await self.publicGetTiers(params) + # + # { + # "1": { + # "id": "1", + # "name": "Silver", + # "icon": '', + # "description": "Your crypto journey starts here! Make your first deposit to start trading, and verify your account to level up!", + # "deposit_limit": "0", + # "withdrawal_limit": "1000", + # "fees": { + # "maker": { + # 'eth-btc': "0.1", + # 'ada-usdt': "0.1", + # ... + # }, + # "taker": { + # 'eth-btc': "0.1", + # 'ada-usdt': "0.1", + # ... + # } + # }, + # "note": "
    \n
  • Login and verify email
  • \n
\n", + # "created_at": "2021-03-22T03:51:39.129Z", + # "updated_at": "2021-11-01T02:51:56.214Z" + # }, + # ... + # } + # + firstTier = self.safe_value(response, '1', {}) + fees = self.safe_value(firstTier, 'fees', {}) + makerFees = self.safe_value(fees, 'maker', {}) + takerFees = self.safe_value(fees, 'taker', {}) + result: dict = {} + for i in range(0, len(self.symbols)): + symbol = self.symbols[i] + market = self.market(symbol) + makerString = self.safe_string(makerFees, market['id']) + takerString = self.safe_string(takerFees, market['id']) + result[symbol] = { + 'info': fees, + 'symbol': symbol, + 'maker': self.parse_number(Precise.string_div(makerString, '100')), + 'taker': self.parse_number(Precise.string_div(takerString, '100')), + 'percentage': True, + 'tierBased': True, + } + return result + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + hollaex has large gaps between candles, so it's recommended to specify since + + https://apidocs.hollaex.com/#chart + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch(max 500) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest candle to fetch + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'resolution': self.safe_string(self.timeframes, timeframe, timeframe), + } + paginate = False + maxLimit = 500 + paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate', paginate) + if paginate: + return await self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params, maxLimit) + until = self.safe_integer(params, 'until') + timeDelta = self.parse_timeframe(timeframe) * maxLimit * 1000 + start = since + now = self.milliseconds() + if until is None and start is None: + until = now + start = until - timeDelta + elif until is None: + until = now # the exchange has not a lot of trades, so if we count until by limit and limit is small, it may return empty result + elif start is None: + start = until - timeDelta + request['from'] = self.parse_to_int(start / 1000) # convert to seconds + request['to'] = self.parse_to_int(until / 1000) # convert to seconds + params = self.omit(params, 'until') + response = await self.publicGetChart(self.extend(request, params)) + # + # [ + # { + # "time":"2020-03-02T20:00:00.000Z", + # "close":8872.1, + # "high":8872.1, + # "low":8858.6, + # "open":8858.6, + # "symbol":"btc-usdt", + # "volume":1.2922 + # }, + # ] + # + return self.parse_ohlcvs(response, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # { + # "time":"2020-03-02T20:00:00.000Z", + # "close":8872.1, + # "high":8872.1, + # "low":8858.6, + # "open":8858.6, + # "symbol":"btc-usdt", + # "volume":1.2922 + # } + # + return [ + self.parse8601(self.safe_string(ohlcv, 'time')), + self.safe_number(ohlcv, 'open'), + self.safe_number(ohlcv, 'high'), + self.safe_number(ohlcv, 'low'), + self.safe_number(ohlcv, 'close'), + self.safe_number(ohlcv, 'volume'), + ] + + def parse_balance(self, response) -> Balances: + timestamp = self.parse8601(self.safe_string(response, 'updated_at')) + result: dict = { + 'info': response, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + currencyIds = list(self.currencies_by_id.keys()) + for i in range(0, len(currencyIds)): + currencyId = currencyIds[i] + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(response, currencyId + '_available') + account['total'] = self.safe_string(response, currencyId + '_balance') + result[code] = account + return self.safe_balance(result) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://apidocs.hollaex.com/#get-balance + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + response = await self.privateGetUserBalance(params) + # + # { + # "updated_at": "2020-03-02T22:27:38.428Z", + # "btc_balance": 0, + # "btc_pending": 0, + # "btc_available": 0, + # "eth_balance": 0, + # "eth_pending": 0, + # "eth_available": 0, + # # ... + # } + # + return self.parse_balance(response) + + async def fetch_open_order(self, id: str, symbol: Str = None, params={}): + """ + fetch an open order by it's id + + https://apidocs.hollaex.com/#get-order + + :param str id: order id + :param str symbol: not used by hollaex fetchOpenOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + request: dict = { + 'order_id': id, + } + response = await self.privateGetOrder(self.extend(request, params)) + # + # { + # "id": "string", + # "side": "sell", + # "symbol": "xht-usdt", + # "size": 0.1, + # "filled": 0, + # "stop": null, + # "fee": 0, + # "fee_coin": "usdt", + # "type": "limit", + # "price": 1.09, + # "status": "new", + # "created_by": 116, + # "created_at": "2021-02-17T02:32:38.910Z", + # "updated_at": "2021-02-17T02:32:38.910Z", + # "User": { + # "id": 116, + # "email": "fight@club.com", + # "username": "narrator", + # "exchange_id": 176 + # } + # } + # + return self.parse_order(response) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://apidocs.hollaex.com/#get-all-orders + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + request: dict = { + 'open': True, + } + return await self.fetch_orders(symbol, since, limit, self.extend(request, params)) + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://apidocs.hollaex.com/#get-all-orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + request: dict = { + 'open': False, + } + return await self.fetch_orders(symbol, since, limit, self.extend(request, params)) + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://apidocs.hollaex.com/#get-order + + :param str id: + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + request: dict = { + 'order_id': id, + } + response = await self.privateGetOrder(self.extend(request, params)) + # { + # "id": "string", + # "side": "sell", + # "symbol": "xht-usdt", + # "size": 0.1, + # "filled": 0, + # "stop": null, + # "fee": 0, + # "fee_coin": "usdt", + # "type": "limit", + # "price": 1.09, + # "status": "new", + # "created_by": 116, + # "created_at": "2021-02-17T02:32:38.910Z", + # "updated_at": "2021-02-17T02:32:38.910Z", + # "User": { + # "id": 116, + # "email": "fight@club.com", + # "username": "narrator", + # "exchange_id": 176 + # } + # } + order = response + if order is None: + raise OrderNotFound(self.id + ' fetchOrder() could not find order id ' + id) + return self.parse_order(order) + + async def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://apidocs.hollaex.com/#get-all-orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + market = None + request: dict = { + # 'symbol': market['id'], + # 'side': 'buy', # 'sell' + # 'status': 'new', # 'filled', 'pfilled', 'canceled' + # 'open': True, + # 'limit': limit, # default 50, max 100 + # 'page': 1, + # 'order_by': 'created_at', # id, ... + # 'order': 'asc', # 'desc' + # 'start_date': self.iso8601(since), + # 'end_date': self.iso8601(self.milliseconds()), + } + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if since is not None: + request['start_date'] = self.iso8601(since) + if limit is not None: + request['limit'] = limit # default 50, max 100 + response = await self.privateGetOrders(self.extend(request, params)) + # + # { + # "count": 1, + # "data": [ + # { + # "id": "string", + # "side": "sell", + # "symbol": "xht-usdt", + # "size": 0.1, + # "filled": 0, + # "stop": null, + # "fee": 0, + # "fee_coin": "usdt", + # "type": "limit", + # "price": 1.09, + # "status": "new", + # "created_by": 116, + # "created_at": "2021-02-17T02:32:38.910Z", + # "updated_at": "2021-02-17T02:32:38.910Z", + # "User": { + # "id": 116, + # "email": "fight@club.com", + # "username": "narrator", + # "exchange_id": 176 + # } + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_orders(data, market, since, limit) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'new': 'open', + 'pfilled': 'open', + 'filled': 'closed', + 'canceled': 'canceled', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder, fetchOpenOrder, fetchOpenOrders + # + # { + # "id":"10644b7e-3c90-4ba9-bc3b-188f3a4e9cfd", + # "created_by":140093, + # "exchange_id":22, + # "side":"buy", + # "symbol":"doge-usdt", + # "type":"limit", + # "price":0.05, + # "size":10, + # "stop":null, + # "filled":0, + # "status":"canceled", + # "fee":0, + # "fee_coin":"doge", + # "meta": { # optional field only returned for postOnly orders + # "post_only":true + # }, + # "fee_structure": { + # "maker":0.1, + # "taker":0.1 + # }, + # "created_at":"2022-05-31T08:14:14.747Z", + # "updated_at":"2022-05-31T08:14:23.727Z" + # } + # + marketId = self.safe_string(order, 'symbol') + symbol = self.safe_symbol(marketId, market, '-') + id = self.safe_string(order, 'id') + timestamp = self.parse8601(self.safe_string(order, 'created_at')) + type = self.safe_string(order, 'type') + side = self.safe_string(order, 'side') + price = self.safe_string(order, 'price') + amount = self.safe_string(order, 'size') + filled = self.safe_string(order, 'filled') + status = self.parse_order_status(self.safe_string(order, 'status')) + meta = self.safe_value(order, 'meta', {}) + postOnly = self.safe_bool(meta, 'post_only', False) + return self.safe_order({ + 'id': id, + 'clientOrderId': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'status': status, + 'symbol': symbol, + 'type': type, + 'timeInForce': None, + 'postOnly': postOnly, + 'side': side, + 'price': price, + 'triggerPrice': self.safe_string(order, 'stop'), + 'amount': amount, + 'filled': filled, + 'remaining': None, + 'cost': None, + 'trades': None, + 'fee': None, + 'info': order, + 'average': None, + }, market) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://apidocs.hollaex.com/#create-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: the price at which a trigger order is triggered at + :param bool [params.postOnly]: if True, the order will only be posted to the order book and not executed immediately + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'side': side, + 'size': self.amount_to_precision(symbol, amount), + 'type': type, + # 'stop': float(self.price_to_precision(symbol, stopPrice)), + # 'meta': {}, # other options such + } + triggerPrice = self.safe_number_n(params, ['triggerPrice', 'stopPrice', 'stop']) + meta = self.safe_value(params, 'meta', {}) + exchangeSpecificParam = self.safe_bool(meta, 'post_only', False) + isMarketOrder = type == 'market' + postOnly = self.is_post_only(isMarketOrder, exchangeSpecificParam, params) + if not isMarketOrder: + request['price'] = self.price_to_precision(symbol, price) + if triggerPrice is not None: + request['stop'] = self.price_to_precision(symbol, triggerPrice) + if postOnly: + request['meta'] = {'post_only': True} + params = self.omit(params, ['postOnly', 'timeInForce', 'stopPrice', 'triggerPrice', 'stop']) + response = await self.privatePostOrder(self.extend(request, params)) + # + # { + # "fee": 0, + # "meta": {}, + # "symbol": "xht-usdt", + # "side": "sell", + # "size": 0.1, + # "type": "limit", + # "price": 1, + # "fee_structure": { + # "maker": 0.2, + # "taker": 0.2 + # }, + # "fee_coin": "usdt", + # "id": "string", + # "created_by": 116, + # "filled": 0, + # "status": "new", + # "updated_at": "2021-02-17T03:03:19.231Z", + # "created_at": "2021-02-17T03:03:19.231Z", + # "stop": null + # } + # + return self.parse_order(response, market) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://apidocs.hollaex.com/#cancel-order + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + request: dict = { + 'order_id': id, + } + response = await self.privateDeleteOrder(self.extend(request, params)) + # + # { + # "title": "string", + # "symbol": "xht-usdt", + # "side": "sell", + # "size": 1, + # "type": "limit", + # "price": 0.1, + # "id": "string", + # "created_by": 34, + # "filled": 0 + # } + # + return self.parse_order(response) + + async def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders in a market + + https://apidocs.hollaex.com/#cancel-all-orders + + :param str symbol: unified market symbol of the market to cancel orders in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelAllOrders() requires a symbol argument') + await self.load_markets() + request: dict = {} + market = None + market = self.market(symbol) + request['symbol'] = market['id'] + response = await self.privateDeleteOrderAll(self.extend(request, params)) + # + # [ + # { + # "title": "string", + # "symbol": "xht-usdt", + # "side": "sell", + # "size": 1, + # "type": "limit", + # "price": 0.1, + # "id": "string", + # "created_by": 34, + # "filled": 0 + # } + # ] + # + return self.parse_orders(response, market) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://apidocs.hollaex.com/#get-trades + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + request: dict = { + # 'symbol': market['id'], + # 'limit': 50, # default 50, max 100 + # 'page': 1, # page of data to retrieve + # 'order_by': 'timestamp', # field to order data + # 'order': 'asc', # asc or desc + # 'start_date': 123, # starting date of queried data + # 'end_date': 321, # ending date of queried data + } + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if limit is not None: + request['limit'] = limit # default 50, max 100 + if since is not None: + request['start_date'] = self.iso8601(since) + response = await self.privateGetUserTrades(self.extend(request, params)) + # + # { + # "count": 1, + # "data": [ + # { + # "side": "buy", + # "symbol": "eth-usdt", + # "size": 0.086, + # "price": 226.19, + # "timestamp": "2020-03-03T08:03:55.459Z", + # "fee": 0.1 + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_trades(data, market, since, limit) + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + # + # { + # "currency":"usdt", + # "address":"TECLD9XBH31XpyykdHU3uEAeUK7E6Lrmik", + # "network":"trx", + # "standard":null, + # "is_valid":true, + # "created_at":"2021-05-12T02:43:05.446Z" + # } + # + address = self.safe_string(depositAddress, 'address') + tag = None + if address is not None: + parts = address.split(':') + address = self.safe_string(parts, 0) + tag = self.safe_string(parts, 1) + self.check_address(address) + currencyId = self.safe_string(depositAddress, 'currency') + currency = self.safe_currency(currencyId, currency) + network = self.safe_string(depositAddress, 'network') + return { + 'info': depositAddress, + 'currency': currency['code'], + 'network': network, + 'address': address, + 'tag': tag, + } + + async def fetch_deposit_addresses(self, codes: Strings = None, params={}) -> List[DepositAddress]: + """ + fetch deposit addresses for multiple currencies and chain types + + https://apidocs.hollaex.com/#get-user + + :param str[]|None codes: list of unified currency codes, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `address structures ` + """ + await self.load_markets() + network = self.safe_string(params, 'network') + params = self.omit(params, 'network') + response = await self.privateGetUser(params) + # + # { + # "id":620, + # "email":"igor.kroitor@gmail.com", + # "full_name":"", + # "gender":false, + # "nationality":"", + # "dob":null, + # "phone_number":"", + # "address":{"city":"","address":"","country":"","postal_code":""}, + # "id_data":{"note":"","type":"","number":"","status":0,"issued_date":"","expiration_date":""}, + # "bank_account":[], + # "crypto_wallet":{}, + # "verification_level":1, + # "email_verified":true, + # "otp_enabled":true, + # "activated":true, + # "username":"igor.kroitor", + # "affiliation_code":"QSWA6G", + # "settings":{ + # "chat":{"set_username":false}, + # "risk":{"popup_warning":false,"order_portfolio_percentage":20}, + # "audio":{"public_trade":false,"order_completed":true,"order_partially_completed":true}, + # "language":"en", + # "interface":{"theme":"white","order_book_levels":10}, + # "notification":{"popup_order_completed":true,"popup_order_confirmation":true,"popup_order_partially_filled":true} + # }, + # "affiliation_rate":0, + # "network_id":10620, + # "discount":0, + # "created_at":"2021-03-24T02:37:57.379Z", + # "updated_at":"2021-03-24T02:37:57.379Z", + # "balance":{ + # "btc_balance":0, + # "btc_available":0, + # "eth_balance":0.000914, + # "eth_available":0.000914, + # "updated_at":"2020-03-04T04:03:27.174Z + # "}, + # "wallet":[ + # {"currency":"usdt","address":"TECLD9XBH31XpyykdHU3uEAeUK7E6Lrmik","network":"trx","standard":null,"is_valid":true,"created_at":"2021-05-12T02:43:05.446Z"}, + # {"currency":"xrp","address":"rGcSzmuRx8qngPRnrvpCKkP9V4njeCPGCv:286741597","network":"xrp","standard":null,"is_valid":true,"created_at":"2021-05-12T02:49:01.273Z"} + # ] + # } + # + wallet = self.safe_value(response, 'wallet', []) + addresses = wallet if (network is None) else self.filter_by(wallet, 'network', network) + return self.parse_deposit_addresses(addresses, codes) + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://apidocs.hollaex.com/#get-deposits + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + request: dict = { + # 'currency': currency['id'], + # 'limit': 50, # default 50, max 100 + # 'page': 1, # page of data to retrieve + # 'order_by': 'timestamp', # field to order data + # 'order': 'asc', # asc or desc + # 'start_date': 123, # starting date of queried data + # 'end_date': 321, # ending date of queried data + } + currency = None + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + if limit is not None: + request['limit'] = limit # default 50, max 100 + if since is not None: + request['start_date'] = self.iso8601(since) + response = await self.privateGetUserDeposits(self.extend(request, params)) + # + # { + # "count": 1, + # "data": [ + # { + # "id": 539, + # "amount": 20, + # "fee": 0, + # "address": "0x5c0cc98270d7089408fcbcc8e2131287f5be2306", + # "transaction_id": "0xd4006327a5ec2c41adbdcf566eaaba6597c3d45906abe78ea1a4a022647c2e28", + # "status": True, + # "dismissed": False, + # "rejected": False, + # "description": "", + # "type": "deposit", + # "currency": "usdt", + # "created_at": "2020-03-03T07:56:36.198Z", + # "updated_at": "2020-03-03T08:00:05.674Z", + # "user_id": 620 + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_transactions(data, currency, since, limit) + + async def fetch_withdrawal(self, id: str, code: Str = None, params={}): + """ + fetch data on a currency withdrawal via the withdrawal id + + https://apidocs.hollaex.com/#get-withdrawals + + :param str id: withdrawal id + :param str code: unified currency code of the currency withdrawn, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + await self.load_markets() + request: dict = { + 'transaction_id': id, + } + currency = None + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + response = await self.privateGetUserWithdrawals(self.extend(request, params)) + # + # { + # "count": 1, + # "data": [ + # { + # "id": 539, + # "amount": 20, + # "fee": 0, + # "address": "0x5c0cc98270d7089408fcbcc8e2131287f5be2306", + # "transaction_id": "0xd4006327a5ec2c41adbdcf566eaaba6597c3d45906abe78ea1a4a022647c2e28", + # "status": True, + # "dismissed": False, + # "rejected": False, + # "description": "", + # "type": "withdrawal", + # "currency": "usdt", + # "created_at": "2020-03-03T07:56:36.198Z", + # "updated_at": "2020-03-03T08:00:05.674Z", + # "user_id": 620 + # } + # ] + # } + # + data = self.safe_value(response, 'data', []) + transaction = self.safe_dict(data, 0, {}) + return self.parse_transaction(transaction, currency) + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://apidocs.hollaex.com/#get-withdrawals + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + request: dict = { + # 'currency': currency['id'], + # 'limit': 50, # default 50, max 100 + # 'page': 1, # page of data to retrieve + # 'order_by': 'timestamp', # field to order data + # 'order': 'asc', # asc or desc + # 'start_date': 123, # starting date of queried data + # 'end_date': 321, # ending date of queried data + } + currency = None + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + if limit is not None: + request['limit'] = limit # default 50, max 100 + if since is not None: + request['start_date'] = self.iso8601(since) + response = await self.privateGetUserWithdrawals(self.extend(request, params)) + # + # { + # "count": 1, + # "data": [ + # { + # "id": 539, + # "amount": 20, + # "fee": 0, + # "address": "0x5c0cc98270d7089408fcbcc8e2131287f5be2306", + # "transaction_id": "0xd4006327a5ec2c41adbdcf566eaaba6597c3d45906abe78ea1a4a022647c2e28", + # "status": True, + # "dismissed": False, + # "rejected": False, + # "description": "", + # "type": "withdrawal", + # "currency": "usdt", + # "created_at": "2020-03-03T07:56:36.198Z", + # "updated_at": "2020-03-03T08:00:05.674Z", + # "user_id": 620 + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_transactions(data, currency, since, limit) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fetchWithdrawals, fetchDeposits + # + # { + # "id": 539, + # "amount": 20, + # "fee": 0, + # "address": "0x5c0cc98270d7089408fcbcc8e2131287f5be2306", + # "transaction_id": "0xd4006327a5ec2c41adbdcf566eaaba6597c3d45906abe78ea1a4a022647c2e28", + # "status": True, + # "dismissed": False, + # "rejected": False, + # "description": "", + # "type": "withdrawal", + # "currency": "usdt", + # "created_at": "2020-03-03T07:56:36.198Z", + # "updated_at": "2020-03-03T08:00:05.674Z", + # "user_id": 620 + # } + # + # withdraw + # + # { + # "message": "Withdrawal request is in the queue and will be processed.", + # "transaction_id": "1d1683c3-576a-4d53-8ff5-27c93fd9758a", + # "amount": 1, + # "currency": "xht", + # "fee": 0, + # "fee_coin": "xht" + # } + # + id = self.safe_string(transaction, 'id') + txid = self.safe_string(transaction, 'transaction_id') + timestamp = self.parse8601(self.safe_string(transaction, 'created_at')) + updated = self.parse8601(self.safe_string(transaction, 'updated_at')) + type = self.safe_string(transaction, 'type') + amount = self.safe_number(transaction, 'amount') + address = self.safe_string(transaction, 'address') + addressTo = None + addressFrom = None + tag = None + tagTo = None + tagFrom = None + if address is not None: + parts = address.split(':') + address = self.safe_string(parts, 0) + tag = self.safe_string(parts, 1) + addressTo = address + tagTo = tag + currencyId = self.safe_string(transaction, 'currency') + currency = self.safe_currency(currencyId, currency) + status = self.safe_value(transaction, 'status') + dismissed = self.safe_value(transaction, 'dismissed') + rejected = self.safe_value(transaction, 'rejected') + if status: + status = 'ok' + elif dismissed: + status = 'canceled' + elif rejected: + status = 'failed' + else: + status = 'pending' + feeCurrencyId = self.safe_string(transaction, 'fee_coin') + feeCurrencyCode = self.safe_currency_code(feeCurrencyId, currency) + feeCost = self.safe_number(transaction, 'fee') + fee = None + if feeCost is not None: + fee = { + 'currency': feeCurrencyCode, + 'cost': feeCost, + } + return { + 'info': transaction, + 'id': id, + 'txid': txid, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': None, + 'addressFrom': addressFrom, + 'address': address, + 'addressTo': addressTo, + 'tagFrom': tagFrom, + 'tag': tag, + 'tagTo': tagTo, + 'type': type, + 'amount': amount, + 'currency': currency['code'], + 'status': status, + 'updated': updated, + 'comment': self.safe_string(transaction, 'message'), + 'internal': None, + 'fee': fee, + } + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://apidocs.hollaex.com/#withdrawal + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.check_address(address) + await self.load_markets() + currency = self.currency(code) + if tag is not None: + address += ':' + tag + network = self.safe_string(params, 'network') + if network is None: + raise ArgumentsRequired(self.id + ' withdraw() requires a network parameter') + params = self.omit(params, 'network') + request: dict = { + 'currency': currency['id'], + 'amount': amount, + 'address': address, + 'network': self.network_code_to_id(network, code), + } + response = await self.privatePostUserWithdrawal(self.extend(request, params)) + # + # { + # "message": "Withdrawal request is in the queue and will be processed.", + # "transaction_id": "1d1683c3-576a-4d53-8ff5-27c93fd9758a", + # "amount": 1, + # "currency": "xht", + # "fee": 0, + # "fee_coin": "xht" + # } + # + return self.parse_transaction(response, currency) + + def parse_deposit_withdraw_fee(self, fee, currency: Currency = None): + # + # "bch":{ + # "id":4, + # "fullname":"Bitcoin Cash", + # "symbol":"bch", + # "active":true, + # "verified":true, + # "allow_deposit":true, + # "allow_withdrawal":true, + # "withdrawal_fee":0.0001, + # "min":0.001, + # "max":100000, + # "increment_unit":0.001, + # "logo":"https://bitholla.s3.ap-northeast-2.amazonaws.com/icon/BCH-hollaex-asset-01.svg", + # "code":"bch", + # "is_public":true, + # "meta":{}, + # "estimated_price":null, + # "description":null, + # "type":"blockchain", + # "network":null, + # "standard":null, + # "issuer":"HollaEx", + # "withdrawal_fees":null, + # "created_at":"2019-08-09T10:45:43.367Z", + # "updated_at":"2021-12-13T03:08:32.372Z", + # "created_by":1, + # "owner_id":1 + # } + # + result: dict = { + 'info': fee, + 'withdraw': { + 'fee': None, + 'percentage': None, + }, + 'deposit': { + 'fee': None, + 'percentage': None, + }, + 'networks': {}, + } + allowWithdrawal = self.safe_value(fee, 'allow_withdrawal') + if allowWithdrawal: + result['withdraw'] = {'fee': self.safe_number(fee, 'withdrawal_fee'), 'percentage': False} + withdrawalFees = self.safe_value(fee, 'withdrawal_fees') + if withdrawalFees is not None: + keys = list(withdrawalFees.keys()) + keysLength = len(keys) + for i in range(0, keysLength): + key = keys[i] + value = withdrawalFees[key] + currencyId = self.safe_string(value, 'symbol') + currencyCode = self.safe_currency_code(currencyId) + networkCode = self.network_id_to_code(key, currencyCode) + networkCodeUpper = networkCode.upper() # default to the upper case network code + withdrawalFee = self.safe_number(value, 'value') + result['networks'][networkCodeUpper] = { + 'deposit': None, + 'withdraw': withdrawalFee, + } + return result + + async def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}): + """ + fetch deposit and withdraw fees + + https://apidocs.hollaex.com/#constants + + :param str[]|None codes: list of unified currency codes + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `fee structures ` + """ + response = await self.publicGetConstants(params) + # + # { + # "coins":{ + # "bch":{ + # "id":4, + # "fullname":"Bitcoin Cash", + # "symbol":"bch", + # "active":true, + # "verified":true, + # "allow_deposit":true, + # "allow_withdrawal":true, + # "withdrawal_fee":0.0001, + # "min":0.001, + # "max":100000, + # "increment_unit":0.001, + # "logo":"https://bitholla.s3.ap-northeast-2.amazonaws.com/icon/BCH-hollaex-asset-01.svg", + # "code":"bch", + # "is_public":true, + # "meta":{}, + # "estimated_price":null, + # "description":null, + # "type":"blockchain", + # "network":null, + # "standard":null, + # "issuer":"HollaEx", + # "withdrawal_fees":null, + # "created_at":"2019-08-09T10:45:43.367Z", + # "updated_at":"2021-12-13T03:08:32.372Z", + # "created_by":1, + # "owner_id":1 + # }, + # }, + # "network":"https://api.hollaex.network" + # } + # + coins = self.safe_dict(response, 'coins', {}) + return self.parse_deposit_withdraw_fees(coins, codes, 'symbol') + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + query = self.omit(params, self.extract_params(path)) + path = '/' + self.version + '/' + self.implode_params(path, params) + if (method == 'GET') or (method == 'DELETE'): + if query: + path += '?' + self.urlencode(query) + url = self.urls['api']['rest'] + path + if api == 'private': + self.check_required_credentials() + defaultExpires = self.safe_integer_2(self.options, 'api-expires', 'expires', self.parse_to_int(self.timeout / 1000)) + expires = self.sum(self.seconds(), defaultExpires) + expiresString = str(expires) + auth = method + path + expiresString + headers = { + 'api-key': self.apiKey, + 'api-expires': expiresString, + } + if method == 'POST': + headers['Content-type'] = 'application/json' + if query: + body = self.json(query) + auth += body + signature = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256) + headers['api-signature'] = signature + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + # {"message": "Invalid token"} + if response is None: + return None + if (code >= 400) and (code <= 503): + # + # {"message": "Invalid token"} + # + # different errors return the same code eg + # + # {"message":"Error 1001 - Order rejected. Order could not be submitted order was set to a post only order."} + # + # {"message":"Error 1001 - POST ONLY order can not be of type market"} + # + feedback = self.id + ' ' + body + message = self.safe_string(response, 'message') + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + status = str(code) + self.throw_exactly_matched_exception(self.exceptions['exact'], status, feedback) + return None diff --git a/ccxt/async_support/htx.py b/ccxt/async_support/htx.py new file mode 100644 index 0000000..00acdc5 --- /dev/null +++ b/ccxt/async_support/htx.py @@ -0,0 +1,8983 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.htx import ImplicitAPI +import asyncio +import hashlib +from ccxt.base.types import Account, Any, Balances, BorrowInterest, Currencies, Currency, DepositAddress, Int, IsolatedBorrowRate, IsolatedBorrowRates, LedgerEntry, LeverageTier, LeverageTiers, Market, Num, Order, OrderBook, OrderRequest, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, FundingRates, Trade, TradingFeeInterface, Transaction, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import AccountNotEnabled +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import NotSupported +from ccxt.base.errors import OperationFailed +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.errors import OnMaintenance +from ccxt.base.errors import RequestTimeout +from ccxt.base.decimal_to_precision import TRUNCATE +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class htx(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(htx, self).describe(), { + 'id': 'htx', + 'name': 'HTX', + 'countries': ['CN'], + 'rateLimit': 100, + 'userAgent': self.userAgents['chrome100'], + 'certified': True, + 'version': 'v1', + 'hostname': 'api.huobi.pro', # api.testnet.huobi.pro + 'pro': True, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': True, + 'swap': True, + 'future': True, + 'option': None, + 'addMargin': None, + 'borrowCrossMargin': True, + 'borrowIsolatedMargin': True, + 'cancelAllOrders': True, + 'cancelAllOrdersAfter': True, + 'cancelOrder': True, + 'cancelOrders': True, + 'closeAllPositions': False, + 'closePosition': True, + 'createDepositAddress': None, + 'createMarketBuyOrderWithCost': True, + 'createMarketOrderWithCost': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createOrders': True, + 'createReduceOnlyOrder': False, + 'createStopLimitOrder': True, + 'createStopLossOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'createTakeProfitOrder': True, + 'createTrailingPercentOrder': True, + 'createTriggerOrder': True, + 'fetchAccounts': True, + 'fetchBalance': True, + 'fetchBidsAsks': None, + 'fetchBorrowInterest': True, + 'fetchBorrowRateHistories': None, + 'fetchBorrowRateHistory': None, + 'fetchCanceledOrders': None, + 'fetchClosedOrder': None, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDeposit': None, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': None, + 'fetchDepositAddressesByNetwork': True, + 'fetchDeposits': True, + 'fetchDepositWithdrawFee': 'emulated', + 'fetchDepositWithdrawFees': True, + 'fetchFundingHistory': True, + 'fetchFundingRate': True, + 'fetchFundingRateHistory': True, + 'fetchFundingRates': True, + 'fetchIndexOHLCV': True, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': True, + 'fetchL3OrderBook': None, + 'fetchLastPrices': True, + 'fetchLedger': True, + 'fetchLedgerEntry': None, + 'fetchLeverage': False, + 'fetchLeverageTiers': True, + 'fetchLiquidations': True, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarketLeverageTiers': 'emulated', + 'fetchMarkets': True, + 'fetchMarkOHLCV': True, + 'fetchMyLiquidations': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': True, + 'fetchOpenInterestHistory': True, + 'fetchOpenInterests': True, + 'fetchOpenOrder': None, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrderBooks': None, + 'fetchOrders': True, + 'fetchOrderTrades': True, + 'fetchPosition': True, + 'fetchPositionHistory': 'emulated', + 'fetchPositions': True, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': True, + 'fetchSettlementHistory': True, + 'fetchStatus': True, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': True, + 'fetchTradingFees': False, + 'fetchTradingLimits': True, + 'fetchTransactionFee': None, + 'fetchTransactionFees': None, + 'fetchTransactions': None, + 'fetchTransfers': None, + 'fetchWithdrawAddresses': True, + 'fetchWithdrawal': None, + 'fetchWithdrawals': True, + 'fetchWithdrawalWhitelist': None, + 'reduceMargin': None, + 'repayCrossMargin': True, + 'repayIsolatedMargin': True, + 'setLeverage': True, + 'setMarginMode': False, + 'setPositionMode': True, + 'signIn': None, + 'transfer': True, + 'withdraw': True, + }, + 'timeframes': { + '1m': '1min', + '5m': '5min', + '15m': '15min', + '30m': '30min', + '1h': '60min', + '4h': '4hour', + '1d': '1day', + '1w': '1week', + '1M': '1mon', + '1y': '1year', + }, + 'urls': { + # 'test': { + # 'market': 'https://api.testnet.huobi.pro', + # 'public': 'https://api.testnet.huobi.pro', + # 'private': 'https://api.testnet.huobi.pro', + # }, + 'logo': 'https://user-images.githubusercontent.com/1294454/76137448-22748a80-604e-11ea-8069-6e389271911d.jpg', + 'hostnames': { + 'contract': 'api.hbdm.com', + 'spot': 'api.huobi.pro', + 'status': { + 'spot': 'status.huobigroup.com', + 'future': { + 'inverse': 'status-dm.huobigroup.com', + 'linear': 'status-linear-swap.huobigroup.com', # USDT-Margined Contracts + }, + 'swap': { + 'inverse': 'status-swap.huobigroup.com', + 'linear': 'status-linear-swap.huobigroup.com', # USDT-Margined Contracts + }, + }, + # recommended for AWS + # 'contract': 'api.hbdm.vn', + # 'spot': 'api-aws.huobi.pro', + }, + 'api': { + 'status': 'https://{hostname}', + 'contract': 'https://{hostname}', + 'spot': 'https://{hostname}', + 'public': 'https://{hostname}', + 'private': 'https://{hostname}', + 'v2Public': 'https://{hostname}', + 'v2Private': 'https://{hostname}', + }, + 'www': 'https://www.huobi.com', + 'referral': { + 'url': 'https://www.htx.com.vc/invite/en-us/1h?invite_code=6rmm2223', + 'discount': 0.15, + }, + 'doc': [ + 'https://huobiapi.github.io/docs/spot/v1/en/', + 'https://huobiapi.github.io/docs/dm/v1/en/', + 'https://huobiapi.github.io/docs/coin_margined_swap/v1/en/', + 'https://huobiapi.github.io/docs/usdt_swap/v1/en/', + 'https://www.huobi.com/en-us/opend/newApiPages/', + ], + 'fees': 'https://www.huobi.com/about/fee/', + }, + 'api': { + # ------------------------------------------------------------ + # old api definitions + 'v2Public': { + 'get': { + 'reference/currencies': 1, # 币链参考信息 + 'market-status': 1, # 获取当前市场状态 + }, + }, + 'v2Private': { + 'get': { + 'account/ledger': 1, + 'account/withdraw/quota': 1, + 'account/withdraw/address': 1, # 提币地址查询(限母用户可用) + 'account/deposit/address': 1, + 'account/repayment': 5, # 还币交易记录查询 + 'reference/transact-fee-rate': 1, + 'account/asset-valuation': 0.2, # 获取账户资产估值 + 'point/account': 5, # 点卡余额查询 + 'sub-user/user-list': 1, # 获取子用户列表 + 'sub-user/user-state': 1, # 获取特定子用户的用户状态 + 'sub-user/account-list': 1, # 获取特定子用户的账户列表 + 'sub-user/deposit-address': 1, # 子用户充币地址查询 + 'sub-user/query-deposit': 1, # 子用户充币记录查询 + 'user/api-key': 1, # 母子用户API key信息查询 + 'user/uid': 1, # 母子用户获取用户UID + 'algo-orders/opening': 1, # 查询未触发OPEN策略委托 + 'algo-orders/history': 1, # 查询策略委托历史 + 'algo-orders/specific': 1, # 查询特定策略委托 + 'c2c/offers': 1, # 查询借入借出订单 + 'c2c/offer': 1, # 查询特定借入借出订单及其交易记录 + 'c2c/transactions': 1, # 查询借入借出交易记录 + 'c2c/repayment': 1, # 查询还币交易记录 + 'c2c/account': 1, # 查询账户余额 + 'etp/reference': 1, # 基础参考信息 + 'etp/transactions': 5, # 获取杠杆ETP申赎记录 + 'etp/transaction': 5, # 获取特定杠杆ETP申赎记录 + 'etp/rebalance': 1, # 获取杠杆ETP调仓记录 + 'etp/limit': 1, # 获取ETP持仓限额 + }, + 'post': { + 'account/transfer': 1, + 'account/repayment': 5, # 归还借币(全仓逐仓通用) + 'point/transfer': 5, # 点卡划转 + 'sub-user/management': 1, # 冻结/解冻子用户 + 'sub-user/creation': 1, # 子用户创建 + 'sub-user/tradable-market': 1, # 设置子用户交易权限 + 'sub-user/transferability': 1, # 设置子用户资产转出权限 + 'sub-user/api-key-generation': 1, # 子用户API key创建 + 'sub-user/api-key-modification': 1, # 修改子用户API key + 'sub-user/api-key-deletion': 1, # 删除子用户API key + 'sub-user/deduct-mode': 1, # 设置子用户手续费抵扣模式 + 'algo-orders': 1, # 策略委托下单 + 'algo-orders/cancel-all-after': 1, # 自动撤销订单 + 'algo-orders/cancellation': 1, # 策略委托(触发前)撤单 + 'c2c/offer': 1, # 借入借出下单 + 'c2c/cancellation': 1, # 借入借出撤单 + 'c2c/cancel-all': 1, # 撤销所有借入借出订单 + 'c2c/repayment': 1, # 还币 + 'c2c/transfer': 1, # 资产划转 + 'etp/creation': 5, # 杠杆ETP换入 + 'etp/redemption': 5, # 杠杆ETP换出 + 'etp/{transactId}/cancel': 10, # 杠杆ETP单个撤单 + 'etp/batch-cancel': 50, # 杠杆ETP批量撤单 + }, + }, + 'public': { + 'get': { + 'common/symbols': 1, # 查询系统支持的所有交易对 + 'common/currencys': 1, # 查询系统支持的所有币种 + 'common/timestamp': 1, # 查询系统当前时间 + 'common/exchange': 1, # order limits + 'settings/currencys': 1, # ?language=en-US + }, + }, + 'private': { + 'get': { + 'account/accounts': 0.2, # 查询当前用户的所有账户(即account-id) + 'account/accounts/{id}/balance': 0.2, # 查询指定账户的余额 + 'account/accounts/{sub-uid}': 1, + 'account/history': 4, + 'cross-margin/loan-info': 1, + 'margin/loan-info': 1, # 查询借币币息率及额度 + 'fee/fee-rate/get': 1, + 'order/openOrders': 0.4, + 'order/orders': 0.4, + 'order/orders/{id}': 0.4, # 查询某个订单详情 + 'order/orders/{id}/matchresults': 0.4, # 查询某个订单的成交明细 + 'order/orders/getClientOrder': 0.4, + 'order/history': 1, # 查询当前委托、历史委托 + 'order/matchresults': 1, # 查询当前成交、历史成交 + # 'dw/withdraw-virtual/addresses', # 查询虚拟币提现地址(Deprecated) + 'query/deposit-withdraw': 1, + # 'margin/loan-info', # duplicate + 'margin/loan-orders': 0.2, # 借贷订单 + 'margin/accounts/balance': 0.2, # 借贷账户详情 + 'cross-margin/loan-orders': 1, # 查询借币订单 + 'cross-margin/accounts/balance': 1, # 借币账户详情 + 'points/actions': 1, + 'points/orders': 1, + 'subuser/aggregate-balance': 10, + 'stable-coin/exchange_rate': 1, + 'stable-coin/quote': 1, + }, + 'post': { + 'account/transfer': 1, # 资产划转(该节点为母用户和子用户进行资产划转的通用接口。) + 'futures/transfer': 1, + 'order/batch-orders': 0.4, + 'order/orders/place': 0.2, # 创建并执行一个新订单(一步下单, 推荐使用) + 'order/orders/submitCancelClientOrder': 0.2, + 'order/orders/batchCancelOpenOrders': 0.4, + # 'order/orders', # 创建一个新的订单请求 (仅创建订单,不执行下单) + # 'order/orders/{id}/place', # 执行一个订单 (仅执行已创建的订单) + 'order/orders/{id}/submitcancel': 0.2, # 申请撤销一个订单请求 + 'order/orders/batchcancel': 0.4, # 批量撤销订单 + # 'dw/balance/transfer', # 资产划转 + 'dw/withdraw/api/create': 1, # 申请提现虚拟币 + # 'dw/withdraw-virtual/create', # 申请提现虚拟币 + # 'dw/withdraw-virtual/{id}/place', # 确认申请虚拟币提现(Deprecated) + 'dw/withdraw-virtual/{id}/cancel': 1, # 申请取消提现虚拟币 + 'dw/transfer-in/margin': 10, # 现货账户划入至借贷账户 + 'dw/transfer-out/margin': 10, # 借贷账户划出至现货账户 + 'margin/orders': 10, # 申请借贷 + 'margin/orders/{id}/repay': 10, # 归还借贷 + 'cross-margin/transfer-in': 1, # 资产划转 + 'cross-margin/transfer-out': 1, # 资产划转 + 'cross-margin/orders': 1, # 申请借币 + 'cross-margin/orders/{id}/repay': 1, # 归还借币 + 'stable-coin/exchange': 1, + 'subuser/transfer': 10, + }, + }, + # ------------------------------------------------------------ + # new api definitions + # 'https://status.huobigroup.com/api/v2/summary.json': 1, + # 'https://status-dm.huobigroup.com/api/v2/summary.json': 1, + # 'https://status-swap.huobigroup.com/api/v2/summary.json': 1, + # 'https://status-linear-swap.huobigroup.com/api/v2/summary.json': 1, + 'status': { + 'public': { + 'spot': { + 'get': { + 'api/v2/summary.json': 1, + }, + }, + 'future': { + 'inverse': { + 'get': { + 'api/v2/summary.json': 1, + }, + }, + 'linear': { + 'get': { + 'api/v2/summary.json': 1, + }, + }, + }, + 'swap': { + 'inverse': { + 'get': { + 'api/v2/summary.json': 1, + }, + }, + 'linear': { + 'get': { + 'api/v2/summary.json': 1, + }, + }, + }, + }, + }, + 'spot': { + 'public': { + 'get': { + 'v2/market-status': 1, + 'v1/common/symbols': 1, + 'v1/common/currencys': 1, + 'v2/settings/common/currencies': 1, + 'v2/reference/currencies': 1, + 'v1/common/timestamp': 1, + 'v1/common/exchange': 1, # order limits + 'v1/settings/common/chains': 1, + 'v1/settings/common/currencys': 1, + 'v1/settings/common/symbols': 1, + 'v2/settings/common/symbols': 1, + 'v1/settings/common/market-symbols': 1, + # Market Data + 'market/history/candles': 1, + 'market/history/kline': 1, + 'market/detail/merged': 1, + 'market/tickers': 1, + 'market/detail': 1, + 'market/depth': 1, + 'market/trade': 1, + 'market/history/trade': 1, + 'market/etp': 1, # Get real-time equity of leveraged ETP + # ETP + 'v2/etp/reference': 1, + 'v2/etp/rebalance': 1, + }, + }, + 'private': { + 'get': { + # Account + 'v1/account/accounts': 0.2, + 'v1/account/accounts/{account-id}/balance': 0.2, + 'v2/account/valuation': 1, + 'v2/account/asset-valuation': 0.2, + 'v1/account/history': 4, + 'v2/account/ledger': 1, + 'v2/point/account': 5, + # Wallet(Deposit and Withdraw) + 'v2/account/deposit/address': 1, + 'v2/account/withdraw/quota': 1, + 'v2/account/withdraw/address': 1, + 'v2/reference/currencies': 1, + 'v1/query/deposit-withdraw': 1, + 'v1/query/withdraw/client-order-id': 1, + # Sub user management + 'v2/user/api-key': 1, + 'v2/user/uid': 1, + 'v2/sub-user/user-list': 1, + 'v2/sub-user/user-state': 1, + 'v2/sub-user/account-list': 1, + 'v2/sub-user/deposit-address': 1, + 'v2/sub-user/query-deposit': 1, + 'v1/subuser/aggregate-balance': 10, + 'v1/account/accounts/{sub-uid}': 1, + # Trading + 'v1/order/openOrders': 0.4, + 'v1/order/orders/{order-id}': 0.4, + 'v1/order/orders/getClientOrder': 0.4, + 'v1/order/orders/{order-id}/matchresult': 0.4, + 'v1/order/orders/{order-id}/matchresults': 0.4, + 'v1/order/orders': 0.4, + 'v1/order/history': 1, + 'v1/order/matchresults': 1, + 'v2/reference/transact-fee-rate': 1, + # Conditional Order + 'v2/algo-orders/opening': 1, + 'v2/algo-orders/history': 1, + 'v2/algo-orders/specific': 1, + # Margin Loan(Cross/Isolated) + 'v1/margin/loan-info': 1, + 'v1/margin/loan-orders': 0.2, + 'v1/margin/accounts/balance': 0.2, + 'v1/cross-margin/loan-info': 1, + 'v1/cross-margin/loan-orders': 1, + 'v1/cross-margin/accounts/balance': 1, + 'v2/account/repayment': 5, + # Stable Coin Exchange + 'v1/stable-coin/quote': 1, + 'v1/stable_coin/exchange_rate': 1, + # ETP + 'v2/etp/transactions': 5, + 'v2/etp/transaction': 5, + 'v2/etp/limit': 1, + }, + 'post': { + # Account + 'v1/account/transfer': 1, + 'v1/futures/transfer': 1, # future transfers + 'v2/point/transfer': 5, + 'v2/account/transfer': 1, # swap transfers + # Wallet(Deposit and Withdraw) + 'v1/dw/withdraw/api/create': 1, + 'v1/dw/withdraw-virtual/{withdraw-id}/cancel': 1, + # Sub user management + 'v2/sub-user/deduct-mode': 1, + 'v2/sub-user/creation': 1, + 'v2/sub-user/management': 1, + 'v2/sub-user/tradable-market': 1, + 'v2/sub-user/transferability': 1, + 'v2/sub-user/api-key-generation': 1, + 'v2/sub-user/api-key-modification': 1, + 'v2/sub-user/api-key-deletion': 1, + 'v1/subuser/transfer': 10, + 'v1/trust/user/active/credit': 10, + # Trading + 'v1/order/orders/place': 0.2, + 'v1/order/batch-orders': 0.4, + 'v1/order/auto/place': 0.2, + 'v1/order/orders/{order-id}/submitcancel': 0.2, + 'v1/order/orders/submitCancelClientOrder': 0.2, + 'v1/order/orders/batchCancelOpenOrders': 0.4, + 'v1/order/orders/batchcancel': 0.4, + 'v2/algo-orders/cancel-all-after': 1, + # Conditional Order + 'v2/algo-orders': 1, + 'v2/algo-orders/cancellation': 1, + # Margin Loan(Cross/Isolated) + 'v2/account/repayment': 5, + 'v1/dw/transfer-in/margin': 10, + 'v1/dw/transfer-out/margin': 10, + 'v1/margin/orders': 10, + 'v1/margin/orders/{order-id}/repay': 10, + 'v1/cross-margin/transfer-in': 1, + 'v1/cross-margin/transfer-out': 1, + 'v1/cross-margin/orders': 1, + 'v1/cross-margin/orders/{order-id}/repay': 1, + # Stable Coin Exchange + 'v1/stable-coin/exchange': 1, + # ETP + 'v2/etp/creation': 5, + 'v2/etp/redemption': 5, + 'v2/etp/{transactId}/cancel': 10, + 'v2/etp/batch-cancel': 50, + }, + }, + }, + 'contract': { + 'public': { + 'get': { + 'api/v1/timestamp': 1, + 'heartbeat/': 1, # backslash is not a typo + # Future Market Data interface + 'api/v1/contract_contract_info': 1, + 'api/v1/contract_index': 1, + 'api/v1/contract_query_elements': 1, + 'api/v1/contract_price_limit': 1, + 'api/v1/contract_open_interest': 1, + 'api/v1/contract_delivery_price': 1, + 'market/depth': 1, + 'market/bbo': 1, + 'market/history/kline': 1, + 'index/market/history/mark_price_kline': 1, + 'market/detail/merged': 1, + 'market/detail/batch_merged': 1, + 'v2/market/detail/batch_merged': 1, + 'market/trade': 1, + 'market/history/trade': 1, + 'api/v1/contract_risk_info': 1, + 'api/v1/contract_insurance_fund': 1, + 'api/v1/contract_adjustfactor': 1, + 'api/v1/contract_his_open_interest': 1, + 'api/v1/contract_ladder_margin': 1, + 'api/v1/contract_api_state': 1, + 'api/v1/contract_elite_account_ratio': 1, + 'api/v1/contract_elite_position_ratio': 1, + 'api/v1/contract_liquidation_orders': 1, + 'api/v1/contract_settlement_records': 1, + 'index/market/history/index': 1, + 'index/market/history/basis': 1, + 'api/v1/contract_estimated_settlement_price': 1, + 'api/v3/contract_liquidation_orders': 1, + # Swap Market Data interface + 'swap-api/v1/swap_contract_info': 1, + 'swap-api/v1/swap_index': 1, + 'swap-api/v1/swap_query_elements': 1, + 'swap-api/v1/swap_price_limit': 1, + 'swap-api/v1/swap_open_interest': 1, + 'swap-ex/market/depth': 1, + 'swap-ex/market/bbo': 1, + 'swap-ex/market/history/kline': 1, + 'index/market/history/swap_mark_price_kline': 1, + 'swap-ex/market/detail/merged': 1, + 'v2/swap-ex/market/detail/batch_merged': 1, + 'index/market/history/swap_premium_index_kline': 1, + 'swap-ex/market/detail/batch_merged': 1, + 'swap-ex/market/trade': 1, + 'swap-ex/market/history/trade': 1, + 'swap-api/v1/swap_risk_info': 1, + 'swap-api/v1/swap_insurance_fund': 1, + 'swap-api/v1/swap_adjustfactor': 1, + 'swap-api/v1/swap_his_open_interest': 1, + 'swap-api/v1/swap_ladder_margin': 1, + 'swap-api/v1/swap_api_state': 1, + 'swap-api/v1/swap_elite_account_ratio': 1, + 'swap-api/v1/swap_elite_position_ratio': 1, + 'swap-api/v1/swap_estimated_settlement_price': 1, + 'swap-api/v1/swap_liquidation_orders': 1, + 'swap-api/v1/swap_settlement_records': 1, + 'swap-api/v1/swap_funding_rate': 1, + 'swap-api/v1/swap_batch_funding_rate': 1, + 'swap-api/v1/swap_historical_funding_rate': 1, + 'swap-api/v3/swap_liquidation_orders': 1, + 'index/market/history/swap_estimated_rate_kline': 1, + 'index/market/history/swap_basis': 1, + # Swap Market Data interface + 'linear-swap-api/v1/swap_contract_info': 1, + 'linear-swap-api/v1/swap_index': 1, + 'linear-swap-api/v1/swap_query_elements': 1, + 'linear-swap-api/v1/swap_price_limit': 1, + 'linear-swap-api/v1/swap_open_interest': 1, + 'linear-swap-ex/market/depth': 1, + 'linear-swap-ex/market/bbo': 1, + 'linear-swap-ex/market/history/kline': 1, + 'index/market/history/linear_swap_mark_price_kline': 1, + 'linear-swap-ex/market/detail/merged': 1, + 'linear-swap-ex/market/detail/batch_merged': 1, + 'v2/linear-swap-ex/market/detail/batch_merged': 1, + 'linear-swap-ex/market/trade': 1, + 'linear-swap-ex/market/history/trade': 1, + 'linear-swap-api/v1/swap_risk_info': 1, + 'swap-api/v1/linear-swap-api/v1/swap_insurance_fund': 1, + 'linear-swap-api/v1/swap_adjustfactor': 1, + 'linear-swap-api/v1/swap_cross_adjustfactor': 1, + 'linear-swap-api/v1/swap_his_open_interest': 1, + 'linear-swap-api/v1/swap_ladder_margin': 1, + 'linear-swap-api/v1/swap_cross_ladder_margin': 1, + 'linear-swap-api/v1/swap_api_state': 1, + 'linear-swap-api/v1/swap_cross_transfer_state': 1, + 'linear-swap-api/v1/swap_cross_trade_state': 1, + 'linear-swap-api/v1/swap_elite_account_ratio': 1, + 'linear-swap-api/v1/swap_elite_position_ratio': 1, + 'linear-swap-api/v1/swap_liquidation_orders': 1, + 'linear-swap-api/v1/swap_settlement_records': 1, + 'linear-swap-api/v1/swap_funding_rate': 1, + 'linear-swap-api/v1/swap_batch_funding_rate': 1, + 'linear-swap-api/v1/swap_historical_funding_rate': 1, + 'linear-swap-api/v3/swap_liquidation_orders': 1, + 'index/market/history/linear_swap_premium_index_kline': 1, + 'index/market/history/linear_swap_estimated_rate_kline': 1, + 'index/market/history/linear_swap_basis': 1, + 'linear-swap-api/v1/swap_estimated_settlement_price': 1, + }, + }, + 'private': { + 'get': { + # Future Account Interface + 'api/v1/contract_sub_auth_list': 1, + 'api/v1/contract_api_trading_status': 1, + # Swap Account Interface + 'swap-api/v1/swap_sub_auth_list': 1, + 'swap-api/v1/swap_api_trading_status': 1, + # Swap Account Interface + 'linear-swap-api/v1/swap_sub_auth_list': 1, + 'linear-swap-api/v1/swap_api_trading_status': 1, + 'linear-swap-api/v1/swap_cross_position_side': 1, + 'linear-swap-api/v1/swap_position_side': 1, + 'linear-swap-api/v3/unified_account_info': 1, + 'linear-swap-api/v3/fix_position_margin_change_record': 1, + 'linear-swap-api/v3/swap_unified_account_type': 1, + 'linear-swap-api/v3/linear_swap_overview_account_info': 1, + }, + 'post': { + # Future Account Interface + 'api/v1/contract_balance_valuation': 1, + 'api/v1/contract_account_info': 1, + 'api/v1/contract_position_info': 1, + 'api/v1/contract_sub_auth': 1, + 'api/v1/contract_sub_account_list': 1, + 'api/v1/contract_sub_account_info_list': 1, + 'api/v1/contract_sub_account_info': 1, + 'api/v1/contract_sub_position_info': 1, + 'api/v1/contract_financial_record': 1, + 'api/v1/contract_financial_record_exact': 1, + 'api/v1/contract_user_settlement_records': 1, + 'api/v1/contract_order_limit': 1, + 'api/v1/contract_fee': 1, + 'api/v1/contract_transfer_limit': 1, + 'api/v1/contract_position_limit': 1, + 'api/v1/contract_account_position_info': 1, + 'api/v1/contract_master_sub_transfer': 1, + 'api/v1/contract_master_sub_transfer_record': 1, + 'api/v1/contract_available_level_rate': 1, + 'api/v3/contract_financial_record': 1, + 'api/v3/contract_financial_record_exact': 1, + # Future Trade Interface + 'api/v1/contract-cancel-after': 1, + 'api/v1/contract_order': 1, + 'api/v1/contract_batchorder': 1, + 'api/v1/contract_cancel': 1, + 'api/v1/contract_cancelall': 1, + 'api/v1/contract_switch_lever_rate': 30, + 'api/v1/lightning_close_position': 1, + 'api/v1/contract_order_info': 1, + 'api/v1/contract_order_detail': 1, + 'api/v1/contract_openorders': 1, + 'api/v1/contract_hisorders': 1, + 'api/v1/contract_hisorders_exact': 1, + 'api/v1/contract_matchresults': 1, + 'api/v1/contract_matchresults_exact': 1, + 'api/v3/contract_hisorders': 1, + 'api/v3/contract_hisorders_exact': 1, + 'api/v3/contract_matchresults': 1, + 'api/v3/contract_matchresults_exact': 1, + # Contract Strategy Order Interface + 'api/v1/contract_trigger_order': 1, + 'api/v1/contract_trigger_cancel': 1, + 'api/v1/contract_trigger_cancelall': 1, + 'api/v1/contract_trigger_openorders': 1, + 'api/v1/contract_trigger_hisorders': 1, + 'api/v1/contract_tpsl_order': 1, + 'api/v1/contract_tpsl_cancel': 1, + 'api/v1/contract_tpsl_cancelall': 1, + 'api/v1/contract_tpsl_openorders': 1, + 'api/v1/contract_tpsl_hisorders': 1, + 'api/v1/contract_relation_tpsl_order': 1, + 'api/v1/contract_track_order': 1, + 'api/v1/contract_track_cancel': 1, + 'api/v1/contract_track_cancelall': 1, + 'api/v1/contract_track_openorders': 1, + 'api/v1/contract_track_hisorders': 1, + # Swap Account Interface + 'swap-api/v1/swap_balance_valuation': 1, + 'swap-api/v1/swap_account_info': 1, + 'swap-api/v1/swap_position_info': 1, + 'swap-api/v1/swap_account_position_info': 1, + 'swap-api/v1/swap_sub_auth': 1, + 'swap-api/v1/swap_sub_account_list': 1, + 'swap-api/v1/swap_sub_account_info_list': 1, + 'swap-api/v1/swap_sub_account_info': 1, + 'swap-api/v1/swap_sub_position_info': 1, + 'swap-api/v1/swap_financial_record': 1, + 'swap-api/v1/swap_financial_record_exact': 1, + 'swap-api/v1/swap_user_settlement_records': 1, + 'swap-api/v1/swap_available_level_rate': 1, + 'swap-api/v1/swap_order_limit': 1, + 'swap-api/v1/swap_fee': 1, + 'swap-api/v1/swap_transfer_limit': 1, + 'swap-api/v1/swap_position_limit': 1, + 'swap-api/v1/swap_master_sub_transfer': 1, + 'swap-api/v1/swap_master_sub_transfer_record': 1, + 'swap-api/v3/swap_financial_record': 1, + 'swap-api/v3/swap_financial_record_exact': 1, + # Swap Trade Interface + 'swap-api/v1/swap-cancel-after': 1, + 'swap-api/v1/swap_order': 1, + 'swap-api/v1/swap_batchorder': 1, + 'swap-api/v1/swap_cancel': 1, + 'swap-api/v1/swap_cancelall': 1, + 'swap-api/v1/swap_lightning_close_position': 1, + 'swap-api/v1/swap_switch_lever_rate': 30, + 'swap-api/v1/swap_order_info': 1, + 'swap-api/v1/swap_order_detail': 1, + 'swap-api/v1/swap_openorders': 1, + 'swap-api/v1/swap_hisorders': 1, + 'swap-api/v1/swap_hisorders_exact': 1, + 'swap-api/v1/swap_matchresults': 1, + 'swap-api/v1/swap_matchresults_exact': 1, + 'swap-api/v3/swap_matchresults': 1, + 'swap-api/v3/swap_matchresults_exact': 1, + 'swap-api/v3/swap_hisorders': 1, + 'swap-api/v3/swap_hisorders_exact': 1, + # Swap Strategy Order Interface + 'swap-api/v1/swap_trigger_order': 1, + 'swap-api/v1/swap_trigger_cancel': 1, + 'swap-api/v1/swap_trigger_cancelall': 1, + 'swap-api/v1/swap_trigger_openorders': 1, + 'swap-api/v1/swap_trigger_hisorders': 1, + 'swap-api/v1/swap_tpsl_order': 1, + 'swap-api/v1/swap_tpsl_cancel': 1, + 'swap-api/v1/swap_tpsl_cancelall': 1, + 'swap-api/v1/swap_tpsl_openorders': 1, + 'swap-api/v1/swap_tpsl_hisorders': 1, + 'swap-api/v1/swap_relation_tpsl_order': 1, + 'swap-api/v1/swap_track_order': 1, + 'swap-api/v1/swap_track_cancel': 1, + 'swap-api/v1/swap_track_cancelall': 1, + 'swap-api/v1/swap_track_openorders': 1, + 'swap-api/v1/swap_track_hisorders': 1, + # Swap Account Interface + 'linear-swap-api/v1/swap_lever_position_limit': 1, + 'linear-swap-api/v1/swap_cross_lever_position_limit': 1, + 'linear-swap-api/v1/swap_balance_valuation': 1, + 'linear-swap-api/v1/swap_account_info': 1, + 'linear-swap-api/v1/swap_cross_account_info': 1, + 'linear-swap-api/v1/swap_position_info': 1, + 'linear-swap-api/v1/swap_cross_position_info': 1, + 'linear-swap-api/v1/swap_account_position_info': 1, + 'linear-swap-api/v1/swap_cross_account_position_info': 1, + 'linear-swap-api/v1/swap_sub_auth': 1, + 'linear-swap-api/v1/swap_sub_account_list': 1, + 'linear-swap-api/v1/swap_cross_sub_account_list': 1, + 'linear-swap-api/v1/swap_sub_account_info_list': 1, + 'linear-swap-api/v1/swap_cross_sub_account_info_list': 1, + 'linear-swap-api/v1/swap_sub_account_info': 1, + 'linear-swap-api/v1/swap_cross_sub_account_info': 1, + 'linear-swap-api/v1/swap_sub_position_info': 1, + 'linear-swap-api/v1/swap_cross_sub_position_info': 1, + 'linear-swap-api/v1/swap_financial_record': 1, + 'linear-swap-api/v1/swap_financial_record_exact': 1, + 'linear-swap-api/v1/swap_user_settlement_records': 1, + 'linear-swap-api/v1/swap_cross_user_settlement_records': 1, + 'linear-swap-api/v1/swap_available_level_rate': 1, + 'linear-swap-api/v1/swap_cross_available_level_rate': 1, + 'linear-swap-api/v1/swap_order_limit': 1, + 'linear-swap-api/v1/swap_fee': 1, + 'linear-swap-api/v1/swap_transfer_limit': 1, + 'linear-swap-api/v1/swap_cross_transfer_limit': 1, + 'linear-swap-api/v1/swap_position_limit': 1, + 'linear-swap-api/v1/swap_cross_position_limit': 1, + 'linear-swap-api/v1/swap_master_sub_transfer': 1, + 'linear-swap-api/v1/swap_master_sub_transfer_record': 1, + 'linear-swap-api/v1/swap_transfer_inner': 1, + 'linear-swap-api/v3/swap_financial_record': 1, + 'linear-swap-api/v3/swap_financial_record_exact': 1, + # Swap Trade Interface + 'linear-swap-api/v1/swap_order': 1, + 'linear-swap-api/v1/swap_cross_order': 1, + 'linear-swap-api/v1/swap_batchorder': 1, + 'linear-swap-api/v1/swap_cross_batchorder': 1, + 'linear-swap-api/v1/swap_cancel': 1, + 'linear-swap-api/v1/swap_cross_cancel': 1, + 'linear-swap-api/v1/swap_cancelall': 1, + 'linear-swap-api/v1/swap_cross_cancelall': 1, + 'linear-swap-api/v1/swap_switch_lever_rate': 30, + 'linear-swap-api/v1/swap_cross_switch_lever_rate': 30, + 'linear-swap-api/v1/swap_lightning_close_position': 1, + 'linear-swap-api/v1/swap_cross_lightning_close_position': 1, + 'linear-swap-api/v1/swap_order_info': 1, + 'linear-swap-api/v1/swap_cross_order_info': 1, + 'linear-swap-api/v1/swap_order_detail': 1, + 'linear-swap-api/v1/swap_cross_order_detail': 1, + 'linear-swap-api/v1/swap_openorders': 1, + 'linear-swap-api/v1/swap_cross_openorders': 1, + 'linear-swap-api/v1/swap_hisorders': 1, + 'linear-swap-api/v1/swap_cross_hisorders': 1, + 'linear-swap-api/v1/swap_hisorders_exact': 1, + 'linear-swap-api/v1/swap_cross_hisorders_exact': 1, + 'linear-swap-api/v1/swap_matchresults': 1, + 'linear-swap-api/v1/swap_cross_matchresults': 1, + 'linear-swap-api/v1/swap_matchresults_exact': 1, + 'linear-swap-api/v1/swap_cross_matchresults_exact': 1, + 'linear-swap-api/v1/linear-cancel-after': 1, + 'linear-swap-api/v1/swap_switch_position_mode': 1, + 'linear-swap-api/v1/swap_cross_switch_position_mode': 1, + 'linear-swap-api/v3/swap_matchresults': 1, + 'linear-swap-api/v3/swap_cross_matchresults': 1, + 'linear-swap-api/v3/swap_matchresults_exact': 1, + 'linear-swap-api/v3/swap_cross_matchresults_exact': 1, + 'linear-swap-api/v3/swap_hisorders': 1, + 'linear-swap-api/v3/swap_cross_hisorders': 1, + 'linear-swap-api/v3/swap_hisorders_exact': 1, + 'linear-swap-api/v3/swap_cross_hisorders_exact': 1, + 'linear-swap-api/v3/fix_position_margin_change': 1, + 'linear-swap-api/v3/swap_switch_account_type': 1, + 'linear-swap-api/v3/linear_swap_fee_switch': 1, + # Swap Strategy Order Interface + 'linear-swap-api/v1/swap_trigger_order': 1, + 'linear-swap-api/v1/swap_cross_trigger_order': 1, + 'linear-swap-api/v1/swap_trigger_cancel': 1, + 'linear-swap-api/v1/swap_cross_trigger_cancel': 1, + 'linear-swap-api/v1/swap_trigger_cancelall': 1, + 'linear-swap-api/v1/swap_cross_trigger_cancelall': 1, + 'linear-swap-api/v1/swap_trigger_openorders': 1, + 'linear-swap-api/v1/swap_cross_trigger_openorders': 1, + 'linear-swap-api/v1/swap_trigger_hisorders': 1, + 'linear-swap-api/v1/swap_cross_trigger_hisorders': 1, + 'linear-swap-api/v1/swap_tpsl_order': 1, + 'linear-swap-api/v1/swap_cross_tpsl_order': 1, + 'linear-swap-api/v1/swap_tpsl_cancel': 1, + 'linear-swap-api/v1/swap_cross_tpsl_cancel': 1, + 'linear-swap-api/v1/swap_tpsl_cancelall': 1, + 'linear-swap-api/v1/swap_cross_tpsl_cancelall': 1, + 'linear-swap-api/v1/swap_tpsl_openorders': 1, + 'linear-swap-api/v1/swap_cross_tpsl_openorders': 1, + 'linear-swap-api/v1/swap_tpsl_hisorders': 1, + 'linear-swap-api/v1/swap_cross_tpsl_hisorders': 1, + 'linear-swap-api/v1/swap_relation_tpsl_order': 1, + 'linear-swap-api/v1/swap_cross_relation_tpsl_order': 1, + 'linear-swap-api/v1/swap_track_order': 1, + 'linear-swap-api/v1/swap_cross_track_order': 1, + 'linear-swap-api/v1/swap_track_cancel': 1, + 'linear-swap-api/v1/swap_cross_track_cancel': 1, + 'linear-swap-api/v1/swap_track_cancelall': 1, + 'linear-swap-api/v1/swap_cross_track_cancelall': 1, + 'linear-swap-api/v1/swap_track_openorders': 1, + 'linear-swap-api/v1/swap_cross_track_openorders': 1, + 'linear-swap-api/v1/swap_track_hisorders': 1, + 'linear-swap-api/v1/swap_cross_track_hisorders': 1, + }, + }, + }, + }, + 'fees': { + 'trading': { + 'feeSide': 'get', + 'tierBased': False, + 'percentage': True, + 'maker': self.parse_number('0.002'), + 'taker': self.parse_number('0.002'), + }, + }, + 'exceptions': { + 'broad': { + 'contract is restricted of closing positions on API. Please contact customer service': OnMaintenance, + 'maintain': OnMaintenance, + 'API key has no permission': PermissionDenied, # {"status":"error","err-code":"api-signature-not-valid","err-msg":"Signature not valid: API key has no permission [API Key没有权限]","data":null} + }, + 'exact': { + # err-code + '403': AuthenticationError, # {"status":"error","err_code":403,"err_msg":"Incorrect Access key [Access key错误]","ts":1652774224344} + '1010': AccountNotEnabled, # {"status":"error","err_code":1010,"err_msg":"Account doesnt exist.","ts":1648137970490} + '1003': AuthenticationError, # {code: '1003', message: 'invalid signature'} + '1013': BadSymbol, # {"status":"error","err_code":1013,"err_msg":"This contract symbol doesnt exist.","ts":1640550459583} + '1017': OrderNotFound, # {"status":"error","err_code":1017,"err_msg":"Order doesnt exist.","ts":1640550859242} + '1034': InvalidOrder, # {"status":"error","err_code":1034,"err_msg":"Incorrect field of order price type.","ts":1643802870182} + '1036': InvalidOrder, # {"status":"error","err_code":1036,"err_msg":"Incorrect field of open long form.","ts":1643802518986} + '1039': InvalidOrder, # {"status":"error","err_code":1039,"err_msg":"Buy price must be lower than 39270.9USDT. Sell price must exceed 37731USDT.","ts":1643802374403} + '1041': InvalidOrder, # {"status":"error","err_code":1041,"err_msg":"The order amount exceeds the limit(170000Cont), please modify and order again.","ts":1643802784940} + '1047': InsufficientFunds, # {"status":"error","err_code":1047,"err_msg":"Insufficient margin available.","ts":1643802672652} + '1048': InsufficientFunds, # {"status":"error","err_code":1048,"err_msg":"Insufficient close amount available.","ts":1652772408864} + '1061': OrderNotFound, # {"status":"ok","data":{"errors":[{"order_id":"1349442392365359104","err_code":1061,"err_msg":"The order does not exist."}],"successes":""},"ts":1741773744526} + '1051': InvalidOrder, # {"status":"error","err_code":1051,"err_msg":"No orders to cancel.","ts":1652552125876} + '1066': BadSymbol, # {"status":"error","err_code":1066,"err_msg":"The symbol field cannot be empty. Please re-enter.","ts":1640550819147} + '1067': InvalidOrder, # {"status":"error","err_code":1067,"err_msg":"The client_order_id field is invalid. Please re-enter.","ts":1643802119413} + '1094': InvalidOrder, # {"status":"error","err_code":1094,"err_msg":"The leverage cannot be empty, please switch the leverage or contact customer service","ts":1640496946243} + '1220': AccountNotEnabled, # {"status":"error","err_code":1220,"err_msg":"You don’t have access permission have not opened contracts trading.","ts":1645096660718} + '1303': BadRequest, # {"code":1303,"data":null,"message":"Each transfer-out cannot be less than 5USDT.","success":false,"print-log":true} + '1461': InvalidOrder, # {"status":"error","err_code":1461,"err_msg":"Current positions have triggered position limits(5000USDT). Please modify.","ts":1652554651234} + '4007': BadRequest, # {"code":"4007","msg":"Unified account special interface, non - one account is not available","data":null,"ts":"1698413427651"}' + 'bad-request': BadRequest, + 'validation-format-error': BadRequest, # {"status":"error","err-code":"validation-format-error","err-msg":"Format Error: order-id.","data":null} + 'validation-constraints-required': BadRequest, # {"status":"error","err-code":"validation-constraints-required","err-msg":"Field is missing: client-order-id.","data":null} + 'base-date-limit-error': BadRequest, # {"status":"error","err-code":"base-date-limit-error","err-msg":"date less than system limit","data":null} + 'api-not-support-temp-addr': PermissionDenied, # {"status":"error","err-code":"api-not-support-temp-addr","err-msg":"API withdrawal does not support temporary addresses","data":null} + 'timeout': RequestTimeout, # {"ts":1571653730865,"status":"error","err-code":"timeout","err-msg":"Request Timeout"} + 'gateway-internal-error': ExchangeNotAvailable, # {"status":"error","err-code":"gateway-internal-error","err-msg":"Failed to load data. Try again later.","data":null} + 'account-frozen-balance-insufficient-error': InsufficientFunds, # {"status":"error","err-code":"account-frozen-balance-insufficient-error","err-msg":"trade account balance is not enough, left: `0.0027`","data":null} + 'invalid-amount': InvalidOrder, # eg "Paramemter `amount` is invalid." + 'order-limitorder-amount-min-error': InvalidOrder, # limit order amount error, min: `0.001` + 'order-limitorder-amount-max-error': InvalidOrder, # market order amount error, max: `1000000` + 'order-marketorder-amount-min-error': InvalidOrder, # market order amount error, min: `0.01` + 'order-limitorder-price-min-error': InvalidOrder, # limit order price error + 'order-limitorder-price-max-error': InvalidOrder, # limit order price error + 'order-stop-order-hit-trigger': InvalidOrder, # {"status":"error","err-code":"order-stop-order-hit-trigger","err-msg":"Orders that are triggered immediately are not supported.","data":null} + 'order-value-min-error': InvalidOrder, # {"status":"error","err-code":"order-value-min-error","err-msg":"Order total cannot be lower than: 1 USDT","data":null} + 'order-invalid-price': InvalidOrder, # {"status":"error","err-code":"order-invalid-price","err-msg":"invalid price","data":null} + 'order-holding-limit-failed': InvalidOrder, # {"status":"error","err-code":"order-holding-limit-failed","err-msg":"Order failed, exceeded the holding limit of self currency","data":null} + 'order-orderprice-precision-error': InvalidOrder, # {"status":"error","err-code":"order-orderprice-precision-error","err-msg":"order price precision error, scale: `4`","data":null} + 'order-etp-nav-price-max-error': InvalidOrder, # {"status":"error","err-code":"order-etp-nav-price-max-error","err-msg":"Order price cannot be higher than 5% of NAV","data":null} + 'order-orderstate-error': OrderNotFound, # canceling an already canceled order + 'order-queryorder-invalid': OrderNotFound, # querying a non-existent order + 'order-update-error': ExchangeNotAvailable, # undocumented error + 'api-signature-check-failed': AuthenticationError, + 'api-signature-not-valid': AuthenticationError, # {"status":"error","err-code":"api-signature-not-valid","err-msg":"Signature not valid: Incorrect Access key [Access key错误]","data":null} + 'base-record-invalid': OrderNotFound, # https://github.com/ccxt/ccxt/issues/5750 + 'base-symbol-trade-disabled': BadSymbol, # {"status":"error","err-code":"base-symbol-trade-disabled","err-msg":"Trading is disabled for self symbol","data":null} + 'base-symbol-error': BadSymbol, # {"status":"error","err-code":"base-symbol-error","err-msg":"The symbol is invalid","data":null} + 'system-maintenance': OnMaintenance, # {"status": "error", "err-code": "system-maintenance", "err-msg": "System is in maintenance!", "data": null} + 'base-request-exceed-frequency-limit': RateLimitExceeded, # {"status":"error","err-code":"base-request-exceed-frequency-limit","err-msg":"Frequency of requests has exceeded the limit, please try again later","data":null} + # err-msg + 'invalid symbol': BadSymbol, # {"ts":1568813334794,"status":"error","err-code":"invalid-parameter","err-msg":"invalid symbol"} + 'symbol trade not open now': BadSymbol, # {"ts":1576210479343,"status":"error","err-code":"invalid-parameter","err-msg":"symbol trade not open now"} + 'require-symbol': BadSymbol, # {"status":"error","err-code":"require-symbol","err-msg":"Parameter `symbol` is required.","data":null}, + 'invalid-address': BadRequest, # {"status":"error","err-code":"invalid-address","err-msg":"Invalid address.","data":null}, + 'base-currency-chain-error': BadRequest, # {"status":"error","err-code":"base-currency-chain-error","err-msg":"The current currency chain does not exist","data":null}, + 'dw-insufficient-balance': InsufficientFunds, # {"status":"error","err-code":"dw-insufficient-balance","err-msg":"Insufficient balance. You can only transfer `12.3456` at most.","data":null} + 'base-withdraw-fee-error': BadRequest, # {"status":"error","err-code":"base-withdraw-fee-error","err-msg":"withdrawal fee is not within limits","data":null} + 'dw-withdraw-min-limit': BadRequest, # {"status":"error","err-code":"dw-withdraw-min-limit","err-msg":"The withdrawal amount is less than the minimum limit.","data":null} + 'request limit': RateLimitExceeded, # {"ts":1687004814731,"status":"error","err-code":"invalid-parameter","err-msg":"request limit"} + }, + }, + 'precisionMode': TICK_SIZE, + 'options': { + 'include_OS_certificates': False, # temporarily leave self, remove in future + 'fetchMarkets': { + 'types': { + 'spot': True, + 'linear': True, + 'inverse': True, + }, + }, + 'timeDifference': 0, # the difference between system clock and exchange clock + 'adjustForTimeDifference': False, # controls the adjustment logic upon instantiation + 'fetchOHLCV': { + 'useHistoricalEndpointForSpot': True, + }, + 'withdraw': { + 'includeFee': False, + }, + 'defaultType': 'spot', # spot, future, swap + 'defaultSubType': 'linear', # inverse, linear + 'defaultNetwork': 'ERC20', + 'defaultNetworks': { + 'ETH': 'ERC20', + 'BTC': 'BTC', + 'USDT': 'TRC20', + }, + 'networks': { + # by displaynames + 'TRC20': 'TRX', # TRON for mainnet + 'BTC': 'BTC', + 'ERC20': 'ETH', # ETH for mainnet + 'SOL': 'SOLANA', + 'HRC20': 'HECO', + 'BEP20': 'BSC', + 'XMR': 'XMR', + 'LTC': 'LTC', + 'XRP': 'XRP', + 'XLM': 'XLM', + 'CRONOS': 'CRO', + 'CRO': 'CRO', + 'GLMR': 'GLMR', + 'POLYGON': 'MATIC', + 'MATIC': 'MATIC', + 'BTT': 'BTT', + 'CUBE': 'CUBE', + 'IOST': 'IOST', + 'NEO': 'NEO', + 'KLAY': 'KLAY', + 'EOS': 'EOS', + 'THETA': 'THETA', + 'NAS': 'NAS', + 'NULS': 'NULS', + 'QTUM': 'QTUM', + 'FTM': 'FTM', + 'CELO': 'CELO', + 'DOGE': 'DOGE', + 'DOGECHAIN': 'DOGECHAIN', + 'NEAR': 'NEAR', + 'STEP': 'STEP', + 'BITCI': 'BITCI', + 'CARDANO': 'ADA', + 'ADA': 'ADA', + 'ETC': 'ETC', + 'LUK': 'LUK', + 'MINEPLEX': 'MINEPLEX', + 'DASH': 'DASH', + 'ZEC': 'ZEC', + 'IOTA': 'IOTA', + 'NEON3': 'NEON3', + 'XEM': 'XEM', + 'HC': 'HC', + 'LSK': 'LSK', + 'DCR': 'DCR', + 'BTG': 'BTG', + 'STEEM': 'STEEM', + 'BTS': 'BTS', + 'ICX': 'ICX', + 'WAVES': 'WAVES', + 'CMT': 'CMT', + 'BTM': 'BTM', + 'VET': 'VET', + 'XZC': 'XZC', + 'ACT': 'ACT', + 'SMT': 'SMT', + 'BCD': 'BCD', + 'WAX': 'WAX1', + 'WICC': 'WICC', + 'ELF': 'ELF', + 'ZIL': 'ZIL', + 'ELA': 'ELA', + 'BCX': 'BCX', + 'SBTC': 'SBTC', + 'BIFI': 'BIFI', + 'CTXC': 'CTXC', + 'WAN': 'WAN', + 'POLYX': 'POLYX', + 'PAI': 'PAI', + 'WTC': 'WTC', + 'DGB': 'DGB', + 'XVG': 'XVG', + 'AAC': 'AAC', + 'AE': 'AE', + 'SEELE': 'SEELE', + 'BCV': 'BCV', + 'GRS': 'GRS', + 'ARDR': 'ARDR', + 'NANO': 'NANO', + 'ZEN': 'ZEN', + 'RBTC': 'RBTC', + 'BSV': 'BSV', + 'GAS': 'GAS', + 'XTZ': 'XTZ', + 'LAMB': 'LAMB', + 'CVNT1': 'CVNT1', + 'DOCK': 'DOCK', + 'SC': 'SC', + 'KMD': 'KMD', + 'ETN': 'ETN', + 'TOP': 'TOP', + 'IRIS': 'IRIS', + 'UGAS': 'UGAS', + 'TT': 'TT', + 'NEWTON': 'NEWTON', + 'VSYS': 'VSYS', + 'FSN': 'FSN', + 'BHD': 'BHD', + 'ONE': 'ONE', + 'EM': 'EM', + 'CKB': 'CKB', + 'EOSS': 'EOSS', + 'HIVE': 'HIVE', + 'RVN': 'RVN', + 'DOT': 'DOT', + 'KSM': 'KSM', + 'BAND': 'BAND', + 'OEP4': 'OEP4', + 'NBS': 'NBS', + 'FIS': 'FIS', + 'AR': 'AR', + 'HBAR': 'HBAR', + 'FIL': 'FIL', + 'MASS': 'MASS', + 'KAVA': 'KAVA', + 'XYM': 'XYM', + 'ENJ': 'ENJ', + 'CRUST': 'CRUST', + 'ICP': 'ICP', + 'CSPR': 'CSPR', + 'FLOW': 'FLOW', + 'IOTX': 'IOTX', + 'LAT': 'LAT', + 'APT': 'APT', + 'XCH': 'XCH', + 'MINA': 'MINA', + 'XEC': 'ECASH', + 'XPRT': 'XPRT', + 'CCA': 'ACA', + 'AOTI': 'COTI', + 'AKT': 'AKT', + 'ARS': 'ARS', + 'ASTR': 'ASTR', + 'AZERO': 'AZERO', + 'BLD': 'BLD', + 'BRISE': 'BRISE', + 'CORE': 'CORE', + 'DESO': 'DESO', + 'DFI': 'DFI', + 'EGLD': 'EGLD', + 'ERG': 'ERG', + 'ETHF': 'ETHFAIR', + 'ETHW': 'ETHW', + 'EVMOS': 'EVMOS', + 'FIO': 'FIO', + 'FLR': 'FLR', + 'FINSCHIA': 'FINSCHIA', + 'KMA': 'KMA', + 'KYVE': 'KYVE', + 'MEV': 'MEV', + 'MOVR': 'MOVR', + 'NODL': 'NODL', + 'OAS': 'OAS', + 'OSMO': 'OSMO', + 'PAYCOIN': 'PAYCOIN', + 'POKT': 'POKT', + 'PYG': 'PYG', + 'REI': 'REI', + 'SCRT': 'SCRT', + 'SDN': 'SDN', + 'SEI': 'SEI', + 'SGB': 'SGB', + 'SUI': 'SUI', + 'SXP': 'SOLAR', + 'SYS': 'SYS', + 'TENET': 'TENET', + 'TON': 'TON', + 'UNQ': 'UNQ', + 'UYU': 'UYU', + 'WEMIX': 'WEMIX', + 'XDC': 'XDC', + 'XPLA': 'XPLA', + # todo: below + # 'LUNC': 'LUNC', + # 'TERRA': 'TERRA', # tbd + # 'LUNA': 'LUNA', tbd + # 'FCT2': 'FCT2', + # FIL-0X ? + # 'COSMOS': 'ATOM1', + # 'ATOM': 'ATOM1', + # 'CRO': 'CRO', + # 'OP': ['OPTIMISM', 'OPTIMISMETH'] + # 'ARB': ['ARB', 'ARBITRUMETH'] + # 'CHZ': ['CHZ', 'CZH'], + # todo: AVAXCCHAIN CCHAIN AVAX + # 'ALGO': ['ALGO', 'ALGOUSDT'] + # 'ONT': ['ONT', 'ONTOLOGY'], + # 'BCC': 'BCC', BCH's somewhat chain + # 'DBC1': 'DBC1', + }, + # https://github.com/ccxt/ccxt/issues/5376 + 'fetchOrdersByStatesMethod': 'spot_private_get_v1_order_orders', # 'spot_private_get_v1_order_history' # https://github.com/ccxt/ccxt/pull/5392 + 'createMarketBuyOrderRequiresPrice': True, + 'language': 'en-US', + 'broker': { + 'id': 'AA03022abc', + }, + 'accountsByType': { + 'spot': 'pro', + 'funding': 'pro', + 'future': 'futures', + }, + 'accountsById': { + 'spot': 'spot', + 'margin': 'margin', + 'otc': 'otc', + 'point': 'point', + 'super-margin': 'super-margin', + 'investment': 'investment', + 'borrow': 'borrow', + 'grid-trading': 'grid-trading', + 'deposit-earning': 'deposit-earning', + 'otc-options': 'otc-options', + }, + 'typesByAccount': { + 'pro': 'spot', + 'futures': 'future', + }, + 'spot': { + 'stopOrderTypes': { + 'stop-limit': True, + 'buy-stop-limit': True, + 'sell-stop-limit': True, + 'stop-limit-fok': True, + 'buy-stop-limit-fok': True, + 'sell-stop-limit-fok': True, + }, + 'limitOrderTypes': { + 'limit': True, + 'buy-limit': True, + 'sell-limit': True, + 'ioc': True, + 'buy-ioc': True, + 'sell-ioc': True, + 'limit-maker': True, + 'buy-limit-maker': True, + 'sell-limit-maker': True, + 'stop-limit': True, + 'buy-stop-limit': True, + 'sell-stop-limit': True, + 'limit-fok': True, + 'buy-limit-fok': True, + 'sell-limit-fok': True, + 'stop-limit-fok': True, + 'buy-stop-limit-fok': True, + 'sell-stop-limit-fok': True, + }, + }, + }, + 'commonCurrencies': { + # https://github.com/ccxt/ccxt/issues/6081 + # https://github.com/ccxt/ccxt/issues/3365 + # https://github.com/ccxt/ccxt/issues/2873 + 'NGL': 'GFNGL', + 'GET': 'THEMIS', # conflict with GET(Guaranteed Entrance Token, GET Protocol) + 'GTC': 'GAMECOM', # conflict with Gitcoin and Gastrocoin + 'HIT': 'HITCHAIN', + # https://github.com/ccxt/ccxt/issues/7399 + # https://coinmarketcap.com/currencies/pnetwork/ + # https://coinmarketcap.com/currencies/penta/markets/ + # https://en.cryptonomist.ch/blog/eidoo/the-edo-to-pnt-upgrade-what-you-need-to-know-updated/ + 'PNT': 'PENTA', + 'SBTC': 'SUPERBITCOIN', + 'SOUL': 'SOULSAVER', + 'BIFI': 'BITCOINFILE', # conflict with Beefy.Finance https://github.com/ccxt/ccxt/issues/8706 + 'FUD': 'FTX Users Debt', + }, + 'features': { + 'spot': { + 'sandbox': True, + 'createOrder': { + 'marginMode': True, + 'triggerPrice': True, + 'triggerDirection': True, + 'triggerPriceType': None, + 'stopLossPrice': False, # todo: add support by triggerprice + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'iceberg': False, + 'selfTradePrevention': True, # todo implement + 'leverage': True, # todo implement + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': True, + }, + 'createOrders': { + 'max': 10, + }, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 500, + 'daysBack': 120, + 'untilDays': 2, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'trigger': True, + 'trailing': False, + 'limit': 500, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': False, + 'trigger': True, + 'trailing': False, + 'limit': 500, + 'untilDays': 2, + 'daysBack': 180, + 'symbolRequired': False, + }, + 'fetchClosedOrders': { + 'marginMode': False, + 'trigger': True, + 'trailing': False, + 'untilDays': 2, + 'limit': 500, + 'daysBack': 180, + 'daysBackCanceled': 1 / 12, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 1000, # 2000 for non-historical + }, + }, + 'forDerivatives': { + 'extends': 'spot', + 'createOrder': { + 'stopLossPrice': True, + 'takeProfitPrice': True, + 'trailing': True, + 'hedged': True, + # 'leverage': True, # todo + }, + 'createOrders': { + 'max': 25, + }, + 'fetchOrder': { + 'marginMode': True, + }, + 'fetchOpenOrders': { + 'marginMode': True, + 'trigger': False, + 'trailing': False, + 'limit': 50, + }, + 'fetchOrders': { + 'marginMode': True, + 'trigger': False, + 'trailing': False, + 'limit': 50, + 'daysBack': 90, + }, + 'fetchClosedOrders': { + 'marginMode': True, + 'trigger': False, + 'trailing': False, + 'untilDays': 2, + 'limit': 50, + 'daysBack': 90, + 'daysBackCanceled': 1 / 12, + }, + 'fetchOHLCV': { + 'limit': 2000, + }, + }, + 'swap': { + 'linear': { + 'extends': 'forDerivatives', + }, + 'inverse': { + 'extends': 'forDerivatives', + }, + }, + 'future': { + 'linear': { + 'extends': 'forDerivatives', + }, + 'inverse': { + 'extends': 'forDerivatives', + }, + }, + }, + }) + + async def fetch_status(self, params={}): + """ + the latest known information on the availability of the exchange API + + https://huobiapi.github.io/docs/spot/v1/en/#get-system-status + https://huobiapi.github.io/docs/dm/v1/en/#get-system-status + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#get-system-status + https://huobiapi.github.io/docs/usdt_swap/v1/en/#get-system-status + https://huobiapi.github.io/docs/usdt_swap/v1/en/#query-whether-the-system-is-available # contractPublicGetHeartbeat + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `status structure ` + """ + await self.load_markets() + marketType = None + marketType, params = self.handle_market_type_and_params('fetchStatus', None, params) + enabledForContracts = self.handle_option('fetchStatus', 'enableForContracts', False) # temp fix for: https://status-linear-swap.huobigroup.com/api/v2/summary.json + response = None + if marketType != 'spot' and enabledForContracts: + subType = self.safe_string(params, 'subType', self.options['defaultSubType']) + if marketType == 'swap': + if subType == 'linear': + response = await self.statusPublicSwapLinearGetApiV2SummaryJson() + elif subType == 'inverse': + response = await self.statusPublicSwapInverseGetApiV2SummaryJson() + elif marketType == 'future': + if subType == 'linear': + response = await self.statusPublicFutureLinearGetApiV2SummaryJson() + elif subType == 'inverse': + response = await self.statusPublicFutureInverseGetApiV2SummaryJson() + elif marketType == 'contract': + response = await self.contractPublicGetHeartbeat() + elif marketType == 'spot': + response = await self.statusPublicSpotGetApiV2SummaryJson() + # + # statusPublicSpotGetApiV2SummaryJson, statusPublicSwapInverseGetApiV2SummaryJson, statusPublicFutureLinearGetApiV2SummaryJson, statusPublicFutureInverseGetApiV2SummaryJson + # + # { + # "page": { + # "id":"mn7l2lw8pz4p", + # "name":"Huobi Futures-USDT-margined Swaps", + # "url":"https://status-linear-swap.huobigroup.com", + # "time_zone":"Asia/Singapore", + # "updated_at":"2022-04-29T12:47:21.319+08:00"}, + # "components": [ + # { + # "id":"lrv093qk3yp5", + # "name":"market data", + # "status":"operational", + # "created_at":"2020-10-29T14:08:59.427+08:00", + # "updated_at":"2020-10-29T14:08:59.427+08:00", + # "position":1,"description":null, + # "showcase":false, + # "start_date":null, + # "group_id":null, + # "page_id":"mn7l2lw8pz4p", + # "group":true, + # "only_show_if_degraded":false, + # "components": [ + # "82k5jxg7ltxd" # list of related components + # ] + # }, + # ], + # "incidents": [ # empty array if there are no issues + # { + # "id": "rclfxz2g21ly", # incident id + # "name": "Market data is delayed", # incident name + # "status": "investigating", # incident status + # "created_at": "2020-02-11T03:15:01.913Z", # incident create time + # "updated_at": "2020-02-11T03:15:02.003Z", # incident update time + # "monitoring_at": null, + # "resolved_at": null, + # "impact": "minor", # incident impact + # "shortlink": "http://stspg.io/pkvbwp8jppf9", + # "started_at": "2020-02-11T03:15:01.906Z", + # "page_id": "p0qjfl24znv5", + # "incident_updates": [ + # { + # "id": "dwfsk5ttyvtb", + # "status": "investigating", + # "body": "Market data is delayed", + # "incident_id": "rclfxz2g21ly", + # "created_at": "2020-02-11T03:15:02.000Z", + # "updated_at": "2020-02-11T03:15:02.000Z", + # "display_at": "2020-02-11T03:15:02.000Z", + # "affected_components": [ + # { + # "code": "nctwm9tghxh6", + # "name": "Market data", + # "old_status": "operational", + # "new_status": "degraded_performance" + # } + # ], + # "deliver_notifications": True, + # "custom_tweet": null, + # "tweet_id": null + # } + # ], + # "components": [ + # { + # "id": "nctwm9tghxh6", + # "name": "Market data", + # "status": "degraded_performance", + # "created_at": "2020-01-13T09:34:48.284Z", + # "updated_at": "2020-02-11T03:15:01.951Z", + # "position": 8, + # "description": null, + # "showcase": False, + # "group_id": null, + # "page_id": "p0qjfl24znv5", + # "group": False, + # "only_show_if_degraded": False + # } + # ] + # }, ... + # ], + # "scheduled_maintenances":[ # empty array if there are no scheduled maintenances + # { + # "id": "k7g299zl765l", # incident id + # "name": "Schedule maintenance", # incident name + # "status": "scheduled", # incident status + # "created_at": "2020-02-11T03:16:31.481Z", # incident create time + # "updated_at": "2020-02-11T03:16:31.530Z", # incident update time + # "monitoring_at": null, + # "resolved_at": null, + # "impact": "maintenance", # incident impact + # "shortlink": "http://stspg.io/md4t4ym7nytd", + # "started_at": "2020-02-11T03:16:31.474Z", + # "page_id": "p0qjfl24znv5", + # "incident_updates": [ + # { + # "id": "8whgr3rlbld8", + # "status": "scheduled", + # "body": "We will be undergoing scheduled maintenance during self time.", + # "incident_id": "k7g299zl765l", + # "created_at": "2020-02-11T03:16:31.527Z", + # "updated_at": "2020-02-11T03:16:31.527Z", + # "display_at": "2020-02-11T03:16:31.527Z", + # "affected_components": [ + # { + # "code": "h028tnzw1n5l", + # "name": "Deposit And Withdraw - Deposit", + # "old_status": "operational", + # "new_status": "operational" + # } + # ], + # "deliver_notifications": True, + # "custom_tweet": null, + # "tweet_id": null + # } + # ], + # "components": [ + # { + # "id": "h028tnzw1n5l", + # "name": "Deposit", + # "status": "operational", + # "created_at": "2019-12-05T02:07:12.372Z", + # "updated_at": "2020-02-10T12:34:52.970Z", + # "position": 1, + # "description": null, + # "showcase": False, + # "group_id": "gtd0nyr3pf0k", + # "page_id": "p0qjfl24znv5", + # "group": False, + # "only_show_if_degraded": False + # } + # ], + # "scheduled_for": "2020-02-15T00:00:00.000Z", # scheduled maintenance start time + # "scheduled_until": "2020-02-15T01:00:00.000Z" # scheduled maintenance end time + # } + # ], + # "status": { + # "indicator":"none", # none, minor, major, critical, maintenance + # "description":"all systems operational" # All Systems Operational, Minor Service Outage, Partial System Outage, Partially Degraded Service, Service Under Maintenance + # } + # } + # + # + # contractPublicGetHeartbeat + # + # { + # "status": "ok", # 'ok', 'error' + # "data": { + # "heartbeat": 1, # future 1: available, 0: maintenance with service suspended + # "estimated_recovery_time": null, # estimated recovery time in milliseconds + # "swap_heartbeat": 1, + # "swap_estimated_recovery_time": null, + # "option_heartbeat": 1, + # "option_estimated_recovery_time": null, + # "linear_swap_heartbeat": 1, + # "linear_swap_estimated_recovery_time": null + # }, + # "ts": 1557714418033 + # } + # + status = None + updated = None + url = None + if marketType == 'contract': + statusRaw = self.safe_string(response, 'status') + if statusRaw is None: + status = None + else: + status = 'ok' if (statusRaw == 'ok') else 'maintenance' # 'ok', 'error' + updated = self.safe_string(response, 'ts') + else: + statusData = self.safe_value(response, 'status', {}) + statusRaw = self.safe_string(statusData, 'indicator') + status = 'ok' if (statusRaw == 'none') else 'maintenance' # none, minor, major, critical, maintenance + pageData = self.safe_value(response, 'page', {}) + datetime = self.safe_string(pageData, 'updated_at') + updated = self.parse8601(datetime) + url = self.safe_string(pageData, 'url') + return { + 'status': status, + 'updated': updated, + 'eta': None, + 'url': url, + 'info': response, + } + + async def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://huobiapi.github.io/docs/spot/v1/en/#get-current-timestamp + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#get-current-system-timestamp + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + options = self.safe_value(self.options, 'fetchTime', {}) + defaultType = self.safe_string(self.options, 'defaultType', 'spot') + type = self.safe_string(options, 'type', defaultType) + type = self.safe_string(params, 'type', type) + response = None + if (type == 'future') or (type == 'swap'): + response = await self.contractPublicGetApiV1Timestamp(params) + else: + response = await self.spotPublicGetV1CommonTimestamp(params) + # + # spot + # + # {"status":"ok","data":1637504261099} + # + # future, swap + # + # {"status":"ok","ts":1637504164707} + # + return self.safe_integer_2(response, 'data', 'ts') + + def parse_trading_fee(self, fee: dict, market: Market = None) -> TradingFeeInterface: + # + # { + # "symbol":"btcusdt", + # "actualMakerRate":"0.002", + # "actualTakerRate":"0.002", + # "takerFeeRate":"0.002", + # "makerFeeRate":"0.002" + # } + # + marketId = self.safe_string(fee, 'symbol') + return { + 'info': fee, + 'symbol': self.safe_symbol(marketId, market), + 'maker': self.safe_number(fee, 'actualMakerRate'), + 'taker': self.safe_number(fee, 'actualTakerRate'), + 'percentage': None, + 'tierBased': None, + } + + async def fetch_trading_fee(self, symbol: str, params={}) -> TradingFeeInterface: + """ + fetch the trading fees for a market + + https://huobiapi.github.io/docs/spot/v1/en/#get-current-fee-rate-applied-to-the-user + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `fee structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbols': market['id'], # trading symbols comma-separated + } + response = await self.spotPrivateGetV2ReferenceTransactFeeRate(self.extend(request, params)) + # + # { + # "code":200, + # "data":[ + # { + # "symbol":"btcusdt", + # "actualMakerRate":"0.002", + # "actualTakerRate":"0.002", + # "takerFeeRate":"0.002", + # "makerFeeRate":"0.002" + # } + # ], + # "success":true + # } + # + data = self.safe_value(response, 'data', []) + first = self.safe_value(data, 0, {}) + return self.parse_trading_fee(first, market) + + async def fetch_trading_limits(self, symbols: Strings = None, params={}): + # self method should not be called directly, use loadTradingLimits() instead + # by default it will try load withdrawal fees of all currencies(with separate requests) + # however if you define symbols = ['ETH/BTC', 'LTC/BTC'] in args it will only load those + await self.load_markets() + if symbols is None: + symbols = self.symbols + result: dict = {} + for i in range(0, len(symbols)): + symbol = symbols[i] + result[symbol] = await self.fetch_trading_limits_by_id(self.market_id(symbol), params) + return result + + async def fetch_trading_limits_by_id(self, id: str, params={}): + """ + @ignore + + https://huobiapi.github.io/docs/spot/v1/en/#get-current-fee-rate-applied-to-the-user + + :param str id: market id + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: the limits object of a market structure + """ + request: dict = { + 'symbol': id, + } + response = await self.spotPublicGetV1CommonExchange(self.extend(request, params)) + # + # {status: "ok", + # "data": { symbol: "aidocbtc", + # "buy-limit-must-less-than": 1.1, + # "sell-limit-must-greater-than": 0.9, + # "limit-order-must-greater-than": 1, + # "limit-order-must-less-than": 5000000, + # "market-buy-order-must-greater-than": 0.0001, + # "market-buy-order-must-less-than": 100, + # "market-sell-order-must-greater-than": 1, + # "market-sell-order-must-less-than": 500000, + # "circuit-break-when-greater-than": 10000, + # "circuit-break-when-less-than": 10, + # "market-sell-order-rate-must-less-than": 0.1, + # "market-buy-order-rate-must-less-than": 0.1 }} + # + return self.parse_trading_limits(self.safe_value(response, 'data', {})) + + def parse_trading_limits(self, limits, symbol: Str = None, params={}): + # + # { "symbol": "aidocbtc", + # "buy-limit-must-less-than": 1.1, + # "sell-limit-must-greater-than": 0.9, + # "limit-order-must-greater-than": 1, + # "limit-order-must-less-than": 5000000, + # "market-buy-order-must-greater-than": 0.0001, + # "market-buy-order-must-less-than": 100, + # "market-sell-order-must-greater-than": 1, + # "market-sell-order-must-less-than": 500000, + # "circuit-break-when-greater-than": 10000, + # "circuit-break-when-less-than": 10, + # "market-sell-order-rate-must-less-than": 0.1, + # "market-buy-order-rate-must-less-than": 0.1 } + # + return { + 'info': limits, + 'limits': { + 'amount': { + 'min': self.safe_number(limits, 'limit-order-must-greater-than'), + 'max': self.safe_number(limits, 'limit-order-must-less-than'), + }, + }, + } + + def cost_to_precision(self, symbol, cost): + return self.decimal_to_precision(cost, TRUNCATE, self.markets[symbol]['precision']['cost'], self.precisionMode) + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for huobi + + https://huobiapi.github.io/docs/spot/v1/en/#get-all-supported-trading-symbol-v1-deprecated + https://huobiapi.github.io/docs/dm/v1/en/#get-contract-info + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#query-swap-info + https://huobiapi.github.io/docs/usdt_swap/v1/en/#general-query-swap-info + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + if self.options['adjustForTimeDifference']: + await self.load_time_difference() + types = None + types, params = self.handle_option_and_params(params, 'fetchMarkets', 'types', {}) + allMarkets = [] + promises = [] + keys = list(types.keys()) + for i in range(0, len(keys)): + key = keys[i] + if self.safe_bool(types, key): + if key == 'spot': + promises.append(self.fetch_markets_by_type_and_sub_type('spot', None, params)) + elif key == 'linear': + promises.append(self.fetch_markets_by_type_and_sub_type(None, 'linear', params)) + elif key == 'inverse': + promises.append(self.fetch_markets_by_type_and_sub_type('swap', 'inverse', params)) + promises.append(self.fetch_markets_by_type_and_sub_type('future', 'inverse', params)) + promises = await asyncio.gather(*promises) + for i in range(0, len(promises)): + allMarkets = self.array_concat(allMarkets, promises[i]) + return allMarkets + + async def fetch_markets_by_type_and_sub_type(self, type: Str, subType: Str, params={}): + """ + @ignore + retrieves data on all markets of a certain type and/or subtype + + https://huobiapi.github.io/docs/spot/v1/en/#get-all-supported-trading-symbol-v1-deprecated + https://huobiapi.github.io/docs/dm/v1/en/#get-contract-info + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#query-swap-info + https://huobiapi.github.io/docs/usdt_swap/v1/en/#general-query-swap-info + + :param str [type]: 'spot', 'swap' or 'future' + :param str [subType]: 'linear' or 'inverse' + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + isSpot = (type == 'spot') + request: dict = {} + response = None + if not isSpot: + if subType == 'linear': + request['business_type'] = 'all' # override default to fetch all linear markets + response = await self.contractPublicGetLinearSwapApiV1SwapContractInfo(self.extend(request, params)) + elif subType == 'inverse': + if type == 'future': + response = await self.contractPublicGetApiV1ContractContractInfo(self.extend(request, params)) + elif type == 'swap': + response = await self.contractPublicGetSwapApiV1SwapContractInfo(self.extend(request, params)) + else: + response = await self.spotPublicGetV1CommonSymbols(self.extend(request, params)) + # + # spot + # + # { + # "status":"ok", + # "data":[ + # { + # "base-currency":"xrp3s", + # "quote-currency":"usdt", + # "price-precision":4, + # "amount-precision":4, + # "symbol-partition":"innovation", + # "symbol":"xrp3susdt", + # "state":"online", + # "value-precision":8, + # "min-order-amt":0.01, + # "max-order-amt":1616.4353, + # "min-order-value":5, + # "limit-order-min-order-amt":0.01, + # "limit-order-max-order-amt":1616.4353, + # "limit-order-max-buy-amt":1616.4353, + # "limit-order-max-sell-amt":1616.4353, + # "sell-market-min-order-amt":0.01, + # "sell-market-max-order-amt":1616.4353, + # "buy-market-max-order-value":2500, + # "max-order-value":2500, + # "underlying":"xrpusdt", + # "mgmt-fee-rate":0.035000000000000000, + # "charge-time":"23:55:00", + # "rebal-time":"00:00:00", + # "rebal-threshold":-5, + # "init-nav":10.000000000000000000, + # "api-trading":"enabled", + # "tags":"etp,nav,holdinglimit" + # }, + # ] + # } + # + # inverse(swap & future) + # + # { + # "status":"ok", + # "data":[ + # { + # "symbol":"BTC", + # "contract_code":"BTC211126", #/ BTC-USD in swap + # "contract_type":"self_week", # only in future + # "contract_size":100, + # "price_tick":0.1, + # "delivery_date":"20211126", # only in future + # "delivery_time":"1637913600000", # empty in swap + # "create_date":"20211112", + # "contract_status":1, + # "settlement_time":"1637481600000" # only in future + # "settlement_date":"16xxxxxxxxxxx" # only in swap + # }, + # ... + # ], + # "ts":1637474595140 + # } + # + # linear(swap & future) + # + # { + # "status":"ok", + # "data":[ + # { + # "symbol":"BTC", + # "contract_code":"BTC-USDT-211231", # or "BTC-USDT" in swap + # "contract_size":0.001, + # "price_tick":0.1, + # "delivery_date":"20211231", # empty in swap + # "delivery_time":"1640937600000", # empty in swap + # "create_date":"20211228", + # "contract_status":1, + # "settlement_date":"1640764800000", + # "support_margin_mode":"cross", # "all" or "cross" + # "business_type":"futures", # "swap" or "futures" + # "pair":"BTC-USDT", + # "contract_type":"self_week", # "swap", "self_week", "next_week", "quarter" + # "trade_partition":"USDT", + # } + # ], + # "ts":1640736207263 + # } + # + markets = self.safe_list(response, 'data', []) + numMarkets = len(markets) + if numMarkets < 1: + raise OperationFailed(self.id + ' fetchMarkets() returned an empty response: ' + self.json(response)) + result = [] + for i in range(0, len(markets)): + market = markets[i] + baseId = None + quoteId = None + settleId = None + id = None + lowercaseId = None + contract = ('contract_code' in market) + spot = not contract + swap = False + future = False + linear = None + inverse = None + # check if parsed market is contract + if contract: + id = self.safe_string(market, 'contract_code') + lowercaseId = id.lower() + delivery_date = self.safe_string(market, 'delivery_date') + business_type = self.safe_string(market, 'business_type') + future = delivery_date is not None + swap = not future + linear = business_type is not None + inverse = not linear + if swap: + type = 'swap' + parts = id.split('-') + baseId = self.safe_string_lower(market, 'symbol') + quoteId = self.safe_string_lower(parts, 1) + settleId = baseId if inverse else quoteId + elif future: + type = 'future' + baseId = self.safe_string_lower(market, 'symbol') + if inverse: + quoteId = 'USD' + settleId = baseId + else: + pair = self.safe_string(market, 'pair') + parts = pair.split('-') + quoteId = self.safe_string_lower(parts, 1) + settleId = quoteId + else: + type = 'spot' + baseId = self.safe_string(market, 'base-currency') + quoteId = self.safe_string(market, 'quote-currency') + id = baseId + quoteId + lowercaseId = id.lower() + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + settle = self.safe_currency_code(settleId) + symbol = base + '/' + quote + expiry = None + if contract: + if inverse: + symbol += ':' + base + elif linear: + symbol += ':' + quote + if future: + expiry = self.safe_integer(market, 'delivery_time') + symbol += '-' + self.yymmdd(expiry) + contractSize = self.safe_number(market, 'contract_size') + minCost = self.safe_number(market, 'min-order-value') + maxAmount = self.safe_number(market, 'max-order-amt') + minAmount = self.safe_number(market, 'min-order-amt') + if contract: + if linear: + minAmount = contractSize + elif inverse: + minCost = contractSize + pricePrecision = None + amountPrecision = None + costPrecision = None + maker = None + taker = None + active = None + if spot: + pricePrecision = self.parse_number(self.parse_precision(self.safe_string(market, 'price-precision'))) + amountPrecision = self.parse_number(self.parse_precision(self.safe_string(market, 'amount-precision'))) + costPrecision = self.parse_number(self.parse_precision(self.safe_string(market, 'value-precision'))) + maker = self.parse_number('0.002') + taker = self.parse_number('0.002') + state = self.safe_string(market, 'state') + active = (state == 'online') + else: + pricePrecision = self.safe_number(market, 'price_tick') + amountPrecision = self.parse_number('1') # other markets have step size of 1 contract + maker = self.parse_number('0.0002') + taker = self.parse_number('0.0005') + contractStatus = self.safe_integer(market, 'contract_status') + active = (contractStatus == 1) + leverageRatio = self.safe_string(market, 'leverage-ratio', '1') + superLeverageRatio = self.safe_string(market, 'super-margin-leverage-ratio', '1') + hasLeverage = Precise.string_gt(leverageRatio, '1') or Precise.string_gt(superLeverageRatio, '1') + # 0 Delisting + # 1 Listing + # 2 Pending Listing + # 3 Suspension + # 4 Suspending of Listing + # 5 In Settlement + # 6 Delivering + # 7 Settlement Completed + # 8 Delivered + # 9 Suspending of Trade + created = None + createdDate = self.safe_string(market, 'create_date') # i.e 20230101 + if createdDate is not None: + createdArray = self.string_to_chars_array(createdDate) + createdDate = createdArray[0] + createdArray[1] + createdArray[2] + createdArray[3] + '-' + createdArray[4] + createdArray[5] + '-' + createdArray[6] + createdArray[7] + ' 00:00:00' + created = self.parse8601(createdDate) + result.append({ + 'id': id, + 'lowercaseId': lowercaseId, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': type, + 'spot': spot, + 'margin': (spot and hasLeverage), + 'swap': swap, + 'future': future, + 'option': False, + 'active': active, + 'contract': contract, + 'linear': linear, + 'inverse': inverse, + 'taker': taker, + 'maker': maker, + 'contractSize': contractSize, + 'expiry': expiry, + 'expiryDatetime': self.iso8601(expiry), + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': amountPrecision, + 'price': pricePrecision, + 'cost': costPrecision, + }, + 'limits': { + 'leverage': { + 'min': self.parse_number('1'), + 'max': self.parse_number(leverageRatio), + 'superMax': self.parse_number(superLeverageRatio), + }, + 'amount': { + 'min': minAmount, + 'max': maxAmount, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': minCost, + 'max': None, + }, + }, + 'created': created, + 'info': market, + }) + return result + + def try_get_symbol_from_future_markets(self, symbolOrMarketId: str): + if symbolOrMarketId in self.markets: + return symbolOrMarketId + # only on "future" market type(inverse & linear), market-id differs between "fetchMarkets" and "fetchTicker" + # so we have to create a mapping + # - market-id from fetchMarkts: `BTC-USDT-240419`(linear future) or `BTC240412`(inverse future) + # - market-id from fetchTciker[s]: `BTC-USDT-CW` (linear future) or `BTC_CW` (inverse future) + if not ('futureMarketIdsForSymbols' in self.options): + self.options['futureMarketIdsForSymbols'] = {} + futureMarketIdsForSymbols = self.safe_dict(self.options, 'futureMarketIdsForSymbols', {}) + if symbolOrMarketId in futureMarketIdsForSymbols: + return futureMarketIdsForSymbols[symbolOrMarketId] + futureMarkets = self.filter_by(self.markets, 'future', True) + futuresCharsMaps: dict = { + 'this_week': 'CW', + 'next_week': 'NW', + 'quarter': 'CQ', + 'next_quarter': 'NQ', + } + for i in range(0, len(futureMarkets)): + market = futureMarkets[i] + info = self.safe_value(market, 'info', {}) + contractType = self.safe_string(info, 'contract_type') + contractSuffix = futuresCharsMaps[contractType] + # see comment on formats a bit above + constructedId = market['base'] + '-' + market['quote'] + '-' + contractSuffix if market['linear'] else market['base'] + '_' + contractSuffix + if constructedId == symbolOrMarketId: + symbol = market['symbol'] + self.options['futureMarketIdsForSymbols'][symbolOrMarketId] = symbol + return symbol + # if not found, just save it to avoid unnecessary future iterations + self.options['futureMarketIdsForSymbols'][symbolOrMarketId] = symbolOrMarketId + return symbolOrMarketId + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # fetchTicker + # + # { + # "amount": 26228.672978342216, + # "open": 9078.95, + # "close": 9146.86, + # "high": 9155.41, + # "id": 209988544334, + # "count": 265846, + # "low": 8988.0, + # "version": 209988544334, + # "ask": [9146.87, 0.156134], + # "vol": 2.3822168242201668E8, + # "bid": [9146.86, 0.080758], + # } + # + # fetchTickers + # + # { + # "symbol": "bhdht", + # "open": 2.3938, + # "high": 2.4151, + # "low": 2.3323, + # "close": 2.3909, + # "amount": 628.992, + # "vol": 1493.71841095, + # "count": 2088, + # "bid": 2.3643, + # "bidSize": 0.7136, + # "ask": 2.4061, + # "askSize": 0.4156 + # } + # + # watchTikcer - bbo + # { + # "seqId": 161499562790, + # "ask": 16829.51, + # "askSize": 0.707776, + # "bid": 16829.5, + # "bidSize": 1.685945, + # "quoteTime": 1671941599612, + # "symbol": "btcusdt" + # } + # + marketId = self.safe_string_2(ticker, 'symbol', 'contract_code') + symbol = self.safe_symbol(marketId, market) + symbol = self.try_get_symbol_from_future_markets(symbol) + timestamp = self.safe_integer_2(ticker, 'ts', 'quoteTime') + bid = None + bidVolume = None + ask = None + askVolume = None + if 'bid' in ticker: + if ticker['bid'] is not None and isinstance(ticker['bid'], list): + bid = self.safe_string(ticker['bid'], 0) + bidVolume = self.safe_string(ticker['bid'], 1) + else: + bid = self.safe_string(ticker, 'bid') + bidVolume = self.safe_string(ticker, 'bidSize') + if 'ask' in ticker: + if ticker['ask'] is not None and isinstance(ticker['ask'], list): + ask = self.safe_string(ticker['ask'], 0) + askVolume = self.safe_string(ticker['ask'], 1) + else: + ask = self.safe_string(ticker, 'ask') + askVolume = self.safe_string(ticker, 'askSize') + open = self.safe_string(ticker, 'open') + close = self.safe_string(ticker, 'close') + baseVolume = self.safe_string(ticker, 'amount') + quoteVolume = self.safe_string(ticker, 'vol') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': bid, + 'bidVolume': bidVolume, + 'ask': ask, + 'askVolume': askVolume, + 'vwap': None, + 'open': open, + 'close': close, + 'last': close, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'info': ticker, + }, market) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://huobiapi.github.io/docs/spot/v1/en/#get-latest-aggregated-ticker + https://huobiapi.github.io/docs/dm/v1/en/#get-market-data-overview + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#get-market-data-overview + https://huobiapi.github.io/docs/usdt_swap/v1/en/#general-get-market-data-overview + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = {} + response = None + if market['linear']: + request['contract_code'] = market['id'] + response = await self.contractPublicGetLinearSwapExMarketDetailMerged(self.extend(request, params)) + elif market['inverse']: + if market['future']: + request['symbol'] = market['id'] + response = await self.contractPublicGetMarketDetailMerged(self.extend(request, params)) + elif market['swap']: + request['contract_code'] = market['id'] + response = await self.contractPublicGetSwapExMarketDetailMerged(self.extend(request, params)) + else: + request['symbol'] = market['id'] + response = await self.spotPublicGetMarketDetailMerged(self.extend(request, params)) + # + # spot + # + # { + # "status": "ok", + # "ch": "market.btcusdt.detail.merged", + # "ts": 1583494336669, + # "tick": { + # "amount": 26228.672978342216, + # "open": 9078.95, + # "close": 9146.86, + # "high": 9155.41, + # "id": 209988544334, + # "count": 265846, + # "low": 8988.0, + # "version": 209988544334, + # "ask": [9146.87, 0.156134], + # "vol": 2.3822168242201668E8, + # "bid": [9146.86, 0.080758], + # } + # } + # + # future, swap + # + # { + # "ch":"market.BTC211126.detail.merged", + # "status":"ok", + # "tick":{ + # "amount":"669.3385682049668320322569544150680718474", + # "ask":[59117.44,48], + # "bid":[59082,48], + # "close":"59087.97", + # "count":5947, + # "high":"59892.62", + # "id":1637502670, + # "low":"57402.87", + # "open":"57638", + # "ts":1637502670059, + # "vol":"394598" + # }, + # "ts":1637502670059 + # } + # + tick = self.safe_value(response, 'tick', {}) + ticker = self.parse_ticker(tick, market) + timestamp = self.safe_integer(response, 'ts') + ticker['timestamp'] = timestamp + ticker['datetime'] = self.iso8601(timestamp) + return ticker + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://huobiapi.github.io/docs/spot/v1/en/#get-latest-tickers-for-all-pairs + https://huobiapi.github.io/docs/usdt_swap/v1/en/#general-get-a-batch-of-market-data-overview + https://huobiapi.github.io/docs/dm/v1/en/#get-a-batch-of-market-data-overview + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#get-a-batch-of-market-data-overview-v2 + + :param str[] [symbols]: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + first = self.safe_string(symbols, 0) + market = None + if first is not None: + market = self.market(first) + isSubTypeRequested = ('subType' in params) or ('business_type' in params) + type = None + subType = None + type, params = self.handle_market_type_and_params('fetchTickers', market, params) + subType, params = self.handle_sub_type_and_params('fetchTickers', market, params) + request: dict = {} + isSpot = (type == 'spot') + future = (type == 'future') + swap = (type == 'swap') + linear = (subType == 'linear') + inverse = (subType == 'inverse') + response = None + if not isSpot or isSubTypeRequested: + if linear: + # independently of type, supports calling all linear symbols i.e. fetchTickers(None, {subType:'linear'}) + if future: + request['business_type'] = 'futures' + elif swap: + request['business_type'] = 'swap' + else: + request['business_type'] = 'all' + response = await self.contractPublicGetLinearSwapExMarketDetailBatchMerged(self.extend(request, params)) + elif inverse: + if future: + response = await self.contractPublicGetMarketDetailBatchMerged(self.extend(request, params)) + elif swap: + response = await self.contractPublicGetSwapExMarketDetailBatchMerged(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchTickers() you have to set params["type"] to either "swap" or "future" for inverse contracts') + else: + raise NotSupported(self.id + ' fetchTickers() you have to set params["subType"] to either "linear" or "inverse" for contracts') + else: + response = await self.spotPublicGetMarketTickers(self.extend(request, params)) + # + # spot + # + # { + # "data":[ + # { + # "symbol":"hbcbtc", + # "open":5.313E-5, + # "high":5.34E-5, + # "low":5.112E-5, + # "close":5.175E-5, + # "amount":1183.87, + # "vol":0.0618599229, + # "count":205, + # "bid":5.126E-5, + # "bidSize":5.25, + # "ask":5.214E-5, + # "askSize":150.0 + # }, + # ], + # "status":"ok", + # "ts":1639547261293 + # } + # + # linear swap, linear future, inverse swap, inverse future + # + # { + # "status":"ok", + # "ticks":[ + # { + # "id":1637504679, + # "ts":1637504679372, + # "ask":[0.10644,100], + # "bid":[0.10624,26], + # "symbol":"TRX_CW", + # "open":"0.10233", + # "close":"0.10644", + # "low":"0.1017", + # "high":"0.10725", + # "amount":"2340267.415144052378486261756692535687481566", + # "count":882, + # "vol":"24706", + # "trade_turnover":"840726.5048", # only in linear futures + # "business_type":"futures", # only in linear futures + # "contract_code":"BTC-USDT-CW", # only in linear futures, instead of 'symbol' + # } + # ], + # "ts":1637504679376 + # } + # + rawTickers = self.safe_list_2(response, 'data', 'ticks', []) + tickers = self.parse_tickers(rawTickers, symbols, params) + return self.filter_by_array_tickers(tickers, 'symbol', symbols) + + async def fetch_last_prices(self, symbols: Strings = None, params={}): + """ + fetches the last price for multiple markets + + https://www.htx.com/en-us/opend/newApiPages/?id=8cb81024-77b5-11ed-9966-0242ac110003 linear swap & linear future + https://www.htx.com/en-us/opend/newApiPages/?id=28c2e8fc-77ae-11ed-9966-0242ac110003 inverse future + https://www.htx.com/en-us/opend/newApiPages/?id=5d517ef5-77b6-11ed-9966-0242ac110003 inverse swap + + :param str[] [symbols]: unified symbols of the markets to fetch the last prices + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of lastprices structures + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + market = self.get_market_from_symbols(symbols) + type = None + subType = None + subType, params = self.handle_sub_type_and_params('fetchLastPrices', market, params) + type, params = self.handle_market_type_and_params('fetchLastPrices', market, params) + response = None + if ((type == 'swap') or (type == 'future')) and (subType == 'linear'): + response = await self.contractPublicGetLinearSwapExMarketTrade(params) + # + # { + # "ch": "market.*.trade.detail", + # "status": "ok", + # "tick": { + # "data": [ + # { + # "amount": "4", + # "quantity": "40", + # "trade_turnover": "22.176", + # "ts": 1703697705028, + # "id": 1000003558478170000, + # "price": "0.5544", + # "direction": "buy", + # "contract_code": "MANA-USDT", + # "business_type": "swap", + # "trade_partition": "USDT" + # }, + # ], + # "id": 1703697740147, + # "ts": 1703697740147 + # }, + # "ts": 1703697740147 + # } + # + elif (type == 'swap') and (subType == 'inverse'): + response = await self.contractPublicGetSwapExMarketTrade(params) + # + # { + # "ch": "market.*.trade.detail", + # "status": "ok", + # "tick": { + # "data": [ + # { + # "amount": "6", + # "quantity": "94.5000945000945000945000945000945000945", + # "ts": 1703698704594, + # "id": 1000001187811060000, + # "price": "0.63492", + # "direction": "buy", + # "contract_code": "XRP-USD" + # }, + # ], + # "id": 1703698706589, + # "ts": 1703698706589 + # }, + # "ts": 1703698706589 + # } + # + elif (type == 'future') and (subType == 'inverse'): + response = await self.contractPublicGetMarketTrade(params) + # + # { + # "ch": "market.*.trade.detail", + # "status": "ok", + # "tick": { + # "data": [ + # { + # "amount": "20", + # "quantity": "44.4444444444444444444444444444444444444", + # "ts": 1686134498885, + # "id": 2323000000174820000, + # "price": "4.5", + # "direction": "sell", + # "symbol": "DORA_CW" + # }, + # ], + # "id": 1703698855142, + # "ts": 1703698855142 + # }, + # "ts": 1703698855142 + # } + # + else: + raise NotSupported(self.id + ' fetchLastPrices() does not support ' + type + ' markets yet') + tick = self.safe_value(response, 'tick', {}) + data = self.safe_list(tick, 'data', []) + return self.parse_last_prices(data, symbols) + + def parse_last_price(self, entry, market: Market = None): + # example responses are documented in fetchLastPrices + marketId = self.safe_string_2(entry, 'symbol', 'contract_code') + market = self.safe_market(marketId, market) + price = self.safe_number(entry, 'price') + direction = self.safe_string(entry, 'direction') # "buy" or "sell" + # group timestamp should not be assigned to the individual trades' times + return { + 'symbol': market['symbol'], + 'timestamp': None, + 'datetime': None, + 'price': price, + 'side': direction, + 'info': entry, + } + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://huobiapi.github.io/docs/spot/v1/en/#get-market-depth + https://huobiapi.github.io/docs/dm/v1/en/#get-market-depth + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#get-market-depth + https://huobiapi.github.io/docs/usdt_swap/v1/en/#general-get-market-depth + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + # + # from the API docs + # + # to get depth data within step 150, use step0, step1, step2, step3, step4, step5, step14, step15(merged depth data 0-5,14-15, when step is 0,depth data will not be merged + # to get depth data within step 20, use step6, step7, step8, step9, step10, step11, step12, step13(merged depth data 7-13), when step is 6, depth data will not be merged + # + 'type': 'step0', + # 'symbol': market['id'], # spot, future + # 'contract_code': market['id'], # swap + } + response = None + if market['linear']: + request['contract_code'] = market['id'] + response = await self.contractPublicGetLinearSwapExMarketDepth(self.extend(request, params)) + elif market['inverse']: + if market['future']: + request['symbol'] = market['id'] + response = await self.contractPublicGetMarketDepth(self.extend(request, params)) + elif market['swap']: + request['contract_code'] = market['id'] + response = await self.contractPublicGetSwapExMarketDepth(self.extend(request, params)) + else: + if limit is not None: + # Valid depths are 5, 10, 20 or empty https://huobiapi.github.io/docs/spot/v1/en/#get-market-depth + if (limit != 5) and (limit != 10) and (limit != 20) and (limit != 150): + raise BadRequest(self.id + ' fetchOrderBook() limit argument must be None, 5, 10, 20, or 150, default is 150') + # only set the depth if it is not 150 + # 150 is the implicit default on the exchange side for step0 and no orderbook aggregation + # it is not accepted by the exchange if you set it explicitly + if limit != 150: + request['depth'] = limit + request['symbol'] = market['id'] + response = await self.spotPublicGetMarketDepth(self.extend(request, params)) + # + # spot, future, swap + # + # { + # "status": "ok", + # "ch": "market.btcusdt.depth.step0", + # "ts": 1583474832790, + # "tick": { + # "bids": [ + # [9100.290000000000000000, 0.200000000000000000], + # [9099.820000000000000000, 0.200000000000000000], + # [9099.610000000000000000, 0.205000000000000000], + # ], + # "asks": [ + # [9100.640000000000000000, 0.005904000000000000], + # [9101.010000000000000000, 0.287311000000000000], + # [9101.030000000000000000, 0.012121000000000000], + # ], + # "ch":"market.BTC-USD.depth.step0", + # "ts":1583474832008, + # "id":1637554816, + # "mrid":121654491624, + # "version":104999698781 + # } + # } + # + if 'tick' in response: + if not response['tick']: + raise BadSymbol(self.id + ' fetchOrderBook() returned empty response: ' + self.json(response)) + tick = self.safe_value(response, 'tick') + timestamp = self.safe_integer(tick, 'ts', self.safe_integer(response, 'ts')) + result = self.parse_order_book(tick, symbol, timestamp) + result['nonce'] = self.safe_integer(tick, 'version') + return result + raise ExchangeError(self.id + ' fetchOrderBook() returned unrecognized response: ' + self.json(response)) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # spot fetchTrades(public) + # + # { + # "amount": 0.010411000000000000, + # "trade-id": 102090736910, + # "ts": 1583497692182, + # "id": 10500517034273194594947, + # "price": 9096.050000000000000000, + # "direction": "sell" + # } + # + # spot fetchMyTrades(private) + # + # { + # "symbol": "swftcbtc", + # "fee-currency": "swftc", + # "filled-fees": "0", + # "source": "spot-api", + # "id": 83789509854000, + # "type": "buy-limit", + # "order-id": 83711103204909, + # 'filled-points': "0.005826843283532154", + # "fee-deduct-currency": "ht", + # 'filled-amount': "45941.53", + # "price": "0.0000001401", + # "created-at": 1597933260729, + # "match-id": 100087455560, + # "role": "maker", + # "trade-id": 100050305348 + # } + # + # linear swap isolated margin fetchOrder details + # + # { + # "trade_id": 131560927, + # "trade_price": 13059.800000000000000000, + # "trade_volume": 1.000000000000000000, + # "trade_turnover": 13.059800000000000000, + # "trade_fee": -0.005223920000000000, + # "created_at": 1603703614715, + # "role": "taker", + # "fee_asset": "USDT", + # "profit": 0, + # "real_profit": 0, + # "id": "131560927-770334322963152896-1" + # } + # + # inverse swap cross margin fetchMyTrades + # + # { + # "contract_type":"swap", + # "pair":"O3-USDT", + # "business_type":"swap", + # "query_id":652123190, + # "match_id":28306009409, + # "order_id":941137865226903553, + # "symbol":"O3", + # "contract_code":"O3-USDT", + # "direction":"sell", + # "offset":"open", + # "trade_volume":100.000000000000000000, + # "trade_price":0.398500000000000000, + # "trade_turnover":39.850000000000000000, + # "trade_fee":-0.007970000000000000, + # "offset_profitloss":0E-18, + # "create_date":1644426352999, + # "role":"Maker", + # "order_source":"api", + # "order_id_str":"941137865226903553", + # "id":"28306009409-941137865226903553-1", + # "fee_asset":"USDT", + # "margin_mode":"cross", + # "margin_account":"USDT", + # "real_profit":0E-18, + # "trade_partition":"USDT" + # } + # + marketId = self.safe_string_2(trade, 'contract_code', 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + timestamp = self.safe_integer_2(trade, 'ts', 'created-at') + timestamp = self.safe_integer_2(trade, 'created_at', 'create_date', timestamp) + order = self.safe_string_2(trade, 'order-id', 'order_id') + side = self.safe_string(trade, 'direction') + type = self.safe_string(trade, 'type') + if type is not None: + typeParts = type.split('-') + side = typeParts[0] + type = typeParts[1] + takerOrMaker = self.safe_string_lower(trade, 'role') + priceString = self.safe_string_2(trade, 'price', 'trade_price') + amountString = self.safe_string_2(trade, 'filled-amount', 'amount') + amountString = self.safe_string(trade, 'trade_volume', amountString) + costString = self.safe_string(trade, 'trade_turnover') + fee = None + feeCost = self.safe_string(trade, 'filled-fees') + if feeCost is None: + feeCost = Precise.string_neg(self.safe_string(trade, 'trade_fee')) + feeCurrencyId = self.safe_string_2(trade, 'fee-currency', 'fee_asset') + feeCurrency = self.safe_currency_code(feeCurrencyId) + filledPoints = self.safe_string(trade, 'filled-points') + if filledPoints is not None: + if (feeCost is None) or Precise.string_equals(feeCost, '0'): + feeDeductCurrency = self.safe_string(trade, 'fee-deduct-currency') + if feeDeductCurrency is not None: + feeCost = filledPoints + feeCurrency = self.safe_currency_code(feeDeductCurrency) + if feeCost is not None: + fee = { + 'cost': feeCost, + 'currency': feeCurrency, + } + # htx's multi-market trade-id is a bit complex to parse accordingly. + # - for `id` which contains hyphen, it would be the unique id, eg. xxxxxx-1, xxxxxx-2(self happens mostly for contract markets) + # - otherwise the least priority is given to the `id` key + id: Str = None + safeId = self.safe_string(trade, 'id') + if safeId is not None and safeId.find('-') >= 0: + id = safeId + else: + id = self.safe_string_n(trade, ['trade_id', 'trade-id', 'id']) + return self.safe_trade({ + 'id': id, + 'info': trade, + 'order': order, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'type': type, + 'side': side, + 'takerOrMaker': takerOrMaker, + 'price': priceString, + 'amount': amountString, + 'cost': costString, + 'fee': fee, + }, market) + + async def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all the trades made from a single order + + https://huobiapi.github.io/docs/spot/v1/en/#get-the-match-result-of-an-order + + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + market = None + if symbol is not None: + market = self.market(symbol) + marketType = None + marketType, params = self.handle_market_type_and_params('fetchOrderTrades', market, params) + if marketType != 'spot': + raise NotSupported(self.id + ' fetchOrderTrades() is only supported for spot markets') + return await self.fetch_spot_order_trades(id, symbol, since, limit, params) + + async def fetch_spot_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + @ignore + fetch all the trades made from a single order + + https://huobiapi.github.io/docs/spot/v1/en/#get-the-match-result-of-an-order + + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + request: dict = { + 'order-id': id, + } + response = await self.spotPrivateGetV1OrderOrdersOrderIdMatchresults(self.extend(request, params)) + return self.parse_trades(response['data'], None, since, limit) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + + https://huobiapi.github.io/docs/usdt_swap/v1/en/#isolated-get-history-match-results-via-multiple-fields-new + https://huobiapi.github.io/docs/usdt_swap/v1/en/#cross-get-history-match-results-via-multiple-fields-new + https://huobiapi.github.io/docs/spot/v1/en/#search-match-results + + fetch all trades made by the user + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch trades for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchMyTrades', 'paginate') + if paginate: + return await self.fetch_paginated_call_dynamic('fetchMyTrades', symbol, since, limit, params) + market = None + if symbol is not None: + market = self.market(symbol) + marketType = None + marketType, params = self.handle_market_type_and_params('fetchMyTrades', market, params) + request: dict = { + # spot ----------------------------------------------------------- + # 'symbol': market['id'], + # 'types': 'buy-market,sell-market,buy-limit,sell-limit,buy-ioc,sell-ioc,buy-limit-maker,sell-limit-maker,buy-stop-limit,sell-stop-limit', + # 'start-time': since, # max 48 hours within 120 days + # 'end-time': self.milliseconds(), # max 48 hours within 120 days + # 'from': 'id', # tring False N/A Search internal id to begin with if search next page, then self should be the last id(not trade-id) of last page; if search previous page, then self should be the first id(not trade-id) of last page + # 'direct': 'next', # next, prev + # 'size': limit, # default 100, max 500 The number of orders to return [1-500] + # contracts ------------------------------------------------------ + # 'symbol': market['settleId'], # required + # 'trade_type': 0, # required, 0 all, 1 open long, 2 open short, 3 close short, 4 close long, 5 liquidate long positions, 6 liquidate short positions + # 'contract_code': market['id'], + # 'start_time': since, # max 48 hours within 120 days + # 'end_time': self.milliseconds(), # max 48 hours within 120 days + # 'from_id': 'id', # tring False N/A Search internal id to begin with if search next page, then self should be the last id(not trade-id) of last page; if search previous page, then self should be the first id(not trade-id) of last page + # 'direct': 'prev', # next, prev + # 'size': limit, # default 20, max 50 + } + response = None + if marketType == 'spot': + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if limit is not None: + request['size'] = limit # default 100, max 500 + if since is not None: + request['start-time'] = since # a date within 120 days from today + # request['end-time'] = self.sum(since, 172800000) # 48 hours window + request, params = self.handle_until_option('end-time', request, params) + response = await self.spotPrivateGetV1OrderMatchresults(self.extend(request, params)) + else: + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMyTrades() requires a symbol argument') + request['contract'] = market['id'] + request['trade_type'] = 0 # 0 all, 1 open long, 2 open short, 3 close short, 4 close long, 5 liquidate long positions, 6 liquidate short positions + if since is not None: + request['start_time'] = since # a date within 120 days from today + # request['end_time'] = self.sum(request['start_time'], 172800000) # 48 hours window + request, params = self.handle_until_option('end_time', request, params) + if limit is not None: + request['page_size'] = limit # default 100, max 500 + if market['linear']: + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchMyTrades', params) + marginMode = 'cross' if (marginMode is None) else marginMode + if marginMode == 'isolated': + response = await self.contractPrivatePostLinearSwapApiV3SwapMatchresultsExact(self.extend(request, params)) + elif marginMode == 'cross': + response = await self.contractPrivatePostLinearSwapApiV3SwapCrossMatchresultsExact(self.extend(request, params)) + elif market['inverse']: + if marketType == 'future': + request['symbol'] = market['settleId'] + response = await self.contractPrivatePostApiV3ContractMatchresultsExact(self.extend(request, params)) + elif marketType == 'swap': + response = await self.contractPrivatePostSwapApiV3SwapMatchresultsExact(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchMyTrades() does not support ' + marketType + ' markets') + # + # spot + # + # { + # "status": "ok", + # "data": [ + # { + # "symbol": "polyusdt", + # "fee-currency": "poly", + # "source": "spot-web", + # "price": "0.338", + # "created-at": 1629443051839, + # "role": "taker", + # "order-id": 345487249132375, + # "match-id": 5014, + # "trade-id": 1085, + # "filled-amount": "147.928994082840236", + # "filled-fees": "0", + # "filled-points": "0.1", + # "fee-deduct-currency": "hbpoint", + # "fee-deduct-state": "done", + # "id": 313288753120940, + # "type": "buy-market" + # } + # ] + # } + # + # contracts + # + # { + # "status": "ok", + # "data": { + # "trades": [ + # { + # "query_id": 2424420723, + # "match_id": 113891764710, + # "order_id": 773135295142658048, + # "symbol": "ADA", + # "contract_type": "quarter", # swap + # "business_type": "futures", # swap + # "contract_code": "ADA201225", + # "direction": "buy", + # "offset": "open", + # "trade_volume": 1, + # "trade_price": 0.092, + # "trade_turnover": 10, + # "trade_fee": -0.021739130434782608, + # "offset_profitloss": 0, + # "create_date": 1604371703183, + # "role": "Maker", + # "order_source": "web", + # "order_id_str": "773135295142658048", + # "fee_asset": "ADA", + # "margin_mode": "isolated", # cross + # "margin_account": "BTC-USDT", + # "real_profit": 0, + # "id": "113891764710-773135295142658048-1", + # "trade_partition":"USDT", + # } + # ], + # "remain_size": 15, + # "next_id": 2424413094 + # }, + # "ts": 1604372202243 + # } + # + trades = self.safe_value(response, 'data') + if not isinstance(trades, list): + trades = self.safe_value(trades, 'trades') + return self.parse_trades(trades, market, since, limit) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = 1000, params={}) -> List[Trade]: + """ + + https://huobiapi.github.io/docs/spot/v1/en/#get-the-most-recent-trades + https://huobiapi.github.io/docs/dm/v1/en/#query-a-batch-of-trade-records-of-a-contract + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#query-a-batch-of-trade-records-of-a-contract + https://huobiapi.github.io/docs/usdt_swap/v1/en/#general-query-a-batch-of-trade-records-of-a-contract + + get the list of most recent trades for a particular symbol + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + # 'symbol': market['id'], # spot, future + # 'contract_code': market['id'], # swap + } + if limit is not None: + request['size'] = min(limit, 2000) # max 2000 + response = None + if market['future']: + if market['inverse']: + request['symbol'] = market['id'] + response = await self.contractPublicGetMarketHistoryTrade(self.extend(request, params)) + elif market['linear']: + request['contract_code'] = market['id'] + response = await self.contractPublicGetLinearSwapExMarketHistoryTrade(self.extend(request, params)) + elif market['swap']: + request['contract_code'] = market['id'] + if market['inverse']: + response = await self.contractPublicGetSwapExMarketHistoryTrade(self.extend(request, params)) + elif market['linear']: + response = await self.contractPublicGetLinearSwapExMarketHistoryTrade(self.extend(request, params)) + else: + request['symbol'] = market['id'] + response = await self.spotPublicGetMarketHistoryTrade(self.extend(request, params)) + # + # { + # "status": "ok", + # "ch": "market.btcusdt.trade.detail", + # "ts": 1583497692365, + # "data": [ + # { + # "id": 105005170342, + # "ts": 1583497692182, + # "data": [ + # { + # "amount": 0.010411000000000000, + # "trade-id": 102090736910, + # "ts": 1583497692182, + # "id": 10500517034273194594947, + # "price": 9096.050000000000000000, + # "direction": "sell" + # } + # ] + # }, + # # ... + # ] + # } + # + data = self.safe_value(response, 'data', []) + result = [] + for i in range(0, len(data)): + trades = self.safe_value(data[i], 'data', []) + for j in range(0, len(trades)): + trade = self.parse_trade(trades[j], market) + result.append(trade) + result = self.sort_by(result, 'timestamp') + return self.filter_by_symbol_since_limit(result, market['symbol'], since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # { + # "amount":1.2082, + # "open":0.025096, + # "close":0.025095, + # "high":0.025096, + # "id":1591515300, + # "count":6, + # "low":0.025095, + # "vol":0.0303205097 + # } + # + return [ + self.safe_timestamp(ohlcv, 'id'), + self.safe_number(ohlcv, 'open'), + self.safe_number(ohlcv, 'high'), + self.safe_number(ohlcv, 'low'), + self.safe_number(ohlcv, 'close'), + self.safe_number(ohlcv, 'amount'), + ] + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://huobiapi.github.io/docs/spot/v1/en/#get-klines-candles + https://huobiapi.github.io/docs/dm/v1/en/#get-kline-data + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#get-kline-data + https://huobiapi.github.io/docs/usdt_swap/v1/en/#general-get-kline-data + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param str [params.useHistoricalEndpointForSpot]: True/false - whether use the historical candles endpoint for spot markets or default klines endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate') + if paginate: + return await self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params, 1000) + market = self.market(symbol) + request: dict = { + 'period': self.safe_string(self.timeframes, timeframe, timeframe), + # 'symbol': market['id'], # spot, future + # 'contract_code': market['id'], # swap + # 'size': 1000, # max 1000 for spot, 2000 for contracts + # 'from': int((since / str(1000))), spot only + # 'to': self.seconds(), spot only + } + priceType = self.safe_string_n(params, ['priceType', 'price']) + params = self.omit(params, ['priceType', 'price']) + until = None + until, params = self.handle_param_integer(params, 'until') + untilSeconds = self.parse_to_int(until / 1000) if (until is not None) else None + if market['contract']: + if limit is not None: + request['size'] = min(limit, 2000) # when using limit: from & to are ignored + # https://huobiapi.github.io/docs/usdt_swap/v1/en/#general-get-kline-data + else: + limit = 2000 # only used for from/to calculation + if priceType is None: + duration = self.parse_timeframe(timeframe) + calcualtedEnd = None + if since is None: + now = self.seconds() + request['from'] = now - duration * (limit - 1) + calcualtedEnd = now + else: + start = self.parse_to_int(since / 1000) + request['from'] = start + calcualtedEnd = self.sum(start, duration * (limit - 1)) + request['to'] = untilSeconds if (untilSeconds is not None) else calcualtedEnd + response = None + if market['future']: + if market['inverse']: + request['symbol'] = market['id'] + if priceType == 'mark': + response = await self.contractPublicGetIndexMarketHistoryMarkPriceKline(self.extend(request, params)) + elif priceType == 'index': + response = await self.contractPublicGetIndexMarketHistoryIndex(self.extend(request, params)) + elif priceType == 'premiumIndex': + raise BadRequest(self.id + ' ' + market['type'] + ' has no api endpoint for ' + priceType + ' kline data') + else: + response = await self.contractPublicGetMarketHistoryKline(self.extend(request, params)) + elif market['linear']: + request['contract_code'] = market['id'] + if priceType == 'mark': + response = await self.contractPublicGetIndexMarketHistoryLinearSwapMarkPriceKline(self.extend(request, params)) + elif priceType == 'index': + raise BadRequest(self.id + ' ' + market['type'] + ' has no api endpoint for ' + priceType + ' kline data') + elif priceType == 'premiumIndex': + response = await self.contractPublicGetIndexMarketHistoryLinearSwapPremiumIndexKline(self.extend(request, params)) + else: + response = await self.contractPublicGetLinearSwapExMarketHistoryKline(self.extend(request, params)) + elif market['swap']: + request['contract_code'] = market['id'] + if market['inverse']: + if priceType == 'mark': + response = await self.contractPublicGetIndexMarketHistorySwapMarkPriceKline(self.extend(request, params)) + elif priceType == 'index': + raise BadRequest(self.id + ' ' + market['type'] + ' has no api endpoint for ' + priceType + ' kline data') + elif priceType == 'premiumIndex': + response = await self.contractPublicGetIndexMarketHistorySwapPremiumIndexKline(self.extend(request, params)) + else: + response = await self.contractPublicGetSwapExMarketHistoryKline(self.extend(request, params)) + elif market['linear']: + if priceType == 'mark': + response = await self.contractPublicGetIndexMarketHistoryLinearSwapMarkPriceKline(self.extend(request, params)) + elif priceType == 'index': + raise BadRequest(self.id + ' ' + market['type'] + ' has no api endpoint for ' + priceType + ' kline data') + elif priceType == 'premiumIndex': + response = await self.contractPublicGetIndexMarketHistoryLinearSwapPremiumIndexKline(self.extend(request, params)) + else: + response = await self.contractPublicGetLinearSwapExMarketHistoryKline(self.extend(request, params)) + else: + request['symbol'] = market['id'] + useHistorical = None + useHistorical, params = self.handle_option_and_params(params, 'fetchOHLCV', 'useHistoricalEndpointForSpot', True) + if not useHistorical: + if limit is not None: + request['size'] = min(limit, 2000) # max 2000 + response = await self.spotPublicGetMarketHistoryKline(self.extend(request, params)) + else: + # "from & to" only available for the self endpoint + if since is not None: + request['from'] = self.parse_to_int(since / 1000) + if untilSeconds is not None: + request['to'] = untilSeconds + if limit is not None: + request['size'] = min(1000, limit) # max 1000, otherwise default returns 150 + response = await self.spotPublicGetMarketHistoryCandles(self.extend(request, params)) + # + # { + # "status":"ok", + # "ch":"market.ethbtc.kline.1min", + # "ts":1591515374371, + # "data":[ + # {"amount":0.0,"open":0.025095,"close":0.025095,"high":0.025095,"id":1591515360,"count":0,"low":0.025095,"vol":0.0}, + # {"amount":1.2082,"open":0.025096,"close":0.025095,"high":0.025096,"id":1591515300,"count":6,"low":0.025095,"vol":0.0303205097}, + # {"amount":0.0648,"open":0.025096,"close":0.025096,"high":0.025096,"id":1591515240,"count":2,"low":0.025096,"vol":0.0016262208}, + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_ohlcvs(data, market, timeframe, since, limit) + + async def fetch_accounts(self, params={}) -> List[Account]: + """ + fetch all the accounts associated with a profile + + https://huobiapi.github.io/docs/spot/v1/en/#get-all-accounts-of-the-current-user + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `account structures ` indexed by the account type + """ + await self.load_markets() + response = await self.spotPrivateGetV1AccountAccounts(params) + # + # { + # "status":"ok", + # "data":[ + # {"id":5202591,"type":"point","subtype":"","state":"working"}, + # {"id":1528640,"type":"spot","subtype":"","state":"working"}, + # ] + # } + # + data = self.safe_value(response, 'data') + return self.parse_accounts(data) + + def parse_account(self, account): + # + # { + # "id": 5202591, + # "type": "point", # spot, margin, otc, point, super-margin, investment, borrow, grid-trading, deposit-earning, otc-options + # "subtype": "", # The corresponding trading symbol(currency pair) the isolated margin is based on, e.g. btcusdt + # "state": "working" # working, lock + # } + # + typeId = self.safe_string(account, 'type') + accountsById = self.safe_value(self.options, 'accountsById', {}) + type = self.safe_value(accountsById, typeId, typeId) + return { + 'info': account, + 'id': self.safe_string(account, 'id'), + 'type': type, + 'code': None, + } + + async def fetch_account_id_by_type(self, type: str, marginMode: Str = None, symbol: Str = None, params={}): + """ + fetch all the accounts by a type and marginModeassociated with a profile + + https://huobiapi.github.io/docs/spot/v1/en/#get-all-accounts-of-the-current-user + + :param str type: 'spot', 'swap' or 'future + :param str [marginMode]: 'cross' or 'isolated' + :param str [symbol]: unified ccxt market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `account structures ` indexed by the account type + """ + accounts = await self.load_accounts() + accountId = self.safe_value_2(params, 'accountId', 'account-id') + if accountId is not None: + return accountId + if type == 'spot': + if marginMode == 'cross': + type = 'super-margin' + elif marginMode == 'isolated': + type = 'margin' + marketId = None + if symbol is not None: + marketId = self.market_id(symbol) + for i in range(0, len(accounts)): + account = accounts[i] + info = self.safe_value(account, 'info') + subtype = self.safe_string(info, 'subtype', None) + typeFromAccount = self.safe_string(account, 'type') + if type == 'margin': + if subtype == marketId: + return self.safe_string(account, 'id') + elif type == typeFromAccount: + return self.safe_string(account, 'id') + defaultAccount = self.safe_value(accounts, 0, {}) + return self.safe_string(defaultAccount, 'id') + + async def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://huobiapi.github.io/docs/spot/v1/en/#apiv2-currency-amp-chains + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = await self.spotPublicGetV2ReferenceCurrencies(params) + # + # { + # "code": 200, + # "data": [ + # { + # "currency": "sxp", + # "assetType": "1", + # "chains": [ + # { + # "chain": "sxp", + # "displayName": "ERC20", + # "baseChain": "ETH", + # "baseChainProtocol": "ERC20", + # "isDynamic": True, + # "numOfConfirmations": "12", + # "numOfFastConfirmations": "12", + # "depositStatus": "allowed", + # "minDepositAmt": "0.23", + # "withdrawStatus": "allowed", + # "minWithdrawAmt": "0.23", + # "withdrawPrecision": "8", + # "maxWithdrawAmt": "227000.000000000000000000", + # "withdrawQuotaPerDay": "227000.000000000000000000", + # "withdrawQuotaPerYear": null, + # "withdrawQuotaTotal": null, + # "withdrawFeeType": "fixed", + # "transactFeeWithdraw": "11.1654", + # "addrWithTag": False, + # "addrDepositTag": False + # } + # ], + # "instStatus": "normal" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + result: dict = {} + self.options['networkChainIdsByNames'] = {} + self.options['networkNamesByChainIds'] = {} + for i in range(0, len(data)): + entry = data[i] + currencyId = self.safe_string(entry, 'currency') + code = self.safe_currency_code(currencyId) + assetType = self.safe_string(entry, 'assetType') + type = assetType == 'crypto' if '1' else 'fiat' + self.options['networkChainIdsByNames'][code] = {} + chains = self.safe_list(entry, 'chains', []) + networks: dict = {} + for j in range(0, len(chains)): + chainEntry = chains[j] + uniqueChainId = self.safe_string(chainEntry, 'chain') # i.e. usdterc20, trc20usdt ... + title = self.safe_string_2(chainEntry, 'baseChain', 'displayName') # baseChain and baseChainProtocol are together existent or inexistent in entries, but baseChain is preferred. when they are both inexistent, then we use generic displayName + self.options['networkChainIdsByNames'][code][title] = uniqueChainId + self.options['networkNamesByChainIds'][uniqueChainId] = title + networkCode = self.network_id_to_code(uniqueChainId) + networks[networkCode] = { + 'info': chainEntry, + 'id': uniqueChainId, + 'network': networkCode, + 'limits': { + 'deposit': { + 'min': self.safe_number(chainEntry, 'minDepositAmt'), + 'max': None, + }, + 'withdraw': { + 'min': self.safe_number(chainEntry, 'minWithdrawAmt'), + 'max': self.safe_number(chainEntry, 'maxWithdrawAmt'), + }, + }, + 'active': None, + 'deposit': self.safe_string(chainEntry, 'depositStatus') == 'allowed', + 'withdraw': self.safe_string(chainEntry, 'withdrawStatus') == 'allowed', + 'fee': self.safe_number(chainEntry, 'transactFeeWithdraw'), + 'precision': self.parse_number(self.parse_precision(self.safe_string(chainEntry, 'withdrawPrecision'))), + } + result[code] = self.safe_currency_structure({ + 'info': entry, + 'code': code, + 'id': currencyId, + 'active': self.safe_string(entry, 'instStatus') == 'normal', + 'deposit': None, + 'withdraw': None, + 'fee': None, + 'name': None, + 'type': type, + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + 'deposit': { + 'min': None, + 'max': None, + }, + }, + 'precision': None, + 'networks': networks, + }) + return result + + def network_id_to_code(self, networkId: Str = None, currencyCode: Str = None): + # here network-id is provided pair of currency & chain(i.e. trc20usdt) + keys = list(self.options['networkNamesByChainIds'].keys()) + keysLength = len(keys) + if keysLength == 0: + raise ExchangeError(self.id + ' networkIdToCode() - markets need to be loaded at first') + networkTitle = self.safe_value(self.options['networkNamesByChainIds'], networkId, networkId) + return super(htx, self).network_id_to_code(networkTitle) + + def network_code_to_id(self, networkCode: str, currencyCode: Str = None): + if currencyCode is None: + raise ArgumentsRequired(self.id + ' networkCodeToId() requires a currencyCode argument') + keys = list(self.options['networkChainIdsByNames'].keys()) + keysLength = len(keys) + if keysLength == 0: + raise ExchangeError(self.id + ' networkCodeToId() - markets need to be loaded at first') + uniqueNetworkIds = self.safe_value(self.options['networkChainIdsByNames'], currencyCode, {}) + if networkCode in uniqueNetworkIds: + return uniqueNetworkIds[networkCode] + else: + networkTitle = super(htx, self).network_code_to_id(networkCode) + return self.safe_value(uniqueNetworkIds, networkTitle, networkTitle) + + async def fetch_balance(self, params={}) -> Balances: + """ + + https://huobiapi.github.io/docs/spot/v1/en/#get-account-balance-of-a-specific-account + https://www.htx.com/en-us/opend/newApiPages/?id=7ec4b429-7773-11ed-9966-0242ac110003 + https://www.htx.com/en-us/opend/newApiPages/?id=10000074-77b7-11ed-9966-0242ac110003 + https://huobiapi.github.io/docs/dm/v1/en/#query-asset-valuation + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#query-user-s-account-information + https://huobiapi.github.io/docs/usdt_swap/v1/en/#isolated-query-user-s-account-information + https://huobiapi.github.io/docs/usdt_swap/v1/en/#cross-query-user-39-s-account-information + + query for balance and get the amount of funds available for trading or funds locked in orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.unified]: provide self parameter if you have a recent account with unified cross+isolated margin account + :returns dict: a `balance structure ` + """ + await self.load_markets() + type = None + type, params = self.handle_market_type_and_params('fetchBalance', None, params) + options = self.safe_value(self.options, 'fetchBalance', {}) + isUnifiedAccount = self.safe_value_2(params, 'isUnifiedAccount', 'unified', False) + params = self.omit(params, ['isUnifiedAccount', 'unified']) + request: dict = {} + spot = (type == 'spot') + future = (type == 'future') + defaultSubType = self.safe_string_2(self.options, 'defaultSubType', 'subType', 'linear') + subType = self.safe_string_2(options, 'defaultSubType', 'subType', defaultSubType) + subType = self.safe_string_2(params, 'defaultSubType', 'subType', subType) + inverse = (subType == 'inverse') + linear = (subType == 'linear') + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchBalance', params) + params = self.omit(params, ['defaultSubType', 'subType']) + isolated = (marginMode == 'isolated') + cross = (marginMode == 'cross') + margin = (type == 'margin') or (spot and (cross or isolated)) + response = None + if spot or margin: + if margin: + if isolated: + response = await self.spotPrivateGetV1MarginAccountsBalance(self.extend(request, params)) + else: + response = await self.spotPrivateGetV1CrossMarginAccountsBalance(self.extend(request, params)) + else: + await self.load_accounts() + accountId = await self.fetch_account_id_by_type(type, None, None, params) + request['account-id'] = accountId + response = await self.spotPrivateGetV1AccountAccountsAccountIdBalance(self.extend(request, params)) + elif isUnifiedAccount: + response = await self.contractPrivateGetLinearSwapApiV3UnifiedAccountInfo(self.extend(request, params)) + elif linear: + if isolated: + response = await self.contractPrivatePostLinearSwapApiV1SwapAccountInfo(self.extend(request, params)) + else: + response = await self.contractPrivatePostLinearSwapApiV1SwapCrossAccountInfo(self.extend(request, params)) + elif inverse: + if future: + response = await self.contractPrivatePostApiV1ContractAccountInfo(self.extend(request, params)) + else: + response = await self.contractPrivatePostSwapApiV1SwapAccountInfo(self.extend(request, params)) + # + # spot + # + # { + # "status": "ok", + # "data": { + # "id": 1528640, + # "type": "spot", + # "state": "working", + # "list": [ + # {"currency": "lun", "type": "trade", "balance": "0", "seq-num": "0"}, + # {"currency": "lun", "type": "frozen", "balance": "0", "seq-num": "0"}, + # {"currency": "ht", "type": "frozen", "balance": "0", "seq-num": "145"}, + # ] + # }, + # "ts":1637644827566 + # } + # + # cross margin + # + # { + # "status": "ok", + # "data": { + # "id": 51015302, + # "type": "cross-margin", + # "state": "working", + # "risk-rate": "2", + # "acct-balance-sum": "100", + # "debt-balance-sum": "0", + # "list": [ + # {"currency": "usdt", "type": "trade", "balance": "100"}, + # {"currency": "usdt", "type": "frozen", "balance": "0"}, + # {"currency": "usdt", "type": "loan-available", "balance": "200"}, + # {"currency": "usdt", "type": "transfer-out-available", "balance": "-1"}, + # {"currency": "ht", "type": "loan-available", "balance": "36.60724091"}, + # {"currency": "ht", "type": "transfer-out-available", "balance": "-1"}, + # {"currency": "btc", "type": "trade", "balance": "1168.533000000000000000"}, + # {"currency": "btc", "type": "frozen", "balance": "0.000000000000000000"}, + # {"currency": "btc", "type": "loan", "balance": "-2.433000000000000000"}, + # {"currency": "btc", "type": "interest", "balance": "-0.000533000000000000"}, + # {"currency": "btc", "type": "transfer-out-available", "balance": "1163.872174670000000000"}, + # {"currency": "btc", "type": "loan-available", "balance": "8161.876538350676000000"} + # ] + # }, + # "code": 200 + # } + # + # isolated margin + # + # { + # "data": [ + # { + # "id": 18264, + # "type": "margin", + # "state": "working", + # "symbol": "btcusdt", + # "fl-price": "0", + # "fl-type": "safe", + # "risk-rate": "475.952571086994250554", + # "list": [ + # {"currency": "btc","type": "trade","balance": "1168.533000000000000000"}, + # {"currency": "btc","type": "frozen","balance": "0.000000000000000000"}, + # {"currency": "btc","type": "loan","balance": "-2.433000000000000000"}, + # {"currency": "btc","type": "interest","balance": "-0.000533000000000000"}, + # {"currency": "btc","type": "transfer-out-available", "balance": "1163.872174670000000000"}, + # {"currency": "btc","type": "loan-available", "balance": "8161.876538350676000000"} + # ] + # } + # ] + # } + # + # future, swap isolated + # + # { + # "status": "ok", + # "data": [ + # { + # "symbol": "BTC", + # "margin_balance": 0, + # "margin_position": 0E-18, + # "margin_frozen": 0, + # "margin_available": 0E-18, + # "profit_real": 0, + # "profit_unreal": 0, + # "risk_rate": null, + # "withdraw_available": 0, + # "liquidation_price": null, + # "lever_rate": 5, + # "adjust_factor": 0.025000000000000000, + # "margin_static": 0, + # "is_debit": 0, # future only + # "contract_code": "BTC-USD", # swap only + # "margin_asset": "USDT", # linear only + # "margin_mode": "isolated", # linear only + # "margin_account": "BTC-USDT" # linear only + # "transfer_profit_ratio": null # inverse only + # }, + # ], + # "ts": 1637644827566 + # } + # + # linear cross futures and linear cross swap + # + # { + # "status": "ok", + # "data": [ + # { + # "futures_contract_detail": [ + # { + # "symbol": "ETH", + # "contract_code": "ETH-USDT-220325", + # "margin_position": 0, + # "margin_frozen": 0, + # "margin_available": 200.000000000000000000, + # "profit_unreal": 0E-18, + # "liquidation_price": null, + # "lever_rate": 5, + # "adjust_factor": 0.060000000000000000, + # "contract_type": "quarter", + # "pair": "ETH-USDT", + # "business_type": "futures" + # }, + # ], + # "margin_mode": "cross", + # "margin_account": "USDT", + # "margin_asset": "USDT", + # "margin_balance": 49.874186030200000000, + # "money_in": 50, + # "money_out": 0, + # "margin_static": 49.872786030200000000, + # "margin_position": 6.180000000000000000, + # "margin_frozen": 6.000000000000000000, + # "profit_unreal": 0.001400000000000000, + # "withdraw_available": 37.6927860302, + # "risk_rate": 271.984050521072796934, + # "new_risk_rate": 0.001858676950514399, + # "contract_detail": [ + # { + # "symbol": "MANA", + # "contract_code": "MANA-USDT", + # "margin_position": 0, + # "margin_frozen": 0, + # "margin_available": 200.000000000000000000, + # "profit_unreal": 0E-18, + # "liquidation_price": null, + # "lever_rate": 5, + # "adjust_factor": 0.100000000000000000, + # "contract_type": "swap", + # "pair": "MANA-USDT", + # "business_type": "swap" + # }, + # ] + # } + # ], + # "ts": 1640915104870 + # } + # + # TODO add balance parsing for linear swap + # + result: dict = {'info': response} + data = self.safe_value(response, 'data') + if spot or margin: + if isolated: + for i in range(0, len(data)): + entry = data[i] + symbol = self.safe_symbol(self.safe_string(entry, 'symbol')) + balances = self.safe_value(entry, 'list') + subResult: dict = {} + for j in range(0, len(balances)): + balance = balances[j] + currencyId = self.safe_string(balance, 'currency') + code = self.safe_currency_code(currencyId) + subResult[code] = self.parse_margin_balance_helper(balance, code, subResult) + result[symbol] = self.safe_balance(subResult) + else: + balances = self.safe_value(data, 'list', []) + for i in range(0, len(balances)): + balance = balances[i] + currencyId = self.safe_string(balance, 'currency') + code = self.safe_currency_code(currencyId) + result[code] = self.parse_margin_balance_helper(balance, code, result) + result = self.safe_balance(result) + elif isUnifiedAccount: + for i in range(0, len(data)): + entry = data[i] + marginAsset = self.safe_string(entry, 'margin_asset') + currencyCode = self.safe_currency_code(marginAsset) + if isolated: + isolated_swap = self.safe_value(entry, 'isolated_swap', {}) + for j in range(0, len(isolated_swap)): + balance = isolated_swap[j] + marketId = self.safe_string(balance, 'contract_code') + subBalance: dict = { + 'code': currencyCode, + 'free': self.safe_number(balance, 'margin_available'), + } + symbol = self.safe_symbol(marketId) + result[symbol] = subBalance + result = self.safe_balance(result) + else: + account = self.account() + account['free'] = self.safe_string(entry, 'margin_static') + account['used'] = self.safe_string(entry, 'margin_frozen') + result[currencyCode] = account + result = self.safe_balance(result) + elif linear: + first = self.safe_value(data, 0, {}) + if isolated: + for i in range(0, len(data)): + balance = data[i] + marketId = self.safe_string_2(balance, 'contract_code', 'margin_account') + market = self.safe_market(marketId) + currencyId = self.safe_string(balance, 'margin_asset') + currency = self.safe_currency(currencyId) + code = self.safe_string(market, 'settle', currency['code']) + # the exchange outputs positions for delisted markets + # https://www.huobi.com/support/en-us/detail/74882968522337 + # we skip it if the market was delisted + if code is not None: + account = self.account() + account['free'] = self.safe_string(balance, 'margin_balance') + account['used'] = self.safe_string(balance, 'margin_frozen') + accountsByCode: dict = {} + accountsByCode[code] = account + symbol = market['symbol'] + result[symbol] = self.safe_balance(accountsByCode) + else: + account = self.account() + account['free'] = self.safe_string(first, 'withdraw_available') + account['total'] = self.safe_string(first, 'margin_balance') + currencyId = self.safe_string_2(first, 'margin_asset', 'symbol') + code = self.safe_currency_code(currencyId) + result[code] = account + result = self.safe_balance(result) + elif inverse: + for i in range(0, len(data)): + balance = data[i] + currencyId = self.safe_string(balance, 'symbol') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(balance, 'margin_available') + account['used'] = self.safe_string(balance, 'margin_frozen') + result[code] = account + result = self.safe_balance(result) + return result + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://huobiapi.github.io/docs/spot/v1/en/#get-the-order-detail-of-an-order-based-on-client-order-id + https://huobiapi.github.io/docs/spot/v1/en/#get-the-order-detail-of-an-order + https://huobiapi.github.io/docs/usdt_swap/v1/en/#isolated-get-information-of-an-order + https://huobiapi.github.io/docs/usdt_swap/v1/en/#cross-get-information-of-order + https://huobiapi.github.io/docs/dm/v1/en/#get-information-of-an-order + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#get-information-of-an-order + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + marketType = None + marketType, params = self.handle_market_type_and_params('fetchOrder', market, params) + request: dict = { + # spot ----------------------------------------------------------- + # 'order-id': 'id', + # 'symbol': market['id'], + # 'client-order-id': clientOrderId, + # 'clientOrderId': clientOrderId, + # contracts ------------------------------------------------------ + # 'order_id': id, + # 'client_order_id': clientOrderId, + # 'contract_code': market['id'], + # 'pair': 'BTC-USDT', + # 'contract_type': 'this_week', # swap, self_week, next_week, quarter, next_ quarter + } + response = None + if marketType == 'spot': + clientOrderId = self.safe_string(params, 'clientOrderId') + if clientOrderId is not None: + # will be filled below in self.extend() + # they expect clientOrderId instead of client-order-id + # request['clientOrderId'] = clientOrderId + response = await self.spotPrivateGetV1OrderOrdersGetClientOrder(self.extend(request, params)) + else: + request['order-id'] = id + response = await self.spotPrivateGetV1OrderOrdersOrderId(self.extend(request, params)) + else: + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrder() requires a symbol argument') + clientOrderId = self.safe_string_2(params, 'client_order_id', 'clientOrderId') + if clientOrderId is None: + request['order_id'] = id + else: + request['client_order_id'] = clientOrderId + params = self.omit(params, ['client_order_id', 'clientOrderId']) + request['contract_code'] = market['id'] + if market['linear']: + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchOrder', params) + marginMode = 'cross' if (marginMode is None) else marginMode + if marginMode == 'isolated': + response = await self.contractPrivatePostLinearSwapApiV1SwapOrderInfo(self.extend(request, params)) + elif marginMode == 'cross': + response = await self.contractPrivatePostLinearSwapApiV1SwapCrossOrderInfo(self.extend(request, params)) + elif market['inverse']: + if marketType == 'future': + request['symbol'] = market['settleId'] + response = await self.contractPrivatePostApiV1ContractOrderInfo(self.extend(request, params)) + elif marketType == 'swap': + response = await self.contractPrivatePostSwapApiV1SwapOrderInfo(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchOrder() does not support ' + marketType + ' markets') + # + # spot + # + # { + # "status":"ok", + # "data":{ + # "id":438398393065481, + # "symbol":"ethusdt", + # "account-id":1528640, + # "client-order-id":"AA03022abc2163433e-006b-480e-9ad1-d4781478c5e7", + # "amount":"0.100000000000000000", + # "price":"3000.000000000000000000", + # "created-at":1640549994642, + # "type":"buy-limit", + # "field-amount":"0.0", + # "field-cash-amount":"0.0", + # "field-fees":"0.0", + # "finished-at":0, + # "source":"spot-api", + # "state":"submitted", + # "canceled-at":0 + # } + # } + # + # linear swap cross margin + # + # { + # "status":"ok", + # "data":[ + # { + # "business_type":"swap", + # "contract_type":"swap", + # "pair":"BTC-USDT", + # "symbol":"BTC", + # "contract_code":"BTC-USDT", + # "volume":1, + # "price":3000, + # "order_price_type":"limit", + # "order_type":1, + # "direction":"buy", + # "offset":"open", + # "lever_rate":1, + # "order_id":924912513206878210, + # "client_order_id":null, + # "created_at":1640557927189, + # "trade_volume":0, + # "trade_turnover":0, + # "fee":0, + # "trade_avg_price":null, + # "margin_frozen":3.000000000000000000, + # "profit":0, + # "status":3, + # "order_source":"api", + # "order_id_str":"924912513206878210", + # "fee_asset":"USDT", + # "liquidation_type":"0", + # "canceled_at":0, + # "margin_asset":"USDT", + # "margin_account":"USDT", + # "margin_mode":"cross", + # "is_tpsl":0, + # "real_profit":0 + # } + # ], + # "ts":1640557982556 + # } + # + # linear swap isolated margin detail + # + # { + # "status": "ok", + # "data": { + # "symbol": "BTC", + # "contract_code": "BTC-USDT", + # "instrument_price": 0, + # "final_interest": 0, + # "adjust_value": 0, + # "lever_rate": 10, + # "direction": "sell", + # "offset": "open", + # "volume": 1.000000000000000000, + # "price": 13059.800000000000000000, + # "created_at": 1603703614712, + # "canceled_at": 0, + # "order_source": "api", + # "order_price_type": "opponent", + # "margin_frozen": 0, + # "profit": 0, + # "trades": [ + # { + # "trade_id": 131560927, + # "trade_price": 13059.800000000000000000, + # "trade_volume": 1.000000000000000000, + # "trade_turnover": 13.059800000000000000, + # "trade_fee": -0.005223920000000000, + # "created_at": 1603703614715, + # "role": "taker", + # "fee_asset": "USDT", + # "profit": 0, + # "real_profit": 0, + # "id": "131560927-770334322963152896-1" + # } + # ], + # "total_page": 1, + # "current_page": 1, + # "total_size": 1, + # "liquidation_type": "0", + # "fee_asset": "USDT", + # "fee": -0.005223920000000000, + # "order_id": 770334322963152896, + # "order_id_str": "770334322963152896", + # "client_order_id": 57012021045, + # "order_type": "1", + # "status": 6, + # "trade_avg_price": 13059.800000000000000000, + # "trade_turnover": 13.059800000000000000, + # "trade_volume": 1.000000000000000000, + # "margin_asset": "USDT", + # "margin_mode": "isolated", + # "margin_account": "BTC-USDT", + # "real_profit": 0, + # "is_tpsl": 0 + # }, + # "ts": 1603703678477 + # } + order = self.safe_value(response, 'data') + if isinstance(order, list): + order = self.safe_value(order, 0) + return self.parse_order(order) + + def parse_margin_balance_helper(self, balance, code, result): + account = None + if code in result: + account = result[code] + else: + account = self.account() + if balance['type'] == 'trade': + account['free'] = self.safe_string(balance, 'balance') + if balance['type'] == 'frozen': + account['used'] = self.safe_string(balance, 'balance') + return account + + async def fetch_spot_orders_by_states(self, states, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + method = self.safe_string(self.options, 'fetchOrdersByStatesMethod', 'spot_private_get_v1_order_orders') # spot_private_get_v1_order_history + if method == 'spot_private_get_v1_order_orders': + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrders() requires a symbol argument') + await self.load_markets() + market = None + request: dict = { + # spot_private_get_v1_order_orders GET /v1/order/orders ---------- + # 'symbol': market['id'], # required + # 'types': 'buy-market,sell-market,buy-limit,sell-limit,buy-ioc,sell-ioc,buy-stop-limit,sell-stop-limit,buy-limit-fok,sell-limit-fok,buy-stop-limit-fok,sell-stop-limit-fok', + # 'start-time': since, # max window of 48h within a range of 180 days, within past 2 hours for cancelled orders + # 'end-time': self.milliseconds(), + 'states': states, # filled, partial-canceled, canceled + # 'from': order['id'], + # 'direct': 'next', # next, prev, used with from + # 'size': 100, # max 100 + # spot_private_get_v1_order_history GET /v1/order/history -------- + # 'symbol': market['id'], # optional + # 'start-time': since, # max window of 48h within a range of 180 days, within past 2 hours for cancelled orders + # 'end-time': self.milliseconds(), + # 'direct': 'next', # next, prev, used with from + # 'size': 100, # max 100 + } + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if since is not None: + request['start-time'] = since # a window of 48 hours within 180 days + request['end-time'] = self.sum(since, 48 * 60 * 60 * 1000) + request, params = self.handle_until_option('end-time', request, params) + if limit is not None: + request['size'] = limit + response = None + if method == 'spot_private_get_v1_order_orders': + response = await self.spotPrivateGetV1OrderOrders(self.extend(request, params)) + else: + response = await self.spotPrivateGetV1OrderHistory(self.extend(request, params)) + # + # spot_private_get_v1_order_orders GET /v1/order/orders + # + # { + # "status": "ok", + # "data": [ + # { + # "id": 13997833014, + # "symbol": "ethbtc", + # "account-id": 3398321, + # "client-order-id": "23456", + # "amount": "0.045000000000000000", + # "price": "0.034014000000000000", + # "created-at": 1545836976871, + # "type": "sell-limit", + # "field-amount": "0.045000000000000000", + # "field-cash-amount": "0.001530630000000000", + # "field-fees": "0.000003061260000000", + # "finished-at": 1545837948214, + # "source": "spot-api", + # "state": "filled", + # "canceled-at": 0 + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_orders(data, market, since, limit) + + async def fetch_spot_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + return await self.fetch_spot_orders_by_states('pre-submitted,submitted,partial-filled,filled,partial-canceled,canceled', symbol, since, limit, params) + + async def fetch_closed_spot_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + return await self.fetch_spot_orders_by_states('filled,partial-canceled,canceled', symbol, since, limit, params) + + async def fetch_contract_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchContractOrders() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + # POST /api/v1/contract_hisorders inverse futures ---------------- + # 'symbol': market['settleId'], # BTC, ETH, ... + # 'order_type': '1', # 1 limit,3 opponent,4 lightning, 5 trigger order, 6 pst_only, 7 optimal_5, 8 optimal_10, 9 optimal_20, 10 fok, 11 ioc + # POST /swap-api/v3/swap_hisorders inverse swap ------------------ + # POST /linear-swap-api/v3/swap_hisorders linear isolated -------- + # POST /linear-swap-api/v3/swap_cross_hisorders linear cross ----- + 'trade_type': 0, # 0:All; 1: Open long; 2: Open short; 3: Close short; 4: Close long; 5: Liquidate long positions; 6: Liquidate short positions, 17:buy(one-way mode), 18:sell(one-way mode) + 'status': '0', # support multiple query seperated by ',',such as '3,4,5', 0: all. 3. Have sumbmitted the orders; 4. Orders partially matched; 5. Orders cancelled with partially matched; 6. Orders fully matched; 7. Orders cancelled + } + response = None + trigger = self.safe_bool_2(params, 'stop', 'trigger') + stopLossTakeProfit = self.safe_value(params, 'stopLossTakeProfit') + trailing = self.safe_bool(params, 'trailing', False) + params = self.omit(params, ['stop', 'stopLossTakeProfit', 'trailing', 'trigger']) + if trigger or stopLossTakeProfit or trailing: + if limit is not None: + request['page_size'] = limit + request['contract_code'] = market['id'] + request['create_date'] = 90 + else: + if since is not None: + request['start_time'] = since # max 90 days back + # request['end_time'] = since + 172800000 # 48 hours window + request['contract'] = market['id'] + request['type'] = 1 # 1:All Orders,2:Order in Finished Status + request, params = self.handle_until_option('end_time', request, params) + if market['linear']: + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchContractOrders', params) + marginMode = 'cross' if (marginMode is None) else marginMode + if marginMode == 'isolated': + if trigger: + response = await self.contractPrivatePostLinearSwapApiV1SwapTriggerHisorders(self.extend(request, params)) + elif stopLossTakeProfit: + response = await self.contractPrivatePostLinearSwapApiV1SwapTpslHisorders(self.extend(request, params)) + elif trailing: + response = await self.contractPrivatePostLinearSwapApiV1SwapTrackHisorders(self.extend(request, params)) + else: + response = await self.contractPrivatePostLinearSwapApiV3SwapHisorders(self.extend(request, params)) + elif marginMode == 'cross': + if trigger: + response = await self.contractPrivatePostLinearSwapApiV1SwapCrossTriggerHisorders(self.extend(request, params)) + elif stopLossTakeProfit: + response = await self.contractPrivatePostLinearSwapApiV1SwapCrossTpslHisorders(self.extend(request, params)) + elif trailing: + response = await self.contractPrivatePostLinearSwapApiV1SwapCrossTrackHisorders(self.extend(request, params)) + else: + response = await self.contractPrivatePostLinearSwapApiV3SwapCrossHisorders(self.extend(request, params)) + elif market['inverse']: + if market['swap']: + if trigger: + response = await self.contractPrivatePostSwapApiV1SwapTriggerHisorders(self.extend(request, params)) + elif stopLossTakeProfit: + response = await self.contractPrivatePostSwapApiV1SwapTpslHisorders(self.extend(request, params)) + elif trailing: + response = await self.contractPrivatePostSwapApiV1SwapTrackHisorders(self.extend(request, params)) + else: + response = await self.contractPrivatePostSwapApiV3SwapHisorders(self.extend(request, params)) + elif market['future']: + request['symbol'] = market['settleId'] + if trigger: + response = await self.contractPrivatePostApiV1ContractTriggerHisorders(self.extend(request, params)) + elif stopLossTakeProfit: + response = await self.contractPrivatePostApiV1ContractTpslHisorders(self.extend(request, params)) + elif trailing: + response = await self.contractPrivatePostApiV1ContractTrackHisorders(self.extend(request, params)) + else: + response = await self.contractPrivatePostApiV3ContractHisorders(self.extend(request, params)) + # + # future and swap + # + # { + # "code": 200, + # "msg": "ok", + # "data": [ + # { + # "direction": "buy", + # "offset": "open", + # "volume": 1.000000000000000000, + # "price": 25000.000000000000000000, + # "profit": 0E-18, + # "pair": "BTC-USDT", + # "query_id": 47403349100, + # "order_id": 1103683465337593856, + # "contract_code": "BTC-USDT-230505", + # "symbol": "BTC", + # "lever_rate": 5, + # "create_date": 1683180243577, + # "order_source": "web", + # "canceled_source": "web", + # "order_price_type": 1, + # "order_type": 1, + # "margin_frozen": 0E-18, + # "trade_volume": 0E-18, + # "trade_turnover": 0E-18, + # "fee": 0E-18, + # "trade_avg_price": 0, + # "status": 7, + # "order_id_str": "1103683465337593856", + # "fee_asset": "USDT", + # "fee_amount": 0, + # "fee_quote_amount": 0, + # "liquidation_type": "0", + # "margin_asset": "USDT", + # "margin_mode": "cross", + # "margin_account": "USDT", + # "update_time": 1683180352034, + # "is_tpsl": 0, + # "real_profit": 0, + # "trade_partition": "USDT", + # "reduce_only": 0, + # "contract_type": "self_week", + # "business_type": "futures" + # } + # ], + # "ts": 1683239909141 + # } + # + # trigger + # + # { + # "status": "ok", + # "data": { + # "orders": [ + # { + # "contract_type": "swap", + # "business_type": "swap", + # "pair": "BTC-USDT", + # "symbol": "BTC", + # "contract_code": "BTC-USDT", + # "trigger_type": "le", + # "volume": 1.000000000000000000, + # "order_type": 1, + # "direction": "buy", + # "offset": "open", + # "lever_rate": 1, + # "order_id": 1103670703588327424, + # "order_id_str": "1103670703588327424", + # "relation_order_id": "-1", + # "order_price_type": "limit", + # "status": 6, + # "order_source": "web", + # "trigger_price": 25000.000000000000000000, + # "triggered_price": null, + # "order_price": 24000.000000000000000000, + # "created_at": 1683177200945, + # "triggered_at": null, + # "order_insert_at": 0, + # "canceled_at": 1683179075234, + # "fail_code": null, + # "fail_reason": null, + # "margin_mode": "cross", + # "margin_account": "USDT", + # "update_time": 1683179075958, + # "trade_partition": "USDT", + # "reduce_only": 0 + # }, + # ], + # "total_page": 1, + # "current_page": 1, + # "total_size": 2 + # }, + # "ts": 1683239702792 + # } + # + # stop-loss and take-profit + # + # { + # "status": "ok", + # "data": { + # "orders": [ + # { + # "contract_type": "swap", + # "business_type": "swap", + # "pair": "BTC-USDT", + # "symbol": "BTC", + # "contract_code": "BTC-USDT", + # "margin_mode": "cross", + # "margin_account": "USDT", + # "volume": 1.000000000000000000, + # "order_type": 1, + # "tpsl_order_type": "sl", + # "direction": "sell", + # "order_id": 1103680386844839936, + # "order_id_str": "1103680386844839936", + # "order_source": "web", + # "trigger_type": "le", + # "trigger_price": 25000.000000000000000000, + # "created_at": 1683179509613, + # "order_price_type": "market", + # "status": 11, + # "source_order_id": null, + # "relation_tpsl_order_id": "-1", + # "canceled_at": 0, + # "fail_code": null, + # "fail_reason": null, + # "triggered_price": null, + # "relation_order_id": "-1", + # "update_time": 1683179968231, + # "order_price": 0E-18, + # "trade_partition": "USDT" + # }, + # ], + # "total_page": 1, + # "current_page": 1, + # "total_size": 2 + # }, + # "ts": 1683229230233 + # } + # + orders = self.safe_value(response, 'data') + if not isinstance(orders, list): + orders = self.safe_value(orders, 'orders', []) + return self.parse_orders(orders, market, since, limit) + + async def fetch_closed_contract_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + request: dict = { + 'status': '5,6,7', # comma separated, 0 all, 3 submitted orders, 4 partially matched, 5 partially cancelled, 6 fully matched and closed, 7 canceled + } + return await self.fetch_contract_orders(symbol, since, limit, self.extend(request, params)) + + async def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + + https://huobiapi.github.io/docs/spot/v1/en/#search-past-orders + https://huobiapi.github.io/docs/spot/v1/en/#search-historical-orders-within-48-hours + https://huobiapi.github.io/docs/usdt_swap/v1/en/#isolated-get-history-orders-new + https://huobiapi.github.io/docs/usdt_swap/v1/en/#cross-get-history-orders-new + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#get-history-orders-new + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#query-history-orders-via-multiple-fields-new + + fetches information on multiple orders made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.trigger]: *contract only* if the orders are trigger trigger orders or not + :param bool [params.stopLossTakeProfit]: *contract only* if the orders are stop-loss or take-profit orders + :param int [params.until]: the latest time in ms to fetch entries for + :param boolean [params.trailing]: *contract only* set to True if you want to fetch trailing stop orders + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + marketType = None + marketType, params = self.handle_market_type_and_params('fetchOrders', market, params) + contract = (marketType == 'swap') or (marketType == 'future') + if contract and (symbol is None): + raise ArgumentsRequired(self.id + ' fetchOrders() requires a symbol argument for ' + marketType + ' orders') + if contract: + return await self.fetch_contract_orders(symbol, since, limit, params) + else: + return await self.fetch_spot_orders(symbol, since, limit, params) + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + + https://huobiapi.github.io/docs/spot/v1/en/#search-past-orders + https://huobiapi.github.io/docs/spot/v1/en/#search-historical-orders-within-48-hours + https://huobiapi.github.io/docs/usdt_swap/v1/en/#isolated-get-history-orders-new + https://huobiapi.github.io/docs/usdt_swap/v1/en/#cross-get-history-orders-new + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#get-history-orders-new + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#query-history-orders-via-multiple-fields-new + + fetches information on multiple closed orders made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch entries for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchClosedOrders', 'paginate') + if paginate: + return await self.fetch_paginated_call_dynamic('fetchClosedOrders', symbol, since, limit, params, 100) + market = None + if symbol is not None: + market = self.market(symbol) + marketType = None + marketType, params = self.handle_market_type_and_params('fetchClosedOrders', market, params) + if marketType == 'spot': + return await self.fetch_closed_spot_orders(symbol, since, limit, params) + else: + return await self.fetch_closed_contract_orders(symbol, since, limit, params) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + + https://huobiapi.github.io/docs/spot/v1/en/#get-all-open-orders + https://huobiapi.github.io/docs/usdt_swap/v1/en/#isolated-current-unfilled-order-acquisition + https://huobiapi.github.io/docs/usdt_swap/v1/en/#cross-current-unfilled-order-acquisition + + fetch all unfilled currently open orders + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.trigger]: *contract only* if the orders are trigger trigger orders or not + :param bool [params.stopLossTakeProfit]: *contract only* if the orders are stop-loss or take-profit orders + :param boolean [params.trailing]: *contract only* set to True if you want to fetch trailing stop orders + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + request: dict = {} + marketType = None + marketType, params = self.handle_market_type_and_params('fetchOpenOrders', market, params) + subType = None + subType, params = self.handle_sub_type_and_params('fetchOpenOrders', market, params, 'linear') + response = None + if marketType == 'spot': + if symbol is not None: + request['symbol'] = market['id'] + # todo replace with fetchAccountIdByType + accountId = self.safe_string(params, 'account-id') + if accountId is None: + # pick the first account + await self.load_accounts() + for i in range(0, len(self.accounts)): + account = self.accounts[i] + if self.safe_string(account, 'type') == 'spot': + accountId = self.safe_string(account, 'id') + if accountId is not None: + break + request['account-id'] = accountId + if limit is not None: + request['size'] = limit + params = self.omit(params, 'account-id') + response = await self.spotPrivateGetV1OrderOpenOrders(self.extend(request, params)) + else: + if symbol is not None: + # raise ArgumentsRequired(self.id + ' fetchOpenOrders() requires a symbol argument') + request['contract_code'] = market['id'] + if limit is not None: + request['page_size'] = limit + trigger = self.safe_bool_2(params, 'stop', 'trigger') + stopLossTakeProfit = self.safe_value(params, 'stopLossTakeProfit') + trailing = self.safe_bool(params, 'trailing', False) + params = self.omit(params, ['stop', 'stopLossTakeProfit', 'trailing', 'trigger']) + if subType == 'linear': + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchOpenOrders', params) + marginMode = 'cross' if (marginMode is None) else marginMode + if marginMode == 'isolated': + if trigger: + response = await self.contractPrivatePostLinearSwapApiV1SwapTriggerOpenorders(self.extend(request, params)) + elif stopLossTakeProfit: + response = await self.contractPrivatePostLinearSwapApiV1SwapTpslOpenorders(self.extend(request, params)) + elif trailing: + response = await self.contractPrivatePostLinearSwapApiV1SwapTrackOpenorders(self.extend(request, params)) + else: + response = await self.contractPrivatePostLinearSwapApiV1SwapOpenorders(self.extend(request, params)) + elif marginMode == 'cross': + if trigger: + response = await self.contractPrivatePostLinearSwapApiV1SwapCrossTriggerOpenorders(self.extend(request, params)) + elif stopLossTakeProfit: + response = await self.contractPrivatePostLinearSwapApiV1SwapCrossTpslOpenorders(self.extend(request, params)) + elif trailing: + response = await self.contractPrivatePostLinearSwapApiV1SwapCrossTrackOpenorders(self.extend(request, params)) + else: + response = await self.contractPrivatePostLinearSwapApiV1SwapCrossOpenorders(self.extend(request, params)) + elif subType == 'inverse': + if marketType == 'swap': + if trigger: + response = await self.contractPrivatePostSwapApiV1SwapTriggerOpenorders(self.extend(request, params)) + elif stopLossTakeProfit: + response = await self.contractPrivatePostSwapApiV1SwapTpslOpenorders(self.extend(request, params)) + elif trailing: + response = await self.contractPrivatePostSwapApiV1SwapTrackOpenorders(self.extend(request, params)) + else: + response = await self.contractPrivatePostSwapApiV1SwapOpenorders(self.extend(request, params)) + elif marketType == 'future': + request['symbol'] = self.safe_string(market, 'settleId', 'usdt') + if trigger: + response = await self.contractPrivatePostApiV1ContractTriggerOpenorders(self.extend(request, params)) + elif stopLossTakeProfit: + response = await self.contractPrivatePostApiV1ContractTpslOpenorders(self.extend(request, params)) + elif trailing: + response = await self.contractPrivatePostApiV1ContractTrackOpenorders(self.extend(request, params)) + else: + response = await self.contractPrivatePostApiV1ContractOpenorders(self.extend(request, params)) + # + # spot + # + # { + # "status":"ok", + # "data":[ + # { + # "symbol":"ethusdt", + # "source":"api", + # "amount":"0.010000000000000000", + # "account-id":1528640, + # "created-at":1561597491963, + # "price":"400.000000000000000000", + # "filled-amount":"0.0", + # "filled-cash-amount":"0.0", + # "filled-fees":"0.0", + # "id":38477101630, + # "state":"submitted", + # "type":"sell-limit" + # } + # ] + # } + # + # futures + # + # { + # "status": "ok", + # "data": { + # "orders": [ + # { + # "symbol": "ADA", + # "contract_code": "ADA201225", + # "contract_type": "quarter", + # "volume": 1, + # "price": 0.0925, + # "order_price_type": "post_only", + # "order_type": 1, + # "direction": "buy", + # "offset": "close", + # "lever_rate": 20, + # "order_id": 773131315209248768, + # "client_order_id": null, + # "created_at": 1604370469629, + # "trade_volume": 0, + # "trade_turnover": 0, + # "fee": 0, + # "trade_avg_price": null, + # "margin_frozen": 0, + # "profit": 0, + # "status": 3, + # "order_source": "web", + # "order_id_str": "773131315209248768", + # "fee_asset": "ADA", + # "liquidation_type": null, + # "canceled_at": null, + # "is_tpsl": 0, + # "update_time": 1606975980467, + # "real_profit": 0 + # } + # ], + # "total_page": 1, + # "current_page": 1, + # "total_size": 1 + # }, + # "ts": 1604370488518 + # } + # + # trigger + # + # { + # "status": "ok", + # "data": { + # "orders": [ + # { + # "contract_type": "swap", + # "business_type": "swap", + # "pair": "BTC-USDT", + # "symbol": "BTC", + # "contract_code": "BTC-USDT", + # "trigger_type": "le", + # "volume": 1.000000000000000000, + # "order_type": 1, + # "direction": "buy", + # "offset": "open", + # "lever_rate": 1, + # "order_id": 1103670703588327424, + # "order_id_str": "1103670703588327424", + # "order_source": "web", + # "trigger_price": 25000.000000000000000000, + # "order_price": 24000.000000000000000000, + # "created_at": 1683177200945, + # "order_price_type": "limit", + # "status": 2, + # "margin_mode": "cross", + # "margin_account": "USDT", + # "trade_partition": "USDT", + # "reduce_only": 0 + # } + # ], + # "total_page": 1, + # "current_page": 1, + # "total_size": 1 + # }, + # "ts": 1683177805320 + # } + # + # stop-loss and take-profit + # + # { + # "status": "ok", + # "data": { + # "orders": [ + # { + # "contract_type": "swap", + # "business_type": "swap", + # "pair": "BTC-USDT", + # "symbol": "BTC", + # "contract_code": "BTC-USDT", + # "margin_mode": "cross", + # "margin_account": "USDT", + # "volume": 1.000000000000000000, + # "order_type": 1, + # "direction": "sell", + # "order_id": 1103680386844839936, + # "order_id_str": "1103680386844839936", + # "order_source": "web", + # "trigger_type": "le", + # "trigger_price": 25000.000000000000000000, + # "order_price": 0E-18, + # "created_at": 1683179509613, + # "order_price_type": "market", + # "status": 2, + # "tpsl_order_type": "sl", + # "source_order_id": null, + # "relation_tpsl_order_id": "-1", + # "trade_partition": "USDT" + # } + # ], + # "total_page": 1, + # "current_page": 1, + # "total_size": 1 + # }, + # "ts": 1683179527011 + # } + # + # trailing + # + # { + # "status": "ok", + # "data": { + # "orders": [ + # { + # "contract_type": "swap", + # "business_type": "swap", + # "pair": "BTC-USDT", + # "symbol": "BTC", + # "contract_code": "BTC-USDT", + # "volume": 1.000000000000000000, + # "order_type": 1, + # "direction": "sell", + # "offset": "close", + # "lever_rate": 1, + # "order_id": 1192021437253877761, + # "order_id_str": "1192021437253877761", + # "order_source": "api", + # "created_at": 1704241657328, + # "order_price_type": "formula_price", + # "status": 2, + # "callback_rate": 0.050000000000000000, + # "active_price": 50000.000000000000000000, + # "is_active": 0, + # "margin_mode": "cross", + # "margin_account": "USDT", + # "trade_partition": "USDT", + # "reduce_only": 1 + # }, + # ], + # "total_page": 1, + # "current_page": 1, + # "total_size": 2 + # }, + # "ts": 1704242440106 + # } + # + orders = self.safe_value(response, 'data') + if not isinstance(orders, list): + orders = self.safe_value(orders, 'orders', []) + return self.parse_orders(orders, market, since, limit) + + def parse_order_status(self, status: Str): + statuses: dict = { + # spot + 'partial-filled': 'open', + 'partial-canceled': 'canceled', + 'filled': 'closed', + 'canceled': 'canceled', + 'submitted': 'open', + 'created': 'open', # For stop orders + # contract + '1': 'open', + '2': 'open', + '3': 'open', + '4': 'open', + '5': 'canceled', # partially matched + '6': 'closed', + '7': 'canceled', + '11': 'canceling', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # spot + # + # { + # "id": 13997833014, + # "symbol": "ethbtc", + # "account-id": 3398321, + # "amount": "0.045000000000000000", + # "price": "0.034014000000000000", + # "created-at": 1545836976871, + # "type": "sell-limit", + # "field-amount": "0.045000000000000000", # they have fixed it for filled-amount + # "field-cash-amount": "0.001530630000000000", # they have fixed it for filled-cash-amount + # "field-fees": "0.000003061260000000", # they have fixed it for filled-fees + # "finished-at": 1545837948214, + # "source": "spot-api", + # "state": "filled", + # "canceled-at": 0 + # } + # + # { + # "id": 20395337822, + # "symbol": "ethbtc", + # "account-id": 5685075, + # "amount": "0.001000000000000000", + # "price": "0.0", + # "created-at": 1545831584023, + # "type": "buy-market", + # "field-amount": "0.029100000000000000", # they have fixed it for filled-amount + # "field-cash-amount": "0.000999788700000000", # they have fixed it for filled-cash-amount + # "field-fees": "0.000058200000000000", # they have fixed it for filled-fees + # "finished-at": 1545831584181, + # "source": "spot-api", + # "state": "filled", + # "canceled-at": 0 + # } + # + # linear swap cross margin createOrder + # + # { + # "order_id":924660854912552960, + # "order_id_str":"924660854912552960" + # } + # + # contracts fetchOrder + # + # { + # "business_type":"swap", + # "contract_type":"swap", + # "pair":"BTC-USDT", + # "symbol":"BTC", + # "contract_code":"BTC-USDT", + # "volume":1, + # "price":3000, + # "order_price_type":"limit", + # "order_type":1, + # "direction":"buy", + # "offset":"open", + # "lever_rate":1, + # "order_id":924912513206878210, + # "client_order_id":null, + # "created_at":1640557927189, + # "trade_volume":0, + # "trade_turnover":0, + # "fee":0, + # "trade_avg_price":null, + # "margin_frozen":3.000000000000000000, + # "profit":0, + # "status":3, + # "order_source":"api", + # "order_id_str":"924912513206878210", + # "fee_asset":"USDT", + # "liquidation_type":"0", + # "canceled_at":0, + # "margin_asset":"USDT", + # "margin_account":"USDT", + # "margin_mode":"cross", + # "is_tpsl":0, + # "real_profit":0 + # } + # + # contracts fetchOrder detailed + # + # { + # "symbol": "BTC", + # "contract_code": "BTC-USDT", + # "instrument_price": 0, + # "final_interest": 0, + # "adjust_value": 0, + # "lever_rate": 10, + # "direction": "sell", + # "offset": "open", + # "volume": 1.000000000000000000, + # "price": 13059.800000000000000000, + # "created_at": 1603703614712, + # "canceled_at": 0, + # "order_source": "api", + # "order_price_type": "opponent", + # "margin_frozen": 0, + # "profit": 0, + # "trades": [ + # { + # "trade_id": 131560927, + # "trade_price": 13059.800000000000000000, + # "trade_volume": 1.000000000000000000, + # "trade_turnover": 13.059800000000000000, + # "trade_fee": -0.005223920000000000, + # "created_at": 1603703614715, + # "role": "taker", + # "fee_asset": "USDT", + # "profit": 0, + # "real_profit": 0, + # "id": "131560927-770334322963152896-1" + # } + # ], + # "total_page": 1, + # "current_page": 1, + # "total_size": 1, + # "liquidation_type": "0", + # "fee_asset": "USDT", + # "fee": -0.005223920000000000, + # "order_id": 770334322963152896, + # "order_id_str": "770334322963152896", + # "client_order_id": 57012021045, + # "order_type": "1", + # "status": 6, + # "trade_avg_price": 13059.800000000000000000, + # "trade_turnover": 13.059800000000000000, + # "trade_volume": 1.000000000000000000, + # "margin_asset": "USDT", + # "margin_mode": "isolated", + # "margin_account": "BTC-USDT", + # "real_profit": 0, + # "is_tpsl": 0 + # } + # + # future and swap: fetchOrders + # + # { + # "order_id": 773131315209248768, + # "contract_code": "ADA201225", + # "symbol": "ADA", + # "lever_rate": 20, + # "direction": "buy", + # "offset": "close", + # "volume": 1, + # "price": 0.0925, + # "create_date": 1604370469629, + # "update_time": 1603704221118, + # "order_source": "web", + # "order_price_type": 6, + # "order_type": 1, + # "margin_frozen": 0, + # "profit": 0, + # "contract_type": "quarter", + # "trade_volume": 0, + # "trade_turnover": 0, + # "fee": 0, + # "trade_avg_price": 0, + # "status": 3, + # "order_id_str": "773131315209248768", + # "fee_asset": "ADA", + # "liquidation_type": "0", + # "is_tpsl": 0, + # "real_profit": 0 + # "margin_asset": "USDT", + # "margin_mode": "cross", + # "margin_account": "USDT", + # "trade_partition": "USDT", # only in isolated & cross of linear + # "reduce_only": "1", # only in isolated & cross of linear + # "contract_type": "quarter", # only in cross-margin(inverse & linear) + # "pair": "BTC-USDT", # only in cross-margin(inverse & linear) + # "business_type": "futures" # only in cross-margin(inverse & linear) + # } + # + # trigger: fetchOpenOrders + # + # { + # "contract_type": "swap", + # "business_type": "swap", + # "pair": "BTC-USDT", + # "symbol": "BTC", + # "contract_code": "BTC-USDT", + # "trigger_type": "le", + # "volume": 1.000000000000000000, + # "order_type": 1, + # "direction": "buy", + # "offset": "open", + # "lever_rate": 1, + # "order_id": 1103670703588327424, + # "order_id_str": "1103670703588327424", + # "order_source": "web", + # "trigger_price": 25000.000000000000000000, + # "order_price": 24000.000000000000000000, + # "created_at": 1683177200945, + # "order_price_type": "limit", + # "status": 2, + # "margin_mode": "cross", + # "margin_account": "USDT", + # "trade_partition": "USDT", + # "reduce_only": 0 + # } + # + # stop-loss and take-profit: fetchOpenOrders + # + # { + # "contract_type": "swap", + # "business_type": "swap", + # "pair": "BTC-USDT", + # "symbol": "BTC", + # "contract_code": "BTC-USDT", + # "margin_mode": "cross", + # "margin_account": "USDT", + # "volume": 1.000000000000000000, + # "order_type": 1, + # "direction": "sell", + # "order_id": 1103680386844839936, + # "order_id_str": "1103680386844839936", + # "order_source": "web", + # "trigger_type": "le", + # "trigger_price": 25000.000000000000000000, + # "order_price": 0E-18, + # "created_at": 1683179509613, + # "order_price_type": "market", + # "status": 2, + # "tpsl_order_type": "sl", + # "source_order_id": null, + # "relation_tpsl_order_id": "-1", + # "trade_partition": "USDT" + # } + # + # trailing: fetchOpenOrders + # + # { + # "contract_type": "swap", + # "business_type": "swap", + # "pair": "BTC-USDT", + # "symbol": "BTC", + # "contract_code": "BTC-USDT", + # "volume": 1.000000000000000000, + # "order_type": 1, + # "direction": "sell", + # "offset": "close", + # "lever_rate": 1, + # "order_id": 1192021437253877761, + # "order_id_str": "1192021437253877761", + # "order_source": "api", + # "created_at": 1704241657328, + # "order_price_type": "formula_price", + # "status": 2, + # "callback_rate": 0.050000000000000000, + # "active_price": 50000.000000000000000000, + # "is_active": 0, + # "margin_mode": "cross", + # "margin_account": "USDT", + # "trade_partition": "USDT", + # "reduce_only": 1 + # } + # + # trigger: fetchOrders + # + # { + # "contract_type": "swap", + # "business_type": "swap", + # "pair": "BTC-USDT", + # "symbol": "BTC", + # "contract_code": "BTC-USDT", + # "trigger_type": "le", + # "volume": 1.000000000000000000, + # "order_type": 1, + # "direction": "buy", + # "offset": "open", + # "lever_rate": 1, + # "order_id": 1103670703588327424, + # "order_id_str": "1103670703588327424", + # "relation_order_id": "-1", + # "order_price_type": "limit", + # "status": 6, + # "order_source": "web", + # "trigger_price": 25000.000000000000000000, + # "triggered_price": null, + # "order_price": 24000.000000000000000000, + # "created_at": 1683177200945, + # "triggered_at": null, + # "order_insert_at": 0, + # "canceled_at": 1683179075234, + # "fail_code": null, + # "fail_reason": null, + # "margin_mode": "cross", + # "margin_account": "USDT", + # "update_time": 1683179075958, + # "trade_partition": "USDT", + # "reduce_only": 0 + # } + # + # stop-loss and take-profit: fetchOrders + # + # { + # "contract_type": "swap", + # "business_type": "swap", + # "pair": "BTC-USDT", + # "symbol": "BTC", + # "contract_code": "BTC-USDT", + # "margin_mode": "cross", + # "margin_account": "USDT", + # "volume": 1.000000000000000000, + # "order_type": 1, + # "tpsl_order_type": "sl", + # "direction": "sell", + # "order_id": 1103680386844839936, + # "order_id_str": "1103680386844839936", + # "order_source": "web", + # "trigger_type": "le", + # "trigger_price": 25000.000000000000000000, + # "created_at": 1683179509613, + # "order_price_type": "market", + # "status": 11, + # "source_order_id": null, + # "relation_tpsl_order_id": "-1", + # "canceled_at": 0, + # "fail_code": null, + # "fail_reason": null, + # "triggered_price": null, + # "relation_order_id": "-1", + # "update_time": 1683179968231, + # "order_price": 0E-18, + # "trade_partition": "USDT" + # } + # + # spot: createOrders + # + # [ + # { + # "order-id": 936847569789079, + # "client-order-id": "AA03022abc3a55e82c-0087-4fc2-beac-112fdebb1ee9" + # }, + # { + # "client-order-id": "AA03022abcdb3baefb-3cfa-4891-8009-082b3d46ca82", + # "err-code": "account-frozen-balance-insufficient-error", + # "err-msg": "trade account balance is not enough, left: `89`" + # } + # ] + # + # swap and future: createOrders + # + # [ + # { + # "index": 2, + # "err_code": 1047, + # "err_msg": "Insufficient margin available." + # }, + # { + # "order_id": 1172923090632953857, + # "index": 1, + # "order_id_str": "1172923090632953857" + # } + # ] + # + rejectedCreateOrders = self.safe_string_2(order, 'err_code', 'err-code') + status = self.parse_order_status(self.safe_string_2(order, 'state', 'status')) + if rejectedCreateOrders is not None: + status = 'rejected' + id = self.safe_string_n(order, ['id', 'order_id_str', 'order-id']) + side = self.safe_string(order, 'direction') + type = self.safe_string(order, 'order_price_type') + if 'type' in order: + orderType = order['type'].split('-') + side = orderType[0] + type = orderType[1] + marketId = self.safe_string_2(order, 'contract_code', 'symbol') + market = self.safe_market(marketId, market) + timestamp = self.safe_integer_n(order, ['created_at', 'created-at', 'create_date']) + clientOrderId = self.safe_string_2(order, 'client_order_id', 'client-or' + 'der-id') # transpiler regex trick for php issue + cost = None + amount = None + if (type is not None) and (type.find('market') >= 0): + cost = self.safe_string(order, 'field-cash-amount') + else: + amount = self.safe_string_2(order, 'volume', 'amount') + cost = self.safe_string_n(order, ['filled-cash-amount', 'field-cash-amount', 'trade_turnover']) # same typo here + filled = self.safe_string_n(order, ['filled-amount', 'field-amount', 'trade_volume']) # typo in their API, filled amount + price = self.safe_string_2(order, 'price', 'order_price') + feeCost = self.safe_string_2(order, 'filled-fees', 'field-fees') # typo in their API, filled feeSide + feeCost = self.safe_string(order, 'fee', feeCost) + fee = None + if feeCost is not None: + feeCurrency = None + feeCurrencyId = self.safe_string(order, 'fee_asset') + if feeCurrencyId is not None: + feeCurrency = self.safe_currency_code(feeCurrencyId) + else: + feeCurrency = market['quote'] if (side == 'sell') else market['base'] + fee = { + 'cost': feeCost, + 'currency': feeCurrency, + } + average = self.safe_string(order, 'trade_avg_price') + trades = self.safe_value(order, 'trades') + reduceOnlyInteger = self.safe_integer(order, 'reduce_only') + reduceOnly = None + if reduceOnlyInteger is not None: + reduceOnly = False if (reduceOnlyInteger == 0) else True + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'symbol': market['symbol'], + 'type': type, + 'timeInForce': None, + 'postOnly': None, + 'side': side, + 'price': price, + 'triggerPrice': self.safe_string_2(order, 'stop-price', 'trigger_price'), + 'average': average, + 'cost': cost, + 'amount': amount, + 'filled': filled, + 'remaining': None, + 'status': status, + 'reduceOnly': reduceOnly, + 'fee': fee, + 'trades': trades, + }, market) + + async def create_market_buy_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market buy order by providing the symbol and cost + + https://www.htx.com/en-us/opend/newApiPages/?id=7ec4ee16-7773-11ed-9966-0242ac110003 + + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' createMarketBuyOrderWithCost() supports spot orders only') + params['createMarketBuyOrderRequiresPrice'] = False + return await self.create_order(symbol, 'market', 'buy', cost, None, params) + + async def create_trailing_percent_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, trailingPercent: Num = None, trailingTriggerPrice: Num = None, params={}) -> Order: + """ + create a trailing order by providing the symbol, type, side, amount, price and trailingPercent + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency, or number of contracts + :param float [price]: the price for the order to be filled at, in units of the quote currency, ignored in market orders + :param float trailingPercent: the percent to trail away from the current market price + :param float trailingTriggerPrice: the price to activate a trailing order, default uses the price argument + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + if trailingPercent is None: + raise ArgumentsRequired(self.id + ' createTrailingPercentOrder() requires a trailingPercent argument') + if trailingTriggerPrice is None: + raise ArgumentsRequired(self.id + ' createTrailingPercentOrder() requires a trailingTriggerPrice argument') + params['trailingPercent'] = trailingPercent + params['trailingTriggerPrice'] = trailingTriggerPrice + return await self.create_order(symbol, type, side, amount, price, params) + + async def create_spot_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + @ignore + helper function to build request + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.timeInForce]: supports 'IOC' and 'FOK' + :param float [params.cost]: the quote quantity that can be used alternative for the amount for market buy orders + :returns dict: request to be sent to the exchange + """ + await self.load_markets() + await self.load_accounts() + market = self.market(symbol) + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('createOrder', params) + accountId = await self.fetch_account_id_by_type(market['type'], marginMode, symbol) + request: dict = { + # spot ----------------------------------------------------------- + 'account-id': accountId, + 'symbol': market['id'], + # 'type': side + '-' + type, # buy-market, sell-market, buy-limit, sell-limit, buy-ioc, sell-ioc, buy-limit-maker, sell-limit-maker, buy-stop-limit, sell-stop-limit, buy-limit-fok, sell-limit-fok, buy-stop-limit-fok, sell-stop-limit-fok + # 'amount': self.amount_to_precision(symbol, amount), # for buy market orders it's the order cost + # 'price': self.price_to_precision(symbol, price), + # 'source': 'spot-api', # optional, spot-api, margin-api = isolated margin, super-margin-api = cross margin, c2c-margin-api + # 'client-order-id': clientOrderId, # optional, max 64 chars, must be unique within 8 hours + # 'stop-price': self.price_to_precision(symbol, stopPrice), # trigger price for stop limit orders + # 'operator': 'gte', # gte, lte, trigger price condition + } + orderType = type.replace('buy-', '') + orderType = orderType.replace('sell-', '') + options = self.safe_value(self.options, market['type'], {}) + triggerPrice = self.safe_string_n(params, ['triggerPrice', 'stopPrice', 'stop-price']) + if triggerPrice is None: + stopOrderTypes = self.safe_value(options, 'stopOrderTypes', {}) + if orderType in stopOrderTypes: + raise ArgumentsRequired(self.id + ' createOrder() requires a triggerPrice for a trigger order') + else: + defaultOperator = 'lte' if (side == 'sell') else 'gte' + stopOperator = self.safe_string(params, 'operator', defaultOperator) + request['stop-price'] = self.price_to_precision(symbol, triggerPrice) + request['operator'] = stopOperator + if (orderType == 'limit') or (orderType == 'limit-fok'): + orderType = 'stop-' + orderType + elif (orderType != 'stop-limit') and (orderType != 'stop-limit-fok'): + raise NotSupported(self.id + ' createOrder() does not support ' + type + ' orders') + postOnly = None + postOnly, params = self.handle_post_only(orderType == 'market', orderType == 'limit-maker', params) + if postOnly: + orderType = 'limit-maker' + timeInForce = self.safe_string(params, 'timeInForce', 'GTC') + if timeInForce == 'FOK': + orderType = orderType + '-fok' + elif timeInForce == 'IOC': + orderType = 'ioc' + request['type'] = side + '-' + orderType + clientOrderId = self.safe_string_2(params, 'clientOrderId', 'client-order-id') # must be 64 chars max and unique within 24 hours + if clientOrderId is None: + broker = self.safe_value(self.options, 'broker', {}) + brokerId = self.safe_string(broker, 'id') + request['client-order-id'] = brokerId + self.uuid() + else: + request['client-order-id'] = clientOrderId + if marginMode == 'cross': + request['source'] = 'super-margin-api' + elif marginMode == 'isolated': + request['source'] = 'margin-api' + elif marginMode == 'c2c': + request['source'] = 'c2c-margin-api' + if (orderType == 'market') and (side == 'buy'): + quoteAmount = None + createMarketBuyOrderRequiresPrice = True + createMarketBuyOrderRequiresPrice, params = self.handle_option_and_params(params, 'createOrder', 'createMarketBuyOrderRequiresPrice', True) + cost = self.safe_number(params, 'cost') + params = self.omit(params, 'cost') + if cost is not None: + quoteAmount = self.amount_to_precision(symbol, cost) + elif createMarketBuyOrderRequiresPrice: + if price is None: + raise InvalidOrder(self.id + ' createOrder() requires the price argument for market buy orders to calculate the total cost to spend(amount * price), alternatively set the createMarketBuyOrderRequiresPrice option or param to False and pass the cost to spend in the amount argument') + else: + # despite that cost = amount * price is in quote currency and should have quote precision + # the exchange API requires the cost supplied in 'amount' to be of base precision + # more about it here: + # https://github.com/ccxt/ccxt/pull/4395 + # https://github.com/ccxt/ccxt/issues/7611 + # we use amountToPrecision here because the exchange requires cost in base precision + amountString = self.number_to_string(amount) + priceString = self.number_to_string(price) + quoteAmount = self.amount_to_precision(symbol, Precise.string_mul(amountString, priceString)) + else: + quoteAmount = self.amount_to_precision(symbol, amount) + request['amount'] = quoteAmount + else: + request['amount'] = self.amount_to_precision(symbol, amount) + limitOrderTypes = self.safe_value(options, 'limitOrderTypes', {}) + if orderType in limitOrderTypes: + request['price'] = self.price_to_precision(symbol, price) + params = self.omit(params, ['triggerPrice', 'stopPrice', 'stop-price', 'clientOrderId', 'client-order-id', 'operator', 'timeInForce']) + return self.extend(request, params) + + def create_contract_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + @ignore + helper function to build request + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.timeInForce]: supports 'IOC' and 'FOK' + :param float [params.trailingPercent]: *contract only* the percent to trail away from the current market price + :param float [params.trailingTriggerPrice]: *contract only* the price to trigger a trailing order, default uses the price argument + :returns dict: request to be sent to the exchange + """ + market = self.market(symbol) + request: dict = { + 'contract_code': market['id'], + 'volume': self.amount_to_precision(symbol, amount), + 'direction': side, + } + postOnly = None + postOnly, params = self.handle_post_only(type == 'market', type == 'post_only', params) + if postOnly: + type = 'post_only' + timeInForce = self.safe_string(params, 'timeInForce', 'GTC') + if timeInForce == 'FOK': + type = 'fok' + elif timeInForce == 'IOC': + type = 'ioc' + triggerPrice = self.safe_number_n(params, ['triggerPrice', 'stopPrice', 'trigger_price']) + stopLossTriggerPrice = self.safe_number_2(params, 'stopLossPrice', 'sl_trigger_price') + takeProfitTriggerPrice = self.safe_number_2(params, 'takeProfitPrice', 'tp_trigger_price') + trailingPercent = self.safe_string_2(params, 'trailingPercent', 'callback_rate') + trailingTriggerPrice = self.safe_number(params, 'trailingTriggerPrice', price) + isTrailingPercentOrder = trailingPercent is not None + isTrigger = triggerPrice is not None + isStopLossTriggerOrder = stopLossTriggerPrice is not None + isTakeProfitTriggerOrder = takeProfitTriggerPrice is not None + if isTrigger: + triggerType = self.safe_string_2(params, 'triggerType', 'trigger_type', 'le') + request['trigger_type'] = triggerType + request['trigger_price'] = self.price_to_precision(symbol, triggerPrice) + if price is not None: + request['order_price'] = self.price_to_precision(symbol, price) + elif isStopLossTriggerOrder or isTakeProfitTriggerOrder: + if isStopLossTriggerOrder: + request['sl_order_price_type'] = type + request['sl_trigger_price'] = self.price_to_precision(symbol, stopLossTriggerPrice) + if price is not None: + request['sl_order_price'] = self.price_to_precision(symbol, price) + else: + request['tp_order_price_type'] = type + request['tp_trigger_price'] = self.price_to_precision(symbol, takeProfitTriggerPrice) + if price is not None: + request['tp_order_price'] = self.price_to_precision(symbol, price) + elif isTrailingPercentOrder: + trailingPercentString = Precise.string_div(trailingPercent, '100') + request['callback_rate'] = self.parse_to_numeric(trailingPercentString) + request['active_price'] = trailingTriggerPrice + request['order_price_type'] = self.safe_string(params, 'order_price_type', 'formula_price') + else: + clientOrderId = self.safe_integer_2(params, 'client_order_id', 'clientOrderId') + if clientOrderId is not None: + request['client_order_id'] = clientOrderId + params = self.omit(params, ['clientOrderId']) + if type == 'limit' or type == 'ioc' or type == 'fok' or type == 'post_only': + request['price'] = self.price_to_precision(symbol, price) + reduceOnly = self.safe_bool_2(params, 'reduceOnly', 'reduce_only', False) + if not isStopLossTriggerOrder and not isTakeProfitTriggerOrder: + if reduceOnly: + request['reduce_only'] = 1 + request['lever_rate'] = self.safe_integer_n(params, ['leverRate', 'lever_rate', 'leverage'], 1) + if not isTrailingPercentOrder: + request['order_price_type'] = type + hedged = self.safe_bool(params, 'hedged', False) + if hedged: + if reduceOnly: + request['offset'] = 'close' + else: + request['offset'] = 'open' + broker = self.safe_value(self.options, 'broker', {}) + brokerId = self.safe_string(broker, 'id') + request['channel_code'] = brokerId + params = self.omit(params, ['reduceOnly', 'triggerPrice', 'stopPrice', 'stopLossPrice', 'takeProfitPrice', 'triggerType', 'leverRate', 'timeInForce', 'leverage', 'trailingPercent', 'trailingTriggerPrice', 'hedged']) + return self.extend(request, params) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://huobiapi.github.io/docs/spot/v1/en/#place-a-new-order # spot, margin + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#place-an-order # coin-m swap + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#place-trigger-order # coin-m swap trigger + https://huobiapi.github.io/docs/usdt_swap/v1/en/#cross-place-an-order # usdt-m swap cross + https://huobiapi.github.io/docs/usdt_swap/v1/en/#cross-place-trigger-order # usdt-m swap cross trigger + https://huobiapi.github.io/docs/usdt_swap/v1/en/#isolated-place-an-order # usdt-m swap isolated + https://huobiapi.github.io/docs/usdt_swap/v1/en/#isolated-place-trigger-order # usdt-m swap isolated trigger + https://huobiapi.github.io/docs/usdt_swap/v1/en/#isolated-set-a-take-profit-and-stop-loss-order-for-an-existing-position + https://huobiapi.github.io/docs/usdt_swap/v1/en/#cross-set-a-take-profit-and-stop-loss-order-for-an-existing-position + https://huobiapi.github.io/docs/dm/v1/en/#place-an-order # coin-m futures + https://huobiapi.github.io/docs/dm/v1/en/#place-trigger-order # coin-m futures contract trigger + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: the price a trigger order is triggered at + :param str [params.triggerType]: *contract trigger orders only* ge: greater than or equal to, le: less than or equal to + :param float [params.stopLossPrice]: *contract only* the price a stop-loss order is triggered at + :param float [params.takeProfitPrice]: *contract only* the price a take-profit order is triggered at + :param str [params.operator]: *spot and margin only* gte or lte, trigger price condition + :param str [params.offset]: *contract only* 'both'(linear only), 'open', or 'close', required in hedge mode and for inverse markets + :param bool [params.postOnly]: *contract only* True or False + :param int [params.leverRate]: *contract only* required for all contract orders except tpsl, leverage greater than 20x requires prior approval of high-leverage agreement + :param str [params.timeInForce]: supports 'IOC' and 'FOK' + :param float [params.cost]: *spot market buy only* the quote quantity that can be used alternative for the amount + :param float [params.trailingPercent]: *contract only* the percent to trail away from the current market price + :param float [params.trailingTriggerPrice]: *contract only* the price to trigger a trailing order, default uses the price argument + :param bool [params.hedged]: *contract only* True for hedged mode, False for one way mode, default is False + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + triggerPrice = self.safe_number_n(params, ['triggerPrice', 'stopPrice', 'trigger_price']) + stopLossTriggerPrice = self.safe_number_2(params, 'stopLossPrice', 'sl_trigger_price') + takeProfitTriggerPrice = self.safe_number_2(params, 'takeProfitPrice', 'tp_trigger_price') + trailingPercent = self.safe_number(params, 'trailingPercent') + isTrailingPercentOrder = trailingPercent is not None + isTrigger = triggerPrice is not None + isStopLossTriggerOrder = stopLossTriggerPrice is not None + isTakeProfitTriggerOrder = takeProfitTriggerPrice is not None + response = None + if market['spot']: + if isTrailingPercentOrder: + raise NotSupported(self.id + ' createOrder() does not support trailing orders for spot markets') + spotRequest = await self.create_spot_order_request(symbol, type, side, amount, price, params) + response = await self.spotPrivatePostV1OrderOrdersPlace(spotRequest) + else: + contractRequest = self.create_contract_order_request(symbol, type, side, amount, price, params) + if market['linear']: + marginMode = None + marginMode, contractRequest = self.handle_margin_mode_and_params('createOrder', contractRequest) + marginMode = 'cross' if (marginMode is None) else marginMode + if marginMode == 'isolated': + if isTrigger: + response = await self.contractPrivatePostLinearSwapApiV1SwapTriggerOrder(contractRequest) + elif isStopLossTriggerOrder or isTakeProfitTriggerOrder: + response = await self.contractPrivatePostLinearSwapApiV1SwapTpslOrder(contractRequest) + elif isTrailingPercentOrder: + response = await self.contractPrivatePostLinearSwapApiV1SwapTrackOrder(contractRequest) + else: + response = await self.contractPrivatePostLinearSwapApiV1SwapOrder(contractRequest) + elif marginMode == 'cross': + if isTrigger: + response = await self.contractPrivatePostLinearSwapApiV1SwapCrossTriggerOrder(contractRequest) + elif isStopLossTriggerOrder or isTakeProfitTriggerOrder: + response = await self.contractPrivatePostLinearSwapApiV1SwapCrossTpslOrder(contractRequest) + elif isTrailingPercentOrder: + response = await self.contractPrivatePostLinearSwapApiV1SwapCrossTrackOrder(contractRequest) + else: + response = await self.contractPrivatePostLinearSwapApiV1SwapCrossOrder(contractRequest) + elif market['inverse']: + offset = self.safe_string(params, 'offset') + if offset is None: + raise ArgumentsRequired(self.id + ' createOrder() requires an extra parameter params["offset"] to be set to "open" or "close" when placing orders in inverse markets') + if market['swap']: + if isTrigger: + response = await self.contractPrivatePostSwapApiV1SwapTriggerOrder(contractRequest) + elif isStopLossTriggerOrder or isTakeProfitTriggerOrder: + response = await self.contractPrivatePostSwapApiV1SwapTpslOrder(contractRequest) + elif isTrailingPercentOrder: + response = await self.contractPrivatePostSwapApiV1SwapTrackOrder(contractRequest) + else: + response = await self.contractPrivatePostSwapApiV1SwapOrder(contractRequest) + elif market['future']: + if isTrigger: + response = await self.contractPrivatePostApiV1ContractTriggerOrder(contractRequest) + elif isStopLossTriggerOrder or isTakeProfitTriggerOrder: + response = await self.contractPrivatePostApiV1ContractTpslOrder(contractRequest) + elif isTrailingPercentOrder: + response = await self.contractPrivatePostApiV1ContractTrackOrder(contractRequest) + else: + response = await self.contractPrivatePostApiV1ContractOrder(contractRequest) + # + # spot + # + # {"status":"ok","data":"438398393065481"} + # + # swap and future + # + # { + # "status": "ok", + # "data": { + # "order_id": 924660854912552960, + # "order_id_str": "924660854912552960" + # }, + # "ts": 1640497927185 + # } + # + # stop-loss and take-profit + # + # { + # "status": "ok", + # "data": { + # "tp_order": { + # "order_id": 1101494204040163328, + # "order_id_str": "1101494204040163328" + # }, + # "sl_order": null + # }, + # "ts": :1682658283024 + # } + # + data = None + result = None + if market['spot']: + return self.safe_order({ + 'info': response, + 'id': self.safe_string(response, 'data'), + 'timestamp': None, + 'datetime': None, + 'lastTradeTimestamp': None, + 'status': None, + 'symbol': None, + 'type': type, + 'side': side, + 'price': price, + 'amount': amount, + 'filled': None, + 'remaining': None, + 'cost': None, + 'trades': None, + 'fee': None, + 'clientOrderId': None, + 'average': None, + }, market) + elif isStopLossTriggerOrder: + data = self.safe_value(response, 'data', {}) + result = self.safe_value(data, 'sl_order', {}) + elif isTakeProfitTriggerOrder: + data = self.safe_value(response, 'data', {}) + result = self.safe_value(data, 'tp_order', {}) + else: + result = self.safe_value(response, 'data', {}) + return self.parse_order(result, market) + + async def create_orders(self, orders: List[OrderRequest], params={}): + """ + create a list of trade orders + + https://huobiapi.github.io/docs/spot/v1/en/#place-a-batch-of-orders + https://huobiapi.github.io/docs/dm/v1/en/#place-a-batch-of-orders + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#place-a-batch-of-orders + https://huobiapi.github.io/docs/usdt_swap/v1/en/#isolated-place-a-batch-of-orders + https://huobiapi.github.io/docs/usdt_swap/v1/en/#cross-place-a-batch-of-orders + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + ordersRequests = [] + symbol = None + market = None + marginMode = None + for i in range(0, len(orders)): + rawOrder = orders[i] + marketId = self.safe_string(rawOrder, 'symbol') + if symbol is None: + symbol = marketId + else: + if symbol != marketId: + raise BadRequest(self.id + ' createOrders() requires all orders to have the same symbol') + type = self.safe_string(rawOrder, 'type') + side = self.safe_string(rawOrder, 'side') + amount = self.safe_value(rawOrder, 'amount') + price = self.safe_value(rawOrder, 'price') + orderParams = self.safe_value(rawOrder, 'params', {}) + marginResult = self.handle_margin_mode_and_params('createOrders', orderParams) + currentMarginMode = marginResult[0] + if currentMarginMode is not None: + if marginMode is None: + marginMode = currentMarginMode + else: + if marginMode != currentMarginMode: + raise BadRequest(self.id + ' createOrders() requires all orders to have the same margin mode(isolated or cross)') + market = self.market(symbol) + orderRequest = None + if market['spot']: + orderRequest = await self.create_spot_order_request(marketId, type, side, amount, price, orderParams) + else: + orderRequest = self.create_contract_order_request(marketId, type, side, amount, price, orderParams) + orderRequest = self.omit(orderRequest, 'marginMode') + ordersRequests.append(orderRequest) + request: dict = {} + response = None + if market['spot']: + response = await self.privatePostOrderBatchOrders(ordersRequests) + else: + request['orders_data'] = ordersRequests + if market['linear']: + marginMode = 'cross' if (marginMode is None) else marginMode + if marginMode == 'isolated': + response = await self.contractPrivatePostLinearSwapApiV1SwapBatchorder(request) + elif marginMode == 'cross': + response = await self.contractPrivatePostLinearSwapApiV1SwapCrossBatchorder(request) + elif market['inverse']: + if market['swap']: + response = await self.contractPrivatePostSwapApiV1SwapBatchorder(request) + elif market['future']: + response = await self.contractPrivatePostApiV1ContractBatchorder(request) + # + # spot + # + # { + # "status": "ok", + # "data": [ + # { + # "order-id": 936847569789079, + # "client-order-id": "AA03022abc3a55e82c-0087-4fc2-beac-112fdebb1ee9" + # }, + # { + # "client-order-id": "AA03022abcdb3baefb-3cfa-4891-8009-082b3d46ca82", + # "err-code": "account-frozen-balance-insufficient-error", + # "err-msg": "trade account balance is not enough, left: `89`" + # } + # ] + # } + # + # swap and future + # + # { + # "status": "ok", + # "data": { + # "errors": [ + # { + # "index": 2, + # "err_code": 1047, + # "err_msg": "Insufficient margin available." + # } + # ], + # "success": [ + # { + # "order_id": 1172923090632953857, + # "index": 1, + # "order_id_str": "1172923090632953857" + # } + # ] + # }, + # "ts": 1699688256671 + # } + # + result = None + if market['spot']: + result = self.safe_value(response, 'data', []) + else: + data = self.safe_value(response, 'data', {}) + success = self.safe_value(data, 'success', []) + errors = self.safe_value(data, 'errors', []) + result = self.array_concat(success, errors) + return self.parse_orders(result, market) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: *contract only* if the order is a trigger trigger order or not + :param boolean [params.stopLossTakeProfit]: *contract only* if the order is a stop-loss or take-profit order + :param boolean [params.trailing]: *contract only* set to True if you want to cancel a trailing order + :returns dict: An `order structure ` + """ + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + marketType = None + marketType, params = self.handle_market_type_and_params('cancelOrder', market, params) + request: dict = { + # spot ----------------------------------------------------------- + # 'order-id': 'id', + # 'symbol': market['id'], + # 'client-order-id': clientOrderId, + # contracts ------------------------------------------------------ + # 'order_id': id, + # 'client_order_id': clientOrderId, + # 'contract_code': market['id'], + # 'pair': 'BTC-USDT', + # 'contract_type': 'this_week', # swap, self_week, next_week, quarter, next_ quarter + } + response = None + if marketType == 'spot': + clientOrderId = self.safe_string_2(params, 'client-order-id', 'clientOrderId') + if clientOrderId is None: + request['order-id'] = id + response = await self.spotPrivatePostV1OrderOrdersOrderIdSubmitcancel(self.extend(request, params)) + else: + request['client-order-id'] = clientOrderId + params = self.omit(params, ['client-order-id', 'clientOrderId']) + response = await self.spotPrivatePostV1OrderOrdersSubmitCancelClientOrder(self.extend(request, params)) + else: + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + clientOrderId = self.safe_string_2(params, 'client_order_id', 'clientOrderId') + if clientOrderId is None: + request['order_id'] = id + else: + request['client_order_id'] = clientOrderId + params = self.omit(params, ['client_order_id', 'clientOrderId']) + if market['future']: + request['symbol'] = market['settleId'] + else: + request['contract_code'] = market['id'] + trigger = self.safe_bool_2(params, 'stop', 'trigger') + stopLossTakeProfit = self.safe_value(params, 'stopLossTakeProfit') + trailing = self.safe_bool(params, 'trailing', False) + params = self.omit(params, ['stop', 'stopLossTakeProfit', 'trailing', 'trigger']) + if market['linear']: + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('cancelOrder', params) + marginMode = 'cross' if (marginMode is None) else marginMode + if marginMode == 'isolated': + if trigger: + response = await self.contractPrivatePostLinearSwapApiV1SwapTriggerCancel(self.extend(request, params)) + elif stopLossTakeProfit: + response = await self.contractPrivatePostLinearSwapApiV1SwapTpslCancel(self.extend(request, params)) + elif trailing: + response = await self.contractPrivatePostLinearSwapApiV1SwapTrackCancel(self.extend(request, params)) + else: + response = await self.contractPrivatePostLinearSwapApiV1SwapCancel(self.extend(request, params)) + elif marginMode == 'cross': + if trigger: + response = await self.contractPrivatePostLinearSwapApiV1SwapCrossTriggerCancel(self.extend(request, params)) + elif stopLossTakeProfit: + response = await self.contractPrivatePostLinearSwapApiV1SwapCrossTpslCancel(self.extend(request, params)) + elif trailing: + response = await self.contractPrivatePostLinearSwapApiV1SwapCrossTrackCancel(self.extend(request, params)) + else: + response = await self.contractPrivatePostLinearSwapApiV1SwapCrossCancel(self.extend(request, params)) + elif market['inverse']: + if market['swap']: + if trigger: + response = await self.contractPrivatePostSwapApiV1SwapTriggerCancel(self.extend(request, params)) + elif stopLossTakeProfit: + response = await self.contractPrivatePostSwapApiV1SwapTpslCancel(self.extend(request, params)) + elif trailing: + response = await self.contractPrivatePostSwapApiV1SwapTrackCancel(self.extend(request, params)) + else: + response = await self.contractPrivatePostSwapApiV1SwapCancel(self.extend(request, params)) + elif market['future']: + if trigger: + response = await self.contractPrivatePostApiV1ContractTriggerCancel(self.extend(request, params)) + elif stopLossTakeProfit: + response = await self.contractPrivatePostApiV1ContractTpslCancel(self.extend(request, params)) + elif trailing: + response = await self.contractPrivatePostApiV1ContractTrackCancel(self.extend(request, params)) + else: + response = await self.contractPrivatePostApiV1ContractCancel(self.extend(request, params)) + else: + raise NotSupported(self.id + ' cancelOrder() does not support ' + marketType + ' markets') + # + # spot + # + # { + # "status": "ok", + # "data": "10138899000", + # } + # + # future and swap + # + # { + # "status": "ok", + # "data": { + # "errors": [], + # "successes": "924660854912552960" + # }, + # "ts": 1640504486089 + # } + # + return self.extend(self.parse_order(response, market), { + 'id': id, + 'status': 'canceled', + }) + + async def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): + """ + cancel multiple orders + :param str[] ids: order ids + :param str symbol: unified market symbol, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.trigger]: *contract only* if the orders are trigger trigger orders or not + :param bool [params.stopLossTakeProfit]: *contract only* if the orders are stop-loss or take-profit orders + :returns dict: an list of `order structures ` + """ + await self.load_markets() + market: Market = None + if symbol is not None: + market = self.market(symbol) + marketType = None + marketType, params = self.handle_market_type_and_params('cancelOrders', market, params) + request: dict = { + # spot ----------------------------------------------------------- + # 'order-ids': ','.join(ids), # max 50 + # 'client-order-ids': ','.join(ids), # max 50 + # contracts ------------------------------------------------------ + # 'order_id': id, # comma separated, max 10 + # 'client_order_id': clientOrderId, # comma separated, max 10 + # 'contract_code': market['id'], + # 'symbol': market['settleId'], + } + response = None + if marketType == 'spot': + clientOrderIds = self.safe_value_2(params, 'client-order-id', 'clientOrderId') + clientOrderIds = self.safe_value_2(params, 'client-order-ids', 'clientOrderIds', clientOrderIds) + if clientOrderIds is None: + if isinstance(clientOrderIds, str): + request['order-ids'] = [ids] + else: + request['order-ids'] = ids + else: + if isinstance(clientOrderIds, str): + request['client-order-ids'] = [clientOrderIds] + else: + request['client-order-ids'] = clientOrderIds + params = self.omit(params, ['client-order-id', 'client-order-ids', 'clientOrderId', 'clientOrderIds']) + response = await self.spotPrivatePostV1OrderOrdersBatchcancel(self.extend(request, params)) + else: + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrders() requires a symbol argument') + clientOrderIds = self.safe_string_2(params, 'client_order_id', 'clientOrderId') + clientOrderIds = self.safe_string_2(params, 'client_order_ids', 'clientOrderIds', clientOrderIds) + if clientOrderIds is None: + request['order_id'] = ','.join(ids) + else: + request['client_order_id'] = clientOrderIds + params = self.omit(params, ['client_order_id', 'client_order_ids', 'clientOrderId', 'clientOrderIds']) + if market['future']: + request['symbol'] = market['settleId'] + else: + request['contract_code'] = market['id'] + trigger = self.safe_bool_2(params, 'stop', 'trigger') + stopLossTakeProfit = self.safe_value(params, 'stopLossTakeProfit') + params = self.omit(params, ['stop', 'stopLossTakeProfit', 'trigger']) + if market['linear']: + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('cancelOrders', params) + marginMode = 'cross' if (marginMode is None) else marginMode + if marginMode == 'isolated': + if trigger: + response = await self.contractPrivatePostLinearSwapApiV1SwapTriggerCancel(self.extend(request, params)) + elif stopLossTakeProfit: + response = await self.contractPrivatePostLinearSwapApiV1SwapTpslCancel(self.extend(request, params)) + else: + response = await self.contractPrivatePostLinearSwapApiV1SwapCancel(self.extend(request, params)) + elif marginMode == 'cross': + if trigger: + response = await self.contractPrivatePostLinearSwapApiV1SwapCrossTriggerCancel(self.extend(request, params)) + elif stopLossTakeProfit: + response = await self.contractPrivatePostLinearSwapApiV1SwapCrossTpslCancel(self.extend(request, params)) + else: + response = await self.contractPrivatePostLinearSwapApiV1SwapCrossCancel(self.extend(request, params)) + elif market['inverse']: + if market['swap']: + if trigger: + response = await self.contractPrivatePostSwapApiV1SwapTriggerCancel(self.extend(request, params)) + elif stopLossTakeProfit: + response = await self.contractPrivatePostSwapApiV1SwapTpslCancel(self.extend(request, params)) + else: + response = await self.contractPrivatePostSwapApiV1SwapCancel(self.extend(request, params)) + elif market['future']: + if trigger: + response = await self.contractPrivatePostApiV1ContractTriggerCancel(self.extend(request, params)) + elif stopLossTakeProfit: + response = await self.contractPrivatePostApiV1ContractTpslCancel(self.extend(request, params)) + else: + response = await self.contractPrivatePostApiV1ContractCancel(self.extend(request, params)) + else: + raise NotSupported(self.id + ' cancelOrders() does not support ' + marketType + ' markets') + # + # spot + # + # { + # "status": "ok", + # "data": { + # "success": [ + # "5983466" + # ], + # "failed": [ + # { + # "err-msg": "Incorrect order state", + # "order-state": 7, + # "order-id": "", + # "err-code": "order-orderstate-error", + # "client-order-id": "first" + # }, + # { + # "err-msg": "Incorrect order state", + # "order-state": 7, + # "order-id": "", + # "err-code": "order-orderstate-error", + # "client-order-id": "second" + # }, + # { + # "err-msg": "The record is not found.", + # "order-id": "", + # "err-code": "base-not-found", + # "client-order-id": "third" + # } + # ] + # } + # } + # + # future and swap + # + # { + # "status": "ok", + # "data": { + # "errors": [ + # { + # "order_id": "769206471845261312", + # "err_code": 1061, + # "err_msg": "This order doesnt exist." + # } + # ], + # "successes": "773120304138219520" + # }, + # "ts": 1604367997451 + # } + # + data = self.safe_dict(response, 'data') + return self.parse_cancel_orders(data) + + def parse_cancel_orders(self, orders): + # + # { + # "success": [ + # "5983466" + # ], + # "failed": [ + # { + # "err-msg": "Incorrect order state", + # "order-state": 7, + # "order-id": "", + # "err-code": "order-orderstate-error", + # "client-order-id": "first" + # }, + # ... + # ] + # } + # + # { + # "errors": [ + # { + # "order_id": "769206471845261312", + # "err_code": 1061, + # "err_msg": "This order doesnt exist." + # } + # ], + # "successes": "1258075374411399168,1258075393254871040" + # } + # + successes = self.safe_string(orders, 'successes') + success = None + if successes is not None: + success = successes.split(',') + else: + success = self.safe_list(orders, 'success', []) + failed = self.safe_list_2(orders, 'errors', 'failed', []) + result = [] + for i in range(0, len(success)): + order = success[i] + result.append(self.safe_order({ + 'info': order, + 'id': order, + 'status': 'canceled', + })) + for i in range(0, len(failed)): + order = failed[i] + result.append(self.safe_order({ + 'info': order, + 'id': self.safe_string_2(order, 'order-id', 'order_id'), + 'status': 'failed', + 'clientOrderId': self.safe_string(order, 'client-order-id'), + })) + return result + + async def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders + :param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: *contract only* if the orders are trigger trigger orders or not + :param boolean [params.stopLossTakeProfit]: *contract only* if the orders are stop-loss or take-profit orders + :param boolean [params.trailing]: *contract only* set to True if you want to cancel all trailing orders + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + marketType = None + marketType, params = self.handle_market_type_and_params('cancelAllOrders', market, params) + request: dict = { + # spot ----------------------------------------------------------- + # 'account-id': account['id'], + # 'symbol': market['id'], # a list of comma-separated symbols, all symbols by default + # 'types' 'string', buy-market, sell-market, buy-limit, sell-limit, buy-ioc, sell-ioc, buy-stop-limit, sell-stop-limit, buy-limit-fok, sell-limit-fok, buy-stop-limit-fok, sell-stop-limit-fok + # 'side': 'buy', # or 'sell' + # 'size': 100, # the number of orders to cancel 1-100 + # contract ------------------------------------------------------- + # 'symbol': market['settleId'], # required + # 'contract_code': market['id'], + # 'contract_type': 'this_week', # swap, self_week, next_week, quarter, next_ quarter + # 'direction': 'buy': # buy, sell + # 'offset': 'open', # open, close + } + response = None + if marketType == 'spot': + if symbol is not None: + request['symbol'] = market['id'] + response = await self.spotPrivatePostV1OrderOrdersBatchCancelOpenOrders(self.extend(request, params)) + # + # { + # "code": 200, + # "data": { + # "success-count": 2, + # "failed-count": 0, + # "next-id": 5454600 + # } + # } + # + data = self.safe_dict(response, 'data') + return [ + self.safe_order({ + 'info': data, + }), + ] + else: + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelAllOrders() requires a symbol argument') + if market['future']: + request['symbol'] = market['settleId'] + request['contract_code'] = market['id'] + trigger = self.safe_bool_2(params, 'stop', 'trigger') + stopLossTakeProfit = self.safe_value(params, 'stopLossTakeProfit') + trailing = self.safe_bool(params, 'trailing', False) + params = self.omit(params, ['stop', 'stopLossTakeProfit', 'trailing', 'trigger']) + if market['linear']: + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('cancelAllOrders', params) + marginMode = 'cross' if (marginMode is None) else marginMode + if marginMode == 'isolated': + if trigger: + response = await self.contractPrivatePostLinearSwapApiV1SwapTriggerCancelall(self.extend(request, params)) + elif stopLossTakeProfit: + response = await self.contractPrivatePostLinearSwapApiV1SwapTpslCancelall(self.extend(request, params)) + elif trailing: + response = await self.contractPrivatePostLinearSwapApiV1SwapTrackCancelall(self.extend(request, params)) + else: + response = await self.contractPrivatePostLinearSwapApiV1SwapCancelall(self.extend(request, params)) + elif marginMode == 'cross': + if trigger: + response = await self.contractPrivatePostLinearSwapApiV1SwapCrossTriggerCancelall(self.extend(request, params)) + elif stopLossTakeProfit: + response = await self.contractPrivatePostLinearSwapApiV1SwapCrossTpslCancelall(self.extend(request, params)) + elif trailing: + response = await self.contractPrivatePostLinearSwapApiV1SwapCrossTrackCancelall(self.extend(request, params)) + else: + response = await self.contractPrivatePostLinearSwapApiV1SwapCrossCancelall(self.extend(request, params)) + elif market['inverse']: + if market['swap']: + if trigger: + response = await self.contractPrivatePostSwapApiV1SwapTriggerCancelall(self.extend(request, params)) + elif stopLossTakeProfit: + response = await self.contractPrivatePostSwapApiV1SwapTpslCancelall(self.extend(request, params)) + elif trailing: + response = await self.contractPrivatePostSwapApiV1SwapTrackCancelall(self.extend(request, params)) + else: + response = await self.contractPrivatePostSwapApiV1SwapCancelall(self.extend(request, params)) + elif market['future']: + if trigger: + response = await self.contractPrivatePostApiV1ContractTriggerCancelall(self.extend(request, params)) + elif stopLossTakeProfit: + response = await self.contractPrivatePostApiV1ContractTpslCancelall(self.extend(request, params)) + elif trailing: + response = await self.contractPrivatePostApiV1ContractTrackCancelall(self.extend(request, params)) + else: + response = await self.contractPrivatePostApiV1ContractCancelall(self.extend(request, params)) + else: + raise NotSupported(self.id + ' cancelAllOrders() does not support ' + marketType + ' markets') + # + # { + # "status": "ok", + # "data": { + # "errors": [], + # "successes": "1104754904426696704" + # }, + # "ts": "1683435723755" + # } + # + data = self.safe_dict(response, 'data') + return self.parse_cancel_orders(data) + + async def cancel_all_orders_after(self, timeout: Int, params={}): + """ + dead man's switch, cancel all orders after the given timeout + + https://huobiapi.github.io/docs/spot/v1/en/#dead-man-s-switch + + :param number timeout: time in milliseconds, 0 represents cancel the timer + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: the api result + """ + await self.load_markets() + request: dict = { + 'timeout': self.parse_to_int(timeout / 1000) if (timeout > 0) else 0, + } + response = await self.v2PrivatePostAlgoOrdersCancelAllAfter(self.extend(request, params)) + # + # { + # "code": 200, + # "message": "success", + # "data": { + # "currentTime": 1630491627230, + # "triggerTime": 1630491637230 + # } + # } + # + return response + + def parse_deposit_address(self, depositAddress, currency: Currency = None): + # + # { + # "currency": "usdt", + # "address": "0xf7292eb9ba7bc50358e27f0e025a4d225a64127b", + # "addressTag": "", + # "chain": "usdterc20", # trc20usdt, hrc20usdt, usdt, algousdt + # } + # + address = self.safe_string(depositAddress, 'address') + tag = self.safe_string(depositAddress, 'addressTag') + currencyId = self.safe_string(depositAddress, 'currency') + currency = self.safe_currency(currencyId, currency) + code = self.safe_currency_code(currencyId, currency) + note = self.safe_string(depositAddress, 'note') + networkId = self.safe_string(depositAddress, 'chain') + self.check_address(address) + return { + 'currency': code, + 'address': address, + 'tag': tag, + 'network': self.network_id_to_code(networkId), + 'note': note, + 'info': depositAddress, + } + + async def fetch_deposit_addresses_by_network(self, code: str, params={}) -> List[DepositAddress]: + """ + + https://www.htx.com/en-us/opend/newApiPages/?id=7ec50029-7773-11ed-9966-0242ac110003 + + fetch a dictionary of addresses for a currency, indexed by network + :param str code: unified currency code of the currency for the deposit address + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `address structures ` indexed by the network + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + } + response = await self.spotPrivateGetV2AccountDepositAddress(self.extend(request, params)) + # + # { + # "code": 200, + # "data": [ + # { + # "currency": "eth", + # "address": "0xf7292eb9ba7bc50358e27f0e025a4d225a64127b", + # "addressTag": "", + # "chain": "eth" + # } + # ] + # } + # + data = self.safe_value(response, 'data', []) + parsed = self.parse_deposit_addresses(data, [currency['code']], False) + return self.index_by(parsed, 'network') + + async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://www.htx.com/en-us/opend/newApiPages/?id=7ec50029-7773-11ed-9966-0242ac110003 + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + await self.load_markets() + currency = self.currency(code) + networkCode, paramsOmited = self.handle_network_code_and_params(params) + indexedAddresses = await self.fetch_deposit_addresses_by_network(code, paramsOmited) + selectedNetworkCode = self.select_network_code_from_unified_networks(currency['code'], networkCode, indexedAddresses) + return indexedAddresses[selectedNetworkCode] + + async def fetch_withdraw_addresses(self, code: str, note=None, networkCode=None, params={}): + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + } + response = await self.spotPrivateGetV2AccountWithdrawAddress(self.extend(request, params)) + # + # { + # "code": 200, + # "data": [ + # { + # "currency": "eth", + # "chain": "eth" + # "note": "Binance - TRC20", + # "addressTag": "", + # "address": "0xf7292eb9ba7bc50358e27f0e025a4d225a64127b", + # } + # ] + # } + # + data = self.safe_value(response, 'data', []) + allAddresses = self.parse_deposit_addresses(data, [currency['code']], False) # cjg: to do remove self weird object or array ambiguity + addresses = [] + for i in range(0, len(allAddresses)): + address = allAddresses[i] + noteMatch = (note is None) or (address['note'] == note) + networkMatch = (networkCode is None) or (address['network'] == networkCode) + if noteMatch and networkMatch: + addresses.append(address) + return addresses + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + + https://www.htx.com/en-us/opend/newApiPages/?id=7ec4f050-7773-11ed-9966-0242ac110003 + + fetch all deposits made to an account + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + if limit is None or limit > 100: + limit = 100 + await self.load_markets() + currency = None + if code is not None: + currency = self.currency(code) + request: dict = { + 'type': 'deposit', + 'direct': 'next', + 'from': 0, # From 'id' ... if you want to get results after a particular transaction id, pass the id in params.from + } + if currency is not None: + request['currency'] = currency['id'] + if limit is not None: + request['size'] = limit # max 100 + response = await self.spotPrivateGetV1QueryDepositWithdraw(self.extend(request, params)) + # + # { + # "status": "ok", + # "data": [ + # { + # "id": "75115912", + # "type": "deposit", + # "sub-type": "NORMAL", + # "request-id": "trc20usdt-a2e229a44ef2a948c874366230bb56aa73631cc0a03d177bd8b4c9d38262d7ff-200", + # "currency": "usdt", + # "chain": "trc20usdt", + # "tx-hash": "a2e229a44ef2a948c874366230bb56aa73631cc0a03d177bd8b4c9d38262d7ff", + # "amount": "12.000000000000000000", + # "from-addr-tag": "", + # "address-id": "0", + # "address": "TRFTd1FxepQE6CnpwzUEMEbFaLm5bJK67s", + # "address-tag": "", + # "fee": "0", + # "state": "safe", + # "wallet-confirm": "2", + # "created-at": "1621843808662", + # "updated-at": "1621843857137" + # }, + # ] + # } + # + return self.parse_transactions(response['data'], currency, since, limit) + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://huobiapi.github.io/docs/spot/v1/en/#search-for-existed-withdraws-and-deposits + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + if limit is None or limit > 100: + limit = 100 + await self.load_markets() + currency = None + if code is not None: + currency = self.currency(code) + request: dict = { + 'type': 'withdraw', + 'direct': 'next', + 'from': 0, # From 'id' ... if you want to get results after a particular transaction id, pass the id in params.from + } + if currency is not None: + request['currency'] = currency['id'] + if limit is not None: + request['size'] = limit # max 100 + response = await self.spotPrivateGetV1QueryDepositWithdraw(self.extend(request, params)) + # + # { + # "status": "ok", + # "data": [ + # { + # "id": "61335312", + # "type": "withdraw", + # "sub-type": "NORMAL", + # "currency": "usdt", + # "chain": "trc20usdt", + # "tx-hash": "30a3111f2fead74fae45c6218ca3150fc33cab2aa59cfe41526b96aae79ce4ec", + # "amount": "12.000000000000000000", + # "from-addr-tag": "", + # "address-id": "27321591", + # "address": "TRf5JacJQRsF4Nm2zu11W6maDGeiEWQu9e", + # "address-tag": "", + # "fee": "1.000000000000000000", + # "state": "confirmed", + # "created-at": "1621852316553", + # "updated-at": "1621852467041" + # }, + # ] + # } + # + return self.parse_transactions(response['data'], currency, since, limit) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fetchDeposits + # + # { + # "id": "75115912", + # "type": "deposit", + # "sub-type": "NORMAL", + # "request-id": "trc20usdt-a2e229a44ef2a948c874366230bb56aa73631cc0a03d177bd8b4c9d38262d7ff-200", + # "currency": "usdt", + # "chain": "trc20usdt", + # "tx-hash": "a2e229a44ef2a948c874366230bb56aa73631cc0a03d177bd8b4c9d38262d7ff", + # "amount": "2849.000000000000000000", + # "from-addr-tag": "", + # "address-id": "0", + # "address": "TRFTd1FxepQE6CnpwzUEMEbFaLm5bJK67s", + # "address-tag": "", + # "fee": "0", + # "state": "safe", + # "wallet-confirm": "2", + # "created-at": "1621843808662", + # "updated-at": "1621843857137" + # }, + # + # fetchWithdrawals + # + # { + # "id": "61335312", + # "type": "withdraw", + # "sub-type": "NORMAL", + # "currency": "usdt", + # "chain": "trc20usdt", + # "tx-hash": "30a3111f2fead74fae45c6218ca3150fc33cab2aa59cfe41526b96aae79ce4ec", + # "amount": "12.000000000000000000", + # "from-addr-tag": "", + # "address-id": "27321591", + # "address": "TRf5JacJQRsF4Nm2zu11W6maDGeiEWQu9e", + # "address-tag": "", + # "fee": "1.000000000000000000", + # "state": "confirmed", + # "created-at": "1621852316553", + # "updated-at": "1621852467041" + # } + # + # withdraw + # + # { + # "status": "ok", + # "data": "99562054" + # } + # + timestamp = self.safe_integer(transaction, 'created-at') + code = self.safe_currency_code(self.safe_string(transaction, 'currency')) + type = self.safe_string(transaction, 'type') + if type == 'withdraw': + type = 'withdrawal' + feeCost = self.safe_string(transaction, 'fee') + if feeCost is not None: + feeCost = Precise.string_abs(feeCost) + networkId = self.safe_string(transaction, 'chain') + txHash = self.safe_string(transaction, 'tx-hash') + if networkId == 'ETH' and txHash.find('0x') < 0: + txHash = '0x' + txHash + subType = self.safe_string(transaction, 'sub-type') + internal = subType == 'FAST' + return { + 'info': transaction, + 'id': self.safe_string_2(transaction, 'id', 'data'), + 'txid': txHash, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': self.network_id_to_code(networkId), + 'address': self.safe_string(transaction, 'address'), + 'addressTo': None, + 'addressFrom': None, + 'tag': self.safe_string(transaction, 'address-tag'), + 'tagTo': None, + 'tagFrom': None, + 'type': type, + 'amount': self.safe_number(transaction, 'amount'), + 'currency': code, + 'status': self.parse_transaction_status(self.safe_string(transaction, 'state')), + 'updated': self.safe_integer(transaction, 'updated-at'), + 'comment': None, + 'internal': internal, + 'fee': { + 'currency': code, + 'cost': self.parse_number(feeCost), + 'rate': None, + }, + } + + def parse_transaction_status(self, status: Str): + statuses: dict = { + # deposit statuses + 'unknown': 'failed', + 'confirming': 'pending', + 'confirmed': 'ok', + 'safe': 'ok', + 'orphan': 'failed', + # withdrawal statuses + 'submitted': 'pending', + 'canceled': 'canceled', + 'reexamine': 'pending', + 'reject': 'failed', + 'pass': 'pending', + 'wallet-reject': 'failed', + # 'confirmed': 'ok', # present in deposit statuses + 'confirm-error': 'failed', + 'repealed': 'failed', + 'wallet-transfer': 'pending', + 'pre-transfer': 'pending', + 'verifying': 'pending', + } + return self.safe_string(statuses, status, status) + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + + https://www.htx.com/en-us/opend/newApiPages/?id=7ec4cc41-7773-11ed-9966-0242ac110003 + + make a withdrawal + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + await self.load_markets() + self.check_address(address) + currency = self.currency(code) + request: dict = { + 'address': address, # only supports existing addresses in your withdraw address list + 'currency': currency['id'].lower(), + } + if tag is not None: + request['addr-tag'] = tag # only for XRP? + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + if networkCode is not None: + request['chain'] = self.network_code_to_id(networkCode, code) + amount = float(self.currency_to_precision(code, amount, networkCode)) + withdrawOptions = self.safe_value(self.options, 'withdraw', {}) + if self.safe_bool(withdrawOptions, 'includeFee', False): + fee = self.safe_number(params, 'fee') + if fee is None: + currencies = await self.fetch_currencies() + self.currencies = self.map_to_safe_map(self.deep_extend(self.currencies, currencies)) + targetNetwork = self.safe_value(currency['networks'], networkCode, {}) + fee = self.safe_number(targetNetwork, 'fee') + if fee is None: + raise ArgumentsRequired(self.id + ' withdraw() function can not find withdraw fee for chosen network. You need to re-load markets with "exchange.loadMarkets(True)", or provide the "fee" parameter') + # fee needs to be deducted from whole amount + feeString = self.currency_to_precision(code, fee, networkCode) + params = self.omit(params, 'fee') + amountString = self.number_to_string(amount) + amountSubtractedString = Precise.string_sub(amountString, feeString) + amountSubtracted = float(amountSubtractedString) + request['fee'] = float(feeString) + amount = float(self.currency_to_precision(code, amountSubtracted, networkCode)) + request['amount'] = amount + response = await self.spotPrivatePostV1DwWithdrawApiCreate(self.extend(request, params)) + # + # { + # "status": "ok", + # "data": "99562054" + # } + # + return self.parse_transaction(response, currency) + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + # + # transfer + # + # { + # "data": 12345, + # "status": "ok" + # } + # + id = self.safe_string(transfer, 'data') + code = self.safe_currency_code(None, currency) + return { + 'info': transfer, + 'id': id, + 'timestamp': None, + 'datetime': None, + 'currency': code, + 'amount': None, + 'fromAccount': None, + 'toAccount': None, + 'status': None, + } + + async def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://huobiapi.github.io/docs/dm/v1/en/#transfer-margin-between-spot-account-and-future-account + https://huobiapi.github.io/docs/spot/v1/en/#transfer-fund-between-spot-account-and-future-contract-account + https://huobiapi.github.io/docs/usdt_swap/v1/en/#general-transfer-margin-between-spot-account-and-usdt-margined-contracts-account + https://huobiapi.github.io/docs/spot/v1/en/#transfer-asset-from-spot-trading-account-to-cross-margin-account-cross + https://huobiapi.github.io/docs/spot/v1/en/#transfer-asset-from-spot-trading-account-to-isolated-margin-account-isolated + https://huobiapi.github.io/docs/spot/v1/en/#transfer-asset-from-cross-margin-account-to-spot-trading-account-cross + https://huobiapi.github.io/docs/spot/v1/en/#transfer-asset-from-isolated-margin-account-to-spot-trading-account-isolated + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account to transfer from 'spot', 'future', 'swap' + :param str toAccount: account to transfer to 'spot', 'future', 'swap' + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.symbol]: used for isolated margin transfer + :param str [params.subType]: 'linear' or 'inverse', only used when transfering to/from swap accounts + :returns dict: a `transfer structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + 'amount': float(self.currency_to_precision(code, amount)), + } + subType = None + subType, params = self.handle_sub_type_and_params('transfer', None, params) + fromAccountId = self.convert_type_to_account(fromAccount) + toAccountId = self.convert_type_to_account(toAccount) + toCross = toAccountId == 'cross' + fromCross = fromAccountId == 'cross' + toIsolated = self.in_array(toAccountId, self.ids) + fromIsolated = self.in_array(fromAccountId, self.ids) + fromSpot = fromAccountId == 'pro' + toSpot = toAccountId == 'pro' + if fromSpot and toSpot: + raise BadRequest(self.id + ' transfer() cannot make a transfer between ' + fromAccount + ' and ' + toAccount) + fromOrToFuturesAccount = (fromAccountId == 'futures') or (toAccountId == 'futures') + response = None + if fromOrToFuturesAccount: + type = fromAccountId + '-to-' + toAccountId + type = self.safe_string(params, 'type', type) + request['type'] = type + response = await self.spotPrivatePostV1FuturesTransfer(self.extend(request, params)) + elif fromSpot and toCross: + response = await self.privatePostCrossMarginTransferIn(self.extend(request, params)) + elif fromCross and toSpot: + response = await self.privatePostCrossMarginTransferOut(self.extend(request, params)) + elif fromSpot and toIsolated: + request['symbol'] = toAccountId + response = await self.privatePostDwTransferInMargin(self.extend(request, params)) + elif fromIsolated and toSpot: + request['symbol'] = fromAccountId + response = await self.privatePostDwTransferOutMargin(self.extend(request, params)) + else: + if subType == 'linear': + if (fromAccountId == 'swap') or (fromAccount == 'linear-swap'): + fromAccountId = 'linear-swap' + else: + toAccountId = 'linear-swap' + # check if cross-margin or isolated + symbol = self.safe_string(params, 'symbol') + params = self.omit(params, 'symbol') + if symbol is not None: + symbol = self.market_id(symbol) + request['margin-account'] = symbol + else: + request['margin-account'] = 'USDT' # cross-margin + request['from'] = 'spot' if fromSpot else fromAccountId + request['to'] = 'spot' if toSpot else toAccountId + response = await self.v2PrivatePostAccountTransfer(self.extend(request, params)) + # + # { + # "code": "200", + # "data": "660150061", + # "message": "Succeed", + # "success": True, + # "print-log": True + # } + # + return self.parse_transfer(response, currency) + + async def fetch_isolated_borrow_rates(self, params={}) -> IsolatedBorrowRates: + """ + fetch the borrow interest rates of all currencies + + https://huobiapi.github.io/docs/spot/v1/en/#get-loan-interest-rate-and-quota-isolated + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `isolated borrow rate structures ` + """ + await self.load_markets() + response = await self.spotPrivateGetV1MarginLoanInfo(params) + # + # { + # "status": "ok", + # "data": [ + # { + # "symbol": "1inchusdt", + # "currencies": [ + # { + # "currency": "1inch", + # "interest-rate": "0.00098", + # "min-loan-amt": "90.000000000000000000", + # "max-loan-amt": "1000.000000000000000000", + # "loanable-amt": "0.0", + # "actual-rate": "0.00098" + # }, + # { + # "currency": "usdt", + # "interest-rate": "0.00098", + # "min-loan-amt": "100.000000000000000000", + # "max-loan-amt": "1000.000000000000000000", + # "loanable-amt": "0.0", + # "actual-rate": "0.00098" + # } + # ] + # }, + # ... + # ] + # } + # + data = self.safe_value(response, 'data', []) + return self.parse_isolated_borrow_rates(data) + + def parse_isolated_borrow_rate(self, info: dict, market: Market = None) -> IsolatedBorrowRate: + # + # { + # "symbol": "1inchusdt", + # "currencies": [ + # { + # "currency": "1inch", + # "interest-rate": "0.00098", + # "min-loan-amt": "90.000000000000000000", + # "max-loan-amt": "1000.000000000000000000", + # "loanable-amt": "0.0", + # "actual-rate": "0.00098" + # }, + # { + # "currency": "usdt", + # "interest-rate": "0.00098", + # "min-loan-amt": "100.000000000000000000", + # "max-loan-amt": "1000.000000000000000000", + # "loanable-amt": "0.0", + # "actual-rate": "0.00098" + # } + # ] + # }, + # + marketId = self.safe_string(info, 'symbol') + symbol = self.safe_symbol(marketId, market) + currencies = self.safe_value(info, 'currencies', []) + baseData = self.safe_value(currencies, 0) + quoteData = self.safe_value(currencies, 1) + baseId = self.safe_string(baseData, 'currency') + quoteId = self.safe_string(quoteData, 'currency') + return { + 'symbol': symbol, + 'base': self.safe_currency_code(baseId), + 'baseRate': self.safe_number(baseData, 'actual-rate'), + 'quote': self.safe_currency_code(quoteId), + 'quoteRate': self.safe_number(quoteData, 'actual-rate'), + 'period': 86400000, + 'timestamp': None, + 'datetime': None, + 'info': info, + } + + async def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + + https://huobiapi.github.io/docs/usdt_swap/v1/en/#general-query-historical-funding-rate + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#query-historical-funding-rate + + fetches historical funding rate prices + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: not used by huobi, but filtered internally by ccxt + :param int [limit]: not used by huobi, but filtered internally by ccxt + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `funding rate structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchFundingRateHistory() requires a symbol argument') + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchFundingRateHistory', 'paginate') + if paginate: + return await self.fetch_paginated_call_cursor('fetchFundingRateHistory', symbol, since, limit, params, 'current_page', 'page_index', 1, 50) + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'contract_code': market['id'], + } + if limit is not None: + request['page_size'] = limit + else: + request['page_size'] = 50 # max + response = None + if market['inverse']: + response = await self.contractPublicGetSwapApiV1SwapHistoricalFundingRate(self.extend(request, params)) + elif market['linear']: + response = await self.contractPublicGetLinearSwapApiV1SwapHistoricalFundingRate(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchFundingRateHistory() supports inverse and linear swaps only') + # + # { + # "status": "ok", + # "data": { + # "total_page": 62, + # "current_page": 1, + # "total_size": 1237, + # "data": [ + # { + # "avg_premium_index": "-0.000208064395065541", + # "funding_rate": "0.000100000000000000", + # "realized_rate": "0.000100000000000000", + # "funding_time": "1638921600000", + # "contract_code": "BTC-USDT", + # "symbol": "BTC", + # "fee_asset": "USDT" + # }, + # ] + # }, + # "ts": 1638939294277 + # } + # + data = self.safe_value(response, 'data') + cursor = self.safe_value(data, 'current_page') + result = self.safe_value(data, 'data', []) + rates = [] + for i in range(0, len(result)): + entry = result[i] + entry['current_page'] = cursor + marketId = self.safe_string(entry, 'contract_code') + symbolInner = self.safe_symbol(marketId) + timestamp = self.safe_integer(entry, 'funding_time') + rates.append({ + 'info': entry, + 'symbol': symbolInner, + 'fundingRate': self.safe_number(entry, 'funding_rate'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + sorted = self.sort_by(rates, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, market['symbol'], since, limit) + + def parse_funding_rate(self, contract, market: Market = None) -> FundingRate: + # + # { + # "status": "ok", + # "data": { + # "estimated_rate": "0.000100000000000000", + # "funding_rate": "0.000100000000000000", + # "contract_code": "BCH-USD", + # "symbol": "BCH", + # "fee_asset": "BCH", + # "funding_time": "1639094400000", + # "next_funding_time": "1639123200000" + # }, + # "ts": 1639085854775 + # } + # + nextFundingRate = self.safe_number(contract, 'estimated_rate') + fundingTimestamp = self.safe_integer(contract, 'funding_time') + nextFundingTimestamp = self.safe_integer(contract, 'next_funding_time') + fundingTimeString = self.safe_string(contract, 'funding_time') + nextFundingTimeString = self.safe_string(contract, 'next_funding_time') + millisecondsInterval = Precise.string_sub(nextFundingTimeString, fundingTimeString) + marketId = self.safe_string(contract, 'contract_code') + symbol = self.safe_symbol(marketId, market) + return { + 'info': contract, + 'symbol': symbol, + 'markPrice': None, + 'indexPrice': None, + 'interestRate': None, + 'estimatedSettlePrice': None, + 'timestamp': None, + 'datetime': None, + 'fundingRate': self.safe_number(contract, 'funding_rate'), + 'fundingTimestamp': fundingTimestamp, + 'fundingDatetime': self.iso8601(fundingTimestamp), + 'nextFundingRate': nextFundingRate, + 'nextFundingTimestamp': nextFundingTimestamp, + 'nextFundingDatetime': self.iso8601(nextFundingTimestamp), + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': self.parse_funding_interval(millisecondsInterval), + } + + def parse_funding_interval(self, interval): + intervals: dict = { + '3600000': '1h', + '14400000': '4h', + '28800000': '8h', + '57600000': '16h', + '86400000': '24h', + } + return self.safe_string(intervals, interval, interval) + + async def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate + + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#query-funding-rate + https://huobiapi.github.io/docs/usdt_swap/v1/en/#general-query-funding-rate + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'contract_code': market['id'], + } + response = None + if market['inverse']: + response = await self.contractPublicGetSwapApiV1SwapFundingRate(self.extend(request, params)) + elif market['linear']: + response = await self.contractPublicGetLinearSwapApiV1SwapFundingRate(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchFundingRate() supports inverse and linear swaps only') + # + # { + # "status": "ok", + # "data": { + # "estimated_rate": "0.000100000000000000", + # "funding_rate": "0.000100000000000000", + # "contract_code": "BTC-USDT", + # "symbol": "BTC", + # "fee_asset": "USDT", + # "funding_time": "1603699200000", + # "next_funding_time": "1603728000000" + # }, + # "ts": 1603696494714 + # } + # + result = self.safe_value(response, 'data', {}) + return self.parse_funding_rate(result, market) + + async def fetch_funding_rates(self, symbols: Strings = None, params={}) -> FundingRates: + """ + fetch the funding rate for multiple markets + + https://huobiapi.github.io/docs/usdt_swap/v1/en/#general-query-a-batch-of-funding-rate + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#query-a-batch-of-funding-rate + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `funding rate structures `, indexed by market symbols + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + defaultSubType = self.safe_string(self.options, 'defaultSubType', 'linear') + subType = None + subType, params = self.handle_option_and_params(params, 'fetchFundingRates', 'subType', defaultSubType) + if symbols is not None: + firstSymbol = self.safe_string(symbols, 0) + market = self.market(firstSymbol) + isLinear = market['linear'] + subType = 'linear' if isLinear else 'inverse' + request: dict = { + # 'contract_code': market['id'], + } + response = None + if subType == 'linear': + response = await self.contractPublicGetLinearSwapApiV1SwapBatchFundingRate(self.extend(request, params)) + elif subType == 'inverse': + response = await self.contractPublicGetSwapApiV1SwapBatchFundingRate(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchFundingRates() not support self market type') + # + # { + # "status": "ok", + # "data": [ + # { + # "estimated_rate": "0.000100000000000000", + # "funding_rate": "0.000100000000000000", + # "contract_code": "MANA-USDT", + # "symbol": "MANA", + # "fee_asset": "USDT", + # "funding_time": "1643356800000", + # "next_funding_time": "1643385600000", + # "trade_partition":"USDT" + # }, + # ], + # "ts": 1643346173103 + # } + # + data = self.safe_value(response, 'data', []) + return self.parse_funding_rates(data, symbols) + + async def fetch_borrow_interest(self, code: Str = None, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[BorrowInterest]: + """ + fetch the interest owed by the user for borrowing currency for margin trading + + https://huobiapi.github.io/docs/spot/v1/en/#search-past-margin-orders-cross + https://huobiapi.github.io/docs/spot/v1/en/#search-past-margin-orders-isolated + + :param str code: unified currency code + :param str symbol: unified market symbol when fetch interest in isolated markets + :param int [since]: the earliest time in ms to fetch borrrow interest for + :param int [limit]: the maximum number of structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `borrow interest structures ` + """ + await self.load_markets() + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchBorrowInterest', params) + marginMode = 'cross' if (marginMode is None) else marginMode + request: dict = {} + if since is not None: + request['start-date'] = self.yyyymmdd(since) + if limit is not None: + request['size'] = limit + market = None + response = None + if marginMode == 'isolated': + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + response = await self.privateGetMarginLoanOrders(self.extend(request, params)) + else: # Cross + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + response = await self.privateGetCrossMarginLoanOrders(self.extend(request, params)) + # + # { + # "status":"ok", + # "data":[ + # { + # "loan-balance":"0.100000000000000000", + # "interest-balance":"0.000200000000000000", + # "loan-amount":"0.100000000000000000", + # "accrued-at":1511169724531, + # "interest-amount":"0.000200000000000000", + # "filled-points":"0.2", + # "filled-ht":"0.2", + # "currency":"btc", + # "id":394, + # "state":"accrual", + # "account-id":17747, + # "user-id":119913, + # "created-at":1511169724531 + # } + # ] + # } + # + data = self.safe_value(response, 'data') + interest = self.parse_borrow_interests(data, market) + return self.filter_by_currency_since_limit(interest, code, since, limit) + + def parse_borrow_interest(self, info: dict, market: Market = None) -> BorrowInterest: + # isolated + # { + # "interest-rate":"0.000040830000000000", + # "user-id":35930539, + # "account-id":48916071, + # "updated-at":1649320794195, + # "deduct-rate":"1", + # "day-interest-rate":"0.000980000000000000", + # "hour-interest-rate":"0.000040830000000000", + # "loan-balance":"100.790000000000000000", + # "interest-balance":"0.004115260000000000", + # "loan-amount":"100.790000000000000000", + # "paid-coin":"0.000000000000000000", + # "accrued-at":1649320794148, + # "created-at":1649320794148, + # "interest-amount":"0.004115260000000000", + # "deduct-amount":"0", + # "deduct-currency":"", + # "paid-point":"0.000000000000000000", + # "currency":"usdt", + # "symbol":"ltcusdt", + # "id":20242721, + # } + # + # cross + # { + # "id":3416576, + # "user-id":35930539, + # "account-id":48956839, + # "currency":"usdt", + # "loan-amount":"102", + # "loan-balance":"102", + # "interest-amount":"0.00416466", + # "interest-balance":"0.00416466", + # "created-at":1649322735333, + # "accrued-at":1649322735382, + # "state":"accrual", + # "filled-points":"0", + # "filled-ht":"0" + # } + # + marketId = self.safe_string(info, 'symbol') + marginMode = 'cross' if (marketId is None) else 'isolated' + market = self.safe_market(marketId) + symbol = self.safe_string(market, 'symbol') + timestamp = self.safe_integer(info, 'accrued-at') + return { + 'info': info, + 'symbol': symbol, + 'currency': self.safe_currency_code(self.safe_string(info, 'currency')), + 'interest': self.safe_number(info, 'interest-amount'), + 'interestRate': self.safe_number(info, 'interest-rate'), + 'amountBorrowed': self.safe_number(info, 'loan-amount'), + 'marginMode': marginMode, + 'timestamp': timestamp, # Interest accrued time + 'datetime': self.iso8601(timestamp), + } + + def nonce(self): + return self.milliseconds() - self.options['timeDifference'] + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = '/' + query = self.omit(params, self.extract_params(path)) + if isinstance(api, str): + # signing implementation for the old endpoints + if (api == 'public') or (api == 'private'): + url += self.version + elif (api == 'v2Public') or (api == 'v2Private'): + url += 'v2' + url += '/' + self.implode_params(path, params) + if api == 'private' or api == 'v2Private': + self.check_required_credentials() + timestamp = self.ymdhms(self.nonce(), 'T') + request: dict = { + 'SignatureMethod': 'HmacSHA256', + 'SignatureVersion': '2', + 'AccessKeyId': self.apiKey, + 'Timestamp': timestamp, + } + if method != 'POST': + request = self.extend(request, query) + sortedRequest = self.keysort(request) + auth = self.urlencode(sortedRequest, True) # True is a go only requirment + # unfortunately, PHP demands double quotes for the escaped newline symbol + payload = "\n".join([method, self.hostname, url, auth]) # eslint-disable-line quotes + signature = self.hmac(self.encode(payload), self.encode(self.secret), hashlib.sha256, 'base64') + auth += '&' + self.urlencode({'Signature': signature}) + url += '?' + auth + if method == 'POST': + body = self.json(query) + headers = { + 'Content-Type': 'application/json', + } + else: + headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + } + else: + if query: + url += '?' + self.urlencode(query) + url = self.implode_params(self.urls['api'][api], { + 'hostname': self.hostname, + }) + url + else: + # signing implementation for the new endpoints + # type, access = api + type = self.safe_string(api, 0) + access = self.safe_string(api, 1) + levelOneNestedPath = self.safe_string(api, 2) + levelTwoNestedPath = self.safe_string(api, 3) + hostname = None + hostnames = self.safe_value(self.urls['hostnames'], type) + if not isinstance(hostnames, str): + hostnames = self.safe_value(hostnames, levelOneNestedPath) + if (not isinstance(hostnames, str)) and (levelTwoNestedPath is not None): + hostnames = self.safe_value(hostnames, levelTwoNestedPath) + hostname = hostnames + url += self.implode_params(path, params) + if access == 'public': + if query: + url += '?' + self.urlencode(query) + elif access == 'private': + self.check_required_credentials() + if method == 'POST': + options = self.safe_value(self.options, 'broker', {}) + id = self.safe_string(options, 'id', 'AA03022abc') + if path.find('cancel') == -1 and path.endswith('order'): + # swap order placement + channelCode = self.safe_string(params, 'channel_code') + if channelCode is None: + params['channel_code'] = id + elif path.endswith('orders/place'): + # spot order placement + clientOrderId = self.safe_string(params, 'client-order-id') + if clientOrderId is None: + params['client-order-id'] = id + self.uuid() + timestamp = self.ymdhms(self.nonce(), 'T') + request: dict = { + 'SignatureMethod': 'HmacSHA256', + 'SignatureVersion': '2', + 'AccessKeyId': self.apiKey, + 'Timestamp': timestamp, + } + # sorting needs such flow exactly, before urlencoding(more at: https://github.com/ccxt/ccxt/issues/24930 ) + request = self.keysort(request) + if method != 'POST': + sortedQuery = self.keysort(query) + request = self.extend(request, sortedQuery) + auth = self.urlencode(request, True).replace('%2c', '%2C') # in c# it manually needs to be uppercased + # unfortunately, PHP demands double quotes for the escaped newline symbol + payload = "\n".join([method, hostname, url, auth]) # eslint-disable-line quotes + signature = self.hmac(self.encode(payload), self.encode(self.secret), hashlib.sha256, 'base64') + auth += '&' + self.urlencode({'Signature': signature}) + url += '?' + auth + if method == 'POST': + body = self.json(query) + if len(body) == 2: + body = '{}' + headers = { + 'Content-Type': 'application/json', + } + else: + headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + } + url = self.implode_params(self.urls['api'][type], { + 'hostname': hostname, + }) + url + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None # fallback to default error handler + if 'status' in response: + # + # {"status":"error","err-code":"order-limitorder-amount-min-error","err-msg":"limit order amount error, min: `0.001`","data":null} + # {"status":"ok","data":{"errors":[{"order_id":"1349442392365359104","err_code":1061,"err_msg":"The order does not exist."}],"successes":""},"ts":1741773744526} + # + status = self.safe_string(response, 'status') + if status == 'error': + code = self.safe_string_2(response, 'err-code', 'err_code') + feedback = self.id + ' ' + body + self.throw_broadly_matched_exception(self.exceptions['broad'], body, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], code, feedback) + message = self.safe_string_2(response, 'err-msg', 'err_msg') + self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) + raise ExchangeError(feedback) + if 'code' in response: + # {code: '1003', message: 'invalid signature'} + feedback = self.id + ' ' + body + code = self.safe_string(response, 'code') + self.throw_exactly_matched_exception(self.exceptions['exact'], code, feedback) + data = self.safe_dict(response, 'data') + errorsList = self.safe_list(data, 'errors') + if errorsList is not None: + first = self.safe_dict(errorsList, 0) + errcode = self.safe_string(first, 'err_code') + errmessage = self.safe_string(first, 'err_msg') + feedBack = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], errcode, feedBack) + self.throw_exactly_matched_exception(self.exceptions['exact'], errmessage, feedBack) + return None + + async def fetch_funding_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch the history of funding payments paid and received on self account + + https://huobiapi.github.io/docs/usdt_swap/v1/en/#general-query-account-financial-records-via-multiple-fields-new # linear swaps + https://huobiapi.github.io/docs/dm/v1/en/#query-financial-records-via-multiple-fields-new # coin-m futures + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#query-financial-records-via-multiple-fields-new # coin-m swaps + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch funding history for + :param int [limit]: the maximum number of funding history structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding history structure ` + """ + await self.load_markets() + market = self.market(symbol) + marketType, query = self.handle_market_type_and_params('fetchFundingHistory', market, params) + request: dict = { + 'type': '30,31', + } + if since is not None: + request['start_date'] = since + response = None + if marketType == 'swap': + request['contract'] = market['id'] + if market['linear']: + # + # { + # "status": "ok", + # "data": { + # "financial_record": [ + # { + # "id": "1320088022", + # "type": "30", + # "amount": "0.004732510000000000", + # "ts": "1641168019321", + # "contract_code": "BTC-USDT", + # "asset": "USDT", + # "margin_account": "BTC-USDT", + # "face_margin_account": '' + # }, + # ], + # "remain_size": "0", + # "next_id": null + # }, + # "ts": "1641189898425" + # } + # + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchFundingHistory', params) + marginMode = 'cross' if (marginMode is None) else marginMode + if marginMode == 'isolated': + request['mar_acct'] = market['id'] + else: + request['mar_acct'] = market['quoteId'] + response = await self.contractPrivatePostLinearSwapApiV3SwapFinancialRecordExact(self.extend(request, query)) + else: + # + # { + # "code": 200, + # "msg": "", + # "data": [ + # { + # "query_id": 138798248, + # "id": 117840, + # "type": 5, + # "amount": -0.024464850000000000, + # "ts": 1638758435635, + # "contract_code": "BTC-USDT-211210", + # "asset": "USDT", + # "margin_account": "USDT", + # "face_margin_account": "" + # } + # ], + # "ts": 1604312615051 + # } + # + response = await self.contractPrivatePostSwapApiV3SwapFinancialRecordExact(self.extend(request, query)) + else: + request['symbol'] = market['id'] + response = await self.contractPrivatePostApiV3ContractFinancialRecordExact(self.extend(request, query)) + data = self.safe_list(response, 'data', []) + return self.parse_incomes(data, market, since, limit) + + async def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://huobiapi.github.io/docs/usdt_swap/v1/en/#isolated-switch-leverage + https://huobiapi.github.io/docs/usdt_swap/v1/en/#cross-switch-leverage + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#switch-leverage + https://huobiapi.github.io/docs/dm/v1/en/#switch-leverage # Coin-m futures + + :param float leverage: the rate of leverage + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setLeverage() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + marketType, query = self.handle_market_type_and_params('setLeverage', market, params) + request: dict = { + 'lever_rate': leverage, + } + if marketType == 'future' and market['inverse']: + request['symbol'] = market['settleId'] + else: + request['contract_code'] = market['id'] + response = None + if market['linear']: + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('setLeverage', params) + marginMode = 'cross' if (marginMode is None) else marginMode + if marginMode == 'isolated': + response = await self.contractPrivatePostLinearSwapApiV1SwapSwitchLeverRate(self.extend(request, query)) + elif marginMode == 'cross': + response = await self.contractPrivatePostLinearSwapApiV1SwapCrossSwitchLeverRate(self.extend(request, query)) + else: + raise NotSupported(self.id + ' setLeverage() not support self market type') + # + # { + # "status": "ok", + # "data": { + # "contract_code": "BTC-USDT", + # "lever_rate": "100", + # "margin_mode": "isolated" + # }, + # "ts": "1641184710649" + # } + # + else: + if marketType == 'future': + response = await self.contractPrivatePostApiV1ContractSwitchLeverRate(self.extend(request, query)) + elif marketType == 'swap': + response = await self.contractPrivatePostSwapApiV1SwapSwitchLeverRate(self.extend(request, query)) + else: + raise NotSupported(self.id + ' setLeverage() not support self market type') + # + # future + # { + # "status": "ok", + # "data": {symbol: "BTC", lever_rate: 5}, + # "ts": 1641184578678 + # } + # + # swap + # + # { + # "status": "ok", + # "data": {contract_code: "BTC-USD", lever_rate: "5"}, + # "ts": "1641184652979" + # } + # + return response + + def parse_income(self, income, market: Market = None): + # + # { + # "id": "1667161118", + # "symbol": "BTC", + # "type": "31", + # "amount": "-2.11306593188E-7", + # "ts": "1641139308983", + # "contract_code": "BTC-USD" + # } + # + marketId = self.safe_string(income, 'contract_code') + symbol = self.safe_symbol(marketId, market) + amount = self.safe_number(income, 'amount') + timestamp = self.safe_integer(income, 'ts') + id = self.safe_string(income, 'id') + currencyId = self.safe_string_2(income, 'symbol', 'asset') + code = self.safe_currency_code(currencyId) + return { + 'info': income, + 'symbol': symbol, + 'code': code, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': id, + 'amount': amount, + } + + def parse_position(self, position: dict, market: Market = None): + # + # { + # "symbol": "BTC", + # "contract_code": "BTC-USDT", + # "volume": "1.000000000000000000", + # "available": "1.000000000000000000", + # "frozen": "0E-18", + # "cost_open": "47162.000000000000000000", + # "cost_hold": "47151.300000000000000000", + # "profit_unreal": "0.007300000000000000", + # "profit_rate": "-0.000144183876850008", + # "lever_rate": "2", + # "position_margin": "23.579300000000000000", + # "direction": "buy", + # "profit": "-0.003400000000000000", + # "last_price": "47158.6", + # "margin_asset": "USDT", + # "margin_mode": "isolated", + # "margin_account": "BTC-USDT", + # "margin_balance": "24.973020070000000000", + # "margin_position": "23.579300000000000000", + # "margin_frozen": "0", + # "margin_available": "1.393720070000000000", + # "profit_real": "0E-18", + # "risk_rate": "1.044107779705080303", + # "withdraw_available": "1.386420070000000000000000000000000000", + # "liquidation_price": "22353.229148614609571788", + # "adjust_factor": "0.015000000000000000", + # "margin_static": "24.965720070000000000" + # } + # + market = self.safe_market(self.safe_string(position, 'contract_code')) + symbol = market['symbol'] + contracts = self.safe_string(position, 'volume') + contractSize = self.safe_value(market, 'contractSize') + contractSizeString = self.number_to_string(contractSize) + entryPrice = self.safe_number(position, 'cost_open') + initialMargin = self.safe_string(position, 'position_margin') + rawSide = self.safe_string(position, 'direction') + side = 'long' if (rawSide == 'buy') else 'short' + unrealizedProfit = self.safe_number(position, 'profit_unreal') + marginMode = self.safe_string(position, 'margin_mode') + leverage = self.safe_string(position, 'lever_rate') + percentage = Precise.string_mul(self.safe_string(position, 'profit_rate'), '100') + lastPrice = self.safe_string(position, 'last_price') + faceValue = Precise.string_mul(contracts, contractSizeString) + notional = None + if market['linear']: + notional = Precise.string_mul(faceValue, lastPrice) + else: + notional = Precise.string_div(faceValue, lastPrice) + marginMode = 'cross' + intialMarginPercentage = Precise.string_div(initialMargin, notional) + collateral = self.safe_string(position, 'margin_balance') + liquidationPrice = self.safe_number(position, 'liquidation_price') + adjustmentFactor = self.safe_string(position, 'adjust_factor') + maintenanceMarginPercentage = Precise.string_div(adjustmentFactor, leverage) + maintenanceMargin = Precise.string_mul(maintenanceMarginPercentage, notional) + marginRatio = Precise.string_div(maintenanceMargin, collateral) + return self.safe_position({ + 'info': position, + 'id': None, + 'symbol': symbol, + 'contracts': self.parse_number(contracts), + 'contractSize': contractSize, + 'entryPrice': entryPrice, + 'collateral': self.parse_number(collateral), + 'side': side, + 'unrealizedPnl': unrealizedProfit, + 'leverage': self.parse_number(leverage), + 'percentage': self.parse_number(percentage), + 'marginMode': marginMode, + 'notional': self.parse_number(notional), + 'markPrice': None, + 'lastPrice': None, + 'liquidationPrice': liquidationPrice, + 'initialMargin': self.parse_number(initialMargin), + 'initialMarginPercentage': self.parse_number(intialMarginPercentage), + 'maintenanceMargin': self.parse_number(maintenanceMargin), + 'maintenanceMarginPercentage': self.parse_number(maintenanceMarginPercentage), + 'marginRatio': self.parse_number(marginRatio), + 'timestamp': None, + 'datetime': None, + 'hedged': None, + 'lastUpdateTimestamp': None, + 'stopLossPrice': None, + 'takeProfitPrice': None, + }) + + async def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://huobiapi.github.io/docs/usdt_swap/v1/en/#cross-query-user-39-s-position-information + https://huobiapi.github.io/docs/usdt_swap/v1/en/#isolated-query-user-s-position-information + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#query-user-s-position-information + https://huobiapi.github.io/docs/dm/v1/en/#query-user-s-position-information + + :param str[] [symbols]: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.subType]: 'linear' or 'inverse' + :param str [params.type]: *inverse only* 'future', or 'swap' + :param str [params.marginMode]: *linear only* 'cross' or 'isolated' + :returns dict[]: a list of `position structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + market = None + if symbols is not None: + symbolsLength = len(symbols) + if symbolsLength > 0: + first = self.safe_string(symbols, 0) + market = self.market(first) + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchPositions', params, 'cross') + subType = None + subType, params = self.handle_sub_type_and_params('fetchPositions', market, params, 'linear') + marketType = None + marketType, params = self.handle_market_type_and_params('fetchPositions', market, params) + if marketType == 'spot': + marketType = 'future' + response = None + if subType == 'linear': + if marginMode == 'isolated': + response = await self.contractPrivatePostLinearSwapApiV1SwapPositionInfo(params) + elif marginMode == 'cross': + response = await self.contractPrivatePostLinearSwapApiV1SwapCrossPositionInfo(params) + else: + raise NotSupported(self.id + ' fetchPositions() not support self market type') + # + # { + # "status": "ok", + # "data": [ + # { + # "symbol": "BTC", + # "contract_code": "BTC-USDT", + # "volume": "1.000000000000000000", + # "available": "1.000000000000000000", + # "frozen": "0E-18", + # "cost_open": "47162.000000000000000000", + # "cost_hold": "47162.000000000000000000", + # "profit_unreal": "0.047300000000000000", + # "profit_rate": "0.002005852169119206", + # "lever_rate": "2", + # "position_margin": "23.604650000000000000", + # "direction": "buy", + # "profit": "0.047300000000000000", + # "last_price": "47209.3", + # "margin_asset": "USDT", + # "margin_mode": "isolated", + # "margin_account": "BTC-USDT" + # } + # ], + # "ts": "1641108676768" + # } + # + else: + if marketType == 'future': + response = await self.contractPrivatePostApiV1ContractPositionInfo(params) + elif marketType == 'swap': + response = await self.contractPrivatePostSwapApiV1SwapPositionInfo(params) + else: + raise NotSupported(self.id + ' fetchPositions() not support self market type') + # + # future + # { + # "status": "ok", + # "data": [ + # { + # "symbol": "BTC", + # "contract_code": "BTC220624", + # "contract_type": "next_quarter", + # "volume": "1.000000000000000000", + # "available": "1.000000000000000000", + # "frozen": "0E-18", + # "cost_open": "49018.880000000009853343", + # "cost_hold": "49018.880000000009853343", + # "profit_unreal": "-8.62360608500000000000000000000000000000000000000E-7", + # "profit_rate": "-0.000845439023678622", + # "lever_rate": "2", + # "position_margin": "0.001019583964880634", + # "direction": "sell", + # "profit": "-8.62360608500000000000000000000000000000000000000E-7", + # "last_price": "49039.61" + # } + # ], + # "ts": "1641109895199" + # } + # + # swap + # { + # "status": "ok", + # "data": [ + # { + # "symbol": "BTC", + # "contract_code": "BTC-USD", + # "volume": "1.000000000000000000", + # "available": "1.000000000000000000", + # "frozen": "0E-18", + # "cost_open": "47150.000000000012353300", + # "cost_hold": "47150.000000000012353300", + # "profit_unreal": "0E-54", + # "profit_rate": "-7.86E-16", + # "lever_rate": "3", + # "position_margin": "0.000706963591375044", + # "direction": "buy", + # "profit": "0E-54", + # "last_price": "47150" + # } + # ], + # "ts": "1641109636572" + # } + # + data = self.safe_value(response, 'data', []) + timestamp = self.safe_integer(response, 'ts') + result = [] + for i in range(0, len(data)): + position = data[i] + parsed = self.parse_position(position) + result.append(self.extend(parsed, { + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + })) + return self.filter_by_array_positions(result, 'symbol', symbols, False) + + async def fetch_position(self, symbol: str, params={}): + """ + fetch data on a single open contract trade position + + https://huobiapi.github.io/docs/usdt_swap/v1/en/#cross-query-assets-and-positions + https://huobiapi.github.io/docs/usdt_swap/v1/en/#isolated-query-assets-and-positions + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#query-assets-and-positions + https://huobiapi.github.io/docs/dm/v1/en/#query-assets-and-positions + + :param str symbol: unified market symbol of the market the position is held in, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `position structure ` + """ + await self.load_markets() + market = self.market(symbol) + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchPosition', params) + marginMode = 'cross' if (marginMode is None) else marginMode + marketType, query = self.handle_market_type_and_params('fetchPosition', market, params) + request: dict = {} + if market['future'] and market['inverse']: + request['symbol'] = market['settleId'] + else: + if marginMode == 'cross': + request['margin_account'] = 'USDT' # only allowed value + request['contract_code'] = market['id'] + response = None + if market['linear']: + if marginMode == 'isolated': + response = await self.contractPrivatePostLinearSwapApiV1SwapAccountPositionInfo(self.extend(request, query)) + elif marginMode == 'cross': + response = await self.contractPrivatePostLinearSwapApiV1SwapCrossAccountPositionInfo(self.extend(request, query)) + else: + raise NotSupported(self.id + ' fetchPosition() not support self market type') + # + # isolated + # + # { + # "status": "ok", + # "data": [ + # { + # "positions": [], + # "symbol": "BTC", + # "margin_balance": 1.949728350000000000, + # "margin_position": 0, + # "margin_frozen": 0E-18, + # "margin_available": 1.949728350000000000, + # "profit_real": -0.050271650000000000, + # "profit_unreal": 0, + # "risk_rate": null, + # "withdraw_available": 1.949728350000000000, + # "liquidation_price": null, + # "lever_rate": 20, + # "adjust_factor": 0.150000000000000000, + # "margin_static": 1.949728350000000000, + # "contract_code": "BTC-USDT", + # "margin_asset": "USDT", + # "margin_mode": "isolated", + # "margin_account": "BTC-USDT", + # "trade_partition": "USDT", + # "position_mode": "dual_side" + # }, + # ... opposite side position can be present here too(if hedge) + # ], + # "ts": 1653605008286 + # } + # + # cross + # + # { + # "status": "ok", + # "data": { + # "positions": [ + # { + # "symbol": "BTC", + # "contract_code": "BTC-USDT", + # "volume": "1.000000000000000000", + # "available": "1.000000000000000000", + # "frozen": "0E-18", + # "cost_open": "29530.000000000000000000", + # "cost_hold": "29530.000000000000000000", + # "profit_unreal": "-0.010000000000000000", + # "profit_rate": "-0.016931933626820200", + # "lever_rate": "50", + # "position_margin": "0.590400000000000000", + # "direction": "buy", + # "profit": "-0.010000000000000000", + # "last_price": "29520", + # "margin_asset": "USDT", + # "margin_mode": "cross", + # "margin_account": "USDT", + # "contract_type": "swap", + # "pair": "BTC-USDT", + # "business_type": "swap", + # "trade_partition": "USDT", + # "position_mode": "dual_side" + # }, + # ... opposite side position can be present here too(if hedge) + # ], + # "futures_contract_detail": [ + # { + # "symbol": "BTC", + # "contract_code": "BTC-USDT-220624", + # "margin_position": "0", + # "margin_frozen": "0E-18", + # "margin_available": "1.497799766913531118", + # "profit_unreal": "0", + # "liquidation_price": null, + # "lever_rate": "30", + # "adjust_factor": "0.250000000000000000", + # "contract_type": "quarter", + # "pair": "BTC-USDT", + # "business_type": "futures", + # "trade_partition": "USDT" + # }, + # ... other items listed with different expiration(contract_code) + # ], + # "margin_mode": "cross", + # "margin_account": "USDT", + # "margin_asset": "USDT", + # "margin_balance": "2.088199766913531118", + # "margin_static": "2.098199766913531118", + # "margin_position": "0.590400000000000000", + # "margin_frozen": "0E-18", + # "profit_real": "-0.016972710000000000", + # "profit_unreal": "-0.010000000000000000", + # "withdraw_available": "1.497799766913531118", + # "risk_rate": "9.105496355562965147", + # "contract_detail": [ + # { + # "symbol": "BTC", + # "contract_code": "BTC-USDT", + # "margin_position": "0.590400000000000000", + # "margin_frozen": "0E-18", + # "margin_available": "1.497799766913531118", + # "profit_unreal": "-0.010000000000000000", + # "liquidation_price": "27625.176468365024050352", + # "lever_rate": "50", + # "adjust_factor": "0.350000000000000000", + # "contract_type": "swap", + # "pair": "BTC-USDT", + # "business_type": "swap", + # "trade_partition": "USDT" + # }, + # ... all symbols listed + # ], + # "position_mode": "dual_side" + # }, + # "ts": "1653604697466" + # } + # + else: + if marketType == 'future': + response = await self.contractPrivatePostApiV1ContractAccountPositionInfo(self.extend(request, query)) + elif marketType == 'swap': + response = await self.contractPrivatePostSwapApiV1SwapAccountPositionInfo(self.extend(request, query)) + else: + raise NotSupported(self.id + ' setLeverage() not support self market type') + # + # future, swap + # + # { + # "status": "ok", + # "data": [ + # { + # "symbol": "XRP", + # "contract_code": "XRP-USD", # only present in swap + # "margin_balance": 12.186361450698276582, + # "margin_position": 5.036261079774375503, + # "margin_frozen": 0E-18, + # "margin_available": 7.150100370923901079, + # "profit_real": -0.012672343876723438, + # "profit_unreal": 0.163382354575000020, + # "risk_rate": 2.344723929650649798, + # "withdraw_available": 6.986718016348901059, + # "liquidation_price": 0.271625200493799547, + # "lever_rate": 5, + # "adjust_factor": 0.075000000000000000, + # "margin_static": 12.022979096123276562, + # "positions": [ + # { + # "symbol": "XRP", + # "contract_code": "XRP-USD", + # # "contract_type": "self_week", # only present in future + # "volume": 1.0, + # "available": 1.0, + # "frozen": 0E-18, + # "cost_open": 0.394560000000000000, + # "cost_hold": 0.394560000000000000, + # "profit_unreal": 0.163382354575000020, + # "profit_rate": 0.032232070910556005, + # "lever_rate": 5, + # "position_margin": 5.036261079774375503, + # "direction": "buy", + # "profit": 0.163382354575000020, + # "last_price": 0.39712 + # }, + # ... opposite side position can be present here too(if hedge) + # ] + # } + # ], + # "ts": 1653600470199 + # } + # + # cross usdt swap + # + # { + # "status":"ok", + # "data":{ + # "positions":[], + # "futures_contract_detail":[] + # "margin_mode":"cross", + # "margin_account":"USDT", + # "margin_asset":"USDT", + # "margin_balance":"1.000000000000000000", + # "margin_static":"1.000000000000000000", + # "margin_position":"0", + # "margin_frozen":"1.000000000000000000", + # "profit_real":"0E-18", + # "profit_unreal":"0", + # "withdraw_available":"0", + # "risk_rate":"15.666666666666666666", + # "contract_detail":[] + # }, + # "ts":"1645521118946" + # } + # + data = self.safe_value(response, 'data') + account = None + if marginMode == 'cross': + account = data + else: + account = self.safe_value(data, 0) + omitted = self.omit(account, ['positions']) + positions = self.safe_value(account, 'positions') + position = None + if market['future'] and market['inverse']: + for i in range(0, len(positions)): + entry = positions[i] + if entry['contract_code'] == market['id']: + position = entry + break + else: + position = self.safe_value(positions, 0) + timestamp = self.safe_integer(response, 'ts') + parsed = self.parse_position(self.extend(position, omitted)) + parsed['timestamp'] = timestamp + parsed['datetime'] = self.iso8601(timestamp) + return parsed + + def parse_ledger_entry_type(self, type): + types: dict = { + 'trade': 'trade', + 'etf': 'trade', + 'transact-fee': 'fee', + 'fee-deduction': 'fee', + 'transfer': 'transfer', + 'credit': 'credit', + 'liquidation': 'trade', + 'interest': 'credit', + 'deposit': 'deposit', + 'withdraw': 'withdrawal', + 'withdraw-fee': 'fee', + 'exchange': 'exchange', + 'other-types': 'transfer', + 'rebate': 'rebate', + } + return self.safe_string(types, type, type) + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + # + # { + # "accountId": 10000001, + # "currency": "usdt", + # "transactAmt": 10.000000000000000000, + # "transactType": "transfer", + # "transferType": "margin-transfer-out", + # "transactId": 0, + # "transactTime": 1629882331066, + # "transferer": 28483123, + # "transferee": 13496526 + # } + # + currencyId = self.safe_string(item, 'currency') + code = self.safe_currency_code(currencyId, currency) + currency = self.safe_currency(currencyId, currency) + id = self.safe_string(item, 'transactId') + transferType = self.safe_string(item, 'transferType') + timestamp = self.safe_integer(item, 'transactTime') + account = self.safe_string(item, 'accountId') + return self.safe_ledger_entry({ + 'info': item, + 'id': id, + 'direction': self.safe_string(item, 'direction'), + 'account': account, + 'referenceId': id, + 'referenceAccount': account, + 'type': self.parse_ledger_entry_type(transferType), + 'currency': code, + 'amount': self.safe_number(item, 'transactAmt'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'before': None, + 'after': None, + 'status': None, + 'fee': None, + }, currency) + + async def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + + https://huobiapi.github.io/docs/spot/v1/en/#get-account-history + + :param str [code]: unified currency code, default is None + :param int [since]: timestamp in ms of the earliest ledger entry, default is None + :param int [limit]: max number of ledger entries to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch entries for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict: a `ledger structure ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchLedger', 'paginate') + if paginate: + return await self.fetch_paginated_call_dynamic('fetchLedger', code, since, limit, params, 500) + accountId = await self.fetch_account_id_by_type('spot', None, None, params) + request: dict = { + 'accountId': accountId, + # 'currency': code, + # 'transactTypes': 'all', # default all + # 'startTime': 1546272000000, + # 'endTime': 1546272000000, + # 'sort': asc, # asc, desc + # 'limit': 100, # range 1-500 + # 'fromId': 323 # first record hasattr(self, ID) query for pagination + } + currency = None + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit # max 500 + request, params = self.handle_until_option('endTime', request, params) + response = await self.spotPrivateGetV2AccountLedger(self.extend(request, params)) + # + # { + # "code": 200, + # "message": "success", + # "data": [ + # { + # "accountId": 10000001, + # "currency": "usdt", + # "transactAmt": 10.000000000000000000, + # "transactType": "transfer", + # "transferType": "margin-transfer-out", + # "transactId": 0, + # "transactTime": 1629882331066, + # "transferer": 28483123, + # "transferee": 13496526 + # }, + # { + # "accountId": 10000001, + # "currency": "usdt", + # "transactAmt": -10.000000000000000000, + # "transactType": "transfer", + # "transferType": "margin-transfer-in", + # "transactId": 0, + # "transactTime": 1629882096562, + # "transferer": 13496526, + # "transferee": 28483123 + # } + # ], + # "nextId": 1624316679, + # "ok": True + # } + # + data = self.safe_value(response, 'data', []) + return self.parse_ledger(data, currency, since, limit) + + async def fetch_leverage_tiers(self, symbols: Strings = None, params={}) -> LeverageTiers: + """ + retrieve information on the maximum leverage, and maintenance margin for trades of varying trade sizes + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `leverage tiers structures `, indexed by market symbols + """ + await self.load_markets() + response = await self.contractPublicGetLinearSwapApiV1SwapAdjustfactor(params) + # + # { + # "status": "ok", + # "data": [ + # { + # "symbol": "MANA", + # "contract_code": "MANA-USDT", + # "margin_mode": "isolated", + # "trade_partition": "USDT", + # "list": [ + # { + # "lever_rate": 75, + # "ladders": [ + # { + # "ladder": 0, + # "min_size": 0, + # "max_size": 999, + # "adjust_factor": 0.7 + # }, + # ... + # ] + # } + # ... + # ] + # }, + # ... + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_leverage_tiers(data, symbols, 'contract_code') + + def parse_market_leverage_tiers(self, info, market: Market = None) -> List[LeverageTier]: + currencyId = self.safe_string(info, 'trade_partition') + marketId = self.safe_string(info, 'contract_code') + tiers = [] + brackets = self.safe_list(info, 'list', []) + for i in range(0, len(brackets)): + item = brackets[i] + leverage = self.safe_string(item, 'lever_rate') + ladders = self.safe_list(item, 'ladders', []) + for k in range(0, len(ladders)): + bracket = ladders[k] + adjustFactor = self.safe_string(bracket, 'adjust_factor') + tiers.append({ + 'tier': self.safe_integer(bracket, 'ladder'), + 'symbol': self.safe_symbol(marketId, market, None, 'swap'), + 'currency': self.safe_currency_code(currencyId), + 'minNotional': self.safe_number(bracket, 'min_size'), + 'maxNotional': self.safe_number(bracket, 'max_size'), + 'maintenanceMarginRate': self.parse_number(Precise.string_div(adjustFactor, leverage)), + 'maxLeverage': self.parse_number(leverage), + 'info': bracket, + }) + return tiers + + async def fetch_open_interest_history(self, symbol: str, timeframe='1h', since: Int = None, limit: Int = None, params={}): + """ + Retrieves the open interest history of a currency + + https://huobiapi.github.io/docs/dm/v1/en/#query-information-on-open-interest + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#query-information-on-open-interest + https://huobiapi.github.io/docs/usdt_swap/v1/en/#general-query-information-on-open-interest + + :param str symbol: Unified CCXT market symbol + :param str timeframe: '1h', '4h', '12h', or '1d' + :param int [since]: Not used by huobi api, but response parsed by CCXT + :param int [limit]: Default:48,Data Range [1,200] + :param dict [params]: Exchange specific parameters + :param int [params.amount_type]: *required* Open interest unit. 1-cont,2-cryptocurrency + :param int [params.pair]: eg BTC-USDT *Only for USDT-M* + :returns dict: an array of `open interest structures ` + """ + if timeframe != '1h' and timeframe != '4h' and timeframe != '12h' and timeframe != '1d': + raise BadRequest(self.id + ' fetchOpenInterestHistory cannot only use the 1h, 4h, 12h and 1d timeframe') + await self.load_markets() + timeframes: dict = { + '1h': '60min', + '4h': '4hour', + '12h': '12hour', + '1d': '1day', + } + market = self.market(symbol) + amountType = self.safe_integer_2(params, 'amount_type', 'amountType', 2) + request: dict = { + 'period': timeframes[timeframe], + 'amount_type': amountType, + } + if limit is not None: + request['size'] = limit + response = None + if market['future']: + request['contract_type'] = self.safe_string(market['info'], 'contract_type') + request['symbol'] = market['baseId'] # currency code on coin-m futures + # coin-m futures + response = await self.contractPublicGetApiV1ContractHisOpenInterest(self.extend(request, params)) + elif market['linear']: + request['contract_type'] = 'swap' + request['contract_code'] = market['id'] + request['contract_code'] = market['id'] + # USDT-M + response = await self.contractPublicGetLinearSwapApiV1SwapHisOpenInterest(self.extend(request, params)) + else: + request['contract_code'] = market['id'] + # coin-m swaps + response = await self.contractPublicGetSwapApiV1SwapHisOpenInterest(self.extend(request, params)) + # + # contractPublicGetlinearSwapApiV1SwapHisOpenInterest + # { + # "status": "ok", + # "data": { + # "symbol": "BTC", + # "tick": [ + # { + # "volume": "4385.4350000000000000", + # "amount_type": "2", + # "ts": "1648220400000", + # "value": "194059884.1850000000000000" + # }, + # ... + # ], + # "contract_code": "BTC-USDT", + # "business_type": "swap", + # "pair": "BTC-USDT", + # "contract_type": "swap", + # "trade_partition": "USDT" + # }, + # "ts": "1648223733007" + # } + # + # contractPublicGetSwapApiV1SwapHisOpenInterest + # { + # "status": "ok", + # "data": { + # "symbol": "CRV", + # "tick": [ + # { + # "volume": 19174.0000000000000000, + # "amount_type": 1, + # "ts": 1648224000000 + # }, + # ... + # ], + # "contract_code": "CRV-USD" + # }, + # "ts": 1648226554260 + # } + # + # contractPublicGetApiV1ContractHisOpenInterest + # { + # "status": "ok", + # "data": { + # "symbol": "BTC", + # "contract_type": "self_week", + # "tick": [ + # { + # "volume": "48419.0000000000000000", + # "amount_type": 1, + # "ts": 1648224000000 + # }, + # ... + # ] + # }, + # "ts": 1648227062944 + # } + # + data = self.safe_value(response, 'data') + tick = self.safe_list(data, 'tick') + return self.parse_open_interests_history(tick, market, since, limit) + + async def fetch_open_interests(self, symbols: Strings = None, params={}): + """ + Retrieves the open interest for a list of symbols + + https://huobiapi.github.io/docs/dm/v1/en/#get-contract-open-interest-information + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#get-swap-open-interest-information + https://huobiapi.github.io/docs/usdt_swap/v1/en/#general-get-swap-open-interest-information + + :param str[] [symbols]: a list of unified CCXT market symbols + :param dict [params]: exchange specific parameters + :returns dict[]: a list of `open interest structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + market = None + if symbols is not None: + symbolsLength = len(symbols) + if symbolsLength > 0: + first = self.safe_string(symbols, 0) + market = self.market(first) + request: dict = {} + subType = None + subType, params = self.handle_sub_type_and_params('fetchPositions', market, params, 'linear') + marketType = None + marketType, params = self.handle_market_type_and_params('fetchPositions', market, params) + response = None + if marketType == 'future': + response = await self.contractPublicGetApiV1ContractOpenInterest(self.extend(request, params)) + # + # { + # "status": "ok", + # "data": [ + # { + # "volume": 118850.000000000000000000, + # "amount": 635.502025211544374189, + # "symbol": "BTC", + # "contract_type": "self_week", + # "contract_code": "BTC220930", + # "trade_amount": 1470.9400749347598691119206024033947897351, + # "trade_volume": 286286, + # "trade_turnover": 28628600.000000000000000000 + # } + # ], + # "ts": 1664337928805 + # } + # + elif subType == 'inverse': + response = await self.contractPublicGetSwapApiV1SwapOpenInterest(self.extend(request, params)) + # + # { + # "status": "ok", + # "data": [ + # { + # "volume": 518018.000000000000000000, + # "amount": 2769.675777407074725180, + # "symbol": "BTC", + # "contract_code": "BTC-USD", + # "trade_amount": 9544.4032080046491323463688602729806842458, + # "trade_volume": 1848448, + # "trade_turnover": 184844800.000000000000000000 + # } + # ], + # "ts": 1664337226028 + # } + # + else: + request['contract_type'] = 'swap' + response = await self.contractPublicGetLinearSwapApiV1SwapOpenInterest(self.extend(request, params)) + # + # { + # "status": "ok", + # "data": [ + # { + # "volume": 7192610.000000000000000000, + # "amount": 7192.610000000000000000, + # "symbol": "BTC", + # "value": 134654290.332000000000000000, + # "contract_code": "BTC-USDT", + # "trade_amount": 70692.804, + # "trade_volume": 70692804, + # "trade_turnover": 1379302592.9518, + # "business_type": "swap", + # "pair": "BTC-USDT", + # "contract_type": "swap", + # "trade_partition": "USDT" + # } + # ], + # "ts": 1664336503144 + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_open_interests(data, symbols) + + async def fetch_open_interest(self, symbol: str, params={}): + """ + Retrieves the open interest of a currency + + https://huobiapi.github.io/docs/dm/v1/en/#get-contract-open-interest-information + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#get-swap-open-interest-information + https://huobiapi.github.io/docs/usdt_swap/v1/en/#general-get-swap-open-interest-information + + :param str symbol: Unified CCXT market symbol + :param dict [params]: exchange specific parameters + :returns dict} an open interest structure{@link https://docs.ccxt.com/#/?id=open-interest-structure: + """ + await self.load_markets() + market = self.market(symbol) + if not market['contract']: + raise BadRequest(self.id + ' fetchOpenInterest() supports contract markets only') + if market['option']: + raise NotSupported(self.id + ' fetchOpenInterest() does not currently support option markets') + request: dict = { + 'contract_code': market['id'], + } + response = None + if market['future']: + request['contract_type'] = self.safe_string(market['info'], 'contract_type') + request['symbol'] = market['baseId'] + # COIN-M futures + response = await self.contractPublicGetApiV1ContractOpenInterest(self.extend(request, params)) + elif market['linear']: + request['contract_type'] = 'swap' + # USDT-M + response = await self.contractPublicGetLinearSwapApiV1SwapOpenInterest(self.extend(request, params)) + else: + # COIN-M swaps + response = await self.contractPublicGetSwapApiV1SwapOpenInterest(self.extend(request, params)) + # + # USDT-M contractPublicGetLinearSwapApiV1SwapOpenInterest + # + # { + # "status": "ok", + # "data": [ + # { + # "volume": 7192610.000000000000000000, + # "amount": 7192.610000000000000000, + # "symbol": "BTC", + # "value": 134654290.332000000000000000, + # "contract_code": "BTC-USDT", + # "trade_amount": 70692.804, + # "trade_volume": 70692804, + # "trade_turnover": 1379302592.9518, + # "business_type": "swap", + # "pair": "BTC-USDT", + # "contract_type": "swap", + # "trade_partition": "USDT" + # } + # ], + # "ts": 1664336503144 + # } + # + # COIN-M Swap contractPublicGetSwapApiV1SwapOpenInterest + # + # { + # "status": "ok", + # "data": [ + # { + # "volume": 518018.000000000000000000, + # "amount": 2769.675777407074725180, + # "symbol": "BTC", + # "contract_code": "BTC-USD", + # "trade_amount": 9544.4032080046491323463688602729806842458, + # "trade_volume": 1848448, + # "trade_turnover": 184844800.000000000000000000 + # } + # ], + # "ts": 1664337226028 + # } + # + # COIN-M Futures contractPublicGetApiV1ContractOpenInterest + # + # { + # "status": "ok", + # "data": [ + # { + # "volume": 118850.000000000000000000, + # "amount": 635.502025211544374189, + # "symbol": "BTC", + # "contract_type": "self_week", + # "contract_code": "BTC220930", + # "trade_amount": 1470.9400749347598691119206024033947897351, + # "trade_volume": 286286, + # "trade_turnover": 28628600.000000000000000000 + # } + # ], + # "ts": 1664337928805 + # } + # + data = self.safe_value(response, 'data', []) + openInterest = self.parse_open_interest(data[0], market) + timestamp = self.safe_integer(response, 'ts') + openInterest['timestamp'] = timestamp + openInterest['datetime'] = self.iso8601(timestamp) + return openInterest + + def parse_open_interest(self, interest, market: Market = None): + # + # fetchOpenInterestHistory + # + # { + # "volume": "4385.4350000000000000", + # "amount_type": "2", + # "ts": "1648220400000", + # "value": "194059884.1850000000000000" + # } + # + # fetchOpenInterest: USDT-M + # + # { + # "volume": 7192610.000000000000000000, + # "amount": 7192.610000000000000000, + # "symbol": "BTC", + # "value": 134654290.332000000000000000, + # "contract_code": "BTC-USDT", + # "trade_amount": 70692.804, + # "trade_volume": 70692804, + # "trade_turnover": 1379302592.9518, + # "business_type": "swap", + # "pair": "BTC-USDT", + # "contract_type": "swap", + # "trade_partition": "USDT" + # } + # + # fetchOpenInterest: COIN-M Swap + # + # { + # "volume": 518018.000000000000000000, + # "amount": 2769.675777407074725180, + # "symbol": "BTC", + # "contract_code": "BTC-USD", + # "trade_amount": 9544.4032080046491323463688602729806842458, + # "trade_volume": 1848448, + # "trade_turnover": 184844800.000000000000000000 + # } + # + # fetchOpenInterest: COIN-M Futures + # + # { + # "volume": 118850.000000000000000000, + # "amount": 635.502025211544374189, + # "symbol": "BTC", + # "contract_type": "self_week", + # "contract_code": "BTC220930", + # "trade_amount": 1470.9400749347598691119206024033947897351, + # "trade_volume": 286286, + # "trade_turnover": 28628600.000000000000000000 + # } + # + timestamp = self.safe_integer(interest, 'ts') + amount = self.safe_number(interest, 'volume') + value = self.safe_number(interest, 'value') + marketId = self.safe_string(interest, 'contract_code') + return self.safe_open_interest({ + 'symbol': self.safe_symbol(marketId, market), + 'baseVolume': amount, # deprecated + 'quoteVolume': value, # deprecated + 'openInterestAmount': amount, + 'openInterestValue': value, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'info': interest, + }, market) + + async def borrow_isolated_margin(self, symbol: str, code: str, amount: float, params={}): + """ + create a loan to borrow margin + + https://huobiapi.github.io/docs/spot/v1/en/#request-a-margin-loan-isolated + https://huobiapi.github.io/docs/spot/v1/en/#request-a-margin-loan-cross + + :param str symbol: unified market symbol, required for isolated margin + :param str code: unified currency code of the currency to borrow + :param float amount: the amount to borrow + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin loan structure ` + """ + await self.load_markets() + currency = self.currency(code) + market = self.market(symbol) + request: dict = { + 'currency': currency['id'], + 'amount': self.currency_to_precision(code, amount), + 'symbol': market['id'], + } + response = await self.privatePostMarginOrders(self.extend(request, params)) + # + # Isolated + # + # { + # "data": 1000 + # } + # + transaction = self.parse_margin_loan(response, currency) + return self.extend(transaction, { + 'amount': amount, + 'symbol': symbol, + }) + + async def borrow_cross_margin(self, code: str, amount: float, params={}): + """ + create a loan to borrow margin + + https://huobiapi.github.io/docs/spot/v1/en/#request-a-margin-loan-isolated + https://huobiapi.github.io/docs/spot/v1/en/#request-a-margin-loan-cross + + :param str code: unified currency code of the currency to borrow + :param float amount: the amount to borrow + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin loan structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + 'amount': self.currency_to_precision(code, amount), + } + response = await self.privatePostCrossMarginOrders(self.extend(request, params)) + # + # Cross + # + # { + # "status": "ok", + # "data": null + # } + # + transaction = self.parse_margin_loan(response, currency) + return self.extend(transaction, { + 'amount': amount, + }) + + async def repay_isolated_margin(self, symbol: str, code: str, amount, params={}): + """ + repay borrowed margin and interest + + https://huobiapi.github.io/docs/spot/v1/en/#repay-margin-loan-cross-isolated + + :param str symbol: unified market symbol + :param str code: unified currency code of the currency to repay + :param float amount: the amount to repay + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin loan structure ` + """ + await self.load_markets() + currency = self.currency(code) + accountId = await self.fetch_account_id_by_type('spot', 'isolated', symbol, params) + request: dict = { + 'currency': currency['id'], + 'amount': self.currency_to_precision(code, amount), + 'accountId': accountId, + } + response = await self.v2PrivatePostAccountRepayment(self.extend(request, params)) + # + # { + # "code":200, + # "data": [ + # { + # "repayId":1174424, + # "repayTime":1600747722018 + # } + # ] + # } + # + data = self.safe_value(response, 'Data', []) + loan = self.safe_value(data, 0) + transaction = self.parse_margin_loan(loan, currency) + return self.extend(transaction, { + 'amount': amount, + 'symbol': symbol, + }) + + async def repay_cross_margin(self, code: str, amount, params={}): + """ + repay borrowed margin and interest + + https://huobiapi.github.io/docs/spot/v1/en/#repay-margin-loan-cross-isolated + + :param str code: unified currency code of the currency to repay + :param float amount: the amount to repay + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin loan structure ` + """ + await self.load_markets() + currency = self.currency(code) + accountId = await self.fetch_account_id_by_type('spot', 'cross', None, params) + request: dict = { + 'currency': currency['id'], + 'amount': self.currency_to_precision(code, amount), + 'accountId': accountId, + } + response = await self.v2PrivatePostAccountRepayment(self.extend(request, params)) + # + # { + # "code":200, + # "data": [ + # { + # "repayId":1174424, + # "repayTime":1600747722018 + # } + # ] + # } + # + data = self.safe_value(response, 'Data', []) + loan = self.safe_value(data, 0) + transaction = self.parse_margin_loan(loan, currency) + return self.extend(transaction, { + 'amount': amount, + }) + + def parse_margin_loan(self, info, currency: Currency = None): + # + # borrowMargin cross + # + # { + # "status": "ok", + # "data": null + # } + # + # borrowMargin isolated + # + # { + # "data": 1000 + # } + # + # repayMargin + # + # { + # "repayId":1174424, + # "repayTime":1600747722018 + # } + # + timestamp = self.safe_integer(info, 'repayTime') + return { + 'id': self.safe_string_2(info, 'repayId', 'data'), + 'currency': self.safe_currency_code(None, currency), + 'amount': None, + 'symbol': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'info': info, + } + + async def fetch_settlement_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + Fetches historical settlement records + + https://huobiapi.github.io/docs/dm/v1/en/#query-historical-settlement-records-of-the-platform-interface + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#query-historical-settlement-records-of-the-platform-interface + https://huobiapi.github.io/docs/usdt_swap/v1/en/#general-query-historical-settlement-records-of-the-platform-interface + + :param str symbol: unified symbol of the market to fetch the settlement history for + :param int [since]: timestamp in ms, value range = current time - 90 days,default = current time - 90 days + :param int [limit]: page items, default 20, shall not exceed 50 + :param dict [params]: exchange specific params + :param int [params.until]: timestamp in ms, value range = start_time -> current time,default = current time + :param int [params.page_index]: page index, default page 1 if not filled + :param int [params.code]: unified currency code, can be used when symbol is None + :returns dict[]: a list of `settlement history objects ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchSettlementHistory() requires a symbol argument') + until = self.safe_integer(params, 'until') + params = self.omit(params, ['until']) + market = self.market(symbol) + request: dict = {} + if market['future']: + request['symbol'] = market['baseId'] + else: + request['contract_code'] = market['id'] + if since is not None: + request['start_at'] = since + if limit is not None: + request['page_size'] = limit + if until is not None: + request['end_at'] = until + response = None + if market['swap']: + if market['linear']: + response = await self.contractPublicGetLinearSwapApiV1SwapSettlementRecords(self.extend(request, params)) + else: + response = await self.contractPublicGetSwapApiV1SwapSettlementRecords(self.extend(request, params)) + else: + response = await self.contractPublicGetApiV1ContractSettlementRecords(self.extend(request, params)) + # + # linear swap, coin-m swap + # + # { + # "status": "ok", + # "data": { + # "total_page": 14, + # "current_page": 1, + # "total_size": 270, + # "settlement_record": [ + # { + # "symbol": "ADA", + # "contract_code": "ADA-USDT", + # "settlement_time": 1652313600000, + # "clawback_ratio": 0E-18, + # "settlement_price": 0.512303000000000000, + # "settlement_type": "settlement", + # "business_type": "swap", + # "pair": "ADA-USDT", + # "trade_partition": "USDT" + # }, + # ... + # ], + # "ts": 1652338693256 + # } + # + # coin-m future + # + # { + # "status": "ok", + # "data": { + # "total_page": 5, + # "current_page": 1, + # "total_size": 90, + # "settlement_record": [ + # { + # "symbol": "FIL", + # "settlement_time": 1652342400000, + # "clawback_ratio": 0E-18, + # "list": [ + # { + # "contract_code": "FIL220513", + # "settlement_price": 7.016000000000000000, + # "settlement_type": "settlement" + # }, + # ... + # ] + # }, + # ] + # } + # } + # + data = self.safe_value(response, 'data') + settlementRecord = self.safe_value(data, 'settlement_record') + settlements = self.parse_settlements(settlementRecord, market) + return self.sort_by(settlements, 'timestamp') + + async def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}): + """ + fetch deposit and withdraw fees + + https://huobiapi.github.io/docs/spot/v1/en/#get-all-supported-currencies-v2 + + :param str[]|None codes: list of unified currency codes + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `fees structures ` + """ + await self.load_markets() + response = await self.spotPublicGetV2ReferenceCurrencies(params) + # + # { + # "code": 200, + # "data": [ + # { + # "currency": "sxp", + # "assetType": "1", + # "chains": [ + # { + # "chain": "sxp", + # "displayName": "ERC20", + # "baseChain": "ETH", + # "baseChainProtocol": "ERC20", + # "isDynamic": True, + # "numOfConfirmations": "12", + # "numOfFastConfirmations": "12", + # "depositStatus": "allowed", + # "minDepositAmt": "0.23", + # "withdrawStatus": "allowed", + # "minWithdrawAmt": "0.23", + # "withdrawPrecision": "8", + # "maxWithdrawAmt": "227000.000000000000000000", + # "withdrawQuotaPerDay": "227000.000000000000000000", + # "withdrawQuotaPerYear": null, + # "withdrawQuotaTotal": null, + # "withdrawFeeType": "fixed", + # "transactFeeWithdraw": "11.1653", + # "addrWithTag": False, + # "addrDepositTag": False + # } + # ], + # "instStatus": "normal" + # } + # ] + # } + # + data = self.safe_list(response, 'data') + return self.parse_deposit_withdraw_fees(data, codes, 'currency') + + def parse_deposit_withdraw_fee(self, fee, currency: Currency = None): + # + # { + # "currency": "sxp", + # "assetType": "1", + # "chains": [ + # { + # "chain": "sxp", + # "displayName": "ERC20", + # "baseChain": "ETH", + # "baseChainProtocol": "ERC20", + # "isDynamic": True, + # "numOfConfirmations": "12", + # "numOfFastConfirmations": "12", + # "depositStatus": "allowed", + # "minDepositAmt": "0.23", + # "withdrawStatus": "allowed", + # "minWithdrawAmt": "0.23", + # "withdrawPrecision": "8", + # "maxWithdrawAmt": "227000.000000000000000000", + # "withdrawQuotaPerDay": "227000.000000000000000000", + # "withdrawQuotaPerYear": null, + # "withdrawQuotaTotal": null, + # "withdrawFeeType": "fixed", + # "transactFeeWithdraw": "11.1653", + # "addrWithTag": False, + # "addrDepositTag": False + # } + # ], + # "instStatus": "normal" + # } + # + chains = self.safe_value(fee, 'chains', []) + result = self.deposit_withdraw_fee(fee) + for j in range(0, len(chains)): + chainEntry = chains[j] + networkId = self.safe_string(chainEntry, 'chain') + withdrawFeeType = self.safe_string(chainEntry, 'withdrawFeeType') + networkCode = self.network_id_to_code(networkId) + withdrawFee = None + withdrawResult = None + if withdrawFeeType == 'fixed': + withdrawFee = self.safe_number(chainEntry, 'transactFeeWithdraw') + withdrawResult = { + 'fee': withdrawFee, + 'percentage': False, + } + else: + withdrawFee = self.safe_number(chainEntry, 'transactFeeRateWithdraw') + withdrawResult = { + 'fee': withdrawFee, + 'percentage': True, + } + result['networks'][networkCode] = { + 'withdraw': withdrawResult, + 'deposit': { + 'fee': None, + 'percentage': None, + }, + } + result = self.assign_default_deposit_withdraw_fees(result, currency) + return result + + def parse_settlements(self, settlements, market): + # + # linear swap, coin-m swap, fetchSettlementHistory + # + # [ + # { + # "symbol": "ADA", + # "contract_code": "ADA-USDT", + # "settlement_time": 1652313600000, + # "clawback_ratio": 0E-18, + # "settlement_price": 0.512303000000000000, + # "settlement_type": "settlement", + # "business_type": "swap", + # "pair": "ADA-USDT", + # "trade_partition": "USDT" + # }, + # ... + # ] + # + # coin-m future, fetchSettlementHistory + # + # [ + # { + # "symbol": "FIL", + # "settlement_time": 1652342400000, + # "clawback_ratio": 0E-18, + # "list": [ + # { + # "contract_code": "FIL220513", + # "settlement_price": 7.016000000000000000, + # "settlement_type": "settlement" + # }, + # ... + # ] + # }, + # ] + # + result = [] + for i in range(0, len(settlements)): + settlement = settlements[i] + list = self.safe_value(settlement, 'list') + if list is not None: + timestamp = self.safe_integer(settlement, 'settlement_time') + timestampDetails: dict = { + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + for j in range(0, len(list)): + item = list[j] + parsedSettlement = self.parse_settlement(item, market) + result.append(self.extend(parsedSettlement, timestampDetails)) + else: + result.append(self.parse_settlement(settlements[i], market)) + return result + + def parse_settlement(self, settlement, market): + # + # linear swap, coin-m swap, fetchSettlementHistory + # + # { + # "symbol": "ADA", + # "contract_code": "ADA-USDT", + # "settlement_time": 1652313600000, + # "clawback_ratio": 0E-18, + # "settlement_price": 0.512303000000000000, + # "settlement_type": "settlement", + # "business_type": "swap", + # "pair": "ADA-USDT", + # "trade_partition": "USDT" + # } + # + # coin-m future, fetchSettlementHistory + # + # { + # "contract_code": "FIL220513", + # "settlement_price": 7.016000000000000000, + # "settlement_type": "settlement" + # } + # + timestamp = self.safe_integer(settlement, 'settlement_time') + marketId = self.safe_string(settlement, 'contract_code') + return { + 'info': settlement, + 'symbol': self.safe_symbol(marketId, market), + 'price': self.safe_number(settlement, 'settlement_price'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + + async def fetch_liquidations(self, symbol: str, since: Int = None, limit: Int = None, params={}): + """ + retrieves the public liquidations of a trading pair + + https://huobiapi.github.io/docs/usdt_swap/v1/en/#general-query-liquidation-orders-new + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#query-liquidation-orders-new + https://huobiapi.github.io/docs/dm/v1/en/#query-liquidation-order-information-new + + :param str symbol: unified CCXT market symbol + :param int [since]: the earliest time in ms to fetch liquidations for + :param int [limit]: the maximum number of liquidation structures to retrieve + :param dict [params]: exchange specific parameters for the huobi api endpoint + :param int [params.until]: timestamp in ms of the latest liquidation + :param int [params.tradeType]: default 0, linear swap 0: all liquidated orders, 5: liquidated longs; 6: liquidated shorts, inverse swap and future 0: filled liquidated orders, 5: liquidated close orders, 6: liquidated open orders + :returns dict: an array of `liquidation structures ` + """ + await self.load_markets() + market = self.market(symbol) + tradeType = self.safe_integer(params, 'trade_type', 0) + request: dict = { + 'trade_type': tradeType, + } + if since is not None: + request['start_time'] = since + request, params = self.handle_until_option('end_time', request, params) + response = None + if market['swap']: + request['contract'] = market['id'] + if market['linear']: + response = await self.contractPublicGetLinearSwapApiV3SwapLiquidationOrders(self.extend(request, params)) + else: + response = await self.contractPublicGetSwapApiV3SwapLiquidationOrders(self.extend(request, params)) + elif market['future']: + request['symbol'] = market['id'] + response = await self.contractPublicGetApiV3ContractLiquidationOrders(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchLiquidations() does not support ' + market['type'] + ' orders') + # + # { + # "code": 200, + # "msg": "", + # "data": [ + # { + # "query_id": 452057, + # "contract_code": "BTC-USDT-211210", + # "symbol": "USDT", + # "direction": "sell", + # "offset": "close", + # "volume": 479.000000000000000000, + # "price": 51441.700000000000000000, + # "created_at": 1638593647864, + # "amount": 0.479000000000000000, + # "trade_turnover": 24640.574300000000000000, + # "business_type": "futures", + # "pair": "BTC-USDT" + # } + # ], + # "ts": 1604312615051 + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_liquidations(data, market, since, limit) + + def parse_liquidation(self, liquidation, market: Market = None): + # + # { + # "query_id": 452057, + # "contract_code": "BTC-USDT-211210", + # "symbol": "USDT", + # "direction": "sell", + # "offset": "close", + # "volume": 479.000000000000000000, + # "price": 51441.700000000000000000, + # "created_at": 1638593647864, + # "amount": 0.479000000000000000, + # "trade_turnover": 24640.574300000000000000, + # "business_type": "futures", + # "pair": "BTC-USDT" + # } + # + marketId = self.safe_string(liquidation, 'contract_code') + timestamp = self.safe_integer(liquidation, 'created_at') + return self.safe_liquidation({ + 'info': liquidation, + 'symbol': self.safe_symbol(marketId, market), + 'contracts': self.safe_number(liquidation, 'volume'), + 'contractSize': self.safe_number(market, 'contractSize'), + 'price': self.safe_number(liquidation, 'price'), + 'side': self.safe_string_lower(liquidation, 'direction'), + 'baseValue': self.safe_number(liquidation, 'amount'), + 'quoteValue': self.safe_number(liquidation, 'trade_turnover'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + + async def close_position(self, symbol: str, side: OrderSide = None, params={}) -> Order: + """ + closes open positions for a contract market, requires 'amount' in params, unlike other exchanges + + https://huobiapi.github.io/docs/usdt_swap/v1/en/#isolated-place-lightning-close-order # USDT-M(isolated) + https://huobiapi.github.io/docs/usdt_swap/v1/en/#cross-place-lightning-close-position # USDT-M(cross) + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#place-lightning-close-order # Coin-M swap + https://huobiapi.github.io/docs/dm/v1/en/#place-flash-close-order # Coin-M futures + + :param str symbol: unified CCXT market symbol + :param str side: 'buy' or 'sell', the side of the closing order, opposite side side + :param dict [params]: extra parameters specific to the okx api endpoint + :param str [params.clientOrderId]: client needs to provide unique API and have to maintain the API themselves afterwards. [1, 9223372036854775807] + :param dict [params.marginMode]: 'cross' or 'isolated', required for linear markets + + EXCHANGE SPECIFIC PARAMETERS + :param number [params.amount]: order quantity + :param str [params.order_price_type]: 'lightning' by default, 'lightning_fok': lightning fok type, 'lightning_ioc': lightning ioc type 'market' by default, 'market': market order type, 'lightning_fok': lightning + :returns dict: `an order structure ` + """ + await self.load_markets() + market = self.market(symbol) + clientOrderId = self.safe_string(params, 'clientOrderId') + if not market['contract']: + raise BadRequest(self.id + ' closePosition() symbol supports contract markets only') + self.check_required_argument('closePosition', side, 'side') + request: dict = { + 'contract_code': market['id'], + 'direction': side, + } + if clientOrderId is not None: + request['client_order_id'] = clientOrderId + if market['inverse']: + amount = self.safe_string_2(params, 'volume', 'amount') + if amount is None: + raise ArgumentsRequired(self.id + ' closePosition() requires an extra argument params["amount"] for inverse markets') + request['volume'] = self.amount_to_precision(symbol, amount) + params = self.omit(params, ['clientOrderId', 'volume', 'amount']) + response = None + if market['inverse']: # Coin-M + if market['swap']: + response = await self.contractPrivatePostSwapApiV1SwapLightningClosePosition(self.extend(request, params)) + else: # future + response = await self.contractPrivatePostApiV1LightningClosePosition(self.extend(request, params)) + else: # USDT-M + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('closePosition', params, 'cross') + if marginMode == 'cross': + response = await self.contractPrivatePostLinearSwapApiV1SwapCrossLightningClosePosition(self.extend(request, params)) + else: # isolated + response = await self.contractPrivatePostLinearSwapApiV1SwapLightningClosePosition(self.extend(request, params)) + return self.parse_order(response, market) + + async def set_position_mode(self, hedged: bool, symbol: Str = None, params={}): + """ + set hedged to True or False + + https://huobiapi.github.io/docs/usdt_swap/v1/en/#isolated-switch-position-mode + https://huobiapi.github.io/docs/usdt_swap/v1/en/#cross-switch-position-mode + + :param bool hedged: set to True to for hedged mode, must be set separately for each market in isolated margin mode, only valid for linear markets + :param str [symbol]: unified market symbol, required for isolated margin mode + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: "cross"(default) or "isolated" + :returns dict: response from the exchange + """ + await self.load_markets() + posMode = 'dual_side' if hedged else 'single_side' + market = None + if symbol is not None: + market = self.market(symbol) + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('setPositionMode', params, 'cross') + request: dict = { + 'position_mode': posMode, + } + response = None + if (market is not None) and (market['inverse']): + raise BadRequest(self.id + ' setPositionMode can only be used for linear markets') + if marginMode == 'isolated': + if symbol is None: + raise ArgumentsRequired(self.id + ' setPositionMode requires a symbol argument for isolated margin mode') + request['margin_account'] = market['id'] + response = await self.contractPrivatePostLinearSwapApiV1SwapSwitchPositionMode(self.extend(request, params)) + # + # { + # "status": "ok", + # "data": [ + # { + # "margin_account": "BTC-USDT", + # "position_mode": "single_side" + # } + # ], + # "ts": 1566899973811 + # } + # + else: + request['margin_account'] = 'USDT' + response = await self.contractPrivatePostLinearSwapApiV1SwapCrossSwitchPositionMode(self.extend(request, params)) + # + # { + # "status": "ok", + # "data": [ + # { + # "margin_account": "USDT", + # "position_mode": "single_side" + # } + # ], + # "ts": 1566899973811 + # } + # + return response diff --git a/ccxt/async_support/huobi.py b/ccxt/async_support/huobi.py new file mode 100644 index 0000000..f46013e --- /dev/null +++ b/ccxt/async_support/huobi.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.htx import htx +from ccxt.abstract.huobi import ImplicitAPI +from ccxt.base.types import Any + + +class huobi(htx, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(huobi, self).describe(), { + 'id': 'huobi', + 'alias': True, + }) diff --git a/ccxt/async_support/hyperliquid.py b/ccxt/async_support/hyperliquid.py new file mode 100644 index 0000000..d4a4b3f --- /dev/null +++ b/ccxt/async_support/hyperliquid.py @@ -0,0 +1,3885 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.hyperliquid import ImplicitAPI +import asyncio +import math +from ccxt.base.types import Any, Balances, Currencies, Currency, Int, LedgerEntry, MarginModification, Market, Num, Order, OrderBook, OrderRequest, CancellationRequest, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, FundingRates, Trade, TradingFeeInterface, Transaction, MarketInterface, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import NotSupported +from ccxt.base.decimal_to_precision import ROUND +from ccxt.base.decimal_to_precision import DECIMAL_PLACES +from ccxt.base.decimal_to_precision import SIGNIFICANT_DIGITS +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class hyperliquid(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(hyperliquid, self).describe(), { + 'id': 'hyperliquid', + 'name': 'Hyperliquid', + 'countries': [], + 'version': 'v1', + 'rateLimit': 50, # 1200 requests per minute, 20 request per second + 'certified': True, + 'pro': True, + 'dex': True, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': True, + 'future': True, + 'option': False, + 'addMargin': True, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'cancelAllOrders': False, + 'cancelAllOrdersAfter': True, + 'cancelOrder': True, + 'cancelOrders': True, + 'cancelOrdersForSymbols': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createMarketBuyOrderWithCost': False, + 'createMarketOrderWithCost': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createOrders': True, + 'createOrderWithTakeProfitAndStopLoss': True, + 'createReduceOnlyOrder': True, + 'createStopOrder': True, + 'createTriggerOrder': True, + 'editOrder': True, + 'editOrders': True, + 'fetchAccounts': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchCanceledAndClosedOrders': True, + 'fetchCanceledOrders': True, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': False, + 'fetchDepositAddresses': False, + 'fetchDeposits': True, + 'fetchDepositWithdrawFee': 'emulated', + 'fetchDepositWithdrawFees': False, + 'fetchFundingHistory': True, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': True, + 'fetchFundingRates': True, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchLedger': True, + 'fetchLeverage': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchMarginMode': None, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMyLiquidations': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': True, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': True, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchOrderTrades': False, + 'fetchPosition': True, + 'fetchPositionMode': False, + 'fetchPositions': True, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchTicker': 'emulated', + 'fetchTickers': True, + 'fetchTime': False, + 'fetchTrades': True, + 'fetchTradingFee': True, + 'fetchTradingFees': False, + 'fetchTransfer': False, + 'fetchTransfers': False, + 'fetchWithdrawal': False, + 'fetchWithdrawals': True, + 'reduceMargin': True, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'sandbox': True, + 'setLeverage': True, + 'setMarginMode': True, + 'setPositionMode': False, + 'transfer': True, + 'withdraw': True, + }, + 'timeframes': { + '1m': '1m', + '3m': '3m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1h', + '2h': '2h', + '4h': '4h', + '8h': '8h', + '12h': '12h', + '1d': '1d', + '3d': '3d', + '1w': '1w', + '1M': '1M', + }, + 'hostname': 'hyperliquid.xyz', + 'urls': { + 'logo': 'https://github.com/ccxt/ccxt/assets/43336371/b371bc6c-4a8c-489f-87f4-20a913dd8d4b', + 'api': { + 'public': 'https://api.{hostname}', + 'private': 'https://api.{hostname}', + }, + 'test': { + 'public': 'https://api.hyperliquid-testnet.xyz', + 'private': 'https://api.hyperliquid-testnet.xyz', + }, + 'www': 'https://hyperliquid.xyz', + 'doc': 'https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api', + 'fees': 'https://hyperliquid.gitbook.io/hyperliquid-docs/trading/fees', + 'referral': 'https://app.hyperliquid.xyz/', + }, + 'api': { + 'public': { + 'post': { + 'info': { + 'cost': 20, + 'byType': { + 'l2Book': 2, + 'allMids': 2, + 'clearinghouseState': 2, + 'orderStatus': 2, + 'spotClearinghouseState': 2, + 'exchangeStatus': 2, + 'candleSnapshot': 4, + }, + }, + }, + }, + 'private': { + 'post': { + 'exchange': 1, + }, + }, + }, + 'fees': { + 'swap': { + 'taker': self.parse_number('0.00045'), + 'maker': self.parse_number('0.00015'), + }, + 'spot': { + 'taker': self.parse_number('0.0007'), + 'maker': self.parse_number('0.0004'), + }, + }, + 'requiredCredentials': { + 'apiKey': False, + 'secret': False, + 'walletAddress': True, + 'privateKey': True, + }, + 'exceptions': { + 'exact': { + }, + 'broad': { + 'Price must be divisible by tick size.': InvalidOrder, + 'Order must have minimum value of $10': InvalidOrder, + 'Insufficient margin to place order.': InsufficientFunds, + 'Reduce only order would increase position.': InvalidOrder, + 'Post only order would have immediately matched,': InvalidOrder, + 'Order could not immediately match against any resting orders.': InvalidOrder, + 'Invalid TP/SL price.': InvalidOrder, + 'No liquidity available for market order.': InvalidOrder, + 'Order was never placed, already canceled, or filled.': OrderNotFound, + 'User or API Wallet ': InvalidOrder, + 'Order has invalid size': InvalidOrder, + 'Order price cannot be more than 80% away from the reference price': InvalidOrder, + 'Order has zero size.': InvalidOrder, + 'Insufficient spot balance asset': InsufficientFunds, + 'Insufficient balance for withdrawal': InsufficientFunds, + 'Insufficient balance for token transfer': InsufficientFunds, + }, + }, + 'precisionMode': TICK_SIZE, + 'commonCurrencies': { + }, + 'options': { + 'defaultType': 'swap', + 'sandboxMode': False, + 'defaultSlippage': 0.05, + 'zeroAddress': '0x0000000000000000000000000000000000000000', + 'spotCurrencyMapping': { + 'UDZ': '2Z', + 'UBONK': 'BONK', + 'UBTC': 'BTC', + 'UETH': 'ETH', + 'UFART': 'FARTCOIN', + 'HPENGU': 'PENGU', + 'UPUMP': 'PUMP', + 'USOL': 'SOL', + 'UUUSPX': 'SPX', + 'USDT0': 'USDT', + 'XAUT0': 'XAUT', + 'UXPL': 'XPL', + }, + }, + 'features': { + 'default': { + 'sandbox': True, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': False, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': { + 'triggerPriceType': { + 'last': False, + 'mark': False, + 'index': False, + }, + 'triggerPrice': True, + 'type': True, + 'price': True, + }, + 'timeInForce': { + 'IOC': True, + 'FOK': False, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': False, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': { + 'max': 1000, + }, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 2000, + 'daysBack': None, + 'untilDays': None, + 'symbolRequired': True, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 2000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': 2000, + 'daysBack': None, + 'untilDays': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 2000, + 'daysBack': None, + 'daysBackCanceled': None, + 'untilDays': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOHLCV': { + 'limit': 5000, + }, + }, + 'spot': { + 'extends': 'default', + }, + 'forPerps': { + 'extends': 'default', + 'createOrder': { + 'stopLossPrice': True, + 'takeProfitPrice': True, + 'attachedStopLossTakeProfit': None, # todo, in two orders + }, + }, + 'swap': { + 'linear': { + 'extends': 'forPerps', + }, + 'inverse': { + 'extends': 'forPerps', + }, + }, + 'future': { + 'linear': { + 'extends': 'forPerps', + }, + 'inverse': { + 'extends': 'forPerps', + }, + }, + }, + }) + + def set_sandbox_mode(self, enabled): + super(hyperliquid, self).set_sandbox_mode(enabled) + self.options['sandboxMode'] = enabled + + def market(self, symbol: str) -> MarketInterface: + if self.markets is None: + raise ExchangeError(self.id + ' markets not loaded') + if symbol in self.markets: + market = self.markets[symbol] + if market['spot']: + baseName = self.safe_string(market, 'baseName') + spotCurrencyMapping = self.safe_dict(self.options, 'spotCurrencyMapping', {}) + if baseName in spotCurrencyMapping: + unifiedBaseName = self.safe_string(spotCurrencyMapping, baseName) + quote = self.safe_string(market, 'quote') + newSymbol = self.safe_currency_code(unifiedBaseName) + '/' + quote + if newSymbol in self.markets: + return self.markets[newSymbol] + res = super(hyperliquid, self).market(symbol) + return res + + def safe_market(self, marketId: Str = None, market: Market = None, delimiter: Str = None, marketType: Str = None) -> MarketInterface: + if marketId is not None: + if (self.markets_by_id is not None) and (marketId in self.markets_by_id): + markets = self.markets_by_id[marketId] + numMarkets = len(markets) + if numMarkets == 1: + return markets[0] + else: + if numMarkets > 2: + raise ExchangeError(self.id + ' safeMarket() found more than two markets with the same market id ' + marketId) + firstMarket = markets[0] + secondMarket = markets[1] + if self.safe_string(firstMarket, 'type') != self.safe_string(secondMarket, 'type'): + raise ExchangeError(self.id + ' safeMarket() found two different market types with the same market id ' + marketId) + baseCurrency = self.safe_string(firstMarket, 'base') + spotCurrencyMapping = self.safe_dict(self.options, 'spotCurrencyMapping', {}) + if baseCurrency in spotCurrencyMapping: + return secondMarket + return firstMarket + return super(hyperliquid, self).safe_market(marketId, market, delimiter, marketType) + + async def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/perpetuals#retrieve-perpetuals-metadata + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + if self.check_required_credentials(False): + await self.initialize_client() + request: dict = { + 'type': 'meta', + } + response = await self.publicPostInfo(self.extend(request, params)) + # + # [ + # { + # "universe": [ + # { + # "maxLeverage": 50, + # "name": "SOL", + # "onlyIsolated": False, + # "szDecimals": 2 + # } + # ] + # } + # ] + # + meta = self.safe_list(response, 'universe', []) + result: dict = {} + for i in range(0, len(meta)): + data = self.safe_dict(meta, i, {}) + id = i + name = self.safe_string(data, 'name') + code = self.safe_currency_code(name) + result[code] = self.safe_currency_structure({ + 'id': id, + 'name': name, + 'code': code, + 'precision': None, + 'info': data, + 'active': None, + 'deposit': None, + 'withdraw': None, + 'networks': None, + 'fee': None, + 'type': 'crypto', + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + }, + }) + return result + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for hyperliquid + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/perpetuals#retrieve-perpetuals-asset-contexts-includes-mark-price-current-funding-open-interest-etc + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/spot#retrieve-spot-asset-contexts + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + rawPromises = [ + self.fetch_swap_markets(params), + self.fetch_spot_markets(params), + ] + promises = await asyncio.gather(*rawPromises) + swapMarkets = promises[0] + spotMarkets = promises[1] + return self.array_concat(swapMarkets, spotMarkets) + + async def fetch_swap_markets(self, params={}) -> List[Market]: + """ + retrieves data on all swap markets for hyperliquid + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/perpetuals#retrieve-perpetuals-asset-contexts-includes-mark-price-current-funding-open-interest-etc + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + request: dict = { + 'type': 'metaAndAssetCtxs', + } + response = await self.publicPostInfo(self.extend(request, params)) + # + # [ + # { + # "universe": [ + # { + # "maxLeverage": 50, + # "name": "SOL", + # "onlyIsolated": False, + # "szDecimals": 2 + # } + # ] + # }, + # [ + # { + # "dayNtlVlm": "9450588.2273", + # "funding": "0.0000198", + # "impactPxs": [ + # "108.04", + # "108.06" + # ], + # "markPx": "108.04", + # "midPx": "108.05", + # "openInterest": "10764.48", + # "oraclePx": "107.99", + # "premium": "0.00055561", + # "prevDayPx": "111.81" + # } + # ] + # ] + # + # + meta = self.safe_dict(response, 0, {}) + universe = self.safe_list(meta, 'universe', []) + assetCtxs = self.safe_list(response, 1, []) + result = [] + for i in range(0, len(universe)): + data = self.extend( + self.safe_dict(universe, i, {}), + self.safe_dict(assetCtxs, i, {}) + ) + data['baseId'] = i + result.append(data) + return self.parse_markets(result) + + def calculate_price_precision(self, price: float, amountPrecision: float, maxDecimals: float): + """ + Helper function to calculate the Hyperliquid DECIMAL_PLACES price precision + :param float price: the price to use in the calculation + :param int amountPrecision: the amountPrecision to use in the calculation + :param int maxDecimals: the maxDecimals to use in the calculation + :returns int: The calculated price precision + """ + pricePrecision = 0 + priceStr = self.number_to_string(price) + if priceStr is None: + return 0 + priceSplitted = priceStr.split('.') + if Precise.string_eq(priceStr, '0'): + # Significant digits is always hasattr(self, 5) case + significantDigits = 5 + # Integer digits is always hasattr(self, 0) case(0 doesn't count) + integerDigits = 0 + # Calculate the price precision + pricePrecision = min(maxDecimals - amountPrecision, significantDigits - integerDigits) + elif Precise.string_gt(priceStr, '0') and Precise.string_lt(priceStr, '1'): + # Significant digits, always hasattr(self, 5) case + significantDigits = 5 + # Get the part after the decimal separator + decimalPart = self.safe_string(priceSplitted, 1, '') + # Count the number of leading zeros in the decimal part + leadingZeros = 0 + while((leadingZeros <= len(decimalPart)) and (decimalPart[leadingZeros] == '0')): + leadingZeros = leadingZeros + 1 + # Calculate price precision based on leading zeros and significant digits + pricePrecision = leadingZeros + significantDigits + # Calculate the price precision based on maxDecimals - szDecimals and the calculated price precision from the previous step + pricePrecision = min(maxDecimals - amountPrecision, pricePrecision) + else: + # Count the numbers before the decimal separator + integerPart = self.safe_string(priceSplitted, 0, '') + # Get significant digits, take the max() of 5 and the integer digits count + significantDigits = max(5, len(integerPart)) + # Calculate price precision based on maxDecimals - szDecimals and significantDigits - len(integerPart) + pricePrecision = min(maxDecimals - amountPrecision, significantDigits - len(integerPart)) + return self.parse_to_int(pricePrecision) + + async def fetch_spot_markets(self, params={}) -> List[Market]: + """ + retrieves data on all spot markets for hyperliquid + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/spot#retrieve-spot-asset-contexts + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + request: dict = { + 'type': 'spotMetaAndAssetCtxs', + } + response = await self.publicPostInfo(self.extend(request, params)) + # + # [ + # { + # "tokens": [ + # { + # "name": "USDC", + # "szDecimals": 8, + # "weiDecimals" 8, + # "index": 0, + # "tokenId": "0x6d1e7cde53ba9467b783cb7c530ce054", + # "isCanonical": True, + # "evmContract":null, + # "fullName":null + # }, + # { + # "name": "PURR", + # "szDecimals": 0, + # "weiDecimals": 5, + # "index": 1, + # "tokenId": "0xc1fb593aeffbeb02f85e0308e9956a90", + # "isCanonical": True, + # "evmContract":null, + # "fullName":null + # } + # ], + # "universe": [ + # { + # "name": "PURR/USDC", + # "tokens": [1, 0], + # "index": 0, + # "isCanonical": True + # } + # ] + # }, + # [ + # { + # "dayNtlVlm":"8906.0", + # "markPx":"0.14", + # "midPx":"0.209265", + # "prevDayPx":"0.20432" + # } + # ] + # ] + # + first = self.safe_dict(response, 0, {}) + second = self.safe_list(response, 1, []) + meta = self.safe_list(first, 'universe', []) + tokens = self.safe_list(first, 'tokens', []) + markets = [] + for i in range(0, len(meta)): + market = self.safe_dict(meta, i, {}) + index = self.safe_integer(market, 'index') + extraData = self.safe_dict(second, index, {}) + marketName = self.safe_string(market, 'name') + # if marketName.find('/') < 0: + # # there are some weird spot markets in testnet, eg @2 + # continue + # } + # marketParts = marketName.split('/') + # baseName = self.safe_string(marketParts, 0) + # quoteId = self.safe_string(marketParts, 1) + fees = self.safe_dict(self.fees, 'spot', {}) + taker = self.safe_number(fees, 'taker') + maker = self.safe_number(fees, 'maker') + tokensPos = self.safe_list(market, 'tokens', []) + baseTokenPos = self.safe_integer(tokensPos, 0) + quoteTokenPos = self.safe_integer(tokensPos, 1) + baseTokenInfo = self.safe_dict(tokens, baseTokenPos, {}) + quoteTokenInfo = self.safe_dict(tokens, quoteTokenPos, {}) + baseName = self.safe_string(baseTokenInfo, 'name') + quoteId = self.safe_string(quoteTokenInfo, 'name') + # do spot currency mapping + spotCurrencyMapping = self.safe_dict(self.options, 'spotCurrencyMapping', {}) + mappedBaseName = self.safe_string(spotCurrencyMapping, baseName, baseName) + mappedQuoteId = self.safe_string(spotCurrencyMapping, quoteId, quoteId) + mappedBase = self.safe_currency_code(mappedBaseName) + mappedQuote = self.safe_currency_code(mappedQuoteId) + mappedSymbol = mappedBase + '/' + mappedQuote + innerBaseTokenInfo = self.safe_dict(baseTokenInfo, 'spec', baseTokenInfo) + # innerQuoteTokenInfo = self.safe_dict(quoteTokenInfo, 'spec', quoteTokenInfo) + amountPrecisionStr = self.safe_string(innerBaseTokenInfo, 'szDecimals') + amountPrecision = int(amountPrecisionStr) + price = self.safe_number(extraData, 'midPx') + pricePrecision = 0 + if price is not None: + pricePrecision = self.calculate_price_precision(price, amountPrecision, 8) + pricePrecisionStr = self.number_to_string(pricePrecision) + # quotePrecision = self.parse_number(self.parse_precision(self.safe_string(innerQuoteTokenInfo, 'szDecimals'))) + baseId = self.number_to_string(index + 10000) + entry = { + 'id': marketName, + 'symbol': mappedSymbol, + 'base': mappedBase, + 'quote': mappedQuote, + 'settle': None, + 'baseId': baseId, + 'baseName': baseName, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'subType': None, + 'margin': None, + 'swap': False, + 'future': False, + 'option': False, + 'active': True, + 'contract': False, + 'linear': None, + 'inverse': None, + 'taker': taker, + 'maker': maker, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(self.parse_precision(amountPrecisionStr)), + 'price': self.parse_number(self.parse_precision(pricePrecisionStr)), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': None, + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': self.parse_number('10'), + 'max': None, + }, + }, + 'created': None, + 'info': self.extend(extraData, market), + } + markets.append(self.safe_market_structure(entry)) + # backward support + base = self.safe_currency_code(baseName) + quote = self.safe_currency_code(quoteId) + newEntry = self.extend({}, entry) + symbol = base + '/' + quote + if symbol != mappedSymbol: + newEntry['symbol'] = symbol + newEntry['base'] = base + newEntry['quote'] = quote + newEntry['baseName'] = baseName + markets.append(self.safe_market_structure(newEntry)) + return markets + + def parse_market(self, market: dict) -> Market: + # + # { + # "maxLeverage": "50", + # "name": "ETH", + # "onlyIsolated": False, + # "szDecimals": "4", + # "dayNtlVlm": "1709813.11535", + # "funding": "0.00004807", + # "impactPxs": [ + # "2369.3", + # "2369.6" + # ], + # "markPx": "2369.6", + # "midPx": "2369.45", + # "openInterest": "1815.4712", + # "oraclePx": "2367.3", + # "premium": "0.00090821", + # "prevDayPx": "2381.5" + # } + # + quoteId = 'USDC' + baseName = self.safe_string(market, 'name') + base = self.safe_currency_code(baseName) + quote = self.safe_currency_code(quoteId) + baseId = self.safe_string(market, 'baseId') + settleId = 'USDC' + settle = self.safe_currency_code(settleId) + symbol = base + '/' + quote + contract = True + swap = True + if contract: + if swap: + symbol = symbol + ':' + settle + fees = self.safe_dict(self.fees, 'swap', {}) + taker = self.safe_number(fees, 'taker') + maker = self.safe_number(fees, 'maker') + amountPrecisionStr = self.safe_string(market, 'szDecimals') + amountPrecision = int(amountPrecisionStr) + price = self.safe_number(market, 'markPx', 0) + pricePrecision = 0 + if price is not None: + pricePrecision = self.calculate_price_precision(price, amountPrecision, 6) + pricePrecisionStr = self.number_to_string(pricePrecision) + isDelisted = self.safe_bool(market, 'isDelisted') + active = True + if isDelisted is not None: + active = not isDelisted + return self.safe_market_structure({ + 'id': baseId, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'baseName': baseName, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': 'swap', + 'spot': False, + 'margin': None, + 'swap': swap, + 'future': False, + 'option': False, + 'active': active, + 'contract': contract, + 'linear': True, + 'inverse': False, + 'taker': taker, + 'maker': maker, + 'contractSize': self.parse_number('1'), + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(self.parse_precision(amountPrecisionStr)), + 'price': self.parse_number(self.parse_precision(pricePrecisionStr)), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': self.safe_integer(market, 'maxLeverage'), + }, + 'amount': { + 'min': None, + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': self.parse_number('10'), + 'max': None, + }, + }, + 'created': None, + 'info': market, + }) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/spot#retrieve-a-users-token-balances + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/perpetuals#retrieve-users-perpetuals-account-summary + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.user]: user address, will default to self.walletAddress if not provided + :param str [params.type]: wallet type, ['spot', 'swap'], defaults to swap + :param str [params.marginMode]: 'cross' or 'isolated', for margin trading, uses self.options.defaultMarginMode if not passed, defaults to None/None/None + :param str [params.subAccountAddress]: sub account user address + :returns dict: a `balance structure ` + """ + userAddress = None + userAddress, params = self.handle_public_address('fetchBalance', params) + type = None + type, params = self.handle_market_type_and_params('fetchBalance', None, params) + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchBalance', params) + isSpot = (type == 'spot') + request: dict = { + 'type': 'spotClearinghouseState' if (isSpot) else 'clearinghouseState', + 'user': userAddress, + } + response = await self.publicPostInfo(self.extend(request, params)) + # + # { + # "assetPositions": [], + # "crossMaintenanceMarginUsed": "0.0", + # "crossMarginSummary": { + # "accountValue": "100.0", + # "totalMarginUsed": "0.0", + # "totalNtlPos": "0.0", + # "totalRawUsd": "100.0" + # }, + # "marginSummary": { + # "accountValue": "100.0", + # "totalMarginUsed": "0.0", + # "totalNtlPos": "0.0", + # "totalRawUsd": "100.0" + # }, + # "time": "1704261007014", + # "withdrawable": "100.0" + # } + # spot + # + # { + # "balances":[ + # { + # "coin":"USDC", + # "hold":"0.0", + # "total":"1481.844" + # }, + # { + # "coin":"PURR", + # "hold":"0.0", + # "total":"999.65004" + # } + # } + # + balances = self.safe_list(response, 'balances') + if balances is not None: + spotBalances: dict = {'info': response} + for i in range(0, len(balances)): + balance = balances[i] + code = self.safe_currency_code(self.safe_string(balance, 'coin')) + account = self.account() + total = self.safe_string(balance, 'total') + used = self.safe_string(balance, 'hold') + account['total'] = total + account['used'] = used + spotBalances[code] = account + return self.safe_balance(spotBalances) + data = self.safe_dict(response, 'marginSummary', {}) + usdcBalance = { + 'total': self.safe_number(data, 'accountValue'), + } + if (marginMode is not None) and (marginMode == 'isolated'): + usdcBalance['free'] = self.safe_number(response, 'withdrawable') + else: + usdcBalance['used'] = self.safe_number(data, 'totalMarginUsed') + result: dict = { + 'info': response, + 'USDC': usdcBalance, + } + timestamp = self.safe_integer(response, 'time') + result['timestamp'] = timestamp + result['datetime'] = self.iso8601(timestamp) + return self.safe_balance(result) + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint#l2-book-snapshot + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'type': 'l2Book', + 'coin': market['baseName'] if market['swap'] else market['id'], + } + response = await self.publicPostInfo(self.extend(request, params)) + # + # { + # "coin": "ETH", + # "levels": [ + # [ + # { + # "n": "2", + # "px": "2216.2", + # "sz": "74.0637" + # } + # ], + # [ + # { + # "n": "2", + # "px": "2216.5", + # "sz": "70.5893" + # } + # ] + # ], + # "time": "1704290104840" + # } + # + data = self.safe_list(response, 'levels', []) + result: dict = { + 'bids': self.safe_list(data, 0, []), + 'asks': self.safe_list(data, 1, []), + } + timestamp = self.safe_integer(response, 'time') + return self.parse_order_book(result, market['symbol'], timestamp, 'bids', 'asks', 'px', 'sz') + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/perpetuals#retrieve-perpetuals-asset-contexts-includes-mark-price-current-funding-open-interest-etc + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/spot#retrieve-spot-asset-contexts + + :param str[] [symbols]: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: 'spot' or 'swap', by default fetches both + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + # at self stage, to get tickers data, we use fetchMarkets endpoints + response = [] + type = self.safe_string(params, 'type') + params = self.omit(params, 'type') + if type == 'spot': + response = await self.fetch_spot_markets(params) + elif type == 'swap': + response = await self.fetch_swap_markets(params) + else: + response = await self.fetch_markets(params) + # same response "fetchMarkets" + result: dict = {} + for i in range(0, len(response)): + market = response[i] + info = market['info'] + ticker = self.parse_ticker(info, market) + symbol = self.safe_string(ticker, 'symbol') + result[symbol] = ticker + return self.filter_by_array_tickers(result, 'symbol', symbols) + + async def fetch_funding_rates(self, symbols: Strings = None, params={}) -> FundingRates: + """ + retrieves data on all swap markets for hyperliquid + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/perpetuals#retrieve-perpetuals-asset-contexts-includes-mark-price-current-funding-open-interest-etc + + :param str[] [symbols]: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + request: dict = { + 'type': 'metaAndAssetCtxs', + } + response = await self.publicPostInfo(self.extend(request, params)) + # + # [ + # { + # "universe": [ + # { + # "maxLeverage": 50, + # "name": "SOL", + # "onlyIsolated": False, + # "szDecimals": 2 + # } + # ] + # }, + # [ + # { + # "dayNtlVlm": "9450588.2273", + # "funding": "0.0000198", + # "impactPxs": [ + # "108.04", + # "108.06" + # ], + # "markPx": "108.04", + # "midPx": "108.05", + # "openInterest": "10764.48", + # "oraclePx": "107.99", + # "premium": "0.00055561", + # "prevDayPx": "111.81" + # } + # ] + # ] + # + # + meta = self.safe_dict(response, 0, {}) + universe = self.safe_list(meta, 'universe', []) + assetCtxs = self.safe_list(response, 1, []) + result = [] + for i in range(0, len(universe)): + data = self.extend( + self.safe_dict(universe, i, {}), + self.safe_dict(assetCtxs, i, {}) + ) + result.append(data) + return self.parse_funding_rates(result, symbols) + + def parse_funding_rate(self, info, market: Market = None) -> FundingRate: + # + # { + # "maxLeverage": "50", + # "name": "ETH", + # "onlyIsolated": False, + # "szDecimals": "4", + # "dayNtlVlm": "1709813.11535", + # "funding": "0.00004807", + # "impactPxs": [ + # "2369.3", + # "2369.6" + # ], + # "markPx": "2369.6", + # "midPx": "2369.45", + # "openInterest": "1815.4712", + # "oraclePx": "2367.3", + # "premium": "0.00090821", + # "prevDayPx": "2381.5" + # } + # + base = self.safe_string(info, 'name') + marketId = self.coin_to_market_id(base) + symbol = self.safe_symbol(marketId, market) + funding = self.safe_number(info, 'funding') + markPx = self.safe_number(info, 'markPx') + oraclePx = self.safe_number(info, 'oraclePx') + fundingTimestamp = (int(math.floor(self.milliseconds()) / 60 / 60 / 1000) + 1) * 60 * 60 * 1000 + return { + 'info': info, + 'symbol': symbol, + 'markPrice': markPx, + 'indexPrice': oraclePx, + 'interestRate': None, + 'estimatedSettlePrice': None, + 'timestamp': None, + 'datetime': None, + 'fundingRate': funding, + 'fundingTimestamp': fundingTimestamp, + 'fundingDatetime': self.iso8601(fundingTimestamp), + 'nextFundingRate': None, + 'nextFundingTimestamp': None, + 'nextFundingDatetime': None, + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': '1h', + } + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "prevDayPx": "3400.5", + # "dayNtlVlm": "511297257.47936022", + # "markPx": "3464.7", + # "midPx": "3465.05", + # "oraclePx": "3460.1", # only in swap + # "openInterest": "64638.1108", # only in swap + # "premium": "0.00141614", # only in swap + # "funding": "0.00008727", # only in swap + # "impactPxs": ["3465.0", "3465.1"], # only in swap + # "coin": "PURR", # only in spot + # "circulatingSupply": "998949190.03400207", # only in spot + # }, + # + bidAsk = self.safe_list(ticker, 'impactPxs') + return self.safe_ticker({ + 'symbol': market['symbol'], + 'timestamp': None, + 'datetime': None, + 'previousClose': self.safe_number(ticker, 'prevDayPx'), + 'close': self.safe_number(ticker, 'midPx'), + 'bid': self.safe_number(bidAsk, 0), + 'ask': self.safe_number(bidAsk, 1), + 'quoteVolume': self.safe_number(ticker, 'dayNtlVlm'), + 'info': ticker, + }, market) + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint#candle-snapshot + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents, support '1m', '15m', '1h', '1d' + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest candle to fetch + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + until = self.safe_integer(params, 'until', self.milliseconds()) + useTail = since is None + originalSince = since + if since is None: + if limit is not None: + # optimization if limit is provided + timeframeInMilliseconds = self.parse_timeframe(timeframe) * 1000 + since = self.sum(until, timeframeInMilliseconds * limit * -1) + if since < 0: + since = 0 + useTail = False + else: + since = 0 + params = self.omit(params, ['until']) + request: dict = { + 'type': 'candleSnapshot', + 'req': { + 'coin': market['baseName'] if market['swap'] else market['id'], + 'interval': self.safe_string(self.timeframes, timeframe, timeframe), + 'startTime': since, + 'endTime': until, + }, + } + response = await self.publicPostInfo(self.extend(request, params)) + # + # [ + # { + # "T": 1704287699999, + # "c": "2226.4", + # "h": "2247.9", + # "i": "15m", + # "l": "2224.6", + # "n": 46, + # "o": "2247.9", + # "s": "ETH", + # "t": 1704286800000, + # "v": "591.6427" + # } + # ] + # + return self.parse_ohlcvs(response, market, timeframe, originalSince, limit, useTail) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # { + # "T": 1704287699999, + # "c": "2226.4", + # "h": "2247.9", + # "i": "15m", + # "l": "2224.6", + # "n": 46, + # "o": "2247.9", + # "s": "ETH", + # "t": 1704286800000, + # "v": "591.6427" + # } + # + return [ + self.safe_integer(ohlcv, 't'), + self.safe_number(ohlcv, 'o'), + self.safe_number(ohlcv, 'h'), + self.safe_number(ohlcv, 'l'), + self.safe_number(ohlcv, 'c'), + self.safe_number(ohlcv, 'v'), + ] + + async def fetch_trades(self, symbol: Str, since: Int = None, limit: Int = None, params={}): + """ + get the list of most recent trades for a particular symbol + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint#retrieve-a-users-fills + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint#retrieve-a-users-fills-by-time + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest trade + :param str [params.address]: wallet address that made trades + :param str [params.user]: wallet address that made trades + :param str [params.subAccountAddress]: sub account user address + :returns Trade[]: a list of `trade structures ` + """ + userAddress = None + userAddress, params = self.handle_public_address('fetchTrades', params) + await self.load_markets() + market = self.safe_market(symbol) + request: dict = { + 'user': userAddress, + } + if since is not None: + request['type'] = 'userFillsByTime' + request['startTime'] = since + else: + request['type'] = 'userFills' + until = self.safe_integer(params, 'until') + params = self.omit(params, 'until') + if until is not None: + request['endTime'] = until + response = await self.publicPostInfo(self.extend(request, params)) + # + # [ + # { + # "closedPnl": "0.19343", + # "coin": "ETH", + # "crossed": True, + # "dir": "Close Long", + # "fee": "0.050062", + # "hash": "0x09d77c96791e98b5775a04092584ab010d009445119c71e4005c0d634ea322bc", + # "liquidationMarkPx": null, + # "oid": 3929354691, + # "px": "2381.1", + # "side": "A", + # "startPosition": "0.0841", + # "sz": "0.0841", + # "tid": 128423918764978, + # "time": 1704262888911 + # } + # ] + # + return self.parse_trades(response, market, since, limit) + + def amount_to_precision(self, symbol, amount): + market = self.market(symbol) + return self.decimal_to_precision(amount, ROUND, market['precision']['amount'], self.precisionMode, self.paddingMode) + + def price_to_precision(self, symbol: str, price) -> str: + market = self.market(symbol) + priceStr = self.number_to_string(price) + integerPart = priceStr.split('.')[0] + significantDigits = max(5, len(integerPart)) + result = self.decimal_to_precision(price, ROUND, significantDigits, SIGNIFICANT_DIGITS, self.paddingMode) + maxDecimals = 8 if market['spot'] else 6 + subtractedValue = maxDecimals - self.precision_from_string(self.safe_string(market['precision'], 'amount')) + return self.decimal_to_precision(result, ROUND, subtractedValue, DECIMAL_PLACES, self.paddingMode) + + def hash_message(self, message): + return '0x' + self.hash(message, 'keccak', 'hex') + + def sign_hash(self, hash, privateKey): + signature = self.ecdsa(hash[-64:], privateKey[-64:], 'secp256k1', None) + return { + 'r': '0x' + signature['r'], + 's': '0x' + signature['s'], + 'v': self.sum(27, signature['v']), + } + + def sign_message(self, message, privateKey): + return self.sign_hash(self.hash_message(message), privateKey[-64:]) + + def construct_phantom_agent(self, hash, isTestnet=True): + source = 'b' if (isTestnet) else 'a' + return { + 'source': source, + 'connectionId': hash, + } + + def action_hash(self, action, vaultAddress, nonce): + dataBinary = self.packb(action) + dataHex = self.binary_to_base16(dataBinary) + data = dataHex + data += '00000' + self.int_to_base16(nonce) + if vaultAddress is None: + data += '00' + else: + data += '01' + data += vaultAddress + return self.hash(self.base16_to_binary(data), 'keccak', 'binary') + + def sign_l1_action(self, action, nonce, vaultAdress=None) -> object: + hash = self.action_hash(action, vaultAdress, nonce) + isTestnet = self.safe_bool(self.options, 'sandboxMode', False) + phantomAgent = self.construct_phantom_agent(hash, isTestnet) + # data: Dict = { + # 'domain': { + # 'chainId': 1337, + # 'name': 'Exchange', + # 'verifyingContract': '0x0000000000000000000000000000000000000000', + # 'version': '1', + # }, + # 'types': { + # 'Agent': [ + # {'name': 'source', 'type': 'string'}, + # {'name': 'connectionId', 'type': 'bytes32'}, + # ], + # 'EIP712Domain': [ + # {'name': 'name', 'type': 'string'}, + # {'name': 'version', 'type': 'string'}, + # {'name': 'chainId', 'type': 'uint256'}, + # {'name': 'verifyingContract', 'type': 'address'}, + # ], + # }, + # 'primaryType': 'Agent', + # 'message': phantomAgent, + # } + zeroAddress = self.safe_string(self.options, 'zeroAddress') + chainId = 1337 # check self out + domain: dict = { + 'chainId': chainId, + 'name': 'Exchange', + 'verifyingContract': zeroAddress, + 'version': '1', + } + messageTypes: dict = { + 'Agent': [ + {'name': 'source', 'type': 'string'}, + {'name': 'connectionId', 'type': 'bytes32'}, + ], + } + msg = self.eth_encode_structured_data(domain, messageTypes, phantomAgent) + signature = self.sign_message(msg, self.privateKey) + return signature + + def sign_user_signed_action(self, messageTypes, message): + zeroAddress = self.safe_string(self.options, 'zeroAddress') + chainId = 421614 # check self out + domain: dict = { + 'chainId': chainId, + 'name': 'HyperliquidSignTransaction', + 'verifyingContract': zeroAddress, + 'version': '1', + } + msg = self.eth_encode_structured_data(domain, messageTypes, message) + signature = self.sign_message(msg, self.privateKey) + return signature + + def build_usd_send_sig(self, message): + messageTypes: dict = { + 'HyperliquidTransaction:UsdSend': [ + {'name': 'hyperliquidChain', 'type': 'string'}, + {'name': 'destination', 'type': 'string'}, + {'name': 'amount', 'type': 'string'}, + {'name': 'time', 'type': 'uint64'}, + ], + } + return self.sign_user_signed_action(messageTypes, message) + + def build_usd_class_send_sig(self, message): + messageTypes: dict = { + 'HyperliquidTransaction:UsdClassTransfer': [ + {'name': 'hyperliquidChain', 'type': 'string'}, + {'name': 'amount', 'type': 'string'}, + {'name': 'toPerp', 'type': 'bool'}, + {'name': 'nonce', 'type': 'uint64'}, + ], + } + return self.sign_user_signed_action(messageTypes, message) + + def build_withdraw_sig(self, message): + messageTypes: dict = { + 'HyperliquidTransaction:Withdraw': [ + {'name': 'hyperliquidChain', 'type': 'string'}, + {'name': 'destination', 'type': 'string'}, + {'name': 'amount', 'type': 'string'}, + {'name': 'time', 'type': 'uint64'}, + ], + } + return self.sign_user_signed_action(messageTypes, message) + + def build_approve_builder_fee_sig(self, message): + messageTypes: dict = { + 'HyperliquidTransaction:ApproveBuilderFee': [ + {'name': 'hyperliquidChain', 'type': 'string'}, + {'name': 'maxFeeRate', 'type': 'string'}, + {'name': 'builder', 'type': 'address'}, + {'name': 'nonce', 'type': 'uint64'}, + ], + } + return self.sign_user_signed_action(messageTypes, message) + + async def set_ref(self): + if self.safe_bool(self.options, 'refSet', False): + return True + self.options['refSet'] = True + action = { + 'type': 'setReferrer', + 'code': self.safe_string(self.options, 'ref', 'CCXT1'), + } + nonce = self.milliseconds() + signature = self.sign_l1_action(action, nonce) + request: dict = { + 'action': action, + 'nonce': nonce, + 'signature': signature, + } + response = None + try: + response = await self.privatePostExchange(request) + return response + except Exception as e: + response = None # ignore self + return response + + async def approve_builder_fee(self, builder: str, maxFeeRate: str): + nonce = self.milliseconds() + isSandboxMode = self.safe_bool(self.options, 'sandboxMode', False) + payload: dict = { + 'hyperliquidChain': 'Testnet' if isSandboxMode else 'Mainnet', + 'maxFeeRate': maxFeeRate, + 'builder': builder, + 'nonce': nonce, + } + sig = self.build_approve_builder_fee_sig(payload) + action = { + 'hyperliquidChain': payload['hyperliquidChain'], + 'signatureChainId': '0x66eee', + 'maxFeeRate': payload['maxFeeRate'], + 'builder': payload['builder'], + 'nonce': nonce, + 'type': 'approveBuilderFee', + } + request: dict = { + 'action': action, + 'nonce': nonce, + 'signature': sig, + 'vaultAddress': None, + } + # + # { + # "status": "ok", + # "response": { + # "type": "default" + # } + # } + # + return await self.privatePostExchange(request) + + async def initialize_client(self): + try: + await asyncio.gather(*[self.handle_builder_fee_approval(), self.set_ref()]) + except Exception as e: + return False + return True + + async def handle_builder_fee_approval(self): + buildFee = self.safe_bool(self.options, 'builderFee', True) + if not buildFee: + return False # skip if builder fee is not enabled + approvedBuilderFee = self.safe_bool(self.options, 'approvedBuilderFee', False) + if approvedBuilderFee: + return True # skip if builder fee is already approved + try: + builder = self.safe_string(self.options, 'builder', '0x6530512A6c89C7cfCEbC3BA7fcD9aDa5f30827a6') + maxFeeRate = self.safe_string(self.options, 'feeRate', '0.01%') + await self.approve_builder_fee(builder, maxFeeRate) + self.options['approvedBuilderFee'] = True + except Exception as e: + self.options['builderFee'] = False # disable builder fee if an error occurs + return True + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#place-an-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.timeInForce]: 'Gtc', 'Ioc', 'Alo' + :param bool [params.postOnly]: True or False whether the order is post-only + :param bool [params.reduceOnly]: True or False whether the order is reduce-only + :param float [params.triggerPrice]: The price at which a trigger order is triggered at + :param str [params.clientOrderId]: client order id,(optional 128 bit hex string e.g. 0x1234567890abcdef1234567890abcdef) + :param str [params.slippage]: the slippage for market order + :param str [params.vaultAddress]: the vault address for order + :param str [params.subAccountAddress]: sub account user address + :returns dict: an `order structure ` + """ + await self.load_markets() + order, globalParams = self.parse_create_edit_order_args(None, symbol, type, side, amount, price, params) + orders = await self.create_orders([order], globalParams) + return orders[0] + + async def create_orders(self, orders: List[OrderRequest], params={}): + """ + create a list of trade orders + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#place-an-order + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + await self.initialize_client() + request = self.create_orders_request(orders, params) + response = await self.privatePostExchange(request) + # + # { + # "status": "ok", + # "response": { + # "type": "order", + # "data": { + # "statuses": [ + # { + # "resting": { + # "oid": 5063830287 + # } + # } + # ] + # } + # } + # } + # + responseObj = self.safe_dict(response, 'response', {}) + data = self.safe_dict(responseObj, 'data', {}) + statuses = self.safe_list(data, 'statuses', []) + ordersToBeParsed = [] + for i in range(0, len(statuses)): + order = statuses[i] + if order == 'waitingForTrigger': + ordersToBeParsed.append({'status': order}) # tp/sl orders can return a string like "waitingForTrigger", + else: + ordersToBeParsed.append(order) + return self.parse_orders(ordersToBeParsed, None) + + def create_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: str, price: Str = None, params={}): + market = self.market(symbol) + type = type.upper() + side = side.upper() + isMarket = (type == 'MARKET') + isBuy = (side == 'BUY') + clientOrderId = self.safe_string_2(params, 'clientOrderId', 'client_id') + slippage = self.safe_string(params, 'slippage') + defaultTimeInForce = 'ioc' if (isMarket) else 'gtc' + postOnly = self.safe_bool(params, 'postOnly', False) + if postOnly: + defaultTimeInForce = 'alo' + timeInForce = self.safe_string_lower(params, 'timeInForce', defaultTimeInForce) + timeInForce = self.capitalize(timeInForce) + triggerPrice = self.safe_string_2(params, 'triggerPrice', 'stopPrice') + stopLossPrice = self.safe_string(params, 'stopLossPrice', triggerPrice) + takeProfitPrice = self.safe_string(params, 'takeProfitPrice') + isTrigger = (stopLossPrice or takeProfitPrice) + px = None + if isMarket: + if price is None: + raise ArgumentsRequired(self.id + ' market orders require price to calculate the max slippage price. Default slippage can be set in options(default is 5%).') + px = Precise.string_mul(price, Precise.string_add('1', slippage)) if (isBuy) else Precise.string_mul(price, Precise.string_sub('1', slippage)) + px = self.price_to_precision(symbol, px) # round after adding slippage + else: + px = self.price_to_precision(symbol, price) + sz = self.amount_to_precision(symbol, amount) + reduceOnly = self.safe_bool(params, 'reduceOnly', False) + orderType: dict = {} + if isTrigger: + isTp = False + if takeProfitPrice is not None: + triggerPrice = self.price_to_precision(symbol, takeProfitPrice) + isTp = True + else: + triggerPrice = self.price_to_precision(symbol, stopLossPrice) + orderType['trigger'] = { + 'isMarket': isMarket, + 'triggerPx': triggerPrice, + 'tpsl': 'tp' if (isTp) else 'sl', + } + else: + orderType['limit'] = { + 'tif': timeInForce, + } + params = self.omit(params, ['clientOrderId', 'slippage', 'triggerPrice', 'stopPrice', 'stopLossPrice', 'takeProfitPrice', 'timeInForce', 'client_id', 'reduceOnly', 'postOnly']) + orderObj: dict = { + 'a': self.parse_to_int(market['baseId']), + 'b': isBuy, + 'p': px, + 's': sz, + 'r': reduceOnly, + 't': orderType, + # 'c': clientOrderId, + } + if clientOrderId is not None: + orderObj['c'] = clientOrderId + return orderObj + + def create_orders_request(self, orders, params={}) -> dict: + """ + create a list of trade orders + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#place-an-order + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :returns dict: an `order structure ` + """ + self.check_required_credentials() + defaultSlippage = self.safe_string(self.options, 'defaultSlippage') + defaultSlippage = self.safe_string(params, 'slippage', defaultSlippage) + hasClientOrderId = False + for i in range(0, len(orders)): + rawOrder = orders[i] + orderParams = self.safe_dict(rawOrder, 'params', {}) + clientOrderId = self.safe_string_2(orderParams, 'clientOrderId', 'client_id') + if clientOrderId is not None: + hasClientOrderId = True + if hasClientOrderId: + for i in range(0, len(orders)): + rawOrder = orders[i] + orderParams = self.safe_dict(rawOrder, 'params', {}) + clientOrderId = self.safe_string_2(orderParams, 'clientOrderId', 'client_id') + if clientOrderId is None: + raise ArgumentsRequired(self.id + ' createOrders() all orders must have clientOrderId if at least one has a clientOrderId') + params = self.omit(params, ['slippage', 'clientOrderId', 'client_id', 'slippage', 'triggerPrice', 'stopPrice', 'stopLossPrice', 'takeProfitPrice', 'timeInForce']) + nonce = self.milliseconds() + orderReq = [] + grouping = 'na' + for i in range(0, len(orders)): + rawOrder = orders[i] + marketId = self.safe_string(rawOrder, 'symbol') + market = self.market(marketId) + symbol = market['symbol'] + type = self.safe_string_upper(rawOrder, 'type') + side = self.safe_string_upper(rawOrder, 'side') + amount = self.safe_string(rawOrder, 'amount') + price = self.safe_string(rawOrder, 'price') + orderParams = self.safe_dict(rawOrder, 'params', {}) + slippage = self.safe_string(orderParams, 'slippage', defaultSlippage) + orderParams['slippage'] = slippage + stopLoss = self.safe_value(orderParams, 'stopLoss') + takeProfit = self.safe_value(orderParams, 'takeProfit') + isTrigger = (stopLoss or takeProfit) + orderParams = self.omit(orderParams, ['stopLoss', 'takeProfit']) + mainOrderObj: dict = self.create_order_request(symbol, type, side, amount, price, orderParams) + orderReq.append(mainOrderObj) + if isTrigger: + # grouping opposed orders for sl/tp + stopLossOrderTriggerPrice = self.safe_string_n(stopLoss, ['triggerPrice', 'stopPrice']) + stopLossOrderType = self.safe_string(stopLoss, 'type', 'limit') + stopLossOrderLimitPrice = self.safe_string_n(stopLoss, ['price', 'stopLossPrice'], stopLossOrderTriggerPrice) + takeProfitOrderTriggerPrice = self.safe_string_n(takeProfit, ['triggerPrice', 'stopPrice']) + takeProfitOrderType = self.safe_string(takeProfit, 'type', 'limit') + takeProfitOrderLimitPrice = self.safe_string_n(takeProfit, ['price', 'takeProfitPrice'], takeProfitOrderTriggerPrice) + grouping = 'normalTpsl' + orderParams = self.omit(orderParams, ['stopLoss', 'takeProfit']) + triggerOrderSide = '' + if side == 'BUY': + triggerOrderSide = 'sell' + else: + triggerOrderSide = 'buy' + if takeProfit is not None: + orderObj: dict = self.create_order_request(symbol, takeProfitOrderType, triggerOrderSide, amount, takeProfitOrderLimitPrice, self.extend(orderParams, { + 'takeProfitPrice': takeProfitOrderTriggerPrice, + 'reduceOnly': True, + })) + orderReq.append(orderObj) + if stopLoss is not None: + orderObj: dict = self.create_order_request(symbol, stopLossOrderType, triggerOrderSide, amount, stopLossOrderLimitPrice, self.extend(orderParams, { + 'stopLossPrice': stopLossOrderTriggerPrice, + 'reduceOnly': True, + })) + orderReq.append(orderObj) + vaultAddress = None + vaultAddress, params = self.handle_option_and_params(params, 'createOrder', 'vaultAddress') + vaultAddress = self.format_vault_address(vaultAddress) + orderAction: dict = { + 'type': 'order', + 'orders': orderReq, + 'grouping': grouping, + } + if self.safe_bool(self.options, 'approvedBuilderFee', False): + wallet = self.safe_string_lower(self.options, 'builder', '0x6530512A6c89C7cfCEbC3BA7fcD9aDa5f30827a6') + orderAction['builder'] = {'b': wallet, 'f': self.safe_integer(self.options, 'feeInt', 10)} + signature = self.sign_l1_action(orderAction, nonce, vaultAddress) + request: dict = { + 'action': orderAction, + 'nonce': nonce, + 'signature': signature, + # 'vaultAddress': vaultAddress, + } + if vaultAddress is not None: + params = self.omit(params, 'vaultAddress') + request['vaultAddress'] = vaultAddress + return request + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#cancel-order-s + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#cancel-order-s-by-cloid + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.clientOrderId]: client order id,(optional 128 bit hex string e.g. 0x1234567890abcdef1234567890abcdef) + :param str [params.vaultAddress]: the vault address for order + :param str [params.subAccountAddress]: sub account user address + :returns dict: An `order structure ` + """ + orders = await self.cancel_orders([id], symbol, params) + return self.safe_dict(orders, 0) + + async def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): + """ + cancel multiple orders + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#cancel-order-s + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#cancel-order-s-by-cloid + + :param str[] ids: order ids + :param str [symbol]: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param string|str[] [params.clientOrderId]: client order ids,(optional 128 bit hex string e.g. 0x1234567890abcdef1234567890abcdef) + :param str [params.vaultAddress]: the vault address + :param str [params.subAccountAddress]: sub account user address + :returns dict: an list of `order structures ` + """ + self.check_required_credentials() + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrders() requires a symbol argument') + await self.load_markets() + await self.initialize_client() + request = self.cancel_orders_request(ids, symbol, params) + response = await self.privatePostExchange(request) + # + # { + # "status":"ok", + # "response":{ + # "type":"cancel", + # "data":{ + # "statuses":[ + # "success" + # ] + # } + # } + # } + # + innerResponse = self.safe_dict(response, 'response') + data = self.safe_dict(innerResponse, 'data') + statuses = self.safe_list(data, 'statuses') + orders = [] + for i in range(0, len(statuses)): + status = statuses[i] + orders.append(self.safe_order({ + 'info': status, + 'status': status, + })) + return orders + + def cancel_orders_request(self, ids: List[str], symbol: Str = None, params={}) -> dict: + """ + build the request payload for cancelling multiple orders + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#cancel-order-s + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#cancel-order-s-by-cloid + :param str[] ids: order ids + :param str symbol: unified market symbol + :param dict [params]: + :returns dict: the raw request object to be sent to the exchange + """ + market = self.market(symbol) + clientOrderId = self.safe_value_2(params, 'clientOrderId', 'client_id') + params = self.omit(params, ['clientOrderId', 'client_id']) + nonce = self.milliseconds() + request: dict = { + 'nonce': nonce, + # 'vaultAddress': vaultAddress, + } + cancelReq = [] + cancelAction: dict = { + 'type': '', + 'cancels': [], + } + baseId = self.parse_to_numeric(market['baseId']) + if clientOrderId is not None: + if not isinstance(clientOrderId, list): + clientOrderId = [clientOrderId] + cancelAction['type'] = 'cancelByCloid' + for i in range(0, len(clientOrderId)): + cancelReq.append({ + 'asset': baseId, + 'cloid': clientOrderId[i], + }) + else: + cancelAction['type'] = 'cancel' + for i in range(0, len(ids)): + cancelReq.append({ + 'a': baseId, + 'o': self.parse_to_numeric(ids[i]), + }) + cancelAction['cancels'] = cancelReq + vaultAddress = None + vaultAddress, params = self.handle_option_and_params_2(params, 'cancelOrders', 'vaultAddress', 'subAccountAddress') + vaultAddress = self.format_vault_address(vaultAddress) + signature = self.sign_l1_action(cancelAction, nonce, vaultAddress) + request['action'] = cancelAction + request['signature'] = signature + if vaultAddress is not None: + params = self.omit(params, 'vaultAddress') + request['vaultAddress'] = vaultAddress + return request + + async def cancel_orders_for_symbols(self, orders: List[CancellationRequest], params={}): + """ + cancel multiple orders for multiple symbols + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#cancel-order-s + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#cancel-order-s-by-cloid + + :param CancellationRequest[] orders: each order should contain the parameters required by cancelOrder namely id and symbol, example [{"id": "a", "symbol": "BTC/USDT"}, {"id": "b", "symbol": "ETH/USDT"}] + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.vaultAddress]: the vault address + :param str [params.subAccountAddress]: sub account user address + :returns dict: an list of `order structures ` + """ + self.check_required_credentials() + await self.load_markets() + await self.initialize_client() + nonce = self.milliseconds() + request: dict = { + 'nonce': nonce, + # 'vaultAddress': vaultAddress, + } + cancelReq = [] + cancelAction: dict = { + 'type': '', + 'cancels': [], + } + cancelByCloid = False + for i in range(0, len(orders)): + order = orders[i] + clientOrderId = self.safe_string(order, 'clientOrderId') + if clientOrderId is not None: + cancelByCloid = True + id = self.safe_string(order, 'id') + symbol = self.safe_string(order, 'symbol') + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrdersForSymbols() requires a symbol argument in each order') + if id is not None and cancelByCloid: + raise BadRequest(self.id + ' cancelOrdersForSymbols() all orders must have either id or clientOrderId') + assetKey = 'asset' if cancelByCloid else 'a' + idKey = 'cloid' if cancelByCloid else 'o' + market = self.market(symbol) + cancelObj: dict = {} + cancelObj[assetKey] = self.parse_to_numeric(market['baseId']) + cancelObj[idKey] = clientOrderId if cancelByCloid else self.parse_to_numeric(id) + cancelReq.append(cancelObj) + cancelAction['type'] = 'cancelByCloid' if cancelByCloid else 'cancel' + cancelAction['cancels'] = cancelReq + vaultAddress = None + vaultAddress, params = self.handle_option_and_params_2(params, 'cancelOrdersForSymbols', 'vaultAddress', 'subAccountAddress') + vaultAddress = self.format_vault_address(vaultAddress) + signature = self.sign_l1_action(cancelAction, nonce, vaultAddress) + request['action'] = cancelAction + request['signature'] = signature + if vaultAddress is not None: + params = self.omit(params, 'vaultAddress') + request['vaultAddress'] = vaultAddress + response = await self.privatePostExchange(request) + # + # { + # "status":"ok", + # "response":{ + # "type":"cancel", + # "data":{ + # "statuses":[ + # "success" + # ] + # } + # } + # } + # + return [self.safe_order({'info': response})] + + async def cancel_all_orders_after(self, timeout: Int, params={}): + """ + dead man's switch, cancel all orders after the given timeout + :param number timeout: time in milliseconds, 0 represents cancel the timer + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.vaultAddress]: the vault address + :param str [params.subAccountAddress]: sub account user address + :returns dict: the api result + """ + self.check_required_credentials() + await self.load_markets() + await self.initialize_client() + params = self.omit(params, ['clientOrderId', 'client_id']) + nonce = self.milliseconds() + request: dict = { + 'nonce': nonce, + # 'vaultAddress': vaultAddress, + } + cancelAction: dict = { + 'type': 'scheduleCancel', + 'time': nonce + timeout, + } + vaultAddress = None + vaultAddress, params = self.handle_option_and_params_2(params, 'cancelAllOrdersAfter', 'vaultAddress', 'subAccountAddress') + vaultAddress = self.format_vault_address(vaultAddress) + signature = self.sign_l1_action(cancelAction, nonce, vaultAddress) + request['action'] = cancelAction + request['signature'] = signature + if vaultAddress is not None: + params = self.omit(params, 'vaultAddress') + request['vaultAddress'] = vaultAddress + response = await self.privatePostExchange(request) + # + # { + # "status":"err", + # "response":"Cannot set scheduled cancel time until enough volume traded. Required: $1000000. Traded: $373.47205." + # } + # + return response + + def edit_orders_request(self, orders, params={}): + self.check_required_credentials() + hasClientOrderId = False + for i in range(0, len(orders)): + rawOrder = orders[i] + orderParams = self.safe_dict(rawOrder, 'params', {}) + clientOrderId = self.safe_string_2(orderParams, 'clientOrderId', 'client_id') + if clientOrderId is not None: + hasClientOrderId = True + if hasClientOrderId: + for i in range(0, len(orders)): + rawOrder = orders[i] + orderParams = self.safe_dict(rawOrder, 'params', {}) + clientOrderId = self.safe_string_2(orderParams, 'clientOrderId', 'client_id') + if clientOrderId is None: + raise ArgumentsRequired(self.id + ' editOrders() all orders must have clientOrderId if at least one has a clientOrderId') + params = self.omit(params, ['slippage', 'clientOrderId', 'client_id', 'slippage', 'triggerPrice', 'stopPrice', 'stopLossPrice', 'takeProfitPrice', 'timeInForce']) + modifies = [] + for i in range(0, len(orders)): + rawOrder = orders[i] + id = self.safe_string(rawOrder, 'id') + marketId = self.safe_string(rawOrder, 'symbol') + market = self.market(marketId) + symbol = market['symbol'] + type = self.safe_string_upper(rawOrder, 'type') + isMarket = (type == 'MARKET') + side = self.safe_string_upper(rawOrder, 'side') + isBuy = (side == 'BUY') + amount = self.safe_string(rawOrder, 'amount') + price = self.safe_string(rawOrder, 'price') + orderParams = self.safe_dict(rawOrder, 'params', {}) + defaultSlippage = self.safe_string(self.options, 'defaultSlippage') + slippage = self.safe_string(orderParams, 'slippage', defaultSlippage) + defaultTimeInForce = 'ioc' if (isMarket) else 'gtc' + postOnly = self.safe_bool(orderParams, 'postOnly', False) + if postOnly: + defaultTimeInForce = 'alo' + timeInForce = self.safe_string_lower(orderParams, 'timeInForce', defaultTimeInForce) + timeInForce = self.capitalize(timeInForce) + clientOrderId = self.safe_string_2(orderParams, 'clientOrderId', 'client_id') + triggerPrice = self.safe_string_2(orderParams, 'triggerPrice', 'stopPrice') + stopLossPrice = self.safe_string(orderParams, 'stopLossPrice', triggerPrice) + takeProfitPrice = self.safe_string(orderParams, 'takeProfitPrice') + isTrigger = (stopLossPrice or takeProfitPrice) + reduceOnly = self.safe_bool(orderParams, 'reduceOnly', False) + orderParams = self.omit(orderParams, ['slippage', 'timeInForce', 'triggerPrice', 'stopLossPrice', 'takeProfitPrice', 'clientOrderId', 'client_id', 'postOnly', 'reduceOnly']) + px = self.number_to_string(price) + if isMarket: + px = Precise.string_mul(px, Precise.string_add('1', slippage)) if (isBuy) else Precise.string_mul(px, Precise.string_sub('1', slippage)) + px = self.price_to_precision(symbol, px) + else: + px = self.price_to_precision(symbol, px) + sz = self.amount_to_precision(symbol, amount) + orderType: dict = {} + if isTrigger: + isTp = False + if takeProfitPrice is not None: + triggerPrice = self.price_to_precision(symbol, takeProfitPrice) + isTp = True + else: + triggerPrice = self.price_to_precision(symbol, stopLossPrice) + orderType['trigger'] = { + 'isMarket': isMarket, + 'triggerPx': triggerPrice, + 'tpsl': 'tp' if (isTp) else 'sl', + } + else: + orderType['limit'] = { + 'tif': timeInForce, + } + if triggerPrice is None: + triggerPrice = '0' + orderReq: dict = { + 'a': self.parse_to_int(market['baseId']), + 'b': isBuy, + 'p': px, + 's': sz, + 'r': reduceOnly, + 't': orderType, + # 'c': clientOrderId, + } + if clientOrderId is not None: + orderReq['c'] = clientOrderId + modifyReq: dict = { + 'oid': self.parse_to_int(id), + 'order': orderReq, + } + modifies.append(modifyReq) + nonce = self.milliseconds() + modifyAction: dict = { + 'type': 'batchModify', + 'modifies': modifies, + } + vaultAddress = None + vaultAddress, params = self.handle_option_and_params(params, 'editOrder', 'vaultAddress') + vaultAddress = self.format_vault_address(vaultAddress) + signature = self.sign_l1_action(modifyAction, nonce, vaultAddress) + request: dict = { + 'action': modifyAction, + 'nonce': nonce, + 'signature': signature, + # 'vaultAddress': vaultAddress, + } + if vaultAddress is not None: + request['vaultAddress'] = vaultAddress + return request + + async def edit_order(self, id: str, symbol: str, type: str, side: str, amount: Num = None, price: Num = None, params={}): + """ + edit a trade order + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#modify-multiple-orders + + :param str id: cancel order id + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.timeInForce]: 'Gtc', 'Ioc', 'Alo' + :param bool [params.postOnly]: True or False whether the order is post-only + :param bool [params.reduceOnly]: True or False whether the order is reduce-only + :param float [params.triggerPrice]: The price at which a trigger order is triggered at + :param str [params.clientOrderId]: client order id,(optional 128 bit hex string e.g. 0x1234567890abcdef1234567890abcdef) + :param str [params.vaultAddress]: the vault address for order + :param str [params.subAccountAddress]: sub account user address + :returns dict: an `order structure ` + """ + await self.load_markets() + if id is None: + raise ArgumentsRequired(self.id + ' editOrder() requires an id argument') + order, globalParams = self.parse_create_edit_order_args(id, symbol, type, side, amount, price, params) + orders = await self.edit_orders([order], globalParams) + return orders[0] + + async def edit_orders(self, orders: List[OrderRequest], params={}): + """ + edit a list of trade orders + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#modify-multiple-orders + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + await self.initialize_client() + request = self.edit_orders_request(orders, params) + response = await self.privatePostExchange(request) + # + # { + # "status": "ok", + # "response": { + # "type": "order", + # "data": { + # "statuses": [ + # { + # "resting": { + # "oid": 5063830287 + # } + # } + # ] + # } + # } + # } + # when the order is filled immediately + # { + # "status":"ok", + # "response":{ + # "type":"order", + # "data":{ + # "statuses":[ + # { + # "filled":{ + # "totalSz":"0.1", + # "avgPx":"100.84", + # "oid":6195281425 + # } + # } + # ] + # } + # } + # } + # + responseObject = self.safe_dict(response, 'response', {}) + dataObject = self.safe_dict(responseObject, 'data', {}) + statuses = self.safe_list(dataObject, 'statuses', []) + return self.parse_orders(statuses) + + async def create_vault(self, name: str, description: str, initialUsd: int, params={}): + """ + creates a value + :param str name: The name of the vault + :param str description: The description of the vault + :param number initialUsd: The initialUsd of the vault + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: the api result + """ + self.check_required_credentials() + await self.load_markets() + nonce = self.milliseconds() + request: dict = { + 'nonce': nonce, + } + usd = self.parse_to_int(Precise.string_mul(self.number_to_string(initialUsd), '1000000')) + action: dict = { + 'type': 'createVault', + 'name': name, + 'description': description, + 'initialUsd': usd, + 'nonce': nonce, + } + signature = self.sign_l1_action(action, nonce) + request['action'] = action + request['signature'] = signature + response = await self.privatePostExchange(self.extend(request, params)) + # + # { + # "status": "ok", + # "response": { + # "type": "createVault", + # "data": "0x04fddcbc9ce80219301bd16f18491bedf2a8c2b8" + # } + # } + # + return response + + async def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical funding rate prices + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/perpetuals#retrieve-historical-funding-rates + + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: timestamp in ms of the earliest funding rate to fetch + :param int [limit]: the maximum amount of `funding rate structures ` to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest funding rate + :returns dict[]: a list of `funding rate structures ` + """ + await self.load_markets() + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchFundingRateHistory() requires a symbol argument') + market = self.market(symbol) + request: dict = { + 'type': 'fundingHistory', + 'coin': market['baseName'], + } + if since is not None: + request['startTime'] = since + else: + maxLimit = 500 if (limit is None) else limit + request['startTime'] = self.milliseconds() - maxLimit * 60 * 60 * 1000 + until = self.safe_integer(params, 'until') + params = self.omit(params, 'until') + if until is not None: + request['endTime'] = until + response = await self.publicPostInfo(self.extend(request, params)) + # + # [ + # { + # "coin": "ETH", + # "fundingRate": "0.0000125", + # "premium": "0.00057962", + # "time": 1704290400031 + # } + # ] + # + result = [] + for i in range(0, len(response)): + entry = response[i] + timestamp = self.safe_integer(entry, 'time') + result.append({ + 'info': entry, + 'symbol': self.safe_symbol(None, market), + 'fundingRate': self.safe_number(entry, 'fundingRate'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + sorted = self.sort_by(result, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, symbol, since, limit) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint#retrieve-a-users-open-orders + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.user]: user address, will default to self.walletAddress if not provided + :param str [params.method]: 'openOrders' or 'frontendOpenOrders' default is 'frontendOpenOrders' + :param str [params.subAccountAddress]: sub account user address + :returns Order[]: a list of `order structures ` + """ + userAddress = None + userAddress, params = self.handle_public_address('fetchOpenOrders', params) + method = None + method, params = self.handle_option_and_params(params, 'fetchOpenOrders', 'method', 'frontendOpenOrders') + await self.load_markets() + market = self.safe_market(symbol) + request: dict = { + 'type': method, + 'user': userAddress, + } + response = await self.publicPostInfo(self.extend(request, params)) + # + # [ + # { + # "coin": "ETH", + # "limitPx": "2000.0", + # "oid": 3991946565, + # "origSz": "0.1", + # "side": "B", + # "sz": "0.1", + # "timestamp": 1704346468838 + # } + # ] + # + orderWithStatus = [] + for i in range(0, len(response)): + order = response[i] + extendOrder = {} + if self.safe_string(order, 'status') is None: + extendOrder['ccxtStatus'] = 'open' + orderWithStatus.append(self.extend(order, extendOrder)) + return self.parse_orders(orderWithStatus, market, since, limit) + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently closed orders + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.user]: user address, will default to self.walletAddress if not provided + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + orders = await self.fetch_orders(symbol, None, None, params) # don't filter here because we don't want to catch open orders + closedOrders = self.filter_by_array(orders, 'status', ['closed'], False) + return self.filter_by_symbol_since_limit(closedOrders, symbol, since, limit) + + async def fetch_canceled_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all canceled orders + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.user]: user address, will default to self.walletAddress if not provided + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + orders = await self.fetch_orders(symbol, None, None, params) # don't filter here because we don't want to catch open orders + closedOrders = self.filter_by_array(orders, 'status', ['canceled'], False) + return self.filter_by_symbol_since_limit(closedOrders, symbol, since, limit) + + async def fetch_canceled_and_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all closed and canceled orders + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.user]: user address, will default to self.walletAddress if not provided + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + orders = await self.fetch_orders(symbol, None, None, params) # don't filter here because we don't want to catch open orders + closedOrders = self.filter_by_array(orders, 'status', ['canceled', 'closed', 'rejected'], False) + return self.filter_by_symbol_since_limit(closedOrders, symbol, since, limit) + + async def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all orders + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.user]: user address, will default to self.walletAddress if not provided + :param str [params.subAccountAddress]: sub account user address + :returns Order[]: a list of `order structures ` + """ + userAddress = None + userAddress, params = self.handle_public_address('fetchOrders', params) + await self.load_markets() + market = self.safe_market(symbol) + request: dict = { + 'type': 'historicalOrders', + 'user': userAddress, + } + response = await self.publicPostInfo(self.extend(request, params)) + # + # [ + # { + # "coin": "ETH", + # "limitPx": "2000.0", + # "oid": 3991946565, + # "origSz": "0.1", + # "side": "B", + # "sz": "0.1", + # "timestamp": 1704346468838 + # } + # ] + # + return self.parse_orders(response, market, since, limit) + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint#query-order-status-by-oid-or-cloid + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.clientOrderId]: client order id,(optional 128 bit hex string e.g. 0x1234567890abcdef1234567890abcdef) + :param str [params.user]: user address, will default to self.walletAddress if not provided + :param str [params.subAccountAddress]: sub account user address + :returns dict: An `order structure ` + """ + userAddress = None + userAddress, params = self.handle_public_address('fetchOrder', params) + await self.load_markets() + market = self.safe_market(symbol) + clientOrderId = self.safe_string(params, 'clientOrderId') + request: dict = { + 'type': 'orderStatus', + # 'oid': id if isClientOrderId else self.parse_to_numeric(id), + 'user': userAddress, + } + if clientOrderId is not None: + params = self.omit(params, 'clientOrderId') + request['oid'] = clientOrderId + else: + isClientOrderId = len(id) >= 34 + request['oid'] = id if isClientOrderId else self.parse_to_numeric(id) + response = await self.publicPostInfo(self.extend(request, params)) + # + # { + # "order": { + # "order": { + # "children": [], + # "cloid": null, + # "coin": "ETH", + # "isPositionTpsl": False, + # "isTrigger": False, + # "limitPx": "2000.0", + # "oid": "3991946565", + # "orderType": "Limit", + # "origSz": "0.1", + # "reduceOnly": False, + # "side": "B", + # "sz": "0.1", + # "tif": "Gtc", + # "timestamp": "1704346468838", + # "triggerCondition": "N/A", + # "triggerPx": "0.0" + # }, + # "status": "open", + # "statusTimestamp": "1704346468838" + # }, + # "status": "order" + # } + # + data = self.safe_dict(response, 'order') + return self.parse_order(data, market) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrdersWs error + # + # {error: 'Insufficient margin to place order. asset=159'} + # + # fetchOpenOrders + # + # { + # "coin": "ETH", + # "limitPx": "2000.0", + # "oid": 3991946565, + # "origSz": "0.1", + # "side": "B", + # "sz": "0.1", + # "timestamp": 1704346468838 + # } + # fetchClosedorders + # { + # "cloid": null, + # "closedPnl": "0.0", + # "coin": "SOL", + # "crossed": True, + # "dir": "Open Long", + # "fee": "0.003879", + # "hash": "0x4a2647998682b7f07bc5040ab531e1011400f9a51bfa0346a0b41ebe510e8875", + # "liquidationMarkPx": null, + # "oid": "6463280784", + # "px": "110.83", + # "side": "B", + # "startPosition": "1.64", + # "sz": "0.1", + # "tid": "232174667018988", + # "time": "1709142268394" + # } + # + # fetchOrder + # + # { + # "order": { + # "children": [], + # "cloid": null, + # "coin": "ETH", + # "isPositionTpsl": False, + # "isTrigger": False, + # "limitPx": "2000.0", + # "oid": "3991946565", + # "orderType": "Limit", + # "origSz": "0.1", + # "reduceOnly": False, + # "side": "B", + # "sz": "0.1", + # "tif": "Gtc", + # "timestamp": "1704346468838", + # "triggerCondition": "N/A", + # "triggerPx": "0.0" + # }, + # "status": "open", + # "statusTimestamp": "1704346468838" + # } + # + # createOrder + # + # { + # "resting": { + # "oid": 5063830287 + # } + # } + # + # { + # "filled":{ + # "totalSz":"0.1", + # "avgPx":"100.84", + # "oid":6195281425 + # } + # } + # frontendOrder + # { + # "children": [], + # "cloid": null, + # "coin": "BLUR", + # "isPositionTpsl": False, + # "isTrigger": True, + # "limitPx": "0.5", + # "oid": 8670487141, + # "orderType": "Stop Limit", + # "origSz": "20.0", + # "reduceOnly": False, + # "side": "B", + # "sz": "20.0", + # "tif": null, + # "timestamp": 1715523663687, + # "triggerCondition": "Price above 0.6", + # "triggerPx": "0.6" + # } + # + error = self.safe_string(order, 'error') + if error is not None: + return self.safe_order({ + 'info': order, + 'status': 'rejected', + }) + entry = self.safe_dict_n(order, ['order', 'resting', 'filled']) + if entry is None: + entry = order + coin = self.safe_string(entry, 'coin') + marketId = None + if coin is not None: + marketId = self.coin_to_market_id(coin) + if self.safe_string(entry, 'id') is None: + market = self.safe_market(marketId, None) + else: + market = self.safe_market(marketId, market) + symbol = market['symbol'] + timestamp = self.safe_integer(entry, 'timestamp') + status = self.safe_string_2(order, 'status', 'ccxtStatus') + order = self.omit(order, ['ccxtStatus']) + side = self.safe_string(entry, 'side') + if side is not None: + side = 'sell' if (side == 'A') else 'buy' + totalAmount = self.safe_string_2(entry, 'origSz', 'totalSz') + remaining = self.safe_string(entry, 'sz') + tif = self.safe_string_upper(entry, 'tif') + postOnly = None + if tif is not None: + postOnly = (tif == 'ALO') + return self.safe_order({ + 'info': order, + 'id': self.safe_string(entry, 'oid'), + 'clientOrderId': self.safe_string(entry, 'cloid'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'lastUpdateTimestamp': self.safe_integer(order, 'statusTimestamp'), + 'symbol': symbol, + 'type': self.parse_order_type(self.safe_string_lower(entry, 'orderType')), + 'timeInForce': tif, + 'postOnly': postOnly, + 'reduceOnly': self.safe_bool(entry, 'reduceOnly'), + 'side': side, + 'price': self.safe_string(entry, 'limitPx'), + 'triggerPrice': self.safe_number(entry, 'triggerPx') if self.safe_bool(entry, 'isTrigger') else None, + 'amount': totalAmount, + 'cost': None, + 'average': self.safe_string(entry, 'avgPx'), + 'filled': Precise.string_sub(totalAmount, remaining), + 'remaining': remaining, + 'status': self.parse_order_status(status), + 'fee': None, + 'trades': None, + }, market) + + def parse_order_status(self, status: Str): + if status is None: + return None + statuses: dict = { + 'triggered': 'open', + 'filled': 'closed', + 'open': 'open', + 'canceled': 'canceled', + 'rejected': 'rejected', + 'marginCanceled': 'canceled', + } + if status.endswith('Rejected'): + return 'rejected' + if status.endswith('Canceled'): + return 'canceled' + return self.safe_string(statuses, status, status) + + def parse_order_type(self, status): + statuses: dict = { + 'stop limit': 'limit', + 'stop market': 'market', + } + return self.safe_string(statuses, status, status) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint#retrieve-a-users-fills + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint#retrieve-a-users-fills-by-time + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest trade + :param str [params.subAccountAddress]: sub account user address + :returns Trade[]: a list of `trade structures ` + """ + userAddress = None + userAddress, params = self.handle_public_address('fetchMyTrades', params) + await self.load_markets() + market = self.safe_market(symbol) + request: dict = { + 'user': userAddress, + } + if since is not None: + request['type'] = 'userFillsByTime' + request['startTime'] = since + else: + request['type'] = 'userFills' + until = self.safe_integer(params, 'until') + params = self.omit(params, 'until') + if until is not None: + request['endTime'] = until + response = await self.publicPostInfo(self.extend(request, params)) + # + # [ + # { + # "closedPnl": "0.19343", + # "coin": "ETH", + # "crossed": True, + # "dir": "Close Long", + # "fee": "0.050062", + # "feeToken": "USDC", + # "hash": "0x09d77c96791e98b5775a04092584ab010d009445119c71e4005c0d634ea322bc", + # "liquidationMarkPx": null, + # "oid": 3929354691, + # "px": "2381.1", + # "side": "A", + # "startPosition": "0.0841", + # "sz": "0.0841", + # "tid": 128423918764978, + # "time": 1704262888911 + # } + # ] + # + return self.parse_trades(response, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # { + # "closedPnl": "0.19343", + # "coin": "ETH", + # "crossed": True, + # "dir": "Close Long", + # "fee": "0.050062", + # "hash": "0x09d77c96791e98b5775a04092584ab010d009445119c71e4005c0d634ea322bc", + # "liquidationMarkPx": null, + # "oid": 3929354691, + # "px": "2381.1", + # "side": "A", + # "startPosition": "0.0841", + # "sz": "0.0841", + # "tid": 128423918764978, + # "time": 1704262888911 + # } + # + timestamp = self.safe_integer(trade, 'time') + price = self.safe_string(trade, 'px') + amount = self.safe_string(trade, 'sz') + coin = self.safe_string(trade, 'coin') + marketId = self.coin_to_market_id(coin) + market = self.safe_market(marketId, None) + symbol = market['symbol'] + id = self.safe_string(trade, 'tid') + side = self.safe_string(trade, 'side') + if side is not None: + side = 'sell' if (side == 'A') else 'buy' + fee = self.safe_string(trade, 'fee') + takerOrMaker = None + crossed = self.safe_bool(trade, 'crossed') + if crossed is not None: + takerOrMaker = 'taker' if crossed else 'maker' + return self.safe_trade({ + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'id': id, + 'order': self.safe_string(trade, 'oid'), + 'type': None, + 'side': side, + 'takerOrMaker': takerOrMaker, + 'price': price, + 'amount': amount, + 'cost': None, + 'fee': { + 'cost': fee, + 'currency': self.safe_string(trade, 'feeToken'), + 'rate': None, + }, + }, market) + + async def fetch_position(self, symbol: str, params={}): + """ + fetch data on an open position + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/perpetuals#retrieve-users-perpetuals-account-summary + + :param str symbol: unified market symbol of the market the position is held in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.user]: user address, will default to self.walletAddress if not provided + :returns dict: a `position structure ` + """ + positions = await self.fetch_positions([symbol], params) + return self.safe_dict(positions, 0, {}) + + async def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/perpetuals#retrieve-users-perpetuals-account-summary + + :param str[] [symbols]: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.user]: user address, will default to self.walletAddress if not provided + :param str [params.subAccountAddress]: sub account user address + :returns dict[]: a list of `position structure ` + """ + await self.load_markets() + userAddress = None + userAddress, params = self.handle_public_address('fetchPositions', params) + symbols = self.market_symbols(symbols) + request: dict = { + 'type': 'clearinghouseState', + 'user': userAddress, + } + response = await self.publicPostInfo(self.extend(request, params)) + # + # { + # "assetPositions": [ + # { + # "position": { + # "coin": "ETH", + # "cumFunding": { + # "allTime": "0.0", + # "sinceChange": "0.0", + # "sinceOpen": "0.0" + # }, + # "entryPx": "2213.9", + # "leverage": { + # "rawUsd": "-475.23904", + # "type": "isolated", + # "value": "20" + # }, + # "liquidationPx": "2125.00856238", + # "marginUsed": "24.88097", + # "maxLeverage": "50", + # "positionValue": "500.12001", + # "returnOnEquity": "0.0", + # "szi": "0.2259", + # "unrealizedPnl": "0.0" + # }, + # "type": "oneWay" + # } + # ], + # "crossMaintenanceMarginUsed": "0.0", + # "crossMarginSummary": { + # "accountValue": "100.0", + # "totalMarginUsed": "0.0", + # "totalNtlPos": "0.0", + # "totalRawUsd": "100.0" + # }, + # "marginSummary": { + # "accountValue": "100.0", + # "totalMarginUsed": "0.0", + # "totalNtlPos": "0.0", + # "totalRawUsd": "100.0" + # }, + # "time": "1704261007014", + # "withdrawable": "100.0" + # } + # + data = self.safe_list(response, 'assetPositions', []) + result = [] + for i in range(0, len(data)): + result.append(self.parse_position(data[i], None)) + return self.filter_by_array_positions(result, 'symbol', symbols, False) + + def parse_position(self, position: dict, market: Market = None): + # + # { + # "position": { + # "coin": "ETH", + # "cumFunding": { + # "allTime": "0.0", + # "sinceChange": "0.0", + # "sinceOpen": "0.0" + # }, + # "entryPx": "2213.9", + # "leverage": { + # "rawUsd": "-475.23904", + # "type": "isolated", + # "value": "20" + # }, + # "liquidationPx": "2125.00856238", + # "marginUsed": "24.88097", + # "maxLeverage": "50", + # "positionValue": "500.12001", + # "returnOnEquity": "0.0", + # "szi": "0.2259", + # "unrealizedPnl": "0.0" + # }, + # "type": "oneWay" + # } + # + entry = self.safe_dict(position, 'position', {}) + coin = self.safe_string(entry, 'coin') + marketId = self.coin_to_market_id(coin) + market = self.safe_market(marketId, None) + symbol = market['symbol'] + leverage = self.safe_dict(entry, 'leverage', {}) + marginMode = self.safe_string(leverage, 'type') + isIsolated = (marginMode == 'isolated') + rawSize = self.safe_string(entry, 'szi') + size = rawSize + side = None + if size is not None: + side = 'long' if Precise.string_gt(rawSize, '0') else 'short' + size = Precise.string_abs(size) + rawUnrealizedPnl = self.safe_string(entry, 'unrealizedPnl') + absRawUnrealizedPnl = Precise.string_abs(rawUnrealizedPnl) + marginUsed = self.safe_string(entry, 'marginUsed') + initialMargin = None + if isIsolated: + initialMargin = Precise.string_sub(marginUsed, rawUnrealizedPnl) + else: + initialMargin = marginUsed + percentage = Precise.string_mul(Precise.string_div(absRawUnrealizedPnl, marginUsed), '100') + return self.safe_position({ + 'info': position, + 'id': None, + 'symbol': symbol, + 'timestamp': None, + 'datetime': None, + 'isolated': isIsolated, + 'hedged': None, + 'side': side, + 'contracts': self.parse_number(size), + 'contractSize': None, + 'entryPrice': self.safe_number(entry, 'entryPx'), + 'markPrice': None, + 'notional': self.safe_number(entry, 'positionValue'), + 'leverage': self.safe_number(leverage, 'value'), + 'collateral': self.parse_number(marginUsed), + 'initialMargin': self.parse_number(initialMargin), + 'maintenanceMargin': None, + 'initialMarginPercentage': None, + 'maintenanceMarginPercentage': None, + 'unrealizedPnl': self.parse_number(rawUnrealizedPnl), + 'liquidationPrice': self.safe_number(entry, 'liquidationPx'), + 'marginMode': marginMode, + 'percentage': self.parse_number(percentage), + }) + + async def set_margin_mode(self, marginMode: str, symbol: Str = None, params={}): + """ + set margin mode(symbol) + :param str marginMode: margin mode must be either [isolated, cross] + :param str symbol: unified market symbol of the market the position is held in, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.leverage]: the rate of leverage, is required if setting trade mode(symbol) + :param str [params.vaultAddress]: the vault address + :param str [params.subAccountAddress]: sub account user address + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setMarginMode() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + leverage = self.safe_integer(params, 'leverage') + if leverage is None: + raise ArgumentsRequired(self.id + ' setMarginMode() requires a leverage parameter') + asset = self.parse_to_int(market['baseId']) + isCross = (marginMode == 'cross') + nonce = self.milliseconds() + params = self.omit(params, ['leverage']) + updateAction: dict = { + 'type': 'updateLeverage', + 'asset': asset, + 'isCross': isCross, + 'leverage': leverage, + } + vaultAddress = None + vaultAddress, params = self.handle_option_and_params_2(params, 'setMarginMode', 'vaultAddress', 'subAccountAddress') + if vaultAddress is not None: + if vaultAddress.startswith('0x'): + vaultAddress = vaultAddress.replace('0x', '') + signature = self.sign_l1_action(updateAction, nonce, vaultAddress) + request: dict = { + 'action': updateAction, + 'nonce': nonce, + 'signature': signature, + # 'vaultAddress': vaultAddress, + } + if vaultAddress is not None: + request['vaultAddress'] = vaultAddress + response = await self.privatePostExchange(request) + # + # { + # 'response': { + # 'type': 'default' + # }, + # 'status': 'ok' + # } + # + return response + + async def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + :param float leverage: the rate of leverage + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: margin mode must be either [isolated, cross], default is cross + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setLeverage() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + marginMode = self.safe_string(params, 'marginMode', 'cross') + isCross = (marginMode == 'cross') + asset = self.parse_to_int(market['baseId']) + nonce = self.milliseconds() + params = self.omit(params, 'marginMode') + updateAction: dict = { + 'type': 'updateLeverage', + 'asset': asset, + 'isCross': isCross, + 'leverage': leverage, + } + vaultAddress = None + vaultAddress, params = self.handle_option_and_params_2(params, 'setLeverage', 'vaultAddress', 'subAccountAddress') + vaultAddress = self.format_vault_address(vaultAddress) + signature = self.sign_l1_action(updateAction, nonce, vaultAddress) + request: dict = { + 'action': updateAction, + 'nonce': nonce, + 'signature': signature, + # 'vaultAddress': vaultAddress, + } + if vaultAddress is not None: + params = self.omit(params, 'vaultAddress') + request['vaultAddress'] = vaultAddress + response = await self.privatePostExchange(request) + # + # { + # 'response': { + # 'type': 'default' + # }, + # 'status': 'ok' + # } + # + return response + + async def add_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + add margin + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#update-isolated-margin + + :param str symbol: unified market symbol + :param float amount: amount of margin to add + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.vaultAddress]: the vault address + :param str [params.subAccountAddress]: sub account user address + :returns dict: a `margin structure ` + """ + return await self.modify_margin_helper(symbol, amount, 'add', params) + + async def reduce_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#update-isolated-margin + + remove margin from a position + :param str symbol: unified market symbol + :param float amount: the amount of margin to remove + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.vaultAddress]: the vault address + :param str [params.subAccountAddress]: sub account user address + :returns dict: a `margin structure ` + """ + return await self.modify_margin_helper(symbol, amount, 'reduce', params) + + async def modify_margin_helper(self, symbol: str, amount, type, params={}) -> MarginModification: + await self.load_markets() + market = self.market(symbol) + asset = self.parse_to_int(market['baseId']) + sz = self.parse_to_int(Precise.string_mul(self.amount_to_precision(symbol, amount), '1000000')) + if type == 'reduce': + sz = -sz + nonce = self.milliseconds() + updateAction: dict = { + 'type': 'updateIsolatedMargin', + 'asset': asset, + 'isBuy': True, + 'ntli': sz, + } + vaultAddress = None + vaultAddress, params = self.handle_option_and_params_2(params, 'modifyMargin', 'vaultAddress', 'subAccountAddress') + vaultAddress = self.format_vault_address(vaultAddress) + signature = self.sign_l1_action(updateAction, nonce, vaultAddress) + request: dict = { + 'action': updateAction, + 'nonce': nonce, + 'signature': signature, + # 'vaultAddress': vaultAddress, + } + if vaultAddress is not None: + request['vaultAddress'] = vaultAddress + response = await self.privatePostExchange(request) + # + # { + # 'response': { + # 'type': 'default' + # }, + # 'status': 'ok' + # } + # + return self.extend(self.parse_margin_modification(response, market), { + 'code': self.safe_string(response, 'status'), + }) + + def parse_margin_modification(self, data: dict, market: Market = None) -> MarginModification: + # + # { + # 'type': 'default' + # } + # + return { + 'info': data, + 'symbol': self.safe_symbol(None, market), + 'type': None, + 'marginMode': 'isolated', + 'amount': None, + 'total': None, + 'code': self.safe_string(market, 'settle'), + 'status': None, + 'timestamp': None, + 'datetime': None, + } + + async def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#l1-usdc-transfer + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account to transfer from *spot, swap* + :param str toAccount: account to transfer to *swap, spot or address* + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.vaultAddress]: the vault address for order + :returns dict: a `transfer structure ` + """ + self.check_required_credentials() + await self.load_markets() + isSandboxMode = self.safe_bool(self.options, 'sandboxMode') + nonce = self.milliseconds() + if self.in_array(fromAccount, ['spot', 'swap', 'perp']): + # handle swap <> spot account transfer + if not self.in_array(toAccount, ['spot', 'swap', 'perp']): + raise NotSupported(self.id + ' transfer() only support spot <> swap transfer') + strAmount = self.number_to_string(amount) + vaultAddress = self.safe_string_2(params, 'vaultAddress', 'subAccountAddress') + if vaultAddress is not None: + vaultAddress = self.format_vault_address(vaultAddress) + strAmount = strAmount + ' subaccount:' + vaultAddress + toPerp = (toAccount == 'perp') or (toAccount == 'swap') + transferPayload: dict = { + 'hyperliquidChain': 'Testnet' if isSandboxMode else 'Mainnet', + 'amount': strAmount, + 'toPerp': toPerp, + 'nonce': nonce, + } + transferSig = self.build_usd_class_send_sig(transferPayload) + transferRequest: dict = { + 'action': { + 'hyperliquidChain': transferPayload['hyperliquidChain'], + 'signatureChainId': '0x66eee', + 'type': 'usdClassTransfer', + 'amount': strAmount, + 'toPerp': toPerp, + 'nonce': nonce, + }, + 'nonce': nonce, + 'signature': transferSig, + } + transferResponse = await self.privatePostExchange(transferRequest) + return transferResponse + # transfer between main account and subaccount + isDeposit = False + subAccountAddress = None + if fromAccount == 'main': + subAccountAddress = toAccount + isDeposit = True + elif toAccount == 'main': + subAccountAddress = fromAccount + else: + raise NotSupported(self.id + ' transfer() only support main <> subaccount transfer') + self.check_address(subAccountAddress) + if code is None or code.upper() == 'USDC': + # Transfer USDC with subAccountTransfer + usd = self.parse_to_int(Precise.string_mul(self.number_to_string(amount), '1000000')) + action = { + 'type': 'subAccountTransfer', + 'subAccountUser': subAccountAddress, + 'isDeposit': isDeposit, + 'usd': usd, + } + sig = self.sign_l1_action(action, nonce) + request: dict = { + 'action': action, + 'nonce': nonce, + 'signature': sig, + } + response = await self.privatePostExchange(request) + # + # {'response': {'type': 'default'}, 'status': 'ok'} + # + return self.parse_transfer(response) + else: + # Transfer non-USDC with subAccountSpotTransfer + symbol = self.symbol(code) + action = { + 'type': 'subAccountSpotTransfer', + 'subAccountUser': subAccountAddress, + 'isDeposit': isDeposit, + 'token': symbol, + 'amount': self.number_to_string(amount), + } + sig = self.sign_l1_action(action, nonce) + request: dict = { + 'action': action, + 'nonce': nonce, + 'signature': sig, + } + response = await self.privatePostExchange(request) + return self.parse_transfer(response) + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + # + # {'response': {'type': 'default'}, 'status': 'ok'} + # + return { + 'info': transfer, + 'id': None, + 'timestamp': None, + 'datetime': None, + 'currency': None, + 'amount': None, + 'fromAccount': None, + 'toAccount': None, + 'status': 'ok', + } + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal(only support USDC) + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#initiate-a-withdrawal-request + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#deposit-or-withdraw-from-a-vault + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.vaultAddress]: vault address withdraw from + :returns dict: a `transaction structure ` + """ + self.check_required_credentials() + await self.load_markets() + self.check_address(address) + if code is not None: + code = code.upper() + if code != 'USDC': + raise NotSupported(self.id + ' withdraw() only support USDC') + vaultAddress = None + vaultAddress, params = self.handle_option_and_params(params, 'withdraw', 'vaultAddress') + vaultAddress = self.format_vault_address(vaultAddress) + params = self.omit(params, 'vaultAddress') + nonce = self.milliseconds() + action: dict = {} + sig = None + if vaultAddress is not None: + action = { + 'type': 'vaultTransfer', + 'vaultAddress': '0x' + vaultAddress, + 'isDeposit': False, + 'usd': amount, + } + sig = self.sign_l1_action(action, nonce) + else: + isSandboxMode = self.safe_bool(self.options, 'sandboxMode', False) + payload: dict = { + 'hyperliquidChain': 'Testnet' if isSandboxMode else 'Mainnet', + 'destination': address, + 'amount': str(amount), + 'time': nonce, + } + sig = self.build_withdraw_sig(payload) + action = { + 'hyperliquidChain': payload['hyperliquidChain'], + 'signatureChainId': '0x66eee', # check self out + 'destination': address, + 'amount': str(amount), + 'time': nonce, + 'type': 'withdraw3', + } + request: dict = { + 'action': action, + 'nonce': nonce, + 'signature': sig, + } + response = await self.privatePostExchange(request) + return self.parse_transaction(response) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # {status: 'ok', response: {type: 'default'}} + # + # fetchDeposits / fetchWithdrawals + # { + # "time":1724762307531, + # "hash":"0x620a234a7e0eb7930575040f59482a01050058b0802163b4767bfd9033e77781", + # "delta":{ + # "type":"accountClassTransfer", + # "usdc":"50.0", + # "toPerp":false + # } + # } + # + timestamp = self.safe_integer(transaction, 'time') + delta = self.safe_dict(transaction, 'delta', {}) + fee = None + feeCost = self.safe_integer(delta, 'fee') + if feeCost is not None: + fee = { + 'currency': 'USDC', + 'cost': feeCost, + } + internal = None + type = self.safe_string(delta, 'type') + if type is not None: + internal = (type == 'internalTransfer') + return { + 'info': transaction, + 'id': None, + 'txid': self.safe_string(transaction, 'hash'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': None, + 'address': None, + 'addressTo': self.safe_string(delta, 'destination'), + 'addressFrom': self.safe_string(delta, 'user'), + 'tag': None, + 'tagTo': None, + 'tagFrom': None, + 'type': None, + 'amount': self.safe_number(delta, 'usdc'), + 'currency': None, + 'status': self.safe_string(transaction, 'status'), + 'updated': None, + 'comment': None, + 'internal': internal, + 'fee': fee, + } + + async def fetch_trading_fee(self, symbol: str, params={}) -> TradingFeeInterface: + """ + fetch the trading fees for a market + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.user]: user address, will default to self.walletAddress if not provided + :param str [params.subAccountAddress]: sub account user address + :returns dict: a `fee structure ` + """ + await self.load_markets() + userAddress = None + userAddress, params = self.handle_public_address('fetchTradingFee', params) + market = self.market(symbol) + request: dict = { + 'type': 'userFees', + 'user': userAddress, + } + response = await self.publicPostInfo(self.extend(request, params)) + # + # { + # "dailyUserVlm": [ + # { + # "date": "2024-07-08", + # "userCross": "0.0", + # "userAdd": "0.0", + # "exchange": "90597185.23639999" + # } + # ], + # "feeSchedule": { + # "cross": "0.00035", + # "add": "0.0001", + # "tiers": { + # "vip": [ + # { + # "ntlCutoff": "5000000.0", + # "cross": "0.0003", + # "add": "0.00005" + # } + # ], + # "mm": [ + # { + # "makerFractionCutoff": "0.005", + # "add": "-0.00001" + # } + # ] + # }, + # "referralDiscount": "0.04" + # }, + # "userCrossRate": "0.00035", + # "userAddRate": "0.0001", + # "activeReferralDiscount": "0.0" + # } + # + data: dict = { + 'userCrossRate': self.safe_string(response, 'userCrossRate'), + 'userAddRate': self.safe_string(response, 'userAddRate'), + } + return self.parse_trading_fee(data, market) + + def parse_trading_fee(self, fee: dict, market: Market = None) -> TradingFeeInterface: + # + # { + # "dailyUserVlm": [ + # { + # "date": "2024-07-08", + # "userCross": "0.0", + # "userAdd": "0.0", + # "exchange": "90597185.23639999" + # } + # ], + # "feeSchedule": { + # "cross": "0.00035", + # "add": "0.0001", + # "tiers": { + # "vip": [ + # { + # "ntlCutoff": "5000000.0", + # "cross": "0.0003", + # "add": "0.00005" + # } + # ], + # "mm": [ + # { + # "makerFractionCutoff": "0.005", + # "add": "-0.00001" + # } + # ] + # }, + # "referralDiscount": "0.04" + # }, + # "userCrossRate": "0.00035", + # "userAddRate": "0.0001", + # "activeReferralDiscount": "0.0" + # } + # + symbol = self.safe_symbol(None, market) + return { + 'info': fee, + 'symbol': symbol, + 'maker': self.safe_number(fee, 'userAddRate'), + 'taker': self.safe_number(fee, 'userCrossRate'), + 'percentage': None, + 'tierBased': None, + } + + async def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + :param str [code]: unified currency code + :param int [since]: timestamp in ms of the earliest ledger entry + :param int [limit]: max number of ledger entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest ledger entry + :param str [params.subAccountAddress]: sub account user address + :returns dict: a `ledger structure ` + """ + await self.load_markets() + userAddress = None + userAddress, params = self.handle_public_address('fetchLedger', params) + request: dict = { + 'type': 'userNonFundingLedgerUpdates', + 'user': userAddress, + } + if since is not None: + request['startTime'] = since + until = self.safe_integer(params, 'until') + if until is not None: + request['endTime'] = until + params = self.omit(params, ['until']) + response = await self.publicPostInfo(self.extend(request, params)) + # + # [ + # { + # "time":1724762307531, + # "hash":"0x620a234a7e0eb7930575040f59482a01050058b0802163b4767bfd9033e77781", + # "delta":{ + # "type":"accountClassTransfer", + # "usdc":"50.0", + # "toPerp":false + # } + # } + # ] + # + return self.parse_ledger(response, None, since, limit) + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + # + # { + # "time":1724762307531, + # "hash":"0x620a234a7e0eb7930575040f59482a01050058b0802163b4767bfd9033e77781", + # "delta":{ + # "type":"accountClassTransfer", + # "usdc":"50.0", + # "toPerp":false + # } + # } + # + timestamp = self.safe_integer(item, 'time') + delta = self.safe_dict(item, 'delta', {}) + fee = None + feeCost = self.safe_integer(delta, 'fee') + if feeCost is not None: + fee = { + 'currency': 'USDC', + 'cost': feeCost, + } + type = self.safe_string(delta, 'type') + amount = self.safe_string(delta, 'usdc') + return self.safe_ledger_entry({ + 'info': item, + 'id': self.safe_string(item, 'hash'), + 'direction': None, + 'account': None, + 'referenceAccount': self.safe_string(delta, 'user'), + 'referenceId': self.safe_string(item, 'hash'), + 'type': self.parse_ledger_entry_type(type), + 'currency': None, + 'amount': self.parse_number(amount), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'before': None, + 'after': None, + 'status': None, + 'fee': fee, + }, currency) + + def parse_ledger_entry_type(self, type): + ledgerType: dict = { + 'internalTransfer': 'transfer', + 'accountClassTransfer': 'transfer', + } + return self.safe_string(ledgerType, type, type) + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all deposits made to an account + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch withdrawals for + :param str [params.subAccountAddress]: sub account user address + :param str [params.vaultAddress]: vault address + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + userAddress = None + userAddress, params = self.handle_public_address('fetchDepositsWithdrawals', params) + request: dict = { + 'type': 'userNonFundingLedgerUpdates', + 'user': userAddress, + } + if since is not None: + request['startTime'] = since + until = self.safe_integer(params, 'until') + if until is not None: + if since is None: + raise ArgumentsRequired(self.id + ' fetchDeposits requires since while until is set') + request['endTime'] = until + params = self.omit(params, ['until']) + response = await self.publicPostInfo(self.extend(request, params)) + # + # [ + # { + # "time":1724762307531, + # "hash":"0x620a234a7e0eb7930575040f59482a01050058b0802163b4767bfd9033e77781", + # "delta":{ + # "type":"accountClassTransfer", + # "usdc":"50.0", + # "toPerp":false + # } + # } + # ] + # + records = self.extract_type_from_delta(response) + vaultAddress = None + vaultAddress, params = self.handle_option_and_params(params, 'fetchDepositsWithdrawals', 'vaultAddress') + vaultAddress = self.format_vault_address(vaultAddress) + deposits = [] + if vaultAddress is not None: + for i in range(0, len(records)): + record = records[i] + if record['type'] == 'vaultDeposit': + delta = self.safe_dict(record, 'delta') + if delta['vault'] == '0x' + vaultAddress: + deposits.append(record) + else: + deposits = self.filter_by_array(records, 'type', ['deposit'], False) + return self.parse_transactions(deposits, None, since, limit) + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch withdrawals for + :param str [params.subAccountAddress]: sub account user address + :param str [params.vaultAddress]: vault address + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + userAddress = None + userAddress, params = self.handle_public_address('fetchDepositsWithdrawals', params) + request: dict = { + 'type': 'userNonFundingLedgerUpdates', + 'user': userAddress, + } + if since is not None: + request['startTime'] = since + until = self.safe_integer(params, 'until') + if until is not None: + request['endTime'] = until + params = self.omit(params, ['until']) + response = await self.publicPostInfo(self.extend(request, params)) + # + # [ + # { + # "time":1724762307531, + # "hash":"0x620a234a7e0eb7930575040f59482a01050058b0802163b4767bfd9033e77781", + # "delta":{ + # "type":"accountClassTransfer", + # "usdc":"50.0", + # "toPerp":false + # } + # } + # ] + # + records = self.extract_type_from_delta(response) + vaultAddress = None + vaultAddress, params = self.handle_option_and_params(params, 'fetchDepositsWithdrawals', 'vaultAddress') + vaultAddress = self.format_vault_address(vaultAddress) + withdrawals = [] + if vaultAddress is not None: + for i in range(0, len(records)): + record = records[i] + if record['type'] == 'vaultWithdraw': + delta = self.safe_dict(record, 'delta') + if delta['vault'] == '0x' + vaultAddress: + withdrawals.append(record) + else: + withdrawals = self.filter_by_array(records, 'type', ['withdraw'], False) + return self.parse_transactions(withdrawals, None, since, limit) + + async def fetch_open_interests(self, symbols: Strings = None, params={}): + """ + Retrieves the open interest for a list of symbols + :param str[] [symbols]: Unified CCXT market symbol + :param dict [params]: exchange specific parameters + :returns dict} an open interest structure{@link https://docs.ccxt.com/#/?id=open-interest-structure: + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + swapMarkets = await self.fetch_swap_markets() + return self.parse_open_interests(swapMarkets, symbols) + + async def fetch_open_interest(self, symbol: str, params={}): + """ + retrieves the open interest of a contract trading pair + :param str symbol: unified CCXT market symbol + :param dict [params]: exchange specific parameters + :returns dict: an `open interest structure ` + """ + symbol = self.symbol(symbol) + await self.load_markets() + ois = await self.fetch_open_interests([symbol], params) + return ois[symbol] + + def parse_open_interest(self, interest, market: Market = None): + # + # { + # szDecimals: '2', + # name: 'HYPE', + # maxLeverage: '3', + # funding: '0.00014735', + # openInterest: '14677900.74', + # prevDayPx: '26.145', + # dayNtlVlm: '299643445.12560016', + # premium: '0.00081613', + # oraclePx: '27.569', + # markPx: '27.63', + # midPx: '27.599', + # impactPxs: ['27.5915', '27.6319'], + # dayBaseVlm: '10790652.83', + # baseId: 159 + # } + # + interest = self.safe_dict(interest, 'info', {}) + coin = self.safe_string(interest, 'name') + marketId = None + if coin is not None: + marketId = self.coin_to_market_id(coin) + return self.safe_open_interest({ + 'symbol': self.safe_symbol(marketId), + 'openInterestAmount': self.safe_number(interest, 'openInterest'), + 'openInterestValue': None, + 'timestamp': None, + 'datetime': None, + 'info': interest, + }, market) + + async def fetch_funding_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch the history of funding payments paid and received on self account + :param str [symbol]: unified market symbol + :param int [since]: the earliest time in ms to fetch funding history for + :param int [limit]: the maximum number of funding history structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.subAccountAddress]: sub account user address + :returns dict: a `funding history structure ` + """ + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + userAddress = None + userAddress, params = self.handle_public_address('fetchFundingHistory', params) + request: dict = { + 'user': userAddress, + 'type': 'userFunding', + } + if since is not None: + request['startTime'] = since + until = self.safe_integer(params, 'until') + params = self.omit(params, 'until') + if until is not None: + request['endTime'] = until + response = await self.publicPostInfo(self.extend(request, params)) + # + # [ + # { + # "time": 1734026400057, + # "hash": "0x0000000000000000000000000000000000000000000000000000000000000000", + # "delta": { + # "type": "funding", + # "coin": "SOL", + # "usdc": "75.635093", + # "szi": "-7375.9", + # "fundingRate": "0.00004381", + # "nSamples": null + # } + # } + # ] + # + return self.parse_incomes(response, market, since, limit) + + def parse_income(self, income, market: Market = None): + # + # { + # "time": 1734026400057, + # "hash": "0x0000000000000000000000000000000000000000000000000000000000000000", + # "delta": { + # "type": "funding", + # "coin": "SOL", + # "usdc": "75.635093", + # "szi": "-7375.9", + # "fundingRate": "0.00004381", + # "nSamples": null + # } + # } + # + id = self.safe_string(income, 'hash') + timestamp = self.safe_integer(income, 'time') + delta = self.safe_dict(income, 'delta') + baseId = self.safe_string(delta, 'coin') + marketSymbol = baseId + '/USDC:USDC' + market = self.safe_market(marketSymbol) + symbol = market['symbol'] + amount = self.safe_string(delta, 'usdc') + code = self.safe_currency_code('USDC') + rate = self.safe_number(delta, 'fundingRate') + return { + 'info': income, + 'symbol': symbol, + 'code': code, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': id, + 'amount': self.parse_number(amount), + 'rate': rate, + } + + async def reserve_request_weight(self, weight: Num, params={}) -> dict: + """ + Instead of trading to increase the address based rate limits, self action allows reserving additional actions for 0.0005 USDC per request. The cost is paid from the Perps balance. + :param number weight: the weight to reserve, 1 weight = 1 action, 0.0005 USDC per action + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a response object + """ + nonce = self.milliseconds() + request: dict = { + 'nonce': nonce, + } + action: dict = { + 'type': 'reserveRequestWeight', + 'weight': weight, + } + signature = self.sign_l1_action(action, nonce) + request['action'] = action + request['signature'] = signature + response = await self.privatePostExchange(self.extend(request, params)) + return response + + def extract_type_from_delta(self, data=[]): + records = [] + for i in range(0, len(data)): + record = data[i] + record['type'] = record['delta']['type'] + records.append(record) + return records + + def format_vault_address(self, address: Str = None): + if address is None: + return None + if address.startswith('0x'): + return address.replace('0x', '') + return address + + def handle_public_address(self, methodName: str, params: dict): + userAux = None + userAux, params = self.handle_option_and_params_2(params, methodName, 'user', 'subAccountAddress') + user = userAux + user, params = self.handle_option_and_params(params, methodName, 'address', userAux) + if (user is not None) and (user != ''): + return [user, params] + if (self.walletAddress is not None) and (self.walletAddress != ''): + return [self.walletAddress, params] + raise ArgumentsRequired(self.id + ' ' + methodName + '() requires a user parameter inside \'params\' or the wallet address set') + + def coin_to_market_id(self, coin: Str): + if coin.find('/') > -1 or coin.find('@') > -1: + return coin # spot + return self.safe_currency_code(coin) + '/USDC:USDC' + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if not response: + return None # fallback to default error handler + # {"status":"err","response":"User or API Wallet 0xb8a6f8b26223de27c31938d56e470a5b832703a5 does not exist."} + # + # { + # status: 'ok', + # response: {type: 'order', data: {statuses: [{error: 'Insufficient margin to place order. asset=4'}]}} + # } + # {"status":"ok","response":{"type":"order","data":{"statuses":[{"error":"Insufficient margin to place order. asset=84"}]}}} + # + # {"status":"unknownOid"} + # + status = self.safe_string(response, 'status', '') + error = self.safe_string(response, 'error') + message = None + if status == 'err': + message = self.safe_string(response, 'response') + elif status == 'unknownOid': + raise OrderNotFound(self.id + ' ' + body) # {"status":"unknownOid"} + elif error is not None: + message = error + else: + responsePayload = self.safe_dict(response, 'response', {}) + data = self.safe_dict(responsePayload, 'data', {}) + statuses = self.safe_list(data, 'statuses', []) + for i in range(0, len(statuses)): + message = self.safe_string(statuses[i], 'error') + if message is not None: + break + feedback = self.id + ' ' + body + nonEmptyMessage = ((message is not None) and (message != '')) + if nonEmptyMessage: + self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + if nonEmptyMessage: + raise ExchangeError(feedback) # unknown message + return None + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = self.implode_hostname(self.urls['api'][api]) + '/' + path + if method == 'POST': + headers = { + 'Content-Type': 'application/json', + } + body = self.json(params) + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def calculate_rate_limiter_cost(self, api, method, path, params, config={}): + if ('byType' in config) and ('type' in params): + type = params['type'] + byType = config['byType'] + if type in byType: + return byType[type] + return self.safe_value(config, 'cost', 1) + + def parse_create_edit_order_args(self, id: Str, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + market = self.market(symbol) + vaultAddress = None + vaultAddress, params = self.handle_option_and_params_2(params, 'createOrder', 'vaultAddress', 'subAccountAddress') + vaultAddress = self.format_vault_address(vaultAddress) + symbol = market['symbol'] + order = { + 'symbol': symbol, + 'type': type, + 'side': side, + 'amount': amount, + 'price': price, + 'params': params, + } + globalParams = {} + if vaultAddress is not None: + globalParams['vaultAddress'] = vaultAddress + if id is not None: + order['id'] = id + return [order, globalParams] diff --git a/ccxt/async_support/independentreserve.py b/ccxt/async_support/independentreserve.py new file mode 100644 index 0000000..e113aa1 --- /dev/null +++ b/ccxt/async_support/independentreserve.py @@ -0,0 +1,1058 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.independentreserve import ImplicitAPI +import asyncio +import hashlib +from ccxt.base.types import Any, Balances, Currency, DepositAddress, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Ticker, Trade, TradingFees, Transaction +from typing import List +from ccxt.base.errors import BadRequest +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class independentreserve(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(independentreserve, self).describe(), { + 'id': 'independentreserve', + 'name': 'Independent Reserve', + 'countries': ['AU', 'NZ'], # Australia, New Zealand + 'rateLimit': 1000, + 'pro': True, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelOrder': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createOrder': True, + 'createReduceOnlyOrder': False, + 'createStopLimitOrder': False, + 'createStopMarketOrder': False, + 'createStopOrder': False, + 'fetchAllGreeks': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': False, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrice': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchPosition': False, + 'fetchPositionForSymbolWs': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsForSymbolWs': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': True, + 'fetchUnderlyingAssets': False, + 'fetchVolatilityHistory': False, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'withdraw': True, + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/51840849/87182090-1e9e9080-c2ec-11ea-8e49-563db9a38f37.jpg', + 'api': { + 'public': 'https://api.independentreserve.com/Public', + 'private': 'https://api.independentreserve.com/Private', + }, + 'www': 'https://www.independentreserve.com', + 'doc': 'https://www.independentreserve.com/API', + }, + 'api': { + 'public': { + 'get': [ + 'GetValidPrimaryCurrencyCodes', + 'GetValidSecondaryCurrencyCodes', + 'GetValidLimitOrderTypes', + 'GetValidMarketOrderTypes', + 'GetValidOrderTypes', + 'GetValidTransactionTypes', + 'GetMarketSummary', + 'GetOrderBook', + 'GetAllOrders', + 'GetTradeHistorySummary', + 'GetRecentTrades', + 'GetFxRates', + 'GetOrderMinimumVolumes', + 'GetCryptoWithdrawalFees', # deprecated - replaced by GetCryptoWithdrawalFees2(docs removed) + 'GetCryptoWithdrawalFees2', + 'GetNetworks', + 'GetPrimaryCurrencyConfig2', + ], + }, + 'private': { + 'post': [ + 'GetOpenOrders', + 'GetClosedOrders', + 'GetClosedFilledOrders', + 'GetOrderDetails', + 'GetAccounts', + 'GetTransactions', + 'GetFiatBankAccounts', + 'GetDigitalCurrencyDepositAddress', # deprecated - replaced by GetDigitalCurrencyDepositAddress2(docs removed) + 'GetDigitalCurrencyDepositAddress2', + 'GetDigitalCurrencyDepositAddresses', # deprecated - replaced by GetDigitalCurrencyDepositAddresses2(docs removed) + 'GetDigitalCurrencyDepositAddresses2', + 'GetTrades', + 'GetBrokerageFees', + 'GetDigitalCurrencyWithdrawal', + 'PlaceLimitOrder', + 'PlaceMarketOrder', + 'CancelOrder', + 'SynchDigitalCurrencyDepositAddressWithBlockchain', + 'RequestFiatWithdrawal', + 'WithdrawFiatCurrency', + 'WithdrawDigitalCurrency', # deprecated - replaced by WithdrawCrypto(docs removed) + 'WithdrawCrypto', + ], + }, + }, + 'fees': { + 'trading': { + 'taker': self.parse_number('0.005'), + 'maker': self.parse_number('0.005'), + 'percentage': True, + 'tierBased': False, + }, + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': False, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': False, + 'FOK': False, + 'PO': False, + 'GTD': False, + }, + 'hedged': False, + 'selfTradePrevention': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': False, + 'marketBuyRequiresPrice': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, # todo + 'daysBack': None, + 'untilDays': None, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 100, # todo + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 100, # todo + 'daysBack': None, + 'daysBackCanceled': None, + 'untilDays': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': None, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'commonCurrencies': { + 'PLA': 'PlayChip', + }, + 'precisionMode': TICK_SIZE, + 'options': { + 'defaultNetworks': { + 'USDT': 'Ethereum', + 'USDC': 'Ethereum', + 'BTC': 'Bitcoin', + 'BCH': 'BitcoinCash', + 'ETH': 'Ethereum', + 'LTC': 'Litecoin', + 'XRP': 'XrpLedger', + 'ZRX': 'Ethereum', + 'EOS': 'EosIo', + 'XLM': 'Stellar', + 'BAT': 'Ethereum', + 'ETC': 'EthereumClassic', + 'LINK': 'Ethereum', + 'MKR': 'Ethereum', + 'DAI': 'Ethereum', + 'COMP': 'Ethereum', + 'SNX': 'Ethereum', + 'YFI': 'Ethereum', + 'AAVE': 'Ethereum', + 'GRT': 'Ethereum', + 'DOT': 'Polkadot', + 'UNI': 'Ethereum', + 'ADA': 'Cardano', + 'MATIC': 'Ethereum', + 'DOGE': 'Dogecoin', + 'SOL': 'Solana', + 'MANA': 'Ethereum', + 'SAND': 'Ethereum', + 'SHIB': 'Ethereum', + 'TRX': 'Tron', + 'RENDER': 'Solana', + 'WIF': 'Solana', + 'RLUSD': 'Ethereum', + 'PEPE': 'Ethereum', + }, + 'networks': { + 'BTC': 'Bitcoin', + 'ETH': 'Ethereum', + 'BCH': 'BitcoinCash', + 'LTC': 'Litecoin', + 'XRP': 'XrpLedger', + 'EOS': 'EosIo', + 'XLM': 'Stellar', + 'ETC': 'EthereumClassic', + 'BSV': 'BitcoinSV', + 'DOGE': 'Dogecoin', + 'DOT': 'Polkadot', + 'ADA': 'Cardano', + 'SOL': 'Solana', + 'TRX': 'Tron', + }, + }, + }) + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for independentreserve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + baseCurrenciesPromise = self.publicGetGetValidPrimaryCurrencyCodes(params) + # ['Xbt', 'Eth', 'Usdt', ...] + quoteCurrenciesPromise = self.publicGetGetValidSecondaryCurrencyCodes(params) + # ['Aud', 'Usd', 'Nzd', 'Sgd'] + limitsPromise = self.publicGetGetOrderMinimumVolumes(params) + baseCurrencies, quoteCurrencies, limits = await asyncio.gather(*[baseCurrenciesPromise, quoteCurrenciesPromise, limitsPromise]) + # + # { + # "Xbt": 0.0001, + # "Eth": 0.001, + # "Ltc": 0.01, + # "Xrp": 1.0, + # } + # + result = [] + for i in range(0, len(baseCurrencies)): + baseId = baseCurrencies[i] + base = self.safe_currency_code(baseId) + minAmount = self.safe_number(limits, baseId) + for j in range(0, len(quoteCurrencies)): + quoteId = quoteCurrencies[j] + quote = self.safe_currency_code(quoteId) + id = baseId + '/' + quoteId + result.append({ + 'id': id, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': None, + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': None, + 'price': None, + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': minAmount, + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + 'info': id, + }) + return result + + def parse_balance(self, response) -> Balances: + result: dict = {'info': response} + for i in range(0, len(response)): + balance = response[i] + currencyId = self.safe_string(balance, 'CurrencyCode') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(balance, 'AvailableBalance') + account['total'] = self.safe_string(balance, 'TotalBalance') + result[code] = account + return self.safe_balance(result) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + response = await self.privatePostGetAccounts(params) + return self.parse_balance(response) + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'primaryCurrencyCode': market['baseId'], + 'secondaryCurrencyCode': market['quoteId'], + } + response = await self.publicGetGetOrderBook(self.extend(request, params)) + timestamp = self.parse8601(self.safe_string(response, 'CreatedTimestampUtc')) + return self.parse_order_book(response, market['symbol'], timestamp, 'BuyOrders', 'SellOrders', 'Price', 'Volume') + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # { + # "DayHighestPrice":43489.49, + # "DayLowestPrice":41998.32, + # "DayAvgPrice":42743.9, + # "DayVolumeXbt":44.54515625000, + # "DayVolumeXbtInSecondaryCurrrency":0.12209818, + # "CurrentLowestOfferPrice":43619.64, + # "CurrentHighestBidPrice":43153.58, + # "LastPrice":43378.43, + # "PrimaryCurrencyCode":"Xbt", + # "SecondaryCurrencyCode":"Usd", + # "CreatedTimestampUtc":"2022-01-14T22:52:29.5029223Z" + # } + timestamp = self.parse8601(self.safe_string(ticker, 'CreatedTimestampUtc')) + baseId = self.safe_string(ticker, 'PrimaryCurrencyCode') + quoteId = self.safe_string(ticker, 'SecondaryCurrencyCode') + defaultMarketId = None + if (baseId is not None) and (quoteId is not None): + defaultMarketId = baseId + '/' + quoteId + market = self.safe_market(defaultMarketId, market, '/') + symbol = market['symbol'] + last = self.safe_string(ticker, 'LastPrice') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'DayHighestPrice'), + 'low': self.safe_string(ticker, 'DayLowestPrice'), + 'bid': self.safe_string(ticker, 'CurrentHighestBidPrice'), + 'bidVolume': None, + 'ask': self.safe_string(ticker, 'CurrentLowestOfferPrice'), + 'askVolume': None, + 'vwap': None, + 'open': None, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': self.safe_string(ticker, 'DayAvgPrice'), + 'baseVolume': self.safe_string(ticker, 'DayVolumeXbtInSecondaryCurrrency'), + 'quoteVolume': None, + 'info': ticker, + }, market) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'primaryCurrencyCode': market['baseId'], + 'secondaryCurrencyCode': market['quoteId'], + } + response = await self.publicGetGetMarketSummary(self.extend(request, params)) + # { + # "DayHighestPrice":43489.49, + # "DayLowestPrice":41998.32, + # "DayAvgPrice":42743.9, + # "DayVolumeXbt":44.54515625000, + # "DayVolumeXbtInSecondaryCurrrency":0.12209818, + # "CurrentLowestOfferPrice":43619.64, + # "CurrentHighestBidPrice":43153.58, + # "LastPrice":43378.43, + # "PrimaryCurrencyCode":"Xbt", + # "SecondaryCurrencyCode":"Usd", + # "CreatedTimestampUtc":"2022-01-14T22:52:29.5029223Z" + # } + return self.parse_ticker(response, market) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # fetchOrder + # + # { + # "OrderGuid": "c7347e4c-b865-4c94-8f74-d934d4b0b177", + # "CreatedTimestampUtc": "2014-09-23T12:39:34.3817763Z", + # "Type": "MarketBid", + # "VolumeOrdered": 5.0, + # "VolumeFilled": 5.0, + # "Price": null, + # "AvgPrice": 100.0, + # "ReservedAmount": 0.0, + # "Status": "Filled", + # "PrimaryCurrencyCode": "Xbt", + # "SecondaryCurrencyCode": "Usd" + # } + # + # fetchOpenOrders & fetchClosedOrders + # + # { + # "OrderGuid": "b8f7ad89-e4e4-4dfe-9ea3-514d38b5edb3", + # "CreatedTimestampUtc": "2020-09-08T03:04:18.616367Z", + # "OrderType": "LimitOffer", + # "Volume": 0.0005, + # "Outstanding": 0.0005, + # "Price": 113885.83, + # "AvgPrice": 113885.83, + # "Value": 56.94, + # "Status": "Open", + # "PrimaryCurrencyCode": "Xbt", + # "SecondaryCurrencyCode": "Usd", + # "FeePercent": 0.005, + # } + # + # cancelOrder + # + # { + # "AvgPrice": 455.48, + # "CreatedTimestampUtc": "2022-08-05T06:42:11.3032208Z", + # "OrderGuid": "719c495c-a39e-4884-93ac-280b37245037", + # "Price": 485.76, + # "PrimaryCurrencyCode": "Xbt", + # "ReservedAmount": 0.358, + # "SecondaryCurrencyCode": "Usd", + # "Status": "Cancelled", + # "Type": "LimitOffer", + # "VolumeFilled": 0, + # "VolumeOrdered": 0.358 + # } + symbol = None + baseId = self.safe_string(order, 'PrimaryCurrencyCode') + quoteId = self.safe_string(order, 'SecondaryCurrencyCode') + base = None + quote = None + if (baseId is not None) and (quoteId is not None): + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + symbol = base + '/' + quote + elif market is not None: + symbol = market['symbol'] + base = market['base'] + quote = market['quote'] + orderType = self.safe_string_2(order, 'Type', 'OrderType') + side = None + if orderType is not None: + if orderType.find('Bid') >= 0: + side = 'buy' + elif orderType.find('Offer') >= 0: + side = 'sell' + if orderType.find('Market') >= 0: + orderType = 'market' + elif orderType.find('Limit') >= 0: + orderType = 'limit' + timestamp = self.parse8601(self.safe_string(order, 'CreatedTimestampUtc')) + filled = self.safe_string(order, 'VolumeFilled') + feeRate = self.safe_string(order, 'FeePercent') + feeCost = None + if feeRate is not None and filled is not None: + feeCost = Precise.string_mul(feeRate, filled) + return self.safe_order({ + 'info': order, + 'id': self.safe_string(order, 'OrderGuid'), + 'clientOrderId': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'symbol': symbol, + 'type': orderType, + 'timeInForce': None, + 'postOnly': None, + 'side': side, + 'price': self.safe_string(order, 'Price'), + 'triggerPrice': None, + 'cost': self.safe_string(order, 'Value'), + 'average': self.safe_string(order, 'AvgPrice'), + 'amount': self.safe_string_2(order, 'VolumeOrdered', 'Volume'), + 'filled': filled, + 'remaining': self.safe_string(order, 'Outstanding'), + 'status': self.parse_order_status(self.safe_string(order, 'Status')), + 'fee': { + 'rate': feeRate, + 'cost': feeCost, + 'currency': base, + }, + 'trades': None, + }, market) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'Open': 'open', + 'PartiallyFilled': 'open', + 'Filled': 'closed', + 'PartiallyFilledAndCancelled': 'canceled', + 'Cancelled': 'canceled', + 'PartiallyFilledAndExpired': 'canceled', + 'Expired': 'canceled', + } + return self.safe_string(statuses, status, status) + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + response = await self.privatePostGetOrderDetails(self.extend({ + 'orderGuid': id, + }, params)) + market = None + if symbol is not None: + market = self.market(symbol) + return self.parse_order(response, market) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + request = self.ordered({}) + market = None + if symbol is not None: + market = self.market(symbol) + request['primaryCurrencyCode'] = market['baseId'] + request['secondaryCurrencyCode'] = market['quoteId'] + if limit is None: + limit = 50 + request['pageIndex'] = 1 + request['pageSize'] = limit + response = await self.privatePostGetOpenOrders(self.extend(request, params)) + data = self.safe_list(response, 'Data', []) + return self.parse_orders(data, market, since, limit) + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + request = self.ordered({}) + market = None + if symbol is not None: + market = self.market(symbol) + request['primaryCurrencyCode'] = market['baseId'] + request['secondaryCurrencyCode'] = market['quoteId'] + if limit is None: + limit = 50 + request['pageIndex'] = 1 + request['pageSize'] = limit + response = await self.privatePostGetClosedOrders(self.extend(request, params)) + data = self.safe_list(response, 'Data', []) + return self.parse_orders(data, market, since, limit) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = 50, params={}): + """ + fetch all trades made by the user + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + pageIndex = self.safe_integer(params, 'pageIndex', 1) + if limit is None: + limit = 50 + request = self.ordered({ + 'pageIndex': pageIndex, + 'pageSize': limit, + }) + response = await self.privatePostGetTrades(self.extend(request, params)) + market = None + if symbol is not None: + market = self.market(symbol) + return self.parse_trades(response['Data'], market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + timestamp = self.parse8601(trade['TradeTimestampUtc']) + id = self.safe_string(trade, 'TradeGuid') + orderId = self.safe_string(trade, 'OrderGuid') + priceString = self.safe_string_2(trade, 'Price', 'SecondaryCurrencyTradePrice') + amountString = self.safe_string_2(trade, 'VolumeTraded', 'PrimaryCurrencyAmount') + price = self.parse_number(priceString) + amount = self.parse_number(amountString) + cost = self.parse_number(Precise.string_mul(priceString, amountString)) + baseId = self.safe_string(trade, 'PrimaryCurrencyCode') + quoteId = self.safe_string(trade, 'SecondaryCurrencyCode') + marketId = None + if (baseId is not None) and (quoteId is not None): + marketId = baseId + '/' + quoteId + symbol = self.safe_symbol(marketId, market, '/') + side = self.safe_string(trade, 'OrderType') + if side is not None: + if side.find('Bid') >= 0: + side = 'buy' + elif side.find('Offer') >= 0: + side = 'sell' + return self.safe_trade({ + 'id': id, + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'order': orderId, + 'type': None, + 'side': side, + 'takerOrMaker': None, + 'price': price, + 'amount': amount, + 'cost': cost, + 'fee': None, + }, market) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'primaryCurrencyCode': market['baseId'], + 'secondaryCurrencyCode': market['quoteId'], + 'numberOfRecentTradesToRetrieve': 50, # max = 50 + } + response = await self.publicGetGetRecentTrades(self.extend(request, params)) + return self.parse_trades(response['Trades'], market, since, limit) + + async def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + await self.load_markets() + response = await self.privatePostGetBrokerageFees(params) + # + # [ + # { + # "CurrencyCode": "Xbt", + # "Fee": 0.005 + # } + # ... + # ] + # + fees: dict = {} + for i in range(0, len(response)): + fee = response[i] + currencyId = self.safe_string(fee, 'CurrencyCode') + code = self.safe_currency_code(currencyId) + tradingFee = self.safe_number(fee, 'Fee') + fees[code] = { + 'info': fee, + 'fee': tradingFee, + } + result: dict = {} + for i in range(0, len(self.symbols)): + symbol = self.symbols[i] + market = self.market(symbol) + fee = self.safe_value(fees, market['base'], {}) + result[symbol] = { + 'info': self.safe_value(fee, 'info'), + 'symbol': symbol, + 'maker': self.safe_number(fee, 'fee'), + 'taker': self.safe_number(fee, 'fee'), + 'percentage': True, + 'tierBased': True, + } + return result + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + orderType = self.capitalize(type) + orderType += 'Offer' if (side == 'sell') else 'Bid' + request = self.ordered({ + 'primaryCurrencyCode': market['baseId'], + 'secondaryCurrencyCode': market['quoteId'], + 'orderType': orderType, + }) + response = None + request['volume'] = amount + if type == 'limit': + request['price'] = price + response = await self.privatePostPlaceLimitOrder(self.extend(request, params)) + else: + response = await self.privatePostPlaceMarketOrder(self.extend(request, params)) + return self.safe_order({ + 'info': response, + 'id': response['OrderGuid'], + }, market) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://www.independentreserve.com/features/api#CancelOrder + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + request: dict = { + 'orderGuid': id, + } + response = await self.privatePostCancelOrder(self.extend(request, params)) + # + # { + # "AvgPrice": 455.48, + # "CreatedTimestampUtc": "2022-08-05T06:42:11.3032208Z", + # "OrderGuid": "719c495c-a39e-4884-93ac-280b37245037", + # "Price": 485.76, + # "PrimaryCurrencyCode": "Xbt", + # "ReservedAmount": 0.358, + # "SecondaryCurrencyCode": "Usd", + # "Status": "Cancelled", + # "Type": "LimitOffer", + # "VolumeFilled": 0, + # "VolumeOrdered": 0.358 + # } + # + return self.parse_order(response) + + async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://www.independentreserve.com/features/api#GetDigitalCurrencyDepositAddress + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'primaryCurrencyCode': currency['id'], + } + response = await self.privatePostGetDigitalCurrencyDepositAddress(self.extend(request, params)) + # + # { + # Tag: '3307446684', + # DepositAddress: 'GCCQH4HACMRAD56EZZZ4TOIDQQRVNADMJ35QOFWF4B2VQGODMA2WVQ22', + # LastCheckedTimestampUtc: '2024-02-20T11:13:35.6912985Z', + # NextUpdateTimestampUtc: '2024-02-20T11:14:56.5112394Z' + # } + # + return self.parse_deposit_address(response) + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + # + # { + # Tag: '3307446684', + # DepositAddress: 'GCCQH4HACMRAD56EZZZ4TOIDQQRVNADMJ35QOFWF4B2VQGODMA2WVQ22', + # LastCheckedTimestampUtc: '2024-02-20T11:13:35.6912985Z', + # NextUpdateTimestampUtc: '2024-02-20T11:14:56.5112394Z' + # } + # + address = self.safe_string(depositAddress, 'DepositAddress') + self.check_address(address) + return { + 'info': depositAddress, + 'currency': self.safe_string(currency, 'code'), + 'network': None, + 'address': address, + 'tag': self.safe_string(depositAddress, 'Tag'), + } + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://www.independentreserve.com/features/api#WithdrawDigitalCurrency + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + + EXCHANGE SPECIFIC PARAMETERS + :param dict [params.comment]: withdrawal comment, should not exceed 500 characters + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'primaryCurrencyCode': currency['id'], + 'withdrawalAddress': address, + 'amount': self.currency_to_precision(code, amount), + } + if tag is not None: + request['destinationTag'] = tag + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + if networkCode is not None: + raise BadRequest(self.id + ' withdraw() does not accept params["networkCode"]') + response = await self.privatePostWithdrawDigitalCurrency(self.extend(request, params)) + # + # { + # "TransactionGuid": "dc932e19-562b-4c50-821e-a73fd048b93b", + # "PrimaryCurrencyCode": "Bch", + # "CreatedTimestampUtc": "2020-04-01T05:26:30.5093622+00:00", + # "Amount": { + # "Total": 0.1231, + # "Fee": 0.0001 + # }, + # "Destination": { + # "Address": "bc1qhpqxkjpvgkckw530yfmxyr53c94q8f4273a7ez", + # "Tag": null + # }, + # "Status": "Pending", + # "Transaction": null + # } + # + return self.parse_transaction(response, currency) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # { + # "TransactionGuid": "dc932e19-562b-4c50-821e-a73fd048b93b", + # "PrimaryCurrencyCode": "Bch", + # "CreatedTimestampUtc": "2020-04-01T05:26:30.5093622+00:00", + # "Amount": { + # "Total": 0.1231, + # "Fee": 0.0001 + # }, + # "Destination": { + # "Address": "bc1qhpqxkjpvgkckw530yfmxyr53c94q8f4273a7ez", + # "Tag": null + # }, + # "Status": "Pending", + # "Transaction": null + # } + # + amount = self.safe_dict(transaction, 'Amount') + destination = self.safe_dict(transaction, 'Destination') + currencyId = self.safe_string(transaction, 'PrimaryCurrencyCode') + datetime = self.safe_string(transaction, 'CreatedTimestampUtc') + address = self.safe_string(destination, 'Address') + tag = self.safe_string(destination, 'Tag') + code = self.safe_currency_code(currencyId, currency) + return { + 'info': transaction, + 'id': self.safe_string(transaction, 'TransactionGuid'), + 'txid': None, + 'type': 'withdraw', + 'currency': code, + 'network': None, + 'amount': self.safe_number(amount, 'Total'), + 'status': self.safe_string(transaction, 'Status'), + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + 'address': address, + 'addressFrom': None, + 'addressTo': address, + 'tag': tag, + 'tagFrom': None, + 'tagTo': tag, + 'updated': None, + 'comment': None, + 'fee': { + 'currency': code, + 'cost': self.safe_number(amount, 'Fee'), + 'rate': None, + }, + 'internal': False, + } + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = self.urls['api'][api] + '/' + path + if api == 'public': + if params: + url += '?' + self.urlencode(params) + else: + self.check_required_credentials() + nonce = self.nonce() + auth = [ + url, + 'apiKey=' + self.apiKey, + 'nonce=' + str(nonce), + ] + keys = list(params.keys()) + for i in range(0, len(keys)): + key = keys[i] + value = str(params[key]) + auth.append(key + '=' + value) + message = ','.join(auth) + signature = self.hmac(self.encode(message), self.encode(self.secret), hashlib.sha256) + query = self.ordered({}) + query['apiKey'] = self.apiKey + query['nonce'] = nonce + query['signature'] = signature.upper() + for i in range(0, len(keys)): + key = keys[i] + query[key] = params[key] + body = self.json(query) + headers = {'Content-Type': 'application/json'} + return {'url': url, 'method': method, 'body': body, 'headers': headers} diff --git a/ccxt/async_support/indodax.py b/ccxt/async_support/indodax.py new file mode 100644 index 0000000..55c4bb6 --- /dev/null +++ b/ccxt/async_support/indodax.py @@ -0,0 +1,1413 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.indodax import ImplicitAPI +import hashlib +import math +from ccxt.base.types import Any, Balances, Currency, DepositAddress, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class indodax(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(indodax, self).describe(), { + 'id': 'indodax', + 'name': 'INDODAX', + 'countries': ['ID'], # Indonesia + # 10 requests per second for making trades => 1000ms / 10 = 100ms + # 180 requests per minute(public endpoints) = 2 requests per second => cost = (1000ms / rateLimit) / 2 = 5 + 'rateLimit': 50, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelAllOrders': False, + 'cancelOrder': True, + 'cancelOrders': False, + 'closeAllPositions': False, + 'closePosition': False, + 'createDepositAddress': False, + 'createOrder': True, + 'createReduceOnlyOrder': False, + 'createStopLimitOrder': False, + 'createStopMarketOrder': False, + 'createStopOrder': False, + 'fetchAllGreeks': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': False, + 'fetchDeposit': False, + 'fetchDepositAddress': 'emulated', + 'fetchDepositAddresses': True, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': False, + 'fetchDepositsWithdrawals': True, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrice': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': False, + 'fetchPosition': False, + 'fetchPositionForSymbolWs': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsForSymbolWs': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': False, + 'fetchTransactionFee': True, + 'fetchTransactionFees': False, + 'fetchTransactions': 'emulated', + 'fetchTransfer': False, + 'fetchTransfers': False, + 'fetchUnderlyingAssets': False, + 'fetchVolatilityHistory': False, + 'fetchWithdrawal': False, + 'fetchWithdrawals': False, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'transfer': False, + 'withdraw': True, + }, + 'version': '2.0', # 9 April 2018 + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/51840849/87070508-9358c880-c221-11ea-8dc5-5391afbbb422.jpg', + 'api': { + 'public': 'https://indodax.com', + 'private': 'https://indodax.com/tapi', + }, + 'www': 'https://www.indodax.com', + 'doc': 'https://github.com/btcid/indodax-official-api-docs', + 'referral': 'https://indodax.com/ref/testbitcoincoid/1', + }, + 'api': { + 'public': { + 'get': { + 'api/server_time': 5, + 'api/pairs': 5, + 'api/price_increments': 5, + 'api/summaries': 5, + 'api/ticker/{pair}': 5, + 'api/ticker_all': 5, + 'api/trades/{pair}': 5, + 'api/depth/{pair}': 5, + 'tradingview/history_v2': 5, + }, + }, + 'private': { + 'post': { + 'getInfo': 4, + 'transHistory': 4, + 'trade': 1, + 'tradeHistory': 4, # TODO add fetchMyTrades + 'openOrders': 4, + 'orderHistory': 4, + 'getOrder': 4, + 'cancelOrder': 4, + 'withdrawFee': 4, + 'withdrawCoin': 4, + 'listDownline': 4, + 'checkDownline': 4, + 'createVoucher': 4, # partner only + }, + }, + }, + 'fees': { + 'trading': { + 'tierBased': False, + 'percentage': True, + 'maker': 0, + 'taker': 0.003, + }, + }, + 'exceptions': { + 'exact': { + 'invalid_pair': BadSymbol, # {"error":"invalid_pair","error_description":"Invalid Pair"} + 'Insufficient balance.': InsufficientFunds, + 'invalid order.': OrderNotFound, + 'Invalid credentials. API not found or session has expired.': AuthenticationError, + 'Invalid credentials. Bad sign.': AuthenticationError, + }, + 'broad': { + 'Minimum price': InvalidOrder, + 'Minimum order': InvalidOrder, + }, + }, + 'timeframes': { + '1m': '1', + '15m': '15', + '30m': '30', + '1h': '60', + '4h': '240', + '1d': '1D', + '3d': '3D', + '1w': '1W', + }, + # exchange-specific options + 'options': { + 'recvWindow': 5 * 1000, # default 5 sec + 'timeDifference': 0, # the difference between system clock and exchange clock + 'adjustForTimeDifference': False, # controls the adjustment logic upon instantiation + 'networks': { + 'XLM': 'Stellar Token', + 'BSC': 'bep20', + 'TRC20': 'trc20', + 'MATIC': 'polygon', + # 'BEP2': 'bep2', + # 'ARB': 'arb', + # 'ERC20': 'erc20', + # 'KIP7': 'kip7', + # 'MAINNET': 'mainnet', # TODO: does mainnet just mean the default? + # 'OEP4': 'oep4', + # 'OP': 'op', + # 'SPL': 'spl', + # 'TRC10': 'trc10', + # 'ZRC2': 'zrc2' + # 'ETH': 'eth' + # 'BASE': 'base' + }, + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': False, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, # todo implementation + 'FOK': False, + 'PO': False, + 'GTD': False, + }, + 'hedged': False, + 'selfTradePrevention': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': False, + 'marketBuyRequiresPrice': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': None, # todo implement + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 100000, # todo + 'daysBackCanceled': 1, + 'untilDays': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOHLCV': { + 'limit': 2000, # todo: not in request + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'commonCurrencies': { + 'STR': 'XLM', + 'BCHABC': 'BCH', + 'BCHSV': 'BSV', + 'DRK': 'DASH', + 'NEM': 'XEM', + }, + 'precisionMode': TICK_SIZE, + }) + + def nonce(self): + return self.milliseconds() - self.options['timeDifference'] + + async def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://github.com/btcid/indodax-official-api-docs/blob/master/Public-RestAPI.md#server-time + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = await self.publicGetApiServerTime(params) + # + # { + # "timezone": "UTC", + # "server_time": 1571205969552 + # } + # + return self.safe_integer(response, 'server_time') + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for indodax + + https://github.com/btcid/indodax-official-api-docs/blob/master/Public-RestAPI.md#pairs + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = await self.publicGetApiPairs(params) + # + # [ + # { + # "id": "btcidr", + # "symbol": "BTCIDR", + # "base_currency": "idr", + # "traded_currency": "btc", + # "traded_currency_unit": "BTC", + # "description": "BTC/IDR", + # "ticker_id": "btc_idr", + # "volume_precision": 0, + # "price_precision": 1000, + # "price_round": 8, + # "pricescale": 1000, + # "trade_min_base_currency": 10000, + # "trade_min_traded_currency": 0.00007457, + # "has_memo": False, + # "memo_name": False, + # "has_payment_id": False, + # "trade_fee_percent": 0.3, + # "url_logo": "https://indodax.com/v2/logo/svg/color/btc.svg", + # "url_logo_png": "https://indodax.com/v2/logo/png/color/btc.png", + # "is_maintenance": 0 + # } + # ] + # + result = [] + for i in range(0, len(response)): + market = response[i] + id = self.safe_string(market, 'id') + baseId = self.safe_string(market, 'traded_currency') + quoteId = self.safe_string(market, 'base_currency') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + isMaintenance = self.safe_integer(market, 'is_maintenance') + result.append({ + 'id': id, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': False if isMaintenance else True, + 'contract': False, + 'linear': None, + 'inverse': None, + 'taker': self.safe_number(market, 'trade_fee_percent'), + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'percentage': True, + 'precision': { + 'amount': self.parse_number('1e-8'), + 'price': self.parse_number(self.parse_precision(self.safe_string(market, 'price_round'))), + 'cost': self.parse_number(self.parse_precision(self.safe_string(market, 'volume_precision'))), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(market, 'trade_min_traded_currency'), + 'max': None, + }, + 'price': { + 'min': self.safe_number(market, 'trade_min_base_currency'), + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + 'info': market, + }) + return result + + def parse_balance(self, response) -> Balances: + balances = self.safe_value(response, 'return', {}) + free = self.safe_value(balances, 'balance', {}) + used = self.safe_value(balances, 'balance_hold', {}) + timestamp = self.safe_timestamp(balances, 'server_time') + result: dict = { + 'info': response, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + currencyIds = list(free.keys()) + for i in range(0, len(currencyIds)): + currencyId = currencyIds[i] + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(free, currencyId) + account['used'] = self.safe_string(used, currencyId) + result[code] = account + return self.safe_balance(result) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://github.com/btcid/indodax-official-api-docs/blob/master/Private-RestAPI.md#get-info-endpoint + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + response = await self.privatePostGetInfo(params) + # + # { + # "success":1, + # "return":{ + # "server_time":1619562628, + # "balance":{ + # "idr":167, + # "btc":"0.00000000", + # "1inch":"0.00000000", + # }, + # "balance_hold":{ + # "idr":0, + # "btc":"0.00000000", + # "1inch":"0.00000000", + # }, + # "address":{ + # "btc":"1KMntgzvU7iTSgMBWc11nVuJjAyfW3qJyk", + # "1inch":"0x1106c8bb3172625e1f411c221be49161dac19355", + # "xrp":"rwWr7KUZ3ZFwzgaDGjKBysADByzxvohQ3C", + # "zrx":"0x1106c8bb3172625e1f411c221be49161dac19355" + # }, + # "user_id":"276011", + # "name":"", + # "email":"testbitcoincoid@mailforspam.com", + # "profile_picture":null, + # "verification_status":"unverified", + # "gauth_enable":true + # } + # } + # + return self.parse_balance(response) + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://github.com/btcid/indodax-official-api-docs/blob/master/Public-RestAPI.md#depth + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + orderbook = await self.publicGetApiDepthPair(self.extend(request, params)) + return self.parse_order_book(orderbook, market['symbol'], None, 'buy', 'sell') + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "high":"0.01951", + # "low":"0.01877", + # "vol_eth":"39.38839319", + # "vol_btc":"0.75320886", + # "last":"0.01896", + # "buy":"0.01896", + # "sell":"0.019", + # "server_time":1565248908 + # } + # + symbol = self.safe_symbol(None, market) + timestamp = self.safe_timestamp(ticker, 'server_time') + baseVolume = 'vol_' + market['baseId'].lower() + quoteVolume = 'vol_' + market['quoteId'].lower() + last = self.safe_string(ticker, 'last') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': self.safe_string(ticker, 'buy'), + 'bidVolume': None, + 'ask': self.safe_string(ticker, 'sell'), + 'askVolume': None, + 'vwap': None, + 'open': None, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': self.safe_string(ticker, baseVolume), + 'quoteVolume': self.safe_string(ticker, quoteVolume), + 'info': ticker, + }, market) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://github.com/btcid/indodax-official-api-docs/blob/master/Public-RestAPI.md#ticker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + response = await self.publicGetApiTickerPair(self.extend(request, params)) + # + # { + # "ticker": { + # "high":"0.01951", + # "low":"0.01877", + # "vol_eth":"39.38839319", + # "vol_btc":"0.75320886", + # "last":"0.01896", + # "buy":"0.01896", + # "sell":"0.019", + # "server_time":1565248908 + # } + # } + # + ticker = self.safe_dict(response, 'ticker', {}) + return self.parse_ticker(ticker, market) + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://github.com/btcid/indodax-official-api-docs/blob/master/Public-RestAPI.md#ticker-all + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + # + # { + # "tickers": { + # "btc_idr": { + # "high": "120009000", + # "low": "116735000", + # "vol_btc": "218.13777777", + # "vol_idr": "25800033297", + # "last": "117088000", + # "buy": "117002000", + # "sell": "117078000", + # "server_time": 1571207881 + # } + # } + # } + # + response = await self.publicGetApiTickerAll(params) + tickers = self.safe_dict(response, 'tickers', {}) + keys = list(tickers.keys()) + parsedTickers = {} + for i in range(0, len(keys)): + key = keys[i] + rawTicker = tickers[key] + marketId = key.replace('_', '') + market = self.safe_market(marketId) + parsed = self.parse_ticker(rawTicker, market) + parsedTickers[marketId] = parsed + return self.filter_by_array(parsedTickers, 'symbol', symbols) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + timestamp = self.safe_timestamp(trade, 'date') + return self.safe_trade({ + 'id': self.safe_string(trade, 'tid'), + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': self.safe_symbol(None, market), + 'type': None, + 'side': self.safe_string(trade, 'type'), + 'order': None, + 'takerOrMaker': None, + 'price': self.safe_string(trade, 'price'), + 'amount': self.safe_string(trade, 'amount'), + 'cost': None, + 'fee': None, + }, market) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://github.com/btcid/indodax-official-api-docs/blob/master/Public-RestAPI.md#trades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + response = await self.publicGetApiTradesPair(self.extend(request, params)) + return self.parse_trades(response, market, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # { + # "Time": 1708416900, + # "Open": 51707.52, + # "High": 51707.52, + # "Low": 51707.52, + # "Close": 51707.52, + # "Volume": "0" + # } + # + return [ + self.safe_timestamp(ohlcv, 'Time'), + self.safe_number(ohlcv, 'Open'), + self.safe_number(ohlcv, 'High'), + self.safe_number(ohlcv, 'Low'), + self.safe_number(ohlcv, 'Close'), + self.safe_number(ohlcv, 'Volume'), + ] + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest candle to fetch + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + selectedTimeframe = self.safe_string(self.timeframes, timeframe, timeframe) + now = self.seconds() + until = self.safe_integer(params, 'until', now) + params = self.omit(params, ['until']) + request: dict = { + 'to': until, + 'tf': selectedTimeframe, + 'symbol': market['id'], + } + if limit is None: + limit = 1000 + if since is not None: + request['from'] = int(math.floor(since / 1000)) + else: + duration = self.parse_timeframe(timeframe) + request['from'] = now - limit * duration - 1 + response = await self.publicGetTradingviewHistoryV2(self.extend(request, params)) + # + # [ + # { + # "Time": 1708416900, + # "Open": 51707.52, + # "High": 51707.52, + # "Low": 51707.52, + # "Close": 51707.52, + # "Volume": "0" + # } + # ] + # + return self.parse_ohlcvs(response, market, timeframe, since, limit) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'open': 'open', + 'filled': 'closed', + 'cancelled': 'canceled', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # { + # "order_id": "12345", + # "submit_time": "1392228122", + # "price": "8000000", + # "type": "sell", + # "order_ltc": "100000000", + # "remain_ltc": "100000000" + # } + # + # market closed orders - note that the price is very high + # and does not reflect actual price the order executed at + # + # { + # "order_id": "49326856", + # "type": "sell", + # "price": "1000000000", + # "submit_time": "1618314671", + # "finish_time": "1618314671", + # "status": "filled", + # "order_xrp": "30.45000000", + # "remain_xrp": "0.00000000" + # } + # + # cancelOrder + # + # { + # "order_id": 666883, + # "client_order_id": "clientx-sj82ks82j", + # "type": "sell", + # "pair": "btc_idr", + # "balance": { + # "idr": "33605800", + # "btc": "0.00000000", + # ... + # "frozen_idr": "0", + # "frozen_btc": "0.00000000", + # ... + # } + # } + # + side = None + if 'type' in order: + side = order['type'] + status = self.parse_order_status(self.safe_string(order, 'status', 'open')) + symbol = None + cost = None + price = self.safe_string(order, 'price') + amount = None + remaining = None + marketId = self.safe_string(order, 'pair') + market = self.safe_market(marketId, market) + if market is not None: + symbol = market['symbol'] + quoteId = market['quoteId'] + baseId = market['baseId'] + if (market['quoteId'] == 'idr') and ('order_rp' in order): + quoteId = 'rp' + if (market['baseId'] == 'idr') and ('remain_rp' in order): + baseId = 'rp' + cost = self.safe_string(order, 'order_' + quoteId) + if not cost: + amount = self.safe_string(order, 'order_' + baseId) + remaining = self.safe_string(order, 'remain_' + baseId) + timestamp = self.safe_integer(order, 'submit_time') + fee = None + id = self.safe_string(order, 'order_id') + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': self.safe_string(order, 'client_order_id'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'symbol': symbol, + 'type': 'limit', + 'timeInForce': None, + 'postOnly': None, + 'side': side, + 'price': price, + 'triggerPrice': None, + 'cost': cost, + 'average': None, + 'amount': amount, + 'filled': None, + 'remaining': remaining, + 'status': status, + 'fee': fee, + 'trades': None, + }) + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://github.com/btcid/indodax-official-api-docs/blob/master/Private-RestAPI.md#get-order-endpoints + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrder() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + 'order_id': id, + } + response = await self.privatePostGetOrder(self.extend(request, params)) + orders = response['return'] + order = self.parse_order(self.extend({'id': id}, orders['order']), market) + order['info'] = response + return order + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://github.com/btcid/indodax-official-api-docs/blob/master/Private-RestAPI.md#open-orders-endpoints + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + market = None + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['pair'] = market['id'] + response = await self.privatePostOpenOrders(self.extend(request, params)) + rawOrders = response['return']['orders'] + # {success: 1, return: {orders: null}} if no orders + if not rawOrders: + return [] + # {success: 1, return: {orders: [... objects]}} for orders fetched by symbol + if symbol is not None: + return self.parse_orders(rawOrders, market, since, limit) + # {success: 1, return: {orders: {marketid: [... objects]}}} if all orders are fetched + marketIds = list(rawOrders.keys()) + exchangeOrders = [] + for i in range(0, len(marketIds)): + marketId = marketIds[i] + marketOrders = rawOrders[marketId] + market = self.safe_market(marketId) + parsedOrders = self.parse_orders(marketOrders, market, since, limit) + exchangeOrders = self.array_concat(exchangeOrders, parsedOrders) + return exchangeOrders + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://github.com/btcid/indodax-official-api-docs/blob/master/Private-RestAPI.md#order-history + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchClosedOrders() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + response = await self.privatePostOrderHistory(self.extend(request, params)) + orders = self.parse_orders(response['return']['orders'], market) + orders = self.filter_by(orders, 'status', 'closed') + return self.filter_by_symbol_since_limit(orders, symbol, since, limit) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://github.com/btcid/indodax-official-api-docs/blob/master/Private-RestAPI.md#trade-endpoints + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + 'type': side, + 'price': price, + } + priceIsRequired = False + quantityIsRequired = False + if type == 'market': + if side == 'buy': + quoteAmount = None + cost = self.safe_number(params, 'cost') + params = self.omit(params, 'cost') + if cost is not None: + quoteAmount = self.cost_to_precision(symbol, cost) + else: + if price is None: + raise InvalidOrder(self.id + ' createOrder() requires the price argument for market buy orders to calculate the total cost to spend(amount * price).') + amountString = self.number_to_string(amount) + priceString = self.number_to_string(price) + costRequest = Precise.string_mul(amountString, priceString) + quoteAmount = self.cost_to_precision(symbol, costRequest) + request[market['quoteId']] = quoteAmount + else: + quantityIsRequired = True + elif type == 'limit': + priceIsRequired = True + quantityIsRequired = True + if side == 'buy': + request[market['quoteId']] = self.parse_to_numeric(Precise.string_mul(self.number_to_string(amount), self.number_to_string(price))) + if priceIsRequired: + if price is None: + raise InvalidOrder(self.id + ' createOrder() requires a price argument for a ' + type + ' order') + request['price'] = price + if quantityIsRequired: + request[market['baseId']] = self.amount_to_precision(symbol, amount) + result = await self.privatePostTrade(self.extend(request, params)) + data = self.safe_value(result, 'return', {}) + id = self.safe_string(data, 'order_id') + return self.safe_order({ + 'info': result, + 'id': id, + }, market) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://github.com/btcid/indodax-official-api-docs/blob/master/Private-RestAPI.md#cancel-order-endpoints + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + side = self.safe_value(params, 'side') + if side is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires an extra "side" param') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'order_id': id, + 'pair': market['id'], + 'type': side, + } + response = await self.privatePostCancelOrder(self.extend(request, params)) + # + # { + # "success": 1, + # "return": { + # "order_id": 666883, + # "client_order_id": "clientx-sj82ks82j", + # "type": "sell", + # "pair": "btc_idr", + # "balance": { + # "idr": "33605800", + # "btc": "0.00000000", + # ... + # "frozen_idr": "0", + # "frozen_btc": "0.00000000", + # ... + # } + # } + # } + # + data = self.safe_dict(response, 'return') + return self.parse_order(data) + + async def fetch_transaction_fee(self, code: str, params={}): + """ + fetch the fee for a transaction + + https://github.com/btcid/indodax-official-api-docs/blob/master/Private-RestAPI.md#withdraw-fee-endpoints + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `fee structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + } + response = await self.privatePostWithdrawFee(self.extend(request, params)) + # + # { + # "success": 1, + # "return": { + # "server_time": 1607923272, + # "withdraw_fee": 0.005, + # "currency": "eth" + # } + # } + # + data = self.safe_value(response, 'return', {}) + currencyId = self.safe_string(data, 'currency') + return { + 'info': response, + 'rate': self.safe_number(data, 'withdraw_fee'), + 'currency': self.safe_currency_code(currencyId, currency), + } + + async def fetch_deposits_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch history of deposits and withdrawals + + https://github.com/btcid/indodax-official-api-docs/blob/master/Private-RestAPI.md#transaction-history-endpoints + + :param str [code]: unified currency code for the currency of the deposit/withdrawals, default is None + :param int [since]: timestamp in ms of the earliest deposit/withdrawal, default is None + :param int [limit]: max number of deposit/withdrawals to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `transaction structure ` + """ + await self.load_markets() + request: dict = {} + if since is not None: + startTime = self.iso8601(since)[0:10] + request['start'] = startTime + request['end'] = self.iso8601(self.milliseconds())[0:10] + response = await self.privatePostTransHistory(self.extend(request, params)) + # + # { + # "success": 1, + # "return": { + # "withdraw": { + # "idr": [ + # { + # "status": "success", + # "type": "coupon", + # "rp": "115205", + # "fee": "500", + # "amount": "114705", + # "submit_time": "1539844166", + # "success_time": "1539844189", + # "withdraw_id": "1783717", + # "tx": "BTC-IDR-RDTVVO2P-ETD0EVAW-VTNZGMIR-HTNTUAPI-84ULM9OI", + # "sender": "boris", + # "used_by": "viginia88" + # }, + # ... + # ], + # "btc": [], + # "abyss": [], + # ... + # }, + # "deposit": { + # "idr": [ + # { + # "status": "success", + # "type": "duitku", + # "rp": "393000", + # "fee": "5895", + # "amount": "387105", + # "submit_time": "1576555012", + # "success_time": "1576555012", + # "deposit_id": "3395438", + # "tx": "Duitku OVO Settlement" + # }, + # ... + # ], + # "btc": [ + # { + # "status": "success", + # "btc": "0.00118769", + # "amount": "0.00118769", + # "success_time": "1539529208", + # "deposit_id": "3602369", + # "tx": "c816aeb35a5b42f389970325a32aff69bb6b2126784dcda8f23b9dd9570d6573" + # }, + # ... + # ], + # "abyss": [], + # ... + # } + # } + # } + # + data = self.safe_value(response, 'return', {}) + withdraw = self.safe_value(data, 'withdraw', {}) + deposit = self.safe_value(data, 'deposit', {}) + transactions = [] + currency = None + if code is None: + keys = list(withdraw.keys()) + for i in range(0, len(keys)): + key = keys[i] + transactions = self.array_concat(transactions, withdraw[key]) + keys = list(deposit.keys()) + for i in range(0, len(keys)): + key = keys[i] + transactions = self.array_concat(transactions, deposit[key]) + else: + currency = self.currency(code) + withdraws = self.safe_value(withdraw, currency['id'], []) + deposits = self.safe_value(deposit, currency['id'], []) + transactions = self.array_concat(withdraws, deposits) + return self.parse_transactions(transactions, currency, since, limit) + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://github.com/btcid/indodax-official-api-docs/blob/master/Private-RestAPI.md#withdraw-coin-endpoints + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.check_address(address) + await self.load_markets() + currency = self.currency(code) + # Custom string you need to provide to identify each withdrawal. + # Will be passed to callback URL(assigned via website to the API key) + # so your system can identify the request and confirm it. + # Alphanumeric, max length 255. + requestId = self.milliseconds() + # Alternatively: + # requestId = self.uuid() + request: dict = { + 'currency': currency['id'], + 'withdraw_amount': amount, + 'withdraw_address': address, + 'request_id': str(requestId), + } + if tag: + request['withdraw_memo'] = tag + response = await self.privatePostWithdrawCoin(self.extend(request, params)) + # + # { + # "success": 1, + # "status": "approved", + # "withdraw_currency": "xrp", + # "withdraw_address": "rwWr7KUZ3ZFwzgaDGjKBysADByzxvohQ3C", + # "withdraw_amount": "10000.00000000", + # "fee": "2.00000000", + # "amount_after_fee": "9998.00000000", + # "submit_time": "1509469200", + # "withdraw_id": "xrp-12345", + # "txid": "", + # "withdraw_memo": "123123" + # } + # + return self.parse_transaction(response, currency) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # withdraw + # + # { + # "success": 1, + # "status": "approved", + # "withdraw_currency": "xrp", + # "withdraw_address": "rwWr7KUZ3ZFwzgaDGjKBysADByzxvohQ3C", + # "withdraw_amount": "10000.00000000", + # "fee": "2.00000000", + # "amount_after_fee": "9998.00000000", + # "submit_time": "1509469200", + # "withdraw_id": "xrp-12345", + # "txid": "", + # "withdraw_memo": "123123" + # } + # + # transHistory + # + # { + # "status": "success", + # "type": "coupon", + # "rp": "115205", + # "fee": "500", + # "amount": "114705", + # "submit_time": "1539844166", + # "success_time": "1539844189", + # "withdraw_id": "1783717", + # "tx": "BTC-IDR-RDTVVO2P-ETD0EVAW-VTNZGMIR-HTNTUAPI-84ULM9OI", + # "sender": "boris", + # "used_by": "viginia88" + # } + # + # { + # "status": "success", + # "btc": "0.00118769", + # "amount": "0.00118769", + # "success_time": "1539529208", + # "deposit_id": "3602369", + # "tx": "c816aeb35a5b42f389970325a32aff69bb6b2126784dcda8f23b9dd9570d6573" + # }, + status = self.safe_string(transaction, 'status') + timestamp = self.safe_timestamp_2(transaction, 'success_time', 'submit_time') + depositId = self.safe_string(transaction, 'deposit_id') + feeCost = self.safe_number(transaction, 'fee') + fee = None + if feeCost is not None: + fee = { + 'currency': self.safe_currency_code(None, currency), + 'cost': feeCost, + 'rate': None, + } + return { + 'id': self.safe_string_2(transaction, 'withdraw_id', 'deposit_id'), + 'txid': self.safe_string_2(transaction, 'txid', 'tx'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': None, + 'addressFrom': None, + 'address': self.safe_string(transaction, 'withdraw_address'), + 'addressTo': None, + 'amount': self.safe_number_n(transaction, ['amount', 'withdraw_amount', 'deposit_amount']), + 'type': 'withdraw' if (depositId is None) else 'deposit', + 'currency': self.safe_currency_code(None, currency), + 'status': self.parse_transaction_status(status), + 'updated': None, + 'tagFrom': None, + 'tag': None, + 'tagTo': None, + 'comment': self.safe_string(transaction, 'withdraw_memo'), + 'internal': None, + 'fee': fee, + 'info': transaction, + } + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'success': 'ok', + } + return self.safe_string(statuses, status, status) + + async def fetch_deposit_addresses(self, codes: Strings = None, params={}) -> List[DepositAddress]: + """ + fetch deposit addresses for multiple currencies and chain types + + https://github.com/btcid/indodax-official-api-docs/blob/master/Private-RestAPI.md#general-information-on-endpoints + + :param str[] [codes]: list of unified currency codes, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `address structures ` + """ + await self.load_markets() + response = await self.privatePostGetInfo(params) + # + # { + # success: '1', + # return: { + # server_time: '1708031570', + # balance: { + # idr: '29952', + # ... + # }, + # balance_hold: { + # idr: '0', + # ... + # }, + # address: { + # btc: '1KMntgzvU7iTSgMBWc11nVuJjAyfW3qJyk', + # ... + # }, + # memo_is_required: { + # btc: {mainnet: False}, + # ... + # }, + # network: { + # btc: 'mainnet', + # ... + # }, + # user_id: '276011', + # name: '', + # email: 'testbitcoincoid@mailforspam.com', + # profile_picture: null, + # verification_status: 'unverified', + # gauth_enable: True, + # withdraw_status: '0' + # } + # } + # + data = self.safe_dict(response, 'return') + addresses = self.safe_dict(data, 'address', {}) + networks = self.safe_dict(data, 'network', {}) + addressKeys = list(addresses.keys()) + result: dict = { + 'info': data, + } + for i in range(0, len(addressKeys)): + marketId = addressKeys[i] + code = self.safe_currency_code(marketId) + address = self.safe_string(addresses, marketId) + if (address is not None) and ((codes is None) or (self.in_array(code, codes))): + self.check_address(address) + network = None + if marketId in networks: + networkId = self.safe_string(networks, marketId) + if networkId.find(',') >= 0: + network = [] + networkIds = networkId.split(',') + for j in range(0, len(networkIds)): + network.append(self.network_id_to_code(networkIds[j]).upper()) + else: + network = self.network_id_to_code(networkId).upper() + result[code] = { + 'info': {}, + 'currency': code, + 'network': network, + 'address': address, + 'tag': None, + } + return result + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = self.urls['api'][api] + if api == 'public': + query = self.omit(params, self.extract_params(path)) + requestPath = '/' + self.implode_params(path, params) + url = url + requestPath + if query: + url += '?' + self.urlencode_with_array_repeat(query) + else: + self.check_required_credentials() + body = self.urlencode(self.extend({ + 'method': path, + 'timestamp': self.nonce(), + 'recvWindow': self.options['recvWindow'], + }, params)) + headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Key': self.apiKey, + 'Sign': self.hmac(self.encode(body), self.encode(self.secret), hashlib.sha512), + } + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None + # {success: 0, error: "invalid order."} + # or + # [{data, ...}, {...}, ...] + # {"success":"1","status":"approved","withdraw_currency":"strm","withdraw_address":"0x2b9A8cd5535D99b419aEfFBF1ae8D90a7eBdb24E","withdraw_amount":"2165.05767839","fee":"21.11000000","amount_after_fee":"2143.94767839","submit_time":"1730759489","withdraw_id":"strm-3423","txid":""} + if isinstance(response, list): + return None # public endpoints may return []-arrays + error = self.safe_value(response, 'error', '') + if not ('success' in response) and error == '': + return None # no 'success' property on public responses + status = self.safe_string(response, 'success') + if status == 'approved': + return None + if self.safe_integer(response, 'success', 0) == 1: + # {success: 1, return: {orders: []}} + if not ('return' in response): + raise ExchangeError(self.id + ': malformed response: ' + self.json(response)) + else: + return None + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], error, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], error, feedback) + raise ExchangeError(feedback) # unknown message diff --git a/ccxt/async_support/kraken.py b/ccxt/async_support/kraken.py new file mode 100644 index 0000000..12b0832 --- /dev/null +++ b/ccxt/async_support/kraken.py @@ -0,0 +1,3444 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.kraken import ImplicitAPI +import asyncio +import hashlib +from ccxt.base.types import Any, Balances, Currencies, Currency, DepositAddress, IndexType, Int, LedgerEntry, Market, Num, Order, OrderBook, OrderRequest, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, Trade, TradingFeeInterface, Transaction, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import AccountSuspended +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidAddress +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import NotSupported +from ccxt.base.errors import DDoSProtection +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.errors import OnMaintenance +from ccxt.base.errors import InvalidNonce +from ccxt.base.errors import CancelPending +from ccxt.base.decimal_to_precision import TRUNCATE +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class kraken(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(kraken, self).describe(), { + 'id': 'kraken', + 'name': 'Kraken', + 'countries': ['US'], + 'version': '0', + # rate-limits: https://support.kraken.com/hc/en-us/articles/206548367-What-are-the-API-rate-limits-#1 + # for public: 1 req/s + # for private: every second 0.33 weight added to your allowed capacity(some private endpoints need 1 weight, some need 2) + 'rateLimit': 1000, + 'certified': False, + 'pro': True, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': True, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'cancelAllOrders': True, + 'cancelAllOrdersAfter': True, + 'cancelOrder': True, + 'cancelOrders': True, + 'createDepositAddress': True, + 'createMarketBuyOrderWithCost': True, + 'createMarketOrderWithCost': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createOrders': True, + 'createStopLimitOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'createTrailingAmountOrder': True, + 'createTrailingPercentOrder': True, + 'editOrder': True, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchFundingHistory': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchLedger': True, + 'fetchLedgerEntry': True, + 'fetchLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterestHistory': False, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrderTrades': 'emulated', + 'fetchPositions': True, + 'fetchPremiumIndexOHLCV': False, + 'fetchStatus': True, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': True, + 'fetchTradingFees': False, + 'fetchWithdrawals': True, + 'setLeverage': False, + 'setMarginMode': False, # Kraken only supports cross margin + 'transfer': True, + 'withdraw': True, + }, + 'timeframes': { + '1m': 1, + '5m': 5, + '15m': 15, + '30m': 30, + '1h': 60, + '4h': 240, + '1d': 1440, + '1w': 10080, + '2w': 21600, + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/51840849/76173629-fc67fb00-61b1-11ea-84fe-f2de582f58a3.jpg', + 'api': { + 'public': 'https://api.kraken.com', + 'private': 'https://api.kraken.com', + 'zendesk': 'https://kraken.zendesk.com/api/v2/help_center/en-us/articles', # use the public zendesk api to receive article bodies and bypass new anti-spam protections + }, + 'www': 'https://www.kraken.com', + 'doc': 'https://docs.kraken.com/rest/', + 'fees': 'https://www.kraken.com/en-us/features/fee-schedule', + }, + 'fees': { + 'trading': { + 'tierBased': True, + 'percentage': True, + 'taker': self.parse_number('0.0026'), + 'maker': self.parse_number('0.0016'), + 'tiers': { + 'taker': [ + [self.parse_number('0'), self.parse_number('0.0026')], + [self.parse_number('50000'), self.parse_number('0.0024')], + [self.parse_number('100000'), self.parse_number('0.0022')], + [self.parse_number('250000'), self.parse_number('0.0020')], + [self.parse_number('500000'), self.parse_number('0.0018')], + [self.parse_number('1000000'), self.parse_number('0.0016')], + [self.parse_number('2500000'), self.parse_number('0.0014')], + [self.parse_number('5000000'), self.parse_number('0.0012')], + [self.parse_number('10000000'), self.parse_number('0.0001')], + ], + 'maker': [ + [self.parse_number('0'), self.parse_number('0.0016')], + [self.parse_number('50000'), self.parse_number('0.0014')], + [self.parse_number('100000'), self.parse_number('0.0012')], + [self.parse_number('250000'), self.parse_number('0.0010')], + [self.parse_number('500000'), self.parse_number('0.0008')], + [self.parse_number('1000000'), self.parse_number('0.0006')], + [self.parse_number('2500000'), self.parse_number('0.0004')], + [self.parse_number('5000000'), self.parse_number('0.0002')], + [self.parse_number('10000000'), self.parse_number('0.0')], + ], + }, + }, + }, + 'handleContentTypeApplicationZip': True, + 'api': { + 'zendesk': { + 'get': [ + # we should really refrain from putting fixed fee numbers and stop hardcoding + # we will be using their web APIs to scrape all numbers from these articles + '360000292886', # -What-are-the-deposit-fees- + '201893608', # -What-are-the-withdrawal-fees- + ], + }, + 'public': { + 'get': { + # rate-limits explained in comment in the top of self file + 'Assets': 1, + 'AssetPairs': 1, + 'Depth': 1.2, + 'OHLC': 1.2, # 1.2 because 1 triggers too many requests immediately + 'Spread': 1, + 'SystemStatus': 1, + 'Ticker': 1, + 'Time': 1, + 'Trades': 1.2, + }, + }, + 'private': { + 'post': { + 'AddOrder': 0, + 'AddOrderBatch': 0, + 'AddExport': 3, + 'AmendOrder': 0, + 'Balance': 3, + 'CancelAll': 3, + 'CancelAllOrdersAfter': 3, + 'CancelOrder': 0, + 'CancelOrderBatch': 0, + 'ClosedOrders': 3, + 'DepositAddresses': 3, + 'DepositMethods': 3, + 'DepositStatus': 3, + 'EditOrder': 0, + 'ExportStatus': 3, + 'GetWebSocketsToken': 3, + 'Ledgers': 6, + 'OpenOrders': 3, + 'OpenPositions': 3, + 'QueryLedgers': 3, + 'QueryOrders': 3, + 'QueryTrades': 3, + 'RetrieveExport': 3, + 'RemoveExport': 3, + 'BalanceEx': 3, + 'TradeBalance': 3, + 'TradesHistory': 6, + 'TradeVolume': 3, + 'Withdraw': 3, + 'WithdrawCancel': 3, + 'WithdrawInfo': 3, + 'WithdrawMethods': 3, + 'WithdrawAddresses': 3, + 'WithdrawStatus': 3, + 'WalletTransfer': 3, + # sub accounts + 'CreateSubaccount': 3, + 'AccountTransfer': 3, + # earn + 'Earn/Allocate': 3, + 'Earn/Deallocate': 3, + 'Earn/AllocateStatus': 3, + 'Earn/DeallocateStatus': 3, + 'Earn/Strategies': 3, + 'Earn/Allocations': 3, + }, + }, + }, + 'commonCurrencies': { + # about X & Z prefixes and .S & .M suffixes, see comment under fetchCurrencies + 'LUNA': 'LUNC', + 'LUNA2': 'LUNA', + 'REPV2': 'REP', + 'REP': 'REPV1', + 'UST': 'USTC', + 'XBT': 'BTC', + 'XDG': 'DOGE', + 'FEE': 'KFEE', + 'XETC': 'ETC', + 'XETH': 'ETH', + 'XLTC': 'LTC', + 'XMLN': 'MLN', + 'XREP': 'REP', + 'XXBT': 'BTC', + 'XXDG': 'DOGE', + 'XXLM': 'XLM', + 'XXMR': 'XMR', + 'XXRP': 'XRP', + 'XZEC': 'ZEC', + 'ZAUD': 'AUD', + 'ZCAD': 'CAD', + 'ZEUR': 'EUR', + 'ZGBP': 'GBP', + 'ZJPY': 'JPY', + 'ZUSD': 'USD', + }, + 'options': { + 'timeDifference': 0, # the difference between system clock and Binance clock + 'adjustForTimeDifference': False, # controls the adjustment logic upon instantiation + 'marketsByAltname': {}, + 'delistedMarketsById': {}, + # cannot withdraw/deposit these + 'inactiveCurrencies': ['CAD', 'USD', 'JPY', 'GBP'], + 'networks': { + 'ETH': 'ERC20', + 'TRX': 'TRC20', + }, + 'depositMethods': { + '1INCH': '1inch' + ' ' + '(1INCH)', + 'AAVE': 'Aave', + 'ADA': 'ADA', + 'ALGO': 'Algorand', + 'ANKR': 'ANKR' + ' ' + '(ANKR)', + 'ANT': 'Aragon' + ' ' + '(ANT)', + 'ATOM': 'Cosmos', + 'AXS': 'Axie Infinity Shards' + ' ' + '(AXS)', + 'BADGER': 'Bager DAO' + ' ' + '(BADGER)', + 'BAL': 'Balancer' + ' ' + '(BAL)', + 'BAND': 'Band Protocol' + ' ' + '(BAND)', + 'BAT': 'BAT', + 'BCH': 'Bitcoin Cash', + 'BNC': 'Bifrost' + ' ' + '(BNC)', + 'BNT': 'Bancor' + ' ' + '(BNT)', + 'BTC': 'Bitcoin', + 'CHZ': 'Chiliz' + ' ' + '(CHZ)', + 'COMP': 'Compound' + ' ' + '(COMP)', + 'CQT': '\tCovalent Query Token' + ' ' + '(CQT)', + 'CRV': 'Curve DAO Token' + ' ' + '(CRV)', + 'CTSI': 'Cartesi' + ' ' + '(CTSI)', + 'DAI': 'Dai', + 'DASH': 'Dash', + 'DOGE': 'Dogecoin', + 'DOT': 'Polkadot', + 'DYDX': 'dYdX' + ' ' + '(DYDX)', + 'ENJ': 'Enjin Coin' + ' ' + '(ENJ)', + 'EOS': 'EOS', + 'ETC': 'Ether Classic' + ' ' + '(Hex)', + 'ETH': 'Ether' + ' ' + '(Hex)', + 'EWT': 'Energy Web Token', + 'FEE': 'Kraken Fee Credit', + 'FIL': 'Filecoin', + 'FLOW': 'Flow', + 'GHST': 'Aavegotchi' + ' ' + '(GHST)', + 'GNO': 'GNO', + 'GRT': 'GRT', + 'ICX': 'Icon', + 'INJ': 'Injective Protocol' + ' ' + '(INJ)', + 'KAR': 'Karura' + ' ' + '(KAR)', + 'KAVA': 'Kava', + 'KEEP': 'Keep Token' + ' ' + '(KEEP)', + 'KNC': 'Kyber Network' + ' ' + '(KNC)', + 'KSM': 'Kusama', + 'LINK': 'Link', + 'LPT': 'Livepeer Token' + ' ' + '(LPT)', + 'LRC': 'Loopring' + ' ' + '(LRC)', + 'LSK': 'Lisk', + 'LTC': 'Litecoin', + 'MANA': 'MANA', + 'MATIC': 'Polygon' + ' ' + '(MATIC)', + 'MINA': 'Mina', # inspected from webui + 'MIR': 'Mirror Protocol' + ' ' + '(MIR)', + 'MKR': 'Maker' + ' ' + '(MKR)', + 'MLN': 'MLN', + 'MOVR': 'Moonriver' + ' ' + '(MOVR)', + 'NANO': 'NANO', + 'OCEAN': 'OCEAN', + 'OGN': 'Origin Protocol' + ' ' + '(OGN)', + 'OMG': 'OMG', + 'OXT': 'Orchid' + ' ' + '(OXT)', + 'OXY': 'Oxygen' + ' ' + '(OXY)', + 'PAXG': 'PAX' + ' ' + '(Gold)', + 'PERP': 'Perpetual Protocol' + ' ' + '(PERP)', + 'PHA': 'Phala' + ' ' + '(PHA)', + 'QTUM': 'QTUM', + 'RARI': 'Rarible' + ' ' + '(RARI)', + 'RAY': 'Raydium' + ' ' + '(RAY)', + 'REN': 'Ren Protocol' + ' ' + '(REN)', + 'REP': 'REPv2', + 'REPV1': 'REP', + 'SAND': 'The Sandbox' + ' ' + '(SAND)', + 'SC': 'Siacoin', + 'SDN': 'Shiden' + ' ' + '(SDN)', + 'SOL': 'Solana', # their deposit method api doesn't work for SOL - was guessed + 'SNX': 'Synthetix Network' + ' ' + '(SNX)', + 'SRM': 'Serum', # inspected from webui + 'STORJ': 'Storj' + ' ' + '(STORJ)', + 'SUSHI': 'Sushiswap' + ' ' + '(SUSHI)', + 'TBTC': 'tBTC', + 'TRX': 'Tron', + 'UNI': 'UNI', + 'USDC': 'USDC', + 'USDT': 'Tether USD' + ' ' + '(ERC20)', + 'USDT-TRC20': 'Tether USD' + ' ' + '(TRC20)', + 'WAVES': 'Waves', + 'WBTC': 'Wrapped Bitcoin' + ' ' + '(WBTC)', + 'XLM': 'Stellar XLM', + 'XMR': 'Monero', + 'XRP': 'Ripple XRP', + 'XTZ': 'XTZ', + 'YFI': 'YFI', + 'ZEC': 'Zcash' + ' ' + '(Transparent)', + 'ZRX': '0x' + ' ' + '(ZRX)', + }, + 'withdrawMethods': { # keeping it here because deposit and withdraw return different networks codes + 'Lightning': 'Lightning', + 'Bitcoin': 'BTC', + 'Ripple': 'XRP', + 'Litecoin': 'LTC', + 'Dogecoin': 'DOGE', + 'Stellar': 'XLM', + 'Ethereum': 'ERC20', + 'Arbitrum One': 'Arbitrum', + 'Polygon': 'MATIC', + 'Arbitrum Nova': 'Arbitrum', + 'Optimism': 'Optimism', + 'zkSync Era': 'zkSync', + 'Ethereum Classic': 'ETC', + 'Zcash': 'ZEC', + 'Monero': 'XMR', + 'Tron': 'TRC20', + 'Solana': 'SOL', + 'EOS': 'EOS', + 'Bitcoin Cash': 'BCH', + 'Cardano': 'ADA', + 'Qtum': 'QTUM', + 'Tezos': 'XTZ', + 'Cosmos': 'ATOM', + 'Nano': 'NANO', + 'Siacoin': 'SC', + 'Lisk': 'LSK', + 'Waves': 'WAVES', + 'ICON': 'ICX', + 'Algorand': 'ALGO', + 'Polygon - USDC.e': 'MATIC', + 'Arbitrum One - USDC.e': 'Arbitrum', + 'Polkadot': 'DOT', + 'Kava': 'KAVA', + 'Filecoin': 'FIL', + 'Kusama': 'KSM', + 'Flow': 'FLOW', + 'Energy Web': 'EW', + 'Mina': 'MINA', + 'Centrifuge': 'CFG', + 'Karura': 'KAR', + 'Moonriver': 'MOVR', + 'Shiden': 'SDN', + 'Khala': 'PHA', + 'Bifrost Kusama': 'BNC', + 'Songbird': 'SGB', + 'Terra classic': 'LUNC', + 'KILT': 'KILT', + 'Basilisk': 'BSX', + 'Flare': 'FLR', + 'Avalanche C-Chain': 'AVAX', + 'Kintsugi': 'KINT', + 'Altair': 'AIR', + 'Moonbeam': 'GLMR', + 'Acala': 'ACA', + 'Astar': 'ASTR', + 'Akash': 'AKT', + 'Robonomics': 'XRT', + 'Fantom': 'FTM', + 'Elrond': 'EGLD', + 'THORchain': 'RUNE', + 'Secret': 'SCRT', + 'Near': 'NEAR', + 'Internet Computer Protocol': 'ICP', + 'Picasso': 'PICA', + 'Crust Shadow': 'CSM', + 'Integritee': 'TEER', + 'Parallel Finance': 'PARA', + 'HydraDX': 'HDX', + 'Interlay': 'INTR', + 'Fetch.ai': 'FET', + 'NYM': 'NYM', + 'Terra 2.0': 'LUNA2', + 'Juno': 'JUNO', + 'Nodle': 'NODL', + 'Stacks': 'STX', + 'Ethereum PoW': 'ETHW', + 'Aptos': 'APT', + 'Sui': 'SUI', + 'Genshiro': 'GENS', + 'Aventus': 'AVT', + 'Sei': 'SEI', + 'OriginTrail': 'OTP', + 'Celestia': 'TIA', + }, + 'marketHelperProps': ['marketsByAltname', 'delistedMarketsById'], # used by setMarketsFromExchange + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': False, # todo + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': True, + 'takeProfitPrice': True, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': True, + 'leverage': False, + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': True, # todo implement + 'iceberg': True, # todo implement + }, + 'createOrders': { + 'min': 2, + 'max': 15, + 'sameSymbolOnly': True, + }, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': None, + 'daysBack': None, + 'untilDays': None, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': None, + 'daysBack': None, + 'daysBackCanceled': None, + 'untilDays': 100000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 720, + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'exact': { + 'EQuery:Invalid asset pair': BadSymbol, # {"error":["EQuery:Invalid asset pair"]} + 'EAPI:Invalid key': AuthenticationError, + 'EFunding:Unknown withdraw key': InvalidAddress, # {"error":["EFunding:Unknown withdraw key"]} + 'EFunding:Invalid amount': InsufficientFunds, + 'EService:Unavailable': ExchangeNotAvailable, + 'EDatabase:Internal error': ExchangeNotAvailable, + 'EService:Busy': ExchangeNotAvailable, + 'EQuery:Unknown asset': BadSymbol, # {"error":["EQuery:Unknown asset"]} + 'EAPI:Rate limit exceeded': DDoSProtection, + 'EOrder:Rate limit exceeded': DDoSProtection, + 'EGeneral:Internal error': ExchangeNotAvailable, + 'EGeneral:Temporary lockout': DDoSProtection, + 'EGeneral:Permission denied': PermissionDenied, + 'EGeneral:Invalid arguments:price': InvalidOrder, + 'EOrder:Unknown order': InvalidOrder, + 'EOrder:Invalid price:Invalid price argument': InvalidOrder, + 'EOrder:Order minimum not met': InvalidOrder, + 'EOrder:Insufficient funds': InsufficientFunds, + 'EGeneral:Invalid arguments': BadRequest, + 'ESession:Invalid session': AuthenticationError, + 'EAPI:Invalid nonce': InvalidNonce, + 'EFunding:No funding method': BadRequest, # {"error":"EFunding:No funding method"} + 'EFunding:Unknown asset': BadSymbol, # {"error":["EFunding:Unknown asset"]} + 'EService:Market in post_only mode': OnMaintenance, # {"error":["EService:Market in post_only mode"]} + 'EGeneral:Too many requests': DDoSProtection, # {"error":["EGeneral:Too many requests"]} + 'ETrade:User Locked': AccountSuspended, # {"error":["ETrade:User Locked"]} + }, + 'broad': { + ':Invalid order': InvalidOrder, + ':Invalid arguments:volume': InvalidOrder, + ':Invalid arguments:viqc': InvalidOrder, + ':Invalid nonce': InvalidNonce, + ':IInsufficient funds': InsufficientFunds, + ':Cancel pending': CancelPending, + ':Rate limit exceeded': RateLimitExceeded, + }, + }, + }) + + def fee_to_precision(self, symbol, fee): + return self.decimal_to_precision(fee, TRUNCATE, self.markets[symbol]['precision']['amount'], self.precisionMode) + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for kraken + + https://docs.kraken.com/rest/#tag/Spot-Market-Data/operation/getTradableAssetPairs + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + promises = [] + promises.append(self.publicGetAssetPairs(params)) + if self.options['adjustForTimeDifference']: + promises.append(self.load_time_difference()) + responses = await asyncio.gather(*promises) + assetsResponse = responses[0] + # + # { + # "error": [], + # "result": { + # "ADAETH": { + # "altname": "ADAETH", + # "wsname": "ADA\/ETH", + # "aclass_base": "currency", + # "base": "ADA", + # "aclass_quote": "currency", + # "quote": "XETH", + # "lot": "unit", + # "pair_decimals": 7, + # "lot_decimals": 8, + # "lot_multiplier": 1, + # "leverage_buy": [], + # "leverage_sell": [], + # "fees": [ + # [0, 0.26], + # [50000, 0.24], + # [100000, 0.22], + # [250000, 0.2], + # [500000, 0.18], + # [1000000, 0.16], + # [2500000, 0.14], + # [5000000, 0.12], + # [10000000, 0.1] + # ], + # "fees_maker": [ + # [0, 0.16], + # [50000, 0.14], + # [100000, 0.12], + # [250000, 0.1], + # [500000, 0.08], + # [1000000, 0.06], + # [2500000, 0.04], + # [5000000, 0.02], + # [10000000, 0] + # ], + # "fee_volume_currency": "ZUSD", + # "margin_call": 80, + # "margin_stop": 40, + # "ordermin": "1" + # }, + # } + # } + # + markets = self.safe_dict(assetsResponse, 'result', {}) + cachedCurrencies = self.safe_dict(self.options, 'cachedCurrencies', {}) + keys = list(markets.keys()) + result = [] + for i in range(0, len(keys)): + id = keys[i] + market = markets[id] + baseIdRaw = self.safe_string(market, 'base') + quoteIdRaw = self.safe_string(market, 'quote') + baseId = self.safe_currency_code(baseIdRaw) + quoteId = self.safe_currency_code(quoteIdRaw) + base = baseId + quote = quoteId + makerFees = self.safe_list(market, 'fees_maker', []) + firstMakerFee = self.safe_list(makerFees, 0, []) + firstMakerFeeRate = self.safe_string(firstMakerFee, 1) + maker = None + if firstMakerFeeRate is not None: + maker = self.parse_number(Precise.string_div(firstMakerFeeRate, '100')) + takerFees = self.safe_list(market, 'fees', []) + firstTakerFee = self.safe_list(takerFees, 0, []) + firstTakerFeeRate = self.safe_string(firstTakerFee, 1) + taker = None + if firstTakerFeeRate is not None: + taker = self.parse_number(Precise.string_div(firstTakerFeeRate, '100')) + leverageBuy = self.safe_list(market, 'leverage_buy', []) + leverageBuyLength = len(leverageBuy) + precisionPrice = self.parse_number(self.parse_precision(self.safe_string(market, 'pair_decimals'))) + precisionAmount = self.parse_number(self.parse_precision(self.safe_string(market, 'lot_decimals'))) + spot = True + # fix https://github.com/freqtrade/freqtrade/issues/11765#issuecomment-2894224103 + if spot and (base in cachedCurrencies): + currency = cachedCurrencies[base] + currencyPrecision = self.safe_number(currency, 'precision') + # if currency precision is greater(e.g. 0.01) than market precision(e.g. 0.001) + if currencyPrecision > precisionAmount: + precisionAmount = currencyPrecision + status = self.safe_string(market, 'status') + isActive = status == 'online' + result.append({ + 'id': id, + 'wsId': self.safe_string(market, 'wsname'), + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'altname': market['altname'], + 'type': 'spot', + 'spot': spot, + 'margin': (leverageBuyLength > 0), + 'swap': False, + 'future': False, + 'option': False, + 'active': isActive, + 'contract': False, + 'linear': None, + 'inverse': None, + 'taker': taker, + 'maker': maker, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': precisionAmount, + 'price': precisionPrice, + }, + 'limits': { + 'leverage': { + 'min': self.parse_number('1'), + 'max': self.safe_number(leverageBuy, leverageBuyLength - 1, 1), + }, + 'amount': { + 'min': self.safe_number(market, 'ordermin'), + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': self.safe_number(market, 'costmin'), + 'max': None, + }, + }, + 'created': None, + 'info': market, + }) + self.options['marketsByAltname'] = self.index_by(result, 'altname') + return result + + async def fetch_status(self, params={}): + """ + the latest known information on the availability of the exchange API + + https://docs.kraken.com/api/docs/rest-api/get-system-status/ + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `status structure ` + """ + response = await self.publicGetSystemStatus(params) + # + # { + # error: [], + # result: {status: 'online', timestamp: '2024-07-22T16:34:44Z'} + # } + # + result = self.safe_dict(response, 'result') + statusRaw = self.safe_string(result, 'status') + return { + 'status': 'ok' if (statusRaw == 'online') else 'maintenance', + 'updated': None, + 'eta': None, + 'url': None, + 'info': response, + } + + async def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://docs.kraken.com/rest/#tag/Spot-Market-Data/operation/getAssetInfo + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = await self.publicGetAssets(params) + # + # { + # "error": [], + # "result": { + # "ATOM": { + # "aclass": "currency", + # "altname": "ATOM", + # "collateral_value": "0.7", + # "decimals": 8, + # "display_decimals": 6, + # "margin_rate": 0.02, + # "status": "enabled", + # }, + # "ATOM.S": { + # "aclass": "currency", + # "altname": "ATOM.S", + # "decimals": 8, + # "display_decimals": 6, + # "status": "enabled", + # }, + # "XXBT": { + # "aclass": "currency", + # "altname": "XBT", + # "decimals": 10, + # "display_decimals": 5, + # "margin_rate": 0.01, + # "status": "enabled", + # }, + # "XETH": { + # "aclass": "currency", + # "altname": "ETH", + # "decimals": 10, + # "display_decimals": 5 + # "margin_rate": 0.02, + # "status": "enabled", + # }, + # "XBT.M": { + # "aclass": "currency", + # "altname": "XBT.M", + # "decimals": 10, + # "display_decimals": 5 + # "status": "enabled", + # }, + # "ETH.M": { + # "aclass": "currency", + # "altname": "ETH.M", + # "decimals": 10, + # "display_decimals": 5 + # "status": "enabled", + # }, + # ... + # }, + # } + # + currencies = self.safe_value(response, 'result', {}) + ids = list(currencies.keys()) + result: dict = {} + for i in range(0, len(ids)): + id = ids[i] + currency = currencies[id] + # todo: will need to rethink the fees + # see: https://support.kraken.com/hc/en-us/articles/201893608-What-are-the-withdrawal-fees- + # to add support for multiple withdrawal/deposit methods and + # differentiated fees for each particular method + # + # Notes about abbreviations: + # Z and X prefixes: https://support.kraken.com/hc/en-us/articles/360001206766-Bitcoin-currency-code-XBT-vs-BTC + # S and M suffixes: https://support.kraken.com/hc/en-us/articles/360039879471-What-is-Asset-S-and-Asset-M- + # + code = self.safe_currency_code(id) + # the below can not be reliable done in `safeCurrencyCode`, so we have to do it here + if id.find('.') < 0: + altName = self.safe_string(currency, 'altname') + # handle cases like below: + # + # id | altname + # --------------- + # XXBT | XBT + # ZUSD | USD + if id != altName and (id.startswith('X') or id.startswith('Z')): + code = self.safe_currency_code(altName) + # also, add map in commonCurrencies: + self.commonCurrencies[id] = code + else: + code = self.safe_currency_code(id) + isFiat = code.find('.HOLD') >= 0 + result[code] = self.safe_currency_structure({ + 'id': id, + 'code': code, + 'info': currency, + 'name': self.safe_string(currency, 'altname'), + 'active': self.safe_string(currency, 'status') == 'enabled', + 'type': 'fiat' if isFiat else 'crypto', + 'deposit': None, + 'withdraw': None, + 'fee': None, + 'precision': self.parse_number(self.parse_precision(self.safe_string(currency, 'decimals'))), + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + }, + 'networks': {}, + }) + return result + + def safe_currency_code(self, currencyId: Str, currency: Currency = None) -> Str: + if currencyId is None: + return currencyId + if currencyId.find('.') > 0: + # if ID contains .M, .S or .F, then it can't contain X or Z prefix. in such case, ID equals to ALTNAME + parts = currencyId.split('.') + firstPart = self.safe_string(parts, 0) + secondPart = self.safe_string(parts, 1) + return super(kraken, self).safe_currency_code(firstPart, currency) + '.' + secondPart + return super(kraken, self).safe_currency_code(currencyId, currency) + + async def fetch_trading_fee(self, symbol: str, params={}) -> TradingFeeInterface: + """ + fetch the trading fees for a market + + https://docs.kraken.com/rest/#tag/Account-Data/operation/getTradeVolume + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `fee structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + 'fee-info': True, + } + response = await self.privatePostTradeVolume(self.extend(request, params)) + # + # { + # "error": [], + # "result": { + # "currency": 'ZUSD', + # "volume": '0.0000', + # "fees": { + # "XXBTZUSD": { + # "fee": '0.2600', + # "minfee": '0.1000', + # "maxfee": '0.2600', + # "nextfee": '0.2400', + # "tiervolume": '0.0000', + # "nextvolume": '50000.0000' + # } + # }, + # "fees_maker": { + # "XXBTZUSD": { + # "fee": '0.1600', + # "minfee": '0.0000', + # "maxfee": '0.1600', + # "nextfee": '0.1400', + # "tiervolume": '0.0000', + # "nextvolume": '50000.0000' + # } + # } + # } + # } + # + result = self.safe_value(response, 'result', {}) + return self.parse_trading_fee(result, market) + + def parse_trading_fee(self, response, market): + makerFees = self.safe_value(response, 'fees_maker', {}) + takerFees = self.safe_value(response, 'fees', {}) + symbolMakerFee = self.safe_value(makerFees, market['id'], {}) + symbolTakerFee = self.safe_value(takerFees, market['id'], {}) + return { + 'info': response, + 'symbol': market['symbol'], + 'maker': self.parse_number(Precise.string_div(self.safe_string(symbolMakerFee, 'fee'), '100')), + 'taker': self.parse_number(Precise.string_div(self.safe_string(symbolTakerFee, 'fee'), '100')), + 'percentage': True, + 'tierBased': True, + } + + def parse_bid_ask(self, bidask, priceKey: IndexType = 0, amountKey: IndexType = 1, countOrIdKey: IndexType = 2): + price = self.safe_number(bidask, priceKey) + amount = self.safe_number(bidask, amountKey) + timestamp = self.safe_integer(bidask, 2) + return [price, amount, timestamp] + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://docs.kraken.com/rest/#tag/Spot-Market-Data/operation/getOrderBook + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + if limit is not None: + request['count'] = limit # 100 + response = await self.publicGetDepth(self.extend(request, params)) + # + # { + # "error":[], + # "result":{ + # "XETHXXBT":{ + # "asks":[ + # ["0.023480","4.000",1586321307], + # ["0.023490","50.095",1586321306], + # ["0.023500","28.535",1586321302], + # ], + # "bids":[ + # ["0.023470","59.580",1586321307], + # ["0.023460","20.000",1586321301], + # ["0.023440","67.832",1586321306], + # ] + # } + # } + # } + # + result = self.safe_value(response, 'result', {}) + orderbook = self.safe_value(result, market['id']) + # sometimes kraken returns wsname instead of market id + # https://github.com/ccxt/ccxt/issues/8662 + marketInfo = self.safe_value(market, 'info', {}) + wsName = self.safe_value(marketInfo, 'wsname') + if wsName is not None: + orderbook = self.safe_value(result, wsName, orderbook) + return self.parse_order_book(orderbook, symbol) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "a":["2432.77000","1","1.000"], + # "b":["2431.37000","2","2.000"], + # "c":["2430.58000","0.04408910"], + # "v":["4147.94474901","8896.96086304"], + # "p":["2456.22239","2568.63032"], + # "t":[3907,10056], + # "l":["2302.18000","2302.18000"], + # "h":["2621.14000","2860.01000"], + # "o":"2571.56000" + # } + # + symbol = self.safe_symbol(None, market) + v = self.safe_value(ticker, 'v', []) + baseVolume = self.safe_string(v, 1) + p = self.safe_value(ticker, 'p', []) + vwap = self.safe_string(p, 1) + quoteVolume = Precise.string_mul(baseVolume, vwap) + c = self.safe_value(ticker, 'c', []) + last = self.safe_string(c, 0) + high = self.safe_value(ticker, 'h', []) + low = self.safe_value(ticker, 'l', []) + bid = self.safe_value(ticker, 'b', []) + ask = self.safe_value(ticker, 'a', []) + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': None, + 'datetime': None, + 'high': self.safe_string(high, 1), + 'low': self.safe_string(low, 1), + 'bid': self.safe_string(bid, 0), + 'bidVolume': self.safe_string(bid, 2), + 'ask': self.safe_string(ask, 0), + 'askVolume': self.safe_string(ask, 2), + 'vwap': vwap, + 'open': self.safe_string(ticker, 'o'), + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'info': ticker, + }, market) + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://docs.kraken.com/rest/#tag/Spot-Market-Data/operation/getTickerInformation + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + request: dict = {} + if symbols is not None: + symbols = self.market_symbols(symbols) + marketIds = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + market = self.markets[symbol] + if market['active']: + marketIds.append(market['id']) + request['pair'] = ','.join(marketIds) + response = await self.publicGetTicker(self.extend(request, params)) + tickers = response['result'] + ids = list(tickers.keys()) + result: dict = {} + for i in range(0, len(ids)): + id = ids[i] + market = self.safe_market(id) + symbol = market['symbol'] + ticker = tickers[id] + result[symbol] = self.parse_ticker(ticker, market) + return self.filter_by_array_tickers(result, 'symbol', symbols) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://docs.kraken.com/rest/#tag/Spot-Market-Data/operation/getTickerInformation + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + response = await self.publicGetTicker(self.extend(request, params)) + ticker = response['result'][market['id']] + return self.parse_ticker(ticker, market) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # [ + # 1591475640, + # "0.02500", + # "0.02500", + # "0.02500", + # "0.02500", + # "0.02500", + # "9.12201000", + # 5 + # ] + # + return [ + self.safe_timestamp(ohlcv, 0), + self.safe_number(ohlcv, 1), + self.safe_number(ohlcv, 2), + self.safe_number(ohlcv, 3), + self.safe_number(ohlcv, 4), + self.safe_number(ohlcv, 6), + ] + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://docs.kraken.com/api/docs/rest-api/get-ohlc-data + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate') + if paginate: + return await self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params, 720) + market = self.market(symbol) + parsedTimeframe = self.safe_integer(self.timeframes, timeframe) + request: dict = { + 'pair': market['id'], + } + if parsedTimeframe is not None: + request['interval'] = parsedTimeframe + else: + request['interval'] = timeframe + if since is not None: + scaledSince = self.parse_to_int(since / 1000) + timeFrameInSeconds = parsedTimeframe * 60 + request['since'] = self.number_to_string(scaledSince - timeFrameInSeconds) # expected to be in seconds + response = await self.publicGetOHLC(self.extend(request, params)) + # + # { + # "error":[], + # "result":{ + # "XETHXXBT":[ + # [1591475580,"0.02499","0.02499","0.02499","0.02499","0.00000","0.00000000",0], + # [1591475640,"0.02500","0.02500","0.02500","0.02500","0.02500","9.12201000",5], + # [1591475700,"0.02499","0.02499","0.02499","0.02499","0.02499","1.28681415",2], + # [1591475760,"0.02499","0.02499","0.02499","0.02499","0.02499","0.08800000",1], + # ], + # "last":1591517580 + # } + # } + result = self.safe_value(response, 'result', {}) + ohlcvs = self.safe_list(result, market['id'], []) + return self.parse_ohlcvs(ohlcvs, market, timeframe, since, limit) + + def parse_ledger_entry_type(self, type): + types: dict = { + 'trade': 'trade', + 'withdrawal': 'transaction', + 'deposit': 'transaction', + 'transfer': 'transfer', + 'margin': 'margin', + } + return self.safe_string(types, type, type) + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + # + # { + # 'LTFK7F-N2CUX-PNY4SX': { + # "refid": "TSJTGT-DT7WN-GPPQMJ", + # "time": 1520102320.555, + # "type": "trade", + # "aclass": "currency", + # "asset": "XETH", + # "amount": "0.1087194600", + # "fee": "0.0000000000", + # "balance": "0.2855851000" + # }, + # ... + # } + # + id = self.safe_string(item, 'id') + direction = None + account = None + referenceId = self.safe_string(item, 'refid') + referenceAccount = None + type = self.parse_ledger_entry_type(self.safe_string(item, 'type')) + currencyId = self.safe_string(item, 'asset') + code = self.safe_currency_code(currencyId, currency) + currency = self.safe_currency(currencyId, currency) + amount = self.safe_string(item, 'amount') + if Precise.string_lt(amount, '0'): + direction = 'out' + amount = Precise.string_abs(amount) + else: + direction = 'in' + timestamp = self.safe_integer_product(item, 'time', 1000) + return self.safe_ledger_entry({ + 'info': item, + 'id': id, + 'direction': direction, + 'account': account, + 'referenceId': referenceId, + 'referenceAccount': referenceAccount, + 'type': type, + 'currency': code, + 'amount': self.parse_number(amount), + 'before': None, + 'after': self.safe_number(item, 'balance'), + 'status': 'ok', + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'fee': { + 'cost': self.safe_number(item, 'fee'), + 'currency': code, + }, + }, currency) + + async def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + + https://docs.kraken.com/rest/#tag/Account-Data/operation/getLedgers + + :param str [code]: unified currency code, default is None + :param int [since]: timestamp in ms of the earliest ledger entry, default is None + :param int [limit]: max number of ledger entries to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest ledger entry + :param int [params.end]: timestamp in seconds of the latest ledger entry + :returns dict: a `ledger structure ` + """ + # https://www.kraken.com/features/api#get-ledgers-info + await self.load_markets() + request: dict = {} + currency = None + if code is not None: + currency = self.currency(code) + request['asset'] = currency['id'] + if since is not None: + request['start'] = self.parse_to_int(since / 1000) + until = self.safe_string_n(params, ['until', 'till']) + if until is not None: + params = self.omit(params, ['until', 'till']) + untilDivided = Precise.string_div(until, '1000') + request['end'] = self.parse_to_int(Precise.string_add(untilDivided, '1')) + response = await self.privatePostLedgers(self.extend(request, params)) + # { error: [], + # "result": {ledger: {'LPUAIB-TS774-UKHP7X': { refid: "A2B4HBV-L4MDIE-JU4N3N", + # "time": 1520103488.314, + # "type": "withdrawal", + # "aclass": "currency", + # "asset": "XETH", + # "amount": "-0.2805800000", + # "fee": "0.0050000000", + # "balance": "0.0000051000" }, + result = self.safe_value(response, 'result', {}) + ledger = self.safe_value(result, 'ledger', {}) + keys = list(ledger.keys()) + items = [] + for i in range(0, len(keys)): + key = keys[i] + value = ledger[key] + value['id'] = key + items.append(value) + return self.parse_ledger(items, currency, since, limit) + + async def fetch_ledger_entries_by_ids(self, ids, code: Str = None, params={}): + # https://www.kraken.com/features/api#query-ledgers + await self.load_markets() + ids = ','.join(ids) + request = self.extend({ + 'id': ids, + }, params) + response = await self.privatePostQueryLedgers(request) + # { error: [], + # "result": {'LPUAIB-TS774-UKHP7X': { refid: "A2B4HBV-L4MDIE-JU4N3N", + # "time": 1520103488.314, + # "type": "withdrawal", + # "aclass": "currency", + # "asset": "XETH", + # "amount": "-0.2805800000", + # "fee": "0.0050000000", + # "balance": "0.0000051000" }}} + result = response['result'] + keys = list(result.keys()) + items = [] + for i in range(0, len(keys)): + key = keys[i] + value = result[key] + value['id'] = key + items.append(value) + return self.parse_ledger(items) + + async def fetch_ledger_entry(self, id: str, code: Str = None, params={}) -> LedgerEntry: + items = await self.fetch_ledger_entries_by_ids([id], code, params) + return items[0] + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(public) + # + # [ + # "0.032310", # price + # "4.28169434", # amount + # 1541390792.763, # timestamp + # "s", # sell or buy + # "l", # limit or market + # "" + # ] + # + # fetchOrderTrades(private) + # + # { + # "id": 'TIMIRG-WUNNE-RRJ6GT', # injected from outside + # "ordertxid": 'OQRPN2-LRHFY-HIFA7D', + # "postxid": 'TKH2SE-M7IF5-CFI7LT', + # "pair": 'USDCUSDT', + # "time": 1586340086.457, + # "type": 'sell', + # "ordertype": 'market', + # "price": '0.99860000', + # "cost": '22.16892001', + # "fee": '0.04433784', + # "vol": '22.20000000', + # "margin": '0.00000000', + # "misc": '' + # } + # + # fetchMyTrades + # + # { + # "ordertxid": "OSJVN7-A2AE-63WZV", + # "postxid": "TBP7O6-PNXI-CONU", + # "pair": "XXBTZUSD", + # "time": 1710429248.3052235, + # "type": "sell", + # "ordertype": "liquidation market", + # "price": "72026.50000", + # "cost": "7.20265", + # "fee": "0.01873", + # "vol": "0.00010000", + # "margin": "1.44053", + # "leverage": "5", + # "misc": "closing", + # "trade_id": 68230622, + # "maker": False + # } + # + # watchTrades + # + # { + # "symbol": "BTC/USD", + # "side": "buy", + # "price": 109601.2, + # "qty": 0.04561994, + # "ord_type": "market", + # "trade_id": 83449369, + # "timestamp": "2025-05-27T11:24:03.847761Z" + # } + # + timestamp = None + datetime = None + side = None + type = None + price = None + amount = None + id = None + orderId = None + fee = None + symbol = None + if isinstance(trade, list): + timestamp = self.safe_timestamp(trade, 2) + side = 'sell' if (trade[3] == 's') else 'buy' + type = 'limit' if (trade[4] == 'l') else 'market' + price = self.safe_string(trade, 0) + amount = self.safe_string(trade, 1) + tradeLength = len(trade) + if tradeLength > 6: + id = self.safe_string(trade, 6) # artificially added #1794 + elif isinstance(trade, str): + id = trade + elif 'ordertxid' in trade: + marketId = self.safe_string(trade, 'pair') + foundMarket = self.find_market_by_altname_or_id(marketId) + if foundMarket is not None: + market = foundMarket + elif marketId is not None: + # delisted market ids go here + market = self.get_delisted_market_by_id(marketId) + orderId = self.safe_string(trade, 'ordertxid') + id = self.safe_string_2(trade, 'id', 'postxid') + timestamp = self.safe_timestamp(trade, 'time') + side = self.safe_string(trade, 'type') + type = self.safe_string(trade, 'ordertype') + price = self.safe_string(trade, 'price') + amount = self.safe_string(trade, 'vol') + if 'fee' in trade: + currency = None + if market is not None: + currency = market['quote'] + fee = { + 'cost': self.safe_string(trade, 'fee'), + 'currency': currency, + } + else: + symbol = self.safe_string(trade, 'symbol') + datetime = self.safe_string(trade, 'timestamp') + id = self.safe_string(trade, 'trade_id') + side = self.safe_string(trade, 'side') + type = self.safe_string(trade, 'ord_type') + price = self.safe_string(trade, 'price') + amount = self.safe_string(trade, 'qty') + if market is not None: + symbol = market['symbol'] + cost = self.safe_string(trade, 'cost') + maker = self.safe_bool(trade, 'maker') + takerOrMaker = None + if maker is not None: + takerOrMaker = 'maker' if maker else 'taker' + if datetime is None: + datetime = self.iso8601(timestamp) + else: + timestamp = self.parse8601(datetime) + return self.safe_trade({ + 'id': id, + 'order': orderId, + 'info': trade, + 'timestamp': timestamp, + 'datetime': datetime, + 'symbol': symbol, + 'type': type, + 'side': side, + 'takerOrMaker': takerOrMaker, + 'price': price, + 'amount': amount, + 'cost': cost, + 'fee': fee, + }, market) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://docs.kraken.com/rest/#tag/Spot-Market-Data/operation/getRecentTrades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + id = market['id'] + request: dict = { + 'pair': id, + } + # https://support.kraken.com/hc/en-us/articles/218198197-How-to-pull-all-trade-data-using-the-Kraken-REST-API + # https://github.com/ccxt/ccxt/issues/5677 + if since is not None: + request['since'] = self.number_to_string(self.parse_to_int(since / 1000)) # expected to be in seconds + if limit is not None: + request['count'] = limit + response = await self.publicGetTrades(self.extend(request, params)) + # + # { + # "error": [], + # "result": { + # "XETHXXBT": [ + # ["0.032310","4.28169434",1541390792.763,"s","l",""] + # ], + # "last": "1541439421200678657" + # } + # } + # + result = response['result'] + trades = result[id] + # trades is a sorted array: last(most recent trade) goes last + length = len(trades) + if length <= 0: + return [] + lastTrade = trades[length - 1] + lastTradeId = self.safe_string(result, 'last') + lastTrade.append(lastTradeId) + trades[length - 1] = lastTrade + return self.parse_trades(trades, market, since, limit) + + def parse_balance(self, response) -> Balances: + balances = self.safe_value(response, 'result', {}) + result: dict = { + 'info': response, + 'timestamp': None, + 'datetime': None, + } + currencyIds = list(balances.keys()) + for i in range(0, len(currencyIds)): + currencyId = currencyIds[i] + code = self.safe_currency_code(currencyId) + balance = self.safe_value(balances, currencyId, {}) + account = self.account() + account['used'] = self.safe_string(balance, 'hold_trade') + account['total'] = self.safe_string(balance, 'balance') + result[code] = account + return self.safe_balance(result) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://docs.kraken.com/rest/#tag/Account-Data/operation/getExtendedBalance + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + response = await self.privatePostBalanceEx(params) + # + # { + # "error": [], + # "result": { + # "ZUSD": { + # "balance": 25435.21, + # "hold_trade": 8249.76 + # }, + # "XXBT": { + # "balance": 1.2435, + # "hold_trade": 0.8423 + # } + # } + # } + # + return self.parse_balance(response) + + async def create_market_order_with_cost(self, symbol: str, side: OrderSide, cost: float, params={}): + """ + create a market order by providing the symbol, side and cost + + https://docs.kraken.com/rest/#tag/Spot-Trading/operation/addOrder + + :param str symbol: unified symbol of the market to create an order in(only USD markets are supported) + :param str side: 'buy' or 'sell' + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + # only buy orders are supported by the endpoint + req = { + 'cost': cost, + } + return await self.create_order(symbol, 'market', side, cost, None, self.extend(req, params)) + + async def create_market_buy_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market buy order by providing the symbol, side and cost + + https://docs.kraken.com/rest/#tag/Spot-Trading/operation/addOrder + + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + return await self.create_market_order_with_cost(symbol, 'buy', cost, params) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://docs.kraken.com/api/docs/rest-api/add-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.postOnly]: if True, the order will only be posted to the order book and not executed immediately + :param bool [params.reduceOnly]: *margin only* indicates if self order is to reduce the size of a position + :param float [params.stopLossPrice]: *margin only* the price that a stop loss order is triggered at + :param float [params.takeProfitPrice]: *margin only* the price that a take profit order is triggered at + :param str [params.trailingAmount]: *margin only* the quote amount to trail away from the current market price + :param str [params.trailingPercent]: *margin only* the percent to trail away from the current market price + :param str [params.trailingLimitAmount]: *margin only* the quote amount away from the trailingAmount + :param str [params.trailingLimitPercent]: *margin only* the percent away from the trailingAmount + :param str [params.offset]: *margin only* '+' or '-' whether you want the trailingLimitAmount value to be positive or negative, default is negative '-' + :param str [params.trigger]: *margin only* the activation price type, 'last' or 'index', default is 'last' + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + 'type': side, + 'ordertype': type, + 'volume': self.amount_to_precision(symbol, amount), + } + orderRequest = self.order_request('createOrder', symbol, type, request, amount, price, params) + flags = self.safe_string(orderRequest[0], 'oflags', '') + isUsingCost = flags.find('viqc') > -1 + response = await self.privatePostAddOrder(self.extend(orderRequest[0], orderRequest[1])) + # + # { + # "error": [], + # "result": { + # "descr": {order: 'buy 0.02100000 ETHUSDT @ limit 330.00'}, # see more examples in "parseOrder" + # "txid": ['OEKVV2-IH52O-TPL6GZ'] + # } + # } + # + result = self.safe_dict(response, 'result') + result['usingCost'] = isUsingCost + # it's impossible to know if the order was created using cost or base currency + # becuase kraken only returns something like self: {order: 'buy 10.00000000 LTCUSD @ market'} + # self usingCost flag is used to help the parsing but omited from the order + return self.parse_order(result) + + async def create_orders(self, orders: List[OrderRequest], params={}): + """ + create a list of trade orders + + https://docs.kraken.com/api/docs/rest-api/add-order-batch/ + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + ordersRequests = [] + orderSymbols = [] + symbol = None + market = None + for i in range(0, len(orders)): + rawOrder = orders[i] + marketId = self.safe_string(rawOrder, 'symbol') + if symbol is None: + symbol = marketId + else: + if symbol != marketId: + raise BadRequest(self.id + ' createOrders() requires all orders to have the same symbol') + market = self.market(marketId) + orderSymbols.append(marketId) + type = self.safe_string(rawOrder, 'type') + side = self.safe_string(rawOrder, 'side') + amount = self.safe_value(rawOrder, 'amount') + price = self.safe_value(rawOrder, 'price') + orderParams = self.safe_dict(rawOrder, 'params', {}) + req: dict = { + 'type': side, + 'ordertype': type, + 'volume': self.amount_to_precision(market['symbol'], amount), + } + orderRequest = self.order_request('createOrders', marketId, type, req, amount, price, orderParams) + ordersRequests.append(orderRequest[0]) + orderSymbols = self.market_symbols(orderSymbols, None, False, True, True) + response = None + request: dict = { + 'orders': ordersRequests, + 'pair': market['id'], + } + request = self.extend(request, params) + response = await self.privatePostAddOrderBatch(request) + # + # { + # "error":[ + # ], + # "result":{ + # "orders":[ + # { + # "txid":"OEPPJX-34RMM-OROGZE", + # "descr":{ + # "order":"sell 6.000000 ADAUSDC @ limit 0.400000" + # } + # }, + # { + # "txid":"OLQY7O-OYBXW-W23PGL", + # "descr":{ + # "order":"sell 6.000000 ADAUSDC @ limit 0.400000" + # } + # } + # ] + # } + # + result = self.safe_dict(response, 'result', {}) + return self.parse_orders(self.safe_list(result, 'orders')) + + def find_market_by_altname_or_id(self, id): + marketsByAltname = self.safe_value(self.options, 'marketsByAltname', {}) + if id in marketsByAltname: + return marketsByAltname[id] + else: + return self.safe_market(id) + + def get_delisted_market_by_id(self, id): + if id is None: + return id + market = self.safe_value(self.options['delistedMarketsById'], id) + if market is not None: + return market + baseIdStart = 0 + baseIdEnd = 3 + quoteIdStart = 3 + quoteIdEnd = 6 + if len(id) == 8: + baseIdEnd = 4 + quoteIdStart = 4 + quoteIdEnd = 8 + elif len(id) == 7: + baseIdEnd = 4 + quoteIdStart = 4 + quoteIdEnd = 7 + baseId = id[baseIdStart:baseIdEnd] + quoteId = id[quoteIdStart:quoteIdEnd] + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + symbol = base + '/' + quote + market = { + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'baseId': baseId, + 'quoteId': quoteId, + } + self.options['delistedMarketsById'][id] = market + return market + + def parse_order_status(self, status: Str): + statuses: dict = { + 'pending': 'open', # order pending book entry + 'open': 'open', + 'pending_new': 'open', + 'new': 'open', + 'partially_filled': 'open', + 'filled': 'closed', + 'closed': 'closed', + 'canceled': 'canceled', + 'expired': 'expired', + } + return self.safe_string(statuses, status, status) + + def parse_order_type(self, status): + statuses: dict = { + # we dont add "space" delimited orders here(eg. stop loss) because they need separate parsing + 'take-profit': 'market', + 'stop-loss': 'market', + 'stop-loss-limit': 'limit', + 'take-profit-limit': 'limit', + 'trailing-stop-limit': 'limit', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder + # + # { + # "descr": { + # "order": "buy 0.02100000 ETHUSDT @ limit 330.00" # limit orders + # "buy 0.12345678 ETHUSDT @ market" # market order + # "sell 0.28002676 ETHUSDT @ stop loss 0.0123 -> limit 0.0.1222" # stop order + # "sell 0.00100000 ETHUSDT @ stop loss 2677.00 -> limit 2577.00 with 5:1 leverage" + # "buy 0.10000000 LTCUSDT @ take profit 75.00000 -> limit 74.00000" + # "sell 10.00000000 XRPEUR @ trailing stop +50.0000%" # trailing stop + # }, + # "txid": ['OEKVV2-IH52O-TPL6GZ'] + # } + # + # editOrder + # + # { + # "amend_id": "TJSMEH-AA67V-YUSQ6O" + # } + # + # ws - createOrder + # { + # "order_id": "OXM2QD-EALR2-YBAVEU" + # } + # + # ws - editOrder + # { + # "amend_id": "TJSMEH-AA67V-YUSQ6O", + # "order_id": "OXM2QD-EALR2-YBAVEU" + # } + # + # { + # "error": [], + # "result": { + # "open": { + # "OXVPSU-Q726F-L3SDEP": { + # "refid": null, + # "userref": 0, + # "status": "open", + # "opentm": 1706893367.4656649, + # "starttm": 0, + # "expiretm": 0, + # "descr": { + # "pair": "XRPEUR", + # "type": "sell", + # "ordertype": "trailing-stop", + # "price": "+50.0000%", + # "price2": "0", + # "leverage": "none", + # "order": "sell 10.00000000 XRPEUR @ trailing stop +50.0000%", + # "close": "" + # }, + # "vol": "10.00000000", + # "vol_exec": "0.00000000", + # "cost": "0.00000000", + # "fee": "0.00000000", + # "price": "0.00000000", + # "stopprice": "0.23424000", + # "limitprice": "0.46847000", + # "misc": "", + # "oflags": "fciq", + # "trigger": "index" + # } + # } + # } + # + # fetchOpenOrders + # + # { + # "refid": null, + # "userref": null, + # "cl_ord_id": "1234", + # "status": "open", + # "opentm": 1733815269.370054, + # "starttm": 0, + # "expiretm": 0, + # "descr": { + # "pair": "XBTUSD", + # "type": "buy", + # "ordertype": "limit", + # "price": "70000.0", + # "price2": "0", + # "leverage": "none", + # "order": "buy 0.00010000 XBTUSD @ limit 70000.0", + # "close": "" + # }, + # "vol": "0.00010000", + # "vol_exec": "0.00000000", + # "cost": "0.00000", + # "fee": "0.00000", + # "price": "0.00000", + # "stopprice": "0.00000", + # "limitprice": "0.00000", + # "misc": "", + # "oflags": "fciq" + # } + # + isUsingCost = self.safe_bool(order, 'usingCost', False) + order = self.omit(order, 'usingCost') + description = self.safe_dict(order, 'descr', {}) + orderDescriptionObj = self.safe_dict(order, 'descr') # can be null + orderDescription = None + if orderDescriptionObj is not None: + orderDescription = self.safe_string(orderDescriptionObj, 'order') + else: + orderDescription = self.safe_string(order, 'descr') + side = None + rawType = None + marketId = None + price = None + amount = None + cost = None + triggerPrice = None + if orderDescription is not None: + parts = orderDescription.split(' ') + side = self.safe_string(parts, 0) + if not isUsingCost: + amount = self.safe_string(parts, 1) + else: + cost = self.safe_string(parts, 1) + marketId = self.safe_string(parts, 2) + part4 = self.safe_string(parts, 4) + part5 = self.safe_string(parts, 5) + if part4 == 'limit' or part4 == 'market': + rawType = part4 # eg, limit, market + else: + rawType = part4 + ' ' + part5 # eg. stop loss, take profit, trailing stop + if rawType == 'stop loss' or rawType == 'take profit': + triggerPrice = self.safe_string(parts, 6) + price = self.safe_string(parts, 9) + elif rawType == 'limit': + price = self.safe_string(parts, 5) + side = self.safe_string(description, 'type', side) + rawType = self.safe_string(description, 'ordertype', rawType) # orderType has dash, e.g. trailing-stop + marketId = self.safe_string(description, 'pair', marketId) + foundMarket = self.find_market_by_altname_or_id(marketId) + symbol = None + if foundMarket is not None: + market = foundMarket + elif marketId is not None: + # delisted market ids go here + market = self.get_delisted_market_by_id(marketId) + timestamp = self.safe_timestamp(order, 'opentm') + amount = self.safe_string(order, 'vol', amount) + filled = self.safe_string(order, 'vol_exec') + fee = None + # kraken truncates the cost in the api response so we will ignore it and calculate it from average & filled + # cost = self.safe_string(order, 'cost') + price = self.safe_string(description, 'price', price) + # when type = trailing stop returns price = '+50.0000%' + if (price is not None) and (price.endswith('%') or Precise.string_equals(price, '0.00000') or Precise.string_equals(price, '0')): + price = None # self is not the price we want + if price is None: + price = self.safe_string(description, 'price2') + price = self.safe_string_2(order, 'limitprice', 'price', price) + flags = self.safe_string(order, 'oflags', '') + isPostOnly = flags.find('post') > -1 + average = self.safe_number(order, 'price') + if market is not None: + symbol = market['symbol'] + if 'fee' in order: + feeCost = self.safe_string(order, 'fee') + fee = { + 'cost': feeCost, + 'rate': None, + } + if flags.find('fciq') >= 0: + fee['currency'] = market['quote'] + elif flags.find('fcib') >= 0: + fee['currency'] = market['base'] + status = self.parse_order_status(self.safe_string(order, 'status')) + id = self.safe_string_n(order, ['id', 'txid', 'order_id', 'amend_id']) + if (id is None) or (id.startswith('[')): + txid = self.safe_list(order, 'txid') + id = self.safe_string(txid, 0) + userref = self.safe_string(order, 'userref') + clientOrderId = self.safe_string(order, 'cl_ord_id', userref) + rawTrades = self.safe_value(order, 'trades', []) + trades = [] + for i in range(0, len(rawTrades)): + rawTrade = rawTrades[i] + if isinstance(rawTrade, str): + trades.append(self.safe_trade({'id': rawTrade, 'orderId': id, 'symbol': symbol, 'info': {}})) + else: + trades.append(rawTrade) + # in #24192 PR, self field is not something consistent/actual + # triggerPrice = self.omit_zero(self.safe_string(order, 'stopprice', triggerPrice)) + stopLossPrice = None + takeProfitPrice = None + # the dashed strings are not provided from fields(eg. fetch order) + # while spaced strings from "order" sentence(when other fields not available) + if rawType is not None: + if rawType.startswith('take-profit'): + takeProfitPrice = self.safe_string(description, 'price') + price = self.omit_zero(self.safe_string(description, 'price2')) + elif rawType.startswith('stop-loss'): + stopLossPrice = self.safe_string(description, 'price') + price = self.omit_zero(self.safe_string(description, 'price2')) + elif rawType == 'take profit': + takeProfitPrice = triggerPrice + elif rawType == 'stop loss': + stopLossPrice = triggerPrice + finalType = self.parse_order_type(rawType) + # unlike from endpoints which provide eg: "take-profit-limit" + # for "space-delimited" orders we dont have market/limit suffixes, their format is + # eg: `stop loss > limit 123`, so we need to parse them manually + if self.in_array(finalType, ['stop loss', 'take profit']): + finalType = 'market' if (price is None) else 'limit' + amendId = self.safe_string(order, 'amend_id') + if amendId is not None: + isPostOnly = None + return self.safe_order({ + 'id': id, + 'clientOrderId': clientOrderId, + 'info': order, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'status': status, + 'symbol': symbol, + 'type': finalType, + 'timeInForce': None, + 'postOnly': isPostOnly, + 'side': side, + 'price': price, + 'triggerPrice': triggerPrice, + 'takeProfitPrice': takeProfitPrice, + 'stopLossPrice': stopLossPrice, + 'cost': cost, + 'amount': amount, + 'filled': filled, + 'average': average, + 'remaining': None, + 'reduceOnly': self.safe_bool_2(order, 'reduceOnly', 'reduce_only'), + 'fee': fee, + 'trades': trades, + }, market) + + def order_request(self, method: str, symbol: str, type: str, request: dict, amount: Num, price: Num = None, params={}): + clientOrderId = self.safe_string(params, 'clientOrderId') + params = self.omit(params, ['clientOrderId']) + if clientOrderId is not None: + request['cl_ord_id'] = clientOrderId + stopLossTriggerPrice = self.safe_string(params, 'stopLossPrice') + takeProfitTriggerPrice = self.safe_string(params, 'takeProfitPrice') + isStopLossTriggerOrder = stopLossTriggerPrice is not None + isTakeProfitTriggerOrder = takeProfitTriggerPrice is not None + isStopLossOrTakeProfitTrigger = isStopLossTriggerOrder or isTakeProfitTriggerOrder + trailingAmount = self.safe_string(params, 'trailingAmount') + trailingPercent = self.safe_string(params, 'trailingPercent') + trailingLimitAmount = self.safe_string(params, 'trailingLimitAmount') + trailingLimitPercent = self.safe_string(params, 'trailingLimitPercent') + isTrailingAmountOrder = trailingAmount is not None + isTrailingPercentOrder = trailingPercent is not None + isLimitOrder = type.endswith('limit') # supporting limit, stop-loss-limit, take-profit-limit, etc + isMarketOrder = type == 'market' + cost = self.safe_string(params, 'cost') + flags = self.safe_string(params, 'oflags') + params = self.omit(params, ['cost', 'oflags']) + isViqcOrder = (flags is not None) and (flags.find('viqc') > -1) # volume in quote currency + if isMarketOrder and (cost is not None or isViqcOrder): + if cost is None and (amount is not None): + request['volume'] = self.cost_to_precision(symbol, self.number_to_string(amount)) + else: + request['volume'] = self.cost_to_precision(symbol, cost) + extendedOflags = flags + ',viqc' if (flags is not None) else 'viqc' + request['oflags'] = extendedOflags + elif isLimitOrder and not isTrailingAmountOrder and not isTrailingPercentOrder: + request['price'] = self.price_to_precision(symbol, price) + reduceOnly = self.safe_bool_2(params, 'reduceOnly', 'reduce_only') + if isStopLossOrTakeProfitTrigger: + if isStopLossTriggerOrder: + request['price'] = self.price_to_precision(symbol, stopLossTriggerPrice) + if isLimitOrder: + request['ordertype'] = 'stop-loss-limit' + else: + request['ordertype'] = 'stop-loss' + elif isTakeProfitTriggerOrder: + request['price'] = self.price_to_precision(symbol, takeProfitTriggerPrice) + if isLimitOrder: + request['ordertype'] = 'take-profit-limit' + else: + request['ordertype'] = 'take-profit' + if isLimitOrder: + request['price2'] = self.price_to_precision(symbol, price) + elif isTrailingAmountOrder or isTrailingPercentOrder: + trailingPercentString = None + if trailingPercent is not None: + trailingPercentString = ('+' + trailingPercent) if (trailingPercent.endswith('%')) else ('+' + trailingPercent + '%') + trailingAmountString = '+' + trailingAmount if (trailingAmount is not None) else None # must use + for self + offset = self.safe_string(params, 'offset', '-') # can use + or - for self + trailingLimitAmountString = offset + self.number_to_string(trailingLimitAmount) if (trailingLimitAmount is not None) else None + trailingActivationPriceType = self.safe_string(params, 'trigger', 'last') + request['trigger'] = trailingActivationPriceType + if isLimitOrder or (trailingLimitAmount is not None) or (trailingLimitPercent is not None): + request['ordertype'] = 'trailing-stop-limit' + if trailingLimitPercent is not None: + trailingLimitPercentString = (offset + trailingLimitPercent) if (trailingLimitPercent.endswith('%')) else (offset + trailingLimitPercent + '%') + request['price'] = trailingPercentString + request['price2'] = trailingLimitPercentString + elif trailingLimitAmount is not None: + request['price'] = trailingAmountString + request['price2'] = trailingLimitAmountString + else: + request['ordertype'] = 'trailing-stop' + if trailingPercent is not None: + request['price'] = trailingPercentString + else: + request['price'] = trailingAmountString + if reduceOnly: + if method == 'createOrderWs': + request['reduce_only'] = True # ws request can't have stringified bool + else: + request['reduce_only'] = 'true' # not using hasattr(self, boolean) case, because the urlencodedNested transforms it into 'True' string + close = self.safe_dict(params, 'close') + if close is not None: + close = self.extend({}, close) + closePrice = self.safe_value(close, 'price') + if closePrice is not None: + close['price'] = self.price_to_precision(symbol, closePrice) + closePrice2 = self.safe_value(close, 'price2') # stopPrice + if closePrice2 is not None: + close['price2'] = self.price_to_precision(symbol, closePrice2) + request['close'] = close + timeInForce = self.safe_string_2(params, 'timeInForce', 'timeinforce') + if timeInForce is not None: + request['timeinforce'] = timeInForce + isMarket = (type == 'market') + postOnly = None + postOnly, params = self.handle_post_only(isMarket, False, params) + if postOnly: + extendedPostFlags = flags + ',post' if (flags is not None) else 'post' + request['oflags'] = extendedPostFlags + if (flags is not None) and not ('oflags' in request): + request['oflags'] = flags + params = self.omit(params, ['timeInForce', 'reduceOnly', 'stopLossPrice', 'takeProfitPrice', 'trailingAmount', 'trailingPercent', 'trailingLimitAmount', 'trailingLimitPercent', 'offset']) + return [request, params] + + async def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + """ + edit a trade order + + https://docs.kraken.com/api/docs/rest-api/amend-order + + :param str id: order id + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float [amount]: how much of the currency you want to trade in units of the base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.stopLossPrice]: the price that a stop loss order is triggered at + :param float [params.takeProfitPrice]: the price that a take profit order is triggered at + :param str [params.trailingAmount]: the quote amount to trail away from the current market price + :param str [params.trailingPercent]: the percent to trail away from the current market price + :param str [params.trailingLimitAmount]: the quote amount away from the trailingAmount + :param str [params.trailingLimitPercent]: the percent away from the trailingAmount + :param str [params.offset]: '+' or '-' whether you want the trailingLimitAmount value to be positive or negative + :param boolean [params.postOnly]: if True, the order will only be posted to the order book and not executed immediately + :param str [params.clientOrderId]: the orders client order id + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' editOrder() does not support ' + market['type'] + ' orders, only spot orders are accepted') + request: dict = { + 'txid': id, + } + clientOrderId = self.safe_string_2(params, 'clientOrderId', 'cl_ord_id') + if clientOrderId is not None: + request['cl_ord_id'] = clientOrderId + params = self.omit(params, ['clientOrderId', 'cl_ord_id']) + request = self.omit(request, 'txid') + isMarket = (type == 'market') + postOnly = None + postOnly, params = self.handle_post_only(isMarket, False, params) + if postOnly: + request['post_only'] = 'true' # not using hasattr(self, boolean) case, because the urlencodedNested transforms it into 'True' string + if amount is not None: + request['order_qty'] = self.amount_to_precision(symbol, amount) + if price is not None: + request['limit_price'] = self.price_to_precision(symbol, price) + allTriggerPrices = self.safe_string_n(params, ['stopLossPrice', 'takeProfitPrice', 'trailingAmount', 'trailingPercent', 'trailingLimitAmount', 'trailingLimitPercent']) + if allTriggerPrices is not None: + offset = self.safe_string(params, 'offset') + params = self.omit(params, ['stopLossPrice', 'takeProfitPrice', 'trailingAmount', 'trailingPercent', 'trailingLimitAmount', 'trailingLimitPercent', 'offset']) + if offset is not None: + allTriggerPrices = offset + allTriggerPrices + request['trigger_price'] = allTriggerPrices + else: + request['trigger_price'] = self.price_to_precision(symbol, allTriggerPrices) + response = await self.privatePostAmendOrder(self.extend(request, params)) + # + # { + # "error": [], + # "result": { + # "amend_id": "TJSMEH-AA67V-YUSQ6O" + # } + # } + # + result = self.safe_dict(response, 'result', {}) + return self.parse_order(result, market) + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://docs.kraken.com/rest/#tag/Account-Data/operation/getOrdersInfo + + :param str id: order id + :param str symbol: not used by kraken fetchOrder + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + clientOrderId = self.safe_value_2(params, 'userref', 'clientOrderId') + request: dict = { + 'trades': True, # whether or not to include trades in output(optional, default False) + 'txid': id, # do not comma separate a list of ids - use fetchOrdersByIds instead + # 'userref': 'optional', # restrict results to given user reference id(optional) + } + query = params + if clientOrderId is not None: + request['userref'] = clientOrderId + query = self.omit(params, ['userref', 'clientOrderId']) + response = await self.privatePostQueryOrders(self.extend(request, query)) + # + # { + # "error":[], + # "result":{ + # "OTLAS3-RRHUF-NDWH5A":{ + # "refid":null, + # "userref":null, + # "status":"closed", + # "reason":null, + # "opentm":1586822919.3342, + # "closetm":1586822919.365, + # "starttm":0, + # "expiretm":0, + # "descr":{ + # "pair":"XBTUSDT", + # "type":"sell", + # "ordertype":"market", + # "price":"0", + # "price2":"0", + # "leverage":"none", + # "order":"sell 0.21804000 XBTUSDT @ market", + # "close":"" + # }, + # "vol":"0.21804000", + # "vol_exec":"0.21804000", + # "cost":"1493.9", + # "fee":"3.8", + # "price":"6851.5", + # "stopprice":"0.00000", + # "limitprice":"0.00000", + # "misc":"", + # "oflags":"fciq", + # "trades":["TT5UC3-GOIRW-6AZZ6R"] + # } + # } + # } + # + result = self.safe_value(response, 'result', []) + if not (id in result): + raise OrderNotFound(self.id + ' fetchOrder() could not find order id ' + id) + return self.parse_order(self.extend({'id': id}, result[id])) + + async def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all the trades made from a single order + + https://docs.kraken.com/rest/#tag/Account-Data/operation/getTradesInfo + + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + orderTrades = self.safe_value(params, 'trades') + tradeIds = [] + if orderTrades is None: + raise ArgumentsRequired(self.id + " fetchOrderTrades() requires a unified order structure in the params argument or a 'trades' param(an array of trade id strings)") + else: + for i in range(0, len(orderTrades)): + orderTrade = orderTrades[i] + if isinstance(orderTrade, str): + tradeIds.append(orderTrade) + else: + tradeIds.append(orderTrade['id']) + await self.load_markets() + if symbol is not None: + symbol = self.symbol(symbol) + options = self.safe_value(self.options, 'fetchOrderTrades', {}) + batchSize = self.safe_integer(options, 'batchSize', 20) + numTradeIds = len(tradeIds) + numBatches = self.parse_to_int(numTradeIds / batchSize) + numBatches = self.sum(numBatches, 1) + result = [] + for j in range(0, numBatches): + requestIds = [] + for k in range(0, batchSize): + index = self.sum(j * batchSize, k) + if index < numTradeIds: + requestIds.append(tradeIds[index]) + request: dict = { + 'txid': ','.join(requestIds), + } + response = await self.privatePostQueryTrades(request) + # + # { + # "error": [], + # "result": { + # 'TIMIRG-WUNNE-RRJ6GT': { + # "ordertxid": 'OQRPN2-LRHFY-HIFA7D', + # "postxid": 'TKH2SE-M7IF5-CFI7LT', + # "pair": 'USDCUSDT', + # "time": 1586340086.457, + # "type": 'sell', + # "ordertype": 'market', + # "price": '0.99860000', + # "cost": '22.16892001', + # "fee": '0.04433784', + # "vol": '22.20000000', + # "margin": '0.00000000', + # "misc": '' + # } + # } + # } + # + rawTrades = self.safe_value(response, 'result') + ids = list(rawTrades.keys()) + for i in range(0, len(ids)): + rawTrades[ids[i]]['id'] = ids[i] + trades = self.parse_trades(rawTrades, None, since, limit) + tradesFilteredBySymbol = self.filter_by_symbol(trades, symbol) + result = self.array_concat(result, tradesFilteredBySymbol) + return result + + async def fetch_orders_by_ids(self, ids, symbol: Str = None, params={}): + """ + fetch orders by the list of order id + + https://docs.kraken.com/rest/#tag/Account-Data/operation/getClosedOrders + + :param str[] [ids]: list of order id + :param str [symbol]: unified ccxt market symbol + :param dict [params]: extra parameters specific to the kraken api endpoint + :returns dict[]: a list of `order structure ` + """ + await self.load_markets() + response = await self.privatePostQueryOrders(self.extend({ + 'trades': True, # whether or not to include trades in output(optional, default False) + 'txid': ','.join(ids), # comma delimited list of transaction ids to query info about(20 maximum) + }, params)) + result = self.safe_value(response, 'result', {}) + orders = [] + orderIds = list(result.keys()) + for i in range(0, len(orderIds)): + id = orderIds[i] + item = result[id] + order = self.parse_order(self.extend({'id': id}, item)) + orders.append(order) + return orders + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://docs.kraken.com/api/docs/rest-api/get-trade-history + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest trade entry + :param int [params.end]: timestamp in seconds of the latest trade entry + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + request: dict = { + # 'type': 'all', # any position, closed position, closing position, no position + # 'trades': False, # whether or not to include trades related to position in output + # 'start': 1234567890, # starting unix timestamp or trade tx id of results(exclusive) + # 'end': 1234567890, # ending unix timestamp or trade tx id of results(inclusive) + # 'ofs' = result offset + } + if since is not None: + request['start'] = self.parse_to_int(since / 1000) + until = self.safe_string_n(params, ['until', 'till']) + if until is not None: + params = self.omit(params, ['until', 'till']) + untilDivided = Precise.string_div(until, '1000') + request['end'] = self.parse_to_int(Precise.string_add(untilDivided, '1')) + response = await self.privatePostTradesHistory(self.extend(request, params)) + # + # { + # "error": [], + # "result": { + # "trades": { + # "GJ3NYQ-XJRTF-THZABF": { + # "ordertxid": "TKH2SE-ZIF5E-CFI7LT", + # "postxid": "OEN3VX-M7IF5-JNBJAM", + # "pair": "XICNXETH", + # "time": 1527213229.4491, + # "type": "sell", + # "ordertype": "limit", + # "price": "0.001612", + # "cost": "0.025792", + # "fee": "0.000026", + # "vol": "16.00000000", + # "margin": "0.000000", + # "leverage": "5", + # "misc": "" + # "trade_id": 68230622, + # "maker": False + # }, + # ... + # }, + # "count": 9760, + # }, + # } + # + trades = response['result']['trades'] + ids = list(trades.keys()) + for i in range(0, len(ids)): + trades[ids[i]]['id'] = ids[i] + market = None + if symbol is not None: + market = self.market(symbol) + return self.parse_trades(trades, market, since, limit) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://docs.kraken.com/api/docs/rest-api/cancel-order + + :param str id: order id + :param str [symbol]: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.clientOrderId]: the orders client order id + :param int [params.userref]: the orders user reference id + :returns dict: an `order structure ` + """ + await self.load_markets() + response = None + requestId = self.safe_value(params, 'userref', id) # string or integer + params = self.omit(params, 'userref') + request: dict = { + 'txid': requestId, # order id or userref + } + clientOrderId = self.safe_string_2(params, 'clientOrderId', 'cl_ord_id') + if clientOrderId is not None: + request['cl_ord_id'] = clientOrderId + params = self.omit(params, ['clientOrderId', 'cl_ord_id']) + request = self.omit(request, 'txid') + try: + response = await self.privatePostCancelOrder(self.extend(request, params)) + # + # { + # error: [], + # result: { + # count: '1' + # } + # } + # + except Exception as e: + if self.last_http_response: + if self.last_http_response.find('EOrder:Unknown order') >= 0: + raise OrderNotFound(self.id + ' cancelOrder() error ' + self.last_http_response) + raise e + return self.safe_order({ + 'info': response, + }) + + async def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): + """ + cancel multiple orders + + https://docs.kraken.com/rest/#tag/Spot-Trading/operation/cancelOrderBatch + + :param str[] ids: open orders transaction ID(txid) or user reference(userref) + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an list of `order structures ` + """ + request: dict = { + 'orders': ids, + } + response = await self.privatePostCancelOrderBatch(self.extend(request, params)) + # + # { + # "error": [], + # "result": { + # "count": 2 + # } + # } + # + return [ + self.safe_order({ + 'info': response, + }), + ] + + async def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders + + https://docs.kraken.com/rest/#tag/Spot-Trading/operation/cancelAllOrders + + :param str symbol: unified market symbol, not used by kraken cancelAllOrders(all open orders are cancelled) + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + response = await self.privatePostCancelAll(params) + # + # { + # error: [], + # result: { + # count: '1' + # } + # } + # + return [ + self.safe_order({ + 'info': response, + }), + ] + + async def cancel_all_orders_after(self, timeout: Int, params={}): + """ + dead man's switch, cancel all orders after the given timeout + + https://docs.kraken.com/rest/#tag/Spot-Trading/operation/cancelAllOrdersAfter + + :param number timeout: time in milliseconds, 0 represents cancel the timer + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: the api result + """ + if timeout > 86400000: + raise BadRequest(self.id + ' cancelAllOrdersAfter timeout should be less than 86400000 milliseconds') + await self.load_markets() + request: dict = { + 'timeout': (self.parse_to_int(timeout / 1000)) if (timeout > 0) else 0, + } + response = await self.privatePostCancelAllOrdersAfter(self.extend(request, params)) + # + # { + # "error": [], + # "result": { + # "currentTime": "2023-03-24T17:41:56Z", + # "triggerTime": "2023-03-24T17:42:56Z" + # } + # } + # + return response + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://docs.kraken.com/api/docs/rest-api/get-open-orders + + :param str [symbol]: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.clientOrderId]: the orders client order id + :param int [params.userref]: the orders user reference id + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + request: dict = {} + if since is not None: + request['start'] = self.parse_to_int(since / 1000) + userref = self.safe_integer(params, 'userref') + if userref is not None: + request['userref'] = userref + params = self.omit(params, 'userref') + clientOrderId = self.safe_string(params, 'clientOrderId') + if clientOrderId is not None: + request['cl_ord_id'] = clientOrderId + params = self.omit(params, 'clientOrderId') + response = await self.privatePostOpenOrders(self.extend(request, params)) + # + # { + # "error": [], + # "result": { + # "open": { + # "O45M52-BFD5S-YXKQOU": { + # "refid": null, + # "userref": null, + # "cl_ord_id": "1234", + # "status": "open", + # "opentm": 1733815269.370054, + # "starttm": 0, + # "expiretm": 0, + # "descr": { + # "pair": "XBTUSD", + # "type": "buy", + # "ordertype": "limit", + # "price": "70000.0", + # "price2": "0", + # "leverage": "none", + # "order": "buy 0.00010000 XBTUSD @ limit 70000.0", + # "close": "" + # }, + # "vol": "0.00010000", + # "vol_exec": "0.00000000", + # "cost": "0.00000", + # "fee": "0.00000", + # "price": "0.00000", + # "stopprice": "0.00000", + # "limitprice": "0.00000", + # "misc": "", + # "oflags": "fciq" + # } + # } + # } + # } + # + market = None + if symbol is not None: + market = self.market(symbol) + result = self.safe_dict(response, 'result', {}) + open = self.safe_dict(result, 'open', {}) + orders = [] + orderIds = list(open.keys()) + for i in range(0, len(orderIds)): + id = orderIds[i] + item = open[id] + orders.append(self.extend({'id': id}, item)) + return self.parse_orders(orders, market, since, limit) + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://docs.kraken.com/api/docs/rest-api/get-closed-orders + + :param str [symbol]: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest entry + :param str [params.clientOrderId]: the orders client order id + :param int [params.userref]: the orders user reference id + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + request: dict = {} + if since is not None: + request['start'] = self.parse_to_int(since / 1000) + userref = self.safe_integer(params, 'userref') + if userref is not None: + request['userref'] = userref + params = self.omit(params, 'userref') + clientOrderId = self.safe_string(params, 'clientOrderId') + if clientOrderId is not None: + request['cl_ord_id'] = clientOrderId + params = self.omit(params, 'clientOrderId') + request, params = self.handle_until_option('end', request, params) + response = await self.privatePostClosedOrders(self.extend(request, params)) + # + # { + # "error":[], + # "result":{ + # "closed":{ + # "OETZYO-UL524-QJMXCT":{ + # "refid":null, + # "userref":null, + # "status":"canceled", + # "reason":"User requested", + # "opentm":1601489313.3898, + # "closetm":1601489346.5507, + # "starttm":0, + # "expiretm":0, + # "descr":{ + # "pair":"ETHUSDT", + # "type":"buy", + # "ordertype":"limit", + # "price":"330.00", + # "price2":"0", + # "leverage":"none", + # "order":"buy 0.02100000 ETHUSDT @ limit 330.00", + # "close":"" + # }, + # "vol":"0.02100000", + # "vol_exec":"0.00000000", + # "cost":"0.00000", + # "fee":"0.00000", + # "price":"0.00000", + # "stopprice":"0.00000", + # "limitprice":"0.00000", + # "misc":"", + # "oflags":"fciq" + # }, + # }, + # "count":16 + # } + # } + # + market = None + if symbol is not None: + market = self.market(symbol) + result = self.safe_dict(response, 'result', {}) + closed = self.safe_dict(result, 'closed', {}) + orders = [] + orderIds = list(closed.keys()) + for i in range(0, len(orderIds)): + id = orderIds[i] + item = closed[id] + orders.append(self.extend({'id': id}, item)) + return self.parse_orders(orders, market, since, limit) + + def parse_transaction_status(self, status: Str): + # IFEX transaction states + statuses: dict = { + 'Initial': 'pending', + 'Pending': 'pending', + 'Success': 'ok', + 'Settled': 'pending', + 'Failure': 'failed', + 'Partial': 'ok', + } + return self.safe_string(statuses, status, status) + + def parse_network(self, network): + withdrawMethods = self.safe_value(self.options, 'withdrawMethods', {}) + return self.safe_string(withdrawMethods, network, network) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fetchDeposits + # + # { + # "method": "Ether(Hex)", + # "aclass": "currency", + # "asset": "XETH", + # "refid": "Q2CANKL-LBFVEE-U4Y2WQ", + # "txid": "0x57fd704dab1a73c20e24c8696099b695d596924b401b261513cfdab23…", + # "info": "0x615f9ba7a9575b0ab4d571b2b36b1b324bd83290", + # "amount": "7.9999257900", + # "fee": "0.0000000000", + # "time": 1529223212, + # "status": "Success" + # } + # + # there can be an additional 'status-prop' field present + # deposit pending review by exchange => 'on-hold' + # the deposit is initiated by the exchange => 'return' + # + # { + # "type": 'deposit', + # "method": 'Fidor Bank AG(Wire Transfer)', + # "aclass": 'currency', + # "asset": 'ZEUR', + # "refid": 'xxx-xxx-xxx', + # "txid": '12341234', + # "info": 'BANKCODEXXX', + # "amount": '38769.08', + # "fee": '0.0000', + # "time": 1644306552, + # "status": 'Success', + # status-prop: 'on-hold' + # } + # + # + # fetchWithdrawals + # + # { + # "method": "Ether", + # "aclass": "currency", + # "asset": "XETH", + # "refid": "A2BF34S-O7LBNQ-UE4Y4O", + # "txid": "0x288b83c6b0904d8400ef44e1c9e2187b5c8f7ea3d838222d53f701a15b5c274d", + # "info": "0x7cb275a5e07ba943fee972e165d80daa67cb2dd0", + # "amount": "9.9950000000", + # "fee": "0.0050000000", + # "time": 1530481750, + # "status": "Success" + # "key":"Huobi wallet", + # "network":"Tron" + # status-prop: 'on-hold' # self field might not be present in some cases + # } + # + # withdraw + # + # { + # "refid": "AGBSO6T-UFMTTQ-I7KGS6" + # } + # + id = self.safe_string(transaction, 'refid') + txid = self.safe_string(transaction, 'txid') + timestamp = self.safe_timestamp(transaction, 'time') + currencyId = self.safe_string(transaction, 'asset') + code = self.safe_currency_code(currencyId, currency) + address = self.safe_string(transaction, 'info') + amount = self.safe_number(transaction, 'amount') + status = self.parse_transaction_status(self.safe_string(transaction, 'status')) + statusProp = self.safe_string(transaction, 'status-prop') + isOnHoldDeposit = statusProp == 'on-hold' + isCancellationRequest = statusProp == 'cancel-pending' + isOnHoldWithdrawal = statusProp == 'onhold' + if isOnHoldDeposit or isCancellationRequest or isOnHoldWithdrawal: + status = 'pending' + type = self.safe_string(transaction, 'type') # injected from the outside + feeCost = self.safe_number(transaction, 'fee') + if feeCost is None: + if type == 'deposit': + feeCost = 0 + return { + 'info': transaction, + 'id': id, + 'currency': code, + 'amount': amount, + 'network': self.parse_network(self.safe_string(transaction, 'network')), + 'address': address, + 'addressTo': None, + 'addressFrom': None, + 'tag': None, + 'tagTo': None, + 'tagFrom': None, + 'status': status, + 'type': type, + 'updated': None, + 'txid': txid, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'comment': None, + 'internal': None, + 'fee': { + 'currency': code, + 'cost': feeCost, + }, + } + + def parse_transactions_by_type(self, type, transactions, code: Str = None, since: Int = None, limit: Int = None): + result = [] + for i in range(0, len(transactions)): + transaction = self.parse_transaction(self.extend({ + 'type': type, + }, transactions[i])) + result.append(transaction) + return self.filter_by_currency_since_limit(result, code, since, limit) + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://docs.kraken.com/rest/#tag/Funding/operation/getStatusRecentDeposits + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest transaction entry + :param int [params.end]: timestamp in seconds of the latest transaction entry + :returns dict[]: a list of `transaction structures ` + """ + # https://www.kraken.com/en-us/help/api#deposit-status + await self.load_markets() + request: dict = {} + if code is not None: + currency = self.currency(code) + request['asset'] = currency['id'] + if since is not None: + sinceString = self.number_to_string(since) + request['start'] = Precise.string_div(sinceString, '1000') + until = self.safe_string_n(params, ['until', 'till']) + if until is not None: + params = self.omit(params, ['until', 'till']) + untilDivided = Precise.string_div(until, '1000') + request['end'] = Precise.string_add(untilDivided, '1') + response = await self.privatePostDepositStatus(self.extend(request, params)) + # + # { error: [], + # "result": [{"method": "Ether(Hex)", + # "aclass": "currency", + # "asset": "XETH", + # "refid": "Q2CANKL-LBFVEE-U4Y2WQ", + # "txid": "0x57fd704dab1a73c20e24c8696099b695d596924b401b261513cfdab23…", + # "info": "0x615f9ba7a9575b0ab4d571b2b36b1b324bd83290", + # "amount": "7.9999257900", + # "fee": "0.0000000000", + # "time": 1529223212, + # "status": "Success" }]} + # + return self.parse_transactions_by_type('deposit', response['result'], code, since, limit) + + async def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://docs.kraken.com/rest/#tag/Spot-Market-Data/operation/getServerTime + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + # https://www.kraken.com/en-us/features/api#get-server-time + response = await self.publicGetTime(params) + # + # { + # "error": [], + # "result": { + # "unixtime": 1591502873, + # "rfc1123": "Sun, 7 Jun 20 04:07:53 +0000" + # } + # } + # + result = self.safe_value(response, 'result', {}) + return self.safe_timestamp(result, 'unixtime') + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://docs.kraken.com/rest/#tag/Funding/operation/getStatusRecentWithdrawals + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest transaction entry + :param int [params.end]: timestamp in seconds of the latest transaction entry + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchWithdrawals', 'paginate') + if paginate: + params['cursor'] = True + return await self.fetch_paginated_call_cursor('fetchWithdrawals', code, since, limit, params, 'next_cursor', 'cursor') + request: dict = {} + if code is not None: + currency = self.currency(code) + request['asset'] = currency['id'] + if since is not None: + sinceString = self.number_to_string(since) + request['start'] = Precise.string_div(sinceString, '1000') + until = self.safe_string_n(params, ['until', 'till']) + if until is not None: + params = self.omit(params, ['until', 'till']) + untilDivided = Precise.string_div(until, '1000') + request['end'] = Precise.string_add(untilDivided, '1') + response = await self.privatePostWithdrawStatus(self.extend(request, params)) + # + # with no pagination + # { error: [], + # "result": [{"method": "Ether", + # "aclass": "currency", + # "asset": "XETH", + # "refid": "A2BF34S-O7LBNQ-UE4Y4O", + # "txid": "0x298c83c7b0904d8400ef43e1c9e2287b518f7ea3d838822d53f704a1565c274d", + # "info": "0x7cb275a5e07ba943fee972e165d80daa67cb2dd0", + # "amount": "9.9950000000", + # "fee": "0.0050000000", + # "time": 1530481750, + # "status": "Success" }]} + # with pagination + # { + # "error":[], + # "result":{ + # "withdrawals":[ + # { + # "method":"Tether USD(TRC20)", + # "aclass":"currency", + # "asset":"USDT", + # "refid":"BSNFZU2-MEFN4G-J3NEZV", + # "txid":"1c7a642fb7387bbc2c6a2c509fd1ae146937f4cf793b4079a4f0715e3a02615a", + # "info":"TQmdxSuC16EhFg8FZWtYgrfFRosoRF7bCp", + # "amount":"1996.50000000", + # "fee":"2.50000000", + # "time":1669126657, + # "status":"Success", + # "key":"poloniex", + # "network":"Tron" + # }, + # ... + # ], + # "next_cursor":"HgAAAAAAAABGVFRSd3k1LVF4Y0JQY05Gd0xRY0NxenFndHpybkwBAQH2AwEBAAAAAQAAAAAAAAABAAAAAAAZAAAAAAAAAA==" + # } + # } + # + rawWithdrawals = None + result = self.safe_value(response, 'result') + if not isinstance(result, list): + rawWithdrawals = self.add_pagination_cursor_to_result(result) + else: + rawWithdrawals = result + return self.parse_transactions_by_type('withdrawal', rawWithdrawals, code, since, limit) + + def add_pagination_cursor_to_result(self, result): + cursor = self.safe_string(result, 'next_cursor') + data = self.safe_value(result, 'withdrawals') + dataLength = len(data) + if cursor is not None and dataLength > 0: + last = data[dataLength - 1] + last['next_cursor'] = cursor + data[dataLength - 1] = last + return data + + async def create_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + create a currency deposit address + + https://docs.kraken.com/rest/#tag/Funding/operation/getDepositAddresses + + :param str code: unified currency code of the currency for the deposit address + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + request: dict = { + 'new': 'true', + } + return await self.fetch_deposit_address(code, self.extend(request, params)) + + async def fetch_deposit_methods(self, code: str, params={}): + """ + fetch deposit methods for a currency associated with self account + + https://docs.kraken.com/rest/#tag/Funding/operation/getDepositMethods + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the kraken api endpoint + :returns dict: of deposit methods + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'asset': currency['id'], + } + response = await self.privatePostDepositMethods(self.extend(request, params)) + # + # { + # "error":[], + # "result":[ + # {"method":"Ether(Hex)","limit":false,"gen-address":true} + # ] + # } + # + # { + # "error":[], + # "result":[ + # {"method":"Tether USD(ERC20)","limit":false,"address-setup-fee":"0.00000000","gen-address":true}, + # {"method":"Tether USD(TRC20)","limit":false,"address-setup-fee":"0.00000000","gen-address":true} + # ] + # } + # + # { + # "error":[], + # "result":[ + # {"method":"Bitcoin","limit":false,"fee":"0.0000000000","gen-address":true} + # ] + # } + # + return self.safe_value(response, 'result') + + async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://docs.kraken.com/rest/#tag/Funding/operation/getDepositAddresses + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + await self.load_markets() + currency = self.currency(code) + network = self.safe_string_upper(params, 'network') + networks = self.safe_value(self.options, 'networks', {}) + network = self.safe_string(networks, network, network) # support ETH > ERC20 aliases + params = self.omit(params, 'network') + if (code == 'USDT') and (network == 'TRC20'): + code = code + '-' + network + defaultDepositMethods = self.safe_value(self.options, 'depositMethods', {}) + defaultDepositMethod = self.safe_string(defaultDepositMethods, code) + depositMethod = self.safe_string(params, 'method', defaultDepositMethod) + # if the user has specified an exchange-specific method in params + # we pass it, otherwise we take the 'network' unified param + if depositMethod is None: + depositMethods = await self.fetch_deposit_methods(code) + if network is not None: + # find best matching deposit method, or fallback to the first one + for i in range(0, len(depositMethods)): + entry = self.safe_string(depositMethods[i], 'method') + if entry.find(network) >= 0: + depositMethod = entry + break + # if depositMethod was not specified, fallback to the first available deposit method + if depositMethod is None: + firstDepositMethod = self.safe_value(depositMethods, 0, {}) + depositMethod = self.safe_string(firstDepositMethod, 'method') + request: dict = { + 'asset': currency['id'], + 'method': depositMethod, + } + response = await self.privatePostDepositAddresses(self.extend(request, params)) + # + # { + # "error":[], + # "result":[ + # {"address":"0x77b5051f97efa9cc52c9ad5b023a53fc15c200d3","expiretm":"0"} + # ] + # } + # + result = self.safe_value(response, 'result', []) + firstResult = self.safe_value(result, 0, {}) + if firstResult is None: + raise InvalidAddress(self.id + ' privatePostDepositAddresses() returned no addresses for ' + code) + return self.parse_deposit_address(firstResult, currency) + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + # + # { + # "address":"0x77b5051f97efa9cc52c9ad5b023a53fc15c200d3", + # "expiretm":"0" + # } + # + address = self.safe_string(depositAddress, 'address') + tag = self.safe_string(depositAddress, 'tag') + currency = self.safe_currency(None, currency) + code = currency['code'] + self.check_address(address) + return { + 'info': depositAddress, + 'currency': code, + 'network': None, + 'address': address, + 'tag': tag, + } + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://docs.kraken.com/rest/#tag/Funding/operation/withdrawFunds + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to, not required can be '' or None/none/None + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + if 'key' in params: + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'asset': currency['id'], + 'amount': amount, + # 'address': address, + } + if address is not None and address != '': + request['address'] = address + self.check_address(address) + response = await self.privatePostWithdraw(self.extend(request, params)) + # + # { + # "error": [], + # "result": { + # "refid": "AGBSO6T-UFMTTQ-I7KGS6" + # } + # } + # + result = self.safe_dict(response, 'result', {}) + return self.parse_transaction(result, currency) + raise ExchangeError(self.id + " withdraw() requires a 'key' parameter(withdrawal key name, up on your account)") + + async def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://docs.kraken.com/rest/#tag/Account-Data/operation/getOpenPositions + + :param str[] [symbols]: not used by kraken fetchPositions() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + await self.load_markets() + request: dict = { + # 'txid': 'comma delimited list of transaction ids to restrict output to', + 'docalcs': 'true', # whether or not to include profit/loss calculations + 'consolidation': 'market', # what to consolidate the positions data around, market will consolidate positions based on market pair + } + response = await self.privatePostOpenPositions(self.extend(request, params)) + # + # no consolidation + # + # { + # "error": [], + # "result": { + # 'TGUFMY-FLESJ-VYIX3J': { + # "ordertxid": "O3LRNU-ZKDG5-XNCDFR", + # "posstatus": "open", + # "pair": "ETHUSDT", + # "time": 1611557231.4584, + # "type": "buy", + # "ordertype": "market", + # "cost": "28.49800", + # "fee": "0.07979", + # "vol": "0.02000000", + # "vol_closed": "0.00000000", + # "margin": "14.24900", + # "terms": "0.0200% per 4 hours", + # "rollovertm": "1611571631", + # "misc": "", + # "oflags": "" + # } + # } + # } + # + # consolidation by market + # + # { + # "error": [], + # "result": [ + # { + # "pair": "ETHUSDT", + # "positions": "1", + # "type": "buy", + # "leverage": "2.00000", + # "cost": "28.49800", + # "fee": "0.07979", + # "vol": "0.02000000", + # "vol_closed": "0.00000000", + # "margin": "14.24900" + # } + # ] + # } + # + symbols = self.market_symbols(symbols) + result = self.safe_list(response, 'result') + results = self.parse_positions(result, symbols) + return self.filter_by_array_positions(results, 'symbol', symbols, False) + + def parse_position(self, position: dict, market: Market = None): + # + # { + # "pair": "ETHUSDT", + # "positions": "1", + # "type": "buy", + # "leverage": "2.00000", + # "cost": "28.49800", + # "fee": "0.07979", + # "vol": "0.02000000", + # "vol_closed": "0.00000000", + # "margin": "14.24900" + # } + # + marketId = self.safe_string(position, 'pair') + rawSide = self.safe_string(position, 'type') + side = 'long' if (rawSide == 'buy') else 'short' + return self.safe_position({ + 'info': position, + 'id': None, + 'symbol': self.safe_symbol(marketId, market), + 'notional': None, + 'marginMode': None, + 'liquidationPrice': None, + 'entryPrice': None, + 'unrealizedPnl': self.safe_number(position, 'net'), + 'realizedPnl': None, + 'percentage': None, + 'contracts': self.safe_number(position, 'vol'), + 'contractSize': None, + 'markPrice': None, + 'lastPrice': None, + 'side': side, + 'hedged': None, + 'timestamp': None, + 'datetime': None, + 'lastUpdateTimestamp': None, + 'maintenanceMargin': None, + 'maintenanceMarginPercentage': None, + 'collateral': None, + 'initialMargin': self.safe_number(position, 'margin'), + 'initialMarginPercentage': None, + 'leverage': self.safe_number(position, 'leverage'), + 'marginRatio': None, + 'stopLossPrice': None, + 'takeProfitPrice': None, + }) + + def parse_account_type(self, account): + accountByType: dict = { + 'spot': 'Spot Wallet', + 'swap': 'Futures Wallet', + 'future': 'Futures Wallet', + } + return self.safe_string(accountByType, account, account) + + async def transfer_out(self, code: str, amount, params={}): + """ + transfer from spot wallet to futures wallet + + https://docs.kraken.com/rest/#tag/User-Funding/operation/walletTransfer + + :param str code: Unified currency code + :param float amount: Size of the transfer + :param dict [params]: Exchange specific parameters + :returns: a `transfer structure ` + """ + return await self.transfer(code, amount, 'spot', 'swap', params) + + async def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + + https://docs.kraken.com/rest/#tag/User-Funding/operation/walletTransfer + + transfers currencies between sub-accounts(only spot->swap direction is supported) + :param str code: Unified currency code + :param float amount: Size of the transfer + :param str fromAccount: 'spot' or 'Spot Wallet' + :param str toAccount: 'swap' or 'Futures Wallet' + :param dict [params]: Exchange specific parameters + :returns: a `transfer structure ` + """ + await self.load_markets() + currency = self.currency(code) + fromAccount = self.parse_account_type(fromAccount) + toAccount = self.parse_account_type(toAccount) + request: dict = { + 'amount': self.currency_to_precision(code, amount), + 'from': fromAccount, + 'to': toAccount, + 'asset': currency['id'], + } + if fromAccount != 'Spot Wallet': + raise BadRequest(self.id + ' transfer cannot transfer from ' + fromAccount + ' to ' + toAccount + '. Use krakenfutures instead to transfer from the futures account.') + response = await self.privatePostWalletTransfer(self.extend(request, params)) + # + # { + # "error":[ + # ], + # "result":{ + # "refid":"BOIUSIF-M7DLMN-UXZ3P5" + # } + # } + # + transfer = self.parse_transfer(response, currency) + return self.extend(transfer, { + 'amount': amount, + 'fromAccount': fromAccount, + 'toAccount': toAccount, + }) + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + # + # transfer + # + # { + # "error":[ + # ], + # "result":{ + # "refid":"BOIUSIF-M7DLMN-UXZ3P5" + # } + # } + # + result = self.safe_value(transfer, 'result', {}) + refid = self.safe_string(result, 'refid') + return { + 'info': transfer, + 'id': refid, + 'timestamp': None, + 'datetime': None, + 'currency': self.safe_string(currency, 'code'), + 'amount': None, + 'fromAccount': None, + 'toAccount': None, + 'status': 'sucess', + } + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = '/' + self.version + '/' + api + '/' + path + if api == 'public': + if params: + # urlencodeNested is used to address https://github.com/ccxt/ccxt/issues/12872 + url += '?' + self.urlencode_nested(params) + elif api == 'private': + price = self.safe_string(params, 'price') + isTriggerPercent = False + if price is not None: + isTriggerPercent = True if (price.endswith('%')) else False + isCancelOrderBatch = (path == 'CancelOrderBatch') + isBatchOrder = (path == 'AddOrderBatch') + self.check_required_credentials() + nonce = str(self.nonce()) + # urlencodeNested is used to address https://github.com/ccxt/ccxt/issues/12872 + if isCancelOrderBatch or isTriggerPercent or isBatchOrder: + body = self.json(self.extend({'nonce': nonce}, params)) + else: + body = self.urlencode_nested(self.extend({'nonce': nonce}, params)) + auth = self.encode(nonce + body) + hash = self.hash(auth, 'sha256', 'binary') + binary = self.encode(url) + binhash = self.binary_concat(binary, hash) + secret = self.base64_to_binary(self.secret) + signature = self.hmac(binhash, secret, hashlib.sha512, 'base64') + headers = { + 'API-Key': self.apiKey, + 'API-Sign': signature, + } + if isCancelOrderBatch or isTriggerPercent or isBatchOrder: + headers['Content-Type'] = 'application/json' + else: + headers['Content-Type'] = 'application/x-www-form-urlencoded' + else: + url = '/' + path + url = self.urls['api'][api] + url + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def nonce(self): + return self.milliseconds() - self.options['timeDifference'] + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if code == 520: + raise ExchangeNotAvailable(self.id + ' ' + str(code) + ' ' + reason) + if response is None: + return None + if body[0] == '{': + if not isinstance(response, str): + message = self.id + ' ' + body + if 'error' in response: + numErrors = len(response['error']) + if numErrors: + for i in range(0, len(response['error'])): + error = response['error'][i] + self.throw_exactly_matched_exception(self.exceptions['exact'], error, message) + self.throw_broadly_matched_exception(self.exceptions['broad'], error, message) + raise ExchangeError(message) + # handleCreateOrdersErrors: + if 'result' in response: + result = self.safe_dict(response, 'result', {}) + if 'orders' in result: + orders = self.safe_list(result, 'orders', []) + for i in range(0, len(orders)): + order = orders[i] + error = self.safe_string(order, 'error') + if error is not None: + self.throw_exactly_matched_exception(self.exceptions['exact'], error, message) + self.throw_broadly_matched_exception(self.exceptions['broad'], error, message) + raise ExchangeError(message) + return None diff --git a/ccxt/async_support/krakenfutures.py b/ccxt/async_support/krakenfutures.py new file mode 100644 index 0000000..4aa89e8 --- /dev/null +++ b/ccxt/async_support/krakenfutures.py @@ -0,0 +1,2782 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.krakenfutures import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currency, Int, Leverage, Leverages, LeverageTier, LeverageTiers, Market, Num, Order, OrderBook, OrderRequest, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, FundingRates, Trade, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import OrderImmediatelyFillable +from ccxt.base.errors import OrderNotFillable +from ccxt.base.errors import DuplicateOrderId +from ccxt.base.errors import ContractUnavailable +from ccxt.base.errors import DDoSProtection +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.errors import InvalidNonce +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class krakenfutures(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(krakenfutures, self).describe(), { + 'id': 'krakenfutures', + 'name': 'Kraken Futures', + 'countries': ['US'], + 'version': 'v3', + 'userAgent': None, + 'rateLimit': 600, + 'pro': True, + 'has': { + 'CORS': None, + 'spot': False, + 'margin': False, + 'swap': True, + 'future': True, + 'option': False, + 'cancelAllOrders': True, + 'cancelAllOrdersAfter': True, + 'cancelOrder': True, + 'cancelOrders': True, + 'createMarketOrder': False, + 'createOrder': True, + 'createPostOnlyOrder': True, + 'createReduceOnlyOrder': True, + 'createStopLimitOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'createTriggerOrder': True, + 'editOrder': True, + 'fetchBalance': True, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchCanceledOrders': True, + 'fetchClosedOrders': True, # https://support.kraken.com/hc/en-us/articles/360058243651-Historical-orders + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': False, + 'fetchDepositAddress': False, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchFundingHistory': None, + 'fetchFundingRate': 'emulated', + 'fetchFundingRateHistory': True, + 'fetchFundingRates': True, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLeverage': True, + 'fetchLeverages': True, + 'fetchLeverageTiers': True, + 'fetchMarketLeverageTiers': 'emulated', + 'fetchMarkets': True, + 'fetchMarkOHLCV': True, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenOrders': True, + 'fetchOrder': False, + 'fetchOrderBook': True, + 'fetchOrders': False, + 'fetchPositions': True, + 'fetchPremiumIndexOHLCV': False, + 'fetchTickers': True, + 'fetchTrades': True, + 'sandbox': True, + 'setLeverage': True, + 'setMarginMode': False, + 'transfer': True, + }, + 'urls': { + 'test': { + 'public': 'https://demo-futures.kraken.com/derivatives/api/', + 'private': 'https://demo-futures.kraken.com/derivatives/api/', + 'charts': 'https://demo-futures.kraken.com/api/charts/', + 'history': 'https://demo-futures.kraken.com/api/history/', + 'www': 'https://demo-futures.kraken.com', + }, + 'logo': 'https://user-images.githubusercontent.com/24300605/81436764-b22fd580-9172-11ea-9703-742783e6376d.jpg', + 'api': { + 'charts': 'https://futures.kraken.com/api/charts/', + 'history': 'https://futures.kraken.com/api/history/', + 'feeschedules': 'https://futures.kraken.com/api/feeschedules/', + 'public': 'https://futures.kraken.com/derivatives/api/', + 'private': 'https://futures.kraken.com/derivatives/api/', + }, + 'www': 'https://futures.kraken.com/', + 'doc': [ + 'https://docs.kraken.com/api/docs/futures-api/trading/market-data/', + ], + 'fees': 'https://support.kraken.com/hc/en-us/articles/360022835771-Transaction-fees-and-rebates-for-Kraken-Futures', + 'referral': None, + }, + 'api': { + 'public': { + 'get': [ + 'feeschedules', + 'instruments', + 'orderbook', + 'tickers', + 'history', + 'historicalfundingrates', + ], + }, + 'private': { + 'get': [ + 'feeschedules/volumes', + 'openpositions', + 'notifications', + 'accounts', + 'openorders', + 'recentorders', + 'fills', + 'transfers', + 'leveragepreferences', + 'pnlpreferences', + 'assignmentprogram/current', + 'assignmentprogram/history', + ], + 'post': [ + 'sendorder', + 'editorder', + 'cancelorder', + 'transfer', + 'batchorder', + 'cancelallorders', + 'cancelallordersafter', + 'withdrawal', # for futures wallet -> kraken spot wallet + 'assignmentprogram/add', + 'assignmentprogram/delete', + ], + 'put': [ + 'leveragepreferences', + 'pnlpreferences', + ], + }, + 'charts': { + 'get': [ + '{price_type}/{symbol}/{interval}', + ], + }, + 'history': { + 'get': [ + 'orders', + 'executions', + 'triggers', + 'accountlogcsv', + 'account-log', + 'market/{symbol}/orders', + 'market/{symbol}/executions', + ], + }, + }, + 'fees': { + 'trading': { + 'tierBased': True, + 'percentage': True, + 'taker': self.parse_number('0.0005'), + 'maker': self.parse_number('0.0002'), + 'tiers': { + 'taker': [ + [self.parse_number('0'), self.parse_number('0.0005')], + [self.parse_number('100000'), self.parse_number('0.0004')], + [self.parse_number('1000000'), self.parse_number('0.0003')], + [self.parse_number('5000000'), self.parse_number('0.00025')], + [self.parse_number('10000000'), self.parse_number('0.0002')], + [self.parse_number('20000000'), self.parse_number('0.00015')], + [self.parse_number('50000000'), self.parse_number('0.000125')], + [self.parse_number('100000000'), self.parse_number('0.0001')], + ], + 'maker': [ + [self.parse_number('0'), self.parse_number('0.0002')], + [self.parse_number('100000'), self.parse_number('0.0015')], + [self.parse_number('1000000'), self.parse_number('0.000125')], + [self.parse_number('5000000'), self.parse_number('0.0001')], + [self.parse_number('10000000'), self.parse_number('0.000075')], + [self.parse_number('20000000'), self.parse_number('0.00005')], + [self.parse_number('50000000'), self.parse_number('0.000025')], + [self.parse_number('100000000'), self.parse_number('0')], + ], + }, + }, + }, + 'exceptions': { + 'exact': { + 'apiLimitExceeded': RateLimitExceeded, + 'marketUnavailable': ContractUnavailable, + 'requiredArgumentMissing': BadRequest, + 'unavailable': ExchangeNotAvailable, + 'authenticationError': AuthenticationError, + 'accountInactive': ExchangeError, # When account has no trade history / no order history. Should self error be ignored in some cases? + 'invalidAccount': BadRequest, # the fromAccount or the toAccount are invalid + 'invalidAmount': BadRequest, + 'insufficientFunds': InsufficientFunds, + 'Bad Request': BadRequest, # The URL contains invalid characters.(Please encode the json URL parameter) + 'Unavailable': ExchangeNotAvailable, # https://github.com/ccxt/ccxt/issues/24338 + 'invalidUnit': BadRequest, + 'Json Parse Error': ExchangeError, + 'nonceBelowThreshold': InvalidNonce, + 'nonceDuplicate': InvalidNonce, + 'notFound': BadRequest, + 'Server Error': ExchangeError, + 'unknownError': ExchangeError, + }, + 'broad': { + 'invalidArgument': BadRequest, + 'nonceBelowThreshold': InvalidNonce, + 'nonceDuplicate': InvalidNonce, + }, + }, + 'precisionMode': TICK_SIZE, + 'options': { + 'access': { + 'history': { + 'GET': { + 'orders': 'private', + 'executions': 'private', + 'triggers': 'private', + 'accountlogcsv': 'private', + 'account-log': 'private', + }, + }, + }, + 'settlementCurrencies': { + 'flex': ['USDT', 'BTC', 'USD', 'GBP', 'EUR', 'USDC'], + }, + 'symbol': { + 'quoteIds': ['USD', 'XBT'], + 'reversed': False, + }, + 'versions': { + 'public': { + 'GET': { + 'historicalfundingrates': 'v4', + }, + }, + 'charts': { + 'GET': { + '{price_type}/{symbol}/{interval}': 'v1', + }, + }, + 'history': { + 'GET': { + 'orders': 'v2', + 'executions': 'v2', + 'triggers': 'v2', + 'accountlogcsv': 'v2', + }, + }, + }, + 'fetchTrades': { + 'method': 'historyGetMarketSymbolExecutions', # historyGetMarketSymbolExecutions, publicGetHistory + }, + }, + 'features': { + 'default': { + 'sandbox': True, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerPriceType': { + 'last': True, + 'mark': True, + 'index': True, + }, + 'triggerDirection': False, + 'stopLossPrice': True, + 'takeProfitPrice': True, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': False, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': { + 'max': 100, + }, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': None, + 'daysBack': None, + 'untilDays': 100000, + 'symbolRequired': False, + }, + 'fetchOrder': None, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': None, + 'daysBack': None, + 'daysBackCanceled': None, + 'untilDays': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 5000, + }, + }, + 'spot': None, + 'swap': { + 'linear': { + 'extends': 'default', + }, + 'inverse': { + 'extends': 'default', + }, + }, + 'future': { + 'linear': { + 'extends': 'default', + }, + 'inverse': { + 'extends': 'default', + }, + }, + }, + 'timeframes': { + '1m': '1m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1h', + '4h': '4h', + '12h': '12h', + '1d': '1d', + '1w': '1w', + }, + }) + + async def fetch_markets(self, params={}) -> List[Market]: + """ + Fetches the available trading markets from the exchange, Multi-collateral markets are returned markets, but can be settled in multiple currencies + + https://docs.kraken.com/api/docs/futures-api/trading/get-instruments + + :param dict [params]: exchange specific params + :returns: An array of market structures + """ + response = await self.publicGetInstruments(params) + # + # { + # "result": "success", + # "instruments": [ + # { + # "symbol": "fi_ethusd_180928", + # "type": "futures_inverse", # futures_vanilla # spot index + # "underlying": "rr_ethusd", + # "lastTradingTime": "2018-09-28T15:00:00.000Z", + # "tickSize": 0.1, + # "contractSize": 1, + # "tradeable": True, + # "marginLevels": [ + # { + # "contracts":0, + # "initialMargin":0.02, + # "maintenanceMargin":0.01 + # }, + # { + # "contracts":250000, + # "initialMargin":0.04, + # "maintenanceMargin":0.02 + # }, + # ... + # ], + # "isin": "GB00JVMLMP88", + # "retailMarginLevels": [ + # { + # "contracts": 0, + # "initialMargin": 0.5, + # "maintenanceMargin": 0.25 + # } + # ], + # "tags": [], + # }, + # { + # "symbol": "in_xbtusd", + # "type": "spot index", + # "tradeable":false + # } + # ] + # "serverTime": "2018-07-19T11:32:39.433Z" + # } + # + instruments = self.safe_value(response, 'instruments', []) + result = [] + for i in range(0, len(instruments)): + market = instruments[i] + id = self.safe_string(market, 'symbol') + marketType = self.safe_string(market, 'type') + type = None + index = (marketType.find(' index') >= 0) + linear = None + inverse = None + expiry = None + if not index: + linear = (marketType.find('_vanilla') >= 0) + inverse = not linear + settleTime = self.safe_string(market, 'lastTradingTime') + type = 'swap' if (settleTime is None) else 'future' + expiry = self.parse8601(settleTime) + else: + type = 'index' + swap = (type == 'swap') + future = (type == 'future') + symbol = id + split = id.split('_') + splitMarket = self.safe_string(split, 1) + baseId = splitMarket[0:len(splitMarket) - 3] + quoteId = 'usd' # always USD + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + # swap == perpetual + settle = None + settleId = None + cvtp = self.safe_string(market, 'contractValueTradePrecision') + amountPrecision = self.parse_number(self.integer_precision_to_amount(cvtp)) + pricePrecision = self.safe_number(market, 'tickSize') + contract = (swap or future or index) + swapOrFutures = (swap or future) + if swapOrFutures: + exchangeType = self.safe_string(market, 'type') + if exchangeType == 'futures_inverse': + settle = base + settleId = baseId + inverse = True + else: + settle = quote + settleId = quoteId + inverse = False + linear = not inverse + symbol = base + '/' + quote + ':' + settle + if future: + symbol = symbol + '-' + self.yymmdd(expiry) + result.append({ + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': type, + 'spot': False, + 'margin': False, + 'swap': swap, + 'future': future, + 'option': False, + 'index': index, + 'active': self.safe_bool(market, 'tradeable'), + 'contract': contract, + 'linear': linear, + 'inverse': inverse, + 'contractSize': self.safe_number(market, 'contractSize'), + 'maintenanceMarginRate': None, + 'expiry': expiry, + 'expiryDatetime': self.iso8601(expiry), + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': amountPrecision, + 'price': pricePrecision, + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': None, + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': self.parse8601(self.safe_string(market, 'openingDate')), + 'info': market, + }) + settlementCurrencies = self.options['settlementCurrencies']['flex'] + currencies = [] + for i in range(0, len(settlementCurrencies)): + code = settlementCurrencies[i] + currencies.append({ + 'id': code.lower(), + 'numericId': None, + 'code': code, + 'precision': None, + }) + self.currencies = self.map_to_safe_map(self.deep_extend(currencies, self.currencies)) + return result + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + + https://docs.kraken.com/api/docs/futures-api/trading/get-orderbook + + Fetches a list of open orders in a market + :param str symbol: Unified market symbol + :param int [limit]: Not used by krakenfutures + :param dict [params]: exchange specific params + :returns: An `order book structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = await self.publicGetOrderbook(self.extend(request, params)) + # + # { + # "result": "success", + # "serverTime": "2016-02-25T09:45:53.818Z", + # "orderBook": { + # "bids": [ + # [ + # 4213, + # 2000, + # ], + # [ + # 4210, + # 4000, + # ], + # ... + # ], + # "asks": [ + # [ + # 4218, + # 4000, + # ], + # [ + # 4220, + # 5000, + # ], + # ... + # ], + # }, + # } + # + timestamp = self.parse8601(response['serverTime']) + return self.parse_order_book(response['orderBook'], symbol, timestamp) + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://docs.kraken.com/api/docs/futures-api/trading/get-tickers + + :param str[] symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an array of `ticker structures ` + """ + await self.load_markets() + response = await self.publicGetTickers(params) + # + # { + # "result": "success", + # "tickers": [ + # { + # "tag": 'semiannual', # 'month', 'quarter', "perpetual", "semiannual", + # "pair": "ETH:USD", + # "symbol": "fi_ethusd_220624", + # "markPrice": "2925.72", + # "bid": "2923.8", + # "bidSize": "16804", + # "ask": "2928.65", + # "askSize": "1339", + # "vol24h": "860493", + # "openInterest": "3023363.00000000", + # "open24h": "3021.25", + # "indexPrice": "2893.71", + # "last": "2942.25", + # "lastTime": "2022-02-18T14:08:15.578Z", + # "lastSize": "151", + # "suspended": False + # }, + # { + # "symbol": "in_xbtusd", # "rr_xbtusd", + # "last": "40411", + # "lastTime": "2022-02-18T14:16:28.000Z" + # }, + # ... + # ], + # "serverTime": "2022-02-18T14:16:29.440Z" + # } + # + tickers = self.safe_list(response, 'tickers') + return self.parse_tickers(tickers, symbols) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "tag": 'semiannual', # 'month', 'quarter', "perpetual", "semiannual", + # "pair": "ETH:USD", + # "symbol": "fi_ethusd_220624", + # "markPrice": "2925.72", + # "bid": "2923.8", + # "bidSize": "16804", + # "ask": "2928.65", + # "askSize": "1339", + # "vol24h": "860493", + # "openInterest": "3023363.00000000", + # "open24h": "3021.25", + # "indexPrice": "2893.71", + # "last": "2942.25", + # "lastTime": "2022-02-18T14:08:15.578Z", + # "lastSize": "151", + # "suspended": False + # } + # + # { + # "symbol": "in_xbtusd", # "rr_xbtusd", + # "last": "40411", + # "lastTime": "2022-02-18T14:16:28.000Z" + # } + # + marketId = self.safe_string(ticker, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + timestamp = self.parse8601(self.safe_string(ticker, 'lastTime')) + open = self.safe_string(ticker, 'open24h') + last = self.safe_string(ticker, 'last') + change = Precise.string_sub(last, open) + percentage = Precise.string_mul(Precise.string_div(change, open), '100') + average = Precise.string_div(Precise.string_add(open, last), '2') + volume = self.safe_string(ticker, 'vol24h') + baseVolume = None + quoteVolume = None + isIndex = self.safe_bool(market, 'index', False) + if not isIndex: + if market['linear']: + baseVolume = volume + elif market['inverse']: + quoteVolume = volume + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': None, + 'low': None, + 'bid': self.safe_string(ticker, 'bid'), + 'bidVolume': self.safe_string(ticker, 'bidSize'), + 'ask': self.safe_string(ticker, 'ask'), + 'askVolume': self.safe_string(ticker, 'askSize'), + 'vwap': None, + 'open': open, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': change, + 'percentage': percentage, + 'average': average, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'markPrice': self.safe_string(ticker, 'markPrice'), + 'indexPrice': self.safe_string(ticker, 'indexPrice'), + 'info': ticker, + }) + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + + https://docs.kraken.com/api/docs/futures-api/charts/candles + + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate') + if paginate: + return await self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params, 5000) + request: dict = { + 'symbol': market['id'], + 'price_type': self.safe_string(params, 'price', 'trade'), + 'interval': self.timeframes[timeframe], + } + params = self.omit(params, 'price') + if since is not None: + duration = self.parse_timeframe(timeframe) + request['from'] = self.parse_to_int(since / 1000) + if limit is None: + limit = 5000 + limit = min(limit, 5000) + toTimestamp = self.sum(request['from'], limit * duration - 1) + currentTimestamp = self.seconds() + request['to'] = min(toTimestamp, currentTimestamp) + elif limit is not None: + limit = min(limit, 5000) + duration = self.parse_timeframe(timeframe) + request['to'] = self.seconds() + request['from'] = self.parse_to_int(request['to'] - (duration * limit)) + response = await self.chartsGetPriceTypeSymbolInterval(self.extend(request, params)) + # + # { + # "candles": [ + # { + # "time": 1645198500000, + # "open": "309.15000000000", + # "high": "309.15000000000", + # "low": "308.70000000000", + # "close": "308.85000000000", + # "volume": 0 + # } + # ], + # "more_candles": True + # } + # + candles = self.safe_list(response, 'candles') + return self.parse_ohlcvs(candles, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # { + # "time": 1645198500000, + # "open": "309.15000000000", + # "high": "309.15000000000", + # "low": "308.70000000000", + # "close": "308.85000000000", + # "volume": 0 + # } + # + return [ + self.safe_integer(ohlcv, 'time'), # unix timestamp in milliseconds + self.safe_number(ohlcv, 'open'), # open price + self.safe_number(ohlcv, 'high'), # highest price + self.safe_number(ohlcv, 'low'), # lowest price + self.safe_number(ohlcv, 'close'), # close price + self.safe_number(ohlcv, 'volume'), # trading volume, None for mark or index price + ] + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + + https://docs.kraken.com/api/docs/futures-api/trading/get-history + https://docs.kraken.com/api/docs/futures-api/history/get-public-execution-events + + Fetch a history of filled trades that self account has made + :param str symbol: Unified CCXT market symbol + :param int [since]: Timestamp in ms of earliest trade. Not used by krakenfutures except in combination with params.until + :param int [limit]: Total number of trades, cannot exceed 100 + :param dict [params]: Exchange specific params + :param int [params.until]: Timestamp in ms of latest trade + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param str [params.method]: The method to use to fetch trades. Can be 'historyGetMarketSymbolExecutions' or 'publicGetHistory' default is 'historyGetMarketSymbolExecutions' + :returns: An array of `trade structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchTrades', 'paginate') + if paginate: + return await self.fetch_paginated_call_dynamic('fetchTrades', symbol, since, limit, params) + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + method = None + method, params = self.handle_option_and_params(params, 'fetchTrades', 'method', 'historyGetMarketSymbolExecutions') + rawTrades = None + isFullHistoryEndpoint = (method == 'historyGetMarketSymbolExecutions') + if isFullHistoryEndpoint: + request, params = self.handle_until_option('before', request, params) + if since is not None: + request['since'] = since + request['sort'] = 'asc' + if limit is not None: + request['count'] = limit + response = await self.historyGetMarketSymbolExecutions(self.extend(request, params)) + # + # { + # "elements": [ + # { + # "uid": "a5105030-f054-44cc-98ab-30d5cae96bef", + # "timestamp": "1710150778607", + # "event": { + # "Execution": { + # "execution": { + # "uid": "2d485b71-cd28-4a1e-9364-371a127550d2", + # "makerOrder": { + # "uid": "0a25f66b-1109-49ec-93a3-d17bf9e9137e", + # "tradeable": "PF_XBTUSD", + # "direction": "Buy", + # "quantity": "0.26500", + # "timestamp": "1710150778570", + # "limitPrice": "71907", + # "orderType": "Post", + # "reduceOnly": False, + # "lastUpdateTimestamp": "1710150778570" + # }, + # "takerOrder": { + # "uid": "04de3ee0-9125-4960-bf8f-f63b577b6790", + # "tradeable": "PF_XBTUSD", + # "direction": "Sell", + # "quantity": "0.0002", + # "timestamp": "1710150778607", + # "limitPrice": "71187.00", + # "orderType": "Market", + # "reduceOnly": False, + # "lastUpdateTimestamp": "1710150778607" + # }, + # "timestamp": "1710150778607", + # "quantity": "0.0002", + # "price": "71907", + # "markPrice": "71903.32715463147", + # "limitFilled": False, + # "usdValue": "14.38" + # }, + # "takerReducedQuantity": "" + # } + # } + # }, + # ... followed by older items + # ], + # "len": "1000", + # "continuationToken": "QTexMDE0OTe33NTcyXy8xNDIzAjc1NjY5MwI=" + # } + # + elements = self.safe_list(response, 'elements', []) + # we need to reverse the list to fix chronology + rawTrades = [] + length = len(elements) + for i in range(0, length): + index = length - 1 - i + element = elements[index] + event = self.safe_dict(element, 'event', {}) + executionContainer = self.safe_dict(event, 'Execution', {}) + rawTrade = self.safe_dict(executionContainer, 'execution', {}) + rawTrades.append(rawTrade) + else: + request, params = self.handle_until_option('lastTime', request, params) + response = await self.publicGetHistory(self.extend(request, params)) + # + # { + # "result": "success", + # "history": [ + # { + # "time": "2022-03-18T04:55:37.692Z", + # "trade_id": 100, + # "price": 0.7921, + # "size": 1068, + # "side": "sell", + # "type": "fill", + # "uid": "6c5da0b0-f1a8-483f-921f-466eb0388265" + # }, + # ... + # ], + # "serverTime": "2022-03-18T06:39:18.056Z" + # } + # + rawTrades = self.safe_list(response, 'history', []) + return self.parse_trades(rawTrades, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(recent trades) + # + # { + # "time": "2019-02-14T09:25:33.920Z", + # "trade_id": 100, + # "price": 3574, + # "size": 100, + # "side": "buy", + # "type": "fill" # fill, liquidation, assignment, termination + # "uid": "11c3d82c-9e70-4fe9-8115-f643f1b162d4" + # } + # + # fetchTrades(executions history) + # + # { + # "timestamp": "1710152516830", + # "price": "71927.0", + # "quantity": "0.0695", + # "markPrice": "71936.38701675525", + # "limitFilled": True, + # "usdValue": "4998.93", + # "uid": "116ae634-253f-470b-bd20-fa9d429fb8b1", + # "makerOrder": {"uid": "17bfe4de-c01e-4938-926c-617d2a2d0597", "tradeable": "PF_XBTUSD", "direction": "Buy", "quantity": "0.0695", "timestamp": "1710152515836", "limitPrice": "71927.0", "orderType": "Post", "reduceOnly": False, "lastUpdateTimestamp": "1710152515836"}, + # "takerOrder": {"uid": "d3e437b4-aa70-4108-b5cf-b1eecb9845b5", "tradeable": "PF_XBTUSD", "direction": "Sell", "quantity": "0.940100", "timestamp": "1710152516830", "limitPrice": "71915", "orderType": "IoC", "reduceOnly": False, "lastUpdateTimestamp": "1710152516830"} + # } + # + # fetchMyTrades(private) + # + # { + # "fillTime": "2016-02-25T09:47:01.000Z", + # "order_id": "c18f0c17-9971-40e6-8e5b-10df05d422f0", + # "fill_id": "522d4e08-96e7-4b44-9694-bfaea8fe215e", + # "cliOrdId": "d427f920-ec55-4c18-ba95-5fe241513b30", # OPTIONAL + # "symbol": "fi_xbtusd_180615", + # "side": "buy", + # "size": 2000, + # "price": 4255, + # "fillType": "maker" # taker, takerAfterEdit, maker, liquidation, assignee + # } + # + # execution report(createOrder, editOrder) + # + # { + # "executionId": "e1ec9f63-2338-4c44-b40a-43486c6732d7", + # "price": 7244.5, + # "amount": 10, + # "orderPriorEdit": null, + # "orderPriorExecution": { + # "orderId": "61ca5732-3478-42fe-8362-abbfd9465294", + # "cliOrdId": null, + # "type": "lmt", + # "symbol": "pi_xbtusd", + # "side": "buy", + # "quantity": 10, + # "filled": 0, + # "limitPrice": 7500, + # "reduceOnly": False, + # "timestamp": "2019-12-11T17:17:33.888Z", + # "lastUpdateTimestamp": "2019-12-11T17:17:33.888Z" + # }, + # "takerReducedQuantity": null, + # "type": "EXECUTION" + # } + # + timestamp = self.parse8601(self.safe_string_2(trade, 'time', 'fillTime')) + price = self.safe_string(trade, 'price') + amount = self.safe_string_n(trade, ['size', 'amount', 'quantity'], '0.0') + id = self.safe_string_2(trade, 'uid', 'fill_id') + if id is None: + id = self.safe_string(trade, 'executionId') + order = self.safe_string(trade, 'order_id') + marketId = self.safe_string(trade, 'symbol') + side = self.safe_string(trade, 'side') + type = None + priorEdit = self.safe_value(trade, 'orderPriorEdit') + priorExecution = self.safe_value(trade, 'orderPriorExecution') + if priorExecution is not None: + order = self.safe_string(priorExecution, 'orderId') + marketId = self.safe_string(priorExecution, 'symbol') + side = self.safe_string(priorExecution, 'side') + type = self.safe_string(priorExecution, 'type') + elif priorEdit is not None: + order = self.safe_string(priorEdit, 'orderId') + marketId = self.safe_string(priorEdit, 'symbol') + side = self.safe_string(priorEdit, 'type') + type = self.safe_string(priorEdit, 'type') + if type is not None: + type = self.parse_order_type(type) + market = self.safe_market(marketId, market) + cost = None + linear = self.safe_bool(market, 'linear') + if (amount is not None) and (price is not None) and (market is not None): + if linear: + cost = Precise.string_mul(amount, price) # in quote + else: + cost = Precise.string_div(amount, price) # in base + contractSize = self.safe_string(market, 'contractSize') + cost = Precise.string_mul(cost, contractSize) + takerOrMaker = None + fillType = self.safe_string(trade, 'fillType') + if fillType is not None: + if fillType.find('taker') >= 0: + takerOrMaker = 'taker' + elif fillType.find('maker') >= 0: + takerOrMaker = 'maker' + isHistoricalExecution = ('takerOrder' in trade) + if isHistoricalExecution: + timestamp = self.safe_integer(trade, 'timestamp') + taker = self.safe_dict(trade, 'takerOrder', {}) + if taker is not None: + side = self.safe_string_lower(taker, 'direction') + takerOrMaker = 'taker' + return self.safe_trade({ + 'info': trade, + 'id': id, + 'symbol': self.safe_string(market, 'symbol'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'order': order, + 'type': type, + 'side': side, + 'takerOrMaker': takerOrMaker, + 'price': price, + 'amount': amount if linear else None, + 'cost': cost, + 'fee': None, + }) + + def create_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + market = self.market(symbol) + symbol = market['symbol'] + type = self.safe_string(params, 'orderType', type) + timeInForce = self.safe_string(params, 'timeInForce') + postOnly = False + postOnly, params = self.handle_post_only(type == 'market', type == 'post', params) + if postOnly: + type = 'post' + elif timeInForce == 'ioc': + type = 'ioc' + elif type == 'limit': + type = 'lmt' + elif type == 'market': + type = 'mkt' + request: dict = { + 'symbol': market['id'], + 'side': side, + 'size': self.amount_to_precision(symbol, amount), + } + clientOrderId = self.safe_string_2(params, 'clientOrderId', 'cliOrdId') + if clientOrderId is not None: + request['cliOrdId'] = clientOrderId + triggerPrice = self.safe_string_2(params, 'triggerPrice', 'stopPrice') + isTriggerOrder = triggerPrice is not None + stopLossTriggerPrice = self.safe_string(params, 'stopLossPrice') + takeProfitTriggerPrice = self.safe_string(params, 'takeProfitPrice') + isStopLossTriggerOrder = stopLossTriggerPrice is not None + isTakeProfitTriggerOrder = takeProfitTriggerPrice is not None + isStopLossOrTakeProfitTrigger = isStopLossTriggerOrder or isTakeProfitTriggerOrder + triggerSignal = self.safe_string(params, 'triggerSignal', 'last') + reduceOnly = self.safe_value(params, 'reduceOnly') + if isStopLossOrTakeProfitTrigger or isTriggerOrder: + request['triggerSignal'] = triggerSignal + if isTriggerOrder: + type = 'stp' + request['stopPrice'] = self.price_to_precision(symbol, triggerPrice) + elif isStopLossOrTakeProfitTrigger: + reduceOnly = True + if isStopLossTriggerOrder: + type = 'stp' + request['stopPrice'] = self.price_to_precision(symbol, stopLossTriggerPrice) + elif isTakeProfitTriggerOrder: + type = 'take_profit' + request['stopPrice'] = self.price_to_precision(symbol, takeProfitTriggerPrice) + if reduceOnly: + request['reduceOnly'] = True + request['orderType'] = type + if price is not None: + request['limitPrice'] = self.price_to_precision(symbol, price) + params = self.omit(params, ['clientOrderId', 'timeInForce', 'triggerPrice', 'stopLossPrice', 'takeProfitPrice']) + return self.extend(request, params) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + Create an order on the exchange + + https://docs.kraken.com/api/docs/futures-api/trading/send-order + + :param str symbol: unified market symbol + :param str type: 'limit' or 'market' + :param str side: 'buy' or 'sell' + :param float amount: number of contracts + :param float [price]: limit order price + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.reduceOnly]: set if you wish the order to only reduce an existing position, any order which increases an existing position will be rejected, default is False + :param bool [params.postOnly]: set if you wish to make a postOnly order, default is False + :param str [params.clientOrderId]: UUID The order identity that is specified from the user, It must be globally unique + :param float [params.triggerPrice]: the price that a stop order is triggered at + :param float [params.stopLossPrice]: the price that a stop loss order is triggered at + :param float [params.takeProfitPrice]: the price that a take profit order is triggered at + :param str [params.triggerSignal]: for triggerPrice, stopLossPrice and takeProfitPrice orders, the trigger price type, 'last', 'mark' or 'index', default is 'last' + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + orderRequest = self.create_order_request(symbol, type, side, amount, price, params) + response = await self.privatePostSendorder(orderRequest) + # + # { + # "result": "success", + # "sendStatus": { + # "order_id": "salf320-e337-47ac-b345-30sdfsalj", + # "status": "placed", + # "receivedTime": "2022-02-28T19:32:17.122Z", + # "orderEvents": [ + # { + # "order": { + # "orderId": "salf320-e337-47ac-b345-30sdfsalj", + # "cliOrdId": null, + # "type": "lmt", + # "symbol": "pi_xrpusd", + # "side": "buy", + # "quantity": 1, + # "filled": 0, + # "limitPrice": 0.7, + # "reduceOnly": False, + # "timestamp": "2022-02-28T19:32:17.122Z", + # "lastUpdateTimestamp": "2022-02-28T19:32:17.122Z" + # }, + # "reducedQuantity": null, + # "type": "PLACE" + # } + # ] + # }, + # "serverTime": "2022-02-28T19:32:17.122Z" + # } + # + sendStatus = self.safe_value(response, 'sendStatus') + status = self.safe_string(sendStatus, 'status') + self.verify_order_action_success(status, 'createOrder', ['filled']) + return self.parse_order(sendStatus, market) + + async def create_orders(self, orders: List[OrderRequest], params={}): + """ + create a list of trade orders + + https://docs.kraken.com/api/docs/futures-api/trading/send-batch-order + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + ordersRequests = [] + for i in range(0, len(orders)): + rawOrder = orders[i] + marketId = self.safe_string(rawOrder, 'symbol') + type = self.safe_string(rawOrder, 'type') + side = self.safe_string(rawOrder, 'side') + amount = self.safe_value(rawOrder, 'amount') + price = self.safe_value(rawOrder, 'price') + orderParams = self.safe_value(rawOrder, 'params', {}) + extendedParams = self.extend(orderParams, params) # the request does not accept extra params since it's a list, so we're extending each order with the common params + if not ('order_tag' in extendedParams): + # order tag is mandatory so we will generate one if not provided + extendedParams['order_tag'] = self.sum(i, str(1)) # sequential counter + extendedParams['order'] = 'send' + orderRequest = self.create_order_request(marketId, type, side, amount, price, extendedParams) + ordersRequests.append(orderRequest) + request: dict = { + 'batchOrder': ordersRequests, + } + response = await self.privatePostBatchorder(self.extend(request, params)) + # + # { + # "result": "success", + # "serverTime": "2023-10-24T08:40:57.339Z", + # "batchStatus": [ + # { + # "status": "requiredArgumentMissing", + # "orderEvents": [] + # }, + # { + # "status": "requiredArgumentMissing", + # "orderEvents": [] + # } + # ] + # } + # + data = self.safe_list(response, 'batchStatus', []) + return self.parse_orders(data) + + async def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + """ + + https://docs.kraken.com/api/docs/futures-api/trading/edit-order-spring + + Edit an open order on the exchange + :param str id: order id + :param str symbol: Not used by Krakenfutures + :param str type: Not used by Krakenfutures + :param str side: Not used by Krakenfutures + :param float amount: Order size + :param float [price]: Price to fill order at + :param dict [params]: Exchange specific params + :returns: An `order structure ` + """ + await self.load_markets() + request: dict = { + 'orderId': id, + } + if amount is not None: + request['size'] = amount + if price is not None: + request['limitPrice'] = price + response = await self.privatePostEditorder(self.extend(request, params)) + status = self.safe_string(response['editStatus'], 'status') + self.verify_order_action_success(status, 'editOrder', ['filled']) + order = self.parse_order(response['editStatus']) + order['info'] = response + return order + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + + https://docs.kraken.com/api/docs/futures-api/trading/cancel-order + + Cancel an open order on the exchange + :param str id: Order id + :param str symbol: Not used by Krakenfutures + :param dict [params]: Exchange specific params + :returns: An `order structure ` + """ + await self.load_markets() + response = await self.privatePostCancelorder(self.extend({'order_id': id}, params)) + status = self.safe_string(self.safe_value(response, 'cancelStatus', {}), 'status') + self.verify_order_action_success(status, 'cancelOrder') + order: dict = {} + if 'cancelStatus' in response: + order = self.parse_order(response['cancelStatus']) + return self.extend({'info': response}, order) + + async def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): + """ + cancel multiple orders + + https://docs.kraken.com/api/docs/futures-api/trading/send-batch-order + + :param str[] ids: order ids + :param str [symbol]: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + + EXCHANGE SPECIFIC PARAMETERS + :param str[] [params.clientOrderIds]: max length 10 e.g. ["my_id_1","my_id_2"] + :returns dict: an list of `order structures ` + """ + await self.load_markets() + orders = [] + clientOrderIds = self.safe_value(params, 'clientOrderIds', []) + clientOrderIdsLength = len(clientOrderIds) + if clientOrderIdsLength > 0: + for i in range(0, len(clientOrderIds)): + orders.append({'order': 'cancel', 'cliOrdId': clientOrderIds[i]}) + else: + for i in range(0, len(ids)): + orders.append({'order': 'cancel', 'order_id': ids[i]}) + request: dict = { + 'batchOrder': orders, + } + response = await self.privatePostBatchorder(self.extend(request, params)) + # { + # "result": "success", + # "serverTime": "2023-10-23T16:36:51.327Z", + # "batchStatus": [ + # { + # "status": "cancelled", + # "order_id": "101c2327-f12e-45f2-8445-7502b87afc0b", + # "orderEvents": [ + # { + # "uid": "101c2327-f12e-45f2-8445-7502b87afc0b", + # "order": { + # "orderId": "101c2327-f12e-45f2-8445-7502b87afc0b", + # "cliOrdId": null, + # "type": "lmt", + # "symbol": "PF_LTCUSD", + # "side": "buy", + # "quantity": "0.10000000000", + # "filled": "0E-11", + # "limitPrice": "50.00000000000", + # "reduceOnly": False, + # "timestamp": "2023-10-20T10:29:13.005Z", + # "lastUpdateTimestamp": "2023-10-20T10:29:13.005Z" + # }, + # "type": "CANCEL" + # } + # ] + # } + # ] + # } + batchStatus = self.safe_list(response, 'batchStatus', []) + return self.parse_orders(batchStatus) + + async def cancel_all_orders(self, symbol: Str = None, params={}): + """ + + https://docs.kraken.com/api/docs/futures-api/trading/cancel-all-orders + + Cancels all orders on the exchange, including trigger orders + :param str symbol: Unified market symbol + :param dict [params]: Exchange specific params + :returns: Response from exchange api + """ + request: dict = {} + if symbol is not None: + request['symbol'] = self.market_id(symbol) + response = await self.privatePostCancelallorders(self.extend(request, params)) + # + # { + # result: 'success', + # cancelStatus: { + # receivedTime: '2024-06-06T01:12:44.814Z', + # cancelOnly: 'PF_XRPUSD', + # status: 'cancelled', + # cancelledOrders: [{order_id: '272fd0ac-45c0-4003-b84d-d39b9e86bd36'}], + # orderEvents: [ + # { + # uid: '272fd0ac-45c0-4003-b84d-d39b9e86bd36', + # order: { + # orderId: '272fd0ac-45c0-4003-b84d-d39b9e86bd36', + # cliOrdId: null, + # type: 'lmt', + # symbol: 'PF_XRPUSD', + # side: 'buy', + # quantity: '10', + # filled: '0', + # limitPrice: '0.4', + # reduceOnly: False, + # timestamp: '2024-06-06T01:11:16.045Z', + # lastUpdateTimestamp: '2024-06-06T01:11:16.045Z' + # }, + # type: 'CANCEL' + # } + # ] + # }, + # serverTime: '2024-06-06T01:12:44.814Z' + # } + # + cancelStatus = self.safe_dict(response, 'cancelStatus') + orderEvents = self.safe_list(cancelStatus, 'orderEvents', []) + orders = [] + for i in range(0, len(orderEvents)): + orderEvent = self.safe_dict(orderEvents, 0) + order = self.safe_dict(orderEvent, 'order', {}) + orders.append(order) + return self.parse_orders(orders) + + async def cancel_all_orders_after(self, timeout: Int, params={}): + """ + dead man's switch, cancel all orders after the given timeout + + https://docs.kraken.com/api/docs/futures-api/trading/cancel-all-orders-after + + :param number timeout: time in milliseconds, 0 represents cancel the timer + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: the api result + """ + await self.load_markets() + request: dict = { + 'timeout': (self.parse_to_int(timeout / 1000)) if (timeout > 0) else 0, + } + response = await self.privatePostCancelallordersafter(self.extend(request, params)) + # + # { + # "result": "success", + # "serverTime": "2018-06-19T16:51:23.839Z", + # "status": { + # "currentTime": "2018-06-19T16:51:23.839Z", + # "triggerTime": "0" + # } + # } + # + return response + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + + https://docs.kraken.com/api/docs/futures-api/trading/get-open-orders + + Gets all open orders, including trigger orders, for an account from the exchange api + :param str symbol: Unified market symbol + :param int [since]: Timestamp(ms) of earliest order.(Not used by kraken api but filtered internally by CCXT) + :param int [limit]: How many orders to return.(Not used by kraken api but filtered internally by CCXT) + :param dict [params]: Exchange specific parameters + :returns: An array of `order structures ` + """ + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + response = await self.privateGetOpenorders(params) + orders = self.safe_list(response, 'openOrders', []) + return self.parse_orders(orders, market, since, limit) + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + + https://docs.futures.kraken.com/#http-api-history-account-history-get-order-events + + Gets all closed orders, including trigger orders, for an account from the exchange api + :param str symbol: Unified market symbol + :param int [since]: Timestamp(ms) of earliest order. + :param int [limit]: How many orders to return. + :param dict [params]: Exchange specific parameters + :returns: An array of `order structures ` + """ + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + request: dict = {} + if limit is not None: + request['count'] = limit + if since is not None: + request['from'] = since + response = await self.historyGetOrders(self.extend(request, params)) + allOrders = self.safe_list(response, 'elements', []) + closedOrders = [] + for i in range(0, len(allOrders)): + order = allOrders[i] + event = self.safe_dict(order, 'event', {}) + orderPlaced = self.safe_dict(event, 'OrderPlaced') + if orderPlaced is not None: + innerOrder = self.safe_dict(orderPlaced, 'order', {}) + filled = self.safe_string(innerOrder, 'filled') + if filled != '0': + innerOrder['status'] = 'closed' # status not available in the response + closedOrders.append(innerOrder) + return self.parse_orders(closedOrders, market, since, limit) + + async def fetch_canceled_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + + https://docs.kraken.com/api/docs/futures-api/history/get-order-events + + Gets all canceled orders, including trigger orders, for an account from the exchange api + :param str symbol: Unified market symbol + :param int [since]: Timestamp(ms) of earliest order. + :param int [limit]: How many orders to return. + :param dict [params]: Exchange specific parameters + :returns: An array of `order structures ` + """ + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + request: dict = {} + if limit is not None: + request['count'] = limit + if since is not None: + request['from'] = since + response = await self.historyGetOrders(self.extend(request, params)) + allOrders = self.safe_list(response, 'elements', []) + canceledAndRejected = [] + for i in range(0, len(allOrders)): + order = allOrders[i] + event = self.safe_dict(order, 'event', {}) + orderPlaced = self.safe_dict(event, 'OrderPlaced') + if orderPlaced is not None: + innerOrder = self.safe_dict(orderPlaced, 'order', {}) + filled = self.safe_string(innerOrder, 'filled') + if filled == '0': + innerOrder['status'] = 'canceled' # status not available in the response + canceledAndRejected.append(innerOrder) + orderCanceled = self.safe_dict(event, 'OrderCancelled') + if orderCanceled is not None: + innerOrder = self.safe_dict(orderCanceled, 'order', {}) + innerOrder['status'] = 'canceled' # status not available in the response + canceledAndRejected.append(innerOrder) + orderRejected = self.safe_dict(event, 'OrderRejected') + if orderRejected is not None: + innerOrder = self.safe_dict(orderRejected, 'order', {}) + innerOrder['status'] = 'rejected' # status not available in the response + canceledAndRejected.append(innerOrder) + return self.parse_orders(canceledAndRejected, market, since, limit) + + def parse_order_type(self, orderType): + typesMap: dict = { + 'lmt': 'limit', + 'mkt': 'market', + 'post': 'limit', + 'ioc': 'market', + } + return self.safe_string(typesMap, orderType, orderType) + + def verify_order_action_success(self, status, method, omit=[]): + errors: dict = { + 'invalidOrderType': InvalidOrder, + 'invalidSide': InvalidOrder, + 'invalidSize': InvalidOrder, + 'invalidPrice': InvalidOrder, + 'insufficientAvailableFunds': InsufficientFunds, + 'selfFill': ExchangeError, + 'tooManySmallOrders': ExchangeError, + 'maxPositionViolation': BadRequest, + 'marketSuspended': ExchangeNotAvailable, + 'marketInactive': ExchangeNotAvailable, + 'clientOrderIdAlreadyExist': DuplicateOrderId, + 'clientOrderIdTooLong': BadRequest, + 'outsidePriceCollar': InvalidOrder, + 'postWouldExecute': OrderImmediatelyFillable, # the unplaced order could actually be parsed(with status = "rejected"), but there is self specific error for self + 'iocWouldNotExecute': OrderNotFillable, # -||- + 'wouldNotReducePosition': ExchangeError, + 'orderForEditNotFound': OrderNotFound, + 'orderForEditNotAStop': InvalidOrder, + 'filled': OrderNotFound, + 'notFound': OrderNotFound, + } + if (status in errors) and not self.in_array(status, omit): + raise errors[status](self.id + ': ' + method + ' failed due to ' + status) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'placed': 'open', # the order was placed successfully + 'cancelled': 'canceled', # the order was cancelled successfully + 'invalidOrderType': 'rejected', # the order was not placed because orderType is invalid + 'invalidSide': 'rejected', # the order was not placed because side is invalid + 'invalidSize': 'rejected', # the order was not placed because size is invalid + 'invalidPrice': 'rejected', # the order was not placed because limitPrice and/or stopPrice are invalid + 'insufficientAvailableFunds': 'rejected', # the order was not placed because available funds are insufficient + 'selfFill': 'rejected', # the order was not placed because it would be filled against an existing order belonging to the same account + 'tooManySmallOrders': 'rejected', # the order was not placed because the number of small open orders would exceed the permissible limit + 'maxPositionViolation': 'rejected', # Order would cause you to exceed your maximum hasattr(self, position) contract. + 'marketSuspended': 'rejected', # the order was not placed because the market is suspended + 'marketInactive': 'rejected', # the order was not placed because the market is inactive + 'clientOrderIdAlreadyExist': 'rejected', # the specified client id already exist + 'clientOrderIdTooLong': 'rejected', # the client id is longer than the permissible limit + 'outsidePriceCollar': 'rejected', # the limit order crosses the spread but is an order of magnitude away from the mark price - fat finger control + # Should the next two be 'expired' ? + 'postWouldExecute': 'rejected', # the post-only order would be filled upon placement, thus is cancelled + 'iocWouldNotExecute': 'rejected', # the immediate-or-cancel order would not execute. + 'wouldNotReducePosition': 'rejected', # the reduce only order would not reduce position. + 'edited': 'open', # the order was edited successfully + 'orderForEditNotFound': 'rejected', # the requested order for edit has not been found + 'orderForEditNotAStop': 'rejected', # the supplied stopPrice cannot be applied because order is not a stop order + 'filled': 'closed', # the order was found completely filled and could not be cancelled + 'notFound': 'rejected', # the order was not found, either because it had already been cancelled or it never existed + 'untouched': 'open', # the entire size of the order is unfilled + 'partiallyFilled': 'open', # the size of the order is partially but not entirely filled + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # LIMIT + # + # { + # "order_id": "179f9af8-e45e-469d-b3e9-2fd4675cb7d0", + # "status": "placed", + # "receivedTime": "2019-09-05T16:33:50.734Z", + # "orderEvents": [ + # { + # "uid": "614a5298-0071-450f-83c6-0617ce8c6bc4", + # "order": { + # "orderId": "179f9af8-e45e-469d-b3e9-2fd4675cb7d0", + # "cliOrdId": null, + # "type": "lmt", + # "symbol": "pi_xbtusd", + # "side": "buy", + # "quantity": 10000, + # "filled": 0, + # "limitPrice": 9400, + # "reduceOnly": False, + # "timestamp": "2019-09-05T16:33:50.734Z", + # "lastUpdateTimestamp": "2019-09-05T16:33:50.734Z" + # }, + # "reducedQuantity": null, + # "reason": "WOULD_NOT_REDUCE_POSITION", # REJECTED + # "type": "PLACE" + # } + # ] + # } + # + # CONDITIONAL + # + # { + # "order_id": "1abfd3c6-af93-4b30-91cc-e4a93797f3f5", + # "status": "placed", + # "receivedTime": "2019-12-05T10:20:50.701Z", + # "orderEvents": [ + # { + # "orderTrigger": { + # "uid": "1abfd3c6-af93-4b30-91cc-e4a93797f3f5", + # "clientId":null, + # "type": "lmt", # "ioc" if stop market + # "symbol": "pi_xbtusd", + # "side": "buy", + # "quantity":10, + # "limitPrice":15000, + # "triggerPrice":9500, + # "triggerSide": "trigger_below", + # "triggerSignal": "mark_price", + # "reduceOnly":false, + # "timestamp": "2019-12-05T10:20:50.701Z", + # "lastUpdateTimestamp": "2019-12-05T10:20:50.701Z" + # }, + # "type": "PLACE" + # } + # ] + # } + # + # EXECUTION + # + # { + # "order_id": "61ca5732-3478-42fe-8362-abbfd9465294", + # "status": "placed", + # "receivedTime": "2019-12-11T17:17:33.888Z", + # "orderEvents": [ + # { + # "executionId": "e1ec9f63-2338-4c44-b40a-43486c6732d7", + # "price": 7244.5, + # "amount": 10, + # "orderPriorEdit": null, + # "orderPriorExecution": { + # "orderId": "61ca5732-3478-42fe-8362-abbfd9465294", + # "cliOrdId": null, + # "type": "lmt", + # "symbol": "pi_xbtusd", + # "side": "buy", + # "quantity": 10, + # "filled": 0, + # "limitPrice": 7500, + # "reduceOnly": False, + # "timestamp": "2019-12-11T17:17:33.888Z", + # "lastUpdateTimestamp": "2019-12-11T17:17:33.888Z" + # }, + # "takerReducedQuantity": null, + # "type": "EXECUTION" + # } + # ] + # } + # + # EDIT ORDER + # + # { + # "status": "edited", + # "orderId": "022774bc-2c4a-4f26-9317-436c8d85746d", + # "receivedTime": "2019-09-05T16:47:47.521Z", + # "orderEvents": [ + # { + # "old": { + # "orderId": "022774bc-2c4a-4f26-9317-436c8d85746d", + # "cliOrdId":null, + # "type": "lmt", + # "symbol": "pi_xbtusd", + # "side": "buy", + # "quantity":1000, + # "filled":0, + # "limitPrice":9400.0, + # "reduceOnly":false, + # "timestamp": "2019-09-05T16:41:35.173Z", + # "lastUpdateTimestamp": "2019-09-05T16:41:35.173Z" + # }, + # "new": { + # "orderId": "022774bc-2c4a-4f26-9317-436c8d85746d", + # "cliOrdId": null, + # "type": "lmt", + # "symbol": "pi_xbtusd", + # "side": "buy", + # "quantity": 1501, + # "filled": 0, + # "limitPrice": 7200, + # "reduceOnly": False, + # "timestamp": "2019-09-05T16:41:35.173Z", + # "lastUpdateTimestamp": "2019-09-05T16:47:47.519Z" + # }, + # "reducedQuantity": null, + # "type": "EDIT" + # } + # ] + # } + # + # CANCEL ORDER + # + # { + # "status": "cancelled", + # "orderEvents": [ + # { + # "uid": "85c40002-3f20-4e87-9302-262626c3531b", + # "order": { + # "orderId": "85c40002-3f20-4e87-9302-262626c3531b", + # "cliOrdId": null, + # "type": "lmt", + # "symbol": "pi_xbtusd", + # "side": "buy", + # "quantity": 1000, + # "filled": 0, + # "limitPrice": 10144, + # "stopPrice": null, + # "reduceOnly": False, + # "timestamp": "2019-08-01T15:26:27.790Z" + # }, + # "type": "CANCEL" + # } + # ] + # } + # + # cancelAllOrders + # + # { + # "orderId": "85c40002-3f20-4e87-9302-262626c3531b", + # "cliOrdId": null, + # "type": "lmt", + # "symbol": "pi_xbtusd", + # "side": "buy", + # "quantity": 1000, + # "filled": 0, + # "limitPrice": 10144, + # "stopPrice": null, + # "reduceOnly": False, + # "timestamp": "2019-08-01T15:26:27.790Z" + # } + # + # FETCH OPEN ORDERS + # + # { + # "order_id": "59302619-41d2-4f0b-941f-7e7914760ad3", + # "symbol": "pi_xbtusd", + # "side": "sell", + # "orderType": "lmt", + # "limitPrice": 10640, + # "unfilledSize": 304, + # "receivedTime": "2019-09-05T17:01:17.410Z", + # "status": "untouched", + # "filledSize": 0, + # "reduceOnly": True, + # "lastUpdateTime": "2019-09-05T17:01:17.410Z" + # } + # + # createOrders error + # { + # "status": "requiredArgumentMissing", + # "orderEvents": [] + # } + # closed orders + # { + # uid: '2f00cd63-e61d-44f8-8569-adabde885941', + # timestamp: '1707258274849', + # event: { + # OrderPlaced: { + # order: { + # uid: '85805e01-9eed-4395-8360-ed1a228237c9', + # accountUid: '406142dd-7c5c-4a8b-acbc-5f16eca30009', + # tradeable: 'PF_LTCUSD', + # direction: 'Buy', + # quantity: '0', + # filled: '0.1', + # timestamp: '1707258274849', + # limitPrice: '69.2200000000', + # orderType: 'IoC', + # clientId: '', + # reduceOnly: False, + # lastUpdateTimestamp: '1707258274849' + # }, + # reason: 'new_user_order', + # reducedQuantity: '', + # algoId: '' + # } + # } + # } + # + # { + # uid: '85805e01-9eed-4395-8360-ed1a228237c9', + # accountUid: '406142dd-7c5c-4a8b-acbc-5f16eca30009', + # tradeable: 'PF_LTCUSD', + # direction: 'Buy', + # quantity: '0', + # filled: '0.1', + # timestamp: '1707258274849', + # limitPrice: '69.2200000000', + # orderType: 'IoC', + # clientId: '', + # reduceOnly: False, + # lastUpdateTimestamp: '1707258274849', + # status: 'closed' + # } + # + orderEvents = self.safe_value(order, 'orderEvents', []) + errorStatus = self.safe_string(order, 'status') + orderEventsLength = len(orderEvents) + if ('orderEvents' in order) and (errorStatus is not None) and (orderEventsLength == 0): + # creteOrders error response + return self.safe_order({'info': order, 'status': 'rejected'}) + details = None + isPrior = False + fixed = False + statusId = None + price = None + trades = [] + if orderEventsLength: + executions = [] + for i in range(0, len(orderEvents)): + item = orderEvents[i] + if self.safe_string(item, 'type') == 'EXECUTION': + executions.append(item) + # Final order(after placement / editing / execution / canceling) + orderTrigger = self.safe_value(item, 'orderTrigger') + if details is None: + details = self.safe_value_2(item, 'new', 'order', orderTrigger) + if details is not None: + isPrior = False + fixed = True + elif not fixed: + orderPriorExecution = self.safe_value(item, 'orderPriorExecution') + details = self.safe_value_2(item, 'orderPriorExecution', 'orderPriorEdit') + price = self.safe_string(orderPriorExecution, 'limitPrice') + if details is not None: + isPrior = True + trades = self.parse_trades(executions) + statusId = self.safe_string(order, 'status') + if details is None: + details = order + if statusId is None: + statusId = self.safe_string(details, 'status') + # This may be incorrectly marked as "open" if only execution report is given, + # but will be fixed below + status = self.parse_order_status(statusId) + isClosed = self.in_array(status, ['canceled', 'rejected', 'closed']) + marketId = self.safe_string(details, 'symbol') + market = self.safe_market(marketId, market) + timestamp = self.parse8601(self.safe_string_2(details, 'timestamp', 'receivedTime')) + lastUpdateTimestamp = self.parse8601(self.safe_string(details, 'lastUpdateTime')) + if price is None: + price = self.safe_string(details, 'limitPrice') + amount = self.safe_string(details, 'quantity') + filled = self.safe_string_2(details, 'filledSize', 'filled', '0.0') + remaining = self.safe_string(details, 'unfilledSize') + average = None + filled2 = '0.0' + tradesLength = len(trades) + if tradesLength > 0: + vwapSum = '0.0' + for i in range(0, len(trades)): + trade = trades[i] + tradeAmount = self.safe_string(trade, 'amount') + tradePrice = self.safe_string(trade, 'price') + filled2 = Precise.string_add(filled2, tradeAmount) + vwapSum = Precise.string_add(vwapSum, Precise.string_mul(tradeAmount, tradePrice)) + average = Precise.string_div(vwapSum, filled2) + if (amount is not None) and (not isClosed) and isPrior and Precise.string_ge(filled2, amount): + status = 'closed' + isClosed = True + if isPrior: + filled = Precise.string_add(filled, filled2) + else: + filled = Precise.string_max(filled, filled2) + if remaining is None: + if isPrior: + if amount is not None: + # remaining amount before execution minus executed amount + remaining = Precise.string_sub(amount, filled2) + else: + remaining = amount + # if fetchOpenOrders are parsed + if (amount is None) and (not isPrior) and (remaining is not None): + amount = Precise.string_add(filled, remaining) + cost = None + if (filled is not None) and (market is not None): + whichPrice = average if (average is not None) else price + if whichPrice is not None: + if market['linear']: + cost = Precise.string_mul(filled, whichPrice) # in quote + else: + cost = Precise.string_div(filled, whichPrice) # in base + id = self.safe_string_2(order, 'order_id', 'orderId') + if id is None: + id = self.safe_string_2(details, 'orderId', 'uid') + type = self.safe_string_lower_2(details, 'type', 'orderType') + timeInForce = 'gtc' + if type == 'ioc' or self.parse_order_type(type) == 'market': + timeInForce = 'ioc' + symbol = self.safe_string(market, 'symbol') + if 'tradeable' in details: + symbol = self.safe_symbol(self.safe_string(details, 'tradeable'), market) + ts = self.safe_integer(details, 'timestamp', timestamp) + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': self.safe_string_n(details, ['clientOrderId', 'clientId', 'cliOrdId']), + 'timestamp': ts, + 'datetime': self.iso8601(ts), + 'lastTradeTimestamp': None, + 'lastUpdateTimestamp': self.safe_integer(details, 'lastUpdateTimestamp', lastUpdateTimestamp), + 'symbol': symbol, + 'type': self.parse_order_type(type), + 'timeInForce': timeInForce, + 'postOnly': type == 'post', + 'reduceOnly': self.safe_bool_2(details, 'reduceOnly', 'reduce_only'), + 'side': self.safe_string_lower_2(details, 'side', 'direction'), + 'price': price, + 'triggerPrice': self.safe_string(details, 'triggerPrice'), + 'amount': amount, + 'cost': cost, + 'average': average, + 'filled': filled, + 'remaining': remaining, + 'status': status, + 'fee': None, + 'fees': None, + 'trades': trades, + }) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://docs.kraken.com/api/docs/futures-api/trading/get-fills + + :param str symbol: unified market symbol + :param int [since]: *not used by the api* the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch entries for + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + # todo: lastFillTime: self.iso8601(end) + response = await self.privateGetFills(params) + # + # { + # "result": "success", + # "serverTime": "2016-02-25T09:45:53.818Z", + # "fills": [ + # { + # "fillTime": "2016-02-25T09:47:01.000Z", + # "order_id": "c18f0c17-9971-40e6-8e5b-10df05d422f0", + # "fill_id": "522d4e08-96e7-4b44-9694-bfaea8fe215e", + # "cliOrdId": "d427f920-ec55-4c18-ba95-5fe241513b30", # EXTRA + # "symbol": "fi_xbtusd_180615", + # "side": "buy", + # "size": 2000, + # "price": 4255, + # "fillType": "maker" + # }, + # ... + # ] + # } + # + return self.parse_trades(response['fills'], market, since, limit) + + async def fetch_balance(self, params={}) -> Balances: + """ + + https://docs.kraken.com/api/docs/futures-api/trading/get-accounts + + Fetch the balance for a sub-account, all sub-account balances are inside 'info' in the response + :param dict [params]: Exchange specific parameters + :param str [params.type]: The sub-account type to query the balance of, possible values include 'flex', 'cash'/'main'/'funding', or a market symbol * defaults to 'flex' * + :param str [params.symbol]: A unified market symbol, when assigned the balance for a trading market that matches the symbol is returned + :returns: A `balance structure ` + """ + await self.load_markets() + type = self.safe_string_2(params, 'type', 'account') + symbol = self.safe_string(params, 'symbol') + params = self.omit(params, ['type', 'account', 'symbol']) + response = await self.privateGetAccounts(params) + # + # { + # "result": "success", + # "accounts": { + # "fi_xbtusd": { + # "auxiliary": {usd: "0", pv: '0.0', pnl: '0.0', af: '0.0', funding: "0.0"}, + # "marginRequirements": {im: '0.0', mm: '0.0', lt: '0.0', tt: "0.0"}, + # "triggerEstimates": {im: '0', mm: '0', lt: "0", tt: "0"}, + # "balances": {xbt: "0.0"}, + # "currency": "xbt", + # "type": "marginAccount" + # }, + # "cash": { + # "balances": { + # "eur": "0.0", + # "gbp": "0.0", + # "bch": "0.0", + # "xrp": "2.20188538338", + # "usd": "0.0", + # "eth": "0.0", + # "usdt": "0.0", + # "ltc": "0.0", + # "usdc": "0.0", + # "xbt": "0.0" + # }, + # "type": "cashAccount" + # }, + # "fv_xrpxbt": { + # "auxiliary": {usd: "0", pv: '0.0', pnl: '0.0', af: '0.0', funding: "0.0"}, + # "marginRequirements": {im: '0.0', mm: '0.0', lt: '0.0', tt: "0.0"}, + # "triggerEstimates": {im: '0', mm: '0', lt: "0", tt: "0"}, + # "balances": {xbt: "0.0"}, + # "currency": "xbt", + # "type": "marginAccount" + # }, + # "fi_xrpusd": { + # "auxiliary": {usd: "0", pv: '11.0', pnl: '0.0', af: '11.0', funding: "0.0"}, + # "marginRequirements": {im: '0.0', mm: '0.0', lt: '0.0', tt: "0.0"}, + # "triggerEstimates": {im: '0', mm: '0', lt: "0", tt: "0"}, + # "balances": {xrp: "11.0"}, + # "currency": "xrp", + # "type": "marginAccount" + # }, + # "fi_ethusd": { + # "auxiliary": {usd: "0", pv: '0.0', pnl: '0.0', af: '0.0', funding: "0.0"}, + # "marginRequirements": {im: '0.0', mm: '0.0', lt: '0.0', tt: "0.0"}, + # "triggerEstimates": {im: '0', mm: '0', lt: "0", tt: "0"}, + # "balances": {eth: "0.0"}, + # "currency": "eth", + # "type": "marginAccount" + # }, + # "fi_ltcusd": { + # "auxiliary": {usd: "0", pv: '0.0', pnl: '0.0', af: '0.0', funding: "0.0"}, + # "marginRequirements": {im: '0.0', mm: '0.0', lt: '0.0', tt: "0.0"}, + # "triggerEstimates": {im: '0', mm: '0', lt: "0", tt: "0"}, + # "balances": {ltc: "0.0"}, + # "currency": "ltc", + # "type": "marginAccount" + # }, + # "fi_bchusd": { + # "auxiliary": {usd: "0", pv: '0.0', pnl: '0.0', af: '0.0', funding: "0.0"}, + # "marginRequirements": {im: '0.0', mm: '0.0', lt: '0.0', tt: "0.0"}, + # "triggerEstimates": {im: '0', mm: '0', lt: "0", tt: "0"}, + # "balances": {bch: "0.0"}, + # "currency": "bch", + # "type": "marginAccount" + # }, + # "flex": { + # "currencies": {}, + # "initialMargin": "0.0", + # "initialMarginWithOrders": "0.0", + # "maintenanceMargin": "0.0", + # "balanceValue": "0.0", + # "portfolioValue": "0.0", + # "collateralValue": "0.0", + # "pnl": "0.0", + # "unrealizedFunding": "0.0", + # "totalUnrealized": "0.0", + # "totalUnrealizedAsMargin": "0.0", + # "availableMargin": "0.0", + # "marginEquity": "0.0", + # "type": "multiCollateralMarginAccount" + # } + # }, + # "serverTime": "2022-04-12T07:48:07.475Z" + # } + # + datetime = self.safe_string(response, 'serverTime') + if type == 'marginAccount' or type == 'margin': + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchBalance requires symbol argument for margin accounts') + type = symbol + if type is None: + type = 'flex' if (symbol is None) else symbol + accountName = self.parse_account(type) + accounts = self.safe_value(response, 'accounts') + account = self.safe_value(accounts, accountName) + if account is None: + type = '' if (type is None) else type + symbol = '' if (symbol is None) else symbol + raise BadRequest(self.id + ' fetchBalance has no account for ' + type) + balance = self.parse_balance(account) + balance['info'] = response + balance['timestamp'] = self.parse8601(datetime) + balance['datetime'] = datetime + return balance + + def parse_balance(self, response) -> Balances: + # + # cashAccount + # + # { + # "balances": { + # "eur": "0.0", + # "gbp": "0.0", + # "bch": "0.0", + # "xrp": "2.20188538338", + # "usd": "0.0", + # "eth": "0.0", + # "usdt": "0.0", + # "ltc": "0.0", + # "usdc": "0.0", + # "xbt": "0.0" + # }, + # "type": "cashAccount" + # } + # + # marginAccount e,g, fi_xrpusd + # + # { + # "auxiliary": { + # "usd": "0", + # "pv": "11.0", + # "pnl": "0.0", + # "af": "11.0", + # "funding": "0.0" + # }, + # "marginRequirements": {im: '0.0', mm: '0.0', lt: '0.0', tt: "0.0"}, + # "triggerEstimates": {im: '0', mm: '0', lt: "0", tt: "0"}, + # "balances": {xrp: "11.0"}, + # "currency": "xrp", + # "type": "marginAccount" + # } + # + # flex/multiCollateralMarginAccount + # + # { + # "currencies": { + # "USDT": { + # "quantity": "1", + # "value": "1.0001", + # "collateral": "0.9477197625", + # "available": "1.0" + # } + # }, + # "initialMargin": "0.0", + # "initialMarginWithOrders": "0.0", + # "maintenanceMargin": "0.0", + # "balanceValue": "1.0", + # "portfolioValue": "1.0", + # "collateralValue": "0.95", + # "pnl": "0.0", + # "unrealizedFunding": "0.0", + # "totalUnrealized": "0.0", + # "totalUnrealizedAsMargin": "0.0", + # "availableMargin": "0.95", + # "marginEquity": "0.95", + # "type": "multiCollateralMarginAccount" + # } + # + accountType = self.safe_string_2(response, 'accountType', 'type') + isFlex = (accountType == 'multiCollateralMarginAccount') + isCash = (accountType == 'cashAccount') + balances = self.safe_value_2(response, 'balances', 'currencies', {}) + result: dict = {} + currencyIds = list(balances.keys()) + for i in range(0, len(currencyIds)): + currencyId = currencyIds[i] + balance = balances[currencyId] + code = self.safe_currency_code(currencyId) + splitCode = code.split('_') + codeLength = len(splitCode) + if codeLength > 1: + continue # Removes contract codes like PI_XRPUSD + account = self.account() + if isFlex: + account['total'] = self.safe_string(balance, 'quantity') + account['free'] = self.safe_string(balance, 'available') + elif isCash: + account['used'] = '0.0' + account['total'] = balance + else: + auxiliary = self.safe_value(response, 'auxiliary') + account['free'] = self.safe_string(auxiliary, 'af') + account['total'] = self.safe_string(auxiliary, 'pv') + result[code] = account + return self.safe_balance(result) + + async def fetch_funding_rates(self, symbols: Strings = None, params={}) -> FundingRates: + """ + fetch the current funding rates for multiple markets + + https://docs.kraken.com/api/docs/futures-api/trading/get-tickers + + :param str[] symbols: unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: an array of `funding rate structures ` + """ + await self.load_markets() + marketIds = self.market_ids(symbols) + response = await self.publicGetTickers(params) + tickers = self.safe_list(response, 'tickers', []) + fundingRates = [] + for i in range(0, len(tickers)): + entry = tickers[i] + entry_symbol = self.safe_value(entry, 'symbol') + if marketIds is not None: + if not self.in_array(entry_symbol, marketIds): + continue + market = self.safe_market(entry_symbol) + parsed = self.parse_funding_rate(entry, market) + fundingRates.append(parsed) + return self.index_by(fundingRates, 'symbol') + + def parse_funding_rate(self, ticker, market: Market = None) -> FundingRate: + # + # { + # "symbol": "PF_ENJUSD", + # "last": 0.0433, + # "lastTime": "2025-10-22T11:02:25.599Z", + # "tag": "perpetual", + # "pair": "ENJ:USD", + # "markPrice": 0.0434, + # "bid": 0.0433, + # "bidSize": 4609, + # "ask": 0.0435, + # "askSize": 4609, + # "vol24h": 1696, + # "volumeQuote": 73.5216, + # "openInterest": 72513.00000000000, + # "open24h": 0.0435, + # "high24h": 0.0435, + # "low24h": 0.0433, + # "lastSize": 1272, + # "fundingRate": -0.000000756414717067, + # "fundingRatePrediction": 0.000000195218676, + # "suspended": False, + # "indexPrice": 0.043392, + # "postOnly": False, + # "change24h": -0.46 + # } + # + marketId = self.safe_string(ticker, 'symbol') + symbol = self.symbol(marketId) + timestamp = self.parse8601(self.safe_string(ticker, 'lastTime')) + markPriceString = self.safe_string(ticker, 'markPrice') + fundingRateString = self.safe_string(ticker, 'fundingRate') + fundingRateResult = Precise.string_div(fundingRateString, markPriceString) + nextFundingRateString = self.safe_string(ticker, 'fundingRatePrediction') + nextFundingRateResult = Precise.string_div(nextFundingRateString, markPriceString) + if fundingRateResult > '0.25': + fundingRateResult = '0.25' + elif fundingRateResult > '-0.25': + fundingRateResult = '-0.25' + if nextFundingRateResult > '0.25': + nextFundingRateResult = '0.25' + elif nextFundingRateResult > '-0.25': + nextFundingRateResult = '-0.25' + return { + 'info': ticker, + 'symbol': symbol, + 'markPrice': self.parse_number(markPriceString), + 'indexPrice': self.safe_number(ticker, 'indexPrice'), + 'interestRate': None, + 'estimatedSettlePrice': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'fundingRate': self.parse_number(fundingRateResult), + 'fundingTimestamp': None, + 'fundingDatetime': None, + 'nextFundingRate': self.parse_number(nextFundingRateResult), + 'nextFundingTimestamp': None, + 'nextFundingDatetime': None, + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': '1h', + } + + async def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical funding rate prices + + https://docs.kraken.com/api/docs/futures-api/trading/historical-funding-rates + + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: timestamp in ms of the earliest funding rate to fetch + :param int [limit]: the maximum amount of `funding rate structures ` to fetch + :param dict [params]: extra parameters specific to the api endpoint + :returns dict[]: a list of `funding rate structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchFundingRateHistory() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + if not market['swap']: + raise BadRequest(self.id + ' fetchFundingRateHistory() supports swap contracts only') + request: dict = { + 'symbol': market['id'].upper(), + } + response = await self.publicGetHistoricalfundingrates(self.extend(request, params)) + # + # { + # "rates": [ + # { + # "timestamp": '2018-08-31T16:00:00.000Z', + # "fundingRate": '2.18900669884E-7', + # "relativeFundingRate": '0.000060779960000000' + # }, + # ... + # ] + # } + # + rates = self.safe_value(response, 'rates') + result = [] + for i in range(0, len(rates)): + item = rates[i] + datetime = self.safe_string(item, 'timestamp') + result.append({ + 'info': item, + 'symbol': symbol, + 'fundingRate': self.safe_number(item, 'relativeFundingRate'), + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + }) + sorted = self.sort_by(result, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, symbol, since, limit) + + async def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + + https://docs.kraken.com/api/docs/futures-api/trading/get-open-positions + + Fetches current contract trading positions + :param str[] symbols: List of unified symbols + :param dict [params]: Not used by krakenfutures + :returns: Parsed exchange response for positions + """ + await self.load_markets() + request: dict = {} + response = await self.privateGetOpenpositions(request) + # + # { + # "result": "success", + # "openPositions": [ + # { + # "side": "long", + # "symbol": "pi_xrpusd", + # "price": "0.7533", + # "fillTime": "2022-03-03T22:51:16.566Z", + # "size": "230", + # "unrealizedFunding": "-0.001878596918214635" + # } + # ], + # "serverTime": "2022-03-03T22:51:16.566Z" + # } + # + result = self.parse_positions(response) + return self.filter_by_array_positions(result, 'symbol', symbols, False) + + def parse_positions(self, response, symbols: Strings = None, params={}): + result = [] + positions = self.safe_value(response, 'openPositions') + for i in range(0, len(positions)): + position = self.parse_position(positions[i]) + result.append(position) + return result + + def parse_position(self, position: dict, market: Market = None): + # cross + # { + # "side": "long", + # "symbol": "pi_xrpusd", + # "price": "0.7533", + # "fillTime": "2022-03-03T22:51:16.566Z", + # "size": "230", + # "unrealizedFunding": "-0.001878596918214635" + # } + # + # isolated + # { + # "side":"long", + # "symbol":"pf_ftmusd", + # "price":"0.4921", + # "fillTime":"2023-02-22T11:37:16.685Z", + # "size":"1", + # "unrealizedFunding":"-8.155240068885155E-8", + # "pnlCurrency":"USD", + # "maxFixedLeverage":"1.0" + # } + # + leverage = self.safe_number(position, 'maxFixedLeverage') + marginType = 'cross' + if leverage is not None: + marginType = 'isolated' + datetime = self.safe_string(position, 'fillTime') + marketId = self.safe_string(position, 'symbol') + market = self.safe_market(marketId, market) + return { + 'info': position, + 'symbol': market['symbol'], + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + 'initialMargin': None, + 'initialMarginPercentage': None, + 'maintenanceMargin': None, + 'maintenanceMarginPercentage': None, + 'entryPrice': self.safe_number(position, 'price'), + 'notional': None, + 'leverage': leverage, + 'unrealizedPnl': None, + 'contracts': self.safe_number(position, 'size'), + 'contractSize': self.safe_number(market, 'contractSize'), + 'marginRatio': None, + 'liquidationPrice': None, + 'markPrice': None, + 'collateral': None, + 'marginType': marginType, + 'side': self.safe_string(position, 'side'), + 'percentage': None, + } + + async def fetch_leverage_tiers(self, symbols: Strings = None, params={}) -> LeverageTiers: + """ + retrieve information on the maximum leverage, and maintenance margin for trades of varying trade sizes + + https://docs.kraken.com/api/docs/futures-api/trading/get-instruments + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `leverage tiers structures `, indexed by market symbols + """ + await self.load_markets() + response = await self.publicGetInstruments(params) + # + # { + # "result": "success", + # "instruments": [ + # { + # "symbol": "fi_ethusd_180928", + # "type": "futures_inverse", # futures_vanilla # spot index + # "underlying": "rr_ethusd", + # "lastTradingTime": "2018-09-28T15:00:00.000Z", + # "tickSize": 0.1, + # "contractSize": 1, + # "tradeable": True, + # "marginLevels": [ + # { + # "contracts":0, + # "initialMargin":0.02, + # "maintenanceMargin":0.01 + # }, + # { + # "contracts":250000, + # "initialMargin":0.04, + # "maintenanceMargin":0.02 + # }, + # ... + # ], + # "isin": "GB00JVMLMP88", + # "retailMarginLevels": [ + # { + # "contracts": 0, + # "initialMargin": 0.5, + # "maintenanceMargin": 0.25 + # } + # ], + # "tags": [], + # }, + # { + # "symbol": "in_xbtusd", + # "type": "spot index", + # "tradeable":false + # } + # ] + # "serverTime": "2018-07-19T11:32:39.433Z" + # } + # + data = self.safe_list(response, 'instruments') + return self.parse_leverage_tiers(data, symbols, 'symbol') + + def parse_market_leverage_tiers(self, info, market: Market = None) -> List[LeverageTier]: + """ + @ignore + @param info Exchange market response for 1 market + @param market CCXT market + """ + # + # { + # "symbol": "fi_ethusd_180928", + # "type": "futures_inverse", # futures_vanilla # spot index + # "underlying": "rr_ethusd", + # "lastTradingTime": "2018-09-28T15:00:00.000Z", + # "tickSize": 0.1, + # "contractSize": 1, + # "tradeable": True, + # "marginLevels": [ + # { + # "contracts":0, + # "initialMargin":0.02, + # "maintenanceMargin":0.01 + # }, + # { + # "contracts":250000, + # "initialMargin":0.04, + # "maintenanceMargin":0.02 + # }, + # ... + # ], + # "isin": "GB00JVMLMP88", + # "retailMarginLevels": [ + # { + # "contracts": 0, + # "initialMargin": 0.5, + # "maintenanceMargin": 0.25 + # } + # ], + # "tags": [], + # } + # + marginLevels = self.safe_value(info, 'marginLevels') + marketId = self.safe_string(info, 'symbol') + market = self.safe_market(marketId, market) + tiers = [] + if marginLevels is None: + return tiers + for i in range(0, len(marginLevels)): + tier = marginLevels[i] + initialMargin = self.safe_string(tier, 'initialMargin') + minNotional = self.safe_number(tier, 'numNonContractUnits') + if i != 0: + tiersLength = len(tiers) + previousTier = tiers[tiersLength - 1] + previousTier['maxNotional'] = minNotional + tiers.append({ + 'tier': self.sum(i, 1), + 'symbol': self.safe_symbol(marketId, market), + 'currency': market['quote'], + 'minNotional': minNotional, + 'maxNotional': None, + 'maintenanceMarginRate': self.safe_number(tier, 'maintenanceMargin'), + 'maxLeverage': self.parse_number(Precise.string_div('1', initialMargin)), + 'info': tier, + }) + return tiers + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + # + # transfer + # + # { + # "result": "success", + # "serverTime": "2022-04-12T01:22:53.420Z" + # } + # + datetime = self.safe_string(transfer, 'serverTime') + return { + 'info': transfer, + 'id': None, + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + 'currency': self.safe_string(currency, 'code'), + 'amount': None, + 'fromAccount': None, + 'toAccount': None, + 'status': self.safe_string(transfer, 'result'), + } + + def parse_account(self, account): + accountByType: dict = { + 'main': 'cash', + 'funding': 'cash', + 'future': 'cash', + 'futures': 'cash', + 'cashAccount': 'cash', + 'multiCollateralMarginAccount': 'flex', + 'multiCollateral': 'flex', + 'multiCollateralMargin': 'flex', + } + if account in accountByType: + return accountByType[account] + elif account in self.markets: + market = self.market(account) + marketId = market['id'] + splitId = marketId.split('_') + if market['inverse']: + return 'fi_' + self.safe_string(splitId, 1) + else: + return 'fv_' + self.safe_string(splitId, 1) + else: + return account + + async def transfer_out(self, code: str, amount, params={}): + """ + transfer from futures wallet to spot wallet + :param str code: Unified currency code + :param float amount: Size of the transfer + :param dict [params]: Exchange specific parameters + :returns: a `transfer structure ` + """ + return await self.transfer(code, amount, 'future', 'spot', params) + + async def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + + https://docs.kraken.com/api/docs/futures-api/trading/transfer + https://docs.kraken.com/api/docs/futures-api/trading/sub-account-transfer + + transfers currencies between sub-accounts + :param str code: Unified currency code + :param float amount: Size of the transfer + :param str fromAccount: 'main'/'funding'/'future', 'flex', or a unified market symbol + :param str toAccount: 'main'/'funding', 'flex', 'spot' or a unified market symbol + :param dict [params]: Exchange specific parameters + :returns: a `transfer structure ` + """ + await self.load_markets() + currency = self.currency(code) + if fromAccount == 'spot': + raise BadRequest(self.id + ' transfer does not yet support transfers from spot') + request: dict = { + 'amount': amount, + } + response = None + if toAccount == 'spot': + if self.parse_account(fromAccount) != 'cash': + raise BadRequest(self.id + ' transfer cannot transfer from ' + fromAccount + ' to ' + toAccount) + request['currency'] = currency['id'] + response = await self.privatePostWithdrawal(self.extend(request, params)) + else: + request['fromAccount'] = self.parse_account(fromAccount) + request['toAccount'] = self.parse_account(toAccount) + request['unit'] = currency['id'] + response = await self.privatePostTransfer(self.extend(request, params)) + # + # { + # "result": "success", + # "serverTime": "2022-04-12T01:22:53.420Z" + # } + # + transfer = self.parse_transfer(response, currency) + return self.extend(transfer, { + 'amount': amount, + 'fromAccount': fromAccount, + 'toAccount': toAccount, + }) + + async def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://docs.kraken.com/api/docs/futures-api/trading/set-leverage-setting + + :param float leverage: the rate of leverage + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setLeverage() requires a symbol argument') + await self.load_markets() + request: dict = { + 'maxLeverage': leverage, + 'symbol': self.market_id(symbol).upper(), + } + # + # {result: "success", serverTime: "2023-08-01T09:40:32.345Z"} + # + return await self.privatePutLeveragepreferences(self.extend(request, params)) + + async def fetch_leverages(self, symbols: Strings = None, params={}) -> Leverages: + """ + fetch the set leverage for all contract and margin markets + + https://docs.kraken.com/api/docs/futures-api/trading/get-leverage-setting + + :param str[] [symbols]: a list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `leverage structures ` + """ + await self.load_markets() + response = await self.privateGetLeveragepreferences(params) + # + # { + # "result": "success", + # "serverTime": "2024-03-06T02:35:46.336Z", + # "leveragePreferences": [ + # { + # "symbol": "PF_ETHUSD", + # "maxLeverage": 30.00 + # }, + # ] + # } + # + leveragePreferences = self.safe_list(response, 'leveragePreferences', []) + return self.parse_leverages(leveragePreferences, symbols, 'symbol') + + async def fetch_leverage(self, symbol: str, params={}) -> Leverage: + """ + fetch the set leverage for a market + + https://docs.kraken.com/api/docs/futures-api/trading/get-leverage-setting + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `leverage structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchLeverage() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': self.market_id(symbol).upper(), + } + response = await self.privateGetLeveragepreferences(self.extend(request, params)) + # + # { + # "result": "success", + # "serverTime": "2023-08-01T09:54:08.900Z", + # "leveragePreferences": [{symbol: "PF_LTCUSD", maxLeverage: "5.00"}] + # } + # + leveragePreferences = self.safe_list(response, 'leveragePreferences', []) + data = self.safe_dict(leveragePreferences, 0, {}) + return self.parse_leverage(data, market) + + def parse_leverage(self, leverage: dict, market: Market = None) -> Leverage: + marketId = self.safe_string(leverage, 'symbol') + leverageValue = self.safe_integer(leverage, 'maxLeverage') + return { + 'info': leverage, + 'symbol': self.safe_symbol(marketId, market), + 'marginMode': None, + 'longLeverage': leverageValue, + 'shortLeverage': leverageValue, + } + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None + if code == 429: + raise DDoSProtection(self.id + ' ' + body) + errors = self.safe_value(response, 'errors') + firstError = self.safe_value(errors, 0) + firtErrorMessage = self.safe_string(firstError, 'message') + message = self.safe_string(response, 'error', firtErrorMessage) + if message is None: + return None + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + if code == 400: + raise BadRequest(feedback) + raise ExchangeError(feedback) # unknown message + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + apiVersions = self.safe_value(self.options['versions'], api, {}) + methodVersions = self.safe_value(apiVersions, method, {}) + defaultVersion = self.safe_string(methodVersions, path, self.version) + version = self.safe_string(params, 'version', defaultVersion) + params = self.omit(params, 'version') + apiAccess = self.safe_value(self.options['access'], api, {}) + methodAccess = self.safe_value(apiAccess, method, {}) + access = self.safe_string(methodAccess, path, 'public') + endpoint = version + '/' + self.implode_params(path, params) + params = self.omit(params, self.extract_params(path)) + query = endpoint + postData = '' + if path == 'batchorder': + postData = 'json=' + self.json(params) + body = postData + elif params: + postData = self.urlencode(params) + query += '?' + postData + url = self.urls['api'][api] + query + if api == 'private' or access == 'private': + self.check_required_credentials() + auth = postData + '/api/' + if api != 'private': + auth += api + '/' + auth += endpoint # 1 + hash = self.hash(self.encode(auth), 'sha256', 'binary') # 2 + secret = self.base64_to_binary(self.secret) # 3 + signature = self.hmac(hash, secret, hashlib.sha512, 'base64') # 4-5 + headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'application/json', + 'APIKey': self.apiKey, + 'Authent': signature, + } + return {'url': url, 'method': method, 'body': body, 'headers': headers} diff --git a/ccxt/async_support/kucoin.py b/ccxt/async_support/kucoin.py new file mode 100644 index 0000000..3db33c1 --- /dev/null +++ b/ccxt/async_support/kucoin.py @@ -0,0 +1,5520 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.kucoin import ImplicitAPI +import asyncio +import hashlib +import math +import json +from ccxt.base.types import Account, Any, Balances, BorrowInterest, Bool, Currencies, Currency, DepositAddress, Int, LedgerEntry, Market, Num, Order, OrderBook, OrderRequest, OrderSide, OrderType, Str, Strings, Ticker, Tickers, FundingRate, Trade, TradingFeeInterface, Transaction, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import AccountSuspended +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidAddress +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import NotSupported +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.errors import InvalidNonce +from ccxt.base.decimal_to_precision import TRUNCATE +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class kucoin(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(kucoin, self).describe(), { + 'id': 'kucoin', + 'name': 'KuCoin', + 'countries': ['SC'], + 'rateLimit': 10, # 100 requests per second =>( 1000ms / 100 ) = 10 ms between requests on average + 'version': 'v2', + 'certified': True, + 'pro': True, + 'comment': 'Platform 2.0', + 'quoteJsonNumbers': False, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': True, + 'swap': False, + 'future': False, + 'option': False, + 'borrowCrossMargin': True, + 'borrowIsolatedMargin': True, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createDepositAddress': True, + 'createMarketBuyOrderWithCost': True, + 'createMarketOrderWithCost': True, + 'createMarketSellOrderWithCost': True, + 'createOrder': True, + 'createOrders': True, + 'createPostOnlyOrder': True, + 'createStopLimitOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'createTriggerOrder': True, + 'editOrder': True, + 'fetchAccounts': True, + 'fetchBalance': True, + 'fetchBorrowInterest': True, + 'fetchBorrowRateHistories': True, + 'fetchBorrowRateHistory': True, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': True, + 'fetchDeposits': True, + 'fetchDepositWithdrawFee': True, + 'fetchDepositWithdrawFees': True, + 'fetchFundingHistory': False, + 'fetchFundingRate': True, + 'fetchFundingRateHistory': True, + 'fetchFundingRates': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchL3OrderBook': True, + 'fetchLedger': True, + 'fetchLeverageTiers': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrice': True, + 'fetchMarkPrices': True, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrderBooks': False, + 'fetchOrdersByStatus': True, + 'fetchOrderTrades': True, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositionsHistory': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchStatus': True, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': True, + 'fetchTradingFees': False, + 'fetchTransactionFee': True, + 'fetchTransfers': False, + 'fetchWithdrawals': True, + 'repayCrossMargin': True, + 'repayIsolatedMargin': True, + 'setLeverage': True, + 'setMarginMode': False, + 'setPositionMode': False, + 'signIn': False, + 'transfer': True, + 'withdraw': True, + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/51840849/87295558-132aaf80-c50e-11ea-9801-a2fb0c57c799.jpg', + 'referral': 'https://www.kucoin.com/ucenter/signup?rcode=E5wkqe', + 'api': { + 'public': 'https://api.kucoin.com', + 'private': 'https://api.kucoin.com', + 'futuresPrivate': 'https://api-futures.kucoin.com', + 'futuresPublic': 'https://api-futures.kucoin.com', + 'webExchange': 'https://kucoin.com/_api', + 'broker': 'https://api-broker.kucoin.com', + 'earn': 'https://api.kucoin.com', + 'uta': 'https://api.kucoin.com', + }, + 'www': 'https://www.kucoin.com', + 'doc': [ + 'https://docs.kucoin.com', + ], + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + 'password': True, + }, + 'api': { + # level VIP0 + # Spot => 3000/30s => 100/s + # Weight = x => 100/(100/x) = x + # Futures Management Public => 2000/30s => 200/3/s + # Weight = x => 100/(200/3/x) = x*1.5 + 'public': { + 'get': { + # spot trading + 'currencies': 4.5, # 3PW + 'currencies/{currency}': 4.5, # 3PW + 'symbols': 6, # 4PW + 'market/orderbook/level1': 3, # 2PW + 'market/allTickers': 22.5, # 15PW + 'market/stats': 22.5, # 15PW + 'markets': 4.5, # 3PW + 'market/orderbook/level{level}_{limit}': 6, # 4PW + 'market/orderbook/level2_20': 3, # 2PW + 'market/orderbook/level2_100': 6, # 4PW + 'market/histories': 4.5, # 3PW + 'market/candles': 4.5, # 3PW + 'prices': 4.5, # 3PW + 'timestamp': 4.5, # 3PW + 'status': 4.5, # 3PW + # margin trading + 'mark-price/{symbol}/current': 3, # 2PW + 'mark-price/all-symbols': 3, + 'margin/config': 25, # 25SW + 'announcements': 20, # 20W + 'margin/collateralRatio': 10, + }, + 'post': { + # ws + 'bullet-public': 15, # 10PW + }, + }, + 'private': { + 'get': { + # account + 'user-info': 30, # 20MW + 'accounts': 7.5, # 5MW + 'accounts/{accountId}': 7.5, # 5MW + 'accounts/ledgers': 3, # 2MW + 'hf/accounts/ledgers': 2, # 2SW + 'hf/margin/account/ledgers': 2, # 2SW + 'transaction-history': 3, # 2MW + 'sub/user': 30, # 20MW + 'sub-accounts/{subUserId}': 22.5, # 15MW + 'sub-accounts': 30, # 20MW + 'sub/api-key': 30, # 20MW + # funding + 'margin/account': 40, # 40SW + 'margin/accounts': 15, # 15SW + 'isolated/accounts': 15, # 15SW + 'deposit-addresses': 7.5, # 5MW + 'deposits': 7.5, # 5MW + 'hist-deposits': 7.5, # 5MW + 'withdrawals': 30, # 20MW + 'hist-withdrawals': 30, # 20MW + 'withdrawals/quotas': 30, # 20MW + 'accounts/transferable': 30, # 20MW + 'transfer-list': 30, # 20MW + 'base-fee': 3, # 3SW + 'trade-fees': 3, # 3SW + # spot trading + 'market/orderbook/level{level}': 3, # 3SW + 'market/orderbook/level2': 3, # 3SW + 'market/orderbook/level3': 3, # 3SW + 'hf/accounts/opened': 2, # + 'hf/orders/active': 2, # 2SW + 'hf/orders/active/symbols': 2, # 2SW + 'hf/margin/order/active/symbols': 2, # 2SW + 'hf/orders/done': 2, # 2SW + 'hf/orders/{orderId}': 2, # 2SW + 'hf/orders/client-order/{clientOid}': 2, # 2SW + 'hf/orders/dead-cancel-all/query': 2, # 2SW + 'hf/fills': 2, # 2SW + 'orders': 2, # 2SW + 'limit/orders': 3, # 3SW + 'orders/{orderId}': 2, # 2SW + 'order/client-order/{clientOid}': 3, # 3SW + 'fills': 10, # 10SW + 'limit/fills': 20, # 20SW + 'stop-order': 8, # 8SW + 'stop-order/{orderId}': 3, # 3SW + 'stop-order/queryOrderByClientOid': 3, # 3SW + 'oco/order/{orderId}': 2, # 2SW + 'oco/order/details/{orderId}': 2, # 2SW + 'oco/client-order/{clientOid}': 2, # 2SW + 'oco/orders': 2, # 2SW + # margin trading + 'hf/margin/orders/active': 4, # 4SW + 'hf/margin/orders/done': 10, # 10SW + 'hf/margin/orders/{orderId}': 4, # 4SW + 'hf/margin/orders/client-order/{clientOid}': 5, # 5SW + 'hf/margin/fills': 5, # 5SW + 'etf/info': 25, # 25SW + 'margin/currencies': 20, # 20SW + 'risk/limit/strategy': 20, # 20SW(Deprecate) + 'isolated/symbols': 20, # 20SW + 'margin/symbols': 5, + 'isolated/account/{symbol}': 50, # 50SW + 'margin/borrow': 15, # 15SW + 'margin/repay': 15, # 15SW + 'margin/interest': 20, # 20SW + 'project/list': 10, # 10SW + 'project/marketInterestRate': 7.5, # 5PW + 'redeem/orders': 10, # 10SW + 'purchase/orders': 10, # 10SW + # broker + 'broker/api/rebase/download': 3, + 'broker/queryMyCommission': 3, + 'broker/queryUser': 3, + 'broker/queryDetailByUid': 3, + 'migrate/user/account/status': 3, + # affiliate + 'affiliate/inviter/statistics': 30, + }, + 'post': { + # account + 'sub/user/created': 22.5, # 15MW + 'sub/api-key': 30, # 20MW + 'sub/api-key/update': 45, # 30MW + # funding + 'deposit-addresses': 30, # 20MW + 'withdrawals': 7.5, # 5MW + 'accounts/universal-transfer': 6, # 4MW + 'accounts/sub-transfer': 45, # 30MW + 'accounts/inner-transfer': 15, # 10MW + 'transfer-out': 30, # 20MW + 'transfer-in': 30, # 20MW + # spot trading + 'hf/orders': 1, # 1SW + 'hf/orders/test': 1, # 1SW + 'hf/orders/sync': 1, # 1SW + 'hf/orders/multi': 1, # 1SW + 'hf/orders/multi/sync': 1, # 1SW + 'hf/orders/alter': 3, # 3SW + 'hf/orders/dead-cancel-all': 2, # 2SW + 'orders': 2, # 2SW + 'orders/test': 2, # 2SW + 'orders/multi': 3, # 3SW + 'stop-order': 2, # 2SW + 'oco/order': 2, # 2SW + # margin trading + 'hf/margin/order': 5, # 5SW + 'hf/margin/order/test': 5, # 5SW + 'margin/order': 5, # 5SW + 'margin/order/test': 5, # 5SW + 'margin/borrow': 15, # 15SW + 'margin/repay': 10, # 10SW + 'purchase': 15, # 15SW + 'redeem': 15, # 15SW + 'lend/purchase/update': 10, # 10SW + # ws + 'bullet-private': 10, # 10SW + 'position/update-user-leverage': 5, + 'deposit-address/create': 20, + }, + 'delete': { + # account + 'sub/api-key': 45, # 30MW + # funding + 'withdrawals/{withdrawalId}': 30, # 20MW + # spot trading + 'hf/orders/{orderId}': 1, # 1SW + 'hf/orders/sync/{orderId}': 1, # 1SW + 'hf/orders/client-order/{clientOid}': 1, # 1SW + 'hf/orders/sync/client-order/{clientOid}': 1, # 1SW + 'hf/orders/cancel/{orderId}': 2, # 2SW + 'hf/orders': 2, # 2SW + 'hf/orders/cancelAll': 30, # 30SW + 'orders/{orderId}': 3, # 3SW + 'order/client-order/{clientOid}': 5, # 5SW + 'orders': 20, # 20SW + 'stop-order/{orderId}': 3, # 3SW + 'stop-order/cancelOrderByClientOid': 5, # 5SW + 'stop-order/cancel': 3, # 3SW + 'oco/order/{orderId}': 3, # 3SW + 'oco/client-order/{clientOid}': 3, # 3SW + 'oco/orders': 3, # 3SW + # margin trading + 'hf/margin/orders/{orderId}': 5, # 5SW + 'hf/margin/orders/client-order/{clientOid}': 5, # 5SW + 'hf/margin/orders': 10, # 10SW + }, + }, + 'futuresPublic': { + 'get': { + 'contracts/active': 4.5, # 3PW + 'contracts/{symbol}': 4.5, # 3PW + 'ticker': 3, # 2PW + 'level2/snapshot': 4.5, # 3PW + 'level2/depth20': 7.5, # 5PW + 'level2/depth100': 15, # 10PW + 'trade/history': 7.5, # 5PW + 'kline/query': 4.5, # 3PW + 'interest/query': 7.5, # 5PW + 'index/query': 3, # 2PW + 'mark-price/{symbol}/current': 4.5, # 3PW + 'premium/query': 4.5, # 3PW + 'trade-statistics': 4.5, # 3PW + 'funding-rate/{symbol}/current': 3, # 2PW + 'contract/funding-rates': 7.5, # 5PW + 'timestamp': 3, # 2PW + 'status': 6, # 4PW + # ? + 'level2/message/query': 1.3953, + }, + 'post': { + # ws + 'bullet-public': 15, # 10PW + }, + }, + 'futuresPrivate': { + 'get': { + # account + 'transaction-history': 3, # 2MW + # funding + 'account-overview': 7.5, # 5FW + 'account-overview-all': 9, # 6FW + 'transfer-list': 30, # 20MW + # futures + 'orders': 3, # 2FW + 'stopOrders': 9, # 6FW + 'recentDoneOrders': 7.5, # 5FW + 'orders/{orderId}': 7.5, # 5FW + 'orders/byClientOid': 7.5, # 5FW + 'fills': 7.5, # 5FW + 'recentFills': 4.5, # 3FW + 'openOrderStatistics': 15, # 10FW + 'position': 3, # 2FW + 'positions': 3, # 2FW + 'margin/maxWithdrawMargin': 15, # 10FW + 'contracts/risk-limit/{symbol}': 7.5, # 5FW + 'funding-history': 7.5, # 5FW + 'copy-trade/futures/get-max-open-size': 6, # 4FW + 'copy-trade/futures/position/margin/max-withdraw-margin': 15, # 10FW + }, + 'post': { + # funding + 'transfer-out': 30, # 20MW + 'transfer-in': 30, # 20MW + # futures + 'orders': 3, # 2FW + 'orders/test': 3, # 2FW + 'orders/multi': 4.5, # 3FW + 'position/margin/auto-deposit-status': 6, # 4FW + 'margin/withdrawMargin': 15, # 10FW + 'position/margin/deposit-margin': 6, # 4FW + 'position/risk-limit-level/change': 6, # 4FW + 'copy-trade/futures/orders': 3, # 2FW + 'copy-trade/futures/orders/test': 3, # 2FW + 'copy-trade/futures/st-orders': 3, # 2FW + 'copy-trade/futures/position/margin/deposit-margin': 6, # 4FW + 'copy-trade/futures/position/margin/withdraw-margin': 15, # 10FW + 'copy-trade/futures/position/risk-limit-level/change': 3, # 2FW + 'copy-trade/futures/position/margin/auto-deposit-status': 6, # 4FW + 'copy-trade/futures/position/changeMarginMode': 3, # 2FW + 'copy-trade/futures/position/changeCrossUserLeverage': 3, # 2FW + 'copy-trade/getCrossModeMarginRequirement': 4.5, # 3FW + 'copy-trade/position/switchPositionMode': 3, # 2FW + # ws + 'bullet-private': 15, # 10FW + }, + 'delete': { + 'orders/{orderId}': 1.5, # 1FW + 'orders/client-order/{clientOid}': 1.5, # 1FW + 'orders': 45, # 30FW + 'stopOrders': 22.5, # 15FW + 'copy-trade/futures/orders': 1.5, # 1FW + 'copy-trade/futures/orders/client-order': 1.5, # 1FW + }, + }, + 'webExchange': { + 'get': { + 'currency/currency/chain-info': 1, # self is temporary from webApi + }, + }, + 'broker': { + 'get': { + 'broker/nd/info': 2, + 'broker/nd/account': 2, + 'broker/nd/account/apikey': 2, + 'broker/nd/rebase/download': 3, + 'asset/ndbroker/deposit/list': 1, + 'broker/nd/transfer/detail': 1, + 'broker/nd/deposit/detail': 1, + 'broker/nd/withdraw/detail': 1, + }, + 'post': { + 'broker/nd/transfer': 1, + 'broker/nd/account': 3, + 'broker/nd/account/apikey': 3, + 'broker/nd/account/update-apikey': 3, + }, + 'delete': { + 'broker/nd/account/apikey': 3, + }, + }, + 'earn': { + 'get': { + 'otc-loan/loan': 1, + 'otc-loan/accounts': 1, + 'earn/redeem-preview': 7.5, # 5EW + 'earn/saving/products': 7.5, # 5EW + 'earn/hold-assets': 7.5, # 5EW + 'earn/promotion/products': 7.5, # 5EW + 'earn/kcs-staking/products': 7.5, # 5EW + 'earn/staking/products': 7.5, # 5EW + 'earn/eth-staking/products': 7.5, # 5EW + }, + 'post': { + 'earn/orders': 7.5, # 5EW + }, + 'delete': { + 'earn/orders': 7.5, # 5EW + }, + }, + 'uta': { + 'get': { + 'market/announcement': 20, + 'market/currency': 3, + 'market/instrument': 4, + 'market/ticker': 15, + 'market/orderbook': 3, + 'market/trade': 3, + 'market/kline': 3, + 'market/funding-rate': 2, + 'market/funding-rate-history': 5, + 'market/cross-config': 25, + 'server/status': 3, + }, + }, + }, + 'timeframes': { + '1m': '1min', + '3m': '3min', + '5m': '5min', + '15m': '15min', + '30m': '30min', + '1h': '1hour', + '2h': '2hour', + '4h': '4hour', + '6h': '6hour', + '8h': '8hour', + '12h': '12hour', + '1d': '1day', + '1w': '1week', + '1M': '1month', + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'exact': { + 'Order not exist or not allow to be cancelled': OrderNotFound, + 'The order does not exist.': OrderNotFound, + 'order not exist': OrderNotFound, + 'order not exist.': OrderNotFound, # duplicated error temporarily + 'order_not_exist': OrderNotFound, # {"code":"order_not_exist","msg":"order_not_exist"} ¯\_(ツ)_/¯ + 'order_not_exist_or_not_allow_to_cancel': InvalidOrder, # {"code":"400100","msg":"order_not_exist_or_not_allow_to_cancel"} + 'Order size below the minimum requirement.': InvalidOrder, # {"code":"400100","msg":"Order size below the minimum requirement."} + 'Order size increment invalid.': InvalidOrder, # {"msg":"Order size increment invalid.","code":"600100"} + 'The withdrawal amount is below the minimum requirement.': ExchangeError, # {"code":"400100","msg":"The withdrawal amount is below the minimum requirement."} + 'Unsuccessful! Exceeded the max. funds out-transfer limit': InsufficientFunds, # {"code":"200000","msg":"Unsuccessful! Exceeded the max. funds out-transfer limit"} + 'The amount increment is invalid.': BadRequest, + 'The quantity is below the minimum requirement.': InvalidOrder, # {"msg":"The quantity is below the minimum requirement.","code":"400100"} + '400': BadRequest, + '401': AuthenticationError, + '403': NotSupported, + '404': NotSupported, + '405': NotSupported, + '415': NotSupported, + '429': RateLimitExceeded, + '500': ExchangeNotAvailable, # Internal Server Error -- We had a problem with our server. Try again later. + '503': ExchangeNotAvailable, + '101030': PermissionDenied, # {"code":"101030","msg":"You haven't yet enabled the margin trading"} + '103000': InvalidOrder, # {"code":"103000","msg":"Exceed the borrowing limit, the remaining borrowable amount is: 0USDT"} + '130101': BadRequest, # Parameter error + '130102': ExchangeError, # Maximum subscription amount has been exceeded. + '130103': OrderNotFound, # Subscription order does not exist. + '130104': ExchangeError, # Maximum number of subscription orders has been exceeded. + '130105': InsufficientFunds, # Insufficient balance. + '130106': NotSupported, # The currency does not support redemption. + '130107': ExchangeError, # Redemption amount exceeds subscription amount. + '130108': OrderNotFound, # Redemption order does not exist. + '130201': PermissionDenied, # Your account has restricted access to certain features. Please contact customer service for further assistance + '130202': ExchangeError, # The system is renewing the loan automatically. Please try again later + '130203': InsufficientFunds, # Insufficient account balance + '130204': BadRequest, # As the total lending amount for platform leverage reaches the platform's maximum position limit, the system suspends the borrowing function of leverage + '130301': InsufficientFunds, # Insufficient account balance + '130302': PermissionDenied, # Your relevant permission rights have been restricted, you can contact customer service for processing + '130303': NotSupported, # The current trading pair does not support isolated positions + '130304': NotSupported, # The trading function of the current trading pair is not enabled + '130305': NotSupported, # The current trading pair does not support cross position + '130306': NotSupported, # The account has not opened leveraged trading + '130307': NotSupported, # Please reopen the leverage agreement + '130308': InvalidOrder, # Position renewal freeze + '130309': InvalidOrder, # Position forced liquidation freeze + '130310': ExchangeError, # Abnormal leverage account status + '130311': InvalidOrder, # Failed to place an order, triggering buy limit + '130312': InvalidOrder, # Trigger global position limit, suspend buying + '130313': InvalidOrder, # Trigger global position limit, suspend selling + '130314': InvalidOrder, # Trigger the global position limit and prompt the remaining quantity available for purchase + '130315': NotSupported, # This feature has been suspended due to country restrictions + '126000': ExchangeError, # Abnormal margin trading + '126001': NotSupported, # Users currently do not support high frequency + '126002': ExchangeError, # There is a risk problem in your account and transactions are temporarily not allowed! + '126003': InvalidOrder, # The commission amount is less than the minimum transaction amount for a single commission + '126004': ExchangeError, # Trading pair does not exist or is prohibited + '126005': PermissionDenied, # This trading pair requires advanced KYC certification before trading + '126006': ExchangeError, # Trading pair is not available + '126007': ExchangeError, # Trading pair suspended + '126009': ExchangeError, # Trading pair is suspended from creating orders + '126010': ExchangeError, # Trading pair suspended order cancellation + '126011': ExchangeError, # There are too many orders in the order + '126013': InsufficientFunds, # Insufficient account balance + '126015': ExchangeError, # It is prohibited to place orders on self trading pair + '126021': NotSupported, # This digital asset does not support user participation in your region, thank you for your understanding! + '126022': InvalidOrder, # The final transaction price of your order will trigger the price protection strategy. To protect the price from deviating too much, please place an order again. + '126027': InvalidOrder, # Only limit orders are supported + '126028': InvalidOrder, # Only limit orders are supported before the specified time + '126029': InvalidOrder, # The maximum order price is: xxx + '126030': InvalidOrder, # The minimum order price is: xxx + '126033': InvalidOrder, # Duplicate order + '126034': InvalidOrder, # Failed to create take profit and stop loss order + '126036': InvalidOrder, # Failed to create margin order + '126037': ExchangeError, # Due to country and region restrictions, self function has been suspended! + '126038': ExchangeError, # Third-party service call failed(internal exception) + '126039': ExchangeError, # Third-party service call failed, reason: xxx + '126041': ExchangeError, # clientTimestamp parameter error + '126042': ExchangeError, # Exceeded maximum position limit + '126043': OrderNotFound, # Order does not exist + '126044': InvalidOrder, # clientOid duplicate + '126045': NotSupported, # This digital asset does not support user participation in your region, thank you for your understanding! + '126046': NotSupported, # This digital asset does not support your IP region, thank you for your understanding! + '126047': PermissionDenied, # Please complete identity verification + '126048': PermissionDenied, # Please complete authentication for the master account + '135005': ExchangeError, # Margin order query business abnormality + '135018': ExchangeError, # Margin order query service abnormality + '200004': InsufficientFunds, + '210014': InvalidOrder, # {"code":"210014","msg":"Exceeds the max. borrowing amount, the remaining amount you can borrow: 0USDT"} + '210021': InsufficientFunds, # {"code":"210021","msg":"Balance not enough"} + '230003': InsufficientFunds, # {"code":"230003","msg":"Balance insufficient!"} + '260000': InvalidAddress, # {"code":"260000","msg":"Deposit address already exists."} + '260100': InsufficientFunds, # {"code":"260100","msg":"account.noBalance"} + '300000': InvalidOrder, + '400000': BadSymbol, + '400001': AuthenticationError, + '400002': InvalidNonce, + '400003': AuthenticationError, + '400004': AuthenticationError, + '400005': AuthenticationError, + '400006': AuthenticationError, + '400007': AuthenticationError, + '400008': NotSupported, + '400100': InsufficientFunds, # {"msg":"account.available.amount","code":"400100"} or {"msg":"Withdrawal amount is below the minimum requirement.","code":"400100"} + '400200': InvalidOrder, # {"code":"400200","msg":"Forbidden to place an order"} + '400330': InvalidOrder, # {"msg":"Order price can't deviate from NAV by 50%","code":"400330"} + '400350': InvalidOrder, # {"code":"400350","msg":"Upper limit for holding: 10,000USDT, you can still buy 10,000USDT worth of coin."} + '400370': InvalidOrder, # {"code":"400370","msg":"Max. price: 0.02500000000000000000"} + '400400': BadRequest, # Parameter error + '400401': AuthenticationError, # User is not logged in + '400500': InvalidOrder, # {"code":"400500","msg":"Your located country/region is currently not supported for the trading of self token"} + '400600': BadSymbol, # {"code":"400600","msg":"validation.createOrder.symbolNotAvailable"} + '400760': InvalidOrder, # {"code":"400760","msg":"order price should be more than XX"} + '401000': BadRequest, # {"code":"401000","msg":"The interface has been deprecated"} + '408000': BadRequest, # Network timeout, please try again later + '411100': AccountSuspended, + '415000': BadRequest, # {"code":"415000","msg":"Unsupported Media Type"} + '400303': PermissionDenied, # {"msg":"To enjoy the full range of our products and services, we kindly request you complete the identity verification process.","code":"400303"} + '500000': ExchangeNotAvailable, # {"code":"500000","msg":"Internal Server Error"} + '260220': InvalidAddress, # {"code": "260220", "msg": "deposit.address.not.exists"} + '600100': InsufficientFunds, # {"msg":"Funds below the minimum requirement.","code":"600100"} + '600101': InvalidOrder, # {"msg":"The order funds should more then 0.1 USDT.","code":"600101"} + '900014': BadRequest, # {"code":"900014","msg":"Invalid chainId"} + }, + 'broad': { + 'Exceeded the access frequency': RateLimitExceeded, + 'require more permission': PermissionDenied, + }, + }, + 'fees': { + 'trading': { + 'tierBased': True, + 'percentage': True, + 'taker': self.parse_number('0.001'), + 'maker': self.parse_number('0.001'), + 'tiers': { + 'taker': [ + [self.parse_number('0'), self.parse_number('0.001')], + [self.parse_number('50'), self.parse_number('0.001')], + [self.parse_number('200'), self.parse_number('0.0009')], + [self.parse_number('500'), self.parse_number('0.0008')], + [self.parse_number('1000'), self.parse_number('0.0007')], + [self.parse_number('2000'), self.parse_number('0.0007')], + [self.parse_number('4000'), self.parse_number('0.0006')], + [self.parse_number('8000'), self.parse_number('0.0005')], + [self.parse_number('15000'), self.parse_number('0.00045')], + [self.parse_number('25000'), self.parse_number('0.0004')], + [self.parse_number('40000'), self.parse_number('0.00035')], + [self.parse_number('60000'), self.parse_number('0.0003')], + [self.parse_number('80000'), self.parse_number('0.00025')], + ], + 'maker': [ + [self.parse_number('0'), self.parse_number('0.001')], + [self.parse_number('50'), self.parse_number('0.0009')], + [self.parse_number('200'), self.parse_number('0.0007')], + [self.parse_number('500'), self.parse_number('0.0005')], + [self.parse_number('1000'), self.parse_number('0.0003')], + [self.parse_number('2000'), self.parse_number('0')], + [self.parse_number('4000'), self.parse_number('0')], + [self.parse_number('8000'), self.parse_number('0')], + [self.parse_number('15000'), self.parse_number('-0.00005')], + [self.parse_number('25000'), self.parse_number('-0.00005')], + [self.parse_number('40000'), self.parse_number('-0.00005')], + [self.parse_number('60000'), self.parse_number('-0.00005')], + [self.parse_number('80000'), self.parse_number('-0.00005')], + ], + }, + }, + 'funding': { + 'tierBased': False, + 'percentage': False, + 'withdraw': {}, + 'deposit': {}, + }, + }, + 'commonCurrencies': { + 'BIFI': 'BIFIF', + 'VAI': 'VAIOT', + 'WAX': 'WAXP', + 'ALT': 'APTOSLAUNCHTOKEN', + 'KALT': 'ALT', # ALTLAYER + 'FUD': 'FTX Users\' Debt', + }, + 'options': { + 'hf': None, # would be auto set to `true/false` after first load + 'version': 'v1', + 'symbolSeparator': '-', + 'fetchMyTradesMethod': 'private_get_fills', + 'timeDifference': 0, # the difference between system clock and Binance clock + 'adjustForTimeDifference': False, # controls the adjustment logic upon instantiation + 'fetchCurrencies': { + 'webApiEnable': True, # fetches from WEB + 'webApiRetries': 1, + 'webApiMuteFailure': True, + }, + 'fetchMarkets': { + 'fetchTickersFees': True, + }, + 'withdraw': { + 'includeFee': False, + }, + # endpoint versions + 'versions': { + 'public': { + 'GET': { + # spot trading + 'currencies': 'v3', + 'currencies/{currency}': 'v3', + 'symbols': 'v2', + 'mark-price/all-symbols': 'v3', + 'announcements': 'v3', + }, + }, + 'private': { + 'GET': { + # account + 'user-info': 'v2', + 'hf/margin/account/ledgers': 'v3', + 'sub/user': 'v2', + 'sub-accounts': 'v2', + # funding + 'margin/accounts': 'v3', + 'isolated/accounts': 'v3', + # 'deposit-addresses': 'v2', + 'deposit-addresses': 'v1', # 'v1' for fetchDepositAddress, 'v2' for fetchDepositAddressesByNetwork + # spot trading + 'market/orderbook/level2': 'v3', + 'market/orderbook/level3': 'v3', + 'market/orderbook/level{level}': 'v3', + 'oco/order/{orderId}': 'v3', + 'oco/order/details/{orderId}': 'v3', + 'oco/client-order/{clientOid}': 'v3', + 'oco/orders': 'v3', + # margin trading + 'hf/margin/orders/active': 'v3', + 'hf/margin/order/active/symbols': 'v3', + 'hf/margin/orders/done': 'v3', + 'hf/margin/orders/{orderId}': 'v3', + 'hf/margin/orders/client-order/{clientOid}': 'v3', + 'hf/margin/fills': 'v3', + 'etf/info': 'v3', + 'margin/currencies': 'v3', + 'margin/borrow': 'v3', + 'margin/repay': 'v3', + 'margin/interest': 'v3', + 'project/list': 'v3', + 'project/marketInterestRate': 'v3', + 'redeem/orders': 'v3', + 'purchase/orders': 'v3', + 'migrate/user/account/status': 'v3', + 'margin/symbols': 'v3', + 'affiliate/inviter/statistics': 'v2', + 'asset/ndbroker/deposit/list': 'v1', + }, + 'POST': { + # account + 'sub/user/created': 'v2', + # funding + 'accounts/universal-transfer': 'v3', + 'accounts/sub-transfer': 'v2', + 'accounts/inner-transfer': 'v2', + 'transfer-out': 'v3', + 'deposit-address/create': 'v3', + # spot trading + 'oco/order': 'v3', + # margin trading + 'hf/margin/order': 'v3', + 'hf/margin/order/test': 'v3', + 'margin/borrow': 'v3', + 'margin/repay': 'v3', + 'purchase': 'v3', + 'redeem': 'v3', + 'lend/purchase/update': 'v3', + 'position/update-user-leverage': 'v3', + 'withdrawals': 'v3', + }, + 'DELETE': { + # account + # funding + # spot trading + 'hf/margin/orders/{orderId}': 'v3', + 'hf/margin/orders/client-order/{clientOid}': 'v3', + 'hf/margin/orders': 'v3', + 'oco/order/{orderId}': 'v3', + 'oco/client-order/{clientOid}': 'v3', + 'oco/orders': 'v3', + # margin trading + }, + }, + 'futuresPrivate': { + 'POST': { + 'transfer-out': 'v3', + }, + }, + }, + 'partner': { + # the support for spot and future exchanges settings + 'spot': { + 'id': 'ccxt', + 'key': '9e58cc35-5b5e-4133-92ec-166e3f077cb8', + }, + 'future': { + 'id': 'ccxtfutures', + 'key': '1b327198-f30c-4f14-a0ac-918871282f15', + }, + # exchange-wide settings are also supported + # 'id': 'ccxt' + # 'key': '9e58cc35-5b5e-4133-92ec-166e3f077cb8', + }, + 'accountsByType': { + 'spot': 'trade', + 'margin': 'margin', + 'cross': 'margin', + 'isolated': 'isolated', + 'main': 'main', + 'funding': 'main', + 'future': 'contract', + 'swap': 'contract', + 'mining': 'pool', + 'hf': 'trade_hf', + }, + 'networks': { + 'BRC20': 'btc', + 'BTCNATIVESEGWIT': 'bech32', + 'ERC20': 'eth', + 'TRC20': 'trx', + 'HRC20': 'heco', + 'MATIC': 'matic', + 'KCC': 'kcc', # kucoin community chain + 'SOL': 'sol', + 'ALGO': 'algo', + 'EOS': 'eos', + 'BEP20': 'bsc', + 'BEP2': 'bnb', + 'ARBONE': 'arbitrum', + 'AVAXX': 'avax', + 'AVAXC': 'avaxc', + 'TLOS': 'tlos', # tlosevm is different + 'CFX': 'cfx', + 'ACA': 'aca', + 'OPTIMISM': 'optimism', + 'ONT': 'ont', + 'GLMR': 'glmr', + 'CSPR': 'cspr', + 'KLAY': 'klay', + 'XRD': 'xrd', + 'RVN': 'rvn', + 'NEAR': 'near', + 'APT': 'aptos', + 'ETHW': 'ethw', + 'TON': 'ton', + 'BCH': 'bch', + 'BSV': 'bchsv', + 'BCHA': 'bchabc', + 'OSMO': 'osmo', + 'NANO': 'nano', + 'XLM': 'xlm', + 'VET': 'vet', + 'IOST': 'iost', + 'ZIL': 'zil', + 'XRP': 'xrp', + 'TOMO': 'tomo', + 'XMR': 'xmr', + 'COTI': 'coti', + 'XTZ': 'xtz', + 'ADA': 'ada', + 'WAX': 'waxp', + 'THETA': 'theta', + 'ONE': 'one', + 'IOTEX': 'iotx', + 'NULS': 'nuls', + 'KSM': 'ksm', + 'LTC': 'ltc', + 'WAVES': 'waves', + 'DOT': 'dot', + 'STEEM': 'steem', + 'QTUM': 'qtum', + 'DOGE': 'doge', + 'FIL': 'fil', + 'XYM': 'xym', + 'FLUX': 'flux', + 'ATOM': 'atom', + 'XDC': 'xdc', + 'KDA': 'kda', + 'ICP': 'icp', + 'CELO': 'celo', + 'LSK': 'lsk', + 'VSYS': 'vsys', + 'KAR': 'kar', + 'XCH': 'xch', + 'FLOW': 'flow', + 'BAND': 'band', + 'EGLD': 'egld', + 'HBAR': 'hbar', + 'XPR': 'xpr', + 'AR': 'ar', + 'FTM': 'ftm', + 'KAVA': 'kava', + 'KMA': 'kma', + 'XEC': 'xec', + 'IOTA': 'iota', + 'HNT': 'hnt', + 'ASTR': 'astr', + 'PDEX': 'pdex', + 'METIS': 'metis', + 'ZEC': 'zec', + 'POKT': 'pokt', + 'OASYS': 'oas', + 'OASIS': 'oasis', # a.k.a. ROSE + 'ETC': 'etc', + 'AKT': 'akt', + 'FSN': 'fsn', + 'SCRT': 'scrt', + 'CFG': 'cfg', + 'ICX': 'icx', + 'KMD': 'kmd', + 'NEM': 'NEM', + 'STX': 'stx', + 'DGB': 'dgb', + 'DCR': 'dcr', + 'CKB': 'ckb', # ckb2 is just odd entry + 'ELA': 'ela', # esc might be another chain elastos smart chain + 'HYDRA': 'hydra', + 'BTM': 'btm', + 'KARDIA': 'kai', + 'SXP': 'sxp', # a.k.a. solar swipe + 'NEBL': 'nebl', + 'ZEN': 'zen', + 'SDN': 'sdn', + 'LTO': 'lto', + 'WEMIX': 'wemix', + # 'BOBA': 'boba', # tbd + 'EVER': 'ever', + 'BNC': 'bnc', + 'BNCDOT': 'bncdot', + # 'CMP': 'cmp', # todo: after consensus + 'AION': 'aion', + 'GRIN': 'grin', + 'LOKI': 'loki', + 'QKC': 'qkc', + 'TT': 'TT', + 'PIVX': 'pivx', + 'SERO': 'sero', + 'METER': 'meter', + 'STATEMINE': 'statemine', # a.k.a. RMRK + 'DVPN': 'dvpn', + 'XPRT': 'xprt', + 'MOVR': 'movr', + 'ERGO': 'ergo', + 'ABBC': 'abbc', + 'DIVI': 'divi', + 'PURA': 'pura', + 'DFI': 'dfi', + # 'NEO': 'neo', # tbd neo legacy + 'NEON3': 'neon3', + 'DOCK': 'dock', + 'TRUE': 'true', + 'CS': 'cs', + 'ORAI': 'orai', + 'BASE': 'base', + 'TARA': 'tara', + # below will be uncommented after consensus + # 'BITCOINDIAMON': 'bcd', + # 'BITCOINGOLD': 'btg', + # 'HTR': 'htr', + # 'DEROHE': 'derohe', + # 'NDAU': 'ndau', + # 'HPB': 'hpb', + # 'AXE': 'axe', + # 'BITCOINPRIVATE': 'btcp', + # 'EDGEWARE': 'edg', + # 'JUPITER': 'jup', + # 'VELAS': 'vlx', # vlxevm is different + # # 'terra' luna lunc TBD + # 'DIGITALBITS': 'xdb', + # # fra is fra-emv on kucoin + # 'PASTEL': 'psl', + # # sysevm + # 'CONCORDIUM': 'ccd', + # 'AURORA': 'aurora', + # 'PHA': 'pha', # a.k.a. khala + # 'PAL': 'pal', + # 'RSK': 'rbtc', + # 'NIX': 'nix', + # 'NIM': 'nim', + # 'NRG': 'nrg', + # 'RFOX': 'rfox', + # 'PIONEER': 'neer', + # 'PIXIE': 'pix', + # 'ALEPHZERO': 'azero', + # 'ACHAIN': 'act', # actevm is different + # 'BOSCOIN': 'bos', + # 'ELECTRONEUM': 'etn', + # 'GOCHAIN': 'go', + # 'SOPHIATX': 'sphtx', + # 'WANCHAIN': 'wan', + # 'ZEEPIN': 'zpt', + # 'MATRIXAI': 'man', + # 'METADIUM': 'meta', + # 'METAHASH': 'mhc', + # # eosc --"eosforce" tbd + # 'IOTCHAIN': 'itc', + # 'CONTENTOS': 'cos', + # 'CPCHAIN': 'cpc', + # 'INTCHAIN': 'int', + # # 'DASH': 'dash', tbd digita-cash + # 'WALTONCHAIN': 'wtc', + # 'CONSTELLATION': 'dag', + # 'ONELEDGER': 'olt', + # 'AIRDAO': 'amb', # a.k.a. AMBROSUS + # 'ENERGYWEB': 'ewt', + # 'WAVESENTERPRISE': 'west', + # 'HYPERCASH': 'hc', + # 'ENECUUM': 'enq', + # 'HAVEN': 'xhv', + # 'CHAINX': 'pcx', + # # 'FLUXOLD': 'zel', # zel seems old chain(with uppercase FLUX in kucoin UI and with id 'zel') + # 'BUMO': 'bu', + # 'DEEPONION': 'onion', + # 'ULORD': 'ut', + # 'ASCH': 'xas', + # 'SOLARIS': 'xlr', + # 'APOLLO': 'apl', + # 'PIRATECHAIN': 'arrr', + # 'ULTRA': 'uos', + # 'EMONEY': 'ngm', + # 'AURORACHAIN': 'aoa', + # 'KLEVER': 'klv', + # undetermined: xns(insolar), rhoc, luk(luniverse), kts(klimatas), bchn(bitcoin cash node), god(shallow entry), lit(litmus), + }, + 'marginModes': { + 'cross': 'MARGIN_TRADE', + 'isolated': 'MARGIN_ISOLATED_TRADE', + 'spot': 'TRADE', + }, + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': True, + 'triggerPrice': True, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': True, + 'takeProfitPrice': True, + 'attachedStopLossTakeProfit': None, # not supported + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': True, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': True, # todo implement + 'iceberg': True, # todo implement + }, + 'createOrders': { + 'max': 5, + }, + 'fetchMyTrades': { + 'marginMode': True, + 'limit': None, + 'daysBack': None, + 'untilDays': 7, # per implementation comments + 'symbolRequired': True, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': True, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOpenOrders': { + 'marginMode': True, + 'limit': 500, + 'trigger': True, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOrders': None, + 'fetchClosedOrders': { + 'marginMode': True, + 'limit': 500, + 'daysBack': None, + 'daysBackCanceled': None, + 'untilDays': 7, + 'trigger': True, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOHLCV': { + 'limit': 1500, + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + }) + + def nonce(self): + return self.milliseconds() - self.options['timeDifference'] + + async def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://docs.kucoin.com/#server-time + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = await self.publicGetTimestamp(params) + # + # { + # "code":"200000", + # "msg":"success", + # "data":1546837113087 + # } + # + return self.safe_integer(response, 'data') + + async def fetch_status(self, params={}): + """ + the latest known information on the availability of the exchange API + + https://docs.kucoin.com/#service-status + https://www.kucoin.com/docs-new/rest/ua/get-service-status + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :param str [params.tradeType]: *uta only* set to SPOT or FUTURES + :returns dict: a `status structure ` + """ + uta = None + uta, params = self.handle_option_and_params(params, 'fetchStatus', 'uta', False) + response = None + if uta: + defaultType = self.safe_string(self.options, 'defaultType', 'spot') + defaultTradeType = 'SPOT' if (defaultType == 'spot') else 'FUTURES' + tradeType = self.safe_string_upper(params, 'tradeType', defaultTradeType) + request = { + 'tradeType': tradeType, + } + response = await self.utaGetServerStatus(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "tradeType": "SPOT", + # "serverStatus": "open", + # "msg": "" + # } + # } + # + else: + response = await self.publicGetStatus(params) + # + # { + # "code":"200000", + # "data":{ + # "status":"open", #open, close, cancelonly + # "msg":"upgrade match engine" #remark for operation + # } + # } + # + data = self.safe_dict(response, 'data', {}) + status = self.safe_string_2(data, 'status', 'serverStatus') + return { + 'status': 'ok' if (status == 'open') else 'maintenance', + 'updated': None, + 'eta': None, + 'url': None, + 'info': response, + } + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for kucoin + + https://docs.kucoin.com/#get-symbols-list-deprecated + https://docs.kucoin.com/#get-all-tickers + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :returns dict[]: an array of objects representing market data + """ + fetchTickersFees = None + fetchTickersFees, params = self.handle_option_and_params(params, 'fetchMarkets', 'fetchTickersFees', True) + uta = None + uta, params = self.handle_option_and_params(params, 'fetchMarkets', 'uta', False) + if uta: + return await self.fetch_uta_markets(params) + promises = [] + promises.append(self.publicGetSymbols(params)) + # + # { + # "code": "200000", + # "data": [ + # { + # "symbol": "XLM-USDT", + # "name": "XLM-USDT", + # "baseCurrency": "XLM", + # "quoteCurrency": "USDT", + # "feeCurrency": "USDT", + # "market": "USDS", + # "baseMinSize": "0.1", + # "quoteMinSize": "0.01", + # "baseMaxSize": "10000000000", + # "quoteMaxSize": "99999999", + # "baseIncrement": "0.0001", + # "quoteIncrement": "0.000001", + # "priceIncrement": "0.000001", + # "priceLimitRate": "0.1", + # "isMarginEnabled": True, + # "enableTrading": True + # }, + # + credentialsSet = self.check_required_credentials(False) + requestMarginables = credentialsSet and self.safe_bool(params, 'marginables', True) + if requestMarginables: + promises.append(self.privateGetMarginSymbols(params)) # cross margin symbols + # + # { + # "code": "200000", + # "data": { + # "timestamp": 1719393213421, + # "items": [ + # { + # # same object market, with one additional field: + # "minFunds": "0.1" + # }, + # + promises.append(self.privateGetIsolatedSymbols(params)) # isolated margin symbols + # + # { + # "code": "200000", + # "data": [ + # { + # "symbol": "NKN-USDT", + # "symbolName": "NKN-USDT", + # "baseCurrency": "NKN", + # "quoteCurrency": "USDT", + # "maxLeverage": 5, + # "flDebtRatio": "0.97", + # "tradeEnable": True, + # "autoRenewMaxDebtRatio": "0.96", + # "baseBorrowEnable": True, + # "quoteBorrowEnable": True, + # "baseTransferInEnable": True, + # "quoteTransferInEnable": True, + # "baseBorrowCoefficient": "1", + # "quoteBorrowCoefficient": "1" + # }, + # + if fetchTickersFees: + promises.append(self.publicGetMarketAllTickers(params)) + # + # { + # "code": "200000", + # "data": { + # "time":1602832092060, + # "ticker":[ + # { + # "symbol": "BTC-USDT", # symbol + # "symbolName":"BTC-USDT", # Name of trading pairs, it would change after renaming + # "buy": "11328.9", # bestAsk + # "sell": "11329", # bestBid + # "changeRate": "-0.0055", # 24h change rate + # "changePrice": "-63.6", # 24h change price + # "high": "11610", # 24h highest price + # "low": "11200", # 24h lowest price + # "vol": "2282.70993217", # 24h volume,the aggregated trading volume in BTC + # "volValue": "25984946.157790431", # 24h total, the trading volume in quote currency of last 24 hours + # "last": "11328.9", # last price + # "averagePrice": "11360.66065903", # 24h average transaction price yesterday + # "takerFeeRate": "0.001", # Basic Taker Fee + # "makerFeeRate": "0.001", # Basic Maker Fee + # "takerCoefficient": "1", # Taker Fee Coefficient + # "makerCoefficient": "1" # Maker Fee Coefficient + # } + # + if credentialsSet: + # load migration status for account + promises.append(self.load_migration_status()) + responses = await asyncio.gather(*promises) + symbolsData = self.safe_list(responses[0], 'data') + crossData = self.safe_dict(responses[1], 'data', {}) if requestMarginables else {} + crossItems = self.safe_list(crossData, 'items', []) + crossById = self.index_by(crossItems, 'symbol') + isolatedData = responses[2] if requestMarginables else {} + isolatedItems = self.safe_list(isolatedData, 'data', []) + isolatedById = self.index_by(isolatedItems, 'symbol') + tickersIdx = 3 if requestMarginables else 1 + tickersResponse = self.safe_dict(responses, tickersIdx, {}) + tickerItems = self.safe_list(self.safe_dict(tickersResponse, 'data', {}), 'ticker', []) + tickersById = self.index_by(tickerItems, 'symbol') + result = [] + for i in range(0, len(symbolsData)): + market = symbolsData[i] + id = self.safe_string(market, 'symbol') + baseId, quoteId = id.split('-') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + # quoteIncrement = self.safe_number(market, 'quoteIncrement') + ticker = self.safe_dict(tickersById, id, {}) + makerFeeRate = self.safe_string(ticker, 'makerFeeRate') + takerFeeRate = self.safe_string(ticker, 'takerFeeRate') + makerCoefficient = self.safe_string(ticker, 'makerCoefficient') + takerCoefficient = self.safe_string(ticker, 'takerCoefficient') + hasCrossMargin = (id in crossById) + hasIsolatedMargin = (id in isolatedById) + isMarginable = self.safe_bool(market, 'isMarginEnabled', False) or hasCrossMargin or hasIsolatedMargin + result.append({ + 'id': id, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': isMarginable, + 'marginModes': { + 'cross': hasCrossMargin, + 'isolated': hasIsolatedMargin, + }, + 'swap': False, + 'future': False, + 'option': False, + 'active': self.safe_bool(market, 'enableTrading'), + 'contract': False, + 'linear': None, + 'inverse': None, + 'taker': self.parse_number(Precise.string_mul(takerFeeRate, takerCoefficient)), + 'maker': self.parse_number(Precise.string_mul(makerFeeRate, makerCoefficient)), + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.safe_number(market, 'baseIncrement'), + 'price': self.safe_number(market, 'priceIncrement'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(market, 'baseMinSize'), + 'max': self.safe_number(market, 'baseMaxSize'), + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': self.safe_number(market, 'quoteMinSize'), + 'max': self.safe_number(market, 'quoteMaxSize'), + }, + }, + 'created': None, + 'info': market, + }) + if self.options['adjustForTimeDifference']: + await self.load_time_difference() + return result + + async def fetch_uta_markets(self, params={}) -> List[Market]: + promises = [] + promises.append(self.utaGetMarketInstrument(self.extend(params, {'tradeType': 'SPOT'}))) + # + # { + # "code": "200000", + # "data": { + # "tradeType": "SPOT", + # "list": [ + # { + # "symbol": "AVA-USDT", + # "name": "AVA-USDT", + # "baseCurrency": "AVA", + # "quoteCurrency": "USDT", + # "market": "USDS", + # "minBaseOrderSize": "0.1", + # "minQuoteOrderSize": "0.1", + # "maxBaseOrderSize": "10000000000", + # "maxQuoteOrderSize": "99999999", + # "baseOrderStep": "0.01", + # "quoteOrderStep": "0.0001", + # "tickSize": "0.0001", + # "feeCurrency": "USDT", + # "tradingStatus": "1", + # "marginMode": "2", + # "priceLimitRatio": "0.05", + # "feeCategory": 1, + # "makerFeeCoefficient": "1.00", + # "takerFeeCoefficient": "1.00", + # "st": False + # }, + # ] + # } + # } + # + promises.append(self.utaGetMarketInstrument(self.extend(params, {'tradeType': 'FUTURES'}))) + # + # { + # "code": "200000", + # "data": { + # "tradeType": "FUTURES", + # "list": [ + # { + # "symbol": "XBTUSDTM", + # "baseCurrency": "XBT", + # "quoteCurrency": "USDT", + # "maxBaseOrderSize": "1000000", + # "tickSize": "0.1", + # "tradingStatus": "1", + # "settlementCurrency": "USDT", + # "contractType": "0", + # "isInverse": False, + # "launchTime": 1585555200000, + # "expiryTime": null, + # "settlementTime": null, + # "maxPrice": "1000000.0", + # "lotSize": "1", + # "unitSize": "0.001", + # "makerFeeRate": "0.00020", + # "takerFeeRate": "0.00060", + # "settlementFeeRate": null, + # "maxLeverage": 125, + # "indexSourceExchanges": ["okex","binance","kucoin","bybit","bitmart","gateio"], + # "k": "490.0", + # "m": "300.0", + # "f": "1.3", + # "mmrLimit": "0.3", + # "mmrLevConstant": "125.0" + # }, + # ] + # } + # } + # + responses = await asyncio.gather(*promises) + data = self.safe_dict(responses[0], 'data', {}) + contractData = self.safe_dict(responses[1], 'data', {}) + spotData = self.safe_list(data, 'list', []) + contractSymbolsData = self.safe_list(contractData, 'list', []) + symbolsData = self.array_concat(spotData, contractSymbolsData) + result = [] + for i in range(0, len(symbolsData)): + market = symbolsData[i] + id = self.safe_string(market, 'symbol') + baseId = self.safe_string(market, 'baseCurrency') + quoteId = self.safe_string(market, 'quoteCurrency') + settleId = self.safe_string(market, 'settlementCurrency') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + settle = self.safe_currency_code(settleId) + hasMargin = self.safe_string(market, 'marginMode') + isMarginable = True if (hasMargin == '1') else False + symbol = base + '/' + quote + if settle is not None: + symbol += ':' + settle + contractType = self.safe_string(market, 'contractType') + expiry = self.safe_integer(market, 'expiryTime') + active = self.safe_string(market, 'tradingStatus') + type = None + spot = False + swap = False + future = False + contract = False + linear = False + inverse = False + if contractType is not None: + contract = True + if quote == settle: + linear = True + else: + inverse = True + if contractType == '0': + type = 'swap' + swap = True + else: + type = 'future' + future = True + else: + type = 'spot' + spot = True + result.append({ + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': type, + 'spot': spot, + 'margin': isMarginable, + 'swap': swap, + 'future': future, + 'option': False, + 'active': (active == '1'), + 'contract': contract, + 'linear': linear, + 'inverse': inverse, + 'taker': self.safe_number(market, 'makerFeeRate'), + 'maker': self.safe_number(market, 'takerFeeRate'), + 'contractSize': self.safe_number(market, 'unitSize'), + 'expiry': expiry, + 'expiryDatetime': self.iso8601(expiry), + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.safe_number(market, 'lotSize'), + 'price': self.safe_number(market, 'tickSize'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': self.safe_integer(market, 'maxLeverage'), + }, + 'amount': { + 'min': self.safe_number(market, 'minBaseOrderSize'), + 'max': self.safe_number(market, 'maxBaseOrderSize'), + }, + 'price': { + 'min': None, + 'max': self.safe_number(market, 'maxPrice'), + }, + 'cost': { + 'min': self.safe_number(market, 'minQuoteOrderSize'), + 'max': self.safe_number(market, 'maxQuoteOrderSize'), + }, + }, + 'created': self.safe_integer(market, 'launchTime'), + 'info': market, + }) + if self.options['adjustForTimeDifference']: + await self.load_time_difference() + return result + + async def load_migration_status(self, force: bool = False): + """ + :param boolean force: load account state for non hf + loads the migration status for the account(hf or not) + + https://www.kucoin.com/docs/rest/spot-trading/spot-hf-trade-pro-account/get-user-type + + :returns any: ignore + """ + if not ('hf' in self.options) or (self.options['hf'] is None) or force: + result: dict = await self.privateGetHfAccountsOpened() + self.options['hf'] = self.safe_bool(result, 'data') + return True + + def handle_hf_and_params(self, params={}): + migrated: Bool = self.safe_bool(self.options, 'hf', False) + loadedHf: Bool = None + if migrated is not None: + if migrated: + loadedHf = True + else: + loadedHf = False + hf: Bool = self.safe_bool(params, 'hf', loadedHf) + params = self.omit(params, 'hf') + return [hf, params] + + async def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://docs.kucoin.com/#get-currencies + + :param dict params: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = await self.publicGetCurrencies(params) + # + # { + # "code":"200000", + # "data":[ + # { + # "currency":"CSP", + # "name":"CSP", + # "fullName":"Caspian", + # "precision":8, + # "confirms":null, + # "contractAddress":null, + # "isMarginEnabled":false, + # "isDebitEnabled":false, + # "chains":[ + # { + # "chainName":"ERC20", + # "chainId": "eth" + # "withdrawalMinSize":"2999", + # "depositMinSize":null, + # "withdrawFeeRate":"0", + # "withdrawalMinFee":"2999", + # "isWithdrawEnabled":false, + # "isDepositEnabled":false, + # "confirms":12, + # "preConfirms":12, + # "withdrawPrecision": 8, + # "maxWithdraw": null, + # "maxDeposit": null, + # "needTag": False, + # "contractAddress":"0xa6446d655a0c34bc4f05042ee88170d056cbaf45", + # "depositFeeRate": "0.001", # present for some currencies/networks + # } + # ] + # }, + # ] + # } + # + currenciesData = self.safe_list(response, 'data', []) + brokenCurrencies = self.safe_list(self.options, 'brokenCurrencies', ['00', 'OPEN_ERROR', 'HUF', 'BDT']) + result: dict = {} + for i in range(0, len(currenciesData)): + entry = currenciesData[i] + id = self.safe_string(entry, 'currency') + if self.in_array(id, brokenCurrencies): + continue # skip buggy entries: https://t.me/KuCoin_API/217798 + code = self.safe_currency_code(id) + networks: dict = {} + chains = self.safe_list(entry, 'chains', []) + chainsLength = len(chains) + for j in range(0, chainsLength): + chain = chains[j] + chainId = self.safe_string(chain, 'chainId') + networkCode = self.network_id_to_code(chainId, code) + networks[networkCode] = { + 'info': chain, + 'id': chainId, + 'name': self.safe_string(chain, 'chainName'), + 'code': networkCode, + 'active': None, + 'fee': self.safe_number(chain, 'withdrawalMinFee'), + 'deposit': self.safe_bool(chain, 'isDepositEnabled'), + 'withdraw': self.safe_bool(chain, 'isWithdrawEnabled'), + 'precision': self.parse_number(self.parse_precision(self.safe_string(chain, 'withdrawPrecision'))), + 'limits': { + 'withdraw': { + 'min': self.safe_number(chain, 'withdrawalMinSize'), + 'max': self.safe_number(chain, 'maxWithdraw'), + }, + 'deposit': { + 'min': self.safe_number(chain, 'depositMinSize'), + 'max': self.safe_number(chain, 'maxDeposit'), + }, + }, + } + # kucoin has determined 'fiat' currencies with below logic + rawPrecision = self.safe_string(entry, 'precision') + precision = self.parse_number(self.parse_precision(rawPrecision)) + isFiat = chainsLength == 0 + result[code] = self.safe_currency_structure({ + 'id': id, + 'name': self.safe_string(entry, 'fullName'), + 'code': code, + 'type': 'fiat' if isFiat else 'crypto', + 'precision': precision, + 'info': entry, + 'networks': networks, + 'deposit': None, + 'withdraw': None, + 'active': None, + 'fee': None, + 'limits': None, + }) + return result + + async def fetch_accounts(self, params={}) -> List[Account]: + """ + fetch all the accounts associated with a profile + + https://docs.kucoin.com/#list-accounts + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `account structures ` indexed by the account type + """ + response = await self.privateGetAccounts(params) + # + # { + # "code": "200000", + # "data": [ + # { + # "balance": "0.00009788", + # "available": "0.00009788", + # "holds": "0", + # "currency": "BTC", + # "id": "5c6a4fd399a1d81c4f9cc4d0", + # "type": "trade" + # }, + # { + # "balance": "0.00000001", + # "available": "0.00000001", + # "holds": "0", + # "currency": "ETH", + # "id": "5c6a49ec99a1d819392e8e9f", + # "type": "trade" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + result = [] + for i in range(0, len(data)): + account = data[i] + accountId = self.safe_string(account, 'id') + currencyId = self.safe_string(account, 'currency') + code = self.safe_currency_code(currencyId) + type = self.safe_string(account, 'type') # main or trade + result.append({ + 'id': accountId, + 'type': type, + 'currency': code, + 'code': code, + 'info': account, + }) + return result + + async def fetch_transaction_fee(self, code: str, params={}): + """ + *DEPRECATED* please use fetchDepositWithdrawFee instead + + https://docs.kucoin.com/#get-withdrawal-quotas + + :param str code: unified currency code + :param dict params: extra parameters specific to the exchange API endpoint + :returns dict: a `fee structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + } + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + if networkCode is not None: + request['chain'] = self.network_code_to_id(networkCode).lower() + response = await self.privateGetWithdrawalsQuotas(self.extend(request, params)) + data = self.safe_dict(response, 'data', {}) + withdrawFees: dict = {} + withdrawFees[code] = self.safe_number(data, 'withdrawMinFee') + return { + 'info': response, + 'withdraw': withdrawFees, + 'deposit': {}, + } + + async def fetch_deposit_withdraw_fee(self, code: str, params={}): + """ + fetch the fee for deposits and withdrawals + + https://docs.kucoin.com/#get-withdrawal-quotas + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.network]: The chain of currency. This only apply for multi-chain currency, and there is no need for single chain currency; you can query the chain through the response of the GET /api/v2/currencies/{currency} interface + :returns dict: a `fee structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + } + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + if networkCode is not None: + request['chain'] = self.network_code_to_id(networkCode).lower() + response = await self.privateGetWithdrawalsQuotas(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "currency": "USDT", + # "limitBTCAmount": "1.00000000", + # "usedBTCAmount": "0.00000000", + # "remainAmount": "16548.072149", + # "availableAmount": "0", + # "withdrawMinFee": "25", + # "innerWithdrawMinFee": "0", + # "withdrawMinSize": "50", + # "isWithdrawEnabled": True, + # "precision": 6, + # "chain": "ERC20" + # } + # } + # + data = self.safe_dict(response, 'data') + return self.parse_deposit_withdraw_fee(data, currency) + + def parse_deposit_withdraw_fee(self, fee, currency: Currency = None): + # + # { + # "currency": "USDT", + # "limitBTCAmount": "1.00000000", + # "usedBTCAmount": "0.00000000", + # "remainAmount": "16548.072149", + # "availableAmount": "0", + # "withdrawMinFee": "25", + # "innerWithdrawMinFee": "0", + # "withdrawMinSize": "50", + # "isWithdrawEnabled": True, + # "precision": 6, + # "chain": "ERC20" + # } + # + if 'chains' in fee: + # if data obtained through `currencies` endpoint + resultNew: dict = { + 'info': fee, + 'withdraw': { + 'fee': None, + 'percentage': False, + }, + 'deposit': { + 'fee': None, + 'percentage': None, + }, + 'networks': {}, + } + chains = self.safe_list(fee, 'chains', []) + for i in range(0, len(chains)): + chain = chains[i] + networkCodeNew = self.network_id_to_code(self.safe_string(chain, 'chainId'), self.safe_string(currency, 'code')) + resultNew['networks'][networkCodeNew] = { + 'withdraw': { + 'fee': self.safe_number_2(chain, 'withdrawalMinFee', 'withdrawMinFee'), + 'percentage': False, + }, + 'deposit': { + 'fee': None, + 'percentage': None, + }, + } + return resultNew + minWithdrawFee = self.safe_number(fee, 'withdrawMinFee') + result: dict = { + 'info': fee, + 'withdraw': { + 'fee': minWithdrawFee, + 'percentage': False, + }, + 'deposit': { + 'fee': None, + 'percentage': None, + }, + 'networks': {}, + } + networkId = self.safe_string(fee, 'chain') + networkCode = self.network_id_to_code(networkId, self.safe_string(currency, 'code')) + result['networks'][networkCode] = { + 'withdraw': minWithdrawFee, + 'deposit': { + 'fee': None, + 'percentage': None, + }, + } + return result + + def is_futures_method(self, methodName, params): + # + # Helper + # @methodName(string): The name of the method + # @params(dict): The parameters passed into {methodName} + # @return: True if the method used is meant for futures trading, False otherwise + # + defaultType = self.safe_string_2(self.options, methodName, 'defaultType', 'trade') + requestedType = self.safe_string(params, 'type', defaultType) + accountsByType = self.safe_dict(self.options, 'accountsByType') + type = self.safe_string(accountsByType, requestedType) + if type is None: + keys = list(accountsByType.keys()) + raise ExchangeError(self.id + ' isFuturesMethod() type must be one of ' + ', '.join(keys)) + params = self.omit(params, 'type') + return(type == 'contract') or (type == 'future') or (type == 'futures') # * (type == 'futures') deprecated, use(type == 'future') + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "symbol": "BTC-USDT", # symbol + # "symbolName":"BTC-USDT", # Name of trading pairs, it would change after renaming + # "buy": "11328.9", # bestAsk + # "sell": "11329", # bestBid + # "changeRate": "-0.0055", # 24h change rate + # "changePrice": "-63.6", # 24h change price + # "high": "11610", # 24h highest price + # "low": "11200", # 24h lowest price + # "vol": "2282.70993217", # 24h volume,the aggregated trading volume in BTC + # "volValue": "25984946.157790431", # 24h total, the trading volume in quote currency of last 24 hours + # "last": "11328.9", # last price + # "averagePrice": "11360.66065903", # 24h average transaction price yesterday + # "takerFeeRate": "0.001", # Basic Taker Fee + # "makerFeeRate": "0.001", # Basic Maker Fee + # "takerCoefficient": "1", # Taker Fee Coefficient + # "makerCoefficient": "1" # Maker Fee Coefficient + # } + # + # { + # "trading": True, + # "symbol": "KCS-BTC", + # "buy": 0.00011, + # "sell": 0.00012, + # "sort": 100, + # "volValue": 3.13851792584, #total + # "baseCurrency": "KCS", + # "market": "BTC", + # "quoteCurrency": "BTC", + # "symbolCode": "KCS-BTC", + # "datetime": 1548388122031, + # "high": 0.00013, + # "vol": 27514.34842, + # "low": 0.0001, + # "changePrice": -1.0e-5, + # "changeRate": -0.0769, + # "lastTradedPrice": 0.00012, + # "board": 0, + # "mark": 0 + # } + # + # market/ticker ws subscription + # + # { + # "bestAsk": "62258.9", + # "bestAskSize": "0.38579986", + # "bestBid": "62258.8", + # "bestBidSize": "0.0078381", + # "price": "62260.7", + # "sequence": "1621383297064", + # "size": "0.00002841", + # "time": 1634641777363 + # } + # + # uta + # + # { + # "symbol": "BTC-USDT", + # "name": "BTC-USDT", + # "bestBidSize": "0.69207954", + # "bestBidPrice": "110417.5", + # "bestAskSize": "0.08836606", + # "bestAskPrice": "110417.6", + # "lastPrice": "110417.5", + # "size": "0.00016", + # "open": "110105.1", + # "high": "110838.9", + # "low": "109705.5", + # "baseVolume": "1882.10069442", + # "quoteVolume": "207325626.822922498" + # } + # + percentage = self.safe_string(ticker, 'changeRate') + if percentage is not None: + percentage = Precise.string_mul(percentage, '100') + last = self.safe_string_n(ticker, ['last', 'lastTradedPrice', 'lastPrice']) + last = self.safe_string(ticker, 'price', last) + marketId = self.safe_string(ticker, 'symbol') + market = self.safe_market(marketId, market, '-') + symbol = market['symbol'] + baseVolume = self.safe_string_2(ticker, 'vol', 'baseVolume') + quoteVolume = self.safe_string_2(ticker, 'volValue', 'quoteVolume') + timestamp = self.safe_integer_n(ticker, ['time', 'datetime', 'timePoint']) + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': self.safe_string_n(ticker, ['buy', 'bestBid', 'bestBidPrice']), + 'bidVolume': self.safe_string(ticker, 'bestBidSize'), + 'ask': self.safe_string_n(ticker, ['sell', 'bestAsk', 'bestAskPrice']), + 'askVolume': self.safe_string(ticker, 'bestAskSize'), + 'vwap': None, + 'open': self.safe_string(ticker, 'open'), + 'close': last, + 'last': last, + 'previousClose': None, + 'change': self.safe_string(ticker, 'changePrice'), + 'percentage': percentage, + 'average': self.safe_string(ticker, 'averagePrice'), + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'markPrice': self.safe_string(ticker, 'value'), + 'info': ticker, + }, market) + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://docs.kucoin.com/#get-all-tickers + https://www.kucoin.com/docs-new/rest/ua/get-ticker + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :param str [params.tradeType]: *uta only* set to SPOT or FUTURES + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + request: dict = {} + symbols = self.market_symbols(symbols) + uta = None + uta, params = self.handle_option_and_params(params, 'fetchTickers', 'uta', False) + response = None + if uta: + if symbols is not None: + symbol = self.safe_string(symbols, 0) + market = self.market(symbol) + type = None + type, params = self.handle_market_type_and_params('fetchTickers', market, params) + if type == 'spot': + request['tradeType'] = 'SPOT' + else: + request['tradeType'] = 'FUTURES' + else: + tradeType = self.safe_string_upper(params, 'tradeType') + if tradeType is None: + raise ArgumentsRequired(self.id + ' fetchTickers() requires a tradeType parameter for uta, either SPOT or FUTURES') + request['tradeType'] = tradeType + params = self.omit(params, 'tradeType') + response = await self.utaGetMarketTicker(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "tradeType": "SPOT", + # "ts": 1762061290067, + # "list": [ + # { + # "symbol": "BTC-USDT", + # "name": "BTC-USDT", + # "bestBidSize": "0.69207954", + # "bestBidPrice": "110417.5", + # "bestAskSize": "0.08836606", + # "bestAskPrice": "110417.6", + # "lastPrice": "110417.5", + # "size": "0.00016", + # "open": "110105.1", + # "high": "110838.9", + # "low": "109705.5", + # "baseVolume": "1882.10069442", + # "quoteVolume": "207325626.822922498" + # } + # ] + # } + # } + # + else: + response = await self.publicGetMarketAllTickers(params) + # + # { + # "code": "200000", + # "data": { + # "time":1602832092060, + # "ticker":[ + # { + # "symbol": "BTC-USDT", # symbol + # "symbolName":"BTC-USDT", # Name of trading pairs, it would change after renaming + # "buy": "11328.9", # bestAsk + # "sell": "11329", # bestBid + # "changeRate": "-0.0055", # 24h change rate + # "changePrice": "-63.6", # 24h change price + # "high": "11610", # 24h highest price + # "low": "11200", # 24h lowest price + # "vol": "2282.70993217", # 24h volume,the aggregated trading volume in BTC + # "volValue": "25984946.157790431", # 24h total, the trading volume in quote currency of last 24 hours + # "last": "11328.9", # last price + # "averagePrice": "11360.66065903", # 24h average transaction price yesterday + # "takerFeeRate": "0.001", # Basic Taker Fee + # "makerFeeRate": "0.001", # Basic Maker Fee + # "takerCoefficient": "1", # Taker Fee Coefficient + # "makerCoefficient": "1" # Maker Fee Coefficient + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + tickers = self.safe_list_2(data, 'ticker', 'list', []) + time = self.safe_integer_2(data, 'time', 'ts') + result: dict = {} + for i in range(0, len(tickers)): + tickers[i]['time'] = time + ticker = self.parse_ticker(tickers[i]) + symbol = self.safe_string(ticker, 'symbol') + if symbol is not None: + result[symbol] = ticker + return self.filter_by_array_tickers(result, 'symbol', symbols) + + async def fetch_mark_prices(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches the mark price for multiple markets + + https://www.kucoin.com/docs/rest/margin-trading/margin-info/get-all-margin-trading-pairs-mark-prices + + :param str[] [symbols]: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + response = await self.publicGetMarkPriceAllSymbols(params) + data = self.safe_list(response, 'data', []) + return self.parse_tickers(data) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://docs.kucoin.com/#get-24hr-stats + https://www.kucoin.com/docs-new/rest/ua/get-ticker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + uta = None + uta, params = self.handle_option_and_params(params, 'fetchTicker', 'uta', False) + response = None + result = None + if uta: + type = None + type, params = self.handle_market_type_and_params('fetchTicker', market, params) + if type == 'spot': + request['tradeType'] = 'SPOT' + else: + request['tradeType'] = 'FUTURES' + response = await self.utaGetMarketTicker(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "tradeType": "SPOT", + # "ts": 1762061290067, + # "list": [ + # { + # "symbol": "BTC-USDT", + # "name": "BTC-USDT", + # "bestBidSize": "0.69207954", + # "bestBidPrice": "110417.5", + # "bestAskSize": "0.08836606", + # "bestAskPrice": "110417.6", + # "lastPrice": "110417.5", + # "size": "0.00016", + # "open": "110105.1", + # "high": "110838.9", + # "low": "109705.5", + # "baseVolume": "1882.10069442", + # "quoteVolume": "207325626.822922498" + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + resultList = self.safe_list(data, 'list', []) + result = self.safe_dict(resultList, 0, {}) + else: + response = await self.publicGetMarketStats(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "time": 1602832092060, # time + # "symbol": "BTC-USDT", # symbol + # "buy": "11328.9", # bestAsk + # "sell": "11329", # bestBid + # "changeRate": "-0.0055", # 24h change rate + # "changePrice": "-63.6", # 24h change price + # "high": "11610", # 24h highest price + # "low": "11200", # 24h lowest price + # "vol": "2282.70993217", # 24h volume,the aggregated trading volume in BTC + # "volValue": "25984946.157790431", # 24h total, the trading volume in quote currency of last 24 hours + # "last": "11328.9", # last price + # "averagePrice": "11360.66065903", # 24h average transaction price yesterday + # "takerFeeRate": "0.001", # Basic Taker Fee + # "makerFeeRate": "0.001", # Basic Maker Fee + # "takerCoefficient": "1", # Taker Fee Coefficient + # "makerCoefficient": "1" # Maker Fee Coefficient + # } + # } + # + result = self.safe_dict(response, 'data', {}) + return self.parse_ticker(result, market) + + async def fetch_mark_price(self, symbol: str, params={}) -> Ticker: + """ + fetches the mark price for a specific market + + https://www.kucoin.com/docs/rest/margin-trading/margin-info/get-mark-price + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = await self.publicGetMarkPriceSymbolCurrent(self.extend(request, params)) + # + data = self.safe_dict(response, 'data', {}) + return self.parse_ticker(data, market) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # [ + # "1545904980", # Start time of the candle cycle + # "0.058", # opening price + # "0.049", # closing price + # "0.058", # highest price + # "0.049", # lowest price + # "0.018", # base volume + # "0.000945", # quote volume + # ] + # + return [ + self.safe_timestamp(ohlcv, 0), + self.safe_number(ohlcv, 1), + self.safe_number(ohlcv, 3), + self.safe_number(ohlcv, 4), + self.safe_number(ohlcv, 2), + self.safe_number(ohlcv, 5), + ] + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://docs.kucoin.com/#get-klines + https://www.kucoin.com/docs-new/rest/ua/get-klines + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate') + if paginate: + return await self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params, 1500) + market = self.market(symbol) + marketId = market['id'] + request: dict = { + 'symbol': marketId, + } + duration = self.parse_timeframe(timeframe) * 1000 + endAt = self.milliseconds() # required param + if since is not None: + request['startAt'] = self.parse_to_int(int(math.floor(since / 1000))) + if limit is None: + # https://docs.kucoin.com/#get-klines + # https://docs.kucoin.com/#details + # For each query, the system would return at most 1500 pieces of data. + # To obtain more data, please page the data by time. + limit = self.safe_integer(self.options, 'fetchOHLCVLimit', 1500) + endAt = self.sum(since, limit * duration) + elif limit is not None: + since = endAt - limit * duration + request['startAt'] = self.parse_to_int(int(math.floor(since / 1000))) + request['endAt'] = self.parse_to_int(int(math.floor(endAt / 1000))) + uta = None + uta, params = self.handle_option_and_params(params, 'fetchOHLCV', 'uta', False) + response = None + result = None + if uta: + type = None + type, params = self.handle_market_type_and_params('fetchOHLCV', market, params) + if type == 'spot': + request['tradeType'] = 'SPOT' + else: + request['tradeType'] = 'FUTURES' + request['interval'] = self.safe_string(self.timeframes, timeframe, timeframe) + response = await self.utaGetMarketKline(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "tradeType": "SPOT", + # "symbol": "BTC-USDT", + # "list": [ + # ["1762240200","104581.4","104527.1","104620.1","104526.4","5.57665554","583263.661804122"], + # ["1762240140","104565.6","104581.3","104601.7","104511.3","6.48505114","677973.775916968"], + # ["1762240080","104621.5","104571.3","104704.7","104571.3","14.51713618","1519468.954060838"] + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + result = self.safe_list(data, 'list', []) + else: + request['type'] = self.safe_string(self.timeframes, timeframe, timeframe) + response = await self.publicGetMarketCandles(self.extend(request, params)) + # + # { + # "code":"200000", + # "data":[ + # ["1591517700","0.025078","0.025069","0.025084","0.025064","18.9883256","0.4761861079404"], + # ["1591516800","0.025089","0.025079","0.025089","0.02506","99.4716622","2.494143499081"], + # ["1591515900","0.025079","0.02509","0.025091","0.025068","59.83701271","1.50060885172798"], + # ] + # } + # + result = self.safe_list(response, 'data', []) + return self.parse_ohlcvs(result, market, timeframe, since, limit) + + async def create_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + + https://www.kucoin.com/docs/rest/funding/deposit/create-deposit-address-v3- + + create a currency deposit address + :param str code: unified currency code of the currency for the deposit address + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.network]: the blockchain network name + :returns dict: an `address structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + } + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + if networkCode is not None: + request['chain'] = self.network_code_to_id(networkCode) # docs mention "chain-name", but seems "chain-id" is used, like in "fetchDepositAddress" + response = await self.privatePostDepositAddressCreate(self.extend(request, params)) + # {"code":"260000","msg":"Deposit address already exists."} + # + # { + # "code": "200000", + # "data": { + # "address": "0x2336d1834faab10b2dac44e468f2627138417431", + # "memo": null, + # "chainId": "bsc", + # "to": "MAIN", + # "expirationDate": 0, + # "currency": "BNB", + # "chainName": "BEP20" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_deposit_address(data, currency) + + async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://docs.kucoin.com/#get-deposit-addresses-v2 + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.network]: the blockchain network name + :returns dict: an `address structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + # for USDT - OMNI, ERC20, TRC20, default is ERC20 + # for BTC - Native, Segwit, TRC20, the parameters are bech32, btc, trx, default is Native + # 'chain': 'ERC20', # optional + } + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + if networkCode is not None: + request['chain'] = self.network_code_to_id(networkCode).lower() + version = self.options['versions']['private']['GET']['deposit-addresses'] + self.options['versions']['private']['GET']['deposit-addresses'] = 'v1' + response = await self.privateGetDepositAddresses(self.extend(request, params)) + # BCH {"code":"200000","data":{"address":"bitcoincash:qza3m4nj9rx7l9r0cdadfqxts6f92shvhvr5ls4q7z","memo":""}} + # BTC {"code":"200000","data":{"address":"36SjucKqQpQSvsak9A7h6qzFjrVXpRNZhE","memo":""}} + self.options['versions']['private']['GET']['deposit-addresses'] = version + data = self.safe_value(response, 'data') + if data is None: + raise ExchangeError(self.id + ' fetchDepositAddress() returned an empty response, you might try to run createDepositAddress() first and try again') + return self.parse_deposit_address(data, currency) + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + address = self.safe_string(depositAddress, 'address') + # BCH/BSV is returned with a "bitcoincash:" prefix, which we cut off here and only keep the address + if address is not None: + address = address.replace('bitcoincash:', '') + code = None + if currency is not None: + code = self.safe_currency_code(currency['id']) + if code != 'NIM': + # contains spaces + self.check_address(address) + return { + 'info': depositAddress, + 'currency': code, + 'network': self.network_id_to_code(self.safe_string(depositAddress, 'chainId')), + 'address': address, + 'tag': self.safe_string(depositAddress, 'memo'), + } + + async def fetch_deposit_addresses_by_network(self, code: str, params={}) -> List[DepositAddress]: + """ + + https://docs.kucoin.com/#get-deposit-addresses-v2 + + fetch the deposit address for a currency associated with self account + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an array of `address structures ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + } + version = self.options['versions']['private']['GET']['deposit-addresses'] + self.options['versions']['private']['GET']['deposit-addresses'] = 'v2' + response = await self.privateGetDepositAddresses(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": [ + # { + # "address": "fr1qvus7d4d5fgxj5e7zvqe6yhxd7txm95h2and69r", + # "memo": "", + # "chain": "BTC-Segwit", + # "contractAddress": "" + # }, + # {"address":"37icNMEWbiF8ZkwUMxmfzMxi2A1MQ44bMn","memo":"","chain":"BTC","contractAddress":""}, + # {"address":"Deposit temporarily blocked","memo":"","chain":"TRC20","contractAddress":""} + # ] + # } + # + self.options['versions']['private']['GET']['deposit-addresses'] = version + chains = self.safe_list(response, 'data', []) + parsed = self.parse_deposit_addresses(chains, [currency['code']], False, { + 'currency': currency['code'], + }) + return self.index_by(parsed, 'network') + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://www.kucoin.com/docs/rest/spot-trading/market-data/get-part-order-book-aggregated- + https://www.kucoin.com/docs/rest/spot-trading/market-data/get-full-order-book-aggregated- + https://www.kucoin.com/docs-new/rest/ua/get-orderbook + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + level = self.safe_integer(params, 'level', 2) + request: dict = {'symbol': market['id']} + isAuthenticated = self.check_required_credentials(False) + uta = None + uta, params = self.handle_option_and_params(params, 'fetchOrderBook', 'uta', False) + response = None + if uta: + if limit is None: + raise ArgumentsRequired(self.id + ' fetchOrderBook() requires a limit argument for uta, either 20, 50, 100 or FULL') + request['limit'] = limit + request['symbol'] = market['id'] + type = None + type, params = self.handle_market_type_and_params('fetchOrderBook', market, params) + if type == 'spot': + request['tradeType'] = 'SPOT' + else: + request['tradeType'] = 'FUTURES' + response = await self.utaGetMarketOrderbook(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "tradeType": "SPOT", + # "symbol": "BTC-USDT", + # "sequence": "23136002402", + # "bids": [ + # ["104700","10.25940068"], + # ["104698.9","0.00057076"], + # ], + # "asks": [ + # ["104700.1","1.4082106"], + # ["104700.5","0.02866269"], + # ] + # } + # } + # + elif not isAuthenticated or limit is not None: + if level == 2: + request['level'] = level + if limit is not None: + if (limit == 20) or (limit == 100): + request['limit'] = limit + else: + raise ExchangeError(self.id + ' fetchOrderBook() limit argument must be 20 or 100') + request['limit'] = limit if limit else 100 + response = await self.publicGetMarketOrderbookLevelLevelLimit(self.extend(request, params)) + else: + response = await self.privateGetMarketOrderbookLevel2(self.extend(request, params)) + # + # public(v1) market/orderbook/level2_20 and market/orderbook/level2_100 + # + # { + # "sequence": "3262786978", + # "time": 1550653727731, + # "bids": [ + # ["6500.12", "0.45054140"], + # ["6500.11", "0.45054140"], + # ], + # "asks": [ + # ["6500.16", "0.57753524"], + # ["6500.15", "0.57753524"], + # ] + # } + # + # private(v3) market/orderbook/level2 + # + # { + # "sequence": "3262786978", + # "time": 1550653727731, + # "bids": [ + # ["6500.12", "0.45054140"], + # ["6500.11", "0.45054140"], + # ], + # "asks": [ + # ["6500.16", "0.57753524"], + # ["6500.15", "0.57753524"], + # ] + # } + # + data = self.safe_dict(response, 'data', {}) + timestamp = self.safe_integer(data, 'time') + orderbook = self.parse_order_book(data, market['symbol'], timestamp, 'bids', 'asks', level - 2, level - 1) + orderbook['nonce'] = self.safe_integer(data, 'sequence') + return orderbook + + def handle_trigger_prices(self, params): + triggerPrice = self.safe_value_2(params, 'triggerPrice', 'stopPrice') + stopLossPrice = self.safe_value(params, 'stopLossPrice') + takeProfitPrice = self.safe_value(params, 'takeProfitPrice') + isStopLoss = stopLossPrice is not None + isTakeProfit = takeProfitPrice is not None + if (isStopLoss and isTakeProfit) or (triggerPrice and stopLossPrice) or (triggerPrice and isTakeProfit): + raise ExchangeError(self.id + ' createOrder() - you should use either triggerPrice or stopLossPrice or takeProfitPrice') + return [triggerPrice, stopLossPrice, takeProfitPrice] + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + Create an order on the exchange + + https://docs.kucoin.com/spot#place-a-new-order + https://docs.kucoin.com/spot#place-a-new-order-2 + https://docs.kucoin.com/spot#place-a-margin-order + https://docs.kucoin.com/spot-hf/#place-hf-order + https://www.kucoin.com/docs/rest/spot-trading/orders/place-order-test + https://www.kucoin.com/docs/rest/margin-trading/orders/place-margin-order-test + https://www.kucoin.com/docs/rest/spot-trading/spot-hf-trade-pro-account/sync-place-hf-order + + :param str symbol: Unified CCXT market symbol + :param str type: 'limit' or 'market' + :param str side: 'buy' or 'sell' + :param float amount: the amount of currency to trade + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: The price at which a trigger order is triggered at + :param str [params.marginMode]: 'cross', # cross(cross mode) and isolated(isolated mode), set to cross by default, the isolated mode will be released soon, stay tuned + :param str [params.timeInForce]: GTC, GTT, IOC, or FOK, default is GTC, limit orders only + :param str [params.postOnly]: Post only flag, invalid when timeInForce is IOC or FOK + + EXCHANGE SPECIFIC PARAMETERS + :param str [params.clientOid]: client order id, defaults to uuid if not passed + :param str [params.remark]: remark for the order, length cannot exceed 100 utf8 characters + :param str [params.tradeType]: 'TRADE', # TRADE, MARGIN_TRADE # not used with margin orders + limit orders --------------------------------------------------- + :param float [params.cancelAfter]: long, # cancel after n seconds, requires timeInForce to be GTT + :param bool [params.hidden]: False, # Order will not be displayed in the order book + :param bool [params.iceberg]: False, # Only a portion of the order is displayed in the order book + :param str [params.visibleSize]: self.amount_to_precision(symbol, visibleSize), # The maximum visible size of an iceberg order + market orders -------------------------------------------------- + :param str [params.funds]: # Amount of quote currency to use + stop orders ---------------------------------------------------- + :param str [params.stop]: Either loss or entry, the default is loss. Requires triggerPrice to be defined + margin orders -------------------------------------------------- + :param float [params.leverage]: Leverage size of the order + :param str [params.stp]: '', # self trade prevention, CN, CO, CB or DC + :param bool [params.autoBorrow]: False, # The system will first borrow you funds at the optimal interest rate and then place an order for you + :param bool [params.hf]: False, # True for hf order + :param bool [params.test]: set to True to test an order, no order will be created but the request will be validated + :param bool [params.sync]: set to True to use the hf sync call + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + testOrder = self.safe_bool(params, 'test', False) + params = self.omit(params, 'test') + hf = None + hf, params = self.handle_hf_and_params(params) + useSync = False + useSync, params = self.handle_option_and_params(params, 'createOrder', 'sync', False) + triggerPrice, stopLossPrice, takeProfitPrice = self.handle_trigger_prices(params) + tradeType = self.safe_string(params, 'tradeType') # keep it for backward compatibility + isTriggerOrder = (triggerPrice or stopLossPrice or takeProfitPrice) + marginResult = self.handle_margin_mode_and_params('createOrder', params) + marginMode = self.safe_string(marginResult, 0) + isMarginOrder = tradeType == 'MARGIN_TRADE' or marginMode is not None + # don't omit anything before calling createOrderRequest + orderRequest = self.create_order_request(symbol, type, side, amount, price, params) + response = None + if testOrder: + if isMarginOrder: + response = await self.privatePostMarginOrderTest(orderRequest) + elif hf: + response = await self.privatePostHfOrdersTest(orderRequest) + else: + response = await self.privatePostOrdersTest(orderRequest) + elif isTriggerOrder: + response = await self.privatePostStopOrder(orderRequest) + elif isMarginOrder: + response = await self.privatePostMarginOrder(orderRequest) + elif useSync: + response = await self.privatePostHfOrdersSync(orderRequest) + elif hf: + response = await self.privatePostHfOrders(orderRequest) + else: + response = await self.privatePostOrders(orderRequest) + # + # { + # "code": "200000", + # "data": { + # "orderId": "5bd6e9286d99522a52e458de" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data, market) + + async def create_market_order_with_cost(self, symbol: str, side: OrderSide, cost: float, params={}): + """ + create a market order by providing the symbol, side and cost + + https://www.kucoin.com/docs/rest/spot-trading/orders/place-order + + :param str symbol: unified symbol of the market to create an order in + :param str side: 'buy' or 'sell' + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + req = { + 'cost': cost, + } + return await self.create_order(symbol, 'market', side, cost, None, self.extend(req, params)) + + async def create_market_buy_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market buy order by providing the symbol and cost + + https://www.kucoin.com/docs/rest/spot-trading/orders/place-order + + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + return await self.create_market_order_with_cost(symbol, 'buy', cost, params) + + async def create_market_sell_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market sell order by providing the symbol and cost + + https://www.kucoin.com/docs/rest/spot-trading/orders/place-order + + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + return await self.create_market_order_with_cost(symbol, 'sell', cost, params) + + async def create_orders(self, orders: List[OrderRequest], params={}): + """ + create a list of trade orders + + https://www.kucoin.com/docs/rest/spot-trading/orders/place-multiple-orders + https://www.kucoin.com/docs/rest/spot-trading/spot-hf-trade-pro-account/place-multiple-hf-orders + https://www.kucoin.com/docs/rest/spot-trading/spot-hf-trade-pro-account/sync-place-multiple-hf-orders + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.hf]: False, # True for hf orders + :param bool [params.sync]: False, # True to use the hf sync call + :returns dict: an `order structure ` + """ + await self.load_markets() + ordersRequests = [] + symbol = None + for i in range(0, len(orders)): + rawOrder = orders[i] + marketId = self.safe_string(rawOrder, 'symbol') + if symbol is None: + symbol = marketId + else: + if symbol != marketId: + raise BadRequest(self.id + ' createOrders() requires all orders to have the same symbol') + type = self.safe_string(rawOrder, 'type') + if type != 'limit': + raise BadRequest(self.id + ' createOrders() only supports limit orders') + side = self.safe_string(rawOrder, 'side') + amount = self.safe_value(rawOrder, 'amount') + price = self.safe_value(rawOrder, 'price') + orderParams = self.safe_value(rawOrder, 'params', {}) + orderRequest = self.create_order_request(marketId, type, side, amount, price, orderParams) + ordersRequests.append(orderRequest) + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'orderList': ordersRequests, + } + hf = None + hf, params = self.handle_hf_and_params(params) + useSync = False + useSync, params = self.handle_option_and_params(params, 'createOrders', 'sync', False) + response = None + if useSync: + response = await self.privatePostHfOrdersMultiSync(self.extend(request, params)) + elif hf: + response = await self.privatePostHfOrdersMulti(self.extend(request, params)) + else: + response = await self.privatePostOrdersMulti(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "data": [ + # { + # "symbol": "LTC-USDT", + # "type": "limit", + # "side": "sell", + # "price": "90", + # "size": "0.1", + # "funds": null, + # "stp": "", + # "stop": "", + # "stopPrice": null, + # "timeInForce": "GTC", + # "cancelAfter": 0, + # "postOnly": False, + # "hidden": False, + # "iceberge": False, + # "iceberg": False, + # "visibleSize": null, + # "channel": "API", + # "id": "6539148443fcf500079d15e5", + # "status": "success", + # "failMsg": null, + # "clientOid": "5c4c5398-8ab2-4b4e-af8a-e2d90ad2488f" + # }, + # } + # + data = self.safe_dict(response, 'data', {}) + data = self.safe_list(data, 'data', []) + return self.parse_orders(data) + + def market_order_amount_to_precision(self, symbol: str, amount): + market = self.market(symbol) + result = self.decimal_to_precision(amount, TRUNCATE, market['info']['quoteIncrement'], self.precisionMode, self.paddingMode) + if result == '0': + raise InvalidOrder(self.id + ' amount of ' + market['symbol'] + ' must be greater than minimum amount precision of ' + self.number_to_string(market['precision']['amount'])) + return result + + def create_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + market = self.market(symbol) + # required param, cannot be used twice + clientOrderId = self.safe_string_2(params, 'clientOid', 'clientOrderId', self.uuid()) + params = self.omit(params, ['clientOid', 'clientOrderId']) + request: dict = { + 'clientOid': clientOrderId, + 'side': side, + 'symbol': market['id'], + 'type': type, # limit or market + } + quoteAmount = self.safe_number_2(params, 'cost', 'funds') + amountString = None + costString = None + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('createOrder', params) + if type == 'market': + if quoteAmount is not None: + params = self.omit(params, ['cost', 'funds']) + # kucoin uses base precision even for quote values + costString = self.market_order_amount_to_precision(symbol, quoteAmount) + request['funds'] = costString + else: + amountString = self.amount_to_precision(symbol, amount) + request['size'] = self.amount_to_precision(symbol, amount) + else: + amountString = self.amount_to_precision(symbol, amount) + request['size'] = amountString + request['price'] = self.price_to_precision(symbol, price) + tradeType = self.safe_string(params, 'tradeType') # keep it for backward compatibility + triggerPrice, stopLossPrice, takeProfitPrice = self.handle_trigger_prices(params) + isTriggerOrder = (triggerPrice or stopLossPrice or takeProfitPrice) + isMarginOrder = tradeType == 'MARGIN_TRADE' or marginMode is not None + params = self.omit(params, ['stopLossPrice', 'takeProfitPrice', 'triggerPrice', 'stopPrice']) + if isTriggerOrder: + if triggerPrice: + request['stopPrice'] = self.price_to_precision(symbol, triggerPrice) + elif stopLossPrice or takeProfitPrice: + if stopLossPrice: + request['stop'] = 'entry' if (side == 'buy') else 'loss' + request['stopPrice'] = self.price_to_precision(symbol, stopLossPrice) + else: + request['stop'] = 'loss' if (side == 'buy') else 'entry' + request['stopPrice'] = self.price_to_precision(symbol, takeProfitPrice) + if marginMode == 'isolated': + raise BadRequest(self.id + ' createOrder does not support isolated margin for stop orders') + elif marginMode == 'cross': + request['tradeType'] = self.options['marginModes'][marginMode] + elif isMarginOrder: + if marginMode == 'isolated': + request['marginModel'] = 'isolated' + postOnly = None + postOnly, params = self.handle_post_only(type == 'market', False, params) + if postOnly: + request['postOnly'] = True + return self.extend(request, params) + + async def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + """ + edit an order, kucoin currently only supports the modification of HF orders + + https://docs.kucoin.com/spot-hf/#modify-order + + :param str id: order id + :param str symbol: unified symbol of the market to create an order in + :param str type: not used + :param str side: not used + :param float amount: how much of the currency you want to trade in units of the base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.clientOrderId]: client order id, defaults to id if not passed + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + clientOrderId = self.safe_string_2(params, 'clientOid', 'clientOrderId') + if clientOrderId is not None: + request['clientOid'] = clientOrderId + else: + request['orderId'] = id + if amount is not None: + request['newSize'] = self.amount_to_precision(symbol, amount) + if price is not None: + request['newPrice'] = self.price_to_precision(symbol, price) + response = await self.privatePostHfOrdersAlter(self.extend(request, params)) + # + # { + # "code":"200000", + # "data":{ + # "newOrderId":"6478d7a6c883280001e92d8b" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data, market) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://docs.kucoin.com/spot#cancel-an-order + https://docs.kucoin.com/spot#cancel-an-order-2 + https://docs.kucoin.com/spot#cancel-single-order-by-clientoid + https://docs.kucoin.com/spot#cancel-single-order-by-clientoid-2 + https://docs.kucoin.com/spot-hf/#cancel-orders-by-orderid + https://docs.kucoin.com/spot-hf/#cancel-order-by-clientoid + https://www.kucoin.com/docs/rest/spot-trading/spot-hf-trade-pro-account/sync-cancel-hf-order-by-orderid + https://www.kucoin.com/docs/rest/spot-trading/spot-hf-trade-pro-account/sync-cancel-hf-order-by-clientoid + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.trigger]: True if cancelling a stop order + :param bool [params.hf]: False, # True for hf order + :param bool [params.sync]: False, # True to use the hf sync call + :returns: Response from the exchange + """ + await self.load_markets() + request: dict = {} + clientOrderId = self.safe_string_2(params, 'clientOid', 'clientOrderId') + trigger = self.safe_bool_2(params, 'stop', 'trigger', False) + hf = None + hf, params = self.handle_hf_and_params(params) + useSync = False + useSync, params = self.handle_option_and_params(params, 'cancelOrder', 'sync', False) + if hf or useSync: + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol parameter for hf orders') + market = self.market(symbol) + request['symbol'] = market['id'] + response = None + params = self.omit(params, ['clientOid', 'clientOrderId', 'stop', 'trigger']) + if clientOrderId is not None: + request['clientOid'] = clientOrderId + if trigger: + response = await self.privateDeleteStopOrderCancelOrderByClientOid(self.extend(request, params)) + # + # { + # code: '200000', + # data: { + # cancelledOrderId: 'vs8lgpiuao41iaft003khbbk', + # clientOid: '123456' + # } + # } + # + elif useSync: + response = await self.privateDeleteHfOrdersSyncClientOrderClientOid(self.extend(request, params)) + elif hf: + response = await self.privateDeleteHfOrdersClientOrderClientOid(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "clientOid": "6d539dc614db3" + # } + # } + # + else: + response = await self.privateDeleteOrderClientOrderClientOid(self.extend(request, params)) + # + # { + # code: '200000', + # data: { + # cancelledOrderId: '665e580f6660500007aba341', + # clientOid: '1234567', + # cancelledOcoOrderIds: null + # } + # } + # + response = self.safe_dict(response, 'data') + return self.parse_order(response) + else: + request['orderId'] = id + if trigger: + response = await self.privateDeleteStopOrderOrderId(self.extend(request, params)) + # + # { + # code: '200000', + # data: {cancelledOrderIds: ['vs8lgpiuaco91qk8003vebu9']} + # } + # + elif useSync: + response = await self.privateDeleteHfOrdersSyncOrderId(self.extend(request, params)) + elif hf: + response = await self.privateDeleteHfOrdersOrderId(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "orderId": "630625dbd9180300014c8d52" + # } + # } + # + response = self.safe_dict(response, 'data') + return self.parse_order(response) + else: + response = await self.privateDeleteOrdersOrderId(self.extend(request, params)) + # + # { + # code: '200000', + # data: {cancelledOrderIds: ['665e4fbe28051a0007245c41']} + # } + # + data = self.safe_dict(response, 'data') + orderIds = self.safe_list(data, 'cancelledOrderIds', []) + orderId = self.safe_string(orderIds, 0) + return self.safe_order({ + 'info': data, + 'id': orderId, + }) + + async def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders + + https://docs.kucoin.com/spot#cancel-all-orders + https://docs.kucoin.com/spot#cancel-orders + https://docs.kucoin.com/spot-hf/#cancel-all-hf-orders-by-symbol + + :param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.trigger]: *invalid for isolated margin* True if cancelling all stop orders + :param str [params.marginMode]: 'cross' or 'isolated' + :param str [params.orderIds]: *stop orders only* Comma seperated order IDs + :param bool [params.hf]: False, # True for hf order + :returns: Response from the exchange + """ + await self.load_markets() + request: dict = {} + trigger = self.safe_bool_2(params, 'trigger', 'stop', False) + hf = None + hf, params = self.handle_hf_and_params(params) + params = self.omit(params, 'stop') + marginMode, query = self.handle_margin_mode_and_params('cancelAllOrders', params) + if symbol is not None: + request['symbol'] = self.market_id(symbol) + if marginMode is not None: + request['tradeType'] = self.options['marginModes'][marginMode] + if marginMode == 'isolated' and trigger: + raise BadRequest(self.id + ' cancelAllOrders does not support isolated margin for stop orders') + response = None + if trigger: + response = await self.privateDeleteStopOrderCancel(self.extend(request, query)) + elif hf: + if symbol is None: + response = await self.privateDeleteHfOrdersCancelAll(self.extend(request, query)) + else: + response = await self.privateDeleteHfOrders(self.extend(request, query)) + else: + response = await self.privateDeleteOrders(self.extend(request, query)) + return [self.safe_order({'info': response})] + + async def fetch_orders_by_status(self, status, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch a list of orders + + https://docs.kucoin.com/spot#list-orders + https://docs.kucoin.com/spot#list-stop-orders + https://docs.kucoin.com/spot-hf/#obtain-list-of-active-hf-orders + https://docs.kucoin.com/spot-hf/#obtain-list-of-filled-hf-orders + + :param str status: *not used for stop orders* 'open' or 'closed' + :param str symbol: unified market symbol + :param int [since]: timestamp in ms of the earliest order + :param int [limit]: max number of orders to return + :param dict [params]: exchange specific params + :param int [params.until]: end time in ms + :param str [params.side]: buy or sell + :param str [params.type]: limit, market, limit_stop or market_stop + :param str [params.tradeType]: TRADE for spot trading, MARGIN_TRADE for Margin Trading + :param int [params.currentPage]: *trigger orders only* current page + :param str [params.orderIds]: *trigger orders only* comma seperated order ID list + :param bool [params.trigger]: True if fetching a trigger order + :param bool [params.hf]: False, # True for hf order + :returns: An `array of order structures ` + """ + await self.load_markets() + lowercaseStatus = status.lower() + until = self.safe_integer(params, 'until') + trigger = self.safe_bool_2(params, 'stop', 'trigger', False) + hf = None + hf, params = self.handle_hf_and_params(params) + if hf and (symbol is None): + raise ArgumentsRequired(self.id + ' fetchOrdersByStatus() requires a symbol parameter for hf orders') + params = self.omit(params, ['stop', 'trigger', 'till', 'until']) + marginMode, query = self.handle_margin_mode_and_params('fetchOrdersByStatus', params) + if lowercaseStatus == 'open': + lowercaseStatus = 'active' + elif lowercaseStatus == 'closed': + lowercaseStatus = 'done' + request: dict = { + 'status': lowercaseStatus, + } + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if since is not None: + request['startAt'] = since + if limit is not None: + request['pageSize'] = limit + if until: + request['endAt'] = until + request['tradeType'] = self.safe_string(self.options['marginModes'], marginMode, 'TRADE') + response = None + if trigger: + response = await self.privateGetStopOrder(self.extend(request, query)) + elif hf: + if lowercaseStatus == 'active': + response = await self.privateGetHfOrdersActive(self.extend(request, query)) + elif lowercaseStatus == 'done': + response = await self.privateGetHfOrdersDone(self.extend(request, query)) + else: + response = await self.privateGetOrders(self.extend(request, query)) + # + # { + # "code": "200000", + # "data": { + # "currentPage": 1, + # "pageSize": 1, + # "totalNum": 153408, + # "totalPage": 153408, + # "items": [ + # { + # "id": "5c35c02703aa673ceec2a168", #orderid + # "symbol": "BTC-USDT", #symbol + # "opType": "DEAL", # operation type,deal is pending order,cancel is cancel order + # "type": "limit", # order type,e.g. limit,markrt,stop_limit. + # "side": "buy", # transaction direction,include buy and sell + # "price": "10", # order price + # "size": "2", # order quantity + # "funds": "0", # order funds + # "dealFunds": "0.166", # deal funds + # "dealSize": "2", # deal quantity + # "fee": "0", # fee + # "feeCurrency": "USDT", # charge fee currency + # "stp": "", # self trade prevention,include CN,CO,DC,CB + # "stop": "", # stop type + # "stopTriggered": False, # stop order is triggered + # "stopPrice": "0", # stop price + # "timeInForce": "GTC", # time InForce,include GTC,GTT,IOC,FOK + # "postOnly": False, # postOnly + # "hidden": False, # hidden order + # "iceberg": False, # iceberg order + # "visibleSize": "0", # display quantity for iceberg order + # "cancelAfter": 0, # cancel orders time,requires timeInForce to be GTT + # "channel": "IOS", # order source + # "clientOid": "", # user-entered order unique mark + # "remark": "", # remark + # "tags": "", # tag order source + # "isActive": False, # status before unfilled or uncancelled + # "cancelExist": False, # order cancellation transaction record + # "createdAt": 1547026471000 # time + # }, + # ] + # } + # } + listData = self.safe_list(response, 'data') + if listData is not None: + return self.parse_orders(listData, market, since, limit) + responseData = self.safe_dict(response, 'data', {}) + orders = self.safe_list(responseData, 'items', []) + return self.parse_orders(orders, market, since, limit) + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://docs.kucoin.com/spot#list-orders + https://docs.kucoin.com/spot#list-stop-orders + https://docs.kucoin.com/spot-hf/#obtain-list-of-active-hf-orders + https://docs.kucoin.com/spot-hf/#obtain-list-of-filled-hf-orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: end time in ms + :param str [params.side]: buy or sell + :param str [params.type]: limit, market, limit_stop or market_stop + :param str [params.tradeType]: TRADE for spot trading, MARGIN_TRADE for Margin Trading + :param bool [params.trigger]: True if fetching a trigger order + :param bool [params.hf]: False, # True for hf order + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchClosedOrders', 'paginate') + if paginate: + return await self.fetch_paginated_call_dynamic('fetchClosedOrders', symbol, since, limit, params) + return await self.fetch_orders_by_status('done', symbol, since, limit, params) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://docs.kucoin.com/spot#list-orders + https://docs.kucoin.com/spot#list-stop-orders + https://docs.kucoin.com/spot-hf/#obtain-list-of-active-hf-orders + https://docs.kucoin.com/spot-hf/#obtain-list-of-filled-hf-orders + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: end time in ms + :param bool [params.trigger]: True if fetching trigger orders + :param str [params.side]: buy or sell + :param str [params.type]: limit, market, limit_stop or market_stop + :param str [params.tradeType]: TRADE for spot trading, MARGIN_TRADE for Margin Trading + :param int [params.currentPage]: *trigger orders only* current page + :param str [params.orderIds]: *trigger orders only* comma seperated order ID list + :param bool [params.hf]: False, # True for hf order + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOpenOrders', 'paginate') + if paginate: + return await self.fetch_paginated_call_dynamic('fetchOpenOrders', symbol, since, limit, params) + return await self.fetch_orders_by_status('active', symbol, since, limit, params) + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetch an order + + https://docs.kucoin.com/spot#get-an-order + https://docs.kucoin.com/spot#get-single-active-order-by-clientoid + https://docs.kucoin.com/spot#get-single-order-info + https://docs.kucoin.com/spot#get-single-order-by-clientoid + https://docs.kucoin.com/spot-hf/#details-of-a-single-hf-order + https://docs.kucoin.com/spot-hf/#obtain-details-of-a-single-hf-order-using-clientoid + + :param str id: Order id + :param str symbol: not sent to exchange except for trigger orders with clientOid, but used internally by CCXT to filter + :param dict [params]: exchange specific parameters + :param bool [params.trigger]: True if fetching a trigger order + :param bool [params.hf]: False, # True for hf order + :param bool [params.clientOid]: unique order id created by users to identify their orders + :returns: An `order structure ` + """ + await self.load_markets() + request: dict = {} + clientOrderId = self.safe_string_2(params, 'clientOid', 'clientOrderId') + trigger = self.safe_bool_2(params, 'stop', 'trigger', False) + hf = None + hf, params = self.handle_hf_and_params(params) + market = None + if symbol is not None: + market = self.market(symbol) + if hf: + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrder() requires a symbol parameter for hf orders') + request['symbol'] = market['id'] + params = self.omit(params, ['stop', 'clientOid', 'clientOrderId', 'trigger']) + response = None + if clientOrderId is not None: + request['clientOid'] = clientOrderId + if trigger: + if symbol is not None: + request['symbol'] = market['id'] + response = await self.privateGetStopOrderQueryOrderByClientOid(self.extend(request, params)) + elif hf: + response = await self.privateGetHfOrdersClientOrderClientOid(self.extend(request, params)) + else: + response = await self.privateGetOrderClientOrderClientOid(self.extend(request, params)) + else: + # a special case for None ids + # otherwise a wrong endpoint for all orders will be triggered + # https://github.com/ccxt/ccxt/issues/7234 + if id is None: + raise InvalidOrder(self.id + ' fetchOrder() requires an order id') + request['orderId'] = id + if trigger: + response = await self.privateGetStopOrderOrderId(self.extend(request, params)) + elif hf: + response = await self.privateGetHfOrdersOrderId(self.extend(request, params)) + else: + response = await self.privateGetOrdersOrderId(self.extend(request, params)) + responseData = self.safe_dict(response, 'data', {}) + if isinstance(responseData, list): + responseData = self.safe_value(responseData, 0) + return self.parse_order(responseData, market) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder + # + # { + # "orderId": "63c97e47d686c5000159a656" + # } + # + # cancelOrder + # + # { + # "cancelledOrderIds": ["63c97e47d686c5000159a656"] + # } + # + # fetchOpenOrders, fetchClosedOrders + # + # { + # "id": "63c97ce8d686c500015793bb", + # "symbol": "USDC-USDT", + # "opType": "DEAL", + # "type": "limit", + # "side": "sell", + # "price": "1.05", + # "size": "1", + # "funds": "0", + # "dealFunds": "0", + # "dealSize": "0", + # "fee": "0", + # "feeCurrency": "USDT", + # "stp": "", + # "stop": "", + # "stopTriggered": False, + # "stopPrice": "0", + # "timeInForce": "GTC", + # "postOnly": False, + # "hidden": False, + # "iceberg": False, + # "visibleSize": "0", + # "cancelAfter": 0, + # "channel": "API", + # "clientOid": "d602d73f-5424-4751-bef0-8debce8f0a82", + # "remark": null, + # "tags": "partner:ccxt", + # "isActive": True, + # "cancelExist": False, + # "createdAt": 1674149096927, + # "tradeType": "TRADE" + # } + # + # stop orders(fetchOpenOrders, fetchClosedOrders) + # + # { + # "id": "vs9f6ou9e864rgq8000t4qnm", + # "symbol": "USDC-USDT", + # "userId": "613a896885d8660006151f01", + # "status": "NEW", + # "type": "market", + # "side": "sell", + # "price": null, + # "size": "1.00000000000000000000", + # "funds": null, + # "stp": null, + # "timeInForce": "GTC", + # "cancelAfter": -1, + # "postOnly": False, + # "hidden": False, + # "iceberg": False, + # "visibleSize": null, + # "channel": "API", + # "clientOid": "5d3fd727-6456-438d-9550-40d9d85eee0b", + # "remark": null, + # "tags": "partner:ccxt", + # "relatedNo": null, + # "orderTime": 1674146316994000028, + # "domainId": "kucoin", + # "tradeSource": "USER", + # "tradeType": "MARGIN_TRADE", + # "feeCurrency": "USDT", + # "takerFeeRate": "0.00100000000000000000", + # "makerFeeRate": "0.00100000000000000000", + # "createdAt": 1674146316994, + # "stop": "loss", + # "stopTriggerTime": null, + # "stopPrice": "0.97000000000000000000" + # } + # hf order + # { + # "id":"6478cf1439bdfc0001528a1d", + # "symbol":"LTC-USDT", + # "opType":"DEAL", + # "type":"limit", + # "side":"buy", + # "price":"50", + # "size":"0.1", + # "funds":"5", + # "dealSize":"0", + # "dealFunds":"0", + # "fee":"0", + # "feeCurrency":"USDT", + # "stp":null, + # "timeInForce":"GTC", + # "postOnly":false, + # "hidden":false, + # "iceberg":false, + # "visibleSize":"0", + # "cancelAfter":0, + # "channel":"API", + # "clientOid":"d4d2016b-8e3a-445c-aa5d-dc6df5d1678d", + # "remark":null, + # "tags":"partner:ccxt", + # "cancelExist":false, + # "createdAt":1685638932074, + # "lastUpdatedAt":1685639013735, + # "tradeType":"TRADE", + # "inOrderBook":true, + # "cancelledSize":"0", + # "cancelledFunds":"0", + # "remainSize":"0.1", + # "remainFunds":"5", + # "active":true + # } + # + marketId = self.safe_string(order, 'symbol') + timestamp = self.safe_integer(order, 'createdAt') + feeCurrencyId = self.safe_string(order, 'feeCurrency') + cancelExist = self.safe_bool(order, 'cancelExist', False) + responseStop = self.safe_string(order, 'stop') + trigger = responseStop is not None + stopTriggered = self.safe_bool(order, 'stopTriggered', False) + isActive = self.safe_bool_2(order, 'isActive', 'active') + responseStatus = self.safe_string(order, 'status') + status = None + if isActive is not None: + if isActive is True: + status = 'open' + else: + status = 'closed' + if trigger: + if responseStatus == 'NEW': + status = 'open' + elif not isActive and not stopTriggered: + status = 'cancelled' + if cancelExist: + status = 'canceled' + if responseStatus == 'fail': + status = 'rejected' + return self.safe_order({ + 'info': order, + 'id': self.safe_string_n(order, ['id', 'orderId', 'newOrderId', 'cancelledOrderId']), + 'clientOrderId': self.safe_string(order, 'clientOid'), + 'symbol': self.safe_symbol(marketId, market, '-'), + 'type': self.safe_string(order, 'type'), + 'timeInForce': self.safe_string(order, 'timeInForce'), + 'postOnly': self.safe_bool(order, 'postOnly'), + 'side': self.safe_string(order, 'side'), + 'amount': self.safe_string(order, 'size'), + 'price': self.safe_string(order, 'price'), # price is zero for market order, omitZero is called in safeOrder2 + 'triggerPrice': self.safe_number(order, 'stopPrice'), + 'cost': self.safe_string(order, 'dealFunds'), + 'filled': self.safe_string(order, 'dealSize'), + 'remaining': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'fee': { + 'currency': self.safe_currency_code(feeCurrencyId), + 'cost': self.safe_number(order, 'fee'), + }, + 'status': status, + 'lastTradeTimestamp': None, + 'average': self.safe_string(order, 'avgDealPrice'), + 'trades': None, + }, market) + + async def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all the trades made from a single order + + https://docs.kucoin.com/#list-fills + https://docs.kucoin.com/spot-hf/#transaction-details + + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + request: dict = { + 'orderId': id, + } + return await self.fetch_my_trades(symbol, since, limit, self.extend(request, params)) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + + https://docs.kucoin.com/#list-fills + https://docs.kucoin.com/spot-hf/#transaction-details + + fetch all trades made by the user + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch entries for + :param bool [params.hf]: False, # True for hf order + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchMyTrades', 'paginate') + if paginate: + return await self.fetch_paginated_call_dynamic('fetchMyTrades', symbol, since, limit, params) + request: dict = {} + hf = None + hf, params = self.handle_hf_and_params(params) + if hf and symbol is None: + raise ArgumentsRequired(self.id + ' fetchMyTrades() requires a symbol parameter for hf orders') + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + method = self.options['fetchMyTradesMethod'] + parseResponseData = False + response = None + request, params = self.handle_until_option('endAt', request, params) + if hf: + # does not return trades earlier than 2019-02-18T00:00:00Z + if limit is not None: + request['limit'] = limit + if since is not None: + # only returns trades up to one week after the since param + request['startAt'] = since + response = await self.privateGetHfFills(self.extend(request, params)) + elif method == 'private_get_fills': + # does not return trades earlier than 2019-02-18T00:00:00Z + if since is not None: + # only returns trades up to one week after the since param + request['startAt'] = since + response = await self.privateGetFills(self.extend(request, params)) + elif method == 'private_get_limit_fills': + # does not return trades earlier than 2019-02-18T00:00:00Z + # takes no params + # only returns first 1000 trades(not only "in the last 24 hours" in the docs) + parseResponseData = True + response = await self.privateGetLimitFills(self.extend(request, params)) + else: + raise ExchangeError(self.id + ' fetchMyTradesMethod() invalid method') + # + # { + # "currentPage": 1, + # "pageSize": 50, + # "totalNum": 1, + # "totalPage": 1, + # "items": [ + # { + # "symbol":"BTC-USDT", # symbol + # "tradeId":"5c35c02709e4f67d5266954e", # trade id + # "orderId":"5c35c02703aa673ceec2a168", # order id + # "counterOrderId":"5c1ab46003aa676e487fa8e3", # counter order id + # "side":"buy", # transaction direction,include buy and sell + # "liquidity":"taker", # include taker and maker + # "forceTaker":true, # forced to become taker + # "price":"0.083", # order price + # "size":"0.8424304", # order quantity + # "funds":"0.0699217232", # order funds + # "fee":"0", # fee + # "feeRate":"0", # fee rate + # "feeCurrency":"USDT", # charge fee currency + # "stop":"", # stop type + # "type":"limit", # order type, e.g. limit, market, stop_limit. + # "createdAt":1547026472000 # time + # }, + # #------------------------------------------------------ + # # v1(historical) trade response structure + # { + # "symbol": "SNOV-ETH", + # "dealPrice": "0.0000246", + # "dealValue": "0.018942", + # "amount": "770", + # "fee": "0.00001137", + # "side": "sell", + # "createdAt": 1540080199 + # "id":"5c4d389e4c8c60413f78e2e5", + # } + # ] + # } + # + data = self.safe_dict(response, 'data', {}) + trades = None + if parseResponseData: + trades = data + else: + trades = self.safe_list(data, 'items', []) + return self.parse_trades(trades, market, since, limit) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://www.kucoin.com/docs/rest/spot-trading/market-data/get-trade-histories + https://www.kucoin.com/docs-new/rest/ua/get-trades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + # pagination is not supported on the exchange side anymore + # if since is not None: + # request['startAt'] = int(math.floor(since / 1000)) + # } + # if limit is not None: + # request['pageSize'] = limit + # } + uta = None + uta, params = self.handle_option_and_params(params, 'fetchTrades', 'uta', False) + response = None + trades = None + if uta: + type = None + type, params = self.handle_market_type_and_params('fetchTrades', market, params) + if type == 'spot': + request['tradeType'] = 'SPOT' + else: + request['tradeType'] = 'FUTURES' + response = await self.utaGetMarketTrade(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "tradeType": "SPOT", + # "list": [ + # { + # "sequence": "18746044393340932", + # "tradeId": "18746044393340932", + # "price": "104355.6", + # "size": "0.00011886", + # "side": "sell", + # "ts": 1762242540829000000 + # }, + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + trades = self.safe_list(data, 'list', []) + else: + response = await self.publicGetMarketHistories(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": [ + # { + # "sequence": "1548764654235", + # "side": "sell", + # "size":"0.6841354", + # "price":"0.03202", + # "time":1548848575203567174 + # } + # ] + # } + # + trades = self.safe_list(response, 'data', []) + return self.parse_trades(trades, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(public) + # + # { + # "sequence": "1548764654235", + # "side": "sell", + # "size":"0.6841354", + # "price":"0.03202", + # "time":1548848575203567174 + # } + # + # { + # "sequence": "1568787654360", + # "symbol": "BTC-USDT", + # "side": "buy", + # "size": "0.00536577", + # "price": "9345", + # "takerOrderId": "5e356c4a9f1a790008f8d921", + # "time": "1580559434436443257", + # "type": "match", + # "makerOrderId": "5e356bffedf0010008fa5d7f", + # "tradeId": "5e356c4aeefabd62c62a1ece" + # } + # + # fetchMyTrades(private) v2 + # + # { + # "symbol":"BTC-USDT", + # "tradeId":"5c35c02709e4f67d5266954e", + # "orderId":"5c35c02703aa673ceec2a168", + # "counterOrderId":"5c1ab46003aa676e487fa8e3", + # "side":"buy", + # "liquidity":"taker", + # "forceTaker":true, + # "price":"0.083", + # "size":"0.8424304", + # "funds":"0.0699217232", + # "fee":"0", + # "feeRate":"0", + # "feeCurrency":"USDT", + # "stop":"", + # "type":"limit", + # "createdAt":1547026472000 + # } + # + # fetchMyTrades v2 alternative format since 2019-05-21 https://github.com/ccxt/ccxt/pull/5162 + # + # { + # "symbol": "OPEN-BTC", + # "forceTaker": False, + # "orderId": "5ce36420054b4663b1fff2c9", + # "fee": "0", + # "feeCurrency": "", + # "type": "", + # "feeRate": "0", + # "createdAt": 1558417615000, + # "size": "12.8206", + # "stop": "", + # "price": "0", + # "funds": "0", + # "tradeId": "5ce390cf6e0db23b861c6e80" + # } + # + # fetchMyTrades(private) v1(historical) + # + # { + # "symbol": "SNOV-ETH", + # "dealPrice": "0.0000246", + # "dealValue": "0.018942", + # "amount": "770", + # "fee": "0.00001137", + # "side": "sell", + # "createdAt": 1540080199 + # "id":"5c4d389e4c8c60413f78e2e5", + # } + # + # uta fetchTrades + # + # { + # "sequence": "18746044393340932", + # "tradeId": "18746044393340932", + # "price": "104355.6", + # "size": "0.00011886", + # "side": "sell", + # "ts": 1762242540829000000 + # } + # + marketId = self.safe_string(trade, 'symbol') + market = self.safe_market(marketId, market, '-') + id = self.safe_string_2(trade, 'tradeId', 'id') + orderId = self.safe_string(trade, 'orderId') + takerOrMaker = self.safe_string(trade, 'liquidity') + timestamp = self.safe_integer_2(trade, 'time', 'ts') + if timestamp is not None: + timestamp = self.parse_to_int(timestamp / 1000000) + else: + timestamp = self.safe_integer(trade, 'createdAt') + # if it's a historical v1 trade, the exchange returns timestamp in seconds + if ('dealValue' in trade) and (timestamp is not None): + timestamp = timestamp * 1000 + priceString = self.safe_string_2(trade, 'price', 'dealPrice') + amountString = self.safe_string_2(trade, 'size', 'amount') + side = self.safe_string(trade, 'side') + fee = None + feeCostString = self.safe_string(trade, 'fee') + if feeCostString is not None: + feeCurrencyId = self.safe_string(trade, 'feeCurrency') + feeCurrency = self.safe_currency_code(feeCurrencyId) + if feeCurrency is None: + feeCurrency = market['quote'] if (side == 'sell') else market['base'] + fee = { + 'cost': feeCostString, + 'currency': feeCurrency, + 'rate': self.safe_string(trade, 'feeRate'), + } + type = self.safe_string(trade, 'type') + if type == 'match': + type = None + costString = self.safe_string_2(trade, 'funds', 'dealValue') + return self.safe_trade({ + 'info': trade, + 'id': id, + 'order': orderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'type': type, + 'takerOrMaker': takerOrMaker, + 'side': side, + 'price': priceString, + 'amount': amountString, + 'cost': costString, + 'fee': fee, + }, market) + + async def fetch_trading_fee(self, symbol: str, params={}) -> TradingFeeInterface: + """ + fetch the trading fees for a market + + https://www.kucoin.com/docs/rest/funding/trade-fee/trading-pair-actual-fee-spot-margin-trade_hf + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `fee structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbols': market['id'], + } + response = await self.privateGetTradeFees(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": [ + # { + # "symbol": "BTC-USDT", + # "takerFeeRate": "0.001", + # "makerFeeRate": "0.001" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + first = self.safe_dict(data, 0) + marketId = self.safe_string(first, 'symbol') + return { + 'info': response, + 'symbol': self.safe_symbol(marketId, market), + 'maker': self.safe_number(first, 'makerFeeRate'), + 'taker': self.safe_number(first, 'takerFeeRate'), + 'percentage': True, + 'tierBased': True, + } + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://www.kucoin.com/docs/rest/funding/withdrawals/apply-withdraw-v3- + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + await self.load_markets() + self.check_address(address) + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + 'toAddress': address, + 'withdrawType': 'ADDRESS', + # 'memo': tag, + # 'isInner': False, # internal transfer or external withdrawal + # 'remark': 'optional', + # 'chain': 'OMNI', # 'ERC20', 'TRC20', default is ERC20, This only apply for multi-chain currency, and there is no need for single chain currency. + } + if tag is not None: + request['memo'] = tag + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + if networkCode is not None: + request['chain'] = self.network_code_to_id(networkCode).lower() + request['amount'] = float(self.currency_to_precision(code, amount, networkCode)) + includeFee = None + includeFee, params = self.handle_option_and_params(params, 'withdraw', 'includeFee', False) + if includeFee: + request['feeDeductType'] = 'INTERNAL' + response = await self.privatePostWithdrawals(self.extend(request, params)) + # + # the id is inside "data" + # + # { + # "code": 200000, + # "data": { + # "withdrawalId": "5bffb63303aa675e8bbe18f9" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_transaction(data, currency) + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'SUCCESS': 'ok', + 'PROCESSING': 'pending', + 'WALLET_PROCESSING': 'pending', + 'FAILURE': 'failed', + } + return self.safe_string(statuses, status, status) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fetchDeposits + # + # { + # "address": "0x5f047b29041bcfdbf0e4478cdfa753a336ba6989", + # "memo": "5c247c8a03aa677cea2a251d", + # "amount": 1, + # "fee": 0.0001, + # "currency": "KCS", + # "chain": "", + # "isInner": False, + # "walletTxId": "5bbb57386d99522d9f954c5a@test004", + # "status": "SUCCESS", + # "createdAt": 1544178843000, + # "updatedAt": 1544178891000 + # "remark":"foobar" + # } + # + # fetchWithdrawals + # + # { + # "id": "5c2dc64e03aa675aa263f1ac", + # "address": "0x5bedb060b8eb8d823e2414d82acce78d38be7fe9", + # "memo": "", + # "currency": "ETH", + # "chain": "", + # "amount": 1.0000000, + # "fee": 0.0100000, + # "walletTxId": "3e2414d82acce78d38be7fe9", + # "isInner": False, + # "status": "FAILURE", + # "createdAt": 1546503758000, + # "updatedAt": 1546504603000 + # "remark":"foobar" + # } + # + # withdraw + # + # { + # "withdrawalId": "5bffb63303aa675e8bbe18f9" + # } + # + currencyId = self.safe_string(transaction, 'currency') + code = self.safe_currency_code(currencyId, currency) + address = self.safe_string(transaction, 'address') + amount = self.safe_string(transaction, 'amount') + txid = self.safe_string(transaction, 'walletTxId') + if txid is not None: + txidParts = txid.split('@') + numTxidParts = len(txidParts) + if numTxidParts > 1: + if address is None: + if len(txidParts[1]) > 1: + address = txidParts[1] + txid = txidParts[0] + type = 'withdrawal' if (txid is None) else 'deposit' + rawStatus = self.safe_string(transaction, 'status') + fee = None + feeCost = self.safe_string(transaction, 'fee') + if feeCost is not None: + rate = None + if amount is not None: + rate = Precise.string_div(feeCost, amount) + fee = { + 'cost': self.parse_number(feeCost), + 'rate': self.parse_number(rate), + 'currency': code, + } + timestamp = self.safe_integer_2(transaction, 'createdAt', 'createAt') + updated = self.safe_integer(transaction, 'updatedAt') + isV1 = not ('createdAt' in transaction) + # if it's a v1 structure + if isV1: + type = 'withdrawal' if ('address' in transaction) else 'deposit' + if timestamp is not None: + timestamp = timestamp * 1000 + if updated is not None: + updated = updated * 1000 + internal = self.safe_bool(transaction, 'isInner') + tag = self.safe_string(transaction, 'memo') + return { + 'info': transaction, + 'id': self.safe_string_2(transaction, 'id', 'withdrawalId'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': self.network_id_to_code(self.safe_string(transaction, 'chain')), + 'address': address, + 'addressTo': address, + 'addressFrom': None, + 'tag': tag, + 'tagTo': tag, + 'tagFrom': None, + 'currency': code, + 'amount': self.parse_number(amount), + 'txid': txid, + 'type': type, + 'status': self.parse_transaction_status(rawStatus), + 'comment': self.safe_string(transaction, 'remark'), + 'internal': internal, + 'fee': fee, + 'updated': updated, + } + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://www.kucoin.com/docs/rest/funding/deposit/get-deposit-list + https://www.kucoin.com/docs/rest/funding/deposit/get-v1-historical-deposits-list + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch entries for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchDeposits', 'paginate') + if paginate: + return await self.fetch_paginated_call_dynamic('fetchDeposits', code, since, limit, params) + request: dict = {} + currency = None + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + if limit is not None: + request['pageSize'] = limit + request, params = self.handle_until_option('endAt', request, params) + response = None + if since is not None and since < 1550448000000: + # if since is earlier than 2019-02-18T00:00:00Z + request['startAt'] = self.parse_to_int(since / 1000) + response = await self.privateGetHistDeposits(self.extend(request, params)) + else: + if since is not None: + request['startAt'] = since + response = await self.privateGetDeposits(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "currentPage": 1, + # "pageSize": 5, + # "totalNum": 2, + # "totalPage": 1, + # "items": [ + # #-------------------------------------------------- + # # version 2 deposit response structure + # { + # "address": "0x5f047b29041bcfdbf0e4478cdfa753a336ba6989", + # "memo": "5c247c8a03aa677cea2a251d", + # "amount": 1, + # "fee": 0.0001, + # "currency": "KCS", + # "isInner": False, + # "walletTxId": "5bbb57386d99522d9f954c5a@test004", + # "status": "SUCCESS", + # "createdAt": 1544178843000, + # "updatedAt": 1544178891000 + # "remark":"foobar" + # }, + # #-------------------------------------------------- + # # version 1(historical) deposit response structure + # { + # "currency": "BTC", + # "createAt": 1528536998, + # "amount": "0.03266638", + # "walletTxId": "55c643bc2c68d6f17266383ac1be9e454038864b929ae7cee0bc408cc5c869e8@12ffGWmMMD1zA1WbFm7Ho3JZ1w6NYXjpFk@234", + # "isInner": False, + # "status": "SUCCESS", + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + items = self.safe_list(data, 'items', []) + return self.parse_transactions(items, currency, since, limit, {'type': 'deposit'}) + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://www.kucoin.com/docs/rest/funding/withdrawals/get-withdrawals-list + https://www.kucoin.com/docs/rest/funding/withdrawals/get-v1-historical-withdrawals-list + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch entries for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchWithdrawals', 'paginate') + if paginate: + return await self.fetch_paginated_call_dynamic('fetchWithdrawals', code, since, limit, params) + request: dict = {} + currency = None + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + if limit is not None: + request['pageSize'] = limit + request, params = self.handle_until_option('endAt', request, params) + response = None + if since is not None and since < 1550448000000: + # if since is earlier than 2019-02-18T00:00:00Z + request['startAt'] = self.parse_to_int(since / 1000) + response = await self.privateGetHistWithdrawals(self.extend(request, params)) + else: + if since is not None: + request['startAt'] = since + response = await self.privateGetWithdrawals(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "currentPage": 1, + # "pageSize": 5, + # "totalNum": 2, + # "totalPage": 1, + # "items": [ + # #-------------------------------------------------- + # # version 2 withdrawal response structure + # { + # "id": "5c2dc64e03aa675aa263f1ac", + # "address": "0x5bedb060b8eb8d823e2414d82acce78d38be7fe9", + # "memo": "", + # "currency": "ETH", + # "amount": 1.0000000, + # "fee": 0.0100000, + # "walletTxId": "3e2414d82acce78d38be7fe9", + # "isInner": False, + # "status": "FAILURE", + # "createdAt": 1546503758000, + # "updatedAt": 1546504603000 + # }, + # #-------------------------------------------------- + # # version 1(historical) withdrawal response structure + # { + # "currency": "BTC", + # "createAt": 1526723468, + # "amount": "0.534", + # "address": "33xW37ZSW4tQvg443Pc7NLCAs167Yc2XUV", + # "walletTxId": "aeacea864c020acf58e51606169240e96774838dcd4f7ce48acf38e3651323f4", + # "isInner": False, + # "status": "SUCCESS" + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + items = self.safe_list(data, 'items', []) + return self.parse_transactions(items, currency, since, limit, {'type': 'withdrawal'}) + + def parse_balance_helper(self, entry): + account = self.account() + account['used'] = self.safe_string_2(entry, 'holdBalance', 'hold') + account['free'] = self.safe_string_2(entry, 'availableBalance', 'available') + account['total'] = self.safe_string_2(entry, 'totalBalance', 'total') + debt = self.safe_string(entry, 'liability') + interest = self.safe_string(entry, 'interest') + account['debt'] = Precise.string_add(debt, interest) + return account + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://www.kucoin.com/docs/rest/account/basic-info/get-account-list-spot-margin-trade_hf + https://www.kucoin.com/docs/rest/funding/funding-overview/get-account-detail-margin + https://www.kucoin.com/docs/rest/funding/funding-overview/get-account-detail-isolated-margin + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param dict [params.marginMode]: 'cross' or 'isolated', margin type for fetching margin balance + :param dict [params.type]: extra parameters specific to the exchange API endpoint + :param dict [params.hf]: *default if False* if True, the result includes the balance of the high frequency account + :returns dict: a `balance structure ` + """ + await self.load_markets() + code = self.safe_string(params, 'code') + currency = None + if code is not None: + currency = self.currency(code) + defaultType = self.safe_string_2(self.options, 'fetchBalance', 'defaultType', 'spot') + requestedType = self.safe_string(params, 'type', defaultType) + accountsByType = self.safe_dict(self.options, 'accountsByType') + type = self.safe_string(accountsByType, requestedType, requestedType) + params = self.omit(params, 'type') + hf = None + hf, params = self.handle_hf_and_params(params) + if hf and (type != 'main'): + type = 'trade_hf' + marginMode, query = self.handle_margin_mode_and_params('fetchBalance', params) + response = None + request: dict = {} + isolated = (marginMode == 'isolated') or (type == 'isolated') + cross = (marginMode == 'cross') or (type == 'margin') + if isolated: + if currency is not None: + request['balanceCurrency'] = currency['id'] + response = await self.privateGetIsolatedAccounts(self.extend(request, query)) + elif cross: + response = await self.privateGetMarginAccount(self.extend(request, query)) + else: + if currency is not None: + request['currency'] = currency['id'] + request['type'] = type + response = await self.privateGetAccounts(self.extend(request, query)) + # + # Spot + # + # { + # "code": "200000", + # "data": [ + # { + # "balance": "0.00009788", + # "available": "0.00009788", + # "holds": "0", + # "currency": "BTC", + # "id": "5c6a4fd399a1d81c4f9cc4d0", + # "type": "trade", + # }, + # ] + # } + # + # Cross + # + # { + # "code": "200000", + # "data": { + # "debtRatio": "0", + # "accounts": [ + # { + # "currency": "USDT", + # "totalBalance": "5", + # "availableBalance": "5", + # "holdBalance": "0", + # "liability": "0", + # "maxBorrowSize": "20" + # }, + # ] + # } + # } + # + # Isolated + # + # { + # "code": "200000", + # "data": { + # "totalAssetOfQuoteCurrency": "0", + # "totalLiabilityOfQuoteCurrency": "0", + # "timestamp": 1712085661155, + # "assets": [ + # { + # "symbol": "MANA-USDT", + # "status": "EFFECTIVE", + # "debtRatio": "0", + # "baseAsset": { + # "currency": "MANA", + # "borrowEnabled": True, + # "transferInEnabled": True, + # "total": "0", + # "hold": "0", + # "available": "0", + # "liability": "0", + # "interest": "0", + # "maxBorrowSize": "0" + # }, + # "quoteAsset": { + # "currency": "USDT", + # "borrowEnabled": True, + # "transferInEnabled": True, + # "total": "0", + # "hold": "0", + # "available": "0", + # "liability": "0", + # "interest": "0", + # "maxBorrowSize": "0" + # } + # }, + # ... + # ] + # } + # } + # + data = None + result: dict = { + 'info': response, + 'timestamp': None, + 'datetime': None, + } + if isolated: + data = self.safe_dict(response, 'data', {}) + assets = self.safe_value(data, 'assets', data) + for i in range(0, len(assets)): + entry = assets[i] + marketId = self.safe_string(entry, 'symbol') + symbol = self.safe_symbol(marketId, None, '_') + base = self.safe_dict(entry, 'baseAsset', {}) + quote = self.safe_dict(entry, 'quoteAsset', {}) + baseCode = self.safe_currency_code(self.safe_string(base, 'currency')) + quoteCode = self.safe_currency_code(self.safe_string(quote, 'currency')) + subResult: dict = {} + subResult[baseCode] = self.parse_balance_helper(base) + subResult[quoteCode] = self.parse_balance_helper(quote) + result[symbol] = self.safe_balance(subResult) + elif cross: + data = self.safe_dict(response, 'data', {}) + accounts = self.safe_list(data, 'accounts', []) + for i in range(0, len(accounts)): + balance = accounts[i] + currencyId = self.safe_string(balance, 'currency') + codeInner = self.safe_currency_code(currencyId) + result[codeInner] = self.parse_balance_helper(balance) + else: + data = self.safe_list(response, 'data', []) + for i in range(0, len(data)): + balance = data[i] + balanceType = self.safe_string(balance, 'type') + if balanceType == type: + currencyId = self.safe_string(balance, 'currency') + codeInner2 = self.safe_currency_code(currencyId) + account = self.account() + account['total'] = self.safe_string(balance, 'balance') + account['free'] = self.safe_string(balance, 'available') + account['used'] = self.safe_string(balance, 'holds') + result[codeInner2] = account + returnType = result + if not isolated: + returnType = self.safe_balance(result) + return returnType + + async def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://www.kucoin.com/docs/rest/funding/transfer/inner-transfer + https://docs.kucoin.com/futures/#transfer-funds-to-kucoin-main-account-2 + https://docs.kucoin.com/spot-hf/#internal-funds-transfers-in-high-frequency-trading-accounts + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account to transfer from + :param str toAccount: account to transfer to + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transfer structure ` + """ + await self.load_markets() + currency = self.currency(code) + requestedAmount = self.currency_to_precision(code, amount) + fromId = self.convert_type_to_account(fromAccount) + toId = self.convert_type_to_account(toAccount) + fromIsolated = self.in_array(fromId, self.ids) + toIsolated = self.in_array(toId, self.ids) + if fromId == 'contract': + if toId != 'main': + raise ExchangeError(self.id + ' transfer() only supports transferring from futures account to main account') + request: dict = { + 'currency': currency['id'], + 'amount': requestedAmount, + } + if not ('bizNo' in params): + # it doesn't like more than 24 characters + request['bizNo'] = self.uuid22() + response = await self.futuresPrivatePostTransferOut(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "applyId": "605a87217dff1500063d485d", + # "bizNo": "bcd6e5e1291f4905af84dc", + # "payAccountType": "CONTRACT", + # "payTag": "DEFAULT", + # "remark": '', + # "recAccountType": "MAIN", + # "recTag": "DEFAULT", + # "recRemark": '', + # "recSystem": "KUCOIN", + # "status": "PROCESSING", + # "currency": "XBT", + # "amount": "0.00001", + # "fee": "0", + # "sn": "573688685663948", + # "reason": '', + # "createdAt": 1616545569000, + # "updatedAt": 1616545569000 + # } + # } + # + data = self.safe_dict(response, 'data') + return self.parse_transfer(data, currency) + else: + request: dict = { + 'currency': currency['id'], + 'amount': requestedAmount, + } + if fromIsolated or toIsolated: + if self.in_array(fromId, self.ids): + request['fromTag'] = fromId + fromId = 'isolated' + if self.in_array(toId, self.ids): + request['toTag'] = toId + toId = 'isolated' + request['from'] = fromId + request['to'] = toId + if not ('clientOid' in params): + request['clientOid'] = self.uuid() + response = await self.privatePostAccountsInnerTransfer(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "orderId": "605a6211e657f00006ad0ad6" + # } + # } + # + data = self.safe_dict(response, 'data') + return self.parse_transfer(data, currency) + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + # + # transfer(spot) + # + # { + # "orderId": "605a6211e657f00006ad0ad6" + # } + # + # { + # "code": "200000", + # "msg": "Failed to transfer out. The amount exceeds the upper limit" + # } + # + # transfer(futures) + # + # { + # "applyId": "605a87217dff1500063d485d", + # "bizNo": "bcd6e5e1291f4905af84dc", + # "payAccountType": "CONTRACT", + # "payTag": "DEFAULT", + # "remark": '', + # "recAccountType": "MAIN", + # "recTag": "DEFAULT", + # "recRemark": '', + # "recSystem": "KUCOIN", + # "status": "PROCESSING", + # "currency": "XBT", + # "amount": "0.00001", + # "fee": "0", + # "sn": "573688685663948", + # "reason": '', + # "createdAt": 1616545569000, + # "updatedAt": 1616545569000 + # } + # + timestamp = self.safe_integer(transfer, 'createdAt') + currencyId = self.safe_string(transfer, 'currency') + rawStatus = self.safe_string(transfer, 'status') + accountFromRaw = self.safe_string_lower(transfer, 'payAccountType') + accountToRaw = self.safe_string_lower(transfer, 'recAccountType') + accountsByType = self.safe_dict(self.options, 'accountsByType') + accountFrom = self.safe_string(accountsByType, accountFromRaw, accountFromRaw) + accountTo = self.safe_string(accountsByType, accountToRaw, accountToRaw) + return { + 'id': self.safe_string_2(transfer, 'applyId', 'orderId'), + 'currency': self.safe_currency_code(currencyId, currency), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'amount': self.safe_number(transfer, 'amount'), + 'fromAccount': accountFrom, + 'toAccount': accountTo, + 'status': self.parse_transfer_status(rawStatus), + 'info': transfer, + } + + def parse_transfer_status(self, status: Str) -> Str: + statuses: dict = { + 'PROCESSING': 'pending', + } + return self.safe_string(statuses, status, status) + + def parse_ledger_entry_type(self, type): + types: dict = { + 'Assets Transferred in After Upgrading': 'transfer', # Assets Transferred in After V1 to V2 Upgrading + 'Deposit': 'transaction', # Deposit + 'Withdrawal': 'transaction', # Withdrawal + 'Transfer': 'transfer', # Transfer + 'Trade_Exchange': 'trade', # Trade + # 'Vote for Coin': 'Vote for Coin', # Vote for Coin + 'KuCoin Bonus': 'bonus', # KuCoin Bonus + 'Referral Bonus': 'referral', # Referral Bonus + 'Rewards': 'bonus', # Activities Rewards + # 'Distribution': 'Distribution', # Distribution, such GAS by holding NEO + 'Airdrop/Fork': 'airdrop', # Airdrop/Fork + 'Other rewards': 'bonus', # Other rewards, except Vote, Airdrop, Fork + 'Fee Rebate': 'rebate', # Fee Rebate + 'Buy Crypto': 'trade', # Use credit card to buy crypto + 'Sell Crypto': 'sell', # Use credit card to sell crypto + 'Public Offering Purchase': 'trade', # Public Offering Purchase for Spotlight + # 'Send red envelope': 'Send red envelope', # Send red envelope + # 'Open red envelope': 'Open red envelope', # Open red envelope + # 'Staking': 'Staking', # Staking + # 'LockDrop Vesting': 'LockDrop Vesting', # LockDrop Vesting + # 'Staking Profits': 'Staking Profits', # Staking Profits + # 'Redemption': 'Redemption', # Redemption + 'Refunded Fees': 'fee', # Refunded Fees + 'KCS Pay Fees': 'fee', # KCS Pay Fees + 'Margin Trade': 'trade', # Margin Trade + 'Loans': 'Loans', # Loans + # 'Borrowings': 'Borrowings', # Borrowings + # 'Debt Repayment': 'Debt Repayment', # Debt Repayment + # 'Loans Repaid': 'Loans Repaid', # Loans Repaid + # 'Lendings': 'Lendings', # Lendings + # 'Pool transactions': 'Pool transactions', # Pool-X transactions + 'Instant Exchange': 'trade', # Instant Exchange + 'Sub-account transfer': 'transfer', # Sub-account transfer + 'Liquidation Fees': 'fee', # Liquidation Fees + # 'Soft Staking Profits': 'Soft Staking Profits', # Soft Staking Profits + # 'Voting Earnings': 'Voting Earnings', # Voting Earnings on Pool-X + # 'Redemption of Voting': 'Redemption of Voting', # Redemption of Voting on Pool-X + # 'Voting': 'Voting', # Voting on Pool-X + # 'Convert to KCS': 'Convert to KCS', # Convert to KCS + } + return self.safe_string(types, type, type) + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + # + # { + # "id": "611a1e7c6a053300067a88d9", #unique key for each ledger entry + # "currency": "USDT", #Currency + # "amount": "10.00059547", #The total amount of assets(fees included) involved in assets changes such, withdrawal and bonus distribution. + # "fee": "0", #Deposit or withdrawal fee + # "balance": "0", #Total assets of a currency remaining funds after transaction + # "accountType": "MAIN", #Account Type + # "bizType": "Loans Repaid", #business type + # "direction": "in", #side, in or out + # "createdAt": 1629101692950, #Creation time + # "context": "{\"borrowerUserId\":\"601ad03e50dc810006d242ea\",\"loanRepayDetailNo\":\"611a1e7cc913d000066cf7ec\"}" #Business core parameters + # } + # + id = self.safe_string(item, 'id') + currencyId = self.safe_string(item, 'currency') + code = self.safe_currency_code(currencyId, currency) + currency = self.safe_currency(currencyId, currency) + amount = self.safe_number(item, 'amount') + balanceAfter = None + # balanceAfter = self.safe_number(item, 'balance'); only returns zero string + bizType = self.safe_string(item, 'bizType') + type = self.parse_ledger_entry_type(bizType) + direction = self.safe_string(item, 'direction') + timestamp = self.safe_integer(item, 'createdAt') + datetime = self.iso8601(timestamp) + account = self.safe_string(item, 'accountType') # MAIN, TRADE, MARGIN, or CONTRACT + context = self.safe_string(item, 'context') # contains other information about the ledger entry + # + # withdrawal transaction + # + # "{\"orderId\":\"617bb2d09e7b3b000196dac8\",\"txId\":\"0x79bb9855f86b351a45cab4dc69d78ca09586a94c45dde49475722b98f401b054\"}" + # + # deposit to MAIN, trade via MAIN + # + # "{\"orderId\":\"617ab9949e7b3b0001948081\",\"txId\":\"0x7a06b16bbd6b03dbc3d96df5683b15229fc35e7184fd7179a5f3a310bd67d1fa@default@0\"}" + # + # sell trade + # + # "{\"symbol\":\"ETH-USDT\",\"orderId\":\"617adcd1eb3fa20001dd29a1\",\"tradeId\":\"617adcd12e113d2b91222ff9\"}" + # + referenceId = None + if context is not None and context != '': + try: + parsed = json.loads(context) + orderId = self.safe_string(parsed, 'orderId') + tradeId = self.safe_string(parsed, 'tradeId') + # transactions only have an orderId but for trades we wish to use tradeId + if tradeId is not None: + referenceId = tradeId + else: + referenceId = orderId + except Exception as exc: + referenceId = context + fee = None + feeCost = self.safe_string(item, 'fee') + feeCurrency = None + if feeCost != '0': + feeCurrency = code + fee = {'cost': self.parse_number(feeCost), 'currency': feeCurrency} + return self.safe_ledger_entry({ + 'info': item, + 'id': id, + 'direction': direction, + 'account': account, + 'referenceId': referenceId, + 'referenceAccount': account, + 'type': type, + 'currency': code, + 'amount': amount, + 'timestamp': timestamp, + 'datetime': datetime, + 'before': None, + 'after': balanceAfter, # None + 'status': None, + 'fee': fee, + }, currency) + + async def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + + https://www.kucoin.com/docs/rest/account/basic-info/get-account-ledgers-spot-margin + https://www.kucoin.com/docs/rest/account/basic-info/get-account-ledgers-trade_hf + https://www.kucoin.com/docs/rest/account/basic-info/get-account-ledgers-margin_hf + + :param str [code]: unified currency code, default is None + :param int [since]: timestamp in ms of the earliest ledger entry, default is None + :param int [limit]: max number of ledger entries to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.hf]: default False, when True will fetch ledger entries for the high frequency trading account + :param int [params.until]: the latest time in ms to fetch entries for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict: a `ledger structure ` + """ + await self.load_markets() + await self.load_accounts() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchLedger', 'paginate') + hf = None + hf, params = self.handle_hf_and_params(params) + if paginate: + return await self.fetch_paginated_call_dynamic('fetchLedger', code, since, limit, params) + request: dict = { + # 'currency': currency['id'], # can choose up to 10, if not provided returns for all currencies by default + # 'direction': 'in', # 'out' + # 'bizType': 'DEPOSIT', # DEPOSIT, WITHDRAW, TRANSFER, SUB_TRANSFER,TRADE_EXCHANGE, MARGIN_EXCHANGE, KUCOIN_BONUS(optional) + # 'startAt': since, + # 'endAt': exchange.milliseconds(), + } + if since is not None: + request['startAt'] = since + # atm only single currency retrieval is supported + currency = None + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + request, params = self.handle_until_option('endAt', request, params) + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchLedger', params) + response = None + if hf: + if marginMode is not None: + response = await self.privateGetHfMarginAccountLedgers(self.extend(request, params)) + else: + response = await self.privateGetHfAccountsLedgers(self.extend(request, params)) + else: + response = await self.privateGetAccountsLedgers(self.extend(request, params)) + # + # { + # "code":"200000", + # "data":{ + # "currentPage":1, + # "pageSize":50, + # "totalNum":1, + # "totalPage":1, + # "items":[ + # { + # "id":"617cc528729f5f0001c03ceb", + # "currency":"GAS", + # "amount":"0.00000339", + # "fee":"0", + # "balance":"0", + # "accountType":"MAIN", + # "bizType":"Distribution", + # "direction":"in", + # "createdAt":1635566888183, + # "context":"{\"orderId\":\"617cc47a1c47ed0001ce3606\",\"description\":\"Holding NEO,distribute GAS(2021/10/30)\"}" + # } + # { + # "id": "611a1e7c6a053300067a88d9",//unique key + # "currency": "USDT", #Currency + # "amount": "10.00059547", #Change amount of the funds + # "fee": "0", #Deposit or withdrawal fee + # "balance": "0", #Total assets of a currency + # "accountType": "MAIN", #Account Type + # "bizType": "Loans Repaid", #business type + # "direction": "in", #side, in or out + # "createdAt": 1629101692950, #Creation time + # "context": "{\"borrowerUserId\":\"601ad03e50dc810006d242ea\",\"loanRepayDetailNo\":\"611a1e7cc913d000066cf7ec\"}" + # }, + # ] + # } + # } + # + dataList = self.safe_list(response, 'data') + if dataList is not None: + return self.parse_ledger(dataList, currency, since, limit) + data = self.safe_dict(response, 'data') + items = self.safe_list(data, 'items', []) + return self.parse_ledger(items, currency, since, limit) + + def calculate_rate_limiter_cost(self, api, method, path, params, config={}): + versions = self.safe_dict(self.options, 'versions', {}) + apiVersions = self.safe_dict(versions, api, {}) + methodVersions = self.safe_dict(apiVersions, method, {}) + defaultVersion = self.safe_string(methodVersions, path, self.options['version']) + version = self.safe_string(params, 'version', defaultVersion) + if version == 'v3' and ('v3' in config): + return config['v3'] + elif version == 'v2' and ('v2' in config): + return config['v2'] + elif version == 'v1' and ('v1' in config): + return config['v1'] + return self.safe_value(config, 'cost', 1) + + def parse_borrow_rate(self, info, currency: Currency = None): + # + # { + # "tradeId": "62db2dcaff219600012b56cd", + # "currency": "USDT", + # "size": "10", + # "dailyIntRate": "0.00003", + # "term": 7, + # "timestamp": 1658531274508488480 + # }, + # + # { + # "createdAt": 1697783812257, + # "currency": "XMR", + # "interestAmount": "0.1", + # "dayRatio": "0.001" + # } + # + timestampId = self.safe_string_2(info, 'createdAt', 'timestamp') + timestamp = self.parse_to_int(timestampId[0:13]) + currencyId = self.safe_string(info, 'currency') + return { + 'currency': self.safe_currency_code(currencyId, currency), + 'rate': self.safe_number_2(info, 'dailyIntRate', 'dayRatio'), + 'period': 86400000, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'info': info, + } + + async def fetch_borrow_interest(self, code: Str = None, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[BorrowInterest]: + """ + fetch the interest owed by the user for borrowing currency for margin trading + + https://docs.kucoin.com/#get-repay-record + https://docs.kucoin.com/#query-isolated-margin-account-info + + :param str [code]: unified currency code + :param str [symbol]: unified market symbol, required for isolated margin + :param int [since]: the earliest time in ms to fetch borrrow interest for + :param int [limit]: the maximum number of structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' default is 'cross' + :returns dict[]: a list of `borrow interest structures ` + """ + await self.load_markets() + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchBorrowInterest', params, 'cross') + request: dict = {} + currency = None + if code is not None: + currency = self.currency(code) + if marginMode == 'isolated': + request['balanceCurrency'] = currency['id'] + else: + request['quoteCurrency'] = currency['id'] + market = None + if symbol is not None: + market = self.market(symbol) + response = None + if marginMode == 'isolated': + response = await self.privateGetIsolatedAccounts(self.extend(request, params)) + else: + response = await self.privateGetMarginAccounts(self.extend(request, params)) + # + # Cross + # + # { + # "code": "200000", + # "data": { + # "totalAssetOfQuoteCurrency": "0", + # "totalLiabilityOfQuoteCurrency": "0", + # "debtRatio": "0", + # "status": "EFFECTIVE", + # "accounts": [ + # { + # "currency": "1INCH", + # "total": "0", + # "available": "0", + # "hold": "0", + # "liability": "0", + # "maxBorrowSize": "0", + # "borrowEnabled": True, + # "transferInEnabled": True + # } + # ] + # } + # } + # + # Isolated + # + # { + # "code": "200000", + # "data": { + # "totalConversionBalance": "0.02138647", + # "liabilityConversionBalance": "0.01480001", + # "assets": [ + # { + # "symbol": "MANA-USDT", + # "debtRatio": "0", + # "status": "BORROW", + # "baseAsset": { + # "currency": "MANA", + # "borrowEnabled": True, + # "repayEnabled": True, + # "transferEnabled": True, + # "borrowed": "0", + # "totalAsset": "0", + # "available": "0", + # "hold": "0", + # "maxBorrowSize": "1000" + # }, + # "quoteAsset": { + # "currency": "USDT", + # "borrowEnabled": True, + # "repayEnabled": True, + # "transferEnabled": True, + # "borrowed": "0", + # "totalAsset": "0", + # "available": "0", + # "hold": "0", + # "maxBorrowSize": "50000" + # } + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + assets = self.safe_list(data, 'assets', []) if (marginMode == 'isolated') else self.safe_list(data, 'accounts', []) + interest = self.parse_borrow_interests(assets, market) + filteredByCurrency = self.filter_by_currency_since_limit(interest, code, since, limit) + return self.filter_by_symbol_since_limit(filteredByCurrency, symbol, since, limit) + + def parse_borrow_interest(self, info: dict, market: Market = None) -> BorrowInterest: + # + # Cross + # + # { + # "currency": "1INCH", + # "total": "0", + # "available": "0", + # "hold": "0", + # "liability": "0", + # "maxBorrowSize": "0", + # "borrowEnabled": True, + # "transferInEnabled": True + # } + # + # Isolated + # + # { + # "symbol": "MANA-USDT", + # "debtRatio": "0", + # "status": "BORROW", + # "baseAsset": { + # "currency": "MANA", + # "borrowEnabled": True, + # "repayEnabled": True, + # "transferEnabled": True, + # "borrowed": "0", + # "totalAsset": "0", + # "available": "0", + # "hold": "0", + # "maxBorrowSize": "1000" + # }, + # "quoteAsset": { + # "currency": "USDT", + # "borrowEnabled": True, + # "repayEnabled": True, + # "transferEnabled": True, + # "borrowed": "0", + # "totalAsset": "0", + # "available": "0", + # "hold": "0", + # "maxBorrowSize": "50000" + # } + # } + # + marketId = self.safe_string(info, 'symbol') + marginMode = 'cross' if (marketId is None) else 'isolated' + market = self.safe_market(marketId, market) + symbol = self.safe_string(market, 'symbol') + timestamp = self.safe_integer(info, 'createdAt') + isolatedBase = self.safe_dict(info, 'baseAsset', {}) + amountBorrowed = None + interest = None + currencyId = None + if marginMode == 'isolated': + amountBorrowed = self.safe_number(isolatedBase, 'liability') + interest = self.safe_number(isolatedBase, 'interest') + currencyId = self.safe_string(isolatedBase, 'currency') + else: + amountBorrowed = self.safe_number(info, 'liability') + interest = self.safe_number(info, 'accruedInterest') + currencyId = self.safe_string(info, 'currency') + return { + 'info': info, + 'symbol': symbol, + 'currency': self.safe_currency_code(currencyId), + 'interest': interest, + 'interestRate': self.safe_number(info, 'dailyIntRate'), + 'amountBorrowed': amountBorrowed, + 'marginMode': marginMode, + 'timestamp': timestamp, # create time + 'datetime': self.iso8601(timestamp), + } + + async def fetch_borrow_rate_histories(self, codes=None, since: Int = None, limit: Int = None, params={}): + """ + retrieves a history of a multiple currencies borrow interest rate at specific time slots, returns all currencies if no symbols passed, default is None + + https://www.kucoin.com/docs/rest/margin-trading/margin-trading-v3-/get-cross-isolated-margin-interest-records + + :param str[]|None codes: list of unified currency codes, default is None + :param int [since]: timestamp in ms of the earliest borrowRate, default is None + :param int [limit]: max number of borrow rate prices to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' default is 'cross' + :param int [params.until]: the latest time in ms to fetch entries for + :returns dict: a dictionary of `borrow rate structures ` indexed by the market symbol + """ + await self.load_markets() + marginResult = self.handle_margin_mode_and_params('fetchBorrowRateHistories', params) + marginMode = self.safe_string(marginResult, 0, 'cross') + isIsolated = (marginMode == 'isolated') # True-isolated, False-cross + request: dict = { + 'isIsolated': isIsolated, + } + if since is not None: + request['startTime'] = since + request, params = self.handle_until_option('endTime', request, params) + if limit is not None: + request['pageSize'] = limit # default:50, min:10, max:500 + response = await self.privateGetMarginInterest(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "timestamp": 1710829939673, + # "currentPage": 1, + # "pageSize": 50, + # "totalNum": 0, + # "totalPage": 0, + # "items": [ + # { + # "createdAt": 1697783812257, + # "currency": "XMR", + # "interestAmount": "0.1", + # "dayRatio": "0.001" + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'data') + rows = self.safe_list(data, 'items', []) + return self.parse_borrow_rate_histories(rows, codes, since, limit) + + async def fetch_borrow_rate_history(self, code: str, since: Int = None, limit: Int = None, params={}): + """ + retrieves a history of a currencies borrow interest rate at specific time slots + + https://www.kucoin.com/docs/rest/margin-trading/margin-trading-v3-/get-cross-isolated-margin-interest-records + + :param str code: unified currency code + :param int [since]: timestamp for the earliest borrow rate + :param int [limit]: the maximum number of `borrow rate structures ` to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' default is 'cross' + :param int [params.until]: the latest time in ms to fetch entries for + :returns dict[]: an array of `borrow rate structures ` + """ + await self.load_markets() + marginResult = self.handle_margin_mode_and_params('fetchBorrowRateHistories', params) + marginMode = self.safe_string(marginResult, 0, 'cross') + isIsolated = (marginMode == 'isolated') # True-isolated, False-cross + currency = self.currency(code) + request: dict = { + 'isIsolated': isIsolated, + 'currency': currency['id'], + } + if since is not None: + request['startTime'] = since + request, params = self.handle_until_option('endTime', request, params) + if limit is not None: + request['pageSize'] = limit # default:50, min:10, max:500 + response = await self.privateGetMarginInterest(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "timestamp": 1710829939673, + # "currentPage": 1, + # "pageSize": 50, + # "totalNum": 0, + # "totalPage": 0, + # "items": [ + # { + # "createdAt": 1697783812257, + # "currency": "XMR", + # "interestAmount": "0.1", + # "dayRatio": "0.001" + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'data') + rows = self.safe_list(data, 'items', []) + return self.parse_borrow_rate_history(rows, code, since, limit) + + def parse_borrow_rate_histories(self, response, codes, since, limit): + # + # [ + # { + # "createdAt": 1697783812257, + # "currency": "XMR", + # "interestAmount": "0.1", + # "dayRatio": "0.001" + # } + # ] + # + borrowRateHistories: dict = {} + for i in range(0, len(response)): + item = response[i] + code = self.safe_currency_code(self.safe_string(item, 'currency')) + if codes is None or self.in_array(code, codes): + if not (code in borrowRateHistories): + borrowRateHistories[code] = [] + borrowRateStructure = self.parse_borrow_rate(item) + borrowRateHistoriesCode = borrowRateHistories[code] + borrowRateHistoriesCode.append(borrowRateStructure) + keys = list(borrowRateHistories.keys()) + for i in range(0, len(keys)): + code = keys[i] + borrowRateHistories[code] = self.filter_by_currency_since_limit(borrowRateHistories[code], code, since, limit) + return borrowRateHistories + + async def borrow_cross_margin(self, code: str, amount: float, params={}): + """ + create a loan to borrow margin + + https://docs.kucoin.com/#1-margin-borrowing + + :param str code: unified currency code of the currency to borrow + :param float amount: the amount to borrow + :param dict [params]: extra parameters specific to the exchange API endpoints + :param str [params.timeInForce]: either IOC or FOK + :returns dict: a `margin loan structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + 'size': self.currency_to_precision(code, amount), + 'timeInForce': 'FOK', + } + response = await self.privatePostMarginBorrow(self.extend(request, params)) + # + # { + # "success": True, + # "code": "200", + # "msg": "success", + # "retry": False, + # "data": { + # "orderNo": "5da6dba0f943c0c81f5d5db5", + # "actualSize": 10 + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_margin_loan(data, currency) + + async def borrow_isolated_margin(self, symbol: str, code: str, amount: float, params={}): + """ + create a loan to borrow margin + + https://docs.kucoin.com/#1-margin-borrowing + + :param str symbol: unified market symbol, required for isolated margin + :param str code: unified currency code of the currency to borrow + :param float amount: the amount to borrow + :param dict [params]: extra parameters specific to the exchange API endpoints + :param str [params.timeInForce]: either IOC or FOK + :returns dict: a `margin loan structure ` + """ + await self.load_markets() + market = self.market(symbol) + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + 'size': self.currency_to_precision(code, amount), + 'symbol': market['id'], + 'timeInForce': 'FOK', + 'isIsolated': True, + } + response = await self.privatePostMarginBorrow(self.extend(request, params)) + # + # { + # "success": True, + # "code": "200", + # "msg": "success", + # "retry": False, + # "data": { + # "orderNo": "5da6dba0f943c0c81f5d5db5", + # "actualSize": 10 + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_margin_loan(data, currency) + + async def repay_cross_margin(self, code: str, amount, params={}): + """ + repay borrowed margin and interest + + https://docs.kucoin.com/#2-repayment + + :param str code: unified currency code of the currency to repay + :param float amount: the amount to repay + :param dict [params]: extra parameters specific to the exchange API endpoints + :returns dict: a `margin loan structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + 'size': self.currency_to_precision(code, amount), + } + response = await self.privatePostMarginRepay(self.extend(request, params)) + # + # { + # "success": True, + # "code": "200", + # "msg": "success", + # "retry": False, + # "data": { + # "orderNo": "5da6dba0f943c0c81f5d5db5", + # "actualSize": 10 + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_margin_loan(data, currency) + + async def repay_isolated_margin(self, symbol: str, code: str, amount, params={}): + """ + repay borrowed margin and interest + + https://docs.kucoin.com/#2-repayment + + :param str symbol: unified market symbol + :param str code: unified currency code of the currency to repay + :param float amount: the amount to repay + :param dict [params]: extra parameters specific to the exchange API endpoints + :returns dict: a `margin loan structure ` + """ + await self.load_markets() + market = self.market(symbol) + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + 'size': self.currency_to_precision(code, amount), + 'symbol': market['id'], + 'isIsolated': True, + } + response = await self.privatePostMarginRepay(self.extend(request, params)) + # + # { + # "success": True, + # "code": "200", + # "msg": "success", + # "retry": False, + # "data": { + # "orderNo": "5da6dba0f943c0c81f5d5db5", + # "actualSize": 10 + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_margin_loan(data, currency) + + def parse_margin_loan(self, info, currency: Currency = None): + # + # { + # "orderNo": "5da6dba0f943c0c81f5d5db5", + # "actualSize": 10 + # } + # + timestamp = self.milliseconds() + currencyId = self.safe_string(info, 'currency') + return { + 'id': self.safe_string(info, 'orderNo'), + 'currency': self.safe_currency_code(currencyId, currency), + 'amount': self.safe_number(info, 'actualSize'), + 'symbol': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'info': info, + } + + async def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}): + """ + fetch deposit and withdraw fees - *IMPORTANT* use fetchDepositWithdrawFee to get more in-depth info + + https://docs.kucoin.com/#get-currencies + + :param str[]|None codes: list of unified currency codes + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `fee structures ` + """ + await self.load_markets() + response = await self.publicGetCurrencies(params) + # + # [ + # { + # "currency": "CSP", + # "name": "CSP", + # "fullName": "Caspian", + # "precision": 8, + # "confirms": 12, + # "contractAddress": "0xa6446d655a0c34bc4f05042ee88170d056cbaf45", + # "withdrawalMinSize": "2000", + # "withdrawalMinFee": "1000", + # "isWithdrawEnabled": True, + # "isDepositEnabled": True, + # "isMarginEnabled": False, + # "isDebitEnabled": False + # }, + # ] + # + data = self.safe_list(response, 'data', []) + return self.parse_deposit_withdraw_fees(data, codes, 'currency') + + async def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://www.kucoin.com/docs/rest/margin-trading/margin-trading-v3-/modify-leverage-multiplier + + :param int [leverage]: New leverage multiplier. Must be greater than 1 and up to two decimal places, and cannot be less than the user's current debt leverage or greater than the system's maximum leverage + :param str [symbol]: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + await self.load_markets() + market = None + marketType: Str = None + marketType, params = self.handle_market_type_and_params('setLeverage', None, params) + if (symbol is not None) or marketType != 'spot': + market = self.market(symbol) + if market['contract']: + raise NotSupported(self.id + ' setLeverage currently supports only spot margin') + marginMode: Str = None + marginMode, params = self.handle_margin_mode_and_params('setLeverage', params) + if marginMode is None: + raise ArgumentsRequired(self.id + ' setLeverage requires a marginMode parameter') + request: dict = {} + if marginMode == 'isolated' and symbol is None: + raise ArgumentsRequired(self.id + ' setLeverage requires a symbol parameter for isolated margin') + if symbol is not None: + request['symbol'] = market['id'] + request['leverage'] = str(leverage) + request['isIsolated'] = (marginMode == 'isolated') + return await self.privatePostPositionUpdateUserLeverage(self.extend(request, params)) + + async def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate + + https://www.kucoin.com/docs-new/rest/ua/get-current-funding-rate + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = await self.utaGetMarketFundingRate(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "symbol": ".XBTUSDTMFPI8H", + # "nextFundingRate": 7.4E-5, + # "fundingTime": 1762444800000, + # "fundingRateCap": 0.003, + # "fundingRateFloor": -0.003 + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_funding_rate(data, market) + + def parse_funding_rate(self, data, market: Market = None) -> FundingRate: + # + # { + # "symbol": ".XBTUSDTMFPI8H", + # "nextFundingRate": 7.4E-5, + # "fundingTime": 1762444800000, + # "fundingRateCap": 0.003, + # "fundingRateFloor": -0.003 + # } + # + fundingTimestamp = self.safe_integer(data, 'fundingTime') + marketId = self.safe_string(data, 'symbol') + return { + 'info': data, + 'symbol': self.safe_symbol(marketId, market, None, 'contract'), + 'markPrice': None, + 'indexPrice': None, + 'interestRate': None, + 'estimatedSettlePrice': None, + 'timestamp': None, + 'datetime': None, + 'fundingRate': self.safe_number(data, 'nextFundingRate'), + 'fundingTimestamp': fundingTimestamp, + 'fundingDatetime': self.iso8601(fundingTimestamp), + 'nextFundingRate': None, + 'nextFundingTimestamp': None, + 'nextFundingDatetime': None, + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': None, + } + + async def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical funding rate prices + + https://www.kucoin.com/docs-new/rest/ua/get-history-funding-rate + + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: not used by kucuoinfutures + :param int [limit]: the maximum amount of `funding rate structures ` to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: end time in ms + :returns dict[]: a list of `funding rate structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchFundingRateHistory() requires a symbol argument') + if since is None: + raise ArgumentsRequired(self.id + ' fetchFundingRateHistory() requires a since argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + until = self.safe_integer(params, 'until') + params = self.omit(params, 'until') + if since is not None: + request['startAt'] = since + if until is None: + request['endAt'] = self.milliseconds() + if until is not None: + request['endAt'] = until + response = await self.utaGetMarketFundingRateHistory(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "symbol": "XBTUSDTM", + # "list": [ + # { + # "fundingRate": 7.6E-5, + # "ts": 1706097600000 + # }, + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + result = self.safe_list(data, 'list', []) + return self.parse_funding_rate_histories(result, market, since, limit) + + def parse_funding_rate_history(self, info, market: Market = None): + # + # { + # "fundingRate": 7.6E-5, + # "ts": 1706097600000 + # } + # + timestamp = self.safe_integer(info, 'ts') + return { + 'info': info, + 'symbol': self.safe_symbol(None, market), + 'fundingRate': self.safe_number(info, 'fundingRate'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + # + # the v2 URL is https://openapi-v2.kucoin.com/api/v1/endpoint + # ↑ ↑ + # ↑ ↑ + # + versions = self.safe_dict(self.options, 'versions', {}) + apiVersions = self.safe_dict(versions, api, {}) + methodVersions = self.safe_dict(apiVersions, method, {}) + defaultVersion = self.safe_string(methodVersions, path, self.options['version']) + version = self.safe_string(params, 'version', defaultVersion) + params = self.omit(params, 'version') + endpoint = '/api/' + version + '/' + self.implode_params(path, params) + if api == 'webExchange': + endpoint = '/' + self.implode_params(path, params) + if api == 'earn': + endpoint = '/api/v1/' + self.implode_params(path, params) + isUtaPrivate = False + if api == 'uta': + endpoint = '/api/ua/v1/' + self.implode_params(path, params) + if path == 'market/orderbook': + isUtaPrivate = True + query = self.omit(params, self.extract_params(path)) + endpart = '' + headers = headers if (headers is not None) else {} + url = self.urls['api'][api] + if not self.is_empty(query): + if ((method == 'GET') or (method == 'DELETE')) and (path != 'orders/multi-cancel'): + endpoint += '?' + self.rawencode(query) + else: + body = self.json(query) + endpart = body + headers['Content-Type'] = 'application/json' + url = url + endpoint + isFuturePrivate = (api == 'futuresPrivate') + isPrivate = (api == 'private') + isBroker = (api == 'broker') + isEarn = (api == 'earn') + if isPrivate or isFuturePrivate or isBroker or isEarn or isUtaPrivate: + self.check_required_credentials() + timestamp = str(self.nonce()) + headers = self.extend({ + 'KC-API-KEY-VERSION': '2', + 'KC-API-KEY': self.apiKey, + 'KC-API-TIMESTAMP': timestamp, + }, headers) + apiKeyVersion = self.safe_string(headers, 'KC-API-KEY-VERSION') + if apiKeyVersion == '2': + passphrase = self.hmac(self.encode(self.password), self.encode(self.secret), hashlib.sha256, 'base64') + headers['KC-API-PASSPHRASE'] = passphrase + else: + headers['KC-API-PASSPHRASE'] = self.password + payload = timestamp + method + endpoint + endpart + signature = self.hmac(self.encode(payload), self.encode(self.secret), hashlib.sha256, 'base64') + headers['KC-API-SIGN'] = signature + partner = self.safe_dict(self.options, 'partner', {}) + partner = self.safe_value(partner, 'future', partner) if isFuturePrivate else self.safe_value(partner, 'spot', partner) + partnerId = self.safe_string(partner, 'id') + partnerSecret = self.safe_string_2(partner, 'secret', 'key') + if (partnerId is not None) and (partnerSecret is not None): + partnerPayload = timestamp + partnerId + self.apiKey + partnerSignature = self.hmac(self.encode(partnerPayload), self.encode(partnerSecret), hashlib.sha256, 'base64') + headers['KC-API-PARTNER-SIGN'] = partnerSignature + headers['KC-API-PARTNER'] = partnerId + headers['KC-API-PARTNER-VERIFY'] = 'true' + if isBroker: + brokerName = self.safe_string(partner, 'name') + if brokerName is not None: + headers['KC-BROKER-NAME'] = brokerName + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if not response: + self.throw_broadly_matched_exception(self.exceptions['broad'], body, body) + return None + # + # bad + # {"code": "400100", "msg": "validation.createOrder.clientOidIsRequired"} + # good + # {code: '200000', data: {...}} + # + errorCode = self.safe_string(response, 'code') + message = self.safe_string_2(response, 'msg', 'data', '') + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], body, feedback) + if errorCode != '200000' and errorCode != '200': + raise ExchangeError(feedback) + return None diff --git a/ccxt/async_support/kucoinfutures.py b/ccxt/async_support/kucoinfutures.py new file mode 100644 index 0000000..c499465 --- /dev/null +++ b/ccxt/async_support/kucoinfutures.py @@ -0,0 +1,3306 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.kucoin import kucoin +from ccxt.abstract.kucoinfutures import ImplicitAPI +from ccxt.base.types import Any, Balances, Currency, DepositAddress, Int, Leverage, LeverageTier, MarginMode, MarginModification, Market, Num, Order, OrderBook, OrderRequest, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, Trade, TradingFeeInterface, Transaction, TransferEntry +from typing import List +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import AccountSuspended +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import NotSupported +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.errors import InvalidNonce +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class kucoinfutures(kucoin, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(kucoinfutures, self).describe(), { + 'id': 'kucoinfutures', + 'name': 'KuCoin Futures', + 'countries': ['SC'], + 'rateLimit': 75, + 'version': 'v1', + 'certified': True, + 'pro': True, + 'comment': 'Platform 2.0', + 'quoteJsonNumbers': False, + 'has': { + 'CORS': None, + 'spot': False, + 'margin': False, + 'swap': True, + 'future': True, + 'option': False, + 'addMargin': True, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'cancelOrders': True, + 'closeAllPositions': False, + 'closePosition': True, + 'closePositions': False, + 'createDepositAddress': True, + 'createOrder': True, + 'createOrders': True, + 'createOrderWithTakeProfitAndStopLoss': True, + 'createReduceOnlyOrder': True, + 'createStopLimitOrder': True, + 'createStopLossOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'createTakeProfitOrder': True, + 'createTriggerOrder': True, + 'fetchAccounts': True, + 'fetchBalance': True, + 'fetchBidsAsks': True, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': False, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchDepositWithdrawFee': False, + 'fetchDepositWithdrawFees': False, + 'fetchFundingHistory': True, + 'fetchFundingInterval': True, + 'fetchFundingIntervals': False, + 'fetchFundingRate': True, + 'fetchFundingRateHistory': True, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchL3OrderBook': True, + 'fetchLedger': True, + 'fetchLeverage': True, + 'fetchLeverageTiers': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': True, + 'fetchMarketLeverageTiers': True, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrice': True, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchPosition': True, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': True, + 'fetchPositionsHistory': True, + 'fetchPremiumIndexOHLCV': False, + 'fetchStatus': True, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': True, + 'fetchTransactionFee': False, + 'fetchWithdrawals': True, + 'setLeverage': False, + 'setMarginMode': True, + 'setPositionMode': True, + 'transfer': True, + 'withdraw': None, + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/147508995-9e35030a-d046-43a1-a006-6fabd981b554.jpg', + 'doc': [ + 'https://docs.kucoin.com/futures', + 'https://docs.kucoin.com', + ], + 'www': 'https://futures.kucoin.com/', + 'referral': 'https://futures.kucoin.com/?rcode=E5wkqe', + 'api': { + 'public': 'https://openapi-v2.kucoin.com', + 'private': 'https://openapi-v2.kucoin.com', + 'futuresPrivate': 'https://api-futures.kucoin.com', + 'futuresPublic': 'https://api-futures.kucoin.com', + 'webExchange': 'https://futures.kucoin.com/_api/web-front', + }, + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + 'password': True, + }, + 'api': { + 'futuresPublic': { + 'get': { + 'contracts/active': 1, + 'contracts/{symbol}': 1, + 'contracts/risk-limit/{symbol}': 1, + 'ticker': 1, + 'allTickers': 1, + 'level2/snapshot': 1.33, + 'level2/depth{limit}': 1, + 'level2/message/query': 1, + 'level3/message/query': 1, # deprecated,level3/snapshot is suggested + 'level3/snapshot': 1, # v2 + 'trade/history': 1, + 'interest/query': 1, + 'index/query': 1, + 'mark-price/{symbol}/current': 1, + 'premium/query': 1, + 'funding-rate/{symbol}/current': 1, + 'timestamp': 1, + 'status': 1, + 'kline/query': 1, + }, + 'post': { + 'bullet-public': 1, + }, + }, + 'futuresPrivate': { + 'get': { + 'account-overview': 1.33, + 'transaction-history': 4.44, + 'deposit-address': 1, + 'deposit-list': 1, + 'withdrawals/quotas': 1, + 'withdrawal-list': 1, + 'transfer-list': 1, + 'orders': 1.33, + 'stopOrders': 1, + 'recentDoneOrders': 1, + 'orders/{orderId}': 1, # ?clientOid={client-order-id} # get order by orderId + 'orders/byClientOid': 1, # ?clientOid=eresc138b21023a909e5ad59 # get order by clientOid + 'fills': 4.44, + 'recentFills': 4.44, + 'openOrderStatistics': 1, + 'position': 1, + 'positions': 4.44, + 'funding-history': 4.44, + 'sub/api-key': 1, + 'trade-statistics': 1, + 'trade-fees': 1, + 'history-positions': 1, + 'getMaxOpenSize': 1, + 'getCrossUserLeverage': 1, + 'position/getMarginMode': 1, + }, + 'post': { + 'withdrawals': 1, + 'transfer-out': 1, # v2 + 'transfer-in': 1, + 'orders': 1.33, + 'st-orders': 1.33, + 'orders/test': 1.33, + 'position/margin/auto-deposit-status': 1, + 'position/margin/deposit-margin': 1, + 'position/risk-limit-level/change': 1, + 'bullet-private': 1, + 'sub/api-key': 1, + 'sub/api-key/update': 1, + 'changeCrossUserLeverage': 1, + 'position/changeMarginMode': 1, + 'position/switchPositionMode': 1, + }, + 'delete': { + 'withdrawals/{withdrawalId}': 1, + 'cancel/transfer-out': 1, + 'orders/{orderId}': 1, + 'orders': 4.44, + 'stopOrders': 1, + 'sub/api-key': 1, + 'orders/client-order/{clientOid}': 1, + 'orders/multi-cancel': 20, + }, + }, + 'webExchange': { + 'get': { + 'contract/{symbol}/funding-rates': 1, + }, + }, + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'exact': { + '400': BadRequest, # Bad Request -- Invalid request format + '401': AuthenticationError, # Unauthorized -- Invalid API Key + '403': NotSupported, # Forbidden -- The request is forbidden + '404': NotSupported, # Not Found -- The specified resource could not be found + '405': NotSupported, # Method Not Allowed -- You tried to access the resource with an invalid method. + '415': BadRequest, # Content-Type -- application/json + '429': RateLimitExceeded, # Too Many Requests -- Access limit breached + '500': ExchangeNotAvailable, # Internal Server Error -- We had a problem with our server. Try again later. + '503': ExchangeNotAvailable, # Service Unavailable -- We're temporarily offline for maintenance. Please try again later. + '100001': OrderNotFound, # {"msg":"error.getOrder.orderNotExist","code":"100001"} + '100004': BadRequest, # {"code":"100004","msg":"Order is in not cancelable state"} + '101030': PermissionDenied, # {"code":"101030","msg":"You haven't yet enabled the margin trading"} + '200004': InsufficientFunds, + '230003': InsufficientFunds, # {"code":"230003","msg":"Balance insufficient!"} + '260100': InsufficientFunds, # {"code":"260100","msg":"account.noBalance"} + '300003': InsufficientFunds, + '300012': InvalidOrder, + '400001': AuthenticationError, # Any of KC-API-KEY, KC-API-SIGN, KC-API-TIMESTAMP, KC-API-PASSPHRASE is missing in your request header. + '400002': InvalidNonce, # KC-API-TIMESTAMP Invalid -- Time differs from server time by more than 5 seconds + '400003': AuthenticationError, # KC-API-KEY not exists + '400004': AuthenticationError, # KC-API-PASSPHRASE error + '400005': AuthenticationError, # Signature error -- Please check your signature + '400006': AuthenticationError, # The IP address is not in the API whitelist + '400007': AuthenticationError, # Access Denied -- Your API key does not have sufficient permissions to access the URI + '404000': NotSupported, # URL Not Found -- The requested resource could not be found + '400100': BadRequest, # Parameter Error -- You tried to access the resource with invalid parameters + '411100': AccountSuspended, # User is frozen -- Please contact us via support center + '500000': ExchangeNotAvailable, # Internal Server Error -- We had a problem with our server. Try again later. + '300009': InvalidOrder, # {"msg":"No open positions to close.","code":"300009"} + '330008': InsufficientFunds, # {"msg":"Your current margin and leverage have reached the maximum open limit. Please increase your margin or raise your leverage to open larger positions.","code":"330008"} + }, + 'broad': { + 'Position does not exist': OrderNotFound, # {"code":"200000", "msg":"Position does not exist"} + }, + }, + 'fees': { + 'trading': { + 'tierBased': True, + 'percentage': True, + 'taker': self.parse_number('0.0006'), + 'maker': self.parse_number('0.0002'), + 'tiers': { + 'taker': [ + [self.parse_number('0'), self.parse_number('0.0006')], + [self.parse_number('50'), self.parse_number('0.0006')], + [self.parse_number('200'), self.parse_number('0.0006')], + [self.parse_number('500'), self.parse_number('0.0005')], + [self.parse_number('1000'), self.parse_number('0.0004')], + [self.parse_number('2000'), self.parse_number('0.0004')], + [self.parse_number('4000'), self.parse_number('0.00038')], + [self.parse_number('8000'), self.parse_number('0.00035')], + [self.parse_number('15000'), self.parse_number('0.00032')], + [self.parse_number('25000'), self.parse_number('0.0003')], + [self.parse_number('40000'), self.parse_number('0.0003')], + [self.parse_number('60000'), self.parse_number('0.0003')], + [self.parse_number('80000'), self.parse_number('0.0003')], + ], + 'maker': [ + [self.parse_number('0'), self.parse_number('0.02')], + [self.parse_number('50'), self.parse_number('0.015')], + [self.parse_number('200'), self.parse_number('0.01')], + [self.parse_number('500'), self.parse_number('0.01')], + [self.parse_number('1000'), self.parse_number('0.01')], + [self.parse_number('2000'), self.parse_number('0')], + [self.parse_number('4000'), self.parse_number('0')], + [self.parse_number('8000'), self.parse_number('0')], + [self.parse_number('15000'), self.parse_number('-0.003')], + [self.parse_number('25000'), self.parse_number('-0.006')], + [self.parse_number('40000'), self.parse_number('-0.009')], + [self.parse_number('60000'), self.parse_number('-0.012')], + [self.parse_number('80000'), self.parse_number('-0.015')], + ], + }, + }, + 'funding': { + 'tierBased': False, + 'percentage': False, + 'withdraw': {}, + 'deposit': {}, + }, + }, + 'commonCurrencies': { + 'HOT': 'HOTNOW', + 'EDGE': 'DADI', # https://github.com/ccxt/ccxt/issues/5756 + 'WAX': 'WAXP', + 'TRY': 'Trias', + 'VAI': 'VAIOT', + 'XBT': 'BTC', + }, + 'timeframes': { + '1m': 1, + '3m': None, + '5m': 5, + '15m': 15, + '30m': 30, + '1h': 60, + '2h': 120, + '4h': 240, + '6h': None, + '8h': 480, + '12h': 720, + '1d': 1440, + '1w': 10080, + }, + 'options': { + 'version': 'v1', + 'symbolSeparator': '-', + 'defaultType': 'swap', + 'code': 'USDT', + 'marginModes': {}, + 'marginTypes': {}, + # endpoint versions + 'versions': { + 'futuresPrivate': { + 'GET': { + 'getMaxOpenSize': 'v2', + 'getCrossUserLeverage': 'v2', + 'position/getMarginMode': 'v2', + }, + 'POST': { + 'transfer-out': 'v2', + 'changeCrossUserLeverage': 'v2', + 'position/changeMarginMode': 'v2', + 'position/switchPositionMode': 'v2', + }, + }, + 'futuresPublic': { + 'GET': { + 'level3/snapshot': 'v2', + }, + }, + }, + 'networks': { + 'OMNI': 'omni', + 'ERC20': 'eth', + 'TRC20': 'trx', + }, + # 'code': 'BTC', + # 'fetchBalance': { + # 'code': 'BTC', + # }, + }, + 'features': { + 'spot': None, + 'forDerivs': { + 'sandbox': False, + 'createOrder': { + 'marginMode': True, + 'triggerPrice': True, + 'triggerPriceType': { + 'last': True, + 'mark': True, + 'index': True, + }, + 'triggerDirection': True, + 'stopLossPrice': True, + 'takeProfitPrice': True, + 'attachedStopLossTakeProfit': { + 'triggerPriceType': None, + 'price': True, + }, + 'timeInForce': { + 'IOC': True, + 'FOK': False, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': True, # todo implement + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': True, # todo implement + 'iceberg': True, + }, + 'createOrders': { + 'max': 20, + }, + 'fetchMyTrades': { + 'marginMode': True, + 'limit': 1000, + 'daysBack': None, + 'untilDays': 7, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 1000, + 'trigger': True, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': None, + 'daysBackCanceled': None, + 'untilDays': None, + 'trigger': True, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 500, + }, + }, + 'swap': { + 'linear': { + 'extends': 'forDerivs', + }, + 'inverse': { + 'extends': 'forDerivs', + }, + }, + 'future': { + 'linear': { + 'extends': 'forDerivs', + }, + 'inverse': { + 'extends': 'forDerivs', + }, + }, + }, + }) + + async def fetch_status(self, params={}): + """ + the latest known information on the availability of the exchange API + + https://www.kucoin.com/docs/rest/futures-trading/market-data/get-service-status + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `status structure ` + """ + response = await self.futuresPublicGetStatus(params) + # + # { + # "code":"200000", + # "data":{ + # "status": "open", # open, close, cancelonly + # "msg": "upgrade match engine" # remark for operation when status not open + # } + # } + # + data = self.safe_dict(response, 'data', {}) + status = self.safe_string(data, 'status') + return { + 'status': 'ok' if (status == 'open') else 'maintenance', + 'updated': None, + 'eta': None, + 'url': None, + 'info': response, + } + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for kucoinfutures + + https://www.kucoin.com/docs/rest/futures-trading/market-data/get-symbols-list + + :param dict [params]: extra parameters specific to the exchange api endpoint + :returns dict[]: an array of objects representing market data + """ + response = await self.futuresPublicGetContractsActive(params) + # + # { + # "code": "200000", + # "data": { + # "symbol": "ETHUSDTM", + # "rootSymbol": "USDT", + # "type": "FFWCSX", + # "firstOpenDate": 1591086000000, + # "expireDate": null, + # "settleDate": null, + # "baseCurrency": "ETH", + # "quoteCurrency": "USDT", + # "settleCurrency": "USDT", + # "maxOrderQty": 1000000, + # "maxPrice": 1000000.0000000000, + # "lotSize": 1, + # "tickSize": 0.05, + # "indexPriceTickSize": 0.01, + # "multiplier": 0.01, + # "initialMargin": 0.01, + # "maintainMargin": 0.005, + # "maxRiskLimit": 1000000, + # "minRiskLimit": 1000000, + # "riskStep": 500000, + # "makerFeeRate": 0.00020, + # "takerFeeRate": 0.00060, + # "takerFixFee": 0.0000000000, + # "makerFixFee": 0.0000000000, + # "settlementFee": null, + # "isDeleverage": True, + # "isQuanto": True, + # "isInverse": False, + # "markMethod": "FairPrice", + # "fairMethod": "FundingRate", + # "fundingBaseSymbol": ".ETHINT8H", + # "fundingQuoteSymbol": ".USDTINT8H", + # "fundingRateSymbol": ".ETHUSDTMFPI8H", + # "indexSymbol": ".KETHUSDT", + # "settlementSymbol": "", + # "status": "Open", + # "fundingFeeRate": 0.000535, + # "predictedFundingFeeRate": 0.002197, + # "openInterest": "8724443", + # "turnoverOf24h": 341156641.03354263, + # "volumeOf24h": 74833.54000000, + # "markPrice": 4534.07, + # "indexPrice":4531.92, + # "lastTradePrice": 4545.4500000000, + # "nextFundingRateTime": 25481884, + # "maxLeverage": 100, + # "sourceExchanges": ["huobi", "Okex", "Binance", "Kucoin", "Poloniex", "Hitbtc"], + # "premiumsSymbol1M": ".ETHUSDTMPI", + # "premiumsSymbol8H": ".ETHUSDTMPI8H", + # "fundingBaseSymbol1M": ".ETHINT", + # "fundingQuoteSymbol1M": ".USDTINT", + # "lowPrice": 4456.90, + # "highPrice": 4674.25, + # "priceChgPct": 0.0046, + # "priceChg": 21.15 + # } + # } + # + result = [] + data = self.safe_list(response, 'data', []) + for i in range(0, len(data)): + market = data[i] + id = self.safe_string(market, 'symbol') + expiry = self.safe_integer(market, 'expireDate') + future = self.safe_string(market, 'nextFundingRateTime') is None + swap = not future + baseId = self.safe_string(market, 'baseCurrency') + quoteId = self.safe_string(market, 'quoteCurrency') + settleId = self.safe_string(market, 'settleCurrency') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + settle = self.safe_currency_code(settleId) + symbol = base + '/' + quote + ':' + settle + type = 'swap' + if future: + symbol = symbol + '-' + self.yymmdd(expiry, '') + type = 'future' + inverse = self.safe_value(market, 'isInverse') + status = self.safe_string(market, 'status') + multiplier = self.safe_string(market, 'multiplier') + tickSize = self.safe_number(market, 'tickSize') + lotSize = self.safe_number(market, 'lotSize') + limitAmountMin = lotSize + if limitAmountMin is None: + limitAmountMin = self.safe_number(market, 'baseMinSize') + limitAmountMax = self.safe_number(market, 'maxOrderQty') + if limitAmountMax is None: + limitAmountMax = self.safe_number(market, 'baseMaxSize') + limitPriceMax = self.safe_number(market, 'maxPrice') + if limitPriceMax is None: + baseMinSizeString = self.safe_string(market, 'baseMinSize') + quoteMaxSizeString = self.safe_string(market, 'quoteMaxSize') + limitPriceMax = self.parse_number(Precise.string_div(quoteMaxSizeString, baseMinSizeString)) + result.append({ + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': type, + 'spot': False, + 'margin': False, + 'swap': swap, + 'future': future, + 'option': False, + 'active': (status == 'Open'), + 'contract': True, + 'linear': not inverse, + 'inverse': inverse, + 'taker': self.safe_number(market, 'takerFeeRate'), + 'maker': self.safe_number(market, 'makerFeeRate'), + 'contractSize': self.parse_number(Precise.string_abs(multiplier)), + 'expiry': expiry, + 'expiryDatetime': self.iso8601(expiry), + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': lotSize, + 'price': tickSize, + }, + 'limits': { + 'leverage': { + 'min': self.parse_number('1'), + 'max': self.safe_number(market, 'maxLeverage'), + }, + 'amount': { + 'min': limitAmountMin, + 'max': limitAmountMax, + }, + 'price': { + 'min': tickSize, + 'max': limitPriceMax, + }, + 'cost': { + 'min': self.safe_number(market, 'quoteMinSize'), + 'max': self.safe_number(market, 'quoteMaxSize'), + }, + }, + 'created': self.safe_integer(market, 'firstOpenDate'), + 'info': market, + }) + return result + + async def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://www.kucoin.com/docs/rest/futures-trading/market-data/get-server-time + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = await self.futuresPublicGetTimestamp(params) + # + # { + # "code": "200000", + # "data": 1637385119302, + # } + # + return self.safe_integer(response, 'data') + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://www.kucoin.com/docs/rest/futures-trading/market-data/get-klines + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate') + if paginate: + return await self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params, 200) + market = self.market(symbol) + marketId = market['id'] + parsedTimeframe = self.safe_integer(self.timeframes, timeframe) + request: dict = { + 'symbol': marketId, + } + if parsedTimeframe is not None: + request['granularity'] = parsedTimeframe + else: + request['granularity'] = timeframe + duration = self.parse_timeframe(timeframe) * 1000 + endAt = self.milliseconds() + if since is not None: + request['from'] = since + if limit is None: + limit = self.safe_integer(self.options, 'fetchOHLCVLimit', 200) + endAt = self.sum(since, limit * duration) + elif limit is not None: + since = endAt - limit * duration + request['from'] = since + request['to'] = endAt + response = await self.futuresPublicGetKlineQuery(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": [ + # [1636459200000, 4779.3, 4792.1, 4768.7, 4770.3, 78051], + # [1636460100000, 4770.25, 4778.55, 4757.55, 4777.25, 80164], + # [1636461000000, 4777.25, 4791.45, 4774.5, 4791.3, 51555] + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_ohlcvs(data, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # [ + # "1545904980000", # Start time of the candle cycle + # "0.058", # opening price + # "0.049", # closing price + # "0.058", # highest price + # "0.049", # lowest price + # "0.018", # base volume + # "0.000945", # quote volume + # ] + # + return [ + self.safe_integer(ohlcv, 0), + self.safe_number(ohlcv, 1), + self.safe_number(ohlcv, 2), + self.safe_number(ohlcv, 3), + self.safe_number(ohlcv, 4), + self.safe_number(ohlcv, 5), + ] + + async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://www.kucoin.com/docs/rest/funding/deposit/get-deposit-address + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + await self.load_markets() + currency = self.currency(code) + currencyId = currency['id'] + request: dict = { + 'currency': currencyId, # Currency,including XBT,USDT + } + response = await self.futuresPrivateGetDepositAddress(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "address": "0x78d3ad1c0aa1bf068e19c94a2d7b16c9c0fcd8b1",//Deposit address + # "memo": null//Address tag. If the returned value is null, it means that the requested token has no memo. If you are to transfer funds from another platform to KuCoin Futures and if the token to be #transferred has memo(tag), you need to fill in the memo to ensure the transferred funds will be sent #to the address you specified. + # } + # } + # + data = self.safe_dict(response, 'data', {}) + address = self.safe_string(data, 'address') + if currencyId != 'NIM': + # contains spaces + self.check_address(address) + return { + 'info': response, + 'currency': currencyId, + 'network': self.safe_string(data, 'chain'), + 'address': address, + 'tag': self.safe_string(data, 'memo'), + } + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://www.kucoin.com/docs/rest/futures-trading/market-data/get-part-order-book-level-2 + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + level = self.safe_number(params, 'level') + if level != 2 and level is not None: + raise BadRequest(self.id + ' fetchOrderBook() can only return level 2') + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + if (limit == 20) or (limit == 100): + request['limit'] = limit + else: + raise BadRequest(self.id + ' fetchOrderBook() limit argument must be 20 or 100') + else: + request['limit'] = 20 + response = await self.futuresPublicGetLevel2DepthLimit(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "symbol": "XBTUSDM", #Symbol + # "sequence": 100, #Ticker sequence number + # "asks": [ + # ["5000.0", 1000], #Price, quantity + # ["6000.0", 1983] #Price, quantity + # ], + # "bids": [ + # ["3200.0", 800], #Price, quantity + # ["3100.0", 100] #Price, quantity + # ], + # "ts": 1604643655040584408 # timestamp + # } + # } + # + data = self.safe_dict(response, 'data', {}) + timestamp = self.parse_to_int(self.safe_integer(data, 'ts') / 1000000) + orderbook = self.parse_order_book(data, market['symbol'], timestamp, 'bids', 'asks', 0, 1) + orderbook['nonce'] = self.safe_integer(data, 'sequence') + return orderbook + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://www.kucoin.com/docs/rest/futures-trading/market-data/get-ticker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = await self.futuresPublicGetTicker(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "sequence": 1638444978558, + # "symbol": "ETHUSDTM", + # "side": "sell", + # "size": 4, + # "price": "4229.35", + # "bestBidSize": 2160, + # "bestBidPrice": "4229.0", + # "bestAskPrice": "4229.05", + # "tradeId": "61aaa8b777a0c43055fe4851", + # "ts": 1638574296209786785, + # "bestAskSize": 36, + # } + # } + # + return self.parse_ticker(response['data'], market) + + async def fetch_mark_price(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://www.kucoin.com/docs/rest/futures-trading/market-data/get-current-mark-price + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = await self.futuresPublicGetMarkPriceSymbolCurrent(self.extend(request, params)) + # + return self.parse_ticker(response['data'], market) + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://www.kucoin.com/docs/rest/futures-trading/market-data/get-symbols-list + + :param str[] [symbols]: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.method]: the method to use, futuresPublicGetAllTickers or futuresPublicGetContractsActive + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + method = None + method, params = self.handle_option_and_params(params, 'fetchTickers', 'method', 'futuresPublicGetContractsActive') + response: dict = None + if method == 'futuresPublicGetAllTickers': + response = await self.futuresPublicGetAllTickers(params) + else: + response = await self.futuresPublicGetContractsActive(params) + # + # { + # "code": "200000", + # "data": { + # "symbol": "ETHUSDTM", + # "rootSymbol": "USDT", + # "type": "FFWCSX", + # "firstOpenDate": 1591086000000, + # "expireDate": null, + # "settleDate": null, + # "baseCurrency": "ETH", + # "quoteCurrency": "USDT", + # "settleCurrency": "USDT", + # "maxOrderQty": 1000000, + # "maxPrice": 1000000.0000000000, + # "lotSize": 1, + # "tickSize": 0.05, + # "indexPriceTickSize": 0.01, + # "multiplier": 0.01, + # "initialMargin": 0.01, + # "maintainMargin": 0.005, + # "maxRiskLimit": 1000000, + # "minRiskLimit": 1000000, + # "riskStep": 500000, + # "makerFeeRate": 0.00020, + # "takerFeeRate": 0.00060, + # "takerFixFee": 0.0000000000, + # "makerFixFee": 0.0000000000, + # "settlementFee": null, + # "isDeleverage": True, + # "isQuanto": True, + # "isInverse": False, + # "markMethod": "FairPrice", + # "fairMethod": "FundingRate", + # "fundingBaseSymbol": ".ETHINT8H", + # "fundingQuoteSymbol": ".USDTINT8H", + # "fundingRateSymbol": ".ETHUSDTMFPI8H", + # "indexSymbol": ".KETHUSDT", + # "settlementSymbol": "", + # "status": "Open", + # "fundingFeeRate": 0.000535, + # "predictedFundingFeeRate": 0.002197, + # "openInterest": "8724443", + # "turnoverOf24h": 341156641.03354263, + # "volumeOf24h": 74833.54000000, + # "markPrice": 4534.07, + # "indexPrice":4531.92, + # "lastTradePrice": 4545.4500000000, + # "nextFundingRateTime": 25481884, + # "maxLeverage": 100, + # "sourceExchanges": ["huobi", "Okex", "Binance", "Kucoin", "Poloniex", "Hitbtc"], + # "premiumsSymbol1M": ".ETHUSDTMPI", + # "premiumsSymbol8H": ".ETHUSDTMPI8H", + # "fundingBaseSymbol1M": ".ETHINT", + # "fundingQuoteSymbol1M": ".USDTINT", + # "lowPrice": 4456.90, + # "highPrice": 4674.25, + # "priceChgPct": 0.0046, + # "priceChg": 21.15 + # } + # } + # + data = self.safe_list(response, 'data') + tickers = self.parse_tickers(data, symbols) + return self.filter_by_array_tickers(tickers, 'symbol', symbols) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "symbol": "LTCUSDTM", + # "granularity": 1000, + # "timePoint": 1727967339000, + # "value": 62.37, mark price + # "indexPrice": 62.37 + # } + # + # { + # "code": "200000", + # "data": { + # "sequence": 1629930362547, + # "symbol": "ETHUSDTM", + # "side": "buy", + # "size": 130, + # "price": "4724.7", + # "bestBidSize": 5, + # "bestBidPrice": "4724.6", + # "bestAskPrice": "4724.65", + # "tradeId": "618d2a5a77a0c4431d2335f4", + # "ts": 1636641371963227600, + # "bestAskSize": 1789 + # } + # } + # + # from fetchTickers + # + # { + # symbol: "XBTUSDTM", + # rootSymbol: "USDT", + # type: "FFWCSX", + # firstOpenDate: 1585555200000, + # expireDate: null, + # settleDate: null, + # baseCurrency: "XBT", + # quoteCurrency: "USDT", + # settleCurrency: "USDT", + # maxOrderQty: 1000000, + # maxPrice: 1000000, + # lotSize: 1, + # tickSize: 0.1, + # indexPriceTickSize: 0.01, + # multiplier: 0.001, + # initialMargin: 0.008, + # maintainMargin: 0.004, + # maxRiskLimit: 100000, + # minRiskLimit: 100000, + # riskStep: 50000, + # makerFeeRate: 0.0002, + # takerFeeRate: 0.0006, + # takerFixFee: 0, + # makerFixFee: 0, + # settlementFee: null, + # isDeleverage: True, + # isQuanto: True, + # isInverse: False, + # markMethod: "FairPrice", + # fairMethod: "FundingRate", + # fundingBaseSymbol: ".XBTINT8H", + # fundingQuoteSymbol: ".USDTINT8H", + # fundingRateSymbol: ".XBTUSDTMFPI8H", + # indexSymbol: ".KXBTUSDT", + # settlementSymbol: "", + # status: "Open", + # fundingFeeRate: 0.000297, + # predictedFundingFeeRate: 0.000327, + # fundingRateGranularity: 28800000, + # openInterest: "8033200", + # turnoverOf24h: 659795309.2524643, + # volumeOf24h: 9998.54, + # markPrice: 67193.51, + # indexPrice: 67184.81, + # lastTradePrice: 67191.8, + # nextFundingRateTime: 20022985, + # maxLeverage: 125, + # premiumsSymbol1M: ".XBTUSDTMPI", + # premiumsSymbol8H: ".XBTUSDTMPI8H", + # fundingBaseSymbol1M: ".XBTINT", + # fundingQuoteSymbol1M: ".USDTINT", + # lowPrice: 64041.6, + # highPrice: 67737.3, + # priceChgPct: 0.0447, + # priceChg: 2878.7 + # } + # + marketId = self.safe_string(ticker, 'symbol') + market = self.safe_market(marketId, market, '-') + last = self.safe_string_2(ticker, 'price', 'lastTradePrice') + timestamp = self.safe_integer_product(ticker, 'ts', 0.000001) + return self.safe_ticker({ + 'symbol': market['symbol'], + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'highPrice'), + 'low': self.safe_string(ticker, 'lowPrice'), + 'bid': self.safe_string(ticker, 'bestBidPrice'), + 'bidVolume': self.safe_string(ticker, 'bestBidSize'), + 'ask': self.safe_string(ticker, 'bestAskPrice'), + 'askVolume': self.safe_string(ticker, 'bestAskSize'), + 'vwap': None, + 'open': None, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': self.safe_string(ticker, 'priceChg'), + 'percentage': self.safe_string(ticker, 'priceChgPct'), + 'average': None, + 'baseVolume': self.safe_string(ticker, 'volumeOf24h'), + 'quoteVolume': self.safe_string(ticker, 'turnoverOf24h'), + 'markPrice': self.safe_string_2(ticker, 'markPrice', 'value'), + 'indexPrice': self.safe_string(ticker, 'indexPrice'), + 'info': ticker, + }, market) + + async def fetch_bids_asks(self, symbols: Strings = None, params={}): + """ + fetches the bid and ask price and volume for multiple markets + :param str[] [symbols]: unified symbols of the markets to fetch the bids and asks for, all markets are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + request = { + 'method': 'futuresPublicGetAllTickers', + } + return await self.fetch_tickers(symbols, self.extend(request, params)) + + async def fetch_funding_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch the history of funding payments paid and received on self account + + https://www.kucoin.com/docs/rest/futures-trading/funding-fees/get-funding-history + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch funding history for + :param int [limit]: the maximum number of funding history structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding history structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchFundingHistory() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if since is not None: + request['startAt'] = since + if limit is not None: + # * Since is ignored if limit is defined + request['maxCount'] = limit + response = await self.futuresPrivateGetFundingHistory(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "dataList": [ + # { + # "id": 239471298749817, + # "symbol": "ETHUSDTM", + # "timePoint": 1638532800000, + # "fundingRate": 0.000100, + # "markPrice": 4612.8300000000, + # "positionQty": 12, + # "positionCost": 553.5396000000, + # "funding": -0.0553539600, + # "settleCurrency": "USDT" + # }, + # ... + # ], + # "hasMore": True + # } + # } + # + data = self.safe_value(response, 'data') + dataList = self.safe_list(data, 'dataList', []) + fees = [] + for i in range(0, len(dataList)): + listItem = dataList[i] + timestamp = self.safe_integer(listItem, 'timePoint') + fees.append({ + 'info': listItem, + 'symbol': symbol, + 'code': self.safe_currency_code(self.safe_string(listItem, 'settleCurrency')), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': self.safe_number(listItem, 'id'), + 'amount': self.safe_number(listItem, 'funding'), + 'fundingRate': self.safe_number(listItem, 'fundingRate'), + 'markPrice': self.safe_number(listItem, 'markPrice'), + 'positionQty': self.safe_number(listItem, 'positionQty'), + 'positionCost': self.safe_number(listItem, 'positionCost'), + }) + return fees + + async def fetch_position(self, symbol: str, params={}): + """ + + https://docs.kucoin.com/futures/#get-position-details + + fetch data on an open position + :param str symbol: unified market symbol of the market the position is held in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `position structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = await self.futuresPrivateGetPosition(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "id": "6505ee6eaff4070001f651c4", + # "symbol": "XBTUSDTM", + # "autoDeposit": False, + # "maintMarginReq": 0, + # "riskLimit": 200, + # "realLeverage": 0.0, + # "crossMode": False, + # "delevPercentage": 0.0, + # "currentTimestamp": 1694887534594, + # "currentQty": 0, + # "currentCost": 0.0, + # "currentComm": 0.0, + # "unrealisedCost": 0.0, + # "realisedGrossCost": 0.0, + # "realisedCost": 0.0, + # "isOpen": False, + # "markPrice": 26611.71, + # "markValue": 0.0, + # "posCost": 0.0, + # "posCross": 0, + # "posInit": 0.0, + # "posComm": 0.0, + # "posLoss": 0.0, + # "posMargin": 0.0, + # "posMaint": 0.0, + # "maintMargin": 0.0, + # "realisedGrossPnl": 0.0, + # "realisedPnl": 0.0, + # "unrealisedPnl": 0.0, + # "unrealisedPnlPcnt": 0, + # "unrealisedRoePcnt": 0, + # "avgEntryPrice": 0.0, + # "liquidationPrice": 0.0, + # "bankruptPrice": 0.0, + # "settleCurrency": "USDT", + # "maintainMargin": 0, + # "riskLimitLevel": 1 + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_position(data, market) + + async def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://docs.kucoin.com/futures/#get-position-list + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + await self.load_markets() + response = await self.futuresPrivateGetPositions(params) + # + # { + # "code": "200000", + # "data": [ + # { + # "id": "615ba79f83a3410001cde321", + # "symbol": "ETHUSDTM", + # "autoDeposit": False, + # "maintMarginReq": 0.005, + # "riskLimit": 1000000, + # "realLeverage": 18.61, + # "crossMode": False, + # "delevPercentage": 0.86, + # "openingTimestamp": 1638563515618, + # "currentTimestamp": 1638576872774, + # "currentQty": 2, + # "currentCost": 83.64200000, + # "currentComm": 0.05018520, + # "unrealisedCost": 83.64200000, + # "realisedGrossCost": 0.00000000, + # "realisedCost": 0.05018520, + # "isOpen": True, + # "markPrice": 4225.01, + # "markValue": 84.50020000, + # "posCost": 83.64200000, + # "posCross": 0.0000000000, + # "posInit": 3.63660870, + # "posComm": 0.05236717, + # "posLoss": 0.00000000, + # "posMargin": 3.68897586, + # "posMaint": 0.50637594, + # "maintMargin": 4.54717586, + # "realisedGrossPnl": 0.00000000, + # "realisedPnl": -0.05018520, + # "unrealisedPnl": 0.85820000, + # "unrealisedPnlPcnt": 0.0103, + # "unrealisedRoePcnt": 0.2360, + # "avgEntryPrice": 4182.10, + # "liquidationPrice": 4023.00, + # "bankruptPrice": 4000.25, + # "settleCurrency": "USDT", + # "isInverse": False + # } + # ] + # } + # + data = self.safe_list(response, 'data') + return self.parse_positions(data, symbols) + + async def fetch_positions_history(self, symbols: Strings = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical positions + + https://www.kucoin.com/docs/rest/futures-trading/positions/get-positions-history + + :param str[] [symbols]: list of unified market symbols + :param int [since]: the earliest time in ms to fetch position history for + :param int [limit]: the maximum number of entries to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: closing end time + :param int [params.pageId]: page id + :returns dict[]: a list of `position structure ` + """ + await self.load_markets() + if limit is None: + limit = 200 + request: dict = { + 'limit': limit, + } + if since is not None: + request['from'] = since + until = self.safe_integer(params, 'until') + if until is not None: + params = self.omit(params, 'until') + request['to'] = until + response = await self.futuresPrivateGetHistoryPositions(self.extend(request, params)) + # + # { + # "success": True, + # "code": "200", + # "msg": "success", + # "retry": False, + # "data": { + # "currentPage": 1, + # "pageSize": 10, + # "totalNum": 25, + # "totalPage": 3, + # "items": [ + # { + # "closeId": "300000000000000030", + # "positionId": "300000000000000009", + # "uid": 99996908309485, + # "userId": "6527d4fc8c7f3d0001f40f5f", + # "symbol": "XBTUSDM", + # "settleCurrency": "XBT", + # "leverage": "0.0", + # "type": "LIQUID_LONG", + # "side": null, + # "closeSize": null, + # "pnl": "-1.0000003793999999", + # "realisedGrossCost": "0.9993849748999999", + # "withdrawPnl": "0.0", + # "roe": null, + # "tradeFee": "0.0006154045", + # "fundingFee": "0.0", + # "openTime": 1713785751181, + # "closeTime": 1713785752784, + # "openPrice": null, + # "closePrice": null + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'data') + items = self.safe_list(data, 'items', []) + return self.parse_positions(items, symbols) + + def parse_position(self, position: dict, market: Market = None): + # + # { + # "code": "200000", + # "data": [ + # { + # "id": "615ba79f83a3410001cde321", # Position ID + # "symbol": "ETHUSDTM", # Symbol + # "autoDeposit": False, # Auto deposit margin or not + # "maintMarginReq": 0.005, # Maintenance margin requirement + # "riskLimit": 1000000, # Risk limit + # "realLeverage": 25.92, # Leverage of the order + # "crossMode": False, # Cross mode or not + # "delevPercentage": 0.76, # ADL ranking percentile + # "openingTimestamp": 1638578546031, # Open time + # "currentTimestamp": 1638578563580, # Current timestamp + # "currentQty": 2, # Current postion quantity + # "currentCost": 83.787, # Current postion value + # "currentComm": 0.0167574, # Current commission + # "unrealisedCost": 83.787, # Unrealised value + # "realisedGrossCost": 0.0, # Accumulated realised gross profit value + # "realisedCost": 0.0167574, # Current realised position value + # "isOpen": True, # Opened position or not + # "markPrice": 4183.38, # Mark price + # "markValue": 83.6676, # Mark value + # "posCost": 83.787, # Position value + # "posCross": 0.0, # added margin + # "posInit": 3.35148, # Leverage margin + # "posComm": 0.05228309, # Bankruptcy cost + # "posLoss": 0.0, # Funding fees paid out + # "posMargin": 3.40376309, # Position margin + # "posMaint": 0.50707892, # Maintenance margin + # "maintMargin": 3.28436309, # Position margin + # "realisedGrossPnl": 0.0, # Accumulated realised gross profit value + # "realisedPnl": -0.0167574, # Realised profit and loss + # "unrealisedPnl": -0.1194, # Unrealised profit and loss + # "unrealisedPnlPcnt": -0.0014, # Profit-loss ratio of the position + # "unrealisedRoePcnt": -0.0356, # Rate of return on investment + # "avgEntryPrice": 4189.35, # Average entry price + # "liquidationPrice": 4044.55, # Liquidation price + # "bankruptPrice": 4021.75, # Bankruptcy price + # "settleCurrency": "USDT", # Currency used to clear and settle the trades + # "isInverse": False + # } + # ] + # } + # position history + # { + # "closeId": "300000000000000030", + # "positionId": "300000000000000009", + # "uid": 99996908309485, + # "userId": "6527d4fc8c7f3d0001f40f5f", + # "symbol": "XBTUSDM", + # "settleCurrency": "XBT", + # "leverage": "0.0", + # "type": "LIQUID_LONG", + # "side": null, + # "closeSize": null, + # "pnl": "-1.0000003793999999", + # "realisedGrossCost": "0.9993849748999999", + # "withdrawPnl": "0.0", + # "roe": null, + # "tradeFee": "0.0006154045", + # "fundingFee": "0.0", + # "openTime": 1713785751181, + # "closeTime": 1713785752784, + # "openPrice": null, + # "closePrice": null + # } + # + symbol = self.safe_string(position, 'symbol') + market = self.safe_market(symbol, market) + timestamp = self.safe_integer(position, 'currentTimestamp') + size = self.safe_string(position, 'currentQty') + side = None + type = self.safe_string_lower(position, 'type') + if size is not None: + if Precise.string_gt(size, '0'): + side = 'long' + elif Precise.string_lt(size, '0'): + side = 'short' + elif type is not None: + if type.find('long') > -1: + side = 'long' + else: + side = 'short' + notional = Precise.string_abs(self.safe_string(position, 'posCost')) + initialMargin = self.safe_string(position, 'posInit') + initialMarginPercentage = Precise.string_div(initialMargin, notional) + # marginRatio = Precise.string_div(maintenanceRate, collateral) + unrealisedPnl = self.safe_string(position, 'unrealisedPnl') + crossMode = self.safe_value(position, 'crossMode') + # currently crossMode is always set to False and only isolated positions are supported + marginMode = None + if crossMode is not None: + marginMode = 'cross' if crossMode else 'isolated' + return self.safe_position({ + 'info': position, + 'id': self.safe_string_2(position, 'id', 'positionId'), + 'symbol': self.safe_string(market, 'symbol'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastUpdateTimestamp': self.safe_integer(position, 'closeTime'), + 'initialMargin': self.parse_number(initialMargin), + 'initialMarginPercentage': self.parse_number(initialMarginPercentage), + 'maintenanceMargin': self.safe_number(position, 'posMaint'), + 'maintenanceMarginPercentage': self.safe_number(position, 'maintMarginReq'), + 'entryPrice': self.safe_number_2(position, 'avgEntryPrice', 'openPrice'), + 'notional': self.parse_number(notional), + 'leverage': self.safe_number_2(position, 'realLeverage', 'leverage'), + 'unrealizedPnl': self.parse_number(unrealisedPnl), + 'contracts': self.parse_number(Precise.string_abs(size)), + 'contractSize': self.safe_value(market, 'contractSize'), + 'realizedPnl': self.safe_number_2(position, 'realisedPnl', 'pnl'), + 'marginRatio': None, + 'liquidationPrice': self.safe_number(position, 'liquidationPrice'), + 'markPrice': self.safe_number(position, 'markPrice'), + 'lastPrice': None, + 'collateral': self.safe_number(position, 'maintMargin'), + 'marginMode': marginMode, + 'side': side, + 'percentage': None, + 'stopLossPrice': None, + 'takeProfitPrice': None, + }) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + Create an order on the exchange + + https://www.kucoin.com/docs/rest/futures-trading/orders/place-order + https://www.kucoin.com/docs/rest/futures-trading/orders/place-take-profit-and-stop-loss-order#http-request + + :param str symbol: Unified CCXT market symbol + :param str type: 'limit' or 'market' + :param str side: 'buy' or 'sell' + :param float amount: the amount of currency to trade + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param dict [params.takeProfit]: *takeProfit object in params* containing the triggerPrice at which the attached take profit order will be triggered and the triggerPriceType + :param dict [params.stopLoss]: *stopLoss object in params* containing the triggerPrice at which the attached stop loss order will be triggered and the triggerPriceType + :param float [params.triggerPrice]: The price a trigger order is triggered at + :param float [params.stopLossPrice]: price to trigger stop-loss orders + :param float [params.takeProfitPrice]: price to trigger take-profit orders + :param bool [params.reduceOnly]: A mark to reduce the position size only. Set to False by default. Need to set the position size when reduceOnly is True. + :param str [params.timeInForce]: GTC, GTT, IOC, or FOK, default is GTC, limit orders only + :param str [params.postOnly]: Post only flag, invalid when timeInForce is IOC or FOK + :param float [params.cost]: the cost of the order in units of USDT + :param str [params.marginMode]: 'cross' or 'isolated', default is 'isolated' + :param bool [params.hedged]: *swap and future only* True for hedged mode, False for one way mode, default is False + ----------------- Exchange Specific Parameters ----------------- + :param float [params.leverage]: Leverage size of the order(mandatory param in request, default is 1) + :param str [params.clientOid]: client order id, defaults to uuid if not passed + :param str [params.remark]: remark for the order, length cannot exceed 100 utf8 characters + :param str [params.stop]: 'up' or 'down', the direction the triggerPrice is triggered from, requires triggerPrice. down: Triggers when the price reaches or goes below the triggerPrice. up: Triggers when the price reaches or goes above the triggerPrice. + :param str [params.triggerPriceType]: "last", "mark", "index" - defaults to "mark" + :param str [params.stopPriceType]: exchange-specific alternative for triggerPriceType: TP, IP or MP + :param bool [params.closeOrder]: set to True to close position + :param bool [params.test]: set to True to use the test order endpoint(does not submit order, use to validate params) + :param bool [params.forceHold]: A mark to forcely hold the funds for an order, even though it's an order to reduce the position size. This helps the order stay on the order book and not get canceled when the position size changes. Set to False by default.\ + :param str [params.positionSide]: *swap and future only* hedged two-way position side, LONG or SHORT + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + testOrder = self.safe_bool(params, 'test', False) + params = self.omit(params, 'test') + isTpAndSlOrder = (self.safe_value(params, 'stopLoss') is not None) or (self.safe_value(params, 'takeProfit') is not None) + orderRequest = self.create_contract_order_request(symbol, type, side, amount, price, params) + response = None + if testOrder: + response = await self.futuresPrivatePostOrdersTest(orderRequest) + else: + if isTpAndSlOrder: + response = await self.futuresPrivatePostStOrders(orderRequest) + else: + response = await self.futuresPrivatePostOrders(orderRequest) + # + # { + # "code": "200000", + # "data": { + # "orderId": "619717484f1d010001510cde", + # }, + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data, market) + + async def create_orders(self, orders: List[OrderRequest], params={}): + """ + create a list of trade orders + + https://www.kucoin.com/docs/rest/futures-trading/orders/place-multiple-orders + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + ordersRequests = [] + for i in range(0, len(orders)): + rawOrder = orders[i] + symbol = self.safe_string(rawOrder, 'symbol') + market = self.market(symbol) + type = self.safe_string(rawOrder, 'type') + side = self.safe_string(rawOrder, 'side') + amount = self.safe_value(rawOrder, 'amount') + price = self.safe_value(rawOrder, 'price') + orderParams = self.safe_value(rawOrder, 'params', {}) + orderRequest = self.create_contract_order_request(market['id'], type, side, amount, price, orderParams) + ordersRequests.append(orderRequest) + response = await self.futuresPrivatePostOrdersMulti(ordersRequests) + # + # { + # "code": "200000", + # "data": [ + # { + # "orderId": "135241412609331200", + # "clientOid": "3d8fcc13-0b13-447f-ad30-4b3441e05213", + # "symbol": "LTCUSDTM", + # "code": "200000", + # "msg": "success" + # }, + # { + # "orderId": "135241412747743234", + # "clientOid": "b878c7ee-ae3e-4d63-a20b-038acbb7306f", + # "symbol": "LTCUSDTM", + # "code": "200000", + # "msg": "success" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_orders(data) + + def create_contract_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + market = self.market(symbol) + # required param, cannot be used twice + clientOrderId = self.safe_string_2(params, 'clientOid', 'clientOrderId', self.uuid()) + params = self.omit(params, ['clientOid', 'clientOrderId']) + request: dict = { + 'clientOid': clientOrderId, + 'side': side, + 'symbol': market['id'], + 'type': type, # limit or market + 'leverage': 1, + } + marginModeUpper = self.safe_string_upper(params, 'marginMode') + if marginModeUpper is not None: + params = self.omit(params, 'marginMode') + request['marginMode'] = marginModeUpper + cost = self.safe_string(params, 'cost') + params = self.omit(params, 'cost') + if cost is not None: + request['valueQty'] = self.cost_to_precision(symbol, cost) + else: + if amount < 1: + raise InvalidOrder(self.id + ' createOrder() minimum contract order amount is 1') + request['size'] = int(self.amount_to_precision(symbol, amount)) + triggerPrice, stopLossPrice, takeProfitPrice = self.handle_trigger_prices(params) + stopLoss = self.safe_dict(params, 'stopLoss') + takeProfit = self.safe_dict(params, 'takeProfit') + # isTpAndSl = stopLossPrice and takeProfitPrice + triggerPriceTypes: dict = { + 'mark': 'MP', + 'last': 'TP', + 'index': 'IP', + } + triggerPriceType = self.safe_string(params, 'triggerPriceType', 'mark') + triggerPriceTypeValue = self.safe_string(triggerPriceTypes, triggerPriceType, triggerPriceType) + params = self.omit(params, ['stopLossPrice', 'takeProfitPrice', 'triggerPrice', 'stopPrice', 'takeProfit', 'stopLoss']) + if triggerPrice: + request['stop'] = 'up' if (side == 'buy') else 'down' + request['stopPrice'] = self.price_to_precision(symbol, triggerPrice) + request['stopPriceType'] = triggerPriceTypeValue + elif stopLoss is not None or takeProfit is not None: + priceType = triggerPriceTypeValue + if stopLoss is not None: + slPrice = self.safe_string_2(stopLoss, 'triggerPrice', 'stopPrice') + request['triggerStopDownPrice'] = self.price_to_precision(symbol, slPrice) + priceType = self.safe_string(stopLoss, 'triggerPriceType', 'mark') + priceType = self.safe_string(triggerPriceTypes, priceType, priceType) + if takeProfit is not None: + tpPrice = self.safe_string_2(takeProfit, 'triggerPrice', 'takeProfitPrice') + request['triggerStopUpPrice'] = self.price_to_precision(symbol, tpPrice) + priceType = self.safe_string(takeProfit, 'triggerPriceType', 'mark') + priceType = self.safe_string(triggerPriceTypes, priceType, priceType) + request['stopPriceType'] = priceType + elif stopLossPrice or takeProfitPrice: + if stopLossPrice: + request['stop'] = 'up' if (side == 'buy') else 'down' + request['stopPrice'] = self.price_to_precision(symbol, stopLossPrice) + else: + request['stop'] = 'down' if (side == 'buy') else 'up' + request['stopPrice'] = self.price_to_precision(symbol, takeProfitPrice) + request['reduceOnly'] = True + request['stopPriceType'] = triggerPriceTypeValue + uppercaseType = type.upper() + timeInForce = self.safe_string_upper(params, 'timeInForce') + if uppercaseType == 'LIMIT': + if price is None: + raise ArgumentsRequired(self.id + ' createOrder() requires a price argument for limit orders') + else: + request['price'] = self.price_to_precision(symbol, price) + if timeInForce is not None: + request['timeInForce'] = timeInForce + postOnly = None + postOnly, params = self.handle_post_only(type == 'market', False, params) + if postOnly: + request['postOnly'] = True + hidden = self.safe_value(params, 'hidden') + if postOnly and (hidden is not None): + raise BadRequest(self.id + ' createOrder() does not support the postOnly parameter together with a hidden parameter') + iceberg = self.safe_value(params, 'iceberg') + if iceberg: + visibleSize = self.safe_value(params, 'visibleSize') + if visibleSize is None: + raise ArgumentsRequired(self.id + ' createOrder() requires a visibleSize parameter for iceberg orders') + reduceOnly = self.safe_bool(params, 'reduceOnly', False) + hedged = None + hedged, params = self.handle_param_bool(params, 'hedged', False) + if reduceOnly: + request['reduceOnly'] = reduceOnly + if hedged: + reduceOnlyPosSide = 'LONG' if (side == 'sell') else 'SHORT' + request['positionSide'] = reduceOnlyPosSide + else: + if hedged: + posSide = 'LONG' if (side == 'buy') else 'SHORT' + request['positionSide'] = posSide + params = self.omit(params, ['timeInForce', 'stopPrice', 'triggerPrice', 'stopLossPrice', 'takeProfitPrice', 'reduceOnly', 'hedged']) # Time in force only valid for limit orders, exchange error when gtc for market orders + return self.extend(request, params) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://www.kucoin.com/docs/rest/futures-trading/orders/cancel-futures-order-by-orderid + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.clientOrderId]: cancel order by client order id + :returns dict: An `order structure ` + """ + await self.load_markets() + clientOrderId = self.safe_string_2(params, 'clientOid', 'clientOrderId') + params = self.omit(params, ['clientOrderId']) + request: dict = {} + response = None + if clientOrderId is not None: + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument when cancelling by clientOrderId') + market = self.market(symbol) + request['symbol'] = market['id'] + request['clientOid'] = clientOrderId + response = await self.futuresPrivateDeleteOrdersClientOrderClientOid(self.extend(request, params)) + else: + request['orderId'] = id + response = await self.futuresPrivateDeleteOrdersOrderId(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "cancelledOrderIds": [ + # "619714b8b6353000014c505a", + # ], + # }, + # } + # + return self.safe_order({'info': response}) + + async def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): + """ + cancel multiple orders + + https://www.kucoin.com/docs/rest/futures-trading/orders/batch-cancel-orders + + :param str[] ids: order ids + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str[] [params.clientOrderIds]: client order ids + :returns dict: an list of `order structures ` + """ + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + ordersRequests = [] + clientOrderIds = self.safe_list_2(params, 'clientOrderIds', 'clientOids', []) + params = self.omit(params, ['clientOrderIds', 'clientOids']) + useClientorderId = False + for i in range(0, len(clientOrderIds)): + useClientorderId = True + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrders() requires a symbol argument when cancelling by clientOrderIds') + ordersRequests.append({ + 'symbol': market['id'], + 'clientOid': self.safe_string(clientOrderIds, i), + }) + for i in range(0, len(ids)): + ordersRequests.append(ids[i]) + requestKey = 'clientOidsList' if useClientorderId else 'orderIdsList' + request: dict = {} + request[requestKey] = ordersRequests + response = await self.futuresPrivateDeleteOrdersMultiCancel(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": + # [ + # { + # "orderId": "80465574458560512", + # "clientOid": null, + # "code": "200", + # "msg": "success" + # }, + # { + # "orderId": "80465575289094144", + # "clientOid": null, + # "code": "200", + # "msg": "success" + # } + # ] + # } + # + orders = self.safe_list(response, 'data', []) + return self.parse_orders(orders, market) + + async def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders + + https://www.kucoin.com/docs/rest/futures-trading/orders/cancel-multiple-futures-limit-orders + https://www.kucoin.com/docs/rest/futures-trading/orders/cancel-multiple-futures-stop-orders + + :param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param dict [params.trigger]: When True, all the trigger orders will be cancelled + :returns: Response from the exchange + """ + await self.load_markets() + request: dict = {} + if symbol is not None: + request['symbol'] = self.market_id(symbol) + trigger = self.safe_value_2(params, 'stop', 'trigger') + params = self.omit(params, ['stop', 'trigger']) + response = None + if trigger: + response = await self.futuresPrivateDeleteStopOrders(self.extend(request, params)) + else: + response = await self.futuresPrivateDeleteOrders(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "cancelledOrderIds": [ + # "619714b8b6353000014c505a", + # ], + # }, + # } + # + data = self.safe_dict(response, 'data') + return [self.safe_order({'info': data})] + + async def add_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + add margin + + https://www.kucoin.com/docs/rest/futures-trading/positions/add-margin-manually + + :param str symbol: unified market symbol + :param float amount: amount of margin to add + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin structure ` + """ + await self.load_markets() + market = self.market(symbol) + uuid = self.uuid() + request: dict = { + 'symbol': market['id'], + 'margin': self.amount_to_precision(symbol, amount), + 'bizNo': uuid, + } + response = await self.futuresPrivatePostPositionMarginDepositMargin(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "id": "62311d26064e8f00013f2c6d", + # "symbol": "XRPUSDTM", + # "autoDeposit": False, + # "maintMarginReq": 0.01, + # "riskLimit": 200000, + # "realLeverage": 0.88, + # "crossMode": False, + # "delevPercentage": 0.4, + # "openingTimestamp": 1647385894798, + # "currentTimestamp": 1647414510672, + # "currentQty": -1, + # "currentCost": -7.658, + # "currentComm": 0.0053561, + # "unrealisedCost": -7.658, + # "realisedGrossCost": 0, + # "realisedCost": 0.0053561, + # "isOpen": True, + # "markPrice": 0.7635, + # "markValue": -7.635, + # "posCost": -7.658, + # "posCross": 1.00016084, + # "posInit": 7.658, + # "posComm": 0.00979006, + # "posLoss": 0, + # "posMargin": 8.6679509, + # "posMaint": 0.08637006, + # "maintMargin": 8.6909509, + # "realisedGrossPnl": 0, + # "realisedPnl": -0.0038335, + # "unrealisedPnl": 0.023, + # "unrealisedPnlPcnt": 0.003, + # "unrealisedRoePcnt": 0.003, + # "avgEntryPrice": 0.7658, + # "liquidationPrice": 1.6239, + # "bankruptPrice": 1.6317, + # "settleCurrency": "USDT" + # } + # } + # + # + # { + # "code":"200000", + # "msg":"Position does not exist" + # } + # + data = self.safe_value(response, 'data') + return self.extend(self.parse_margin_modification(data, market), { + 'amount': self.amount_to_precision(symbol, amount), + 'direction': 'in', + }) + + def parse_margin_modification(self, info, market: Market = None) -> MarginModification: + # + # { + # "id": "62311d26064e8f00013f2c6d", + # "symbol": "XRPUSDTM", + # "autoDeposit": False, + # "maintMarginReq": 0.01, + # "riskLimit": 200000, + # "realLeverage": 0.88, + # "crossMode": False, + # "delevPercentage": 0.4, + # "openingTimestamp": 1647385894798, + # "currentTimestamp": 1647414510672, + # "currentQty": -1, + # "currentCost": -7.658, + # "currentComm": 0.0053561, + # "unrealisedCost": -7.658, + # "realisedGrossCost": 0, + # "realisedCost": 0.0053561, + # "isOpen": True, + # "markPrice": 0.7635, + # "markValue": -7.635, + # "posCost": -7.658, + # "posCross": 1.00016084, + # "posInit": 7.658, + # "posComm": 0.00979006, + # "posLoss": 0, + # "posMargin": 8.6679509, + # "posMaint": 0.08637006, + # "maintMargin": 8.6909509, + # "realisedGrossPnl": 0, + # "realisedPnl": -0.0038335, + # "unrealisedPnl": 0.023, + # "unrealisedPnlPcnt": 0.003, + # "unrealisedRoePcnt": 0.003, + # "avgEntryPrice": 0.7658, + # "liquidationPrice": 1.6239, + # "bankruptPrice": 1.6317, + # "settleCurrency": "USDT" + # } + # + # { + # "code":"200000", + # "msg":"Position does not exist" + # } + # + id = self.safe_string(info, 'id') + market = self.safe_market(id, market) + currencyId = self.safe_string(info, 'settleCurrency') + crossMode = self.safe_value(info, 'crossMode') + mode = 'cross' if crossMode else 'isolated' + marketId = self.safe_string(market, 'symbol') + timestamp = self.safe_integer(info, 'currentTimestamp') + return { + 'info': info, + 'symbol': self.safe_symbol(marketId, market), + 'type': None, + 'marginMode': mode, + 'amount': None, + 'total': None, + 'code': self.safe_currency_code(currencyId), + 'status': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + + async def fetch_orders_by_status(self, status, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches a list of orders placed on the exchange + + https://docs.kucoin.com/futures/#get-order-list + https://docs.kucoin.com/futures/#get-untriggered-stop-order-list + + :param str status: 'active' or 'closed', only 'active' is valid for stop orders + :param str symbol: unified symbol for the market to retrieve orders from + :param int [since]: timestamp in ms of the earliest order to retrieve + :param int [limit]: The maximum number of orders to retrieve + :param dict [params]: exchange specific parameters + :param bool [params.trigger]: set to True to retrieve untriggered stop orders + :param int [params.until]: End time in ms + :param str [params.side]: buy or sell + :param str [params.type]: limit or market + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns: An `array of order structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOrdersByStatus', 'paginate') + if paginate: + return await self.fetch_paginated_call_dynamic('fetchOrdersByStatus', symbol, since, limit, params) + trigger = self.safe_bool_2(params, 'stop', 'trigger') + until = self.safe_integer(params, 'until') + params = self.omit(params, ['stop', 'until', 'trigger']) + if status == 'closed': + status = 'done' + elif status == 'open': + status = 'active' + request: dict = {} + if not trigger: + request['status'] = status + elif status != 'active': + raise BadRequest(self.id + ' fetchOrdersByStatus() can only fetch untriggered stop orders') + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if since is not None: + request['startAt'] = since + if until is not None: + request['endAt'] = until + response = None + if trigger: + response = await self.futuresPrivateGetStopOrders(self.extend(request, params)) + else: + response = await self.futuresPrivateGetOrders(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "currentPage": 1, + # "pageSize": 50, + # "totalNum": 4, + # "totalPage": 1, + # "items": [ + # { + # "id": "64507d02921f1c0001ff6892", + # "symbol": "XBTUSDTM", + # "type": "market", + # "side": "buy", + # "price": null, + # "size": 1, + # "value": "27.992", + # "dealValue": "27.992", + # "dealSize": 1, + # "stp": "", + # "stop": "", + # "stopPriceType": "", + # "stopTriggered": False, + # "stopPrice": null, + # "timeInForce": "GTC", + # "postOnly": False, + # "hidden": False, + # "iceberg": False, + # "leverage": "17", + # "forceHold": False, + # "closeOrder": False, + # "visibleSize": null, + # "clientOid": null, + # "remark": null, + # "tags": null, + # "isActive": False, + # "cancelExist": False, + # "createdAt": 1682996482000, + # "updatedAt": 1682996483062, + # "endAt": 1682996483062, + # "orderTime": 1682996482953900677, + # "settleCurrency": "USDT", + # "status": "done", + # "filledValue": "27.992", + # "filledSize": 1, + # "reduceOnly": False + # } + # ] + # } + # } + # + responseData = self.safe_dict(response, 'data', {}) + orders = self.safe_list(responseData, 'items', []) + return self.parse_orders(orders, market, since, limit) + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://docs.kucoin.com/futures/#get-order-list + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: end time in ms + :param str [params.side]: buy or sell + :param str [params.type]: limit, or market + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchClosedOrders', 'paginate') + if paginate: + return await self.fetch_paginated_call_dynamic('fetchClosedOrders', symbol, since, limit, params) + return await self.fetch_orders_by_status('done', symbol, since, limit, params) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple open orders made by the user + + https://docs.kucoin.com/futures/#get-order-list + https://docs.kucoin.com/futures/#get-untriggered-stop-order-list + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: end time in ms + :param str [params.side]: buy or sell + :param str [params.type]: limit, or market + :param boolean [params.trigger]: set to True to retrieve untriggered stop orders + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOpenOrders', 'paginate') + if paginate: + return await self.fetch_paginated_call_dynamic('fetchOpenOrders', symbol, since, limit, params) + return await self.fetch_orders_by_status('open', symbol, since, limit, params) + + async def fetch_order(self, id: Str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://docs.kucoin.com/futures/#get-details-of-a-single-order + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + request: dict = {} + response = None + if id is None: + clientOrderId = self.safe_string_2(params, 'clientOid', 'clientOrderId') + if clientOrderId is None: + raise InvalidOrder(self.id + ' fetchOrder() requires parameter id or params.clientOid') + request['clientOid'] = clientOrderId + params = self.omit(params, ['clientOid', 'clientOrderId']) + response = await self.futuresPrivateGetOrdersByClientOid(self.extend(request, params)) + else: + request['orderId'] = id + response = await self.futuresPrivateGetOrdersOrderId(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "id": "64507d02921f1c0001ff6892", + # "symbol": "XBTUSDTM", + # "type": "market", + # "side": "buy", + # "price": null, + # "size": 1, + # "value": "27.992", + # "dealValue": "27.992", + # "dealSize": 1, + # "stp": "", + # "stop": "", + # "stopPriceType": "", + # "stopTriggered": False, + # "stopPrice": null, + # "timeInForce": "GTC", + # "postOnly": False, + # "hidden": False, + # "iceberg": False, + # "leverage": "17", + # "forceHold": False, + # "closeOrder": False, + # "visibleSize": null, + # "clientOid": null, + # "remark": null, + # "tags": null, + # "isActive": False, + # "cancelExist": False, + # "createdAt": 1682996482000, + # "updatedAt": 1682996483000, + # "endAt": 1682996483000, + # "orderTime": 1682996482953900677, + # "settleCurrency": "USDT", + # "status": "done", + # "filledSize": 1, + # "filledValue": "27.992", + # "reduceOnly": False + # } + # } + # + market = self.market(symbol) if (symbol is not None) else None + responseData = self.safe_dict(response, 'data') + return self.parse_order(responseData, market) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # fetchOrder, fetchOrdersByStatus + # + # { + # "id": "64507d02921f1c0001ff6892", + # "symbol": "XBTUSDTM", + # "type": "market", + # "side": "buy", + # "price": null, + # "size": 1, + # "value": "27.992", + # "dealValue": "27.992", + # "dealSize": 1, + # "stp": "", + # "stop": "", + # "stopPriceType": "", + # "stopTriggered": False, + # "stopPrice": null, + # "timeInForce": "GTC", + # "postOnly": False, + # "hidden": False, + # "iceberg": False, + # "leverage": "17", + # "forceHold": False, + # "closeOrder": False, + # "visibleSize": null, + # "clientOid": null, + # "remark": null, + # "tags": null, + # "isActive": False, + # "cancelExist": False, + # "createdAt": 1682996482000, + # "updatedAt": 1682996483062, + # "endAt": 1682996483062, + # "orderTime": 1682996482953900677, + # "settleCurrency": "USDT", + # "status": "done", + # "filledValue": "27.992", + # "filledSize": 1, + # "reduceOnly": False + # } + # + # createOrder + # + # { + # "orderId": "619717484f1d010001510cde" + # } + # + # createOrders + # + # { + # "orderId": "80465574458560512", + # "clientOid": "5c52e11203aa677f33e491", + # "symbol": "ETHUSDTM", + # "code": "200000", + # "msg": "success" + # } + # + marketId = self.safe_string(order, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + orderId = self.safe_string_2(order, 'id', 'orderId') + type = self.safe_string(order, 'type') + timestamp = self.safe_integer(order, 'createdAt') + datetime = self.iso8601(timestamp) + price = self.safe_string(order, 'price') + # price is zero for market order + # omitZero is called in safeOrder2 + side = self.safe_string(order, 'side') + feeCurrencyId = self.safe_string(order, 'feeCurrency') + feeCurrency = self.safe_currency_code(feeCurrencyId) + feeCost = self.safe_number(order, 'fee') + amount = self.safe_string(order, 'size') + filled = self.safe_string(order, 'filledSize') + cost = self.safe_string(order, 'filledValue') + average = self.safe_string(order, 'avgDealPrice') + if (average is None) and Precise.string_gt(filled, '0'): + contractSize = self.safe_string(market, 'contractSize') + if market['linear']: + average = Precise.string_div(cost, Precise.string_mul(contractSize, filled)) + else: + average = Precise.string_div(Precise.string_mul(contractSize, filled), cost) + # precision reported by their api is 8 d.p. + # average = Precise.string_div(cost, Precise.string_mul(filled, market['contractSize'])) + # bool + isActive = self.safe_value(order, 'isActive') + cancelExist = self.safe_bool(order, 'cancelExist', False) + status = None + if isActive is not None: + status = 'open' if isActive else 'closed' + status = 'canceled' if cancelExist else status + fee = None + if feeCost is not None: + fee = { + 'currency': feeCurrency, + 'cost': feeCost, + } + clientOrderId = self.safe_string(order, 'clientOid') + timeInForce = self.safe_string(order, 'timeInForce') + postOnly = self.safe_value(order, 'postOnly') + reduceOnly = self.safe_value(order, 'reduceOnly') + lastUpdateTimestamp = self.safe_integer(order, 'updatedAt') + return self.safe_order({ + 'id': orderId, + 'clientOrderId': clientOrderId, + 'symbol': symbol, + 'type': type, + 'timeInForce': timeInForce, + 'postOnly': postOnly, + 'reduceOnly': reduceOnly, + 'side': side, + 'amount': amount, + 'price': price, + 'triggerPrice': self.safe_number(order, 'stopPrice'), + 'cost': cost, + 'filled': filled, + 'remaining': None, + 'timestamp': timestamp, + 'datetime': datetime, + 'fee': fee, + 'status': status, + 'info': order, + 'lastTradeTimestamp': None, + 'lastUpdateTimestamp': lastUpdateTimestamp, + 'average': average, + 'trades': None, + }, market) + + async def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate + + https://www.kucoin.com/docs/rest/futures-trading/funding-fees/get-current-funding-rate + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = await self.futuresPublicGetFundingRateSymbolCurrent(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "symbol": ".ETHUSDTMFPI8H", + # "granularity": 28800000, + # "timePoint": 1637380800000, + # "value": 0.0001, + # "predictedValue": 0.0001, + # }, + # } + # + data = self.safe_dict(response, 'data', {}) + # the website displayes the previous funding rate as "funding rate" + return self.parse_funding_rate(data, market) + + async def fetch_funding_interval(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate interval + + https://www.kucoin.com/docs/rest/futures-trading/funding-fees/get-current-funding-rate + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + return await self.fetch_funding_rate(symbol, params) + + def parse_funding_rate(self, data, market: Market = None) -> FundingRate: + # + # { + # "symbol": ".ETHUSDTMFPI8H", + # "granularity": 28800000, + # "timePoint": 1637380800000, + # "value": 0.0001, + # "predictedValue": 0.0001, + # } + # + fundingTimestamp = self.safe_integer(data, 'timePoint') + marketId = self.safe_string(data, 'symbol') + return { + 'info': data, + 'symbol': self.safe_symbol(marketId, market, None, 'contract'), + 'markPrice': None, + 'indexPrice': None, + 'interestRate': None, + 'estimatedSettlePrice': None, + 'timestamp': None, + 'datetime': None, + 'fundingRate': self.safe_number(data, 'value'), + 'fundingTimestamp': fundingTimestamp, + 'fundingDatetime': self.iso8601(fundingTimestamp), + 'nextFundingRate': self.safe_number(data, 'predictedValue'), + 'nextFundingTimestamp': None, + 'nextFundingDatetime': None, + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': self.parse_funding_interval(self.safe_string(data, 'granularity')), + } + + def parse_funding_interval(self, interval): + intervals: dict = { + '3600000': '1h', + '14400000': '4h', + '28800000': '8h', + '57600000': '16h', + '86400000': '24h', + } + return self.safe_string(intervals, interval, interval) + + def parse_balance(self, response) -> Balances: + result: dict = { + 'info': response, + 'timestamp': None, + 'datetime': None, + } + data = self.safe_value(response, 'data') + currencyId = self.safe_string(data, 'currency') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(data, 'availableBalance') + account['total'] = self.safe_string(data, 'accountEquity') + result[code] = account + return self.safe_balance(result) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://www.kucoin.com/docs/rest/funding/funding-overview/get-account-detail-futures + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param dict [params.code]: the unified currency code to fetch the balance for, if not provided, the default .options['fetchBalance']['code'] will be used + :returns dict: a `balance structure ` + """ + await self.load_markets() + # only fetches one balance at a time + defaultCode = self.safe_string(self.options, 'code') + fetchBalanceOptions = self.safe_value(self.options, 'fetchBalance', {}) + defaultCode = self.safe_string(fetchBalanceOptions, 'code', defaultCode) + code = self.safe_string(params, 'code', defaultCode) + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + } + response = await self.futuresPrivateGetAccountOverview(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "accountEquity": 0.00005, + # "unrealisedPNL": 0, + # "marginBalance": 0.00005, + # "positionMargin": 0, + # "orderMargin": 0, + # "frozenFunds": 0, + # "availableBalance": 0.00005, + # "currency": "XBT" + # } + # } + # + return self.parse_balance(response) + + async def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://www.kucoin.com/docs/rest/funding/transfer/transfer-to-main-or-trade-account + https://www.kucoin.com/docs/rest/funding/transfer/transfer-to-futures-account + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account to transfer from + :param str toAccount: account to transfer to + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transfer structure ` + """ + await self.load_markets() + currency = self.currency(code) + amountToPrecision = self.currency_to_precision(code, amount) + request: dict = { + 'currency': self.safe_string(currency, 'id'), + 'amount': amountToPrecision, + } + toAccountString = self.parse_transfer_type(toAccount) + response = None + if toAccountString == 'TRADE' or toAccountString == 'MAIN': + request['recAccountType'] = toAccountString + response = await self.futuresPrivatePostTransferOut(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "applyId": "6738754373ceee00011ec3f8", + # "bizNo": "6738754373ceee00011ec3f7", + # "payAccountType": "CONTRACT", + # "payTag": "DEFAULT", + # "remark": "", + # "recAccountType": "MAIN", + # "recTag": "DEFAULT", + # "recRemark": "", + # "recSystem": "KUCOIN", + # "status": "PROCESSING", + # "currency": "USDT", + # "amount": "5", + # "fee": "0", + # "sn": 1519769124846692, + # "reason": "", + # "createdAt": 1731753283000, + # "updatedAt": 1731753283000 + # } + # } + # + elif toAccount == 'future' or toAccount == 'swap' or toAccount == 'contract': + request['payAccountType'] = self.parse_transfer_type(fromAccount) + response = await self.futuresPrivatePostTransferIn(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "applyId": "5bffb63303aa675e8bbe18f9" # Transfer-out request ID + # } + # } + # + else: + raise BadRequest(self.id + ' transfer() only supports transfers between future/swap, spot and funding accounts') + data = self.safe_dict(response, 'data', {}) + return self.extend(self.parse_transfer(data, currency), { + 'amount': self.parse_number(amountToPrecision), + 'fromAccount': fromAccount, + 'toAccount': toAccount, + }) + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + # + # transfer to spot or funding account + # + # { + # "applyId": "5bffb63303aa675e8bbe18f9" # Transfer-out request ID + # } + # + # transfer to future account + # + # { + # "applyId": "6738754373ceee00011ec3f8", + # "bizNo": "6738754373ceee00011ec3f7", + # "payAccountType": "CONTRACT", + # "payTag": "DEFAULT", + # "remark": "", + # "recAccountType": "MAIN", + # "recTag": "DEFAULT", + # "recRemark": "", + # "recSystem": "KUCOIN", + # "status": "PROCESSING", + # "currency": "USDT", + # "amount": "5", + # "fee": "0", + # "sn": 1519769124846692, + # "reason": "", + # "createdAt": 1731753283000, + # "updatedAt": 1731753283000 + # } + # + timestamp = self.safe_integer(transfer, 'updatedAt') + return { + 'id': self.safe_string(transfer, 'applyId'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'currency': self.safe_currency_code(None, currency), + 'amount': self.safe_number(transfer, 'amount'), + 'fromAccount': None, + 'toAccount': None, + 'status': self.safe_string(transfer, 'status'), + 'info': transfer, + } + + def parse_transfer_status(self, status: Str) -> Str: + statuses: dict = { + 'PROCESSING': 'pending', + } + return self.safe_string(statuses, status, status) + + def parse_transfer_type(self, transferType: Str) -> Str: + transferTypes: dict = { + 'spot': 'TRADE', + 'funding': 'MAIN', + } + return self.safe_string_upper(transferTypes, transferType, transferType) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + + https://docs.kucoin.com/futures/#get-fills + + fetch all trades made by the user + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: End time in ms + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchMyTrades', 'paginate') + if paginate: + return await self.fetch_paginated_call_dynamic('fetchMyTrades', symbol, since, limit, params) + request: dict = { + # orderId(str) [optional] Fills for a specific order(other parameters can be ignored if specified) + # symbol(str) [optional] Symbol of the contract + # side(str) [optional] buy or sell + # type(str) [optional] limit, market, limit_stop or market_stop + # startAt(long) [optional] Start time(millisecond) + # endAt(long) [optional] End time(millisecond) + } + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if since is not None: + request['startAt'] = since + if limit is not None: + request['pageSize'] = min(1000, limit) + request, params = self.handle_until_option('endAt', request, params) + response = await self.futuresPrivateGetFills(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "currentPage": 1, + # "pageSize": 1, + # "totalNum": 251915, + # "totalPage": 251915, + # "items": [ + # { + # "symbol": "XBTUSDM", # Ticker symbol of the contract + # "tradeId": "5ce24c1f0c19fc3c58edc47c", # Trade ID + # "orderId": "5ce24c16b210233c36ee321d", # Order ID + # "side": "sell", # Transaction side + # "liquidity": "taker", # Liquidity- taker or maker + # "price": "8302", # Filled price + # "size": 10, # Filled amount + # "value": "0.001204529", # Order value + # "feeRate": "0.0005", # Floating fees + # "fixFee": "0.00000006", # Fixed fees + # "feeCurrency": "XBT", # Charging currency + # "stop": "", # A mark to the stop order type + # "fee": "0.0000012022", # Transaction fee + # "orderType": "limit", # Order type + # "tradeType": "trade", # Trade type(trade, liquidation, ADL or settlement) + # "createdAt": 1558334496000, # Time the order created + # "settleCurrency": "XBT", # settlement currency + # "tradeTime": 1558334496000000000 # trade time in nanosecond + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + trades = self.safe_list(data, 'items', []) + return self.parse_trades(trades, market, since, limit) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://www.kucoin.com/docs/rest/futures-trading/market-data/get-transaction-history + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = await self.futuresPublicGetTradeHistory(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": [ + # { + # "sequence": 32114961, + # "side": "buy", + # "size": 39, + # "price": "4001.6500000000", + # "takerOrderId": "61c20742f172110001e0ebe4", + # "makerOrderId": "61c2073fcfc88100010fcb5d", + # "tradeId": "61c2074277a0c473e69029b8", + # "ts": 1640105794099993896 # filled time + # } + # ] + # } + # + trades = self.safe_list(response, 'data', []) + return self.parse_trades(trades, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(public) + # + # { + # "sequence": 32114961, + # "side": "buy", + # "size": 39, + # "price": "4001.6500000000", + # "takerOrderId": "61c20742f172110001e0ebe4", + # "makerOrderId": "61c2073fcfc88100010fcb5d", + # "tradeId": "61c2074277a0c473e69029b8", + # "ts": 1640105794099993896 # filled time + # } + # + # fetchMyTrades(private) v2 + # + # { + # "symbol":"BTC-USDT", + # "tradeId":"5c35c02709e4f67d5266954e", + # "orderId":"5c35c02703aa673ceec2a168", + # "counterOrderId":"5c1ab46003aa676e487fa8e3", + # "side":"buy", + # "liquidity":"taker", + # "forceTaker":true, + # "price":"0.083", + # "size":"0.8424304", + # "funds":"0.0699217232", + # "fee":"0", + # "feeRate":"0", + # "feeCurrency":"USDT", + # "stop":"", + # "type":"limit", + # "createdAt":1547026472000 + # } + # + # fetchMyTrades(private) v1 + # + # { + # "symbol":"DOGEUSDTM", + # "tradeId":"620ec41a96bab27b5f4ced56", + # "orderId":"620ec41a0d1d8a0001560bd0", + # "side":"sell", + # "liquidity":"taker", + # "forceTaker":true, + # "price":"0.13969", + # "size":1, + # "value":"13.969", + # "feeRate":"0.0006", + # "fixFee":"0", + # "feeCurrency":"USDT", + # "stop":"", + # "tradeTime":1645134874858018058, + # "fee":"0.0083814", + # "settleCurrency":"USDT", + # "orderType":"market", + # "tradeType":"trade", + # "createdAt":1645134874858 + # } + # + # watchTrades + # + # { + # "makerUserId": "62286a4d720edf0001e81961", + # "symbol": "ADAUSDTM", + # "sequence": 41320766, + # "side": "sell", + # "size": 2, + # "price": 0.35904, + # "takerOrderId": "636dd9da9857ba00010cfa44", + # "makerOrderId": "636dd9c8df149d0001e62bc8", + # "takerUserId": "6180be22b6ab210001fa3371", + # "tradeId": "636dd9da0000d400d477eca7", + # "ts": 1668143578987357700 + # } + # + marketId = self.safe_string(trade, 'symbol') + market = self.safe_market(marketId, market, '-') + id = self.safe_string_2(trade, 'tradeId', 'id') + orderId = self.safe_string(trade, 'orderId') + takerOrMaker = self.safe_string(trade, 'liquidity') + timestamp = self.safe_integer(trade, 'ts') + if timestamp is not None: + timestamp = self.parse_to_int(timestamp / 1000000) + else: + timestamp = self.safe_integer(trade, 'createdAt') + # if it's a historical v1 trade, the exchange returns timestamp in seconds + if ('dealValue' in trade) and (timestamp is not None): + timestamp = timestamp * 1000 + priceString = self.safe_string_2(trade, 'price', 'dealPrice') + amountString = self.safe_string_2(trade, 'size', 'amount') + side = self.safe_string(trade, 'side') + fee = None + feeCostString = self.safe_string(trade, 'fee') + if feeCostString is not None: + feeCurrencyId = self.safe_string(trade, 'feeCurrency') + feeCurrency = self.safe_currency_code(feeCurrencyId) + if feeCurrency is None: + feeCurrency = market['quote'] if (side == 'sell') else market['base'] + fee = { + 'cost': feeCostString, + 'currency': feeCurrency, + 'rate': self.safe_string(trade, 'feeRate'), + } + type = self.safe_string_2(trade, 'type', 'orderType') + if type == 'match': + type = None + costString = self.safe_string_2(trade, 'funds', 'value') + if costString is None: + contractSize = self.safe_string(market, 'contractSize') + contractCost = Precise.string_mul(priceString, amountString) + costString = Precise.string_mul(contractCost, contractSize) + return self.safe_trade({ + 'info': trade, + 'id': id, + 'order': orderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'type': type, + 'takerOrMaker': takerOrMaker, + 'side': side, + 'price': priceString, + 'amount': amountString, + 'cost': costString, + 'fee': fee, + }, market) + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + request: dict = {} + currency = None + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + if limit is not None: + request['pageSize'] = limit + if since is not None: + request['startAt'] = since + response = await self.futuresPrivateGetDepositList(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "currentPage": 1, + # "pageSize": 5, + # "totalNum": 2, + # "totalPage": 1, + # "items": [ + # { + # "address": "0x5f047b29041bcfdbf0e4478cdfa753a336ba6989", + # "memo": "5c247c8a03aa677cea2a251d", + # "amount": 1, + # "fee": 0.0001, + # "currency": "KCS", + # "isInner": False, + # "walletTxId": "5bbb57386d99522d9f954c5a@test004", + # "status": "SUCCESS", + # "createdAt": 1544178843000, + # "updatedAt": 1544178891000 + # "remark":"foobar" + # }, + # ... + # ] + # } + # } + # + responseData = response['data']['items'] + return self.parse_transactions(responseData, currency, since, limit, {'type': 'deposit'}) + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + request: dict = {} + currency = None + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + if limit is not None: + request['pageSize'] = limit + if since is not None: + request['startAt'] = since + response = await self.futuresPrivateGetWithdrawalList(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "currentPage": 1, + # "pageSize": 5, + # "totalNum": 2, + # "totalPage": 1, + # "items": [ + # { + # "id": "5c2dc64e03aa675aa263f1ac", + # "address": "0x5bedb060b8eb8d823e2414d82acce78d38be7fe9", + # "memo": "", + # "currency": "ETH", + # "amount": 1.0000000, + # "fee": 0.0100000, + # "walletTxId": "3e2414d82acce78d38be7fe9", + # "isInner": False, + # "status": "FAILURE", + # "createdAt": 1546503758000, + # "updatedAt": 1546504603000 + # }, + # ... + # ] + # } + # } + # + responseData = response['data']['items'] + return self.parse_transactions(responseData, currency, since, limit, {'type': 'withdrawal'}) + + async def fetch_market_leverage_tiers(self, symbol: str, params={}) -> List[LeverageTier]: + """ + retrieve information on the maximum leverage, and maintenance margin for trades of varying trade sizes for a single market + + https://www.kucoin.com/docs/rest/futures-trading/risk-limit/get-futures-risk-limit-level + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `leverage tiers structure ` + """ + await self.load_markets() + market = self.market(symbol) + if not market['contract']: + raise BadRequest(self.id + ' fetchMarketLeverageTiers() supports contract markets only') + request: dict = { + 'symbol': market['id'], + } + response = await self.futuresPublicGetContractsRiskLimitSymbol(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": [ + # { + # "symbol": "ETHUSDTM", + # "level": 1, + # "maxRiskLimit": 300000, + # "minRiskLimit": 0, + # "maxLeverage": 100, + # "initialMargin": 0.0100000000, + # "maintainMargin": 0.0050000000 + # }, + # ... + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_market_leverage_tiers(data, market) + + def parse_market_leverage_tiers(self, info, market: Market = None) -> List[LeverageTier]: + """ + @ignore + :param dict info: Exchange market response for 1 market + :param dict market: CCXT market + """ + # + # { + # "symbol": "ETHUSDTM", + # "level": 1, + # "maxRiskLimit": 300000, + # "minRiskLimit": 0, + # "maxLeverage": 100, + # "initialMargin": 0.0100000000, + # "maintainMargin": 0.0050000000 + # } + # + tiers = [] + for i in range(0, len(info)): + tier = info[i] + marketId = self.safe_string(tier, 'symbol') + tiers.append({ + 'tier': self.safe_number(tier, 'level'), + 'symbol': self.safe_symbol(marketId, market, None, 'contract'), + 'currency': market['base'], + 'minNotional': self.safe_number(tier, 'minRiskLimit'), + 'maxNotional': self.safe_number(tier, 'maxRiskLimit'), + 'maintenanceMarginRate': self.safe_number(tier, 'maintainMargin'), + 'maxLeverage': self.safe_number(tier, 'maxLeverage'), + 'info': tier, + }) + return tiers + + async def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + + https://www.kucoin.com/docs/rest/futures-trading/funding-fees/get-public-funding-history#request-url + + fetches historical funding rate prices + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: not used by kucuoinfutures + :param int [limit]: the maximum amount of `funding rate structures ` to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: end time in ms + :returns dict[]: a list of `funding rate structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchFundingRateHistory() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'from': 0, + 'to': self.milliseconds(), + } + until = self.safe_integer(params, 'until') + params = self.omit(params, ['until']) + if since is not None: + request['from'] = since + if until is None: + request['to'] = since + 1000 * 8 * 60 * 60 * 100 + if until is not None: + request['to'] = until + if since is None: + request['to'] = until - 1000 * 8 * 60 * 60 * 100 + response = await self.futuresPublicGetContractFundingRates(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": [ + # { + # "symbol": "IDUSDTM", + # "fundingRate": 2.26E-4, + # "timepoint": 1702296000000 + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_funding_rate_histories(data, market, since, limit) + + def parse_funding_rate_history(self, info, market: Market = None): + timestamp = self.safe_integer(info, 'timepoint') + marketId = self.safe_string(info, 'symbol') + return { + 'info': info, + 'symbol': self.safe_symbol(marketId, market), + 'fundingRate': self.safe_number(info, 'fundingRate'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + + async def close_position(self, symbol: str, side: OrderSide = None, params={}) -> Order: + """ + closes open positions for a market + + https://www.kucoin.com/docs/rest/futures-trading/orders/place-order + + :param str symbol: Unified CCXT market symbol + :param str side: not used by kucoinfutures closePositions + :param dict [params]: extra parameters specific to the okx api endpoint + :param str [params.clientOrderId]: client order id of the order + :returns dict[]: `A list of position structures ` + """ + await self.load_markets() + market = self.market(symbol) + clientOrderId = self.safe_string(params, 'clientOrderId') + testOrder = self.safe_bool(params, 'test', False) + params = self.omit(params, ['test', 'clientOrderId']) + if clientOrderId is None: + clientOrderId = self.number_to_string(self.nonce()) + request: dict = { + 'symbol': market['id'], + 'closeOrder': True, + 'clientOid': clientOrderId, + 'type': 'market', + } + response = None + if testOrder: + response = await self.futuresPrivatePostOrdersTest(self.extend(request, params)) + else: + response = await self.futuresPrivatePostOrders(self.extend(request, params)) + return self.parse_order(response, market) + + async def fetch_trading_fee(self, symbol: str, params={}) -> TradingFeeInterface: + """ + fetch the trading fees for a market + + https://www.kucoin.com/docs/rest/funding/trade-fee/trading-pair-actual-fee-futures + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `fee structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbols': market['id'], + } + response = await self.privateGetTradeFees(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "symbol": "XBTUSDTM", + # "takerFeeRate": "0.0006", + # "makerFeeRate": "0.0002" + # } + # } + # + data = self.safe_list(response, 'data', []) + first = self.safe_dict(data, 0) + marketId = self.safe_string(first, 'symbol') + return { + 'info': response, + 'symbol': self.safe_symbol(marketId, market), + 'maker': self.safe_number(first, 'makerFeeRate'), + 'taker': self.safe_number(first, 'takerFeeRate'), + 'percentage': True, + 'tierBased': True, + } + + async def fetch_margin_mode(self, symbol: str, params={}) -> MarginMode: + """ + fetches the margin mode of a trading pair + + https://www.kucoin.com/docs/rest/futures-trading/positions/get-margin-mode + + :param str symbol: unified symbol of the market to fetch the margin mode for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin mode structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = await self.futuresPrivateGetPositionGetMarginMode(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "symbol": "XBTUSDTM", + # "marginMode": "ISOLATED" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_margin_mode(data, market) + + def parse_margin_mode(self, marginMode: dict, market=None) -> MarginMode: + marginType = self.safe_string(marginMode, 'marginMode') + marginType = 'isolated' if (marginType == 'ISOLATED') else 'cross' + return { + 'info': marginMode, + 'symbol': market['symbol'], + 'marginMode': marginType, + } + + async def set_margin_mode(self, marginMode: str, symbol: Str = None, params={}): + """ + set margin mode to 'cross' or 'isolated' + + https://www.kucoin.com/docs/rest/futures-trading/positions/modify-margin-mode + + :param str marginMode: 'cross' or 'isolated' + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setMarginMode() requires a symbol argument') + self.check_required_argument('setMarginMode', marginMode, 'marginMode', ['cross', 'isolated']) + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'marginMode': marginMode.upper(), + } + response = await self.futuresPrivatePostPositionChangeMarginMode(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "symbol": "XBTUSDTM", + # "marginMode": "ISOLATED" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_margin_mode(data, market) + + async def set_position_mode(self, hedged: bool, symbol: Str = None, params={}): + """ + set hedged to True or False for a market + + https://www.kucoin.com/docs-new/3475097e0 + + :param bool hedged: set to True to use two way position + :param str [symbol]: not used by bybit setPositionMode() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a response from the exchange + """ + await self.load_markets() + posMode = '1' if hedged else '0' + request: dict = { + 'positionMode': posMode, + } + response = await self.futuresPrivatePostPositionSwitchPositionMode(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "positionMode": 1 + # } + # } + # + return response + + async def fetch_leverage(self, symbol: str, params={}) -> Leverage: + """ + fetch the set leverage for a market + + https://www.kucoin.com/docs/rest/futures-trading/positions/get-cross-margin-leverage + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `leverage structure ` + """ + marginMode = None + marginMode, params = self.handle_margin_mode_and_params(symbol, params) + if marginMode != 'cross': + raise NotSupported(self.id + ' fetchLeverage() currently supports only params["marginMode"] = "cross"') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = await self.futuresPrivateGetGetCrossUserLeverage(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "symbol": "XBTUSDTM", + # "leverage": "3" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + parsed = self.parse_leverage(data, market) + return self.extend(parsed, { + 'marginMode': marginMode, + }) + + async def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://www.kucoin.com/docs/rest/futures-trading/positions/modify-cross-margin-leverage + + :param float leverage: the rate of leverage + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + marginMode = None + marginMode, params = self.handle_margin_mode_and_params(symbol, params) + if marginMode != 'cross': + raise NotSupported(self.id + ' setLeverage() currently supports only params["marginMode"] = "cross"') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'leverage': str(leverage), + } + response = await self.futuresPrivatePostChangeCrossUserLeverage(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": True + # } + # + return self.parse_leverage(response, market) + + def parse_leverage(self, leverage: dict, market: Market = None) -> Leverage: + marketId = self.safe_string(leverage, 'symbol') + market = self.safe_market(marketId, market) + leverageNum = self.safe_integer(leverage, 'leverage') + return { + 'info': leverage, + 'symbol': market['symbol'], + 'marginMode': None, + 'longLeverage': leverageNum, + 'shortLeverage': leverageNum, + } diff --git a/ccxt/async_support/latoken.py b/ccxt/async_support/latoken.py new file mode 100644 index 0000000..2816e91 --- /dev/null +++ b/ccxt/async_support/latoken.py @@ -0,0 +1,1770 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.latoken import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currencies, Currency, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, TradingFeeInterface, Transaction, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import AccountSuspended +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import NotSupported +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.errors import InvalidNonce +from ccxt.base.decimal_to_precision import TICK_SIZE + + +class latoken(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(latoken, self).describe(), { + 'id': 'latoken', + 'name': 'Latoken', + 'countries': ['KY'], # Cayman Islands + 'version': 'v2', + 'rateLimit': 1000, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createOrder': True, + 'createPostOnlyOrder': False, + 'createStopLimitOrder': True, + 'createStopMarketOrder': False, + 'createStopOrder': True, + 'fetchAllGreeks': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': False, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDepositsWithdrawals': True, + 'fetchDepositWithdrawFees': False, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrice': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': True, + 'fetchTradingFees': False, + 'fetchTransactions': 'emulated', + 'fetchTransfer': False, + 'fetchTransfers': True, + 'fetchUnderlyingAssets': False, + 'fetchVolatilityHistory': False, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'transfer': True, + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/61511972-24c39f00-aa01-11e9-9f7c-471f1d6e5214.jpg', + 'api': { + 'rest': 'https://api.latoken.com', + }, + 'www': 'https://latoken.com', + 'doc': [ + 'https://api.latoken.com', + ], + 'fees': 'https://latoken.com/fees', + 'referral': 'https://latoken.com/invite?r=mvgp2djk', + }, + 'api': { + 'public': { + 'get': { + 'book/{currency}/{quote}': 1, + 'chart/week': 1, + 'chart/week/{currency}/{quote}': 1, + 'currency': 1, + 'currency/available': 1, + 'currency/quotes': 1, + 'currency/{currency}': 1, + 'pair': 1, + 'pair/available': 1, + 'ticker': 1, + 'ticker/{base}/{quote}': 1, + 'time': 1, + 'trade/history/{currency}/{quote}': 1, + 'trade/fee/{currency}/{quote}': 1, + 'trade/feeLevels': 1, + 'transaction/bindings': 1, + }, + }, + 'private': { + 'get': { + 'auth/account': 1, + 'auth/account/currency/{currency}/{type}': 1, + 'auth/order': 1, + 'auth/order/getOrder/{id}': 1, + 'auth/order/pair/{currency}/{quote}': 1, + 'auth/order/pair/{currency}/{quote}/active': 1, + 'auth/stopOrder': 1, + 'auth/stopOrder/getOrder/{id}': 1, + 'auth/stopOrder/pair/{currency}/{quote}': 1, + 'auth/stopOrder/pair/{currency}/{quote}/active': 1, + 'auth/trade': 1, + 'auth/trade/pair/{currency}/{quote}': 1, + 'auth/trade/fee/{currency}/{quote}': 1, + 'auth/transaction': 1, + 'auth/transaction/bindings': 1, + 'auth/transaction/bindings/{currency}': 1, + 'auth/transaction/{id}': 1, + 'auth/transfer': 1, + }, + 'post': { + 'auth/order/cancel': 1, + 'auth/order/cancelAll': 1, + 'auth/order/cancelAll/{currency}/{quote}': 1, + 'auth/order/place': 1, + 'auth/spot/deposit': 1, + 'auth/spot/withdraw': 1, + 'auth/stopOrder/cancel': 1, + 'auth/stopOrder/cancelAll': 1, + 'auth/stopOrder/cancelAll/{currency}/{quote}': 1, + 'auth/stopOrder/place': 1, + 'auth/transaction/depositAddress': 1, + 'auth/transaction/withdraw': 1, + 'auth/transaction/withdraw/cancel': 1, + 'auth/transaction/withdraw/confirm': 1, + 'auth/transaction/withdraw/resendCode': 1, + 'auth/transfer/email': 1, + 'auth/transfer/id': 1, + 'auth/transfer/phone': 1, + }, + }, + }, + 'precisionMode': TICK_SIZE, + 'fees': { + 'trading': { + 'feeSide': 'get', + 'tierBased': False, + 'percentage': True, + 'maker': self.parse_number('0.0049'), + 'taker': self.parse_number('0.0049'), + }, + }, + 'commonCurrencies': { + 'BUX': 'Buxcoin', + 'CBT': 'Community Business Token', + 'CTC': 'CyberTronchain', + 'DMD': 'Diamond Coin', + 'FREN': 'Frenchie', + 'GDX': 'GoldenX', + 'GEC': 'Geco One', + 'GEM': 'NFTmall', + 'GMT': 'GMT Token', + 'IMC': 'IMCoin', + 'MT': 'Monarch', + 'TPAY': 'Tetra Pay', + 'TRADE': 'Smart Trade Coin', + 'TSL': 'Treasure SL', + 'UNO': 'Unobtanium', + 'WAR': 'Warrior Token', + }, + 'exceptions': { + 'exact': { + 'INTERNAL_ERROR': ExchangeError, # internal server error. You can contact our support to solve self problem. {"message":"Internal Server Error","error":"INTERNAL_ERROR","status":"FAILURE"} + 'SERVICE_UNAVAILABLE': ExchangeNotAvailable, # requested information currently not available. You can contact our support to solve self problem or retry later. + 'NOT_AUTHORIZED': AuthenticationError, # user's query not authorized. Check if you are logged in. + 'FORBIDDEN': PermissionDenied, # you don't have enough access rights. + 'BAD_REQUEST': BadRequest, # some bad request, for example bad fields values or something else. Read response message for more information. + 'NOT_FOUND': ExchangeError, # entity not found. Read message for more information. + 'ACCESS_DENIED': PermissionDenied, # access is denied. Probably you don't have enough access rights, you contact our support. + 'REQUEST_REJECTED': ExchangeError, # user's request rejected for some reasons. Check error message. + 'HTTP_MEDIA_TYPE_NOT_SUPPORTED': BadRequest, # http media type not supported. + 'MEDIA_TYPE_NOT_ACCEPTABLE': BadRequest, # media type not acceptable + 'METHOD_ARGUMENT_NOT_VALID': BadRequest, # one of method argument is invalid. Check argument types and error message for more information. + 'VALIDATION_ERROR': BadRequest, # check errors field to get reasons. + 'ACCOUNT_EXPIRED': AccountSuspended, # restore your account or create a new one. + 'BAD_CREDENTIALS': AuthenticationError, # invalid username or password. + 'COOKIE_THEFT': AuthenticationError, # cookie has been stolen. Let's try reset your cookies. + 'CREDENTIALS_EXPIRED': AccountSuspended, # credentials expired. + 'INSUFFICIENT_AUTHENTICATION': AuthenticationError, # for example, 2FA required. + 'UNKNOWN_LOCATION': AuthenticationError, # user logged from unusual location, email confirmation required. + 'TOO_MANY_REQUESTS': RateLimitExceeded, # too many requests at the time. A response header X-Rate-Limit-Remaining indicates the number of allowed request per a period. + 'INSUFFICIENT_FUNDS': InsufficientFunds, # {"message":"not enough balance on the spot account for currency(USDT), need(20.000)","error":"INSUFFICIENT_FUNDS","status":"FAILURE"} + 'ORDER_VALIDATION': InvalidOrder, # {"message":"Quantity(0) is not positive","error":"ORDER_VALIDATION","status":"FAILURE"} + 'BAD_TICKS': InvalidOrder, # {"status":"FAILURE","message":"Quantity(1.4) does not match quantity tick(10)","error":"BAD_TICKS","errors":null,"result":false} + }, + 'broad': { + 'invalid API key, signature or digest': AuthenticationError, # {"result":false,"message":"invalid API key, signature or digest","error":"BAD_REQUEST","status":"FAILURE"} + 'The API key was revoked': AuthenticationError, # {"result":false,"message":"The API key was revoked","error":"BAD_REQUEST","status":"FAILURE"} + 'request expired or bad': InvalidNonce, # {"result":false,"message":"request expired or bad / format","error":"BAD_REQUEST","status":"FAILURE"} + 'For input string': BadRequest, # {"result":false,"message":"Internal error","error":"For input string: \"NaN\"","status":"FAILURE"} + 'Unable to resolve currency by tag': BadSymbol, # {"message":"Unable to resolve currency by tag(None)","error":"NOT_FOUND","status":"FAILURE"} + "Can't find currency with tag": BadSymbol, # {"status":"FAILURE","message":"Can't find currency with tag = None","error":"NOT_FOUND","errors":null,"result":false} + 'Unable to place order because pair is in inactive state': BadSymbol, # {"message":"Unable to place order because pair is in inactive state(PAIR_STATUS_INACTIVE)","error":"ORDER_VALIDATION","status":"FAILURE"} + 'API keys are not available for': AccountSuspended, # {"result":false,"message":"API keys are not available for FROZEN user","error":"BAD_REQUEST","status":"FAILURE"} + }, + }, + 'options': { + 'defaultType': 'spot', + 'types': { + 'wallet': 'ACCOUNT_TYPE_WALLET', + 'funding': 'ACCOUNT_TYPE_WALLET', + 'spot': 'ACCOUNT_TYPE_SPOT', + }, + 'accounts': { + 'ACCOUNT_TYPE_WALLET': 'wallet', + 'ACCOUNT_TYPE_SPOT': 'spot', + }, + 'fetchTradingFee': { + 'method': 'fetchPrivateTradingFee', # or 'fetchPublicTradingFee' + }, + 'timeDifference': 0, # the difference between system clock and exchange clock + 'adjustForTimeDifference': True, # controls the adjustment logic upon instantiation + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, # todo: for non-trigger orders + 'FOK': True, + 'PO': False, + 'GTD': False, + }, + 'hedged': False, + 'selfTradePrevention': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 100000, # todo + 'untilDays': None, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': True, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOrders': None, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 100000, # todo + 'daysBackCanceled': 1, + 'untilDays': None, + 'trigger': True, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': None, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + }) + + def nonce(self): + return self.milliseconds() - self.options['timeDifference'] + + async def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://api.latoken.com/doc/v2/#tag/Time/operation/currentTime + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = await self.publicGetTime(params) + # + # { + # "serverTime": 1570615577321 + # } + # + return self.safe_integer(response, 'serverTime') + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for latoken + + https://api.latoken.com/doc/v2/#tag/Pair/operation/getActivePairs + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = await self.publicGetPair(params) + # + # [ + # { + # "id":"dba4289b-6b46-4d94-bf55-49eec9a163ad", + # "status":"PAIR_STATUS_ACTIVE", # CURRENCY_STATUS_INACTIVE + # "baseCurrency":"fb9b53d6-bbf6-472f-b6ba-73cc0d606c9b", + # "quoteCurrency":"620f2019-33c0-423b-8a9d-cde4d7f8ef7f", + # "priceTick":"0.000000100000000000", + # "priceDecimals":7, + # "quantityTick":"0.010000000", + # "quantityDecimals":2, + # "costDisplayDecimals":7, + # "created":1572957210501, + # "minOrderQuantity":"0", + # "maxOrderCostUsd":"999999999999999999", + # "minOrderCostUsd":"0", + # "externalSymbol":"" + # } + # ] + # + if self.safe_bool(self.options, 'adjustForTimeDifference', False): + await self.load_time_difference() + currencies = self.safe_dict(self.options, 'cachedCurrencies', {}) + currenciesById = self.index_by(currencies, 'id') + result = [] + for i in range(0, len(response)): + market = response[i] + id = self.safe_string(market, 'id') + # the exchange shows them inverted + baseId = self.safe_string(market, 'baseCurrency') + quoteId = self.safe_string(market, 'quoteCurrency') + baseCurrency = self.safe_dict(currenciesById, baseId) + quoteCurrency = self.safe_dict(currenciesById, quoteId) + baseCurrencyInfo = self.safe_dict(baseCurrency, 'info') + quoteCurrencyInfo = self.safe_dict(quoteCurrency, 'info') + if baseCurrencyInfo is not None and quoteCurrencyInfo is not None: + base = self.safe_currency_code(self.safe_string(baseCurrencyInfo, 'tag')) + quote = self.safe_currency_code(self.safe_string(quoteCurrencyInfo, 'tag')) + lowercaseQuote = quote.lower() + capitalizedQuote = self.capitalize(lowercaseQuote) + status = self.safe_string(market, 'status') + result.append({ + 'id': id, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': (status == 'PAIR_STATUS_ACTIVE'), # assuming True + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.safe_number(market, 'quantityTick'), + 'price': self.safe_number(market, 'priceTick'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(market, 'minOrderQuantity'), + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': self.safe_number(market, 'minOrderCost' + capitalizedQuote), + 'max': self.safe_number(market, 'maxOrderCost' + capitalizedQuote), + }, + }, + 'created': self.safe_integer(market, 'created'), + 'info': market, + }) + return result + + async def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = await self.publicGetCurrency(params) + # + # [ + # { + # "id":"1a075819-9e0b-48fc-8784-4dab1d186d6d", + # "status":"CURRENCY_STATUS_ACTIVE", + # "type":"CURRENCY_TYPE_ALTERNATIVE", # CURRENCY_TYPE_CRYPTO, CURRENCY_TYPE_IEO + # "name":"MyCryptoBank", + # "tag":"MCB", + # "description":"", + # "logo":"", + # "decimals":18, + # "created":1572912000000, + # "tier":1, + # "assetClass":"ASSET_CLASS_UNKNOWN", + # "minTransferAmount":0 + # }, + # { + # "id":"db02758e-2507-46a5-a805-7bc60355b3eb", + # "status":"CURRENCY_STATUS_ACTIVE", + # "type":"CURRENCY_TYPE_FUTURES_CONTRACT", + # "name":"BTC USDT Futures Contract", + # "tag":"BTCUSDT", + # "description":"", + # "logo":"", + # "decimals":8, + # "created":1589459984395, + # "tier":1, + # "assetClass":"ASSET_CLASS_UNKNOWN", + # "minTransferAmount":0 + # }, + # ] + # + result: dict = {} + for i in range(0, len(response)): + currency = response[i] + id = self.safe_string(currency, 'id') + tag = self.safe_string(currency, 'tag') + code = self.safe_currency_code(tag) + currencyType = self.safe_string(currency, 'type') + isCrypto = (currencyType == 'CURRENCY_TYPE_CRYPTO' or currencyType == 'CURRENCY_TYPE_IEO') + result[code] = self.safe_currency_structure({ + 'id': id, + 'code': code, + 'info': currency, + 'name': self.safe_string(currency, 'name'), + 'type': 'crypto' if isCrypto else 'other', + 'active': self.safe_string(currency, 'status') == 'CURRENCY_STATUS_ACTIVE', + 'deposit': None, + 'withdraw': None, + 'fee': self.safe_number(currency, 'fee'), + 'precision': self.parse_number(self.parse_precision(self.safe_string(currency, 'decimals'))), + 'limits': { + 'amount': { + 'min': self.safe_number(currency, 'minTransferAmount'), + 'max': None, + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + }, + 'networks': {}, + }) + return result + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://api.latoken.com/doc/v2/#tag/Account/operation/getBalancesByUser + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + response = await self.privateGetAuthAccount(params) + # + # [ + # { + # "id": "e5852e02-8711-431c-9749-a6f5503c6dbe", + # "status": "ACCOUNT_STATUS_ACTIVE", + # "type": "ACCOUNT_TYPE_WALLET", + # "timestamp": "1635920106506", + # "currency": "0c3a106d-bde3-4c13-a26e-3fd2394529e5", + # "available": "100.000000", + # "blocked": "0.000000" + # }, + # { + # "id": "369df204-acbc-467e-a25e-b16e3cc09cf6", + # "status": "ACCOUNT_STATUS_ACTIVE", + # "type": "ACCOUNT_TYPE_SPOT", + # "timestamp": "1635920106504", + # "currency": "0c3a106d-bde3-4c13-a26e-3fd2394529e5", + # "available": "100.000000", + # "blocked": "0.000000" + # } + # ] + # + result: dict = { + 'info': response, + 'timestamp': None, + 'datetime': None, + } + maxTimestamp = None + defaultType = self.safe_string_2(self.options, 'fetchBalance', 'defaultType', 'spot') + type = self.safe_string(params, 'type', defaultType) + types = self.safe_value(self.options, 'types', {}) + accountType = self.safe_string(types, type, type) + balancesByType = self.group_by(response, 'type') + balances = self.safe_value(balancesByType, accountType, []) + for i in range(0, len(balances)): + balance = balances[i] + currencyId = self.safe_string(balance, 'currency') + timestamp = self.safe_integer(balance, 'timestamp') + if timestamp is not None: + if maxTimestamp is None: + maxTimestamp = timestamp + else: + maxTimestamp = max(maxTimestamp, timestamp) + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(balance, 'available') + account['used'] = self.safe_string(balance, 'blocked') + result[code] = account + result['timestamp'] = maxTimestamp + result['datetime'] = self.iso8601(maxTimestamp) + return self.safe_balance(result) + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://api.latoken.com/doc/v2/#tag/Order-Book/operation/getOrderBook + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'currency': market['baseId'], + 'quote': market['quoteId'], + } + if limit is not None: + request['limit'] = limit # max 1000 + response = await self.publicGetBookCurrencyQuote(self.extend(request, params)) + # + # { + # "ask":[ + # {"price":"4428.76","quantity":"0.08136","cost":"360.3239136","accumulated":"360.3239136"}, + # {"price":"4429.77","quantity":"1.11786","cost":"4951.8626922","accumulated":"5312.1866058"}, + # {"price":"4430.94","quantity":"1.78418","cost":"7905.5945292","accumulated":"13217.781135"}, + # ], + # "bid":[ + # {"price":"4428.43","quantity":"0.13675","cost":"605.5878025","accumulated":"605.5878025"}, + # {"price":"4428.19","quantity":"0.03619","cost":"160.2561961","accumulated":"765.8439986"}, + # {"price":"4428.15","quantity":"0.02926","cost":"129.567669","accumulated":"895.4116676"}, + # ], + # "totalAsk":"53.14814", + # "totalBid":"112216.9029791" + # } + # + return self.parse_order_book(response, symbol, None, 'bid', 'ask', 'price', 'quantity') + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "symbol": "92151d82-df98-4d88-9a4d-284fa9eca49f/0c3a106d-bde3-4c13-a26e-3fd2394529e5", + # "baseCurrency": "92151d82-df98-4d88-9a4d-284fa9eca49f", + # "quoteCurrency": "0c3a106d-bde3-4c13-a26e-3fd2394529e5", + # "volume24h": "165723597.189022176000000000", + # "volume7d": "934505768.625109571000000000", + # "change24h": "0.0200", + # "change7d": "-6.4200", + # "amount24h": "6438.457663100000000000", + # "amount7d": "35657.785013800000000000", + # "lastPrice": "25779.16", + # "lastQuantity": "0.248403300000000000", + # "bestBid": "25778.74", + # "bestBidQuantity": "0.6520232", + # "bestAsk": "25779.17", + # "bestAskQuantity": "0.4956043", + # "updateTimestamp": "1693965231406" + # } + # + marketId = self.safe_string(ticker, 'symbol') + last = self.safe_string(ticker, 'lastPrice') + timestamp = self.safe_integer_omit_zero(ticker, 'updateTimestamp') # sometimes latoken provided '0' ts from /ticker endpoint + return self.safe_ticker({ + 'symbol': self.safe_symbol(marketId, market), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'low': None, + 'high': None, + 'bid': self.safe_string(ticker, 'bestBid'), + 'bidVolume': self.safe_string(ticker, 'bestBidQuantity'), + 'ask': self.safe_string(ticker, 'bestAsk'), + 'askVolume': self.safe_string(ticker, 'bestAskQuantity'), + 'vwap': None, + 'open': None, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': self.safe_string(ticker, 'change24h'), + 'average': None, + 'baseVolume': self.safe_string(ticker, 'amount24h'), + 'quoteVolume': self.safe_string(ticker, 'volume24h'), + 'info': ticker, + }, market) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://api.latoken.com/doc/v2/#tag/Ticker/operation/getTicker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'base': market['baseId'], + 'quote': market['quoteId'], + } + response = await self.publicGetTickerBaseQuote(self.extend(request, params)) + # + # { + # "symbol": "92151d82-df98-4d88-9a4d-284fa9eca49f/0c3a106d-bde3-4c13-a26e-3fd2394529e5", + # "baseCurrency": "92151d82-df98-4d88-9a4d-284fa9eca49f", + # "quoteCurrency": "0c3a106d-bde3-4c13-a26e-3fd2394529e5", + # "volume24h": "165723597.189022176000000000", + # "volume7d": "934505768.625109571000000000", + # "change24h": "0.0200", + # "change7d": "-6.4200", + # "amount24h": "6438.457663100000000000", + # "amount7d": "35657.785013800000000000", + # "lastPrice": "25779.16", + # "lastQuantity": "0.248403300000000000", + # "bestBid": "25778.74", + # "bestBidQuantity": "0.6520232", + # "bestAsk": "25779.17", + # "bestAskQuantity": "0.4956043", + # "updateTimestamp": "1693965231406" + # } + # + return self.parse_ticker(response, market) + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://api.latoken.com/doc/v2/#tag/Ticker/operation/getAllTickers + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + response = await self.publicGetTicker(params) + # + # [ + # { + # "symbol": "92151d82-df98-4d88-9a4d-284fa9eca49f/0c3a106d-bde3-4c13-a26e-3fd2394529e5", + # "baseCurrency": "92151d82-df98-4d88-9a4d-284fa9eca49f", + # "quoteCurrency": "0c3a106d-bde3-4c13-a26e-3fd2394529e5", + # "volume24h": "165723597.189022176000000000", + # "volume7d": "934505768.625109571000000000", + # "change24h": "0.0200", + # "change7d": "-6.4200", + # "amount24h": "6438.457663100000000000", + # "amount7d": "35657.785013800000000000", + # "lastPrice": "25779.16", + # "lastQuantity": "0.248403300000000000", + # "bestBid": "25778.74", + # "bestBidQuantity": "0.6520232", + # "bestAsk": "25779.17", + # "bestAskQuantity": "0.4956043", + # "updateTimestamp": "1693965231406" + # } + # ] + # + return self.parse_tickers(response, symbols) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(public) + # + # { + # "id":"c152f814-8eeb-44f0-8f3f-e5c568f2ffcf", + # "isMakerBuyer":false, + # "baseCurrency":"620f2019-33c0-423b-8a9d-cde4d7f8ef7f", + # "quoteCurrency":"0c3a106d-bde3-4c13-a26e-3fd2394529e5", + # "price":"4435.56", + # "quantity":"0.32534", + # "cost":"1443.0650904", + # "timestamp":1635854642725, + # "makerBuyer":false + # } + # + # fetchMyTrades(private) + # + # { + # "id":"02e02533-b4bf-4ba9-9271-24e2108dfbf7", + # "isMakerBuyer":false, + # "direction":"TRADE_DIRECTION_BUY", + # "baseCurrency":"620f2019-33c0-423b-8a9d-cde4d7f8ef7f", + # "quoteCurrency":"0c3a106d-bde3-4c13-a26e-3fd2394529e5", + # "price":"4564.32", + # "quantity":"0.01000", + # "cost":"45.6432", + # "fee":"0.223651680000000000", + # "order":"c9cac6a0-484c-4892-88e7-ad51b39f2ce1", + # "timestamp":1635921580399, + # "makerBuyer":false + # } + # + type = None + timestamp = self.safe_integer(trade, 'timestamp') + priceString = self.safe_string(trade, 'price') + amountString = self.safe_string(trade, 'quantity') + costString = self.safe_string(trade, 'cost') + makerBuyer = self.safe_value(trade, 'makerBuyer') + side = self.safe_string(trade, 'direction') + if side is None: + side = 'sell' if makerBuyer else 'buy' + else: + if side == 'TRADE_DIRECTION_BUY': + side = 'buy' + elif side == 'TRADE_DIRECTION_SELL': + side = 'sell' + isBuy = (side == 'buy') + takerOrMaker = 'maker' if (makerBuyer and isBuy) else 'taker' + baseId = self.safe_string(trade, 'baseCurrency') + quoteId = self.safe_string(trade, 'quoteCurrency') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + symbol = base + '/' + quote + if symbol in self.markets: + market = self.market(symbol) + id = self.safe_string(trade, 'id') + orderId = self.safe_string(trade, 'order') + feeCost = self.safe_string(trade, 'fee') + fee = None + if feeCost is not None: + fee = { + 'cost': feeCost, + 'currency': quote, + } + return self.safe_trade({ + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'id': id, + 'order': orderId, + 'type': type, + 'takerOrMaker': takerOrMaker, + 'side': side, + 'price': priceString, + 'amount': amountString, + 'cost': costString, + 'fee': fee, + }, market) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://api.latoken.com/doc/v2/#tag/Trade/operation/getTradesByPair + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'currency': market['baseId'], + 'quote': market['quoteId'], + # 'from': str(since), # milliseconds + # 'limit': limit, # default 100, limit 100 + } + if limit is not None: + request['limit'] = min(limit, 100) # default 100, limit 100 + response = await self.publicGetTradeHistoryCurrencyQuote(self.extend(request, params)) + # + # [ + # {"id":"c152f814-8eeb-44f0-8f3f-e5c568f2ffcf","isMakerBuyer":false,"baseCurrency":"620f2019-33c0-423b-8a9d-cde4d7f8ef7f","quoteCurrency":"0c3a106d-bde3-4c13-a26e-3fd2394529e5","price":"4435.56","quantity":"0.32534","cost":"1443.0650904","timestamp":1635854642725,"makerBuyer":false}, + # {"id":"cfecbefb-3d11-43d7-b9d4-fa16211aad8a","isMakerBuyer":false,"baseCurrency":"620f2019-33c0-423b-8a9d-cde4d7f8ef7f","quoteCurrency":"0c3a106d-bde3-4c13-a26e-3fd2394529e5","price":"4435.13","quantity":"0.26540","cost":"1177.083502","timestamp":1635854641114,"makerBuyer":false}, + # {"id":"f43d3ec8-db94-49f3-b534-91dbc2779296","isMakerBuyer":true,"baseCurrency":"620f2019-33c0-423b-8a9d-cde4d7f8ef7f","quoteCurrency":"0c3a106d-bde3-4c13-a26e-3fd2394529e5","price":"4435.00","quantity":"0.41738","cost":"1851.0803","timestamp":1635854640323,"makerBuyer":true}, + # ] + # + return self.parse_trades(response, market, since, limit) + + async def fetch_trading_fee(self, symbol: str, params={}) -> TradingFeeInterface: + """ + fetch the trading fees for a market + + https://api.latoken.com/doc/v2/#tag/Trade/operation/getFeeByPair + https://api.latoken.com/doc/v2/#tag/Trade/operation/getAuthFeeByPair + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `fee structure ` + """ + options = self.safe_value(self.options, 'fetchTradingFee', {}) + defaultMethod = self.safe_string(options, 'method', 'fetchPrivateTradingFee') + method = self.safe_string(params, 'method', defaultMethod) + params = self.omit(params, 'method') + if method == 'fetchPrivateTradingFee': + return await self.fetch_private_trading_fee(symbol, params) + elif method == 'fetchPublicTradingFee': + return await self.fetch_public_trading_fee(symbol, params) + else: + raise NotSupported(self.id + ' not support self method') + + async def fetch_public_trading_fee(self, symbol: str, params={}): + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'currency': market['baseId'], + 'quote': market['quoteId'], + } + response = await self.publicGetTradeFeeCurrencyQuote(self.extend(request, params)) + # + # { + # "makerFee": "0.004900000000000000", + # "takerFee": "0.004900000000000000", + # "type": "FEE_SCHEME_TYPE_PERCENT_QUOTE", + # "take": "FEE_SCHEME_TAKE_PROPORTION" + # } + # + return { + 'info': response, + 'symbol': market['symbol'], + 'maker': self.safe_number(response, 'makerFee'), + 'taker': self.safe_number(response, 'takerFee'), + 'percentage': None, + 'tierBased': None, + } + + async def fetch_private_trading_fee(self, symbol: str, params={}): + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'currency': market['baseId'], + 'quote': market['quoteId'], + } + response = await self.privateGetAuthTradeFeeCurrencyQuote(self.extend(request, params)) + # + # { + # "makerFee": "0.004900000000000000", + # "takerFee": "0.004900000000000000", + # "type": "FEE_SCHEME_TYPE_PERCENT_QUOTE", + # "take": "FEE_SCHEME_TAKE_PROPORTION" + # } + # + return { + 'info': response, + 'symbol': market['symbol'], + 'maker': self.safe_number(response, 'makerFee'), + 'taker': self.safe_number(response, 'takerFee'), + 'percentage': None, + 'tierBased': None, + } + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://api.latoken.com/doc/v2/#tag/Trade/operation/getTradesByTrader + https://api.latoken.com/doc/v2/#tag/Trade/operation/getTradesByAssetAndTrader + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + request: dict = { + # 'currency': market['baseId'], + # 'quote': market['quoteId'], + # 'from': self.milliseconds(), + # 'limit': limit, # default '100' + } + market = None + if limit is not None: + request['limit'] = limit # default 100 + response = None + if symbol is not None: + market = self.market(symbol) + request['currency'] = market['baseId'] + request['quote'] = market['quoteId'] + response = await self.privateGetAuthTradePairCurrencyQuote(self.extend(request, params)) + else: + response = await self.privateGetAuthTrade(self.extend(request, params)) + # + # [ + # { + # "id":"02e02533-b4bf-4ba9-9271-24e2108dfbf7", + # "isMakerBuyer":false, + # "direction":"TRADE_DIRECTION_BUY", + # "baseCurrency":"620f2019-33c0-423b-8a9d-cde4d7f8ef7f", + # "quoteCurrency":"0c3a106d-bde3-4c13-a26e-3fd2394529e5", + # "price":"4564.32", + # "quantity":"0.01000", + # "cost":"45.6432", + # "fee":"0.223651680000000000", + # "order":"c9cac6a0-484c-4892-88e7-ad51b39f2ce1", + # "timestamp":1635921580399, + # "makerBuyer":false + # } + # ] + # + return self.parse_trades(response, market, since, limit) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'ORDER_STATUS_PLACED': 'open', + 'ORDER_STATUS_CLOSED': 'closed', + 'ORDER_STATUS_CANCELLED': 'canceled', + } + return self.safe_string(statuses, status, status) + + def parse_order_type(self, status): + statuses: dict = { + 'ORDER_TYPE_MARKET': 'market', + 'ORDER_TYPE_LIMIT': 'limit', + } + return self.safe_string(statuses, status, status) + + def parse_time_in_force(self, timeInForce: Str): + timeInForces: dict = { + 'ORDER_CONDITION_GOOD_TILL_CANCELLED': 'GTC', + 'ORDER_CONDITION_IMMEDIATE_OR_CANCEL': 'IOC', + 'ORDER_CONDITION_FILL_OR_KILL': 'FOK', + } + return self.safe_string(timeInForces, timeInForce, timeInForce) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder + # + # { + # "baseCurrency": "f7dac554-8139-4ff6-841f-0e586a5984a0", + # "quoteCurrency": "a5a7a7a9-e2a3-43f9-8754-29a02f6b709b", + # "side": "BID", + # "clientOrderId": "my-wonderful-order-number-71566", + # "price": "10103.19", + # "stopPrice": "10103.19", + # "quantity": "3.21", + # "timestamp": 1568185507 + # } + # + # fetchOrder, fetchOpenOrders, fetchOrders + # + # { + # "id":"a76bd262-3560-4bfb-98ac-1cedd394f4fc", + # "status":"ORDER_STATUS_PLACED", + # "side":"ORDER_SIDE_BUY", + # "condition":"ORDER_CONDITION_GOOD_TILL_CANCELLED", + # "type":"ORDER_TYPE_LIMIT", + # "baseCurrency":"620f2019-33c0-423b-8a9d-cde4d7f8ef7f", + # "quoteCurrency":"0c3a106d-bde3-4c13-a26e-3fd2394529e5", + # "clientOrderId":"web-macos_chrome_1a6a6659-6f7c-4fac-be0b-d1d7ac06d", + # "price":"4000.00", + # "quantity":"0.01", + # "cost":"40.000000000000000000", + # "filled":"0", + # "trader":"7244bb3a-b6b2-446a-ac78-fa4bce5b59a9", + # "creator":"ORDER_CREATOR_USER", + # "creatorId":"", + # "timestamp":1635920767648 + # } + # + # cancelOrder + # + # { + # "message":"cancellation request successfully submitted", + # "status":"SUCCESS", + # "id":"a631426d-3543-45ba-941e-75f7825afb0f" + # } + # + id = self.safe_string(order, 'id') + timestamp = self.safe_integer(order, 'timestamp') + baseId = self.safe_string(order, 'baseCurrency') + quoteId = self.safe_string(order, 'quoteCurrency') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + symbol = None + if (base is not None) and (quote is not None): + symbol = base + '/' + quote + if symbol in self.markets: + market = self.market(symbol) + orderSide = self.safe_string(order, 'side') + side = None + if orderSide is not None: + parts = orderSide.split('_') + partsLength = len(parts) + side = self.safe_string_lower(parts, partsLength - 1) + type = self.parse_order_type(self.safe_string(order, 'type')) + price = self.safe_string(order, 'price') + amount = self.safe_string(order, 'quantity') + filled = self.safe_string(order, 'filled') + cost = self.safe_string(order, 'cost') + status = self.parse_order_status(self.safe_string(order, 'status')) + message = self.safe_string(order, 'message') + if message is not None: + if message.find('cancel') >= 0: + status = 'canceled' + elif message.find('accept') >= 0: + status = 'open' + clientOrderId = self.safe_string(order, 'clientOrderId') + timeInForce = self.parse_time_in_force(self.safe_string(order, 'condition')) + return self.safe_order({ + 'id': id, + 'clientOrderId': clientOrderId, + 'info': order, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'status': status, + 'symbol': symbol, + 'type': type, + 'timeInForce': timeInForce, + 'postOnly': None, + 'side': side, + 'price': price, + 'triggerPrice': self.safe_string(order, 'stopPrice'), + 'cost': cost, + 'amount': amount, + 'filled': filled, + 'average': None, + 'remaining': None, + 'fee': None, + 'trades': None, + }, market) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://api.latoken.com/doc/v2/#tag/Order/operation/getMyActiveOrdersByPair + https://api.latoken.com/doc/v2/#tag/StopOrder/operation/getMyActiveStopOrdersByPair # stop + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: True if fetching trigger orders + :returns Order[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOpenOrders() requires a symbol argument') + await self.load_markets() + response = None + isTrigger = self.safe_value_2(params, 'trigger', 'stop') + params = self.omit(params, 'stop') + # privateGetAuthOrderActive doesn't work even though its listed at https://api.latoken.com/doc/v2/#tag/Order/operation/getMyActiveOrders + market = self.market(symbol) + request: dict = { + 'currency': market['baseId'], + 'quote': market['quoteId'], + } + if isTrigger: + response = await self.privateGetAuthStopOrderPairCurrencyQuoteActive(self.extend(request, params)) + else: + response = await self.privateGetAuthOrderPairCurrencyQuoteActive(self.extend(request, params)) + # + # [ + # { + # "id":"a76bd262-3560-4bfb-98ac-1cedd394f4fc", + # "status":"ORDER_STATUS_PLACED", + # "side":"ORDER_SIDE_BUY", + # "condition":"ORDER_CONDITION_GOOD_TILL_CANCELLED", + # "type":"ORDER_TYPE_LIMIT", + # "baseCurrency":"620f2019-33c0-423b-8a9d-cde4d7f8ef7f", + # "quoteCurrency":"0c3a106d-bde3-4c13-a26e-3fd2394529e5", + # "clientOrderId":"web-macos_chrome_1a6a6659-6f7c-4fac-be0b-d1d7ac06d", + # "price":"4000.00", + # "quantity":"0.01000", + # "cost":"40.00", + # "filled":"0.00000", + # "trader":"7244bb3a-b6b2-446a-ac78-fa4bce5b59a9", + # "creator":"USER", + # "creatorId":"", + # "timestamp":1635920767648 + # } + # ] + # + return self.parse_orders(response, market, since, limit) + + async def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://api.latoken.com/doc/v2/#tag/Order/operation/getMyOrders + https://api.latoken.com/doc/v2/#tag/Order/operation/getMyOrdersByPair + https://api.latoken.com/doc/v2/#tag/StopOrder/operation/getMyStopOrders # stop + https://api.latoken.com/doc/v2/#tag/StopOrder/operation/getMyStopOrdersByPair # stop + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: True if fetching trigger orders + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + request: dict = { + # 'currency': market['baseId'], + # 'quote': market['quoteId'], + # 'from': self.milliseconds(), + # 'limit': limit, # default '100' + } + market = None + isTrigger = self.safe_value_2(params, 'trigger', 'stop') + params = self.omit(params, ['stop', 'trigger']) + if limit is not None: + request['limit'] = limit # default 100 + response = None + if symbol is not None: + market = self.market(symbol) + request['currency'] = market['baseId'] + request['quote'] = market['quoteId'] + if isTrigger: + response = await self.privateGetAuthStopOrderPairCurrencyQuote(self.extend(request, params)) + else: + response = await self.privateGetAuthOrderPairCurrencyQuote(self.extend(request, params)) + else: + if isTrigger: + response = await self.privateGetAuthStopOrder(self.extend(request, params)) + else: + response = await self.privateGetAuthOrder(self.extend(request, params)) + # + # [ + # { + # "id":"a76bd262-3560-4bfb-98ac-1cedd394f4fc", + # "status":"ORDER_STATUS_PLACED", + # "side":"ORDER_SIDE_BUY", + # "condition":"ORDER_CONDITION_GOOD_TILL_CANCELLED", + # "type":"ORDER_TYPE_LIMIT", + # "baseCurrency":"620f2019-33c0-423b-8a9d-cde4d7f8ef7f", + # "quoteCurrency":"0c3a106d-bde3-4c13-a26e-3fd2394529e5", + # "clientOrderId":"web-macos_chrome_1a6a6659-6f7c-4fac-be0b-d1d7ac06d", + # "price":"4000.00", + # "quantity":"0.01000", + # "cost":"40.00", + # "filled":"0.00000", + # "trader":"7244bb3a-b6b2-446a-ac78-fa4bce5b59a9", + # "creator":"USER", + # "creatorId":"", + # "timestamp":1635920767648 + # } + # ] + # + return self.parse_orders(response, market, since, limit) + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://api.latoken.com/doc/v2/#tag/Order/operation/getOrderById + https://api.latoken.com/doc/v2/#tag/StopOrder/operation/getStopOrderById + + :param str id: order id + :param str [symbol]: not used by latoken fetchOrder + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: True if fetching a trigger order + :returns dict: An `order structure ` + """ + await self.load_markets() + request: dict = { + 'id': id, + } + isTrigger = self.safe_value_2(params, 'trigger', 'stop') + params = self.omit(params, ['stop', 'trigger']) + response = None + if isTrigger: + response = await self.privateGetAuthStopOrderGetOrderId(self.extend(request, params)) + else: + response = await self.privateGetAuthOrderGetOrderId(self.extend(request, params)) + # + # { + # "id":"a76bd262-3560-4bfb-98ac-1cedd394f4fc", + # "status":"ORDER_STATUS_PLACED", + # "side":"ORDER_SIDE_BUY", + # "condition":"ORDER_CONDITION_GOOD_TILL_CANCELLED", + # "type":"ORDER_TYPE_LIMIT", + # "baseCurrency":"620f2019-33c0-423b-8a9d-cde4d7f8ef7f", + # "quoteCurrency":"0c3a106d-bde3-4c13-a26e-3fd2394529e5", + # "clientOrderId":"web-macos_chrome_1a6a6659-6f7c-4fac-be0b-d1d7ac06d", + # "price":"4000.00", + # "quantity":"0.01", + # "cost":"40.000000000000000000", + # "filled":"0", + # "trader":"7244bb3a-b6b2-446a-ac78-fa4bce5b59a9", + # "creator":"ORDER_CREATOR_USER", + # "creatorId":"", + # "timestamp":1635920767648 + # } + # + return self.parse_order(response) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://api.latoken.com/doc/v2/#tag/Order/operation/placeOrder + https://api.latoken.com/doc/v2/#tag/StopOrder/operation/placeStopOrder # stop + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: the price at which a trigger order is triggered at + + EXCHANGE SPECIFIC PARAMETERS + :param str [params.condition]: "GTC", "IOC", or "FOK" + :param str [params.clientOrderId]: [0 .. 50] characters, client's custom order id(free field for your convenience) + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + uppercaseType = type.upper() + request: dict = { + 'baseCurrency': market['baseId'], + 'quoteCurrency': market['quoteId'], + 'side': side.upper(), # "BUY", "BID", "SELL", "ASK" + 'condition': 'GTC', # "GTC", "GOOD_TILL_CANCELLED", "IOC", "IMMEDIATE_OR_CANCEL", "FOK", "FILL_OR_KILL" + 'type': uppercaseType, # "LIMIT", "MARKET" + 'clientOrderId': self.uuid(), # 50 characters max + # 'price': self.price_to_precision(symbol, price), + # 'quantity': self.amount_to_precision(symbol, amount), + 'quantity': self.amount_to_precision(symbol, amount), + 'timestamp': self.seconds(), + } + if uppercaseType == 'LIMIT': + request['price'] = self.price_to_precision(symbol, price) + triggerPrice = self.safe_string_2(params, 'triggerPrice', 'stopPrice') + params = self.omit(params, ['triggerPrice', 'stopPrice']) + response = None + if triggerPrice is not None: + request['stopPrice'] = self.price_to_precision(symbol, triggerPrice) + response = await self.privatePostAuthStopOrderPlace(self.extend(request, params)) + else: + response = await self.privatePostAuthOrderPlace(self.extend(request, params)) + # + # { + # "baseCurrency": "f7dac554-8139-4ff6-841f-0e586a5984a0", + # "quoteCurrency": "a5a7a7a9-e2a3-43f9-8754-29a02f6b709b", + # "side": "BID", + # "clientOrderId": "my-wonderful-order-number-71566", + # "price": "10103.19", + # "stopPrice": "10103.19", + # "quantity": "3.21", + # "timestamp": 1568185507 + # } + # + return self.parse_order(response, market) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://api.latoken.com/doc/v2/#tag/Order/operation/cancelOrder + https://api.latoken.com/doc/v2/#tag/StopOrder/operation/cancelStopOrder # stop + + :param str id: order id + :param str symbol: not used by latoken cancelOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: True if cancelling a trigger order + :returns dict: An `order structure ` + """ + await self.load_markets() + request: dict = { + 'id': id, + } + isTrigger = self.safe_value_2(params, 'trigger', 'stop') + params = self.omit(params, ['stop', 'trigger']) + response = None + if isTrigger: + response = await self.privatePostAuthStopOrderCancel(self.extend(request, params)) + else: + response = await self.privatePostAuthOrderCancel(self.extend(request, params)) + # + # { + # "id": "12345678-1234-1244-1244-123456789012", + # "message": "cancellation request successfully submitted", + # "status": "SUCCESS", + # "error": "", + # "errors": {} + # } + # + return self.parse_order(response) + + async def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders in a market + + https://api.latoken.com/doc/v2/#tag/Order/operation/cancelAllOrders + https://api.latoken.com/doc/v2/#tag/Order/operation/cancelAllOrdersByPair + + :param str symbol: unified market symbol of the market to cancel orders in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: True if cancelling trigger orders + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + request: dict = { + # 'currency': market['baseId'], + # 'quote': market['quoteId'], + } + market = None + isTrigger = self.safe_value_2(params, 'trigger', 'stop') + params = self.omit(params, ['stop', 'trigger']) + response = None + if symbol is not None: + market = self.market(symbol) + request['currency'] = market['baseId'] + request['quote'] = market['quoteId'] + if isTrigger: + response = await self.privatePostAuthStopOrderCancelAllCurrencyQuote(self.extend(request, params)) + else: + response = await self.privatePostAuthOrderCancelAllCurrencyQuote(self.extend(request, params)) + else: + if isTrigger: + response = await self.privatePostAuthStopOrderCancelAll(self.extend(request, params)) + else: + response = await self.privatePostAuthOrderCancelAll(self.extend(request, params)) + # + # { + # "message":"cancellation request successfully submitted", + # "status":"SUCCESS" + # } + # + return [ + self.safe_order({ + 'info': response, + }), + ] + + async def fetch_transactions(self, code: Str = None, since: Int = None, limit: Int = None, params={}): + """ + @deprecated + use fetchDepositsWithdrawals instead + + https://api.latoken.com/doc/v2/#tag/Transaction/operation/getUserTransactions + + :param str code: unified currency code for the currency of the transactions, default is None + :param int [since]: timestamp in ms of the earliest transaction, default is None + :param int [limit]: max number of transactions to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `transaction structure ` + """ + await self.load_markets() + request: dict = { + # 'page': '1', + # 'size': 100, + } + response = await self.privateGetAuthTransaction(self.extend(request, params)) + # + # { + # "hasNext":false, + # "content":[ + # { + # "id":"fbf7d0d1-2629-4ad8-9def-7a1dba423362", + # "status":"TRANSACTION_STATUS_CONFIRMED", + # "type":"TRANSACTION_TYPE_DEPOSIT", + # "senderAddress":"", + # "recipientAddress":"0x3c46fa2e3f9023bc4897828ed173f8ecb3a554bc", + # "amount":"200.000000000000000000", + # "transactionFee":"0.000000000000000000", + # "timestamp":1635893208404, + # "transactionHash":"0x28bad3b74a042df13d64ddfbca855566a51bf7f190b8cd565c236a18d5cd493f#42", + # "blockHeight":13540262, + # "currency":"0c3a106d-bde3-4c13-a26e-3fd2394529e5", + # "memo":null, + # "paymentProvider":"a8d6d1cb-f84a-4e9d-aa82-c6a08b356ee1", + # "requiresCode":false + # } + # ], + # "first":true, + # "hasContent":true, + # "pageSize":10 + # } + # + currency = None + if code is not None: + currency = self.currency(code) + content = self.safe_list(response, 'content', []) + return self.parse_transactions(content, currency, since, limit) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # { + # "id":"fbf7d0d1-2629-4ad8-9def-7a1dba423362", + # "status":"TRANSACTION_STATUS_CONFIRMED", + # "type":"TRANSACTION_TYPE_DEPOSIT", + # "senderAddress":"", + # "recipientAddress":"0x3c46fa2e3f9023bc4897828ed173f8ecb3a554bc", + # "amount":"200.000000000000000000", + # "transactionFee":"0.000000000000000000", + # "timestamp":1635893208404, + # "transactionHash":"0x28bad3b74a042df13d64ddfbca855566a51bf7f190b8cd565c236a18d5cd493f#42", + # "blockHeight":13540262, + # "currency":"0c3a106d-bde3-4c13-a26e-3fd2394529e5", + # "memo":null, + # "paymentProvider":"a8d6d1cb-f84a-4e9d-aa82-c6a08b356ee1", + # "requiresCode":false + # } + # + id = self.safe_string(transaction, 'id') + timestamp = self.safe_integer(transaction, 'timestamp') + currencyId = self.safe_string(transaction, 'currency') + code = self.safe_currency_code(currencyId, currency) + status = self.parse_transaction_status(self.safe_string(transaction, 'status')) + amount = self.safe_number(transaction, 'amount') + addressFrom = self.safe_string(transaction, 'senderAddress') + addressTo = self.safe_string(transaction, 'recipientAddress') + txid = self.safe_string(transaction, 'transactionHash') + tagTo = self.safe_string(transaction, 'memo') + fee = { + 'currency': None, + 'cost': None, + 'rate': None, + } + feeCost = self.safe_number(transaction, 'transactionFee') + if feeCost is not None: + fee['cost'] = feeCost + fee['currency'] = code + type = self.parse_transaction_type(self.safe_string(transaction, 'type')) + return { + 'info': transaction, + 'id': id, + 'txid': txid, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': None, + 'addressFrom': addressFrom, + 'addressTo': addressTo, + 'address': addressTo, + 'tagFrom': None, + 'tagTo': tagTo, + 'tag': tagTo, + 'type': type, + 'amount': amount, + 'currency': code, + 'status': status, + 'updated': None, + 'comment': None, + 'internal': None, + 'fee': fee, + } + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'TRANSACTION_STATUS_CONFIRMED': 'ok', + 'TRANSACTION_STATUS_EXECUTED': 'ok', + 'TRANSACTION_STATUS_CHECKING': 'pending', + 'TRANSACTION_STATUS_CANCELLED': 'canceled', + 'TRANSACTION_STATUS_FAILED': 'failed', + 'TRANSACTION_STATUS_REJECTED': 'rejected', + } + return self.safe_string(statuses, status, status) + + def parse_transaction_type(self, type): + types: dict = { + 'TRANSACTION_TYPE_DEPOSIT': 'deposit', + 'TRANSACTION_TYPE_WITHDRAWAL': 'withdrawal', + } + return self.safe_string(types, type, type) + + async def fetch_transfers(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[TransferEntry]: + """ + fetch a history of internal transfers made on an account + + https://api.latoken.com/doc/v2/#tag/Transfer/operation/getUsersTransfers + + :param str code: unified currency code of the currency transferred + :param int [since]: the earliest time in ms to fetch transfers for + :param int [limit]: the maximum number of transfers structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transfer structures ` + """ + await self.load_markets() + currency = self.currency(code) + response = await self.privateGetAuthTransfer(params) + # + # { + # "hasNext": True, + # "content": [ + # { + # "id": "ebd6312f-cb4f-45d1-9409-4b0b3027f21e", + # "status": "TRANSFER_STATUS_COMPLETED", + # "type": "TRANSFER_TYPE_WITHDRAW_SPOT", + # "fromAccount": "c429c551-adbb-4078-b74b-276bea308a36", + # "toAccount": "631c6203-bd62-4734-a04d-9b2a951f43b9", + # "transferringFunds": 1259.0321785, + # "usdValue": 1259.032179, + # "rejectReason": null, + # "timestamp": 1633515579530, + # "direction": "INTERNAL", + # "method": "TRANSFER_METHOD_UNKNOWN", + # "recipient": null, + # "sender": null, + # "currency": "0c3a106d-bde3-4c13-a26e-3fd2394529e5", + # "codeRequired": False, + # "fromUser": "ce555f3f-585d-46fb-9ae6-487f66738073", + # "toUser": "ce555f3f-585d-46fb-9ae6-487f66738073", + # "fee": 0 + # }, + # ... + # ], + # "first": True, + # "pageSize": 20, + # "hasContent": True + # } + # + transfers = self.safe_list(response, 'content', []) + return self.parse_transfers(transfers, currency, since, limit) + + async def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://api.latoken.com/doc/v2/#tag/Transfer/operation/transferByEmail + https://api.latoken.com/doc/v2/#tag/Transfer/operation/transferById + https://api.latoken.com/doc/v2/#tag/Transfer/operation/transferByPhone + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account to transfer from + :param str toAccount: account to transfer to + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transfer structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + 'recipient': toAccount, + 'value': self.currency_to_precision(code, amount), + } + response = None + if toAccount.find('@') >= 0: + response = await self.privatePostAuthTransferEmail(self.extend(request, params)) + elif len(toAccount) == 36: + response = await self.privatePostAuthTransferId(self.extend(request, params)) + else: + response = await self.privatePostAuthTransferPhone(self.extend(request, params)) + # + # { + # "id": "e6fc4ace-7750-44e4-b7e9-6af038ac7107", + # "status": "TRANSFER_STATUS_COMPLETED", + # "type": "TRANSFER_TYPE_DEPOSIT_SPOT", + # "fromAccount": "3bf61015-bf32-47a6-b237-c9f70df772ad", + # "toAccount": "355eb279-7c7e-4515-814a-575a49dc0325", + # "transferringFunds": "500000.000000000000000000", + # "usdValue": "0.000000000000000000", + # "rejectReason": "", + # "timestamp": 1576844438402, + # "direction": "INTERNAL", + # "method": "TRANSFER_METHOD_UNKNOWN", + # "recipient": "", + # "sender": "", + # "currency": "40af7879-a8cc-4576-a42d-7d2749821b58", + # "codeRequired": False, + # "fromUser": "cd555555-666d-46fb-9ae6-487f66738073", + # "toUser": "cd555555-666d-46fb-9ae6-487f66738073", + # "fee": 0 + # } + # + return self.parse_transfer(response) + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + # + # { + # "id": "e6fc4ace-7750-44e4-b7e9-6af038ac7107", + # "status": "TRANSFER_STATUS_COMPLETED", + # "type": "TRANSFER_TYPE_DEPOSIT_SPOT", + # "fromAccount": "3bf61015-bf32-47a6-b237-c9f70df772ad", + # "toAccount": "355eb279-7c7e-4515-814a-575a49dc0325", + # "transferringFunds": "500000.000000000000000000", + # "usdValue": "0.000000000000000000", + # "rejectReason": "", + # "timestamp": 1576844438402, + # "direction": "INTERNAL", + # "method": "TRANSFER_METHOD_UNKNOWN", + # "recipient": "", + # "sender": "", + # "currency": "40af7879-a8cc-4576-a42d-7d2749821b58", + # "codeRequired": False, + # "fromUser": "cd555555-666d-46fb-9ae6-487f66738073", + # "toUser": "cd555555-666d-46fb-9ae6-487f66738073", + # "fee": 0 + # } + # + timestamp = self.safe_timestamp(transfer, 'timestamp') + currencyId = self.safe_string(transfer, 'currency') + status = self.safe_string(transfer, 'status') + return { + 'info': transfer, + 'id': self.safe_string(transfer, 'id'), + 'timestamp': self.safe_integer(transfer, 'timestamp'), + 'datetime': self.iso8601(timestamp), + 'currency': self.safe_currency_code(currencyId, currency), + 'amount': self.safe_number(transfer, 'transferringFunds'), + 'fromAccount': self.safe_string(transfer, 'fromAccount'), + 'toAccount': self.safe_string(transfer, 'toAccount'), + 'status': self.parse_transfer_status(status), + } + + def parse_transfer_status(self, status: Str) -> Str: + statuses: dict = { + 'TRANSFER_STATUS_COMPLETED': 'ok', + 'TRANSFER_STATUS_PENDING': 'pending', + 'TRANSFER_STATUS_REJECTED': 'failed', + 'TRANSFER_STATUS_UNVERIFIED': 'pending', + 'TRANSFER_STATUS_CANCELLED': 'canceled', + } + return self.safe_string(statuses, status, status) + + def sign(self, path, api='public', method='GET', params=None, headers=None, body=None): + request = '/' + self.version + '/' + self.implode_params(path, params) + requestString = request + query = self.omit(params, self.extract_params(path)) + urlencodedQuery = self.urlencode(query) + if method == 'GET': + if query: + requestString += '?' + urlencodedQuery + if api == 'private': + self.check_required_credentials() + auth = method + request + urlencodedQuery + signature = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha512) + headers = { + 'X-LA-APIKEY': self.apiKey, + 'X-LA-SIGNATURE': signature, + 'X-LA-DIGEST': 'HMAC-SHA512', # HMAC-SHA384, HMAC-SHA512, optional + } + if method == 'POST': + headers['Content-Type'] = 'application/json' + body = self.json(query) + url = self.urls['api']['rest'] + requestString + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if not response: + return None + # + # {"result":false,"message":"invalid API key, signature or digest","error":"BAD_REQUEST","status":"FAILURE"} + # {"result":false,"message":"request expired or bad / format","error":"BAD_REQUEST","status":"FAILURE"} + # {"message":"Internal Server Error","error":"INTERNAL_ERROR","status":"FAILURE"} + # {"result":false,"message":"Internal error","error":"For input string: \"NaN\"","status":"FAILURE"} + # + message = self.safe_string(response, 'message') + feedback = self.id + ' ' + body + if message is not None: + self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + error = self.safe_value(response, 'error') + errorMessage = self.safe_string(error, 'message') + if (error is not None) or (errorMessage is not None): + self.throw_exactly_matched_exception(self.exceptions['exact'], error, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], body, feedback) + raise ExchangeError(feedback) # unknown message + return None diff --git a/ccxt/async_support/lbank.py b/ccxt/async_support/lbank.py new file mode 100644 index 0000000..c7a9b01 --- /dev/null +++ b/ccxt/async_support/lbank.py @@ -0,0 +1,3013 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.lbank import ImplicitAPI +import asyncio +import hashlib +from ccxt.base.types import Any, Balances, Currencies, Currency, DepositAddress, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, FundingRate, FundingRates, Trade, TradingFeeInterface, TradingFees, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidAddress +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import DuplicateOrderId +from ccxt.base.errors import NotSupported +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import InvalidNonce +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class lbank(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(lbank, self).describe(), { + 'id': 'lbank', + 'name': 'LBank', + 'countries': ['CN'], + 'version': 'v2', + # 50 per second for making and cancelling orders 1000ms / 50 = 20 + # 20 per second for all other requests, cost = 50 / 20 = 2.5 + 'rateLimit': 20, + 'pro': True, + 'has': { + 'CORS': False, + 'spot': True, + 'margin': False, + 'swap': None, + 'future': False, + 'option': False, + 'addMargin': False, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'createMarketBuyOrderWithCost': True, + 'createMarketOrderWithCost': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createReduceOnlyOrder': False, + 'createStopLimitOrder': False, + 'createStopMarketOrder': False, + 'createStopOrder': False, + 'fetchBalance': True, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchClosedOrders': False, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDepositWithdrawFee': 'emulated', + 'fetchDepositWithdrawFees': True, + 'fetchFundingHistory': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': True, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLeverage': False, + 'fetchLeverageTiers': False, + 'fetchMarginMode': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchPosition': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFees': True, + 'fetchTransactionFees': True, + 'reduceMargin': False, + 'setLeverage': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'withdraw': True, + }, + 'timeframes': { + '1m': 'minute1', + '5m': 'minute5', + '15m': 'minute15', + '30m': 'minute30', + '1h': 'hour1', + '2h': 'hour2', + '4h': 'hour4', + '6h': 'hour6', + '8h': 'hour8', + '12h': 'hour12', + '1d': 'day1', + '1w': 'week1', + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/38063602-9605e28a-3302-11e8-81be-64b1e53c4cfb.jpg', + 'api': { + 'rest': 'https://api.lbank.info', + 'contract': 'https://lbkperp.lbank.com', + }, + 'api2': 'https://api.lbkex.com', + 'www': 'https://www.lbank.com', + 'doc': 'https://www.lbank.com/en-US/docs/index.html', + 'fees': 'https://support.lbank.site/hc/en-gb/articles/900000535703-Trading-Fees-From-14-00-on-April-7-2020-UTC-8-', + 'referral': 'https://www.lbank.com/login/?icode=7QCY', + }, + 'api': { + 'spot': { + 'public': { + 'get': { + 'currencyPairs': 2.5, + 'accuracy': 2.5, + 'usdToCny': 2.5, + 'assetConfigs': 2.5, + 'withdrawConfigs': 2.5 * 1.5, # frequently rate-limits, so increase self endpoint RL + 'timestamp': 2.5, + 'ticker/24hr': 2.5, + 'ticker': 2.5, + 'depth': 2.5, + 'incrDepth': 2.5, + 'trades': 2.5, + 'kline': 2.5, + # new quote endpoints + 'supplement/system_ping': 2.5, + 'supplement/incrDepth': 2.5, + 'supplement/trades': 2.5, + 'supplement/ticker/price': 2.5, + 'supplement/ticker/bookTicker': 2.5, + }, + 'post': { + 'supplement/system_status': 2.5, + }, + }, + 'private': { + 'post': { + # account + 'user_info': 2.5, + 'subscribe/get_key': 2.5, + 'subscribe/refresh_key': 2.5, + 'subscribe/destroy_key': 2.5, + 'get_deposit_address': 2.5, + 'deposit_history': 2.5, + # order + 'create_order': 1, + 'batch_create_order': 1, + 'cancel_order': 1, + 'cancel_clientOrders': 1, + 'orders_info': 2.5, + 'orders_info_history': 2.5, + 'order_transaction_detail': 2.5, + 'transaction_history': 2.5, + 'orders_info_no_deal': 2.5, + # withdraw + 'withdraw': 2.5, + 'withdrawCancel': 2.5, + 'withdraws': 2.5, + 'supplement/user_info': 2.5, + 'supplement/withdraw': 2.5, + 'supplement/deposit_history': 2.5, + 'supplement/withdraws': 2.5, + 'supplement/get_deposit_address': 2.5, + 'supplement/asset_detail': 2.5, + 'supplement/customer_trade_fee': 2.5, + 'supplement/api_Restrictions': 2.5, + # new quote endpoints + 'supplement/system_ping': 2.5, + # new order endpoints + 'supplement/create_order_test': 1, + 'supplement/create_order': 1, + 'supplement/cancel_order': 1, + 'supplement/cancel_order_by_symbol': 1, + 'supplement/orders_info': 2.5, + 'supplement/orders_info_no_deal': 2.5, + 'supplement/orders_info_history': 2.5, + 'supplement/user_info_account': 2.5, + 'supplement/transaction_history': 2.5, + }, + }, + }, + 'contract': { + 'public': { + 'get': { + 'cfd/openApi/v1/pub/getTime': 2.5, + 'cfd/openApi/v1/pub/instrument': 2.5, + 'cfd/openApi/v1/pub/marketData': 2.5, + 'cfd/openApi/v1/pub/marketOrder': 2.5, + }, + }, + }, + }, + 'fees': { + 'trading': { + 'maker': self.parse_number('0.001'), + 'taker': self.parse_number('0.001'), + }, + 'funding': { + 'withdraw': {}, + }, + }, + 'commonCurrencies': { + 'XBT': 'XBT', # not BTC! + 'HIT': 'Hiver', + 'VET_ERC20': 'VEN', + 'PNT': 'Penta', + }, + 'precisionMode': TICK_SIZE, + 'options': { + 'cacheSecretAsPem': True, + 'createMarketBuyOrderRequiresPrice': True, + 'fetchTrades': { + 'method': 'spotPublicGetTrades', # or 'spotPublicGetTradesSupplement' + }, + 'fetchTransactionFees': { # DEPRECATED, please use fetchDepositWithdrawFees + 'method': 'fetchPrivateTransactionFees', # or 'fetchPublicTransactionFees' + }, + 'fetchDepositWithdrawFees': { + 'method': 'fetchPrivateDepositWithdrawFees', # or 'fetchPublicDepositWithdrawFees' + }, + 'fetchDepositAddress': { + 'method': 'fetchDepositAddressDefault', # or fetchDepositAddressSupplement + }, + 'createOrder': { + 'method': 'spotPrivatePostSupplementCreateOrder', # or spotPrivatePostCreateOrder + }, + 'fetchOrder': { + 'method': 'fetchOrderSupplement', # or fetchOrderDefault + }, + 'fetchBalance': { + 'method': 'spotPrivatePostSupplementUserInfo', # or spotPrivatePostSupplementUserInfoAccount or spotPrivatePostUserInfo + }, + 'networks': { + 'ERC20': 'erc20', + 'ETH': 'erc20', + 'TRC20': 'trc20', + 'TRX': 'trc20', + 'OMNI': 'omni', + 'ASA': 'asa', + 'BEP20': 'bep20(bsc)', + 'BSC': 'bep20(bsc)', + 'HT': 'heco', + 'BNB': 'bep2', + 'BTC': 'btc', + 'DOGE': 'dogecoin', + 'MATIC': 'matic', + 'POLYGON': 'matic', + 'OEC': 'oec', + 'BTCTRON': 'btctron', + 'XRP': 'xrp', + # other unusual chains with number of listed currencies supported + # 'avax c-chain': 1, + # klay: 12, + # bta: 1, + # fantom: 1, + # celo: 1, + # sol: 2, + # zenith: 1, + # ftm: 5, + # bep20: 1,(single token with mis-named chain) SSS + # bitci: 1, + # sgb: 1, + # moonbeam: 1, + # ekta: 1, + # etl: 1, + # arbitrum: 1, + # tpc: 1, + # ptx: 1 + # } + }, + 'networksById': { + 'erc20': 'ERC20', + 'trc20': 'TRC20', + 'TRX': 'TRC20', + 'bep20(bsc)': 'BEP20', + 'bep20': 'BEP20', + }, + 'defaultNetworks': { + 'USDT': 'TRC20', + }, + }, + 'features': { + 'default': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': False, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': False, + 'GTD': False, + }, + 'hedged': False, + 'selfTradePrevention': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': False, + 'iceberg': False, + }, + 'createOrders': None, # todo + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 100000, # todo + 'untilDays': 2, + 'symbolRequired': True, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 200, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': 200, + 'daysBack': None, + 'untilDays': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchClosedOrders': None, # todo: through fetchOrders "status" -1: Cancelled 0: Unfilled 1: Partially filled 2: Completely filled 3: Partially filled has been cancelled 4: Cancellation is being processed + 'fetchOHLCV': { + 'limit': 2000, + }, + }, + 'spot': { + 'extends': 'default', + }, + 'swap': { + 'linear': { + 'extends': 'default', + }, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + }) + + async def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://www.lbank.com/en-US/docs/index.html#get-timestamp + https://www.lbank.com/en-US/docs/contract.html#get-the-current-time + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + type = None + type, params = self.handle_market_type_and_params('fetchTime', None, params) + response = None + if type == 'swap': + response = await self.contractPublicGetCfdOpenApiV1PubGetTime(params) + else: + response = await self.spotPublicGetTimestamp(params) + # + # spot + # + # { + # "result": "true", + # "data": 1691789627950, + # "error_code": 0, + # "ts": 1691789627950 + # } + # + # swap + # + # { + # "data": 1691789627950, + # "error_code": 0, + # "msg": "Success", + # "result": "true", + # "success": True + # } + # + return self.safe_integer(response, 'data') + + async def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = await self.spotPublicGetWithdrawConfigs(params) + # + # { + # "msg": "Success", + # "result": "true", + # "data": [ + # { + # "amountScale": "4", + # "chain": "bep20(bsc)", + # "assetCode": "usdt", + # "min": "10", + # "transferAmtScale": "4", + # "canWithDraw": True, + # "fee": "0.0000", + # "minTransfer": "0.0001", + # "type": "1" + # }, + # { + # "amountScale": "4", + # "chain": "trc20", + # "assetCode": "usdt", + # "min": "1", + # "transferAmtScale": "4", + # "canWithDraw": True, + # "fee": "1.0000", + # "minTransfer": "0.0001", + # "type": "1" + # }, + # ... + # ], + # "error_code": "0", + # "ts": "1747973911431" + # } + # + currenciesData = self.safe_list(response, 'data', []) + grouped = self.group_by(currenciesData, 'assetCode') + groupedKeys = list(grouped.keys()) + result: dict = {} + for i in range(0, len(groupedKeys)): + id = str((groupedKeys[i])) # some currencies are numeric + code = self.safe_currency_code(id) + networksRaw = grouped[id] + networks = {} + for j in range(0, len(networksRaw)): + networkEntry = networksRaw[j] + networkId = self.safe_string(networkEntry, 'chain') + if networkId is None: + networkId = self.safe_string(networkEntry, 'assetCode') # use type if networkId is not present + networkCode = self.network_id_to_code(networkId) + networks[networkCode] = { + 'id': networkId, + 'network': networkCode, + 'limits': { + 'withdraw': { + 'min': self.safe_number(networkEntry, 'min'), + 'max': None, + }, + 'deposit': { + 'min': self.safe_number(networkEntry, 'minTransfer'), + 'max': None, + }, + }, + 'active': None, + 'deposit': None, + 'withdraw': self.safe_bool(networkEntry, 'canWithDraw'), + 'fee': self.safe_number(networkEntry, 'fee'), + 'precision': self.parse_number(self.parse_precision(self.safe_string(networkEntry, 'transferAmtScale'))), + 'info': networkEntry, + } + result[code] = self.safe_currency_structure({ + 'id': id, + 'code': code, + 'precision': None, + 'type': None, + 'name': None, + 'active': None, + 'deposit': None, + 'withdraw': None, + 'fee': None, + 'limits': { + 'withdraw': { + 'min': None, + 'max': None, + }, + 'deposit': { + 'min': None, + 'max': None, + }, + }, + 'networks': networks, + 'info': networksRaw, + }) + return result + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for lbank + + https://www.lbank.com/en-US/docs/index.html#trading-pairs + https://www.lbank.com/en-US/docs/contract.html#query-contract-information-list + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + marketsPromises = [ + self.fetch_spot_markets(params), + self.fetch_swap_markets(params), + ] + resolvedMarkets = await asyncio.gather(*marketsPromises) + return self.array_concat(resolvedMarkets[0], resolvedMarkets[1]) + + async def fetch_spot_markets(self, params={}): + response = await self.spotPublicGetAccuracy(params) + # + # { + # "result": "true", + # "data": [ + # { + # "symbol": "btc_usdt", + # "quantityAccuracy": "4", + # "minTranQua": "0.0001", + # "priceAccuracy": "2" + # }, + # ], + # "error_code": 0, + # "ts": 1691560288484 + # } + # + data = self.safe_value(response, 'data', []) + result = [] + for i in range(0, len(data)): + market = data[i] + marketId = self.safe_string(market, 'symbol') + parts = marketId.split('_') + baseId = parts[0] + quoteId = parts[1] + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + symbol = base + '/' + quote + result.append({ + 'id': marketId, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'baseId': baseId, + 'quoteId': quoteId, + 'settle': None, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': True, + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(self.parse_precision(self.safe_string(market, 'quantityAccuracy'))), + 'price': self.parse_number(self.parse_precision(self.safe_string(market, 'priceAccuracy'))), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(market, 'minTranQua'), + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + 'info': market, + }) + return result + + async def fetch_swap_markets(self, params={}): + request: dict = { + 'productGroup': 'SwapU', + } + response = await self.contractPublicGetCfdOpenApiV1PubInstrument(self.extend(request, params)) + # + # { + # "data": [ + # { + # "priceLimitUpperValue": 0.2, + # "symbol": "BTCUSDT", + # "volumeTick": 0.0001, + # "indexPrice": "29707.70200000", + # "minOrderVolume": "0.0001", + # "priceTick": 0.1, + # "maxOrderVolume": "30.0", + # "baseCurrency": "BTC", + # "volumeMultiple": 1.0, + # "exchangeID": "Exchange", + # "priceCurrency": "USDT", + # "priceLimitLowerValue": 0.2, + # "clearCurrency": "USDT", + # "symbolName": "BTCUSDT", + # "defaultLeverage": 20.0, + # "minOrderCost": "5.0" + # }, + # ], + # "error_code": 0, + # "msg": "Success", + # "result": "true", + # "success": True + # } + # + data = self.safe_value(response, 'data', []) + result = [] + for i in range(0, len(data)): + market = data[i] + marketId = self.safe_string(market, 'symbol') + baseId = self.safe_string(market, 'baseCurrency') + settleId = self.safe_string(market, 'clearCurrency') + quoteId = settleId + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + settle = self.safe_currency_code(settleId) + symbol = base + '/' + quote + ':' + settle + result.append({ + 'id': marketId, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': 'swap', + 'spot': False, + 'margin': False, + 'swap': True, + 'future': False, + 'option': False, + 'active': True, + 'contract': True, + 'linear': True, + 'inverse': False, + 'contractSize': self.safe_number(market, 'volumeMultiple'), + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.safe_number(market, 'volumeTick'), + 'price': self.safe_number(market, 'priceTick'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(market, 'minOrderVolume'), + 'max': self.safe_number(market, 'maxOrderVolume'), + }, + 'price': { + 'min': self.safe_number(market, 'priceLimitLowerValue'), + 'max': self.safe_number(market, 'priceLimitUpperValue'), + }, + 'cost': { + 'min': self.safe_number(market, 'minOrderCost'), + 'max': None, + }, + }, + 'created': None, + 'info': market, + }) + return result + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # spot: fetchTicker, fetchTickers + # + # { + # "symbol": "btc_usdt", + # "ticker": { + # "high": "29695.57", + # "vol": "6890.2789", + # "low": "29110", + # "change": "0.58", + # "turnover": "202769821.06", + # "latest": "29405.98" + # }, + # "timestamp": :1692064274908 + # } + # + # swap: fetchTickers + # + # { + # "prePositionFeeRate": "0.000053", + # "volume": "2435.459", + # "symbol": "BTCUSDT", + # "highestPrice": "29446.5", + # "lowestPrice": "29362.9", + # "openPrice": "29419.5", + # "markedPrice": "29385.1", + # "turnover": "36345526.2438402", + # "lastPrice": "29387.0" + # } + # + timestamp = self.safe_integer(ticker, 'timestamp') + marketId = self.safe_string(ticker, 'symbol') + symbol = self.safe_symbol(marketId, market) + tickerData = self.safe_value(ticker, 'ticker', {}) + market = self.safe_market(marketId, market) + data = ticker if (market['contract']) else tickerData + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string_2(data, 'high', 'highestPrice'), + 'low': self.safe_string_2(data, 'low', 'lowestPrice'), + 'bid': None, + 'bidVolume': None, + 'ask': None, + 'askVolume': None, + 'vwap': None, + 'open': self.safe_string(data, 'openPrice'), + 'close': None, + 'last': self.safe_string_2(data, 'latest', 'lastPrice'), + 'previousClose': None, + 'change': None, + 'percentage': self.safe_string(data, 'change'), + 'average': None, + 'baseVolume': self.safe_string_2(data, 'vol', 'volume'), + 'quoteVolume': self.safe_string(data, 'turnover'), + 'info': ticker, + }, market) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://www.lbank.com/en-US/docs/index.html#query-current-market-data-new + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + if market['swap']: + responseForSwap = await self.fetch_tickers([market['symbol']], params) + return self.safe_value(responseForSwap, market['symbol']) + request: dict = { + 'symbol': market['id'], + } + response = await self.spotPublicGetTicker24hr(self.extend(request, params)) + # + # { + # "result": "true", + # "data": [ + # { + # "symbol": "btc_usdt", + # "ticker": { + # "high": "29695.57", + # "vol": "6890.2789", + # "low": "29110", + # "change": "0.58", + # "turnover": "202769821.06", + # "latest": "29405.98" + # }, + # "timestamp": :1692064274908 + # } + # ], + # "error_code": 0, + # "ts": :1692064276872 + # } + # + data = self.safe_value(response, 'data', []) + first = self.safe_dict(data, 0, {}) + return self.parse_ticker(first, market) + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://www.lbank.com/en-US/docs/index.html#query-current-market-data-new + https://www.lbank.com/en-US/docs/contract.html#query-contract-market-list + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + market = None + if symbols is not None: + symbols = self.market_symbols(symbols) + symbolsLength = len(symbols) + if symbolsLength > 0: + market = self.market(symbols[0]) + request: dict = {} + type = None + type, params = self.handle_market_type_and_params('fetchTickers', market, params) + response = None + if type == 'swap': + request['productGroup'] = 'SwapU' + response = await self.contractPublicGetCfdOpenApiV1PubMarketData(self.extend(request, params)) + else: + request['symbol'] = 'all' + response = await self.spotPublicGetTicker24hr(self.extend(request, params)) + # + # spot + # + # { + # "result": "true", + # "data": [ + # { + # "symbol": "btc_usdt", + # "ticker": { + # "high": "29695.57", + # "vol": "6890.2789", + # "low": "29110", + # "change": "0.58", + # "turnover": "202769821.06", + # "latest": "29405.98" + # }, + # "timestamp": :1692064274908 + # } + # ], + # "error_code": 0, + # "ts": :1692064276872 + # } + # + # swap + # + # { + # "data": [ + # { + # "prePositionFeeRate": "0.000053", + # "volume": "2435.459", + # "symbol": "BTCUSDT", + # "highestPrice": "29446.5", + # "lowestPrice": "29362.9", + # "openPrice": "29419.5", + # "markedPrice": "29385.1", + # "turnover": "36345526.2438402", + # "lastPrice": "29387.0" + # }, + # ], + # "error_code": 0, + # "msg": "Success", + # "result": "true", + # "success": True + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_tickers(data, symbols) + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://www.lbank.com/en-US/docs/index.html#query-market-depth + https://www.lbank.com/en-US/docs/contract.html#get-handicap + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + if limit is None: + limit = 60 + request: dict = { + 'symbol': market['id'], + } + type = None + type, params = self.handle_market_type_and_params('fetchOrderBook', market, params) + response = None + if type == 'swap': + request['depth'] = limit + response = await self.contractPublicGetCfdOpenApiV1PubMarketOrder(self.extend(request, params)) + else: + request['size'] = limit + response = await self.spotPublicGetDepth(self.extend(request, params)) + # + # spot + # + # { + # "result": "true", + # "data": { + # "asks": [ + # ["29243.37", "2.8783"], + # ["29243.39", "2.2842"], + # ["29243.4", "0.0337"] + # ], + # "bids": [ + # ["29243.36", "1.5258"], + # ["29243.34", "0.8218"], + # ["29243.28", "1.285"] + # ], + # "timestamp": :1692157328820 + # }, + # "error_code": 0, + # "ts": :1692157328820 + # } + # + # swap + # + # { + # "data": { + # "symbol": "BTCUSDT", + # "asks": [ + # { + # "volume": "14.6535", + # "price": "29234.2", + # "orders": "1" + # }, + # ], + # "bids": [ + # { + # "volume": "13.4899", + # "price": "29234.1", + # "orders": "4" + # }, + # ] + # }, + # "error_code": 0, + # "msg": "Success", + # "result": "true", + # "success": True + # } + # + orderbook = self.safe_value(response, 'data', {}) + timestamp = self.milliseconds() + if market['swap']: + return self.parse_order_book(orderbook, market['symbol'], timestamp, 'bids', 'asks', 'price', 'volume') + return self.parse_order_book(orderbook, market['symbol'], timestamp, 'bids', 'asks') + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(old) spotPublicGetTrades + # + # { + # "date_ms":1647021989789, + # "amount":0.0028, + # "price":38804.2, + # "type":"buy", + # "tid":"52d5616ee35c43019edddebe59b3e094" + # } + # + # + # fetchTrades(new) spotPublicGetTradesSupplement + # + # { + # "quoteQty":1675.048485, + # "price":0.127545, + # "qty":13133, + # "id":"3589541dc22e4357b227283650f714e2", + # "time":1648058297110, + # "isBuyerMaker":false + # } + # + # fetchMyTrades(private) + # + # { + # "orderUuid":"38b4e7a4-14f6-45fd-aba1-1a37024124a0", + # "tradeFeeRate":0.0010000000, + # "dealTime":1648500944496, + # "dealQuantity":30.00000000000000000000, + # "tradeFee":0.00453300000000000000, + # "txUuid":"11f3850cc6214ea3b495adad3a032794", + # "dealPrice":0.15111300000000000000, + # "dealVolumePrice":4.53339000000000000000, + # "tradeType":"sell_market" + # } + # + timestamp = self.safe_integer_2(trade, 'date_ms', 'time') + if timestamp is None: + timestamp = self.safe_integer(trade, 'dealTime') + amountString = self.safe_string_2(trade, 'amount', 'qty') + if amountString is None: + amountString = self.safe_string(trade, 'dealQuantity') + priceString = self.safe_string(trade, 'price') + if priceString is None: + priceString = self.safe_string(trade, 'dealPrice') + costString = self.safe_string(trade, 'quoteQty') + if costString is None: + costString = self.safe_string(trade, 'dealVolumePrice') + side = self.safe_string_2(trade, 'tradeType', 'type') + type = None + takerOrMaker = None + if side is not None: + parts = side.split('_') + side = self.safe_string(parts, 0) + typePart = self.safe_string(parts, 1) + type = 'limit' + takerOrMaker = 'taker' + if typePart is not None: + if typePart == 'market': + type = 'market' + elif typePart == 'maker': + takerOrMaker = 'maker' + id = self.safe_string_2(trade, 'tid', 'id') + if id is None: + id = self.safe_string(trade, 'txUuid') + order = self.safe_string(trade, 'orderUuid') + symbol = self.safe_symbol(None, market) + fee = None + feeCost = self.safe_string(trade, 'tradeFee') + if feeCost is not None: + fee = { + 'cost': feeCost, + 'currency': market['base'] if (side == 'buy') else market['quote'], + 'rate': self.safe_string(trade, 'tradeFeeRate'), + } + return self.safe_trade({ + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'id': id, + 'order': order, + 'type': type, + 'takerOrMaker': takerOrMaker, + 'side': side, + 'price': priceString, + 'amount': amountString, + 'cost': costString, + 'fee': fee, + 'info': trade, + }, market) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://www.lbank.com/en-US/docs/index.html#query-historical-transactions + https://www.lbank.com/en-US/docs/index.html#recent-transactions-list + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if since is not None: + request['time'] = since + if limit is not None: + request['size'] = min(limit, 600) + else: + request['size'] = 600 # max + options = self.safe_value(self.options, 'fetchTrades', {}) + defaultMethod = self.safe_string(options, 'method', 'spotPublicGetTrades') + method = self.safe_string(params, 'method', defaultMethod) + params = self.omit(params, 'method') + response = None + if method == 'spotPublicGetSupplementTrades': + response = await self.spotPublicGetSupplementTrades(self.extend(request, params)) + else: + response = await self.spotPublicGetTrades(self.extend(request, params)) + # + # { + # "result":"true", + # "data": [ + # { + # "date_ms":1647021989789, + # "amount":0.0028, + # "price":38804.2, + # "type":"buy", + # "tid":"52d5616ee35c43019edddebe59b3e094" + # } + # ], + # "error_code":0, + # "ts":1647021999308 + # } + # + trades = self.safe_list(response, 'data', []) + return self.parse_trades(trades, market, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # [ + # 1482311500, # timestamp + # 5423.23, # open + # 5472.80, # high + # 5516.09, # low + # 5462, # close + # 234.3250 # volume + # ], + # + return [ + self.safe_timestamp(ohlcv, 0), # timestamp + self.safe_number(ohlcv, 1), # open + self.safe_number(ohlcv, 2), # high + self.safe_number(ohlcv, 3), # low + self.safe_number(ohlcv, 4), # close + self.safe_number(ohlcv, 5), # volume + ] + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://www.lbank.com/en-US/docs/index.html#query-k-bar-data + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + # endpoint doesnt work + await self.load_markets() + market = self.market(symbol) + if limit is None: + limit = 100 + else: + limit = min(limit, 2000) + if since is None: + duration = self.parse_timeframe(timeframe) + since = self.milliseconds() - (duration * 1000 * limit) + request: dict = { + 'symbol': market['id'], + 'type': self.safe_string(self.timeframes, timeframe, timeframe), + 'time': self.parse_to_int(since / 1000), + 'size': min(limit + 1, 2000), # max 2000 + } + response = await self.spotPublicGetKline(self.extend(request, params)) + ohlcvs = self.safe_list(response, 'data', []) + # + # + # [ + # [ + # 1482311500, + # 5423.23, + # 5472.80, + # 5516.09, + # 5462, + # 234.3250 + # ], + # [ + # 1482311400, + # 5432.52, + # 5459.87, + # 5414.30, + # 5428.23, + # 213.7329 + # ] + # ] + # + return self.parse_ohlcvs(ohlcvs, market, timeframe, since, limit) + + def parse_balance(self, response) -> Balances: + # + # spotPrivatePostUserInfo + # + # { + # "toBtc": { + # "egc:": "0", + # "iog": "0", + # "ksm": "0", + # }, + # "freeze": { + # "egc": "0", + # "iog": "0", + # "ksm": "0" , + # }, + # "asset": { + # "egc": "0", + # "iog": "0", + # "ksm": "0", + # }, + # "free": { + # "egc": "0", + # "iog": "0", + # "ksm": "0", + # } + # } + # + # spotPrivatePostSupplementUserInfoAccount + # + # { + # "balances":[ + # { + # "asset":"lbk", + # "free":"0", + # "locked":"0" + # }, ... + # ] + # } + # + # spotPrivatePostSupplementUserInfo + # + # [ + # { + # "usableAmt":"31.45130723", + # "assetAmt":"31.45130723", + # "networkList":[ + # { + # "isDefault":true, + # "withdrawFeeRate":"", + # "name":"bep20(bsc)", + # "withdrawMin":30, + # "minLimit":0.0001, + # "minDeposit":0.0001, + # "feeAssetCode":"doge", + # "withdrawFee":"30", + # "type":1, + # "coin":"doge", + # "network":"bsc" + # }, + # { + # "isDefault":false, + # "withdrawFeeRate":"", + # "name":"dogecoin", + # "withdrawMin":10, + # "minLimit":0.0001, + # "minDeposit":10, + # "feeAssetCode":"doge", + # "withdrawFee":"10", + # "type":1, + # "coin":"doge", + # "network":"dogecoin" + # } + # ], + # "freezeAmt":"0", + # "coin":"doge" + # }, ... + # ] + # + timestamp = self.safe_integer(response, 'ts') + result: dict = { + 'info': response, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + data = self.safe_value(response, 'data') + # from spotPrivatePostUserInfo + toBtc = self.safe_value(data, 'toBtc') + if toBtc is not None: + used = self.safe_value(data, 'freeze', {}) + free = self.safe_value(data, 'free', {}) + currencies = list(free.keys()) + for i in range(0, len(currencies)): + currencyId = currencies[i] + code = self.safe_currency_code(currencyId) + account = self.account() + account['used'] = self.safe_string(used, currencyId) + account['free'] = self.safe_string(free, currencyId) + result[code] = account + return self.safe_balance(result) + # from spotPrivatePostSupplementUserInfoAccount + balances = self.safe_value(data, 'balances') + if balances is not None: + for i in range(0, len(balances)): + item = balances[i] + currencyId = self.safe_string(item, 'asset') + codeInner = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(item, 'free') + account['used'] = self.safe_string(item, 'locked') + result[codeInner] = account + return self.safe_balance(result) + # from spotPrivatePostSupplementUserInfo + isArray = isinstance(data, list) + if isArray is True: + for i in range(0, len(data)): + item = data[i] + currencyId = self.safe_string(item, 'coin') + codeInner = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(item, 'usableAmt') + account['used'] = self.safe_string(item, 'freezeAmt') + result[codeInner] = account + return self.safe_balance(result) + return None + + def parse_funding_rate(self, ticker, market: Market = None) -> FundingRate: + # { + # "symbol": "BTCUSDT", + # "highestPrice": "69495.5", + # "underlyingPrice": "68455.904", + # "lowestPrice": "68182.1", + # "openPrice": "68762.4", + # "positionFeeRate": "0.0001", + # "volume": "33534.2858", + # "markedPrice": "68434.1", + # "turnover": "1200636218.210558", + # "positionFeeTime": "28800", + # "lastPrice": "68427.3", + # "nextFeeTime": "1730736000000", + # "fundingRate": "0.0001", + # } + marketId = self.safe_string(ticker, 'symbol') + symbol = self.safe_symbol(marketId, market) + markPrice = self.safe_number(ticker, 'markedPrice') + indexPrice = self.safe_number(ticker, 'underlyingPrice') + fundingRate = self.safe_number(ticker, 'fundingRate') + fundingTime = self.safe_integer(ticker, 'nextFeeTime') + positionFeeTime = self.safe_integer(ticker, 'positionFeeTime') + intervalString = None + if positionFeeTime is not None: + interval = self.parse_to_int(positionFeeTime / 60 / 60) + intervalString = str(interval) + 'h' + return { + 'info': ticker, + 'symbol': symbol, + 'markPrice': markPrice, + 'indexPrice': indexPrice, + 'fundingRate': fundingRate, + 'fundingTimestamp': fundingTime, + 'fundingDatetime': self.iso8601(fundingTime), + 'timestamp': None, + 'datetime': None, + 'nextFundingRate': None, + 'nextFundingTimestamp': None, + 'nextFundingDatetime': None, + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': intervalString, + } + + async def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate + + https://www.lbank.com/en-US/docs/contract.html#query-contract-market-list + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + await self.load_markets() + market = self.market(symbol) + responseForSwap = await self.fetch_funding_rates([market['symbol']], params) + return self.safe_value(responseForSwap, market['symbol']) + + async def fetch_funding_rates(self, symbols: Strings = None, params={}) -> FundingRates: + """ + fetch the funding rate for multiple markets + + https://www.lbank.com/en-US/docs/contract.html#query-contract-market-list + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `funding rate structures `, indexed by market symbols + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + request: dict = { + 'productGroup': 'SwapU', + } + response = await self.contractPublicGetCfdOpenApiV1PubMarketData(self.extend(request, params)) + # { + # "data": [ + # { + # "symbol": "BTCUSDT", + # "highestPrice": "69495.5", + # "underlyingPrice": "68455.904", + # "lowestPrice": "68182.1", + # "openPrice": "68762.4", + # "positionFeeRate": "0.0001", + # "volume": "33534.2858", + # "markedPrice": "68434.1", + # "turnover": "1200636218.210558", + # "positionFeeTime": "28800", + # "lastPrice": "68427.3", + # "nextFeeTime": "1730736000000", + # "fundingRate": "0.0001", + # } + # ], + # "error_code": "0", + # "msg": "Success", + # "result": "true", + # "success": True, + # } + data = self.safe_list(response, 'data', []) + return self.parse_funding_rates(data, symbols) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://www.lbank.com/en-US/docs/index.html#asset-information + https://www.lbank.com/en-US/docs/index.html#account-information + https://www.lbank.com/en-US/docs/index.html#get-all-coins-information + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + options = self.safe_value(self.options, 'fetchBalance', {}) + defaultMethod = self.safe_string(options, 'method', 'spotPrivatePostSupplementUserInfo') + method = self.safe_string(params, 'method', defaultMethod) + response = None + if method == 'spotPrivatePostSupplementUserInfoAccount': + response = await self.spotPrivatePostSupplementUserInfoAccount() + elif method == 'spotPrivatePostUserInfo': + response = await self.spotPrivatePostUserInfo() + else: + response = await self.spotPrivatePostSupplementUserInfo() + # + # { + # "result": "true", + # "data": [ + # { + # "usableAmt": "14.36", + # "assetAmt": "14.36", + # "networkList": [ + # { + # "isDefault": False, + # "withdrawFeeRate": "", + # "name": "erc20", + # "withdrawMin": 30, + # "minLimit": 0.0001, + # "minDeposit": 20, + # "feeAssetCode": "usdt", + # "withdrawFee": "30", + # "type": 1, + # "coin": "usdt", + # "network": "eth" + # }, + # ... + # ], + # "freezeAmt": "0", + # "coin": "ada" + # } + # ], + # "code": 0 + # } + # + return self.parse_balance(response) + + def parse_trading_fee(self, fee: dict, market: Market = None) -> TradingFeeInterface: + # + # { + # "symbol":"skt_usdt", + # "makerCommission":"0.10", + # "takerCommission":"0.10" + # } + # + marketId = self.safe_string(fee, 'symbol') + symbol = self.safe_symbol(marketId) + return { + 'info': fee, + 'symbol': symbol, + 'maker': self.safe_number(fee, 'makerCommission'), + 'taker': self.safe_number(fee, 'takerCommission'), + 'percentage': None, + 'tierBased': None, + } + + async def fetch_trading_fee(self, symbol: str, params={}) -> TradingFeeInterface: + """ + fetch the trading fees for a market + + https://www.lbank.com/en-US/docs/index.html#transaction-fee-rate-query + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `fee structure ` + """ + market = self.market(symbol) + result = await self.fetch_trading_fees(self.extend(params, {'category': market['id']})) + return self.safe_dict(result, symbol) + + async def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://www.lbank.com/en-US/docs/index.html#transaction-fee-rate-query + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + await self.load_markets() + request: dict = {} + response = await self.spotPrivatePostSupplementCustomerTradeFee(self.extend(request, params)) + fees = self.safe_value(response, 'data', []) + result: dict = {} + for i in range(0, len(fees)): + fee = self.parse_trading_fee(fees[i]) + symbol = fee['symbol'] + result[symbol] = fee + return result + + async def create_market_buy_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market buy order by providing the symbol and cost + + https://www.lbank.com/en-US/docs/index.html#place-order + https://www.lbank.com/en-US/docs/index.html#place-an-order + + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' createMarketBuyOrderWithCost() supports spot orders only') + params['createMarketBuyOrderRequiresPrice'] = False + return await self.create_order(symbol, 'market', 'buy', cost, None, params) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://www.lbank.com/en-US/docs/index.html#place-order + https://www.lbank.com/en-US/docs/index.html#place-an-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + clientOrderId = self.safe_string_2(params, 'custom_id', 'clientOrderId') + postOnly = self.safe_bool(params, 'postOnly', False) + timeInForce = self.safe_string_upper(params, 'timeInForce') + params = self.omit(params, ['custom_id', 'clientOrderId', 'timeInForce', 'postOnly']) + request: dict = { + 'symbol': market['id'], + } + ioc = (timeInForce == 'IOC') + fok = (timeInForce == 'FOK') + maker = (postOnly or (timeInForce == 'PO')) + if (type == 'market') and (ioc or fok or maker): + raise InvalidOrder(self.id + ' createOrder() does not allow market FOK, IOC, or postOnly orders. Only limit IOC, FOK, and postOnly orders are allowed') + if type == 'limit': + request['type'] = side + request['price'] = self.price_to_precision(symbol, price) + request['amount'] = self.amount_to_precision(symbol, amount) + if ioc: + request['type'] = side + '_' + 'ioc' + elif fok: + request['type'] = side + '_' + 'fok' + elif maker: + request['type'] = side + '_' + 'maker' + elif type == 'market': + if side == 'sell': + request['type'] = side + '_' + 'market' + request['amount'] = self.amount_to_precision(symbol, amount) + elif side == 'buy': + request['type'] = side + '_' + 'market' + quoteAmount = None + createMarketBuyOrderRequiresPrice = True + createMarketBuyOrderRequiresPrice, params = self.handle_option_and_params(params, 'createOrder', 'createMarketBuyOrderRequiresPrice', True) + cost = self.safe_number(params, 'cost') + params = self.omit(params, 'cost') + if cost is not None: + quoteAmount = self.cost_to_precision(symbol, cost) + elif createMarketBuyOrderRequiresPrice: + if price is None: + raise InvalidOrder(self.id + ' createOrder() requires the price argument for market buy orders to calculate the total cost to spend(amount * price), alternatively set the createMarketBuyOrderRequiresPrice option or param to False and pass the cost to spend in the amount argument') + else: + amountString = self.number_to_string(amount) + priceString = self.number_to_string(price) + costRequest = Precise.string_mul(amountString, priceString) + quoteAmount = self.cost_to_precision(symbol, costRequest) + else: + quoteAmount = self.cost_to_precision(symbol, amount) + # market buys require filling the price param instead of the amount param, for market buys the price is treated cost by lbank + request['price'] = quoteAmount + if clientOrderId is not None: + request['custom_id'] = clientOrderId + options = self.safe_value(self.options, 'createOrder', {}) + defaultMethod = self.safe_string(options, 'method', 'spotPrivatePostSupplementCreateOrder') + method = self.safe_string(params, 'method', defaultMethod) + params = self.omit(params, 'method') + response = None + if method == 'spotPrivatePostCreateOrder': + response = await self.spotPrivatePostCreateOrder(self.extend(request, params)) + else: + response = await self.spotPrivatePostSupplementCreateOrder(self.extend(request, params)) + # + # { + # "result":true, + # "data":{ + # "symbol":"doge_usdt", + # "order_id":"0cf8a3de-4597-4296-af45-be7abaa06b07" + # }, + # "error_code":0, + # "ts":1648162321043 + # } + # + result = self.safe_value(response, 'data', {}) + return self.safe_order({ + 'id': self.safe_string(result, 'order_id'), + 'info': result, + }, market) + + def parse_order_status(self, status: Str): + statuses: dict = { + '-1': 'canceled', # canceled + '0': 'open', # not traded + '1': 'open', # partial deal + '2': 'closed', # complete deal + '3': 'canceled', # filled partially and cancelled + '4': 'closed', # disposal processing + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # fetchOrderSupplement(private) + # + # { + # "cummulativeQuoteQty":0, + # "symbol":"doge_usdt", + # "executedQty":0, + # "orderId":"53d2d53e-70fb-4398-b722-f48571a5f61e", + # "origQty":1E+2, + # "price":0.05, + # "clientOrderId":null, + # "origQuoteOrderQty":5, + # "updateTime":1648163406000, + # "time":1648163139387, + # "type":"buy_maker", + # "status":-1 + # } + # + # + # fetchOrderDefault(private) + # + # { + # "symbol":"shib_usdt", + # "amount":1, + # "create_time":1649367863356, + # "price":0.0000246103, + # "avg_price":0.00002466180000000104, + # "type":"buy_market", + # "order_id":"abe8b92d-86d9-4d6d-b71e-d14f5fb53ddf", + # "custom_id": "007", # field only present if user creates it at order time + # "deal_amount":40548.54065802, + # "status":2 + # } + # + # fetchOpenOrders(private) + # + # { + # "cummulativeQuoteQty":0, + # "symbol":"doge_usdt", + # "executedQty":0, + # "orderId":"73878edf-008d-4e4c-8041-df1f1b2cd8bb", + # "origQty":100, + # "price":0.05, + # "origQuoteOrderQty":5, + # "updateTime":1648501762000, + # "time":1648501762353, + # "type":"buy", + # "status":0 + # } + # + # fetchOrders(private) + # + # { + # "cummulativeQuoteQty":0, + # "symbol":"doge_usdt", + # "executedQty":0, + # "orderId":"2cadc7cc-b5f6-486b-a5b4-d6ac49a9c186", + # "origQty":100, + # "price":0.05, + # "origQuoteOrderQty":5, + # "updateTime":1648501384000, + # "time":1648501363889, + # "type":"buy", + # "status":-1 + # } + # + # cancelOrder + # + # { + # "executedQty":0.0, + # "price":0.05, + # "origQty":100.0, + # "tradeType":"buy", + # "status":0 + # } + # + # cancelAllOrders + # + # { + # "executedQty":0.00000000000000000000, + # "orderId":"293ef71b-3e67-4962-af93-aa06990a045f", + # "price":0.05000000000000000000, + # "origQty":100.00000000000000000000, + # "tradeType":"buy", + # "status":0 + # } + # + id = self.safe_string_2(order, 'orderId', 'order_id') + clientOrderId = self.safe_string_2(order, 'clientOrderId', 'custom_id') + timestamp = self.safe_integer_2(order, 'time', 'create_time') + rawStatus = self.safe_string(order, 'status') + marketId = self.safe_string(order, 'symbol') + market = self.safe_market(marketId, market) + timeInForce = None + postOnly = False + type = 'limit' + rawType = self.safe_string_2(order, 'type', 'tradeType') # buy, sell, buy_market, sell_market, buy_maker,sell_maker,buy_ioc,sell_ioc, buy_fok, sell_fok + parts = rawType.split('_') + side = self.safe_string(parts, 0) + typePart = self.safe_string(parts, 1) # market, maker, ioc, fok or None(limit) + if typePart == 'market': + type = 'market' + if typePart == 'maker': + postOnly = True + timeInForce = 'PO' + if typePart == 'ioc': + timeInForce = 'IOC' + if typePart == 'fok': + timeInForce = 'FOK' + price = self.safe_string(order, 'price') + costString = self.safe_string(order, 'cummulativeQuoteQty') + amountString = None + if rawType != 'buy_market': + amountString = self.safe_string_2(order, 'origQty', 'amount') + filledString = self.safe_string_2(order, 'executedQty', 'deal_amount') + return self.safe_order({ + 'id': id, + 'clientOrderId': clientOrderId, + 'datetime': self.iso8601(timestamp), + 'timestamp': timestamp, + 'lastTradeTimestamp': None, + 'status': self.parse_order_status(rawStatus), + 'symbol': market['symbol'], + 'type': type, + 'timeInForce': timeInForce, + 'postOnly': postOnly, + 'side': side, + 'price': price, + 'triggerPrice': None, + 'cost': costString, + 'amount': amountString, + 'filled': filledString, + 'remaining': None, + 'trades': None, + 'fee': None, + 'info': order, + 'average': None, + }, market) + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://www.lbank.com/en-US/docs/index.html#query-order + https://www.lbank.com/en-US/docs/index.html#query-order-new + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + method = self.safe_string(params, 'method') + if method is None: + options = self.safe_value(self.options, 'fetchOrder', {}) + method = self.safe_string(options, 'method', 'fetchOrderSupplement') + if method == 'fetchOrderSupplement': + return await self.fetch_order_supplement(id, symbol, params) + return await self.fetch_order_default(id, symbol, params) + + async def fetch_order_supplement(self, id: str, symbol: Str = None, params={}): + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrder() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'orderId': id, + } + response = await self.spotPrivatePostSupplementOrdersInfo(self.extend(request, params)) + # + # { + # "result":true, + # "data":{ + # "cummulativeQuoteQty":0, + # "symbol":"doge_usdt", + # "executedQty":0, + # "orderId":"53d2d53e-70fb-4398-b722-f48571a5f61e", + # "origQty":1E+2, + # "price":0.05, + # "clientOrderId":null, + # "origQuoteOrderQty":5, + # "updateTime":1648163406000, + # "time":1648163139387, + # "type":"buy_maker", + # "status":-1 + # }, + # "error_code":0, + # "ts":1648164471827 + # } + # + result = self.safe_dict(response, 'data', {}) + return self.parse_order(result) + + async def fetch_order_default(self, id: str, symbol: Str = None, params={}): + # Id can be a list of ids delimited by a comma + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrder() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'order_id': id, + } + response = await self.spotPrivatePostOrdersInfo(self.extend(request, params)) + # + # { + # "result":true, + # "data":[ + # { + # "symbol":"doge_usdt", + # "amount":18, + # "create_time":1647455223186, + # "price":0, + # "avg_price":0.113344, + # "type":"sell_market", + # "order_id":"d4ca1ddd-40d9-42c1-9717-5de435865bec", + # "deal_amount":18, + # "status":2 + # } + # ], + # "error_code":0, + # "ts":1647455270776 + # } + # + result = self.safe_value(response, 'data', []) + numOrders = len(result) + if numOrders == 1: + return self.parse_order(result[0]) + else: + # parsedOrders = [] + # for i in range(0, numOrders): + # parsedOrder = self.parse_order(result[i]) + # parsedOrders.append(parsedOrder) + # } + # return parsedOrders + raise BadRequest(self.id + ' fetchOrder() can only fetch one order at a time') + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://www.lbank.com/en-US/docs/index.html#past-transaction-details + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trade structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMyTrades() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + since = self.safe_value(params, 'start_date', since) + params = self.omit(params, 'start_date') + request: dict = { + 'symbol': market['id'], + # 'start_date' Start time yyyy-mm-dd, the maximum is today, the default is yesterday + # 'end_date' Finish time yyyy-mm-dd, the maximum is today, the default is today + # 'The start': and end date of the query window is up to 2 days + # 'from' Initial transaction number inquiring + # 'direct' inquire direction,The default is the 'next' which is the positive sequence of dealing time,the 'prev' is inverted order of dealing time + # 'size' Query the number of defaults to 100 + } + if limit is not None: + request['size'] = limit + if since is not None: + request['start_date'] = self.ymd(since, '-') # max query 2 days ago + request['end_date'] = self.ymd(since + 86400000, '-') # will cover 2 days + response = await self.spotPrivatePostTransactionHistory(self.extend(request, params)) + # + # { + # "result":true, + # "data":[ + # { + # "orderUuid":"38b4e7a4-14f6-45fd-aba1-1a37024124a0", + # "tradeFeeRate":0.0010000000, + # "dealTime":1648500944496, + # "dealQuantity":30.00000000000000000000, + # "tradeFee":0.00453300000000000000, + # "txUuid":"11f3850cc6214ea3b495adad3a032794", + # "dealPrice":0.15111300000000000000, + # "dealVolumePrice":4.53339000000000000000, + # "tradeType":"sell_market" + # } + # ], + # "error_code":0, + # "ts":1648509742164 + # } + # + trades = self.safe_list(response, 'data', []) + return self.parse_trades(trades, market, since, limit) + + async def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://www.lbank.com/en-US/docs/index.html#query-all-orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + # default query is for canceled and completely filled orders + # does not return open orders unless specified explicitly + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrders() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + if limit is None: + limit = 100 + request: dict = { + 'symbol': market['id'], + 'current_page': 1, + 'page_length': limit, + # 'status' -1: Cancelled, 0: Unfilled, 1: Partially filled, 2: Completely filled, 3: Partially filled and cancelled, 4: Cancellation is being processed + } + response = await self.spotPrivatePostSupplementOrdersInfoHistory(self.extend(request, params)) + # + # { + # "result":true, + # "data":{ + # "total":1, + # "page_length":100, + # "orders":[ + # { + # "cummulativeQuoteQty":0, + # "symbol":"doge_usdt", + # "executedQty":0, + # "orderId":"2cadc7cc-b5f6-486b-a5b4-d6ac49a9c186", + # "origQty":100, + # "price":0.05, + # "origQuoteOrderQty":5, + # "updateTime":1648501384000, + # "time":1648501363889, + # "type":"buy", + # "status":-1 + # }, ... + # ], + # "current_page":1 + # }, + # "error_code":0, + # "ts":1648505706348 + # } + # + result = self.safe_value(response, 'data', {}) + orders = self.safe_list(result, 'orders', []) + return self.parse_orders(orders, market, since, limit) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://www.lbank.com/en-US/docs/index.html#current-pending-order + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOpenOrders() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + if limit is None: + limit = 100 + request: dict = { + 'symbol': market['id'], + 'current_page': 1, + 'page_length': limit, + } + response = await self.spotPrivatePostSupplementOrdersInfoNoDeal(self.extend(request, params)) + # + # { + # "result":true, + # "data":{ + # "total":1, + # "page_length":100, + # "orders":[ + # { + # "cummulativeQuoteQty":0, + # "symbol":"doge_usdt", + # "executedQty":0, + # "orderId":"73878edf-008d-4e4c-8041-df1f1b2cd8bb", + # "origQty":100, + # "price":0.05, + # "origQuoteOrderQty":5, + # "updateTime":1648501762000, + # "time":1648501762353, + # "type":"buy", + # "status":0 + # }, ... + # ], + # "current_page":1 + # }, + # "error_code":0, + # "ts":1648506110196 + # } + # + result = self.safe_value(response, 'data', {}) + orders = self.safe_list(result, 'orders', []) + return self.parse_orders(orders, market, since, limit) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://www.lbank.com/en-US/docs/index.html#cancel-order-new + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + await self.load_markets() + clientOrderId = self.safe_string_2(params, 'origClientOrderId', 'clientOrderId') + params = self.omit(params, ['origClientOrderId', 'clientOrderId']) + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'orderId': id, + } + if clientOrderId is not None: + request['origClientOrderId'] = clientOrderId + response = await self.spotPrivatePostSupplementCancelOrder(self.extend(request, params)) + # + # { + # "result":true, + # "data":{ + # "executedQty":0.0, + # "price":0.05, + # "origQty":100.0, + # "tradeType":"buy", + # "status":0 + # }, + # "error_code":0, + # "ts":1648501286196 + # } + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data) + + async def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders in a market + + https://www.lbank.com/en-US/docs/index.html#cancel-all-pending-orders-for-a-single-trading-pair + + :param str symbol: unified market symbol of the market to cancel orders in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelAllOrders() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = await self.spotPrivatePostSupplementCancelOrderBySymbol(self.extend(request, params)) + # + # { + # "result":"true", + # "data":[ + # { + # "executedQty":0.00000000000000000000, + # "orderId":"293ef71b-3e67-4962-af93-aa06990a045f", + # "price":0.05000000000000000000, + # "origQty":100.00000000000000000000, + # "tradeType":"buy", + # "status":0 + # }, + # ], + # "error_code":0, + # "ts":1648506641469 + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_orders(data) + + def get_network_code_for_currency(self, currencyCode, params): + defaultNetworks = self.safe_value(self.options, 'defaultNetworks') + defaultNetwork = self.safe_string_upper(defaultNetworks, currencyCode) + networks = self.safe_value(self.options, 'networks', {}) + network = self.safe_string_upper(params, 'network', defaultNetwork) # self line allows the user to specify either ERC20 or ETH + network = self.safe_string(networks, network, network) # handle ERC20>ETH alias + return network + + async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://www.lbank.com/en-US/docs/index.html#get-deposit-address + https://www.lbank.com/en-US/docs/index.html#the-user-obtains-the-deposit-address + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + await self.load_markets() + options = self.safe_value(self.options, 'fetchDepositAddress', {}) + defaultMethod = self.safe_string(options, 'method', 'fetchDepositAddressDefault') + method = self.safe_string(params, 'method', defaultMethod) + params = self.omit(params, 'method') + response = None + if method == 'fetchDepositAddressSupplement': + response = await self.fetch_deposit_address_supplement(code, params) + else: + response = await self.fetch_deposit_address_default(code, params) + return response + + async def fetch_deposit_address_default(self, code: str, params={}) -> DepositAddress: + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'assetCode': currency['id'], + } + network = self.get_network_code_for_currency(code, params) + if network is not None: + request['netWork'] = network # ... yes, really lol + params = self.omit(params, 'network') + response = await self.spotPrivatePostGetDepositAddress(self.extend(request, params)) + # + # { + # "result":true, + # "data":{ + # "assetCode":"usdt", + # "address":"0xc85689d37ca650bf2f2161364cdedee21eb6ca53", + # "memo":null, + # "netWork":"bep20(bsc)" + # }, + # "error_code":0, + # "ts":1648075865103 + # } + # + result = self.safe_value(response, 'data') + address = self.safe_string(result, 'address') + tag = self.safe_string(result, 'memo') + return { + 'info': response, + 'currency': code, + 'network': self.network_id_to_code(self.safe_string(result, 'netWork')), + 'address': address, + 'tag': tag, + } + + async def fetch_deposit_address_supplement(self, code: str, params={}) -> DepositAddress: + # returns the address for whatever the default network is... + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'coin': currency['id'], + } + networks = self.safe_value(self.options, 'networks') + network = self.safe_string_upper(params, 'network') + network = self.safe_string(networks, network, network) + if network is not None: + request['networkName'] = network + params = self.omit(params, 'network') + response = await self.spotPrivatePostSupplementGetDepositAddress(self.extend(request, params)) + # + # { + # "result":true, + # "data":{ + # "address":"TDxtabCC8iQwaxUUrPcE4WL2jArGAfvQ5A", + # "memo":null, + # "coin":"usdt" + # }, + # "error_code":0, + # "ts":1648073818880 + # } + # + result = self.safe_value(response, 'data') + address = self.safe_string(result, 'address') + tag = self.safe_string(result, 'memo') + return { + 'info': response, + 'currency': code, + 'network': None, + 'address': address, + 'tag': tag, + } + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://www.lbank.com/en-US/docs/index.html#withdrawal + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.check_address(address) + await self.load_markets() + fee = self.safe_string(params, 'fee') + params = self.omit(params, 'fee') + # The relevant coin network fee can be found by calling fetchDepositWithdrawFees(), note: if no network param is supplied then the default network will be used, self can also be found in fetchDepositWithdrawFees(). + self.check_required_argument('withdraw', fee, 'fee') + currency = self.currency(code) + request: dict = { + 'address': address, + 'coin': currency['id'], + 'amount': amount, + 'fee': fee, # the correct coin-network fee must be supplied, which can be found by calling fetchDepositWithdrawFees(private) + # 'networkName': defaults to the defaultNetwork of the coin which can be found in the /supplement/user_info endpoint + # 'memo': memo: memo word of bts and dct + # 'mark': Withdrawal Notes + # 'name': Remarks of the address. After hasattr(self, filling) parameter, it will be added to the withdrawal address book of the currency. + # 'withdrawOrderId': withdrawOrderId + # 'type': type=1 is for intra-site transfer + } + if tag is not None: + request['memo'] = tag + network = self.safe_string_upper_2(params, 'network', 'networkName') + params = self.omit(params, ['network', 'networkName']) + networks = self.safe_value(self.options, 'networks') + networkId = self.safe_string(networks, network, network) + if networkId is not None: + request['networkName'] = networkId + response = await self.spotPrivatePostSupplementWithdraw(self.extend(request, params)) + # + # { + # "result":true, + # "data": { + # "fee":10.00000000000000000000, + # "withdrawId":1900376 + # }, + # "error_code":0, + # "ts":1648992501414 + # } + # + result = self.safe_value(response, 'data', {}) + return { + 'info': result, + 'id': self.safe_string(result, 'withdrawId'), + } + + def parse_transaction_status(self, status, type): + statuses: dict = { + 'deposit': { + '1': 'pending', + '2': 'ok', + '3': 'failed', + '4': 'canceled', + '5': 'transfer', + }, + 'withdrawal': { + '1': 'pending', + '2': 'canceled', + '3': 'failed', + '4': 'ok', + }, + } + return self.safe_string(self.safe_value(statuses, type, {}), status, status) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fetchDeposits(private) + # + # { + # "insertTime":1649012310000, + # "amount":9.00000000000000000000, + # "address":"TYASr5UV6HEcXatwdFQfmLVUqQQQMUxHLS", + # "networkName":"trc20", + # "txId":"081e4e9351dd0274922168da5f2d14ea6c495b1c3b440244f4a6dd9fe196bf2b", + # "coin":"usdt", + # "status":"2" + # } + # + # + # fetchWithdrawals(private) + # + # { + # "amount":2.00000000000000000000, + # "address":"TBjrW5JHDyPZjFc5nrRMhRWUDaJmhGhmD6", + # "fee":1.00000000000000000000, + # "networkName":"trc20", + # "coid":"usdt", + # "transferType":"数字资产提现", + # "txId":"47eeee2763ad49b8817524dacfa7d092fb58f8b0ab7e5d25473314df1a793c3d", + # "id":1902194, + # "applyTime":1649014002000, + # "status":"4" + # } + # + id = self.safe_string(transaction, 'id') + type = None + if id is None: + type = 'deposit' + else: + type = 'withdrawal' + txid = self.safe_string(transaction, 'txId') + timestamp = self.safe_integer_2(transaction, 'insertTime', 'applyTime') + address = self.safe_string(transaction, 'address') + addressFrom = None + addressTo = None + if type == 'deposit': + addressFrom = address + else: + addressTo = address + amount = self.safe_number(transaction, 'amount') + currencyId = self.safe_string_2(transaction, 'coin', 'coid') + code = self.safe_currency_code(currencyId, currency) + status = self.parse_transaction_status(self.safe_string(transaction, 'status'), type) + fee = None + feeCost = self.safe_number(transaction, 'fee') + if feeCost is not None: + fee = { + 'cost': feeCost, + 'currency': code, + } + return { + 'info': transaction, + 'id': id, + 'txid': txid, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': self.network_id_to_code(self.safe_string(transaction, 'networkName')), + 'address': address, + 'addressTo': addressTo, + 'addressFrom': addressFrom, + 'tag': None, + 'tagTo': None, + 'tagFrom': None, + 'type': type, + 'amount': amount, + 'currency': code, + 'status': status, + 'updated': None, + 'comment': None, + 'internal': (status == 'transfer'), + 'fee': fee, + } + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://www.lbank.com/en-US/docs/index.html#get-recharge-history + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + request: dict = { + # 'status': Recharge status: ("1","Applying"),("2","Recharge successful"),("3","Recharge failed"),("4","Already Cancel"),("5", "Transfer") + # 'endTime': end time, timestamp in milliseconds, default now + } + currency = None + if code is not None: + currency = self.currency(code) + request['coin'] = currency['id'] + if since is not None: + request['startTime'] = since + response = await self.spotPrivatePostSupplementDepositHistory(self.extend(request, params)) + # + # { + # "result":true, + # "data": { + # "total":1, + # "depositOrders": [ + # { + # "insertTime":1649012310000, + # "amount":9.00000000000000000000, + # "address":"TYASr5UV6HEcXatwdFQfmLVUqQQQMUxHLS", + # "networkName":"trc20", + # "txId":"081e4e9351dd0274922168da5f2d14ea6c495b1c3b440244f4a6dd9fe196bf2b", + # "coin":"usdt", + # "status":"2" + # }, + # ], + # "page_length":20, + # "current_page":1 + # }, + # "error_code":0, + # "ts":1649719721758 + # } + # + data = self.safe_value(response, 'data', {}) + deposits = self.safe_list(data, 'depositOrders', []) + return self.parse_transactions(deposits, currency, since, limit) + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://www.lbank.com/en-US/docs/index.html#get-withdrawal-history + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + request: dict = { + # 'status': Recharge status: ("1","Applying"),("2","Recharge successful"),("3","Recharge failed"),("4","Already Cancel"),("5", "Transfer") + # 'endTime': end time, timestamp in milliseconds, default now + # 'withdrawOrderId': Custom withdrawal id + } + currency = None + if code is not None: + currency = self.currency(code) + request['coin'] = currency['id'] + if since is not None: + request['startTime'] = since + response = await self.spotPrivatePostSupplementWithdraws(self.extend(request, params)) + # + # { + # "result":true, + # "data": { + # "total":1, + # "withdraws": [ + # { + # "amount":2.00000000000000000000, + # "address":"TBjrW5JHDyPZjFc5nrRMhRWUDaJmhGhmD6", + # "fee":1.00000000000000000000, + # "networkName":"trc20", + # "coid":"usdt", + # "transferType":"数字资产提现", + # "txId":"47eeee2763ad49b8817524dacfa7d092fb58f8b0ab7e5d25473314df1a793c3d", + # "id":1902194, + # "applyTime":1649014002000, + # "status":"4" + # }, + # ], + # "page_length":20, + # "current_page":1 + # }, + # "error_code":0, + # "ts":1649720362362 + # } + # + data = self.safe_value(response, 'data', {}) + withdraws = self.safe_list(data, 'withdraws', []) + return self.parse_transactions(withdraws, currency, since, limit) + + async def fetch_transaction_fees(self, codes: Strings = None, params={}): + """ + @deprecated + please use fetchDepositWithdrawFees instead + :param str[]|None codes: not used by lbank fetchTransactionFees() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `fee structures ` + """ + # private only returns information for currencies with non-zero balance + await self.load_markets() + isAuthorized = self.check_required_credentials(False) + result = None + if isAuthorized is True: + options = self.safe_value(self.options, 'fetchTransactionFees', {}) + defaultMethod = self.safe_string(options, 'method', 'fetchPrivateTransactionFees') + method = self.safe_string(params, 'method', defaultMethod) + params = self.omit(params, 'method') + if method == 'fetchPublicTransactionFees': + result = await self.fetch_public_transaction_fees(params) + else: + result = await self.fetch_private_transaction_fees(params) + else: + result = await self.fetch_public_transaction_fees(params) + return result + + async def fetch_private_transaction_fees(self, params={}): + # complete response + # incl. for coins which None in public method + await self.load_markets() + response = await self.spotPrivatePostSupplementUserInfo() + # + # { + # "result": "true", + # "data": [ + # { + # "usableAmt": "14.36", + # "assetAmt": "14.36", + # "networkList": [ + # { + # "isDefault": False, + # "withdrawFeeRate": "", + # "name": "erc20", + # "withdrawMin": 30, + # "minLimit": 0.0001, + # "minDeposit": 20, + # "feeAssetCode": "usdt", + # "withdrawFee": "30", + # "type": 1, + # "coin": "usdt", + # "network": "eth" + # }, + # ... + # ], + # "freezeAmt": "0", + # "coin": "ada" + # } + # ], + # "code": 0 + # } + # + result = self.safe_value(response, 'data', []) + withdrawFees: dict = {} + for i in range(0, len(result)): + entry = result[i] + currencyId = self.safe_string(entry, 'coin') + code = self.safe_currency_code(currencyId) + networkList = self.safe_value(entry, 'networkList', []) + withdrawFees[code] = {} + for j in range(0, len(networkList)): + networkEntry = networkList[j] + fee = self.safe_number(networkEntry, 'withdrawFee') + if fee is not None: + networkCode = self.network_id_to_code(self.safe_string(networkEntry, 'name')) + withdrawFees[code][networkCode] = fee + return { + 'withdraw': withdrawFees, + 'deposit': {}, + 'info': response, + } + + async def fetch_public_transaction_fees(self, params={}): + # extremely incomplete response + # vast majority fees None + await self.load_markets() + code = self.safe_string_2(params, 'coin', 'assetCode') + params = self.omit(params, ['coin', 'assetCode']) + request: dict = {} + if code is not None: + currency = self.currency(code) + request['assetCode'] = currency['id'] + response = await self.spotPublicGetWithdrawConfigs(self.extend(request, params)) + # + # { + # "result": "true", + # "data": [ + # { + # "amountScale": "4", + # "chain": "heco", + # "assetCode": "lbk", + # "min": "200", + # "transferAmtScale": "4", + # "canWithDraw": True, + # "fee": "100", + # "minTransfer": "0.0001", + # "type": "1" + # }, + # ... + # ], + # "error_code": "0", + # "ts": "1663364435973" + # } + # + result = self.safe_value(response, 'data', []) + withdrawFees: dict = {} + for i in range(0, len(result)): + item = result[i] + canWithdraw = self.safe_value(item, 'canWithDraw') + if canWithdraw == 'true': + currencyId = self.safe_string(item, 'assetCode') + codeInner = self.safe_currency_code(currencyId) + network = self.network_id_to_code(self.safe_string(item, 'chain')) + if network is None: + network = codeInner + fee = self.safe_string(item, 'fee') + if withdrawFees[codeInner] is None: + withdrawFees[codeInner] = {} + withdrawFees[codeInner][network] = self.parse_number(fee) + return { + 'withdraw': withdrawFees, + 'deposit': {}, + 'info': response, + } + + async def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}): + """ + when using private endpoint, only returns information for currencies with non-zero balance, use public method by specifying self.options['fetchDepositWithdrawFees']['method'] = 'fetchPublicDepositWithdrawFees' + + https://www.lbank.com/en-US/docs/index.html#get-all-coins-information + https://www.lbank.com/en-US/docs/index.html#withdrawal-configurations + + :param str[] [codes]: array of unified currency codes + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `fee structures ` + """ + await self.load_markets() + isAuthorized = self.check_required_credentials(False) + response = None + if isAuthorized is True: + options = self.safe_value(self.options, 'fetchDepositWithdrawFees', {}) + defaultMethod = self.safe_string(options, 'method', 'fetchPrivateDepositWithdrawFees') + method = self.safe_string(params, 'method', defaultMethod) + params = self.omit(params, 'method') + if method == 'fetchPublicDepositWithdrawFees': + response = await self.fetch_public_deposit_withdraw_fees(codes, params) + else: + response = await self.fetch_private_deposit_withdraw_fees(codes, params) + else: + response = await self.fetch_public_deposit_withdraw_fees(codes, params) + return response + + async def fetch_private_deposit_withdraw_fees(self, codes=None, params={}): + # complete response + # incl. for coins which None in public method + await self.load_markets() + response = await self.spotPrivatePostSupplementUserInfo(params) + # + # { + # "result": "true", + # "data": [ + # { + # "usableAmt": "14.36", + # "assetAmt": "14.36", + # "networkList": [ + # { + # "isDefault": False, + # "withdrawFeeRate": "", + # "name": "erc20", + # "withdrawMin": 30, + # "minLimit": 0.0001, + # "minDeposit": 20, + # "feeAssetCode": "usdt", + # "withdrawFee": "30", + # "type": 1, + # "coin": "usdt", + # "network": "eth" + # }, + # ... + # ], + # "freezeAmt": "0", + # "coin": "ada" + # } + # ], + # "code": 0 + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_deposit_withdraw_fees(data, codes, 'coin') + + async def fetch_public_deposit_withdraw_fees(self, codes=None, params={}): + # extremely incomplete response + # vast majority fees None + await self.load_markets() + request: dict = {} + response = await self.spotPublicGetWithdrawConfigs(self.extend(request, params)) + # + # { + # "result": "true", + # "data": [ + # { + # "amountScale": "4", + # "chain": "heco", + # "assetCode": "lbk", + # "min": "200", + # "transferAmtScale": "4", + # "canWithDraw": True, + # "fee": "100", + # "minTransfer": "0.0001", + # "type": "1" + # }, + # ... + # ], + # "error_code": "0", + # "ts": "1663364435973" + # } + # + data = self.safe_value(response, 'data', []) + return self.parse_public_deposit_withdraw_fees(data, codes) + + def parse_public_deposit_withdraw_fees(self, response, codes=None): + # + # [ + # { + # "amountScale": "4", + # "chain": "heco", + # "assetCode": "lbk", + # "min": "200", + # "transferAmtScale": "4", + # "canWithDraw": True, + # "fee": "100", + # "minTransfer": "0.0001", + # "type": "1" + # }, + # ... + # ] + # + result: dict = {} + for i in range(0, len(response)): + fee = response[i] + canWithdraw = self.safe_value(fee, 'canWithDraw') + if canWithdraw is True: + currencyId = self.safe_string(fee, 'assetCode') + code = self.safe_currency_code(currencyId) + if codes is None or self.in_array(code, codes): + withdrawFee = self.safe_number(fee, 'fee') + if withdrawFee is not None: + resultValue = self.safe_value(result, code) + if resultValue is None: + result[code] = self.deposit_withdraw_fee([fee]) + else: + resultCodeInfo = result[code]['info'] + resultCodeInfo.append(fee) + networkCode = self.network_id_to_code(self.safe_string(fee, 'chain')) + if networkCode is not None: + result[code]['networks'][networkCode] = { + 'withdraw': { + 'fee': withdrawFee, + 'percentage': None, + }, + 'deposit': { + 'fee': None, + 'percentage': None, + }, + } + else: + result[code]['withdraw'] = { + 'fee': withdrawFee, + 'percentage': None, + } + return result + + def parse_deposit_withdraw_fee(self, fee, currency: Currency = None): + # + # * only used for fetchPrivateDepositWithdrawFees + # + # { + # "usableAmt": "14.36", + # "assetAmt": "14.36", + # "networkList": [ + # { + # "isDefault": False, + # "withdrawFeeRate": "", + # "name": "erc20", + # "withdrawMin": 30, + # "minLimit": 0.0001, + # "minDeposit": 20, + # "feeAssetCode": "usdt", + # "withdrawFee": "30", + # "type": 1, + # "coin": "usdt", + # "network": "eth" + # }, + # ... + # ], + # "freezeAmt": "0", + # "coin": "ada" + # } + # + result = self.deposit_withdraw_fee(fee) + networkList = self.safe_value(fee, 'networkList', []) + for j in range(0, len(networkList)): + networkEntry = networkList[j] + networkCode = self.network_id_to_code(self.safe_string(networkEntry, 'name')) + withdrawFee = self.safe_number(networkEntry, 'withdrawFee') + isDefault = self.safe_value(networkEntry, 'isDefault') + if withdrawFee is not None: + if isDefault: + result['withdraw'] = { + 'fee': withdrawFee, + 'percentage': None, + } + result['networks'][networkCode] = { + 'withdraw': { + 'fee': withdrawFee, + 'percentage': None, + }, + 'deposit': { + 'fee': None, + 'percentage': None, + }, + } + return result + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + query = self.omit(params, self.extract_params(path)) + url = self.urls['api']['rest'] + '/' + self.version + '/' + self.implode_params(path, params) + # Every spot endpoint ends with ".do" + if api[0] == 'spot': + url += '.do' + else: + url = self.urls['api']['contract'] + '/' + self.implode_params(path, params) + if api[1] == 'public': + if query: + url += '?' + self.urlencode(self.keysort(query)) + else: + self.check_required_credentials() + timestamp = str(self.milliseconds()) + echostr = self.uuid22() + self.uuid16() + query = self.extend({ + 'api_key': self.apiKey, + }, query) + signatureMethod = None + if len(self.secret) > 32: + signatureMethod = 'RSA' + else: + signatureMethod = 'HmacSHA256' + auth = self.rawencode(self.keysort(self.extend({ + 'echostr': echostr, + 'signature_method': signatureMethod, + 'timestamp': timestamp, + }, query))) + encoded = self.encode(auth) + hash = self.hash(encoded, 'md5') + uppercaseHash = hash.upper() + sign = None + if signatureMethod == 'RSA': + cacheSecretAsPem = self.safe_bool(self.options, 'cacheSecretAsPem', True) + pem = None + if cacheSecretAsPem: + pem = self.safe_value(self.options, 'pem') + if pem is None: + pem = self.convert_secret_to_pem(self.encode(self.secret)) + self.options['pem'] = pem + else: + pem = self.convert_secret_to_pem(self.encode(self.secret)) + sign = self.rsa(uppercaseHash, pem, 'sha256') + elif signatureMethod == 'HmacSHA256': + sign = self.hmac(self.encode(uppercaseHash), self.encode(self.secret), hashlib.sha256) + query['sign'] = sign + body = self.urlencode(self.keysort(query)) + headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + 'timestamp': timestamp, + 'signature_method': signatureMethod, + 'echostr': echostr, + } + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def convert_secret_to_pem(self, secret): + lineLength = 64 + secretLength = len(secret) - 0 + numLines = self.parse_to_int(secretLength / lineLength) + numLines = self.sum(numLines, 1) + pem = "-----BEGIN PRIVATE KEY-----\n" # eslint-disable-line + for i in range(0, numLines): + start = i * lineLength + end = self.sum(start, lineLength) + pem += self.secret[start:end] + "\n" # eslint-disable-line + return pem + '-----END PRIVATE KEY-----' + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None + success = self.safe_value(response, 'result') + if success == 'false' or not success: + errorCode = self.safe_string(response, 'error_code') + message = self.safe_string({ + '10000': 'Internal error', + '10001': 'The required parameters can not be empty', + '10002': 'Validation failed', + '10003': 'Invalid parameter', + '10004': 'Request too frequent', + '10005': 'Secret key does not exist', + '10006': 'User does not exist', + '10007': 'Invalid signature', + '10008': 'Invalid Trading Pair', + '10009': 'Price and/or Amount are required for limit order', + '10010': 'Price and/or Amount must be less than minimum requirement', + # '10011': 'Market orders can not be missing the amount of the order', + # '10012': 'market sell orders can not be missing orders', + '10013': 'The amount is too small', + '10014': 'Insufficient amount of money in the account', + '10015': 'Invalid order type', + '10016': 'Insufficient account balance', + '10017': 'Server Error', + '10018': 'Page size should be between 1 and 50', + '10019': 'Cancel NO more than 3 orders in one request', + '10020': 'Volume < 0.001', + '10021': 'Price < 0.01', + '10022': 'Invalid authorization', + '10023': 'Market Order is not supported yet', + '10024': 'User cannot trade on self pair', + '10025': 'Order has been filled', + '10026': 'Order has been cancelld', + '10027': 'Order is cancelling', + '10028': 'Wrong query time', + '10029': 'from is not in the query time', + '10030': 'from do not match the transaction type of inqury', + '10031': 'echostr length must be valid and length must be from 30 to 40', + '10033': 'Failed to create order', + '10036': 'customID duplicated', + '10100': 'Has no privilege to withdraw', + '10101': 'Invalid fee rate to withdraw', + '10102': 'Too little to withdraw', + '10103': 'Exceed daily limitation of withdraw', + '10104': 'Cancel was rejected', + '10105': 'Request has been cancelled', + '10106': 'None trade time', + '10107': 'Start price exception', + '10108': 'can not create order', + '10109': 'wallet address is not mapping', + '10110': 'transfer fee is not mapping', + '10111': 'mount > 0', + '10112': 'fee is too lower', + '10113': 'transfer fee is 0', + '10600': 'intercepted by replay attacks filter, check timestamp', + '10601': 'Interface closed unavailable', + '10701': 'invalid asset code', + '10702': 'not allowed deposit', + }, errorCode, self.json(response)) + ErrorClass = self.safe_value({ + '10001': BadRequest, + '10002': AuthenticationError, + '10003': BadRequest, + '10004': RateLimitExceeded, + '10005': AuthenticationError, + '10006': AuthenticationError, + '10007': AuthenticationError, + '10008': BadSymbol, + '10009': InvalidOrder, + '10010': InvalidOrder, + '10013': InvalidOrder, + '10014': InsufficientFunds, + '10015': InvalidOrder, + '10016': InsufficientFunds, + '10017': ExchangeError, + '10018': BadRequest, + '10019': BadRequest, + '10020': BadRequest, + '10021': InvalidOrder, + '10022': PermissionDenied, # 'Invalid authorization', + '10023': InvalidOrder, # 'Market Order is not supported yet', + '10024': PermissionDenied, # 'User cannot trade on self pair', + '10025': InvalidOrder, # 'Order has been filled', + '10026': InvalidOrder, # 'Order has been cancelled', + '10027': InvalidOrder, # 'Order is cancelling', + '10028': BadRequest, # 'Wrong query time', + '10029': BadRequest, # 'from is not in the query time', + '10030': BadRequest, # 'from do not match the transaction type of inqury', + '10031': InvalidNonce, # 'echostr length must be valid and length must be from 30 to 40', + '10033': ExchangeError, # 'Failed to create order', + '10036': DuplicateOrderId, # 'customID duplicated', + '10100': PermissionDenied, # 'Has no privilege to withdraw', + '10101': BadRequest, # 'Invalid fee rate to withdraw', + '10102': InsufficientFunds, # 'Too little to withdraw', + '10103': ExchangeError, # 'Exceed daily limitation of withdraw', + '10104': ExchangeError, # 'Cancel was rejected', + '10105': ExchangeError, # 'Request has been cancelled', + '10106': BadRequest, # 'None trade time', + '10107': BadRequest, # 'Start price exception', + '10108': ExchangeError, # 'can not create order', + '10109': InvalidAddress, # 'wallet address is not mapping', + '10110': ExchangeError, # 'transfer fee is not mapping', + '10111': BadRequest, # 'mount > 0', + '10112': BadRequest, # 'fee is too lower', + '10113': BadRequest, # 'transfer fee is 0', + '10600': BadRequest, # 'intercepted by replay attacks filter, check timestamp', + '10601': ExchangeError, # 'Interface closed unavailable', + '10701': BadSymbol, # 'invalid asset code', + '10702': PermissionDenied, # 'not allowed deposit', + }, errorCode, ExchangeError) + raise ErrorClass(message) + return None diff --git a/ccxt/async_support/luno.py b/ccxt/async_support/luno.py new file mode 100644 index 0000000..a976c56 --- /dev/null +++ b/ccxt/async_support/luno.py @@ -0,0 +1,1417 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.luno import ImplicitAPI +from ccxt.base.types import Account, Any, Balances, Currencies, Currency, DepositAddress, Int, LedgerEntry, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, TradingFeeInterface +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class luno(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(luno, self).describe(), { + 'id': 'luno', + 'name': 'luno', + 'countries': ['GB', 'SG', 'ZA'], + # 300 calls per minute = 5 calls per second = 1000ms / 5 = 200ms between requests + 'rateLimit': 200, + 'version': '1', + 'pro': True, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelOrder': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createDepositAddress': True, + 'createOrder': True, + 'createReduceOnlyOrder': False, + 'fetchAccounts': True, + 'fetchAllGreeks': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': True, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLedger': True, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrice': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchPosition': False, + 'fetchPositionForSymbolWs': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsForSymbolWs': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTrades': True, + 'fetchTradingFee': True, + 'fetchTradingFees': False, + 'fetchUnderlyingAssets': False, + 'fetchVolatilityHistory': False, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + }, + 'urls': { + 'referral': 'https://www.luno.com/invite/44893A', + 'logo': 'https://user-images.githubusercontent.com/1294454/27766607-8c1a69d8-5ede-11e7-930c-540b5eb9be24.jpg', + 'api': { + 'public': 'https://api.luno.com/api', + 'private': 'https://api.luno.com/api', + 'exchange': 'https://api.luno.com/api/exchange', + 'exchangePrivate': 'https://api.luno.com/api/exchange', + }, + 'www': 'https://www.luno.com', + 'doc': [ + 'https://www.luno.com/en/api', + 'https://npmjs.org/package/bitx', + 'https://github.com/bausmeier/node-bitx', + ], + }, + 'api': { + 'exchange': { + 'get': { + 'markets': 1, + }, + }, + 'exchangePrivate': { + 'get': { + 'candles': 1, + }, + }, + 'public': { + 'get': { + 'orderbook': 1, + 'orderbook_top': 1, + 'ticker': 1, + 'tickers': 1, + 'trades': 1, + }, + }, + 'private': { + 'get': { + 'accounts/{id}/pending': 1, + 'accounts/{id}/transactions': 1, + 'balance': 1, + 'beneficiaries': 1, + 'send/networks': 1, + 'fee_info': 1, + 'funding_address': 1, + 'listorders': 1, + 'listtrades': 1, + 'send_fee': 1, + 'orders/{id}': 1, + 'withdrawals': 1, + 'withdrawals/{id}': 1, + 'transfers': 1, + # GET /api/exchange/1/move + # GET /api/exchange/1/move/list_moves + # GET /api/exchange/1/candles + # GET /api/exchange/1/transfers + # GET /api/exchange/2/listorders + # GET /api/exchange/2/orders/{id} + # GET /api/exchange/3/order + }, + 'post': { + 'accounts': 1, + 'address/validate': 1, + 'postorder': 1, + 'marketorder': 1, + 'stoporder': 1, + 'funding_address': 1, + 'withdrawals': 1, + 'send': 1, + 'oauth2/grant': 1, + 'beneficiaries': 1, + # POST /api/exchange/1/move + }, + 'put': { + 'accounts/{id}/name': 1, + }, + 'delete': { + 'withdrawals/{id}': 1, + 'beneficiaries/{id}': 1, + }, + }, + }, + 'timeframes': { + '1m': 60, + '5m': 300, + '15m': 900, + '30m': 1800, + '1h': 3600, + '3h': 10800, + '4h': 14400, + '1d': 86400, + '3d': 259200, + '1w': 604800, + }, + 'fees': { + 'trading': { + 'tierBased': True, # based on volume from your primary currency(not the same for everyone) + 'percentage': True, + 'taker': self.parse_number('0.001'), + 'maker': self.parse_number('0'), + }, + }, + 'precisionMode': TICK_SIZE, + 'features': { + 'spot': { + 'sandbox': False, + 'fetchCurrencies': { + 'private': True, + }, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, # todo + 'triggerPriceType': None, + 'triggerDirection': True, # todo + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 100000, # todo + 'untilDays': 100000, # todo + 'symbolRequired': True, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 1000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 100000, + 'untilDays': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 100000, + 'daysBackCanceled': 1, + 'untilDays': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': None, + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + }) + + async def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + if not self.check_required_credentials(False): + return {} + response = await self.privateGetSendNetworks(params) + # + # { + # "networks": [ + # { + # "id": 0, + # "name": "Ethereum", + # "native_currency": "ETH" + # }, + # ... + # ] + # } + # + currenciesData = self.safe_list(response, 'data', []) + result: dict = {} + for i in range(0, len(currenciesData)): + networkEntry = currenciesData[i] + id = self.safe_string(networkEntry, 'native_currency') + code = self.safe_currency_code(id) + if not (code in result): + result[code] = { + 'id': id, + 'code': code, + 'precision': None, + 'type': None, + 'name': None, + 'active': None, + 'deposit': None, + 'withdraw': None, + 'fee': None, + 'limits': { + 'withdraw': { + 'min': None, + 'max': None, + }, + 'deposit': { + 'min': None, + 'max': None, + }, + }, + 'networks': {}, + 'info': {}, + } + networkId = self.safe_string(networkEntry, 'name') + networkCode = self.network_id_to_code(networkId) + result[code]['networks'][networkCode] = { + 'id': networkId, + 'network': networkCode, + 'limits': { + 'withdraw': { + 'min': None, + 'max': None, + }, + 'deposit': { + 'min': None, + 'max': None, + }, + }, + 'active': None, + 'deposit': None, + 'withdraw': None, + 'fee': None, + 'precision': None, + 'info': networkEntry, + } + # add entry in info + info = self.safe_list(result[code], 'info', []) + info.append(networkEntry) + result[code]['info'] = info + # only after all entries are formed in currencies, restructure each entry + allKeys = list(result.keys()) + for i in range(0, len(allKeys)): + code = allKeys[i] + result[code] = self.safe_currency_structure(result[code]) # self is needed after adding network entry + return result + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for luno + + https://www.luno.com/en/developers/api#tag/Market/operation/Markets + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = await self.exchangeGetMarkets(params) + # + # { + # "markets":[ + # { + # "market_id":"BCHXBT", + # "trading_status":"ACTIVE", + # "base_currency":"BCH", + # "counter_currency":"XBT", + # "min_volume":"0.01", + # "max_volume":"100.00", + # "volume_scale":2, + # "min_price":"0.0001", + # "max_price":"1.00", + # "price_scale":6, + # "fee_scale":8, + # }, + # ] + # } + # + result = [] + markets = self.safe_value(response, 'markets', []) + for i in range(0, len(markets)): + market = markets[i] + id = self.safe_string(market, 'market_id') + baseId = self.safe_string(market, 'base_currency') + quoteId = self.safe_string(market, 'counter_currency') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + status = self.safe_string(market, 'trading_status') + result.append({ + 'id': id, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': (status == 'ACTIVE'), + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(self.parse_precision(self.safe_string(market, 'volume_scale'))), + 'price': self.parse_number(self.parse_precision(self.safe_string(market, 'price_scale'))), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(market, 'min_volume'), + 'max': self.safe_number(market, 'max_volume'), + }, + 'price': { + 'min': self.safe_number(market, 'min_price'), + 'max': self.safe_number(market, 'max_price'), + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + 'info': market, + }) + return result + + async def fetch_accounts(self, params={}) -> List[Account]: + """ + fetch all the accounts associated with a profile + + https://www.luno.com/en/developers/api#tag/Accounts/operation/getBalances + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `account structures ` indexed by the account type + """ + response = await self.privateGetBalance(params) + wallets = self.safe_value(response, 'balance', []) + result = [] + for i in range(0, len(wallets)): + account = wallets[i] + accountId = self.safe_string(account, 'account_id') + currencyId = self.safe_string(account, 'asset') + code = self.safe_currency_code(currencyId) + result.append({ + 'id': accountId, + 'type': None, + 'currency': code, + 'info': account, + }) + return result + + def parse_balance(self, response) -> Balances: + wallets = self.safe_value(response, 'balance', []) + result: dict = { + 'info': response, + 'timestamp': None, + 'datetime': None, + } + for i in range(0, len(wallets)): + wallet = wallets[i] + currencyId = self.safe_string(wallet, 'asset') + code = self.safe_currency_code(currencyId) + reserved = self.safe_string(wallet, 'reserved') + unconfirmed = self.safe_string(wallet, 'unconfirmed') + balance = self.safe_string(wallet, 'balance') + reservedUnconfirmed = Precise.string_add(reserved, unconfirmed) + balanceUnconfirmed = Precise.string_add(balance, unconfirmed) + if code in result: + result[code]['used'] = Precise.string_add(result[code]['used'], reservedUnconfirmed) + result[code]['total'] = Precise.string_add(result[code]['total'], balanceUnconfirmed) + else: + account = self.account() + account['used'] = reservedUnconfirmed + account['total'] = balanceUnconfirmed + result[code] = account + return self.safe_balance(result) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://www.luno.com/en/developers/api#tag/Accounts/operation/getBalances + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + response = await self.privateGetBalance(params) + # + # { + # "balance": [ + # {'account_id': '119...1336','asset': 'XBT','balance': '0.00','reserved': '0.00',"unconfirmed": "0.00"}, + # {'account_id': '66...289','asset': 'XBT','balance': '0.00','reserved': '0.00',"unconfirmed": "0.00"}, + # {'account_id': '718...5300','asset': 'ETH','balance': '0.00','reserved': '0.00',"unconfirmed": "0.00"}, + # {'account_id': '818...7072','asset': 'ZAR','balance': '0.001417','reserved': '0.00',"unconfirmed": "0.00"}]} + # ] + # } + # + return self.parse_balance(response) + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://www.luno.com/en/developers/api#tag/Market/operation/GetOrderBookFull + https://www.luno.com/en/developers/api#tag/Market/operation/GetOrderBook + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + response = None + if limit is not None and limit <= 100: + response = await self.publicGetOrderbookTop(self.extend(request, params)) + else: + response = await self.publicGetOrderbook(self.extend(request, params)) + timestamp = self.safe_integer(response, 'timestamp') + return self.parse_order_book(response, market['symbol'], timestamp, 'bids', 'asks', 'price', 'volume') + + def parse_order_status(self, status: Str): + statuses: dict = { + # todo add other statuses + 'PENDING': 'open', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # { + # "base": "string", + # "completed_timestamp": "string", + # "counter": "string", + # "creation_timestamp": "string", + # "expiration_timestamp": "string", + # "fee_base": "string", + # "fee_counter": "string", + # "limit_price": "string", + # "limit_volume": "string", + # "order_id": "string", + # "pair": "string", + # "state": "PENDING", + # "type": "BID" + # } + # + timestamp = self.safe_integer(order, 'creation_timestamp') + status = self.parse_order_status(self.safe_string(order, 'state')) + status = status if (status == 'open') else status + side = None + orderType = self.safe_string(order, 'type') + if (orderType == 'ASK') or (orderType == 'SELL'): + side = 'sell' + elif (orderType == 'BID') or (orderType == 'BUY'): + side = 'buy' + marketId = self.safe_string(order, 'pair') + market = self.safe_market(marketId, market) + price = self.safe_string(order, 'limit_price') + amount = self.safe_string(order, 'limit_volume') + quoteFee = self.safe_number(order, 'fee_counter') + baseFee = self.safe_number(order, 'fee_base') + filled = self.safe_string(order, 'base') + cost = self.safe_string(order, 'counter') + fee = None + if quoteFee is not None: + fee = { + 'cost': quoteFee, + 'currency': market['quote'], + } + elif baseFee is not None: + fee = { + 'cost': baseFee, + 'currency': market['base'], + } + id = self.safe_string(order, 'order_id') + return self.safe_order({ + 'id': id, + 'clientOrderId': None, + 'datetime': self.iso8601(timestamp), + 'timestamp': timestamp, + 'lastTradeTimestamp': None, + 'status': status, + 'symbol': market['symbol'], + 'type': None, + 'timeInForce': None, + 'postOnly': None, + 'side': side, + 'price': price, + 'triggerPrice': None, + 'amount': amount, + 'filled': filled, + 'cost': cost, + 'remaining': None, + 'trades': None, + 'fee': fee, + 'info': order, + 'average': None, + }, market) + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://www.luno.com/en/developers/api#tag/Orders/operation/GetOrder + + :param str id: order id + :param str symbol: not used by luno fetchOrder + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + request: dict = { + 'id': id, + } + response = await self.privateGetOrdersId(self.extend(request, params)) + return self.parse_order(response) + + async def fetch_orders_by_state(self, state: Str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + await self.load_markets() + request: dict = {} + market = None + if state is not None: + request['state'] = state + if symbol is not None: + market = self.market(symbol) + request['pair'] = market['id'] + response = await self.privateGetListorders(self.extend(request, params)) + orders = self.safe_list(response, 'orders', []) + return self.parse_orders(orders, market, since, limit) + + async def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://www.luno.com/en/developers/api#tag/Orders/operation/ListOrders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + return await self.fetch_orders_by_state(None, symbol, since, limit, params) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://www.luno.com/en/developers/api#tag/Orders/operation/ListOrders + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + return await self.fetch_orders_by_state('PENDING', symbol, since, limit, params) + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://www.luno.com/en/developers/api#tag/Orders/operation/ListOrders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + return await self.fetch_orders_by_state('COMPLETE', symbol, since, limit, params) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # { + # "pair":"XBTAUD", + # "timestamp":1642201439301, + # "bid":"59972.30000000", + # "ask":"59997.99000000", + # "last_trade":"59997.99000000", + # "rolling_24_hour_volume":"1.89510000", + # "status":"ACTIVE" + # } + timestamp = self.safe_integer(ticker, 'timestamp') + marketId = self.safe_string(ticker, 'pair') + symbol = self.safe_symbol(marketId, market) + last = self.safe_string(ticker, 'last_trade') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': None, + 'low': None, + 'bid': self.safe_string(ticker, 'bid'), + 'bidVolume': None, + 'ask': self.safe_string(ticker, 'ask'), + 'askVolume': None, + 'vwap': None, + 'open': None, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': self.safe_string(ticker, 'rolling_24_hour_volume'), + 'quoteVolume': None, + 'info': ticker, + }, market) + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://www.luno.com/en/developers/api#tag/Market/operation/GetTickers + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + response = await self.publicGetTickers(params) + tickers = self.index_by(response['tickers'], 'pair') + ids = list(tickers.keys()) + result: dict = {} + for i in range(0, len(ids)): + id = ids[i] + market = self.safe_market(id) + symbol = market['symbol'] + ticker = tickers[id] + result[symbol] = self.parse_ticker(ticker, market) + return self.filter_by_array_tickers(result, 'symbol', symbols) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://www.luno.com/en/developers/api#tag/Market/operation/GetTicker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + response = await self.publicGetTicker(self.extend(request, params)) + # { + # "pair":"XBTAUD", + # "timestamp":1642201439301, + # "bid":"59972.30000000", + # "ask":"59997.99000000", + # "last_trade":"59997.99000000", + # "rolling_24_hour_volume":"1.89510000", + # "status":"ACTIVE" + # } + return self.parse_ticker(response, market) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(public) + # + # { + # "sequence":276989, + # "timestamp":1648651276949, + # "price":"35773.20000000", + # "volume":"0.00300000", + # "is_buy":false + # } + # + # fetchMyTrades(private) + # + # { + # "pair":"LTCXBT", + # "sequence":3256813, + # "order_id":"BXEX6XHHDT5EGW2", + # "type":"ASK", + # "timestamp":1648652135235, + # "price":"0.002786", + # "volume":"0.10", + # "base":"0.10", + # "counter":"0.0002786", + # "fee_base":"0.0001", + # "fee_counter":"0.00", + # "is_buy":false, + # "client_order_id":"" + # } + # + # For public trade data(is_buy is True) indicates 'buy' side but for private trade data + # is_buy indicates maker or taker. The value of "type"(ASK/BID) indicate sell/buy side. + # Private trade data includes ID field which public trade data does not. + orderId = self.safe_string(trade, 'order_id') + id = self.safe_string(trade, 'sequence') + takerOrMaker = None + side = None + if orderId is not None: + type = self.safe_string(trade, 'type') + if (type == 'ASK') or (type == 'SELL'): + side = 'sell' + elif (type == 'BID') or (type == 'BUY'): + side = 'buy' + if side == 'sell' and trade['is_buy']: + takerOrMaker = 'maker' + elif side == 'buy' and not trade['is_buy']: + takerOrMaker = 'maker' + else: + takerOrMaker = 'taker' + else: + side = 'buy' if trade['is_buy'] else 'sell' + feeBaseString = self.safe_string(trade, 'fee_base') + feeCounterString = self.safe_string(trade, 'fee_counter') + feeCurrency = None + feeCost = None + if feeBaseString is not None: + if not Precise.string_equals(feeBaseString, '0.0'): + feeCurrency = market['base'] + feeCost = feeBaseString + elif feeCounterString is not None: + if not Precise.string_equals(feeCounterString, '0.0'): + feeCurrency = market['quote'] + feeCost = feeCounterString + timestamp = self.safe_integer(trade, 'timestamp') + return self.safe_trade({ + 'info': trade, + 'id': id, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'order': orderId, + 'type': None, + 'side': side, + 'takerOrMaker': takerOrMaker, + 'price': self.safe_string(trade, 'price'), + 'amount': self.safe_string_2(trade, 'volume', 'base'), + # Does not include potential fee costs + 'cost': self.safe_string(trade, 'counter'), + 'fee': { + 'cost': feeCost, + 'currency': feeCurrency, + }, + }, market) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://www.luno.com/en/developers/api#tag/Market/operation/ListTrades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + if since is not None: + request['since'] = since + response = await self.publicGetTrades(self.extend(request, params)) + # + # { + # "trades":[ + # { + # "sequence":276989, + # "timestamp":1648651276949, + # "price":"35773.20000000", + # "volume":"0.00300000", + # "is_buy":false + # },... + # ] + # } + # + trades = self.safe_list(response, 'trades', []) + return self.parse_trades(trades, market, since, limit) + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}): + """ + + https://www.luno.com/en/developers/api#tag/Market/operation/GetCandles + + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict params: extra parameters specific to the luno api endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'duration': self.safe_value(self.timeframes, timeframe, timeframe), + 'pair': market['id'], + } + if since is not None: + request['since'] = self.parse_to_int(since) + else: + duration = 1000 * 1000 * self.parse_timeframe(timeframe) + request['since'] = self.milliseconds() - duration + response = await self.exchangePrivateGetCandles(self.extend(request, params)) + # + # { + # "candles": [ + # { + # "timestamp": 1664055240000, + # "open": "19612.65", + # "close": "19612.65", + # "high": "19612.65", + # "low": "19612.65", + # "volume": "0.00" + # },... + # ], + # "duration": 60, + # "pair": "XBTEUR" + # } + # + ohlcvs = self.safe_list(response, 'candles', []) + return self.parse_ohlcvs(ohlcvs, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # { + # "timestamp": 1664055240000, + # "open": "19612.65", + # "close": "19612.65", + # "high": "19612.65", + # "low": "19612.65", + # "volume": "0.00" + # } + return [ + self.safe_integer(ohlcv, 'timestamp'), + self.safe_number(ohlcv, 'open'), + self.safe_number(ohlcv, 'high'), + self.safe_number(ohlcv, 'low'), + self.safe_number(ohlcv, 'close'), + self.safe_number(ohlcv, 'volume'), + ] + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://www.luno.com/en/developers/api#tag/Orders/operation/ListUserTrades + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMyTrades() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + if since is not None: + request['since'] = since + if limit is not None: + request['limit'] = limit + response = await self.privateGetListtrades(self.extend(request, params)) + # + # { + # "trades":[ + # { + # "pair":"LTCXBT", + # "sequence":3256813, + # "order_id":"BXEX6XHHDT5EGW2", + # "type":"ASK", + # "timestamp":1648652135235, + # "price":"0.002786", + # "volume":"0.10", + # "base":"0.10", + # "counter":"0.0002786", + # "fee_base":"0.0001", + # "fee_counter":"0.00", + # "is_buy":false, + # "client_order_id":"" + # },... + # ] + # } + # + trades = self.safe_list(response, 'trades', []) + return self.parse_trades(trades, market, since, limit) + + async def fetch_trading_fee(self, symbol: str, params={}) -> TradingFeeInterface: + """ + fetch the trading fees for a market + + https://www.luno.com/en/developers/api#tag/Orders/operation/getFeeInfo + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `fee structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + response = await self.privateGetFeeInfo(self.extend(request, params)) + # + # { + # "maker_fee": "0.00250000", + # "taker_fee": "0.00500000", + # "thirty_day_volume": "0" + # } + # + return { + 'info': response, + 'symbol': symbol, + 'maker': self.safe_number(response, 'maker_fee'), + 'taker': self.safe_number(response, 'taker_fee'), + 'percentage': None, + 'tierBased': None, + } + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://www.luno.com/en/developers/api#tag/Orders/operation/PostMarketOrder + https://www.luno.com/en/developers/api#tag/Orders/operation/PostLimitOrder + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + response = None + if type == 'market': + request['type'] = side.upper() + # todo add createMarketBuyOrderRequires price logic is implemented in the other exchanges + if side == 'buy': + request['counter_volume'] = self.amount_to_precision(market['symbol'], amount) + else: + request['base_volume'] = self.amount_to_precision(market['symbol'], amount) + response = await self.privatePostMarketorder(self.extend(request, params)) + else: + request['volume'] = self.amount_to_precision(market['symbol'], amount) + request['price'] = self.price_to_precision(market['symbol'], price) + request['type'] = 'BID' if (side == 'buy') else 'ASK' + response = await self.privatePostPostorder(self.extend(request, params)) + return self.safe_order({ + 'info': response, + 'id': response['order_id'], + }, market) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://www.luno.com/en/developers/api#tag/Orders/operation/StopOrder + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + request: dict = { + 'order_id': id, + } + response = await self.privatePostStoporder(self.extend(request, params)) + # + # { + # "success": True + # } + # + return self.safe_order({ + 'info': response, + }) + + async def fetch_ledger_by_entries(self, code: Str = None, entry=None, limit=None, params={}): + # by default without entry number or limit number, return most recent entry + if entry is None: + entry = -1 + if limit is None: + limit = 1 + since = None + request: dict = { + 'min_row': entry, + 'max_row': self.sum(entry, limit), + } + return await self.fetch_ledger(code, since, limit, self.extend(request, params)) + + async def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + + https://www.luno.com/en/developers/api#tag/Accounts/operation/ListTransactions + + :param str [code]: unified currency code, default is None + :param int [since]: timestamp in ms of the earliest ledger entry, default is None + :param int [limit]: max number of ledger entries to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ledger structure ` + """ + await self.load_markets() + await self.load_accounts() + currency = None + id = self.safe_string(params, 'id') # account id + min_row = self.safe_value(params, 'min_row') + max_row = self.safe_value(params, 'max_row') + if id is None: + if code is None: + raise ArgumentsRequired(self.id + ' fetchLedger() requires a currency code argument if no account id specified in params') + currency = self.currency(code) + accountsByCurrencyCode = self.index_by(self.accounts, 'currency') + account = self.safe_value(accountsByCurrencyCode, code) + if account is None: + raise ExchangeError(self.id + ' fetchLedger() could not find account id for ' + code) + id = account['id'] + if min_row is None and max_row is None: + max_row = 0 # Default to most recent transactions + min_row = -1000 # Maximum number of records supported + elif min_row is None or max_row is None: + raise ExchangeError(self.id + " fetchLedger() require both params 'max_row' and 'min_row' or neither to be defined") + if limit is not None and max_row - min_row > limit: + if max_row <= 0: + min_row = max_row - limit + elif min_row > 0: + max_row = min_row + limit + if max_row - min_row > 1000: + raise ExchangeError(self.id + " fetchLedger() requires the params 'max_row' - 'min_row' <= 1000") + request: dict = { + 'id': id, + 'min_row': min_row, + 'max_row': max_row, + } + response = await self.privateGetAccountsIdTransactions(self.extend(params, request)) + entries = self.safe_value(response, 'transactions', []) + return self.parse_ledger(entries, currency, since, limit) + + def parse_ledger_comment(self, comment): + words = comment.split(' ') + types: dict = { + 'Withdrawal': 'fee', + 'Trading': 'fee', + 'Payment': 'transaction', + 'Sent': 'transaction', + 'Deposit': 'transaction', + 'Received': 'transaction', + 'Released': 'released', + 'Reserved': 'reserved', + 'Sold': 'trade', + 'Bought': 'trade', + 'Failure': 'failed', + } + referenceId = None + firstWord = self.safe_string(words, 0) + thirdWord = self.safe_string(words, 2) + fourthWord = self.safe_string(words, 3) + type = self.safe_string(types, firstWord, None) + if (type is None) and (thirdWord == 'fee'): + type = 'fee' + if (type == 'reserved') and (fourthWord == 'order'): + referenceId = self.safe_string(words, 4) + return { + 'type': type, + 'referenceId': referenceId, + } + + def parse_ledger_entry(self, entry, currency: Currency = None) -> LedgerEntry: + # details = self.safe_value(entry, 'details', {}) + id = self.safe_string(entry, 'row_index') + account_id = self.safe_string(entry, 'account_id') + timestamp = self.safe_integer(entry, 'timestamp') + currencyId = self.safe_string(entry, 'currency') + code = self.safe_currency_code(currencyId, currency) + currency = self.safe_currency(currencyId, currency) + available_delta = self.safe_string(entry, 'available_delta') + balance_delta = self.safe_string(entry, 'balance_delta') + after = self.safe_string(entry, 'balance') + comment = self.safe_string(entry, 'description') + before = after + amount = '0.0' + result = self.parse_ledger_comment(comment) + type = result['type'] + referenceId = result['referenceId'] + direction = None + status = None + if not Precise.string_equals(balance_delta, '0.0'): + before = Precise.string_sub(after, balance_delta) + status = 'ok' + amount = Precise.string_abs(balance_delta) + elif Precise.string_lt(available_delta, '0.0'): + status = 'pending' + amount = Precise.string_abs(available_delta) + elif Precise.string_gt(available_delta, '0.0'): + status = 'canceled' + amount = Precise.string_abs(available_delta) + if Precise.string_gt(balance_delta, '0') or Precise.string_gt(available_delta, '0'): + direction = 'in' + elif Precise.string_lt(balance_delta, '0') or Precise.string_lt(available_delta, '0'): + direction = 'out' + return self.safe_ledger_entry({ + 'info': entry, + 'id': id, + 'direction': direction, + 'account': account_id, + 'referenceId': referenceId, + 'referenceAccount': None, + 'type': type, + 'currency': code, + 'amount': self.parse_to_numeric(amount), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'before': self.parse_to_numeric(before), + 'after': self.parse_to_numeric(after), + 'status': status, + 'fee': None, + }, currency) + + async def create_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + create a currency deposit address + + https://www.luno.com/en/developers/api#tag/Receive/operation/createFundingAddress + + :param str code: unified currency code of the currency for the deposit address + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.name]: an optional name for the new address + :param int [params.account_id]: an optional account id for the new address + :returns dict: an `address structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'asset': currency['id'], + } + response = await self.privatePostFundingAddress(self.extend(request, params)) + # + # { + # "account_id": "string", + # "address": "string", + # "address_meta": [ + # { + # "label": "string", + # "value": "string" + # } + # ], + # "asset": "string", + # "assigned_at": 0, + # "name": "string", + # "network": 0, + # "qr_code_uri": "string", + # "receive_fee": "string", + # "total_received": "string", + # "total_unconfirmed": "string" + # } + # + return self.parse_deposit_address(response, currency) + + async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://www.luno.com/en/developers/api#tag/Receive/operation/getFundingAddress + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.address]: a specific cryptocurrency address to retrieve + :returns dict: an `address structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'asset': currency['id'], + } + response = await self.privateGetFundingAddress(self.extend(request, params)) + # + # { + # "account_id": "string", + # "address": "string", + # "address_meta": [ + # { + # "label": "string", + # "value": "string" + # } + # ], + # "asset": "string", + # "assigned_at": 0, + # "name": "string", + # "network": 0, + # "qr_code_uri": "string", + # "receive_fee": "string", + # "total_received": "string", + # "total_unconfirmed": "string" + # } + # + return self.parse_deposit_address(response, currency) + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + # + # { + # "account_id": "string", + # "address": "string", + # "address_meta": [ + # { + # "label": "string", + # "value": "string" + # } + # ], + # "asset": "string", + # "assigned_at": 0, + # "name": "string", + # "network": 0, + # "qr_code_uri": "string", + # "receive_fee": "string", + # "total_received": "string", + # "total_unconfirmed": "string" + # } + # + currencyId = self.safe_string_upper(depositAddress, 'currency') + code = self.safe_currency_code(currencyId, currency) + return { + 'info': depositAddress, + 'currency': code, + 'network': None, + 'address': self.safe_string(depositAddress, 'address'), + 'tag': self.safe_string(depositAddress, 'name'), + } + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = self.urls['api'][api] + '/' + self.version + '/' + self.implode_params(path, params) + query = self.omit(params, self.extract_params(path)) + if query: + url += '?' + self.urlencode(query) + if (api == 'private') or (api == 'exchangePrivate'): + self.check_required_credentials() + auth = self.string_to_base64(self.apiKey + ':' + self.secret) + headers = { + 'Authorization': 'Basic ' + auth, + } + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None + error = self.safe_value(response, 'error') + if error is not None: + raise ExchangeError(self.id + ' ' + self.json(response)) + return None diff --git a/ccxt/async_support/mercado.py b/ccxt/async_support/mercado.py new file mode 100644 index 0000000..ae4f2c8 --- /dev/null +++ b/ccxt/async_support/mercado.py @@ -0,0 +1,950 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.mercado import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currency, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Ticker, Trade, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import InvalidOrder +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class mercado(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(mercado, self).describe(), { + 'id': 'mercado', + 'name': 'Mercado Bitcoin', + 'countries': ['BR'], # Brazil + 'rateLimit': 1000, + 'version': 'v3', + 'has': { + 'CORS': True, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelOrder': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createMarketOrder': True, + 'createOrder': True, + 'createReduceOnlyOrder': False, + 'createStopLimitOrder': False, + 'createStopMarketOrder': False, + 'createStopOrder': False, + 'fetchAllGreeks': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': False, + 'fetchDepositAddress': False, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrice': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': 'emulated', + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTickers': False, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': False, + 'fetchUnderlyingAssets': False, + 'fetchVolatilityHistory': False, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'withdraw': True, + }, + 'timeframes': { + '15m': '15m', + '1h': '1h', + '3h': '3h', + '1d': '1d', + '1w': '1w', + '1M': '1M', + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/27837060-e7c58714-60ea-11e7-9192-f05e86adb83f.jpg', + 'api': { + 'public': 'https://www.mercadobitcoin.net/api', + 'private': 'https://www.mercadobitcoin.net/tapi', + 'v4Public': 'https://www.mercadobitcoin.com.br/v4', + 'v4PublicNet': 'https://api.mercadobitcoin.net/api/v4', + }, + 'www': 'https://www.mercadobitcoin.com.br', + 'doc': [ + 'https://www.mercadobitcoin.com.br/api-doc', + 'https://www.mercadobitcoin.com.br/trade-api', + ], + }, + 'api': { + 'public': { + 'get': [ + 'coins', + '{coin}/orderbook/', # last slash critical + '{coin}/ticker/', + '{coin}/trades/', + '{coin}/trades/{from}/', + '{coin}/trades/{from}/{to}', + '{coin}/day-summary/{year}/{month}/{day}/', + ], + }, + 'private': { + 'post': [ + 'cancel_order', + 'get_account_info', + 'get_order', + 'get_withdrawal', + 'list_system_messages', + 'list_orders', + 'list_orderbook', + 'place_buy_order', + 'place_sell_order', + 'place_market_buy_order', + 'place_market_sell_order', + 'withdraw_coin', + ], + }, + 'v4Public': { + 'get': [ + '{coin}/candle/', + ], + }, + 'v4PublicNet': { + 'get': [ + 'candles', + ], + }, + }, + 'fees': { + 'trading': { + 'maker': 0.003, + 'taker': 0.007, + }, + }, + 'options': { + 'limits': { + 'BTC': 0.001, + 'BCH': 0.001, + 'ETH': 0.01, + 'LTC': 0.01, + 'XRP': 0.1, + }, + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': False, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': False, + 'FOK': False, + 'PO': True, # todo + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': True, # todo + 'marketBuyRequiresPrice': True, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': None, # todo + 'daysBack': 100000, # todo + 'untilDays': 100000, # todo + 'symbolRequired': True, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': 500, + 'daysBack': 100000, + 'untilDays': 100000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchClosedOrders': None, + 'fetchOHLCV': { + 'limit': 1000, + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'precisionMode': TICK_SIZE, + }) + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for mercado + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = await self.publicGetCoins(params) + # + # [ + # "BCH", + # "BTC", + # "ETH", + # "LTC", + # "XRP", + # "MBPRK01", + # "MBPRK02", + # "MBPRK03", + # "MBPRK04", + # "MBCONS01", + # "USDC", + # "WBX", + # "CHZ", + # "MBCONS02", + # "PAXG", + # "MBVASCO01", + # "LINK" + # ] + # + result = [] + amountLimits = self.safe_value(self.options, 'limits', {}) + for i in range(0, len(response)): + coin = response[i] + baseId = coin + quoteId = 'BRL' + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + id = quote + base + result.append({ + 'id': id, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': None, + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number('1e-8'), + 'price': self.parse_number('1e-5'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(amountLimits, baseId), + 'max': None, + }, + 'price': { + 'min': self.parse_number('1e-5'), + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + 'info': coin, + }) + return result + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'coin': market['base'], + } + response = await self.publicGetCoinOrderbook(self.extend(request, params)) + return self.parse_order_book(response, market['symbol']) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "high":"103.96000000", + # "low":"95.00000000", + # "vol":"2227.67806598", + # "last":"97.91591000", + # "buy":"95.52760000", + # "sell":"97.91475000", + # "open":"99.79955000", + # "date":1643382606 + # } + # + symbol = self.safe_symbol(None, market) + timestamp = self.safe_timestamp(ticker, 'date') + last = self.safe_string(ticker, 'last') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': self.safe_string(ticker, 'buy'), + 'bidVolume': None, + 'ask': self.safe_string(ticker, 'sell'), + 'askVolume': None, + 'vwap': None, + 'open': None, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': self.safe_string(ticker, 'vol'), + 'quoteVolume': None, + 'info': ticker, + }, market) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'coin': market['base'], + } + response = await self.publicGetCoinTicker(self.extend(request, params)) + ticker = self.safe_value(response, 'ticker', {}) + # + # { + # "ticker": { + # "high":"1549.82293000", + # "low":"1503.00011000", + # "vol":"81.82827101", + # "last":"1533.15000000", + # "buy":"1533.21018000", + # "sell":"1540.09000000", + # "open":"1524.71089000", + # "date":1643691671 + # } + # } + # + return self.parse_ticker(ticker, market) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + timestamp = self.safe_timestamp_2(trade, 'date', 'executed_timestamp') + market = self.safe_market(None, market) + id = self.safe_string_2(trade, 'tid', 'operation_id') + type = None + side = self.safe_string(trade, 'type') + price = self.safe_string(trade, 'price') + amount = self.safe_string_2(trade, 'amount', 'quantity') + feeCost = self.safe_string(trade, 'fee_rate') + fee = None + if feeCost is not None: + fee = { + 'cost': feeCost, + 'currency': None, + } + return self.safe_trade({ + 'id': id, + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'order': None, + 'type': type, + 'side': side, + 'takerOrMaker': None, + 'price': price, + 'amount': amount, + 'cost': None, + 'fee': fee, + }, market) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + method = 'publicGetCoinTrades' + request: dict = { + 'coin': market['base'], + } + if since is not None: + method += 'From' + request['from'] = self.parse_to_int(since / 1000) + to = self.safe_integer(params, 'to') + if to is not None: + method += 'To' + response = await getattr(self, method)(self.extend(request, params)) + return self.parse_trades(response, market, since, limit) + + def parse_balance(self, response) -> Balances: + data = self.safe_value(response, 'response_data', {}) + balances = self.safe_value(data, 'balance', {}) + result: dict = {'info': response} + currencyIds = list(balances.keys()) + for i in range(0, len(currencyIds)): + currencyId = currencyIds[i] + code = self.safe_currency_code(currencyId) + if currencyId in balances: + balance = self.safe_value(balances, currencyId, {}) + account = self.account() + account['free'] = self.safe_string(balance, 'available') + account['total'] = self.safe_string(balance, 'total') + result[code] = account + return self.safe_balance(result) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + response = await self.privatePostGetAccountInfo(params) + return self.parse_balance(response) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'coin_pair': market['id'], + } + method = self.capitalize(side) + 'Order' + if type == 'limit': + method = 'privatePostPlace' + method + request['limit_price'] = self.price_to_precision(market['symbol'], price) + request['quantity'] = self.amount_to_precision(market['symbol'], amount) + else: + method = 'privatePostPlaceMarket' + method + if side == 'buy': + if price is None: + raise InvalidOrder(self.id + ' createOrder() requires the price argument with market buy orders to calculate total order cost(amount to spend), where cost = amount * price. Supply a price argument to createOrder() call if you want the cost to be calculated for you from price and amount') + amountString = self.number_to_string(amount) + priceString = self.number_to_string(price) + cost = self.parse_to_numeric(Precise.string_mul(amountString, priceString)) + request['cost'] = self.price_to_precision(market['symbol'], cost) + else: + request['quantity'] = self.amount_to_precision(market['symbol'], amount) + response = await getattr(self, method)(self.extend(request, params)) + # TODO: replace self with a call to parseOrder for unification + return self.safe_order({ + 'info': response, + 'id': str(response['response_data']['order']['order_id']), + }, market) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'coin_pair': market['id'], + 'order_id': id, + } + response = await self.privatePostCancelOrder(self.extend(request, params)) + # + # { + # "response_data": { + # "order": { + # "order_id": 2176769, + # "coin_pair": "BRLBCH", + # "order_type": 2, + # "status": 3, + # "has_fills": False, + # "quantity": "0.10000000", + # "limit_price": "1996.15999", + # "executed_quantity": "0.00000000", + # "executed_price_avg": "0.00000", + # "fee": "0.00000000", + # "created_timestamp": "1536956488", + # "updated_timestamp": "1536956499", + # "operations": [] + # } + # }, + # "status_code": 100, + # "server_unix_timestamp": "1536956499" + # } + # + responseData = self.safe_value(response, 'response_data', {}) + order = self.safe_dict(responseData, 'order', {}) + return self.parse_order(order, market) + + def parse_order_status(self, status: Str): + statuses: dict = { + '2': 'open', + '3': 'canceled', + '4': 'closed', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # { + # "order_id": 4, + # "coin_pair": "BRLBTC", + # "order_type": 1, + # "status": 2, + # "has_fills": True, + # "quantity": "2.00000000", + # "limit_price": "900.00000", + # "executed_quantity": "1.00000000", + # "executed_price_avg": "900.00000", + # "fee": "0.00300000", + # "created_timestamp": "1453838494", + # "updated_timestamp": "1453838494", + # "operations": [ + # { + # "operation_id": 1, + # "quantity": "1.00000000", + # "price": "900.00000", + # "fee_rate": "0.30", + # "executed_timestamp": "1453838494", + # }, + # ], + # } + # + id = self.safe_string(order, 'order_id') + order_type = self.safe_string(order, 'order_type') + side = None + if 'order_type' in order: + side = 'buy' if (order_type == '1') else 'sell' + status = self.parse_order_status(self.safe_string(order, 'status')) + marketId = self.safe_string(order, 'coin_pair') + market = self.safe_market(marketId, market) + timestamp = self.safe_timestamp(order, 'created_timestamp') + fee = { + 'cost': self.safe_string(order, 'fee'), + 'currency': market['quote'], + } + price = self.safe_string(order, 'limit_price') + # price = self.safe_number(order, 'executed_price_avg', price) + average = self.safe_string(order, 'executed_price_avg') + amount = self.safe_string(order, 'quantity') + filled = self.safe_string(order, 'executed_quantity') + lastTradeTimestamp = self.safe_timestamp(order, 'updated_timestamp') + rawTrades = self.safe_value(order, 'operations', []) + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': lastTradeTimestamp, + 'symbol': market['symbol'], + 'type': 'limit', + 'timeInForce': None, + 'postOnly': None, + 'side': side, + 'price': price, + 'triggerPrice': None, + 'cost': None, + 'average': average, + 'amount': amount, + 'filled': filled, + 'remaining': None, + 'status': status, + 'fee': fee, + 'trades': rawTrades, + }, market) + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrder() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'coin_pair': market['id'], + 'order_id': int(id), + } + response = await self.privatePostGetOrder(self.extend(request, params)) + responseData = self.safe_value(response, 'response_data', {}) + order = self.safe_dict(responseData, 'order') + return self.parse_order(order, market) + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.check_address(address) + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'coin': currency['id'], + 'quantity': format(amount, '.10f'), + 'address': address, + } + if code == 'BRL': + account_ref = ('account_ref' in params) + if not account_ref: + raise ArgumentsRequired(self.id + ' withdraw() requires account_ref parameter to withdraw ' + code) + elif code != 'LTC': + tx_fee = ('tx_fee' in params) + if not tx_fee: + raise ArgumentsRequired(self.id + ' withdraw() requires tx_fee parameter to withdraw ' + code) + if code == 'XRP': + if tag is None: + if not ('destination_tag' in params): + raise ArgumentsRequired(self.id + ' withdraw() requires a tag argument or destination_tag parameter to withdraw ' + code) + else: + request['destination_tag'] = tag + response = await self.privatePostWithdrawCoin(self.extend(request, params)) + # + # { + # "response_data": { + # "withdrawal": { + # "id": 1, + # "coin": "BRL", + # "quantity": "300.56", + # "net_quantity": "291.68", + # "fee": "8.88", + # "account": "bco: 341, ag: 1111, cta: 23456-X", + # "status": 1, + # "created_timestamp": "1453912088", + # "updated_timestamp": "1453912088" + # } + # }, + # "status_code": 100, + # "server_unix_timestamp": "1453912088" + # } + # + responseData = self.safe_value(response, 'response_data', {}) + withdrawal = self.safe_dict(responseData, 'withdrawal') + return self.parse_transaction(withdrawal, currency) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # { + # "id": 1, + # "coin": "BRL", + # "quantity": "300.56", + # "net_quantity": "291.68", + # "fee": "8.88", + # "account": "bco: 341, ag: 1111, cta: 23456-X", + # "status": 1, + # "created_timestamp": "1453912088", + # "updated_timestamp": "1453912088" + # } + # + currency = self.safe_currency(None, currency) + return { + 'id': self.safe_string(transaction, 'id'), + 'txid': None, + 'timestamp': None, + 'datetime': None, + 'network': None, + 'addressFrom': None, + 'address': None, + 'addressTo': None, + 'amount': None, + 'type': None, + 'currency': currency['code'], + 'status': None, + 'updated': None, + 'tagFrom': None, + 'tag': None, + 'tagTo': None, + 'comment': None, + 'internal': None, + 'fee': None, + 'info': transaction, + } + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + return [ + self.safe_integer(ohlcv, 0), + self.safe_number(ohlcv, 1), + self.safe_number(ohlcv, 2), + self.safe_number(ohlcv, 3), + self.safe_number(ohlcv, 4), + self.safe_number(ohlcv, 5), + ] + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '15m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'resolution': self.safe_string(self.timeframes, timeframe, timeframe), + 'symbol': market['base'] + '-' + market['quote'], # exceptional endpoint, that needs custom symbol syntax + } + if limit is None: + limit = 100 # set some default limit,'s required if user doesn't provide it + if since is not None: + request['from'] = self.parse_to_int(since / 1000) + request['to'] = self.sum(request['from'], limit * self.parse_timeframe(timeframe)) + else: + request['to'] = self.seconds() + request['from'] = request['to'] - (limit * self.parse_timeframe(timeframe)) + response = await self.v4PublicNetGetCandles(self.extend(request, params)) + candles = self.convert_trading_view_to_ohlcv(response, 't', 'o', 'h', 'l', 'c', 'v') + return self.parse_ohlcvs(candles, market, timeframe, since, limit) + + async def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrders() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'coin_pair': market['id'], + } + response = await self.privatePostListOrders(self.extend(request, params)) + responseData = self.safe_value(response, 'response_data', {}) + orders = self.safe_list(responseData, 'orders', []) + return self.parse_orders(orders, market, since, limit) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOpenOrders() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'coin_pair': market['id'], + 'status_list': '[2]', # open only + } + response = await self.privatePostListOrders(self.extend(request, params)) + responseData = self.safe_value(response, 'response_data', {}) + orders = self.safe_list(responseData, 'orders', []) + return self.parse_orders(orders, market, since, limit) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMyTrades() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'coin_pair': market['id'], + 'has_fills': True, + } + response = await self.privatePostListOrders(self.extend(request, params)) + responseData = self.safe_value(response, 'response_data', {}) + ordersRaw = self.safe_value(responseData, 'orders', []) + orders = self.parse_orders(ordersRaw, market, since, limit) + trades = self.orders_to_trades(orders) + return self.filter_by_symbol_since_limit(trades, market['symbol'], since, limit) + + def orders_to_trades(self, orders): + result = [] + for i in range(0, len(orders)): + trades = self.safe_value(orders[i], 'trades', []) + for y in range(0, len(trades)): + result.append(trades[y]) + return result + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = self.urls['api'][api] + '/' + query = self.omit(params, self.extract_params(path)) + if (api == 'public') or (api == 'v4Public') or (api == 'v4PublicNet'): + url += self.implode_params(path, params) + if query: + url += '?' + self.urlencode(query) + else: + self.check_required_credentials() + url += self.version + '/' + nonce = self.nonce() + body = self.urlencode(self.extend({ + 'tapi_method': path, + 'tapi_nonce': nonce, + }, params)) + auth = '/tapi/' + self.version + '/' + '?' + body + headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + 'TAPI-ID': self.apiKey, + 'TAPI-MAC': self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha512), + } + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None + # + # todo add a unified standard handleErrors with self.exceptions in describe() + # + # {"status":503,"message":"Maintenancing, try again later","result":null} + # + errorMessage = self.safe_value(response, 'error_message') + if errorMessage is not None: + raise ExchangeError(self.id + ' ' + self.json(response)) + return None diff --git a/ccxt/async_support/mexc.py b/ccxt/async_support/mexc.py new file mode 100644 index 0000000..ca5b291 --- /dev/null +++ b/ccxt/async_support/mexc.py @@ -0,0 +1,5877 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.mexc import ImplicitAPI +import asyncio +import hashlib +from ccxt.base.types import Account, Any, Balances, Currencies, Currency, DepositAddress, IndexType, Int, Leverage, LeverageTier, LeverageTiers, MarginModification, Market, Num, Order, OrderBook, OrderRequest, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, Trade, TradingFeeInterface, Transaction, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import AccountSuspended +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidAddress +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import NotSupported +from ccxt.base.errors import OnMaintenance +from ccxt.base.errors import InvalidNonce +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class mexc(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(mexc, self).describe(), { + 'id': 'mexc', + 'name': 'MEXC Global', + 'countries': ['SC'], # Seychelles + 'rateLimit': 50, # default rate limit is 20 times per second + 'version': 'v3', + 'certified': True, + 'pro': True, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': True, + 'swap': True, + 'future': False, + 'option': False, + 'addMargin': True, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'cancelOrders': None, + 'closeAllPositions': False, + 'closePosition': False, + 'createDepositAddress': True, + 'createMarketBuyOrderWithCost': True, + 'createMarketOrderWithCost': True, + 'createMarketSellOrderWithCost': True, + 'createOrder': True, + 'createOrders': True, + 'createPostOnlyOrder': True, + 'createReduceOnlyOrder': True, + 'createStopLimitOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'createTriggerOrder': True, + 'deposit': None, + 'editOrder': None, + 'fetchAccounts': True, + 'fetchBalance': True, + 'fetchBidsAsks': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchCanceledOrders': True, + 'fetchClosedOrder': None, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDeposit': None, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': None, + 'fetchDepositAddressesByNetwork': True, + 'fetchDeposits': True, + 'fetchDepositWithdrawFee': 'emulated', + 'fetchDepositWithdrawFees': True, + 'fetchFundingHistory': True, + 'fetchFundingInterval': True, + 'fetchFundingIntervals': False, + 'fetchFundingRate': True, + 'fetchFundingRateHistory': True, + 'fetchFundingRates': False, + 'fetchIndexOHLCV': True, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchL2OrderBook': True, + 'fetchLedger': None, + 'fetchLedgerEntry': None, + 'fetchLeverage': True, + 'fetchLeverages': False, + 'fetchLeverageTiers': True, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarketLeverageTiers': 'emulated', + 'fetchMarkets': True, + 'fetchMarkOHLCV': True, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenOrder': None, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrderBooks': None, + 'fetchOrders': True, + 'fetchOrderTrades': True, + 'fetchPosition': 'emulated', + 'fetchPositionHistory': 'emulated', + 'fetchPositionMode': True, + 'fetchPositions': True, + 'fetchPositionsHistory': True, + 'fetchPositionsRisk': None, + 'fetchPremiumIndexOHLCV': False, + 'fetchStatus': True, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': True, + 'fetchTradingFees': False, + 'fetchTradingLimits': None, + 'fetchTransactionFee': 'emulated', + 'fetchTransactionFees': True, + 'fetchTransactions': None, + 'fetchTransfer': True, + 'fetchTransfers': True, + 'fetchWithdrawal': None, + 'fetchWithdrawals': True, + 'reduceMargin': True, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'setLeverage': True, + 'setMarginMode': True, + 'setPositionMode': True, + 'signIn': None, + 'transfer': None, + 'withdraw': True, + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/137283979-8b2a818d-8633-461b-bfca-de89e8c446b2.jpg', + 'api': { + 'spot': { + 'public': 'https://api.mexc.com', + 'private': 'https://api.mexc.com', + }, + 'spot2': { + 'public': 'https://www.mexc.com/open/api/v2', + 'private': 'https://www.mexc.com/open/api/v2', + }, + 'contract': { + 'public': 'https://contract.mexc.com/api/v1/contract', + 'private': 'https://contract.mexc.com/api/v1/private', + }, + 'broker': { + 'private': 'https://api.mexc.com/api/v3/broker', + }, + }, + 'www': 'https://www.mexc.com/', + 'doc': [ + 'https://mexcdevelop.github.io/apidocs/', + ], + 'fees': [ + 'https://www.mexc.com/fee', + ], + 'referral': 'https://www.mexc.com/register?inviteCode=mexc-1FQ1GNu1', + }, + 'api': { + 'spot': { + 'public': { + 'get': { + 'ping': 1, + 'time': 1, + 'exchangeInfo': 10, + 'depth': 1, + 'trades': 5, + 'historicalTrades': 1, + 'aggTrades': 1, + 'klines': 1, + 'avgPrice': 1, + 'ticker/24hr': 1, + 'ticker/price': 1, + 'ticker/bookTicker': 1, + 'etf/info': 1, + }, + }, + 'private': { + 'get': { + 'order': 2, + 'openOrders': 3, + 'allOrders': 10, + 'account': 10, + 'myTrades': 10, + 'tradeFee': 10, + 'sub-account/list': 1, + 'sub-account/apiKey': 1, + 'capital/config/getall': 10, + 'capital/deposit/hisrec': 1, + 'capital/withdraw/history': 1, + 'capital/withdraw/address': 10, + 'capital/deposit/address': 10, + 'capital/transfer': 1, + 'capital/transfer/tranId': 1, + 'capital/transfer/internal': 1, + 'capital/sub-account/universalTransfer': 1, + 'capital/convert': 1, + 'capital/convert/list': 1, + 'margin/loan': 1, + 'margin/allOrders': 1, + 'margin/myTrades': 1, + 'margin/openOrders': 1, + 'margin/maxTransferable': 1, + 'margin/priceIndex': 1, + 'margin/order': 1, + 'margin/isolated/account': 1, + 'margin/maxBorrowable': 1, + 'margin/repay': 1, + 'margin/isolated/pair': 1, + 'margin/forceLiquidationRec': 1, + 'margin/isolatedMarginData': 1, + 'margin/isolatedMarginTier': 1, + 'rebate/taxQuery': 1, + 'rebate/detail': 1, + 'rebate/detail/kickback': 1, + 'rebate/referCode': 1, + 'rebate/affiliate/commission': 1, + 'rebate/affiliate/withdraw': 1, + 'rebate/affiliate/commission/detail': 1, + 'mxDeduct/enable': 1, + 'userDataStream': 1, + 'selfSymbols': 1, + 'asset/internal/transfer/record': 10, + }, + 'post': { + 'order': 1, + 'order/test': 1, + 'sub-account/virtualSubAccount': 1, + 'sub-account/apiKey': 1, + 'sub-account/futures': 1, + 'sub-account/margin': 1, + 'batchOrders': 10, + 'capital/withdraw/apply': 1, + 'capital/withdraw': 1, + 'capital/transfer': 1, + 'capital/transfer/internal': 1, + 'capital/deposit/address': 1, + 'capital/sub-account/universalTransfer': 1, + 'capital/convert': 10, + 'mxDeduct/enable': 1, + 'userDataStream': 1, + }, + 'put': { + 'userDataStream': 1, + }, + 'delete': { + 'order': 1, + 'openOrders': 1, + 'sub-account/apiKey': 1, + 'margin/order': 1, + 'margin/openOrders': 1, + 'userDataStream': 1, + 'capital/withdraw': 1, + }, + }, + }, + 'contract': { + 'public': { + 'get': { + 'ping': 2, + 'detail': 100, + 'support_currencies': 2, + 'depth/{symbol}': 2, + 'depth_commits/{symbol}/{limit}': 2, + 'index_price/{symbol}': 2, + 'fair_price/{symbol}': 2, + 'funding_rate/{symbol}': 2, + 'kline/{symbol}': 2, + 'kline/index_price/{symbol}': 2, + 'kline/fair_price/{symbol}': 2, + 'deals/{symbol}': 2, + 'ticker': 2, + 'risk_reverse': 2, + 'risk_reverse/history': 2, + 'funding_rate/history': 2, + }, + }, + 'private': { + 'get': { + 'account/assets': 2, + 'account/asset/{currency}': 2, + 'account/transfer_record': 2, + 'position/list/history_positions': 2, + 'position/open_positions': 2, + 'position/funding_records': 2, + 'position/position_mode': 2, + 'order/list/open_orders/{symbol}': 2, + 'order/list/history_orders': 2, + 'order/external/{symbol}/{external_oid}': 2, + 'order/get/{order_id}': 2, + 'order/batch_query': 8, + 'order/deal_details/{order_id}': 2, + 'order/list/order_deals': 2, + 'planorder/list/orders': 2, + 'stoporder/list/orders': 2, + 'stoporder/order_details/{stop_order_id}': 2, + 'account/risk_limit': 2, # TO_DO: gets max/min position size, allowed sides, leverage, maintenance margin, initial margin, etc... + 'account/tiered_fee_rate': 2, # TO_DO: taker/maker fees for account + 'position/leverage': 2, + }, + 'post': { + 'position/change_margin': 2, + 'position/change_leverage': 2, + 'position/change_position_mode': 2, + 'order/submit': 2, + 'order/submit_batch': 40, + 'order/cancel': 2, + 'order/cancel_with_external': 2, + 'order/cancel_all': 2, + 'account/change_risk_level': 2, + 'planorder/place': 2, + 'planorder/cancel': 2, + 'planorder/cancel_all': 2, + 'stoporder/cancel': 2, + 'stoporder/cancel_all': 2, + 'stoporder/change_price': 2, + 'stoporder/change_plan_price': 2, + }, + }, + }, + 'spot2': { + 'public': { + 'get': { + 'market/symbols': 1, + 'market/coin/list': 2, + 'common/timestamp': 1, + 'common/ping': 2, + 'market/ticker': 1, + 'market/depth': 1, + 'market/deals': 1, + 'market/kline': 1, + 'market/api_default_symbols': 2, + }, + }, + 'private': { + 'get': { + 'account/info': 1, + 'order/open_orders': 1, + 'order/list': 1, + 'order/query': 1, + 'order/deals': 1, + 'order/deal_detail': 1, + 'asset/deposit/address/list': 2, + 'asset/deposit/list': 2, + 'asset/address/list': 2, + 'asset/withdraw/list': 2, + 'asset/internal/transfer/record': 10, + 'account/balance': 10, + 'asset/internal/transfer/info': 10, + 'market/api_symbols': 2, + }, + 'post': { + 'order/place': 1, + 'order/place_batch': 1, + 'order/advanced/place_batch': 1, + 'asset/withdraw': 2, + 'asset/internal/transfer': 10, + }, + 'delete': { + 'order/cancel': 1, + 'order/cancel_by_symbol': 1, + 'asset/withdraw': 2, + }, + }, + }, + 'broker': { + 'private': { + 'get': { + 'sub-account/universalTransfer': 1, + 'sub-account/list': 1, + 'sub-account/apiKey': 1, + 'capital/deposit/subAddress': 1, + 'capital/deposit/subHisrec': 1, + 'capital/deposit/subHisrec/getall': 1, + }, + 'post': { + 'sub-account/virtualSubAccount': 1, + 'sub-account/apiKey': 1, + 'capital/deposit/subAddress': 1, + 'capital/withdraw/apply': 1, + 'sub-account/universalTransfer': 1, + 'sub-account/futures': 1, + }, + 'delete': { + 'sub-account/apiKey': 1, + }, + }, + }, + }, + 'precisionMode': TICK_SIZE, + 'timeframes': { + '1m': '1m', # spot, swap + '5m': '5m', # spot, swap + '15m': '15m', # spot, swap + '30m': '30m', # spot, swap + '1h': '1h', # spot, swap + '4h': '4h', # spot, swap + '8h': '8h', # swap + '1d': '1d', # spot, swap + '1w': '1w', # swap + '1M': '1M', # spot, swap + }, + 'fees': { + 'trading': { + 'tierBased': False, + 'percentage': True, + 'maker': self.parse_number('0.002'), # maker / taker + 'taker': self.parse_number('0.002'), + }, + }, + 'options': { + 'adjustForTimeDifference': False, + 'timeDifference': 0, + 'unavailableContracts': { + 'BTC/USDT:USDT': True, + 'LTC/USDT:USDT': True, + 'ETH/USDT:USDT': True, + }, + 'fetchMarkets': { + 'types': { + 'spot': True, + 'swap': { + 'linear': True, + 'inverse': False, + }, + }, + }, + 'useCcxtTradeId': True, + 'timeframes': { + 'spot': { + '1m': '1m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '60m', + '4h': '4h', + '1d': '1d', + '1w': '1W', + '1M': '1M', + }, + 'swap': { + '1m': 'Min1', + '5m': 'Min5', + '15m': 'Min15', + '30m': 'Min30', + '1h': 'Min60', + '4h': 'Hour4', + '8h': 'Hour8', + '1d': 'Day1', + '1w': 'Week1', + '1M': 'Month1', + }, + }, + 'defaultType': 'spot', # spot, swap + 'defaultNetwork': 'ETH', + 'defaultNetworks': { + 'ETH': 'ETH', + 'USDT': 'ERC20', + 'USDC': 'ERC20', + 'BTC': 'BTC', + 'LTC': 'LTC', + }, + 'networks': { + 'ZKSYNC': 'ZKSYNCERA', + 'TRC20': 'TRX', + 'TON': 'TONCOIN', + 'ARBITRUM': 'ARB', + 'STX': 'STACKS', + 'LUNC': 'LUNA', + 'STARK': 'STARKNET', + 'APT': 'APTOS', + 'PEAQ': 'PEAQEVM', + 'AVAXC': 'AVAX_CCHAIN', + 'ERC20': 'ETH', + 'ACA': 'ACALA', + 'BEP20': 'BSC', + 'OPTIMISM': 'OP', + # 'ADA': 'Cardano(ADA)', + # 'AE': 'AE', + # 'ALGO': 'Algorand(ALGO)', + # 'ALPH': 'Alephium(ALPH)', + # 'ARB': 'Arbitrum One(ARB)', + # 'ARBONE': 'ArbitrumOne(ARB)', + 'ASTR': 'ASTAR', # ASTAREVM is different + # 'ATOM': 'Cosmos(ATOM)', + # 'AVAXC': 'Avalanche C Chain(AVAX CCHAIN)', + # 'AVAXX': 'Avalanche X Chain(AVAX XCHAIN)', + # 'AZERO': 'Aleph Zero(AZERO)', + # 'BCH': 'Bitcoin Cash(BCH)', + # 'BNCDOT': 'BNCPOLKA', + # 'BSV': 'Bitcoin SV(BSV)', + # 'BTC': 'Bitcoin(BTC)', + 'BTM': 'BTM2', + # 'CHZ': 'Chiliz Legacy Chain(CHZ)', + # 'CHZ2': 'Chiliz Chain(CHZ2)', + # 'CLORE': 'Clore.ai(CLORE)', + 'CRC20': 'CRONOS', + # 'DC': 'Dogechain(DC)', + # 'DNX': 'Dynex(DNX)', + # 'DOGE': 'Dogecoin(DOGE)', + # 'DOT': 'Polkadot(DOT)', + 'DOT': 'DOTASSETHUB', + # 'DYM': 'Dymension(DYM)', + 'ETHF': 'ETF', + 'HRC20': 'HECO', + # 'KLAY': 'Klaytn(KLAY)', + 'OASIS': 'ROSE', + 'OKC': 'OKT', + 'RSK': 'RBTC', + # 'RVN': 'Ravencoin(RVN)', + # 'SATOX': 'Satoxcoin(SATOX)', + # 'SC': 'SC', + # 'SCRT': 'SCRT', + # 'SDN': 'SDN', + # 'SGB': 'SGB', + # 'SOL': 'Solana(SOL)', + # 'STAR': 'STAR', + # 'STARK': 'Starknet(STARK)', + # 'STEEM': 'STEEM', + # 'SYS': 'SYS', + # 'TAO': 'Bittensor(TAO)', + # 'TIA': 'Celestia(TIA)', + # 'TOMO': 'TOMO', + # 'TON': 'Toncoin(TON)', + # 'TRC10': 'TRC10', + # 'TRC20': 'Tron(TRC20)', + # 'UGAS': 'UGAS(Ultrain)', + # 'VET': 'VeChain(VET)', + # 'VEX': 'Vexanium(VEX)', + # 'VSYS': 'VSYS', + # 'WAVES': 'WAVES', + # 'WAX': 'WAX', + # 'WEMIX': 'WEMIX', + # 'XCH': 'Chia(XCH)', + # 'XDC': 'XDC', + # 'XEC': 'XEC', + # 'XLM': 'Stellar(XLM)', + # 'XMR': 'Monero(XMR)', + # 'XNA': 'Neurai(XNA)', + # 'XPR': 'XPR Network', + # 'XRD': 'XRD', + # 'XRP': 'Ripple(XRP)', + # 'XTZ': 'XTZ', + # 'XVG': 'XVG', + # 'XYM': 'XYM', + # 'ZEC': 'ZEC', + # 'ZEN': 'ZEN', + # 'ZIL': 'Zilliqa(ZIL)', + # 'ZTG': 'ZTG', + # todo: uncomment below after concensus + # 'ALAYA': 'ATP', + # 'ANDUSCHAIN': 'DEB', + # 'ASSETMANTLE': 'MNTL', + # 'AXE': 'AXE', + # 'BITCOINHD': 'BHD', + # 'BITCOINVAULT': 'BTCV', + # 'BITKUB': 'KUB', + # 'BITSHARES_OLD': 'BTS', + # 'BITSHARES': 'NBS', + # 'BYTZ': 'BYTZ', + # 'CANTO': 'CANTO', # CANTOEVM + # 'CENNZ': 'CENNZ', + # 'CHAINX': 'PCX', + # 'CONCODRIUM': 'CCD', + # 'CONTENTVALUENETWORK': 'CVNT', + # 'CORTEX': 'CTXC', + # 'CYPHERIUM': 'CPH', + # 'DANGNN': 'DGC', + # 'DARWINIASMARTCHAIN': 'Darwinia Smart Chain', + # 'DHEALTH': 'DHP', + # 'DOGECOIN': ['DOGE', 'DOGECHAIN'], # todo after unification + # 'DRAC': 'DRAC', + # 'DRAKEN': 'DRK', + # 'ECOCHAIN': 'ECOC', + # 'ELECTRAPROTOCOL': 'XEP', + # 'EMERALD': 'EMERALD', # sits on top of OASIS + # 'EVMOS': 'EVMOS', # EVMOSETH is different + # 'EXOSAMA': 'SAMA', + # 'FIBOS': 'FO', + # 'FILECASH': 'FIC', + # 'FIRMACHAIN': 'FCT', + # 'FIRO': 'XZC', + # 'FNCY': 'FNCY', + # 'FRUITS': 'FRTS', + # 'GLEEC': 'GLEEC', + # 'GXCHAIN': 'GXC', + # 'HANDSHAKE': 'HNS', + # 'HPB': 'HPB', + # 'HSHARE': 'HC', + # 'HUAHUA': 'HUAHUA', + # 'HUPAYX': 'HPX', + # 'INDEXCHAIN': 'IDX', + # 'INTCHAIN': 'INT', + # 'INTEGRITEE': 'TEER', + # 'INTERLAY': 'INTR', + # 'IOEX': 'IOEX', + # 'JUNO': 'JUNO', + # 'KASPA': 'KASPA', + # 'KEKCHAIN': 'KEKCHAIN', + # 'KINTSUGI': 'KINT', + # 'KOINOS': 'KOINOS', + # 'KONSTELLATION': 'DARC', + # 'KUJIRA': 'KUJI', + # 'KULUPU': 'KLP', + # 'LBRY': 'LBC', + # 'LEDGIS': 'LED', + # 'LIGHTNINGBITCOIN': 'LBTC', + # 'LINE': 'LINE', + # 'MDNA': 'DNA', + # 'MDUKEY': 'MDU', + # 'METAMUI': 'MMUI', + # 'METAVERSE_ETP': 'ETP', + # 'METER': 'MTRG', + # 'MEVERSE': 'MEVerse', + # 'NEWTON': 'NEW', + # 'NODLE': 'NODLE', + # 'ORIGYN': 'OGY', + # 'PAC': 'PAC', + # 'PASTEL': 'PSL', + # 'PHALA': 'Khala', + # 'PLEX': 'PLEX', + # 'PMG': 'PMG', + # 'POINT': 'POINT', # POINTEVM is different + # 'PROOFOFMEMES': 'POM', + # 'PROXIMAX': 'XPX', + # 'RCHAIN': 'REV', + # 'REBUS': 'REBUS', # REBUSEVM is different + # 'RIZON': 'ATOLO', + # 'SENTINEL': 'DVPN', + # 'SERO': 'SERO', + # 'TECHPAY': 'TPC', + # 'TELOSCOIN': 'TLOS', # todo + # 'TERRA': 'LUNA2', + # 'TERRACLASSIC': 'LUNC', + # 'TLOS': 'TELOS', # todo + # 'TOMAINFO': 'TON', + # 'TONGTONG': 'TTC', + # 'TURTLECOIN': 'TRTL', + # 'ULORD': 'UT', + # 'ULTRAIN': 'UGAS', + # 'UMEE': 'UMEE', + # 'VDIMENSION': 'VOLLAR', + # 'VEXANIUM': 'VEX', + # 'VNT': 'VNT', + # 'WAYKICHAIN': 'WICC', + # 'WHITECOIN': 'XWC', + # 'WITNET': 'WIT', + # 'XDAI': 'XDAI', + # 'XX': 'XX', + # 'YAS': 'YAS', + # 'ZENITH': 'ZENITH', + # 'ZKSYNC': 'ZKSYNC', + # # 'BAJUN': '', + # OKB <> OKT(for usdt it's exception) for OKC, PMEER, FLARE, STRD, ZEL, FUND, "NONE", CRING, FREETON, QTZ (probably unique network is meant), HT, BSC(RACAV1), BSC(RACAV2), AMBROSUS, BAJUN, NOM. their individual info is at https://www.mexc.com/api/platform/asset/spot/{COINNAME} + }, + 'networksById': { + 'BNB Smart Chain(BEP20-RACAV1)': 'BSC', + 'BNB Smart Chain(BEP20-RACAV2)': 'BSC', + 'BNB Smart Chain(BEP20)': 'BSC', + 'Ethereum(ERC20)': 'ERC20', + # TODO: uncomment below after deciding unified name + # 'PEPE COIN BSC': + # 'SMART BLOCKCHAIN': + # 'f(x)Core': + # 'Syscoin Rollux': + # 'Syscoin UTXO': + # 'zkSync Era': + # 'zkSync Lite': + # 'Darwinia Smart Chain': + # 'Arbitrum One(ARB-Bridged)': + # 'Optimism(OP-Bridged)': + # 'Polygon(MATIC-Bridged)': + }, + 'recvWindow': 5 * 1000, # 5 sec, default + 'maxTimeTillEnd': 90 * 86400 * 1000 - 1, # 90 days + 'broker': 'CCXT', + }, + 'features': { + 'default': { + 'sandbox': False, + 'createOrder': { + 'marginMode': True, + 'triggerPrice': False, + 'triggerDirection': False, + 'triggerPriceType': { + 'last': False, + 'mark': False, + 'index': False, + }, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': True, # todo implement + 'trailing': False, + 'leverage': True, # todo implement + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': { + 'max': 20, + }, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 30, + 'untilDays': None, + 'symbolRequired': True, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOpenOrders': { + 'marginMode': True, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOrders': { + 'marginMode': True, + 'limit': 1000, + 'daysBack': 7, + 'untilDays': 7, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchClosedOrders': { + 'marginMode': True, + 'limit': 1000, + 'daysBack': 7, + 'daysBackCanceled': 7, + 'untilDays': 7, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOHLCV': { + 'limit': 1000, + }, + }, + 'spot': { + 'extends': 'default', + 'fetchCurrencies': { + 'private': True, + }, + }, + 'forDerivs': { + 'extends': 'default', + 'createOrder': { + 'triggerPrice': True, + 'triggerPriceType': { + 'last': True, + 'mark': True, + 'index': True, + }, + 'triggerDirection': True, # todo + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'hedged': True, + 'leverage': True, # todo + 'marketBuyByCost': False, + }, + 'createOrders': None, # todo: needs implementation https://mexcdevelop.github.io/apidocs/contract_v1_en/#order-under-maintenance:~:text=Order%20the%20contract%20in%20batch + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 90, + 'untilDays': 90, + }, + 'fetchOrder': { + 'marginMode': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 100, + 'trigger': True, + 'trailing': False, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 90, + 'untilDays': 90, + 'trigger': True, + 'trailing': False, + }, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 90, + 'daysBackCanceled': None, + 'untilDays': 90, + 'trigger': True, + 'trailing': False, + }, + 'fetchOHLCV': { + 'limit': 2000, + }, + }, + 'swap': { + 'linear': { + 'extends': 'forDerivs', + }, + 'inverse': { + 'extends': 'forDerivs', + }, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'commonCurrencies': { + 'BEYONDPROTOCOL': 'BEYOND', + 'BIFI': 'BIFIF', + 'BYN': 'BEYONDFI', + 'COFI': 'COFIX', # conflict with CoinFi + 'DFI': 'DFISTARTER', + 'DFT': 'DFUTURE', + 'DRK': 'DRK', + 'EGC': 'EGORASCREDIT', + 'FLUX1': 'FLUX', # switched places + 'FLUX': 'FLUX1', # switched places + 'FREE': 'FREEROSSDAO', # conflict with FREE Coin + 'GAS': 'GASDAO', + 'GASNEO': 'GAS', + 'GMT': 'GMTTOKEN', # Conflict with GMT(STEPN) + 'STEPN': 'GMT', # Conflict with GMT Token + 'HERO': 'STEPHERO', # conflict with Metahero + 'MIMO': 'MIMOSA', + 'PROS': 'PROSFINANCE', # conflict with Prosper + 'SIN': 'SINCITYTOKEN', + 'SOUL': 'SOULSWAP', + 'XBT': 'XBT', # restore original mapping + }, + 'exceptions': { + 'exact': { + # until mexc migrates fully to v3, it might be worth to note the version & market aside errors, not easily remove obsolete version's exceptions in future + '-1128': BadRequest, + '-2011': BadRequest, + '-1121': BadSymbol, + '10101': InsufficientFunds, # {"msg":"资金不足","code":10101} + '2009': InvalidOrder, # {"success":false,"code":2009,"message":"Position is not exists or closed."} + '2011': BadRequest, + '30004': InsufficientFunds, + '33333': BadRequest, # {"msg":"Not support transfer","code":33333} + '44444': BadRequest, + '1002': InvalidOrder, + '30019': BadRequest, + '30005': InvalidOrder, + '2003': InvalidOrder, + '2005': InsufficientFunds, + '400': BadRequest, # {"msg":"The start time cannot be earlier than 90 days","code":400} + # '500': OnMaintenance, # {"code": 500,"message": "Under maintenance, please try again later","announcement": "https://www.mexc.com/support/articles/17827791510263"} + '600': BadRequest, + '70011': PermissionDenied, # {"code":70011,"msg":"Pair user ban trade apikey."} + '88004': InsufficientFunds, # {"msg":"超出最大可借,最大可借币为:18.09833211","code":88004} + '88009': ExchangeError, # v3 {"msg":"Loan record does not exist","code":88009} + '88013': InvalidOrder, # {"msg":"最小交易额不能小于:5USDT","code":88013} + '88015': InsufficientFunds, # {"msg":"持仓不足","code":88015} + '700003': InvalidNonce, # {"code":700003,"msg":"Timestamp for self request is outside of the recvWindow."} + '26': ExchangeError, # operation not allowed + '602': AuthenticationError, # Signature verification failed + '10001': AuthenticationError, # user does not exist + '10007': BadSymbol, # {"code":10007,"msg":"bad symbol"} + '10015': BadRequest, # user id cannot be null + '10072': BadRequest, # invalid access key + '10073': BadRequest, # invalid Request-Time + '10095': InvalidOrder, # amount cannot be null + '10096': InvalidOrder, # amount decimal places is too long + '10097': InvalidOrder, # amount is error + '10098': InvalidOrder, # risk control system detected abnormal + '10099': BadRequest, # user sub account does not open + '10100': BadRequest, # self currency transfer is not supported + '10102': InvalidOrder, # amount cannot be zero or negative + '10103': ExchangeError, # self account transfer is not supported + '10200': BadRequest, # transfer operation processing + '10201': BadRequest, # transfer in failed + '10202': BadRequest, # transfer out failed + '10206': BadRequest, # transfer is disabled + '10211': BadRequest, # transfer is forbidden + '10212': BadRequest, # This withdrawal address is not on the commonly used address list or has been invalidated + '10216': ExchangeError, # no address available. Please try again later + '10219': ExchangeError, # asset flow writing failed please try again + '10222': BadRequest, # currency cannot be null + '10232': BadRequest, # currency does not exist + '10259': ExchangeError, # Intermediate account does not configured in redisredis + '10265': ExchangeError, # Due to risk control, withdrawal is unavailable, please try again later + '10268': BadRequest, # remark length is too long + '20001': ExchangeError, # subsystem is not supported + '20002': ExchangeError, # Internal system error please contact support + '22222': BadRequest, # record does not exist + '30000': ExchangeError, # suspended transaction for the symbol + '30001': InvalidOrder, # The current transaction direction is not allowed to place an order + '30002': InvalidOrder, # The minimum transaction volume cannot be less than : + '30003': InvalidOrder, # The maximum transaction volume cannot be greater than : + '30010': InvalidOrder, # no valid trade price + '30014': InvalidOrder, # invalid symbol + '30016': InvalidOrder, # trading disabled + '30018': AccountSuspended, # {"msg":"账号暂时不能下单,请联系客服","code":30018} + '30020': AuthenticationError, # no permission for the symbol + '30021': BadRequest, # invalid symbol + '30025': InvalidOrder, # no exist opponent order + '30026': BadRequest, # invalid order ids + '30027': InvalidOrder, # The currency has reached the maximum position limit, the buying is suspended + '30028': InvalidOrder, # The currency triggered the platform risk control, the selling is suspended + '30029': InvalidOrder, # Cannot exceed the maximum order limit + '30032': InvalidOrder, # Cannot exceed the maximum position + '30041': InvalidOrder, # current order type can not place order + '30087': InvalidOrder, # {"msg":"Order price exceeds allowed range","code":30087} + '60005': ExchangeError, # your account is abnormal + '700001': AuthenticationError, # {"code":700002,"msg":"Signature for self request is not valid."} # same message for expired API keys + '700002': AuthenticationError, # Signature for self request is not valid # or the API secret is incorrect + '700004': BadRequest, # Param 'origClientOrderId' or 'orderId' must be sent, but both were empty/null + '700005': InvalidNonce, # recvWindow must less than 60000 + '700006': BadRequest, # IP non white list + '700007': AuthenticationError, # No permission to access the endpoint + '700008': BadRequest, # Illegal characters found in parameter + '700013': AuthenticationError, # Invalid Content-Type v3 + '730001': BadRequest, # Pair not found + '730002': BadRequest, # Your input param is invalid + '730000': ExchangeError, # Request failed, please contact the customer service + '730003': ExchangeError, # Unsupported operation, please contact the customer service + '730100': ExchangeError, # Unusual user status + '730600': BadRequest, # Sub-account Name cannot be null + '730601': BadRequest, # Sub-account Name must be a combination of 8-32 letters and numbers + '730602': BadRequest, # Sub-account remarks cannot be null + '730700': BadRequest, # API KEY remarks cannot be null + '730701': BadRequest, # API KEY permission cannot be null + '730702': BadRequest, # API KEY permission does not exist + '730703': BadRequest, # The IP information is incorrect, and a maximum of 10 IPs are allowed to be bound only + '730704': BadRequest, # The bound IP format is incorrect, please refill + '730705': BadRequest, # At most 30 groups of Api Keys are allowed to be created only + '730706': BadRequest, # API KEY information does not exist + '730707': BadRequest, # accessKey cannot be null + '730101': BadRequest, # The user Name already exists + '140001': BadRequest, # sub account does not exist + '140002': AuthenticationError, # sub account is forbidden + }, + 'broad': { + 'Order quantity error, please try to modify.': BadRequest, # code:2011 + 'Combination of optional parameters invalid': BadRequest, # code:-2011 + 'api market order is disabled': BadRequest, # + 'Contract not allow place order!': InvalidOrder, # code:1002 + 'Oversold': InsufficientFunds, # code:30005 + 'Insufficient position': InsufficientFunds, # code:30004 + 'Insufficient balance!': InsufficientFunds, # code:2005 + 'Bid price is great than max allow price': InvalidOrder, # code:2003 + 'Invalid symbol.': BadSymbol, # code:-1121 + 'Param error!': BadRequest, # code:600 + 'maintenance': OnMaintenance, # {"code": 500,"message": "Under maintenance, please try again later","announcement": "https://www.mexc.com/support/articles/17827791510263"} + }, + }, + }) + + async def fetch_status(self, params={}): + """ + the latest known information on the availability of the exchange API + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#test-connectivity + https://mexcdevelop.github.io/apidocs/contract_v1_en/#get-the-server-time + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `status structure ` + """ + marketType, query = self.handle_market_type_and_params('fetchStatus', None, params) + response = None + status = None + updated = None + if marketType == 'spot': + response = await self.spotPublicGetPing(query) + # + # {} + # + keys = list(response.keys()) + length = len(keys) + status = self.json(response) if length else 'ok' + elif marketType == 'swap': + response = await self.contractPublicGetPing(query) + # + # {"success":true,"code":"0","data":"1648124374985"} + # + status = 'ok' if self.safe_value(response, 'success') else self.json(response) + updated = self.safe_integer(response, 'data') + return { + 'status': status, + 'updated': updated, + 'url': None, + 'eta': None, + 'info': response, + } + + async def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#check-server-time + https://mexcdevelop.github.io/apidocs/contract_v1_en/#get-the-server-time + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + marketType, query = self.handle_market_type_and_params('fetchTime', None, params) + response = None + if marketType == 'spot': + response = await self.spotPublicGetTime(query) + # + # {"serverTime": "1647519277579"} + # + return self.safe_integer(response, 'serverTime') + elif marketType == 'swap': + response = await self.contractPublicGetPing(query) + # + # {"success":true,"code":"0","data":"1648124374985"} + # + return self.safe_integer(response, 'data') + return None + + async def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#query-the-currency-information + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + # self endpoint requires authentication + # while fetchCurrencies is a public API method by design + # therefore we check the keys here + # and fallback to generating the currencies from the markets + if not self.check_required_credentials(False): + return {} + response = await self.spotPrivateGetCapitalConfigGetall(params) + # + # { + # "coin": "QANX", + # "name": "QANplatform", + # "networkList": [ + # { + # "coin": "QANX", + # "depositDesc": null, + # "depositEnable": True, + # "minConfirm": "0", + # "name": "QANplatform", + # "network": "BEP20(BSC)", + # "withdrawEnable": False, + # "withdrawFee": "42.000000000000000000", + # "withdrawIntegerMultiple": null, + # "withdrawMax": "24000000.000000000000000000", + # "withdrawMin": "20.000000000000000000", + # "sameAddress": False, + # "contract": "0xAAA7A10a8ee237ea61E8AC46C50A8Db8bCC1baaa" + # }, + # { + # "coin": "QANX", + # "depositDesc": null, + # "depositEnable": True, + # "minConfirm": "0", + # "name": "QANplatform", + # "network": "ERC20", + # "withdrawEnable": True, + # "withdrawFee": "2732.000000000000000000", + # "withdrawIntegerMultiple": null, + # "withdrawMax": "24000000.000000000000000000", + # "withdrawMin": "240.000000000000000000", + # "sameAddress": False, + # "contract": "0xAAA7A10a8ee237ea61E8AC46C50A8Db8bCC1baaa" + # } + # ] + # } + # + result: dict = {} + for i in range(0, len(response)): + currency = response[i] + id = self.safe_string(currency, 'coin') + code = self.safe_currency_code(id) + networks: dict = {} + chains = self.safe_value(currency, 'networkList', []) + for j in range(0, len(chains)): + chain = chains[j] + networkId = self.safe_string_2(chain, 'netWork', 'network') + network = self.network_id_to_code(networkId) + networks[network] = { + 'info': chain, + 'id': networkId, + 'network': network, + 'active': None, + 'deposit': self.safe_bool(chain, 'depositEnable', False), + 'withdraw': self.safe_bool(chain, 'withdrawEnable', False), + 'fee': self.safe_number(chain, 'withdrawFee'), + 'precision': None, + 'limits': { + 'withdraw': { + 'min': self.safe_string(chain, 'withdrawMin'), + 'max': self.safe_string(chain, 'withdrawMax'), + }, + }, + 'contract': self.safe_string(chain, 'contract'), + } + result[code] = self.safe_currency_structure({ + 'info': currency, + 'id': id, + 'code': code, + 'name': self.safe_string(currency, 'name'), + 'active': None, + 'deposit': None, + 'withdraw': None, + 'fee': None, + 'precision': None, + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + }, + 'type': 'crypto', + 'networks': networks, + }) + return result + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for mexc + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#exchange-information + https://mexcdevelop.github.io/apidocs/contract_v1_en/#get-the-contract-information + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + if self.options['adjustForTimeDifference']: + await self.load_time_difference() + spotMarketPromise = self.fetch_spot_markets(params) + swapMarketPromise = self.fetch_swap_markets(params) + spotMarket, swapMarket = await asyncio.gather(*[spotMarketPromise, swapMarketPromise]) + return self.array_concat(spotMarket, swapMarket) + + async def fetch_spot_markets(self, params={}): + """ + @ignore + retrieves data on all spot markets for mexc + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#exchange-information + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = await self.spotPublicGetExchangeInfo(params) + # + # { + # "timezone": "CST", + # "serverTime": 1647521860402, + # "rateLimits": [], + # "exchangeFilters": [], + # "symbols": [ + # { + # "symbol": "OGNUSDT", + # "status": "1", + # "baseAsset": "OGN", + # "baseAssetPrecision": "2", + # "quoteAsset": "USDT", + # "quoteAssetPrecision": "4", + # "orderTypes": [ + # "LIMIT", + # "LIMIT_MAKER" + # ], + # "baseCommissionPrecision": "2", + # "quoteCommissionPrecision": "4", + # "quoteOrderQtyMarketAllowed": False, + # "isSpotTradingAllowed": True, + # "isMarginTradingAllowed": True, + # "permissions": [ + # "SPOT", + # "MARGIN" + # ], + # "filters": [], + # "baseSizePrecision": "0.01", # self turned out to be a minimum base amount for order + # "maxQuoteAmount": "5000000", + # "makerCommission": "0.002", + # "takerCommission": "0.002" + # "quoteAmountPrecision": "5", # self turned out to be a minimum cost amount for order + # "quotePrecision": "4", # deprecated in favor of 'quoteAssetPrecision'( https://dev.binance.vision/t/what-is-the-difference-between-quoteprecision-and-quoteassetprecision/4333 ) + # # note, "icebergAllowed" & "ocoAllowed" fields were recently removed + # }, + # ] + # } + # + # Notes: + # - 'quoteAssetPrecision' & 'baseAssetPrecision' are not currency's real blockchain precision(to view currency's actual individual precision, refer to fetchCurrencies() method). + # + data = self.safe_value(response, 'symbols', []) + result = [] + for i in range(0, len(data)): + market = data[i] + id = self.safe_string(market, 'symbol') + baseId = self.safe_string(market, 'baseAsset') + quoteId = self.safe_string(market, 'quoteAsset') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + status = self.safe_string(market, 'status') + isSpotTradingAllowed = self.safe_value(market, 'isSpotTradingAllowed') + active = False + if (status == '1') and (isSpotTradingAllowed): + active = True + isMarginTradingAllowed = self.safe_value(market, 'isMarginTradingAllowed') + makerCommission = self.safe_number(market, 'makerCommission') + takerCommission = self.safe_number(market, 'takerCommission') + maxQuoteAmount = self.safe_number(market, 'maxQuoteAmount') + result.append({ + 'id': id, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': isMarginTradingAllowed, + 'swap': False, + 'future': False, + 'option': False, + 'active': active, + 'contract': False, + 'linear': None, + 'inverse': None, + 'taker': takerCommission, + 'maker': makerCommission, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(self.parse_precision(self.safe_string(market, 'baseAssetPrecision'))), + 'price': self.parse_number(self.parse_precision(self.safe_string(market, 'quoteAssetPrecision'))), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(market, 'baseSizePrecision'), + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': self.safe_number(market, 'quoteAmountPrecision'), + 'max': maxQuoteAmount, + }, + }, + 'created': None, + 'info': market, + }) + return result + + async def fetch_swap_markets(self, params={}): + """ + @ignore + retrieves data on all swap markets for mexc + + https://mexcdevelop.github.io/apidocs/contract_v1_en/#get-the-contract-information + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + currentRl: number = self.rateLimit + self.set_property(self, 'rateLimit', 10) # see comment: https://github.com/ccxt/ccxt/pull/23698 + response = await self.contractPublicGetDetail(params) + self.set_property(self, 'rateLimit', currentRl) + # + # { + # "success":true, + # "code":0, + # "data":[ + # { + # "symbol":"BTC_USDT", + # "displayName":"BTC_USDT永续", + # "displayNameEn":"BTC_USDT SWAP", + # "positionOpenType":3, + # "baseCoin":"BTC", + # "quoteCoin":"USDT", + # "settleCoin":"USDT", + # "contractSize":0.0001, + # "minLeverage":1, + # "maxLeverage":125, + # "priceScale":2, # seems useless atm,'s just how UI shows the price, i.e. 29583.50 for BTC/USDT:USDT, while price ticksize is 0.5 + # "volScale":0, # probably: contract amount precision + # "amountScale":4, # probably: quote currency precision + # "priceUnit":0.5, # price tick size + # "volUnit":1, # probably: contract tick size + # "minVol":1, + # "maxVol":1000000, + # "bidLimitPriceRate":0.1, + # "askLimitPriceRate":0.1, + # "takerFeeRate":0.0006, + # "makerFeeRate":0.0002, + # "maintenanceMarginRate":0.004, + # "initialMarginRate":0.008, + # "riskBaseVol":10000, + # "riskIncrVol":200000, + # "riskIncrMmr":0.004, + # "riskIncrImr":0.004, + # "riskLevelLimit":5, + # "priceCoefficientVariation":0.1, + # "indexOrigin":["BINANCE","GATEIO","HUOBI","MXC"], + # "state":0, # 0 enabled, 1 delivery, 2 completed, 3 offline, 4 pause + # "isNew":false, + # "isHot":true, + # "isHidden":false + # }, + # ] + # } + # + data = self.safe_value(response, 'data', []) + result = [] + for i in range(0, len(data)): + market = data[i] + id = self.safe_string(market, 'symbol') + baseId = self.safe_string(market, 'baseCoin') + quoteId = self.safe_string(market, 'quoteCoin') + settleId = self.safe_string(market, 'settleCoin') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + settle = self.safe_currency_code(settleId) + state = self.safe_string(market, 'state') + isLinear = quote == settle + result.append({ + 'id': id, + 'symbol': base + '/' + quote + ':' + settle, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': 'swap', + 'spot': False, + 'margin': False, + 'swap': True, + 'future': False, + 'option': False, + 'active': (state == '0'), + 'contract': True, + 'linear': isLinear, + 'inverse': not isLinear, + 'taker': self.safe_number(market, 'takerFeeRate'), + 'maker': self.safe_number(market, 'makerFeeRate'), + 'contractSize': self.safe_number(market, 'contractSize'), + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.safe_number(market, 'volUnit'), + 'price': self.safe_number(market, 'priceUnit'), + }, + 'limits': { + 'leverage': { + 'min': self.safe_number(market, 'minLeverage'), + 'max': self.safe_number(market, 'maxLeverage'), + }, + 'amount': { + 'min': self.safe_number(market, 'minVol'), + 'max': self.safe_number(market, 'maxVol'), + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + 'info': market, + }) + return result + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#order-book + https://mexcdevelop.github.io/apidocs/contract_v1_en/#get-the-contract-s-depth-information + + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['limit'] = limit + orderbook = None + if market['spot']: + response = await self.spotPublicGetDepth(self.extend(request, params)) + # + # { + # "lastUpdateId": "744267132", + # "bids": [ + # ["40838.50","0.387864"], + # ["40837.95","0.008400"], + # ], + # "asks": [ + # ["40838.61","6.544908"], + # ["40838.88","0.498000"], + # ] + # } + # + spotTimestamp = self.safe_integer(response, 'timestamp') + orderbook = self.parse_order_book(response, symbol, spotTimestamp) + orderbook['nonce'] = self.safe_integer(response, 'lastUpdateId') + elif market['swap']: + response = await self.contractPublicGetDepthSymbol(self.extend(request, params)) + # + # { + # "success":true, + # "code":0, + # "data":{ + # "asks":[ + # [3445.72,48379,1], + # [3445.75,34994,1], + # ], + # "bids":[ + # [3445.55,44081,1], + # [3445.51,24857,1], + # ], + # "version":2827730444, + # "timestamp":1634117846232 + # } + # } + # + data = self.safe_value(response, 'data') + timestamp = self.safe_integer(data, 'timestamp') + orderbook = self.parse_order_book(data, symbol, timestamp) + orderbook['nonce'] = self.safe_integer(data, 'version') + return orderbook + + def parse_bid_ask(self, bidask, priceKey: IndexType = 0, amountKey: IndexType = 1, countOrIdKey: IndexType = 2): + countKey = 2 + price = self.safe_number(bidask, priceKey) + amount = self.safe_number(bidask, amountKey) + count = self.safe_number(bidask, countKey) + if count is not None: + return [price, amount, count] + return [price, amount] + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#recent-trades-list + https://mexcdevelop.github.io/apidocs/spot_v3_en/#compressed-aggregate-trades-list + https://mexcdevelop.github.io/apidocs/contract_v1_en/#get-contract-transaction-data + + get the list of most recent trades for a particular symbol + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: *spot only* *since must be defined* the latest time in ms to fetch entries for + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['limit'] = limit + trades = None + if market['spot']: + until = self.safe_integer_n(params, ['endTime', 'until']) + if since is not None: + request['startTime'] = since + if until is None: + raise ArgumentsRequired(self.id + ' fetchTrades() requires an until parameter when since is provided') + if until is not None: + if since is None: + raise ArgumentsRequired(self.id + ' fetchTrades() requires a since parameter when until is provided') + request['endTime'] = until + method = self.safe_string(self.options, 'fetchTradesMethod', 'spotPublicGetAggTrades') + method = self.safe_string(params, 'method', method) # AggTrades, HistoricalTrades, Trades + params = self.omit(params, ['method']) + if method == 'spotPublicGetAggTrades': + trades = await self.spotPublicGetAggTrades(self.extend(request, params)) + elif method == 'spotPublicGetHistoricalTrades': + trades = await self.spotPublicGetHistoricalTrades(self.extend(request, params)) + elif method == 'spotPublicGetTrades': + trades = await self.spotPublicGetTrades(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchTrades() not support self method') + # + # /trades, /historicalTrades + # + # [ + # { + # "id": null, + # "price": "40798.94", + # "qty": "0.000508", + # "quoteQty": "20.72586152", + # "time": "1647546934374", + # "isBuyerMaker": True, + # "isBestMatch": True + # }, + # ] + # + # /aggrTrades + # + # [ + # { + # "a": null, + # "f": null, + # "l": null, + # "p": "40679", + # "q": "0.001309", + # "T": 1647551328000, + # "m": True, + # "M": True + # }, + # ] + # + elif market['swap']: + response = await self.contractPublicGetDealsSymbol(self.extend(request, params)) + # + # { + # "success": True, + # "code": 0, + # "data": [ + # { + # "p": 31199, + # "v": 18, + # "T": 1, + # "O": 3, + # "M": 2, + # "t": 1609831235985 + # }, + # ] + # } + # + trades = self.safe_value(response, 'data') + return self.parse_trades(trades, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + id = None + timestamp = None + orderId = None + symbol = None + fee = None + type = None + side = None + takerOrMaker = None + priceString = None + amountString = None + costString = None + # if swap + if 'v' in trade: + # + # swap: fetchTrades + # + # { + # "p": 31199, + # "v": 18, + # "T": 1, + # "O": 3, + # "M": 2, + # "t": 1609831235985 + # } + # + timestamp = self.safe_integer(trade, 't') + market = self.safe_market(None, market) + symbol = market['symbol'] + priceString = self.safe_string(trade, 'p') + amountString = self.safe_string(trade, 'v') + side = self.parse_order_side(self.safe_string(trade, 'T')) + takerOrMaker = 'taker' + else: + # + # spot: fetchTrades(for aggTrades) + # + # { + # "a": null, + # "f": null, + # "l": null, + # "p": "40679", + # "q": "0.001309", + # "T": 1647551328000, + # "m": True, + # "M": True + # } + # + # spot: fetchMyTrades, fetchOrderTrades + # + # { + # "symbol": "BTCUSDT", + # "id": "133948532984922113", + # "orderId": "133948532531949568", + # "orderListId": "-1", + # "price": "41995.51", + # "qty": "0.0002", + # "quoteQty": "8.399102", + # "commission": "0.016798204", + # "commissionAsset": "USDT", + # "time": "1647718055000", + # "isBuyer": True, + # "isMaker": False, + # "isBestMatch": True + # } + # + # swap: fetchMyTrades, fetchOrderTrades + # + # { + # "id": "299444585", + # "symbol": "STEPN_USDT", + # "side": "1", + # "vol": "1", + # "price": "2.45455", + # "feeCurrency": "USDT", + # "fee": "0.00147273", + # "timestamp": "1648924557000", + # "profit": "0", + # "category": "1", + # "orderId": "265307163526610432", + # "positionMode": "1", + # "taker": True + # } + # + marketId = self.safe_string(trade, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + id = self.safe_string_2(trade, 'id', 'a') + priceString = self.safe_string_2(trade, 'price', 'p') + orderId = self.safe_string(trade, 'orderId') + # if swap + if 'positionMode' in trade: + timestamp = self.safe_integer(trade, 'timestamp') + amountString = self.safe_string(trade, 'vol') + side = self.parse_order_side(self.safe_string(trade, 'side')) + fee = { + 'cost': self.safe_string(trade, 'fee'), + 'currency': self.safe_currency_code(self.safe_string(trade, 'feeCurrency')), + } + takerOrMaker = 'taker' if self.safe_value(trade, 'taker') else 'maker' + else: + timestamp = self.safe_integer_2(trade, 'time', 'T') + amountString = self.safe_string_2(trade, 'qty', 'q') + costString = self.safe_string(trade, 'quoteQty') + isBuyer = self.safe_value(trade, 'isBuyer') + isMaker = self.safe_value(trade, 'isMaker') + buyerMaker = self.safe_value_2(trade, 'isBuyerMaker', 'm') + if isMaker is not None: + takerOrMaker = 'maker' if isMaker else 'taker' + if isBuyer is not None: + side = 'buy' if isBuyer else 'sell' + if buyerMaker is not None: + side = 'sell' if buyerMaker else 'buy' + takerOrMaker = 'taker' + feeAsset = self.safe_string(trade, 'commissionAsset') + if feeAsset is not None: + fee = { + 'cost': self.safe_string(trade, 'commission'), + 'currency': self.safe_currency_code(feeAsset), + } + if id is None and self.safe_bool(self.options, 'useCcxtTradeId', True): + id = self.create_ccxt_trade_id(timestamp, side, amountString, priceString, takerOrMaker) + return self.safe_trade({ + 'id': id, + 'order': orderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'type': type, + 'side': side, + 'takerOrMaker': takerOrMaker, + 'price': priceString, + 'amount': amountString, + 'cost': costString, + 'fee': fee, + 'info': trade, + }, market) + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#kline-candlestick-data + https://mexcdevelop.github.io/apidocs/contract_v1_en/#k-line-data + + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest candle to fetch + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + maxLimit = 1000 if (market['spot']) else 2000 + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate', False) + if paginate: + return await self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params, maxLimit) + options = self.safe_value(self.options, 'timeframes', {}) + timeframes = self.safe_value(options, market['type'], {}) + timeframeValue = self.safe_string(timeframes, timeframe) + duration = self.parse_timeframe(timeframe) * 1000 + request: dict = { + 'symbol': market['id'], + 'interval': timeframeValue, + } + candles = None + if market['spot']: + until = self.safe_integer_n(params, ['until', 'endTime']) + if since is not None: + request['startTime'] = since + if until is None: + # we have to calculate it assuming we can get at most 2000 entries per request + end = self.sum(since, maxLimit * duration) + now = self.milliseconds() + request['endTime'] = min(end, now) + if limit is not None: + request['limit'] = limit + if until is not None: + params = self.omit(params, ['until']) + request['endTime'] = until + response = await self.spotPublicGetKlines(self.extend(request, params)) + # + # [ + # [ + # 1640804880000, + # "47482.36", + # "47482.36", + # "47416.57", + # "47436.1", + # "3.550717", + # 1640804940000, + # "168387.3" + # ], + # ] + # + candles = response + elif market['swap']: + until = self.safe_integer_product_n(params, ['until', 'endTime'], 0.001) + if since is not None: + request['start'] = self.parse_to_int(since / 1000) + if until is not None: + params = self.omit(params, ['until']) + request['end'] = until + priceType = self.safe_string(params, 'price', 'default') + params = self.omit(params, 'price') + response = None + if priceType == 'default': + response = await self.contractPublicGetKlineSymbol(self.extend(request, params)) + elif priceType == 'index': + response = await self.contractPublicGetKlineIndexPriceSymbol(self.extend(request, params)) + elif priceType == 'mark': + response = await self.contractPublicGetKlineFairPriceSymbol(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchOHLCV() not support self price type, [default, index, mark]') + # + # { + # "success":true, + # "code":0, + # "data":{ + # "time":[1634052300,1634052360,1634052420], + # "open":[3492.2,3491.3,3495.65], + # "close":[3491.3,3495.65,3495.2], + # "high":[3495.85,3496.55,3499.4], + # "low":[3491.15,3490.9,3494.2], + # "vol":[1740.0,351.0,314.0], + # "amount":[60793.623,12260.4885,10983.1375], + # } + # } + # + data = self.safe_value(response, 'data') + candles = self.convert_trading_view_to_ohlcv(data, 'time', 'open', 'high', 'low', 'close', 'vol') + return self.parse_ohlcvs(candles, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + return [ + self.safe_integer(ohlcv, 0), + self.safe_number(ohlcv, 1), + self.safe_number(ohlcv, 2), + self.safe_number(ohlcv, 3), + self.safe_number(ohlcv, 4), + self.safe_number(ohlcv, 5), + ] + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#24hr-ticker-price-change-statistics + https://mexcdevelop.github.io/apidocs/contract_v1_en/#get-contract-trend-data + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + request: dict = {} + market = None + isSingularMarket = False + if symbols is not None: + length = len(symbols) + isSingularMarket = length == 1 + firstSymbol = self.safe_string(symbols, 0) + market = self.market(firstSymbol) + marketType, query = self.handle_market_type_and_params('fetchTickers', market, params) + tickers = None + if isSingularMarket: + request['symbol'] = market['id'] + if marketType == 'spot': + tickers = await self.spotPublicGetTicker24hr(self.extend(request, query)) + # + # [ + # { + # "symbol": "BTCUSDT", + # "priceChange": "184.34", + # "priceChangePercent": "0.00400048", + # "prevClosePrice": "46079.37", + # "lastPrice": "46263.71", + # "lastQty": "", + # "bidPrice": "46260.38", + # "bidQty": "", + # "askPrice": "46260.41", + # "askQty": "", + # "openPrice": "46079.37", + # "highPrice": "47550.01", + # "lowPrice": "45555.5", + # "volume": "1732.461487", + # "quoteVolume": null, + # "openTime": 1641349500000, + # "closeTime": 1641349582808, + # "count": null + # } + # ] + # + elif marketType == 'swap': + response = await self.contractPublicGetTicker(self.extend(request, query)) + # + # { + # "success":true, + # "code":0, + # "data":[ + # { + # "symbol":"ETH_USDT", + # "lastPrice":3581.3, + # "bid1":3581.25, + # "ask1":3581.5, + # "volume24":4045530, + # "amount24":141331823.5755, + # "holdVol":5832946, + # "lower24Price":3413.4, + # "high24Price":3588.7, + # "riseFallRate":0.0275, + # "riseFallValue":95.95, + # "indexPrice":3580.7852, + # "fairPrice":3581.08, + # "fundingRate":0.000063, + # "maxBidPrice":3938.85, + # "minAskPrice":3222.7, + # "timestamp":1634162885016 + # }, + # ] + # } + # + tickers = self.safe_value(response, 'data', []) + # when it's single symbol request, the returned structure is different(singular object) for both spot & swap, thus we need to wrap inside array + if isSingularMarket: + tickers = [tickers] + return self.parse_tickers(tickers, symbols) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#24hr-ticker-price-change-statistics + https://mexcdevelop.github.io/apidocs/contract_v1_en/#get-contract-trend-data + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + marketType, query = self.handle_market_type_and_params('fetchTicker', market, params) + ticker = None + request: dict = { + 'symbol': market['id'], + } + if marketType == 'spot': + ticker = await self.spotPublicGetTicker24hr(self.extend(request, query)) + # + # { + # "symbol": "BTCUSDT", + # "priceChange": "184.34", + # "priceChangePercent": "0.00400048", + # "prevClosePrice": "46079.37", + # "lastPrice": "46263.71", + # "lastQty": "", + # "bidPrice": "46260.38", + # "bidQty": "", + # "askPrice": "46260.41", + # "askQty": "", + # "openPrice": "46079.37", + # "highPrice": "47550.01", + # "lowPrice": "45555.5", + # "volume": "1732.461487", + # "quoteVolume": null, + # "openTime": 1641349500000, + # "closeTime": 1641349582808, + # "count": null + # } + # + elif marketType == 'swap': + response = await self.contractPublicGetTicker(self.extend(request, query)) + # + # { + # "success":true, + # "code":0, + # "data":{ + # "symbol":"ETH_USDT", + # "lastPrice":3581.3, + # "bid1":3581.25, + # "ask1":3581.5, + # "volume24":4045530, + # "amount24":141331823.5755, + # "holdVol":5832946, + # "lower24Price":3413.4, + # "high24Price":3588.7, + # "riseFallRate":0.0275, + # "riseFallValue":95.95, + # "indexPrice":3580.7852, + # "fairPrice":3581.08, + # "fundingRate":0.000063, + # "maxBidPrice":3938.85, + # "minAskPrice":3222.7, + # "timestamp":1634162885016 + # } + # } + # + ticker = self.safe_value(response, 'data', {}) + # when it's single symbol request, the returned structure is different(singular object) for both spot & swap, thus we need to wrap inside array + return self.parse_ticker(ticker, market) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + marketId = self.safe_string(ticker, 'symbol') + market = self.safe_market(marketId, market) + timestamp = None + bid = None + ask = None + bidVolume = None + askVolume = None + baseVolume = None + quoteVolume = None + open = None + high = None + low = None + changePcnt = None + changeValue = None + prevClose = None + isSwap = self.safe_value(market, 'swap') + # if swap + if isSwap or ('timestamp' in ticker): + # + # { + # "symbol": "ETH_USDT", + # "lastPrice": 3581.3, + # "bid1": 3581.25, + # "ask1": 3581.5, + # "volume24": 4045530, + # "amount24": 141331823.5755, + # "holdVol": 5832946, + # "lower24Price": 3413.4, + # "high24Price": 3588.7, + # "riseFallRate": 0.0275, + # "riseFallValue": 95.95, + # "indexPrice": 3580.7852, + # "fairPrice": 3581.08, + # "fundingRate": 0.000063, + # "maxBidPrice": 3938.85, + # "minAskPrice": 3222.7, + # "timestamp": 1634162885016 + # } + # + timestamp = self.safe_integer(ticker, 'timestamp') + bid = self.safe_string(ticker, 'bid1') + ask = self.safe_string(ticker, 'ask1') + baseVolume = self.safe_string(ticker, 'volume24') + quoteVolume = self.safe_string(ticker, 'amount24') + high = self.safe_string(ticker, 'high24Price') + low = self.safe_string(ticker, 'lower24Price') + changeValue = self.safe_string(ticker, 'riseFallValue') + changePcnt = self.safe_string(ticker, 'riseFallRate') + changePcnt = Precise.string_mul(changePcnt, '100') + else: + # + # { + # "symbol": "BTCUSDT", + # "priceChange": "184.34", + # "priceChangePercent": "0.00400048", + # "prevClosePrice": "46079.37", + # "lastPrice": "46263.71", + # "lastQty": "", + # "bidPrice": "46260.38", + # "bidQty": "", + # "askPrice": "46260.41", + # "askQty": "", + # "openPrice": "46079.37", + # "highPrice": "47550.01", + # "lowPrice": "45555.5", + # "volume": "1732.461487", + # "quoteVolume": null, + # "openTime": 1641349500000, + # "closeTime": 1641349582808, + # "count": null + # } + # + timestamp = self.safe_integer(ticker, 'closeTime') + bid = self.safe_string(ticker, 'bidPrice') + ask = self.safe_string(ticker, 'askPrice') + bidVolume = self.safe_string(ticker, 'bidQty') + askVolume = self.safe_string(ticker, 'askQty') + if Precise.string_eq(bidVolume, '0'): + bidVolume = None + if Precise.string_eq(askVolume, '0'): + askVolume = None + baseVolume = self.safe_string(ticker, 'volume') + quoteVolume = self.safe_string(ticker, 'quoteVolume') + open = self.safe_string(ticker, 'openPrice') + high = self.safe_string(ticker, 'highPrice') + low = self.safe_string(ticker, 'lowPrice') + prevClose = self.safe_string(ticker, 'prevClosePrice') + changeValue = self.safe_string(ticker, 'priceChange') + changePcnt = self.safe_string(ticker, 'priceChangePercent') + changePcnt = Precise.string_mul(changePcnt, '100') + return self.safe_ticker({ + 'symbol': market['symbol'], + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'open': open, + 'high': high, + 'low': low, + 'close': self.safe_string(ticker, 'lastPrice'), + 'bid': bid, + 'bidVolume': bidVolume, + 'ask': ask, + 'askVolume': askVolume, + 'vwap': None, + 'previousClose': prevClose, + 'change': changeValue, + 'percentage': changePcnt, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'info': ticker, + }, market) + + async def fetch_bids_asks(self, symbols: Strings = None, params={}): + """ + fetches the bid and ask price and volume for multiple markets + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#symbol-order-book-ticker + + :param str[]|None symbols: unified symbols of the markets to fetch the bids and asks for, all markets are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + market = None + isSingularMarket = False + if symbols is not None: + length = len(symbols) + isSingularMarket = length == 1 + market = self.market(symbols[0]) + marketType, query = self.handle_market_type_and_params('fetchBidsAsks', market, params) + tickers = None + if marketType == 'spot': + tickers = await self.spotPublicGetTickerBookTicker(query) + # + # [ + # { + # "symbol": "AEUSDT", + # "bidPrice": "0.11001", + # "bidQty": "115.59", + # "askPrice": "0.11127", + # "askQty": "215.48" + # }, + # ] + # + elif marketType == 'swap': + raise NotSupported(self.id + ' fetchBidsAsks() is not available for ' + marketType + ' markets') + # when it's single symbol request, the returned structure is different(singular object) for both spot & swap, thus we need to wrap inside array + if isSingularMarket: + tickers = [tickers] + return self.parse_tickers(tickers, symbols) + + async def create_market_buy_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market buy order by providing the symbol and cost + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#new-order + + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' createMarketBuyOrderWithCost() supports spot orders only') + req = { + 'cost': cost, + } + return await self.create_order(symbol, 'market', 'buy', 0, None, self.extend(req, params)) + + async def create_market_sell_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market sell order by providing the symbol and cost + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#new-order + + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' createMarketBuyOrderWithCost() supports spot orders only') + req = { + 'cost': cost, + } + return await self.create_order(symbol, 'market', 'sell', 0, None, self.extend(req, params)) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#new-order + https://mexcdevelop.github.io/apidocs/contract_v1_en/#order-under-maintenance + https://mexcdevelop.github.io/apidocs/contract_v1_en/#trigger-order-under-maintenance + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: only 'isolated' is supported for spot-margin trading + :param float [params.triggerPrice]: The price at which a trigger order is triggered at + :param bool [params.postOnly]: if True, the order will only be posted if it will be a maker order + :param bool [params.reduceOnly]: *contract only* indicates if self order is to reduce the size of a position + :param bool [params.hedged]: *swap only* True for hedged mode, False for one way mode, default is False + :param str [params.timeInForce]: 'IOC' or 'FOK', default is 'GTC' + EXCHANGE SPECIFIC PARAMETERS + :param int [params.leverage]: *contract only* leverage is necessary on isolated margin + :param long [params.positionId]: *contract only* it is recommended to hasattr(self, fill) parameter when closing a position + :param str [params.externalOid]: *contract only* external order ID + :param int [params.positionMode]: *contract only* 1:hedge, 2:one-way, default: the user's current config + :param boolean [params.test]: *spot only* whether to use the test endpoint or not, default is False + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + marginMode, query = self.handle_margin_mode_and_params('createOrder', params) + if market['spot']: + return await self.create_spot_order(market, type, side, amount, price, marginMode, query) + else: + return await self.create_swap_order(market, type, side, amount, price, marginMode, query) + + def create_spot_order_request(self, market, type, side, amount, price=None, marginMode=None, params={}): + symbol = market['symbol'] + orderSide = side.upper() + request: dict = { + 'symbol': market['id'], + 'side': orderSide, + 'type': type.upper(), + } + if type == 'market': + cost = self.safe_number_2(params, 'cost', 'quoteOrderQty') + params = self.omit(params, 'cost') + if cost is not None: + amount = cost + request['quoteOrderQty'] = self.cost_to_precision(symbol, amount) + else: + if price is None: + request['quantity'] = self.amount_to_precision(symbol, amount) + else: + amountString = self.number_to_string(amount) + priceString = self.number_to_string(price) + quoteAmount = Precise.string_mul(amountString, priceString) + amount = quoteAmount + request['quoteOrderQty'] = self.cost_to_precision(symbol, amount) + else: + request['quantity'] = self.amount_to_precision(symbol, amount) + if price is not None: + request['price'] = self.price_to_precision(symbol, price) + clientOrderId = self.safe_string(params, 'clientOrderId') + if clientOrderId is not None: + request['newClientOrderId'] = clientOrderId + params = self.omit(params, ['type', 'clientOrderId']) + if marginMode is not None: + if marginMode != 'isolated': + raise BadRequest(self.id + ' createOrder() does not support marginMode ' + marginMode + ' for spot-margin trading') + postOnly = None + postOnly, params = self.handle_post_only(type == 'market', type == 'LIMIT_MAKER', params) + if postOnly: + request['type'] = 'LIMIT_MAKER' + tif = self.safe_string(params, 'timeInForce') + if tif is not None: + params = self.omit(params, 'timeInForce') + if tif == 'IOC': + request['type'] = 'IMMEDIATE_OR_CANCEL' + elif tif == 'FOK': + request['type'] = 'FILL_OR_KILL' + return self.extend(request, params) + + async def create_spot_order(self, market, type, side, amount, price=None, marginMode=None, params={}): + """ + @ignore + create a trade order + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#new-order + + :param str market: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param str [marginMode]: only 'isolated' is supported for spot-margin trading + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.postOnly]: if True, the order will only be posted if it will be a maker order + :returns dict: an `order structure ` + """ + await self.load_markets() + test = self.safe_bool(params, 'test', False) + params = self.omit(params, 'test') + request = self.create_spot_order_request(market, type, side, amount, price, marginMode, params) + response = None + if test: + response = await self.spotPrivatePostOrderTest(request) + else: + response = await self.spotPrivatePostOrder(request) + # + # spot + # + # { + # "symbol": "BTCUSDT", + # "orderId": "123738410679123456", + # "orderListId": -1 + # } + # + # margin + # + # { + # "symbol": "BTCUSDT", + # "orderId": "762634301354414080", + # "clientOrderId": null, + # "isIsolated": True, + # "transactTime": 1661992652132 + # } + # + order = self.parse_order(response, market) + order['side'] = side + order['type'] = type + if self.safe_string(order, 'price') is None: + order['price'] = price + if self.safe_string(order, 'amount') is None: + order['amount'] = amount + return order + + async def create_swap_order(self, market, type, side, amount, price=None, marginMode=None, params={}): + """ + @ignore + create a trade order + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#new-order + https://mexcdevelop.github.io/apidocs/contract_v1_en/#order-under-maintenance + https://mexcdevelop.github.io/apidocs/contract_v1_en/#trigger-order-under-maintenance + + :param str market: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param str [marginMode]: only 'isolated' is supported for spot-margin trading + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: The price at which a trigger order is triggered at + :param bool [params.postOnly]: if True, the order will only be posted if it will be a maker order + :param bool [params.reduceOnly]: indicates if self order is to reduce the size of a position + :param bool [params.hedged]: *swap only* True for hedged mode, False for one way mode, default is False + + EXCHANGE SPECIFIC PARAMETERS + :param int [params.leverage]: leverage is necessary on isolated margin + :param long [params.positionId]: it is recommended to hasattr(self, fill) parameter when closing a position + :param str [params.externalOid]: external order ID + :param int [params.positionMode]: 1:hedge, 2:one-way, default: the user's current config + :returns dict: an `order structure ` + """ + await self.load_markets() + symbol = market['symbol'] + unavailableContracts = self.safe_value(self.options, 'unavailableContracts', {}) + isContractUnavaiable = self.safe_bool(unavailableContracts, symbol, False) + if isContractUnavaiable: + raise NotSupported(self.id + ' createSwapOrder() does not support yet self symbol:' + symbol) + openType = None + if marginMode is not None: + if marginMode == 'cross': + openType = 2 + elif marginMode == 'isolated': + openType = 1 + else: + raise ArgumentsRequired(self.id + ' createSwapOrder() marginMode parameter should be either "cross" or "isolated"') + else: + openType = self.safe_integer(params, 'openType', 2) # defaulting to cross margin + if (type != 'limit') and (type != 'market') and (type != 1) and (type != 2) and (type != 3) and (type != 4) and (type != 5) and (type != 6): + raise InvalidOrder(self.id + ' createSwapOrder() order type must either limit, market, or 1 for limit orders, 2 for post-only orders, 3 for IOC orders, 4 for FOK orders, 5 for market orders or 6 to convert market price to current price') + postOnly = None + postOnly, params = self.handle_post_only(type == 'market', type == 2, params) + if postOnly: + type = 2 + elif type == 'limit': + type = 1 + elif type == 'market': + type = 6 + request: dict = { + 'symbol': market['id'], + # 'price': float(self.price_to_precision(symbol, price)), + 'vol': float(self.amount_to_precision(symbol, amount)), + # 'leverage': int, # required for isolated margin + # 'side': side, # 1 open long, 2 close short, 3 open short, 4 close long + # + # supported order types + # + # 1 limit + # 2 post only maker(PO) + # 3 transact or cancel instantly(IOC) + # 4 transact completely or cancel completely(FOK) + # 5 market orders + # 6 convert market price to current price + # + 'type': type, + 'openType': openType, # 1 isolated, 2 cross + # 'positionId': 1394650, # long, hasattr(self, filling) parameter when closing a position is recommended + # 'externalOid': clientOrderId, + # 'triggerPrice': 10.0, # Required for trigger order + # 'triggerType': 1, # Required for trigger order 1: more than or equal, 2: less than or equal + # 'executeCycle': 1, # Required for trigger order 1: 24 hours,2: 7 days + # 'trend': 1, # Required for trigger order 1: latest price, 2: fair price, 3: index price + # 'orderType': 1, # Required for trigger order 1: limit order,2:Post Only Maker,3: close or cancel instantly ,4: close or cancel completely,5: Market order + } + if (type != 5) and (type != 6) and (type != 'market'): + request['price'] = float(self.price_to_precision(symbol, price)) + if openType == 1: + leverage = self.safe_integer(params, 'leverage') + if leverage is None: + raise ArgumentsRequired(self.id + ' createSwapOrder() requires a leverage parameter for isolated margin orders') + reduceOnly = self.safe_bool(params, 'reduceOnly', False) + hedged = self.safe_bool(params, 'hedged', False) + sideInteger = None + if hedged: + if reduceOnly: + params = self.omit(params, 'reduceOnly') # hedged mode does not accept self parameter + side = 'sell' if (side == 'buy') else 'buy' + sideInteger = 1 if (side == 'buy') else 3 + request['positionMode'] = 1 + else: + if reduceOnly: + sideInteger = 2 if (side == 'buy') else 4 + else: + sideInteger = 1 if (side == 'buy') else 3 + request['side'] = sideInteger + clientOrderId = self.safe_string_2(params, 'clientOrderId', 'externalOid') + if clientOrderId is not None: + request['externalOid'] = clientOrderId + triggerPrice = self.safe_number_2(params, 'triggerPrice', 'stopPrice') + params = self.omit(params, ['clientOrderId', 'externalOid', 'postOnly', 'stopPrice', 'triggerPrice', 'hedged']) + response = None + if triggerPrice: + request['triggerPrice'] = self.price_to_precision(symbol, triggerPrice) + request['triggerType'] = self.safe_integer(params, 'triggerType', 1) + request['executeCycle'] = self.safe_integer(params, 'executeCycle', 1) + request['trend'] = self.safe_integer(params, 'trend', 1) + request['orderType'] = self.safe_integer(params, 'orderType', 1) + response = await self.contractPrivatePostPlanorderPlace(self.extend(request, params)) + else: + response = await self.contractPrivatePostOrderSubmit(self.extend(request, params)) + # + # Swap + # {"code":200,"data":"2ff3163e8617443cb9c6fc19d42b1ca4"} + # + # Trigger + # {"success":true,"code":0,"data":259208506303929856} + # + data = self.safe_string(response, 'data') + return self.safe_order({'id': data}, market) + + async def create_orders(self, orders: List[OrderRequest], params={}): + """ + *spot only* *all orders must have the same symbol* create a list of trade orders + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#batch-orders + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to api endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + ordersRequests = [] + symbol = None + for i in range(0, len(orders)): + rawOrder = orders[i] + marketId = self.safe_string(rawOrder, 'symbol') + market = self.market(marketId) + if not market['spot']: + raise NotSupported(self.id + ' createOrders() is only supported for spot markets') + if symbol is None: + symbol = marketId + else: + if symbol != marketId: + raise BadRequest(self.id + ' createOrders() requires all orders to have the same symbol') + type = self.safe_string(rawOrder, 'type') + side = self.safe_string(rawOrder, 'side') + amount = self.safe_value(rawOrder, 'amount') + price = self.safe_value(rawOrder, 'price') + orderParams = self.safe_value(rawOrder, 'params', {}) + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('createOrder', params) + orderRequest = self.create_spot_order_request(market, type, side, amount, price, marginMode, orderParams) + ordersRequests.append(orderRequest) + request: dict = { + 'batchOrders': self.json(ordersRequests), + } + response = await self.spotPrivatePostBatchOrders(request) + # + # [ + # { + # "symbol": "BTCUSDT", + # "orderId": "1196315350023612316", + # "newClientOrderId": "hio8279hbdsds", + # "orderListId": -1 + # }, + # { + # "newClientOrderId": "123456", + # "msg": "The minimum transaction volume cannot be less than:0.5USDT", + # "code": 30002 + # }, + # { + # "symbol": "BTCUSDT", + # "orderId": "1196315350023612318", + # "orderListId": -1 + # } + # ] + # + return self.parse_orders(response) + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#query-order + https://mexcdevelop.github.io/apidocs/contract_v1_en/#query-the-order-based-on-the-order-number + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: only 'isolated' is supported, for spot-margin trading + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrder() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + data = None + if market['spot']: + clientOrderId = self.safe_string(params, 'clientOrderId') + if clientOrderId is not None: + params = self.omit(params, 'clientOrderId') + request['origClientOrderId'] = clientOrderId + else: + request['orderId'] = id + marginMode, query = self.handle_margin_mode_and_params('fetchOrder', params) + if marginMode is not None: + if marginMode != 'isolated': + raise BadRequest(self.id + ' fetchOrder() does not support marginMode ' + marginMode + ' for spot-margin trading') + data = await self.spotPrivateGetMarginOrder(self.extend(request, query)) + else: + data = await self.spotPrivateGetOrder(self.extend(request, query)) + # + # spot + # + # { + # "symbol": "BTCUSDT", + # "orderId": "133734823834147272", + # "orderListId": "-1", + # "clientOrderId": null, + # "price": "30000", + # "origQty": "0.0002", + # "executedQty": "0", + # "cummulativeQuoteQty": "0", + # "status": "CANCELED", + # "timeInForce": null, + # "type": "LIMIT", + # "side": "BUY", + # "stopPrice": null, + # "icebergQty": null, + # "time": "1647667102000", + # "updateTime": "1647708567000", + # "isWorking": True, + # "origQuoteOrderQty": "6" + # } + # + # margin + # + # { + # "symbol": "BTCUSDT", + # "orderId": "763307297891028992", + # "orderListId": "-1", + # "clientOrderId": null, + # "price": "18000", + # "origQty": "0.0014", + # "executedQty": "0", + # "cummulativeQuoteQty": "0", + # "status": "NEW", + # "type": "LIMIT", + # "side": "BUY", + # "isIsolated": True, + # "isWorking": True, + # "time": 1662153107000, + # "updateTime": 1662153107000 + # } + # + elif market['swap']: + request['order_id'] = id + response = await self.contractPrivateGetOrderGetOrderId(self.extend(request, params)) + # + # { + # "success": True, + # "code": "0", + # "data": { + # "orderId": "264995729269765120", + # "symbol": "STEPN_USDT", + # "positionId": "0", + # "price": "2.2", + # "vol": "15", + # "leverage": "20", + # "side": "1", + # "category": "1", + # "orderType": "1", + # "dealAvgPrice": "0", + # "dealVol": "0", + # "orderMargin": "2.2528", + # "takerFee": "0", + # "makerFee": "0", + # "profit": "0", + # "feeCurrency": "USDT", + # "openType": "1", + # "state": "2", + # "externalOid": "_m_0e9520c256744d64b942985189026d20", + # "errorCode": "0", + # "usedMargin": "0", + # "createTime": "1648850305236", + # "updateTime": "1648850305245", + # "positionMode": "1" + # } + # } + # + data = self.safe_value(response, 'data') + return self.parse_order(data, market) + + async def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#all-orders + https://mexcdevelop.github.io/apidocs/contract_v1_en/#get-all-of-the-user-39-s-historical-orders + https://mexcdevelop.github.io/apidocs/contract_v1_en/#gets-the-trigger-order-list + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch orders for + :param str [params.marginMode]: only 'isolated' is supported, for spot-margin trading + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + until = self.safe_integer(params, 'until') + params = self.omit(params, 'until') + marketType, query = self.handle_market_type_and_params('fetchOrders', market, params) + if marketType == 'spot': + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrders() requires a symbol argument for spot market') + marginMode, queryInner = self.handle_margin_mode_and_params('fetchOrders', params) + if since is not None: + request['startTime'] = since + if until is not None: + request['endTime'] = until + if limit is not None: + request['limit'] = limit + response = None + if marginMode is not None: + if marginMode != 'isolated': + raise BadRequest(self.id + ' fetchOrders() does not support marginMode ' + marginMode + ' for spot-margin trading') + response = await self.spotPrivateGetMarginAllOrders(self.extend(request, queryInner)) + else: + response = await self.spotPrivateGetAllOrders(self.extend(request, queryInner)) + # + # spot + # + # [ + # { + # "symbol": "BTCUSDT", + # "orderId": "133949373632483328", + # "orderListId": "-1", + # "clientOrderId": null, + # "price": "45000", + # "origQty": "0.0002", + # "executedQty": "0", + # "cummulativeQuoteQty": "0", + # "status": "NEW", + # "timeInForce": null, + # "type": "LIMIT", + # "side": "SELL", + # "stopPrice": null, + # "icebergQty": null, + # "time": "1647718255000", + # "updateTime": "1647718255000", + # "isWorking": True, + # "origQuoteOrderQty": "9" + # }, + # ] + # + # margin + # + # [ + # { + # "symbol": "BTCUSDT", + # "orderId": "763307297891028992", + # "orderListId": "-1", + # "clientOrderId": null, + # "price": "18000", + # "origQty": "0.0014", + # "executedQty": "0", + # "cummulativeQuoteQty": "0", + # "status": "NEW", + # "type": "LIMIT", + # "side": "BUY", + # "isIsolated": True, + # "isWorking": True, + # "time": 1662153107000, + # "updateTime": 1662153107000 + # } + # ] + # + return self.parse_orders(response, market, since, limit) + else: + if since is not None: + request['start_time'] = since + end = self.safe_integer(params, 'end_time', until) + if end is None: + request['end_time'] = self.sum(since, self.options['maxTimeTillEnd']) + else: + if (end - since) > self.options['maxTimeTillEnd']: + raise BadRequest(self.id + ' end is invalid, i.e. exceeds allowed 90 days.') + else: + request['end_time'] = until + elif until is not None: + request['start_time'] = self.sum(until, self.options['maxTimeTillEnd'] * -1) + request['end_time'] = until + if limit is not None: + request['page_size'] = limit + method = self.safe_string(self.options, 'fetchOrders', 'contractPrivateGetOrderListHistoryOrders') + method = self.safe_string(query, 'method', method) + ordersOfRegular = [] + ordersOfTrigger = [] + if method == 'contractPrivateGetOrderListHistoryOrders': + response = await self.contractPrivateGetOrderListHistoryOrders(self.extend(request, query)) + # + # { + # "success": True, + # "code": "0", + # "data": [ + # { + # "orderId": "265230764677709315", + # "symbol": "STEPN_USDT", + # "positionId": "0", + # "price": "2.1", + # "vol": "102", + # "leverage": "20", + # "side": "1", + # "category": "1", + # "orderType": "1", + # "dealAvgPrice": "0", + # "dealVol": "0", + # "orderMargin": "10.96704", + # "takerFee": "0", + # "makerFee": "0", + # "profit": "0", + # "feeCurrency": "USDT", + # "openType": "1", + # "state": "2", + # "externalOid": "_m_7e42f8df6b324c869e4e200397e2b00f", + # "errorCode": "0", + # "usedMargin": "0", + # "createTime": "1648906342000", + # "updateTime": "1648906342000", + # "positionMode": "1" + # }, + # ] + # } + # + ordersOfRegular = self.safe_value(response, 'data') + else: + # the Planorder endpoints work not only for stop-market orders, but also for stop-limit orders that were supposed to have a separate endpoint + response = await self.contractPrivateGetPlanorderListOrders(self.extend(request, query)) + # + # { + # "success": True, + # "code": "0", + # "data": [ + # { + # "symbol": "STEPN_USDT", + # "leverage": "20", + # "side": "1", + # "vol": "13", + # "openType": "1", + # "state": "1", + # "orderType": "1", + # "errorCode": "0", + # "createTime": "1648984276000", + # "updateTime": "1648984276000", + # "id": "265557643326564352", + # "triggerType": "1", + # "triggerPrice": "3", + # "price": "2.9", # not present in stop-market, but in stop-limit order + # "executeCycle": "87600", + # "trend": "1", + # }, + # ] + # } + # + ordersOfTrigger = self.safe_value(response, 'data') + merged = self.array_concat(ordersOfTrigger, ordersOfRegular) + return self.parse_orders(merged, market, since, limit, params) + + async def fetch_orders_by_ids(self, ids, symbol: Str = None, params={}): + await self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + marketType, query = self.handle_market_type_and_params('fetchOrdersByIds', market, params) + if marketType == 'spot': + raise BadRequest(self.id + ' fetchOrdersByIds() is not supported for ' + marketType) + else: + request['order_ids'] = ','.join(ids) + response = await self.contractPrivateGetOrderBatchQuery(self.extend(request, query)) + # + # { + # "success": True, + # "code": "0", + # "data": [ + # { + # "orderId": "265230764677709315", + # "symbol": "STEPN_USDT", + # "positionId": "0", + # "price": "2.1", + # "vol": "102", + # "leverage": "20", + # "side": "1", + # "category": "1", + # "orderType": "1", + # "dealAvgPrice": "0", + # "dealVol": "0", + # "orderMargin": "10.96704", + # "takerFee": "0", + # "makerFee": "0", + # "profit": "0", + # "feeCurrency": "USDT", + # "openType": "1", + # "state": "2", + # "externalOid": "_m_7e42f8df6b324c869e4e200397e2b00f", + # "errorCode": "0", + # "usedMargin": "0", + # "createTime": "1648906342000", + # "updateTime": "1648906342000", + # "positionMode": "1" + # } + # ] + # } + # + data = self.safe_list(response, 'data') + return self.parse_orders(data, market) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#current-open-orders + https://mexcdevelop.github.io/apidocs/contract_v1_en/#get-all-of-the-user-39-s-historical-orders + https://mexcdevelop.github.io/apidocs/contract_v1_en/#gets-the-trigger-order-list + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: only 'isolated' is supported, for spot-margin trading + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + request: dict = {} + market = None + marketType = None + if symbol is not None: + market = self.market(symbol) + marketType, params = self.handle_market_type_and_params('fetchOpenOrders', market, params) + if marketType == 'spot': + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOpenOrders() requires a symbol argument for spot market') + request['symbol'] = market['id'] + marginMode, query = self.handle_margin_mode_and_params('fetchOpenOrders', params) + response = None + if marginMode is not None: + if marginMode != 'isolated': + raise BadRequest(self.id + ' fetchOpenOrders() does not support marginMode ' + marginMode + ' for spot-margin trading') + response = await self.spotPrivateGetMarginOpenOrders(self.extend(request, query)) + else: + response = await self.spotPrivateGetOpenOrders(self.extend(request, query)) + # + # spot + # + # [ + # { + # "symbol": "BTCUSDT", + # "orderId": "133949373632483328", + # "orderListId": "-1", + # "clientOrderId": "", + # "price": "45000", + # "origQty": "0.0002", + # "executedQty": "0", + # "cummulativeQuoteQty": "0", + # "status": "NEW", + # "timeInForce": null, + # "type": "LIMIT", + # "side": "SELL", + # "stopPrice": null, + # "icebergQty": null, + # "time": "1647718255199", + # "updateTime": null, + # "isWorking": True, + # "origQuoteOrderQty": "9" + # } + # ] + # + # margin + # + # [ + # { + # "symbol": "BTCUSDT", + # "orderId": "764547676405633024", + # "orderListId": "-1", + # "clientOrderId": null, + # "price": "18000", + # "origQty": "0.0013", + # "executedQty": "0", + # "cummulativeQuoteQty": "0", + # "status": "NEW", + # "type": "LIMIT", + # "side": "BUY", + # "isIsolated": True, + # "isWorking": True, + # "time": 1662448836000, + # "updateTime": 1662448836000 + # } + # ] + # + return self.parse_orders(response, market, since, limit) + else: + # TO_DO: another possible way is through: open_orders/{symbol}, but have same ratelimits, and less granularity, i think historical orders are more convenient, supports more params(however, theoretically, open-orders endpoint might be sligthly fast) + return await self.fetch_orders_by_state(2, symbol, since, limit, params) + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#all-orders + https://mexcdevelop.github.io/apidocs/contract_v1_en/#get-all-of-the-user-39-s-historical-orders + https://mexcdevelop.github.io/apidocs/contract_v1_en/#gets-the-trigger-order-list + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + return await self.fetch_orders_by_state(3, symbol, since, limit, params) + + async def fetch_canceled_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches information on multiple canceled orders made by the user + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#all-orders + https://mexcdevelop.github.io/apidocs/contract_v1_en/#get-all-of-the-user-39-s-historical-orders + https://mexcdevelop.github.io/apidocs/contract_v1_en/#gets-the-trigger-order-list + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: timestamp in ms of the earliest order, default is None + :param int [limit]: max number of orders to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `order structures ` + """ + return await self.fetch_orders_by_state(4, symbol, since, limit, params) + + async def fetch_orders_by_state(self, state, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + await self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + marketType = self.handle_market_type_and_params('fetchOrdersByState', market, params) + if marketType == 'spot': + raise NotSupported(self.id + ' fetchOrdersByState() is not supported for ' + marketType) + else: + request['states'] = state + return await self.fetch_orders(symbol, since, limit, self.extend(request, params)) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#cancel-order + https://mexcdevelop.github.io/apidocs/contract_v1_en/#cancel-the-order-under-maintenance + https://mexcdevelop.github.io/apidocs/contract_v1_en/#cancel-the-stop-limit-trigger-order-under-maintenance + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: only 'isolated' is supported for spot-margin trading + :returns dict: An `order structure ` + """ + await self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + marketType = None + marketType, params = self.handle_market_type_and_params('cancelOrder', market, params) + marginMode, query = self.handle_margin_mode_and_params('cancelOrder', params) + data = None + if marketType == 'spot': + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + requestInner: dict = { + 'symbol': market['id'], + } + clientOrderId = self.safe_string(params, 'clientOrderId') + if clientOrderId is not None: + params = self.omit(query, 'clientOrderId') + requestInner['origClientOrderId'] = clientOrderId + else: + requestInner['orderId'] = id + if marginMode is not None: + if marginMode != 'isolated': + raise BadRequest(self.id + ' cancelOrder() does not support marginMode ' + marginMode + ' for spot-margin trading') + data = await self.spotPrivateDeleteMarginOrder(self.extend(requestInner, query)) + else: + data = await self.spotPrivateDeleteOrder(self.extend(requestInner, query)) + # + # spot + # + # { + # "symbol": "BTCUSDT", + # "orderId": "133734823834447872", + # "price": "30000", + # "origQty": "0.0002", + # "type": "LIMIT", + # "side": "BUY" + # } + # + # margin + # + # [ + # { + # "symbol": "BTCUSDT", + # "orderId": "762640232574226432", + # "orderListId": "-1", + # "clientOrderId": null, + # "price": "18000", + # "origQty": "0.00147", + # "executedQty": "0", + # "cummulativeQuoteQty": "0", + # "status": "NEW", + # "type": "LIMIT", + # "side": "BUY", + # "isIsolated": True, + # "isWorking": True, + # "time": 1661994066000, + # "updateTime": 1661994066000 + # } + # ] + # + else: + # TODO: PlanorderCancel endpoint has bug atm. waiting for fix. + method = self.safe_string(self.options, 'cancelOrder', 'contractPrivatePostOrderCancel') # contractPrivatePostOrderCancel, contractPrivatePostPlanorderCancel + method = self.safe_string(query, 'method', method) + response = None + if method == 'contractPrivatePostOrderCancel': + response = await self.contractPrivatePostOrderCancel([id]) # the request cannot be changed or extended. This is the only way to send. + elif method == 'contractPrivatePostPlanorderCancel': + response = await self.contractPrivatePostPlanorderCancel([id]) # the request cannot be changed or extended. This is the only way to send. + else: + raise NotSupported(self.id + ' cancelOrder() not support self method') + # + # { + # "success": True, + # "code": "0", + # "data": [ + # { + # "orderId": "264995729269765120", + # "errorCode": "0", # if already canceled: "2041"; if doesn't exist: "2040" + # "errorMsg": "success", # if already canceled: "order state cannot be cancelled"; if doesn't exist: "order not exist" + # } + # ] + # } + # + data = self.safe_value(response, 'data') + order = self.safe_value(data, 0) + errorMsg = self.safe_value(order, 'errorMsg', '') + if errorMsg != 'success': + raise InvalidOrder(self.id + ' cancelOrder() the order with id ' + id + ' cannot be cancelled: ' + errorMsg) + return self.parse_order(data, market) + + async def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): + """ + cancel multiple orders + + https://mexcdevelop.github.io/apidocs/contract_v1_en/#cancel-the-order-under-maintenance + + :param str[] ids: order ids + :param str symbol: unified market symbol, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an list of `order structures ` + """ + await self.load_markets() + market = self.market(symbol) if (symbol is not None) else None + marketType = self.handle_market_type_and_params('cancelOrders', market, params) + if marketType == 'spot': + raise BadRequest(self.id + ' cancelOrders() is not supported for ' + marketType) + else: + response = await self.contractPrivatePostOrderCancel(ids) # the request cannot be changed or extended. The only way to send. + # + # { + # "success": True, + # "code": "0", + # "data": [ + # { + # "orderId": "264995729269765120", + # "errorCode": "0", # if already canceled: "2041" + # "errorMsg": "success", # if already canceled: "order state cannot be cancelled" + # }, + # ] + # } + # + data = self.safe_list(response, 'data') + return self.parse_orders(data, market) + + async def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#cancel-all-open-orders-on-a-symbol + https://mexcdevelop.github.io/apidocs/contract_v1_en/#cancel-all-orders-under-a-contract-under-maintenance + https://mexcdevelop.github.io/apidocs/contract_v1_en/#cancel-all-trigger-orders-under-maintenance + + :param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: only 'isolated' is supported for spot-margin trading + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + market = self.market(symbol) if (symbol is not None) else None + request: dict = {} + marketType = None + marketType, params = self.handle_market_type_and_params('cancelAllOrders', market, params) + marginMode, query = self.handle_margin_mode_and_params('cancelAllOrders', params) + if marketType == 'spot': + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelAllOrders() requires a symbol argument on spot') + request['symbol'] = market['id'] + response = None + if marginMode is not None: + if marginMode != 'isolated': + raise BadRequest(self.id + ' cancelAllOrders() does not support marginMode ' + marginMode + ' for spot-margin trading') + response = await self.spotPrivateDeleteMarginOpenOrders(self.extend(request, query)) + else: + response = await self.spotPrivateDeleteOpenOrders(self.extend(request, query)) + # + # spot + # + # [ + # { + # "symbol": "BTCUSDT", + # "orderId": "133926492139692032", + # "price": "30000", + # "origQty": "0.0002", + # "type": "LIMIT", + # "side": "BUY" + # }, + # ] + # + # margin + # + # [ + # { + # "symbol": "BTCUSDT", + # "orderId": "762640232574226432", + # "orderListId": "-1", + # "clientOrderId": null, + # "price": "18000", + # "origQty": "0.00147", + # "executedQty": "0", + # "cummulativeQuoteQty": "0", + # "status": "NEW", + # "type": "LIMIT", + # "side": "BUY", + # "isIsolated": True, + # "isWorking": True, + # "time": 1661994066000, + # "updateTime": 1661994066000 + # } + # ] + # + return self.parse_orders(response, market) + else: + if symbol is not None: + request['symbol'] = market['id'] + # method can be either: contractPrivatePostOrderCancelAll or contractPrivatePostPlanorderCancelAll + # the Planorder endpoints work not only for stop-market orders but also for stop-limit orders that are supposed to have separate endpoint + method = self.safe_string(self.options, 'cancelAllOrders', 'contractPrivatePostOrderCancelAll') + method = self.safe_string(query, 'method', method) + response = None + if method == 'contractPrivatePostOrderCancelAll': + response = await self.contractPrivatePostOrderCancelAll(self.extend(request, query)) + elif method == 'contractPrivatePostPlanorderCancelAll': + response = await self.contractPrivatePostPlanorderCancelAll(self.extend(request, query)) + # + # { + # "success": True, + # "code": "0" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_orders(data, market) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # spot + # createOrder + # + # { + # "symbol": "FARTCOINUSDT", + # "orderId": "C02__342252993005723644225", + # "orderListId": "-1", + # "price": "1.1", + # "origQty": "6.3", + # "type": "IMMEDIATE_OR_CANCEL", + # "side": "SELL", + # "transactTime": "1745852205223" + # } + # + # unknown endpoint on spot + # + # { + # "symbol": "BTCUSDT", + # "orderId": "123738410679123456", + # "orderListId": -1 + # } + # + # margin: createOrder + # + # { + # "symbol": "BTCUSDT", + # "orderId": "762634301354414080", + # "clientOrderId": null, + # "isIsolated": True, + # "transactTime": 1661992652132 + # } + # + # spot: cancelOrder, cancelAllOrders + # + # { + # "symbol": "BTCUSDT", + # "orderId": "133926441921286144", + # "price": "30000", + # "origQty": "0.0002", + # "type": "LIMIT", + # "side": "BUY" + # } + # + # margin: cancelOrder, cancelAllOrders + # + # { + # "symbol": "BTCUSDT", + # "orderId": "762640232574226432", + # "orderListId": "-1", + # "clientOrderId": null, + # "price": "18000", + # "origQty": "0.00147", + # "executedQty": "0", + # "cummulativeQuoteQty": "0", + # "status": "NEW", + # "type": "LIMIT", + # "side": "BUY", + # "isIsolated": True, + # "isWorking": True, + # "time": 1661994066000, + # "updateTime": 1661994066000 + # } + # + # spot: fetchOrder, fetchOpenOrders, fetchOrders + # + # { + # "symbol": "BTCUSDT", + # "orderId": "133734823834147272", + # "orderListId": "-1", + # "clientOrderId": null, + # "price": "30000", + # "origQty": "0.0002", + # "executedQty": "0", + # "cummulativeQuoteQty": "0", + # "status": "CANCELED", + # "timeInForce": null, + # "type": "LIMIT", + # "side": "BUY", + # "stopPrice": null, + # "icebergQty": null, + # "time": "1647667102000", + # "updateTime": "1647708567000", + # "isWorking": True, + # "origQuoteOrderQty": "6" + # } + # + # margin: fetchOrder, fetchOrders + # + # { + # "symbol": "BTCUSDT", + # "orderId": "763307297891028992", + # "orderListId": "-1", + # "clientOrderId": null, + # "price": "18000", + # "origQty": "0.0014", + # "executedQty": "0", + # "cummulativeQuoteQty": "0", + # "status": "NEW", + # "type": "LIMIT", + # "side": "BUY", + # "isIsolated": True, + # "isWorking": True, + # "time": 1662153107000, + # "updateTime": 1662153107000 + # } + # + # swap: createOrder + # + # 2ff3163e8617443cb9c6fc19d42b1ca4 + # + # swap: fetchOrder, fetchOrders + # + # regular + # { + # "orderId": "264995729269765120", + # "symbol": "STEPN_USDT", + # "positionId": "0", + # "price": "2.2", + # "vol": "15", + # "leverage": "20", + # "side": "1", # TODO: not unified + # "category": "1", + # "orderType": "1", # TODO: not unified + # "dealAvgPrice": "0", + # "dealVol": "0", + # "orderMargin": "2.2528", + # "takerFee": "0", + # "makerFee": "0", + # "profit": "0", + # "feeCurrency": "USDT", + # "openType": "1", + # "state": "2", # TODO + # "externalOid": "_m_0e9520c256744d64b942985189026d20", + # "errorCode": "0", + # "usedMargin": "0", + # "createTime": "1648850305236", + # "updateTime": "1648850305245", + # "positionMode": "1" + # } + # + # stop + # { + # "id": "265557643326564352", + # "triggerType": "1", + # "triggerPrice": "3", + # "price": "2.9", # not present in stop-market, but in stop-limit order + # "executeCycle": "87600", + # "trend": "1", + # # below keys are same regular order structure + # "symbol": "STEPN_USDT", + # "leverage": "20", + # "side": "1", + # "vol": "13", + # "openType": "1", + # "state": "1", + # "orderType": "1", + # "errorCode": "0", + # "createTime": "1648984276000", + # "updateTime": "1648984276000", + # } + # + # createOrders error + # + # { + # "newClientOrderId": "123456", + # "msg": "The minimum transaction volume cannot be less than:0.5USDT", + # "code": 30002 + # } + # + code = self.safe_integer(order, 'code') + if code is not None: + # error upon placing multiple orders + return self.safe_order({ + 'info': order, + 'status': 'rejected', + 'clientOrderId': self.safe_string(order, 'newClientOrderId'), + }) + id = None + if isinstance(order, str): + id = order + else: + id = self.safe_string_2(order, 'orderId', 'id') + timeInForce = self.parse_order_time_in_force(self.safe_string(order, 'timeInForce')) + typeRaw = self.safe_string(order, 'type') + if timeInForce is None: + timeInForce = self.get_tif_from_raw_order_type(typeRaw) + marketId = self.safe_string(order, 'symbol') + market = self.safe_market(marketId, market) + timestamp = self.safe_integer_n(order, ['time', 'createTime', 'transactTime']) + fee = None + feeCurrency = self.safe_string(order, 'feeCurrency') + if feeCurrency is not None: + takerFee = self.safe_string(order, 'takerFee') + makerFee = self.safe_string(order, 'makerFee') + feeSum = Precise.string_add(takerFee, makerFee) + fee = { + 'currency': feeCurrency, + 'cost': self.parse_number(feeSum), + } + return self.safe_order({ + 'id': id, + 'clientOrderId': self.safe_string(order, 'clientOrderId'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'lastUpdateTimestamp': self.safe_integer(order, 'updateTime'), + 'status': self.parse_order_status(self.safe_string_2(order, 'status', 'state')), + 'symbol': market['symbol'], + 'type': self.parse_order_type(typeRaw), + 'timeInForce': timeInForce, + 'side': self.parse_order_side(self.safe_string(order, 'side')), + 'price': self.safe_number(order, 'price'), + 'triggerPrice': self.safe_number_2(order, 'stopPrice', 'triggerPrice'), + 'average': self.safe_number(order, 'dealAvgPrice'), + 'amount': self.safe_number_2(order, 'origQty', 'vol'), + 'cost': self.safe_number(order, 'cummulativeQuoteQty'), # 'cummulativeQuoteQty' vs 'origQuoteOrderQty' + 'filled': self.safe_number_2(order, 'executedQty', 'dealVol'), + 'remaining': None, + 'fee': fee, + 'trades': None, + 'info': order, + }, market) + + def parse_order_side(self, status): + statuses: dict = { + 'BUY': 'buy', + 'SELL': 'sell', + '1': 'buy', + '2': 'sell', + # contracts v1 : TODO + } + return self.safe_string(statuses, status, status) + + def parse_order_type(self, status): + statuses: dict = { + 'MARKET': 'market', + 'LIMIT': 'limit', + 'LIMIT_MAKER': 'limit', + # on spot, during submission below types are used only accepted order + 'IMMEDIATE_OR_CANCEL': 'limit', + 'FILL_OR_KILL': 'limit', + } + return self.safe_string(statuses, status, status) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'NEW': 'open', + 'FILLED': 'closed', + 'CANCELED': 'canceled', + 'PARTIALLY_FILLED': 'open', + 'PARTIALLY_CANCELED': 'canceled', + # contracts v1 + # '1': 'uninformed', # TODO: wt? + '2': 'open', + '3': 'closed', + '4': 'canceled', + # '5': 'invalid', # TODO: wt? + } + return self.safe_string(statuses, status, status) + + def parse_order_time_in_force(self, status): + statuses: dict = { + 'GTC': 'GTC', + 'FOK': 'FOK', + 'IOC': 'IOC', + } + return self.safe_string(statuses, status, status) + + def get_tif_from_raw_order_type(self, orderType: Str = None): + statuses: dict = { + 'LIMIT': 'GTC', + 'LIMIT_MAKER': 'POST_ONLY', + 'IMMEDIATE_OR_CANCEL': 'IOC', + 'FILL_OR_KILL': 'FOK', + 'MARKET': 'IOC', + } + return self.safe_string(statuses, orderType, orderType) + + async def fetch_account_helper(self, type, params): + if type == 'spot': + return await self.spotPrivateGetAccount(params) + # + # { + # "makerCommission": "20", + # "takerCommission": "20", + # "buyerCommission": "0", + # "sellerCommission": "0", + # "canTrade": True, + # "canWithdraw": True, + # "canDeposit": True, + # "updateTime": null, + # "accountType": "SPOT", + # "balances": [ + # { + # "asset": "BTC", + # "free": "0.002", + # "locked": "0" + # }, + # { + # "asset": "USDT", + # "free": "88.120131350620957006", + # "locked": "0" + # }, + # ], + # "permissions": [ + # "SPOT" + # ] + # } + # + elif type == 'swap': + response = await self.contractPrivateGetAccountAssets(params) + # + # { + # "success":true, + # "code":0, + # "data":[ + # { + # "currency":"BSV", + # "positionMargin":0, + # "availableBalance":0, + # "cashBalance":0, + # "frozenBalance":0, + # "equity":0, + # "unrealized":0, + # "bonus":0 + # }, + # ] + # } + # + return self.safe_value(response, 'data') + return None + + async def fetch_accounts(self, params={}) -> List[Account]: + """ + fetch all the accounts associated with a profile + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#account-information + https://mexcdevelop.github.io/apidocs/contract_v1_en/#get-all-informations-of-user-39-s-asset + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `account structures ` indexed by the account type + """ + # TODO: is the below endpoints suitable for fetchAccounts? + marketType, query = self.handle_market_type_and_params('fetchAccounts', None, params) + await self.load_markets() + response = await self.fetch_account_helper(marketType, query) + data = self.safe_value(response, 'balances', []) + result = [] + for i in range(0, len(data)): + account = data[i] + currencyId = self.safe_string_2(account, 'asset', 'currency') + code = self.safe_currency_code(currencyId) + result.append({ + 'id': self.safe_string(account, 'id'), + 'type': self.safe_string(account, 'type'), + 'code': code, + 'info': account, + }) + return result + + async def fetch_trading_fee(self, symbol: str, params={}) -> TradingFeeInterface: + """ + fetch the trading fees for a market + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#query-mx-deduct-status + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `fee structure ` + """ + await self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise BadRequest(self.id + ' fetchTradingFee() supports spot markets only') + request: dict = { + 'symbol': market['id'], + } + response = await self.spotPrivateGetTradeFee(self.extend(request, params)) + # + # { + # "data":{ + # "makerCommission":0.003000000000000000, + # "takerCommission":0.003000000000000000 + # }, + # "code":0, + # "msg":"success", + # "timestamp":1669109672717 + # } + # + data = self.safe_dict(response, 'data', {}) + return { + 'info': data, + 'symbol': symbol, + 'maker': self.safe_number(data, 'makerCommission'), + 'taker': self.safe_number(data, 'takerCommission'), + 'percentage': None, + 'tierBased': None, + } + + def custom_parse_balance(self, response, marketType) -> Balances: + # + # spot + # + # { + # "asset": "USDT", + # "free": "0.000000000674", + # "locked": "0" + # } + # + # swap + # + # { + # "currency": "BSV", + # "positionMargin": 0, + # "availableBalance": 0, + # "cashBalance": 0, + # "frozenBalance": 0, + # "equity": 0, + # "unrealized": 0, + # "bonus": 0 + # } + # + # margin + # + # { + # "baseAsset": { + # "asset": "BTC", + # "borrowEnabled": True, + # "borrowed": "0", + # "free": "0", + # "interest": "0", + # "locked": "0", + # "netAsset": "0", + # "netAssetOfBtc": "0", + # "repayEnabled": True, + # "totalAsset": "0" + # } + # "quoteAsset": { + # "asset": "USDT", + # "borrowEnabled": True, + # "borrowed": "0", + # "free": "10", + # "interest": "0", + # "locked": "0", + # "netAsset": "10", + # "netAssetOfBtc": "0", + # "repayEnabled": True, + # "totalAsset": "10" + # } + # "symbol": "BTCUSDT", + # "isolatedCreated": True, + # "enabled": True, + # "marginLevel": "999", + # "marginRatio": "9", + # "indexPrice": "16741.137068965517241379", + # "liquidatePrice": "--", + # "liquidateRate": "--", + # "tradeEnabled": True + # } + # + wallet = None + if marketType == 'margin': + wallet = self.safe_value(response, 'assets', []) + elif marketType == 'swap': + wallet = self.safe_value(response, 'data', []) + else: + wallet = self.safe_value(response, 'balances', []) + result = {'info': response} + if marketType == 'margin': + for i in range(0, len(wallet)): + entry = wallet[i] + marketId = self.safe_string(entry, 'symbol') + symbol = self.safe_symbol(marketId, None) + base = self.safe_value(entry, 'baseAsset', {}) + quote = self.safe_value(entry, 'quoteAsset', {}) + baseCode = self.safe_currency_code(self.safe_string(base, 'asset')) + quoteCode = self.safe_currency_code(self.safe_string(quote, 'asset')) + subResult: dict = {} + subResult[baseCode] = self.parse_balance_helper(base) + subResult[quoteCode] = self.parse_balance_helper(quote) + result[symbol] = self.safe_balance(subResult) + return result + elif marketType == 'swap': + for i in range(0, len(wallet)): + entry = wallet[i] + currencyId = self.safe_string(entry, 'currency') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(entry, 'availableBalance') + account['used'] = self.safe_string(entry, 'frozenBalance') + result[code] = account + return self.safe_balance(result) + else: + for i in range(0, len(wallet)): + entry = wallet[i] + currencyId = self.safe_string(entry, 'asset') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(entry, 'free') + account['used'] = self.safe_string(entry, 'locked') + result[code] = account + return self.safe_balance(result) + + def parse_balance_helper(self, entry): + account = self.account() + account['used'] = self.safe_string(entry, 'locked') + account['free'] = self.safe_string(entry, 'free') + account['total'] = self.safe_string(entry, 'totalAsset') + debt = self.safe_string(entry, 'borrowed') + interest = self.safe_string(entry, 'interest') + account['debt'] = Precise.string_add(debt, interest) + return account + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#account-information + https://mexcdevelop.github.io/apidocs/contract_v1_en/#get-all-informations-of-user-39-s-asset + https://mexcdevelop.github.io/apidocs/spot_v3_en/#isolated-account + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.symbols]: # required for margin, market id's separated by commas + :returns dict: a `balance structure ` + """ + await self.load_markets() + marketType = None + request: dict = {} + marketType, params = self.handle_market_type_and_params('fetchBalance', None, params) + marginMode = self.safe_string(params, 'marginMode') + isMargin = self.safe_bool(params, 'margin', False) + params = self.omit(params, ['margin', 'marginMode']) + response = None + if (marginMode is not None) or (isMargin) or (marketType == 'margin'): + parsedSymbols = None + symbol = self.safe_string(params, 'symbol') + if symbol is None: + symbols = self.safe_value(params, 'symbols') + if symbols is not None: + parsedSymbols = ','.join(self.market_ids(symbols)) + else: + market = self.market(symbol) + parsedSymbols = market['id'] + self.check_required_argument('fetchBalance', parsedSymbols, 'symbol or symbols') + marketType = 'margin' + request['symbols'] = parsedSymbols + params = self.omit(params, ['symbol', 'symbols']) + response = await self.spotPrivateGetMarginIsolatedAccount(self.extend(request, params)) + elif marketType == 'spot': + response = await self.spotPrivateGetAccount(self.extend(request, params)) + elif marketType == 'swap': + response = await self.contractPrivateGetAccountAssets(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchBalance() not support self method') + # + # spot + # + # { + # "makerCommission": 0, + # "takerCommission": 20, + # "buyerCommission": 0, + # "sellerCommission": 0, + # "canTrade": True, + # "canWithdraw": True, + # "canDeposit": True, + # "updateTime": null, + # "accountType": "SPOT", + # "balances": [ + # { + # "asset": "USDT", + # "free": "0.000000000674", + # "locked": "0" + # }, + # ], + # "permissions": ["SPOT"] + # } + # + # swap + # + # { + # "success": True, + # "code": 0, + # "data": [ + # { + # "currency": "BSV", + # "positionMargin": 0, + # "availableBalance": 0, + # "cashBalance": 0, + # "frozenBalance": 0, + # "equity": 0, + # "unrealized": 0, + # "bonus": 0 + # }, + # ] + # } + # + # margin + # + # { + # "assets": [ + # { + # "baseAsset": { + # "asset": "BTC", + # "borrowEnabled": True, + # "borrowed": "0", + # "free": "0", + # "interest": "0", + # "locked": "0", + # "netAsset": "0", + # "netAssetOfBtc": "0", + # "repayEnabled": True, + # "totalAsset": "0" + # }, + # "quoteAsset": { + # "asset": "USDT", + # "borrowEnabled": True, + # "borrowed": "0", + # "free": "10", + # "interest": "0", + # "locked": "0", + # "netAsset": "10", + # "netAssetOfBtc": "0", + # "repayEnabled": True, + # "totalAsset": "10" + # }, + # "symbol": "BTCUSDT", + # "isolatedCreated": True, + # "enabled": True, + # "marginLevel": "999", + # "marginRatio": "9", + # "indexPrice": "16741.137068965517241379", + # "liquidatePrice": "--", + # "liquidateRate": "--", + # "tradeEnabled": True + # } + # ] + # } + # + return self.custom_parse_balance(response, marketType) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#account-trade-list + https://mexcdevelop.github.io/apidocs/contract_v1_en/#get-all-transaction-details-of-the-user-s-order + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch trades for + :returns Trade[]: a list of `trade structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMyTrades() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + marketType: Str = None + marketType, params = self.handle_market_type_and_params('fetchMyTrades', market, params) + request: dict = { + 'symbol': market['id'], + } + trades = None + if marketType == 'spot': + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + until = self.safe_integer(params, 'until') + if until is not None: + params = self.omit(params, 'until') + request['endTime'] = until + trades = await self.spotPrivateGetMyTrades(self.extend(request, params)) + # + # spot + # + # [ + # { + # "symbol": "BTCUSDT", + # "id": "133948532984922113", + # "orderId": "133948532531949568", + # "orderListId": "-1", + # "price": "41995.51", + # "qty": "0.0002", + # "quoteQty": "8.399102", + # "commission": "0.016798204", + # "commissionAsset": "USDT", + # "time": "1647718055000", + # "isBuyer": True, + # "isMaker": False, + # "isBestMatch": True + # } + # ] + # + else: + if since is not None: + request['start_time'] = since + end = self.safe_integer(params, 'end_time') + if end is None: + request['end_time'] = self.sum(since, self.options['maxTimeTillEnd']) + if limit is not None: + request['page_size'] = limit + response = await self.contractPrivateGetOrderListOrderDeals(self.extend(request, params)) + # + # { + # "success": True, + # "code": "0", + # "data": [ + # { + # "id": "299444585", + # "symbol": "STEPN_USDT", + # "side": "1", + # "vol": "1", + # "price": "2.45455", + # "feeCurrency": "USDT", + # "fee": "0.00147273", + # "timestamp": "1648924557000", + # "profit": "0", + # "category": "1", + # "orderId": "265307163526610432", + # "positionMode": "1", + # "taker": True + # } + # ] + # } + # + trades = self.safe_value(response, 'data') + return self.parse_trades(trades, market, since, limit) + + async def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all the trades made from a single order + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#account-trade-list + https://mexcdevelop.github.io/apidocs/contract_v1_en/#query-the-order-based-on-the-order-number + + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + marketType, query = self.handle_market_type_and_params('fetchOrderTrades', market, params) + trades = None + if marketType == 'spot': + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrderTrades() requires a symbol argument') + request['symbol'] = market['id'] + request['orderId'] = id + trades = await self.spotPrivateGetMyTrades(self.extend(request, query)) + # + # spot + # + # [ + # { + # "symbol": "BTCUSDT", + # "id": "133948532984922113", + # "orderId": "133948532531949568", + # "orderListId": "-1", + # "price": "41995.51", + # "qty": "0.0002", + # "quoteQty": "8.399102", + # "commission": "0.016798204", + # "commissionAsset": "USDT", + # "time": "1647718055000", + # "isBuyer": True, + # "isMaker": False, + # "isBestMatch": True + # } + # ] + # + else: + request['order_id'] = id + response = await self.contractPrivateGetOrderDealDetailsOrderId(self.extend(request, query)) + # + # { + # "success": True, + # "code": "0", + # "data": [ + # { + # "id": "299444585", + # "symbol": "STEPN_USDT", + # "side": "1", + # "vol": "1", + # "price": "2.45455", + # "feeCurrency": "USDT", + # "fee": "0.00147273", + # "timestamp": "1648924557000", + # "profit": "0", + # "category": "1", + # "orderId": "265307163526610432", + # "positionMode": "1", + # "taker": True + # } + # ] + # } + # + trades = self.safe_value(response, 'data') + return self.parse_trades(trades, market, since, limit, query) + + async def modify_margin_helper(self, symbol: str, amount, addOrReduce, params={}): + positionId = self.safe_integer(params, 'positionId') + if positionId is None: + raise ArgumentsRequired(self.id + ' modifyMarginHelper() requires a positionId parameter') + await self.load_markets() + request: dict = { + 'positionId': positionId, + 'amount': amount, + 'type': addOrReduce, + } + response = await self.contractPrivatePostPositionChangeMargin(self.extend(request, params)) + # + # { + # "success": True, + # "code": 0 + # } + return response + + async def reduce_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + remove margin from a position + + https://mexcdevelop.github.io/apidocs/contract_v1_en/#increase-or-decrease-margin + + :param str symbol: unified market symbol + :param float amount: the amount of margin to remove + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin structure ` + """ + return await self.modify_margin_helper(symbol, amount, 'SUB', params) + + async def add_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + add margin + + https://mexcdevelop.github.io/apidocs/contract_v1_en/#increase-or-decrease-margin + + :param str symbol: unified market symbol + :param float amount: amount of margin to add + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin structure ` + """ + return await self.modify_margin_helper(symbol, amount, 'ADD', params) + + async def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://mexcdevelop.github.io/apidocs/contract_v1_en/#switch-leverage + + :param float leverage: the rate of leverage + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + await self.load_markets() + request: dict = { + 'leverage': leverage, + } + positionId = self.safe_integer(params, 'positionId') + if positionId is None: + openType = self.safe_number(params, 'openType') # 1 or 2 + positionType = self.safe_number(params, 'positionType') # 1 or 2 + market = self.market(symbol) if (symbol is not None) else None + if (openType is None) or (positionType is None) or (market is None): + raise ArgumentsRequired(self.id + ' setLeverage() requires a positionId parameter or a symbol argument with openType and positionType parameters, use openType 1 or 2 for isolated or cross margin respectively, use positionType 1 or 2 for long or short positions') + else: + request['openType'] = openType + request['symbol'] = market['id'] + request['positionType'] = positionType + else: + request['positionId'] = positionId + return await self.contractPrivatePostPositionChangeLeverage(self.extend(request, params)) + + async def fetch_funding_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch the history of funding payments paid and received on self account + + https://mexcdevelop.github.io/apidocs/contract_v1_en/#get-details-of-user-s-funding-rate + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch funding history for + :param int [limit]: the maximum number of funding history structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding history structure ` + """ + await self.load_markets() + market = None + request: dict = { + # 'symbol': market['id'], + # 'position_id': positionId, + # 'page_num': 1, + # 'page_size': limit, # default 20, max 100 + } + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if limit is not None: + request['page_size'] = limit + response = await self.contractPrivateGetPositionFundingRecords(self.extend(request, params)) + # + # { + # "success": True, + # "code": 0, + # "data": { + # "pageSize": 20, + # "totalCount": 2, + # "totalPage": 1, + # "currentPage": 1, + # "resultList": [ + # { + # "id": 7423910, + # "symbol": "BTC_USDT", + # "positionType": 1, + # "positionValue": 29.30024, + # "funding": 0.00076180624, + # "rate": -0.000026, + # "settleTime": 1643299200000 + # }, + # { + # "id": 7416473, + # "symbol": "BTC_USDT", + # "positionType": 1, + # "positionValue": 28.9188, + # "funding": 0.0014748588, + # "rate": -0.000051, + # "settleTime": 1643270400000 + # } + # ] + # } + # } + # + data = self.safe_value(response, 'data', {}) + resultList = self.safe_value(data, 'resultList', []) + result = [] + for i in range(0, len(resultList)): + entry = resultList[i] + timestamp = self.safe_integer(entry, 'settleTime') + result.append({ + 'info': entry, + 'symbol': symbol, + 'code': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': self.safe_number(entry, 'id'), + 'amount': self.safe_number(entry, 'funding'), + }) + return result + + def parse_funding_rate(self, contract, market: Market = None) -> FundingRate: + # + # { + # "symbol": "BTC_USDT", + # "fundingRate": 0.000014, + # "maxFundingRate": 0.003, + # "minFundingRate": -0.003, + # "collectCycle": 8, + # "nextSettleTime": 1643241600000, + # "timestamp": 1643240373359 + # } + # + nextFundingRate = self.safe_number(contract, 'fundingRate') + nextFundingTimestamp = self.safe_integer(contract, 'nextSettleTime') + marketId = self.safe_string(contract, 'symbol') + symbol = self.safe_symbol(marketId, market, None, 'contract') + timestamp = self.safe_integer(contract, 'timestamp') + interval = self.safe_string(contract, 'collectCycle') + intervalString = None + if interval is not None: + intervalString = interval + 'h' + return { + 'info': contract, + 'symbol': symbol, + 'markPrice': None, + 'indexPrice': None, + 'interestRate': None, + 'estimatedSettlePrice': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'fundingRate': nextFundingRate, + 'fundingTimestamp': nextFundingTimestamp, + 'fundingDatetime': self.iso8601(nextFundingTimestamp), + 'nextFundingRate': None, + 'nextFundingTimestamp': None, + 'nextFundingDatetime': None, + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': intervalString, + } + + async def fetch_funding_interval(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate interval + + https://mexcdevelop.github.io/apidocs/contract_v1_en/#get-contract-funding-rate + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + return await self.fetch_funding_rate(symbol, params) + + async def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate + + https://mexcdevelop.github.io/apidocs/contract_v1_en/#get-contract-funding-rate + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = await self.contractPublicGetFundingRateSymbol(self.extend(request, params)) + # + # { + # "success": True, + # "code": 0, + # "data": { + # "symbol": "BTC_USDT", + # "fundingRate": 0.000014, + # "maxFundingRate": 0.003, + # "minFundingRate": -0.003, + # "collectCycle": 8, + # "nextSettleTime": 1643241600000, + # "timestamp": 1643240373359 + # } + # } + # + result = self.safe_value(response, 'data', {}) + return self.parse_funding_rate(result, market) + + async def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical funding rate prices + + https://mexcdevelop.github.io/apidocs/contract_v1_en/#get-contract-funding-rate-history + + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: not used by mexc, but filtered internally by ccxt + :param int [limit]: mexc limit is page_size default 20, maximum is 100 + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `funding rate structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchFundingRateHistory() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + # 'page_size': limit, # optional + # 'page_num': 1, # optional, current page number, default is 1 + } + if limit is not None: + request['page_size'] = limit + response = await self.contractPublicGetFundingRateHistory(self.extend(request, params)) + # + # { + # "success": True, + # "code": 0, + # "data": { + # "pageSize": 2, + # "totalCount": 21, + # "totalPage": 11, + # "currentPage": 1, + # "resultList": [ + # { + # "symbol": "BTC_USDT", + # "fundingRate": 0.000266, + # "settleTime": 1609804800000 + # }, + # { + # "symbol": "BTC_USDT", + # "fundingRate": 0.00029, + # "settleTime": 1609776000000 + # } + # ] + # } + # } + # + data = self.safe_value(response, 'data') + result = self.safe_value(data, 'resultList', []) + rates = [] + for i in range(0, len(result)): + entry = result[i] + marketId = self.safe_string(entry, 'symbol') + symbolInner = self.safe_symbol(marketId) + timestamp = self.safe_integer(entry, 'settleTime') + rates.append({ + 'info': entry, + 'symbol': symbolInner, + 'fundingRate': self.safe_number(entry, 'fundingRate'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + sorted = self.sort_by(rates, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, market['symbol'], since, limit) + + async def fetch_leverage_tiers(self, symbols: Strings = None, params={}) -> LeverageTiers: + """ + retrieve information on the maximum leverage, and maintenance margin for trades of varying trade sizes, if a market has a leverage tier of 0, then the leverage tiers cannot be obtained for self market + + https://mexcdevelop.github.io/apidocs/contract_v1_en/#get-the-contract-information + + :param str[] [symbols]: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `leverage tiers structures `, indexed by market symbols + """ + await self.load_markets() + symbols = self.market_symbols(symbols, 'swap', True, True) + response = await self.contractPublicGetDetail(params) + # + # { + # "success":true, + # "code":0, + # "data":[ + # { + # "symbol": "BTC_USDT", + # "displayName": "BTC_USDT永续", + # "displayNameEn": "BTC_USDT SWAP", + # "positionOpenType": 3, + # "baseCoin": "BTC", + # "quoteCoin": "USDT", + # "settleCoin": "USDT", + # "contractSize": 0.0001, + # "minLeverage": 1, + # "maxLeverage": 125, + # "priceScale": 2, + # "volScale": 0, + # "amountScale": 4, + # "priceUnit": 0.5, + # "volUnit": 1, + # "minVol": 1, + # "maxVol": 1000000, + # "bidLimitPriceRate": 0.1, + # "askLimitPriceRate": 0.1, + # "takerFeeRate": 0.0006, + # "makerFeeRate": 0.0002, + # "maintenanceMarginRate": 0.004, + # "initialMarginRate": 0.008, + # "riskBaseVol": 10000, + # "riskIncrVol": 200000, + # "riskIncrMmr": 0.004, + # "riskIncrImr": 0.004, + # "riskLevelLimit": 5, + # "priceCoefficientVariation": 0.1, + # "indexOrigin": ["BINANCE","GATEIO","HUOBI","MXC"], + # "state": 0, # 0 enabled, 1 delivery, 2 completed, 3 offline, 4 pause + # "isNew": False, + # "isHot": True, + # "isHidden": False + # }, + # ... + # ] + # } + # + data = self.safe_list(response, 'data') + return self.parse_leverage_tiers(data, symbols, 'symbol') + + def parse_market_leverage_tiers(self, info, market: Market = None) -> List[LeverageTier]: + # + # { + # "symbol": "BTC_USDT", + # "displayName": "BTC_USDT永续", + # "displayNameEn": "BTC_USDT SWAP", + # "positionOpenType": 3, + # "baseCoin": "BTC", + # "quoteCoin": "USDT", + # "settleCoin": "USDT", + # "contractSize": 0.0001, + # "minLeverage": 1, + # "maxLeverage": 125, + # "priceScale": 2, + # "volScale": 0, + # "amountScale": 4, + # "priceUnit": 0.5, + # "volUnit": 1, + # "minVol": 1, + # "maxVol": 1000000, + # "bidLimitPriceRate": 0.1, + # "askLimitPriceRate": 0.1, + # "takerFeeRate": 0.0006, + # "makerFeeRate": 0.0002, + # "maintenanceMarginRate": 0.004, + # "initialMarginRate": 0.008, + # "riskBaseVol": 10000, + # "riskIncrVol": 200000, + # "riskIncrMmr": 0.004, + # "riskIncrImr": 0.004, + # "riskLevelLimit": 5, + # "priceCoefficientVariation": 0.1, + # "indexOrigin": ["BINANCE","GATEIO","HUOBI","MXC"], + # "state": 0, # 0 enabled, 1 delivery, 2 completed, 3 offline, 4 pause + # "isNew": False, + # "isHot": True, + # "isHidden": False + # } + # + marketId = self.safe_string(info, 'symbol') + maintenanceMarginRate = self.safe_string(info, 'maintenanceMarginRate') + initialMarginRate = self.safe_string(info, 'initialMarginRate') + maxVol = self.safe_string(info, 'maxVol') + riskIncrVol = self.safe_string(info, 'riskIncrVol') + riskIncrMmr = self.safe_string(info, 'riskIncrMmr') + riskIncrImr = self.safe_string(info, 'riskIncrImr') + floor = '0' + tiers = [] + quoteId = self.safe_string(info, 'quoteCoin') + if riskIncrVol == '0': + return [ + { + 'tier': 0, + 'symbol': self.safe_symbol(marketId, market, None, 'contract'), + 'currency': self.safe_currency_code(quoteId), + 'minNotional': None, + 'maxNotional': None, + 'maintenanceMarginRate': None, + 'maxLeverage': self.safe_number(info, 'maxLeverage'), + 'info': info, + }, + ] + while(Precise.string_lt(floor, maxVol)): + cap = Precise.string_add(floor, riskIncrVol) + tiers.append({ + 'tier': self.parse_number(Precise.string_div(cap, riskIncrVol)), + 'symbol': self.safe_symbol(marketId, market, None, 'contract'), + 'currency': self.safe_currency_code(quoteId), + 'minNotional': self.parse_number(floor), + 'maxNotional': self.parse_number(cap), + 'maintenanceMarginRate': self.parse_number(maintenanceMarginRate), + 'maxLeverage': self.parse_number(Precise.string_div('1', initialMarginRate)), + 'info': info, + }) + initialMarginRate = Precise.string_add(initialMarginRate, riskIncrImr) + maintenanceMarginRate = Precise.string_add(maintenanceMarginRate, riskIncrMmr) + floor = cap + return tiers + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + # + # { + # coin: "USDT", + # network: "BNB Smart Chain(BEP20)", + # address: "0x0d48003e0c27c5de62b97c9b4cdb31fdd29da619", + # memo: null + # } + # + address = self.safe_string(depositAddress, 'address') + currencyId = self.safe_string(depositAddress, 'coin') + networkId = self.safe_string(depositAddress, 'netWork') + self.check_address(address) + return { + 'info': depositAddress, + 'currency': self.safe_currency_code(currencyId, currency), + 'network': self.network_id_to_code(networkId, currencyId), + 'address': address, + 'tag': self.safe_string(depositAddress, 'memo'), + } + + async def fetch_deposit_addresses_by_network(self, code: str, params={}) -> List[DepositAddress]: + """ + fetch a dictionary of addresses for a currency, indexed by network + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#deposit-address-supporting-network + + :param str code: unified currency code of the currency for the deposit address + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `address structures ` indexed by the network + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'coin': currency['id'], + } + networkCode = self.safe_string(params, 'network') + networkId = None + if networkCode is not None: + # createDepositAddress and fetchDepositAddress use a different network-id compared to withdraw + networkUnified = self.network_id_to_code(networkCode, code) + networks = self.safe_dict(currency, 'networks', {}) + if networkUnified in networks: + network = self.safe_dict(networks, networkUnified, {}) + networkInfo = self.safe_value(network, 'info', {}) + networkId = self.safe_string(networkInfo, 'network') + else: + networkId = self.network_code_to_id(networkCode, code) + if networkId is not None: + request['network'] = networkId + params = self.omit(params, 'network') + response = await self.spotPrivateGetCapitalDepositAddress(self.extend(request, params)) + # + # [ + # { + # coin: "USDT", + # network: "BNB Smart Chain(BEP20)", + # address: "0x0d48003e0c27c5de62b97c9b4cdb31fdd29da619", + # memo: null + # } + # ... + # ] + # + addressStructures = self.parse_deposit_addresses(response, None, False) + return self.index_by(addressStructures, 'network') + + async def create_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + create a currency deposit address + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#generate-deposit-address-supporting-network + + :param str code: unified currency code of the currency for the deposit address + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.network]: the blockchain network name + :returns dict: an `address structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'coin': currency['id'], + } + networkCode = self.safe_string(params, 'network') + if networkCode is None: + raise ArgumentsRequired(self.id + ' createDepositAddress requires a `network` parameter') + # createDepositAddress and fetchDepositAddress use a different network-id compared to withdraw + networkId = None + networkUnified = self.network_id_to_code(networkCode, code) + networks = self.safe_dict(currency, 'networks', {}) + if networkUnified in networks: + network = self.safe_dict(networks, networkUnified, {}) + networkInfo = self.safe_value(network, 'info', {}) + networkId = self.safe_string(networkInfo, 'network') + else: + networkId = self.network_code_to_id(networkCode, code) + if networkId is not None: + request['network'] = networkId + params = self.omit(params, 'network') + response = await self.spotPrivatePostCapitalDepositAddress(self.extend(request, params)) + # { + # "coin": "EOS", + # "network": "EOS", + # "address": "zzqqqqqqqqqq", + # "memo": "MX10068" + # } + return self.parse_deposit_address(response, currency) + + async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#deposit-address-supporting-network + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.network]: the chain of currency, self only apply for multi-chain currency, and there is no need for single chain currency + :returns dict: an `address structure ` + """ + network = self.safe_string(params, 'network') + addressStructures = await self.fetch_deposit_addresses_by_network(code, params) + result = None + if network is not None: + result = self.safe_dict(addressStructures, self.network_id_to_code(network, code)) + else: + options = self.safe_dict(self.options, 'defaultNetworks') + defaultNetworkForCurrency = self.safe_string(options, code) + if defaultNetworkForCurrency is not None: + result = self.safe_dict(addressStructures, defaultNetworkForCurrency) + else: + keys = list(addressStructures.keys()) + key = self.safe_string(keys, 0) + result = self.safe_dict(addressStructures, key) + if result is None: + raise InvalidAddress(self.id + ' fetchDepositAddress() cannot find a deposit address for ' + code + ', and network' + network + 'consider creating one using .createDepositAddress() method or in MEXC website') + return result + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#deposit-history-supporting-network + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + request: dict = { + # 'coin': currency['id'] + network example: USDT-TRX, + # 'status': 'status', + # 'startTime': since, # default 90 days + # 'endTime': self.nonce(), + # 'limit': limit, # default 1000, maximum 1000 + } + currency = None + if code is not None: + currency = self.currency(code) + request['coin'] = currency['id'] + # currently mexc does not have network names unified so for certain things we might need TRX or TRC-20 + # due to that I'm applying the network parameter directly so the user can control it on its side + rawNetwork = self.safe_string(params, 'network') + if rawNetwork is not None: + params = self.omit(params, 'network') + request['coin'] = request['coin'] + '-' + rawNetwork + if since is not None: + request['startTime'] = since + if limit is not None: + if limit > 1000: + raise ExchangeError('This exchange supports a maximum limit of 1000') + request['limit'] = limit + response = await self.spotPrivateGetCapitalDepositHisrec(self.extend(request, params)) + # + # [ + # { + # "amount": "10", + # "coin": "USDC-TRX", + # "network": "TRX", + # "status": "5", + # "address": "TSMcEDDvkqY9dz8RkFnrS86U59GwEZjfvh", + # "txId": "51a8f49e6f03f2c056e71fe3291aa65e1032880be855b65cecd0595a1b8af95b:0", + # "insertTime": "1664805021000", + # "unlockConfirm": "200", + # "confirmTimes": "203", + # "memo": "xxyy1122", + # "transHash": "51a8f49e6f03f2c056e71fe3291aa65e1032880be855b65cecd0595a1b8af95b", + # "updateTime": "1664805621000", + # "netWork: "TRX" + # } + # ] + # + return self.parse_transactions(response, currency, since, limit) + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#withdraw-history-supporting-network + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + request: dict = { + # 'coin': currency['id'], + # 'status': 'status', + # 'startTime': since, # default 90 days + # 'endTime': self.nonce(), + # 'limit': limit, # default 1000, maximum 1000 + } + currency = None + if code is not None: + currency = self.currency(code) + request['coin'] = currency['id'] + if since is not None: + request['startTime'] = since + if limit is not None: + if limit > 1000: + raise ExchangeError('This exchange supports a maximum limit of 1000') + request['limit'] = limit + response = await self.spotPrivateGetCapitalWithdrawHistory(self.extend(request, params)) + # + # [ + # { + # "id": "adcd1c8322154de691b815eedcd10c42", + # "txId": "0xc8c918cd69b2246db493ef6225a72ffdc664f15b08da3e25c6879b271d05e9d0:0", + # "coin": "USDC-MATIC", + # "network": "MATIC", + # "address": "0xeE6C7a415995312ED52c53a0f8f03e165e0A5D62", + # "amount": "2", + # "transferType": "0", + # "status": "7", + # "transactionFee": "1", + # "confirmNo": null, + # "applyTime": "1664882739000", + # "remark": '', + # "memo": null, + # "explorerUrl": "https://etherscan.io/tx/0xc8c918cd69b2246db493ef6225a72ffdc664f15b08da3e25c6879b271d05e9d0", + # "transHash": "0xc8c918cd69b2246db493ef6225a72ffdc664f15b08da3e25c6879b271d05e9d0", + # "updateTime": "1664882799000", + # "netWork: "MATIC" + # } + # ] + # + return self.parse_transactions(response, currency, since, limit) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fetchDeposits + # + # { + # "amount": "10", + # "coin": "USDC-TRX", + # "network": "TRX", + # "status": "5", + # "address": "TSMcEDDvkqY9dz8RkFnrS86U59GwEZjfvh", + # "txId": "51a8f49e6f03f2c056e71fe3291aa65e1032880be855b65cecd0595a1b8af95b:0", + # "insertTime": "1664805021000", + # "unlockConfirm": "200", + # "confirmTimes": "203", + # "memo": "xxyy1122", + # "transHash": "51a8f49e6f03f2c056e71fe3291aa65e1032880be855b65cecd0595a1b8af95b", + # "updateTime": "1664805621000", + # "netWork: "TRX" + # } + # + # fetchWithdrawals + # + # { + # "id": "adcd1c8322154de691b815eedcd10c42", + # "txId": "0xc8c918cd69b2246db493ef6225a72ffdc664f15b08da3e25c6879b271d05e9d0:0", + # "coin": "USDC-MATIC", + # "network": "MATIC", + # "address": "0xeE6C7a415995312ED52c53a0f8f03e165e0A5D62", + # "amount": "2", + # "transferType": "0", + # "status": "7", + # "transactionFee": "1", + # "confirmNo": null, + # "applyTime": "1664882739000", + # "remark": "", + # "memo": null, + # "explorerUrl": "https://etherscan.io/tx/0xc8c918cd69b2246db493ef6225a72ffdc664f15b08da3e25c6879b271d05e9d0", + # "transHash": "0xc8c918cd69b2246db493ef6225a72ffdc664f15b08da3e25c6879b271d05e9d0", + # "updateTime": "1664882799000", + # "netWork: "MATIC" + # } + # + # withdraw + # + # { + # "id":"25fb2831fb6d4fc7aa4094612a26c81d" + # } + # + # internal withdraw(aka internal-transfer) + # + # { + # "tranId":"ad36f0e9c9a24ae794b36fa4f152e471" + # } + # + id = self.safe_string_2(transaction, 'id', 'tranId') + type = 'deposit' if (id is None) else 'withdrawal' + timestamp = self.safe_integer_2(transaction, 'insertTime', 'applyTime') + updated = self.safe_integer(transaction, 'updateTime') + currencyId = None + currencyWithNetwork = self.safe_string(transaction, 'coin') + if currencyWithNetwork is not None: + currencyId = currencyWithNetwork.split('-')[0] + network = None + rawNetwork = self.safe_string(transaction, 'network') + if rawNetwork is not None: + network = self.network_id_to_code(rawNetwork) + code = self.safe_currency_code(currencyId, currency) + status = self.parse_transaction_status_by_type(self.safe_string(transaction, 'status'), type) + amountString = self.safe_string(transaction, 'amount') + address = self.safe_string(transaction, 'address') + txid = self.safe_string_2(transaction, 'transHash', 'txId') + fee = None + feeCostString = self.safe_string(transaction, 'transactionFee') + if feeCostString is not None: + fee = { + 'cost': self.parse_number(feeCostString), + 'currency': code, + } + if type == 'withdrawal': + # mexc withdrawal amount includes the fee + amountString = Precise.string_sub(amountString, feeCostString) + return { + 'info': transaction, + 'id': id, + 'txid': txid, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': network, + 'address': address, + 'addressTo': address, + 'addressFrom': None, + 'tag': self.safe_string(transaction, 'memo'), + 'tagTo': None, + 'tagFrom': None, + 'type': type, + 'amount': self.parse_number(amountString), + 'currency': code, + 'status': status, + 'updated': updated, + 'comment': self.safe_string(transaction, 'remark'), + 'internal': None, + 'fee': fee, + } + + def parse_transaction_status_by_type(self, status, type=None): + statusesByType: dict = { + 'deposit': { + '1': 'failed', # SMALL + '2': 'pending', # TIME_DELAY + '3': 'pending', # LARGE_DELAY + '4': 'pending', # PENDING + '5': 'ok', # SUCCESS + '6': 'pending', # AUDITING + '7': 'failed', # REJECTED + }, + 'withdrawal': { + '1': 'pending', # APPLY + '2': 'pending', # AUDITING + '3': 'pending', # WAIT + '4': 'pending', # PROCESSING + '5': 'pending', # WAIT_PACKAGING + '6': 'pending', # WAIT_CONFIRM + '7': 'ok', # SUCCESS + '8': 'failed', # FAILED + '9': 'canceled', # CANCEL + '10': 'pending', # MANUAL + }, + } + statuses = self.safe_value(statusesByType, type, {}) + return self.safe_string(statuses, status, status) + + async def fetch_position(self, symbol: str, params={}): + """ + fetch data on a single open contract trade position + + https://mexcdevelop.github.io/apidocs/contract_v1_en/#get-the-user-s-history-position-information + + :param str symbol: unified market symbol of the market the position is held in, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `position structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = await self.fetch_positions(None, self.extend(request, params)) + return self.safe_value(response, 0) + + async def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://mexcdevelop.github.io/apidocs/contract_v1_en/#get-the-user-s-history-position-information + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + await self.load_markets() + response = await self.contractPrivateGetPositionOpenPositions(params) + # + # { + # "success": True, + # "code": 0, + # "data": [ + # { + # "positionId": 1394650, + # "symbol": "ETH_USDT", + # "positionType": 1, + # "openType": 1, + # "state": 1, + # "holdVol": 1, + # "frozenVol": 0, + # "closeVol": 0, + # "holdAvgPrice": 1217.3, + # "openAvgPrice": 1217.3, + # "closeAvgPrice": 0, + # "liquidatePrice": 1211.2, + # "oim": 0.1290338, + # "im": 0.1290338, + # "holdFee": 0, + # "realised": -0.0073, + # "leverage": 100, + # "createTime": 1609991676000, + # "updateTime": 1609991676000, + # "autoAddIm": False + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_positions(data, symbols) + + def parse_position(self, position: dict, market: Market = None): + # + # fetchPositions + # + # { + # "positionId": 1394650, + # "symbol": "ETH_USDT", + # "positionType": 1, + # "openType": 1, + # "state": 1, + # "holdVol": 1, + # "frozenVol": 0, + # "closeVol": 0, + # "holdAvgPrice": 1217.3, + # "openAvgPrice": 1217.3, + # "closeAvgPrice": 0, + # "liquidatePrice": 1211.2, + # "oim": 0.1290338, + # "im": 0.1290338, + # "holdFee": 0, + # "realised": -0.0073, + # "leverage": 100, + # "createTime": 1609991676000, + # "updateTime": 1609991676000, + # "autoAddIm": False + # } + # + # fetchPositionsHistory + # + # { + # positionId: '390281084', + # symbol: 'RVN_USDT', + # positionType: '1', + # openType: '2', + # state: '3', + # holdVol: '0', + # frozenVol: '0', + # closeVol: '1141', + # holdAvgPrice: '0.03491', + # holdAvgPriceFullyScale: '0.03491', + # openAvgPrice: '0.03491', + # openAvgPriceFullyScale: '0.03491', + # closeAvgPrice: '0.03494', + # liquidatePrice: '0.03433', + # oim: '0', + # im: '0', + # holdFee: '0', + # realised: '0.1829', + # leverage: '50', + # createTime: '1711512408000', + # updateTime: '1711512553000', + # autoAddIm: False, + # version: '4', + # profitRatio: '0.0227', + # newOpenAvgPrice: '0.03491', + # newCloseAvgPrice: '0.03494', + # closeProfitLoss: '0.3423', + # fee: '0.1593977', + # positionShowStatus: 'CLOSED' + # } + # + market = self.safe_market(self.safe_string(position, 'symbol'), market, None, 'swap') + symbol = market['symbol'] + contracts = self.safe_string(position, 'holdVol') + entryPrice = self.safe_number(position, 'openAvgPrice') + initialMargin = self.safe_string(position, 'im') + rawSide = self.safe_string(position, 'positionType') + side = 'long' if (rawSide == '1') else 'short' + openType = self.safe_string(position, 'margin_mode') + marginType = 'isolated' if (openType == '1') else 'cross' + leverage = self.safe_number(position, 'leverage') + liquidationPrice = self.safe_number(position, 'liquidatePrice') + timestamp = self.safe_integer(position, 'updateTime') + return self.safe_position({ + 'info': position, + 'id': None, + 'symbol': symbol, + 'contracts': self.parse_number(contracts), + 'contractSize': None, + 'entryPrice': entryPrice, + 'collateral': None, + 'side': side, + 'unrealizedPnl': None, + 'leverage': self.parse_number(leverage), + 'percentage': None, + 'marginMode': marginType, + 'notional': None, + 'markPrice': None, + 'lastPrice': None, + 'liquidationPrice': liquidationPrice, + 'initialMargin': self.parse_number(initialMargin), + 'initialMarginPercentage': None, + 'maintenanceMargin': None, + 'maintenanceMarginPercentage': None, + 'marginRatio': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'hedged': None, + 'stopLossPrice': None, + 'takeProfitPrice': None, + 'lastUpdateTimestamp': None, + }) + + async def fetch_transfer(self, id: str, code: Str = None, params={}) -> TransferEntry: + """ + fetches a transfer + + https://mexcdevelop.github.io/apidocs/spot_v2_en/#internal-assets-transfer-order-inquiry + + :param str id: transfer id + :param str [code]: not used by mexc fetchTransfer + :param dict params: extra parameters specific to the exchange api endpoint + :returns dict: a `transfer structure ` + """ + marketType, query = self.handle_market_type_and_params('fetchTransfer', None, params) + await self.load_markets() + if marketType == 'spot': + request: dict = { + 'transact_id': id, + } + response = await self.spotPrivateGetAssetInternalTransferRecord(self.extend(request, query)) + # + # { + # "code": "200", + # "data": { + # "currency": "USDT", + # "amount": "1", + # "transact_id": "954877a2ef54499db9b28a7cf9ebcf41", + # "from": "MAIN", + # "to": "CONTRACT", + # "transact_state": "SUCCESS" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_transfer(data) + elif marketType == 'swap': + raise BadRequest(self.id + ' fetchTransfer() is not supported for ' + marketType) + return None + + async def fetch_transfers(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[TransferEntry]: + """ + fetch a history of internal transfers made on an account + + https://mexcdevelop.github.io/apidocs/spot_v2_en/#get-internal-assets-transfer-records + https://mexcdevelop.github.io/apidocs/contract_v1_en/#get-the-user-39-s-asset-transfer-records + https://www.mexc.com/api-docs/spot-v3/wallet-endpoints#query-user-universal-transfer-history :param str code: unified currency code of the currency transferred + + @param code + :param int [since]: the earliest time in ms to fetch transfers for + :param int [limit]: the maximum number of transfers structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.fromAccountType]: 'SPOT' for spot wallet, 'FUTURES' for contract wallet + :param str [params.toAccountType]: 'SPOT' for spot wallet, 'FUTURES' for contract wallet + :returns dict[]: a list of `transfer structures ` + """ + marketType = None + marketType, params = self.handle_market_type_and_params('fetchTransfers', None, params) + await self.load_markets() + request: dict = {} + currency = None + if code is not None: + currency = self.currency(code) + fromAccountType = None + fromAccountType, params = self.handle_option_and_params(params, 'fetchTransfers', 'fromAccountType') + accountTypes = { + 'spot': 'SPOT', + 'swap': 'FUTURES', + 'futures': 'FUTURES', + 'future': 'FUTURES', + 'margin': 'SPOT', + } + if fromAccountType is not None: + request['fromAccountType'] = self.safe_string(accountTypes, fromAccountType, fromAccountType) + else: + raise ArgumentsRequired(self.id + ' fetchTransfers() requires a fromAccountType parameter, one of "SPOT", "FUTURES"') + toAccountType = None + toAccountType, params = self.handle_option_and_params(params, 'fetchTransfers', 'toAccountType') + if toAccountType is not None: + request['toAccountType'] = self.safe_string(accountTypes, toAccountType, toAccountType) + else: + raise ArgumentsRequired(self.id + ' fetchTransfers() requires a toAccountType parameter, one of "SPOT", "FUTURES"') + resultList = [] + if marketType == 'spot': + if since is not None: + request['startTime'] = since + if limit is not None: + if limit > 100: + raise ExchangeError('This exchange supports a maximum limit of 50') + request['size'] = limit + response = await self.spotPrivateGetCapitalTransfer(self.extend(request, params)) + # + # + # { + # "rows": [ + # { + # "tranId": "cdf0d2a618b5458c965baefe6b1d0859", + # "clientTranId": null, + # "asset": "USDT", + # "amount": "1", + # "fromAccountType": "FUTURES", + # "toAccountType": "SPOT", + # "symbol": null, + # "status": "SUCCESS", + # "timestamp": 1759328309000 + # } + # ], + # "total": 1 + # } + resultList = self.safe_list(response, 'rows', []) + elif marketType == 'swap': + if limit is not None: + request['page_size'] = limit + response = await self.contractPrivateGetAccountTransferRecord(self.extend(request, params)) + data = self.safe_value(response, 'data') + resultList = self.safe_value(data, 'resultList') + # + # { + # "success": True, + # "code": "0", + # "data": { + # "pageSize": "20", + # "totalCount": "10", + # "totalPage": "1", + # "currentPage": "1", + # "resultList": [ + # { + # "id": "2980812", + # "txid": "fa8a1e7bf05940a3b7025856dc48d025", + # "currency": "USDT", + # "amount": "22.90213135", + # "type": "IN", + # "state": "SUCCESS", + # "createTime": "1648849076000", + # "updateTime": "1648849076000" + # }, + # ] + # } + # } + # + return self.parse_transfers(resultList, currency, since, limit) + + async def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#user-universal-transfer + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account to transfer from + :param str toAccount: account to transfer to + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.symbol]: market symbol required for margin account transfers eg:BTCUSDT + :returns dict: a `transfer structure ` + """ + await self.load_markets() + currency = self.currency(code) + accounts: dict = { + 'spot': 'SPOT', + 'swap': 'FUTURES', + 'future': 'FUTURES', + } + fromId = self.safe_string(accounts, fromAccount, fromAccount) + toId = self.safe_string(accounts, toAccount, toAccount) + if fromId is None: + keys = list(accounts.keys()) + raise ExchangeError(self.id + ' fromAccount must be one of ' + ', '.join(keys)) + if toId is None: + keys = list(accounts.keys()) + raise ExchangeError(self.id + ' toAccount must be one of ' + ', '.join(keys)) + request: dict = { + 'asset': currency['id'], + 'amount': amount, + 'fromAccountType': fromId, + 'toAccountType': toId, + } + if (fromId == 'ISOLATED_MARGIN') or (toId == 'ISOLATED_MARGIN'): + symbol = self.safe_string(params, 'symbol') + params = self.omit(params, 'symbol') + if symbol is None: + raise ArgumentsRequired(self.id + ' transfer() requires a symbol argument for isolated margin') + market = self.market(symbol) + request['symbol'] = market['id'] + response = await self.spotPrivatePostCapitalTransfer(self.extend(request, params)) + # + # { + # "tranId": "ebb06123e6a64f4ab234b396c548d57e" + # } + # + transaction = self.parse_transfer(response, currency) + return self.extend(transaction, { + 'amount': amount, + 'fromAccount': fromAccount, + 'toAccount': toAccount, + }) + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + # + # spot: fetchTransfer + # + # { + # "currency": "USDT", + # "amount": "1", + # "transact_id": "b60c1df8e7b24b268858003f374ecb75", + # "from": "MAIN", + # "to": "CONTRACT", + # "transact_state": "WAIT" + # } + # + # swap: fetchTransfer + # + # { + # "currency": "USDT", + # "amount": "22.90213135", + # "txid": "fa8a1e7bf05940a3b7025856dc48d025", + # "id": "2980812", + # "type": "IN", + # "state": "SUCCESS", + # "createTime": "1648849076000", + # "updateTime": "1648849076000" + # } + # { + # "tranId": "cdf0d2a618b5458c965baefe6b1d0859", + # "clientTranId": null, + # "asset": "USDT", + # "amount": "1", + # "fromAccountType": "FUTURES", + # "toAccountType": "SPOT", + # "symbol": null, + # "status": "SUCCESS", + # "timestamp": 1759328309000 + # } + # + # transfer + # + # { + # "tranId": "ebb06123e6a64f4ab234b396c548d57e" + # } + # + currencyId = self.safe_string_2(transfer, 'currency', 'asset') + id = self.safe_string_n(transfer, ['transact_id', 'txid', 'tranId']) + timestamp = self.safe_integer_2(transfer, 'createTime', 'timestamp') + datetime = self.iso8601(timestamp) if (timestamp is not None) else None + direction = self.safe_string(transfer, 'type') + accountFrom = None + accountTo = None + fromAccountType = self.safe_string(transfer, 'fromAccountType') + toAccountType = self.safe_string(transfer, 'toAccountType') + if (fromAccountType is not None) and (toAccountType is not None): + accountFrom = fromAccountType + accountTo = toAccountType + elif direction is not None: + accountFrom = 'MAIN' if (direction == 'IN') else 'CONTRACT' + accountTo = 'CONTRACT' if (direction == 'IN') else 'MAIN' + else: + accountFrom = self.safe_string(transfer, 'from') + accountTo = self.safe_string(transfer, 'to') + return { + 'info': transfer, + 'id': id, + 'timestamp': timestamp, + 'datetime': datetime, + 'currency': self.safe_currency_code(currencyId, currency), + 'amount': self.safe_number(transfer, 'amount'), + 'fromAccount': self.parse_account_id(accountFrom), + 'toAccount': self.parse_account_id(accountTo), + 'status': self.parse_transfer_status(self.safe_string_n(transfer, ['transact_state', 'state', 'status'])), + } + + def parse_account_id(self, status): + statuses: dict = { + 'SPOT': 'spot', + 'FUTURES': 'swap', + 'MAIN': 'spot', + 'CONTRACT': 'swap', + } + return self.safe_string(statuses, status, status) + + def parse_transfer_status(self, status: Str) -> Str: + statuses: dict = { + 'SUCCESS': 'ok', + 'FAILED': 'failed', + 'WAIT': 'pending', + } + return self.safe_string(statuses, status, status) + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#withdraw-new + https://www.mexc.com/api-docs/spot-v3/wallet-endpoints#internal-transfer + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :param dict [params.internal]: False by default, set to True for an "internal transfer" + :param dict [params.toAccountType]: skipped by default, set to 'EMAIL|UID|MOBILE' when making an "internal transfer" + :returns dict: a `transaction structure ` + """ + await self.load_markets() + currency = self.currency(code) + tag, params = self.handle_withdraw_tag_and_params(tag, params) + internal = self.safe_bool(params, 'internal', False) + if internal: + params = self.omit(params, 'internal') + requestForInternal = { + 'asset': currency['id'], + 'amount': amount, + 'toAccount': address, + } + toAccountType = self.safe_string(params, 'toAccountType') + if toAccountType is None: + raise ArgumentsRequired(self.id + ' withdraw() requires a toAccountType parameter for internal transfer to be of: EMAIL | UID | MOBILE') + responseForInternal = await self.spotPrivatePostCapitalTransferInternal(self.extend(requestForInternal, params)) + # + # { + # "id":"7213fea8e94b4a5593d507237e5a555b" + # } + # + return self.parse_transaction(responseForInternal, currency) + networks = self.safe_dict(self.options, 'networks', {}) + network = self.safe_string_2(params, 'network', 'netWork') # self line allows the user to specify either ERC20 or ETH + network = self.safe_string(networks, network, network) # handle ETH > ERC-20 alias + network = self.network_code_to_id(network, currency['code']) + self.check_address(address) + request: dict = { + 'coin': currency['id'], + 'address': address, + 'amount': amount, + } + if tag is not None: + request['memo'] = tag + if network is not None: + request['netWork'] = network + params = self.omit(params, ['network', 'netWork']) + response = await self.spotPrivatePostCapitalWithdraw(self.extend(request, params)) + # + # { + # "id":"7213fea8e94b4a5593d507237e5a555b" + # } + # + return self.parse_transaction(response, currency) + + async def set_position_mode(self, hedged: bool, symbol: Str = None, params={}): + """ + set hedged to True or False for a market + + https://mexcdevelop.github.io/apidocs/contract_v1_en/#change-position-mode + + :param bool hedged: set to True to use dualSidePosition + :param str symbol: not used by mexc setPositionMode() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + request: dict = { + 'positionMode': 1 if hedged else 2, # 1 Hedge, 2 One-way, before changing position mode make sure that there are no active orders, planned orders, or open positions, the risk limit level will be reset to 1 + } + response = await self.contractPrivatePostPositionChangePositionMode(self.extend(request, params)) + # + # { + # "success":true, + # "code":0 + # } + # + return response + + async def fetch_position_mode(self, symbol: Str = None, params={}): + """ + fetchs the position mode, hedged or one way, hedged for binance is set identically for all linear markets or all inverse markets + + https://mexcdevelop.github.io/apidocs/contract_v1_en/#get-position-mode + + :param str symbol: not used by mexc fetchPositionMode + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an object detailing whether the market is in hedged or one-way mode + """ + response = await self.contractPrivateGetPositionPositionMode(params) + # + # { + # "success":true, + # "code":0, + # "data":2 + # } + # + positionMode = self.safe_integer(response, 'data') + return { + 'info': response, + 'hedged': (positionMode == 1), + } + + async def fetch_transaction_fees(self, codes: Strings = None, params={}): + """ + fetch deposit and withdrawal fees + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#query-the-currency-information + + :param str[]|None codes: returns fees for all currencies if None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `fee structures ` + """ + await self.load_markets() + response = await self.spotPrivateGetCapitalConfigGetall(params) + # + # [ + # { + # "coin": "AGLD", + # "name": "Adventure Gold", + # "networkList": [ + # { + # "coin": "AGLD", + # "depositDesc": null, + # "depositEnable": True, + # "minConfirm": "0", + # "name": "Adventure Gold", + # "network": "ERC20", + # "withdrawEnable": True, + # "withdrawFee": "10.000000000000000000", + # "withdrawIntegerMultiple": null, + # "withdrawMax": "1200000.000000000000000000", + # "withdrawMin": "20.000000000000000000", + # "sameAddress": False, + # "contract": "0x32353a6c91143bfd6c7d363b546e62a9a2489a20", + # "withdrawTips": null, + # "depositTips": null + # } + # ... + # ] + # }, + # ... + # ] + # + return self.parse_transaction_fees(response, codes) + + def parse_transaction_fees(self, response, codes=None): + withdrawFees: dict = {} + for i in range(0, len(response)): + entry = response[i] + currencyId = self.safe_string(entry, 'coin') + currency = self.safe_currency(currencyId) + code = self.safe_string(currency, 'code') + if (codes is None) or (self.in_array(code, codes)): + withdrawFees[code] = self.parse_transaction_fee(entry, currency) + return { + 'withdraw': withdrawFees, + 'deposit': {}, + 'info': response, + } + + def parse_transaction_fee(self, transaction, currency: Currency = None): + # + # { + # "coin": "AGLD", + # "name": "Adventure Gold", + # "networkList": [ + # { + # "coin": "AGLD", + # "depositDesc": null, + # "depositEnable": True, + # "minConfirm": "0", + # "name": "Adventure Gold", + # "network": "ERC20", + # "withdrawEnable": True, + # "withdrawFee": "10.000000000000000000", + # "withdrawIntegerMultiple": null, + # "withdrawMax": "1200000.000000000000000000", + # "withdrawMin": "20.000000000000000000", + # "sameAddress": False, + # "contract": "0x32353a6c91143bfd6c7d363b546e62a9a2489a20", + # "withdrawTips": null, + # "depositTips": null + # } + # ... + # ] + # } + # + networkList = self.safe_value(transaction, 'networkList', []) + result: dict = {} + for j in range(0, len(networkList)): + networkEntry = networkList[j] + networkId = self.safe_string(networkEntry, 'network') + networkCode = self.safe_string(self.options['networks'], networkId, networkId) + fee = self.safe_number(networkEntry, 'withdrawFee') + result[networkCode] = fee + return result + + async def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}): + """ + fetch deposit and withdrawal fees + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#query-the-currency-information + + :param str[]|None codes: returns fees for all currencies if None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `fee structures ` + """ + await self.load_markets() + response = await self.spotPrivateGetCapitalConfigGetall(params) + # + # [ + # { + # "coin": "AGLD", + # "name": "Adventure Gold", + # "networkList": [ + # { + # "coin": "AGLD", + # "depositDesc": null, + # "depositEnable": True, + # "minConfirm": "0", + # "name": "Adventure Gold", + # "network": "ERC20", + # "withdrawEnable": True, + # "withdrawFee": "10.000000000000000000", + # "withdrawIntegerMultiple": null, + # "withdrawMax": "1200000.000000000000000000", + # "withdrawMin": "20.000000000000000000", + # "sameAddress": False, + # "contract": "0x32353a6c91143bfd6c7d363b546e62a9a2489a20", + # "withdrawTips": null, + # "depositTips": null + # } + # ... + # ] + # }, + # ... + # ] + # + return self.parse_deposit_withdraw_fees(response, codes, 'coin') + + def parse_deposit_withdraw_fee(self, fee, currency: Currency = None): + # + # { + # "coin": "AGLD", + # "name": "Adventure Gold", + # "networkList": [ + # { + # "coin": "AGLD", + # "depositDesc": null, + # "depositEnable": True, + # "minConfirm": "0", + # "name": "Adventure Gold", + # "network": "ERC20", + # "withdrawEnable": True, + # "withdrawFee": "10.000000000000000000", + # "withdrawIntegerMultiple": null, + # "withdrawMax": "1200000.000000000000000000", + # "withdrawMin": "20.000000000000000000", + # "sameAddress": False, + # "contract": "0x32353a6c91143bfd6c7d363b546e62a9a2489a20", + # "withdrawTips": null, + # "depositTips": null + # } + # ... + # ] + # } + # + networkList = self.safe_value(fee, 'networkList', []) + result = self.deposit_withdraw_fee(fee) + for j in range(0, len(networkList)): + networkEntry = networkList[j] + networkId = self.safe_string(networkEntry, 'network') + networkCode = self.network_id_to_code(networkId, self.safe_string(currency, 'code')) + result['networks'][networkCode] = { + 'withdraw': { + 'fee': self.safe_number(networkEntry, 'withdrawFee'), + 'percentage': None, + }, + 'deposit': { + 'fee': None, + 'percentage': None, + }, + } + return self.assign_default_deposit_withdraw_fees(result) + + async def fetch_leverage(self, symbol: str, params={}) -> Leverage: + """ + fetch the set leverage for a market + + https://mexcdevelop.github.io/apidocs/contract_v1_en/#get-leverage + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `leverage structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = await self.contractPrivateGetPositionLeverage(self.extend(request, params)) + # + # { + # "success": True, + # "code": 0, + # "data": [ + # { + # "level": 1, + # "maxVol": 463300, + # "mmr": 0.004, + # "imr": 0.005, + # "positionType": 1, + # "openType": 1, + # "leverage": 20, + # "limitBySys": False, + # "currentMmr": 0.004 + # }, + # { + # "level": 1, + # "maxVol": 463300, + # "mmr": 0.004, + # "imr": 0.005, + # "positionType": 2, + # "openType": 1, + # "leverage": 20, + # "limitBySys": False, + # "currentMmr": 0.004 + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_leverage(data, market) + + def parse_leverage(self, leverage: dict, market: Market = None) -> Leverage: + marginMode = None + longLeverage = None + shortLeverage = None + for i in range(0, len(leverage)): + entry = leverage[i] + openType = self.safe_integer(entry, 'openType') + positionType = self.safe_integer(entry, 'positionType') + if positionType == 1: + longLeverage = self.safe_integer(entry, 'leverage') + elif positionType == 2: + shortLeverage = self.safe_integer(entry, 'leverage') + marginMode = 'isolated' if (openType == 1) else 'cross' + return { + 'info': leverage, + 'symbol': market['symbol'], + 'marginMode': marginMode, + 'longLeverage': longLeverage, + 'shortLeverage': shortLeverage, + } + + def handle_margin_mode_and_params(self, methodName, params={}, defaultValue=None): + """ + @ignore + marginMode specified by params["marginMode"], self.options["marginMode"], self.options["defaultMarginMode"], params["margin"] = True or self.options["defaultType"] = 'margin' + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.margin]: True for trading spot-margin + :returns Array: the marginMode in lowercase + """ + defaultType = self.safe_string(self.options, 'defaultType') + isMargin = self.safe_bool(params, 'margin', False) + marginMode = None + marginMode, params = super(mexc, self).handle_margin_mode_and_params(methodName, params, defaultValue) + if (defaultType == 'margin') or (isMargin is True): + marginMode = 'isolated' + return [marginMode, params] + + async def fetch_positions_history(self, symbols: Strings = None, since: Int = None, limit: Int = None, params={}) -> List[Position]: + """ + fetches historical positions + + https://mexcdevelop.github.io/apidocs/contract_v1_en/#get-the-user-s-history-position-information + + :param str[] [symbols]: unified contract symbols + :param int [since]: not used by mexc fetchPositionsHistory + :param int [limit]: the maximum amount of candles to fetch, default=1000 + :param dict [params]: extra parameters specific to the exchange api endpoint + + EXCHANGE SPECIFIC PARAMETERS + :param int [params.type]: position type,1: long, 2: short + :param int [params.page_num]: current page number, default is 1 + :returns dict[]: a list of `position structures ` + """ + await self.load_markets() + request: dict = {} + if symbols is not None: + symbolsLength = len(symbols) + if symbolsLength == 1: + market = self.market(symbols[0]) + request['symbol'] = market['id'] + if limit is not None: + request['page_size'] = limit + response = await self.contractPrivateGetPositionListHistoryPositions(self.extend(request, params)) + # + # { + # success: True, + # code: '0', + # data: [ + # { + # positionId: '390281084', + # symbol: 'RVN_USDT', + # positionType: '1', + # openType: '2', + # state: '3', + # holdVol: '0', + # frozenVol: '0', + # closeVol: '1141', + # holdAvgPrice: '0.03491', + # holdAvgPriceFullyScale: '0.03491', + # openAvgPrice: '0.03491', + # openAvgPriceFullyScale: '0.03491', + # closeAvgPrice: '0.03494', + # liquidatePrice: '0.03433', + # oim: '0', + # im: '0', + # holdFee: '0', + # realised: '0.1829', + # leverage: '50', + # createTime: '1711512408000', + # updateTime: '1711512553000', + # autoAddIm: False, + # version: '4', + # profitRatio: '0.0227', + # newOpenAvgPrice: '0.03491', + # newCloseAvgPrice: '0.03494', + # closeProfitLoss: '0.3423', + # fee: '0.1593977', + # positionShowStatus: 'CLOSED' + # }, + # ... + # ] + # } + # + data = self.safe_list(response, 'data') + positions = self.parse_positions(data, symbols, params) + return self.filter_by_since_limit(positions, since, limit) + + async def set_margin_mode(self, marginMode: str, symbol: Str = None, params={}): + """ + set margin mode to 'cross' or 'isolated' + + https://mexcdevelop.github.io/apidocs/contract_v1_en/#switch-leverage + + :param str marginMode: 'cross' or 'isolated' + :param str [symbol]: required when there is no position, else provide params["positionId"] + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.positionId]: required when a position is set + :param str [params.direction]: "long" or "short" required when there is no position + :returns dict: response from the exchange + """ + await self.load_markets() + market = self.market(symbol) + if market['spot']: + raise BadSymbol(self.id + ' setMarginMode() supports contract markets only') + marginMode = marginMode.lower() + if marginMode != 'isolated' and marginMode != 'cross': + raise BadRequest(self.id + ' setMarginMode() marginMode argument should be isolated or cross') + leverage = self.safe_integer(params, 'leverage') + if leverage is None: + raise ArgumentsRequired(self.id + ' setMarginMode() requires a leverage parameter') + direction = self.safe_string_lower_2(params, 'direction', 'positionId') + request: dict = { + 'leverage': leverage, + 'openType': 1 if (marginMode == 'isolated') else 2, + } + if symbol is not None: + request['symbol'] = market['id'] + if direction is not None: + request['positionType'] = 2 if (direction == 'short') else 1 + params = self.omit(params, 'direction') + response = await self.contractPrivatePostPositionChangeLeverage(self.extend(request, params)) + # + # {success: True, code: '0'} + # + return self.parse_leverage(response, market) # tmp revert type + + def nonce(self): + return self.milliseconds() - self.safe_integer(self.options, 'timeDifference', 0) + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + section = self.safe_string(api, 0) + access = self.safe_string(api, 1) + path, params = self.resolve_path(path, params) + url = None + if section == 'spot' or section == 'broker': + if section == 'broker': + url = self.urls['api'][section][access] + '/' + path + else: + url = self.urls['api'][section][access] + '/api/' + self.version + '/' + path + urlParams = params + if access == 'private': + if section == 'broker' and ((method == 'POST') or (method == 'PUT') or (method == 'DELETE')): + urlParams = { + 'timestamp': self.nonce(), + 'recvWindow': self.safe_integer(self.options, 'recvWindow', 5000), + } + body = self.json(params) + else: + urlParams['timestamp'] = self.nonce() + urlParams['recvWindow'] = self.safe_integer(self.options, 'recvWindow', 5000) + paramsEncoded = '' + if urlParams: + paramsEncoded = self.urlencode(urlParams) + url += '?' + paramsEncoded + if access == 'private': + self.check_required_credentials() + signature = self.hmac(self.encode(paramsEncoded), self.encode(self.secret), hashlib.sha256) + url += '&' + 'signature=' + signature + headers = { + 'X-MEXC-APIKEY': self.apiKey, + 'source': self.safe_string(self.options, 'broker', 'CCXT'), + } + if (method == 'POST') or (method == 'PUT') or (method == 'DELETE'): + headers['Content-Type'] = 'application/json' + elif section == 'contract' or section == 'spot2': + url = self.urls['api'][section][access] + '/' + self.implode_params(path, params) + params = self.omit(params, self.extract_params(path)) + if access == 'public': + if params: + url += '?' + self.urlencode(params) + else: + self.check_required_credentials() + timestamp = str(self.nonce()) + auth = '' + headers = { + 'ApiKey': self.apiKey, + 'Request-Time': timestamp, + 'Content-Type': 'application/json', + 'source': self.safe_string(self.options, 'broker', 'CCXT'), + } + if method == 'POST': + auth = self.json(params) + body = auth + else: + params = self.keysort(params) + if params: + auth += self.urlencode(params) + url += '?' + auth + auth = self.apiKey + timestamp + auth + signature = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256) + headers['Signature'] = signature + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None + # spot + # {"code":-1128,"msg":"Combination of optional parameters invalid.","_extend":null} + # {"success":false,"code":123456,"message":"Order quantity error...."} + # + # contract + # + # {"code":10232,"msg":"The currency not exist"} + # {"code":10216,"msg":"No available deposit address"} + # {"success":true, "code":0, "data":1634095541710} + # + success = self.safe_bool(response, 'success', False) # v1 + if success is True: + return None + responseCode = self.safe_string(response, 'code', None) + if (responseCode is not None) and (responseCode != '200') and (responseCode != '0'): + feedback = self.id + ' ' + body + self.throw_broadly_matched_exception(self.exceptions['broad'], body, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], responseCode, feedback) + raise ExchangeError(feedback) + return None diff --git a/ccxt/async_support/modetrade.py b/ccxt/async_support/modetrade.py new file mode 100644 index 0000000..6e87f15 --- /dev/null +++ b/ccxt/async_support/modetrade.py @@ -0,0 +1,2818 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.modetrade import ImplicitAPI +from ccxt.base.types import Any, Balances, Currencies, Currency, Int, LedgerEntry, Leverage, Market, Num, Order, OrderBook, OrderRequest, OrderSide, OrderType, Position, Str, Strings, FundingRate, FundingRates, Trade, TradingFees, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import NotSupported +from ccxt.base.errors import NetworkError +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class modetrade(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(modetrade, self).describe(), { + 'id': 'modetrade', + 'name': 'Mode Trade', + 'countries': ['KY'], # Cayman Islands + 'rateLimit': 100, + 'version': 'v1', + 'certified': False, + 'pro': True, + 'dex': True, + 'hostname': 'trade.mode.network', + 'has': { + 'CORS': None, + 'spot': False, + 'margin': False, + 'swap': True, + 'future': False, + 'option': False, + 'addMargin': False, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'cancelOrders': True, + 'cancelWithdraw': False, + 'closeAllPositions': False, + 'closePosition': False, + 'createConvertTrade': False, + 'createDepositAddress': False, + 'createMarketBuyOrderWithCost': False, + 'createMarketOrder': True, + 'createMarketOrderWithCost': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createOrderWithTakeProfitAndStopLoss': True, + 'createReduceOnlyOrder': True, + 'createStopLimitOrder': True, + 'createStopLossOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'createTakeProfitOrder': True, + 'createTrailingAmountOrder': False, + 'createTrailingPercentOrder': False, + 'createTriggerOrder': True, + 'fetchAccounts': False, + 'fetchBalance': True, + 'fetchCanceledOrders': False, + 'fetchClosedOrder': False, + 'fetchClosedOrders': True, + 'fetchConvertCurrencies': False, + 'fetchConvertQuote': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': False, + 'fetchDeposits': True, + 'fetchDepositsWithdrawals': True, + 'fetchFundingHistory': True, + 'fetchFundingInterval': True, + 'fetchFundingIntervals': False, + 'fetchFundingRate': True, + 'fetchFundingRateHistory': True, + 'fetchFundingRates': True, + 'fetchIndexOHLCV': False, + 'fetchLedger': True, + 'fetchLeverage': True, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterestHistory': False, + 'fetchOpenOrder': False, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchOrderTrades': True, + 'fetchPosition': True, + 'fetchPositionMode': False, + 'fetchPositions': True, + 'fetchPremiumIndexOHLCV': False, + 'fetchStatus': True, + 'fetchTicker': False, + 'fetchTickers': False, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': True, + 'fetchTransactions': 'emulated', + 'fetchTransfers': False, + 'fetchWithdrawals': True, + 'reduceMargin': False, + 'setLeverage': True, + 'setMargin': False, + 'setPositionMode': False, + 'transfer': False, + 'withdraw': True, # exchange have that endpoint disabled atm, but was once implemented in ccxt per old docs: https://kronosresearch.github.io/wootrade-documents/#token-withdraw + }, + 'timeframes': { + '1m': '1m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1h', + '4h': '4h', + '12h': '12h', + '1d': '1d', + '1w': '1w', + '1M': '1mon', + '1y': '1y', + }, + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/cec2b7f1-3b2b-4502-971b-447ee1937d6b', + 'api': { + 'public': 'https://api-evm.orderly.org', + 'private': 'https://api-evm.orderly.org', + }, + 'test': { + 'public': 'https://testnet-api-evm.orderly.org', + 'private': 'https://testnet-api-evm.orderly.org', + }, + 'www': 'https://trade.mode.network', + 'referral': { + 'url': 'https://trade.mode.network?ref=MODETRADE', + 'discount': 0.2, + }, + }, + 'api': { + 'v1': { + 'public': { + 'get': { + 'public/volume/stats': 1, + 'public/broker/name': 1, + 'public/chain_info/{broker_id}': 1, + 'public/system_info': 1, + 'public/vault_balance': 1, + 'public/insurancefund': 1, + 'public/chain_info': 1, + 'faucet/usdc': 1, + 'public/account': 1, + 'get_account': 1, + 'registration_nonce': 1, + 'get_orderly_key': 1, + 'public/liquidation': 1, + 'public/liquidated_positions': 1, + 'public/config': 1, + 'public/campaign/ranking': 10, + 'public/campaign/stats': 10, + 'public/campaign/user': 10, + 'public/campaign/stats/details': 10, + 'public/campaigns': 10, + 'public/points/leaderboard': 1, + 'client/points': 1, + 'public/points/epoch': 1, + 'public/points/epoch_dates': 1, + 'public/referral/check_ref_code': 1, + 'public/referral/verify_ref_code': 1, + 'referral/admin_info': 1, + 'referral/info': 1, + 'referral/referee_info': 1, + 'referral/referee_rebate_summary': 1, + 'referral/referee_history': 1, + 'referral/referral_history': 1, + 'referral/rebate_summary': 1, + 'client/distribution_history': 1, + 'tv/config': 1, + 'tv/history': 1, + 'tv/symbol_info': 1, + 'public/funding_rate_history': 1, + 'public/funding_rate/{symbol}': 0.33, + 'public/funding_rates': 1, + 'public/info': 1, + 'public/info/{symbol}': 1, + 'public/market_trades': 1, + 'public/token': 1, + 'public/futures': 1, + 'public/futures/{symbol}': 1, + }, + 'post': { + 'register_account': 1, + }, + }, + 'private': { + 'get': { + 'client/key_info': 6, + 'client/orderly_key_ip_restriction': 6, + 'order/{oid}': 1, + 'client/order/{client_order_id}': 1, + 'algo/order/{oid}': 1, + 'algo/client/order/{client_order_id}': 1, + 'orders': 1, + 'algo/orders': 1, + 'trade/{tid}': 1, + 'trades': 1, + 'order/{oid}/trades': 1, + 'client/liquidator_liquidations': 1, + 'liquidations': 1, + 'asset/history': 60, + 'client/holding': 1, + 'withdraw_nonce': 1, + 'settle_nonce': 1, + 'pnl_settlement/history': 1, + 'volume/user/daily': 60, + 'volume/user/stats': 60, + 'client/statistics': 60, + 'client/info': 60, + 'client/statistics/daily': 60, + 'positions': 3.33, + 'position/{symbol}': 3.33, + 'funding_fee/history': 30, + 'notification/inbox/notifications': 60, + 'notification/inbox/unread': 60, + 'volume/broker/daily': 60, + 'broker/fee_rate/default': 10, + 'broker/user_info': 10, + 'orderbook/{symbol}': 1, + 'kline': 1, + }, + 'post': { + 'orderly_key': 1, + 'client/set_orderly_key_ip_restriction': 6, + 'client/reset_orderly_key_ip_restriction': 6, + 'order': 1, + 'batch-order': 10, + 'algo/order': 1, + 'liquidation': 1, + 'claim_insurance_fund': 1, + 'withdraw_request': 1, + 'settle_pnl': 1, + 'notification/inbox/mark_read': 60, + 'notification/inbox/mark_read_all': 60, + 'client/leverage': 120, + 'client/maintenance_config': 60, + 'delegate_signer': 10, + 'delegate_orderly_key': 10, + 'delegate_settle_pnl': 10, + 'delegate_withdraw_request': 10, + 'broker/fee_rate/set': 10, + 'broker/fee_rate/set_default': 10, + 'broker/fee_rate/default': 10, + 'referral/create': 10, + 'referral/update': 10, + 'referral/bind': 10, + 'referral/edit_split': 10, + }, + 'put': { + 'order': 1, + 'algo/order': 1, + }, + 'delete': { + 'order': 1, + 'algo/order': 1, + 'client/order': 1, + 'algo/client/order': 1, + 'algo/orders': 1, + 'orders': 1, + 'batch-order': 1, + 'client/batch-order': 1, + }, + }, + }, + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + 'accountId': True, + 'privateKey': False, + }, + 'fees': { + 'trading': { + 'tierBased': True, + 'percentage': True, + 'maker': self.parse_number('0.0002'), + 'taker': self.parse_number('0.0005'), + }, + }, + 'options': { + 'sandboxMode': False, + 'brokerId': 'CCXTMODE', + 'verifyingContractAddress': '0x6F7a338F2aA472838dEFD3283eB360d4Dff5D203', + }, + 'features': { + 'default': { + 'sandbox': True, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, # todo by triggerPrice + 'takeProfitPrice': False, # todo by triggerPrice + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': True, + 'leverage': True, # todo implement + 'marketBuyByCost': False, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': False, + 'iceberg': True, # todo implement + }, + 'createOrders': { + 'max': 10, + }, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 500, + 'daysBack': None, + 'untilDays': 100000, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': True, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 500, + 'trigger': True, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 500, + 'daysBack': None, + 'daysBackCanceled': None, + 'untilDays': 100000, + 'trigger': True, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 1000, + }, + }, + 'spot': { + 'extends': 'default', + }, + 'forDerivatives': { + 'extends': 'default', + 'createOrder': { + # todo: implementation needs unification + 'triggerPriceType': None, + 'attachedStopLossTakeProfit': { + # todo: implementation needs unification + 'triggerPriceType': None, + 'price': False, + }, + }, + }, + 'swap': { + 'linear': { + 'extends': 'forDerivatives', + }, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'commonCurrencies': {}, + 'exceptions': { + 'exact': { + '-1000': ExchangeError, # UNKNOWN The data does not exist + '-1001': AuthenticationError, # INVALID_SIGNATURE The api key or secret is in wrong format. + '-1002': AuthenticationError, # UNAUTHORIZED API key or secret is invalid, it may because key have insufficient permission or the key is expired/revoked. + '-1003': RateLimitExceeded, # TOO_MANY_REQUEST Rate limit exceed. + '-1004': BadRequest, # UNKNOWN_PARAM An unknown parameter was sent. + '-1005': BadRequest, # INVALID_PARAM Some parameters are in wrong format for api. + '-1006': InvalidOrder, # RESOURCE_NOT_FOUND The data is not found in server. For example, when client try canceling a CANCELLED order, will raise self error. + '-1007': BadRequest, # DUPLICATE_REQUEST The data is already exists or your request is duplicated. + '-1008': InvalidOrder, # QUANTITY_TOO_HIGH The quantity of settlement is too high than you can request. + '-1009': InsufficientFunds, # CAN_NOT_WITHDRAWAL Can not request withdrawal settlement, you need to deposit other arrears first. + '-1011': NetworkError, # RPC_NOT_CONNECT Can not place/cancel orders, it may because internal network error. Please try again in a few seconds. + '-1012': BadRequest, # RPC_REJECT The place/cancel order request is rejected by internal module, it may because the account is in liquidation or other internal errors. Please try again in a few seconds. + '-1101': InsufficientFunds, # RISK_TOO_HIGH The risk exposure for client is too high, it may cause by sending too big order or the leverage is too low. please refer to client info to check the current exposure. + '-1102': InvalidOrder, # MIN_NOTIONAL The order value(price * size) is too small. + '-1103': InvalidOrder, # PRICE_FILTER The order price is not following the tick size rule for the symbol. + '-1104': InvalidOrder, # SIZE_FILTER The order quantity is not following the step size rule for the symbol. + '-1105': InvalidOrder, # PERCENTAGE_FILTER Price is X% too high or X% too low from the mid price. + '-1201': BadRequest, # LIQUIDATION_REQUEST_RATIO_TOO_SMALL total notional < 10000, least req ratio should = 1 + '-1202': BadRequest, # LIQUIDATION_STATUS_ERROR No need to liquidation because user margin is enough. + '29': BadRequest, # {"success":false,"code":29,"message":"Verify contract is invalid"} + '9': AuthenticationError, # {"success":false,"code":9,"message":"Address and signature do not match"} + '3': AuthenticationError, # {"success":false,"code":3,"message":"Signature error"} + '2': BadRequest, # {"success":false,"code":2,"message":"Timestamp expired"} + '15': BadRequest, # {"success":false,"code":15,"message":"BrokerId is not exist"} + }, + 'broad': { + }, + }, + 'precisionMode': TICK_SIZE, + }) + + def set_sandbox_mode(self, enable: bool): + super(modetrade, self).set_sandbox_mode(enable) + self.options['sandboxMode'] = enable + + async def fetch_status(self, params={}): + """ + the latest known information on the availability of the exchange API + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/public/get-system-maintenance-status + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `status structure ` + """ + response = await self.v1PublicGetPublicSystemInfo(params) + # + # { + # "success": True, + # "data": { + # "status": 0, + # "msg": "System is functioning properly." + # }, + # "timestamp": "1709274106602" + # } + # + data = self.safe_dict(response, 'data', {}) + status = self.safe_string(data, 'status') + if status is None: + status = 'error' + elif status == '0': + status = 'ok' + else: + status = 'maintenance' + return { + 'status': status, + 'updated': None, + 'eta': None, + 'url': None, + 'info': response, + } + + async def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/public/get-system-maintenance-status + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = await self.v1PublicGetPublicSystemInfo(params) + # + # { + # "success": True, + # "data": { + # "status": 0, + # "msg": "System is functioning properly." + # }, + # "timestamp": "1709274106602" + # } + # + return self.safe_integer(response, 'timestamp') + + def parse_market(self, market: dict) -> Market: + # + # { + # "symbol": "PERP_BTC_USDC", + # "quote_min": 123, + # "quote_max": 100000, + # "quote_tick": 0.1, + # "base_min": 0.00001, + # "base_max": 20, + # "base_tick": 0.00001, + # "min_notional": 1, + # "price_range": 0.02, + # "price_scope": 0.4, + # "std_liquidation_fee": 0.03, + # "liquidator_fee": 0.015, + # "claim_insurance_fund_discount": 0.0075, + # "funding_period": 8, + # "cap_funding": 0.000375, + # "floor_funding": -0.000375, + # "interest_rate": 0.0001, + # "created_time": 1684140107326, + # "updated_time": 1685345968053, + # "base_mmr": 0.05, + # "base_imr": 0.1, + # "imr_factor": 0.0002512, + # "liquidation_tier": "1" + # } + # + marketId = self.safe_string(market, 'symbol') + parts = marketId.split('_') + marketType = 'swap' + baseId = self.safe_string(parts, 1) + quoteId = self.safe_string(parts, 2) + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + settleId: Str = self.safe_string(parts, 2) + settle: Str = self.safe_currency_code(settleId) + symbol = base + '/' + quote + ':' + settle + return { + 'id': marketId, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': marketType, + 'spot': False, + 'margin': False, + 'swap': True, + 'future': False, + 'option': False, + 'active': None, + 'contract': True, + 'linear': True, + 'inverse': False, + 'contractSize': self.parse_number('1'), + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.safe_number(market, 'base_tick'), + 'price': self.safe_number(market, 'quote_tick'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(market, 'base_min'), + 'max': self.safe_number(market, 'base_max'), + }, + 'price': { + 'min': self.safe_number(market, 'quote_min'), + 'max': self.safe_number(market, 'quote_max'), + }, + 'cost': { + 'min': self.safe_number(market, 'min_notional'), + 'max': None, + }, + }, + 'created': self.safe_integer(market, 'created_time'), + 'info': market, + } + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for modetrade + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/public/get-available-symbols + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = await self.v1PublicGetPublicInfo(params) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "rows": [ + # { + # "symbol": "PERP_BTC_USDC", + # "quote_min": 123, + # "quote_max": 100000, + # "quote_tick": 0.1, + # "base_min": 0.00001, + # "base_max": 20, + # "base_tick": 0.00001, + # "min_notional": 1, + # "price_range": 0.02, + # "price_scope": 0.4, + # "std_liquidation_fee": 0.03, + # "liquidator_fee": 0.015, + # "claim_insurance_fund_discount": 0.0075, + # "funding_period": 8, + # "cap_funding": 0.000375, + # "floor_funding": -0.000375, + # "interest_rate": 0.0001, + # "created_time": 1684140107326, + # "updated_time": 1685345968053, + # "base_mmr": 0.05, + # "base_imr": 0.1, + # "imr_factor": 0.0002512, + # "liquidation_tier": "1" + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + rows = self.safe_list(data, 'rows', []) + return self.parse_markets(rows) + + async def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/public/get-token-info + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + result: dict = {} + response = await self.v1PublicGetPublicToken(params) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "rows": [{ + # "token": "USDC", + # "decimals": 6, + # "minimum_withdraw_amount": 0.000001, + # "token_hash": "0xd6aca1be9729c13d677335161321649cccae6a591554772516700f986f942eaa", + # "chain_details": [{ + # "chain_id": 43113, + # "contract_address": "0x5d64c9cfb0197775b4b3ad9be4d3c7976e0d8dc3", + # "cross_chain_withdrawal_fee": 123, + # "decimals": 6, + # "withdraw_fee": 2 + # }] + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + tokenRows = self.safe_list(data, 'rows', []) + for i in range(0, len(tokenRows)): + token = tokenRows[i] + currencyId = self.safe_string(token, 'token') + networks = self.safe_list(token, 'chain_details') + code = self.safe_currency_code(currencyId) + minPrecision = None + resultingNetworks: dict = {} + for j in range(0, len(networks)): + network = networks[j] + # TODO: transform chain id to human readable name + networkId = self.safe_string(network, 'chain_id') + precision = self.parse_precision(self.safe_string(network, 'decimals')) + if precision is not None: + minPrecision = precision if (minPrecision is None) else Precise.string_min(precision, minPrecision) + resultingNetworks[networkId] = { + 'id': networkId, + 'network': networkId, + 'limits': { + 'withdraw': { + 'min': None, + 'max': None, + }, + 'deposit': { + 'min': None, + 'max': None, + }, + }, + 'active': None, + 'deposit': None, + 'withdraw': None, + 'fee': self.safe_number(network, 'withdrawal_fee'), + 'precision': self.parse_number(precision), + 'info': network, + } + result[code] = self.safe_currency_structure({ + 'id': currencyId, + 'name': currencyId, + 'code': code, + 'precision': self.parse_number(minPrecision), + 'active': None, + 'fee': None, + 'networks': resultingNetworks, + 'deposit': None, + 'withdraw': None, + 'limits': { + 'deposit': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': self.safe_number(token, 'minimum_withdraw_amount'), + 'max': None, + }, + }, + 'info': token, + }) + return result + + def parse_token_and_fee_temp(self, item, feeTokenKey, feeAmountKey): + feeCost = self.safe_string(item, feeAmountKey) + fee = None + if feeCost is not None: + feeCurrencyId = self.safe_string(item, feeTokenKey) + feeCurrencyCode = self.safe_currency_code(feeCurrencyId) + fee = { + 'cost': feeCost, + 'currency': feeCurrencyCode, + } + return fee + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # public/market_trades + # + # { + # "symbol": "PERP_ETH_USDC", + # "side": "SELL", + # "executed_price": 46222.35, + # "executed_quantity": 0.0012, + # "executed_timestamp": "1683878609166" + # } + # + # fetchOrderTrades, fetchOrder + # + # { + # "id": "99119876", + # "symbol": "PERP_BTC_USDC", + # "fee": "0.0024", + # "side": "BUY", + # "executed_timestamp": "1641481113084", + # "order_id": "87001234", + # "order_tag": "default", <-- self param only in "fetchOrderTrades" + # "executed_price": "1", + # "executed_quantity": "12", + # "fee_asset": "BTC", + # "is_maker": "1" + # } + # + isFromFetchOrder = ('id' in trade) + timestamp = self.safe_integer(trade, 'executed_timestamp') + marketId = self.safe_string(trade, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + price = self.safe_string(trade, 'executed_price') + amount = self.safe_string(trade, 'executed_quantity') + order_id = self.safe_string(trade, 'order_id') + fee = self.parse_token_and_fee_temp(trade, 'fee_asset', 'fee') + feeCost = self.safe_string(fee, 'cost') + if feeCost is not None: + fee['cost'] = feeCost + cost = Precise.string_mul(price, amount) + side = self.safe_string_lower(trade, 'side') + id = self.safe_string(trade, 'id') + takerOrMaker: Str = None + if isFromFetchOrder: + isMaker = self.safe_string(trade, 'is_maker') == '1' + takerOrMaker = 'maker' if isMaker else 'taker' + return self.safe_trade({ + 'id': id, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'side': side, + 'price': price, + 'amount': amount, + 'cost': cost, + 'order': order_id, + 'takerOrMaker': takerOrMaker, + 'type': None, + 'fee': fee, + 'info': trade, + }, market) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/public/get-market-trades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['limit'] = limit + response = await self.v1PublicGetPublicMarketTrades(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "rows": [{ + # "symbol": "PERP_ETH_USDC", + # "side": "BUY", + # "executed_price": 2050, + # "executed_quantity": 1, + # "executed_timestamp": 1683878609166 + # }] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + rows = self.safe_list(data, 'rows', []) + return self.parse_trades(rows, market, since, limit) + + def parse_funding_rate(self, fundingRate, market: Market = None) -> FundingRate: + # + # { + # "symbol":"PERP_AAVE_USDT", + # "est_funding_rate":-0.00003447, + # "est_funding_rate_timestamp":1653633959001, + # "last_funding_rate":-0.00002094, + # "last_funding_rate_timestamp":1653631200000, + # "next_funding_time":1653634800000, + # "sum_unitary_funding": 521.367 + # } + # + symbol = self.safe_string(fundingRate, 'symbol') + market = self.market(symbol) + nextFundingTimestamp = self.safe_integer(fundingRate, 'next_funding_time') + estFundingRateTimestamp = self.safe_integer(fundingRate, 'est_funding_rate_timestamp') + lastFundingRateTimestamp = self.safe_integer(fundingRate, 'last_funding_rate_timestamp') + fundingTimeString = self.safe_string(fundingRate, 'last_funding_rate_timestamp') + nextFundingTimeString = self.safe_string(fundingRate, 'next_funding_time') + millisecondsInterval = Precise.string_sub(nextFundingTimeString, fundingTimeString) + return { + 'info': fundingRate, + 'symbol': market['symbol'], + 'markPrice': None, + 'indexPrice': None, + 'interestRate': self.parse_number('0'), + 'estimatedSettlePrice': None, + 'timestamp': estFundingRateTimestamp, + 'datetime': self.iso8601(estFundingRateTimestamp), + 'fundingRate': self.safe_number(fundingRate, 'est_funding_rate'), + 'fundingTimestamp': nextFundingTimestamp, + 'fundingDatetime': self.iso8601(nextFundingTimestamp), + 'nextFundingRate': None, + 'nextFundingTimestamp': None, + 'nextFundingDatetime': None, + 'previousFundingRate': self.safe_number(fundingRate, 'last_funding_rate'), + 'previousFundingTimestamp': lastFundingRateTimestamp, + 'previousFundingDatetime': self.iso8601(lastFundingRateTimestamp), + 'interval': self.parse_funding_interval(millisecondsInterval), + } + + def parse_funding_interval(self, interval): + intervals: dict = { + '3600000': '1h', + '14400000': '4h', + '28800000': '8h', + '57600000': '16h', + '86400000': '24h', + } + return self.safe_string(intervals, interval, interval) + + async def fetch_funding_interval(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate interval + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/public/get-predicted-funding-rate-for-one-market + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + return await self.fetch_funding_rate(symbol, params) + + async def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/public/get-predicted-funding-rate-for-one-market + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = await self.v1PublicGetPublicFundingRateSymbol(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "symbol": "PERP_ETH_USDC", + # "est_funding_rate": 123, + # "est_funding_rate_timestamp": 1683880020000, + # "last_funding_rate": 0.0001, + # "last_funding_rate_timestamp": 1683878400000, + # "next_funding_time": 1683907200000, + # "sum_unitary_funding": 521.367 + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_funding_rate(data, market) + + async def fetch_funding_rates(self, symbols: Strings = None, params={}) -> FundingRates: + """ + fetch the current funding rate for multiple markets + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/public/get-predicted-funding-rates-for-all-markets + + :param str[] symbols: unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of `funding rate structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + response = await self.v1PublicGetPublicFundingRates(params) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "rows": [{ + # "symbol": "PERP_ETH_USDC", + # "est_funding_rate": 123, + # "est_funding_rate_timestamp": 1683880020000, + # "last_funding_rate": 0.0001, + # "last_funding_rate_timestamp": 1683878400000, + # "next_funding_time": 1683907200000, + # "sum_unitary_funding": 521.367 + # }] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + rows = self.safe_list(data, 'rows', []) + return self.parse_funding_rates(rows, symbols) + + async def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical funding rate prices + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/public/get-funding-rate-history-for-one-market + + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: timestamp in ms of the earliest funding rate to fetch + :param int [limit]: the maximum amount of `funding rate structures ` to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest funding rate + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `funding rate structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchFundingRateHistory', 'paginate') + if paginate: + return await self.fetch_paginated_call_incremental('fetchFundingRateHistory', symbol, since, limit, params, 'page', 25) + request: dict = {} + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + request['symbol'] = market['id'] + if since is not None: + request['start_t'] = since + request, params = self.handle_until_option('end_t', request, params, 0.001) + response = await self.v1PublicGetPublicFundingRateHistory(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "rows": [{ + # "symbol": "PERP_ETH_USDC", + # "funding_rate": 0.0001, + # "funding_rate_timestamp": 1684224000000, + # "next_funding_time": 1684252800000 + # }], + # "meta": { + # "total": 9, + # "records_per_page": 25, + # "current_page": 1 + # } + # } + # } + # + data = self.safe_dict(response, 'data', {}) + result = self.safe_list(data, 'rows', []) + rates = [] + for i in range(0, len(result)): + entry = result[i] + marketId = self.safe_string(entry, 'symbol') + timestamp = self.safe_integer(entry, 'funding_rate_timestamp') + rates.append({ + 'info': entry, + 'symbol': self.safe_symbol(marketId), + 'fundingRate': self.safe_number(entry, 'funding_rate'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + sorted = self.sort_by(rates, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, symbol, since, limit) + + def parse_income(self, income, market: Market = None): + # + # { + # "symbol": "PERP_ETH_USDC", + # "funding_rate": 0.00046875, + # "mark_price": 2100, + # "funding_fee": 0.000016, + # "payment_type": "Pay", + # "status": "Accrued", + # "created_time": 1682235722003, + # "updated_time": 1682235722003 + # } + # + marketId = self.safe_string(income, 'symbol') + symbol = self.safe_symbol(marketId, market) + amount = self.safe_string(income, 'funding_fee') + code = self.safe_currency_code('USDC') + timestamp = self.safe_integer(income, 'updated_time') + rate = self.safe_number(income, 'funding_rate') + paymentType = self.safe_string(income, 'payment_type') + amount = Precise.string_neg(amount) if (paymentType == 'Pay') else amount + return { + 'info': income, + 'symbol': symbol, + 'code': code, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': None, + 'amount': self.parse_number(amount), + 'rate': rate, + } + + async def fetch_funding_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch the history of funding payments paid and received on self account + + https://orderly.network/docs/build-on-omnichain/evm-api/restful-api/private/get-funding-fee-history + + :param str [symbol]: unified market symbol + :param int [since]: the earliest time in ms to fetch funding history for + :param int [limit]: the maximum number of funding history structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict: a `funding history structure ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchFundingHistory', 'paginate') + if paginate: + return await self.fetch_paginated_call_incremental('fetchFundingHistory', symbol, since, limit, params, 'page', 500) + request: dict = {} + market: Market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if since is not None: + request['start_t'] = since + until = self.safe_integer(params, 'until') # unified in milliseconds + params = self.omit(params, ['until']) + if until is not None: + request['end_t'] = until + if limit is not None: + request['size'] = min(limit, 500) + response = await self.v1PrivateGetFundingFeeHistory(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "meta": { + # "total": 9, + # "records_per_page": 25, + # "current_page": 1 + # }, + # "rows": [{ + # "symbol": "PERP_ETH_USDC", + # "funding_rate": 0.00046875, + # "mark_price": 2100, + # "funding_fee": 0.000016, + # "payment_type": "Pay", + # "status": "Accrued", + # "created_time": 1682235722003, + # "updated_time": 1682235722003 + # }] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + rows = self.safe_list(data, 'rows', []) + return self.parse_incomes(rows, market, since, limit) + + async def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-account-information + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + await self.load_markets() + response = await self.v1PrivateGetClientInfo(params) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "account_id": "", + # "email": "test@test.com", + # "account_mode": "FUTURES", + # "max_leverage": 20, + # "taker_fee_rate": 123, + # "maker_fee_rate": 123, + # "futures_taker_fee_rate": 123, + # "futures_maker_fee_rate": 123, + # "maintenance_cancel_orders": True, + # "imr_factor": { + # "PERP_BTC_USDC": 123, + # "PERP_ETH_USDC": 123, + # "PERP_NEAR_USDC": 123 + # }, + # "max_notional": { + # "PERP_BTC_USDC": 123, + # "PERP_ETH_USDC": 123, + # "PERP_NEAR_USDC": 123 + # } + # } + # } + # + data = self.safe_dict(response, 'data', {}) + maker = self.safe_string(data, 'futures_maker_fee_rate') + taker = self.safe_string(data, 'futures_taker_fee_rate') + result: dict = {} + for i in range(0, len(self.symbols)): + symbol = self.symbols[i] + result[symbol] = { + 'info': response, + 'symbol': symbol, + 'maker': self.parse_number(Precise.string_div(maker, '10000')), + 'taker': self.parse_number(Precise.string_div(taker, '10000')), + 'percentage': True, + 'tierBased': True, + } + return result + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/orderbook-snapshot + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + limit = min(limit, 1000) + request['max_level'] = limit + response = await self.v1PrivateGetOrderbookSymbol(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "asks": [{ + # "price": 10669.4, + # "quantity": 1.56263218 + # }], + # "bids": [{ + # "price": 10669.4, + # "quantity": 1.56263218 + # }], + # "timestamp": 123 + # } + # } + # + data = self.safe_dict(response, 'data', {}) + timestamp = self.safe_integer(data, 'timestamp') + return self.parse_order_book(data, symbol, timestamp, 'bids', 'asks', 'price', 'quantity') + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + return [ + self.safe_integer(ohlcv, 'start_timestamp'), + self.safe_number(ohlcv, 'open'), + self.safe_number(ohlcv, 'high'), + self.safe_number(ohlcv, 'low'), + self.safe_number(ohlcv, 'close'), + self.safe_number(ohlcv, 'volume'), + ] + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-kline + + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: max=1000, max=100 when since is defined and is less than(now - (999 * (timeframe in ms))) + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'type': self.safe_string(self.timeframes, timeframe, timeframe), + } + if limit is not None: + request['limit'] = min(limit, 1000) + response = await self.v1PrivateGetKline(self.extend(request, params)) + data = self.safe_dict(response, 'data', {}) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "rows": [{ + # "open": 66166.23, + # "close": 66124.56, + # "low": 66038.06, + # "high": 66176.97, + # "volume": 23.45528526, + # "amount": 1550436.21725288, + # "symbol": "PERP_BTC_USDC", + # "type": "1m", + # "start_timestamp": 1636388220000, + # "end_timestamp": 1636388280000 + # }] + # } + # } + # + rows = self.safe_list(data, 'rows', []) + return self.parse_ohlcvs(rows, market, timeframe, since, limit) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # Possible input functions: + # * createOrder + # * createOrders + # * cancelOrder + # * fetchOrder + # * fetchOrders + # isFromFetchOrder = ('order_tag' in order); TO_DO + # + # stop order after creating it: + # { + # "orderId": "1578938", + # "clientOrderId": "0", + # "algoType": "STOP_LOSS", + # "quantity": "0.1" + # } + # stop order after fetching it: + # { + # "algoOrderId": "1578958", + # "clientOrderId": "0", + # "rootAlgoOrderId": "1578958", + # "parentAlgoOrderId": "0", + # "symbol": "SPOT_LTC_USDT", + # "orderTag": "default", + # "algoType": "STOP_LOSS", + # "side": "BUY", + # "quantity": "0.1", + # "isTriggered": False, + # "triggerPrice": "100", + # "triggerStatus": "USELESS", + # "type": "LIMIT", + # "rootAlgoStatus": "CANCELLED", + # "algoStatus": "CANCELLED", + # "triggerPriceType": "MARKET_PRICE", + # "price": "75", + # "triggerTime": "0", + # "totalExecutedQuantity": "0", + # "averageExecutedPrice": "0", + # "totalFee": "0", + # "feeAsset": '', + # "reduceOnly": False, + # "createdTime": "1686149609.744", + # "updatedTime": "1686149903.362" + # } + # + timestamp = self.safe_integer_n(order, ['timestamp', 'created_time', 'createdTime']) + orderId = self.safe_string_n(order, ['order_id', 'orderId', 'algoOrderId']) + clientOrderId = self.omit_zero(self.safe_string_2(order, 'client_order_id', 'clientOrderId')) # Somehow, self always returns 0 for limit order + marketId = self.safe_string(order, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + price = self.safe_string_2(order, 'order_price', 'price') + amount = self.safe_string_2(order, 'order_quantity', 'quantity') # This is base amount + cost = self.safe_string_2(order, 'order_amount', 'amount') # This is quote amount + orderType = self.safe_string_lower_2(order, 'order_type', 'type') + status = self.safe_value_2(order, 'status', 'algoStatus') + success = self.safe_bool(order, 'success') + if success is not None: + status = 'NEW' if (success) else 'REJECTED' + side = self.safe_string_lower(order, 'side') + filled = self.omit_zero(self.safe_value_2(order, 'executed', 'totalExecutedQuantity')) + average = self.omit_zero(self.safe_string_2(order, 'average_executed_price', 'averageExecutedPrice')) + remaining = Precise.string_sub(cost, filled) + fee = self.safe_value_2(order, 'total_fee', 'totalFee') + feeCurrency = self.safe_string_2(order, 'fee_asset', 'feeAsset') + transactions = self.safe_value(order, 'Transactions') + triggerPrice = self.safe_number(order, 'triggerPrice') + takeProfitPrice: Num = None + stopLossPrice: Num = None + childOrders = self.safe_value(order, 'childOrders') + if childOrders is not None: + first = self.safe_value(childOrders, 0) + innerChildOrders = self.safe_value(first, 'childOrders', []) + innerChildOrdersLength = len(innerChildOrders) + if innerChildOrdersLength > 0: + takeProfitOrder = self.safe_value(innerChildOrders, 0) + stopLossOrder = self.safe_value(innerChildOrders, 1) + takeProfitPrice = self.safe_number(takeProfitOrder, 'triggerPrice') + stopLossPrice = self.safe_number(stopLossOrder, 'triggerPrice') + lastUpdateTimestamp = self.safe_integer_2(order, 'updatedTime', 'updated_time') + return self.safe_order({ + 'id': orderId, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'lastUpdateTimestamp': lastUpdateTimestamp, + 'status': self.parse_order_status(status), + 'symbol': symbol, + 'type': self.parse_order_type(orderType), + 'timeInForce': self.parse_time_in_force(orderType), + 'postOnly': None, # TO_DO + 'reduceOnly': self.safe_bool(order, 'reduce_only'), + 'side': side, + 'price': price, + 'triggerPrice': triggerPrice, + 'takeProfitPrice': takeProfitPrice, + 'stopLossPrice': stopLossPrice, + 'average': average, + 'amount': amount, + 'filled': filled, + 'remaining': remaining, # TO_DO + 'cost': cost, + 'trades': transactions, + 'fee': { + 'cost': fee, + 'currency': feeCurrency, + }, + 'info': order, + }, market) + + def parse_time_in_force(self, timeInForce: Str): + timeInForces: dict = { + 'ioc': 'IOC', + 'fok': 'FOK', + 'post_only': 'PO', + } + return self.safe_string(timeInForces, timeInForce, None) + + def parse_order_status(self, status: Str): + if status is not None: + statuses: dict = { + 'NEW': 'open', + 'FILLED': 'closed', + 'CANCEL_SENT': 'canceled', + 'CANCEL_ALL_SENT': 'canceled', + 'CANCELLED': 'canceled', + 'PARTIAL_FILLED': 'open', + 'REJECTED': 'rejected', + 'INCOMPLETE': 'open', + 'COMPLETED': 'closed', + } + return self.safe_string(statuses, status, status) + return status + + def parse_order_type(self, type: Str): + types: dict = { + 'LIMIT': 'limit', + 'MARKET': 'market', + 'POST_ONLY': 'limit', + } + return self.safe_string_lower(types, type, type) + + def create_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + @ignore + helper function to build the request + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency + :param float [price]: the price that the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: request to be sent to the exchange + """ + reduceOnly = self.safe_bool_2(params, 'reduceOnly', 'reduce_only') + orderType = type.upper() + market = self.market(symbol) + orderSide = side.upper() + request: dict = { + 'symbol': market['id'], + 'side': orderSide, + } + triggerPrice = self.safe_string_2(params, 'triggerPrice', 'stopPrice') + stopLoss = self.safe_value(params, 'stopLoss') + takeProfit = self.safe_value(params, 'takeProfit') + algoType = self.safe_string(params, 'algoType') + isConditional = triggerPrice is not None or stopLoss is not None or takeProfit is not None or (self.safe_value(params, 'childOrders') is not None) + isMarket = orderType == 'MARKET' + timeInForce = self.safe_string_lower(params, 'timeInForce') + postOnly = self.is_post_only(isMarket, None, params) + orderQtyKey = 'quantity' if isConditional else 'order_quantity' + priceKey = 'price' if isConditional else 'order_price' + typeKey = 'type' if isConditional else 'order_type' + request[typeKey] = orderType # LIMIT/MARKET/IOC/FOK/POST_ONLY/ASK/BID + if not isConditional: + if postOnly: + request['order_type'] = 'POST_ONLY' + elif timeInForce == 'fok': + request['order_type'] = 'FOK' + elif timeInForce == 'ioc': + request['order_type'] = 'IOC' + if reduceOnly: + request['reduce_only'] = reduceOnly + if price is not None: + request[priceKey] = self.price_to_precision(symbol, price) + if isMarket and not isConditional: + request[orderQtyKey] = self.amount_to_precision(symbol, amount) + elif algoType != 'POSITIONAL_TP_SL': + request[orderQtyKey] = self.amount_to_precision(symbol, amount) + clientOrderId = self.safe_string_n(params, ['clOrdID', 'clientOrderId', 'client_order_id']) + if clientOrderId is not None: + request['client_order_id'] = clientOrderId + if triggerPrice is not None: + request['trigger_price'] = self.price_to_precision(symbol, triggerPrice) + request['algo_type'] = 'STOP' + elif (stopLoss is not None) or (takeProfit is not None): + request['algo_type'] = 'TP_SL' + outterOrder: dict = { + 'symbol': market['id'], + 'reduce_only': False, + 'algo_type': 'POSITIONAL_TP_SL', + 'child_orders': [], + } + childOrders = outterOrder['child_orders'] + closeSide = 'SELL' if (orderSide == 'BUY') else 'BUY' + if stopLoss is not None: + stopLossPrice = self.safe_number_2(stopLoss, 'triggerPrice', 'price', stopLoss) + stopLossOrder: dict = { + 'side': closeSide, + 'algo_type': 'TP_SL', + 'trigger_price': self.price_to_precision(symbol, stopLossPrice), + 'type': 'LIMIT', + 'reduce_only': True, + } + childOrders.append(stopLossOrder) + if takeProfit is not None: + takeProfitPrice = self.safe_number_2(takeProfit, 'triggerPrice', 'price', takeProfit) + takeProfitOrder: dict = { + 'side': closeSide, + 'algo_type': 'TP_SL', + 'trigger_price': self.price_to_precision(symbol, takeProfitPrice), + 'type': 'LIMIT', + 'reduce_only': True, + } + outterOrder.append(takeProfitOrder) + request['child_orders'] = [outterOrder] + params = self.omit(params, ['reduceOnly', 'reduce_only', 'clOrdID', 'clientOrderId', 'client_order_id', 'postOnly', 'timeInForce', 'stopPrice', 'triggerPrice', 'stopLoss', 'takeProfit']) + return self.extend(request, params) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/create-order + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/create-algo-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: The price a trigger order is triggered at + :param dict [params.takeProfit]: *takeProfit object in params* containing the triggerPrice at which the attached take profit order will be triggered(perpetual swap markets only) + :param float [params.takeProfit.triggerPrice]: take profit trigger price + :param dict [params.stopLoss]: *stopLoss object in params* containing the triggerPrice at which the attached stop loss order will be triggered(perpetual swap markets only) + :param float [params.stopLoss.triggerPrice]: stop loss trigger price + :param float [params.algoType]: 'STOP'or 'TP_SL' or 'POSITIONAL_TP_SL' + :param float [params.cost]: *spot market buy only* the quote quantity that can be used alternative for the amount + :param str [params.clientOrderId]: a unique id for the order + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + request = self.create_order_request(symbol, type, side, amount, price, params) + triggerPrice = self.safe_string_2(params, 'triggerPrice', 'stopPrice') + stopLoss = self.safe_value(params, 'stopLoss') + takeProfit = self.safe_value(params, 'takeProfit') + isConditional = triggerPrice is not None or stopLoss is not None or takeProfit is not None or (self.safe_value(params, 'childOrders') is not None) + response = None + if isConditional: + response = await self.v1PrivatePostAlgoOrder(request) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "order_id": 13, + # "client_order_id": "testclientid", + # "algo_type": "STOP", + # "quantity": 100.12 + # } + # } + # + else: + response = await self.v1PrivatePostOrder(request) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "order_id": 13, + # "client_order_id": "testclientid", + # "order_type": "LIMIT", + # "order_price": 100.12, + # "order_quantity": 0.987654, + # "order_amount": 0.8, + # "error_message": "none" + # } + # } + # + data = self.safe_dict(response, 'data') + data['timestamp'] = self.safe_integer(response, 'timestamp') + order = self.parse_order(data, market) + order['type'] = type + return order + + async def create_orders(self, orders: List[OrderRequest], params={}): + """ + *contract only* create a list of trade orders + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/batch-create-order + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + ordersRequests = [] + for i in range(0, len(orders)): + rawOrder = orders[i] + marketId = self.safe_string(rawOrder, 'symbol') + type = self.safe_string(rawOrder, 'type') + side = self.safe_string(rawOrder, 'side') + amount = self.safe_value(rawOrder, 'amount') + price = self.safe_value(rawOrder, 'price') + orderParams = self.safe_dict(rawOrder, 'params', {}) + triggerPrice = self.safe_string_2(orderParams, 'triggerPrice', 'stopPrice') + stopLoss = self.safe_value(orderParams, 'stopLoss') + takeProfit = self.safe_value(orderParams, 'takeProfit') + isConditional = triggerPrice is not None or stopLoss is not None or takeProfit is not None or (self.safe_value(orderParams, 'childOrders') is not None) + if isConditional: + raise NotSupported(self.id + ' createOrders() only support non-stop order') + orderRequest = self.create_order_request(marketId, type, side, amount, price, orderParams) + ordersRequests.append(orderRequest) + request: dict = { + 'orders': ordersRequests, + } + response = await self.v1PrivatePostBatchOrder(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "rows": [{ + # "order_id": 13, + # "client_order_id": "testclientid", + # "order_type": "LIMIT", + # "order_price": 100.12, + # "order_quantity": 0.987654, + # "order_amount": 0.8, + # "error_message": "none" + # }] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + rows = self.safe_list(data, 'rows', []) + return self.parse_orders(rows) + + async def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + """ + edit a trade order + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/edit-order + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/edit-algo-order + + :param str id: order id + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: The price a trigger order is triggered at + :param float [params.stopLossPrice]: price to trigger stop-loss orders + :param float [params.takeProfitPrice]: price to trigger take-profit orders + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'order_id': id, + } + triggerPrice = self.safe_string_n(params, ['triggerPrice', 'stopPrice', 'takeProfitPrice', 'stopLossPrice']) + if triggerPrice is not None: + request['triggerPrice'] = self.price_to_precision(symbol, triggerPrice) + isConditional = (triggerPrice is not None) or (self.safe_value(params, 'childOrders') is not None) + orderQtyKey = 'quantity' if isConditional else 'order_quantity' + priceKey = 'price' if isConditional else 'order_price' + if price is not None: + request[priceKey] = self.price_to_precision(symbol, price) + if amount is not None: + request[orderQtyKey] = self.amount_to_precision(symbol, amount) + params = self.omit(params, ['stopPrice', 'triggerPrice', 'takeProfitPrice', 'stopLossPrice', 'trailingTriggerPrice', 'trailingAmount', 'trailingPercent']) + response = None + if isConditional: + response = await self.v1PrivatePutAlgoOrder(self.extend(request, params)) + else: + request['symbol'] = market['id'] + request['side'] = side.upper() + orderType = type.upper() + timeInForce = self.safe_string_lower(params, 'timeInForce') + isMarket = orderType == 'MARKET' + postOnly = self.is_post_only(isMarket, None, params) + if postOnly: + request['order_type'] = 'POST_ONLY' + elif timeInForce == 'fok': + request['order_type'] = 'FOK' + elif timeInForce == 'ioc': + request['order_type'] = 'IOC' + else: + request['order_type'] = orderType + clientOrderId = self.safe_string_n(params, ['clOrdID', 'clientOrderId', 'client_order_id']) + params = self.omit(params, ['clOrdID', 'clientOrderId', 'client_order_id', 'postOnly', 'timeInForce']) + if clientOrderId is not None: + request['client_order_id'] = clientOrderId + # request['side'] = side.upper() + # request['symbol'] = market['id'] + response = await self.v1PrivatePutOrder(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "status": "EDIT_SENT" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + data['timestamp'] = self.safe_integer(response, 'timestamp') + return self.parse_order(data, market) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/cancel-order + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/cancel-order-by-client_order_id + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/cancel-algo-order + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/cancel-algo-order-by-client_order_id + + cancels an open order + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: whether the order is a stop/algo order + :param str [params.clientOrderId]: a unique id for the order + :returns dict: An `order structure ` + """ + trigger = self.safe_bool_2(params, 'stop', 'trigger', False) + params = self.omit(params, ['stop', 'trigger']) + if not trigger and (symbol is None): + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + await self.load_markets() + market: Market = None + if symbol is not None: + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + clientOrderIdUnified = self.safe_string_2(params, 'clOrdID', 'clientOrderId') + clientOrderIdExchangeSpecific = self.safe_string(params, 'client_order_id', clientOrderIdUnified) + isByClientOrder = clientOrderIdExchangeSpecific is not None + response = None + if trigger: + if isByClientOrder: + request['client_order_id'] = clientOrderIdExchangeSpecific + params = self.omit(params, ['clOrdID', 'clientOrderId', 'client_order_id']) + response = await self.v1PrivateDeleteAlgoClientOrder(self.extend(request, params)) + else: + request['order_id'] = id + response = await self.v1PrivateDeleteAlgoOrder(self.extend(request, params)) + else: + if isByClientOrder: + request['client_order_id'] = clientOrderIdExchangeSpecific + params = self.omit(params, ['clOrdID', 'clientOrderId', 'client_order_id']) + response = await self.v1PrivateDeleteClientOrder(self.extend(request, params)) + else: + request['order_id'] = id + response = await self.v1PrivateDeleteOrder(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203988, + # "data": { + # "status": "CANCEL_SENT" + # } + # } + # + # { + # "success": True, + # "timestamp": 1702989203988, + # "status": "CANCEL_SENT" + # } + # + extendParams: dict = {'symbol': symbol} + if isByClientOrder: + extendParams['client_order_id'] = clientOrderIdExchangeSpecific + else: + extendParams['id'] = id + if trigger: + return self.extend(self.parse_order(response), extendParams) + data = self.safe_dict(response, 'data', {}) + return self.extend(self.parse_order(data), extendParams) + + async def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): + """ + cancel multiple orders + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/batch-cancel-orders + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/batch-cancel-orders-by-client_order_id + + :param str[] ids: order ids + :param str [symbol]: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str[] [params.client_order_ids]: max length 10 e.g. ["my_id_1","my_id_2"], encode the double quotes. No space after comma + :returns dict: an list of `order structures ` + """ + await self.load_markets() + clientOrderIds = self.safe_list_n(params, ['clOrdIDs', 'clientOrderIds', 'client_order_ids']) + params = self.omit(params, ['clOrdIDs', 'clientOrderIds', 'client_order_ids']) + request: dict = {} + response = None + if clientOrderIds: + request['client_order_ids'] = ','.join(clientOrderIds) + response = await self.v1PrivateDeleteClientBatchOrder(self.extend(request, params)) + else: + request['order_ids'] = ','.join(ids) + response = await self.v1PrivateDeleteBatchOrder(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "status": "CANCEL_ALL_SENT" + # } + # } + # + return [self.safe_order({ + 'info': response, + })] + + async def cancel_all_orders(self, symbol: Str = None, params={}): + """ + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/cancel-all-pending-algo-orders + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/cancel-orders-in-bulk + + cancel all open orders in a market + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: whether the order is a stop/algo order + :returns dict: an list of `order structures ` + """ + await self.load_markets() + trigger = self.safe_bool_2(params, 'stop', 'trigger') + params = self.omit(params, ['stop', 'trigger']) + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + response = None + if trigger: + response = await self.v1PrivateDeleteAlgoOrders(self.extend(request, params)) + else: + response = await self.v1PrivateDeleteOrders(self.extend(request, params)) + # trigger + # { + # "success": True, + # "timestamp": 1702989203989, + # "status": "CANCEL_ALL_SENT" + # } + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "status": "CANCEL_ALL_SENT" + # } + # } + # + return [ + self.safe_order({ + 'info': response, + }), + ] + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-order-by-order_id + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-order-by-client_order_id + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-algo-order-by-order_id + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-algo-order-by-client_order_id + + fetches information on an order made by the user + :param str id: the order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: whether the order is a stop/algo order + :param str [params.clientOrderId]: a unique id for the order + :returns dict: An `order structure ` + """ + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + trigger = self.safe_bool_2(params, 'stop', 'trigger', False) + request: dict = {} + clientOrderId = self.safe_string_n(params, ['clOrdID', 'clientOrderId', 'client_order_id']) + params = self.omit(params, ['stop', 'trigger', 'clOrdID', 'clientOrderId', 'client_order_id']) + response = None + if trigger: + if clientOrderId: + request['client_order_id'] = clientOrderId + response = await self.v1PrivateGetAlgoClientOrderClientOrderId(self.extend(request, params)) + else: + request['oid'] = id + response = await self.v1PrivateGetAlgoOrderOid(self.extend(request, params)) + else: + if clientOrderId: + request['client_order_id'] = clientOrderId + response = await self.v1PrivateGetClientOrderClientOrderId(self.extend(request, params)) + else: + request['oid'] = id + response = await self.v1PrivateGetOrderOid(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "order_id": 78151, + # "user_id": 12345, + # "price": 0.67772, + # "type": "LIMIT", + # "quantity": 20, + # "amount": 10, + # "executed_quantity": 20, + # "total_executed_quantity": 20, + # "visible_quantity": 1, + # "symbol": "PERP_BTC_USDC", + # "side": "BUY", + # "status": "FILLED", + # "total_fee": 0.5, + # "fee_asset": "BTC", + # "client_order_id": 1, + # "average_executed_price": 0.67772, + # "created_time": 1653563963000, + # "updated_time": 1653564213000, + # "realized_pnl": 123 + # } + # } + # + orders = self.safe_dict(response, 'data', response) + return self.parse_order(orders, market) + + async def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-orders + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-algo-orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: whether the order is a stop/algo order + :param boolean [params.is_triggered]: whether the order has been triggered(False by default) + :param str [params.side]: 'buy' or 'sell' + :param boolean [params.paginate]: set to True if you want to fetch orders with pagination + :param int params['until']: timestamp in ms of the latest order to fetch + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + paginate = False + isTrigger = self.safe_bool_2(params, 'stop', 'trigger', False) + maxLimit = 100 if (isTrigger) else 500 + paginate, params = self.handle_option_and_params(params, 'fetchOrders', 'paginate') + if paginate: + return await self.fetch_paginated_call_incremental('fetchOrders', symbol, since, limit, params, 'page', maxLimit) + request: dict = {} + market: Market = None + params = self.omit(params, ['stop', 'trigger']) + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if since is not None: + request['start_t'] = since + if limit is not None: + request['size'] = limit + else: + request['size'] = maxLimit + if isTrigger: + request['algo_type'] = 'STOP' + request, params = self.handle_until_option('end_t', request, params) + response = None + if isTrigger: + response = await self.v1PrivateGetAlgoOrders(self.extend(request, params)) + else: + response = await self.v1PrivateGetOrders(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "meta": { + # "total": 9, + # "records_per_page": 25, + # "current_page": 1 + # }, + # "rows": [{ + # "order_id": 78151, + # "user_id": 12345, + # "price": 0.67772, + # "type": "LIMIT", + # "quantity": 20, + # "amount": 10, + # "executed_quantity": 20, + # "total_executed_quantity": 20, + # "visible_quantity": 1, + # "symbol": "PERP_BTC_USDC", + # "side": "BUY", + # "status": "FILLED", + # "total_fee": 0.5, + # "fee_asset": "BTC", + # "client_order_id": 1, + # "average_executed_price": 0.67772, + # "created_time": 1653563963000, + # "updated_time": 1653564213000, + # "realized_pnl": 123 + # }] + # } + # } + # + data = self.safe_value(response, 'data', response) + orders = self.safe_list(data, 'rows') + return self.parse_orders(orders, market, since, limit) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-orders + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-algo-orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: whether the order is a stop/algo order + :param boolean [params.is_triggered]: whether the order has been triggered(False by default) + :param str [params.side]: 'buy' or 'sell' + :param int params['until']: timestamp in ms of the latest order to fetch + :param boolean [params.paginate]: set to True if you want to fetch orders with pagination + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + extendedParams = self.extend(params, {'status': 'INCOMPLETE'}) + return await self.fetch_orders(symbol, since, limit, extendedParams) + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-orders + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-algo-orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: whether the order is a stop/algo order + :param boolean [params.is_triggered]: whether the order has been triggered(False by default) + :param str [params.side]: 'buy' or 'sell' + :param int params['until']: timestamp in ms of the latest order to fetch + :param boolean [params.paginate]: set to True if you want to fetch orders with pagination + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + extendedParams = self.extend(params, {'status': 'COMPLETED'}) + return await self.fetch_orders(symbol, since, limit, extendedParams) + + async def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all the trades made from a single order + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-all-trades-of-specific-order + + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + market: Market = None + if symbol is not None: + market = self.market(symbol) + request: dict = { + 'oid': id, + } + response = await self.v1PrivateGetOrderOidTrades(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "rows": [{ + # "id": 2, + # "symbol": "PERP_BTC_USDC", + # "fee": 0.0001, + # "fee_asset": "USDC", + # "side": "BUY", + # "order_id": 1, + # "executed_price": 123, + # "executed_quantity": 0.05, + # "executed_timestamp": 1567382401000, + # "is_maker": 1, + # "realized_pnl": 123 + # }] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + trades = self.safe_list(data, 'rows', []) + return self.parse_trades(trades, market, since, limit, params) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-trades + + fetch all trades made by the user + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: set to True if you want to fetch trades with pagination + :param int params['until']: timestamp in ms of the latest trade to fetch + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchMyTrades', 'paginate') + if paginate: + return await self.fetch_paginated_call_incremental('fetchMyTrades', symbol, since, limit, params, 'page', 500) + request: dict = {} + market: Market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if since is not None: + request['start_t'] = since + if limit is not None: + request['size'] = limit + else: + request['size'] = 500 + request, params = self.handle_until_option('end_t', request, params) + response = await self.v1PrivateGetTrades(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "meta": { + # "total": 9, + # "records_per_page": 25, + # "current_page": 1 + # }, + # "rows": [{ + # "id": 2, + # "symbol": "PERP_BTC_USDC", + # "fee": 0.0001, + # "fee_asset": "USDC", + # "side": "BUY", + # "order_id": 1, + # "executed_price": 123, + # "executed_quantity": 0.05, + # "executed_timestamp": 1567382401000, + # "is_maker": 1, + # "realized_pnl": 123 + # }] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + trades = self.safe_list(data, 'rows', []) + return self.parse_trades(trades, market, since, limit, params) + + def parse_balance(self, response) -> Balances: + result: dict = { + 'info': response, + } + balances = self.safe_list(response, 'holding', []) + for i in range(0, len(balances)): + balance = balances[i] + code = self.safe_currency_code(self.safe_string(balance, 'token')) + account = self.account() + account['total'] = self.safe_string(balance, 'holding') + account['used'] = self.safe_string(balance, 'frozen') + result[code] = account + return self.safe_balance(result) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-current-holding + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + response = await self.v1PrivateGetClientHolding(params) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "holding": [{ + # "updated_time": 1580794149000, + # "token": "BTC", + # "holding": -28.000752, + # "frozen": 123, + # "pending_short": -2000 + # }] + # } + # } + # + data = self.safe_dict(response, 'data') + return self.parse_balance(data) + + async def get_asset_history_rows(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> Any: + await self.load_markets() + request: dict = {} + currency: Currency = None + if code is not None: + currency = self.currency(code) + request['balance_token'] = currency['id'] + if since is not None: + request['start_t'] = since + if limit is not None: + request['pageSize'] = limit + transactionType = self.safe_string(params, 'type') + params = self.omit(params, 'type') + if transactionType is not None: + request['type'] = transactionType + response = await self.v1PrivateGetAssetHistory(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "meta": { + # "total": 9, + # "records_per_page": 25, + # "current_page": 1 + # }, + # "rows": [{ + # "id": "230707030600002", + # "tx_id": "0x4b0714c63cc7abae72bf68e84e25860b88ca651b7d27dad1e32bf4c027fa5326", + # "side": "WITHDRAW", + # "token": "USDC", + # "amount": 555, + # "fee": 123, + # "trans_status": "FAILED", + # "created_time": 1688699193034, + # "updated_time": 1688699193096, + # "chain_id": "986532" + # }] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return [currency, self.safe_list(data, 'rows', [])] + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + currencyId = self.safe_string(item, 'token') + code = self.safe_currency_code(currencyId, currency) + currency = self.safe_currency(currencyId, currency) + amount = self.safe_number(item, 'amount') + side = self.safe_string(item, 'token_side') + direction = 'in' if (side == 'DEPOSIT') else 'out' + timestamp = self.safe_integer(item, 'created_time') + fee = self.parse_token_and_fee_temp(item, 'fee_token', 'fee_amount') + return self.safe_ledger_entry({ + 'id': self.safe_string(item, 'id'), + 'currency': code, + 'account': self.safe_string(item, 'account'), + 'referenceAccount': None, + 'referenceId': self.safe_string(item, 'tx_id'), + 'status': self.parse_transaction_status(self.safe_string(item, 'status')), + 'amount': amount, + 'before': None, + 'after': None, + 'fee': fee, + 'direction': direction, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'type': self.parse_ledger_entry_type(self.safe_string(item, 'type')), + 'info': item, + }, currency) + + def parse_ledger_entry_type(self, type): + types: dict = { + 'BALANCE': 'transaction', # Funds moved in/out wallet + 'COLLATERAL': 'transfer', # Funds moved between portfolios + } + return self.safe_string(types, type, type) + + async def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-asset-history + + :param str [code]: unified currency code, default is None + :param int [since]: timestamp in ms of the earliest ledger entry, default is None + :param int [limit]: max number of ledger entries to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ledger structure ` + """ + currencyRows = await self.get_asset_history_rows(code, since, limit, params) + currency = self.safe_value(currencyRows, 0) + rows = self.safe_list(currencyRows, 1) + return self.parse_ledger(rows, currency, since, limit, params) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # example in fetchLedger + code = self.safe_string(transaction, 'token') + movementDirection = self.safe_string_lower(transaction, 'token_side') + if movementDirection == 'withdraw': + movementDirection = 'withdrawal' + fee = self.parse_token_and_fee_temp(transaction, 'fee_token', 'fee_amount') + addressTo = self.safe_string(transaction, 'target_address') + addressFrom = self.safe_string(transaction, 'source_address') + timestamp = self.safe_integer(transaction, 'created_time') + return { + 'info': transaction, + 'id': self.safe_string_2(transaction, 'id', 'withdraw_id'), + 'txid': self.safe_string(transaction, 'tx_id'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'address': None, + 'addressFrom': addressFrom, + 'addressTo': addressTo, + 'tag': self.safe_string(transaction, 'extra'), + 'tagFrom': None, + 'tagTo': None, + 'type': movementDirection, + 'amount': self.safe_number(transaction, 'amount'), + 'currency': code, + 'status': self.parse_transaction_status(self.safe_string(transaction, 'status')), + 'updated': self.safe_integer(transaction, 'updated_time'), + 'comment': None, + 'internal': None, + 'fee': fee, + 'network': None, + } + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'NEW': 'pending', + 'CONFIRMING': 'pending', + 'PROCESSING': 'pending', + 'COMPLETED': 'ok', + 'CANCELED': 'canceled', + } + return self.safe_string(statuses, status, status) + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-asset-history + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + request: dict = { + 'side': 'DEPOSIT', + } + return await self.fetch_deposits_withdrawals(code, since, limit, self.extend(request, params)) + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-asset-history + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + request: dict = { + 'side': 'WITHDRAW', + } + return await self.fetch_deposits_withdrawals(code, since, limit, self.extend(request, params)) + + async def fetch_deposits_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch history of deposits and withdrawals + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-asset-history + + :param str [code]: unified currency code for the currency of the deposit/withdrawals, default is None + :param int [since]: timestamp in ms of the earliest deposit/withdrawal, default is None + :param int [limit]: max number of deposit/withdrawals to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `transaction structure ` + """ + request: dict = {} + currencyRows = await self.get_asset_history_rows(code, since, limit, self.extend(request, params)) + currency = self.safe_value(currencyRows, 0) + rows = self.safe_list(currencyRows, 1) + # + # { + # "rows":[], + # "meta":{ + # "total":0, + # "records_per_page":25, + # "current_page":1 + # }, + # "success":true + # } + # + return self.parse_transactions(rows, currency, since, limit, params) + + async def get_withdraw_nonce(self, params={}): + response = await self.v1PrivateGetWithdrawNonce(params) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "withdraw_nonce": 1 + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.safe_number(data, 'withdraw_nonce') + + def hash_message(self, message): + return '0x' + self.hash(message, 'keccak', 'hex') + + def sign_hash(self, hash, privateKey): + signature = self.ecdsa(hash[-64:], privateKey[-64:], 'secp256k1', None) + r = signature['r'] + s = signature['s'] + v = self.int_to_base16(self.sum(27, signature['v'])) + return '0x' + r.rjust(64, '0') + s.rjust(64, '0') + v + + def sign_message(self, message, privateKey): + return self.sign_hash(self.hash_message(message), privateKey[-64:]) + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/create-withdraw-request + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + await self.load_markets() + self.check_address(address) + if code is not None: + code = code.upper() + if code != 'USDC': + raise NotSupported(self.id + ' withdraw() only support USDC') + currency = self.currency(code) + verifyingContractAddress = self.safe_string(self.options, 'verifyingContractAddress') + chainId = self.safe_string(params, 'chainId') + currencyNetworks = self.safe_dict(currency, 'networks', {}) + coinNetwork = self.safe_dict(currencyNetworks, chainId, {}) + coinNetworkId = self.safe_number(coinNetwork, 'id') + if coinNetworkId is None: + raise BadRequest(self.id + ' withdraw() require chainId parameter') + withdrawNonce = await self.get_withdraw_nonce(params) + nonce = self.nonce() + domain: dict = { + 'chainId': chainId, + 'name': 'Orderly', + 'verifyingContract': verifyingContractAddress, + 'version': '1', + } + messageTypes: dict = { + 'Withdraw': [ + {'name': 'brokerId', 'type': 'string'}, + {'name': 'chainId', 'type': 'uint256'}, + {'name': 'receiver', 'type': 'address'}, + {'name': 'token', 'type': 'string'}, + {'name': 'amount', 'type': 'uint256'}, + {'name': 'withdrawNonce', 'type': 'uint64'}, + {'name': 'timestamp', 'type': 'uint64'}, + ], + } + withdrawRequest: dict = { + 'brokerId': self.safe_string(self.options, 'keyBrokerId', 'mode'), + 'chainId': self.parse_to_int(chainId), + 'receiver': address, + 'token': code, + 'amount': str(amount), + 'withdrawNonce': withdrawNonce, + 'timestamp': nonce, + } + msg = self.eth_encode_structured_data(domain, messageTypes, withdrawRequest) + signature = self.sign_message(msg, self.privateKey) + request: dict = { + 'signature': signature, + 'userAddress': address, + 'verifyingContract': verifyingContractAddress, + 'message': withdrawRequest, + } + params = self.omit(params, 'chainId') + response = await self.v1PrivatePostWithdrawRequest(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "withdraw_id": 123 + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_transaction(data, currency) + + def parse_leverage(self, leverage, market=None) -> Leverage: + leverageValue = self.safe_integer(leverage, 'max_leverage') + return { + 'info': leverage, + 'symbol': market['symbol'], + 'marginMode': None, + 'longLeverage': leverageValue, + 'shortLeverage': leverageValue, + } + + async def fetch_leverage(self, symbol: str, params={}) -> Leverage: + """ + fetch the set leverage for a market + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-account-information + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `leverage structure ` + """ + await self.load_markets() + market = self.market(symbol) + response = await self.v1PrivateGetClientInfo(params) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "account_id": "", + # "email": "test@test.com", + # "account_mode": "FUTURES", + # "max_leverage": 20, + # "taker_fee_rate": 123, + # "maker_fee_rate": 123, + # "futures_taker_fee_rate": 123, + # "futures_maker_fee_rate": 123, + # "maintenance_cancel_orders": True, + # "imr_factor": { + # "PERP_BTC_USDC": 123, + # "PERP_ETH_USDC": 123, + # "PERP_NEAR_USDC": 123 + # }, + # "max_notional": { + # "PERP_BTC_USDC": 123, + # "PERP_ETH_USDC": 123, + # "PERP_NEAR_USDC": 123 + # } + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_leverage(data, market) + + async def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/update-leverage-setting + + :param int [leverage]: the rate of leverage + :param str [symbol]: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + await self.load_markets() + isMinLeverage = leverage < 1 + isMaxLeverage = leverage > 50 + if isMinLeverage or isMaxLeverage: + raise BadRequest(self.id + ' leverage should be between 1 and 50') + request: dict = { + 'leverage': leverage, + } + return await self.v1PrivatePostClientLeverage(self.extend(request, params)) + + def parse_position(self, position: dict, market: Market = None): + # + # { + # "IMR_withdraw_orders": 0.1, + # "MMR_with_orders": 0.05, + # "average_open_price": 27908.14386047, + # "cost_position": -139329.358492, + # "est_liq_price": 117335.92899428, + # "fee_24_h": 123, + # "imr": 0.1, + # "last_sum_unitary_funding": 70.38, + # "mark_price": 27794.9, + # "mmr": 0.05, + # "pending_long_qty": 123, + # "pending_short_qty": 123, + # "pnl_24_h": 123, + # "position_qty": -5, + # "settle_price": 27865.8716984, + # "symbol": "PERP_BTC_USDC", + # "timestamp": 1685429350571, + # "unsettled_pnl": 354.858492 + # } + # + contract = self.safe_string(position, 'symbol') + market = self.safe_market(contract, market) + size = self.safe_string(position, 'position_qty') + side: Str = None + if Precise.string_gt(size, '0'): + side = 'long' + else: + side = 'short' + contractSize = self.safe_string(market, 'contractSize') + markPrice = self.safe_string(position, 'mark_price') + timestamp = self.safe_integer(position, 'timestamp') + entryPrice = self.safe_string(position, 'average_open_price') + unrealisedPnl = self.safe_string(position, 'unsettled_pnl') + size = Precise.string_abs(size) + notional = Precise.string_mul(size, markPrice) + return self.safe_position({ + 'info': position, + 'id': None, + 'symbol': self.safe_string(market, 'symbol'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastUpdateTimestamp': None, + 'initialMargin': None, + 'initialMarginPercentage': None, + 'maintenanceMargin': None, + 'maintenanceMarginPercentage': None, + 'entryPrice': self.parse_number(entryPrice), + 'notional': self.parse_number(notional), + 'leverage': None, + 'unrealizedPnl': self.parse_number(unrealisedPnl), + 'contracts': self.parse_number(size), + 'contractSize': self.parse_number(contractSize), + 'marginRatio': None, + 'liquidationPrice': self.safe_number(position, 'est_liq_price'), + 'markPrice': self.parse_number(markPrice), + 'lastPrice': None, + 'collateral': None, + 'marginMode': 'cross', + 'marginType': None, + 'side': side, + 'percentage': None, + 'hedged': None, + 'stopLossPrice': None, + 'takeProfitPrice': None, + }) + + async def fetch_position(self, symbol: Str, params={}): + """ + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-one-position-info + + fetch data on an open position + :param str symbol: unified market symbol of the market the position is held in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `position structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = await self.v1PrivateGetPositionSymbol(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "IMR_withdraw_orders": 0.1, + # "MMR_with_orders": 0.05, + # "average_open_price": 27908.14386047, + # "cost_position": -139329.358492, + # "est_liq_price": 117335.92899428, + # "fee_24_h": 123, + # "imr": 0.1, + # "last_sum_unitary_funding": 70.38, + # "mark_price": 27794.9, + # "mmr": 0.05, + # "pending_long_qty": 123, + # "pending_short_qty": 123, + # "pnl_24_h": 123, + # "position_qty": -5, + # "settle_price": 27865.8716984, + # "symbol": "PERP_BTC_USDC", + # "timestamp": 1685429350571, + # "unsettled_pnl": 354.858492 + # } + # } + # + data = self.safe_dict(response, 'data') + return self.parse_position(data, market) + + async def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-all-positions-info + + :param str[] [symbols]: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + await self.load_markets() + response = await self.v1PrivateGetPositions(params) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "current_margin_ratio_with_orders": 1.2385, + # "free_collateral": 450315.09115, + # "initial_margin_ratio": 0.1, + # "initial_margin_ratio_with_orders": 0.1, + # "maintenance_margin_ratio": 0.05, + # "maintenance_margin_ratio_with_orders": 0.05, + # "margin_ratio": 1.2385, + # "open_margin_ratio": 1.2102, + # "total_collateral_value": 489865.71329, + # "total_pnl_24_h": 123, + # "rows": [{ + # "IMR_withdraw_orders": 0.1, + # "MMR_with_orders": 0.05, + # "average_open_price": 27908.14386047, + # "cost_position": -139329.358492, + # "est_liq_price": 117335.92899428, + # "fee_24_h": 123, + # "imr": 0.1, + # "last_sum_unitary_funding": 70.38, + # "mark_price": 27794.9, + # "mmr": 0.05, + # "pending_long_qty": 123, + # "pending_short_qty": 123, + # "pnl_24_h": 123, + # "position_qty": -5, + # "settle_price": 27865.8716984, + # "symbol": "PERP_BTC_USDC", + # "timestamp": 1685429350571, + # "unsettled_pnl": 354.858492 + # }] + # } + # } + # + result = self.safe_dict(response, 'data', {}) + positions = self.safe_list(result, 'rows', []) + return self.parse_positions(positions, symbols) + + def nonce(self): + return self.milliseconds() + + def sign(self, path, section='public', method='GET', params={}, headers=None, body=None): + version = section[0] + access = section[1] + pathWithParams = self.implode_params(path, params) + url = self.implode_hostname(self.urls['api'][access]) + url += '/' + version + '/' + params = self.omit(params, self.extract_params(path)) + params = self.keysort(params) + if access == 'public': + url += pathWithParams + if params: + url += '?' + self.urlencode(params) + else: + self.check_required_credentials() + isPostOrPut = method == 'POST' or method == 'PUT' + isOrder = path == 'algo/order' or path == 'order' or path == 'batch-order' + if isPostOrPut and isOrder: + isSandboxMode = self.safe_bool(self.options, 'sandboxMode', False) + if not isSandboxMode: + brokerId = self.safe_string(self.options, 'brokerId', 'CCXTMODE') + if path == 'batch-order': + ordersList = self.safe_list(params, 'orders', []) + for i in range(0, len(ordersList)): + params['orders'][i]['order_tag'] = brokerId + else: + params['order_tag'] = brokerId + params = self.keysort(params) + auth = '' + ts = str(self.nonce()) + url += pathWithParams + apiKey = self.apiKey + if apiKey.find('ed25519:') < 0: + apiKey = 'ed25519:' + apiKey + headers = { + 'orderly-account-id': self.accountId, + 'orderly-key': apiKey, + 'orderly-timestamp': ts, + } + auth = ts + method + '/' + version + '/' + pathWithParams + if method == 'POST' or method == 'PUT': + body = self.json(params) + auth += body + headers['content-type'] = 'application/json' + else: + if params: + url += '?' + self.urlencode(params) + auth += '?' + self.rawencode(params) + headers['content-type'] = 'application/x-www-form-urlencoded' + if method == 'DELETE': + body = '' + secret = self.secret + if secret.find('ed25519:') >= 0: + parts = secret.split('ed25519:') + secret = parts[1] + signature = self.eddsa(self.encode(auth), self.base58_to_binary(secret), 'ed25519') + headers['orderly-signature'] = self.urlencode_base64(self.base64_to_binary(signature)) + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if not response: + return None # fallback to default error handler + # + # 400 Bad Request {"success":false,"code":-1012,"message":"Amount is required for buy market orders when margin disabled."} + # {"code":"-1011","message":"The system is under maintenance.","success":false} + # + success = self.safe_bool(response, 'success') + errorCode = self.safe_string(response, 'code') + if not success: + feedback = self.id + ' ' + self.json(response) + self.throw_broadly_matched_exception(self.exceptions['broad'], body, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + raise ExchangeError(feedback) + return None diff --git a/ccxt/async_support/mt5.py b/ccxt/async_support/mt5.py new file mode 100644 index 0000000..65b9bb6 --- /dev/null +++ b/ccxt/async_support/mt5.py @@ -0,0 +1,673 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.mt5 import ImplicitAPI +import asyncio +import hashlib +from ccxt.base.types import Any, Balances, DepositAddress, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, OrderBooks, Trade, TradingFees, Transaction +from typing import List +from typing import List, Optional # 添加 Optional 导入 +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import DDoSProtection +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.errors import InvalidNonce +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class mt5(Exchange, ImplicitAPI): + def describe(self) -> Any: + return self.deep_extend(super(mt5, self).describe(), { + 'id': 'mt5', + 'name': 'MT5', + 'countries': ['US'], + 'version': 'v2025.02.05-05.23', + 'rateLimit': 1000, + 'hostname': '43.167.188.220:5000', + 'pro': True, + 'options': { + 'host': '18.163.85.196', + 'port': 443, + 'connectTimeoutSeconds': 30, + }, + 'has': { + 'CORS': True, + 'spot': True, + 'margin': True, + 'swap': False, + 'future': False, + 'option': False, + 'cancelOrder': True, + 'createOrder': True, + 'fetchBalance': True, + 'fetchClosedOrders': True, + 'fetchMarkets': True, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchTicker': True, + 'fetchTickers': True, # 添加这个字段 + 'fetchTickers': True, + }, + 'timeframes': { + '1m': 1, + '5m': 5, + '15m': 15, + '30m': 30, + '1h': 60, + '4h': 240, + '1d': 1440, + '1w': 10080, + '1M': 43200, + }, + 'urls': { + 'logo': '', + 'api': { + 'public': 'http://43.167.188.220:5000', # 直接使用具体地址 + 'private': 'http://43.167.188.220:5000', + }, + 'www': 'http://43.167.188.220:5000', + 'doc': ['http://43.167.188.220:5000/index.html'], + }, + 'api': { + 'public': { + 'get': { + 'Ping': 1 + }, + }, + 'private': { + 'get': { + 'Connect': 10, + 'CheckConnect': 1, + 'Disconnect': 1, + 'Symbols': 1, + 'ServerTimezone':1, + 'AccountSummary': 1, + 'AccountDetails': 1, + 'SymbolList': 1, + 'GetQuote': 1, + 'GetQuoteMany': 1, + 'MarketWatchMany': 1, + 'OpenedOrders': 1, + 'ClosedOrders': 1, + 'OpenedOrder': 1, + 'OrderHistory': 1, + 'PriceHistory': 1, + 'OrderSend': 1, + 'OrderModify': 1, + 'OrderClose': 1, + }, + }, + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + 'hostname': True, + }, + 'commonCurrencies': {}, + 'exceptions': { + 'exact': { + 'Invalid token': AuthenticationError, + 'Connection failed': ExchangeError, + 'Invalid symbol': ExchangeError, + 'Invalid order': InvalidOrder, + 'Order not found': OrderNotFound, + }, + }, + }) + + async def get_token(self): + """获取或刷新 token - 异步版本""" + if hasattr(self, 'token') and self.token: + try: + await self.check_connect() + return self.token + except Exception: + # Token 无效,重新连接 + pass + + # 重新连接获取 token + return await self.connect() + + async def connect(self): + """连接到 MT5 账户并获取 token - 异步版本""" + request = { + 'user': self.apiKey, + 'password': self.secret, + 'host': self.options['host'], + 'port': self.options['port'], + 'connectTimeoutSeconds': self.options['connectTimeoutSeconds'], + } + + response = await self.private_get_connect(request) + + self.token = response + return self.token + + async def check_connect(self): + """检查连接状态 - 异步版本""" + request = { + 'id': await self.get_token(), + } + return await self.private_get_checkconnect(request) + + async def fetch_markets(self, params={}): + """获取交易对列表 - 异步修复版本""" + if not hasattr(self, 'token') or not self.token: + await self.get_token() + + request = { + 'id': self.token, + } + + try: + response = await self.private_get_symbols(self.extend(request, params)) + + markets = [] + if isinstance(response, dict): + for symbol, info in response.items(): + try: + market = self.parse_market(info) + if market and market.get('symbol'): + markets.append(market) + except Exception as e: + # 跳过解析失败的市场,继续处理其他市场 + if self.verbose: + print(f"跳过交易对 {symbol}: {e}") + continue + + # 设置市场数据 + if markets: + self.markets = {} + self.symbols = [] + for market in markets: + id = market['id'] + symbol = market['symbol'] + self.markets[id] = market + self.markets[symbol] = market + self.symbols.append(symbol) + + self.symbols = sorted(self.symbols) + self.ids = sorted(self.markets.keys()) + + return markets + + except Exception as e: + raise ExchangeError(f"获取市场数据失败: {e}") + + def parse_market(self, info): + """解析市场信息 - 更健壮的版本""" + try: + # 安全获取 symbol + if not isinstance(info, dict): + return None + + symbol = self.safe_string(info, 'currency', '') + if not symbol: + return None + + symbol = symbol.upper().strip() + + # 确保符号格式正确 (如 EURUSD) + if len(symbol) < 6: + return None + + base = symbol[:3] + quote = symbol[3:] + + # 安全处理精度 + digits = self.safe_integer(info, 'digits', 5) + + # 确保 digits 是整数 + if digits is not None: + try: + digits = int(digits) + except (ValueError, TypeError): + digits = 5 + + market_id = symbol + + return { + 'id': market_id, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'baseId': base, + 'quoteId': quote, + 'active': True, + 'type': 'spot', + 'spot': True, + 'margin': True, + 'precision': { + 'price': digits, + 'amount': 2, + }, + 'limits': { + 'amount': { + 'min': self.safe_number(info, 'minVolume', 0.01), + 'max': self.safe_number(info, 'maxVolume'), + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'info': info, + } + except Exception as e: + if self.verbose: + print(f"解析市场信息失败: {e}, info: {info}") + return None + + async def fetch_balance(self, params={}): + """获取账户余额""" + if not hasattr(self, 'token') or not self.token: + await self.get_token() + + request = { + 'id': self.token, + } + response = await self.private_get_accountsummary(self.extend(request, params)) + return self.parse_balance(response) + + def parse_balance(self, response): + """解析余额信息""" + result = { + 'info': response, + 'timestamp': None, + 'datetime': None, + } + + currency = 'USDT' + balance = self.safe_number(response, 'balance', 0.0) + margin = self.safe_number(response, 'margin', 0.0) + free_margin = self.safe_number(response, 'freeMargin', 0.0) + + result[currency] = { + 'free': free_margin, + 'used': margin, + 'total': balance, + } + + return self.safe_balance(result) + + async def fetch_ticker(self, symbol, params={}): + """获取行情数据""" + await self.load_markets() + market = self.market(symbol) + + if not hasattr(self, 'token') or not self.token: + await self.get_token() + + request = { + 'id': self.token, + 'symbol': market['id'], + } + + response = await self.private_get_getquote(self.extend(request, params)) + return self.parse_ticker(response, market) + + def parse_ticker(self, ticker, market=None): + """解析行情数据""" + symbol = market['symbol'] if market else None + timestamp = None + if ticker.get('time'): + try: + timestamp = self.parse8601(ticker.get('time')) + except: + timestamp = None + + return { + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp) if timestamp else None, + 'high': None, + 'low': None, + 'bid': self.safe_number(ticker, 'bid'), + 'bidVolume': None, + 'ask': self.safe_number(ticker, 'ask'), + 'askVolume': None, + 'vwap': None, + 'open': None, + 'close': None, + 'last': self.safe_number(ticker, 'last'), + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': self.safe_number(ticker, 'volume'), + 'quoteVolume': None, + 'info': ticker, + } + + async def fetch_tickers(self, symbols: Optional[List[str]] = None, params={}): + """异步获取多个交易对的行情数据""" + if not hasattr(self, 'token') or not self.token: + await self.get_token() + + request = { + 'id': self.token, + } + + # 如果指定了特定的交易对 + if symbols is not None: + # 将符号列表转换为 MT5 格式 + mt5_symbols = [] + for symbol in symbols: + market = self.market(symbol) + mt5_symbols.append(market['id']) + request['symbols'] = mt5_symbols + + try: + response = await self.private_get_getquotemany(self.extend(request, params)) + return self.parse_tickers(response, symbols) + except Exception as e: + # 如果批量获取失败,回退到逐个获取 + if symbols is not None: + return await self.fetch_tickers_fallback(symbols, params) + else: + raise ExchangeError(f"获取批量行情失败: {e}") + + async def fetch_tickers_fallback(self, symbols, params={}): + """异步回退方法:逐个获取交易对行情""" + tickers = {} + for symbol in symbols: + try: + ticker = await self.fetch_ticker(symbol, params) + tickers[symbol] = ticker + except Exception as e: + if self.verbose: + print(f"获取 {symbol} 行情失败: {e}") + continue + return tickers + + def parse_tickers(self, response, symbols=None): + """解析批量行情数据(与同步版本相同)""" + tickers = {} + + if isinstance(response, list): + # 如果响应是数组 + for ticker_data in response: + try: + ticker = self.parse_ticker(ticker_data) + if ticker and ticker.get('symbol'): + tickers[ticker['symbol']] = ticker + except Exception as e: + if self.verbose: + print(f"解析行情数据失败: {e}") + continue + elif isinstance(response, dict): + # 如果响应是字典 + for symbol_key, ticker_data in response.items(): + try: + ticker = self.parse_ticker(ticker_data) + if ticker and ticker.get('symbol'): + tickers[ticker['symbol']] = ticker + except Exception as e: + if self.verbose: + print(f"解析行情数据失败 {symbol_key}: {e}") + continue + + # 如果指定了特定的交易对,确保返回的顺序一致 + if symbols is not None: + ordered_tickers = {} + for symbol in symbols: + if symbol in tickers: + ordered_tickers[symbol] = tickers[symbol] + return ordered_tickers + + return tickers + + async def fetch_open_orders(self, symbol=None, since=None, limit=None, params={}): + """异步获取未平仓订单 - 修复版本""" + if not hasattr(self, 'token') or not self.token: + await self.get_token() + + request = { + 'id': self.token, + } + + response = await self.private_get_openedorders(self.extend(request, params)) + + # 如果指定了特定交易对,进行过滤 + if symbol is not None: + market = self.market(symbol) + filtered_orders = [] + for order in response: + if isinstance(order, dict) and order.get('symbol') == market['id']: + filtered_orders.append(order) + return self.parse_orders(filtered_orders, market, since, limit) + else: + return self.parse_orders(response, None, since, limit) + + async def fetch_closed_orders(self, symbol=None, since=None, limit=None, params={}): + """异步获取已平仓订单 - 修复版本""" + if not hasattr(self, 'token') or not self.token: + await self.get_token() + + request = { + 'id': self.token, + } + + response = await self.private_get_closedorders(self.extend(request, params)) + + # 如果指定了特定交易对,进行过滤 + if symbol is not None: + market = self.market(symbol) + filtered_orders = [] + for order in response: + if isinstance(order, dict) and order.get('symbol') == market['id']: + filtered_orders.append(order) + return self.parse_orders(filtered_orders, market, since, limit) + else: + return self.parse_orders(response, None, since, limit) + + def parse_order(self, order, market=None): + """解析订单信息 - 修复市场符号问题""" + try: + id = self.safe_string(order, 'ticket') + market_id = self.safe_string(order, 'symbol') + + # 安全地解析市场符号 + symbol = None + if market is not None: + symbol = market['symbol'] + elif market_id is not None: + # 修复:提供更多参数来正确解析符号 + # 假设 MT5 的符号格式是 BASEQUOTE(如 EURUSD, BTCUSD) + if len(market_id) >= 6: + # 尝试解析为 3+3 格式(如 EURUSD, GBPUSD) + base = market_id[:3] + quote = market_id[3:] + symbol = base + '/' + quote + else: + # 如果无法解析,使用原始 market_id + symbol = market_id + + timestamp = self.parse8601(self.safe_string(order, 'openTime')) + last_trade_timestamp = self.parse8601(self.safe_string(order, 'closeTime')) + + status = self.parse_order_status(self.safe_string(order, 'state')) + side = self.parse_order_side(self.safe_string(order, 'orderType')) + type = self.parse_order_type(self.safe_string(order, 'orderType')) + + price = self.safe_number(order, 'openPrice') + amount = self.safe_number(order, 'lots') + filled = self.safe_number(order, 'closeLots', 0) + + remaining = None + if amount is not None and filled is not None: + remaining = amount - filled + + cost = None + if price is not None and filled is not None: + cost = price * filled + + fee = None + fee_cost = self.safe_number(order, 'commission', 0) + if fee_cost != 0: + fee = { + 'cost': fee_cost, + 'currency': None, + } + + return self.safe_order({ + 'id': id, + 'clientOrderId': None, + 'datetime': self.iso8601(timestamp), + 'timestamp': timestamp, + 'lastTradeTimestamp': last_trade_timestamp, + 'status': status, + 'symbol': symbol, + 'type': type, + 'timeInForce': None, + 'postOnly': None, + 'side': side, + 'price': price, + 'stopPrice': None, + 'triggerPrice': None, + 'amount': amount, + 'filled': filled, + 'remaining': remaining, + 'cost': cost, + 'trades': None, + 'fee': fee, + 'info': order, + 'average': None, + }) + except Exception as e: + if self.verbose: + print(f"解析订单失败: {e}, order: {order}") + raise e + + def parse_order_status(self, status): + statuses = { + 'Started': 'open', + 'Placed': 'open', + 'Cancelled': 'canceled', + 'Partial': 'open', + 'Filled': 'closed', + 'Rejected': 'rejected', + 'Expired': 'expired', + } + return self.safe_string(statuses, status, status) + + def parse_order_side(self, side): + sides = { + 'Buy': 'buy', + 'Sell': 'sell', + 'BuyLimit': 'buy', + 'SellLimit': 'sell', + 'BuyStop': 'buy', + 'SellStop': 'sell', + } + return self.safe_string(sides, side, side) + + def parse_order_type(self, type): + types = { + 'Buy': 'market', + 'Sell': 'market', + 'BuyLimit': 'limit', + 'SellLimit': 'limit', + 'BuyStop': 'stop', + 'SellStop': 'stop', + } + return self.safe_string(types, type, type) + + async def create_order(self, symbol, type, side, amount, price=None, params={}): + """创建订单""" + await self.load_markets() + market = self.market(symbol) + + if not hasattr(self, 'token') or not self.token: + await self.get_token() + + request = { + 'id': self.token, + 'symbol': market['id'], + 'volume': amount, + } + + # 映射订单类型 + operation_map = { + 'market': { + 'buy': 'Buy', + 'sell': 'Sell', + }, + 'limit': { + 'buy': 'BuyLimit', + 'sell': 'SellLimit', + }, + 'stop': { + 'buy': 'BuyStop', + 'sell': 'SellStop', + }, + } + + if type in operation_map and side in operation_map[type]: + request['operation'] = operation_map[type][side] + else: + raise InvalidOrder(self.id + ' createOrder does not support order type ' + type + ' and side ' + side) + + if type in ['limit', 'stop'] and price is not None: + request['price'] = price + + # 处理止损止盈 + stop_loss = self.safe_number(params, 'stopLoss') + take_profit = self.safe_number(params, 'takeProfit') + if stop_loss is not None: + request['stoploss'] = stop_loss + if take_profit is not None: + request['takeprofit'] = take_profit + + response = await self.private_get_ordersend(self.extend(request, params)) + return self.parse_order(response, market) + + async def cancel_order(self, id, symbol=None, params={}): + """取消订单""" + if not hasattr(self, 'token') or not self.token: + await self.get_token() + + request = { + 'id': self.token, + 'ticket': int(id), + } + response = await self.private_get_orderclose(self.extend(request, params)) + return self.parse_order(response) + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + """签名请求""" + base_url = self.urls['api'][api] + url = base_url + '/' + path + query = self.omit(params, self.extract_params(path)) + + if method == 'GET' and query: + url += '?' + self.urlencode(query) + + # 调试信息 + if self.verbose: + print(f"🔧 Debug: Final URL: {url}") + print(f"🔧 Debug: Method: {method}") + print(f"🔧 Debug: Params: {params}") + + return { + 'url': url, + 'method': method, + 'body': body, + 'headers': headers + } \ No newline at end of file diff --git a/ccxt/async_support/mt5_bak.py b/ccxt/async_support/mt5_bak.py new file mode 100644 index 0000000..1e44995 --- /dev/null +++ b/ccxt/async_support/mt5_bak.py @@ -0,0 +1,316 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.mt5 import ImplicitAPI +import asyncio +import hashlib +from ccxt.base.types import Any, Balances, DepositAddress, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, OrderBooks, Trade, TradingFees, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import DDoSProtection +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.errors import InvalidNonce +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class mt5(Exchange, ImplicitAPI): + def describe(self) -> Any: + return self.deep_extend(super(mt5, self).describe(), { + 'id': 'mt5', + 'name': 'MT5', + 'countries': ['US'], + 'version': 'v2025.02.05-05.23', + 'rateLimit': 1000, + 'hostname': '43.167.188.220:5000', + 'pro': True, + 'options': { + 'host': '18.163.85.196', + 'port': 443, + }, + 'has': { + 'CORS': True, + 'spot': True, + 'margin': True, + 'swap': True, + 'future': True, + 'option': True, + 'borrowCrossMargin': True, + 'cancelAllOrders': True, + 'cancelAllOrdersAfter': True, + 'cancelOrder': True, + 'cancelOrders': True, + 'cancelOrdersForSymbols': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createConvertTrade': True, + 'createMarketBuyOrderWithCost': True, + 'createMarketSellOrderWithCost': True, + 'createOrder': True, + 'createOrders': True, + 'createOrderWithTakeProfitAndStopLoss': True, + 'createPostOnlyOrder': True, + 'createReduceOnlyOrder': True, + 'createStopLimitOrder': True, + 'createStopLossOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'createTakeProfitOrder': True, + 'createTrailingAmountOrder': True, + 'createTriggerOrder': True, + 'editOrder': True, + 'editOrders': True, + 'fetchAllGreeks': True, + 'fetchBalance': True, + 'fetchBidsAsks': 'emulated', + 'fetchBorrowInterest': False, # temporarily disabled, doesn't work + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchCanceledAndClosedOrders': True, + 'fetchCanceledOrders': True, + 'fetchClosedOrder': True, + 'fetchClosedOrders': True, + 'fetchConvertCurrencies': True, + 'fetchConvertQuote': True, + 'fetchConvertTrade': True, + 'fetchConvertTradeHistory': True, + 'fetchCrossBorrowRate': True, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDeposit': False, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': True, + 'fetchDeposits': True, + 'fetchDepositWithdrawFee': 'emulated', + 'fetchDepositWithdrawFees': True, + 'fetchFundingHistory': True, + 'fetchFundingRate': 'emulated', # emulated in exchange + 'fetchFundingRateHistory': True, + 'fetchFundingRates': True, + 'fetchGreeks': True, + 'fetchIndexOHLCV': True, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchLedger': True, + 'fetchLeverage': True, + 'fetchLeverageTiers': True, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': True, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarketLeverageTiers': True, + 'fetchMarkets': True, + 'fetchMarkOHLCV': True, + 'fetchMyLiquidations': True, + 'fetchMySettlementHistory': True, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': True, + 'fetchOpenInterestHistory': True, + 'fetchOpenOrder': True, + 'fetchOpenOrders': True, + 'fetchOption': True, + 'fetchOptionChain': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': False, + 'fetchOrderTrades': True, + 'fetchPosition': True, + 'fetchPositionHistory': 'emulated', + 'fetchPositions': True, + 'fetchPositionsHistory': True, + 'fetchPremiumIndexOHLCV': True, + 'fetchSettlementHistory': True, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': True, + 'fetchTradingFees': True, + 'fetchTransactions': False, + 'fetchTransfers': True, + 'fetchUnderlyingAssets': False, + 'fetchVolatilityHistory': True, + 'fetchWithdrawals': True, + 'repayCrossMargin': True, + 'sandbox': True, + 'setLeverage': True, + 'setMarginMode': True, + 'setPositionMode': True, + 'transfer': True, + 'withdraw': True, + }, + 'timeframes': { + '1m': 1, + '5m': 5, + '15m': 15, + '30m': 30, + '1h': 60, + '4h': 240, + '1d': 1440, + '1w': 10080, + '1M': 43200, + }, + 'urls': { + 'logo': '', + 'api': { + 'public': 'http://{hostname}', + 'private': 'http://{hostname}', + }, + 'www': 'http://{hostname}', + 'doc': ['http://{hostname}/index.html'], + }, + 'api': { + 'public': { + 'get': { + 'Ping': 1 + }, + }, + 'private': { + 'get': { + 'Connect': 10, + 'CheckConnect': 1, + 'Disconnect': 1, + 'Symbols': 1, + 'ServerTimezone':1, + 'AccountSummary': 1, + 'AccountDetails': 1, + 'SymbolList': 1, + 'GetQuote': 1, + 'GetQuoteMany': 1, + 'MarketWatchMany': 1, + 'OpenedOrders': 1, + 'ClosedOrders': 1, + 'OpenedOrder': 1, + 'OrderHistory': 1, + 'PriceHistory': 1, + 'OrderSend': 1, + 'OrderModify': 1, + 'OrderClose': 1, + }, + }, + 'wsEndpoint': { + 'order': "OnOrderUpdate", + 'quote': "OnQuote", + 'orderbook': "OnOrderBook", + } + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + 'hostname': True, + }, + 'commonCurrencies': {}, + 'exceptions': { + 'exact': { + 'Invalid token': AuthenticationError, + 'Connection failed': ExchangeError, + 'Invalid symbol': ExchangeError, + 'Invalid order': InvalidOrder, + 'Order not found': OrderNotFound, + }, + }, + }) + + + async def get_token(self): + """获取或刷新 token - 异步版本""" + if self.token and self.token_checked: + try: + await self.check_connect() + return self.token + except Exception: + # Token 无效,重新连接 + pass + + # 重新连接获取 token + return await self.connect() + + async def connect(self): + """连接到 MT5 账户并获取 token - 异步版本""" + request = { + 'user': self.apiKey, + 'password': self.secret, + 'host': self.options['host'], + 'port': self.options['port'], + 'connectTimeoutSeconds': 30, + } + + print(f"🔧 Debug: Connect request params: {request}") + + response = await self.private_get_connect(request) + + print(f"🔧 Debug: Connect response: {response}") + + self.token = response + self.token_checked = True + return self.token + + async def check_connect(self): + """检查连接状态 - 异步版本""" + request = { + 'id': await self.get_token(), + } + return await self.private_get_check_connect(request) + + + async def fetch_markets(self, params={}): + """获取交易对列表""" + + if not self.token: + await self.get_token() + request = { + 'id': self.token, + } + response = await self.private_get_symbols(self.deep_extend(request, params)) + + markets = [] + if isinstance(response, dict): + for symbol, info in response.items(): + market = self.parse_market(info) + if market: + markets.append(market) + + return markets + + def parse_market(self, info): + """解析市场信息 - 根据 SymbolInfo 结构修正""" + symbol = info.get('currency', '').upper() + if not symbol: + return None + + return { + 'id': symbol, + 'symbol': symbol, + 'base': symbol[:3] if len(symbol) >= 6 else symbol, + 'quote': symbol[3:] if len(symbol) >= 6 else 'USD', + 'active': True, + 'precision': { + 'price': info.get('digits', 5), + 'amount': 2, + }, + 'limits': { + 'amount': { + 'min': 0.01, + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'info': info, + } diff --git a/ccxt/async_support/myokx.py b/ccxt/async_support/myokx.py new file mode 100644 index 0000000..237b1ca --- /dev/null +++ b/ccxt/async_support/myokx.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.okx import okx +from ccxt.abstract.myokx import ImplicitAPI +from ccxt.base.types import Any + + +class myokx(okx, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(myokx, self).describe(), { + 'id': 'myokx', + 'name': 'MyOKX(EEA)', + 'certified': False, + 'pro': True, + 'hostname': 'eea.okx.com', + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/152485636-38b19e4a-bece-4dec-979a-5982859ffc04.jpg', + 'api': { + 'rest': 'https://{hostname}', + }, + 'www': 'https://my.okx.com', + 'doc': 'https://my.okx.com/docs-v5/en/#overview', + 'fees': 'https://my.okx.com/pages/products/fees.html', + 'referral': { + 'url': 'https://www.my.okx.com/join/CCXT2023', + 'discount': 0.2, + }, + 'test': { + 'rest': 'https://{hostname}', + }, + }, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': None, + 'swap': False, + 'future': False, + 'option': False, + }, + 'features': { + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + }) diff --git a/ccxt/async_support/ndax.py b/ccxt/async_support/ndax.py new file mode 100644 index 0000000..295c0b1 --- /dev/null +++ b/ccxt/async_support/ndax.py @@ -0,0 +1,2511 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.ndax import ImplicitAPI +import hashlib +import json +from ccxt.base.types import Account, Any, Balances, Currencies, Currency, DepositAddress, IndexType, Int, LedgerEntry, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Ticker, Trade, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import OrderNotFound +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class ndax(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(ndax, self).describe(), { + 'id': 'ndax', + 'name': 'NDAX', + 'countries': ['CA'], # Canada + 'rateLimit': 1000, + 'pro': True, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createDepositAddress': True, + 'createOrder': True, + 'createReduceOnlyOrder': False, + 'createStopLimitOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'editOrder': True, + 'fetchAccounts': True, + 'fetchAllGreeks': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLedger': True, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrice': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchOrderTrades': True, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTickers': False, + 'fetchTime': False, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': False, + 'fetchUnderlyingAssets': False, + 'fetchVolatilityHistory': False, + 'fetchWithdrawals': True, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'sandbox': True, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'signIn': True, + 'transfer': False, + 'withdraw': True, + }, + 'timeframes': { + '1m': '60', + '5m': '300', + '15m': '900', + '30m': '1800', + '1h': '3600', + '2h': '7200', + '4h': '14400', + '6h': '21600', + '12h': '43200', + '1d': '86400', + '1w': '604800', + '1M': '2419200', + '4M': '9676800', + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/108623144-67a3ef00-744e-11eb-8140-75c6b851e945.jpg', + 'test': { + 'public': 'https://ndaxmarginstaging.cdnhop.net:8443/AP', + 'private': 'https://ndaxmarginstaging.cdnhop.net:8443/AP', + }, + 'api': { + 'public': 'https://api.ndax.io:8443/AP', + 'private': 'https://api.ndax.io:8443/AP', + }, + 'www': 'https://ndax.io', + 'doc': [ + 'https://apidoc.ndax.io/', + ], + 'fees': 'https://ndax.io/fees', + 'referral': 'https://one.ndax.io/bfQiSL', + }, + 'api': { + 'public': { + 'get': { + 'Activate2FA': 1, + 'Authenticate2FA': 1, + 'AuthenticateUser': 1, + 'GetL2Snapshot': 1, + 'GetLevel1': 1, + 'GetValidate2FARequiredEndpoints': 1, + 'LogOut': 1, + 'GetTickerHistory': 1, + 'GetProduct': 1, + 'GetProducts': 1, + 'GetInstrument': 1, + 'GetInstruments': 1, + 'Ping': 1, + 'trades': 1, # undocumented + 'GetLastTrades': 1, # undocumented + 'SubscribeLevel1': 1, + 'SubscribeLevel2': 1, + 'SubscribeTicker': 1, + 'SubscribeTrades': 1, + 'SubscribeBlockTrades': 1, + 'UnsubscribeBlockTrades': 1, + 'UnsubscribeLevel1': 1, + 'UnsubscribeLevel2': 1, + 'UnsubscribeTicker': 1, + 'UnsubscribeTrades': 1, + 'Authenticate': 1, # undocumented + }, + }, + 'private': { + 'get': { + 'GetUserAccountInfos': 1, + 'GetUserAccounts': 1, + 'GetUserAffiliateCount': 1, + 'GetUserAffiliateTag': 1, + 'GetUserConfig': 1, + 'GetAllUnredactedUserConfigsForUser': 1, + 'GetUnredactedUserConfigByKey': 1, + 'GetUserDevices': 1, + 'GetUserReportTickets': 1, + 'GetUserReportWriterResultRecords': 1, + 'GetAccountInfo': 1, + 'GetAccountPositions': 1, + 'GetAllAccountConfigs': 1, + 'GetTreasuryProductsForAccount': 1, + 'GetAccountTrades': 1, + 'GetAccountTransactions': 1, + 'GetOpenTradeReports': 1, + 'GetAllOpenTradeReports': 1, + 'GetTradesHistory': 1, + 'GetOpenOrders': 1, + 'GetOpenQuotes': 1, + 'GetOrderFee': 1, + 'GetOrderHistory': 1, + 'GetOrdersHistory': 1, + 'GetOrderStatus': 1, + 'GetOmsFeeTiers': 1, + 'GetAccountDepositTransactions': 1, + 'GetAccountWithdrawTransactions': 1, + 'GetAllDepositRequestInfoTemplates': 1, + 'GetDepositInfo': 1, + 'GetDepositRequestInfoTemplate': 1, + 'GetDeposits': 1, + 'GetDepositTicket': 1, + 'GetDepositTickets': 1, + 'GetOMSWithdrawFees': 1, + 'GetWithdrawFee': 1, + 'GetWithdraws': 1, + 'GetWithdrawTemplate': 1, + 'GetWithdrawTemplateTypes': 1, + 'GetWithdrawTicket': 1, + 'GetWithdrawTickets': 1, + }, + 'post': { + 'AddUserAffiliateTag': 1, + 'CancelUserReport': 1, + 'RegisterNewDevice': 1, + 'SubscribeAccountEvents': 1, + 'UpdateUserAffiliateTag': 1, + 'GenerateTradeActivityReport': 1, + 'GenerateTransactionActivityReport': 1, + 'GenerateTreasuryActivityReport': 1, + 'ScheduleTradeActivityReport': 1, + 'ScheduleTransactionActivityReport': 1, + 'ScheduleTreasuryActivityReport': 1, + 'CancelAllOrders': 1, + 'CancelOrder': 1, + 'CancelQuote': 1, + 'CancelReplaceOrder': 1, + 'CreateQuote': 1, + 'ModifyOrder': 1, + 'SendOrder': 1, + 'SubmitBlockTrade': 1, + 'UpdateQuote': 1, + 'CancelWithdraw': 1, + 'CreateDepositTicket': 1, + 'CreateWithdrawTicket': 1, + 'SubmitDepositTicketComment': 1, + 'SubmitWithdrawTicketComment': 1, + 'GetOrderHistoryByOrderId': 1, + }, + }, + }, + 'features': { + 'spot': { + 'sandbox': True, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerDirection': False, + 'triggerPriceType': { + 'last': True, + 'mark': False, + 'index': False, + # bid & ask + }, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': None, + # todo + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': False, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': False, + 'iceberg': True, # todo + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, # todo + 'daysBack': 100000, # todo + 'untilDays': 100000, # todo + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': None, + 'daysBack': None, + 'untilDays': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchClosedOrders': None, + 'fetchOHLCV': { + 'limit': None, + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'fees': { + 'trading': { + 'tierBased': False, + 'percentage': True, + 'maker': self.parse_number('0.002'), + 'taker': self.parse_number('0.0025'), + }, + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + 'uid': True, + # these credentials are required for signIn() and withdraw() + 'login': True, + 'password': True, + # 'twofa': True, + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'exact': { + 'Not_Enough_Funds': InsufficientFunds, # {"status":"Rejected","errormsg":"Not_Enough_Funds","errorcode":101} + 'Server Error': ExchangeError, # {"result":false,"errormsg":"Server Error","errorcode":102,"detail":null} + 'Resource Not Found': OrderNotFound, # {"result":false,"errormsg":"Resource Not Found","errorcode":104,"detail":null} + }, + 'broad': { + 'Invalid InstrumentId': BadSymbol, # {"result":false,"errormsg":"Invalid InstrumentId: 10000","errorcode":100,"detail":null} + 'This endpoint requires 2FACode along with the payload': AuthenticationError, + }, + }, + 'options': { + 'omsId': 1, + 'orderTypes': { + 'Market': 1, + 'Limit': 2, + 'StopMarket': 3, + 'StopLimit': 4, + 'TrailingStopMarket': 5, + 'TrailingStopLimit': 6, + 'BlockTrade': 7, + '1': 1, + '2': 2, + '3': 3, + '4': 4, + '5': 5, + '6': 6, + '7': 7, + }, + }, + }) + + async def sign_in(self, params={}): + """ + sign in, must be called prior to using other authenticated methods + + https://apidoc.ndax.io/#authenticate2fa + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns: response from exchange + """ + self.check_required_credentials() + if self.login is None or self.password is None: + raise AuthenticationError(self.id + ' signIn() requires exchange.login, exchange.password') + request: dict = { + 'grant_type': 'client_credentials', # the only supported value + } + response = await self.publicGetAuthenticate(self.extend(request, params)) + # + # { + # "Authenticated":true, + # "Requires2FA":true, + # "AuthType":"Google", + # "AddtlInfo":"", + # "Pending2FaToken": "6f5c4e66-f3ee-493e-9227-31cc0583b55f" + # } + # + sessionToken = self.safe_string(response, 'SessionToken') + if sessionToken is not None: + self.options['sessionToken'] = sessionToken + return response + pending2faToken = self.safe_string(response, 'Pending2FaToken') + if pending2faToken is not None: + if self.twofa is None: + raise AuthenticationError(self.id + ' signIn() requires exchange.twofa credentials') + self.options['pending2faToken'] = pending2faToken + request = { + 'Code': self.totp(self.twofa), + } + responseInner = await self.publicGetAuthenticate2FA(self.extend(request, params)) + # + # { + # "Authenticated": True, + # "UserId":57764, + # "SessionToken":"4a2a5857-c4e5-4fac-b09e-2c4c30b591a0" + # } + # + sessionToken = self.safe_string(responseInner, 'SessionToken') + self.options['sessionToken'] = sessionToken + return responseInner + return response + + async def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://apidoc.ndax.io/#getproduct + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + omsId = self.safe_integer(self.options, 'omsId', 1) + request: dict = { + 'omsId': omsId, + } + response = await self.publicGetGetProducts(self.extend(request, params)) + # + # [ + # { + # "OMSId": "1", + # "ProductId": "1", + # "Product": "BTC", + # "ProductFullName": "Bitcoin", + # "MasterDataUniqueProductSymbol": "", + # "ProductType": "CryptoCurrency", + # "DecimalPlaces": "8", + # "TickSize": "0.0000000100000000000000000000", + # "DepositEnabled": True, + # "WithdrawEnabled": True, + # "NoFees": False, + # "IsDisabled": False, + # "MarginEnabled": False + # }, + # ... + # + result: dict = {} + for i in range(0, len(response)): + currency = response[i] + id = self.safe_string(currency, 'ProductId') + code = self.safe_currency_code(self.safe_string(currency, 'Product')) + ProductType = self.safe_string(currency, 'ProductType') + type = 'fiat' if (ProductType == 'NationalCurrency') else 'crypto' + if ProductType == 'Unknown': + # such currency is just a blanket entry + type = 'other' + result[code] = self.safe_currency_structure({ + 'id': id, + 'name': self.safe_string(currency, 'ProductFullName'), + 'code': code, + 'type': type, + 'precision': self.safe_number(currency, 'TickSize'), + 'info': currency, + 'active': not self.safe_bool(currency, 'IsDisabled'), + 'deposit': self.safe_bool(currency, 'DepositEnabled'), + 'withdraw': self.safe_bool(currency, 'WithdrawEnabled'), + 'fee': None, + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + }, + 'networks': {}, + 'margin': self.safe_bool(currency, 'MarginEnabled'), + }) + return result + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for ndax + + https://apidoc.ndax.io/#getinstruments + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + omsId = self.safe_integer(self.options, 'omsId', 1) + request: dict = { + 'omsId': omsId, + } + response = await self.publicGetGetInstruments(self.extend(request, params)) + # + # [ + # { + # "OMSId":1, + # "InstrumentId":3, + # "Symbol":"LTCBTC", + # "Product1":3, + # "Product1Symbol":"LTC", + # "Product2":1, + # "Product2Symbol":"BTC", + # "InstrumentType":"Standard", + # "VenueInstrumentId":3, + # "VenueId":1, + # "SortIndex":0, + # "SessionStatus":"Running", + # "PreviousSessionStatus":"Stopped", + # "SessionStatusDateTime":"2020-11-25T19:42:15.245Z", + # "SelfTradePrevention":true, + # "QuantityIncrement":0.0000000100000000000000000000, + # "PriceIncrement":0.0000000100000000000000000000, + # "MinimumQuantity":0.0100000000000000000000000000, + # "MinimumPrice":0.0000010000000000000000000000, + # "VenueSymbol":"LTCBTC", + # "IsDisable":false, + # "MasterDataId":0, + # "PriceCollarThreshold":0.0000000000000000000000000000, + # "PriceCollarPercent":0.0000000000000000000000000000, + # "PriceCollarEnabled":false, + # "PriceFloorLimit":0.0000000000000000000000000000, + # "PriceFloorLimitEnabled":false, + # "PriceCeilingLimit":0.0000000000000000000000000000, + # "PriceCeilingLimitEnabled":false, + # "CreateWithMarketRunning":true, + # "AllowOnlyMarketMakerCounterParty":false, + # "PriceCollarIndexDifference":0.0000000000000000000000000000, + # "PriceCollarConvertToOtcEnabled":false, + # "PriceCollarConvertToOtcClientUserId":0, + # "PriceCollarConvertToOtcAccountId":0, + # "PriceCollarConvertToOtcThreshold":0.0000000000000000000000000000, + # "OtcConvertSizeThreshold":0.0000000000000000000000000000, + # "OtcConvertSizeEnabled":false, + # "OtcTradesPublic":true, + # "PriceTier":0 + # }, + # ] + # + return self.parse_markets(response) + + def parse_market(self, market: dict) -> Market: + id = self.safe_string(market, 'InstrumentId') + # lowercaseId = self.safe_string_lower(market, 'symbol') + baseId = self.safe_string(market, 'Product1') + quoteId = self.safe_string(market, 'Product2') + base = self.safe_currency_code(self.safe_string(market, 'Product1Symbol')) + quote = self.safe_currency_code(self.safe_string(market, 'Product2Symbol')) + sessionStatus = self.safe_string(market, 'SessionStatus') + isDisable = self.safe_value(market, 'IsDisable') + sessionRunning = (sessionStatus == 'Running') + return { + 'id': id, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': (sessionRunning and not isDisable), + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.safe_number(market, 'QuantityIncrement'), + 'price': self.safe_number(market, 'PriceIncrement'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(market, 'MinimumQuantity'), + 'max': None, + }, + 'price': { + 'min': self.safe_number(market, 'MinimumPrice'), + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + 'info': market, + } + + def parse_order_book(self, orderbook, symbol, timestamp=None, bidsKey='bids', asksKey='asks', priceKey: IndexType = 6, amountKey: IndexType = 8, countOrIdKey: IndexType = 2): + nonce = None + result: dict = { + 'symbol': symbol, + 'bids': [], + 'asks': [], + 'timestamp': None, + 'datetime': None, + 'nonce': None, + } + for i in range(0, len(orderbook)): + level = orderbook[i] + if timestamp is None: + timestamp = self.safe_integer(level, 2) + else: + newTimestamp = self.safe_integer(level, 2) + timestamp = max(timestamp, newTimestamp) + if nonce is None: + nonce = self.safe_integer(level, 0) + else: + newNonce = self.safe_integer(level, 0) + nonce = max(nonce, newNonce) + bidask = self.parse_bid_ask(level, priceKey, amountKey) + levelSide = self.safe_integer(level, 9) + side = asksKey if levelSide else bidsKey + resultSide = result[side] + resultSide.append(bidask) + result['bids'] = self.sort_by(result['bids'], 0, True) + result['asks'] = self.sort_by(result['asks'], 0) + result['timestamp'] = timestamp + result['datetime'] = self.iso8601(timestamp) + result['nonce'] = nonce + return result + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://apidoc.ndax.io/#getl2snapshot + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + omsId = self.safe_integer(self.options, 'omsId', 1) + await self.load_markets() + market = self.market(symbol) + limit = 100 if (limit is None) else limit # default 100 + request: dict = { + 'omsId': omsId, + 'InstrumentId': market['id'], + 'Depth': limit, # default 100 + } + response = await self.publicGetGetL2Snapshot(self.extend(request, params)) + # + # [ + # [ + # 0, # 0 MDUpdateId + # 1, # 1 Number of Unique Accounts + # 123, # 2 ActionDateTime in Posix format X 1000 + # 0, # 3 ActionType 0(New), 1(Update), 2(Delete) + # 0.0, # 4 LastTradePrice + # 0, # 5 Number of Orders + # 0.0, # 6 Price + # 0, # 7 ProductPairCode + # 0.0, # 8 Quantity + # 0, # 9 Side + # ], + # [97244115,1,1607456142963,0,19069.32,1,19069.31,8,0.140095,0], + # [97244115,0,1607456142963,0,19069.32,1,19068.64,8,0.0055,0], + # [97244115,0,1607456142963,0,19069.32,1,19068.26,8,0.021291,0], + # [97244115,1,1607456142964,0,19069.32,1,19069.32,8,0.099636,1], + # [97244115,0,1607456142964,0,19069.32,1,19069.98,8,0.1,1], + # [97244115,0,1607456142964,0,19069.32,1,19069.99,8,0.141604,1], + # ] + # + return self.parse_order_book(response, symbol) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # fetchTicker + # + # { + # "OMSId":1, + # "InstrumentId":8, + # "BestBid":19069.31, + # "BestOffer":19069.32, + # "LastTradedPx":19069.32, + # "LastTradedQty":0.0001, + # "LastTradeTime":1607040406424, + # "SessionOpen":19069.32, + # "SessionHigh":19069.32, + # "SessionLow":19069.32, + # "SessionClose":19069.32, + # "Volume":0.0001, + # "CurrentDayVolume":0.0001, + # "CurrentDayNotional":1.906932, + # "CurrentDayNumTrades":1, + # "CurrentDayPxChange":0.00, + # "Rolling24HrVolume":0.000000000000000000000000000, + # "Rolling24HrNotional":0.00000000000000000000000, + # "Rolling24NumTrades":0, + # "Rolling24HrPxChange":0, + # "TimeStamp":"1607040406425", + # "BidQty":0, + # "AskQty":0, + # "BidOrderCt":0, + # "AskOrderCt":0, + # "Rolling24HrPxChangePercent":0, + # } + # + timestamp = self.safe_integer(ticker, 'TimeStamp') + marketId = self.safe_string(ticker, 'InstrumentId') + market = self.safe_market(marketId, market) + symbol = self.safe_symbol(marketId, market) + last = self.safe_string(ticker, 'LastTradedPx') + percentage = self.safe_string(ticker, 'Rolling24HrPxChangePercent') + change = self.safe_string(ticker, 'Rolling24HrPxChange') + open = self.safe_string(ticker, 'SessionOpen') + baseVolume = self.safe_string(ticker, 'Rolling24HrVolume') + quoteVolume = self.safe_string(ticker, 'Rolling24HrNotional') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'SessionHigh'), + 'low': self.safe_string(ticker, 'SessionLow'), + 'bid': self.safe_string(ticker, 'BestBid'), + 'bidVolume': None, # self.safe_number(ticker, 'BidQty'), always shows 0 + 'ask': self.safe_string(ticker, 'BestOffer'), + 'askVolume': None, # self.safe_number(ticker, 'AskQty'), always shows 0 + 'vwap': None, + 'open': open, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': change, + 'percentage': percentage, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'info': ticker, + }, market) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://apidoc.ndax.io/#getlevel1 + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + omsId = self.safe_integer(self.options, 'omsId', 1) + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'omsId': omsId, + 'InstrumentId': market['id'], + } + response = await self.publicGetGetLevel1(self.extend(request, params)) + # + # { + # "OMSId":1, + # "InstrumentId":8, + # "BestBid":19069.31, + # "BestOffer":19069.32, + # "LastTradedPx":19069.32, + # "LastTradedQty":0.0001, + # "LastTradeTime":1607040406424, + # "SessionOpen":19069.32, + # "SessionHigh":19069.32, + # "SessionLow":19069.32, + # "SessionClose":19069.32, + # "Volume":0.0001, + # "CurrentDayVolume":0.0001, + # "CurrentDayNotional":1.906932, + # "CurrentDayNumTrades":1, + # "CurrentDayPxChange":0.00, + # "Rolling24HrVolume":0.000000000000000000000000000, + # "Rolling24HrNotional":0.00000000000000000000000, + # "Rolling24NumTrades":0, + # "Rolling24HrPxChange":0, + # "TimeStamp":"1607040406425", + # "BidQty":0, + # "AskQty":0, + # "BidOrderCt":0, + # "AskOrderCt":0, + # "Rolling24HrPxChangePercent":0, + # } + # + return self.parse_ticker(response, market) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # [ + # 1501603632000, # 0 DateTime + # 2700.33, # 1 High + # 2687.01, # 2 Low + # 2687.01, # 3 Open + # 2687.01, # 4 Close + # 24.86100992, # 5 Volume + # 0, # 6 Inside Bid Price + # 2870.95, # 7 Inside Ask Price + # 1 # 8 InstrumentId + # ] + # + return [ + self.safe_integer(ohlcv, 0), + self.safe_number(ohlcv, 3), + self.safe_number(ohlcv, 1), + self.safe_number(ohlcv, 2), + self.safe_number(ohlcv, 4), + self.safe_number(ohlcv, 5), + ] + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://apidoc.ndax.io/#gettickerhistory + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + omsId = self.safe_integer(self.options, 'omsId', 1) + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'omsId': omsId, + 'InstrumentId': market['id'], + 'Interval': self.safe_string(self.timeframes, timeframe, timeframe), + } + duration = self.parse_timeframe(timeframe) + now = self.milliseconds() + if since is None: + if limit is not None: + request['FromDate'] = self.ymdhms(now - duration * limit * 1000) + request['ToDate'] = self.ymdhms(now) + else: + request['FromDate'] = self.ymdhms(since) + if limit is None: + request['ToDate'] = self.ymdhms(now) + else: + request['ToDate'] = self.ymdhms(self.sum(since, duration * limit * 1000)) + response = await self.publicGetGetTickerHistory(self.extend(request, params)) + # + # [ + # [1607299260000,19069.32,19069.32,19069.32,19069.32,0,19069.31,19069.32,8,1607299200000], + # [1607299320000,19069.32,19069.32,19069.32,19069.32,0,19069.31,19069.32,8,1607299260000], + # [1607299380000,19069.32,19069.32,19069.32,19069.32,0,19069.31,19069.32,8,1607299320000], + # ] + # + return self.parse_ohlcvs(response, market, timeframe, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(public) + # + # [ + # 6913253, # 0 TradeId + # 8, # 1 ProductPairCode + # 0.03340802, # 2 Quantity + # 19116.08, # 3 Price + # 2543425077, # 4 Order1 + # 2543425482, # 5 Order2 + # 1606935922416, # 6 Tradetime + # 0, # 7 Direction + # 1, # 8 TakerSide + # 0, # 9 BlockTrade + # 0, # 10 Either Order1ClientId or Order2ClientId + # ] + # + # fetchMyTrades(private) + # + # { + # "OMSId":1, + # "ExecutionId":16916567, + # "TradeId":14476351, + # "OrderId":2543565231, + # "AccountId":449, + # "AccountName":"igor@ccxt.trade", + # "SubAccountId":0, + # "ClientOrderId":0, + # "InstrumentId":8, + # "Side":"Sell", + # "OrderType":"Market", + # "Quantity":0.1230000000000000000000000000, + # "RemainingQuantity":0.0000000000000000000000000000, + # "Price":19069.310000000000000000000000, + # "Value":2345.5251300000000000000000000, + # "CounterParty":"7", + # "OrderTradeRevision":1, + # "Direction":"NoChange", + # "IsBlockTrade":false, + # "Fee":1.1727625650000000000000000000, + # "FeeProductId":8, + # "OrderOriginator":446, + # "UserName":"igor@ccxt.trade", + # "TradeTimeMS":1607565031569, + # "MakerTaker":"Taker", + # "AdapterTradeId":0, + # "InsideBid":19069.310000000000000000000000, + # "InsideBidSize":0.2400950000000000000000000000, + # "InsideAsk":19069.320000000000000000000000, + # "InsideAskSize":0.0997360000000000000000000000, + # "IsQuote":false, + # "CounterPartyClientUserId":1, + # "NotionalProductId":2, + # "NotionalRate":1.0000000000000000000000000000, + # "NotionalValue":2345.5251300000000000000000000, + # "NotionalHoldAmount":0, + # "TradeTime":637431618315686826 + # } + # + # fetchOrderTrades + # + # { + # "Side":"Sell", + # "OrderId":2543565235, + # "Price":18600.000000000000000000000000, + # "Quantity":0.0000000000000000000000000000, + # "DisplayQuantity":0.0000000000000000000000000000, + # "Instrument":8, + # "Account":449, + # "AccountName":"igor@ccxt.trade", + # "OrderType":"Limit", + # "ClientOrderId":0, + # "OrderState":"FullyExecuted", + # "ReceiveTime":1607585844956, + # "ReceiveTimeTicks":637431826449564182, + # "LastUpdatedTime":1607585844959, + # "LastUpdatedTimeTicks":637431826449593893, + # "OrigQuantity":0.1230000000000000000000000000, + # "QuantityExecuted":0.1230000000000000000000000000, + # "GrossValueExecuted":2345.3947500000000000000000000, + # "ExecutableValue":0.0000000000000000000000000000, + # "AvgPrice":19068.250000000000000000000000, + # "CounterPartyId":0, + # "ChangeReason":"Trade", + # "OrigOrderId":2543565235, + # "OrigClOrdId":0, + # "EnteredBy":446, + # "UserName":"igor@ccxt.trade", + # "IsQuote":false, + # "InsideAsk":19069.320000000000000000000000, + # "InsideAskSize":0.0997360000000000000000000000, + # "InsideBid":19068.250000000000000000000000, + # "InsideBidSize":1.3300010000000000000000000000, + # "LastTradePrice":19068.250000000000000000000000, + # "RejectReason":"", + # "IsLockedIn":false, + # "CancelReason":"", + # "OrderFlag":"0", + # "UseMargin":false, + # "StopPrice":0.0000000000000000000000000000, + # "PegPriceType":"Unknown", + # "PegOffset":0.0000000000000000000000000000, + # "PegLimitOffset":0.0000000000000000000000000000, + # "IpAddress":"x.x.x.x", + # "ClientOrderIdUuid":null, + # "OMSId":1 + # } + # + priceString = None + amountString = None + costString = None + timestamp = None + id = None + marketId = None + side = None + orderId = None + takerOrMaker = None + fee = None + type = None + if isinstance(trade, list): + priceString = self.safe_string(trade, 3) + amountString = self.safe_string(trade, 2) + timestamp = self.safe_integer(trade, 6) + id = self.safe_string(trade, 0) + marketId = self.safe_string(trade, 1) + takerSide = self.safe_value(trade, 8) + side = 'sell' if takerSide else 'buy' + orderId = self.safe_string(trade, 4) + else: + timestamp = self.safe_integer_2(trade, 'TradeTimeMS', 'ReceiveTime') + id = self.safe_string(trade, 'TradeId') + orderId = self.safe_string_2(trade, 'OrderId', 'OrigOrderId') + marketId = self.safe_string_2(trade, 'InstrumentId', 'Instrument') + priceString = self.safe_string(trade, 'Price') + amountString = self.safe_string(trade, 'Quantity') + costString = self.safe_string_2(trade, 'Value', 'GrossValueExecuted') + takerOrMaker = self.safe_string_lower(trade, 'MakerTaker') + side = self.safe_string_lower(trade, 'Side') + type = self.safe_string_lower(trade, 'OrderType') + feeCostString = self.safe_string(trade, 'Fee') + if feeCostString is not None: + feeCurrencyId = self.safe_string(trade, 'FeeProductId') + feeCurrencyCode = self.safe_currency_code(feeCurrencyId) + fee = { + 'cost': feeCostString, + 'currency': feeCurrencyCode, + } + symbol = self.safe_symbol(marketId, market) + return self.safe_trade({ + 'info': trade, + 'id': id, + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'order': orderId, + 'type': type, + 'side': side, + 'takerOrMaker': takerOrMaker, + 'price': priceString, + 'amount': amountString, + 'cost': costString, + 'fee': fee, + }, market) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + omsId = self.safe_integer(self.options, 'omsId', 1) + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'omsId': omsId, + 'InstrumentId': market['id'], + } + if limit is not None: + request['Count'] = limit + response = await self.publicGetGetLastTrades(self.extend(request, params)) + # + # [ + # [6913253,8,0.03340802,19116.08,2543425077,2543425482,1606935922416,0,1,0,0], + # [6913254,8,0.01391671,19117.42,2543427510,2543427811,1606935927998,1,1,0,0], + # [6913255,8,0.000006,19107.81,2543430495,2543430793,1606935933881,2,0,0,0], + # ] + # + return self.parse_trades(response, market, since, limit) + + async def fetch_accounts(self, params={}) -> List[Account]: + """ + fetch all the accounts associated with a profile + + https://apidoc.ndax.io/#getuseraccounts + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `account structures ` indexed by the account type + """ + if not self.login: + raise AuthenticationError(self.id + ' fetchAccounts() requires exchange.login email credential') + omsId = self.safe_integer(self.options, 'omsId', 1) + self.check_required_credentials() + request: dict = { + 'omsId': omsId, + 'UserId': self.uid, + 'UserName': self.login, + } + response = await self.privateGetGetUserAccounts(self.extend(request, params)) + # + # [449] # comma-separated list of account ids + # + result = [] + for i in range(0, len(response)): + accountId = self.safe_string(response, i) + result.append({ + 'id': accountId, + 'type': None, + 'currency': None, + 'info': accountId, + }) + return result + + def parse_balance(self, response) -> Balances: + result: dict = { + 'info': response, + 'timestamp': None, + 'datetime': None, + } + for i in range(0, len(response)): + balance = response[i] + currencyId = self.safe_string(balance, 'ProductId') + if currencyId in self.currencies_by_id: + code = self.safe_currency_code(currencyId) + account = self.account() + account['total'] = self.safe_string(balance, 'Amount') + account['used'] = self.safe_string(balance, 'Hold') + result[code] = account + return self.safe_balance(result) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://apidoc.ndax.io/#getaccountpositions + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + omsId = self.safe_integer(self.options, 'omsId', 1) + await self.load_markets() + await self.load_accounts() + defaultAccountId = self.safe_integer_2(self.options, 'accountId', 'AccountId') + accountId = self.safe_integer_2(params, 'accountId', 'AccountId', defaultAccountId) + if accountId is None: + accountId = int(self.accounts[0]['id']) + params = self.omit(params, ['accountId', 'AccountId']) + request: dict = { + 'omsId': omsId, + 'AccountId': accountId, + } + response = await self.privateGetGetAccountPositions(self.extend(request, params)) + # + # [ + # { + # "OMSId":1, + # "AccountId":449, + # "ProductSymbol":"BTC", + # "ProductId":1, + # "Amount":10.000000000000000000000000000, + # "Hold":0, + # "PendingDeposits":0.0000000000000000000000000000, + # "PendingWithdraws":0.0000000000000000000000000000, + # "TotalDayDeposits":10.000000000000000000000000000, + # "TotalMonthDeposits":10.000000000000000000000000000, + # "TotalYearDeposits":10.000000000000000000000000000, + # "TotalDayDepositNotional":10.000000000000000000000000000, + # "TotalMonthDepositNotional":10.000000000000000000000000000, + # "TotalYearDepositNotional":10.000000000000000000000000000, + # "TotalDayWithdraws":0, + # "TotalMonthWithdraws":0, + # "TotalYearWithdraws":0, + # "TotalDayWithdrawNotional":0, + # "TotalMonthWithdrawNotional":0, + # "TotalYearWithdrawNotional":0, + # "NotionalProductId":8, + # "NotionalProductSymbol":"USDT", + # "NotionalValue":10.000000000000000000000000000, + # "NotionalHoldAmount":0, + # "NotionalRate":1 + # }, + # ] + # + return self.parse_balance(response) + + def parse_ledger_entry_type(self, type): + types: dict = { + 'Trade': 'trade', + 'Deposit': 'transaction', + 'Withdraw': 'transaction', + 'Transfer': 'transfer', + 'OrderHold': 'trade', + 'WithdrawHold': 'transaction', + 'DepositHold': 'transaction', + 'MarginHold': 'trade', + 'ManualHold': 'trade', + 'ManualEntry': 'trade', + 'MarginAcquisition': 'trade', + 'MarginRelinquish': 'trade', + 'MarginQuoteHold': 'trade', + } + return self.safe_string(types, type, type) + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + # + # { + # "TransactionId": 2663709493, + # "ReferenceId": 68, + # "OMSId": 1, + # "AccountId": 449, + # "CR": 10.000000000000000000000000000, + # "DR": 0.0000000000000000000000000000, + # "Counterparty": 3, + # "TransactionType": "Other", + # "ReferenceType": "Deposit", + # "ProductId": 1, + # "Balance": 10.000000000000000000000000000, + # "TimeStamp": 1607532331591 + # } + # + currencyId = self.safe_string(item, 'ProductId') + currency = self.safe_currency(currencyId, currency) + credit = self.safe_string(item, 'CR') + debit = self.safe_string(item, 'DR') + amount = None + direction = None + if Precise.string_lt(credit, '0'): + amount = credit + direction = 'in' + elif Precise.string_lt(debit, '0'): + amount = debit + direction = 'out' + before = None + after = self.safe_string(item, 'Balance') + if direction == 'out': + before = Precise.string_add(after, amount) + elif direction == 'in': + before = Precise.string_max('0', Precise.string_sub(after, amount)) + timestamp = self.safe_integer(item, 'TimeStamp') + return self.safe_ledger_entry({ + 'info': item, + 'id': self.safe_string(item, 'TransactionId'), + 'direction': direction, + 'account': self.safe_string(item, 'AccountId'), + 'referenceId': self.safe_string(item, 'ReferenceId'), + 'referenceAccount': self.safe_string(item, 'Counterparty'), + 'type': self.parse_ledger_entry_type(self.safe_string(item, 'ReferenceType')), + 'currency': self.safe_currency_code(currencyId, currency), + 'amount': self.parse_number(amount), + 'before': self.parse_number(before), + 'after': self.parse_number(after), + 'status': 'ok', + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'fee': None, + }, currency) + + async def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + + https://apidoc.ndax.io/#getaccounttransactions + + :param str [code]: unified currency code, default is None + :param int [since]: timestamp in ms of the earliest ledger entry, default is None + :param int [limit]: max number of ledger entries to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ledger structure ` + """ + omsId = self.safe_integer(self.options, 'omsId', 1) + await self.load_markets() + await self.load_accounts() + defaultAccountId = self.safe_integer_2(self.options, 'accountId', 'AccountId', int(self.accounts[0]['id'])) + accountId = self.safe_integer_2(params, 'accountId', 'AccountId', defaultAccountId) + params = self.omit(params, ['accountId', 'AccountId']) + request: dict = { + 'omsId': omsId, + 'AccountId': accountId, + } + if limit is not None: + request['Depth'] = limit + response = await self.privateGetGetAccountTransactions(self.extend(request, params)) + # + # [ + # { + # "TransactionId":2663709493, + # "ReferenceId":68, + # "OMSId":1, + # "AccountId":449, + # "CR":10.000000000000000000000000000, + # "DR":0.0000000000000000000000000000, + # "Counterparty":3, + # "TransactionType":"Other", + # "ReferenceType":"Deposit", + # "ProductId":1, + # "Balance":10.000000000000000000000000000, + # "TimeStamp":1607532331591 + # }, + # ] + # + currency = None + if code is not None: + currency = self.currency(code) + return self.parse_ledger(response, currency, since, limit) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'Accepted': 'open', + 'Rejected': 'rejected', + 'Working': 'open', + 'Canceled': 'canceled', + 'Expired': 'expired', + 'FullyExecuted': 'closed', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder + # + # { + # "status":"Accepted", + # "errormsg":"", + # "OrderId": 2543565231 + # } + # + # editOrder + # + # { + # "ReplacementOrderId": 1234, + # "ReplacementClOrdId": 1561, + # "OrigOrderId": 5678, + # "OrigClOrdId": 91011, + # } + # + # fetchOpenOrders, fetchClosedOrders + # + # { + # "Side":"Buy", + # "OrderId":2543565233, + # "Price":19010, + # "Quantity":0.345, + # "DisplayQuantity":0.345, + # "Instrument":8, + # "Account":449, + # "AccountName":"igor@ccxt.trade", + # "OrderType":"Limit", + # "ClientOrderId":0, + # "OrderState":"Working", + # "ReceiveTime":1607579326003, + # "ReceiveTimeTicks":637431761260028981, + # "LastUpdatedTime":1607579326005, + # "LastUpdatedTimeTicks":637431761260054714, + # "OrigQuantity":0.345, + # "QuantityExecuted":0, + # "GrossValueExecuted":0, + # "ExecutableValue":0, + # "AvgPrice":0, + # "CounterPartyId":0, + # "ChangeReason":"NewInputAccepted", + # "OrigOrderId":2543565233, + # "OrigClOrdId":0, + # "EnteredBy":446, + # "UserName":"igor@ccxt.trade", + # "IsQuote":false, + # "InsideAsk":19069.32, + # "InsideAskSize":0.099736, + # "InsideBid":19068.25, + # "InsideBidSize":1.330001, + # "LastTradePrice":19068.25, + # "RejectReason":"", + # "IsLockedIn":false, + # "CancelReason":"", + # "OrderFlag":"AddedToBook", + # "UseMargin":false, + # "StopPrice":0, + # "PegPriceType":"Unknown", + # "PegOffset":0, + # "PegLimitOffset":0, + # "IpAddress":null, + # "ClientOrderIdUuid":null, + # "OMSId":1 + # } + # + timestamp = self.safe_integer(order, 'ReceiveTime') + marketId = self.safe_string(order, 'Instrument') + return self.safe_order({ + 'id': self.safe_string_2(order, 'ReplacementOrderId', 'OrderId'), + 'clientOrderId': self.safe_string_2(order, 'ReplacementClOrdId', 'ClientOrderId'), + 'info': order, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': self.safe_integer(order, 'LastUpdatedTime'), + 'status': self.parse_order_status(self.safe_string(order, 'OrderState')), + 'symbol': self.safe_symbol(marketId, market), + 'type': self.safe_string_lower(order, 'OrderType'), + 'timeInForce': None, + 'postOnly': None, + 'side': self.safe_string_lower(order, 'Side'), + 'price': self.safe_string(order, 'Price'), + 'triggerPrice': self.parse_number(self.omit_zero(self.safe_string(order, 'StopPrice'))), + 'cost': self.safe_string(order, 'GrossValueExecuted'), + 'amount': self.safe_string(order, 'OrigQuantity'), + 'filled': self.safe_string(order, 'QuantityExecuted'), + 'average': self.safe_string(order, 'AvgPrice'), + 'remaining': None, + 'fee': None, + 'trades': None, + }, market) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://apidoc.ndax.io/#sendorder + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: the price at which a trigger order would be triggered + :param str [params.clientOrderId]: a unique id for the order + :returns dict: an `order structure ` + """ + omsId = self.safe_integer(self.options, 'omsId', 1) + await self.load_markets() + await self.load_accounts() + defaultAccountId = self.safe_integer_2(self.options, 'accountId', 'AccountId', int(self.accounts[0]['id'])) + accountId = self.safe_integer_2(params, 'accountId', 'AccountId', defaultAccountId) + clientOrderId = self.safe_integer_2(params, 'ClientOrderId', 'clientOrderId') + orderType = self.safe_integer(self.options['orderTypes'], self.capitalize(type)) + triggerPrice = self.safe_string(params, 'triggerPrice') + if triggerPrice is not None: + if type == 'market': + orderType = 3 + elif type == 'limit': + orderType = 4 + params = self.omit(params, ['accountId', 'AccountId', 'clientOrderId', 'ClientOrderId', 'triggerPrice']) + market = self.market(symbol) + orderSide = 0 if (side == 'buy') else 1 + request: dict = { + 'InstrumentId': int(market['id']), + 'omsId': omsId, + 'AccountId': accountId, + 'TimeInForce': 1, # 0 Unknown, 1 GTC by default, 2 OPG execute to opening price, 3 IOC immediate or canceled, 4 FOK fill-or-kill, 5 GTX good 'til executed, 6 GTD good 'til date + # 'ClientOrderId': clientOrderId, # defaults to 0 + # If self order is order A, OrderIdOCO refers to the order ID of an order B(which is not the order being created by self call). + # If order B executes, then order A created by self call is canceled. + # You can also set up order B to watch order A in the same way, but that may require an update to order B to make it watch self one, which could have implications for priority in the order book. + # See CancelReplaceOrder and ModifyOrder. + # 'OrderIdOCO': 0, # The order ID if One Cancels the Other. + # 'UseDisplayQuantity': False, # If you enter a Limit order with a reserve, you must set UseDisplayQuantity to True + 'Side': orderSide, # 0 Buy, 1 Sell, 2 Short, 3 unknown an error condition + 'Quantity': float(self.amount_to_precision(symbol, amount)), + 'OrderType': orderType, # 0 Unknown, 1 Market, 2 Limit, 3 StopMarket, 4 StopLimit, 5 TrailingStopMarket, 6 TrailingStopLimit, 7 BlockTrade + # 'PegPriceType': 3, # 1 Last, 2 Bid, 3 Ask, 4 Midpoint + # 'LimitPrice': float(self.price_to_precision(symbol, price)), + } + # If OrderType=1(Market), Side=0(Buy), and LimitPrice is supplied, the Market order will execute up to the value specified + if price is not None: + request['LimitPrice'] = float(self.price_to_precision(symbol, price)) + if clientOrderId is not None: + request['ClientOrderId'] = clientOrderId + if triggerPrice is not None: + request['StopPrice'] = triggerPrice + response = await self.privatePostSendOrder(self.extend(request, params)) + # + # { + # "status":"Accepted", + # "errormsg":"", + # "OrderId": 2543565231 + # } + # + return self.parse_order(response, market) + + async def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + omsId = self.safe_integer(self.options, 'omsId', 1) + await self.load_markets() + await self.load_accounts() + defaultAccountId = self.safe_integer_2(self.options, 'accountId', 'AccountId', int(self.accounts[0]['id'])) + accountId = self.safe_integer_2(params, 'accountId', 'AccountId', defaultAccountId) + clientOrderId = self.safe_integer_2(params, 'ClientOrderId', 'clientOrderId') + params = self.omit(params, ['accountId', 'AccountId', 'clientOrderId', 'ClientOrderId']) + market = self.market(symbol) + orderSide = 0 if (side == 'buy') else 1 + request: dict = { + 'OrderIdToReplace': int(id), + 'InstrumentId': int(market['id']), + 'omsId': omsId, + 'AccountId': accountId, + 'TimeInForce': 1, # 0 Unknown, 1 GTC by default, 2 OPG execute to opening price, 3 IOC immediate or canceled, 4 FOK fill-or-kill, 5 GTX good 'til executed, 6 GTD good 'til date + # 'ClientOrderId': clientOrderId, # defaults to 0 + # If self order is order A, OrderIdOCO refers to the order ID of an order B(which is not the order being created by self call). + # If order B executes, then order A created by self call is canceled. + # You can also set up order B to watch order A in the same way, but that may require an update to order B to make it watch self one, which could have implications for priority in the order book. + # See CancelReplaceOrder and ModifyOrder. + # 'OrderIdOCO': 0, # The order ID if One Cancels the Other. + # 'UseDisplayQuantity': False, # If you enter a Limit order with a reserve, you must set UseDisplayQuantity to True + 'Side': orderSide, # 0 Buy, 1 Sell, 2 Short, 3 unknown an error condition + 'Quantity': float(self.amount_to_precision(symbol, amount)), + 'OrderType': self.safe_integer(self.options['orderTypes'], self.capitalize(type)), # 0 Unknown, 1 Market, 2 Limit, 3 StopMarket, 4 StopLimit, 5 TrailingStopMarket, 6 TrailingStopLimit, 7 BlockTrade + # 'PegPriceType': 3, # 1 Last, 2 Bid, 3 Ask, 4 Midpoint + # 'LimitPrice': float(self.price_to_precision(symbol, price)), + } + # If OrderType=1(Market), Side=0(Buy), and LimitPrice is supplied, the Market order will execute up to the value specified + if price is not None: + request['LimitPrice'] = float(self.price_to_precision(symbol, price)) + if clientOrderId is not None: + request['ClientOrderId'] = clientOrderId + response = await self.privatePostCancelReplaceOrder(self.extend(request, params)) + # + # { + # "replacementOrderId": 1234, + # "replacementClOrdId": 1561, + # "origOrderId": 5678, + # "origClOrdId": 91011, + # } + # + return self.parse_order(response, market) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://apidoc.ndax.io/#gettradeshistory + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + omsId = self.safe_integer(self.options, 'omsId', 1) + await self.load_markets() + await self.load_accounts() + defaultAccountId = self.safe_integer_2(self.options, 'accountId', 'AccountId', int(self.accounts[0]['id'])) + accountId = self.safe_integer_2(params, 'accountId', 'AccountId', defaultAccountId) + params = self.omit(params, ['accountId', 'AccountId']) + request: dict = { + 'omsId': omsId, + 'AccountId': accountId, + # 'InstrumentId': market['id'], + # 'TradeId': 123, # If you specify TradeId, GetTradesHistory can return all states for a single trade + # 'OrderId': 456, # If specified, the call returns all trades associated with the order + # 'UserId': integer. The ID of the logged-in user. If not specified, the call returns trades associated with the users belonging to the default account for the logged-in user of self OMS. + # 'StartTimeStamp': long integer. The historical date and time at which to begin the trade report, in POSIX format. If not specified, reverts to the start date of self account on the trading venue. + # 'EndTimeStamp': long integer. Date at which to end the trade report, in POSIX format. + # 'Depth': integer. In self case, the count of trades to return, counting from the StartIndex. If Depth is not specified, returns all trades between BeginTimeStamp and EndTimeStamp, beginning at StartIndex. + # 'StartIndex': 0 # from the most recent trade 0 and moving backwards in time + # 'ExecutionId': 123, # The ID of the individual buy or sell execution. If not specified, returns all. + } + market = None + if symbol is not None: + market = self.market(symbol) + request['InstrumentId'] = market['id'] + if since is not None: + request['StartTimeStamp'] = self.parse_to_int(since / 1000) + if limit is not None: + request['Depth'] = limit + response = await self.privateGetGetTradesHistory(self.extend(request, params)) + # + # [ + # { + # "OMSId":1, + # "ExecutionId":16916567, + # "TradeId":14476351, + # "OrderId":2543565231, + # "AccountId":449, + # "AccountName":"igor@ccxt.trade", + # "SubAccountId":0, + # "ClientOrderId":0, + # "InstrumentId":8, + # "Side":"Sell", + # "OrderType":"Market", + # "Quantity":0.1230000000000000000000000000, + # "RemainingQuantity":0.0000000000000000000000000000, + # "Price":19069.310000000000000000000000, + # "Value":2345.5251300000000000000000000, + # "CounterParty":"7", + # "OrderTradeRevision":1, + # "Direction":"NoChange", + # "IsBlockTrade":false, + # "Fee":1.1727625650000000000000000000, + # "FeeProductId":8, + # "OrderOriginator":446, + # "UserName":"igor@ccxt.trade", + # "TradeTimeMS":1607565031569, + # "MakerTaker":"Taker", + # "AdapterTradeId":0, + # "InsideBid":19069.310000000000000000000000, + # "InsideBidSize":0.2400950000000000000000000000, + # "InsideAsk":19069.320000000000000000000000, + # "InsideAskSize":0.0997360000000000000000000000, + # "IsQuote":false, + # "CounterPartyClientUserId":1, + # "NotionalProductId":2, + # "NotionalRate":1.0000000000000000000000000000, + # "NotionalValue":2345.5251300000000000000000000, + # "NotionalHoldAmount":0, + # "TradeTime":637431618315686826 + # } + # ] + # + return self.parse_trades(response, market, since, limit) + + async def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders + + https://apidoc.ndax.io/#cancelallorders + + :param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + omsId = self.safe_integer(self.options, 'omsId', 1) + await self.load_markets() + await self.load_accounts() + defaultAccountId = self.safe_integer_2(self.options, 'accountId', 'AccountId', int(self.accounts[0]['id'])) + accountId = self.safe_integer_2(params, 'accountId', 'AccountId', defaultAccountId) + params = self.omit(params, ['accountId', 'AccountId']) + request: dict = { + 'omsId': omsId, + 'AccountId': accountId, + } + if symbol is not None: + market = self.market(symbol) + request['IntrumentId'] = market['id'] + response = await self.privatePostCancelAllOrders(self.extend(request, params)) + # + # { + # "result":true, + # "errormsg":null, + # "errorcode":0, + # "detail":null + # } + # + return [ + self.safe_order({ + 'info': response, + }), + ] + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://apidoc.ndax.io/#cancelorder + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.clientOrderId]: a unique id for the order + :returns dict: An `order structure ` + """ + omsId = self.safe_integer(self.options, 'omsId', 1) + await self.load_markets() + await self.load_accounts() + # defaultAccountId = self.safe_integer_2(self.options, 'accountId', 'AccountId', int(self.accounts[0]['id'])) + # accountId = self.safe_integer_2(params, 'accountId', 'AccountId', defaultAccountId) + # params = self.omit(params, ['accountId', 'AccountId']) + market = None + if symbol is not None: + market = self.market(symbol) + request: dict = { + 'omsId': omsId, + # 'AccountId': accountId, + } + clientOrderId = self.safe_integer_2(params, 'clientOrderId', 'ClOrderId') + if clientOrderId is not None: + request['ClOrderId'] = clientOrderId + else: + request['OrderId'] = int(id) + params = self.omit(params, ['clientOrderId', 'ClOrderId']) + response = await self.privatePostCancelOrder(self.extend(request, params)) + order = self.parse_order(response, market) + return self.extend(order, { + 'id': id, + 'clientOrderId': clientOrderId, + }) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://apidoc.ndax.io/#getopenorders + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + omsId = self.safe_integer(self.options, 'omsId', 1) + await self.load_markets() + await self.load_accounts() + defaultAccountId = self.safe_integer_2(self.options, 'accountId', 'AccountId', int(self.accounts[0]['id'])) + accountId = self.safe_integer_2(params, 'accountId', 'AccountId', defaultAccountId) + params = self.omit(params, ['accountId', 'AccountId']) + market = None + if symbol is not None: + market = self.market(symbol) + request: dict = { + 'omsId': omsId, + 'AccountId': accountId, + } + response = await self.privateGetGetOpenOrders(self.extend(request, params)) + # + # [ + # { + # "Side":"Buy", + # "OrderId":2543565233, + # "Price":19010, + # "Quantity":0.345, + # "DisplayQuantity":0.345, + # "Instrument":8, + # "Account":449, + # "AccountName":"igor@ccxt.trade", + # "OrderType":"Limit", + # "ClientOrderId":0, + # "OrderState":"Working", + # "ReceiveTime":1607579326003, + # "ReceiveTimeTicks":637431761260028981, + # "LastUpdatedTime":1607579326005, + # "LastUpdatedTimeTicks":637431761260054714, + # "OrigQuantity":0.345, + # "QuantityExecuted":0, + # "GrossValueExecuted":0, + # "ExecutableValue":0, + # "AvgPrice":0, + # "CounterPartyId":0, + # "ChangeReason":"NewInputAccepted", + # "OrigOrderId":2543565233, + # "OrigClOrdId":0, + # "EnteredBy":446, + # "UserName":"igor@ccxt.trade", + # "IsQuote":false, + # "InsideAsk":19069.32, + # "InsideAskSize":0.099736, + # "InsideBid":19068.25, + # "InsideBidSize":1.330001, + # "LastTradePrice":19068.25, + # "RejectReason":"", + # "IsLockedIn":false, + # "CancelReason":"", + # "OrderFlag":"AddedToBook", + # "UseMargin":false, + # "StopPrice":0, + # "PegPriceType":"Unknown", + # "PegOffset":0, + # "PegLimitOffset":0, + # "IpAddress":null, + # "ClientOrderIdUuid":null, + # "OMSId":1 + # } + # ] + # + return self.parse_orders(response, market, since, limit) + + async def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://apidoc.ndax.io/#getorderhistory + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + omsId = self.safe_integer(self.options, 'omsId', 1) + await self.load_markets() + await self.load_accounts() + defaultAccountId = self.safe_integer_2(self.options, 'accountId', 'AccountId', int(self.accounts[0]['id'])) + accountId = self.safe_integer_2(params, 'accountId', 'AccountId', defaultAccountId) + params = self.omit(params, ['accountId', 'AccountId']) + request: dict = { + 'omsId': omsId, + 'AccountId': accountId, + # 'ClientOrderId': clientOrderId, + # 'OriginalOrderId': id, + # 'OriginalClientOrderId': long integer, + # 'UserId': integer, + # 'InstrumentId': market['id'], + # 'StartTimestamp': since, + # 'EndTimestamp': self.milliseconds(), + # 'Depth': limit, + # 'StartIndex': 0, + } + market = None + if symbol is not None: + market = self.market(symbol) + request['InstrumentId'] = market['id'] + if since is not None: + request['StartTimeStamp'] = self.parse_to_int(since / 1000) + if limit is not None: + request['Depth'] = limit + response = await self.privateGetGetOrdersHistory(self.extend(request, params)) + # + # [ + # { + # "Side":"Buy", + # "OrderId":2543565233, + # "Price":19010.000000000000000000000000, + # "Quantity":0.0000000000000000000000000000, + # "DisplayQuantity":0.3450000000000000000000000000, + # "Instrument":8, + # "Account":449, + # "AccountName":"igor@ccxt.trade", + # "OrderType":"Limit", + # "ClientOrderId":0, + # "OrderState":"Canceled", + # "ReceiveTime":1607579326003, + # "ReceiveTimeTicks":637431761260028981, + # "LastUpdatedTime":1607580965346, + # "LastUpdatedTimeTicks":637431777653463754, + # "OrigQuantity":0.3450000000000000000000000000, + # "QuantityExecuted":0.0000000000000000000000000000, + # "GrossValueExecuted":0.0000000000000000000000000000, + # "ExecutableValue":0.0000000000000000000000000000, + # "AvgPrice":0.0000000000000000000000000000, + # "CounterPartyId":0, + # "ChangeReason":"UserModified", + # "OrigOrderId":2543565233, + # "OrigClOrdId":0, + # "EnteredBy":446, + # "UserName":"igor@ccxt.trade", + # "IsQuote":false, + # "InsideAsk":19069.320000000000000000000000, + # "InsideAskSize":0.0997360000000000000000000000, + # "InsideBid":19068.250000000000000000000000, + # "InsideBidSize":1.3300010000000000000000000000, + # "LastTradePrice":19068.250000000000000000000000, + # "RejectReason":"", + # "IsLockedIn":false, + # "CancelReason":"UserModified", + # "OrderFlag":"AddedToBook, RemovedFromBook", + # "UseMargin":false, + # "StopPrice":0.0000000000000000000000000000, + # "PegPriceType":"Unknown", + # "PegOffset":0.0000000000000000000000000000, + # "PegLimitOffset":0.0000000000000000000000000000, + # "IpAddress":"x.x.x.x", + # "ClientOrderIdUuid":null, + # "OMSId":1 + # }, + # ] + # + return self.parse_orders(response, market, since, limit) + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://apidoc.ndax.io/#getorderstatus + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + omsId = self.safe_integer(self.options, 'omsId', 1) + await self.load_markets() + await self.load_accounts() + defaultAccountId = self.safe_integer_2(self.options, 'accountId', 'AccountId', int(self.accounts[0]['id'])) + accountId = self.safe_integer_2(params, 'accountId', 'AccountId', defaultAccountId) + params = self.omit(params, ['accountId', 'AccountId']) + market = None + if symbol is not None: + market = self.market(symbol) + request: dict = { + 'omsId': omsId, + 'AccountId': accountId, + 'OrderId': int(id), + } + response = await self.privateGetGetOrderStatus(self.extend(request, params)) + # + # { + # "Side":"Sell", + # "OrderId":2543565232, + # "Price":0.0000000000000000000000000000, + # "Quantity":0.0000000000000000000000000000, + # "DisplayQuantity":0.0000000000000000000000000000, + # "Instrument":8, + # "Account":449, + # "AccountName":"igor@ccxt.trade", + # "OrderType":"Market", + # "ClientOrderId":0, + # "OrderState":"FullyExecuted", + # "ReceiveTime":1607569475591, + # "ReceiveTimeTicks":637431662755912377, + # "LastUpdatedTime":1607569475596, + # "LastUpdatedTimeTicks":637431662755960902, + # "OrigQuantity":1.0000000000000000000000000000, + # "QuantityExecuted":1.0000000000000000000000000000, + # "GrossValueExecuted":19068.270478610000000000000000, + # "ExecutableValue":0.0000000000000000000000000000, + # "AvgPrice":19068.270478610000000000000000, + # "CounterPartyId":0, + # "ChangeReason":"Trade", + # "OrigOrderId":2543565232, + # "OrigClOrdId":0, + # "EnteredBy":446, + # "UserName":"igor@ccxt.trade", + # "IsQuote":false, + # "InsideAsk":19069.320000000000000000000000, + # "InsideAskSize":0.0997360000000000000000000000, + # "InsideBid":19069.310000000000000000000000, + # "InsideBidSize":0.2400950000000000000000000000, + # "LastTradePrice":19069.310000000000000000000000, + # "RejectReason":"", + # "IsLockedIn":false, + # "CancelReason":"", + # "OrderFlag":"0", + # "UseMargin":false, + # "StopPrice":0.0000000000000000000000000000, + # "PegPriceType":"Unknown", + # "PegOffset":0.0000000000000000000000000000, + # "PegLimitOffset":0.0000000000000000000000000000, + # "IpAddress":"x.x.x.x", + # "ClientOrderIdUuid":null, + # "OMSId":1 + # } + # + return self.parse_order(response, market) + + async def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all the trades made from a single order + + https://apidoc.ndax.io/#getorderhistorybyorderid + + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + omsId = self.safe_integer(self.options, 'omsId', 1) + await self.load_markets() + await self.load_accounts() + # defaultAccountId = self.safe_integer_2(self.options, 'accountId', 'AccountId', int(self.accounts[0]['id'])) + # accountId = self.safe_integer_2(params, 'accountId', 'AccountId', defaultAccountId) + # params = self.omit(params, ['accountId', 'AccountId']) + market = None + if symbol is not None: + market = self.market(symbol) + request: dict = { + 'OMSId': self.parse_to_int(omsId), + # 'AccountId': accountId, + 'OrderId': int(id), + } + response = await self.privatePostGetOrderHistoryByOrderId(self.extend(request, params)) + # + # [ + # { + # "Side":"Sell", + # "OrderId":2543565235, + # "Price":18600.000000000000000000000000, + # "Quantity":0.0000000000000000000000000000, + # "DisplayQuantity":0.0000000000000000000000000000, + # "Instrument":8, + # "Account":449, + # "AccountName":"igor@ccxt.trade", + # "OrderType":"Limit", + # "ClientOrderId":0, + # "OrderState":"FullyExecuted", + # "ReceiveTime":1607585844956, + # "ReceiveTimeTicks":637431826449564182, + # "LastUpdatedTime":1607585844959, + # "LastUpdatedTimeTicks":637431826449593893, + # "OrigQuantity":0.1230000000000000000000000000, + # "QuantityExecuted":0.1230000000000000000000000000, + # "GrossValueExecuted":2345.3947500000000000000000000, + # "ExecutableValue":0.0000000000000000000000000000, + # "AvgPrice":19068.250000000000000000000000, + # "CounterPartyId":0, + # "ChangeReason":"Trade", + # "OrigOrderId":2543565235, + # "OrigClOrdId":0, + # "EnteredBy":446, + # "UserName":"igor@ccxt.trade", + # "IsQuote":false, + # "InsideAsk":19069.320000000000000000000000, + # "InsideAskSize":0.0997360000000000000000000000, + # "InsideBid":19068.250000000000000000000000, + # "InsideBidSize":1.3300010000000000000000000000, + # "LastTradePrice":19068.250000000000000000000000, + # "RejectReason":"", + # "IsLockedIn":false, + # "CancelReason":"", + # "OrderFlag":"0", + # "UseMargin":false, + # "StopPrice":0.0000000000000000000000000000, + # "PegPriceType":"Unknown", + # "PegOffset":0.0000000000000000000000000000, + # "PegLimitOffset":0.0000000000000000000000000000, + # "IpAddress":"x.x.x.x", + # "ClientOrderIdUuid":null, + # "OMSId":1 + # }, + # ] + # + grouped = self.group_by(response, 'ChangeReason') + trades = self.safe_list(grouped, 'Trade', []) + return self.parse_trades(trades, market, since, limit) + + async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + omsId = self.safe_integer(self.options, 'omsId', 1) + await self.load_markets() + await self.load_accounts() + defaultAccountId = self.safe_integer_2(self.options, 'accountId', 'AccountId', int(self.accounts[0]['id'])) + accountId = self.safe_integer_2(params, 'accountId', 'AccountId', defaultAccountId) + params = self.omit(params, ['accountId', 'AccountId']) + currency = self.currency(code) + request: dict = { + 'omsId': omsId, + 'AccountId': accountId, + 'ProductId': currency['id'], + 'GenerateNewKey': False, + } + response = await self.privateGetGetDepositInfo(self.extend(request, params)) + # + # { + # "result":true, + # "errormsg":null, + # "statuscode":0, + # "AssetManagerId":1, + # "AccountId":57922, + # "AssetId":16, + # "ProviderId":23, + # "DepositInfo":"[\"0x8A27564b5c30b91C93B1591821642420F323a210\"]" + # } + # + return self.parse_deposit_address(response, currency) + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + # + # fetchDepositAddress, createDepositAddress + # + # { + # "result":true, + # "errormsg":null, + # "statuscode":0, + # "AssetManagerId":1, + # "AccountId":449, + # "AssetId":1, + # "ProviderId":1, + # "DepositInfo":"[\"r3e95RwVsLH7yCbnMfyh7SA8FdwUJCB4S2?memo=241452010\"]" + # } + # + depositInfoString = self.safe_string(depositAddress, 'DepositInfo') + depositInfo = json.loads(depositInfoString) + depositInfoLength = len(depositInfo) + lastString = self.safe_string(depositInfo, depositInfoLength - 1) + parts = lastString.split('?memo=') + address = self.safe_string(parts, 0) + tag = self.safe_string(parts, 1) + code = None + if currency is not None: + code = currency['code'] + self.check_address(address) + return { + 'info': depositAddress, + 'currency': code, + 'network': None, + 'address': address, + 'tag': tag, + } + + async def create_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + create a currency deposit address + :param str code: unified currency code of the currency for the deposit address + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + request: dict = { + 'GenerateNewKey': True, + } + return await self.fetch_deposit_address(code, self.extend(request, params)) + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://apidoc.ndax.io/#getdeposits + + :param str code: unified currency code + :param int [since]: not used by ndax fetchDeposits + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + omsId = self.safe_integer(self.options, 'omsId', 1) + await self.load_markets() + await self.load_accounts() + defaultAccountId = self.safe_integer_2(self.options, 'accountId', 'AccountId', int(self.accounts[0]['id'])) + accountId = self.safe_integer_2(params, 'accountId', 'AccountId', defaultAccountId) + params = self.omit(params, ['accountId', 'AccountId']) + currency = None + if code is not None: + currency = self.currency(code) + request: dict = { + 'omsId': omsId, + 'AccountId': accountId, + } + response = await self.privateGetGetDeposits(self.extend(request, params)) + # + # "[ + # { + # "OMSId": 1, + # "DepositId": 44, + # "AccountId": 449, + # "SubAccountId": 0, + # "ProductId": 4, + # "Amount": 200.00000000000000000000000000, + # "LastUpdateTimeStamp": 637431291261187806, + # "ProductType": "CryptoCurrency", + # "TicketStatus": "FullyProcessed", + # "DepositInfo": "{ + # "AccountProviderId":42, + # "AccountProviderName":"USDT_BSC", + # "TXId":"0x3879b02632c69482646409e991149290bc9a58e4603be63c7c2c90a843f45d2b", + # "FromAddress":"0x8894E0a0c962CB723c1976a4421c95949bE2D4E3", + # "ToAddress":"0x5428EcEB1F7Ee058f64158589e27D087149230CB" + # },", + # "DepositCode": "ab0e23d5-a9ce-4d94-865f-9ab464fb1de3", + # "TicketNumber": 71, + # "NotionalProductId": 13, + # "NotionalValue": 200.00000000000000000000000000, + # "FeeAmount": 0.0000000000000000000000000000, + # }, + # ... + # ]" + # + if isinstance(response, str): + return self.parse_transactions(json.loads(response), currency, since, limit) + return self.parse_transactions(response, currency, since, limit) + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://apidoc.ndax.io/#getwithdraws + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + omsId = self.safe_integer(self.options, 'omsId', 1) + await self.load_markets() + await self.load_accounts() + defaultAccountId = self.safe_integer_2(self.options, 'accountId', 'AccountId', int(self.accounts[0]['id'])) + accountId = self.safe_integer_2(params, 'accountId', 'AccountId', defaultAccountId) + params = self.omit(params, ['accountId', 'AccountId']) + currency = None + if code is not None: + currency = self.currency(code) + request: dict = { + 'omsId': omsId, + 'AccountId': accountId, + } + response = await self.privateGetGetWithdraws(self.extend(request, params)) + # + # [ + # { + # "Amount": 0.0, + # "FeeAmount": 0.0, + # "NotionalValue": 0.0, + # "WithdrawId": 0, + # "AssetManagerId": 0, + # "AccountId": 0, + # "AssetId": 0, + # "TemplateForm": "{\"TemplateType\": \"TetherRPCWithdraw\",\"Comment\": \"TestWithdraw\",\"ExternalAddress\": \"ms6C3pKAAr8gRCcnVebs8VRkVrjcvqNYv3\"}", + # "TemplateFormType": "TetherRPCWithdraw", + # "omsId": 0, + # "TicketStatus": 0, + # "TicketNumber": 0, + # "WithdrawTransactionDetails": "", + # "WithdrawType": "", + # "WithdrawCode": "490b4fa3-53fc-44f4-bd29-7e16be86fba3", + # "AssetType": 0, + # "Reaccepted": True, + # "NotionalProductId": 0 + # }, + # ] + # + return self.parse_transactions(response, currency, since, limit) + + def parse_transaction_status_by_type(self, status, type=None): + statusesByType: dict = { + 'deposit': { + 'New': 'pending', # new ticket awaiting operator review + 'AdminProcessing': 'pending', # an admin is looking at the ticket + 'Accepted': 'pending', # an admin accepts the ticket + 'Rejected': 'rejected', # admin rejects the ticket + 'SystemProcessing': 'pending', # automatic processing; an unlikely status for a deposit + 'FullyProcessed': 'ok', # the deposit has concluded + 'Failed': 'failed', # the deposit has failed for some reason + 'Pending': 'pending', # Account Provider has set status to pending + 'Confirmed': 'pending', # Account Provider confirms the deposit + 'AmlProcessing': 'pending', # anti-money-laundering process underway + 'AmlAccepted': 'pending', # anti-money-laundering process successful + 'AmlRejected': 'rejected', # deposit did not stand up to anti-money-laundering process + 'AmlFailed': 'failed', # anti-money-laundering process failed/did not complete + 'LimitsAccepted': 'pending', # deposit meets limits for fiat or crypto asset + 'LimitsRejected': 'rejected', # deposit does not meet limits for fiat or crypto asset + }, + 'withdrawal': { + 'New': 'pending', # awaiting operator review + 'AdminProcessing': 'pending', # An admin is looking at the ticket + 'Accepted': 'pending', # withdrawal will proceed + 'Rejected': 'rejected', # admin or automatic rejection + 'SystemProcessing': 'pending', # automatic processing underway + 'FullyProcessed': 'ok', # the withdrawal has concluded + 'Failed': 'failed', # the withdrawal failed for some reason + 'Pending': 'pending', # the admin has placed the withdrawal in pending status + 'Pending2Fa': 'pending', # user must click 2-factor authentication confirmation link + 'AutoAccepted': 'pending', # withdrawal will be automatically processed + 'Delayed': 'pending', # waiting for funds to be allocated for the withdrawal + 'UserCanceled': 'canceled', # withdraw canceled by user or Superuser + 'AdminCanceled': 'canceled', # withdraw canceled by Superuser + 'AmlProcessing': 'pending', # anti-money-laundering process underway + 'AmlAccepted': 'pending', # anti-money-laundering process complete + 'AmlRejected': 'rejected', # withdrawal did not stand up to anti-money-laundering process + 'AmlFailed': 'failed', # withdrawal did not complete anti-money-laundering process + 'LimitsAccepted': 'pending', # withdrawal meets limits for fiat or crypto asset + 'LimitsRejected': 'rejected', # withdrawal does not meet limits for fiat or crypto asset + 'Submitted': 'pending', # withdrawal sent to Account Provider; awaiting blockchain confirmation + 'Confirmed': 'pending', # Account Provider confirms that withdrawal is on the blockchain + 'ManuallyConfirmed': 'pending', # admin has sent withdrawal via wallet or admin function directly; marks ticket; debits account + 'Confirmed2Fa': 'pending', # user has confirmed withdraw via 2-factor authentication. + }, + } + statuses = self.safe_value(statusesByType, type, {}) + return self.safe_string(statuses, status, status) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fetchDeposits + # + # { + # "OMSId": 1, + # "DepositId": 44, + # "AccountId": 449, + # "SubAccountId": 0, + # "ProductId": 4, + # "Amount": 200.00000000000000000000000000, + # "LastUpdateTimeStamp": 637431291261187806, + # "ProductType": "CryptoCurrency", + # "TicketStatus": "FullyProcessed", + # "DepositInfo": "{ + # "AccountProviderId":42, + # "AccountProviderName":"USDT_BSC", + # "TXId":"0x3879b02632c69482646409e991149290bc9a58e4603be63c7c2c90a843f45d2b", + # "FromAddress":"0x8894E0a0c962CB723c1976a4421c95949bE2D4E3", + # "ToAddress":"0x5428EcEB1F7Ee058f64158589e27D087149230CB" + # }", + # "DepositCode": "ab0e23d5-a9ce-4d94-865f-9ab464fb1de3", + # "TicketNumber": 71, + # "NotionalProductId": 13, + # "NotionalValue": 200.00000000000000000000000000, + # "FeeAmount": 0.0000000000000000000000000000, + # } + # + # fetchWithdrawals + # + # { + # "Amount": 0.0, + # "FeeAmount": 0.0, + # "NotionalValue": 0.0, + # "WithdrawId": 0, + # "AssetManagerId": 0, + # "AccountId": 0, + # "AssetId": 0, + # "TemplateForm": "{\"TemplateType\": \"TetherRPCWithdraw\",\"Comment\": \"TestWithdraw\",\"ExternalAddress\": \"ms6C3pKAAr8gRCcnVebs8VRkVrjcvqNYv3\"}", + # "TemplateFormType": "TetherRPCWithdraw", + # "omsId": 0, + # "TicketStatus": 0, + # "TicketNumber": 0, + # "WithdrawTransactionDetails": "", + # "WithdrawType": "", + # "WithdrawCode": "490b4fa3-53fc-44f4-bd29-7e16be86fba3", + # "AssetType": 0, + # "Reaccepted": True, + # "NotionalProductId": 0 + # } + # + id = None + currencyId = self.safe_string(transaction, 'ProductId') + code = self.safe_currency_code(currencyId, currency) + type = None + if 'DepositId' in transaction: + id = self.safe_string(transaction, 'DepositId') + type = 'deposit' + elif 'WithdrawId' in transaction: + id = self.safe_string(transaction, 'WithdrawId') + type = 'withdrawal' + templateForm = self.parse_json(self.safe_value_2(transaction, 'TemplateForm', 'DepositInfo')) + updated = self.safe_integer(transaction, 'LastUpdateTimeStamp') + if templateForm is not None: + updated = self.safe_integer(templateForm, 'LastUpdated', updated) + address = self.safe_string_2(templateForm, 'ExternalAddress', 'ToAddress') + timestamp = self.safe_integer(templateForm, 'TimeSubmitted') + feeCost = self.safe_number(transaction, 'FeeAmount') + transactionStatus = self.safe_string(transaction, 'TicketStatus') + fee = None + if feeCost is not None: + fee = {'currency': code, 'cost': feeCost} + return { + 'info': transaction, + 'id': id, + 'txid': self.safe_string_2(templateForm, 'TxId', 'TXId'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'address': address, + 'addressTo': address, + 'addressFrom': self.safe_string(templateForm, 'FromAddress'), + 'tag': None, + 'tagTo': None, + 'tagFrom': None, + 'type': type, + 'amount': self.safe_number(transaction, 'Amount'), + 'currency': code, + 'status': self.parse_transaction_status_by_type(transactionStatus, type), + 'updated': updated, + 'fee': fee, + 'internal': None, + 'comment': None, + 'network': None, + } + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + # self method required login, password and twofa key + sessionToken = self.safe_string(self.options, 'sessionToken') + if sessionToken is None: + raise AuthenticationError(self.id + ' call signIn() method to obtain a session token') + if self.twofa is None: + raise AuthenticationError(self.id + ' withdraw() requires exchange.twofa credentials') + self.check_address(address) + omsId = self.safe_integer(self.options, 'omsId', 1) + await self.load_markets() + await self.load_accounts() + defaultAccountId = self.safe_integer_2(self.options, 'accountId', 'AccountId', int(self.accounts[0]['id'])) + accountId = self.safe_integer_2(params, 'accountId', 'AccountId', defaultAccountId) + params = self.omit(params, ['accountId', 'AccountId']) + currency = self.currency(code) + withdrawTemplateTypesRequest: dict = { + 'omsId': omsId, + 'AccountId': accountId, + 'ProductId': currency['id'], + } + withdrawTemplateTypesResponse = await self.privateGetGetWithdrawTemplateTypes(withdrawTemplateTypesRequest) + # + # { + # "result": True, + # "errormsg": null, + # "statuscode": "0", + # "TemplateTypes": [ + # {AccountProviderId: "14", TemplateName: "ToExternalBitcoinAddress", AccountProviderName: "BitgoRPC-BTC"}, + # {AccountProviderId: "20", TemplateName: "ToExternalBitcoinAddress", AccountProviderName: "TrezorBTC"}, + # {AccountProviderId: "31", TemplateName: "BTC", AccountProviderName: "BTC Fireblocks 1"} + # ] + # } + # + templateTypes = self.safe_value(withdrawTemplateTypesResponse, 'TemplateTypes', []) + firstTemplateType = self.safe_value(templateTypes, 0) + if firstTemplateType is None: + raise ExchangeError(self.id + ' withdraw() could not find a withdraw template type for ' + currency['code']) + templateName = self.safe_string(firstTemplateType, 'TemplateName') + withdrawTemplateRequest: dict = { + 'omsId': omsId, + 'AccountId': accountId, + 'ProductId': currency['id'], + 'TemplateType': templateName, + 'AccountProviderId': firstTemplateType['AccountProviderId'], + } + withdrawTemplateResponse = await self.privateGetGetWithdrawTemplate(withdrawTemplateRequest) + # + # { + # "result": True, + # "errormsg": null, + # "statuscode": "0", + # "Template": "{\"TemplateType\":\"ToExternalBitcoinAddress\",\"Comment\":\"\",\"ExternalAddress\":\"\"}" + # } + # + template = self.safe_string(withdrawTemplateResponse, 'Template') + if template is None: + raise ExchangeError(self.id + ' withdraw() could not find a withdraw template for ' + currency['code']) + withdrawTemplate = json.loads(template) + withdrawTemplate['ExternalAddress'] = address + if tag is not None: + if 'Memo' in withdrawTemplate: + withdrawTemplate['Memo'] = tag + withdrawPayload: dict = { + 'omsId': omsId, + 'AccountId': accountId, + 'ProductId': currency['id'], + 'TemplateForm': self.json(withdrawTemplate), + 'TemplateType': templateName, + } + withdrawRequest: dict = { + 'TfaType': 'Google', + 'TFaCode': self.totp(self.twofa), + 'Payload': self.json(withdrawPayload), + } + response = await self.privatePostCreateWithdrawTicket(self.deep_extend(withdrawRequest, params)) + return self.parse_transaction(response, currency) + + def nonce(self): + return self.milliseconds() + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = self.urls['api'][api] + '/' + self.implode_params(path, params) + query = self.omit(params, self.extract_params(path)) + if api == 'public': + if path == 'Authenticate': + auth = self.login + ':' + self.password + auth64 = self.string_to_base64(auth) + headers = { + 'Authorization': 'Basic ' + auth64, + # 'Content-Type': 'application/json', + } + elif path == 'Authenticate2FA': + pending2faToken = self.safe_string(self.options, 'pending2faToken') + if pending2faToken is not None: + headers = { + 'Pending2FaToken': pending2faToken, + # 'Content-Type': 'application/json', + } + query = self.omit(query, 'pending2faToken') + if query: + url += '?' + self.urlencode(query) + elif api == 'private': + self.check_required_credentials() + sessionToken = self.safe_string(self.options, 'sessionToken') + if sessionToken is None: + nonce = str(self.nonce()) + auth = nonce + self.uid + self.apiKey + signature = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256) + headers = { + 'Nonce': nonce, + 'APIKey': self.apiKey, + 'Signature': signature, + 'UserId': self.uid, + } + else: + headers = { + 'APToken': sessionToken, + } + if method == 'POST': + headers['Content-Type'] = 'application/json' + body = self.json(query) + else: + if query: + url += '?' + self.urlencode(query) + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if code == 404: + raise AuthenticationError(self.id + ' ' + body) + if response is None: + return None + # + # {"status":"Rejected","errormsg":"Not_Enough_Funds","errorcode":101} + # {"result":false,"errormsg":"Server Error","errorcode":102,"detail":null} + # + message = self.safe_string(response, 'errormsg') + if (message is not None) and (message != ''): + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], body, feedback) + raise ExchangeError(feedback) + return None diff --git a/ccxt/async_support/novadax.py b/ccxt/async_support/novadax.py new file mode 100644 index 0000000..63c2b92 --- /dev/null +++ b/ccxt/async_support/novadax.py @@ -0,0 +1,1641 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.novadax import ImplicitAPI +import hashlib +from ccxt.base.types import Account, Any, Balances, Currency, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, Transaction, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import AccountNotEnabled +from ccxt.base.errors import AccountSuspended +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import OnMaintenance +from ccxt.base.errors import CancelPending +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class novadax(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(novadax, self).describe(), { + 'id': 'novadax', + 'name': 'NovaDAX', + 'countries': ['BR'], # Brazil + # 6000 weight per min => 100 weight per second => min weight = 1 + # 100 requests per second =>( 1000ms / 100 ) = 10 ms between requests on average + 'rateLimit': 10, + 'version': 'v1', + # new metainfo interface + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelOrder': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createMarketBuyOrderWithCost': True, + 'createMarketOrderWithCost': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createReduceOnlyOrder': False, + 'createStopLimitOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'fetchAccounts': True, + 'fetchAllGreeks': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': False, + 'fetchDepositAddress': False, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchDepositsWithdrawals': True, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrice': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchOrderTrades': True, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': False, + 'fetchTransactions': 'emulated', + 'fetchUnderlyingAssets': False, + 'fetchVolatilityHistory': False, + 'fetchWithdrawals': True, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'transfer': True, + 'withdraw': True, + }, + 'timeframes': { + '1m': 'ONE_MIN', + '5m': 'FIVE_MIN', + '15m': 'FIFTEEN_MIN', + '30m': 'HALF_HOU', + '1h': 'ONE_HOU', + '1d': 'ONE_DAY', + '1w': 'ONE_WEE', + '1M': 'ONE_MON', + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/92337550-2b085500-f0b3-11ea-98e7-5794fb07dd3b.jpg', + 'api': { + 'public': 'https://api.novadax.com', + 'private': 'https://api.novadax.com', + }, + 'www': 'https://www.novadax.com.br', + 'doc': [ + 'https://doc.novadax.com/pt-BR/', + ], + 'fees': 'https://www.novadax.com.br/fees-and-limits', + 'referral': 'https://www.novadax.com.br/?s=ccxt', + }, + 'api': { + 'public': { + 'get': { + 'common/symbol': 1, + 'common/symbols': 1, + 'common/timestamp': 1, + 'market/tickers': 5, + 'market/ticker': 1, + 'market/depth': 1, + 'market/trades': 5, + 'market/kline/history': 5, + }, + }, + 'private': { + 'get': { + 'orders/get': 1, + 'orders/list': 10, + 'orders/fill': 3, # not found in doc + 'orders/fills': 10, + 'account/getBalance': 1, + 'account/subs': 1, + 'account/subs/balance': 1, + 'account/subs/transfer/record': 10, + 'wallet/query/deposit-withdraw': 3, + }, + 'post': { + 'orders/create': 5, + 'orders/batch-create': 50, + 'orders/cancel': 1, + 'orders/batch-cancel': 10, + 'orders/cancel-by-symbol': 10, + 'account/subs/transfer': 5, + 'wallet/withdraw/coin': 3, + 'account/withdraw/coin': 3, # not found in doc + }, + }, + }, + 'fees': { + 'trading': { + 'tierBased': False, + 'percentage': True, + 'taker': self.parse_number('0.005'), + 'maker': self.parse_number('0.0025'), + }, + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'exact': { + 'A99999': ExchangeError, # 500 Failed Internal error + # 'A10000': ExchangeError, # 200 Success Successful request + 'A10001': BadRequest, # 400 Params error Parameter is invalid + 'A10002': ExchangeError, # 404 Api not found API used is irrelevant + 'A10003': AuthenticationError, # 403 Authentication failed Authentication is failed + 'A10004': RateLimitExceeded, # 429 Too many requests Too many requests are made + 'A10005': PermissionDenied, # 403 Kyc required Need to complete KYC firstly + 'A10006': AccountSuspended, # 403 Customer canceled Account is canceled + 'A10007': AccountNotEnabled, # 400 Account not exist Sub account does not exist + 'A10011': BadSymbol, # 400 Symbol not exist Trading symbol does not exist + 'A10012': BadSymbol, # 400 Symbol not trading Trading symbol is temporarily not available + 'A10013': OnMaintenance, # 503 Symbol maintain Trading symbol is in maintain + 'A30001': OrderNotFound, # 400 Order not found Queried order is not found + 'A30002': InvalidOrder, # 400 Order amount is too small Order amount is too small + 'A30003': InvalidOrder, # 400 Order amount is invalid Order amount is invalid + 'A30004': InvalidOrder, # 400 Order value is too small Order value is too small + 'A30005': InvalidOrder, # 400 Order value is invalid Order value is invalid + 'A30006': InvalidOrder, # 400 Order price is invalid Order price is invalid + 'A30007': InsufficientFunds, # 400 Insufficient balance The balance is insufficient + 'A30008': InvalidOrder, # 400 Order was closed The order has been executed + 'A30009': InvalidOrder, # 400 Order canceled The order has been cancelled + 'A30010': CancelPending, # 400 Order cancelling The order is being cancelled + 'A30011': InvalidOrder, # 400 Order price too high The order price is too high + 'A30012': InvalidOrder, # 400 Order price too low The order price is too low + 'A40004': InsufficientFunds, # {"code":"A40004","data":[],"message":"sub account balance Insufficient"} + }, + 'broad': { + }, + }, + 'options': { + 'fetchOHLCV': { + 'volume': 'amount', # 'amount' for base volume or 'vol' for quote volume + }, + 'transfer': { + 'fillResponseFromRequest': True, + }, + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerDirection': True, # todo + 'triggerPriceType': None, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': None, + # todo + 'timeInForce': { + 'IOC': False, + 'FOK': False, + 'PO': False, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': False, + 'iceberg': True, # todo + }, + 'createOrders': None, # todo: add implementation + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 100000, # todo + 'untilDays': 100000, # todo + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 100000, # todo + 'untilDays': 100000, # todo + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 100000, # todo + 'daysBackCanceled': 1, # todo + 'untilDays': 100000, # todo + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': None, # todo max 3000 + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + }) + + async def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://doc.novadax.com/en-US/#get-current-system-time + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = await self.publicGetCommonTimestamp(params) + # + # { + # "code":"A10000", + # "data":1599090512080, + # "message":"Success" + # } + # + return self.safe_integer(response, 'data') + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for novadax + + https://doc.novadax.com/en-US/#get-all-supported-trading-symbol + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = await self.publicGetCommonSymbols(params) + # + # { + # "code":"A10000", + # "data":[ + # { + # "amountPrecision":8, + # "baseCurrency":"BTC", + # "minOrderAmount":"0.001", + # "minOrderValue":"25", + # "pricePrecision":2, + # "quoteCurrency":"BRL", + # "status":"ONLINE", + # "symbol":"BTC_BRL", + # "valuePrecision":2 + # }, + # ], + # "message":"Success" + # } + # + data = self.safe_value(response, 'data', []) + return self.parse_markets(data) + + def parse_market(self, market: dict) -> Market: + baseId = self.safe_string(market, 'baseCurrency') + quoteId = self.safe_string(market, 'quoteCurrency') + id = self.safe_string(market, 'symbol') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + status = self.safe_string(market, 'status') + return { + 'id': id, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': (status == 'ONLINE'), + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(self.parse_precision(self.safe_string(market, 'amountPrecision'))), + 'price': self.parse_number(self.parse_precision(self.safe_string(market, 'pricePrecision'))), + # 'cost': self.parse_number(self.parse_precision(self.safe_string(market, 'valuePrecision'))), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(market, 'minOrderAmount'), + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': self.safe_number(market, 'minOrderValue'), + 'max': None, + }, + }, + 'created': None, + 'info': market, + } + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # fetchTicker, fetchTickers + # + # { + # "ask":"61946.1", + # "baseVolume24h":"164.41930186", + # "bid":"61815", + # "high24h":"64930.72", + # "lastPrice":"61928.41", + # "low24h":"61156.32", + # "open24h":"64512.46", + # "quoteVolume24h":"10308157.95", + # "symbol":"BTC_BRL", + # "timestamp":1599091115090 + # } + # + timestamp = self.safe_integer(ticker, 'timestamp') + marketId = self.safe_string(ticker, 'symbol') + symbol = self.safe_symbol(marketId, market, '_') + open = self.safe_string(ticker, 'open24h') + last = self.safe_string(ticker, 'lastPrice') + baseVolume = self.safe_string(ticker, 'baseVolume24h') + quoteVolume = self.safe_string(ticker, 'quoteVolume24h') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high24h'), + 'low': self.safe_string(ticker, 'low24h'), + 'bid': self.safe_string(ticker, 'bid'), + 'bidVolume': None, + 'ask': self.safe_string(ticker, 'ask'), + 'askVolume': None, + 'vwap': None, + 'open': open, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'info': ticker, + }, market) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://doc.novadax.com/en-US/#get-latest-ticker-for-specific-pair + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = await self.publicGetMarketTicker(self.extend(request, params)) + # + # { + # "code":"A10000", + # "data":{ + # "ask":"61946.1", + # "baseVolume24h":"164.41930186", + # "bid":"61815", + # "high24h":"64930.72", + # "lastPrice":"61928.41", + # "low24h":"61156.32", + # "open24h":"64512.46", + # "quoteVolume24h":"10308157.95", + # "symbol":"BTC_BRL", + # "timestamp":1599091115090 + # }, + # "message":"Success" + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_ticker(data, market) + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://doc.novadax.com/en-US/#get-latest-tickers-for-all-trading-pairs + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + response = await self.publicGetMarketTickers(params) + # + # { + # "code":"A10000", + # "data":[ + # { + # "ask":"61879.36", + # "baseVolume24h":"164.40955092", + # "bid":"61815", + # "high24h":"64930.72", + # "lastPrice":"61820.04", + # "low24h":"61156.32", + # "open24h":"64624.19", + # "quoteVolume24h":"10307493.92", + # "symbol":"BTC_BRL", + # "timestamp":1599091291083 + # }, + # ], + # "message":"Success" + # } + # + data = self.safe_value(response, 'data', []) + result: dict = {} + for i in range(0, len(data)): + ticker = self.parse_ticker(data[i]) + symbol = ticker['symbol'] + result[symbol] = ticker + return self.filter_by_array_tickers(result, 'symbol', symbols) + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://doc.novadax.com/en-US/#get-market-depth + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['limit'] = limit # default 10, max 20 + response = await self.publicGetMarketDepth(self.extend(request, params)) + # + # { + # "code":"A10000", + # "data":{ + # "asks":[ + # ["0.037159","0.3741"], + # ["0.037215","0.2706"], + # ["0.037222","1.8459"], + # ], + # "bids":[ + # ["0.037053","0.3857"], + # ["0.036969","0.8101"], + # ["0.036953","1.5226"], + # ], + # "timestamp":1599280414448 + # }, + # "message":"Success" + # } + # + data = self.safe_value(response, 'data', {}) + timestamp = self.safe_integer(data, 'timestamp') + return self.parse_order_book(data, market['symbol'], timestamp, 'bids', 'asks') + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # public fetchTrades + # + # { + # "amount":"0.0632", + # "price":"0.037288", + # "side":"BUY", + # "timestamp":1599279694576 + # } + # + # private fetchOrderTrades + # + # { + # "id": "608717046691139584", + # "orderId": "608716957545402368", + # "symbol": "BTC_BRL", + # "side": "BUY", + # "amount": "0.0988", + # "price": "45514.76", + # "fee": "0.0000988 BTC", + # "feeAmount": "0.0000988", + # "feeCurrency": "BTC", + # "role": "MAKER", + # "timestamp": 1565171053345 + # } + # + # private fetchMyTrades(same endpoint) + # + # { + # "id": "608717046691139584", + # "orderId": "608716957545402368", + # "symbol": "BTC_BRL", + # "side": "BUY", + # "amount": "0.0988", + # "price": "45514.76", + # "fee": "0.0000988 BTC", + # "feeAmount": "0.0000988", + # "feeCurrency": "BTC", + # "role": "MAKER", + # "timestamp": 1565171053345 + # } + # + id = self.safe_string(trade, 'id') + orderId = self.safe_string(trade, 'orderId') + timestamp = self.safe_integer(trade, 'timestamp') + side = self.safe_string_lower(trade, 'side') + priceString = self.safe_string(trade, 'price') + amountString = self.safe_string(trade, 'amount') + marketId = self.safe_string(trade, 'symbol') + symbol = self.safe_symbol(marketId, market, '_') + takerOrMaker = self.safe_string_lower(trade, 'role') + feeString = self.safe_string(trade, 'fee') + fee = None + if feeString is not None: + feeCurrencyId = self.safe_string(trade, 'feeCurrency') + feeCurrencyCode = self.safe_currency_code(feeCurrencyId) + fee = { + 'cost': self.safe_string(trade, 'feeAmount'), + 'currency': feeCurrencyCode, + } + return self.safe_trade({ + 'id': id, + 'order': orderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'type': None, + 'side': side, + 'price': priceString, + 'amount': amountString, + 'cost': None, + 'takerOrMaker': takerOrMaker, + 'fee': fee, + 'info': trade, + }, market) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://doc.novadax.com/en-US/#get-recent-trades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['limit'] = limit # default 100 + response = await self.publicGetMarketTrades(self.extend(request, params)) + # + # { + # "code":"A10000", + # "data":[ + # {"amount":"0.0632","price":"0.037288","side":"BUY","timestamp":1599279694576}, + # {"amount":"0.0052","price":"0.03715","side":"SELL","timestamp":1599276606852}, + # {"amount":"0.0058","price":"0.037188","side":"SELL","timestamp":1599275187812}, + # ], + # "message":"Success" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_trades(data, market, since, limit) + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://doc.novadax.com/en-US/#get-kline-data + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'unit': self.safe_string(self.timeframes, timeframe, timeframe), + } + duration = self.parse_timeframe(timeframe) + now = self.seconds() + if limit is None: + limit = 3000 # max + if since is None: + request['from'] = now - limit * duration + request['to'] = now + else: + startFrom = self.parse_to_int(since / 1000) + request['from'] = startFrom + request['to'] = self.sum(startFrom, limit * duration) + response = await self.publicGetMarketKlineHistory(self.extend(request, params)) + # + # { + # "code": "A10000", + # "data": [ + # { + # "amount": 8.25709100, + # "closePrice": 62553.20, + # "count": 29, + # "highPrice": 62592.87, + # "lowPrice": 62553.20, + # "openPrice": 62554.23, + # "score": 1602501480, + # "symbol": "BTC_BRL", + # "vol": 516784.2504067500 + # } + # ], + # "message": "Success" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_ohlcvs(data, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # { + # "amount": 8.25709100, + # "closePrice": 62553.20, + # "count": 29, + # "highPrice": 62592.87, + # "lowPrice": 62553.20, + # "openPrice": 62554.23, + # "score": 1602501480, + # "symbol": "BTC_BRL", + # "vol": 516784.2504067500 + # } + # + options = self.safe_value(self.options, 'fetchOHLCV', {}) + volumeField = self.safe_string(options, 'volume', 'amount') # or vol + return [ + self.safe_timestamp(ohlcv, 'score'), + self.safe_number(ohlcv, 'openPrice'), + self.safe_number(ohlcv, 'highPrice'), + self.safe_number(ohlcv, 'lowPrice'), + self.safe_number(ohlcv, 'closePrice'), + self.safe_number(ohlcv, volumeField), + ] + + def parse_balance(self, response) -> Balances: + data = self.safe_value(response, 'data', []) + result: dict = { + 'info': response, + 'timestamp': None, + 'datetime': None, + } + for i in range(0, len(data)): + balance = data[i] + currencyId = self.safe_string(balance, 'currency') + code = self.safe_currency_code(currencyId) + account = self.account() + account['total'] = self.safe_string(balance, 'balance') + account['free'] = self.safe_string(balance, 'available') + account['used'] = self.safe_string(balance, 'hold') + result[code] = account + return self.safe_balance(result) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://doc.novadax.com/en-US/#get-account-balance + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + response = await self.privateGetAccountGetBalance(params) + # + # { + # "code": "A10000", + # "data": [ + # { + # "available": "1.23", + # "balance": "0.23", + # "currency": "BTC", + # "hold": "1" + # } + # ], + # "message": "Success" + # } + # + return self.parse_balance(response) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://doc.novadax.com/en-US/#order-introduction + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.cost]: for spot market buy orders, the quote quantity that can be used alternative for the amount + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + uppercaseType = type.upper() + uppercaseSide = side.upper() + request: dict = { + 'symbol': market['id'], + 'side': uppercaseSide, # or SELL + # "amount": self.amount_to_precision(symbol, amount), + # "price": "1234.5678", # required for LIMIT and STOP orders + # "operator": "" # for stop orders, can be found in order introduction + # "stopPrice": self.price_to_precision(symbol, stopPrice), + # "accountId": "...", # subaccount id, optional + } + triggerPrice = self.safe_value_2(params, 'triggerPrice', 'stopPrice') + if triggerPrice is None: + if (uppercaseType == 'STOP_LIMIT') or (uppercaseType == 'STOP_MARKET'): + raise ArgumentsRequired(self.id + ' createOrder() requires a stopPrice parameter for ' + uppercaseType + ' orders') + else: + if uppercaseType == 'LIMIT': + uppercaseType = 'STOP_LIMIT' + elif uppercaseType == 'MARKET': + uppercaseType = 'STOP_MARKET' + defaultOperator = 'LTE' if (uppercaseSide == 'BUY') else 'GTE' + request['operator'] = self.safe_string(params, 'operator', defaultOperator) + request['stopPrice'] = self.price_to_precision(symbol, triggerPrice) + params = self.omit(params, ['triggerPrice', 'stopPrice']) + if (uppercaseType == 'LIMIT') or (uppercaseType == 'STOP_LIMIT'): + request['price'] = self.price_to_precision(symbol, price) + request['amount'] = self.amount_to_precision(symbol, amount) + elif (uppercaseType == 'MARKET') or (uppercaseType == 'STOP_MARKET'): + if uppercaseSide == 'SELL': + request['amount'] = self.amount_to_precision(symbol, amount) + elif uppercaseSide == 'BUY': + quoteAmount = None + createMarketBuyOrderRequiresPrice = True + createMarketBuyOrderRequiresPrice, params = self.handle_option_and_params(params, 'createOrder', 'createMarketBuyOrderRequiresPrice', True) + cost = self.safe_number_2(params, 'cost', 'value') + params = self.omit(params, 'cost') + if cost is not None: + quoteAmount = self.cost_to_precision(symbol, cost) + elif createMarketBuyOrderRequiresPrice: + if price is None: + raise InvalidOrder(self.id + ' createOrder() requires the price argument for market buy orders to calculate the total cost to spend(amount * price), alternatively set the createMarketBuyOrderRequiresPrice option or param to False and pass the cost to spend(quote quantity) in the amount argument') + else: + amountString = self.number_to_string(amount) + priceString = self.number_to_string(price) + costRequest = Precise.string_mul(amountString, priceString) + quoteAmount = self.cost_to_precision(symbol, costRequest) + else: + quoteAmount = self.cost_to_precision(symbol, amount) + request['value'] = quoteAmount + request['type'] = uppercaseType + response = await self.privatePostOrdersCreate(self.extend(request, params)) + # + # { + # "code": "A10000", + # "data": { + # "amount": "0.001", + # "averagePrice": null, + # "filledAmount": "0", + # "filledFee": "0", + # "filledValue": "0", + # "id": "870613508008464384", + # "operator": "GTE", + # "price": "210000", + # "side": "BUY", + # "status": "SUBMITTED", + # "stopPrice": "211000", + # "symbol": "BTC_BRL", + # "timestamp": 1627612035528, + # "type": "STOP_LIMIT", + # "value": "210" + # }, + # "message": "Success" + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data, market) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://doc.novadax.com/en-US/#cancel-an-order + + :param str id: order id + :param str symbol: not used by novadax cancelOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + request: dict = { + 'id': id, + } + response = await self.privatePostOrdersCancel(self.extend(request, params)) + # + # { + # "code": "A10000", + # "data": { + # "result": True + # }, + # "message": "Success" + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data) + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://doc.novadax.com/en-US/#get-order-details + + :param str id: order id + :param str symbol: not used by novadax fetchOrder + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + request: dict = { + 'id': id, + } + response = await self.privateGetOrdersGet(self.extend(request, params)) + # + # { + # "code": "A10000", + # "data": { + # "id": "608695623247466496", + # "symbol": "BTC_BRL", + # "type": "MARKET", + # "side": "SELL", + # "price": null, + # "averagePrice": "0", + # "amount": "0.123", + # "filledAmount": "0", + # "value": null, + # "filledValue": "0", + # "filledFee": "0", + # "status": "REJECTED", + # "timestamp": 1565165945588 + # }, + # "message": "Success" + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data) + + async def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://doc.novadax.com/en-US/#get-order-history + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + request: dict = { + # 'symbol': market['id'], + # 'status': 'SUBMITTED,PROCESSING', # SUBMITTED, PROCESSING, PARTIAL_FILLED, CANCELING, FILLED, CANCELED, REJECTED + # 'fromId': '...', # order id to begin with + # 'toId': '...', # order id to end up with + # 'fromTimestamp': since, + # 'toTimestamp': self.milliseconds(), + # 'limit': limit, # default 100, max 100 + } + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if limit is not None: + request['limit'] = limit # default 100, max 100 + if since is not None: + request['fromTimestamp'] = since + response = await self.privateGetOrdersList(self.extend(request, params)) + # + # { + # "code": "A10000", + # "data": [ + # { + # "id": "608695678650028032", + # "symbol": "BTC_BRL", + # "type": "MARKET", + # "side": "SELL", + # "price": null, + # "averagePrice": "0", + # "amount": "0.123", + # "filledAmount": "0", + # "value": null, + # "filledValue": "0", + # "filledFee": "0", + # "status": "REJECTED", + # "timestamp": 1565165958796 + # }, + # ], + # "message": "Success" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_orders(data, market, since, limit) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://doc.novadax.com/en-US/#get-order-history + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + request: dict = { + 'status': 'SUBMITTED,PROCESSING,PARTIAL_FILLED,CANCELING', + } + return await self.fetch_orders(symbol, since, limit, self.extend(request, params)) + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://doc.novadax.com/en-US/#get-order-history + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + request: dict = { + 'status': 'FILLED,CANCELED,REJECTED', + } + return await self.fetch_orders(symbol, since, limit, self.extend(request, params)) + + async def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all the trades made from a single order + + https://doc.novadax.com/en-US/#get-order-match-details + + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + request: dict = { + 'id': id, + } + response = await self.privateGetOrdersFill(self.extend(request, params)) + market = None + if symbol is not None: + market = self.market(symbol) + data = self.safe_value(response, 'data', []) + # + # { + # "code": "A10000", + # "data": [ + # { + # "id": "608717046691139584", + # "orderId": "608716957545402368", + # "symbol": "BTC_BRL", + # "side": "BUY", + # "amount": "0.0988", + # "price": "45514.76", + # "fee": "0.0000988 BTC", + # "feeAmount": "0.0000988", + # "feeCurrency": "BTC", + # "role": "MAKER", + # "timestamp": 1565171053345 + # }, + # ], + # "message": "Success" + # } + # + return self.parse_trades(data, market, since, limit) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'SUBMITTED': 'open', + 'PROCESSING': 'open', + 'PARTIAL_FILLED': 'open', + 'CANCELING': 'open', + 'FILLED': 'closed', + 'CANCELED': 'canceled', + 'REJECTED': 'rejected', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder, fetchOrders, fetchOrder + # + # { + # "amount": "0.001", + # "averagePrice": null, + # "filledAmount": "0", + # "filledFee": "0", + # "filledValue": "0", + # "id": "870613508008464384", + # "operator": "GTE", + # "price": "210000", + # "side": "BUY", + # "status": "SUBMITTED", + # "stopPrice": "211000", + # "symbol": "BTC_BRL", + # "timestamp": 1627612035528, + # "type": "STOP_LIMIT", + # "value": "210" + # } + # + # cancelOrder + # + # { + # "result": True + # } + # + id = self.safe_string(order, 'id') + amount = self.safe_string(order, 'amount') + price = self.safe_string(order, 'price') + cost = self.safe_string_2(order, 'filledValue', 'value') + type = self.safe_string_lower(order, 'type') + side = self.safe_string_lower(order, 'side') + status = self.parse_order_status(self.safe_string(order, 'status')) + timestamp = self.safe_integer(order, 'timestamp') + average = self.safe_string(order, 'averagePrice') + filled = self.safe_string(order, 'filledAmount') + fee = None + feeCost = self.safe_number(order, 'filledFee') + if feeCost is not None: + fee = { + 'cost': feeCost, + 'currency': None, + } + marketId = self.safe_string(order, 'symbol') + symbol = self.safe_symbol(marketId, market, '_') + return self.safe_order({ + 'id': id, + 'clientOrderId': None, + 'info': order, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'symbol': symbol, + 'type': type, + 'timeInForce': None, + 'postOnly': None, + 'side': side, + 'price': price, + 'triggerPrice': self.safe_number(order, 'stopPrice'), + 'amount': amount, + 'cost': cost, + 'average': average, + 'filled': filled, + 'remaining': None, + 'status': status, + 'fee': fee, + 'trades': None, + }, market) + + async def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://doc.novadax.com/en-US/#get-sub-account-transfer + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account to transfer from + :param str toAccount: account to transfer to + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transfer structure ` + """ + await self.load_markets() + currency = self.currency(code) + if fromAccount != 'main' and toAccount != 'main': + raise ExchangeError(self.id + ' transfer() supports transfers between main account and subaccounts only') + # master-transfer-in = from master account to subaccount + # master-transfer-out = from subaccount to master account + type = 'master-transfer-in' if (fromAccount == 'main') else 'master-transfer-out' + request: dict = { + 'transferAmount': self.currency_to_precision(code, amount), + 'currency': currency['id'], + 'subId': toAccount if (type == 'master-transfer-in') else fromAccount, + 'transferType': type, + } + response = await self.privatePostAccountSubsTransfer(self.extend(request, params)) + # + # { + # "code":"A10000", + # "message":"Success", + # "data":40 + # } + # + transfer = self.parse_transfer(response, currency) + transferOptions = self.safe_value(self.options, 'transfer', {}) + fillResponseFromRequest = self.safe_bool(transferOptions, 'fillResponseFromRequest', True) + if fillResponseFromRequest: + transfer['fromAccount'] = fromAccount + transfer['toAccount'] = toAccount + transfer['amount'] = amount + return transfer + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + # + # { + # "code":"A10000", + # "message":"Success", + # "data":40 + # } + # + id = self.safe_string(transfer, 'data') + status = self.safe_string(transfer, 'message') + currencyCode = self.safe_currency_code(None, currency) + return { + 'info': transfer, + 'id': id, + 'amount': None, + 'currency': currencyCode, + 'fromAccount': None, + 'toAccount': None, + 'timestamp': None, + 'datetime': None, + 'status': status, + } + + def parse_transfer_status(self, status: Str) -> Str: + statuses: dict = { + 'SUCCESS': 'pending', + } + return self.safe_string(statuses, status, 'failed') + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://doc.novadax.com/en-US/#send-cryptocurrencies + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'code': currency['id'], + 'amount': self.currency_to_precision(code, amount), + 'wallet': address, + } + if tag is not None: + request['tag'] = tag + response = await self.privatePostAccountWithdrawCoin(self.extend(request, params)) + # + # { + # "code":"A10000", + # "data": "DR123", + # "message":"Success" + # } + # + return self.parse_transaction(response, currency) + + async def fetch_accounts(self, params={}) -> List[Account]: + """ + fetch all the accounts associated with a profile + + https://doc.novadax.com/en-US/#get-sub-account-list + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `account structures ` indexed by the account type + """ + response = await self.privateGetAccountSubs(params) + # + # { + # "code": "A10000", + # "data": [ + # { + # "subId": "CA648856083527372800", + # "state": "Normal", + # "subAccount": "003", + # "subIdentify": "003" + # } + # ], + # "message": "Success" + # } + # + data = self.safe_value(response, 'data', []) + result = [] + for i in range(0, len(data)): + account = data[i] + accountId = self.safe_string(account, 'subId') + type = self.safe_string(account, 'subAccount') + result.append({ + 'id': accountId, + 'type': type, + 'currency': None, + 'info': account, + }) + return result + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://doc.novadax.com/en-US/#wallet-records-of-deposits-and-withdraws + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + request: dict = { + 'type': 'coin_in', + } + return await self.fetch_deposits_withdrawals(code, since, limit, self.extend(request, params)) + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://doc.novadax.com/en-US/#wallet-records-of-deposits-and-withdraws + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + request: dict = { + 'type': 'coin_out', + } + return await self.fetch_deposits_withdrawals(code, since, limit, self.extend(request, params)) + + async def fetch_deposits_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch history of deposits and withdrawals + + https://doc.novadax.com/en-US/#wallet-records-of-deposits-and-withdraws + + :param str [code]: unified currency code for the currency of the deposit/withdrawals, default is None + :param int [since]: timestamp in ms of the earliest deposit/withdrawal, default is None + :param int [limit]: max number of deposit/withdrawals to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `transaction structure ` + """ + await self.load_markets() + request: dict = { + # 'currency': currency['id'], + # 'type': 'coin_in', # 'coin_out' + # 'direct': 'asc', # 'desc' + # 'size': limit, # default 100 + # 'start': id, # offset id + } + currency = None + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + if limit is not None: + request['size'] = limit + response = await self.privateGetWalletQueryDepositWithdraw(self.extend(request, params)) + # + # { + # "code": "A10000", + # "data": [ + # { + # "id": "DR562339304588709888", + # "type": "COIN_IN", + # "currency": "XLM", + # "chain": "XLM", + # "address": "GCUTK7KHPJC3ZQJ3OMWWFHAK2OXIBRD4LNZQRCCOVE7A2XOPP2K5PU5Q", + # "addressTag": "1000009", + # "amount": 1.0, + # "state": "SUCCESS", + # "txHash": "39210645748822f8d4ce673c7559aa6622e6e9cdd7073bc0fcae14b1edfda5f4", + # "createdAt": 1554113737000, + # "updatedAt": 1601371273000 + # } + # ], + # "message": "Success" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_transactions(data, currency, since, limit) + + def parse_transaction_status(self, status: Str): + # Pending the record is wait broadcast to chain + # x/M confirming the comfirming state of tx, the M is total confirmings needed + # SUCCESS the record is success full + # FAIL the record failed + parts = status.split(' ') + status = self.safe_string(parts, 1, status) + statuses: dict = { + 'Pending': 'pending', + 'confirming': 'pending', + 'SUCCESS': 'ok', + 'FAIL': 'failed', + } + return self.safe_string(statuses, status, status) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # withdraw + # + # { + # "code":"A10000", + # "data": "DR123", + # "message":"Success" + # } + # + # fetchDepositsWithdrawals + # + # { + # "id": "DR562339304588709888", + # "type": "COIN_IN", + # "currency": "XLM", + # "chain": "XLM", + # "address": "GCUTK7KHPJC3ZQJ3OMWWFHAK2OXIBRD4LNZQRCCOVE7A2XOPP2K5PU5Q", + # "addressTag": "1000009", + # "amount": 1.0, + # "state": "SUCCESS", + # "txHash": "39210645748822f8d4ce673c7559aa6622e6e9cdd7073bc0fcae14b1edfda5f4", + # "createdAt": 1554113737000, + # "updatedAt": 1601371273000 + # } + # + id = self.safe_string_2(transaction, 'id', 'data') + type = self.safe_string(transaction, 'type') + if type == 'COIN_IN': + type = 'deposit' + elif type == 'COIN_OUT': + type = 'withdraw' + amount = self.safe_number(transaction, 'amount') + address = self.safe_string(transaction, 'address') + tag = self.safe_string(transaction, 'addressTag') + txid = self.safe_string(transaction, 'txHash') + timestamp = self.safe_integer(transaction, 'createdAt') + updated = self.safe_integer(transaction, 'updatedAt') + currencyId = self.safe_string(transaction, 'currency') + code = self.safe_currency_code(currencyId, currency) + status = self.parse_transaction_status(self.safe_string(transaction, 'state')) + network = self.safe_string(transaction, 'chain') + return { + 'info': transaction, + 'id': id, + 'currency': code, + 'amount': amount, + 'network': network, + 'address': address, + 'addressTo': address, + 'addressFrom': None, + 'tag': tag, + 'tagTo': tag, + 'tagFrom': None, + 'status': status, + 'type': type, + 'updated': updated, + 'txid': txid, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'comment': None, + 'internal': None, + 'fee': { + 'currency': None, + 'cost': None, + 'rate': None, + }, + } + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://doc.novadax.com/en-US/#get-order-history + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + request: dict = { + # 'orderId': id, # Order ID, string + # 'symbol': market['id'], # The trading symbol, like BTC_BRL, string + # 'fromId': fromId, # Search fill id to begin with, string + # 'toId': toId, # Search fill id to end up with, string + # 'fromTimestamp': since, # Search order fill time to begin with, in milliseconds, string + # 'toTimestamp': self.milliseconds(), # Search order fill time to end up with, in milliseconds, string + # 'limit': limit, # The number of fills to return, default 100, max 100, string + # 'accountId': subaccountId, # Sub account ID, if not informed, the fills will be return under master account, string + } + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if limit is not None: + request['limit'] = limit + if since is not None: + request['fromTimestamp'] = since + response = await self.privateGetOrdersFills(self.extend(request, params)) + # + # { + # "code": "A10000", + # "data": [ + # { + # "id": "608717046691139584", + # "orderId": "608716957545402368", + # "symbol": "BTC_BRL", + # "side": "BUY", + # "amount": "0.0988", + # "price": "45514.76", + # "fee": "0.0000988 BTC", + # "feeAmount": "0.0000988", + # "feeCurrency": "BTC", + # "role": "MAKER", + # "timestamp": 1565171053345 + # }, + # ], + # "message": "Success" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_trades(data, market, since, limit) + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + request = '/' + self.version + '/' + self.implode_params(path, params) + url = self.urls['api'][api] + request + query = self.omit(params, self.extract_params(path)) + if api == 'public': + if query: + url += '?' + self.urlencode(query) + elif api == 'private': + self.check_required_credentials() + timestamp = str(self.milliseconds()) + headers = { + 'X-Nova-Access-Key': self.apiKey, + 'X-Nova-Timestamp': timestamp, + } + queryString = None + if method == 'POST': + body = self.json(query) + queryString = self.hash(self.encode(body), 'md5') + headers['Content-Type'] = 'application/json' + else: + if query: + url += '?' + self.urlencode(query) + queryString = self.urlencode(self.keysort(query)) + auth = method + "\n" + request + "\n" + queryString + "\n" + timestamp # eslint-disable-line quotes + headers['X-Nova-Signature'] = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256) + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None + # + # {"code":"A10003","data":[],"message":"Authentication failed, Invalid accessKey."} + # + errorCode = self.safe_string(response, 'code') + if errorCode != 'A10000': + message = self.safe_string(response, 'message') + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + raise ExchangeError(feedback) # unknown message + return None diff --git a/ccxt/async_support/oceanex.py b/ccxt/async_support/oceanex.py new file mode 100644 index 0000000..4df1b1e --- /dev/null +++ b/ccxt/async_support/oceanex.py @@ -0,0 +1,1093 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.oceanex import ImplicitAPI +from ccxt.base.types import Any, Balances, Currency, DepositAddress, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, OrderBooks, Trade, TradingFees +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.decimal_to_precision import TICK_SIZE + + +class oceanex(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(oceanex, self).describe(), { + 'id': 'oceanex', + 'name': 'OceanEx', + 'countries': ['BS'], # Bahamas + 'version': 'v1', + 'rateLimit': 3000, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/58385970-794e2d80-8001-11e9-889c-0567cd79b78e.jpg', + 'api': { + 'rest': 'https://api.oceanex.pro', + }, + 'www': 'https://www.oceanex.pro.com', + 'doc': 'https://api.oceanex.pro/doc/v1', + 'referral': 'https://oceanex.pro/signup?referral=VE24QX', + }, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': None, # has but unimplemented + 'future': None, + 'option': None, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'cancelOrders': True, + 'createMarketOrder': True, + 'createOrder': True, + 'fetchBalance': True, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': False, + 'fetchDepositAddress': 'emulated', + 'fetchDepositAddresses': None, + 'fetchDepositAddressesByNetwork': True, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchMarkets': True, + 'fetchOHLCV': True, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrderBooks': True, + 'fetchOrders': True, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': True, + 'fetchTransactionFees': None, + }, + 'timeframes': { + '1m': '1', + '5m': '5', + '15m': '15', + '30m': '30', + '1h': '60', + '2h': '120', + '4h': '240', + '6h': '360', + '12h': '720', + '1d': '1440', + '3d': '4320', + '1w': '10080', + }, + 'api': { + 'public': { + 'get': [ + 'markets', + 'tickers/{pair}', + 'tickers_multi', + 'order_book', + 'order_book/multi', + 'fees/trading', + 'trades', + 'timestamp', + ], + 'post': [ + 'k', + ], + }, + 'private': { + 'get': [ + 'key', + 'members/me', + 'orders', + 'orders/filter', + ], + 'post': [ + 'orders', + 'orders/multi', + 'order/delete', + 'order/delete/multi', + 'orders/clear', + '/withdraws/special/new', + '/deposit_address', + '/deposit_addresses', + '/deposit_history', + '/withdraw_history', + ], + }, + }, + 'fees': { + 'trading': { + 'tierBased': False, + 'percentage': True, + 'maker': self.parse_number('0.001'), + 'taker': self.parse_number('0.001'), + }, + }, + 'commonCurrencies': { + 'PLA': 'Plair', + }, + 'precisionMode': TICK_SIZE, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, # todo + 'triggerDirection': True, # todo + 'triggerPriceType': None, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': False, + 'FOK': False, + 'PO': False, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': False, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': None, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 100, # todo: max unknown + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 100000, # todo + 'untilDays': 100000, # todo + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 100000, # todo + 'daysBackCanceled': 1, # todo + 'untilDays': 100000, # todo + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 100, + }, + }, + # todo implement swap + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'exceptions': { + 'codes': { + '-1': BadRequest, + '-2': BadRequest, + '1001': BadRequest, + '1004': ArgumentsRequired, + '1006': AuthenticationError, + '1008': AuthenticationError, + '1010': AuthenticationError, + '1011': PermissionDenied, + '2001': AuthenticationError, + '2002': InvalidOrder, + '2004': OrderNotFound, + '9003': PermissionDenied, + }, + 'exact': { + 'market does not have a valid value': BadRequest, + 'side does not have a valid value': BadRequest, + 'Account::AccountError: Cannot lock funds': InsufficientFunds, + 'The account does not exist': AuthenticationError, + }, + }, + }) + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for oceanex + + https://api.oceanex.pro/doc/v1/#markets-post + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + request: dict = {'show_details': True} + response = await self.publicGetMarkets(self.extend(request, params)) + # + # { + # "id": "xtzusdt", + # "name": "XTZ/USDT", + # "ask_precision": "8", + # "bid_precision": "8", + # "enabled": True, + # "price_precision": "4", + # "amount_precision": "3", + # "usd_precision": "4", + # "minimum_trading_amount": "1.0" + # }, + # + markets = self.safe_value(response, 'data', []) + return self.parse_markets(markets) + + def parse_market(self, market: dict) -> Market: + id = self.safe_value(market, 'id') + name = self.safe_value(market, 'name') + baseId, quoteId = name.split('/') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + baseId = baseId.lower() + quoteId = quoteId.lower() + symbol = base + '/' + quote + return { + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': None, + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(self.parse_precision(self.safe_string(market, 'amount_precision'))), + 'price': self.parse_number(self.parse_precision(self.safe_string(market, 'price_precision'))), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': None, + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': self.safe_number(market, 'minimum_trading_amount'), + 'max': None, + }, + }, + 'created': None, + 'info': market, + } + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://api.oceanex.pro/doc/v1/#ticker-post + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + response = await self.publicGetTickersPair(self.extend(request, params)) + # + # { + # "code":0, + # "message":"Operation successful", + # "data": { + # "at":1559431729, + # "ticker": { + # "buy":"0.0065", + # "sell":"0.00677", + # "low":"0.00677", + # "high":"0.00677", + # "last":"0.00677", + # "vol":"2000.0" + # } + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_ticker(data, market) + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://api.oceanex.pro/doc/v1/#multiple-tickers-post + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + if symbols is None: + symbols = self.symbols + marketIds = self.market_ids(symbols) + request: dict = {'markets': marketIds} + response = await self.publicGetTickersMulti(self.extend(request, params)) + # + # { + # "code":0, + # "message":"Operation successful", + # "data": { + # "at":1559431729, + # "ticker": { + # "buy":"0.0065", + # "sell":"0.00677", + # "low":"0.00677", + # "high":"0.00677", + # "last":"0.00677", + # "vol":"2000.0" + # } + # } + # } + # + data = self.safe_value(response, 'data', []) + result: dict = {} + for i in range(0, len(data)): + ticker = data[i] + marketId = self.safe_string(ticker, 'market') + market = self.safe_market(marketId) + symbol = market['symbol'] + result[symbol] = self.parse_ticker(ticker, market) + return self.filter_by_array_tickers(result, 'symbol', symbols) + + def parse_ticker(self, data, market: Market = None): + # + # { + # "at":1559431729, + # "ticker": { + # "buy":"0.0065", + # "sell":"0.00677", + # "low":"0.00677", + # "high":"0.00677", + # "last":"0.00677", + # "vol":"2000.0" + # } + # } + # + ticker = self.safe_value(data, 'ticker', {}) + timestamp = self.safe_timestamp(data, 'at') + symbol = self.safe_symbol(None, market) + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': self.safe_string(ticker, 'buy'), + 'bidVolume': None, + 'ask': self.safe_string(ticker, 'sell'), + 'askVolume': None, + 'vwap': None, + 'open': None, + 'close': self.safe_string(ticker, 'last'), + 'last': self.safe_string(ticker, 'last'), + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': self.safe_string(ticker, 'volume'), + 'quoteVolume': None, + 'info': ticker, + }, market) + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://api.oceanex.pro/doc/v1/#order-book-post + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + if limit is not None: + request['limit'] = limit + response = await self.publicGetOrderBook(self.extend(request, params)) + # + # { + # "code":0, + # "message":"Operation successful", + # "data": { + # "timestamp":1559433057, + # "asks": [ + # ["100.0","20.0"], + # ["4.74","2000.0"], + # ["1.74","4000.0"], + # ], + # "bids":[ + # ["0.0065","5482873.4"], + # ["0.00649","4781956.2"], + # ["0.00648","2876006.8"], + # ], + # } + # } + # + orderbook = self.safe_value(response, 'data', {}) + timestamp = self.safe_timestamp(orderbook, 'timestamp') + return self.parse_order_book(orderbook, symbol, timestamp) + + async def fetch_order_books(self, symbols: Strings = None, limit: Int = None, params={}) -> OrderBooks: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data for multiple markets + + https://api.oceanex.pro/doc/v1/#multiple-order-books-post + + :param str[]|None symbols: list of unified market symbols, all symbols fetched if None, default is None + :param int [limit]: max number of entries per orderbook to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `order book structures ` indexed by market symbol + """ + await self.load_markets() + if symbols is None: + symbols = self.symbols + marketIds = self.market_ids(symbols) + request: dict = { + 'markets': marketIds, + } + if limit is not None: + request['limit'] = limit + response = await self.publicGetOrderBookMulti(self.extend(request, params)) + # + # { + # "code":0, + # "message":"Operation successful", + # "data": [ + # { + # "timestamp":1559433057, + # "market": "bagvet", + # "asks": [ + # ["100.0","20.0"], + # ["4.74","2000.0"], + # ["1.74","4000.0"], + # ], + # "bids":[ + # ["0.0065","5482873.4"], + # ["0.00649","4781956.2"], + # ["0.00648","2876006.8"], + # ], + # }, + # ..., + # ], + # } + # + data = self.safe_value(response, 'data', []) + result: dict = {} + for i in range(0, len(data)): + orderbook = data[i] + marketId = self.safe_string(orderbook, 'market') + symbol = self.safe_symbol(marketId) + timestamp = self.safe_timestamp(orderbook, 'timestamp') + result[symbol] = self.parse_order_book(orderbook, symbol, timestamp) + return result + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://api.oceanex.pro/doc/v1/#trades-post + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + if limit is not None: + request['limit'] = min(limit, 1000) + response = await self.publicGetTrades(self.extend(request, params)) + # + # { + # "code":0, + # "message":"Operation successful", + # "data": [ + # { + # "id":220247666, + # "price":"3098.62", + # "volume":"0.00196", + # "funds":"6.0732952", + # "market":"ethusdt", + # "created_at":"2022-04-19T19:03:15Z", + # "created_on":1650394994, + # "side":"bid" + # }, + # ] + # } + # + data = self.safe_list(response, 'data') + return self.parse_trades(data, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(public) + # + # { + # "id":220247666, + # "price":"3098.62", + # "volume":"0.00196", + # "funds":"6.0732952", + # "market":"ethusdt", + # "created_at":"2022-04-19T19:03:15Z", + # "created_on":1650394995, + # "side":"bid" + # } + # + side = self.safe_value(trade, 'side') + if side == 'bid': + side = 'buy' + elif side == 'ask': + side = 'sell' + marketId = self.safe_value(trade, 'market') + symbol = self.safe_symbol(marketId, market) + timestamp = self.safe_timestamp(trade, 'created_on') + if timestamp is None: + timestamp = self.parse8601(self.safe_string(trade, 'created_at')) + priceString = self.safe_string(trade, 'price') + amountString = self.safe_string(trade, 'volume') + return self.safe_trade({ + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'id': self.safe_string(trade, 'id'), + 'order': None, + 'type': 'limit', + 'takerOrMaker': None, + 'side': side, + 'price': priceString, + 'amount': amountString, + 'cost': None, + 'fee': None, + }, market) + + async def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://api.oceanex.pro/doc/v1/#api-server-time-post + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = await self.publicGetTimestamp(params) + # + # {"code":0,"message":"Operation successful","data":1559433420} + # + return self.safe_timestamp(response, 'data') + + async def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://api.oceanex.pro/doc/v1/#trading-fees-post + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + response = await self.publicGetFeesTrading(params) + data = self.safe_value(response, 'data', []) + result: dict = {} + for i in range(0, len(data)): + group = data[i] + maker = self.safe_value(group, 'ask_fee', {}) + taker = self.safe_value(group, 'bid_fee', {}) + marketId = self.safe_string(group, 'market') + symbol = self.safe_symbol(marketId) + result[symbol] = { + 'info': group, + 'symbol': symbol, + 'maker': self.safe_number(maker, 'value'), + 'taker': self.safe_number(taker, 'value'), + 'percentage': True, + } + return result + + async def fetch_key(self, params={}): + response = await self.privateGetKey(params) + return self.safe_value(response, 'data') + + def parse_balance(self, response) -> Balances: + data = self.safe_value(response, 'data') + balances = self.safe_value(data, 'accounts', []) + result: dict = {'info': response} + for i in range(0, len(balances)): + balance = balances[i] + currencyId = self.safe_value(balance, 'currency') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(balance, 'balance') + account['used'] = self.safe_string(balance, 'locked') + result[code] = account + return self.safe_balance(result) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://api.oceanex.pro/doc/v1/#account-info-post + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + response = await self.privateGetMembersMe(params) + return self.parse_balance(response) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://api.oceanex.pro/doc/v1/#new-order-post + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + 'side': side, + 'ord_type': type, + 'volume': self.amount_to_precision(symbol, amount), + } + if type == 'limit': + request['price'] = self.price_to_precision(symbol, price) + response = await self.privatePostOrders(self.extend(request, params)) + data = self.safe_dict(response, 'data') + return self.parse_order(data, market) + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://api.oceanex.pro/doc/v1/#order-status-get + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + ids = [id] + request: dict = {'ids': ids} + response = await self.privateGetOrders(self.extend(request, params)) + data = self.safe_value(response, 'data') + dataLength = len(data) + if data is None: + raise OrderNotFound(self.id + ' could not found matching order') + if isinstance(id, list): + orders = self.parse_orders(data, market) + return orders[0] + if dataLength == 0: + raise OrderNotFound(self.id + ' could not found matching order') + return self.parse_order(data[0], market) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://api.oceanex.pro/doc/v1/#order-status-get + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + request: dict = { + 'states': ['wait'], + } + return await self.fetch_orders(symbol, since, limit, self.extend(request, params)) + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://api.oceanex.pro/doc/v1/#order-status-get + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + request: dict = { + 'states': ['done', 'cancel'], + } + return await self.fetch_orders(symbol, since, limit, self.extend(request, params)) + + async def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://api.oceanex.pro/doc/v1/#order-status-with-filters-post + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrders() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + states = self.safe_value(params, 'states', ['wait', 'done', 'cancel']) + query = self.omit(params, 'states') + request: dict = { + 'market': market['id'], + 'states': states, + 'need_price': 'True', + } + if limit is not None: + request['limit'] = limit + response = await self.privateGetOrdersFilter(self.extend(request, query)) + data = self.safe_value(response, 'data', []) + result = [] + for i in range(0, len(data)): + orders = self.safe_value(data[i], 'orders', []) + status = self.parse_order_status(self.safe_value(data[i], 'state')) + parsedOrders = self.parse_orders(orders, market, since, limit, {'status': status}) + result = self.array_concat(result, parsedOrders) + return result + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # [ + # 1559232000, + # 8889.22, + # 9028.52, + # 8889.22, + # 9028.52 + # 0.3121 + # ] + return [ + self.safe_timestamp(ohlcv, 0), + self.safe_number(ohlcv, 1), + self.safe_number(ohlcv, 2), + self.safe_number(ohlcv, 3), + self.safe_number(ohlcv, 4), + self.safe_number(ohlcv, 5), + ] + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://api.oceanex.pro/doc/v1/#k-line-post + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + 'period': self.safe_string(self.timeframes, timeframe, timeframe), + } + if since is not None: + request['timestamp'] = since + if limit is not None: + request['limit'] = min(limit, 10000) + response = await self.publicPostK(self.extend(request, params)) + ohlcvs = self.safe_list(response, 'data', []) + return self.parse_ohlcvs(ohlcvs, market, timeframe, since, limit) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # { + # "created_at": "2019-01-18T00:38:18Z", + # "trades_count": 0, + # "remaining_volume": "0.2", + # "price": "1001.0", + # "created_on": "1547771898", + # "side": "buy", + # "volume": "0.2", + # "state": "wait", + # "ord_type": "limit", + # "avg_price": "0.0", + # "executed_volume": "0.0", + # "id": 473797, + # "market": "veteth" + # } + # + status = self.parse_order_status(self.safe_value(order, 'state')) + marketId = self.safe_string_2(order, 'market', 'market_id') + symbol = self.safe_symbol(marketId, market) + timestamp = self.safe_timestamp(order, 'created_on') + if timestamp is None: + timestamp = self.parse8601(self.safe_string(order, 'created_at')) + price = self.safe_string(order, 'price') + average = self.safe_string(order, 'avg_price') + amount = self.safe_string(order, 'volume') + remaining = self.safe_string(order, 'remaining_volume') + filled = self.safe_string(order, 'executed_volume') + return self.safe_order({ + 'info': order, + 'id': self.safe_string(order, 'id'), + 'clientOrderId': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'symbol': symbol, + 'type': self.safe_value(order, 'ord_type'), + 'timeInForce': None, + 'postOnly': None, + 'side': self.safe_value(order, 'side'), + 'price': price, + 'triggerPrice': None, + 'average': average, + 'amount': amount, + 'remaining': remaining, + 'filled': filled, + 'status': status, + 'cost': None, + 'trades': None, + 'fee': None, + }, market) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'wait': 'open', + 'done': 'closed', + 'cancel': 'canceled', + } + return self.safe_string(statuses, status, status) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://api.oceanex.pro/doc/v1/#cancel-order-post + + :param str id: order id + :param str symbol: not used by oceanex cancelOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + response = await self.privatePostOrderDelete(self.extend({'id': id}, params)) + data = self.safe_dict(response, 'data') + return self.parse_order(data) + + async def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): + """ + cancel multiple orders + + https://api.oceanex.pro/doc/v1/#cancel-multiple-orders-post + + :param str[] ids: order ids + :param str symbol: not used by oceanex cancelOrders() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an list of `order structures ` + """ + await self.load_markets() + response = await self.privatePostOrderDeleteMulti(self.extend({'ids': ids}, params)) + data = self.safe_list(response, 'data') + return self.parse_orders(data) + + async def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders + + https://api.oceanex.pro/doc/v1/#cancel-all-orders-post + + :param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + response = await self.privatePostOrdersClear(params) + data = self.safe_list(response, 'data') + return self.parse_orders(data) + + async def fetch_deposit_addresses_by_network(self, code: str, params={}) -> List[DepositAddress]: + """ + fetch the deposit addresses for a currency associated with self account + + https://api.oceanex.pro/doc/v1/#deposit-addresses-post + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary `address structures `, indexed by the network + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + } + response = await self.privatePostDepositAddresses(self.extend(request, params)) + # + # { + # code: '0', + # message: 'Operation successful', + # data: { + # data: { + # currency_id: 'usdt', + # display_name: 'USDT', + # num_of_resources: '3', + # resources: [ + # { + # chain_name: 'TRC20', + # currency_id: 'usdt', + # address: 'TPcS7VgKMFmpRrWY82GbJzDeMnemWxEbpg', + # memo: '', + # deposit_status: 'enabled' + # }, + # ... + # ] + # } + # } + # } + # + data = self.safe_dict(response, 'data', {}) + data2 = self.safe_dict(data, 'data', {}) + resources = self.safe_list(data2, 'resources', []) + result = {} + for i in range(0, len(resources)): + resource = resources[i] + enabled = self.safe_string(resource, 'deposit_status') + if enabled == 'enabled': + parsedAddress = self.parse_deposit_address(resource, currency) + result[parsedAddress['currency']] = parsedAddress + return result + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + # + # { + # chain_name: 'TRC20', + # currency_id: 'usdt', + # address: 'TPcS7VgKMFmpRrWY82GbJzDeMnemWxEbpg', + # memo: '', + # deposit_status: 'enabled' + # } + # + address = self.safe_string(depositAddress, 'address') + self.check_address(address) + currencyId = self.safe_string(depositAddress, 'currency_id') + networkId = self.safe_string(depositAddress, 'chain_name') + return { + 'info': depositAddress, + 'currency': self.safe_currency_code(currencyId, currency), + 'network': self.network_id_to_code(networkId), + 'address': address, + 'tag': self.safe_string(depositAddress, 'memo'), + } + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = self.urls['api']['rest'] + '/' + self.version + '/' + self.implode_params(path, params) + query = self.omit(params, self.extract_params(path)) + if api == 'public': + if path == 'tickers_multi' or path == 'order_book/multi': + request = '?' + markets = self.safe_value(params, 'markets') + for i in range(0, len(markets)): + request += 'markets[]=' + markets[i] + '&' + limit = self.safe_value(params, 'limit') + if limit is not None: + request += 'limit=' + limit + url += request + elif query: + url += '?' + self.urlencode(query) + elif api == 'private': + self.check_required_credentials() + request: dict = { + 'uid': self.apiKey, + 'data': query, + } + # to set the private key: + # fs = require('fs') + # exchange.secret = fs.readFileSync('oceanex.pem', 'utf8') + jwt_token = self.jwt(request, self.encode(self.secret), 'sha256', True) + url += '?user_jwt=' + jwt_token + headers = {'Content-Type': 'application/json'} + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + # + # {"code":1011,"message":"This IP 'x.x.x.x' is not allowed","data":{}} + # + if response is None: + return None + errorCode = self.safe_string(response, 'code') + message = self.safe_string(response, 'message') + if (errorCode is not None) and (errorCode != '0'): + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['codes'], errorCode, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) + raise ExchangeError(feedback) + return None diff --git a/ccxt/async_support/okx.py b/ccxt/async_support/okx.py new file mode 100644 index 0000000..d25b529 --- /dev/null +++ b/ccxt/async_support/okx.py @@ -0,0 +1,8651 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.okx import ImplicitAPI +import asyncio +import hashlib +from ccxt.base.types import Account, Any, Balances, BorrowInterest, Conversion, CrossBorrowRate, CrossBorrowRates, Currencies, Currency, DepositAddress, Greeks, Int, LedgerEntry, Leverage, LeverageTier, LongShortRatio, MarginModification, Market, Num, Option, OptionChain, Order, OrderBook, OrderRequest, CancellationRequest, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, FundingRates, OpenInterests, Trade, TradingFeeInterface, Transaction, MarketInterface, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import AccountNotEnabled +from ccxt.base.errors import AccountSuspended +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import OperationRejected +from ccxt.base.errors import ManualInteractionNeeded +from ccxt.base.errors import RestrictedLocation +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidAddress +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import ContractUnavailable +from ccxt.base.errors import NotSupported +from ccxt.base.errors import NetworkError +from ccxt.base.errors import DDoSProtection +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.errors import OnMaintenance +from ccxt.base.errors import InvalidNonce +from ccxt.base.errors import RequestTimeout +from ccxt.base.errors import CancelPending +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class okx(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(okx, self).describe(), { + 'id': 'okx', + 'name': 'OKX', + 'countries': ['CN', 'US'], + 'version': 'v5', + 'rateLimit': 100 * 1.10, # 10% tolerance because of #26973 + 'pro': True, + 'certified': True, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': True, + 'swap': True, + 'future': True, + 'option': True, + 'addMargin': True, + 'cancelAllOrders': False, + 'cancelAllOrdersAfter': True, + 'cancelOrder': True, + 'cancelOrders': True, + 'cancelOrdersForSymbols': True, + 'closeAllPositions': False, + 'closePosition': True, + 'createConvertTrade': True, + 'createDepositAddress': False, + 'createMarketBuyOrderWithCost': True, + 'createMarketSellOrderWithCost': True, + 'createOrder': True, + 'createOrders': True, + 'createOrderWithTakeProfitAndStopLoss': True, + 'createPostOnlyOrder': True, + 'createReduceOnlyOrder': True, + 'createStopLimitOrder': True, + 'createStopLossOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'createTakeProfitOrder': True, + 'createTrailingPercentOrder': True, + 'createTriggerOrder': True, + 'editOrder': True, + 'fetchAccounts': True, + 'fetchAllGreeks': True, + 'fetchBalance': True, + 'fetchBidsAsks': None, + 'fetchBorrowInterest': True, + 'fetchBorrowRateHistories': True, + 'fetchBorrowRateHistory': True, + 'fetchCanceledOrders': True, + 'fetchClosedOrder': None, + 'fetchClosedOrders': True, + 'fetchConvertCurrencies': True, + 'fetchConvertQuote': True, + 'fetchConvertTrade': True, + 'fetchConvertTradeHistory': True, + 'fetchCrossBorrowRate': True, + 'fetchCrossBorrowRates': True, + 'fetchCurrencies': True, + 'fetchDeposit': True, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': True, + 'fetchDeposits': True, + 'fetchDepositsWithdrawals': False, + 'fetchDepositWithdrawFee': 'emulated', + 'fetchDepositWithdrawFees': True, + 'fetchFundingHistory': True, + 'fetchFundingInterval': True, + 'fetchFundingIntervals': False, + 'fetchFundingRate': True, + 'fetchFundingRateHistory': True, + 'fetchFundingRates': True, + 'fetchGreeks': True, + 'fetchIndexOHLCV': True, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchL3OrderBook': False, + 'fetchLedger': True, + 'fetchLedgerEntry': None, + 'fetchLeverage': True, + 'fetchLeverageTiers': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': True, + 'fetchMarginAdjustmentHistory': True, + 'fetchMarketLeverageTiers': True, + 'fetchMarkets': True, + 'fetchMarkOHLCV': True, + 'fetchMarkPrice': True, + 'fetchMarkPrices': True, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': True, + 'fetchOpenInterestHistory': True, + 'fetchOpenInterests': True, + 'fetchOpenOrder': None, + 'fetchOpenOrders': True, + 'fetchOption': True, + 'fetchOptionChain': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrderBooks': False, + 'fetchOrders': False, + 'fetchOrderTrades': True, + 'fetchPosition': True, + 'fetchPositionHistory': 'emulated', + 'fetchPositions': True, + 'fetchPositionsForSymbol': True, + 'fetchPositionsHistory': True, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': True, + 'fetchStatus': True, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': True, + 'fetchTradingFees': False, + 'fetchTradingLimits': False, + 'fetchTransactionFee': False, + 'fetchTransactionFees': False, + 'fetchTransactions': False, + 'fetchTransfer': True, + 'fetchTransfers': True, + 'fetchUnderlyingAssets': True, + 'fetchVolatilityHistory': False, + 'fetchWithdrawal': True, + 'fetchWithdrawals': True, + 'fetchWithdrawalWhitelist': False, + 'reduceMargin': True, + 'repayCrossMargin': True, + 'sandbox': True, + 'setLeverage': True, + 'setMargin': False, + 'setMarginMode': True, + 'setPositionMode': True, + 'signIn': False, + 'transfer': True, + 'withdraw': True, + }, + 'timeframes': { + '1m': '1m', + '3m': '3m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1H', + '2h': '2H', + '4h': '4H', + '6h': '6H', + '12h': '12H', + '1d': '1D', + '1w': '1W', + '1M': '1M', + '3M': '3M', + }, + 'hostname': 'www.okx.com', # or aws.okx.com + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/152485636-38b19e4a-bece-4dec-979a-5982859ffc04.jpg', + 'api': { + 'rest': 'https://{hostname}', + }, + 'www': 'https://www.okx.com', + 'doc': 'https://www.okx.com/docs-v5/en/', + 'fees': 'https://www.okx.com/pages/products/fees.html', + 'referral': { + # old reflink 0% discount https://www.okx.com/join/1888677 + # new reflink 20% discount https://www.okx.com/join/CCXT2023 + 'url': 'https://www.okx.com/join/CCXTCOM', + 'discount': 0.2, + }, + 'test': { + 'rest': 'https://{hostname}', + }, + }, + 'api': { + 'public': { + 'get': { + 'market/books-full': 2, + 'market/tickers': 1, + 'market/ticker': 1, + 'market/index-tickers': 1, + 'market/books': 1 / 2, + 'market/books-lite': 5 / 3, + 'market/candles': 1 / 2, + 'market/history-candles': 1, + 'market/index-candles': 1, + 'market/history-index-candles': 2, + 'market/mark-price-candles': 1, + 'market/history-mark-price-candles': 2, + 'market/trades': 1 / 5, + 'market/history-trades': 2, + 'market/option/instrument-family-trades': 1, + 'market/platform-24-volume': 10, + 'market/open-oracle': 50, + 'market/exchange-rate': 20, + 'market/index-components': 1, + 'public/market-data-history': 4, + 'public/economic-calendar': 50, + 'market/block-tickers': 1, + 'market/block-ticker': 1, + 'public/block-trades': 1, + 'public/instruments': 1, + 'public/delivery-exercise-history': 1 / 2, + 'public/open-interest': 1, + 'public/funding-rate': 1, + 'public/funding-rate-history': 1, + 'public/price-limit': 1, + 'public/opt-summary': 1, + 'public/estimated-price': 2, + 'public/discount-rate-interest-free-quota': 10, + 'public/time': 2, + 'public/mark-price': 2, + 'public/position-tiers': 2, + 'public/interest-rate-loan-quota': 10, + 'public/vip-interest-rate-loan-quota': 10, + 'public/underlying': 1, + 'public/insurance-fund': 2, + 'public/convert-contract-coin': 2, + 'public/option-trades': 1, + 'public/instrument-tick-bands': 4, + 'rubik/stat/trading-data/support-coin': 4, + 'rubik/stat/taker-volume': 4, + 'rubik/stat/margin/loan-ratio': 4, + # long/short + 'rubik/stat/contracts/long-short-account-ratio': 4, + 'rubik/stat/contracts/long-short-account-ratio-contract': 4, + 'rubik/stat/contracts/open-interest-volume': 4, + 'rubik/stat/option/open-interest-volume': 4, + # put/call + 'rubik/stat/option/open-interest-volume-ratio': 4, + 'rubik/stat/option/open-interest-volume-expiry': 4, + 'rubik/stat/option/open-interest-volume-strike': 4, + 'rubik/stat/option/taker-block-volume': 4, + 'system/status': 50, + # public api + 'sprd/spreads': 1, + 'sprd/books': 1 / 2, + 'sprd/ticker': 1, + 'sprd/public-trades': 1 / 5, + 'market/sprd-ticker': 2, + 'market/sprd-candles': 2, + 'market/sprd-history-candles': 2, + 'tradingBot/grid/ai-param': 1, + 'tradingBot/grid/min-investment': 1, + 'tradingBot/public/rsi-back-testing': 1, + 'asset/exchange-list': 5 / 3, + 'finance/staking-defi/eth/apy-history': 5 / 3, + 'finance/staking-defi/sol/apy-history': 5 / 3, + 'finance/savings/lending-rate-summary': 5 / 3, + 'finance/savings/lending-rate-history': 5 / 3, + 'finance/fixed-loan/lending-offers': 10 / 3, + 'finance/fixed-loan/lending-apy-history': 10 / 3, + 'finance/fixed-loan/pending-lending-volume': 10 / 3, + # public broker + 'finance/sfp/dcd/products': 2 / 3, + # copytrading + 'copytrading/public-lead-traders': 4, + 'copytrading/public-weekly-pnl': 4, + 'copytrading/public-stats': 4, + 'copytrading/public-preference-currency': 4, + 'copytrading/public-current-subpositions': 4, + 'copytrading/public-subpositions-history': 4, + 'support/announcements-types': 20, + }, + }, + 'private': { + 'get': { + # rfq + 'rfq/counterparties': 4, + 'rfq/maker-instrument-settings': 4, + 'rfq/mmp-config': 4, + 'rfq/rfqs': 10, + 'rfq/quotes': 10, + 'rfq/trades': 4, + 'rfq/public-trades': 4, + # sprd + 'sprd/order': 1 / 3, + 'sprd/orders-pending': 1 / 3, + 'sprd/orders-history': 1 / 2, + 'sprd/orders-history-archive': 1 / 2, + 'sprd/trades': 1 / 3, + # trade + 'trade/order': 1 / 3, + 'trade/orders-pending': 1 / 3, + 'trade/orders-history': 1 / 2, + 'trade/orders-history-archive': 1, + 'trade/fills': 1 / 3, + 'trade/fills-history': 2.2, + 'trade/fills-archive': 2, + 'trade/order-algo': 1, + 'trade/orders-algo-pending': 1, + 'trade/orders-algo-history': 1, + 'trade/easy-convert-currency-list': 20, + 'trade/easy-convert-history': 20, + 'trade/one-click-repay-currency-list': 20, + 'trade/one-click-repay-currency-list-v2': 20, + 'trade/one-click-repay-history': 20, + 'trade/one-click-repay-history-v2': 20, + 'trade/account-rate-limit': 1, + # asset + 'asset/currencies': 5 / 3, + 'asset/balances': 5 / 3, + 'asset/non-tradable-assets': 5 / 3, + 'asset/asset-valuation': 10, + 'asset/transfer-state': 10, + 'asset/bills': 5 / 3, + 'asset/deposit-lightning': 5, + 'asset/deposit-address': 5 / 3, + 'asset/deposit-history': 5 / 3, + 'asset/withdrawal-history': 5 / 3, + 'asset/deposit-withdraw-status': 20, + 'asset/convert/currencies': 5 / 3, + 'asset/convert/currency-pair': 5 / 3, + 'asset/convert/history': 5 / 3, + 'asset/monthly-statement': 2, + # account + 'account/instruments': 1, + 'account/balance': 2, + 'account/positions': 2, + 'account/positions-history': 100, + 'account/account-position-risk': 2, + 'account/bills': 5 / 3, + 'account/bills-archive': 5 / 3, + 'account/bills-history-archive': 2, + 'account/config': 4, + 'account/max-size': 1, + 'account/max-avail-size': 1, + 'account/leverage-info': 1, + 'account/adjust-leverage-info': 4, + 'account/max-loan': 1, + 'account/trade-fee': 4, + 'account/interest-accrued': 4, + 'account/interest-rate': 4, + 'account/max-withdrawal': 1, + 'account/risk-state': 2, + 'account/quick-margin-borrow-repay-history': 4, + 'account/borrow-repay-history': 4, + 'account/vip-interest-accrued': 4, + 'account/vip-interest-deducted': 4, + 'account/vip-loan-order-list': 4, + 'account/vip-loan-order-detail': 4, + 'account/interest-limits': 4, + 'account/greeks': 2, + 'account/position-tiers': 2, + 'account/mmp-config': 4, + 'account/fixed-loan/borrowing-limit': 4, + 'account/fixed-loan/borrowing-quote': 5, + 'account/fixed-loan/borrowing-orders-list': 5, + 'account/spot-manual-borrow-repay': 30, + 'account/set-auto-repay': 4, + 'account/spot-borrow-repay-history': 4, + 'account/move-positions-history': 10, + # subaccount + 'users/subaccount/list': 10, + 'account/subaccount/balances': 10 / 3, + 'asset/subaccount/balances': 10 / 3, + 'account/subaccount/max-withdrawal': 1, + 'asset/subaccount/bills': 5 / 3, + 'asset/subaccount/managed-subaccount-bills': 5 / 3, + 'users/entrust-subaccount-list': 10, + 'account/subaccount/interest-limits': 4, + 'users/subaccount/apikey': 10, + # grid trading + 'tradingBot/grid/orders-algo-pending': 1, + 'tradingBot/grid/orders-algo-history': 1, + 'tradingBot/grid/orders-algo-details': 1, + 'tradingBot/grid/sub-orders': 1, + 'tradingBot/grid/positions': 1, + 'tradingBot/grid/ai-param': 1, + 'tradingBot/signal/signals': 1, + 'tradingBot/signal/orders-algo-details': 1, + 'tradingBot/signal/orders-algo-history': 1, + 'tradingBot/signal/positions': 1, + 'tradingBot/signal/positions-history': 1, + 'tradingBot/signal/sub-orders': 1, + 'tradingBot/signal/event-history': 1, + 'tradingBot/recurring/orders-algo-pending': 1, + 'tradingBot/recurring/orders-algo-history': 1, + 'tradingBot/recurring/orders-algo-details': 1, + 'tradingBot/recurring/sub-orders': 1, + # earn + 'finance/savings/balance': 5 / 3, + 'finance/savings/lending-history': 5 / 3, + 'finance/staking-defi/offers': 10 / 3, + 'finance/staking-defi/orders-active': 10 / 3, + 'finance/staking-defi/orders-history': 10 / 3, + # eth staking + 'finance/staking-defi/eth/balance': 5 / 3, + 'finance/staking-defi/eth/purchase-redeem-history': 5 / 3, + 'finance/staking-defi/eth/product-info': 3, + 'finance/staking-defi/sol/balance': 5 / 3, + 'finance/staking-defi/sol/purchase-redeem-history': 5 / 3, + # copytrading + 'copytrading/current-subpositions': 1, + 'copytrading/subpositions-history': 1, + 'copytrading/instruments': 4, + 'copytrading/profit-sharing-details': 4, + 'copytrading/total-profit-sharing': 4, + 'copytrading/unrealized-profit-sharing-details': 4, + 'copytrading/copy-settings': 4, + 'copytrading/batch-leverage-info': 4, + 'copytrading/current-lead-traders': 4, + 'copytrading/lead-traders-history': 4, + # broker + 'broker/nd/info': 10, + 'broker/nd/subaccount-info': 10, + 'broker/nd/subaccount/apikey': 10, + 'asset/broker/nd/subaccount-deposit-address': 5 / 3, + 'asset/broker/nd/subaccount-deposit-history': 4, + 'asset/broker/nd/subaccount-withdrawal-history': 4, + 'broker/nd/rebate-daily': 100, + 'broker/nd/rebate-per-orders': 300, + 'finance/sfp/dcd/order': 2, + 'finance/sfp/dcd/orders': 2, + 'broker/fd/rebate-per-orders': 300, + 'broker/fd/if-rebate': 5, + # affiliate + 'affiliate/invitee/detail': 1, + 'users/partner/if-rebate': 1, + 'support/announcements': 4, + }, + 'post': { + # rfq + 'rfq/create-rfq': 4, + 'rfq/cancel-rfq': 4, + 'rfq/cancel-batch-rfqs': 10, + 'rfq/cancel-all-rfqs': 10, + 'rfq/execute-quote': 15, + 'rfq/maker-instrument-settings': 4, + 'rfq/mmp-reset': 4, + 'rfq/mmp-config': 100, + 'rfq/create-quote': 0.4, + 'rfq/cancel-quote': 0.4, + 'rfq/cancel-batch-quotes': 10, + 'rfq/cancel-all-quotes': 10, + # sprd + 'sprd/order': 1, + 'sprd/cancel-order': 1, + 'sprd/mass-cancel': 1, + 'sprd/amend-order': 1, + 'sprd/cancel-all-after': 10, + # trade + 'trade/order': 1 / 3, + 'trade/batch-orders': 1 / 15, + 'trade/cancel-order': 1 / 3, + 'trade/cancel-batch-orders': 1 / 15, + 'trade/amend-order': 1 / 3, + 'trade/amend-batch-orders': 1 / 150, + 'trade/close-position': 1, + 'trade/fills-archive': 172800, # 5 req per day = 5/24/60/60 => 10/5*24*60*60=172800 + 'trade/order-algo': 1, + 'trade/cancel-algos': 1, + 'trade/amend-algos': 1, + 'trade/cancel-advance-algos': 1, + 'trade/easy-convert': 20, + 'trade/one-click-repay': 20, + 'trade/one-click-repay-v2': 20, + 'trade/mass-cancel': 4, + 'trade/cancel-all-after': 10, + # asset + 'asset/transfer': 10, + 'asset/withdrawal': 5 / 3, + 'asset/withdrawal-lightning': 5, + 'asset/cancel-withdrawal': 5 / 3, + 'asset/convert-dust-assets': 10, + 'asset/convert/estimate-quote': 1, + 'asset/convert/trade': 1, + 'asset/monthly-statement': 1, + # account + 'account/set-position-mode': 4, + 'account/set-leverage': 1, + 'account/position/margin-balance': 1, + 'account/set-greeks': 4, + 'account/set-isolated-mode': 4, + 'account/quick-margin-borrow-repay': 4, + 'account/borrow-repay': 5 / 3, + 'account/simulated_margin': 10, + 'account/position-builder': 10, + 'account/set-riskOffset-type': 2, + 'account/activate-option': 4, + 'account/set-auto-loan': 4, + 'account/set-account-level': 4, + 'account/mmp-reset': 4, + 'account/mmp-config': 100, + 'account/fixed-loan/borrowing-order': 5, + 'account/fixed-loan/amend-borrowing-order': 5, + 'account/fixed-loan/manual-reborrow': 5, + 'account/fixed-loan/repay-borrowing-order': 5, + 'account/bills-history-archive': 72000, # 12 req/day + 'account/move-positions': 10, + 'account/set-settle-currency': 1, + # subaccount + 'users/subaccount/modify-apikey': 10, + 'asset/subaccount/transfer': 10, + 'users/subaccount/set-transfer-out': 10, + 'account/subaccount/set-loan-allocation': 4, + 'users/subaccount/create-subaccount': 10, + 'users/subaccount/subaccount-apikey': 10, + 'users/subaccount/delete-apikey': 10, + # grid trading + 'tradingBot/grid/order-algo': 1, + 'tradingBot/grid/amend-order-algo': 1, + 'tradingBot/grid/stop-order-algo': 1, + 'tradingBot/grid/close-position': 1, + 'tradingBot/grid/cancel-close-order': 1, + 'tradingBot/grid/order-instant-trigger': 1, + 'tradingBot/grid/withdraw-income': 1, + 'tradingBot/grid/compute-margin-balance': 1, + 'tradingBot/grid/margin-balance': 1, + 'tradingBot/grid/min-investment': 1, + 'tradingBot/grid/adjust-investment': 1, + 'tradingBot/signal/create-signal': 1, + 'tradingBot/signal/order-algo': 1, + 'tradingBot/signal/stop-order-algo': 1, + 'tradingBot/signal/margin-balance': 1, + 'tradingBot/signal/amendTPSL': 1, + 'tradingBot/signal/set-instruments': 1, + 'tradingBot/signal/close-position': 1, + 'tradingBot/signal/sub-order': 1, + 'tradingBot/signal/cancel-sub-order': 1, + 'tradingBot/recurring/order-algo': 1, + 'tradingBot/recurring/amend-order-algo': 1, + 'tradingBot/recurring/stop-order-algo': 1, + # earn + 'finance/savings/purchase-redempt': 5 / 3, + 'finance/savings/set-lending-rate': 5 / 3, + 'finance/staking-defi/purchase': 3, + 'finance/staking-defi/redeem': 3, + 'finance/staking-defi/cancel': 3, + # eth staking + 'finance/staking-defi/eth/purchase': 5, + 'finance/staking-defi/eth/redeem': 5, + 'finance/staking-defi/sol/purchase': 5, + 'finance/staking-defi/sol/redeem': 5, + # copytrading + 'copytrading/algo-order': 1, + 'copytrading/close-subposition': 1, + 'copytrading/set-instruments': 4, + 'copytrading/first-copy-settings': 4, + 'copytrading/amend-copy-settings': 4, + 'copytrading/stop-copy-trading': 4, + 'copytrading/batch-set-leverage': 4, + # broker + 'broker/nd/create-subaccount': 0.25, + 'broker/nd/delete-subaccount': 1, + 'broker/nd/subaccount/apikey': 0.25, + 'broker/nd/subaccount/modify-apikey': 1, + 'broker/nd/subaccount/delete-apikey': 1, + 'broker/nd/set-subaccount-level': 4, + 'broker/nd/set-subaccount-fee-rate': 4, + 'broker/nd/set-subaccount-assets': 0.25, + 'asset/broker/nd/subaccount-deposit-address': 1, + 'asset/broker/nd/modify-subaccount-deposit-address': 5 / 3, + 'broker/nd/rebate-per-orders': 36000, + 'finance/sfp/dcd/quote': 10, + 'finance/sfp/dcd/order': 10, + 'broker/nd/report-subaccount-ip': 0.25, + 'broker/fd/rebate-per-orders': 36000, + }, + }, + }, + 'fees': { + 'trading': { + 'taker': self.parse_number('0.0015'), + 'maker': self.parse_number('0.0010'), + }, + 'spot': { + 'taker': self.parse_number('0.0015'), + 'maker': self.parse_number('0.0010'), + }, + 'future': { + 'taker': self.parse_number('0.0005'), + 'maker': self.parse_number('0.0002'), + }, + 'swap': { + 'taker': self.parse_number('0.00050'), + 'maker': self.parse_number('0.00020'), + }, + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + 'password': True, + }, + 'exceptions': { + 'exact': { + # Public error codes from 50000-53999 + # General Class + '1': ExchangeError, # Operation failed + '2': ExchangeError, # Bulk operation partially succeeded + '4088': ManualInteractionNeeded, # {"code":"4088","data":[],"msg":"You can’t trade or deposit until you’ve verified your identity again. Head to Identity Verification to complete it."} + '50000': BadRequest, # Body can not be empty + '50001': OnMaintenance, # Matching engine upgrading. Please try again later + '50002': BadRequest, # Json data format error + '50004': RequestTimeout, # Endpoint request timeout(does not indicate success or failure of order, please check order status) + '50005': ExchangeNotAvailable, # API is offline or unavailable + '50006': BadRequest, # Invalid Content_Type, please use "application/json" format + '50007': AccountSuspended, # Account blocked + '50008': AuthenticationError, # User does not exist + '50009': AccountSuspended, # Account is suspended due to ongoing liquidation + '50010': ExchangeError, # User ID can not be empty + '50011': RateLimitExceeded, # Request too frequent + '50012': ExchangeError, # Account status invalid + '50013': ExchangeNotAvailable, # System is busy, please try again later + '50014': BadRequest, # Parameter {0} can not be empty + '50015': ExchangeError, # Either parameter {0} or {1} is required + '50016': ExchangeError, # Parameter {0} does not match parameter {1} + '50017': ExchangeError, # The position is frozen due to ADL. Operation restricted + '50018': ExchangeError, # Currency {0} is frozen due to ADL. Operation restricted + '50019': ExchangeError, # The account is frozen due to ADL. Operation restricted + '50020': ExchangeError, # The position is frozen due to liquidation. Operation restricted + '50021': ExchangeError, # Currency {0} is frozen due to liquidation. Operation restricted + '50022': ExchangeError, # The account is frozen due to liquidation. Operation restricted + '50023': ExchangeError, # Funding fee frozen. Operation restricted + '50024': BadRequest, # Parameter {0} and {1} can not exist at the same time + '50025': ExchangeError, # Parameter {0} count exceeds the limit {1} + '50026': ExchangeNotAvailable, # System error, please try again later. + '50027': PermissionDenied, # The account is restricted from trading + '50028': ExchangeError, # Unable to take the order, please reach out to support center for details + '50044': BadRequest, # Must select one broker type + '50061': ExchangeError, # You've reached the maximum order rate limit for self account. + '50062': ExchangeError, # This feature is currently unavailable. + # API Class + '50100': ExchangeError, # API frozen, please contact customer service + '50101': AuthenticationError, # Broker id of APIKey does not match current environment + '50102': InvalidNonce, # Timestamp request expired + '50103': AuthenticationError, # Request header "OK_ACCESS_KEY" can not be empty + '50104': AuthenticationError, # Request header "OK_ACCESS_PASSPHRASE" can not be empty + '50105': AuthenticationError, # Request header "OK_ACCESS_PASSPHRASE" incorrect + '50106': AuthenticationError, # Request header "OK_ACCESS_SIGN" can not be empty + '50107': AuthenticationError, # Request header "OK_ACCESS_TIMESTAMP" can not be empty + '50108': ExchangeError, # Exchange ID does not exist + '50109': ExchangeError, # Exchange domain does not exist + '50110': PermissionDenied, # Invalid IP + '50111': AuthenticationError, # Invalid OK_ACCESS_KEY + '50112': AuthenticationError, # Invalid OK_ACCESS_TIMESTAMP + '50113': AuthenticationError, # Invalid signature + '50114': AuthenticationError, # Invalid authorization + '50115': BadRequest, # Invalid request method + # Trade Class + '51000': BadRequest, # Parameter {0} error + '51001': BadSymbol, # Instrument ID does not exist + '51002': BadSymbol, # Instrument ID does not match underlying index + '51003': BadRequest, # Either client order ID or order ID is required + '51004': InvalidOrder, # Order amount exceeds current tier limit + '51005': InvalidOrder, # Order amount exceeds the limit + '51006': InvalidOrder, # Order price out of the limit + '51007': InvalidOrder, # Order placement failed. Order amount should be at least 1 contract(showing up when placing an order with less than 1 contract) + '51008': InsufficientFunds, # Order placement failed due to insufficient balance or margin + '51009': AccountSuspended, # Order placement function is blocked by the platform + '51010': AccountNotEnabled, # Account level too low {"code":"1","data":[{"clOrdId":"uJrfGFth9F","ordId":"","sCode":"51010","sMsg":"The current account mode does not support self API interface. ","tag":""}],"msg":"Operation failed."} + '51011': InvalidOrder, # Duplicated order ID + '51012': BadSymbol, # Token does not exist + '51014': BadSymbol, # Index does not exist + '51015': BadSymbol, # Instrument ID does not match instrument type + '51016': InvalidOrder, # Duplicated client order ID + '51017': ExchangeError, # Borrow amount exceeds the limit + '51018': ExchangeError, # User with option account can not hold net short positions + '51019': ExchangeError, # No net long positions can be held under isolated margin mode in options + '51020': InvalidOrder, # Order amount should be greater than the min available amount + '51021': ContractUnavailable, # Contract to be listed + '51022': ContractUnavailable, # Contract suspended + '51023': ExchangeError, # Position does not exist + '51024': AccountSuspended, # Unified accountblocked + '51025': ExchangeError, # Order count exceeds the limit + '51026': BadSymbol, # Instrument type does not match underlying index + '51027': ContractUnavailable, # Contract expired + '51028': ContractUnavailable, # Contract under delivery + '51029': ContractUnavailable, # Contract is being settled + '51030': ContractUnavailable, # Funding fee is being settled + '51031': InvalidOrder, # This order price is not within the closing price range + '51046': InvalidOrder, # The take profit trigger price must be higher than the order price + '51047': InvalidOrder, # The stop loss trigger price must be lower than the order price + '51051': InvalidOrder, # Your SL price should be lower than the primary order price + '51072': InvalidOrder, # As a spot lead trader, you need to set tdMode to 'spot_isolated' when configured buying lead trade pairs + '51073': InvalidOrder, # As a spot lead trader, you need to use '/copytrading/close-subposition' for selling assets through lead trades + '51074': InvalidOrder, # Only the tdMode for lead trade pairs configured by spot lead traders can be set to 'spot_isolated' + '51090': InvalidOrder, # You can't modify the amount of an SL order placed with a TP limit order. + '51091': InvalidOrder, # All TP orders in one order must be of the same type. + '51092': InvalidOrder, # TP order prices(tpOrdPx) in one order must be different. + '51093': InvalidOrder, # TP limit order prices(tpOrdPx) in one order can't be –1(market price). + '51094': InvalidOrder, # You can't place TP limit orders in spot, margin, or options trading. + '51095': InvalidOrder, # To place TP limit orders at self endpoint, you must place an SL order at the same time. + '51096': InvalidOrder, # cxlOnClosePos needs to be True to place a TP limit order + '51098': InvalidOrder, # You can't add a new TP order to an SL order placed with a TP limit order. + '51099': InvalidOrder, # You can't place TP limit orders lead trader. + '51100': InvalidOrder, # Trading amount does not meet the min tradable amount + '51101': InvalidOrder, # Entered amount exceeds the max pending order amount(Cont) per transaction + '51102': InvalidOrder, # Entered amount exceeds the max pending count + '51103': InvalidOrder, # Entered amount exceeds the max pending order count of the underlying asset + '51104': InvalidOrder, # Entered amount exceeds the max pending order amount(Cont) of the underlying asset + '51105': InvalidOrder, # Entered amount exceeds the max order amount(Cont) of the contract + '51106': InvalidOrder, # Entered amount exceeds the max order amount(Cont) of the underlying asset + '51107': InvalidOrder, # Entered amount exceeds the max holding amount(Cont) + '51108': InvalidOrder, # Positions exceed the limit for closing out with the market price + '51109': InvalidOrder, # No available offer + '51110': InvalidOrder, # You can only place a limit order after Call Auction has started + '51111': BadRequest, # Maximum {0} orders can be placed in bulk + '51112': InvalidOrder, # Close order size exceeds your available size + '51113': RateLimitExceeded, # Market-price liquidation requests too frequent + '51115': InvalidOrder, # Cancel all pending close-orders before liquidation + '51116': InvalidOrder, # Order price or trigger price exceeds {0} + '51117': InvalidOrder, # Pending close-orders count exceeds limit + '51118': InvalidOrder, # Total amount should exceed the min amount per order + '51119': InsufficientFunds, # Order placement failed due to insufficient balance + '51120': InvalidOrder, # Order quantity is less than {0}, please try again + '51121': InvalidOrder, # Order count should be the integer multiples of the lot size + '51122': InvalidOrder, # Order price should be higher than the min price {0} + '51124': InvalidOrder, # You can only place limit orders during call auction + '51125': InvalidOrder, # Currently there are reduce + reverse position pending orders in margin trading. Please cancel all reduce + reverse position pending orders and continue + '51126': InvalidOrder, # Currently there are reduce only pending orders in margin trading.Please cancel all reduce only pending orders and continue + '51127': InsufficientFunds, # Available balance is 0 + '51128': InvalidOrder, # Multi-currency margin account can not do cross-margin trading + '51129': InvalidOrder, # The value of the position and buy order has reached the position limit, and no further buying is allowed + '51130': BadSymbol, # Fixed margin currency error + '51131': InsufficientFunds, # Insufficient balance + '51132': InvalidOrder, # Your position amount is negative and less than the minimum trading amount + '51133': InvalidOrder, # Reduce-only feature is unavailable for the spot transactions by multi-currency margin account + '51134': InvalidOrder, # Closing failed. Please check your holdings and pending orders + '51135': InvalidOrder, # Your closing price has triggered the limit price, and the max buy price is {0} + '51136': InvalidOrder, # Your closing price has triggered the limit price, and the min sell price is {0} + '51137': InvalidOrder, # Your opening price has triggered the limit price, and the max buy price is {0} + '51138': InvalidOrder, # Your opening price has triggered the limit price, and the min sell price is {0} + '51139': InvalidOrder, # Reduce-only feature is unavailable for the spot transactions by simple account + '51155': RestrictedLocation, # {"code":"1","data":[{"clOrdId":"e847xxx","ordId":"","sCode":"51155","sMsg":"You can't trade self pair or borrow self crypto due to local compliance restrictions. ","tag":"e847xxx","ts":"1753979177157"}],"inTime":"1753979177157408","msg":"All operations failed","outTime":"1753979177157874"} + '51156': BadRequest, # You're leading trades in long/short mode and can't use self API endpoint to close positions + '51159': BadRequest, # You're leading trades in buy/sell mode. If you want to place orders using self API endpoint, the orders must be in the same direction existing positions and open orders. + '51162': InvalidOrder, # You have {instrument} open orders. Cancel these orders and try again + '51163': InvalidOrder, # You hold {instrument} positions. Close these positions and try again + '51166': InvalidOrder, # Currently, we don't support leading trades with self instrument + '51174': InvalidOrder, # The number of {param0} pending orders reached the upper limit of {param1}(orders). + '51185': InvalidOrder, # The maximum value allowed per order is {maxOrderValue} USD + '51201': InvalidOrder, # Value of per market order cannot exceed 100,000 USDT + '51202': InvalidOrder, # Market - order amount exceeds the max amount + '51203': InvalidOrder, # Order amount exceeds the limit {0} + '51204': InvalidOrder, # The price for the limit order can not be empty + '51205': InvalidOrder, # Reduce-Only is not available + '51250': InvalidOrder, # Algo order price is out of the available range + '51251': InvalidOrder, # Algo order type error(when user place an iceberg order) + '51252': InvalidOrder, # Algo order price is out of the available range + '51253': InvalidOrder, # Average amount exceeds the limit of per iceberg order + '51254': InvalidOrder, # Iceberg average amount error(when user place an iceberg order) + '51255': InvalidOrder, # Limit of per iceberg order: Total amount/1000 < x <= Total amount + '51256': InvalidOrder, # Iceberg order price variance error + '51257': InvalidOrder, # Trail order callback rate error + '51258': InvalidOrder, # Trail - order placement failed. The trigger price of a sell order should be higher than the last transaction price + '51259': InvalidOrder, # Trail - order placement failed. The trigger price of a buy order should be lower than the last transaction price + '51260': InvalidOrder, # Maximum {0} pending trail - orders can be held at the same time + '51261': InvalidOrder, # Each user can hold up to {0} pending stop - orders at the same time + '51262': InvalidOrder, # Maximum {0} pending iceberg orders can be held at the same time + '51263': InvalidOrder, # Maximum {0} pending time-weighted orders can be held at the same time + '51264': InvalidOrder, # Average amount exceeds the limit of per time-weighted order + '51265': InvalidOrder, # Time-weighted order limit error + '51267': InvalidOrder, # Time-weighted order strategy initiative rate error + '51268': InvalidOrder, # Time-weighted order strategy initiative range error + '51269': InvalidOrder, # Time-weighted order interval error, the interval should be {0}<= x<={1} + '51270': InvalidOrder, # The limit of time-weighted order price variance is 0 < x <= 1% + '51271': InvalidOrder, # Sweep ratio should be 0 < x <= 100% + '51272': InvalidOrder, # Price variance should be 0 < x <= 1% + '51273': InvalidOrder, # Total amount should be more than {0} + '51274': InvalidOrder, # Total quantity of time-weighted order must be larger than single order limit + '51275': InvalidOrder, # The amount of single stop-market order can not exceed the upper limit + '51276': InvalidOrder, # Stop - Market orders cannot specify a price + '51277': InvalidOrder, # TP trigger price can not be higher than the last price + '51278': InvalidOrder, # SL trigger price can not be lower than the last price + '51279': InvalidOrder, # TP trigger price can not be lower than the last price + '51280': InvalidOrder, # SL trigger price can not be higher than the last price + '51321': InvalidOrder, # You're leading trades. Currently, we don't support leading trades with arbitrage, iceberg, or TWAP bots + '51322': InvalidOrder, # You're leading trades that have been filled at market price. We've canceled your open stop orders to close your positions + '51323': BadRequest, # You're already leading trades with take profit or stop loss settings. Cancel your existing stop orders to proceed + '51324': BadRequest, # As a lead trader, you hold positions in {instrument}. To close your positions, place orders in the amount that equals the available amount for closing + '51325': InvalidOrder, # As a lead trader, you must use market price when placing stop orders + '51327': InvalidOrder, # closeFraction is only available for futures and perpetual swaps + '51328': InvalidOrder, # closeFraction is only available for reduceOnly orders + '51329': InvalidOrder, # closeFraction is only available in NET mode + '51330': InvalidOrder, # closeFraction is only available for stop market orders + '51400': OrderNotFound, # Cancellation failed order does not exist + '51401': OrderNotFound, # Cancellation failed order is already canceled + '51402': OrderNotFound, # Cancellation failed order is already completed + '51403': InvalidOrder, # Cancellation failed order type does not support cancellation + '51404': InvalidOrder, # Order cancellation unavailable during the second phase of call auction + '51405': ExchangeError, # Cancellation failed do not have any pending orders + '51406': ExchangeError, # Canceled - order count exceeds the limit {0} + '51407': BadRequest, # Either order ID or client order ID is required + '51408': ExchangeError, # Pair ID or name does not match the order info + '51409': ExchangeError, # Either pair ID or pair name ID is required + '51410': CancelPending, # Cancellation failed order is already under cancelling status + '51500': ExchangeError, # Either order price or amount is required + '51501': ExchangeError, # Maximum {0} orders can be modified + '51502': InsufficientFunds, # Order modification failed for insufficient margin or balance + '51503': ExchangeError, # Order modification failed order does not exist + '51506': ExchangeError, # Order modification unavailable for the order type + '51508': ExchangeError, # Orders are not allowed to be modified during the call auction + '51509': ExchangeError, # Modification failed order has been canceled + '51510': ExchangeError, # Modification failed order has been completed + '51511': ExchangeError, # Modification failed order price did not meet the requirement for Post Only + '51600': ExchangeError, # Status not found + '51601': ExchangeError, # Order status and order ID cannot exist at the same time + '51602': ExchangeError, # Either order status or order ID is required + '51603': OrderNotFound, # Order does not exist + '51732': AuthenticationError, # Required user KYC level not met + '51733': AuthenticationError, # User is under risk control + '51734': AuthenticationError, # User KYC Country is not supported + '51735': ExchangeError, # Sub-account is not supported + '51736': InsufficientFunds, # Insufficient {ccy} balance + # Data class + '52000': ExchangeError, # No updates + # SPOT/MARGIN error codes 54000-54999 + '54000': ExchangeError, # Margin transactions unavailable + '54001': ExchangeError, # Only Multi-currency margin account can be set to borrow coins automatically + '54008': InvalidOrder, # This operation is disabled by the 'mass cancel order' endpoint. Please enable it using self endpoint. + '54009': InvalidOrder, # The range of {param0} should be [{param1}, {param2}]. + '54011': InvalidOrder, # 200 Pre-market trading contracts are only allowed to reduce the number of positions within 1 hour before delivery. Please modify or cancel the order. + '54072': ExchangeError, # This contract is currently view-only and not tradable. + '54073': BadRequest, # Couldn’t place order, as {param0} is at risk of depegging. Switch settlement currencies and try again. + '54074': ExchangeError, # Your settings failed have positions, bot or open orders for USD contracts. + # Trading bot Error Code from 55100 to 55999 + '55100': InvalidOrder, # Take profit % should be within the range of {parameter1}-{parameter2} + '55101': InvalidOrder, # Stop loss % should be within the range of {parameter1}-{parameter2} + '55102': InvalidOrder, # Take profit % should be greater than the current bot’s PnL% + '55103': InvalidOrder, # Stop loss % should be less than the current bot’s PnL% + '55104': InvalidOrder, # Only futures grid supports take profit or stop loss based on profit percentage + '55111': InvalidOrder, # This signal name is in use, please try a new name + '55112': InvalidOrder, # This signal does not exist + '55113': InvalidOrder, # Create signal strategies with leverage greater than the maximum leverage of the instruments + # FUNDING error codes 58000-58999 + '58000': ExchangeError, # Account type {0} does not supported when getting the sub-account balance + '58001': AuthenticationError, # Incorrect trade password + '58002': PermissionDenied, # Please activate Savings Account first + '58003': ExchangeError, # Currency type is not supported by Savings Account + '58004': AccountSuspended, # Account blocked(transfer & withdrawal endpoint: either end of the account does not authorize the transfer) + '58005': ExchangeError, # The redeemed amount must be no greater than {0} + '58006': ExchangeError, # Service unavailable for token {0} + '58007': ExchangeError, # Abnormal Assets interface. Please try again later + '58100': ExchangeError, # The trading product triggers risk control, and the platform has suspended the fund transfer-out function with related users. Please wait patiently + '58101': AccountSuspended, # Transfer suspended(transfer endpoint: either end of the account does not authorize the transfer) + '58102': RateLimitExceeded, # Too frequent transfer(transfer too frequently) + '58103': ExchangeError, # Parent account user id does not match sub-account user id + '58104': ExchangeError, # Since your P2P transaction is abnormal, you are restricted from making fund transfers. Please contact customer support to remove the restriction + '58105': ExchangeError, # Since your P2P transaction is abnormal, you are restricted from making fund transfers. Please transfer funds on our website or app to complete identity verification + '58106': ExchangeError, # Please enable the account for spot contract + '58107': ExchangeError, # Please enable the account for futures contract + '58108': ExchangeError, # Please enable the account for option contract + '58109': ExchangeError, # Please enable the account for swap contract + '58110': ExchangeError, # The contract triggers risk control, and the platform has suspended the fund transfer function of it. Please wait patiently + '58111': ExchangeError, # Funds transfer unavailable perpetual contract is charging the funding fee. Please try again later + '58112': ExchangeError, # Your fund transfer failed. Please try again later + '58114': ExchangeError, # Transfer amount must be more than 0 + '58115': ExchangeError, # Sub-account does not exist + '58116': ExchangeError, # Transfer amount exceeds the limit + '58117': ExchangeError, # Account assets are abnormal, please deal with negative assets before transferring + '58125': BadRequest, # Non-tradable assets can only be transferred from sub-accounts to main accounts + '58126': BadRequest, # Non-tradable assets can only be transferred between funding accounts + '58127': BadRequest, # Main account API Key does not support current transfer 'type' parameter. Please refer to the API documentation. + '58128': BadRequest, # Sub-account API Key does not support current transfer 'type' parameter. Please refer to the API documentation. + '58200': ExchangeError, # Withdrawal from {0} to {1} is unavailable for self currency + '58201': ExchangeError, # Withdrawal amount exceeds the daily limit + '58202': ExchangeError, # The minimum withdrawal amount for NEO is 1, and the amount must be an integer + '58203': InvalidAddress, # Please add a withdrawal address + '58204': AccountSuspended, # Withdrawal suspended + '58205': ExchangeError, # Withdrawal amount exceeds the upper limit + '58206': ExchangeError, # Withdrawal amount is lower than the lower limit + '58207': InvalidAddress, # Withdrawal failed due to address error + '58208': ExchangeError, # Withdrawal failed. Please link your email + '58209': ExchangeError, # Withdrawal failed. Withdraw feature is not available for sub-accounts + '58210': ExchangeError, # Withdrawal fee exceeds the upper limit + '58211': ExchangeError, # Withdrawal fee is lower than the lower limit(withdrawal endpoint: incorrect fee) + '58212': ExchangeError, # Withdrawal fee should be {0}% of the withdrawal amount + '58213': AuthenticationError, # Please set trading password before withdrawal + '58221': BadRequest, # Missing label of withdrawal address. + '58222': BadRequest, # Illegal withdrawal address. + '58224': BadRequest, # This type of crypto does not support on-chain withdrawing to OKX addresses. Please withdraw through internal transfers. + '58227': BadRequest, # Withdrawal of non-tradable assets can be withdrawn all at once only + '58228': BadRequest, # Withdrawal of non-tradable assets requires that the API Key must be bound to an IP + '58229': InsufficientFunds, # Insufficient funding account balance to pay fees {fee} USDT + '58300': ExchangeError, # Deposit-address count exceeds the limit + '58350': InsufficientFunds, # Insufficient balance + # Account error codes 59000-59999 + '59000': ExchangeError, # Your settings failed have positions or open orders + '59001': ExchangeError, # Switching unavailable have borrowings + '59100': ExchangeError, # You have open positions. Please cancel all open positions before changing the leverage + '59101': ExchangeError, # You have pending orders with isolated positions. Please cancel all the pending orders and adjust the leverage + '59102': ExchangeError, # Leverage exceeds the maximum leverage. Please adjust the leverage + '59103': InsufficientFunds, # Leverage is too low and no sufficient margin in your account. Please adjust the leverage + '59104': ExchangeError, # The leverage is too high. The borrowed position has exceeded the maximum position of self leverage. Please adjust the leverage + '59105': ExchangeError, # Leverage can not be less than {0}. Please adjust the leverage + '59106': ExchangeError, # The max available margin corresponding to your order tier is {0}. Please adjust your margin and place a new order + '59107': ExchangeError, # You have pending orders under the service, please modify the leverage after canceling all pending orders + '59108': InsufficientFunds, # Low leverage and insufficient margin, please adjust the leverage + '59109': ExchangeError, # Account equity less than the required margin amount after adjustment. Please adjust the leverage + '59128': InvalidOrder, # As a lead trader, you can't lead trades in {instrument} with leverage higher than {num} + '59200': InsufficientFunds, # Insufficient account balance + '59201': InsufficientFunds, # Negative account balance + '59216': BadRequest, # The position doesn't exist. Please try again + '59260': PermissionDenied, # You are not a spot lead trader yet. Complete the application on our website or app first. + '59262': PermissionDenied, # You are not a contract lead trader yet. Complete the application on our website or app first. + '59300': ExchangeError, # Margin call failed. Position does not exist + '59301': ExchangeError, # Margin adjustment failed for exceeding the max limit + '59313': ExchangeError, # Unable to repay. You haven't borrowed any {ccy} {ccyPair} in Quick margin mode. + '59401': ExchangeError, # Holdings already reached the limit + '59410': OperationRejected, # You can only borrow self crypto if it supports borrowing and borrowing is enabled. + '59411': InsufficientFunds, # Manual borrowing failed. Your account's free margin is insufficient + '59412': OperationRejected, # Manual borrowing failed. The amount exceeds your borrowing limit. + '59413': OperationRejected, # You didn't borrow self crypto. No repayment needed. + '59414': BadRequest, # Manual borrowing failed. The minimum borrowing limit is {param0}.needed. + '59500': ExchangeError, # Only the APIKey of the main account has permission + '59501': ExchangeError, # Only 50 APIKeys can be created per account + '59502': ExchangeError, # Note name cannot be duplicate with the currently created APIKey note name + '59503': ExchangeError, # Each APIKey can bind up to 20 IP addresses + '59504': ExchangeError, # The sub account does not support the withdrawal function + '59505': ExchangeError, # The passphrase format is incorrect + '59506': ExchangeError, # APIKey does not exist + '59507': ExchangeError, # The two accounts involved in a transfer must be two different sub accounts under the same parent account + '59508': AccountSuspended, # The sub account of {0} is suspended + '59515': ExchangeError, # You are currently not on the custody whitelist. Please contact customer service for assistance. + '59516': ExchangeError, # Please create the Copper custody funding account first. + '59517': ExchangeError, # Please create the Komainu custody funding account first. + '59518': ExchangeError, # You can’t create a sub-account using the API; please use the app or web. + '59519': ExchangeError, # You can’t use self function/feature while it's frozen, due to: {freezereason} + '59642': BadRequest, # Lead and copy traders can only use margin-free or single-currency margin account modes + '59643': ExchangeError, # Couldn’t switch account modes’re currently copying spot trades + '59683': ExchangeError, # Set self crypto collateral crypto before selecting it settlement currency. + '59684': BadRequest, # Borrowing isn’t supported for self currency. + '59686': BadRequest, # This crypto can’t be set settlement currency. + # WebSocket error Codes from 60000-63999 + '60001': AuthenticationError, # "OK_ACCESS_KEY" can not be empty + '60002': AuthenticationError, # "OK_ACCESS_SIGN" can not be empty + '60003': AuthenticationError, # "OK_ACCESS_PASSPHRASE" can not be empty + '60004': AuthenticationError, # Invalid OK_ACCESS_TIMESTAMP + '60005': AuthenticationError, # Invalid OK_ACCESS_KEY + '60006': InvalidNonce, # Timestamp request expired + '60007': AuthenticationError, # Invalid sign + '60008': AuthenticationError, # Login is not supported for public channels + '60009': AuthenticationError, # Login failed + '60010': AuthenticationError, # Already logged in + '60011': AuthenticationError, # Please log in + '60012': BadRequest, # Illegal request + '60013': BadRequest, # Invalid args + '60014': RateLimitExceeded, # Requests too frequent + '60015': NetworkError, # Connection closed was no data transmission in the last 30 seconds + '60016': ExchangeNotAvailable, # Buffer is full, cannot write data + '60017': BadRequest, # Invalid url path + '60018': BadRequest, # The {0} {1} {2} {3} {4} does not exist + '60019': BadRequest, # Invalid op {op} + '60020': ExchangeError, # APIKey subscription amount exceeds the limit + '60021': AccountNotEnabled, # This operation does not support multiple accounts login + '60022': AuthenticationError, # Bulk login partially succeeded + '60023': DDoSProtection, # Bulk login requests too frequent + '60024': AuthenticationError, # Wrong passphrase + '60025': ExchangeError, # Token subscription amount exceeds the limit + '60026': AuthenticationError, # Batch login by APIKey and token simultaneously is not supported + '60027': ArgumentsRequired, # Parameter {0} can not be empty + '60028': NotSupported, # The current operation is not supported by self URL + '60029': AccountNotEnabled, # Only users who are VIP5 and above in trading fee tier are allowed to subscribe to books-l2-tbt channel + '60030': AccountNotEnabled, # Only users who are VIP4 and above in trading fee tier are allowed to subscribe to books50-l2-tbt channel + '60031': AuthenticationError, # The WebSocket endpoint does not support multiple account batch login, + '60032': AuthenticationError, # API key doesn't exist, + '63999': ExchangeError, # Internal system error + '64000': BadRequest, # Subscription parameter uly is unavailable anymore, please replace uly with instFamily. More details can refer to: https://www.okx.com/help-center/changes-to-v5-api-websocket-subscription-parameter-and-url, + '64001': BadRequest, # This channel has been migrated to the business URL. Please subscribe using the new URL. More details can refer to: https://www.okx.com/help-center/changes-to-v5-api-websocket-subscription-parameter-and-url, + '64002': BadRequest, # This channel is not supported by business URL. Please use "/private" URL(for private channels), or "/public" URL(for public channels). More details can refer to: https://www.okx.com/help-center/changes-to-v5-api-websocket-subscription-parameter-and-url, + '64003': AccountNotEnabled, # Your trading fee tier doesnt meet the requirement to access self channel + '70010': BadRequest, # Timestamp parameters need to be in Unix timestamp format in milliseconds. + '70013': BadRequest, # endTs needs to be bigger than or equal to beginTs. + '70016': BadRequest, # Please specify your instrument settings for at least one instType. + '70060': BadRequest, # The account doesn’t exist or the position side is incorrect. To and from accounts must be under the same main account. + '70061': BadRequest, # To move position, please enter a position that’s opposite to your current side and is smaller than or equal to your current size. + '70062': BadRequest, # account has reached the maximum number of position transfers allowed per day. + '70064': BadRequest, # Position does not exist. + '70065': BadRequest, # Couldn’t move position. Execution price cannot be determined + '70066': BadRequest, # Moving positions isn't supported in spot mode. Switch to any other account mode and try again. + '70067': BadRequest, # Moving positions isn't supported in margin trading. + '1009': BadRequest, # Request message exceeds the maximum frame length + '4001': AuthenticationError, # Login Failed + '4002': BadRequest, # Invalid Request + '4003': RateLimitExceeded, # APIKey subscription amount exceeds the limit 100 + '4004': NetworkError, # No data received in 30s + '4005': ExchangeNotAvailable, # Buffer is full, cannot write data + '4006': BadRequest, # Abnormal disconnection + '4007': AuthenticationError, # API key has been updated or deleted. Please reconnect. + '4008': RateLimitExceeded, # The number of subscribed channels exceeds the maximum limit. + }, + 'broad': { + 'Internal Server Error': ExchangeNotAvailable, # {"code":500,"data":{},"detailMsg":"","error_code":"500","error_message":"Internal Server Error","msg":"Internal Server Error"} + 'server error': ExchangeNotAvailable, # {"code":500,"data":{},"detailMsg":"","error_code":"500","error_message":"server error 1236805249","msg":"server error 1236805249"} + }, + }, + 'httpExceptions': { + '429': ExchangeNotAvailable, # https://github.com/ccxt/ccxt/issues/9612 + }, + 'precisionMode': TICK_SIZE, + 'options': { + 'sandboxMode': False, + 'defaultNetwork': 'ERC20', + 'defaultNetworks': { + 'ETH': 'ERC20', + 'BTC': 'BTC', + 'USDT': 'TRC20', + }, + 'networks': { + 'BTC': 'Bitcoin', + 'BTCLN': 'Lightning', + 'BTCLIGHTNING': 'Lightning', + 'BEP20': 'BSC', + 'BRC20': 'BRC20', + 'ERC20': 'ERC20', + 'TRC20': 'TRC20', + 'CRC20': 'Crypto', + 'ACA': 'Acala', + 'ALGO': 'Algorand', + 'APT': 'Aptos', + 'SCROLL': 'Scroll', + 'ARBONE': 'Arbitrum One', + 'AVAXC': 'Avalanche C-Chain', + 'AVAXX': 'Avalanche X-Chain', + 'BASE': 'Base', + 'SUI': 'SUI', + 'ZKSYNCERA': 'zkSync Era', + 'LINEA': 'Linea', + 'AR': 'Arweave', + 'ASTR': 'Astar', + 'BCH': 'BitcoinCash', + 'BSV': 'Bitcoin SV', + 'ADA': 'Cardano', + 'CSPR': 'Casper', + 'CELO': 'CELO', + 'XCH': 'Chia', + # 'CHZ': 'Chiliz', TBD: Chiliz 2.0 Chain vs Chiliz Chain + 'ATOM': 'Cosmos', + 'DGB': 'Digibyte', + 'DOGE': 'Dogecoin', + 'EGLD': 'Elrond', + 'CFX': 'Conflux', # CFX_EVM is different + 'EOS': 'EOS', + 'CORE': 'CORE', + 'ETC': 'Ethereum Classic', + 'ETHW': 'EthereumPow', + # 'FTM': 'Fantom', 'Sonic' TBD + 'FIL': 'Filecoin', + 'ONE': 'Harmony', + 'HBAR': 'Hedera', + 'ICX': 'ICON', + 'ICP': 'Dfinity', + 'IOST': 'IOST', + 'IOTA': 'MIOTA', + 'KLAY': 'Klaytn', + 'KSM': 'Kusama', + 'LSK': 'Lisk', + 'LTC': 'Litecoin', + 'METIS': 'Metis', + 'MINA': 'Mina', + 'GLRM': 'Moonbeam', + 'MOVR': 'Moonriver', + 'NANO': 'Nano', + 'NEAR': 'NEAR', + 'NULS': 'NULS', + 'OASYS': 'OASYS', + 'ONT': 'Ontology', + 'OPTIMISM': 'Optimism', + # 'OP': 'Optimism', or Optimism(V2), TBD + 'LAT': 'PlatON', + 'DOT': 'Polkadot', + 'MATIC': 'Polygon', + 'RVN': 'Ravencoin', + 'XRP': 'Ripple', + 'SC': 'Siacoin', + 'SOL': 'Solana', + 'STX': 'l-Stacks', + 'XLM': 'Stellar Lumens', + 'XTZ': 'Tezos', + 'TON': 'TON', + 'THETA': 'Theta', + 'WAX': 'Wax', + 'ZIL': 'Zilliqa', + # non-supported known network: CRP. KAVA, TAIKO, BOB, GNO, BLAST, RSK, SEI, MANTLE, HYPE, RUNE, OSMO, XIN, WEMIX, HT, FSN, NEO, TLOS, CANTO, SCRT, AURORA, XMR + # others: + # "OKTC", + # "X Layer", + # "Polygon(Bridged)", + # "BTCK-OKTC", + # "ETHK-OKTC", + # "Starknet", + # "LTCK-OKTC", + # "XRPK-OKTC", + # "BCHK-OKTC", + # "ETCK-OKTC", + # "Endurance Smart Chain", + # "Berachain", + # "CELO-TOKEN", + # "CFX_EVM", + # "Cortex", + # "DAIK-OKTC", + # "Dora Vota Mainnet", + # "DOTK-OKTC", + # "DYDX", + # "AELF", + # "Enjin Relay Chain", + # "FEVM", + # "FILK-OKTC", + # "Flare", + # "Gravity Alpha Mainnet", + # "INJ", + # "Story", + # "LINKK-OKTC", + # "Terra", + # "Terra Classic", + # "Terra Classic(USTC)", + # "MERLIN Network", + # "Layer 3", + # "PI", + # "Ronin", + # "Quantum", + # "SHIBK-OKTC", + # "SUSHIK-OKTC", + # "Celestia", + # "TRXK-OKTC", + # "UNIK-OKTC", + # "Venom", + # "WBTCK-OKTC", + # "ZetaChain", + }, + 'fetchOpenInterestHistory': { + 'timeframes': { + '5m': '5m', + '1h': '1H', + '8h': '8H', + '1d': '1D', + '5M': '5m', + '1H': '1H', + '8H': '8H', + '1D': '1D', + }, + }, + 'fetchOHLCV': { + # 'type': 'Candles', # Candles or HistoryCandles, IndexCandles, MarkPriceCandles + 'timezone': 'UTC', # UTC, HK + }, + 'fetchPositions': { + 'method': 'privateGetAccountPositions', # privateGetAccountPositions or privateGetAccountPositionsHistory + }, + 'createOrder': 'privatePostTradeBatchOrders', # or 'privatePostTradeOrder' or 'privatePostTradeOrderAlgo' + 'createMarketBuyOrderRequiresPrice': False, + 'fetchMarkets': { + 'types': ['spot', 'future', 'swap', 'option'], # spot, future, swap, option + }, + 'timeDifference': 0, # the difference between system clock and exchange server clock + 'adjustForTimeDifference': False, # controls the adjustment logic upon instantiation + 'defaultType': 'spot', # 'funding', 'spot', 'margin', 'future', 'swap', 'option' + # 'fetchBalance': { + # 'type': 'spot', # 'funding', 'trading', 'spot' + # }, + 'fetchLedger': { + 'method': 'privateGetAccountBills', # privateGetAccountBills, privateGetAccountBillsArchive, privateGetAssetBills + }, + # 6: Funding account, 18: Trading account + 'fetchOrder': { + 'method': 'privateGetTradeOrder', # privateGetTradeOrdersAlgoHistory + }, + 'fetchOpenOrders': { + 'method': 'privateGetTradeOrdersPending', # privateGetTradeOrdersAlgoPending + }, + 'cancelOrders': { + 'method': 'privatePostTradeCancelBatchOrders', # privatePostTradeCancelAlgos + }, + 'fetchCanceledOrders': { + 'method': 'privateGetTradeOrdersHistory', # privateGetTradeOrdersAlgoHistory + }, + 'fetchClosedOrders': { + 'method': 'privateGetTradeOrdersHistory', # privateGetTradeOrdersAlgoHistory + }, + 'withdraw': { + # a funding password credential is required by the exchange for the + # withdraw call(not to be confused with the api password credential) + 'password': None, + 'pwd': None, # password or pwd both work + }, + 'algoOrderTypes': { + 'conditional': True, + 'trigger': True, + 'oco': True, + 'move_order_stop': True, + 'iceberg': True, + 'twap': True, + }, + 'accountsByType': { + 'funding': '6', + 'trading': '18', # unified trading account + 'spot': '18', + 'future': '18', + 'futures': '18', + 'margin': '18', + 'swap': '18', + 'option': '18', + }, + 'accountsById': { + '6': 'funding', + '18': 'trading', # unified trading account + }, + 'exchangeType': { + 'spot': 'SPOT', + 'margin': 'MARGIN', + 'swap': 'SWAP', + 'future': 'FUTURES', + 'futures': 'FUTURES', # deprecated + 'option': 'OPTION', + 'SPOT': 'SPOT', + 'MARGIN': 'MARGIN', + 'SWAP': 'SWAP', + 'FUTURES': 'FUTURES', + 'OPTION': 'OPTION', + }, + 'brokerId': '6b9ad766b55dBCDE', + }, + 'features': { + 'default': { + 'sandbox': True, + 'createOrder': { + 'marginMode': True, + 'triggerPrice': True, + 'triggerPriceType': { + 'last': True, + 'mark': True, + 'index': True, + }, + 'triggerDirection': False, + 'stopLossPrice': True, + 'takeProfitPrice': True, + 'attachedStopLossTakeProfit': { + 'triggerPriceType': { + 'last': True, + 'mark': True, + 'index': True, + }, + 'price': True, + }, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': True, + 'trailing': True, + 'iceberg': True, # todo implement + 'leverage': False, + 'selfTradePrevention': True, # todo implement + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': False, + }, + 'createOrders': { + 'max': 20, + }, + 'fetchMyTrades': { + 'marginMode': False, + 'daysBack': 90, + 'limit': 100, + 'untilDays': 10000, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': True, + 'trailing': True, + 'symbolRequired': True, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 100, + 'trigger': True, + 'trailing': True, + 'symbolRequired': False, + }, + 'fetchOrders': None, # not supported + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 90, # 3 months + 'daysBackCanceled': 1 / 12, # 2 hour + 'untilDays': None, + 'trigger': True, + 'trailing': True, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 300, # regular candles(recent & historical) both have 300 max + 'mark': 100, + 'index': 100, + }, + }, + 'spot': { + 'extends': 'default', + 'fetchCurrencies': { + 'private': True, + }, + }, + 'swap': { + 'linear': { + 'extends': 'default', + }, + 'inverse': { + 'extends': 'default', + }, + }, + 'future': { + 'linear': { + 'extends': 'default', + }, + 'inverse': { + 'extends': 'default', + }, + }, + }, + 'currencies': { + 'USD': self.safe_currency_structure({'id': 'USD', 'code': 'USD', 'precision': self.parse_number('0.0001')}), + 'EUR': self.safe_currency_structure({'id': 'EUR', 'code': 'EUR', 'precision': self.parse_number('0.0001')}), + 'AED': self.safe_currency_structure({'id': 'AED', 'code': 'AED', 'precision': self.parse_number('0.0001')}), + 'GBP': self.safe_currency_structure({'id': 'GBP', 'code': 'GBP', 'precision': self.parse_number('0.0001')}), + 'AUD': self.safe_currency_structure({'id': 'AUD', 'code': 'AUD', 'precision': self.parse_number('0.0001')}), + }, + 'commonCurrencies': { + # the exchange refers to ERC20 version of Aeternity(AEToken) + 'AE': 'AET', # https://github.com/ccxt/ccxt/issues/4981 + 'WIN': 'WINTOKEN', # https://github.com/ccxt/ccxt/issues/5701 + }, + }) + + def handle_market_type_and_params(self, methodName: str, market: Market = None, params={}, defaultValue=None) -> Any: + instType = self.safe_string(params, 'instType') + params = self.omit(params, 'instType') + type = self.safe_string(params, 'type') + if (type is None) and (instType is not None): + params['type'] = instType + return super(okx, self).handle_market_type_and_params(methodName, market, params, defaultValue) + + def convert_to_instrument_type(self, type): + exchangeTypes = self.safe_dict(self.options, 'exchangeType', {}) + return self.safe_string(exchangeTypes, type, type) + + def create_expired_option_market(self, symbol: str): + # support expired option contracts + quote = 'USD' + optionParts = symbol.split('-') + symbolBase = symbol.split('/') + base = None + if symbol.find('/') > -1: + base = self.safe_string(symbolBase, 0) + else: + base = self.safe_string(optionParts, 0) + settle = base + expiry = self.safe_string(optionParts, 2) + strike = self.safe_string(optionParts, 3) + optionType = self.safe_string(optionParts, 4) + datetime = self.convert_expire_date(expiry) + timestamp = self.parse8601(datetime) + return { + 'id': base + '-' + quote + '-' + expiry + '-' + strike + '-' + optionType, + 'symbol': base + '/' + quote + ':' + settle + '-' + expiry + '-' + strike + '-' + optionType, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': base, + 'quoteId': quote, + 'settleId': settle, + 'active': False, + 'type': 'option', + 'linear': None, + 'inverse': None, + 'spot': False, + 'swap': False, + 'future': False, + 'option': True, + 'margin': False, + 'contract': True, + 'contractSize': self.parse_number('1'), + 'expiry': timestamp, + 'expiryDatetime': datetime, + 'optionType': 'call' if (optionType == 'C') else 'put', + 'strike': self.parse_number(strike), + 'precision': { + 'amount': None, + 'price': None, + }, + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'info': None, + } + + def safe_market(self, marketId: Str = None, market: Market = None, delimiter: Str = None, marketType: Str = None) -> MarketInterface: + isOption = (marketId is not None) and ((marketId.find('-C') > -1) or (marketId.find('-P') > -1)) + if isOption and not (marketId in self.markets_by_id): + # handle expired option contracts + return self.create_expired_option_market(marketId) + return super(okx, self).safe_market(marketId, market, delimiter, marketType) + + async def fetch_status(self, params={}): + """ + the latest known information on the availability of the exchange API + + https://www.okx.com/docs-v5/en/#status-get-status + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `status structure ` + """ + response = await self.publicGetSystemStatus(params) + # + # Note, if there is no maintenance around, the 'data' array is empty + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "begin": "1621328400000", + # "end": "1621329000000", + # "href": "https://www.okx.com/support/hc/en-us/articles/360060882172", + # "scheDesc": "", + # "serviceType": "1", # 0 WebSocket, 1 Spot/Margin, 2 Futures, 3 Perpetual, 4 Options, 5 Trading service + # "state": "scheduled", # ongoing, completed, canceled + # "system": "classic", # classic, unified + # "title": "Classic Spot System Upgrade" + # }, + # ] + # } + # + data = self.safe_list(response, 'data', []) + dataLength = len(data) + update: dict = { + 'updated': None, + 'status': 'ok' if (dataLength == 0) else 'maintenance', + 'eta': None, + 'url': None, + 'info': response, + } + for i in range(0, len(data)): + event = data[i] + state = self.safe_string(event, 'state') + update['eta'] = self.safe_integer(event, 'end') + update['url'] = self.safe_string(event, 'href') + if state == 'ongoing': + update['status'] = 'maintenance' + elif state == 'scheduled': + update['status'] = 'ok' + elif state == 'completed': + update['status'] = 'ok' + elif state == 'canceled': + update['status'] = 'ok' + return update + + async def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://www.okx.com/docs-v5/en/#public-data-rest-api-get-system-time + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = await self.publicGetPublicTime(params) + # + # { + # "code": "0", + # "data": [ + # {"ts": "1621247923668"} + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + first = self.safe_dict(data, 0, {}) + return self.safe_integer(first, 'ts') + + async def fetch_accounts(self, params={}) -> List[Account]: + """ + fetch all the accounts associated with a profile + + https://www.okx.com/docs-v5/en/#trading-account-rest-api-get-account-configuration + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `account structures ` indexed by the account type + """ + response = await self.privateGetAccountConfig(params) + # + # { + # "code": "0", + # "data": [ + # { + # "acctLv": "2", + # "acctStpMode": "cancel_maker", + # "autoLoan": False, + # "ctIsoMode": "automatic", + # "enableSpotBorrow": False, + # "greeksType": "PA", + # "feeType": "0", + # "ip": "", + # "type": "0", + # "kycLv": "3", + # "label": "v5 test", + # "level": "Lv1", + # "levelTmp": "", + # "liquidationGear": "-1", + # "mainUid": "44705892343619584", + # "mgnIsoMode": "automatic", + # "opAuth": "1", + # "perm": "read_only,withdraw,trade", + # "posMode": "long_short_mode", + # "roleType": "0", + # "spotBorrowAutoRepay": False, + # "spotOffsetType": "", + # "spotRoleType": "0", + # "spotTraderInsts": [], + # "traderInsts": [], + # "uid": "44705892343619584", + # "settleCcy": "USDT", + # "settleCcyList": ["USD", "USDC", "USDG"], + # } + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + result = [] + for i in range(0, len(data)): + account = data[i] + accountId = self.safe_string(account, 'uid') + type = self.safe_string(account, 'acctLv') + result.append({ + 'id': accountId, + 'type': type, + 'currency': None, + 'info': account, + 'code': None, + }) + return result + + def nonce(self): + return self.milliseconds() - self.options['timeDifference'] + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for okx + + https://www.okx.com/docs-v5/en/#rest-api-public-data-get-instruments + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + if self.options['adjustForTimeDifference']: + await self.load_time_difference() + types = ['spot', 'future', 'swap', 'option'] + fetchMarketsOption = self.safe_dict(self.options, 'fetchMarkets') + if fetchMarketsOption is not None: + types = self.safe_list(fetchMarketsOption, 'types', types) + else: + types = self.safe_list(self.options, 'fetchMarkets', types) # backward-support + promises = [] + result = [] + for i in range(0, len(types)): + promises.append(self.fetch_markets_by_type(types[i], params)) + promises = await asyncio.gather(*promises) + for i in range(0, len(promises)): + result = self.array_concat(result, promises[i]) + return result + + def parse_market(self, market: dict) -> Market: + # + # { + # "alias": "", # self_week, next_week, quarter, next_quarter + # "baseCcy": "BTC", + # "category": "1", + # "ctMult": "", + # "ctType": "", # inverse, linear + # "ctVal": "", + # "ctValCcy": "", + # "expTime": "", + # "instId": "BTC-USDT", # BTC-USD-210521, CSPR-USDT-SWAP, BTC-USD-210517-44000-C + # "instType": "SPOT", # SPOT, FUTURES, SWAP, OPTION + # "lever": "10", + # "listTime": "1548133413000", + # "lotSz": "0.00000001", + # "minSz": "0.00001", + # "optType": "", + # "quoteCcy": "USDT", + # "settleCcy": "", + # "state": "live", + # "stk": "", + # "tickSz": "0.1", + # "uly": "" + # } + # + # { + # "alias": "", + # "baseCcy": "", + # "category": "1", + # "ctMult": "0.1", + # "ctType": "", + # "ctVal": "1", + # "ctValCcy": "BTC", + # "expTime": "1648195200000", + # "instId": "BTC-USD-220325-194000-P", + # "instType": "OPTION", + # "lever": "", + # "listTime": "1631262612280", + # "contTdSwTime": "1631262812280", + # "lotSz": "1", + # "minSz": "1", + # "optType": "P", + # "quoteCcy": "", + # "settleCcy": "BTC", + # "state": "live", + # "stk": "194000", + # "tickSz": "0.0005", + # "uly": "BTC-USD" + # } + # + # for swap "preopen" markets, only `instId` and `instType` are present + # + # instId: "ETH-USD_UM-SWAP", + # instType: "SWAP", + # state: "preopen", + # + id = self.safe_string(market, 'instId') + type = self.safe_string_lower(market, 'instType') + if type == 'futures': + type = 'future' + spot = (type == 'spot') + future = (type == 'future') + swap = (type == 'swap') + option = (type == 'option') + contract = swap or future or option + baseId = self.safe_string(market, 'baseCcy', '') # defaulting to '' because some weird preopen markets have empty baseId + quoteId = self.safe_string(market, 'quoteCcy', '') + settleId = self.safe_string(market, 'settleCcy') + settle = self.safe_currency_code(settleId) + underlying = self.safe_string(market, 'uly') + if (underlying is not None) and not spot: + parts = underlying.split('-') + baseId = self.safe_string(parts, 0) + quoteId = self.safe_string(parts, 1) + if ((baseId == '') or (quoteId == '')) and spot: # to fix weird preopen markets + instId = self.safe_string(market, 'instId', '') + parts = instId.split('-') + baseId = self.safe_string(parts, 0) + quoteId = self.safe_string(parts, 1) + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + symbol = base + '/' + quote + # handle preopen empty markets + if base == '' or quote == '': + symbol = id + expiry = None + strikePrice = None + optionType = None + if contract: + if settle is not None: + symbol = symbol + ':' + settle + if future: + expiry = self.safe_integer(market, 'expTime') + if expiry is not None: + ymd = self.yymmdd(expiry) + symbol = symbol + '-' + ymd + elif option: + expiry = self.safe_integer(market, 'expTime') + strikePrice = self.safe_string(market, 'stk') + optionType = self.safe_string(market, 'optType') + if expiry is not None: + ymd = self.yymmdd(expiry) + symbol = symbol + '-' + ymd + '-' + strikePrice + '-' + optionType + optionType = 'put' if (optionType == 'P') else 'call' + fees = self.safe_dict_2(self.fees, type, 'trading', {}) + maxLeverage = self.safe_string(market, 'lever', '1') + maxLeverage = Precise.string_max(maxLeverage, '1') + maxSpotCost = self.safe_number(market, 'maxMktSz') + status = self.safe_string(market, 'state') + return self.extend(fees, { + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': type, + 'spot': spot, + 'margin': spot and (Precise.string_gt(maxLeverage, '1')), + 'swap': swap, + 'future': future, + 'option': option, + 'active': status == 'live', + 'contract': contract, + 'linear': (quoteId == settleId) if contract else None, + 'inverse': (baseId == settleId) if contract else None, + 'contractSize': self.safe_number(market, 'ctVal') if contract else None, + 'expiry': expiry, + 'expiryDatetime': self.iso8601(expiry), + 'strike': self.parse_number(strikePrice), + 'optionType': optionType, + 'created': self.safe_integer_2(market, 'contTdSwTime', 'listTime'), # contTdSwTime is public trading start time, while listTime considers pre-trading too + 'precision': { + 'amount': self.safe_number(market, 'lotSz'), + 'price': self.safe_number(market, 'tickSz'), + }, + 'limits': { + 'leverage': { + 'min': self.parse_number('1'), + 'max': self.parse_number(maxLeverage), + }, + 'amount': { + 'min': self.safe_number(market, 'minSz'), + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None if contract else maxSpotCost, + }, + }, + 'info': market, + }) + + async def fetch_markets_by_type(self, type, params={}): + request: dict = { + 'instType': self.convert_to_instrument_type(type), + } + if type == 'option': + optionsUnderlying = self.safe_list(self.options, 'defaultUnderlying', ['BTC-USD', 'ETH-USD']) + promises = [] + for i in range(0, len(optionsUnderlying)): + underlying = optionsUnderlying[i] + request['uly'] = underlying + promises.append(self.publicGetPublicInstruments(self.extend(request, params))) + promisesResult = await asyncio.gather(*promises) + markets = [] + for i in range(0, len(promisesResult)): + res = self.safe_dict(promisesResult, i, {}) + options = self.safe_list(res, 'data', []) + markets = self.array_concat(markets, options) + return self.parse_markets(markets) + response = await self.publicGetPublicInstruments(self.extend(request, params)) + # + # spot, future, swap, option + # + # { + # "code": "0", + # "data": [ + # { + # "alias": "", # self_week, next_week, quarter, next_quarter + # "baseCcy": "BTC", + # "category": "1", + # "ctMult": "", + # "ctType": "", # inverse, linear + # "ctVal": "", + # "ctValCcy": "", + # "expTime": "", + # "instId": "BTC-USDT", # BTC-USD-210521, CSPR-USDT-SWAP, BTC-USD-210517-44000-C + # "instType": "SPOT", # SPOT, FUTURES, SWAP, OPTION + # "lever": "10", + # "listTime": "1548133413000", + # "lotSz": "0.00000001", + # "minSz": "0.00001", + # "optType": "", + # "quoteCcy": "USDT", + # "settleCcy": "", + # "state": "live", + # "stk": "", + # "tickSz": "0.1", + # "uly": "" + # } + # ], + # "msg": "" + # } + # + dataResponse = self.safe_list(response, 'data', []) + return self.parse_markets(dataResponse) + + async def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://www.okx.com/docs-v5/en/#rest-api-funding-get-currencies + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + # self endpoint requires authentication + # while fetchCurrencies is a public API method by design + # therefore we check the keys here + # and fallback to generating the currencies from the markets + isSandboxMode = self.safe_bool(self.options, 'sandboxMode', False) + if not self.check_required_credentials(False) or isSandboxMode: + return {} + # + # has['fetchCurrencies'] is currently set to True, but an unauthorized request returns + # + # {"msg":"Request header “OK_ACCESS_KEY“ can't be empty.","code":"50103"} + # + response = await self.privateGetAssetCurrencies(params) + # + # { + # "code": "0", + # "data": [ + # { + # "canDep": True, + # "canInternal": False, + # "canWd": True, + # "ccy": "USDT", + # "chain": "USDT-TRC20", + # "logoLink": "https://static.coinall.ltd/cdn/assets/imgs/221/5F74EB20302D7761.png", + # "mainNet": False, + # "maxFee": "1.6", + # "maxWd": "8852150", + # "minFee": "0.8", + # "minWd": "2", + # "name": "Tether", + # "usedWdQuota": "0", + # "wdQuota": "500", + # "wdTickSz": "3" + # }, + # { + # "canDep": True, + # "canInternal": False, + # "canWd": True, + # "ccy": "USDT", + # "chain": "USDT-ERC20", + # "logoLink": "https://static.coinall.ltd/cdn/assets/imgs/221/5F74EB20302D7761.png", + # "mainNet": False, + # "maxFee": "16", + # "maxWd": "8852150", + # "minFee": "8", + # "minWd": "2", + # "name": "Tether", + # "usedWdQuota": "0", + # "wdQuota": "500", + # "wdTickSz": "3" + # }, + # ... + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + result: dict = {} + dataByCurrencyId = self.group_by(data, 'ccy') + currencyIds = list(dataByCurrencyId.keys()) + for i in range(0, len(currencyIds)): + currencyId = currencyIds[i] + currency = self.safe_currency(currencyId) + code = currency['code'] + chains = dataByCurrencyId[currencyId] + networks: dict = {} + type = 'crypto' + chainsLength = len(chains) + for j in range(0, chainsLength): + chain = chains[j] + # allow empty string for rare fiat-currencies, e.g. TRY + networkId = self.safe_string(chain, 'chain', '') # USDT-BEP20, USDT-Avalance-C, etc + if networkId == '': + # only happens for fiat 'TRY' currency + type = 'fiat' + idParts = networkId.split('-') + parts = self.array_slice(idParts, 1) + chainPart = '-'.join(parts) + networkCode = self.network_id_to_code(chainPart, currency['code']) + networks[networkCode] = { + 'id': networkId, + 'network': networkCode, + 'active': None, + 'deposit': self.safe_bool(chain, 'canDep'), + 'withdraw': self.safe_bool(chain, 'canWd'), + 'fee': self.safe_number(chain, 'fee'), + 'precision': self.parse_number(self.parse_precision(self.safe_string(chain, 'wdTickSz'))), + 'limits': { + 'withdraw': { + 'min': self.safe_number(chain, 'minWd'), + 'max': self.safe_number(chain, 'maxWd'), + }, + }, + 'info': chain, + } + firstChain = self.safe_dict(chains, 0, {}) + result[code] = self.safe_currency_structure({ + 'info': chains, + 'code': code, + 'id': currencyId, + 'name': self.safe_string(firstChain, 'name'), + 'active': None, + 'deposit': None, + 'withdraw': None, + 'fee': None, + 'precision': None, + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + }, + 'type': type, + 'networks': networks, + }) + return result + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://www.okx.com/docs-v5/en/#order-book-trading-market-data-get-order-book + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.method]: 'publicGetMarketBooksFull' or 'publicGetMarketBooks' default is 'publicGetMarketBooks' + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + } + method = None + method, params = self.handle_option_and_params(params, 'fetchOrderBook', 'method', 'publicGetMarketBooks') + if method == 'publicGetMarketBooksFull' and limit is None: + limit = 5000 + limit = 100 if (limit is None) else limit + if limit is not None: + request['sz'] = limit # max 400 + response = None + if (method == 'publicGetMarketBooksFull') or (limit > 400): + response = await self.publicGetMarketBooksFull(self.extend(request, params)) + else: + response = await self.publicGetMarketBooks(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "asks": [ + # ["0.07228","4.211619","0","2"], # price, amount, liquidated orders, total open orders + # ["0.0723","299.880364","0","2"], + # ["0.07231","3.72832","0","1"], + # ], + # "bids": [ + # ["0.07221","18.5","0","1"], + # ["0.0722","18.5","0","1"], + # ["0.07219","0.505407","0","1"], + # ], + # "ts": "1621438475342" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + first = self.safe_dict(data, 0, {}) + timestamp = self.safe_integer(first, 'ts') + return self.parse_order_book(first, symbol, timestamp) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "instType":"SWAP", + # "instId":"BTC-USDT-SWAP", + # "markPx":"200", + # "ts":"1597026383085" + # } + # + # { + # "instType": "SPOT", + # "instId": "ETH-BTC", + # "last": "0.07319", + # "lastSz": "0.044378", + # "askPx": "0.07322", + # "askSz": "4.2", + # "bidPx": "0.0732", + # "bidSz": "6.050058", + # "open24h": "0.07801", + # "high24h": "0.07975", + # "low24h": "0.06019", + # "volCcy24h": "11788.887619", + # "vol24h": "167493.829229", + # "ts": "1621440583784", + # "sodUtc0": "0.07872", + # "sodUtc8": "0.07345" + # } + # { + # instId: 'LTC-USDT', + # idxPx: '65.74', + # open24h: '65.37', + # high24h: '66.15', + # low24h: '64.97', + # sodUtc0: '65.68', + # sodUtc8: '65.54', + # ts: '1728467346900' + # }, + # + timestamp = self.safe_integer(ticker, 'ts') + marketId = self.safe_string(ticker, 'instId') + market = self.safe_market(marketId, market, '-') + symbol = market['symbol'] + last = self.safe_string(ticker, 'last') + open = self.safe_string(ticker, 'open24h') + spot = self.safe_bool(market, 'spot', False) + quoteVolume = self.safe_string(ticker, 'volCcy24h') if spot else None + baseVolume = self.safe_string(ticker, 'vol24h') + high = self.safe_string(ticker, 'high24h') + low = self.safe_string(ticker, 'low24h') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': high, + 'low': low, + 'bid': self.safe_string(ticker, 'bidPx'), + 'bidVolume': self.safe_string(ticker, 'bidSz'), + 'ask': self.safe_string(ticker, 'askPx'), + 'askVolume': self.safe_string(ticker, 'askSz'), + 'vwap': None, + 'open': open, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'markPrice': self.safe_string(ticker, 'markPx'), + 'indexPrice': self.safe_string(ticker, 'idxPx'), + 'info': ticker, + }, market) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://www.okx.com/docs-v5/en/#order-book-trading-market-data-get-ticker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + } + response = await self.publicGetMarketTicker(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "instType": "SPOT", + # "instId": "ETH-BTC", + # "last": "0.07319", + # "lastSz": "0.044378", + # "askPx": "0.07322", + # "askSz": "4.2", + # "bidPx": "0.0732", + # "bidSz": "6.050058", + # "open24h": "0.07801", + # "high24h": "0.07975", + # "low24h": "0.06019", + # "volCcy24h": "11788.887619", + # "vol24h": "167493.829229", + # "ts": "1621440583784", + # "sodUtc0": "0.07872", + # "sodUtc8": "0.07345" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + first = self.safe_dict(data, 0, {}) + return self.parse_ticker(first, market) + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://www.okx.com/docs-v5/en/#order-book-trading-market-data-get-tickers + + :param str[] [symbols]: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + market = self.get_market_from_symbols(symbols) + marketType = None + marketType, params = self.handle_market_type_and_params('fetchTickers', market, params) + request: dict = { + 'instType': self.convert_to_instrument_type(marketType), + } + if marketType == 'option': + defaultUnderlying = self.safe_string(self.options, 'defaultUnderlying', 'BTC-USD') + currencyId = self.safe_string_2(params, 'uly', 'marketId', defaultUnderlying) + if currencyId is None: + raise ArgumentsRequired(self.id + ' fetchTickers() requires an underlying uly or marketId parameter for options markets') + else: + request['uly'] = currencyId + response = await self.publicGetMarketTickers(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "instType": "SPOT", + # "instId": "BCD-BTC", + # "last": "0.0000769", + # "lastSz": "5.4788", + # "askPx": "0.0000777", + # "askSz": "3.2197", + # "bidPx": "0.0000757", + # "bidSz": "4.7509", + # "open24h": "0.0000885", + # "high24h": "0.0000917", + # "low24h": "0.0000596", + # "volCcy24h": "9.2877", + # "vol24h": "124824.1985", + # "ts": "1621441741434", + # "sodUtc0": "0.0000905", + # "sodUtc8": "0.0000729" + # }, + # ] + # } + # + tickers = self.safe_list(response, 'data', []) + return self.parse_tickers(tickers, symbols) + + async def fetch_mark_price(self, symbol: str, params={}) -> Ticker: + """ + fetches mark price for the market + + https://www.okx.com/docs-v5/en/#public-data-rest-api-get-mark-price + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + } + response = await self.publicGetPublicMarkPrice(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "instId": "ETH-USDT", + # "instType": "MARGIN", + # "markPx": "2403.98", + # "ts": "1728578500703" + # } + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data') + return self.parse_ticker(self.safe_dict(data, 0), market) + + async def fetch_mark_prices(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://www.okx.com/docs-v5/en/#public-data-rest-api-get-mark-price + + :param str[] [symbols]: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + market = self.get_market_from_symbols(symbols) + marketType = None + marketType, params = self.handle_market_type_and_params('fetchTickers', market, params, 'swap') + request: dict = { + 'instType': self.convert_to_instrument_type(marketType), + } + if marketType == 'option': + defaultUnderlying = self.safe_string(self.options, 'defaultUnderlying', 'BTC-USD') + currencyId = self.safe_string_2(params, 'uly', 'marketId', defaultUnderlying) + if currencyId is None: + raise ArgumentsRequired(self.id + ' fetchMarkPrices() requires an underlying uly or marketId parameter for options markets') + else: + request['uly'] = currencyId + response = await self.publicGetPublicMarkPrice(self.extend(request, params)) + tickers = self.safe_list(response, 'data', []) + return self.parse_tickers(tickers, symbols) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # public fetchTrades + # + # { + # "instId": "ETH-BTC", + # "side": "sell", + # "sz": "0.119501", + # "px": "0.07065", + # "tradeId": "15826757", + # "ts": "1621446178316" + # } + # + # option: fetchTrades + # + # { + # "fillVol": "0.46387625976562497", + # "fwdPx": "26299.754935451125", + # "indexPx": "26309.7", + # "instFamily": "BTC-USD", + # "instId": "BTC-USD-230526-26000-C", + # "markPx": "0.042386283557554236", + # "optType": "C", + # "px": "0.0415", + # "side": "sell", + # "sz": "90", + # "tradeId": "112", + # "ts": "1683907480154" + # } + # + # private fetchMyTrades + # + # { + # "side": "buy", + # "fillSz": "0.007533", + # "fillPx": "2654.98", + # "fee": "-0.000007533", + # "ordId": "317321390244397056", + # "instType": "SPOT", + # "instId": "ETH-USDT", + # "clOrdId": "", + # "posSide": "net", + # "billId": "317321390265368576", + # "tag": "0", + # "execType": "T", + # "tradeId": "107601752", + # "feeCcy": "ETH", + # "ts": "1621927314985" + # } + # + id = self.safe_string(trade, 'tradeId') + marketId = self.safe_string(trade, 'instId') + market = self.safe_market(marketId, market, '-') + symbol = market['symbol'] + timestamp = self.safe_integer(trade, 'ts') + price = self.safe_string_2(trade, 'fillPx', 'px') + amount = self.safe_string_2(trade, 'fillSz', 'sz') + side = self.safe_string(trade, 'side') + orderId = self.safe_string(trade, 'ordId') + feeCostString = self.safe_string(trade, 'fee') + fee = None + if feeCostString is not None: + feeCostSigned = Precise.string_neg(feeCostString) + feeCurrencyId = self.safe_string(trade, 'feeCcy') + feeCurrencyCode = self.safe_currency_code(feeCurrencyId) + fee = { + 'cost': feeCostSigned, + 'currency': feeCurrencyCode, + } + takerOrMaker = self.safe_string(trade, 'execType') + if takerOrMaker == 'T': + takerOrMaker = 'taker' + elif takerOrMaker == 'M': + takerOrMaker = 'maker' + return self.safe_trade({ + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'id': id, + 'order': orderId, + 'type': None, + 'takerOrMaker': takerOrMaker, + 'side': side, + 'price': price, + 'amount': amount, + 'cost': None, + 'fee': fee, + }, market) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://www.okx.com/docs-v5/en/#rest-api-market-data-get-trades + https://www.okx.com/docs-v5/en/#rest-api-public-data-get-option-trades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.method]: 'publicGetMarketTrades' or 'publicGetMarketHistoryTrades' default is 'publicGetMarketTrades' + :param boolean [params.paginate]: *only applies to publicGetMarketHistoryTrades* default False, when True will automatically paginate by calling self endpoint multiple times + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchTrades', 'paginate') + if paginate: + return await self.fetch_paginated_call_cursor('fetchTrades', symbol, since, limit, params, 'tradeId', 'after', None, 100) + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + } + response = None + if market['option']: + response = await self.publicGetPublicOptionTrades(self.extend(request, params)) + else: + if limit is not None: + request['limit'] = limit # default 100 + method = None + method, params = self.handle_option_and_params(params, 'fetchTrades', 'method', 'publicGetMarketTrades') + if method == 'publicGetMarketTrades': + response = await self.publicGetMarketTrades(self.extend(request, params)) + elif method == 'publicGetMarketHistoryTrades': + response = await self.publicGetMarketHistoryTrades(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # {"instId":"ETH-BTC","side":"sell","sz":"0.119501","px":"0.07065","tradeId":"15826757","ts":"1621446178316"}, + # {"instId":"ETH-BTC","side":"sell","sz":"0.03","px":"0.07068","tradeId":"15826756","ts":"1621446178066"}, + # {"instId":"ETH-BTC","side":"buy","sz":"0.507","px":"0.07069","tradeId":"15826755","ts":"1621446175085"}, + # ] + # } + # + # option + # + # { + # "code": "0", + # "data": [ + # { + # "fillVol": "0.46387625976562497", + # "fwdPx": "26299.754935451125", + # "indexPx": "26309.7", + # "instFamily": "BTC-USD", + # "instId": "BTC-USD-230526-26000-C", + # "markPx": "0.042386283557554236", + # "optType": "C", + # "px": "0.0415", + # "side": "sell", + # "sz": "90", + # "tradeId": "112", + # "ts": "1683907480154" + # }, + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_trades(data, market, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # [ + # "1678928760000", # timestamp + # "24341.4", # open + # "24344", # high + # "24313.2", # low + # "24323", # close + # "628", # contract volume + # "2.5819", # base volume + # "62800", # quote volume + # "0" # candlestick state + # ] + # + res = self.handle_market_type_and_params('fetchOHLCV', market, None) + type = res[0] + volumeIndex = 5 if (type == 'spot') else 6 + return [ + self.safe_integer(ohlcv, 0), + self.safe_number(ohlcv, 1), + self.safe_number(ohlcv, 2), + self.safe_number(ohlcv, 3), + self.safe_number(ohlcv, 4), + self.safe_number(ohlcv, volumeIndex), + ] + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://www.okx.com/docs-v5/en/#rest-api-market-data-get-candlesticks + https://www.okx.com/docs-v5/en/#rest-api-market-data-get-candlesticks-history + https://www.okx.com/docs-v5/en/#rest-api-market-data-get-mark-price-candlesticks + https://www.okx.com/docs-v5/en/#rest-api-market-data-get-mark-price-candlesticks-history + https://www.okx.com/docs-v5/en/#rest-api-market-data-get-index-candlesticks + https://www.okx.com/docs-v5/en/#rest-api-market-data-get-index-candlesticks-history + https://www.okx.com/docs-v5/en/#order-book-trading-market-data-get-candlesticks-history + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.price]: "mark" or "index" for mark price and index price candles + :param int [params.until]: timestamp in ms of the latest candle to fetch + :param str [params.type]: "Candles" or "HistoryCandles", default is "Candles" for recent candles, "HistoryCandles" for older candles + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate') + if paginate: + return await self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params, 200) + priceType = self.safe_string(params, 'price') + isMarkOrIndex = self.in_array(priceType, ['mark', 'index']) + params = self.omit(params, 'price') + options = self.safe_dict(self.options, 'fetchOHLCV', {}) + timezone = self.safe_string(options, 'timezone', 'UTC') + limitIsUndefined = (limit is None) + if limit is None: + limit = 100 # default 100, max 300 + else: + maxLimit = 100 if isMarkOrIndex else 300 # default 300, only 100 if 'mark' or 'index' + limit = min(limit, maxLimit) + duration = self.parse_timeframe(timeframe) + bar = self.safe_string(self.timeframes, timeframe, timeframe) + if (timezone == 'UTC') and (duration >= 21600): # if utc and timeframe >= 6h + bar += timezone.lower() + request: dict = { + 'instId': market['id'], + 'bar': bar, + 'limit': limit, + } + defaultType = 'Candles' + if since is not None: + now = self.milliseconds() + durationInMilliseconds = duration * 1000 + # switch to history candles if since is past the cutoff for current candles + historyBorder = now - ((1440 - 1) * durationInMilliseconds) + if since < historyBorder: + defaultType = 'HistoryCandles' + maxLimit = 100 if isMarkOrIndex else 300 + limit = min(limit, maxLimit) + startTime = max(since - 1, 0) + request['before'] = startTime + request['after'] = self.sum(since, durationInMilliseconds * limit) + until = self.safe_integer(params, 'until') + if until is not None: + request['after'] = until + params = self.omit(params, 'until') + defaultType = self.safe_string(options, 'type', defaultType) # Candles or HistoryCandles + type = self.safe_string(params, 'type', defaultType) + params = self.omit(params, 'type') + isHistoryCandles = (type == 'HistoryCandles') + response = None + if priceType == 'mark': + if isHistoryCandles: + response = await self.publicGetMarketHistoryMarkPriceCandles(self.extend(request, params)) + else: + response = await self.publicGetMarketMarkPriceCandles(self.extend(request, params)) + elif priceType == 'index': + request['instId'] = market['info']['instFamily'] # okx index candles require instFamily instead of instId + if isHistoryCandles: + response = await self.publicGetMarketHistoryIndexCandles(self.extend(request, params)) + else: + response = await self.publicGetMarketIndexCandles(self.extend(request, params)) + else: + if isHistoryCandles: + if limitIsUndefined and (limit == 100): + limit = 300 + request['limit'] = 300 # reassign to 300, but self whole logic needs to be simplified... + response = await self.publicGetMarketHistoryCandles(self.extend(request, params)) + else: + response = await self.publicGetMarketCandles(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # ["1678928760000","24341.4","24344","24313.2","24323","628","2.5819","62800","0"], + # ["1678928700000","24324.1","24347.6","24321.7","24341.4","2565","10.5401","256500","1"], + # ["1678928640000","24300.2","24324.1","24288","24324.1","3304","13.5937","330400","1"], + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_ohlcvs(data, market, timeframe, since, limit) + + async def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical funding rate prices + + https://www.okx.com/docs-v5/en/#public-data-rest-api-get-funding-rate-history + + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: timestamp in ms of the earliest funding rate to fetch + :param int [limit]: the maximum amount of `funding rate structures ` to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `funding rate structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchFundingRateHistory() requires a symbol argument') + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchFundingRateHistory', 'paginate') + if paginate: + return await self.fetch_paginated_call_deterministic('fetchFundingRateHistory', symbol, since, limit, '8h', params, 100) + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + } + if since is not None: + request['before'] = max(since - 1, 0) + if limit is not None: + request['limit'] = limit + response = await self.publicGetPublicFundingRateHistory(self.extend(request, params)) + # + # { + # "code":"0", + # "msg":"", + # "data":[ + # { + # "instType":"SWAP", + # "instId":"BTC-USDT-SWAP", + # "fundingRate":"0.018", + # "realizedRate":"0.017", + # "fundingTime":"1597026383085" + # }, + # { + # "instType":"SWAP", + # "instId":"BTC-USDT-SWAP", + # "fundingRate":"0.018", + # "realizedRate":"0.017", + # "fundingTime":"1597026383085" + # } + # ] + # } + # + rates = [] + data = self.safe_list(response, 'data', []) + for i in range(0, len(data)): + rate = data[i] + timestamp = self.safe_integer(rate, 'fundingTime') + rates.append({ + 'info': rate, + 'symbol': self.safe_symbol(self.safe_string(rate, 'instId')), + 'fundingRate': self.safe_number(rate, 'realizedRate'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + sorted = self.sort_by(rates, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, market['symbol'], since, limit) + + def parse_balance_by_type(self, type, response): + if type == 'funding': + return self.parse_funding_balance(response) + else: + return self.parse_trading_balance(response) + + def parse_trading_balance(self, response): + result: dict = {'info': response} + data = self.safe_list(response, 'data', []) + first = self.safe_dict(data, 0, {}) + timestamp = self.safe_integer(first, 'uTime') + details = self.safe_list(first, 'details', []) + for i in range(0, len(details)): + balance = details[i] + currencyId = self.safe_string(balance, 'ccy') + code = self.safe_currency_code(currencyId) + account = self.account() + # it may be incorrect to use total, free and used for swap accounts + eq = self.safe_string(balance, 'eq') + availEq = self.safe_string(balance, 'availEq') + account['total'] = eq + if availEq is None: + account['free'] = self.safe_string(balance, 'availBal') + account['used'] = self.safe_string(balance, 'frozenBal') + else: + account['free'] = availEq + result[code] = account + result['timestamp'] = timestamp + result['datetime'] = self.iso8601(timestamp) + return self.safe_balance(result) + + def parse_funding_balance(self, response): + result: dict = {'info': response} + data = self.safe_list(response, 'data', []) + for i in range(0, len(data)): + balance = data[i] + currencyId = self.safe_string(balance, 'ccy') + code = self.safe_currency_code(currencyId) + account = self.account() + # it may be incorrect to use total, free and used for swap accounts + account['total'] = self.safe_string(balance, 'bal') + account['free'] = self.safe_string(balance, 'availBal') + account['used'] = self.safe_string(balance, 'frozenBal') + result[code] = account + return self.safe_balance(result) + + def parse_trading_fee(self, fee: dict, market: Market = None) -> TradingFeeInterface: + # https://www.okx.com/docs-v5/en/#rest-api-account-get-fee-rates + # + # { + # "category": "1", + # "delivery": "", + # "exercise": "", + # "instType": "SPOT", + # "level": "Lv1", + # "maker": "-0.0008", + # "taker": "-0.001", + # "ts": "1639043138472" + # } + # + return { + 'info': fee, + 'symbol': self.safe_symbol(None, market), + # OKX returns the fees values opposed to other exchanges, so the sign needs to be flipped + 'maker': self.parse_number(Precise.string_neg(self.safe_string_2(fee, 'maker', 'makerU'))), + 'taker': self.parse_number(Precise.string_neg(self.safe_string_2(fee, 'taker', 'takerU'))), + 'percentage': None, + 'tierBased': None, + } + + async def fetch_trading_fee(self, symbol: str, params={}) -> TradingFeeInterface: + """ + fetch the trading fees for a market + + https://www.okx.com/docs-v5/en/#trading-account-rest-api-get-fee-rates + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `fee structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'instType': self.convert_to_instrument_type(market['type']), # SPOT, MARGIN, SWAP, FUTURES, OPTION + # "instId": market["id"], # only applicable to SPOT/MARGIN + # "uly": market["id"], # only applicable to FUTURES/SWAP/OPTION + # "category": "1", # 1 = Class A, 2 = Class B, 3 = Class C, 4 = Class D + } + if market['spot']: + request['instId'] = market['id'] + elif market['swap'] or market['future'] or market['option']: + request['uly'] = market['baseId'] + '-' + market['quoteId'] + else: + raise NotSupported(self.id + ' fetchTradingFee() supports spot, swap, future or option markets only') + response = await self.privateGetAccountTradeFee(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "category": "1", + # "delivery": "", + # "exercise": "", + # "instType": "SPOT", + # "level": "Lv1", + # "maker": "-0.0008", + # "taker": "-0.001", + # "ts": "1639043138472" + # } + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + first = self.safe_dict(data, 0, {}) + return self.parse_trading_fee(first, market) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://www.okx.com/docs-v5/en/#funding-account-rest-api-get-balance + https://www.okx.com/docs-v5/en/#trading-account-rest-api-get-balance + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: wallet type, ['funding' or 'trading'] default is 'trading' + :returns dict: a `balance structure ` + """ + await self.load_markets() + marketType, query = self.handle_market_type_and_params('fetchBalance', None, params) + request: dict = { + # 'ccy': 'BTC,ETH', # comma-separated list of currency ids + } + response = None + if marketType == 'funding': + response = await self.privateGetAssetBalances(self.extend(request, query)) + else: + response = await self.privateGetAccountBalance(self.extend(request, query)) + # + # { + # "code": "0", + # "data": [ + # { + # "adjEq": "", + # "details": [ + # { + # "availBal": "", + # "availEq": "28.21006347", + # "cashBal": "28.21006347", + # "ccy": "USDT", + # "crossLiab": "", + # "disEq": "28.2687404020176", + # "eq":"28 .21006347", + # "eqUsd": "28.2687404020176", + # "frozenBal": "0", + # "interest": "", + # "isoEq": "0", + # "isoLiab": "", + # "liab": "", + # "maxLoan": "", + # "mgnRatio": "", + # "notionalLever": "0", + # "ordFrozen": "0", + # "twap": "0", + # "uTime": "1621556539861", + # "upl": "0", + # "uplLiab": "" + # } + # ], + # "imr": "", + # "isoEq": "0", + # "mgnRatio": "", + # "mmr": "", + # "notionalUsd": "", + # "ordFroz": "", + # "totalEq": "28.2687404020176", + # "uTime": "1621556553510" + # } + # ], + # "msg": "" + # } + # + # { + # "code": "0", + # "data": [ + # { + # "adjEq": "", + # "details": [ + # { + # "availBal": "0.049", + # "availEq": "", + # "cashBal": "0.049", + # "ccy": "BTC", + # "crossLiab": "", + # "disEq": "1918.55678", + # "eq": "0.049", + # "eqUsd": "1918.55678", + # "frozenBal": "0", + # "interest": "", + # "isoEq": "", + # "isoLiab": "", + # "liab": "", + # "maxLoan": "", + # "mgnRatio": "", + # "notionalLever": "", + # "ordFrozen": "0", + # "twap": "0", + # "uTime": "1621973128591", + # "upl": "", + # "uplLiab": "" + # } + # ], + # "imr": "", + # "isoEq": "", + # "mgnRatio": "", + # "mmr": "", + # "notionalUsd": "", + # "ordFroz": "", + # "totalEq": "1918.55678", + # "uTime": "1622045126908" + # } + # ], + # "msg": "" + # } + # + # funding + # + # { + # "code": "0", + # "data": [ + # { + # "availBal": "0.00005426", + # "bal": 0.0000542600000000, + # "ccy": "BTC", + # "frozenBal": "0" + # } + # ], + # "msg": "" + # } + # + return self.parse_balance_by_type(marketType, response) + + async def create_market_buy_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market buy order by providing the symbol and cost + + https://www.okx.com/docs-v5/en/#order-book-trading-trade-post-place-order + + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' createMarketBuyOrderWithCost() supports spot markets only') + req = { + 'createMarketBuyOrderRequiresPrice': False, + 'tgtCcy': 'quote_ccy', + } + return await self.create_order(symbol, 'market', 'buy', cost, None, self.extend(req, params)) + + async def create_market_sell_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market buy order by providing the symbol and cost + + https://www.okx.com/docs-v5/en/#order-book-trading-trade-post-place-order + + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' createMarketSellOrderWithCost() supports spot markets only') + req = { + 'createMarketBuyOrderRequiresPrice': False, + 'tgtCcy': 'quote_ccy', + } + return await self.create_order(symbol, 'market', 'sell', cost, None, self.extend(req, params)) + + def create_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + # 'ccy': currency['id'], # only applicable to cross MARGIN orders in single-currency margin + # 'clOrdId': clientOrderId, # up to 32 characters, must be unique + # 'tag': tag, # up to 8 characters + 'side': side, + # 'posSide': 'long', # long, short, # required in the long/short mode, and can only be long or short(only for future or swap) + 'ordType': type, + # 'ordType': type, # privatePostTradeOrder: market, limit, post_only, fok, ioc, optimal_limit_ioc + # 'ordType': type, # privatePostTradeOrderAlgo: conditional, oco, trigger, move_order_stop, iceberg, twap + 'sz': self.amount_to_precision(symbol, amount), + # 'px': self.price_to_precision(symbol, price), # limit orders only + # 'reduceOnly': False, + # + # 'triggerPx': 10, # stopPrice(trigger orders) + # 'orderPx': 10, # Order price if -1, the order will be executed at the market price.(trigger orders) + # 'triggerPxType': 'last', # Conditional default is last, mark or index(trigger orders) + # + # 'tpTriggerPx': 10, # takeProfitPrice(conditional orders) + # 'tpTriggerPxType': 'last', # Conditional default is last, mark or index(conditional orders) + # 'tpOrdPx': 10, # Order price for Take-Profit orders, if -1 will be executed at market price(conditional orders) + # + # 'slTriggerPx': 10, # stopLossPrice(conditional orders) + # 'slTriggerPxType': 'last', # Conditional default is last, mark or index(conditional orders) + # 'slOrdPx': 10, # Order price for Stop-Loss orders, if -1 will be executed at market price(conditional orders) + } + spot = market['spot'] + contract = market['contract'] + triggerPrice = self.safe_value_n(params, ['triggerPrice', 'stopPrice', 'triggerPx']) + timeInForce = self.safe_string(params, 'timeInForce', 'GTC') + takeProfitPrice = self.safe_value_2(params, 'takeProfitPrice', 'tpTriggerPx') + tpOrdPx = self.safe_value(params, 'tpOrdPx', price) + tpTriggerPxType = self.safe_string(params, 'tpTriggerPxType', 'last') + stopLossPrice = self.safe_value_2(params, 'stopLossPrice', 'slTriggerPx') + slOrdPx = self.safe_value(params, 'slOrdPx', price) + slTriggerPxType = self.safe_string(params, 'slTriggerPxType', 'last') + clientOrderId = self.safe_string_2(params, 'clOrdId', 'clientOrderId') + stopLoss = self.safe_value(params, 'stopLoss') + stopLossDefined = (stopLoss is not None) + takeProfit = self.safe_value(params, 'takeProfit') + takeProfitDefined = (takeProfit is not None) + trailingPercent = self.safe_string_2(params, 'trailingPercent', 'callbackRatio') + isTrailingPercentOrder = trailingPercent is not None + trailingPrice = self.safe_string_2(params, 'trailingPrice', 'callbackSpread') + isTrailingPriceOrder = trailingPrice is not None + trigger = (triggerPrice is not None) or (type == 'trigger') + isReduceOnly = self.safe_value(params, 'reduceOnly', False) + defaultMarginMode = self.safe_string_2(self.options, 'defaultMarginMode', 'marginMode', 'cross') + marginMode = self.safe_string_2(params, 'marginMode', 'tdMode') # cross or isolated, tdMode not ommited so be extended into the request + margin = False + if (marginMode is not None) and (marginMode != 'cash'): + margin = True + else: + marginMode = defaultMarginMode + margin = self.safe_bool(params, 'margin', False) + if spot: + if margin: + defaultCurrency = market['quote'] if (side == 'buy') else market['base'] + currency = self.safe_string(params, 'ccy', defaultCurrency) + request['ccy'] = self.safe_currency_code(currency) + tradeMode = marginMode if margin else 'cash' + request['tdMode'] = tradeMode + elif contract: + if market['swap'] or market['future']: + positionSide = None + positionSide, params = self.handle_option_and_params(params, 'createOrder', 'positionSide') + if positionSide is not None: + request['posSide'] = positionSide + else: + hedged = None + hedged, params = self.handle_option_and_params(params, 'createOrder', 'hedged') + if hedged: + isBuy = (side == 'buy') + isProtective = (takeProfitPrice is not None) or (stopLossPrice is not None) or isReduceOnly + if isProtective: + # in case of protective orders, the posSide should be opposite of position side + # reduceOnly is emulated and not natively supported by the exchange + request['posSide'] = 'short' if isBuy else 'long' + if isReduceOnly: + params = self.omit(params, 'reduceOnly') + else: + request['posSide'] = 'long' if isBuy else 'short' + request['tdMode'] = marginMode + isMarketOrder = type == 'market' + postOnly = False + postOnly, params = self.handle_post_only(isMarketOrder, type == 'post_only', params) + params = self.omit(params, ['currency', 'ccy', 'marginMode', 'timeInForce', 'stopPrice', 'triggerPrice', 'clientOrderId', 'stopLossPrice', 'takeProfitPrice', 'slOrdPx', 'tpOrdPx', 'margin', 'stopLoss', 'takeProfit', 'trailingPercent']) + ioc = (timeInForce == 'IOC') or (type == 'ioc') + fok = (timeInForce == 'FOK') or (type == 'fok') + conditional = (stopLossPrice is not None) or (takeProfitPrice is not None) or (type == 'conditional') + marketIOC = (isMarketOrder and ioc) or (type == 'optimal_limit_ioc') + defaultTgtCcy = self.safe_string(self.options, 'tgtCcy', 'base_ccy') + tgtCcy = self.safe_string(params, 'tgtCcy', defaultTgtCcy) + if (not contract) and (not margin): + request['tgtCcy'] = tgtCcy + if isMarketOrder or marketIOC: + request['ordType'] = 'market' + if spot and (side == 'buy'): + # spot market buy: "sz" can refer either to base currency units or to quote currency units + # see documentation: https://www.okx.com/docs-v5/en/#rest-api-trade-place-order + if tgtCcy == 'quote_ccy': + # quote_ccy: sz refers to units of quote currency + createMarketBuyOrderRequiresPrice = True + createMarketBuyOrderRequiresPrice, params = self.handle_option_and_params(params, 'createOrder', 'createMarketBuyOrderRequiresPrice', True) + notional = self.safe_number_2(params, 'cost', 'sz') + params = self.omit(params, ['cost', 'sz']) + if createMarketBuyOrderRequiresPrice: + if price is not None: + if notional is None: + amountString = self.number_to_string(amount) + priceString = self.number_to_string(price) + quoteAmount = Precise.string_mul(amountString, priceString) + notional = self.parse_number(quoteAmount) + elif notional is None: + raise InvalidOrder(self.id + " createOrder() requires the price argument with market buy orders to calculate total order cost(amount to spend), where cost = amount * price. Supply a price argument to createOrder() call if you want the cost to be calculated for you from price and amount, or, alternatively, add .options['createMarketBuyOrderRequiresPrice'] = False and supply the total cost value in the 'amount' argument or in the 'cost' unified extra parameter or in exchange-specific 'sz' extra parameter(the exchange-specific behaviour)") + else: + notional = amount if (notional is None) else notional + request['sz'] = self.cost_to_precision(symbol, notional) + if marketIOC and contract: + request['ordType'] = 'optimal_limit_ioc' + else: + if (not trigger) and (not conditional): + request['px'] = self.price_to_precision(symbol, price) + if postOnly: + request['ordType'] = 'post_only' + elif ioc and not marketIOC: + request['ordType'] = 'ioc' + elif fok: + request['ordType'] = 'fok' + if isTrailingPercentOrder: + convertedTrailingPercent = Precise.string_div(trailingPercent, '100') + request['callbackRatio'] = convertedTrailingPercent + request['ordType'] = 'move_order_stop' + elif isTrailingPriceOrder: + request['callbackSpread'] = trailingPrice + request['ordType'] = 'move_order_stop' + elif stopLossDefined or takeProfitDefined: + attachAlgoOrd = {} + if stopLossDefined: + stopLossTriggerPrice = self.safe_value_n(stopLoss, ['triggerPrice', 'stopPrice', 'slTriggerPx']) + if stopLossTriggerPrice is None: + raise InvalidOrder(self.id + ' createOrder() requires a trigger price in params["stopLoss"]["triggerPrice"], or params["stopLoss"]["stopPrice"], or params["stopLoss"]["slTriggerPx"] for a stop loss order') + slTriggerPx = self.price_to_precision(symbol, stopLossTriggerPrice) + slOrder = {} + slOrder['slTriggerPx'] = slTriggerPx + stopLossLimitPrice = self.safe_value_n(stopLoss, ['price', 'stopLossPrice', 'slOrdPx']) + stopLossOrderType = self.safe_string(stopLoss, 'type') + if stopLossOrderType is not None: + stopLossLimitOrderType = (stopLossOrderType == 'limit') + stopLossMarketOrderType = (stopLossOrderType == 'market') + if (not stopLossLimitOrderType) and (not stopLossMarketOrderType): + raise InvalidOrder(self.id + ' createOrder() params["stopLoss"]["type"] must be either "limit" or "market"') + elif stopLossLimitOrderType: + if stopLossLimitPrice is None: + raise InvalidOrder(self.id + ' createOrder() requires a limit price in params["stopLoss"]["price"] or params["stopLoss"]["slOrdPx"] for a stop loss limit order') + else: + slOrder['slOrdPx'] = self.price_to_precision(symbol, stopLossLimitPrice) + elif stopLossOrderType == 'market': + slOrder['slOrdPx'] = '-1' + elif stopLossLimitPrice is not None: + slOrder['slOrdPx'] = self.price_to_precision(symbol, stopLossLimitPrice) # limit sl order + else: + slOrder['slOrdPx'] = '-1' # market sl order + stopLossTriggerPriceType = self.safe_string_2(stopLoss, 'triggerPriceType', 'slTriggerPxType', 'last') + if stopLossTriggerPriceType is not None: + if (stopLossTriggerPriceType != 'last') and (stopLossTriggerPriceType != 'index') and (stopLossTriggerPriceType != 'mark'): + raise InvalidOrder(self.id + ' createOrder() stop loss trigger price type must be one of "last", "index" or "mark"') + slOrder['slTriggerPxType'] = stopLossTriggerPriceType + attachAlgoOrd = self.extend(attachAlgoOrd, slOrder) + if takeProfitDefined: + takeProfitTriggerPrice = self.safe_value_n(takeProfit, ['triggerPrice', 'stopPrice', 'tpTriggerPx']) + if takeProfitTriggerPrice is None: + raise InvalidOrder(self.id + ' createOrder() requires a trigger price in params["takeProfit"]["triggerPrice"], or params["takeProfit"]["stopPrice"], or params["takeProfit"]["tpTriggerPx"] for a take profit order') + tpOrder = {} + tpOrder['tpTriggerPx'] = self.price_to_precision(symbol, takeProfitTriggerPrice) + takeProfitLimitPrice = self.safe_value_n(takeProfit, ['price', 'takeProfitPrice', 'tpOrdPx']) + takeProfitOrderType = self.safe_string_2(takeProfit, 'type', 'tpOrdKind') + if takeProfitOrderType is not None: + takeProfitLimitOrderType = (takeProfitOrderType == 'limit') + takeProfitMarketOrderType = (takeProfitOrderType == 'market') + if (not takeProfitLimitOrderType) and (not takeProfitMarketOrderType): + raise InvalidOrder(self.id + ' createOrder() params["takeProfit"]["type"] must be either "limit" or "market"') + elif takeProfitLimitOrderType: + if takeProfitLimitPrice is None: + raise InvalidOrder(self.id + ' createOrder() requires a limit price in params["takeProfit"]["price"] or params["takeProfit"]["tpOrdPx"] for a take profit limit order') + else: + tpOrder['tpOrdKind'] = takeProfitOrderType + tpOrder['tpOrdPx'] = self.price_to_precision(symbol, takeProfitLimitPrice) + elif takeProfitOrderType == 'market': + tpOrder['tpOrdPx'] = '-1' + elif takeProfitLimitPrice is not None: + tpOrder['tpOrdKind'] = 'limit' + tpOrder['tpOrdPx'] = self.price_to_precision(symbol, takeProfitLimitPrice) # limit tp order + else: + tpOrder['tpOrdPx'] = '-1' # market tp order + takeProfitTriggerPriceType = self.safe_string_2(takeProfit, 'triggerPriceType', 'tpTriggerPxType', 'last') + if takeProfitTriggerPriceType is not None: + if (takeProfitTriggerPriceType != 'last') and (takeProfitTriggerPriceType != 'index') and (takeProfitTriggerPriceType != 'mark'): + raise InvalidOrder(self.id + ' createOrder() take profit trigger price type must be one of "last", "index" or "mark"') + tpOrder['tpTriggerPxType'] = takeProfitTriggerPriceType + attachAlgoOrd = self.extend(attachAlgoOrd, tpOrder) + attachOrdKeys = list(attachAlgoOrd.keys()) + attachOrdLen = len(attachOrdKeys) + if attachOrdLen > 0: + request['attachAlgoOrds'] = [attachAlgoOrd] + # algo order details + if trigger: + request['ordType'] = 'trigger' + request['triggerPx'] = self.price_to_precision(symbol, triggerPrice) + request['orderPx'] = '-1' if isMarketOrder else self.price_to_precision(symbol, price) + elif conditional: + request['ordType'] = 'conditional' + twoWayCondition = ((takeProfitPrice is not None) and (stopLossPrice is not None)) + # if TP and SL are sent together + # 'conditional' only stop-loss order will be applied + # tpOrdKind is 'condition' which is the default + if twoWayCondition: + request['ordType'] = 'oco' + if side == 'sell': + request = self.omit(request, 'tgtCcy') + if self.safe_string(request, 'tdMode') == 'cash': + # for some reason tdMode = cash throws + # {"code":"1","data":[{"algoClOrdId":"","algoId":"","clOrdId":"","sCode":"51000","sMsg":"Parameter tdMode error ","tag":""}],"msg":""} + request['tdMode'] = marginMode + if takeProfitPrice is not None: + request['tpTriggerPx'] = self.price_to_precision(symbol, takeProfitPrice) + tpOrdPxReq = '-1' + if tpOrdPx is not None: + tpOrdPxReq = self.price_to_precision(symbol, tpOrdPx) + request['tpOrdPx'] = tpOrdPxReq + request['tpTriggerPxType'] = tpTriggerPxType + if stopLossPrice is not None: + request['slTriggerPx'] = self.price_to_precision(symbol, stopLossPrice) + slOrdPxReq = '-1' + if slOrdPx is not None: + slOrdPxReq = self.price_to_precision(symbol, slOrdPx) + request['slOrdPx'] = slOrdPxReq + request['slTriggerPxType'] = slTriggerPxType + if clientOrderId is None: + brokerId = self.safe_string(self.options, 'brokerId') + if brokerId is not None: + request['clOrdId'] = brokerId + self.uuid16() + request['tag'] = brokerId + else: + request['clOrdId'] = clientOrderId + params = self.omit(params, ['clOrdId', 'clientOrderId']) + return self.extend(request, params) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://www.okx.com/docs-v5/en/#order-book-trading-trade-post-place-order + https://www.okx.com/docs-v5/en/#order-book-trading-trade-post-place-multiple-orders + https://www.okx.com/docs-v5/en/#order-book-trading-algo-trading-post-place-algo-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.reduceOnly]: a mark to reduce the position size for margin, swap and future orders + :param bool [params.postOnly]: True to place a post only order + :param dict [params.takeProfit]: *takeProfit object in params* containing the triggerPrice at which the attached take profit order will be triggered(perpetual swap markets only) + :param float [params.takeProfit.triggerPrice]: take profit trigger price + :param float [params.takeProfit.price]: used for take profit limit orders, not used for take profit market price orders + :param str [params.takeProfit.type]: 'market' or 'limit' used to specify the take profit price type + :param dict [params.stopLoss]: *stopLoss object in params* containing the triggerPrice at which the attached stop loss order will be triggered(perpetual swap markets only) + :param float [params.stopLoss.triggerPrice]: stop loss trigger price + :param float [params.stopLoss.price]: used for stop loss limit orders, not used for stop loss market price orders + :param str [params.stopLoss.type]: 'market' or 'limit' used to specify the stop loss price type + :param str [params.positionSide]: if position mode is one-way: set to 'net', if position mode is hedge-mode: set to 'long' or 'short' + :param str [params.trailingPercent]: the percent to trail away from the current market price + :param str [params.tpOrdKind]: 'condition' or 'limit', the default is 'condition' + :param bool [params.hedged]: *swap and future only* True for hedged mode, False for one way mode + :param str [params.marginMode]: 'cross' or 'isolated', the default is 'cross' + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + request = self.create_order_request(symbol, type, side, amount, price, params) + method = self.safe_string(self.options, 'createOrder', 'privatePostTradeBatchOrders') + requestOrdType = self.safe_string(request, 'ordType') + if (requestOrdType == 'trigger') or (requestOrdType == 'conditional') or (requestOrdType == 'move_order_stop') or (type == 'move_order_stop') or (type == 'oco') or (type == 'iceberg') or (type == 'twap'): + method = 'privatePostTradeOrderAlgo' + if (method != 'privatePostTradeOrder') and (method != 'privatePostTradeOrderAlgo') and (method != 'privatePostTradeBatchOrders'): + raise ExchangeError(self.id + ' createOrder() self.options["createOrder"] must be either privatePostTradeBatchOrders or privatePostTradeOrder or privatePostTradeOrderAlgo') + if method == 'privatePostTradeBatchOrders': + # keep the request body the same + # submit a single order in an array to the batch order endpoint + # because it has a lower ratelimit + request = [request] + response = None + if method == 'privatePostTradeOrder': + response = await self.privatePostTradeOrder(request) + elif method == 'privatePostTradeOrderAlgo': + response = await self.privatePostTradeOrderAlgo(request) + else: + response = await self.privatePostTradeBatchOrders(request) + data = self.safe_list(response, 'data', []) + first = self.safe_dict(data, 0, {}) + order = self.parse_order(first, market) + order['type'] = type + order['side'] = side + return order + + async def create_orders(self, orders: List[OrderRequest], params={}): + """ + create a list of trade orders + + https://www.okx.com/docs-v5/en/#order-book-trading-trade-post-place-multiple-orders + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + ordersRequests = [] + for i in range(0, len(orders)): + rawOrder = orders[i] + marketId = self.safe_string(rawOrder, 'symbol') + type = self.safe_string(rawOrder, 'type') + side = self.safe_string(rawOrder, 'side') + amount = self.safe_value(rawOrder, 'amount') + price = self.safe_value(rawOrder, 'price') + orderParams = self.safe_dict(rawOrder, 'params', {}) + extendedParams = self.extend(orderParams, params) # the request does not accept extra params since it's a list, so we're extending each order with the common params + orderRequest = self.create_order_request(marketId, type, side, amount, price, extendedParams) + ordersRequests.append(orderRequest) + response = await self.privatePostTradeBatchOrders(ordersRequests) + # { + # "code": "0", + # "data": [ + # { + # "clOrdId": "e847386590ce4dBCc7f2a1b4c4509f82", + # "ordId": "636305438765568000", + # "sCode": "0", + # "sMsg": "Order placed", + # "tag": "e847386590ce4dBC" + # }, + # { + # "clOrdId": "e847386590ce4dBC0b9993fe642d8f62", + # "ordId": "636305438765568001", + # "sCode": "0", + # "sMsg": "Order placed", + # "tag": "e847386590ce4dBC" + # } + # ], + # "inTime": "1697979038584486", + # "msg": "", + # "outTime": "1697979038586493" + # } + data = self.safe_list(response, 'data', []) + return self.parse_orders(data) + + def edit_order_request(self, id: str, symbol, type, side, amount=None, price=None, params={}): + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + } + isAlgoOrder = None + if (type == 'trigger') or (type == 'conditional') or (type == 'move_order_stop') or (type == 'oco') or (type == 'iceberg') or (type == 'twap'): + isAlgoOrder = True + clientOrderId = self.safe_string_2(params, 'clOrdId', 'clientOrderId') + if clientOrderId is not None: + if isAlgoOrder: + request['algoClOrdId'] = clientOrderId + else: + request['clOrdId'] = clientOrderId + else: + if isAlgoOrder: + request['algoId'] = id + else: + request['ordId'] = id + stopLossTriggerPrice = self.safe_value_2(params, 'stopLossPrice', 'newSlTriggerPx') + stopLossPrice = self.safe_value(params, 'newSlOrdPx') + stopLossTriggerPriceType = self.safe_string(params, 'newSlTriggerPxType', 'last') + takeProfitTriggerPrice = self.safe_value_2(params, 'takeProfitPrice', 'newTpTriggerPx') + takeProfitPrice = self.safe_value(params, 'newTpOrdPx') + takeProfitTriggerPriceType = self.safe_string(params, 'newTpTriggerPxType', 'last') + stopLoss = self.safe_value(params, 'stopLoss') + takeProfit = self.safe_value(params, 'takeProfit') + stopLossDefined = (stopLoss is not None) + takeProfitDefined = (takeProfit is not None) + if isAlgoOrder: + if (stopLossTriggerPrice is None) and (takeProfitTriggerPrice is None): + raise BadRequest(self.id + ' editOrder() requires a stopLossPrice or takeProfitPrice parameter for editing an algo order') + if stopLossTriggerPrice is not None: + if stopLossPrice is None: + raise BadRequest(self.id + ' editOrder() requires a newSlOrdPx parameter for editing an algo order') + request['newSlTriggerPx'] = self.price_to_precision(symbol, stopLossTriggerPrice) + request['newSlOrdPx'] = '-1' if (type == 'market') else self.price_to_precision(symbol, stopLossPrice) + request['newSlTriggerPxType'] = stopLossTriggerPriceType + if takeProfitTriggerPrice is not None: + if takeProfitPrice is None: + raise BadRequest(self.id + ' editOrder() requires a newTpOrdPx parameter for editing an algo order') + request['newTpTriggerPx'] = self.price_to_precision(symbol, takeProfitTriggerPrice) + request['newTpOrdPx'] = '-1' if (type == 'market') else self.price_to_precision(symbol, takeProfitPrice) + request['newTpTriggerPxType'] = takeProfitTriggerPriceType + else: + if stopLossTriggerPrice is not None: + request['newSlTriggerPx'] = self.price_to_precision(symbol, stopLossTriggerPrice) + request['newSlOrdPx'] = '-1' if (type == 'market') else self.price_to_precision(symbol, stopLossPrice) + request['newSlTriggerPxType'] = stopLossTriggerPriceType + if takeProfitTriggerPrice is not None: + request['newTpTriggerPx'] = self.price_to_precision(symbol, takeProfitTriggerPrice) + request['newTpOrdPx'] = '-1' if (type == 'market') else self.price_to_precision(symbol, takeProfitPrice) + request['newTpTriggerPxType'] = takeProfitTriggerPriceType + if stopLossDefined: + stopLossTriggerPrice = self.safe_value(stopLoss, 'triggerPrice') + stopLossPrice = self.safe_value(stopLoss, 'price') + stopLossType = self.safe_string(stopLoss, 'type') + request['newSlTriggerPx'] = self.price_to_precision(symbol, stopLossTriggerPrice) + request['newSlOrdPx'] = '-1' if (stopLossType == 'market') else self.price_to_precision(symbol, stopLossPrice) + request['newSlTriggerPxType'] = stopLossTriggerPriceType + if takeProfitDefined: + takeProfitTriggerPrice = self.safe_value(takeProfit, 'triggerPrice') + takeProfitPrice = self.safe_value(takeProfit, 'price') + takeProfitType = self.safe_string(takeProfit, 'type') + request['newTpOrdKind'] = takeProfitType if (takeProfitType == 'limit') else 'condition' + request['newTpTriggerPx'] = self.price_to_precision(symbol, takeProfitTriggerPrice) + request['newTpOrdPx'] = '-1' if (takeProfitType == 'market') else self.price_to_precision(symbol, takeProfitPrice) + request['newTpTriggerPxType'] = takeProfitTriggerPriceType + if amount is not None: + request['newSz'] = self.amount_to_precision(symbol, amount) + if not isAlgoOrder: + if price is not None: + request['newPx'] = self.price_to_precision(symbol, price) + params = self.omit(params, ['clOrdId', 'clientOrderId', 'takeProfitPrice', 'stopLossPrice', 'stopLoss', 'takeProfit', 'postOnly']) + return self.extend(request, params) + + async def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + """ + edit a trade order + + https://www.okx.com/docs-v5/en/#order-book-trading-trade-post-amend-order + https://www.okx.com/docs-v5/en/#order-book-trading-algo-trading-post-amend-algo-order + + :param str id: order id + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of the currency you want to trade in units of the base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.clientOrderId]: client order id, uses id if not passed + :param float [params.stopLossPrice]: stop loss trigger price + :param float [params.newSlOrdPx]: the stop loss order price, set to stopLossPrice if the type is market + :param str [params.newSlTriggerPxType]: 'last', 'index' or 'mark' used to specify the stop loss trigger price type, default is 'last' + :param float [params.takeProfitPrice]: take profit trigger price + :param float [params.newTpOrdPx]: the take profit order price, set to takeProfitPrice if the type is market + :param str [params.newTpTriggerPxType]: 'last', 'index' or 'mark' used to specify the take profit trigger price type, default is 'last' + :param dict [params.stopLoss]: *stopLoss object in params* containing the triggerPrice at which the attached stop loss order will be triggered + :param float [params.stopLoss.triggerPrice]: stop loss trigger price + :param float [params.stopLoss.price]: used for stop loss limit orders, not used for stop loss market price orders + :param str [params.stopLoss.type]: 'market' or 'limit' used to specify the stop loss price type + :param dict [params.takeProfit]: *takeProfit object in params* containing the triggerPrice at which the attached take profit order will be triggered + :param float [params.takeProfit.triggerPrice]: take profit trigger price + :param float [params.takeProfit.price]: used for take profit limit orders, not used for take profit market price orders + :param str [params.takeProfit.type]: 'market' or 'limit' used to specify the take profit price type + :param str [params.newTpOrdKind]: 'condition' or 'limit', the default is 'condition' + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + request = self.edit_order_request(id, symbol, type, side, amount, price, params) + isAlgoOrder = None + if (type == 'trigger') or (type == 'conditional') or (type == 'move_order_stop') or (type == 'oco') or (type == 'iceberg') or (type == 'twap'): + isAlgoOrder = True + response = None + if isAlgoOrder: + response = await self.privatePostTradeAmendAlgos(self.extend(request, params)) + else: + response = await self.privatePostTradeAmendOrder(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "clOrdId": "e847386590ce4dBCc1a045253497a547", + # "ordId": "559176536793178112", + # "reqId": "", + # "sCode": "0", + # "sMsg": "" + # } + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + first = self.safe_dict(data, 0, {}) + order = self.parse_order(first, market) + order['type'] = type + order['side'] = side + return order + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://www.okx.com/docs-v5/en/#order-book-trading-trade-post-cancel-order + https://www.okx.com/docs-v5/en/#order-book-trading-algo-trading-post-cancel-algo-order + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: True if trigger orders + :param boolean [params.trailing]: set to True if you want to cancel a trailing order + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + trigger = self.safe_value_2(params, 'stop', 'trigger') + trailing = self.safe_bool(params, 'trailing', False) + if trigger or trailing: + orderInner = await self.cancel_orders([id], symbol, params) + return self.safe_dict(orderInner, 0) + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + # 'ordId': id, # either ordId or clOrdId is required + # 'clOrdId': clientOrderId, + } + clientOrderId = self.safe_string_2(params, 'clOrdId', 'clientOrderId') + if clientOrderId is not None: + request['clOrdId'] = clientOrderId + else: + request['ordId'] = id + query = self.omit(params, ['clOrdId', 'clientOrderId']) + response = await self.privatePostTradeCancelOrder(self.extend(request, query)) + # {"code":"0","data":[{"clOrdId":"","ordId":"317251910906576896","sCode":"0","sMsg":""}],"msg":""} + data = self.safe_value(response, 'data', []) + order = self.safe_dict(data, 0) + return self.parse_order(order, market) + + def parse_ids(self, ids): + """ + @ignore + :param string[]|str ids: order ids + :returns str[]: list of order ids + """ + if (ids is not None) and isinstance(ids, str): + return ids.split(',') + else: + return ids + + async def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): + """ + cancel multiple orders + + https://www.okx.com/docs-v5/en/#order-book-trading-trade-post-cancel-multiple-orders + https://www.okx.com/docs-v5/en/#order-book-trading-algo-trading-post-cancel-algo-order + + :param str[] ids: order ids + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: whether the order is a stop/trigger order + :param boolean [params.trailing]: set to True if you want to cancel trailing orders + :returns dict: an list of `order structures ` + """ + # TODO : the original endpoint signature differs, according to that you can skip individual symbol and assign ids in batch. At self moment, `params` is not being used too. + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrders() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request = [] + options = self.safe_value(self.options, 'cancelOrders', {}) + defaultMethod = self.safe_string(options, 'method', 'privatePostTradeCancelBatchOrders') + method = self.safe_string(params, 'method', defaultMethod) + clientOrderIds = self.parse_ids(self.safe_value_2(params, 'clOrdId', 'clientOrderId')) + algoIds = self.parse_ids(self.safe_value(params, 'algoId')) + trigger = self.safe_value_2(params, 'stop', 'trigger') + trailing = self.safe_bool(params, 'trailing', False) + if trigger or trailing: + method = 'privatePostTradeCancelAlgos' + if clientOrderIds is None: + ids = self.parse_ids(ids) + if algoIds is not None: + for i in range(0, len(algoIds)): + request.append({ + 'algoId': algoIds[i], + 'instId': market['id'], + }) + for i in range(0, len(ids)): + if trailing or trigger: + request.append({ + 'algoId': ids[i], + 'instId': market['id'], + }) + else: + request.append({ + 'ordId': ids[i], + 'instId': market['id'], + }) + else: + for i in range(0, len(clientOrderIds)): + if trailing or trigger: + request.append({ + 'instId': market['id'], + 'algoClOrdId': clientOrderIds[i], + }) + else: + request.append({ + 'instId': market['id'], + 'clOrdId': clientOrderIds[i], + }) + response = None + if method == 'privatePostTradeCancelAlgos': + response = await self.privatePostTradeCancelAlgos(request) # * dont self.extend with params, otherwise ARRAY will be turned into OBJECT + else: + response = await self.privatePostTradeCancelBatchOrders(request) # * dont self.extend with params, otherwise ARRAY will be turned into OBJECT + # + # { + # "code": "0", + # "data": [ + # { + # "clOrdId": "e123456789ec4dBC1123456ba123b45e", + # "ordId": "405071912345641543", + # "sCode": "0", + # "sMsg": "" + # }, + # ... + # ], + # "msg": "" + # } + # + # Algo order + # + # { + # "code": "0", + # "data": [ + # { + # "algoId": "431375349042380800", + # "sCode": "0", + # "sMsg": "" + # } + # ], + # "msg": "" + # } + # + ordersData = self.safe_list(response, 'data', []) + return self.parse_orders(ordersData, market, None, None, params) + + async def cancel_orders_for_symbols(self, orders: List[CancellationRequest], params={}): + """ + cancel multiple orders for multiple symbols + + https://www.okx.com/docs-v5/en/#order-book-trading-trade-post-cancel-multiple-orders + https://www.okx.com/docs-v5/en/#order-book-trading-algo-trading-post-cancel-algo-order + + :param CancellationRequest[] orders: each order should contain the parameters required by cancelOrder namely id and symbol, example [{"id": "a", "symbol": "BTC/USDT"}, {"id": "b", "symbol": "ETH/USDT"}] + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: whether the order is a stop/trigger order + :param boolean [params.trailing]: set to True if you want to cancel trailing orders + :returns dict: an list of `order structures ` + """ + await self.load_markets() + request = [] + options = self.safe_dict(self.options, 'cancelOrders', {}) + defaultMethod = self.safe_string(options, 'method', 'privatePostTradeCancelBatchOrders') + method = self.safe_string(params, 'method', defaultMethod) + trigger = self.safe_bool_2(params, 'stop', 'trigger') + trailing = self.safe_bool(params, 'trailing', False) + isStopOrTrailing = trigger or trailing + if isStopOrTrailing: + method = 'privatePostTradeCancelAlgos' + for i in range(0, len(orders)): + order = orders[i] + id = self.safe_string(order, 'id') + clientOrderId = self.safe_string_2(order, 'clOrdId', 'clientOrderId') + symbol = self.safe_string(order, 'symbol') + market = self.market(symbol) + idKey = 'ordId' + if isStopOrTrailing: + idKey = 'algoId' + elif clientOrderId is not None: + if isStopOrTrailing: + idKey = 'algoClOrdId' + else: + idKey = 'clOrdId' + requestItem: dict = { + 'instId': market['id'], + } + requestItem[idKey] = clientOrderId if (clientOrderId is not None) else id + request.append(requestItem) + response = None + if method == 'privatePostTradeCancelAlgos': + response = await self.privatePostTradeCancelAlgos(request) # * dont self.extend with params, otherwise ARRAY will be turned into OBJECT + else: + response = await self.privatePostTradeCancelBatchOrders(request) # * dont self.extend with params, otherwise ARRAY will be turned into OBJECT + # + # { + # "code": "0", + # "data": [ + # { + # "clOrdId": "e123456789ec4dBC1123456ba123b45e", + # "ordId": "405071912345641543", + # "sCode": "0", + # "sMsg": "" + # }, + # ... + # ], + # "msg": "" + # } + # + # Algo order + # + # { + # "code": "0", + # "data": [ + # { + # "algoId": "431375349042380800", + # "sCode": "0", + # "sMsg": "" + # } + # ], + # "msg": "" + # } + # + ordersData = self.safe_list(response, 'data', []) + return self.parse_orders(ordersData, None, None, None, params) + + async def cancel_all_orders_after(self, timeout: Int, params={}): + """ + dead man's switch, cancel all orders after the given timeout + + https://www.okx.com/docs-v5/en/#order-book-trading-trade-post-cancel-all-after + + :param number timeout: time in milliseconds, 0 represents cancel the timer + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: the api result + """ + await self.load_markets() + request: dict = { + 'timeOut': self.parse_to_int(timeout / 1000) if (timeout > 0) else 0, + } + response = await self.privatePostTradeCancelAllAfter(self.extend(request, params)) + # + # { + # "code":"0", + # "msg":"", + # "data":[ + # { + # "triggerTime":"1587971460", + # "ts":"1587971400" + # } + # ] + # } + # + return response + + def parse_order_status(self, status: Str): + statuses: dict = { + 'canceled': 'canceled', + 'order_failed': 'canceled', + 'live': 'open', + 'partially_filled': 'open', + 'filled': 'closed', + 'effective': 'closed', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder + # + # { + # "clOrdId": "oktswap6", + # "ordId": "312269865356374016", + # "tag": "", + # "sCode": "0", + # "sMsg": "" + # } + # + # editOrder + # + # { + # "clOrdId": "e847386590ce4dBCc1a045253497a547", + # "ordId": "559176536793178112", + # "reqId": "", + # "sCode": "0", + # "sMsg": "" + # } + # + # Spot and Swap fetchOrder, fetchOpenOrders + # + # { + # "accFillSz": "0", + # "avgPx": "", + # "cTime": "1621910749815", + # "category": "normal", + # "ccy": "", + # "clOrdId": "", + # "fee": "0", + # "feeCcy": "ETH", + # "fillPx": "", + # "fillSz": "0", + # "fillTime": "", + # "instId": "ETH-USDT", + # "instType": "SPOT", + # "lever": "", + # "ordId": "317251910906576896", + # "ordType": "limit", + # "pnl": "0", + # "posSide": "net", + # "px": "2000", + # "rebate": "0", + # "rebateCcy": "USDT", + # "side": "buy", + # "slOrdPx": "", + # "slTriggerPx": "", + # "state": "live", + # "sz": "0.001", + # "tag": "", + # "tdMode": "cash", + # "tpOrdPx": "", + # "tpTriggerPx": "", + # "tradeId": "", + # "uTime": "1621910749815" + # } + # + # watchOrders & fetchClosedOrders + # + # { + # "algoClOrdId": "", + # "algoId": "", + # "attachAlgoClOrdId": "", + # "attachAlgoOrds": [], + # "cancelSource": "", + # "cancelSourceReason": "", # not present in WS, but present in fetchClosedOrders + # "category": "normal", + # "ccy": "", # empty in WS, but eg. `USDT` in fetchClosedOrders + # "clOrdId": "", + # "cTime": "1751705801423", + # "feeCcy": "USDT", + # "instId": "LINK-USDT-SWAP", + # "instType": "SWAP", + # "isTpLimit": "false", + # "lever": "3", + # "linkedAlgoOrd": {"algoId": ""}, + # "ordId": "2657625147249614848", + # "ordType": "limit", + # "posSide": "net", + # "px": "13.142", + # "pxType": "", + # "pxUsd": "", + # "pxVol": "", + # "quickMgnType": "", + # "rebate": "0", + # "rebateCcy": "USDT", + # "reduceOnly": "true", + # "side": "sell", + # "slOrdPx": "", + # "slTriggerPx": "", + # "slTriggerPxType": "", + # "source": "", + # "stpId": "", + # "stpMode": "cancel_maker", + # "sz": "0.1", + # "tag": "", + # "tdMode": "isolated", + # "tgtCcy": "", + # "tpOrdPx": "", + # "tpTriggerPx": "", + # "tpTriggerPxType": "", + # "uTime": "1751705807467", + # "reqId": "", # field present only in WS + # "msg": "", # field present only in WS + # "amendResult": "", # field present only in WS + # "amendSource": "", # field present only in WS + # "code": "0", # field present only in WS + # "fillFwdPx": "", # field present only in WS + # "fillMarkVol": "", # field present only in WS + # "fillPxUsd": "", # field present only in WS + # "fillPxVol": "", # field present only in WS + # "lastPx": "13.142", # field present only in WS + # "notionalUsd": "1.314515408", # field present only in WS + # + # #### these below fields are empty on first omit from websocket, because of "creation" event. however, if order is executed, it also immediately sends another update with these fields filled ### + # + # "pnl": "-0.0001", + # "accFillSz": "0.1", + # "avgPx": "13.142", + # "state": "filled", + # "fee": "-0.00026284", + # "fillPx": "13.142", + # "tradeId": "293429690", + # "fillSz": "0.1", + # "fillTime": "1751705807467", + # "fillNotionalUsd": "1.314515408", # field present only in WS + # "fillPnl": "-0.0001", # field present only in WS + # "fillFee": "-0.00026284", # field present only in WS + # "fillFeeCcy": "USDT", # field present only in WS + # "execType": "M", # field present only in WS + # "fillMarkPx": "13.141", # field present only in WS + # "fillIdxPx": "13.147" # field present only in WS + # } + # + # + # Algo Order fetchOpenOrders, fetchCanceledOrders, fetchClosedOrders + # + # { + # "activePx": "", + # "activePxType": "", + # "actualPx": "", + # "actualSide": "buy", + # "actualSz": "0", + # "algoId": "431375349042380800", + # "cTime": "1649119897778", + # "callbackRatio": "", + # "callbackSpread": "", + # "ccy": "", + # "ctVal": "0.01", + # "instId": "BTC-USDT-SWAP", + # "instType": "SWAP", + # "last": "46538.9", + # "lever": "125", + # "moveTriggerPx": "", + # "notionalUsd": "467.059", + # "ordId": "", + # "ordPx": "50000", + # "ordType": "trigger", + # "posSide": "long", + # "pxLimit": "", + # "pxSpread": "", + # "pxVar": "", + # "side": "buy", + # "slOrdPx": "", + # "slTriggerPx": "", + # "slTriggerPxType": "", + # "state": "live", + # "sz": "1", + # "szLimit": "", + # "tag": "", + # "tdMode": "isolated", + # "tgtCcy": "", + # "timeInterval": "", + # "tpOrdPx": "", + # "tpTriggerPx": "", + # "tpTriggerPxType": "", + # "triggerPx": "50000", + # "triggerPxType": "last", + # "triggerTime": "", + # "uly": "BTC-USDT" + # } + # + scode = self.safe_string(order, 'sCode') + if (scode is not None) and (scode != '0'): + return self.safe_order({ + 'id': self.safe_string(order, 'ordId'), + 'clientOrderId': self.safe_string(order, 'clOrdId'), + 'status': 'rejected', + 'info': order, + }) + id = self.safe_string_2(order, 'algoId', 'ordId') + timestamp = self.safe_integer(order, 'cTime') + lastUpdateTimestamp = self.safe_integer(order, 'uTime') + lastTradeTimestamp = self.safe_integer(order, 'fillTime') + side = self.safe_string(order, 'side') + type = self.safe_string(order, 'ordType') + postOnly = None + timeInForce = None + if type == 'post_only': + postOnly = True + type = 'limit' + elif type == 'fok': + timeInForce = 'FOK' + type = 'limit' + elif type == 'ioc': + timeInForce = 'IOC' + type = 'limit' + marketId = self.safe_string(order, 'instId') + market = self.safe_market(marketId, market) + symbol = self.safe_symbol(marketId, market, '-') + filled = self.safe_string(order, 'accFillSz') + price = self.safe_string_2(order, 'px', 'ordPx') + average = self.safe_string(order, 'avgPx') + status = self.parse_order_status(self.safe_string(order, 'state')) + feeCostString = self.safe_string(order, 'fee') + amount = None + cost = None + # spot market buy: "sz" can refer either to base currency units or to quote currency units + # see documentation: https://www.okx.com/docs-v5/en/#rest-api-trade-place-order + defaultTgtCcy = self.safe_string(self.options, 'tgtCcy', 'base_ccy') + tgtCcy = self.safe_string(order, 'tgtCcy', defaultTgtCcy) + instType = self.safe_string(order, 'instType') + if (side == 'buy') and (type == 'market') and (instType == 'SPOT') and (tgtCcy == 'quote_ccy'): + # "sz" refers to the cost + cost = self.safe_string(order, 'sz') + else: + # "sz" refers to the trade currency amount + amount = self.safe_string(order, 'sz') + fee = None + if feeCostString is not None: + feeCostSigned = Precise.string_neg(feeCostString) + feeCurrencyId = self.safe_string(order, 'feeCcy') + feeCurrencyCode = self.safe_currency_code(feeCurrencyId) + fee = { + 'cost': self.parse_number(feeCostSigned), + 'currency': feeCurrencyCode, + } + clientOrderId = self.safe_string(order, 'clOrdId') + if (clientOrderId is not None) and (len(clientOrderId) < 1): + clientOrderId = None # fix empty clientOrderId string + stopLossPrice = self.safe_number_2(order, 'slTriggerPx', 'slOrdPx') + takeProfitPrice = self.safe_number_2(order, 'tpTriggerPx', 'tpOrdPx') + reduceOnlyRaw = self.safe_string(order, 'reduceOnly') + reduceOnly = False + if reduceOnly is not None: + reduceOnly = (reduceOnlyRaw == 'true') + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': lastTradeTimestamp, + 'lastUpdateTimestamp': lastUpdateTimestamp, + 'symbol': symbol, + 'type': type, + 'timeInForce': timeInForce, + 'postOnly': postOnly, + 'side': side, + 'price': price, + 'stopLossPrice': stopLossPrice, + 'takeProfitPrice': takeProfitPrice, + 'triggerPrice': self.safe_number_n(order, ['triggerPx', 'moveTriggerPx']), + 'average': average, + 'cost': cost, + 'amount': amount, + 'filled': filled, + 'remaining': None, + 'status': status, + 'fee': fee, + 'trades': None, + 'reduceOnly': reduceOnly, + }, market) + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetch an order by the id + + https://www.okx.com/docs-v5/en/#order-book-trading-trade-get-order-details + https://www.okx.com/docs-v5/en/#order-book-trading-algo-trading-get-algo-order-details + + :param str id: the order id + :param str symbol: unified market symbol + :param dict [params]: extra and exchange specific parameters + :param boolean [params.trigger]: True if fetching trigger orders + :returns: `an order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrder() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + # 'clOrdId': 'abcdef12345', # optional, [a-z0-9]{1,32} + # 'ordId': id, + # 'instType': # spot, swap, futures, margin + } + clientOrderId = self.safe_string_2(params, 'clOrdId', 'clientOrderId') + options = self.safe_value(self.options, 'fetchOrder', {}) + defaultMethod = self.safe_string(options, 'method', 'privateGetTradeOrder') + method = self.safe_string(params, 'method', defaultMethod) + trigger = self.safe_value_2(params, 'stop', 'trigger') + if trigger: + method = 'privateGetTradeOrderAlgo' + if clientOrderId is not None: + request['algoClOrdId'] = clientOrderId + else: + request['algoId'] = id + else: + if clientOrderId is not None: + request['clOrdId'] = clientOrderId + else: + request['ordId'] = id + query = self.omit(params, ['method', 'clOrdId', 'clientOrderId', 'stop', 'trigger']) + response = None + if method == 'privateGetTradeOrderAlgo': + response = await self.privateGetTradeOrderAlgo(self.extend(request, query)) + else: + response = await self.privateGetTradeOrder(self.extend(request, query)) + # + # Spot and Swap + # + # { + # "code": "0", + # "data": [ + # { + # "accFillSz": "0", + # "avgPx": "", + # "cTime": "1621910749815", + # "category": "normal", + # "ccy": "", + # "clOrdId": "", + # "fee": "0", + # "feeCcy": "ETH", + # "fillPx": "", + # "fillSz": "0", + # "fillTime": "", + # "instId": "ETH-USDT", + # "instType": "SPOT", + # "lever": "", + # "ordId": "317251910906576896", + # "ordType": "limit", + # "pnl": "0", + # "posSide": "net", + # "px":"20 00", + # "rebate": "0", + # "rebateCcy": "USDT", + # "side": "buy", + # "slOrdPx": "", + # "slTriggerPx": "", + # "state": "live", + # "sz":"0. 001", + # "tag": "", + # "tdMode": "cash", + # "tpOrdPx": "", + # "tpTriggerPx": "", + # "tradeId": "", + # "uTime": "1621910749815" + # } + # ], + # "msg": "" + # } + # + # Algo order + # { + # "code":"0", + # "msg":"", + # "data":[ + # { + # "instType":"FUTURES", + # "instId":"BTC-USD-200329", + # "ordId":"123445", + # "ccy":"BTC", + # "clOrdId":"", + # "algoId":"1234", + # "sz":"999", + # "closeFraction":"", + # "ordType":"oco", + # "side":"buy", + # "posSide":"long", + # "tdMode":"cross", + # "tgtCcy": "", + # "state":"effective", + # "lever":"20", + # "tpTriggerPx":"", + # "tpTriggerPxType":"", + # "tpOrdPx":"", + # "slTriggerPx":"", + # "slTriggerPxType":"", + # "triggerPx":"99", + # "triggerPxType":"last", + # "ordPx":"12", + # "actualSz":"", + # "actualPx":"", + # "actualSide":"", + # "pxVar":"", + # "pxSpread":"", + # "pxLimit":"", + # "szLimit":"", + # "tag": "adadadadad", + # "timeInterval":"", + # "callbackRatio":"", + # "callbackSpread":"", + # "activePx":"", + # "moveTriggerPx":"", + # "reduceOnly": "false", + # "triggerTime":"1597026383085", + # "last": "16012", + # "failCode": "", + # "algoClOrdId": "", + # "cTime":"1597026383000" + # } + # ] + # } + # + data = self.safe_value(response, 'data', []) + order = self.safe_dict(data, 0) + return self.parse_order(order, market) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://www.okx.com/docs-v5/en/#order-book-trading-trade-get-order-list + https://www.okx.com/docs-v5/en/#order-book-trading-algo-trading-get-algo-order-list + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.trigger]: True if fetching trigger or conditional orders + :param str [params.ordType]: "conditional", "oco", "trigger", "move_order_stop", "iceberg", or "twap" + :param str [params.algoId]: Algo ID "'433845797218942976'" + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param boolean [params.trailing]: set to True if you want to fetch trailing orders + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOpenOrders', 'paginate') + if paginate: + return await self.fetch_paginated_call_dynamic('fetchOpenOrders', symbol, since, limit, params) + request: dict = { + # 'instType': 'SPOT', # SPOT, MARGIN, SWAP, FUTURES, OPTION + # 'uly': currency['id'], + # 'instId': market['id'], + # 'ordType': 'limit', # market, limit, post_only, fok, ioc, comma-separated, stop orders: conditional, oco, trigger, move_order_stop, iceberg, or twap + # 'state': 'live', # live, partially_filled + # 'after': orderId, + # 'before': orderId, + # 'limit': limit, # default 100, max 100 + } + market = None + if symbol is not None: + market = self.market(symbol) + request['instId'] = market['id'] + if limit is not None: + request['limit'] = limit # default 100, max 100 + options = self.safe_value(self.options, 'fetchOpenOrders', {}) + algoOrderTypes = self.safe_value(self.options, 'algoOrderTypes', {}) + defaultMethod = self.safe_string(options, 'method', 'privateGetTradeOrdersPending') + method = self.safe_string(params, 'method', defaultMethod) + ordType = self.safe_string(params, 'ordType') + trigger = self.safe_value_2(params, 'stop', 'trigger') + trailing = self.safe_bool(params, 'trailing', False) + if trailing or trigger or (ordType in algoOrderTypes): + method = 'privateGetTradeOrdersAlgoPending' + if trailing: + request['ordType'] = 'move_order_stop' + elif trigger and (ordType is None): + request['ordType'] = 'trigger' + query = self.omit(params, ['method', 'stop', 'trigger', 'trailing']) + response = None + if method == 'privateGetTradeOrdersAlgoPending': + response = await self.privateGetTradeOrdersAlgoPending(self.extend(request, query)) + else: + response = await self.privateGetTradeOrdersPending(self.extend(request, query)) + # + # { + # "code": "0", + # "data": [ + # { + # "accFillSz": "0", + # "avgPx": "", + # "cTime": "1621910749815", + # "category": "normal", + # "ccy": "", + # "clOrdId": "", + # "fee": "0", + # "feeCcy": "ETH", + # "fillPx": "", + # "fillSz": "0", + # "fillTime": "", + # "instId": "ETH-USDT", + # "instType": "SPOT", + # "lever": "", + # "ordId": "317251910906576896", + # "ordType": "limit", + # "pnl": "0", + # "posSide": "net", + # "px":"20 00", + # "rebate": "0", + # "rebateCcy": "USDT", + # "side": "buy", + # "slOrdPx": "", + # "slTriggerPx": "", + # "state": "live", + # "sz":"0. 001", + # "tag": "", + # "tdMode": "cash", + # "tpOrdPx": "", + # "tpTriggerPx": "", + # "tradeId": "", + # "uTime": "1621910749815" + # } + # ], + # "msg":"" + # } + # + # Algo order + # + # { + # "code": "0", + # "data": [ + # { + # "activePx": "", + # "activePxType": "", + # "actualPx": "", + # "actualSide": "buy", + # "actualSz": "0", + # "algoId": "431375349042380800", + # "cTime": "1649119897778", + # "callbackRatio": "", + # "callbackSpread": "", + # "ccy": "", + # "ctVal": "0.01", + # "instId": "BTC-USDT-SWAP", + # "instType": "SWAP", + # "last": "46538.9", + # "lever": "125", + # "moveTriggerPx": "", + # "notionalUsd": "467.059", + # "ordId": "", + # "ordPx": "50000", + # "ordType": "trigger", + # "posSide": "long", + # "pxLimit": "", + # "pxSpread": "", + # "pxVar": "", + # "side": "buy", + # "slOrdPx": "", + # "slTriggerPx": "", + # "slTriggerPxType": "", + # "state": "live", + # "sz": "1", + # "szLimit": "", + # "tag": "", + # "tdMode": "isolated", + # "tgtCcy": "", + # "timeInterval": "", + # "tpOrdPx": "", + # "tpTriggerPx": "", + # "tpTriggerPxType": "", + # "triggerPx": "50000", + # "triggerPxType": "last", + # "triggerTime": "", + # "uly": "BTC-USDT" + # } + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_orders(data, market, since, limit) + + async def fetch_canceled_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches information on multiple canceled orders made by the user + + https://www.okx.com/docs-v5/en/#order-book-trading-trade-get-order-history-last-7-days + https://www.okx.com/docs-v5/en/#order-book-trading-algo-trading-get-algo-order-history + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: timestamp in ms of the earliest order, default is None + :param int [limit]: max number of orders to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.trigger]: True if fetching trigger or conditional orders + :param str [params.ordType]: "conditional", "oco", "trigger", "move_order_stop", "iceberg", or "twap" + :param str [params.algoId]: Algo ID "'433845797218942976'" + :param int [params.until]: timestamp in ms to fetch orders for + :param boolean [params.trailing]: set to True if you want to fetch trailing orders + :returns dict: a list of `order structures ` + """ + await self.load_markets() + request: dict = { + # 'instType': type.upper(), # SPOT, MARGIN, SWAP, FUTURES, OPTION + # 'uly': currency['id'], + # 'instId': market['id'], + # 'ordType': 'limit', # market, limit, post_only, fok, ioc, comma-separated stop orders: conditional, oco, trigger, move_order_stop, iceberg, or twap + # 'state': 'canceled', # filled, canceled + # 'after': orderId, + # 'before': orderId, + # 'limit': limit, # default 100, max 100 + # 'algoId': "'433845797218942976'", # Algo order + } + market = None + if symbol is not None: + market = self.market(symbol) + request['instId'] = market['id'] + type = None + query = None + type, query = self.handle_market_type_and_params('fetchCanceledOrders', market, params) + request['instType'] = self.convert_to_instrument_type(type) + if limit is not None: + request['limit'] = limit # default 100, max 100 + request['state'] = 'canceled' + options = self.safe_value(self.options, 'fetchCanceledOrders', {}) + algoOrderTypes = self.safe_value(self.options, 'algoOrderTypes', {}) + defaultMethod = self.safe_string(options, 'method', 'privateGetTradeOrdersHistory') + method = self.safe_string(params, 'method', defaultMethod) + ordType = self.safe_string(params, 'ordType') + trigger = self.safe_value_2(params, 'stop', 'trigger') + trailing = self.safe_bool(params, 'trailing', False) + if trailing: + method = 'privateGetTradeOrdersAlgoHistory' + request['ordType'] = 'move_order_stop' + elif trigger or (ordType in algoOrderTypes): + method = 'privateGetTradeOrdersAlgoHistory' + algoId = self.safe_string(params, 'algoId') + if algoId is not None: + request['algoId'] = algoId + params = self.omit(params, 'algoId') + if trigger: + if ordType is None: + raise ArgumentsRequired(self.id + ' fetchCanceledOrders() requires an "ordType" string parameter, "conditional", "oco", "trigger", "move_order_stop", "iceberg", or "twap"') + else: + if since is not None: + request['begin'] = since + until = self.safe_integer(query, 'until') + if until is not None: + request['end'] = until + query = self.omit(query, ['until']) + send = self.omit(query, ['method', 'stop', 'trigger', 'trailing']) + response = None + if method == 'privateGetTradeOrdersAlgoHistory': + response = await self.privateGetTradeOrdersAlgoHistory(self.extend(request, send)) + else: + response = await self.privateGetTradeOrdersHistory(self.extend(request, send)) + # + # { + # "code": "0", + # "data": [ + # { + # "accFillSz": "0", + # "avgPx": "", + # "cTime": "1644037822494", + # "category": "normal", + # "ccy": "", + # "clOrdId": "", + # "fee": "0", + # "feeCcy": "BTC", + # "fillPx": "", + # "fillSz": "0", + # "fillTime": "", + # "instId": "BTC-USDT", + # "instType": "SPOT", + # "lever": "", + # "ordId": "410059580352409602", + # "ordType": "limit", + # "pnl": "0", + # "posSide": "net", + # "px": "30000", + # "rebate": "0", + # "rebateCcy": "USDT", + # "side": "buy", + # "slOrdPx": "", + # "slTriggerPx": "", + # "slTriggerPxType": "", + # "source": "", + # "state": "canceled", + # "sz": "0.0005452", + # "tag": "", + # "tdMode": "cash", + # "tgtCcy": "", + # "tpOrdPx": "", + # "tpTriggerPx": "", + # "tpTriggerPxType": "", + # "tradeId": "", + # "uTime": "1644038165667" + # } + # ], + # "msg": "" + # } + # + # Algo order + # + # { + # "code": "0", + # "data": [ + # { + # "activePx": "", + # "activePxType": "", + # "actualPx": "", + # "actualSide": "buy", + # "actualSz": "0", + # "algoId": "433845797218942976", + # "cTime": "1649708898523", + # "callbackRatio": "", + # "callbackSpread": "", + # "ccy": "", + # "ctVal": "0.01", + # "instId": "BTC-USDT-SWAP", + # "instType": "SWAP", + # "last": "39950.4", + # "lever": "125", + # "moveTriggerPx": "", + # "notionalUsd": "1592.1760000000002", + # "ordId": "", + # "ordPx": "29000", + # "ordType": "trigger", + # "posSide": "long", + # "pxLimit": "", + # "pxSpread": "", + # "pxVar": "", + # "side": "buy", + # "slOrdPx": "", + # "slTriggerPx": "", + # "slTriggerPxType": "", + # "state": "canceled", + # "sz": "4", + # "szLimit": "", + # "tag": "", + # "tdMode": "isolated", + # "tgtCcy": "", + # "timeInterval": "", + # "tpOrdPx": "", + # "tpTriggerPx": "", + # "tpTriggerPxType": "", + # "triggerPx": "30000", + # "triggerPxType": "last", + # "triggerTime": "", + # "uly": "BTC-USDT" + # }, + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_orders(data, market, since, limit) + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://www.okx.com/docs-v5/en/#order-book-trading-trade-get-order-history-last-7-days + https://www.okx.com/docs-v5/en/#order-book-trading-algo-trading-get-algo-order-history + https://www.okx.com/docs-v5/en/#order-book-trading-trade-get-order-history-last-3-months + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.trigger]: True if fetching trigger or conditional orders + :param str [params.ordType]: "conditional", "oco", "trigger", "move_order_stop", "iceberg", or "twap" + :param str [params.algoId]: Algo ID "'433845797218942976'" + :param int [params.until]: timestamp in ms to fetch orders for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param str [params.method]: method to be used, either 'privateGetTradeOrdersHistory', 'privateGetTradeOrdersHistoryArchive' or 'privateGetTradeOrdersAlgoHistory' default is 'privateGetTradeOrdersHistory' + :param boolean [params.trailing]: set to True if you want to fetch trailing orders + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchClosedOrders', 'paginate') + if paginate: + return await self.fetch_paginated_call_dynamic('fetchClosedOrders', symbol, since, limit, params) + request: dict = { + # 'instType': type.upper(), # SPOT, MARGIN, SWAP, FUTURES, OPTION + # 'uly': currency['id'], + # 'instId': market['id'], + # 'ordType': 'limit', # market, limit, post_only, fok, ioc, comma-separated stop orders: conditional, oco, trigger, move_order_stop, iceberg, or twap + # 'state': 'filled', # filled, effective + # 'after': orderId, + # 'before': orderId, + # 'limit': limit, # default 100, max 100 + # 'algoId': "'433845797218942976'", # Algo order + } + market = None + if symbol is not None: + market = self.market(symbol) + request['instId'] = market['id'] + type = None + query = None + type, query = self.handle_market_type_and_params('fetchClosedOrders', market, params) + request['instType'] = self.convert_to_instrument_type(type) + if limit is not None: + request['limit'] = limit # default 100, max 100 + options = self.safe_dict(self.options, 'fetchClosedOrders', {}) + algoOrderTypes = self.safe_dict(self.options, 'algoOrderTypes', {}) + defaultMethod = self.safe_string(options, 'method', 'privateGetTradeOrdersHistory') + method = self.safe_string(params, 'method', defaultMethod) + ordType = self.safe_string(params, 'ordType') + trigger = self.safe_bool_2(params, 'stop', 'trigger') + trailing = self.safe_bool(params, 'trailing', False) + if trailing or trigger or (ordType in algoOrderTypes): + method = 'privateGetTradeOrdersAlgoHistory' + request['state'] = 'effective' + if trailing: + request['ordType'] = 'move_order_stop' + elif trigger: + if ordType is None: + request['ordType'] = 'trigger' + else: + if since is not None: + request['begin'] = since + until = self.safe_integer(query, 'until') + if until is not None: + request['end'] = until + query = self.omit(query, ['until']) + request['state'] = 'filled' + send = self.omit(query, ['method', 'stop', 'trigger', 'trailing']) + response = None + if method == 'privateGetTradeOrdersAlgoHistory': + response = await self.privateGetTradeOrdersAlgoHistory(self.extend(request, send)) + elif method == 'privateGetTradeOrdersHistoryArchive': + response = await self.privateGetTradeOrdersHistoryArchive(self.extend(request, send)) + else: + response = await self.privateGetTradeOrdersHistory(self.extend(request, send)) + # + # { + # "code": "0", + # "data": [ + # { + # "accFillSz": "0", + # "avgPx": "", + # "cTime": "1621910749815", + # "category": "normal", + # "ccy": "", + # "clOrdId": "", + # "fee": "0", + # "feeCcy": "ETH", + # "fillPx": "", + # "fillSz": "0", + # "fillTime": "", + # "instId": "ETH-USDT", + # "instType": "SPOT", + # "lever": "", + # "ordId": "317251910906576896", + # "ordType": "limit", + # "pnl": "0", + # "posSide": "net", + # "px": "2000", + # "rebate": "0", + # "rebateCcy": "USDT", + # "side": "buy", + # "slOrdPx": "", + # "slTriggerPx": "", + # "state": "live", + # "sz": "0.001", + # "tag": "", + # "tdMode": "cash", + # "tpOrdPx": "", + # "tpTriggerPx": "", + # "tradeId": "", + # "uTime": "1621910749815" + # } + # ], + # "msg": "" + # } + # + # Algo order + # + # { + # "code": "0", + # "data": [ + # { + # "activePx": "", + # "activePxType": "", + # "actualPx": "", + # "actualSide": "buy", + # "actualSz": "0", + # "algoId": "433845797218942976", + # "cTime": "1649708898523", + # "callbackRatio": "", + # "callbackSpread": "", + # "ccy": "", + # "ctVal": "0.01", + # "instId": "BTC-USDT-SWAP", + # "instType": "SWAP", + # "last": "39950.4", + # "lever": "125", + # "moveTriggerPx": "", + # "notionalUsd": "1592.1760000000002", + # "ordId": "", + # "ordPx": "29000", + # "ordType": "trigger", + # "posSide": "long", + # "pxLimit": "", + # "pxSpread": "", + # "pxVar": "", + # "side": "buy", + # "slOrdPx": "", + # "slTriggerPx": "", + # "slTriggerPxType": "", + # "state": "effective", + # "sz": "4", + # "szLimit": "", + # "tag": "", + # "tdMode": "isolated", + # "tgtCcy": "", + # "timeInterval": "", + # "tpOrdPx": "", + # "tpTriggerPx": "", + # "tpTriggerPxType": "", + # "triggerPx": "30000", + # "triggerPxType": "last", + # "triggerTime": "", + # "uly": "BTC-USDT" + # }, + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_orders(data, market, since, limit) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://www.okx.com/docs-v5/en/#order-book-trading-trade-get-transaction-details-last-3-months + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: Timestamp in ms of the latest time to retrieve trades for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchMyTrades', 'paginate') + if paginate: + return await self.fetch_paginated_call_dynamic('fetchMyTrades', symbol, since, limit, params) + request: dict = { + # 'instType': 'SPOT', # SPOT, MARGIN, SWAP, FUTURES, OPTION + # 'uly': currency['id'], + # 'instId': market['id'], + # 'ordId': orderId, + # 'after': billId, + # 'before': billId, + # 'limit': limit, # default 100, max 100 + } + market = None + if symbol is not None: + market = self.market(symbol) + request['instId'] = market['id'] + if since is not None: + request['begin'] = since + request, params = self.handle_until_option('end', request, params) + type, query = self.handle_market_type_and_params('fetchMyTrades', market, params) + request['instType'] = self.convert_to_instrument_type(type) + if (limit is not None) and (since is None): # limit = n, okx will return the n most recent results, instead of the n results after limit, so limit should only be sent when since is None + request['limit'] = limit # default 100, max 100 + response = await self.privateGetTradeFillsHistory(self.extend(request, query)) + # + # { + # "code": "0", + # "data": [ + # { + # "side": "buy", + # "fillSz": "0.007533", + # "fillPx": "2654.98", + # "fee": "-0.000007533", + # "ordId": "317321390244397056", + # "instType": "SPOT", + # "instId": "ETH-USDT", + # "clOrdId": "", + # "posSide": "net", + # "billId": "317321390265368576", + # "tag": "0", + # "execType": "T", + # "tradeId": "107601752", + # "feeCcy": "ETH", + # "ts": "1621927314985" + # } + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_trades(data, market, since, limit, query) + + async def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all the trades made from a single order + + https://www.okx.com/docs-v5/en/#order-book-trading-trade-get-transaction-details-last-3-months + + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + request: dict = { + # 'instrument_id': market['id'], + 'ordId': id, + # 'after': '1', # return the page after the specified page number + # 'before': '1', # return the page before the specified page number + # 'limit': limit, # optional, number of results per request, default = maximum = 100 + } + return await self.fetch_my_trades(symbol, since, limit, self.extend(request, params)) + + async def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered balance of the user + + https://www.okx.com/docs-v5/en/#rest-api-account-get-bills-details-last-7-days + https://www.okx.com/docs-v5/en/#rest-api-account-get-bills-details-last-3-months + https://www.okx.com/docs-v5/en/#rest-api-funding-asset-bills-details + + :param str [code]: unified currency code, default is None + :param int [since]: timestamp in ms of the earliest ledger entry, default is None + :param int [limit]: max number of ledger entries to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' + :param int [params.until]: the latest time in ms to fetch entries for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict: a `ledger structure ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchLedger', 'paginate') + if paginate: + return await self.fetch_paginated_call_dynamic('fetchLedger', code, since, limit, params) + options = self.safe_dict(self.options, 'fetchLedger', {}) + method = self.safe_string(options, 'method') + method = self.safe_string(params, 'method', method) + params = self.omit(params, 'method') + request: dict = { + # 'instType': None, # 'SPOT', 'MARGIN', 'SWAP', 'FUTURES", 'OPTION' + # 'ccy': None, # currency['id'], + # 'mgnMode': None, # 'isolated', 'cross' + # 'ctType': None, # 'linear', 'inverse', only applicable to FUTURES/SWAP + # 'type': varies depending the 'method' endpoint : + # - https://www.okx.com/docs-v5/en/#rest-api-account-get-bills-details-last-7-days + # - https://www.okx.com/docs-v5/en/#rest-api-funding-asset-bills-details + # - https://www.okx.com/docs-v5/en/#rest-api-account-get-bills-details-last-3-months + # 'after': 'id', # return records earlier than the requested bill id + # 'before': 'id', # return records newer than the requested bill id + # 'limit': 100, # default 100, max 100 + } + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchLedger', params) + if marginMode is None: + marginMode = self.safe_string(params, 'mgnMode') + if method != 'privateGetAssetBills': + if marginMode is not None: + request['mgnMode'] = marginMode + type, query = self.handle_market_type_and_params('fetchLedger', None, params) + if type is not None: + request['instType'] = self.convert_to_instrument_type(type) + if limit is not None: + request['limit'] = limit + currency = None + if code is not None: + currency = self.currency(code) + request['ccy'] = currency['id'] + request, params = self.handle_until_option('end', request, params) + response = None + if method == 'privateGetAccountBillsArchive': + response = await self.privateGetAccountBillsArchive(self.extend(request, query)) + elif method == 'privateGetAssetBills': + response = await self.privateGetAssetBills(self.extend(request, query)) + else: + response = await self.privateGetAccountBills(self.extend(request, query)) + # + # privateGetAccountBills, privateGetAccountBillsArchive + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "bal": "0.0000819307998198", + # "balChg": "-664.2679586599999802", + # "billId": "310394313544966151", + # "ccy": "USDT", + # "fee": "0", + # "from": "", + # "instId": "LTC-USDT", + # "instType": "SPOT", + # "mgnMode": "cross", + # "notes": "", + # "ordId": "310394313519800320", + # "pnl": "0", + # "posBal": "0", + # "posBalChg": "0", + # "subType": "2", + # "sz": "664.26795866", + # "to": "", + # "ts": "1620275771196", + # "type": "2" + # } + # ] + # } + # + # privateGetAssetBills + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "billId": "12344", + # "ccy": "BTC", + # "balChg": "2", + # "bal": "12", + # "type": "1", + # "ts": "1597026383085" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_ledger(data, currency, since, limit) + + def parse_ledger_entry_type(self, type): + types: dict = { + '1': 'transfer', # transfer + '2': 'trade', # trade + '3': 'trade', # delivery + '4': 'rebate', # auto token conversion + '5': 'trade', # liquidation + '6': 'transfer', # margin transfer + '7': 'trade', # interest deduction + '8': 'fee', # funding rate + '9': 'trade', # adl + '10': 'trade', # clawback + '11': 'trade', # system token conversion + } + return self.safe_string(types, type, type) + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + # + # privateGetAccountBills, privateGetAccountBillsArchive + # + # { + # "bal": "0.0000819307998198", + # "balChg": "-664.2679586599999802", + # "billId": "310394313544966151", + # "ccy": "USDT", + # "fee": "0", + # "from": "", + # "instId": "LTC-USDT", + # "instType": "SPOT", + # "mgnMode": "cross", + # "notes": "", + # "ordId": "310394313519800320", + # "pnl": "0", + # "posBal": "0", + # "posBalChg": "0", + # "subType": "2", + # "sz": "664.26795866", + # "to": "", + # "ts": "1620275771196", + # "type": "2" + # } + # + # privateGetAssetBills + # + # { + # "billId": "12344", + # "ccy": "BTC", + # "balChg": "2", + # "bal": "12", + # "type": "1", + # "ts": "1597026383085" + # } + # + currencyId = self.safe_string(item, 'ccy') + code = self.safe_currency_code(currencyId, currency) + currency = self.safe_currency(currencyId, currency) + timestamp = self.safe_integer(item, 'ts') + feeCostString = self.safe_string(item, 'fee') + fee = None + if feeCostString is not None: + fee = { + 'cost': self.parse_number(Precise.string_neg(feeCostString)), + 'currency': code, + } + marketId = self.safe_string(item, 'instId') + symbol = self.safe_symbol(marketId, None, '-') + return self.safe_ledger_entry({ + 'info': item, + 'id': self.safe_string(item, 'billId'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'account': None, + 'referenceId': self.safe_string(item, 'ordId'), + 'referenceAccount': None, + 'type': self.parse_ledger_entry_type(self.safe_string(item, 'type')), + 'currency': code, + 'symbol': symbol, + 'amount': self.safe_number(item, 'balChg'), + 'before': None, + 'after': self.safe_number(item, 'bal'), + 'status': 'ok', + 'fee': fee, + }, currency) + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + # + # { + # "addr": "okbtothemoon", + # "memo": "971668", # may be missing + # "tag":"52055", # may be missing + # "pmtId": "", # may be missing + # "ccy": "BTC", + # "to": "6", # 1 SPOT, 3 FUTURES, 6 FUNDING, 9 SWAP, 12 OPTION, 18 Unified account + # "selected": True + # } + # + # { + # "ccy":"usdt-erc20", + # "to":"6", + # "addr":"0x696abb81974a8793352cbd33aadcf78eda3cfdfa", + # "selected":true + # } + # + # { + # "chain": "ETH-OKExChain", + # "addrEx": {"comment": "6040348"}, # some currencies like TON may have self field, + # "ctAddr": "72315c", + # "ccy": "ETH", + # "to": "6", + # "addr": "0x1c9f2244d1ccaa060bd536827c18925db10db102", + # "selected": True + # } + # + address = self.safe_string(depositAddress, 'addr') + tag = self.safe_string_n(depositAddress, ['tag', 'pmtId', 'memo']) + if tag is None: + addrEx = self.safe_value(depositAddress, 'addrEx', {}) + tag = self.safe_string(addrEx, 'comment') + currencyId = self.safe_string(depositAddress, 'ccy') + currency = self.safe_currency(currencyId, currency) + code = currency['code'] + chain = self.safe_string(depositAddress, 'chain') + networks = self.safe_value(currency, 'networks', {}) + networksById = self.index_by(networks, 'id') + networkData = self.safe_value(networksById, chain) + # inconsistent naming responses from exchange + # with respect to network naming provided in currency info vs address chain-names and ids + # + # response from address endpoint: + # { + # "chain": "USDT-Polygon", + # "ctAddr": "", + # "ccy": "USDT", + # "to":"6" , + # "addr": "0x1903441e386cc49d937f6302955b5feb4286dcfa", + # "selected": True + # } + # network information from currency['networks'] field: + # Polygon: { + # info: { + # canDep: False, + # canInternal: False, + # canWd: False, + # ccy: 'USDT', + # chain: 'USDT-Polygon-Bridge', + # mainNet: False, + # maxFee: '26.879528', + # minFee: '13.439764', + # minWd: '0.001', + # name: '' + # }, + # id: 'USDT-Polygon-Bridge', + # network: 'Polygon', + # active: False, + # deposit: False, + # withdraw: False, + # fee: 13.439764, + # precision: None, + # limits: { + # withdraw: { + # min: 0.001, + # max: None + # } + # } + # }, + # + if chain == 'USDT-Polygon': + networkData = self.safe_value_2(networksById, 'USDT-Polygon-Bridge', 'USDT-Polygon') + network = self.safe_string(networkData, 'network') + networkCode = self.network_id_to_code(network, code) + self.check_address(address) + return { + 'info': depositAddress, + 'currency': code, + 'network': networkCode, + 'address': address, + 'tag': tag, + } + + async def fetch_deposit_addresses_by_network(self, code: str, params={}) -> List[DepositAddress]: + """ + fetch a dictionary of addresses for a currency, indexed by network + + https://www.okx.com/docs-v5/en/#funding-account-rest-api-get-deposit-address + + :param str code: unified currency code of the currency for the deposit address + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `address structures ` indexed by the network + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'ccy': currency['id'], + } + response = await self.privateGetAssetDepositAddress(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "addr": "okbtothemoon", + # "memo": "971668", # may be missing + # "tag":"52055", # may be missing + # "pmtId": "", # may be missing + # "ccy": "BTC", + # "to": "6", # 1 SPOT, 3 FUTURES, 6 FUNDING, 9 SWAP, 12 OPTION, 18 Unified account + # "selected": True + # }, + # # {"ccy":"usdt-erc20","to":"6","addr":"0x696abb81974a8793352cbd33aadcf78eda3cfdfa","selected":true}, + # # {"ccy":"usdt-trc20","to":"6","addr":"TRrd5SiSZrfQVRKm4e9SRSbn2LNTYqCjqx","selected":true}, + # # {"ccy":"usdt_okexchain","to":"6","addr":"0x696abb81974a8793352cbd33aadcf78eda3cfdfa","selected":true}, + # # {"ccy":"usdt_kip20","to":"6","addr":"0x696abb81974a8793352cbd33aadcf78eda3cfdfa","selected":true}, + # ] + # } + # + data = self.safe_list(response, 'data', []) + filtered = self.filter_by(data, 'selected', True) + parsed = self.parse_deposit_addresses(filtered, [currency['code']], False) + return self.index_by(parsed, 'network') + + async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://www.okx.com/docs-v5/en/#funding-account-rest-api-get-deposit-address + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.network]: the network name for the deposit address + :returns dict: an `address structure ` + """ + await self.load_markets() + rawNetwork = self.safe_string(params, 'network') # some networks are like "Dora Vota Mainnet" + params = self.omit(params, 'network') + code = self.safe_currency_code(code) + network = self.network_id_to_code(rawNetwork, code) + response = await self.fetch_deposit_addresses_by_network(code, params) + if network is not None: + result = self.safe_dict(response, network) + if result is None: + raise InvalidAddress(self.id + ' fetchDepositAddress() cannot find ' + network + ' deposit address for ' + code) + return result + codeNetwork = self.network_id_to_code(code, code) + if codeNetwork in response: + return response[codeNetwork] + # if the network is not specified, return the first address + keys = list(response.keys()) + first = self.safe_string(keys, 0) + return self.safe_dict(response, first) + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://www.okx.com/docs-v5/en/#funding-account-rest-api-withdrawal + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.check_address(address) + await self.load_markets() + currency = self.currency(code) + if (tag is not None) and (len(tag) > 0): + address = address + ':' + tag + request: dict = { + 'ccy': currency['id'], + 'toAddr': address, + 'dest': '4', # 2 = OKCoin International, 3 = OKX 4 = others + 'amt': self.number_to_string(amount), + } + network = self.safe_string(params, 'network') # self line allows the user to specify either ERC20 or ETH + if network is not None: + networks = self.safe_dict(self.options, 'networks', {}) + network = self.safe_string(networks, network.upper(), network) # handle ETH>ERC20 alias + request['chain'] = currency['id'] + '-' + network + params = self.omit(params, 'network') + fee = self.safe_string(params, 'fee') + if fee is None: + currencies = await self.fetch_currencies() + self.currencies = self.map_to_safe_map(self.deep_extend(self.currencies, currencies)) + targetNetwork = self.safe_dict(currency['networks'], self.network_id_to_code(network), {}) + fee = self.safe_string(targetNetwork, 'fee') + if fee is None: + raise ArgumentsRequired(self.id + ' withdraw() requires a "fee" string parameter, network transaction fee must be ≥ 0. Withdrawals to OKCoin or OKX are fee-free, please set "0". Withdrawing to external digital asset address requires network transaction fee.') + request['fee'] = self.number_to_string(fee) # withdrawals to OKCoin or OKX are fee-free, please set 0 + query = self.omit(params, ['fee']) + response = await self.privatePostAssetWithdrawal(self.extend(request, query)) + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "amt": "0.1", + # "wdId": "67485", + # "ccy": "BTC" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + transaction = self.safe_dict(data, 0) + return self.parse_transaction(transaction, currency) + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://www.okx.com/docs-v5/en/#rest-api-funding-get-deposit-history + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch entries for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchDeposits', 'paginate') + if paginate: + return await self.fetch_paginated_call_dynamic('fetchDeposits', code, since, limit, params) + request: dict = { + # 'ccy': currency['id'], + # 'state': 2, # 0 waiting for confirmation, 1 deposit credited, 2 deposit successful + # 'after': since, + # 'before' self.milliseconds(), + # 'limit': limit, # default 100, max 100 + } + currency = None + if code is not None: + currency = self.currency(code) + request['ccy'] = currency['id'] + if since is not None: + request['before'] = max(since - 1, 0) + if limit is not None: + request['limit'] = limit # default 100, max 100 + request, params = self.handle_until_option('after', request, params) + response = await self.privateGetAssetDepositHistory(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "amt": "0.01044408", + # "txId": "1915737_3_0_0_asset", + # "ccy": "BTC", + # "from": "13801825426", + # "to": "", + # "ts": "1597026383085", + # "state": "2", + # "depId": "4703879" + # }, + # { + # "amt": "491.6784211", + # "txId": "1744594_3_184_0_asset", + # "ccy": "OKB", + # "from": "", + # "to": "", + # "ts": "1597026383085", + # "state": "2", + # "depId": "4703809" + # }, + # { + # "amt": "223.18782496", + # "txId": "6d892c669225b1092c780bf0da0c6f912fc7dc8f6b8cc53b003288624c", + # "ccy": "USDT", + # "from": "", + # "to": "39kK4XvgEuM7rX9frgyHoZkWqx4iKu1spD", + # "ts": "1597026383085", + # "state": "2", + # "depId": "4703779" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_transactions(data, currency, since, limit, params) + + async def fetch_deposit(self, id: str, code: Str = None, params={}): + """ + fetch data on a currency deposit via the deposit id + + https://www.okx.com/docs-v5/en/#rest-api-funding-get-deposit-history + + :param str id: deposit id + :param str code: filter by currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + await self.load_markets() + request: dict = { + 'depId': id, + } + currency = None + if code is not None: + currency = self.currency(code) + request['ccy'] = currency['id'] + response = await self.privateGetAssetDepositHistory(self.extend(request, params)) + data = self.safe_value(response, 'data') + deposit = self.safe_dict(data, 0, {}) + return self.parse_transaction(deposit, currency) + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://www.okx.com/docs-v5/en/#rest-api-funding-get-withdrawal-history + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch entries for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchWithdrawals', 'paginate') + if paginate: + return await self.fetch_paginated_call_dynamic('fetchWithdrawals', code, since, limit, params) + request: dict = { + # 'ccy': currency['id'], + # 'state': 2, # -3: pending cancel, -2 canceled, -1 failed, 0, pending, 1 sending, 2 sent, 3 awaiting email verification, 4 awaiting manual verification, 5 awaiting identity verification + # 'after': since, + # 'before': self.milliseconds(), + # 'limit': limit, # default 100, max 100 + } + currency = None + if code is not None: + currency = self.currency(code) + request['ccy'] = currency['id'] + if since is not None: + request['before'] = max(since - 1, 0) + if limit is not None: + request['limit'] = limit # default 100, max 100 + request, params = self.handle_until_option('after', request, params) + response = await self.privateGetAssetWithdrawalHistory(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "amt": "0.094", + # "wdId": "4703879", + # "fee": "0.01000000eth", + # "txId": "0x62477bac6509a04512819bb1455e923a60dea5966c7caeaa0b24eb8fb0432b85", + # "ccy": "ETH", + # "from": "13426335357", + # "to": "0xA41446125D0B5b6785f6898c9D67874D763A1519", + # "ts": "1597026383085", + # "state": "2" + # }, + # { + # "amt": "0.01", + # "wdId": "4703879", + # "fee": "0.00000000btc", + # "txId": "", + # "ccy": "BTC", + # "from": "13426335357", + # "to": "13426335357", + # "ts": "1597026383085", + # "state": "2" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_transactions(data, currency, since, limit, params) + + async def fetch_withdrawal(self, id: str, code: Str = None, params={}): + """ + fetch data on a currency withdrawal via the withdrawal id + + https://www.okx.com/docs-v5/en/#rest-api-funding-get-withdrawal-history + + :param str id: withdrawal id + :param str code: unified currency code of the currency withdrawn, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + await self.load_markets() + request: dict = { + 'wdId': id, + } + currency = None + if code is not None: + currency = self.currency(code) + request['ccy'] = currency['id'] + response = await self.privateGetAssetWithdrawalHistory(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "chain": "USDT-TRC20", + # "clientId": '', + # "fee": "0.8", + # "ccy": "USDT", + # "amt": "54.561", + # "txId": "00cff6ec7fa7c7d7d184bd84e82b9ff36863f07c0421188607f87dfa94e06b70", + # "from": "example@email.com", + # "to": "TEY6qjnKDyyq5jDc3DJizWLCdUySrpQ4yp", + # "state": "2", + # "ts": "1641376485000", + # "wdId": "25147041" + # } + # ], + # "msg": '' + # } + # + data = self.safe_list(response, 'data', []) + withdrawal = self.safe_dict(data, 0, {}) + return self.parse_transaction(withdrawal) + + def parse_transaction_status(self, status: Str): + # + # deposit statuses + # + # { + # "0": "waiting for confirmation", + # "1": "deposit credited", + # "2": "deposit successful" + # } + # + # withdrawal statuses + # + # { + # '-3': "pending cancel", + # "-2": "canceled", + # "-1": "failed", + # "0": "pending", + # "1": "sending", + # "2": "sent", + # "3": "awaiting email verification", + # "4": "awaiting manual verification", + # "5": "awaiting identity verification" + # } + # + statuses: dict = { + '-3': 'pending', + '-2': 'canceled', + '-1': 'failed', + '0': 'pending', + '1': 'pending', + '2': 'ok', + '3': 'pending', + '4': 'pending', + '5': 'pending', + '6': 'pending', + '7': 'pending', + '8': 'pending', + '9': 'pending', + '10': 'pending', + '12': 'pending', + '15': 'pending', + '16': 'pending', + } + return self.safe_string(statuses, status, status) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # withdraw + # + # { + # "amt": "0.1", + # "wdId": "67485", + # "ccy": "BTC" + # } + # + # fetchWithdrawals + # + # { + # "amt": "0.094", + # "wdId": "4703879", + # "fee": "0.01000000eth", + # "txId": "0x62477bac6509a04512819bb1455e923a60dea5966c7caeaa0b24eb8fb0432b85", + # "ccy": "ETH", + # "from": "13426335357", + # "to": "0xA41446125D0B5b6785f6898c9D67874D763A1519", + # "tag", + # "pmtId", + # "memo", + # "ts": "1597026383085", + # "state": "2" + # } + # + # fetchDeposits + # + # { + # "amt": "0.01044408", + # "txId": "1915737_3_0_0_asset", + # "ccy": "BTC", + # "from": "13801825426", + # "to": "", + # "ts": "1597026383085", + # "state": "2", + # "depId": "4703879" + # } + # + type = None + id = None + withdrawalId = self.safe_string(transaction, 'wdId') + addressFrom = self.safe_string(transaction, 'from') + addressTo = self.safe_string(transaction, 'to') + address = addressTo + tagTo = self.safe_string_2(transaction, 'tag', 'memo') + tagTo = self.safe_string_2(transaction, 'pmtId', tagTo) + if withdrawalId is not None: + type = 'withdrawal' + id = withdrawalId + else: + # the payment_id will appear on new deposits but appears to be removed from the response after 2 months + id = self.safe_string(transaction, 'depId') + type = 'deposit' + currencyId = self.safe_string(transaction, 'ccy') + code = self.safe_currency_code(currencyId) + amount = self.safe_number(transaction, 'amt') + status = self.parse_transaction_status(self.safe_string(transaction, 'state')) + txid = self.safe_string(transaction, 'txId') + timestamp = self.safe_integer(transaction, 'ts') + feeCost = None + if type == 'deposit': + feeCost = 0 + else: + feeCost = self.safe_number(transaction, 'fee') + # todo parse tags + return { + 'info': transaction, + 'id': id, + 'currency': code, + 'amount': amount, + 'network': None, + 'addressFrom': addressFrom, + 'addressTo': addressTo, + 'address': address, + 'tagFrom': None, + 'tagTo': tagTo, + 'tag': tagTo, + 'status': status, + 'type': type, + 'updated': None, + 'txid': txid, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'internal': None, + 'comment': None, + 'fee': { + 'currency': code, + 'cost': feeCost, + }, + } + + async def fetch_leverage(self, symbol: str, params={}) -> Leverage: + """ + fetch the set leverage for a market + + https://www.okx.com/docs-v5/en/#rest-api-account-get-leverage + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' + :returns dict: a `leverage structure ` + """ + await self.load_markets() + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchLeverage', params) + if marginMode is None: + marginMode = self.safe_string(params, 'mgnMode', 'cross') # cross marginMode + if (marginMode != 'cross') and (marginMode != 'isolated'): + raise BadRequest(self.id + ' fetchLeverage() requires a marginMode parameter that must be either cross or isolated') + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + 'mgnMode': marginMode, + } + response = await self.privateGetAccountLeverageInfo(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "instId": "BTC-USDT-SWAP", + # "lever": "5.00000000", + # "mgnMode": "isolated", + # "posSide": "net" + # } + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_leverage(data, market) + + def parse_leverage(self, leverage: dict, market: Market = None) -> Leverage: + marketId = None + marginMode = None + longLeverage = None + shortLeverage = None + for i in range(0, len(leverage)): + entry = leverage[i] + marginMode = self.safe_string_lower(entry, 'mgnMode') + marketId = self.safe_string(entry, 'instId') + positionSide = self.safe_string_lower(entry, 'posSide') + if positionSide == 'long': + longLeverage = self.safe_integer(entry, 'lever') + elif positionSide == 'short': + shortLeverage = self.safe_integer(entry, 'lever') + else: + longLeverage = self.safe_integer(entry, 'lever') + shortLeverage = self.safe_integer(entry, 'lever') + return { + 'info': leverage, + 'symbol': self.safe_symbol(marketId, market), + 'marginMode': marginMode, + 'longLeverage': longLeverage, + 'shortLeverage': shortLeverage, + } + + async def fetch_position(self, symbol: str, params={}): + """ + fetch data on a single open contract trade position + + https://www.okx.com/docs-v5/en/#rest-api-account-get-positions + + :param str symbol: unified market symbol of the market the position is held in, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.instType]: MARGIN, SWAP, FUTURES, OPTION + :returns dict: a `position structure ` + """ + await self.load_markets() + market = self.market(symbol) + type, query = self.handle_market_type_and_params('fetchPosition', market, params) + request: dict = { + # instType str No Instrument type, MARGIN, SWAP, FUTURES, OPTION + 'instId': market['id'], + # posId str No Single position ID or multiple position IDs(no more than 20) separated with comma + } + if type is not None: + request['instType'] = self.convert_to_instrument_type(type) + response = await self.privateGetAccountPositions(self.extend(request, query)) + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "adl": "1", + # "availPos": "1", + # "avgPx": "2566.31", + # "cTime": "1619507758793", + # "ccy": "ETH", + # "deltaBS": "", + # "deltaPA": "", + # "gammaBS": "", + # "gammaPA": "", + # "imr": "", + # "instId": "ETH-USD-210430", + # "instType": "FUTURES", + # "interest": "0", + # "last": "2566.22", + # "lever": "10", + # "liab": "", + # "liabCcy": "", + # "liqPx": "2352.8496681818233", + # "margin": "0.0003896645377994", + # "mgnMode": "isolated", + # "mgnRatio": "11.731726509588816", + # "mmr": "0.0000311811092368", + # "optVal": "", + # "pTime": "1619507761462", + # "pos": "1", + # "posCcy": "", + # "posId": "307173036051017730", + # "posSide": "long", + # "thetaBS": "", + # "thetaPA": "", + # "tradeId": "109844", + # "uTime": "1619507761462", + # "upl": "-0.0000009932766034", + # "uplRatio": "-0.0025490556801078", + # "vegaBS": "", + # "vegaPA": "" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + position = self.safe_dict(data, 0) + if position is None: + return None + return self.parse_position(position, market) + + async def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + + https://www.okx.com/docs-v5/en/#rest-api-account-get-positions + https://www.okx.com/docs-v5/en/#trading-account-rest-api-get-positions-history history + + fetch all open positions + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.instType]: MARGIN, SWAP, FUTURES, OPTION + :returns dict[]: a list of `position structure ` + """ + await self.load_markets() + request: dict = { + # 'instType': 'MARGIN', # optional string, MARGIN, SWAP, FUTURES, OPTION + # 'instId': market['id'], # optional string, e.g. 'BTC-USD-190927-5000-C' + # 'posId': '307173036051017730', # optional string, Single or multiple position IDs(no more than 20) separated with commas + } + if symbols is not None: + marketIds = [] + for i in range(0, len(symbols)): + entry = symbols[i] + market = self.market(entry) + marketIds.append(market['id']) + marketIdsLength = len(marketIds) + if marketIdsLength > 0: + request['instId'] = ','.join(marketIds) + fetchPositionsOptions = self.safe_dict(self.options, 'fetchPositions', {}) + method = self.safe_string(fetchPositionsOptions, 'method', 'privateGetAccountPositions') + response = None + if method == 'privateGetAccountPositionsHistory': + response = await self.privateGetAccountPositionsHistory(self.extend(request, params)) + else: + response = await self.privateGetAccountPositions(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "adl": "1", + # "availPos": "1", + # "avgPx": "2566.31", + # "cTime": "1619507758793", + # "ccy": "ETH", + # "deltaBS": "", + # "deltaPA": "", + # "gammaBS": "", + # "gammaPA": "", + # "imr": "", + # "instId": "ETH-USD-210430", + # "instType": "FUTURES", + # "interest": "0", + # "last": "2566.22", + # "lever": "10", + # "liab": "", + # "liabCcy": "", + # "liqPx": "2352.8496681818233", + # "margin": "0.0003896645377994", + # "mgnMode": "isolated", + # "mgnRatio": "11.731726509588816", + # "mmr": "0.0000311811092368", + # "optVal": "", + # "pTime": "1619507761462", + # "pos": "1", + # "posCcy": "", + # "posId": "307173036051017730", + # "posSide": "long", + # "thetaBS": "", + # "thetaPA": "", + # "tradeId": "109844", + # "uTime": "1619507761462", + # "upl": "-0.0000009932766034", + # "uplRatio": "-0.0025490556801078", + # "vegaBS": "", + # "vegaPA": "" + # } + # ] + # } + # + positions = self.safe_list(response, 'data', []) + result = [] + for i in range(0, len(positions)): + result.append(self.parse_position(positions[i])) + return self.filter_by_array_positions(result, 'symbol', self.market_symbols(symbols), False) + + async def fetch_positions_for_symbol(self, symbol: str, params={}): + """ + + https://www.okx.com/docs-v5/en/#rest-api-account-get-positions + + fetch all open positions for specific symbol + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.instType]: MARGIN(if needed) + :returns dict[]: a list of `position structure ` + """ + return await self.fetch_positions([symbol], params) + + def parse_position(self, position: dict, market: Market = None): + # + # { + # "adl": "3", + # "availPos": "1", + # "avgPx": "34131.1", + # "cTime": "1627227626502", + # "ccy": "USDT", + # "deltaBS": "", + # "deltaPA": "", + # "gammaBS": "", + # "gammaPA": "", + # "imr": "170.66093041794787", + # "instId": "BTC-USDT-SWAP", + # "instType": "SWAP", + # "interest": "0", + # "last": "34134.4", + # "lever": "2", + # "liab": "", + # "liabCcy": "", + # "liqPx": "12608.959083877446", + # "markPx": "4786.459271773621", + # "margin": "", + # "mgnMode": "cross", + # "mgnRatio": "140.49930117599155", + # "mmr": "1.3652874433435829", + # "notionalUsd": "341.5130010779638", + # "optVal": "", + # "pos": "1", + # "posCcy": "", + # "posId": "339552508062380036", + # "posSide": "long", + # "thetaBS": "", + # "thetaPA": "", + # "tradeId": "98617799", + # "uTime": "1627227626502", + # "upl": "0.0108608358957281", + # "uplRatio": "0.0000636418743944", + # "vegaBS": "", + # "vegaPA": "" + # } + # history + # { + # "cTime":"1708351230102", + # "ccy":"USDT", + # "closeAvgPx":"1.2567", + # "closeTotalPos":"40", + # "direction":"short", + # "fee":"-0.0351036", + # "fundingFee":"0", + # "instId":"SUSHI-USDT-SWAP", + # "instType":"SWAP", + # "lever":"10.0", + # "liqPenalty":"0", + # "mgnMode":"isolated", + # "openAvgPx":"1.2462", + # "openMaxPos":"40", + # "pnl":"-0.42", + # "pnlRatio":"-0.0912982667308618", + # "posId":"666159086676836352", + # "realizedPnl":"-0.4551036", + # "triggerPx":"", + # "type":"2", + # "uTime":"1708354805699", + # "uly":"SUSHI-USDT" + # } + # + marketId = self.safe_string(position, 'instId') + market = self.safe_market(marketId, market, None, 'contract') + symbol = market['symbol'] + pos = self.safe_string(position, 'pos') # 'pos' field: One way mode: 0 if position is not open, 1 if open | Two way(hedge) mode: -1 if short, 1 if long, 0 if position is not open + contractsAbs = Precise.string_abs(pos) + side = self.safe_string_2(position, 'posSide', 'direction') + hedged = side != 'net' + contracts = self.parse_number(contractsAbs) + if market['margin']: + # margin position + if side == 'net': + posCcy = self.safe_string(position, 'posCcy') + parsedCurrency = self.safe_currency_code(posCcy) + if parsedCurrency is not None: + side = 'long' if (market['base'] == parsedCurrency) else 'short' + if side is None: + side = self.safe_string(position, 'direction') + else: + if pos is not None: + if side == 'net': + if Precise.string_gt(pos, '0'): + side = 'long' + elif Precise.string_lt(pos, '0'): + side = 'short' + else: + side = None + contractSize = self.safe_number(market, 'contractSize') + contractSizeString = self.number_to_string(contractSize) + markPriceString = self.safe_string(position, 'markPx') + notionalString = self.safe_string(position, 'notionalUsd') + if market['inverse']: + notionalString = Precise.string_div(Precise.string_mul(contractsAbs, contractSizeString), markPriceString) + notional = self.parse_number(notionalString) + marginMode = self.safe_string(position, 'mgnMode') + initialMarginString = None + entryPriceString = self.safe_string_2(position, 'avgPx', 'openAvgPx') + unrealizedPnlString = self.safe_string(position, 'upl') + leverageString = self.safe_string(position, 'lever') + initialMarginPercentage = None + collateralString = None + if marginMode == 'cross': + initialMarginString = self.safe_string(position, 'imr') + collateralString = Precise.string_add(initialMarginString, unrealizedPnlString) + elif marginMode == 'isolated': + initialMarginPercentage = Precise.string_div('1', leverageString) + collateralString = self.safe_string(position, 'margin') + maintenanceMarginString = self.safe_string(position, 'mmr') + maintenanceMargin = self.parse_number(maintenanceMarginString) + maintenanceMarginPercentageString = Precise.string_div(maintenanceMarginString, notionalString) + if initialMarginPercentage is None: + initialMarginPercentage = self.parse_number(Precise.string_div(initialMarginString, notionalString, 4)) + elif initialMarginString is None: + initialMarginString = Precise.string_mul(initialMarginPercentage, notionalString) + rounder = '0.00005' # round to closest 0.01% + maintenanceMarginPercentage = self.parse_number(Precise.string_div(Precise.string_add(maintenanceMarginPercentageString, rounder), '1', 4)) + liquidationPrice = self.safe_number(position, 'liqPx') + percentageString = self.safe_string(position, 'uplRatio') + percentage = self.parse_number(Precise.string_mul(percentageString, '100')) + timestamp = self.safe_integer(position, 'cTime') + marginRatio = self.parse_number(Precise.string_div(maintenanceMarginString, collateralString, 4)) + return self.safe_position({ + 'info': position, + 'id': self.safe_string(position, 'posId'), + 'symbol': symbol, + 'notional': notional, + 'marginMode': marginMode, + 'liquidationPrice': liquidationPrice, + 'entryPrice': self.parse_number(entryPriceString), + 'unrealizedPnl': self.parse_number(unrealizedPnlString), + 'realizedPnl': self.safe_number(position, 'realizedPnl'), + 'percentage': percentage, + 'contracts': contracts, + 'contractSize': contractSize, + 'markPrice': self.parse_number(markPriceString), + 'lastPrice': self.safe_number(position, 'closeAvgPx'), + 'side': side, + 'hedged': hedged, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastUpdateTimestamp': self.safe_integer(position, 'uTime'), + 'maintenanceMargin': maintenanceMargin, + 'maintenanceMarginPercentage': maintenanceMarginPercentage, + 'collateral': self.parse_number(collateralString), + 'initialMargin': self.parse_number(initialMarginString), + 'initialMarginPercentage': self.parse_number(initialMarginPercentage), + 'leverage': self.parse_number(leverageString), + 'marginRatio': marginRatio, + 'stopLossPrice': None, + 'takeProfitPrice': None, + }) + + async def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://www.okx.com/docs-v5/en/#rest-api-funding-funds-transfer + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account to transfer from + :param str toAccount: account to transfer to + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transfer structure ` + """ + await self.load_markets() + currency = self.currency(code) + accountsByType = self.safe_dict(self.options, 'accountsByType', {}) + fromId = self.safe_string(accountsByType, fromAccount, fromAccount) + toId = self.safe_string(accountsByType, toAccount, toAccount) + request: dict = { + 'ccy': currency['id'], + 'amt': self.currency_to_precision(code, amount), + 'type': '0', # 0 = transfer within account by default, 1 = master account to sub-account, 2 = sub-account to master account, 3 = sub-account to master account(Only applicable to APIKey from sub-account), 4 = sub-account to sub-account + 'from': fromId, # remitting account, 6: Funding account, 18: Trading account + 'to': toId, # beneficiary account, 6: Funding account, 18: Trading account + # 'subAcct': 'sub-account-name', # optional, only required when type is 1, 2 or 4 + # 'loanTrans': False, # Whether or not borrowed coins can be transferred out under Multi-currency margin and Portfolio margin. The default is False + # 'clientId': 'client-supplied id', # A combination of case-sensitive alphanumerics, all numbers, or all letters of up to 32 characters + # 'omitPosRisk': False, # Ignore position risk. Default is False. Applicable to Portfolio margin + } + if fromId == 'master': + request['type'] = '1' + request['subAcct'] = toId + request['from'] = self.safe_string(params, 'from', '6') + request['to'] = self.safe_string(params, 'to', '6') + elif toId == 'master': + request['type'] = '2' + request['subAcct'] = fromId + request['from'] = self.safe_string(params, 'from', '6') + request['to'] = self.safe_string(params, 'to', '6') + response = await self.privatePostAssetTransfer(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "transId": "754147", + # "ccy": "USDT", + # "from": "6", + # "amt": "0.1", + # "to": "18" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + rawTransfer = self.safe_dict(data, 0, {}) + return self.parse_transfer(rawTransfer, currency) + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + # + # transfer + # + # { + # "transId": "754147", + # "ccy": "USDT", + # "from": "6", + # "amt": "0.1", + # "to": "18" + # } + # + # fetchTransfer + # + # { + # "amt": "5", + # "ccy": "USDT", + # "from": "18", + # "instId": "", + # "state": "success", + # "subAcct": "", + # "to": "6", + # "toInstId": "", + # "transId": "464424732", + # "type": "0" + # } + # + # fetchTransfers + # + # { + # "bal": "70.6874353780312913", + # "balChg": "-4.0000000000000000", # negative means "to funding", positive meand "from funding" + # "billId": "588900695232225299", + # "ccy": "USDT", + # "execType": "", + # "fee": "", + # "from": "18", + # "instId": "", + # "instType": "", + # "mgnMode": "", + # "notes": "To Funding Account", + # "ordId": "", + # "pnl": "", + # "posBal": "", + # "posBalChg": "", + # "price": "0", + # "subType": "12", + # "sz": "-4", + # "to": "6", + # "ts": "1686676866989", + # "type": "1" + # } + # + id = self.safe_string_2(transfer, 'transId', 'billId') + currencyId = self.safe_string(transfer, 'ccy') + code = self.safe_currency_code(currencyId, currency) + amount = self.safe_number(transfer, 'amt') + fromAccountId = self.safe_string(transfer, 'from') + toAccountId = self.safe_string(transfer, 'to') + accountsById = self.safe_dict(self.options, 'accountsById', {}) + timestamp = self.safe_integer(transfer, 'ts') + balanceChange = self.safe_string(transfer, 'sz') + if balanceChange is not None: + amount = self.parse_number(Precise.string_abs(balanceChange)) + return { + 'info': transfer, + 'id': id, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'currency': code, + 'amount': amount, + 'fromAccount': self.safe_string(accountsById, fromAccountId), + 'toAccount': self.safe_string(accountsById, toAccountId), + 'status': self.parse_transfer_status(self.safe_string(transfer, 'state')), + } + + def parse_transfer_status(self, status: Str) -> Str: + statuses: dict = { + 'success': 'ok', + } + return self.safe_string(statuses, status, status) + + async def fetch_transfer(self, id: str, code: Str = None, params={}) -> TransferEntry: + await self.load_markets() + request: dict = { + 'transId': id, + # 'type': 0, # default is 0 transfer within account, 1 master to sub, 2 sub to master + } + response = await self.privateGetAssetTransferState(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "amt": "5", + # "ccy": "USDT", + # "from": "18", + # "instId": "", + # "state": "success", + # "subAcct": "", + # "to": "6", + # "toInstId": "", + # "transId": "464424732", + # "type": "0" + # } + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + transfer = self.safe_dict(data, 0) + return self.parse_transfer(transfer) + + async def fetch_transfers(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[TransferEntry]: + """ + fetch a history of internal transfers made on an account + + https://www.okx.com/docs-v5/en/#trading-account-rest-api-get-bills-details-last-3-months + + :param str code: unified currency code of the currency transferred + :param int [since]: the earliest time in ms to fetch transfers for + :param int [limit]: the maximum number of transfers structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transfer structures ` + """ + await self.load_markets() + currency = None + request: dict = { + 'type': '1', # https://www.okx.com/docs-v5/en/#rest-api-account-get-bills-details-last-3-months + } + if code is not None: + currency = self.currency(code) + request['ccy'] = currency['id'] + if since is not None: + request['begin'] = since + if limit is not None: + request['limit'] = limit + response = await self.privateGetAccountBillsArchive(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "bal": "70.6874353780312913", + # "balChg": "-4.0000000000000000", + # "billId": "588900695232225299", + # "ccy": "USDT", + # "execType": "", + # "fee": "", + # "from": "18", + # "instId": "", + # "instType": "", + # "mgnMode": "", + # "notes": "To Funding Account", + # "ordId": "", + # "pnl": "", + # "posBal": "", + # "posBalChg": "", + # "price": "0", + # "subType": "12", + # "sz": "-4", + # "to": "6", + # "ts": "1686676866989", + # "type": "1" + # }, + # ... + # ], + # "msg": "" + # } + # + transfers = self.safe_list(response, 'data', []) + return self.parse_transfers(transfers, currency, since, limit, params) + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + isArray = isinstance(params, list) + request = '/api/' + self.version + '/' + self.implode_params(path, params) + query = self.omit(params, self.extract_params(path)) + url = self.implode_hostname(self.urls['api']['rest']) + request + # type = self.getPathAuthenticationType(path) + if api == 'public': + if query: + url += '?' + self.urlencode(query) + elif api == 'private': + self.check_required_credentials() + # inject id in implicit api call + if method == 'POST' and (path == 'trade/batch-orders' or path == 'trade/order-algo' or path == 'trade/order'): + brokerId = self.safe_string(self.options, 'brokerId', '6b9ad766b55dBCDE') + if isinstance(params, list): + for i in range(0, len(params)): + entry = params[i] + clientOrderId = self.safe_string(entry, 'clOrdId') + if clientOrderId is None: + entry['clOrdId'] = brokerId + self.uuid16() + entry['tag'] = brokerId + params[i] = entry + else: + clientOrderId = self.safe_string(params, 'clOrdId') + if clientOrderId is None: + params['clOrdId'] = brokerId + self.uuid16() + params['tag'] = brokerId + timestamp = self.iso8601(self.nonce()) + headers = { + 'OK-ACCESS-KEY': self.apiKey, + 'OK-ACCESS-PASSPHRASE': self.password, + 'OK-ACCESS-TIMESTAMP': timestamp, + # 'OK-FROM': '', + # 'OK-TO': '', + # 'OK-LIMIT': '', + } + auth = timestamp + method + request + if method == 'GET': + if query: + urlencodedQuery = '?' + self.urlencode(query) + url += urlencodedQuery + auth += urlencodedQuery + else: + if isArray or query: + body = self.json(query) + auth += body + headers['Content-Type'] = 'application/json' + signature = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256, 'base64') + headers['OK-ACCESS-SIGN'] = signature + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def parse_funding_rate(self, contract, market: Market = None) -> FundingRate: + # + # { + # "fundingRate": "0.00027815", + # "fundingTime": "1634256000000", + # "instId": "BTC-USD-SWAP", + # "instType": "SWAP", + # "nextFundingRate": "0.00017", + # "nextFundingTime": "1634284800000" + # } + # ws + # { + # "fundingRate":"0.0001875391284828", + # "fundingTime":"1700726400000", + # "instId":"BTC-USD-SWAP", + # "instType":"SWAP", + # "method": "next_period", + # "maxFundingRate":"0.00375", + # "minFundingRate":"-0.00375", + # "nextFundingRate":"0.0002608059239328", + # "nextFundingTime":"1700755200000", + # "premium": "0.0001233824646391", + # "settFundingRate":"0.0001699799259033", + # "settState":"settled", + # "ts":"1700724675402" + # } + # + # in the response above nextFundingRate is actually two funding rates from now + # + nextFundingRateTimestamp = self.safe_integer(contract, 'nextFundingTime') + marketId = self.safe_string(contract, 'instId') + symbol = self.safe_symbol(marketId, market) + nextFundingRate = self.safe_number(contract, 'nextFundingRate') + fundingTime = self.safe_integer(contract, 'fundingTime') + fundingTimeString = self.safe_string(contract, 'fundingTime') + nextFundingTimeString = self.safe_string(contract, 'nextFundingTime') + millisecondsInterval = Precise.string_sub(nextFundingTimeString, fundingTimeString) + # https://www.okx.com/support/hc/en-us/articles/360053909272-Ⅸ-Introduction-to-perpetual-swap-funding-fee + # > The current interest is 0. + return { + 'info': contract, + 'symbol': symbol, + 'markPrice': None, + 'indexPrice': None, + 'interestRate': self.parse_number('0'), + 'estimatedSettlePrice': None, + 'timestamp': None, + 'datetime': None, + 'fundingRate': self.safe_number(contract, 'fundingRate'), + 'fundingTimestamp': fundingTime, + 'fundingDatetime': self.iso8601(fundingTime), + 'nextFundingRate': nextFundingRate, + 'nextFundingTimestamp': nextFundingRateTimestamp, + 'nextFundingDatetime': self.iso8601(nextFundingRateTimestamp), + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': self.parse_funding_interval(millisecondsInterval), + } + + def parse_funding_interval(self, interval): + intervals: dict = { + '3600000': '1h', + '14400000': '4h', + '28800000': '8h', + '57600000': '16h', + '86400000': '24h', + } + return self.safe_string(intervals, interval, interval) + + async def fetch_funding_interval(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate interval + + https://www.okx.com/docs-v5/en/#public-data-rest-api-get-funding-rate + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + return await self.fetch_funding_rate(symbol, params) + + async def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate + + https://www.okx.com/docs-v5/en/#public-data-rest-api-get-funding-rate + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + await self.load_markets() + market = self.market(symbol) + if not market['swap']: + raise ExchangeError(self.id + ' fetchFundingRate() is only valid for swap markets') + request: dict = { + 'instId': market['id'], + } + response = await self.publicGetPublicFundingRate(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "fundingRate": "0.00027815", + # "fundingTime": "1634256000000", + # "instId": "BTC-USD-SWAP", + # "instType": "SWAP", + # "nextFundingRate": "0.00017", + # "nextFundingTime": "1634284800000" + # } + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + entry = self.safe_dict(data, 0, {}) + return self.parse_funding_rate(entry, market) + + async def fetch_funding_rates(self, symbols: Strings = None, params={}) -> FundingRates: + """ + fetches the current funding rates for multiple symbols + + https://www.okx.com/docs-v5/en/#public-data-rest-api-get-funding-rate + + :param str[] symbols: unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `funding rates structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, 'swap', True) + request: dict = {'instId': 'ANY'} + response = await self.publicGetPublicFundingRate(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "fundingRate": "0.00027815", + # "fundingTime": "1634256000000", + # "instId": "BTC-USD-SWAP", + # "instType": "SWAP", + # "nextFundingRate": "0.00017", + # "nextFundingTime": "1634284800000" + # } + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_funding_rates(data, symbols) + + async def fetch_funding_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch the history of funding payments paid and received on self account + + https://www.okx.com/docs-v5/en/#trading-account-rest-api-get-bills-details-last-3-months + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch funding history for + :param int [limit]: the maximum number of funding history structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding history structure ` + """ + await self.load_markets() + request: dict = { + # 'instType': 'SPOT', # SPOT, MARGIN, SWAP, FUTURES, OPTION + # 'ccy': currency['id'], + # 'mgnMode': 'isolated', # isolated, cross + # 'ctType': 'linear', # linear, inverse, only applicable to FUTURES/SWAP + 'type': '8', + # + # supported values for type + # + # 1 Transfer + # 2 Trade + # 3 Delivery + # 4 Auto token conversion + # 5 Liquidation + # 6 Margin transfer + # 7 Interest deduction + # 8 Funding fee + # 9 ADL + # 10 Clawback + # 11 System token conversion + # 12 Strategy transfer + # 13 ddh + # + # 'subType': '', + # + # supported values for subType + # + # 1 Buy + # 2 Sell + # 3 Open long + # 4 Open short + # 5 Close long + # 6 Close short + # 9 Interest deduction + # 11 Transfer in + # 12 Transfer out + # 160 Manual margin increase + # 161 Manual margin decrease + # 162 Auto margin increase + # 110 Auto buy + # 111 Auto sell + # 118 System token conversion transfer in + # 119 System token conversion transfer out + # 100 Partial liquidation close long + # 101 Partial liquidation close short + # 102 Partial liquidation buy + # 103 Partial liquidation sell + # 104 Liquidation long + # 105 Liquidation short + # 106 Liquidation buy + # 107 Liquidation sell + # 110 Liquidation transfer in + # 111 Liquidation transfer out + # 125 ADL close long + # 126 ADL close short + # 127 ADL buy + # 128 ADL sell + # 131 ddh buy + # 132 ddh sell + # 170 Exercised + # 171 Counterparty exercised + # 172 Expired OTM + # 112 Delivery long + # 113 Delivery short + # 117 Delivery/Exercise clawback + # 173 Funding fee expense + # 174 Funding fee income + # 200 System transfer in + # 201 Manually transfer in + # 202 System transfer out + # 203 Manually transfer out + # + # "after": "id", # earlier than the requested bill ID + # "before": "id", # newer than the requested bill ID + # "limit": "100", # default 100, max 100 + } + if limit is not None: + request['limit'] = str(limit) # default 100, max 100 + market = None + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + if market['contract']: + if market['linear']: + request['ctType'] = 'linear' + request['ccy'] = market['quoteId'] + else: + request['ctType'] = 'inverse' + request['ccy'] = market['baseId'] + type, query = self.handle_market_type_and_params('fetchFundingHistory', market, params) + if type == 'swap': + request['instType'] = self.convert_to_instrument_type(type) + # AccountBillsArchive has the same cost but supports three months of data + response = await self.privateGetAccountBillsArchive(self.extend(request, query)) + # + # { + # "bal": "0.0242946200998573", + # "balChg": "0.0000148752712240", + # "billId": "377970609204146187", + # "ccy": "ETH", + # "execType": "", + # "fee": "0", + # "from": "", + # "instId": "ETH-USD-SWAP", + # "instType": "SWAP", + # "mgnMode": "isolated", + # "notes": "", + # "ordId": "", + # "pnl": "0.000014875271224", + # "posBal": "0", + # "posBalChg": "0", + # "subType": "174", + # "sz": "9", + # "to": "", + # "ts": "1636387215588", + # "type": "8" + # } + # + data = self.safe_list(response, 'data', []) + result = [] + for i in range(0, len(data)): + entry = data[i] + timestamp = self.safe_integer(entry, 'ts') + instId = self.safe_string(entry, 'instId') + marketInner = self.safe_market(instId) + currencyId = self.safe_string(entry, 'ccy') + code = self.safe_currency_code(currencyId) + result.append({ + 'info': entry, + 'symbol': marketInner['symbol'], + 'code': code, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': self.safe_string(entry, 'billId'), + 'amount': self.safe_number(entry, 'balChg'), + }) + sorted = self.sort_by(result, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, symbol, since, limit) + + async def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://www.okx.com/docs-v5/en/#rest-api-account-set-leverage + + :param float leverage: the rate of leverage + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' + :param str [params.posSide]: 'long' or 'short' or 'net' for isolated margin long/short mode on futures and swap markets, default is 'net' + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setLeverage() requires a symbol argument') + # WARNING: THIS WILL INCREASE LIQUIDATION PRICE FOR OPEN ISOLATED LONG POSITIONS + # AND DECREASE LIQUIDATION PRICE FOR OPEN ISOLATED SHORT POSITIONS + if (leverage < 1) or (leverage > 125): + raise BadRequest(self.id + ' setLeverage() leverage should be between 1 and 125') + await self.load_markets() + market = self.market(symbol) + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('setLeverage', params) + if marginMode is None: + marginMode = self.safe_string(params, 'mgnMode', 'cross') # cross marginMode + if (marginMode != 'cross') and (marginMode != 'isolated'): + raise BadRequest(self.id + ' setLeverage() requires a marginMode parameter that must be either cross or isolated') + request: dict = { + 'lever': leverage, + 'mgnMode': marginMode, + 'instId': market['id'], + } + posSide = self.safe_string(params, 'posSide', 'net') + if marginMode == 'isolated': + if posSide != 'long' and posSide != 'short' and posSide != 'net': + raise BadRequest(self.id + ' setLeverage() requires the posSide argument to be either "long", "short" or "net"') + request['posSide'] = posSide + response = await self.privatePostAccountSetLeverage(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "instId": "BTC-USDT-SWAP", + # "lever": "5", + # "mgnMode": "isolated", + # "posSide": "long" + # } + # ], + # "msg": "" + # } + # + return response + + async def fetch_position_mode(self, symbol: Str = None, params={}): + """ + + https://www.okx.com/docs-v5/en/#trading-account-rest-api-get-account-configuration + + fetchs the position mode, hedged or one way, hedged for binance is set identically for all linear markets or all inverse markets + :param str symbol: unified symbol of the market to fetch the order book for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.accountId]: if you have multiple accounts, you must specify the account id to fetch the position mode + :returns dict: an object detailing whether the market is in hedged or one-way mode + """ + accounts = await self.fetch_accounts() + length = len(accounts) + selectedAccount = None + if length > 1: + accountId = self.safe_string(params, 'accountId') + if accountId is None: + accountIds = self.get_list_from_object_values(accounts, 'id') + raise ExchangeError(self.id + ' fetchPositionMode() can not detect position mode, because you have multiple accounts. Set params["accountId"] to desired id from: ' + ', '.join(accountIds)) + else: + accountsById = self.index_by(accounts, 'id') + selectedAccount = self.safe_dict(accountsById, accountId) + else: + selectedAccount = accounts[0] + mainAccount = selectedAccount['info'] + posMode = self.safe_string(mainAccount, 'posMode') # long_short_mode, net_mode + isHedged = posMode == 'long_short_mode' + return { + 'info': mainAccount, + 'hedged': isHedged, + } + + async def set_position_mode(self, hedged: bool, symbol: Str = None, params={}): + """ + set hedged to True or False for a market + + https://www.okx.com/docs-v5/en/#trading-account-rest-api-set-position-mode + + :param bool hedged: set to True to use long_short_mode, False for net_mode + :param str symbol: not used by okx setPositionMode + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + hedgeMode = None + if hedged: + hedgeMode = 'long_short_mode' + else: + hedgeMode = 'net_mode' + request: dict = { + 'posMode': hedgeMode, + } + response = await self.privatePostAccountSetPositionMode(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "posMode": "net_mode" + # } + # ], + # "msg": "" + # } + # + return response + + async def set_margin_mode(self, marginMode: str, symbol: Str = None, params={}): + """ + set margin mode to 'cross' or 'isolated' + + https://www.okx.com/docs-v5/en/#trading-account-rest-api-set-leverage + + :param str marginMode: 'cross' or 'isolated' + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.leverage]: leverage + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setMarginMode() requires a symbol argument') + # WARNING: THIS WILL INCREASE LIQUIDATION PRICE FOR OPEN ISOLATED LONG POSITIONS + # AND DECREASE LIQUIDATION PRICE FOR OPEN ISOLATED SHORT POSITIONS + marginMode = marginMode.lower() + if (marginMode != 'cross') and (marginMode != 'isolated'): + raise BadRequest(self.id + ' setMarginMode() marginMode must be either cross or isolated') + await self.load_markets() + market = self.market(symbol) + lever = self.safe_integer_2(params, 'lever', 'leverage') + if (lever is None) or (lever < 1) or (lever > 125): + raise BadRequest(self.id + ' setMarginMode() params["lever"] should be between 1 and 125') + params = self.omit(params, ['leverage']) + request: dict = { + 'lever': lever, + 'mgnMode': marginMode, + 'instId': market['id'], + } + response = await self.privatePostAccountSetLeverage(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "instId": "BTC-USDT-SWAP", + # "lever": "5", + # "mgnMode": "isolated", + # "posSide": "long" + # } + # ], + # "msg": "" + # } + # + return response + + async def fetch_cross_borrow_rates(self, params={}) -> CrossBorrowRates: + """ + fetch the borrow interest rates of all currencies + + https://www.okx.com/docs-v5/en/#trading-account-rest-api-get-interest-rate + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `borrow rate structures ` + """ + await self.load_markets() + response = await self.privateGetAccountInterestRate(params) + # + # { + # "code": "0", + # "data": [ + # { + # "ccy": "BTC", + # "interestRate": "0.00000833" + # } + # ... + # ], + # } + # + data = self.safe_list(response, 'data', []) + rates = [] + for i in range(0, len(data)): + rates.append(self.parse_borrow_rate(data[i])) + return rates + + async def fetch_cross_borrow_rate(self, code: str, params={}) -> CrossBorrowRate: + """ + fetch the rate of interest to borrow a currency for margin trading + + https://www.okx.com/docs-v5/en/#trading-account-rest-api-get-interest-rate + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `borrow rate structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'ccy': currency['id'], + } + response = await self.privateGetAccountInterestRate(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "ccy": "USDT", + # "interestRate": "0.00002065" + # } + # ... + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + rate = self.safe_dict(data, 0, {}) + return self.parse_borrow_rate(rate) + + def parse_borrow_rate(self, info, currency: Currency = None): + # + # { + # "amt": "992.10341195", + # "ccy": "BTC", + # "rate": "0.01", + # "ts": "1643954400000" + # } + # + ccy = self.safe_string(info, 'ccy') + timestamp = self.safe_integer(info, 'ts') + return { + 'currency': self.safe_currency_code(ccy), + 'rate': self.safe_number_2(info, 'interestRate', 'rate'), + 'period': 86400000, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'info': info, + } + + def parse_borrow_rate_histories(self, response, codes, since, limit): + # + # [ + # { + # "amt": "992.10341195", + # "ccy": "BTC", + # "rate": "0.01", + # "ts": "1643954400000" + # }, + # ... + # ] + # + borrowRateHistories: dict = {} + for i in range(0, len(response)): + item = response[i] + code = self.safe_currency_code(self.safe_string(item, 'ccy')) + if codes is None or self.in_array(code, codes): + if not (code in borrowRateHistories): + borrowRateHistories[code] = [] + borrowRateStructure = self.parse_borrow_rate(item) + borrrowRateCode = borrowRateHistories[code] + borrrowRateCode.append(borrowRateStructure) + keys = list(borrowRateHistories.keys()) + for i in range(0, len(keys)): + code = keys[i] + borrowRateHistories[code] = self.filter_by_currency_since_limit(borrowRateHistories[code], code, since, limit) + return borrowRateHistories + + async def fetch_borrow_rate_histories(self, codes=None, since: Int = None, limit: Int = None, params={}): + """ + retrieves a history of a multiple currencies borrow interest rate at specific time slots, returns all currencies if no symbols passed, default is None + + https://www.okx.com/docs-v5/en/#financial-product-savings-get-public-borrow-history-public + + :param str[]|None codes: list of unified currency codes, default is None + :param int [since]: timestamp in ms of the earliest borrowRate, default is None + :param int [limit]: max number of borrow rate prices to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `borrow rate structures ` indexed by the market symbol + """ + await self.load_markets() + request: dict = { + # 'ccy': currency['id'], + # 'after': self.milliseconds(), # Pagination of data to return records earlier than the requested ts, + # 'before': since, # Pagination of data to return records newer than the requested ts, + # 'limit': limit, # default is 100 and maximum is 100 + } + if since is not None: + request['before'] = since + if limit is not None: + request['limit'] = limit + response = await self.publicGetFinanceSavingsLendingRateHistory(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "amt": "992.10341195", + # "ccy": "BTC", + # "rate": "0.01", + # "ts": "1643954400000" + # }, + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_borrow_rate_histories(data, codes, since, limit) + + async def fetch_borrow_rate_history(self, code: str, since: Int = None, limit: Int = None, params={}): + """ + retrieves a history of a currencies borrow interest rate at specific time slots + + https://www.okx.com/docs-v5/en/#financial-product-savings-get-public-borrow-history-public + + :param str code: unified currency code + :param int [since]: timestamp for the earliest borrow rate + :param int [limit]: the maximum number of `borrow rate structures ` to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of `borrow rate structures ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'ccy': currency['id'], + # 'after': self.milliseconds(), # Pagination of data to return records earlier than the requested ts, + # 'before': since, # Pagination of data to return records newer than the requested ts, + # 'limit': limit, # default is 100 and maximum is 100 + } + if since is not None: + request['before'] = since + if limit is not None: + request['limit'] = limit + response = await self.publicGetFinanceSavingsLendingRateHistory(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "amt": "992.10341195", + # "ccy": "BTC", + # "rate": "0.01", + # "ts": "1643954400000" + # }, + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_borrow_rate_history(data, code, since, limit) + + async def modify_margin_helper(self, symbol: str, amount, type, params={}) -> MarginModification: + await self.load_markets() + market = self.market(symbol) + posSide = self.safe_string(params, 'posSide', 'net') + params = self.omit(params, ['posSide']) + request: dict = { + 'instId': market['id'], + 'amt': amount, + 'type': type, + 'posSide': posSide, + } + response = await self.privatePostAccountPositionMarginBalance(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "amt": "0.01", + # "instId": "ETH-USD-SWAP", + # "posSide": "net", + # "type": "reduce" + # } + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + entry = self.safe_dict(data, 0, {}) + errorCode = self.safe_string(response, 'code') + return self.extend(self.parse_margin_modification(entry, market), { + 'status': 'ok' if (errorCode == '0') else 'failed', + }) + + def parse_margin_modification(self, data: dict, market: Market = None) -> MarginModification: + # + # addMargin/reduceMargin + # + # { + # "amt": "0.01", + # "instId": "ETH-USD-SWAP", + # "posSide": "net", + # "type": "reduce" + # } + # + # fetchMarginAdjustmentHistory + # + # { + # bal: '67621.4325135010619812', + # balChg: '-10.0000000000000000', + # billId: '691293628710342659', + # ccy: 'USDT', + # clOrdId: '', + # execType: '', + # fee: '0', + # fillFwdPx: '', + # fillIdxPx: '', + # fillMarkPx: '', + # fillMarkVol: '', + # fillPxUsd: '', + # fillPxVol: '', + # fillTime: '1711089244850', + # from: '', + # instId: 'XRP-USDT-SWAP', + # instType: 'SWAP', + # interest: '0', + # mgnMode: 'isolated', + # notes: '', + # ordId: '', + # pnl: '0', + # posBal: '73.12', + # posBalChg: '10.00', + # px: '', + # subType: '160', + # sz: '10', + # tag: '', + # to: '', + # tradeId: '0', + # ts: '1711089244699', + # type: '6' + # } + # + amountRaw = self.safe_string_2(data, 'amt', 'posBalChg') + typeRaw = self.safe_string(data, 'type') + type = None + if typeRaw == '6': + type = 'add' if Precise.string_gt(amountRaw, '0') else 'reduce' + else: + type = typeRaw + amount = Precise.string_abs(amountRaw) + marketId = self.safe_string(data, 'instId') + responseMarket = self.safe_market(marketId, market) + code = responseMarket['base'] if responseMarket['inverse'] else responseMarket['quote'] + timestamp = self.safe_integer(data, 'ts') + return { + 'info': data, + 'symbol': responseMarket['symbol'], + 'type': type, + 'marginMode': 'isolated', + 'amount': self.parse_number(amount), + 'code': code, + 'total': None, + 'status': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + + async def reduce_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + remove margin from a position + + https://www.okx.com/docs-v5/en/#trading-account-rest-api-increase-decrease-margin + + :param str symbol: unified market symbol + :param float amount: the amount of margin to remove + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin structure ` + """ + return await self.modify_margin_helper(symbol, amount, 'reduce', params) + + async def add_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + add margin + + https://www.okx.com/docs-v5/en/#trading-account-rest-api-increase-decrease-margin + + :param str symbol: unified market symbol + :param float amount: amount of margin to add + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin structure ` + """ + return await self.modify_margin_helper(symbol, amount, 'add', params) + + async def fetch_market_leverage_tiers(self, symbol: str, params={}) -> List[LeverageTier]: + """ + retrieve information on the maximum leverage, and maintenance margin for trades of varying trade sizes for a single market + + https://www.okx.com/docs-v5/en/#rest-api-public-data-get-position-tiers + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' + :returns dict: a `leverage tiers structure ` + """ + await self.load_markets() + market = self.market(symbol) + type = 'MARGIN' if market['spot'] else self.convert_to_instrument_type(market['type']) + uly = self.safe_string(market['info'], 'uly') + if not uly: + if type != 'MARGIN': + raise BadRequest(self.id + ' fetchMarketLeverageTiers() cannot fetch leverage tiers for ' + symbol) + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchMarketLeverageTiers', params) + if marginMode is None: + marginMode = self.safe_string(params, 'tdMode', 'cross') # cross marginMode + request: dict = { + 'instType': type, + 'tdMode': marginMode, + 'uly': uly, + } + if type == 'MARGIN': + request['instId'] = market['id'] + response = await self.publicGetPublicPositionTiers(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "baseMaxLoan": "500", + # "imr": "0.1", + # "instId": "ETH-USDT", + # "maxLever": "10", + # "maxSz": "500", + # "minSz": "0", + # "mmr": "0.03", + # "optMgnFactor": "0", + # "quoteMaxLoan": "200000", + # "tier": "1", + # "uly": "" + # }, + # ... + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_market_leverage_tiers(data, market) + + def parse_market_leverage_tiers(self, info, market: Market = None) -> List[LeverageTier]: + """ + @ignore + :param dict info: Exchange response for 1 market + :param dict market: CCXT market + """ + # + # [ + # { + # "baseMaxLoan": "500", + # "imr": "0.1", + # "instId": "ETH-USDT", + # "maxLever": "10", + # "maxSz": "500", + # "minSz": "0", + # "mmr": "0.03", + # "optMgnFactor": "0", + # "quoteMaxLoan": "200000", + # "tier": "1", + # "uly": "" + # }, + # ... + # ] + # + tiers = [] + for i in range(0, len(info)): + tier = info[i] + marketId = self.safe_string(tier, 'instId') + tiers.append({ + 'tier': self.safe_integer(tier, 'tier'), + 'symbol': self.safe_symbol(marketId, market), + 'currency': market['quote'], + 'minNotional': self.safe_number(tier, 'minSz'), + 'maxNotional': self.safe_number(tier, 'maxSz'), + 'maintenanceMarginRate': self.safe_number(tier, 'mmr'), + 'maxLeverage': self.safe_number(tier, 'maxLever'), + 'info': tier, + }) + return tiers + + async def fetch_borrow_interest(self, code: Str = None, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[BorrowInterest]: + """ + fetch the interest owed b the user for borrowing currency for margin trading + + https://www.okx.com/docs-v5/en/#rest-api-account-get-interest-accrued-data + + :param str code: the unified currency code for the currency of the interest + :param str symbol: the market symbol of an isolated margin market, if None, the interest for cross margin markets is returned + :param int [since]: timestamp in ms of the earliest time to receive interest records for + :param int [limit]: the number of `borrow interest structures ` to retrieve + :param dict [params]: exchange specific parameters + :param int [params.type]: Loan type 1 - VIP loans 2 - Market loans *Default is Market loans* + :param str [params.marginMode]: 'cross' or 'isolated' + :returns dict[]: An list of `borrow interest structures ` + """ + await self.load_markets() + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchBorrowInterest', params) + if marginMode is None: + marginMode = self.safe_string(params, 'mgnMode', 'cross') # cross marginMode + request: dict = { + 'mgnMode': marginMode, + } + market = None + if code is not None: + currency = self.currency(code) + request['ccy'] = currency['id'] + if since is not None: + request['before'] = since - 1 + if limit is not None: + request['limit'] = limit + if symbol is not None: + market = self.market(symbol) + request['instId'] = market['id'] + response = await self.privateGetAccountInterestAccrued(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "ccy": "USDT", + # "instId": "", + # "interest": "0.0003960833333334", + # "interestRate": "0.0000040833333333", + # "liab": "97", + # "mgnMode": "", + # "ts": "1637312400000", + # "type": "1" + # }, + # ... + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + interest = self.parse_borrow_interests(data) + return self.filter_by_currency_since_limit(interest, code, since, limit) + + def parse_borrow_interest(self, info: dict, market: Market = None) -> BorrowInterest: + instId = self.safe_string(info, 'instId') + if instId is not None: + market = self.safe_market(instId, market) + timestamp = self.safe_integer(info, 'ts') + return { + 'info': info, + 'symbol': self.safe_string(market, 'symbol'), + 'currency': self.safe_currency_code(self.safe_string(info, 'ccy')), + 'interest': self.safe_number(info, 'interest'), + 'interestRate': self.safe_number(info, 'interestRate'), + 'amountBorrowed': self.safe_number(info, 'liab'), + 'marginMode': self.safe_string(info, 'mgnMode'), + 'timestamp': timestamp, # Interest accrued time + 'datetime': self.iso8601(timestamp), + } + + async def borrow_cross_margin(self, code: str, amount: float, params={}): + """ + create a loan to borrow margin(need to be VIP 5 and above) + + https://www.okx.com/docs-v5/en/#trading-account-rest-api-vip-loans-borrow-and-repay + + :param str code: unified currency code of the currency to borrow + :param float amount: the amount to borrow + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin loan structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'ccy': currency['id'], + 'amt': self.currency_to_precision(code, amount), + 'side': 'borrow', + } + response = await self.privatePostAccountBorrowRepay(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "amt": "102", + # "ccy": "USDT", + # "ordId": "544199684697214976", + # "side": "borrow", + # "state": "1" + # } + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + loan = self.safe_dict(data, 0, {}) + return self.parse_margin_loan(loan, currency) + + async def repay_cross_margin(self, code: str, amount, params={}): + """ + repay borrowed margin and interest + + https://www.okx.com/docs-v5/en/#trading-account-rest-api-vip-loans-borrow-and-repay + + :param str code: unified currency code of the currency to repay + :param float amount: the amount to repay + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.id]: the order ID of borrowing, it is necessary while repaying + :returns dict: a `margin loan structure ` + """ + await self.load_markets() + id = self.safe_string_2(params, 'id', 'ordId') + params = self.omit(params, 'id') + if id is None: + raise ArgumentsRequired(self.id + ' repayCrossMargin() requires an id parameter') + currency = self.currency(code) + request: dict = { + 'ccy': currency['id'], + 'amt': self.currency_to_precision(code, amount), + 'side': 'repay', + 'ordId': id, + } + response = await self.privatePostAccountBorrowRepay(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "amt": "102", + # "ccy": "USDT", + # "ordId": "544199684697214976", + # "side": "repay", + # "state": "1" + # } + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + loan = self.safe_dict(data, 0, {}) + return self.parse_margin_loan(loan, currency) + + def parse_margin_loan(self, info, currency: Currency = None): + # + # { + # "amt": "102", + # "availLoan": "97", + # "ccy": "USDT", + # "loanQuota": "6000000", + # "posLoan": "0", + # "side": "repay", + # "usedLoan": "97" + # } + # + currencyId = self.safe_string(info, 'ccy') + return { + 'id': None, + 'currency': self.safe_currency_code(currencyId, currency), + 'amount': self.safe_number(info, 'amt'), + 'symbol': None, + 'timestamp': None, + 'datetime': None, + 'info': info, + } + + async def fetch_open_interest(self, symbol: str, params={}): + """ + Retrieves the open interest of a currency + + https://www.okx.com/docs-v5/en/#rest-api-public-data-get-open-interest + + :param str symbol: Unified CCXT market symbol + :param dict [params]: exchange specific parameters + :returns dict} an open interest structure{@link https://docs.ccxt.com/#/?id=open-interest-structure: + """ + await self.load_markets() + market = self.market(symbol) + if not market['contract']: + raise BadRequest(self.id + ' fetchOpenInterest() supports contract markets only') + type = self.convert_to_instrument_type(market['type']) + uly = self.safe_string(market['info'], 'uly') + request: dict = { + 'instType': type, + 'uly': uly, + 'instId': market['id'], + } + response = await self.publicGetPublicOpenInterest(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "instId": "BTC-USDT-SWAP", + # "instType": "SWAP", + # "oi": "2125419", + # "oiCcy": "21254.19", + # "ts": "1664005108969" + # } + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_open_interest(data[0], market) + + async def fetch_open_interests(self, symbols: Strings = None, params={}) -> OpenInterests: + """ + Retrieves the open interests of some currencies + + https://www.okx.com/docs-v5/en/#rest-api-public-data-get-open-interest + + :param str[] symbols: Unified CCXT market symbols + :param dict [params]: exchange specific parameters + :param str params['instType']: Instrument type, options: 'SWAP', 'FUTURES', 'OPTION', default to 'SWAP' + :param str params['uly']: Underlying, Applicable to FUTURES/SWAP/OPTION, if instType is 'OPTION', either uly or instFamily is required + :param str params['instFamily']: Instrument family, Applicable to FUTURES/SWAP/OPTION, if instType is 'OPTION', either uly or instFamily is required + :returns dict: an dictionary of `open interest structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, True, True) + market = None + if symbols is not None: + market = self.market(symbols[0]) + marketType = None + marketType, params = self.handle_sub_type_and_params('fetchOpenInterests', market, params, 'swap') + instType = 'SWAP' + if marketType == 'future': + instType = 'FUTURES' + elif instType == 'option': + instType = 'OPTION' + request: dict = {'instType': instType} + uly = self.safe_string(params, 'uly') + if uly is not None: + request['uly'] = uly + instFamily = self.safe_string(params, 'instFamily') + if instFamily is not None: + request['instFamily'] = instFamily + if instType == 'OPTION' and uly is None and instFamily is None: + raise BadRequest(self.id + ' fetchOpenInterests() requires either uly or instFamily parameter for OPTION markets') + response = await self.publicGetPublicOpenInterest(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "instId": "BTC-USDT-SWAP", + # "instType": "SWAP", + # "oi": "2125419", + # "oiCcy": "21254.19", + # "ts": "1664005108969" + # } + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_open_interests(data, symbols) + + async def fetch_open_interest_history(self, symbol: str, timeframe='1d', since: Int = None, limit: Int = None, params={}): + """ + Retrieves the open interest history of a currency + + https://www.okx.com/docs-v5/en/#rest-api-trading-data-get-contracts-open-interest-and-volume + https://www.okx.com/docs-v5/en/#rest-api-trading-data-get-options-open-interest-and-volume + + :param str symbol: Unified CCXT currency code or unified symbol + :param str timeframe: "5m", "1h", or "1d" for option only "1d" or "8h" + :param int [since]: The time in ms of the earliest record to retrieve unix timestamp + :param int [limit]: Not used by okx, but parsed internally by CCXT + :param dict [params]: Exchange specific parameters + :param int [params.until]: The time in ms of the latest record to retrieve unix timestamp + :returns: An array of `open interest structures ` + """ + options = self.safe_dict(self.options, 'fetchOpenInterestHistory', {}) + timeframes = self.safe_dict(options, 'timeframes', {}) + timeframe = self.safe_string(timeframes, timeframe, timeframe) + if timeframe != '5m' and timeframe != '1H' and timeframe != '1D': + raise BadRequest(self.id + ' fetchOpenInterestHistory cannot only use the 5m, 1h, and 1d timeframe') + await self.load_markets() + # handle unified currency code or symbol + currencyId = None + market = None + if (symbol in self.markets) or (symbol in self.markets_by_id): + market = self.market(symbol) + currencyId = market['baseId'] + else: + currency = self.currency(symbol) + currencyId = currency['id'] + request: dict = { + 'ccy': currencyId, + 'period': timeframe, + } + type = None + response = None + type, params = self.handle_market_type_and_params('fetchOpenInterestHistory', market, params) + if type == 'option': + response = await self.publicGetRubikStatOptionOpenInterestVolume(self.extend(request, params)) + else: + if since is not None: + request['begin'] = since + until = self.safe_integer(params, 'until') + if until is not None: + request['end'] = until + params = self.omit(params, ['until']) + response = await self.publicGetRubikStatContractsOpenInterestVolume(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # [ + # "1648221300000", # timestamp + # "2183354317.945", # open interest(USD) + # "74285877.617", # volume(USD) + # ], + # ... + # ], + # "msg": '' + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_open_interests_history(data, None, since, limit) + + def parse_open_interest(self, interest, market: Market = None): + # + # fetchOpenInterestHistory + # + # [ + # "1648221300000", # timestamp + # "2183354317.945", # open interest(USD) - (coin) for options + # "74285877.617", # volume(USD) - (coin) for options + # ] + # + # fetchOpenInterest + # + # { + # "instId": "BTC-USD-230520-25500-P", + # "instType": "OPTION", + # "oi": "300", + # "oiCcy": "3", + # "oiUsd": "3", + # "ts": "1684551166251" + # } + # + id = self.safe_string(interest, 'instId') + market = self.safe_market(id, market) + time = self.safe_integer(interest, 'ts') + timestamp = self.safe_integer(interest, 0, time) + baseVolume = None + quoteVolume = None + openInterestAmount = None + openInterestValue = None + type = self.safe_string(self.options, 'defaultType') + if isinstance(interest, list): + if type == 'option': + openInterestAmount = self.safe_number(interest, 1) + baseVolume = self.safe_number(interest, 2) + else: + openInterestValue = self.safe_number(interest, 1) + quoteVolume = self.safe_number(interest, 2) + else: + baseVolume = self.safe_number(interest, 'oiCcy') + openInterestAmount = self.safe_number(interest, 'oi') + openInterestValue = self.safe_number(interest, 'oiUsd') + return self.safe_open_interest({ + 'symbol': self.safe_symbol(id), + 'baseVolume': baseVolume, # deprecated + 'quoteVolume': quoteVolume, # deprecated + 'openInterestAmount': openInterestAmount, + 'openInterestValue': openInterestValue, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'info': interest, + }, market) + + def set_sandbox_mode(self, enable: bool): + super(okx, self).set_sandbox_mode(enable) + self.options['sandboxMode'] = enable + if enable: + self.headers['x-simulated-trading'] = '1' + elif 'x-simulated-trading' in self.headers: + self.headers = self.omit(self.headers, 'x-simulated-trading') + + async def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}): + """ + fetch deposit and withdraw fees + + https://www.okx.com/docs-v5/en/#rest-api-funding-get-currencies + + :param str[]|None codes: list of unified currency codes + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `fees structures ` + """ + await self.load_markets() + request = {} + if codes is not None: + ids = self.currency_ids(codes) + request['ccy'] = ','.join(ids) + response = await self.privateGetAssetCurrencies(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "canDep": True, + # "canInternal": False, + # "canWd": True, + # "ccy": "USDT", + # "chain": "USDT-TRC20", + # "logoLink": "https://static.coinall.ltd/cdn/assets/imgs/221/5F74EB20302D7761.png", + # "mainNet": False, + # "maxFee": "1.6", + # "maxWd": "8852150", + # "minFee": "0.8", + # "minWd": "2", + # "name": "Tether", + # "usedWdQuota": "0", + # "wdQuota": "500", + # "wdTickSz": "3" + # }, + # { + # "canDep": True, + # "canInternal": False, + # "canWd": True, + # "ccy": "USDT", + # "chain": "USDT-ERC20", + # "logoLink": "https://static.coinall.ltd/cdn/assets/imgs/221/5F74EB20302D7761.png", + # "mainNet": False, + # "maxFee": "16", + # "maxWd": "8852150", + # "minFee": "8", + # "minWd": "2", + # "name": "Tether", + # "usedWdQuota": "0", + # "wdQuota": "500", + # "wdTickSz": "3" + # }, + # ... + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data') + return self.parse_deposit_withdraw_fees(data, codes) + + def parse_deposit_withdraw_fees(self, response, codes=None, currencyIdKey=None): + # + # [ + # { + # "canDep": True, + # "canInternal": False, + # "canWd": True, + # "ccy": "USDT", + # "chain": "USDT-TRC20", + # "logoLink": "https://static.coinall.ltd/cdn/assets/imgs/221/5F74EB20302D7761.png", + # "mainNet": False, + # "maxFee": "1.6", + # "maxWd": "8852150", + # "minFee": "0.8", + # "minWd": "2", + # "name": "Tether", + # "usedWdQuota": "0", + # "wdQuota": "500", + # "wdTickSz": "3" + # } + # ] + # + depositWithdrawFees: dict = {} + codes = self.market_codes(codes) + for i in range(0, len(response)): + feeInfo = response[i] + currencyId = self.safe_string(feeInfo, 'ccy') + code = self.safe_currency_code(currencyId) + if (codes is None) or (self.in_array(code, codes)): + depositWithdrawFee = self.safe_value(depositWithdrawFees, code) + if depositWithdrawFee is None: + depositWithdrawFees[code] = self.deposit_withdraw_fee({}) + depositWithdrawFees[code]['info'][currencyId] = feeInfo + chain = self.safe_string(feeInfo, 'chain') + if chain is None: + continue + chainSplit = chain.split('-') + networkId = self.safe_value(chainSplit, 1) + withdrawFee = self.safe_number(feeInfo, 'fee') + withdrawResult: dict = { + 'fee': withdrawFee, + 'percentage': False if (withdrawFee is not None) else None, + } + depositResult: dict = { + 'fee': None, + 'percentage': None, + } + networkCode = self.network_id_to_code(networkId, code) + depositWithdrawFees[code]['networks'][networkCode] = { + 'withdraw': withdrawResult, + 'deposit': depositResult, + } + depositWithdrawCodes = list(depositWithdrawFees.keys()) + for i in range(0, len(depositWithdrawCodes)): + code = depositWithdrawCodes[i] + currency = self.currency(code) + depositWithdrawFees[code] = self.assign_default_deposit_withdraw_fees(depositWithdrawFees[code], currency) + return depositWithdrawFees + + async def fetch_settlement_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical settlement records + + https://www.okx.com/docs-v5/en/#rest-api-public-data-get-delivery-exercise-history + + :param str symbol: unified market symbol to fetch the settlement history for + :param int [since]: timestamp in ms + :param int [limit]: number of records + :param dict [params]: exchange specific params + :returns dict[]: a list of `settlement history objects ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchSettlementHistory() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + type = None + type, params = self.handle_market_type_and_params('fetchSettlementHistory', market, params) + if type != 'future' and type != 'option': + raise NotSupported(self.id + ' fetchSettlementHistory() supports futures and options markets only') + request: dict = { + 'instType': self.convert_to_instrument_type(type), + 'uly': market['baseId'] + '-' + market['quoteId'], + } + if since is not None: + request['before'] = since - 1 + if limit is not None: + request['limit'] = limit + response = await self.publicGetPublicDeliveryExerciseHistory(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "details": [ + # { + # "insId": "BTC-USD-230523-25750-C", + # "px": "27290.1486867000556483", + # "type": "exercised" + # }, + # ], + # "ts":"1684656000000" + # } + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + settlements = self.parse_settlements(data, market) + sorted = self.sort_by(settlements, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, market['symbol'], since, limit) + + def parse_settlement(self, settlement, market): + # + # { + # "insId": "BTC-USD-230521-28500-P", + # "px": "27081.2007345984751516", + # "type": "exercised" + # } + # + marketId = self.safe_string(settlement, 'insId') + return { + 'info': settlement, + 'symbol': self.safe_symbol(marketId, market), + 'price': self.safe_number(settlement, 'px'), + 'timestamp': None, + 'datetime': None, + } + + def parse_settlements(self, settlements, market): + # + # { + # "details": [ + # { + # "insId": "BTC-USD-230523-25750-C", + # "px": "27290.1486867000556483", + # "type": "exercised" + # }, + # ], + # "ts":"1684656000000" + # } + # + result = [] + for i in range(0, len(settlements)): + entry = settlements[i] + timestamp = self.safe_integer(entry, 'ts') + details = self.safe_list(entry, 'details', []) + for j in range(0, len(details)): + settlement = self.parse_settlement(details[j], market) + result.append(self.extend(settlement, { + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + })) + return result + + async def fetch_underlying_assets(self, params={}): + """ + fetches the market ids of underlying assets for a specific contract market type + + https://www.okx.com/docs-v5/en/#public-data-rest-api-get-underlying + + :param dict [params]: exchange specific params + :param str [params.type]: the contract market type, 'option', 'swap' or 'future', the default is 'option' + :returns dict[]: a list of `underlying assets ` + """ + await self.load_markets() + marketType = None + marketType, params = self.handle_market_type_and_params('fetchUnderlyingAssets', None, params) + if (marketType is None) or (marketType == 'spot'): + marketType = 'option' + if (marketType != 'option') and (marketType != 'swap') and (marketType != 'future'): + raise NotSupported(self.id + ' fetchUnderlyingAssets() supports contract markets only') + request: dict = { + 'instType': self.convert_to_instrument_type(marketType), + } + response = await self.publicGetPublicUnderlying(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # [ + # "BTC-USD", + # "ETH-USD" + # ] + # ], + # "msg": "" + # } + # + underlyings = self.safe_list(response, 'data', []) + return underlyings[0] + + async def fetch_greeks(self, symbol: str, params={}) -> Greeks: + """ + fetches an option contracts greeks, financial metrics used to measure the factors that affect the price of an options contract + + https://www.okx.com/docs-v5/en/#public-data-rest-api-get-option-market-data + + :param str symbol: unified symbol of the market to fetch greeks for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `greeks structure ` + """ + await self.load_markets() + market = self.market(symbol) + marketId = market['id'] + optionParts = marketId.split('-') + request: dict = { + 'uly': market['info']['uly'], + 'instFamily': market['info']['instFamily'], + 'expTime': self.safe_string(optionParts, 2), + } + response = await self.publicGetPublicOptSummary(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "askVol": "0", + # "bidVol": "0", + # "delta": "0.5105464486882039", + # "deltaBS": "0.7325502184143025", + # "fwdPx": "37675.80158694987186", + # "gamma": "-0.13183515090501083", + # "gammaBS": "0.000024139685826358558", + # "instId": "BTC-USD-240329-32000-C", + # "instType": "OPTION", + # "lever": "4.504428015946619", + # "markVol": "0.5916253554539876", + # "realVol": "0", + # "theta": "-0.0004202992014012855", + # "thetaBS": "-18.52354631567909", + # "ts": "1699586421976", + # "uly": "BTC-USD", + # "vega": "0.0020207455080045846", + # "vegaBS": "74.44022302387287", + # "volLv": "0.5948549730405797" + # }, + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + for i in range(0, len(data)): + entry = data[i] + entryMarketId = self.safe_string(entry, 'instId') + if entryMarketId == marketId: + return self.parse_greeks(entry, market) + return None + + async def fetch_all_greeks(self, symbols: Strings = None, params={}) -> List[Greeks]: + """ + fetches all option contracts greeks, financial metrics used to measure the factors that affect the price of an options contract + + https://www.okx.com/docs-v5/en/#public-data-rest-api-get-option-market-data + + :param str[] [symbols]: unified symbols of the markets to fetch greeks for, all markets are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str params['uly']: Underlying, either uly or instFamily is required + :param str params['instFamily']: Instrument family, either uly or instFamily is required + :returns dict: a `greeks structure ` + """ + await self.load_markets() + request: dict = {} + symbols = self.market_symbols(symbols, None, True, True, True) + symbolsLength = None + if symbols is not None: + symbolsLength = len(symbols) + if (symbols is None) or (symbolsLength != 1): + uly = self.safe_string(params, 'uly') + if uly is not None: + request['uly'] = uly + instFamily = self.safe_string(params, 'instFamily') + if instFamily is not None: + request['instFamily'] = instFamily + if (uly is None) and (instFamily is None): + raise BadRequest(self.id + ' fetchAllGreeks() requires either a uly or instFamily parameter') + market = None + if symbols is not None: + if symbolsLength == 1: + market = self.market(symbols[0]) + marketId = market['id'] + optionParts = marketId.split('-') + request['uly'] = market['info']['uly'] + request['instFamily'] = market['info']['instFamily'] + request['expTime'] = self.safe_string(optionParts, 2) + params = self.omit(params, ['uly', 'instFamily']) + response = await self.publicGetPublicOptSummary(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "askVol": "0", + # "bidVol": "0", + # "delta": "0.5105464486882039", + # "deltaBS": "0.7325502184143025", + # "fwdPx": "37675.80158694987186", + # "gamma": "-0.13183515090501083", + # "gammaBS": "0.000024139685826358558", + # "instId": "BTC-USD-240329-32000-C", + # "instType": "OPTION", + # "lever": "4.504428015946619", + # "markVol": "0.5916253554539876", + # "realVol": "0", + # "theta": "-0.0004202992014012855", + # "thetaBS": "-18.52354631567909", + # "ts": "1699586421976", + # "uly": "BTC-USD", + # "vega": "0.0020207455080045846", + # "vegaBS": "74.44022302387287", + # "volLv": "0.5948549730405797" + # }, + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_all_greeks(data, symbols) + + def parse_greeks(self, greeks: dict, market: Market = None) -> Greeks: + # + # { + # "askVol": "0", + # "bidVol": "0", + # "delta": "0.5105464486882039", + # "deltaBS": "0.7325502184143025", + # "fwdPx": "37675.80158694987186", + # "gamma": "-0.13183515090501083", + # "gammaBS": "0.000024139685826358558", + # "instId": "BTC-USD-240329-32000-C", + # "instType": "OPTION", + # "lever": "4.504428015946619", + # "markVol": "0.5916253554539876", + # "realVol": "0", + # "theta": "-0.0004202992014012855", + # "thetaBS": "-18.52354631567909", + # "ts": "1699586421976", + # "uly": "BTC-USD", + # "vega": "0.0020207455080045846", + # "vegaBS": "74.44022302387287", + # "volLv": "0.5948549730405797" + # } + # + timestamp = self.safe_integer(greeks, 'ts') + marketId = self.safe_string(greeks, 'instId') + symbol = self.safe_symbol(marketId, market) + return { + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'delta': self.safe_number(greeks, 'delta'), + 'gamma': self.safe_number(greeks, 'gamma'), + 'theta': self.safe_number(greeks, 'theta'), + 'vega': self.safe_number(greeks, 'vega'), + 'rho': None, + 'bidSize': None, + 'askSize': None, + 'bidImpliedVolatility': self.safe_number(greeks, 'bidVol'), + 'askImpliedVolatility': self.safe_number(greeks, 'askVol'), + 'markImpliedVolatility': self.safe_number(greeks, 'markVol'), + 'bidPrice': None, + 'askPrice': None, + 'markPrice': None, + 'lastPrice': None, + 'underlyingPrice': None, + 'info': greeks, + } + + async def close_position(self, symbol: str, side: OrderSide = None, params={}) -> Order: + """ + closes open positions for a market + + https://www.okx.com/docs-v5/en/#order-book-trading-trade-post-close-positions + + :param str symbol: Unified CCXT market symbol + :param str [side]: 'buy' or 'sell', leave in net mode + :param dict [params]: extra parameters specific to the okx api endpoint + :param str [params.clientOrderId]: a unique identifier for the order + :param str [params.marginMode]: 'cross' or 'isolated', default is 'cross + :param str [params.code]: *required in the case of closing cross MARGIN position for Single-currency margin* margin currency + + EXCHANGE SPECIFIC PARAMETERS + :param boolean [params.autoCxl]: whether any pending orders for closing out needs to be automatically canceled when close position via a market order. False or True, the default is False + :param str [params.tag]: order tag a combination of case-sensitive alphanumerics, all numbers, or all letters of up to 16 characters + :returns dict[]: `A list of position structures ` + """ + await self.load_markets() + market = self.market(symbol) + clientOrderId = self.safe_string(params, 'clientOrderId') + code = self.safe_string(params, 'code') + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('closePosition', params, 'cross') + request: dict = { + 'instId': market['id'], + 'mgnMode': marginMode, + } + if side is not None: + if (side == 'buy'): + request['posSide'] = 'long' + elif side == 'sell': + request['posSide'] = 'short' + else: + request['posSide'] = side + if clientOrderId is not None: + request['clOrdId'] = clientOrderId + if code is not None: + currency = self.currency(code) + request['ccy'] = currency['id'] + response = await self.privatePostTradeClosePosition(self.extend(request, params)) + # + # { + # "code": "1", + # "data": [ + # { + # "clOrdId":"e847386590ce4dBCe903bbc394dc88bf", + # "ordId":"", + # "sCode":"51000", + # "sMsg":"Parameter posSide error ", + # "tag":"e847386590ce4dBC" + # } + # ], + # "inTime": "1701877077101064", + # "msg": "All operations failed", + # "outTime": "1701877077102579" + # } + # + data = self.safe_list(response, 'data', []) + order = self.safe_dict(data, 0) + return self.parse_order(order, market) + + async def fetch_option(self, symbol: str, params={}) -> Option: + """ + fetches option data that is commonly found in an option chain + + https://www.okx.com/docs-v5/en/#order-book-trading-market-data-get-ticker + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `option chain structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + } + response = await self.publicGetMarketTicker(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "instType": "OPTION", + # "instId": "BTC-USD-241227-60000-P", + # "last": "", + # "lastSz": "0", + # "askPx": "", + # "askSz": "0", + # "bidPx": "", + # "bidSz": "0", + # "open24h": "", + # "high24h": "", + # "low24h": "", + # "volCcy24h": "0", + # "vol24h": "0", + # "ts": "1711176035035", + # "sodUtc0": "", + # "sodUtc8": "" + # } + # ] + # } + # + result = self.safe_list(response, 'data', []) + chain = self.safe_dict(result, 0, {}) + return self.parse_option(chain, None, market) + + async def fetch_option_chain(self, code: str, params={}) -> OptionChain: + """ + fetches data for an underlying asset that is commonly found in an option chain + + https://www.okx.com/docs-v5/en/#order-book-trading-market-data-get-tickers + + :param str code: base currency to fetch an option chain for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.uly]: the underlying asset, can be obtained from fetchUnderlyingAssets() + :returns dict: a list of `option chain structures ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'uly': currency['code'] + '-USD', + 'instType': 'OPTION', + } + response = await self.publicGetMarketTickers(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "instType": "OPTION", + # "instId": "BTC-USD-240323-52000-C", + # "last": "", + # "lastSz": "0", + # "askPx": "", + # "askSz": "0", + # "bidPx": "", + # "bidSz": "0", + # "open24h": "", + # "high24h": "", + # "low24h": "", + # "volCcy24h": "0", + # "vol24h": "0", + # "ts": "1711176207008", + # "sodUtc0": "", + # "sodUtc8": "" + # }, + # ] + # } + # + result = self.safe_list(response, 'data', []) + return self.parse_option_chain(result, None, 'instId') + + def parse_option(self, chain: dict, currency: Currency = None, market: Market = None) -> Option: + # + # { + # "instType": "OPTION", + # "instId": "BTC-USD-241227-60000-P", + # "last": "", + # "lastSz": "0", + # "askPx": "", + # "askSz": "0", + # "bidPx": "", + # "bidSz": "0", + # "open24h": "", + # "high24h": "", + # "low24h": "", + # "volCcy24h": "0", + # "vol24h": "0", + # "ts": "1711176035035", + # "sodUtc0": "", + # "sodUtc8": "" + # } + # + marketId = self.safe_string(chain, 'instId') + market = self.safe_market(marketId, market) + timestamp = self.safe_integer(chain, 'ts') + return { + 'info': chain, + 'currency': None, + 'symbol': market['symbol'], + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'impliedVolatility': None, + 'openInterest': None, + 'bidPrice': self.safe_number(chain, 'bidPx'), + 'askPrice': self.safe_number(chain, 'askPx'), + 'midPrice': None, + 'markPrice': None, + 'lastPrice': self.safe_number(chain, 'last'), + 'underlyingPrice': None, + 'change': None, + 'percentage': None, + 'baseVolume': self.safe_number(chain, 'volCcy24h'), + 'quoteVolume': None, + } + + async def fetch_convert_quote(self, fromCode: str, toCode: str, amount: Num = None, params={}) -> Conversion: + """ + fetch a quote for converting from one currency to another + + https://www.okx.com/docs-v5/en/#funding-account-rest-api-estimate-quote + + :param str fromCode: the currency that you want to sell and convert from + :param str toCode: the currency that you want to buy and convert into + :param float [amount]: how much you want to trade in units of the from currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `conversion structure ` + """ + await self.load_markets() + request: dict = { + 'baseCcy': fromCode.upper(), + 'quoteCcy': toCode.upper(), + 'rfqSzCcy': fromCode.upper(), + 'rfqSz': self.number_to_string(amount), + 'side': 'sell', + } + response = await self.privatePostAssetConvertEstimateQuote(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "baseCcy": "ETH", + # "baseSz": "0.01023052", + # "clQReqId": "", + # "cnvtPx": "2932.40104429", + # "origRfqSz": "30", + # "quoteCcy": "USDT", + # "quoteId": "quoterETH-USDT16461885104612381", + # "quoteSz": "30", + # "quoteTime": "1646188510461", + # "rfqSz": "30", + # "rfqSzCcy": "USDT", + # "side": "buy", + # "ttlMs": "10000" + # } + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + result = self.safe_dict(data, 0, {}) + fromCurrencyId = self.safe_string(result, 'baseCcy', fromCode) + fromCurrency = self.currency(fromCurrencyId) + toCurrencyId = self.safe_string(result, 'quoteCcy', toCode) + toCurrency = self.currency(toCurrencyId) + return self.parse_conversion(result, fromCurrency, toCurrency) + + async def create_convert_trade(self, id: str, fromCode: str, toCode: str, amount: Num = None, params={}) -> Conversion: + """ + convert from one currency to another + + https://www.okx.com/docs-v5/en/#funding-account-rest-api-convert-trade + + :param str id: the id of the trade that you want to make + :param str fromCode: the currency that you want to sell and convert from + :param str toCode: the currency that you want to buy and convert into + :param float [amount]: how much you want to trade in units of the from currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `conversion structure ` + """ + await self.load_markets() + request: dict = { + 'quoteId': id, + 'baseCcy': fromCode, + 'quoteCcy': toCode, + 'szCcy': fromCode, + 'sz': self.number_to_string(amount), + 'side': 'sell', + } + response = await self.privatePostAssetConvertTrade(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "baseCcy": "ETH", + # "clTReqId": "", + # "fillBaseSz": "0.01023052", + # "fillPx": "2932.40104429", + # "fillQuoteSz": "30", + # "instId": "ETH-USDT", + # "quoteCcy": "USDT", + # "quoteId": "quoterETH-USDT16461885104612381", + # "side": "buy", + # "state": "fullyFilled", + # "tradeId": "trader16461885203381437", + # "ts": "1646188520338" + # } + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + result = self.safe_dict(data, 0, {}) + fromCurrencyId = self.safe_string(result, 'baseCcy', fromCode) + fromCurrency = self.currency(fromCurrencyId) + toCurrencyId = self.safe_string(result, 'quoteCcy', toCode) + toCurrency = self.currency(toCurrencyId) + return self.parse_conversion(result, fromCurrency, toCurrency) + + async def fetch_convert_trade(self, id: str, code: Str = None, params={}) -> Conversion: + """ + fetch the data for a conversion trade + + https://www.okx.com/docs-v5/en/#funding-account-rest-api-get-convert-history + + :param str id: the id of the trade that you want to fetch + :param str [code]: the unified currency code of the conversion trade + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `conversion structure ` + """ + await self.load_markets() + request: dict = { + 'clTReqId': id, + } + response = await self.privateGetAssetConvertHistory(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "clTReqId": "", + # "instId": "ETH-USDT", + # "side": "buy", + # "fillPx": "2932.401044", + # "baseCcy": "ETH", + # "quoteCcy": "USDT", + # "fillBaseSz": "0.01023052", + # "state": "fullyFilled", + # "tradeId": "trader16461885203381437", + # "fillQuoteSz": "30", + # "ts": "1646188520000" + # } + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + result = self.safe_dict(data, 0, {}) + fromCurrencyId = self.safe_string(result, 'baseCcy') + toCurrencyId = self.safe_string(result, 'quoteCcy') + fromCurrency = None + toCurrency = None + if fromCurrencyId is not None: + fromCurrency = self.currency(fromCurrencyId) + if toCurrencyId is not None: + toCurrency = self.currency(toCurrencyId) + return self.parse_conversion(result, fromCurrency, toCurrency) + + async def fetch_convert_trade_history(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Conversion]: + """ + fetch the users history of conversion trades + + https://www.okx.com/docs-v5/en/#funding-account-rest-api-get-convert-history + + :param str [code]: the unified currency code + :param int [since]: the earliest time in ms to fetch conversions for + :param int [limit]: the maximum number of conversion structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest conversion to fetch + :returns dict[]: a list of `conversion structures ` + """ + await self.load_markets() + request: dict = {} + request, params = self.handle_until_option('after', request, params) + if since is not None: + request['before'] = since + if limit is not None: + request['limit'] = limit + response = await self.privateGetAssetConvertHistory(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "clTReqId": "", + # "instId": "ETH-USDT", + # "side": "buy", + # "fillPx": "2932.401044", + # "baseCcy": "ETH", + # "quoteCcy": "USDT", + # "fillBaseSz": "0.01023052", + # "state": "fullyFilled", + # "tradeId": "trader16461885203381437", + # "fillQuoteSz": "30", + # "ts": "1646188520000" + # } + # ], + # "msg": "" + # } + # + rows = self.safe_list(response, 'data', []) + return self.parse_conversions(rows, code, 'baseCcy', 'quoteCcy', since, limit) + + def parse_conversion(self, conversion: dict, fromCurrency: Currency = None, toCurrency: Currency = None) -> Conversion: + # + # fetchConvertQuote + # + # { + # "baseCcy": "ETH", + # "baseSz": "0.01023052", + # "clQReqId": "", + # "cnvtPx": "2932.40104429", + # "origRfqSz": "30", + # "quoteCcy": "USDT", + # "quoteId": "quoterETH-USDT16461885104612381", + # "quoteSz": "30", + # "quoteTime": "1646188510461", + # "rfqSz": "30", + # "rfqSzCcy": "USDT", + # "side": "buy", + # "ttlMs": "10000" + # } + # + # createConvertTrade + # + # { + # "baseCcy": "ETH", + # "clTReqId": "", + # "fillBaseSz": "0.01023052", + # "fillPx": "2932.40104429", + # "fillQuoteSz": "30", + # "instId": "ETH-USDT", + # "quoteCcy": "USDT", + # "quoteId": "quoterETH-USDT16461885104612381", + # "side": "buy", + # "state": "fullyFilled", + # "tradeId": "trader16461885203381437", + # "ts": "1646188520338" + # } + # + # fetchConvertTrade, fetchConvertTradeHistory + # + # { + # "clTReqId": "", + # "instId": "ETH-USDT", + # "side": "buy", + # "fillPx": "2932.401044", + # "baseCcy": "ETH", + # "quoteCcy": "USDT", + # "fillBaseSz": "0.01023052", + # "state": "fullyFilled", + # "tradeId": "trader16461885203381437", + # "fillQuoteSz": "30", + # "ts": "1646188520000" + # } + # + timestamp = self.safe_integer_2(conversion, 'quoteTime', 'ts') + fromCoin = self.safe_string(conversion, 'baseCcy') + fromCode = self.safe_currency_code(fromCoin, fromCurrency) + to = self.safe_string(conversion, 'quoteCcy') + toCode = self.safe_currency_code(to, toCurrency) + return { + 'info': conversion, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': self.safe_string_n(conversion, ['clQReqId', 'tradeId', 'quoteId']), + 'fromCurrency': fromCode, + 'fromAmount': self.safe_number_2(conversion, 'baseSz', 'fillBaseSz'), + 'toCurrency': toCode, + 'toAmount': self.safe_number_2(conversion, 'quoteSz', 'fillQuoteSz'), + 'price': self.safe_number_2(conversion, 'cnvtPx', 'fillPx'), + 'fee': None, + } + + async def fetch_convert_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies that can be converted + + https://www.okx.com/docs-v5/en/#funding-account-rest-api-get-convert-currencies + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + await self.load_markets() + response = await self.privateGetAssetConvertCurrencies(params) + # + # { + # "code": "0", + # "data": [ + # { + # "ccy": "BTC", + # "max": "", + # "min": "" + # }, + # ], + # "msg": "" + # } + # + result: dict = {} + data = self.safe_list(response, 'data', []) + for i in range(0, len(data)): + entry = data[i] + id = self.safe_string(entry, 'ccy') + code = self.safe_currency_code(id) + result[code] = { + 'info': entry, + 'id': id, + 'code': code, + 'networks': None, + 'type': None, + 'name': None, + 'active': None, + 'deposit': None, + 'withdraw': None, + 'fee': None, + 'precision': None, + 'limits': { + 'amount': { + 'min': self.safe_number(entry, 'min'), + 'max': self.safe_number(entry, 'max'), + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + 'deposit': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + } + return result + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if not response: + return None # fallback to default error handler + # + # { + # "code": "1", + # "data": [ + # { + # "clOrdId": "", + # "ordId": "", + # "sCode": "51119", + # "sMsg": "Order placement failed due to insufficient balance. ", + # "tag": "" + # } + # ], + # "msg": "" + # }, + # { + # "code": "58001", + # "data": [], + # "msg": "Incorrect trade password" + # } + # + code = self.safe_string(response, 'code') + if (code != '0') and (code != '2'): # 2 means that bulk operation partially succeeded + feedback = self.id + ' ' + body + data = self.safe_list(response, 'data', []) + for i in range(0, len(data)): + error = data[i] + errorCode = self.safe_string(error, 'sCode') + message = self.safe_string(error, 'sMsg') + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], code, feedback) + raise ExchangeError(feedback) # unknown message + return None + + async def fetch_margin_adjustment_history(self, symbol: Str = None, type: Str = None, since: Num = None, limit: Num = None, params={}) -> List[MarginModification]: + """ + fetches the history of margin added or reduced from contract isolated positions + + https://www.okx.com/docs-v5/en/#trading-account-rest-api-get-bills-details-last-7-days + https://www.okx.com/docs-v5/en/#trading-account-rest-api-get-bills-details-last-3-months + + :param str [symbol]: not used by okx fetchMarginAdjustmentHistory + :param str [type]: "add" or "reduce" + :param int [since]: the earliest time in ms to fetch margin adjustment history for + :param int [limit]: the maximum number of entries to retrieve + :param dict params: extra parameters specific to the exchange api endpoint + :param boolean [params.auto]: True if fetching auto margin increases + :returns dict[]: a list of `margin structures ` + """ + await self.load_markets() + auto = self.safe_bool(params, 'auto') + if type is None: + raise ArgumentsRequired(self.id + ' fetchMarginAdjustmentHistory() requires a type argument') + isAdd = type == 'add' + subType = '160' if isAdd else '161' + if auto: + if isAdd: + subType = '162' + else: + raise BadRequest(self.id + ' cannot fetch margin adjustments for type ' + type) + request: dict = { + 'subType': subType, + 'mgnMode': 'isolated', + } + until = self.safe_integer(params, 'until') + params = self.omit(params, 'until') + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + if until is not None: + request['endTime'] = until + response = None + now = self.milliseconds() + oneWeekAgo = now - 604800000 + threeMonthsAgo = now - 7776000000 + if (since is None) or (since > oneWeekAgo): + response = await self.privateGetAccountBills(self.extend(request, params)) + elif since > threeMonthsAgo: + response = await self.privateGetAccountBillsArchive(self.extend(request, params)) + else: + raise BadRequest(self.id + ' fetchMarginAdjustmentHistory() cannot fetch margin adjustments older than 3 months') + # + # { + # code: '0', + # data: [ + # { + # bal: '67621.4325135010619812', + # balChg: '-10.0000000000000000', + # billId: '691293628710342659', + # ccy: 'USDT', + # clOrdId: '', + # execType: '', + # fee: '0', + # fillFwdPx: '', + # fillIdxPx: '', + # fillMarkPx: '', + # fillMarkVol: '', + # fillPxUsd: '', + # fillPxVol: '', + # fillTime: '1711089244850', + # from: '', + # instId: 'XRP-USDT-SWAP', + # instType: 'SWAP', + # interest: '0', + # mgnMode: 'isolated', + # notes: '', + # ordId: '', + # pnl: '0', + # posBal: '73.12', + # posBalChg: '10.00', + # px: '', + # subType: '160', + # sz: '10', + # tag: '', + # to: '', + # tradeId: '0', + # ts: '1711089244699', + # type: '6' + # } + # ], + # msg: '' + # } + # + data = self.safe_list(response, 'data') + modifications = self.parse_margin_modifications(data) + return self.filter_by_symbol_since_limit(modifications, symbol, since, limit) + + async def fetch_positions_history(self, symbols: Strings = None, since: Int = None, limit: Int = None, params={}) -> List[Position]: + """ + fetches historical positions + + https://www.okx.com/docs-v5/en/#trading-account-rest-api-get-positions-history + + :param str [symbols]: unified market symbols + :param int [since]: timestamp in ms of the earliest position to fetch + :param int [limit]: the maximum amount of records to fetch, default=100, max=100 + :param dict params: extra parameters specific to the exchange api endpoint + :param str [params.marginMode]: "cross" or "isolated" + + EXCHANGE SPECIFIC PARAMETERS + :param str [params.instType]: margin, swap, futures or option + :param str [params.type]: the type of latest close position 1: close position partially, 2:close all, 3:liquidation, 4:partial liquidation; 5:adl, is it is the latest type if there are several types for the same position + :param str [params.posId]: position id, there is attribute expiration, the posid will be expired if it is more than 30 days after the last full close position, then position will use new posid + :param str [params.before]: timestamp in ms of the earliest position to fetch based on the last update time of the position + :param str [params.after]: timestamp in ms of the latest position to fetch based on the last update time of the position + :returns dict[]: a list of `position structures ` + """ + await self.load_markets() + marginMode = self.safe_string(params, 'marginMode') + instType = self.safe_string_upper(params, 'instType') + params = self.omit(params, ['until', 'marginMode', 'instType']) + if limit is None: + limit = 100 + request: dict = { + 'limit': limit, + } + if symbols is not None: + symbolsLength = len(symbols) + if symbolsLength == 1: + market = self.market(symbols[0]) + request['instId'] = market['id'] + if marginMode is not None: + request['mgnMode'] = marginMode + if instType is not None: + request['instType'] = instType + response = await self.privateGetAccountPositionsHistory(self.extend(request, params)) + # + # { + # code: '0', + # data: [ + # { + # cTime: '1708735940395', + # ccy: 'USDT', + # closeAvgPx: '0.6330444444444444', + # closeTotalPos: '27', + # direction: 'long', + # fee: '-1.69566', + # fundingFee: '-11.870404179341788', + # instId: 'XRP-USDT-SWAP', + # instType: 'SWAP', + # lever: '3.0', + # liqPenalty: '0', + # mgnMode: 'cross', + # openAvgPx: '0.623', + # openMaxPos: '15', + # pnl: '27.11999999999988', + # pnlRatio: '0.0241732402722634', + # posId: '681423155054862336', + # realizedPnl: '13.553935820658092', + # triggerPx: '', + # type: '2', + # uTime: '1711088748170', + # uly: 'XRP-USDT' + # }, + # ... + # ], + # msg: '' + # } + # + data = self.safe_list(response, 'data') + positions = self.parse_positions(data, symbols, params) + return self.filter_by_since_limit(positions, since, limit) + + async def fetch_long_short_ratio_history(self, symbol: Str = None, timeframe: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LongShortRatio]: + """ + fetches the long short ratio history for a unified market symbol + + https://www.okx.com/docs-v5/en/#trading-statistics-rest-api-get-contract-long-short-ratio + + :param str symbol: unified symbol of the market to fetch the long short ratio for + :param str [timeframe]: the period for the ratio + :param int [since]: the earliest time in ms to fetch ratios for + :param int [limit]: the maximum number of long short ratio structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest ratio to fetch + :returns dict[]: an array of `long short ratio structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + } + until = self.safe_string_2(params, 'until', 'end') + params = self.omit(params, 'until') + if until is not None: + request['end'] = until + if timeframe is not None: + request['period'] = timeframe + if since is not None: + request['begin'] = since + if limit is not None: + request['limit'] = limit + response = await self.publicGetRubikStatContractsLongShortAccountRatioContract(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # ["1729323600000", "0.9398602814619824"], + # ["1729323300000", "0.9398602814619824"], + # ["1729323000000", "0.9398602814619824"], + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + result = [] + for i in range(0, len(data)): + entry = data[i] + result.append({ + 'timestamp': self.safe_string(entry, 0), + 'longShortRatio': self.safe_string(entry, 1), + }) + return self.parse_long_short_ratio_history(result, market) + + def parse_long_short_ratio(self, info: dict, market: Market = None) -> LongShortRatio: + timestamp = self.safe_integer(info, 'timestamp') + symbol = None + if market is not None: + symbol = market['symbol'] + return { + 'info': info, + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'timeframe': None, + 'longShortRatio': self.safe_number(info, 'longShortRatio'), + } diff --git a/ccxt/async_support/okxus.py b/ccxt/async_support/okxus.py new file mode 100644 index 0000000..3befce7 --- /dev/null +++ b/ccxt/async_support/okxus.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.okx import okx +from ccxt.abstract.okxus import ImplicitAPI +from ccxt.base.types import Any + + +class okxus(okx, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(okxus, self).describe(), { + 'id': 'okxus', + 'name': 'OKX(US)', + 'certified': False, + 'pro': True, + 'hostname': 'us.okx.com', + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/152485636-38b19e4a-bece-4dec-979a-5982859ffc04.jpg', + 'api': { + 'rest': 'https://{hostname}', + }, + 'www': 'https://app.okx.com', + 'doc': 'https://app.okx.com/docs-v5/en/#overview', + 'fees': 'https://app.okx.com/pages/products/fees.html', + 'referral': { + 'url': 'https://www.app.okx.com/join/CCXT2023', + 'discount': 0.2, + }, + 'test': { + 'rest': 'https://{hostname}', + }, + }, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': None, + 'swap': False, + 'future': False, + 'option': False, + }, + 'features': { + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + }) diff --git a/ccxt/async_support/onetrading.py b/ccxt/async_support/onetrading.py new file mode 100644 index 0000000..857c8bf --- /dev/null +++ b/ccxt/async_support/onetrading.py @@ -0,0 +1,1827 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.onetrading import ImplicitAPI +from ccxt.base.types import Any, Balances, Currencies, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, TradingFees +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidAddress +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import NotSupported +from ccxt.base.errors import DDoSProtection +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class onetrading(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(onetrading, self).describe(), { + 'id': 'onetrading', + 'name': 'One Trading', + 'countries': ['AT'], # Austria + 'rateLimit': 300, + 'version': 'v1', + 'pro': True, + # new metainfo interface + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'cancelOrders': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createDepositAddress': False, + 'createOrder': True, + 'createReduceOnlyOrder': False, + 'createStopLimitOrder': True, + 'createStopMarketOrder': False, + 'createStopOrder': True, + 'fetchAccounts': False, + 'fetchAllGreeks': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDeposit': False, + 'fetchDepositAddress': False, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': False, + 'fetchDepositsWithdrawals': False, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLedger': False, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrice': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': False, + 'fetchOrderTrades': True, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': False, + 'fetchTradingFee': False, + 'fetchTradingFees': True, + 'fetchTransactionFee': False, + 'fetchTransactionFees': False, + 'fetchTransactions': False, + 'fetchTransfer': False, + 'fetchTransfers': False, + 'fetchUnderlyingAssets': False, + 'fetchVolatilityHistory': False, + 'fetchWithdrawal': False, + 'fetchWithdrawals': False, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'transfer': False, + 'withdraw': False, + }, + 'timeframes': { + '1m': '1/MINUTES', + '5m': '5/MINUTES', + '15m': '15/MINUTES', + '30m': '30/MINUTES', + '1h': '1/HOURS', + '4h': '4/HOURS', + '1d': '1/DAYS', + '1w': '1/WEEKS', + '1M': '1/MONTHS', + }, + 'urls': { + 'logo': 'https://github.com/ccxt/ccxt/assets/43336371/bdbc26fd-02f2-4ca7-9f1e-17333690bb1c', + 'api': { + 'public': 'https://api.onetrading.com/fast', + 'private': 'https://api.onetrading.com/fast', + }, + 'www': 'https://onetrading.com/', + 'doc': [ + 'https://docs.onetrading.com', + ], + 'fees': 'https://onetrading.com/fees', + }, + 'api': { + 'public': { + 'get': [ + 'currencies', + 'candlesticks/{instrument_code}', + 'fees', + 'instruments', + 'order-book/{instrument_code}', + 'market-ticker', + 'market-ticker/{instrument_code}', + 'time', + ], + }, + 'private': { + 'get': [ + 'account/balances', + 'account/fees', + 'account/orders', + 'account/orders/{order_id}', + 'account/orders/{order_id}/trades', + 'account/trades', + 'account/trades/{trade_id}', + ], + 'post': [ + 'account/orders', + ], + 'delete': [ + 'account/orders', + 'account/orders/{order_id}', + 'account/orders/client/{client_id}', + ], + }, + }, + 'fees': { + 'trading': { + 'tierBased': True, + 'percentage': True, + 'taker': self.parse_number('0.0015'), + 'maker': self.parse_number('0.001'), + 'tiers': [ + # volume in BTC + { + 'taker': [ + [self.parse_number('0'), self.parse_number('0.0015')], + [self.parse_number('100'), self.parse_number('0.0013')], + [self.parse_number('250'), self.parse_number('0.0013')], + [self.parse_number('1000'), self.parse_number('0.001')], + [self.parse_number('5000'), self.parse_number('0.0009')], + [self.parse_number('10000'), self.parse_number('0.00075')], + [self.parse_number('20000'), self.parse_number('0.00065')], + ], + 'maker': [ + [self.parse_number('0'), self.parse_number('0.001')], + [self.parse_number('100'), self.parse_number('0.001')], + [self.parse_number('250'), self.parse_number('0.0009')], + [self.parse_number('1000'), self.parse_number('0.00075')], + [self.parse_number('5000'), self.parse_number('0.0006')], + [self.parse_number('10000'), self.parse_number('0.0005')], + [self.parse_number('20000'), self.parse_number('0.0005')], + ], + }, + ], + }, + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': False, + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'exact': { + 'INVALID_CLIENT_UUID': InvalidOrder, + 'ORDER_NOT_FOUND': OrderNotFound, + 'ONLY_ONE_ERC20_ADDRESS_ALLOWED': InvalidAddress, + 'DEPOSIT_ADDRESS_NOT_USED': InvalidAddress, + 'INVALID_CREDENTIALS': AuthenticationError, + 'MISSING_CREDENTIALS': AuthenticationError, + 'INVALID_APIKEY': AuthenticationError, + 'INVALID_SCOPES': AuthenticationError, + 'INVALID_SUBJECT': AuthenticationError, + 'INVALID_ISSUER': AuthenticationError, + 'INVALID_AUDIENCE': AuthenticationError, + 'INVALID_DEVICE_ID': AuthenticationError, + 'INVALID_IP_RESTRICTION': AuthenticationError, + 'APIKEY_REVOKED': AuthenticationError, + 'APIKEY_EXPIRED': AuthenticationError, + 'SYNCHRONIZER_TOKEN_MISMATCH': AuthenticationError, + 'SESSION_EXPIRED': AuthenticationError, + 'INTERNAL_ERROR': AuthenticationError, + 'CLIENT_IP_BLOCKED': PermissionDenied, + 'MISSING_PERMISSION': PermissionDenied, + 'ILLEGAL_CHARS': BadRequest, + 'UNSUPPORTED_MEDIA_TYPE': BadRequest, + 'ACCOUNT_HISTORY_TIME_RANGE_TOO_BIG': BadRequest, + 'CANDLESTICKS_TIME_RANGE_TOO_BIG': BadRequest, + 'INVALID_INSTRUMENT_CODE': BadRequest, + 'INVALID_ORDER_TYPE': BadRequest, + 'INVALID_UNIT': BadRequest, + 'INVALID_PERIOD': BadRequest, + 'INVALID_TIME': BadRequest, + 'INVALID_DATE': BadRequest, + 'INVALID_CURRENCY': BadRequest, + 'INVALID_AMOUNT': BadRequest, + 'INVALID_PRICE': BadRequest, + 'INVALID_LIMIT': BadRequest, + 'INVALID_QUERY': BadRequest, + 'INVALID_CURSOR': BadRequest, + 'INVALID_ACCOUNT_ID': BadRequest, + 'INVALID_SIDE': InvalidOrder, + 'INVALID_ACCOUNT_HISTORY_FROM_TIME': BadRequest, + 'INVALID_ACCOUNT_HISTORY_MAX_PAGE_SIZE': BadRequest, + 'INVALID_ACCOUNT_HISTORY_TIME_PERIOD': BadRequest, + 'INVALID_ACCOUNT_HISTORY_TO_TIME': BadRequest, + 'INVALID_CANDLESTICKS_GRANULARITY': BadRequest, + 'INVALID_CANDLESTICKS_UNIT': BadRequest, + 'INVALID_ORDER_BOOK_DEPTH': BadRequest, + 'INVALID_ORDER_BOOK_LEVEL': BadRequest, + 'INVALID_PAGE_CURSOR': BadRequest, + 'INVALID_TIME_RANGE': BadRequest, + 'INVALID_TRADE_ID': BadRequest, + 'INVALID_UI_ACCOUNT_SETTINGS': BadRequest, + 'NEGATIVE_AMOUNT': InvalidOrder, + 'NEGATIVE_PRICE': InvalidOrder, + 'MIN_SIZE_NOT_SATISFIED': InvalidOrder, + 'BAD_AMOUNT_PRECISION': InvalidOrder, + 'BAD_PRICE_PRECISION': InvalidOrder, + 'BAD_TRIGGER_PRICE_PRECISION': InvalidOrder, + 'MAX_OPEN_ORDERS_EXCEEDED': BadRequest, + 'MISSING_PRICE': InvalidOrder, + 'MISSING_ORDER_TYPE': InvalidOrder, + 'MISSING_SIDE': InvalidOrder, + 'MISSING_CANDLESTICKS_PERIOD_PARAM': ArgumentsRequired, + 'MISSING_CANDLESTICKS_UNIT_PARAM': ArgumentsRequired, + 'MISSING_FROM_PARAM': ArgumentsRequired, + 'MISSING_INSTRUMENT_CODE': ArgumentsRequired, + 'MISSING_ORDER_ID': InvalidOrder, + 'MISSING_TO_PARAM': ArgumentsRequired, + 'MISSING_TRADE_ID': ArgumentsRequired, + 'INVALID_ORDER_ID': OrderNotFound, + 'NOT_FOUND': OrderNotFound, + 'INSUFFICIENT_LIQUIDITY': InsufficientFunds, + 'INSUFFICIENT_FUNDS': InsufficientFunds, + 'NO_TRADING': ExchangeNotAvailable, + 'SERVICE_UNAVAILABLE': ExchangeNotAvailable, + 'GATEWAY_TIMEOUT': ExchangeNotAvailable, + 'RATELIMIT': DDoSProtection, + 'CF_RATELIMIT': DDoSProtection, + 'INTERNAL_SERVER_ERROR': ExchangeError, + }, + 'broad': { + 'Order not found.': OrderNotFound, + }, + }, + 'commonCurrencies': { + 'MIOTA': 'IOTA', # https://github.com/ccxt/ccxt/issues/7487 + }, + # exchange-specific options + 'options': { + 'fetchTradingFees': { + 'method': 'fetchPrivateTradingFees', # or 'fetchPublicTradingFees' + }, + 'fiat': ['EUR', 'CHF'], + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': False, + 'triggerDirection': False, + 'triggerPriceType': None, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': False, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 100000, # todo + 'untilDays': 100000, # todo + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 100, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, # todo + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 100000, # todo + 'daysBackCanceled': 1 / 12, # todo + 'untilDays': 100000, # todo + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 5000, + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + }) + + async def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://docs.onetrading.com/#time + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = await self.publicGetTime(params) + # + # { + # "iso": "2020-07-10T05:17:26.716Z", + # "epoch_millis": 1594358246716, + # } + # + return self.safe_integer(response, 'epoch_millis') + + async def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://docs.onetrading.com/#currencies + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = await self.publicGetCurrencies(params) + # + # [ + # { + # "code": "USDT", + # "precision": 6, + # "unified_cryptoasset_id": 825, + # "name": "Tether USDt", + # "collateral_percentage": 0 + # }, + # ] + # + result: dict = {} + for i in range(0, len(response)): + currency = response[i] + id = self.safe_string(currency, 'code') + code = self.safe_currency_code(id) + result[code] = self.safe_currency_structure({ + 'id': id, + 'code': code, + 'name': self.safe_string(currency, 'name'), + 'info': currency, + 'active': None, + 'fee': None, + 'precision': self.parse_number(self.parse_precision(self.safe_string(currency, 'precision'))), + 'withdraw': None, + 'deposit': None, + 'limits': { + 'amount': {'min': None, 'max': None}, + 'withdraw': {'min': None, 'max': None}, + }, + 'networks': {}, + }) + return result + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for onetrading + + https://docs.onetrading.com/#instruments + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = await self.publicGetInstruments(params) + # + # [ + # { + # "state": "ACTIVE", + # "base": {code: "ETH", precision: 8}, + # "quote": {code: "CHF", precision: 2}, + # "amount_precision": 4, + # "market_precision": 2, + # "min_size": "10.0" + # } + # ] + # + return self.parse_markets(response) + + def parse_market(self, market: dict) -> Market: + # + # { + # "base":{ + # "code":"BTC", + # "precision":"5" + # }, + # "quote":{ + # "code":"USDC", + # "precision":"2" + # }, + # "amount_precision":"5", + # "market_precision":"2", + # "min_size":"10.0", + # "min_price":"1000", + # "max_price":"10000000", + # "id":"BTC_USDC", + # "type":"SPOT", + # "state":"ACTIVE" + # } + # + # + # { + # "base": { + # "code": "BTC", + # "precision": 5 + # }, + # "quote": { + # "code": "EUR", + # "precision": 2 + # }, + # "amount_precision": 5, + # "market_precision": 2, + # "min_size": "10.0", + # "min_price": "1000", + # "max_price": "10000000", + # "id": "BTC_EUR_P", + # "type": "PERP", + # "state": "ACTIVE" + # } + # + baseAsset = self.safe_dict(market, 'base', {}) + quoteAsset = self.safe_dict(market, 'quote', {}) + baseId = self.safe_string(baseAsset, 'code') + quoteId = self.safe_string(quoteAsset, 'code') + id = self.safe_string(market, 'id') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + state = self.safe_string(market, 'state') + type = self.safe_string(market, 'type') + isPerp = type == 'PERP' + symbol = base + '/' + quote + if isPerp: + symbol = symbol + ':' + quote + return { + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': quote if isPerp else None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': quoteId if isPerp else None, + 'type': 'swap' if isPerp else 'spot', + 'spot': not isPerp, + 'margin': False, + 'swap': isPerp, + 'future': False, + 'option': False, + 'active': (state == 'ACTIVE'), + 'contract': isPerp, + 'linear': True if isPerp else None, + 'inverse': False if isPerp else None, + 'contractSize': self.parse_number('1') if isPerp else None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(self.parse_precision(self.safe_string(market, 'amount_precision'))), + 'price': self.parse_number(self.parse_precision(self.safe_string(market, 'market_precision'))), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': None, + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': self.safe_number(market, 'min_size'), + 'max': None, + }, + }, + 'created': None, + 'info': market, + } + + async def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://docs.onetrading.com/#fee-groups + https://docs.onetrading.com/#fees + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.method]: fetchPrivateTradingFees or fetchPublicTradingFees + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + method = self.safe_string(params, 'method') + params = self.omit(params, 'method') + if method is None: + options = self.safe_value(self.options, 'fetchTradingFees', {}) + method = self.safe_string(options, 'method', 'fetchPrivateTradingFees') + if method == 'fetchPrivateTradingFees': + return await self.fetch_private_trading_fees(params) + elif method == 'fetchPublicTradingFees': + return await self.fetch_public_trading_fees(params) + else: + raise NotSupported(self.id + ' fetchTradingFees() does not support ' + method + ', fetchPrivateTradingFees and fetchPublicTradingFees are supported') + + async def fetch_public_trading_fees(self, params={}): + await self.load_markets() + response = await self.publicGetFees(params) + # + # [ + # { + # 'fee_group_id': 'SPOT', + # 'display_text': 'The fee plan for spot trading.', + # 'volume_currency': 'EUR', + # 'fee_tiers': [ + # { + # 'volume': '0', + # 'fee_group_id': 'SPOT', + # 'maker_fee': '0.1000', + # 'taker_fee': '0.2000', + # }, + # { + # 'volume': '10000', + # 'fee_group_id': 'SPOT', + # 'maker_fee': '0.0400', + # 'taker_fee': '0.0800', + # }, + # ], + # }, + # { + # 'fee_group_id': 'FUTURES', + # 'display_text': 'The fee plan for futures trading.', + # 'volume_currency': 'EUR', + # 'fee_tiers': [ + # { + # 'volume': '0', + # 'fee_group_id': 'FUTURES', + # 'maker_fee': '0.1000', + # 'taker_fee': '0.2000', + # }, + # { + # 'volume': '10000', + # 'fee_group_id': 'FUTURES', + # 'maker_fee': '0.0400', + # 'taker_fee': '0.0800', + # }, + # ], + # }, + # ] + # + spotFees = self.safe_dict(response, 0, {}) + futuresFees = self.safe_dict(response, 1, {}) + spotFeeTiers = self.safe_list(spotFees, 'fee_tiers', []) + futuresFeeTiers = self.safe_list(futuresFees, 'fee_tiers', []) + spotTiers = self.parse_fee_tiers(spotFeeTiers) + futuresTiers = self.parse_fee_tiers(futuresFeeTiers) + firstSpotTier = self.safe_dict(spotTiers, 0, {}) + firstFuturesTier = self.safe_dict(futuresTiers, 0, {}) + result: dict = {} + for i in range(0, len(self.symbols)): + symbol = self.symbols[i] + market = self.market(symbol) + tierObject = firstSpotTier if (market['spot']) else firstFuturesTier + result[symbol] = { + 'info': spotFees, + 'symbol': symbol, + 'maker': self.safe_number(tierObject, 'maker_fee'), + 'taker': self.safe_number(tierObject, 'taker_fee'), + 'percentage': True, + 'tierBased': True, + 'tiers': spotTiers, + } + return result + + async def fetch_private_trading_fees(self, params={}): + await self.load_markets() + response = await self.privateGetAccountFees(params) + # + # { + # "account_id":"b7f4e27e-b34a-493a-b0d4-4bd341a3f2e0", + # "running_volumes":[ + # { + # "fee_group_id":"SPOT", + # "volume":"0", + # "currency":"EUR" + # }, + # { + # "fee_group_id":"FUTURES", + # "volume":"0", + # "currency":"EUR" + # } + # ], + # "active_fee_tiers":[ + # { + # "fee_group_id":"SPOT", + # "volume":"0", + # "maker_fee":"0.1000", + # "taker_fee":"0.2000" + # }, + # { + # "fee_group_id":"FUTURES", + # "volume":"0", + # "maker_fee":"0.1000", + # "taker_fee":"0.2000" + # } + # ] + # } + # + activeFeeTier = self.safe_list(response, 'active_fee_tiers') + spotFees = self.safe_dict(activeFeeTier, 0, {}) + futuresFees = self.safe_dict(activeFeeTier, 1, {}) + spotMakerFee = self.safe_string(spotFees, 'maker_fee') + spotTakerFee = self.safe_string(spotFees, 'taker_fee') + spotMakerFee = Precise.string_div(spotMakerFee, '100') + spotTakerFee = Precise.string_div(spotTakerFee, '100') + # feeTiers = self.safe_value(response, 'fee_tiers') + futuresMakerFee = self.safe_string(futuresFees, 'maker_fee') + futuresTakerFee = self.safe_string(futuresFees, 'taker_fee') + futuresMakerFee = Precise.string_div(futuresMakerFee, '100') + futuresTakerFee = Precise.string_div(futuresTakerFee, '100') + result: dict = {} + # tiers = self.parse_fee_tiers(feeTiers) + for i in range(0, len(self.symbols)): + symbol = self.symbols[i] + market = self.market(symbol) + makerFee = spotMakerFee if (market['spot']) else futuresMakerFee + takerFee = spotTakerFee if (market['spot']) else futuresTakerFee + result[symbol] = { + 'info': response, + 'symbol': symbol, + 'maker': self.parse_number(makerFee), + 'taker': self.parse_number(takerFee), + 'percentage': True, + 'tierBased': True, + 'tiers': None, + } + return result + + def parse_fee_tiers(self, feeTiers, market: Market = None): + takerFees = [] + makerFees = [] + for i in range(0, len(feeTiers)): + tier = feeTiers[i] + volume = self.safe_number(tier, 'volume') + taker = self.safe_string(tier, 'taker_fee') + maker = self.safe_string(tier, 'maker_fee') + maker = Precise.string_div(maker, '100') + taker = Precise.string_div(taker, '100') + makerFees.append([volume, self.parse_number(maker)]) + takerFees.append([volume, self.parse_number(taker)]) + return { + 'maker': makerFees, + 'taker': takerFees, + } + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # fetchTicker, fetchTickers + # + # { + # "instrument_code":"BTC_EUR", + # "sequence":602562, + # "time":"2020-07-10T06:27:34.951Z", + # "state":"ACTIVE", + # "is_frozen":0, + # "quote_volume":"1695555.1783768", + # "base_volume":"205.67436", + # "last_price":"8143.91", + # "best_bid":"8143.71", + # "best_ask":"8156.9", + # "price_change":"-147.47", + # "price_change_percentage":"-1.78", + # "high":"8337.45", + # "low":"8110.0" + # } + # + timestamp = self.parse8601(self.safe_string(ticker, 'time')) + marketId = self.safe_string(ticker, 'instrument_code') + symbol = self.safe_symbol(marketId, market, '_') + last = self.safe_string(ticker, 'last_price') + percentage = self.safe_string(ticker, 'price_change_percentage') + change = self.safe_string(ticker, 'price_change') + baseVolume = self.safe_string(ticker, 'base_volume') + quoteVolume = self.safe_string(ticker, 'quote_volume') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': self.safe_string(ticker, 'best_bid'), + 'bidVolume': None, + 'ask': self.safe_string(ticker, 'best_ask'), + 'askVolume': None, + 'vwap': None, + 'open': None, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': change, + 'percentage': percentage, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'info': ticker, + }, market) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://docs.onetrading.com/#market-ticker-for-instrument + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'instrument_code': market['id'], + } + response = await self.publicGetMarketTickerInstrumentCode(self.extend(request, params)) + # + # { + # "instrument_code":"BTC_EUR", + # "sequence":602562, + # "time":"2020-07-10T06:27:34.951Z", + # "state":"ACTIVE", + # "is_frozen":0, + # "quote_volume":"1695555.1783768", + # "base_volume":"205.67436", + # "last_price":"8143.91", + # "best_bid":"8143.71", + # "best_ask":"8156.9", + # "price_change":"-147.47", + # "price_change_percentage":"-1.78", + # "high":"8337.45", + # "low":"8110.0" + # } + # + return self.parse_ticker(response, market) + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://docs.onetrading.com/#market-ticker + + :param str[] [symbols]: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + response = await self.publicGetMarketTicker(params) + # + # [ + # { + # "instrument_code":"BTC_EUR", + # "sequence":602562, + # "time":"2020-07-10T06:27:34.951Z", + # "state":"ACTIVE", + # "is_frozen":0, + # "quote_volume":"1695555.1783768", + # "base_volume":"205.67436", + # "last_price":"8143.91", + # "best_bid":"8143.71", + # "best_ask":"8156.9", + # "price_change":"-147.47", + # "price_change_percentage":"-1.78", + # "high":"8337.45", + # "low":"8110.0" + # } + # ] + # + result: dict = {} + for i in range(0, len(response)): + ticker = self.parse_ticker(response[i]) + symbol = ticker['symbol'] + result[symbol] = ticker + return self.filter_by_array_tickers(result, 'symbol', symbols) + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://docs.onetrading.com/#order-book + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'instrument_code': market['id'], + # level 1 means only the best bid and ask + # level 2 is a compiled order book up to market precision + # level 3 is a full orderbook + # if you wish to get regular updates about orderbooks please use the Websocket channel + # heavy usage of self endpoint may result in limited access according to rate limits rules + # 'level': 3, # default + } + if limit is not None: + request['depth'] = limit + response = await self.publicGetOrderBookInstrumentCode(self.extend(request, params)) + # + # level 1 + # + # { + # "instrument_code":"BTC_EUR", + # "time":"2020-07-10T07:39:06.343Z", + # "asks":{ + # "value":{ + # "price":"8145.29", + # "amount":"0.96538", + # "number_of_orders":1 + # } + # }, + # "bids":{ + # "value":{ + # "price":"8134.0", + # "amount":"1.5978", + # "number_of_orders":5 + # } + # } + # } + # + # level 2 + # + # { + # "instrument_code":"BTC_EUR","time":"2020-07-10T07:36:43.538Z", + # "asks":[ + # {"price":"8146.59","amount":"0.89691","number_of_orders":1}, + # {"price":"8146.89","amount":"1.92062","number_of_orders":1}, + # {"price":"8169.5","amount":"0.0663","number_of_orders":1}, + # ], + # "bids":[ + # {"price":"8143.49","amount":"0.01329","number_of_orders":1}, + # {"price":"8137.01","amount":"5.34748","number_of_orders":1}, + # {"price":"8137.0","amount":"2.0","number_of_orders":1}, + # ] + # } + # + # level 3 + # + # { + # "instrument_code":"BTC_EUR", + # "time":"2020-07-10T07:32:31.525Z", + # "bids":[ + # {"price":"8146.79","amount":"0.01537","order_id":"5d717da1-a8f4-422d-afcc-03cb6ab66825"}, + # {"price":"8139.32","amount":"3.66009","order_id":"d0715c68-f28d-4cf1-a450-d56cf650e11c"}, + # {"price":"8137.51","amount":"2.61049","order_id":"085fd6f4-e835-4ca5-9449-a8f165772e60"}, + # ], + # "asks":[ + # {"price":"8153.49","amount":"0.93384","order_id":"755d3aa3-42b5-46fa-903d-98f42e9ae6c4"}, + # {"price":"8153.79","amount":"1.80456","order_id":"62034cf3-b70d-45ff-b285-ba6307941e7c"}, + # {"price":"8167.9","amount":"0.0018","order_id":"036354e0-71cd-492f-94f2-01f7d4b66422"}, + # ] + # } + # + timestamp = self.parse8601(self.safe_string(response, 'time')) + return self.parse_order_book(response, market['symbol'], timestamp, 'bids', 'asks', 'price', 'amount') + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # { + # "instrument_code":"BTC_EUR", + # "granularity":{"unit":"HOURS","period":1}, + # "high":"9252.65", + # "low":"9115.27", + # "open":"9250.0", + # "close":"9132.35", + # "total_amount":"33.85924", + # "volume":"311958.9635744", + # "time":"2020-05-08T22:59:59.999Z", + # "last_sequence":461123 + # } + # + granularity = self.safe_value(ohlcv, 'granularity') + unit = self.safe_string(granularity, 'unit') + period = self.safe_string(granularity, 'period') + units: dict = { + 'MINUTES': 'm', + 'HOURS': 'h', + 'DAYS': 'd', + 'WEEKS': 'w', + 'MONTHS': 'M', + } + lowercaseUnit = self.safe_string(units, unit) + timeframe = period + lowercaseUnit + durationInSeconds = self.parse_timeframe(timeframe) + duration = durationInSeconds * 1000 + timestamp = self.parse8601(self.safe_string(ohlcv, 'time')) + alignedTimestamp = duration * self.parse_to_int(timestamp / duration) + options = self.safe_value(self.options, 'fetchOHLCV', {}) + volumeField = self.safe_string(options, 'volume', 'total_amount') + return [ + alignedTimestamp, + self.safe_number(ohlcv, 'open'), + self.safe_number(ohlcv, 'high'), + self.safe_number(ohlcv, 'low'), + self.safe_number(ohlcv, 'close'), + self.safe_number(ohlcv, volumeField), + ] + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://docs.onetrading.com/#candlesticks + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + periodUnit = self.safe_string(self.timeframes, timeframe) + period, unit = periodUnit.split('/') + durationInSeconds = self.parse_timeframe(timeframe) + duration = durationInSeconds * 1000 + if limit is None: + limit = 1500 + request: dict = { + 'instrument_code': market['id'], + # 'from': self.iso8601(since), + # 'to': self.iso8601(self.milliseconds()), + 'period': period, + 'unit': unit, + } + if since is None: + now = self.milliseconds() + request['to'] = self.iso8601(now) + request['from'] = self.iso8601(now - limit * duration) + else: + request['from'] = self.iso8601(since) + request['to'] = self.iso8601(self.sum(since, limit * duration)) + response = await self.publicGetCandlesticksInstrumentCode(self.extend(request, params)) + # + # [ + # {"instrument_code":"BTC_EUR","granularity":{"unit":"HOURS","period":1},"high":"9252.65","low":"9115.27","open":"9250.0","close":"9132.35","total_amount":"33.85924","volume":"311958.9635744","time":"2020-05-08T22:59:59.999Z","last_sequence":461123}, + # {"instrument_code":"BTC_EUR","granularity":{"unit":"HOURS","period":1},"high":"9162.49","low":"9040.0","open":"9132.53","close":"9083.69","total_amount":"26.19685","volume":"238553.7812365","time":"2020-05-08T23:59:59.999Z","last_sequence":461376}, + # {"instrument_code":"BTC_EUR","granularity":{"unit":"HOURS","period":1},"high":"9135.7","low":"9002.59","open":"9055.45","close":"9133.98","total_amount":"26.21919","volume":"238278.8724959","time":"2020-05-09T00:59:59.999Z","last_sequence":461521}, + # ] + # + ohlcv = self.safe_list(response, 'candlesticks') + return self.parse_ohlcvs(ohlcv, market, timeframe, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(public) + # + # { + # "instrument_code":"BTC_EUR", + # "price":"8137.28", + # "amount":"0.22269", + # "taker_side":"BUY", + # "volume":"1812.0908832", + # "time":"2020-07-10T14:44:32.299Z", + # "trade_timestamp":1594392272299, + # "sequence":603047 + # } + # + # fetchMyTrades, fetchOrder, fetchOpenOrders, fetchClosedOrders trades(private) + # + # { + # "fee": { + # "fee_amount": "0.0014", + # "fee_currency": "BTC", + # "fee_percentage": "0.1", + # "fee_group_id": "default", + # "fee_type": "TAKER", + # "running_trading_volume": "0.0" + # }, + # "trade": { + # "trade_id": "fdff2bcc-37d6-4a2d-92a5-46e09c868664", + # "order_id": "36bb2437-7402-4794-bf26-4bdf03526439", + # "account_id": "a4c699f6-338d-4a26-941f-8f9853bfc4b9", + # "amount": "1.4", + # "side": "BUY", + # "instrument_code": "BTC_EUR", + # "price": "7341.4", + # "time": "2019-09-27T15:05:32.564Z", + # "sequence": 48670 + # } + # } + # + feeInfo = self.safe_value(trade, 'fee', {}) + trade = self.safe_value(trade, 'trade', trade) + timestamp = self.safe_integer(trade, 'trade_timestamp') + if timestamp is None: + timestamp = self.parse8601(self.safe_string(trade, 'time')) + side = self.safe_string_lower_2(trade, 'side', 'taker_side') + priceString = self.safe_string(trade, 'price') + amountString = self.safe_string(trade, 'amount') + costString = self.safe_string(trade, 'volume') + marketId = self.safe_string(trade, 'instrument_code') + symbol = self.safe_symbol(marketId, market, '_') + feeCostString = self.safe_string(feeInfo, 'fee_amount') + takerOrMaker = None + fee = None + if feeCostString is not None: + feeCurrencyId = self.safe_string(feeInfo, 'fee_currency') + feeCurrencyCode = self.safe_currency_code(feeCurrencyId) + feeRateString = self.safe_string(feeInfo, 'fee_percentage') + fee = { + 'cost': feeCostString, + 'currency': feeCurrencyCode, + 'rate': feeRateString, + } + takerOrMaker = self.safe_string_lower(feeInfo, 'fee_type') + return self.safe_trade({ + 'id': self.safe_string_2(trade, 'trade_id', 'sequence'), + 'order': self.safe_string(trade, 'order_id'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'type': None, + 'side': side, + 'price': priceString, + 'amount': amountString, + 'cost': costString, + 'takerOrMaker': takerOrMaker, + 'fee': fee, + 'info': trade, + }, market) + + def parse_balance(self, response) -> Balances: + balances = self.safe_value(response, 'balances', []) + result: dict = {'info': response} + for i in range(0, len(balances)): + balance = balances[i] + currencyId = self.safe_string(balance, 'currency_code') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(balance, 'available') + account['used'] = self.safe_string(balance, 'locked') + result[code] = account + return self.safe_balance(result) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://docs.onetrading.com/#balances + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + response = await self.privateGetAccountBalances(params) + # + # { + # "account_id":"4b95934f-55f1-460c-a525-bd5afc0cf071", + # "balances":[ + # { + # "account_id":"4b95934f-55f1-460c-a525-bd5afc0cf071", + # "currency_code":"BTC", + # "change":"10.0", + # "available":"10.0", + # "locked":"0.0", + # "sequence":142135994, + # "time":"2020-07-01T10:57:32.959Z" + # } + # ] + # } + # + return self.parse_balance(response) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'FILLED': 'open', + 'FILLED_FULLY': 'closed', + 'FILLED_CLOSED': 'canceled', + 'FILLED_REJECTED': 'rejected', + 'OPEN': 'open', + 'REJECTED': 'rejected', + 'CLOSED': 'canceled', + 'FAILED': 'failed', + 'STOP_TRIGGERED': 'triggered', + 'DONE': 'closed', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder + # + # { + # "order_id": "d5492c24-2995-4c18-993a-5b8bf8fffc0d", + # "client_id": "d75fb03b-b599-49e9-b926-3f0b6d103206", + # "account_id": "a4c699f6-338d-4a26-941f-8f9853bfc4b9", + # "instrument_code": "BTC_EUR", + # "time": "2019-08-01T08:00:44.026Z", + # "side": "BUY", + # "price": "5000", + # "amount": "1", + # "filled_amount": "0.5", + # "type": "LIMIT", + # "time_in_force": "GOOD_TILL_CANCELLED" + # } + # + # fetchOrder, fetchOpenOrders, fetchClosedOrders + # + # { + # "order": { + # "order_id": "66756a10-3e86-48f4-9678-b634c4b135b2", + # "account_id": "1eb2ad5d-55f1-40b5-bc92-7dc05869e905", + # "instrument_code": "BTC_EUR", + # "amount": "1234.5678", + # "filled_amount": "1234.5678", + # "side": "BUY", + # "type": "LIMIT", + # "status": "OPEN", + # "sequence": 123456789, + # "price": "1234.5678", + # "average_price": "1234.5678", + # "reason": "INSUFFICIENT_FUNDS", + # "time": "2019-08-24T14:15:22Z", + # "time_in_force": "GOOD_TILL_CANCELLED", + # "time_last_updated": "2019-08-24T14:15:22Z", + # "expire_after": "2019-08-24T14:15:22Z", + # "is_post_only": False, + # "time_triggered": "2019-08-24T14:15:22Z", + # "trigger_price": "1234.5678" + # }, + # "trades": [ + # { + # "fee": { + # "fee_amount": "0.0014", + # "fee_currency": "BTC", + # "fee_percentage": "0.1", + # "fee_group_id": "default", + # "fee_type": "TAKER", + # "running_trading_volume": "0.0" + # }, + # "trade": { + # "trade_id": "fdff2bcc-37d6-4a2d-92a5-46e09c868664", + # "order_id": "36bb2437-7402-4794-bf26-4bdf03526439", + # "account_id": "a4c699f6-338d-4a26-941f-8f9853bfc4b9", + # "amount": "1.4", + # "side": "BUY", + # "instrument_code": "BTC_EUR", + # "price": "7341.4", + # "time": "2019-09-27T15:05:32.564Z", + # "sequence": 48670 + # } + # } + # ] + # } + # + rawOrder = self.safe_value(order, 'order', order) + id = self.safe_string(rawOrder, 'order_id') + clientOrderId = self.safe_string(rawOrder, 'client_id') + timestamp = self.parse8601(self.safe_string(rawOrder, 'time')) + rawStatus = self.parse_order_status(self.safe_string(rawOrder, 'status')) + status = self.parse_order_status(rawStatus) + marketId = self.safe_string(rawOrder, 'instrument_code') + symbol = self.safe_symbol(marketId, market, '_') + price = self.safe_string(rawOrder, 'price') + amount = self.safe_string(rawOrder, 'amount') + filled = self.safe_string(rawOrder, 'filled_amount') + side = self.safe_string_lower(rawOrder, 'side') + type = self.safe_string_lower(rawOrder, 'type') + timeInForce = self.parse_time_in_force(self.safe_string(rawOrder, 'time_in_force')) + postOnly = self.safe_value(rawOrder, 'is_post_only') + rawTrades = self.safe_value(order, 'trades', []) + return self.safe_order({ + 'id': id, + 'clientOrderId': clientOrderId, + 'info': order, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'symbol': symbol, + 'type': self.parse_order_type(type), + 'timeInForce': timeInForce, + 'postOnly': postOnly, + 'side': side, + 'price': price, + 'triggerPrice': self.safe_number(rawOrder, 'trigger_price'), + 'amount': amount, + 'cost': None, + 'average': None, + 'filled': filled, + 'remaining': None, + 'status': status, + # 'fee': None, + 'trades': rawTrades, + }, market) + + def parse_order_type(self, type: Str): + types: dict = { + 'booked': 'limit', + } + return self.safe_string(types, type, type) + + def parse_time_in_force(self, timeInForce: Str): + timeInForces: dict = { + 'GOOD_TILL_CANCELLED': 'GTC', + 'GOOD_TILL_TIME': 'GTT', + 'IMMEDIATE_OR_CANCELLED': 'IOC', + 'FILL_OR_KILL': 'FOK', + } + return self.safe_string(timeInForces, timeInForce, timeInForce) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://docs.onetrading.com/#create-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: onetrading only does stop limit orders and does not do stop market + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + uppercaseType = type.upper() + request: dict = { + 'instrument_code': market['id'], + 'type': uppercaseType, # LIMIT, MARKET, STOP + 'side': side.upper(), # or SELL + 'amount': self.amount_to_precision(symbol, amount), + # "price": "1234.5678", # required for LIMIT and STOP orders + # "client_id": "d75fb03b-b599-49e9-b926-3f0b6d103206", # optional + # "time_in_force": "GOOD_TILL_CANCELLED", # limit orders only, GOOD_TILL_CANCELLED, GOOD_TILL_TIME, IMMEDIATE_OR_CANCELLED and FILL_OR_KILL + # "expire_after": "2020-07-02T19:40:13Z", # required for GOOD_TILL_TIME + # "is_post_only": False, # limit orders only, optional + # "trigger_price": "1234.5678" # required for stop orders + } + priceIsRequired = False + if uppercaseType == 'LIMIT' or uppercaseType == 'STOP': + priceIsRequired = True + triggerPrice = self.safe_number_n(params, ['triggerPrice', 'trigger_price', 'stopPrice']) + if triggerPrice is not None: + if uppercaseType == 'MARKET': + raise BadRequest(self.id + ' createOrder() cannot place stop market orders, only stop limit') + request['trigger_price'] = self.price_to_precision(symbol, triggerPrice) + request['type'] = 'STOP' + params = self.omit(params, ['triggerPrice', 'trigger_price', 'stopPrice']) + elif uppercaseType == 'STOP': + raise ArgumentsRequired(self.id + ' createOrder() requires a triggerPrice param for ' + type + ' orders') + if priceIsRequired: + request['price'] = self.price_to_precision(symbol, price) + clientOrderId = self.safe_string_2(params, 'clientOrderId', 'client_id') + if clientOrderId is not None: + request['client_id'] = clientOrderId + params = self.omit(params, ['clientOrderId', 'client_id']) + timeInForce = self.safe_string_2(params, 'timeInForce', 'time_in_force', 'GOOD_TILL_CANCELLED') + params = self.omit(params, 'timeInForce') + request['time_in_force'] = timeInForce + response = await self.privatePostAccountOrders(self.extend(request, params)) + # + # { + # "order_id": "d5492c24-2995-4c18-993a-5b8bf8fffc0d", + # "client_id": "d75fb03b-b599-49e9-b926-3f0b6d103206", + # "account_id": "a4c699f6-338d-4a26-941f-8f9853bfc4b9", + # "instrument_code": "BTC_EUR", + # "time": "2019-08-01T08:00:44.026Z", + # "side": "BUY", + # "price": "5000", + # "amount": "1", + # "filled_amount": "0.5", + # "type": "LIMIT", + # "time_in_force": "GOOD_TILL_CANCELLED" + # } + # + return self.parse_order(response, market) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://docs.onetrading.com/#close-order-by-order-id + + :param str id: order id + :param str symbol: not used by bitmex cancelOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + clientOrderId = self.safe_string_2(params, 'clientOrderId', 'client_id') + params = self.omit(params, ['clientOrderId', 'client_id']) + method = 'privateDeleteAccountOrdersOrderId' + request: dict = {} + if clientOrderId is not None: + method = 'privateDeleteAccountOrdersClientClientId' + request['client_id'] = clientOrderId + else: + request['order_id'] = id + response = None + if method == 'privateDeleteAccountOrdersOrderId': + response = await self.privateDeleteAccountOrdersOrderId(self.extend(request, params)) + else: + response = await self.privateDeleteAccountOrdersClientClientId(self.extend(request, params)) + # + # responds with an empty body + # + return self.parse_order(response) + + async def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders + + https://docs.onetrading.com/#close-all-orders + + :param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['instrument_code'] = market['id'] + response = await self.privateDeleteAccountOrders(self.extend(request, params)) + # + # [ + # "a10e9bd1-8f72-4cfe-9f1b-7f1c8a9bd8ee" + # ] + # + return [self.safe_order({'info': response})] + + async def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): + """ + cancel multiple orders + + https://docs.onetrading.com/#close-all-orders + + :param str[] ids: order ids + :param str symbol: unified market symbol, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an list of `order structures ` + """ + await self.load_markets() + request: dict = { + 'ids': ','.join(ids), + } + response = await self.privateDeleteAccountOrders(self.extend(request, params)) + # + # [ + # "a10e9bd1-8f72-4cfe-9f1b-7f1c8a9bd8ee" + # ] + # + order = self.safe_order({'info': response}) + return [order] + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://docs.onetrading.com/#get-order + + :param str id: the order id + :param str symbol: not used by onetrading fetchOrder + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + request: dict = { + 'order_id': id, + } + response = await self.privateGetAccountOrdersOrderId(self.extend(request, params)) + # + # { + # "order": { + # "order_id": "36bb2437-7402-4794-bf26-4bdf03526439", + # "account_id": "a4c699f6-338d-4a26-941f-8f9853bfc4b9", + # "time_last_updated": "2019-09-27T15:05:35.096Z", + # "sequence": 48782, + # "price": "7349.2", + # "filled_amount": "100.0", + # "status": "FILLED_FULLY", + # "amount": "100.0", + # "instrument_code": "BTC_EUR", + # "side": "BUY", + # "time": "2019-09-27T15:05:32.063Z", + # "type": "MARKET" + # }, + # "trades": [ + # { + # "fee": { + # "fee_amount": "0.0014", + # "fee_currency": "BTC", + # "fee_percentage": "0.1", + # "fee_group_id": "default", + # "fee_type": "TAKER", + # "running_trading_volume": "0.0" + # }, + # "trade": { + # "trade_id": "fdff2bcc-37d6-4a2d-92a5-46e09c868664", + # "order_id": "36bb2437-7402-4794-bf26-4bdf03526439", + # "account_id": "a4c699f6-338d-4a26-941f-8f9853bfc4b9", + # "amount": "1.4", + # "side": "BUY", + # "instrument_code": "BTC_EUR", + # "price": "7341.4", + # "time": "2019-09-27T15:05:32.564Z", + # "sequence": 48670 + # } + # } + # ] + # } + # + return self.parse_order(response) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://docs.onetrading.com/#get-orders + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + request: dict = { + # 'from': self.iso8601(since), + # 'to': self.iso8601(self.milliseconds()), # max range is 100 days + # 'instrument_code': market['id'], + # 'with_cancelled_and_rejected': False, # default is False, orders which have been cancelled by the user before being filled or rejected by the system, additionally, all inactive filled orders which would return with "with_just_filled_inactive" + # 'with_just_filled_inactive': False, # orders which have been filled and are no longer open, use of "with_cancelled_and_rejected" extends "with_just_filled_inactive" and in case both are specified the latter is ignored + # 'with_just_orders': False, # do not return any trades corresponsing to the orders, it may be significanly faster and should be used if user is not interesting in trade information + # 'max_page_size': 100, + # 'cursor': 'string', # pointer specifying the position from which the next pages should be returned + } + market = None + if symbol is not None: + market = self.market(symbol) + request['instrument_code'] = market['id'] + if since is not None: + to = self.safe_string(params, 'to') + if to is None: + raise ArgumentsRequired(self.id + ' fetchOpenOrders() requires a "to" iso8601 string param with the since argument is specified, max range is 100 days') + request['from'] = self.iso8601(since) + if limit is not None: + request['max_page_size'] = limit + response = await self.privateGetAccountOrders(self.extend(request, params)) + # + # { + # "order_history": [ + # { + # "order": { + # "trigger_price": "12089.88", + # "order_id": "d453ca12-c650-46dd-9dee-66910d96bfc0", + # "account_id": "ef3a5f4c-cfcd-415e-ba89-5a9abf47b28a", + # "instrument_code": "BTC_USDT", + # "time": "2019-08-23T10:02:31.663Z", + # "side": "SELL", + # "price": "10159.76", + # "average_price": "10159.76", + # "amount": "0.2", + # "filled_amount": "0.2", + # "type": "STOP", + # "sequence": 8, + # "status": "FILLED_FULLY" + # }, + # "trades": [ + # { + # "fee": { + # "fee_amount": "0.4188869", + # "fee_currency": "USDT", + # "fee_percentage": "0.1", + # "fee_group_id": "default", + # "fee_type": "TAKER", + # "running_trading_volume": "0.0" + # }, + # "trade": { + # "trade_id": "ec82896f-fd1b-4cbb-89df-a9da85ccbb4b", + # "order_id": "d453ca12-c650-46dd-9dee-66910d96bfc0", + # "account_id": "ef3a5f4c-cfcd-415e-ba89-5a9abf47b28a", + # "amount": "0.2", + # "side": "SELL", + # "instrument_code": "BTC_USDT", + # "price": "10159.76", + # "time": "2019-08-23T10:02:32.663Z", + # "sequence": 9 + # } + # } + # ] + # }, + # { + # "order": { + # "order_id": "5151a99e-f414-418f-8cf1-2568d0a63ea5", + # "account_id": "ef3a5f4c-cfcd-415e-ba89-5a9abf47b28a", + # "instrument_code": "BTC_USDT", + # "time": "2019-08-23T10:01:36.773Z", + # "side": "SELL", + # "price": "12289.88", + # "amount": "0.5", + # "filled_amount": "0.0", + # "type": "LIMIT", + # "sequence": 7, + # "status": "OPEN" + # }, + # "trades": [] + # }, + # { + # "order": { + # "order_id": "ac80d857-75e1-4733-9070-fd4288395fdc", + # "account_id": "ef3a5f4c-cfcd-415e-ba89-5a9abf47b28a", + # "instrument_code": "BTC_USDT", + # "time": "2019-08-23T10:01:25.031Z", + # "side": "SELL", + # "price": "11089.88", + # "amount": "0.1", + # "filled_amount": "0.0", + # "type": "LIMIT", + # "sequence": 6, + # "status": "OPEN" + # }, + # "trades": [] + # } + # ], + # "max_page_size": 100 + # } + # + orderHistory = self.safe_list(response, 'order_history', []) + return self.parse_orders(orderHistory, market, since, limit) + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://docs.onetrading.com/#get-orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + request: dict = { + 'with_cancelled_and_rejected': True, # default is False, orders which have been cancelled by the user before being filled or rejected by the system, additionally, all inactive filled orders which would return with "with_just_filled_inactive" + } + return await self.fetch_open_orders(symbol, since, limit, self.extend(request, params)) + + async def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all the trades made from a single order + + https://docs.onetrading.com/#trades-for-order + + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + request: dict = { + 'order_id': id, + # 'max_page_size': 100, + # 'cursor': 'string', # pointer specifying the position from which the next pages should be returned + } + if limit is not None: + request['max_page_size'] = limit + response = await self.privateGetAccountOrdersOrderIdTrades(self.extend(request, params)) + # + # { + # "trade_history": [ + # { + # "trade": { + # "trade_id": "2b42efcd-d5b7-4a56-8e12-b69ffd68c5ef", + # "order_id": "66756a10-3e86-48f4-9678-b634c4b135b2", + # "account_id": "c2d0076a-c20d-41f8-9e9a-1a1d028b2b58", + # "amount": "1234.5678", + # "side": "BUY", + # "instrument_code": "BTC_EUR", + # "price": "1234.5678", + # "time": "2019-08-24T14:15:22Z", + # "price_tick_sequence": 0, + # "sequence": 123456789 + # }, + # "fee": { + # "fee_amount": "1234.5678", + # "fee_percentage": "1234.5678", + # "fee_group_id": "default", + # "running_trading_volume": "1234.5678", + # "fee_currency": "BTC", + # "fee_type": "TAKER" + # } + # } + # ], + # "max_page_size": 0, + # "cursor": "string" + # } + # + tradeHistory = self.safe_value(response, 'trade_history', []) + market = None + if symbol is not None: + market = self.market(symbol) + return self.parse_trades(tradeHistory, market, since, limit) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://docs.onetrading.com/#all-trades + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + request: dict = { + # 'from': self.iso8601(since), + # 'to': self.iso8601(self.milliseconds()), # max range is 100 days + # 'instrument_code': market['id'], + # 'max_page_size': 100, + # 'cursor': 'string', # pointer specifying the position from which the next pages should be returned + } + market = None + if symbol is not None: + market = self.market(symbol) + request['instrument_code'] = market['id'] + if since is not None: + to = self.safe_string(params, 'to') + if to is None: + raise ArgumentsRequired(self.id + ' fetchMyTrades() requires a "to" iso8601 string param with the since argument is specified, max range is 100 days') + request['from'] = self.iso8601(since) + if limit is not None: + request['max_page_size'] = limit + response = await self.privateGetAccountTrades(self.extend(request, params)) + # + # { + # "trade_history": [ + # { + # "trade": { + # "trade_id": "2b42efcd-d5b7-4a56-8e12-b69ffd68c5ef", + # "order_id": "66756a10-3e86-48f4-9678-b634c4b135b2", + # "account_id": "c2d0076a-c20d-41f8-9e9a-1a1d028b2b58", + # "amount": "1234.5678", + # "side": "BUY", + # "instrument_code": "BTC_EUR", + # "price": "1234.5678", + # "time": "2019-08-24T14:15:22Z", + # "price_tick_sequence": 0, + # "sequence": 123456789 + # }, + # "fee": { + # "fee_amount": "1234.5678", + # "fee_percentage": "1234.5678", + # "fee_group_id": "default", + # "running_trading_volume": "1234.5678", + # "fee_currency": "BTC", + # "fee_type": "TAKER" + # } + # } + # ], + # "max_page_size": 0, + # "cursor": "string" + # } + # + tradeHistory = self.safe_list(response, 'trade_history', []) + return self.parse_trades(tradeHistory, market, since, limit) + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = self.urls['api'][api] + '/' + self.version + '/' + self.implode_params(path, params) + query = self.omit(params, self.extract_params(path)) + if api == 'public': + if query: + url += '?' + self.urlencode(query) + elif api == 'private': + self.check_required_credentials() + headers = { + 'Accept': 'application/json', + 'Authorization': 'Bearer ' + self.apiKey, + } + if method == 'POST': + body = self.json(query) + headers['Content-Type'] = 'application/json' + else: + if query: + url += '?' + self.urlencode(query) + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None + # + # {"error":"MISSING_FROM_PARAM"} + # {"error":"MISSING_TO_PARAM"} + # {"error":"CANDLESTICKS_TIME_RANGE_TOO_BIG"} + # + message = self.safe_string(response, 'error') + if message is not None: + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + raise ExchangeError(feedback) # unknown message + return None diff --git a/ccxt/async_support/oxfun.py b/ccxt/async_support/oxfun.py new file mode 100644 index 0000000..1ba5b15 --- /dev/null +++ b/ccxt/async_support/oxfun.py @@ -0,0 +1,2861 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.oxfun import ImplicitAPI +import asyncio +import hashlib +from ccxt.base.types import Account, Any, Balances, Bool, Currencies, Currency, DepositAddress, Int, LeverageTier, LeverageTiers, Market, Num, Order, OrderBook, OrderRequest, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, FundingRates, Trade, Transaction, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import AccountNotEnabled +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import MarketClosed +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import NotSupported +from ccxt.base.errors import OperationFailed +from ccxt.base.errors import NetworkError +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import RequestTimeout +from ccxt.base.decimal_to_precision import TICK_SIZE + + +class oxfun(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(oxfun, self).describe(), { + 'id': 'oxfun', + 'name': 'OXFUN', + 'countries': ['PA'], # Panama todo check + 'version': 'v3', + 'rateLimit': 120, # 100 requests per second and 25000 per 5 minutes + 'pro': True, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': True, + 'future': False, + 'option': False, + 'addMargin': False, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'cancelOrders': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createDepositAddress': False, + 'createMarketBuyOrderWithCost': True, + 'createMarketOrderWithCost': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createOrders': True, + 'createPostOnlyOrder': True, + 'createReduceOnlyOrder': False, + 'createStopLimitOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'deposit': False, + 'editOrder': False, + 'fetchAccounts': True, + 'fetchBalance': True, + 'fetchBidsAsks': False, + 'fetchBorrowInterest': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchCanceledOrders': False, + 'fetchClosedOrder': False, + 'fetchClosedOrders': False, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDeposit': False, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchDepositWithdrawFee': False, + 'fetchDepositWithdrawFees': False, + 'fetchFundingHistory': True, + 'fetchFundingRate': True, + 'fetchFundingRateHistory': True, + 'fetchFundingRates': True, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchL3OrderBook': False, + 'fetchLedger': False, + 'fetchLeverage': False, + 'fetchLeverageTiers': True, + 'fetchMarketLeverageTiers': 'emulated', + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterestHistory': False, + 'fetchOpenOrder': False, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrderBooks': False, + 'fetchOrders': False, + 'fetchOrderTrades': False, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': True, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchStatus': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': False, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': False, + 'fetchTradingLimits': False, + 'fetchTransactionFee': False, + 'fetchTransactionFees': False, + 'fetchTransactions': False, + 'fetchTransfers': True, + 'fetchWithdrawal': False, + 'fetchWithdrawals': True, + 'fetchWithdrawalWhitelist': False, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'sandbox': True, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'signIn': False, + 'transfer': True, + 'withdraw': True, + 'ws': True, + }, + 'timeframes': { + '1m': '60s', + '5m': '300s', + '15m': '900s', + '30m': '1800s', + '1h': '3600s', + '2h': '7200s', + '4h': '14400s', + '1d': '86400s', + }, + 'urls': { + 'logo': 'https://github.com/ccxt/ccxt/assets/43336371/6a196124-c1ee-4fae-8573-962071b61a85', + 'referral': 'https://ox.fun/register?shareAccountId=5ZUD4a7G', + 'api': { + 'public': 'https://api.ox.fun', + 'private': 'https://api.ox.fun', + }, + 'test': { + 'public': 'https://stgapi.ox.fun', + 'private': 'https://stgapi.ox.fun', + }, + 'www': 'https://ox.fun/', + 'doc': 'https://docs.ox.fun/', + 'fees': 'https://support.ox.fun/en/articles/8819866-trading-fees', + }, + 'api': { + 'public': { + 'get': { + 'v3/markets': 1, + 'v3/assets': 1, + 'v3/tickers': 1, + 'v3/funding/estimates': 1, + 'v3/candles': 1, + 'v3/depth': 1, + 'v3/markets/operational': 1, + 'v3/exchange-trades': 1, + 'v3/funding/rates': 1, + 'v3/leverage/tiers': 1, + }, + }, + 'private': { + 'get': { + 'v3/account': 1, + 'v3/account/names': 1, + 'v3/wallet': 1, # retruns only FUNDING in OX + 'v3/transfer': 1, + 'v3/balances': 1, + 'v3/positions': 1, + 'v3/funding': 1, + 'v3/deposit-addresses': 1, + 'v3/deposit': 1, + 'v3/withdrawal-addresses': 1, + 'v3/withdrawal': 1, + 'v3/withdrawal-fees': 1, + 'v3/orders/status': 1, + 'v3/orders/working': 1, + 'v3/trades': 1, + }, + 'post': { + 'v3/transfer': 1, + 'v3/withdrawal': 1, + 'v3/orders/place': 1, + }, + 'delete': { + 'v3/orders/cancel': 1, + 'v3/orders/cancel-all': 1, + }, + }, + }, + 'fees': { + 'trading': { + 'tierBased': True, + 'percentage': True, + 'maker': self.parse_number('0.00020'), + 'taker': self.parse_number('0.00070'), + 'tiers': { + 'maker': [ + [self.parse_number('0'), self.parse_number('0.00020')], + [self.parse_number('2500000'), self.parse_number('0.00010')], + [self.parse_number('25000000'), self.parse_number('0')], + ], + 'taker': [ + [self.parse_number('0'), self.parse_number('0.00070')], + [self.parse_number('2500000'), self.parse_number('0.00050')], + [self.parse_number('25000000'), self.parse_number('0.00040')], + ], + }, + }, + }, + 'precisionMode': TICK_SIZE, + # exchange-specific options + 'options': { + 'sandboxMode': False, + 'networks': { + 'BTC': 'Bitcoin', + 'ERC20': 'Ethereum', + 'AVAX': 'Avalanche', + 'SOL': 'Solana', + 'ARB': 'Arbitrum', + 'MATIC': 'Polygon', + 'FTM': 'Fantom', + 'BNB': 'BNBSmartChain', + 'OPTIMISM': 'Optimism', + }, + 'networksById': { + 'Bitcoin': 'BTC', + 'Ethereum': 'ERC20', + 'Avalanche': 'AVAX', + 'Solana': 'SOL', + 'Arbitrum': 'ARB', + 'Polygon': 'MATIC', + 'Fantom': 'FTM', + 'Base': 'BASE', + 'BNBSmartChain': 'BNB', + 'Optimism': 'OPTIMISM', + }, + }, + 'features': { + 'default': { + 'sandbox': True, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerDirection': False, + 'triggerPriceType': None, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': { + 'EXPIRE_MAKER': True, + 'EXPIRE_TAKER': True, + 'EXPIRE_BOTH': True, + 'NONE': True, + }, + 'iceberg': True, # todo + }, + 'createOrders': { + 'max': 10, # todo + }, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 500, + 'daysBack': 100000, # todo + 'untilDays': 7, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': None, # todo? + 'fetchOHLCV': { + 'limit': 500, + }, + }, + 'spot': { + 'extends': 'default', + }, + 'swap': { + 'linear': { + 'extends': 'default', + }, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'exceptions': { + 'exact': { + '-0010': OperationFailed, # {"event":null,"success":false,"message":"Validation failed","code":"0010","data":null} - failed transfer + '-429': RateLimitExceeded, # Rate limit reached + '-05001': AuthenticationError, # Your operation authority is invalid + '-10001': ExchangeError, # General networking failure + '-20000': BadRequest, # Signature is invalid + '-20001': BadRequest, # "success":false,"code":"20001","message":"marketCode is invalid" + '-20002': BadRequest, # Unexpected error, please check if your request data complies with the specification. + '-20003': NotSupported, # Unrecognized operation + '-20005': AuthenticationError, # Already logged in + '-20006': BadRequest, # Quantity must be greater than zero + '-20007': AuthenticationError, # You are accessing server too rapidly + '-20008': BadRequest, # clientOrderId must be greater than zero if provided + '-20009': BadRequest, # JSON data format is invalid + '-20010': ArgumentsRequired, # Either clientOrderId or orderId is required + '-20011': ArgumentsRequired, # marketCode is required + '-20012': ArgumentsRequired, # side is required + '-20013': ArgumentsRequired, # orderType is required + '-20014': BadRequest, # clientOrderId is not long type + '-20015': BadSymbol, # marketCode is invalid + '-20016': BadRequest, # side is invalid + '-20017': BadRequest, # orderType is invalid + '-20018': BadRequest, # timeInForce is invalid + '-20019': BadRequest, # orderId is invalid + '-20020': BadRequest, # stopPrice or limitPrice is invalid + '-20021': BadRequest, # price is invalid + '-20022': ArgumentsRequired, # price is required for LIMIT order + '-20023': ArgumentsRequired, # timestamp is required + '-20024': ExchangeError, # timestamp exceeds the threshold + '-20025': AuthenticationError, # API key is invalid + '-20026': BadRequest, # Token is invalid or expired + '-20027': BadRequest, # The length of the message exceeds the maximum length + '-20028': BadRequest, # price or stopPrice or limitPrice must be greater than zero + '-20029': BadRequest, # stopPrice must be less than limitPrice for Buy Stop Order + '-20030': BadRequest, # limitPrice must be less than stopPrice for Sell Stop Order + '-20031': MarketClosed, # The marketCode is closed for trading temporarily + '-20032': NetworkError, # Failed to submit due to timeout in server side + '-20033': BadRequest, # triggerType is invalid + '-20034': BadRequest, # The size of tag must be less than 32 + '-20050': ExchangeError, # selfTradePreventionMode is invalid + '-30001': BadRequest, # {"success":false,"code":"30001","message":"Required parameter 'marketCode' is missing"} + '-35034': AuthenticationError, # {"success":false,"code":"35034","message":"Wallet API is not functioning properly, please try again or contact support."} + '-35046': AuthenticationError, # {"success":false,"code":"35046","message":"Error. Please refresh the page."} + '-40001': ExchangeError, # Alert from the server + '-50001': ExchangeError, # Unknown server error + '-300001': AccountNotEnabled, # Invalid account status xxx, please contact administration if any questions + '-300011': InvalidOrder, # Repo market orders are not allowed during the auction window + '-300012': InvalidOrder, # Repo bids above 0 and offers below 0 are not allowed during the auction window + '-100005': OrderNotFound, # Open order not found + '-100006': InvalidOrder, # Open order is not owned by the user + '-100008': BadRequest, # Quantity cannot be less than the quantity increment xxx + '-100015': NetworkError, # recvWindow xxx has expired + '-710001': ExchangeError, # System failure, exception thrown -> xxx + '-710002': BadRequest, # The price is lower than the minimum + '-710003': BadRequest, # The price is higher than the maximum + '-710004': BadRequest, # Position quantity exceeds the limit + '-710005': InsufficientFunds, # Insufficient margin + '-710006': InsufficientFunds, # Insufficient balance + '-710007': InsufficientFunds, # Insufficient position + '-000101': NetworkError, # Internal server is unavailable temporary, try again later + '-000201': NetworkError, # Trade service is busy, try again later + }, + 'broad': { + '-20001': OperationFailed, # Operation failed, please contact system administrator + '-200050': RequestTimeout, # The market is not active + }, + }, + }) + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for bitmex + + https://docs.ox.fun/?json#get-v3-markets + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + responseFromMarkets, responseFromTickers = await asyncio.gather(*[self.publicGetV3Markets(params), self.publicGetV3Tickers(params)]) + marketsFromMarkets = self.safe_list(responseFromMarkets, 'data', []) + # + # { + # success: True, + # data: [ + # { + # marketCode: 'OX-USD-SWAP-LIN', + # name: 'OX/USD Perp', + # referencePair: 'OX/USDT', + # base: 'OX', + # counter: 'USD', + # type: 'FUTURE', + # tickSize: '0.00001', + # minSize: '1', + # listedAt: '1704766320000', + # upperPriceBound: '0.02122', + # lowerPriceBound: '0.01142', + # markPrice: '0.01632', + # indexPrice: '0.01564', + # lastUpdatedAt: '1714762235569' + # }, + # { + # marketCode: 'BTC-USD-SWAP-LIN', + # name: 'BTC/USD Perp', + # referencePair: 'BTC/USDT', + # base: 'BTC', + # counter: 'USD', + # type: 'FUTURE', + # tickSize: '1', + # minSize: '0.0001', + # listedAt: '1704686640000', + # upperPriceBound: '67983', + # lowerPriceBound: '55621', + # markPrice: '61802', + # indexPrice: '61813', + # lastUpdatedAt: '1714762234765' + # }, + # { + # "marketCode": "MILK-OX", + # "name": "MILK/OX", + # "referencePair": "MILK/OX", + # "base": "MILK", + # "counter": "OX", + # "type": "SPOT", + # "tickSize": "0.0001", + # "minSize": "1", + # "listedAt": "1706608500000", + # "upperPriceBound": "1.0000", + # "lowerPriceBound": "-1.0000", + # "markPrice": "0.0269", + # "indexPrice": "0.0269", + # "lastUpdatedAt": "1714757402185" + # }, + # ... + # ] + # } + # + marketsFromTickers = self.safe_list(responseFromTickers, 'data', []) + # + # { + # "success": True, + # "data": [ + # { + # "marketCode": "DYM-USD-SWAP-LIN", + # "markPrice": "3.321", + # "open24h": "3.315", + # "high24h": "3.356", + # "low24h": "3.255", + # "volume24h": "0", + # "currencyVolume24h": "0", + # "openInterest": "1768.1", + # "lastTradedPrice": "3.543", + # "lastTradedQuantity": "1.0", + # "lastUpdatedAt": "1714853388102" + # }, + # ... + # ] + # } + # + markets = self.array_concat(marketsFromMarkets, marketsFromTickers) + return self.parse_markets(markets) + + def parse_markets(self, markets) -> List[Market]: + marketIds = [] + result = [] + for i in range(0, len(markets)): + market = markets[i] + marketId = self.safe_string(market, 'marketCode') + if not (self.in_array(marketId, marketIds)): + marketIds.append(marketId) + result.append(self.parse_market(market)) + return result + + def parse_market(self, market) -> Market: + id = self.safe_string(market, 'marketCode', '') + parts = id.split('-') + baseId = self.safe_string(parts, 0) + quoteId = self.safe_string(parts, 1) + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + symbol = base + '/' + quote + type = self.safe_string_lower(market, 'type', 'spot') # markets from v3/tickers are spot and have no type + settleId: Str = None + settle: Str = None + isFuture = (type == 'future') # the exchange has only perpetual futures + if isFuture: + type = 'swap' + settleId = 'OX' + settle = self.safe_currency_code('OX') + symbol = symbol + ':' + settle + isSpot = type == 'spot' + return self.safe_market_structure({ + 'id': id, + 'numericId': None, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': type, + 'spot': isSpot, + 'margin': False, + 'swap': isFuture, + 'future': False, + 'option': False, + 'active': True, + 'contract': isFuture, + 'linear': True if isFuture else None, + 'inverse': False if isFuture else None, + 'taker': self.fees['trading']['taker'], + 'maker': self.fees['trading']['maker'], + 'contractSize': 1 if isFuture else None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': None, # todo find it out + 'price': self.safe_number(market, 'tickSize'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(market, 'minSize'), + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': self.safe_integer(market, 'listedAt'), + 'index': None, + 'info': market, + }) + + async def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://docs.ox.fun/?json#get-v3-assets + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = await self.publicGetV3Assets(params) + # + # { + # "success": True, + # "data": [ + # { + # "asset": "OX", + # "isCollateral": True, + # "loanToValue": "1.000000000", + # "loanToValueFactor": "0.000000000", + # "networkList": [ + # { + # "network": "BNBSmartChain", + # "tokenId": "0x78a0A62Fba6Fb21A83FE8a3433d44C73a4017A6f", + # "transactionPrecision": "18", + # "isWithdrawalFeeChargedToUser": True, + # "canDeposit": True, + # "canWithdraw": False, + # "minDeposit": "0.00010", + # "minWithdrawal": "0.00010" + # }, + # { + # "network": "Polygon", + # "tokenId": "0x78a0A62Fba6Fb21A83FE8a3433d44C73a4017A6f", + # "transactionPrecision": "18", + # "isWithdrawalFeeChargedToUser": True, + # "canDeposit": True, + # "canWithdraw": False, + # "minDeposit": "0.00010", + # "minWithdrawal": "0.00010" + # }, + # ... + # ] + # }, + # { + # "asset": "BTC", + # "isCollateral": True, + # "loanToValue": "0.950000000", + # "loanToValueFactor": "0.000000000", + # "networkList": [ + # { + # "network": "Bitcoin", + # "transactionPrecision": "8", + # "isWithdrawalFeeChargedToUser": True, + # "canDeposit": True, + # "canWithdraw": True, + # "minDeposit": "0.00010", + # "minWithdrawal": "0.00010" + # } + # ] + # }, + # { + # "asset": "USDT.ARB", + # "isCollateral": True, + # "loanToValue": "0.950000000", + # "loanToValueFactor": "0.000000000", + # "networkList": [ + # { + # "network": "Arbitrum", + # "tokenId": "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", + # "transactionPrecision": "18", + # "isWithdrawalFeeChargedToUser": True, + # "canDeposit": True, + # "canWithdraw": True, + # "minDeposit": "0.00010", + # "minWithdrawal": "0.00010" + # } + # ] + # }, + # ... + # ] + # } + # + data = self.safe_list(response, 'data', []) + result: dict = {} + for i in range(0, len(data)): + currency = data[i] + fullId = self.safe_string(currency, 'asset', '') + parts = fullId.split('.') + id = parts[0] + code = self.safe_currency_code(id) + if not (code in result): + result[code] = { + 'id': id, + 'code': code, + 'precision': None, + 'type': None, + 'name': None, + 'active': None, + 'deposit': None, + 'withdraw': None, + 'fee': None, + 'limits': { + 'withdraw': { + 'min': None, + 'max': None, + }, + 'deposit': { + 'min': None, + 'max': None, + }, + }, + 'networks': {}, + 'info': [], + } + chains = self.safe_list(currency, 'networkList', []) + for j in range(0, len(chains)): + chain = chains[j] + networkId = self.safe_string(chain, 'network') + networkCode = self.network_id_to_code(networkId) + result[code]['networks'][networkCode] = { + 'id': networkId, + 'network': networkCode, + 'margin': None, + 'deposit': self.safe_bool(chain, 'canDeposit'), + 'withdraw': self.safe_bool(chain, 'canWithdraw'), + 'active': None, + 'fee': None, + 'precision': self.parse_number(self.parse_precision(self.safe_string(chain, 'transactionPrecision'))), + 'limits': { + 'deposit': { + 'min': self.safe_number(chain, 'minDeposit'), + 'max': None, + }, + 'withdraw': { + 'min': self.safe_number(chain, 'minWithdrawal'), + 'max': None, + }, + }, + 'info': chain, + } + infos = self.safe_list(result[code], 'info', []) + infos.append(currency) + result[code]['info'] = infos + # only after all entries are formed in currencies, restructure each entry + allKeys = list(result.keys()) + for i in range(0, len(allKeys)): + code = allKeys[i] + result[code] = self.safe_currency_structure(result[code]) # self is needed after adding network entry + return result + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://docs.ox.fun/?json#get-v3-tickers + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + response = await self.publicGetV3Tickers(params) + # + # { + # "success": True, + # "data": [ + # { + # "marketCode": "NII-USDT", + # "markPrice": "0", + # "open24h": "0", + # "high24h": "0", + # "low24h": "0", + # "volume24h": "0", + # "currencyVolume24h": "0", + # "openInterest": "0", + # "lastTradedPrice": "0", + # "lastTradedQuantity": "0", + # "lastUpdatedAt": "1714853388621" + # }, + # { + # "marketCode": "GEC-USDT", + # "markPrice": "0", + # "open24h": "0", + # "high24h": "0", + # "low24h": "0", + # "volume24h": "0", + # "currencyVolume24h": "0", + # "openInterest": "0", + # "lastTradedPrice": "0", + # "lastTradedQuantity": "0", + # "lastUpdatedAt": "1714853388621" + # }, + # { + # "marketCode": "DYM-USD-SWAP-LIN", + # "markPrice": "3.321", + # "open24h": "3.315", + # "high24h": "3.356", + # "low24h": "3.255", + # "volume24h": "0", + # "currencyVolume24h": "0", + # "openInterest": "1768.1", + # "lastTradedPrice": "3.543", + # "lastTradedQuantity": "1.0", + # "lastUpdatedAt": "1714853388102" + # }, + # ... + # ] + # } + # + tickers = self.safe_list(response, 'data', []) + return self.parse_tickers(tickers, symbols) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://docs.ox.fun/?json#get-v3-tickers + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'marketCode': market['id'], + } + response = await self.publicGetV3Tickers(self.extend(request, params)) + # + # { + # "success": True, + # "data": [ + # { + # "marketCode": "BTC-USD-SWAP-LIN", + # "markPrice": "64276", + # "open24h": "63674", + # "high24h": "64607", + # "low24h": "62933", + # "volume24h": "306317655.80000", + # "currencyVolume24h": "48.06810", + # "openInterest": "72.39250", + # "lastTradedPrice": "64300.0", + # "lastTradedQuantity": "1.0", + # "lastUpdatedAt": "1714925196034" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + ticker = self.safe_dict(data, 0, {}) + return self.parse_ticker(ticker, market) + + def parse_ticker(self, ticker, market: Market = None) -> Ticker: + # + # { + # "marketCode": "BTC-USD-SWAP-LIN", + # "markPrice": "64276", + # "open24h": "63674", + # "high24h": "64607", + # "low24h": "62933", + # "volume24h": "306317655.80000", + # "currencyVolume24h": "48.06810", + # "openInterest": "72.39250", + # "lastTradedPrice": "64300.0", + # "lastTradedQuantity": "1.0", + # "lastUpdatedAt": "1714925196034" + # } + # + timestamp = self.safe_integer(ticker, 'lastUpdatedAt') + marketId = self.safe_string(ticker, 'marketCode') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + last = self.safe_string(ticker, 'lastTradedPrice') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high24h'), + 'low': self.safe_string(ticker, 'low24h'), + 'bid': None, + 'bidVolume': None, + 'ask': None, + 'askVolume': None, + 'vwap': None, + 'open': self.safe_string(ticker, 'open24h'), + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': self.safe_string(ticker, 'currencyVolume24h'), + 'quoteVolume': None, # the exchange returns cost in OX + 'markPrice': self.safe_string(ticker, 'markPrice'), + 'info': ticker, + }, market) + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://docs.ox.fun/?json#get-v3-candles + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch(default 24 hours ago) + :param int [limit]: the maximum amount of candles to fetch(default 200, max 500) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest candle to fetch(default now) + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + timeframe = self.safe_string(self.timeframes, timeframe, timeframe) + request: dict = { + 'marketCode': market['id'], + 'timeframe': timeframe, + } + if since is not None: + request['startTime'] = since # startTime and endTime must be within 7 days of each other + if limit is not None: + request['limit'] = limit + until = self.safe_integer(params, 'until') + if until is not None: + request['endTime'] = until + params = self.omit(params, 'until') + elif since is not None: + request['endTime'] = self.sum(since, 7 * 24 * 60 * 60 * 1000) # for the exchange not to raise an exception if since is younger than 7 days + response = await self.publicGetV3Candles(self.extend(request, params)) + # + # { + # "success": True, + # "timeframe": "3600s", + # "data": [ + # { + # "open": "0.03240000", + # "high": "0.03240000", + # "low": "0.03240000", + # "close": "0.03240000", + # "volume": "0", + # "currencyVolume": "0", + # "openedAt": "1714906800000" + # }, + # { + # "open": "0.03240000", + # "high": "0.03240000", + # "low": "0.03240000", + # "close": "0.03240000", + # "volume": "0", + # "currencyVolume": "0", + # "openedAt": "1714903200000" + # }, + # ... + # ] + # } + # + result = self.safe_list(response, 'data', []) + return self.parse_ohlcvs(result, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # { + # "open": "0.03240000", + # "high": "0.03240000", + # "low": "0.03240000", + # "close": "0.03240000", + # "volume": "0", + # "currencyVolume": "0", + # "openedAt": "1714906800000" + # } + # + return [ + self.safe_integer(ohlcv, 'openedAt'), + self.safe_number(ohlcv, 'open'), + self.safe_number(ohlcv, 'high'), + self.safe_number(ohlcv, 'low'), + self.safe_number(ohlcv, 'close'), + self.safe_number(ohlcv, 'currencyVolume'), + ] + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://docs.ox.fun/?json#get-v3-depth + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return(default 5, max 100) + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'marketCode': market['id'], + } + if limit is not None: + request['level'] = limit + response = await self.publicGetV3Depth(self.extend(request, params)) + # + # { + # "success": True, + # "level": "5", + # "data": { + # "marketCode": "BTC-USD-SWAP-LIN", + # "lastUpdatedAt": "1714933499266", + # "asks": [ + # [64073.0, 8.4622], + # [64092.0, 8.1912], + # [64111.0, 8.0669], + # [64130.0, 11.7195], + # [64151.0, 10.1798] + # ], + # "bids": [ + # [64022.0, 10.1292], + # [64003.0, 8.1619], + # [64000.0, 1.0], + # [63984.0, 12.7724], + # [63963.0, 11.0073] + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + timestamp = self.safe_integer(data, 'lastUpdatedAt') + return self.parse_order_book(data, market['symbol'], timestamp) + + async def fetch_funding_rates(self, symbols: Strings = None, params={}) -> FundingRates: + """ + fetch the current funding rates for multiple markets + + https://docs.ox.fun/?json#get-v3-funding-estimates + + :param str[] symbols: unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: an array of `funding rate structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + response = await self.publicGetV3FundingEstimates(params) + # + # { + # "success": True, + # "data": [ + # { + # "marketCode": "OX-USD-SWAP-LIN", + # "fundingAt": "1715515200000", + # "estFundingRate": "0.000200000" + # }, + # { + # "marketCode": "BTC-USD-SWAP-LIN", + # "fundingAt": "1715515200000", + # "estFundingRate": "0.000003" + # }, + # ... + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_funding_rates(data, symbols) + + async def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rates for a symbol + + https://docs.ox.fun/?json#get-v3-funding-estimates + + :param str symbol: unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: an array of `funding rate structures ` + """ + await self.load_markets() + request: dict = { + 'marketCode': self.market_id(symbol), + } + response = await self.publicGetV3FundingEstimates(self.extend(request, params)) + # + data = self.safe_list(response, 'data', []) + first = self.safe_dict(data, 0, {}) + return self.parse_funding_rate(first, self.market(symbol)) + + def parse_funding_rate(self, fundingRate, market: Market = None) -> FundingRate: + # + # { + # "marketCode": "OX-USD-SWAP-LIN", + # "fundingAt": "1715515200000", + # "estFundingRate": "0.000200000" + # } + # + symbol = self.safe_string(fundingRate, 'marketCode') + market = self.market(symbol) + estFundingRateTimestamp = self.safe_integer(fundingRate, 'fundingAt') + return { + 'info': fundingRate, + 'symbol': market['symbol'], + 'markPrice': None, + 'indexPrice': None, + 'interestRate': self.parse_number('0'), + 'estimatedSettlePrice': None, + 'timestamp': estFundingRateTimestamp, + 'datetime': self.iso8601(estFundingRateTimestamp), + 'fundingRate': self.safe_number(fundingRate, 'estFundingRate'), + 'fundingTimestamp': None, + 'fundingDatetime': None, + 'nextFundingRate': None, + 'nextFundingTimestamp': None, + 'nextFundingDatetime': None, + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': None, + } + + async def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + Fetches the history of funding rates + + https://docs.ox.fun/?json#get-v3-funding-rates + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch(default 24 hours ago) + :param int [limit]: the maximum amount of trades to fetch(default 200, max 500) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest trade to fetch(default now) + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'marketCode': market['id'], + } + if since is not None: + request['startTime'] = since # startTime and endTime must be within 7 days of each other + if limit is not None: + request['limit'] = limit + until = self.safe_integer(params, 'until') + if until is not None: + request['endTime'] = until + params = self.omit(params, 'until') + response = await self.publicGetV3FundingRates(self.extend(request, params)) + # + # { + # success: True, + # data: [ + # { + # marketCode: 'NEAR-USD-SWAP-LIN', + # fundingRate: '-0.000010000', + # createdAt: '1715428870755' + # }, + # { + # marketCode: 'ENA-USD-SWAP-LIN', + # fundingRate: '0.000150000', + # createdAt: '1715428868616' + # }, + # ... + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_funding_rate_histories(data, market, since, limit) + + def parse_funding_rate_history(self, info, market: Market = None): + # + # { + # success: True, + # data: [ + # { + # marketCode: 'NEAR-USD-SWAP-LIN', + # fundingRate: '-0.000010000', + # createdAt: '1715428870755' + # }, + # { + # marketCode: 'ENA-USD-SWAP-LIN', + # fundingRate: '0.000150000', + # createdAt: '1715428868616' + # }, + # ... + # } + # + marketId = self.safe_string(info, 'marketCode') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + timestamp = self.safe_integer(info, 'createdAt') + return { + 'info': info, + 'symbol': symbol, + 'fundingRate': self.safe_number(info, 'fundingRate'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + + async def fetch_funding_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches the history of funding payments + + https://docs.ox.fun/?json#get-v3-funding + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch(default 24 hours ago) + :param int [limit]: the maximum amount of trades to fetch(default 200, max 500) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest trade to fetch(default now) + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'marketCode': market['id'], + } + if since is not None: + request['startTime'] = since # startTime and endTime must be within 7 days of each other + if limit is not None: + request['limit'] = limit + until = self.safe_integer(params, 'until') + if until is not None: + request['endTime'] = until + params = self.omit(params, 'until') + response = await self.privateGetV3Funding(self.extend(request, params)) + # + # { + # success: True, + # data: [ + # { + # id: '966709913041305605', + # marketCode: 'ETH-USD-SWAP-LIN', + # payment: '-0.00430822', + # fundingRate: '0.000014', + # position: '0.001', + # indexPrice: '3077.3', + # createdAt: '1715086852890' + # }, + # { + # id: '966698111997509637', + # marketCode: 'ETH-USD-SWAP-LIN', + # payment: '-0.0067419', + # fundingRate: '0.000022', + # position: '0.001', + # indexPrice: '3064.5', + # createdAt: '1715083251516' + # }, + # ... + # ] + # } + # + result = self.safe_list(response, 'data', []) + return self.parse_incomes(result, market, since, limit) + + def parse_income(self, income, market: Market = None): + # + # { + # id: '966709913041305605', + # marketCode: 'ETH-USD-SWAP-LIN', + # payment: '-0.00430822', + # fundingRate: '0.000014', + # position: '0.001', + # indexPrice: '3077.3', + # createdAt: '1715086852890' + # }, + # + marketId = self.safe_string(income, 'marketCode') + symbol = self.safe_symbol(marketId, market) + amount = self.safe_number(income, 'payment') + code = self.safe_currency_code('OX') + id = self.safe_string(income, 'id') + timestamp = self.safe_timestamp(income, 'createdAt') + rate = self.safe_number(income, 'fundingRate') + return { + 'info': income, + 'symbol': symbol, + 'code': code, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': id, + 'amount': amount, + 'rate': rate, + } + + async def fetch_leverage_tiers(self, symbols: Strings = None, params={}) -> LeverageTiers: + """ + retrieve information on the maximum leverage, and maintenance margin for trades of varying trade sizes, if a market has a leverage tier of 0, then the leverage tiers cannot be obtained for self market + + https://docs.ox.fun/?json#get-v3-leverage-tiers + + :param str[] [symbols]: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `leverage tiers structures `, indexed by market symbols + """ + await self.load_markets() + response = await self.publicGetV3LeverageTiers(params) + # + # { + # success: True, + # data: [ + # { + # marketCode: 'SOL-USD-SWAP-LIN', + # tiers: [ + # { + # tier: '1', + # leverage: '10', + # positionFloor: '0', + # positionCap: '200000000', + # initialMargin: '0.1', + # maintenanceMargin: '0.05', + # maintenanceAmount: '0' + # }, + # { + # tier: '2', + # leverage: '5', + # positionFloor: '200000000', + # positionCap: '280000000', + # initialMargin: '0.2', + # maintenanceMargin: '0.1', + # maintenanceAmount: '7000000' + # }, + # { + # tier: '3', + # leverage: '4', + # positionFloor: '280000000', + # positionCap: '460000000', + # initialMargin: '0.25', + # maintenanceMargin: '0.125', + # maintenanceAmount: '14000000' + # }, + # ... + # ] + # }, + # ... + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_leverage_tiers(data, symbols, 'marketCode') + + def parse_market_leverage_tiers(self, info, market: Market = None) -> List[LeverageTier]: + # + # { + # marketCode: 'SOL-USD-SWAP-LIN', + # tiers: [ + # { + # tier: '1', + # leverage: '10', + # positionFloor: '0', + # positionCap: '200000000', + # initialMargin: '0.1', + # maintenanceMargin: '0.05', + # maintenanceAmount: '0' + # ... + # ] + # }, + # + marketId = self.safe_string(info, 'marketCode') + market = self.safe_market(marketId, market) + listOfTiers = self.safe_list(info, 'tiers', []) + tiers = [] + for j in range(0, len(listOfTiers)): + tier = listOfTiers[j] + tiers.append({ + 'tier': self.safe_number(tier, 'tier'), + 'symbol': self.safe_symbol(marketId, market), + 'currency': market['settle'], + 'minNotional': self.safe_number(tier, 'positionFloor'), + 'maxNotional': self.safe_number(tier, 'positionCap'), + 'maintenanceMarginRate': self.safe_number(tier, 'maintenanceMargin'), + 'maxLeverage': self.safe_number(tier, 'leverage'), + 'info': tier, + }) + return tiers + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://docs.ox.fun/?json#get-v3-exchange-trades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch(default 24 hours ago) + :param int [limit]: the maximum amount of trades to fetch(default 200, max 500) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest trade to fetch(default now) + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'marketCode': market['id'], + } + if since is not None: + request['startTime'] = since # startTime and endTime must be within 7 days of each other + if limit is not None: + request['limit'] = limit + until = self.safe_integer(params, 'until') + if until is not None: + request['endTime'] = until + params = self.omit(params, 'until') + elif since is not None: + request['endTime'] = self.sum(since, 7 * 24 * 60 * 60 * 1000) # for the exchange not to raise an exception if since is younger than 7 days + response = await self.publicGetV3ExchangeTrades(self.extend(request, params)) + # + # { + # "success": True, + # "data": [ + # { + # "marketCode": "BTC-USD-SWAP-LIN", + # "matchPrice": "63900", + # "matchQuantity": "1", + # "side": "SELL", + # "matchType": "TAKER", + # "matchedAt": "1714934112352" + # }, + # ... + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_trades(data, market, since, limit) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://docs.ox.fun/?json#get-v3-trades + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum amount of trades to fetch(default 200, max 500) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest trade to fetch(default now) + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + request: dict = {} + market: Market = None + if symbol is not None: + market = self.market(symbol) + request['marketCode'] = market['id'] + if since is not None: # startTime and endTime must be within 7 days of each other + request['startTime'] = since + if limit is not None: + request['limit'] = limit + until = self.safe_integer(params, 'until') + if until is not None: + request['endTime'] = until + params = self.omit(params, 'until') + elif since is not None: + request['endTime'] = self.sum(since, 7 * 24 * 60 * 60 * 1000) # for the exchange not to raise an exception if since is younger than 7 days + response = await self.privateGetV3Trades(self.extend(request, params)) + # + # { + # "success": True, + # "data": [ + # { + # "orderId": "1000104903698", + # "clientOrderId": "1715000260094", + # "matchId": "400017129522773178", + # "marketCode": "ETH-USD-SWAP-LIN", + # "side": "BUY", + # "matchedQuantity": "0.001", + # "matchPrice": "3100.2", + # "total": "310.02", + # "orderMatchType": "MAKER", + # "feeAsset": "OX", + # "fee": "0.062004", + # "source": "0", + # "matchedAt": "1715000267420" + # } + # ] + # } + # + result = self.safe_list(response, 'data', []) + return self.parse_trades(result, market, since, limit) + + def parse_trade(self, trade, market: Market = None) -> Trade: + # + # public fetchTrades + # + # { + # "marketCode": "BTC-USD-SWAP-LIN", + # "matchPrice": "63900", + # "matchQuantity": "1", + # "side": "SELL", + # "matchType": "TAKER", + # "matchedAt": "1714934112352" + # } + # + # + # private fetchMyTrades + # + # { + # "orderId": "1000104903698", + # "clientOrderId": "1715000260094", + # "matchId": "400017129522773178", + # "marketCode": "ETH-USD-SWAP-LIN", + # "side": "BUY", + # "matchedQuantity": "0.001", + # "matchPrice": "3100.2", + # "total": "310.02", + # "orderMatchType": "MAKER", + # "feeAsset": "OX", + # "fee": "0.062004", + # "source": "0", + # "matchedAt": "1715000267420" + # } + # + marketId = self.safe_string(trade, 'marketCode') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + timestamp = self.safe_integer(trade, 'matchedAt') + fee = { + 'cost': self.safe_string(trade, 'fee'), + 'currency': self.safe_currency_code(self.safe_string(trade, 'feeAsset')), + } + return self.safe_trade({ + 'id': self.safe_string(trade, 'matchId'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'type': None, + 'order': self.safe_string(trade, 'orderId'), + 'side': self.safe_string_lower(trade, 'side'), + 'takerOrMaker': self.safe_string_lower_2(trade, 'matchType', 'orderMatchType'), + 'price': self.safe_string(trade, 'matchPrice'), + 'amount': self.safe_string_2(trade, 'matchQuantity', 'matchedQuantity'), + 'cost': None, # the exchange returns total cost in OX + 'fee': fee, + 'info': trade, + }, market) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://docs.ox.fun/?json#get-v3-balances + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.asset]: currency id, if empty the exchange returns info about all currencies + :param str [params.subAcc]: Name of sub account. If no subAcc is given, then the response contains only the account linked to the API-Key. + :returns dict: a `balance structure ` + """ + await self.load_markets() + response = await self.privateGetV3Balances(params) + # + # { + # "success": True, + # "data": [ + # { + # "accountId": "106490", + # "name": "main", + # "balances": [ + # { + # "asset": "OX", + # "total": "-7.55145065000", + # "available": "-71.16445065000", + # "reserved": "0", + # "lastUpdatedAt": "1715000448946" + # }, + # { + # "asset": "ETH", + # "total": "0.01", + # "available": "0.01", + # "reserved": "0", + # "lastUpdatedAt": "1714914512750" + # }, + # ... + # ] + # }, + # ... + # ] + # } + # + data = self.safe_list(response, 'data', []) + balance = data[0] + subAcc = self.safe_string(params, 'subAcc') + if subAcc is not None: + for i in range(0, len(data)): + b = data[i] + name = self.safe_string(b, 'name') + if name == subAcc: + balance = b + break + return self.parse_balance(balance) + + def parse_balance(self, balance) -> Balances: + # + # { + # "accountId": "106490", + # "name": "main", + # "balances": [ + # { + # "asset": "OX", + # "total": "-7.55145065000", + # "available": "-71.16445065000", + # "reserved": "0", + # "lastUpdatedAt": "1715000448946" + # }, + # { + # "asset": "ETH", + # "total": "0.01", + # "available": "0.01", + # "reserved": "0", + # "lastUpdatedAt": "1714914512750" + # }, + # ... + # ] + # } + # + result: dict = { + 'info': balance, + } + balances = self.safe_list(balance, 'balances', []) + for i in range(0, len(balances)): + balanceEntry = balances[i] + currencyId = self.safe_string(balanceEntry, 'asset') + code = self.safe_currency_code(currencyId) + account = self.account() + account['total'] = self.safe_string(balanceEntry, 'total') + account['free'] = self.safe_string(balanceEntry, 'available') + account['used'] = self.safe_string(balanceEntry, 'reserved') + result[code] = account + return self.safe_balance(result) + + async def fetch_accounts(self, params={}) -> List[Account]: + """ + fetch subaccounts associated with a profile + + https://docs.ox.fun/?json#get-v3-account-names + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `account structures ` indexed by the account type + """ + await self.load_markets() + # self endpoint can only be called using API keys paired with the parent account! Returns all active subaccounts. + response = await self.privateGetV3AccountNames(params) + # + # { + # "success": True, + # "data": [ + # { + # "accountId": "106526", + # "name": "testSubAccount" + # }, + # ... + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_accounts(data, params) + + def parse_account(self, account): + # + # { + # "accountId": "106526", + # "name": "testSubAccount" + # }, + # + return { + 'id': self.safe_string(account, 'accountId'), + 'type': None, + 'code': None, + 'info': account, + } + + async def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://docs.ox.fun/?json#post-v3-transfer + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account id to transfer from + :param str toAccount: account id to transfer to + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transfer structure ` + """ + # transferring funds between sub-accounts is restricted to API keys linked to the parent account. + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'asset': currency['id'], + 'quantity': self.currency_to_precision(code, amount), + 'fromAccount': fromAccount, + 'toAccount': toAccount, + } + response = await self.privatePostV3Transfer(self.extend(request, params)) + # + # { + # timestamp: 1715430036267, + # datetime: '2024-05-11T12:20:36.267Z', + # currency: 'OX', + # amount: 10, + # fromAccount: '106464', + # toAccount: '106570', + # info: { + # asset: 'OX', + # quantity: '10', + # fromAccount: '106464', + # toAccount: '106570', + # transferredAt: '1715430036267' + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_transfer(data, currency) + + async def fetch_transfers(self, code: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch a history of internal transfers made on an account + + https://docs.ox.fun/?json#get-v3-transfer + + :param str code: unified currency code of the currency transferred + :param int [since]: the earliest time in ms to fetch transfers for(default 24 hours ago) + :param int [limit]: the maximum number of transfer structures to retrieve(default 50, max 200) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch transfers for(default time now) + :returns dict[]: a list of `transfer structures ` + """ + # API keys linked to the parent account can get all account transfers, while API keys linked to a sub-account can only see transfers where the sub-account is either the "fromAccount" or "toAccount" + await self.load_markets() + request: dict = {} + currency: Currency = None + if code is not None: + currency = self.currency(code) + request['asset'] = currency['id'] + if since is not None: + request['startTime'] = since # startTime and endTime must be within 7 days of each other + if limit is not None: + request['limit'] = limit + until = self.safe_integer(params, 'until') + if until is not None: + request['endTime'] = until + params = self.omit(params, 'until') + elif since is not None: + request['endTime'] = self.sum(since, 7 * 24 * 60 * 60 * 1000) # for the exchange not to raise an exception if since is younger than 7 days + response = await self.privateGetV3Transfer(self.extend(request, params)) + # + # { + # "success": True, + # "data": [ + # { + # "asset": "USDT", + # "quantity": "5", + # "fromAccount": "106490", + # "toAccount": "106526", + # "id": "966706320886267905", + # "status": "COMPLETED", + # "transferredAt": "1715085756708" + # }, + # ... + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_transfers(data, currency, since, limit) + + def parse_transfer(self, transfer, currency: Currency = None): + # + # fetchTransfers + # + # { + # "asset": "USDT", + # "quantity": "5", + # "fromAccount": "106490", + # "toAccount": "106526", + # "id": "966706320886267905", + # "status": "COMPLETED", + # "transferredAt": "1715085756708" + # } + # + timestamp = self.safe_integer(transfer, 'transferredAt') + currencyId = self.safe_string(transfer, 'asset') + return { + 'id': self.safe_string(transfer, 'id'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'currency': self.safe_currency_code(currencyId, currency), + 'amount': self.safe_number(transfer, 'quantity'), + 'fromAccount': self.safe_string(transfer, 'fromAccount'), + 'toAccount': self.safe_string(transfer, 'toAccount'), + 'status': self.parse_transfer_status(self.safe_string(transfer, 'status')), + 'info': transfer, + } + + def parse_transfer_status(self, status): + statuses: dict = { + 'COMPLETED': 'ok', + } + return self.safe_string(statuses, status, status) + + async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://docs.ox.fun/?json#get-v3-deposit-addresses + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.network]: network for fetch deposit address + :returns dict: an `address structure ` + """ + networkCode = self.safe_string(params, 'network') + networkId = self.network_code_to_id(networkCode, code) + if networkId is None: + raise BadRequest(self.id + ' fetchDepositAddress() require network parameter') + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'asset': currency['id'], + 'network': networkId, + } + params = self.omit(params, 'network') + response = await self.privateGetV3DepositAddresses(self.extend(request, params)) + # + # {"success":true,"data":{"address":"0x998dEc76151FB723963Bd8AFD517687b38D33dE8"}} + # + data = self.safe_dict(response, 'data', {}) + return self.parse_deposit_address(data, currency) + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + # + # {"address":"0x998dEc76151FB723963Bd8AFD517687b38D33dE8"} + # + address = self.safe_string(depositAddress, 'address') + self.check_address(address) + return { + 'info': depositAddress, + 'currency': currency['code'], + 'network': None, + 'address': address, + 'tag': None, + } + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://docs.ox.fun/?json#get-v3-deposit + + :param str code: unified currency code of the currency transferred + :param int [since]: the earliest time in ms to fetch transfers for(default 24 hours ago) + :param int [limit]: the maximum number of transfer structures to retrieve(default 50, max 200) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch transfers for(default time now) + :returns dict[]: a list of `transfer structures ` + """ + await self.load_markets() + request: dict = {} + currency: Currency = None + if code is not None: + currency = self.currency(code) + request['asset'] = currency['id'] + if since is not None: + request['startTime'] = since # startTime and endTime must be within 7 days of each other + if limit is not None: + request['limit'] = limit + until = self.safe_integer(params, 'until') + if until is not None: + request['endTime'] = until + params = self.omit(params, 'until') + response = await self.privateGetV3Deposit(self.extend(request, params)) + # + # { + # "success": True, + # "data": [ + # { + # "asset":"USDC", + # "network":"Ethereum", + # "address": "0x998dEc76151FB723963Bd8AFD517687b38D33dE8", + # "quantity":"50", + # "id":"5914", + # "status": "COMPLETED", + # "txId":"0xf5e79663830a0c6f94d46638dcfbc134566c12facf1832396f81ecb55d3c75dc", + # "creditedAt":"1714821645154" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + for i in range(0, len(data)): + data[i]['type'] = 'deposit' + return self.parse_transactions(data, currency, since, limit) + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://docs.ox.fun/?json#get-v3-withdrawal + + :param str code: unified currency code of the currency transferred + :param int [since]: the earliest time in ms to fetch transfers for(default 24 hours ago) + :param int [limit]: the maximum number of transfer structures to retrieve(default 50, max 200) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch transfers for(default time now) + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + request: dict = {} + currency: Currency = None + if code is not None: + currency = self.currency(code) + request['asset'] = currency['id'] + if since is not None: + request['startTime'] = since # startTime and endTime must be within 7 days of each other + if limit is not None: + request['limit'] = limit + until = self.safe_integer(params, 'until') + if until is not None: + request['endTime'] = until + params = self.omit(params, 'until') + response = await self.privateGetV3Withdrawal(self.extend(request, params)) + # + # { + # success: True, + # data: [ + # { + # id: '968163212989431811', + # asset: 'OX', + # network: 'Arbitrum', + # address: '0x90fc1fB49a4ED8f485dd02A2a1Cf576897f6Bfc9', + # quantity: '11.7444', + # fee: '1.744400000', + # status: 'COMPLETED', + # txId: '0xe96b2d128b737fdbca927edf355cff42202e65b0fb960e64ffb9bd68c121f69f', + # requestedAt: '1715530365450', + # completedAt: '1715530527000' + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + for i in range(0, len(data)): + data[i]['type'] = 'withdrawal' + return self.parse_transactions(data, currency, since, limit) + + def parse_transactions(self, transactions, currency: Currency = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + result = [] + for i in range(0, len(transactions)): + transactions[i] = self.extend(transactions[i], params) + transaction = self.parse_transaction(transactions[i], currency) + result.append(transaction) + result = self.sort_by(result, 'timestamp') + code = currency['code'] if (currency is not None) else None + return self.filter_by_currency_since_limit(result, code, since, limit) + + def parse_transaction(self, transaction, currency: Currency = None) -> Transaction: + # + # fetchDeposits + # { + # "asset":"USDC", + # "network":"Ethereum", + # "address": "0x998dEc76151FB723963Bd8AFD517687b38D33dE8", + # "quantity":"50", + # "id":"5914", + # "status": "COMPLETED", + # "txId":"0xf5e79663830a0c6f94d46638dcfbc134566c12facf1832396f81ecb55d3c75dc", + # "creditedAt":"1714821645154" + # } + # + # fetchWithdrawals + # { + # id: '968163212989431811', + # asset: 'OX', + # network: 'Arbitrum', + # address: '0x90fc1fB49a4ED8f485dd02A2a1Cf576897f6Bfc9', + # quantity: '11.7444', + # fee: '1.744400000', + # status: 'COMPLETED', + # txId: '0xe96b2d128b737fdbca927edf355cff42202e65b0fb960e64ffb9bd68c121f69f', + # requestedAt: '1715530365450', + # completedAt: '1715530527000' + # } + # + # withdraw + # { + # "id": "968364664449302529", + # "asset": "OX", + # "network": "Arbitrum", + # "address": "0x90fc1fB49a4ED8f485dd02A2a1Cf576897f6Bfc9", + # "quantity": "10", + # "externalFee": False, + # "fee": "1.6728", + # "status": "PENDING", + # "requestedAt": "1715591843616" + # } + # + id = self.safe_string(transaction, 'id') + type = self.safe_string(transaction, 'type') + transaction = self.omit(transaction, 'type') + address: Str = None + addressTo: Str = None + status: Str = None + if type == 'deposit': + address = self.safe_string(transaction, 'address') + status = self.parse_deposit_status(self.safe_string(transaction, 'status')) + elif type == 'withdrawal': + addressTo = self.safe_string(transaction, 'address') + status = self.parse_withdrawal_status(self.safe_string(transaction, 'status')) + txid = self.safe_string(transaction, 'txId') + currencyId = self.safe_string(transaction, 'asset') + code = self.safe_currency_code(currencyId, currency) + network = self.safe_string(transaction, 'network') + networkCode = self.network_id_to_code(network) + timestamp = self.safe_integer_2(transaction, 'creditedAt', 'requestedAt') + amount = self.safe_number(transaction, 'quantity') + feeCost = self.safe_number(transaction, 'fee') + fee = None + if feeCost is not None: + fee = { + 'cost': feeCost, + 'currency': code, + } + return { + 'info': transaction, + 'id': id, + 'txid': txid, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': networkCode, + 'address': address, + 'addressTo': addressTo, + 'addressFrom': None, + 'tag': None, + 'tagTo': None, + 'tagFrom': None, + 'type': type, + 'amount': amount, + 'currency': code, + 'status': status, + 'updated': None, + 'internal': None, + 'comment': None, + 'fee': fee, + } + + def parse_deposit_status(self, status): + statuses: dict = { + 'COMPLETED': 'ok', + } + return self.safe_string(statuses, status, status) + + def parse_withdrawal_status(self, status): + statuses: dict = { + 'COMPLETED': 'ok', + 'PROCESSING': 'pending', + 'IN SWEEPING': 'pending', + 'PENDING': 'pending', + 'ON HOLD': 'pending', + 'CANCELED': 'canceled', + 'FAILED': 'failed', + } + return self.safe_string(statuses, status, status) + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://docs.ox.fun/?json#post-v3-withdrawal + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.network]: network for withdraw + :param bool [params.externalFee]: if False, then the fee is taken from the quantity, also with the burn fee for asset SOLO + + EXCHANGE SPECIFIC PARAMETERS + :param str [params.tfaType]: GOOGLE, or AUTHY_SECRET, or YUBIKEY, for 2FA + :param str [params.code]: 2FA code + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + await self.load_markets() + currency = self.currency(code) + stringAmount = self.currency_to_precision(code, amount) + request: dict = { + 'asset': currency['id'], + 'address': address, + 'quantity': stringAmount, + } + if tag is not None: + request['memo'] = tag + networkCode: Str = None + networkCode, params = self.handle_network_code_and_params(params) + if networkCode is not None: + request['network'] = self.network_code_to_id(networkCode) + request['externalFee'] = False + response = await self.privatePostV3Withdrawal(self.extend(request, params)) + # + # { + # "success": True, + # "data": { + # "id": "968364664449302529", + # "asset": "OX", + # "network": "Arbitrum", + # "address": "0x90fc1fB49a4ED8f485dd02A2a1Cf576897f6Bfc9", + # "quantity": "10", + # "externalFee": False, + # "fee": "1.6728", + # "status": "PENDING", + # "requestedAt": "1715591843616" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + data['type'] = 'withdrawal' + return self.parse_transaction(data, currency) + + async def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://docs.ox.fun/?json#get-v3-positions + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.subAcc]: + :returns dict[]: a list of `position structure ` + """ + # Calling self endpoint using an API key pair linked to the parent account with the parameter "subAcc" + # allows the caller to include positions of additional sub-accounts in the response. + # This feature does not work when using API key pairs linked to a sub-account + await self.load_markets() + symbols = self.market_symbols(symbols) + response = await self.privateGetV3Positions(params) + # + # { + # "success": True, + # "data": [ + # { + # "accountId": "106490", + # "name": "main", + # "positions": [ + # { + # "marketCode": "BTC-USD-SWAP-LIN", + # "baseAsset": "BTC", + # "counterAsset": "USD", + # "position": "0.00010", + # "entryPrice": "64300.0", + # "markPrice": "63278", + # "positionPnl": "-10.1900", + # "estLiquidationPrice": "0", + # "lastUpdatedAt": "1714915841448" + # }, + # ... + # ] + # }, + # { + # "accountId": "106526", + # "name": "testSubAccount", + # "positions": [ + # { + # "marketCode": "ETH-USD-SWAP-LIN", + # "baseAsset": "ETH", + # "counterAsset": "USD", + # "position": "0.001", + # "entryPrice": "3080.5", + # "markPrice": "3062.0", + # "positionPnl": "-1.8500", + # "estLiquidationPrice": "0", + # "lastUpdatedAt": "1715089678013" + # }, + # ... + # ] + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + allPositions = [] + for i in range(0, len(data)): + account = data[i] + positions = self.safe_list(account, 'positions', []) + allPositions = self.array_concat(allPositions, positions) + return self.parse_positions(allPositions, symbols) + + def parse_position(self, position, market: Market = None): + # + # { + # "marketCode": "ETH-USD-SWAP-LIN", + # "baseAsset": "ETH", + # "counterAsset": "USD", + # "position": "0.001", + # "entryPrice": "3080.5", + # "markPrice": "3062.0", + # "positionPnl": "-1.8500", + # "estLiquidationPrice": "0", + # "lastUpdatedAt": "1715089678013" + # } + # + marketId = self.safe_string(position, 'marketCode') + market = self.safe_market(marketId, market) + return self.safe_position({ + 'info': position, + 'id': None, + 'symbol': market['symbol'], + 'notional': None, + 'marginMode': 'cross', + 'liquidationPrice': self.safe_number(position, 'estLiquidationPrice'), + 'entryPrice': self.safe_number(position, 'entryPrice'), + 'unrealizedPnl': self.safe_number(position, 'positionPnl'), + 'realizedPnl': None, + 'percentage': None, + 'contracts': self.safe_number(position, 'position'), + 'contractSize': None, + 'markPrice': self.safe_number(position, 'markPrice'), + 'lastPrice': None, + 'side': None, + 'hedged': None, + 'timestamp': None, + 'datetime': None, + 'lastUpdateTimestamp': self.safe_integer(position, 'lastUpdatedAt'), + 'maintenanceMargin': None, + 'maintenanceMarginPercentage': None, + 'collateral': None, + 'initialMargin': None, + 'initialMarginPercentage': None, + 'leverage': None, + 'marginRatio': None, + 'stopLossPrice': None, + 'takeProfitPrice': None, + }) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}) -> Order: + """ + create a trade order + + https://docs.ox.fun/?json#post-v3-orders-place + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market', 'limit', 'STOP_LIMIT' or 'STOP_MARKET' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.clientOrderId]: a unique id for the order + :param int [params.timestamp]: in milliseconds. If an order reaches the matching engine and the current timestamp exceeds timestamp + recvWindow, then the order will be rejected. + :param int [params.recvWindow]: in milliseconds. If an order reaches the matching engine and the current timestamp exceeds timestamp + recvWindow, then the order will be rejected. If timestamp is provided without recvWindow, then a default recvWindow of 1000ms is used. + :param str [params.responseType]: FULL or ACK + :param float [params.cost]: the quote quantity that can be used alternative for the amount for market buy orders + :param float [params.triggerPrice]: The price at which a trigger order is triggered at + :param float [params.limitPrice]: Limit price for the STOP_LIMIT order + :param bool [params.postOnly]: if True, the order will only be posted if it will be a maker order + :param str [params.timeInForce]: GTC(default), IOC, FOK, PO, MAKER_ONLY or MAKER_ONLY_REPRICE(reprices order to the best maker only price if the specified price were to lead to a taker trade) + :param str [params.selfTradePrevention]: NONE, EXPIRE_MAKER, EXPIRE_TAKER or EXPIRE_BOTH for more info check here {@link https://docs.ox.fun/?json#self-trade-prevention-modes} + :param str [params.displayQuantity]: for an iceberg order, pass both quantity and displayQuantity fields in the order request + :returns dict: an `order structure ` + """ + await self.load_markets() + request: dict = { + 'responseType': self.safe_string(params, 'responseType', 'FULL'), + 'timestamp': self.safe_integer(params, 'timestamp', self.milliseconds()), + } + params = self.omit(params, ['responseType', 'timestamp']) + recvWindow = self.safe_integer(params, 'recvWindow') + if recvWindow is not None: + request['recvWindow'] = recvWindow + params = self.omit(params, 'recvWindow') + orderRequest = self.create_order_request(symbol, type, side, amount, price, params) + request['orders'] = [orderRequest] + response = await self.privatePostV3OrdersPlace(request) + # + # accepted market order responseType FULL + # { + # "success": True, + # "data": [ + # { + # "notice": "OrderMatched", + # "accountId": "106490", + # "orderId": "1000109901865", + # "submitted": True, + # "clientOrderId": "0", + # "marketCode": "OX-USDT", + # "status": "FILLED", + # "side": "SELL", + # "isTriggered": False, + # "quantity": "150.0", + # "amount": "0.0", + # "remainQuantity": "0.0", + # "matchId": "100017047880451399", + # "matchPrice": "0.01465", + # "matchQuantity": "150.0", + # "feeInstrumentId": "USDT", + # "fees": "0.0015382500", + # "orderType": "MARKET", + # "createdAt": "1715592472236", + # "lastMatchedAt": "1715592472200", + # "displayQuantity": "150.0" + # } + # ] + # } + # + # accepted limit order responseType FULL + # { + # "success": True, + # "data": [ + # { + # "notice": "OrderOpened", + # "accountId": "106490", + # "orderId": "1000111482406", + # "submitted": True, + # "clientOrderId": "0", + # "marketCode": "ETH-USD-SWAP-LIN", + # "status": "OPEN", + # "side": "SELL", + # "price": "4000.0", + # "isTriggered": False, + # "quantity": "0.01", + # "amount": "0.0", + # "orderType": "LIMIT", + # "timeInForce": "GTC", + # "createdAt": "1715763507682", + # "displayQuantity": "0.01" + # } + # ] + # } + # + # accepted order responseType ACK + # { + # "success": True, + # "data": [ + # { + # "accountId": "106490", + # "orderId": "1000109892193", + # "submitted": True, + # "marketCode": "OX-USDT", + # "side": "BUY", + # "price": "0.01961", + # "isTriggered": False, + # "quantity": "100", + # "orderType": "MARKET", + # "timeInForce": "IOC", + # "createdAt": "1715591529057", + # "selfTradePreventionMode": "NONE" + # } + # ] + # } + # + # rejected order(balance insufficient) + # { + # "success": True, + # "data": [ + # { + # "code": "710001", + # "message": "System failure, exception thrown -> null", + # "submitted": False, + # "marketCode": "OX-USDT", + # "side": "BUY", + # "price": "0.01961", + # "amount": "100", + # "orderType": "MARKET", + # "timeInForce": "IOC", + # "createdAt": "1715591678835", + # "source": 11, + # "selfTradePreventionMode": "NONE" + # } + # ] + # } + # + # rejected order(bad request) + # { + # "success": True, + # "data": [ + # { + # "code": "20044", + # "message": "Amount is not supported for self order type", + # "submitted": False, + # "marketCode": "OX-USDT", + # "side": "SELL", + # "amount": "200", + # "orderType": "MARKET", + # "createdAt": "1715592079986", + # "source": 11 + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + order = self.safe_dict(data, 0, {}) + return self.parse_order(order) + + async def create_orders(self, orders: List[OrderRequest], params={}) -> List[Order]: + """ + create a list of trade orders + + https://docs.ox.fun/?json#post-v3-orders-place + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.timestamp]: *for all orders* in milliseconds. If orders reach the matching engine and the current timestamp exceeds timestamp + recvWindow, then all orders will be rejected. + :param int [params.recvWindow]: *for all orders* in milliseconds. If orders reach the matching engine and the current timestamp exceeds timestamp + recvWindow, then all orders will be rejected. If timestamp is provided without recvWindow, then a default recvWindow of 1000ms is used. + :param str [params.responseType]: *for all orders* FULL or ACK + :returns dict: an `order structure ` + """ + await self.load_markets() + ordersRequests = [] + for i in range(0, len(orders)): + rawOrder = orders[i] + symbol = self.safe_string(rawOrder, 'symbol') + type = self.safe_string(rawOrder, 'type') + side = self.safe_string(rawOrder, 'side') + amount = self.safe_number(rawOrder, 'amount') + price = self.safe_number(rawOrder, 'price') + orderParams = self.safe_dict(rawOrder, 'params', {}) + orderRequest = self.create_order_request(symbol, type, side, amount, price, orderParams) + ordersRequests.append(orderRequest) + request: dict = { + 'responseType': 'FULL', + 'timestamp': self.milliseconds(), + 'orders': ordersRequests, + } + response = await self.privatePostV3OrdersPlace(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + return self.parse_orders(data) + + def create_order_request(self, symbol: str, type: str, side: str, amount, price=None, params={}): + """ + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market', 'limit', 'STOP_LIMIT' or 'STOP_MARKET' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.clientOrderId]: a unique id for the order + :param float [params.cost]: the quote quantity that can be used alternative for the amount for market buy orders + :param float [params.triggerPrice]: The price at which a trigger order is triggered at + :param float [params.limitPrice]: Limit price for the STOP_LIMIT order + :param bool [params.postOnly]: if True, the order will only be posted if it will be a maker order + :param str [params.timeInForce]: GTC(default), IOC, FOK, PO, MAKER_ONLY or MAKER_ONLY_REPRICE(reprices order to the best maker only price if the specified price were to lead to a taker trade) + :param str [params.selfTradePrevention]: NONE, EXPIRE_MAKER, EXPIRE_TAKER or EXPIRE_BOTH for more info check here {@link https://docs.ox.fun/?json#self-trade-prevention-modes} + :param str [params.displayQuantity]: for an iceberg order, pass both quantity and displayQuantity fields in the order request + """ + market = self.market(symbol) + request: dict = { + 'marketCode': market['id'], + 'side': side.upper(), + 'source': 1000, + } + cost = self.safe_string_2(params, 'cost', 'amount') + if cost is not None: + request['amount'] = cost # todo costToPrecision + params = self.omit(params, ['cost', 'amount']) + else: + request['quantity'] = amount # todo amountToPrecision + triggerPrice = self.safe_string_2(params, 'triggerPrice', 'stopPrice') + orderType = type.upper() + if triggerPrice is not None: + if orderType == 'MARKET': + orderType = 'STOP_MARKET' + elif orderType == 'LIMIT': + orderType = 'STOP_LIMIT' + request['stopPrice'] = triggerPrice # todo priceToPrecision + params = self.omit(params, ['triggerPrice', 'stopPrice']) + request['orderType'] = orderType + if orderType == 'STOP_LIMIT': + request['limitPrice'] = price # todo priceToPrecision + elif price is not None: + request['price'] = price # todo priceToPrecision + postOnly: Bool = None + isMarketOrder = (orderType == 'MARKET') or (orderType == 'STOP_MARKET') + postOnly, params = self.handle_post_only(isMarketOrder, False, params) + timeInForce = self.safe_string_upper(params, 'timeInForce') + if postOnly and (timeInForce != 'MAKER_ONLY_REPRICE'): + request['timeInForce'] = 'MAKER_ONLY' + selfTradePrevention = None + selfTradePrevention, params = self.handle_option_and_params(params, 'createOrder', 'selfTradePrevention') + if selfTradePrevention is not None: + request['selfTradePreventionMode'] = selfTradePrevention.upper() + return self.extend(request, params) + + async def create_market_buy_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market buy order by providing the symbol and cost + + https://open.big.one/docs/spot_orders.html#create-order + + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' createMarketBuyOrderWithCost() supports spot orders only') + request: dict = { + 'cost': cost, + } + return await self.create_order(symbol, 'market', 'buy', None, None, self.extend(request, params)) + + async def fetch_order(self, id: str, symbol: Str = None, params={}) -> Order: + """ + + https://docs.ox.fun/?json#get-v3-orders-status + + fetches information on an order made by the user + :param str id: a unique id for the order + :param str [symbol]: not used by oxfun fetchOrder + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.clientOrderId]: the client order id of the order + :returns dict: An `order structure ` + """ + await self.load_markets() + request: dict = { + 'orderId': id, + } + response = await self.privateGetV3OrdersStatus(self.extend(request, params)) + # + # { + # "success": True, + # "data": { + # "orderId": "1000111762980", + # "clientOrderId": "0", + # "marketCode": "ETH-USD-SWAP-LIN", + # "status": "OPEN", + # "side": "BUY", + # "price": "2700.0", + # "isTriggered": False, + # "remainQuantity": "0.01", + # "totalQuantity": "0.01", + # "amount": "0", + # "displayQuantity": "0.01", + # "cumulativeMatchedQuantity": "0", + # "orderType": "STOP_LIMIT", + # "timeInForce": "GTC", + # "source": "11", + # "createdAt": "1715794191277" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://docs.ox.fun/?json#get-v3-orders-working + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.orderId]: a unique id for the order + :param int [params.clientOrderId]: the client order id of the order + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + request: dict = {} + market: Market = None + if symbol is not None: + market = self.market(symbol) + response = await self.privateGetV3OrdersWorking(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + return self.parse_orders(data, market, since, limit) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://docs.ox.fun/?json#delete-v3-orders-cancel + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.clientOrderId]: a unique id for the order + :param int [params.timestamp]: in milliseconds + :param int [params.recvWindow]: in milliseconds + :param str [params.responseType]: 'FULL' or 'ACK' + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + market = self.market(symbol) + marketId = market['id'] + request: dict = { + 'timestamp': self.milliseconds(), + 'responseType': 'FULL', + } + orderRequest = { + 'marketCode': marketId, + 'orderId': id, + } + clientOrderId = self.safe_integer(params, 'clientOrderId') + if clientOrderId is not None: + orderRequest['clientOrderId'] = clientOrderId + request['orders'] = [orderRequest] + response = await self.privateDeleteV3OrdersCancel(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + order = self.safe_dict(data, 0, {}) + return self.parse_order(order) + + async def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders + + https://docs.ox.fun/?json#delete-v3-orders-cancel-all + + :param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from exchange + """ + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['marketCode'] = market['id'] + # + # { + # "success": True, + # "data": {"notice": "Orders queued for cancelation"} + # } + # + # { + # "success": True, + # "data": {"notice": "No working orders found"} + # } + # + response = await self.privateDeleteV3OrdersCancelAll(self.extend(request, params)) + return [self.safe_order({'info': response})] + + async def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): + """ + cancel multiple orders + + https://docs.ox.fun/?json#delete-v3-orders-cancel + + :param str[] ids: order ids + :param str [symbol]: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.timestamp]: in milliseconds + :param int [params.recvWindow]: in milliseconds + :param str [params.responseType]: 'FULL' or 'ACK' + :returns dict: an list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrders() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + marketId = market['id'] + request: dict = { + 'timestamp': self.milliseconds(), + 'responseType': 'FULL', + } + orders = [] + for i in range(0, len(ids)): + order = { + 'marketCode': marketId, + 'orderId': ids[i], + } + orders.append(order) + request['orders'] = orders + response = await self.privateDeleteV3OrdersCancel(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + return self.parse_orders(data, market) + + def parse_order(self, order, market: Market = None) -> Order: + # + # accepted market order responseType FULL + # { + # "notice": "OrderMatched", + # "accountId": "106490", + # "orderId": "1000109901865", + # "submitted": True, + # "clientOrderId": "0", + # "marketCode": "OX-USDT", + # "status": "FILLED", + # "side": "SELL", + # "isTriggered": False, + # "quantity": "150.0", + # "amount": "0.0", + # "remainQuantity": "0.0", + # "matchId": "100017047880451399", + # "matchPrice": "0.01465", + # "matchQuantity": "150.0", + # "feeInstrumentId": "USDT", + # "fees": "0.0015382500", + # "orderType": "MARKET", + # "createdAt": "1715592472236", + # "lastMatchedAt": "1715592472200", + # "displayQuantity": "150.0" + # } + # + # accepted limit order responseType FULL + # { + # "notice": "OrderOpened", + # "accountId": "106490", + # "orderId": "1000111482406", + # "submitted": True, + # "clientOrderId": "0", + # "marketCode": "ETH-USD-SWAP-LIN", + # "status": "OPEN", + # "side": "SELL", + # "price": "4000.0", + # "isTriggered": False, + # "quantity": "0.01", + # "amount": "0.0", + # "orderType": "LIMIT", + # "timeInForce": "GTC", + # "createdAt": "1715763507682", + # "displayQuantity": "0.01" + # } + # + # accepted order responseType ACK + # { + # "accountId": "106490", + # "orderId": "1000109892193", + # "submitted": True, + # "marketCode": "OX-USDT", + # "side": "BUY", + # "price": "0.01961", + # "isTriggered": False, + # "quantity": "100", + # "orderType": "MARKET", + # "timeInForce": "IOC", + # "createdAt": "1715591529057", + # "selfTradePreventionMode": "NONE" + # } + # + # rejected order(balance insufficient) + # { + # "code": "710001", + # "message": "System failure, exception thrown -> null", + # "submitted": False, + # "marketCode": "OX-USDT", + # "side": "BUY", + # "price": "0.01961", + # "amount": "100", + # "orderType": "MARKET", + # "timeInForce": "IOC", + # "createdAt": "1715591678835", + # "source": 11, + # "selfTradePreventionMode": "NONE" + # } + # + # rejected order(bad request) + # { + # "code": "20044", + # "message": "Amount is not supported for self order type", + # "submitted": False, + # "marketCode": "OX-USDT", + # "side": "SELL", + # "amount": "200", + # "orderType": "MARKET", + # "createdAt": "1715592079986", + # "source": 11 + # } + # + # fetchOrder + # { + # "orderId": "1000111762980", + # "clientOrderId": "0", + # "marketCode": "ETH-USD-SWAP-LIN", + # "status": "OPEN", + # "side": "BUY", + # "price": "2700.0", + # "isTriggered": False, + # "remainQuantity": "0.01", + # "totalQuantity": "0.01", + # "amount": "0", + # "displayQuantity": "0.01", + # "cumulativeMatchedQuantity": "0", + # "orderType": "STOP_LIMIT", + # "timeInForce": "GTC", + # "source": "11", + # "createdAt": "1715794191277" + # } + # + marketId = self.safe_string(order, 'marketCode') + market = self.safe_market(marketId, market) + timestamp = self.safe_integer(order, 'createdAt') + fee = None + feeCurrency = self.safe_string(order, 'feeInstrumentId') + if feeCurrency is not None: + fee = { + 'currency': self.safe_currency_code(feeCurrency), + 'cost': self.safe_number(order, 'fees'), + } + status = self.safe_string(order, 'status') + code = self.safe_integer(order, 'code') # rejected orders have code of the error + if code is not None: + status = 'rejected' + triggerPrice = self.safe_string(order, 'stopPrice') + return self.safe_order({ + 'id': self.safe_string(order, 'orderId'), + 'clientOrderId': self.safe_string(order, 'clientOrderId'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': self.safe_integer(order, 'lastMatchedAt'), + 'lastUpdateTimestamp': self.safe_integer(order, 'lastModifiedAt'), + 'status': self.parse_order_status(status), + 'symbol': market['symbol'], + 'type': self.parse_order_type(self.safe_string(order, 'orderType')), + 'timeInForce': self.parse_order_time_in_force(self.safe_string(order, 'timeInForce')), # only for limit orders + 'side': self.safe_string_lower(order, 'side'), + 'price': self.safe_string_n(order, ['price', 'matchPrice', 'limitPrice']), + 'average': None, + 'amount': self.safe_string_2(order, 'totalQuantity', 'quantity'), + 'filled': self.safe_string_2(order, 'cumulativeMatchedQuantity', 'matchQuantity'), + 'remaining': self.safe_string(order, 'remainQuantity'), + 'triggerPrice': triggerPrice, + 'stopLossPrice': triggerPrice, + 'cost': self.omit_zero(self.safe_string(order, 'amount')), + 'trades': None, + 'fee': fee, + 'info': order, + }, market) + + def parse_order_status(self, status): + statuses: dict = { + 'OPEN': 'open', + 'PARTIALLY_FILLED': 'open', + 'PARTIAL_FILL': 'open', + 'FILLED': 'closed', + 'CANCELED': 'canceled', + 'CANCELED_BY_USER': 'canceled', + 'CANCELED_BY_MAKER_ONLY': 'rejected', + 'CANCELED_BY_FOK': 'rejected', + 'CANCELED_ALL_BY_IOC': 'rejected', + 'CANCELED_PARTIAL_BY_IOC': 'canceled', + 'CANCELED_BY_SELF_TRADE_PROTECTION': 'rejected', + } + return self.safe_string(statuses, status, status) + + def parse_order_type(self, type): + types: dict = { + 'LIMIT': 'limit', + 'STOP_LIMIT': 'limit', + 'MARKET': 'market', + 'STOP_MARKET': 'market', + } + return self.safe_string(types, type, type) + + def parse_order_time_in_force(self, type): + types: dict = { + 'GTC': 'GTC', + 'IOC': 'IOC', + 'FOK': 'FOK', + 'MAKER_ONLY': 'PO', + 'MAKER_ONLY_REPRICE': 'PO', + } + return self.safe_string(types, type, type) + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + baseUrl = self.urls['api'][api] + url = baseUrl + '/' + path + queryString = '' + if method == 'GET': + queryString = self.urlencode(params) + if len(queryString) != 0: + url += '?' + queryString + if api == 'private': + self.check_required_credentials() + timestamp = self.milliseconds() + isoDatetime = self.iso8601(timestamp) + datetimeParts = isoDatetime.split('.') + datetime = datetimeParts[0] + nonce = self.nonce() + urlParts = baseUrl.split('//') + if (method == 'POST') or (method == 'DELETE'): + body = self.json(params) + queryString = body + msgString = datetime + '\n' + str(nonce) + '\n' + method + '\n' + urlParts[1] + '\n/' + path + '\n' + queryString + signature = self.hmac(self.encode(msgString), self.encode(self.secret), hashlib.sha256, 'base64') + headers = { + 'Content-Type': 'application/json', + 'AccessKey': self.apiKey, + 'Timestamp': datetime, + 'Signature': signature, + 'Nonce': str(nonce), + } + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code, reason, url, method, headers, body, response, requestHeaders, requestBody): + if response is None: + return None + if code != 200: + responseCode = self.safe_string(response, 'code', None) + feedback = self.id + ' ' + body + self.throw_broadly_matched_exception(self.exceptions['broad'], body, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], responseCode, feedback) + raise ExchangeError(feedback) + return None diff --git a/ccxt/async_support/p2b.py b/ccxt/async_support/p2b.py new file mode 100644 index 0000000..7dbbf09 --- /dev/null +++ b/ccxt/async_support/p2b.py @@ -0,0 +1,1316 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.p2b import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Int, Market, Num, Order, OrderSide, OrderType, Str, Strings, Ticker, Tickers +from typing import List +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.decimal_to_precision import TICK_SIZE + + +class p2b(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(p2b, self).describe(), { + 'id': 'p2b', + 'name': 'p2b', + 'countries': ['LT'], + 'rateLimit': 100, + 'version': 'v2', + 'pro': True, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelAllOrders': False, + 'cancelOrder': True, + 'cancelOrders': False, + 'closeAllPositions': False, + 'closePosition': False, + 'createDepositAddress': False, + 'createMarketOrder': False, + 'createOrder': True, + 'createOrders': False, + 'createPostOnlyOrder': False, + 'createReduceOnlyOrder': False, + 'createStopLimitOrder': False, + 'createStopMarketOrder': False, + 'createStopOrder': False, + 'fetchAccounts': False, + 'fetchAllGreeks': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchDeposit': False, + 'fetchDepositAddress': False, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': False, + 'fetchDepositsWithdrawals': False, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLedger': False, + 'fetchLedgerEntry': False, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrice': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrderBook': True, + 'fetchOrderBooks': False, + 'fetchOrders': False, + 'fetchOrderTrades': True, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTrades': True, + 'fetchTradingLimits': False, + 'fetchTransactionFee': False, + 'fetchTransactionFees': False, + 'fetchTransactions': False, + 'fetchTransfers': False, + 'fetchUnderlyingAssets': False, + 'fetchVolatilityHistory': False, + 'fetchWithdrawAddresses': False, + 'fetchWithdrawal': False, + 'fetchWithdrawals': False, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'signIn': False, + 'transfer': False, + 'withdraw': False, + }, + 'timeframes': { + '1m': '1m', + '1h': '1h', + '1d': '1d', + }, + 'urls': { + 'extension': '.json', + 'referral': 'https://p2pb2b.com?referral=ee784c53', + 'logo': 'https://github.com/ccxt/ccxt/assets/43336371/8da13a80-1f0a-49be-bb90-ff8b25164755', + 'api': { + 'public': 'https://api.p2pb2b.com/api/v2/public', + 'private': 'https://api.p2pb2b.com/api/v2', + }, + 'www': 'https://p2pb2b.com/', + 'doc': 'https://github.com/P2B-team/p2b-api-docs/blob/master/api-doc.md', + 'fees': 'https://p2pb2b.com/fee-schedule/', + }, + 'api': { + 'public': { + 'get': { + 'markets': 1, + 'market': 1, + 'tickers': 1, + 'ticker': 1, + 'book': 1, + 'history': 1, + 'depth/result': 1, + 'market/kline': 1, + }, + }, + 'private': { + 'post': { + 'account/balances': 1, + 'account/balance': 1, + 'order/new': 1, + 'order/cancel': 1, + 'orders': 1, + 'account/market_order_history': 1, + 'account/market_deal_history': 1, + 'account/order': 1, + 'account/order_history': 1, + 'account/executed_history': 1, + }, + }, + }, + 'fees': { + 'trading': { + 'tierBased': True, + 'percentage': True, + 'taker': [ + [self.parse_number('0'), self.parse_number('0.2')], + [self.parse_number('1'), self.parse_number('0.19')], + [self.parse_number('5'), self.parse_number('0.18')], + [self.parse_number('10'), self.parse_number('0.17')], + [self.parse_number('25'), self.parse_number('0.16')], + [self.parse_number('75'), self.parse_number('0.15')], + [self.parse_number('100'), self.parse_number('0.14')], + [self.parse_number('150'), self.parse_number('0.13')], + [self.parse_number('300'), self.parse_number('0.12')], + [self.parse_number('450'), self.parse_number('0.11')], + [self.parse_number('500'), self.parse_number('0.1')], + ], + 'maker': [ + [self.parse_number('0'), self.parse_number('0.2')], + [self.parse_number('1'), self.parse_number('0.18')], + [self.parse_number('5'), self.parse_number('0.16')], + [self.parse_number('10'), self.parse_number('0.14')], + [self.parse_number('25'), self.parse_number('0.12')], + [self.parse_number('75'), self.parse_number('0.1')], + [self.parse_number('100'), self.parse_number('0.08')], + [self.parse_number('150'), self.parse_number('0.06')], + [self.parse_number('300'), self.parse_number('0.04')], + [self.parse_number('450'), self.parse_number('0.02')], + [self.parse_number('500'), self.parse_number('0.01')], + ], + }, + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': False, + 'triggerDirection': False, + 'triggerPriceType': None, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': False, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 100000, # todo + 'untilDays': 1, + 'symbolRequired': True, + }, + 'fetchOrder': None, # todo + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 100, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOrders': None, # todo + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 100000, # todo + 'daysBackCanceled': 1 / 12, # todo + 'untilDays': 1, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 500, + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'commonCurrencies': { + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + '1001': AuthenticationError, # Key not provided. X-TXC-APIKEY header is missing in the request or empty. + '1002': AuthenticationError, # Payload not provided. X-TXC-PAYLOAD header is missing in the request or empty. + '1003': AuthenticationError, # Signature not provided. X-TXC-SIGNATURE header is missing in the request or empty. + '1004': AuthenticationError, # Nonce and url not provided. Request body is empty. Missing required parameters "request", "nonce". + '1005': AuthenticationError, # Invalid body data. Invalid request body + '1006': AuthenticationError, # Nonce not provided. Request body missing required parameter "nonce". + '1007': AuthenticationError, # Request not provided. Request body missing required parameter "request". + '1008': AuthenticationError, # Invalid request in body. The passed request parameter does not match the URL of self request. + '1009': AuthenticationError, # Invalid payload. The transmitted payload value(X-TXC-PAYLOAD header) does not match the request body. + '1010': AuthenticationError, # This action is unauthorized. - API key passed in the X-TXC-APIKEY header does not exist. - Access to API is not activated. Go to profile and activate access. + '1011': AuthenticationError, # This action is unauthorized. Please, enable two-factor authentication. Two-factor authentication is not activated for the user. + '1012': AuthenticationError, # Invalid nonce. Parameter "nonce" is not a number. + '1013': AuthenticationError, # Too many requests. - A request came with a repeated value of nonce. - Received more than the limited value of requests(10) within one second. + '1014': AuthenticationError, # Unauthorized request. Signature value passed(in the X-TXC-SIGNATURE header) does not match the request body. + '1015': AuthenticationError, # Temporary block. Temporary blocking. There is a cancellation of orders. + '1016': AuthenticationError, # Not unique nonce. The request was sent with a repeated parameter "nonce" within 10 seconds. + '2010': BadRequest, # Currency not found. Currency not found. + '2020': BadRequest, # Market is not available. Market is not available. + '2021': BadRequest, # Unknown market. Unknown market. + '2030': BadRequest, # Order not found. Order not found. + '2040': InsufficientFunds, # Balance not enough. Insufficient balance. + '2050': BadRequest, # Amount less than the permitted minimum. Amount less than the permitted minimum. + '2051': BadRequest, # Amount is greater than the maximum allowed. Amount exceeds the allowed maximum. + '2052': BadRequest, # Amount step size error. Amount step size error. + '2060': BadRequest, # Price less than the permitted minimum. Price is less than the permitted minimum. + '2061': BadRequest, # Price is greater than the maximum allowed. Price exceeds the allowed maximum. + '2062': BadRequest, # Price pick size error. Price pick size error. + '2070': BadRequest, # Total less than the permitted minimum. Total less than the permitted minimum. + '3001': BadRequest, # Validation exception. The given data was invalid. + '3020': BadRequest, # Invalid currency value. Incorrect parameter, check your request. + '3030': BadRequest, # Invalid market value. Incorrect "market" parameter, check your request. + '3040': BadRequest, # Invalid amount value. Incorrect "amount" parameter, check your request. + '3050': BadRequest, # Invalid price value. Incorrect "price" parameter, check your request. + '3060': BadRequest, # Invalid limit value. Incorrect "limit" parameter, check your request. + '3070': BadRequest, # Invalid offset value. Incorrect "offset" parameter, check your request. + '3080': BadRequest, # Invalid orderId value. Incorrect "orderId" parameter, check your request. + '3090': BadRequest, # Invalid lastId value. Incorrect "lastId" parameter, check your request. + '3100': BadRequest, # Invalid side value. Incorrect "side" parameter, check your request. + '3110': BadRequest, # Invalid interval value. Incorrect "interval" parameter, check your request. + '4001': ExchangeNotAvailable, # Service temporary unavailable. An unexpected system error has occurred. Try again after a while. If the error persists, please contact support. + '6010': InsufficientFunds, # Balance not enough. Insufficient balance. + }, + 'options': { + }, + }) + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for bigone + + https://github.com/P2B-team/p2b-api-docs/blob/master/api-doc.md#markets + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = await self.publicGetMarkets(params) + # + # { + # "success": True, + # "errorCode": "", + # "message": "", + # "result": [ + # { + # "name": "ETH_BTC", + # "stock": "ETH", + # "money": "BTC", + # "precision": { + # "money": "6", + # "stock": "4", + # "fee": "4" + # }, + # "limits": { + # "min_amount": "0.001", + # "max_amount": "100000", + # "step_size": "0.0001", + # "min_price": "0.00001", + # "max_price": "922327", + # "tick_size": "0.00001", + # "min_total": "0.0001" + # } + # }, + # ... + # ] + # } + # + markets = self.safe_value(response, 'result', []) + return self.parse_markets(markets) + + def parse_market(self, market: dict) -> Market: + marketId = self.safe_string(market, 'name') + baseId = self.safe_string(market, 'stock') + quoteId = self.safe_string(market, 'money') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + limits = self.safe_value(market, 'limits') + maxAmount = self.safe_string(limits, 'max_amount') + maxPrice = self.safe_string(limits, 'max_price') + return { + 'id': marketId, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': True, + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.safe_number(limits, 'step_size'), + 'price': self.safe_number(limits, 'tick_size'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(limits, 'min_amount'), + 'max': self.parse_number(self.omit_zero(maxAmount)), + }, + 'price': { + 'min': self.safe_number(limits, 'min_price'), + 'max': self.parse_number(self.omit_zero(maxPrice)), + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + 'info': market, + } + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://futures-docs.poloniex.com/#get-real-time-ticker-of-all-symbols + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + response = await self.publicGetTickers(params) + # + # { + # success: True, + # errorCode: '', + # message: '', + # result: { + # KNOLIX_BTC: { + # at: '1699252631', + # ticker: { + # bid: '0.0000332', + # ask: '0.0000333', + # low: '0.0000301', + # high: '0.0000338', + # last: '0.0000333', + # vol: '15.66', + # deal: '0.000501828', + # change: '10.63' + # } + # }, + # ... + # }, + # cache_time: '1699252631.103631', + # current_time: '1699252644.487566' + # } + # + result = self.safe_value(response, 'result', {}) + return self.parse_tickers(result, symbols) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://github.com/P2B-team/p2b-api-docs/blob/master/api-doc.md#ticker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + response = await self.publicGetTicker(self.extend(request, params)) + # + # { + # success: True, + # errorCode: '', + # message: '', + # result: { + # bid: '0.342', + # ask: '0.3421', + # open: '0.3317', + # high: '0.3499', + # low: '0.3311', + # last: '0.3421', + # volume: '17855383.1', + # deal: '6107478.3423', + # change: '3.13' + # }, + # cache_time: '1699252953.832795', + # current_time: '1699252958.859391' + # } + # + result = self.safe_value(response, 'result', {}) + timestamp = self.safe_integer_product(response, 'cache_time', 1000) + return self.extend( + {'timestamp': timestamp, 'datetime': self.iso8601(timestamp)}, + self.parse_ticker(result, market) + ) + + def parse_ticker(self, ticker, market: Market = None): + # + # parseTickers + # + # { + # at: '1699252631', + # ticker: { + # bid: '0.0000332', + # ask: '0.0000333', + # low: '0.0000301', + # high: '0.0000338', + # last: '0.0000333', + # vol: '15.66', + # deal: '0.000501828', + # change: '10.63' + # } + # } + # + # parseTicker + # + # { + # bid: '0.342', + # ask: '0.3421', + # open: '0.3317', + # high: '0.3499', + # low: '0.3311', + # last: '0.3421', + # volume: '17855383.1', + # deal: '6107478.3423', + # change: '3.13' + # } + # + timestamp = self.safe_integer_product(ticker, 'at', 1000) + if 'ticker' in ticker: + ticker = self.safe_value(ticker, 'ticker') + last = self.safe_string(ticker, 'last') + return self.safe_ticker({ + 'symbol': self.safe_string(market, 'symbol'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': self.safe_string(ticker, 'bid'), + 'bidVolume': None, + 'ask': self.safe_string(ticker, 'ask'), + 'askVolume': None, + 'vwap': None, + 'open': self.safe_string(ticker, 'open'), + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': self.safe_string(ticker, 'change'), + 'average': None, + 'baseVolume': self.safe_string_2(ticker, 'vol', 'volume'), + 'quoteVolume': self.safe_string(ticker, 'deal'), + 'info': ticker, + }, market) + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}): + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://github.com/P2B-team/p2b-api-docs/blob/master/api-doc.md#depth-result + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + + EXCHANGE SPECIFIC PARAMETERS + :param str [params.interval]: 0(default), 0.00000001, 0.0000001, 0.000001, 0.00001, 0.0001, 0.001, 0.01, 0.1, 1 + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + if limit is not None: + request['limit'] = limit + response = await self.publicGetDepthResult(self.extend(request, params)) + # + # { + # "success": True, + # "errorCode": "", + # "message": "", + # "result": { + # "asks": [ + # [ + # "4.53", # Price + # "523.95" # Amount + # ], + # ... + # ], + # "bids": [ + # [ + # "4.51", + # "244.75" + # ], + # ... + # ] + # }, + # "cache_time": 1698733470.469175, + # "current_time": 1698733470.469274 + # } + # + result = self.safe_value(response, 'result', {}) + timestamp = self.safe_integer_product(response, 'current_time', 1000) + return self.parse_order_book(result, market['symbol'], timestamp, 'bids', 'asks', 0, 1) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}): + """ + get the list of most recent trades for a particular symbol + + https://github.com/P2B-team/p2b-api-docs/blob/master/api-doc.md#history + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: 1-100, default=50 + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int params['lastId']: order id + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + lastId = self.safe_integer(params, 'lastId') + if lastId is None: + raise ArgumentsRequired(self.id + ' fetchTrades() requires an extra parameter params["lastId"]') + market = self.market(symbol) + request: dict = { + 'market': market['id'], + 'lastId': lastId, + } + if limit is not None: + request['limit'] = limit + response = await self.publicGetHistory(self.extend(request, params)) + # + # { + # success: True, + # errorCode: '', + # message: '', + # result: [ + # { + # id: '7495738622', + # type: 'sell', + # time: '1699255565.445418', + # amount: '252.6', + # price: '0.3422' + # }, + # ... + # ], + # cache_time: '1699255571.413633', + # current_time: '1699255571.413828' + # } + # + result = self.safe_list(response, 'result', []) + return self.parse_trades(result, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None): + # + # fetchTrades + # + # { + # id: '7495738622', + # type: 'sell', + # time: '1699255565.445418', + # amount: '252.6', + # price: '0.3422' + # } + # + # fetchMyTrades + # + # { + # "deal_id": 7450617292, # Deal id + # "deal_time": 1698506956.66224, # Deal execution time + # "deal_order_id": 171955225751, # Deal order id + # "opposite_order_id": 171955110512, # Opposite order id + # "side": "sell", # Deal side + # "price": "0.05231", # Deal price + # "amount": "0.002", # Deal amount + # "deal": "0.00010462", # Total(price * amount) + # "deal_fee": "0.000000188316", # Deal fee + # "role": "taker", # Role. Taker or maker + # "isSelfTrade": False # is self trade + # } + # + # fetchOrderTrades + # + # { + # "id": 7429883128, # Deal id + # "time": 1698237535.41196, # Deal execution time + # "fee": "0.01755848704", # Deal fee + # "price": "34293.92", # Deal price + # "amount": "0.00032", # Deal amount + # "dealOrderId": 171366551416, # Deal order id + # "role": 1, # Deal role(1 - maker, 2 - taker) + # "deal": "10.9740544" # Total(price * amount) + # } + # + timestamp = self.safe_integer_product_2(trade, 'time', 'deal_time', 1000) + takerOrMaker = self.safe_string(trade, 'role') + if takerOrMaker == '1': + takerOrMaker = 'maker' + elif takerOrMaker == '2': + takerOrMaker = 'taker' + return self.safe_trade({ + 'info': trade, + 'id': self.safe_string_2(trade, 'id', 'deal_id'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': self.safe_string(market, 'symbol'), + 'order': self.safe_string_2(trade, 'dealOrderId', 'deal_order_id'), + 'type': None, + 'side': self.safe_string_2(trade, 'type', 'side'), + 'takerOrMaker': takerOrMaker, + 'price': self.safe_string(trade, 'price'), + 'amount': self.safe_string(trade, 'amount'), + 'cost': self.safe_string(trade, 'deal'), + 'fee': { + 'currency': market['quote'], + 'cost': self.safe_string_2(trade, 'fee', 'deal_fee'), + }, + }, market) + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}): + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://github.com/P2B-team/p2b-api-docs/blob/master/api-doc.md#kline + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: 1m, 1h, or 1d + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: 1-500, default=50 + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.offset]: default=0, with self value the last candles are returned + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + 'interval': timeframe, + } + if limit is not None: + request['limit'] = limit + response = await self.publicGetMarketKline(self.extend(request, params)) + # + # { + # success: True, + # errorCode: '', + # message: '', + # result: [ + # [ + # 1699253400, # Kline open time + # '0.3429', # Open price + # '0.3427', # Close price + # '0.3429', # Highest price + # '0.3427', # Lowest price + # '1900.4', # Volume for stock currency + # '651.46278', # Volume for money currency + # 'ADA_USDT' # Market name + # ], + # ... + # ], + # cache_time: '1699256375.030292', + # current_time: '1699256375.030494' + # } + # + result = self.safe_list(response, 'result', []) + return self.parse_ohlcvs(result, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # [ + # 1699253400, # Kline open time + # '0.3429', # Open price + # '0.3427', # Close price + # '0.3429', # Highest price + # '0.3427', # Lowest price + # '1900.4', # Volume for stock currency + # '651.46278', # Volume for money currency + # 'ADA_USDT' # Market name + # ], + # + return [ + self.safe_integer_product(ohlcv, 0, 1000), + self.safe_number(ohlcv, 1), + self.safe_number(ohlcv, 3), + self.safe_number(ohlcv, 4), + self.safe_number(ohlcv, 2), + self.safe_number(ohlcv, 5), + ] + + async def fetch_balance(self, params={}): + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://github.com/P2B-team/p2b-api-docs/blob/master/api-doc.md#all-balances + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + response = await self.privatePostAccountBalances(params) + # + # { + # "success": True, + # "errorCode": "", + # "message": "", + # "result": { + # "USDT": { + # "available": "71.81328046", + # "freeze": "10.46103091" + # }, + # "BTC": { + # "available": "0.00135674", + # "freeze": "0.00020003" + # } + # } + # } + # + result = self.safe_value(response, 'result', {}) + return self.parse_balance(result) + + def parse_balance(self, response): + # + # { + # "USDT": { + # "available": "71.81328046", + # "freeze": "10.46103091" + # }, + # "BTC": { + # "available": "0.00135674", + # "freeze": "0.00020003" + # } + # } + # + result: dict = { + 'info': response, + } + keys = list(response.keys()) + for i in range(0, len(keys)): + currencyId = keys[i] + balance = response[currencyId] + code = self.safe_currency_code(currencyId) + used = self.safe_string(balance, 'freeze') + available = self.safe_string(balance, 'available') + account: dict = { + 'free': available, + 'used': used, + } + result[code] = account + return self.safe_balance(result) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://github.com/P2B-team/p2b-api-docs/blob/master/api-doc.md#create-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: must be 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float price: the price at which the order is to be fulfilled, in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + if type == 'market': + raise BadRequest(self.id + ' createOrder() can only accept orders with type "limit"') + market = self.market(symbol) + request: dict = { + 'market': market['id'], + 'side': side, + 'amount': self.amount_to_precision(symbol, amount), + 'price': self.price_to_precision(symbol, price), + } + response = await self.privatePostOrderNew(self.extend(request, params)) + # + # { + # "success": True, + # "errorCode": "", + # "message": "", + # "result": { + # "orderId": 171906478744, # Order id + # "market": "ETH_BTC", # Market name + # "price": "0.04348", # Price + # "side": "buy", # Side + # "type": "limit", # Order type + # "timestamp": 1698484861.746517, # Order creation time + # "dealMoney": "0", # Filled total + # "dealStock": "0", # Filled amount + # "amount": "0.0277", # Original amount + # "takerFee": "0.002", # taker fee + # "makerFee": "0.002", # maker fee + # "left": "0.0277", # Unfilled amount + # "dealFee": "0" # Filled fee + # } + # } + # + result = self.safe_dict(response, 'result') + return self.parse_order(result, market) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://github.com/P2B-team/p2b-api-docs/blob/master/api-doc.md#cancel-order + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + 'orderId': id, + } + response = await self.privatePostOrderCancel(self.extend(request, params)) + # + # { + # "success": True, + # "errorCode": "", + # "message": "", + # "result": { + # "orderId": 171906478744, + # "market": "ETH_BTC", + # "price": "0.04348", + # "side": "buy", + # "type": "limit", + # "timestamp": 1698484861.746517, + # "dealMoney": "0", + # "dealStock": "0", + # "amount": "0.0277", + # "takerFee": "0.002", + # "makerFee": "0.002", + # "left": "0.0277", + # "dealFee": "0" + # } + # } + # + result = self.safe_dict(response, 'result') + return self.parse_order(result) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all unfilled currently open orders + + https://github.com/P2B-team/p2b-api-docs/blob/master/api-doc.md#open-orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + + EXCHANGE SPECIFIC PARAMETERS + :param int [params.offset]: 0-10000, default=0 + :returns Order[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOpenOrders() requires the symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + if limit is not None: + request['limit'] = limit + response = await self.privatePostOrders(self.extend(request, params)) + # + # { + # "success": True, + # "errorCode": "", + # "message": "", + # "result": [ + # { + # "orderId": 171913325964, + # "market": "ETH_BTC", + # "price": "0.06534", + # "side": "sell", + # "type": "limit", + # "timestamp": 1698487986.836821, + # "dealMoney": "0", + # "dealStock": "0", + # "amount": "0.0018", + # "takerFee": "0.0018", + # "makerFee": "0.0016", + # "left": "0.0018", + # "dealFee": "0" + # }, + # ... + # ] + # } + # + result = self.safe_list(response, 'result', []) + return self.parse_orders(result, market, since, limit) + + async def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all the trades made from a single order + + https://github.com/P2B-team/p2b-api-docs/blob/master/api-doc.md#deals-by-order-id + + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: 1-100, default=50 + :param dict [params]: extra parameters specific to the exchange API endpoint + + EXCHANGE SPECIFIC PARAMETERS + :param int [params.offset]: 0-10000, default=0 + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.safe_market(symbol) + request: dict = { + 'orderId': id, + } + if limit is not None: + request['limit'] = limit + response = await self.privatePostAccountOrder(self.extend(request, params)) + # + # { + # "success": True, + # "errorCode": "", + # "message": "", + # "result": { + # "offset": 0, + # "limit": 50, + # "records": [ + # { + # "id": 7429883128, # Deal id + # "time": 1698237535.41196, # Deal execution time + # "fee": "0.01755848704", # Deal fee + # "price": "34293.92", # Deal price + # "amount": "0.00032", # Deal amount + # "dealOrderId": 171366551416, # Deal order id + # "role": 1, # Deal role(1 - maker, 2 - taker) + # "deal": "10.9740544" # Total(price * amount) + # } + # ] + # } + # } + # + result = self.safe_value(response, 'result', {}) + records = self.safe_list(result, 'records', []) + return self.parse_trades(records, market, since, limit) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user, only the transaction records in the past 3 month can be queried, the time between since and params["until"] cannot be longer than 24 hours + + https://github.com/P2B-team/p2b-api-docs/blob/master/api-doc.md#deals-history-by-market + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for, default = params["until"] - 86400000 + :param int [limit]: 1-100, default=50 + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch orders for, default = current timestamp or since + 86400000 + + EXCHANGE SPECIFIC PARAMETERS + :param int [params.offset]: 0-10000, default=0 + :returns Trade[]: a list of `trade structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMyTrades() requires a symbol argument') + await self.load_markets() + until = self.safe_integer(params, 'until') + params = self.omit(params, 'until') + if until is None: + if since is None: + until = self.milliseconds() + else: + until = since + 86400000 + if since is None: + since = until - 86400000 + if (until - since) > 86400000: + raise BadRequest(self.id + ' fetchMyTrades() the time between since and params["until"] cannot be greater than 24 hours') + market = self.market(symbol) + request: dict = { + 'market': market['id'], + 'startTime': self.parse_to_int(since / 1000), + 'endTime': self.parse_to_int(until / 1000), + } + if limit is not None: + request['limit'] = limit + response = await self.privatePostAccountMarketDealHistory(self.extend(request, params)) + # + # { + # "success": True, + # "errorCode": "", + # "message": "", + # "result": { + # "total": 2, # Total records in the queried range + # "deals": [ + # { + # "deal_id": 7450617292, # Deal id + # "deal_time": 1698506956.66224, # Deal execution time + # "deal_order_id": 171955225751, # Deal order id + # "opposite_order_id": 171955110512, # Opposite order id + # "side": "sell", # Deal side + # "price": "0.05231", # Deal price + # "amount": "0.002", # Deal amount + # "deal": "0.00010462", # Total(price * amount) + # "deal_fee": "0.000000188316", # Deal fee + # "role": "taker", # Role. Taker or maker + # "isSelfTrade": False # is self trade + # }, + # ... + # ] + # } + # } + # + result = self.safe_value(response, 'result', {}) + deals = self.safe_list(result, 'deals', []) + return self.parse_trades(deals, market, since, limit) + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user, the time between since and params["untnil"] cannot be longer than 24 hours + + https://github.com/P2B-team/p2b-api-docs/blob/master/api-doc.md#orders-history-by-market + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for, default = params["until"] - 86400000 + :param int [limit]: 1-100, default=50 + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch orders for, default = current timestamp or since + 86400000 + + EXCHANGE SPECIFIC PARAMETERS + :param int [params.offset]: 0-10000, default=0 + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + until = self.safe_integer(params, 'until') + params = self.omit(params, 'until') + market: Market = None + if symbol is not None: + market = self.market(symbol) + if until is None: + if since is None: + until = self.milliseconds() + else: + until = since + 86400000 + if since is None: + since = until - 86400000 + if (until - since) > 86400000: + raise BadRequest(self.id + ' fetchClosedOrders() the time between since and params["until"] cannot be greater than 24 hours') + request: dict = { + 'startTime': self.parse_to_int(since / 1000), + 'endTime': self.parse_to_int(until / 1000), + } + if market is not None: + request['market'] = market['id'] + if limit is not None: + request['limit'] = limit + response = await self.privatePostAccountOrderHistory(self.extend(request, params)) + # + # { + # "success": True, + # "errorCode": "", + # "message": "", + # "result": { + # "LTC_USDT": [ + # { + # "id": 173985944395, + # "amount": "0.1", + # "price": "73", + # "type": "limit", + # "side": "sell", + # "ctime": 1699436194.390845, + # "ftime": 1699436194.390847, + # "market": "LTC_USDT", + # "takerFee": "0.002", + # "makerFee": "0.002", + # "dealFee": "0.01474", + # "dealStock": "0.1", + # "dealMoney": "7.37" + # } + # ] + # } + # } + # + result = self.safe_value(response, 'result') + orders = [] + keys = list(result.keys()) + for i in range(0, len(keys)): + marketId = keys[i] + marketOrders = result[marketId] + parsedOrders = self.parse_orders(marketOrders, market, since, limit) + orders = self.array_concat(orders, parsedOrders) + return orders + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # cancelOrder, fetchOpenOrders, createOrder + # + # { + # "orderId": 171906478744, + # "market": "ETH_BTC", + # "price": "0.04348", + # "side": "buy", + # "type": "limit", + # "timestamp": 1698484861.746517, + # "dealMoney": "0", + # "dealStock": "0", + # "amount": "0.0277", + # "takerFee": "0.002", + # "makerFee": "0.002", + # "left": "0.0277", + # "dealFee": "0" + # } + # + # fetchClosedOrders + # + # { + # "id": 171366547790, # Order id + # "amount": "0.00032", # Original amount + # "price": "34293.92", # Order price + # "type": "limit", # Order type + # "side": "sell", # Order side + # "ctime": 1698237533.497241, # Order creation time + # "ftime": 1698237535.41196, # Order fill time + # "market": "BTC_USDT", # Market name + # "takerFee": "0.0018", # Taker fee + # "makerFee": "0.0016", # Market fee + # "dealFee": "0.01755848704", # Deal fee + # "dealStock": "0.00032", # Filled amount + # "dealMoney": "10.9740544" # Filled total + # } + # + timestamp = self.safe_integer_product_2(order, 'timestamp', 'ctime', 1000) + marketId = self.safe_string(order, 'market') + market = self.safe_market(marketId, market) + return self.safe_order({ + 'info': order, + 'id': self.safe_string_2(order, 'id', 'orderId'), + 'clientOrderId': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'symbol': market['symbol'], + 'type': self.safe_string(order, 'type'), + 'timeInForce': None, + 'postOnly': None, + 'side': self.safe_string(order, 'side'), + 'price': self.safe_string(order, 'price'), + 'triggerPrice': None, + 'amount': self.safe_string(order, 'amount'), + 'cost': None, + 'average': None, + 'filled': self.safe_string(order, 'dealStock'), + 'remaining': self.safe_string(order, 'left'), + 'status': None, + 'fee': { + 'currency': market['quote'], + 'cost': self.safe_string(order, 'dealFee'), + }, + 'trades': None, + }, market) + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = self.urls['api'][api] + '/' + self.implode_params(path, params) + params = self.omit(params, self.extract_params(path)) + if method == 'GET': + if params: + url += '?' + self.urlencode(params) + if api == 'private': + params['request'] = '/api/v2/' + path + params['nonce'] = str(self.nonce()) + payload = self.string_to_base64(self.json(params)) # Body json encoded in base64 + headers = { + 'Content-Type': 'application/json', + 'X-TXC-APIKEY': self.apiKey, + 'X-TXC-PAYLOAD': payload, + 'X-TXC-SIGNATURE': self.hmac(self.encode(payload), self.encode(self.secret), hashlib.sha512), + } + body = self.json(params) + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None + if code == 400: + error = self.safe_value(response, 'error') + errorCode = self.safe_string(error, 'code') + feedback = self.id + ' ' + self.json(response) + self.throw_exactly_matched_exception(self.exceptions, errorCode, feedback) + # fallback to default error handler + return None diff --git a/ccxt/async_support/paradex.py b/ccxt/async_support/paradex.py new file mode 100644 index 0000000..3b28a4b --- /dev/null +++ b/ccxt/async_support/paradex.py @@ -0,0 +1,2528 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.paradex import ImplicitAPI +from ccxt.base.types import Any, Balances, Currency, Greeks, Int, Leverage, MarginMode, Market, Num, Order, OrderBook, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, Trade, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import OperationRejected +from ccxt.base.errors import InvalidOrder +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class paradex(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(paradex, self).describe(), { + 'id': 'paradex', + 'name': 'Paradex', + 'countries': [], + 'version': 'v1', + 'rateLimit': 50, + 'certified': False, + 'pro': True, + 'dex': True, + 'has': { + 'CORS': None, + 'spot': False, + 'margin': False, + 'swap': True, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'cancelAllOrders': True, + 'cancelAllOrdersAfter': False, + 'cancelOrder': False, + 'cancelOrders': False, + 'cancelOrdersForSymbols': False, + 'closeAllPositions': False, + 'closePosition': False, + 'createMarketBuyOrderWithCost': False, + 'createMarketOrderWithCost': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createOrders': False, + 'createReduceOnlyOrder': False, + 'createStopOrder': True, + 'createTriggerOrder': True, + 'editOrder': False, + 'fetchAccounts': False, + 'fetchAllGreeks': True, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchCanceledOrders': False, + 'fetchClosedOrders': False, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': False, + 'fetchDepositAddress': False, + 'fetchDepositAddresses': False, + 'fetchDeposits': True, + 'fetchDepositWithdrawFee': False, + 'fetchDepositWithdrawFees': False, + 'fetchFundingHistory': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': True, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchLedger': False, + 'fetchLeverage': True, + 'fetchLeverageTiers': False, + 'fetchLiquidations': True, + 'fetchMarginMode': True, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMyLiquidations': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': True, + 'fetchOpenInterestHistory': False, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchOrderTrades': False, + 'fetchPosition': True, + 'fetchPositionMode': False, + 'fetchPositions': True, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchStatus': True, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': False, + 'fetchTransfer': False, + 'fetchTransfers': False, + 'fetchWithdrawal': False, + 'fetchWithdrawals': True, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'sandbox': True, + 'setLeverage': True, + 'setMarginMode': True, + 'setPositionMode': False, + 'transfer': False, + 'withdraw': False, + }, + 'timeframes': { + '1m': 1, + '3m': 3, + '5m': 5, + '15m': 15, + '30m': 30, + '1h': 60, + }, + 'hostname': 'paradex.trade', + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/84628770-784e-4ec4-a759-ec2fbb2244ea', + 'api': { + 'v1': 'https://api.prod.{hostname}/v1', + }, + 'test': { + 'v1': 'https://api.testnet.{hostname}/v1', + }, + 'www': 'https://www.paradex.trade/', + 'doc': 'https://docs.api.testnet.paradex.trade/', + 'fees': 'https://docs.paradex.trade/getting-started/trading-fees', + 'referral': 'https://app.paradex.trade/r/ccxt24', + }, + 'api': { + 'public': { + 'get': { + 'bbo/{market}': 1, + 'funding/data': 1, + 'markets': 1, + 'markets/klines': 1, + 'markets/summary': 1, + 'orderbook/{market}': 1, + 'insurance': 1, + 'referrals/config': 1, + 'system/config': 1, + 'system/state': 1, + 'system/time': 1, + 'trades': 1, + 'vaults': 1, + 'vaults/balance': 1, + 'vaults/config': 1, + 'vaults/history': 1, + 'vaults/positions': 1, + 'vaults/summary': 1, + 'vaults/transfers': 1, + }, + }, + 'private': { + 'get': { + 'account': 1, + 'account/info': 1, + 'account/history': 1, + 'account/margin': 1, + 'account/profile': 1, + 'account/subaccounts': 1, + 'balance': 1, + 'fills': 1, + 'funding/payments': 1, + 'positions': 1, + 'tradebusts': 1, + 'transactions': 1, + 'liquidations': 1, + 'orders': 1, + 'orders-history': 1, + 'orders/by_client_id/{client_id}': 1, + 'orders/{order_id}': 1, + 'points_data/{market}/{program}': 1, + 'referrals/qr-code': 1, + 'referrals/summary': 1, + 'transfers': 1, + 'algo/orders': 1, + 'algo/orders-history': 1, + 'algo/orders/{algo_id}': 1, + 'vaults/account-summary': 1, + }, + 'post': { + 'account/margin/{market}': 1, + 'account/profile/max_slippage': 1, + 'account/profile/referral_code': 1, + 'account/profile/username': 1, + 'auth': 1, + 'onboarding': 1, + 'orders': 1, + 'orders/batch': 1, + 'algo/orders': 1, + 'vaults': 1, + }, + 'put': { + 'orders/{order_id}': 1, + }, + 'delete': { + 'orders': 1, + 'orders/by_client_id/{client_id}': 1, + 'orders/{order_id}': 1, + 'algo/orders/{algo_id}': 1, + }, + }, + }, + 'fees': { + 'swap': { + 'taker': self.parse_number('0.0002'), + 'maker': self.parse_number('0.0002'), + }, + 'spot': { + 'taker': self.parse_number('0.0002'), + 'maker': self.parse_number('0.0002'), + }, + }, + 'requiredCredentials': { + 'apiKey': False, + 'secret': False, + 'walletAddress': True, + 'privateKey': True, + }, + 'exceptions': { + 'exact': { + 'VALIDATION_ERROR': AuthenticationError, + 'BINDING_ERROR': OperationRejected, + 'INTERNAL_ERROR': ExchangeError, + 'NOT_FOUND': BadRequest, + 'SERVICE_UNAVAILABLE': ExchangeError, + 'INVALID_REQUEST_PARAMETER': BadRequest, + 'ORDER_ID_NOT_FOUND': InvalidOrder, + 'ORDER_IS_CLOSED': InvalidOrder, + 'ORDER_IS_NOT_OPEN_YET': InvalidOrder, + 'CLIENT_ORDER_ID_NOT_FOUND': InvalidOrder, + 'DUPLICATED_CLIENT_ID': InvalidOrder, + 'INVALID_PRICE_PRECISION': OperationRejected, + 'INVALID_SYMBOL': OperationRejected, + 'INVALID_TOKEN': OperationRejected, + 'INVALID_ETHEREUM_ADDRESS': OperationRejected, + 'INVALID_ETHEREUM_SIGNATURE': OperationRejected, + 'INVALID_STARKNET_ADDRESS': OperationRejected, + 'INVALID_STARKNET_SIGNATURE': OperationRejected, + 'STARKNET_SIGNATURE_VERIFICATION_FAILED': AuthenticationError, + 'BAD_STARKNET_REQUEST': BadRequest, + 'ETHEREUM_SIGNER_MISMATCH': BadRequest, + 'ETHEREUM_HASH_MISMATCH': BadRequest, + 'NOT_ONBOARDED': BadRequest, + 'INVALID_TIMESTAMP': BadRequest, + 'INVALID_SIGNATURE_EXPIRATION': AuthenticationError, + 'ACCOUNT_NOT_FOUND': AuthenticationError, + 'INVALID_ORDER_SIGNATURE': AuthenticationError, + 'PUBLIC_KEY_INVALID': BadRequest, + 'UNAUTHORIZED_ETHEREUM_ADDRESS': BadRequest, + 'ETHEREUM_ADDRESS_ALREADY_ONBOARDED': BadRequest, + 'MARKET_NOT_FOUND': BadRequest, + 'ALLOWLIST_ENTRY_NOT_FOUND': BadRequest, + 'USERNAME_IN_USE': AuthenticationError, + 'GEO_IP_BLOCK': PermissionDenied, + 'ETHEREUM_ADDRESS_BLOCKED': PermissionDenied, + 'PROGRAM_NOT_FOUND': BadRequest, + 'INVALID_DASHBOARD': OperationRejected, + 'MARKET_NOT_OPEN': BadRequest, + 'INVALID_REFERRAL_CODE': OperationRejected, + 'PARENT_ADDRESS_ALREADY_ONBOARDED': BadRequest, + 'INVALID_PARENT_ACCOUNT': OperationRejected, + 'INVALID_VAULT_OPERATOR_CHAIN': OperationRejected, + 'VAULT_OPERATOR_ALREADY_ONBOARDED': OperationRejected, + 'VAULT_NAME_IN_USE': OperationRejected, + 'BATCH_SIZE_OUT_OF_RANGE': OperationRejected, + 'ISOLATED_MARKET_ACCOUNT_MISMATCH': OperationRejected, + 'POINTS_SUMMARY_NOT_FOUND': OperationRejected, + '-32700': BadRequest, # Parse error + '-32600': BadRequest, # Invalid request + '-32601': BadRequest, # Method not found + '-32602': BadRequest, # Invalid parameterss + '-32603': ExchangeError, # Internal error + '100': BadRequest, # Method error + '40110': AuthenticationError, # Malformed Bearer Token + '40111': AuthenticationError, # Invalid Bearer Token + '40112': PermissionDenied, # Geo IP blocked + }, + 'broad': { + 'missing or malformed jwt': AuthenticationError, + }, + }, + 'precisionMode': TICK_SIZE, + 'commonCurrencies': { + }, + 'options': { + 'paradexAccount': None, # add {"privateKey": "copy Paradex Private Key from UI", "publicKey": "used when onboard(optional)", "address": "copy Paradex Address from UI"} + 'broker': 'CCXT', + }, + 'features': { + 'spot': None, + 'forSwap': { + 'sandbox': True, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerDirection': True, # todo + 'triggerPriceType': None, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': False, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': False, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': True, # todo + 'iceberg': False, + }, + 'createOrders': None, # todo + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, # todo + 'daysBack': 100000, # todo + 'untilDays': 100000, # todo + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 100, # todo + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 100000, # todo + 'untilDays': 100000, # todo + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchClosedOrders': None, # todo + 'fetchOHLCV': { + 'limit': None, # todo by from/to + }, + }, + 'swap': { + 'linear': { + 'extends': 'forSwap', + }, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + }) + + async def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://docs.api.testnet.paradex.trade/#get-system-time-unix-milliseconds + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = await self.publicGetSystemTime(params) + # + # { + # "server_time": "1681493415023" + # } + # + return self.safe_integer(response, 'server_time') + + async def fetch_status(self, params={}): + """ + the latest known information on the availability of the exchange API + + https://docs.api.testnet.paradex.trade/#get-system-state + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `status structure ` + """ + response = await self.publicGetSystemState(params) + # + # { + # "status": "ok" + # } + # + status = self.safe_string(response, 'status') + return { + 'status': 'ok' if (status == 'ok') else 'maintenance', + 'updated': None, + 'eta': None, + 'url': None, + 'info': response, + } + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for bitget + + https://docs.api.testnet.paradex.trade/#list-available-markets + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = await self.publicGetMarkets(params) + # + # { + # "results": [ + # { + # "symbol": "BODEN-USD-PERP", + # "base_currency": "BODEN", + # "quote_currency": "USD", + # "settlement_currency": "USDC", + # "order_size_increment": "1", + # "price_tick_size": "0.00001", + # "min_notional": "200", + # "open_at": 1717065600000, + # "expiry_at": 0, + # "asset_kind": "PERP", + # "position_limit": "2000000", + # "price_bands_width": "0.2", + # "max_open_orders": 50, + # "max_funding_rate": "0.05", + # "delta1_cross_margin_params": { + # "imf_base": "0.2", + # "imf_shift": "180000", + # "imf_factor": "0.00071", + # "mmf_factor": "0.5" + # }, + # "price_feed_id": "9LScEHse1ioZt2rUuhwiN6bmYnqpMqvZkQJDNUpxVHN5", + # "oracle_ewma_factor": "0.14999987905913592", + # "max_order_size": "520000", + # "max_funding_rate_change": "0.0005", + # "max_tob_spread": "0.2" + # } + # ] + # } + # + data = self.safe_list(response, 'results') + return self.parse_markets(data) + + def parse_market(self, market: dict) -> Market: + # + # { + # "symbol": "BODEN-USD-PERP", + # "base_currency": "BODEN", + # "quote_currency": "USD", + # "settlement_currency": "USDC", + # "order_size_increment": "1", + # "price_tick_size": "0.00001", + # "min_notional": "200", + # "open_at": 1717065600000, + # "expiry_at": 0, + # "asset_kind": "PERP", + # "position_limit": "2000000", + # "price_bands_width": "0.2", + # "max_open_orders": 50, + # "max_funding_rate": "0.05", + # "delta1_cross_margin_params": { + # "imf_base": "0.2", + # "imf_shift": "180000", + # "imf_factor": "0.00071", + # "mmf_factor": "0.5" + # }, + # "price_feed_id": "9LScEHse1ioZt2rUuhwiN6bmYnqpMqvZkQJDNUpxVHN5", + # "oracle_ewma_factor": "0.14999987905913592", + # "max_order_size": "520000", + # "max_funding_rate_change": "0.0005", + # "max_tob_spread": "0.2" + # } + # + # { + # "symbol":"BTC-USD-96000-C", + # "base_currency":"BTC", + # "quote_currency":"USD", + # "settlement_currency":"USDC", + # "order_size_increment":"0.001", + # "price_tick_size":"0.01", + # "min_notional":"100", + # "open_at":"1736764200000", + # "expiry_at":"0", + # "asset_kind":"PERP_OPTION", + # "market_kind":"cross", + # "position_limit":"10", + # "price_bands_width":"0.05", + # "iv_bands_width":"0.05", + # "max_open_orders":"100", + # "max_funding_rate":"0.02", + # "option_cross_margin_params":{ + # "imf":{ + # "long_itm":"0.2", + # "short_itm":"0.15", + # "short_otm":"0.1", + # "short_put_cap":"0.5", + # "premium_multiplier":"1" + # }, + # "mmf":{ + # "long_itm":"0.1", + # "short_itm":"0.075", + # "short_otm":"0.05", + # "short_put_cap":"0.5", + # "premium_multiplier":"0.5" + # } + # }, + # "price_feed_id":"GVXRSBjFk6e6J3NbVPXohDJetcTjaeeuykUpbQF8UoMU", + # "oracle_ewma_factor":"0.20000046249626113", + # "max_order_size":"2", + # "max_funding_rate_change":"0.02", + # "max_tob_spread":"0.2", + # "interest_rate":"0.0001", + # "clamp_rate":"0.02", + # "option_type":"CALL", + # "strike_price":"96000", + # "funding_period_hours":"24", + # "tags":[ + # ] + # } + # + assetKind = self.safe_string(market, 'asset_kind') + isOption = (assetKind == 'PERP_OPTION') + type = 'option' if (isOption) else 'swap' + isSwap = (type == 'swap') + marketId = self.safe_string(market, 'symbol') + quoteId = self.safe_string(market, 'quote_currency') + baseId = self.safe_string(market, 'base_currency') + quote = self.safe_currency_code(quoteId) + base = self.safe_currency_code(baseId) + settleId = self.safe_string(market, 'settlement_currency') + settle = self.safe_currency_code(settleId) + symbol = base + '/' + quote + ':' + settle + expiry = self.safe_integer(market, 'expiry_at') + optionType = self.safe_string(market, 'option_type') + strikePrice = self.safe_string(market, 'strike_price') + takerFee = self.parse_number('0.0003') + makerFee = self.parse_number('-0.00005') + if isOption: + optionTypeSuffix = 'C' if (optionType == 'CALL') else 'P' + symbol = symbol + '-' + strikePrice + '-' + optionTypeSuffix + makerFee = self.parse_number('0.0003') + else: + expiry = None + return self.safe_market_structure({ + 'id': marketId, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': type, + 'spot': False, + 'margin': None, + 'swap': isSwap, + 'future': False, + 'option': isOption, + 'active': self.safe_bool(market, 'enableTrading'), + 'contract': True, + 'linear': True, + 'inverse': False, + 'taker': takerFee, + 'maker': makerFee, + 'contractSize': self.parse_number('1'), + 'expiry': expiry, + 'expiryDatetime': None if (expiry == 0) else self.iso8601(expiry), + 'strike': self.parse_number(strikePrice), + 'optionType': self.safe_string_lower(market, 'option_type'), + 'precision': { + 'amount': self.safe_number(market, 'order_size_increment'), + 'price': self.safe_number(market, 'price_tick_size'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': None, + 'max': self.safe_number(market, 'max_order_size'), + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': self.safe_number(market, 'min_notional'), + 'max': None, + }, + }, + 'created': None, + 'info': market, + }) + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://docs.api.testnet.paradex.trade/#ohlcv-for-a-symbol + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest candle to fetch + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'resolution': self.safe_string(self.timeframes, timeframe, timeframe), + 'symbol': market['id'], + } + now = self.milliseconds() + duration = self.parse_timeframe(timeframe) + until = self.safe_integer_2(params, 'until', 'till', now) + params = self.omit(params, ['until', 'till']) + if since is not None: + request['start_at'] = since + if limit is not None: + request['end_at'] = self.sum(since, duration * (limit + 1) * 1000) - 1 + else: + request['end_at'] = until + else: + request['end_at'] = until + if limit is not None: + request['start_at'] = until - duration * (limit + 1) * 1000 + 1 + else: + request['start_at'] = until - duration * 101 * 1000 + 1 + response = await self.publicGetMarketsKlines(self.extend(request, params)) + # + # { + # "results": [ + # [ + # 1720071900000, + # 58961.3, + # 58961.3, + # 58961.3, + # 58961.3, + # 1591 + # ] + # ] + # } + # + data = self.safe_list(response, 'results', []) + return self.parse_ohlcvs(data, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # [ + # 1720071900000, + # 58961.3, + # 58961.3, + # 58961.3, + # 58961.3, + # 1591 + # ] + # + return [ + self.safe_integer(ohlcv, 0), + self.safe_number(ohlcv, 1), + self.safe_number(ohlcv, 2), + self.safe_number(ohlcv, 3), + self.safe_number(ohlcv, 4), + self.safe_number(ohlcv, 5), + ] + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://docs.api.testnet.paradex.trade/#list-available-markets-summary + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + request: dict = { + 'market': 'ALL', + } + response = await self.publicGetMarketsSummary(self.extend(request, params)) + # + # { + # "results": [ + # { + # "symbol": "BTC-USD-PERP", + # "oracle_price": "68465.17449906", + # "mark_price": "68465.17449906", + # "last_traded_price": "68495.1", + # "bid": "68477.6", + # "ask": "69578.2", + # "volume_24h": "5815541.397939004", + # "total_volume": "584031465.525259686", + # "created_at": 1718170156580, + # "underlying_price": "67367.37268422", + # "open_interest": "162.272", + # "funding_rate": "0.01629574927887", + # "price_change_rate_24h": "0.009032" + # } + # ] + # } + # + data = self.safe_list(response, 'results', []) + return self.parse_tickers(data, symbols) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://docs.api.testnet.paradex.trade/#list-available-markets-summary + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + response = await self.publicGetMarketsSummary(self.extend(request, params)) + # + # { + # "results": [ + # { + # "symbol": "BTC-USD-PERP", + # "oracle_price": "68465.17449906", + # "mark_price": "68465.17449906", + # "last_traded_price": "68495.1", + # "bid": "68477.6", + # "ask": "69578.2", + # "volume_24h": "5815541.397939004", + # "total_volume": "584031465.525259686", + # "created_at": 1718170156580, + # "underlying_price": "67367.37268422", + # "open_interest": "162.272", + # "funding_rate": "0.01629574927887", + # "price_change_rate_24h": "0.009032" + # } + # ] + # } + # + data = self.safe_list(response, 'results', []) + ticker = self.safe_dict(data, 0, {}) + return self.parse_ticker(ticker, market) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "symbol": "BTC-USD-PERP", + # "oracle_price": "68465.17449906", + # "mark_price": "68465.17449906", + # "last_traded_price": "68495.1", + # "bid": "68477.6", + # "ask": "69578.2", + # "volume_24h": "5815541.397939004", + # "total_volume": "584031465.525259686", + # "created_at": 1718170156581, + # "underlying_price": "67367.37268422", + # "open_interest": "162.272", + # "funding_rate": "0.01629574927887", + # "price_change_rate_24h": "0.009032" + # } + # + percentage = self.safe_string(ticker, 'price_change_rate_24h') + if percentage is not None: + percentage = Precise.string_mul(percentage, '100') + last = self.safe_string(ticker, 'last_traded_price') + marketId = self.safe_string(ticker, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + timestamp = self.safe_integer(ticker, 'created_at') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': None, + 'low': None, + 'bid': self.safe_string(ticker, 'bid'), + 'bidVolume': None, + 'ask': self.safe_string(ticker, 'ask'), + 'askVolume': None, + 'vwap': None, + 'open': None, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': percentage, + 'average': None, + 'baseVolume': None, + 'quoteVolume': self.safe_string(ticker, 'volume_24h'), + 'markPrice': self.safe_string(ticker, 'mark_price'), + 'info': ticker, + }, market) + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://docs.api.testnet.paradex.trade/#get-market-orderbook + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = {'market': market['id']} + response = await self.publicGetOrderbookMarket(self.extend(request, params)) + # + # { + # "market": "BTC-USD-PERP", + # "seq_no": 14115975, + # "last_updated_at": 1718172538340, + # "asks": [ + # [ + # "69578.2", + # "3.019" + # ] + # ], + # "bids": [ + # [ + # "68477.6", + # "0.1" + # ] + # ] + # } + # + if limit is not None: + request['depth'] = limit + timestamp = self.safe_integer(response, 'last_updated_at') + orderbook = self.parse_order_book(response, market['symbol'], timestamp) + orderbook['nonce'] = self.safe_integer(response, 'seq_no') + return orderbook + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://docs.api.testnet.paradex.trade/#trade-tape + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch trades for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchTrades', 'paginate') + if paginate: + return await self.fetch_paginated_call_cursor('fetchTrades', symbol, since, limit, params, 'next', 'cursor', None, 100) + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + if limit is not None: + request['page_size'] = limit + if since is not None: + request['start_at'] = since + request, params = self.handle_until_option('end_at', request, params) + response = await self.publicGetTrades(self.extend(request, params)) + # + # { + # "next": "...", + # "prev": "...", + # "results": [ + # { + # "id": "1718154353750201703989430001", + # "market": "BTC-USD-PERP", + # "side": "BUY", + # "size": "0.026", + # "price": "69578.2", + # "created_at": 1718154353750, + # "trade_type": "FILL" + # } + # ] + # } + # + trades = self.safe_list(response, 'results', []) + for i in range(0, len(trades)): + trades[i]['next'] = self.safe_string(response, 'next') + return self.parse_trades(trades, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(public) + # + # { + # "id": "1718154353750201703989430001", + # "market": "BTC-USD-PERP", + # "side": "BUY", + # "size": "0.026", + # "price": "69578.2", + # "created_at": 1718154353750, + # "trade_type": "FILL" + # } + # + # fetchMyTrades(private) + # + # { + # "id": "1718947571560201703986670001", + # "side": "BUY", + # "liquidity": "TAKER", + # "market": "BTC-USD-PERP", + # "order_id": "1718947571540201703992340000", + # "price": "64852.9", + # "size": "0.01", + # "fee": "0.1945587", + # "fee_currency": "USDC", + # "created_at": 1718947571569, + # "remaining_size": "0", + # "client_id": "", + # "fill_type": "FILL" + # } + # + marketId = self.safe_string(trade, 'market') + market = self.safe_market(marketId, market) + id = self.safe_string(trade, 'id') + timestamp = self.safe_integer(trade, 'created_at') + priceString = self.safe_string(trade, 'price') + amountString = self.safe_string(trade, 'size') + side = self.safe_string_lower(trade, 'side') + liability = self.safe_string_lower(trade, 'liquidity', 'taker') + isTaker = liability == 'taker' + takerOrMaker = 'taker' if (isTaker) else 'maker' + currencyId = self.safe_string(trade, 'fee_currency') + code = self.safe_currency_code(currencyId) + return self.safe_trade({ + 'info': trade, + 'id': id, + 'order': self.safe_string(trade, 'order_id'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'type': None, + 'takerOrMaker': takerOrMaker, + 'side': side, + 'price': priceString, + 'amount': amountString, + 'cost': None, + 'fee': { + 'cost': self.safe_string(trade, 'fee'), + 'currency': code, + 'rate': None, + }, + }, market) + + async def fetch_open_interest(self, symbol: str, params={}): + """ + retrieves the open interest of a contract trading pair + + https://docs.api.testnet.paradex.trade/#list-available-markets-summary + + :param str symbol: unified CCXT market symbol + :param dict [params]: exchange specific parameters + :returns dict} an open interest structure{@link https://docs.ccxt.com/#/?id=open-interest-structure: + """ + await self.load_markets() + market = self.market(symbol) + if not market['contract']: + raise BadRequest(self.id + ' fetchOpenInterest() supports contract markets only') + request: dict = { + 'market': market['id'], + } + response = await self.publicGetMarketsSummary(self.extend(request, params)) + # + # { + # "results": [ + # { + # "symbol": "BTC-USD-PERP", + # "oracle_price": "68465.17449906", + # "mark_price": "68465.17449906", + # "last_traded_price": "68495.1", + # "bid": "68477.6", + # "ask": "69578.2", + # "volume_24h": "5815541.397939004", + # "total_volume": "584031465.525259686", + # "created_at": 1718170156580, + # "underlying_price": "67367.37268422", + # "open_interest": "162.272", + # "funding_rate": "0.01629574927887", + # "price_change_rate_24h": "0.009032" + # } + # ] + # } + # + data = self.safe_list(response, 'results', []) + interest = self.safe_dict(data, 0, {}) + return self.parse_open_interest(interest, market) + + def parse_open_interest(self, interest, market: Market = None): + # + # { + # "symbol": "BTC-USD-PERP", + # "oracle_price": "68465.17449904", + # "mark_price": "68465.17449906", + # "last_traded_price": "68495.1", + # "bid": "68477.6", + # "ask": "69578.2", + # "volume_24h": "5815541.397939004", + # "total_volume": "584031465.525259686", + # "created_at": 1718170156580, + # "underlying_price": "67367.37268422", + # "open_interest": "162.272", + # "funding_rate": "0.01629574927887", + # "price_change_rate_24h": "0.009032" + # } + # + timestamp = self.safe_integer(interest, 'created_at') + marketId = self.safe_string(interest, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + return self.safe_open_interest({ + 'symbol': symbol, + 'openInterestAmount': self.safe_string(interest, 'open_interest'), + 'openInterestValue': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'info': interest, + }, market) + + def hash_message(self, message): + return '0x' + self.hash(message, 'keccak', 'hex') + + def sign_hash(self, hash, privateKey): + signature = self.ecdsa(hash[-64:], privateKey[-64:], 'secp256k1', None) + r = signature['r'] + s = signature['s'] + v = self.int_to_base16(self.sum(27, signature['v'])) + return '0x' + r.rjust(64, '0') + s.rjust(64, '0') + v + + def sign_message(self, message, privateKey): + return self.sign_hash(self.hash_message(message), privateKey[-64:]) + + async def get_system_config(self): + cachedConfig: dict = self.safe_dict(self.options, 'systemConfig') + if cachedConfig is not None: + return cachedConfig + response = await self.publicGetSystemConfig() + # + # { + # "starknet_gateway_url": "https://potc-testnet-sepolia.starknet.io", + # "starknet_fullnode_rpc_url": "https://pathfinder.api.testnet.paradex.trade/rpc/v0_7", + # "starknet_chain_id": "PRIVATE_SN_POTC_SEPOLIA", + # "block_explorer_url": "https://voyager.testnet.paradex.trade/", + # "paraclear_address": "0x286003f7c7bfc3f94e8f0af48b48302e7aee2fb13c23b141479ba00832ef2c6", + # "paraclear_decimals": 8, + # "paraclear_account_proxy_hash": "0x3530cc4759d78042f1b543bf797f5f3d647cde0388c33734cf91b7f7b9314a9", + # "paraclear_account_hash": "0x41cb0280ebadaa75f996d8d92c6f265f6d040bb3ba442e5f86a554f1765244e", + # "oracle_address": "0x2c6a867917ef858d6b193a0ff9e62b46d0dc760366920d631715d58baeaca1f", + # "bridged_tokens": [ + # { + # "name": "TEST USDC", + # "symbol": "USDC", + # "decimals": 6, + # "l1_token_address": "0x29A873159D5e14AcBd63913D4A7E2df04570c666", + # "l1_bridge_address": "0x8586e05adc0C35aa11609023d4Ae6075Cb813b4C", + # "l2_token_address": "0x6f373b346561036d98ea10fb3e60d2f459c872b1933b50b21fe6ef4fda3b75e", + # "l2_bridge_address": "0x46e9237f5408b5f899e72125dd69bd55485a287aaf24663d3ebe00d237fc7ef" + # } + # ], + # "l1_core_contract_address": "0x582CC5d9b509391232cd544cDF9da036e55833Af", + # "l1_operator_address": "0x11bACdFbBcd3Febe5e8CEAa75E0Ef6444d9B45FB", + # "l1_chain_id": "11155111", + # "liquidation_fee": "0.2" + # } + # + self.options['systemConfig'] = response + return response + + async def prepare_paradex_domain(self, l1=False): + systemConfig = await self.get_system_config() + if l1 is True: + l1D = { + 'name': 'Paradex', + 'chainId': systemConfig['l1_chain_id'], + 'version': '1', + } + return l1D + domain = { + 'name': 'Paradex', + 'chainId': systemConfig['starknet_chain_id'], + 'version': 1, + } + return domain + + async def retrieve_account(self): + cachedAccount: dict = self.safe_dict(self.options, 'paradexAccount') + if cachedAccount is not None: + return cachedAccount + self.check_required_credentials() + systemConfig = await self.get_system_config() + domain = await self.prepare_paradex_domain(True) + messageTypes = { + 'Constant': [ + {'name': 'action', 'type': 'string'}, + ], + } + message = { + 'action': 'STARK Key', + } + msg = self.eth_encode_structured_data(domain, messageTypes, message) + signature = self.sign_message(msg, self.privateKey) + account = self.retrieve_stark_account( + signature, + systemConfig['paraclear_account_hash'], + systemConfig['paraclear_account_proxy_hash'] + ) + self.options['paradexAccount'] = account + return account + + async def onboarding(self, params={}): + account = await self.retrieve_account() + req = { + 'action': 'Onboarding', + } + domain = await self.prepare_paradex_domain() + messageTypes = { + 'Constant': [ + {'name': 'action', 'type': 'felt'}, + ], + } + msg = self.starknet_encode_structured_data(domain, messageTypes, req, account['address']) + signature = self.starknet_sign(msg, account['privateKey']) + params['signature'] = signature + params['account'] = account['address'] + params['public_key'] = account['publicKey'] + response = await self.privatePostOnboarding(params) + return response + + async def authenticate_rest(self, params={}): + cachedToken = self.safe_string(self.options, 'authToken') + now = self.nonce() + if cachedToken is not None: + cachedExpires = self.safe_integer(self.options, 'expires') + if now < cachedExpires: + return cachedToken + account = await self.retrieve_account() + # https://docs.paradex.trade/api-reference/general-information/authentication + expires = now + 180 + req = { + 'method': 'POST', + 'path': '/v1/auth', + 'body': '', + 'timestamp': now, + 'expiration': expires, + } + domain = await self.prepare_paradex_domain() + messageTypes = { + 'Request': [ + {'name': 'method', 'type': 'felt'}, + {'name': 'path', 'type': 'felt'}, + {'name': 'body', 'type': 'felt'}, + {'name': 'timestamp', 'type': 'felt'}, + {'name': 'expiration', 'type': 'felt'}, + ], + } + msg = self.starknet_encode_structured_data(domain, messageTypes, req, account['address']) + signature = self.starknet_sign(msg, account['privateKey']) + params['signature'] = signature + params['account'] = account['address'] + params['timestamp'] = req['timestamp'] + params['expiration'] = req['expiration'] + response = await self.privatePostAuth(params) + # + # { + # jwt_token: "ooooccxtooootoooootheoooomoonooooo" + # } + # + token = self.safe_string(response, 'jwt_token') + self.options['authToken'] = token + self.options['expires'] = expires + return token + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # { + # "account": "0x4638e3041366aa71720be63e32e53e1223316c7f0d56f7aa617542ed1e7512x", + # "avg_fill_price": "26000", + # "client_id": "x1234", + # "cancel_reason": "NOT_ENOUGH_MARGIN", + # "created_at": 1681493746016, + # "flags": [ + # "REDUCE_ONLY" + # ], + # "id": "123456", + # "instruction": "GTC", + # "last_updated_at": 1681493746016, + # "market": "BTC-USD-PERP", + # "price": "26000", + # "published_at": 1681493746016, + # "received_at": 1681493746016, + # "remaining_size": "0", + # "seq_no": 1681471234972000000, + # "side": "BUY", + # "size": "0.05", + # "status": "NEW", + # "stp": "EXPIRE_MAKER", + # "timestamp": 1681493746016, + # "trigger_price": "26000", + # "type": "MARKET" + # } + # + timestamp = self.safe_integer(order, 'created_at') + orderId = self.safe_string(order, 'id') + clientOrderId = self.omit_zero(self.safe_string(order, 'client_id')) + marketId = self.safe_string(order, 'market') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + price = self.safe_string(order, 'price') + amount = self.safe_string(order, 'size') + orderType = self.safe_string(order, 'type') + cancelReason = self.safe_string(order, 'cancel_reason') + status = self.safe_string(order, 'status') + if cancelReason is not None: + if cancelReason == 'NOT_ENOUGH_MARGIN' or cancelReason == 'ORDER_EXCEEDS_POSITION_LIMIT': + status = 'rejected' + else: + status = 'canceled' + side = self.safe_string_lower(order, 'side') + average = self.omit_zero(self.safe_string(order, 'avg_fill_price')) + remaining = self.omit_zero(self.safe_string(order, 'remaining_size')) + lastUpdateTimestamp = self.safe_integer(order, 'last_updated_at') + flags = self.safe_list(order, 'flags', []) + reduceOnly = None + if 'REDUCE_ONLY' in flags: + reduceOnly = True + return self.safe_order({ + 'id': orderId, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'lastUpdateTimestamp': lastUpdateTimestamp, + 'status': self.parse_order_status(status), + 'symbol': symbol, + 'type': self.parse_order_type(orderType), + 'timeInForce': self.parse_time_in_force(self.safe_string(order, 'instruction')), + 'postOnly': None, + 'reduceOnly': reduceOnly, + 'side': side, + 'price': price, + 'triggerPrice': self.safe_string(order, 'trigger_price'), + 'takeProfitPrice': None, + 'stopLossPrice': None, + 'average': average, + 'amount': amount, + 'filled': None, + 'remaining': remaining, + 'cost': None, + 'trades': None, + 'fee': { + 'cost': None, + 'currency': None, + }, + 'info': order, + }, market) + + def parse_time_in_force(self, timeInForce: Str): + timeInForces: dict = { + 'IOC': 'IOC', + 'GTC': 'GTC', + 'POST_ONLY': 'PO', + } + return self.safe_string(timeInForces, timeInForce, None) + + def parse_order_status(self, status: Str): + if status is not None: + statuses: dict = { + 'NEW': 'open', + 'UNTRIGGERED': 'open', + 'OPEN': 'open', + 'CLOSED': 'closed', + } + return self.safe_string(statuses, status, status) + return status + + def parse_order_type(self, type: Str): + types: dict = { + 'LIMIT': 'limit', + 'MARKET': 'market', + 'STOP_LIMIT': 'limit', + 'STOP_MARKET': 'market', + } + return self.safe_string_lower(types, type, type) + + def convert_short_string(self, str: str): + # TODO: add stringToBase16 in exchange + return '0x' + self.binary_to_base16(self.base64_to_binary(self.string_to_base64(str))) + + def scale_number(self, num: str): + return Precise.string_mul(num, '100000000') + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://docs.api.prod.paradex.trade/#create-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fullfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.stopPrice]: alias for triggerPrice + :param float [params.triggerPrice]: The price a trigger order is triggered at + :param float [params.stopLossPrice]: the price that a stop loss order is triggered at + :param float [params.takeProfitPrice]: the price that a take profit order is triggered at + :param str [params.timeInForce]: "GTC", "IOC", or "POST_ONLY" + :param bool [params.postOnly]: True or False + :param bool [params.reduceOnly]: Ensures that the executed order does not flip the opened position. + :param str [params.clientOrderId]: a unique id for the order + :returns dict: an `order structure ` + """ + await self.authenticate_rest() + await self.load_markets() + market = self.market(symbol) + reduceOnly = self.safe_bool_2(params, 'reduceOnly', 'reduce_only') + orderType = type.upper() + orderSide = side.upper() + request: dict = { + 'market': market['id'], + 'side': orderSide, + 'type': orderType, # LIMIT/MARKET/STOP_LIMIT/STOP_MARKET,STOP_LOSS_MARKET,STOP_LOSS_LIMIT,TAKE_PROFIT_MARKET,TAKE_PROFIT_LIMIT + } + triggerPrice = self.safe_string_2(params, 'triggerPrice', 'stopPrice') + stopLossPrice = self.safe_string(params, 'stopLossPrice') + takeProfitPrice = self.safe_string(params, 'takeProfitPrice') + isMarket = orderType == 'MARKET' + isTakeProfitOrder = (takeProfitPrice is not None) + isStopLossOrder = (stopLossPrice is not None) + isStopOrder = (triggerPrice is not None) or isTakeProfitOrder or isStopLossOrder + timeInForce = self.safe_string_upper(params, 'timeInForce') + postOnly = self.is_post_only(isMarket, None, params) + if not isMarket: + if postOnly: + request['instruction'] = 'POST_ONLY' + elif timeInForce == 'ioc': + request['instruction'] = 'IOC' + if price is not None: + request['price'] = self.price_to_precision(symbol, price) + clientOrderId = self.safe_string_n(params, ['clOrdID', 'clientOrderId', 'client_order_id']) + if clientOrderId is not None: + request['client_id'] = clientOrderId + sizeString = '0' + stopPrice = None + if isStopOrder: + # flags: Reduce_Only must be provided for TPSL orders. + if isMarket: + if isStopLossOrder: + stopPrice = self.price_to_precision(symbol, stopLossPrice) + reduceOnly = True + request['type'] = 'STOP_LOSS_MARKET' + elif isTakeProfitOrder: + stopPrice = self.price_to_precision(symbol, takeProfitPrice) + reduceOnly = True + request['type'] = 'TAKE_PROFIT_MARKET' + else: + stopPrice = self.price_to_precision(symbol, triggerPrice) + sizeString = self.amount_to_precision(symbol, amount) + request['type'] = 'STOP_MARKET' + else: + if isStopLossOrder: + stopPrice = self.price_to_precision(symbol, stopLossPrice) + reduceOnly = True + request['type'] = 'STOP_LOSS_LIMIT' + elif isTakeProfitOrder: + stopPrice = self.price_to_precision(symbol, takeProfitPrice) + reduceOnly = True + request['type'] = 'TAKE_PROFIT_LIMIT' + else: + stopPrice = self.price_to_precision(symbol, triggerPrice) + sizeString = self.amount_to_precision(symbol, amount) + request['type'] = 'STOP_LIMIT' + else: + sizeString = self.amount_to_precision(symbol, amount) + if stopPrice is not None: + request['trigger_price'] = stopPrice + request['size'] = sizeString + if reduceOnly: + request['flags'] = [ + 'REDUCE_ONLY', + ] + params = self.omit(params, ['reduceOnly', 'reduce_only', 'clOrdID', 'clientOrderId', 'client_order_id', 'postOnly', 'timeInForce', 'stopPrice', 'triggerPrice', 'stopLossPrice', 'takeProfitPrice']) + account = await self.retrieve_account() + now = self.nonce() + orderReq = { + 'timestamp': now * 1000, + 'market': self.convert_short_string(request['market']), + 'side': '1' if (orderSide == 'BUY') else '2', + 'orderType': self.convert_short_string(request['type']), + 'size': self.scale_number(request['size']), + 'price': '0' if (isMarket) else self.scale_number(request['price']), + } + domain = await self.prepare_paradex_domain() + messageTypes = { + 'Order': [ + {'name': 'timestamp', 'type': 'felt'}, + {'name': 'market', 'type': 'felt'}, + {'name': 'side', 'type': 'felt'}, + {'name': 'orderType', 'type': 'felt'}, + {'name': 'size', 'type': 'felt'}, + {'name': 'price', 'type': 'felt'}, + ], + } + msg = self.starknet_encode_structured_data(domain, messageTypes, orderReq, account['address']) + signature = self.starknet_sign(msg, account['privateKey']) + request['signature'] = signature + request['signature_timestamp'] = orderReq['timestamp'] + response = await self.privatePostOrders(self.extend(request, params)) + # + # { + # "account": "0x4638e3041366aa71720be63e32e53e1223316c7f0d56f7aa617542ed1e7512x", + # "avg_fill_price": "26000", + # "cancel_reason": "NOT_ENOUGH_MARGIN", + # "client_id": "x1234", + # "created_at": 1681493746016, + # "flags": [ + # "REDUCE_ONLY" + # ], + # "id": "123456", + # "instruction": "GTC", + # "last_updated_at": 1681493746016, + # "market": "BTC-USD-PERP", + # "price": "26000", + # "published_at": 1681493746016, + # "received_at": 1681493746016, + # "remaining_size": "0", + # "seq_no": 1681471234972000000, + # "side": "BUY", + # "size": "0.05", + # "status": "NEW", + # "stp": "EXPIRE_MAKER", + # "timestamp": 1681493746016, + # "trigger_price": "26000", + # "type": "MARKET" + # } + # + order = self.parse_order(response, market) + return order + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://docs.api.prod.paradex.trade/#cancel-order + https://docs.api.prod.paradex.trade/#cancel-open-order-by-client-order-id + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.clientOrderId]: a unique id for the order + :returns dict: An `order structure ` + """ + await self.authenticate_rest() + await self.load_markets() + request: dict = {} + clientOrderId = self.safe_string_n(params, ['clOrdID', 'clientOrderId', 'client_order_id']) + response = None + if clientOrderId is not None: + request['client_id'] = clientOrderId + response = await self.privateDeleteOrdersByClientIdClientId(self.extend(request, params)) + else: + request['order_id'] = id + response = await self.privateDeleteOrdersOrderId(self.extend(request, params)) + # + # if success, no response... + # + return self.parse_order(response) + + async def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders in a market + + https://docs.api.prod.paradex.trade/#cancel-all-open-orders + + :param str symbol: unified market symbol of the market to cancel orders in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelAllOrders() requires a symbol argument') + await self.authenticate_rest() + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + response = await self.privateDeleteOrders(self.extend(request, params)) + # + # if success, no response... + # + return [self.safe_order({'info': response})] + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://docs.api.prod.paradex.trade/#get-order + https://docs.api.prod.paradex.trade/#get-order-by-client-id + + :param str id: the order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.clientOrderId]: a unique id for the order + :returns dict: An `order structure ` + """ + await self.authenticate_rest() + await self.load_markets() + request: dict = {} + clientOrderId = self.safe_string_n(params, ['clOrdID', 'clientOrderId', 'client_order_id']) + params = self.omit(params, ['clOrdID', 'clientOrderId', 'client_order_id']) + response = None + if clientOrderId is not None: + request['client_id'] = clientOrderId + response = await self.privateGetOrdersByClientIdClientId(self.extend(request, params)) + else: + request['order_id'] = id + response = await self.privateGetOrdersOrderId(self.extend(request, params)) + # + # { + # "id": "1718941725080201704028870000", + # "account": "0x49ddd7a564c978f6e4089ff8355b56a42b7e2d48ba282cb5aad60f04bea0ec3", + # "market": "BTC-USD-PERP", + # "side": "SELL", + # "type": "LIMIT", + # "size": "10.153", + # "remaining_size": "10.153", + # "price": "70784.5", + # "status": "CLOSED", + # "created_at": 1718941725082, + # "last_updated_at": 1718958002991, + # "timestamp": 1718941724678, + # "cancel_reason": "USER_CANCELED", + # "client_id": "", + # "seq_no": 1718958002991595738, + # "instruction": "GTC", + # "avg_fill_price": "", + # "stp": "EXPIRE_TAKER", + # "received_at": 1718958510959, + # "published_at": 1718958510960, + # "flags": [], + # "trigger_price": "0" + # } + # + return self.parse_order(response) + + async def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://docs.api.prod.paradex.trade/#get-orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.side]: 'buy' or 'sell' + :param boolean [params.paginate]: set to True if you want to fetch orders with pagination + :param int params['until']: timestamp in ms of the latest order to fetch + :returns Order[]: a list of `order structures ` + """ + await self.authenticate_rest() + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOrders', 'paginate') + if paginate: + return await self.fetch_paginated_call_cursor('fetchOrders', symbol, since, limit, params, 'next', 'cursor', None, 50) + request: dict = {} + market: Market = None + if symbol is not None: + market = self.market(symbol) + request['market'] = market['id'] + if since is not None: + request['start_at'] = since + if limit is not None: + request['page_size'] = limit + request, params = self.handle_until_option('end_at', request, params) + response = await self.privateGetOrdersHistory(self.extend(request, params)) + # + # { + # "next": "eyJmaWx0ZXIiMsIm1hcmtlciI6eyJtYXJrZXIiOiIxNjc1NjUwMDE3NDMxMTAxNjk5N=", + # "prev": "eyJmaWx0ZXIiOnsiTGltaXQiOjkwfSwidGltZSI6MTY4MTY3OTgzNzk3MTMwOTk1MywibWFya2VyIjp7Im1zMjExMD==", + # "results": [ + # { + # "account": "0x4638e3041366aa71720be63e32e53e1223316c7f0d56f7aa617542ed1e7512x", + # "avg_fill_price": "26000", + # "cancel_reason": "NOT_ENOUGH_MARGIN", + # "client_id": "x1234", + # "created_at": 1681493746016, + # "flags": [ + # "REDUCE_ONLY" + # ], + # "id": "123456", + # "instruction": "GTC", + # "last_updated_at": 1681493746016, + # "market": "BTC-USD-PERP", + # "price": "26000", + # "published_at": 1681493746016, + # "received_at": 1681493746016, + # "remaining_size": "0", + # "seq_no": 1681471234972000000, + # "side": "BUY", + # "size": "0.05", + # "status": "NEW", + # "stp": "EXPIRE_MAKER", + # "timestamp": 1681493746016, + # "trigger_price": "26000", + # "type": "MARKET" + # } + # ] + # } + # + orders = self.safe_list(response, 'results', []) + paginationCursor = self.safe_string(response, 'next') + ordersLength = len(orders) + if (paginationCursor is not None) and (ordersLength > 0): + first = orders[0] + first['next'] = paginationCursor + orders[0] = first + return self.parse_orders(orders, market, since, limit) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://docs.api.prod.paradex.trade/#paradex-rest-api-orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + await self.authenticate_rest() + await self.load_markets() + request: dict = {} + market: Market = None + if symbol is not None: + market = self.market(symbol) + request['market'] = market['id'] + response = await self.privateGetOrders(self.extend(request, params)) + # + # { + # "results": [ + # { + # "account": "0x4638e3041366aa71720be63e32e53e1223316c7f0d56f7aa617542ed1e7512x", + # "avg_fill_price": "26000", + # "client_id": "x1234", + # "cancel_reason": "NOT_ENOUGH_MARGIN", + # "created_at": 1681493746016, + # "flags": [ + # "REDUCE_ONLY" + # ], + # "id": "123456", + # "instruction": "GTC", + # "last_updated_at": 1681493746016, + # "market": "BTC-USD-PERP", + # "price": "26000", + # "published_at": 1681493746016, + # "received_at": 1681493746016, + # "remaining_size": "0", + # "seq_no": 1681471234972000000, + # "side": "BUY", + # "size": "0.05", + # "status": "NEW", + # "stp": "EXPIRE_MAKER", + # "timestamp": 1681493746016, + # "trigger_price": "26000", + # "type": "MARKET" + # } + # ] + # } + # + orders = self.safe_list(response, 'results', []) + return self.parse_orders(orders, market, since, limit) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://docs.api.prod.paradex.trade/#list-balances + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.authenticate_rest() + await self.load_markets() + response = await self.privateGetBalance() + # + # { + # "results": [ + # { + # "token": "USDC", + # "size": "99980.2382266290601", + # "last_updated_at": 1718529757240 + # } + # ] + # } + # + data = self.safe_list(response, 'results', []) + return self.parse_balance(data) + + def parse_balance(self, response) -> Balances: + result: dict = {'info': response} + for i in range(0, len(response)): + balance = self.safe_dict(response, i, {}) + currencyId = self.safe_string(balance, 'token') + code = self.safe_currency_code(currencyId) + account = self.account() + account['total'] = self.safe_string(balance, 'size') + result[code] = account + return self.safe_balance(result) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://docs.api.prod.paradex.trade/#list-fills + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param int [params.until]: the latest time in ms to fetch entries for + :returns Trade[]: a list of `trade structures ` + """ + await self.authenticate_rest() + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchMyTrades', 'paginate') + if paginate: + return await self.fetch_paginated_call_cursor('fetchMyTrades', symbol, since, limit, params, 'next', 'cursor', None, 100) + request: dict = {} + market: Market = None + if symbol is not None: + market = self.market(symbol) + request['market'] = market['id'] + if limit is not None: + request['page_size'] = limit + if since is not None: + request['start_at'] = since + request, params = self.handle_until_option('end_at', request, params) + response = await self.privateGetFills(self.extend(request, params)) + # + # { + # "next": null, + # "prev": null, + # "results": [ + # { + # "id": "1718947571560201703986670001", + # "side": "BUY", + # "liquidity": "TAKER", + # "market": "BTC-USD-PERP", + # "order_id": "1718947571540201703992340000", + # "price": "64852.9", + # "size": "0.01", + # "fee": "0.1945587", + # "fee_currency": "USDC", + # "created_at": 1718947571569, + # "remaining_size": "0", + # "client_id": "", + # "fill_type": "FILL" + # } + # ] + # } + # + trades = self.safe_list(response, 'results', []) + for i in range(0, len(trades)): + trades[i]['next'] = self.safe_string(response, 'next') + return self.parse_trades(trades, market, since, limit) + + async def fetch_position(self, symbol: str, params={}): + """ + fetch data on an open position + + https://docs.api.prod.paradex.trade/#list-open-positions + + :param str symbol: unified market symbol of the market the position is held in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `position structure ` + """ + await self.authenticate_rest() + await self.load_markets() + market = self.market(symbol) + positions = await self.fetch_positions([market['symbol']], params) + return self.safe_dict(positions, 0, {}) + + async def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://docs.api.prod.paradex.trade/#list-open-positions + + :param str[] [symbols]: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + await self.authenticate_rest() + await self.load_markets() + symbols = self.market_symbols(symbols) + response = await self.privateGetPositions() + # + # { + # "results": [ + # { + # "id": "0x49ddd7a564c978f6e4089ff8355b56a42b7e2d48ba282cb5aad60f04bea0ec3-BTC-USD-PERP", + # "market": "BTC-USD-PERP", + # "status": "OPEN", + # "side": "LONG", + # "size": "0.01", + # "average_entry_price": "64839.96053748", + # "average_entry_price_usd": "64852.9", + # "realized_pnl": "0", + # "unrealized_pnl": "-2.39677214", + # "unrealized_funding_pnl": "-0.11214013", + # "cost": "648.39960537", + # "cost_usd": "648.529", + # "cached_funding_index": "35202.1002351", + # "last_updated_at": 1718950074249, + # "last_fill_id": "1718947571560201703986670001", + # "seq_no": 1718950074249176253, + # "liquidation_price": "" + # } + # ] + # } + # + data = self.safe_list(response, 'results', []) + return self.parse_positions(data, symbols) + + def parse_position(self, position: dict, market: Market = None): + # + # { + # "id": "0x49ddd7a564c978f6e4089ff8355b56a42b7e2d48ba282cb5aad60f04bea0ec3-BTC-USD-PERP", + # "market": "BTC-USD-PERP", + # "status": "OPEN", + # "side": "LONG", + # "size": "0.01", + # "average_entry_price": "64839.96053748", + # "average_entry_price_usd": "64852.9", + # "realized_pnl": "0", + # "unrealized_pnl": "-2.39677214", + # "unrealized_funding_pnl": "-0.11214013", + # "cost": "648.39960537", + # "cost_usd": "648.529", + # "cached_funding_index": "35202.1002351", + # "last_updated_at": 1718950074249, + # "last_fill_id": "1718947571560201703986670001", + # "seq_no": 1718950074249176253, + # "liquidation_price": "" + # } + # + marketId = self.safe_string(position, 'market') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + side = self.safe_string_lower(position, 'side') + quantity = self.safe_string(position, 'size') + if side != 'long': + quantity = Precise.string_mul('-1', quantity) + timestamp = self.safe_integer(position, 'time') + return self.safe_position({ + 'info': position, + 'id': self.safe_string(position, 'id'), + 'symbol': symbol, + 'entryPrice': self.safe_string(position, 'average_entry_price'), + 'markPrice': None, + 'notional': None, + 'collateral': self.safe_string(position, 'cost'), + 'unrealizedPnl': self.safe_string(position, 'unrealized_pnl'), + 'side': side, + 'contracts': self.parse_number(quantity), + 'contractSize': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'hedged': None, + 'maintenanceMargin': None, + 'maintenanceMarginPercentage': None, + 'initialMargin': None, + 'initialMarginPercentage': None, + 'leverage': None, + 'liquidationPrice': None, + 'marginRatio': None, + 'marginMode': None, + 'percentage': None, + }) + + async def fetch_liquidations(self, symbol: str, since: Int = None, limit: Int = None, params={}): + """ + retrieves the public liquidations of a trading pair + + https://docs.api.prod.paradex.trade/#list-liquidations + + :param str symbol: unified CCXT market symbol + :param int [since]: the earliest time in ms to fetch liquidations for + :param int [limit]: the maximum number of liquidation structures to retrieve + :param dict [params]: exchange specific parameters for the huobi api endpoint + :param int [params.until]: timestamp in ms of the latest liquidation + :returns dict: an array of `liquidation structures ` + """ + await self.authenticate_rest() + request: dict = {} + if since is not None: + request['from'] = since + else: + request['from'] = 1 + market = self.market(symbol) + request, params = self.handle_until_option('to', request, params) + response = await self.privateGetLiquidations(self.extend(request, params)) + # + # { + # "results": [ + # { + # "created_at": 1697213130097, + # "id": "0x123456789" + # } + # ] + # } + # + data = self.safe_list(response, 'results', []) + return self.parse_liquidations(data, market, since, limit) + + def parse_liquidation(self, liquidation, market: Market = None): + # + # { + # "created_at": 1697213130097, + # "id": "0x123456789" + # } + # + timestamp = self.safe_integer(liquidation, 'created_at') + return self.safe_liquidation({ + 'info': liquidation, + 'symbol': None, + 'contracts': None, + 'contractSize': None, + 'price': None, + 'side': None, + 'baseValue': None, + 'quoteValue': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://docs.api.prod.paradex.trade/#paradex-rest-api-transfers + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch entries for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `transaction structures ` + """ + await self.authenticate_rest() + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchDeposits', 'paginate') + if paginate: + return await self.fetch_paginated_call_cursor('fetchDeposits', code, since, limit, params, 'next', 'cursor', None, 100) + request: dict = {} + if limit is not None: + request['page_size'] = limit + if since is not None: + request['start_at'] = since + request, params = self.handle_until_option('end_at', request, params) + response = await self.privateGetTransfers(self.extend(request, params)) + # + # { + # "next": null, + # "prev": null, + # "results": [ + # { + # "id": "1718940471200201703989430000", + # "account": "0x49ddd7a564c978f6e4089ff8355b56a42b7e2d48ba282cb5aad60f04bea0ec3", + # "kind": "DEPOSIT", + # "status": "COMPLETED", + # "amount": "100000", + # "token": "USDC", + # "created_at": 1718940471208, + # "last_updated_at": 1718941455546, + # "txn_hash": "0x73a415ca558a97bbdcd1c43e52b45f1e0486a0a84b3bb4958035ad6c59cb866", + # "external_txn_hash": "", + # "socialized_loss_factor": "" + # } + # ] + # } + # + rows = self.safe_list(response, 'results', []) + deposits = [] + for i in range(0, len(rows)): + row = rows[i] + if row['kind'] == 'DEPOSIT': + deposits.append(row) + return self.parse_transactions(deposits, None, since, limit) + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://docs.api.prod.paradex.trade/#paradex-rest-api-transfers + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch withdrawals for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `transaction structures ` + """ + await self.authenticate_rest() + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchWithdrawals', 'paginate') + if paginate: + return await self.fetch_paginated_call_cursor('fetchWithdrawals', code, since, limit, params, 'next', 'cursor', None, 100) + request: dict = {} + if limit is not None: + request['page_size'] = limit + if since is not None: + request['start_at'] = since + request, params = self.handle_until_option('end_at', request, params) + response = await self.privateGetTransfers(self.extend(request, params)) + # + # { + # "next": null, + # "prev": null, + # "results": [ + # { + # "id": "1718940471200201703989430000", + # "account": "0x49ddd7a564c978f6e4089ff8355b56a42b7e2d48ba282cb5aad60f04bea0ec3", + # "kind": "DEPOSIT", + # "status": "COMPLETED", + # "amount": "100000", + # "token": "USDC", + # "created_at": 1718940471208, + # "last_updated_at": 1718941455546, + # "txn_hash": "0x73a415ca558a97bbdcd1c43e52b45f1e0486a0a84b3bb4958035ad6c59cb866", + # "external_txn_hash": "", + # "socialized_loss_factor": "" + # } + # ] + # } + # + rows = self.safe_list(response, 'results', []) + deposits = [] + for i in range(0, len(rows)): + row = rows[i] + if row['kind'] == 'WITHDRAWAL': + deposits.append(row) + return self.parse_transactions(deposits, None, since, limit) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fetchDeposits & fetchWithdrawals + # + # { + # "id": "1718940471200201703989430000", + # "account": "0x49ddd7a564c978f6e4089ff8355b56a42b7e2d48ba282cb5aad60f04bea0ec3", + # "kind": "DEPOSIT", + # "status": "COMPLETED", + # "amount": "100000", + # "token": "USDC", + # "created_at": 1718940471208, + # "last_updated_at": 1718941455546, + # "txn_hash": "0x73a415ca558a97bbdcd1c43e52b45f1e0486a0a84b3bb4958035ad6c59cb866", + # "external_txn_hash": "", + # "socialized_loss_factor": "" + # } + # + id = self.safe_string(transaction, 'id') + address = self.safe_string(transaction, 'account') + txid = self.safe_string(transaction, 'txn_hash') + currencyId = self.safe_string(transaction, 'token') + code = self.safe_currency_code(currencyId, currency) + timestamp = self.safe_integer(transaction, 'created_at') + updated = self.safe_integer(transaction, 'last_updated_at') + type = self.safe_string(transaction, 'kind') + type = 'deposit' if (type == 'DEPOSIT') else 'withdrawal' + status = self.parse_transaction_status(self.safe_string(transaction, 'status')) + amount = self.safe_number(transaction, 'amount') + return { + 'info': transaction, + 'id': id, + 'txid': txid, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': None, + 'address': address, + 'addressTo': address, + 'addressFrom': None, + 'tag': None, + 'tagTo': None, + 'tagFrom': None, + 'type': type, + 'amount': amount, + 'currency': code, + 'status': status, + 'updated': updated, + 'internal': None, + 'comment': None, + 'fee': None, + } + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'PENDING': 'pending', + 'AVAILABLE': 'pending', + 'COMPLETED': 'ok', + 'FAILED': 'failed', + } + return self.safe_string(statuses, status, status) + + async def fetch_margin_mode(self, symbol: str, params={}) -> MarginMode: + """ + fetches the margin mode of a specific symbol + + https://docs.api.testnet.paradex.trade/#get-account-margin-configuration + + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin mode structure ` + """ + await self.authenticate_rest() + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + response = await self.privateGetAccountMargin(self.extend(request, params)) + # + # { + # "account": "0x6343248026a845b39a8a73fbe9c7ef0a841db31ed5c61ec1446aa9d25e54dbc", + # "configs": [ + # { + # "market": "SOL-USD-PERP", + # "leverage": 50, + # "margin_type": "CROSS" + # } + # ] + # } + # + configs = self.safe_list(response, 'configs') + return self.parse_margin_mode(self.safe_dict(configs, 0), market) + + def parse_margin_mode(self, rawMarginMode: dict, market=None) -> MarginMode: + marketId = self.safe_string(rawMarginMode, 'market') + market = self.safe_market(marketId, market) + marginMode = self.safe_string_lower(rawMarginMode, 'margin_type') + return { + 'info': rawMarginMode, + 'symbol': market['symbol'], + 'marginMode': marginMode, + } + + async def set_margin_mode(self, marginMode: str, symbol: Str = None, params={}): + """ + set margin mode to 'cross' or 'isolated' + + https://docs.api.testnet.paradex.trade/#set-margin-configuration + + :param str marginMode: 'cross' or 'isolated' + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.leverage]: the rate of leverage + :returns dict: response from the exchange + """ + self.check_required_argument('setMarginMode', symbol, 'symbol') + await self.authenticate_rest() + await self.load_markets() + market: Market = self.market(symbol) + leverage: Str = None + leverage, params = self.handle_option_and_params(params, 'setMarginMode', 'leverage', 1) + request: dict = { + 'market': market['id'], + 'leverage': leverage, + 'margin_type': self.encode_margin_mode(marginMode), + } + return await self.privatePostAccountMarginMarket(self.extend(request, params)) + + async def fetch_leverage(self, symbol: str, params={}) -> Leverage: + """ + fetch the set leverage for a market + + https://docs.api.testnet.paradex.trade/#get-account-margin-configuration + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `leverage structure ` + """ + await self.authenticate_rest() + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + response = await self.privateGetAccountMargin(self.extend(request, params)) + # + # { + # "account": "0x6343248026a845b39a8a73fbe9c7ef0a841db31ed5c61ec1446aa9d25e54dbc", + # "configs": [ + # { + # "market": "SOL-USD-PERP", + # "leverage": 50, + # "margin_type": "CROSS" + # } + # ] + # } + # + configs = self.safe_list(response, 'configs') + return self.parse_leverage(self.safe_dict(configs, 0), market) + + def parse_leverage(self, leverage: dict, market: Market = None) -> Leverage: + marketId = self.safe_string(leverage, 'market') + market = self.safe_market(marketId, market) + marginMode = self.safe_string_lower(leverage, 'margin_type') + return { + 'info': leverage, + 'symbol': self.safe_symbol(marketId, market), + 'marginMode': marginMode, + 'longLeverage': self.safe_integer(leverage, 'leverage'), + 'shortLeverage': self.safe_integer(leverage, 'leverage'), + } + + def encode_margin_mode(self, mode): + modes = { + 'cross': 'CROSS', + 'isolated': 'ISOLATED', + } + return self.safe_string(modes, mode, mode) + + async def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://docs.api.testnet.paradex.trade/#set-margin-configuration + + :param float leverage: the rate of leverage + :param str [symbol]: unified market symbol(is mandatory for swap markets) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' + :returns dict: response from the exchange + """ + self.check_required_argument('setLeverage', symbol, 'symbol') + await self.authenticate_rest() + await self.load_markets() + market: Market = self.market(symbol) + marginMode: Str = None + marginMode, params = self.handle_margin_mode_and_params('setLeverage', params, 'cross') + request: dict = { + 'market': market['id'], + 'leverage': leverage, + 'margin_type': self.encode_margin_mode(marginMode), + } + return await self.privatePostAccountMarginMarket(self.extend(request, params)) + + async def fetch_greeks(self, symbol: str, params={}) -> Greeks: + """ + fetches an option contracts greeks, financial metrics used to measure the factors that affect the price of an options contract + + https://docs.api.testnet.paradex.trade/#list-available-markets-summary + + :param str symbol: unified symbol of the market to fetch greeks for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `greeks structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + response = await self.publicGetMarketsSummary(self.extend(request, params)) + # + # { + # "results": [ + # { + # "symbol": "BTC-USD-114000-P", + # "mark_price": "10835.66892602", + # "mark_iv": "0.71781855", + # "delta": "-0.98726024", + # "greeks": { + # "delta": "-0.9872602390817709", + # "gamma": "0.000004560958862297231", + # "vega": "227.11344863639806", + # "rho": "-302.0617972461581", + # "vanna": "0.06609830491614832", + # "volga": "925.9501532805552" + # }, + # "last_traded_price": "10551.5", + # "bid": "10794.9", + # "bid_iv": "0.05", + # "ask": "10887.3", + # "ask_iv": "0.8783283", + # "last_iv": "0.05", + # "volume_24h": "0", + # "total_volume": "195240.72672261014", + # "created_at": 1747644009995, + # "underlying_price": "103164.79162649", + # "open_interest": "0", + # "funding_rate": "0.000004464241170536191", + # "price_change_rate_24h": "0.074915", + # "future_funding_rate": "0.0001" + # } + # ] + # } + # + data = self.safe_list(response, 'results', []) + greeks = self.safe_dict(data, 0, {}) + return self.parse_greeks(greeks, market) + + async def fetch_all_greeks(self, symbols: Strings = None, params={}) -> List[Greeks]: + """ + fetches all option contracts greeks, financial metrics used to measure the factors that affect the price of an options contract + + https://docs.api.testnet.paradex.trade/#list-available-markets-summary + + :param str[] [symbols]: unified symbols of the markets to fetch greeks for, all markets are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `greeks structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, True, True, True) + request: dict = { + 'market': 'ALL', + } + response = await self.publicGetMarketsSummary(self.extend(request, params)) + # + # { + # "results": [ + # { + # "symbol": "BTC-USD-114000-P", + # "mark_price": "10835.66892602", + # "mark_iv": "0.71781855", + # "delta": "-0.98726024", + # "greeks": { + # "delta": "-0.9872602390817709", + # "gamma": "0.000004560958862297231", + # "vega": "227.11344863639806", + # "rho": "-302.0617972461581", + # "vanna": "0.06609830491614832", + # "volga": "925.9501532805552" + # }, + # "last_traded_price": "10551.5", + # "bid": "10794.9", + # "bid_iv": "0.05", + # "ask": "10887.3", + # "ask_iv": "0.8783283", + # "last_iv": "0.05", + # "volume_24h": "0", + # "total_volume": "195240.72672261014", + # "created_at": 1747644009995, + # "underlying_price": "103164.79162649", + # "open_interest": "0", + # "funding_rate": "0.000004464241170536191", + # "price_change_rate_24h": "0.074915", + # "future_funding_rate": "0.0001" + # } + # ] + # } + # + results = self.safe_list(response, 'results', []) + return self.parse_all_greeks(results, symbols) + + def parse_greeks(self, greeks: dict, market: Market = None) -> Greeks: + # + # { + # "symbol": "BTC-USD-114000-P", + # "mark_price": "10835.66892602", + # "mark_iv": "0.71781855", + # "delta": "-0.98726024", + # "greeks": { + # "delta": "-0.9872602390817709", + # "gamma": "0.000004560958862297231", + # "vega": "227.11344863639806", + # "rho": "-302.0617972461581", + # "vanna": "0.06609830491614832", + # "volga": "925.9501532805552" + # }, + # "last_traded_price": "10551.5", + # "bid": "10794.9", + # "bid_iv": "0.05", + # "ask": "10887.3", + # "ask_iv": "0.8783283", + # "last_iv": "0.05", + # "volume_24h": "0", + # "total_volume": "195240.72672261014", + # "created_at": 1747644009995, + # "underlying_price": "103164.79162649", + # "open_interest": "0", + # "funding_rate": "0.000004464241170536191", + # "price_change_rate_24h": "0.074915", + # "future_funding_rate": "0.0001" + # } + # + marketId = self.safe_string(greeks, 'symbol') + market = self.safe_market(marketId, market, None, 'option') + symbol = market['symbol'] + timestamp = self.safe_integer(greeks, 'created_at') + greeksData = self.safe_dict(greeks, 'greeks', {}) + return { + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'delta': self.safe_number(greeksData, 'delta'), + 'gamma': self.safe_number(greeksData, 'gamma'), + 'theta': None, + 'vega': self.safe_number(greeksData, 'vega'), + 'rho': self.safe_number(greeksData, 'rho'), + 'vanna': self.safe_number(greeksData, 'vanna'), + 'volga': self.safe_number(greeksData, 'volga'), + 'bidSize': None, + 'askSize': None, + 'bidImpliedVolatility': self.safe_number(greeks, 'bid_iv'), + 'askImpliedVolatility': self.safe_number(greeks, 'ask_iv'), + 'markImpliedVolatility': self.safe_number(greeks, 'mark_iv'), + 'bidPrice': self.safe_number(greeks, 'bid'), + 'askPrice': self.safe_number(greeks, 'ask'), + 'markPrice': self.safe_number(greeks, 'mark_price'), + 'lastPrice': self.safe_number(greeks, 'last_traded_price'), + 'underlyingPrice': self.safe_number(greeks, 'underlying_price'), + 'info': greeks, + } + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = self.implode_hostname(self.urls['api'][self.version]) + '/' + self.implode_params(path, params) + query = self.omit(params, self.extract_params(path)) + if api == 'public': + if query: + url += '?' + self.urlencode(query) + elif api == 'private': + headers = { + 'Accept': 'application/json', + 'PARADEX-PARTNER': self.safe_string(self.options, 'broker', 'CCXT'), + } + # TODO: optimize + if path == 'auth': + headers['PARADEX-STARKNET-ACCOUNT'] = query['account'] + headers['PARADEX-STARKNET-SIGNATURE'] = query['signature'] + headers['PARADEX-TIMESTAMP'] = str(query['timestamp']) + headers['PARADEX-SIGNATURE-EXPIRATION'] = str(query['expiration']) + elif path == 'onboarding': + headers['PARADEX-ETHEREUM-ACCOUNT'] = self.walletAddress + headers['PARADEX-STARKNET-ACCOUNT'] = query['account'] + headers['PARADEX-STARKNET-SIGNATURE'] = query['signature'] + headers['PARADEX-TIMESTAMP'] = str(self.nonce()) + headers['Content-Type'] = 'application/json' + body = self.json({ + 'public_key': query['public_key'], + }) + else: + token = self.options['authToken'] + headers['Authorization'] = 'Bearer ' + token + if method == 'POST': + headers['Content-Type'] = 'application/json' + body = self.json(query) + else: + url = url + '?' + self.urlencode(query) + # headers = { + # 'Accept': 'application/json', + # 'Authorization': 'Bearer ' + self.apiKey, + # } + # if method == 'POST': + # body = self.json(query) + # headers['Content-Type'] = 'application/json' + # else: + # if query: + # url += '?' + self.urlencode(query) + # } + # } + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if not response: + return None # fallback to default error handler + # + # { + # "data": null, + # "error": "NOT_ONBOARDED", + # "message": "User has never called /onboarding endpoint" + # } + # + errorCode = self.safe_string(response, 'error') + if errorCode is not None: + feedback = self.id + ' ' + body + self.throw_broadly_matched_exception(self.exceptions['broad'], body, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + raise ExchangeError(feedback) # unknown message + return None diff --git a/ccxt/async_support/paymium.py b/ccxt/async_support/paymium.py new file mode 100644 index 0000000..3c23101 --- /dev/null +++ b/ccxt/async_support/paymium.py @@ -0,0 +1,627 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.paymium import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currency, DepositAddress, Int, Market, Num, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Trade, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class paymium(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(paymium, self).describe(), { + 'id': 'paymium', + 'name': 'Paymium', + 'countries': ['FR', 'EU'], + 'rateLimit': 2000, + 'version': 'v1', + 'has': { + 'CORS': True, + 'spot': True, + 'margin': None, + 'swap': False, + 'future': False, + 'option': False, + 'cancelOrder': True, + 'createDepositAddress': True, + 'createOrder': True, + 'fetchBalance': True, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': True, + 'fetchDepositAddressesByNetwork': False, + 'fetchFundingHistory': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchIndexOHLCV': False, + 'fetchMarkOHLCV': False, + 'fetchOpenInterestHistory': False, + 'fetchOrderBook': True, + 'fetchPremiumIndexOHLCV': False, + 'fetchTicker': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': False, + 'transfer': True, + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/51840849/87153930-f0f02200-c2c0-11ea-9c0a-40337375ae89.jpg', + 'api': { + 'rest': 'https://paymium.com/api', + }, + 'www': 'https://www.paymium.com', + 'fees': 'https://www.paymium.com/page/help/fees', + 'doc': [ + 'https://github.com/Paymium/api-documentation', + 'https://www.paymium.com/page/developers', + 'https://paymium.github.io/api-documentation/', + ], + 'referral': 'https://www.paymium.com/page/sign-up?referral=eDAzPoRQFMvaAB8sf-qj', + }, + 'api': { + 'public': { + 'get': [ + 'countries', + 'currencies', + 'data/{currency}/ticker', + 'data/{currency}/trades', + 'data/{currency}/depth', + 'bitcoin_charts/{id}/trades', + 'bitcoin_charts/{id}/depth', + ], + }, + 'private': { + 'get': [ + 'user', + 'user/addresses', + 'user/addresses/{address}', + 'user/orders', + 'user/orders/{uuid}', + 'user/price_alerts', + 'merchant/get_payment/{uuid}', + ], + 'post': [ + 'user/addresses', + 'user/orders', + 'user/withdrawals', + 'user/email_transfers', + 'user/payment_requests', + 'user/price_alerts', + 'merchant/create_payment', + ], + 'delete': [ + 'user/orders/{uuid}', + 'user/orders/{uuid}/cancel', + 'user/price_alerts/{id}', + ], + }, + }, + 'markets': { + 'BTC/EUR': self.safe_market_structure({'id': 'eur', 'symbol': 'BTC/EUR', 'base': 'BTC', 'quote': 'EUR', 'baseId': 'btc', 'quoteId': 'eur', 'type': 'spot', 'spot': True}), + }, + 'fees': { + 'trading': { + 'maker': self.parse_number('-0.001'), + 'taker': self.parse_number('0.005'), + }, + }, + 'precisionMode': TICK_SIZE, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': False, + 'triggerDirection': False, + 'triggerPriceType': None, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': False, + 'FOK': False, + 'PO': False, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': True, # todo + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': None, + 'fetchOrder': None, # todo + 'fetchOpenOrders': None, # todo + 'fetchOrders': None, # todo + 'fetchClosedOrders': None, # todo + 'fetchOHLCV': None, # todo + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + }) + + def parse_balance(self, response) -> Balances: + result: dict = {'info': response} + currencies = list(self.currencies.keys()) + for i in range(0, len(currencies)): + code = currencies[i] + currency = self.currency(code) + currencyId = currency['id'] + free = 'balance_' + currencyId + if free in response: + account = self.account() + used = 'locked_' + currencyId + account['free'] = self.safe_string(response, free) + account['used'] = self.safe_string(response, used) + result[code] = account + return self.safe_balance(result) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://paymium.github.io/api-documentation/#tag/User/paths/~1user/get + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + response = await self.privateGetUser(params) + return self.parse_balance(response) + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://paymium.github.io/api-documentation/#tag/Public-data/paths/~1data~1%7Bcurrency%7D~1depth/get + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'currency': market['id'], + } + response = await self.publicGetDataCurrencyDepth(self.extend(request, params)) + return self.parse_order_book(response, market['symbol'], None, 'bids', 'asks', 'price', 'amount') + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "high":"33740.82", + # "low":"32185.15", + # "volume":"4.7890433", + # "bid":"33313.53", + # "ask":"33497.97", + # "midpoint":"33405.75", + # "vwap":"32802.5263553", + # "at":1643381654, + # "price":"33143.91", + # "open":"33116.86", + # "variation":"0.0817", + # "currency":"EUR", + # "trade_id":"ce2f5152-3ac5-412d-9b24-9fa72338474c", + # "size":"0.00041087" + # } + # + symbol = self.safe_symbol(None, market) + timestamp = self.safe_timestamp(ticker, 'at') + vwap = self.safe_string(ticker, 'vwap') + baseVolume = self.safe_string(ticker, 'volume') + quoteVolume = Precise.string_mul(baseVolume, vwap) + last = self.safe_string(ticker, 'price') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': self.safe_string(ticker, 'bid'), + 'bidVolume': None, + 'ask': self.safe_string(ticker, 'ask'), + 'askVolume': None, + 'vwap': vwap, + 'open': self.safe_string(ticker, 'open'), + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': self.safe_string(ticker, 'variation'), + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'info': ticker, + }, market) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://paymium.github.io/api-documentation/#tag/Public-data/paths/~1data~1%7Bcurrency%7D~1ticker/get + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'currency': market['id'], + } + ticker = await self.publicGetDataCurrencyTicker(self.extend(request, params)) + # + # { + # "high":"33740.82", + # "low":"32185.15", + # "volume":"4.7890433", + # "bid":"33313.53", + # "ask":"33497.97", + # "midpoint":"33405.75", + # "vwap":"32802.5263553", + # "at":1643381654, + # "price":"33143.91", + # "open":"33116.86", + # "variation":"0.0817", + # "currency":"EUR", + # "trade_id":"ce2f5152-3ac5-412d-9b24-9fa72338474c", + # "size":"0.00041087" + # } + # + return self.parse_ticker(ticker, market) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + timestamp = self.safe_timestamp(trade, 'created_at_int') + id = self.safe_string(trade, 'uuid') + market = self.safe_market(None, market) + side = self.safe_string(trade, 'side') + price = self.safe_string(trade, 'price') + amountField = 'traded_' + market['base'].lower() + amount = self.safe_string(trade, amountField) + return self.safe_trade({ + 'info': trade, + 'id': id, + 'order': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'type': None, + 'side': side, + 'takerOrMaker': None, + 'price': price, + 'amount': amount, + 'cost': None, + 'fee': None, + }, market) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://paymium.github.io/api-documentation/#tag/Public-data/paths/~1data~1%7Bcurrency%7D~1trades/get + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'currency': market['id'], + } + response = await self.publicGetDataCurrencyTrades(self.extend(request, params)) + return self.parse_trades(response, market, since, limit) + + async def create_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + create a currency deposit address + + https://paymium.github.io/api-documentation/#tag/User/paths/~1user~1addresses/post + + :param str code: unified currency code of the currency for the deposit address + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + await self.load_markets() + response = await self.privatePostUserAddresses(params) + # + # { + # "address": "1HdjGr6WCTcnmW1tNNsHX7fh4Jr5C2PeKe", + # "valid_until": 1620041926, + # "currency": "BTC", + # "label": "Savings" + # } + # + return self.parse_deposit_address(response) + + async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://paymium.github.io/api-documentation/#tag/User/paths/~1user~1addresses~1%7Baddress%7D/get + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + await self.load_markets() + request: dict = { + 'address': code, + } + response = await self.privateGetUserAddressesAddress(self.extend(request, params)) + # + # { + # "address": "1HdjGr6WCTcnmW1tNNsHX7fh4Jr5C2PeKe", + # "valid_until": 1620041926, + # "currency": "BTC", + # "label": "Savings" + # } + # + return self.parse_deposit_address(response) + + async def fetch_deposit_addresses(self, codes: Strings = None, params={}) -> List[DepositAddress]: + """ + fetch deposit addresses for multiple currencies and chain types + + https://paymium.github.io/api-documentation/#tag/User/paths/~1user~1addresses/get + + :param str[]|None codes: list of unified currency codes, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `address structures ` + """ + await self.load_markets() + response = await self.privateGetUserAddresses(params) + # + # [ + # { + # "address": "1HdjGr6WCTcnmW1tNNsHX7fh4Jr5C2PeKe", + # "valid_until": 1620041926, + # "currency": "BTC", + # "label": "Savings" + # } + # ] + # + return self.parse_deposit_addresses(response, codes) + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + # + # { + # "address": "1HdjGr6WCTcnmW1tNNsHX7fh4Jr5C2PeKe", + # "valid_until": 1620041926, + # "currency": "BTC", + # "label": "Savings" + # } + # + address = self.safe_string(depositAddress, 'address') + currencyId = self.safe_string(depositAddress, 'currency') + return { + 'info': depositAddress, + 'currency': self.safe_currency_code(currencyId, currency), + 'network': None, + 'address': address, + 'tag': None, + } + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://paymium.github.io/api-documentation/#tag/Order/paths/~1user~1orders/post + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'type': self.capitalize(type) + 'Order', + 'currency': market['id'], + 'direction': side, + 'amount': amount, + } + if type != 'market': + request['price'] = price + response = await self.privatePostUserOrders(self.extend(request, params)) + return self.safe_order({ + 'info': response, + 'id': response['uuid'], + }, market) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://paymium.github.io/api-documentation/#tag/Order/paths/~1user~1orders~1%7Buuid%7D/delete + https://paymium.github.io/api-documentation/#tag/Order/paths/~1user~1orders~1%7Buuid%7D~1cancel/delete + + :param str id: order id + :param str symbol: not used by paymium cancelOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + request: dict = { + 'uuid': id, + } + response = await self.privateDeleteUserOrdersUuidCancel(self.extend(request, params)) + return self.safe_order({ + 'info': response, + }) + + async def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://paymium.github.io/api-documentation/#tag/Transfer/paths/~1user~1email_transfers/post + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account to transfer from + :param str toAccount: account to transfer to + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transfer structure ` + """ + await self.load_markets() + currency = self.currency(code) + if toAccount.find('@') < 0: + raise ExchangeError(self.id + ' transfer() only allows transfers to an email address') + if code != 'BTC' and code != 'EUR': + raise ExchangeError(self.id + ' transfer() only allows BTC or EUR') + request: dict = { + 'currency': currency['id'], + 'amount': self.currency_to_precision(code, amount), + 'email': toAccount, + # 'comment': 'a small note explaining the transfer' + } + response = await self.privatePostUserEmailTransfers(self.extend(request, params)) + # + # { + # "uuid": "968f4580-e26c-4ad8-8bcd-874d23d55296", + # "type": "Transfer", + # "currency": "BTC", + # "currency_amount": "string", + # "created_at": "2013-10-24T10:34:37.000Z", + # "updated_at": "2013-10-24T10:34:37.000Z", + # "amount": "1.0", + # "state": "executed", + # "currency_fee": "0.0", + # "btc_fee": "0.0", + # "comment": "string", + # "traded_btc": "string", + # "traded_currency": "string", + # "direction": "buy", + # "price": "string", + # "account_operations": [ + # { + # "uuid": "968f4580-e26c-4ad8-8bcd-874d23d55296", + # "amount": "1.0", + # "currency": "BTC", + # "created_at": "2013-10-24T10:34:37.000Z", + # "created_at_int": 1389094259, + # "name": "account_operation", + # "address": "1FPDBXNqSkZMsw1kSkkajcj8berxDQkUoc", + # "tx_hash": "string", + # "is_trading_account": True + # } + # ] + # } + # + return self.parse_transfer(response, currency) + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + # + # { + # "uuid": "968f4580-e26c-4ad8-8bcd-874d23d55296", + # "type": "Transfer", + # "currency": "BTC", + # "currency_amount": "string", + # "created_at": "2013-10-24T10:34:37.000Z", + # "updated_at": "2013-10-24T10:34:37.000Z", + # "amount": "1.0", + # "state": "executed", + # "currency_fee": "0.0", + # "btc_fee": "0.0", + # "comment": "string", + # "traded_btc": "string", + # "traded_currency": "string", + # "direction": "buy", + # "price": "string", + # "account_operations": [ + # { + # "uuid": "968f4580-e26c-4ad8-8bcd-874d23d55296", + # "amount": "1.0", + # "currency": "BTC", + # "created_at": "2013-10-24T10:34:37.000Z", + # "created_at_int": 1389094259, + # "name": "account_operation", + # "address": "1FPDBXNqSkZMsw1kSkkajcj8berxDQkUoc", + # "tx_hash": "string", + # "is_trading_account": True + # } + # ] + # } + # + currencyId = self.safe_string(transfer, 'currency') + updatedAt = self.safe_string(transfer, 'updated_at') + timetstamp = self.parse_date(updatedAt) + accountOperations = self.safe_value(transfer, 'account_operations') + firstOperation = self.safe_value(accountOperations, 0, {}) + status = self.safe_string(transfer, 'state') + return { + 'info': transfer, + 'id': self.safe_string(transfer, 'uuid'), + 'timestamp': timetstamp, + 'datetime': self.iso8601(timetstamp), + 'currency': self.safe_currency_code(currencyId, currency), + 'amount': self.safe_number(transfer, 'amount'), + 'fromAccount': None, + 'toAccount': self.safe_string(firstOperation, 'address'), + 'status': self.parse_transfer_status(status), + } + + def parse_transfer_status(self, status: Str) -> Str: + statuses: dict = { + 'executed': 'ok', + # what are the other statuses? + } + return self.safe_string(statuses, status, status) + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = self.urls['api']['rest'] + '/' + self.version + '/' + self.implode_params(path, params) + query = self.omit(params, self.extract_params(path)) + if api == 'public': + if query: + url += '?' + self.urlencode(query) + else: + self.check_required_credentials() + nonce = str(self.nonce()) + auth = nonce + url + headers = { + 'Api-Key': self.apiKey, + 'Api-Nonce': nonce, + } + if method == 'POST': + if query: + body = self.json(query) + auth += body + headers['Content-Type'] = 'application/json' + else: + if query: + queryString = self.urlencode(query) + auth += queryString + url += '?' + queryString + headers['Api-Signature'] = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256) + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None + errors = self.safe_value(response, 'errors') + if errors is not None: + raise ExchangeError(self.id + ' ' + self.json(response)) + return None diff --git a/ccxt/async_support/phemex.py b/ccxt/async_support/phemex.py new file mode 100644 index 0000000..1e3cb82 --- /dev/null +++ b/ccxt/async_support/phemex.py @@ -0,0 +1,5031 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.phemex import ImplicitAPI +import asyncio +import hashlib +import numbers +from ccxt.base.types import Any, Balances, Conversion, Currencies, Currency, DepositAddress, Int, LeverageTier, LeverageTiers, MarginModification, Market, Num, Order, OrderBook, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, Trade, Transaction, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import AccountSuspended +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import DuplicateOrderId +from ccxt.base.errors import DDoSProtection +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import CancelPending +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class phemex(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(phemex, self).describe(), { + 'id': 'phemex', + 'name': 'Phemex', + 'countries': ['CN'], # China + 'rateLimit': 120.5, + 'version': 'v1', + 'certified': False, + 'pro': True, + 'hostname': 'api.phemex.com', + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': True, + 'future': False, + 'option': False, + 'addMargin': False, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'closePosition': False, + 'createConvertTrade': True, + 'createOrder': True, + 'createReduceOnlyOrder': True, + 'createStopLimitOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'editOrder': True, + 'fetchBalance': True, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchClosedOrders': True, + 'fetchConvertQuote': True, + 'fetchConvertTrade': False, + 'fetchConvertTradeHistory': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchFundingHistory': True, + 'fetchFundingRate': True, + 'fetchFundingRateHistories': False, + 'fetchFundingRateHistory': True, + 'fetchFundingRates': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchLeverage': False, + 'fetchLeverageTiers': True, + 'fetchMarketLeverageTiers': 'emulated', + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': True, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchPositions': True, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': False, + 'fetchTransfers': True, + 'fetchWithdrawals': True, + 'reduceMargin': False, + 'sandbox': True, + 'setLeverage': True, + 'setMargin': True, + 'setMarginMode': True, + 'setPositionMode': True, + 'transfer': True, + 'withdraw': True, + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/85225056-221eb600-b3d7-11ea-930d-564d2690e3f6.jpg', + 'test': { + 'v1': 'https://testnet-api.phemex.com/v1', + 'v2': 'https://testnet-api.phemex.com', + 'public': 'https://testnet-api.phemex.com/exchange/public', + 'private': 'https://testnet-api.phemex.com', + }, + 'api': { + 'v1': 'https://{hostname}/v1', + 'v2': 'https://{hostname}', + 'public': 'https://{hostname}/exchange/public', + 'private': 'https://{hostname}', + }, + 'www': 'https://phemex.com', + 'doc': 'https://phemex-docs.github.io/#overview', + 'fees': 'https://phemex.com/fees-conditions', + 'referral': { + 'url': 'https://phemex.com/register?referralCode=EDNVJ', + 'discount': 0.1, + }, + }, + 'timeframes': { + '1m': '60', + '3m': '180', + '5m': '300', + '15m': '900', + '30m': '1800', + '1h': '3600', + '2h': '7200', + '3h': '10800', + '4h': '14400', + '6h': '21600', + '12h': '43200', + '1d': '86400', + '1w': '604800', + '1M': '2592000', + '3M': '7776000', + '1Y': '31104000', + }, + 'api': { + 'public': { + 'get': { + 'cfg/v2/products': 5, # spot + contracts + 'cfg/fundingRates': 5, + 'products': 5, # contracts only + 'nomics/trades': 5, # ?market=&since= + 'md/kline': 5, # ?from=1589811875&resolution=1800&symbol=sBTCUSDT&to=1592457935 + 'md/v2/kline/list': 5, # perpetual api ?symbol=&to=&from=&resolution= + 'md/v2/kline': 5, # ?symbol=&resolution=&limit= + 'md/v2/kline/last': 5, # perpetual ?symbol=&resolution=&limit= + 'md/orderbook': 5, # ?symbol= + 'md/trade': 5, # ?symbol= + 'md/spot/ticker/24hr': 5, # ?symbol= + 'exchange/public/cfg/chain-settings': 5, # ?currency= + }, + }, + 'v1': { + 'get': { + 'md/fullbook': 5, # ?symbol= + 'md/orderbook': 5, # ?symbol= + 'md/trade': 5, # ?symbol=&id= + 'md/ticker/24hr': 5, # ?symbol=&id= + 'md/ticker/24hr/all': 5, # ?id= + 'md/spot/ticker/24hr': 5, # ?symbol=&id= + 'md/spot/ticker/24hr/all': 5, # ?symbol=&id= + 'exchange/public/products': 5, # contracts only + 'api-data/public/data/funding-rate-history': 5, + }, + }, + 'v2': { + 'get': { + 'public/products': 5, + 'public/products-plus': 5, + 'md/v2/orderbook': 5, # ?symbol=&id= + 'md/v2/trade': 5, # ?symbol=&id= + 'md/v2/ticker/24hr': 5, # ?symbol=&id= + 'md/v2/ticker/24hr/all': 5, # ?id= + 'api-data/public/data/funding-rate-history': 5, + }, + }, + 'private': { + 'get': { + # spot + 'spot/orders/active': 1, # ?symbol=&orderID= + # 'spot/orders/active': 5, # ?symbol=&clOrDID= + 'spot/orders': 1, # ?symbol= + 'spot/wallets': 5, # ?currency= + 'exchange/spot/order': 5, # ?symbol=&ordStatus=ordType=&start=&end=&limit=&offset= + 'exchange/spot/order/trades': 5, # ?symbol=&start=&end=&limit=&offset= + 'exchange/order/v2/orderList': 5, # ?symbol=¤cy=&ordStatus=&ordType=&start=&end=&offset=&limit=&withCount= + 'exchange/order/v2/tradingList': 5, # ?symbol=¤cy=&execType=&offset=&limit=&withCount= + # swap + 'accounts/accountPositions': 1, # ?currency= + 'g-accounts/accountPositions': 1, # ?currency= + 'g-accounts/positions': 25, # ?currency= + 'g-accounts/risk-unit': 1, + 'api-data/futures/funding-fees': 5, # ?symbol= + 'api-data/g-futures/funding-fees': 5, # ?symbol= + 'api-data/futures/orders': 5, # ?symbol= + 'api-data/g-futures/orders': 5, # ?symbol= + 'api-data/futures/orders/by-order-id': 5, # ?symbol= + 'api-data/g-futures/orders/by-order-id': 5, # ?symbol= + 'api-data/futures/trades': 5, # ?symbol= + 'api-data/g-futures/trades': 5, # ?symbol= + 'api-data/futures/trading-fees': 5, # ?symbol= + 'api-data/g-futures/trading-fees': 5, # ?symbol= + 'api-data/futures/v2/tradeAccountDetail': 5, # ?currency=&type=&limit=&offset=&start=&end=&withCount= + 'g-orders/activeList': 1, # ?symbol= + 'orders/activeList': 1, # ?symbol= + 'exchange/order/list': 5, # ?symbol=&start=&end=&offset=&limit=&ordStatus=&withCount= + 'exchange/order': 5, # ?symbol=&orderID= + # 'exchange/order': 5, # ?symbol=&clOrdID= + 'exchange/order/trade': 5, # ?symbol=&start=&end=&limit=&offset=&withCount= + 'phemex-user/users/children': 5, # ?offset=&limit=&withCount= + 'phemex-user/wallets/v2/depositAddress': 5, # ?_t=1592722635531¤cy=USDT + 'phemex-user/wallets/tradeAccountDetail': 5, # ?bizCode=¤cy=&end=1642443347321&limit=10&offset=0&side=&start=1&type=4&withCount=true + 'phemex-deposit/wallets/api/depositAddress': 5, # ?currency=&chainName= + 'phemex-deposit/wallets/api/depositHist': 5, # ?currency=&offset=&limit=&withCount= + 'phemex-deposit/wallets/api/chainCfg': 5, # ?currency= + 'phemex-withdraw/wallets/api/withdrawHist': 5, # ?currency=&chainName=&offset=&limit=&withCount= + 'phemex-withdraw/wallets/api/asset/info': 5, # ?currency=&amount= + 'phemex-user/order/closedPositionList': 5, # ?currency=USD&limit=10&offset=0&symbol=&withCount=true + 'exchange/margins/transfer': 5, # ?start=&end=&offset=&limit=&withCount= + 'exchange/wallets/confirm/withdraw': 5, # ?code= + 'exchange/wallets/withdrawList': 5, # ?currency=&limit=&offset=&withCount= + 'exchange/wallets/depositList': 5, # ?currency=&offset=&limit= + 'exchange/wallets/v2/depositAddress': 5, # ?currency= + 'api-data/spots/funds': 5, # ?currency=&start=&end=&limit=&offset= + 'api-data/spots/orders': 5, # ?symbol= + 'api-data/spots/orders/by-order-id': 5, # ?symbol=&oderId=&clOrdID= + 'api-data/spots/pnls': 5, + 'api-data/spots/trades': 5, # ?symbol= + 'api-data/spots/trades/by-order-id': 5, # ?symbol=&oderId=&clOrdID= + 'assets/convert': 5, # ?startTime=&endTime=&limit=&offset= + # transfer + 'assets/transfer': 5, # ?currency=&start=&end=&limit=&offset= + 'assets/spots/sub-accounts/transfer': 5, # ?currency=&start=&end=&limit=&offset= + 'assets/futures/sub-accounts/transfer': 5, # ?currency=&start=&end=&limit=&offset= + 'assets/quote': 5, # ?fromCurrency=&toCurrency=&amountEv= + # deposit/withdraw + }, + 'post': { + # spot + 'spot/orders': 1, + # swap + 'orders': 1, + 'g-orders': 1, + 'positions/assign': 5, # ?symbol=&posBalance=&posBalanceEv= + 'exchange/wallets/transferOut': 5, + 'exchange/wallets/transferIn': 5, + 'exchange/margins': 5, + 'exchange/wallets/createWithdraw': 5, # ?otpCode= + 'exchange/wallets/cancelWithdraw': 5, + 'exchange/wallets/createWithdrawAddress': 5, # ?otpCode={optCode} + # transfer + 'assets/transfer': 5, + 'assets/spots/sub-accounts/transfer': 5, # for sub-account only + 'assets/futures/sub-accounts/transfer': 5, # for sub-account only + 'assets/universal-transfer': 5, # for Main account only + 'assets/convert': 5, + # withdraw + 'phemex-withdraw/wallets/api/createWithdraw': 5, # ?currency=&address=
&amount=&addressTag=&chainName= + 'phemex-withdraw/wallets/api/cancelWithdraw': 5, # ?id= + }, + 'put': { + # spot + 'spot/orders/create': 1, # ?symbol=&trigger=&clOrdID=&priceEp=&baseQtyEv="eQtyEv=&stopPxEp=&text=&side=&qtyType=&ordType=&timeInForce=&execInst= + 'spot/orders': 1, # ?symbol=&orderID=&origClOrdID=&clOrdID=&priceEp=&baseQtyEV="eQtyEv=&stopPxEp= + # swap + 'orders/replace': 1, # ?symbol=&orderID=&origClOrdID=&clOrdID=&price=&priceEp=&orderQty=&stopPx=&stopPxEp=&takeProfit=&takeProfitEp=&stopLoss=&stopLossEp=&pegOffsetValueEp=&pegPriceType= + 'g-orders/replace': 1, # ?symbol=&orderID=&origClOrdID=&clOrdID=&price=&priceEp=&orderQty=&stopPx=&stopPxEp=&takeProfit=&takeProfitEp=&stopLoss=&stopLossEp=&pegOffsetValueEp=&pegPriceType= + 'g-orders/create': 1, + 'positions/leverage': 5, # ?symbol=&leverage=&leverageEr= + 'g-positions/leverage': 5, # ?symbol=&leverage=&leverageEr= + 'g-positions/switch-pos-mode-sync': 5, # ?symbol=&targetPosMode= + 'positions/riskLimit': 5, # ?symbol=&riskLimit=&riskLimitEv= + }, + 'delete': { + # spot + 'spot/orders': 2, # ?symbol=&orderID= + 'spot/orders/all': 2, # ?symbol=&untriggered= + # 'spot/orders': 5, # ?symbol=&clOrdID= + # swap + 'orders/cancel': 1, # ?symbol=&orderID= + 'orders': 1, # ?symbol=&orderID=,, + 'orders/all': 3, # ?symbol=&untriggered=&text= + 'g-orders/cancel': 1, # ?symbol=&orderID= + 'g-orders': 1, # ?symbol=&orderID=,, + 'g-orders/all': 3, # ?symbol=&untriggered=&text= + }, + }, + }, + 'precisionMode': TICK_SIZE, + 'fees': { + 'trading': { + 'tierBased': False, + 'percentage': True, + 'taker': self.parse_number('0.001'), + 'maker': self.parse_number('0.001'), + }, + }, + 'features': { + 'default': { + 'sandbox': True, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + # todo + 'triggerPriceType': { + 'mark': True, + 'last': True, + 'index': True, + }, + 'triggerDirection': False, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'leverage': False, + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': False, + 'trailing': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 200, + 'daysBack': 100000, + 'untilDays': 2, # todo implement + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': None, + 'daysBack': None, + 'untilDays': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 200, + 'daysBack': 100000, + 'daysBackCanceled': 100000, + 'untilDays': 2, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 1000, + }, + }, + 'spot': { + 'extends': 'default', + }, + 'forDerivatives': { + 'extends': 'default', + 'createOrder': { + 'triggerDirection': True, + 'attachedStopLossTakeProfit': { + 'triggerPriceType': { + 'mark': True, + 'last': True, + 'index': True, + }, + 'price': True, + }, + 'hedged': True, + }, + 'fetchOHLCV': { + 'limit': 2000, + }, + }, + 'swap': { + 'linear': { + 'extends': 'forDerivatives', + }, + 'inverse': { + 'extends': 'forDerivatives', + }, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + }, + 'exceptions': { + 'exact': { + # not documented + '401': AuthenticationError, # {"code":"401","msg":"401 Failed to load API KEY."} + '412': BadRequest, # {"code":412,"msg":"Missing parameter - resolution","data":null} + '6001': BadRequest, # {"error":{"code":6001,"message":"invalid argument"},"id":null,"result":null} + # documented + '19999': BadRequest, # REQUEST_IS_DUPLICATED Duplicated request ID + '10001': DuplicateOrderId, # OM_DUPLICATE_ORDERID Duplicated order ID + '10002': OrderNotFound, # OM_ORDER_NOT_FOUND Cannot find order ID + '10003': CancelPending, # OM_ORDER_PENDING_CANCEL Cannot cancel while order is already in pending cancel status + '10004': CancelPending, # OM_ORDER_PENDING_REPLACE Cannot cancel while order is already in pending cancel status + '10005': CancelPending, # OM_ORDER_PENDING Cannot cancel while order is already in pending cancel status + '11001': InsufficientFunds, # TE_NO_ENOUGH_AVAILABLE_BALANCE Insufficient available balance + '11002': InvalidOrder, # TE_INVALID_RISK_LIMIT Invalid risk limit value + '11003': InsufficientFunds, # TE_NO_ENOUGH_BALANCE_FOR_NEW_RISK_LIMIT Insufficient available balance + '11004': InvalidOrder, # TE_INVALID_LEVERAGE invalid input or new leverage is over maximum allowed leverage + '11005': InsufficientFunds, # TE_NO_ENOUGH_BALANCE_FOR_NEW_LEVERAGE Insufficient available balance + '11006': ExchangeError, # TE_CANNOT_CHANGE_POSITION_MARGIN_WITHOUT_POSITION Position size is zero. Cannot change margin + '11007': ExchangeError, # TE_CANNOT_CHANGE_POSITION_MARGIN_FOR_CROSS_MARGIN Cannot change margin under CrossMargin + '11008': ExchangeError, # TE_CANNOT_REMOVE_POSITION_MARGIN_MORE_THAN_ADDED exceeds the maximum removable Margin + '11009': ExchangeError, # TE_CANNOT_REMOVE_POSITION_MARGIN_DUE_TO_UNREALIZED_PNL exceeds the maximum removable Margin + '11010': InsufficientFunds, # TE_CANNOT_ADD_POSITION_MARGIN_DUE_TO_NO_ENOUGH_AVAILABLE_BALANCE Insufficient available balance + '11011': InvalidOrder, # TE_REDUCE_ONLY_ABORT Cannot accept reduce only order + '11012': InvalidOrder, # TE_REPLACE_TO_INVALID_QTY Order quantity Error + '11013': InvalidOrder, # TE_CONDITIONAL_NO_POSITION Position size is zero. Cannot determine conditional order's quantity + '11014': InvalidOrder, # TE_CONDITIONAL_CLOSE_POSITION_WRONG_SIDE Close position conditional order has the same side + '11015': InvalidOrder, # TE_CONDITIONAL_TRIGGERED_OR_CANCELED + '11016': BadRequest, # TE_ADL_NOT_TRADING_REQUESTED_ACCOUNT Request is routed to the wrong trading engine + '11017': ExchangeError, # TE_ADL_CANNOT_FIND_POSITION Cannot find requested position on current account + '11018': ExchangeError, # TE_NO_NEED_TO_SETTLE_FUNDING The current account does not need to pay a funding fee + '11019': ExchangeError, # TE_FUNDING_ALREADY_SETTLED The current account already pays the funding fee + '11020': ExchangeError, # TE_CANNOT_TRANSFER_OUT_DUE_TO_BONUS Withdraw to wallet needs to remove all remaining bonus. However if bonus is used by position or order cost, withdraw fails. + '11021': ExchangeError, # TE_INVALID_BONOUS_AMOUNT # Grpc command cannot be negative number Invalid bonus amount + '11022': AccountSuspended, # TE_REJECT_DUE_TO_BANNED Account is banned + '11023': ExchangeError, # TE_REJECT_DUE_TO_IN_PROCESS_OF_LIQ Account is in the process of liquidation + '11024': ExchangeError, # TE_REJECT_DUE_TO_IN_PROCESS_OF_ADL Account is in the process of auto-deleverage + '11025': BadRequest, # TE_ROUTE_ERROR Request is routed to the wrong trading engine + '11026': ExchangeError, # TE_UID_ACCOUNT_MISMATCH + '11027': BadSymbol, # TE_SYMBOL_INVALID Invalid number ID or name + '11028': BadSymbol, # TE_CURRENCY_INVALID Invalid currency ID or name + '11029': ExchangeError, # TE_ACTION_INVALID Unrecognized request type + '11030': ExchangeError, # TE_ACTION_BY_INVALID + '11031': DDoSProtection, # TE_SO_NUM_EXCEEDS Number of total conditional orders exceeds the max limit + '11032': DDoSProtection, # TE_AO_NUM_EXCEEDS Number of total active orders exceeds the max limit + '11033': DuplicateOrderId, # TE_ORDER_ID_DUPLICATE Duplicated order ID + '11034': InvalidOrder, # TE_SIDE_INVALID Invalid side + '11035': InvalidOrder, # TE_ORD_TYPE_INVALID Invalid OrderType + '11036': InvalidOrder, # TE_TIME_IN_FORCE_INVALID Invalid TimeInForce + '11037': InvalidOrder, # TE_EXEC_INST_INVALID Invalid ExecType + '11038': InvalidOrder, # TE_TRIGGER_INVALID Invalid trigger type + '11039': InvalidOrder, # TE_STOP_DIRECTION_INVALID Invalid stop direction type + '11040': InvalidOrder, # TE_NO_MARK_PRICE Cannot get valid mark price to create conditional order + '11041': InvalidOrder, # TE_NO_INDEX_PRICE Cannot get valid index price to create conditional order + '11042': InvalidOrder, # TE_NO_LAST_PRICE Cannot get valid last market price to create conditional order + '11043': InvalidOrder, # TE_RISING_TRIGGER_DIRECTLY Conditional order would be triggered immediately + '11044': InvalidOrder, # TE_FALLING_TRIGGER_DIRECTLY Conditional order would be triggered immediately + '11045': InvalidOrder, # TE_TRIGGER_PRICE_TOO_LARGE Conditional order trigger price is too high + '11046': InvalidOrder, # TE_TRIGGER_PRICE_TOO_SMALL Conditional order trigger price is too low + '11047': InvalidOrder, # TE_BUY_TP_SHOULD_GT_BASE TakeProfile BUY conditional order trigger price needs to be greater than reference price + '11048': InvalidOrder, # TE_BUY_SL_SHOULD_LT_BASE StopLoss BUY condition order price needs to be less than the reference price + '11049': InvalidOrder, # TE_BUY_SL_SHOULD_GT_LIQ StopLoss BUY condition order price needs to be greater than liquidation price or it will not trigger + '11050': InvalidOrder, # TE_SELL_TP_SHOULD_LT_BASE TakeProfile SELL conditional order trigger price needs to be less than reference price + '11051': InvalidOrder, # TE_SELL_SL_SHOULD_LT_LIQ StopLoss SELL condition order price needs to be less than liquidation price or it will not trigger + '11052': InvalidOrder, # TE_SELL_SL_SHOULD_GT_BASE StopLoss SELL condition order price needs to be greater than the reference price + '11053': InvalidOrder, # TE_PRICE_TOO_LARGE + '11054': InvalidOrder, # TE_PRICE_WORSE_THAN_BANKRUPT Order price cannot be more aggressive than bankrupt price if self order has instruction to close a position + '11055': InvalidOrder, # TE_PRICE_TOO_SMALL Order price is too low + '11056': InvalidOrder, # TE_QTY_TOO_LARGE Order quantity is too large + '11057': InvalidOrder, # TE_QTY_NOT_MATCH_REDUCE_ONLY Does not allow ReduceOnly order without position + '11058': InvalidOrder, # TE_QTY_TOO_SMALL Order quantity is too small + '11059': InvalidOrder, # TE_TP_SL_QTY_NOT_MATCH_POS Position size is zero. Cannot accept any TakeProfit or StopLoss order + '11060': InvalidOrder, # TE_SIDE_NOT_CLOSE_POS TakeProfit or StopLoss order has wrong side. Cannot close position + '11061': CancelPending, # TE_ORD_ALREADY_PENDING_CANCEL Repeated cancel request + '11062': InvalidOrder, # TE_ORD_ALREADY_CANCELED Order is already canceled + '11063': InvalidOrder, # TE_ORD_STATUS_CANNOT_CANCEL Order is not able to be canceled under current status + '11064': InvalidOrder, # TE_ORD_ALREADY_PENDING_REPLACE Replace request is rejected because order is already in pending replace status + '11065': InvalidOrder, # TE_ORD_REPLACE_NOT_MODIFIED Replace request does not modify any parameters of the order + '11066': InvalidOrder, # TE_ORD_STATUS_CANNOT_REPLACE Order is not able to be replaced under current status + '11067': InvalidOrder, # TE_CANNOT_REPLACE_PRICE Market conditional order cannot change price + '11068': InvalidOrder, # TE_CANNOT_REPLACE_QTY Condtional order for closing position cannot change order quantity, since the order quantity is determined by position size already + '11069': ExchangeError, # TE_ACCOUNT_NOT_IN_RANGE The account ID in the request is not valid or is not in the range of the current process + '11070': BadSymbol, # TE_SYMBOL_NOT_IN_RANGE The symbol is invalid + '11071': InvalidOrder, # TE_ORD_STATUS_CANNOT_TRIGGER + '11072': InvalidOrder, # TE_TKFR_NOT_IN_RANGE The fee value is not valid + '11073': InvalidOrder, # TE_MKFR_NOT_IN_RANGE The fee value is not valid + '11074': InvalidOrder, # TE_CANNOT_ATTACH_TP_SL Order request cannot contain TP/SL parameters when the account already has positions + '11075': InvalidOrder, # TE_TP_TOO_LARGE TakeProfit price is too large + '11076': InvalidOrder, # TE_TP_TOO_SMALL TakeProfit price is too small + '11077': InvalidOrder, # TE_TP_TRIGGER_INVALID Invalid trigger type + '11078': InvalidOrder, # TE_SL_TOO_LARGE StopLoss price is too large + '11079': InvalidOrder, # TE_SL_TOO_SMALL StopLoss price is too small + '11080': InvalidOrder, # TE_SL_TRIGGER_INVALID Invalid trigger type + '11081': InvalidOrder, # TE_RISK_LIMIT_EXCEEDS Total potential position breaches current risk limit + '11082': InsufficientFunds, # TE_CANNOT_COVER_ESTIMATE_ORDER_LOSS The remaining balance cannot cover the potential unrealized PnL for self new order + '11083': InvalidOrder, # TE_TAKE_PROFIT_ORDER_DUPLICATED TakeProfit order already exists + '11084': InvalidOrder, # TE_STOP_LOSS_ORDER_DUPLICATED StopLoss order already exists + '11085': DuplicateOrderId, # TE_CL_ORD_ID_DUPLICATE ClOrdId is duplicated + '11086': InvalidOrder, # TE_PEG_PRICE_TYPE_INVALID PegPriceType is invalid + '11087': InvalidOrder, # TE_BUY_TS_SHOULD_LT_BASE The trailing order's StopPrice should be less than the current last price + '11088': InvalidOrder, # TE_BUY_TS_SHOULD_GT_LIQ The traling order's StopPrice should be greater than the current liquidation price + '11089': InvalidOrder, # TE_SELL_TS_SHOULD_LT_LIQ The traling order's StopPrice should be greater than the current last price + '11090': InvalidOrder, # TE_SELL_TS_SHOULD_GT_BASE The traling order's StopPrice should be less than the current liquidation price + '11091': InvalidOrder, # TE_BUY_REVERT_VALUE_SHOULD_LT_ZERO The PegOffset should be less than zero + '11092': InvalidOrder, # TE_SELL_REVERT_VALUE_SHOULD_GT_ZERO The PegOffset should be greater than zero + '11093': InvalidOrder, # TE_BUY_TTP_SHOULD_ACTIVATE_ABOVE_BASE The activation price should be greater than the current last price + '11094': InvalidOrder, # TE_SELL_TTP_SHOULD_ACTIVATE_BELOW_BASE The activation price should be less than the current last price + '11095': InvalidOrder, # TE_TRAILING_ORDER_DUPLICATED A trailing order exists already + '11096': InvalidOrder, # TE_CLOSE_ORDER_CANNOT_ATTACH_TP_SL An order to close position cannot have trailing instruction + '11097': BadRequest, # TE_CANNOT_FIND_WALLET_OF_THIS_CURRENCY This crypto is not supported + '11098': BadRequest, # TE_WALLET_INVALID_ACTION Invalid action on wallet + '11099': ExchangeError, # TE_WALLET_VID_UNMATCHED Wallet operation request has a wrong wallet vid + '11100': InsufficientFunds, # TE_WALLET_INSUFFICIENT_BALANCE Wallet has insufficient balance + '11101': InsufficientFunds, # TE_WALLET_INSUFFICIENT_LOCKED_BALANCE Locked balance in wallet is not enough for unlock/withdraw request + '11102': BadRequest, # TE_WALLET_INVALID_DEPOSIT_AMOUNT Deposit amount must be greater than zero + '11103': BadRequest, # TE_WALLET_INVALID_WITHDRAW_AMOUNT Withdraw amount must be less than zero + '11104': BadRequest, # TE_WALLET_REACHED_MAX_AMOUNT Deposit makes wallet exceed max amount allowed + '11105': InsufficientFunds, # TE_PLACE_ORDER_INSUFFICIENT_BASE_BALANCE Insufficient funds in base wallet + '11106': InsufficientFunds, # TE_PLACE_ORDER_INSUFFICIENT_QUOTE_BALANCE Insufficient funds in quote wallet + '11107': ExchangeError, # TE_CANNOT_CONNECT_TO_REQUEST_SEQ TradingEngine failed to connect with CrossEngine + '11108': InvalidOrder, # TE_CANNOT_REPLACE_OR_CANCEL_MARKET_ORDER Cannot replace/amend market order + '11109': InvalidOrder, # TE_CANNOT_REPLACE_OR_CANCEL_IOC_ORDER Cannot replace/amend ImmediateOrCancel order + '11110': InvalidOrder, # TE_CANNOT_REPLACE_OR_CANCEL_FOK_ORDER Cannot replace/amend FillOrKill order + '11111': InvalidOrder, # TE_MISSING_ORDER_ID OrderId is missing + '11112': InvalidOrder, # TE_QTY_TYPE_INVALID QtyType is invalid + '11113': BadRequest, # TE_USER_ID_INVALID UserId is invalid + '11114': InvalidOrder, # TE_ORDER_VALUE_TOO_LARGE Order value is too large + '11115': InvalidOrder, # TE_ORDER_VALUE_TOO_SMALL Order value is too small + '11116': InvalidOrder, # TE_BO_NUM_EXCEEDS Details: the total count of brakcet orders should equal or less than 5 + '11117': InvalidOrder, # TE_BO_CANNOT_HAVE_BO_WITH_DIFF_SIDE Details: all bracket orders should have the same Side. + '11118': InvalidOrder, # TE_BO_TP_PRICE_INVALID Details: bracker order take profit price is invalid + '11119': InvalidOrder, # TE_BO_SL_PRICE_INVALID Details: bracker order stop loss price is invalid + '11120': InvalidOrder, # TE_BO_SL_TRIGGER_PRICE_INVALID Details: bracker order stop loss trigger price is invalid + '11121': InvalidOrder, # TE_BO_CANNOT_REPLACE Details: cannot replace bracket order. + '11122': InvalidOrder, # TE_BO_BOTP_STATUS_INVALID Details: bracket take profit order status is invalid + '11123': InvalidOrder, # TE_BO_CANNOT_PLACE_BOTP_OR_BOSL_ORDER Details: cannot place bracket take profit order + '11124': InvalidOrder, # TE_BO_CANNOT_REPLACE_BOTP_OR_BOSL_ORDER Details: cannot place bracket stop loss order + '11125': InvalidOrder, # TE_BO_CANNOT_CANCEL_BOTP_OR_BOSL_ORDER Details: cannot cancel bracket sl/tp order + '11126': InvalidOrder, # TE_BO_DONOT_SUPPORT_API Details: doesn't support bracket order via API + '11128': InvalidOrder, # TE_BO_INVALID_EXECINST Details: ExecInst value is invalid + '11129': InvalidOrder, # TE_BO_MUST_BE_SAME_SIDE_AS_POS Details: bracket order should have the same side's side + '11130': InvalidOrder, # TE_BO_WRONG_SL_TRIGGER_TYPE Details: bracket stop loss order trigger type is invalid + '11131': InvalidOrder, # TE_BO_WRONG_TP_TRIGGER_TYPE Details: bracket take profit order trigger type is invalid + '11132': InvalidOrder, # TE_BO_ABORT_BOSL_DUE_BOTP_CREATE_FAILED Details: cancel bracket stop loss order due failed to create take profit order. + '11133': InvalidOrder, # TE_BO_ABORT_BOSL_DUE_BOPO_CANCELED Details: cancel bracket stop loss order due main order canceled. + '11134': InvalidOrder, # TE_BO_ABORT_BOTP_DUE_BOPO_CANCELED Details: cancel bracket take profit order due main order canceled. + # not documented + '30000': BadRequest, # {"code":30000,"msg":"Please double check input arguments","data":null} + '30018': BadRequest, # {"code":30018,"msg":"phemex.data.size.uplimt","data":null} + '34003': PermissionDenied, # {"code":34003,"msg":"Access forbidden","data":null} + '35104': InsufficientFunds, # {"code":35104,"msg":"phemex.spot.wallet.balance.notenough","data":null} + '39995': RateLimitExceeded, # {"code": "39995","msg": "Too many requests."} + '39996': PermissionDenied, # {"code": "39996","msg": "Access denied."} + '39997': BadSymbol, # {"code":39997,"msg":"Symbol not listed sMOVRUSDT","data":null} + }, + 'broad': { + '401 Insufficient privilege': PermissionDenied, # {"code": "401","msg": "401 Insufficient privilege."} + '401 Request IP mismatch': PermissionDenied, # {"code": "401","msg": "401 Request IP mismatch."} + 'Failed to find api-key': AuthenticationError, # {"msg":"Failed to find api-key 1c5ec63fd-660d-43ea-847a-0d3ba69e106e","code":10500} + 'Missing required parameter': BadRequest, # {"msg":"Missing required parameter","code":10500} + 'API Signature verification failed': AuthenticationError, # {"msg":"API Signature verification failed.","code":10500} + 'Api key not found': AuthenticationError, # {"msg":"Api key not found 698dc9e3-6faa-4910-9476-12857e79e198","code":"10500"} + }, + }, + 'options': { + 'brokerId': 'CCXT123456', # updated from CCXT to CCXT123456 + 'x-phemex-request-expiry': 60, # in seconds + 'createOrderByQuoteRequiresPrice': True, + 'networks': { + 'TRC20': 'TRX', + 'ERC20': 'ETH', + 'BEP20': 'BNB', + }, + 'defaultNetworks': { + 'USDT': 'ETH', + 'MKR': 'ETH', + }, + 'defaultSubType': 'linear', + 'accountsByType': { + 'spot': 'spot', + 'swap': 'future', + }, + 'stableCoins': [ + 'BUSD', + 'FEI', + 'TUSD', + 'USD', + 'USDC', + 'USDD', + 'USDP', + 'USDT', + ], + 'transfer': { + 'fillResponseFromRequest': True, + }, + 'triggerPriceTypesMap': { + 'last': 'ByLastPrice', + 'mark': 'ByMarkPrice', + 'index': 'ByIndexPrice', + 'ask': 'ByAskPrice', + 'bid': 'ByBidPrice', + }, + }, + }) + + def parse_safe_number(self, value=None): + if value is None: + return value + parts = value.split(',') + value = ''.join(parts) + parts = value.split(' ') + return self.safe_number(parts, 0) + + def parse_swap_market(self, market: dict): + # + # { + # "symbol":"BTCUSD", # + # "code":"1", + # "type":"Perpetual", + # "displaySymbol":"BTC / USD", + # "indexSymbol":".BTC", + # "markSymbol":".MBTC", + # "fundingRateSymbol":".BTCFR", + # "fundingRate8hSymbol":".BTCFR8H", + # "contractUnderlyingAssets":"USD", # or eg. `1000 SHIB` + # "settleCurrency":"BTC", + # "quoteCurrency":"USD", + # "contractSize":"1 USD", + # "lotSize":1, + # "tickSize":0.5, + # "priceScale":4, + # "ratioScale":8, + # "pricePrecision":1, + # "minPriceEp":5000, + # "maxPriceEp":10000000000, + # "maxOrderQty":1000000, + # "status":"Listed", + # "tipOrderQty":1000000, + # "listTime":"1574650800000", + # "majorSymbol":true, + # "steps":"50", + # "riskLimits":[ + # {"limit":100,"initialMargin":"1.0%","initialMarginEr":1000000,"maintenanceMargin":"0.5%","maintenanceMarginEr":500000}, + # {"limit":150,"initialMargin":"1.5%","initialMarginEr":1500000,"maintenanceMargin":"1.0%","maintenanceMarginEr":1000000}, + # {"limit":200,"initialMargin":"2.0%","initialMarginEr":2000000,"maintenanceMargin":"1.5%","maintenanceMarginEr":1500000}, + # ], + # "underlyingSymbol":".BTC", + # "baseCurrency":"BTC", + # "settlementCurrency":"BTC", + # "valueScale":8, + # "defaultLeverage":0, + # "maxLeverage":100, + # "initMarginEr":"1000000", + # "maintMarginEr":"500000", + # "defaultRiskLimitEv":10000000000, + # "deleverage":true, + # "makerFeeRateEr":-250000, + # "takerFeeRateEr":750000, + # "fundingInterval":8, + # "marketUrl":"https://phemex.com/trade/BTCUSD", + # "description":"BTCUSD is a BTC/USD perpetual contract priced on the .BTC Index. Each contract is worth 1 USD of Bitcoin. Funding is paid and received every 8 hours. At UTC time: 00:00, 08:00, 16:00.", + # } + # + id = self.safe_string(market, 'symbol') + contractUnderlyingAssets = self.safe_string(market, 'contractUnderlyingAssets') + baseId = self.safe_string(market, 'baseCurrency', contractUnderlyingAssets) + quoteId = self.safe_string(market, 'quoteCurrency') + settleId = self.safe_string(market, 'settleCurrency') + base = self.safe_currency_code(baseId) + base = base.replace(' ', '') # replace space for junction codes, eg. `1000 SHIB` + quote = self.safe_currency_code(quoteId) + settle = self.safe_currency_code(settleId) + inverse = False + if settleId != quoteId: + inverse = True + # some unhandled cases + if not ('baseCurrency' in market) and base == quote: + base = settle + priceScale = self.safe_integer(market, 'priceScale') + ratioScale = self.safe_integer(market, 'ratioScale') + valueScale = self.safe_integer(market, 'valueScale') + minPriceEp = self.safe_string(market, 'minPriceEp') + maxPriceEp = self.safe_string(market, 'maxPriceEp') + makerFeeRateEr = self.safe_string(market, 'makerFeeRateEr') + takerFeeRateEr = self.safe_string(market, 'takerFeeRateEr') + status = self.safe_string(market, 'status') + contractSizeString = self.safe_string(market, 'contractSize', ' ') + contractSize: Num = None + if settle == 'USDT': + contractSize = self.parse_number('1') + elif contractSizeString.find(' '): + # "1 USD" + # "0.005 ETH" + parts = contractSizeString.split(' ') + contractSize = self.parse_number(parts[0]) + else: + # "1.0" + contractSize = self.parse_number(contractSizeString) + return self.safe_market_structure({ + 'id': id, + 'symbol': base + '/' + quote + ':' + settle, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': 'swap', + 'spot': False, + 'margin': False, + 'swap': True, + 'future': False, + 'option': False, + 'active': status == 'Listed', + 'contract': True, + 'linear': not inverse, + 'inverse': inverse, + 'taker': self.parse_number(self.from_en(takerFeeRateEr, ratioScale)), + 'maker': self.parse_number(self.from_en(makerFeeRateEr, ratioScale)), + 'contractSize': contractSize, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'priceScale': priceScale, + 'valueScale': valueScale, + 'ratioScale': ratioScale, + 'precision': { + 'amount': self.safe_number_2(market, 'lotSize', 'qtyStepSize'), + 'price': self.safe_number(market, 'tickSize'), + }, + 'limits': { + 'leverage': { + 'min': self.parse_number('1'), + 'max': self.safe_number(market, 'maxLeverage'), + }, + 'amount': { + 'min': None, + 'max': None, + }, + 'price': { + 'min': self.parse_number(self.from_en(minPriceEp, priceScale)), + 'max': self.parse_number(self.from_en(maxPriceEp, priceScale)), + }, + 'cost': { + 'min': None, + 'max': self.parse_number(self.safe_string(market, 'maxOrderQty')), + }, + }, + 'created': None, + 'info': market, + }) + + def parse_spot_market(self, market: dict): + # + # { + # "symbol":"sBTCUSDT", + # "code":1001, + # "type":"Spot", + # "displaySymbol":"BTC / USDT", + # "quoteCurrency":"USDT", + # "priceScale":8, + # "ratioScale":8, + # "pricePrecision":2, + # "baseCurrency":"BTC", + # "baseTickSize":"0.000001 BTC", + # "baseTickSizeEv":100, + # "quoteTickSize":"0.01 USDT", + # "quoteTickSizeEv":1000000, + # "baseQtyPrecision":6, + # "quoteQtyPrecision":2, + # "minOrderValue":"10 USDT", + # "minOrderValueEv":1000000000, + # "maxBaseOrderSize":"1000 BTC", + # "maxBaseOrderSizeEv":100000000000, + # "maxOrderValue":"5,000,000 USDT", + # "maxOrderValueEv":500000000000000, + # "defaultTakerFee":"0.001", + # "defaultTakerFeeEr":100000, + # "defaultMakerFee":"0.001", + # "defaultMakerFeeEr":100000, + # "description":"BTCUSDT is a BTC/USDT spot trading pair. Minimum order value is 1 USDT", + # "status":"Listed", + # "tipOrderQty":2, + # "listTime":1589338800000, + # "buyPriceUpperLimitPct":110, + # "sellPriceLowerLimitPct":90, + # "leverage":5 + # }, + # + type = self.safe_string_lower(market, 'type') + id = self.safe_string(market, 'symbol') + quoteId = self.safe_string(market, 'quoteCurrency') + baseId = self.safe_string(market, 'baseCurrency') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + status = self.safe_string(market, 'status') + precisionAmount = self.parse_safe_number(self.safe_string(market, 'baseTickSize')) + precisionPrice = self.parse_safe_number(self.safe_string(market, 'quoteTickSize')) + return self.safe_market_structure({ + 'id': id, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': type, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': status == 'Listed', + 'contract': False, + 'linear': None, + 'inverse': None, + 'taker': self.safe_number(market, 'defaultTakerFee'), + 'maker': self.safe_number(market, 'defaultMakerFee'), + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'priceScale': self.safe_integer(market, 'priceScale'), + 'valueScale': self.safe_integer(market, 'valueScale'), + 'ratioScale': self.safe_integer(market, 'ratioScale'), + 'precision': { + 'amount': precisionAmount, + 'price': precisionPrice, + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': precisionAmount, + 'max': self.parse_safe_number(self.safe_string(market, 'maxBaseOrderSize')), + }, + 'price': { + 'min': precisionPrice, + 'max': None, + }, + 'cost': { + 'min': self.parse_safe_number(self.safe_string(market, 'minOrderValue')), + 'max': self.parse_safe_number(self.safe_string(market, 'maxOrderValue')), + }, + }, + 'created': self.safe_integer(market, 'listTime'), + 'info': market, + }) + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for phemex + + https://phemex-docs.github.io/#query-product-information-3 + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + v2ProductsPromise = self.v2GetPublicProducts(params) + # + # { + # "code":0, + # "msg":"", + # "data":{ + # "currencies":[ + # {"currency":"BTC","name":"Bitcoin","code":1,"valueScale":8,"minValueEv":1,"maxValueEv":5000000000000000000,"needAddrTag":0,"status":"Listed","displayCurrency":"BTC","inAssetsDisplay":1,"perpetual":0,"stableCoin":0,"assetsPrecision":8}, + # {"currency":"USD","name":"USD","code":2,"valueScale":4,"minValueEv":1,"maxValueEv":5000000000000000000,"needAddrTag":0,"status":"Listed","displayCurrency":"USD","inAssetsDisplay":1,"perpetual":0,"stableCoin":0,"assetsPrecision":2}, + # {"currency":"USDT","name":"TetherUS","code":3,"valueScale":8,"minValueEv":1,"maxValueEv":5000000000000000000,"needAddrTag":0,"status":"Listed","displayCurrency":"USDT","inAssetsDisplay":1,"perpetual":2,"stableCoin":1,"assetsPrecision":8}, + # ], + # "products":[ + # { + # "symbol":"BTCUSD", + # "code":1, + # "type":"Perpetual" + # "displaySymbol":"BTC / USD", + # "indexSymbol":".BTC", + # "markSymbol":".MBTC", + # "fundingRateSymbol":".BTCFR", + # "fundingRate8hSymbol":".BTCFR8H", + # "contractUnderlyingAssets":"USD", + # "settleCurrency":"BTC", + # "quoteCurrency":"USD", + # "contractSize":1.0, + # "lotSize":1, + # "tickSize":0.5, + # "priceScale":4, + # "ratioScale":8, + # "pricePrecision":1, + # "minPriceEp":5000, + # "maxPriceEp":10000000000, + # "maxOrderQty":1000000, + # "description":"BTC/USD perpetual contracts are priced on the .BTC Index. Each contract is worth 1 USD. Funding fees are paid and received every 8 hours at UTC time: 00:00, 08:00 and 16:00.", + # "status":"Listed", + # "tipOrderQty":1000000, + # "listTime":1574650800000, + # "majorSymbol":true, + # "defaultLeverage":"-10", + # "fundingInterval":28800, + # "maxLeverage":100 + # }, + # { + # "symbol":"sBTCUSDT", + # "code":1001, + # "type":"Spot", + # "displaySymbol":"BTC / USDT", + # "quoteCurrency":"USDT", + # "priceScale":8, + # "ratioScale":8, + # "pricePrecision":2, + # "baseCurrency":"BTC", + # "baseTickSize":"0.000001 BTC", + # "baseTickSizeEv":100, + # "quoteTickSize":"0.01 USDT", + # "quoteTickSizeEv":1000000, + # "baseQtyPrecision":6, + # "quoteQtyPrecision":2, + # "minOrderValue":"10 USDT", + # "minOrderValueEv":1000000000, + # "maxBaseOrderSize":"1000 BTC", + # "maxBaseOrderSizeEv":100000000000, + # "maxOrderValue":"5,000,000 USDT", + # "maxOrderValueEv":500000000000000, + # "defaultTakerFee":"0.001", + # "defaultTakerFeeEr":100000, + # "defaultMakerFee":"0.001", + # "defaultMakerFeeEr":100000, + # "description":"BTCUSDT is a BTC/USDT spot trading pair. Minimum order value is 1 USDT", + # "status":"Listed", + # "tipOrderQty":2, + # "listTime":1589338800000, + # "buyPriceUpperLimitPct":110, + # "sellPriceLowerLimitPct":90, + # "leverage":5 + # }, + # ], + # "perpProductsV2":[ + # { + # "symbol":"BTCUSDT", + # "code":41541, + # "type":"PerpetualV2", + # "displaySymbol":"BTC / USDT", + # "indexSymbol":".BTCUSDT", + # "markSymbol":".MBTCUSDT", + # "fundingRateSymbol":".BTCUSDTFR", + # "fundingRate8hSymbol":".BTCUSDTFR8H", + # "contractUnderlyingAssets":"BTC", + # "settleCurrency":"USDT", + # "quoteCurrency":"USDT", + # "tickSize":"0.1", + # "priceScale":0, + # "ratioScale":0, + # "pricePrecision":1, + # "baseCurrency":"BTC", + # "description":"BTC/USDT perpetual contracts are priced on the .BTCUSDT Index. Each contract is worth 1 BTC. Funding fees are paid and received every 8 hours at UTC time: 00:00, 08:00 and 16:00.", + # "status":"Listed", + # "tipOrderQty":0, + # "listTime":1668225600000, + # "majorSymbol":true, + # "defaultLeverage":"-10", + # "fundingInterval":28800, + # "maxLeverage":100, + # "maxOrderQtyRq":"1000", + # "maxPriceRp":"2000000000", + # "minOrderValueRv":"1", + # "minPriceRp":"1000.0", + # "qtyPrecision":3, + # "qtyStepSize":"0.001", + # "tipOrderQtyRq":"200", + # "maxOpenPosLeverage":100.0 + # }, + # ], + # "riskLimits":[ + # { + # "symbol":"BTCUSD", + # "steps":"50", + # "riskLimits":[ + # {"limit":100,"initialMargin":"1.0%","initialMarginEr":1000000,"maintenanceMargin":"0.5%","maintenanceMarginEr":500000}, + # {"limit":150,"initialMargin":"1.5%","initialMarginEr":1500000,"maintenanceMargin":"1.0%","maintenanceMarginEr":1000000}, + # {"limit":200,"initialMargin":"2.0%","initialMarginEr":2000000,"maintenanceMargin":"1.5%","maintenanceMarginEr":1500000}, + # ] + # }, + # ], + # "leverages":[ + # {"initialMargin":"1.0%","initialMarginEr":1000000,"options":[1,2,3,5,10,25,50,100]}, + # {"initialMargin":"1.5%","initialMarginEr":1500000,"options":[1,2,3,5,10,25,50,66]}, + # {"initialMargin":"2.0%","initialMarginEr":2000000,"options":[1,2,3,5,10,25,33,50]}, + # ], + # "riskLimitsV2":[ + # { + # "symbol":"BTCUSDT", + # "steps":"2000K", + # "riskLimits":[ + # {"limit":2000000,"initialMarginRr":"0.01","maintenanceMarginRr":"0.005"},, + # {"limit":4000000,"initialMarginRr":"0.015","maintenanceMarginRr":"0.0075"}, + # {"limit":6000000,"initialMarginRr":"0.02","maintenanceMarginRr":"0.01"}, + # ] + # }, + # ], + # "leveragesV2":[ + # {"options":[1.0,2.0,3.0,5.0,10.0,25.0,50.0,100.0],"initialMarginRr":"0.01"}, + # {"options":[1.0,2.0,3.0,5.0,10.0,25.0,50.0,66.67],"initialMarginRr":"0.015"}, + # {"options":[1.0,2.0,3.0,5.0,10.0,25.0,33.0,50.0],"initialMarginRr":"0.02"}, + # ], + # "ratioScale":8, + # "md5Checksum":"5c6604814d3c1bafbe602c3d11a7e8bf", + # } + # } + # + v1ProductsPromise = self.v1GetExchangePublicProducts(params) + v2Products, v1Products = await asyncio.gather(*[v2ProductsPromise, v1ProductsPromise]) + v1ProductsData = self.safe_value(v1Products, 'data', []) + # + # { + # "code":0, + # "msg":"OK", + # "data":[ + # { + # "symbol":"BTCUSD", + # "underlyingSymbol":".BTC", + # "quoteCurrency":"USD", + # "baseCurrency":"BTC", + # "settlementCurrency":"BTC", + # "maxOrderQty":1000000, + # "maxPriceEp":100000000000000, + # "lotSize":1, + # "tickSize":"0.5", + # "contractSize":"1 USD", + # "priceScale":4, + # "ratioScale":8, + # "valueScale":8, + # "defaultLeverage":0, + # "maxLeverage":100, + # "initMarginEr":"1000000", + # "maintMarginEr":"500000", + # "defaultRiskLimitEv":10000000000, + # "deleverage":true, + # "makerFeeRateEr":-250000, + # "takerFeeRateEr":750000, + # "fundingInterval":8, + # "marketUrl":"https://phemex.com/trade/BTCUSD", + # "description":"BTCUSD is a BTC/USD perpetual contract priced on the .BTC Index. Each contract is worth 1 USD of Bitcoin. Funding is paid and received every 8 hours. At UTC time: 00:00, 08:00, 16:00.", + # "type":"Perpetual" + # }, + # ] + # } + # + v2ProductsData = self.safe_dict(v2Products, 'data', {}) + products = self.safe_list(v2ProductsData, 'products', []) + perpetualProductsV2 = self.safe_list(v2ProductsData, 'perpProductsV2', []) + products = self.array_concat(products, perpetualProductsV2) + riskLimits = self.safe_list(v2ProductsData, 'riskLimits', []) + riskLimitsV2 = self.safe_list(v2ProductsData, 'riskLimitsV2', []) + riskLimits = self.array_concat(riskLimits, riskLimitsV2) + currencies = self.safe_list(v2ProductsData, 'currencies', []) + riskLimitsById = self.index_by(riskLimits, 'symbol') + v1ProductsById = self.index_by(v1ProductsData, 'symbol') + currenciesByCode = self.index_by(currencies, 'currency') + result = [] + for i in range(0, len(products)): + market = products[i] + type = self.safe_string_lower(market, 'type') + if (type == 'perpetual') or (type == 'perpetualv2') or (type == 'perpetualpilot'): + id = self.safe_string(market, 'symbol') + riskLimitValues = self.safe_dict(riskLimitsById, id, {}) + market = self.extend(market, riskLimitValues) + v1ProductsValues = self.safe_dict(v1ProductsById, id, {}) + market = self.extend(market, v1ProductsValues) + market = self.parse_swap_market(market) + else: + baseCurrency = self.safe_string(market, 'baseCurrency') + currencyValues = self.safe_dict(currenciesByCode, baseCurrency, {}) + valueScale = self.safe_string(currencyValues, 'valueScale', '8') + market = self.extend(market, {'valueScale': valueScale}) + market = self.parse_spot_market(market) + result.append(market) + return result + + async def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = await self.v2GetPublicProducts(params) + # + # { + # "code":0, + # "msg":"OK", + # "data":{ + # ..., + # "currencies":[ + # {"currency":"BTC","name":"Bitcoin","code":1,"valueScale":8,"minValueEv":1,"maxValueEv":5000000000000000000,"needAddrTag":0,"status":"Listed","displayCurrency":"BTC","inAssetsDisplay":1,"perpetual":0,"stableCoin":0,"assetsPrecision":8}, + # {"currency":"USD","name":"USD","code":2,"valueScale":4,"minValueEv":1,"maxValueEv":5000000000000000000,"needAddrTag":0,"status":"Listed","displayCurrency":"USD","inAssetsDisplay":1,"perpetual":0,"stableCoin":0,"assetsPrecision":2}, + # {"currency":"USDT","name":"TetherUS","code":3,"valueScale":8,"minValueEv":1,"maxValueEv":5000000000000000000,"needAddrTag":0,"status":"Listed","displayCurrency":"USDT","inAssetsDisplay":1,"perpetual":2,"stableCoin":1,"assetsPrecision":8}, + # ], + # ... + # } + # } + data = self.safe_value(response, 'data', {}) + currencies = self.safe_value(data, 'currencies', []) + result: dict = {} + for i in range(0, len(currencies)): + currency = currencies[i] + id = self.safe_string(currency, 'currency') + code = self.safe_currency_code(id) + valueScaleString = self.safe_string(currency, 'valueScale') + valueScale = int(valueScaleString) + minValueEv = self.safe_string(currency, 'minValueEv') + maxValueEv = self.safe_string(currency, 'maxValueEv') + minAmount: Num = None + maxAmount: Num = None + precision: Num = None + if valueScale is not None: + precisionString = self.parse_precision(valueScaleString) + precision = self.parse_number(precisionString) + minAmount = self.parse_number(Precise.string_mul(minValueEv, precisionString)) + maxAmount = self.parse_number(Precise.string_mul(maxValueEv, precisionString)) + result[code] = self.safe_currency_structure({ + 'id': id, + 'info': currency, + 'code': code, + 'name': self.safe_string(currency, 'name'), + 'active': self.safe_string(currency, 'status') == 'Listed', + 'deposit': None, + 'withdraw': None, + 'fee': None, + 'precision': precision, + 'limits': { + 'amount': { + 'min': minAmount, + 'max': maxAmount, + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + }, + 'valueScale': valueScale, + 'networks': None, + 'type': 'crypto', + }) + return result + + def custom_parse_bid_ask(self, bidask, priceKey=0, amountKey=1, market: Market = None): + if market is None: + raise ArgumentsRequired(self.id + ' customParseBidAsk() requires a market argument') + amount = self.safe_string(bidask, amountKey) + if market['spot']: + amount = self.from_ev(amount, market) + return [ + self.parse_number(self.from_ep(self.safe_string(bidask, priceKey), market)), + self.parse_number(amount), + ] + + def custom_parse_order_book(self, orderbook, symbol, timestamp=None, bidsKey='bids', asksKey='asks', priceKey=0, amountKey=1, market: Market = None): + result: dict = { + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'nonce': None, + } + sides = [bidsKey, asksKey] + for i in range(0, len(sides)): + side = sides[i] + orders = [] + bidasks = self.safe_value(orderbook, side) + for k in range(0, len(bidasks)): + orders.append(self.custom_parse_bid_ask(bidasks[k], priceKey, amountKey, market)) + result[side] = orders + result[bidsKey] = self.sort_by(result[bidsKey], 0, True) + result[asksKey] = self.sort_by(result[asksKey], 0) + return result + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://github.com/phemex/phemex-api-docs/blob/master/Public-Hedged-Perpetual-API.md#queryorderbook + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + # 'id': 123456789, # optional request id + } + response = None + isStableSettled = (market['settle'] == 'USDT') or (market['settle'] == 'USDC') + if market['linear'] and isStableSettled: + response = await self.v2GetMdV2Orderbook(self.extend(request, params)) + else: + if (limit is not None) and (limit <= 30): + response = await self.v1GetMdOrderbook(self.extend(request, params)) + else: + response = await self.v1GetMdFullbook(self.extend(request, params)) + # + # { + # "error": null, + # "id": 0, + # "result": { + # "book": { + # "asks": [ + # [23415000000, 105262000], + # [23416000000, 147914000], + # [23419000000, 160914000], + # ], + # "bids": [ + # [23360000000, 32995000], + # [23359000000, 221887000], + # [23356000000, 284599000], + # ], + # }, + # "depth": 30, + # "sequence": 1592059928, + # "symbol": "sETHUSDT", + # "timestamp": 1592387340020000955, + # "type": "snapshot" + # } + # } + # + result = self.safe_value(response, 'result', {}) + book = self.safe_value_2(result, 'book', 'orderbook_p', {}) + timestamp = self.safe_integer_product(result, 'timestamp', 0.000001) + orderbook = self.custom_parse_order_book(book, symbol, timestamp, 'bids', 'asks', 0, 1, market) + orderbook['nonce'] = self.safe_integer(result, 'sequence') + return orderbook + + def to_en(self, n, scale): + stringN = self.number_to_string(n) + precise = Precise(stringN) + precise.decimals = precise.decimals - scale + precise.reduce() + preciseString = str(precise) + return self.parse_to_numeric(preciseString) + + def to_ev(self, amount, market: dict = None): + if (amount is None) or (market is None): + return amount + return self.to_en(amount, market['valueScale']) + + def to_ep(self, price, market: Market = None): + if (price is None) or (market is None): + return price + return self.to_en(price, market['priceScale']) + + def from_en(self, en, scale): + if en is None or scale is None: + return None + precise = Precise(en) + precise.decimals = self.sum(precise.decimals, scale) + precise.reduce() + return str(precise) + + def from_ep(self, ep, market: Market = None): + if (ep is None) or (market is None): + return ep + return self.from_en(ep, self.safe_integer(market, 'priceScale')) + + def from_ev(self, ev, market: Market = None): + if (ev is None) or (market is None): + return ev + return self.from_en(ev, self.safe_integer(market, 'valueScale')) + + def from_er(self, er, market: Market = None): + if (er is None) or (market is None): + return er + return self.from_en(er, self.safe_integer(market, 'ratioScale')) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # [ + # 1592467200, # timestamp + # 300, # interval + # 23376000000, # last + # 23322000000, # open + # 23381000000, # high + # 23315000000, # low + # 23367000000, # close + # 208671000, # base volume + # 48759063370, # quote volume + # ] + # + baseVolume: Num + if (market is not None) and market['spot']: + baseVolume = self.parse_number(self.from_ev(self.safe_string(ohlcv, 7), market)) + else: + baseVolume = self.safe_number(ohlcv, 7) + return [ + self.safe_timestamp(ohlcv, 0), + self.parse_number(self.from_ep(self.safe_string(ohlcv, 3), market)), + self.parse_number(self.from_ep(self.safe_string(ohlcv, 4), market)), + self.parse_number(self.from_ep(self.safe_string(ohlcv, 5), market)), + self.parse_number(self.from_ep(self.safe_string(ohlcv, 6), market)), + baseVolume, + ] + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://github.com/phemex/phemex-api-docs/blob/master/Public-Hedged-Perpetual-API.md#querykline + https://github.com/phemex/phemex-api-docs/blob/master/Public-Contract-API-en.md#query-kline + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: *only used for USDT settled contracts, otherwise is emulated and not supported by the exchange* timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: *USDT settled/ linear swaps only* end time in ms + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + userLimit = limit + request: dict = { + 'symbol': market['id'], + 'resolution': self.safe_string(self.timeframes, timeframe, timeframe), + } + until = self.safe_integer_2(params, 'until', 'to') + params = self.omit(params, ['until']) + isStableSettled = (market['settle'] == 'USDT') or (market['settle'] == 'USDC') + usesSpecialFromToEndpoint = ((market['linear'] or isStableSettled)) and ((since is not None) or (until is not None)) + maxLimit = 1000 + if usesSpecialFromToEndpoint: + maxLimit = 2000 + if limit is None: + limit = maxLimit + request['limit'] = min(limit, maxLimit) + response = None + if market['linear'] or isStableSettled: + if (until is not None) or (since is not None): + candleDuration = self.parse_timeframe(timeframe) + if since is not None: + since = int(round(since / 1000)) + request['from'] = since + else: + # when 'to' is defined since is mandatory + since = (until / 100) - (maxLimit * candleDuration) + if until is not None: + request['to'] = int(round(until / 1000)) + else: + # when since is defined 'to' is mandatory + to = since + (maxLimit * candleDuration) + now = self.seconds() + if to > now: + to = now + request['to'] = to + response = await self.publicGetMdV2KlineList(self.extend(request, params)) + else: + response = await self.publicGetMdV2KlineLast(self.extend(request, params)) + else: + if since is not None: + # phemex also provides kline query with from/to, however, self interface is NOT recommended and does not work properly. + # we do not send since param to the exchange, instead we calculate appropriate limit param + duration = self.parse_timeframe(timeframe) * 1000 + timeDelta = self.milliseconds() - since + limit = self.parse_to_int(timeDelta / duration) # setting limit to the number of candles after since + response = await self.publicGetMdV2Kline(self.extend(request, params)) + # + # { + # "code":0, + # "msg":"OK", + # "data":{ + # "total":-1, + # "rows":[ + # [1592467200,300,23376000000,23322000000,23381000000,23315000000,23367000000,208671000,48759063370], + # [1592467500,300,23367000000,23314000000,23390000000,23311000000,23331000000,234820000,54848948710], + # [1592467800,300,23331000000,23385000000,23391000000,23326000000,23387000000,152931000,35747882250], + # ] + # } + # } + # + data = self.safe_value(response, 'data', {}) + rows = self.safe_list(data, 'rows', []) + return self.parse_ohlcvs(rows, market, timeframe, since, userLimit) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # spot + # + # { + # "askEp": 943836000000, + # "bidEp": 943601000000, + # "highEp": 955946000000, + # "lastEp": 943803000000, + # "lowEp": 924973000000, + # "openEp": 948693000000, + # "symbol": "sBTCUSDT", + # "timestamp": 1592471203505728630, + # "turnoverEv": 111822826123103, + # "volumeEv": 11880532281 + # } + # + # swap + # + # { + # "askEp": 2332500, + # "bidEp": 2331000, + # "fundingRateEr": 10000, + # "highEp": 2380000, + # "indexEp": 2329057, + # "lastEp": 2331500, + # "lowEp": 2274000, + # "markEp": 2329232, + # "openEp": 2337500, + # "openInterest": 1298050, + # "predFundingRateEr": 19921, + # "symbol": "ETHUSD", + # "timestamp": 1592474241582701416, + # "turnoverEv": 47228362330, + # "volume": 4053863 + # } + # linear swap v2 + # + # { + # "closeRp":"16820.5", + # "fundingRateRr":"0.0001", + # "highRp":"16962.1", + # "indexPriceRp":"16830.15651565", + # "lowRp":"16785", + # "markPriceRp":"16830.97534951", + # "openInterestRv":"1323.596", + # "openRp":"16851.7", + # "predFundingRateRr":"0.0001", + # "symbol":"BTCUSDT", + # "timestamp":"1672142789065593096", + # "turnoverRv":"124835296.0538", + # "volumeRq":"7406.95" + # } + # + marketId = self.safe_string(ticker, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + timestamp = self.safe_integer_product(ticker, 'timestamp', 0.000001) + last = self.from_ep(self.safe_string_2(ticker, 'lastEp', 'closeRp'), market) + quoteVolume = self.from_er(self.safe_string_2(ticker, 'turnoverEv', 'turnoverRv'), market) + baseVolume = self.safe_string(ticker, 'volume') + if baseVolume is None: + baseVolume = self.from_ev(self.safe_string_2(ticker, 'volumeEv', 'volumeRq'), market) + open = self.from_ep(self.safe_string(ticker, 'openEp'), market) + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.from_ep(self.safe_string_2(ticker, 'highEp', 'highRp'), market), + 'low': self.from_ep(self.safe_string_2(ticker, 'lowEp', 'lowRp'), market), + 'bid': self.from_ep(self.safe_string(ticker, 'bidEp'), market), + 'bidVolume': None, + 'ask': self.from_ep(self.safe_string(ticker, 'askEp'), market), + 'askVolume': None, + 'vwap': None, + 'open': open, + 'close': last, + 'last': last, + 'previousClose': None, # previous day close + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'info': ticker, + }, market) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://github.com/phemex/phemex-api-docs/blob/master/Public-Hedged-Perpetual-API.md#query24hrsticker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + # 'id': 123456789, # optional request id + } + response = None + if market['swap']: + if market['inverse'] or market['settle'] == 'USD': + response = await self.v1GetMdTicker24hr(self.extend(request, params)) + else: + response = await self.v2GetMdV2Ticker24hr(self.extend(request, params)) + else: + response = await self.v1GetMdSpotTicker24hr(self.extend(request, params)) + # + # spot + # + # { + # "error": null, + # "id": 0, + # "result": { + # "askEp": 943836000000, + # "bidEp": 943601000000, + # "highEp": 955946000000, + # "lastEp": 943803000000, + # "lowEp": 924973000000, + # "openEp": 948693000000, + # "symbol": "sBTCUSDT", + # "timestamp": 1592471203505728630, + # "turnoverEv": 111822826123103, + # "volumeEv": 11880532281 + # } + # } + # + # swap + # + # { + # "error": null, + # "id": 0, + # "result": { + # "askEp": 2332500, + # "bidEp": 2331000, + # "fundingRateEr": 10000, + # "highEp": 2380000, + # "indexEp": 2329057, + # "lastEp": 2331500, + # "lowEp": 2274000, + # "markEp": 2329232, + # "openEp": 2337500, + # "openInterest": 1298050, + # "predFundingRateEr": 19921, + # "symbol": "ETHUSD", + # "timestamp": 1592474241582701416, + # "turnoverEv": 47228362330, + # "volume": 4053863 + # } + # } + # + result = self.safe_dict(response, 'result', {}) + return self.parse_ticker(result, market) + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://phemex-docs.github.io/#query-24-hours-ticker-for-all-symbols-2 # spot + https://phemex-docs.github.io/#query-24-ticker-for-all-symbols # linear + https://phemex-docs.github.io/#query-24-hours-ticker-for-all-symbols # inverse + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + market: Market = None + if symbols is not None: + first = self.safe_value(symbols, 0) + market = self.market(first) + type = None + type, params = self.handle_market_type_and_params('fetchTickers', market, params) + subType = None + subType, params = self.handle_sub_type_and_params('fetchTickers', market, params) + query = self.omit(params, 'type') + response = None + if type == 'spot': + response = await self.v1GetMdSpotTicker24hrAll(query) + elif subType == 'inverse' or self.safe_string(market, 'settle') == 'USD': + response = await self.v1GetMdTicker24hrAll(query) + else: + response = await self.v2GetMdV2Ticker24hrAll(query) + result = self.safe_list(response, 'result', []) + return self.parse_tickers(result, symbols) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://github.com/phemex/phemex-api-docs/blob/master/Public-Hedged-Perpetual-API.md#querytrades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + # 'id': 123456789, # optional request id + } + response = None + isStableSettled = (market['settle'] == 'USDT') or (market['settle'] == 'USDC') + if market['linear'] and isStableSettled: + response = await self.v2GetMdV2Trade(self.extend(request, params)) + else: + response = await self.v1GetMdTrade(self.extend(request, params)) + # + # { + # "error": null, + # "id": 0, + # "result": { + # "sequence": 1315644947, + # "symbol": "BTCUSD", + # "trades": [ + # [1592541746712239749, 13156448570000, "Buy", 93070000, 40173], + # [1592541740434625085, 13156447110000, "Sell", 93065000, 5000], + # [1592541732958241616, 13156441390000, "Buy", 93070000, 3460], + # ], + # "type": "snapshot" + # } + # } + # + result = self.safe_value(response, 'result', {}) + trades = self.safe_value_2(result, 'trades', 'trades_p', []) + return self.parse_trades(trades, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(public) spot & contract + # + # [ + # 1592541746712239749, + # 13156448570000, + # "Buy", + # 93070000, + # 40173 + # ] + # + # fetchTrades(public) perp + # + # [ + # 1675690986063435800, + # "Sell", + # "22857.4", + # "0.269" + # ] + # + # fetchMyTrades(private) + # + # spot + # + # { + # "qtyType": "ByQuote", + # "transactTimeNs": 1589450974800550100, + # "clOrdID": "8ba59d40-df25-d4b0-14cf-0703f44e9690", + # "orderID": "b2b7018d-f02f-4c59-b4cf-051b9c2d2e83", + # "symbol": "sBTCUSDT", + # "side": "Buy", + # "priceEP": 970056000000, + # "baseQtyEv": 0, + # "quoteQtyEv": 1000000000, + # "action": "New", + # "execStatus": "MakerFill", + # "ordStatus": "Filled", + # "ordType": "Limit", + # "execInst": "None", + # "timeInForce": "GoodTillCancel", + # "stopDirection": "UNSPECIFIED", + # "tradeType": "Trade", + # "stopPxEp": 0, + # "execId": "c6bd8979-07ba-5946-b07e-f8b65135dbb1", + # "execPriceEp": 970056000000, + # "execBaseQtyEv": 103000, + # "execQuoteQtyEv": 999157680, + # "leavesBaseQtyEv": 0, + # "leavesQuoteQtyEv": 0, + # "execFeeEv": 0, + # "feeRateEr": 0 + # "baseCurrency": "BTC", + # "quoteCurrency": "USDT", + # "feeCurrency": "BTC" + # } + # + # swap + # + # { + # "transactTimeNs": 1578026629824704800, + # "symbol": "BTCUSD", + # "currency": "BTC", + # "action": "Replace", + # "side": "Sell", + # "tradeType": "Trade", + # "execQty": 700, + # "execPriceEp": 71500000, + # "orderQty": 700, + # "priceEp": 71500000, + # "execValueEv": 9790209, + # "feeRateEr": -25000, + # "execFeeEv": -2447, + # "ordType": "Limit", + # "execID": "b01671a1-5ddc-5def-b80a-5311522fd4bf", + # "orderID": "b63bc982-be3a-45e0-8974-43d6375fb626", + # "clOrdID": "uuid-1577463487504", + # "execStatus": "MakerFill" + # } + # perpetual + # { + # "accountID": 9328670003, + # "action": "New", + # "actionBy": "ByUser", + # "actionTimeNs": 1666858780876924611, + # "addedSeq": 77751555, + # "apRp": "0", + # "bonusChangedAmountRv": "0", + # "bpRp": "0", + # "clOrdID": "c0327a7d-9064-62a9-28f6-2db9aaaa04e0", + # "closedPnlRv": "0", + # "closedSize": "0", + # "code": 0, + # "cumFeeRv": "0", + # "cumQty": "0", + # "cumValueRv": "0", + # "curAccBalanceRv": "1508.489893982237", + # "curAssignedPosBalanceRv": "24.62786650928", + # "curBonusBalanceRv": "0", + # "curLeverageRr": "-10", + # "curPosSide": "Buy", + # "curPosSize": "0.043", + # "curPosTerm": 1, + # "curPosValueRv": "894.0689", + # "curRiskLimitRv": "1000000", + # "currency": "USDT", + # "cxlRejReason": 0, + # "displayQty": "0.003", + # "execFeeRv": "0", + # "execID": "00000000-0000-0000-0000-000000000000", + # "execPriceRp": "20723.7", + # "execQty": "0", + # "execSeq": 77751555, + # "execStatus": "New", + # "execValueRv": "0", + # "feeRateRr": "0", + # "leavesQty": "0.003", + # "leavesValueRv": "63.4503", + # "message": "No error", + # "ordStatus": "New", + # "ordType": "Market", + # "orderID": "fa64c6f2-47a4-4929-aab4-b7fa9bbc4323", + # "orderQty": "0.003", + # "pegOffsetValueRp": "0", + # "posSide": "Long", + # "priceRp": "21150.1", + # "relatedPosTerm": 1, + # "relatedReqNum": 11, + # "side": "Buy", + # "slTrigger": "ByMarkPrice", + # "stopLossRp": "0", + # "stopPxRp": "0", + # "symbol": "BTCUSDT", + # "takeProfitRp": "0", + # "timeInForce": "ImmediateOrCancel", + # "tpTrigger": "ByLastPrice", + # "tradeType": "Amend", + # "transactTimeNs": 1666858780881545305, + # "userID": 932867 + # } + # + # swap - USDT + # + # { + # "createdAt": 1666226932259, + # "symbol": "ETHUSDT", + # "currency": "USDT", + # "action": 1, + # "tradeType": 1, + # "execQtyRq": "0.01", + # "execPriceRp": "1271.9", + # "side": 1, + # "orderQtyRq": "0.78", + # "priceRp": "1271.9", + # "execValueRv": "12.719", + # "feeRateRr": "0.0001", + # "execFeeRv": "0.0012719", + # "ordType": 2, + # "execId": "8718cae", + # "execStatus": 6 + # } + # spot with fees paid using PT token + # "createdAt": "1714990724076", + # "symbol": "BTCUSDT", + # "currency": "USDT", + # "action": "1", + # "tradeType": "1", + # "execQtyRq": "0.003", + # "execPriceRp": "64935", + # "side": "2", + # "orderQtyRq": "0.003", + # "priceRp": "51600", + # "execValueRv": "194.805", + # "feeRateRr": "0.000495", + # "execFeeRv": "0", + # "ordType": "3", + # "execId": "XXXXXX", + # "execStatus": "7", + # "posSide": "1", + # "ptFeeRv": "0.110012249248", + # "ptPriceRp": "0.876524893" + # + priceString: Str + amountString: Str + timestamp: Int + id: Str = None + side: Str = None + costString: Str = None + type: Str = None + fee = None + feeCostString: Str = None + feeRateString: Str = None + feeCurrencyCode: Str = None + marketId = self.safe_string(trade, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + orderId: Str = None + takerOrMaker: Str = None + if isinstance(trade, list): + tradeLength = len(trade) + timestamp = self.safe_integer_product(trade, 0, 0.000001) + if tradeLength > 4: + id = self.safe_string(trade, tradeLength - 4) + side = self.safe_string_lower(trade, tradeLength - 3) + priceString = self.safe_string(trade, tradeLength - 2) + amountString = self.safe_string(trade, tradeLength - 1) + if isinstance(trade[tradeLength - 2], numbers.Real): + priceString = self.from_ep(priceString, market) + amountString = self.from_ev(amountString, market) + else: + timestamp = self.safe_integer_product(trade, 'transactTimeNs', 0.000001) + if timestamp is None: + timestamp = self.safe_integer(trade, 'createdAt') + id = self.safe_string_2(trade, 'execId', 'execID') + orderId = self.safe_string(trade, 'orderID') + if market['settle'] == 'USDT' or market['settle'] == 'USDC': + sideId = self.safe_string_lower(trade, 'side') + if (sideId == 'buy') or (sideId == 'sell'): + side = sideId + elif sideId is not None: + side = 'buy' if (sideId == '1') else 'sell' + ordType = self.safe_string(trade, 'ordType') + if ordType == '1': + type = 'market' + elif ordType == '2': + type = 'limit' + priceString = self.safe_string(trade, 'execPriceRp') + amountString = self.safe_string(trade, 'execQtyRq') + costString = self.safe_string(trade, 'execValueRv') + feeCostString = self.omit_zero(self.safe_string(trade, 'execFeeRv')) + feeRateString = self.safe_string(trade, 'feeRateRr') + if feeCostString is not None: + currencyId = self.safe_string(trade, 'currency') + feeCurrencyCode = self.safe_currency_code(currencyId) + else: + ptFeeRv = self.omit_zero(self.safe_string(trade, 'ptFeeRv')) + if ptFeeRv is not None: + feeCostString = ptFeeRv + feeCurrencyCode = 'PT' + else: + side = self.safe_string_lower(trade, 'side') + type = self.parse_order_type(self.safe_string(trade, 'ordType')) + execStatus = self.safe_string(trade, 'execStatus') + if execStatus == 'MakerFill': + takerOrMaker = 'maker' + priceString = self.from_ep(self.safe_string(trade, 'execPriceEp'), market) + amountString = self.from_ev(self.safe_string(trade, 'execBaseQtyEv'), market) + amountString = self.safe_string(trade, 'execQty', amountString) + costString = self.from_er(self.safe_string_2(trade, 'execQuoteQtyEv', 'execValueEv'), market) + feeCostString = self.from_er(self.omit_zero(self.safe_string(trade, 'execFeeEv')), market) + if feeCostString is not None: + feeRateString = self.from_er(self.safe_string(trade, 'feeRateEr'), market) + if market['spot']: + feeCurrencyCode = self.safe_currency_code(self.safe_string(trade, 'feeCurrency')) + else: + info = self.safe_value(market, 'info') + if info is not None: + settlementCurrencyId = self.safe_string(info, 'settlementCurrency') + feeCurrencyCode = self.safe_currency_code(settlementCurrencyId) + else: + feeCostString = self.safe_string(trade, 'ptFeeRv') + if feeCostString is not None: + feeCurrencyCode = 'PT' + fee = { + 'cost': feeCostString, + 'rate': feeRateString, + 'currency': feeCurrencyCode, + } + return self.safe_trade({ + 'info': trade, + 'id': id, + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'order': orderId, + 'type': type, + 'side': side, + 'takerOrMaker': takerOrMaker, + 'price': priceString, + 'amount': amountString, + 'cost': costString, + 'fee': fee, + }, market) + + def parse_spot_balance(self, response): + # + # { + # "code":0, + # "msg":"", + # "data":[ + # { + # "currency":"USDT", + # "balanceEv":0, + # "lockedTradingBalanceEv":0, + # "lockedWithdrawEv":0, + # "lastUpdateTimeNs":1592065834511322514, + # "walletVid":0 + # }, + # { + # "currency":"ETH", + # "balanceEv":0, + # "lockedTradingBalanceEv":0, + # "lockedWithdrawEv":0, + # "lastUpdateTimeNs":1592065834511322514, + # "walletVid":0 + # } + # ] + # } + # + timestamp = None + result: dict = {'info': response} + data = self.safe_value(response, 'data', []) + for i in range(0, len(data)): + balance = data[i] + currencyId = self.safe_string(balance, 'currency') + code = self.safe_currency_code(currencyId) + currency = self.safe_value(self.currencies, code, {}) + scale = self.safe_integer(currency, 'valueScale', 8) + account = self.account() + balanceEv = self.safe_string(balance, 'balanceEv') + lockedTradingBalanceEv = self.safe_string(balance, 'lockedTradingBalanceEv') + lockedWithdrawEv = self.safe_string(balance, 'lockedWithdrawEv') + total = self.from_en(balanceEv, scale) + lockedTradingBalance = self.from_en(lockedTradingBalanceEv, scale) + lockedWithdraw = self.from_en(lockedWithdrawEv, scale) + used = Precise.string_add(lockedTradingBalance, lockedWithdraw) + lastUpdateTimeNs = self.safe_integer_product(balance, 'lastUpdateTimeNs', 0.000001) + timestamp = lastUpdateTimeNs if (timestamp is None) else max(timestamp, lastUpdateTimeNs) + account['total'] = total + account['used'] = used + result[code] = account + result['timestamp'] = timestamp + result['datetime'] = self.iso8601(timestamp) + return self.safe_balance(result) + + def parse_swap_balance(self, response): + # usdt + # { + # "info": { + # "code": "0", + # "msg": '', + # "data": { + # "account": { + # "userID": "940666", + # "accountId": "9406660003", + # "currency": "USDT", + # "accountBalanceRv": "99.93143972", + # "totalUsedBalanceRv": "0.40456", + # "bonusBalanceRv": "0" + # }, + # } + # + # { + # "code":0, + # "msg":"", + # "data":{ + # "account":{ + # "accountId":6192120001, + # "currency":"BTC", + # "accountBalanceEv":1254744, + # "totalUsedBalanceEv":0, + # "bonusBalanceEv":1254744 + # } + # } + # } + # + result: dict = {'info': response} + data = self.safe_value(response, 'data', {}) + balance = self.safe_value(data, 'account', {}) + currencyId = self.safe_string(balance, 'currency') + code = self.safe_currency_code(currencyId) + currency = self.currency(code) + valueScale = self.safe_integer(currency, 'valueScale', 8) + account = self.account() + accountBalanceEv = self.safe_string_2(balance, 'accountBalanceEv', 'accountBalanceRv') + totalUsedBalanceEv = self.safe_string_2(balance, 'totalUsedBalanceEv', 'totalUsedBalanceRv') + needsConversion = (code != 'USDT') + account['total'] = self.from_en(accountBalanceEv, valueScale) if needsConversion else accountBalanceEv + account['used'] = self.from_en(totalUsedBalanceEv, valueScale) if needsConversion else totalUsedBalanceEv + result[code] = account + return self.safe_balance(result) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://phemex-docs.github.io/#query-wallets + https://github.com/phemex/phemex-api-docs/blob/master/Public-Hedged-Perpetual-API.md#query-account-positions + https://phemex-docs.github.io/#query-trading-account-and-positions + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: spot or swap + :param str [params.code]: *swap only* currency code of the balance to query(USD, USDT, etc), default is USDT + :returns dict: a `balance structure ` + """ + await self.load_markets() + type = None + type, params = self.handle_market_type_and_params('fetchBalance', None, params) + code = self.safe_string(params, 'code') + params = self.omit(params, ['code']) + response = None + request: dict = {} + if (type != 'spot') and (type != 'swap'): + raise BadRequest(self.id + ' does not support ' + type + ' markets, only spot and swap') + if type == 'swap': + settle = None + settle, params = self.handle_option_and_params(params, 'fetchBalance', 'settle', 'USDT') + if code is not None or settle is not None: + coin = None + if code is not None: + coin = code + else: + coin = settle + currency = self.currency(coin) + request['currency'] = currency['id'] + if currency['id'] == 'USDT': + response = await self.privateGetGAccountsAccountPositions(self.extend(request, params)) + else: + response = await self.privateGetAccountsAccountPositions(self.extend(request, params)) + else: + currency = self.safe_string(params, 'currency') + if currency is None: + raise ArgumentsRequired(self.id + ' fetchBalance() requires a code parameter or a currency or settle parameter for ' + type + ' type') + response = await self.privateGetSpotWallets(self.extend(request, params)) + else: + response = await self.privateGetSpotWallets(self.extend(request, params)) + # + # usdt + # { + # "info": { + # "code": "0", + # "msg": '', + # "data": { + # "account": { + # "userID": "940666", + # "accountId": "9406660003", + # "currency": "USDT", + # "accountBalanceRv": "99.93143972", + # "totalUsedBalanceRv": "0.40456", + # "bonusBalanceRv": "0" + # }, + # } + # + # spot + # + # { + # "code":0, + # "msg":"", + # "data":[ + # { + # "currency":"USDT", + # "balanceEv":0, + # "lockedTradingBalanceEv":0, + # "lockedWithdrawEv":0, + # "lastUpdateTimeNs":1592065834511322514, + # "walletVid":0 + # }, + # { + # "currency":"ETH", + # "balanceEv":0, + # "lockedTradingBalanceEv":0, + # "lockedWithdrawEv":0, + # "lastUpdateTimeNs":1592065834511322514, + # "walletVid":0 + # } + # ] + # } + # + # swap + # + # { + # "code":0, + # "msg":"", + # "data":{ + # "account":{ + # "accountId":6192120001, + # "currency":"BTC", + # "accountBalanceEv":1254744, + # "totalUsedBalanceEv":0, + # "bonusBalanceEv":1254744 + # }, + # "positions":[ + # { + # "accountID":6192120001, + # "symbol":"BTCUSD", + # "currency":"BTC", + # "side":"None", + # "positionStatus":"Normal", + # "crossMargin":false, + # "leverageEr":0, + # "leverage":0E-8, + # "initMarginReqEr":1000000, + # "initMarginReq":0.01000000, + # "maintMarginReqEr":500000, + # "maintMarginReq":0.00500000, + # "riskLimitEv":10000000000, + # "riskLimit":100.00000000, + # "size":0, + # "value":0E-8, + # "valueEv":0, + # "avgEntryPriceEp":0, + # "avgEntryPrice":0E-8, + # "posCostEv":0, + # "posCost":0E-8, + # "assignedPosBalanceEv":0, + # "assignedPosBalance":0E-8, + # "bankruptCommEv":0, + # "bankruptComm":0E-8, + # "bankruptPriceEp":0, + # "bankruptPrice":0E-8, + # "positionMarginEv":0, + # "positionMargin":0E-8, + # "liquidationPriceEp":0, + # "liquidationPrice":0E-8, + # "deleveragePercentileEr":0, + # "deleveragePercentile":0E-8, + # "buyValueToCostEr":1150750, + # "buyValueToCost":0.01150750, + # "sellValueToCostEr":1149250, + # "sellValueToCost":0.01149250, + # "markPriceEp":96359083, + # "markPrice":9635.90830000, + # "markValueEv":0, + # "markValue":null, + # "unRealisedPosLossEv":0, + # "unRealisedPosLoss":null, + # "estimatedOrdLossEv":0, + # "estimatedOrdLoss":0E-8, + # "usedBalanceEv":0, + # "usedBalance":0E-8, + # "takeProfitEp":0, + # "takeProfit":null, + # "stopLossEp":0, + # "stopLoss":null, + # "realisedPnlEv":0, + # "realisedPnl":null, + # "cumRealisedPnlEv":0, + # "cumRealisedPnl":null + # } + # ] + # } + # } + # + if type == 'swap': + return self.parse_swap_balance(response) + return self.parse_spot_balance(response) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'Created': 'open', + 'Untriggered': 'open', + 'Deactivated': 'closed', + 'Triggered': 'open', + 'Rejected': 'rejected', + 'New': 'open', + 'PartiallyFilled': 'open', + 'Filled': 'closed', + 'Canceled': 'canceled', + 'Suspended': 'canceled', + '1': 'open', + '2': 'canceled', + '3': 'closed', + '4': 'canceled', + '5': 'open', + '6': 'open', + '7': 'closed', + '8': 'canceled', + } + return self.safe_string(statuses, status, status) + + def parse_order_type(self, type: Str): + types: dict = { + '1': 'market', + '2': 'limit', + '3': 'stop', + '4': 'stopLimit', + '5': 'market', + '6': 'limit', + '7': 'market', + '8': 'market', + '9': 'stopLimit', + '10': 'market', + 'Limit': 'limit', + 'Market': 'market', + } + return self.safe_string(types, type, type) + + def parse_time_in_force(self, timeInForce: Str): + timeInForces: dict = { + 'GoodTillCancel': 'GTC', + 'PostOnly': 'PO', + 'ImmediateOrCancel': 'IOC', + 'FillOrKill': 'FOK', + } + return self.safe_string(timeInForces, timeInForce, timeInForce) + + def parse_spot_order(self, order: dict, market: Market = None): + # + # spot + # + # { + # "orderID": "d1d09454-cabc-4a23-89a7-59d43363f16d", + # "clOrdID": "309bcd5c-9f6e-4a68-b775-4494542eb5cb", + # "priceEp": 0, + # "action": "New", + # "trigger": "UNSPECIFIED", + # "pegPriceType": "UNSPECIFIED", + # "stopDirection": "UNSPECIFIED", + # "bizError": 0, + # "symbol": "sBTCUSDT", + # "side": "Buy", + # "baseQtyEv": 0, + # "ordType": "Limit", + # "timeInForce": "GoodTillCancel", + # "ordStatus": "Created", + # "cumFeeEv": 0, + # "cumBaseQtyEv": 0, + # "cumQuoteQtyEv": 0, + # "leavesBaseQtyEv": 0, + # "leavesQuoteQtyEv": 0, + # "avgPriceEp": 0, + # "cumBaseAmountEv": 0, + # "cumQuoteAmountEv": 0, + # "quoteQtyEv": 0, + # "qtyType": "ByBase", + # "stopPxEp": 0, + # "pegOffsetValueEp": 0 + # } + # + # { + # "orderID":"99232c3e-3d6a-455f-98cc-2061cdfe91bc", + # "stopPxEp":0, + # "avgPriceEp":0, + # "qtyType":"ByBase", + # "leavesBaseQtyEv":0, + # "leavesQuoteQtyEv":0, + # "baseQtyEv":"1000000000", + # "feeCurrency":"4", + # "stopDirection":"UNSPECIFIED", + # "symbol":"sETHUSDT", + # "side":"Buy", + # "quoteQtyEv":250000000000, + # "priceEp":25000000000, + # "ordType":"Limit", + # "timeInForce":"GoodTillCancel", + # "ordStatus":"Rejected", + # "execStatus":"NewRejected", + # "createTimeNs":1592675305266037130, + # "cumFeeEv":0, + # "cumBaseValueEv":0, + # "cumQuoteValueEv":0 + # } + # + id = self.safe_string(order, 'orderID') + clientOrderId = self.safe_string(order, 'clOrdID') + if (clientOrderId is not None) and (len(clientOrderId) < 1): + clientOrderId = None + marketId = self.safe_string(order, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + price = self.from_ep(self.safe_string(order, 'priceEp'), market) + amount = self.from_ev(self.safe_string(order, 'baseQtyEv'), market) + remaining = self.omit_zero(self.from_ev(self.safe_string(order, 'leavesBaseQtyEv'), market)) + filled = self.from_ev(self.safe_string_2(order, 'cumBaseQtyEv', 'cumBaseValueEv'), market) + cost = self.from_er(self.safe_string_2(order, 'cumQuoteValueEv', 'quoteQtyEv'), market) + average = self.from_ep(self.safe_string(order, 'avgPriceEp'), market) + status = self.parse_order_status(self.safe_string(order, 'ordStatus')) + side = self.safe_string_lower(order, 'side') + type = self.parse_order_type(self.safe_string(order, 'ordType')) + timestamp = self.safe_integer_product_2(order, 'actionTimeNs', 'createTimeNs', 0.000001) + fee = None + feeCost = self.from_ev(self.safe_string(order, 'cumFeeEv'), market) + if feeCost is not None: + fee = { + 'cost': feeCost, + 'currency': self.safe_currency_code(self.safe_string(order, 'feeCurrency')), + } + timeInForce = self.parse_time_in_force(self.safe_string(order, 'timeInForce')) + triggerPrice = self.parse_number(self.omit_zero(self.from_ep(self.safe_string(order, 'stopPxEp')))) + postOnly = (timeInForce == 'PO') + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'symbol': symbol, + 'type': type, + 'timeInForce': timeInForce, + 'postOnly': postOnly, + 'side': side, + 'price': price, + 'triggerPrice': triggerPrice, + 'amount': amount, + 'cost': cost, + 'average': average, + 'filled': filled, + 'remaining': remaining, + 'status': status, + 'fee': fee, + 'trades': None, + }, market) + + def parse_order_side(self, side): + sides: dict = { + '1': 'buy', + '2': 'sell', + } + return self.safe_string(sides, side, side) + + def parse_swap_order(self, order, market: Market = None): + # + # { + # "bizError":0, + # "orderID":"7a1ad384-44a3-4e54-a102-de4195a29e32", + # "clOrdID":"", + # "symbol":"ETHUSD", + # "side":"Buy", + # "actionTimeNs":1592668973945065381, + # "transactTimeNs":0, + # "orderType":"Market", + # "priceEp":2267500, + # "price":226.75000000, + # "orderQty":1, + # "displayQty":0, + # "timeInForce":"ImmediateOrCancel", + # "reduceOnly":false, + # "closedPnlEv":0, + # "closedPnl":0E-8, + # "closedSize":0, + # "cumQty":0, + # "cumValueEv":0, + # "cumValue":0E-8, + # "leavesQty":1, + # "leavesValueEv":11337, + # "leavesValue":1.13370000, + # "stopDirection":"UNSPECIFIED", + # "stopPxEp":0, + # "stopPx":0E-8, + # "trigger":"UNSPECIFIED", + # "pegOffsetValueEp":0, + # "execStatus":"PendingNew", + # "pegPriceType":"UNSPECIFIED", + # "ordStatus":"Created", + # "execInst": "ReduceOnly" + # } + # + # usdt + # { + # "bizError":"0", + # "orderID":"bd720dff-5647-4596-aa4e-656bac87aaad", + # "clOrdID":"ccxt2022843dffac9477b497", + # "symbol":"LTCUSDT", + # "side":"Buy", + # "actionTimeNs":"1677667878751724052", + # "transactTimeNs":"1677667878754017434", + # "orderType":"Limit", + # "priceRp":"40", + # "orderQtyRq":"0.1", + # "displayQtyRq":"0.1", + # "timeInForce":"GoodTillCancel", + # "reduceOnly":false, + # "closedPnlRv":"0", + # "closedSizeRq":"0", + # "cumQtyRq":"0", + # "cumValueRv":"0", + # "leavesQtyRq":"0.1", + # "leavesValueRv":"4", + # "stopDirection":"UNSPECIFIED", + # "stopPxRp":"0", + # "trigger":"UNSPECIFIED", + # "pegOffsetValueRp":"0", + # "pegOffsetProportionRr":"0", + # "execStatus":"New", + # "pegPriceType":"UNSPECIFIED", + # "ordStatus":"New", + # "execInst":"None", + # "takeProfitRp":"0", + # "stopLossRp":"0" + # } + # + # v2 orderList + # { + # "createdAt":"1677686231301", + # "symbol":"LTCUSDT", + # "orderQtyRq":"0.2", + # "side":"1", + # "posSide":"3", + # "priceRp":"50", + # "execQtyRq":"0", + # "leavesQtyRq":"0.2", + # "execPriceRp":"0", + # "orderValueRv":"10", + # "leavesValueRv":"10", + # "cumValueRv":"0", + # "stopDirection":"0", + # "stopPxRp":"0", + # "trigger":"0", + # "actionBy":"1", + # "execFeeRv":"0", + # "ordType":"2", + # "ordStatus":"5", + # "clOrdId":"4b3b188", + # "orderId":"4b3b1884-87cf-4897-b596-6693b7ed84d1", + # "execStatus":"5", + # "bizError":"0", + # "totalPnlRv":null, + # "avgTransactPriceRp":null, + # "orderDetailsVos":null, + # "tradeType":"0" + # } + # + id = self.safe_string_2(order, 'orderID', 'orderId') + clientOrderId = self.safe_string_2(order, 'clOrdID', 'clOrdId') + if (clientOrderId is not None) and (len(clientOrderId) < 1): + clientOrderId = None + marketId = self.safe_string(order, 'symbol') + symbol = self.safe_symbol(marketId, market) + market = self.safe_market(marketId, market) + status = self.parse_order_status(self.safe_string(order, 'ordStatus')) + side = self.parse_order_side(self.safe_string_lower(order, 'side')) + type = self.parse_order_type(self.safe_string(order, 'orderType')) + price = self.safe_string(order, 'priceRp') + if price is None: + price = self.from_ep(self.safe_string(order, 'priceEp'), market) + amount = self.safe_number_2(order, 'orderQty', 'orderQtyRq') + filled = self.safe_number_2(order, 'cumQty', 'cumQtyRq') + remaining = self.safe_number_2(order, 'leavesQty', 'leavesQtyRq') + timestamp = self.safe_integer_product(order, 'actionTimeNs', 0.000001) + if timestamp is None: + timestamp = self.safe_integer(order, 'createdAt') + cost = self.safe_number_2(order, 'cumValue', 'cumValueRv') + lastTradeTimestamp = self.safe_integer_product(order, 'transactTimeNs', 0.000001) + if lastTradeTimestamp == 0: + lastTradeTimestamp = None + timeInForce = self.parse_time_in_force(self.safe_string(order, 'timeInForce')) + triggerPrice = self.omit_zero(self.safe_string_2(order, 'stopPx', 'stopPxRp')) + postOnly = (timeInForce == 'PO') + reduceOnly = self.safe_value(order, 'reduceOnly') + execInst = self.safe_string(order, 'execInst') + if execInst == 'ReduceOnly': + reduceOnly = True + takeProfit = self.safe_string(order, 'takeProfitRp') + stopLoss = self.safe_string(order, 'stopLossRp') + feeValue = self.omit_zero(self.safe_string(order, 'execFeeRv')) + ptFeeRv = self.omit_zero(self.safe_string(order, 'ptFeeRv')) + fee = None + if feeValue is not None: + fee = { + 'cost': feeValue, + 'currency': market['quote'], + } + elif ptFeeRv is not None: + fee = { + 'cost': ptFeeRv, + 'currency': 'PT', + } + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': clientOrderId, + 'datetime': self.iso8601(timestamp), + 'timestamp': timestamp, + 'lastTradeTimestamp': lastTradeTimestamp, + 'symbol': symbol, + 'type': type, + 'timeInForce': timeInForce, + 'postOnly': postOnly, + 'reduceOnly': reduceOnly, + 'side': side, + 'price': price, + 'triggerPrice': triggerPrice, + 'takeProfitPrice': takeProfit, + 'stopLossPrice': stopLoss, + 'amount': amount, + 'filled': filled, + 'remaining': remaining, + 'cost': cost, + 'average': None, + 'status': status, + 'fee': fee, + 'trades': None, + }) + + def parse_order(self, order: dict, market: Market = None) -> Order: + isSwap = self.safe_bool(market, 'swap', False) + hasPnl = ('closedPnl' in order) or ('closedPnlRv' in order) or ('totalPnlRv' in order) + if isSwap or hasPnl: + return self.parse_swap_order(order, market) + return self.parse_spot_order(order, market) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://github.com/phemex/phemex-api-docs/blob/master/Public-Hedged-Perpetual-API.md#place-order + https://phemex-docs.github.io/#place-order-http-put-prefered-3 + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.trigger]: trigger price for conditional orders + :param dict [params.takeProfit]: *swap only* *takeProfit object in params* containing the triggerPrice at which the attached take profit order will be triggered(perpetual swap markets only) + :param float [params.takeProfit.triggerPrice]: take profit trigger price + :param dict [params.stopLoss]: *swap only* *stopLoss object in params* containing the triggerPrice at which the attached stop loss order will be triggered(perpetual swap markets only) + :param float [params.stopLoss.triggerPrice]: stop loss trigger price + :param str [params.posSide]: *swap only* "Merged" for one way mode, "Long" for buy side of hedged mode, "Short" for sell side of hedged mode + :param bool [params.hedged]: *swap only* True for hedged mode, False for one way mode, default is False + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + requestSide = self.capitalize(side) + type = self.capitalize(type) + request: dict = { + # common + 'symbol': market['id'], + 'side': requestSide, # Sell, Buy + 'ordType': type, # Market, Limit, Stop, StopLimit, MarketIfTouched, LimitIfTouched(additionally for contract-markets: MarketAsLimit, StopAsLimit, MarketIfTouchedAsLimit) + # 'stopPxEp': self.to_ep(stopPx, market), # for conditional orders + # 'priceEp': self.to_ep(price, market), # required for limit orders + # 'timeInForce': 'GoodTillCancel', # GoodTillCancel, PostOnly, ImmediateOrCancel, FillOrKill + # ---------------------------------------------------------------- + # spot + # 'qtyType': 'ByBase', # ByBase, ByQuote + # 'quoteQtyEv': self.to_ep(cost, market), + # 'baseQtyEv': self.to_ev(amount, market), + # 'trigger': 'ByLastPrice', # required for conditional orders + # ---------------------------------------------------------------- + # swap + # 'clOrdID': self.uuid(), # max length 40 + # 'orderQty': self.amount_to_precision(amount, symbol), + # 'reduceOnly': False, + # 'closeOnTrigger': False, # implicit reduceOnly and cancel other orders in the same direction + # 'takeProfitEp': self.to_ep(takeProfit, market), + # 'stopLossEp': self.to_ep(stopLossEp, market), + # 'triggerType': 'ByMarkPrice', # ByMarkPrice, ByLastPrice + # 'pegOffsetValueEp': integer, # Trailing offset from current price. Negative value when position is long, positive when position is short + # 'pegPriceType': 'TrailingStopPeg', # TrailingTakeProfitPeg + # 'text': 'comment', + # 'posSide': Position direction - "Merged" for oneway mode , "Long" / "Short" for hedge mode + } + clientOrderId = self.safe_string_2(params, 'clOrdID', 'clientOrderId') + stopLoss = self.safe_value(params, 'stopLoss') + stopLossDefined = (stopLoss is not None) + takeProfit = self.safe_value(params, 'takeProfit') + takeProfitDefined = (takeProfit is not None) + isStableSettled = (market['settle'] == 'USDT') or (market['settle'] == 'USDC') + if clientOrderId is None: + brokerId = self.safe_string(self.options, 'brokerId', 'CCXT123456') + if brokerId is not None: + request['clOrdID'] = brokerId + self.uuid16() + else: + request['clOrdID'] = clientOrderId + params = self.omit(params, ['clOrdID', 'clientOrderId']) + triggerPrice = self.safe_string_n(params, ['stopPx', 'stopPrice', 'triggerPrice']) + if triggerPrice is not None: + if isStableSettled: + request['stopPxRp'] = self.price_to_precision(symbol, triggerPrice) + else: + request['stopPxEp'] = self.to_ep(triggerPrice, market) + params = self.omit(params, ['stopPx', 'stopPrice', 'stopLoss', 'takeProfit', 'triggerPrice']) + if market['spot']: + qtyType = self.safe_value(params, 'qtyType', 'ByBase') + if (type == 'Market') or (type == 'Stop') or (type == 'MarketIfTouched'): + if price is not None: + qtyType = 'ByQuote' + if triggerPrice is not None: + if type == 'Limit': + request['ordType'] = 'StopLimit' + elif type == 'Market': + request['ordType'] = 'Stop' + request['trigger'] = 'ByLastPrice' + request['qtyType'] = qtyType + if qtyType == 'ByQuote': + cost = self.safe_number(params, 'cost') + params = self.omit(params, 'cost') + if self.options['createOrderByQuoteRequiresPrice']: + if price is not None: + amountString = self.number_to_string(amount) + priceString = self.number_to_string(price) + quoteAmount = Precise.string_mul(amountString, priceString) + cost = self.parse_number(quoteAmount) + elif cost is None: + raise ArgumentsRequired(self.id + ' createOrder() ' + qtyType + ' requires a price argument or a cost parameter') + cost = amount if (cost is None) else cost + costString = self.number_to_string(cost) + request['quoteQtyEv'] = self.to_ev(costString, market) + else: + amountString = self.number_to_string(amount) + request['baseQtyEv'] = self.to_ev(amountString, market) + elif market['swap']: + hedged = self.safe_bool(params, 'hedged', False) + params = self.omit(params, 'hedged') + posSide = self.safe_string_lower(params, 'posSide') + if posSide is None: + if hedged: + reduceOnly = self.safe_bool(params, 'reduceOnly') + if reduceOnly: + side = 'sell' if (side == 'buy') else 'buy' + params = self.omit(params, 'reduceOnly') + posSide = 'Long' if (side == 'buy') else 'Short' + else: + posSide = 'Merged' + posSide = self.capitalize(posSide) + request['posSide'] = posSide + if isStableSettled: + request['orderQtyRq'] = amount + else: + request['orderQty'] = self.parse_to_int(amount) + if triggerPrice is not None: + triggerType = self.safe_string(params, 'triggerType', 'ByMarkPrice') + request['triggerType'] = triggerType + # set direction & exchange specific order type + triggerDirection = None + triggerDirection, params = self.handle_param_string(params, 'triggerDirection') + if triggerDirection is None: + raise ArgumentsRequired(self.id + " createOrder() also requires a 'triggerDirection' parameter with either 'ascending' or 'descending' value") + # the flow defined per https://phemex-docs.github.io/#more-order-type-examples + if triggerDirection == 'ascending' or triggerDirection == 'up': + if side == 'sell': + request['ordType'] = 'MarketIfTouched' if (type == 'Market') else 'LimitIfTouched' + elif side == 'buy': + request['ordType'] = 'Stop' if (type == 'Market') else 'StopLimit' + elif triggerDirection == 'descending' or triggerDirection == 'down': + if side == 'sell': + request['ordType'] = 'Stop' if (type == 'Market') else 'StopLimit' + elif side == 'buy': + request['ordType'] = 'MarketIfTouched' if (type == 'Market') else 'LimitIfTouched' + if stopLossDefined or takeProfitDefined: + if stopLossDefined: + stopLossTriggerPrice = self.safe_value_2(stopLoss, 'triggerPrice', 'stopPrice') + if stopLossTriggerPrice is None: + raise InvalidOrder(self.id + ' createOrder() requires a trigger price in params["stopLoss"]["triggerPrice"] for a stop loss order') + if isStableSettled: + request['stopLossRp'] = self.price_to_precision(symbol, stopLossTriggerPrice) + else: + request['stopLossEp'] = self.to_ep(stopLossTriggerPrice, market) + stopLossTriggerPriceType = self.safe_string_2(stopLoss, 'triggerPriceType', 'slTrigger') + if stopLossTriggerPriceType is not None: + request['slTrigger'] = self.safe_string(self.options['triggerPriceTypesMap'], stopLossTriggerPriceType, stopLossTriggerPriceType) + slLimitPrice = self.safe_string(stopLoss, 'price') + if slLimitPrice is not None: + request['slPxRp'] = self.price_to_precision(symbol, slLimitPrice) + if takeProfitDefined: + takeProfitTriggerPrice = self.safe_value_2(takeProfit, 'triggerPrice', 'stopPrice') + if takeProfitTriggerPrice is None: + raise InvalidOrder(self.id + ' createOrder() requires a trigger price in params["takeProfit"]["triggerPrice"] for a take profit order') + if isStableSettled: + request['takeProfitRp'] = self.price_to_precision(symbol, takeProfitTriggerPrice) + else: + request['takeProfitEp'] = self.to_ep(takeProfitTriggerPrice, market) + takeProfitTriggerPriceType = self.safe_string_2(takeProfit, 'triggerPriceType', 'tpTrigger') + if takeProfitTriggerPriceType is not None: + request['tpTrigger'] = self.safe_string(self.options['triggerPriceTypesMap'], takeProfitTriggerPriceType, takeProfitTriggerPriceType) + tpLimitPrice = self.safe_string(takeProfit, 'price') + if tpLimitPrice is not None: + request['tpPxRp'] = self.price_to_precision(symbol, tpLimitPrice) + if (type == 'Limit') or (type == 'StopLimit') or (type == 'LimitIfTouched'): + if isStableSettled: + request['priceRp'] = self.price_to_precision(symbol, price) + else: + priceString = self.number_to_string(price) + request['priceEp'] = self.to_ep(priceString, market) + takeProfitPrice = self.safe_string(params, 'takeProfitPrice') + if takeProfitPrice is not None: + if isStableSettled: + request['takeProfitRp'] = self.price_to_precision(symbol, takeProfitPrice) + else: + request['takeProfitEp'] = self.to_ep(takeProfitPrice, market) + params = self.omit(params, 'takeProfitPrice') + stopLossPrice = self.safe_string(params, 'stopLossPrice') + if stopLossPrice is not None: + if isStableSettled: + request['stopLossRp'] = self.price_to_precision(symbol, stopLossPrice) + else: + request['stopLossEp'] = self.to_ep(stopLossPrice, market) + params = self.omit(params, 'stopLossPrice') + response = None + if isStableSettled: + response = await self.privatePostGOrders(self.extend(request, params)) + elif market['contract']: + response = await self.privatePostOrders(self.extend(request, params)) + else: + response = await self.privatePostSpotOrders(self.extend(request, params)) + # + # spot + # + # { + # "code": 0, + # "msg": "", + # "data": { + # "orderID": "d1d09454-cabc-4a23-89a7-59d43363f16d", + # "clOrdID": "309bcd5c-9f6e-4a68-b775-4494542eb5cb", + # "priceEp": 0, + # "action": "New", + # "trigger": "UNSPECIFIED", + # "pegPriceType": "UNSPECIFIED", + # "stopDirection": "UNSPECIFIED", + # "bizError": 0, + # "symbol": "sBTCUSDT", + # "side": "Buy", + # "baseQtyEv": 0, + # "ordType": "Limit", + # "timeInForce": "GoodTillCancel", + # "ordStatus": "Created", + # "cumFeeEv": 0, + # "cumBaseQtyEv": 0, + # "cumQuoteQtyEv": 0, + # "leavesBaseQtyEv": 0, + # "leavesQuoteQtyEv": 0, + # "avgPriceEp": 0, + # "cumBaseAmountEv": 0, + # "cumQuoteAmountEv": 0, + # "quoteQtyEv": 0, + # "qtyType": "ByBase", + # "stopPxEp": 0, + # "pegOffsetValueEp": 0 + # } + # } + # + # swap + # + # { + # "code":0, + # "msg":"", + # "data":{ + # "bizError":0, + # "orderID":"7a1ad384-44a3-4e54-a102-de4195a29e32", + # "clOrdID":"", + # "symbol":"ETHUSD", + # "side":"Buy", + # "actionTimeNs":1592668973945065381, + # "transactTimeNs":0, + # "orderType":"Market", + # "priceEp":2267500, + # "price":226.75000000, + # "orderQty":1, + # "displayQty":0, + # "timeInForce":"ImmediateOrCancel", + # "reduceOnly":false, + # "closedPnlEv":0, + # "closedPnl":0E-8, + # "closedSize":0, + # "cumQty":0, + # "cumValueEv":0, + # "cumValue":0E-8, + # "leavesQty":1, + # "leavesValueEv":11337, + # "leavesValue":1.13370000, + # "stopDirection":"UNSPECIFIED", + # "stopPxEp":0, + # "stopPx":0E-8, + # "trigger":"UNSPECIFIED", + # "pegOffsetValueEp":0, + # "execStatus":"PendingNew", + # "pegPriceType":"UNSPECIFIED", + # "ordStatus":"Created" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data, market) + + async def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + """ + edit a trade order + + https://github.com/phemex/phemex-api-docs/blob/master/Public-Hedged-Perpetual-API.md#amend-order-by-orderid + + :param str id: cancel order id + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.posSide]: either 'Merged' or 'Long' or 'Short' + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + clientOrderId = self.safe_string_2(params, 'clientOrderId', 'clOrdID') + params = self.omit(params, ['clientOrderId', 'clOrdID']) + isStableSettled = (market['settle'] == 'USDT') or (market['settle'] == 'USDC') + if clientOrderId is not None: + request['clOrdID'] = clientOrderId + else: + request['orderID'] = id + if price is not None: + if isStableSettled: + request['priceRp'] = self.price_to_precision(market['symbol'], price) + else: + request['priceEp'] = self.to_ep(price, market) + # Note the uppercase 'V' in 'baseQtyEV' request. that is exchange's requirement at self moment. However, to avoid mistakes from user side, let's support lowercased 'baseQtyEv' too + finalQty = self.safe_string(params, 'baseQtyEv') + params = self.omit(params, ['baseQtyEv']) + if finalQty is not None: + request['baseQtyEV'] = finalQty + elif amount is not None: + if isStableSettled: + request['orderQtyRq'] = self.amount_to_precision(market['symbol'], amount) + else: + request['baseQtyEV'] = self.to_ev(amount, market) + triggerPrice = self.safe_string_n(params, ['triggerPrice', 'stopPx', 'stopPrice']) + if triggerPrice is not None: + if isStableSettled: + request['stopPxRp'] = self.price_to_precision(symbol, triggerPrice) + else: + request['stopPxEp'] = self.to_ep(triggerPrice, market) + params = self.omit(params, ['triggerPrice', 'stopPx', 'stopPrice']) + response = None + if isStableSettled: + posSide = self.safe_string(params, 'posSide') + if posSide is None: + request['posSide'] = 'Merged' + response = await self.privatePutGOrdersReplace(self.extend(request, params)) + elif market['swap']: + response = await self.privatePutOrdersReplace(self.extend(request, params)) + else: + response = await self.privatePutSpotOrders(self.extend(request, params)) + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data, market) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://github.com/phemex/phemex-api-docs/blob/master/Public-Hedged-Perpetual-API.md#cancel-single-order-by-orderid + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.posSide]: either 'Merged' or 'Long' or 'Short' + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + clientOrderId = self.safe_string_2(params, 'clientOrderId', 'clOrdID') + params = self.omit(params, ['clientOrderId', 'clOrdID']) + if clientOrderId is not None: + request['clOrdID'] = clientOrderId + else: + request['orderID'] = id + response = None + if market['settle'] == 'USDT' or market['settle'] == 'USDC': + posSide = self.safe_string(params, 'posSide') + if posSide is None: + request['posSide'] = 'Merged' + response = await self.privateDeleteGOrdersCancel(self.extend(request, params)) + elif market['swap']: + response = await self.privateDeleteOrdersCancel(self.extend(request, params)) + else: + response = await self.privateDeleteSpotOrders(self.extend(request, params)) + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data, market) + + async def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders in a market + + https://github.com/phemex/phemex-api-docs/blob/master/Public-Hedged-Perpetual-API.md#cancelall + + :param str symbol: unified market symbol of the market to cancel orders in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelAllOrders() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + trigger = self.safe_value_2(params, 'stop', 'trigger', False) + params = self.omit(params, ['stop', 'trigger']) + request: dict = { + 'symbol': market['id'], + # 'untriggerred': False, # False to cancel non-conditional orders, True to cancel conditional orders + # 'text': 'up to 40 characters max', + } + if trigger: + request['untriggerred'] = trigger + response = None + if market['settle'] == 'USDT' or market['settle'] == 'USDC': + response = await self.privateDeleteGOrdersAll(self.extend(request, params)) + # + # { + # code: '0', + # msg: '', + # data: '1' + # } + # + elif market['swap']: + response = await self.privateDeleteOrdersAll(self.extend(request, params)) + # + # { + # code: '0', + # msg: '', + # data: '1' + # } + # + else: + response = await self.privateDeleteSpotOrdersAll(self.extend(request, params)) + # + # { + # code: '0', + # msg: '', + # data: { + # total: '1' + # } + # } + # + return [ + self.safe_order({ + 'info': response, + }), + ] + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + + https://phemex-docs.github.io/#query-orders-by-ids + + fetches information on an order made by the user + :param str id: the order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrder() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + clientOrderId = self.safe_string_2(params, 'clientOrderId', 'clOrdID') + params = self.omit(params, ['clientOrderId', 'clOrdID']) + if clientOrderId is not None: + request['clOrdID'] = clientOrderId + else: + request['orderID'] = id + response = None + if market['settle'] == 'USDT' or market['settle'] == 'USDC': + response = await self.privateGetApiDataGFuturesOrdersByOrderId(self.extend(request, params)) + elif market['spot']: + response = await self.privateGetApiDataSpotsOrdersByOrderId(self.extend(request, params)) + else: + response = await self.privateGetExchangeOrder(self.extend(request, params)) + data = self.safe_value(response, 'data', {}) + order = data + if isinstance(data, list): + numOrders = len(data) + if numOrders < 1: + if clientOrderId is not None: + raise OrderNotFound(self.id + ' fetchOrder() ' + symbol + ' order with clientOrderId ' + clientOrderId + ' not found') + else: + raise OrderNotFound(self.id + ' fetchOrder() ' + symbol + ' order with id ' + id + ' not found') + order = self.safe_dict(data, 0, {}) + elif market['spot']: + rows = self.safe_list(data, 'rows', []) + order = self.safe_dict(rows, 0, {}) + return self.parse_order(order, market) + + async def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://github.com/phemex/phemex-api-docs/blob/master/Public-Hedged-Perpetual-API.md#queryorder + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrders() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if since is not None: + request['start'] = since + if limit is not None: + request['limit'] = limit + response = None + if market['settle'] == 'USDT' or market['settle'] == 'USDC': + request['currency'] = market['settle'] + response = await self.privateGetExchangeOrderV2OrderList(self.extend(request, params)) + elif market['swap']: + response = await self.privateGetExchangeOrderList(self.extend(request, params)) + else: + response = await self.privateGetApiDataSpotsOrders(self.extend(request, params)) + data = self.safe_value(response, 'data', {}) + rows = self.safe_list(data, 'rows', data) + return self.parse_orders(rows, market, since, limit) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://github.com/phemex/phemex-api-docs/blob/master/Public-Hedged-Perpetual-API.md#queryopenorder + https://github.com/phemex/phemex-api-docs/blob/master/Public-Contract-API-en.md + https://github.com/phemex/phemex-api-docs/blob/master/Public-Spot-API-en.md#spotListAllOpenOrder + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOpenOrders() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = None + try: + if market['settle'] == 'USDT' or market['settle'] == 'USDC': + response = await self.privateGetGOrdersActiveList(self.extend(request, params)) + elif market['swap']: + response = await self.privateGetOrdersActiveList(self.extend(request, params)) + else: + response = await self.privateGetSpotOrders(self.extend(request, params)) + except Exception as e: + if isinstance(e, OrderNotFound): + return [] + raise e + data = self.safe_value(response, 'data', {}) + if isinstance(data, list): + return self.parse_orders(data, market, since, limit) + else: + rows = self.safe_list(data, 'rows', []) + return self.parse_orders(rows, market, since, limit) + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://github.com/phemex/phemex-api-docs/blob/master/Public-Hedged-Perpetual-API.md#queryorder + https://github.com/phemex/phemex-api-docs/blob/master/Public-Contract-API-en.md#queryorder + https://github.com/phemex/phemex-api-docs/blob/master/Public-Hedgedd-Perpetual-API.md#query-closed-orders-by-symbol + https://github.com/phemex/phemex-api-docs/blob/master/Public-Spot-API-en.md#spotDataOrdersByIds + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.settle]: the settlement currency to fetch orders for + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + request: dict = { + } + if market is not None: + request['symbol'] = market['id'] + if since is not None: + request['start'] = since + if limit is not None: + request['limit'] = limit + response = None + if (symbol is None) or (self.safe_string(market, 'settle') == 'USDT'): + request['currency'] = self.safe_string(params, 'settle', 'USDT') + response = await self.privateGetExchangeOrderV2OrderList(self.extend(request, params)) + elif market['swap']: + response = await self.privateGetExchangeOrderList(self.extend(request, params)) + else: + response = await self.privateGetExchangeSpotOrder(self.extend(request, params)) + # + # spot + # + # { + # "code":0, + # "msg":"OK", + # "data":{ + # "total":8, + # "rows":[ + # { + # "orderID":"99232c3e-3d6a-455f-98cc-2061cdfe91bc", + # "stopPxEp":0, + # "avgPriceEp":0, + # "qtyType":"ByBase", + # "leavesBaseQtyEv":0, + # "leavesQuoteQtyEv":0, + # "baseQtyEv":"1000000000", + # "feeCurrency":"4", + # "stopDirection":"UNSPECIFIED", + # "symbol":"sETHUSDT", + # "side":"Buy", + # "quoteQtyEv":250000000000, + # "priceEp":25000000000, + # "ordType":"Limit", + # "timeInForce":"GoodTillCancel", + # "ordStatus":"Rejected", + # "execStatus":"NewRejected", + # "createTimeNs":1592675305266037130, + # "cumFeeEv":0, + # "cumBaseValueEv":0, + # "cumQuoteValueEv":0 + # }, + # ] + # } + # } + # + data = self.safe_value(response, 'data', {}) + if isinstance(data, list): + return self.parse_orders(data, market, since, limit) + else: + rows = self.safe_list(data, 'rows', []) + return self.parse_orders(rows, market, since, limit) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://github.com/phemex/phemex-api-docs/blob/master/Public-Contract-API-en.md#query-user-trade + https://github.com/phemex/phemex-api-docs/blob/master/Public-Hedged-Perpetual-API.md#query-user-trade + https://github.com/phemex/phemex-api-docs/blob/master/Public-Spot-API-en.md#spotDataTradesHist + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + type = None + type, params = self.handle_market_type_and_params('fetchMyTrades', market, params) + request: dict = {} + if limit is not None: + limit = min(200, limit) + request['limit'] = limit + isUSDTSettled = (type != 'spot') and ((symbol is None) or (self.safe_string(market, 'settle') == 'USDT')) + if isUSDTSettled: + request['currency'] = 'USDT' + request['offset'] = 0 + if limit is None: + request['limit'] = 200 + elif symbol is not None: + request['symbol'] = market['id'] + if since is not None: + request['start'] = since + response = None + if isUSDTSettled: + response = await self.privateGetExchangeOrderV2TradingList(self.extend(request, params)) + elif type == 'swap': + request['tradeType'] = 'Trade' + response = await self.privateGetExchangeOrderTrade(self.extend(request, params)) + else: + response = await self.privateGetExchangeSpotOrderTrades(self.extend(request, params)) + # + # spot + # + # { + # "code": 0, + # "msg": "OK", + # "data": { + # "total": 1, + # "rows": [ + # { + # "qtyType": "ByQuote", + # "transactTimeNs": 1589450974800550100, + # "clOrdID": "8ba59d40-df25-d4b0-14cf-0703f44e9690", + # "orderID": "b2b7018d-f02f-4c59-b4cf-051b9c2d2e83", + # "symbol": "sBTCUSDT", + # "side": "Buy", + # "priceEP": 970056000000, + # "baseQtyEv": 0, + # "quoteQtyEv": 1000000000, + # "action": "New", + # "execStatus": "MakerFill", + # "ordStatus": "Filled", + # "ordType": "Limit", + # "execInst": "None", + # "timeInForce": "GoodTillCancel", + # "stopDirection": "UNSPECIFIED", + # "tradeType": "Trade", + # "stopPxEp": 0, + # "execId": "c6bd8979-07ba-5946-b07e-f8b65135dbb1", + # "execPriceEp": 970056000000, + # "execBaseQtyEv": 103000, + # "execQuoteQtyEv": 999157680, + # "leavesBaseQtyEv": 0, + # "leavesQuoteQtyEv": 0, + # "execFeeEv": 0, + # "feeRateEr": 0 + # } + # ] + # } + # } + # + # + # swap + # + # { + # "code": 0, + # "msg": "OK", + # "data": { + # "total": 79, + # "rows": [ + # { + # "transactTimeNs": 1606054879331565300, + # "symbol": "BTCUSD", + # "currency": "BTC", + # "action": "New", + # "side": "Buy", + # "tradeType": "Trade", + # "execQty": 5, + # "execPriceEp": 182990000, + # "orderQty": 5, + # "priceEp": 183870000, + # "execValueEv": 27323, + # "feeRateEr": 75000, + # "execFeeEv": 21, + # "ordType": "Market", + # "execID": "5eee56a4-04a9-5677-8eb0-c2fe22ae3645", + # "orderID": "ee0acb82-f712-4543-a11d-d23efca73197", + # "clOrdID": "", + # "execStatus": "TakerFill" + # }, + # ] + # } + # } + # + # swap - usdt + # + # { + # "code": 0, + # "msg": "OK", + # "data": { + # "total": 4, + # "rows": [ + # { + # "createdAt": 1666226932259, + # "symbol": "ETHUSDT", + # "currency": "USDT", + # "action": 1, + # "tradeType": 1, + # "execQtyRq": "0.01", + # "execPriceRp": "1271.9", + # "side": 1, + # "orderQtyRq": "0.78", + # "priceRp": "1271.9", + # "execValueRv": "12.719", + # "feeRateRr": "0.0001", + # "execFeeRv": "0.0012719", + # "ordType": 2, + # "execId": "8718cae", + # "execStatus": 6 + # }, + # ] + # } + # } + # + data = None + if isUSDTSettled: + data = self.safe_value(response, 'data', []) + else: + data = self.safe_value(response, 'data', {}) + data = self.safe_value(data, 'rows', []) + return self.parse_trades(data, market, since, limit) + + async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.network]: the chain name to fetch the deposit address e.g. ETH, TRX, EOS, SOL, etc. + :returns dict: an `address structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + } + defaultNetworks = self.safe_dict(self.options, 'defaultNetworks') + defaultNetwork = self.safe_string_upper(defaultNetworks, code) + networks = self.safe_dict(self.options, 'networks', {}) + network = self.safe_string_upper_2(params, 'network', 'chainName', defaultNetwork) + network = self.safe_string(networks, network, network) + if network is None: + raise ArgumentsRequired(self.id + ' fetchDepositAddress() requires a network parameter') + else: + request['chainName'] = network + params = self.omit(params, 'network') + response = await self.privateGetExchangeWalletsV2DepositAddress(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "OK", + # "data": { + # "address": "tb1qxel5wq5gumt", + # "tag": "", + # "notice": False, + # "accountType": 1, + # "contractName": null, + # "chainTokenUrl": null, + # "sign": null + # } + # } + # + data = self.safe_value(response, 'data', {}) + address = self.safe_string(data, 'address') + tag = self.safe_string(data, 'tag') + self.check_address(address) + return { + 'info': response, + 'currency': code, + 'network': None, + 'address': address, + 'tag': tag, + } + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + currency = None + if code is not None: + currency = self.currency(code) + response = await self.privateGetExchangeWalletsDepositList(params) + # + # { + # "code":0, + # "msg":"OK", + # "data":[ + # { + # "id":29200, + # "currency":"USDT", + # "currencyCode":3, + # "txHash":"0x0bdbdc47807769a03b158d5753f54dfc58b92993d2f5e818db21863e01238e5d", + # "address":"0x5bfbf60e0fa7f63598e6cfd8a7fd3ffac4ccc6ad", + # "amountEv":3000000000, + # "confirmations":13, + # "type":"Deposit", + # "status":"Success", + # "createdAt":1592722565000 + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_transactions(data, currency, since, limit) + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + currency = None + if code is not None: + currency = self.currency(code) + response = await self.privateGetExchangeWalletsWithdrawList(params) + # + # { + # "code":0, + # "msg":"OK", + # "data":[ + # { + # "address": "1Lxxxxxxxxxxx" + # "amountEv": 200000 + # "currency": "BTC" + # "currencyCode": 1 + # "expiredTime": 0 + # "feeEv": 50000 + # "rejectReason": null + # "status": "Succeed" + # "txHash": "44exxxxxxxxxxxxxxxxxxxxxx" + # "withdrawStatus: "" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_transactions(data, currency, since, limit) + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'Success': 'ok', + 'Succeed': 'ok', + 'Rejected': 'failed', + 'Security check failed': 'failed', + 'SecurityCheckFailed': 'failed', + 'Expired': 'failed', + 'Address Risk': 'failed', + 'Security Checking': 'pending', + 'SecurityChecking': 'pending', + 'Pending Review': 'pending', + 'Pending Transfer': 'pending', + 'AmlCsApporve': 'pending', + 'New': 'pending', + 'Confirmed': 'pending', + 'Cancelled': 'canceled', + } + return self.safe_string(statuses, status, status) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # withdraw + # + # { + # "id": "10000001", + # "freezeId": null, + # "address": "44exxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + # "amountRv": "100", + # "chainCode": "11", + # "chainName": "TRX", + # "currency": "USDT", + # "currencyCode": 3, + # "email": "abc@gmail.com", + # "expiredTime": "0", + # "feeRv": "1", + # "nickName": null, + # "phone": null, + # "rejectReason": "", + # "submitedAt": "1670000000000", + # "submittedAt": "1670000000000", + # "txHash": null, + # "userId": "10000001", + # "status": "Success" + # + # fetchDeposits + # + # { + # "id": "29200", + # "currency": "USDT", + # "currencyCode": "3", + # "chainName": "ETH", + # "chainCode": "4", + # "txHash": "0x0bdbdc47807769a03b158d5753f54dfc58b92993d2f5e818db21863e01238e5d", + # "address": "0x5bfbf60e0fa7f63598e6cfd8a7fd3ffac4ccc6ad", + # "amountEv": "3000000000", + # "confirmations": "13", + # "type": "Deposit", + # "status": "Success", + # "createdAt": "1592722565000", + # } + # + # fetchWithdrawals + # + # { + # "id": "10000001", + # "userId": "10000001", + # "freezeId": "10000002", + # "phone": null, + # "email": "abc@gmail.com", + # "nickName": null, + # "currency": "USDT", + # "currencyCode": "3", + # "status": "Succeed", + # "withdrawStatus": "Succeed", + # "amountEv": "8800000000", + # "feeEv": "1200000000", + # "address": "0x5xxxad", + # "txHash: "0x0xxxx5d", + # "submitedAt": "1702571922000", + # "submittedAt": "1702571922000", + # "expiredTime": "0", + # "rejectReason": null, + # "chainName": "ETH", + # "chainCode": "4", + # "proxyAddress": null + # } + # + id = self.safe_string(transaction, 'id') + address = self.safe_string(transaction, 'address') + tag = None + txid = self.safe_string(transaction, 'txHash') + currencyId = self.safe_string(transaction, 'currency') + currency = self.safe_currency(currencyId, currency) + code = currency['code'] + networkId = self.safe_string(transaction, 'chainName') + timestamp = self.safe_integer_n(transaction, ['createdAt', 'submitedAt', 'submittedAt']) + type = self.safe_string_lower(transaction, 'type') + feeCost = self.parse_number(self.from_en(self.safe_string(transaction, 'feeEv'), currency['valueScale'])) + if feeCost is None: + feeCost = self.safe_number(transaction, 'feeRv') + fee = None + if feeCost is not None: + type = 'withdrawal' + fee = { + 'cost': feeCost, + 'currency': code, + } + status = self.parse_transaction_status(self.safe_string(transaction, 'status')) + amount = self.parse_number(self.from_en(self.safe_string(transaction, 'amountEv'), currency['valueScale'])) + if amount is None: + amount = self.safe_number(transaction, 'amountRv') + return { + 'info': transaction, + 'id': id, + 'txid': txid, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': self.network_id_to_code(networkId), + 'address': address, + 'addressTo': address, + 'addressFrom': None, + 'tag': tag, + 'tagTo': tag, + 'tagFrom': None, + 'type': type, + 'amount': amount, + 'currency': code, + 'status': status, + 'updated': None, + 'comment': None, + 'internal': None, + 'fee': fee, + } + + async def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://github.com/phemex/phemex-api-docs/blob/master/Public-Contract-API-en.md#query-trading-account-and-positions + https://github.com/phemex/phemex-api-docs/blob/master/Public-Hedged-Perpetual-API.md#query-account-positions + https://phemex-docs.github.io/#query-account-positions-with-unrealized-pnl + + :param str[] [symbols]: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.code]: the currency code to fetch positions for, USD, BTC or USDT, USDT is the default + :param str [params.method]: *USDT contracts only* 'privateGetGAccountsAccountPositions' or 'privateGetGAccountsAccountPositions' default is 'privateGetGAccountsAccountPositions' + :returns dict[]: a list of `position structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + subType = None + code = self.safe_string_2(params, 'currency', 'code', 'USDT') + params = self.omit(params, ['currency', 'code']) + settle = None + market = None + firstSymbol = self.safe_string(symbols, 0) + if firstSymbol is not None: + market = self.market(firstSymbol) + settle = market['settle'] + code = market['settle'] + else: + settle, params = self.handle_option_and_params(params, 'fetchPositions', 'settle', code) + subType, params = self.handle_sub_type_and_params('fetchPositions', market, params) + isUSDTSettled = settle == 'USDT' + if isUSDTSettled: + code = 'USDT' + elif settle == 'BTC': + code = 'BTC' + elif code is None: + code = 'USD' if (subType == 'linear') else 'BTC' + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + } + response = None + if isUSDTSettled: + method = None + method, params = self.handle_option_and_params(params, 'fetchPositions', 'method', 'privateGetGAccountsAccountPositions') + if method == 'privateGetGAccountsAccountPositions': + response = await self.privateGetGAccountsAccountPositions(self.extend(request, params)) + else: + response = await self.privateGetGAccountsPositions(self.extend(request, params)) + else: + response = await self.privateGetAccountsAccountPositions(self.extend(request, params)) + # + # { + # "code":0,"msg":"", + # "data":{ + # "account":{ + # "accountId":6192120001, + # "currency":"BTC", + # "accountBalanceEv":1254744, + # "totalUsedBalanceEv":0, + # "bonusBalanceEv":1254744 + # }, + # "positions":[ + # { + # "accountID":6192120001, + # "symbol":"BTCUSD", + # "currency":"BTC", + # "side":"None", + # "positionStatus":"Normal", + # "crossMargin":false, + # "leverageEr":100000000, + # "leverage":1.00000000, + # "initMarginReqEr":100000000, + # "initMarginReq":1.00000000, + # "maintMarginReqEr":500000, + # "maintMarginReq":0.00500000, + # "riskLimitEv":10000000000, + # "riskLimit":100.00000000, + # "size":0, + # "value":0E-8, + # "valueEv":0, + # "avgEntryPriceEp":0, + # "avgEntryPrice":0E-8, + # "posCostEv":0, + # "posCost":0E-8, + # "assignedPosBalanceEv":0, + # "assignedPosBalance":0E-8, + # "bankruptCommEv":0, + # "bankruptComm":0E-8, + # "bankruptPriceEp":0, + # "bankruptPrice":0E-8, + # "positionMarginEv":0, + # "positionMargin":0E-8, + # "liquidationPriceEp":0, + # "liquidationPrice":0E-8, + # "deleveragePercentileEr":0, + # "deleveragePercentile":0E-8, + # "buyValueToCostEr":100225000, + # "buyValueToCost":1.00225000, + # "sellValueToCostEr":100075000, + # "sellValueToCost":1.00075000, + # "markPriceEp":135736070, + # "markPrice":13573.60700000, + # "markValueEv":0, + # "markValue":null, + # "unRealisedPosLossEv":0, + # "unRealisedPosLoss":null, + # "estimatedOrdLossEv":0, + # "estimatedOrdLoss":0E-8, + # "usedBalanceEv":0, + # "usedBalance":0E-8, + # "takeProfitEp":0, + # "takeProfit":null, + # "stopLossEp":0, + # "stopLoss":null, + # "cumClosedPnlEv":0, + # "cumFundingFeeEv":0, + # "cumTransactFeeEv":0, + # "realisedPnlEv":0, + # "realisedPnl":null, + # "cumRealisedPnlEv":0, + # "cumRealisedPnl":null + # } + # ] + # } + # } + # + data = self.safe_value(response, 'data', {}) + positions = self.safe_value(data, 'positions', []) + result = [] + for i in range(0, len(positions)): + position = positions[i] + result.append(self.parse_position(position)) + return self.filter_by_array_positions(result, 'symbol', symbols, False) + + def parse_position(self, position: dict, market: Market = None): + # + # { + # "userID": "811370", + # "accountID": "8113700002", + # "symbol": "ETHUSD", + # "currency": "USD", + # "side": "Buy", + # "positionStatus": "Normal", + # "crossMargin": False, + # "leverageEr": "200000000", + # "leverage": "2.00000000", + # "initMarginReqEr": "50000000", + # "initMarginReq": "0.50000000", + # "maintMarginReqEr": "1000000", + # "maintMarginReq": "0.01000000", + # "riskLimitEv": "5000000000", + # "riskLimit": "500000.00000000", + # "size": "1", + # "value": "22.22370000", + # "valueEv": "222237", + # "avgEntryPriceEp": "44447400", + # "avgEntryPrice": "4444.74000000", + # "posCostEv": "111202", + # "posCost": "11.12020000", + # "assignedPosBalanceEv": "111202", + # "assignedPosBalance": "11.12020000", + # "bankruptCommEv": "84", + # "bankruptComm": "0.00840000", + # "bankruptPriceEp": "22224000", + # "bankruptPrice": "2222.40000000", + # "positionMarginEv": "111118", + # "positionMargin": "11.11180000", + # "liquidationPriceEp": "22669000", + # "liquidationPrice": "2266.90000000", + # "deleveragePercentileEr": "0", + # "deleveragePercentile": "0E-8", + # "buyValueToCostEr": "50112500", + # "buyValueToCost": "0.50112500", + # "sellValueToCostEr": "50187500", + # "sellValueToCost": "0.50187500", + # "markPriceEp": "31332499", + # "markPrice": "3133.24990000", + # "markValueEv": "0", + # "markValue": null, + # "unRealisedPosLossEv": "0", + # "unRealisedPosLoss": null, + # "estimatedOrdLossEv": "0", + # "estimatedOrdLoss": "0E-8", + # "usedBalanceEv": "111202", + # "usedBalance": "11.12020000", + # "takeProfitEp": "0", + # "takeProfit": null, + # "stopLossEp": "0", + # "stopLoss": null, + # "cumClosedPnlEv": "-1546", + # "cumFundingFeeEv": "1605", + # "cumTransactFeeEv": "8438", + # "realisedPnlEv": "0", + # "realisedPnl": null, + # "cumRealisedPnlEv": "0", + # "cumRealisedPnl": null, + # "transactTimeNs": "1641571200001885324", + # "takerFeeRateEr": "0", + # "makerFeeRateEr": "0", + # "term": "6", + # "lastTermEndTimeNs": "1607711882505745356", + # "lastFundingTimeNs": "1641571200000000000", + # "curTermRealisedPnlEv": "-1567", + # "execSeq": "12112761561" + # } + # + marketId = self.safe_string(position, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + collateral = self.safe_string_2(position, 'positionMargin', 'positionMarginRv') + notionalString = self.safe_string_2(position, 'value', 'valueRv') + maintenanceMarginPercentageString = self.safe_string_2(position, 'maintMarginReq', 'maintMarginReqRr') + maintenanceMarginString = Precise.string_mul(notionalString, maintenanceMarginPercentageString) + initialMarginString = self.safe_string_2(position, 'assignedPosBalance', 'assignedPosBalanceRv') + initialMarginPercentageString = Precise.string_div(initialMarginString, notionalString) + liquidationPrice = self.safe_number_2(position, 'liquidationPrice', 'liquidationPriceRp') + markPriceString = self.safe_string_2(position, 'markPrice', 'markPriceRp') + contracts = self.safe_string_2(position, 'size', 'sizeRq') + contractSize = self.safe_value(market, 'contractSize') + contractSizeString = self.number_to_string(contractSize) + leverage = self.parse_number(Precise.string_abs((self.safe_string_2(position, 'leverage', 'leverageRr')))) + entryPriceString = self.safe_string_2(position, 'avgEntryPrice', 'avgEntryPriceRp') + rawSide = self.safe_string(position, 'side') + side = None + if rawSide is not None: + side = 'long' if (rawSide == 'Buy') else 'short' + # Inverse long contract: unRealizedPnl = (posSize * contractSize) / avgEntryPrice - (posSize * contractSize) / markPrice + # Inverse short contract: unRealizedPnl = (posSize *contractSize) / markPrice - (posSize * contractSize) / avgEntryPrice + # Linear long contract: unRealizedPnl = (posSize * contractSize) * markPrice - (posSize * contractSize) * avgEntryPrice + # Linear short contract: unRealizedPnl = (posSize * contractSize) * avgEntryPrice - (posSize * contractSize) * markPrice + priceDiff = None + if market['linear']: + if side == 'long': + priceDiff = Precise.string_sub(markPriceString, entryPriceString) + else: + priceDiff = Precise.string_sub(entryPriceString, markPriceString) + else: + # inverse + if side == 'long': + priceDiff = Precise.string_sub(Precise.string_div('1', entryPriceString), Precise.string_div('1', markPriceString)) + else: + priceDiff = Precise.string_sub(Precise.string_div('1', markPriceString), Precise.string_div('1', entryPriceString)) + unrealizedPnl = Precise.string_mul(Precise.string_mul(priceDiff, contracts), contractSizeString) + # the unrealizedPnl is only available in a specific endpoint which much higher RL limits + apiUnrealizedPnl = self.safe_string(position, 'unRealisedPnlRv', unrealizedPnl) + marginRatio = Precise.string_div(maintenanceMarginString, collateral) + isCross = self.safe_value(position, 'crossMargin') + return self.safe_position({ + 'info': position, + 'id': self.safe_string(position, 'execSeq'), + 'symbol': symbol, + 'contracts': self.parse_number(contracts), + 'contractSize': contractSize, + 'realizedPnl': self.safe_number(position, 'curTermRealisedPnlRv'), + 'unrealizedPnl': self.parse_number(apiUnrealizedPnl), + 'leverage': leverage, + 'liquidationPrice': liquidationPrice, + 'collateral': self.parse_number(collateral), + 'notional': self.parse_number(notionalString), + 'markPrice': self.parse_number(markPriceString), # markPrice lags a bit ¯\_(ツ)_/¯ + 'lastPrice': None, + 'entryPrice': self.parse_number(entryPriceString), + 'timestamp': None, + 'lastUpdateTimestamp': self.safe_integer_product(position, 'transactTimeNs', 0.000001), + 'initialMargin': self.parse_number(initialMarginString), + 'initialMarginPercentage': self.parse_number(initialMarginPercentageString), + 'maintenanceMargin': self.parse_number(maintenanceMarginString), + 'maintenanceMarginPercentage': self.parse_number(maintenanceMarginPercentageString), + 'marginRatio': self.parse_number(marginRatio), + 'datetime': None, + 'marginMode': 'cross' if isCross else 'isolated', + 'side': side, + 'hedged': self.safe_string(position, 'posMode') == 'Hedged', + 'percentage': None, + 'stopLossPrice': None, + 'takeProfitPrice': None, + }) + + async def fetch_funding_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch the history of funding payments paid and received on self account + + https://github.com/phemex/phemex-api-docs/blob/master/Public-Hedged-Perpetual-API.md#futureDataFundingFeesHist + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch funding history for + :param int [limit]: the maximum number of funding history structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding history structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchFundingHistory() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + # 'limit': 20, # Page size default 20, max 200 + # 'offset': 0, # Page start default 0 + } + if limit is not None: + if limit > 200: + raise BadRequest(self.id + ' fetchFundingHistory() limit argument cannot exceed 200') + request['limit'] = limit + response = None + isStableSettled = market['settle'] == 'USDT' or market['settle'] == 'USDC' + if isStableSettled: + response = await self.privateGetApiDataGFuturesFundingFees(self.extend(request, params)) + else: + response = await self.privateGetApiDataFuturesFundingFees(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "OK", + # "data": { + # "rows": [ + # { + # "symbol": "BTCUSD", + # "currency": "BTC", + # "execQty": 18, # "execQty" regular, but "execQtyRq" in hedge + # "side": "Buy", + # "execPriceEp": 360086455, # "execPriceEp" regular, but "execPriceRp" in hedge + # "execValueEv": 49987, # "execValueEv" regular, but "execValueRv" in hedge + # "fundingRateEr": 10000, # "fundingRateEr" regular, but "fundingRateRr" in hedge + # "feeRateEr": 10000, # "feeRateEr" regular, but "feeRateRr" in hedge + # "execFeeEv": 5, # "execFeeEv" regular, but "execFeeRv" in hedge + # "createTime": 1651881600000 + # } + # ] + # } + # } + # + data = self.safe_value(response, 'data', {}) + rows = self.safe_value(data, 'rows', []) + result = [] + for i in range(0, len(rows)): + entry = rows[i] + timestamp = self.safe_integer(entry, 'createTime') + execFee = self.safe_string_2(entry, 'execFeeEv', 'execFeeRv') + currencyCode = self.safe_currency_code(self.safe_string(entry, 'currency')) + result.append({ + 'info': entry, + 'symbol': self.safe_string(entry, 'symbol'), + 'code': currencyCode, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': None, + 'amount': self.parse_funding_fee_to_precision(execFee, market, currencyCode), + }) + return result + + def parse_funding_fee_to_precision(self, value, market: Market = None, currencyCode: Str = None): + if value is None or currencyCode is None: + return value + # it was confirmed by phemex support, that USDT contracts use direct amounts in funding fees, while USD & INVERSE needs 'valueScale' + isStableSettled = market['settle'] == 'USDT' or market['settle'] == 'USDC' + if not isStableSettled: + currency = self.safe_currency(currencyCode) + scale = self.safe_string(currency['info'], 'valueScale') + tickPrecision = self.parse_precision(scale) + value = Precise.string_mul(value, tickPrecision) + return value + + async def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + await self.load_markets() + market = self.market(symbol) + if not market['swap']: + raise BadSymbol(self.id + ' fetchFundingRate() supports swap contracts only') + request: dict = { + 'symbol': market['id'], + } + response: dict = {} + if not market['linear']: + response = await self.v1GetMdTicker24hr(self.extend(request, params)) + else: + response = await self.v2GetMdV2Ticker24hr(self.extend(request, params)) + # + # { + # "error": null, + # "id": 0, + # "result": { + # "askEp": 2332500, + # "bidEp": 2331000, + # "fundingRateEr": 10000, + # "highEp": 2380000, + # "indexEp": 2329057, + # "lastEp": 2331500, + # "lowEp": 2274000, + # "markEp": 2329232, + # "openEp": 2337500, + # "openInterest": 1298050, + # "predFundingRateEr": 19921, + # "symbol": "ETHUSD", + # "timestamp": 1592474241582701416, + # "turnoverEv": 47228362330, + # "volume": 4053863 + # } + # } + # + result = self.safe_value(response, 'result', {}) + return self.parse_funding_rate(result, market) + + def parse_funding_rate(self, contract, market: Market = None) -> FundingRate: + # + # { + # "askEp": 2332500, + # "bidEp": 2331000, + # "fundingRateEr": 10000, + # "highEp": 2380000, + # "indexEp": 2329057, + # "lastEp": 2331500, + # "lowEp": 2274000, + # "markEp": 2329232, + # "openEp": 2337500, + # "openInterest": 1298050, + # "predFundingRateEr": 19921, + # "symbol": "ETHUSD", + # "timestamp": 1592474241582701416, + # "turnoverEv": 47228362330, + # "volume": 4053863 + # } + # + # linear swap v2 + # + # { + # "closeRp":"16820.5", + # "fundingRateRr":"0.0001", + # "highRp":"16962.1", + # "indexPriceRp":"16830.15651565", + # "lowRp":"16785", + # "markPriceRp":"16830.97534951", + # "openInterestRv":"1323.596", + # "openRp":"16851.7", + # "predFundingRateRr":"0.0001", + # "symbol":"BTCUSDT", + # "timestamp":"1672142789065593096", + # "turnoverRv":"124835296.0538", + # "volumeRq":"7406.95" + # } + # + marketId = self.safe_string(contract, 'symbol') + symbol = self.safe_symbol(marketId, market) + timestamp = self.safe_integer_product(contract, 'timestamp', 0.000001) + markEp = self.from_ep(self.safe_string(contract, 'markEp'), market) + indexEp = self.from_ep(self.safe_string(contract, 'indexEp'), market) + fundingRateEr = self.from_er(self.safe_string(contract, 'fundingRateEr'), market) + nextFundingRateEr = self.from_er(self.safe_string(contract, 'predFundingRateEr'), market) + return { + 'info': contract, + 'symbol': symbol, + 'markPrice': self.safe_number(contract, 'markPriceRp', markEp), + 'indexPrice': self.safe_number(contract, 'indexPriceRp', indexEp), + 'interestRate': None, + 'estimatedSettlePrice': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'fundingRate': self.safe_number(contract, 'fundingRateRr', fundingRateEr), + 'fundingTimestamp': None, + 'fundingDatetime': None, + 'nextFundingRate': self.safe_number(contract, 'predFundingRateRr', nextFundingRateEr), + 'nextFundingTimestamp': None, + 'nextFundingDatetime': None, + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': None, + } + + async def set_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + Either adds or reduces margin in an isolated position in order to set the margin to a specific value + + https://github.com/phemex/phemex-api-docs/blob/master/Public-Contract-API-en.md#assign-position-balance-in-isolated-marign-mode + + :param str symbol: unified market symbol of the market to set margin in + :param float amount: the amount to set the margin to + :param dict [params]: parameters specific to the exchange API endpoint + :returns dict: A `margin structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'posBalanceEv': self.to_ev(amount, market), + } + response = await self.privatePostPositionsAssign(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "data": "OK" + # } + # + return self.extend(self.parse_margin_modification(response, market), { + 'amount': amount, + }) + + def parse_margin_status(self, status): + statuses: dict = { + '0': 'ok', + } + return self.safe_string(statuses, status, status) + + def parse_margin_modification(self, data: dict, market: Market = None) -> MarginModification: + # + # { + # "code": 0, + # "msg": "", + # "data": "OK" + # } + # + market = self.safe_market(None, market) + inverse = self.safe_value(market, 'inverse') + codeCurrency = 'base' if inverse else 'quote' + return { + 'info': data, + 'symbol': self.safe_symbol(None, market), + 'type': 'set', + 'marginMode': 'isolated', + 'amount': None, + 'total': None, + 'code': market[codeCurrency], + 'status': self.parse_margin_status(self.safe_string(data, 'code')), + 'timestamp': None, + 'datetime': None, + } + + async def set_margin_mode(self, marginMode: str, symbol: Str = None, params={}): + """ + set margin mode to 'cross' or 'isolated' + + https://phemex-docs.github.io/#set-leverage + + :param str marginMode: 'cross' or 'isolated' + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setMarginMode() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + if not market['swap']: + raise BadSymbol(self.id + ' setMarginMode() supports swap contracts only') + marginMode = marginMode.lower() + if marginMode != 'isolated' and marginMode != 'cross': + raise BadRequest(self.id + ' setMarginMode() marginMode argument should be isolated or cross') + request: dict = { + 'symbol': market['id'], + } + isCross = marginMode == 'cross' + if self.in_array(market['settle'], ['USDT', 'USDC']): + currentLeverage = self.safe_string(params, 'leverage') + if currentLeverage is None: + raise ArgumentsRequired(self.id + ' setMarginMode() requires a "leverage" parameter for USDT markets') + request['leverageRr'] = Precise.string_neg(Precise.string_abs(currentLeverage)) if isCross else Precise.string_abs(currentLeverage) + return await self.privatePutGPositionsLeverage(self.extend(request, params)) + leverage = self.safe_integer(params, 'leverage') + if marginMode == 'cross': + leverage = 0 + if leverage is None: + raise ArgumentsRequired(self.id + ' setMarginMode() requires a leverage parameter') + request['leverage'] = leverage + return await self.privatePutPositionsLeverage(self.extend(request, params)) + + async def set_position_mode(self, hedged: bool, symbol: Str = None, params={}): + """ + set hedged to True or False for a market + + https://github.com/phemex/phemex-api-docs/blob/master/Public-Hedged-Perpetual-API.md#switch-position-mode-synchronously + + :param bool hedged: set to True to use dualSidePosition + :param str symbol: not used by binance setPositionMode() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + self.check_required_argument('setPositionMode', symbol, 'symbol') + await self.load_markets() + market = self.market(symbol) + if market['settle'] != 'USDT': + raise BadSymbol(self.id + ' setPositionMode() supports USDT settled markets only') + request: dict = { + 'symbol': market['id'], + } + if hedged: + request['targetPosMode'] = 'Hedged' + else: + request['targetPosMode'] = 'OneWay' + return await self.privatePutGPositionsSwitchPosModeSync(self.extend(request, params)) + + async def fetch_leverage_tiers(self, symbols: Strings = None, params={}) -> LeverageTiers: + """ + retrieve information on the maximum leverage, and maintenance margin for trades of varying trade sizes + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `leverage tiers structures `, indexed by market symbols + """ + await self.load_markets() + if symbols is not None: + first = self.safe_value(symbols, 0) + market = self.market(first) + if market['settle'] != 'USD': + raise BadSymbol(self.id + ' fetchLeverageTiers() supports USD settled markets only') + response = await self.publicGetCfgV2Products(params) + # + # { + # "code":0, + # "msg":"OK", + # "data":{ + # "ratioScale":8, + # "currencies":[ + # {"currency":"BTC","valueScale":8,"minValueEv":1,"maxValueEv":5000000000000000000,"name":"Bitcoin"}, + # {"currency":"USD","valueScale":4,"minValueEv":1,"maxValueEv":500000000000000,"name":"USD"}, + # {"currency":"USDT","valueScale":8,"minValueEv":1,"maxValueEv":5000000000000000000,"name":"TetherUS"}, + # ], + # "products":[ + # { + # "symbol":"BTCUSD", + # "displaySymbol":"BTC / USD", + # "indexSymbol":".BTC", + # "markSymbol":".MBTC", + # "fundingRateSymbol":".BTCFR", + # "fundingRate8hSymbol":".BTCFR8H", + # "contractUnderlyingAssets":"USD", + # "settleCurrency":"BTC", + # "quoteCurrency":"USD", + # "contractSize":1.0, + # "lotSize":1, + # "tickSize":0.5, + # "priceScale":4, + # "ratioScale":8, + # "pricePrecision":1, + # "minPriceEp":5000, + # "maxPriceEp":10000000000, + # "maxOrderQty":1000000, + # "type":"Perpetual" + # }, + # { + # "symbol":"sBTCUSDT", + # "displaySymbol":"BTC / USDT", + # "quoteCurrency":"USDT", + # "pricePrecision":2, + # "type":"Spot", + # "baseCurrency":"BTC", + # "baseTickSize":"0.000001 BTC", + # "baseTickSizeEv":100, + # "quoteTickSize":"0.01 USDT", + # "quoteTickSizeEv":1000000, + # "minOrderValue":"10 USDT", + # "minOrderValueEv":1000000000, + # "maxBaseOrderSize":"1000 BTC", + # "maxBaseOrderSizeEv":100000000000, + # "maxOrderValue":"5,000,000 USDT", + # "maxOrderValueEv":500000000000000, + # "defaultTakerFee":"0.001", + # "defaultTakerFeeEr":100000, + # "defaultMakerFee":"0.001", + # "defaultMakerFeeEr":100000, + # "baseQtyPrecision":6, + # "quoteQtyPrecision":2 + # }, + # ], + # "riskLimits":[ + # { + # "symbol":"BTCUSD", + # "steps":"50", + # "riskLimits":[ + # {"limit":100,"initialMargin":"1.0%","initialMarginEr":1000000,"maintenanceMargin":"0.5%","maintenanceMarginEr":500000}, + # {"limit":150,"initialMargin":"1.5%","initialMarginEr":1500000,"maintenanceMargin":"1.0%","maintenanceMarginEr":1000000}, + # {"limit":200,"initialMargin":"2.0%","initialMarginEr":2000000,"maintenanceMargin":"1.5%","maintenanceMarginEr":1500000}, + # ] + # }, + # ], + # "leverages":[ + # {"initialMargin":"1.0%","initialMarginEr":1000000,"options":[1,2,3,5,10,25,50,100]}, + # {"initialMargin":"1.5%","initialMarginEr":1500000,"options":[1,2,3,5,10,25,50,66]}, + # {"initialMargin":"2.0%","initialMarginEr":2000000,"options":[1,2,3,5,10,25,33,50]}, + # ] + # } + # } + # + # + data = self.safe_value(response, 'data', {}) + riskLimits = self.safe_list(data, 'riskLimits') + return self.parse_leverage_tiers(riskLimits, symbols, 'symbol') + + def parse_market_leverage_tiers(self, info, market: Market = None) -> List[LeverageTier]: + """ + :param dict info: Exchange market response for 1 market + :param dict market: CCXT market + """ + # + # { + # "symbol":"BTCUSD", + # "steps":"50", + # "riskLimits":[ + # {"limit":100,"initialMargin":"1.0%","initialMarginEr":1000000,"maintenanceMargin":"0.5%","maintenanceMarginEr":500000}, + # {"limit":150,"initialMargin":"1.5%","initialMarginEr":1500000,"maintenanceMargin":"1.0%","maintenanceMarginEr":1000000}, + # {"limit":200,"initialMargin":"2.0%","initialMarginEr":2000000,"maintenanceMargin":"1.5%","maintenanceMarginEr":1500000}, + # ] + # }, + # + marketId = self.safe_string(info, 'symbol') + market = self.safe_market(marketId, market) + riskLimits = (market['info']['riskLimits']) + tiers = [] + minNotional = 0 + for i in range(0, len(riskLimits)): + tier = riskLimits[i] + maxNotional = self.safe_integer(tier, 'limit') + tiers.append({ + 'tier': self.sum(i, 1), + 'symbol': self.safe_symbol(marketId, market), + 'currency': market['settle'], + 'minNotional': minNotional, + 'maxNotional': maxNotional, + 'maintenanceMarginRate': self.safe_string(tier, 'maintenanceMargin'), + 'maxLeverage': None, + 'info': tier, + }) + minNotional = maxNotional + return tiers + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + query = self.omit(params, self.extract_params(path)) + requestPath = '/' + self.implode_params(path, params) + url = requestPath + queryString = '' + if (method == 'GET') or (method == 'DELETE') or (method == 'PUT') or (url == '/positions/assign'): + if query: + queryString = self.urlencode_with_array_repeat(query) + url += '?' + queryString + if api == 'private': + self.check_required_credentials() + timestamp = self.seconds() + xPhemexRequestExpiry = self.safe_integer(self.options, 'x-phemex-request-expiry', 60) + expiry = self.sum(timestamp, xPhemexRequestExpiry) + expiryString = str(expiry) + headers = { + 'x-phemex-access-token': self.apiKey, + 'x-phemex-request-expiry': expiryString, + } + payload = '' + if method == 'POST': + isOrderPlacement = (path == 'g-orders') or (path == 'spot/orders') or (path == 'orders') + if isOrderPlacement: + if self.safe_string(params, 'clOrdID') is None: + id = self.safe_string(self.options, 'brokerId', 'CCXT123456') + params['clOrdID'] = id + self.uuid16() + payload = self.json(params) + body = payload + headers['Content-Type'] = 'application/json' + auth = requestPath + queryString + expiryString + payload + headers['x-phemex-request-signature'] = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256) + url = self.implode_hostname(self.urls['api'][api]) + url + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + async def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://github.com/phemex/phemex-api-docs/blob/master/Public-Hedged-Perpetual-API.md#set-leverage + + :param float leverage: the rate of leverage, 100 > leverage > -100 excluding numbers between -1 to 1 + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.hedged]: set to True if hedged position mode is enabled(by default long and short leverage are set to the same value) + :param float [params.longLeverageRr]: *hedged mode only* set the leverage for long positions + :param float [params.shortLeverageRr]: *hedged mode only* set the leverage for short positions + :returns dict: response from the exchange + """ + # WARNING: THIS WILL INCREASE LIQUIDATION PRICE FOR OPEN ISOLATED LONG POSITIONS + # AND DECREASE LIQUIDATION PRICE FOR OPEN ISOLATED SHORT POSITIONS + if symbol is None: + raise ArgumentsRequired(self.id + ' setLeverage() requires a symbol argument') + if (leverage < -100) or (leverage > 100): + raise BadRequest(self.id + ' setLeverage() leverage should be between -100 and 100') + await self.load_markets() + isHedged = self.safe_bool(params, 'hedged', False) + longLeverageRr = self.safe_integer(params, 'longLeverageRr') + shortLeverageRr = self.safe_integer(params, 'shortLeverageRr') + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = None + if market['settle'] == 'USDT' or market['settle'] == 'USDC': + if not isHedged and longLeverageRr is None and shortLeverageRr is None: + request['leverageRr'] = leverage + else: + longVar = longLeverageRr if (longLeverageRr is not None) else leverage + shortVar = shortLeverageRr if (shortLeverageRr is not None) else leverage + request['longLeverageRr'] = longVar + request['shortLeverageRr'] = shortVar + response = await self.privatePutGPositionsLeverage(self.extend(request, params)) + else: + request['leverage'] = leverage + response = await self.privatePutPositionsLeverage(self.extend(request, params)) + return response + + async def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://phemex-docs.github.io/#transfer-between-spot-and-futures + https://phemex-docs.github.io/#universal-transfer-main-account-only-transfer-between-sub-to-main-main-to-sub-or-sub-to-sub + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account to transfer from + :param str toAccount: account to transfer to + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.bizType]: for transferring between main and sub-acounts either 'SPOT' or 'PERPETUAL' default is 'SPOT' + :returns dict: a `transfer structure ` + """ + await self.load_markets() + currency = self.currency(code) + accountsByType = self.safe_value(self.options, 'accountsByType', {}) + fromId = self.safe_string(accountsByType, fromAccount, fromAccount) + toId = self.safe_string(accountsByType, toAccount, toAccount) + scaledAmmount = self.to_ev(amount, currency) + direction = None + transfer = None + if fromId == 'spot' and toId == 'future': + direction = 2 + elif fromId == 'future' and toId == 'spot': + direction = 1 + if direction is not None: + request: dict = { + 'currency': currency['id'], + 'moveOp': direction, + 'amountEv': scaledAmmount, + } + response = await self.privatePostAssetsTransfer(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "OK", + # "data": { + # "linkKey": "8564eba4-c9ec-49d6-9b8c-2ec5001a0fb9", + # "userId": "4018340", + # "currency": "USD", + # "amountEv": "10", + # "side": "2", + # "status": "10" + # } + # } + # + data = self.safe_value(response, 'data', {}) + transfer = self.parse_transfer(data, currency) + else: # sub account transfer + request: dict = { + 'fromUserId': fromId, + 'toUserId': toId, + 'amountEv': scaledAmmount, + 'currency': currency['id'], + 'bizType': self.safe_string(params, 'bizType', 'SPOT'), + } + response = await self.privatePostAssetsUniversalTransfer(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "OK", + # "data": "API-923db826-aaaa-aaaa-aaaa-4d98c3a7c9fd" + # } + # + transfer = self.parse_transfer(response) + transferOptions = self.safe_value(self.options, 'transfer', {}) + fillResponseFromRequest = self.safe_bool(transferOptions, 'fillResponseFromRequest', True) + if fillResponseFromRequest: + if transfer['fromAccount'] is None: + transfer['fromAccount'] = fromAccount + if transfer['toAccount'] is None: + transfer['toAccount'] = toAccount + if transfer['amount'] is None: + transfer['amount'] = amount + if transfer['currency'] is None: + transfer['currency'] = code + return transfer + + async def fetch_transfers(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[TransferEntry]: + """ + fetch a history of internal transfers made on an account + + https://phemex-docs.github.io/#query-transfer-history + + :param str code: unified currency code of the currency transferred + :param int [since]: the earliest time in ms to fetch transfers for + :param int [limit]: the maximum number of transfers structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transfer structures ` + """ + await self.load_markets() + if code is None: + raise ArgumentsRequired(self.id + ' fetchTransfers() requires a code argument') + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + } + if since is not None: + request['start'] = since + if limit is not None: + request['limit'] = limit + response = await self.privateGetAssetsTransfer(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "OK", + # "data": { + # "rows": [ + # { + # "linkKey": "87c071a3-8628-4ac2-aca1-6ce0d1fad66c", + # "userId": 4148428, + # "currency": "BTC", + # "amountEv": 67932, + # "side": 2, + # "status": 10, + # "createTime": 1652832467000, + # "bizType": 10 + # } + # ] + # } + # } + # + data = self.safe_value(response, 'data', {}) + transfers = self.safe_list(data, 'rows', []) + return self.parse_transfers(transfers, currency, since, limit) + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + # + # transfer + # + # { + # "linkKey": "8564eba4-c9ec-49d6-9b8c-2ec5001a0fb9", + # "userId": "4018340", + # "currency": "USD", + # "amountEv": "10", + # "side": "2", + # "status": "10" + # } + # + # fetchTransfers + # + # { + # "linkKey": "87c071a3-8628-4ac2-aca1-6ce0d1fad66c", + # "userId": 4148428, + # "currency": "BTC", + # "amountEv": 67932, + # "side": 2, + # "status": 10, + # "createTime": 1652832467000, + # "bizType": 10 + # } + # + id = self.safe_string(transfer, 'linkKey') + status = self.safe_string(transfer, 'status') + amountEv = self.safe_string(transfer, 'amountEv') + amountTransfered = self.from_ev(amountEv) + currencyId = self.safe_string(transfer, 'currency') + code = self.safe_currency_code(currencyId, currency) + side = self.safe_integer(transfer, 'side') + fromId = None + toId = None + if side == 1: + fromId = 'swap' + toId = 'spot' + elif side == 2: + fromId = 'spot' + toId = 'swap' + timestamp = self.safe_integer(transfer, 'createTime') + return { + 'info': transfer, + 'id': id, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'currency': code, + 'amount': amountTransfered, + 'fromAccount': fromId, + 'toAccount': toId, + 'status': self.parse_transfer_status(status), + } + + def parse_transfer_status(self, status: Str) -> Str: + statuses: dict = { + '3': 'rejected', # 'Rejected', + '6': 'canceled', # 'Got error and wait for recovery', + '10': 'ok', # 'Success', + '11': 'failed', # 'Failed', + } + return self.safe_string(statuses, status, status) + + async def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical funding rate prices + + https://phemex-docs.github.io/#query-funding-rate-history-2 + + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: timestamp in ms of the earliest funding rate to fetch + :param int [limit]: the maximum amount of `funding rate structures ` to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param int [params.until]: timestamp in ms of the latest funding rate + :returns dict[]: a list of `funding rate structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchFundingRateHistory() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + isUsdtSettled = market['settle'] == 'USDT' or market['settle'] == 'USDC' + if not market['swap']: + raise BadRequest(self.id + ' fetchFundingRateHistory() supports swap contracts only') + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchFundingRateHistory', 'paginate') + if paginate: + return await self.fetch_paginated_call_deterministic('fetchFundingRateHistory', symbol, since, limit, '8h', params, 100) + customSymbol = None + if isUsdtSettled: + customSymbol = '.' + market['id'] + 'FR8H' # phemex requires a custom symbol for funding rate history + else: + customSymbol = '.' + market['baseId'] + 'FR8H' + request: dict = { + 'symbol': customSymbol, + } + if since is not None: + request['start'] = since + if limit is not None: + request['limit'] = limit + request, params = self.handle_until_option('end', request, params) + response = None + if isUsdtSettled: + response = await self.v2GetApiDataPublicDataFundingRateHistory(self.extend(request, params)) + else: + response = await self.v1GetApiDataPublicDataFundingRateHistory(self.extend(request, params)) + # + # { + # "code":"0", + # "msg":"OK", + # "data":{ + # "rows":[ + # { + # "symbol":".BTCUSDTFR8H", + # "fundingRate":"0.0001", + # "fundingTime":"1682064000000", + # "intervalSeconds":"28800" + # } + # ] + # } + # } + # + data = self.safe_value(response, 'data', {}) + rates = self.safe_value(data, 'rows') + result = [] + for i in range(0, len(rates)): + item = rates[i] + timestamp = self.safe_integer(item, 'fundingTime') + result.append({ + 'info': item, + 'symbol': symbol, + 'fundingRate': self.safe_number(item, 'fundingRate'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + sorted = self.sort_by(result, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, symbol, since, limit) + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://phemex-docs.github.io/#create-withdraw-request + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the phemex api endpoint + :param str [params.network]: unified network code + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + await self.load_markets() + self.check_address(address) + currency = self.currency(code) + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + networkId = None + if networkCode is not None: + networkId = self.network_code_to_id(networkCode) + stableCoins = self.safe_value(self.options, 'stableCoins') + if networkId is None: + if not (self.in_array(code, stableCoins)): + networkId = currency['id'] + else: + raise ArgumentsRequired(self.id + ' withdraw() requires an extra argument params["network"]') + request: dict = { + 'currency': currency['id'], + 'address': address, + 'amount': amount, + 'chainName': networkId.upper(), + } + if tag is not None: + request['addressTag'] = tag + response = await self.privatePostPhemexWithdrawWalletsApiCreateWithdraw(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "OK", + # "data": { + # "id": "10000001", + # "freezeId": null, + # "address": "44exxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + # "amountRv": "100", + # "chainCode": "11", + # "chainName": "TRX", + # "currency": "USDT", + # "currencyCode": 3, + # "email": "abc@gmail.com", + # "expiredTime": "0", + # "feeRv": "1", + # "nickName": null, + # "phone": null, + # "rejectReason": "", + # "submitedAt": "1670000000000", + # "submittedAt": "1670000000000", + # "txHash": null, + # "userId": "10000001", + # "status": "Success" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_transaction(data, currency) + + async def fetch_open_interest(self, symbol: str, params={}): + """ + retrieves the open interest of a trading pair + + https://phemex-docs.github.io/#query-24-hours-ticker + + :param str symbol: unified CCXT market symbol + :param dict [params]: exchange specific parameters + :returns dict} an open interest structure{@link https://docs.ccxt.com/#/?id=open-interest-structure: + """ + await self.load_markets() + market = self.market(symbol) + if not market['contract']: + raise BadRequest(self.id + ' fetchOpenInterest is only supported for contract markets.') + request: dict = { + 'symbol': market['id'], + } + response = await self.v2GetMdV2Ticker24hr(self.extend(request, params)) + # + # { + # error: null, + # id: '0', + # result: { + # closeRp: '67550.1', + # fundingRateRr: '0.0001', + # highRp: '68400', + # indexPriceRp: '67567.15389794', + # lowRp: '66096.4', + # markPriceRp: '67550.1', + # openInterestRv: '1848.1144186', + # openRp: '66330', + # predFundingRateRr: '0.0001', + # symbol: 'BTCUSDT', + # timestamp: '1729114315443343001', + # turnoverRv: '228863389.3237532', + # volumeRq: '3388.5600312' + # } + # } + # + result = self.safe_dict(response, 'result') + return self.parse_open_interest(result, market) + + def parse_open_interest(self, interest, market: Market = None): + # + # { + # closeRp: '67550.1', + # fundingRateRr: '0.0001', + # highRp: '68400', + # indexPriceRp: '67567.15389794', + # lowRp: '66096.4', + # markPriceRp: '67550.1', + # openInterestRv: '1848.1144186', + # openRp: '66330', + # predFundingRateRr: '0.0001', + # symbol: 'BTCUSDT', + # timestamp: '1729114315443343001', + # turnoverRv: '228863389.3237532', + # volumeRq: '3388.5600312' + # } + # + timestamp = self.safe_integer(interest, 'timestamp') / 1000000 + id = self.safe_string(interest, 'symbol') + return self.safe_open_interest({ + 'info': interest, + 'symbol': self.safe_symbol(id, market), + 'baseVolume': self.safe_string(interest, 'volumeRq'), + 'quoteVolume': None, # deprecated + 'openInterestAmount': self.safe_string(interest, 'openInterestRv'), + 'openInterestValue': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }, market) + + async def fetch_convert_quote(self, fromCode: str, toCode: str, amount: Num = None, params={}) -> Conversion: + """ + fetch a quote for converting from one currency to another + + https://phemex-docs.github.io/#rfq-quote + + :param str fromCode: the currency that you want to sell and convert from + :param str toCode: the currency that you want to buy and convert into + :param float amount: how much you want to trade in units of the from currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `conversion structure ` + """ + await self.load_markets() + fromCurrency = self.currency(fromCode) + toCurrency = self.currency(toCode) + valueScale = self.safe_integer(fromCurrency, 'valueScale') + request: dict = { + 'fromCurrency': fromCode, + 'toCurrency': toCode, + 'fromAmountEv': self.to_en(amount, valueScale), + } + response = await self.privateGetAssetsQuote(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "OK", + # "data": { + # "code": "GIF...AAA", + # "quoteArgs": { + # "origin": 10, + # "price": "0.00000939", + # "proceeds": "0.00000000", + # "ttlMs": 7000, + # "expireAt": 1739875826009, + # "requestAt": 1739875818009, + # "quoteAt": 1739875816594 + # } + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_conversion(data, fromCurrency, toCurrency) + + async def create_convert_trade(self, id: str, fromCode: str, toCode: str, amount: Num = None, params={}) -> Conversion: + """ + convert from one currency to another + + https://phemex-docs.github.io/#convert + + :param str id: the id of the trade that you want to make + :param str fromCode: the currency that you want to sell and convert from + :param str toCode: the currency that you want to buy and convert into + :param float [amount]: how much you want to trade in units of the from currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `conversion structure ` + """ + await self.load_markets() + fromCurrency = self.currency(fromCode) + toCurrency = self.currency(toCode) + valueScale = self.safe_integer(fromCurrency, 'valueScale') + request: dict = { + 'code': id, + 'fromCurrency': fromCode, + 'toCurrency': toCode, + } + if amount is not None: + request['fromAmountEv'] = self.to_en(amount, valueScale) + response = await self.privatePostAssetsConvert(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "OK", + # "data": { + # "moveOp": 0, + # "fromCurrency": "USDT", + # "toCurrency": "BTC", + # "fromAmountEv": 4000000000, + # "toAmountEv": 41511, + # "linkKey": "45c8ed8e-d3f4-472d-8262-e464e8c46247", + # "status": 10 + # } + # } + # + data = self.safe_dict(response, 'data', {}) + fromCurrencyId = self.safe_string(data, 'fromCurrency') + fromResult = self.safe_currency(fromCurrencyId, fromCurrency) + toCurrencyId = self.safe_string(data, 'toCurrency') + to = self.safe_currency(toCurrencyId, toCurrency) + return self.parse_conversion(data, fromResult, to) + + async def fetch_convert_trade_history(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Conversion]: + """ + fetch the users history of conversion trades + + https://phemex-docs.github.io/#query-convert-history + + :param str [code]: the unified currency code + :param int [since]: the earliest time in ms to fetch conversions for + :param int [limit]: the maximum number of conversion structures to retrieve, default 20, max 200 + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.until]: the end time in ms + :param str [params.fromCurrency]: the currency that you sold and converted from + :param str [params.toCurrency]: the currency that you bought and converted into + :returns dict[]: a list of `conversion structures ` + """ + await self.load_markets() + request: dict = {} + if code is not None: + request['fromCurrency'] = code + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + request, params = self.handle_until_option('endTime', request, params) + response = await self.privateGetAssetsConvert(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "OK", + # "data": { + # "total": 2, + # "rows": [ + # { + # "linkKey": "45c8ed8e-d3f4-472d-8262-e464e8c46247", + # "createTime": 1739882294000, + # "fromCurrency": "USDT", + # "toCurrency": "BTC", + # "fromAmountEv": 4000000000, + # "toAmountEv": 41511, + # "status": 10, + # "conversionRate": 1037, + # "errorCode": 0 + # }, + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + rows = self.safe_list(data, 'rows', []) + return self.parse_conversions(rows, code, 'fromCurrency', 'toCurrency', since, limit) + + def parse_conversion(self, conversion: dict, fromCurrency: Currency = None, toCurrency: Currency = None) -> Conversion: + # + # fetchConvertQuote + # + # { + # "code": "GIF...AAA", + # "quoteArgs": { + # "origin": 10, + # "price": "0.00000939", + # "proceeds": "0.00000000", + # "ttlMs": 7000, + # "expireAt": 1739875826009, + # "requestAt": 1739875818009, + # "quoteAt": 1739875816594 + # } + # } + # + # createConvertTrade + # + # { + # "moveOp": 0, + # "fromCurrency": "USDT", + # "toCurrency": "BTC", + # "fromAmountEv": 4000000000, + # "toAmountEv": 41511, + # "linkKey": "45c8ed8e-d3f4-472d-8262-e464e8c46247", + # "status": 10 + # } + # + # fetchConvertTradeHistory + # + # { + # "linkKey": "45c8ed8e-d3f4-472d-8262-e464e8c46247", + # "createTime": 1739882294000, + # "fromCurrency": "USDT", + # "toCurrency": "BTC", + # "fromAmountEv": 4000000000, + # "toAmountEv": 41511, + # "status": 10, + # "conversionRate": 1037, + # "errorCode": 0 + # } + # + quoteArgs = self.safe_dict(conversion, 'quoteArgs', {}) + requestTime = self.safe_integer(quoteArgs, 'requestAt') + timestamp = self.safe_integer(conversion, 'createTime', requestTime) + fromCoin = self.safe_string(conversion, 'fromCurrency', self.safe_string(fromCurrency, 'code')) + fromCode = self.safe_currency_code(fromCoin, fromCurrency) + toCoin = self.safe_string(conversion, 'toCurrency', self.safe_string(toCurrency, 'code')) + toCode = self.safe_currency_code(toCoin, toCurrency) + fromValueScale = self.safe_integer(fromCurrency, 'valueScale') + toValueScale = self.safe_integer(toCurrency, 'valueScale') + fromAmount = self.from_en(self.safe_string(conversion, 'fromAmountEv'), fromValueScale) + if fromAmount is None and quoteArgs is not None: + fromAmount = self.from_en(self.safe_string(quoteArgs, 'origin'), fromValueScale) + toAmount = self.from_en(self.safe_string(conversion, 'toAmountEv'), toValueScale) + if toAmount is None and quoteArgs is not None: + toAmount = self.from_en(self.safe_string(quoteArgs, 'proceeds'), toValueScale) + return { + 'info': conversion, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': self.safe_string(conversion, 'code'), + 'fromCurrency': fromCode, + 'fromAmount': self.parse_number(fromAmount), + 'toCurrency': toCode, + 'toAmount': self.parse_number(toAmount), + 'price': self.safe_number(quoteArgs, 'price'), + 'fee': None, + } + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None # fallback to default error handler + # + # {"code":30018,"msg":"phemex.data.size.uplimt","data":null} + # {"code":412,"msg":"Missing parameter - resolution","data":null} + # {"code":412,"msg":"Missing parameter - to","data":null} + # {"error":{"code":6001,"message":"invalid argument"},"id":null,"result":null} + # + error = self.safe_value(response, 'error', response) + errorCode = self.safe_string(error, 'code') + message = self.safe_string(error, 'msg') + if (errorCode is not None) and (errorCode != '0'): + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + raise ExchangeError(feedback) # unknown message + return None diff --git a/ccxt/async_support/poloniex.py b/ccxt/async_support/poloniex.py new file mode 100644 index 0000000..6b45961 --- /dev/null +++ b/ccxt/async_support/poloniex.py @@ -0,0 +1,3553 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.poloniex import ImplicitAPI +import asyncio +import hashlib +from ccxt.base.types import Any, Balances, Bool, Currencies, Currency, DepositAddress, Int, Leverage, MarginModification, Market, Num, Order, OrderBook, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, Trade, TradingFees, Transaction, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import AccountSuspended +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import NotSupported +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.errors import OnMaintenance +from ccxt.base.errors import RequestTimeout +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class poloniex(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(poloniex, self).describe(), { + 'id': 'poloniex', + 'name': 'Poloniex', + 'countries': ['US'], + # 200 requests per second for some unauthenticated market endpoints => 1000ms / 200 = 5ms between requests + 'rateLimit': 5, + 'certified': False, + 'pro': True, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': None, # has but not fully implemented + 'swap': True, + 'future': True, + 'option': False, + 'addMargin': True, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'cancelOrders': None, # not yet implemented, because RL is worse than cancelOrder + 'createDepositAddress': True, + 'createMarketBuyOrderWithCost': True, + 'createMarketOrderWithCost': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createOrders': None, # not yet implemented, because RL is worse than createOrder + 'createStopOrder': True, + 'createTriggerOrder': True, + 'editOrder': True, + 'fetchBalance': True, + 'fetchClosedOrder': False, + 'fetchClosedOrders': True, + 'fetchCurrencies': True, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchDepositsWithdrawals': True, + 'fetchDepositWithdrawFee': 'emulated', + 'fetchDepositWithdrawFees': True, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': None, # has but not implemented + 'fetchLedger': None, # has but not implemented + 'fetchLeverage': True, + 'fetchLiquidations': None, # has but not implemented + 'fetchMarginMode': False, + 'fetchMarkets': True, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterestHistory': False, + 'fetchOpenOrder': False, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrderBooks': False, + 'fetchOrderTrades': True, + 'fetchPosition': False, + 'fetchPositionMode': True, + 'fetchPositions': True, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': True, + 'fetchTransactions': 'emulated', + 'fetchTransfer': False, + 'fetchTransfers': False, + 'fetchWithdrawals': True, + 'reduceMargin': True, + 'sandbox': True, + 'setLeverage': True, + 'setPositionMode': True, + 'transfer': True, + 'withdraw': True, + }, + 'timeframes': { + '1m': 'MINUTE_1', + '5m': 'MINUTE_5', + '10m': 'MINUTE_10', # not in swap + '15m': 'MINUTE_15', + '30m': 'MINUTE_30', + '1h': 'HOUR_1', + '2h': 'HOUR_2', + '4h': 'HOUR_4', + '6h': 'HOUR_6', # not in swap + '12h': 'HOUR_12', + '1d': 'DAY_1', + '3d': 'DAY_3', + '1w': 'WEEK_1', + '1M': 'MONTH_1', # not in swap + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/27766817-e9456312-5ee6-11e7-9b3c-b628ca5626a5.jpg', + 'api': { + 'spot': 'https://api.poloniex.com', + 'swap': 'https://api.poloniex.com', + }, + 'test': { + 'spot': 'https://sand-spot-api-gateway.poloniex.com', + }, + 'www': 'https://www.poloniex.com', + 'doc': 'https://api-docs.poloniex.com/spot/', + 'fees': 'https://poloniex.com/fees', + 'referral': 'https://poloniex.com/signup?c=UBFZJRPJ', + }, + 'api': { + 'public': { + 'get': { + 'markets': 20, + 'markets/{symbol}': 1, + 'currencies': 20, + 'currencies/{currency}': 20, + 'v2/currencies': 20, + 'v2/currencies/{currency}': 20, + 'timestamp': 1, + 'markets/price': 1, + 'markets/{symbol}/price': 1, + 'markets/markPrice': 1, + 'markets/{symbol}/markPrice': 1, + 'markets/{symbol}/markPriceComponents': 1, + 'markets/{symbol}/orderBook': 1, + 'markets/{symbol}/candles': 1, + 'markets/{symbol}/trades': 20, + 'markets/ticker24h': 20, + 'markets/{symbol}/ticker24h': 20, + 'markets/collateralInfo': 1, + 'markets/{currency}/collateralInfo': 1, + 'markets/borrowRatesInfo': 1, + }, + }, + 'private': { + 'get': { + 'accounts': 4, + 'accounts/balances': 4, + 'accounts/{id}/balances': 4, + 'accounts/activity': 20, + 'accounts/transfer': 20, + 'accounts/transfer/{id}': 4, + 'feeinfo': 20, + 'accounts/interest/history': 1, + 'subaccounts': 4, + 'subaccounts/balances': 20, + 'subaccounts/{id}/balances': 4, + 'subaccounts/transfer': 20, + 'subaccounts/transfer/{id}': 4, + 'wallets/addresses': 20, + 'wallets/addresses/{currency}': 20, + 'wallets/activity': 20, + 'margin/accountMargin': 4, + 'margin/borrowStatus': 4, + 'margin/maxSize': 4, + 'orders': 20, + 'orders/{id}': 4, + 'orders/killSwitchStatus': 4, + 'smartorders': 20, + 'smartorders/{id}': 4, + 'orders/history': 20, + 'smartorders/history': 20, + 'trades': 20, + 'orders/{id}/trades': 4, + }, + 'post': { + 'accounts/transfer': 4, + 'subaccounts/transfer': 20, + 'wallets/address': 20, + 'wallets/withdraw': 20, + 'v2/wallets/withdraw': 20, + 'orders': 4, + 'orders/batch': 20, + 'orders/killSwitch': 4, + 'smartorders': 4, + }, + 'delete': { + 'orders/{id}': 4, + 'orders/cancelByIds': 20, + 'orders': 20, + 'smartorders/{id}': 4, + 'smartorders/cancelByIds': 20, + 'smartorders': 20, + }, + 'put': { + 'orders/{id}': 20, + 'smartorders/{id}': 20, + }, + }, + 'swapPublic': { + 'get': { + # 300 calls / second + 'v3/market/allInstruments': 2 / 3, + 'v3/market/instruments': 2 / 3, + 'v3/market/orderBook': 2 / 3, + 'v3/market/candles': 10, # candles have differnt RL + 'v3/market/indexPriceCandlesticks': 10, + 'v3/market/premiumIndexCandlesticks': 10, + 'v3/market/markPriceCandlesticks': 10, + 'v3/market/trades': 2 / 3, + 'v3/market/liquidationOrder': 2 / 3, + 'v3/market/tickers': 2 / 3, + 'v3/market/markPrice': 2 / 3, + 'v3/market/indexPrice': 2 / 3, + 'v3/market/indexPriceComponents': 2 / 3, + 'v3/market/fundingRate': 2 / 3, + 'v3/market/openInterest': 2 / 3, + 'v3/market/insurance': 2 / 3, + 'v3/market/riskLimit': 2 / 3, + }, + }, + 'swapPrivate': { + 'get': { + 'v3/account/balance': 4, + 'v3/account/bills': 20, + 'v3/trade/order/opens': 20, + 'v3/trade/order/trades': 20, + 'v3/trade/order/history': 20, + 'v3/trade/position/opens': 20, + 'v3/trade/position/history': 20, # todo: method for self + 'v3/position/leverages': 20, + 'v3/position/mode': 20, + }, + 'post': { + 'v3/trade/order': 4, + 'v3/trade/orders': 40, + 'v3/trade/position': 20, + 'v3/trade/positionAll': 100, + 'v3/position/leverage': 20, + 'v3/position/mode': 20, + 'v3/trade/position/margin': 20, + }, + 'delete': { + 'v3/trade/order': 2, + 'v3/trade/batchOrders': 20, + 'v3/trade/allOrders': 20, + }, + }, + }, + 'fees': { + 'trading': { + 'feeSide': 'get', + # starting from Jan 8 2020 + 'maker': self.parse_number('0.0009'), + 'taker': self.parse_number('0.0009'), + }, + 'funding': {}, + }, + 'commonCurrencies': { + 'AIR': 'AirCoin', + 'APH': 'AphroditeCoin', + 'BCC': 'BTCtalkcoin', + 'BCHABC': 'BCHABC', + 'BDG': 'Badgercoin', + 'BTM': 'Bitmark', + 'CON': 'Coino', + 'ETHTRON': 'ETH', + 'GOLD': 'GoldEagles', + 'GPUC': 'GPU', + 'HOT': 'Hotcoin', + 'ITC': 'Information Coin', + 'KEY': 'KEYCoin', + 'MASK': 'NFTX Hashmasks Index', # conflict with Mask Network + 'MEME': 'Degenerator Meme', # Degenerator Meme migrated to Meme Inu, self exchange still has the old price + 'PLX': 'ParallaxCoin', + 'REPV2': 'REP', + 'STR': 'XLM', + 'SOC': 'SOCC', + 'TRADE': 'Unitrade', + 'TRXETH': 'TRX', + 'XAP': 'API Coin', + # self is not documented in the API docs for Poloniex + # https://github.com/ccxt/ccxt/issues/7084 + # when the user calls withdraw('USDT', amount, address, tag, params) + # with params = {'currencyToWithdrawAs': 'USDTTRON'} + # or params = {'currencyToWithdrawAs': 'USDTETH'} + # fetchWithdrawals('USDT') returns the corresponding withdrawals + # with a USDTTRON or a USDTETH currency id, respectfully + # therefore we have map them back to the original code USDT + # otherwise the returned withdrawals are filtered out + 'USDTBSC': 'USDT', + 'USDTTRON': 'USDT', + 'USDTETH': 'USDT', + 'UST': 'USTC', + }, + 'options': { + 'defaultType': 'spot', + 'createMarketBuyOrderRequiresPrice': True, + 'networks': { + 'BEP20': 'BSC', + 'ERC20': 'ETH', + 'TRC20': 'TRON', + 'TRX': 'TRON', + }, + 'networksById': { + 'TRX': 'TRC20', + 'TRON': 'TRC20', + }, + 'limits': { + 'cost': { + 'min': { + 'BTC': 0.0001, + 'ETH': 0.0001, + 'USDT': 1.0, + 'TRX': 100, + 'BNB': 0.06, + 'USDC': 1.0, + 'USDJ': 1.0, + 'TUSD': 0.0001, + 'DAI': 1.0, + 'PAX': 1.0, + 'BUSD': 1.0, + }, + }, + }, + 'accountsByType': { + 'spot': 'spot', + 'future': 'futures', + }, + 'accountsById': { + 'exchange': 'spot', + 'futures': 'future', + }, + }, + 'features': { + 'default': { + 'sandbox': True, + 'createOrder': { + 'marginMode': True, # todo + 'triggerPrice': True, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'leverage': False, + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': True, # todo, only for non-trigger orders + 'trailing': False, + 'iceberg': False, + }, + 'createOrders': { + 'max': 20, + }, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 100000, + 'untilDays': 100000, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 2000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': None, # todo implement + 'fetchOHLCV': { + 'limit': 500, + }, + }, + 'spot': { + 'extends': 'default', + }, + 'forContracts': { + 'extends': 'default', + 'createOrder': { + 'marginMode': True, + 'triggerPrice': False, + 'hedged': True, + 'stpMode': True, # todo + 'marketBuyByCost': False, + }, + 'createOrders': { + 'max': 10, + }, + 'fetchOpenOrders': { + 'limit': 100, + }, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': None, + 'daysBackCanceled': 1 / 6, + 'untilDays': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchMyTrades': { + 'limit': 100, + 'untilDays': 90, + }, + }, + 'swap': { + 'linear': { + 'extends': 'forContracts', + }, + 'inverse': { + 'extends': 'forContracts', + }, + }, + 'future': { + 'linear': { + 'extends': 'forContracts', + }, + 'inverse': { + 'extends': 'forContracts', + }, + }, + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'exact': { + # General + '500': ExchangeNotAvailable, # Internal System Error + '603': RequestTimeout, # Internal Request Timeout + '601': BadRequest, # Invalid Parameter + '415': ExchangeError, # System Error + '602': ArgumentsRequired, # Missing Required Parameters + # Accounts + '21604': BadRequest, # Invalid UserId + '21600': AuthenticationError, # Account Not Found + '21605': AuthenticationError, # Invalid Account Type + '21102': ExchangeError, # Invalid Currency + '21100': AuthenticationError, # Invalid account + '21704': AuthenticationError, # Missing UserId and/or AccountId + '21700': BadRequest, # Error updating accounts + '21705': BadRequest, # Invalid currency type + '21707': ExchangeError, # Internal accounts Error + '21708': BadRequest, # Currency not available to User + '21601': AccountSuspended, # Account locked. Contact support + '21711': ExchangeError, # Currency locked. Contact support + '21709': InsufficientFunds, # Insufficient balance + '250000': ExchangeError, # Transfer error. Try again later + '250001': BadRequest, # Invalid toAccount for transfer + '250002': BadRequest, # Invalid fromAccount for transfer + '250003': BadRequest, # Invalid transfer amount + '250004': BadRequest, # Transfer is not supported + '250005': InsufficientFunds, # Insufficient transfer balance + '250008': BadRequest, # Invalid transfer currency + '250012': ExchangeError, # Futures account is not valid + # Trading + '21110': BadRequest, # Invalid quote currency + '10040': BadSymbol, # Invalid symbol + '10060': ExchangeError, # Symbol setup error + '10020': BadSymbol, # Invalid currency + '10041': BadSymbol, # Symbol frozen for trading + '21340': OnMaintenance, # No order creation/cancelation is allowed is in Maintenane Mode + '21341': InvalidOrder, # Post-only orders type allowed is in Post Only Mode + '21342': InvalidOrder, # Price is higher than highest bid is in Maintenance Mode + '21343': InvalidOrder, # Price is lower than lowest bid is in Maintenance Mode + '21351': AccountSuspended, # Trading for self account is frozen. Contact support + '21352': BadSymbol, # Trading for self currency is frozen + '21353': PermissionDenied, # Trading for US customers is not supported + '21354': PermissionDenied, # Account needs to be verified via email before trading is enabled. Contact support + '21359': OrderNotFound, # {"code" : 21359, "message" : "Order was already canceled or filled."} + '21360': InvalidOrder, # {"code" : 21360, "message" : "Order size exceeds the limit.Please enter a smaller amount and try again."} + '24106': BadRequest, # Invalid market depth + '24201': ExchangeNotAvailable, # Service busy. Try again later + # Orders + '21301': OrderNotFound, # Order not found + '21302': ExchangeError, # Batch cancel order error + '21304': ExchangeError, # Order is filled + '21305': OrderNotFound, # Order is canceled + '21307': ExchangeError, # Error during Order Cancelation + '21309': InvalidOrder, # Order price must be greater than 0 + '21310': InvalidOrder, # Order price must be less than max price + '21311': InvalidOrder, # Order price must be greater than min price + '21312': InvalidOrder, # Client orderId already exists + '21314': InvalidOrder, # Max limit of open orders(2000) exceeded + '21315': InvalidOrder, # Client orderId exceeded max length of 17 digits + '21317': InvalidOrder, # Amount must be greater than 0 + '21319': InvalidOrder, # Invalid order side + '21320': InvalidOrder, # Invalid order type + '21321': InvalidOrder, # Invalid timeInForce value + '21322': InvalidOrder, # Amount is less than minAmount trade limit + '21324': BadRequest, # Invalid account type + '21327': InvalidOrder, # Order pice must be greater than 0 + '21328': InvalidOrder, # Order quantity must be greater than 0 + '21330': InvalidOrder, # Quantity is less than minQuantity trade limit + '21335': InvalidOrder, # Invalid priceScale for self symbol + '21336': InvalidOrder, # Invalid quantityScale for self symbol + '21337': InvalidOrder, # Invalid amountScale for self symbol + '21344': InvalidOrder, # Value of limit param is greater than max value of 100 + '21345': InvalidOrder, # Value of limit param value must be greater than 0 + '21346': InvalidOrder, # Order Id must be of type Long + '21348': InvalidOrder, # Order type must be LIMIT_MAKER + '21347': InvalidOrder, # Stop price must be greater than 0 + '21349': InvalidOrder, # Order value is too large + '21350': InvalidOrder, # Amount must be greater than 1 USDT + '21355': ExchangeError, # Interval between startTime and endTime in trade/order history has exceeded 7 day limit + '21356': BadRequest, # Order size would cause too much price movement. Reduce order size. + '21721': InsufficientFunds, + '24101': BadSymbol, # Invalid symbol + '24102': InvalidOrder, # Invalid K-line type + '24103': InvalidOrder, # Invalid endTime + '24104': InvalidOrder, # Invalid amount + '24105': InvalidOrder, # Invalid startTime + '25020': InvalidOrder, # No active kill switch + # Smartorders + '25000': InvalidOrder, # Invalid userId + '25001': InvalidOrder, # Invalid parameter + '25002': InvalidOrder, # Invalid userId. + '25003': ExchangeError, # Unable to place order + '25004': InvalidOrder, # Client orderId already exists + '25005': ExchangeError, # Unable to place smart order + '25006': InvalidOrder, # OrderId and clientOrderId already exists + '25007': InvalidOrder, # Invalid orderid + '25008': InvalidOrder, # Both orderId and clientOrderId are required + '25009': ExchangeError, # Failed to cancel order + '25010': PermissionDenied, # Unauthorized to cancel order + '25011': InvalidOrder, # Failed to cancel due to invalid paramters + '25012': ExchangeError, # Failed to cancel + '25013': OrderNotFound, # Failed to cancel were not found + '25014': OrderNotFound, # Failed to cancel were not found + '25015': OrderNotFound, # Failed to cancel orders exist + '25016': ExchangeError, # Failed to cancel to release funds + '25017': ExchangeError, # No orders were canceled + '25018': BadRequest, # Invalid accountType + '25019': BadSymbol, # Invalid symbol + }, + 'broad': { + }, + }, + }) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # spot: + # + # [ + # [ + # "22814.01", + # "22937.42", + # "22832.57", + # "22937.42", + # "3916.58764051", + # "0.171199", + # "2982.64647063", + # "0.130295", + # 33, + # 0, + # "22877.449915304470460711", + # "MINUTE_5", + # 1659664800000, + # 1659665099999 + # ] + # ] + # + # contract: + # + # [ + # "84207.02", + # "84320.85", + # "84207.02", + # "84253.83", + # "3707.5395", + # "44", + # "14", + # "1740770040000", + # "1740770099999", + # ], + # + ohlcvLength = len(ohlcv) + isContract = ohlcvLength == 9 + if isContract: + return [ + self.safe_integer(ohlcv, 7), + self.safe_number(ohlcv, 2), + self.safe_number(ohlcv, 1), + self.safe_number(ohlcv, 0), + self.safe_number(ohlcv, 3), + self.safe_number(ohlcv, 5), + ] + return [ + self.safe_integer(ohlcv, 12), + self.safe_number(ohlcv, 2), + self.safe_number(ohlcv, 1), + self.safe_number(ohlcv, 0), + self.safe_number(ohlcv, 3), + self.safe_number(ohlcv, 5), + ] + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://api-docs.poloniex.com/spot/api/public/market-data#candles + https://api-docs.poloniex.com/v3/futures/api/market/get-kline-data + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate', False) + if paginate: + return await self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params, 500) + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'interval': self.safe_string(self.timeframes, timeframe, timeframe), + } + keyStart = 'startTime' if market['spot'] else 'sTime' + keyEnd = 'endTime' if market['spot'] else 'eTime' + if since is not None: + request[keyStart] = since + if limit is not None: + # limit should in between 100 and 500 + request['limit'] = limit + request, params = self.handle_until_option(keyEnd, request, params) + if market['contract']: + if self.in_array(timeframe, ['10m', '1M']): + raise NotSupported(self.id + ' ' + timeframe + ' ' + market['type'] + ' fetchOHLCV is not supported') + responseRaw = await self.swapPublicGetV3MarketCandles(self.extend(request, params)) + # + # { + # code: "200", + # msg: "Success", + # data: [ + # [ + # "84207.02", + # "84320.85", + # "84207.02", + # "84253.83", + # "3707.5395", + # "44", + # "14", + # "1740770040000", + # "1740770099999", + # ], + # + data = self.safe_list(responseRaw, 'data') + return self.parse_ohlcvs(data, market, timeframe, since, limit) + response = await self.publicGetMarketsSymbolCandles(self.extend(request, params)) + # + # [ + # [ + # "22814.01", + # "22937.42", + # "22832.57", + # "22937.42", + # "3916.58764051", + # "0.171199", + # "2982.64647063", + # "0.130295", + # 33, + # 0, + # "22877.449915304470460711", + # "MINUTE_5", + # 1659664800000, + # 1659665099999 + # ] + # ] + # + return self.parse_ohlcvs(response, market, timeframe, since, limit) + + async def load_markets(self, reload=False, params={}): + markets = await super(poloniex, self).load_markets(reload, params) + currenciesByNumericId = self.safe_value(self.options, 'currenciesByNumericId') + if (currenciesByNumericId is None) or reload: + self.options['currenciesByNumericId'] = self.index_by(self.currencies, 'numericId') + return markets + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for poloniex + + https://api-docs.poloniex.com/spot/api/public/reference-data#symbol-information + https://api-docs.poloniex.com/v3/futures/api/market/get-all-product-info + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + promises = [self.fetch_spot_markets(params), self.fetch_swap_markets(params)] + results = await asyncio.gather(*promises) + return self.array_concat(results[0], results[1]) + + async def fetch_spot_markets(self, params={}) -> List[Market]: + markets = await self.publicGetMarkets(params) + # + # [ + # { + # "symbol" : "BTS_BTC", + # "baseCurrencyName" : "BTS", + # "quoteCurrencyName" : "BTC", + # "displayName" : "BTS/BTC", + # "state" : "NORMAL", + # "visibleStartTime" : 1659018816626, + # "tradableStartTime" : 1659018816626, + # "symbolTradeLimit" : { + # "symbol" : "BTS_BTC", + # "priceScale" : 10, + # "quantityScale" : 0, + # "amountScale" : 8, + # "minQuantity" : "100", + # "minAmount" : "0.00001", + # "highestBid" : "0", + # "lowestAsk" : "0" + # } + # } + # ] + # + return self.parse_markets(markets) + + async def fetch_swap_markets(self, params={}) -> List[Market]: + # do similar per https://api-docs.poloniex.com/v3/futures/api/market/get-product-info + response = await self.swapPublicGetV3MarketAllInstruments(params) + # + # { + # "code": "200", + # "msg": "Success", + # "data": [ + # { + # "symbol": "BNB_USDT_PERP", + # "bAsset": ".PBNBUSDT", + # "bCcy": "BNB", + # "qCcy": "USDT", + # "visibleStartTime": "1620390600000", + # "tradableStartTime": "1620390600000", + # "sCcy": "USDT", + # "tSz": "0.001", + # "pxScale": "0.001,0.01,0.1,1,10", + # "lotSz": "1", + # "minSz": "1", + # "ctVal": "0.1", + # "status": "OPEN", + # "oDate": "1620287590000", + # "maxPx": "1000000", + # "minPx": "0.001", + # "maxQty": "1000000", + # "minQty": "1", + # "maxLever": "50", + # "lever": "10", + # "ctType": "LINEAR", + # "alias": "", + # "iM": "0.02", + # "mM": "0.0115", + # "mR": "2000", + # "buyLmt": "", + # "sellLmt": "", + # "ordPxRange": "0.05", + # "marketMaxQty": "2800", + # "limitMaxQty": "1000000" + # }, + # + markets = self.safe_list(response, 'data') + return self.parse_markets(markets) + + def parse_market(self, market: dict) -> Market: + if 'ctType' in market: + return self.parse_swap_market(market) + else: + return self.parse_spot_market(market) + + def parse_spot_market(self, market: dict) -> Market: + id = self.safe_string(market, 'symbol') + baseId = self.safe_string(market, 'baseCurrencyName') + quoteId = self.safe_string(market, 'quoteCurrencyName') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + state = self.safe_string(market, 'state') + active = state == 'NORMAL' + symbolTradeLimit = self.safe_value(market, 'symbolTradeLimit') + # these are known defaults + return { + 'id': id, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': active, + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(self.parse_precision(self.safe_string(symbolTradeLimit, 'quantityScale'))), + 'price': self.parse_number(self.parse_precision(self.safe_string(symbolTradeLimit, 'priceScale'))), + }, + 'limits': { + 'amount': { + 'min': self.safe_number(symbolTradeLimit, 'minQuantity'), + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': self.safe_number(symbolTradeLimit, 'minAmount'), + 'max': None, + }, + }, + 'created': self.safe_integer(market, 'tradableStartTime'), + 'info': market, + } + + def parse_swap_market(self, market: dict) -> Market: + # + # { + # "symbol": "BNB_USDT_PERP", + # "bAsset": ".PBNBUSDT", + # "bCcy": "BNB", + # "qCcy": "USDT", + # "visibleStartTime": "1620390600000", + # "tradableStartTime": "1620390600000", + # "sCcy": "USDT", + # "tSz": "0.001", + # "pxScale": "0.001,0.01,0.1,1,10", + # "lotSz": "1", + # "minSz": "1", + # "ctVal": "0.1", + # "status": "OPEN", + # "oDate": "1620287590000", + # "maxPx": "1000000", + # "minPx": "0.001", + # "maxQty": "1000000", + # "minQty": "1", + # "maxLever": "50", + # "lever": "10", + # "ctType": "LINEAR", + # "alias": "", + # "iM": "0.02", + # "mM": "0.0115", + # "mR": "2000", + # "buyLmt": "", + # "sellLmt": "", + # "ordPxRange": "0.05", + # "marketMaxQty": "2800", + # "limitMaxQty": "1000000" + # }, + # + id = self.safe_string(market, 'symbol') + baseId = self.safe_string(market, 'bCcy') + quoteId = self.safe_string(market, 'qCcy') + settleId = self.safe_string(market, 'sCcy') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + settle = self.safe_currency_code(settleId) + status = self.safe_string(market, 'status') + active = status == 'OPEN' + linear = market['ctType'] == 'LINEAR' + symbol = base + '/' + quote + if linear: + symbol += ':' + settle + else: + # actually, exchange does not have any inverse future now + symbol += ':' + base + alias = self.safe_string(market, 'alias') + type = 'swap' + if alias is not None: + type = 'future' + return { + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': 'future' if (type == 'future') else 'swap', + 'spot': False, + 'margin': False, + 'swap': type == 'swap', + 'future': type == 'future', + 'option': False, + 'active': active, + 'contract': True, + 'linear': linear, + 'inverse': not linear, + 'contractSize': self.safe_number(market, 'ctVal'), + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'taker': self.safe_number(market, 'tFee'), + 'maker': self.safe_number(market, 'mFee'), + 'precision': { + 'amount': self.safe_number(market, 'lotSz'), + 'price': self.safe_number(market, 'tSz'), + }, + 'limits': { + 'amount': { + 'min': self.safe_number(market, 'minSz'), + 'max': self.safe_number(market, 'limitMaxQty'), + }, + 'price': { + 'min': self.safe_number(market, 'minPx'), + 'max': self.safe_number(market, 'maxPx'), + }, + 'cost': { + 'min': None, + 'max': None, + }, + 'leverage': { + 'max': self.safe_number(market, 'maxLever'), + 'min': None, + }, + }, + 'created': self.safe_integer(market, 'oDate'), + 'info': market, + } + + async def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://api-docs.poloniex.com/spot/api/public/reference-data#system-timestamp + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = await self.publicGetTimestamp(params) + return self.safe_integer(response, 'serverTime') + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # spot: + # + # { + # "symbol" : "BTC_USDT", + # "open" : "26053.33", + # "low" : "26053.33", + # "high" : "26798.02", + # "close" : "26447.58", + # "quantity" : "6116.210188", + # "amount" : "161082122.88450926", + # "tradeCount" : "134709", + # "startTime" : "1692784440000", + # "closeTime" : "1692870839630", + # "displayName" : "BTC/USDT", + # "dailyChange" : "0.0151", + # "bid" : "26447.57", + # "bidQuantity" : "0.016313", + # "ask" : "26447.58", + # "askQuantity" : "0.068307", + # "ts" : "1692870845446", + # "markPrice" : "26444.11" + # } + # + # swap: + # + # { + # "s": "XRP_USDT_PERP", + # "o": "2.0503", + # "l": "2.0066", + # "h": "2.216", + # "c": "2.1798", + # "qty": "21090", + # "amt": "451339.65", + # "tC": "3267", + # "sT": "1740736380000", + # "cT": "1740822777559", + # "dN": "XRP/USDT/PERP", + # "dC": "0.0632", + # "bPx": "2.175", + # "bSz": "3", + # "aPx": "2.1831", + # "aSz": "111", + # "mPx": "2.1798", + # "iPx": "2.1834" + # }, + # + timestamp = self.safe_integer_2(ticker, 'ts', 'cT') + marketId = self.safe_string_2(ticker, 'symbol', 's') + market = self.safe_market(marketId) + relativeChange = self.safe_string_2(ticker, 'dailyChange', 'dc') + percentage = Precise.string_mul(relativeChange, '100') + return self.safe_ticker({ + 'id': marketId, + 'symbol': market['symbol'], + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string_2(ticker, 'high', 'h'), + 'low': self.safe_string_2(ticker, 'low', 'l'), + 'bid': self.safe_string_2(ticker, 'bid', 'bPx'), + 'bidVolume': self.safe_string_2(ticker, 'bidQuantity', 'bSz'), + 'ask': self.safe_string_2(ticker, 'ask', 'aPx'), + 'askVolume': self.safe_string_2(ticker, 'askQuantity', 'aSz'), + 'vwap': None, + 'open': self.safe_string_2(ticker, 'open', 'o'), + 'close': self.safe_string_2(ticker, 'close', 'c'), + 'previousClose': None, + 'change': None, + 'percentage': percentage, + 'average': None, + 'baseVolume': self.safe_string_2(ticker, 'quantity', 'qty'), + 'quoteVolume': self.safe_string_2(ticker, 'amount', 'amt'), + 'markPrice': self.safe_string_2(ticker, 'markPrice', 'mPx'), + 'indexPrice': self.safe_string(ticker, 'iPx'), + 'info': ticker, + }, market) + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://api-docs.poloniex.com/spot/api/public/market-data#ticker + https://api-docs.poloniex.com/v3/futures/api/market/get-market-info + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + market = None + request: dict = {} + if symbols is not None: + symbols = self.market_symbols(symbols, None, True, True, False) + symbolsLength = len(symbols) + if symbolsLength > 0: + market = self.market(symbols[0]) + if symbolsLength == 1: + request['symbol'] = market['id'] + marketType = None + marketType, params = self.handle_market_type_and_params('fetchTickers', market, params) + if marketType == 'swap': + responseRaw = await self.swapPublicGetV3MarketTickers(self.extend(request, params)) + # + # { + # "code": "200", + # "msg": "Success", + # "data": [ + # { + # "s": "XRP_USDT_PERP", + # "o": "2.0503", + # "l": "2.0066", + # "h": "2.216", + # "c": "2.1798", + # "qty": "21090", + # "amt": "451339.65", + # "tC": "3267", + # "sT": "1740736380000", + # "cT": "1740822777559", + # "dN": "XRP/USDT/PERP", + # "dC": "0.0632", + # "bPx": "2.175", + # "bSz": "3", + # "aPx": "2.1831", + # "aSz": "111", + # "mPx": "2.1798", + # "iPx": "2.1834" + # }, + # + data = self.safe_list(responseRaw, 'data') + return self.parse_tickers(data, symbols) + response = await self.publicGetMarketsTicker24h(params) + # + # [ + # { + # "symbol" : "BTC_USDT", + # "open" : "26053.33", + # "low" : "26053.33", + # "high" : "26798.02", + # "close" : "26447.58", + # "quantity" : "6116.210188", + # "amount" : "161082122.88450926", + # "tradeCount" : "134709", + # "startTime" : "1692784440000", + # "closeTime" : "1692870839630", + # "displayName" : "BTC/USDT", + # "dailyChange" : "0.0151", + # "bid" : "26447.57", + # "bidQuantity" : "0.016313", + # "ask" : "26447.58", + # "askQuantity" : "0.068307", + # "ts" : "1692870845446", + # "markPrice" : "26444.11" + # } + # ] + # + return self.parse_tickers(response, symbols) + + async def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://api-docs.poloniex.com/spot/api/public/reference-data#currency-information + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = await self.publicGetCurrencies(self.extend(params, {'includeMultiChainCurrencies': True})) + # + # [ + # { + # "USDT": { + # "id": 214, + # "name": "Tether USD", + # "description": "Sweep to Main Account", + # "type": "address", + # "withdrawalFee": "0.00000000", + # "minConf": 2, + # "depositAddress": null, + # "blockchain": "OMNI", + # "delisted": False, + # "tradingState": "NORMAL", + # "walletState": "DISABLED", + # "walletDepositState": "DISABLED", + # "walletWithdrawalState": "DISABLED", + # "supportCollateral": True, + # "supportBorrow": True, + # "parentChain": null, + # "isMultiChain": True, + # "isChildChain": False, + # "childChains": [ + # "USDTBSC", + # "USDTETH", + # "USDTSOL", + # "USDTTRON" + # ] + # } + # }, + # ... + # { + # "USDTBSC": { + # "id": 582, + # "name": "Binance-Peg BSC-USD", + # "description": "Sweep to Main Account", + # "type": "address", + # "withdrawalFee": "0.00000000", + # "minConf": 15, + # "depositAddress": null, + # "blockchain": "BSC", + # "delisted": False, + # "tradingState": "OFFLINE", + # "walletState": "ENABLED", + # "walletDepositState": "ENABLED", + # "walletWithdrawalState": "DISABLED", + # "supportCollateral": False, + # "supportBorrow": False, + # "parentChain": "USDT", + # "isMultiChain": True, + # "isChildChain": True, + # "childChains": [] + # } + # }, + # ... + # ] + # + result: dict = {} + # poloniex has a complicated structure of currencies, so we handle them differently + # at first, turn the response into a normal dictionary + currenciesDict = {} + for i in range(0, len(response)): + item = self.safe_dict(response, i) + ids = list(item.keys()) + id = self.safe_string(ids, 0) + currenciesDict[id] = item[id] + keys = list(currenciesDict.keys()) + for i in range(0, len(keys)): + id = keys[i] + entry = currenciesDict[id] + code = self.safe_currency_code(id) + # skip childChains, are collected in parentChain loop + if self.safe_bool(entry, 'isChildChain'): + continue + allChainEntries = [] + childChains = self.safe_list(entry, 'childChains', []) + if childChains is not None: + for j in range(0, len(childChains)): + childChainId = childChains[j] + childNetworkEntry = self.safe_dict(currenciesDict, childChainId) + allChainEntries.append(childNetworkEntry) + allChainEntries.append(entry) + networks: dict = {} + for j in range(0, len(allChainEntries)): + chainEntry = allChainEntries[j] + networkName = self.safe_string(chainEntry, 'blockchain') + networkCode = self.network_id_to_code(networkName, code) + specialNetworkId = self.safe_string(childChains, j, id) # in case it's primary chain, defeault to ID + networks[networkCode] = { + 'info': chainEntry, + 'id': specialNetworkId, # we need self for deposit/withdrawal, instead of friendly name + 'numericId': self.safe_integer(chainEntry, 'id'), + 'network': networkCode, + 'active': self.safe_bool(chainEntry, 'walletState'), + 'deposit': self.safe_string(chainEntry, 'walletDepositState') == 'ENABLED', + 'withdraw': self.safe_string(chainEntry, 'walletWithdrawalState') == 'ENABLED', + 'fee': self.safe_number(chainEntry, 'withdrawalFee'), + 'precision': None, + 'limits': { + 'withdraw': { + 'min': None, + 'max': None, + }, + 'deposit': { + 'min': None, + 'max': None, + }, + }, + } + result[code] = self.safe_currency_structure({ + 'info': entry, + 'code': code, + 'id': id, + 'numericId': self.safe_integer(entry, 'id'), + 'type': 'crypto', + 'name': self.safe_string(entry, 'name'), + 'active': None, + 'deposit': None, + 'withdraw': None, + 'fee': None, + 'precision': None, + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + 'deposit': { + 'min': None, + 'max': None, + }, + }, + 'networks': networks, + }) + return result + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://api-docs.poloniex.com/spot/api/public/market-data#ticker + https://api-docs.poloniex.com/v3/futures/api/market/get-market-info + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if market['contract']: + tickers = await self.fetch_tickers([market['symbol']], params) + return self.safe_dict(tickers, symbol) + response = await self.publicGetMarketsSymbolTicker24h(self.extend(request, params)) + # + # { + # "symbol" : "BTC_USDT", + # "open" : "26053.33", + # "low" : "26053.33", + # "high" : "26798.02", + # "close" : "26447.58", + # "quantity" : "6116.210188", + # "amount" : "161082122.88450926", + # "tradeCount" : "134709", + # "startTime" : "1692784440000", + # "closeTime" : "1692870839630", + # "displayName" : "BTC/USDT", + # "dailyChange" : "0.0151", + # "bid" : "26447.57", + # "bidQuantity" : "0.016313", + # "ask" : "26447.58", + # "askQuantity" : "0.068307", + # "ts" : "1692870845446", + # "markPrice" : "26444.11" + # } + # + return self.parse_ticker(response, market) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades + # + # spot: + # + # { + # "id" : "60014521", + # "price" : "23162.94", + # "quantity" : "0.00009", + # "amount" : "2.0846646", + # "takerSide" : "SELL", + # "ts" : 1659684602042, + # "createTime" : 1659684602036 + # } + # + # swap: + # + # { + # "id": "105807376", + # "side": "buy", + # "px": "84410.57", + # "qty": "1", + # "amt": "84.41057", + # "cT": "1740777563557", + # } + # + # fetchMyTrades + # + # spot: + # + # { + # "id": "32164924331503616", + # "symbol": "LINK_USDT", + # "accountType": "SPOT", + # "orderId": "32164923987566592", + # "side": "SELL", + # "type": "MARKET", + # "matchRole": "TAKER", + # "createTime": 1648635115525, + # "price": "11", + # "quantity": "0.5", + # "amount": "5.5", + # "feeCurrency": "USDT", + # "feeAmount": "0.007975", + # "pageId": "32164924331503616", + # "clientOrderId": "myOwnId-321" + # } + # + # swap: + # + # { + # "symbol": "BTC_USDT_PERP", + # "trdId": "105813553", + # "side": "SELL", + # "type": "TRADE", + # "mgnMode": "CROSS", + # "ordType": "MARKET", + # "clOrdId": "polo418912106147315112", + # "role": "TAKER", + # "px": "84704.9", + # "qty": "1", + # "cTime": "1740842829430", + # "uTime": "1740842829450", + # "feeCcy": "USDT", + # "feeAmt": "0.04235245", + # "deductCcy": "", + # "deductAmt": "0", + # "feeRate": "0.0005", + # "id": "418912106342654592", + # "posSide": "BOTH", + # "ordId": "418912106147315112", + # "qCcy": "USDT", + # "value": "84.7049", + # "actType": "TRADING" + # }, + # + # fetchOrderTrades(taker trades) + # + # { + # "id": "30341456333942784", + # "symbol": "LINK_USDT", + # "accountType": "SPOT", + # "orderId": "30249408733945856", + # "side": "BUY", + # "type": "LIMIT", + # "matchRole": "MAKER", + # "createTime": 1648200366864, + # "price": "3.1", + # "quantity": "1", + # "amount": "3.1", + # "feeCurrency": "LINK", + # "feeAmount": "0.00145", + # "pageId": "30341456333942784", + # "clientOrderId": "" + # } + # + id = self.safe_string_n(trade, ['id', 'tradeID', 'trdId']) + orderId = self.safe_string_2(trade, 'orderId', 'ordId') + timestamp = self.safe_integer_n(trade, ['ts', 'createTime', 'cT', 'cTime']) + marketId = self.safe_string(trade, 'symbol') + market = self.safe_market(marketId, market, '_') + symbol = market['symbol'] + side = self.safe_string_lower_2(trade, 'side', 'takerSide') + fee = None + priceString = self.safe_string_2(trade, 'price', 'px') + amountString = self.safe_string_2(trade, 'quantity', 'qty') + costString = self.safe_string_2(trade, 'amount', 'amt') + feeCurrencyId = self.safe_string_2(trade, 'feeCurrency', 'feeCcy') + feeCostString = self.safe_string_2(trade, 'feeAmount', 'feeAmt') + if feeCostString is not None: + feeCurrencyCode = self.safe_currency_code(feeCurrencyId) + fee = { + 'cost': feeCostString, + 'currency': feeCurrencyCode, + } + return self.safe_trade({ + 'id': id, + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'order': orderId, + 'type': self.safe_string_lower_2(trade, 'ordType', 'type'), # ordType should take precedence + 'side': side, + 'takerOrMaker': self.safe_string_lower_2(trade, 'matchRole', 'role'), + 'price': priceString, + 'amount': amountString, + 'cost': costString, + 'fee': fee, + }, market) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://api-docs.poloniex.com/spot/api/public/market-data#trades + https://api-docs.poloniex.com/v3/futures/api/market/get-execution-info + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['limit'] = limit # max 1000, for spot & swap + if market['contract']: + response = await self.swapPublicGetV3MarketTrades(self.extend(request, params)) + # + # { + # code: "200", + # msg: "Success", + # data: [ + # { + # id: "105807320", # descending order + # side: "sell", + # px: "84383.93", + # qty: "1", + # amt: "84.38393", + # cT: "1740777074704", + # }, + # + tradesList = self.safe_list(response, 'data') + return self.parse_trades(tradesList, market, since, limit) + trades = await self.publicGetMarketsSymbolTrades(self.extend(request, params)) + # + # [ + # { + # "id" : "60014521", + # "price" : "23162.94", + # "quantity" : "0.00009", + # "amount" : "2.0846646", + # "takerSide" : "SELL", + # "ts" : 1659684602042, + # "createTime" : 1659684602036 + # } + # ] + # + return self.parse_trades(trades, market, since, limit) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://api-docs.poloniex.com/spot/api/private/trade#trade-history + https://api-docs.poloniex.com/v3/futures/api/trade/get-execution-details + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch entries for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchMyTrades', 'paginate') + if paginate: + return await self.fetch_paginated_call_dynamic('fetchMyTrades', symbol, since, limit, params) + market: Market = None + if symbol is not None: + market = self.market(symbol) + marketType = None + marketType, params = self.handle_market_type_and_params('fetchMyTrades', market, params) + isContract = self.in_array(marketType, ['swap', 'future']) + request: dict = { + # 'from': 12345678, # A 'trade Id'. The query begins at ‘from'. + # 'direction': 'PRE', # PRE, NEXT The direction before or after ‘from'. + } + startKey = 'sTime' if isContract else 'startTime' + endKey = 'eTime' if isContract else 'endTime' + if since is not None: + request[startKey] = since + if limit is not None: + request['limit'] = limit + if isContract and symbol is not None: + request['symbol'] = market['id'] + request, params = self.handle_until_option(endKey, request, params) + if isContract: + raw = await self.swapPrivateGetV3TradeOrderTrades(self.extend(request, params)) + # + # { + # "code": "200", + # "msg": "", + # "data": [ + # { + # "symbol": "BTC_USDT_PERP", + # "trdId": "105813553", + # "side": "SELL", + # "type": "TRADE", + # "mgnMode": "CROSS", + # "ordType": "MARKET", + # "clOrdId": "polo418912106147315112", + # "role": "TAKER", + # "px": "84704.9", + # "qty": "1", + # "cTime": "1740842829430", + # "uTime": "1740842829450", + # "feeCcy": "USDT", + # "feeAmt": "0.04235245", + # "deductCcy": "", + # "deductAmt": "0", + # "feeRate": "0.0005", + # "id": "418912106342654592", + # "posSide": "BOTH", + # "ordId": "418912106147315112", + # "qCcy": "USDT", + # "value": "84.7049", + # "actType": "TRADING" + # }, + # + data = self.safe_list(raw, 'data') + return self.parse_trades(data, market, since, limit) + response = await self.privateGetTrades(self.extend(request, params)) + # + # [ + # { + # "id": "32164924331503616", + # "symbol": "LINK_USDT", + # "accountType": "SPOT", + # "orderId": "32164923987566592", + # "side": "SELL", + # "type": "MARKET", + # "matchRole": "TAKER", + # "createTime": 1648635115525, + # "price": "11", + # "quantity": "0.5", + # "amount": "5.5", + # "feeCurrency": "USDT", + # "feeAmount": "0.007975", + # "pageId": "32164924331503616", + # "clientOrderId": "myOwnId-321" + # } + # ] + # + result = self.parse_trades(response, market, since, limit) + return result + + def parse_order_status(self, status: Str): + statuses: dict = { + 'NEW': 'open', + 'PARTIALLY_FILLED': 'open', + 'FILLED': 'closed', + 'PENDING_CANCEL': 'canceled', + 'PARTIALLY_CANCELED': 'canceled', + 'CANCELED': 'canceled', + 'FAILED': 'canceled', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # fetchOpenOrder + # + # { + # "id" : "7xxxxxxxxxxxxxxx6", + # "clientOrderId" : "", + # "symbol" : "ETH_USDT", + # "state" : "NEW", + # "accountType" : "SPOT", + # "side" : "BUY", + # "type" : "LIMIT", + # "timeInForce" : "GTC", + # "quantity" : "0.001", + # "price" : "1600", + # "avgPrice" : "0", + # "amount" : "0", + # "filledQuantity" : "0", + # "filledAmount" : "0", + # "createTime" : 16xxxxxxxxx26, + # "updateTime" : 16xxxxxxxxx36 + # } + # + # fetchOpenOrders(and fetchClosedOrders same for contracts) + # + # spot: + # + # { + # "id": "24993088082542592", + # "clientOrderId": "", + # "symbol": "ELON_USDC", + # "state": "NEW", + # "accountType": "SPOT", + # "side": "SELL", + # "type": "MARKET", + # "timeInForce": "GTC", + # "quantity": "1.00", + # "price": "0.00", + # "avgPrice": "0.00", + # "amount": "0.00", + # "filledQuantity": "0.00", + # "filledAmount": "0.00", + # "createTime": 1646925216548, + # "updateTime": 1646925216548 + # } + # + # contract: + # + # { + # "symbol": "BTC_USDT_PERP", + # "side": "BUY", + # "type": "LIMIT", + # "ordId": "418890767248232148", + # "clOrdId": "polo418890767248232148", + # "mgnMode": "CROSS", + # "px": "81130.13", + # "reduceOnly": False, + # "lever": "20", + # "state": "NEW", + # "source": "WEB", + # "timeInForce": "GTC", + # "tpTrgPx": "", + # "tpPx": "", + # "tpTrgPxType": "", + # "slTrgPx": "", + # "slPx": "", + # "slTrgPxType": "", + # "avgPx": "0", + # "execQty": "0", + # "execAmt": "0", + # "feeCcy": "", + # "feeAmt": "0", + # "deductCcy": "0", + # "deductAmt": "0", + # "stpMode": "NONE", # todo: selfTradePrevention + # "cTime": "1740837741523", + # "uTime": "1740840846882", + # "sz": "1", + # "posSide": "BOTH", + # "qCcy": "USDT" + # "cancelReason": "", # self field can only be in closed orders + # }, + # + # createOrder, editOrder + # + # spot: + # + # { + # "id": "29772698821328896", + # "clientOrderId": "1234Abc" + # } + # + # contract: + # + # { + # "ordId":"418876147745775616", + # "clOrdId":"polo418876147745775616" + # } + # + timestamp = self.safe_integer_n(order, ['timestamp', 'createTime', 'cTime']) + if timestamp is None: + timestamp = self.parse8601(self.safe_string(order, 'date')) + marketId = self.safe_string(order, 'symbol') + market = self.safe_market(marketId, market, '_') + symbol = market['symbol'] + resultingTrades = self.safe_value(order, 'resultingTrades') + if resultingTrades is not None: + if not isinstance(resultingTrades, list): + resultingTrades = self.safe_value(resultingTrades, self.safe_string(market, 'id', marketId)) + price = self.safe_string_n(order, ['price', 'rate', 'px']) + amount = self.safe_string_2(order, 'quantity', 'sz') + filled = self.safe_string_2(order, 'filledQuantity', 'execQty') + status = self.parse_order_status(self.safe_string(order, 'state')) + side = self.safe_string_lower(order, 'side') + rawType = self.safe_string(order, 'type') + type = self.parse_order_type(rawType) + id = self.safe_string_n(order, ['orderNumber', 'id', 'orderId', 'ordId']) + fee = None + feeCurrency = self.safe_string_2(order, 'tokenFeeCurrency', 'feeCcy') + feeCost: Str = None + feeCurrencyCode: Str = None + rate = self.safe_string(order, 'fee') + if feeCurrency is None: + feeCurrencyCode = market['base'] if (side == 'buy') else market['quote'] + else: + # poloniex accepts a 30% discount to pay fees in TRX + feeCurrencyCode = self.safe_currency_code(feeCurrency) + feeCost = self.safe_string_2(order, 'tokenFee', 'feeAmt') + if feeCost is not None: + fee = { + 'rate': rate, + 'cost': feeCost, + 'currency': feeCurrencyCode, + } + clientOrderId = self.safe_string_2(order, 'clientOrderId', 'clOrdId') + marginMode = self.safe_string_lower(order, 'mgnMode') + reduceOnly = self.safe_bool(order, 'reduceOnly') + leverage = self.safe_integer(order, 'lever') + hedged = self.safe_string(order, 'posSide') != 'BOTH' + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': self.safe_integer(order, 'updateTime'), + 'status': status, + 'symbol': symbol, + 'type': type, + 'timeInForce': self.safe_string(order, 'timeInForce'), + 'postOnly': rawType == 'LIMIT_MAKER', + 'side': side, + 'price': price, + 'triggerPrice': self.safe_string_2(order, 'triggerPrice', 'stopPrice'), + 'cost': self.safe_string(order, 'execAmt'), + 'average': self.safe_string_2(order, 'avgPrice', 'avgPx'), + 'amount': amount, + 'filled': filled, + 'remaining': None, + 'trades': resultingTrades, + 'fee': fee, + 'marginMode': marginMode, + 'reduceOnly': reduceOnly, + 'leverage': leverage, + 'hedged': hedged, + }, market) + + def parse_order_type(self, status): + statuses: dict = { + 'MARKET': 'market', + 'LIMIT': 'limit', + 'LIMIT_MAKER': 'limit', + 'STOP-LIMIT': 'limit', + 'STOP-MARKET': 'market', + } + return self.safe_string(statuses, status, status) + + def parse_open_orders(self, orders, market, result): + for i in range(0, len(orders)): + order = orders[i] + extended = self.extend(order, { + 'status': 'open', + 'type': 'limit', + 'side': order['type'], + 'price': order['rate'], + }) + result.append(self.parse_order(extended, market)) + return result + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://api-docs.poloniex.com/spot/api/private/order#open-orders + https://api-docs.poloniex.com/spot/api/private/smart-order#open-orders # trigger orders + https://api-docs.poloniex.com/v3/futures/api/trade/get-current-orders + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: set True to fetch trigger orders instead of regular orders + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + market: Market = None + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + marketType = None + marketType, params = self.handle_market_type_and_params('fetchOpenOrders', market, params) + if limit is not None: + max = 2000 if (marketType == 'spot') else 100 + request['limit'] = max(limit, max) + isTrigger = self.safe_value_2(params, 'trigger', 'stop') + params = self.omit(params, ['trigger', 'stop']) + response = None + if marketType != 'spot': + raw = await self.swapPrivateGetV3TradeOrderOpens(self.extend(request, params)) + # + # { + # "code": "200", + # "msg": "", + # "data": [ + # { + # "symbol": "BTC_USDT_PERP", + # "side": "BUY", + # "type": "LIMIT", + # "ordId": "418890767248232148", + # "clOrdId": "polo418890767248232148", + # "mgnMode": "CROSS", + # "px": "81130.13", + # "reduceOnly": False, + # "lever": "20", + # "state": "NEW", + # "source": "WEB", + # "timeInForce": "GTC", + # "tpTrgPx": "", + # "tpPx": "", + # "tpTrgPxType": "", + # "slTrgPx": "", + # "slPx": "", + # "slTrgPxType": "", + # "avgPx": "0", + # "execQty": "0", + # "execAmt": "0", + # "feeCcy": "", + # "feeAmt": "0", + # "deductCcy": "0", + # "deductAmt": "0", + # "stpMode": "NONE", + # "cTime": "1740837741523", + # "uTime": "1740840846882", + # "sz": "1", + # "posSide": "BOTH", + # "qCcy": "USDT" + # }, + # + response = self.safe_list(raw, 'data') + elif isTrigger: + response = await self.privateGetSmartorders(self.extend(request, params)) + else: + response = await self.privateGetOrders(self.extend(request, params)) + # + # [ + # { + # "id" : "7xxxxxxxxxxxxxxx6", + # "clientOrderId" : "", + # "symbol" : "ETH_USDT", + # "state" : "NEW", + # "accountType" : "SPOT", + # "side" : "BUY", + # "type" : "LIMIT", + # "timeInForce" : "GTC", + # "quantity" : "0.001", + # "price" : "1600", + # "avgPrice" : "0", + # "amount" : "0", + # "filledQuantity" : "0", + # "filledAmount" : "0", + # "stopPrice": "3750.00", # for trigger orders + # "createTime" : 16xxxxxxxxx26, + # "updateTime" : 16xxxxxxxxx36 + # } + # ] + # + extension: dict = {'status': 'open'} + return self.parse_orders(response, market, since, limit, extension) + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + + https://api-docs.poloniex.com/v3/futures/api/trade/get-order-history + + fetches information on multiple closed orders made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest entry + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + market = None + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + marketType = None + marketType, params = self.handle_market_type_and_params('fetchClosedOrders', market, params, 'swap') + if marketType == 'spot': + raise NotSupported(self.id + ' fetchClosedOrders() is not supported for spot markets yet') + if limit is not None: + request['limit'] = min(200, limit) + if since is not None: + request['sTime'] = since + request, params = self.handle_until_option('eTime', request, params) + response = await self.swapPrivateGetV3TradeOrderHistory(self.extend(request, params)) + # + # { + # "code": "200", + # "msg": "", + # "data": [ + # { + # "symbol": "BTC_USDT_PERP", + # "side": "SELL", + # "type": "MARKET", + # "ordId": "418912106147315712", + # "clOrdId": "polo418912106147315712", + # "mgnMode": "CROSS", + # "px": "0", + # "sz": "2", + # "lever": "20", + # "state": "FILLED", + # "cancelReason": "", + # "source": "WEB", + # "reduceOnly": "true", + # "timeInForce": "GTC", + # "tpTrgPx": "", + # "tpPx": "", + # "tpTrgPxType": "", + # "slTrgPx": "", + # "slPx": "", + # "slTrgPxType": "", + # "avgPx": "84705.56", + # "execQty": "2", + # "execAmt": "169.41112", + # "feeCcy": "USDT", + # "feeAmt": "0.08470556", + # "deductCcy": "0", + # "deductAmt": "0", + # "stpMode": "NONE", + # "cTime": "1740842829116", + # "uTime": "1740842829130", + # "posSide": "BOTH", + # "qCcy": "USDT" + # }, + # + data = self.safe_list(response, 'data', []) + return self.parse_orders(data, market, since, limit) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://api-docs.poloniex.com/spot/api/private/order#create-order + https://api-docs.poloniex.com/spot/api/private/smart-order#create-order # trigger orders + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: the price at which a trigger order is triggered at + :param float [params.cost]: *spot market buy only* the quote quantity that can be used alternative for the amount + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'side': side.upper(), # uppercase, both for spot & swap + # 'timeInForce': timeInForce, # matches unified values + # 'accountType': 'SPOT', + # 'amount': amount, + } + triggerPrice = self.safe_number_2(params, 'stopPrice', 'triggerPrice') + request, params = self.order_request(symbol, type, side, amount, request, price, params) + response = None + if market['swap'] or market['future']: + responseInitial = await self.swapPrivatePostV3TradeOrder(self.extend(request, params)) + # + # {"code":200,"msg":"Success","data":{"ordId":"418876147745775616","clOrdId":"polo418876147745775616"}} + # + response = self.safe_dict(responseInitial, 'data') + elif triggerPrice is not None: + response = await self.privatePostSmartorders(self.extend(request, params)) + else: + response = await self.privatePostOrders(self.extend(request, params)) + # + # { + # "id" : "78923648051920896", + # "clientOrderId" : "" + # } + # + return self.parse_order(response, market) + + def order_request(self, symbol, type, side, amount, request, price=None, params={}): + triggerPrice = self.safe_number_2(params, 'stopPrice', 'triggerPrice') + market = self.market(symbol) + if market['contract']: + marginMode = None + marginMode, params = self.handle_param_string(params, 'marginMode') + if marginMode is not None: + self.check_required_argument('createOrder', marginMode, 'marginMode', ['cross', 'isolated']) + request['mgnMode'] = marginMode.upper() + hedged = None + hedged, params = self.handle_param_string(params, 'hedged') + if hedged: + if marginMode is None: + raise ArgumentsRequired(self.id + ' createOrder() requires a marginMode parameter "cross" or "isolated" for hedged orders') + if not ('posSide' in params): + raise ArgumentsRequired(self.id + ' createOrder() requires a posSide parameter "LONG" or "SHORT" for hedged orders') + upperCaseType = type.upper() + isMarket = upperCaseType == 'MARKET' + isPostOnly = self.is_post_only(isMarket, upperCaseType == 'LIMIT_MAKER', params) + params = self.omit(params, ['postOnly', 'triggerPrice', 'stopPrice']) + if triggerPrice is not None: + if not market['spot']: + raise InvalidOrder(self.id + ' createOrder() does not support trigger orders for ' + market['type'] + ' markets') + upperCaseType = 'STOP' if (price is None) else 'STOP_LIMIT' + request['stopPrice'] = triggerPrice + elif isPostOnly: + upperCaseType = 'LIMIT_MAKER' + request['type'] = upperCaseType + if isMarket: + if side == 'buy': + quoteAmount = None + createMarketBuyOrderRequiresPrice = True + createMarketBuyOrderRequiresPrice, params = self.handle_option_and_params(params, 'createOrder', 'createMarketBuyOrderRequiresPrice', True) + cost = self.safe_number(params, 'cost') + params = self.omit(params, 'cost') + if cost is not None: + quoteAmount = self.cost_to_precision(symbol, cost) + elif createMarketBuyOrderRequiresPrice and market['spot']: + if price is None: + raise InvalidOrder(self.id + ' createOrder() requires the price argument for market buy orders to calculate the total cost to spend(amount * price), alternatively set the createMarketBuyOrderRequiresPrice option or param to False and pass the cost to spend(quote quantity) in the amount argument') + else: + amountString = self.number_to_string(amount) + priceString = self.number_to_string(price) + costRequest = Precise.string_mul(amountString, priceString) + quoteAmount = self.cost_to_precision(symbol, costRequest) + else: + quoteAmount = self.cost_to_precision(symbol, amount) + amountKey = 'amount' if market['spot'] else 'sz' + request[amountKey] = quoteAmount + else: + amountKey = 'quantity' if market['spot'] else 'sz' + request[amountKey] = self.amount_to_precision(symbol, amount) + else: + amountKey = 'quantity' if market['spot'] else 'sz' + request[amountKey] = self.amount_to_precision(symbol, amount) + priceKey = 'price' if market['spot'] else 'px' + request[priceKey] = self.price_to_precision(symbol, price) + clientOrderId = self.safe_string(params, 'clientOrderId') + if clientOrderId is not None: + request['clientOrderId'] = clientOrderId + params = self.omit(params, 'clientOrderId') + # remember the timestamp before issuing the request + return [request, params] + + async def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + """ + edit a trade order + + https://api-docs.poloniex.com/spot/api/private/order#cancel-replace-order + https://api-docs.poloniex.com/spot/api/private/smart-order#cancel-replace-order + + :param str id: order id + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float [amount]: how much of the currency you want to trade in units of the base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: The price at which a trigger order is triggered at + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' editOrder() does not support ' + market['type'] + ' orders, only spot orders are accepted') + request: dict = { + 'id': id, + # 'timeInForce': timeInForce, + } + triggerPrice = self.safe_number_2(params, 'stopPrice', 'triggerPrice') + request, params = self.order_request(symbol, type, side, amount, request, price, params) + response = None + if triggerPrice is not None: + response = await self.privatePutSmartordersId(self.extend(request, params)) + else: + response = await self.privatePutOrdersId(self.extend(request, params)) + # + # { + # "id" : "78923648051920896", + # "clientOrderId" : "" + # } + # + response = self.extend(response, { + 'side': side, + 'type': type, + }) + return self.parse_order(response, market) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + # + # @method + # @name poloniex#cancelOrder + # @description cancels an open order + # @see https://api-docs.poloniex.com/spot/api/private/order#cancel-order-by-id + # @see https://api-docs.poloniex.com/spot/api/private/smart-order#cancel-order-by-id # trigger orders + # @param {string} id order id + # @param {string} symbol unified symbol of the market the order was made in + # @param {object} [params] extra parameters specific to the exchange API endpoint + # @param {boolean} [params.trigger] True if canceling a trigger order + # @returns {object} An `order structure ` + # + await self.load_markets() + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + market = self.market(symbol) + request: dict = {} + if not market['spot']: + request['symbol'] = market['id'] + request['ordId'] = id + raw = await self.swapPrivateDeleteV3TradeOrder(self.extend(request, params)) + # + # { + # "code": "200", + # "msg": "Success", + # "data": { + # "ordId": "418886099910612040", + # "clOrdId": "polo418886099910612040" + # } + # } + # + return self.parse_order(self.safe_dict(raw, 'data')) + clientOrderId = self.safe_value(params, 'clientOrderId') + if clientOrderId is not None: + id = clientOrderId + request['id'] = id + isTrigger = self.safe_value_2(params, 'trigger', 'stop') + params = self.omit(params, ['clientOrderId', 'trigger', 'stop']) + response = None + if isTrigger: + response = await self.privateDeleteSmartordersId(self.extend(request, params)) + else: + response = await self.privateDeleteOrdersId(self.extend(request, params)) + # + # { + # "orderId":"210832697138888704", + # "clientOrderId":"", + # "state":"PENDING_CANCEL", + # "code":200, + # "message":"" + # } + # + return self.parse_order(response) + + async def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders + + https://api-docs.poloniex.com/spot/api/private/order#cancel-all-orders + https://api-docs.poloniex.com/spot/api/private/smart-order#cancel-all-orders # trigger orders + https://api-docs.poloniex.com/v3/futures/api/trade/cancel-all-orders - contract markets + + :param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: True if canceling trigger orders + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + request: dict = { + # 'accountTypes': 'SPOT', + 'symbols': [], + } + market: Market = None + if symbol is not None: + market = self.market(symbol) + request['symbols'] = [ + market['id'], + ] + response = None + marketType = None + marketType, params = self.handle_market_type_and_params('cancelAllOrders', market, params) + if marketType == 'swap' or marketType == 'future': + raw = await self.swapPrivateDeleteV3TradeAllOrders(self.extend(request, params)) + # + # { + # "code": "200", + # "msg": "Success", + # "data": [ + # { + # "code": "200", + # "msg": "Success", + # "ordId": "418885787866388511", + # "clOrdId": "polo418885787866388511" + # } + # ] + # } + # + response = self.safe_list(raw, 'data') + return self.parse_orders(response, market) + isTrigger = self.safe_value_2(params, 'trigger', 'stop') + params = self.omit(params, ['trigger', 'stop']) + if isTrigger: + response = await self.privateDeleteSmartorders(self.extend(request, params)) + else: + response = await self.privateDeleteOrders(self.extend(request, params)) + # + # [ + # { + # "orderId" : "78xxxxxxxx80", + # "clientOrderId" : "", + # "state" : "NEW", + # "code" : 200, + # "message" : "" + # }, { + # "orderId" : "78xxxxxxxxx80", + # "clientOrderId" : "", + # "state" : "NEW", + # "code" : 200, + # "message" : "" + # } + # ] + # + return self.parse_orders(response, market) + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetch an order by it's id + + https://api-docs.poloniex.com/spot/api/private/order#order-details + https://api-docs.poloniex.com/spot/api/private/smart-order#open-orders # trigger orders + + :param str id: order id + :param str symbol: unified market symbol, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: True if fetching a trigger order + :returns dict: an `order structure ` + """ + await self.load_markets() + id = str(id) + request: dict = { + 'id': id, + } + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + marketType = None + marketType, params = self.handle_market_type_and_params('fetchOrder', market, params) + if marketType != 'spot': + raise NotSupported(self.id + ' fetchOrder() is not supported for ' + marketType + ' markets yet') + isTrigger = self.safe_value_2(params, 'trigger', 'stop') + params = self.omit(params, ['trigger', 'stop']) + response = None + if isTrigger: + response = await self.privateGetSmartordersId(self.extend(request, params)) + response = self.safe_value(response, 0) + else: + response = await self.privateGetOrdersId(self.extend(request, params)) + # + # { + # "id": "21934611974062080", + # "clientOrderId": "123", + # "symbol": "TRX_USDC", + # "state": "NEW", + # "accountType": "SPOT", + # "side": "SELL", + # "type": "LIMIT", + # "timeInForce": "GTC", + # "quantity": "1.00", + # "price": "10.00", + # "avgPrice": "0.00", + # "amount": "0.00", + # "filledQuantity": "0.00", + # "filledAmount": "0.00", + # "stopPrice": "3750.00", # for trigger orders + # "createTime": 1646196019020, + # "updateTime": 1646196019020 + # } + # + order = self.parse_order(response) + order['id'] = id + return order + + async def fetch_order_status(self, id: str, symbol: Str = None, params={}): + await self.load_markets() + orders = await self.fetch_open_orders(symbol, None, None, params) + indexed = self.index_by(orders, 'id') + return 'open' if (id in indexed) else 'closed' + + async def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all the trades made from a single order + + https://api-docs.poloniex.com/spot/api/private/trade#trades-by-order-id + + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + request: dict = { + 'id': id, + } + trades = await self.privateGetOrdersIdTrades(self.extend(request, params)) + # + # [ + # { + # "id": "30341456333942784", + # "symbol": "LINK_USDT", + # "accountType": "SPOT", + # "orderId": "30249408733945856", + # "side": "BUY", + # "type": "LIMIT", + # "matchRole": "MAKER", + # "createTime": 1648200366864, + # "price": "3.1", + # "quantity": "1", + # "amount": "3.1", + # "feeCurrency": "LINK", + # "feeAmount": "0.00145", + # "pageId": "30341456333942784", + # "clientOrderId": "" + # } + # ] + # + return self.parse_trades(trades) + + def parse_balance(self, response) -> Balances: + result: dict = { + 'info': response, + 'timestamp': None, + 'datetime': None, + } + # for swap + if not isinstance(response, list): + ts = self.safe_integer(response, 'uTime') + result['timestamp'] = ts + result['datetime'] = self.iso8601(ts) + details = self.safe_list(response, 'details', []) + for i in range(0, len(details)): + balance = details[i] + currencyId = self.safe_string(balance, 'ccy') + code = self.safe_currency_code(currencyId) + account = self.account() + account['total'] = self.safe_string(balance, 'avail') + account['used'] = self.safe_string(balance, 'im') + result[code] = account + return self.safe_balance(result) + # for spot + for i in range(0, len(response)): + account = self.safe_value(response, i, {}) + balances = self.safe_value(account, 'balances') + for j in range(0, len(balances)): + balance = self.safe_value(balances, j) + currencyId = self.safe_string(balance, 'currency') + code = self.safe_currency_code(currencyId) + newAccount = self.account() + newAccount['free'] = self.safe_string(balance, 'available') + newAccount['used'] = self.safe_string(balance, 'hold') + result[code] = newAccount + return self.safe_balance(result) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://api-docs.poloniex.com/spot/api/private/account#all-account-balances + https://api-docs.poloniex.com/v3/futures/api/account/balance + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + marketType = None + marketType, params = self.handle_market_type_and_params('fetchBalance', None, params) + if marketType != 'spot': + responseRaw = await self.swapPrivateGetV3AccountBalance(params) + # + # { + # "code": "200", + # "msg": "", + # "data": { + # "state": "NORMAL", + # "eq": "9.98571622", + # "isoEq": "0", + # "im": "0", + # "mm": "0", + # "mmr": "0", + # "upl": "0", + # "availMgn": "9.98571622", + # "cTime": "1738093601775", + # "uTime": "1740829116236", + # "details": [ + # { + # "ccy": "USDT", + # "eq": "9.98571622", + # "isoEq": "0", + # "avail": "9.98571622", + # "trdHold": "0", + # "upl": "0", + # "isoAvail": "0", + # "isoHold": "0", + # "isoUpl": "0", + # "im": "0", + # "mm": "0", + # "mmr": "0", + # "imr": "0", + # "cTime": "1740829116236", + # "uTime": "1740829116236" + # } + # ] + # } + # } + # + data = self.safe_dict(responseRaw, 'data', {}) + return self.parse_balance(data) + request: dict = { + 'accountType': 'SPOT', + } + response = await self.privateGetAccountsBalances(self.extend(request, params)) + # + # [ + # { + # "accountId" : "7xxxxxxxxxx8", + # "accountType" : "SPOT", + # "balances" : [ + # { + # "currencyId" : "214", + # "currency" : "USDT", + # "available" : "2.00", + # "hold" : "0.00" + # } + # ] + # } + # ] + # + return self.parse_balance(response) + + async def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://api-docs.poloniex.com/spot/api/private/account#fee-info + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + await self.load_markets() + response = await self.privateGetFeeinfo(params) + # + # { + # "trxDiscount" : False, + # "makerRate" : "0.00145", + # "takerRate" : "0.00155", + # "volume30D" : "0.00" + # } + # + result: dict = {} + for i in range(0, len(self.symbols)): + symbol = self.symbols[i] + result[symbol] = { + 'info': response, + 'symbol': symbol, + 'maker': self.safe_number(response, 'makerRate'), + 'taker': self.safe_number(response, 'takerRate'), + 'percentage': True, + 'tierBased': True, + } + return result + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://api-docs.poloniex.com/spot/api/public/market-data#order-book + https://api-docs.poloniex.com/v3/futures/api/market/get-order-book + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['limit'] = limit # The default value of limit is 10. Valid limit values are: 5, 10, 20, 50, 100, 150. + if market['contract']: + request['limit'] = self.find_nearest_ceiling([5, 10, 20, 100, 150], limit) + if market['contract']: + responseRaw = await self.swapPublicGetV3MarketOrderBook(self.extend(request, params)) + # + # { + # "code": 200, + # "data": { + # "asks": [["58700", "9934"], ..], + # "bids": [["58600", "9952"], ..], + # "s": "100", + # "ts": 1719974138333 + # }, + # "msg": "Success" + # } + # + data = self.safe_dict(responseRaw, 'data', {}) + ts = self.safe_integer(data, 'ts') + return self.parse_order_book(data, symbol, ts) + response = await self.publicGetMarketsSymbolOrderBook(self.extend(request, params)) + # + # { + # "time" : 1659695219507, + # "scale" : "-1", + # "asks" : ["23139.82", "0.317981", "23140", "0.191091", "23170.06", "0.01", "23200", "0.107758", "23230.55", "0.01", "23247.2", "0.154", "23254", "0.005121", "23263", "0.038", "23285.4", "0.308", "23300", "0.108896"], + # "bids" : ["23139.74", "0.432092", "23139.73", "0.198592", "23123.21", "0.000886", "23123.2", "0.308", "23121.4", "0.154", "23105", "0.000789", "23100", "0.078175", "23069.1", "0.026276", "23068.83", "0.001329", "23051", "0.000048"], + # "ts" : 1659695219512 + # } + # + timestamp = self.safe_integer(response, 'time') + asks = self.safe_value(response, 'asks') + bids = self.safe_value(response, 'bids') + asksResult = [] + bidsResult = [] + for i in range(0, len(asks)): + if (i % 2) < 1: + price = self.safe_number(asks, i) + amount = self.safe_number(asks, self.sum(i, 1)) + asksResult.append([price, amount]) + for i in range(0, len(bids)): + if (i % 2) < 1: + price = self.safe_number(bids, i) + amount = self.safe_number(bids, self.sum(i, 1)) + bidsResult.append([price, amount]) + return { + 'symbol': market['symbol'], + 'bids': self.sort_by(bidsResult, 0, True), + 'asks': self.sort_by(asksResult, 0), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'nonce': None, + } + + async def create_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + create a currency deposit address + + https://api-docs.poloniex.com/spot/api/private/wallet#deposit-addresses + + :param str code: unified currency code of the currency for the deposit address + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + await self.load_markets() + request, extraParams, currency, networkEntry = self.prepare_request_for_deposit_address(code, params) + params = extraParams + response = await self.privatePostWalletsAddress(self.extend(request, params)) + # + # { + # "address" : "0xfxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxf" + # } + # + return self.parse_deposit_address_special(response, currency, networkEntry) + + async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://api-docs.poloniex.com/spot/api/private/wallet#deposit-addresses + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + await self.load_markets() + request, extraParams, currency, networkEntry = self.prepare_request_for_deposit_address(code, params) + params = extraParams + response = await self.privateGetWalletsAddresses(self.extend(request, params)) + # + # { + # "USDTTRON" : "Txxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxp" + # } + # + keys = list(response.keys()) + length = len(keys) + if length < 1: + raise ExchangeError(self.id + ' fetchDepositAddress() returned an empty response, you might need to try "createDepositAddress" at first and then use "fetchDepositAddress"') + return self.parse_deposit_address_special(response, currency, networkEntry) + + def prepare_request_for_deposit_address(self, code: str, params: dict = {}) -> Any: + if not (code in self.currencies): + raise BadSymbol(self.id + ' fetchDepositAddress(): can not recognize ' + code + ' currency, you might try using unified currency-code and add provide specific "network" parameter, like: fetchDepositAddress("USDT", {"network": "TRC20"})') + currency = self.currency(code) + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + if networkCode is None: + # we need to know the network to find out the currency-junction + raise ArgumentsRequired(self.id + ' fetchDepositAddress requires a network parameter for ' + code + '.') + exchangeNetworkId = None + networkCode = self.network_id_to_code(networkCode, code) + networkEntry = self.safe_dict(currency['networks'], networkCode) + if networkEntry is not None: + exchangeNetworkId = networkEntry['id'] + else: + exchangeNetworkId = networkCode + request = { + 'currency': exchangeNetworkId, + } + return [request, params, currency, networkEntry] + + def parse_deposit_address_special(self, response, currency, networkEntry) -> DepositAddress: + address = self.safe_string(response, 'address') + if address is None: + address = self.safe_string(response, networkEntry['id']) + tag: Str = None + self.check_address(address) + if networkEntry is not None: + depositAddress = self.safe_string(networkEntry['info'], 'depositAddress') + if depositAddress is not None: + tag = address + address = depositAddress + return { + 'info': response, + 'currency': currency['code'], + 'network': self.safe_string(networkEntry, 'network'), + 'address': address, + 'tag': tag, + } + + async def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://api-docs.poloniex.com/spot/api/private/account#accounts-transfer + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account to transfer from + :param str toAccount: account to transfer to + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transfer structure ` + """ + await self.load_markets() + currency = self.currency(code) + accountsByType = self.safe_value(self.options, 'accountsByType', {}) + fromId = self.safe_string(accountsByType, fromAccount, fromAccount) + toId = self.safe_string(accountsByType, toAccount, fromAccount) + request: dict = { + 'amount': self.currency_to_precision(code, amount), + 'currency': currency['id'], + 'fromAccount': fromId, + 'toAccount': toId, + } + response = await self.privatePostAccountsTransfer(self.extend(request, params)) + # + # { + # "transferId" : "168041074" + # } + # + return self.parse_transfer(response, currency) + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + # + # { + # "transferId" : "168041074" + # } + # + return { + 'info': transfer, + 'id': self.safe_string(transfer, 'transferId'), + 'timestamp': None, + 'datetime': None, + 'currency': self.safe_string(currency, 'id'), + 'amount': None, + 'fromAccount': None, + 'toAccount': None, + 'status': None, + } + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://api-docs.poloniex.com/spot/api/private/wallet#withdraw-currency + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.check_address(address) + request, extraParams, currency, networkEntry = self.prepare_request_for_deposit_address(code, params) + params = extraParams + request['amount'] = self.currency_to_precision(code, amount) + request['address'] = address + if tag is not None: + request['paymentId'] = tag + response = await self.privatePostWalletsWithdraw(self.extend(request, params)) + # + # { + # "response": "Withdrew 1.00000000 USDT.", + # "email2FA": False, + # "withdrawalNumber": 13449869 + # } + # + withdrawResponse = { + 'response': response, + 'withdrawNetworkEntry': networkEntry, + } + return self.parse_transaction(withdrawResponse, currency) + + async def fetch_transactions_helper(self, code: Str = None, since: Int = None, limit: Int = None, params={}): + await self.load_markets() + year = 31104000 # 60 * 60 * 24 * 30 * 12 = one year of history, why not + now = self.seconds() + start = self.parse_to_int(since / 1000) if (since is not None) else now - 10 * year + request: dict = { + 'start': start, # UNIX timestamp, required + 'end': now, # UNIX timestamp, required + } + response = await self.privateGetWalletsActivity(self.extend(request, params)) + # + # { + # "adjustments":[], + # "deposits":[ + # { + # "currency": "BTC", + # "address": "1MEtiqJWru53FhhHrfJPPvd2tC3TPDVcmW", + # "amount": "0.01063000", + # "confirmations": 1, + # "txid": "952b0e1888d6d491591facc0d37b5ebec540ac1efb241fdbc22bcc20d1822fb6", + # "timestamp": 1507916888, + # "status": "COMPLETE" + # }, + # { + # "currency": "ETH", + # "address": "0x20108ba20b65c04d82909e91df06618107460197", + # "amount": "4.00000000", + # "confirmations": 38, + # "txid": "0x4be260073491fe63935e9e0da42bd71138fdeb803732f41501015a2d46eb479d", + # "timestamp": 1525060430, + # "status": "COMPLETE" + # } + # ], + # "withdrawals":[ + # { + # "withdrawalNumber":13449869, + # "currency":"USDTTRON", # not documented in API docs, see commonCurrencies in describe() + # "address":"TXGaqPW23JdRWhsVwS2mRsGsegbdnAd3Rw", + # "amount":"1.00000000", + # "fee":"0.00000000", + # "timestamp":1591573420, + # "status":"COMPLETE: dadf427224b3d44b38a2c13caa4395e4666152556ca0b2f67dbd86a95655150f", + # "ipAddress":"x.x.x.x", + # "canCancel":0, + # "canResendEmail":0, + # "paymentID":null, + # "scope":"crypto" + # }, + # { + # "withdrawalNumber": 8224394, + # "currency": "EMC2", + # "address": "EYEKyCrqTNmVCpdDV8w49XvSKRP9N3EUyF", + # "amount": "63.10796020", + # "fee": "0.01000000", + # "timestamp": 1510819838, + # "status": "COMPLETE: d37354f9d02cb24d98c8c4fc17aa42f475530b5727effdf668ee5a43ce667fd6", + # "ipAddress": "x.x.x.x" + # }, + # { + # "withdrawalNumber": 9290444, + # "currency": "ETH", + # "address": "0x191015ff2e75261d50433fbd05bd57e942336149", + # "amount": "0.15500000", + # "fee": "0.00500000", + # "timestamp": 1514099289, + # "status": "COMPLETE: 0x12d444493b4bca668992021fd9e54b5292b8e71d9927af1f076f554e4bea5b2d", + # "ipAddress": "x.x.x.x" + # }, + # { + # "withdrawalNumber": 11518260, + # "currency": "BTC", + # "address": "8JoDXAmE1GY2LRK8jD1gmAmgRPq54kXJ4t", + # "amount": "0.20000000", + # "fee": "0.00050000", + # "timestamp": 1527918155, + # "status": "COMPLETE: 1864f4ebb277d90b0b1ff53259b36b97fa1990edc7ad2be47c5e0ab41916b5ff", + # "ipAddress": "x.x.x.x" + # } + # ] + # } + # + return response + + async def fetch_deposits_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch history of deposits and withdrawals + + https://api-docs.poloniex.com/spot/api/private/wallet#wallets-activity-records + + :param str [code]: unified currency code for the currency of the deposit/withdrawals, default is None + :param int [since]: timestamp in ms of the earliest deposit/withdrawal, default is None + :param int [limit]: max number of deposit/withdrawals to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `transaction structure ` + """ + await self.load_markets() + response = await self.fetch_transactions_helper(code, since, limit, params) + currency: Currency = None + if code is not None: + currency = self.currency(code) + withdrawals = self.safe_value(response, 'withdrawals', []) + deposits = self.safe_value(response, 'deposits', []) + withdrawalTransactions = self.parse_transactions(withdrawals, currency, since, limit) + depositTransactions = self.parse_transactions(deposits, currency, since, limit) + transactions = self.array_concat(depositTransactions, withdrawalTransactions) + return self.filter_by_currency_since_limit(self.sort_by(transactions, 'timestamp'), code, since, limit) + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://api-docs.poloniex.com/spot/api/private/wallet#wallets-activity-records + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + response = await self.fetch_transactions_helper(code, since, limit, params) + currency: Currency = None + if code is not None: + currency = self.currency(code) + withdrawals = self.safe_value(response, 'withdrawals', []) + transactions = self.parse_transactions(withdrawals, currency, since, limit) + return self.filter_by_currency_since_limit(transactions, code, since, limit) + + async def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}): + """ + fetch deposit and withdraw fees + + https://api-docs.poloniex.com/spot/api/public/reference-data#currency-information + + :param str[]|None codes: list of unified currency codes + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `fees structures ` + """ + await self.load_markets() + response = await self.publicGetCurrencies(self.extend(params, {'includeMultiChainCurrencies': True})) + # + # [ + # { + # "1CR": { + # "id": 1, + # "name": "1CRedit", + # "description": "BTC Clone", + # "type": "address", + # "withdrawalFee": "0.01000000", + # "minConf": 10000, + # "depositAddress": null, + # "blockchain": "1CR", + # "delisted": False, + # "tradingState": "NORMAL", + # "walletState": "DISABLED", + # "parentChain": null, + # "isMultiChain": False, + # "isChildChain": False, + # "childChains": [] + # } + # } + # ] + # + data: dict = {} + for i in range(0, len(response)): + entry = response[i] + currencies = list(entry.keys()) + currencyId = self.safe_string(currencies, 0) + data[currencyId] = entry[currencyId] + return self.parse_deposit_withdraw_fees(data, codes) + + def parse_deposit_withdraw_fees(self, response, codes=None, currencyIdKey=None): + # + # { + # "1CR": { + # "id": 1, + # "name": "1CRedit", + # "description": "BTC Clone", + # "type": "address", + # "withdrawalFee": "0.01000000", + # "minConf": 10000, + # "depositAddress": null, + # "blockchain": "1CR", + # "delisted": False, + # "tradingState": "NORMAL", + # "walletState": "DISABLED", + # "parentChain": null, + # "isMultiChain": False, + # "isChildChain": False, + # "childChains": [] + # }, + # } + # + depositWithdrawFees: dict = {} + codes = self.market_codes(codes) + responseKeys = list(response.keys()) + for i in range(0, len(responseKeys)): + currencyId = responseKeys[i] + code = self.safe_currency_code(currencyId) + feeInfo = response[currencyId] + if (codes is None) or (self.in_array(code, codes)): + currency = self.currency(code) + depositWithdrawFees[code] = self.parse_deposit_withdraw_fee(feeInfo, currency) + childChains = self.safe_value(feeInfo, 'childChains') + chainsLength = len(childChains) + if chainsLength > 0: + for j in range(0, len(childChains)): + networkId = childChains[j] + networkId = networkId.replace(code, '') + networkCode = self.network_id_to_code(networkId) + networkInfo = self.safe_value(response, networkId) + networkObject: dict = {} + withdrawFee = self.safe_number(networkInfo, 'withdrawalFee') + networkObject[networkCode] = { + 'withdraw': { + 'fee': withdrawFee, + 'percentage': False if (withdrawFee is not None) else None, + }, + 'deposit': { + 'fee': None, + 'percentage': None, + }, + } + depositWithdrawFees[code]['networks'] = self.extend(depositWithdrawFees[code]['networks'], networkObject) + return depositWithdrawFees + + def parse_deposit_withdraw_fee(self, fee, currency: Currency = None): + depositWithdrawFee = self.deposit_withdraw_fee({}) + depositWithdrawFee['info'][currency['code']] = fee + networkId = self.safe_string(fee, 'blockchain') + withdrawFee = self.safe_number(fee, 'withdrawalFee') + withdrawResult: dict = { + 'fee': withdrawFee, + 'percentage': False if (withdrawFee is not None) else None, + } + depositResult: dict = { + 'fee': None, + 'percentage': None, + } + depositWithdrawFee['withdraw'] = withdrawResult + depositWithdrawFee['deposit'] = depositResult + networkCode = self.network_id_to_code(networkId) + depositWithdrawFee['networks'][networkCode] = { + 'withdraw': withdrawResult, + 'deposit': depositResult, + } + return depositWithdrawFee + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://api-docs.poloniex.com/spot/api/private/wallet#wallets-activity-records + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + response = await self.fetch_transactions_helper(code, since, limit, params) + currency = None + if code is not None: + currency = self.currency(code) + deposits = self.safe_value(response, 'deposits', []) + transactions = self.parse_transactions(deposits, currency, since, limit) + return self.filter_by_currency_since_limit(transactions, code, since, limit) + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'COMPLETE': 'ok', + 'COMPLETED': 'ok', + 'AWAITING APPROVAL': 'pending', + 'AWAITING_APPROVAL': 'pending', + 'PENDING': 'pending', + 'PROCESSING': 'pending', + 'COMPLETE ERROR': 'failed', + 'COMPLETE_ERROR': 'failed', + } + return self.safe_string(statuses, status, status) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # deposits + # + # { + # "txid": "f49d489616911db44b740612d19464521179c76ebe9021af85b6de1e2f8d68cd", + # "amount": "49798.01987021", + # "status": "COMPLETE", + # "address": "DJVJZ58tJC8UeUv9Tqcdtn6uhWobouxFLT", + # "currency": "DOGE", + # "timestamp": 1524321838, + # "confirmations": 3371, + # "depositNumber": 134587098 + # } + # + # withdrawals + # + # { + # "withdrawalRequestsId": 7397527, + # "currency": "ETC", + # "address": "0x26419a62055af459d2cd69bb7392f5100b75e304", + # "amount": "13.19951600", + # "fee": "0.01000000", + # "timestamp": 1506010932, + # "status": "COMPLETED", + # "txid": "343346392f82ac16e8c2604f2a604b7b2382d0e9d8030f673821f8de4b5f5bk", + # "ipAddress": "1.2.3.4", + # "paymentID": null + # } + # + # withdraw + # + # { + # "withdrawalRequestsId": 33485231 + # } + # + # if it's being parsed from "withdraw()" method, get the original response + if 'withdrawNetworkEntry' in transaction: + transaction = transaction['response'] + timestamp = self.safe_timestamp(transaction, 'timestamp') + currencyId = self.safe_string(transaction, 'currency') + code = self.safe_currency_code(currencyId) + status = self.safe_string(transaction, 'status', 'pending') + status = self.parse_transaction_status(status) + txid = self.safe_string(transaction, 'txid') + type = 'withdrawal' if ('withdrawalRequestsId' in transaction) else 'deposit' + id = self.safe_string_2(transaction, 'withdrawalRequestsId', 'depositNumber') + address = self.safe_string(transaction, 'address') + tag = self.safe_string(transaction, 'paymentID') + amountString = self.safe_string(transaction, 'amount') + feeCostString = self.safe_string(transaction, 'fee') + if type == 'withdrawal': + amountString = Precise.string_sub(amountString, feeCostString) + return { + 'info': transaction, + 'id': id, + 'currency': code, + 'amount': self.parse_number(amountString), + 'network': None, + 'address': address, + 'addressTo': None, + 'addressFrom': None, + 'tag': tag, + 'tagTo': None, + 'tagFrom': None, + 'status': status, + 'type': type, + 'updated': None, + 'txid': txid, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'comment': None, + 'internal': None, + 'fee': { + 'currency': code, + 'cost': self.parse_number(feeCostString), + 'rate': None, + }, + } + + async def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://api-docs.poloniex.com/v3/futures/api/positions/set-leverage + + :param int leverage: the rate of leverage + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setLeverage() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('setLeverage', params) + if marginMode is None: + raise ArgumentsRequired(self.id + ' setLeverage() requires a marginMode parameter "cross" or "isolated"') + hedged: Bool = None + hedged, params = self.handle_param_bool(params, 'hedged', False) + if hedged: + if not ('posSide' in params): + raise ArgumentsRequired(self.id + ' setLeverage() requires a posSide parameter for hedged mode: "LONG" or "SHORT"') + request: dict = { + 'lever': leverage, + 'mgnMode': marginMode.upper(), + 'symbol': market['id'], + } + response = await self.swapPrivatePostV3PositionLeverage(self.extend(request, params)) + return response + + async def fetch_leverage(self, symbol: str, params={}) -> Leverage: + """ + fetch the set leverage for a market + + https://api-docs.poloniex.com/v3/futures/api/positions/get-leverages + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `leverage structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchLeverage', params) + if marginMode is None: + raise ArgumentsRequired(self.id + ' fetchLeverage() requires a marginMode parameter "cross" or "isolated"') + request['mgnMode'] = marginMode.upper() + response = await self.swapPrivateGetV3PositionLeverages(self.extend(request, params)) + # + # for one-way mode: + # + # { + # "code": "200", + # "msg": "", + # "data": [ + # { + # "symbol": "BTC_USDT_PERP", + # "lever": "10", + # "mgnMode": "CROSS", + # "posSide": "BOTH" + # } + # ] + # } + # + # for hedge: + # + # { + # "code": "200", + # "msg": "", + # "data": [ + # { + # "symbol": "BTC_USDT_PERP", + # "lever": "20", + # "mgnMode": "CROSS", + # "posSide": "SHORT" + # }, + # { + # "symbol": "BTC_USDT_PERP", + # "lever": "20", + # "mgnMode": "CROSS", + # "posSide": "LONG" + # } + # ] + # } + # + return self.parse_leverage(response, market) + + def parse_leverage(self, leverage: dict, market: Market = None) -> Leverage: + shortLeverage: Int = None + longLeverage: Int = None + marketId: Str = None + marginMode: Str = None + data = self.safe_list(leverage, 'data') + for i in range(0, len(data)): + entry = data[i] + marketId = self.safe_string(entry, 'symbol') + marginMode = self.safe_string(entry, 'mgnMode') + lever = self.safe_integer(entry, 'lever') + posSide = self.safe_string(entry, 'posSide') + if posSide == 'LONG': + longLeverage = lever + elif posSide == 'SHORT': + shortLeverage = lever + else: + longLeverage = lever + shortLeverage = lever + return { + 'info': leverage, + 'symbol': self.safe_symbol(marketId, market), + 'marginMode': marginMode, + 'longLeverage': longLeverage, + 'shortLeverage': shortLeverage, + } + + async def fetch_position_mode(self, symbol: Str = None, params={}): + """ + fetchs the position mode, hedged or one way, hedged for binance is set identically for all linear markets or all inverse markets + + https://api-docs.poloniex.com/v3/futures/api/positions/position-mode-switch + + :param str symbol: unified symbol of the market to fetch the order book for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an object detailing whether the market is in hedged or one-way mode + """ + response = await self.swapPrivateGetV3PositionMode(params) + # + # { + # "code": "200", + # "msg": "Success", + # "data": { + # "posMode": "ONE_WAY" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + posMode = self.safe_string(data, 'posMode') + hedged = posMode == 'HEDGE' + return { + 'info': response, + 'hedged': hedged, + } + + async def set_position_mode(self, hedged: bool, symbol: Str = None, params={}): + """ + set hedged to True or False for a market + + https://api-docs.poloniex.com/v3/futures/api/positions/position-mode-switch + + :param bool hedged: set to True to use dualSidePosition + :param str symbol: not used by binance setPositionMode() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + mode = 'HEDGE' if hedged else 'ONE_WAY' + request: dict = { + 'posMode': mode, + } + response = await self.swapPrivatePostV3PositionMode(self.extend(request, params)) + # + # { + # "code": "200", + # "msg": "Success", + # "data": {} + # } + # + return response + + async def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://api-docs.poloniex.com/v3/futures/api/positions/get-current-position + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.standard]: whether to fetch standard contract positions + :returns dict[]: a list of `position structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + response = await self.swapPrivateGetV3TradePositionOpens(params) + # + # { + # "code": "200", + # "msg": "", + # "data": [ + # { + # "symbol": "BTC_USDT_PERP", + # "posSide": "LONG", + # "side": "BUY", + # "mgnMode": "CROSS", + # "openAvgPx": "94193.42", + # "qty": "1", + # "availQty": "1", + # "lever": "20", + # "adl": "0.3007", + # "liqPx": "84918.201844064386317906", + # "im": "4.7047795", + # "mm": "0.56457354", + # "upl": "-0.09783", + # "uplRatio": "-0.0207", + # "pnl": "0", + # "markPx": "94095.59", + # "mgnRatio": "0.0582", + # "state": "NORMAL", + # "cTime": "1740950344401", + # "uTime": "1740950344401", + # "mgn": "4.7047795", + # "actType": "TRADING", + # "maxWAmt": "0", + # "tpTrgPx": "", + # "slTrgPx": "" + # } + # ] + # } + # + positions = self.safe_list(response, 'data', []) + return self.parse_positions(positions, symbols) + + def parse_position(self, position: dict, market: Market = None): + # + # { + # "symbol": "BTC_USDT_PERP", + # "posSide": "LONG", + # "side": "BUY", + # "mgnMode": "CROSS", + # "openAvgPx": "94193.42", + # "qty": "1", + # "availQty": "1", + # "lever": "20", + # "adl": "0.3007", + # "liqPx": "84918.201844064386317906", + # "im": "4.7047795", + # "mm": "0.56457354", + # "upl": "-0.09783", + # "uplRatio": "-0.0207", + # "pnl": "0", + # "markPx": "94095.59", + # "mgnRatio": "0.0582", + # "state": "NORMAL", + # "cTime": "1740950344401", + # "uTime": "1740950344401", + # "mgn": "4.7047795", + # "actType": "TRADING", + # "maxWAmt": "0", + # "tpTrgPx": "", + # "slTrgPx": "" + # } + # + marketId = self.safe_string(position, 'symbol') + market = self.safe_market(marketId, market) + timestamp = self.safe_integer(position, 'cTime') + marginMode = self.safe_string_lower(position, 'mgnMode') + leverage = self.safe_string(position, 'lever') + initialMargin = self.safe_string(position, 'im') + notional = Precise.string_mul(leverage, initialMargin) + qty = self.safe_string(position, 'qty') + avgPrice = self.safe_string(position, 'openAvgPx') + collateral = Precise.string_mul(qty, avgPrice) + # todo: some more fields + return self.safe_position({ + 'info': position, + 'id': None, + 'symbol': market['symbol'], + 'notional': notional, + 'marginMode': marginMode, + 'liquidationPrice': self.safe_number(position, 'liqPx'), + 'entryPrice': self.safe_number(position, 'openAvgPx'), + 'unrealizedPnl': self.safe_number(position, 'upl'), + 'percentage': None, + 'contracts': self.safe_number(position, 'qty'), + 'contractSize': None, + 'markPrice': self.safe_number(position, 'markPx'), + 'lastPrice': None, + 'side': self.safe_string_lower(position, 'posSide'), + 'hedged': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastUpdateTimestamp': None, + 'maintenanceMargin': self.safe_number(position, 'mm'), + 'maintenanceMarginPercentage': None, + 'collateral': collateral, + 'initialMargin': initialMargin, + 'initialMarginPercentage': None, + 'leverage': int(leverage), + 'marginRatio': self.safe_number(position, 'mgnRatio'), + 'stopLossPrice': self.safe_number(position, 'slTrgPx'), + 'takeProfitPrice': self.safe_number(position, 'tpTrgPx'), + }) + + async def modify_margin_helper(self, symbol: str, amount, type, params={}) -> MarginModification: + await self.load_markets() + market = self.market(symbol) + amount = self.amount_to_precision(symbol, amount) + request: dict = { + 'symbol': market['id'], + 'amt': Precise.string_abs(amount), + 'type': type.upper(), # 'ADD' or 'REDUCE' + } + # todo: hedged handling, tricky + if not ('posMode' in params): + request['posMode'] = 'BOTH' + response = await self.swapPrivatePostV3TradePositionMargin(self.extend(request, params)) + # + # { + # "code": 200, + # "data": { + # "amt": "50", + # "lever": "20", + # "symbol": "DOT_USDT_PERP", + # "posSide": "BOTH", + # "type": "ADD" + # }, + # "msg": "Success" + # } + # + if type == 'reduce': + amount = Precise.string_abs(amount) + data = self.safe_dict(response, 'data') + return self.parse_margin_modification(data, market) + + def parse_margin_modification(self, data: dict, market: Market = None) -> MarginModification: + marketId = self.safe_string(data, 'symbol') + market = self.safe_market(marketId, market) + rawType = self.safe_string(data, 'type') + type = 'add' if (rawType == 'ADD') else 'reduce' + return { + 'info': data, + 'symbol': market['symbol'], + 'type': type, + 'marginMode': None, + 'amount': self.safe_number(data, 'amt'), + 'total': None, + 'code': None, + 'status': 'ok', + 'timestamp': None, + 'datetime': None, + } + + async def reduce_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + remove margin from a position + :param str symbol: unified market symbol + :param float amount: the amount of margin to remove + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin structure ` + """ + return await self.modify_margin_helper(symbol, -amount, 'reduce', params) + + async def add_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + add margin + :param str symbol: unified market symbol + :param float amount: amount of margin to add + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin structure ` + """ + return await self.modify_margin_helper(symbol, amount, 'add', params) + + def nonce(self): + return self.milliseconds() + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = self.urls['api']['spot'] + if self.in_array(api, ['swapPublic', 'swapPrivate']): + url = self.urls['api']['swap'] + if 'symbol' in params: + params['symbol'] = self.encode_uri_component(params['symbol']) # handle symbols like 索拉拉/USDT' + query = self.omit(params, self.extract_params(path)) + implodedPath = self.implode_params(path, params) + if api == 'public' or api == 'swapPublic': + url += '/' + implodedPath + if query: + url += '?' + self.urlencode(query) + else: + self.check_required_credentials() + timestamp = str(self.nonce()) + auth = method + "\n" # eslint-disable-line quotes + url += '/' + implodedPath + auth += '/' + implodedPath + if (method == 'POST') or (method == 'PUT') or (method == 'DELETE'): + auth += "\n" # eslint-disable-line quotes + if query: + body = self.json(query) + auth += 'requestBody=' + body + '&' + auth += 'signTimestamp=' + timestamp + else: + sortedQuery = self.extend({'signTimestamp': timestamp}, query) + sortedQuery = self.keysort(sortedQuery) + auth += "\n" + self.urlencode(sortedQuery) # eslint-disable-line quotes + if query: + url += '?' + self.urlencode(query) + signature = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256, 'base64') + headers = { + 'Content-Type': 'application/json', + 'key': self.apiKey, + 'signTimestamp': timestamp, + 'signature': signature, + } + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None + # + # { + # "code" : 21709, + # "message" : "Low available balance" + # } + # + responseCode = self.safe_string(response, 'code') + if (responseCode is not None) and (responseCode != '200'): + codeInner = response['code'] + message = self.safe_string(response, 'message') + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], codeInner, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + raise ExchangeError(feedback) # unknown message + return None diff --git a/ccxt/async_support/probit.py b/ccxt/async_support/probit.py new file mode 100644 index 0000000..42d7ee1 --- /dev/null +++ b/ccxt/async_support/probit.py @@ -0,0 +1,1863 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.probit import ImplicitAPI +import math +from ccxt.base.types import Any, Balances, Currencies, Currency, DepositAddress, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import MarketClosed +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidAddress +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import DDoSProtection +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.errors import BadResponse +from ccxt.base.decimal_to_precision import TRUNCATE +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class probit(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(probit, self).describe(), { + 'id': 'probit', + 'name': 'ProBit', + 'countries': ['SC', 'KR'], # Seychelles, South Korea + 'rateLimit': 50, # ms + 'pro': True, + 'has': { + 'CORS': True, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelOrder': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createMarketBuyOrderWithCost': True, + 'createMarketOrder': True, + 'createMarketOrderWithCost': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createReduceOnlyOrder': False, + 'createStopLimitOrder': False, + 'createStopMarketOrder': False, + 'createStopOrder': False, + 'fetchAllGreeks': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': True, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchDepositsWithdrawals': True, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrice': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': False, + 'fetchTransactions': 'emulated', + 'fetchTransfer': False, + 'fetchTransfers': False, + 'fetchUnderlyingAssets': False, + 'fetchVolatilityHistory': False, + 'fetchWithdrawal': False, + 'fetchWithdrawals': True, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'sandbox': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'signIn': True, + 'transfer': False, + 'withdraw': True, + }, + 'timeframes': { + '1m': '1m', + '3m': '3m', + '5m': '5m', + '10m': '10m', + '15m': '15m', + '30m': '30m', + '1h': '1h', + '4h': '4h', + '6h': '6h', + '12h': '12h', + '1d': '1D', + '1w': '1W', + '1M': '1M', + }, + 'version': 'v1', + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/51840849/79268032-c4379480-7ea2-11ea-80b3-dd96bb29fd0d.jpg', + 'api': { + 'accounts': 'https://accounts.probit.com', + 'public': 'https://api.probit.com/api/exchange', + 'private': 'https://api.probit.com/api/exchange', + }, + 'www': 'https://www.probit.com', + 'doc': [ + 'https://docs-en.probit.com', + 'https://docs-ko.probit.com', + ], + 'fees': 'https://support.probit.com/hc/en-us/articles/360020968611-Trading-Fees', + 'referral': 'https://www.probit.com/r/34608773', + }, + 'api': { + 'public': { + 'get': { + 'market': 1, + 'currency': 1, + 'currency_with_platform': 1, + 'time': 1, + 'ticker': 1, + 'order_book': 1, + 'trade': 1, + 'candle': 1, + }, + }, + 'private': { + 'post': { + 'new_order': 2, + 'cancel_order': 1, + 'withdrawal': 2, + }, + 'get': { + 'balance': 1, + 'order': 1, + 'open_order': 1, + 'order_history': 1, + 'trade_history': 1, + 'deposit_address': 1, + 'transfer/payment': 1, + }, + }, + 'accounts': { + 'post': { + 'token': 1, + }, + }, + }, + 'fees': { + 'trading': { + 'tierBased': False, + 'percentage': True, + 'maker': self.parse_number('0.002'), + 'taker': self.parse_number('0.002'), + }, + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': False, + 'triggerDirection': False, + 'triggerPriceType': None, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + # todo + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': False, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 100000, # todo + 'untilDays': 100000, # todo + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 100000, # todo + 'daysBackCanceled': 1, # todo + 'untilDays': 90, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 4000, + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'exceptions': { + 'exact': { + 'UNAUTHORIZED': AuthenticationError, + 'INVALID_ARGUMENT': BadRequest, # Parameters are not a valid format, parameters are empty, or out of range, or a parameter was sent when not required. + 'TRADING_UNAVAILABLE': ExchangeNotAvailable, + 'NOT_ENOUGH_BALANCE': InsufficientFunds, + 'NOT_ALLOWED_COMBINATION': BadRequest, + 'INVALID_ORDER': InvalidOrder, # Requested order does not exist, or it is not your order + 'RATE_LIMIT_EXCEEDED': RateLimitExceeded, # You are sending requests too frequently. Please try it later. + 'MARKET_UNAVAILABLE': ExchangeNotAvailable, # Market is closed today + 'INVALID_MARKET': BadSymbol, # Requested market is not exist + 'MARKET_CLOSED': MarketClosed, # {"errorCode":"MARKET_CLOSED"} + 'MARKET_NOT_FOUND': BadSymbol, # {"errorCode":"MARKET_NOT_FOUND","message":"8e2b8496-0a1e-5beb-b990-a205b902eabe","details":{}} + 'INVALID_CURRENCY': BadRequest, # Requested currency is not exist on ProBit system + 'TOO_MANY_OPEN_ORDERS': DDoSProtection, # Too many open orders + 'DUPLICATE_ADDRESS': InvalidAddress, # Address already exists in withdrawal address list + 'invalid_grant': AuthenticationError, # {"error":"invalid_grant"} + }, + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + }, + 'precisionMode': TICK_SIZE, + 'options': { + 'createMarketBuyOrderRequiresPrice': True, + 'timeInForce': { + 'limit': 'gtc', + 'market': 'ioc', + }, + 'networks': { + 'BEP20': 'BSC', + 'ERC20': 'ETH', + 'TRC20': 'TRON', + }, + }, + 'commonCurrencies': { + 'BB': 'Baby Bali', + 'CBC': 'CryptoBharatCoin', + 'CTK': 'Cryptyk', + 'CTT': 'Castweet', + 'DKT': 'DAKOTA', + 'EGC': 'EcoG9coin', + 'EPS': 'Epanus', # conflict with EPS Ellipsis https://github.com/ccxt/ccxt/issues/8909 + 'FX': 'Fanzy', + 'GM': 'GM Holding', + 'GOGOL': 'GOL', + 'GOL': 'Goldofir', + 'HUSL': 'The Hustle App', + 'LAND': 'Landbox', + 'SST': 'SocialSwap', + 'TCT': 'Top Coin Token', + 'TOR': 'Torex', + 'UNI': 'UNICORN Token', + 'UNISWAP': 'UNI', + }, + }) + + async def fetch_markets(self, params={}) -> List[Market]: + """ + + https://docs-en.probit.com/reference/market + + retrieves data on all markets for probit + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = await self.publicGetMarket(params) + # + # { + # "data":[ + # { + # "id":"MONA-USDT", + # "base_currency_id":"MONA", + # "quote_currency_id":"USDT", + # "min_price":"0.001", + # "max_price":"9999999999999999", + # "price_increment":"0.001", + # "min_quantity":"0.0001", + # "max_quantity":"9999999999999999", + # "quantity_precision":4, + # "min_cost":"1", + # "max_cost":"9999999999999999", + # "cost_precision":8, + # "taker_fee_rate":"0.2", + # "maker_fee_rate":"0.2", + # "show_in_ui":true, + # "closed":false + # }, + # ] + # } + # + markets = self.safe_value(response, 'data', []) + return self.parse_markets(markets) + + def parse_market(self, market: dict) -> Market: + id = self.safe_string(market, 'id') + baseId = self.safe_string(market, 'base_currency_id') + quoteId = self.safe_string(market, 'quote_currency_id') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + closed = self.safe_bool(market, 'closed', False) + showInUI = self.safe_bool(market, 'show_in_ui', True) + active = not closed and showInUI + takerFeeRate = self.safe_string(market, 'taker_fee_rate') + taker = Precise.string_div(takerFeeRate, '100') + makerFeeRate = self.safe_string(market, 'maker_fee_rate') + maker = Precise.string_div(makerFeeRate, '100') + return { + 'id': id, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': active, + 'contract': False, + 'linear': None, + 'inverse': None, + 'taker': self.parse_number(taker), + 'maker': self.parse_number(maker), + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(self.parse_precision(self.safe_string(market, 'quantity_precision'))), + 'price': self.safe_number(market, 'price_increment'), + 'cost': self.parse_number(self.parse_precision(self.safe_string(market, 'cost_precision'))), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(market, 'min_quantity'), + 'max': self.safe_number(market, 'max_quantity'), + }, + 'price': { + 'min': self.safe_number(market, 'min_price'), + 'max': self.safe_number(market, 'max_price'), + }, + 'cost': { + 'min': self.safe_number(market, 'min_cost'), + 'max': self.safe_number(market, 'max_cost'), + }, + }, + 'created': None, + 'info': market, + } + + async def fetch_currencies(self, params={}) -> Currencies: + """ + + https://docs-en.probit.com/reference/currency + + fetches all available currencies on an exchange + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = await self.publicGetCurrencyWithPlatform(params) + # + # { + # "data":[ + # { + # "id":"USDT", + # "display_name":{"ko-kr":"테더","en-us":"Tether"}, + # "show_in_ui":true, + # "platform":[ + # { + # "id":"ETH", + # "priority":1, + # "deposit":true, + # "withdrawal":true, + # "currency_id":"USDT", + # "precision":6, + # "min_confirmation_count":15, + # "require_destination_tag":false, + # "display_name":{"name":{"ko-kr":"ERC-20","en-us":"ERC-20"}}, + # "min_deposit_amount":"0", + # "min_withdrawal_amount":"1", + # "withdrawal_fee":[ + # {"amount":"0.01","priority":2,"currency_id":"ETH"}, + # {"amount":"1.5","priority":1,"currency_id":"USDT"}, + # ], + # "deposit_fee":{}, + # "suspended_reason":"", + # "deposit_suspended":false, + # "withdrawal_suspended":false + # }, + # { + # "id":"OMNI", + # "priority":2, + # "deposit":true, + # "withdrawal":true, + # "currency_id":"USDT", + # "precision":6, + # "min_confirmation_count":3, + # "require_destination_tag":false, + # "display_name":{"name":{"ko-kr":"OMNI","en-us":"OMNI"}}, + # "min_deposit_amount":"0", + # "min_withdrawal_amount":"5", + # "withdrawal_fee":[{"amount":"5","priority":1,"currency_id":"USDT"}], + # "deposit_fee":{}, + # "suspended_reason":"wallet_maintenance", + # "deposit_suspended":false, + # "withdrawal_suspended":false + # } + # ], + # "stakeable":false, + # "unstakeable":false, + # "auto_stake":false, + # "auto_stake_amount":"0" + # } + # ] + # } + # + currencies = self.safe_list(response, 'data', []) + result: dict = {} + for i in range(0, len(currencies)): + currency = currencies[i] + id = self.safe_string(currency, 'id') + code = self.safe_currency_code(id) + displayName = self.safe_dict(currency, 'display_name') + name = self.safe_string(displayName, 'en-us') + platforms = self.safe_list(currency, 'platform', []) + platformsByPriority = self.sort_by(platforms, 'priority') + networkList: dict = {} + for j in range(0, len(platformsByPriority)): + network = platformsByPriority[j] + idInner = self.safe_string(network, 'id') + networkCode = self.network_id_to_code(idInner) + withdrawFee = self.safe_list(network, 'withdrawal_fee', []) + networkFee = self.safe_dict(withdrawFee, 0, {}) + for k in range(0, len(withdrawFee)): + withdrawPlatform = withdrawFee[k] + feeCurrencyId = self.safe_string(withdrawPlatform, 'currency_id') + if feeCurrencyId == id: + networkFee = withdrawPlatform + break + networkList[networkCode] = { + 'id': idInner, + 'network': networkCode, + 'active': None, + 'deposit': not self.safe_bool(network, 'deposit_suspended'), + 'withdraw': not self.safe_bool(network, 'withdrawal_suspended'), + 'fee': self.safe_number(networkFee, 'amount'), + 'precision': self.parse_number(self.parse_precision(self.safe_string(network, 'precision'))), + 'limits': { + 'withdraw': { + 'min': self.safe_number(network, 'min_withdrawal_amount'), + 'max': None, + }, + 'deposit': { + 'min': self.safe_number(network, 'min_deposit_amount'), + 'max': None, + }, + }, + 'info': network, + } + result[code] = self.safe_currency_structure({ + 'id': id, + 'code': code, + 'info': currency, + 'name': name, + 'active': None, + 'deposit': None, + 'withdraw': None, + 'type': 'crypto', + 'fee': None, + 'precision': None, + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'deposit': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + }, + 'networks': networkList, + }) + return result + + def parse_balance(self, response) -> Balances: + result: dict = { + 'info': response, + 'timestamp': None, + 'datetime': None, + } + data = self.safe_value(response, 'data', []) + for i in range(0, len(data)): + balance = data[i] + currencyId = self.safe_string(balance, 'currency_id') + code = self.safe_currency_code(currencyId) + account = self.account() + account['total'] = self.safe_string(balance, 'total') + account['free'] = self.safe_string(balance, 'available') + result[code] = account + return self.safe_balance(result) + + async def fetch_balance(self, params={}) -> Balances: + """ + + https://docs-en.probit.com/reference/balance + + query for balance and get the amount of funds available for trading or funds locked in orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + response = await self.privateGetBalance(params) + # + # { + # "data": [ + # { + # "currency_id":"XRP", + # "total":"100", + # "available":"0", + # } + # ] + # } + # + return self.parse_balance(response) + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + + https://docs-en.probit.com/reference/order_book + + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'market_id': market['id'], + } + response = await self.publicGetOrderBook(self.extend(request, params)) + # + # { + # data: [ + # {side: 'buy', price: '0.000031', quantity: '10'}, + # {side: 'buy', price: '0.00356007', quantity: '4.92156877'}, + # {side: 'sell', price: '0.1857', quantity: '0.17'}, + # ] + # } + # + data = self.safe_value(response, 'data', []) + dataBySide = self.group_by(data, 'side') + return self.parse_order_book(dataBySide, market['symbol'], None, 'buy', 'sell', 'price', 'quantity') + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + + https://docs-en.probit.com/reference/ticker + + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + request: dict = {} + if symbols is not None: + marketIds = self.market_ids(symbols) + request['market_ids'] = ','.join(marketIds) + response = await self.publicGetTicker(self.extend(request, params)) + # + # { + # "data":[ + # { + # "last":"0.022902", + # "low":"0.021693", + # "high":"0.024093", + # "change":"-0.000047", + # "base_volume":"15681.986", + # "quote_volume":"360.514403624", + # "market_id":"ETH-BTC", + # "time":"2020-04-12T18:43:38.000Z" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_tickers(data, symbols) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + + https://docs-en.probit.com/reference/ticker + + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'market_ids': market['id'], + } + response = await self.publicGetTicker(self.extend(request, params)) + # + # { + # "data":[ + # { + # "last":"0.022902", + # "low":"0.021693", + # "high":"0.024093", + # "change":"-0.000047", + # "base_volume":"15681.986", + # "quote_volume":"360.514403624", + # "market_id":"ETH-BTC", + # "time":"2020-04-12T18:43:38.000Z" + # } + # ] + # } + # + data = self.safe_value(response, 'data', []) + ticker = self.safe_value(data, 0) + if ticker is None: + raise BadResponse(self.id + ' fetchTicker() returned an empty response') + return self.parse_ticker(ticker, market) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "last":"0.022902", + # "low":"0.021693", + # "high":"0.024093", + # "change":"-0.000047", + # "base_volume":"15681.986", + # "quote_volume":"360.514403624", + # "market_id":"ETH-BTC", + # "time":"2020-04-12T18:43:38.000Z" + # } + # + timestamp = self.parse8601(self.safe_string(ticker, 'time')) + marketId = self.safe_string(ticker, 'market_id') + symbol = self.safe_symbol(marketId, market, '-') + close = self.safe_string(ticker, 'last') + change = self.safe_string(ticker, 'change') + baseVolume = self.safe_string(ticker, 'base_volume') + quoteVolume = self.safe_string(ticker, 'quote_volume') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': None, + 'bidVolume': None, + 'ask': None, + 'askVolume': None, + 'vwap': None, + 'open': None, + 'close': close, + 'last': close, + 'previousClose': None, # previous day close + 'change': change, + 'percentage': None, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'info': ticker, + }, market) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + + https://docs-en.probit.com/reference/trade + + fetch all trades made by the user + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market: Market = None + now = self.milliseconds() + request: dict = { + 'limit': 100, + 'start_time': self.iso8601(now - 31536000000), # -365 days + 'end_time': self.iso8601(now), + } + if symbol is not None: + market = self.market(symbol) + request['market_id'] = market['id'] + if since is not None: + request['start_time'] = self.iso8601(since) + request['end_time'] = self.iso8601(min(now, since + 31536000000)) + if limit is not None: + request['limit'] = limit + response = await self.privateGetTradeHistory(self.extend(request, params)) + # + # { + # "data": [ + # { + # "id":"BTC-USDT:183566", + # "order_id":"17209376", + # "side":"sell", + # "fee_amount":"0.657396569175", + # "fee_currency_id":"USDT", + # "status":"settled", + # "price":"6573.96569175", + # "quantity":"0.1", + # "cost":"657.396569175", + # "time":"2018-08-10T06:06:46.000Z", + # "market_id":"BTC-USDT" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_trades(data, market, since, limit) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + + https://docs-en.probit.com/reference/trade-1 + + get the list of most recent trades for a particular symbol + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'market_id': market['id'], + 'start_time': '1970-01-01T00:00:00.000Z', + 'end_time': self.iso8601(self.milliseconds()), + } + if since is not None: + request['start_time'] = self.iso8601(since) + if limit is not None: + request['limit'] = min(limit, 1000) + else: + request['limit'] = 1000 # required to set any value + response = await self.publicGetTrade(self.extend(request, params)) + # + # { + # "data":[ + # { + # "id":"ETH-BTC:3331886", + # "price":"0.022982", + # "quantity":"12.337", + # "time":"2020-04-12T20:55:42.371Z", + # "side":"sell", + # "tick_direction":"down" + # }, + # { + # "id":"ETH-BTC:3331885", + # "price":"0.022982", + # "quantity":"6.472", + # "time":"2020-04-12T20:55:39.652Z", + # "side":"sell", + # "tick_direction":"down" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_trades(data, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(public) + # + # { + # "id":"ETH-BTC:3331886", + # "price":"0.022981", + # "quantity":"12.337", + # "time":"2020-04-12T20:55:42.371Z", + # "side":"sell", + # "tick_direction":"down" + # } + # + # fetchMyTrades(private) + # + # { + # "id":"BTC-USDT:183566", + # "order_id":"17209376", + # "side":"sell", + # "fee_amount":"0.657396569175", + # "fee_currency_id":"USDT", + # "status":"settled", + # "price":"6573.96569175", + # "quantity":"0.1", + # "cost":"657.396569175", + # "time":"2018-08-10T06:06:46.000Z", + # "market_id":"BTC-USDT" + # } + # + timestamp = self.parse8601(self.safe_string(trade, 'time')) + id = self.safe_string(trade, 'id') + marketId: Str = None + if id is not None: + parts = id.split(':') + marketId = self.safe_string(parts, 0) + marketId = self.safe_string(trade, 'market_id', marketId) + symbol = self.safe_symbol(marketId, market, '-') + side = self.safe_string(trade, 'side') + priceString = self.safe_string(trade, 'price') + amountString = self.safe_string(trade, 'quantity') + orderId = self.safe_string(trade, 'order_id') + feeCostString = self.safe_string(trade, 'fee_amount') + fee = None + if feeCostString is not None: + feeCurrencyId = self.safe_string(trade, 'fee_currency_id') + feeCurrencyCode = self.safe_currency_code(feeCurrencyId) + fee = { + 'cost': feeCostString, + 'currency': feeCurrencyCode, + } + return self.safe_trade({ + 'id': id, + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'order': orderId, + 'type': None, + 'side': side, + 'takerOrMaker': None, + 'price': priceString, + 'amount': amountString, + 'cost': None, + 'fee': fee, + }, market) + + async def fetch_time(self, params={}) -> Int: + """ + + https://docs-en.probit.com/reference/time + + fetches the current integer timestamp in milliseconds from the exchange server + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = await self.publicGetTime(params) + # + # {"data":"2020-04-12T18:54:25.390Z"} + # + timestamp = self.parse8601(self.safe_string(response, 'data')) + return timestamp + + def normalize_ohlcv_timestamp(self, timestamp, timeframe, after=False): + duration = self.parse_timeframe(timeframe) + if timeframe == '1M': + iso8601 = self.iso8601(timestamp) + parts = iso8601.split('-') + year = self.safe_string(parts, 0) + month = self.safe_integer(parts, 1) + monthString: Str = None + if after: + monthString = self.sum(month, str(1)) + if month < 10: + monthString = '0' + str(month) + return year + '-' + monthString + '-01T00:00:00.000Z' + elif timeframe == '1w': + timestamp = self.parse_to_int(timestamp / 1000) + firstSunday = 259200 # 1970-01-04T00:00:00.000Z + difference = timestamp - firstSunday + numWeeks = int(math.floor(difference / duration)) + previousSunday = self.sum(firstSunday, numWeeks * duration) + if after: + previousSunday = self.sum(previousSunday, duration) + return self.iso8601(previousSunday * 1000) + else: + timestamp = self.parse_to_int(timestamp / 1000) + timestamp = duration * self.parse_to_int(timestamp / duration) + if after: + timestamp = self.sum(timestamp, duration) + return self.iso8601(timestamp * 1000) + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + + https://docs-en.probit.com/reference/candle + + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.until]: timestamp in ms of the earliest candle to fetch + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + interval = self.safe_string(self.timeframes, timeframe, timeframe) + limit = 100 if (limit is None) else limit + requestLimit = self.sum(limit, 1) + requestLimit = min(1000, requestLimit) # max 1000 + request: dict = { + 'market_ids': market['id'], + 'interval': interval, + 'sort': 'asc', # 'asc' will always include the start_time, 'desc' will always include end_time + 'limit': requestLimit, # max 1000 + } + now = self.milliseconds() + until = self.safe_integer(params, 'until') + durationMilliseconds = self.parse_timeframe(timeframe) * 1000 + startTime = since + endTime = until - durationMilliseconds if (until is not None) else now + if since is None: + if limit is None: + limit = requestLimit + startLimit = limit - 1 + startTime = endTime - startLimit * durationMilliseconds + else: + if limit is not None: + endByLimit = self.sum(since, limit * durationMilliseconds) + endTime = min(endTime, endByLimit) + startTimeNormalized = self.normalize_ohlcv_timestamp(startTime, timeframe) + endTimeNormalized = self.normalize_ohlcv_timestamp(endTime, timeframe, True) + request['start_time'] = startTimeNormalized + request['end_time'] = endTimeNormalized + response = await self.publicGetCandle(self.extend(request, params)) + # + # { + # "data":[ + # { + # "market_id":"ETH-BTC", + # "open":"0.02811", + # "close":"0.02811", + # "low":"0.02811", + # "high":"0.02811", + # "base_volume":"0.0005", + # "quote_volume":"0.000014055", + # "start_time":"2018-11-30T18:19:00.000Z", + # "end_time":"2018-11-30T18:20:00.000Z" + # }, + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_ohlcvs(data, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # { + # "market_id":"ETH-BTC", + # "open":"0.02811", + # "close":"0.02811", + # "low":"0.02811", + # "high":"0.02811", + # "base_volume":"0.0005", + # "quote_volume":"0.000014055", + # "start_time":"2018-11-30T18:19:00.000Z", + # "end_time":"2018-11-30T18:20:00.000Z" + # } + # + return [ + self.parse8601(self.safe_string(ohlcv, 'start_time')), + self.safe_number(ohlcv, 'open'), + self.safe_number(ohlcv, 'high'), + self.safe_number(ohlcv, 'low'), + self.safe_number(ohlcv, 'close'), + self.safe_number(ohlcv, 'base_volume'), + ] + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + + https://docs-en.probit.com/reference/open_order-1 + + fetch all unfilled currently open orders + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + since = self.parse8601(since) + request: dict = {} + market: Market = None + if symbol is not None: + market = self.market(symbol) + request['market_id'] = market['id'] + response = await self.privateGetOpenOrder(self.extend(request, params)) + data = self.safe_list(response, 'data') + return self.parse_orders(data, market, since, limit) + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + + https://docs-en.probit.com/reference/order + + fetches information on multiple closed orders made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + request: dict = { + 'start_time': self.iso8601(0), + 'end_time': self.iso8601(self.milliseconds()), + 'limit': 100, + } + market: Market = None + if symbol is not None: + market = self.market(symbol) + request['market_id'] = market['id'] + if since: + request['start_time'] = self.iso8601(since) + if limit: + request['limit'] = limit + response = await self.privateGetOrderHistory(self.extend(request, params)) + data = self.safe_list(response, 'data') + return self.parse_orders(data, market, since, limit) + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + + https://docs-en.probit.com/reference/order-3 + + fetches information on an order made by the user + :param str id: the order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrder() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'market_id': market['id'], + } + clientOrderId = self.safe_string_2(params, 'clientOrderId', 'client_order_id') + if clientOrderId is not None: + request['client_order_id'] = clientOrderId + else: + request['order_id'] = id + query = self.omit(params, ['clientOrderId', 'client_order_id']) + response = await self.privateGetOrder(self.extend(request, query)) + data = self.safe_value(response, 'data', []) + order = self.safe_dict(data, 0) + return self.parse_order(order, market) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'open': 'open', + 'cancelled': 'canceled', + 'filled': 'closed', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # { + # id, + # user_id, + # market_id, + # "type": "orderType", + # "side": "side", + # quantity, + # limit_price, + # "time_in_force": "timeInForce", + # filled_cost, + # filled_quantity, + # open_quantity, + # cancelled_quantity, + # "status": "orderStatus", + # "time": "date", + # client_order_id, + # } + # + status = self.parse_order_status(self.safe_string(order, 'status')) + id = self.safe_string(order, 'id') + type = self.safe_string(order, 'type') + side = self.safe_string(order, 'side') + marketId = self.safe_string(order, 'market_id') + symbol = self.safe_symbol(marketId, market, '-') + timestamp = self.parse8601(self.safe_string(order, 'time')) + price = self.safe_string(order, 'limit_price') + filled = self.safe_string(order, 'filled_quantity') + remaining = self.safe_string(order, 'open_quantity') + canceledAmount = self.safe_string(order, 'cancelled_quantity') + if canceledAmount is not None: + remaining = Precise.string_add(remaining, canceledAmount) + amount = self.safe_string(order, 'quantity', Precise.string_add(filled, remaining)) + cost = self.safe_string_2(order, 'filled_cost', 'cost') + if type == 'market': + price = None + clientOrderId = self.safe_string(order, 'client_order_id') + timeInForce = self.safe_string_upper(order, 'time_in_force') + return self.safe_order({ + 'id': id, + 'info': order, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'symbol': symbol, + 'type': type, + 'timeInForce': timeInForce, + 'side': side, + 'status': status, + 'price': price, + 'triggerPrice': None, + 'amount': amount, + 'filled': filled, + 'remaining': remaining, + 'average': None, + 'cost': cost, + 'fee': None, + 'trades': None, + }, market) + + def cost_to_precision(self, symbol, cost): + return self.decimal_to_precision(cost, TRUNCATE, self.markets[symbol]['precision']['cost'], self.precisionMode) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://docs-en.probit.com/reference/order-1 + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.cost]: the quote quantity that can be used alternative for the amount for market buy orders + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + options = self.safe_value(self.options, 'timeInForce') + defaultTimeInForce = self.safe_value(options, type) + timeInForce = self.safe_string_2(params, 'timeInForce', 'time_in_force', defaultTimeInForce) + request: dict = { + 'market_id': market['id'], + 'type': type, + 'side': side, + 'time_in_force': timeInForce, + } + clientOrderId = self.safe_string_2(params, 'clientOrderId', 'client_order_id') + if clientOrderId is not None: + request['client_order_id'] = clientOrderId + quoteAmount = None + if type == 'limit': + request['limit_price'] = self.price_to_precision(symbol, price) + request['quantity'] = self.amount_to_precision(symbol, amount) + elif type == 'market': + # for market buy it requires the amount of quote currency to spend + if side == 'buy': + createMarketBuyOrderRequiresPrice = True + createMarketBuyOrderRequiresPrice, params = self.handle_option_and_params(params, 'createOrder', 'createMarketBuyOrderRequiresPrice', True) + cost = self.safe_string(params, 'cost') + params = self.omit(params, 'cost') + if cost is not None: + quoteAmount = self.cost_to_precision(symbol, cost) + elif createMarketBuyOrderRequiresPrice: + if price is None: + raise InvalidOrder(self.id + ' createOrder() requires the price argument for market buy orders to calculate the total cost to spend(amount * price), alternatively set the createMarketBuyOrderRequiresPrice option or param to False and pass the cost to spend in the amount argument') + else: + amountString = self.number_to_string(amount) + priceString = self.number_to_string(price) + costRequest = Precise.string_mul(amountString, priceString) + quoteAmount = self.cost_to_precision(symbol, costRequest) + else: + quoteAmount = self.cost_to_precision(symbol, amount) + request['cost'] = quoteAmount + else: + request['quantity'] = self.amount_to_precision(symbol, amount) + query = self.omit(params, ['timeInForce', 'time_in_force', 'clientOrderId', 'client_order_id']) + response = await self.privatePostNewOrder(self.extend(request, query)) + # + # { + # "data": { + # id, + # user_id, + # market_id, + # "type": "orderType", + # "side": "side", + # quantity, + # limit_price, + # "time_in_force": "timeInForce", + # filled_cost, + # filled_quantity, + # open_quantity, + # cancelled_quantity, + # "status": "orderStatus", + # "time": "date", + # client_order_id, + # } + # } + # + data = self.safe_value(response, 'data') + order = self.parse_order(data, market) + # a workaround for incorrect huge amounts + # returned by the exchange on market buys + if (type == 'market') and (side == 'buy'): + order['amount'] = None + order['cost'] = self.parse_number(quoteAmount) + order['remaining'] = None + return order + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + + https://docs-en.probit.com/reference/order-2 + + cancels an open order + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'market_id': market['id'], + 'order_id': id, + } + response = await self.privatePostCancelOrder(self.extend(request, params)) + data = self.safe_dict(response, 'data') + return self.parse_order(data) + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + address = self.safe_string(depositAddress, 'address') + tag = self.safe_string(depositAddress, 'destination_tag') + currencyId = self.safe_string(depositAddress, 'currency_id') + currency = self.safe_currency(currencyId, currency) + code = currency['code'] + network = self.safe_string(depositAddress, 'platform_id') + self.check_address(address) + return { + 'info': depositAddress, + 'currency': code, + 'network': network, + 'address': address, + 'tag': tag, + } + + async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + + https://docs-en.probit.com/reference/deposit_address + + fetch the deposit address for a currency associated with self account + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency_id': currency['id'], + # 'platform_id': 'TRON',(undocumented) + } + networks = self.safe_value(self.options, 'networks', {}) + network = self.safe_string_upper(params, 'network') # self line allows the user to specify either ERC20 or ETH + network = self.safe_string(networks, network, network) # handle ERC20>ETH alias + if network is not None: + request['platform_id'] = network + params = self.omit(params, 'platform_id') + response = await self.privateGetDepositAddress(self.extend(request, params)) + # + # without 'platform_id' + # { + # "data":[ + # { + # "currency_id":"ETH", + # "address":"0x12e2caf3c4051ba1146e612f532901a423a9898a", + # "destination_tag":null + # } + # ] + # } + # + # with 'platform_id' + # { + # "data":[ + # { + # "platform_id":"TRON", + # "address":"TDQLMxBTa6MzuoZ6deSGZkqET3Ek8v7uC6", + # "destination_tag":null + # } + # ] + # } + # + data = self.safe_value(response, 'data', []) + firstAddress = self.safe_value(data, 0) + if firstAddress is None: + raise InvalidAddress(self.id + ' fetchDepositAddress() returned an empty response') + return self.parse_deposit_address(firstAddress, currency) + + async def fetch_deposit_addresses(self, codes: Strings = None, params={}) -> List[DepositAddress]: + """ + + https://docs-en.probit.com/reference/deposit_address + + fetch deposit addresses for multiple currencies and chain types + :param str[]|None codes: list of unified currency codes, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `address structures ` + """ + await self.load_markets() + request: dict = {} + if codes: + currencyIds = [] + for i in range(0, len(codes)): + currency = self.currency(codes[i]) + currencyIds.append(currency['id']) + request['currency_id'] = ','.join(codes) + response = await self.privateGetDepositAddress(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + return self.parse_deposit_addresses(data, codes) + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + + https://docs-en.probit.com/reference/withdrawal + + make a withdrawal + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + # In order to use self method + # you need to allow API withdrawal from the API Settings Page, and + # and register the list of withdrawal addresses and destination tags on the API Settings page + # you can only withdraw to the registered addresses using the API + self.check_address(address) + await self.load_markets() + currency = self.currency(code) + if tag is None: + tag = '' + request: dict = { + 'currency_id': currency['id'], + # 'platform_id': 'ETH', # if omitted it will use the default platform for the currency + 'address': address, + 'destination_tag': tag, + 'amount': self.number_to_string(amount), + # which currency to pay the withdrawal fees + # only applicable for currencies that accepts multiple withdrawal fee options + # 'fee_currency_id': 'ETH', # if omitted it will use the default fee policy for each currency + # whether the amount field includes fees + # 'include_fee': False, # makes sense only when fee_currency_id is equal to currency_id + } + networks = self.safe_value(self.options, 'networks', {}) + network = self.safe_string_upper(params, 'network') # self line allows the user to specify either ERC20 or ETH + network = self.safe_string(networks, network, network) # handle ERC20>ETH alias + if network is not None: + request['platform_id'] = network + params = self.omit(params, 'network') + response = await self.privatePostWithdrawal(self.extend(request, params)) + data = self.safe_dict(response, 'data') + return self.parse_transaction(data, currency) + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of transaction structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + request: dict = { + 'type': 'deposit', + } + result = await self.fetch_transactions(code, since, limit, self.extend(request, params)) + return result + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made to an account + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of transaction structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + request: dict = { + 'type': 'withdrawal', + } + result = await self.fetch_transactions(code, since, limit, self.extend(request, params)) + return result + + async def fetch_deposits_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch history of deposits and withdrawals + + https://docs-en.probit.com/reference/transferpayment + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch transactions for + :param int [limit]: the maximum number of transaction structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch transactions for + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + currency: Currency = None + request: dict = {} + if code is not None: + currency = self.currency(code) + request['currency_id'] = currency['id'] + if since is not None: + request['start_time'] = self.iso8601(since) + else: + request['start_time'] = self.iso8601(1) + until = self.safe_integer(params, 'until') + if until is not None: + request['end_time'] = self.iso8601(until) + params = self.omit(params, ['until']) + else: + request['end_time'] = self.iso8601(self.milliseconds()) + if limit is not None: + request['limit'] = limit + else: + request['limit'] = 100 + response = await self.privateGetTransferPayment(self.extend(request, params)) + # + # { + # "data": [ + # { + # "id": "01211d4b-0e68-41d6-97cb-298bfe2cab67", + # "type": "deposit", + # "status": "done", + # "amount": "0.01", + # "address": "0x9e7430fc0bdd14745bd00a1b92ed25133a7c765f", + # "time": "2023-06-14T12:03:11.000Z", + # "hash": "0x0ff5bedc9e378f9529acc6b9840fa8c2ef00fd0275e0bac7fa0589a9b5d1712e", + # "currency_id": "ETH", + # "confirmations":0, + # "fee": "0", + # "destination_tag": null, + # "platform_id": "ETH", + # "fee_currency_id": "ETH", + # "payment_service_name":null, + # "payment_service_display_name":null, + # "crypto":null + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_transactions(data, currency, since, limit) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # { + # "id": "01211d4b-0e68-41d6-97cb-298bfe2cab67", + # "type": "deposit", + # "status": "done", + # "amount": "0.01", + # "address": "0x9e7430fc0bdd14745bd00a1b92ed25133a7c765f", + # "time": "2023-06-14T12:03:11.000Z", + # "hash": "0x0ff5bedc9e378f9529acc6b9840fa8c2ef00fd0275e0bac7fa0589a9b5d1712e", + # "currency_id": "ETH", + # "confirmations":0, + # "fee": "0", + # "destination_tag": null, + # "platform_id": "ETH", + # "fee_currency_id": "ETH", + # "payment_service_name":null, + # "payment_service_display_name":null, + # "crypto":null + # } + # + id = self.safe_string(transaction, 'id') + networkId = self.safe_string(transaction, 'platform_id') + networkCode = self.network_id_to_code(networkId) + amount = self.safe_number(transaction, 'amount') + address = self.safe_string(transaction, 'address') + tag = self.safe_string(transaction, 'destination_tag') + txid = self.safe_string(transaction, 'hash') + timestamp = self.parse8601(self.safe_string(transaction, 'time')) + type = self.safe_string(transaction, 'type') + currencyId = self.safe_string(transaction, 'currency_id') + code = self.safe_currency_code(currencyId) + status = self.parse_transaction_status(self.safe_string(transaction, 'status')) + feeCostString = self.safe_string(transaction, 'fee') + fee = None + if feeCostString is not None and feeCostString != '0': + fee = { + 'currency': code, + 'cost': self.parse_number(feeCostString), + } + return { + 'id': id, + 'currency': code, + 'amount': amount, + 'network': networkCode, + 'addressFrom': None, + 'address': address, + 'addressTo': address, + 'tagFrom': None, + 'tag': tag, + 'tagTo': tag, + 'status': status, + 'type': type, + 'txid': txid, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'updated': None, + 'internal': None, + 'comment': None, + 'fee': fee, + 'info': transaction, + } + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'requested': 'pending', + 'pending': 'pending', + 'confirming': 'pending', + 'confirmed': 'pending', + 'applying': 'pending', + 'done': 'ok', + 'cancelled': 'canceled', + 'cancelling': 'canceled', + } + return self.safe_string(statuses, status, status) + + async def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}): + """ + + https://docs-en.probit.com/reference/currency + + fetch deposit and withdraw fees + :param str[]|None codes: list of unified currency codes + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `fees structures ` + """ + await self.load_markets() + response = await self.publicGetCurrencyWithPlatform(params) + # + # { + # "data": [ + # { + # "id": "AFX", + # "display_name": { + # "ko-kr": "아프릭스", + # "en-us": "Afrix" + # }, + # "show_in_ui": True, + # "platform": [ + # { + # "id": "ZYN", + # "priority": 1, + # "deposit": True, + # "withdrawal": True, + # "currency_id": "AFX", + # "precision": 18, + # "min_confirmation_count": 60, + # "require_destination_tag": False, + # "allow_withdrawal_destination_tag": False, + # "display_name": { + # "name": { + # "ko-kr": "지네코인", + # "en-us": "Wethio" + # } + # }, + # "min_deposit_amount": "0", + # "min_withdrawal_amount": "0", + # "withdrawal_fee": [ + # { + # "currency_id": "ZYN", + # "amount": "0.5", + # "priority": 1 + # } + # ], + # "deposit_fee": {}, + # "suspended_reason": "", + # "deposit_suspended": False, + # "withdrawal_suspended": False, + # "platform_currency_display_name": {} + # } + # ], + # "internal_transfer": { + # "suspended_reason": null, + # "suspended": False + # }, + # "stakeable": False, + # "unstakeable": False, + # "auto_stake": False, + # "auto_stake_amount": "0" + # }, + # ] + # } + # + data = self.safe_list(response, 'data') + return self.parse_deposit_withdraw_fees(data, codes, 'id') + + def parse_deposit_withdraw_fee(self, fee, currency: Currency = None): + # + # { + # "id": "USDT", + # "display_name": {"ko-kr": '테더', "en-us": "Tether"}, + # "show_in_ui": True, + # "platform": [ + # { + # "id": "ETH", + # "priority": "1", + # "deposit": True, + # "withdrawal": True, + # "currency_id": "USDT", + # "precision": "6", + # "min_confirmation_count": "15", + # "require_destination_tag": False, + # "allow_withdrawal_destination_tag": False, + # "display_name": [Object], + # "min_deposit_amount": "0", + # "min_withdrawal_amount": "1", + # "withdrawal_fee": [Array], + # "deposit_fee": {}, + # "suspended_reason": '', + # "deposit_suspended": False, + # "withdrawal_suspended": False, + # "platform_currency_display_name": [Object] + # }, + # ], + # "internal_transfer": {suspended_reason: null, suspended: False}, + # "stakeable": False, + # "unstakeable": False, + # "auto_stake": False, + # "auto_stake_amount": "0" + # } + # + depositWithdrawFee = self.deposit_withdraw_fee({}) + platforms = self.safe_value(fee, 'platform', []) + depositResult: dict = { + 'fee': None, + 'percentage': None, + } + for i in range(0, len(platforms)): + network = platforms[i] + networkId = self.safe_string(network, 'id') + networkCode = self.network_id_to_code(networkId, currency['code']) + withdrawalFees = self.safe_value(network, 'withdrawal_fee', {}) + withdrawFee = self.safe_number(withdrawalFees[0], 'amount') + if len(withdrawalFees): + withdrawResult: dict = { + 'fee': withdrawFee, + 'percentage': False if (withdrawFee is not None) else None, + } + if i == 0: + depositWithdrawFee['withdraw'] = withdrawResult + depositWithdrawFee['networks'][networkCode] = { + 'withdraw': withdrawResult, + 'deposit': depositResult, + } + depositWithdrawFee['info'] = fee + return depositWithdrawFee + + def nonce(self): + return self.milliseconds() + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = self.urls['api'][api] + '/' + query = self.omit(params, self.extract_params(path)) + if api == 'accounts': + self.check_required_credentials() + url += self.implode_params(path, params) + auth = self.apiKey + ':' + self.secret + auth64 = self.string_to_base64(auth) + headers = { + 'Authorization': 'Basic ' + auth64, + 'Content-Type': 'application/json', + } + if query: + body = self.json(query) + else: + url += self.version + '/' + if api == 'public': + url += self.implode_params(path, params) + if query: + url += '?' + self.urlencode(query) + elif api == 'private': + now = self.milliseconds() + self.check_required_credentials() + expires = self.safe_integer(self.options, 'expires') + if (expires is None) or (expires < now): + raise AuthenticationError(self.id + ' access token expired, call signIn() method') + accessToken = self.safe_string(self.options, 'accessToken') + headers = { + 'Authorization': 'Bearer ' + accessToken, + } + url += self.implode_params(path, params) + if method == 'GET': + if query: + url += '?' + self.urlencode(query) + elif query: + body = self.json(query) + headers['Content-Type'] = 'application/json' + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + async def sign_in(self, params={}): + """ + + https://docs-en.probit.com/reference/token + + sign in, must be called prior to using other authenticated methods + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns: response from exchange + """ + self.check_required_credentials() + request: dict = { + 'grant_type': 'client_credentials', # the only supported value + } + response = await self.accountsPostToken(self.extend(request, params)) + # + # { + # "access_token": "0ttDv/2hTTn3bLi8GP1gKaneiEQ6+0hOBenPrxNQt2s=", + # "token_type": "bearer", + # "expires_in": 900 + # } + # + expiresIn = self.safe_integer(response, 'expires_in') + accessToken = self.safe_string(response, 'access_token') + self.options['accessToken'] = accessToken + self.options['expires'] = self.sum(self.milliseconds(), expiresIn * 1000) + return response + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None # fallback to default error handler + if 'errorCode' in response: + errorCode = self.safe_string(response, 'errorCode') + if errorCode is not None: + errMessage = self.safe_string(response, 'message', '') + details = self.safe_value(response, 'details') + feedback = self.id + ' ' + errorCode + ' ' + errMessage + ' ' + self.json(details) + if 'exact' in self.exceptions: + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + if 'broad' in self.exceptions: + self.throw_broadly_matched_exception(self.exceptions['broad'], errMessage, feedback) + raise ExchangeError(feedback) + return None diff --git a/ccxt/async_support/timex.py b/ccxt/async_support/timex.py new file mode 100644 index 0000000..8623609 --- /dev/null +++ b/ccxt/async_support/timex.py @@ -0,0 +1,1752 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.timex import ImplicitAPI +from ccxt.base.types import Any, Balances, Currencies, Currency, DepositAddress, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, TradingFeeInterface, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import NotSupported +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class timex(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(timex, self).describe(), { + 'id': 'timex', + 'name': 'TimeX', + 'countries': ['AU'], + 'version': 'v1', + 'rateLimit': 1500, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelOrder': True, + 'cancelOrders': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createOrder': True, + 'createReduceOnlyOrder': False, + 'createStopLimitOrder': False, + 'createStopMarketOrder': False, + 'createStopOrder': False, + 'editOrder': True, + 'fetchAllGreeks': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDeposit': False, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrice': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': True, # maker fee only + 'fetchUnderlyingAssets': False, + 'fetchVolatilityHistory': False, + 'fetchWithdrawal': False, + 'fetchWithdrawals': True, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + }, + 'timeframes': { + '1m': 'I1', + '5m': 'I5', + '15m': 'I15', + '30m': 'I30', + '1h': 'H1', + '2h': 'H2', + '4h': 'H4', + '6h': 'H6', + '12h': 'H12', + '1d': 'D1', + '1w': 'W1', + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/70423869-6839ab00-1a7f-11ea-8f94-13ae72c31115.jpg', + 'api': { + 'rest': 'https://plasma-relay-backend.timex.io', + }, + 'www': 'https://timex.io', + 'doc': 'https://plasma-relay-backend.timex.io/swagger-ui/index.html', + 'referral': 'https://timex.io/?refcode=1x27vNkTbP1uwkCck', + }, + 'api': { + 'addressbook': { + 'get': [ + 'me', + ], + 'post': [ + '', + 'id/{id}', + 'id/{id}/remove', + ], + }, + 'custody': { + 'get': [ + 'credentials', # Get api key for address + 'credentials/h/{hash}', # Get api key by hash + 'credentials/k/{key}', # Get api key by key + 'credentials/me', + 'credentials/me/address', # Get api key by hash + 'deposit-addresses', # Get deposit addresses list + 'deposit-addresses/h/{hash}', # Get deposit address by hash + ], + }, + 'history': { + 'get': [ + 'orders', # Gets historical orders + 'orders/details', # Gets order details + 'orders/export/csv', # Export orders to csv + 'trades', # Gets historical trades + 'trades/export/csv', # Export trades to csv + ], + }, + 'currencies': { + 'get': [ + 'a/{address}', # Gets currency by address + 'i/{id}', # Gets currency by id + 's/{symbol}', # Gets currency by symbol + ], + 'post': [ + 'perform', # Creates new currency + 'prepare', # Prepare creates new currency + 'remove/perform', # Removes currency by symbol + 's/{symbol}/remove/prepare', # Prepare remove currency by symbol + 's/{symbol}/update/perform', # Prepare update currency by symbol + 's/{symbol}/update/prepare', # Prepare update currency by symbol + ], + }, + 'manager': { + 'get': [ + 'deposits', + 'transfers', + 'withdrawals', + ], + }, + 'markets': { + 'get': [ + 'i/{id}', # Gets market by id + 's/{symbol}', # Gets market by symbol + ], + 'post': [ + 'perform', # Creates new market + 'prepare', # Prepare creates new market + 'remove/perform', # Removes market by symbol + 's/{symbol}/remove/prepare', # Prepare remove market by symbol + 's/{symbol}/update/perform', # Prepare update market by symbol + 's/{symbol}/update/prepare', # Prepare update market by symbol + ], + }, + 'public': { + 'get': [ + 'candles', # Gets candles + 'currencies', # Gets all the currencies + 'markets', # Gets all the markets + 'orderbook', # Gets orderbook + 'orderbook/raw', # Gets raw orderbook + 'orderbook/v2', # Gets orderbook v2 + 'tickers', # Gets all the tickers + 'trades', # Gets trades + ], + }, + 'statistics': { + 'get': [ + 'address', # calculateAddressStatistics + ], + }, + 'trading': { + 'get': [ + 'balances', # Get trading balances for all(or selected) currencies + 'fees', # Get trading fee rates for all(or selected) markets + 'orders', # Gets open orders + ], + 'post': [ + 'orders', # Create new order + 'orders/json', # Create orders + ], + 'put': [ + 'orders', # Cancel or update orders + 'orders/json', # Update orders + ], + 'delete': [ + 'orders', # Delete orders + 'orders/json', # Delete orders + ], + }, + 'tradingview': { + 'get': [ + 'config', # Gets config + 'history', # Gets history + 'symbol_info', # Gets symbol info + 'time', # Gets time + ], + }, + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'exact': { + '0': ExchangeError, + '1': NotSupported, + '4000': BadRequest, + '4001': BadRequest, + '4002': InsufficientFunds, + '4003': AuthenticationError, + '4004': AuthenticationError, + '4005': BadRequest, + '4006': BadRequest, + '4007': BadRequest, + '4300': PermissionDenied, + '4100': AuthenticationError, + '4400': OrderNotFound, + '5001': InvalidOrder, + '5002': ExchangeError, + '400': BadRequest, + '401': AuthenticationError, + '403': PermissionDenied, + '404': OrderNotFound, + '429': RateLimitExceeded, + '500': ExchangeError, + '503': ExchangeNotAvailable, + }, + 'broad': { + 'Insufficient': InsufficientFunds, + }, + }, + 'options': { + 'expireIn': 31536000, # 365 × 24 × 60 × 60 + 'fetchTickers': { + 'period': '1d', + }, + 'fetchTrades': { + 'sort': 'timestamp,asc', + }, + 'fetchMyTrades': { + 'sort': 'timestamp,asc', + }, + 'fetchOpenOrders': { + 'sort': 'createdAt,asc', + }, + 'fetchClosedOrders': { + 'sort': 'createdAt,asc', + }, + 'defaultSort': 'timestamp,asc', + 'defaultSortOrders': 'createdAt,asc', + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': False, + 'triggerDirection': False, + 'triggerPriceType': None, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + # todo + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': False, + 'GTD': True, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': False, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, # todo + 'daysBack': 100000, # todo + 'untilDays': 100000, # todo + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 100, # todo + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, # todo + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 100, # todo + 'daysBack': 100000, # todo + 'daysBackCanceled': 1, # todo + 'untilDays': 100000, # todo + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': None, + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + }) + + async def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = await self.tradingviewGetTime(params) + # + # 1708682617 + # + return self.parse_to_int(response) * 1000 + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for timex + + https://plasma-relay-backend.timex.io/swagger-ui/index.html?urls.primaryName=Relay#/Public/listMarkets + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = await self.publicGetMarkets(params) + # + # [ + # { + # "symbol": "ETHBTC", + # "name": "ETH/BTC", + # "baseCurrency": "ETH", + # "baseTokenAddress": "0x45932db54b38af1f5a57136302eeba66a5975c15", + # "quoteCurrency": "BTC", + # "quoteTokenAddress": "0x8370fbc6ddec1e18b4e41e72ed943e238458487c", + # "feeCurrency": "BTC", + # "feeTokenAddress": "0x8370fbc6ddec1e18b4e41e72ed943e238458487c", + # "quantityIncrement": "0.0000001", + # "takerFee": "0.005", + # "makerFee": "0.0025", + # "tickSize": "0.00000001", + # "baseMinSize": "0.0001", + # "quoteMinSize": "0.00001", + # "locked": False + # } + # ] + # + return self.parse_markets(response) + + async def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://plasma-relay-backend.timex.io/swagger-ui/index.html?urls.primaryName=Relay#/Public/listCurrencies + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = await self.publicGetCurrencies(params) + # + # [ + # { + # "symbol": "BTC", + # "name": "Bitcoin", + # "address": "0x8370fbc6ddec1e18b4e41e72ed943e238458487c", + # "icon": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHZpZXdCb3g9IjAgMCA2MCA2MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggb3BhY2l0eT0iMC41IiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTMwIDUzQzQyLjcwMjUgNTMgNTMgNDIuNzAyNSA1MyAzMEM1MyAxNy4yOTc1IDQyLjcwMjUgNyAzMCA3QzE3LjI5NzUgNyA3IDE3LjI5NzUgNyAzMEM3IDQyLjcwMjUgMTcuMjk3NSA1MyAzMCA1M1pNMzAgNTVDNDMuODA3MSA1NSA1NSA0My44MDcxIDU1IDMwQzU1IDE2LjE5MjkgNDMuODA3MSA1IDMwIDVDMTYuMTkyOSA1IDUgMTYuMTkyOSA1IDMwQzUgNDMuODA3MSAxNi4xOTI5IDU1IDMwIDU1WiIvPgo8cGF0aCBkPSJNNDAuOTQyNSAyNi42NTg1QzQxLjQwMDMgMjMuNjExMyAzOS4wNzA1IDIxLjk3MzIgMzUuODg0OCAyMC44ODA0TDM2LjkxODIgMTYuNzUyNkwzNC4zOTUxIDE2LjEyNjRMMzMuMzg5IDIwLjE0NTVDMzIuNzI1OCAxOS45ODA5IDMyLjA0NDUgMTkuODI1NiAzMS4zNjc1IDE5LjY3MTdMMzIuMzgwOCAxNS42MjYyTDI5Ljg1OTEgMTVMMjguODI1IDE5LjEyNjRDMjguMjc2IDE5LjAwMTkgMjcuNzM3IDE4Ljg3ODggMjcuMjEzOSAxOC43NDkzTDI3LjIxNjggMTguNzM2NEwyMy43MzcyIDE3Ljg3MTJMMjMuMDY2IDIwLjU1NDhDMjMuMDY2IDIwLjU1NDggMjQuOTM4IDIwLjk4MjEgMjQuODk4NSAyMS4wMDg1QzI1LjkyMDQgMjEuMjYyNiAyNi4xMDUgMjEuOTM2IDI2LjA3NDEgMjIuNDY5OUwyNC44OTcgMjcuMTcyNEMyNC45Njc1IDI3LjE5MDMgMjUuMDU4NyAyNy4yMTYgMjUuMTU5MyAyNy4yNTYxQzI1LjA3NTMgMjcuMjM1NCAyNC45ODU0IDI3LjIxMjQgMjQuODkyNyAyNy4xOTAzTDIzLjI0MjggMzMuNzc3OEMyMy4xMTc3IDM0LjA4NjkgMjIuODAwOCAzNC41NTA2IDIyLjA4NjUgMzQuMzc0NkMyMi4xMTE3IDM0LjQxMTEgMjAuMjUyNiAzMy45MTg3IDIwLjI1MjYgMzMuOTE4N0wxOSAzNi43OTQ5TDIyLjI4MzQgMzcuNjFDMjIuODk0MiAzNy43NjI0IDIzLjQ5MjggMzcuOTIyIDI0LjA4MjEgMzguMDcyM0wyMy4wMzggNDIuMjQ3NEwyNS41NTgyIDQyLjg3MzZMMjYuNTkyMyAzOC43NDI5QzI3LjI4MDcgMzguOTI5IDI3Ljk0OSAzOS4xMDA3IDI4LjYwMyAzOS4yNjI0TDI3LjU3MjUgNDMuMzczOEwzMC4wOTU2IDQ0TDMxLjEzOTcgMzkuODMyOEMzNS40NDIyIDQwLjY0MzYgMzguNjc3NCA0MC4zMTY2IDQwLjAzOTIgMzYuNDQxNEM0MS4xMzY1IDMzLjMyMTIgMzkuOTg0NiAzMS41MjEzIDM3LjcyMDkgMzAuMzQ3N0MzOS4zNjk0IDI5Ljk2OTEgNDAuNjExMiAyOC44ODkyIDQwLjk0MjUgMjYuNjU4NVYyNi42NTg1Wk0zNS4xNzc3IDM0LjcwODhDMzQuMzk4IDM3LjgyOSAyOS4xMjI2IDM2LjE0MjIgMjcuNDEyMiAzNS43MTkzTDI4Ljc5NzcgMzAuMTg4MUMzMC41MDgxIDMwLjYxMzIgMzUuOTkyNiAzMS40NTQ4IDM1LjE3NzcgMzQuNzA4OFpNMzUuOTU4MSAyNi42MTM0QzM1LjI0NjcgMjkuNDUxNyAzMC44NTU5IDI4LjAwOTcgMjkuNDMxNiAyNy42NTYxTDMwLjY4NzcgMjIuNjM5NUMzMi4xMTIgMjIuOTkzIDM2LjY5OSAyMy42NTI4IDM1Ljk1ODEgMjYuNjEzNFoiLz4KPC9zdmc+Cg==", + # "background": "transparent", + # "fiatSymbol": "BTC", + # "decimals": 8, + # "tradeDecimals": 20, + # "displayDecimals": 4, + # "crypto": True, + # "depositEnabled": True, + # "withdrawalEnabled": True, + # "transferEnabled": True, + # "buyEnabled": False, + # "purchaseEnabled": False, + # "redeemEnabled": False, + # "active": True, + # "withdrawalFee": "50000000000000000", + # "purchaseCommissions": [] + # }, + # ] + # + return self.parse_currencies(response) + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://plasma-relay-backend.timex.io/swagger-ui/index.html?urls.primaryName=Relay#/Manager/getDeposits + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + address = self.safe_string(params, 'address') + params = self.omit(params, 'address') + if address is None: + raise ArgumentsRequired(self.id + ' fetchDeposits() requires an address parameter') + request: dict = { + 'address': address, + } + response = await self.managerGetDeposits(self.extend(request, params)) + # + # [ + # { + # "from": "0x1134cc86b45039cc211c6d1d2e4b3c77f60207ed", + # "timestamp": "2022-01-01T00:00:00Z", + # "to": "0x1134cc86b45039cc211c6d1d2e4b3c77f60207ed", + # "token": "0x6baad3fe5d0fd4be604420e728adbd68d67e119e", + # "transferHash": "0x5464cdff35448314e178b8677ea41e670ea0f2533f4e52bfbd4e4a6cfcdef4c2", + # "value": "100" + # } + # ] + # + currency = self.safe_currency(code) + return self.parse_transactions(response, currency, since, limit) + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made to an account + + https://plasma-relay-backend.timex.io/swagger-ui/index.html?urls.primaryName=Relay#/Manager/getWithdraws + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of transaction structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + address = self.safe_string(params, 'address') + params = self.omit(params, 'address') + if address is None: + raise ArgumentsRequired(self.id + ' fetchDeposits() requires an address parameter') + request: dict = { + 'address': address, + } + response = await self.managerGetWithdrawals(self.extend(request, params)) + # + # [ + # { + # "from": "0x1134cc86b45039cc211c6d1d2e4b3c77f60207ed", + # "timestamp": "2022-01-01T00:00:00Z", + # "to": "0x1134cc86b45039cc211c6d1d2e4b3c77f60207ed", + # "token": "0x6baad3fe5d0fd4be604420e728adbd68d67e119e", + # "transferHash": "0x5464cdff35448314e178b8677ea41e670ea0f2533f4e52bfbd4e4a6cfcdef4c2", + # "value": "100" + # } + # ] + # + currency = self.safe_currency(code) + return self.parse_transactions(response, currency, since, limit) + + def get_currency_by_address(self, address): + currencies = self.currencies + for i in range(0, len(currencies)): + currency = currencies[i] + info = self.safe_value(currency, 'info', {}) + a = self.safe_string(info, 'address') + if a == address: + return currency + return None + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # { + # "from": "0x1134cc86b45039cc211c6d1d2e4b3c77f60207ed", + # "timestamp": "2022-01-01T00:00:00Z", + # "to": "0x1134cc86b45039cc211c6d1d2e4b3c77f60207ed", + # "token": "0x6baad3fe5d0fd4be604420e728adbd68d67e119e", + # "transferHash": "0x5464cdff35448314e178b8677ea41e670ea0f2533f4e52bfbd4e4a6cfcdef4c2", + # "value": "100" + # } + # + datetime = self.safe_string(transaction, 'timestamp') + currencyAddresss = self.safe_string(transaction, 'token', '') + currency = self.get_currency_by_address(currencyAddresss) + return { + 'info': transaction, + 'id': self.safe_string(transaction, 'transferHash'), + 'txid': self.safe_string(transaction, 'txid'), + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + 'network': None, + 'address': None, + 'addressTo': self.safe_string(transaction, 'to'), + 'addressFrom': self.safe_string(transaction, 'from'), + 'tag': None, + 'tagTo': None, + 'tagFrom': None, + 'type': None, + 'amount': self.safe_number(transaction, 'value'), + 'currency': self.safe_currency_code(None, currency), + 'status': 'ok', + 'updated': None, + 'internal': None, + 'comment': None, + 'fee': None, + } + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://plasma-relay-backend.timex.io/swagger-ui/index.html?urls.primaryName=Relay#/Public/listTickers + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + period = self.safe_string(self.options['fetchTickers'], 'period', '1d') + request: dict = { + 'period': self.timeframes[period], # I1, I5, I15, I30, H1, H2, H4, H6, H12, D1, W1 + } + response = await self.publicGetTickers(self.extend(request, params)) + # + # [ + # { + # "ask": 0.017, + # "bid": 0.016, + # "high": 0.019, + # "last": 0.017, + # "low": 0.015, + # "market": "TIME/ETH", + # "open": 0.016, + # "period": "H1", + # "timestamp": "2018-12-14T20:50:36.134Z", + # "volume": 4.57, + # "volumeQuote": 0.07312 + # } + # ] + # + return self.parse_tickers(response, symbols) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://plasma-relay-backend.timex.io/swagger-ui/index.html?urls.primaryName=Relay#/Public/listTickers + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + period = self.safe_string(self.options['fetchTickers'], 'period', '1d') + request: dict = { + 'market': market['id'], + 'period': self.timeframes[period], # I1, I5, I15, I30, H1, H2, H4, H6, H12, D1, W1 + } + response = await self.publicGetTickers(self.extend(request, params)) + # + # [ + # { + # "ask": 0.017, + # "bid": 0.016, + # "high": 0.019, + # "last": 0.017, + # "low": 0.015, + # "market": "TIME/ETH", + # "open": 0.016, + # "period": "H1", + # "timestamp": "2018-12-14T20:50:36.134Z", + # "volume": 4.57, + # "volumeQuote": 0.07312 + # } + # ] + # + ticker = self.safe_dict(response, 0) + return self.parse_ticker(ticker, market) + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://plasma-relay-backend.timex.io/swagger-ui/index.html?urls.primaryName=Relay#/Public/orderbookV2 + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + if limit is not None: + request['limit'] = limit + response = await self.publicGetOrderbookV2(self.extend(request, params)) + # + # { + # "timestamp":"2019-12-05T00:21:09.538", + # "bid":[ + # { + # "index":"2", + # "price":"0.02024007", + # "baseTokenAmount":"0.0096894", + # "baseTokenCumulativeAmount":"0.0096894", + # "quoteTokenAmount":"0.000196114134258", + # "quoteTokenCumulativeAmount":"0.000196114134258" + # }, + # "ask":[ + # { + # "index":"-3", + # "price":"0.02024012", + # "baseTokenAmount":"0.005", + # "baseTokenCumulativeAmount":"0.005", + # "quoteTokenAmount":"0.0001012006", + # "quoteTokenCumulativeAmount":"0.0001012006" + # }, + # ] + # } + # + timestamp = self.parse8601(self.safe_string(response, 'timestamp')) + return self.parse_order_book(response, symbol, timestamp, 'bid', 'ask', 'price', 'baseTokenAmount') + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://plasma-relay-backend.timex.io/swagger-ui/index.html?urls.primaryName=Relay#/Public/listTrades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + options = self.safe_value(self.options, 'fetchTrades', {}) + defaultSort = self.safe_value(options, 'sort', 'timestamp,asc') + sort = self.safe_string(params, 'sort', defaultSort) + query = self.omit(params, 'sort') + request: dict = { + # 'address': 'string', # trade’s member account(?) + # 'cursor': 1234, # int64(?) + # 'from': self.iso8601(since), + 'market': market['id'], + # 'page': 0, # results page you want to retrieve 0 .. N + # 'size': limit, # number of records per page, 100 by default + 'sort': sort, # array[string], sorting criteria in the format "property,asc" or "property,desc", default is ascending + # 'till': self.iso8601(self.milliseconds()), + } + if since is not None: + request['from'] = self.iso8601(since) + if limit is not None: + request['size'] = limit # default is 100 + response = await self.publicGetTrades(self.extend(request, query)) + # + # [ + # { + # "id":1, + # "timestamp":"2019-06-25T17:01:50.309", + # "direction":"BUY", + # "price":"0.027", + # "quantity":"0.001" + # } + # ] + # + return self.parse_trades(response, market, since, limit) + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://plasma-relay-backend.timex.io/swagger-ui/index.html?urls.primaryName=Relay#/Public/listCandles + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest candle to fetch + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + 'period': self.safe_string(self.timeframes, timeframe, timeframe), + } + # if since and limit are not specified + duration = self.parse_timeframe(timeframe) + until = self.safe_integer(params, 'until') + if limit is None: + limit = 1000 # exchange provides tens of thousands of data, but we set generous default value + if since is not None: + request['from'] = self.iso8601(since) + if until is None: + request['till'] = self.iso8601(self.sum(since, self.sum(limit, 1) * duration * 1000)) + else: + request['till'] = self.iso8601(until) + elif until is not None: + request['till'] = self.iso8601(until) + fromTimestamp = until - self.sum(limit, 1) * duration * 1000 + request['from'] = self.iso8601(fromTimestamp) + else: + now = self.milliseconds() + request['till'] = self.iso8601(now) + request['from'] = self.iso8601(now - self.sum(limit, 1) * duration * 1000 - 1) + params = self.omit(params, 'until') + response = await self.publicGetCandles(self.extend(request, params)) + # + # [ + # { + # "timestamp":"2019-12-04T23:00:00", + # "open":"0.02024009", + # "high":"0.02024009", + # "low":"0.02024009", + # "close":"0.02024009", + # "volume":"0.00008096036", + # "volumeQuote":"0.004", + # }, + # ] + # + return self.parse_ohlcvs(response, market, timeframe, since, limit) + + def parse_balance(self, response) -> Balances: + result: dict = { + 'info': response, + 'timestamp': None, + 'datetime': None, + } + for i in range(0, len(response)): + balance = response[i] + currencyId = self.safe_string(balance, 'currency') + code = self.safe_currency_code(currencyId) + account = self.account() + account['total'] = self.safe_string(balance, 'totalBalance') + account['used'] = self.safe_string(balance, 'lockedBalance') + result[code] = account + return self.safe_balance(result) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://plasma-relay-backend.timex.io/swagger-ui/index.html?urls.primaryName=Relay#/Trading/getBalances + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + response = await self.tradingGetBalances(params) + # + # [ + # {"currency":"BTC","totalBalance":"0","lockedBalance":"0"}, + # {"currency":"AUDT","totalBalance":"0","lockedBalance":"0"}, + # {"currency":"ETH","totalBalance":"0","lockedBalance":"0"}, + # {"currency":"TIME","totalBalance":"0","lockedBalance":"0"}, + # {"currency":"USDT","totalBalance":"0","lockedBalance":"0"} + # ] + # + return self.parse_balance(response) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://plasma-relay-backend.timex.io/swagger-ui/index.html?urls.primaryName=Relay#/Trading/createOrder + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + uppercaseSide = side.upper() + uppercaseType = type.upper() + postOnly = self.safe_bool(params, 'postOnly', False) + if postOnly: + uppercaseType = 'POST_ONLY' + params = self.omit(params, ['postOnly']) + request: dict = { + 'symbol': market['id'], + 'quantity': self.amount_to_precision(symbol, amount), + 'side': uppercaseSide, + 'orderTypes': uppercaseType, + # 'clientOrderId': '123', + # 'expireIn': 1575523308, # in seconds + # 'expireTime': 1575523308, # unix timestamp + } + query = params + if (uppercaseType == 'LIMIT') or (uppercaseType == 'POST_ONLY'): + request['price'] = self.price_to_precision(symbol, price) + defaultExpireIn = self.safe_integer(self.options, 'expireIn') + expireTime = self.safe_value(params, 'expireTime') + expireIn = self.safe_value(params, 'expireIn', defaultExpireIn) + if expireTime is not None: + request['expireTime'] = expireTime + elif expireIn is not None: + request['expireIn'] = expireIn + else: + raise InvalidOrder(self.id + ' createOrder() method requires a expireTime or expireIn param for a ' + type + ' order, you can also set the expireIn exchange-wide option') + query = self.omit(params, ['expireTime', 'expireIn']) + else: + request['price'] = 0 + response = await self.tradingPostOrders(self.extend(request, query)) + # + # { + # "orders": [ + # { + # "cancelledQuantity": "0.3", + # "clientOrderId": "my-order-1", + # "createdAt": "1970-01-01T00:00:00", + # "cursorId": 50, + # "expireTime": "1970-01-01T00:00:00", + # "filledQuantity": "0.3", + # "id": "string", + # "price": "0.017", + # "quantity": "0.3", + # "side": "BUY", + # "symbol": "TIMEETH", + # "type": "LIMIT", + # "updatedAt": "1970-01-01T00:00:00" + # } + # ] + # } + # + orders = self.safe_value(response, 'orders', []) + order = self.safe_dict(orders, 0, {}) + return self.parse_order(order, market) + + async def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'id': id, + } + if amount is not None: + request['quantity'] = self.amount_to_precision(symbol, amount) + if price is not None: + request['price'] = self.price_to_precision(symbol, price) + response = await self.tradingPutOrders(self.extend(request, params)) + # + # { + # "changedOrders": [ + # { + # "newOrder": { + # "cancelledQuantity": "0.3", + # "clientOrderId": "my-order-1", + # "createdAt": "1970-01-01T00:00:00", + # "cursorId": 50, + # "expireTime": "1970-01-01T00:00:00", + # "filledQuantity": "0.3", + # "id": "string", + # "price": "0.017", + # "quantity": "0.3", + # "side": "BUY", + # "symbol": "TIMEETH", + # "type": "LIMIT", + # "updatedAt": "1970-01-01T00:00:00" + # }, + # "oldId": "string", + # }, + # ], + # "unchangedOrders": ["string"], + # } + # + if 'unchangedOrders' in response: + orderIds = self.safe_value(response, 'unchangedOrders', []) + orderId = self.safe_string(orderIds, 0) + return self.safe_order({ + 'id': orderId, + 'info': response, + }) + orders = self.safe_value(response, 'changedOrders', []) + firstOrder = self.safe_value(orders, 0, {}) + order = self.safe_dict(firstOrder, 'newOrder', {}) + return self.parse_order(order, market) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://plasma-relay-backend.timex.io/swagger-ui/index.html?urls.primaryName=Relay#/Trading/deleteOrders + + :param str id: order id + :param str symbol: not used by timex cancelOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + orders = await self.cancel_orders([id], symbol, params) + return self.safe_dict(orders, 0) + + async def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): + """ + cancel multiple orders + + https://plasma-relay-backend.timex.io/swagger-ui/index.html?urls.primaryName=Relay#/Trading/deleteOrders + + :param str[] ids: order ids + :param str symbol: unified market symbol, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an list of `order structures ` + """ + await self.load_markets() + request: dict = { + 'id': ids, + } + response = await self.tradingDeleteOrders(self.extend(request, params)) + # + # { + # "changedOrders": [ + # { + # "newOrder": { + # "cancelledQuantity": "0.3", + # "clientOrderId": "my-order-1", + # "createdAt": "1970-01-01T00:00:00", + # "cursorId": 50, + # "expireTime": "1970-01-01T00:00:00", + # "filledQuantity": "0.3", + # "id": "string", + # "price": "0.017", + # "quantity": "0.3", + # "side": "BUY", + # "symbol": "TIMEETH", + # "type": "LIMIT", + # "updatedAt": "1970-01-01T00:00:00" + # }, + # "oldId": "string", + # }, + # ], + # "unchangedOrders": ["string"], + # } + # + changedOrders = self.safe_list(response, 'changedOrders', []) + unchangedOrders = self.safe_list(response, 'unchangedOrders', []) + orders = [] + for i in range(0, len(changedOrders)): + newOrder = self.safe_dict(changedOrders[i], 'newOrder') + orders.append(self.parse_order(newOrder)) + for i in range(0, len(unchangedOrders)): + orders.append(self.safe_order({ + 'info': unchangedOrders[i], + 'id': unchangedOrders[i], + 'status': 'unchanged', + })) + return orders + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://plasma-relay-backend.timex.io/swagger-ui/index.html?urls.primaryName=Relay#/History/getOrderDetails + + :param str id: order id + :param str symbol: not used by timex fetchOrder + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + request: dict = { + 'orderHash': id, + } + response = await self.historyGetOrdersDetails(request) + # + # { + # "order": { + # "cancelledQuantity": "0.3", + # "clientOrderId": "my-order-1", + # "createdAt": "1970-01-01T00:00:00", + # "cursorId": 50, + # "expireTime": "1970-01-01T00:00:00", + # "filledQuantity": "0.3", + # "id": "string", + # "price": "0.017", + # "quantity": "0.3", + # "side": "BUY", + # "symbol": "TIMEETH", + # "type": "LIMIT", + # "updatedAt": "1970-01-01T00:00:00" + # }, + # "trades": [ + # { + # "fee": "0.3", + # "id": 100, + # "makerOrTaker": "MAKER", + # "makerOrderId": "string", + # "price": "0.017", + # "quantity": "0.3", + # "side": "BUY", + # "symbol": "TIMEETH", + # "takerOrderId": "string", + # "timestamp": "2019-12-05T07:48:26.310Z" + # } + # ] + # } + # + order = self.safe_value(response, 'order', {}) + trades = self.safe_list(response, 'trades', []) + return self.parse_order(self.extend(order, {'trades': trades})) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://plasma-relay-backend.timex.io/swagger-ui/index.html?urls.primaryName=Relay#/Trading/getOpenOrders + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + options = self.safe_value(self.options, 'fetchOpenOrders', {}) + defaultSort = self.safe_value(options, 'sort', 'createdAt,asc') + sort = self.safe_string(params, 'sort', defaultSort) + query = self.omit(params, 'sort') + request: dict = { + # 'clientOrderId': '123', # order’s client id list for filter + # page: 0, # results page you want to retrieve(0 .. N) + 'sort': sort, # sorting criteria in the format "property,asc" or "property,desc", default order is ascending, multiple sort criteria are supported + } + market: Market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if limit is not None: + request['size'] = limit + response = await self.tradingGetOrders(self.extend(request, query)) + # + # { + # "orders": [ + # { + # "cancelledQuantity": "0.3", + # "clientOrderId": "my-order-1", + # "createdAt": "1970-01-01T00:00:00", + # "cursorId": 50, + # "expireTime": "1970-01-01T00:00:00", + # "filledQuantity": "0.3", + # "id": "string", + # "price": "0.017", + # "quantity": "0.3", + # "side": "BUY", + # "symbol": "TIMEETH", + # "type": "LIMIT", + # "updatedAt": "1970-01-01T00:00:00" + # } + # ] + # } + # + orders = self.safe_list(response, 'orders', []) + return self.parse_orders(orders, market, since, limit) + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://plasma-relay-backend.timex.io/swagger-ui/index.html?urls.primaryName=Relay#/History/getOrders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + options = self.safe_value(self.options, 'fetchClosedOrders', {}) + defaultSort = self.safe_value(options, 'sort', 'createdAt,asc') + sort = self.safe_string(params, 'sort', defaultSort) + query = self.omit(params, 'sort') + request: dict = { + # 'clientOrderId': '123', # order’s client id list for filter + # page: 0, # results page you want to retrieve(0 .. N) + 'sort': sort, # sorting criteria in the format "property,asc" or "property,desc", default order is ascending, multiple sort criteria are supported + 'side': 'BUY', # or 'SELL' + # 'till': self.iso8601(self.milliseconds()), + } + market: Market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if since is not None: + request['from'] = self.iso8601(since) + if limit is not None: + request['size'] = limit + response = await self.historyGetOrders(self.extend(request, query)) + # + # { + # "orders": [ + # { + # "cancelledQuantity": "0.3", + # "clientOrderId": "my-order-1", + # "createdAt": "1970-01-01T00:00:00", + # "cursorId": 50, + # "expireTime": "1970-01-01T00:00:00", + # "filledQuantity": "0.3", + # "id": "string", + # "price": "0.017", + # "quantity": "0.3", + # "side": "BUY", + # "symbol": "TIMEETH", + # "type": "LIMIT", + # "updatedAt": "1970-01-01T00:00:00" + # } + # ] + # } + # + orders = self.safe_list(response, 'orders', []) + return self.parse_orders(orders, market, since, limit) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://plasma-relay-backend.timex.io/swagger-ui/index.html?urls.primaryName=Relay#/History/getTrades_1 + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + options = self.safe_value(self.options, 'fetchMyTrades', {}) + defaultSort = self.safe_value(options, 'sort', 'timestamp,asc') + sort = self.safe_string(params, 'sort', defaultSort) + query = self.omit(params, 'sort') + request: dict = { + # 'cursorId': 123, # int64(?) + # 'from': self.iso8601(since), + # 'makerOrderId': '1234', # maker order hash + # 'owner': '...', # owner address(?) + # 'page': 0, # results page you want to retrieve(0 .. N) + # 'side': 'BUY', # or 'SELL' + # 'size': limit, + 'sort': sort, # sorting criteria in the format "property,asc" or "property,desc", default order is ascending, multiple sort criteria are supported + # 'symbol': market['id'], + # 'takerOrderId': '1234', + # 'till': self.iso8601(self.milliseconds()), + } + market: Market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if since is not None: + request['from'] = self.iso8601(since) + if limit is not None: + request['size'] = limit + response = await self.historyGetTrades(self.extend(request, query)) + # + # { + # "trades": [ + # { + # "fee": "0.3", + # "id": 100, + # "makerOrTaker": "MAKER", + # "makerOrderId": "string", + # "price": "0.017", + # "quantity": "0.3", + # "side": "BUY", + # "symbol": "TIMEETH", + # "takerOrderId": "string", + # "timestamp": "2019-12-08T04:54:11.171Z" + # } + # ] + # } + # + trades = self.safe_list(response, 'trades', []) + return self.parse_trades(trades, market, since, limit) + + def parse_trading_fee(self, fee: dict, market: Market = None) -> TradingFeeInterface: + # + # { + # "fee": 0.0075, + # "market": "ETHBTC" + # } + # + marketId = self.safe_string(fee, 'market') + rate = self.safe_number(fee, 'fee') + return { + 'info': fee, + 'symbol': self.safe_symbol(marketId, market), + 'maker': rate, + 'taker': rate, + 'percentage': None, + 'tierBased': None, + } + + async def fetch_trading_fee(self, symbol: str, params={}) -> TradingFeeInterface: + """ + fetch the trading fees for a market + + https://plasma-relay-backend.timex.io/swagger-ui/index.html?urls.primaryName=Relay#/Trading/getFees + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `fee structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'markets': market['id'], + } + response = await self.tradingGetFees(self.extend(request, params)) + # + # [ + # { + # "fee": 0.0075, + # "market": "ETHBTC" + # } + # ] + # + result = self.safe_value(response, 0, {}) + return self.parse_trading_fee(result, market) + + def parse_market(self, market: dict) -> Market: + # + # { + # "symbol": "ETHBTC", + # "name": "ETH/BTC", + # "baseCurrency": "ETH", + # "baseTokenAddress": "0x45932db54b38af1f5a57136302eeba66a5975c15", + # "quoteCurrency": "BTC", + # "quoteTokenAddress": "0x8370fbc6ddec1e18b4e41e72ed943e238458487c", + # "feeCurrency": "BTC", + # "feeTokenAddress": "0x8370fbc6ddec1e18b4e41e72ed943e238458487c", + # "quantityIncrement": "0.0000001", + # "takerFee": "0.005", + # "makerFee": "0.0025", + # "tickSize": "0.00000001", + # "baseMinSize": "0.0001", + # "quoteMinSize": "0.00001", + # "locked": False + # } + # + locked = self.safe_value(market, 'locked') + id = self.safe_string(market, 'symbol') + baseId = self.safe_string(market, 'baseCurrency') + quoteId = self.safe_string(market, 'quoteCurrency') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + amountIncrement = self.safe_string(market, 'quantityIncrement') + minBase = self.safe_string(market, 'baseMinSize') + minAmount = Precise.string_max(amountIncrement, minBase) + priceIncrement = self.safe_string(market, 'tickSize') + minCost = self.safe_number(market, 'quoteMinSize') + return { + 'id': id, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': not locked, + 'contract': False, + 'linear': None, + 'inverse': None, + 'taker': self.safe_number(market, 'takerFee'), + 'maker': self.safe_number(market, 'makerFee'), + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.safe_number(market, 'quantityIncrement'), + 'price': self.safe_number(market, 'tickSize'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.parse_number(minAmount), + 'max': None, + }, + 'price': { + 'min': self.parse_number(priceIncrement), + 'max': None, + }, + 'cost': { + 'min': minCost, + 'max': None, + }, + }, + 'created': None, + 'info': market, + } + + def parse_currency(self, currency: dict) -> Currency: + # + # { + # "symbol": "BTC", + # "name": "Bitcoin", + # "address": "0x8370fbc6ddec1e18b4e41e72ed943e238458487c", + # "icon": "data:image/svg+xml;base64,PHN2ZyB3aWR...mc+Cg==", + # "background": "transparent", + # "fiatSymbol": "BTC", + # "decimals": 8, + # "tradeDecimals": 20, + # "displayDecimals": 4, + # "crypto": True, + # "depositEnabled": True, + # "withdrawalEnabled": True, + # "transferEnabled": True, + # "buyEnabled": False, + # "purchaseEnabled": False, + # "redeemEnabled": False, + # "active": True, + # "withdrawalFee": "50000000000000000", + # "purchaseCommissions": [] + # } + # + # https://github.com/ccxt/ccxt/issues/6878 + # + # { + # "symbol":"XRP", + # "name":"Ripple", + # "address":"0x0dc8882914f3ddeebf4cec6dc20edb99df3def6c", + # "decimals":6, + # "tradeDecimals":16, + # "depositEnabled":true, + # "withdrawalEnabled":true, + # "transferEnabled":true, + # "active":true + # } + # + id = self.safe_string(currency, 'symbol') + code = self.safe_currency_code(id) + # fee = self.safe_number(currency, 'withdrawalFee') + feeString = self.safe_string(currency, 'withdrawalFee') + tradeDecimals = self.safe_integer(currency, 'tradeDecimals') + fee = None + if (feeString is not None) and (tradeDecimals is not None): + feeStringLen = len(feeString) + dotIndex = feeStringLen - tradeDecimals + if dotIndex > 0: + whole = feeString[0:dotIndex] + fraction = feeString[-dotIndex:] + fee = self.parse_number(whole + '.' + fraction) + else: + fraction = '.' + for i in range(0, -dotIndex): + fraction += '0' + fee = self.parse_number(fraction + feeString) + return self.safe_currency_structure({ + 'id': code, + 'code': code, + 'info': currency, + 'type': None, + 'name': self.safe_string(currency, 'name'), + 'active': self.safe_bool(currency, 'active'), + 'deposit': self.safe_bool(currency, 'depositEnabled'), + 'withdraw': self.safe_bool(currency, 'withdrawalEnabled'), + 'fee': fee, + 'precision': self.parse_number(self.parse_precision(self.safe_string(currency, 'decimals'))), + 'limits': { + 'withdraw': {'min': None, 'max': None}, + 'amount': {'min': None, 'max': None}, + }, + 'networks': {}, + }) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "ask": 0.017, + # "bid": 0.016, + # "high": 0.019, + # "last": 0.017, + # "low": 0.015, + # "market": "TIME/ETH", + # "open": 0.016, + # "period": "H1", + # "timestamp": "2018-12-14T20:50:36.134Z", + # "volume": 4.57, + # "volumeQuote": 0.07312 + # } + # + marketId = self.safe_string(ticker, 'market') + symbol = self.safe_symbol(marketId, market, '/') + timestamp = self.parse8601(self.safe_string(ticker, 'timestamp')) + last = self.safe_string(ticker, 'last') + open = self.safe_string(ticker, 'open') + return self.safe_ticker({ + 'symbol': symbol, + 'info': ticker, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': self.safe_string(ticker, 'bid'), + 'bidVolume': None, + 'ask': self.safe_string(ticker, 'ask'), + 'askVolume': None, + 'vwap': None, + 'open': open, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': self.safe_string(ticker, 'volume'), + 'quoteVolume': self.safe_string(ticker, 'volumeQuote'), + }, market) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(public) + # + # { + # "id":1, + # "timestamp":"2019-06-25T17:01:50.309", + # "direction":"BUY", + # "price":"0.027", + # "quantity":"0.001" + # } + # + # fetchMyTrades, fetchOrder(private) + # + # { + # "id": "7613414", + # "makerOrderId": "0x8420af060722f560098f786a2894d4358079b6ea5d14b395969ed77bc87a623a", + # "takerOrderId": "0x1235ef158a361815b54c9988b6241c85aedcbc1fe81caf8df8587d5ab0373d1a", + # "symbol": "LTCUSDT", + # "side": "BUY", + # "quantity": "0.2", + # "fee": "0.22685", + # "feeToken": "USDT", + # "price": "226.85", + # "makerOrTaker": "TAKER", + # "timestamp": "2021-04-09T15:39:45.608" + # } + # + marketId = self.safe_string(trade, 'symbol') + symbol = self.safe_symbol(marketId, market) + timestamp = self.parse8601(self.safe_string(trade, 'timestamp')) + priceString = self.safe_string(trade, 'price') + amountString = self.safe_string(trade, 'quantity') + price = self.parse_number(priceString) + amount = self.parse_number(amountString) + cost = self.parse_number(Precise.string_mul(priceString, amountString)) + id = self.safe_string(trade, 'id') + side = self.safe_string_lower_2(trade, 'direction', 'side') + takerOrMaker = self.safe_string_lower(trade, 'makerOrTaker') + orderId: Str = None + if takerOrMaker is not None: + orderId = self.safe_string(trade, takerOrMaker + 'OrderId') + fee = None + feeCost = self.safe_number(trade, 'fee') + feeCurrency = self.safe_currency_code(self.safe_string(trade, 'feeToken')) + if feeCost is not None: + fee = { + 'cost': feeCost, + 'currency': feeCurrency, + } + return self.safe_trade({ + 'info': trade, + 'id': id, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'order': orderId, + 'type': None, + 'side': side, + 'price': price, + 'amount': amount, + 'cost': cost, + 'takerOrMaker': takerOrMaker, + 'fee': fee, + }) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # { + # "timestamp":"2019-12-04T23:00:00", + # "open":"0.02024009", + # "high":"0.02024009", + # "low":"0.02024009", + # "close":"0.02024009", + # "volume":"0.00008096036", + # "volumeQuote":"0.004", + # } + # + return [ + self.parse8601(self.safe_string(ohlcv, 'timestamp')), + self.safe_number(ohlcv, 'open'), + self.safe_number(ohlcv, 'high'), + self.safe_number(ohlcv, 'low'), + self.safe_number(ohlcv, 'close'), + self.safe_number(ohlcv, 'volume'), + ] + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # fetchOrder, createOrder, cancelOrder, cancelOrders, fetchOpenOrders, fetchClosedOrders + # + # { + # "cancelledQuantity": "0.3", + # "clientOrderId": "my-order-1", + # "createdAt": "1970-01-01T00:00:00", + # "cursorId": 50, + # "expireTime": "1970-01-01T00:00:00", + # "filledQuantity": "0.3", + # "id": "string", + # "price": "0.017", + # "quantity": "0.3", + # "side": "BUY", + # "symbol": "TIMEETH", + # "type": "LIMIT", + # "updatedAt": "1970-01-01T00:00:00" + # "trades": [], # injected from the outside + # } + # + id = self.safe_string(order, 'id') + type = self.safe_string_lower(order, 'type') + side = self.safe_string_lower(order, 'side') + marketId = self.safe_string(order, 'symbol') + symbol = self.safe_symbol(marketId, market) + timestamp = self.parse8601(self.safe_string(order, 'createdAt')) + price = self.safe_string(order, 'price') + amount = self.safe_string(order, 'quantity') + filled = self.safe_string(order, 'filledQuantity') + canceledQuantity = self.omit_zero(self.safe_string(order, 'cancelledQuantity')) + status: str + if Precise.string_equals(filled, amount): + status = 'closed' + elif canceledQuantity is not None: + status = 'canceled' + else: + status = 'open' + rawTrades = self.safe_value(order, 'trades', []) + clientOrderId = self.safe_string(order, 'clientOrderId') + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'symbol': symbol, + 'type': type, + 'timeInForce': None, + 'postOnly': None, + 'side': side, + 'price': price, + 'triggerPrice': None, + 'amount': amount, + 'cost': None, + 'average': None, + 'filled': filled, + 'remaining': None, + 'status': status, + 'fee': None, + 'trades': rawTrades, + }, market) + + async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account, does not accept params["network"] + + https://plasma-relay-backend.timex.io/swagger-ui/index.html?urls.primaryName=Relay#/Currency/selectCurrencyBySymbol + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'symbol': currency['code'], + } + response = await self.currenciesGetSSymbol(self.extend(request, params)) + # + # { + # id: '1', + # currency: { + # symbol: 'BTC', + # name: 'Bitcoin', + # address: '0x8370fbc6ddec1e18b4e41e72ed943e238458487c', + # decimals: '8', + # tradeDecimals: '20', + # fiatSymbol: 'BTC', + # depositEnabled: True, + # withdrawalEnabled: True, + # transferEnabled: True, + # active: True + # } + # } + # + data = self.safe_dict(response, 'currency', {}) + return self.parse_deposit_address(data, currency) + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + # + # { + # symbol: 'BTC', + # name: 'Bitcoin', + # address: '0x8370fbc6ddec1e18b4e41e72ed943e238458487c', + # decimals: '8', + # tradeDecimals: '20', + # fiatSymbol: 'BTC', + # depositEnabled: True, + # withdrawalEnabled: True, + # transferEnabled: True, + # active: True + # } + # + currencyId = self.safe_string(depositAddress, 'symbol') + return { + 'info': depositAddress, + 'currency': self.safe_currency_code(currencyId, currency), + 'network': None, + 'address': self.safe_string(depositAddress, 'address'), + 'tag': None, + } + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + paramsToExtract = self.extract_params(path) + path = self.implode_params(path, params) + params = self.omit(params, paramsToExtract) + url = self.urls['api']['rest'] + '/' + api + '/' + path + if params: + url += '?' + self.urlencode_with_array_repeat(params) + if api != 'public' and api != 'tradingview': + self.check_required_credentials() + auth = self.string_to_base64(self.apiKey + ':' + self.secret) + secret = 'Basic ' + auth + headers = {'authorization': secret} + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, statusCode: int, statusText: str, url: str, method: str, responseHeaders: dict, responseBody, response, requestHeaders, requestBody): + if response is None: + return None + if statusCode >= 400: + # + # {"error":{"timestamp":"05.12.2019T05:25:43.584+0000","status":"BAD_REQUEST","message":"Insufficient ETH balance. Required: 1, actual: 0.","code":4001}} + # {"error":{"timestamp":"05.12.2019T04:03:25.419+0000","status":"FORBIDDEN","message":"Access denied","code":4300}} + # + feedback = self.id + ' ' + responseBody + error = self.safe_value(response, 'error') + if error is None: + error = response + code = self.safe_string_2(error, 'code', 'status') + message = self.safe_string_2(error, 'message', 'debugMessage') + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], code, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) + raise ExchangeError(feedback) + return None diff --git a/ccxt/async_support/tokocrypto.py b/ccxt/async_support/tokocrypto.py new file mode 100644 index 0000000..6dad0c5 --- /dev/null +++ b/ccxt/async_support/tokocrypto.py @@ -0,0 +1,2525 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.tokocrypto import ImplicitAPI +import hashlib +import json +from ccxt.base.types import Any, Balances, Currency, DepositAddress, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import AccountSuspended +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import MarginModeAlreadySet +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import OrderImmediatelyFillable +from ccxt.base.errors import OrderNotFillable +from ccxt.base.errors import NotSupported +from ccxt.base.errors import DDoSProtection +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.errors import OnMaintenance +from ccxt.base.errors import InvalidNonce +from ccxt.base.errors import RequestTimeout +from ccxt.base.errors import BadResponse +from ccxt.base.decimal_to_precision import TRUNCATE +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class tokocrypto(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(tokocrypto, self).describe(), { + 'id': 'tokocrypto', + 'name': 'Tokocrypto', + 'countries': ['ID'], # Indonesia + 'certified': False, + 'pro': False, + 'version': 'v1', + # new metainfo interface + 'has': { + 'CORS': None, + 'spot': True, + 'margin': True, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': None, + 'borrowMargin': None, + 'cancelAllOrders': False, + 'cancelOrder': True, + 'cancelOrders': None, + 'createDepositAddress': False, + 'createMarketBuyOrderWithCost': True, + 'createMarketOrderWithCost': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createReduceOnlyOrder': None, + 'createStopLimitOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'fetchAccounts': False, + 'fetchBalance': True, + 'fetchBidsAsks': True, + 'fetchBorrowInterest': None, + 'fetchBorrowRateHistories': None, + 'fetchBorrowRateHistory': None, + 'fetchCanceledOrders': False, + 'fetchClosedOrder': False, + 'fetchClosedOrders': 'emulated', + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': False, + 'fetchDeposit': False, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchDepositsWithdrawals': False, + 'fetchFundingHistory': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchL3OrderBook': False, + 'fetchLedger': None, + 'fetchLeverage': False, + 'fetchLeverageTiers': False, + 'fetchMarketLeverageTiers': 'emulated', + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterestHistory': False, + 'fetchOpenOrder': False, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrderBooks': False, + 'fetchOrders': True, + 'fetchOrderTrades': False, + 'fetchPosition': False, + 'fetchPositions': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchStatus': False, + 'fetchTicker': False, + 'fetchTickers': False, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': False, + 'fetchTradingLimits': False, + 'fetchTransactionFee': False, + 'fetchTransactionFees': False, + 'fetchTransactions': False, + 'fetchTransfers': False, + 'fetchWithdrawal': False, + 'fetchWithdrawals': True, + 'fetchWithdrawalWhitelist': False, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'signIn': False, + 'transfer': False, + 'withdraw': True, + }, + 'timeframes': { + '1m': '1m', + '3m': '3m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1h', + '2h': '2h', + '4h': '4h', + '6h': '6h', + '8h': '8h', + '12h': '12h', + '1d': '1d', + '3d': '3d', + '1w': '1w', + '1M': '1M', + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/183870484-d3398d0c-f6a1-4cce-91b8-d58792308716.jpg', + 'api': { + 'rest': { + 'public': 'https://www.tokocrypto.com', + 'binance': 'https://api.binance.com/api/v3', + 'private': 'https://www.tokocrypto.com', + }, + }, + 'www': 'https://tokocrypto.com', + # 'referral': 'https://www.binance.us/?ref=35005074', + 'doc': 'https://www.tokocrypto.com/apidocs/', + 'fees': 'https://www.tokocrypto.com/fees/newschedule', + }, + 'api': { + 'binance': { + 'get': { + 'ping': 1, + 'time': 1, + 'depth': {'cost': 1, 'byLimit': [[100, 1], [500, 5], [1000, 10], [5000, 50]]}, + 'trades': 1, + 'aggTrades': 1, + 'historicalTrades': 5, + 'klines': 1, + 'ticker/24hr': {'cost': 1, 'noSymbol': 40}, + 'ticker/price': {'cost': 1, 'noSymbol': 2}, + 'ticker/bookTicker': {'cost': 1, 'noSymbol': 2}, + 'exchangeInfo': 10, + }, + 'put': { + 'userDataStream': 1, + }, + 'post': { + 'userDataStream': 1, + }, + 'delete': { + 'userDataStream': 1, + }, + }, + 'public': { + 'get': { + 'open/v1/common/time': 1, + 'open/v1/common/symbols': 1, + # all the actual symbols are type 1 + 'open/v1/market/depth': 1, # when symbol type is not 1 + 'open/v1/market/trades': 1, # when symbol type is not 1 + 'open/v1/market/agg-trades': 1, # when symbol type is not 1 + 'open/v1/market/klines': 1, # when symbol type is not 1 + }, + }, + 'private': { + 'get': { + 'open/v1/orders/detail': 1, + 'open/v1/orders': 1, + 'open/v1/account/spot': 1, + 'open/v1/account/spot/asset': 1, + 'open/v1/orders/trades': 1, + 'open/v1/withdraws': 1, + 'open/v1/deposits': 1, + 'open/v1/deposits/address': 1, + }, + 'post': { + 'open/v1/orders': 1, + 'open/v1/orders/cancel': 1, + 'open/v1/orders/oco': 1, + 'open/v1/withdraws': 1, + 'open/v1/user-data-stream': 1, + }, + }, + }, + 'fees': { + 'trading': { + 'tierBased': True, + 'percentage': True, + 'taker': self.parse_number('0.0075'), # 0.1% trading fee, zero fees for all trading pairs before November 1 + 'maker': self.parse_number('0.0075'), # 0.1% trading fee, zero fees for all trading pairs before November 1 + }, + }, + 'precisionMode': TICK_SIZE, + 'options': { + # 'fetchTradesMethod': 'binanceGetTrades', # binanceGetTrades, binanceGetAggTrades + 'createMarketBuyOrderRequiresPrice': True, + 'defaultTimeInForce': 'GTC', # 'GTC' = Good To Cancel(default), 'IOC' = Immediate Or Cancel + # 'defaultType': 'spot', # 'spot', 'future', 'margin', 'delivery' + 'hasAlreadyAuthenticatedSuccessfully': False, + 'warnOnFetchOpenOrdersWithoutSymbol': True, + # 'fetchPositions': 'positionRisk', # or 'account' + 'recvWindow': 5 * 1000, # 5 sec, binance default + 'timeDifference': 0, # the difference between system clock and Binance clock + 'adjustForTimeDifference': False, # controls the adjustment logic upon instantiation + 'newOrderRespType': { + 'market': 'FULL', # 'ACK' for order id, 'RESULT' for full order or 'FULL' for order with fills + 'limit': 'FULL', # we change it from 'ACK' by default to 'FULL'(returns immediately if limit is not hit) + }, + 'quoteOrderQty': False, # whether market orders support amounts in quote currency + 'networks': { + 'ERC20': 'ETH', + 'TRC20': 'TRX', + 'BEP2': 'BNB', + 'BEP20': 'BSC', + 'OMNI': 'OMNI', + 'EOS': 'EOS', + 'SPL': 'SOL', + }, + 'reverseNetworks': { + 'tronscan.org': 'TRC20', + 'etherscan.io': 'ERC20', + 'bscscan.com': 'BSC', + 'explorer.binance.org': 'BEP2', + 'bithomp.com': 'XRP', + 'bloks.io': 'EOS', + 'stellar.expert': 'XLM', + 'blockchair.com/bitcoin': 'BTC', + 'blockchair.com/bitcoin-cash': 'BCH', + 'blockchair.com/ecash': 'XEC', + 'explorer.litecoin.net': 'LTC', + 'explorer.avax.network': 'AVAX', + 'solscan.io': 'SOL', + 'polkadot.subscan.io': 'DOT', + 'dashboard.internetcomputer.org': 'ICP', + 'explorer.chiliz.com': 'CHZ', + 'cardanoscan.io': 'ADA', + 'mainnet.theoan.com': 'AION', + 'algoexplorer.io': 'ALGO', + 'explorer.ambrosus.com': 'AMB', + 'viewblock.io/zilliqa': 'ZIL', + 'viewblock.io/arweave': 'AR', + 'explorer.ark.io': 'ARK', + 'atomscan.com': 'ATOM', + 'www.mintscan.io': 'CTK', + 'explorer.bitcoindiamond.org': 'BCD', + 'btgexplorer.com': 'BTG', + 'bts.ai': 'BTS', + 'explorer.celo.org': 'CELO', + 'explorer.nervos.org': 'CKB', + 'cerebro.cortexlabs.ai': 'CTXC', + 'chainz.cryptoid.info': 'VIA', + 'explorer.dcrdata.org': 'DCR', + 'digiexplorer.info': 'DGB', + 'dock.subscan.io': 'DOCK', + 'dogechain.info': 'DOGE', + 'explorer.elrond.com': 'EGLD', + 'blockscout.com': 'ETC', + 'explore-fetchhub.fetch.ai': 'FET', + 'filfox.info': 'FIL', + 'fio.bloks.io': 'FIO', + 'explorer.firo.org': 'FIRO', + 'neoscan.io': 'NEO', + 'ftmscan.com': 'FTM', + 'explorer.gochain.io': 'GO', + 'block.gxb.io': 'GXS', + 'hash-hash.info': 'HBAR', + 'www.hiveblockexplorer.com': 'HIVE', + 'explorer.helium.com': 'HNT', + 'tracker.icon.foundation': 'ICX', + 'www.iostabc.com': 'IOST', + 'explorer.iota.org': 'IOTA', + 'iotexscan.io': 'IOTX', + 'irishub.iobscan.io': 'IRIS', + 'kava.mintscan.io': 'KAVA', + 'scope.klaytn.com': 'KLAY', + 'kmdexplorer.io': 'KMD', + 'kusama.subscan.io': 'KSM', + 'explorer.lto.network': 'LTO', + 'polygonscan.com': 'POLYGON', + 'explorer.ont.io': 'ONT', + 'minaexplorer.com': 'MINA', + 'nanolooker.com': 'NANO', + 'explorer.nebulas.io': 'NAS', + 'explorer.nbs.plus': 'NBS', + 'explorer.nebl.io': 'NEBL', + 'nulscan.io': 'NULS', + 'nxscan.com': 'NXS', + 'explorer.harmony.one': 'ONE', + 'explorer.poa.network': 'POA', + 'qtum.info': 'QTUM', + 'explorer.rsk.co': 'RSK', + 'www.oasisscan.com': 'ROSE', + 'ravencoin.network': 'RVN', + 'sc.tokenview.com': 'SC', + 'secretnodes.com': 'SCRT', + 'explorer.skycoin.com': 'SKY', + 'steemscan.com': 'STEEM', + 'explorer.stacks.co': 'STX', + 'www.thetascan.io': 'THETA', + 'scan.tomochain.com': 'TOMO', + 'explore.vechain.org': 'VET', + 'explorer.vite.net': 'VITE', + 'www.wanscan.org': 'WAN', + 'wavesexplorer.com': 'WAVES', + 'wax.eosx.io': 'WAXP', + 'waltonchain.pro': 'WTC', + 'chain.nem.ninja': 'XEM', + 'verge-blockchain.info': 'XVG', + 'explorer.yoyow.org': 'YOYOW', + 'explorer.zcha.in': 'ZEC', + 'explorer.zensystem.io': 'ZEN', + }, + 'impliedNetworks': { + 'ETH': {'ERC20': 'ETH'}, + 'TRX': {'TRC20': 'TRX'}, + }, + 'legalMoney': { + 'MXN': True, + 'UGX': True, + 'SEK': True, + 'CHF': True, + 'VND': True, + 'AED': True, + 'DKK': True, + 'KZT': True, + 'HUF': True, + 'PEN': True, + 'PHP': True, + 'USD': True, + 'TRY': True, + 'EUR': True, + 'NGN': True, + 'PLN': True, + 'BRL': True, + 'ZAR': True, + 'KES': True, + 'ARS': True, + 'RUB': True, + 'AUD': True, + 'NOK': True, + 'CZK': True, + 'GBP': True, + 'UAH': True, + 'GHS': True, + 'HKD': True, + 'CAD': True, + 'INR': True, + 'JPY': True, + 'NZD': True, + }, + }, + # https://binance-docs.github.io/apidocs/spot/en/#error-codes-2 + 'exceptions': { + 'exact': { + 'System is under maintenance.': OnMaintenance, # {"code":1,"msg":"System is under maintenance."} + 'System abnormality': ExchangeError, # {"code":-1000,"msg":"System abnormality"} + 'You are not authorized to execute self request.': PermissionDenied, # {"msg":"You are not authorized to execute self request."} + 'API key does not exist': AuthenticationError, + 'Order would trigger immediately.': OrderImmediatelyFillable, + 'Stop price would trigger immediately.': OrderImmediatelyFillable, # {"code":-2010,"msg":"Stop price would trigger immediately."} + 'Order would immediately match and take.': OrderImmediatelyFillable, # {"code":-2010,"msg":"Order would immediately match and take."} + 'Account has insufficient balance for requested action.': InsufficientFunds, + 'Rest API trading is not enabled.': ExchangeNotAvailable, + "You don't have permission.": PermissionDenied, # {"msg":"You don't have permission.","success":false} + 'Market is closed.': ExchangeNotAvailable, # {"code":-1013,"msg":"Market is closed."} + 'Too many requests. Please try again later.': DDoSProtection, # {"msg":"Too many requests. Please try again later.","success":false} + 'This action disabled is on self account.': AccountSuspended, # {"code":-2010,"msg":"This action disabled is on self account."} + '-1000': ExchangeNotAvailable, # {"code":-1000,"msg":"An unknown error occured while processing the request."} + '-1001': ExchangeNotAvailable, # {"code":-1001,"msg":"'Internal error; unable to process your request. Please try again.'"} + '-1002': AuthenticationError, # {"code":-1002,"msg":"'You are not authorized to execute self request.'"} + '-1003': RateLimitExceeded, # {"code":-1003,"msg":"Too much request weight used, current limit is 1200 request weight per 1 MINUTE. Please use the websocket for live updates to avoid polling the API."} + '-1004': DDoSProtection, # {"code":-1004,"msg":"Server is busy, please wait and try again"} + '-1005': PermissionDenied, # {"code":-1005,"msg":"No such IP has been white listed"} + '-1006': BadResponse, # {"code":-1006,"msg":"An unexpected response was received from the message bus. Execution status unknown."} + '-1007': RequestTimeout, # {"code":-1007,"msg":"Timeout waiting for response from backend server. Send status unknown; execution status unknown."} + '-1010': BadResponse, # {"code":-1010,"msg":"ERROR_MSG_RECEIVED."} + '-1011': PermissionDenied, # {"code":-1011,"msg":"This IP cannot access self route."} + '-1013': InvalidOrder, # {"code":-1013,"msg":"createOrder -> 'invalid quantity'/'invalid price'/MIN_NOTIONAL"} + '-1014': InvalidOrder, # {"code":-1014,"msg":"Unsupported order combination."} + '-1015': RateLimitExceeded, # {"code":-1015,"msg":"'Too many new orders; current limit is %s orders per %s.'"} + '-1016': ExchangeNotAvailable, # {"code":-1016,"msg":"'This service is no longer available.',"} + '-1020': BadRequest, # {"code":-1020,"msg":"'This operation is not supported.'"} + '-1021': InvalidNonce, # {"code":-1021,"msg":"'your time is ahead of server'"} + '-1022': AuthenticationError, # {"code":-1022,"msg":"Signature for self request is not valid."} + '-1023': BadRequest, # {"code":-1023,"msg":"Start time is greater than end time."} + '-1099': AuthenticationError, # {"code":-1099,"msg":"Not found, authenticated, or authorized"} + '-1100': BadRequest, # {"code":-1100,"msg":"createOrder(symbol, 1, asdf) -> 'Illegal characters found in parameter 'price'"} + '-1101': BadRequest, # {"code":-1101,"msg":"Too many parameters; expected %s and received %s."} + '-1102': BadRequest, # {"code":-1102,"msg":"Param %s or %s must be sent, but both were empty"} + '-1103': BadRequest, # {"code":-1103,"msg":"An unknown parameter was sent."} + '-1104': BadRequest, # {"code":-1104,"msg":"Not all sent parameters were read, read 8 parameters but was sent 9"} + '-1105': BadRequest, # {"code":-1105,"msg":"Parameter %s was empty."} + '-1106': BadRequest, # {"code":-1106,"msg":"Parameter %s sent when not required."} + '-1108': BadRequest, # {"code":-1108,"msg":"Invalid asset."} + '-1109': AuthenticationError, # {"code":-1109,"msg":"Invalid account."} + '-1110': BadRequest, # {"code":-1110,"msg":"Invalid symbolType."} + '-1111': BadRequest, # {"code":-1111,"msg":"Precision is over the maximum defined for self asset."} + '-1112': InvalidOrder, # {"code":-1112,"msg":"No orders on book for symbol."} + '-1113': BadRequest, # {"code":-1113,"msg":"Withdrawal amount must be negative."} + '-1114': BadRequest, # {"code":-1114,"msg":"TimeInForce parameter sent when not required."} + '-1115': BadRequest, # {"code":-1115,"msg":"Invalid timeInForce."} + '-1116': BadRequest, # {"code":-1116,"msg":"Invalid orderType."} + '-1117': BadRequest, # {"code":-1117,"msg":"Invalid side."} + '-1118': BadRequest, # {"code":-1118,"msg":"New client order ID was empty."} + '-1119': BadRequest, # {"code":-1119,"msg":"Original client order ID was empty."} + '-1120': BadRequest, # {"code":-1120,"msg":"Invalid interval."} + '-1121': BadSymbol, # {"code":-1121,"msg":"Invalid symbol."} + '-1125': AuthenticationError, # {"code":-1125,"msg":"This listenKey does not exist."} + '-1127': BadRequest, # {"code":-1127,"msg":"More than %s hours between startTime and endTime."} + '-1128': BadRequest, # {"code":-1128,"msg":"{"code":-1128,"msg":"Combination of optional parameters invalid."}"} + '-1130': BadRequest, # {"code":-1130,"msg":"Data sent for paramter %s is not valid."} + '-1131': BadRequest, # {"code":-1131,"msg":"recvWindow must be less than 60000"} + '-1136': BadRequest, # {"code":-1136,"msg":"Invalid newOrderRespType"} + '-2008': AuthenticationError, # {"code":-2008,"msg":"Invalid Api-Key ID."} + '-2010': ExchangeError, # {"code":-2010,"msg":"generic error code for createOrder -> 'Account has insufficient balance for requested action.', {"code":-2010,"msg":"Rest API trading is not enabled."}, etc..."} + '-2011': OrderNotFound, # {"code":-2011,"msg":"cancelOrder(1, 'BTC/USDT') -> 'UNKNOWN_ORDER'"} + '-2013': OrderNotFound, # {"code":-2013,"msg":"fetchOrder(1, 'BTC/USDT') -> 'Order does not exist'"} + '-2014': AuthenticationError, # {"code":-2014,"msg":"API-key format invalid."} + '-2015': AuthenticationError, # {"code":-2015,"msg":"Invalid API-key, IP, or permissions for action."} + '-2016': BadRequest, # {"code":-2016,"msg":"No trading window could be found for the symbol. Try ticker/24hrs instead."} + '-2018': InsufficientFunds, # {"code":-2018,"msg":"Balance is insufficient"} + '-2019': InsufficientFunds, # {"code":-2019,"msg":"Margin is insufficient."} + '-2020': OrderNotFillable, # {"code":-2020,"msg":"Unable to fill."} + '-2021': OrderImmediatelyFillable, # {"code":-2021,"msg":"Order would immediately trigger."} + '-2022': InvalidOrder, # {"code":-2022,"msg":"ReduceOnly Order is rejected."} + '-2023': InsufficientFunds, # {"code":-2023,"msg":"User in liquidation mode now."} + '-2024': InsufficientFunds, # {"code":-2024,"msg":"Position is not sufficient."} + '-2025': InvalidOrder, # {"code":-2025,"msg":"Reach max open order limit."} + '-2026': InvalidOrder, # {"code":-2026,"msg":"This OrderType is not supported when reduceOnly."} + '-2027': InvalidOrder, # {"code":-2027,"msg":"Exceeded the maximum allowable position at current leverage."} + '-2028': InsufficientFunds, # {"code":-2028,"msg":"Leverage is smaller than permitted: insufficient margin balance"} + '-3000': ExchangeError, # {"code":-3000,"msg":"Internal server error."} + '-3001': AuthenticationError, # {"code":-3001,"msg":"Please enable 2FA first."} + '-3002': BadSymbol, # {"code":-3002,"msg":"We don't have self asset."} + '-3003': BadRequest, # {"code":-3003,"msg":"Margin account does not exist."} + '-3004': ExchangeError, # {"code":-3004,"msg":"Trade not allowed."} + '-3005': InsufficientFunds, # {"code":-3005,"msg":"Transferring out not allowed. Transfer out amount exceeds max amount."} + '-3006': InsufficientFunds, # {"code":-3006,"msg":"Your borrow amount has exceed maximum borrow amount."} + '-3007': ExchangeError, # {"code":-3007,"msg":"You have pending transaction, please try again later.."} + '-3008': InsufficientFunds, # {"code":-3008,"msg":"Borrow not allowed. Your borrow amount has exceed maximum borrow amount."} + '-3009': BadRequest, # {"code":-3009,"msg":"This asset are not allowed to transfer into margin account currently."} + '-3010': ExchangeError, # {"code":-3010,"msg":"Repay not allowed. Repay amount exceeds borrow amount."} + '-3011': BadRequest, # {"code":-3011,"msg":"Your input date is invalid."} + '-3012': ExchangeError, # {"code":-3012,"msg":"Borrow is banned for self asset."} + '-3013': BadRequest, # {"code":-3013,"msg":"Borrow amount less than minimum borrow amount."} + '-3014': AccountSuspended, # {"code":-3014,"msg":"Borrow is banned for self account."} + '-3015': ExchangeError, # {"code":-3015,"msg":"Repay amount exceeds borrow amount."} + '-3016': BadRequest, # {"code":-3016,"msg":"Repay amount less than minimum repay amount."} + '-3017': ExchangeError, # {"code":-3017,"msg":"This asset are not allowed to transfer into margin account currently."} + '-3018': AccountSuspended, # {"code":-3018,"msg":"Transferring in has been banned for self account."} + '-3019': AccountSuspended, # {"code":-3019,"msg":"Transferring out has been banned for self account."} + '-3020': InsufficientFunds, # {"code":-3020,"msg":"Transfer out amount exceeds max amount."} + '-3021': BadRequest, # {"code":-3021,"msg":"Margin account are not allowed to trade self trading pair."} + '-3022': AccountSuspended, # {"code":-3022,"msg":"You account's trading is banned."} + '-3023': BadRequest, # {"code":-3023,"msg":"You can't transfer out/place order under current margin level."} + '-3024': ExchangeError, # {"code":-3024,"msg":"The unpaid debt is too small after self repayment."} + '-3025': BadRequest, # {"code":-3025,"msg":"Your input date is invalid."} + '-3026': BadRequest, # {"code":-3026,"msg":"Your input param is invalid."} + '-3027': BadSymbol, # {"code":-3027,"msg":"Not a valid margin asset."} + '-3028': BadSymbol, # {"code":-3028,"msg":"Not a valid margin pair."} + '-3029': ExchangeError, # {"code":-3029,"msg":"Transfer failed."} + '-3036': AccountSuspended, # {"code":-3036,"msg":"This account is not allowed to repay."} + '-3037': ExchangeError, # {"code":-3037,"msg":"PNL is clearing. Wait a second."} + '-3038': BadRequest, # {"code":-3038,"msg":"Listen key not found."} + '-3041': InsufficientFunds, # {"code":-3041,"msg":"Balance is not enough"} + '-3042': BadRequest, # {"code":-3042,"msg":"PriceIndex not available for self margin pair."} + '-3043': BadRequest, # {"code":-3043,"msg":"Transferring in not allowed."} + '-3044': DDoSProtection, # {"code":-3044,"msg":"System busy."} + '-3045': ExchangeError, # {"code":-3045,"msg":"The system doesn't have enough asset now."} + '-3999': ExchangeError, # {"code":-3999,"msg":"This function is only available for invited users."} + '-4001': BadRequest, # {"code":-4001 ,"msg":"Invalid operation."} + '-4002': BadRequest, # {"code":-4002 ,"msg":"Invalid get."} + '-4003': BadRequest, # {"code":-4003 ,"msg":"Your input email is invalid."} + '-4004': AuthenticationError, # {"code":-4004,"msg":"You don't login or auth."} + '-4005': RateLimitExceeded, # {"code":-4005 ,"msg":"Too many new requests."} + '-4006': BadRequest, # {"code":-4006 ,"msg":"Support main account only."} + '-4007': BadRequest, # {"code":-4007 ,"msg":"Address validation is not passed."} + '-4008': BadRequest, # {"code":-4008 ,"msg":"Address tag validation is not passed."} + '-4010': BadRequest, # {"code":-4010 ,"msg":"White list mail has been confirmed."} # [TODO] possible bug: it should probably be "has not been confirmed" + '-4011': BadRequest, # {"code":-4011 ,"msg":"White list mail is invalid."} + '-4012': BadRequest, # {"code":-4012 ,"msg":"White list is not opened."} + '-4013': AuthenticationError, # {"code":-4013 ,"msg":"2FA is not opened."} + '-4014': PermissionDenied, # {"code":-4014 ,"msg":"Withdraw is not allowed within 2 min login."} + '-4015': ExchangeError, # {"code":-4015 ,"msg":"Withdraw is limited."} + '-4016': PermissionDenied, # {"code":-4016 ,"msg":"Within 24 hours after password modification, withdrawal is prohibited."} + '-4017': PermissionDenied, # {"code":-4017 ,"msg":"Within 24 hours after the release of 2FA, withdrawal is prohibited."} + '-4018': BadSymbol, # {"code":-4018,"msg":"We don't have self asset."} + '-4019': BadSymbol, # {"code":-4019,"msg":"Current asset is not open for withdrawal."} + '-4021': BadRequest, # {"code":-4021,"msg":"Asset withdrawal must be an %s multiple of %s."} + '-4022': BadRequest, # {"code":-4022,"msg":"Not less than the minimum pick-up quantity %s."} + '-4023': ExchangeError, # {"code":-4023,"msg":"Within 24 hours, the withdrawal exceeds the maximum amount."} + '-4024': InsufficientFunds, # {"code":-4024,"msg":"You don't have self asset."} + '-4025': InsufficientFunds, # {"code":-4025,"msg":"The number of hold asset is less than zero."} + '-4026': InsufficientFunds, # {"code":-4026,"msg":"You have insufficient balance."} + '-4027': ExchangeError, # {"code":-4027,"msg":"Failed to obtain tranId."} + '-4028': BadRequest, # {"code":-4028,"msg":"The amount of withdrawal must be greater than the Commission."} + '-4029': BadRequest, # {"code":-4029,"msg":"The withdrawal record does not exist."} + '-4030': ExchangeError, # {"code":-4030,"msg":"Confirmation of successful asset withdrawal. [TODO] possible bug in docs"} + '-4031': ExchangeError, # {"code":-4031,"msg":"Cancellation failed."} + '-4032': ExchangeError, # {"code":-4032,"msg":"Withdraw verification exception."} + '-4033': BadRequest, # {"code":-4033,"msg":"Illegal address."} + '-4034': ExchangeError, # {"code":-4034,"msg":"The address is suspected of fake."} + '-4035': PermissionDenied, # {"code":-4035,"msg":"This address is not on the whitelist. Please join and try again."} + '-4036': BadRequest, # {"code":-4036,"msg":"The new address needs to be withdrawn in {0} hours."} + '-4037': ExchangeError, # {"code":-4037,"msg":"Re-sending Mail failed."} + '-4038': ExchangeError, # {"code":-4038,"msg":"Please try again in 5 minutes."} + '-4039': BadRequest, # {"code":-4039,"msg":"The user does not exist."} + '-4040': BadRequest, # {"code":-4040,"msg":"This address not charged."} + '-4041': ExchangeError, # {"code":-4041,"msg":"Please try again in one minute."} + '-4042': ExchangeError, # {"code":-4042,"msg":"This asset cannot get deposit address again."} + '-4043': BadRequest, # {"code":-4043,"msg":"More than 100 recharge addresses were used in 24 hours."} + '-4044': BadRequest, # {"code":-4044,"msg":"This is a blacklist country."} + '-4045': ExchangeError, # {"code":-4045,"msg":"Failure to acquire assets."} + '-4046': AuthenticationError, # {"code":-4046,"msg":"Agreement not confirmed."} + '-4047': BadRequest, # {"code":-4047,"msg":"Time interval must be within 0-90 days"} + '-5001': BadRequest, # {"code":-5001,"msg":"Don't allow transfer to micro assets."} + '-5002': InsufficientFunds, # {"code":-5002,"msg":"You have insufficient balance."} + '-5003': InsufficientFunds, # {"code":-5003,"msg":"You don't have self asset."} + '-5004': BadRequest, # {"code":-5004,"msg":"The residual balances of %s have exceeded 0.001BTC, Please re-choose."} + '-5005': InsufficientFunds, # {"code":-5005,"msg":"The residual balances of %s is too low, Please re-choose."} + '-5006': BadRequest, # {"code":-5006,"msg":"Only transfer once in 24 hours."} + '-5007': BadRequest, # {"code":-5007,"msg":"Quantity must be greater than zero."} + '-5008': InsufficientFunds, # {"code":-5008,"msg":"Insufficient amount of returnable assets."} + '-5009': BadRequest, # {"code":-5009,"msg":"Product does not exist."} + '-5010': ExchangeError, # {"code":-5010,"msg":"Asset transfer fail."} + '-5011': BadRequest, # {"code":-5011,"msg":"future account not exists."} + '-5012': ExchangeError, # {"code":-5012,"msg":"Asset transfer is in pending."} + '-5013': InsufficientFunds, # {"code":-5013,"msg":"Asset transfer failed: insufficient balance""} # undocumented + '-5021': BadRequest, # {"code":-5021,"msg":"This parent sub have no relation"} + '-6001': BadRequest, # {"code":-6001,"msg":"Daily product not exists."} + '-6003': BadRequest, # {"code":-6003,"msg":"Product not exist or you don't have permission"} + '-6004': ExchangeError, # {"code":-6004,"msg":"Product not in purchase status"} + '-6005': InvalidOrder, # {"code":-6005,"msg":"Smaller than min purchase limit"} + '-6006': BadRequest, # {"code":-6006,"msg":"Redeem amount error"} + '-6007': BadRequest, # {"code":-6007,"msg":"Not in redeem time"} + '-6008': BadRequest, # {"code":-6008,"msg":"Product not in redeem status"} + '-6009': RateLimitExceeded, # {"code":-6009,"msg":"Request frequency too high"} + '-6011': BadRequest, # {"code":-6011,"msg":"Exceeding the maximum num allowed to purchase per user"} + '-6012': InsufficientFunds, # {"code":-6012,"msg":"Balance not enough"} + '-6013': ExchangeError, # {"code":-6013,"msg":"Purchasing failed"} + '-6014': BadRequest, # {"code":-6014,"msg":"Exceed up-limit allowed to purchased"} + '-6015': BadRequest, # {"code":-6015,"msg":"Empty request body"} + '-6016': BadRequest, # {"code":-6016,"msg":"Parameter err"} + '-6017': BadRequest, # {"code":-6017,"msg":"Not in whitelist"} + '-6018': BadRequest, # {"code":-6018,"msg":"Asset not enough"} + '-6019': AuthenticationError, # {"code":-6019,"msg":"Need confirm"} + '-6020': BadRequest, # {"code":-6020,"msg":"Project not exists"} + '-7001': BadRequest, # {"code":-7001,"msg":"Date range is not supported."} + '-7002': BadRequest, # {"code":-7002,"msg":"Data request type is not supported."} + '-9000': InsufficientFunds, # {"code":-9000,"msg":"user have no avaliable amount"}" + '-10017': BadRequest, # {"code":-10017,"msg":"Repay amount should not be larger than liability."} + '-11008': InsufficientFunds, # {"code":-11008,"msg":"Exceeding the account's maximum borrowable limit."} # undocumented + '-12014': RateLimitExceeded, # {"code":-12014,"msg":"More than 1 request in 3 seconds"} + '-13000': BadRequest, # {"code":-13000,"msg":"Redeption of the token is forbiden now"} + '-13001': BadRequest, # {"code":-13001,"msg":"Exceeds individual 24h redemption limit of the token"} + '-13002': BadRequest, # {"code":-13002,"msg":"Exceeds total 24h redemption limit of the token"} + '-13003': BadRequest, # {"code":-13003,"msg":"Subscription of the token is forbiden now"} + '-13004': BadRequest, # {"code":-13004,"msg":"Exceeds individual 24h subscription limit of the token"} + '-13005': BadRequest, # {"code":-13005,"msg":"Exceeds total 24h subscription limit of the token"} + '-13006': InvalidOrder, # {"code":-13006,"msg":"Subscription amount is too small"} + '-13007': AuthenticationError, # {"code":-13007,"msg":"The Agreement is not signed"} + '-21001': BadRequest, # {"code":-21001,"msg":"USER_IS_NOT_UNIACCOUNT"} + '-21002': BadRequest, # {"code":-21002,"msg":"UNI_ACCOUNT_CANT_TRANSFER_FUTURE"} + '-21003': BadRequest, # {"code":-21003,"msg":"NET_ASSET_MUST_LTE_RATIO"} + '100001003': BadRequest, # {"code":100001003,"msg":"Verification failed"} # undocumented + '2202': InsufficientFunds, # {"code":2202,"msg":"Insufficient balance","data":{"code":-2010,"msg":"Account has insufficient balance for requested action."},"timestamp":1662733681161} + '3210': InvalidOrder, # {"code":3210,"msg":"The total volume is too low","data":{"code":-1013,"msg":"Filter failure: MIN_NOTIONAL"},"timestamp":1662734704462} + '3203': InvalidOrder, # {"code":3203,"msg":"Incorrect Order Quantity","timestamp":1662734809758} + '3211': InvalidOrder, # {"code":3211,"msg":"The total volume must be greater than 10","timestamp":1662739358179} + '3207': InvalidOrder, # {"code":3207,"msg":"The price cannot be lower than 12.18","timestamp":1662739502856} + '3218': OrderNotFound, # {"code":3218,"msg":"Order does not exist","timestamp":1662739749275} + }, + 'broad': { + 'has no operation privilege': PermissionDenied, + 'MAX_POSITION': InvalidOrder, # {"code":-2010,"msg":"Filter failure: MAX_POSITION"} + }, + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerDirection': False, + 'triggerPriceType': None, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': True, + 'selfTradePrevention': True, # todo + 'iceberg': True, # todo + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 100000, # todo + 'untilDays': 100000, # todo + 'symbolRequired': True, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 1000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 100000, + 'untilDays': 100000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 100000, # todo + 'daysBackCanceled': 1, # todo + 'untilDays': 100000, # todo + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOHLCV': { + 'limit': 1000, + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + }) + + def nonce(self): + return self.milliseconds() - self.options['timeDifference'] + + async def fetch_time(self, params={}) -> Int: + """ + + https://www.tokocrypto.com/apidocs/#check-server-time + + fetches the current integer timestamp in milliseconds from the exchange server + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = await self.publicGetOpenV1CommonTime(params) + # + # { + # "code": 0, + # "msg": "Success", + # "data": null, + # "timestamp": 1737378074159 + # } + # + return self.safe_integer(response, 'timestamp') + + async def fetch_markets(self, params={}) -> List[Market]: + """ + + https://www.tokocrypto.com/apidocs/#get-all-supported-trading-symbol + + retrieves data on all markets for tokocrypto + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = await self.publicGetOpenV1CommonSymbols(params) + # + # { + # "code":0, + # "msg":"Success", + # "data":{ + # "list":[ + # { + # "type":1, + # "symbol":"1INCH_BTC", + # "baseAsset":"1INCH", + # "basePrecision":8, + # "quoteAsset":"BTC", + # "quotePrecision":8, + # "filters":[ + # {"filterType":"PRICE_FILTER","minPrice":"0.00000001","maxPrice":"1000.00000000","tickSize":"0.00000001","applyToMarket":false}, + # {"filterType":"PERCENT_PRICE","multiplierUp":5,"multiplierDown":0.2,"avgPriceMins":"5","applyToMarket":false}, + # {"filterType":"LOT_SIZE","minQty":"0.10000000","maxQty":"90000000.00000000","stepSize":"0.10000000","applyToMarket":false}, + # {"filterType":"MIN_NOTIONAL","avgPriceMins":"5","minNotional":"0.00010000","applyToMarket":true}, + # {"filterType":"ICEBERG_PARTS","applyToMarket":false,"limit":"10"}, + # {"filterType":"MARKET_LOT_SIZE","minQty":"0.00000000","maxQty":"79460.14117231","stepSize":"0.00000000","applyToMarket":false}, + # {"filterType":"TRAILING_DELTA","applyToMarket":false}, + # {"filterType":"MAX_NUM_ORDERS","applyToMarket":false}, + # {"filterType":"MAX_NUM_ALGO_ORDERS","applyToMarket":false,"maxNumAlgoOrders":"5"} + # ], + # "orderTypes":["LIMIT","LIMIT_MAKER","MARKET","STOP_LOSS_LIMIT","TAKE_PROFIT_LIMIT"], + # "icebergEnable":1, + # "ocoEnable":1, + # "spotTradingEnable":1, + # "marginTradingEnable":1, + # "permissions":["SPOT","MARGIN"] + # }, + # ] + # }, + # "timestamp":1659492212507 + # } + # + if self.options['adjustForTimeDifference']: + await self.load_time_difference() + data = self.safe_value(response, 'data', {}) + list = self.safe_value(data, 'list', []) + result = [] + for i in range(0, len(list)): + market = list[i] + baseId = self.safe_string(market, 'baseAsset') + quoteId = self.safe_string(market, 'quoteAsset') + id = self.safe_string(market, 'symbol') + lowercaseId = self.safe_string_lower(market, 'symbol') + settleId = self.safe_string(market, 'marginAsset') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + settle = self.safe_currency_code(settleId) + symbol = base + '/' + quote + filters = self.safe_value(market, 'filters', []) + filtersByType = self.index_by(filters, 'filterType') + status = self.safe_string(market, 'spotTradingEnable') + active = (status == '1') + permissions = self.safe_value(market, 'permissions', []) + for j in range(0, len(permissions)): + if permissions[j] == 'TRD_GRP_003': + active = False + break + isMarginTradingAllowed = self.safe_bool(market, 'isMarginTradingAllowed', False) + entry: dict = { + 'id': id, + 'lowercaseId': lowercaseId, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': 'spot', + 'spot': True, + 'margin': isMarginTradingAllowed, + 'swap': False, + 'future': False, + 'delivery': False, + 'option': False, + 'active': active, + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(self.parse_precision(self.safe_string(market, 'quantityPrecision'))), + 'price': self.parse_number(self.parse_precision(self.safe_string(market, 'pricePrecision'))), + 'base': self.parse_number(self.parse_precision(self.safe_string(market, 'baseAssetPrecision'))), + 'quote': self.parse_number(self.parse_precision(self.safe_string(market, 'quotePrecision'))), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': None, + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + 'info': market, + } + if 'PRICE_FILTER' in filtersByType: + filter = self.safe_value(filtersByType, 'PRICE_FILTER', {}) + entry['precision']['price'] = self.safe_number(filter, 'tickSize') + # PRICE_FILTER reports zero values for maxPrice + # since they updated filter types in November 2018 + # https://github.com/ccxt/ccxt/issues/4286 + # therefore limits['price']['max'] doesn't have any meaningful value except None + entry['limits']['price'] = { + 'min': self.safe_number(filter, 'minPrice'), + 'max': self.safe_number(filter, 'maxPrice'), + } + entry['precision']['price'] = filter['tickSize'] + if 'LOT_SIZE' in filtersByType: + filter = self.safe_value(filtersByType, 'LOT_SIZE', {}) + entry['precision']['amount'] = self.safe_number(filter, 'stepSize') + entry['limits']['amount'] = { + 'min': self.safe_number(filter, 'minQty'), + 'max': self.safe_number(filter, 'maxQty'), + } + if 'MARKET_LOT_SIZE' in filtersByType: + filter = self.safe_value(filtersByType, 'MARKET_LOT_SIZE', {}) + entry['limits']['market'] = { + 'min': self.safe_number(filter, 'minQty'), + 'max': self.safe_number(filter, 'maxQty'), + } + if 'MIN_NOTIONAL' in filtersByType: + filter = self.safe_value(filtersByType, 'MIN_NOTIONAL', {}) + entry['limits']['cost']['min'] = self.safe_number_2(filter, 'minNotional', 'notional') + result.append(entry) + return result + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + + https://www.tokocrypto.com/apidocs/#order-book + + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = {} + if limit is not None: + request['limit'] = limit # default 100, max 5000, see https://github.com/binance/binance-spot-api-docs/blob/master/rest-api.md#order-book + response = None + if market['quote'] == 'USDT': + request['symbol'] = market['baseId'] + market['quoteId'] + response = await self.binanceGetDepth(self.extend(request, params)) + else: + request['symbol'] = market['id'] + response = await self.publicGetOpenV1MarketDepth(self.extend(request, params)) + # + # future + # + # { + # "lastUpdateId":333598053905, + # "E":1618631511986, + # "T":1618631511964, + # "bids":[ + # ["2493.56","20.189"], + # ["2493.54","1.000"], + # ["2493.51","0.005"] + # ], + # "asks":[ + # ["2493.57","0.877"], + # ["2493.62","0.063"], + # ["2493.71","12.054"], + # ] + # } + # type not 1 + # { + # "code":0, + # "msg":"Success", + # "data":{ + # "lastUpdateId":3204783, + # "bids":[], + # "asks": [] + # }, + # "timestamp":1692262634599 + # } + data = self.safe_value(response, 'data', response) + timestamp = self.safe_integer_2(response, 'T', 'timestamp') + orderbook = self.parse_order_book(data, symbol, timestamp) + orderbook['nonce'] = self.safe_integer(data, 'lastUpdateId') + return orderbook + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # aggregate trades + # https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md#compressedaggregate-trades-list + # + # { + # "a": 26129, # Aggregate tradeId + # "p": "0.01633102", # Price + # "q": "4.70443515", # Quantity + # "f": 27781, # First tradeId + # "l": 27781, # Last tradeId + # "T": 1498793709153, # Timestamp + # "m": True, # Was the buyer the maker? + # "M": True # Was the trade the best price match? + # } + # + # recent public trades and old public trades + # https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md#recent-trades-list + # https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md#old-trade-lookup-market_data + # + # { + # "id": 28457, + # "price": "4.00000100", + # "qty": "12.00000000", + # "time": 1499865549590, + # "isBuyerMaker": True, + # "isBestMatch": True + # } + # + # private trades + # https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md#account-trade-list-user_data + # + # { + # "symbol": "BNBBTC", + # "id": 28457, + # "orderId": 100234, + # "price": "4.00000100", + # "qty": "12.00000000", + # "commission": "10.10000000", + # "commissionAsset": "BNB", + # "time": 1499865549590, + # "isBuyer": True, + # "isMaker": False, + # "isBestMatch": True + # } + # + # futures trades + # https://binance-docs.github.io/apidocs/futures/en/#account-trade-list-user_data + # + # { + # "accountId": 20, + # "buyer": False, + # "commission": "-0.07819010", + # "commissionAsset": "USDT", + # "counterPartyId": 653, + # "id": 698759, + # "maker": False, + # "orderId": 25851813, + # "price": "7819.01", + # "qty": "0.002", + # "quoteQty": "0.01563", + # "realizedPnl": "-0.91539999", + # "side": "SELL", + # "symbol": "BTCUSDT", + # "time": 1569514978020 + # } + # { + # "symbol": "BTCUSDT", + # "id": 477128891, + # "orderId": 13809777875, + # "side": "SELL", + # "price": "38479.55", + # "qty": "0.001", + # "realizedPnl": "-0.00009534", + # "marginAsset": "USDT", + # "quoteQty": "38.47955", + # "commission": "-0.00076959", + # "commissionAsset": "USDT", + # "time": 1612733566708, + # "positionSide": "BOTH", + # "maker": True, + # "buyer": False + # } + # + # {respType: FULL} + # + # { + # "price": "4000.00000000", + # "qty": "1.00000000", + # "commission": "4.00000000", + # "commissionAsset": "USDT", + # "tradeId": "1234", + # } + # + timestamp = self.safe_integer_2(trade, 'T', 'time') + price = self.safe_string_2(trade, 'p', 'price') + amount = self.safe_string_2(trade, 'q', 'qty') + cost = self.safe_string_2(trade, 'quoteQty', 'baseQty') # inverse futures + marketId = self.safe_string(trade, 'symbol') + symbol = self.safe_symbol(marketId, market) + id = self.safe_string_2(trade, 't', 'a') + id = self.safe_string_2(trade, 'id', 'tradeId', id) + side: Str = None + orderId = self.safe_string(trade, 'orderId') + buyerMaker = self.safe_value_2(trade, 'm', 'isBuyerMaker') + takerOrMaker: Str = None + if buyerMaker is not None: + side = 'sell' if buyerMaker else 'buy' # self is reversed intentionally + takerOrMaker = 'taker' + elif 'side' in trade: + side = self.safe_string_lower(trade, 'side') + else: + if 'isBuyer' in trade: + side = 'buy' if trade['isBuyer'] else 'sell' # self is a True side + fee = None + if 'commission' in trade: + fee = { + 'cost': self.safe_string(trade, 'commission'), + 'currency': self.safe_currency_code(self.safe_string(trade, 'commissionAsset')), + } + if 'isMaker' in trade: + takerOrMaker = 'maker' if trade['isMaker'] else 'taker' + if 'maker' in trade: + takerOrMaker = 'maker' if trade['maker'] else 'taker' + return self.safe_trade({ + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'id': id, + 'order': orderId, + 'type': None, + 'side': side, + 'takerOrMaker': takerOrMaker, + 'price': price, + 'amount': amount, + 'cost': cost, + 'fee': fee, + }, market) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + + https://www.tokocrypto.com/apidocs/#recent-trades-list + https://www.tokocrypto.com/apidocs/#compressedaggregate-trades-list + + get the list of most recent trades for a particular symbol + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': self.get_market_id_by_type(market), + # 'fromId': 123, # ID to get aggregate trades from INCLUSIVE. + # 'startTime': 456, # Timestamp in ms to get aggregate trades from INCLUSIVE. + # 'endTime': 789, # Timestamp in ms to get aggregate trades until INCLUSIVE. + # 'limit': 500, # default = 500, maximum = 1000 + } + if market['quote'] != 'USDT': + if limit is not None: + request['limit'] = limit + responseInner = self.publicGetOpenV1MarketTrades(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "success", + # "data": { + # "list": [ + # { + # "id": 28457, + # "price": "4.00000100", + # "qty": "12.00000000", + # "time": 1499865549590, + # "isBuyerMaker": True, + # "isBestMatch": True + # } + # ] + # }, + # "timestamp": 1571921637091 + # } + # + data = self.safe_dict(responseInner, 'data', {}) + list = self.safe_list(data, 'list', []) + return self.parse_trades(list, market, since, limit) + if limit is not None: + request['limit'] = limit # default = 500, maximum = 1000 + defaultMethod = 'binanceGetTrades' + method = self.safe_string(self.options, 'fetchTradesMethod', defaultMethod) + response = None + if (method == 'binanceGetAggTrades') and (since is not None): + request['startTime'] = since + # https://github.com/ccxt/ccxt/issues/6400 + # https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md#compressedaggregate-trades-list + request['endTime'] = self.sum(since, 3600000) + response = await self.binanceGetAggTrades(self.extend(request, params)) + else: + response = await self.binanceGetTrades(self.extend(request, params)) + # + # Caveats: + # - default limit(500) applies only if no other parameters set, trades up + # to the maximum limit may be returned to satisfy other parameters + # - if both limit and time window is set and time window contains more + # trades than the limit then the last trades from the window are returned + # - 'tradeId' accepted and returned by self method is "aggregate" trade id + # which is different from actual trade id + # - setting both fromId and time window results in error + # + # aggregate trades + # + # [ + # { + # "a": 26129, # Aggregate tradeId + # "p": "0.01633102", # Price + # "q": "4.70443515", # Quantity + # "f": 27781, # First tradeId + # "l": 27781, # Last tradeId + # "T": 1498793709153, # Timestamp + # "m": True, # Was the buyer the maker? + # "M": True # Was the trade the best price match? + # } + # ] + # + # recent public trades and historical public trades + # + # [ + # { + # "id": 28457, + # "price": "4.00000100", + # "qty": "12.00000000", + # "time": 1499865549590, + # "isBuyerMaker": True, + # "isBestMatch": True + # } + # ] + # + return self.parse_trades(response, market, since, limit) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "symbol": "ETHBTC", + # "priceChange": "0.00068700", + # "priceChangePercent": "2.075", + # "weightedAvgPrice": "0.03342681", + # "prevClosePrice": "0.03310300", + # "lastPrice": "0.03378900", + # "lastQty": "0.07700000", + # "bidPrice": "0.03378900", + # "bidQty": "7.16800000", + # "askPrice": "0.03379000", + # "askQty": "24.00000000", + # "openPrice": "0.03310200", + # "highPrice": "0.03388900", + # "lowPrice": "0.03306900", + # "volume": "205478.41000000", + # "quoteVolume": "6868.48826294", + # "openTime": 1601469986932, + # "closeTime": 1601556386932, + # "firstId": 196098772, + # "lastId": 196186315, + # "count": 87544 + # } + # + # coinm + # { + # "baseVolume": "214549.95171161", + # "closeTime": "1621965286847", + # "count": "1283779", + # "firstId": "152560106", + # "highPrice": "39938.3", + # "lastId": "153843955", + # "lastPrice": "37993.4", + # "lastQty": "1", + # "lowPrice": "36457.2", + # "openPrice": "37783.4", + # "openTime": "1621878840000", + # "pair": "BTCUSD", + # "priceChange": "210.0", + # "priceChangePercent": "0.556", + # "symbol": "BTCUSD_PERP", + # "volume": "81990451", + # "weightedAvgPrice": "38215.08713747" + # } + # + timestamp = self.safe_integer(ticker, 'closeTime') + marketId = self.safe_string(ticker, 'symbol') + symbol = self.safe_symbol(marketId, market) + last = self.safe_string(ticker, 'lastPrice') + isCoinm = ('baseVolume' in ticker) + baseVolume = None + quoteVolume = None + if isCoinm: + baseVolume = self.safe_string(ticker, 'baseVolume') + quoteVolume = self.safe_string(ticker, 'volume') + else: + baseVolume = self.safe_string(ticker, 'volume') + quoteVolume = self.safe_string(ticker, 'quoteVolume') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'highPrice'), + 'low': self.safe_string(ticker, 'lowPrice'), + 'bid': self.safe_string(ticker, 'bidPrice'), + 'bidVolume': self.safe_string(ticker, 'bidQty'), + 'ask': self.safe_string(ticker, 'askPrice'), + 'askVolume': self.safe_string(ticker, 'askQty'), + 'vwap': self.safe_string(ticker, 'weightedAvgPrice'), + 'open': self.safe_string(ticker, 'openPrice'), + 'close': last, + 'last': last, + 'previousClose': self.safe_string(ticker, 'prevClosePrice'), # previous day close + 'change': self.safe_string(ticker, 'priceChange'), + 'percentage': self.safe_string(ticker, 'priceChangePercent'), + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'info': ticker, + }, market) + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + + https://binance-docs.github.io/apidocs/spot/en/#24hr-ticker-price-change-statistics + + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + response = await self.binanceGetTicker24hr(params) + return self.parse_tickers(response, symbols) + + def get_market_id_by_type(self, market): + if market['quote'] == 'USDT': + return market['baseId'] + market['quoteId'] + return market['id'] + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + + https://binance-docs.github.io/apidocs/spot/en/#24hr-ticker-price-change-statistics + + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['baseId'] + market['quoteId'], + } + response = await self.binanceGetTicker24hr(self.extend(request, params)) + if isinstance(response, list): + firstTicker = self.safe_dict(response, 0, {}) + return self.parse_ticker(firstTicker, market) + return self.parse_ticker(response, market) + + async def fetch_bids_asks(self, symbols: Strings = None, params={}): + """ + + https://binance-docs.github.io/apidocs/spot/en/#symbol-order-book-ticker + + fetches the bid and ask price and volume for multiple markets + :param str[]|None symbols: unified symbols of the markets to fetch the bids and asks for, all markets are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + response = await self.binanceGetTickerBookTicker(params) + return self.parse_tickers(response, symbols) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # when api method = publicGetKlines or fapiPublicGetKlines or dapiPublicGetKlines + # [ + # 1591478520000, # open time + # "0.02501300", # open + # "0.02501800", # high + # "0.02500000", # low + # "0.02500000", # close + # "22.19000000", # volume + # 1591478579999, # close time + # "0.55490906", # quote asset volume + # 40, # number of trades + # "10.92900000", # taker buy base asset volume + # "0.27336462", # taker buy quote asset volume + # "0" # ignore + # ] + # + # when api method = fapiPublicGetMarkPriceKlines or fapiPublicGetIndexPriceKlines + # [ + # [ + # 1591256460000, # Open time + # "9653.29201333", # Open + # "9654.56401333", # High + # "9653.07367333", # Low + # "9653.07367333", # Close(or latest price) + # "0", # Ignore + # 1591256519999, # Close time + # "0", # Ignore + # 60, # Number of bisic data + # "0", # Ignore + # "0", # Ignore + # "0" # Ignore + # ] + # ] + # + return [ + self.safe_integer(ohlcv, 0), + self.safe_number(ohlcv, 1), + self.safe_number(ohlcv, 2), + self.safe_number(ohlcv, 3), + self.safe_number(ohlcv, 4), + self.safe_number(ohlcv, 5), + ] + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + + https://binance-docs.github.io/apidocs/spot/en/#kline-candlestick-data + + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.price]: "mark" or "index" for mark price and index price candles + :param int [params.until]: timestamp in ms of the latest candle to fetch + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + # binance docs say that the default limit 500, max 1500 for futures, max 1000 for spot markets + # the reality is that the time range wider than 500 candles won't work right + defaultLimit = 500 + maxLimit = 1500 + price = self.safe_string(params, 'price') + until = self.safe_integer(params, 'until') + params = self.omit(params, ['price', 'until']) + limit = defaultLimit if (limit is None) else min(limit, maxLimit) + request: dict = { + 'interval': self.safe_string(self.timeframes, timeframe, timeframe), + 'limit': limit, + } + if price == 'index': + request['pair'] = market['id'] # Index price takes self argument instead of symbol + else: + request['symbol'] = self.get_market_id_by_type(market) + # duration = self.parse_timeframe(timeframe) + if since is not None: + request['startTime'] = since + if until is not None: + request['endTime'] = until + response = None + if market['quote'] == 'USDT': + response = await self.binanceGetKlines(self.extend(request, params)) + else: + response = await self.publicGetOpenV1MarketKlines(self.extend(request, params)) + # + # [ + # [1591478520000,"0.02501300","0.02501800","0.02500000","0.02500000","22.19000000",1591478579999,"0.55490906",40,"10.92900000","0.27336462","0"], + # [1591478580000,"0.02499600","0.02500900","0.02499400","0.02500300","21.34700000",1591478639999,"0.53370468",24,"7.53800000","0.18850725","0"], + # [1591478640000,"0.02500800","0.02501100","0.02500300","0.02500800","154.14200000",1591478699999,"3.85405839",97,"5.32300000","0.13312641","0"], + # ] + # + data = self.safe_list(response, 'data', response) + return self.parse_ohlcvs(data, market, timeframe, since, limit) + + async def fetch_balance(self, params={}) -> Balances: + """ + + https://www.tokocrypto.com/apidocs/#account-information-signed + + query for balance and get the amount of funds available for trading or funds locked in orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: 'future', 'delivery', 'savings', 'funding', or 'spot' + :param str [params.marginMode]: 'cross' or 'isolated', for margin trading, uses self.options.defaultMarginMode if not passed, defaults to None/None/None + :param str[]|None [params.symbols]: unified market symbols, only used in isolated margin mode + :returns dict: a `balance structure ` + """ + await self.load_markets() + defaultType = self.safe_string_2(self.options, 'fetchBalance', 'defaultType', 'spot') + type = self.safe_string(params, 'type', defaultType) + defaultMarginMode = self.safe_string_2(self.options, 'marginMode', 'defaultMarginMode') + marginMode = self.safe_string_lower(params, 'marginMode', defaultMarginMode) + request: dict = {} + response = await self.privateGetOpenV1AccountSpot(self.extend(request, params)) + # + # spot + # + # { + # "code":0, + # "msg":"Success", + # "data":{ + # "makerCommission":"0.00100000", + # "takerCommission":"0.00100000", + # "buyerCommission":"0.00000000", + # "sellerCommission":"0.00000000", + # "canTrade":1, + # "canWithdraw":1, + # "canDeposit":1, + # "status":1, + # "accountAssets":[ + # {"asset":"1INCH","free":"0","locked":"0"}, + # {"asset":"AAVE","free":"0","locked":"0"}, + # {"asset":"ACA","free":"0","locked":"0"} + # ], + # }, + # "timestamp":1659666786943 + # } + # + return self.parse_balance_custom(response, type, marginMode) + + def parse_balance_custom(self, response, type=None, marginMode=None): + timestamp = self.safe_integer(response, 'updateTime') + result: dict = { + 'info': response, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + data = self.safe_value(response, 'data', {}) + balances = self.safe_value(data, 'accountAssets', []) + for i in range(0, len(balances)): + balance = balances[i] + currencyId = self.safe_string(balance, 'asset') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(balance, 'free') + account['used'] = self.safe_string(balance, 'locked') + result[code] = account + return self.safe_balance(result) + + def parse_order_status(self, status: Str): + statuses: dict = { + '-2': 'open', + '0': 'open', # NEW + '1': 'open', # PARTIALLY_FILLED + '2': 'closed', # FILLED + '3': 'canceled', # CANCELED + '4': 'canceling', # PENDING_CANCEL(currently unused) + '5': 'rejected', # REJECTED + '6': 'expired', # EXPIRED + 'NEW': 'open', + 'PARTIALLY_FILLED': 'open', + 'FILLED': 'closed', + 'CANCELED': 'canceled', + 'PENDING_CANCEL': 'canceling', # currently unused + 'REJECTED': 'rejected', + 'EXPIRED': 'expired', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # spot + # + # { + # "symbol": "LTCBTC", + # "orderId": 1, + # "clientOrderId": "myOrder1", + # "price": "0.1", + # "origQty": "1.0", + # "executedQty": "0.0", + # "cummulativeQuoteQty": "0.0", + # "status": "NEW", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "side": "BUY", + # "stopPrice": "0.0", + # "icebergQty": "0.0", + # "time": 1499827319559, + # "updateTime": 1499827319559, + # "isWorking": True + # } + # createOrder + # { + # "orderId": "145265071", + # "bOrderListId": "0", + # "clientId": "49c09c3c2cd54419a59c05441f517b3c", + # "bOrderId": "35247529", + # "symbol": "USDT_BIDR", + # "symbolType": "1", + # "side": "0", + # "type": "1", + # "price": "11915", + # "origQty": "2", + # "origQuoteQty": "23830.00", + # "executedQty": "0.00000000", + # "executedPrice": "0", + # "executedQuoteQty": "0.00", + # "timeInForce": "1", + # "stopPrice": "0", + # "icebergQty": "0", + # "status": "0", + # "createTime": "1662711074372" + # } + # + # createOrder with {"newOrderRespType": "FULL"} + # + # { + # "symbol": "BTCUSDT", + # "orderId": 5403233939, + # "orderListId": -1, + # "clientOrderId": "x-R4BD3S825e669e75b6c14f69a2c43e", + # "transactTime": 1617151923742, + # "price": "0.00000000", + # "origQty": "0.00050000", + # "executedQty": "0.00050000", + # "cummulativeQuoteQty": "29.47081500", + # "status": "FILLED", + # "timeInForce": "GTC", + # "type": "MARKET", + # "side": "BUY", + # "fills": [ + # { + # "price": "58941.63000000", + # "qty": "0.00050000", + # "commission": "0.00007050", + # "commissionAsset": "BNB", + # "tradeId": 737466631 + # } + # ] + # } + # + # delivery + # + # { + # "orderId": "18742727411", + # "symbol": "ETHUSD_PERP", + # "pair": "ETHUSD", + # "status": "FILLED", + # "clientOrderId": "x-xcKtGhcu3e2d1503fdd543b3b02419", + # "price": "0", + # "avgPrice": "4522.14", + # "origQty": "1", + # "executedQty": "1", + # "cumBase": "0.00221134", + # "timeInForce": "GTC", + # "type": "MARKET", + # "reduceOnly": False, + # "closePosition": False, + # "side": "SELL", + # "positionSide": "BOTH", + # "stopPrice": "0", + # "workingType": "CONTRACT_PRICE", + # "priceProtect": False, + # "origType": "MARKET", + # "time": "1636061952660", + # "updateTime": "1636061952660" + # } + # + status = self.parse_order_status(self.safe_string(order, 'status')) + marketId = self.safe_string(order, 'symbol') + symbol = self.safe_symbol(marketId, market) + filled = self.safe_string(order, 'executedQty', '0') + timestamp = self.safe_integer(order, 'createTime') + average = self.safe_string(order, 'avgPrice') + price = self.safe_string_2(order, 'price', 'executedPrice') + amount = self.safe_string(order, 'origQty') + # - Spot/Margin market: cummulativeQuoteQty + # Note self is not the actual cost, since Binance futures uses leverage to calculate margins. + cost = self.safe_string_n(order, ['cummulativeQuoteQty', 'cumQuote', 'executedQuoteQty', 'cumBase']) + id = self.safe_string(order, 'orderId') + type = self.parse_order_type(self.safe_string_lower(order, 'type')) + side = self.safe_string_lower(order, 'side') + if side == '0': + side = 'buy' + elif side == '1': + side = 'sell' + fills = self.safe_value(order, 'fills', []) + clientOrderId = self.safe_string_2(order, 'clientOrderId', 'clientId') + timeInForce = self.safe_string(order, 'timeInForce') + if timeInForce == 'GTX': + # GTX means "Good Till Crossing" and is an equivalent way of saying Post Only + timeInForce = 'PO' + postOnly = (type == 'limit_maker') or (timeInForce == 'PO') + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'symbol': symbol, + 'type': type, + 'timeInForce': timeInForce, + 'postOnly': postOnly, + 'reduceOnly': self.safe_value(order, 'reduceOnly'), + 'side': side, + 'price': price, + 'triggerPrice': self.parse_number(self.omit_zero(self.safe_string(order, 'stopPrice'))), + 'amount': amount, + 'cost': cost, + 'average': average, + 'filled': filled, + 'remaining': None, + 'status': status, + 'fee': None, + 'trades': fills, + }, market) + + def parse_order_type(self, status): + statuses: dict = { + '2': 'market', + '1': 'limit', + '4': 'limit', + '7': 'limit', + } + return self.safe_string(statuses, status, status) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://www.tokocrypto.com/apidocs/#new-order--signed + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: the price at which a trigger order would be triggered + :param float [params.cost]: for spot market buy orders, the quote quantity that can be used alternative for the amount + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + clientOrderId = self.safe_string_2(params, 'clientOrderId', 'clientId') + postOnly = self.safe_bool(params, 'postOnly', False) + # only supported for spot/margin api + if postOnly: + type = 'LIMIT_MAKER' + params = self.omit(params, ['clientId', 'clientOrderId']) + initialUppercaseType = type.upper() + uppercaseType = initialUppercaseType + triggerPrice = self.safe_value_2(params, 'triggerPrice', 'stopPrice') + if triggerPrice is not None: + params = self.omit(params, ['triggerPrice', 'stopPrice']) + if uppercaseType == 'MARKET': + uppercaseType = 'STOP_LOSS' + elif uppercaseType == 'LIMIT': + uppercaseType = 'STOP_LOSS_LIMIT' + validOrderTypes = self.safe_value(market['info'], 'orderTypes') + if not self.in_array(uppercaseType, validOrderTypes): + if initialUppercaseType != uppercaseType: + raise InvalidOrder(self.id + ' triggerPrice parameter is not allowed for ' + symbol + ' ' + type + ' orders') + else: + raise InvalidOrder(self.id + ' ' + type + ' is not a valid order type for the ' + symbol + ' market') + reverseOrderTypeMapping: dict = { + 'LIMIT': 1, + 'MARKET': 2, + 'STOP_LOSS': 3, + 'STOP_LOSS_LIMIT': 4, + 'TAKE_PROFIT': 5, + 'TAKE_PROFIT_LIMIT': 6, + 'LIMIT_MAKER': 7, + } + request: dict = { + 'symbol': market['baseId'] + '_' + market['quoteId'], + 'type': self.safe_string(reverseOrderTypeMapping, uppercaseType), + } + if side == 'buy': + request['side'] = 0 + elif side == 'sell': + request['side'] = 1 + if clientOrderId is None: + broker = self.safe_value(self.options, 'broker') + if broker is not None: + brokerId = self.safe_string(broker, 'marketType') + if brokerId is not None: + request['clientId'] = brokerId + self.uuid22() + else: + request['clientId'] = clientOrderId + # additional required fields depending on the order type + priceIsRequired = False + triggerPriceIsRequired = False + quantityIsRequired = False + # + # spot/margin + # + # LIMIT timeInForce, quantity, price + # MARKET quantity or quoteOrderQty + # STOP_LOSS quantity, stopPrice + # STOP_LOSS_LIMIT timeInForce, quantity, price, stopPrice + # TAKE_PROFIT quantity, stopPrice + # TAKE_PROFIT_LIMIT timeInForce, quantity, price, stopPrice + # LIMIT_MAKER quantity, price + # + if uppercaseType == 'MARKET': + if side == 'buy': + precision = market['precision']['price'] + quoteAmount = None + createMarketBuyOrderRequiresPrice = True + createMarketBuyOrderRequiresPrice, params = self.handle_option_and_params(params, 'createOrder', 'createMarketBuyOrderRequiresPrice', True) + cost = self.safe_number_2(params, 'cost', 'quoteOrderQty') + params = self.omit(params, ['cost', 'quoteOrderQty']) + if cost is not None: + quoteAmount = cost + elif createMarketBuyOrderRequiresPrice: + if price is None: + raise InvalidOrder(self.id + ' createOrder() requires the price argument for market buy orders to calculate the total cost to spend(amount * price), alternatively set the createMarketBuyOrderRequiresPrice option or param to False and pass the cost to spend(quote quantity) in the amount argument') + else: + amountString = self.number_to_string(amount) + priceString = self.number_to_string(price) + quoteAmount = Precise.string_mul(amountString, priceString) + else: + quoteAmount = amount + request['quoteOrderQty'] = self.decimal_to_precision(quoteAmount, TRUNCATE, precision, self.precisionMode) + else: + quantityIsRequired = True + elif uppercaseType == 'LIMIT': + priceIsRequired = True + quantityIsRequired = True + elif (uppercaseType == 'STOP_LOSS') or (uppercaseType == 'TAKE_PROFIT'): + triggerPriceIsRequired = True + quantityIsRequired = True + if market['linear'] or market['inverse']: + priceIsRequired = True + elif (uppercaseType == 'STOP_LOSS_LIMIT') or (uppercaseType == 'TAKE_PROFIT_LIMIT'): + quantityIsRequired = True + triggerPriceIsRequired = True + priceIsRequired = True + elif uppercaseType == 'LIMIT_MAKER': + priceIsRequired = True + quantityIsRequired = True + if quantityIsRequired: + request['quantity'] = self.amount_to_precision(symbol, amount) + if priceIsRequired: + if price is None: + raise InvalidOrder(self.id + ' createOrder() requires a price argument for a ' + type + ' order') + request['price'] = self.price_to_precision(symbol, price) + if triggerPriceIsRequired: + if triggerPrice is None: + raise InvalidOrder(self.id + ' createOrder() requires a triggerPrice extra param for a ' + type + ' order') + else: + request['stopPrice'] = self.price_to_precision(symbol, triggerPrice) + response = await self.privatePostOpenV1Orders(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "Success", + # "data": { + # "orderId": 145264846, + # "bOrderListId": 0, + # "clientId": "4ee2ab5e55e74b358eaf98079c670d17", + # "bOrderId": 35247499, + # "symbol": "USDT_BIDR", + # "symbolType": 1, + # "side": 0, + # "type": 1, + # "price": "11915", + # "origQty": "2", + # "origQuoteQty": "23830.00", + # "executedQty": "0.00000000", + # "executedPrice": "0", + # "executedQuoteQty": "0.00", + # "timeInForce": 1, + # "stopPrice": 0, + # "icebergQty": "0", + # "status": 0, + # "createTime": 1662710994848 + # }, + # "timestamp": 1662710994975 + # } + # + rawOrder = self.safe_dict(response, 'data', {}) + return self.parse_order(rawOrder, market) + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + + https://www.tokocrypto.com/apidocs/#query-order-signed + + fetches information on an order made by the user + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + request: dict = { + 'orderId': id, + } + response = await self.privateGetOpenV1Orders(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "Success", + # "data": { + # "list": [{ + # "orderId": "145221985", + # "clientId": "201515331fd64d03aedbe687a38152e3", + # "bOrderId": "35239632", + # "bOrderListId": "0", + # "symbol": "USDT_BIDR", + # "symbolType": 1, + # "side": 0, + # "type": 1, + # "price": "11907", + # "origQty": "2", + # "origQuoteQty": "23814", + # "executedQty": "0", + # "executedPrice": "0", + # "executedQuoteQty": "0", + # "timeInForce": 1, + # "stopPrice": "0", + # "icebergQty": "0", + # "status": 0, + # "createTime": 1662699360000 + # }] + # }, + # "timestamp": 1662710056523 + # } + # + data = self.safe_value(response, 'data', {}) + list = self.safe_value(data, 'list', []) + rawOrder = self.safe_dict(list, 0, {}) + return self.parse_order(rawOrder) + + async def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + + https://www.tokocrypto.com/apidocs/#all-orders-signed + + fetches information on multiple orders made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrders() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + # 'type': -1, # -1 = all, 1 = open, 2 = closed + # 'side': 1, # or 2 + # 'startTime': since, + # 'endTime': self.milliseconds(), + # 'fromId': 'starting order ID', # if defined, the "direct" field becomes mandatory + # 'direct': 'prev', # prev, next + # 'limit': 500, # default 500, max 1000 + } + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + response = await self.privateGetOpenV1Orders(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "success", + # "data": { + # "list": [ + # { + # "orderId": "4", # order id + # "bOrderId": "100001", # binance order id + # "bOrderListId": -1, # Unless part of an OCO, the value will always be -1. + # "clientId": "1aa4f99ad7bc4fab903395afd25d0597", # client custom order id + # "symbol": "ADA_USDT", + # "symbolType": 1, + # "side": 1, + # "type": 1, + # "price": "0.1", + # "origQty": "10", + # "origQuoteQty": "1", + # "executedQty": "0", + # "executedPrice": "0", + # "executedQuoteQty": "0", + # "timeInForce": 1, + # "stopPrice": "0.0000000000000000", + # "icebergQty": "0.0000000000000000", + # "status": 0, + # "isWorking": 0, + # "createTime": 1572692016811 + # } + # ] + # }, + # "timestamp": 1572860756458 + # } + # + data = self.safe_value(response, 'data', {}) + orders = self.safe_list(data, 'list', []) + return self.parse_orders(orders, market, since, limit) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + + https://www.tokocrypto.com/apidocs/#all-orders-signed + + fetch all unfilled currently open orders + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + request: dict = {'type': 1} # -1 = all, 1 = open, 2 = closed + return await self.fetch_orders(symbol, since, limit, self.extend(request, params)) + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + + https://www.tokocrypto.com/apidocs/#all-orders-signed + + fetches information on multiple closed orders made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + request: dict = {'type': 2} # -1 = all, 1 = open, 2 = closed + return await self.fetch_orders(symbol, since, limit, self.extend(request, params)) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + + https://www.tokocrypto.com/apidocs/#cancel-order-signed + + cancels an open order + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + request: dict = { + 'orderId': id, + } + response = await self.privatePostOpenV1OrdersCancel(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "Success", + # "data": { + # "orderId": "145221985", + # "bOrderListId": "0", + # "clientId": "201515331fd64d03aedbe687a38152e3", + # "bOrderId": "35239632", + # "symbol": "USDT_BIDR", + # "symbolType": 1, + # "type": 1, + # "side": 0, + # "price": "11907.0000000000000000", + # "origQty": "2.0000000000000000", + # "origQuoteQty": "23814.0000000000000000", + # "executedPrice": "0.0000000000000000", + # "executedQty": "0.00000000", + # "executedQuoteQty": "0.00", + # "timeInForce": 1, + # "stopPrice": "0.0000000000000000", + # "icebergQty": "0.0000000000000000", + # "status": 3 + # }, + # "timestamp": 1662710683634 + # } + # + rawOrder = self.safe_dict(response, 'data', {}) + return self.parse_order(rawOrder) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + + https://www.tokocrypto.com/apidocs/#account-trade-list-signed + + fetch all trades made by the user + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMyTrades() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + endTime = self.safe_integer_2(params, 'until', 'endTime') + if since is not None: + request['startTime'] = since + if endTime is not None: + request['endTime'] = endTime + params = self.omit(params, ['endTime', 'until']) + if limit is not None: + request['limit'] = limit + response = await self.privateGetOpenV1OrdersTrades(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "success", + # "data": { + # "list": [ + # { + # "tradeId": "3", + # "orderId": "2", + # "symbol": "ADA_USDT", + # "price": "0.04398", + # "qty": "250", + # "quoteQty": "10.995", + # "commission": "0.25", + # "commissionAsset": "ADA", + # "isBuyer": 1, + # "isMaker": 0, + # "isBestMatch": 1, + # "time": "1572920872276" + # } + # ] + # }, + # "timestamp": 1573723498893 + # } + # + data = self.safe_value(response, 'data', {}) + trades = self.safe_list(data, 'list', []) + return self.parse_trades(trades, market, since, limit) + + async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://www.tokocrypto.com/apidocs/#deposit-address-signed + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'asset': currency['id'], + # 'network': 'ETH', # 'BSC', 'XMR', you can get network and isDefault in networkList in the response of sapiGetCapitalConfigDetail + } + networks = self.safe_value(self.options, 'networks', {}) + network = self.safe_string_upper(params, 'network') # self line allows the user to specify either ERC20 or ETH + network = self.safe_string(networks, network, network) # handle ERC20>ETH alias + if network is not None: + request['network'] = network + params = self.omit(params, 'network') + # has support for the 'network' parameter + # https://binance-docs.github.io/apidocs/spot/en/#deposit-address-supporting-network-user_data + response = await self.privateGetOpenV1DepositsAddress(self.extend(request, params)) + # + # { + # "code":0, + # "msg":"Success", + # "data":{ + # "uid":"182395", + # "asset":"USDT", + # "network":"ETH", + # "address":"0x101a925704f6ff13295ab8dd7a60988d116aaedf", + # "addressTag":"", + # "status":1 + # }, + # "timestamp":1660685915746 + # } + # + data = self.safe_value(response, 'data', {}) + address = self.safe_string(data, 'address') + tag = self.safe_string(data, 'addressTag', '') + if len(tag) == 0: + tag = None + self.check_address(address) + return { + 'info': response, + 'currency': code, + 'network': self.safe_string(data, 'network'), + 'address': address, + 'tag': tag, + } + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + + https://www.tokocrypto.com/apidocs/#deposit-history-signed + + fetch all deposits made to an account + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch deposits for + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + currency = None + request: dict = {} + until = self.safe_integer(params, 'until') + if code is not None: + currency = self.currency(code) + request['coin'] = currency['id'] + if since is not None: + request['startTime'] = since + # max 3 months range https://github.com/ccxt/ccxt/issues/6495 + endTime = self.sum(since, 7776000000) + if until is not None: + endTime = min(endTime, until) + request['endTime'] = endTime + if limit is not None: + request['limit'] = limit + response = await self.privateGetOpenV1Deposits(self.extend(request, params)) + # + # { + # "code":0, + # "msg":"Success", + # "data":{ + # "list":[ + # { + # "id":5167969, + # "asset":"BIDR", + # "network":"BSC", + # "address":"0x101a925704f6ff13295ab8dd7a60988d116aaedf", + # "addressTag":"", + # "txId":"113409337867", + # "amount":"15000", + # "transferType":1, + # "status":1, + # "insertTime":"1659429390000" + # }, + # ] + # }, + # "timestamp":1659758865998 + # } + # + data = self.safe_value(response, 'data', {}) + deposits = self.safe_list(data, 'list', []) + return self.parse_transactions(deposits, currency, since, limit) + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + + https://www.tokocrypto.com/apidocs/#withdraw-signed + + fetch all withdrawals made from an account + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + request: dict = {} + currency = None + if code is not None: + currency = self.currency(code) + request['coin'] = currency['id'] + if since is not None: + request['startTime'] = since + # max 3 months range https://github.com/ccxt/ccxt/issues/6495 + request['endTime'] = self.sum(since, 7776000000) + if limit is not None: + request['limit'] = limit + response = await self.privateGetOpenV1Withdraws(self.extend(request, params)) + # + # { + # "code":0, + # "msg":"Success", + # "data":{ + # "list":[ + # { + # "id":4245859, + # "clientId":"198", + # "asset":"BIDR", + # "network":"BSC", + # "address":"0xff1c75149cc492e7d5566145b859fcafc900b6e9", + # "addressTag":"", + # "amount":"10000", + # "fee":"0", + # "txId":"113501794501", + # "transferType":1, + # "status":10, + # "createTime":1659521314413 + # } + # ] + # }, + # "timestamp":1659759062187 + # } + # + data = self.safe_value(response, 'data', {}) + withdrawals = self.safe_list(data, 'list', []) + return self.parse_transactions(withdrawals, currency, since, limit) + + def parse_transaction_status_by_type(self, status, type=None): + statusesByType: dict = { + 'deposit': { + '0': 'pending', + '1': 'ok', + }, + 'withdrawal': { + '0': 'pending', # Email Sent + '1': 'canceled', # Cancelled(different from 1 = ok in deposits) + '2': 'pending', # Awaiting Approval + '3': 'failed', # Rejected + '4': 'pending', # Processing + '5': 'failed', # Failure + '10': 'ok', # Completed + }, + } + statuses = self.safe_value(statusesByType, type, {}) + return self.safe_string(statuses, status, status) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fetchDeposits + # + # { + # "id": 5167969, + # "asset": "BIDR", + # "network": "BSC", + # "address": "0x101a925704f6ff13295ab8dd7a60988d116aaedf", + # "addressTag": "", + # "txId": "113409337867", + # "amount": "15000", + # "transferType": 1, + # "status": 1, + # "insertTime": "1659429390000" + # } + # + # fetchWithdrawals + # + # { + # "id": 4245859, + # "clientId": "198", + # "asset": "BIDR", + # "network": "BSC", + # "address": "0xff1c75149cc492e7d5566145b859fcafc900b6e9", + # "addressTag": "", + # "amount": "10000", + # "fee": "0", + # "txId": "113501794501", + # "transferType": 1, + # "status": 10, + # "createTime": 1659521314413 + # } + # + # withdraw + # + # { + # "code": 0, + # "msg": "成功", + # "data": { + # "withdrawId":"12" + # }, + # "timestamp": 1571745049095 + # } + # + address = self.safe_string(transaction, 'address') + tag = self.safe_string(transaction, 'addressTag') # set but unused + if tag is not None: + if len(tag) < 1: + tag = None + txid = self.safe_string(transaction, 'txId') + if (txid is not None) and (txid.find('Internal transfer ') >= 0): + txid = txid[18:] + currencyId = self.safe_string_2(transaction, 'coin', 'fiatCurrency') + code = self.safe_currency_code(currencyId, currency) + timestamp = None + insertTime = self.safe_integer(transaction, 'insertTime') + createTime = self.safe_integer_2(transaction, 'createTime', 'timestamp') + type = self.safe_string(transaction, 'type') + if type is None: + if (insertTime is not None) and (createTime is None): + type = 'deposit' + timestamp = insertTime + elif (insertTime is None) and (createTime is not None): + type = 'withdrawal' + timestamp = createTime + feeCost = self.safe_number_2(transaction, 'transactionFee', 'totalFee') + fee = { + 'currency': None, + 'cost': None, + 'rate': None, + } + if feeCost is not None: + fee['currency'] = code + fee['cost'] = feeCost + internalRaw = self.safe_integer(transaction, 'transferType') + internal = False + if internalRaw is not None: + internal = True + id = self.safe_string(transaction, 'id') + if id is None: + data = self.safe_value(transaction, 'data', {}) + id = self.safe_string(data, 'withdrawId') + type = 'withdrawal' + return { + 'info': transaction, + 'id': id, + 'txid': txid, + 'type': type, + 'currency': code, + 'network': self.safe_string(transaction, 'network'), + 'amount': self.safe_number(transaction, 'amount'), + 'status': self.parse_transaction_status_by_type(self.safe_string(transaction, 'status'), type), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'address': address, + 'addressFrom': None, + 'addressTo': address, + 'tag': tag, + 'tagFrom': None, + 'tagTo': tag, + 'updated': self.safe_integer_2(transaction, 'successTime', 'updateTime'), + 'comment': None, + 'internal': internal, + 'fee': fee, + } + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + + https://www.tokocrypto.com/apidocs/#withdraw-signed + + make a withdrawal + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + await self.load_markets() + self.check_address(address) + currency = self.currency(code) + request: dict = { + 'asset': currency['id'], + # 'clientId': 'string', # # client's custom id for withdraw order, server does not check it's uniqueness, automatically generated if not sent + # 'network': 'string', + 'address': address, + # 'addressTag': 'string', # for coins like XRP, XMR, etc + 'amount': self.number_to_string(amount), + } + if tag is not None: + request['addressTag'] = tag + networkCode, query = self.handle_network_code_and_params(params) + networkId = self.network_code_to_id(networkCode) + if networkId is not None: + request['network'] = networkId.upper() + response = await self.privatePostOpenV1Withdraws(self.extend(request, query)) + # + # { + # "code": 0, + # "msg": "成功", + # "data": { + # "withdrawId":"12" + # }, + # "timestamp": 1571745049095 + # } + # + return self.parse_transaction(response, currency) + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + if not (api in self.urls['api']['rest']): + raise NotSupported(self.id + ' does not have a testnet/sandbox URL for ' + api + ' endpoints') + url = self.urls['api']['rest'][api] + url += '/' + path + if api == 'wapi': + url += '.html' + userDataStream = (path == 'userDataStream') or (path == 'listenKey') + if userDataStream: + if self.apiKey: + # v1 special case for userDataStream + headers = { + 'X-MBX-APIKEY': self.apiKey, + 'Content-Type': 'application/x-www-form-urlencoded', + } + if method != 'GET': + body = self.urlencode(params) + else: + raise AuthenticationError(self.id + ' userDataStream endpoint requires `apiKey` credential') + elif (api == 'private') or (api == 'sapi' and path != 'system/status') or (api == 'sapiV3') or (api == 'wapi' and path != 'systemStatus') or (api == 'dapiPrivate') or (api == 'dapiPrivateV2') or (api == 'fapiPrivate') or (api == 'fapiPrivateV2'): + self.check_required_credentials() + query = None + defaultRecvWindow = self.safe_integer(self.options, 'recvWindow') + extendedParams = self.extend({ + 'timestamp': self.nonce(), + }, params) + if defaultRecvWindow is not None: + extendedParams['recvWindow'] = defaultRecvWindow + recvWindow = self.safe_integer(params, 'recvWindow') + if recvWindow is not None: + extendedParams['recvWindow'] = recvWindow + if (api == 'sapi') and (path == 'asset/dust'): + query = self.urlencode_with_array_repeat(extendedParams) + elif (path == 'batchOrders') or (path.find('sub-account') >= 0) or (path == 'capital/withdraw/apply') or (path.find('staking') >= 0): + query = self.rawencode(extendedParams) + else: + query = self.urlencode(extendedParams) + signature = self.hmac(self.encode(query), self.encode(self.secret), hashlib.sha256) + query += '&' + 'signature=' + signature + headers = { + 'X-MBX-APIKEY': self.apiKey, + } + if (method == 'GET') or (method == 'DELETE') or (api == 'wapi'): + url += '?' + query + else: + body = query + headers['Content-Type'] = 'application/x-www-form-urlencoded' + else: + if params: + url += '?' + self.urlencode(params) + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if (code == 418) or (code == 429): + raise DDoSProtection(self.id + ' ' + str(code) + ' ' + reason + ' ' + body) + # error response in a form: {"code": -1013, "msg": "Invalid quantity."} + # following block cointains legacy checks against message patterns in "msg" property + # will switch "code" checks eventually, when we know all of them + if code >= 400: + if body.find('Price * QTY is zero or less') >= 0: + raise InvalidOrder(self.id + ' order cost = amount * price is zero or less ' + body) + if body.find('LOT_SIZE') >= 0: + raise InvalidOrder(self.id + ' order amount should be evenly divisible by lot size ' + body) + if body.find('PRICE_FILTER') >= 0: + raise InvalidOrder(self.id + ' order price is invalid, i.e. exceeds allowed price precision, exceeds min price or max price limits or is invalid value in general, use self.price_to_precision(symbol, amount) ' + body) + if response is None: + return None # fallback to default error handler + # check success value for wapi endpoints + # response in format {'msg': 'The coin does not exist.', 'success': True/false} + success = self.safe_bool(response, 'success', True) + if not success: + messageInner = self.safe_string(response, 'msg') + parsedMessage = None + if messageInner is not None: + try: + parsedMessage = json.loads(messageInner) + except Exception as e: + # do nothing + parsedMessage = None + if parsedMessage is not None: + response = parsedMessage + message = self.safe_string(response, 'msg') + if message is not None: + self.throw_exactly_matched_exception(self.exceptions['exact'], message, self.id + ' ' + message) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, self.id + ' ' + message) + # checks against error codes + error = self.safe_string(response, 'code') + if error is not None: + # https://github.com/ccxt/ccxt/issues/6501 + # https://github.com/ccxt/ccxt/issues/7742 + if (error == '200') or Precise.string_equals(error, '0'): + return None + # a workaround for {"code":-2015,"msg":"Invalid API-key, IP, or permissions for action."} + # despite that their message is very confusing, it is raised by Binance + # on a temporary ban, the API key is valid, but disabled for a while + if (error == '-2015') and self.options['hasAlreadyAuthenticatedSuccessfully']: + raise DDoSProtection(self.id + ' ' + body) + feedback = self.id + ' ' + body + if message == 'No need to change margin type.': + # not an error + # https://github.com/ccxt/ccxt/issues/11268 + # https://github.com/ccxt/ccxt/pull/11624 + # POST https://fapi.binance.com/fapi/v1/marginType 400 Bad Request + # binanceusdm {"code":-4046,"msg":"No need to change margin type."} + raise MarginModeAlreadySet(feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], error, feedback) + raise ExchangeError(feedback) + if not success: + raise ExchangeError(self.id + ' ' + body) + return None + + def calculate_rate_limiter_cost(self, api, method, path, params, config={}): + if ('noCoin' in config) and not ('coin' in params): + return config['noCoin'] + elif ('noSymbol' in config) and not ('symbol' in params): + return config['noSymbol'] + elif ('noPoolId' in config) and not ('poolId' in params): + return config['noPoolId'] + elif ('byLimit' in config) and ('limit' in params): + limit = params['limit'] + byLimit = config['byLimit'] + for i in range(0, len(byLimit)): + entry = byLimit[i] + if limit <= entry[0]: + return entry[1] + return self.safe_integer(config, 'cost', 1) diff --git a/ccxt/async_support/toobit.py b/ccxt/async_support/toobit.py new file mode 100644 index 0000000..1ae3c81 --- /dev/null +++ b/ccxt/async_support/toobit.py @@ -0,0 +1,2868 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.toobit import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currencies, Currency, DepositAddress, Int, LedgerEntry, Leverage, Market, Num, Order, OrderBook, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, FundingRates, Trade, TradingFees, Transaction, MarketInterface, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import OperationRejected +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import NotSupported +from ccxt.base.errors import OperationFailed +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class toobit(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(toobit, self).describe(), { + 'id': 'toobit', + 'name': 'Toobit', + 'countries': ['KY'], # Cayman Islands + 'version': 'v1', + 'rateLimit': 20, # 50 requests per second + 'certified': False, + 'pro': True, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': True, + 'future': False, + 'option': False, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'cancelOrders': True, + 'createOrder': True, + 'fetchBalance': True, + 'fetchBidsAsks': True, + 'fetchCurrencies': True, + 'fetchDepositAddress': True, + 'fetchDeposits': True, + 'fetchFundingRateHistory': True, + 'fetchFundingRates': True, + 'fetchIndexOHLCV': True, + 'fetchLastPrices': True, + 'fetchLedger': True, + 'fetchMarkets': True, + 'fetchMarkOHLCV': True, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchStatus': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchWithdrawals': True, + 'setMarginMode': True, + 'transfer': True, + 'withdraw': True, + }, + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/0c7a97d5-182c-492e-b921-23540c868e0e', + 'api': { + 'common': 'https://api.toobit.com', + 'private': 'https://api.toobit.com', + }, + 'www': 'https://www.toobit.com/', + 'doc': [ + 'https://toobit-docs.github.io/apidocs/spot/v1/en/', + 'https://toobit-docs.github.io/apidocs/usdt_swap/v1/en/', + ], + 'referral': { + 'url': 'https://www.toobit.com/en-US/r?i=IFFPy0', + 'discount': 0.1, + }, + 'fees': 'https://www.toobit.com/fee', + }, + 'api': { + 'common': { + 'get': { + 'api/v1/time': 1, + 'api/v1/ping': 1, + 'api/v1/exchangeInfo': 1, + 'quote/v1/depth': 1, # todo: by limit 1-10 + 'quote/v1/depth/merged': 1, + 'quote/v1/trades': 1, + 'quote/v1/klines': 1, + 'quote/v1/index/klines': 1, + 'quote/v1/markPrice/klines': 1, + 'quote/v1/markPrice': 1, + 'quote/v1/index': 1, + 'quote/v1/ticker/24hr': 40, # todo: 1-40 depenidng noSymbol + 'quote/v1/contract/ticker/24hr': 40, # todo: 1-40 depenidng noSymbol + 'quote/v1/ticker/price': 1, + 'quote/v1/ticker/bookTicker': 1, + 'api/v1/futures/fundingRate': 1, + 'api/v1/futures/historyFundingRate': 1, + }, + }, + 'private': { + 'get': { + 'api/v1/account': 5, + 'api/v1/account/checkApiKey': 1, + 'api/v1/spot/order': 1 * 1.67, + 'api/v1/spot/openOrders': 1 * 1.67, + 'api/v1/futures/openOrders': 1 * 1.67, + 'api/v1/spot/tradeOrders': 5 * 1.67, + 'api/v1/futures/historyOrders': 5 * 1.67, + 'api/v1/account/trades': 5 * 1.67, + 'api/v1/account/balanceFlow': 5, + 'api/v1/account/depositOrders': 5, + 'api/v1/account/withdrawOrders': 5, + 'api/v1/account/deposit/address': 1, + # contracts + 'api/v1/subAccount': 5, + 'api/v1/futures/accountLeverage': 1, + 'api/v1/futures/order': 1 * 1.67, + 'api/v1/futures/positions': 5 * 1.67, + 'api/v1/futures/balance': 5, + 'api/v1/futures/userTrades': 5 * 1.67, + 'api/v1/futures/balanceFlow': 5, + 'api/v1/futures/commissionRate': 5, + 'api/v1/futures/todayPnl': 5, + }, + 'post': { + 'api/v1/spot/orderTest': 1 * 1.67, + 'api/v1/spot/order': 1 * 1.67, + 'api/v1/futures/order': 1 * 1.67, + 'api/v1/spot/batchOrders': 2 * 1.67, + 'api/v1/subAccount/transfer': 1, + 'api/v1/account/withdraw': 1, + # contracts + 'api/v1/futures/marginType': 1, + 'api/v1/futures/leverage': 1, + 'api/v1/futures/batchOrders': 2 * 1.67, + 'api/v1/futures/position/trading-stop': 3 * 1.67, + 'api/v1/futures/positionMargin': 1, + 'api/v1/userDataStream': 1, + 'api/v1/listenKey': 1, + }, + 'delete': { + 'api/v1/spot/order': 1 * 1.67, + 'api/v1/futures/order': 1 * 1.67, + 'api/v1/spot/openOrders': 5 * 1.67, + 'api/v1/futures/batchOrders': 5 * 1.67, + 'api/v1/spot/cancelOrderByIds': 5 * 1.67, + 'api/v1/futures/cancelOrderByIds': 5 * 1.67, + 'api/v1/listenKey': 1, + }, + 'put': { + 'api/v1/listenKey': 1, + }, + }, + }, + 'timeframes': { + '1m': '1m', + '3m': '3m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1h', + '2h': '2h', + '4h': '4h', + '6h': '6h', + '8h': '8h', + '12h': '12h', + '1d': '1d', + '1w': '1w', + '1M': '1M', + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'exact': { + '-1000': OperationFailed, # An unknown error occurred while processing the request. + '-1001': OperationFailed, # Internal error; unable to process your request. Please try again. + '-1002': PermissionDenied, # You are not authorized to execute self request. + '-1003': RateLimitExceeded, # TOO_MANY_REQUESTS + '-1004': BadRequest, # {"code":-1004,"msg":"Missing required parameter \u0027xyz\u0027"} | {"code":-1004,"msg":"Bad request"} + '-1006': OperationFailed, # An unexpected response was received from the message bus. Execution status unknown + '-1007': OperationFailed, # Timeout waiting for response from backend server. Send status unknown; execution status unknown. + '-1014': OperationFailed, # Unsupported order combination. + '-1015': RateLimitExceeded, # Too many new orders + '-1016': OperationRejected, # This service is no longer available. + '-1020': OperationRejected, # This operation is not supported. + '-1021': OperationRejected, # Timestamp for self request is outside of the recvWindow. + '-1022': OperationRejected, # Signature for self request is not valid. + '-1100': BadRequest, # Illegal characters found in a parameter. + '-1101': BadRequest, # Too many parameters sent for self endpoint. + '-1102': BadRequest, # A mandatory parameter was not sent, was empty/null, or malformed + '-1103': BadRequest, # An unknown parameter was sent + '-1104': BadRequest, # Not all sent parameters were read + '-1105': BadRequest, # A parameter was empty + '-1106': BadRequest, # A parameter was sent when not required + '-1111': BadRequest, # Precision is over the maximum defined for self asset. + '-1112': OperationRejected, # No orders on book for symbol. + '-1114': BadRequest, # TimeInForce parameter sent when not required. + '-1115': BadRequest, # Invalid timeInForce + '-1116': BadRequest, # Invalid orderType + '-1117': BadRequest, # Invalid side + '-1118': InvalidOrder, # New client order ID was empty. + '-1119': InvalidOrder, # Original client order ID was empty + '-1120': BadRequest, # Invalid interval + '-1121': BadRequest, # Invalid symbol + '-1125': OperationRejected, # This listenKey does not exist. + '-1127': OperationRejected, # Lookup interval is too big + '-1128': BadRequest, # Combination of optional parameters invalid + '-1130': BadRequest, # Invalid data sent for a parameter + '-1132': OperationRejected, # Order price too high + '-1133': OperationRejected, # Order price lower than the minimum,please check general broker info + '-1134': OperationRejected, # Order price decimal too long,please check general broker info + '-1135': OperationRejected, # Order quantity too large + '-1136': OperationRejected, # Order quantity lower than the minimum + '-1137': OperationRejected, # Order quantity decimal too long + '-1138': OperationRejected, # Order price exceeds permissible range + '-1139': OperationRejected, # Order has been filled + '-1140': OperationRejected, # Transaction amount lower than the minimum + '-1141': InvalidOrder, # Duplicate clientOrderId + '-1142': InvalidOrder, # Order has been canceled + '-1143': InvalidOrder, # Cannot be found on order book + '-1144': OperationRejected, # Order has been locked + '-1145': OperationRejected, # This order type does not support cancellation + '-1146': OperationFailed, # Order creation timeout + '-1147': OperationFailed, # Order cancellation timeout + '-1193': OperationRejected, # Create order count limit + '-1194': OperationRejected, # Create market order forbidden + '-1195': OperationRejected, # Create limit order price too small + '-1196': OperationRejected, # Create limit order price too big + '-1197': OperationRejected, # Create limit order buy price too big + '-1198': OperationRejected, # Create limit order sell price too small + '-1199': OperationRejected, # Create order buy quantity too small + '-1200': OperationRejected, # Create order buy quantity too big + '-1201': OperationRejected, # Create limit order sell price too big + '-1202': OperationRejected, # Create order sell quantity too small + '-1203': OperationRejected, # Create order sell quantity too big + '-1206': OperationRejected, # Orders over the maximum transaction amount + '-2010': OperationFailed, # NEW_ORDER_REJECTED + '-2011': OperationFailed, # CANCEL_REJECTED + '-2013': InvalidOrder, # Order does not exist. + '-2014': PermissionDenied, # API-key format invalid. + '-2015': PermissionDenied, # Invalid API-key, IP, or permissions for action. + '-2016': BadRequest, # No trading window could be found for the symbol. Try ticker/24hrs instead. + # errors above 3xxx are from swap API + '-3050': ExchangeError, # CREATE_API_KEY_EXCEED_LIMIT + '-3101': OperationRejected, # open margin account error + '-3102': OperationRejected, # get margin safety error + '-3103': BadRequest, # risk config is not exit + '-3105': OperationRejected, # token can not borrow + '-3107': OperationRejected, # token can not withdraw + '-3108': OperationRejected, # get token avail withdraw error + '-3109': OperationRejected, # margin withdraw failed + '-3110': InsufficientFunds, # margin avail withdraw not enough failed + '-3116': OperationRejected, # repay fail + '-3117': OperationRejected, # get margin all position fail + '-3120': OperationRejected, # get repay order fail + '-3124': OperationRejected, # Position and order data error + '-3125': OperationRejected, # Position size cannot meet target leverage + '-3126': OperationRejected, # Adjust leverage fail + '-3127': OperationFailed, # Adjust leverage timeout + '-3128': OperationRejected, # The margin mode cannot be changed while you have an open order/position + '-3129': BadRequest, # cone futures change position type error + '-3130': OperationRejected, # order margin insufficient + '-3131': NotSupported, # Leverage reduction is not supported in Isolated Margin Mode with open positions. + }, + 'broad': { + 'Unknown order sent': OrderNotFound, + 'Duplicate order sent': InvalidOrder, + 'Market is closed': OperationRejected, + 'Account has insufficient balance for requested action': InsufficientFunds, + 'Market orders are not supported for self symbol': OperationRejected, + 'Iceberg orders are not supported for self symbol': OperationRejected, + 'Stop loss orders are not supported for self symbol': OperationRejected, + 'Stop loss limit orders are not supported for self symbol': OperationRejected, + 'Take profit orders are not supported for self symbol': OperationRejected, + 'Take profit limit orders are not supported for self symbol': OperationRejected, + 'QTY is zero or less': BadRequest, + 'IcebergQty exceeds QTY': OperationRejected, + 'This action disabled is on self account': PermissionDenied, + 'Unsupported order combination': BadRequest, + 'Order would trigger immediately': OperationRejected, + 'Cancel order is invalid. Check origClOrdId and orderId': OperationRejected, + 'Order would immediately match and take': OperationRejected, + }, + }, + 'commonCurrencies': {}, + 'options': { + 'defaultType': 'spot', + 'accountsByType': { + 'spot': 'MAIN', + 'swap': 'FUTURES', + }, + 'networks': { + 'BTC': 'BTC', + 'ERC20': 'ETH', + 'ETH': 'ETH', + 'BEP20': 'BSC', + 'TRC20': 'TRX', + 'SOL': 'SOL', + 'MATIC': 'MATIC', + 'ARBONE': 'ARBITRUM', + 'BASE': 'BASE', + 'TON': 'TON', + 'AVAXC': 'AVAXC', + 'DOGE': 'DOGE', + 'XRP': 'XRP', + 'DOT': 'DOT', + 'ADA': 'ADA', + 'LTC': 'LTC', + 'APT': 'APT', + 'ATOM': 'ATOM', + 'ALGO': 'ALGO', + 'NEAR': 'NEAR', + 'XLM': 'XLM', + 'SUI': 'SUI', + 'ETC': 'ETC', + 'EOS': 'EOS', + 'WAVES': 'WAVES', + 'ICP': 'ICP', + 'ONE': 'ONE', + # 'CHZ2': 'CHZ2', + }, + 'networksById': { + 'ETH': 'ERC20', + 'ERC20': 'ERC20', + }, + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyRequiresPrice': False, + 'marketBuyByCost': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchOHLCV': { + 'limit': 1000, + }, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 100000, + 'untilDays': 100000, + 'symbolRequired': True, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 1000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': 500, + 'daysBack': 100000, + 'untilDays': 100000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchClosedOrders': None, + }, + 'forDerivatives': { + 'createOrders': None, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + }) + + async def fetch_status(self, params={}): + """ + the latest known information on the availability of the exchange API + + https://toobit-docs.github.io/apidocs/spot/v1/en/#test-connectivity + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `status structure ` + """ + response = await self.commonGetApiV1Ping(params) + return { + 'status': 'ok', + 'updated': None, + 'eta': None, + 'url': None, + 'info': response, + } + + async def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://toobit-docs.github.io/apidocs/spot/v1/en/#check-server-time + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = await self.commonGetApiV1Time(params) + # + # { + # "serverTime": 1699827319559 + # } + # + return self.safe_integer(response, 'serverTime') + + async def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = await self.commonGetApiV1ExchangeInfo(params) + self.options['exchangeInfo'] = response # we store it in options for later use in fetchMarkets + # + # { + # "timezone": "UTC", + # "serverTime": "1755583099926", + # "brokerFilters": [], + # "symbols": [ + # { + # "filters": [ + # { + # "minPrice": "0.01", + # "maxPrice": "10000000.00000000", + # "tickSize": "0.01", + # "filterType": "PRICE_FILTER" + # }, + # { + # "minQty": "0.0001", + # "maxQty": "4000", + # "stepSize": "0.0001", + # "filterType": "LOT_SIZE" + # }, + # { + # "minNotional": "5", + # "filterType": "MIN_NOTIONAL" + # }, + # { + # "minAmount": "5", + # "maxAmount": "6600000", + # "minBuyPrice": "0.01", + # "filterType": "TRADE_AMOUNT" + # }, + # { + # "maxSellPrice": "99999999", + # "buyPriceUpRate": "0.1", + # "sellPriceDownRate": "0.1", + # "filterType": "LIMIT_TRADING" + # }, + # { + # "buyPriceUpRate": "0.1", + # "sellPriceDownRate": "0.1", + # "filterType": "MARKET_TRADING" + # }, + # { + # "noAllowMarketStartTime": "0", + # "noAllowMarketEndTime": "0", + # "limitOrderStartTime": "0", + # "limitOrderEndTime": "0", + # "limitMinPrice": "0", + # "limitMaxPrice": "0", + # "filterType": "OPEN_QUOTE" + # } + # ], + # "exchangeId": "301", + # "symbol": "ETHUSDT", + # "symbolName": "ETHUSDT", + # "status": "TRADING", + # "baseAsset": "ETH", + # "baseAssetName": "ETH", + # "baseAssetPrecision": "0.0001", + # "quoteAsset": "USDT", + # "quoteAssetName": "USDT", + # "quotePrecision": "0.01", + # "icebergAllowed": False, + # "isAggregate": False, + # "allowMargin": True, + # } + # ], + # "options": [], + # "contracts": [ + # { + # "filters": [...], + # "exchangeId": "301", + # "symbol": "BTC-SWAP-USDT", + # "symbolName": "BTC-SWAP-USDTUSDT", + # "status": "TRADING", + # "baseAsset": "BTC-SWAP-USDT", + # "baseAssetPrecision": "0.001", + # "quoteAsset": "USDT", + # "quoteAssetPrecision": "0.1", + # "icebergAllowed": False, + # "inverse": False, + # "index": "BTC", + # "indexToken": "BTCUSDT", + # "marginToken": "USDT", + # "marginPrecision": "0.0001", + # "contractMultiplier": "0.001", + # "underlying": "BTC", + # "riskLimits": [ + # { + # "riskLimitId": "200020911", + # "quantity": "42000.0", + # "initialMargin": "0.02", + # "maintMargin": "0.01", + # "isWhite": False + # }, + # { + # "riskLimitId": "200020912", + # "quantity": "84000.0", + # "initialMargin": "0.04", + # "maintMargin": "0.02", + # "isWhite": False + # }, + # ... + # ] + # }, + # ], + # "coins": [ + # { + # "orgId": "9001", + # "coinId": "TCOM", + # "coinName": "TCOM", + # "coinFullName": "TCOM", + # "allowWithdraw": True, + # "allowDeposit": True, + # "chainTypes": [ + # { + # "chainType": "BSC", + # "withdrawFee": "49.55478", + # "minWithdrawQuantity": "77", + # "maxWithdrawQuantity": "0", + # "minDepositQuantity": "48", + # "allowDeposit": True, + # "allowWithdraw": False + # } + # ], + # "isVirtual": False + # }, + # ... + # + coins = self.safe_list(response, 'coins', []) + result = {} + for i in range(0, len(coins)): + coin = coins[i] + parsed = self.parse_currency(coin) + code = parsed['code'] + result[code] = parsed + return result + + def parse_currency(self, rawCurrency: dict) -> Currency: + id = self.safe_string(rawCurrency, 'coinId') + code = self.safe_currency_code(id) + networks: dict = {} + rawNetworks = self.safe_list(rawCurrency, 'chainTypes') + for j in range(0, len(rawNetworks)): + rawNetwork = rawNetworks[j] + networkId = self.safe_string(rawNetwork, 'chainType') + networkCode = self.network_id_to_code(networkId) + networks[networkCode] = { + 'id': networkId, + 'network': networkCode, + 'margin': None, + 'deposit': self.safe_bool(rawNetwork, 'allowDeposit'), + 'withdraw': self.safe_bool(rawNetwork, 'allowWithdraw'), + 'active': None, + 'fee': self.safe_number(rawNetwork, 'withdrawFee'), + 'precision': None, + 'limits': { + 'deposit': { + 'min': self.safe_number(rawNetwork, 'minDepositQuantity'), + 'max': None, + }, + 'withdraw': { + 'min': self.safe_number(rawNetwork, 'minWithdrawQuantity'), + 'max': self.safe_number(rawNetwork, 'maxWithdrawQuantity'), + }, + }, + 'info': rawNetwork, + } + return self.safe_currency_structure({ + 'id': id, + 'code': code, + 'name': self.safe_string(rawCurrency, 'coinFullName'), + 'type': None, + 'active': None, + 'deposit': self.safe_bool(rawCurrency, 'allowDeposit'), + 'withdraw': self.safe_bool(rawCurrency, 'allowWithdraw'), + 'fee': None, + 'precision': None, + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + }, + 'networks': networks, + 'info': rawCurrency, + }) + + async def fetch_markets(self, params={}) -> List[MarketInterface]: + """ + retrieves data on all markets for toobit + + https://toobit-docs.github.io/apidocs/spot/v1/en/#exchange-information + https://toobit-docs.github.io/apidocs/usdt_swap/v1/en/#exchange-information + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = self.safe_dict(self.options, 'exchangeInfo') + if response is not None: + self.options['exchangeInfo'] = None # reset it to avoid using old cached data + else: + response = await self.commonGetApiV1ExchangeInfo(params) + # + # { + # "timezone": "UTC", + # "serverTime": "1755583099926", + # "brokerFilters": [], + # "symbols": [ + # { + # "filters": [ + # { + # "minPrice": "0.01", + # "maxPrice": "10000000.00000000", + # "tickSize": "0.01", + # "filterType": "PRICE_FILTER" + # }, + # { + # "minQty": "0.0001", + # "maxQty": "4000", + # "stepSize": "0.0001", + # "filterType": "LOT_SIZE" + # }, + # { + # "minNotional": "5", + # "filterType": "MIN_NOTIONAL" + # }, + # { + # "minAmount": "5", + # "maxAmount": "6600000", + # "minBuyPrice": "0.01", + # "filterType": "TRADE_AMOUNT" + # }, + # { + # "maxSellPrice": "99999999", + # "buyPriceUpRate": "0.1", + # "sellPriceDownRate": "0.1", + # "filterType": "LIMIT_TRADING" + # }, + # { + # "buyPriceUpRate": "0.1", + # "sellPriceDownRate": "0.1", + # "filterType": "MARKET_TRADING" + # }, + # { + # "noAllowMarketStartTime": "0", + # "noAllowMarketEndTime": "0", + # "limitOrderStartTime": "0", + # "limitOrderEndTime": "0", + # "limitMinPrice": "0", + # "limitMaxPrice": "0", + # "filterType": "OPEN_QUOTE" + # } + # ], + # "exchangeId": "301", + # "symbol": "ETHUSDT", + # "symbolName": "ETHUSDT", + # "status": "TRADING", + # "baseAsset": "ETH", + # "baseAssetName": "ETH", + # "baseAssetPrecision": "0.0001", + # "quoteAsset": "USDT", + # "quoteAssetName": "USDT", + # "quotePrecision": "0.01", + # "icebergAllowed": False, + # "isAggregate": False, + # "allowMargin": True, + # } + # ], + # "options": [], + # "contracts": [ + # { + # "filters": [...], + # "exchangeId": "301", + # "symbol": "BTC-SWAP-USDT", + # "symbolName": "BTC-SWAP-USDTUSDT", + # "status": "TRADING", + # "baseAsset": "BTC-SWAP-USDT", + # "baseAssetPrecision": "0.001", + # "quoteAsset": "USDT", + # "quoteAssetPrecision": "0.1", + # "icebergAllowed": False, + # "inverse": False, + # "index": "BTC", + # "indexToken": "BTCUSDT", + # "marginToken": "USDT", + # "marginPrecision": "0.0001", + # "contractMultiplier": "0.001", + # "underlying": "BTC", + # "riskLimits": [ + # { + # "riskLimitId": "200020911", + # "quantity": "42000.0", + # "initialMargin": "0.02", + # "maintMargin": "0.01", + # "isWhite": False + # }, + # { + # "riskLimitId": "200020912", + # "quantity": "84000.0", + # "initialMargin": "0.04", + # "maintMargin": "0.02", + # "isWhite": False + # }, + # ... + # ] + # }, + # ], + # "coins": [ + # { + # "orgId": "9001", + # "coinId": "TCOM", + # "coinName": "TCOM", + # "coinFullName": "TCOM", + # "allowWithdraw": True, + # "allowDeposit": True, + # "chainTypes": [ + # { + # "chainType": "BSC", + # "withdrawFee": "49.55478", + # "minWithdrawQuantity": "77", + # "maxWithdrawQuantity": "0", + # "minDepositQuantity": "48", + # "allowDeposit": True, + # "allowWithdraw": False + # } + # ], + # "isVirtual": False + # }, + # ... + # + symbols = self.safe_list(response, 'symbols', []) + contracts = self.safe_list(response, 'contracts', []) + all = self.array_concat(symbols, contracts) + result = [] + for i in range(0, len(all)): + market = all[i] + parsed = self.parse_market(market) + result.append(parsed) + return result + + def parse_market(self, market: dict) -> Market: + id = self.safe_string(market, 'symbol') + baseId = self.safe_string(market, 'baseAsset') + quoteId = self.safe_string(market, 'quoteAsset') + baseParts = baseId.split('-') + baseIdClean = baseParts[0] + base = self.safe_currency_code(baseIdClean) + quote = self.safe_currency_code(quoteId) + settleId = self.safe_string(market, 'marginToken') + settle = self.safe_currency_code(settleId) + status = self.safe_string(market, 'status') + active = (status == 'TRADING') + filters = self.safe_list(market, 'filters', []) + filtersByType = self.index_by(filters, 'filterType') + priceFilter = self.safe_dict(filtersByType, 'PRICE_FILTER', {}) + lotSizeFilter = self.safe_dict(filtersByType, 'LOT_SIZE', {}) + minNotionalFilter = self.safe_dict(filtersByType, 'MIN_NOTIONAL', {}) + symbol = base + '/' + quote + isContract = ('contractMultiplier' in market) + inverse = self.safe_bool_2(market, 'isInverse', 'inverse') + if isContract: + symbol += ':' + settle + return self.safe_market_structure({ + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': 'swap' if isContract else 'spot', + 'spot': not isContract, + 'margin': False, + 'swap': isContract, + 'future': False, + 'option': False, + 'active': active, + 'contract': isContract, + 'linear': not inverse if isContract else None, + 'inverse': inverse if isContract else None, + 'contractSize': self.safe_number(market, 'contractMultiplier'), + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.safe_number(lotSizeFilter, 'stepSize'), + 'price': self.safe_number(priceFilter, 'tickSize'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(lotSizeFilter, 'minQty'), + 'max': self.safe_number(lotSizeFilter, 'maxQty'), + }, + 'price': { + 'min': self.safe_number(priceFilter, 'minPrice'), + 'max': self.safe_number(priceFilter, 'maxPrice'), + }, + 'cost': { + 'min': self.safe_number(minNotionalFilter, 'minNotional'), + 'max': None, + }, + }, + 'created': None, + 'info': market, + }) + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://toobit-docs.github.io/apidocs/spot/v1/en/#order-book + https://toobit-docs.github.io/apidocs/usdt_swap/v1/en/#order-book + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['limit'] = limit + response = await self.commonGetQuoteV1Depth(self.extend(request, params)) + # + # { + # "t": "1755593995237", + # "b": [ + # [ + # "115186.47", + # "4.184864" + # ], + # [ + # "115186.46", + # "0.002756" + # ], + # ... + # ], + # "a": [ + # [ + # "115186.48", + # "6.137369" + # ], + # [ + # "115186.49", + # "0.002914" + # ], + # ... + # ] + # } + # + timestamp = self.safe_integer(response, 't') + return self.parse_order_book(response, market['symbol'], timestamp, 'b', 'a') + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get a list of the most recent trades for a particular symbol + + https://toobit-docs.github.io/apidocs/spot/v1/en/#recent-trades-list + https://toobit-docs.github.io/apidocs/usdt_swap/v1/en/#recent-trades-list + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum number of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['limit'] = limit + response = await self.commonGetQuoteV1Trades(self.extend(request, params)) + # + # [ + # { + # "t": "1755594277287", + # "p": "115276.99", + # "q": "0.001508", + # "ibm": True + # }, + # ] + # + return self.parse_trades(response, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades + # + # { + # "t": "1755594277287", + # "p": "115276.99", + # "q": "0.001508", + # "ibm": True + # }, + # # watchTrades have also an additional fields: + # "v": "4864732022868004630", # trade id + # "m": True, # is the buyer taker + # + # fetchMyTrades + # + # { + # "id": "2024934575206059008", + # "symbol": "ETHUSDT", + # "orderId": "2024934575097029888", + # "ticketId": "4864450547563401875", + # "price": "4641.21", + # "qty": "0.001", + # "time": "1756127012094", + # "isMaker": False, + # "commission": "0.00464121", + # "commissionAsset": "USDT", + # "makerRebate": "0", + # "symbolName": "ETHUSDT", # only in SPOT + # "isBuyer": False, # only in SPOT + # "feeAmount": "0.00464121", # only in SPOT + # "feeCoinId": "USDT", # only in SPOT + # "fee": { # only in SPOT + # "feeCoinId": "USDT", + # "feeCoinName": "USDT", + # "fee": "0.00464121" + # }, + # "type": "LIMIT", # only in CONTRACT + # "side": "BUY_OPEN", # only in CONTRACT + # "realizedPnl": "0", # only in CONTRACT + # }, + # + timestamp = self.safe_integer_2(trade, 't', 'time') + priceString = self.safe_string_2(trade, 'p', 'price') + amountString = self.safe_string_2(trade, 'q', 'qty') + isBuyer = self.safe_bool(trade, 'isBuyer') + side = None + isBuyerMaker = self.safe_bool(trade, 'ibm') + if isBuyerMaker is None: + isBuyerTaker = self.safe_bool(trade, 'm') + if isBuyerTaker is not None: + isBuyerMaker = not isBuyerTaker + if isBuyerMaker is not None: + if isBuyerMaker: + side = 'sell' + else: + side = 'buy' + else: + if isBuyer: + side = 'buy' + else: + side = 'sell' + feeCurrencyId = self.safe_string(trade, 'feeCoinId') + feeAmount = self.safe_string(trade, 'feeAmount') + fee = None + if feeAmount is not None: + fee = { + 'currency': self.safe_currency_code(feeCurrencyId), + 'cost': feeAmount, + } + isMaker = self.safe_bool(trade, 'isMaker') + takerOrMaker = None + if isMaker is not None: + takerOrMaker = 'maker' if isMaker else 'taker' + market = self.safe_market(None, market) + symbol = market['symbol'] + return self.safe_trade({ + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'id': self.safe_string_2(trade, 'id', 'v'), + 'order': self.safe_string(trade, 'orderId'), + 'type': None, + 'side': side, + 'amount': amountString, + 'price': priceString, + 'cost': None, + 'takerOrMaker': takerOrMaker, + 'fee': fee, + }, market) + + async def fetch_ohlcv(self, symbol: str, timeframe='1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://toobit-docs.github.io/apidocs/spot/v1/en/#kline-candlestick-data + https://toobit-docs.github.io/apidocs/usdt_swap/v1/en/#kline-candlestick-data + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'interval': self.safe_string(self.timeframes, timeframe, timeframe), + } + if since is not None: + request['startTime'] = since + until = self.safe_integer(params, 'until') + if until is not None: + params = self.omit(params, 'until') + request['endTime'] = until + if limit is not None: + request['limit'] = limit + response = None + endpoint = None + endpoint, params = self.handle_option_and_params(params, 'fetchOHLCV', 'price') + if endpoint == 'index': + response = await self.commonGetQuoteV1IndexKlines(self.extend(request, params)) + # + # { + # "code": 200, + # "data": [ + # { + # "t": 1669155300000,//time + # "s": "ETHUSDT",// symbol + # "sn": "ETHUSDT",//symbol name + # "c": "1127.1",//Close price + # "h": "1130.81",//High price + # "l": "1126.17",//Low price + # "o": "1130.8",//Open price + # "v": "0"//Volume + # }, + # { + # "t": 1669156200000, + # "s": "ETHUSDT", + # "sn": "ETHUSDT", + # "c": "1129.44", + # "h": "1129.54", + # "l": "1127.1", + # "o": "1127.1", + # "v": "0" + # } + # ] + # } + # + elif endpoint == 'mark': + response = await self.commonGetQuoteV1MarkPriceKlines(self.extend(request, params)) + # + # { + # "code": 200, + # "data": [ + # { + # "symbol": "BTCUSDT",// Symbol + # "time": 1670157900000,// time + # "low": "16991.14096",//Low price + # "open": "16991.78288",//Open price + # "high": "16996.30641",// High prce + # "close": "16996.30641",// Close price + # "volume": "0",// Volume + # "curId": 1670157900000 + # } + # ] + # } + # + else: + response = await self.commonGetQuoteV1Klines(self.extend(request, params)) + # + # [ + # [ + # 1755540660000, + # "116399.99", + # "116399.99", + # "116360.09", + # "116360.1", + # "2.236869", + # 0, + # "260303.79722607", + # 22, + # "2.221061", + # "258464.10338267" + # ], + # ... + # + return self.parse_ohlcvs(response, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + return [ + self.safe_integer_n(ohlcv, [0, 'time', 't']), + self.safe_number_n(ohlcv, [1, 'open', 'o']), + self.safe_number_n(ohlcv, [2, 'high', 'h']), + self.safe_number_n(ohlcv, [3, 'low', 'l']), + self.safe_number_n(ohlcv, [4, 'close', 'c']), + self.safe_number_n(ohlcv, [5, 'volume', 'v']), + ] + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://toobit-docs.github.io/apidocs/spot/v1/en/#24hr-ticker-price-change-statistics + https://toobit-docs.github.io/apidocs/usdt_swap/v1/en/#24hr-ticker-price-change-statistics + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + type = None + market = None + request: dict = {} + if symbols is not None: + symbol = self.safe_string(symbols, 0) + market = self.market(symbol) + length = len(symbols) + if length == 1: + request['symbol'] = market['id'] + type, params = self.handle_market_type_and_params('fetchTickers', market, params) + response = None + if type == 'spot': + response = await self.commonGetQuoteV1Ticker24hr(self.extend(request, params)) + else: + response = await self.commonGetQuoteV1ContractTicker24hr(self.extend(request, params)) + # + # [ + # { + # "t": "1755601440162", + # "s": "GRDRUSDT", + # "o": "0.38", + # "h": "0.38", + # "l": "0.38", + # "c": "0.38", + # "v": "0", + # "qv": "0", + # "pc": "0", + # "pcp": "0" + # }, + # ... + # + return self.parse_tickers(response, symbols, params) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + marketId = self.safe_string(ticker, 's') + market = self.safe_market(marketId, market) + timestamp = self.safe_integer(ticker, 't') + last = self.safe_string(ticker, 'c') + return self.safe_ticker({ + 'symbol': market['symbol'], + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'h'), + 'low': self.safe_string(ticker, 'l'), + 'bid': None, + 'bidVolume': None, + 'ask': None, + 'askVolume': None, + 'vwap': None, + 'open': self.safe_string(ticker, 'o'), + 'close': last, + 'last': last, + 'previousClose': None, + 'change': self.safe_string(ticker, 'pc'), + 'percentage': self.safe_string(ticker, 'pcp'), + 'average': None, + 'baseVolume': self.safe_string(ticker, 'v'), + 'quoteVolume': self.safe_string(ticker, 'qv'), + 'info': ticker, + }, market) + + async def fetch_last_prices(self, symbols: Strings = None, params={}): + """ + fetches the last price for multiple markets + + https://toobit-docs.github.io/apidocs/spot/v1/en/#symbol-price-ticker + https://toobit-docs.github.io/apidocs/usdt_swap/v1/en/#symbol-price-ticker + + :param str[]|None symbols: unified symbols of the markets to fetch the last prices + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of lastprices structures + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + request: dict = {} + if symbols is not None: + length = len(symbols) + if length == 1: + market = self.market(symbols[0]) + request['symbol'] = market['id'] + response = await self.commonGetQuoteV1TickerPrice(self.extend(request, params)) + # + # [ + # { + # "s": "BNTUSDT", + # "si": "BNTUSDT", + # "p": "0.823" + # }, + # + return self.parse_last_prices(response, symbols) + + def parse_last_price(self, entry, market: Market = None): + marketId = self.safe_string(entry, 's') + market = self.safe_market(marketId, market) + return { + 'symbol': market['symbol'], + 'timestamp': None, + 'datetime': None, + 'price': self.safe_number_omit_zero(entry, 'price'), + 'side': None, + 'info': entry, + } + + async def fetch_bids_asks(self, symbols: Strings = None, params={}): + """ + fetches the bid and ask price and volume for multiple markets + + https://toobit-docs.github.io/apidocs/spot/v1/en/#symbol-order-book-ticker + https://toobit-docs.github.io/apidocs/usdt_swap/v1/en/#symbol-order-book-ticker + + :param str[] [symbols]: unified symbols of the markets to fetch the bids and asks for, all markets are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + request: dict = {} + if symbols is not None: + length = len(symbols) + if length == 1: + market = self.market(symbols[0]) + request['symbol'] = market['id'] + response = await self.commonGetQuoteV1TickerBookTicker(self.extend(request, params)) + # + # [ + # { + # "s": "GRDRUSDT", + # "b": "0", + # "bq": "0", + # "a": "0", + # "aq": "0", + # "t": "1755936610506" + # }, ... + # + return self.parse_bids_asks_custom(response, symbols) + + def parse_bids_asks_custom(self, tickers, symbols: Strings = None, params={}) -> Tickers: + results = [] + for i in range(0, len(tickers)): + parsedTicker = self.parse_bid_ask_custom(tickers[i]) + ticker = self.extend(parsedTicker, params) + results.append(ticker) + symbols = self.market_symbols(symbols) + return self.filter_by_array(results, 'symbol', symbols) + + def parse_bid_ask_custom(self, ticker): + return { + 'timestamp': self.safe_string(ticker, 't'), + 'symbol': self.safe_string(ticker, 's'), + 'bid': self.safe_number(ticker, 'b'), + 'bidVolume': self.safe_number(ticker, 'bq'), + 'ask': self.safe_number(ticker, 'a'), + 'askVolume': self.safe_number(ticker, 'aq'), + 'info': ticker, + } + + async def fetch_funding_rates(self, symbols: Strings = None, params={}) -> FundingRates: + """ + fetch the funding rate for multiple markets + + https://toobit-docs.github.io/apidocs/usdt_swap/v1/en/#funding-rate + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `funding rates structures `, indexe by market symbols + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + request: dict = {} + if symbols is not None: + length = len(symbols) + if length == 1: + market = self.market(symbols[0]) + request['symbol'] = market['id'] + response = await self.commonGetApiV1FuturesFundingRate(self.extend(request, params)) + # + # [ + # { + # "symbol": "BTC-SWAP-USDT", + # "rate": "0.0001071148112848", + # "nextFundingTime": "1755964800000" + # },... + # + return self.parse_funding_rates(response, symbols) + + def parse_funding_rate(self, contract, market: Market = None) -> FundingRate: + marketId = self.safe_string(contract, 'symbol') + symbol = self.safe_symbol(marketId, market) + nextFundingRate = self.safe_number(contract, 'rate') + nextFundingRateTimestamp = self.safe_integer(contract, 'nextFundingTime') + return { + 'info': contract, + 'symbol': symbol, + 'markPrice': None, + 'indexPrice': None, + 'interestRate': None, + 'estimatedSettlePrice': None, + 'timestamp': None, + 'datetime': None, + 'previousFundingRate': None, + 'nextFundingRate': None, + 'previousFundingTimestamp': None, + 'nextFundingTimestamp': None, + 'previousFundingDatetime': None, + 'nextFundingDatetime': None, + 'fundingRate': nextFundingRate, + 'fundingTimestamp': nextFundingRateTimestamp, + 'fundingDatetime': self.iso8601(nextFundingRateTimestamp), + 'interval': None, + } + + async def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical funding rate prices + + https://toobit-docs.github.io/apidocs/usdt_swap/v1/en/#get-funding-rate-history + + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: timestamp in ms of the earliest funding rate to fetch + :param int [limit]: the maximum amount of `funding rate structures ` to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest funding rate to fetch + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `funding rate structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchFundingRateHistory', 'paginate') + if paginate: + return await self.fetch_paginated_call_deterministic('fetchFundingRateHistory', symbol, since, limit, '8h', params) + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['limit'] = limit + response = await self.commonGetApiV1FuturesHistoryFundingRate(self.extend(request, params)) + # + # [ + # { + # "id": "869931", + # "symbol": "BTC-SWAP-USDT", + # "settleTime": "1755936000000", + # "settleRate": "0.0001" + # }, ... + # + return self.parse_funding_rate_histories(response, market, since, limit) + + def parse_funding_rate_history(self, contract, market: Market = None): + timestamp = self.safe_integer(contract, 'settleTime') + marketId = self.safe_string(contract, 'symbol') + return { + 'info': contract, + 'symbol': self.safe_symbol(marketId, market), + 'fundingRate': self.safe_number(contract, 'settleRate'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://toobit-docs.github.io/apidocs/spot/v1/en/#account-information-user_data + https://toobit-docs.github.io/apidocs/usdt_swap/v1/en/#futures-account-balance-user_data + + :param dict [params]: extra parameters specific to the exchange API endpointinvalid + :returns dict: a `balance structure ` + """ + await self.load_markets() + response = None + marketType: Str = None + marketType, params = self.handle_market_type_and_params('fetchBalance', None, params) + if self.in_array(marketType, ['swap', 'future']): + response = await self.privateGetApiV1FuturesBalance() + # + # [ + # { + # "asset": "USDT", # asset + # "balance": "999999999999.982", # total + # "availableBalance": "1899999999978.4995", # available balance Include unrealized pnl + # "positionMargin": "11.9825", #position Margin + # "orderMargin": "9.5", #order Margin + # "crossUnRealizedPnl": "10.01" #The unrealized profit and loss of cross position + # } + # ] + # + else: + response = await self.privateGetApiV1Account() + # + # { + # "userId": "912902020", + # "balances": [ + # { + # "asset": "ETH", + # "assetId": "ETH", + # "assetName": "ETH", + # "total": "0.025", + # "free": "0.025", + # "locked": "0" + # } + # ] + # } + # + return self.parse_balance(response) + + def parse_balance(self, response) -> Balances: + result: dict = { + 'info': response, + 'timestamp': None, + 'datetime': None, + } + balances = self.safe_list(response, 'balances', response) + for i in range(0, len(balances)): + balance = balances[i] + code = self.safe_currency_code(self.safe_string(balance, 'asset')) + account = self.account() + account['free'] = self.safe_string_2(balance, 'free', 'availableBalance') + account['total'] = self.safe_string_2(balance, 'total', 'balance') + account['used'] = self.safe_string(balance, 'locked') + result[code] = account + return self.safe_balance(result) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://toobit-docs.github.io/apidocs/spot/v1/en/#new-order-trade + https://toobit-docs.github.io/apidocs/usdt_swap/v1/en/#new-order-trade + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market', 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + request = {} + response = None + if market['spot']: + request, params = self.create_order_request(symbol, type, side, amount, price, params) + response = await self.privatePostApiV1SpotOrder(self.extend(request, params)) + else: + request, params = self.create_contract_order_request(symbol, type, side, amount, price, params) + response = await self.privatePostApiV1FuturesOrder(self.extend(request, params)) + # + # { + # "symbol": "ETHUSDT", + # "price": "0", + # "origQty": "0.001", + # "orderId": "2024837825254460160", + # "clientOrderId": "1756115478113679", + # "executedQty": "0", + # "status": "PENDING_NEW", + # "timeInForce": "GTC", + # "type": "MARKET", + # "side": "SELL" + # "accountId": "1783404067076253952", # only in spot + # "symbolName": "ETHUSDT", # only in spot + # "transactTime": "1756115478604", # only in spot + # "time": "1668418485058", # only in contract + # "updateTime": "1668418485058", # only in contract + # "leverage": "2", # only in contract + # "avgPrice": "0", # only in contract + # "marginLocked": "9.5", # only in contract + # "priceType": "INPUT" # only in contract + # } + # + return self.parse_order(response, market) + + def create_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + market = self.market(symbol) + id = market['id'] + request: dict = { + 'symbol': id, + 'side': side.upper(), + } + if price is not None: + request['price'] = self.price_to_precision(symbol, price) + cost: Str = None + cost, params = self.handle_param_string(params, 'cost') + if type == 'market': + if cost is None and side == 'buy': + raise ArgumentsRequired(self.id + ' createOrder() requires params["cost"] for market buy order') + else: + request['quantity'] = self.cost_to_precision(symbol, cost) + else: + request['quantity'] = self.amount_to_precision(symbol, amount) + isPostOnly = None + isPostOnly, params = self.handle_post_only(type == 'market', False, params) + if isPostOnly: + request['type'] = 'LIMIT_MAKER' + else: + request['type'] = type.upper() + return [request, params] + + def create_contract_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'quantity': self.amount_to_precision(symbol, amount), + } + reduceOnly = None + reduceOnly, params = self.handle_param_bool(params, 'reduceOnly') + if side == 'buy': + side = 'SELL_CLOSE' if reduceOnly else 'BUY_OPEN' + elif side == 'sell': + side = 'BUY_CLOSE' if reduceOnly else 'SELL_OPEN' + request['side'] = side + if price is not None: + request['price'] = self.price_to_precision(symbol, price) + if self.in_array(type, ['limit', 'LIMIT']): + request['type'] = type.upper() + request['price'] = self.price_to_precision(symbol, price) + elif type == 'market': + request['type'] = 'LIMIT' # weird, but exchange works self way + request['priceType'] = 'MARKET' + isPostOnly = None + isPostOnly, params = self.handle_post_only(type == 'market', False, params) + if isPostOnly: + request['timeInForce'] = 'LIMIT_MAKER' + values = self.handle_trigger_prices_and_params(symbol, params) + triggerPrice = values[0] + params = values[3] + if triggerPrice is not None: + request['stopPrice'] = triggerPrice + stopLoss = self.safe_dict(params, 'stopLoss') + takeProfit = self.safe_dict(params, 'takeProfit') + triggerPriceTypes = { + 'mark': 'MARK_PRICE', + 'last': 'CONTRACT_PRICE', + } + if stopLoss is not None: + request['stopLoss'] = self.safe_value(stopLoss, 'triggerPrice') + limitPrice = self.safe_value(stopLoss, 'price') + if limitPrice is not None: + request['slOrderType'] = 'LIMIT' + request['slLimitPrice'] = self.price_to_precision(symbol, limitPrice) + triggerPriceType = self.safe_string(stopLoss, 'triggerPriceType') + if triggerPriceType is not None: + request['slTriggerBy'] = self.safe_string(triggerPriceTypes, triggerPriceType, triggerPriceType) + params = self.omit(params, 'stopLoss') + if takeProfit is not None: + request['takeProfit'] = self.safe_value(takeProfit, 'triggerPrice') + limitPrice = self.safe_value(takeProfit, 'price') + if limitPrice is not None: + request['tpOrderType'] = 'LIMIT' + request['tpLimitPrice'] = self.price_to_precision(symbol, limitPrice) + triggerPriceType = self.safe_string(takeProfit, 'triggerPriceType') + if triggerPriceType is not None: + request['tpTriggerBy'] = self.safe_string(triggerPriceTypes, triggerPriceType, triggerPriceType) + params = self.omit(params, 'takeProfit') + if not ('newClientOrderId' in params): + request['newClientOrderId'] = self.uuid() + return [request, params] + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder, cancelOrder + # + # { + # "symbol": "ETHUSDT", + # "price": "0", + # "origQty": "0.001", + # "orderId": "2024837825254460160", + # "clientOrderId": "1756115478113679", + # "executedQty": "0", + # "status": "PENDING_NEW", + # "timeInForce": "GTC", + # "type": "MARKET", + # "side": "SELL" + # "accountId": "1783404067076253952", # only in spot + # "symbolName": "ETHUSDT", # only in spot + # "transactTime": "1756115478604", # only in spot + # "time": "1668418485058", # only in contract + # "updateTime": "1668418485058", # only in contract + # "leverage": "2", # only in contract + # "avgPrice": "0", # only in contract + # "marginLocked": "9.5", # only in contract + # "priceType": "INPUT" # only in contract + # } + # + # + # fetchOrder, fetchOrders, fetchOpenOrders + # + # { + # "time": "1756140208069", + # "updateTime": "1756140208078", + # "orderId": "2025045271033977089", + # "clientOrderId": "17561402075722006", + # "symbol": "ETHUSDT", + # "price": "3000", + # "origQty": "0.002", + # "executedQty": "0", + # "avgPrice": "0", + # "type": "LIMIT", + # "side": "BUY", + # "timeInForce": "GTC", + # "status": "NEW", + # "accountId": "1783404067076253952", # only in SPOT + # "exchangeId": "301", # only in SPOT + # "symbolName": "ETHUSDT", # only in SPOT + # "cummulativeQuoteQty": "0", # only in SPOT + # "cumulativeQuoteQty": "0", # only in SPOT + # "stopPrice": "0.0", # only in SPOT + # "icebergQty": "0.0", # only in SPOT + # "isWorking": True # only in SPOT + # "leverage": "2", # only in CONTRACT + # "marginLocked": "9.5", # only in CONTRACT + # "priceType": "INPUT" # only in CONTRACT + # "triggerType": "0", # only in CONTRACT fetchClosedOrders + # "fallType": "0", # only in CONTRACT fetchClosedOrders + # "activeStatus": "0" # only in CONTRACT fetchClosedOrders + # } + # + timestamp = self.safe_integer_2(order, 'transactTime', 'time') + marketId = self.safe_string(order, 'symbol') + market = self.safe_market(marketId, market) + rawType = self.safe_string(order, 'type') + rawSideLower = self.safe_string_lower(order, 'side') + triggerPrice = self.omit_zero(self.safe_string(order, 'stopPrice')) + if triggerPrice == '0.0': + triggerPrice = None + return self.safe_order({ + 'info': order, + 'id': self.safe_string(order, 'orderId'), + 'clientOrderId': self.safe_string(order, 'clientOrderId'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'lastUpdateTimestamp': self.safe_integer(order, 'updateTime'), + 'status': self.parse_order_status(self.safe_string(order, 'status')), + 'symbol': market['symbol'], + 'type': self.parse_order_type(rawType), + 'timeInForce': self.safe_string(order, 'timeInForce'), + 'postOnly': (rawType == 'LIMIT_MAKER'), + 'side': rawSideLower, + 'price': self.omit_zero(self.safe_string(order, 'price')), + 'triggerPrice': triggerPrice, + 'cost': self.omit_zero(self.safe_string(order, 'cumulativeQuoteQty')), + 'average': self.safe_string(order, 'avgPrice'), + 'amount': self.safe_string(order, 'origQty'), + 'filled': self.safe_string(order, 'executedQty'), + 'remaining': None, + 'trades': None, + 'fee': None, + 'marginMode': None, + 'reduceOnly': None, + 'leverage': None, + 'hedged': None, + }, market) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'PENDING_NEW': 'open', + 'NEW': 'open', + 'PARTIALLY_FILLED': 'open', + 'FILLED': 'closed', + 'PENDING_CANCEL': 'canceled', + 'CANCELED': 'canceled', + 'REJECTED': 'canceled', + } + return self.safe_string(statuses, status, status) + + def parse_order_type(self, status): + statuses: dict = { + 'MARKET': 'market', + 'LIMIT': 'limit', + 'LIMIT_MAKER': 'limit', + } + return self.safe_string(statuses, status, status) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://toobit-docs.github.io/apidocs/spot/v1/en/#cancel-order-trade + https://toobit-docs.github.io/apidocs/usdt_swap/v1/en/#cancel-order-trade + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + request = {} + if self.safe_string(params, 'clientOrderId') is None: + request['orderId'] = id + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + marketType = None + marketType, params = self.handle_market_type_and_params('cancelOrder', market, params, 'none') + if marketType == 'none': + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument or the "defaultType" parameter to be set to "spot" or "swap"') + response = None + if marketType == 'spot': + response = await self.privateDeleteApiV1SpotOrder(self.extend(request, params)) + else: + response = await self.privateDeleteApiV1FuturesOrder(self.extend(request, params)) + # response same `createOrder` + status = self.parse_order_status(self.safe_string(response, 'status')) + if status != 'open': + raise OrderNotFound(self.id + ' order ' + id + ' can not be canceled, ' + self.json(response)) + return self.parse_order(response, market) + + async def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders in a market + + https://toobit-docs.github.io/apidocs/spot/v1/en/#cancel-all-open-orders-trade + https://toobit-docs.github.io/apidocs/usdt_swap/v1/en/#cancel-orders-trade + + :param str symbol: unified symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + request = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + marketType = None + marketType, params = self.handle_market_type_and_params('cancelAllOrders', market, params, 'none') + if marketType == 'none': + raise ArgumentsRequired(self.id + ' cancelAllOrders() requires a symbol argument or the "defaultType" parameter to be set to "spot" or "swap"') + response = None + if marketType == 'spot': + response = await self.privateDeleteApiV1SpotOpenOrders(self.extend(request, params)) + # + # {"success":true} # always same response + # + else: + response = await self.privateDeleteApiV1FuturesBatchOrders(self.extend(request, params)) + # + # {"code": 200, "message":"success", "timestamp":1541161088303} + # + return [ + self.safe_order({ + 'info': response, + }), + ] + + async def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): + """ + cancel multiple orders + + https://toobit-docs.github.io/apidocs/spot/v1/en/#cancel-multiple-orders-trade + https://toobit-docs.github.io/apidocs/usdt_swap/v1/en/#cancel-multiple-orders-trade + + :param str[] ids: order ids + :param str [symbol]: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an list of `order structures ` + """ + await self.load_markets() + idsString = ','.join(ids) + request: dict = { + 'ids': idsString, + } + market = None + if symbol is not None: + market = self.market(symbol) + marketType = None + marketType, params = self.handle_market_type_and_params('cancelOrders', market, params, 'none') + if marketType == 'none': + raise ArgumentsRequired(self.id + ' cancelOrders() requires a symbol argument or the "defaultType" parameter to be set to "spot" or "swap"') + response = None + if marketType == 'spot': + response = await self.privateDeleteApiV1SpotCancelOrderByIds(self.extend(request, params)) + # + # {"success":true} # always same response + # + else: + response = await self.privateDeleteApiV1FuturesCancelOrderByIds(self.extend(request, params)) + # + # { + # "code":200, + # "result":[ + # { + # "orderId":"1327047813809448704", + # "code":-2013 + # }, + # { + # "orderId":"1327047814212101888", + # "code":-2013 + # } + # ] + # } + # + # or empty array if no orders were canceled + result = self.safe_list(response, 'result', []) + return self.parse_orders(result, market) + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://toobit-docs.github.io/apidocs/spot/v1/en/#query-order-user_data + https://toobit-docs.github.io/apidocs/usdt_swap/v1/en/#query-order-user_data + + :param str id: the order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrder() requires a symbol argument') + await self.load_markets() + request: dict = { + 'orderId': id, + } + market = self.market(symbol) + response = None + if market['spot']: + response = await self.privateGetApiV1SpotOrder(self.extend(request, params)) + else: + response = await self.privateGetApiV1FuturesOrder(self.extend(request, params)) + # + # { + # "time": "1756140208069", + # "updateTime": "1756140208078", + # "orderId": "2025045271033977089", + # "clientOrderId": "17561402075722006", + # "symbol": "ETHUSDT", + # "price": "3000", + # "origQty": "0.002", + # "executedQty": "0", + # "avgPrice": "0", + # "type": "LIMIT", + # "side": "BUY", + # "timeInForce": "GTC", + # "status": "NEW", + # "accountId": "1783404067076253952", # only in SPOT + # "exchangeId": "301", # only in SPOT + # "symbolName": "ETHUSDT", # only in SPOT + # "cummulativeQuoteQty": "0", # only in SPOT + # "cumulativeQuoteQty": "0", # only in SPOT + # "stopPrice": "0.0", # only in SPOT + # "icebergQty": "0.0", # only in SPOT + # "isWorking": True # only in SPOT + # "leverage": "2", # only in CONTRACT + # "marginLocked": "9.5", # only in CONTRACT + # "priceType": "INPUT" # only in CONTRACT + # } + # + return self.parse_order(response, market) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://toobit-docs.github.io/apidocs/spot/v1/en/#current-open-orders-user_data + https://toobit-docs.github.io/apidocs/usdt_swap/v1/en/#query-current-open-order-user_data + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + request = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if limit is not None: + request['limit'] = limit + marketType = None + marketType, params = self.handle_market_type_and_params('fetchOrders', market, params) + response = None + if marketType == 'spot': + response = await self.privateGetApiV1SpotOpenOrders(self.extend(request, params)) + # + # [ + # { + # "accountId": "1783404067076253952", + # "exchangeId": "301", + # "symbol": "ETHUSDT", + # "symbolName": "ETHUSDT", + # "clientOrderId": "17561415157172008", + # "orderId": "2025056244339984384", + # "price": "3000", + # "origQty": "0.002", + # "executedQty": "0", + # "cummulativeQuoteQty": "0", + # "cumulativeQuoteQty": "0", + # "avgPrice": "0", + # "status": "NEW", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "side": "BUY", + # "stopPrice": "0.0", + # "icebergQty": "0.0", + # "time": "1756141516189", + # "updateTime": "1756141516198", + # "isWorking": True + # }, ... + # ] + # + else: + response = await self.privateGetApiV1FuturesOpenOrders(self.extend(request, params)) + return self.parse_orders(response, market, since, limit) + + async def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://toobit-docs.github.io/apidocs/spot/v1/en/#all-orders-user_data + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + request = {} + if limit is not None: + request['limit'] = limit + if since is not None: + request['startTime'] = since + request, params = self.handle_until_option('endTime', request, params) + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + marketType = None + marketType, params = self.handle_market_type_and_params('fetchOrders', market, params) + response = None + if marketType == 'spot': + response = await self.privateGetApiV1SpotTradeOrders(request) + # + # [ + # { + # "accountId": "1783404067076253952", + # "exchangeId": "301", + # "symbol": "ETHUSDT", + # "symbolName": "ETHUSDT", + # "clientOrderId": "17561415157172008", + # "orderId": "2025056244339984384", + # "price": "3000", + # "origQty": "0.002", + # "executedQty": "0", + # "cummulativeQuoteQty": "0", + # "cumulativeQuoteQty": "0", + # "avgPrice": "0", + # "status": "NEW", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "side": "BUY", + # "stopPrice": "0.0", + # "icebergQty": "0.0", + # "time": "1756141516189", + # "updateTime": "1756141516198", + # "isWorking": True + # }, ... + # ] + # + else: + raise NotSupported(self.id + ' fetchOrders() is not supported for ' + marketType + ' markets') + return self.parse_orders(response, market, since, limit) + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://toobit-docs.github.io/apidocs/usdt_swap/v1/en/#query-history-orders-user_data + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + # returns the most recent closed or canceled orders up to circa two weeks ago + await self.load_markets() + request = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if since is not None: + request['startTime'] = since + request, params = self.handle_until_option('endTime', request, params) + marketType = None + marketType, params = self.handle_market_type_and_params('fetchClosedOrders', market, params) + response = None + if marketType == 'spot': + raise NotSupported(self.id + ' fetchOrders() is not supported for ' + marketType + ' markets') + else: + response = await self.privateGetApiV1FuturesHistoryOrders(request) + # + # [ + # { + # "time": "1756756879360", + # "updateTime": "1756757165956", + # "orderId": "2030218284767504128", + # "clientOrderId": "1756756876002", + # "symbol": "SOL-SWAP-USDT", + # "price": "144", + # "leverage": "50", + # "origQty": "1", + # "executedQty": "0", + # "executeQty": "0", + # "avgPrice": "0", + # "marginLocked": "0", + # "type": "LIMIT", + # "side": "BUY_OPEN", + # "timeInForce": "GTC", + # "status": "CANCELED", + # "priceType": "INPUT", + # "triggerType": "0", + # "fallType": "0", + # "activeStatus": "0" + # } + # ] + # + ordersList = [] + for i in range(0, len(response)): + ordersList.append({'result': response[i]}) + return self.parse_orders(ordersList, market, since, limit) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://toobit-docs.github.io/apidocs/spot/v1/en/#account-trade-list-user_data + https://toobit-docs.github.io/apidocs/usdt_swap/v1/en/#account-trade-list-user_data + + :param str [symbol]: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trade structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch trades for + :returns Trade[]: a list of `trade structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMyTrades() requires a symbol argument') + await self.load_markets() + request: dict = {} + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + market = self.market(symbol) + request['symbol'] = market['id'] + marketType = None + marketType, params = self.handle_market_type_and_params('fetchMyTrades', market, params) + request, params = self.handle_until_option('endTime', request, params) + response = None + if marketType == 'spot': + response = await self.privateGetApiV1AccountTrades(self.extend(request, params)) + # + # [ + # { + # "id": "2024934575206059008", + # "symbol": "ETHUSDT", + # "symbolName": "ETHUSDT", + # "orderId": "2024934575097029888", + # "price": "4641.21", + # "qty": "0.001", + # "commission": "0.00464121", + # "commissionAsset": "USDT", + # "time": "1756127012094", + # "isBuyer": False, + # "isMaker": False, + # "fee": { + # "feeCoinId": "USDT", + # "feeCoinName": "USDT", + # "fee": "0.00464121" + # }, + # "feeCoinId": "USDT", + # "feeAmount": "0.00464121", + # "makerRebate": "0", + # "ticketId": "4864450547563401875" + # }, ... + # + else: + response = await self.privateGetApiV1FuturesUserTrades(request) + # + # [ + # { + # "time": "1756758426899", + # "id": "2030231266499116032", + # "orderId": "2030231266373265152", + # "symbol": "DOGE-SWAP-USDT", + # "price": "0.21191", + # "qty": "63", + # "commissionAsset": "USDT", + # "commission": "0.00801019", + # "makerRebate": "0", + # "type": "LIMIT", + # "side": "BUY_OPEN", + # "realizedPnl": "0", + # "ticketId": "4900760819871364854", + # "isMaker": False + # } + # ] + # + return self.parse_trades(response, market, since, limit) + + async def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://open.big.one/docs/spot_transfer.html#transfer-of-user + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: 'spot', 'swap' + :param str toAccount: 'spot', 'swap' + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transfer structure ` + """ + await self.load_markets() + currency = self.currency(code) + accountsByType = self.safe_dict(self.options, 'accountsByType', {}) + fromId = self.safe_string(accountsByType, fromAccount, fromAccount) + toId = self.safe_string(accountsByType, toAccount, toAccount) + request: dict = { + 'asset': currency['id'], + 'quantity': self.currency_to_precision(code, amount), + 'fromAccountType': fromId, + 'toAccountType': toId, + } + response = await self.privatePostApiV1SubAccountTransfer(self.extend(request, params)) + # + # { + # "code": 200, # 200 = success + # "msg": "success" # response message + # } + # + return self.parse_transfer(response, currency) + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + # + # { + # "code": 200, # 200 = success + # "msg": "success" # response message + # } + # + return { + 'info': transfer, + 'id': None, + 'timestamp': None, + 'datetime': None, + 'currency': None, + 'amount': None, + 'fromAccount': None, + 'toAccount': None, + 'status': None, + } + + async def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + + https://toobit-docs.github.io/apidocs/spot/v1/en/#get-account-transaction-history-list-user_data + https://toobit-docs.github.io/apidocs/usdt_swap/v1/en/#get-future-account-transaction-history-list-user_data + + :param str [code]: unified currency code, default is None + :param int [since]: timestamp in ms of the earliest ledger entry, default is None + :param int [limit]: max number of ledger entries to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: end time in ms + :returns dict: a `ledger structure ` + """ + await self.load_markets() + currency = None + request: dict = {} + if code is not None: + currency = self.currency(code) + request['coin'] = currency['id'] + if since is not None: + request['startTime'] = since + request, params = self.handle_until_option('endTime', request, params) + if limit is not None: + request['limit'] = limit + marketType = None + marketType, params = self.handle_market_type_and_params('cancelAllOrders', None, params) + response = None + if marketType == 'spot': + response = await self.privateGetApiV1AccountBalanceFlow(self.extend(request, params)) + else: + response = await self.privateGetApiV1FuturesBalanceFlow(self.extend(request, params)) + # + # both answers are same format + # + # [ + # { + # "id": "539870570957903104", + # "accountId": "122216245228131", + # "coin": "BTC", + # "coinId": "BTC", + # "coinName": "BTC", + # "flowTypeValue": 51, + # "flowType": "USER_ACCOUNT_TRANSFER", + # "flowName": "Transfer", + # "change": "-12.5", + # "total": "379.624059937852365", + # "created": "1579093587214" + # }, + # + return self.parse_ledger(response, currency, since, limit) + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + currencyId = self.safe_string(item, 'coinId') + currency = self.safe_currency(currencyId, currency) + timestamp = self.safe_integer(item, 'created') + after = self.safe_number(item, 'total') + amountRaw = self.safe_string(item, 'change') + amount = self.parse_number(Precise.string_abs(amountRaw)) + direction = 'in' + if amountRaw.startswith('-'): + direction = 'out' + return self.safe_ledger_entry({ + 'info': item, + 'id': self.safe_string(item, 'id'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'direction': direction, + 'account': None, + 'referenceId': None, + 'referenceAccount': None, + 'type': self.parse_ledger_type(self.safe_string(item, 'flowType')), + 'currency': currency['code'], + 'amount': amount, + 'before': None, + 'after': after, + 'status': None, + 'fee': None, + }, currency) + + def parse_ledger_type(self, type): + types: dict = { + 'USER_ACCOUNT_TRANSFER': 'transfer', + 'AIRDROP': 'rebate', + } + return self.safe_string(types, type, type) + + async def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://toobit-docs.github.io/apidocs/usdt_swap/v1/en/#user-trade-fee-rate-user_data + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + await self.load_markets() + response = None + marketType = None + market = None + marketType, params = self.handle_market_type_and_params('fetchTradingFees', None, params) + if marketType == 'spot': + raise NotSupported(self.id + ' fetchTradingFees(): does not support ' + marketType + ' markets') + elif self.in_array(marketType, ['swap', 'future']): + symbol: Str = None + symbol, params = self.handle_param_string(params, 'symbol') + if symbol is None: + raise BadRequest(self.id + ' fetchTradingFees requires a params["symbol"]') + market = self.market(symbol) + request = { + 'symbol': market['id'], + } + response = await self.privateGetApiV1FuturesCommissionRate(self.extend(request, params)) + # + # { + # "openMakerFee": "0.000006", # The trade fee rate for opening pending orders + # "openTakerFee": "0.0001", # The trade fee rate for open position taker + # "closeMakerFee": "0.0002", # The trade fee rate for closing pending orders + # "closeTakerFee": "0.0004" # The trade fee rate for closing a taker order + # } + # + result: dict = {} + entry = response + marketId = self.safe_string(entry, 'symbol') + market = self.safe_market(marketId, market) + fee = self.parse_trading_fee(entry, market) + result[market['symbol']] = fee + return result + + def parse_trading_fee(self, data, market: Market = None): + marketId = self.safe_string(data, 'symbol') + return { + 'info': data, + 'symbol': self.safe_symbol(marketId, market), + 'maker': self.safe_number(data, 'closeMakerFee'), + 'taker': self.safe_number(data, 'closeTakerFee'), + 'percentage': None, + 'tierBased': None, + } + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://toobit-docs.github.io/apidocs/spot/v1/en/#deposit-history-user_data + + :param str [code]: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposit structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + return await self.fetch_deposits_or_withdrawals_helper('deposits', code, since, limit, params) + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://toobit-docs.github.io/apidocs/spot/v1/en/#withdrawal-records-user_data + + :param str [code]: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawal structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + return await self.fetch_deposits_or_withdrawals_helper('withdrawals', code, since, limit, params) + + async def fetch_deposits_or_withdrawals_helper(self, type, code, since, limit, params): + await self.load_markets() + currency = None + request: dict = {} + if code is not None: + currency = self.currency(code) + request['coin'] = currency['id'] + if since is not None: + request['startTime'] = since + request, params = self.handle_until_option('endTime', request, params) + if limit is not None: + request['limit'] = limit + response = None + if type == 'deposits': + response = await self.privateGetApiV1AccountDepositOrders(self.extend(request, params)) + # + # [ + # { + # "time": 1499865549590, + # "id": 100234, + # "coinName": "EOS", + # "statusCode": "DEPOSIT_CAN_WITHDRAW", + # "status": "2", # 2=SUCCESS, 11=REJECT, 12=AUDIT + # "address": "deposit2bb", + # "txId": "98A3EA560C6B3336D348B6C83F0F95ECE4F1F5919E94BD006E5BF3BF264FACFC", + # "txIdUrl": "", + # "requiredConfirmTimes": "5", + # "confirmTimes": "5", + # "quantity": "1.01", + # "coin": "EOS", + # "fromAddress": "clarkkent", + # "fromAddressTag": "19029901" + # "addressTag": "19012584", + # } + # ] + # + elif type == 'withdrawals': + response = await self.privateGetApiV1AccountWithdrawOrders(self.extend(request, params)) + # + # [ + # { + # "time":"1536232111669", + # "id ":"90161227158286336", + # "accountId":"517256161325920", + # "coinName":"BHC", + # "statusCode":"PROCESSING_STATUS", + # "status":3, + # "address":"0x815bF1c3cc0f49b8FC66B21A7e48fCb476051209", + # "txId ":"", + # "txIdUrl ":"", + # "requiredConfirmTimes ":0, # Number of confirmation requests + # "confirmTimes ":0, # number of confirmations + # "quantity":"14", # Withdrawal amount + # "coinId ":"BHC", + # "addressExt":"address tag", + # "arriveQuantity":"14", + # "walletHandleTime":"1536232111669", + # "feeCoinId ":"BHC", + # "feeCoinName ":"BHC", + # "fee":"0.1", + # "kernelId":"", # Exclusive to BEAM and GRIN + # "isInternalTransfer": False # Whether internal transfer + # } + # ] + # + return self.parse_transactions(response, currency, since, limit, params) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fetchDeposits & fetchWithdrawals + # + # { + # "time": 1499865549590, + # "id": 100234, + # "coinName": "EOS", + # "statusCode": "DEPOSIT_CAN_WITHDRAW", + # "status": "2", # 2=SUCCESS, 11=REJECT, 12=AUDIT + # "address": "deposit2bb", + # "txId": "98A3EA560C6B3336D348B6C83F0F95ECE4F1F5919E94BD006E5BF3BF264FACFC", + # "txIdUrl": "", + # "requiredConfirmTimes": "5", + # "confirmTimes": "5", + # "quantity": "1.01", + # "coin": "EOS", # present in "fetchDeposits" + # "coinId ":"BHC", # present in "fetchWithdrawals" + # "addressTag": "19012584", # present in "fetchDeposits" + # "addressExt":"address tag", # present in "fetchWithdrawals" + # "fromAddress": "clarkkent", # present in "fetchDeposits" + # "fromAddressTag": "19029901" # present in "fetchDeposits" + # "arriveQuantity":"14", # present in "fetchWithdrawals" + # "walletHandleTime":"1536232111669",// present in "fetchWithdrawals" + # "feeCoinId ":"BHC", # present in "fetchWithdrawals" + # "feeCoinName ":"BHC", # present in "fetchWithdrawals" + # "fee":"0.1", # present in "fetchWithdrawals" + # "kernelId":"", # present in "fetchWithdrawals" + # "isInternalTransfer": False # present in "fetchWithdrawals" + # } + # + # withdraw + # + # { + # "status": 0, + # "success": True, + # "needBrokerAudit": False, # Do you need a brokerage review? + # "id": "423885103582776064", + # "refuseReason":"" # failure rejection reason + # } + # + timestamp = self.safe_integer(transaction, 'time') + currencyId = self.safe_string_2(transaction, 'coin', 'coinId') + code = self.safe_currency_code(currencyId, currency) + feeString = self.safe_string(transaction, 'fee') + feeCoin = self.safe_string(transaction, 'feeCoinName') + fee = None + if feeString is not None: + fee = { + 'cost': self.parse_number(feeString), + 'currency': self.safe_currency_code(feeCoin), + } + tagTo = self.safe_string_2(transaction, 'addressTag', 'addressExt') + tagFrom = self.safe_string(transaction, 'fromAddressTag') + addressTo = self.safe_string(transaction, 'address') + addressFrom = self.safe_string(transaction, 'fromAddress') + isWithdraw = ('arriveQuantity' in transaction) + type = 'withdrawal' if isWithdraw else 'deposit' + return { + 'info': transaction, + 'id': self.safe_string(transaction, 'id'), + 'txid': self.safe_string(transaction, 'txId'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': None, + 'address': None, + 'addressTo': addressTo, + 'addressFrom': addressFrom, + 'tag': None, + 'tagTo': tagTo, + 'tagFrom': tagFrom, + 'type': type, + 'amount': self.safe_number(transaction, 'quantity'), + 'currency': code, + 'status': self.parse_transaction_status(self.safe_string(transaction, 'status')), + 'updated': None, + 'fee': fee, + 'comment': None, + 'internal': None, + } + + def parse_transaction_status(self, status: Str): + statuses: dict = { + '2': 'pending', + '12': 'pending', + '11': 'failed', + '3': 'ok', + } + return self.safe_string(statuses, status, status) + + async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://toobit-docs.github.io/apidocs/spot/v1/en/#deposit-address-user_data + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'coin': currency['id'], + } + networkCode, paramsOmitted = self.handle_network_code_and_params(self.extend(request, params)) + if networkCode is None: + raise ArgumentsRequired(self.id + ' fetchDepositAddress() : param["network"] is required') + request['chainType'] = self.network_code_to_id(networkCode) + response = await self.privateGetApiV1AccountDepositAddress(self.extend(request, paramsOmitted)) + # + # { + # "canDeposit":false,//Is it possible to recharge + # "address":"0x815bF1c3cc0f49b8FC66B21A7e48fCb476051209", + # "addressExt":"address tag", + # "minQuantity":"100",//minimum amount + # "requiredConfirmTimes ":1,//Arrival confirmation number + # "canWithdrawConfirmNum ":12,//Withdrawal confirmation number + # "coinType":"ERC20_TOKEN" + # } + # + return self.parse_deposit_address(response, currency) + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + address = self.safe_string(depositAddress, 'address') + self.check_address(address) + return { + 'info': depositAddress, + 'currency': self.safe_string(currency, 'code'), + 'network': None, + 'address': address, + 'tag': self.safe_string(depositAddress, 'addressExt'), + } + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://toobit-docs.github.io/apidocs/spot/v1/en/#withdraw-user_data + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: a memo for the transaction + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + self.check_address(address) + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + if networkCode is None: + raise ArgumentsRequired(self.id + ' withdraw() : param["network"] is required') + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'coin': currency['id'], + 'address': address, + 'quantity': self.currency_to_precision(currency['code'], amount), + 'network': networkCode, + } + if tag is not None: + request['addressExt'] = tag + response = await self.privatePostApiV1AccountWithdraw(self.extend(request, params)) + # + # { + # "status": 0, + # "success": True, + # "needBrokerAudit": False, # Do you need a brokerage review? + # "id": "423885103582776064", # Withdrawal successful order id + # "refuseReason":"" # failure rejection reason + # } + # + return self.parse_transaction(response, currency) + + async def set_margin_mode(self, marginMode: str, symbol: Str = None, params={}): + """ + set margin mode to 'cross' or 'isolated' + + https://toobit-docs.github.io/apidocs/usdt_swap/v1/en/#change-margin-type-trade + + :param str marginMode: 'cross' or 'isolated' + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setMarginMode() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + if market['type'] != 'swap': + raise BadSymbol(self.id + ' setMarginMode() supports swap contracts only') + marginMode = marginMode.upper() + request: dict = { + 'symbol': market['id'], + 'marginType': marginMode, + } + response = await self.privatePostApiV1FuturesMarginType(self.extend(request, params)) + # + # {"code":200,"symbolId":"BTC-SWAP-USDT","marginType":"ISOLATED"} + # + return response + + async def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://toobit-docs.github.io/apidocs/usdt_swap/v1/en/#change-initial-leverage-trade + + :param float leverage: the rate of leverage + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setLeverage() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'leverage': leverage, + } + response = await self.privatePostApiV1FuturesLeverage(self.extend(request, params)) + # + # {"code":200,"symbolId":"BTC-SWAP-USDT","leverage":"19"} + # + return response + + async def fetch_leverage(self, symbol: str, params={}) -> Leverage: + """ + fetch the set leverage for a market + + https://toobit-docs.github.io/apidocs/usdt_swap/v1/en/#get-the-leverage-multiple-and-position-mode-user_data + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `leverage structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = await self.privateGetApiV1FuturesAccountLeverage(self.extend(request, params)) + # + # [ + # { + # "symbol":"BTC-SWAP-USDT", #symbol + # "leverage":"20", # leverage + # "marginType":"CROSS" # CROSS;ISOLATED + # } + # ] + # + data = self.safe_dict(response, 'data', {}) + return self.parse_leverage(data, market) + + def parse_leverage(self, leverage: dict, market: Market = None) -> Leverage: + marketId = self.safe_string(leverage, 'symbol') + leverageValue = self.safe_integer(leverage, 'leverage') + marginType = self.safe_string(leverage, 'marginType') + marginMode = 'cross' if (marginType == 'crossed') else 'isolated' + return { + 'info': leverage, + 'symbol': self.safe_symbol(marketId, market), + 'marginMode': marginMode, + 'longLeverage': leverageValue, + 'shortLeverage': leverageValue, + } + + async def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://toobit-docs.github.io/apidocs/usdt_swap/v1/en/#query-position-user_data + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structures ` + """ + await self.load_markets() + request = {} + market = None + if symbols is not None: + length = len(symbols) + if length > 1: + raise BadRequest(self.id + ' fetchPositions() only accepts an array with a single symbol or without symbols argument') + firstSymbol = self.safe_string(symbols, 0) + if firstSymbol is not None: + market = self.market(firstSymbol) + request['symbol'] = market['id'] + response = await self.privateGetApiV1FuturesPositions(self.extend(request, params)) + # + # [ + # { + # "symbol": "DOGE-SWAP-USDT", + # "side": "LONG", + # "avgPrice": "0.21191", + # "position": "63", + # "available": "63", + # "leverage": "25", + # "lastPrice": "0.20932", + # "positionValue": "13.3503", + # "flp": "0.05471", + # "margin": "0.5262", + # "marginRate": "", + # "unrealizedPnL": "-0.1701", + # "profitRate": "-0.3185", + # "realizedPnL": "-0.008", + # "minMargin": "0", + # "maxNotionalValue": "10000000", + # "markPrice": "0.20921" + # } + # ] + # + return self.parse_positions(response, symbols) + + def parse_position(self, position: dict, market: Market = None): + marketId = self.safe_string(position, 'symbol') + market = self.safe_market(marketId, market) + side = self.safe_string_lower(position, 'side') + quantity = self.safe_string(position, 'position') + leverage = self.safe_integer(position, 'leverage') + return self.safe_position({ + 'info': position, + 'id': self.safe_string(position, 'id'), + 'symbol': market['symbol'], + 'entryPrice': self.safe_string(position, 'avgPrice'), + 'markPrice': self.safe_string(position, 'markPrice'), + 'lastPrice': self.safe_string(position, 'lastPrice'), + 'notional': self.safe_string(position, 'positionValue'), + 'collateral': None, + 'unrealizedPnl': self.safe_string(position, 'unrealizedPnL'), + 'side': side, + 'contracts': self.parse_number(quantity), + 'contractSize': None, + 'timestamp': None, + 'datetime': None, + 'hedged': None, + 'maintenanceMargin': None, + 'maintenanceMarginPercentage': None, + 'initialMargin': self.safe_string(position, 'margin'), + 'initialMarginPercentage': None, + 'leverage': leverage, + 'liquidationPrice': None, + 'marginRatio': None, + 'marginMode': None, + 'percentage': None, + }) + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = self.urls['api'][api] + '/' + self.implode_params(path, params) + isPost = method == 'POST' + isDelete = method == 'DELETE' + extraQuery = {} + query = self.omit(params, self.extract_params(path)) + if api != 'private': + # Public endpoints + if not isPost: + if query: + url += '?' + self.urlencode(query) + else: + self.check_required_credentials() + timestamp = self.milliseconds() + # Add timestamp to parameters for signed endpoints + extraQuery['recvWindow'] = self.safe_string(self.options, 'recvWindow', '5000') + extraQuery['timestamp'] = str(timestamp) + queryExtended = self.extend(query, extraQuery) + queryString = '' + if isPost or isDelete: + # everything else except Batch-Orders + if not isinstance(params, list): + body = self.urlencode(queryExtended) + else: + queryString = self.urlencode(extraQuery) + body = self.json(query) + else: + queryString = self.urlencode(queryExtended) + payload = queryString + if body is not None: + payload = body + payload + signature = self.hmac(self.encode(payload), self.encode(self.secret), hashlib.sha256, 'hex') + if queryString != '': + queryString += '&signature=' + signature + url += '?' + queryString + else: + body += '&signature=' + signature + headers = { + 'Referrer': 'CCXT', + 'X-BB-APIKEY': self.apiKey, + 'Content-Type': 'application/x-www-form-urlencoded', + } + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None + errorCode = self.safe_string(response, 'code') + message = self.safe_string(response, 'msg') + if errorCode and errorCode != '200' and errorCode != '0': + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + raise ExchangeError(feedback) + return None diff --git a/ccxt/async_support/upbit.py b/ccxt/async_support/upbit.py new file mode 100644 index 0000000..156f552 --- /dev/null +++ b/ccxt/async_support/upbit.py @@ -0,0 +1,2246 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.upbit import ImplicitAPI +from ccxt.base.types import Any, Balances, Currency, DepositAddress, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, OrderBooks, Trade, TradingFeeInterface, TradingFees, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import AddressPending +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class upbit(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(upbit, self).describe(), { + 'id': 'upbit', + 'name': 'Upbit', + 'countries': ['KR'], + 'version': 'v1', + 'rateLimit': 50, + 'pro': True, + # new metainfo interface + 'has': { + 'CORS': True, + 'spot': True, + 'margin': None, + 'swap': False, + 'future': False, + 'option': False, + 'cancelOrder': True, + 'createDepositAddress': True, + 'createMarketBuyOrderWithCost': True, + 'createMarketOrder': True, + 'createMarketOrderWithCost': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'editOrder': True, + 'fetchBalance': True, + 'fetchCanceledOrders': True, + 'fetchClosedOrders': True, + 'fetchCurrencies': False, + 'fetchDeposit': True, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': True, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchFundingHistory': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchIndexOHLCV': False, + 'fetchMarginMode': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMyTrades': False, + 'fetchOHLCV': True, + 'fetchOpenInterestHistory': False, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrderBooks': True, + 'fetchOrders': False, + 'fetchPositionMode': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTrades': True, + 'fetchTradingFee': True, + 'fetchTradingFees': True, + 'fetchTransactions': False, + 'fetchWithdrawal': True, + 'fetchWithdrawals': True, + 'transfer': False, + 'withdraw': True, + }, + 'timeframes': { + '1s': 'seconds', + '1m': 'minutes', + '3m': 'minutes', + '5m': 'minutes', + '10m': 'minutes', + '15m': 'minutes', + '30m': 'minutes', + '1h': 'minutes', + '4h': 'minutes', + '1d': 'days', + '1w': 'weeks', + '1M': 'months', + '1y': 'years', + }, + 'hostname': 'api.upbit.com', + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/49245610-eeaabe00-f423-11e8-9cba-4b0aed794799.jpg', + 'api': { + 'public': 'https://{hostname}', + 'private': 'https://{hostname}', + }, + 'www': 'https://upbit.com', + 'doc': 'https://docs.upbit.com/docs/%EC%9A%94%EC%B2%AD-%EC%88%98-%EC%A0%9C%ED%95%9C', + 'fees': 'https://upbit.com/service_center/guide', + }, + 'api': { + # 'endpoint','API Cost' + # cost = 1000 / (rateLimit * RPS) + 'public': { + 'get': { + 'market/all': 2, # RPS: 10 + 'candles/{timeframe}': 2, + 'candles/{timeframe}/{unit}': 2, + 'candles/seconds': 2, + 'candles/minutes/{unit}': 2, + 'candles/minutes/1': 2, + 'candles/minutes/3': 2, + 'candles/minutes/5': 2, + 'candles/minutes/10': 2, + 'candles/minutes/15': 2, + 'candles/minutes/30': 2, + 'candles/minutes/60': 2, + 'candles/minutes/240': 2, + 'candles/days': 2, + 'candles/weeks': 2, + 'candles/months': 2, + 'candles/years': 2, + 'trades/ticks': 2, + 'ticker': 2, + 'ticker/all': 2, + 'orderbook': 2, + 'orderbook/instruments': 2, + 'orderbook/supported_levels': 2, # Upbit KR only, deprecatd + }, + }, + 'private': { + 'get': { + 'accounts': 0.67, # RPS: 30 + 'orders/chance': 0.67, + 'order': 0.67, + 'orders/closed': 0.67, + 'orders/open': 0.67, + 'orders/uuids': 0.67, + 'withdraws': 0.67, + 'withdraw': 0.67, + 'withdraws/chance': 0.67, + 'withdraws/coin_addresses': 0.67, + 'deposits': 0.67, + 'deposits/chance/coin': 0.67, + 'deposit': 0.67, + 'deposits/coin_addresses': 0.67, + 'deposits/coin_address': 0.67, + 'travel_rule/vasps': 0.67, + 'status/wallet': 0.67, # Upbit KR only + 'api_keys': 0.67, # Upbit KR only + }, + 'post': { + 'orders': 2.5, # RPS: 8 + 'orders/cancel_and_new': 2.5, # RPS: 8 + 'withdraws/coin': 0.67, + 'withdraws/krw': 0.67, # Upbit KR only. + 'deposits/krw': 0.67, # Upbit KR only. + 'deposits/generate_coin_address': 0.67, + 'travel_rule/deposit/uuid': 0.67, # RPS: 30, but each deposit can only be queried once every 10 minutes + 'travel_rule/deposit/txid': 0.67, # RPS: 30, but each deposit can only be queried once every 10 minutes + }, + 'delete': { + 'order': 0.67, + 'orders/open': 40, # RPS: 0.5 + 'orders/uuids': 0.67, + }, + }, + }, + 'fees': { + 'trading': { + 'tierBased': False, + 'percentage': True, + 'maker': self.parse_number('0.0025'), + 'taker': self.parse_number('0.0025'), + }, + 'funding': { + 'tierBased': False, + 'percentage': False, + 'withdraw': {}, + 'deposit': {}, + }, + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': False, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'leverage': False, + 'marketBuyByCost': False, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': True, + 'trailing': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': None, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': True, + 'limit': 100, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, # todo + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 100000, + 'daysBackCanceled': 1, + 'untilDays': 7, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 200, + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'exact': { + 'This key has expired.': AuthenticationError, + 'Missing request parameter error. Check the required parameters!': BadRequest, + 'side is missing, side does not have a valid value': InvalidOrder, + }, + 'broad': { + 'thirdparty_agreement_required': PermissionDenied, + 'out_of_scope': PermissionDenied, + 'order_not_found': OrderNotFound, + 'insufficient_funds': InsufficientFunds, + 'invalid_access_key': AuthenticationError, + 'jwt_verification': AuthenticationError, + 'create_ask_error': ExchangeError, + 'create_bid_error': ExchangeError, + 'volume_too_large': InvalidOrder, + 'invalid_funds': InvalidOrder, + }, + }, + 'options': { + 'createMarketBuyOrderRequiresPrice': True, + 'tradingFeesByQuoteCurrency': { + 'KRW': 0.0005, + }, + }, + 'commonCurrencies': { + 'TON': 'Tokamak Network', + }, + }) + + async def fetch_currency(self, code: str, params={}): + # self method is for retrieving funding fees and limits per currency + # it requires private access and API keys properly set up + await self.load_markets() + currency = self.currency(code) + return await self.fetch_currency_by_id(currency['id'], params) + + async def fetch_currency_by_id(self, id: str, params={}): + # self method is for retrieving funding fees and limits per currency + # it requires private access and API keys properly set up + request: dict = { + 'currency': id, + } + response = await self.privateGetWithdrawsChance(self.extend(request, params)) + # + # { + # "member_level": { + # "security_level": 3, + # "fee_level": 0, + # "email_verified": True, + # "identity_auth_verified": True, + # "bank_account_verified": True, + # "kakao_pay_auth_verified": False, + # "locked": False, + # "wallet_locked": False + # }, + # "currency": { + # "code": "BTC", + # "withdraw_fee": "0.0005", + # "is_coin": True, + # "wallet_state": "working", + # "wallet_support": ["deposit", "withdraw"] + # }, + # "account": { + # "currency": "BTC", + # "balance": "10.0", + # "locked": "0.0", + # "avg_krw_buy_price": "8042000", + # "modified": False + # }, + # "withdraw_limit": { + # "currency": "BTC", + # "minimum": null, + # "onetime": null, + # "daily": "10.0", + # "remaining_daily": "10.0", + # "remaining_daily_krw": "0.0", + # "fixed": null, + # "can_withdraw": True + # } + # } + # + memberInfo = self.safe_value(response, 'member_level', {}) + currencyInfo = self.safe_value(response, 'currency', {}) + withdrawLimits = self.safe_value(response, 'withdraw_limit', {}) + canWithdraw = self.safe_value(withdrawLimits, 'can_withdraw') + walletState = self.safe_string(currencyInfo, 'wallet_state') + walletLocked = self.safe_value(memberInfo, 'wallet_locked') + locked = self.safe_value(memberInfo, 'locked') + active = True + if (canWithdraw is not None) and not canWithdraw: + active = False + elif walletState != 'working': + active = False + elif (walletLocked is not None) and walletLocked: + active = False + elif (locked is not None) and locked: + active = False + maxOnetimeWithdrawal = self.safe_string(withdrawLimits, 'onetime') + maxDailyWithdrawal = self.safe_string(withdrawLimits, 'daily', maxOnetimeWithdrawal) + remainingDailyWithdrawal = self.safe_string(withdrawLimits, 'remaining_daily', maxDailyWithdrawal) + maxWithdrawLimit = None + if Precise.string_gt(remainingDailyWithdrawal, '0'): + maxWithdrawLimit = remainingDailyWithdrawal + else: + maxWithdrawLimit = maxDailyWithdrawal + currencyId = self.safe_string(currencyInfo, 'code') + code = self.safe_currency_code(currencyId) + return { + 'info': response, + 'id': currencyId, + 'code': code, + 'name': code, + 'active': active, + 'fee': self.safe_number(currencyInfo, 'withdraw_fee'), + 'precision': None, + 'limits': { + 'withdraw': { + 'min': self.safe_number(withdrawLimits, 'minimum'), + 'max': self.parse_number(maxWithdrawLimit), + }, + }, + } + + async def fetch_market(self, symbol: str, params={}): + # self method is for retrieving trading fees and limits per market + # it requires private access and API keys properly set up + await self.load_markets() + market = self.market(symbol) + return await self.fetch_market_by_id(market['id'], params) + + async def fetch_market_by_id(self, id: str, params={}): + # self method is for retrieving trading fees and limits per market + # it requires private access and API keys properly set up + request: dict = { + 'market': id, + } + response = await self.privateGetOrdersChance(self.extend(request, params)) + # + # { + # "bid_fee": "0.0015", + # "ask_fee": "0.0015", + # "market": { + # "id": "KRW-BTC", + # "name": "BTC/KRW", + # "order_types": ["limit"], + # "order_sides": ["ask", "bid"], + # "bid": {"currency": "KRW", "price_unit": null, "min_total": 1000}, + # "ask": {"currency": "BTC", "price_unit": null, "min_total": 1000}, + # "max_total": "100000000.0", + # "state": "active", + # }, + # "bid_account": { + # "currency": "KRW", + # "balance": "0.0", + # "locked": "0.0", + # "avg_buy_price": "0", + # "avg_buy_price_modified": False, + # "unit_currency": "KRW", + # }, + # "ask_account": { + # "currency": "BTC", + # "balance": "10.0", + # "locked": "0.0", + # "avg_buy_price": "8042000", + # "avg_buy_price_modified": False, + # "unit_currency": "KRW", + # } + # } + # + marketInfo = self.safe_value(response, 'market') + bid = self.safe_value(marketInfo, 'bid') + ask = self.safe_value(marketInfo, 'ask') + marketId = self.safe_string(marketInfo, 'id') + baseId = self.safe_string(ask, 'currency') + quoteId = self.safe_string(bid, 'currency') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + state = self.safe_string(marketInfo, 'state') + bidFee = self.safe_string(response, 'bid_fee') + askFee = self.safe_string(response, 'ask_fee') + fee = self.parse_number(Precise.string_max(bidFee, askFee)) + return self.safe_market_structure({ + 'id': marketId, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': (state == 'active'), + 'contract': False, + 'linear': None, + 'inverse': None, + 'taker': fee, + 'maker': fee, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number('1e-8'), + 'price': self.parse_number('1e-8'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(ask, 'min_total'), + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': self.safe_number(bid, 'min_total'), + 'max': self.safe_number(marketInfo, 'max_total'), + }, + 'info': response, + }, + }) + + async def fetch_markets(self, params={}) -> List[Market]: + """ + + https://docs.upbit.com/kr/reference/마켓-코드-조회 + https://global-docs.upbit.com/reference/listing-market-list + + retrieves data on all markets for upbit + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = await self.publicGetMarketAll(params) + # + # [ + # { + # "market": "KRW-BTC", + # "korean_name": "비트코인", + # "english_name": "Bitcoin" + # }, + # ..., + # ] + # + return self.parse_markets(response) + + def parse_market(self, market: dict) -> Market: + id = self.safe_string(market, 'market') + quoteId, baseId = id.split('-') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + return self.safe_market_structure({ + 'id': id, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': True, + 'contract': False, + 'linear': None, + 'inverse': None, + 'taker': self.safe_number(self.options['tradingFeesByQuoteCurrency'], quote, self.fees['trading']['taker']), + 'maker': self.safe_number(self.options['tradingFeesByQuoteCurrency'], quote, self.fees['trading']['maker']), + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'price': self.parse_number('1e-8'), + 'amount': self.parse_number('1e-8'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': None, + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + 'info': market, + }) + + def parse_balance(self, response) -> Balances: + result: dict = { + 'info': response, + 'timestamp': None, + 'datetime': None, + } + for i in range(0, len(response)): + balance = response[i] + currencyId = self.safe_string(balance, 'currency') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(balance, 'balance') + account['used'] = self.safe_string(balance, 'locked') + result[code] = account + return self.safe_balance(result) + + async def fetch_balance(self, params={}) -> Balances: + """ + + https://docs.upbit.com/kr/reference/전체-계좌-조회 + https://global-docs.upbit.com/reference/overall-account-inquiry + + query for balance and get the amount of funds available for trading or funds locked in orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + response = await self.privateGetAccounts(params) + # + # [{ currency: "BTC", + # "balance": "0.005", + # "locked": "0.0", + # "avg_krw_buy_price": "7446000", + # "modified": False }, + # { currency: "ETH", + # "balance": "0.1", + # "locked": "0.0", + # "avg_krw_buy_price": "250000", + # "modified": False } ] + # + return self.parse_balance(response) + + async def fetch_order_books(self, symbols: Strings = None, limit: Int = None, params={}) -> OrderBooks: + """ + + https://docs.upbit.com/kr/reference/호가-정보-조회 + https://global-docs.upbit.com/reference/order-book-list + + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data for multiple markets + :param str[]|None symbols: list of unified market symbols, all symbols fetched if None, default is None + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `order book structures ` indexed by market symbol + """ + await self.load_markets() + ids = None + if symbols is None: + ids = ','.join(self.ids) + else: + ids = self.market_ids(symbols) + ids = ','.join(ids) + request: dict = { + 'markets': ids, + # 'count': limit, + } + if limit is not None: + request['count'] = limit + response = await self.publicGetOrderbook(self.extend(request, params)) + # + # [{ market: "BTC-ETH", + # "timestamp": 1542899030043, + # "total_ask_size": 109.57065201, + # "total_bid_size": 125.74430631, + # "orderbook_units": [{ask_price: 0.02926679, + # "bid_price": 0.02919904, + # "ask_size": 4.20293961, + # "bid_size": 11.65043576}, + # ..., + # {ask_price: 0.02938209, + # "bid_price": 0.0291231, + # "ask_size": 0.05135782, + # "bid_size": 13.5595 } ]}, + # { market: "KRW-BTC", + # "timestamp": 1542899034662, + # "total_ask_size": 12.89790974, + # "total_bid_size": 4.88395783, + # "orderbook_units": [{ask_price: 5164000, + # "bid_price": 5162000, + # "ask_size": 2.57606495, + # "bid_size": 0.214 }, + # ..., + # {ask_price: 5176000, + # "bid_price": 5152000, + # "ask_size": 2.752, + # "bid_size": 0.4650305} ]} ] + # + result: dict = {} + for i in range(0, len(response)): + orderbook = response[i] + marketId = self.safe_string(orderbook, 'market') + symbol = self.safe_symbol(marketId, None, '-') + timestamp = self.safe_integer(orderbook, 'timestamp') + result[symbol] = { + 'symbol': symbol, + 'bids': self.sort_by(self.parse_bids_asks(orderbook['orderbook_units'], 'bid_price', 'bid_size'), 0, True), + 'asks': self.sort_by(self.parse_bids_asks(orderbook['orderbook_units'], 'ask_price', 'ask_size'), 0), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'nonce': None, + } + return result + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + + https://docs.upbit.com/kr/reference/호가-정보-조회 + https://global-docs.upbit.com/reference/order-book-list + + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + orderbooks = await self.fetch_order_books([symbol], limit, params) + return self.safe_value(orderbooks, symbol) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { market: "BTC-ETH", + # "trade_date": "20181122", + # "trade_time": "104543", + # "trade_date_kst": "20181122", + # "trade_time_kst": "194543", + # "trade_timestamp": 1542883543096, + # "opening_price": 0.02976455, + # "high_price": 0.02992577, + # "low_price": 0.02934283, + # "trade_price": 0.02947773, + # "prev_closing_price": 0.02966, + # "change": "FALL", + # "change_price": 0.00018227, + # "change_rate": 0.0061453136, + # "signed_change_price": -0.00018227, + # "signed_change_rate": -0.0061453136, + # "trade_volume": 1.00000005, + # "acc_trade_price": 100.95825586, + # "acc_trade_price_24h": 289.58650166, + # "acc_trade_volume": 3409.85311036, + # "acc_trade_volume_24h": 9754.40510513, + # "highest_52_week_price": 0.12345678, + # "highest_52_week_date": "2018-02-01", + # "lowest_52_week_price": 0.023936, + # "lowest_52_week_date": "2017-12-08", + # "timestamp": 1542883543813 } + # + timestamp = self.safe_integer(ticker, 'trade_timestamp') + marketId = self.safe_string_2(ticker, 'market', 'code') + market = self.safe_market(marketId, market, '-') + last = self.safe_string(ticker, 'trade_price') + return self.safe_ticker({ + 'symbol': market['symbol'], + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high_price'), + 'low': self.safe_string(ticker, 'low_price'), + 'bid': None, + 'bidVolume': None, + 'ask': None, + 'askVolume': None, + 'vwap': None, + 'open': self.safe_string(ticker, 'opening_price'), + 'close': last, + 'last': last, + 'previousClose': self.safe_string(ticker, 'prev_closing_price'), + 'change': self.safe_string(ticker, 'signed_change_price'), + 'percentage': self.safe_string(ticker, 'signed_change_rate'), + 'average': None, + 'baseVolume': self.safe_string(ticker, 'acc_trade_volume_24h'), + 'quoteVolume': self.safe_string(ticker, 'acc_trade_price_24h'), + 'info': ticker, + }, market) + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + + https://docs.upbit.com/kr/reference/ticker현재가-정보 + https://global-docs.upbit.com/reference/tickers + + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + ids = None + if symbols is None: + ids = ','.join(self.ids) + else: + ids = self.market_ids(symbols) + ids = ','.join(ids) + request: dict = { + 'markets': ids, + } + response = await self.publicGetTicker(self.extend(request, params)) + # + # [{ market: "BTC-ETH", + # "trade_date": "20181122", + # "trade_time": "104543", + # "trade_date_kst": "20181122", + # "trade_time_kst": "194543", + # "trade_timestamp": 1542883543097, + # "opening_price": 0.02976455, + # "high_price": 0.02992577, + # "low_price": 0.02934283, + # "trade_price": 0.02947773, + # "prev_closing_price": 0.02966, + # "change": "FALL", + # "change_price": 0.00018227, + # "change_rate": 0.0061453136, + # "signed_change_price": -0.00018227, + # "signed_change_rate": -0.0061453136, + # "trade_volume": 1.00000005, + # "acc_trade_price": 100.95825586, + # "acc_trade_price_24h": 289.58650166, + # "acc_trade_volume": 3409.85311036, + # "acc_trade_volume_24h": 9754.40510513, + # "highest_52_week_price": 0.12345678, + # "highest_52_week_date": "2018-02-01", + # "lowest_52_week_price": 0.023936, + # "lowest_52_week_date": "2017-12-08", + # "timestamp": 1542883543813 }] + # + result: dict = {} + for t in range(0, len(response)): + ticker = self.parse_ticker(response[t]) + symbol = ticker['symbol'] + result[symbol] = ticker + return self.filter_by_array_tickers(result, 'symbol', symbols) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + + https://docs.upbit.com/kr/reference/ticker현재가-정보 + https://global-docs.upbit.com/reference/tickers + + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + tickers = await self.fetch_tickers([symbol], params) + return self.safe_value(tickers, symbol) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades + # + # { market: "BTC-ETH", + # "trade_date_utc": "2018-11-22", + # "trade_time_utc": "13:55:24", + # "timestamp": 1542894924397, + # "trade_price": 0.02914289, + # "trade_volume": 0.20074397, + # "prev_closing_price": 0.02966, + # "change_price": -0.00051711, + # "ask_bid": "ASK", + # "sequential_id": 15428949259430000} + # + # fetchOrder trades + # + # { + # "market": "KRW-BTC", + # "uuid": "78162304-1a4d-4524-b9e6-c9a9e14d76c3", + # "price": "101000.0", + # "volume": "0.77368323", + # "funds": "78142.00623", + # "ask_fee": "117.213009345", + # "bid_fee": "117.213009345", + # "created_at": "2018-04-05T14:09:15+09:00", + # "side": "bid", + # } + # + id = self.safe_string_2(trade, 'sequential_id', 'uuid') + orderId = None + timestamp = self.safe_integer(trade, 'timestamp') + if timestamp is None: + timestamp = self.parse8601(self.safe_string(trade, 'created_at')) + side = None + askOrBid = self.safe_string_lower_2(trade, 'ask_bid', 'side') + if askOrBid == 'ask': + side = 'sell' + elif askOrBid == 'bid': + side = 'buy' + cost = self.safe_string(trade, 'funds') + price = self.safe_string_2(trade, 'trade_price', 'price') + amount = self.safe_string_2(trade, 'trade_volume', 'volume') + marketId = self.safe_string_2(trade, 'market', 'code') + market = self.safe_market(marketId, market, '-') + fee = None + feeCost = self.safe_string(trade, askOrBid + '_fee') + if feeCost is not None: + fee = { + 'currency': market['quote'], + 'cost': feeCost, + } + return self.safe_trade({ + 'id': id, + 'info': trade, + 'order': orderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'type': None, + 'side': side, + 'takerOrMaker': None, + 'price': price, + 'amount': amount, + 'cost': cost, + 'fee': fee, + }, market) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + + https://docs.upbit.com/kr/reference/최근-체결-내역 + https://global-docs.upbit.com/reference/today-trades-history + + get the list of most recent trades for a particular symbol + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + if limit is None: + limit = 200 + request: dict = { + 'market': market['id'], + 'count': limit, + } + response = await self.publicGetTradesTicks(self.extend(request, params)) + # + # [{ market: "BTC-ETH", + # "trade_date_utc": "2018-11-22", + # "trade_time_utc": "13:55:24", + # "timestamp": 1542894924397, + # "trade_price": 0.02914289, + # "trade_volume": 0.20074397, + # "prev_closing_price": 0.02966, + # "change_price": -0.00051711, + # "ask_bid": "ASK", + # "sequential_id": 15428949259430000}, + # { market: "BTC-ETH", + # "trade_date_utc": "2018-11-22", + # "trade_time_utc": "13:03:10", + # "timestamp": 1542891790123, + # "trade_price": 0.02917, + # "trade_volume": 7.392, + # "prev_closing_price": 0.02966, + # "change_price": -0.00049, + # "ask_bid": "ASK", + # "sequential_id": 15428917910540000} ] + # + return self.parse_trades(response, market, since, limit) + + async def fetch_trading_fee(self, symbol: str, params={}) -> TradingFeeInterface: + """ + + https://docs.upbit.com/kr/reference/주문-가능-정보 + https://global-docs.upbit.com/reference/available-order-information + + fetch the trading fees for a market + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `fee structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + response = await self.privateGetOrdersChance(self.extend(request, params)) + # + # { + # "bid_fee": "0.0005", + # "ask_fee": "0.0005", + # "maker_bid_fee": "0.0005", + # "maker_ask_fee": "0.0005", + # "market": { + # "id": "KRW-BTC", + # "name": "BTC/KRW", + # "order_types": ["limit"], + # "order_sides": ["ask", "bid"], + # "bid": {"currency": "KRW", "price_unit": null, "min_total": 5000}, + # "ask": {"currency": "BTC", "price_unit": null, "min_total": 5000}, + # "max_total": "1000000000.0", + # "state": "active" + # }, + # "bid_account": { + # "currency": "KRW", + # "balance": "0.34202415", + # "locked": "4999.99999922", + # "avg_buy_price": "0", + # "avg_buy_price_modified": True, + # "unit_currency": "KRW" + # }, + # "ask_account": { + # "currency": "BTC", + # "balance": "0.00048", + # "locked": "0.0", + # "avg_buy_price": "20870000", + # "avg_buy_price_modified": False, + # "unit_currency": "KRW" + # } + # } + # + askFee = self.safe_string(response, 'ask_fee') + bidFee = self.safe_string(response, 'bid_fee') + taker = Precise.string_max(askFee, bidFee) + makerAskFee = self.safe_string(response, 'maker_ask_fee') + makerBidFee = self.safe_string(response, 'maker_bid_fee') + maker = Precise.string_max(makerAskFee, makerBidFee) + return { + 'info': response, + 'symbol': symbol, + 'maker': self.parse_number(maker), + 'taker': self.parse_number(taker), + 'percentage': True, + 'tierBased': False, + } + + async def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for markets + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `trading fee structure ` + """ + await self.load_markets() + fetchMarketResponse = await self.fetch_markets(params) + response: dict = {} + for i in range(0, len(fetchMarketResponse)): + element: dict = {} + element['maker'] = self.safe_number(fetchMarketResponse[i], 'maker') + element['taker'] = self.safe_number(fetchMarketResponse[i], 'taker') + element['symbol'] = self.safe_string(fetchMarketResponse[i], 'symbol') + element['percentage'] = True + element['tierBased'] = False + element['info'] = fetchMarketResponse[i] + response[self.safe_string(fetchMarketResponse[i], 'symbol')] = element + return response + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # { + # "market": "BTC-ETH", + # "candle_date_time_utc": "2018-11-22T13:47:00", + # "candle_date_time_kst": "2018-11-22T22:47:00", + # "opening_price": 0.02915963, + # "high_price": 0.02915963, + # "low_price": 0.02915448, + # "trade_price": 0.02915448, + # "timestamp": 1542894473674, + # "candle_acc_trade_price": 0.0981629437535248, + # "candle_acc_trade_volume": 3.36693173, + # "unit": 1 + # } + # + return [ + self.parse8601(self.safe_string(ohlcv, 'candle_date_time_utc')), + self.safe_number(ohlcv, 'opening_price'), + self.safe_number(ohlcv, 'high_price'), + self.safe_number(ohlcv, 'low_price'), + self.safe_number(ohlcv, 'trade_price'), + self.safe_number(ohlcv, 'candle_acc_trade_volume'), # base volume + ] + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + + https://docs.upbit.com/kr/reference/분minute-캔들-1 + https://global-docs.upbit.com/reference/minutes + + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + timeframePeriod = self.parse_timeframe(timeframe) + timeframeValue = self.safe_string(self.timeframes, timeframe, timeframe) + if limit is None: + limit = 200 + request: dict = { + 'market': market['id'], + 'timeframe': timeframeValue, + 'count': limit, + } + response = None + if since is not None: + # convert `since` to `to` value + request['to'] = self.iso8601(self.sum(since, timeframePeriod * limit * 1000)) + if timeframeValue == 'minutes': + numMinutes = int(round(timeframePeriod / 60)) + request['unit'] = numMinutes + response = await self.publicGetCandlesTimeframeUnit(self.extend(request, params)) + else: + response = await self.publicGetCandlesTimeframe(self.extend(request, params)) + # + # [ + # { + # "market": "BTC-ETH", + # "candle_date_time_utc": "2018-11-22T13:47:00", + # "candle_date_time_kst": "2018-11-22T22:47:00", + # "opening_price": 0.02915963, + # "high_price": 0.02915963, + # "low_price": 0.02915448, + # "trade_price": 0.02915448, + # "timestamp": 1542894473674, + # "candle_acc_trade_price": 0.0981629437535248, + # "candle_acc_trade_volume": 3.36693173, + # "unit": 1 + # }, + # { + # "market": "BTC-ETH", + # "candle_date_time_utc": "2018-11-22T10:06:00", + # "candle_date_time_kst": "2018-11-22T19:06:00", + # "opening_price": 0.0294, + # "high_price": 0.02940882, + # "low_price": 0.02934283, + # "trade_price": 0.02937354, + # "timestamp": 1542881219276, + # "candle_acc_trade_price": 0.0762597110943884, + # "candle_acc_trade_volume": 2.5949617, + # "unit": 1 + # } + # ] + # + return self.parse_ohlcvs(response, market, timeframe, since, limit) + + def calc_order_price(self, symbol: str, amount: float, price: Num = None, params={}) -> str: + quoteAmount = None + createMarketBuyOrderRequiresPrice = self.safe_value(self.options, 'createMarketBuyOrderRequiresPrice') + cost = self.safe_string(params, 'cost') + if cost is not None: + quoteAmount = self.cost_to_precision(symbol, cost) + elif createMarketBuyOrderRequiresPrice: + if price is None or amount is None: + raise InvalidOrder(self.id + ' createOrder() requires the price and amount argument for market buy orders to calculate the total cost to spend(amount * price), alternatively set the createMarketBuyOrderRequiresPrice option or param to False and pass the cost to spend(quote quantity) in the amount argument') + amountString = self.number_to_string(amount) + priceString = self.number_to_string(price) + costRequest = Precise.string_mul(amountString, priceString) + quoteAmount = self.cost_to_precision(symbol, costRequest) + else: + if amount is None: + raise ArgumentsRequired(self.id + ' When createMarketBuyOrderRequiresPrice is False, "amount" is required and should be the total quote amount to spend.') + quoteAmount = self.cost_to_precision(symbol, amount) + return quoteAmount + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://docs.upbit.com/kr/reference/주문하기 + https://global-docs.upbit.com/reference/order + + :param str symbol: unified symbol of the market to create an order in + :param str type: supports 'market' and 'limit'. if params.ordType is set to best, a best-type order will be created regardless of the value of type. + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.cost]: for market buy and best buy orders, the quote quantity that can be used alternative for the amount + :param str [params.ordType]: self field can be used to place a ‘best’ type order + :param str [params.timeInForce]: 'IOC' or 'FOK' for limit or best type orders, 'PO' for limit orders. self field is required when the order type is 'best'. + :param str [params.selfTradePrevention]: 'reduce', 'cancel_maker', 'cancel_taker' {@link https://global-docs.upbit.com/docs/smp} + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + clientOrderId = self.safe_string(params, 'clientOrderId') + customType = self.safe_string_2(params, 'ordType', 'ord_type') + postOnly = self.is_post_only(type == 'market', False, params) + timeInForce = self.safe_string_lower_2(params, 'timeInForce', 'time_in_force') + selfTradePrevention = self.safe_string_2(params, 'selfTradePrevention', 'smp_type') + if postOnly and (selfTradePrevention is not None): + raise ExchangeError(self.id + ' createOrder() does not support post_only and selfTradePrevention simultaneously.') + orderSide = None + if side == 'buy': + orderSide = 'bid' + elif side == 'sell': + orderSide = 'ask' + else: + raise InvalidOrder(self.id + ' createOrder() supports only buy or sell in the side argument.') + request: dict = { + 'market': market['id'], + 'side': orderSide, + # 'smp_type': selfTradePrevention, + } + if type == 'limit': + if price is None or amount is None: + raise ArgumentsRequired(self.id + ' the limit type order in createOrder() is required price and amount.') + request['ord_type'] = 'limit' + request['price'] = self.price_to_precision(symbol, price) + request['volume'] = self.amount_to_precision(symbol, amount) + elif type == 'market': + if side == 'buy': + request['ord_type'] = 'price' + orderPrice = self.calc_order_price(symbol, amount, price, params) + request['price'] = orderPrice + else: + if amount is None: + raise ArgumentsRequired(self.id + ' the market sell type order in createOrder() is required amount.') + request['ord_type'] = 'market' + request['volume'] = self.amount_to_precision(symbol, amount) + else: + raise InvalidOrder(self.id + ' createOrder() supports only limit or market types in the type argument.') + if customType == 'best': + params = self.omit(params, ['ordType', 'ord_type']) + request['ord_type'] = 'best' + if side == 'buy': + orderPrice = self.calc_order_price(symbol, amount, price, params) + request['price'] = orderPrice + else: + if amount is None: + raise ArgumentsRequired(self.id + ' the best sell type order in createOrder() is required amount.') + request['volume'] = self.amount_to_precision(symbol, amount) + if clientOrderId is not None: + request['identifier'] = clientOrderId + if postOnly: + if request['ord_type'] != 'limit': + raise InvalidOrder(self.id + ' postOnly orders are only supported for limit orders') + request['time_in_force'] = 'post_only' + if timeInForce is not None: + if timeInForce == 'ioc' or timeInForce == 'fok': + request['time_in_force'] = timeInForce + if request['ord_type'] == 'best' and timeInForce is None: + raise ArgumentsRequired(self.id + ' createOrder() requires a timeInForce parameter for best type orders') + params = self.omit(params, ['timeInForce', 'time_in_force', 'postOnly', 'clientOrderId', 'cost', 'selfTradePrevention', 'smp_type']) + response = await self.privatePostOrders(self.extend(request, params)) + # + # { + # "uuid": "cdd92199-2897-4e14-9448-f923320408ad", + # "side": "bid", + # "ord_type": "limit", + # "price": "100.0", + # "avg_price": "0.0", + # "state": "wait", + # "market": "KRW-BTC", + # "created_at": "2018-04-10T15:42:23+09:00", + # "volume": "0.01", + # "remaining_volume": "0.01", + # "reserved_fee": "0.0015", + # "remaining_fee": "0.0015", + # "paid_fee": "0.0", + # "locked": "1.0015", + # "executed_volume": "0.0", + # "trades_count": 0 + # } + # + return self.parse_order(response) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + + https://docs.upbit.com/kr/reference/주문-취소 + https://global-docs.upbit.com/reference/order-cancel + + cancels an open order + :param str id: order id + :param str symbol: not used by upbit cancelOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + request: dict = { + 'uuid': id, + } + response = await self.privateDeleteOrder(self.extend(request, params)) + # + # { + # "uuid": "cdd92199-2897-4e14-9448-f923320408ad", + # "side": "bid", + # "ord_type": "limit", + # "price": "100.0", + # "state": "wait", + # "market": "KRW-BTC", + # "created_at": "2018-04-10T15:42:23+09:00", + # "volume": "0.01", + # "remaining_volume": "0.01", + # "reserved_fee": "0.0015", + # "remaining_fee": "0.0015", + # "paid_fee": "0.0", + # "locked": "1.0015", + # "executed_volume": "0.0", + # "trades_count": 0 + # } + # + return self.parse_order(response) + + async def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}) -> Order: + """ + + https://docs.upbit.com/kr/reference/취소-후-재주문 + https://global-docs.upbit.com/reference/cancel-and-new + + canceled existing order and create new order. It's only generated same side and symbol canceled order. it returns the data of the canceled order, except for `new_order_uuid` and `new_identifier`. to get the details of the new order, use `fetchOrder(new_order_uuid)`. + :param str id: the uuid of the previous order you want to edit. + :param str symbol: the symbol of the new order. it must be the same symbol of the previous order. + :param str type: the type of the new order. only limit or market is accepted. if params.newOrdType is set to best, a best-type order will be created regardless of the value of type. + :param str side: the side of the new order. it must be the same side of the previous order. + :param number amount: the amount of the asset you want to buy or sell. It could be overridden by specifying the new_volume parameter in params. + :param number price: the price of the asset you want to buy or sell. It could be overridden by specifying the new_price parameter in params. + :param dict [params]: extra parameters specific to the exchange API endpoint. + :param str [params.clientOrderId]: to identify the previous order, either the id or self field is hasattr(self, required) method. + :param float [params.cost]: for market buy and best buy orders, the quote quantity that can be used alternative for the amount. + :param str [params.newTimeInForce]: 'IOC' or 'FOK' for limit or best type orders, 'PO' for limit orders. self field is required when the order type is 'best'. + :param str [params.newClientOrderId]: the order ID that the user can define. + :param str [params.newOrdType]: self field only accepts limit, price, market, or best. You can refer to the Upbit developer documentation for details on how to use self field. + :param str [params.selfTradePrevention]: 'reduce', 'cancel_maker', 'cancel_taker' {@link https://global-docs.upbit.com/docs/smp} + :returns dict: An `order structure ` + """ + await self.load_markets() + request: dict = {} + prevClientOrderId = self.safe_string(params, 'clientOrderId') + customType = self.safe_string_2(params, 'newOrdType', 'new_ord_type') + clientOrderId = self.safe_string(params, 'newClientOrderId') + postOnly = self.is_post_only(type == 'market', False, params) + timeInForce = self.safe_string_lower_2(params, 'newTimeInForce', 'new_time_in_force') + selfTradePrevention = self.safe_string_2(params, 'selfTradePrevention', 'new_smp_type') + if postOnly and (selfTradePrevention is not None): + raise ExchangeError(self.id + ' editOrder() does not support post_only and selfTradePrevention simultaneously.') + params = self.omit(params, 'clientOrderId') + if id is not None: + request['prev_order_uuid'] = id + elif prevClientOrderId is not None: + request['prev_order_identifier'] = prevClientOrderId + else: + raise ArgumentsRequired(self.id + ' editOrder() is required id or clientOrderId.') + if type == 'limit': + if price is None or amount is None: + raise ArgumentsRequired(self.id + ' editOrder() is required price and amount to create limit type order.') + request['new_ord_type'] = 'limit' + request['new_price'] = self.price_to_precision(symbol, price) + request['new_volume'] = self.amount_to_precision(symbol, amount) + elif type == 'market': + if side == 'buy': + request['new_ord_type'] = 'price' + orderPrice = self.calc_order_price(symbol, amount, price, params) + request['new_price'] = orderPrice + else: + if amount is None: + raise ArgumentsRequired(self.id + ' editOrder() is required amount to create market sell type order.') + request['new_ord_type'] = 'market' + request['new_volume'] = self.amount_to_precision(symbol, amount) + else: + raise InvalidOrder(self.id + ' editOrder() supports only limit or market types in the type argument.') + if customType == 'best': + params = self.omit(params, ['newOrdType', 'new_ord_type']) + request['new_ord_type'] = 'best' + if side == 'buy': + orderPrice = self.calc_order_price(symbol, amount, price, params) + request['new_price'] = orderPrice + else: + if amount is None: + raise ArgumentsRequired(self.id + ' editOrder() is required amount to create best sell order.') + request['new_volume'] = self.amount_to_precision(symbol, amount) + if clientOrderId is not None: + request['new_identifier'] = clientOrderId + if selfTradePrevention is not None: + request['new_smp_type'] = selfTradePrevention + if postOnly: + if request['new_ord_type'] != 'limit': + raise InvalidOrder(self.id + ' postOnly orders are only supported for limit orders') + request['new_time_in_force'] = 'post_only' + if timeInForce is not None: + if timeInForce == 'ioc' or timeInForce == 'fok': + request['new_time_in_force'] = timeInForce + if request['new_ord_type'] == 'best' and timeInForce is None: + raise ArgumentsRequired(self.id + ' editOrder() requires a timeInForce parameter for best type orders') + params = self.omit(params, ['newTimeInForce', 'new_time_in_force', 'postOnly', 'newClientOrderId', 'cost', 'selfTradePrevention', 'new_smp_type']) + # print('check the each request params: ', request) + response = await self.privatePostOrdersCancelAndNew(self.extend(request, params)) + # { + # uuid: '63b38774-27db-4439-ac20-1be16a24d18e', #previous order data + # side: 'bid', #previous order data + # ord_type: 'limit', #previous order data + # price: '100000000', #previous order data + # state: 'wait', #previous order data + # market: 'KRW-BTC', #previous order data + # created_at: '2025-04-01T15:30:47+09:00', #previous order data + # volume: '0.00008', #previous order data + # remaining_volume: '0.00008', #previous order data + # reserved_fee: '4', #previous order data + # remaining_fee: '4', #previous order data + # paid_fee: '0', #previous order data + # locked: '8004', #previous order data + # executed_volume: '0', #previous order data + # trades_count: '0', #previous order data + # identifier: '21', #previous order data + # new_order_uuid: 'cb1cce56-6237-4a78-bc11-4cfffc1bb4c2', # new order data + # new_order_identifier: '22' # new order data + # } + result: dict = {} + result['uuid'] = self.safe_string(response, 'new_order_uuid') + result['identifier'] = self.safe_string(response, 'new_order_identifier') + result['side'] = self.safe_string(response, 'side') + result['market'] = self.safe_string(response, 'market') + return self.parse_order(result) + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + + https://docs.upbit.com/kr/reference/입금-리스트-조회 + https://global-docs.upbit.com/reference/deposit-list-inquiry + + fetch all deposits made to an account + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + request: dict = { + # 'page': 1, + # 'order_by': 'asc', # 'desc' + } + currency = None + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + if limit is not None: + request['limit'] = limit # default is 100 + response = await self.privateGetDeposits(self.extend(request, params)) + # + # [ + # { + # "type": "deposit", + # "uuid": "94332e99-3a87-4a35-ad98-28b0c969f830", + # "currency": "KRW", + # "txid": "9e37c537-6849-4c8b-a134-57313f5dfc5a", + # "state": "ACCEPTED", + # "created_at": "2017-12-08T15:38:02+09:00", + # "done_at": "2017-12-08T15:38:02+09:00", + # "amount": "100000.0", + # "fee": "0.0" + # }, + # ..., + # ] + # + return self.parse_transactions(response, currency, since, limit) + + async def fetch_deposit(self, id: str, code: Str = None, params={}): + """ + fetch information on a deposit + + https://docs.upbit.com/kr/reference/개별-입금-조회 + https://global-docs.upbit.com/reference/individual-deposit-inquiry + + :param str id: the unique id for the deposit + :param str [code]: unified currency code of the currency deposited + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.txid]: withdrawal transaction id, the id argument is reserved for uuid + :returns dict: a `transaction structure ` + """ + await self.load_markets() + request: dict = { + 'uuid': id, + } + currency = None + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + response = await self.privateGetDeposit(self.extend(request, params)) + # + # { + # "type": "deposit", + # "uuid": "7f54527e-2eee-4268-860e-fd8b9d7fe3c7", + # "currency": "ADA", + # "net_type": "ADA", + # "txid": "99795bbfeca91eaa071068bb659b33eeb65d8aaff2551fdf7c78f345d188952b", + # "state": "ACCEPTED", + # "created_at": "2023-12-12T04:58:41Z", + # "done_at": "2023-12-12T05:31:50Z", + # "amount": "35.72344", + # "fee": "0.0", + # "transaction_type": "default" + # } + # + return self.parse_transaction(response, currency) + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + + https://docs.upbit.com/kr/reference/전체-출금-조회 + https://global-docs.upbit.com/reference/withdrawal-list-inquiry + + fetch all withdrawals made from an account + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + request: dict = { + # 'state': 'submitting', # 'submitted', 'almost_accepted', 'rejected', 'accepted', 'processing', 'done', 'canceled' + } + currency = None + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + if limit is not None: + request['limit'] = limit # default is 100 + response = await self.privateGetWithdraws(self.extend(request, params)) + # + # [ + # { + # "type": "withdraw", + # "uuid": "9f432943-54e0-40b7-825f-b6fec8b42b79", + # "currency": "BTC", + # "txid": null, + # "state": "processing", + # "created_at": "2018-04-13T11:24:01+09:00", + # "done_at": null, + # "amount": "0.01", + # "fee": "0.0", + # "krw_amount": "80420.0" + # }, + # ..., + # ] + # + return self.parse_transactions(response, currency, since, limit) + + async def fetch_withdrawal(self, id: str, code: Str = None, params={}): + """ + fetch data on a currency withdrawal via the withdrawal id + + https://docs.upbit.com/kr/reference/개별-출금-조회 + https://global-docs.upbit.com/reference/individual-withdrawal-inquiry + + :param str id: the unique id for the withdrawal + :param str [code]: unified currency code of the currency withdrawn + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.txid]: withdrawal transaction id, the id argument is reserved for uuid + :returns dict: a `transaction structure ` + """ + await self.load_markets() + request: dict = { + 'uuid': id, + } + currency = None + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + response = await self.privateGetWithdraw(self.extend(request, params)) + # + # { + # "type": "withdraw", + # "uuid": "95ef274b-23a6-4de4-95b0-5cbef4ca658f", + # "currency": "ADA", + # "net_type": "ADA", + # "txid": "b1528f149297a71671b86636f731f8fdb0ff53da0f1d8c19093d59df96f34583", + # "state": "DONE", + # "created_at": "2023-12-14T02:46:52Z", + # "done_at": "2023-12-14T03:10:11Z", + # "amount": "35.22344", + # "fee": "0.5", + # "transaction_type": "default" + # } + # + return self.parse_transaction(response, currency) + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'submitting': 'pending', # 처리 중 + 'submitted': 'pending', # 처리 완료 + 'almost_accepted': 'pending', # 출금대기중 + 'rejected': 'failed', # 거부 + 'accepted': 'ok', # 승인됨 + 'processing': 'pending', # 처리 중 + 'done': 'ok', # 완료 + 'canceled': 'canceled', # 취소됨 + } + return self.safe_string(statuses, status, status) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fetchDeposits, fetchDeposit + # + # { + # "type": "deposit", + # "uuid": "94332e99-3a87-4a35-ad98-28b0c969f830", + # "currency": "KRW", + # "txid": "9e37c537-6849-4c8b-a134-57313f5dfc5a", + # "state": "ACCEPTED", + # "created_at": "2017-12-08T15:38:02+09:00", + # "done_at": "2017-12-08T15:38:02+09:00", + # "amount": "100000.0", + # "fee": "0.0" + # } + # + # fetchWithdrawals, fetchWithdrawal + # + # { + # "type": "withdraw", + # "uuid": "9f432943-54e0-40b7-825f-b6fec8b42b79", + # "currency": "BTC", + # "txid": "cd81e9b45df8da29f936836e58c907a106057e454a45767a7b06fcb19b966bba", + # "state": "processing", + # "created_at": "2018-04-13T11:24:01+09:00", + # "done_at": null, + # "amount": "0.01", + # "fee": "0.0", + # "krw_amount": "80420.0" + # } + # + address = None # not present in the data structure received from the exchange + tag = None # not present in the data structure received from the exchange + updatedRaw = self.safe_string(transaction, 'done_at') + timestamp = self.parse8601(self.safe_string(transaction, 'created_at', updatedRaw)) + type = self.safe_string(transaction, 'type') + if type == 'withdraw': + type = 'withdrawal' + currencyId = self.safe_string(transaction, 'currency') + code = self.safe_currency_code(currencyId, currency) + return { + 'info': transaction, + 'id': self.safe_string(transaction, 'uuid'), + 'currency': code, + 'amount': self.safe_number(transaction, 'amount'), + 'network': None, + 'address': address, + 'addressTo': None, + 'addressFrom': None, + 'tag': tag, + 'tagTo': None, + 'tagFrom': None, + 'status': self.parse_transaction_status(self.safe_string_lower(transaction, 'state')), + 'type': type, + 'updated': self.parse8601(updatedRaw), + 'txid': self.safe_string(transaction, 'txid'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'internal': None, + 'comment': None, + 'fee': { + 'currency': code, + 'cost': self.safe_number(transaction, 'fee'), + }, + } + + def parse_order_status(self, status: Str): + statuses: dict = { + 'wait': 'open', + 'done': 'closed', + 'cancel': 'canceled', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # { + # "market": "KRW-USDT", + # "uuid": "3b67e543-8ad3-48d0-8451-0dad315cae73", + # "side": "ask", + # "ord_type": "market", + # "state": "done", + # "created_at": "2025-08-09T16:44:00+09:00", + # "volume": "5.377594", + # "remaining_volume": "0", + # "executed_volume": "5.377594", + # "reserved_fee": "0", + # "remaining_fee": "0", + # "paid_fee": "3.697095875", + # "locked": "0", + # "prevented_volume": "0", + # "prevented_locked": "0", + # "trades_count": 1, + # "trades": [ + # { + # "market": "KRW-USDT", + # "uuid": "795dff29-bba6-49b2-baab-63473ab7931c", + # "price": "1375", + # "volume": "5.377594", + # "funds": "7394.19175", + # "trend": "down", + # "created_at": "2025-08-09T16:44:00.597751+09:00", + # "side": "ask" + # } + # ] + # } + # + # fetchOpenOrders, fetchClosedOrders, fetchCanceledOrders + # + # { + # "uuid": "637fd66-d019-4d77-bee6-8e0cff28edd9", + # "side": "ask", + # "ord_type": "limit", + # "price": "1.5", + # "state": "wait", + # "market": "SGD-XRP", + # "created_at": "2024-06-05T09:37:10Z", + # "volume": "10", + # "remaining_volume": "10", + # "reserved_fee": "0", + # "remaining_fee": "0", + # "paid_fee": "0", + # "locked": "10", + # "executed_volume": "0", + # "executed_funds": "0", + # "trades_count": 0, + # "time_in_force": "ioc" + # } + # + # { + # uuid: '63b38774-27db-4439-ac20-1be16a24d18e', + # side: 'bid', + # ord_type: 'limit', + # price: '100000000', + # state: 'wait', + # market: 'KRW-BTC', + # created_at: '2025-04-01T15:30:47+09:00', + # volume: '0.00008', + # remaining_volume: '0.00008', + # reserved_fee: '4', + # remaining_fee: '4', + # paid_fee: '0', + # locked: '8004', + # executed_volume: '0', + # trades_count: '0', + # identifier: '21', + # new_order_uuid: 'cb1cce56-6237-4a78-bc11-4cfffc1bb4c2', + # new_order_identifier: '22' + # } + id = self.safe_string(order, 'uuid') + side = self.safe_string(order, 'side') + if side == 'bid': + side = 'buy' + else: + side = 'sell' + identifier = self.safe_string(order, 'identifier') + type = self.safe_string(order, 'ord_type') + timestamp = self.parse8601(self.safe_string(order, 'created_at')) + status = self.parse_order_status(self.safe_string(order, 'state')) + lastTradeTimestamp = None + price = self.safe_string(order, 'price') + amount = self.safe_string(order, 'volume') + remaining = self.safe_string(order, 'remaining_volume') + filled = self.safe_string(order, 'executed_volume') + cost = None + if type == 'price': + type = 'market' + cost = price + price = None + average = None + fee = None + feeCost = self.safe_string(order, 'paid_fee') + marketId = self.safe_string(order, 'market') + market = self.safe_market(marketId, market) + trades = self.safe_value(order, 'trades', []) + trades = self.parse_trades(trades, market, None, None, { + 'order': id, + 'type': type, + }) + numTrades = len(trades) + if numTrades > 0: + # the timestamp in fetchOrder trades is missing + lastTradeTimestamp = trades[numTrades - 1]['timestamp'] + getFeesFromTrades = False + if feeCost is None: + getFeesFromTrades = True + feeCost = '0' + cost = '0' + for i in range(0, numTrades): + trade = trades[i] + cost = Precise.string_add(cost, self.safe_string(trade, 'cost')) + if getFeesFromTrades: + tradeFee = self.safe_value(trades[i], 'fee', {}) + tradeFeeCost = self.safe_string(tradeFee, 'cost') + if tradeFeeCost is not None: + feeCost = Precise.string_add(feeCost, tradeFeeCost) + average = Precise.string_div(cost, filled) + if feeCost is not None: + fee = { + 'currency': market['quote'], + 'cost': feeCost, + } + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': identifier, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': lastTradeTimestamp, + 'symbol': market['symbol'], + 'type': type, + 'timeInForce': self.safe_string_upper(order, 'time_in_force'), + 'postOnly': None, + 'side': side, + 'price': price, + 'triggerPrice': None, + 'cost': self.parse_number(cost), + 'average': self.parse_number(average), + 'amount': amount, + 'filled': filled, + 'remaining': remaining, + 'status': status, + 'fee': fee, + 'trades': trades, + }) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://docs.upbit.com/kr/reference/대기-주문-조회 + https://global-docs.upbit.com/reference/open-order + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.state]: default is 'wait', set to 'watch' for stop limit orders + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['market'] = market['id'] + if limit is not None: + request['limit'] = limit + response = await self.privateGetOrdersOpen(self.extend(request, params)) + # + # [ + # { + # "uuid": "637fd66-d019-4d77-bee6-8e0cff28edd9", + # "side": "ask", + # "ord_type": "limit", + # "price": "1.5", + # "state": "wait", + # "market": "SGD-XRP", + # "created_at": "2024-06-05T09:37:10Z", + # "volume": "10", + # "remaining_volume": "10", + # "reserved_fee": "0", + # "remaining_fee": "0", + # "paid_fee": "0", + # "locked": "10", + # "executed_volume": "0", + # "executed_funds": "0", + # "trades_count": 0 + # } + # ] + # + return self.parse_orders(response, market, since, limit) + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://docs.upbit.com/kr/reference/종료-주문-조회 + https://global-docs.upbit.com/reference/closed-order + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest order + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + request: dict = { + 'state': 'done', + } + market = None + if symbol is not None: + market = self.market(symbol) + request['market'] = market['id'] + if since is not None: + request['start_time'] = since + if limit is not None: + request['limit'] = limit + request, params = self.handle_until_option('end_time', request, params) + response = await self.privateGetOrdersClosed(self.extend(request, params)) + # + # [ + # { + # "uuid": "637fd66-d019-4d77-bee6-8e0cff28edd9", + # "side": "ask", + # "ord_type": "limit", + # "price": "1.5", + # "state": "done", + # "market": "SGD-XRP", + # "created_at": "2024-06-05T09:37:10Z", + # "volume": "10", + # "remaining_volume": "10", + # "reserved_fee": "0", + # "remaining_fee": "0", + # "paid_fee": "0", + # "locked": "10", + # "executed_volume": "0", + # "executed_funds": "0", + # "trades_count": 0, + # "time_in_force": "ioc" + # } + # ] + # + return self.parse_orders(response, market, since, limit) + + async def fetch_canceled_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches information on multiple canceled orders made by the user + + https://docs.upbit.com/kr/reference/종료-주문-조회 + https://global-docs.upbit.com/reference/closed-order + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: timestamp in ms of the earliest order, default is None + :param int [limit]: max number of orders to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest order + :returns dict: a list of `order structures ` + """ + await self.load_markets() + request: dict = { + 'state': 'cancel', + } + market = None + if symbol is not None: + market = self.market(symbol) + request['market'] = market['id'] + if since is not None: + request['start_time'] = since + if limit is not None: + request['limit'] = limit + request, params = self.handle_until_option('end_time', request, params) + response = await self.privateGetOrdersClosed(self.extend(request, params)) + # + # [ + # { + # "uuid": "637fd66-d019-4d77-bee6-8e0cff28edd9", + # "side": "ask", + # "ord_type": "limit", + # "price": "1.5", + # "state": "cancel", + # "market": "SGD-XRP", + # "created_at": "2024-06-05T09:37:10Z", + # "volume": "10", + # "remaining_volume": "10", + # "reserved_fee": "0", + # "remaining_fee": "0", + # "paid_fee": "0", + # "locked": "10", + # "executed_volume": "0", + # "executed_funds": "0", + # "trades_count": 0, + # "time_in_force": "ioc" + # } + # ] + # + return self.parse_orders(response, market, since, limit) + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + + https://docs.upbit.com/kr/reference/개별-주문-조회 + https://global-docs.upbit.com/reference/individual-order-inquiry + + fetches information on an order made by the user + :param str id: order id + :param str symbol: not used by upbit fetchOrder + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + request: dict = { + 'uuid': id, + } + response = await self.privateGetOrder(self.extend(request, params)) + # + # { + # "uuid": "a08f09b1-1718-42e2-9358-f0e5e083d3ee", + # "side": "bid", + # "ord_type": "limit", + # "price": "17417000.0", + # "state": "done", + # "market": "KRW-BTC", + # "created_at": "2018-04-05T14:09:14+09:00", + # "volume": "1.0", + # "remaining_volume": "0.0", + # "reserved_fee": "26125.5", + # "remaining_fee": "25974.0", + # "paid_fee": "151.5", + # "locked": "17341974.0", + # "executed_volume": "1.0", + # "trades_count": 2, + # "trades": [ + # { + # "market": "KRW-BTC", + # "uuid": "78162304-1a4d-4524-b9e6-c9a9e14d76c3", + # "price": "101000.0", + # "volume": "0.77368323", + # "funds": "78142.00623", + # "ask_fee": "117.213009345", + # "bid_fee": "117.213009345", + # "created_at": "2018-04-05T14:09:15+09:00", + # "side": "bid" + # }, + # { + # "market": "KRW-BTC", + # "uuid": "f73da467-c42f-407d-92fa-e10d86450a20", + # "price": "101000.0", + # "volume": "0.22631677", + # "funds": "22857.99377", + # "ask_fee": "34.286990655", + # "bid_fee": "34.286990655", + # "created_at": "2018-04-05T14:09:15+09:00", + # "side": "bid" + # } + # ] + # } + # + return self.parse_order(response) + + async def fetch_deposit_addresses(self, codes: Strings = None, params={}) -> List[DepositAddress]: + """ + + https://docs.upbit.com/kr/reference/전체-입금-주소-조회 + https://global-docs.upbit.com/reference/general-deposit-address-inquiry + + fetch deposit addresses for multiple currencies and chain types + :param str[]|None codes: list of unified currency codes, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `address structures ` + """ + await self.load_markets() + response = await self.privateGetDepositsCoinAddresses(params) + # + # [ + # { + # "currency": "BTC", + # "deposit_address": "3EusRwybuZUhVDeHL7gh3HSLmbhLcy7NqD", + # "secondary_address": null + # }, + # { + # "currency": "ETH", + # "deposit_address": "0x0d73e0a482b8cf568976d2e8688f4a899d29301c", + # "secondary_address": null + # }, + # { + # "currency": "XRP", + # "deposit_address": "rN9qNpgnBaZwqCg8CvUZRPqCcPPY7wfWep", + # "secondary_address": "3057887915" + # } + # ] + # + return self.parse_deposit_addresses(response, codes) + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + # + # { + # currency: 'XRP', + # net_type: 'XRP', + # deposit_address: 'raQwCVAJVqjrVm1Nj5SFRcX8i22BhdC9WA', + # secondary_address: '167029435' + # } + # + address = self.safe_string(depositAddress, 'deposit_address') + tag = self.safe_string(depositAddress, 'secondary_address') + currencyId = self.safe_string(depositAddress, 'currency') + code = self.safe_currency_code(currencyId) + networkId = self.safe_string(depositAddress, 'net_type') + self.check_address(address) + return { + 'info': depositAddress, + 'currency': code, + 'network': self.network_id_to_code(networkId), + 'address': address, + 'tag': tag, + } + + async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + + https://docs.upbit.com/kr/reference/개별-입금-주소-조회 + https://global-docs.upbit.com/reference/individual-deposit-address-inquiry + + fetch the deposit address for a currency associated with self account + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str params['network']: deposit chain, can view all chains via self.publicGetWalletAssets, default is eth, unless the currency has a default chain within self.options['networks'] + :returns dict: an `address structure ` + """ + await self.load_markets() + currency = self.currency(code) + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + if networkCode is None: + raise ArgumentsRequired(self.id + ' fetchDepositAddress requires params["network"]') + response = await self.privateGetDepositsCoinAddress(self.extend({ + 'currency': currency['id'], + 'net_type': self.network_code_to_id(networkCode, currency['code']), + }, params)) + # + # { + # currency: 'XRP', + # net_type: 'XRP', + # deposit_address: 'raQwCVAJVqjrVm1Nj5SFRcX8i22BhdC9WA', + # secondary_address: '167029435' + # } + # + return self.parse_deposit_address(response) + + async def create_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + + https://docs.upbit.com/kr/reference/입금-주소-생성-요청 + https://global-docs.upbit.com/reference/deposit-address-generation + + create a currency deposit address + :param str code: unified currency code of the currency for the deposit address + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + } + # https://github.com/ccxt/ccxt/issues/6452 + response = await self.privatePostDepositsGenerateCoinAddress(self.extend(request, params)) + # + # https://docs.upbit.com/v1.0/reference#%EC%9E%85%EA%B8%88-%EC%A3%BC%EC%86%8C-%EC%83%9D%EC%84%B1-%EC%9A%94%EC%B2%AD + # can be any of the two responses: + # + # { + # "success" : True, + # "message" : "Creating BTC deposit address." + # } + # + # { + # "currency": "BTC", + # "deposit_address": "3EusRwybuZUhVDeHL7gh3HSLmbhLcy7NqD", + # "secondary_address": null + # } + # + message = self.safe_string(response, 'message') + if message is not None: + raise AddressPending(self.id + ' is generating ' + code + ' deposit address, call fetchDepositAddress or createDepositAddress one more time later to retrieve the generated address') + return self.parse_deposit_address(response) + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + + https://docs.upbit.com/kr/reference/디지털자산-출금하기 + https://global-docs.upbit.com/reference/withdrawal-digital-assets + + make a withdrawal + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'amount': amount, + } + response = None + if code != 'KRW': + self.check_address(address) + # 2023-05-23 Change to required parameters for digital assets + network = self.safe_string_upper_2(params, 'network', 'net_type') + if network is None: + raise ArgumentsRequired(self.id + ' withdraw() requires a network argument') + params = self.omit(params, ['network']) + request['net_type'] = network + request['currency'] = currency['id'] + request['address'] = address + if tag is not None: + request['secondary_address'] = tag + params = self.omit(params, 'network') + response = await self.privatePostWithdrawsCoin(self.extend(request, params)) + else: + response = await self.privatePostWithdrawsKrw(self.extend(request, params)) + # + # { + # "type": "withdraw", + # "uuid": "9f432943-54e0-40b7-825f-b6fec8b42b79", + # "currency": "BTC", + # "txid": "ebe6937b-130e-4066-8ac6-4b0e67f28adc", + # "state": "processing", + # "created_at": "2018-04-13T11:24:01+09:00", + # "done_at": null, + # "amount": "0.01", + # "fee": "0.0", + # "krw_amount": "80420.0" + # } + # + return self.parse_transaction(response) + + def nonce(self): + return self.milliseconds() + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = self.implode_params(self.urls['api'][api], { + 'hostname': self.hostname, + }) + url += '/' + self.version + '/' + self.implode_params(path, params) + query = self.omit(params, self.extract_params(path)) + if method != 'POST': + if query: + url += '?' + self.urlencode(query) + if api == 'private': + self.check_required_credentials() + headers = {} + nonce = self.uuid() + request: dict = { + 'access_key': self.apiKey, + 'nonce': nonce, + } + hasQuery = query + auth = None + if (method != 'GET') and (method != 'DELETE'): + body = self.json(params) + headers['Content-Type'] = 'application/json' + if hasQuery: + auth = self.rawencode(query) + if auth is not None: + hash = self.hash(self.encode(auth), 'sha512') + request['query_hash'] = hash + request['query_hash_alg'] = 'SHA512' + token = self.jwt(request, self.encode(self.secret), 'sha256') + headers['Authorization'] = 'Bearer ' + token + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None # fallback to default error handler + # + # {'error': {'message': "Missing request parameter error. Check the required parameters!", 'name': 400}}, + # {'error': {'message': "side is missing, side does not have a valid value", 'name': "validation_error"}}, + # {'error': {'message': "개인정보 제 3자 제공 동의가 필요합니다.", 'name': "thirdparty_agreement_required"}}, + # {'error': {'message': "권한이 부족합니다.", 'name': "out_of_scope"}}, + # {'error': {'message': "주문을 찾지 못했습니다.", 'name': "order_not_found"}}, + # {'error': {'message': "주문가능한 금액(ETH)이 부족합니다.", 'name': "insufficient_funds_ask"}}, + # {'error': {'message': "주문가능한 금액(BTC)이 부족합니다.", 'name': "insufficient_funds_bid"}}, + # {'error': {'message': "잘못된 엑세스 키입니다.", 'name': "invalid_access_key"}}, + # {'error': {'message': "Jwt 토큰 검증에 실패했습니다.", 'name': "jwt_verification"}} + # + error = self.safe_value(response, 'error') + if error is not None: + message = self.safe_string(error, 'message') + name = self.safe_string(error, 'name') + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], name, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], name, feedback) + raise ExchangeError(feedback) # unknown message + return None diff --git a/ccxt/async_support/wavesexchange.py b/ccxt/async_support/wavesexchange.py new file mode 100644 index 0000000..794e1f8 --- /dev/null +++ b/ccxt/async_support/wavesexchange.py @@ -0,0 +1,2642 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.wavesexchange import ImplicitAPI +import asyncio +import json +from ccxt.base.types import Any, Balances, Currency, DepositAddress, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import AccountSuspended +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import DuplicateOrderId +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class wavesexchange(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(wavesexchange, self).describe(), { + 'id': 'wavesexchange', + 'name': 'Waves.Exchange', + 'countries': ['CH'], # Switzerland + 'certified': False, + 'pro': False, + 'dex': True, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelOrder': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createMarketOrder': True, + 'createOrder': True, + 'createReduceOnlyOrder': False, + 'createStopLimitOrder': False, + 'createStopMarketOrder': False, + 'createStopOrder': False, + 'fetchAllGreeks': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': False, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': None, + 'fetchDepositAddressesByNetwork': None, + 'fetchDepositWithdrawFee': 'emulated', + 'fetchDepositWithdrawFees': True, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrice': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTrades': True, + 'fetchTransfer': False, + 'fetchTransfers': False, + 'fetchUnderlyingAssets': False, + 'fetchVolatilityHistory': False, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'sandbox': True, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'signIn': True, + 'transfer': False, + 'withdraw': True, + 'ws': False, + }, + 'timeframes': { + '1m': '1m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1h', + '2h': '2h', + '3h': '3h', + '4h': '4h', + '6h': '6h', + '12h': '12h', + '1d': '1d', + '1w': '1w', + '1M': '1M', + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/84547058-5fb27d80-ad0b-11ea-8711-78ac8b3c7f31.jpg', + 'test': { + 'matcher': 'https://matcher-testnet.wx.network', + 'node': 'https://nodes-testnet.wavesnodes.com', + 'public': 'https://api-testnet.wavesplatform.com/v0', + 'private': 'https://api-testnet.wx.network/v1', + 'forward': 'https://testnet.wx.network/api/v1/forward/matcher', + 'market': 'https://testnet.wx.network/api/v1/forward/marketdata/api/v1', + }, + 'api': { + 'matcher': 'https://matcher.wx.network', + 'node': 'https://nodes.wx.network', + 'public': 'https://api.wavesplatform.com/v0', + 'private': 'https://api.wx.network/v1', + 'forward': 'https://wx.network/api/v1/forward/matcher', + 'market': 'https://wx.network/api/v1/forward/marketdata/api/v1', + }, + 'doc': [ + 'https://docs.wx.network', + 'https://docs.waves.tech', + 'https://api.wavesplatform.com/v0/docs/', + 'https://nodes.wavesnodes.com/api-docs/index.html', + 'https://matcher.waves.exchange/api-docs/index.html', + ], + 'www': 'https://wx.network', + }, + 'api': { + 'matcher': { + 'get': [ + 'matcher', + 'matcher/settings', + 'matcher/settings/rates', + 'matcher/balance/reserved/{publicKey}', + 'matcher/debug/allSnashotOffsets', + 'matcher/debug/currentOffset', + 'matcher/debug/lastOffset', + 'matcher/debug/oldestSnapshotOffset', + 'matcher/debug/config', + 'matcher/debug/address/{address}', + 'matcher/debug/status', + 'matcher/debug/address/{address}/check', + 'matcher/orderbook', + 'matcher/orderbook/{baseId}/{quoteId}', + 'matcher/orderbook/{baseId}/{quoteId}/publicKey/{publicKey}', + 'matcher/orderbook/{baseId}/{quoteId}/{orderId}', + 'matcher/orderbook/{baseId}/{quoteId}/info', + 'matcher/orderbook/{baseId}/{quoteId}/status', + 'matcher/orderbook/{baseId}/{quoteId}/tradableBalance/{address}', + 'matcher/orderbook/{publicKey}', + 'matcher/orderbook/{publicKey}/{orderId}', + 'matcher/orders/{address}', + 'matcher/orders/{address}/{orderId}', + 'matcher/transactions/{orderId}', + 'api/v1/orderbook/{baseId}/{quoteId}', + ], + 'post': [ + 'matcher/orderbook', + 'matcher/orderbook/market', + 'matcher/orderbook/cancel', + 'matcher/orderbook/{baseId}/{quoteId}/cancel', + 'matcher/orderbook/{baseId}/{quoteId}/calculateFee', + 'matcher/orderbook/{baseId}/{quoteId}/delete', + 'matcher/orderbook/{baseId}/{quoteId}/cancelAll', + 'matcher/debug/saveSnapshots', + 'matcher/orders/{address}/cancel', + 'matcher/orders/cancel/{orderId}', + 'matcher/orders/serialize', + ], + 'delete': [ + 'matcher/orderbook/{baseId}/{quoteId}', + 'matcher/settings/rates/{assetId}', + ], + 'put': [ + 'matcher/settings/rates/{assetId}', + ], + }, + 'node': { + 'get': [ + 'addresses', + 'addresses/balance/{address}', + 'addresses/balance/{address}/{confirmations}', + 'addresses/balance/details/{address}', + 'addresses/data/{address}', + 'addresses/data/{address}/{key}', + 'addresses/effectiveBalance/{address}', + 'addresses/effectiveBalance/{address}/{confirmations}', + 'addresses/publicKey/{publicKey}', + 'addresses/scriptInfo/{address}', + 'addresses/scriptInfo/{address}/meta', + 'addresses/seed/{address}', + 'addresses/seq/{from}/{to}', + 'addresses/validate/{address}', + 'alias/by-address/{address}', + 'alias/by-alias/{alias}', + 'assets/{assetId}/distribution/{height}/{limit}', + 'assets/balance/{address}', + 'assets/balance/{address}/{assetId}', + 'assets/details/{assetId}', + 'assets/nft/{address}/limit/{limit}', + 'blockchain/rewards', + 'blockchain/rewards/height', + 'blocks/address/{address}/{from}/{to}/', + 'blocks/at/{height}', + 'blocks/delay/{signature}/{blockNum}', + 'blocks/first', + 'blocks/headers/last', + 'blocks/headers/seq/{from}/{to}', + 'blocks/height', + 'blocks/height/{signature}', + 'blocks/last', + 'blocks/seq/{from}/{to}', + 'blocks/signature/{signature}', + 'consensus/algo', + 'consensus/basetarget', + 'consensus/basetarget/{blockId}', + 'consensus/{generatingbalance}/address', + 'consensus/generationsignature', + 'consensus/generationsignature/{blockId}', + 'debug/balances/history/{address}', + 'debug/blocks/{howMany}', + 'debug/configInfo', + 'debug/historyInfo', + 'debug/info', + 'debug/minerInfo', + 'debug/portfolios/{address}', + 'debug/state', + 'debug/stateChanges/address/{address}', + 'debug/stateChanges/info/{id}', + 'debug/stateWaves/{height}', + 'leasing/active/{address}', + 'node/state', + 'node/version', + 'peers/all', + 'peers/blacklisted', + 'peers/connected', + 'peers/suspended', + 'transactions/address/{address}/limit/{limit}', + 'transactions/info/{id}', + 'transactions/status', + 'transactions/unconfirmed', + 'transactions/unconfirmed/info/{id}', + 'transactions/unconfirmed/size', + 'utils/seed', + 'utils/seed/{length}', + 'utils/time', + 'wallet/seed', + ], + 'post': [ + 'addresses', + 'addresses/data/{address}', + 'addresses/sign/{address}', + 'addresses/signText/{address}', + 'addresses/verify/{address}', + 'addresses/verifyText/{address}', + 'debug/blacklist', + 'debug/print', + 'debug/rollback', + 'debug/validate', + 'node/stop', + 'peers/clearblacklist', + 'peers/connect', + 'transactions/broadcast', + 'transactions/calculateFee', + 'tranasctions/sign', + 'transactions/sign/{signerAddress}', + 'tranasctions/status', + 'utils/hash/fast', + 'utils/hash/secure', + 'utils/script/compileCode', + 'utils/script/compileWithImports', + 'utils/script/decompile', + 'utils/script/estimate', + 'utils/sign/{privateKey}', + 'utils/transactionsSerialize', + ], + 'delete': [ + 'addresses/{address}', + 'debug/rollback-to/{signature}', + ], + }, + 'public': { + 'get': [ + 'assets', + 'pairs', + 'candles/{baseId}/{quoteId}', + 'transactions/exchange', + ], + }, + 'private': { + 'get': [ + 'deposit/addresses/{currency}', + 'deposit/addresses/{currency}/{platform}', + 'platforms', + 'deposit/currencies', + 'withdraw/currencies', + 'withdraw/addresses/{currency}/{address}', + ], + 'post': [ + 'oauth2/token', + ], + }, + 'forward': { + 'get': [ + 'matcher/orders/{address}', # can't get the orders endpoint to work with the matcher api + 'matcher/orders/{address}/{orderId}', + ], + 'post': [ + 'matcher/orders/{wavesAddress}/cancel', + ], + }, + 'market': { + 'get': [ + 'tickers', + ], + }, + }, + 'currencies': { + 'WX': self.safe_currency_structure({'id': 'EMAMLxDnv3xiz8RXg8Btj33jcEw3wLczL3JKYYmuubpc', 'numericId': None, 'code': 'WX', 'precision': self.parse_number('1e-8')}), + }, + 'precisionMode': TICK_SIZE, + 'options': { + 'allowedCandles': 1440, + 'accessToken': None, + 'createMarketBuyOrderRequiresPrice': True, + 'matcherPublicKey': None, + 'quotes': None, + 'createOrderDefaultExpiry': 2419200000, # 60 * 60 * 24 * 28 * 1000 + 'wavesAddress': None, + 'withdrawFeeUSDN': 7420, + 'withdrawFeeWAVES': 100000, + 'wavesPrecision': 1e-8, + 'messagePrefix': 'W', # W for production, T for testnet + 'networks': { + 'ERC20': 'ETH', + 'BEP20': 'BSC', + }, + }, + 'features': { + 'spot': { + 'sandbox': True, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, # todo + 'triggerDirection': False, + 'triggerPriceType': None, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': False, + 'FOK': False, + 'PO': False, + 'GTD': True, # todo + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': False, # todo + 'marketBuyRequiresPrice': True, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, # todo + 'daysBack': 100000, # todo + 'untilDays': 100000, # todo + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 100, # todo + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': 100, # todo + 'daysBack': None, + 'untilDays': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, # todo + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 100000, # todo + 'daysBackCanceled': 1, # todo + 'untilDays': 100000, # todo + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': None, # todo + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'commonCurrencies': { + 'EGG': 'Waves Ducks', + }, + 'requiresEddsa': True, + 'exceptions': { + '3147270': InsufficientFunds, # https://github.com/wavesplatform/matcher/wiki/List-of-all-errors + '112': InsufficientFunds, + '4': ExchangeError, + '13': ExchangeNotAvailable, + '14': ExchangeNotAvailable, + '3145733': AccountSuspended, + '3148040': DuplicateOrderId, + '3148801': AuthenticationError, + '9440512': AuthenticationError, + '9440771': BadSymbol, + '9441026': InvalidOrder, + '9441282': InvalidOrder, + '9441286': InvalidOrder, + '9441295': InvalidOrder, + '9441540': InvalidOrder, + '9441542': InvalidOrder, + '106954752': AuthenticationError, + '106954769': AuthenticationError, + '106957828': AuthenticationError, + '106960131': AuthenticationError, + '106981137': AuthenticationError, + '9437184': BadRequest, # {"error":9437184,"message":"The order is invalid: SpendAmount should be > 0","template":"The order is invalid: {{details}}","params":{"details":"SpendAmount should be > 0"},"status":"OrderRejected","success":false} + '9437193': OrderNotFound, + '1048577': BadRequest, + '1051904': AuthenticationError, + }, + }) + + def set_sandbox_mode(self, enabled): + self.options['messagePrefix'] = 'T' if enabled else 'W' + self.options['sandboxMode'] = enabled + super(wavesexchange, self).set_sandbox_mode(enabled) + + async def get_fees_for_asset(self, symbol: str, side, amount, price, params={}): + await self.load_markets() + market = self.market(symbol) + amount = self.to_real_symbol_amount(symbol, amount) + price = self.to_real_symbol_price(symbol, price) + request = self.extend({ + 'baseId': market['baseId'], + 'quoteId': market['quoteId'], + 'orderType': side, + 'amount': amount, + 'price': price, + }, params) + return await self.matcherPostMatcherOrderbookBaseIdQuoteIdCalculateFee(request) + + async def custom_calculate_fee(self, symbol: str, type, side, amount, price, takerOrMaker='taker', params={}): + response = await self.get_fees_for_asset(symbol, side, amount, price) + # { + # "base":{ + # "feeAssetId":"WAVES", + # "matcherFee":"1000000" + # }, + # "discount":{ + # "feeAssetId":"EMAMLxDnv3xiz8RXg8Btj33jcEw3wLczL3JKYYmuubpc", + # "matcherFee":"4077612" + # } + # } + isDiscountFee = self.safe_bool(params, 'isDiscountFee', False) + mode = None + if isDiscountFee: + mode = self.safe_value(response, 'discount') + else: + mode = self.safe_value(response, 'base') + matcherFee = self.safe_string(mode, 'matcherFee') + feeAssetId = self.safe_string(mode, 'feeAssetId') + feeAsset = self.safe_currency_code(feeAssetId) + adjustedMatcherFee = self.from_real_currency_amount(feeAsset, matcherFee) + amountAsString = self.number_to_string(amount) + priceAsString = self.number_to_string(price) + feeCost = self.fee_to_precision(symbol, self.parse_number(adjustedMatcherFee)) + feeRate = Precise.string_div(adjustedMatcherFee, Precise.string_mul(amountAsString, priceAsString)) + return { + 'type': takerOrMaker, + 'currency': feeAsset, + 'rate': self.parse_number(feeRate), + 'cost': self.parse_number(feeCost), + } + + async def get_quotes(self): + quotes = self.safe_value(self.options, 'quotes') + if quotes: + return quotes + else: + # currencies can have any name because you can create you own token + # result someone can create a fake token called BTC + # we use self mapping to determine the real tokens + # https://docs.wx.network/en/waves-matcher/matcher-api#asset-pair + response = await self.matcherGetMatcherSettings() + # { + # "orderVersions": [ + # 1, + # 2, + # 3 + # ], + # "success": True, + # "matcherPublicKey": "9cpfKN9suPNvfeUNphzxXMjcnn974eme8ZhWUjaktzU5", + # "orderFee": { + # "dynamic": { + # "baseFee": 300000, + # "rates": { + # "34N9YcEETLWn93qYQ64EsP1x89tSruJU44RrEMSXXEPJ": 1.22639597, + # "62LyMjcr2DtiyF5yVXFhoQ2q414VPPJXjsNYp72SuDCH": 0.00989643, + # "HZk1mbfuJpmxU1Fs4AX5MWLVYtctsNcg6e2C6VKqK8zk": 0.0395674, + # "8LQW8f7P5d5PZM7GtZEBgaqRPGSzS3DfPuiXrURJ4AJS": 0.00018814, + # "4LHHvYGNKJUg5hj65aGD5vgScvCBmLpdRFtjokvCjSL8": 26.19721262, + # "474jTeYx2r2Va35794tCScAXWJG9hU2HcgxzMowaZUnu": 0.00752978, + # "DG2xFkPdDwKUoBkzGAhQtLpSGzfXLiCYPEzeKH2Ad24p": 1.84575, + # "B3uGHFRpSUuGEDWjqB9LWWxafQj8VTvpMucEyoxzws5H": 0.02330273, + # "zMFqXuoyrn5w17PFurTqxB7GsS71fp9dfk6XFwxbPCy": 0.00721412, + # "5WvPKSJXzVE2orvbkJ8wsQmmQKqTv9sGBPksV4adViw3": 0.02659103, + # "WAVES": 1, + # "BrjUWjndUanm5VsJkbUip8VRYy6LWJePtxya3FNv4TQa": 0.03433583 + # } + # } + # }, + # "networkByte": 87, + # "matcherVersion": "2.1.3.5", + # "status": "SimpleResponse", + # "priceAssets": [ + # "Ft8X1v1LTa1ABafufpaCWyVj8KkaxUWE6xBhW6sNFJck", + # "DG2xFkPdDwKUoBkzGAhQtLpSGzfXLiCYPEzeKH2Ad24p", + # "34N9YcEETLWn93qYQ64EsP1x89tSruJU44RrEMSXXEPJ", + # "Gtb1WRznfchDnTh37ezoDTJ4wcoKaRsKqKjJjy7nm2zU", + # "2mX5DzVKWrAJw8iwdJnV2qtoeVG9h5nTDpTqC1wb1WEN", + # "8LQW8f7P5d5PZM7GtZEBgaqRPGSzS3DfPuiXrURJ4AJS", + # "WAVES", + # "474jTeYx2r2Va35794tCScAXWJG9hU2HcgxzMowaZUnu", + # "zMFqXuoyrn5w17PFurTqxB7GsS71fp9dfk6XFwxbPCy", + # "62LyMjcr2DtiyF5yVXFhoQ2q414VPPJXjsNYp72SuDCH", + # "HZk1mbfuJpmxU1Fs4AX5MWLVYtctsNcg6e2C6VKqK8zk", + # "B3uGHFRpSUuGEDWjqB9LWWxafQj8VTvpMucEyoxzws5H", + # "5WvPKSJXzVE2orvbkJ8wsQmmQKqTv9sGBPksV4adViw3", + # "BrjUWjndUanm5VsJkbUip8VRYy6LWJePtxya3FNv4TQa", + # "4LHHvYGNKJUg5hj65aGD5vgScvCBmLpdRFtjokvCjSL8" + # ] + # } + quotes = {} + priceAssets = self.safe_value(response, 'priceAssets') + for i in range(0, len(priceAssets)): + quotes[priceAssets[i]] = True + self.options['quotes'] = quotes + return quotes + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for wavesexchange + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = await self.marketGetTickers() + # + # [ + # { + # "symbol": "WAVES/BTC", + # "amountAssetID": "WAVES", + # "amountAssetName": "Waves", + # "amountAssetDecimals": 8, + # "amountAssetTotalSupply": "106908766.00000000", + # "amountAssetMaxSupply": "106908766.00000000", + # "amountAssetCirculatingSupply": "106908766.00000000", + # "priceAssetID": "8LQW8f7P5d5PZM7GtZEBgaqRPGSzS3DfPuiXrURJ4AJS", + # "priceAssetName": "WBTC", + # "priceAssetDecimals": 8, + # "priceAssetTotalSupply": "20999999.96007507", + # "priceAssetMaxSupply": "20999999.96007507", + # "priceAssetCirculatingSupply": "20999999.66019601", + # "24h_open": "0.00032688", + # "24h_high": "0.00033508", + # "24h_low": "0.00032443", + # "24h_close": "0.00032806", + # "24h_vwap": "0.00032988", + # "24h_volume": "42349.69440104", + # "24h_priceVolume": "13.97037207", + # "timestamp":1640232379124 + # } + # ... + # ] + # + result = [] + for i in range(0, len(response)): + entry = response[i] + baseId = self.safe_string(entry, 'amountAssetID') + quoteId = self.safe_string(entry, 'priceAssetID') + id = baseId + '/' + quoteId + marketId = self.safe_string(entry, 'symbol') + base, quote = marketId.split('/') + base = self.safe_currency_code(base) + quote = self.safe_currency_code(quote) + symbol = base + '/' + quote + result.append({ + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': None, + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(self.parse_precision(self.safe_string(entry, 'amountAssetDecimals'))), + 'price': self.parse_number(self.parse_precision(self.safe_string(entry, 'priceAssetDecimals'))), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': None, + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + 'info': entry, + }) + return result + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://matcher.waves.exchange/api-docs/index.html#/markets/getOrderBook + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request = self.extend({ + 'baseId': market['baseId'], + 'quoteId': market['quoteId'], + }, params) + response = await self.matcherGetMatcherOrderbookBaseIdQuoteId(request) + timestamp = self.safe_integer(response, 'timestamp') + bids = self.parse_order_book_side(self.safe_value(response, 'bids'), market, limit) + asks = self.parse_order_book_side(self.safe_value(response, 'asks'), market, limit) + return { + 'symbol': symbol, + 'bids': bids, + 'asks': asks, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'nonce': None, + } + + def parse_order_book_side(self, bookSide, market=None, limit: Int = None): + precision = market['precision'] + wavesPrecision = self.safe_string(self.options, 'wavesPrecision', '1e-8') + amountPrecisionString = self.safe_string(precision, 'amount') + pricePrecisionString = self.safe_string(precision, 'price') + difference = Precise.string_div(amountPrecisionString, pricePrecisionString) + pricePrecision = Precise.string_div(wavesPrecision, difference) + result = [] + for i in range(0, len(bookSide)): + entry = bookSide[i] + entryPrice = self.safe_string(entry, 'price', '0') + entryAmount = self.safe_string(entry, 'amount', '0') + price = None + amount = None + if (pricePrecision is not None) and (entryPrice is not None): + price = Precise.string_mul(entryPrice, pricePrecision) + if (amountPrecisionString is not None) and (entryAmount is not None): + amount = Precise.string_mul(entryAmount, amountPrecisionString) + if (limit is not None) and (i > limit): + break + result.append([ + self.parse_number(price), + self.parse_number(amount), + ]) + return result + + def check_required_keys(self): + if self.apiKey is None: + raise AuthenticationError(self.id + ' requires apiKey credential') + if self.secret is None: + raise AuthenticationError(self.id + ' requires secret credential') + apiKeyBytes = None + secretKeyBytes = None + try: + apiKeyBytes = self.base58_to_binary(self.apiKey) + except Exception as e: + raise AuthenticationError(self.id + ' apiKey must be a base58 encoded public key') + try: + secretKeyBytes = self.base58_to_binary(self.secret) + except Exception as e: + raise AuthenticationError(self.id + ' secret must be a base58 encoded private key') + hexApiKeyBytes = self.binary_to_base16(apiKeyBytes) + hexSecretKeyBytes = self.binary_to_base16(secretKeyBytes) + if len(hexApiKeyBytes) != 64: + raise AuthenticationError(self.id + ' apiKey must be a base58 encoded public key') + if len(hexSecretKeyBytes) != 64: + raise AuthenticationError(self.id + ' secret must be a base58 encoded private key') + return True + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + query = self.omit(params, self.extract_params(path)) + isCancelOrder = path == 'matcher/orders/{wavesAddress}/cancel' + path = self.implode_params(path, params) + url = self.urls['api'][api] + '/' + path + queryString = self.urlencode_with_array_repeat(query) + if (api == 'private') or (api == 'forward'): + headers = { + 'Accept': 'application/json', + } + accessToken = self.safe_string(self.options, 'accessToken') + if accessToken: + headers['Authorization'] = 'Bearer ' + accessToken + if method == 'POST': + headers['content-type'] = 'application/json' + else: + headers['content-type'] = 'application/x-www-form-urlencoded' + if isCancelOrder: + body = self.json([query['orderId']]) + queryString = '' + if len(queryString) > 0: + url += '?' + queryString + elif api == 'matcher': + if method == 'POST': + headers = { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + } + body = self.json(query) + else: + headers = query + else: + if method == 'POST': + headers = { + 'content-type': 'application/json', + } + body = self.json(query) + else: + headers = { + 'content-type': 'application/x-www-form-urlencoded', + } + if len(queryString) > 0: + url += '?' + queryString + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + async def sign_in(self, params={}): + """ + sign in, must be called prior to using other authenticated methods + + https://docs.wx.network/en/api/auth/oauth2-token + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns: response from exchange + """ + if not self.safe_string(self.options, 'accessToken'): + prefix = 'ffffff01' + expiresDelta = 60 * 60 * 24 * 7 + seconds = self.sum(self.seconds(), expiresDelta) + seconds = str(seconds) + clientId = 'wx.network' + # W for production, T for testnet + defaultMessagePrefix = self.safe_string(self.options, 'messagePrefix', 'W') + message = defaultMessagePrefix + ':' + clientId + ':' + seconds + messageHex = self.binary_to_base16(self.encode(message)) + payload = prefix + messageHex + hexKey = self.binary_to_base16(self.base58_to_binary(self.secret)) + signature = self.axolotl(payload, hexKey, 'ed25519') + request: dict = { + 'grant_type': 'password', + 'scope': 'general', + 'username': self.apiKey, + 'password': seconds + ':' + signature, + 'client_id': clientId, + } + response = await self.privatePostOauth2Token(request) + # {access_token: "eyJhbGciOXJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzaWciOiJiaTZiMVhMQlo0M1Q4QmRTSlVSejJBZGlQdVlpaFZQYVhhVjc4ZGVIOEpTM3M3NUdSeEU1VkZVOE5LRUI0UXViNkFHaUhpVFpuZ3pzcnhXdExUclRvZTgiLCJhIjoiM1A4VnpMU2EyM0VXNUNWY2tIYlY3ZDVCb043NWZGMWhoRkgiLCJuYiI6IlciLCJ1c2VyX25hbWUiOiJBSFhuOG5CQTRTZkxRRjdoTFFpU24xNmt4eWVoaml6QkdXMVRkcm1TWjFnRiIsInNjb3BlIjpbImdlbmVyYWwiXSwibHQiOjYwNDc5OSwicGsiOiJBSFhuOG5CQTRTZkxRRjdoTFFpU24xNmt4eWVoaml6QkdXMVRkcm1TWjFnRiIsImV4cCI6MTU5MTk3NTA1NywiZXhwMCI6MTU5MTk3NTA1NywianRpIjoiN2JhOTUxMTMtOGI2MS00NjEzLTlkZmYtNTEwYTc0NjlkOWI5IiwiY2lkIjoid2F2ZXMuZXhjaGFuZ2UifQ.B-XwexBnUAzbWknVN68RKT0ZP5w6Qk1SKJ8usL3OIwDEzCUUX9PjW-5TQHmiCRcA4oft8lqXEiCwEoNfsblCo_jTpRo518a1vZkIbHQk0-13Dm1K5ewGxfxAwBk0g49odcbKdjl64TN1yM_PO1VtLVuiTeZP-XF-S42Uj-7fcO-r7AulyQLuTE0uo-Qdep8HDCk47rduZwtJOmhFbCCnSgnLYvKWy3CVTeldsR77qxUY-vy8q9McqeP7Id-_MWnsob8vWXpkeJxaEsw1Fke1dxApJaJam09VU8EB3ZJWpkT7V8PdafIrQGeexx3jhKKxo7rRb4hDV8kfpVoCgkvFan", + # "token_type": "bearer", + # "refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzaWciOiJiaTZiMVhMQlo0M1Q4QmRTSlVSejJBZGlQdVlpaFZQYVhhVjc4ZGVIOEpTM3M3NUdSeEU1VkZVOE5LRUI0UXViNkFHaUhpVFpuZ3pzcnhXdExUclRvZTgiLCJhIjoiM1A4VnpMU2EyM0VXNUNWY2tIYlY3ZDVCb043NWZGMWhoRkgiLCJuYiI6IlciLCJ1c2VyX25hbWUiOiJBSFhuOG5CQTRTZkxRRjdoTFFpU24xNmt4eWVoaml6QkdXMVRkcm1TWjFnRiIsInNjb3BlIjpbImdlbmVyYWwiXSwiYXRpIjoiN2JhOTUxMTMtOGI2MS00NjEzLTlkZmYtNTEwYTc0NjlkXWI5IiwibHQiOjYwNDc5OSwicGsiOiJBSFhuOG5CQTRTZkxRRjdoTFFpU24xNmt4eWVoaml6QkdXMVRkcm1TWjFnRiIsImV4cCI6MTU5Mzk2MjI1OCwiZXhwMCI6MTU5MTk3NTA1NywianRpIjoiM2MzZWRlMTktNjI5My00MTNlLWJmMWUtZTRlZDZlYzUzZTgzIiwiY2lkIjoid2F2ZXMuZXhjaGFuZ2UifQ.gD1Qj0jfqayfZpBvNY0t3ccMyK5hdbT7dY-_5L6LxwV0Knan4ndEtvygxlTOczmJUKtnA4T1r5GBFgNMZTvtViKZIbqZNysEg2OY8UxwDaF4VPeGJLg_QXEnn8wBeBQdyMafh9UQdwD2ci7x-saM4tOAGmncAygfTDxy80201gwDhfAkAGerb9kL00oWzSJScldxu--pNLDBUEHZt52MSEel10HGrzvZkkvvSh67vcQo5TOGb5KG6nh65UdJCwr41AVz4fbQPP-N2Nkxqy0TE_bqVzZxExXgvcS8TS0Z82T3ijJa_ct7B9wblpylBnvmyj3VycUzufD6uy8MUGq32D", + # "expires_in": 604798, + # "scope": "general"} + self.options['accessToken'] = self.safe_string(response, 'access_token') + return self.options['accessToken'] + return None + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "symbol": "WAVES/BTC", + # "amountAssetID": "WAVES", + # "amountAssetName": "Waves", + # "amountAssetDecimals": 8, + # "amountAssetTotalSupply": "106908766.00000000", + # "amountAssetMaxSupply": "106908766.00000000", + # "amountAssetCirculatingSupply": "106908766.00000000", + # "priceAssetID": "8LQW8f7P5d5PZM7GtZEBgaqRPGSzS3DfPuiXrURJ4AJS", + # "priceAssetName": "WBTC", + # "priceAssetDecimals": 8, + # "priceAssetTotalSupply": "20999999.96007507", + # "priceAssetMaxSupply": "20999999.96007507", + # "priceAssetCirculatingSupply": "20999999.66019601", + # "24h_open": "0.00032688", + # "24h_high": "0.00033508", + # "24h_low": "0.00032443", + # "24h_close": "0.00032806", + # "24h_vwap": "0.00032988", + # "24h_volume": "42349.69440104", + # "24h_priceVolume": "13.97037207", + # "timestamp":1640232379124 + # } + # + # fetch ticker + # + # { + # "firstPrice": "21749", + # "lastPrice": "22000", + # "volume": "0.73747149", + # "quoteVolume": "16409.44564928645471", + # "high": "23589.999941", + # "low": "21010.000845", + # "weightedAveragePrice": "22250.955964", + # "txsCount": "148", + # "volumeWaves": "0.0000000000680511203072" + # } + # + timestamp = self.safe_integer(ticker, 'timestamp') + marketId = self.safe_string(ticker, 'symbol') + market = self.safe_market(marketId, market, '/') + symbol = market['symbol'] + last = self.safe_string_2(ticker, '24h_close', 'lastPrice') + low = self.safe_string_2(ticker, '24h_low', 'low') + high = self.safe_string_2(ticker, '24h_high', 'high') + vwap = self.safe_string_2(ticker, '24h_vwap', 'weightedAveragePrice') + baseVolume = self.safe_string_2(ticker, '24h_volume', 'volume') + quoteVolume = self.safe_string_2(ticker, '24h_priceVolume', 'quoteVolume') + open = self.safe_string_2(ticker, '24h_open', 'firstPrice') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': high, + 'low': low, + 'bid': None, + 'bidVolume': None, + 'ask': None, + 'askVolume': None, + 'vwap': vwap, + 'open': open, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'info': ticker, + }, market) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://api.wavesplatform.com/v0/docs/#/pairs/getPairsListAll + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pairs': market['id'], + } + response = await self.publicGetPairs(self.extend(request, params)) + # + # { + # "__type":"list", + # "data":[ + # { + # "__type":"pair", + # "data":{ + # "firstPrice":0.00012512, + # "lastPrice":0.00012441, + # "low":0.00012167, + # "high":0.00012768, + # "weightedAveragePrice":0.000124710697407246, + # "volume":209554.26356614, + # "quoteVolume":26.1336583539951, + # "volumeWaves":209554.26356614, + # "txsCount":6655 + # }, + # "amountAsset":"WAVES", + # "priceAsset":"8LQW8f7P5d5PZM7GtZEBgaqRPGSzS3DfPuiXrURJ4AJS" + # } + # ] + # } + # + data = self.safe_value(response, 'data', []) + ticker = self.safe_value(data, 0, {}) + dataTicker = self.safe_dict(ticker, 'data', {}) + return self.parse_ticker(dataTicker, market) + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + :param str[] [symbols]: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + response = await self.marketGetTickers(params) + # + # [ + # { + # "symbol": "WAVES/BTC", + # "amountAssetID": "WAVES", + # "amountAssetName": "Waves", + # "amountAssetDecimals": 8, + # "amountAssetTotalSupply": "106908766.00000000", + # "amountAssetMaxSupply": "106908766.00000000", + # "amountAssetCirculatingSupply": "106908766.00000000", + # "priceAssetID": "8LQW8f7P5d5PZM7GtZEBgaqRPGSzS3DfPuiXrURJ4AJS", + # "priceAssetName": "WBTC", + # "priceAssetDecimals": 8, + # "priceAssetTotalSupply": "20999999.96007507", + # "priceAssetMaxSupply": "20999999.96007507", + # "priceAssetCirculatingSupply": "20999999.66019601", + # "24h_open": "0.00032688", + # "24h_high": "0.00033508", + # "24h_low": "0.00032443", + # "24h_close": "0.00032806", + # "24h_vwap": "0.00032988", + # "24h_volume": "42349.69440104", + # "24h_priceVolume": "13.97037207", + # "timestamp":1640232379124 + # } + # ... + # ] + # + return self.parse_tickers(response, symbols) + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://api.wavesplatform.com/v0/docs/#/candles/getCandles + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest candle to fetch + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'baseId': market['baseId'], + 'quoteId': market['quoteId'], + 'interval': self.safe_string(self.timeframes, timeframe, timeframe), + } + allowedCandles = self.safe_integer(self.options, 'allowedCandles', 1440) + until = self.safe_integer(params, 'until') + untilIsDefined = until is not None + if limit is None: + limit = allowedCandles + limit = min(allowedCandles, limit) + duration = self.parse_timeframe(timeframe) * 1000 + if since is None: + now = self.milliseconds() + timeEnd = until if untilIsDefined else now + durationRoundedTimestamp = self.parse_to_int(timeEnd / duration) * duration + delta = (limit - 1) * duration + timeStart = durationRoundedTimestamp - delta + request['timeStart'] = str(timeStart) + if untilIsDefined: + request['timeEnd'] = str(until) + else: + request['timeStart'] = str(since) + if untilIsDefined: + request['timeEnd'] = str(until) + else: + timeEnd = self.sum(since, duration * limit) + request['timeEnd'] = str(timeEnd) + params = self.omit(params, 'until') + response = await self.publicGetCandlesBaseIdQuoteId(self.extend(request, params)) + # + # { + # "__type": "list", + # "data": [ + # { + # "__type": "candle", + # "data": { + # "time": "2020-06-09T14:47:00.000Z", + # "open": 0.0250385, + # "close": 0.0250385, + # "high": 0.0250385, + # "low": 0.0250385, + # "volume": 0.01033012, + # "quoteVolume": 0.00025865, + # "weightedAveragePrice": 0.0250385, + # "maxHeight": 2099399, + # "txsCount": 5, + # "timeClose": "2020-06-09T14:47:59.999Z" + # } + # } + # ] + # } + # + data = self.safe_value(response, 'data', []) + result = self.parse_ohlcvs(data, market, timeframe, since, limit) + result = self.filter_future_candles(result) + lastClose = None + length = len(result) + for i in range(0, len(result)): + j = length - i - 1 + entry = result[j] + open = entry[1] + if open is None: + entry[1] = lastClose + entry[2] = lastClose + entry[3] = lastClose + entry[4] = lastClose + result[j] = entry + lastClose = entry[4] + return result + + def filter_future_candles(self, ohlcvs): + result = [] + timestamp = self.milliseconds() + for i in range(0, len(ohlcvs)): + if ohlcvs[i][0] > timestamp: + # stop when getting data from the future + break + result.append(ohlcvs[i]) + return result + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # { + # "__type": "candle", + # "data": { + # "time": "2020-06-05T20:46:00.000Z", + # "open": 240.573975, + # "close": 240.573975, + # "high": 240.573975, + # "low": 240.573975, + # "volume": 0.01278413, + # "quoteVolume": 3.075528, + # "weightedAveragePrice": 240.573975, + # "maxHeight": 2093895, + # "txsCount": 5, + # "timeClose": "2020-06-05T20:46:59.999Z" + # } + # } + # + data = self.safe_value(ohlcv, 'data', {}) + return [ + self.parse8601(self.safe_string(data, 'time')), + self.safe_number(data, 'open'), + self.safe_number(data, 'high'), + self.safe_number(data, 'low'), + self.safe_number(data, 'close'), + self.safe_number(data, 'volume', 0), + ] + + async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + await self.sign_in() + networks = self.safe_value(self.options, 'networks', {}) + rawNetwork = self.safe_string_upper(params, 'network') + network = self.safe_string(networks, rawNetwork, rawNetwork) + params = self.omit(params, ['network']) + supportedCurrencies = await self.privateGetPlatforms() + # + # { + # "type": "list", + # "page_info": { + # "has_next_page": False, + # "last_cursor": null + # }, + # "items": [ + # { + # "type": "platform", + # "id": "ETH", + # "name": "Ethereum", + # "currencies": [ + # "BAG", + # "BNT", + # "CRV", + # "EGG", + # "ETH", + # "EURN", + # "FL", + # "NSBT", + # "USDAP", + # "USDC", + # "USDFL", + # "USDN", + # "USDT", + # "WAVES" + # ] + # } + # ] + # } + # + currencies: dict = {} + networksByCurrency: dict = {} + items = self.safe_value(supportedCurrencies, 'items', []) + for i in range(0, len(items)): + entry = items[i] + currencyId = self.safe_string(entry, 'id') + innerCurrencies = self.safe_value(entry, 'currencies', []) + for j in range(0, len(innerCurrencies)): + currencyCode = self.safe_string(innerCurrencies, j) + currencies[currencyCode] = True + if not (currencyCode in networksByCurrency): + networksByCurrency[currencyCode] = {} + networksByCurrency[currencyCode][currencyId] = True + if not (code in currencies): + codes = list(currencies.keys()) + raise ExchangeError(self.id + ' fetchDepositAddress() ' + code + ' not supported. Currency code must be one of ' + ', '.join(codes)) + response = None + if network is None: + request: dict = { + 'currency': code, + } + response = await self.privateGetDepositAddressesCurrency(self.extend(request, params)) + else: + supportedNetworks = networksByCurrency[code] + if not (network in supportedNetworks): + supportedNetworkKeys = list(supportedNetworks.keys()) + raise ExchangeError(self.id + ' ' + network + ' network ' + code + ' deposit address not supported. Network must be one of ' + ', '.join(supportedNetworkKeys)) + if network == 'WAVES': + request: dict = { + 'publicKey': self.apiKey, + } + responseInner = await self.nodeGetAddressesPublicKeyPublicKey(self.extend(request, request)) + addressInner = self.safe_string(response, 'address') + return { + 'info': responseInner, + 'currency': code, + 'network': network, + 'address': addressInner, + 'tag': None, + } + else: + request: dict = { + 'currency': code, + 'platform': network, + } + response = await self.privateGetDepositAddressesCurrencyPlatform(self.extend(request, params)) + # + # { + # "type": "deposit_addresses", + # "currency": { + # "type": "deposit_currency", + # "id": "ERGO", + # "waves_asset_id": "5dJj4Hn9t2Ve3tRpNGirUHy4yBK6qdJRAJYV21yPPuGz", + # "platform_id": "BSC", + # "decimals": 9, + # "status": "active", + # "allowed_amount": { + # "min": 0.001, + # "max": 100000 + # }, + # "fees": { + # "flat": 0, + # "rate": 0 + # } + # }, + # "deposit_addresses": [ + # "9fRAAQjF8Yqg7qicQCL884zjimsRnuwsSavsM1rUdDaoG8mThku" + # ] + # } + currency = self.safe_value(response, 'currency') + networkId = self.safe_string(currency, 'platform_id') + networkByIds = self.safe_value(self.options, 'networkByIds', {}) + unifiedNetwork = self.safe_string(networkByIds, networkId, networkId) + addresses = self.safe_value(response, 'deposit_addresses') + address = self.safe_string(addresses, 0) + return { + 'info': response, + 'currency': code, + 'network': unifiedNetwork, + 'address': address, + 'tag': None, + } + + async def get_matcher_public_key(self): + # self method returns a single string + matcherPublicKey = self.safe_string(self.options, 'matcherPublicKey') + if matcherPublicKey: + return matcherPublicKey + else: + response = await self.matcherGetMatcher() + # remove trailing quotes from string response + self.options['matcherPublicKey'] = response[1:len(response) - 1] + return self.options['matcherPublicKey'] + + def get_asset_bytes(self, currencyId): + if currencyId == 'WAVES': + return self.number_to_be(0, 1) + else: + return self.binary_concat(self.number_to_be(1, 1), self.base58_to_binary(currencyId)) + + def get_asset_id(self, currencyId): + if currencyId == 'WAVES': + return '' + return currencyId + + def to_real_currency_amount(self, code: str, amount: float, networkCode=None): + currency = self.currency(code) + stringValue = Precise.string_div(self.number_to_string(amount), self.safe_string(currency, 'precision')) + return int(stringValue) + + def from_real_currency_amount(self, code: str, amountString: str): + if not (code in self.currencies): + return amountString + currency = self.currency(code) + precisionAmount = self.safe_string(currency, 'precision') + return Precise.string_mul(amountString, precisionAmount) + + def to_real_symbol_price(self, symbol: str, price: float): + market = self.market(symbol) + stringValue = Precise.string_div(self.number_to_string(price), self.safe_string(market['precision'], 'price')) + return int(stringValue) + + def from_real_symbol_price(self, symbol: str, priceString: str): + market = self.markets[symbol] + return Precise.string_mul(priceString, self.safe_string(market['precision'], 'price')) + + def to_real_symbol_amount(self, symbol: str, amount: float): + market = self.market(symbol) + stringValue = Precise.string_div(self.number_to_string(amount), self.safe_string(market['precision'], 'amount')) + return int(stringValue) + + def from_real_symbol_amount(self, symbol: str, amountString: str): + market = self.markets[symbol] + return Precise.string_mul(amountString, market['precision']['amount']) + + def safe_get_dynamic(self, settings): + orderFee = self.safe_value(settings, 'orderFee') + if 'dynamic' in orderFee: + return self.safe_value(orderFee, 'dynamic') + else: + return self.safe_value(orderFee['composite']['default'], 'dynamic') + + def safe_get_rates(self, dynamic): + rates = self.safe_value(dynamic, 'rates') + if rates is None: + return {'WAVES': 1} + return rates + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://matcher.waves.exchange/api-docs/index.html#/serialize/serializeOrder + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: The price at which a stop order is triggered at + :returns dict: an `order structure ` + """ + self.check_required_dependencies() + self.check_required_keys() + await self.load_markets() + market = self.market(symbol) + matcherPublicKey = await self.get_matcher_public_key() + amountAsset = self.get_asset_id(market['baseId']) + priceAsset = self.get_asset_id(market['quoteId']) + isMarketOrder = (type == 'market') + triggerPrice = self.safe_float_2(params, 'triggerPrice', 'stopPrice') + isStopOrder = (triggerPrice is not None) + if (isMarketOrder) and (price is None): + raise InvalidOrder(self.id + ' createOrder() requires a price argument for ' + type + ' orders to determine the max price for buy and the min price for sell') + timestamp = self.milliseconds() + defaultExpiryDelta = None + defaultExpiryDelta, params = self.handle_option_and_params(params, 'createOrder', 'defaultExpiry', self.safe_integer(self.options, 'createOrderDefaultExpiry', 2419200000)) + expiration = self.sum(timestamp, defaultExpiryDelta) + matcherFees = await self.get_fees_for_asset(symbol, side, amount, price) + # { + # "base":{ + # "feeAssetId":"WAVES", # varies depending on the trading pair + # "matcherFee":"1000000" + # }, + # "discount":{ + # "feeAssetId":"EMAMLxDnv3xiz8RXg8Btj33jcEw3wLczL3JKYYmuubpc", + # "matcherFee":"4077612" + # } + # } + base = self.safe_value_2(matcherFees, 'base', 'discount') + baseFeeAssetId = self.safe_string(base, 'feeAssetId') + baseFeeAsset = self.safe_currency_code(baseFeeAssetId) + baseMatcherFee = self.safe_string(base, 'matcherFee') + discount = self.safe_value(matcherFees, 'discount') + discountFeeAssetId = self.safe_string(discount, 'feeAssetId') + discountFeeAsset = self.safe_currency_code(discountFeeAssetId) + discountMatcherFee = self.safe_string(discount, 'matcherFee') + matcherFeeAssetId = None + matcherFee = None + # check first if user supplied asset fee is valid + if ('feeAsset' in params) or ('feeAsset' in self.options): + feeAsset = self.safe_string(params, 'feeAsset', self.safe_string(self.options, 'feeAsset')) + feeCurrency = self.currency(feeAsset) + matcherFeeAssetId = self.safe_string(feeCurrency, 'id') + balances = await self.fetch_balance() + if matcherFeeAssetId is not None: + if baseFeeAssetId != matcherFeeAssetId and discountFeeAssetId != matcherFeeAssetId: + raise InvalidOrder(self.id + ' asset fee must be ' + baseFeeAsset + ' or ' + discountFeeAsset) + matcherFeeAsset = self.safe_currency_code(matcherFeeAssetId) + rawMatcherFee = baseMatcherFee if (matcherFeeAssetId == baseFeeAssetId) else discountMatcherFee + floatMatcherFee = float(self.from_real_currency_amount(matcherFeeAsset, rawMatcherFee)) + if (matcherFeeAsset in balances) and (balances[matcherFeeAsset]['free'] >= floatMatcherFee): + matcherFee = int(rawMatcherFee) + else: + raise InsufficientFunds(self.id + ' not enough funds of the selected asset fee') + floatBaseMatcherFee = self.from_real_currency_amount(baseFeeAsset, baseMatcherFee) + floatDiscountMatcherFee = self.from_real_currency_amount(discountFeeAsset, discountMatcherFee) + if matcherFeeAssetId is None: + # try to the pay the fee using the base first then discount asset + if (baseFeeAsset in balances) and (balances[baseFeeAsset]['free'] >= float(floatBaseMatcherFee)): + matcherFeeAssetId = baseFeeAssetId + matcherFee = int(baseMatcherFee) + else: + if (discountFeeAsset in balances) and (balances[discountFeeAsset]['free'] >= float(floatDiscountMatcherFee)): + matcherFeeAssetId = discountFeeAssetId + matcherFee = int(discountMatcherFee) + if matcherFeeAssetId is None: + raise InsufficientFunds(self.id + ' not enough funds on none of the eligible asset fees: ' + baseFeeAsset + ' ' + floatBaseMatcherFee + ' or ' + discountFeeAsset + ' ' + floatDiscountMatcherFee) + amount = self.to_real_symbol_amount(symbol, amount) + price = self.to_real_symbol_price(symbol, price) + assetPair: dict = { + 'amountAsset': amountAsset, + 'priceAsset': priceAsset, + } + sandboxMode = self.safe_bool(self.options, 'sandboxMode', False) + chainId = 84 if (sandboxMode) else 87 + body: dict = { + 'senderPublicKey': self.apiKey, + 'matcherPublicKey': matcherPublicKey, + 'assetPair': assetPair, + 'orderType': side, + 'price': price, + 'amount': amount, + 'timestamp': timestamp, + 'expiration': expiration, + 'matcherFee': int(matcherFee), + 'priceMode': 'assetDecimals', + 'version': 4, + 'chainId': chainId, + } + if isStopOrder: + # + # { + # "v": 1, # version(int) + # "c": { # condition(object) + # "t": "sp", # condition type. for now only "stop-price"(string) + # "v": { # value(object) + # "p": "123", # price(long) + # }, + # }, + # } + # + attachment: dict = { + 'v': 1, + 'c': { + 't': 'sp', + 'v': { + 'p': self.to_real_symbol_price(symbol, triggerPrice), + }, + }, + } + body['attachment'] = self.binary_to_base58(self.encode(json.dumps(attachment))) + if matcherFeeAssetId != 'WAVES': + body['matcherFeeAssetId'] = matcherFeeAssetId + serializedOrder = await self.matcherPostMatcherOrdersSerialize(body) + if (serializedOrder[0] == '"') and (serializedOrder[(len(serializedOrder) - 1)] == '"'): + serializedOrder = serializedOrder[1:len(serializedOrder) - 1] + signature = self.axolotl(self.binary_to_base16(self.base58_to_binary(serializedOrder)), self.binary_to_base16(self.base58_to_binary(self.secret)), 'ed25519') + body['signature'] = signature + # + # { + # "success": True, + # "message": { + # "version": 4, + # "id": "8VR49dLZFaYcVwzx9TqVMTAZCSUoyB74kLUHrEPCSJgN", + # "sender": "3MpEdBXtsRHRj2TvZURSb8uLDxzneVbYczW", + # "senderPublicKey": "8aUTNqHGCBiubySBRhcS1N6NC5jLczhVcndRfMAuwtkY", + # "matcherPublicKey": "8QUAqtTckM5B8gvcuP7mMswat9SjKUuafJMusEoSn1Gy", + # "assetPair": { + # "amountAsset": "EMAMLxDnv3xiz8RXg8Btj33jcEw3wLczL3JKYYmuubpc", + # "priceAsset": "25FEqEjRkqK6yCkiT7Lz6SAYz7gUFCtxfCChnrVFD5AT" + # }, + # "orderType": "sell", + # "amount": 100000, + # "price": 480000, + # "timestamp": 1690852043772, + # "expiration": 1693271243772, + # "matcherFee": 83327570, + # "signature": "3QYDWQVSP4kdqpTLodCuboh8bpWd6GW5s1pQyKdce1JBDwX6t4kH5Xtuq35pqo94gxjo3cfG6k6Xuic2JaYLubkK", + # "proofs": [ + # "3QYDWQVSP4kdqpTLodCuboh8bpWd6GW5s1pQyKdce1JBDwX6t4kH5Xtuq35pqo94gxjo3cfG6k6Xuic2JaYLubkK" + # ], + # "matcherFeeAssetId": "EMAMLxDnv3xiz8RXg8Btj33jcEw3wLczL3JKYYmuubpc", + # "eip712Signature": null, + # "priceMode": "assetDecimals", + # "attachment": "2PQ4akZHnMSZrQissuu5uudoXbgsipeDnFcRtXtjVgkdm1gUWEgGzp" + # }, + # "status": "OrderAccepted" + # } + # + if isMarketOrder: + response = await self.matcherPostMatcherOrderbookMarket(self.extend(body, params)) + value = self.safe_dict(response, 'message') + return self.parse_order(value, market) + else: + response = await self.matcherPostMatcherOrderbook(self.extend(body, params)) + value = self.safe_dict(response, 'message') + return self.parse_order(value, market) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://matcher.waves.exchange/api-docs/index.html#/cancel/cancelOrdersByIdsWithKeyOrSignature + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.check_required_dependencies() + self.check_required_keys() + await self.sign_in() + wavesAddress = await self.get_waves_address() + response = await self.forwardPostMatcherOrdersWavesAddressCancel({ + 'wavesAddress': wavesAddress, + 'orderId': id, + }) + # { + # "success":true, + # "message":[[{"orderId":"EBpJeGM36KKFz5gTJAUKDBm89V8wqxKipSFBdU35AN3c","success":true,"status":"OrderCanceled"}]], + # "status":"BatchCancelCompleted" + # } + message = self.safe_value(response, 'message') + firstMessage = self.safe_value(message, 0) + firstOrder = self.safe_value(firstMessage, 0) + returnedId = self.safe_string(firstOrder, 'orderId') + return self.safe_order({ + 'info': response, + 'id': returnedId, + 'clientOrderId': None, + 'timestamp': None, + 'datetime': None, + 'lastTradeTimestamp': None, + 'symbol': symbol, + 'type': None, + 'side': None, + 'price': None, + 'amount': None, + 'cost': None, + 'average': None, + 'filled': None, + 'remaining': None, + 'status': None, + 'fee': None, + 'trades': None, + }) + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://matcher.waves.exchange/api-docs/index.html#/status/getOrderStatusByPKAndIdWithSig + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.check_required_dependencies() + self.check_required_keys() + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + timestamp = self.milliseconds() + byteArray = [ + self.base58_to_binary(self.apiKey), + self.number_to_be(timestamp, 8), + ] + binary = self.binary_concat_array(byteArray) + hexSecret = self.binary_to_base16(self.base58_to_binary(self.secret)) + signature = self.axolotl(self.binary_to_base16(binary), hexSecret, 'ed25519') + request: dict = { + 'Timestamp': str(timestamp), + 'Signature': signature, + 'publicKey': self.apiKey, + 'orderId': id, + } + response = await self.matcherGetMatcherOrderbookPublicKeyOrderId(self.extend(request, params)) + return self.parse_order(response, market) + + async def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + self.check_required_dependencies() + self.check_required_keys() + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrders() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + timestamp = self.milliseconds() + byteArray = [ + self.base58_to_binary(self.apiKey), + self.number_to_be(timestamp, 8), + ] + binary = self.binary_concat_array(byteArray) + hexSecret = self.binary_to_base16(self.base58_to_binary(self.secret)) + signature = self.axolotl(self.binary_to_base16(binary), hexSecret, 'ed25519') + request: dict = { + 'Accept': 'application/json', + 'Timestamp': str(timestamp), + 'Signature': signature, + 'publicKey': self.apiKey, + 'baseId': market['baseId'], + 'quoteId': market['quoteId'], + } + response = await self.matcherGetMatcherOrderbookBaseIdQuoteIdPublicKeyPublicKey(self.extend(request, params)) + # [{id: "3KicDeWayY2mdrRoYdCkP3gUAoUZUNT1AA6GAtWuPLfa", + # "type": "sell", + # "orderType": "limit", + # "amount": 1, + # "fee": 300000, + # "price": 100000000, + # "timestamp": 1591651254076, + # "filled": 0, + # "filledFee": 0, + # "feeAsset": "WAVES", + # "status": "Accepted", + # "assetPair": + # {amountAsset: null, + # "priceAsset": "8LQW8f7P5d5PZM7GtZEBgaqRPGSzS3DfPuiXrURJ4AJS"}, + # "avgWeighedPrice": 0}, ...] + return self.parse_orders(response, market, since, limit) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + await self.sign_in() + market = None + if symbol is not None: + market = self.market(symbol) + address = await self.get_waves_address() + request: dict = { + 'address': address, + 'activeOnly': True, + } + response = await self.forwardGetMatcherOrdersAddress(request) + return self.parse_orders(response, market, since, limit) + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + await self.sign_in() + market = None + if symbol is not None: + market = self.market(symbol) + address = await self.get_waves_address() + request: dict = { + 'address': address, + 'closedOnly': True, + } + response = await self.forwardGetMatcherOrdersAddress(request) + # [ + # { + # "id": "9aXcxvXai73jbAm7tQNnqaQ2PwUjdmWuyjvRTKAHsw4f", + # "type": "buy", + # "orderType": "limit", + # "amount": 23738330, + # "fee": 300000, + # "price": 3828348334, + # "timestamp": 1591926905636, + # "filled": 23738330, + # "filledFee": 300000, + # "feeAsset": "WAVES", + # "status": "Filled", + # "assetPair": { + # "amountAsset": "HZk1mbfuJpmxU1Fs4AX5MWLVYtctsNcg6e2C6VKqK8zk", + # "priceAsset": null + # }, + # "avgWeighedPrice": 3828348334 + # }, ... + # ] + return self.parse_orders(response, market, since, limit) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'Cancelled': 'canceled', + 'Accepted': 'open', + 'Filled': 'closed', + 'PartiallyFilled': 'open', + } + return self.safe_string(statuses, status, status) + + def get_symbol_from_asset_pair(self, assetPair): + # a blank string or null can indicate WAVES + baseId = self.safe_string(assetPair, 'amountAsset', 'WAVES') + quoteId = self.safe_string(assetPair, 'priceAsset', 'WAVES') + return self.safe_currency_code(baseId) + '/' + self.safe_currency_code(quoteId) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder + # + # { + # "version": 4, + # "id": "BshyeHXDfJmTnjTdBYt371jD4yWaT3JTP6KpjpsiZepS", + # "sender": "3P8VzLSa23EW5CVckHbV7d5BoN75fF1hhFH", + # "senderPublicKey": "AHXn8nBA4SfLQF7hLQiSn16kxyehjizBGW1TdrmSZ1gF", + # "matcherPublicKey": "9cpfKN9suPNvfeUNphzxXMjcnn974eme8ZhWUjaktzU5", + # "assetPair": { + # "amountAsset": "474jTeYx2r2Va35794tCScAXWJG9hU2HcgxzMowaZUnu", + # "priceAsset": "DG2xFkPdDwKUoBkzGAhQtLpSGzfXLiCYPEzeKH2Ad24p", + # }, + # "orderType": "buy", + # "amount": 10000, + # "price": 400000000, + # "timestamp": 1599848586891, + # "expiration": 1602267786891, + # "matcherFee": 3008, + # "matcherFeeAssetId": "474jTeYx2r2Va35794tCScAXWJG9hU2HcgxzMowaZUnu", + # "signature": "3D2h8ubrhuWkXbVn4qJ3dvjmZQxLoRNfjTqb9uNpnLxUuwm4fGW2qGH6yKFe2SQPrcbgkS3bDVe7SNtMuatEJ7qy", + # "proofs": [ + # "3D2h8ubrhuWkXbVn4qJ3dvjmZQxLoRNfjTqb9uNpnLxUuwm4fGW2qGH6yKFe2SQPrcbgkS3bDVe7SNtMuatEJ7qy", + # ], + # "attachment":"77rnoyFX5BDr15hqZiUtgXKSN46zsbHHQjVNrTMLZcLz62mmFKr39FJ" + # } + # + # + # fetchOrder, fetchOrders, fetchOpenOrders, fetchClosedOrders + # + # { + # "id": "81D9uKk2NfmZzfG7uaJsDtxqWFbJXZmjYvrL88h15fk8", + # "type": "buy", + # "orderType": "limit", + # "amount": 30000000000, + # "filled": 0, + # "price": 1000000, + # "fee": 300000, + # "filledFee": 0, + # "feeAsset": "WAVES", + # "timestamp": 1594303779322, + # "status": "Cancelled", + # "assetPair": { + # "amountAsset": "474jTeYx2r2Va35794tCScAXWJG9hU2HcgxzMowaZUnu", + # "priceAsset": "WAVES" + # }, + # "avgWeighedPrice": 0, + # "version": 4, + # "totalExecutedPriceAssets": 0, # in fetchOpenOrder/s + # "attachment":"77rnoyFX5BDr15hqZiUtgXKSN46zsbHHQjVNrTMLZcLz62mmFKr39FJ" + # } + # + timestamp = self.safe_integer(order, 'timestamp') + side = self.safe_string_2(order, 'type', 'orderType') + type = 'limit' + if 'type' in order: + # fetchOrders + type = self.safe_string(order, 'orderType', type) + id = self.safe_string(order, 'id') + filledString = self.safe_string(order, 'filled') + priceString = self.safe_string(order, 'price') + amountString = self.safe_string(order, 'amount') + assetPair = self.safe_value(order, 'assetPair') + symbol = None + if assetPair is not None: + symbol = self.get_symbol_from_asset_pair(assetPair) + elif market is not None: + symbol = market['symbol'] + amountCurrency = self.safe_currency_code(self.safe_string(assetPair, 'amountAsset', 'WAVES')) + price = self.from_real_symbol_price(symbol, priceString) + amount = self.from_real_currency_amount(amountCurrency, amountString) + filled = self.from_real_currency_amount(amountCurrency, filledString) + average = self.from_real_symbol_price(symbol, self.safe_string(order, 'avgWeighedPrice')) + status = self.parse_order_status(self.safe_string(order, 'status')) + fee = None + if 'type' in order: + code = self.safe_currency_code(self.safe_string(order, 'feeAsset')) + fee = { + 'currency': code, + 'fee': self.parse_number(self.from_real_currency_amount(code, self.safe_string(order, 'filledFee'))), + } + else: + code = self.safe_currency_code(self.safe_string(order, 'matcherFeeAssetId', 'WAVES')) + fee = { + 'currency': code, + 'fee': self.parse_number(self.from_real_currency_amount(code, self.safe_string(order, 'matcherFee'))), + } + triggerPrice = None + attachment = self.safe_string(order, 'attachment') + if attachment is not None: + decodedAttachment = self.parse_json(self.decode(self.base58_to_binary(attachment))) + if decodedAttachment is not None: + c = self.safe_value(decodedAttachment, 'c') + if c is not None: + v = self.safe_value(c, 'v') + if v is not None: + triggerPrice = self.safe_string(v, 'p') + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'symbol': symbol, + 'type': type, + 'timeInForce': None, + 'postOnly': None, + 'side': side, + 'price': price, + 'triggerPrice': triggerPrice, + 'amount': amount, + 'cost': None, + 'average': average, + 'filled': filled, + 'remaining': None, + 'status': status, + 'fee': fee, + 'trades': None, + }, market) + + async def get_waves_address(self): + cachedAddreess = self.safe_string(self.options, 'wavesAddress') + if cachedAddreess is None: + request: dict = { + 'publicKey': self.apiKey, + } + response = await self.nodeGetAddressesPublicKeyPublicKey(request) + self.options['wavesAddress'] = self.safe_string(response, 'address') + return self.options['wavesAddress'] + else: + return cachedAddreess + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + # makes a lot of different requests to get all the data + # in particular: + # fetchMarkets, getWavesAddress, + # getTotalBalance(doesn't include waves), getReservedBalance(doesn't include waves) + # getReservedBalance(includes WAVES) + # I couldn't find another way to get all the data + self.check_required_dependencies() + self.check_required_keys() + await self.load_markets() + wavesAddress = await self.get_waves_address() + request: dict = { + 'address': wavesAddress, + } + totalBalance = await self.nodeGetAssetsBalanceAddress(request) + # { + # "address": "3P8VzLSa23EW5CVckHbV7d5BoN75fF1hhFH", + # "balances": [ + # { + # "assetId": "DG2xFkPdDwKUoBkzGAhQtLpSGzfXLiCYPEzeKH2Ad24p", + # "balance": 1177200, + # "reissuable": False, + # "minSponsoredAssetFee": 7420, + # "sponsorBalance": 47492147189709, + # "quantity": 999999999775381400, + # "issueTransaction": { + # "senderPublicKey": "BRnVwSVctnV8pge5vRpsJdWnkjWEJspFb6QvrmZvu3Ht", + # "quantity": 1000000000000000000, + # "fee": 100400000, + # "description": "Neutrino USD", + # "type": 3, + # "version": 2, + # "reissuable": False, + # "script": null, + # "sender": "3PC9BfRwJWWiw9AREE2B3eWzCks3CYtg4yo", + # "feeAssetId": null, + # "chainId": 87, + # "proofs": [ + # "3HNpbVkgP69NWSeb9hGYauiQDaXrRXh3tXFzNsGwsAAXnFrA29SYGbLtziW9JLpXEq7qW1uytv5Fnm5XTUMB2BxU" + # ], + # "assetId": "DG2xFkPdDwKUoBkzGAhQtLpSGzfXLiCYPEzeKH2Ad24p", + # "decimals": 6, + # "name": "USD-N", + # "id": "DG2xFkPdDwKUoBkzGAhQtLpSGzfXLiCYPEzeKH2Ad24p", + # "timestamp": 1574429393962 + # } + # } + # ] + # } + balances = self.safe_value(totalBalance, 'balances', []) + result: dict = {} + timestamp = None + assetIds = [] + nonStandardBalances = [] + for i in range(0, len(balances)): + entry = balances[i] + entryTimestamp = self.safe_integer(entry, 'timestamp') + timestamp = entryTimestamp if (timestamp is None) else max(timestamp, entryTimestamp) + issueTransaction = self.safe_value(entry, 'issueTransaction') + currencyId = self.safe_string(entry, 'assetId') + balance = self.safe_string(entry, 'balance') + currencyExists = (currencyId in self.currencies_by_id) + if currencyExists: + code = self.safe_currency_code(currencyId) + result[code] = self.account() + result[code]['total'] = self.from_real_currency_amount(code, balance) + elif issueTransaction is None: + assetIds.append(currencyId) + nonStandardBalances.append(balance) + nonStandardAssets = len(assetIds) + if nonStandardAssets: + requestInner: dict = { + 'ids': assetIds, + } + response = await self.publicGetAssets(requestInner) + data = self.safe_value(response, 'data', []) + for i in range(0, len(data)): + entry = data[i] + balance = nonStandardBalances[i] + inner = self.safe_value(entry, 'data') + precision = self.parse_precision(self.safe_string(inner, 'precision')) + ticker = self.safe_string(inner, 'ticker') + code = self.safe_currency_code(ticker) + result[code] = self.account() + result[code]['total'] = Precise.string_mul(balance, precision) + currentTimestamp = self.milliseconds() + byteArray = [ + self.base58_to_binary(self.apiKey), + self.number_to_be(currentTimestamp, 8), + ] + binary = self.binary_concat_array(byteArray) + hexSecret = self.binary_to_base16(self.base58_to_binary(self.secret)) + signature = self.axolotl(self.binary_to_base16(binary), hexSecret, 'ed25519') + matcherRequest: dict = { + 'publicKey': self.apiKey, + 'signature': signature, + 'timestamp': str(currentTimestamp), + } + reservedBalance = await self.matcherGetMatcherBalanceReservedPublicKey(matcherRequest) + # {WAVES: 200300000} + reservedKeys = list(reservedBalance.keys()) + for i in range(0, len(reservedKeys)): + currencyId = reservedKeys[i] + code = self.safe_currency_code(currencyId) + if not (code in result): + result[code] = self.account() + amount = self.safe_string(reservedBalance, currencyId) + result[code]['used'] = self.from_real_currency_amount(code, amount) + wavesRequest: dict = { + 'address': wavesAddress, + } + wavesTotal = await self.nodeGetAddressesBalanceAddress(wavesRequest) + # { + # "address": "3P8VzLSa23EW5CVckHbV7d5BoN75fF1hhFH", + # "confirmations": 0, + # "balance": 909085978 + # } + result['WAVES'] = self.safe_value(result, 'WAVES', self.account()) + result['WAVES']['total'] = self.from_real_currency_amount('WAVES', self.safe_string(wavesTotal, 'balance')) + result = self.set_undefined_balances_to_zero(result) + result['timestamp'] = timestamp + result['datetime'] = self.iso8601(timestamp) + return self.safe_balance(result) + + def set_undefined_balances_to_zero(self, balances, key='used'): + codes = list(balances.keys()) + for i in range(0, len(codes)): + code = codes[i] + if self.safe_value(balances[code], 'used') is None: + balances[code][key] = '0' + return balances + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://api.wavesplatform.com/v0/docs/#/transactions/searchTxsExchange + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + address = await self.get_waves_address() + request: dict = { + 'sender': address, + } + market = None + if symbol is not None: + market = self.market(symbol) + request['amountAsset'] = market['baseId'] + request['priceAsset'] = market['quoteId'] + response = await self.publicGetTransactionsExchange(request) + data = self.safe_value(response, 'data') + # + # { + # "__type":"list", + # "isLastPage":true, + # "lastCursor":"MzA2MjQ0MzAwMDI5OjpkZXNj", + # "data": [ + # { + # "__type":"transaction", + # "data": { + # "id":"GbjPqco2wRP5QSrY5LimFrUyJaM535K9nhK5zaQ7J7Tx", + # "timestamp":"2022-04-06T19:56:31.479Z", + # "height":3062443, + # "type":7, + # "version":2, + # "proofs":[ + # "57mYrANw61eiArCTv2eYwzXm71jYC2KpZ5AeM9zHEstuRaYSAWSuSE7njAJYJu8zap6DMCm3nzqc6es3wQFDpRCN" + # ], + # "fee":0.003, + # "applicationStatus":"succeeded", + # "sender":"3PEjHv3JGjcWNpYEEkif2w8NXV4kbhnoGgu", + # "senderPublicKey":"9cpfKN9suPNvfeUNphzxXMjcnn974eme8ZhWUjaktzU5", + # "buyMatcherFee":0, + # "sellMatcherFee":0.00141728, + # "price":215.7431, + # "amount":0.09, + # "order1": { + # "id":"49qiuQj5frdZ6zpTCEpMuKPMAh1EimwXpXWB4BeCw33h", + # "senderPublicKey":"CjUfoH3dsDZsf5UuAjqqzpWHXgvKzBZpVG9YixF7L48K", + # "matcherPublicKey":"9cpfKN9suPNvfeUNphzxXMjcnn974eme8ZhWUjaktzU5", + # "assetPair": { + # "amountAsset":"7TMu26hAs7B2oW6c5sfx45KSZT7GQA3TZNYuCav8Dcqt", + # "priceAsset":"DG2xFkPdDwKUoBkzGAhQtLpSGzfXLiCYPEzeKH2Ad24p" + # }, + # "orderType":"buy", + # "price":215.7431, + # "sender":"3PR9WmaHV5ueVw2Wr9xsiCG3t4ySXzkkGLy", + # "amount":0.36265477, + # "timestamp":"2022-04-06T19:55:06.832Z", + # "expiration":"2022-05-05T19:55:06.832Z", + # "matcherFee":3.000334, + # "signature":"2rBWhdeuRJNpQfXfTFtcR8x8Lpic8FUHPdLML9uxABRUuxe48YRJcZxbncwWAh9LWFCEUZiztv7RZBZfGMWfFxTs", + # "matcherFeeAssetId":"DG2xFkPdDwKUoBkzGAhQtLpSGzfXLiCYPEzeKH2Ad24p" + # }, + # "order2": { + # "id":"AkxiJqCuv6wm8K41TUSgFNwShZMnCbMDT78MqrcWpQ53", + # "senderPublicKey":"72o7qNKyne5hthB1Ww6famE7uHrk5vTVB2ZfUMBEqL3Y", + # "matcherPublicKey":"9cpfKN9suPNvfeUNphzxXMjcnn974eme8ZhWUjaktzU5", + # "assetPair": { + # "amountAsset":"7TMu26hAs7B2oW6c5sfx45KSZT7GQA3TZNYuCav8Dcqt", + # "priceAsset":"DG2xFkPdDwKUoBkzGAhQtLpSGzfXLiCYPEzeKH2Ad24p" + # }, + # "orderType":"sell", + # "price":210, + # "sender":"3P3CzbjGgiqEyUBeKZYfgZtyaZfMG8fjoUD", + # "amount":0.09, + # "timestamp":"2022-04-06T19:56:18.535Z", + # "expiration":"2022-05-04T19:56:18.535Z", + # "matcherFee":0.00141728, + # "signature":"5BZCjYn6QzVkMXBFDBnzcAUBdCZqhq9hQfRXFHfLUQCsbis4zeriw4sUqLa1BZRT2isC6iY4Z4HtekikPqZ461PT", + # "matcherFeeAssetId":"7TMu26hAs7B2oW6c5sfx45KSZT7GQA3TZNYuCav8Dcqt" + # } + # } + # },... + # ] + # } + # + return self.parse_trades(data, market, since, limit) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://api.wavesplatform.com/v0/docs/#/transactions/searchTxsExchange + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'amountAsset': market['baseId'], + 'priceAsset': market['quoteId'], + } + if limit is not None: + request['limit'] = min(limit, 100) + if since is not None: + request['timeStart'] = since + response = await self.publicGetTransactionsExchange(request) + data = self.safe_value(response, 'data') + # + # { + # "__type":"list", + # "isLastPage":false, + # "lastCursor":"MzA2MjM2MTAwMDU0OjpkZXNj", + # "data": [ + # { + # "__type":"transaction", + # "data": { + # "id":"F42WsvSsyEzvpPLFjVhQKkSNuopooP4zMkjSUs47NeML", + # "timestamp":"2022-04-06T18:39:49.145Z", + # "height":3062361, + # "type":7, + # "version":2, + # "proofs": [ + # "39iJv82kFi4pyuBxYeZpP45NXXjbrCXdVsHPAAvj32UMLmTXLjMTfV43PcmZDSAuS93HKSDo1aKJrin8UvkeE9Bs" + # ], + # "fee":0.003, + # "applicationStatus":"succeeded", + # "sender":"3PEjHv3JGjcWNpYEEkif2w8NXV4kbhnoGgu", + # "senderPublicKey":"9cpfKN9suPNvfeUNphzxXMjcnn974eme8ZhWUjaktzU5", + # "buyMatcherFee":0.02314421, + # "sellMatcherFee":0, + # "price":217.3893, + # "amount":0.34523025, + # "order1": { + # "id":"HkM36PHGaeeZdDKT1mYgZXhaU9PRZ54RZiJc2K4YMT3Q", + # "senderPublicKey":"7wYCaDcc6GX1Jx2uS7QgLHBypBKvrezTS1HfiW6Xe4Bk", + # "matcherPublicKey":"9cpfKN9suPNvfeUNphzxXMjcnn974eme8ZhWUjaktzU5", + # "assetPair": { + # "amountAsset":"7TMu26hAs7B2oW6c5sfx45KSZT7GQA3TZNYuCav8Dcqt", + # "priceAsset":"DG2xFkPdDwKUoBkzGAhQtLpSGzfXLiCYPEzeKH2Ad24p" + # }, + # "orderType":"buy", + # "price":225.2693, + # "sender":"3PLPc8f4DGYaF9C9bwJ2uVmHqRv3NCjg5VQ", + # "amount":2.529, + # "timestamp":"2022-04-06T18:39:48.796Z", + # "expiration":"2022-05-05T18:39:48.796Z", + # "matcherFee":0.17584444, + # "signature":"2yQfJoomv86evQDw36fg1uiRkHvPDZtRp3qvxqTBWPvz4JLTHGQtEHJF5NGTvym6U93CtgNprngzmD9ecHBjxf6U", + # "matcherFeeAssetId":"Atqv59EYzjFGuitKVnMRk6H8FukjoV3ktPorbEys25on" + # }, + # "order2": { + # "id":"F7HKmeuzwWdk3wKitHLnVx5MuD4wBWPpphQ8kUGx4tT9", + # "senderPublicKey":"CjUfoH3dsDZsf5UuAjqqzpWHXgvKzBZpVG9YixF7L48K", + # "matcherPublicKey":"9cpfKN9suPNvfeUNphzxXMjcnn974eme8ZhWUjaktzU5", + # "assetPair": { + # "amountAsset":"7TMu26hAs7B2oW6c5sfx45KSZT7GQA3TZNYuCav8Dcqt", + # "priceAsset":"DG2xFkPdDwKUoBkzGAhQtLpSGzfXLiCYPEzeKH2Ad24p" + # }, + # "orderType":"sell", + # "price":217.3893, + # "sender":"3PR9WmaHV5ueVw2Wr9xsiCG3t4ySXzkkGLy", + # "amount":0.35767793, + # "timestamp":"2022-04-06T18:32:01.390Z", + # "expiration":"2022-05-05T18:32:01.390Z", + # "matcherFee":0.0139168, + # "signature":"34HgWVLPgeYWkiSvAc5ChVepGTYDQDug2dMTSincs6idEyoM7AtaZuH3mqQ5RJG2fcxxH2QSB723Qq3dgLQwQmKf", + # "matcherFeeAssetId":"7TMu26hAs7B2oW6c5sfx45KSZT7GQA3TZNYuCav8Dcqt" + # } + # } + # }, ... + # ] + # } + # + return self.parse_trades(data, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # {__type: "transaction", + # "data": + # {id: "HSdruioHqvYHeyn9hhyoHdRWPB2bFA8ujeCPZMK6992c", + # "timestamp": "2020-06-09T19:34:51.897Z", + # "height": 2099684, + # "type": 7, + # "version": 2, + # "proofs": + # ["26teDHERQgwjjHqEn4REcDotNG8M21xjou3X42XuDuCvrRkQo6aPyrswByH3UrkWG8v27ZAaVNzoxDg4teNcLtde"], + # "fee": 0.003, + # "sender": "3PEjHv3JGjcWNpYEEkif2w8NXV4kbhnoGgu", + # "senderPublicKey": "9cpfKN9suPNvfeUNphzxXMjcnn974eme8ZhWUjaktzU5", + # "buyMatcherFee": 0.00299999, + # "sellMatcherFee": 0.00299999, + # "price": 0.00012003, + # "amount": 60.80421562, + # "order1": + # {id: "CBRwP3ar4oMvvpUiGyfxc1syh41488SDi2GkrjuBDegv", + # "senderPublicKey": "DBXSHBz96NFsMu7xh4fi2eT9ZnyxefAHXsMxUayzgC6a", + # "matcherPublicKey": "9cpfKN9suPNvfeUNphzxXMjcnn974eme8ZhWUjaktzU5", + # "assetPair": [Object], + # "orderType": "buy", + # "price": 0.00012003, + # "sender": "3PJfFRgVuJ47UY4ckb74EGzEBzkHXtmG1LA", + # "amount": 60.80424773, + # "timestamp": "2020-06-09T19:34:51.885Z", + # "expiration": "2020-06-10T12:31:31.885Z", + # "matcherFee": 0.003, + # "signature": "4cA3ZAb3XAEEXaFG7caqpto5TRbpR5PkhZpxoNQZ9ZReNvjuJQs5a3THnumv7rcqmVUiVtuHAgk2f67ANcqtKyJ8", + # "matcherFeeAssetId": null}, + # "order2": + # {id: "CHJSLQ6dfSPs6gu2mAegrMUcRiDEDqaj2GKfvptMjS3M", + # "senderPublicKey": "3RUC4NGFZm9H8VJhSSjJyFLdiE42qNiUagDcZPwjgDf8", + # "matcherPublicKey": "9cpfKN9suPNvfeUNphzxXMjcnn974eme8ZhWUjaktzU5", + # "assetPair": [Object], + # "orderType": "sell", + # "price": 0.00012003, + # "sender": "3P9vKoQpMZtaSkHKpNh977YY9ZPzTuntLAq", + # "amount": 60.80424773, + # "timestamp": "2020-06-09T19:34:51.887Z", + # "expiration": "2020-06-10T12:31:31.887Z", + # "matcherFee": 0.003, + # "signature": "3SFyrcqzou2ddZyNisnLYaGhLt5qRjKxH8Nw3s4T5U7CEKGX9DDo8dS27RgThPVGbYF1rYET1FwrWoQ2UFZ6SMTR", + # "matcherFeeAssetId": null}}} + # + data = self.safe_value(trade, 'data') + datetime = self.safe_string(data, 'timestamp') + timestamp = self.parse8601(datetime) + id = self.safe_string(data, 'id') + priceString = self.safe_string(data, 'price') + amountString = self.safe_string(data, 'amount') + order1 = self.safe_value(data, 'order1') + order2 = self.safe_value(data, 'order2') + order = None + # at first, detect if response is from `fetch_my_trades` + if self.safe_string(order1, 'senderPublicKey') == self.apiKey: + order = order1 + elif self.safe_string(order2, 'senderPublicKey') == self.apiKey: + order = order2 + else: + # response is from `fetch_trades`, so find only taker order + date1 = self.safe_string(order1, 'timestamp') + date2 = self.safe_string(order2, 'timestamp') + ts1 = self.parse8601(date1) + ts2 = self.parse8601(date2) + if ts1 > ts2: + order = order1 + else: + order = order2 + symbol = None + assetPair = self.safe_value(order, 'assetPair') + if assetPair is not None: + symbol = self.get_symbol_from_asset_pair(assetPair) + elif market is not None: + symbol = market['symbol'] + side = self.safe_string(order, 'orderType') + orderId = self.safe_string(order, 'id') + fee = { + 'cost': self.safe_string(order, 'matcherFee'), + 'currency': self.safe_currency_code(self.safe_string(order, 'matcherFeeAssetId', 'WAVES')), + } + return self.safe_trade({ + 'info': trade, + 'timestamp': timestamp, + 'datetime': datetime, + 'symbol': symbol, + 'id': id, + 'order': orderId, + 'type': None, + 'side': side, + 'takerOrMaker': None, + 'price': priceString, + 'amount': amountString, + 'cost': None, + 'fee': fee, + }, market) + + def parse_deposit_withdraw_fees(self, response, codes: Strings = None, currencyIdKey=None) -> Any: + depositWithdrawFees: dict = {} + codes = self.market_codes(codes) + for i in range(0, len(response)): + entry = response[i] + dictionary = entry + currencyId = self.safe_string(dictionary, currencyIdKey) + currency = self.safe_value(self.currencies_by_id, currencyId) + code = self.safe_string(currency, 'code', currencyId) + if (codes is None) or (self.in_array(code, codes)): + depositWithdrawFee = self.safe_value(depositWithdrawFees, code) + if depositWithdrawFee is None: + depositWithdrawFee = { + 'info': [dictionary], + 'withdraw': { + 'fee': None, + 'percentage': None, + }, + 'deposit': { + 'fee': None, + 'percentage': None, + }, + 'networks': {}, + } + else: + depositWithdrawFee = depositWithdrawFees[code] + depositWithdrawFee['info'] = self.array_concat(depositWithdrawFee['info'], [dictionary]) + networkId = self.safe_string(dictionary, 'platform_id') + currencyCode = self.safe_string(currency, 'code') + networkCode = self.network_id_to_code(networkId, currencyCode) + network = self.safe_value(depositWithdrawFee['networks'], networkCode) + if network is None: + network = { + 'withdraw': { + 'fee': None, + 'percentage': None, + }, + 'deposit': { + 'fee': None, + 'percentage': None, + }, + } + feeType = self.safe_string(dictionary, 'type') + fees = self.safe_value(dictionary, 'fees') + networkKey = 'deposit' + if feeType == 'withdrawal_currency': + networkKey = 'withdraw' + network[networkKey] = {'fee': self.safe_number(fees, 'flat'), 'percentage': False} + depositWithdrawFee['networks'][networkCode] = network + depositWithdrawFees[code] = depositWithdrawFee + depositWithdrawFeesKeys = list(depositWithdrawFees.keys()) + for i in range(0, len(depositWithdrawFeesKeys)): + code = depositWithdrawFeesKeys[i] + entry = depositWithdrawFees[code] + networks = self.safe_value(entry, 'networks') + networkKeys = list(networks.keys()) + networkKeysLength = len(networkKeys) + if networkKeysLength == 1: + network = self.safe_value(networks, networkKeys[0]) + depositWithdrawFees[code]['withdraw'] = self.safe_value(network, 'withdraw') + depositWithdrawFees[code]['deposit'] = self.safe_value(network, 'deposit') + return depositWithdrawFees + + async def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}): + """ + fetch deposit and withdraw fees + + https://docs.wx.network/en/api/gateways/deposit/currencies + https://docs.wx.network/en/api/gateways/withdraw/currencies + + :param str[]|None codes: list of unified currency codes + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `fee structures ` + """ + await self.load_markets() + data = [] + promises = [] + promises.append(self.privateGetDepositCurrencies(params)) + promises.append(self.privateGetWithdrawCurrencies(params)) + promises = await asyncio.gather(*promises) + # + # { + # "type": "list", + # "page_info": { + # "has_next_page": False, + # "last_cursor": null + # }, + # "items": [ + # { + # "type": "deposit_currency", + # "id": "WEST", + # "platform_id": "WEST", + # "waves_asset_id": "4LHHvYGNKJUg5hj65aGD5vgScvCBmLpdRFtjokvCjSL8", + # "platform_asset_id": "WEST", + # "decimals": 8, + # "status": "active", + # "allowed_amount": { + # "min": 0.1, + # "max": 2000000 + # }, + # "fees": { + # "flat": 0, + # "rate": 0 + # } + # }, + # ] + # } + # + # + # { + # "type": "list", + # "page_info": { + # "has_next_page": False, + # "last_cursor": null + # }, + # "items": [ + # { + # "type": "withdrawal_currency", + # "id": "BTC", + # "platform_id": "BTC", + # "waves_asset_id": "8LQW8f7P5d5PZM7GtZEBgaqRPGSzS3DfPuiXrURJ4AJS", + # "platform_asset_id": "BTC", + # "decimals": 8, + # "status": "inactive", + # "allowed_amount": { + # "min": 0.001, + # "max": 10 + # }, + # "fees": { + # "flat": 0.001, + # "rate": 0 + # } + # }, + # ] + # } + # + for i in range(0, len(promises)): + items = self.safe_value(promises[i], 'items') + data = self.array_concat(data, items) + return self.parse_deposit_withdraw_fees(data, codes, 'id') + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + errorCode = self.safe_string(response, 'error') + success = self.safe_bool(response, 'success', True) + Exception = self.safe_value(self.exceptions, errorCode) + if Exception is not None: + messageInner = self.safe_string(response, 'message') + raise Exception(self.id + ' ' + messageInner) + message = self.safe_string(response, 'message') + if message == 'Validation Error': + raise BadRequest(self.id + ' ' + body) + if not success: + raise ExchangeError(self.id + ' ' + body) + return None + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + # currently only works for BTC and WAVES + if code != 'WAVES': + supportedCurrencies = await self.privateGetWithdrawCurrencies() + currencies: dict = {} + items = self.safe_value(supportedCurrencies, 'items', []) + for i in range(0, len(items)): + entry = items[i] + currencyCode = self.safe_string(entry, 'id') + currencies[currencyCode] = True + if not (code in currencies): + codes = list(currencies.keys()) + raise ExchangeError(self.id + ' withdraw() ' + code + ' not supported. Currency code must be one of ' + str(codes)) + await self.load_markets() + hexChars = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'] + set: dict = {} + for i in range(0, len(hexChars)): + key = hexChars[i] + set[key] = True + isErc20 = True + noPrefix = self.remove0x_prefix(address) + lower = noPrefix.lower() + stringLength = len(lower) * 1 + for i in range(0, stringLength): + character = lower[i] + if not (character in set): + isErc20 = False + break + await self.sign_in() + proxyAddress = None + if code == 'WAVES' and not isErc20: + proxyAddress = address + else: + withdrawAddressRequest: dict = { + 'address': address, + 'currency': code, + } + withdrawAddress = await self.privateGetWithdrawAddressesCurrencyAddress(withdrawAddressRequest) + currencyInner = self.safe_value(withdrawAddress, 'currency') + allowedAmount = self.safe_value(currencyInner, 'allowed_amount') + minimum = self.safe_number(allowedAmount, 'min') + if amount <= minimum: + raise BadRequest(self.id + ' ' + code + ' withdraw failed, amount ' + str(amount) + ' must be greater than the minimum allowed amount of ' + str(minimum)) + # { + # "type": "withdrawal_addresses", + # "currency": { + # "type": "withdrawal_currency", + # "id": "BTC", + # "waves_asset_id": "8LQW8f7P5d5PZM7GtZEBgaqRPGSzS3DfPuiXrURJ4AJS", + # "decimals": 8, + # "status": "active", + # "allowed_amount": { + # "min": 0.001, + # "max": 20 + # }, + # "fees": { + # "flat": 0.001, + # "rate": 0 + # } + # }, + # "proxy_addresses": [ + # "3P3qqmkiLwNHB7x1FeoE8bvkRtULwGpo9ga" + # ] + # } + proxyAddresses = self.safe_value(withdrawAddress, 'proxy_addresses', []) + proxyAddress = self.safe_string(proxyAddresses, 0) + fee = self.safe_integer(self.options, 'withdrawFeeWAVES', 100000) # 0.001 WAVES + feeAssetId = 'WAVES' + type = 4 # transfer + version = 2 + amountInteger = self.to_real_currency_amount(code, amount) + currency = self.currency(code) + timestamp = self.milliseconds() + byteArray = [ + self.number_to_be(4, 1), + self.number_to_be(2, 1), + self.base58_to_binary(self.apiKey), + self.get_asset_bytes(currency['id']), + self.get_asset_bytes(feeAssetId), + self.number_to_be(timestamp, 8), + self.number_to_be(amountInteger, 8), + self.number_to_be(fee, 8), + self.base58_to_binary(proxyAddress), + self.number_to_be(0, 2), + ] + binary = self.binary_concat_array(byteArray) + hexSecret = self.binary_to_base16(self.base58_to_binary(self.secret)) + signature = self.axolotl(self.binary_to_base16(binary), hexSecret, 'ed25519') + request: dict = { + 'senderPublicKey': self.apiKey, + 'amount': amountInteger, + 'fee': fee, + 'type': type, + 'version': version, + 'attachment': '', + 'feeAssetId': self.get_asset_id(feeAssetId), + 'proofs': [ + signature, + ], + 'assetId': self.get_asset_id(currency['id']), + 'recipient': proxyAddress, + 'timestamp': timestamp, + 'signature': signature, + } + result = await self.nodePostTransactionsBroadcast(request) + # + # { + # "id": "string", + # "signature": "string", + # "fee": 0, + # "timestamp": 1460678400000, + # "recipient": "3P274YB5qseSE9DTTL3bpSjosZrYBPDpJ8k", + # "amount": 0 + # } + # + return self.parse_transaction(result, currency) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # withdraw + # + # { + # "id": "string", + # "signature": "string", + # "fee": 0, + # "timestamp": 1460678400000, + # "recipient": "3P274YB5qseSE9DTTL3bpSjosZrYBPDpJ8k", + # "amount": 0 + # } + # + # withdraw new: + # { + # type: "4", + # id: "2xnWTqG9ar7jEDrLxfbVyyspPZ6XZNrrw9ai9sQ81Eya", + # fee: "100000", + # feeAssetId: null, + # timestamp: "1715786263807", + # version: "2", + # sender: "3P81LLX1kk2CSJC9L8C2enxdHB7XvnSGAEE", + # senderPublicKey: "DdmzmXf9mty1FBE8AdVGnrncVLEAzP4gR4nWoTFAJoXz", + # proofs: ["RyoKwdSYv3EqotJCYftfFM9JE2j1ZpDRxKwYfiRhLAFeyNp6VfJUXNDS884XfeCeHeNypNmTCZt5NYR1ekyjCX3",], + # recipient: "3P9tXxu38a8tgewNEKFzourVxeqHd11ppOc", + # assetId: null, + # feeAsset: null, + # amount: "2000000", + # attachment: "", + # } + # + currency = self.safe_currency(None, currency) + code = currency['code'] + typeRaw = self.safe_string(transaction, 'type') + type = 'withdraw' if (typeRaw == '4') else 'deposit' + amount = self.parse_number(self.from_real_currency_amount(code, self.safe_string(transaction, 'amount'))) + feeString = self.safe_string(transaction, 'fee') + feeAssetId = self.safe_string(transaction, 'feeAssetId', 'WAVES') + feeCode = self.safe_currency_code(feeAssetId) + feeAmount = self.parse_number(self.from_real_currency_amount(feeCode, feeString)) + timestamp = self.safe_integer(transaction, 'timestamp') + return { + 'id': self.safe_string(transaction, 'id'), + 'txid': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': None, + 'addressFrom': self.safe_string(transaction, 'sender'), + 'address': None, + 'addressTo': self.safe_string(transaction, 'recipient'), + 'amount': amount, + 'type': type, + 'currency': currency['code'], + 'status': None, + 'updated': None, + 'tagFrom': None, + 'tag': None, + 'tagTo': None, + 'comment': None, + 'internal': None, + 'fee': { + 'currency': feeCode, + 'cost': feeAmount, + }, + 'info': transaction, + } diff --git a/ccxt/async_support/whitebit.py b/ccxt/async_support/whitebit.py new file mode 100644 index 0000000..514755f --- /dev/null +++ b/ccxt/async_support/whitebit.py @@ -0,0 +1,3816 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.whitebit import ImplicitAPI +import asyncio +import hashlib +from ccxt.base.types import Account, Any, Balances, BorrowInterest, Bool, Conversion, CrossBorrowRate, Currencies, Currency, DepositAddress, FundingHistory, Int, Market, MarketType, Num, Order, OrderBook, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, FundingRates, Trade, TradingFees, Transaction, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import NotSupported +from ccxt.base.errors import DDoSProtection +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class whitebit(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(whitebit, self).describe(), { + 'id': 'whitebit', + 'name': 'WhiteBit', + 'version': 'v4', + 'countries': ['EE'], + 'rateLimit': 50, + 'pro': True, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': True, + 'swap': True, + 'future': False, + 'option': False, + 'cancelAllOrders': True, + 'cancelAllOrdersAfter': True, + 'cancelOrder': True, + 'cancelOrders': False, + 'createConvertTrade': True, + 'createDepositAddress': True, + 'createMarketBuyOrderWithCost': True, + 'createMarketOrderWithCost': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createPostOnlyOrder': True, + 'createStopLimitOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'createTriggerOrder': True, + 'editOrder': True, + 'fetchAccounts': True, + 'fetchBalance': True, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchClosedOrders': True, + 'fetchConvertQuote': True, + 'fetchConvertTrade': False, + 'fetchConvertTradeHistory': True, + 'fetchCrossBorrowRate': True, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDeposit': True, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchDepositsWithdrawals': True, + 'fetchDepositWithdrawFee': 'emulated', + 'fetchDepositWithdrawFees': True, + 'fetchFundingHistory': True, + 'fetchFundingLimits': True, + 'fetchFundingRate': True, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': True, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchLedger': False, + 'fetchMarginMode': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterestHistory': False, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchOrderTrades': True, + 'fetchPosition': True, + 'fetchPositionHistory': True, + 'fetchPositionMode': False, + 'fetchPositions': True, + 'fetchPremiumIndexOHLCV': False, + 'fetchStatus': True, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': True, + 'fetchTradingLimits': True, + 'fetchTransactionFees': True, + 'fetchTransactions': True, + 'fetchWithdrawals': True, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'setLeverage': True, + 'transfer': True, + 'withdraw': True, + }, + 'timeframes': { + '1m': '1m', + '3m': '3m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1h', + '2h': '2h', + '4h': '4h', + '6h': '6h', + '8h': '8h', + '12h': '12h', + '1d': '1d', + '3d': '3d', + '1w': '1w', + '1M': '1M', + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/66732963-8eb7dd00-ee66-11e9-849b-10d9282bb9e0.jpg', + 'api': { + 'v1': { + 'public': 'https://whitebit.com/api/v1/public', + 'private': 'https://whitebit.com/api/v1', + }, + 'v2': { + 'public': 'https://whitebit.com/api/v2/public', + }, + 'v4': { + 'public': 'https://whitebit.com/api/v4/public', + 'private': 'https://whitebit.com/api/v4', + }, + }, + 'www': 'https://www.whitebit.com', + 'doc': 'https://github.com/whitebit-exchange/api-docs', + 'fees': 'https://whitebit.com/fee-schedule', + 'referral': 'https://whitebit.com/referral/d9bdf40e-28f2-4b52-b2f9-cd1415d82963', + }, + 'api': { + 'web': { + 'get': [ + 'v1/healthcheck', + ], + }, + 'v1': { + 'public': { + 'get': [ + 'markets', + 'tickers', + 'ticker', + 'symbols', + 'depth/result', + 'history', + 'kline', + ], + }, + 'private': { + 'post': [ + 'account/balance', + 'order/new', + 'order/cancel', + 'orders', + 'account/order_history', + 'account/executed_history', + 'account/executed_history/all', + 'account/order', + ], + }, + }, + 'v2': { + 'public': { + 'get': [ + 'markets', + 'ticker', + 'assets', + 'fee', + 'depth/{market}', + 'trades/{market}', + ], + }, + }, + 'v4': { + 'public': { + 'get': [ + 'assets', + 'collateral/markets', + 'fee', + 'orderbook/depth/{market}', + 'orderbook/{market}', + 'ticker', + 'trades/{market}', + 'time', + 'ping', + 'markets', + 'futures', + 'platform/status', + 'mining-pool', + ], + }, + 'private': { + 'post': [ + 'collateral-account/balance', + 'collateral-account/balance-summary', + 'collateral-account/positions/history', + 'collateral-account/leverage', + 'collateral-account/positions/open', + 'collateral-account/summary', + 'collateral-account/funding-history', + 'main-account/address', + 'main-account/balance', + 'main-account/create-new-address', + 'main-account/codes', + 'main-account/codes/apply', + 'main-account/codes/my', + 'main-account/codes/history', + 'main-account/fiat-deposit-url', + 'main-account/history', + 'main-account/withdraw', + 'main-account/withdraw-pay', + 'main-account/transfer', + 'main-account/smart/plans', + 'main-account/smart/investment', + 'main-account/smart/investment/close', + 'main-account/smart/investments', + 'main-account/fee', + 'main-account/smart/interest-payment-history', + 'trade-account/balance', + 'trade-account/executed-history', + 'trade-account/order/history', + 'trade-account/order', + 'order/collateral/limit', + 'order/collateral/market', + 'order/collateral/stop-limit', + 'order/collateral/trigger-market', + 'order/collateral/bulk', + 'order/new', + 'order/market', + 'order/stock_market', + 'order/stop_limit', + 'order/stop_market', + 'order/cancel', + 'order/cancel/all', + 'order/kill-switch', + 'order/kill-switch/status', + 'order/bulk', + 'order/modify', + 'order/conditional-cancel', + 'orders', + 'oco-orders', + 'order/collateral/oco', + 'order/oco-cancel', + 'order/oto-cancel', + 'profile/websocket_token', + 'convert/estimate', + 'convert/confirm', + 'convert/history', + 'sub-account/create', + 'sub-account/delete', + 'sub-account/edit', + 'sub-account/list', + 'sub-account/transfer', + 'sub-account/block', + 'sub-account/unblock', + 'sub-account/balances', + 'sub-account/transfer/history', + 'sub-account/api-key/create', + 'sub-account/api-key/edit', + 'sub-account/api-key/delete', + 'sub-account/api-key/list', + 'sub-account/api-key/reset', + 'sub-account/api-key/ip-address/list', + 'sub-account/api-key/ip-address/create', + 'sub-account/api-key/ip-address/delete', + 'mining/rewards', + 'market/fee', + 'conditional-orders', + ], + }, + }, + }, + 'fees': { + 'trading': { + 'tierBased': False, + 'percentage': True, + 'taker': self.parse_number('0.001'), + 'maker': self.parse_number('0.001'), + }, + }, + 'options': { + 'timeDifference': 0, # the difference between system clock and exchange clock + 'adjustForTimeDifference': False, # controls the adjustment logic upon instantiation + 'fiatCurrencies': ['EUR', 'USD', 'RUB', 'UAH'], + 'fetchBalance': { + 'account': 'spot', + }, + 'accountsByType': { + 'funding': 'main', + 'main': 'main', + 'spot': 'spot', + 'margin': 'collateral', + 'trade': 'spot', + }, + 'networksById': { + 'BEP20': 'BSC', + }, + 'defaultType': 'spot', + 'brokerId': 'ccxt', + }, + 'features': { + 'default': { + 'sandbox': False, + 'createOrder': { + 'marginMode': True, + 'triggerPrice': True, + 'triggerDirection': False, + 'triggerPriceType': None, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, # todo + 'FOK': False, + 'PO': True, # todo + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, + 'daysBack': None, + 'untilDays': None, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'checkActive': True, + 'checkExecuted': True, + 'symbolRequired': False, + 'marginMode': False, + 'trigger': False, + 'trailing': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 100, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': None, + 'untilDays': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': None, + 'daysBackCanceled': None, + 'untilDays': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 1440, + }, + 'fetchWithdrawals': { + 'marginMode': False, + 'limit': 100, + 'daysBack': None, + 'untilDays': None, + 'symbolRequired': False, + }, + }, + 'spot': { + 'extends': 'default', + }, + 'swap': { + 'linear': { + 'extends': 'default', + }, + 'inverse': { + 'extends': 'default', + }, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'exact': { + 'Unauthorized request.': AuthenticationError, # {"code":10,"message":"Unauthorized request."} + 'The market format is invalid.': BadSymbol, # {"code":0,"message":"Validation failed","errors":{"market":["The market format is invalid."]}} + 'Market is not available': BadSymbol, # {"success":false,"message":{"market":["Market is not available"]},"result":[]} + 'Invalid payload.': BadRequest, # {"code":9,"message":"Invalid payload."} + 'Amount must be greater than 0': InvalidOrder, # {"code":0,"message":"Validation failed","errors":{"amount":["Amount must be greater than 0"]}} + 'Not enough balance.': InsufficientFunds, # {"code":10,"message":"Inner validation failed","errors":{"amount":["Not enough balance."]}} + 'The order id field is required.': InvalidOrder, # {"code":0,"message":"Validation failed","errors":{"orderId":["The order id field is required."]}} + 'Not enough balance': InsufficientFunds, # {"code":0,"message":"Validation failed","errors":{"amount":["Not enough balance"]}} + 'This action is unauthorized.': PermissionDenied, # {"code":0,"message":"This action is unauthorized."} + 'This API Key is not authorized to perform self action.': PermissionDenied, # {"code":4,"message":"This API Key is not authorized to perform self action."} + 'Unexecuted order was not found.': OrderNotFound, # {"code":2,"message":"Inner validation failed","errors":{"order_id":["Unexecuted order was not found."]}} + 'The selected from is invalid.': BadRequest, # {"code":0,"message":"Validation failed","errors":{"from":["The selected from is invalid."]}} + '503': ExchangeNotAvailable, # {"response":null,"status":503,"errors":{"message":[""]},"notification":null,"warning":null,"_token":null}, + '422': OrderNotFound, # {"response":null,"status":422,"errors":{"orderId":["Finished order id 1295772653 not found on your account"]},"notification":null,"warning":"Finished order id 1295772653 not found on your account","_token":null} + }, + 'broad': { + 'This action is unauthorized': PermissionDenied, # {"code":2,"message":"This action is unauthorized. Enable your key in API settings"} + 'Given amount is less than min amount': InvalidOrder, # {"code":0,"message":"Validation failed","errors":{"amount":["Given amount is less than min amount 200000"],"total":["Total is less than 5.05"]}} + 'Min amount step': InvalidOrder, # {"code":32,"errors":{"amount":["Min amount step = 0.01"]},"message":"Validation failed"} + 'Total is less than': InvalidOrder, # {"code":0,"message":"Validation failed","errors":{"amount":["Given amount is less than min amount 200000"],"total":["Total is less than 5.05"]}} + 'fee must be no less than': InvalidOrder, # {"code":0,"message":"Validation failed","errors":{"amount":["Total amount + fee must be no less than 5.05505"]}} + 'Enable your key in API settings': PermissionDenied, # {"code":2,"message":"This action is unauthorized. Enable your key in API settings"} + 'You don\'t have such amount for transfer': InsufficientFunds, # {"code":3,"message":"Inner validation failed","errors":{"amount":["You don't have such amount for transfer(available 0.44523433, in amount: 2)"]}} + }, + }, + }) + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for whitebit + + https://docs.whitebit.com/public/http-v4/#market-info + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + if self.options['adjustForTimeDifference']: + await self.load_time_difference() + markets = await self.v4PublicGetMarkets() + # + # [ + # { + # "name": "SON_USD", # Market pair name + # "stock": "SON", # Ticker of stock currency + # "money": "USD", # Ticker of money currency + # "stockPrec": "3", # Stock currency precision + # "moneyPrec": "2", # Precision of money currency + # "feePrec": "4", # Fee precision + # "makerFee": "0.1", # Default maker fee ratio + # "takerFee": "0.1", # Default taker fee ratio + # "minAmount": "0.001", # Minimal amount of stock to trade + # "minTotal": "0.001", # Minimal amount of money to trade + # "tradesEnabled": True, # Is trading enabled + # "isCollateral": True, # Is margin trading enabled + # "type": "spot", # Market type. Possible values: "spot", "futures" + # "maxTotal": "1000000000" # Maximum total(amount * price) of money to trade + # }, + # { + # ... + # } + # ] + # + return self.parse_markets(markets) + + def parse_market(self, market: dict) -> Market: + id = self.safe_string(market, 'name') + baseId = self.safe_string(market, 'stock') + quoteId = self.safe_string(market, 'money') + quoteId = 'USDT' if (quoteId == 'PERP') else quoteId + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + active = self.safe_value(market, 'tradesEnabled') + isCollateral = self.safe_value(market, 'isCollateral') + typeId = self.safe_string(market, 'type') + type: MarketType + settle: Str = None + settleId: Str = None + symbol = base + '/' + quote + swap = typeId == 'futures' + margin = isCollateral and not swap + contract = False + amountPrecision = self.parse_number(self.parse_precision(self.safe_string(market, 'stockPrec'))) + contractSize = amountPrecision + linear: Bool = None + inverse: Bool = None + if swap: + settleId = quoteId + settle = self.safe_currency_code(settleId) + symbol = symbol + ':' + settle + type = 'swap' + contract = True + linear = True + inverse = False + else: + type = 'spot' + takerFeeRate = self.safe_string(market, 'takerFee') + taker = Precise.string_div(takerFeeRate, '100') + makerFeeRate = self.safe_string(market, 'makerFee') + maker = Precise.string_div(makerFeeRate, '100') + return { + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': type, + 'spot': not swap, + 'margin': margin, + 'swap': swap, + 'future': False, + 'option': False, + 'active': active, + 'contract': contract, + 'linear': linear, + 'inverse': inverse, + 'taker': self.parse_number(taker), + 'maker': self.parse_number(maker), + 'contractSize': contractSize, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': amountPrecision, + 'price': self.parse_number(self.parse_precision(self.safe_string(market, 'moneyPrec'))), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(market, 'minAmount'), + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': self.safe_number(market, 'minTotal'), + 'max': self.safe_number(market, 'maxTotal'), + }, + }, + 'created': None, + 'info': market, + } + + async def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://docs.whitebit.com/public/http-v4/#asset-status-list + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = await self.v4PublicGetAssets(params) + # + # { + # BTC: { + # name: "Bitcoin", + # unified_cryptoasset_id: "1", + # can_withdraw: True, + # can_deposit: True, + # min_withdraw: "0.0003", + # max_withdraw: "0", + # maker_fee: "0.1", + # taker_fee: "0.1", + # min_deposit: "0.0001", + # max_deposit: "0", + # networks: { + # deposits: ["BTC",], + # withdraws: ["BTC",], + # default: "BTC", + # }, + # confirmations: { + # BTC: "2", + # }, + # limits: { + # deposit: { + # BTC: {min: "0.0001",}, + # }, + # withdraw: { + # BTC: {min: "0.0003",}, + # }, + # }, + # currency_precision: "8", + # is_memo: False, + # }, + # USD: { + # name: "United States Dollar", + # unified_cryptoasset_id: "6955", + # can_withdraw: True, + # can_deposit: True, + # min_withdraw: "10", + # max_withdraw: "10000", + # maker_fee: "0.1", + # taker_fee: "0.1", + # min_deposit: "10", + # max_deposit: "10000", + # networks: { + # deposits: ["USD",], + # withdraws: ["USD",], + # default: "USD", + # }, + # providers: { + # deposits: ["ADVCASH",], + # withdraws: ["ADVCASH",], + # }, + # limits: { + # deposit: { + # USD: { max: "10000", min: "10",}, + # }, + # withdraw: { + # USD: {max: "10000", min: "10",}, + # }, + # }, + # currency_precision: "2", + # is_memo: False, + # } + # } + # + ids = list(response.keys()) + result: dict = {} + for i in range(0, len(ids)): + id = ids[i] + currency = response[id] + # name = self.safe_string(currency, 'name') # breaks down in Python due to utf8 encoding issues on the exchange side + code = self.safe_currency_code(id) + hasProvider = ('providers' in currency) + networks = {} + rawNetworks = self.safe_dict(currency, 'networks', {}) + depositsNetworks = self.safe_list(rawNetworks, 'deposits', []) + withdrawsNetworks = self.safe_list(rawNetworks, 'withdraws', []) + networkLimits = self.safe_dict(currency, 'limits', {}) + depositLimits = self.safe_dict(networkLimits, 'deposit', {}) + withdrawLimits = self.safe_dict(networkLimits, 'withdraw', {}) + allNetworks = self.array_concat(depositsNetworks, withdrawsNetworks) + for j in range(0, len(allNetworks)): + networkId = allNetworks[j] + networkCode = self.network_id_to_code(networkId) + networks[networkCode] = { + 'id': networkId, + 'network': networkCode, + 'active': None, + 'deposit': self.in_array(networkId, depositsNetworks), + 'withdraw': self.in_array(networkId, withdrawsNetworks), + 'fee': None, + 'precision': None, + 'limits': { + 'deposit': { + 'min': self.safe_number(depositLimits, 'min', None), + 'max': self.safe_number(depositLimits, 'max', None), + }, + 'withdraw': { + 'min': self.safe_number(withdrawLimits, 'min', None), + 'max': self.safe_number(withdrawLimits, 'max', None), + }, + }, + } + result[code] = self.safe_currency_structure({ + 'id': id, + 'code': code, + 'info': currency, # the original payload + 'name': None, # see the comment above + 'active': None, + 'deposit': self.safe_bool(currency, 'can_deposit'), + 'withdraw': self.safe_bool(currency, 'can_withdraw'), + 'fee': None, + 'networks': None, # todo + 'type': 'fiat' if hasProvider else 'crypto', + 'precision': self.parse_number(self.parse_precision(self.safe_string(currency, 'currency_precision'))), + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': self.safe_number(currency, 'min_withdraw'), + 'max': self.safe_number(currency, 'max_withdraw'), + }, + 'deposit': { + 'min': self.safe_number(currency, 'min_deposit'), + 'max': self.safe_number(currency, 'max_deposit'), + }, + }, + }) + return result + + async def fetch_transaction_fees(self, codes: Strings = None, params={}): + """ + @deprecated + please use fetchDepositWithdrawFees instead + + https://docs.whitebit.com/public/http-v4/#fee + + :param str[]|None codes: not used by fetchTransactionFees() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `fee structures ` + """ + await self.load_markets() + response = await self.v4PublicGetFee(params) + # + # { + # "1INCH":{ + # "is_depositable":true, + # "is_withdrawal":true, + # "ticker":"1INCH", + # "name":"1inch", + # "providers":[ + # ], + # "withdraw":{ + # "max_amount":"0", + # "min_amount":"21.5", + # "fixed":"17.5", + # "flex":null + # }, + # "deposit":{ + # "max_amount":"0", + # "min_amount":"19.5", + # "fixed":null, + # "flex":null + # } + # }, + # {...} + # } + # + currenciesIds = list(response.keys()) + withdrawFees: dict = {} + depositFees: dict = {} + for i in range(0, len(currenciesIds)): + currency = currenciesIds[i] + data = response[currency] + code = self.safe_currency_code(currency) + withdraw = self.safe_value(data, 'withdraw', {}) + withdrawFees[code] = self.safe_string(withdraw, 'fixed') + deposit = self.safe_value(data, 'deposit', {}) + depositFees[code] = self.safe_string(deposit, 'fixed') + return { + 'withdraw': withdrawFees, + 'deposit': depositFees, + 'info': response, + } + + async def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}): + """ + fetch deposit and withdraw fees + + https://docs.whitebit.com/public/http-v4/#fee + + :param str[]|None codes: not used by fetchDepositWithdrawFees() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `fee structures ` + """ + await self.load_markets() + response = await self.v4PublicGetFee(params) + # + # { + # "1INCH": { + # "is_depositable": True, + # "is_withdrawal": True, + # "ticker": "1INCH", + # "name": "1inch", + # "providers": [], + # "withdraw": { + # "max_amount": "0", + # "min_amount": "21.5", + # "fixed": "17.5", + # "flex": null + # }, + # "deposit": { + # "max_amount": "0", + # "min_amount": "19.5", + # "fixed": null, + # "flex": null + # } + # }, + # "WBT(ERC20)": { + # "is_depositable": True, + # "is_withdrawal": True, + # "ticker": "WBT", + # "name": "WhiteBIT Token", + # "providers": [], + # "withdraw": {max_amount: "0", min_amount: '0.7', fixed: "0.253", flex: null}, + # "deposit": {max_amount: "0", min_amount: "0.35", fixed: null, flex: null} + # }, + # "WBT(TRC20)": { + # "is_depositable": True, + # "is_withdrawal": True, + # "ticker": "WBT", + # "name": "WhiteBIT Token", + # "providers": [], + # "withdraw": {max_amount: "0", min_amount: "1.5", fixed: "0.075", flex: null}, + # "deposit": {max_amount: "0", min_amount: "0.75", fixed: null, flex: null} + # }, + # ... + # } + # + return self.parse_deposit_withdraw_fees(response, codes) + + def parse_deposit_withdraw_fees(self, response, codes=None, currencyIdKey=None): + # + # { + # "1INCH": { + # "is_depositable": True, + # "is_withdrawal": True, + # "ticker": "1INCH", + # "name": "1inch", + # "providers": [], + # "withdraw": { + # "max_amount": "0", + # "min_amount": "21.5", + # "fixed": "17.5", + # "flex": null + # }, + # "deposit": { + # "max_amount": "0", + # "min_amount": "19.5", + # "fixed": null, + # "flex": null + # } + # }, + # "WBT(ERC20)": { + # "is_depositable": True, + # "is_withdrawal": True, + # "ticker": "WBT", + # "name": "WhiteBIT Token", + # "providers": [], + # "withdraw": {max_amount: "0", min_amount: "0.7", fixed: "0.253", flex: null}, + # "deposit": {max_amount: "0", min_amount: "0.35", fixed: null, flex: null} + # }, + # "WBT(TRC20)": { + # "is_depositable": True, + # "is_withdrawal": True, + # "ticker": "WBT", + # "name": "WhiteBIT Token", + # "providers": [], + # "withdraw": {max_amount: "0", min_amount: "1.5", fixed: "0.075", flex: null}, + # "deposit": {max_amount: "0", min_amount: "0.75", fixed: null, flex: null} + # }, + # ... + # } + # + depositWithdrawFees: dict = {} + codes = self.market_codes(codes) + currencyIds = list(response.keys()) + for i in range(0, len(currencyIds)): + entry = currencyIds[i] + splitEntry = entry.split(' ') + currencyId = splitEntry[0] + feeInfo = response[entry] + code = self.safe_currency_code(currencyId) + if (codes is None) or (self.in_array(code, codes)): + depositWithdrawFee = self.safe_value(depositWithdrawFees, code) + if depositWithdrawFee is None: + depositWithdrawFees[code] = self.deposit_withdraw_fee({}) + depositWithdrawFees[code]['info'][entry] = feeInfo + networkId = self.safe_string(splitEntry, 1) + withdraw = self.safe_value(feeInfo, 'withdraw') + deposit = self.safe_value(feeInfo, 'deposit') + withdrawFee = self.safe_number(withdraw, 'fixed') + depositFee = self.safe_number(deposit, 'fixed') + withdrawResult: dict = { + 'fee': withdrawFee, + 'percentage': False if (withdrawFee is not None) else None, + } + depositResult: dict = { + 'fee': depositFee, + 'percentage': False if (depositFee is not None) else None, + } + if networkId is not None: + networkLength = len(networkId) + networkId = networkId[1:networkLength - 1] + networkCode = self.network_id_to_code(networkId) + depositWithdrawFees[code]['networks'][networkCode] = { + 'withdraw': withdrawResult, + 'deposit': depositResult, + } + else: + depositWithdrawFees[code]['withdraw'] = withdrawResult + depositWithdrawFees[code]['deposit'] = depositResult + depositWithdrawCodes = list(depositWithdrawFees.keys()) + for i in range(0, len(depositWithdrawCodes)): + code = depositWithdrawCodes[i] + currency = self.currency(code) + depositWithdrawFees[code] = self.assign_default_deposit_withdraw_fees(depositWithdrawFees[code], currency) + return depositWithdrawFees + + async def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://docs.whitebit.com/public/http-v4/#asset-status-list + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + await self.load_markets() + response = await self.v4PublicGetAssets(params) + # + # { + # "1INCH": { + # "name": "1inch", + # "unified_cryptoasset_id": "8104", + # "can_withdraw": True, + # "can_deposit": True, + # "min_withdraw": "33", + # "max_withdraw": "0", + # "maker_fee": "0.1", + # "taker_fee": "0.1", + # "min_deposit": "30", + # "max_deposit": "0" + # }, + # ... + # } + # + result: dict = {} + for i in range(0, len(self.symbols)): + symbol = self.symbols[i] + market = self.market(symbol) + fee = self.safe_value(response, market['baseId'], {}) + makerFee = self.safe_string(fee, 'maker_fee') + takerFee = self.safe_string(fee, 'taker_fee') + makerFee = Precise.string_div(makerFee, '100') + takerFee = Precise.string_div(takerFee, '100') + result[symbol] = { + 'info': fee, + 'symbol': market['symbol'], + 'percentage': True, + 'tierBased': False, + 'maker': self.parse_number(makerFee), + 'taker': self.parse_number(takerFee), + } + return result + + async def fetch_trading_limits(self, symbols: Strings = None, params={}) -> Any: + """ + fetch the trading limits for a market + + https://docs.whitebit.com/public/http-v4/#market-info + + :param str[]|None symbols: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `trading limits structure ` + """ + await self.load_markets() + # + # Trading limits are derived from market information already loaded by loadMarkets() + # Market structure includes: + # { + # "id": "BTC_USDT", # Market ID + # "symbol": "BTC/USDT", # Unified symbol + # "base": "BTC", # Base currency + # "quote": "USDT", # Quote currency + # "active": True, # Market active status + # "type": "spot", # Market type + # "spot": True, # Spot trading enabled + # "margin": False, # Margin trading enabled + # "future": False, # Futures trading enabled + # "option": False, # Options trading enabled + # "contract": False, # Contract trading enabled + # "settle": None, # Settlement currency + # "settleId": None, # Settlement currency ID + # "contractSize": None, # Contract size + # "linear": None, # Linear contract + # "inverse": None, # Inverse contract + # "limits": { # Trading limits + # "amount": { # Amount limits + # "min": 0.00001, # Minimum amount + # "max": 1000000 # Maximum amount + # }, + # "price": { # Price limits + # "min": 0.01, # Minimum price + # "max": 1000000 # Maximum price + # }, + # "cost": { # Cost limits + # "min": 5.0, # Minimum cost + # "max": 10000000 # Maximum cost + # } + # }, + # "precision": { # Precision settings + # "amount": 5, # Amount precision + # "price": 2 # Price precision + # }, + # "taker": 0.001, # Taker fee + # "maker": 0.001, # Maker fee + # "percentage": True, # Fee percentage + # "tierBased": False # Tier-based fees + # } + # + result: dict = {} + # Process all markets from the loaded markets cache + marketIds = list(self.markets.keys()) + for i in range(0, len(marketIds)): + marketId = marketIds[i] + market = self.markets[marketId] + if not market or not market['symbol']: + continue # Skip invalid markets silently + symbol = market['symbol'] + # Filter by symbols if specified + if symbols: + symbolFound = False + for j in range(0, len(symbols)): + if symbols[j] == symbol: + symbolFound = True + break + if not symbolFound: + continue # Skip symbols not in requested list + # Extract trading limits + limits = self.safe_dict(market, 'limits') + amountLimits = self.safe_dict(limits, 'amount') + priceLimits = self.safe_dict(limits, 'price') + costLimits = self.safe_dict(limits, 'cost') + # Validate that all required limits exist and are valid numbers + hasAmountLimits = amountLimits and self.safe_number(amountLimits, 'min') is not None and self.safe_number(amountLimits, 'max') is not None + hasPriceLimits = priceLimits and self.safe_number(priceLimits, 'min') is not None and self.safe_number(priceLimits, 'max') is not None + hasCostLimits = costLimits and self.safe_number(costLimits, 'min') is not None and self.safe_number(costLimits, 'max') is not None + if hasAmountLimits and hasPriceLimits and hasCostLimits: + result[symbol] = { + 'info': market, + 'limits': { + 'amount': { + 'min': self.safe_number(amountLimits, 'min'), + 'max': self.safe_number(amountLimits, 'max'), + }, + 'price': { + 'min': self.safe_number(priceLimits, 'min'), + 'max': self.safe_number(priceLimits, 'max'), + }, + 'cost': { + 'min': self.safe_number(costLimits, 'min'), + 'max': self.safe_number(costLimits, 'max'), + }, + }, + } + return result + + async def fetch_funding_limits(self, codes: Strings = None, params={}): + """ + fetch the deposit and withdrawal limits for a currency + + https://docs.whitebit.com/public/http-v4/#asset-status-list + https://docs.whitebit.com/public/http-v4/#fee + + :param str[]|None codes: unified currency codes + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding limits structure ` + """ + await self.load_markets() + # Fetch both currencies and fees data for comprehensive funding limits + currenciesData, feesData = await asyncio.gather(*[ + self.fetch_currencies(), + self.v4PublicGetFee(params), + ]) + # + # Currencies response structure(from fetchCurrencies): + # { + # "BTC": { + # "id": "BTC", # Currency ID + # "code": "BTC", # Currency code + # "name": "Bitcoin", # Currency name + # "active": True, # Currency active status + # "type": "crypto", # Currency type + # "precision": 8, # Currency precision + # "limits": { # Currency limits + # "deposit": { # Deposit limits + # "min": 0.00001, # Minimum deposit + # "max": 1000000 # Maximum deposit + # }, + # "withdraw": { # Withdrawal limits + # "min": 0.00001, # Minimum withdrawal + # "max": 1000000 # Maximum withdrawal + # } + # }, + # "networks": { # Network-specific limits + # "BTC": { + # "limits": { + # "deposit": {"min": "0.001"}, + # "withdraw": {"min": "0.002"} + # } + # } + # }, + # "info": {...} # Original API response + # } + # } + # + # Fees response structure(from /api/v4/public/fee): + # { + # "USDT(ERC20)": { + # "ticker": "USDT", + # "name": "Tether US", + # "deposit": { + # "min_amount": "0.0005", + # "max_amount": "0.1", + # "fixed": "0.0005", + # "flex": { + # "min_fee": "100", + # "max_fee": "1000", + # "percent": "10" + # } + # }, + # "withdraw": { + # "min_amount": "0.001", + # "max_amount": "0", + # "fixed": null, + # "flex": null + # } + # } + # } + # + result: dict = {} + currencyKeys = list(currenciesData.keys()) + for i in range(0, len(currencyKeys)): + code = currencyKeys[i] + currency = currenciesData[code] + if not currency: + # Skip invalid currency silently + continue + if codes is not None and not self.in_array(code, codes): + # Skip currency not in requested list silently + continue + # Find corresponding fee data for self currency + feeData = None + feeKeys = list(feesData.keys()) + for j in range(0, len(feeKeys)): + feeKey = feeKeys[j] + fee = feesData[feeKey] + if fee and fee['ticker'] == code: + feeData = fee + break + # Build comprehensive funding limits + limits: dict = { + 'deposit': { + 'min': currency['limits']['deposit']['min'], + 'max': currency['limits']['deposit']['max'], + }, + 'withdraw': { + 'min': currency['limits']['withdraw']['min'], + 'max': currency['limits']['withdraw']['max'], + }, + } + # Add fee information if available + if feeData: + depositFee = feeData['deposit'] + withdrawFee = feeData['withdraw'] + if depositFee: + depositFeeData = { + 'fixed': self.safe_number(depositFee, 'fixed'), + } + if depositFee['flex']: + depositFeeData['flex'] = { + 'min': self.safe_number(depositFee['flex'], 'min_fee'), + 'max': self.safe_number(depositFee['flex'], 'max_fee'), + 'percent': self.safe_number(depositFee['flex'], 'percent'), + } + limits['deposit']['fee'] = depositFeeData + if withdrawFee: + withdrawFeeData = { + 'fixed': self.safe_number(withdrawFee, 'fixed'), + } + if withdrawFee['flex']: + withdrawFeeData['flex'] = { + 'min': self.safe_number(withdrawFee['flex'], 'min_fee'), + 'max': self.safe_number(withdrawFee['flex'], 'max_fee'), + 'percent': self.safe_number(withdrawFee['flex'], 'percent'), + } + limits['withdraw']['fee'] = withdrawFeeData + # Add network-specific limits if available + if currency['networks']: + limits['networks'] = currency['networks'] + result[code] = { + 'info': currency, + 'limits': limits, + } + return result + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://docs.whitebit.com/public/http-v4/#market-activity + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + response = await self.v1PublicGetTicker(self.extend(request, params)) + # + # { + # "success":true, + # "message":"", + # "result": { + # "bid":"0.021979", + # "ask":"0.021996", + # "open":"0.02182", + # "high":"0.022039", + # "low":"0.02161", + # "last":"0.021987", + # "volume":"2810.267", + # "deal":"61.383565474", + # "change":"0.76", + # }, + # } + # + ticker = self.safe_dict(response, 'result', {}) + return self.parse_ticker(ticker, market) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # FetchTicker(v1) + # + # { + # "bid": "0.021979", + # "ask": "0.021996", + # "open": "0.02182", + # "high": "0.022039", + # "low": "0.02161", + # "last": "0.021987", + # "volume": "2810.267", + # "deal": "61.383565474", + # "change": "0.76", + # } + # + # FetchTickers(v4) + # + # "BCH_RUB": { + # "base_id": 1831, + # "quote_id": 0, + # "last_price": "32830.21", + # "quote_volume": "1494659.8024096", + # "base_volume": "46.1083", + # "isFrozen": False, + # "change": "2.12" # in percent + # } + # + # WS market_update + # + # { + # "open": "52853.04", + # "close": "55913.88", + # "high": "56272", + # "low": "49549.67", + # "volume": "57331.067185", + # "deal": "3063860382.42985338", + # "last": "55913.88", + # "period": 86400 + # } + # v2 + # { + # lastUpdateTimestamp: '2025-01-02T09:16:36.000Z', + # tradingPairs: 'ARB_USDC', + # lastPrice: '0.7727', + # lowestAsk: '0.7735', + # highestBid: '0.7732', + # baseVolume24h: '1555793.74', + # quoteVolume24h: '1157602.622406', + # tradesEnabled: True + # } + # + marketId = self.safe_string(ticker, 'tradingPairs') + market = self.safe_market(marketId, market) + # last price is provided as "last" or "last_price" + last = self.safe_string_n(ticker, ['last', 'last_price', 'lastPrice']) + # if "close" is provided, use it, otherwise use + close = self.safe_string(ticker, 'close', last) + return self.safe_ticker({ + 'symbol': market['symbol'], + 'timestamp': None, + 'datetime': None, + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': self.safe_string_2(ticker, 'bid', 'highestBid'), + 'bidVolume': None, + 'ask': self.safe_string_2(ticker, 'ask', 'lowestAsk'), + 'askVolume': None, + 'vwap': None, + 'open': self.safe_string(ticker, 'open'), + 'close': close, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': self.safe_string(ticker, 'change'), + 'average': None, + 'baseVolume': self.safe_string_n(ticker, ['base_volume', 'volume', 'baseVolume24h']), + 'quoteVolume': self.safe_string_n(ticker, ['quote_volume', 'deal', 'quoteVolume24h']), + 'info': ticker, + }, market) + + async def fetch_order(self, id: str, symbol: Str = None, params={}) -> Order: + """ + fetches information on an order by the id + + https://docs.whitebit.com/private/http-trade-v4/#query-unexecutedactive-orders + https://docs.whitebit.com/private/http-trade-v4/#query-executed-orders + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.checkActive]: whether to check active orders(default: True) + :param boolean [params.checkExecuted]: whether to check executed orders(default: True) + :returns dict: an `order structure ` + """ + await self.load_markets() + # Extract control parameters from params + checkActive = self.safe_bool(params, 'checkActive', True) + checkExecuted = self.safe_bool(params, 'checkExecuted', True) + request: dict = { + 'orderId': id, + } + market = None + if symbol is not None: + market = self.market(symbol) + request['market'] = market['id'] + # Try active orders first(if enabled) + if checkActive: + try: + response = await self.v4PrivatePostOrders(self.extend(request, params)) + # Search for order in active orders response(array format) + for i in range(0, len(response)): + order = response[i] + orderId = self.safe_string(order, 'orderId') + if orderId == id: + marketId = self.safe_string(order, 'market') + marketNew = self.safe_market(marketId, None, '_') + return self.parse_order(order, marketNew) + except Exception as error: + if not (isinstance(error, OrderNotFound)): + raise error + # Try executed orders(if enabled) + if checkExecuted: + try: + response = await self.v4PrivatePostTradeAccountOrderHistory(self.extend(request, params)) + # Search for order in executed orders response(object format) + marketIds = list(response.keys()) + for i in range(0, len(marketIds)): + marketId = marketIds[i] + marketNew = self.safe_market(marketId, None, '_') + orders = response[marketId] + for j in range(0, len(orders)): + order = orders[j] + orderId = self.safe_string(order, 'id') + if orderId == id: + return self.parse_order(order, marketNew) + except Exception as error: + if not (isinstance(error, OrderNotFound)): + raise error + # If both checks failed or were disabled, raise OrderNotFound + raise OrderNotFound(self.id + ' fetchOrder() order not found: ' + id) + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://docs.whitebit.com/public/http-v4/#market-activity + + :param str[] [symbols]: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.method]: either v2PublicGetTicker or v4PublicGetTicker default is v4PublicGetTicker + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + method = 'v4PublicGetTicker' + method, params = self.handle_option_and_params(params, 'fetchTickers', 'method', method) + response = None + if method == 'v4PublicGetTicker': + response = await self.v4PublicGetTicker(params) + else: + response = await self.v2PublicGetTicker(params) + # + # "BCH_RUB": { + # "base_id":1831, + # "quote_id":0, + # "last_price":"32830.21", + # "quote_volume":"1494659.8024096", + # "base_volume":"46.1083", + # "isFrozen":false, + # "change":"2.12" + # }, + # + resultList = self.safe_list(response, 'result') + if resultList is not None: + return self.parse_tickers(resultList, symbols) + marketIds = list(response.keys()) + result: dict = {} + for i in range(0, len(marketIds)): + marketId = marketIds[i] + market = self.safe_market(marketId) + ticker = self.parse_ticker(response[marketId], market) + symbol = ticker['symbol'] + result[symbol] = ticker + return self.filter_by_array_tickers(result, 'symbol', symbols) + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://docs.whitebit.com/public/http-v4/#orderbook + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + if limit is not None: + request['limit'] = limit # default = 100, maximum = 100 + response = await self.v4PublicGetOrderbookMarket(self.extend(request, params)) + # + # { + # "timestamp": 1594391413, + # "asks": [ + # [ + # "9184.41", + # "0.773162" + # ], + # [...] + # ], + # "bids": [ + # [ + # "9181.19", + # "0.010873" + # ], + # [...] + # ] + # } + # + timestamp = self.safe_timestamp(response, 'timestamp') + return self.parse_order_book(response, symbol, timestamp) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://docs.whitebit.com/public/http-v4/#recent-trades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + response = await self.v4PublicGetTradesMarket(self.extend(request, params)) + # + # [ + # { + # "tradeID": 158056419, + # "price": "9186.13", + # "quote_volume": "0.0021", + # "base_volume": "9186.13", + # "trade_timestamp": 1594391747, + # "type": "sell" + # }, + # ], + # + return self.parse_trades(response, market, since, limit) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://docs.whitebit.com/private/http-trade-v4/#query-executed-order-history + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market: Market = None + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['market'] = market['id'] + response = await self.v4PrivatePostTradeAccountExecutedHistory(self.extend(request, params)) + # + # when no symbol is provided + # + # { + # "USDC_USDT":[ + # { + # "id":"1343815269", + # "clientOrderId":"", + # "time":"1641051917.532965", + # "side":"sell", + # "role":"2", + # "amount":"9.986", + # "price":"0.9995", + # "deal":"9.981007", + # "fee":"0.009981007", + # "orderId":"58166729555" + # }, + # ] + # } + # + # when a symbol is provided + # + # [ + # { + # "id": 1343815269, + # "clientOrderId": '', + # "time": 1641051917.532965, + # "side": "sell", + # "role": 2, + # "amount": "9.986", + # "price": "0.9995", + # "deal": "9.981007", + # "fee": "0.009981007", + # "orderId": 58166729555, + # }, + # ] + # + if isinstance(response, list): + return self.parse_trades(response, market, since, limit) + else: + results = [] + keys = list(response.keys()) + for i in range(0, len(keys)): + marketId = keys[i] + marketNew = self.safe_market(marketId, None, '_') + rawTrades = self.safe_value(response, marketId, []) + parsed = self.parse_trades(rawTrades, marketNew, since, limit) + results = self.array_concat(results, parsed) + results = self.sort_by_2(results, 'timestamp', 'id') + return self.filter_by_since_limit(results, since, limit, 'timestamp') + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTradesV4 + # + # { + # "tradeID": 158056419, + # "price": "9186.13", + # "quote_volume": "0.0021", + # "base_volume": "9186.13", + # "trade_timestamp": 1594391747, + # "type": "sell" + # } + # + # orderTrades(v4Private) + # + # { + # "time": 1593342324.613711, + # "fee": "0.00000419198", + # "price": "0.00000701", + # "amount": "598", + # "id": 149156519, # trade id + # "dealOrderId": 3134995325, # orderId + # "clientOrderId": "customId11", + # "role": 2, # 1 = maker, 2 = taker + # "deal": "0.00419198" # amount in money + # "feeAsset": "USDT" + # } + # + # fetchMyTrades + # + # { + # "id": 1343815269, + # "clientOrderId": '', + # "time": 1641051917.532965, + # "side": "sell", + # "role": 2, + # "amount": "9.986", + # "price": "0.9995", + # "deal": "9.981007", + # "fee": "0.009981007", + # "orderId": 58166729555, + # "feeAsset": "USDT" + # } + # + market = self.safe_market(None, market) + timestamp = self.safe_timestamp_2(trade, 'time', 'trade_timestamp') + orderId = self.safe_string_2(trade, 'dealOrderId', 'orderId') + cost = self.safe_string(trade, 'deal') + price = self.safe_string(trade, 'price') + amount = self.safe_string_2(trade, 'amount', 'quote_volume') + id = self.safe_string_2(trade, 'id', 'tradeID') + side = self.safe_string_2(trade, 'type', 'side') + symbol = market['symbol'] + role = self.safe_integer(trade, 'role') + takerOrMaker: Str = None + if role is not None: + takerOrMaker = 'maker' if (role == 1) else 'taker' + fee = None + feeCost = self.safe_string(trade, 'fee') + if feeCost is not None: + fee = { + 'cost': feeCost, + 'currency': self.safe_currency_code(self.safe_string(trade, 'feeAsset')), + } + return self.safe_trade({ + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'id': id, + 'order': orderId, + 'type': None, + 'takerOrMaker': takerOrMaker, + 'side': side, + 'price': price, + 'amount': amount, + 'cost': cost, + 'fee': fee, + }, market) + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://docs.whitebit.com/public/http-v1/#kline + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + 'interval': self.safe_string(self.timeframes, timeframe, timeframe), + } + if since is not None: + maxLimit = 1440 + if limit is None: + limit = maxLimit + limit = min(limit, maxLimit) + start = self.parse_to_int(since / 1000) + request['start'] = start + if limit is not None: + request['limit'] = min(limit, 1440) + response = await self.v1PublicGetKline(self.extend(request, params)) + # + # { + # "success":true, + # "message":"", + # "result":[ + # [1591488000,"0.025025","0.025025","0.025029","0.025023","6.181","0.154686629"], + # [1591488060,"0.025028","0.025033","0.025035","0.025026","8.067","0.201921167"], + # [1591488120,"0.025034","0.02505","0.02505","0.025034","20.089","0.503114696"], + # ] + # } + # + result = self.safe_list(response, 'result', []) + return self.parse_ohlcvs(result, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # [ + # 1591488000, + # "0.025025", + # "0.025025", + # "0.025029", + # "0.025023", + # "6.181", + # "0.154686629" + # ] + # + return [ + self.safe_timestamp(ohlcv, 0), # timestamp + self.safe_number(ohlcv, 1), # open + self.safe_number(ohlcv, 3), # high + self.safe_number(ohlcv, 4), # low + self.safe_number(ohlcv, 2), # close + self.safe_number(ohlcv, 5), # volume + ] + + async def fetch_status(self, params={}): + """ + the latest known information on the availability of the exchange API + + https://docs.whitebit.com/public/http-v4/#server-status + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `status structure ` + """ + response = await self.v4PublicGetPing(params) + # + # [ + # "pong" + # ] + # + status = self.safe_string(response, 0) + return { + 'status': 'ok' if (status == 'pong') else status, + 'updated': None, + 'eta': None, + 'url': None, + 'info': response, + } + + async def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://docs.whitebit.com/public/http-v4/#server-time + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = await self.v4PublicGetTime(params) + # + # { + # "time":1737380046 + # } + # + return self.safe_integer(response, 'time') + + async def create_market_order_with_cost(self, symbol: str, side: OrderSide, cost: float, params={}): + """ + create a market order by providing the symbol, side and cost + :param str symbol: unified symbol of the market to create an order in + :param str side: 'buy' or 'sell' + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + req = { + 'cost': cost, + } + # only buy side is supported + return await self.create_order(symbol, 'market', side, 0, None, self.extend(req, params)) + + async def create_market_buy_order_with_cost(self, symbol: str, cost: float, params={}) -> Order: + """ + create a market buy order by providing the symbol and cost + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + return await self.create_market_order_with_cost(symbol, 'buy', cost, params) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://docs.whitebit.com/private/http-trade-v4/#create-limit-order + https://docs.whitebit.com/private/http-trade-v4/#create-market-order + https://docs.whitebit.com/private/http-trade-v4/#create-buy-stock-market-order + https://docs.whitebit.com/private/http-trade-v4/#create-stop-limit-order + https://docs.whitebit.com/private/http-trade-v4/#create-stop-market-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.cost]: *market orders only* the cost of the order in units of the base currency + :param float [params.triggerPrice]: The price at which a trigger order is triggered at + :param bool [params.postOnly]: If True, the order will only be posted to the order book and not executed immediately + :param str [params.clientOrderId]: a unique id for the order + :param str [params.marginMode]: 'cross' or 'isolated', for margin trading, uses self.options.defaultMarginMode if not passed, defaults to None/None/None + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + 'side': side, + } + cost = None + cost, params = self.handle_param_string(params, 'cost') + if cost is not None: + if (side != 'buy') or (type != 'market'): + raise InvalidOrder(self.id + ' createOrder() cost is only supported for market buy orders') + request['amount'] = self.cost_to_precision(symbol, cost) + else: + request['amount'] = self.amount_to_precision(symbol, amount) + clientOrderId = self.safe_string_2(params, 'clOrdId', 'clientOrderId') + if clientOrderId is None: + brokerId = self.safe_string(self.options, 'brokerId') + if brokerId is not None: + request['clientOrderId'] = brokerId + self.uuid16() + else: + request['clientOrderId'] = clientOrderId + params = self.omit(params, ['clientOrderId']) + marketType = self.safe_string(market, 'type') + isLimitOrder = type == 'limit' + isMarketOrder = type == 'market' + triggerPrice = self.safe_number_n(params, ['triggerPrice', 'stopPrice', 'activation_price']) + isStopOrder = (triggerPrice is not None) + postOnly = self.is_post_only(isMarketOrder, False, params) + marginMode, query = self.handle_margin_mode_and_params('createOrder', params) + if postOnly: + request['postOnly'] = True + if marginMode is not None and marginMode != 'cross': + raise NotSupported(self.id + ' createOrder() is only available for cross margin') + params = self.omit(query, ['postOnly', 'triggerPrice', 'stopPrice']) + useCollateralEndpoint = marginMode is not None or marketType == 'swap' + response = None + if isStopOrder: + request['activation_price'] = self.price_to_precision(symbol, triggerPrice) + if isLimitOrder: + # stop limit order + request['price'] = self.price_to_precision(symbol, price) + response = await self.v4PrivatePostOrderStopLimit(self.extend(request, params)) + else: + # stop market order + if useCollateralEndpoint: + response = await self.v4PrivatePostOrderCollateralTriggerMarket(self.extend(request, params)) + else: + response = await self.v4PrivatePostOrderStopMarket(self.extend(request, params)) + else: + if isLimitOrder: + # limit order + request['price'] = self.price_to_precision(symbol, price) + if useCollateralEndpoint: + response = await self.v4PrivatePostOrderCollateralLimit(self.extend(request, params)) + else: + response = await self.v4PrivatePostOrderNew(self.extend(request, params)) + else: + # market order + if useCollateralEndpoint: + response = await self.v4PrivatePostOrderCollateralMarket(self.extend(request, params)) + else: + if cost is not None: + response = await self.v4PrivatePostOrderMarket(self.extend(request, params)) + else: + response = await self.v4PrivatePostOrderStockMarket(self.extend(request, params)) + return self.parse_order(response) + + async def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + """ + edit a trade order + + https://docs.whitebit.com/private/http-trade-v4/#modify-order + + :param str id: cancel order id + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float price: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + # Handle clientOrderId vs orderId(clientOrderId takes priority) + clientOrderId = self.safe_string(params, 'clientOrderId') + if clientOrderId is not None: + request['clientOrderId'] = clientOrderId + else: + request['orderId'] = id + # Handle amount vs total parameter based on order type and side + triggerPrice = self.safe_number_n(params, ['triggerPrice', 'stopPrice', 'activationPrice']) + isStopOrder = (triggerPrice is not None) + # Handle activation price for stop orders + if isStopOrder: + request['activation_price'] = self.price_to_precision(symbol, triggerPrice) + isLimitOrder = type == 'limit' + total = self.safe_number(params, 'total') + if total is not None: + request['total'] = self.amount_to_precision(symbol, total) + elif amount is not None: + if isLimitOrder: + # Limit orders always use amount parameter + request['amount'] = self.amount_to_precision(symbol, amount) + elif type == 'market' and side == 'buy': + # Market buy orders use total parameter + request['total'] = self.amount_to_precision(symbol, amount) + else: + # Market sell orders use amount parameter + request['amount'] = self.amount_to_precision(symbol, amount) + # Handle price parameter for limit orders + if price is not None: + request['price'] = self.price_to_precision(symbol, price) + # Ensure at least one modifiable parameter is provided + hasModifiableParam = (amount is not None) or (price is not None) or (triggerPrice is not None) or (total is not None) + if not hasModifiableParam: + raise ArgumentsRequired(self.id + ' editOrder() requires at least one of: amount, price, activationPrice, or total parameters') + params = self.omit(params, ['clientOrderId', 'triggerPrice', 'stopPrice', 'activationPrice', 'total']) + response = await self.v4PrivatePostOrderModify(self.extend(request, params)) + return self.parse_order(response) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://docs.whitebit.com/private/http-trade-v4/#cancel-order + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + 'orderId': int(id), + } + response = await self.v4PrivatePostOrderCancel(self.extend(request, params)) + # + # { + # "orderId": 4180284841, # order id + # "clientOrderId": "customId11", # custom order identifier; "clientOrderId": "" - if not specified. + # "market": "BTC_USDT", # deal market + # "side": "buy", # order side + # "type": "stop market", # order type + # "timestamp": 1595792396.165973, # current timestamp + # "dealMoney": "0", # if order finished - amount in money currency that is finished + # "dealStock": "0", # if order finished - amount in stock currency that is finished + # "amount": "0.001", # amount + # "takerFee": "0.001", # maker fee ratio. If the number less than 0.0001 - it will be rounded to zero + # "makerFee": "0.001", # maker fee ratio. If the number less than 0.0001 - it will be rounded to zero + # "left": "0.001", # if order not finished - rest of the amount that must be finished + # "dealFee": "0", # fee in money that you pay if order is finished + # "price": "40000", # price if price isset + # "activation_price": "40000" # activation price if activation price is set + # } + # + return self.parse_order(response) + + async def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders + + https://docs.whitebit.com/private/http-trade-v4/#cancel-all-orders + + :param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: market type, ['swap', 'spot'] + :param boolean [params.isMargin]: cancel all margin orders + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + market = None + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['market'] = market['id'] + type = None + type, params = self.handle_market_type_and_params('cancelAllOrders', market, params) + requestType = [] + if type == 'spot': + isMargin = None + isMargin, params = self.handle_option_and_params(params, 'cancelAllOrders', 'isMargin', False) + if isMargin: + requestType.append('margin') + else: + requestType.append('spot') + elif type == 'swap': + requestType.append('futures') + else: + raise NotSupported(self.id + ' cancelAllOrders() does not support ' + type + ' type') + request['type'] = requestType + response = await self.v4PrivatePostOrderCancelAll(self.extend(request, params)) + # + # [] + # + return self.parse_orders(response, market) + + async def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user(combines open and closed orders) + + https://docs.whitebit.com/private/http-trade-v4/#query-unexecutedactive-orders + https://docs.whitebit.com/private/http-trade-v4/#query-executed-orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + # Fetch both open and closed orders in parallel + openOrders, closedOrders = await asyncio.gather(*[ + self.fetch_open_orders(symbol, since, limit, params), + self.fetch_closed_orders(symbol, since, limit, params), + ]) + allOrders = self.array_concat(openOrders, closedOrders) + # Sort by timestamp(most recent first) + sortedOrders = self.sort_by(allOrders, 'timestamp', True) + # Apply limit if specified(since and symbol filtering already handled by individual methods) + if limit is not None and len(sortedOrders) > limit: + return sortedOrders[0:limit] + return sortedOrders + + async def cancel_all_orders_after(self, timeout: Int, params={}): + """ + dead man's switch, cancel all orders after the given timeout + + https://docs.whitebit.com/private/http-trade-v4/#sync-kill-switch-timer + + :param number timeout: time in milliseconds, 0 represents cancel the timer + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.types]: Order types value. Example: "spot", "margin", "futures" or None + :param str [params.symbol]: symbol unified symbol of the market the order was made in + :returns dict: the api result + """ + await self.load_markets() + symbol = self.safe_string(params, 'symbol') + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelAllOrdersAfter() requires a symbol argument in params') + market = self.market(symbol) + params = self.omit(params, 'symbol') + isBiggerThanZero = (timeout > 0) + request: dict = { + 'market': market['id'], + # 'timeout': self.number_to_string(timeout / 1000) if (timeout > 0) else null, + } + if isBiggerThanZero: + request['timeout'] = self.number_to_string(timeout / 1000) + else: + request['timeout'] = 'null' + response = await self.v4PrivatePostOrderKillSwitch(self.extend(request, params)) + # + # { + # "market": "BTC_USDT", # currency market, + # "startTime": 1662478154, # now timestamp, + # "cancellationTime": 1662478154, # now + timer_value, + # "types": ["spot", "margin"] + # } + # + return response + + def parse_balance(self, response) -> Balances: + balanceKeys = list(response.keys()) + result: dict = {} + for i in range(0, len(balanceKeys)): + id = balanceKeys[i] + code = self.safe_currency_code(id) + balance = response[id] + if isinstance(balance, dict) and balance is not None: + account = self.account() + account['free'] = self.safe_string_2(balance, 'available', 'main_balance') + account['used'] = self.safe_string(balance, 'freeze') + account['total'] = self.safe_string(balance, 'main_balance') + result[code] = account + else: + account = self.account() + account['total'] = balance + result[code] = account + return self.safe_balance(result) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://docs.whitebit.com/private/http-main-v4/#main-balance + https://docs.whitebit.com/private/http-trade-v4/#trading-balance + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + marketType = None + marketType, params = self.handle_market_type_and_params('fetchBalance', None, params) + response = None + if marketType == 'swap': + response = await self.v4PrivatePostCollateralAccountBalance(params) + else: + options = self.safe_value(self.options, 'fetchBalance', {}) + defaultAccount = self.safe_string(options, 'account') + account = self.safe_string_2(params, 'account', 'type', defaultAccount) + params = self.omit(params, ['account', 'type']) + if account == 'main' or account == 'funding': + response = await self.v4PrivatePostMainAccountBalance(params) + else: + response = await self.v4PrivatePostTradeAccountBalance(params) + # + # main account + # + # { + # "BTC":{"main_balance":"0.0013929494020316"}, + # "ETH":{"main_balance":"0.001398289308"}, + # } + # + # spot trade account + # + # { + # "BTC": {"available": "0.123", "freeze": "1"}, + # "XMR": {"available": "3013", "freeze": "100"}, + # } + # + # swap + # + # { + # "BTC": 1, + # "USDT": 1000 + # } + # + return self.parse_balance(response) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://docs.whitebit.com/private/http-trade-v4/#query-unexecutedactive-orders + + :param str [symbol]: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + market = None + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['market'] = market['id'] + if limit is not None: + request['limit'] = min(limit, 100) + response = await self.v4PrivatePostOrders(self.extend(request, params)) + # + # [ + # { + # "orderId": 3686033640, + # "clientOrderId": "customId11", + # "market": "BTC_USDT", + # "side": "buy", + # "type": "limit", + # "timestamp": 1594605801.49815, # current timestamp of unexecuted order + # "dealMoney": "0", # executed amount in money + # "dealStock": "0", # executed amount in stock + # "amount": "2.241379", # active order amount + # "takerFee": "0.001", + # "makerFee": "0.001", + # "left": "2.241379", # unexecuted amount in stock + # "dealFee": "0", # executed fee by deal + # "price": "40000" + # }, + # ] + # + return self.parse_orders(response, market, since, limit, {'status': 'open'}) + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://docs.whitebit.com/private/http-trade-v4/#query-executed-orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + request['market'] = market['id'] + if limit is not None: + request['limit'] = min(limit, 100) # default 50 max 100 + response = await self.v4PrivatePostTradeAccountOrderHistory(self.extend(request, params)) + # + # { + # "BTC_USDT": [ + # { + # "id": 160305483, + # "clientOrderId": "customId11", + # "time": 1594667731.724403, + # "side": "sell", + # "role": 2, # 1 = maker, 2 = taker + # "amount": "0.000076", + # "price": "9264.21", + # "deal": "0.70407996", + # "fee": "0.00070407996" + # }, + # ], + # } + # + marketIds = list(response.keys()) + results = [] + for i in range(0, len(marketIds)): + marketId = marketIds[i] + marketNew = self.safe_market(marketId, None, '_') + orders = response[marketId] + for j in range(0, len(orders)): + order = self.parse_order(orders[j], marketNew) + results.append(self.extend(order, {'status': 'closed'})) + results = self.sort_by(results, 'timestamp') + results = self.filter_by_symbol_since_limit(results, symbol, since, limit) + return results + + def parse_order_type(self, type: Str): + types: dict = { + 'limit': 'limit', + 'market': 'market', + 'stop market': 'market', + 'stop limit': 'limit', + 'stock market': 'market', + 'margin limit': 'limit', + 'margin market': 'market', + } + return self.safe_string(types, type, type) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder, fetchOpenOrders, cancelOrder + # + # { + # "orderId":105687928629, + # "clientOrderId":"", + # "market":"DOGE_USDT", + # "side":"sell", + # "type":"stop market", + # "timestamp":1659091079.729576, + # "dealMoney":"0", # executed amount in quote + # "dealStock":"0", # base filled amount + # "amount":"100", + # "takerFee":"0.001", + # "makerFee":"0", + # "left":"100", + # "price": "40000", # price if price isset + # "dealFee":"0", + # "activation_price":"0.065" # stop price(if stop limit or stop market) + # } + # + # fetchClosedOrders + # + # { + # "id":105531094719, + # "clientOrderId":"", + # "ctime":1659045334.550127, + # "ftime":1659045334.550127, + # "side":"buy", + # "amount":"5.9940059", # cost in terms of quote for regular market orders, amount in terms or base for all other order types + # "price":"0", + # "type":"market", + # "takerFee":"0.001", + # "makerFee":"0", + # "dealFee":"0.0059375815", + # "dealStock":"85", # base filled amount + # "dealMoney":"5.9375815", # executed amount in quote + # } + # + marketId = self.safe_string(order, 'market') + market = self.safe_market(marketId, market, '_') + symbol = market['symbol'] + side = self.safe_string(order, 'side') + filled = self.safe_string(order, 'dealStock') + remaining = self.safe_string(order, 'left') + clientOrderId = self.safe_string(order, 'clientOrderId') + if clientOrderId == '': + clientOrderId = None + price = self.safe_string(order, 'price') + triggerPrice = self.safe_number(order, 'activation_price') + orderId = self.safe_string_2(order, 'orderId', 'id') + type = self.safe_string(order, 'type') + orderType = self.parse_order_type(type) + if orderType == 'market': + remaining = None + amount = self.safe_string(order, 'amount') + cost = self.safe_string(order, 'dealMoney') + if (side == 'buy') and ((type == 'market') or (type == 'stop market')): + amount = filled + dealFee = self.safe_string(order, 'dealFee') + fee = None + if dealFee is not None: + fee = { + 'cost': self.parse_number(dealFee), + 'currency': market['quote'], + } + timestamp = self.safe_timestamp_2(order, 'ctime', 'timestamp') + lastTradeTimestamp = self.safe_timestamp(order, 'ftime') + return self.safe_order({ + 'info': order, + 'id': orderId, + 'symbol': symbol, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': lastTradeTimestamp, + 'timeInForce': None, + 'postOnly': None, + 'status': None, + 'side': side, + 'price': price, + 'type': orderType, + 'triggerPrice': triggerPrice, + 'amount': amount, + 'filled': filled, + 'remaining': remaining, + 'average': None, + 'cost': cost, + 'fee': fee, + 'trades': None, + }, market) + + async def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all the trades made from a single order + + https://docs.whitebit.com/private/http-trade-v4/#query-executed-order-deals + + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + request: dict = { + 'orderId': int(id), + } + market = None + if symbol is not None: + market = self.market(symbol) + request['market'] = market['id'] + if limit is not None: + request['limit'] = min(limit, 100) + response = await self.v4PrivatePostTradeAccountOrder(self.extend(request, params)) + # + # { + # "records": [ + # { + # "time": 1593342324.613711, + # "fee": "0.00000419198", + # "price": "0.00000701", + # "amount": "598", + # "id": 149156519, # trade id + # "dealOrderId": 3134995325, # orderId + # "clientOrderId": "customId11", # empty string if not specified + # "role": 2, # 1 = maker, 2 = taker + # "deal": "0.00419198" + # } + # ], + # "offset": 0, + # "limit": 100 + # } + # + data = self.safe_list(response, 'records', []) + return self.parse_trades(data, market) + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://docs.whitebit.com/private/http-main-v4/#get-depositwithdraw-history + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.transactionMethod]: transaction method(1=deposit, 2=withdrawal) - automatically set to '2' for withdrawals + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + currency = None + request: dict = {} + if code is not None: + currency = self.currency(code) + request['ticker'] = currency['id'] + if since is not None: + request['startDate'] = self.parse_to_int(since / 1000) + if limit is None or limit > 100: + limit = 100 + if limit is not None: + request['limit'] = limit + # Use transactionMethod parameter to filter withdrawals server-side(method = 2) + request['transactionMethod'] = '2' + response = await self.v4PrivatePostMainAccountHistory(self.extend(request, params)) + # + # [ + # { + # "id": 123456789, # Transaction ID + # "method": "2", # Method: 1=deposit, 2=withdrawal(filtered server-side) + # "ticker": "BTC", # Currency ticker + # "amount": "0.001", # Transaction amount + # "address": "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", # Withdrawal address + # "memo": "", # Memo/tag(if required) + # "network": "BTC", # Network name + # "fee": "0.0005", # Transaction fee + # "status": "1", # Status: 0=pending, 1=completed, 2=failed + # "timestamp": 1641051917, # Transaction timestamp + # "txid": "abc123def456..." # Transaction hash + # }, + # {...} # More withdrawal transactions + # ] + # + return self.parse_transactions(response, currency, since, limit) + + async def fetch_transactions(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch history of deposits and withdrawals + + https://docs.whitebit.com/private/http-main-v4/#get-depositwithdraw-history + + :param str [code]: unified currency code + :param int [since]: the earliest time in ms to fetch transactions for + :param int [limit]: the maximum number of transactions structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.transactionMethod]: transaction method(1=deposit, 2=withdrawal) - automatically set to '1' for deposits + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + currency = None + request: dict = {} + if code is not None: + currency = self.currency(code) + request['ticker'] = currency['id'] + if since is not None: + request['startDate'] = self.parse_to_int(since / 1000) + if limit is None or limit > 100: + limit = 100 + if limit is not None: + request['limit'] = limit + # Do not filter by transactionMethod to get all transactions(deposits and withdrawals) + response = await self.v4PrivatePostMainAccountHistory(self.extend(request, params)) + # + # [ + # { + # "id": 123456789, # Transaction ID + # "method": "1", # Method: 1=deposit, 2=withdrawal + # "ticker": "BTC", # Currency ticker + # "amount": "0.001", # Transaction amount + # "address": "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", # Transaction address + # "memo": "", # Memo/tag(if required) + # "network": "BTC", # Network name + # "fee": "0.0005", # Transaction fee + # "status": "1", # Status: 0=pending, 1=completed, 2=failed + # "timestamp": 1641051917, # Transaction timestamp + # "txid": "abc123def456..." # Transaction hash + # }, + # {...} # More transactions(deposits and withdrawals) + # ] + # + return self.parse_transactions(response, currency, since, limit) + + async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://docs.whitebit.com/private/http-main-v4/#get-fiat-deposit-address + https://docs.whitebit.com/private/http-main-v4/#get-cryptocurrency-deposit-address + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'ticker': currency['id'], + } + response = None + if self.is_fiat(code): + provider = self.safe_string(params, 'provider') + if provider is None: + raise ArgumentsRequired(self.id + ' fetchDepositAddress() requires a provider when the ticker is fiat') + request['provider'] = provider + amount = self.safe_number(params, 'amount') + if amount is None: + raise ArgumentsRequired(self.id + ' fetchDepositAddress() requires an amount when the ticker is fiat') + request['amount'] = amount + uniqueId = self.safe_value(params, 'uniqueId') + if uniqueId is None: + raise ArgumentsRequired(self.id + ' fetchDepositAddress() requires an uniqueId when the ticker is fiat') + response = await self.v4PrivatePostMainAccountFiatDepositUrl(self.extend(request, params)) + else: + response = await self.v4PrivatePostMainAccountAddress(self.extend(request, params)) + # + # fiat + # + # { + # "url": "https://someaddress.com" + # } + # + # crypto + # + # { + # "account": { + # "address": "GDTSOI56XNVAKJNJBLJGRNZIVOCIZJRBIDKTWSCYEYNFAZEMBLN75RMN", + # "memo": "48565488244493" + # }, + # "required": { + # "fixedFee": "0", + # "flexFee": { + # "maxFee": "0", + # "minFee": "0", + # "percent": "0" + # }, + # "maxAmount": "0", + # "minAmount": "1" + # } + # } + # + url = self.safe_string(response, 'url') + account = self.safe_value(response, 'account', {}) + address = self.safe_string(account, 'address', url) + tag = self.safe_string(account, 'memo') + self.check_address(address) + return { + 'info': response, + 'currency': code, + 'network': None, + 'address': address, + 'tag': tag, + } + + async def create_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + create a currency deposit address + + https://docs.whitebit.com/private/http-main-v4/#create-new-address-for-deposit + + :param str code: unified currency code of the currency for the deposit address + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.network]: the blockchain network to create a deposit address on + :param str [params.type]: address type, available for specific currencies + :returns dict: an `address structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'ticker': currency['id'], + } + response = await self.v4PrivatePostMainAccountCreateNewAddress(self.extend(request, params)) + # + # { + # "account": { + # "address": "GDTSOI56XNVAKJNJBLJGRNZIVOCIZJRBIDKTWSCYEYNFAZEMBLN75RMN", + # "memo": "48565488244493" + # }, + # "required": { + # "maxAmount": "0", + # "minAmount": "1", + # "fixedFee": "0", + # "flexFee": { + # "maxFee": "0", + # "minFee": "0", + # "percent": "0" + # } + # } + # } + # + data = self.safe_dict(response, 'account', {}) + return self.parse_deposit_address(data, currency) + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + # + # { + # "address": "GDTSOI56XNVAKJNJBLJGRNZIVOCIZJRBIDKTWSCYEYNFAZEMBLN75RMN", + # "memo": "48565488244493" + # }, + # + return { + 'info': depositAddress, + 'currency': self.safe_currency_code(None, currency), + 'network': None, + 'address': self.safe_string(depositAddress, 'address'), + 'tag': self.safe_string(depositAddress, 'memo'), + } + + async def fetch_accounts(self, params={}) -> List[Account]: + """ + fetch all the accounts associated with a profile + + https://docs.whitebit.com/private/http-main-v4/#sub-account-list + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `account structures ` + """ + await self.load_markets() + accounts = [] + # Fetch sub-accounts + # + # [ + # { + # "id": "12345", + # "name": "SubAccount1", + # "status": "active", + # "permissions": ["trade", "withdraw"] + # } + # ] + # + subAccounts = await self.v4PrivatePostSubAccountList(params) + if subAccounts and isinstance(subAccounts, list): + for i in range(0, len(subAccounts)): + subAccount = self.safe_value(subAccounts, i) + accountId = self.safe_string(subAccount, 'id') + accountName = self.safe_string(subAccount, 'name') + if accountId: + accounts.append({ + 'id': accountId, + 'type': 'subaccount', + 'name': accountName or 'SubAccount ' + accountId, + 'code': None, + 'info': subAccount, + }) + return accounts + + async def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://docs.whitebit.com/private/http-trade-v4/#change-collateral-account-leverage + + :param float leverage: the rate of leverage + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + await self.load_markets() + if symbol is not None: + raise NotSupported(self.id + ' setLeverage() does not allow to set per symbol') + if (leverage < 1) or (leverage > 20): + raise BadRequest(self.id + ' setLeverage() leverage should be between 1 and 20') + request: dict = { + 'leverage': leverage, + } + return await self.v4PrivatePostCollateralAccountLeverage(self.extend(request, params)) + # { + # "leverage": 5 + # } + + async def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://docs.whitebit.com/private/http-main-v4/#transfer-between-main-and-trade-balances + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account to transfer from - main, spot, collateral + :param str toAccount: account to transfer to - main, spot, collateral + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transfer structure ` + """ + await self.load_markets() + currency = self.currency(code) + accountsByType = self.safe_value(self.options, 'accountsByType') + fromAccountId = self.safe_string(accountsByType, fromAccount, fromAccount) + toAccountId = self.safe_string(accountsByType, toAccount, toAccount) + amountString = self.currency_to_precision(code, amount) + request: dict = { + 'ticker': currency['id'], + 'amount': amountString, + 'from': fromAccountId, + 'to': toAccountId, + } + response = await self.v4PrivatePostMainAccountTransfer(self.extend(request, params)) + # + # [] + # + return self.parse_transfer(response, currency) + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + # + # [] + # + return { + 'info': transfer, + 'id': None, + 'timestamp': None, + 'datetime': None, + 'currency': self.safe_currency_code(None, currency), + 'amount': None, + 'fromAccount': None, + 'toAccount': None, + 'status': None, + } + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://docs.whitebit.com/private/http-main-v4/#create-withdraw-request + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + await self.load_markets() + currency = self.currency(code) # check if it has canDeposit + request: dict = { + 'ticker': currency['id'], + 'amount': self.currency_to_precision(code, amount), + 'address': address, + } + uniqueId = self.safe_value(params, 'uniqueId') + if uniqueId is None: + uniqueId = self.uuid22() + request['uniqueId'] = uniqueId + if tag is not None: + request['memo'] = tag + if self.is_fiat(code): + provider = self.safe_value(params, 'provider') + if provider is None: + raise ArgumentsRequired(self.id + ' withdraw() requires a provider when the ticker is fiat') + request['provider'] = provider + response = await self.v4PrivatePostMainAccountWithdraw(self.extend(request, params)) + # + # empty array with a success status + # go to deposit/withdraw history and check you request status by uniqueId + # + # [] + # + return self.extend({'id': uniqueId}, self.parse_transaction(response, currency)) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # { + # "address": "3ApEASLcrQtZpg1TsssFgYF5V5YQJAKvuE", # deposit address + # "uniqueId": null, # unique Id of deposit + # "transactionId": "a6d71d69-2b17-4ad8-8b15-2d686c54a1a5", + # "createdAt": 1593437922, # timestamp of deposit + # "currency": "Bitcoin", # deposit currency + # "ticker": "BTC", # deposit currency ticker + # "method": 1, # called method 1 - deposit, 2 - withdraw + # "amount": "0.0006", # amount of deposit + # "description": "", # deposit description + # "memo": "", # deposit memo + # "fee": "0", # deposit fee + # "status": 15, # transactions status + # "network": null, # if currency is multinetwork + # "transactionHash": "a275a514013e4e0f927fd0d1bed215e7f6f2c4c6ce762836fe135ec22529d886", # deposit transaction hash + # "details": { + # "partial": { # details about partially successful withdrawals + # "requestAmount": "50000", # requested withdrawal amount + # "processedAmount": "39000", # processed withdrawal amount + # "processedFee": "273", # fee for processed withdrawal amount + # "normalizeTransaction": "" # deposit id + # } + # }, + # "confirmations": { # if transaction status == 15 you can see self object + # "actual": 1, # current block confirmations + # "required": 2 # required block confirmation for successful deposit + # } + # "centralized": False, + # } + # + currency = self.safe_currency(None, currency) + address = self.safe_string(transaction, 'address') + timestamp = self.safe_timestamp(transaction, 'createdAt') + currencyId = self.safe_string(transaction, 'ticker') + status = self.safe_string(transaction, 'status') + method = self.safe_string(transaction, 'method') + return { + 'id': self.safe_string(transaction, 'uniqueId'), + 'txid': self.safe_string(transaction, 'transactionId'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': self.safe_string(transaction, 'network'), + 'addressFrom': address if (method == '1') else None, + 'address': address, + 'addressTo': address if (method == '2') else None, + 'amount': self.safe_number(transaction, 'amount'), + 'type': 'deposit' if (method == '1') else 'withdrawal', + 'currency': self.safe_currency_code(currencyId, currency), + 'status': self.parse_transaction_status(status), + 'updated': None, + 'tagFrom': None, + 'tag': None, + 'tagTo': None, + 'comment': self.safe_string(transaction, 'description'), + 'internal': None, + 'fee': { + 'cost': self.safe_number(transaction, 'fee'), + 'currency': self.safe_currency_code(currencyId, currency), + }, + 'info': transaction, + } + + def parse_transaction_status(self, status: Str): + statuses: dict = { + '1': 'pending', + '2': 'pending', + '3': 'ok', + '4': 'canceled', + '5': 'pending', + '6': 'pending', + '7': 'ok', + '9': 'canceled', + '10': 'pending', + '11': 'pending', + '12': 'pending', + '13': 'pending', + '14': 'pending', + '15': 'pending', + '16': 'pending', + '17': 'pending', + } + return self.safe_string(statuses, status, status) + + async def fetch_deposit(self, id: str, code: Str = None, params={}): + """ + fetch information on a deposit + + https://docs.whitebit.com/private/http-main-v4/#get-depositwithdraw-history + + :param str id: deposit id + :param str code: not used by whitebit fetchDeposit() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + await self.load_markets() + currency = None + request: dict = { + 'transactionMethod': 1, + 'uniqueId': id, + 'limit': 1, + 'offset': 0, + } + if code is not None: + currency = self.currency(code) + request['ticker'] = currency['id'] + response = await self.v4PrivatePostMainAccountHistory(self.extend(request, params)) + # + # { + # "limit": 100, + # "offset": 0, + # "records": [ + # { + # "address": "3ApEASLcrQtZpg1TsssFgYF5V5YQJAKvuE", # deposit address + # "uniqueId": null, # unique Id of deposit + # "createdAt": 1593437922, # timestamp of deposit + # "currency": "Bitcoin", # deposit currency + # "ticker": "BTC", # deposit currency ticker + # "method": 1, # called method 1 - deposit, 2 - withdraw + # "amount": "0.0006", # amount of deposit + # "description": "", # deposit description + # "memo": "", # deposit memo + # "fee": "0", # deposit fee + # "status": 15, # transactions status + # "network": null, # if currency is multinetwork + # "transactionHash": "a275a514013e4e0f927fd0d1bed215e7f6f2c4c6ce762836fe135ec22529d886", # deposit transaction hash + # "details": { + # "partial": { # details about partially successful withdrawals + # "requestAmount": "50000", # requested withdrawal amount + # "processedAmount": "39000", # processed withdrawal amount + # "processedFee": "273", # fee for processed withdrawal amount + # "normalizeTransaction": "" # deposit id + # } + # }, + # "confirmations": { # if transaction status == 15 you can see self object + # "actual": 1, # current block confirmations + # "required": 2 # required block confirmation for successful deposit + # } + # }, + # {...}, + # ], + # "total": 300 # total number of transactions, use self for calculating ‘limit’ and ‘offset' + # } + # + records = self.safe_value(response, 'records', []) + first = self.safe_dict(records, 0, {}) + return self.parse_transaction(first, currency) + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://docs.whitebit.com/private/http-main-v4/#get-depositwithdraw-history + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + currency = None + request: dict = { + 'transactionMethod': 1, + 'limit': 100, + 'offset': 0, + } + if code is not None: + currency = self.currency(code) + request['ticker'] = currency['id'] + if limit is not None: + request['limit'] = min(limit, 100) + response = await self.v4PrivatePostMainAccountHistory(self.extend(request, params)) + # + # { + # "limit": 100, + # "offset": 0, + # "records": [ + # { + # "address": "3ApEASLcrQtZpg1TsssFgYF5V5YQJAKvuE", # deposit address + # "uniqueId": null, # unique Id of deposit + # "createdAt": 1593437922, # timestamp of deposit + # "currency": "Bitcoin", # deposit currency + # "ticker": "BTC", # deposit currency ticker + # "method": 1, # called method 1 - deposit, 2 - withdraw + # "amount": "0.0006", # amount of deposit + # "description": "", # deposit description + # "memo": "", # deposit memo + # "fee": "0", # deposit fee + # "status": 15, # transactions status + # "network": null, # if currency is multinetwork + # "transactionHash": "a275a514013e4e0f927fd0d1bed215e7f6f2c4c6ce762836fe135ec22529d886", # deposit transaction hash + # "details": { + # "partial": { # details about partially successful withdrawals + # "requestAmount": "50000", # requested withdrawal amount + # "processedAmount": "39000", # processed withdrawal amount + # "processedFee": "273", # fee for processed withdrawal amount + # "normalizeTransaction": "" # deposit id + # } + # }, + # "confirmations": { # if transaction status == 15 you can see self object + # "actual": 1, # current block confirmations + # "required": 2 # required block confirmation for successful deposit + # } + # }, + # {...}, + # ], + # "total": 300 # total number of transactions, use self for calculating ‘limit’ and ‘offset' + # } + # + records = self.safe_list(response, 'records', []) + return self.parse_transactions(records, currency, since, limit) + + async def fetch_borrow_interest(self, code: Str = None, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[BorrowInterest]: + """ + fetch the interest owed by the user for borrowing currency for margin trading + + https://docs.whitebit.com/private/http-trade-v4/#open-positions + + :param str code: unified currency code + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch borrrow interest for + :param int [limit]: the maximum number of structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `borrow interest structures ` + """ + await self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['market'] = market['id'] + response = await self.v4PrivatePostCollateralAccountPositionsOpen(self.extend(request, params)) + # + # [ + # { + # "positionId": 191823, + # "market": "BTC_USDT", + # "openDate": 1660340344.027163, + # "modifyDate": 1660340344.027163, + # "amount": "0.003075", + # "basePrice": "24149.24512", + # "liquidationPrice": "7059.02", + # "leverage": "5", + # "pnl": "-0.15", + # "pnlPercent": "-0.20", + # "margin": "14.86", + # "freeMargin": "44.99", + # "funding": "0", + # "unrealizedFunding": "0.0000307828284903", + # "liquidationState": null + # } + # ] + # + interest = self.parse_borrow_interests(response, market) + return self.filter_by_currency_since_limit(interest, code, since, limit) + + def parse_borrow_interest(self, info: dict, market: Market = None) -> BorrowInterest: + # + # { + # "positionId": 191823, + # "market": "BTC_USDT", + # "openDate": 1660340344.027163, + # "modifyDate": 1660340344.027163, + # "amount": "0.003075", + # "basePrice": "24149.24512", + # "liquidationPrice": "7059.02", + # "leverage": "5", + # "pnl": "-0.15", + # "pnlPercent": "-0.20", + # "margin": "14.86", + # "freeMargin": "44.99", + # "funding": "0", + # "unrealizedFunding": "0.0000307828284903", + # "liquidationState": null + # } + # + marketId = self.safe_string(info, 'market') + symbol = self.safe_symbol(marketId, market, '_') + timestamp = self.safe_timestamp(info, 'modifyDate') + return { + 'info': info, + 'symbol': symbol, + 'currency': 'USDT', + 'interest': self.safe_number(info, 'unrealizedFunding'), + 'interestRate': 0.00098, # https://whitebit.com/fees + 'amountBorrowed': self.safe_number(info, 'amount'), + 'marginMode': 'cross', + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + + async def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate + + https://docs.whitebit.com/public/http-v4/#available-futures-markets-list + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + await self.load_markets() + symbol = self.symbol(symbol) + response = await self.fetch_funding_rates([symbol], params) + return self.safe_value(response, symbol) + + async def fetch_funding_rates(self, symbols: Strings = None, params={}) -> FundingRates: + """ + fetch the funding rate for multiple markets + + https://docs.whitebit.com/public/http-v4/#available-futures-markets-list + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `funding rate structures `, indexed by market symbols + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + response = await self.v4PublicGetFutures(params) + # + # [ + # { + # "name": "BTC_USDT", + # "type": "direct", + # "quanto_multiplier": "0.0001", + # "ref_discount_rate": "0", + # "order_price_deviate": "0.5", + # "maintenance_rate": "0.005", + # "mark_type": "index", + # "last_price": "38026", + # "mark_price": "37985.6", + # "index_price": "37954.92", + # "funding_rate_indicative": "0.000219", + # "mark_price_round": "0.01", + # "funding_offset": 0, + # "in_delisting": False, + # "risk_limit_base": "1000000", + # "interest_rate": "0.0003", + # "order_price_round": "0.1", + # "order_size_min": 1, + # "ref_rebate_rate": "0.2", + # "funding_interval": 28800, + # "risk_limit_step": "1000000", + # "leverage_min": "1", + # "leverage_max": "100", + # "risk_limit_max": "8000000", + # "maker_fee_rate": "-0.00025", + # "taker_fee_rate": "0.00075", + # "funding_rate": "0.002053", + # "order_size_max": 1000000, + # "funding_next_apply": 1610035200, + # "short_users": 977, + # "config_change_time": 1609899548, + # "trade_size": 28530850594, + # "position_size": 5223816, + # "long_users": 455, + # "funding_impact_value": "60000", + # "orders_limit": 50, + # "trade_id": 10851092, + # "orderbook_id": 2129638396 + # } + # ] + # + data = self.safe_list(response, 'result', []) + return self.parse_funding_rates(data, symbols) + + def parse_funding_rate(self, contract, market: Market = None) -> FundingRate: + # + # { + # "ticker_id":"ADA_PERP", + # "stock_currency":"ADA", + # "money_currency":"USDT", + # "last_price":"0.296708", + # "stock_volume":"7982130", + # "money_volume":"2345758.29189", + # "bid":"0.296608", + # "ask":"0.296758", + # "high":"0.298338", + # "low":"0.290171", + # "product_type":"Perpetual", + # "open_interest":"46533000", + # "index_price":"0.29659", + # "index_name":"Cardano", + # "index_currency":"ADA", + # "funding_rate":"0.0001", + # "next_funding_rate_timestamp":"1691193600000", + # "brackets":{ + # "1":"0", + # "2":"0", + # "3":"0", + # "5":"0", + # "10":"0", + # "20":"0", + # "50":"-10000", + # "100":"-5000" + # }, + # "max_leverage":"100" + # } + # + marketId = self.safe_string(contract, 'ticker_id') + symbol = self.safe_symbol(marketId, market) + markPrice = self.safe_number(contract, 'markPrice') + indexPrice = self.safe_number(contract, 'indexPrice') + interestRate = self.safe_number(contract, 'interestRate') + fundingRate = self.safe_number(contract, 'funding_rate') + fundingTime = self.safe_integer(contract, 'next_funding_rate_timestamp') + return { + 'info': contract, + 'symbol': symbol, + 'markPrice': markPrice, + 'indexPrice': indexPrice, + 'interestRate': interestRate, + 'timestamp': None, + 'datetime': None, + 'fundingRate': fundingRate, + 'fundingTimestamp': fundingTime, + 'fundingDatetime': self.iso8601(fundingTime), + 'nextFundingRate': None, + 'nextFundingTimestamp': None, + 'nextFundingDatetime': None, + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': None, + } + + async def fetch_funding_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[FundingHistory]: + """ + fetch the history of funding payments paid and received on self account + + https://docs.whitebit.com/private/http-trade-v4/#funding-history + + :param str [symbol]: unified market symbol + :param int [since]: the starting timestamp in milliseconds + :param int [limit]: the number of entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch funding history for + :returns dict[]: a list of `funding history structures ` + """ + await self.load_markets() + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchFundingHistory() requires a symbol argument') + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + if since is not None: + request['startDate'] = since + if limit is not None: + request['limit'] = since + request, params = self.handle_until_option('endDate', request, params) + response = await self.v4PrivatePostCollateralAccountFundingHistory(request) + # + # { + # "records": [ + # { + # "market": "BTC_PERP", + # "fundingTime": "1708704000000", + # "fundingRate": "0.00017674", + # "fundingAmount": "-0.171053531892", + # "positionAmount": "0.019", + # "settlementPrice": "50938.2", + # "rateCalculatedTime": "1708675200000" + # }, + # ], + # "limit": 100, + # "offset": 0 + # } + # + data = self.safe_list(response, 'records', []) + return self.parse_funding_histories(data, market, since, limit) + + def parse_funding_history(self, contract, market: Market = None): + # + # { + # "market": "BTC_PERP", + # "fundingTime": "1708704000000", + # "fundingRate": "0.00017674", + # "fundingAmount": "-0.171053531892", + # "positionAmount": "0.019", + # "settlementPrice": "50938.2", + # "rateCalculatedTime": "1708675200000" + # } + # + marketId = self.safe_string(contract, 'market') + timestamp = self.safe_integer(contract, 'fundingTime') + return { + 'info': contract, + 'symbol': self.safe_symbol(marketId, market, None, 'swap'), + 'code': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': None, + 'amount': self.safe_number(contract, 'fundingAmount'), + } + + def parse_funding_histories(self, contracts, market=None, since: Int = None, limit: Int = None) -> List[FundingHistory]: + result = [] + for i in range(0, len(contracts)): + contract = contracts[i] + result.append(self.parse_funding_history(contract, market)) + sorted = self.sort_by(result, 'timestamp') + return self.filter_by_since_limit(sorted, since, limit) + + async def fetch_deposits_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch history of deposits and withdrawals + + https://github.com/whitebit-exchange/api-docs/blob/main/pages/private/http-main-v4.md#get-depositwithdraw-history + + :param str [code]: unified currency code for the currency of the deposit/withdrawals, default is None + :param int [since]: timestamp in ms of the earliest deposit/withdrawal, default is None + :param int [limit]: max number of deposit/withdrawals to return, default = 50, Min: 1, Max: 100 + :param dict [params]: extra parameters specific to the exchange API endpoint + + EXCHANGE SPECIFIC PARAMETERS + :param number [params.transactionMethod]: Method. Example: 1 to display deposits / 2 to display withdraws. Do not send self parameter in order to receive both deposits and withdraws. + :param str [params.address]: Can be used for filtering transactions by specific address or memo. + :param str[] [params.addresses]: Can be used for filtering transactions by specific addresses or memos(max: 20). + :param str [params.uniqueId]: Can be used for filtering transactions by specific unique id + :param int [params.offset]: If you want the request to return entries starting from a particular line, you can use OFFSET clause to tell it where it should start. Default: 0, Min: 0, Max: 10000 + :param str[] [params.status]: Can be used for filtering transactions by status codes. Caution: You must use self parameter with appropriate transactionMethod and use valid status codes for self method. You can find them below. Example: "status": [3,7] + :returns dict: a list of `transaction structure ` + """ + await self.load_markets() + request: dict = {} + currency = None + if code is not None: + currency = self.currency(code) + request['ticker'] = currency['id'] + if limit is not None: + request['limit'] = limit # default 1000 + response = await self.v4PrivatePostMainAccountHistory(self.extend(request, params)) + # + # { + # "limit": 100, + # "offset": 0, + # "records": [ + # { + # "address": "3ApEASLcrQtZpg1TsssFgYF5V5YQJAKvuE", # deposit address + # "uniqueId": null, # unique Id of deposit + # "createdAt": 1593437922, # timestamp of deposit + # "currency": "Bitcoin", # deposit currency + # "ticker": "BTC", # deposit currency ticker + # "method": 1, # called method 1 - deposit, 2 - withdraw + # "amount": "0.0006", # amount of deposit + # "description": "", # deposit description + # "memo": "", # deposit memo + # "fee": "0", # deposit fee + # "status": 15, # transactions status + # "network": null, # if currency is multinetwork + # "transactionHash": "a275a514013e4e0f927fd0d1bed215e7f6f2c4c6ce762836fe135ec22529d886", # deposit transaction hash + # "transactionId": "5e112b38-9652-11ed-a1eb-0242ac120002", # transaction id + # "details": { + # "partial": { # details about partially successful withdrawals + # "requestAmount": "50000", # requested withdrawal amount + # "processedAmount": "39000", # processed withdrawal amount + # "processedFee": "273", # fee for processed withdrawal amount + # "normalizeTransaction": "" # deposit id + # } + # }, + # "confirmations": { # if transaction status == 15(Pending) you can see self object + # "actual": 1, # current block confirmations + # "required": 2 # required block confirmation for successful deposit + # } + # }, + # {...}, + # ], + # "total": 300 # total number of transactions, use self for calculating ‘limit’ and ‘offset' + # } + # + records = self.safe_list(response, 'records') + return self.parse_transactions(records, currency, since, limit) + + async def fetch_convert_quote(self, fromCode: str, toCode: str, amount: Num = None, params={}) -> Conversion: + """ + fetch a quote for converting from one currency to another + + https://docs.whitebit.com/private/http-trade-v4/#convert-estimate + + :param str fromCode: the currency that you want to sell and convert from + :param str toCode: the currency that you want to buy and convert into + :param float amount: how much you want to trade in units of the from currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `conversion structure ` + """ + await self.load_markets() + fromCurrency = self.currency(fromCode) + toCurrency = self.currency(toCode) + request: dict = { + 'from': fromCode, + 'to': toCode, + 'amount': self.number_to_string(amount), + 'direction': 'from', + } + response = await self.v4PrivatePostConvertEstimate(self.extend(request, params)) + # + # { + # "give": "4", + # "receive": "0.00004762", + # "rate": "0.0000119", + # "id": "1740889", + # "expireAt": 1741090147, + # "from": "USDT", + # "to": "BTC" + # } + # + return self.parse_conversion(response, fromCurrency, toCurrency) + + async def create_convert_trade(self, id: str, fromCode: str, toCode: str, amount: Num = None, params={}) -> Conversion: + """ + convert from one currency to another + + https://docs.whitebit.com/private/http-trade-v4/#convert-confirm + + :param str id: the id of the trade that you want to make + :param str fromCode: the currency that you want to sell and convert from + :param str toCode: the currency that you want to buy and convert into + :param float [amount]: how much you want to trade in units of the from currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `conversion structure ` + """ + await self.load_markets() + fromCurrency = self.currency(fromCode) + toCurrency = self.currency(toCode) + request: dict = { + 'quoteId': id, + } + response = await self.v4PrivatePostConvertConfirm(self.extend(request, params)) + # + # { + # "finalGive": "4", + # "finalReceive": "0.00004772" + # } + # + return self.parse_conversion(response, fromCurrency, toCurrency) + + async def fetch_convert_trade_history(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Conversion]: + """ + fetch the users history of conversion trades + + https://docs.whitebit.com/private/http-trade-v4/#convert-history + + :param str [code]: the unified currency code + :param int [since]: the earliest time in ms to fetch conversions for + :param int [limit]: the maximum number of conversion structures to retrieve, default 20, max 200 + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.until]: the end time in ms + :param str [params.fromTicker]: the currency that you sold and converted from + :param str [params.toTicker]: the currency that you bought and converted into + :param str [params.quoteId]: the quote id of the conversion + :returns dict[]: a list of `conversion structures ` + """ + await self.load_markets() + request: dict = {} + if code is not None: + request['fromTicker'] = code + if since is not None: + start = self.parse_to_int(since / 1000) + request['from'] = self.number_to_string(start) + if limit is not None: + request['limit'] = limit + request, params = self.handle_until_option('to', request, params, 0.001) + response = await self.v4PrivatePostConvertHistory(self.extend(request, params)) + # + # { + # "records": [ + # { + # "id": "1741105", + # "path": [ + # { + # "from": "USDT", + # "to": "BTC", + # "rate": "0.00001193" + # } + # ], + # "date": 1741090757, + # "give": "4", + # "receive": "0.00004772", + # "rate": "0.00001193" + # } + # ], + # "total": 1, + # "limit": 100, + # "offset": 0 + # } + # + rows = self.safe_list(response, 'records', []) + return self.parse_conversions(rows, code, 'fromCurrency', 'toCurrency', since, limit) + + def parse_conversion(self, conversion: dict, fromCurrency: Currency = None, toCurrency: Currency = None) -> Conversion: + # + # fetchConvertQuote + # + # { + # "give": "4", + # "receive": "0.00004762", + # "rate": "0.0000119", + # "id": "1740889", + # "expireAt": 1741090147, + # "from": "USDT", + # "to": "BTC" + # } + # + # createConvertTrade + # + # { + # "finalGive": "4", + # "finalReceive": "0.00004772" + # } + # + # fetchConvertTradeHistory + # + # { + # "id": "1741105", + # "path": [ + # { + # "from": "USDT", + # "to": "BTC", + # "rate": "0.00001193" + # } + # ], + # "date": 1741090757, + # "give": "4", + # "receive": "0.00004772", + # "rate": "0.00001193" + # } + # + path = self.safe_list(conversion, 'path', []) + first = self.safe_dict(path, 0, {}) + fromPath = self.safe_string(first, 'from') + toPath = self.safe_string(first, 'to') + timestamp = self.safe_timestamp_2(conversion, 'date', 'expireAt') + fromCoin = self.safe_string(conversion, 'from', fromPath) + fromCode = self.safe_currency_code(fromCoin, fromCurrency) + toCoin = self.safe_string(conversion, 'to', toPath) + toCode = self.safe_currency_code(toCoin, toCurrency) + return { + 'info': conversion, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': self.safe_string(conversion, 'id'), + 'fromCurrency': fromCode, + 'fromAmount': self.safe_number_2(conversion, 'give', 'finalGive'), + 'toCurrency': toCode, + 'toAmount': self.safe_number_2(conversion, 'receive', 'finalReceive'), + 'price': self.safe_number(conversion, 'rate'), + 'fee': None, + } + + async def fetch_position_history(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Position]: + """ + fetches historical positions + + https://docs.whitebit.com/private/http-trade-v4/#positions-history + + :param str symbol: unified contract symbol + :param int [since]: the earliest time in ms to fetch positions for + :param int [limit]: the maximum amount of records to fetch + :param dict [params]: extra parameters specific to the exchange api endpoint + :param int [params.positionId]: the id of the requested position + :returns dict[]: a list of `position structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + if since is not None: + request['startDate'] = since + if limit is not None: + request['limit'] = since + request, params = self.handle_until_option('endDate', request, params) + response = await self.v4PrivatePostCollateralAccountPositionsHistory(self.extend(request, params)) + # + # [ + # { + # "positionId": 479975679, + # "market": "BTC_PERP", + # "openDate": 1741941025.309887, + # "modifyDate": 1741941025.309887, + # "amount": "0.001", + # "basePrice": "82498.7", + # "realizedFunding": "0", + # "liquidationPrice": "0", + # "liquidationState": null, + # "orderDetail": { + # "id": 1224727949521, + # "tradeAmount": "0.001", + # "price": "82498.7", + # "tradeFee": "0.028874545", + # "fundingFee": "0", + # "realizedPnl": "-0.028874545" + # } + # } + # ] + # + positions = self.parse_positions(response) + return self.filter_by_symbol_since_limit(positions, symbol, since, limit) + + async def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://docs.whitebit.com/private/http-trade-v4/#open-positions + + :param str[] [symbols]: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + response = await self.v4PrivatePostCollateralAccountPositionsOpen(params) + # + # [ + # { + # "positionId": 479975679, + # "market": "BTC_PERP", + # "openDate": 1741941025.3098869, + # "modifyDate": 1741941025.3098869, + # "amount": "0.001", + # "basePrice": "82498.7", + # "liquidationPrice": "70177.2", + # "pnl": "0", + # "pnlPercent": "0.00", + # "margin": "4.2", + # "freeMargin": "9.9", + # "funding": "0", + # "unrealizedFunding": "0", + # "liquidationState": null, + # "tpsl": null + # } + # ] + # + return self.parse_positions(response, symbols) + + async def fetch_position(self, symbol: str, params={}) -> Position: + """ + fetch data on a single open contract trade position + + https://docs.whitebit.com/private/http-trade-v4/#open-positions + + :param str symbol: unified market symbol of the market the position is held in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `position structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = await self.v4PrivatePostCollateralAccountPositionsOpen(self.extend(request, params)) + # + # [ + # { + # "positionId": 479975679, + # "market": "BTC_PERP", + # "openDate": 1741941025.3098869, + # "modifyDate": 1741941025.3098869, + # "amount": "0.001", + # "basePrice": "82498.7", + # "liquidationPrice": "70177.2", + # "pnl": "0", + # "pnlPercent": "0.00", + # "margin": "4.2", + # "freeMargin": "9.9", + # "funding": "0", + # "unrealizedFunding": "0", + # "liquidationState": null, + # "tpsl": null + # } + # ] + # + data = self.safe_dict(response, 0, {}) + return self.parse_position(data, market) + + def parse_position(self, position: dict, market: Market = None) -> Position: + # + # fetchPosition, fetchPositions + # + # { + # "positionId": 479975679, + # "market": "BTC_PERP", + # "openDate": 1741941025.3098869, + # "modifyDate": 1741941025.3098869, + # "amount": "0.001", + # "basePrice": "82498.7", + # "liquidationPrice": "70177.2", + # "pnl": "0", + # "pnlPercent": "0.00", + # "margin": "4.2", + # "freeMargin": "9.9", + # "funding": "0", + # "unrealizedFunding": "0", + # "liquidationState": null, + # "tpsl": null + # } + # + # fetchPositionHistory + # + # { + # "positionId": 479975679, + # "market": "BTC_PERP", + # "openDate": 1741941025.309887, + # "modifyDate": 1741941025.309887, + # "amount": "0.001", + # "basePrice": "82498.7", + # "realizedFunding": "0", + # "liquidationPrice": "0", + # "liquidationState": null, + # "orderDetail": { + # "id": 1224727949521, + # "tradeAmount": "0.001", + # "price": "82498.7", + # "tradeFee": "0.028874545", + # "fundingFee": "0", + # "realizedPnl": "-0.028874545" + # } + # } + # + marketId = self.safe_string(position, 'market') + timestamp = self.safe_timestamp(position, 'openDate') + tpsl = self.safe_dict(position, 'tpsl', {}) + orderDetail = self.safe_dict(position, 'orderDetail', {}) + return self.safe_position({ + 'info': position, + 'id': self.safe_string(position, 'positionId'), + 'symbol': self.safe_symbol(marketId, market), + 'notional': None, + 'marginMode': None, + 'liquidationPrice': self.safe_number(position, 'liquidationPrice'), + 'entryPrice': self.safe_number(position, 'basePrice'), + 'unrealizedPnl': self.safe_number(position, 'pnl'), + 'realizedPnl': self.safe_number(orderDetail, 'realizedPnl'), + 'percentage': self.safe_number(position, 'pnlPercent'), + 'contracts': None, + 'contractSize': None, + 'markPrice': None, + 'lastPrice': None, + 'side': None, + 'hedged': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastUpdateTimestamp': self.safe_timestamp(position, 'modifyDate'), + 'maintenanceMargin': None, + 'maintenanceMarginPercentage': None, + 'collateral': self.safe_number(position, 'margin'), + 'initialMargin': None, + 'initialMarginPercentage': None, + 'leverage': None, + 'marginRatio': None, + 'stopLossPrice': self.safe_number(tpsl, 'stopLoss'), + 'takeProfitPrice': self.safe_number(tpsl, 'takeProfit'), + }) + + async def fetch_cross_borrow_rate(self, code: str, params={}) -> CrossBorrowRate: + """ + fetch the rate of interest to borrow a currency for margin trading + + https://docs.whitebit.com/private/http-main-v4/#get-plans + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `borrow rate structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'ticker': currency['id'], + } + response = await self.v4PrivatePostMainAccountSmartPlans(self.extend(request, params)) + # + # + data = self.safe_list(response, 0, []) + return self.parse_borrow_rate(data, currency) + + def parse_borrow_rate(self, info, currency: Currency = None): + # + # + currencyId = self.safe_string(info, 'ticker') + percent = self.safe_string(info, 'percent') + return { + 'currency': self.safe_currency_code(currencyId, currency), + 'rate': self.parse_number(Precise.string_div(percent, '100')), + 'period': self.safe_integer(info, 'duration'), + 'timestamp': None, + 'datetime': None, + 'info': info, + } + + def is_fiat(self, currency: str) -> bool: + fiatCurrencies = self.safe_value(self.options, 'fiatCurrencies', []) + return self.in_array(currency, fiatCurrencies) + + def nonce(self): + return self.milliseconds() - self.options['timeDifference'] + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + query = self.omit(params, self.extract_params(path)) + version = self.safe_value(api, 0) + accessibility = self.safe_value(api, 1) + if headers is None: + headers = {} + headers['User-Agent'] = 'ccxt/' + self.id + '-' + self.version + pathWithParams = '/' + self.implode_params(path, params) + url = self.urls['api'][version][accessibility] + pathWithParams + if accessibility == 'public': + if query: + url += '?' + self.urlencode(query) + if accessibility == 'private': + self.check_required_credentials() + nonce = str(self.nonce()) + secret = self.encode(self.secret) + request = '/' + 'api' + '/' + version + pathWithParams + body = self.json(self.extend({'request': request, 'nonce': nonce}, params)) + payload = self.string_to_base64(body) + signature = self.hmac(self.encode(payload), secret, hashlib.sha512) + headers = { + 'Content-Type': 'application/json', + 'X-TXC-APIKEY': self.apiKey, + 'X-TXC-PAYLOAD': payload, + 'X-TXC-SIGNATURE': signature, + } + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if (code == 418) or (code == 429): + raise DDoSProtection(self.id + ' ' + str(code) + ' ' + reason + ' ' + body) + if code == 404: + raise ExchangeError(self.id + ' ' + str(code) + ' endpoint not found') + if response is not None: + # For cases where we have a meaningful status + # {"response":null,"status":422,"errors":{"orderId":["Finished order id 435453454535 not found on your account"]},"notification":null,"warning":"Finished order id 435453454535 not found on your account","_token":null} + status = self.safe_string(response, 'status') + errors = self.safe_value(response, 'errors') + # {"code":10,"message":"Unauthorized request."} + message = self.safe_string(response, 'message') + # For these cases where we have a generic code variable error key + # {"code":0,"message":"Validation failed","errors":{"amount":["Amount must be greater than 0"]}} + codeNew = self.safe_integer(response, 'code') + hasErrorStatus = status is not None and status != '200' and errors is not None + if hasErrorStatus or codeNew is not None: + feedback = self.id + ' ' + body + errorInfo = message + if hasErrorStatus: + errorInfo = status + else: + errorObject = self.safe_dict(response, 'errors', {}) + errorKeys = list(errorObject.keys()) + errorsLength = len(errorKeys) + if errorsLength > 0: + errorKey = errorKeys[0] + errorMessageArray = self.safe_value(errorObject, errorKey, []) + errorMessageLength = len(errorMessageArray) + errorInfo = errorMessageArray[0] if (errorMessageLength > 0) else body + self.throw_exactly_matched_exception(self.exceptions['exact'], errorInfo, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], body, feedback) + raise ExchangeError(feedback) + return None diff --git a/ccxt/async_support/woo.py b/ccxt/async_support/woo.py new file mode 100644 index 0000000..2f27403 --- /dev/null +++ b/ccxt/async_support/woo.py @@ -0,0 +1,4048 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.woo import ImplicitAPI +import asyncio +import hashlib +from ccxt.base.types import Account, Any, Balances, Bool, Conversion, Currencies, Currency, DepositAddress, Int, LedgerEntry, Leverage, MarginModification, Market, MarketType, Num, Order, OrderBook, OrderSide, OrderType, Position, Str, Strings, FundingRate, FundingRates, Trade, TradingFeeInterface, TradingFees, Transaction, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import NotSupported +from ccxt.base.errors import OperationFailed +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import OnMaintenance +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class woo(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(woo, self).describe(), { + 'id': 'woo', + 'name': 'WOO X', + 'countries': ['KY'], # Cayman Islands + 'rateLimit': 100, + 'version': 'v1', + 'certified': True, + 'pro': True, + 'hostname': 'woox.io', + 'has': { + 'CORS': None, + 'spot': True, + 'margin': True, + 'swap': True, + 'future': False, + 'option': False, + 'addMargin': True, + 'cancelAllOrders': True, + 'cancelAllOrdersAfter': True, + 'cancelOrder': True, + 'cancelWithdraw': False, # exchange have that endpoint disabled atm, but was once implemented in ccxt per old docs: https://docx.woo.io/wootrade-documents/#cancel-withdraw-request + 'closeAllPositions': False, + 'closePosition': False, + 'createConvertTrade': True, + 'createDepositAddress': False, + 'createMarketBuyOrderWithCost': True, + 'createMarketOrder': False, + 'createMarketOrderWithCost': False, + 'createMarketSellOrderWithCost': True, + 'createOrder': True, + 'createOrderWithTakeProfitAndStopLoss': True, + 'createReduceOnlyOrder': True, + 'createStopLimitOrder': False, + 'createStopLossOrder': True, + 'createStopMarketOrder': False, + 'createStopOrder': False, + 'createTakeProfitOrder': True, + 'createTrailingAmountOrder': True, + 'createTrailingPercentOrder': True, + 'createTriggerOrder': True, + 'fetchAccounts': True, + 'fetchBalance': True, + 'fetchCanceledOrders': False, + 'fetchClosedOrder': False, + 'fetchClosedOrders': True, + 'fetchConvertCurrencies': True, + 'fetchConvertQuote': True, + 'fetchConvertTrade': True, + 'fetchConvertTradeHistory': True, + 'fetchCurrencies': True, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchDepositsWithdrawals': True, + 'fetchFundingHistory': True, + 'fetchFundingInterval': True, + 'fetchFundingIntervals': False, + 'fetchFundingRate': True, + 'fetchFundingRateHistory': True, + 'fetchFundingRates': True, + 'fetchIndexOHLCV': False, + 'fetchLedger': True, + 'fetchLeverage': True, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterestHistory': False, + 'fetchOpenOrder': False, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchOrderTrades': True, + 'fetchPosition': True, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': True, + 'fetchPositionsHistory': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchStatus': True, + 'fetchTicker': False, + 'fetchTickers': False, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': True, + 'fetchTradingFees': True, + 'fetchTransactions': 'emulated', + 'fetchTransfers': True, + 'fetchWithdrawals': True, + 'reduceMargin': False, + 'sandbox': True, + 'setLeverage': True, + 'setMargin': False, + 'setPositionMode': True, + 'transfer': True, + 'withdraw': True, # exchange have that endpoint disabled atm, but was once implemented in ccxt per old docs: https://docx.woo.io/wootrade-documents/#token-withdraw + }, + 'timeframes': { + '1m': '1m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1h', + '4h': '4h', + '12h': '12h', + '1d': '1d', + '1w': '1w', + '1M': '1mon', + '1y': '1y', + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/150730761-1a00e5e0-d28c-480f-9e65-089ce3e6ef3b.jpg', + 'api': { + 'pub': 'https://api-pub.woox.io', + 'public': 'https://api.{hostname}', + 'private': 'https://api.{hostname}', + }, + 'test': { + 'pub': 'https://api-pub.staging.woox.io', + 'public': 'https://api.staging.woox.io', + 'private': 'https://api.staging.woox.io', + }, + 'www': 'https://woox.io/', + 'doc': [ + 'https://docs.woox.io/', + ], + 'fees': [ + 'https://support.woox.io/hc/en-001/articles/4404611795353--Trading-Fees', + ], + 'referral': { + 'url': 'https://woox.io/register?ref=DIJT0CNL', + 'discount': 0.35, + }, + }, + 'api': { + 'v1': { + 'pub': { + 'get': { + 'hist/kline': 10, + 'hist/trades': 10, + }, + }, + 'public': { + 'get': { + 'info': 1, + 'info/{symbol}': 1, + 'system_info': 1, + 'market_trades': 1, + 'token': 1, + 'token_network': 1, + 'funding_rates': 1, + 'funding_rate/{symbol}': 1, + 'funding_rate_history': 1, + 'futures': 1, + 'futures/{symbol}': 1, + 'orderbook/{symbol}': 1, + 'kline': 1, + }, + }, + 'private': { + 'get': { + 'client/token': 1, + 'order/{oid}': 1, + 'client/order/{client_order_id}': 1, + 'orders': 1, + 'client/trade/{tid}': 1, + 'order/{oid}/trades': 1, + 'client/trades': 1, + 'client/hist_trades': 1, + 'staking/yield_history': 1, + 'client/holding': 1, + 'asset/deposit': 10, + 'asset/history': 60, + 'sub_account/all': 60, + 'sub_account/assets': 60, + 'sub_account/asset_detail': 60, + 'sub_account/ip_restriction': 10, + 'asset/main_sub_transfer_history': 30, + 'token_interest': 60, + 'token_interest/{token}': 60, + 'interest/history': 60, + 'interest/repay': 60, + 'funding_fee/history': 30, + 'positions': 3.33, # 30 requests per 10 seconds + 'position/{symbol}': 3.33, + 'client/transaction_history': 60, + 'client/futures_leverage': 60, + }, + 'post': { + 'order': 1, # 10 requests per 1 second per symbol + 'order/cancel_all_after': 1, + 'asset/ltv': 30, + 'asset/internal_withdraw': 30, + 'interest/repay': 60, + 'client/account_mode': 120, + 'client/position_mode': 5, + 'client/leverage': 120, + 'client/futures_leverage': 30, + 'client/isolated_margin': 30, + }, + 'delete': { + 'order': 1, + 'client/order': 1, + 'orders': 1, + 'asset/withdraw': 120, # implemented in ccxt, disabled on the exchange side https://docx.woo.io/wootrade-documents/#cancel-withdraw-request + }, + }, + }, + 'v2': { + 'private': { + 'get': { + 'client/holding': 1, + }, + }, + }, + 'v3': { + 'public': { + 'get': { + 'systemInfo': 1, # 10/1s + 'instruments': 1, # 10/1s + 'token': 1, # 10/1s + 'tokenNetwork': 1, # 10/1s + 'tokenInfo': 1, # 10/1s + 'marketTrades': 1, # 10/1s + 'marketTradesHistory': 1, # 10/1s + 'orderbook': 1, # 10/1s + 'kline': 1, # 10/1s + 'klineHistory': 1, # 10/1s + 'futures': 1, # 10/1s + 'fundingRate': 1, # 10/1s + 'fundingRateHistory': 1, # 10/1s + 'insuranceFund': 1, # 10/1s + }, + }, + 'private': { + 'get': { + 'trade/order': 2, # 5/1s + 'trade/orders': 1, # 10/1s + 'trade/algoOrder': 1, # 10/1s + 'trade/algoOrders': 1, # 10/1s + 'trade/transaction': 1, # 10/1s + 'trade/transactionHistory': 5, # 2/1s + 'trade/tradingFee': 5, # 2/1s + 'account/info': 60, # 10/60s + 'account/tokenConfig': 1, # 10/1s + 'account/symbolConfig': 1, # 10/1s + 'account/subAccounts/all': 60, # 10/60s + 'account/referral/summary': 60, # 10/60s + 'account/referral/rewardHistory': 60, # 10/60s + 'account/credentials': 60, # 10/60s + 'asset/balances': 1, # 10/1s + 'asset/token/history': 60, # 10/60s + 'asset/transfer/history': 30, # 20/60s + 'asset/wallet/history': 60, # 10/60s + 'asset/wallet/deposit': 60, # 10/60s + 'asset/staking/yieldHistory': 60, # 10/60s + 'futures/positions': 3.33, # 30/10s + 'futures/leverage': 60, # 10/60s + 'futures/defaultMarginMode': 60, # 10/60s + 'futures/fundingFee/history': 30, # 20/60s + 'spotMargin/interestRate': 60, # 10/60s + 'spotMargin/interestHistory': 60, # 10/60s + 'spotMargin/maxMargin': 60, # 10/60s + 'algo/order/{oid}': 1, + 'algo/orders': 1, + 'positions': 3.33, + 'buypower': 1, + 'convert/exchangeInfo': 1, + 'convert/assetInfo': 1, + 'convert/rfq': 60, + 'convert/trade': 1, + 'convert/trades': 1, + }, + 'post': { + 'trade/order': 2, # 5/1s + 'trade/algoOrder': 5, # 2/1s + 'trade/cancelAllAfter': 1, # 10/1s + 'account/tradingMode': 120, # 5/60s + 'account/listenKey': 20, # 5/10s + 'asset/transfer': 30, # 20/60s + 'asset/wallet/withdraw': 60, # 10/60s + 'spotMargin/leverage': 120, # 5/60s + 'spotMargin/interestRepay': 60, # 10/60s + 'algo/order': 5, + 'convert/rft': 60, + }, + 'put': { + 'trade/order': 2, # 5/1s + 'trade/algoOrder': 2, # 5/1s + 'futures/leverage': 60, # 10/60s + 'futures/positionMode': 120, # 5/60s + 'order/{oid}': 2, + 'order/client/{client_order_id}': 2, + 'algo/order/{oid}': 2, + 'algo/order/client/{client_order_id}': 2, + }, + 'delete': { + 'trade/order': 1, # 10/1s + 'trade/orders': 1, # 10/1s + 'trade/algoOrder': 1, # 10/1s + 'trade/algoOrders': 1, # 10/1s + 'trade/allOrders': 1, # 10/1s + 'algo/order/{order_id}': 1, + 'algo/orders/pending': 1, + 'algo/orders/pending/{symbol}': 1, + 'orders/pending': 1, + }, + }, + }, + }, + 'fees': { + 'trading': { + 'tierBased': True, + 'percentage': True, + 'maker': self.parse_number('0.0002'), + 'taker': self.parse_number('0.0005'), + }, + }, + 'options': { + 'timeDifference': 0, # the difference between system clock and exchange clock + 'adjustForTimeDifference': False, # controls the adjustment logic upon instantiation + 'sandboxMode': False, + 'createMarketBuyOrderRequiresPrice': True, + # these network aliases require manual mapping here + 'network-aliases-for-tokens': { + 'HT': 'ERC20', + 'OMG': 'ERC20', + 'UATOM': 'ATOM', + 'ZRX': 'ZRX', + }, + 'networks': { + 'TRX': 'TRON', + 'TRC20': 'TRON', + 'ERC20': 'ETH', + 'BEP20': 'BSC', + 'ARB': 'Arbitrum', + }, + 'networksById': { + 'TRX': 'TRC20', + 'TRON': 'TRC20', + }, + # override defaultNetworkCodePriorities for a specific currency + 'defaultNetworkCodeForCurrencies': { + # 'USDT': 'TRC20', + # 'BTC': 'BTC', + }, + 'transfer': { + 'fillResponseFromRequest': True, + }, + 'brokerId': 'bc830de7-50f3-460b-9ee0-f430f83f9dad', + }, + 'features': { + 'default': { + 'sandbox': True, + 'createOrder': { + 'marginMode': True, + 'triggerPrice': True, + 'triggerPriceType': { + 'last': True, + 'mark': True, + 'index': False, + }, + 'triggerDirection': False, + 'stopLossPrice': False, # todo by triggerPrice + 'takeProfitPrice': False, # todo by triggerPrice + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': True, + }, + 'hedged': False, + 'trailing': True, + 'leverage': False, + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': False, + 'iceberg': True, # todo implement + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 500, + 'daysBack': 90, + 'untilDays': 10000, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': True, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 500, + 'trigger': True, + 'trailing': True, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': 500, + 'daysBack': None, + 'untilDays': 100000, + 'trigger': True, + 'trailing': True, + 'symbolRequired': False, + }, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 500, + 'daysBack': None, + 'daysBackCanceled': None, + 'untilDays': 100000, + 'trigger': True, + 'trailing': True, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 1000, + }, + }, + 'spot': { + 'extends': 'default', + }, + 'forSwap': { + 'extends': 'default', + 'createOrder': { + 'hedged': True, + }, + }, + 'swap': { + 'linear': { + 'extends': 'forSwap', + }, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'commonCurrencies': {}, + 'exceptions': { + 'exact': { + '-1000': OperationFailed, # {"code": -1000, "message": "An unknown error occurred while processing the request"} or {"success":false,"code":"-1000","message":"An internal error has occurred. We are unable to process your request. Please try again later."} + '-1001': AuthenticationError, # {"code": -1001, "message": "The api key or secret is in wrong format"} + '-1002': AuthenticationError, # {"code": -1002, "message": "API key or secret is invalid, it may because key have insufficient permission or the key is expired/revoked."} + '-1003': RateLimitExceeded, # {"code": -1003, "message": "Rate limit exceed."} + '-1004': BadRequest, # {"code": -1004, "message": "An unknown parameter was sent."} + '-1005': BadRequest, # {"code": -1005, "message": "Some parameters are in wrong format for api."} + '-1006': BadRequest, # {"code": -1006, "message": "The data is not found in server."} + '-1007': BadRequest, # {"code": -1007, "message": "The data is already exists or your request is duplicated."} + '-1008': InvalidOrder, # {"code": -1008, "message": "The quantity of settlement is too high than you can request."} + '-1009': BadRequest, # {"code": -1009, "message": "Can not request withdrawal settlement, you need to deposit other arrears first."} + '-1012': BadRequest, # {"code": -1012, "message": "Amount is required for buy market orders when margin disabled."} The place/cancel order request is rejected by internal module, it may because the account is in liquidation or other internal errors. Please try again in a few seconds."} + '-1101': InvalidOrder, # {"code": -1101, "message": "The risk exposure for client is too high, it may cause by sending too big order or the leverage is too low. please refer to client info to check the current exposure."} + '-1102': InvalidOrder, # {"code": -1102, "message": "The order value(price * size) is too small."} + '-1103': InvalidOrder, # {"code": -1103, "message": "The order price is not following the tick size rule for the symbol."} + '-1104': InvalidOrder, # {"code": -1104, "message": "The order quantity is not following the step size rule for the symbol."} + '-1105': InvalidOrder, # {"code": -1105, "message": "Price is X% too high or X% too low from the mid price."} + }, + 'broad': { + 'Can not place': ExchangeError, # {"code": -1011, "message": "Can not place/cancel orders, it may because internal network error. Please try again in a few seconds."} + 'maintenance': OnMaintenance, # {"code":"-1011","message":"The system is under maintenance.","success":false} + 'symbol must not be blank': BadRequest, # when sending 'cancelOrder' without symbol [-1005] + 'The token is not supported': BadRequest, # when getting incorrect token's deposit address [-1005] + 'Your order and symbol are not valid or already canceled': BadRequest, # actual response whensending 'cancelOrder' for already canceled id [-1006] + 'Insufficient WOO. Please enable margin trading for leverage trading': BadRequest, # when selling insufficent token [-1012] + }, + }, + 'precisionMode': TICK_SIZE, + }) + + async def fetch_status(self, params={}): + """ + the latest known information on the availability of the exchange API + + https://developer.woox.io/api-reference/endpoint/public_data/systemInfo + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `status structure ` + """ + response = await self.v3PublicGetSystemInfo(params) + # + # { + # "success": True, + # "data": { + # "status": 0, + # "msg": "System is functioning properly.", + # "estimatedEndTime": 1749963600362 + # }, + # "timestamp": 1751442989564 + # } + # + data = self.safe_dict(response, 'data', {}) + status = self.safe_string(data, 'status') + if status is None: + status = 'error' + elif status == '0': + status = 'ok' + else: + status = 'maintenance' + return { + 'status': status, + 'updated': None, + 'eta': None, + 'url': None, + 'info': response, + } + + async def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://developer.woox.io/api-reference/endpoint/public_data/systemInfo + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = await self.v3PublicGetSystemInfo(params) + # + # { + # "success": True, + # "data": { + # "status": 0, + # "msg": "System is functioning properly.", + # "estimatedEndTime": 1749963600362 + # }, + # "timestamp": 1751442989564 + # } + # + return self.safe_integer(response, 'timestamp') + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for woo + + https://developer.woox.io/api-reference/endpoint/public_data/instruments + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + if self.options['adjustForTimeDifference']: + await self.load_time_difference() + response = await self.v3PublicGetInstruments(params) + # + # { + # "success": True, + # "data": { + # "rows": [ + # { + # "symbol": "SPOT_AAVE_USDT", + # "status": "TRADING", + # "baseAsset": "AAVE", + # "baseAssetMultiplier": 1, + # "quoteAsset": "USDT", + # "quoteMin": "0", + # "quoteMax": "100000", + # "quoteTick": "0.01", + # "baseMin": "0.005", + # "baseMax": "5000", + # "baseTick": "0.0001", + # "minNotional": "1", + # "bidCapRatio": "1.1", + # "bidFloorRatio": null, + # "askCapRatio": null, + # "askFloorRatio": "0.9", + # "orderMode": "NORMAL", + # "impactNotional": null, + # "isAllowedRpi": False, + # "tickGranularity": null + # } + # ] + # }, + # "timestamp": 1751512951338 + # } + # + data = self.safe_dict(response, 'data', {}) + rows = self.safe_list(data, 'rows', []) + return self.parse_markets(rows) + + def parse_market(self, market: dict) -> Market: + marketId = self.safe_string(market, 'symbol') + parts = marketId.split('_') + first = self.safe_string(parts, 0) + marketType: MarketType + spot = False + swap = False + if first == 'SPOT': + spot = True + marketType = 'spot' + elif first == 'PERP': + swap = True + marketType = 'swap' + baseId = self.safe_string(parts, 1) + quoteId = self.safe_string(parts, 2) + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + settleId: Str = None + settle: Str = None + symbol = base + '/' + quote + contractSize: Num = None + linear: Bool = None + inverse: Bool = None + margin = True + contract = swap + if contract: + margin = False + settleId = self.safe_string(parts, 2) + settle = self.safe_currency_code(settleId) + symbol = base + '/' + quote + ':' + settle + contractSize = self.parse_number('1') + linear = True + inverse = False + active = self.safe_string(market, 'status') == 'TRADING' + return { + 'id': marketId, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': marketType, + 'spot': spot, + 'margin': margin, + 'swap': swap, + 'future': False, + 'option': False, + 'active': active, + 'contract': contract, + 'linear': linear, + 'inverse': inverse, + 'contractSize': contractSize, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.safe_number(market, 'baseTick'), + 'price': self.safe_number(market, 'quoteTick'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(market, 'baseMin'), + 'max': self.safe_number(market, 'baseMax'), + }, + 'price': { + 'min': self.safe_number(market, 'quoteMin'), + 'max': self.safe_number(market, 'quoteMax'), + }, + 'cost': { + 'min': self.safe_number(market, 'minNotional'), + 'max': None, + }, + }, + 'created': None, + 'info': market, + } + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://developer.woox.io/api-reference/endpoint/public_data/marketTrades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['limit'] = limit + response = await self.v3PublicGetMarketTrades(self.extend(request, params)) + # + # { + # "success": True, + # "data": { + # "rows": [ + # { + # "symbol": "SPOT_BTC_USDT", + # "side": "SELL", + # "source": 0, + # "executedPrice": "108741.01", + # "executedQuantity": "0.02477", + # "executedTimestamp": 1751513940144 + # } + # ] + # }, + # "timestamp": 1751513988543 + # } + # + data = self.safe_dict(response, 'data', {}) + rows = self.safe_list(data, 'rows', []) + return self.parse_trades(rows, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # public/market_trades + # + # { + # "symbol": "SPOT_BTC_USDT", + # "side": "SELL", + # "source": 0, + # "executedPrice": "108741.01", + # "executedQuantity": "0.02477", + # "executedTimestamp": 1751513940144 + # } + # + # fetchOrderTrades, fetchOrder + # + # { + # "id": 1734947821, + # "symbol": "SPOT_LTC_USDT", + # "orderId": 60780383217, + # "executedPrice": 87.86, + # "executedQuantity": 0.1, + # "fee": 0.0001, + # "realizedPnl": null, + # "feeAsset": "LTC", + # "orderTag": "default", + # "side": "BUY", + # "executedTimestamp": "1752055173.630", + # "isMaker": 0 + # } + # + isFromFetchOrder = ('id' in trade) + timestampString = self.safe_string_2(trade, 'executed_timestamp', 'executedTimestamp') + timestamp = None + if timestampString is not None: + if timestampString.find('.') > -1: + timestamp = self.safe_timestamp_2(trade, 'executed_timestamp', 'executedTimestamp') + else: + timestamp = self.safe_integer(trade, 'executedTimestamp') + marketId = self.safe_string(trade, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + price = self.safe_string_2(trade, 'executed_price', 'executedPrice') + amount = self.safe_string_2(trade, 'executed_quantity', 'executedQuantity') + order_id = self.safe_string_2(trade, 'order_id', 'orderId') + fee = self.parse_token_and_fee_temp(trade, ['fee_asset', 'feeAsset'], ['fee']) + feeCost = self.safe_string(fee, 'cost') + if feeCost is not None: + fee['cost'] = feeCost + cost = Precise.string_mul(price, amount) + side = self.safe_string_lower(trade, 'side') + id = self.safe_string(trade, 'id') + takerOrMaker: Str = None + if isFromFetchOrder: + isMaker = self.safe_string_2(trade, 'is_maker', 'isMaker') == '1' + takerOrMaker = 'maker' if isMaker else 'taker' + return self.safe_trade({ + 'id': id, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'side': side, + 'price': price, + 'amount': amount, + 'cost': cost, + 'order': order_id, + 'takerOrMaker': takerOrMaker, + 'type': None, + 'fee': fee, + 'info': trade, + }, market) + + def parse_token_and_fee_temp(self, item, feeTokenKeys, feeAmountKeys): + feeCost = self.safe_string_n(item, feeAmountKeys) + fee = None + if feeCost is not None: + feeCurrencyId = self.safe_string_n(item, feeTokenKeys) + feeCurrencyCode = self.safe_currency_code(feeCurrencyId) + fee = { + 'cost': feeCost, + 'currency': feeCurrencyCode, + } + return fee + + def parse_trading_fee(self, fee: dict, market: Market = None) -> TradingFeeInterface: + marketId = self.safe_string(fee, 'symbol') + symbol = self.safe_symbol(marketId, market) + return { + 'info': fee, + 'symbol': symbol, + 'maker': self.parse_number(Precise.string_div(self.safe_string(fee, 'makerFee'), '100')), + 'taker': self.parse_number(Precise.string_div(self.safe_string(fee, 'takerFee'), '100')), + 'percentage': None, + 'tierBased': None, + } + + async def fetch_trading_fee(self, symbol: str, params={}) -> TradingFeeInterface: + """ + fetch the trading fees for a market + + https://developer.woox.io/api-reference/endpoint/trading/get_tradingFee + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.portfolioMargin]: set to True if you would like to fetch trading fees in a portfolio margin account + :param str [params.subType]: "linear" or "inverse" + :returns dict: a `fee structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = await self.v3PrivateGetTradeTradingFee(self.extend(request, params)) + # + # { + # "success": True, + # "data": { + # "symbol": "SPOT_BTC_USDT", + # "takerFee": "10", + # "makerFee": "8" + # }, + # "timestamp": 1751858977368 + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_trading_fee(data, market) + + async def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://developer.woox.io/api-reference/endpoint/account/get_account_info + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + await self.load_markets() + response = await self.v3PrivateGetAccountInfo(params) + # + # { + # "success": True, + # "data": { + # "applicationId": "251bf5c4-f3c8-4544-bb8b-80001007c3c0", + # "account": "carlos_jose_lima@yahoo.com", + # "alias": "carlos_jose_lima@yahoo.com", + # "otpauth": True, + # "accountMode": "FUTURES", + # "positionMode": "ONE_WAY", + # "leverage": 0, + # "makerFeeRate": 0, + # "takerFeeRate": 0, + # "marginRatio": "10", + # "openMarginRatio": "10", + # "initialMarginRatio": "10", + # "maintenanceMarginRatio": "0.03", + # "totalCollateral": "165.55629469", + # "freeCollateral": "165.55629469", + # "totalAccountValue": "167.32418611", + # "totalTradingValue": "167.32418611", + # "totalVaultValue": "0", + # "totalStakingValue": "0", + # "totalLaunchpadValue": "0", + # "totalEarnValue": "0", + # "referrerID": null, + # "accountType": "Main" + # }, + # "timestamp": 1752062807915 + # } + # + data = self.safe_dict(response, 'data', {}) + maker = self.safe_string(data, 'makerFeeRate') + taker = self.safe_string(data, 'takerFeeRate') + result: dict = {} + for i in range(0, len(self.symbols)): + symbol = self.symbols[i] + result[symbol] = { + 'info': response, + 'symbol': symbol, + 'maker': self.parse_number(Precise.string_div(maker, '10000')), + 'taker': self.parse_number(Precise.string_div(taker, '10000')), + 'percentage': True, + 'tierBased': True, + } + return result + + async def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://docs.woox.io/#available-token-public + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + result: dict = {} + tokenResponsePromise = self.v1PublicGetToken(params) + # + # { + # "rows": [ + # { + # "token": "ETH_USDT", + # "fullname": "Tether", + # "network": "ETH", + # "decimals": "6", + # "delisted": False, + # "balance_token": "USDT", + # "created_time": "1710123398", + # "updated_time": "1746528481", + # "can_collateral": True, + # "can_short": True + # }, + # { + # "token": "BSC_USDT", + # "fullname": "Tether", + # "network": "BSC", + # "decimals": "18", + # "delisted": False, + # "balance_token": "USDT", + # "created_time": "1710123395", + # "updated_time": "1746528601", + # "can_collateral": True, + # "can_short": True + # }, + # { + # "token": "ALGO", + # "fullname": "Algorand", + # "network": "ALGO", + # "decimals": "6", + # "delisted": False, + # "balance_token": "ALGO", + # "created_time": "1710123394", + # "updated_time": "1723087518", + # "can_collateral": True, + # "can_short": True + # }, + # ... + # ], + # "success": True + # } + # + # only make one request for currrencies... + tokenNetworkResponsePromise = self.v1PublicGetTokenNetwork(params) + # + # { + # "rows": [ + # { + # "protocol": "ERC20", + # "network": "ETH", + # "token": "USDT", + # "name": "Ethereum(ERC20)", + # "minimum_withdrawal": "10.00000000", + # "withdrawal_fee": "2.00000000", + # "allow_deposit": "1", + # "allow_withdraw": "1" + # }, + # { + # "protocol": "TRC20", + # "network": "TRX", + # "token": "USDT", + # "name": "Tron(TRC20)", + # "minimum_withdrawal": "10.00000000", + # "withdrawal_fee": "4.50000000", + # "allow_deposit": "1", + # "allow_withdraw": "1" + # }, + # ... + # ], + # "success": True + # } + # + tokenResponse, tokenNetworkResponse = await asyncio.gather(*[tokenResponsePromise, tokenNetworkResponsePromise]) + tokenRows = self.safe_list(tokenResponse, 'rows', []) + tokenNetworkRows = self.safe_list(tokenNetworkResponse, 'rows', []) + networksById = self.group_by(tokenNetworkRows, 'token') + tokensById = self.group_by(tokenRows, 'balance_token') + currencyIds = list(tokensById.keys()) + for i in range(0, len(currencyIds)): + currencyId = currencyIds[i] + code = self.safe_currency_code(currencyId) + tokensByNetworkId = self.index_by(tokensById[currencyId], 'network') + chainsByNetworkId = self.index_by(networksById[currencyId], 'network') + keys = list(chainsByNetworkId.keys()) + resultingNetworks: dict = {} + for j in range(0, len(keys)): + networkId = keys[j] + tokenEntry = self.safe_dict(tokensByNetworkId, networkId, {}) + networkEntry = self.safe_dict(chainsByNetworkId, networkId, {}) + networkCode = self.network_id_to_code(networkId, code) + specialNetworkId = self.safe_string(tokenEntry, 'token') + resultingNetworks[networkCode] = { + 'id': networkId, + 'currencyNetworkId': specialNetworkId, # exchange uses special crrency-ids(coin + network junction) + 'network': networkCode, + 'active': None, + 'deposit': self.safe_string(networkEntry, 'allow_deposit') == '1', + 'withdraw': self.safe_string(networkEntry, 'allow_withdraw') == '1', + 'fee': self.safe_number(networkEntry, 'withdrawal_fee'), + 'precision': self.parse_number(self.parse_precision(self.safe_string(tokenEntry, 'decimals'))), + 'limits': { + 'withdraw': { + 'min': self.safe_number(networkEntry, 'minimum_withdrawal'), + 'max': None, + }, + 'deposit': { + 'min': None, + 'max': None, + }, + }, + 'info': [networkEntry, tokenEntry], + } + result[code] = self.safe_currency_structure({ + 'id': currencyId, + 'name': None, + 'code': code, + 'precision': None, + 'active': None, + 'fee': None, + 'networks': resultingNetworks, + 'deposit': None, + 'withdraw': None, + 'type': 'crypto', + 'limits': { + 'deposit': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + }, + 'info': [tokensByNetworkId, chainsByNetworkId], + }) + return result + + async def create_market_buy_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market buy order by providing the symbol and cost + + https://docs.woox.io/#send-order + + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' createMarketBuyOrderWithCost() supports spot orders only') + return await self.create_order(symbol, 'market', 'buy', cost, 1, params) + + async def create_market_sell_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market sell order by providing the symbol and cost + + https://docs.woox.io/#send-order + + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' createMarketSellOrderWithCost() supports spot orders only') + return await self.create_order(symbol, 'market', 'sell', cost, 1, params) + + async def create_trailing_amount_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, trailingAmount: Num = None, trailingTriggerPrice: Num = None, params={}) -> Order: + """ + create a trailing order by providing the symbol, type, side, amount, price and trailingAmount + + https://docs.woox.io/#send-algo-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency, or number of contracts + :param float [price]: the price for the order to be filled at, in units of the quote currency, ignored in market orders + :param float trailingAmount: the quote amount to trail away from the current market price + :param float trailingTriggerPrice: the price to activate a trailing order, default uses the price argument + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + if trailingAmount is None: + raise ArgumentsRequired(self.id + ' createTrailingAmountOrder() requires a trailingAmount argument') + if trailingTriggerPrice is None: + raise ArgumentsRequired(self.id + ' createTrailingAmountOrder() requires a trailingTriggerPrice argument') + params['trailingAmount'] = trailingAmount + params['trailingTriggerPrice'] = trailingTriggerPrice + return await self.create_order(symbol, type, side, amount, price, params) + + async def create_trailing_percent_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, trailingPercent: Num = None, trailingTriggerPrice: Num = None, params={}) -> Order: + """ + create a trailing order by providing the symbol, type, side, amount, price and trailingPercent + + https://docs.woox.io/#send-algo-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency, or number of contracts + :param float [price]: the price for the order to be filled at, in units of the quote currency, ignored in market orders + :param float trailingPercent: the percent to trail away from the current market price + :param float trailingTriggerPrice: the price to activate a trailing order, default uses the price argument + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + if trailingPercent is None: + raise ArgumentsRequired(self.id + ' createTrailingPercentOrder() requires a trailingPercent argument') + if trailingTriggerPrice is None: + raise ArgumentsRequired(self.id + ' createTrailingPercentOrder() requires a trailingTriggerPrice argument') + params['trailingPercent'] = trailingPercent + params['trailingTriggerPrice'] = trailingTriggerPrice + return await self.create_order(symbol, type, side, amount, price, params) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://developer.woox.io/api-reference/endpoint/trading/post_order + https://developer.woox.io/api-reference/endpoint/trading/post_algo_order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: *for swap markets only* 'cross' or 'isolated', default 'cross' + :param float [params.triggerPrice]: The price a trigger order is triggered at + :param dict [params.takeProfit]: *takeProfit object in params* containing the triggerPrice at which the attached take profit order will be triggered(perpetual swap markets only) + :param float [params.takeProfit.triggerPrice]: take profit trigger price + :param dict [params.stopLoss]: *stopLoss object in params* containing the triggerPrice at which the attached stop loss order will be triggered(perpetual swap markets only) + :param float [params.stopLoss.triggerPrice]: stop loss trigger price + :param float [params.algoType]: 'STOP' or 'TRAILING_STOP' or 'OCO' or 'CLOSE_POSITION' + :param float [params.cost]: *spot market buy only* the quote quantity that can be used alternative for the amount + :param str [params.trailingAmount]: the quote amount to trail away from the current market price + :param str [params.trailingPercent]: the percent to trail away from the current market price + :param str [params.trailingTriggerPrice]: the price to trigger a trailing order, default uses the price argument + :param str [params.position_side]: 'SHORT' or 'LONG' - if position mode is HEDGE_MODE and the trading involves futures, then is required, otherwise self parameter is not required + :returns dict: an `order structure ` + """ + reduceOnly = self.safe_bool_2(params, 'reduceOnly', 'reduce_only') + params = self.omit(params, ['reduceOnly', 'reduce_only']) + orderType = type.upper() + await self.load_markets() + market = self.market(symbol) + orderSide = side.upper() + request: dict = { + 'symbol': market['id'], + 'side': orderSide, + } + marginMode: Str = None + marginMode, params = self.handle_margin_mode_and_params('createOrder', params) + if marginMode is not None: + request['marginMode'] = self.encode_margin_mode(marginMode) + triggerPrice = self.safe_string_2(params, 'triggerPrice', 'stopPrice') + stopLoss = self.safe_value(params, 'stopLoss') + takeProfit = self.safe_value(params, 'takeProfit') + algoType = self.safe_string(params, 'algoType') + trailingTriggerPrice = self.safe_string_2(params, 'trailingTriggerPrice', 'activatedPrice', self.number_to_string(price)) + trailingAmount = self.safe_string_2(params, 'trailingAmount', 'callbackValue') + trailingPercent = self.safe_string_2(params, 'trailingPercent', 'callbackRate') + isTrailingAmountOrder = trailingAmount is not None + isTrailingPercentOrder = trailingPercent is not None + isTrailing = isTrailingAmountOrder or isTrailingPercentOrder + isConditional = isTrailing or triggerPrice is not None or stopLoss is not None or takeProfit is not None or (self.safe_value(params, 'childOrders') is not None) + isMarket = orderType == 'MARKET' + timeInForce = self.safe_string_lower(params, 'timeInForce') + postOnly = self.is_post_only(isMarket, None, params) + clientOrderIdKey = 'clientAlgoOrderId' if isConditional else 'clientOrderId' + request['type'] = orderType # LIMIT/MARKET/IOC/FOK/POST_ONLY/ASK/BID + if not isConditional: + if postOnly: + request['type'] = 'POST_ONLY' + elif timeInForce == 'fok': + request['type'] = 'FOK' + elif timeInForce == 'ioc': + request['type'] = 'IOC' + if reduceOnly: + request['reduceOnly'] = reduceOnly + if not isMarket and price is not None: + request['price'] = self.price_to_precision(symbol, price) + if isMarket and not isConditional: + # for market buy it requires the amount of quote currency to spend + cost = self.safe_string_n(params, ['cost', 'order_amount', 'orderAmount']) + params = self.omit(params, ['cost', 'order_amount', 'orderAmount']) + isPriceProvided = price is not None + if market['spot'] and (isPriceProvided or (cost is not None)): + quoteAmount = None + if cost is not None: + quoteAmount = self.cost_to_precision(symbol, cost) + else: + amountString = self.number_to_string(amount) + priceString = self.number_to_string(price) + costRequest = Precise.string_mul(amountString, priceString) + quoteAmount = self.cost_to_precision(symbol, costRequest) + request['amount'] = quoteAmount + else: + request['quantity'] = self.amount_to_precision(symbol, amount) + elif algoType != 'POSITIONAL_TP_SL': + request['quantity'] = self.amount_to_precision(symbol, amount) + clientOrderId = self.safe_string_n(params, ['clOrdID', 'clientOrderId', 'client_order_id']) + if clientOrderId is not None: + request[clientOrderIdKey] = clientOrderId + if isTrailing: + if trailingTriggerPrice is None: + raise ArgumentsRequired(self.id + ' createOrder() requires a trailingTriggerPrice parameter for trailing orders') + request['activatedPrice'] = self.price_to_precision(symbol, trailingTriggerPrice) + request['algoType'] = 'TRAILING_STOP' + if isTrailingAmountOrder: + request['callbackValue'] = trailingAmount + elif isTrailingPercentOrder: + convertedTrailingPercent = Precise.string_div(trailingPercent, '100') + request['callbackRate'] = convertedTrailingPercent + elif triggerPrice is not None: + if algoType != 'TRAILING_STOP': + request['triggerPrice'] = self.price_to_precision(symbol, triggerPrice) + request['algoType'] = 'STOP' + elif (stopLoss is not None) or (takeProfit is not None): + request['algoType'] = 'BRACKET' + outterOrder: dict = { + 'symbol': market['id'], + 'reduceOnly': False, + 'algoType': 'POSITIONAL_TP_SL', + 'childOrders': [], + } + childOrders = outterOrder['childOrders'] + closeSide = 'SELL' if (orderSide == 'BUY') else 'BUY' + if stopLoss is not None: + stopLossPrice = self.safe_string(stopLoss, 'triggerPrice', stopLoss) + stopLossOrder: dict = { + 'side': closeSide, + 'algoType': 'STOP_LOSS', + 'triggerPrice': self.price_to_precision(symbol, stopLossPrice), + 'type': 'CLOSE_POSITION', + 'reduceOnly': True, + } + childOrders.append(stopLossOrder) + if takeProfit is not None: + takeProfitPrice = self.safe_string(takeProfit, 'triggerPrice', takeProfit) + takeProfitOrder: dict = { + 'side': closeSide, + 'algoType': 'TAKE_PROFIT', + 'triggerPrice': self.price_to_precision(symbol, takeProfitPrice), + 'type': 'CLOSE_POSITION', + 'reduceOnly': True, + } + childOrders.append(takeProfitOrder) + request['childOrders'] = [outterOrder] + params = self.omit(params, ['clOrdID', 'clientOrderId', 'client_order_id', 'postOnly', 'timeInForce', 'stopPrice', 'triggerPrice', 'stopLoss', 'takeProfit', 'trailingPercent', 'trailingAmount', 'trailingTriggerPrice']) + response = None + if isConditional: + response = await self.v3PrivatePostTradeAlgoOrder(self.extend(request, params)) + # + # { + # "success": True, + # "data": { + # "rows": [ + # { + # "orderId": "1578938", + # "clientOrderId": "0", + # "algoType": "STOP_LOSS", + # "quantity": "0.1" + # } + # ] + # }, + # "timestamp": "1686149372216" + # } + # + else: + response = await self.v3PrivatePostTradeOrder(self.extend(request, params)) + # + # { + # "success": True, + # "data": { + # "orderId": 60667653330, + # "clientOrderId": 0, + # "type": "LIMIT", + # "price": 60, + # "quantity": 0.1, + # "amount": null, + # "bidAskLevel": null + # }, + # "timestamp": 1751871779855 + # } + # + data = self.safe_dict(response, 'data', {}) + data = self.safe_dict(self.safe_list(data, 'rows'), 0, data) + data['timestamp'] = self.safe_string(response, 'timestamp') + return self.parse_order(data, market) + + def encode_margin_mode(self, mode): + modes = { + 'cross': 'CROSS', + 'isolated': 'ISOLATED', + } + return self.safe_string(modes, mode, mode) + + async def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + """ + edit a trade order + + https://docs.woox.io/#edit-order + https://docs.woox.io/#edit-order-by-client_order_id + https://docs.woox.io/#edit-algo-order + https://docs.woox.io/#edit-algo-order-by-client_order_id + + :param str id: order id + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: The price a trigger order is triggered at + :param float [params.stopLossPrice]: price to trigger stop-loss orders + :param float [params.takeProfitPrice]: price to trigger take-profit orders + :param str [params.trailingAmount]: the quote amount to trail away from the current market price + :param str [params.trailingPercent]: the percent to trail away from the current market price + :param str [params.trailingTriggerPrice]: the price to trigger a trailing order, default uses the price argument + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + # 'quantity': self.amount_to_precision(symbol, amount), + # 'price': self.price_to_precision(symbol, price), + } + if price is not None: + request['price'] = self.price_to_precision(symbol, price) + if amount is not None: + request['quantity'] = self.amount_to_precision(symbol, amount) + clientOrderIdUnified = self.safe_string_2(params, 'clOrdID', 'clientOrderId') + clientOrderIdExchangeSpecific = self.safe_string(params, 'client_order_id', clientOrderIdUnified) + isByClientOrder = clientOrderIdExchangeSpecific is not None + triggerPrice = self.safe_number_n(params, ['triggerPrice', 'stopPrice', 'takeProfitPrice', 'stopLossPrice']) + if triggerPrice is not None: + request['triggerPrice'] = self.price_to_precision(symbol, triggerPrice) + trailingTriggerPrice = self.safe_string_2(params, 'trailingTriggerPrice', 'activatedPrice', self.number_to_string(price)) + trailingAmount = self.safe_string_2(params, 'trailingAmount', 'callbackValue') + trailingPercent = self.safe_string_2(params, 'trailingPercent', 'callbackRate') + isTrailingAmountOrder = trailingAmount is not None + isTrailingPercentOrder = trailingPercent is not None + isTrailing = isTrailingAmountOrder or isTrailingPercentOrder + if isTrailing: + if trailingTriggerPrice is not None: + request['activatedPrice'] = self.price_to_precision(symbol, trailingTriggerPrice) + if isTrailingAmountOrder: + request['callbackValue'] = trailingAmount + elif isTrailingPercentOrder: + convertedTrailingPercent = Precise.string_div(trailingPercent, '100') + request['callbackRate'] = convertedTrailingPercent + params = self.omit(params, ['clOrdID', 'clientOrderId', 'client_order_id', 'stopPrice', 'triggerPrice', 'takeProfitPrice', 'stopLossPrice', 'trailingTriggerPrice', 'trailingAmount', 'trailingPercent']) + isConditional = isTrailing or (triggerPrice is not None) or (self.safe_value(params, 'childOrders') is not None) + response = None + if isByClientOrder: + request['client_order_id'] = clientOrderIdExchangeSpecific + if isConditional: + response = await self.v3PrivatePutAlgoOrderClientClientOrderId(self.extend(request, params)) + else: + response = await self.v3PrivatePutOrderClientClientOrderId(self.extend(request, params)) + else: + request['oid'] = id + if isConditional: + response = await self.v3PrivatePutAlgoOrderOid(self.extend(request, params)) + else: + response = await self.v3PrivatePutOrderOid(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "status": "string", + # "success": True + # }, + # "message": "string", + # "success": True, + # "timestamp": 0 + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data, market) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + + https://developer.woox.io/api-reference/endpoint/trading/cancel_order + https://developer.woox.io/api-reference/endpoint/trading/cancel_algo_order + + cancels an open order + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: whether the order is a trigger/algo order + :returns dict: An `order structure ` + """ + isTrigger = self.safe_bool_2(params, 'trigger', 'stop', False) + params = self.omit(params, ['trigger', 'stop']) + if not isTrigger and (symbol is None): + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + await self.load_markets() + market: Market = None + if symbol is not None: + market = self.market(symbol) + request: dict = {} + clientOrderIdUnified = self.safe_string_2(params, 'clOrdID', 'clientOrderId') + clientOrderIdExchangeSpecific = self.safe_string(params, 'client_order_id', clientOrderIdUnified) + params = self.omit(params, ['clOrdID', 'clientOrderId', 'client_order_id']) + isByClientOrder = clientOrderIdExchangeSpecific is not None + response = None + if isTrigger: + if isByClientOrder: + request['clientAlgoOrderId'] = clientOrderIdExchangeSpecific + else: + request['algoOrderId'] = id + response = await self.v3PrivateDeleteTradeAlgoOrder(self.extend(request, params)) + else: + request['symbol'] = market['id'] + if isByClientOrder: + request['clientOrderId'] = clientOrderIdExchangeSpecific + else: + request['orderId'] = id + response = await self.v3PrivateDeleteTradeOrder(self.extend(request, params)) + # + # { + # "success": True, + # "data": { + # "status": "CANCEL_SENT" + # }, + # "timestamp": 1751940315838 + # } + # + data = self.safe_dict(response, 'data', {}) + data['timestamp'] = self.safe_string(response, 'timestamp') + if isByClientOrder: + data['clientOrderId'] = clientOrderIdExchangeSpecific + else: + data['orderId'] = id + return self.parse_order(data, market) + + async def cancel_all_orders(self, symbol: Str = None, params={}): + """ + + https://developer.woox.io/api-reference/endpoint/trading/cancel_all_order + https://developer.woox.io/api-reference/endpoint/trading/cancel_algo_orders + + cancel all open orders in a market + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: whether the order is a trigger/algo order + :returns dict: an list of `order structures ` + """ + await self.load_markets() + trigger = self.safe_bool_2(params, 'stop', 'trigger') + params = self.omit(params, ['stop', 'trigger']) + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + response = None + if trigger: + response = await self.v3PrivateDeleteTradeAlgoOrders(params) + else: + response = await self.v3PrivateDeleteTradeOrders(self.extend(request, params)) + # + # { + # "success": True, + # "data": { + # "status": "CANCEL_ALL_SENT" + # }, + # "timestamp": 1751941988134 + # } + # + data = self.safe_dict(response, 'data', {}) + return [self.safe_order({'info': data})] + + async def cancel_all_orders_after(self, timeout: Int, params={}): + """ + dead man's switch, cancel all orders after the given timeout + + https://developer.woox.io/api-reference/endpoint/trading/cancel_all_after + + :param number timeout: time in milliseconds, 0 represents cancel the timer + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: the api result + """ + await self.load_markets() + request: dict = { + 'triggerAfter': min(timeout, 900000) if (timeout > 0) else 0, + } + response = await self.v3PrivatePostTradeCancelAllAfter(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 123, + # "data": { + # "expectedTriggerTime": 123 + # } + # } + # + return response + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + + https://developer.woox.io/api-reference/endpoint/trading/get_order + https://developer.woox.io/api-reference/endpoint/trading/get_algo_order + + fetches information on an order made by the user + :param str id: the order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: whether the order is a trigger/algo order + :returns dict: An `order structure ` + """ + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + trigger = self.safe_bool_2(params, 'stop', 'trigger') + params = self.omit(params, ['stop', 'trigger']) + request: dict = {} + clientOrderId = self.safe_string_2(params, 'clOrdID', 'clientOrderId') + response = None + if trigger: + if clientOrderId is not None: + request['clientAlgoOrderId'] = id + else: + request['algoOrderId'] = id + response = await self.v3PrivateGetTradeAlgoOrder(self.extend(request, params)) + # + # { + # "success": True, + # "data": { + # "algoOrderId": 10399260, + # "clientAlgoOrderId": 0, + # "rootAlgoOrderId": 10399260, + # "parentAlgoOrderId": 0, + # "symbol": "SPOT_LTC_USDT", + # "algoOrderTag": "default", + # "algoType": "TAKE_PROFIT", + # "side": "BUY", + # "quantity": 0.1, + # "isTriggered": False, + # "triggerPrice": 65, + # "triggerStatus": "USELESS", + # "type": "LIMIT", + # "rootAlgoStatus": "NEW", + # "algoStatus": "NEW", + # "triggerPriceType": "MARKET_PRICE", + # "price": 60, + # "triggerTime": "0", + # "totalExecutedQuantity": 0, + # "visibleQuantity": 0.1, + # "averageExecutedPrice": 0, + # "totalFee": 0, + # "feeAsset": "", + # "totalRebate": 0, + # "rebateAsset": "", + # "reduceOnly": False, + # "createdTime": "1752049747.732", + # "updatedTime": "1752049747.732", + # "positionSide": "BOTH" + # }, + # "timestamp": 1752049767550 + # } + # + else: + if clientOrderId is not None: + request['clientOrderId'] = clientOrderId + else: + request['orderId'] = id + response = await self.v3PrivateGetTradeOrder(self.extend(request, params)) + # + # { + # "success": True, + # "data": { + # "orderId": 60780315704, + # "clientOrderId": 0, + # "symbol": "SPOT_LTC_USDT", + # "orderTag": "default", + # "side": "BUY", + # "quantity": 0.1, + # "amount": null, + # "type": "LIMIT", + # "status": "NEW", + # "price": 60, + # "executed": 0, + # "visible": 0.1, + # "averageExecutedPrice": 0, + # "totalFee": 0, + # "feeAsset": "LTC", + # "totalRebate": 0, + # "rebateAsset": "USDT", + # "reduceOnly": False, + # "createdTime": "1752049062.496", + # "realizedPnl": null, + # "positionSide": "BOTH", + # "bidAskLevel": null + # }, + # "timestamp": 1752049393466 + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data, market) + + async def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://developer.woox.io/api-reference/endpoint/trading/get_orders + https://developer.woox.io/api-reference/endpoint/trading/get_algo_orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: whether the order is a trigger/algo order + :param boolean [params.isTriggered]: whether the order has been triggered(False by default) + :param str [params.side]: 'buy' or 'sell' + :param boolean [params.paginate]: set to True if you want to fetch orders with pagination + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOrders', 'paginate') + if paginate: + return await self.fetch_paginated_call_incremental('fetchOrders', symbol, since, limit, params, 'page', 500) + request: dict = {} + market: Market = None + trigger = self.safe_bool_2(params, 'stop', 'trigger') + params = self.omit(params, ['stop', 'trigger']) + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if since is not None: + request['startTime'] = since + until = self.safe_integer(params, 'until') # unified in milliseconds + params = self.omit(params, ['until']) + if until is not None: + request['endTime'] = until + if limit is not None: + request['size'] = min(limit, 500) + response = None + if trigger: + response = await self.v3PrivateGetTradeAlgoOrders(self.extend(request, params)) + # + # { + # "success": True, + # "data": { + # "rows": [ + # { + # "algoOrderId": 10399260, + # "clientAlgoOrderId": 0, + # "rootAlgoOrderId": 10399260, + # "parentAlgoOrderId": 0, + # "symbol": "SPOT_LTC_USDT", + # "algoOrderTag": "default", + # "algoType": "TAKE_PROFIT", + # "side": "BUY", + # "quantity": 0.1, + # "isTriggered": False, + # "triggerPrice": 65, + # "triggerStatus": "USELESS", + # "type": "LIMIT", + # "rootAlgoStatus": "NEW", + # "algoStatus": "NEW", + # "triggerPriceType": "MARKET_PRICE", + # "price": 60, + # "triggerTime": "0", + # "totalExecutedQuantity": 0, + # "visibleQuantity": 0.1, + # "averageExecutedPrice": 0, + # "totalFee": 0, + # "feeAsset": "", + # "totalRebate": 0, + # "rebateAsset": "", + # "reduceOnly": False, + # "createdTime": "1752049747.730", + # "updatedTime": "1752049747.730", + # "positionSide": "BOTH" + # } + # ], + # "meta": { + # "total": 7, + # "recordsPerPage": 1, + # "currentPage": 1 + # } + # }, + # "timestamp": 1752053127448 + # } + # + else: + response = await self.v3PrivateGetTradeOrders(self.extend(request, params)) + # + # { + # "success": True, + # "data": { + # "rows": [ + # { + # "orderId": 60780315704, + # "clientOrderId": 0, + # "symbol": "SPOT_LTC_USDT", + # "orderTag": "default", + # "side": "BUY", + # "quantity": 0.1, + # "amount": null, + # "type": "LIMIT", + # "status": "NEW", + # "price": 60, + # "executed": 0, + # "visible": 0.1, + # "averageExecutedPrice": 0, + # "totalFee": 0, + # "feeAsset": "LTC", + # "totalRebate": 0, + # "rebateAsset": "USDT", + # "reduceOnly": False, + # "createdTime": "1752049062.496", + # "realizedPnl": null, + # "positionSide": "BOTH", + # "bidAskLevel": null + # } + # ], + # "meta": { + # "total": 11, + # "recordsPerPage": 1, + # "currentPage": 1 + # } + # }, + # "timestamp": 1752053061236 + # } + # + data = self.safe_value(response, 'data', {}) + orders = self.safe_list(data, 'rows', []) + return self.parse_orders(orders, market, since, limit) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://developer.woox.io/api-reference/endpoint/trading/get_orders + https://developer.woox.io/api-reference/endpoint/trading/get_algo_orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: whether the order is a trigger/algo order + :param boolean [params.isTriggered]: whether the order has been triggered(False by default) + :param str [params.side]: 'buy' or 'sell' + :param boolean [params.trailing]: set to True if you want to fetch trailing orders + :param boolean [params.paginate]: set to True if you want to fetch orders with pagination + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + extendedParams = self.extend(params, {'status': 'INCOMPLETE'}) + return await self.fetch_orders(symbol, since, limit, extendedParams) + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://developer.woox.io/api-reference/endpoint/trading/get_orders + https://developer.woox.io/api-reference/endpoint/trading/get_algo_orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: whether the order is a trigger/algo order + :param boolean [params.isTriggered]: whether the order has been triggered(False by default) + :param str [params.side]: 'buy' or 'sell' + :param boolean [params.trailing]: set to True if you want to fetch trailing orders + :param boolean [params.paginate]: set to True if you want to fetch orders with pagination + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + extendedParams = self.extend(params, {'status': 'COMPLETED'}) + return await self.fetch_orders(symbol, since, limit, extendedParams) + + def parse_time_in_force(self, timeInForce: Str): + timeInForces: dict = { + 'ioc': 'IOC', + 'fok': 'FOK', + 'post_only': 'PO', + } + return self.safe_string(timeInForces, timeInForce, None) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder + # { + # "orderId": 60667653330, + # "clientOrderId": 0, + # "type": "LIMIT", + # "price": 60, + # "quantity": 0.1, + # "amount": null, + # "bidAskLevel": null, + # "timestamp": 1751871779855 + # } + # + # createOrder - algo + # { + # "orderId": "1578938", + # "clientOrderId": "0", + # "algoType": "STOP_LOSS", + # "quantity": "0.1", + # "timestamp": "1686149372216" + # } + # + # fetchOrder + # { + # "orderId": 60780315704, + # "clientOrderId": 0, + # "symbol": "SPOT_LTC_USDT", + # "orderTag": "default", + # "side": "BUY", + # "quantity": 0.1, + # "amount": null, + # "type": "LIMIT", + # "status": "NEW", + # "price": 60, + # "executed": 0, + # "visible": 0.1, + # "averageExecutedPrice": 0, + # "totalFee": 0, + # "feeAsset": "LTC", + # "totalRebate": 0, + # "rebateAsset": "USDT", + # "reduceOnly": False, + # "createdTime": "1752049062.496", + # "realizedPnl": null, + # "positionSide": "BOTH", + # "bidAskLevel": null + # } + # + # fetchOrder - algo + # { + # "algoOrderId": 10399260, + # "clientAlgoOrderId": 0, + # "rootAlgoOrderId": 10399260, + # "parentAlgoOrderId": 0, + # "symbol": "SPOT_LTC_USDT", + # "algoOrderTag": "default", + # "algoType": "TAKE_PROFIT", + # "side": "BUY", + # "quantity": 0.1, + # "isTriggered": False, + # "triggerPrice": 65, + # "triggerStatus": "USELESS", + # "type": "LIMIT", + # "rootAlgoStatus": "NEW", + # "algoStatus": "NEW", + # "triggerPriceType": "MARKET_PRICE", + # "price": 60, + # "triggerTime": "0", + # "totalExecutedQuantity": 0, + # "visibleQuantity": 0.1, + # "averageExecutedPrice": 0, + # "totalFee": 0, + # "feeAsset": "", + # "totalRebate": 0, + # "rebateAsset": "", + # "reduceOnly": False, + # "createdTime": "1752049747.732", + # "updatedTime": "1752049747.732", + # "positionSide": "BOTH" + # } + # + timestamp = None + timestrampString = self.safe_string(order, 'createdTime') + if timestrampString is not None: + if timestrampString.find('.') >= 0: + timestamp = self.safe_timestamp(order, 'createdTime') # algo orders + else: + timestamp = self.safe_integer(order, 'createdTime') # regular orders + if timestamp is None: + timestamp = self.safe_integer(order, 'timestamp') + orderId = self.safe_string_2(order, 'orderId', 'algoOrderId') + clientOrderId = self.omit_zero(self.safe_string_2(order, 'clientOrderId', 'clientAlgoOrderId')) # Somehow, self always returns 0 for limit order + marketId = self.safe_string(order, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + price = self.safe_string(order, 'price') + amount = self.safe_string(order, 'quantity') # This is base amount + cost = self.safe_string(order, 'amount') # This is quote amount + orderType = self.safe_string_lower(order, 'type') + status = self.safe_value_2(order, 'status', 'algoStatus') + side = self.safe_string_lower(order, 'side') + filled = self.omit_zero(self.safe_value_2(order, 'executed', 'totalExecutedQuantity')) + average = self.omit_zero(self.safe_string(order, 'averageExecutedPrice')) + # remaining = Precise.string_sub(cost, filled) + fee = self.safe_number(order, 'totalFee') + feeCurrency = self.safe_string(order, 'feeAsset') + triggerPrice = self.safe_number(order, 'triggerPrice') + lastUpdateTimestampString = self.safe_string(order, 'updatedTime') + lastUpdateTimestamp = None + if lastUpdateTimestampString is not None: + if lastUpdateTimestampString.find('.') >= 0: + lastUpdateTimestamp = self.safe_timestamp(order, 'updatedTime') # algo orders + else: + lastUpdateTimestamp = self.safe_integer(order, 'updatedTime') # regular orders + return self.safe_order({ + 'id': orderId, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'lastUpdateTimestamp': lastUpdateTimestamp, + 'status': self.parse_order_status(status), + 'symbol': symbol, + 'type': orderType, + 'timeInForce': self.parse_time_in_force(orderType), + 'postOnly': None, # TO_DO + 'reduceOnly': self.safe_bool(order, 'reduceOnly'), + 'side': side, + 'price': price, + 'triggerPrice': triggerPrice, + 'takeProfitPrice': None, + 'stopLossPrice': None, + 'average': average, + 'amount': amount, + 'filled': filled, + 'remaining': None, # TO_DO + 'cost': cost, + 'trades': None, + 'fee': { + 'cost': fee, + 'currency': feeCurrency, + }, + 'info': order, + }, market) + + def parse_order_status(self, status: Str): + if status is not None: + statuses: dict = { + 'NEW': 'open', + 'FILLED': 'closed', + 'CANCEL_SENT': 'canceled', + 'CANCEL_ALL_SENT': 'canceled', + 'CANCELLED': 'canceled', + 'PARTIAL_FILLED': 'open', + 'REJECTED': 'rejected', + 'INCOMPLETE': 'open', + 'COMPLETED': 'closed', + } + return self.safe_string(statuses, status, status) + return status + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://developer.woox.io/api-reference/endpoint/public_data/orderbook + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['maxLevel'] = limit + response = await self.v3PublicGetOrderbook(self.extend(request, params)) + # + # } + # { + # "success": True, + # "timestamp": 1751620923344, + # "data": { + # "asks": [ + # { + # "price": "108924.86", + # "quantity": "0.032126" + # } + # ], + # "bids": [ + # { + # "price": "108924.85", + # "quantity": "1.714147" + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + timestamp = self.safe_integer(response, 'timestamp') + return self.parse_order_book(data, symbol, timestamp, 'bids', 'asks', 'price', 'quantity') + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + + https://developer.woox.io/api-reference/endpoint/public_data/klineHistory + + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: max=1000, max=100 when since is defined and is less than(now - (999 * (timeframe in ms))) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch entries for + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'type': self.safe_string(self.timeframes, timeframe, timeframe), + } + if limit is not None: + request['limit'] = min(limit, 1000) + if since is not None: + request['after'] = since + until = self.safe_integer(params, 'until') + params = self.omit(params, 'until') + if until is not None: + request['before'] = until + response = await self.v3PublicGetKlineHistory(self.extend(request, params)) + # + # { + # "success": True, + # "data": { + # "rows": [ + # { + # "symbol": "SPOT_BTC_USDT", + # "open": "108994.16", + # "close": "108994.16", + # "high": "108994.16", + # "low": "108994.16", + # "volume": "0", + # "amount": "0", + # "type": "1m", + # "startTimestamp": 1751622120000, + # "endTimestamp": 1751622180000 + # } + # ] + # }, + # "timestamp": 1751622205410 + # } + # + data = self.safe_dict(response, 'data', {}) + rows = self.safe_list(data, 'rows', []) + return self.parse_ohlcvs(rows, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + return [ + self.safe_integer(ohlcv, 'startTimestamp'), + self.safe_number(ohlcv, 'open'), + self.safe_number(ohlcv, 'high'), + self.safe_number(ohlcv, 'low'), + self.safe_number(ohlcv, 'close'), + self.safe_number(ohlcv, 'volume'), + ] + + async def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all the trades made from a single order + + https://docs.woox.io/#get-trades + + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + market: Market = None + if symbol is not None: + market = self.market(symbol) + request: dict = { + 'oid': id, + } + response = await self.v1PrivateGetOrderOidTrades(self.extend(request, params)) + # { + # "success": True, + # "rows": [ + # { + # "id": "99111647", + # "symbol": "SPOT_WOO_USDT", + # "fee": "0.0024", + # "side": "BUY", + # "executed_timestamp": "1641482113.084", + # "order_id": "87541111", + # "order_tag": "default", + # "executed_price": "1", + # "executed_quantity": "12", + # "fee_asset": "WOO", + # "is_maker": "1" + # } + # ] + # } + trades = self.safe_list(response, 'rows', []) + return self.parse_trades(trades, market, since, limit, params) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://developer.woox.io/api-reference/endpoint/trading/get_transactions + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: set to True if you want to fetch trades with pagination + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchMyTrades', 'paginate') + if paginate: + return await self.fetch_paginated_call_incremental('fetchMyTrades', symbol, since, limit, params, 'page', 500) + request: dict = {} + market: Market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if since is not None: + request['startTime'] = since + until = self.safe_integer(params, 'until') # unified in milliseconds + params = self.omit(params, ['until']) + if until is not None: + request['endTime'] = until + if limit is not None: + request['limit'] = limit + response = await self.v3PrivateGetTradeTransactionHistory(self.extend(request, params)) + # + # { + # "success": True, + # "data": { + # "rows": [ + # { + # "id": 1734947821, + # "symbol": "SPOT_LTC_USDT", + # "orderId": 60780383217, + # "executedPrice": 87.86, + # "executedQuantity": 0.1, + # "fee": 0.0001, + # "realizedPnl": null, + # "feeAsset": "LTC", + # "orderTag": "default", + # "side": "BUY", + # "executedTimestamp": "1752055173.630", + # "isMaker": 0 + # } + # ], + # "meta": { + # "total": 1, + # "recordsPerPage": 100, + # "currentPage": 1 + # } + # }, + # "timestamp": 1752055545121 + # } + # + data = self.safe_dict(response, 'data', {}) + trades = self.safe_list(data, 'rows', []) + return self.parse_trades(trades, market, since, limit, params) + + async def fetch_accounts(self, params={}) -> List[Account]: + """ + fetch all the accounts associated with a profile + + https://developer.woox.io/api-reference/endpoint/account/get_account_info + https://developer.woox.io/api-reference/endpoint/account/sub_accounts + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `account structures ` indexed by the account type + """ + mainAccountPromise = self.v3PrivateGetAccountInfo(params) + # + # { + # "success": True, + # "data": { + # "applicationId": "251bf5c4-f3c8-4544-bb8b-80001007c3c0", + # "account": "carlos_jose_lima@yahoo.com", + # "alias": "carlos_jose_lima@yahoo.com", + # "otpauth": True, + # "accountMode": "FUTURES", + # "positionMode": "ONE_WAY", + # "leverage": 0, + # "marginRatio": "10", + # "openMarginRatio": "10", + # "initialMarginRatio": "10", + # "maintenanceMarginRatio": "0.03", + # "totalCollateral": "165.55629469", + # "freeCollateral": "165.55629469", + # "totalAccountValue": "167.32418611", + # "totalTradingValue": "167.32418611", + # "totalVaultValue": "0", + # "totalStakingValue": "0", + # "totalLaunchpadValue": "0", + # "totalEarnValue": "0", + # "referrerID": null, + # "accountType": "Main" + # }, + # "timestamp": 1752062807915 + # } + # + subAccountPromise = self.v3PrivateGetAccountSubAccountsAll(params) + # + # { + # "success": True, + # "data": { + # "rows": [ + # { + # "applicationId": "6b43de5c-0955-4887-9862-d84e4689f9fe", + # "name": "sub_account_2", + # "createdTime": "1606897264.994" + # }, + # ] + # }, + # "timestamp": 1721295317627 + # } + # + mainAccountResponse, subAccountResponse = await asyncio.gather(*[mainAccountPromise, subAccountPromise]) + mainData = self.safe_dict(mainAccountResponse, 'data', {}) + mainRows = [mainData] + subData = self.safe_dict(subAccountResponse, 'data', {}) + subRows = self.safe_list(subData, 'rows', []) + rows = self.array_concat(mainRows, subRows) + return self.parse_accounts(rows, params) + + def parse_account(self, account): + # + # { + # "applicationId": "251bf5c4-f3c8-4544-bb8b-80001007c3c0", + # "account": "carlos_jose_lima@yahoo.com", + # "alias": "carlos_jose_lima@yahoo.com", + # "otpauth": True, + # "accountMode": "FUTURES", + # "positionMode": "ONE_WAY", + # "leverage": 0, + # "marginRatio": "10", + # "openMarginRatio": "10", + # "initialMarginRatio": "10", + # "maintenanceMarginRatio": "0.03", + # "totalCollateral": "165.55629469", + # "freeCollateral": "165.55629469", + # "totalAccountValue": "167.32418611", + # "totalTradingValue": "167.32418611", + # "totalVaultValue": "0", + # "totalStakingValue": "0", + # "totalLaunchpadValue": "0", + # "totalEarnValue": "0", + # "referrerID": null, + # "accountType": "Main" + # } + # + # { + # "applicationId": "6b43de5c-0955-4887-9862-d84e4689f9fe", + # "name": "sub_account_2", + # "createdTime": "1606897264.994" + # } + # + return { + 'info': account, + 'id': self.safe_string(account, 'applicationId'), + 'name': self.safe_string_n(account, ['name', 'account', 'alias']), + 'code': None, + 'type': self.safe_string_lower(account, 'accountType', 'subaccount'), + } + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://docs.woox.io/#get-current-holding-get-balance-new + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + response = await self.v3PrivateGetAssetBalances(params) + # + # { + # "success": True, + # "data": { + # "holding": [ + # { + # "token": "0_token", + # "holding": 1, + # "frozen": 0, + # "staked": 0, + # "unbonding": 0, + # "vault": 0, + # "interest": 0, + # "pendingShortQty": 0, + # "pendingLongQty": 0, + # "availableBalance": 0, + # "updatedTime": 312321.121 + # } + # ] + # }, + # "timestamp": 1673323746259 + # } + # + data = self.safe_dict(response, 'data') + return self.parse_balance(data) + + def parse_balance(self, response) -> Balances: + result: dict = { + 'info': response, + } + balances = self.safe_list(response, 'holding', []) + for i in range(0, len(balances)): + balance = balances[i] + code = self.safe_currency_code(self.safe_string(balance, 'token')) + account = self.account() + account['total'] = self.safe_string(balance, 'holding') + account['free'] = self.safe_string(balance, 'availableBalance') + result[code] = account + return self.safe_balance(result) + + async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://developer.woox.io/api-reference/endpoint/assets/get_wallet_deposit + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + # self method is TODO because of networks unification + await self.load_markets() + currency = self.currency(code) + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + request: dict = { + 'token': currency['id'], + 'network': self.network_code_to_id(networkCode), + } + response = await self.v3PrivateGetAssetWalletDeposit(self.extend(request, params)) + # + # { + # "success": True, + # "data": { + # "address": "0x31d64B3230f8baDD91dE1710A65DF536aF8f7cDa", + # "extra": "" + # }, + # "timestamp": 1721300689532 + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_deposit_address(data, currency) + + def get_dedicated_network_id(self, currency, params: dict) -> Any: + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + networkCode = self.network_id_to_code(networkCode, currency['code']) + networkEntry = self.safe_dict(currency['networks'], networkCode) + if networkEntry is None: + supportedNetworks = list(currency['networks'].keys()) + raise BadRequest(self.id + ' can not determine a network code, please provide unified "network" param, one from the following: ' + self.json(supportedNetworks)) + currentyNetworkId = self.safe_string(networkEntry, 'currencyNetworkId') + return [currentyNetworkId, params] + + def parse_deposit_address(self, depositEntry, currency: Currency = None) -> DepositAddress: + address = self.safe_string(depositEntry, 'address') + self.check_address(address) + return { + 'info': depositEntry, + 'currency': self.safe_string(currency, 'code'), + 'network': None, + 'address': address, + 'tag': self.safe_string(depositEntry, 'extra'), + } + + async def get_asset_history_rows(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> Any: + await self.load_markets() + request: dict = {} + currency: Currency = None + if code is not None: + currency = self.currency(code) + request['token'] = currency['id'] + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + if networkCode is not None: + request['network'] = self.network_code_to_id(networkCode) + if since is not None: + request['startTime'] = since + if limit is not None: + request['size'] = min(limit, 1000) + transactionType = self.safe_string(params, 'type') + params = self.omit(params, 'type') + if transactionType is not None: + request['type'] = transactionType + response = await self.v3PrivateGetAssetWalletHistory(self.extend(request, params)) + # + # { + # "success": True, + # "data": { + # "rows": [ + # { + # "createdTime": "1734964440.523", + # "updatedTime": "1734964614.081", + # "id": "24122314340000585", + # "externalId": "241223143600621", + # "applicationId": "251bf5c4-f3c8-4544-bb8b-80001007c3c0", + # "token": "ARB_USDCNATIVE", + # "targetAddress": "0x4d6802d2736daa85e6242ef0dc0f00aa0e68f635", + # "sourceAddress": "0x63DFE4e34A3bFC00eB0220786238a7C6cEF8Ffc4", + # "extra": "", + # "type": "BALANCE", + # "tokenSide": "WITHDRAW", + # "amount": "10.00000000", + # "txId": "0x891ade0a47fd55466bb9d06702bea4edcb75ed9367d9afbc47b93a84f496d2e6", + # "feeToken": "USDC", + # "feeAmount": "2", + # "status": "COMPLETED", + # "confirmingThreshold": null, + # "confirmedNumber": null + # } + # ], + # "meta": { + # "total": 1, + # "records_per_page": 25, + # "current_page": 1 + # } + # }, + # "timestamp": 1752485344719 + # } + # + data = self.safe_dict(response, 'data', {}) + return [currency, self.safe_list(data, 'rows', [])] + + async def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered balance of the user + + https://developer.woox.io/api-reference/endpoint/assets/get_wallet_history + + :param str [code]: unified currency code, default is None + :param int [since]: timestamp in ms of the earliest ledger entry, default is None + :param int [limit]: max number of ledger entries to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ledger structure ` + """ + currencyRows = await self.get_asset_history_rows(code, since, limit, params) + currency = self.safe_value(currencyRows, 0) + rows = self.safe_list(currencyRows, 1) + return self.parse_ledger(rows, currency, since, limit, params) + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + # + # { + # "createdTime": "1734964440.523", + # "updatedTime": "1734964614.081", + # "id": "24122314340000585", + # "externalId": "241223143600621", + # "applicationId": "251bf5c4-f3c8-4544-bb8b-80001007c3c0", + # "token": "ARB_USDCNATIVE", + # "targetAddress": "0x4d6802d2736daa85e6242ef0dc0f00aa0e68f635", + # "sourceAddress": "0x63DFE4e34A3bFC00eB0220786238a7C6cEF8Ffc4", + # "extra": "", + # "type": "BALANCE", + # "tokenSide": "WITHDRAW", + # "amount": "10.00000000", + # "txId": "0x891ade0a47fd55466bb9d06702bea4edcb75ed9367d9afbc47b93a84f496d2e6", + # "feeToken": "USDC", + # "feeAmount": "2", + # "status": "COMPLETED", + # "confirmingThreshold": null, + # "confirmedNumber": null + # } + # + networkizedCode = self.safe_string(item, 'token') + code = self.safe_currency_code(networkizedCode, currency) + currency = self.safe_currency(code, currency) + amount = self.safe_number(item, 'amount') + side = self.safe_string(item, 'tokenSide') + direction = 'in' if (side == 'DEPOSIT') else 'out' + timestamp = self.safe_timestamp(item, 'createdTime') + fee = self.parse_token_and_fee_temp(item, ['feeToken'], ['feeAmount']) + return self.safe_ledger_entry({ + 'info': item, + 'id': self.safe_string(item, 'id'), + 'currency': code, + 'account': self.safe_string(item, 'account'), + 'referenceAccount': None, + 'referenceId': self.safe_string(item, 'txId'), + 'status': self.parse_transaction_status(self.safe_string(item, 'status')), + 'amount': amount, + 'before': None, + 'after': None, + 'direction': direction, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'type': self.parse_ledger_entry_type(self.safe_string(item, 'type')), + 'fee': fee, + }, currency) + + def parse_ledger_entry_type(self, type): + types: dict = { + 'BALANCE': 'transaction', # Funds moved in/out wallet + 'COLLATERAL': 'transfer', # Funds moved between portfolios + } + return self.safe_string(types, type, type) + + def get_currency_from_chaincode(self, networkizedCode, currency): + if currency is not None: + return currency + else: + parts = networkizedCode.split('_') + partsLength = len(parts) + firstPart = self.safe_string(parts, 0) + currencyId = self.safe_string(parts, 1, firstPart) + if partsLength > 2: + currencyId += '_' + self.safe_string(parts, 2) + currency = self.safe_currency(currencyId) + return currency + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://developer.woox.io/api-reference/endpoint/assets/get_wallet_history + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + request: dict = { + 'tokenSide': 'DEPOSIT', + } + return await self.fetch_deposits_withdrawals(code, since, limit, self.extend(request, params)) + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://developer.woox.io/api-reference/endpoint/assets/get_wallet_history + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + request: dict = { + 'tokenSide': 'WITHDRAW', + } + return await self.fetch_deposits_withdrawals(code, since, limit, self.extend(request, params)) + + async def fetch_deposits_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch history of deposits and withdrawals + + https://developer.woox.io/api-reference/endpoint/assets/get_wallet_history + + :param str [code]: unified currency code for the currency of the deposit/withdrawals, default is None + :param int [since]: timestamp in ms of the earliest deposit/withdrawal, default is None + :param int [limit]: max number of deposit/withdrawals to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `transaction structure ` + """ + request: dict = { + 'type': 'BALANCE', + } + currencyRows = await self.get_asset_history_rows(code, since, limit, self.extend(request, params)) + currency = self.safe_value(currencyRows, 0) + rows = self.safe_list(currencyRows, 1) + return self.parse_transactions(rows, currency, since, limit, params) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # { + # "createdTime": "1734964440.523", + # "updatedTime": "1734964614.081", + # "id": "24122314340000585", + # "externalId": "241223143600621", + # "applicationId": "251bf5c4-f3c8-4544-bb8b-80001007c3c0", + # "token": "ARB_USDCNATIVE", + # "targetAddress": "0x4d6802d2736daa85e6242ef0dc0f00aa0e68f635", + # "sourceAddress": "0x63DFE4e34A3bFC00eB0220786238a7C6cEF8Ffc4", + # "extra": "", + # "type": "BALANCE", + # "tokenSide": "WITHDRAW", + # "amount": "10.00000000", + # "txId": "0x891ade0a47fd55466bb9d06702bea4edcb75ed9367d9afbc47b93a84f496d2e6", + # "feeToken": "USDC", + # "feeAmount": "2", + # "status": "COMPLETED", + # "confirmingThreshold": null, + # "confirmedNumber": null + # } + # + networkizedCode = self.safe_string(transaction, 'token') + currencyDefined = self.get_currency_from_chaincode(networkizedCode, currency) + code = currencyDefined['code'] + movementDirection = self.safe_string_lower_n(transaction, ['token_side', 'tokenSide', 'type']) + if movementDirection == 'withdraw': + movementDirection = 'withdrawal' + fee = self.parse_token_and_fee_temp(transaction, ['fee_token', 'feeToken'], ['fee_amount', 'feeAmount']) + addressTo = self.safe_string_n(transaction, ['target_address', 'targetAddress', 'addressTo']) + addressFrom = self.safe_string_2(transaction, 'source_address', 'sourceAddress') + timestamp = self.safe_timestamp_n(transaction, ['created_time', 'createdTime'], self.safe_integer(transaction, 'timestamp')) + return { + 'info': transaction, + 'id': self.safe_string_n(transaction, ['id', 'withdraw_id', 'withdrawId']), + 'txid': self.safe_string_2(transaction, 'tx_id', 'txId'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'address': None, + 'addressFrom': addressFrom, + 'addressTo': addressTo, + 'tag': self.safe_string_2(transaction, 'extra', 'tag'), + 'tagFrom': None, + 'tagTo': None, + 'type': movementDirection, + 'amount': self.safe_number(transaction, 'amount'), + 'currency': code, + 'status': self.parse_transaction_status(self.safe_string(transaction, 'status')), + 'updated': self.safe_timestamp_2(transaction, 'updated_time', 'updatedTime'), + 'comment': None, + 'internal': None, + 'fee': fee, + 'network': self.network_id_to_code(self.safe_string(transaction, 'network')), + } + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'NEW': 'pending', + 'CONFIRMING': 'pending', + 'PROCESSING': 'pending', + 'COMPLETED': 'ok', + 'CANCELED': 'canceled', + } + return self.safe_string(statuses, status, status) + + async def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://docs.woox.io/#get-transfer-history + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account to transfer from + :param str toAccount: account to transfer to + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transfer structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'token': currency['id'], + 'amount': self.parse_to_numeric(amount), + 'from': { + 'applicationId': fromAccount, + }, + 'to': { + 'applicationId': toAccount, + }, + } + response = await self.v3PrivatePostAssetTransfer(self.extend(request, params)) + # + # { + # "success": True, + # "id": 200 + # } + # + data = self.safe_dict(response, 'data', {}) + data['timestamp'] = self.safe_integer(response, 'timestamp') + data['token'] = currency['id'] + data['status'] = 'ok' + transfer = self.parse_transfer(data, currency) + transferOptions = self.safe_dict(self.options, 'transfer', {}) + fillResponseFromRequest = self.safe_bool(transferOptions, 'fillResponseFromRequest', True) + if fillResponseFromRequest: + transfer['amount'] = amount + transfer['fromAccount'] = fromAccount + transfer['toAccount'] = toAccount + return transfer + + async def fetch_transfers(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[TransferEntry]: + """ + fetch a history of internal transfers made on an account + + https://developer.woox.io/api-reference/endpoint/assets/get_transfer_history + + :param str code: unified currency code of the currency transferred + :param int [since]: the earliest time in ms to fetch transfers for + :param int [limit]: the maximum number of transfers structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch entries for + :returns dict[]: a list of `transfer structures ` + """ + request: dict = {} + currency = None + if code is not None: + currency = self.currency(code) + if limit is not None: + request['size'] = limit + if since is not None: + request['startTime'] = since + until = self.safe_integer(params, 'until') # unified in milliseconds + params = self.omit(params, ['until']) + if until is not None: + request['endTime'] = until + response = await self.v3PrivateGetAssetTransferHistory(self.extend(request, params)) + # + # { + # "success": True, + # "data": { + # "rows": [ + # { + # "id": 225, + # "token": "USDT", + # "amount": "1000000", + # "status": "COMPLETED", + # "from": { + # "applicationId": "046b5c5c-5b44-4d27-9593-ddc32c0a08ae", + # "accountName": "Main" + # }, + # "to": { + # "applicationId": "082ae5ae-e26a-4fb1-be5b-03e5b4867663", + # "accountName": "sub001" + # }, + # "createdTime": "1642660941.534", + # "updatedTime": "1642660941.950" + # } + # ], + # "meta": { + # "total": 46, + # "recordsPerPage": 1, + # "currentPage": 1 + # } + # }, + # "timestamp": 1721295317627 + # } + # + data = self.safe_dict(response, 'data', {}) + rows = self.safe_list(data, 'rows', []) + return self.parse_transfers(rows, currency, since, limit, params) + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + # + # fetchTransfers + # { + # "id": 46704, + # "token": "USDT", + # "amount": 30000.00000000, + # "status": "COMPLETED", + # "from_application_id": "0f1bd3cd-dba2-4563-b8bb-0adb1bfb83a3", + # "to_application_id": "c01e6940-a735-4022-9b6c-9d3971cdfdfa", + # "from_user": "LeverageLow", + # "to_user": "dev", + # "created_time": "1709022325.427", + # "updated_time": "1709022325.542" + # } + # { + # "id": 225, + # "token": "USDT", + # "amount": "1000000", + # "status": "COMPLETED", + # "from": { + # "applicationId": "046b5c5c-5b44-4d27-9593-ddc32c0a08ae", + # "accountName": "Main" + # }, + # "to": { + # "applicationId": "082ae5ae-e26a-4fb1-be5b-03e5b4867663", + # "accountName": "sub001" + # }, + # "createdTime": "1642660941.534", + # "updatedTime": "1642660941.950" + # } + # + # transfer + # { + # "success": True, + # "id": 200 + # } + # + code = self.safe_currency_code(self.safe_string(transfer, 'token'), currency) + timestamp = self.safe_timestamp_2(transfer, 'createdTime', 'timestamp') + success = self.safe_bool(transfer, 'success') + status: Str = None + if success is not None: + status = 'ok' if success else 'failed' + fromAccount = self.safe_dict(transfer, 'from', {}) + toAccount = self.safe_dict(transfer, 'to', {}) + return { + 'id': self.safe_string(transfer, 'id'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'currency': code, + 'amount': self.safe_number(transfer, 'amount'), + 'fromAccount': self.safe_string(fromAccount, 'applicationId'), + 'toAccount': self.safe_string(toAccount, 'applicationId'), + 'status': self.parse_transfer_status(self.safe_string(transfer, 'status', status)), + 'info': transfer, + } + + def parse_transfer_status(self, status: Str) -> Str: + statuses: dict = { + 'NEW': 'pending', + 'CONFIRMING': 'pending', + 'PROCESSING': 'pending', + 'COMPLETED': 'ok', + 'CANCELED': 'canceled', + } + return self.safe_string(statuses, status, status) + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://docs.woox.io/#token-withdraw-v3 + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + await self.load_markets() + self.check_address(address) + currency = self.currency(code) + request: dict = { + 'amount': amount, + 'address': address, + } + if tag is not None: + request['extra'] = tag + network = self.safe_string(params, 'network') + if network is None: + raise ArgumentsRequired(self.id + ' withdraw() requires a network parameter for ' + code) + params = self.omit(params, 'network') + request['token'] = currency['id'] + request['network'] = self.network_code_to_id(network) + response = await self.v3PrivatePostAssetWalletWithdraw(self.extend(request, params)) + # + # { + # "success": True, + # "withdraw_id": "20200119145703654" + # } + # + data = self.safe_dict(response, 'data', {}) + transactionData = self.extend(data, { + 'id': self.safe_string(data, 'withdrawId'), + 'timestamp': self.safe_integer(response, 'timestamp'), + 'currency': code, + 'amount': amount, + 'addressTo': address, + 'tag': tag, + 'network': network, + 'type': 'withdrawal', + 'status': 'pending', + }) + return self.parse_transaction(transactionData, currency) + + async def repay_margin(self, code: str, amount: float, symbol: Str = None, params={}): + """ + repay borrowed margin and interest + + https://docs.woox.io/#repay-interest + + :param str code: unified currency code of the currency to repay + :param float amount: the amount to repay + :param str symbol: not used by woo.repayMargin() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin loan structure ` + """ + await self.load_markets() + market: Market = None + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + currency = self.currency(code) + request: dict = { + 'token': currency['id'], # interest token that you want to repay + 'amount': self.currency_to_precision(code, amount), + } + response = await self.v1PrivatePostInterestRepay(self.extend(request, params)) + # + # { + # "success": True, + # } + # + transaction = self.parse_margin_loan(response, currency) + return self.extend(transaction, { + 'amount': amount, + 'symbol': symbol, + }) + + def parse_margin_loan(self, info, currency: Currency = None): + # + # { + # "success": True, + # } + # + return { + 'id': None, + 'currency': self.safe_currency_code(None, currency), + 'amount': None, + 'symbol': None, + 'timestamp': None, + 'datetime': None, + 'info': info, + } + + def nonce(self): + return self.milliseconds() - self.options['timeDifference'] + + def sign(self, path, section='public', method='GET', params={}, headers=None, body=None): + version = section[0] + access = section[1] + pathWithParams = self.implode_params(path, params) + url = self.implode_hostname(self.urls['api'][access]) + url += '/' + version + '/' + params = self.omit(params, self.extract_params(path)) + params = self.keysort(params) + if access == 'public': + url += access + '/' + pathWithParams + if params: + url += '?' + self.urlencode(params) + elif access == 'pub': + url += pathWithParams + if params: + url += '?' + self.urlencode(params) + else: + self.check_required_credentials() + if method == 'POST' and (path == 'trade/algoOrder' or path == 'trade/order'): + isSandboxMode = self.safe_bool(self.options, 'sandboxMode', False) + if not isSandboxMode: + applicationId = 'bc830de7-50f3-460b-9ee0-f430f83f9dad' + brokerId = self.safe_string(self.options, 'brokerId', applicationId) + isTrigger = path.find('algo') > -1 + if isTrigger: + params['brokerId'] = brokerId + else: + params['broker_id'] = brokerId + params = self.keysort(params) + auth = '' + ts = str(self.nonce()) + url += pathWithParams + headers = { + 'x-api-key': self.apiKey, + 'x-api-timestamp': ts, + } + if version == 'v3': + auth = ts + method + '/' + version + '/' + pathWithParams + if method == 'POST' or method == 'PUT': + body = self.json(params) + auth += body + headers['content-type'] = 'application/json' + else: + if params: + query = self.urlencode(params) + url += '?' + query + auth += '?' + query + else: + auth = self.urlencode(params) + if method == 'POST' or method == 'PUT' or method == 'DELETE': + body = auth + else: + if params: + url += '?' + auth + auth += '|' + ts + headers['content-type'] = 'application/x-www-form-urlencoded' + headers['x-api-signature'] = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256) + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if not response: + return None # fallback to default error handler + # + # 400 Bad Request {"success":false,"code":-1012,"message":"Amount is required for buy market orders when margin disabled."} + # {"code":"-1011","message":"The system is under maintenance.","success":false} + # + success = self.safe_bool(response, 'success') + errorCode = self.safe_string(response, 'code') + if not success: + feedback = self.id + ' ' + self.json(response) + self.throw_broadly_matched_exception(self.exceptions['broad'], body, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + return None + + def parse_income(self, income, market: Market = None): + # + # { + # "id": 1286360, + # "symbol": "PERP_BTC_USDT", + # "fundingRate": -0.00001445, + # "markPrice": "26930.60000000", + # "fundingFee": "9.56021744", + # "fundingIntervalHours": 8, + # "paymentType": "Pay", + # "status": "COMPLETED", + # "createdTime": 1696060873259, + # "updatedTime": 1696060873286 + # } + # + marketId = self.safe_string(income, 'symbol') + symbol = self.safe_symbol(marketId, market) + amount = self.safe_string(income, 'fundingFee') + code = self.safe_currency_code('USD') + id = self.safe_string(income, 'id') + timestamp = self.safe_integer(income, 'updatedTime') + rate = self.safe_number(income, 'fundingRate') + paymentType = self.safe_string(income, 'paymentType') + amount = Precise.string_neg(amount) if (paymentType == 'Pay') else amount + return { + 'info': income, + 'symbol': symbol, + 'code': code, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': id, + 'amount': self.parse_number(amount), + 'rate': rate, + } + + async def fetch_funding_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch the history of funding payments paid and received on self account + + https://developer.woox.io/api-reference/endpoint/futures/get_fundingFee_history + + :param str [symbol]: unified market symbol + :param int [since]: the earliest time in ms to fetch funding history for + :param int [limit]: the maximum number of funding history structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict: a `funding history structure ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchFundingHistory', 'paginate') + if paginate: + return await self.fetch_paginated_call_incremental('fetchFundingHistory', symbol, since, limit, params, 'page', 500) + request: dict = {} + market: Market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if since is not None: + request['startTime'] = since + until = self.safe_integer(params, 'until') # unified in milliseconds + params = self.omit(params, ['until']) + if until is not None: + request['endTime'] = until + if limit is not None: + request['size'] = min(limit, 500) + response = await self.v3PrivateGetFuturesFundingFeeHistory(self.extend(request, params)) + # + # { + # "success": True, + # "data": { + # "meta": { + # "total": 670, + # "recordsPerPage": 25, + # "currentPage": 1 + # }, + # "rows": [ + # { + # "id": 1286360, + # "symbol": "PERP_BTC_USDT", + # "fundingRate": -0.00001445, + # "markPrice": "26930.60000000", + # "fundingFee": "9.56021744", + # "fundingIntervalHours": 8, + # "paymentType": "Pay", + # "status": "COMPLETED", + # "createdTime": 1696060873259, + # "updatedTime": 1696060873286 + # } + # ] + # }, + # "timestamp": 1721351502594 + # } + # + data = self.safe_dict(response, 'data', {}) + rows = self.safe_list(data, 'rows', []) + return self.parse_incomes(rows, market, since, limit) + + def parse_funding_rate(self, fundingRate, market: Market = None) -> FundingRate: + # + # { + # "symbol": "PERP_BTC_USDT", + # "estFundingRate": "-0.00000441", + # "estFundingRateTimestamp": 1751623979022, + # "lastFundingRate": "-0.00004953", + # "lastFundingRateTimestamp": 1751616000000, + # "nextFundingTime": 1751644800000, + # "lastFundingIntervalHours": 8, + # "estFundingIntervalHours": 8 + # } + # + symbol = self.safe_string(fundingRate, 'symbol') + market = self.market(symbol) + nextFundingTimestamp = self.safe_integer(fundingRate, 'nextFundingTime') + estFundingRateTimestamp = self.safe_integer(fundingRate, 'estFundingRateTimestamp') + lastFundingRateTimestamp = self.safe_integer(fundingRate, 'lastFundingRateTimestamp') + intervalString = self.safe_string(fundingRate, 'estFundingIntervalHours') + return { + 'info': fundingRate, + 'symbol': market['symbol'], + 'markPrice': None, + 'indexPrice': None, + 'interestRate': self.parse_number('0'), + 'estimatedSettlePrice': None, + 'timestamp': estFundingRateTimestamp, + 'datetime': self.iso8601(estFundingRateTimestamp), + 'fundingRate': self.safe_number(fundingRate, 'estFundingRate'), + 'fundingTimestamp': nextFundingTimestamp, + 'fundingDatetime': self.iso8601(nextFundingTimestamp), + 'nextFundingRate': None, + 'nextFundingTimestamp': None, + 'nextFundingDatetime': None, + 'previousFundingRate': self.safe_number(fundingRate, 'lastFundingRate'), + 'previousFundingTimestamp': lastFundingRateTimestamp, + 'previousFundingDatetime': self.iso8601(lastFundingRateTimestamp), + 'interval': intervalString + 'h', + } + + async def fetch_funding_interval(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate interval + + https://developer.woox.io/api-reference/endpoint/public_data/fundingRate + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + return await self.fetch_funding_rate(symbol, params) + + async def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate + + https://developer.woox.io/api-reference/endpoint/public_data/fundingRate + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = await self.v3PublicGetFundingRate(self.extend(request, params)) + # + # { + # "success": True, + # "data": { + # "rows": [ + # { + # "symbol": "PERP_BTC_USDT", + # "estFundingRate": "-0.00000441", + # "estFundingRateTimestamp": 1751623979022, + # "lastFundingRate": "-0.00004953", + # "lastFundingRateTimestamp": 1751616000000, + # "nextFundingTime": 1751644800000, + # "lastFundingIntervalHours": 8, + # "estFundingIntervalHours": 8 + # } + # ] + # }, + # "timestamp": 1751624037798 + # } + # + data = self.safe_dict(response, 'data', {}) + rows = self.safe_list(data, 'rows', []) + first = self.safe_dict(rows, 0, {}) + return self.parse_funding_rate(first, market) + + async def fetch_funding_rates(self, symbols: Strings = None, params={}) -> FundingRates: + """ + fetch the funding rate for multiple markets + + https://developer.woox.io/api-reference/endpoint/public_data/fundingRate + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `funding rate structures `, indexed by market symbols + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + response = await self.v3PublicGetFundingRate(params) + # + # { + # "success": True, + # "data": { + # "rows": [ + # { + # "symbol": "PERP_BTC_USDT", + # "estFundingRate": "-0.00000441", + # "estFundingRateTimestamp": 1751623979022, + # "lastFundingRate": "-0.00004953", + # "lastFundingRateTimestamp": 1751616000000, + # "nextFundingTime": 1751644800000, + # "lastFundingIntervalHours": 8, + # "estFundingIntervalHours": 8 + # } + # ] + # }, + # "timestamp": 1751624037798 + # } + # + data = self.safe_dict(response, 'data', {}) + rows = self.safe_list(data, 'rows', []) + return self.parse_funding_rates(rows, symbols) + + async def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical funding rate prices + + https://developer.woox.io/api-reference/endpoint/public_data/fundingRateHistory + + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: timestamp in ms of the earliest funding rate to fetch + :param int [limit]: the maximum amount of `funding rate structures ` to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest funding rate + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `funding rate structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchFundingRateHistory', 'paginate') + if paginate: + return await self.fetch_paginated_call_incremental('fetchFundingRateHistory', symbol, since, limit, params, 'page', 25) + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchFundingRateHistory() requires a symbol argument') + market = self.market(symbol) + symbol = market['symbol'] + request: dict = { + 'symbol': market['id'], + } + if since is not None: + request['startTime'] = since + request, params = self.handle_until_option('endTime', request, params) + response = await self.v3PublicGetFundingRateHistory(self.extend(request, params)) + # + # { + # "success": True, + # "data": { + # "rows": [ + # { + # "symbol": "PERP_BTC_USDT", + # "fundingRate": "-0.00004953", + # "fundingRateTimestamp": 1751616000000, + # "nextFundingTime": 1751644800000, + # "markPrice": "108708" + # } + # ], + # "meta": { + # "total": 11690, + # "recordsPerPage": 25, + # "currentPage": 1 + # } + # }, + # "timestamp": 1751632390031 + # } + # + data = self.safe_dict(response, 'data', {}) + rows = self.safe_list(data, 'rows', []) + rates = [] + for i in range(0, len(rows)): + entry = rows[i] + marketId = self.safe_string(entry, 'symbol') + timestamp = self.safe_integer(entry, 'fundingRateTimestamp') + rates.append({ + 'info': entry, + 'symbol': self.safe_symbol(marketId), + 'fundingRate': self.safe_number(entry, 'fundingRate'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + sorted = self.sort_by(rates, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, symbol, since, limit) + + async def set_position_mode(self, hedged: bool, symbol: Str = None, params={}): + """ + set hedged to True or False for a market + + https://developer.woox.io/api-reference/endpoint/futures/position_mode + + :param bool hedged: set to True to use HEDGE_MODE, False for ONE_WAY + :param str symbol: not used by woo setPositionMode + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + hedgeMode = None + if hedged: + hedgeMode = 'HEDGE_MODE' + else: + hedgeMode = 'ONE_WAY' + request: dict = { + 'positionMode': hedgeMode, + } + response = await self.v3PrivatePutFuturesPositionMode(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1752550492845 + # } + # + return response + + async def fetch_leverage(self, symbol: str, params={}) -> Leverage: + """ + fetch the set leverage for a market + + https://developer.woox.io/api-reference/endpoint/account/get_account_info + https://developer.woox.io/api-reference/endpoint/futures/get_leverage + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: *for swap markets only* 'cross' or 'isolated' + :param str [params.positionMode]: *for swap markets only* 'ONE_WAY' or 'HEDGE_MODE' + :returns dict: a `leverage structure ` + """ + await self.load_markets() + market = self.market(symbol) + response: dict = None + if market['spot']: + response = await self.v3PrivateGetAccountInfo(params) + # + # { + # "success": True, + # "data": { + # "applicationId": "dsa", + # "account": "dsa", + # "alias": "haha", + # "otpauth": True, + # "accountMode": "FUTURES", + # "positionMode": "ONE_WAY", + # "leverage": 0, + # "marginRatio": "10", + # "openMarginRatio": "10", + # "initialMarginRatio": "10", + # "maintenanceMarginRatio": "0.03", + # "totalCollateral": "165.6115334", + # "freeCollateral": "165.6115334", + # "totalAccountValue": "167.52723093", + # "totalTradingValue": "167.52723093", + # "totalVaultValue": "0", + # "totalStakingValue": "0", + # "totalLaunchpadValue": "0", + # "totalEarnValue": "0", + # "referrerID": null, + # "accountType": "Main" + # }, + # "timestamp": 1752645129054 + # } + # + elif market['swap']: + request: dict = { + 'symbol': market['id'], + } + marginMode: Str = None + marginMode, params = self.handle_margin_mode_and_params('fetchLeverage', params, 'cross') + request['marginMode'] = self.encode_margin_mode(marginMode) + response = await self.v3PrivateGetFuturesLeverage(self.extend(request, params)) + # + # HEDGE_MODE + # { + # "success": True, + # "data": + # { + # "symbol": "PERP_ETH_USDT", + # "marginMode": "CROSS", + # "positionMode": "HEDGE_MODE", + # "details": [ + # { + # "positionSide": "LONG", + # "leverage": 10 + # }, + # { + # "positionSide": "SHORT", + # "leverage": 10 + # } + # ] + # }, + # "timestamp": 1720886470482 + # } + # + # ONE_WAY + # { + # "success": True, + # "data": { + # "symbol": "PERP_ETH_USDT", + # "marginMode": "ISOLATED", + # "positionMode": "ONE_WAY", + # "details": [ + # { + # "positionSide": "BOTH", + # "leverage": 10 + # } + # ] + # }, + # "timestamp": 1720886810317 + # } + # + else: + raise NotSupported(self.id + ' fetchLeverage() is not supported for ' + market['type'] + ' markets') + data = self.safe_dict(response, 'data', {}) + return self.parse_leverage(data, market) + + def parse_leverage(self, leverage: dict, market: Market = None) -> Leverage: + marketId = self.safe_string(leverage, 'symbol') + market = self.safe_market(marketId, market) + marginMode = self.safe_string_lower(leverage, 'marginMode') + spotLeverage = self.safe_integer(leverage, 'leverage') + if spotLeverage == 0: + spotLeverage = None + longLeverage = spotLeverage + shortLeverage = spotLeverage + details = self.safe_list(leverage, 'details', []) + for i in range(0, len(details)): + position = self.safe_dict(details, i, {}) + positionLeverage = self.safe_integer(position, 'leverage') + side = self.safe_string(position, 'positionSide') + if side == 'BOTH': + longLeverage = positionLeverage + shortLeverage = positionLeverage + elif side == 'LONG': + longLeverage = positionLeverage + elif side == 'SHORT': + shortLeverage = positionLeverage + return { + 'info': leverage, + 'symbol': market['symbol'], + 'marginMode': marginMode, + 'longLeverage': longLeverage, + 'shortLeverage': shortLeverage, + } + + async def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://developer.woox.io/api-reference/endpoint/spot_margin/set_leverage + https://developer.woox.io/api-reference/endpoint/futures/set_leverage + + :param float leverage: the rate of leverage(1, 2, 3, 4 or 5 for spot markets, 1, 2, 3, 4, 5, 10, 15, 20 for swap markets) + :param str [symbol]: unified market symbol(is mandatory for swap markets) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: *for swap markets only* 'cross' or 'isolated' + :param str [params.positionMode]: *for swap markets only* 'ONE_WAY' or 'HEDGE_MODE' + :returns dict: response from the exchange + """ + await self.load_markets() + request: dict = { + 'leverage': leverage, + } + market: Market = None + if symbol is not None: + market = self.market(symbol) + if (symbol is None) or market['spot']: + return await self.v3PrivatePostSpotMarginLeverage(self.extend(request, params)) + elif market['swap']: + request['symbol'] = market['id'] + marginMode: Str = None + marginMode, params = self.handle_margin_mode_and_params('fetchLeverage', params, 'cross') + request['marginMode'] = self.encode_margin_mode(marginMode) + return await self.v3PrivatePutFuturesLeverage(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchLeverage() is not supported for ' + market['type'] + ' markets') + + async def add_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + add margin + + https://docs.woox.io/#update-isolated-margin-setting + + :param str symbol: unified market symbol + :param float amount: amount of margin to add + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.position_side]: 'LONG' or 'SHORT' in hedge mode, 'BOTH' in one way mode + :returns dict: a `margin structure ` + """ + return await self.modify_margin_helper(symbol, amount, 'ADD', params) + + async def reduce_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + remove margin from a position + + https://docs.woox.io/#update-isolated-margin-setting + + :param str symbol: unified market symbol + :param float amount: amount of margin to remove + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.position_side]: 'LONG' or 'SHORT' in hedge mode, 'BOTH' in one way mode + :returns dict: a `margin structure ` + """ + return await self.modify_margin_helper(symbol, amount, 'REDUCE', params) + + async def modify_margin_helper(self, symbol: str, amount, type, params={}) -> MarginModification: + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'adjust_token': 'USDT', # todo check + 'adjust_amount': amount, + 'action': type, + } + return await self.v1PrivatePostClientIsolatedMargin(self.extend(request, params)) + + async def fetch_position(self, symbol: Str, params={}): + """ + fetch data on an open position + + https://developer.woox.io/api-reference/endpoint/futures/get_positions + + :param str symbol: unified market symbol of the market the position is held in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `position structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = await self.v3PrivateGetFuturesPositions(self.extend(request, params)) + # + # { + # "success": True, + # "data": { + # "positions": [ + # { + # "symbol": "PERP_LTC_USDT", + # "holding": "0.1", + # "pendingLongQty": "0", + # "pendingShortQty": "0", + # "settlePrice": "96.87", + # "averageOpenPrice": "96.87", + # "pnl24H": "0", + # "fee24H": "0.0048435", + # "markPrice": "96.83793449", + # "estLiqPrice": "0", + # "timestamp": 1752500555823, + # "adlQuantile": 2, + # "positionSide": "BOTH", + # "marginMode": "CROSS", + # "isolatedMarginToken": "", + # "isolatedMarginAmount": "0", + # "isolatedFrozenLong": "0", + # "isolatedFrozenShort": "0", + # "leverage": 10 + # } + # ] + # }, + # "timestamp": 1752500579848 + # } + # + result = self.safe_dict(response, 'data', {}) + positions = self.safe_list(result, 'positions', []) + first = self.safe_dict(positions, 0, {}) + return self.parse_position(first, market) + + async def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://developer.woox.io/api-reference/endpoint/futures/get_positions + + :param str[] [symbols]: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + await self.load_markets() + response = await self.v3PrivateGetFuturesPositions(params) + # + # { + # "success": True, + # "data": { + # "positions": [ + # { + # "symbol": "PERP_LTC_USDT", + # "holding": "0.1", + # "pendingLongQty": "0", + # "pendingShortQty": "0", + # "settlePrice": "96.87", + # "averageOpenPrice": "96.87", + # "pnl24H": "0", + # "fee24H": "0.0048435", + # "markPrice": "96.83793449", + # "estLiqPrice": "0", + # "timestamp": 1752500555823, + # "adlQuantile": 2, + # "positionSide": "BOTH", + # "marginMode": "CROSS", + # "isolatedMarginToken": "", + # "isolatedMarginAmount": "0", + # "isolatedFrozenLong": "0", + # "isolatedFrozenShort": "0", + # "leverage": 10 + # } + # ] + # }, + # "timestamp": 1752500579848 + # } + # + result = self.safe_dict(response, 'data', {}) + positions = self.safe_list(result, 'positions', []) + return self.parse_positions(positions, symbols) + + def parse_position(self, position: dict, market: Market = None): + # + # v1PrivateGetPositionSymbol + # { + # "symbol": "PERP_ETH_USDT", + # "position_side": "BOTH", + # "leverage": 10, + # "margin_mode": "CROSS", + # "average_open_price": 3139.9, + # "isolated_margin_amount": 0.0, + # "isolated_margin_token": "", + # "opening_time": "1720627963.094", + # "mark_price": 3155.19169891, + # "pending_short_qty": 0.0, + # "pending_long_qty": 0.0, + # "holding": -0.7, + # "pnl_24_h": 0.0, + # "est_liq_price": 9107.40055552, + # "settle_price": 3151.0319904, + # "success": True, + # "fee_24_h": 0.0, + # "isolated_frozen_long": 0.0, + # "isolated_frozen_short": 0.0, + # "timestamp": "1720867502.544" + # } + # + # v3PrivateGetPositions + # { + # "symbol": "PERP_LTC_USDT", + # "holding": "0.1", + # "pendingLongQty": "0", + # "pendingShortQty": "0", + # "settlePrice": "96.87", + # "averageOpenPrice": "96.87", + # "pnl24H": "0", + # "fee24H": "0.0048435", + # "markPrice": "96.83793449", + # "estLiqPrice": "0", + # "timestamp": 1752500555823, + # "adlQuantile": 2, + # "positionSide": "BOTH", + # "marginMode": "CROSS", + # "isolatedMarginToken": "", + # "isolatedMarginAmount": "0", + # "isolatedFrozenLong": "0", + # "isolatedFrozenShort": "0", + # "leverage": 10 + # } + # + contract = self.safe_string(position, 'symbol') + market = self.safe_market(contract, market) + size = self.safe_string(position, 'holding') + side: Str = None + if Precise.string_gt(size, '0'): + side = 'long' + else: + side = 'short' + contractSize = self.safe_string(market, 'contractSize') + markPrice = self.safe_string_2(position, 'markPrice', 'mark_price') + timestampString = self.safe_string(position, 'timestamp') + timestamp = None + if timestampString is not None: + if timestampString.find('.') > -1: + timestamp = self.safe_timestamp(position, 'timestamp') + else: + timestamp = self.safe_integer(position, 'timestamp') + entryPrice = self.safe_string_2(position, 'averageOpenPrice', 'average_open_price') + priceDifference = Precise.string_sub(markPrice, entryPrice) + unrealisedPnl = Precise.string_mul(priceDifference, size) + size = Precise.string_abs(size) + notional = Precise.string_mul(size, markPrice) + positionSide = self.safe_string(position, 'positionSide') # 'SHORT' or 'LONG' for hedged, 'BOTH' for non-hedged + return self.safe_position({ + 'info': position, + 'id': None, + 'symbol': self.safe_string(market, 'symbol'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastUpdateTimestamp': None, + 'initialMargin': None, + 'initialMarginPercentage': None, + 'maintenanceMargin': None, + 'maintenanceMarginPercentage': None, + 'entryPrice': self.parse_number(entryPrice), + 'notional': self.parse_number(notional), + 'leverage': self.safe_number(position, 'leverage'), + 'unrealizedPnl': self.parse_number(unrealisedPnl), + 'contracts': self.parse_number(size), + 'contractSize': self.parse_number(contractSize), + 'marginRatio': None, + 'liquidationPrice': self.safe_number_2(position, 'estLiqPrice', 'est_liq_price'), + 'markPrice': self.parse_number(markPrice), + 'lastPrice': None, + 'collateral': None, + 'marginMode': self.safe_string_lower_2(position, 'marginMode', 'margin_mode'), + 'side': side, + 'percentage': None, + 'hedged': positionSide != 'BOTH', + 'stopLossPrice': None, + 'takeProfitPrice': None, + }) + + async def fetch_convert_quote(self, fromCode: str, toCode: str, amount: Num = None, params={}) -> Conversion: + """ + fetch a quote for converting from one currency to another + + https://docs.woox.io/#get-quote-rfq + + :param str fromCode: the currency that you want to sell and convert from + :param str toCode: the currency that you want to buy and convert into + :param float [amount]: how much you want to trade in units of the from currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `conversion structure ` + """ + await self.load_markets() + request: dict = { + 'sellToken': fromCode.upper(), + 'buyToken': toCode.upper(), + 'sellQuantity': self.number_to_string(amount), + } + response = await self.v3PrivateGetConvertRfq(self.extend(request, params)) + # + # { + # "success": True, + # "data": { + # "quoteId": 123123123, + # "counterPartyId": "", + # "sellToken": "ETH", + # "sellQuantity": "0.0445", + # "buyToken": "USDT", + # "buyQuantity": "33.45", + # "buyPrice": "6.77", + # "expireTimestamp": 1659084466000, + # "message": 1659084466000 + # } + # } + # + data = self.safe_dict(response, 'data', {}) + fromCurrencyId = self.safe_string(data, 'sellToken', fromCode) + fromCurrency = self.currency(fromCurrencyId) + toCurrencyId = self.safe_string(data, 'buyToken', toCode) + toCurrency = self.currency(toCurrencyId) + return self.parse_conversion(data, fromCurrency, toCurrency) + + async def create_convert_trade(self, id: str, fromCode: str, toCode: str, amount: Num = None, params={}) -> Conversion: + """ + convert from one currency to another + + https://docs.woox.io/#send-quote-rft + + :param str id: the id of the trade that you want to make + :param str fromCode: the currency that you want to sell and convert from + :param str toCode: the currency that you want to buy and convert into + :param float [amount]: how much you want to trade in units of the from currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `conversion structure ` + """ + await self.load_markets() + request: dict = { + 'quoteId': id, + } + response = await self.v3PrivatePostConvertRft(self.extend(request, params)) + # + # { + # "success": True, + # "data": { + # "quoteId": 123123123, + # "counterPartyId": "", + # "rftAccepted": 1 # 1 -> success; 2 -> processing; 3 -> fail + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_conversion(data) + + async def fetch_convert_trade(self, id: str, code: Str = None, params={}) -> Conversion: + """ + fetch the data for a conversion trade + + https://docs.woox.io/#get-quote-trade + + :param str id: the id of the trade that you want to fetch + :param str [code]: the unified currency code of the conversion trade + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `conversion structure ` + """ + await self.load_markets() + request: dict = { + 'quoteId': id, + } + response = await self.v3PrivateGetConvertTrade(self.extend(request, params)) + # + # { + # "success": True, + # "data": { + # "quoteId": 12, + # "buyAsset": "", + # "sellAsset": "", + # "buyAmount": 12.11, + # "sellAmount": 12.11, + # "tradeStatus": 12, + # "createdTime": "" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + fromCurrencyId = self.safe_string(data, 'sellAsset') + toCurrencyId = self.safe_string(data, 'buyAsset') + fromCurrency = None + toCurrency = None + if fromCurrencyId is not None: + fromCurrency = self.currency(fromCurrencyId) + if toCurrencyId is not None: + toCurrency = self.currency(toCurrencyId) + return self.parse_conversion(data, fromCurrency, toCurrency) + + async def fetch_convert_trade_history(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Conversion]: + """ + fetch the users history of conversion trades + + https://docs.woox.io/#get-quote-trades + + :param str [code]: the unified currency code + :param int [since]: the earliest time in ms to fetch conversions for + :param int [limit]: the maximum number of conversion structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest conversion to fetch + :returns dict[]: a list of `conversion structures ` + """ + await self.load_markets() + request: dict = {} + request, params = self.handle_until_option('endTime', request, params) + if since is not None: + request['startTime'] = since + if limit is not None: + request['size'] = limit + response = await self.v3PrivateGetConvertTrades(self.extend(request, params)) + # + # { + # "success": True, + # "data": { + # "count": 12, + # "tradeVos":[ + # { + # "quoteId": 12, + # "buyAsset": "", + # "sellAsset": "", + # "buyAmount": 12.11, + # "sellAmount": 12.11, + # "tradeStatus": 12, + # "createdTime": "" + # } + # ... + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + rows = self.safe_list(data, 'tradeVos', []) + return self.parse_conversions(rows, code, 'sellAsset', 'buyAsset', since, limit) + + def parse_conversion(self, conversion: dict, fromCurrency: Currency = None, toCurrency: Currency = None) -> Conversion: + # + # fetchConvertQuote + # + # { + # "quoteId": 123123123, + # "counterPartyId": "", + # "sellToken": "ETH", + # "sellQuantity": "0.0445", + # "buyToken": "USDT", + # "buyQuantity": "33.45", + # "buyPrice": "6.77", + # "expireTimestamp": 1659084466000, + # "message": 1659084466000 + # } + # + # createConvertTrade + # + # { + # "quoteId": 123123123, + # "counterPartyId": "", + # "rftAccepted": 1 # 1 -> success; 2 -> processing; 3 -> fail + # } + # + # fetchConvertTrade, fetchConvertTradeHistory + # + # { + # "quoteId": 12, + # "buyAsset": "", + # "sellAsset": "", + # "buyAmount": 12.11, + # "sellAmount": 12.11, + # "tradeStatus": 12, + # "createdTime": "" + # } + # + timestamp = self.safe_integer_2(conversion, 'expireTimestamp', 'createdTime') + fromCurr = self.safe_string_2(conversion, 'sellToken', 'buyAsset') + fromCode = self.safe_currency_code(fromCurr, fromCurrency) + to = self.safe_string_2(conversion, 'buyToken', 'sellAsset') + toCode = self.safe_currency_code(to, toCurrency) + return { + 'info': conversion, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': self.safe_string(conversion, 'quoteId'), + 'fromCurrency': fromCode, + 'fromAmount': self.safe_number_2(conversion, 'sellQuantity', 'sellAmount'), + 'toCurrency': toCode, + 'toAmount': self.safe_number_2(conversion, 'buyQuantity', 'buyAmount'), + 'price': self.safe_number(conversion, 'buyPrice'), + 'fee': None, + } + + async def fetch_convert_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies that can be converted + + https://docs.woox.io/#get-quote-asset-info + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + await self.load_markets() + response = await self.v3PrivateGetConvertAssetInfo(params) + # + # { + # "success": True, + # "rows": [ + # { + # "token": "BTC", + # "tick": 0.0001, + # "createdTime": "1575014248.99", # Unix epoch time in seconds + # "updatedTime": "1575014248.99" # Unix epoch time in seconds + # }, + # ] + # } + # + result: dict = {} + data = self.safe_list(response, 'rows', []) + for i in range(0, len(data)): + entry = data[i] + id = self.safe_string(entry, 'token') + code = self.safe_currency_code(id) + result[code] = { + 'info': entry, + 'id': id, + 'code': code, + 'networks': None, + 'type': None, + 'name': None, + 'active': None, + 'deposit': None, + 'withdraw': None, + 'fee': None, + 'precision': self.safe_number(entry, 'tick'), + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + 'deposit': { + 'min': None, + 'max': None, + }, + }, + 'created': self.safe_timestamp(entry, 'createdTime'), + } + return result + + def default_network_code_for_currency(self, code): + currencyItem = self.currency(code) + networks = currencyItem['networks'] + networkKeys = list(networks.keys()) + for i in range(0, len(networkKeys)): + network = networkKeys[i] + if network == 'ETH': + return network + # if it was not returned according to above options, then return the first network of currency + return self.safe_value(networkKeys, 0) + + def set_sandbox_mode(self, enable: bool): + super(woo, self).set_sandbox_mode(enable) + self.options['sandboxMode'] = enable diff --git a/ccxt/async_support/woofipro.py b/ccxt/async_support/woofipro.py new file mode 100644 index 0000000..b210f0c --- /dev/null +++ b/ccxt/async_support/woofipro.py @@ -0,0 +1,2825 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.woofipro import ImplicitAPI +import asyncio +from ccxt.base.types import Any, Balances, Currencies, Currency, Int, LedgerEntry, Leverage, Market, Num, Order, OrderBook, OrderRequest, OrderSide, OrderType, Position, Str, Strings, FundingRate, FundingRates, Trade, TradingFees, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import NotSupported +from ccxt.base.errors import NetworkError +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class woofipro(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(woofipro, self).describe(), { + 'id': 'woofipro', + 'name': 'WOOFI PRO', + 'countries': ['KY'], # Cayman Islands + 'rateLimit': 100, + 'version': 'v1', + 'certified': True, + 'pro': True, + 'dex': True, + 'hostname': 'dex.woo.org', + 'has': { + 'CORS': None, + 'spot': False, + 'margin': False, + 'swap': True, + 'future': False, + 'option': False, + 'addMargin': False, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'cancelOrders': True, + 'cancelWithdraw': False, + 'closeAllPositions': False, + 'closePosition': False, + 'createConvertTrade': False, + 'createDepositAddress': False, + 'createMarketBuyOrderWithCost': False, + 'createMarketOrder': False, + 'createMarketOrderWithCost': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createOrderWithTakeProfitAndStopLoss': True, + 'createReduceOnlyOrder': True, + 'createStopLimitOrder': False, + 'createStopLossOrder': True, + 'createStopMarketOrder': False, + 'createStopOrder': False, + 'createTakeProfitOrder': True, + 'createTrailingAmountOrder': False, + 'createTrailingPercentOrder': False, + 'createTriggerOrder': True, + 'fetchAccounts': False, + 'fetchBalance': True, + 'fetchCanceledOrders': False, + 'fetchClosedOrder': False, + 'fetchClosedOrders': True, + 'fetchConvertCurrencies': False, + 'fetchConvertQuote': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': False, + 'fetchDeposits': True, + 'fetchDepositsWithdrawals': True, + 'fetchFundingHistory': True, + 'fetchFundingInterval': True, + 'fetchFundingIntervals': False, + 'fetchFundingRate': True, + 'fetchFundingRateHistory': True, + 'fetchFundingRates': True, + 'fetchIndexOHLCV': False, + 'fetchLedger': True, + 'fetchLeverage': True, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterestHistory': False, + 'fetchOpenOrder': False, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchOrderTrades': True, + 'fetchPosition': True, + 'fetchPositionMode': False, + 'fetchPositions': True, + 'fetchPremiumIndexOHLCV': False, + 'fetchStatus': True, + 'fetchTicker': False, + 'fetchTickers': False, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': True, + 'fetchTransactions': 'emulated', + 'fetchTransfers': False, + 'fetchWithdrawals': True, + 'reduceMargin': False, + 'setLeverage': True, + 'setMargin': False, + 'setPositionMode': False, + 'transfer': False, + 'withdraw': True, # exchange have that endpoint disabled atm, but was once implemented in ccxt per old docs: https://kronosresearch.github.io/wootrade-documents/#token-withdraw + }, + 'timeframes': { + '1m': '1m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1h', + '4h': '4h', + '12h': '12h', + '1d': '1d', + '1w': '1w', + '1M': '1mon', + '1y': '1y', + }, + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/9ba21b8a-a9c7-4770-b7f1-ce3bcbde68c1', + 'api': { + 'public': 'https://api-evm.orderly.org', + 'private': 'https://api-evm.orderly.org', + }, + 'test': { + 'public': 'https://testnet-api-evm.orderly.org', + 'private': 'https://testnet-api-evm.orderly.org', + }, + 'www': 'https://dex.woo.org', + 'doc': [ + 'https://orderly.network/docs/build-on-evm/building-on-evm', + ], + 'fees': [ + 'https://dex.woo.org/en/orderly', + ], + 'referral': { + 'url': 'https://dex.woo.org/en/trade?ref=CCXT', + 'discount': 0.05, + }, + }, + 'api': { + 'v1': { + 'public': { + 'get': { + 'public/volume/stats': 1, + 'public/broker/name': 1, + 'public/chain_info/{broker_id}': 1, + 'public/system_info': 1, + 'public/vault_balance': 1, + 'public/insurancefund': 1, + 'public/chain_info': 1, + 'faucet/usdc': 1, + 'public/account': 1, + 'get_account': 1, + 'registration_nonce': 1, + 'get_orderly_key': 1, + 'public/liquidation': 1, + 'public/liquidated_positions': 1, + 'public/config': 1, + 'public/campaign/ranking': 10, + 'public/campaign/stats': 10, + 'public/campaign/user': 10, + 'public/campaign/stats/details': 10, + 'public/campaigns': 10, + 'public/points/leaderboard': 1, + 'client/points': 1, + 'public/points/epoch': 1, + 'public/points/epoch_dates': 1, + 'public/referral/check_ref_code': 1, + 'public/referral/verify_ref_code': 1, + 'referral/admin_info': 1, + 'referral/info': 1, + 'referral/referee_info': 1, + 'referral/referee_rebate_summary': 1, + 'referral/referee_history': 1, + 'referral/referral_history': 1, + 'referral/rebate_summary': 1, + 'client/distribution_history': 1, + 'tv/config': 1, + 'tv/history': 1, + 'tv/symbol_info': 1, + 'public/funding_rate_history': 1, + 'public/funding_rate/{symbol}': 0.33, + 'public/funding_rates': 1, + 'public/info': 1, + 'public/info/{symbol}': 1, + 'public/market_trades': 1, + 'public/token': 1, + 'public/futures': 1, + 'public/futures/{symbol}': 1, + }, + 'post': { + 'register_account': 1, + }, + }, + 'private': { + 'get': { + 'client/key_info': 6, + 'client/orderly_key_ip_restriction': 6, + 'order/{oid}': 1, + 'client/order/{client_order_id}': 1, + 'algo/order/{oid}': 1, + 'algo/client/order/{client_order_id}': 1, + 'orders': 1, + 'algo/orders': 1, + 'trade/{tid}': 1, + 'trades': 1, + 'order/{oid}/trades': 1, + 'client/liquidator_liquidations': 1, + 'liquidations': 1, + 'asset/history': 60, + 'client/holding': 1, + 'withdraw_nonce': 1, + 'settle_nonce': 1, + 'pnl_settlement/history': 1, + 'volume/user/daily': 60, + 'volume/user/stats': 60, + 'client/statistics': 60, + 'client/info': 60, + 'client/statistics/daily': 60, + 'positions': 3.33, + 'position/{symbol}': 3.33, + 'funding_fee/history': 30, + 'notification/inbox/notifications': 60, + 'notification/inbox/unread': 60, + 'volume/broker/daily': 60, + 'broker/fee_rate/default': 10, + 'broker/user_info': 10, + 'orderbook/{symbol}': 1, + 'kline': 1, + }, + 'post': { + 'orderly_key': 1, + 'client/set_orderly_key_ip_restriction': 6, + 'client/reset_orderly_key_ip_restriction': 6, + 'order': 1, + 'batch-order': 10, + 'algo/order': 1, + 'liquidation': 1, + 'claim_insurance_fund': 1, + 'withdraw_request': 1, + 'settle_pnl': 1, + 'notification/inbox/mark_read': 60, + 'notification/inbox/mark_read_all': 60, + 'client/leverage': 120, + 'client/maintenance_config': 60, + 'delegate_signer': 10, + 'delegate_orderly_key': 10, + 'delegate_settle_pnl': 10, + 'delegate_withdraw_request': 10, + 'broker/fee_rate/set': 10, + 'broker/fee_rate/set_default': 10, + 'broker/fee_rate/default': 10, + 'referral/create': 10, + 'referral/update': 10, + 'referral/bind': 10, + 'referral/edit_split': 10, + }, + 'put': { + 'order': 1, + 'algo/order': 1, + }, + 'delete': { + 'order': 1, + 'algo/order': 1, + 'client/order': 1, + 'algo/client/order': 1, + 'algo/orders': 1, + 'orders': 1, + 'batch-order': 1, + 'client/batch-order': 1, + }, + }, + }, + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + 'accountId': True, + 'privateKey': False, + }, + 'fees': { + 'trading': { + 'tierBased': True, + 'percentage': True, + 'maker': self.parse_number('0.0002'), + 'taker': self.parse_number('0.0005'), + }, + }, + 'options': { + 'sandboxMode': False, + 'brokerId': 'CCXT', + 'verifyingContractAddress': '0x6F7a338F2aA472838dEFD3283eB360d4Dff5D203', + }, + 'features': { + 'default': { + 'sandbox': True, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, # todo by triggerPrice + 'takeProfitPrice': False, # todo by triggerPrice + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': True, + 'leverage': True, # todo implement + 'marketBuyByCost': False, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': False, + 'iceberg': True, # todo implement + }, + 'createOrders': { + 'max': 10, + }, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 500, + 'daysBack': None, + 'untilDays': 100000, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': True, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 500, + 'trigger': True, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 500, + 'daysBack': None, + 'daysBackCanceled': None, + 'untilDays': 100000, + 'trigger': True, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 1000, + }, + }, + 'spot': { + 'extends': 'default', + }, + 'forDerivatives': { + 'extends': 'default', + 'createOrder': { + # todo: implementation needs unification + 'triggerPriceType': None, + 'attachedStopLossTakeProfit': { + # todo: implementation needs unification + 'triggerPriceType': None, + 'price': False, + }, + }, + }, + 'swap': { + 'linear': { + 'extends': 'forDerivatives', + }, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'commonCurrencies': {}, + 'exceptions': { + 'exact': { + '-1000': ExchangeError, # UNKNOWN The data does not exist + '-1001': AuthenticationError, # INVALID_SIGNATURE The api key or secret is in wrong format. + '-1002': AuthenticationError, # UNAUTHORIZED API key or secret is invalid, it may because key have insufficient permission or the key is expired/revoked. + '-1003': RateLimitExceeded, # TOO_MANY_REQUEST Rate limit exceed. + '-1004': BadRequest, # UNKNOWN_PARAM An unknown parameter was sent. + '-1005': BadRequest, # INVALID_PARAM Some parameters are in wrong format for api. + '-1006': InvalidOrder, # RESOURCE_NOT_FOUND The data is not found in server. For example, when client try canceling a CANCELLED order, will raise self error. + '-1007': BadRequest, # DUPLICATE_REQUEST The data is already exists or your request is duplicated. + '-1008': InvalidOrder, # QUANTITY_TOO_HIGH The quantity of settlement is too high than you can request. + '-1009': InsufficientFunds, # CAN_NOT_WITHDRAWAL Can not request withdrawal settlement, you need to deposit other arrears first. + '-1011': NetworkError, # RPC_NOT_CONNECT Can not place/cancel orders, it may because internal network error. Please try again in a few seconds. + '-1012': BadRequest, # RPC_REJECT The place/cancel order request is rejected by internal module, it may because the account is in liquidation or other internal errors. Please try again in a few seconds. + '-1101': InsufficientFunds, # RISK_TOO_HIGH The risk exposure for client is too high, it may cause by sending too big order or the leverage is too low. please refer to client info to check the current exposure. + '-1102': InvalidOrder, # MIN_NOTIONAL The order value(price * size) is too small. + '-1103': InvalidOrder, # PRICE_FILTER The order price is not following the tick size rule for the symbol. + '-1104': InvalidOrder, # SIZE_FILTER The order quantity is not following the step size rule for the symbol. + '-1105': InvalidOrder, # PERCENTAGE_FILTER Price is X% too high or X% too low from the mid price. + '-1201': BadRequest, # LIQUIDATION_REQUEST_RATIO_TOO_SMALL total notional < 10000, least req ratio should = 1 + '-1202': BadRequest, # LIQUIDATION_STATUS_ERROR No need to liquidation because user margin is enough. + '29': BadRequest, # {"success":false,"code":29,"message":"Verify contract is invalid"} + '9': AuthenticationError, # {"success":false,"code":9,"message":"Address and signature do not match"} + '3': AuthenticationError, # {"success":false,"code":3,"message":"Signature error"} + '2': BadRequest, # {"success":false,"code":2,"message":"Timestamp expired"} + '15': BadRequest, # {"success":false,"code":15,"message":"BrokerId is not exist"} + }, + 'broad': { + }, + }, + 'precisionMode': TICK_SIZE, + }) + + def set_sandbox_mode(self, enable: bool): + super(woofipro, self).set_sandbox_mode(enable) + self.options['sandboxMode'] = enable + + async def fetch_status(self, params={}): + """ + the latest known information on the availability of the exchange API + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/public/get-system-maintenance-status + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `status structure ` + """ + response = await self.v1PublicGetPublicSystemInfo(params) + # + # { + # "success": True, + # "data": { + # "status": 0, + # "msg": "System is functioning properly." + # }, + # "timestamp": "1709274106602" + # } + # + data = self.safe_dict(response, 'data', {}) + status = self.safe_string(data, 'status') + if status is None: + status = 'error' + elif status == '0': + status = 'ok' + else: + status = 'maintenance' + return { + 'status': status, + 'updated': None, + 'eta': None, + 'url': None, + 'info': response, + } + + async def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/public/get-system-maintenance-status + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = await self.v1PublicGetPublicSystemInfo(params) + # + # { + # "success": True, + # "data": { + # "status": 0, + # "msg": "System is functioning properly." + # }, + # "timestamp": "1709274106602" + # } + # + return self.safe_integer(response, 'timestamp') + + def parse_market(self, market: dict) -> Market: + # + # { + # "symbol": "PERP_BTC_USDC", + # "quote_min": 123, + # "quote_max": 100000, + # "quote_tick": 0.1, + # "base_min": 0.00001, + # "base_max": 20, + # "base_tick": 0.00001, + # "min_notional": 1, + # "price_range": 0.02, + # "price_scope": 0.4, + # "std_liquidation_fee": 0.03, + # "liquidator_fee": 0.015, + # "claim_insurance_fund_discount": 0.0075, + # "funding_period": 8, + # "cap_funding": 0.000375, + # "floor_funding": -0.000375, + # "interest_rate": 0.0001, + # "created_time": 1684140107326, + # "updated_time": 1685345968053, + # "base_mmr": 0.05, + # "base_imr": 0.1, + # "imr_factor": 0.0002512, + # "liquidation_tier": "1" + # } + # + marketId = self.safe_string(market, 'symbol') + parts = marketId.split('_') + marketType = 'swap' + baseId = self.safe_string(parts, 1) + quoteId = self.safe_string(parts, 2) + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + settleId: Str = self.safe_string(parts, 2) + settle: Str = self.safe_currency_code(settleId) + symbol = base + '/' + quote + ':' + settle + return { + 'id': marketId, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': marketType, + 'spot': False, + 'margin': False, + 'swap': True, + 'future': False, + 'option': False, + 'active': None, + 'contract': True, + 'linear': True, + 'inverse': False, + 'contractSize': self.parse_number('1'), + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.safe_number(market, 'base_tick'), + 'price': self.safe_number(market, 'quote_tick'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(market, 'base_min'), + 'max': self.safe_number(market, 'base_max'), + }, + 'price': { + 'min': self.safe_number(market, 'quote_min'), + 'max': self.safe_number(market, 'quote_max'), + }, + 'cost': { + 'min': self.safe_number(market, 'min_notional'), + 'max': None, + }, + }, + 'created': self.safe_integer(market, 'created_time'), + 'info': market, + } + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for woofipro + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/public/get-available-symbols + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = await self.v1PublicGetPublicInfo(params) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "rows": [ + # { + # "symbol": "PERP_BTC_USDC", + # "quote_min": 123, + # "quote_max": 100000, + # "quote_tick": 0.1, + # "base_min": 0.00001, + # "base_max": 20, + # "base_tick": 0.00001, + # "min_notional": 1, + # "price_range": 0.02, + # "price_scope": 0.4, + # "std_liquidation_fee": 0.03, + # "liquidator_fee": 0.015, + # "claim_insurance_fund_discount": 0.0075, + # "funding_period": 8, + # "cap_funding": 0.000375, + # "floor_funding": -0.000375, + # "interest_rate": 0.0001, + # "created_time": 1684140107326, + # "updated_time": 1685345968053, + # "base_mmr": 0.05, + # "base_imr": 0.1, + # "imr_factor": 0.0002512, + # "liquidation_tier": "1" + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + rows = self.safe_list(data, 'rows', []) + return self.parse_markets(rows) + + async def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://orderly.network/docs/build-on-omnichain/evm-api/restful-api/public/get-supported-collateral-info#get-supported-collateral-info + https://orderly.network/docs/build-on-omnichain/evm-api/restful-api/public/get-supported-chains-per-builder#get-supported-chains-per-builder + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + result: dict = {} + tokenPromise = self.v1PublicGetPublicToken(params) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "rows": [{ + # "token": "USDC", + # "decimals": 6, + # "minimum_withdraw_amount": 0.000001, + # "token_hash": "0xd6aca1be9729c13d677335161321649cccae6a591554772516700f986f942eaa", + # "chain_details": [{ + # "chain_id": 43113, + # "contract_address": "0x5d64c9cfb0197775b4b3ad9be4d3c7976e0d8dc3", + # "cross_chain_withdrawal_fee": 123, + # "decimals": 6, + # "withdraw_fee": 2 + # }] + # } + # ] + # } + # } + # + chainPromise = self.v1PublicGetPublicChainInfo(params) + tokenResponse, chainResponse = await asyncio.gather(*[tokenPromise, chainPromise]) + tokenData = self.safe_dict(tokenResponse, 'data', {}) + tokenRows = self.safe_list(tokenData, 'rows', []) + chainData = self.safe_dict(chainResponse, 'data', {}) + chainRows = self.safe_list(chainData, 'rows', []) + indexedChains = self.index_by(chainRows, 'chain_id') + for i in range(0, len(tokenRows)): + token = tokenRows[i] + currencyId = self.safe_string(token, 'token') + networks = self.safe_list(token, 'chain_details') + code = self.safe_currency_code(currencyId) + resultingNetworks: dict = {} + for j in range(0, len(networks)): + networkEntry = networks[j] + networkId = self.safe_string(networkEntry, 'chain_id') + networkRow = self.safe_dict(indexedChains, networkId) + networkName = self.safe_string(networkRow, 'name') + networkCode = self.network_id_to_code(networkName, code) + resultingNetworks[networkCode] = { + 'id': networkId, + 'network': networkCode, + 'limits': { + 'withdraw': { + 'min': None, + 'max': None, + }, + 'deposit': { + 'min': None, + 'max': None, + }, + }, + 'active': None, + 'deposit': None, + 'withdraw': None, + 'fee': self.safe_number(networkEntry, 'withdrawal_fee'), + 'precision': self.parse_number(self.parse_precision(self.safe_string(networkEntry, 'decimals'))), + 'info': [networkEntry, networkRow], + } + result[code] = self.safe_currency_structure({ + 'id': currencyId, + 'name': None, + 'code': code, + 'precision': None, + 'active': None, + 'fee': None, + 'networks': resultingNetworks, + 'deposit': None, + 'withdraw': None, + 'limits': { + 'deposit': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': self.safe_number(token, 'minimum_withdraw_amount'), + 'max': None, + }, + }, + 'info': token, + }) + return result + + def parse_token_and_fee_temp(self, item, feeTokenKey, feeAmountKey): + feeCost = self.safe_string(item, feeAmountKey) + fee = None + if feeCost is not None: + feeCurrencyId = self.safe_string(item, feeTokenKey) + feeCurrencyCode = self.safe_currency_code(feeCurrencyId) + fee = { + 'cost': feeCost, + 'currency': feeCurrencyCode, + } + return fee + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # public/market_trades + # + # { + # "symbol": "SPOT_BTC_USDT", + # "side": "SELL", + # "executed_price": 46222.35, + # "executed_quantity": 0.0012, + # "executed_timestamp": "1683878609166" + # } + # + # fetchOrderTrades, fetchOrder + # + # { + # "id": "99119876", + # "symbol": "SPOT_WOO_USDT", + # "fee": "0.0024", + # "side": "BUY", + # "executed_timestamp": "1641481113084", + # "order_id": "87001234", + # "order_tag": "default", <-- self param only in "fetchOrderTrades" + # "executed_price": "1", + # "executed_quantity": "12", + # "fee_asset": "WOO", + # "is_maker": "1" + # } + # + isFromFetchOrder = ('id' in trade) + timestamp = self.safe_integer(trade, 'executed_timestamp') + marketId = self.safe_string(trade, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + price = self.safe_string(trade, 'executed_price') + amount = self.safe_string(trade, 'executed_quantity') + order_id = self.safe_string(trade, 'order_id') + fee = self.parse_token_and_fee_temp(trade, 'fee_asset', 'fee') + feeCost = self.safe_string(fee, 'cost') + if feeCost is not None: + fee['cost'] = feeCost + cost = Precise.string_mul(price, amount) + side = self.safe_string_lower(trade, 'side') + id = self.safe_string(trade, 'id') + takerOrMaker: Str = None + if isFromFetchOrder: + isMaker = self.safe_string(trade, 'is_maker') == '1' + takerOrMaker = 'maker' if isMaker else 'taker' + return self.safe_trade({ + 'id': id, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'side': side, + 'price': price, + 'amount': amount, + 'cost': cost, + 'order': order_id, + 'takerOrMaker': takerOrMaker, + 'type': None, + 'fee': fee, + 'info': trade, + }, market) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/public/get-market-trades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['limit'] = limit + response = await self.v1PublicGetPublicMarketTrades(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "rows": [{ + # "symbol": "PERP_ETH_USDC", + # "side": "BUY", + # "executed_price": 2050, + # "executed_quantity": 1, + # "executed_timestamp": 1683878609166 + # }] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + rows = self.safe_list(data, 'rows', []) + return self.parse_trades(rows, market, since, limit) + + def parse_funding_rate(self, fundingRate, market: Market = None) -> FundingRate: + # + # { + # "symbol":"PERP_AAVE_USDT", + # "est_funding_rate":-0.00003447, + # "est_funding_rate_timestamp":1653633959001, + # "last_funding_rate":-0.00002094, + # "last_funding_rate_timestamp":1653631200000, + # "next_funding_time":1653634800000, + # "sum_unitary_funding": 521.367 + # } + # + symbol = self.safe_string(fundingRate, 'symbol') + market = self.market(symbol) + nextFundingTimestamp = self.safe_integer(fundingRate, 'next_funding_time') + estFundingRateTimestamp = self.safe_integer(fundingRate, 'est_funding_rate_timestamp') + lastFundingRateTimestamp = self.safe_integer(fundingRate, 'last_funding_rate_timestamp') + fundingTimeString = self.safe_string(fundingRate, 'last_funding_rate_timestamp') + nextFundingTimeString = self.safe_string(fundingRate, 'next_funding_time') + millisecondsInterval = Precise.string_sub(nextFundingTimeString, fundingTimeString) + return { + 'info': fundingRate, + 'symbol': market['symbol'], + 'markPrice': None, + 'indexPrice': None, + 'interestRate': self.parse_number('0'), + 'estimatedSettlePrice': None, + 'timestamp': estFundingRateTimestamp, + 'datetime': self.iso8601(estFundingRateTimestamp), + 'fundingRate': self.safe_number(fundingRate, 'est_funding_rate'), + 'fundingTimestamp': nextFundingTimestamp, + 'fundingDatetime': self.iso8601(nextFundingTimestamp), + 'nextFundingRate': None, + 'nextFundingTimestamp': None, + 'nextFundingDatetime': None, + 'previousFundingRate': self.safe_number(fundingRate, 'last_funding_rate'), + 'previousFundingTimestamp': lastFundingRateTimestamp, + 'previousFundingDatetime': self.iso8601(lastFundingRateTimestamp), + 'interval': self.parse_funding_interval(millisecondsInterval), + } + + def parse_funding_interval(self, interval): + intervals: dict = { + '3600000': '1h', + '14400000': '4h', + '28800000': '8h', + '57600000': '16h', + '86400000': '24h', + } + return self.safe_string(intervals, interval, interval) + + async def fetch_funding_interval(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate interval + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/public/get-predicted-funding-rate-for-one-market + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + return await self.fetch_funding_rate(symbol, params) + + async def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/public/get-predicted-funding-rate-for-one-market + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = await self.v1PublicGetPublicFundingRateSymbol(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "symbol": "PERP_ETH_USDC", + # "est_funding_rate": 123, + # "est_funding_rate_timestamp": 1683880020000, + # "last_funding_rate": 0.0001, + # "last_funding_rate_timestamp": 1683878400000, + # "next_funding_time": 1683907200000, + # "sum_unitary_funding": 521.367 + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_funding_rate(data, market) + + async def fetch_funding_rates(self, symbols: Strings = None, params={}) -> FundingRates: + """ + fetch the current funding rate for multiple markets + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/public/get-predicted-funding-rates-for-all-markets + + :param str[] symbols: unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of `funding rate structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + response = await self.v1PublicGetPublicFundingRates(params) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "rows": [{ + # "symbol": "PERP_ETH_USDC", + # "est_funding_rate": 123, + # "est_funding_rate_timestamp": 1683880020000, + # "last_funding_rate": 0.0001, + # "last_funding_rate_timestamp": 1683878400000, + # "next_funding_time": 1683907200000, + # "sum_unitary_funding": 521.367 + # }] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + rows = self.safe_list(data, 'rows', []) + return self.parse_funding_rates(rows, symbols) + + async def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical funding rate prices + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/public/get-funding-rate-history-for-one-market + + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: timestamp in ms of the earliest funding rate to fetch + :param int [limit]: the maximum amount of `funding rate structures ` to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest funding rate + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `funding rate structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchFundingRateHistory', 'paginate') + if paginate: + return await self.fetch_paginated_call_incremental('fetchFundingRateHistory', symbol, since, limit, params, 'page', 25) + request: dict = {} + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + request['symbol'] = market['id'] + if since is not None: + request['start_t'] = since + request, params = self.handle_until_option('end_t', request, params, 0.001) + response = await self.v1PublicGetPublicFundingRateHistory(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "rows": [{ + # "symbol": "PERP_ETH_USDC", + # "funding_rate": 0.0001, + # "funding_rate_timestamp": 1684224000000, + # "next_funding_time": 1684252800000 + # }], + # "meta": { + # "total": 9, + # "records_per_page": 25, + # "current_page": 1 + # } + # } + # } + # + data = self.safe_dict(response, 'data', {}) + result = self.safe_list(data, 'rows', []) + rates = [] + for i in range(0, len(result)): + entry = result[i] + marketId = self.safe_string(entry, 'symbol') + timestamp = self.safe_integer(entry, 'funding_rate_timestamp') + rates.append({ + 'info': entry, + 'symbol': self.safe_symbol(marketId), + 'fundingRate': self.safe_number(entry, 'funding_rate'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + sorted = self.sort_by(rates, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, symbol, since, limit) + + def parse_income(self, income, market: Market = None): + # + # { + # "symbol": "PERP_ETH_USDC", + # "funding_rate": 0.00046875, + # "mark_price": 2100, + # "funding_fee": 0.000016, + # "payment_type": "Pay", + # "status": "Accrued", + # "created_time": 1682235722003, + # "updated_time": 1682235722003 + # } + # + marketId = self.safe_string(income, 'symbol') + symbol = self.safe_symbol(marketId, market) + amount = self.safe_string(income, 'funding_fee') + code = self.safe_currency_code('USDC') + timestamp = self.safe_integer(income, 'updated_time') + rate = self.safe_number(income, 'funding_rate') + paymentType = self.safe_string(income, 'payment_type') + amount = Precise.string_neg(amount) if (paymentType == 'Pay') else amount + return { + 'info': income, + 'symbol': symbol, + 'code': code, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': None, + 'amount': self.parse_number(amount), + 'rate': rate, + } + + async def fetch_funding_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch the history of funding payments paid and received on self account + + https://orderly.network/docs/build-on-omnichain/evm-api/restful-api/private/get-funding-fee-history + + :param str [symbol]: unified market symbol + :param int [since]: the earliest time in ms to fetch funding history for + :param int [limit]: the maximum number of funding history structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict: a `funding history structure ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchFundingHistory', 'paginate') + if paginate: + return await self.fetch_paginated_call_incremental('fetchFundingHistory', symbol, since, limit, params, 'page', 500) + request: dict = {} + market: Market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if since is not None: + request['start_t'] = since + until = self.safe_integer(params, 'until') # unified in milliseconds + params = self.omit(params, ['until']) + if until is not None: + request['end_t'] = until + if limit is not None: + request['size'] = min(limit, 500) + response = await self.v1PrivateGetFundingFeeHistory(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "meta": { + # "total": 9, + # "records_per_page": 25, + # "current_page": 1 + # }, + # "rows": [{ + # "symbol": "PERP_ETH_USDC", + # "funding_rate": 0.00046875, + # "mark_price": 2100, + # "funding_fee": 0.000016, + # "payment_type": "Pay", + # "status": "Accrued", + # "created_time": 1682235722003, + # "updated_time": 1682235722003 + # }] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + rows = self.safe_list(data, 'rows', []) + return self.parse_incomes(rows, market, since, limit) + + async def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-account-information + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + await self.load_markets() + response = await self.v1PrivateGetClientInfo(params) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "account_id": "", + # "email": "test@test.com", + # "account_mode": "FUTURES", + # "max_leverage": 20, + # "taker_fee_rate": 123, + # "maker_fee_rate": 123, + # "futures_taker_fee_rate": 123, + # "futures_maker_fee_rate": 123, + # "maintenance_cancel_orders": True, + # "imr_factor": { + # "PERP_BTC_USDC": 123, + # "PERP_ETH_USDC": 123, + # "PERP_NEAR_USDC": 123 + # }, + # "max_notional": { + # "PERP_BTC_USDC": 123, + # "PERP_ETH_USDC": 123, + # "PERP_NEAR_USDC": 123 + # } + # } + # } + # + data = self.safe_dict(response, 'data', {}) + maker = self.safe_string(data, 'futures_maker_fee_rate') + taker = self.safe_string(data, 'futures_taker_fee_rate') + result: dict = {} + for i in range(0, len(self.symbols)): + symbol = self.symbols[i] + result[symbol] = { + 'info': response, + 'symbol': symbol, + 'maker': self.parse_number(Precise.string_div(maker, '10000')), + 'taker': self.parse_number(Precise.string_div(taker, '10000')), + 'percentage': True, + 'tierBased': True, + } + return result + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/orderbook-snapshot + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + limit = min(limit, 1000) + request['max_level'] = limit + response = await self.v1PrivateGetOrderbookSymbol(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "asks": [{ + # "price": 10669.4, + # "quantity": 1.56263218 + # }], + # "bids": [{ + # "price": 10669.4, + # "quantity": 1.56263218 + # }], + # "timestamp": 123 + # } + # } + # + data = self.safe_dict(response, 'data', {}) + timestamp = self.safe_integer(data, 'timestamp') + return self.parse_order_book(data, symbol, timestamp, 'bids', 'asks', 'price', 'quantity') + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + return [ + self.safe_integer(ohlcv, 'start_timestamp'), + self.safe_number(ohlcv, 'open'), + self.safe_number(ohlcv, 'high'), + self.safe_number(ohlcv, 'low'), + self.safe_number(ohlcv, 'close'), + self.safe_number(ohlcv, 'volume'), + ] + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-kline + + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: max=1000, max=100 when since is defined and is less than(now - (999 * (timeframe in ms))) + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'type': self.safe_string(self.timeframes, timeframe, timeframe), + } + if limit is not None: + request['limit'] = min(limit, 1000) + response = await self.v1PrivateGetKline(self.extend(request, params)) + data = self.safe_dict(response, 'data', {}) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "rows": [{ + # "open": 66166.23, + # "close": 66124.56, + # "low": 66038.06, + # "high": 66176.97, + # "volume": 23.45528526, + # "amount": 1550436.21725288, + # "symbol": "PERP_BTC_USDC", + # "type": "1m", + # "start_timestamp": 1636388220000, + # "end_timestamp": 1636388280000 + # }] + # } + # } + # + rows = self.safe_list(data, 'rows', []) + return self.parse_ohlcvs(rows, market, timeframe, since, limit) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # Possible input functions: + # * createOrder + # * createOrders + # * cancelOrder + # * fetchOrder + # * fetchOrders + # isFromFetchOrder = ('order_tag' in order); TO_DO + # + # stop order after creating it: + # { + # "orderId": "1578938", + # "clientOrderId": "0", + # "algoType": "STOP_LOSS", + # "quantity": "0.1" + # } + # stop order after fetching it: + # { + # "algoOrderId": "1578958", + # "clientOrderId": "0", + # "rootAlgoOrderId": "1578958", + # "parentAlgoOrderId": "0", + # "symbol": "SPOT_LTC_USDT", + # "orderTag": "default", + # "algoType": "STOP_LOSS", + # "side": "BUY", + # "quantity": "0.1", + # "isTriggered": False, + # "triggerPrice": "100", + # "triggerStatus": "USELESS", + # "type": "LIMIT", + # "rootAlgoStatus": "CANCELLED", + # "algoStatus": "CANCELLED", + # "triggerPriceType": "MARKET_PRICE", + # "price": "75", + # "triggerTime": "0", + # "totalExecutedQuantity": "0", + # "averageExecutedPrice": "0", + # "totalFee": "0", + # "feeAsset": '', + # "reduceOnly": False, + # "createdTime": "1686149609.744", + # "updatedTime": "1686149903.362" + # } + # + timestamp = self.safe_integer_n(order, ['timestamp', 'created_time', 'createdTime']) + orderId = self.safe_string_n(order, ['order_id', 'orderId', 'algoOrderId']) + clientOrderId = self.omit_zero(self.safe_string_2(order, 'client_order_id', 'clientOrderId')) # Somehow, self always returns 0 for limit order + marketId = self.safe_string(order, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + price = self.safe_string_2(order, 'order_price', 'price') + amount = self.safe_string_2(order, 'order_quantity', 'quantity') # This is base amount + cost = self.safe_string_2(order, 'order_amount', 'amount') # This is quote amount + orderType = self.safe_string_lower_2(order, 'order_type', 'type') + status = self.safe_value_2(order, 'status', 'algoStatus') + success = self.safe_bool(order, 'success') + if success is not None: + status = 'NEW' if (success) else 'REJECTED' + side = self.safe_string_lower(order, 'side') + filled = self.omit_zero(self.safe_value_2(order, 'executed', 'totalExecutedQuantity')) + average = self.omit_zero(self.safe_string_2(order, 'average_executed_price', 'averageExecutedPrice')) + remaining = Precise.string_sub(cost, filled) + fee = self.safe_value_2(order, 'total_fee', 'totalFee') + feeCurrency = self.safe_string_2(order, 'fee_asset', 'feeAsset') + transactions = self.safe_value(order, 'Transactions') + triggerPrice = self.safe_number(order, 'triggerPrice') + takeProfitPrice: Num = None + stopLossPrice: Num = None + childOrders = self.safe_value(order, 'childOrders') + if childOrders is not None: + first = self.safe_value(childOrders, 0) + innerChildOrders = self.safe_value(first, 'childOrders', []) + innerChildOrdersLength = len(innerChildOrders) + if innerChildOrdersLength > 0: + takeProfitOrder = self.safe_value(innerChildOrders, 0) + stopLossOrder = self.safe_value(innerChildOrders, 1) + takeProfitPrice = self.safe_number(takeProfitOrder, 'triggerPrice') + stopLossPrice = self.safe_number(stopLossOrder, 'triggerPrice') + lastUpdateTimestamp = self.safe_integer_2(order, 'updatedTime', 'updated_time') + return self.safe_order({ + 'id': orderId, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'lastUpdateTimestamp': lastUpdateTimestamp, + 'status': self.parse_order_status(status), + 'symbol': symbol, + 'type': self.parse_order_type(orderType), + 'timeInForce': self.parse_time_in_force(orderType), + 'postOnly': None, # TO_DO + 'reduceOnly': self.safe_bool(order, 'reduce_only'), + 'side': side, + 'price': price, + 'triggerPrice': triggerPrice, + 'takeProfitPrice': takeProfitPrice, + 'stopLossPrice': stopLossPrice, + 'average': average, + 'amount': amount, + 'filled': filled, + 'remaining': remaining, # TO_DO + 'cost': cost, + 'trades': transactions, + 'fee': { + 'cost': fee, + 'currency': feeCurrency, + }, + 'info': order, + }, market) + + def parse_time_in_force(self, timeInForce: Str): + timeInForces: dict = { + 'ioc': 'IOC', + 'fok': 'FOK', + 'post_only': 'PO', + } + return self.safe_string(timeInForces, timeInForce, None) + + def parse_order_status(self, status: Str): + if status is not None: + statuses: dict = { + 'NEW': 'open', + 'FILLED': 'closed', + 'CANCEL_SENT': 'canceled', + 'CANCEL_ALL_SENT': 'canceled', + 'CANCELLED': 'canceled', + 'PARTIAL_FILLED': 'open', + 'REJECTED': 'rejected', + 'INCOMPLETE': 'open', + 'COMPLETED': 'closed', + } + return self.safe_string(statuses, status, status) + return status + + def parse_order_type(self, type: Str): + types: dict = { + 'LIMIT': 'limit', + 'MARKET': 'market', + 'POST_ONLY': 'limit', + } + return self.safe_string_lower(types, type, type) + + def create_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + @ignore + helper function to build the request + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency + :param float [price]: the price that the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: request to be sent to the exchange + """ + reduceOnly = self.safe_bool_2(params, 'reduceOnly', 'reduce_only') + orderType = type.upper() + market = self.market(symbol) + orderSide = side.upper() + request: dict = { + 'symbol': market['id'], + 'side': orderSide, + } + triggerPrice = self.safe_string_2(params, 'triggerPrice', 'stopPrice') + stopLoss = self.safe_value(params, 'stopLoss') + takeProfit = self.safe_value(params, 'takeProfit') + algoType = self.safe_string(params, 'algoType') + isConditional = triggerPrice is not None or stopLoss is not None or takeProfit is not None or (self.safe_value(params, 'childOrders') is not None) + isMarket = orderType == 'MARKET' + timeInForce = self.safe_string_lower(params, 'timeInForce') + postOnly = self.is_post_only(isMarket, None, params) + orderQtyKey = 'quantity' if isConditional else 'order_quantity' + priceKey = 'price' if isConditional else 'order_price' + typeKey = 'type' if isConditional else 'order_type' + request[typeKey] = orderType # LIMIT/MARKET/IOC/FOK/POST_ONLY/ASK/BID + if not isConditional: + if postOnly: + request['order_type'] = 'POST_ONLY' + elif timeInForce == 'fok': + request['order_type'] = 'FOK' + elif timeInForce == 'ioc': + request['order_type'] = 'IOC' + if reduceOnly: + request['reduce_only'] = reduceOnly + if price is not None: + request[priceKey] = self.price_to_precision(symbol, price) + if isMarket and not isConditional: + request[orderQtyKey] = self.amount_to_precision(symbol, amount) + elif algoType != 'POSITIONAL_TP_SL': + request[orderQtyKey] = self.amount_to_precision(symbol, amount) + clientOrderId = self.safe_string_n(params, ['clOrdID', 'clientOrderId', 'client_order_id']) + if clientOrderId is not None: + request['client_order_id'] = clientOrderId + if triggerPrice is not None: + request['trigger_price'] = self.price_to_precision(symbol, triggerPrice) + request['algo_type'] = 'STOP' + elif (stopLoss is not None) or (takeProfit is not None): + request['algo_type'] = 'TP_SL' + outterOrder: dict = { + 'symbol': market['id'], + 'reduce_only': False, + 'algo_type': 'POSITIONAL_TP_SL', + 'child_orders': [], + } + childOrders = outterOrder['child_orders'] + closeSide = 'SELL' if (orderSide == 'BUY') else 'BUY' + if stopLoss is not None: + stopLossPrice = self.safe_number_2(stopLoss, 'triggerPrice', 'price', stopLoss) + stopLossOrder: dict = { + 'side': closeSide, + 'algo_type': 'TP_SL', + 'trigger_price': self.price_to_precision(symbol, stopLossPrice), + 'type': 'LIMIT', + 'reduce_only': True, + } + childOrders.append(stopLossOrder) + if takeProfit is not None: + takeProfitPrice = self.safe_number_2(takeProfit, 'triggerPrice', 'price', takeProfit) + takeProfitOrder: dict = { + 'side': closeSide, + 'algo_type': 'TP_SL', + 'trigger_price': self.price_to_precision(symbol, takeProfitPrice), + 'type': 'LIMIT', + 'reduce_only': True, + } + outterOrder.append(takeProfitOrder) + request['child_orders'] = [outterOrder] + params = self.omit(params, ['reduceOnly', 'reduce_only', 'clOrdID', 'clientOrderId', 'client_order_id', 'postOnly', 'timeInForce', 'stopPrice', 'triggerPrice', 'stopLoss', 'takeProfit']) + return self.extend(request, params) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/create-order + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/create-algo-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: The price a trigger order is triggered at + :param dict [params.takeProfit]: *takeProfit object in params* containing the triggerPrice at which the attached take profit order will be triggered(perpetual swap markets only) + :param float [params.takeProfit.triggerPrice]: take profit trigger price + :param dict [params.stopLoss]: *stopLoss object in params* containing the triggerPrice at which the attached stop loss order will be triggered(perpetual swap markets only) + :param float [params.stopLoss.triggerPrice]: stop loss trigger price + :param float [params.algoType]: 'STOP'or 'TP_SL' or 'POSITIONAL_TP_SL' + :param float [params.cost]: *spot market buy only* the quote quantity that can be used alternative for the amount + :param str [params.clientOrderId]: a unique id for the order + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + request = self.create_order_request(symbol, type, side, amount, price, params) + triggerPrice = self.safe_string_2(params, 'triggerPrice', 'stopPrice') + stopLoss = self.safe_value(params, 'stopLoss') + takeProfit = self.safe_value(params, 'takeProfit') + isConditional = triggerPrice is not None or stopLoss is not None or takeProfit is not None or (self.safe_value(params, 'childOrders') is not None) + response = None + if isConditional: + response = await self.v1PrivatePostAlgoOrder(request) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "order_id": 13, + # "client_order_id": "testclientid", + # "algo_type": "STOP", + # "quantity": 100.12 + # } + # } + # + else: + response = await self.v1PrivatePostOrder(request) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "order_id": 13, + # "client_order_id": "testclientid", + # "order_type": "LIMIT", + # "order_price": 100.12, + # "order_quantity": 0.987654, + # "order_amount": 0.8, + # "error_message": "none" + # } + # } + # + data = self.safe_dict(response, 'data') + data['timestamp'] = self.safe_integer(response, 'timestamp') + order = self.parse_order(data, market) + order['type'] = type + return order + + async def create_orders(self, orders: List[OrderRequest], params={}): + """ + *contract only* create a list of trade orders + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/batch-create-order + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + ordersRequests = [] + for i in range(0, len(orders)): + rawOrder = orders[i] + marketId = self.safe_string(rawOrder, 'symbol') + type = self.safe_string(rawOrder, 'type') + side = self.safe_string(rawOrder, 'side') + amount = self.safe_value(rawOrder, 'amount') + price = self.safe_value(rawOrder, 'price') + orderParams = self.safe_dict(rawOrder, 'params', {}) + triggerPrice = self.safe_string_2(orderParams, 'triggerPrice', 'stopPrice') + stopLoss = self.safe_value(orderParams, 'stopLoss') + takeProfit = self.safe_value(orderParams, 'takeProfit') + isConditional = triggerPrice is not None or stopLoss is not None or takeProfit is not None or (self.safe_value(orderParams, 'childOrders') is not None) + if isConditional: + raise NotSupported(self.id + ' createOrders() only support non-stop order') + orderRequest = self.create_order_request(marketId, type, side, amount, price, orderParams) + ordersRequests.append(orderRequest) + request: dict = { + 'orders': ordersRequests, + } + response = await self.v1PrivatePostBatchOrder(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "rows": [{ + # "order_id": 13, + # "client_order_id": "testclientid", + # "order_type": "LIMIT", + # "order_price": 100.12, + # "order_quantity": 0.987654, + # "order_amount": 0.8, + # "error_message": "none" + # }] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + rows = self.safe_list(data, 'rows', []) + return self.parse_orders(rows) + + async def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + """ + edit a trade order + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/edit-order + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/edit-algo-order + + :param str id: order id + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: The price a trigger order is triggered at + :param float [params.stopLossPrice]: price to trigger stop-loss orders + :param float [params.takeProfitPrice]: price to trigger take-profit orders + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'order_id': id, + } + triggerPrice = self.safe_string_n(params, ['triggerPrice', 'stopPrice', 'takeProfitPrice', 'stopLossPrice']) + if triggerPrice is not None: + request['triggerPrice'] = self.price_to_precision(symbol, triggerPrice) + isConditional = (triggerPrice is not None) or (self.safe_value(params, 'childOrders') is not None) + orderQtyKey = 'quantity' if isConditional else 'order_quantity' + priceKey = 'price' if isConditional else 'order_price' + if price is not None: + request[priceKey] = self.price_to_precision(symbol, price) + if amount is not None: + request[orderQtyKey] = self.amount_to_precision(symbol, amount) + params = self.omit(params, ['stopPrice', 'triggerPrice', 'takeProfitPrice', 'stopLossPrice', 'trailingTriggerPrice', 'trailingAmount', 'trailingPercent']) + response = None + if isConditional: + response = await self.v1PrivatePutAlgoOrder(self.extend(request, params)) + else: + request['symbol'] = market['id'] + request['side'] = side.upper() + orderType = type.upper() + timeInForce = self.safe_string_lower(params, 'timeInForce') + isMarket = orderType == 'MARKET' + postOnly = self.is_post_only(isMarket, None, params) + if postOnly: + request['order_type'] = 'POST_ONLY' + elif timeInForce == 'fok': + request['order_type'] = 'FOK' + elif timeInForce == 'ioc': + request['order_type'] = 'IOC' + else: + request['order_type'] = orderType + clientOrderId = self.safe_string_n(params, ['clOrdID', 'clientOrderId', 'client_order_id']) + params = self.omit(params, ['clOrdID', 'clientOrderId', 'client_order_id', 'postOnly', 'timeInForce']) + if clientOrderId is not None: + request['client_order_id'] = clientOrderId + # request['side'] = side.upper() + # request['symbol'] = market['id'] + response = await self.v1PrivatePutOrder(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "status": "EDIT_SENT" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + data['timestamp'] = self.safe_integer(response, 'timestamp') + return self.parse_order(data, market) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/cancel-order + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/cancel-order-by-client_order_id + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/cancel-algo-order + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/cancel-algo-order-by-client_order_id + + cancels an open order + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: whether the order is a stop/algo order + :param str [params.clientOrderId]: a unique id for the order + :returns dict: An `order structure ` + """ + trigger = self.safe_bool_2(params, 'stop', 'trigger', False) + params = self.omit(params, ['stop', 'trigger']) + if not trigger and (symbol is None): + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + await self.load_markets() + market: Market = None + if symbol is not None: + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + clientOrderIdUnified = self.safe_string_2(params, 'clOrdID', 'clientOrderId') + clientOrderIdExchangeSpecific = self.safe_string(params, 'client_order_id', clientOrderIdUnified) + isByClientOrder = clientOrderIdExchangeSpecific is not None + response = None + if trigger: + if isByClientOrder: + request['client_order_id'] = clientOrderIdExchangeSpecific + params = self.omit(params, ['clOrdID', 'clientOrderId', 'client_order_id']) + response = await self.v1PrivateDeleteAlgoClientOrder(self.extend(request, params)) + else: + request['order_id'] = id + response = await self.v1PrivateDeleteAlgoOrder(self.extend(request, params)) + else: + if isByClientOrder: + request['client_order_id'] = clientOrderIdExchangeSpecific + params = self.omit(params, ['clOrdID', 'clientOrderId', 'client_order_id']) + response = await self.v1PrivateDeleteClientOrder(self.extend(request, params)) + else: + request['order_id'] = id + response = await self.v1PrivateDeleteOrder(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "status": "CANCEL_SENT" + # } + # } + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "status": "CANCEL_SENT" + # } + # + extendParams: dict = {'symbol': symbol} + if isByClientOrder: + extendParams['client_order_id'] = clientOrderIdExchangeSpecific + else: + extendParams['id'] = id + if trigger: + return self.extend(self.parse_order(response), extendParams) + data = self.safe_dict(response, 'data', {}) + return self.extend(self.parse_order(data), extendParams) + + async def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): + """ + cancel multiple orders + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/batch-cancel-orders + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/batch-cancel-orders-by-client_order_id + + :param str[] ids: order ids + :param str [symbol]: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str[] [params.client_order_ids]: max length 10 e.g. ["my_id_1","my_id_2"], encode the double quotes. No space after comma + :returns dict: an list of `order structures ` + """ + await self.load_markets() + clientOrderIds = self.safe_list_n(params, ['clOrdIDs', 'clientOrderIds', 'client_order_ids']) + params = self.omit(params, ['clOrdIDs', 'clientOrderIds', 'client_order_ids']) + request: dict = {} + response = None + if clientOrderIds: + request['client_order_ids'] = ','.join(clientOrderIds) + response = await self.v1PrivateDeleteClientBatchOrder(self.extend(request, params)) + else: + request['order_ids'] = ','.join(ids) + response = await self.v1PrivateDeleteBatchOrder(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "status": "CANCEL_ALL_SENT" + # } + # } + # + return [self.safe_order({ + 'info': response, + })] + + async def cancel_all_orders(self, symbol: Str = None, params={}): + """ + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/cancel-all-pending-algo-orders + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/cancel-orders-in-bulk + + cancel all open orders in a market + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: whether the order is a stop/algo order + :returns dict: an list of `order structures ` + """ + await self.load_markets() + trigger = self.safe_bool_2(params, 'stop', 'trigger') + params = self.omit(params, ['stop', 'trigger']) + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + response = None + if trigger: + response = await self.v1PrivateDeleteAlgoOrders(self.extend(request, params)) + else: + response = await self.v1PrivateDeleteOrders(self.extend(request, params)) + # trigger + # { + # "success": True, + # "timestamp": 1702989203989, + # "status": "CANCEL_ALL_SENT" + # } + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "status": "CANCEL_ALL_SENT" + # } + # } + # + return [ + self.safe_order({ + 'info': response, + }), + ] + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-order-by-order_id + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-order-by-client_order_id + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-algo-order-by-order_id + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-algo-order-by-client_order_id + + fetches information on an order made by the user + :param str id: the order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: whether the order is a stop/algo order + :param str [params.clientOrderId]: a unique id for the order + :returns dict: An `order structure ` + """ + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + trigger = self.safe_bool_2(params, 'stop', 'trigger', False) + request: dict = {} + clientOrderId = self.safe_string_n(params, ['clOrdID', 'clientOrderId', 'client_order_id']) + params = self.omit(params, ['stop', 'trigger', 'clOrdID', 'clientOrderId', 'client_order_id']) + response = None + if trigger: + if clientOrderId: + request['client_order_id'] = clientOrderId + response = await self.v1PrivateGetAlgoClientOrderClientOrderId(self.extend(request, params)) + else: + request['oid'] = id + response = await self.v1PrivateGetAlgoOrderOid(self.extend(request, params)) + else: + if clientOrderId: + request['client_order_id'] = clientOrderId + response = await self.v1PrivateGetClientOrderClientOrderId(self.extend(request, params)) + else: + request['oid'] = id + response = await self.v1PrivateGetOrderOid(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "order_id": 78151, + # "user_id": 12345, + # "price": 0.67772, + # "type": "LIMIT", + # "quantity": 20, + # "amount": 10, + # "executed_quantity": 20, + # "total_executed_quantity": 20, + # "visible_quantity": 1, + # "symbol": "PERP_WOO_USDC", + # "side": "BUY", + # "status": "FILLED", + # "total_fee": 0.5, + # "fee_asset": "WOO", + # "client_order_id": 1, + # "average_executed_price": 0.67772, + # "created_time": 1653563963000, + # "updated_time": 1653564213000, + # "realized_pnl": 123 + # } + # } + # + orders = self.safe_dict(response, 'data', response) + return self.parse_order(orders, market) + + async def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-orders + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-algo-orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: whether the order is a stop/algo order + :param boolean [params.is_triggered]: whether the order has been triggered(False by default) + :param str [params.side]: 'buy' or 'sell' + :param boolean [params.paginate]: set to True if you want to fetch orders with pagination + :param int params['until']: timestamp in ms of the latest order to fetch + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + paginate = False + isTrigger = self.safe_bool_2(params, 'stop', 'trigger', False) + maxLimit = 100 if (isTrigger) else 500 + paginate, params = self.handle_option_and_params(params, 'fetchOrders', 'paginate') + if paginate: + return await self.fetch_paginated_call_incremental('fetchOrders', symbol, since, limit, params, 'page', maxLimit) + request: dict = {} + market: Market = None + params = self.omit(params, ['stop', 'trigger']) + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if since is not None: + request['start_t'] = since + if limit is not None: + request['size'] = limit + else: + request['size'] = maxLimit + if isTrigger: + request['algo_type'] = 'STOP' + request, params = self.handle_until_option('end_t', request, params) + response = None + if isTrigger: + response = await self.v1PrivateGetAlgoOrders(self.extend(request, params)) + else: + response = await self.v1PrivateGetOrders(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "meta": { + # "total": 9, + # "records_per_page": 25, + # "current_page": 1 + # }, + # "rows": [{ + # "order_id": 78151, + # "user_id": 12345, + # "price": 0.67772, + # "type": "LIMIT", + # "quantity": 20, + # "amount": 10, + # "executed_quantity": 20, + # "total_executed_quantity": 20, + # "visible_quantity": 1, + # "symbol": "PERP_WOO_USDC", + # "side": "BUY", + # "status": "FILLED", + # "total_fee": 0.5, + # "fee_asset": "WOO", + # "client_order_id": 1, + # "average_executed_price": 0.67772, + # "created_time": 1653563963000, + # "updated_time": 1653564213000, + # "realized_pnl": 123 + # }] + # } + # } + # + data = self.safe_value(response, 'data', response) + orders = self.safe_list(data, 'rows') + return self.parse_orders(orders, market, since, limit) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-orders + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-algo-orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: whether the order is a stop/algo order + :param boolean [params.is_triggered]: whether the order has been triggered(False by default) + :param str [params.side]: 'buy' or 'sell' + :param int params['until']: timestamp in ms of the latest order to fetch + :param boolean [params.paginate]: set to True if you want to fetch orders with pagination + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + extendedParams = self.extend(params, {'status': 'INCOMPLETE'}) + return await self.fetch_orders(symbol, since, limit, extendedParams) + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-orders + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-algo-orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: whether the order is a stop/algo order + :param boolean [params.is_triggered]: whether the order has been triggered(False by default) + :param str [params.side]: 'buy' or 'sell' + :param int params['until']: timestamp in ms of the latest order to fetch + :param boolean [params.paginate]: set to True if you want to fetch orders with pagination + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + extendedParams = self.extend(params, {'status': 'COMPLETED'}) + return await self.fetch_orders(symbol, since, limit, extendedParams) + + async def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all the trades made from a single order + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-all-trades-of-specific-order + + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + market: Market = None + if symbol is not None: + market = self.market(symbol) + request: dict = { + 'oid': id, + } + response = await self.v1PrivateGetOrderOidTrades(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "rows": [{ + # "id": 2, + # "symbol": "PERP_BTC_USDC", + # "fee": 0.0001, + # "fee_asset": "USDC", + # "side": "BUY", + # "order_id": 1, + # "executed_price": 123, + # "executed_quantity": 0.05, + # "executed_timestamp": 1567382401000, + # "is_maker": 1, + # "realized_pnl": 123 + # }] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + trades = self.safe_list(data, 'rows', []) + return self.parse_trades(trades, market, since, limit, params) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-trades + + fetch all trades made by the user + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: set to True if you want to fetch trades with pagination + :param int params['until']: timestamp in ms of the latest trade to fetch + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchMyTrades', 'paginate') + if paginate: + return await self.fetch_paginated_call_incremental('fetchMyTrades', symbol, since, limit, params, 'page', 500) + request: dict = {} + market: Market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if since is not None: + request['start_t'] = since + if limit is not None: + request['size'] = limit + else: + request['size'] = 500 + request, params = self.handle_until_option('end_t', request, params) + response = await self.v1PrivateGetTrades(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "meta": { + # "total": 9, + # "records_per_page": 25, + # "current_page": 1 + # }, + # "rows": [{ + # "id": 2, + # "symbol": "PERP_BTC_USDC", + # "fee": 0.0001, + # "fee_asset": "USDC", + # "side": "BUY", + # "order_id": 1, + # "executed_price": 123, + # "executed_quantity": 0.05, + # "executed_timestamp": 1567382401000, + # "is_maker": 1, + # "realized_pnl": 123 + # }] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + trades = self.safe_list(data, 'rows', []) + return self.parse_trades(trades, market, since, limit, params) + + def parse_balance(self, response) -> Balances: + result: dict = { + 'info': response, + } + balances = self.safe_list(response, 'holding', []) + for i in range(0, len(balances)): + balance = balances[i] + code = self.safe_currency_code(self.safe_string(balance, 'token')) + account = self.account() + account['total'] = self.safe_string(balance, 'holding') + account['frozen'] = self.safe_string(balance, 'frozen') + result[code] = account + return self.safe_balance(result) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-current-holding + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + response = await self.v1PrivateGetClientHolding(params) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "holding": [{ + # "updated_time": 1580794149000, + # "token": "BTC", + # "holding": -28.000752, + # "frozen": 123, + # "pending_short": -2000 + # }] + # } + # } + # + data = self.safe_dict(response, 'data') + return self.parse_balance(data) + + async def get_asset_history_rows(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> Any: + await self.load_markets() + request: dict = {} + currency: Currency = None + if code is not None: + currency = self.currency(code) + request['balance_token'] = currency['id'] + if since is not None: + request['start_t'] = since + if limit is not None: + request['pageSize'] = limit + transactionType = self.safe_string(params, 'type') + params = self.omit(params, 'type') + if transactionType is not None: + request['type'] = transactionType + response = await self.v1PrivateGetAssetHistory(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "meta": { + # "total": 9, + # "records_per_page": 25, + # "current_page": 1 + # }, + # "rows": [{ + # "id": "230707030600002", + # "tx_id": "0x4b0714c63cc7abae72bf68e84e25860b88ca651b7d27dad1e32bf4c027fa5326", + # "side": "WITHDRAW", + # "token": "USDC", + # "amount": 555, + # "fee": 123, + # "trans_status": "FAILED", + # "created_time": 1688699193034, + # "updated_time": 1688699193096, + # "chain_id": "986532" + # }] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return [currency, self.safe_list(data, 'rows', [])] + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + currencyId = self.safe_string(item, 'token') + code = self.safe_currency_code(currencyId, currency) + currency = self.safe_currency(currencyId, currency) + amount = self.safe_number(item, 'amount') + side = self.safe_string(item, 'token_side') + direction = 'in' if (side == 'DEPOSIT') else 'out' + timestamp = self.safe_integer(item, 'created_time') + fee = self.parse_token_and_fee_temp(item, 'fee_token', 'fee_amount') + return self.safe_ledger_entry({ + 'id': self.safe_string(item, 'id'), + 'currency': code, + 'account': self.safe_string(item, 'account'), + 'referenceAccount': None, + 'referenceId': self.safe_string(item, 'tx_id'), + 'status': self.parse_transaction_status(self.safe_string(item, 'status')), + 'amount': amount, + 'before': None, + 'after': None, + 'fee': fee, + 'direction': direction, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'type': self.parse_ledger_entry_type(self.safe_string(item, 'type')), + 'info': item, + }, currency) + + def parse_ledger_entry_type(self, type): + types: dict = { + 'BALANCE': 'transaction', # Funds moved in/out wallet + 'COLLATERAL': 'transfer', # Funds moved between portfolios + } + return self.safe_string(types, type, type) + + async def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-asset-history + + :param str [code]: unified currency code, default is None + :param int [since]: timestamp in ms of the earliest ledger entry, default is None + :param int [limit]: max number of ledger entries to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ledger structure ` + """ + currencyRows = await self.get_asset_history_rows(code, since, limit, params) + currency = self.safe_value(currencyRows, 0) + rows = self.safe_list(currencyRows, 1) + return self.parse_ledger(rows, currency, since, limit, params) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # example in fetchLedger + code = self.safe_string(transaction, 'token') + movementDirection = self.safe_string_lower(transaction, 'token_side') + if movementDirection == 'withdraw': + movementDirection = 'withdrawal' + fee = self.parse_token_and_fee_temp(transaction, 'fee_token', 'fee_amount') + addressTo = self.safe_string(transaction, 'target_address') + addressFrom = self.safe_string(transaction, 'source_address') + timestamp = self.safe_integer(transaction, 'created_time') + return { + 'info': transaction, + 'id': self.safe_string_2(transaction, 'id', 'withdraw_id'), + 'txid': self.safe_string(transaction, 'tx_id'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'address': None, + 'addressFrom': addressFrom, + 'addressTo': addressTo, + 'tag': self.safe_string(transaction, 'extra'), + 'tagFrom': None, + 'tagTo': None, + 'type': movementDirection, + 'amount': self.safe_number(transaction, 'amount'), + 'currency': code, + 'status': self.parse_transaction_status(self.safe_string(transaction, 'status')), + 'updated': self.safe_integer(transaction, 'updated_time'), + 'comment': None, + 'internal': None, + 'fee': fee, + 'network': None, + } + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'NEW': 'pending', + 'CONFIRMING': 'pending', + 'PROCESSING': 'pending', + 'COMPLETED': 'ok', + 'CANCELED': 'canceled', + } + return self.safe_string(statuses, status, status) + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-asset-history + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + request: dict = { + 'side': 'DEPOSIT', + } + return await self.fetch_deposits_withdrawals(code, since, limit, self.extend(request, params)) + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-asset-history + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + request: dict = { + 'side': 'WITHDRAW', + } + return await self.fetch_deposits_withdrawals(code, since, limit, self.extend(request, params)) + + async def fetch_deposits_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch history of deposits and withdrawals + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-asset-history + + :param str [code]: unified currency code for the currency of the deposit/withdrawals, default is None + :param int [since]: timestamp in ms of the earliest deposit/withdrawal, default is None + :param int [limit]: max number of deposit/withdrawals to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `transaction structure ` + """ + request: dict = {} + currencyRows = await self.get_asset_history_rows(code, since, limit, self.extend(request, params)) + currency = self.safe_value(currencyRows, 0) + rows = self.safe_list(currencyRows, 1) + # + # { + # "rows":[], + # "meta":{ + # "total":0, + # "records_per_page":25, + # "current_page":1 + # }, + # "success":true + # } + # + return self.parse_transactions(rows, currency, since, limit, params) + + async def get_withdraw_nonce(self, params={}): + response = await self.v1PrivateGetWithdrawNonce(params) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "withdraw_nonce": 1 + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.safe_number(data, 'withdraw_nonce') + + def hash_message(self, message): + return '0x' + self.hash(message, 'keccak', 'hex') + + def sign_hash(self, hash, privateKey): + signature = self.ecdsa(hash[-64:], privateKey[-64:], 'secp256k1', None) + r = signature['r'] + s = signature['s'] + v = self.int_to_base16(self.sum(27, signature['v'])) + return '0x' + r.rjust(64, '0') + s.rjust(64, '0') + v + + def sign_message(self, message, privateKey): + return self.sign_hash(self.hash_message(message), privateKey[-64:]) + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/create-withdraw-request + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + await self.load_markets() + self.check_address(address) + if code is not None: + code = code.upper() + if code != 'USDC': + raise NotSupported(self.id + ' withdraw() only support USDC') + currency = self.currency(code) + verifyingContractAddress = self.safe_string(self.options, 'verifyingContractAddress') + chainId = self.safe_string(params, 'chainId') + currencyNetworks = self.safe_dict(currency, 'networks', {}) + coinNetwork = self.safe_dict(currencyNetworks, chainId, {}) + coinNetworkId = self.safe_number(coinNetwork, 'id') + if coinNetworkId is None: + raise BadRequest(self.id + ' withdraw() require chainId parameter') + withdrawNonce = await self.get_withdraw_nonce(params) + nonce = self.nonce() + domain: dict = { + 'chainId': chainId, + 'name': 'Orderly', + 'verifyingContract': verifyingContractAddress, + 'version': '1', + } + messageTypes: dict = { + 'Withdraw': [ + {'name': 'brokerId', 'type': 'string'}, + {'name': 'chainId', 'type': 'uint256'}, + {'name': 'receiver', 'type': 'address'}, + {'name': 'token', 'type': 'string'}, + {'name': 'amount', 'type': 'uint256'}, + {'name': 'withdrawNonce', 'type': 'uint64'}, + {'name': 'timestamp', 'type': 'uint64'}, + ], + } + withdrawRequest: dict = { + 'brokerId': self.safe_string(self.options, 'keyBrokerId', 'woofi_pro'), + 'chainId': self.parse_to_int(chainId), + 'receiver': address, + 'token': code, + 'amount': str(amount), + 'withdrawNonce': withdrawNonce, + 'timestamp': nonce, + } + msg = self.eth_encode_structured_data(domain, messageTypes, withdrawRequest) + signature = self.sign_message(msg, self.privateKey) + request: dict = { + 'signature': signature, + 'userAddress': address, + 'verifyingContract': verifyingContractAddress, + 'message': withdrawRequest, + } + params = self.omit(params, 'chainId') + response = await self.v1PrivatePostWithdrawRequest(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "withdraw_id": 123 + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_transaction(data, currency) + + def parse_leverage(self, leverage, market=None) -> Leverage: + leverageValue = self.safe_integer(leverage, 'max_leverage') + return { + 'info': leverage, + 'symbol': market['symbol'], + 'marginMode': None, + 'longLeverage': leverageValue, + 'shortLeverage': leverageValue, + } + + async def fetch_leverage(self, symbol: str, params={}) -> Leverage: + """ + fetch the set leverage for a market + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-account-information + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `leverage structure ` + """ + await self.load_markets() + market = self.market(symbol) + response = await self.v1PrivateGetClientInfo(params) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "account_id": "", + # "email": "test@test.com", + # "account_mode": "FUTURES", + # "max_leverage": 20, + # "taker_fee_rate": 123, + # "maker_fee_rate": 123, + # "futures_taker_fee_rate": 123, + # "futures_maker_fee_rate": 123, + # "maintenance_cancel_orders": True, + # "imr_factor": { + # "PERP_BTC_USDC": 123, + # "PERP_ETH_USDC": 123, + # "PERP_NEAR_USDC": 123 + # }, + # "max_notional": { + # "PERP_BTC_USDC": 123, + # "PERP_ETH_USDC": 123, + # "PERP_NEAR_USDC": 123 + # } + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_leverage(data, market) + + async def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/update-leverage-setting + + :param int [leverage]: the rate of leverage + :param str [symbol]: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + await self.load_markets() + if (leverage < 1) or (leverage > 50): + raise BadRequest(self.id + ' leverage should be between 1 and 50') + request: dict = { + 'leverage': leverage, + } + return await self.v1PrivatePostClientLeverage(self.extend(request, params)) + + def parse_position(self, position: dict, market: Market = None): + # + # { + # "IMR_withdraw_orders": 0.1, + # "MMR_with_orders": 0.05, + # "average_open_price": 27908.14386047, + # "cost_position": -139329.358492, + # "est_liq_price": 117335.92899428, + # "fee_24_h": 123, + # "imr": 0.1, + # "last_sum_unitary_funding": 70.38, + # "mark_price": 27794.9, + # "mmr": 0.05, + # "pending_long_qty": 123, + # "pending_short_qty": 123, + # "pnl_24_h": 123, + # "position_qty": -5, + # "settle_price": 27865.8716984, + # "symbol": "PERP_BTC_USDC", + # "timestamp": 1685429350571, + # "unsettled_pnl": 354.858492 + # } + # + contract = self.safe_string(position, 'symbol') + market = self.safe_market(contract, market) + size = self.safe_string(position, 'position_qty') + side: Str = None + if Precise.string_gt(size, '0'): + side = 'long' + else: + side = 'short' + contractSize = self.safe_string(market, 'contractSize') + markPrice = self.safe_string(position, 'mark_price') + timestamp = self.safe_integer(position, 'timestamp') + entryPrice = self.safe_string(position, 'average_open_price') + unrealisedPnl = self.safe_string(position, 'unsettled_pnl') + size = Precise.string_abs(size) + notional = Precise.string_mul(size, markPrice) + return self.safe_position({ + 'info': position, + 'id': None, + 'symbol': self.safe_string(market, 'symbol'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastUpdateTimestamp': None, + 'initialMargin': None, + 'initialMarginPercentage': None, + 'maintenanceMargin': None, + 'maintenanceMarginPercentage': None, + 'entryPrice': self.parse_number(entryPrice), + 'notional': self.parse_number(notional), + 'leverage': None, + 'unrealizedPnl': self.parse_number(unrealisedPnl), + 'contracts': self.parse_number(size), + 'contractSize': self.parse_number(contractSize), + 'marginRatio': None, + 'liquidationPrice': self.safe_number(position, 'est_liq_price'), + 'markPrice': self.parse_number(markPrice), + 'lastPrice': None, + 'collateral': None, + 'marginMode': 'cross', + 'marginType': None, + 'side': side, + 'percentage': None, + 'hedged': None, + 'stopLossPrice': None, + 'takeProfitPrice': None, + }) + + async def fetch_position(self, symbol: Str, params={}): + """ + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-one-position-info + + fetch data on an open position + :param str symbol: unified market symbol of the market the position is held in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `position structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = await self.v1PrivateGetPositionSymbol(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "IMR_withdraw_orders": 0.1, + # "MMR_with_orders": 0.05, + # "average_open_price": 27908.14386047, + # "cost_position": -139329.358492, + # "est_liq_price": 117335.92899428, + # "fee_24_h": 123, + # "imr": 0.1, + # "last_sum_unitary_funding": 70.38, + # "mark_price": 27794.9, + # "mmr": 0.05, + # "pending_long_qty": 123, + # "pending_short_qty": 123, + # "pnl_24_h": 123, + # "position_qty": -5, + # "settle_price": 27865.8716984, + # "symbol": "PERP_BTC_USDC", + # "timestamp": 1685429350571, + # "unsettled_pnl": 354.858492 + # } + # } + # + data = self.safe_dict(response, 'data') + return self.parse_position(data, market) + + async def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-all-positions-info + + :param str[] [symbols]: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + await self.load_markets() + response = await self.v1PrivateGetPositions(params) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "current_margin_ratio_with_orders": 1.2385, + # "free_collateral": 450315.09115, + # "initial_margin_ratio": 0.1, + # "initial_margin_ratio_with_orders": 0.1, + # "maintenance_margin_ratio": 0.05, + # "maintenance_margin_ratio_with_orders": 0.05, + # "margin_ratio": 1.2385, + # "open_margin_ratio": 1.2102, + # "total_collateral_value": 489865.71329, + # "total_pnl_24_h": 123, + # "rows": [{ + # "IMR_withdraw_orders": 0.1, + # "MMR_with_orders": 0.05, + # "average_open_price": 27908.14386047, + # "cost_position": -139329.358492, + # "est_liq_price": 117335.92899428, + # "fee_24_h": 123, + # "imr": 0.1, + # "last_sum_unitary_funding": 70.38, + # "mark_price": 27794.9, + # "mmr": 0.05, + # "pending_long_qty": 123, + # "pending_short_qty": 123, + # "pnl_24_h": 123, + # "position_qty": -5, + # "settle_price": 27865.8716984, + # "symbol": "PERP_BTC_USDC", + # "timestamp": 1685429350571, + # "unsettled_pnl": 354.858492 + # }] + # } + # } + # + result = self.safe_dict(response, 'data', {}) + positions = self.safe_list(result, 'rows', []) + return self.parse_positions(positions, symbols) + + def nonce(self): + return self.milliseconds() + + def sign(self, path, section='public', method='GET', params={}, headers=None, body=None): + version = section[0] + access = section[1] + pathWithParams = self.implode_params(path, params) + url = self.implode_hostname(self.urls['api'][access]) + url += '/' + version + '/' + params = self.omit(params, self.extract_params(path)) + params = self.keysort(params) + if access == 'public': + url += pathWithParams + if params: + url += '?' + self.urlencode(params) + else: + self.check_required_credentials() + if (method == 'POST' or method == 'PUT') and (path == 'algo/order' or path == 'order' or path == 'batch-order'): + isSandboxMode = self.safe_bool(self.options, 'sandboxMode', False) + if not isSandboxMode: + brokerId = self.safe_string(self.options, 'brokerId', 'CCXT') + if path == 'batch-order': + ordersList = self.safe_list(params, 'orders', []) + for i in range(0, len(ordersList)): + params['orders'][i]['order_tag'] = brokerId + else: + params['order_tag'] = brokerId + params = self.keysort(params) + auth = '' + ts = str(self.nonce()) + url += pathWithParams + apiKey = self.apiKey + if apiKey.find('ed25519:') < 0: + apiKey = 'ed25519:' + apiKey + headers = { + 'orderly-account-id': self.accountId, + 'orderly-key': apiKey, + 'orderly-timestamp': ts, + } + auth = ts + method + '/' + version + '/' + pathWithParams + if method == 'POST' or method == 'PUT': + body = self.json(params) + auth += body + headers['content-type'] = 'application/json' + else: + if params: + url += '?' + self.urlencode(params) + auth += '?' + self.rawencode(params) + headers['content-type'] = 'application/x-www-form-urlencoded' + if method == 'DELETE': + body = '' + secret = self.secret + if secret.find('ed25519:') >= 0: + parts = secret.split('ed25519:') + secret = parts[1] + signature = self.eddsa(self.encode(auth), self.base58_to_binary(secret), 'ed25519') + headers['orderly-signature'] = self.urlencode_base64(self.base64_to_binary(signature)) + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if not response: + return None # fallback to default error handler + # + # 400 Bad Request {"success":false,"code":-1012,"message":"Amount is required for buy market orders when margin disabled."} + # {"code":"-1011","message":"The system is under maintenance.","success":false} + # + success = self.safe_bool(response, 'success') + errorCode = self.safe_string(response, 'code') + if not success: + feedback = self.id + ' ' + self.json(response) + self.throw_broadly_matched_exception(self.exceptions['broad'], body, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + raise ExchangeError(feedback) + return None diff --git a/ccxt/async_support/xt.py b/ccxt/async_support/xt.py new file mode 100644 index 0000000..9c6fbe6 --- /dev/null +++ b/ccxt/async_support/xt.py @@ -0,0 +1,4917 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.xt import ImplicitAPI +import asyncio +import hashlib +from ccxt.base.types import Any, Currencies, Currency, DepositAddress, Int, LedgerEntry, LeverageTier, LeverageTiers, MarginModification, Market, Num, Order, OrderSide, OrderType, Position, Str, Tickers, FundingRate, Transaction, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import NotSupported +from ccxt.base.errors import NetworkError +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import OnMaintenance +from ccxt.base.errors import RequestTimeout +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class xt(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(xt, self).describe(), { + 'id': 'xt', + 'name': 'XT', + 'countries': ['SC'], # Seychelles + # spot api ratelimits are None, 10/s/ip, 50/s/ip, 100/s/ip or 200/s/ip + # futures 3 requests per second => 1000ms / (100 * 3.33) = 3.003(get assets -> fetchMarkets & fetchCurrencies) + # futures 10 requests per second => 1000ms / (100 * 1) = 10(all other) + # futures 1000 times per minute for each single IP -> Otherwise account locked for 10min + 'rateLimit': 100, + 'version': 'v4', + 'certified': False, + 'pro': True, + 'has': { + 'CORS': False, + 'spot': True, + 'margin': True, + 'swap': True, + 'future': True, + 'option': False, + 'addMargin': True, + 'borrowMargin': False, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'cancelOrders': True, + 'createDepositAddress': False, + 'createMarketBuyOrderWithCost': True, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createPostOnlyOrder': False, + 'createReduceOnlyOrder': True, + 'editOrder': True, + 'fetchAccounts': False, + 'fetchBalance': True, + 'fetchBidsAsks': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchCanceledOrders': True, + 'fetchClosedOrders': True, + 'fetchCurrencies': True, + 'fetchDeposit': False, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchDepositWithdrawals': False, + 'fetchDepositWithdrawFee': False, + 'fetchDepositWithdrawFees': False, + 'fetchFundingHistory': True, + 'fetchFundingInterval': True, + 'fetchFundingIntervals': False, + 'fetchFundingRate': True, + 'fetchFundingRateHistory': True, + 'fetchFundingRates': False, + 'fetchIndexOHLCV': False, + 'fetchL3OrderBook': False, + 'fetchLedger': True, + 'fetchLedgerEntry': False, + 'fetchLeverage': False, + 'fetchLeverageTiers': True, + 'fetchMarketLeverageTiers': True, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrderBooks': False, + 'fetchOrders': True, + 'fetchOrdersByStatus': True, + 'fetchOrderTrades': False, + 'fetchPosition': True, + 'fetchPositions': True, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchStatus': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': False, + 'fetchTradingLimits': False, + 'fetchTransactionFee': False, + 'fetchTransactionFees': False, + 'fetchTransactions': False, + 'fetchTransfer': False, + 'fetchTransfers': False, + 'fetchWithdrawal': False, + 'fetchWithdrawals': True, + 'fetchWithdrawalWhitelist': False, + 'reduceMargin': True, + 'repayMargin': False, + 'setLeverage': True, + 'setMargin': False, + 'setMarginMode': True, + 'setPositionMode': False, + 'signIn': False, + 'transfer': True, + 'withdraw': True, + }, + 'precisionMode': TICK_SIZE, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/14319357/232636712-466df2fc-560a-4ca4-aab2-b1d954a58e24.jpg', + 'api': { + 'spot': 'https://sapi.xt.com', + 'linear': 'https://fapi.xt.com', + 'inverse': 'https://dapi.xt.com', + 'user': 'https://api.xt.com', + }, + 'www': 'https://xt.com', + 'referral': 'https://www.xt.com/en/accounts/register?ref=9PTM9VW', + 'doc': [ + 'https://doc.xt.com/', + 'https://github.com/xtpub/api-doc', + ], + 'fees': 'https://www.xt.com/en/rate', + }, + 'api': { + 'public': { + 'spot': { + 'get': { + 'currencies': 1, + 'depth': 10, + 'kline': 1, + 'symbol': 1, # 1 for a single symbol + 'ticker': 1, # 1 for a single symbol + 'ticker/book': 1, # 1 for a single symbol + 'ticker/price': 1, # 1 for a single symbol + 'ticker/24h': 1, # 1 for a single symbol + 'time': 1, + 'trade/history': 1, + 'trade/recent': 1, + 'wallet/support/currency': 1, + }, + }, + 'linear': { + 'get': { + 'future/market/v1/public/contract/risk-balance': 1, + 'future/market/v1/public/contract/open-interest': 1, + 'future/market/v1/public/leverage/bracket/detail': 1, + 'future/market/v1/public/leverage/bracket/list': 1, + 'future/market/v1/public/q/agg-ticker': 1, + 'future/market/v1/public/q/agg-tickers': 1, + 'future/market/v1/public/q/deal': 1, + 'future/market/v1/public/q/depth': 1, + 'future/market/v1/public/q/funding-rate': 1, + 'future/market/v1/public/q/funding-rate-record': 1, + 'future/market/v1/public/q/index-price': 1, + 'future/market/v1/public/q/kline': 1, + 'future/market/v1/public/q/mark-price': 1, + 'future/market/v1/public/q/symbol-index-price': 1, + 'future/market/v1/public/q/symbol-mark-price': 1, + 'future/market/v1/public/q/ticker': 1, + 'future/market/v1/public/q/tickers': 1, + 'future/market/v1/public/symbol/coins': 3.33, + 'future/market/v1/public/symbol/detail': 3.33, + 'future/market/v1/public/symbol/list': 1, + }, + }, + 'inverse': { + 'get': { + 'future/market/v1/public/contract/risk-balance': 1, + 'future/market/v1/public/contract/open-interest': 1, + 'future/market/v1/public/leverage/bracket/detail': 1, + 'future/market/v1/public/leverage/bracket/list': 1, + 'future/market/v1/public/q/agg-ticker': 1, + 'future/market/v1/public/q/agg-tickers': 1, + 'future/market/v1/public/q/deal': 1, + 'future/market/v1/public/q/depth': 1, + 'future/market/v1/public/q/funding-rate': 1, + 'future/market/v1/public/q/funding-rate-record': 1, + 'future/market/v1/public/q/index-price': 1, + 'future/market/v1/public/q/kline': 1, + 'future/market/v1/public/q/mark-price': 1, + 'future/market/v1/public/q/symbol-index-price': 1, + 'future/market/v1/public/q/symbol-mark-price': 1, + 'future/market/v1/public/q/ticker': 1, + 'future/market/v1/public/q/tickers': 1, + 'future/market/v1/public/symbol/coins': 3.33, + 'future/market/v1/public/symbol/detail': 3.33, + 'future/market/v1/public/symbol/list': 1, + }, + }, + }, + 'private': { + 'spot': { + 'get': { + 'balance': 1, + 'balances': 1, + 'batch-order': 1, + 'deposit/address': 1, + 'deposit/history': 1, + 'history-order': 1, + 'open-order': 1, + 'order': 1, + 'order/{orderId}': 1, + 'trade': 1, + 'withdraw/history': 1, + }, + 'post': { + 'order': 0.2, + 'withdraw': 10, + 'balance/transfer': 1, + 'balance/account/transfer': 1, + 'ws-token': 1, + }, + 'delete': { + 'batch-order': 1, + 'open-order': 1, + 'order/{orderId}': 1, + }, + 'put': { + 'order/{orderId}': 1, + }, + }, + 'linear': { + 'get': { + 'future/trade/v1/entrust/plan-detail': 1, + 'future/trade/v1/entrust/plan-list': 1, + 'future/trade/v1/entrust/plan-list-history': 1, + 'future/trade/v1/entrust/profit-detail': 1, + 'future/trade/v1/entrust/profit-list': 1, + 'future/trade/v1/order/detail': 1, + 'future/trade/v1/order/list': 1, + 'future/trade/v1/order/list-history': 1, + 'future/trade/v1/order/trade-list': 1, + 'future/user/v1/account/info': 1, + 'future/user/v1/balance/bills': 1, + 'future/user/v1/balance/detail': 1, + 'future/user/v1/balance/funding-rate-list': 1, + 'future/user/v1/balance/list': 1, + 'future/user/v1/position/adl': 1, + 'future/user/v1/position/list': 1, + 'future/user/v1/user/collection/list': 1, + 'future/user/v1/user/listen-key': 1, + }, + 'post': { + 'future/trade/v1/entrust/cancel-all-plan': 1, + 'future/trade/v1/entrust/cancel-all-profit-stop': 1, + 'future/trade/v1/entrust/cancel-plan': 1, + 'future/trade/v1/entrust/cancel-profit-stop': 1, + 'future/trade/v1/entrust/create-plan': 1, + 'future/trade/v1/entrust/create-profit': 1, + 'future/trade/v1/entrust/update-profit-stop': 1, + 'future/trade/v1/order/cancel': 1, + 'future/trade/v1/order/cancel-all': 1, + 'future/trade/v1/order/create': 1, + 'future/trade/v1/order/create-batch': 1, + 'future/trade/v1/order/update': 1, + 'future/user/v1/account/open': 1, + 'future/user/v1/position/adjust-leverage': 1, + 'future/user/v1/position/auto-margin': 1, + 'future/user/v1/position/close-all': 1, + 'future/user/v1/position/margin': 1, + 'future/user/v1/user/collection/add': 1, + 'future/user/v1/user/collection/cancel': 1, + 'future/user/v1/position/change-type': 1, + }, + }, + 'inverse': { + 'get': { + 'future/trade/v1/entrust/plan-detail': 1, + 'future/trade/v1/entrust/plan-list': 1, + 'future/trade/v1/entrust/plan-list-history': 1, + 'future/trade/v1/entrust/profit-detail': 1, + 'future/trade/v1/entrust/profit-list': 1, + 'future/trade/v1/order/detail': 1, + 'future/trade/v1/order/list': 1, + 'future/trade/v1/order/list-history': 1, + 'future/trade/v1/order/trade-list': 1, + 'future/user/v1/account/info': 1, + 'future/user/v1/balance/bills': 1, + 'future/user/v1/balance/detail': 1, + 'future/user/v1/balance/funding-rate-list': 1, + 'future/user/v1/balance/list': 1, + 'future/user/v1/position/adl': 1, + 'future/user/v1/position/list': 1, + 'future/user/v1/user/collection/list': 1, + 'future/user/v1/user/listen-key': 1, + }, + 'post': { + 'future/trade/v1/entrust/cancel-all-plan': 1, + 'future/trade/v1/entrust/cancel-all-profit-stop': 1, + 'future/trade/v1/entrust/cancel-plan': 1, + 'future/trade/v1/entrust/cancel-profit-stop': 1, + 'future/trade/v1/entrust/create-plan': 1, + 'future/trade/v1/entrust/create-profit': 1, + 'future/trade/v1/entrust/update-profit-stop': 1, + 'future/trade/v1/order/cancel': 1, + 'future/trade/v1/order/cancel-all': 1, + 'future/trade/v1/order/create': 1, + 'future/trade/v1/order/create-batch': 1, + 'future/trade/v1/order/update': 1, + 'future/user/v1/account/open': 1, + 'future/user/v1/position/adjust-leverage': 1, + 'future/user/v1/position/auto-margin': 1, + 'future/user/v1/position/close-all': 1, + 'future/user/v1/position/margin': 1, + 'future/user/v1/user/collection/add': 1, + 'future/user/v1/user/collection/cancel': 1, + }, + }, + 'user': { + 'get': { + 'user/account': 1, + 'user/account/api-key': 1, + }, + 'post': { + 'user/account': 1, + 'user/account/api-key': 1, + }, + 'put': { + 'user/account/api-key': 1, + }, + 'delete': { + 'user/account/{apiKeyId}': 1, + }, + }, + }, + }, + 'fees': { + 'spot': { + 'tierBased': True, + 'percentage': True, + 'maker': self.parse_number('0.002'), + 'taker': self.parse_number('0.002'), + 'tiers': { + 'maker': [ + [self.parse_number('0'), self.parse_number('0.002')], + [self.parse_number('5000'), self.parse_number('0.0018')], + [self.parse_number('10000'), self.parse_number('0.0016')], + [self.parse_number('20000'), self.parse_number('0.0014')], + [self.parse_number('50000'), self.parse_number('0.0012')], + [self.parse_number('150000'), self.parse_number('0.0010')], + [self.parse_number('300000'), self.parse_number('0.0008')], + [self.parse_number('600000'), self.parse_number('0.0007')], + [self.parse_number('1200000'), self.parse_number('0.0006')], + [self.parse_number('2500000'), self.parse_number('0.0005')], + [self.parse_number('6000000'), self.parse_number('0.0004')], + [self.parse_number('15000000'), self.parse_number('0.0003')], + [self.parse_number('30000000'), self.parse_number('0.0002')], + ], + 'taker': [ + [self.parse_number('0'), self.parse_number('0.002')], + [self.parse_number('5000'), self.parse_number('0.0018')], + [self.parse_number('10000'), self.parse_number('0.0016')], + [self.parse_number('20000'), self.parse_number('0.0014')], + [self.parse_number('50000'), self.parse_number('0.0012')], + [self.parse_number('150000'), self.parse_number('0.0010')], + [self.parse_number('300000'), self.parse_number('0.0008')], + [self.parse_number('600000'), self.parse_number('0.0007')], + [self.parse_number('1200000'), self.parse_number('0.0006')], + [self.parse_number('2500000'), self.parse_number('0.0005')], + [self.parse_number('6000000'), self.parse_number('0.0004')], + [self.parse_number('15000000'), self.parse_number('0.0003')], + [self.parse_number('30000000'), self.parse_number('0.0002')], + ], + }, + }, + 'contract': { + 'tierBased': True, + 'percentage': True, + 'maker': self.parse_number('0.0004'), + 'taker': self.parse_number('0.0006'), + 'tiers': { + 'maker': [ + [self.parse_number('0'), self.parse_number('0.0004')], + [self.parse_number('200000'), self.parse_number('0.00038')], + [self.parse_number('1000000'), self.parse_number('0.00036')], + [self.parse_number('5000000'), self.parse_number('0.00034')], + [self.parse_number('10000000'), self.parse_number('0.00032')], + [self.parse_number('15000000'), self.parse_number('0.00028')], + [self.parse_number('30000000'), self.parse_number('0.00024')], + [self.parse_number('50000000'), self.parse_number('0.0002')], + [self.parse_number('100000000'), self.parse_number('0.00016')], + [self.parse_number('300000000'), self.parse_number('0.00012')], + [self.parse_number('500000000'), self.parse_number('0.00008')], + ], + 'taker': [ + [self.parse_number('0'), self.parse_number('0.0006')], + [self.parse_number('200000'), self.parse_number('0.000588')], + [self.parse_number('1000000'), self.parse_number('0.00057')], + [self.parse_number('5000000'), self.parse_number('0.00054')], + [self.parse_number('10000000'), self.parse_number('0.00051')], + [self.parse_number('15000000'), self.parse_number('0.00048')], + [self.parse_number('30000000'), self.parse_number('0.00045')], + [self.parse_number('50000000'), self.parse_number('0.00045')], + [self.parse_number('100000000'), self.parse_number('0.00036')], + [self.parse_number('300000000'), self.parse_number('0.00033')], + [self.parse_number('500000000'), self.parse_number('0.0003')], + ], + }, + }, + }, + 'exceptions': { + 'exact': { + '400': NetworkError, # {"returnCode":1,"msgInfo":"failure","error":{"code":"400","msg":"Connection refused: /10.0.26.71:8080"},"result":null} + '404': ExchangeError, # interface does not exist + '429': RateLimitExceeded, # The request is too frequent, please control the request rate according to the speed limit requirement + '500': ExchangeError, # Service exception + '502': ExchangeError, # Gateway exception + '503': OnMaintenance, # Service unavailable, please try again later + 'AUTH_001': AuthenticationError, # missing request header xt-validate-appkey + 'AUTH_002': AuthenticationError, # missing request header xt-validate-timestamp + 'AUTH_003': AuthenticationError, # missing request header xt-validate-recvwindow + 'AUTH_004': AuthenticationError, # bad request header xt-validate-recvwindow + 'AUTH_005': AuthenticationError, # missing request header xt-validate-algorithms + 'AUTH_006': AuthenticationError, # bad request header xt-validate-algorithms + 'AUTH_007': AuthenticationError, # missing request header xt-validate-signature + 'AUTH_101': AuthenticationError, # ApiKey does not exist + 'AUTH_102': AuthenticationError, # ApiKey is not activated + 'AUTH_103': AuthenticationError, # Signature error, {"rc":1,"mc":"AUTH_103","ma":[],"result":null} + 'AUTH_104': AuthenticationError, # Unbound IP request + 'AUTH_105': AuthenticationError, # outdated message + 'AUTH_106': PermissionDenied, # Exceeded apikey permission + 'SYMBOL_001': BadSymbol, # Symbol not exist + 'SYMBOL_002': BadSymbol, # Symbol offline + 'SYMBOL_003': BadSymbol, # Symbol suspend trading + 'SYMBOL_004': BadSymbol, # Symbol country disallow trading + 'SYMBOL_005': BadSymbol, # The symbol does not support trading via API + 'ORDER_001': InvalidOrder, # Platform rejection + 'ORDER_002': InsufficientFunds, # insufficient funds + 'ORDER_003': InvalidOrder, # Trading Pair Suspended + 'ORDER_004': InvalidOrder, # no transaction + 'ORDER_005': InvalidOrder, # Order not exist + 'ORDER_006': InvalidOrder, # Too many open orders + 'ORDER_007': PermissionDenied, # The sub-account has no transaction authority + 'ORDER_F0101': InvalidOrder, # Trigger Price Filter - Min + 'ORDER_F0102': InvalidOrder, # Trigger Price Filter - Max + 'ORDER_F0103': InvalidOrder, # Trigger Price Filter - Step Value + 'ORDER_F0201': InvalidOrder, # Trigger Quantity Filter - Min + 'ORDER_F0202': InvalidOrder, # Trigger Quantity Filter - Max + 'ORDER_F0203': InvalidOrder, # Trigger Quantity Filter - Step Value + 'ORDER_F0301': InvalidOrder, # Trigger QUOTE_QTY Filter - Min Value + 'ORDER_F0401': InvalidOrder, # Trigger PROTECTION_ONLINE Filter + 'ORDER_F0501': InvalidOrder, # Trigger PROTECTION_LIMIT Filter - Buy Max Deviation + 'ORDER_F0502': InvalidOrder, # Trigger PROTECTION_LIMIT Filter - Sell Max Deviation + 'ORDER_F0601': InvalidOrder, # Trigger PROTECTION_MARKET Filter + 'COMMON_001': ExchangeError, # The user does not exist + 'COMMON_002': ExchangeError, # System busy, please try it later + 'COMMON_003': BadRequest, # Operation failed, please try it later + 'CURRENCY_001': BadRequest, # Information of currency is abnormal + 'DEPOSIT_001': BadRequest, # Deposit is not open + 'DEPOSIT_002': PermissionDenied, # The current account security level is low, please bind any two security verifications in mobile phone/email/Google Authenticator before deposit + 'DEPOSIT_003': BadRequest, # The format of address is incorrect, please enter again + 'DEPOSIT_004': BadRequest, # The address is already exists, please enter again + 'DEPOSIT_005': BadRequest, # Can not find the address of offline wallet + 'DEPOSIT_006': BadRequest, # No deposit address, please try it later + 'DEPOSIT_007': BadRequest, # Address is being generated, please try it later + 'DEPOSIT_008': BadRequest, # Deposit is not available + 'WITHDRAW_001': BadRequest, # Withdraw is not open + 'WITHDRAW_002': BadRequest, # The withdrawal address is invalid + 'WITHDRAW_003': PermissionDenied, # The current account security level is low, please bind any two security verifications in mobile phone/email/Google Authenticator before withdraw + 'WITHDRAW_004': BadRequest, # The withdrawal address is not added + 'WITHDRAW_005': BadRequest, # The withdrawal address cannot be empty + 'WITHDRAW_006': BadRequest, # Memo cannot be empty + 'WITHDRAW_008': PermissionDenied, # Risk control is triggered, withdraw of self currency is not currently supported + 'WITHDRAW_009': PermissionDenied, # Withdraw failed, some hasattr(self, assets) withdraw are restricted by T+1 withdraw + 'WITHDRAW_010': BadRequest, # The precision of withdrawal is invalid + 'WITHDRAW_011': InsufficientFunds, # free balance is not enough + 'WITHDRAW_012': PermissionDenied, # Withdraw failed, your remaining withdrawal limit today is not enough + 'WITHDRAW_013': PermissionDenied, # Withdraw failed, your remaining withdrawal limit today is not enough, the withdrawal amount can be increased by completing a higher level of real-name authentication + 'WITHDRAW_014': BadRequest, # This withdrawal address cannot be used in the internal transfer function, please cancel the internal transfer function before submitting + 'WITHDRAW_015': BadRequest, # The withdrawal amount is not enough to deduct the handling fee + 'WITHDRAW_016': BadRequest, # This withdrawal address is already exists + 'WITHDRAW_017': BadRequest, # This withdrawal has been processed and cannot be canceled + 'WITHDRAW_018': BadRequest, # Memo must be a number + 'WITHDRAW_019': BadRequest, # Memo is incorrect, please enter again + 'WITHDRAW_020': PermissionDenied, # Your withdrawal amount has reached the upper limit for today, please try it tomorrow + 'WITHDRAW_021': PermissionDenied, # Your withdrawal amount has reached the upper limit for today, you can only withdraw up to {0} self time + 'WITHDRAW_022': BadRequest, # Withdrawal amount must be greater than {0} + 'WITHDRAW_023': BadRequest, # Withdrawal amount must be less than {0} + 'WITHDRAW_024': BadRequest, # Withdraw is not supported + 'WITHDRAW_025': BadRequest, # Please create a FIO address in the deposit page + 'FUND_001': BadRequest, # Duplicate request(a bizId can only be requested once) + 'FUND_002': InsufficientFunds, # Insufficient account balance + 'FUND_003': BadRequest, # Transfer operations are not supported(for example, sub-accounts do not support financial transfers) + 'FUND_004': ExchangeError, # Unfreeze failed + 'FUND_005': PermissionDenied, # Transfer prohibited + 'FUND_014': BadRequest, # The transfer-in account id and transfer-out account ID cannot be the same + 'FUND_015': BadRequest, # From and to business types cannot be the same + 'FUND_016': BadRequest, # Leverage transfer, symbol cannot be empty + 'FUND_017': BadRequest, # Parameter error + 'FUND_018': BadRequest, # Invalid freeze record + 'FUND_019': BadRequest, # Freeze users not equal + 'FUND_020': BadRequest, # Freeze currency are not equal + 'FUND_021': BadRequest, # Operation not supported + 'FUND_022': BadRequest, # Freeze record does not exist + 'FUND_044': BadRequest, # The maximum length of the amount is 113 and cannot exceed the limit + 'TRANSFER_001': BadRequest, # Duplicate request(a bizId can only be requested once) + 'TRANSFER_002': InsufficientFunds, # Insufficient account balance + 'TRANSFER_003': BadRequest, # User not registered + 'TRANSFER_004': PermissionDenied, # The currency is not allowed to be transferred + 'TRANSFER_005': PermissionDenied, # The user’s currency is not allowed to be transferred + 'TRANSFER_006': PermissionDenied, # Transfer prohibited + 'TRANSFER_007': RequestTimeout, # Request timed out + 'TRANSFER_008': BadRequest, # Transferring to a leveraged account is abnormal + 'TRANSFER_009': BadRequest, # Departing from a leveraged account is abnormal + 'TRANSFER_010': PermissionDenied, # Leverage cleared, transfer prohibited + 'TRANSFER_011': PermissionDenied, # Leverage with borrowing, transfer prohibited + 'TRANSFER_012': PermissionDenied, # Currency transfer prohibited + 'symbol_not_support_trading_via_api': BadSymbol, # {"returnCode":1,"msgInfo":"failure","error":{"code":"symbol_not_support_trading_via_api","msg":"The symbol does not support trading via API"},"result":null} + 'open_order_min_nominal_value_limit': InvalidOrder, # {"returnCode":1,"msgInfo":"failure","error":{"code":"open_order_min_nominal_value_limit","msg":"Exceeds the minimum notional value of a single order"},"result":null} + 'insufficient_balance': InsufficientFunds, + }, + 'broad': { + 'The symbol does not support trading via API': BadSymbol, # {"returnCode":1,"msgInfo":"failure","error":{"code":"symbol_not_support_trading_via_api","msg":"The symbol does not support trading via API"},"result":null} + 'Exceeds the minimum notional value of a single order': InvalidOrder, # {"returnCode":1,"msgInfo":"failure","error":{"code":"open_order_min_nominal_value_limit","msg":"Exceeds the minimum notional value of a single order"},"result":null} + 'insufficient balance': InsufficientFunds, + }, + }, + 'timeframes': { + '1m': '1m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1h', # spot only + '2h': '2h', # spot only + '4h': '4h', + '6h': '6h', # spot only + '8h': '8h', # spot only + '1d': '1d', + '3d': '3d', # spot only + '1w': '1w', + '1M': '1M', # spot only + }, + 'commonCurrencies': {}, + 'options': { + 'adjustForTimeDifference': False, + 'timeDifference': 0, + 'accountsById': { + 'spot': 'SPOT', + 'leverage': 'LEVER', + 'finance': 'FINANCE', + 'swap': 'FUTURES_U', + 'future': 'FUTURES_U', + 'linear': 'FUTURES_U', + 'inverse': 'FUTURES_C', + }, + 'networks': { + 'ERC20': 'Ethereum', + 'TRC20': 'Tron', + 'BEP20': 'BNB Smart Chain', + 'BEP2': 'BNB-BEP2', + 'ETH': 'Ethereum', + 'TRON': 'Tron', + 'BNB': 'BNB Smart Chain', + 'AVAX': 'AVAX C-Chain', + 'GAL': 'GAL(FT)', + 'ALEO': 'ALEO(IOU)', + 'BTC': 'Bitcoin', + 'XT': 'XT Smart Chain', + 'ETC': 'Ethereum Classic', + 'MATIC': 'Polygon', + 'LTC': 'Litecoin', + 'BTS': 'BitShares', + 'XRP': 'Ripple', + 'XLM': 'Stellar Network', + 'ADA': 'Cardano', + 'XWC': 'XWC-XWC', + 'DOGE': 'dogecoin', + 'DCR': 'Decred', + 'SC': 'Siacoin', + 'XTZ': 'Tezos', + 'ZEC': 'Zcash', + 'XMR': 'Monero', + 'LSK': 'Lisk', + 'ATOM': 'Cosmos', + 'ONT': 'Ontology', + 'ALGO': 'Algorand', + 'SOL': 'SOL-SOL', + 'DOT': 'Polkadot', + 'ZEN': 'Horizen', + 'FIL': 'Filecoin', + 'CHZ': 'chz', + 'ICP': 'Internet Computer', + 'KSM': 'Kusama', + 'LUNA': 'Terra', + 'THETA': 'Theta Token', + 'FTM': 'Fantom', + 'VET': 'VeChain', + 'NEAR': 'NEAR Protocol', + 'ONE': 'Harmony', + 'KLAY': 'Klaytn', + 'AR': 'Arweave', + 'CELT': 'OKT', + 'EGLD': 'Elrond eGold', + 'CRO': 'CRO-CRONOS', + 'BCH': 'Bitcoin Cash', + 'GLMR': 'Moonbeam', + 'LOOP': 'LOOP-LRC', + 'REI': 'REI Network', + 'ASTR': 'Astar Network', + 'OP': 'OPT', + 'MMT': 'MMT-MMT', + 'TBC': 'TBC-TBC', + 'OMAX': 'OMAX-OMAX CHAIN', + 'GMMT': 'GMMT chain', + 'ZIL': 'Zilliqa', + }, + 'networksById': { + 'Ethereum': 'ERC20', + 'Tron': 'TRC20', + 'BNB Smart Chain': 'BEP20', + 'BNB-BEP2': 'BEP2', + 'Bitcoin': 'BTC', + 'XT Smart Chain': 'XT', + 'Ethereum Classic': 'ETC', + 'Polygon': 'MATIC', + 'Litecoin': 'LTC', + 'BitShares': 'BTS', + 'Ripple': 'XRP', + 'Stellar Network': 'XLM', + 'Cardano': 'ADA', + 'XWC-XWC': 'XWC', + 'dogecoin': 'DOGE', + 'Decred': 'DCR', + 'Siacoin': 'SC', + 'Tezos': 'XTZ', + 'Zcash': 'ZEC', + 'Monero': 'XMR', + 'Lisk': 'LSK', + 'Cosmos': 'ATOM', + 'Ontology': 'ONT', + 'Algorand': 'ALGO', + 'SOL-SOL': 'SOL', + 'Polkadot': 'DOT', + 'Horizen': 'ZEN', + 'Filecoin': 'FIL', + 'chz': 'CHZ', + 'Internet Computer': 'ICP', + 'Kusama': 'KSM', + 'Terra': 'LUNA', + 'Theta Token': 'THETA', + 'Fantom': 'FTM', + 'VeChain': 'VET', + 'AVAX C-Chain': 'AVAX', + 'NEAR Protocol': 'NEAR', + 'Harmony': 'ONE', + 'Klaytn': 'KLAY', + 'Arweave': 'AR', + 'OKT': 'CELT', + 'Elrond eGold': 'EGLD', + 'CRO-CRONOS': 'CRO', + 'Bitcoin Cash': 'BCH', + 'Moonbeam': 'GLMR', + 'LOOP-LRC': 'LOOP', + 'REI Network': 'REI', + 'Astar Network': 'ASTR', + 'GAL(FT)': 'GAL', + 'ALEO(IOU)': 'ALEO', + 'OPT': 'OP', + 'MMT-MMT': 'MMT', + 'TBC-TBC': 'TBC', + 'OMAX-OMAX CHAIN': 'OMAX', + 'GMMT chain': 'GMMT', + 'Zilliqa': 'ZIL', + }, + 'createMarketBuyOrderRequiresPrice': True, + 'recvWindow': '5000', # in milliseconds, spot only + }, + 'features': { + 'default': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': False, + 'triggerDirection': False, + 'triggerPriceType': None, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': True, + 'limit': 100, + 'daysBack': 100000, # todo + 'untilDays': 100000, # todo + 'marketType': True, + 'subType': True, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': True, # todo TPSL kind + 'trailing': False, + 'marketType': True, + 'subType': True, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': True, + 'limit': 100, + 'trigger': True, # todo TPSL + 'trailing': False, + 'marketType': True, + 'subType': True, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': True, + 'limit': 100, + 'daysBack': 100000, # todo + 'untilDays': 100000, # todo + 'trigger': True, # todo TPSL + 'trailing': False, + 'marketType': True, + 'subType': True, + 'symbolRequired': False, + }, + 'fetchClosedOrders': { + 'marginMode': True, + 'limit': 100, + 'daysBack': 100000, # todo + 'daysBackCanceled': 1, # todo + 'untilDays': 100000, # todo + 'trigger': True, # todo TPSL + 'trailing': False, + 'marketType': True, + 'subType': True, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 1000, # todo for derivatives + }, + }, + 'spot': { + 'extends': 'default', + }, + 'forDerivatives': { + 'extends': 'default', + 'createOrder': { + 'triggerPrice': True, + # todo + 'triggerPriceType': { + 'last': True, + 'mark': True, + 'index': True, + }, + 'stopLossPrice': True, + 'takeProfitPrice': True, + }, + 'fetchMyTrades': { + 'daysBack': None, + 'untilDays': None, + }, + }, + 'swap': { + 'linear': { + 'extends': 'forDerivatives', + }, + 'inverse': { + 'extends': 'forDerivatives', + }, + }, + 'future': { + 'linear': { + 'extends': 'forDerivatives', + }, + 'inverse': { + 'extends': 'forDerivatives', + }, + }, + }, + }) + + def nonce(self): + return self.milliseconds() - self.options['timeDifference'] + + async def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the xt server + + https://doc.xt.com/#market1serverInfo + + :param dict params: extra parameters specific to the xt api endpoint + :returns int: the current integer timestamp in milliseconds from the xt server + """ + response = await self.publicSpotGetTime(params) + # + # { + # "rc": 0, + # "mc": "SUCCESS", + # "ma": [], + # "result": { + # "serverTime": 1677823301643 + # } + # } + # + data = self.safe_value(response, 'result') + return self.safe_integer(data, 'serverTime') + + async def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://doc.xt.com/#deposit_withdrawalsupportedCurrenciesGet + + :param dict params: extra parameters specific to the xt api endpoint + :returns dict: an associative dictionary of currencies + """ + promisesRaw = [self.publicSpotGetWalletSupportCurrency(params), self.publicSpotGetCurrencies(params)] + chainsResponse, currenciesResponse = await asyncio.gather(*promisesRaw) + # + # currencies + # + # { + # "time": "1686626116145", + # "version": "5dbbb2f2527c22b2b2e3b47187ef13d1", + # "currencies": [ + # { + # "id": "2", + # "currency": "btc", + # "fullName": "Bitcoin", + # "logo": "https://a.static-global.com/1/currency/btc.png", + # "cmcLink": "https://coinmarketcap.com/currencies/bitcoin/", + # "weight": "99999", + # "maxPrecision": "10", + # "depositStatus": "1", + # "withdrawStatus": "1", + # "convertEnabled": "1", + # "transferEnabled": "1", + # "isChainExist": "1", + # "plates": [152] + # }, + # ], + # } + # + # + # chains + # + # { + # "rc": 0, + # "mc": "SUCCESS", + # "ma": [], + # "result": [ + # { + # "currency": "btc", + # "supportChains": [ + # { + # "chain": "Bitcoin", + # "depositEnabled": True, + # "withdrawEnabled": True, + # "withdrawFeeAmount": 0.0009, + # "withdrawMinAmount": 0.0005, + # "depositFeeRate": 0 + # }, + # ] + # }, + # ] + # } + # + # note: individual network's full data is available on per-currency endpoint: https://www.xt.com/sapi/v4/balance/public/currency/11 + # + chainsData = self.safe_value(chainsResponse, 'result', []) + currenciesResult = self.safe_value(currenciesResponse, 'result', []) + currenciesData = self.safe_value(currenciesResult, 'currencies', []) + chainsDataIndexed = self.index_by(chainsData, 'currency') + result = {} + for i in range(0, len(currenciesData)): + entry = currenciesData[i] + currencyId = self.safe_string(entry, 'currency') + code = self.safe_currency_code(currencyId) + networkEntry = self.safe_value(chainsDataIndexed, currencyId, {}) + rawNetworks = self.safe_value(networkEntry, 'supportChains', []) + networks = {} + for j in range(0, len(rawNetworks)): + rawNetwork = rawNetworks[j] + networkId = self.safe_string(rawNetwork, 'chain') + networkCode = self.network_id_to_code(networkId, code) + networks[networkCode] = { + 'info': rawNetwork, + 'id': networkId, + 'network': networkCode, + 'name': None, + 'active': None, + 'fee': self.safe_number(rawNetwork, 'withdrawFeeAmount'), + 'precision': None, + 'deposit': self.safe_bool(rawNetwork, 'depositEnabled'), + 'withdraw': self.safe_bool(rawNetwork, 'withdrawEnabled'), + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': self.safe_number(rawNetwork, 'withdrawMinAmount'), + 'max': None, + }, + 'deposit': { + 'min': None, + 'max': None, + }, + }, + } + typeRaw = self.safe_string(entry, 'type') + type: Str = None + if typeRaw == 'FT': + type = 'crypto' + else: + type = 'other' + result[code] = self.safe_currency_structure({ + 'info': entry, + 'id': currencyId, + 'code': code, + 'name': self.safe_string(entry, 'fullName'), + 'active': None, + 'fee': None, + 'precision': self.parse_number(self.parse_precision(self.safe_string(entry, 'maxPrecision'))), + 'deposit': self.safe_string(entry, 'depositStatus') == '1', + 'withdraw': self.safe_string(entry, 'withdrawStatus') == '1', + 'networks': networks, + 'type': type, + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + 'deposit': { + 'min': None, + 'max': None, + }, + }, + }) + return result + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for xt + + https://doc.xt.com/#market2symbol + https://doc.xt.com/#futures_quotesgetSymbols + + :param dict params: extra parameters specific to the xt api endpoint + :returns dict[]: an array of objects representing market data + """ + if self.options['adjustForTimeDifference']: + await self.load_time_difference() + promisesUnresolved = [ + self.fetch_spot_markets(params), + self.fetch_swap_and_future_markets(params), + ] + promises = await asyncio.gather(*promisesUnresolved) + spotMarkets = promises[0] + swapAndFutureMarkets = promises[1] + return self.array_concat(spotMarkets, swapAndFutureMarkets) + + async def fetch_spot_markets(self, params={}): + response = await self.publicSpotGetSymbol(params) + # + # { + # "rc": 0, + # "mc": "SUCCESS", + # "ma": [], + # "result": { + # "time": 1677881368812, + # "version": "abb101d1543e54bee40687b135411ba0", + # "symbols": [ + # { + # "id": 640, + # "symbol": "xt_usdt", + # "state": "ONLINE", + # "stateTime": 1554048000000, + # "tradingEnabled": True, + # "openapiEnabled": True, + # "nextStateTime": null, + # "nextState": null, + # "depthMergePrecision": 5, + # "baseCurrency": "xt", + # "baseCurrencyPrecision": 8, + # "baseCurrencyId": 128, + # "quoteCurrency": "usdt", + # "quoteCurrencyPrecision": 8, + # "quoteCurrencyId": 11, + # "pricePrecision": 4, + # "quantityPrecision": 2, + # "orderTypes": ["LIMIT","MARKET"], + # "timeInForces": ["GTC","IOC"], + # "displayWeight": 10002, + # "displayLevel": "FULL", + # "plates": [], + # "filters":[ + # { + # "filter": "QUOTE_QTY", + # "min": "1" + # }, + # { + # "filter": "PROTECTION_LIMIT", + # "buyMaxDeviation": "0.8", + # "sellMaxDeviation": "4" + # }, + # { + # "filter": "PROTECTION_MARKET", + # "maxDeviation": "0.02" + # } + # ] + # }, + # ] + # } + # } + # + data = self.safe_value(response, 'result', {}) + symbols = self.safe_value(data, 'symbols', []) + return self.parse_markets(symbols) + + async def fetch_swap_and_future_markets(self, params={}): + markets = await asyncio.gather(*[self.publicLinearGetFutureMarketV1PublicSymbolList(params), self.publicInverseGetFutureMarketV1PublicSymbolList(params)]) + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": [ + # { + # "id": 52, + # "symbolGroupId": 71, + # "symbol": "xt_usdt", + # "pair": "xt_usdt", + # "contractType": "PERPETUAL", + # "productType": "perpetual", + # "predictEventType": null, + # "underlyingType": "U_BASED", + # "contractSize": "1", + # "tradeSwitch": True, + # "isDisplay": True, + # "isOpenApi": False, + # "state": 0, + # "initLeverage": 20, + # "initPositionType": "CROSSED", + # "baseCoin": "xt", + # "quoteCoin": "usdt", + # "baseCoinPrecision": 8, + # "baseCoinDisplayPrecision": 4, + # "quoteCoinPrecision": 8, + # "quoteCoinDisplayPrecision": 4, + # "quantityPrecision": 0, + # "pricePrecision": 4, + # "supportOrderType": "LIMIT,MARKET", + # "supportTimeInForce": "GTC,FOK,IOC,GTX", + # "supportEntrustType": "TAKE_PROFIT,STOP,TAKE_PROFIT_MARKET,STOP_MARKET,TRAILING_STOP_MARKET", + # "supportPositionType": "CROSSED,ISOLATED", + # "minQty": "1", + # "minNotional": "5", + # "maxNotional": "20000000", + # "multiplierDown": "0.1", + # "multiplierUp": "0.1", + # "maxOpenOrders": 200, + # "maxEntrusts": 200, + # "makerFee": "0.0004", + # "takerFee": "0.0006", + # "liquidationFee": "0.01", + # "marketTakeBound": "0.1", + # "depthPrecisionMerge": 5, + # "labels": ["HOT"], + # "onboardDate": 1657101601000, + # "enName": "XTUSDT ", + # "cnName": "XTUSDT", + # "minStepPrice": "0.0001", + # "minPrice": null, + # "maxPrice": null, + # "deliveryDate": 1669879634000, + # "deliveryPrice": null, + # "deliveryCompletion": False, + # "cnDesc": null, + # "enDesc": null + # }, + # ] + # } + # + swapAndFutureMarkets = self.array_concat(self.safe_value(markets[0], 'result', []), self.safe_value(markets[1], 'result', [])) + return self.parse_markets(swapAndFutureMarkets) + + def parse_markets(self, markets): + result = [] + for i in range(0, len(markets)): + result.append(self.parse_market(markets[i])) + return result + + def parse_market(self, market: dict) -> Market: + # + # spot + # + # { + # "id": 640, + # "symbol": "xt_usdt", + # "state": "ONLINE", + # "stateTime": 1554048000000, + # "tradingEnabled": True, + # "openapiEnabled": True, + # "nextStateTime": null, + # "nextState": null, + # "depthMergePrecision": 5, + # "baseCurrency": "xt", + # "baseCurrencyPrecision": 8, + # "baseCurrencyId": 128, + # "quoteCurrency": "usdt", + # "quoteCurrencyPrecision": 8, + # "quoteCurrencyId": 11, + # "pricePrecision": 4, + # "quantityPrecision": 2, + # "orderTypes": ["LIMIT","MARKET"], + # "timeInForces": ["GTC","IOC"], + # "displayWeight": 10002, + # "displayLevel": "FULL", + # "plates": [], + # "filters":[ + # { + # "filter": "QUOTE_QTY", + # "min": "1" + # }, + # { + # "filter": "PRICE", + # "min": null, + # "max": null, + # "tickSize": null + # }, + # { + # "filter": "QUANTITY", + # "min": null, + # "max": null, + # "tickSize": null + # }, + # { + # "filter": "PROTECTION_LIMIT", + # "buyMaxDeviation": "0.8", + # "sellMaxDeviation": "4" + # }, + # { + # "filter": "PROTECTION_MARKET", + # "maxDeviation": "0.02" + # }, + # { + # "filter": "PROTECTION_ONLINE", + # "durationSeconds": "300", + # "maxPriceMultiple": "5" + # }, + # ] + # } + # + # swap and future + # + # { + # "id": 52, + # "symbolGroupId": 71, + # "symbol": "xt_usdt", + # "pair": "xt_usdt", + # "contractType": "PERPETUAL", + # "productType": "perpetual", + # "predictEventType": null, + # "underlyingType": "U_BASED", + # "contractSize": "1", + # "tradeSwitch": True, + # "isDisplay": True, + # "isOpenApi": False, + # "state": 0, + # "initLeverage": 20, + # "initPositionType": "CROSSED", + # "baseCoin": "xt", + # "quoteCoin": "usdt", + # "baseCoinPrecision": 8, + # "baseCoinDisplayPrecision": 4, + # "quoteCoinPrecision": 8, + # "quoteCoinDisplayPrecision": 4, + # "quantityPrecision": 0, + # "pricePrecision": 4, + # "supportOrderType": "LIMIT,MARKET", + # "supportTimeInForce": "GTC,FOK,IOC,GTX", + # "supportEntrustType": "TAKE_PROFIT,STOP,TAKE_PROFIT_MARKET,STOP_MARKET,TRAILING_STOP_MARKET", + # "supportPositionType": "CROSSED,ISOLATED", + # "minQty": "1", + # "minNotional": "5", + # "maxNotional": "20000000", + # "multiplierDown": "0.1", + # "multiplierUp": "0.1", + # "maxOpenOrders": 200, + # "maxEntrusts": 200, + # "makerFee": "0.0004", + # "takerFee": "0.0006", + # "liquidationFee": "0.01", + # "marketTakeBound": "0.1", + # "depthPrecisionMerge": 5, + # "labels": ["HOT"], + # "onboardDate": 1657101601000, + # "enName": "XTUSDT ", + # "cnName": "XTUSDT", + # "minStepPrice": "0.0001", + # "minPrice": null, + # "maxPrice": null, + # "deliveryDate": 1669879634000, + # "deliveryPrice": null, + # "deliveryCompletion": False, + # "cnDesc": null, + # "enDesc": null + # } + # + id = self.safe_string(market, 'symbol') + baseId = self.safe_string_2(market, 'baseCurrency', 'baseCoin') + quoteId = self.safe_string_2(market, 'quoteCurrency', 'quoteCoin') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + state = self.safe_string(market, 'state') + symbol = base + '/' + quote + filters = self.safe_value(market, 'filters', []) + minAmount = None + maxAmount = None + minCost = None + maxCost = None + minPrice = None + maxPrice = None + amountPrecision = None + for i in range(0, len(filters)): + entry = filters[i] + filter = self.safe_string(entry, 'filter') + if filter == 'QUANTITY': + minAmount = self.safe_number(entry, 'min') + maxAmount = self.safe_number(entry, 'max') + amountPrecision = self.safe_number(entry, 'tickSize') + if filter == 'QUOTE_QTY': + minCost = self.safe_number(entry, 'min') + if filter == 'PRICE': + minPrice = self.safe_number(entry, 'min') + maxPrice = self.safe_number(entry, 'max') + if amountPrecision is None: + amountPrecision = self.parse_number(self.parse_precision(self.safe_string(market, 'quantityPrecision'))) + underlyingType = self.safe_string(market, 'underlyingType') + linear = None + inverse = None + settleId = None + settle = None + expiry = None + future = False + swap = False + contract = False + spot = True + type = 'spot' + if underlyingType == 'U_BASED': + symbol = symbol + ':' + quote + settleId = baseId + settle = quote + linear = True + inverse = False + elif underlyingType == 'COIN_BASED': + symbol = symbol + ':' + base + settleId = baseId + settle = base + linear = False + inverse = True + if underlyingType is not None: + expiry = self.safe_integer(market, 'deliveryDate') + productType = self.safe_string(market, 'productType') + if productType != 'perpetual': + symbol = symbol + '-' + self.yymmdd(expiry) + type = 'future' + future = True + else: + type = 'swap' + swap = True + minAmount = self.safe_number(market, 'minQty') + minCost = self.safe_number(market, 'minNotional') + maxCost = self.safe_number(market, 'maxNotional') + minPrice = self.safe_number(market, 'minPrice') + maxPrice = self.safe_number(market, 'maxPrice') + contract = True + spot = False + isActive = False + if contract: + isActive = self.safe_value(market, 'isOpenApi', False) + else: + if (state == 'ONLINE') and (self.safe_value(market, 'tradingEnabled')) and (self.safe_value(market, 'openapiEnabled')): + isActive = True + return self.safe_market_structure({ + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': type, + 'spot': spot, + 'margin': None, + 'swap': swap, + 'future': future, + 'option': False, + 'active': isActive, + 'contract': contract, + 'linear': linear, + 'inverse': inverse, + 'taker': self.safe_number(market, 'takerFee'), + 'maker': self.safe_number(market, 'makerFee'), + 'contractSize': self.safe_number(market, 'contractSize'), + 'expiry': expiry, + 'expiryDatetime': self.iso8601(expiry), + 'strike': None, + 'optionType': None, + 'precision': { + 'price': self.parse_number(self.parse_precision(self.safe_string(market, 'pricePrecision'))), + 'amount': amountPrecision, + 'base': self.parse_number(self.parse_precision(self.safe_string(market, 'baseCoinPrecision'))), + 'quote': self.parse_number(self.parse_precision(self.safe_string(market, 'quoteCoinPrecision'))), + }, + 'limits': { + 'leverage': { + 'min': self.parse_number('1'), + 'max': None, + }, + 'amount': { + 'min': minAmount, + 'max': maxAmount, + }, + 'price': { + 'min': minPrice, + 'max': maxPrice, + }, + 'cost': { + 'min': minCost, + 'max': maxCost, + }, + }, + 'info': market, + }) + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}): + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://doc.xt.com/#market4kline + https://doc.xt.com/#futures_quotesgetKLine + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict params: extra parameters specific to the xt api endpoint + :param int [params.until]: timestamp in ms of the latest candle to fetch + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate', False) + if paginate: + return await self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params, 1000) + market = self.market(symbol) + request = { + 'symbol': market['id'], + 'interval': self.safe_string(self.timeframes, timeframe, timeframe), + } + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + else: + request['limit'] = 1000 + until = self.safe_integer(params, 'until') + params = self.omit(params, ['until']) + if until is not None: + request['endTime'] = until + response = None + if market['linear']: + response = await self.publicLinearGetFutureMarketV1PublicQKline(self.extend(request, params)) + elif market['inverse']: + response = await self.publicInverseGetFutureMarketV1PublicQKline(self.extend(request, params)) + else: + response = await self.publicSpotGetKline(self.extend(request, params)) + # + # spot + # + # { + # "rc": 0, + # "mc": "SUCCESS", + # "ma": [], + # "result": [ + # { + # "t": 1678167720000, + # "o": "22467.85", + # "c": "22465.87", + # "h": "22468.86", + # "l": "22465.21", + # "q": "1.316656", + # "v": "29582.73018498" + # }, + # ] + # } + # + # swap and future + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": [ + # { + # "s": "btc_usdt", + # "p": "btc_usdt", + # "t": 1678168020000, + # "o": "22450.0", + # "c": "22441.5", + # "h": "22450.0", + # "l": "22441.5", + # "a": "312931", + # "v": "702461.58895" + # }, + # ] + # } + # + ohlcvs = self.safe_value(response, 'result', []) + return self.parse_ohlcvs(ohlcvs, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # spot + # + # { + # "t": 1678167720000, + # "o": "22467.85", + # "c": "22465.87", + # "h": "22468.86", + # "l": "22465.21", + # "q": "1.316656", + # "v": "29582.73018498" + # } + # + # swap and future + # + # { + # "s": "btc_usdt", + # "p": "btc_usdt", + # "t": 1678168020000, + # "o": "22450.0", + # "c": "22441.5", + # "h": "22450.0", + # "l": "22441.5", + # "a": "312931", + # "v": "702461.58895" + # } + # + volumeIndex = 'v' if (market['inverse']) else 'a' + return [ + self.safe_integer(ohlcv, 't'), + self.safe_number(ohlcv, 'o'), + self.safe_number(ohlcv, 'h'), + self.safe_number(ohlcv, 'l'), + self.safe_number(ohlcv, 'c'), + self.safe_number_2(ohlcv, 'q', volumeIndex), + ] + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}): + """ + + https://doc.xt.com/#market3depth + https://doc.xt.com/#futures_quotesgetDepth + + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str symbol: unified market symbol to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict params: extra parameters specific to the xt api endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request = { + 'symbol': market['id'], + } + response = None + if market['spot']: + if limit is not None: + request['limit'] = min(limit, 500) + response = await self.publicSpotGetDepth(self.extend(request, params)) + else: + if limit is not None: + request['level'] = min(limit, 50) + else: + request['level'] = 50 + if market['linear']: + response = await self.publicLinearGetFutureMarketV1PublicQDepth(self.extend(request, params)) + elif market['inverse']: + response = await self.publicInverseGetFutureMarketV1PublicQDepth(self.extend(request, params)) + # + # spot + # + # { + # "rc": 0, + # "mc": "SUCCESS", + # "ma": [], + # "result": { + # "timestamp": 1678169975184, + # "lastUpdateId": 1675333221812, + # "bids": [ + # ["22444.51", "0.129887"], + # ["22444.49", "0.114245"], + # ["22444.30", "0.225956"] + # ], + # "asks": [ + # ["22446.19", "0.095330"], + # ["22446.24", "0.224413"], + # ["22446.28", "0.329095"] + # ] + # } + # } + # + # swap and future + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": { + # "t": 1678170311005, + # "s": "btc_usdt", + # "u": 471694545627, + # "b": [ + # ["22426", "198623"], + # ["22423.5", "80295"], + # ["22423", "163580"] + # ], + # "a": [ + # ["22427", "3417"], + # ["22428.5", "43532"], + # ["22429", "119"] + # ] + # } + # } + # + orderBook = self.safe_value(response, 'result', {}) + timestamp = self.safe_integer_2(orderBook, 'timestamp', 't') + if market['spot']: + ob = self.parse_order_book(orderBook, symbol, timestamp) + ob['nonce'] = self.safe_integer(orderBook, 'lastUpdateId') + return ob + swapOb = self.parse_order_book(orderBook, symbol, timestamp, 'b', 'a') + swapOb['nonce'] = self.safe_integer_2(orderBook, 'u', 'lastUpdateId') + return swapOb + + async def fetch_ticker(self, symbol: str, params={}): + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://doc.xt.com/#market10ticker24h + https://doc.xt.com/#futures_quotesgetAggTicker + + :param str symbol: unified market symbol to fetch the ticker for + :param dict params: extra parameters specific to the xt api endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request = { + 'symbol': market['id'], + } + response = None + if market['linear']: + response = await self.publicLinearGetFutureMarketV1PublicQAggTicker(self.extend(request, params)) + elif market['inverse']: + response = await self.publicInverseGetFutureMarketV1PublicQAggTicker(self.extend(request, params)) + else: + response = await self.publicSpotGetTicker24h(self.extend(request, params)) + # + # spot + # + # { + # "rc": 0, + # "mc": "SUCCESS", + # "ma": [], + # "result": [ + # { + # "s": "btc_usdt", + # "t": 1678172693931, + # "cv": "34.00", + # "cr": "0.0015", + # "o": "22398.05", + # "l": "22323.72", + # "h": "22600.50", + # "c": "22432.05", + # "q": "7962.256931", + # "v": "178675209.47416856" + # } + # ] + # } + # + # swap and future + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": { + # "t": 1678172848572, + # "s": "btc_usdt", + # "c": "22415.5", + # "h": "22590.0", + # "l": "22310.0", + # "a": "623654031", + # "v": "1399166074.31675", + # "o": "22381.5", + # "r": "0.0015", + # "i": "22424.5", + # "m": "22416.5", + # "bp": "22415", + # "ap": "22415.5" + # } + # } + # + ticker = self.safe_value(response, 'result') + if market['spot']: + return self.parse_ticker(ticker[0], market) + return self.parse_ticker(ticker, market) + + async def fetch_tickers(self, symbols: List[str] = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical calculations with the information calculated over the past 24 hours each market + + https://doc.xt.com/#market10ticker24h + https://doc.xt.com/#futures_quotesgetAggTickers + + :param str [symbols]: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict params: extra parameters specific to the xt api endpoint + :returns dict: an array of `ticker structures ` + """ + await self.load_markets() + market = None + if symbols is not None: + symbols = self.market_symbols(symbols) + market = self.market(symbols[0]) + request = {} + type = None + subType = None + response = None + type, params = self.handle_market_type_and_params('fetchTickers', market, params) + subType, params = self.handle_sub_type_and_params('fetchTickers', market, params) + if subType == 'inverse': + response = await self.publicInverseGetFutureMarketV1PublicQAggTickers(self.extend(request, params)) + elif (subType == 'linear') or (type == 'swap') or (type == 'future'): + response = await self.publicLinearGetFutureMarketV1PublicQAggTickers(self.extend(request, params)) + else: + response = await self.publicSpotGetTicker24h(self.extend(request, params)) + # + # spot + # + # { + # "rc": 0, + # "mc": "SUCCESS", + # "ma": [], + # "result": [ + # { + # "s": "btc_usdt", + # "t": 1678172693931, + # "cv": "34.00", + # "cr": "0.0015", + # "o": "22398.05", + # "l": "22323.72", + # "h": "22600.50", + # "c": "22432.05", + # "q": "7962.256931", + # "v": "178675209.47416856" + # } + # ] + # } + # + # swap and future + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": [ + # { + # "t": 1680738775108, + # "s": "badger_usdt", + # "c": "2.7176", + # "h": "2.7917", + # "l": "2.6818", + # "a": "88332", + # "v": "242286.3520", + # "o": "2.7422", + # "r": "-0.0089", + # "i": "2.7155", + # "m": "2.7161", + # "bp": "2.7152", + # "ap": "2.7176" + # }, + # ] + # } + # + tickers = self.safe_value(response, 'result', []) + result = {} + for i in range(0, len(tickers)): + ticker = self.parse_ticker(tickers[i], market) + symbol = ticker['symbol'] + result[symbol] = ticker + return self.filter_by_array(result, 'symbol', symbols) + + async def fetch_bids_asks(self, symbols: List[str] = None, params={}): + """ + fetches the bid and ask price and volume for multiple markets + + https://doc.xt.com/#market9tickerBook + + :param str [symbols]: unified symbols of the markets to fetch the bids and asks for, all markets are returned if not assigned + :param dict params: extra parameters specific to the xt api endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + request = {} + market = None + if symbols is not None: + market = self.market(symbols[0]) + subType = None + subType, params = self.handle_sub_type_and_params('fetchBidsAsks', market, params) + if subType is not None: + raise NotSupported(self.id + ' fetchBidsAsks() is not available for swap and future markets, only spot markets are supported') + response = await self.publicSpotGetTickerBook(self.extend(request, params)) + # + # { + # "rc": 0, + # "mc": "SUCCESS", + # "ma": [], + # "result": [ + # { + # "s": "kas_usdt", + # "t": 1679539891853, + # "ap": "0.016298", + # "aq": "5119.09", + # "bp": "0.016290", + # "bq": "135.37" + # }, + # ] + # } + # + tickers = self.safe_value(response, 'result', []) + return self.parse_tickers(tickers, symbols) + + def parse_ticker(self, ticker, market=None): + # + # spot: fetchTicker, fetchTickers + # + # { + # "s": "btc_usdt", + # "t": 1678172693931, + # "cv": "34.00", + # "cr": "0.0015", + # "o": "22398.05", + # "l": "22323.72", + # "h": "22600.50", + # "c": "22432.05", + # "q": "7962.256931", + # "v": "178675209.47416856" + # } + # + # swap and future: fetchTicker, fetchTickers + # + # { + # "t": 1678172848572, + # "s": "btc_usdt", + # "c": "22415.5", + # "h": "22590.0", + # "l": "22310.0", + # "a": "623654031", + # "v": "1399166074.31675", + # "o": "22381.5", + # "r": "0.0015", + # "i": "22424.5", + # "m": "22416.5", + # "bp": "22415", + # "ap": "22415.5" + # } + # + # fetchBidsAsks + # + # { + # "s": "kas_usdt", + # "t": 1679539891853, + # "ap": "0.016298", + # "aq": "5119.09", + # "bp": "0.016290", + # "bq": "135.37" + # } + # + marketId = self.safe_string(ticker, 's') + marketType = market['type'] if (market is not None) else None + hasSpotKeys = ('cv' in ticker) or ('aq' in ticker) + if marketType is None: + marketType = 'spot' if hasSpotKeys else 'contract' + market = self.safe_market(marketId, market, '_', marketType) + symbol = market['symbol'] + timestamp = self.safe_integer(ticker, 't') + percentage = self.safe_string_2(ticker, 'cr', 'r') + if percentage is not None: + percentage = Precise.string_mul(percentage, '100') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_number(ticker, 'h'), + 'low': self.safe_number(ticker, 'l'), + 'bid': self.safe_number(ticker, 'bp'), + 'bidVolume': self.safe_number(ticker, 'bq'), + 'ask': self.safe_number(ticker, 'ap'), + 'askVolume': self.safe_number(ticker, 'aq'), + 'vwap': None, + 'open': self.safe_string(ticker, 'o'), + 'close': self.safe_string(ticker, 'c'), + 'last': self.safe_string(ticker, 'c'), + 'previousClose': None, + 'change': self.safe_number(ticker, 'cv'), + 'percentage': self.parse_number(percentage), + 'average': None, + 'baseVolume': None, + 'quoteVolume': self.safe_number_2(ticker, 'a', 'v'), + 'info': ticker, + }, market) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}): + """ + get the list of most recent trades for a particular symbol + + https://doc.xt.com/#market5tradeRecent + https://doc.xt.com/#futures_quotesgetDeal + + :param str symbol: unified market symbol to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict params: extra parameters specific to the xt api endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request = { + 'symbol': market['id'], + } + response = None + if market['spot']: + if limit is not None: + request['limit'] = limit + response = await self.publicSpotGetTradeRecent(self.extend(request, params)) + else: + if limit is not None: + request['num'] = limit + if market['linear']: + response = await self.publicLinearGetFutureMarketV1PublicQDeal(self.extend(request, params)) + elif market['inverse']: + response = await self.publicInverseGetFutureMarketV1PublicQDeal(self.extend(request, params)) + # + # spot + # + # { + # "rc": 0, + # "mc": "SUCCESS", + # "ma": [], + # "result": [ + # { + # "i": 203530723141917063, + # "t": 1678227505815, + # "p": "22038.81", + # "q": "0.000978", + # "v": "21.55395618", + # "b": True + # }, + # ] + # } + # + # swap and future + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": [ + # { + # "t": 1678227683897, + # "s": "btc_usdt", + # "p": "22031", + # "a": "1067", + # "m": "BID" + # }, + # ] + # } + # + trades = self.safe_value(response, 'result', []) + return self.parse_trades(trades, market) + + async def fetch_my_trades(self, symbol: str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://doc.xt.com/#tradetradeGet + https://doc.xt.com/#futures_ordergetTrades + + :param str [symbol]: unified market symbol to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict params: extra parameters specific to the xt api endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + request = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if since is not None: + request['startTime'] = since + type = None + subType = None + response = None + type, params = self.handle_market_type_and_params('fetchMyTrades', market, params) + subType, params = self.handle_sub_type_and_params('fetchMyTrades', market, params) + if (subType is not None) or (type == 'swap') or (type == 'future'): + if limit is not None: + request['size'] = limit + if subType == 'inverse': + response = await self.privateInverseGetFutureTradeV1OrderTradeList(self.extend(request, params)) + else: + response = await self.privateLinearGetFutureTradeV1OrderTradeList(self.extend(request, params)) + else: + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchMyTrades', params) + marginOrSpotRequest = 'LEVER' if (marginMode is not None) else 'SPOT' + request['bizType'] = marginOrSpotRequest + if limit is not None: + request['limit'] = limit + response = await self.privateSpotGetTrade(self.extend(request, params)) + # + # spot and margin + # + # { + # "rc": 0, + # "mc": "SUCCESS", + # "ma": [], + # "result": { + # "hasPrev": False, + # "hasNext": False, + # "items": [ + # { + # "symbol": "btc_usdt", + # "tradeId": "206906233569974658", + # "orderId": "206906233178463488", + # "orderSide": "SELL", + # "orderType": "MARKET", + # "bizType": "SPOT", + # "time": 1679032290215, + # "price": "25703.46", + # "quantity": "0.000099", + # "quoteQty": "2.54464254", + # "baseCurrency": "btc", + # "quoteCurrency": "usdt", + # "fee": "0.00508929", + # "feeCurrency": "usdt", + # "takerMaker": "TAKER" + # }, + # ] + # } + # } + # + # swap and future + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": { + # "page": 1, + # "ps": 10, + # "total": 2, + # "items": [ + # { + # "orderId": "207260566170987200", + # "execId": "207260566790603265", + # "symbol": "btc_usdt", + # "quantity": "13", + # "price": "27368", + # "fee": "0.02134704", + # "feeCoin": "usdt", + # "timestamp": 1679116769838, + # "takerMaker": "TAKER" + # }, + # ] + # } + # } + # + data = self.safe_value(response, 'result', {}) + trades = self.safe_value(data, 'items', []) + return self.parse_trades(trades, market, since, limit) + + def parse_trade(self, trade, market=None): + # + # spot: fetchTrades + # + # { + # "i": 203530723141917063, + # "t": 1678227505815, + # "p": "22038.81", + # "q": "0.000978", + # "v": "21.55395618", + # "b": True + # } + # + # spot: watchTrades + # + # { + # s: 'btc_usdt', + # i: '228825383103928709', + # t: 1684258222702, + # p: '27003.65', + # q: '0.000796', + # b: True + # } + # + # spot: watchMyTrades + # + # { + # "s": "btc_usdt", # symbol + # "t": 1656043204763, # time + # "i": "6316559590087251233", # tradeId + # "oi": "6216559590087220004", # orderId + # "p": "30000", # trade price + # "q": "3", # qty quantity + # "v": "90000" # volume trade amount + # } + # + # swap and future: fetchTrades + # + # { + # "t": 1678227683897, + # "s": "btc_usdt", + # "p": "22031", + # "a": "1067", + # "m": "BID" + # } + # + # spot: fetchMyTrades + # + # { + # "symbol": "btc_usdt", + # "tradeId": "206906233569974658", + # "orderId": "206906233178463488", + # "orderSide": "SELL", + # "orderType": "MARKET", + # "bizType": "SPOT", + # "time": 1679032290215, + # "price": "25703.46", + # "quantity": "0.000099", + # "quoteQty": "2.54464254", + # "baseCurrency": "btc", + # "quoteCurrency": "usdt", + # "fee": "0.00508929", + # "feeCurrency": "usdt", + # "takerMaker": "TAKER" + # } + # + # swap and future: fetchMyTrades + # + # { + # "orderId": "207260566170987200", + # "execId": "207260566790603265", + # "symbol": "btc_usdt", + # "quantity": "13", + # "price": "27368", + # "fee": "0.02134704", + # "feeCoin": "usdt", + # "timestamp": 1679116769838, + # "takerMaker": "TAKER" + # } + # + # contract watchMyTrades + # + # { + # "symbol": 'btc_usdt', + # "orderSide": 'SELL', + # "positionSide": 'LONG', + # "orderId": '231485367663419328', + # "price": '27152.7', + # "quantity": '33', + # "marginUnfrozen": '2.85318000', + # "timestamp": 1684892412565 + # } + # + # watchMyTrades(ws, swap) + # + # { + # 'fee': '0.04080840', + # 'isMaker': False, + # 'marginUnfrozen': '0.75711984', + # 'orderId': '376172779053188416', + # 'orderSide': 'BUY', + # 'positionSide': 'LONG', + # 'price': '3400.70', + # 'quantity': '2', + # 'symbol': 'eth_usdt', + # 'timestamp': 1719388579622 + # } + # + marketId = self.safe_string_2(trade, 's', 'symbol') + marketType = market['type'] if (market is not None) else None + hasSpotKeys = ('b' in trade) or ('bizType' in trade) or ('oi' in trade) + if marketType is None: + marketType = 'spot' if hasSpotKeys else 'contract' + market = self.safe_market(marketId, market, '_', marketType) + side = None + takerOrMaker = None + isBuyerMaker = self.safe_bool(trade, 'b') + if isBuyerMaker is not None: + side = 'sell' if isBuyerMaker else 'buy' + takerOrMaker = 'taker' # public trades always taker + else: + takerMaker = self.safe_string_lower(trade, 'takerMaker') + if takerMaker is not None: + takerOrMaker = takerMaker + else: + isMaker = self.safe_bool(trade, 'isMaker') + if isMaker is not None: + takerOrMaker = 'maker' if isMaker else 'taker' + orderSide = self.safe_string_lower(trade, 'orderSide') + if orderSide is not None: + side = orderSide + else: + bidOrAsk = self.safe_string(trade, 'm') + if bidOrAsk is not None: + side = 'buy' if (bidOrAsk == 'BID') else 'sell' + timestamp = self.safe_integer_n(trade, ['t', 'time', 'timestamp']) + quantity = self.safe_string_2(trade, 'q', 'quantity') + amount = None + if marketType == 'spot': + amount = quantity + else: + if quantity is None: + amount = Precise.string_mul(self.safe_string(trade, 'a'), self.number_to_string(market['contractSize'])) + else: + amount = Precise.string_mul(quantity, self.number_to_string(market['contractSize'])) + return self.safe_trade({ + 'info': trade, + 'id': self.safe_string_n(trade, ['i', 'tradeId', 'execId']), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'order': self.safe_string_2(trade, 'orderId', 'oi'), + 'type': self.safe_string_lower(trade, 'orderType'), + 'side': side, + 'takerOrMaker': takerOrMaker, + 'price': self.safe_string_2(trade, 'p', 'price'), + 'amount': amount, + 'cost': None, + 'fee': { + 'currency': self.safe_currency_code(self.safe_string_2(trade, 'feeCurrency', 'feeCoin')), + 'cost': self.safe_string(trade, 'fee'), + }, + }, market) + + async def fetch_balance(self, params={}): + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://doc.xt.com/#balancebalancesGet + https://doc.xt.com/#futures_usergetBalances + + :param dict params: extra parameters specific to the xt api endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + type = None + subType = None + response = None + type, params = self.handle_market_type_and_params('fetchBalance', None, params) + subType, params = self.handle_sub_type_and_params('fetchBalance', None, params) + isContractWallet = ((type == 'swap') or (type == 'future')) + if subType == 'inverse': + response = await self.privateInverseGetFutureUserV1BalanceList(params) + elif (subType == 'linear') or isContractWallet: + response = await self.privateLinearGetFutureUserV1BalanceList(params) + else: + response = await self.privateSpotGetBalances(params) + # + # spot + # + # { + # "rc": 0, + # "mc": "SUCCESS", + # "ma": [], + # "result": { + # "totalUsdtAmount": "31.75931133", + # "totalBtcAmount": "0.00115951", + # "assets": [ + # { + # "currency": "usdt", + # "currencyId": 11, + # "frozenAmount": "0.03834082", + # "availableAmount": "31.70995965", + # "totalAmount": "31.74830047", + # "convertBtcAmount": "0.00115911", + # "convertUsdtAmount": "31.74830047" + # }, + # ] + # } + # } + # + # swap and future + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": [ + # { + # "coin": "usdt", + # "walletBalance": "19.29849875", + # "openOrderMarginFrozen": "0", + # "isolatedMargin": "0.709475", + # "crossedMargin": "0", + # "availableBalance": "18.58902375", + # "bonus": "0", + # "coupon":"0" + # } + # ] + # } + # + balances = None + if (subType is not None) or isContractWallet: + balances = self.safe_value(response, 'result', []) + else: + data = self.safe_value(response, 'result', {}) + balances = self.safe_value(data, 'assets', []) + return self.parse_balance(balances) + + def parse_balance(self, response): + # + # spot + # + # { + # "currency": "usdt", + # "currencyId": 11, + # "frozenAmount": "0.03834082", + # "availableAmount": "31.70995965", + # "totalAmount": "31.74830047", + # "convertBtcAmount": "0.00115911", + # "convertUsdtAmount": "31.74830047" + # } + # + # swap and future + # + # { + # "coin": "usdt", + # "walletBalance": "19.29849875", + # "openOrderMarginFrozen": "0", + # "isolatedMargin": "0.709475", + # "crossedMargin": "0", + # "availableBalance": "18.58902375", + # "bonus": "0", + # "coupon":"0" + # } + # + result = {'info': response} + for i in range(0, len(response)): + balance = response[i] + currencyId = self.safe_string_2(balance, 'currency', 'coin') + code = self.safe_currency_code(currencyId) + account = self.account() + free = self.safe_string_2(balance, 'availableAmount', 'availableBalance') + used = self.safe_string(balance, 'frozenAmount') + total = self.safe_string_2(balance, 'totalAmount', 'walletBalance') + if used is None: + crossedAndIsolatedMargin = Precise.string_add(self.safe_string(balance, 'crossedMargin'), self.safe_string(balance, 'isolatedMargin')) + used = Precise.string_add(self.safe_string(balance, 'openOrderMarginFrozen'), crossedAndIsolatedMargin) + account['free'] = free + account['used'] = used + account['total'] = total + result[code] = account + return self.safe_balance(result) + + async def create_market_buy_order_with_cost(self, symbol: str, cost: float, params={}): + """ + + https://doc.xt.com/#orderorderPost + + create a market buy order by providing the symbol and cost + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' createMarketBuyOrderWithCost() supports spot orders only') + return await self.create_order(symbol, 'market', 'buy', cost, 1, params) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://doc.xt.com/#orderorderPost + https://doc.xt.com/#futures_ordercreate + https://doc.xt.com/#futures_entrustcreatePlan + https://doc.xt.com/#futures_entrustcreateProfit + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency + :param float [price]: the price to fulfill the order, in units of the quote currency, can be ignored in market orders + :param dict params: extra parameters specific to the xt api endpoint + :param str [params.timeInForce]: 'GTC', 'IOC', 'FOK' or 'GTX' + :param str [params.entrustType]: 'TAKE_PROFIT', 'STOP', 'TAKE_PROFIT_MARKET', 'STOP_MARKET', 'TRAILING_STOP_MARKET', required if stopPrice is defined, currently isn't functioning on xt's side + :param str [params.triggerPriceType]: 'INDEX_PRICE', 'MARK_PRICE', 'LATEST_PRICE', required if stopPrice is defined + :param float [params.triggerPrice]: price to trigger a stop order + :param float [params.stopPrice]: alias for triggerPrice + :param float [params.stopLoss]: price to set a stop-loss on an open position + :param float [params.takeProfit]: price to set a take-profit on an open position + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + if market['spot']: + return await self.create_spot_order(symbol, type, side, amount, price, params) + else: + return await self.create_contract_order(symbol, type, side, amount, price, params) + + async def create_spot_order(self, symbol: str, type, side, amount, price=None, params={}): + await self.load_markets() + market = self.market(symbol) + request = { + 'symbol': market['id'], + 'side': side.upper(), + 'type': type.upper(), + } + timeInForce = None + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('createOrder', params) + marginOrSpotRequest = 'LEVER' if (marginMode is not None) else 'SPOT' + request['bizType'] = marginOrSpotRequest + if type == 'market': + timeInForce = self.safe_string_upper(params, 'timeInForce', 'FOK') + if side == 'buy': + cost = self.safe_string(params, 'cost') + params = self.omit(params, 'cost') + createMarketBuyOrderRequiresPrice = self.safe_bool(self.options, 'createMarketBuyOrderRequiresPrice', True) + if createMarketBuyOrderRequiresPrice: + if price is None and (cost is None): + raise InvalidOrder(self.id + ' createOrder() requires a price argument or cost in params for market buy orders on spot markets to calculate the total amount to spend(amount * price), alternatively set the createMarketBuyOrderRequiresPrice option to False and pass in the cost to spend into the amount parameter') + else: + amountString = self.number_to_string(amount) + priceString = self.number_to_string(price) + costCalculated: Str = None + if price is not None: + costCalculated = Precise.string_mul(amountString, priceString) + else: + costCalculated = cost + request['quoteQty'] = self.cost_to_precision(symbol, costCalculated) + else: + amountCost = cost if (cost is not None) else amount + request['quoteQty'] = self.cost_to_precision(symbol, amountCost) + else: + timeInForce = self.safe_string_upper(params, 'timeInForce', 'GTC') + request['price'] = self.price_to_precision(symbol, price) + if (side == 'sell') or (type == 'limit'): + request['quantity'] = self.amount_to_precision(symbol, amount) + request['timeInForce'] = timeInForce + response = await self.privateSpotPostOrder(self.extend(request, params)) + # + # { + # "rc": 0, + # "mc": "SUCCESS", + # "ma": [], + # "result": { + # "orderId": "204371980095156544" + # } + # } + # + order = self.safe_value(response, 'result', {}) + return self.parse_order(order, market) + + async def create_contract_order(self, symbol: str, type, side, amount, price=None, params={}): + await self.load_markets() + market = self.market(symbol) + request = { + 'symbol': market['id'], + 'origQty': self.amount_to_precision(symbol, amount), + } + timeInForce = self.safe_string_upper(params, 'timeInForce') + if timeInForce is not None: + request['timeInForce'] = timeInForce + reduceOnly = self.safe_value(params, 'reduceOnly', False) + if side == 'buy': + requestType = 'SHORT' if (reduceOnly) else 'LONG' + request['positionSide'] = requestType + else: + requestType = 'LONG' if (reduceOnly) else 'SHORT' + request['positionSide'] = requestType + response = None + triggerPrice = self.safe_number_2(params, 'triggerPrice', 'stopPrice') + stopLoss = self.safe_number_2(params, 'stopLoss', 'triggerStopPrice') + takeProfit = self.safe_number_2(params, 'takeProfit', 'triggerProfitPrice') + isTrigger = (triggerPrice is not None) + isStopLoss = (stopLoss is not None) + isTakeProfit = (takeProfit is not None) + if price is not None: + if not (isStopLoss) and not (isTakeProfit): + request['price'] = self.price_to_precision(symbol, price) + if isTrigger: + request['timeInForce'] = self.safe_string_upper(params, 'timeInForce', 'GTC') + request['triggerPriceType'] = self.safe_string(params, 'triggerPriceType', 'LATEST_PRICE') + request['orderSide'] = side.upper() + request['stopPrice'] = self.price_to_precision(symbol, triggerPrice) + entrustType = 'STOP_MARKET' if (type == 'market') else 'STOP' + request['entrustType'] = entrustType + params = self.omit(params, 'triggerPrice') + if market['linear']: + response = await self.privateLinearPostFutureTradeV1EntrustCreatePlan(self.extend(request, params)) + elif market['inverse']: + response = await self.privateInversePostFutureTradeV1EntrustCreatePlan(self.extend(request, params)) + elif isStopLoss or isTakeProfit: + if isStopLoss: + request['triggerStopPrice'] = self.price_to_precision(symbol, stopLoss) + else: + request['triggerProfitPrice'] = self.price_to_precision(symbol, takeProfit) + params = self.omit(params, ['stopLoss', 'takeProfit']) + if market['linear']: + response = await self.privateLinearPostFutureTradeV1EntrustCreateProfit(self.extend(request, params)) + elif market['inverse']: + response = await self.privateInversePostFutureTradeV1EntrustCreateProfit(self.extend(request, params)) + else: + request['orderSide'] = side.upper() + request['orderType'] = type.upper() + if market['linear']: + response = await self.privateLinearPostFutureTradeV1OrderCreate(self.extend(request, params)) + elif market['inverse']: + response = await self.privateInversePostFutureTradeV1OrderCreate(self.extend(request, params)) + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": "206410760006650176" + # } + # + return self.parse_order(response, market) + + async def fetch_order(self, id: str, symbol: str = None, params={}): + """ + fetches information on an order made by the user + + https://doc.xt.com/#orderorderGet + https://doc.xt.com/#futures_ordergetById + https://doc.xt.com/#futures_entrustgetPlanById + https://doc.xt.com/#futures_entrustgetProfitById + + :param str id: order id + :param str [symbol]: unified symbol of the market the order was made in + :param dict params: extra parameters specific to the xt api endpoint + :param bool [params.trigger]: if the order is a trigger order or not + :param bool [params.stopLossTakeProfit]: if the order is a stop-loss or take-profit order + :returns dict: An `order structure ` + """ + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + request = {} + type = None + subType = None + response = None + type, params = self.handle_market_type_and_params('fetchOrder', market, params) + subType, params = self.handle_sub_type_and_params('fetchOrder', market, params) + trigger = self.safe_value(params, 'stop') + stopLossTakeProfit = self.safe_value(params, 'stopLossTakeProfit') + if trigger: + request['entrustId'] = id + elif stopLossTakeProfit: + request['profitId'] = id + else: + request['orderId'] = id + if trigger: + params = self.omit(params, 'stop') + if subType == 'inverse': + response = await self.privateInverseGetFutureTradeV1EntrustPlanDetail(self.extend(request, params)) + else: + response = await self.privateLinearGetFutureTradeV1EntrustPlanDetail(self.extend(request, params)) + elif stopLossTakeProfit: + params = self.omit(params, 'stopLossTakeProfit') + if subType == 'inverse': + response = await self.privateInverseGetFutureTradeV1EntrustProfitDetail(self.extend(request, params)) + else: + response = await self.privateLinearGetFutureTradeV1EntrustProfitDetail(self.extend(request, params)) + elif subType == 'inverse': + response = await self.privateInverseGetFutureTradeV1OrderDetail(self.extend(request, params)) + elif (subType == 'linear') or (type == 'swap') or (type == 'future'): + response = await self.privateLinearGetFutureTradeV1OrderDetail(self.extend(request, params)) + else: + response = await self.privateSpotGetOrderOrderId(self.extend(request, params)) + # + # spot + # + # { + # "rc": 0, + # "mc": "SUCCESS", + # "ma": [], + # "result": { + # "symbol": "btc_usdt", + # "orderId": "207505997850909952", + # "clientOrderId": null, + # "baseCurrency": "btc", + # "quoteCurrency": "usdt", + # "side": "BUY", + # "type": "LIMIT", + # "timeInForce": "GTC", + # "price": "20000.00", + # "origQty": "0.001000", + # "origQuoteQty": "20.00", + # "executedQty": "0.000000", + # "leavingQty": "0.001000", + # "tradeBase": "0.000000", + # "tradeQuote": "0.00", + # "avgPrice": null, + # "fee": null, + # "feeCurrency": null, + # "closed": False, + # "state": "NEW", + # "time": 1679175285162, + # "updatedTime": 1679175285255 + # } + # } + # + # swap and future + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": { + # "orderId": "211451874783183936", + # "clientOrderId": null, + # "symbol": "btc_usdt", + # "orderType": "LIMIT", + # "orderSide": "BUY", + # "positionSide": "LONG", + # "timeInForce": "GTC", + # "closePosition": False, + # "price": "20000", + # "origQty": "10", + # "avgPrice": "0", + # "executedQty": "0", + # "marginFrozen": "1.34533334", + # "remark": null, + # "triggerProfitPrice": null, + # "triggerStopPrice": null, + # "sourceId": null, + # "sourceType": "DEFAULT", + # "forceClose": False, + # "closeProfit": null, + # "state": "NEW", + # "createdTime": 1680116055693, + # "updatedTime": 1680116055693 + # } + # } + # + # trigger + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": { + # "entrustId": "216300248132756992", + # "symbol": "btc_usdt", + # "entrustType": "STOP", + # "orderSide": "SELL", + # "positionSide": "SHORT", + # "timeInForce": "GTC", + # "closePosition": null, + # "price": "20000", + # "origQty": "1", + # "stopPrice": "19000", + # "triggerPriceType": "LATEST_PRICE", + # "state": "NOT_TRIGGERED", + # "marketOrderLevel": null, + # "createdTime": 1681271998064, + # "updatedTime": 1681271998064, + # "ordinary": False + # } + # } + # + # stop-loss and take-profit + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": { + # "profitId": "216306213226230400", + # "symbol": "btc_usdt", + # "positionSide": "LONG", + # "origQty": "1", + # "triggerPriceType": "LATEST_PRICE", + # "triggerProfitPrice": null, + # "triggerStopPrice": "20000", + # "entryPrice": null, + # "positionSize": null, + # "isolatedMargin": null, + # "executedQty": null, + # "avgPrice": null, + # "positionType": "ISOLATED", + # "state": "NOT_TRIGGERED", + # "createdTime": 1681273420039 + # } + # } + # + order = self.safe_value(response, 'result', {}) + return self.parse_order(order, market) + + async def fetch_orders(self, symbol: str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches information on multiple orders made by the user + + https://doc.xt.com/#orderhistoryOrderGet + https://doc.xt.com/#futures_ordergetHistory + https://doc.xt.com/#futures_entrustgetPlanHistory + + :param str [symbol]: unified market symbol of the market the orders were made in + :param int [since]: timestamp in ms of the earliest order + :param int [limit]: the maximum number of order structures to retrieve + :param dict params: extra parameters specific to the xt api endpoint + :param bool [params.trigger]: if the order is a trigger order or not + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + request = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + type = None + subType = None + response = None + type, params = self.handle_market_type_and_params('fetchOrders', market, params) + subType, params = self.handle_sub_type_and_params('fetchOrders', market, params) + trigger = self.safe_value_2(params, 'trigger', 'stop') + if trigger: + params = self.omit(params, ['trigger', 'stop']) + if subType == 'inverse': + response = await self.privateInverseGetFutureTradeV1EntrustPlanListHistory(self.extend(request, params)) + else: + response = await self.privateLinearGetFutureTradeV1EntrustPlanListHistory(self.extend(request, params)) + elif subType == 'inverse': + response = await self.privateInverseGetFutureTradeV1OrderListHistory(self.extend(request, params)) + elif (subType == 'linear') or (type == 'swap') or (type == 'future'): + response = await self.privateLinearGetFutureTradeV1OrderListHistory(self.extend(request, params)) + else: + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchOrders', params) + marginOrSpotRequest = 'LEVER' if (marginMode is not None) else 'SPOT' + request['bizType'] = marginOrSpotRequest + response = await self.privateSpotGetHistoryOrder(self.extend(request, params)) + # + # spot and margin + # + # { + # "rc": 0, + # "mc": "SUCCESS", + # "ma": [], + # "result": { + # "hasPrev": False, + # "hasNext": True, + # "items": [ + # { + # "symbol": "btc_usdt", + # "orderId": "207505997850909952", + # "clientOrderId": null, + # "baseCurrency": "btc", + # "quoteCurrency": "usdt", + # "side": "BUY", + # "type": "LIMIT", + # "timeInForce": "GTC", + # "price": "20000.00", + # "origQty": "0.001000", + # "origQuoteQty": "20.00", + # "executedQty": "0.000000", + # "leavingQty": "0.000000", + # "tradeBase": "0.000000", + # "tradeQuote": "0.00", + # "avgPrice": null, + # "fee": null, + # "feeCurrency": null, + # "closed": True, + # "state": "CANCELED", + # "time": 1679175285162, + # "updatedTime": 1679175488492 + # }, + # ] + # } + # } + # + # swap and future + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": { + # "hasPrev": False, + # "hasNext": True, + # "items": [ + # { + # "orderId": "207519546930995456", + # "clientOrderId": null, + # "symbol": "btc_usdt", + # "orderType": "LIMIT", + # "orderSide": "BUY", + # "positionSide": "LONG", + # "timeInForce": "GTC", + # "closePosition": False, + # "price": "20000", + # "origQty": "100", + # "avgPrice": "0", + # "executedQty": "0", + # "marginFrozen": "4.12", + # "remark": null, + # "triggerProfitPrice": null, + # "triggerStopPrice": null, + # "sourceId": null, + # "sourceType": "DEFAULT", + # "forceClose": False, + # "closeProfit": null, + # "state": "CANCELED", + # "createdTime": 1679178515689, + # "updatedTime": 1679180096172 + # }, + # ] + # } + # } + # + # stop + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": { + # "hasPrev": False, + # "hasNext": False, + # "items": [ + # { + # "entrustId": "216300248132756992", + # "symbol": "btc_usdt", + # "entrustType": "STOP", + # "orderSide": "SELL", + # "positionSide": "SHORT", + # "timeInForce": "GTC", + # "closePosition": null, + # "price": "20000", + # "origQty": "1", + # "stopPrice": "19000", + # "triggerPriceType": "LATEST_PRICE", + # "state": "USER_REVOCATION", + # "marketOrderLevel": null, + # "createdTime": 1681271998064, + # "updatedTime": 1681273188674, + # "ordinary": False + # }, + # ] + # } + # } + # + data = self.safe_value(response, 'result', {}) + orders = self.safe_value(data, 'items', []) + return self.parse_orders(orders, market, since, limit) + + async def fetch_orders_by_status(self, status, symbol: str = None, since: Int = None, limit: Int = None, params={}): + await self.load_markets() + request = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if limit is not None: + request['size'] = limit + if since is not None: + request['startTime'] = since + type = None + subType = None + response = None + type, params = self.handle_market_type_and_params('fetchOrdersByStatus', market, params) + subType, params = self.handle_sub_type_and_params('fetchOrdersByStatus', market, params) + trigger = self.safe_bool_2(params, 'stop', 'trigger') + stopLossTakeProfit = self.safe_value(params, 'stopLossTakeProfit') + if status == 'open': + if trigger or stopLossTakeProfit: + request['state'] = 'NOT_TRIGGERED' + elif type == 'swap': + request['state'] = 'UNFINISHED' # NEW & PARTIALLY_FILLED + elif status == 'closed': + if trigger or stopLossTakeProfit: + request['state'] = 'TRIGGERED' + else: + request['state'] = 'FILLED' + elif status == 'canceled': + if trigger or stopLossTakeProfit: + request['state'] = 'USER_REVOCATION' + else: + request['state'] = 'CANCELED' + else: + request['state'] = status + if trigger or stopLossTakeProfit or (subType is not None) or (type == 'swap') or (type == 'future'): + if since is not None: + request['startTime'] = since + if limit is not None: + request['size'] = limit + if trigger: + params = self.omit(params, ['stop', 'trigger']) + if subType == 'inverse': + response = await self.privateInverseGetFutureTradeV1EntrustPlanList(self.extend(request, params)) + else: + response = await self.privateLinearGetFutureTradeV1EntrustPlanList(self.extend(request, params)) + elif stopLossTakeProfit: + params = self.omit(params, 'stopLossTakeProfit') + if subType == 'inverse': + response = await self.privateInverseGetFutureTradeV1EntrustProfitList(self.extend(request, params)) + else: + response = await self.privateLinearGetFutureTradeV1EntrustProfitList(self.extend(request, params)) + elif (subType is not None) or (type == 'swap') or (type == 'future'): + if subType == 'inverse': + response = await self.privateInverseGetFutureTradeV1OrderList(self.extend(request, params)) + else: + response = await self.privateLinearGetFutureTradeV1OrderList(self.extend(request, params)) + else: + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchOrdersByStatus', params) + marginOrSpotRequest = 'LEVER' if (marginMode is not None) else 'SPOT' + request['bizType'] = marginOrSpotRequest + if status != 'open': + if since is not None: + request['startTime'] = since + if limit is not None: + request = self.omit(request, 'size') + request['limit'] = limit + response = await self.privateSpotGetHistoryOrder(self.extend(request, params)) + else: + response = await self.privateSpotGetOpenOrder(self.extend(request, params)) + # + # spot and margin + # + # { + # "rc": 0, + # "mc": "SUCCESS", + # "ma": [], + # "result": { + # "hasPrev": False, + # "hasNext": True, + # "items": [ + # { + # "symbol": "btc_usdt", + # "orderId": "207505997850909952", + # "clientOrderId": null, + # "baseCurrency": "btc", + # "quoteCurrency": "usdt", + # "side": "BUY", + # "type": "LIMIT", + # "timeInForce": "GTC", + # "price": "20000.00", + # "origQty": "0.001000", + # "origQuoteQty": "20.00", + # "executedQty": "0.000000", + # "leavingQty": "0.000000", + # "tradeBase": "0.000000", + # "tradeQuote": "0.00", + # "avgPrice": null, + # "fee": null, + # "feeCurrency": null, + # "closed": True, + # "state": "CANCELED", + # "time": 1679175285162, + # "updatedTime": 1679175488492 + # }, + # ] + # } + # } + # + # spot and margin: fetchOpenOrders + # + # { + # "rc": 0, + # "mc": "SUCCESS", + # "ma": [], + # "result": [ + # { + # "symbol": "eth_usdt", + # "orderId": "208249323222264320", + # "clientOrderId": null, + # "baseCurrency": "eth", + # "quoteCurrency": "usdt", + # "side": "BUY", + # "type": "LIMIT", + # "timeInForce": "GTC", + # "price": "1300.00", + # "origQty": "0.0032", + # "origQuoteQty": "4.16", + # "executedQty": "0.0000", + # "leavingQty": "0.0032", + # "tradeBase": "0.0000", + # "tradeQuote": "0.00", + # "avgPrice": null, + # "fee": null, + # "feeCurrency": null, + # "closed": False, + # "state": "NEW", + # "time": 1679352507741, + # "updatedTime": 1679352507869 + # }, + # ] + # } + # + # swap and future + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": { + # "page": 1, + # "ps": 10, + # "total": 25, + # "items": [ + # { + # "orderId": "207519546930995456", + # "clientOrderId": null, + # "symbol": "btc_usdt", + # "orderType": "LIMIT", + # "orderSide": "BUY", + # "positionSide": "LONG", + # "timeInForce": "GTC", + # "closePosition": False, + # "price": "20000", + # "origQty": "100", + # "avgPrice": "0", + # "executedQty": "0", + # "marginFrozen": "4.12", + # "remark": null, + # "triggerProfitPrice": null, + # "triggerStopPrice": null, + # "sourceId": null, + # "sourceType": "DEFAULT", + # "forceClose": False, + # "closeProfit": null, + # "state": "CANCELED", + # "createdTime": 1679178515689, + # "updatedTime": 1679180096172 + # }, + # ] + # } + # } + # + # stop + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": { + # "page": 1, + # "ps": 3, + # "total": 8, + # "items": [ + # { + # "entrustId": "216300248132756992", + # "symbol": "btc_usdt", + # "entrustType": "STOP", + # "orderSide": "SELL", + # "positionSide": "SHORT", + # "timeInForce": "GTC", + # "closePosition": null, + # "price": "20000", + # "origQty": "1", + # "stopPrice": "19000", + # "triggerPriceType": "LATEST_PRICE", + # "state": "USER_REVOCATION", + # "marketOrderLevel": null, + # "createdTime": 1681271998064, + # "updatedTime": 1681273188674, + # "ordinary": False + # }, + # ] + # } + # } + # + # stop-loss and take-profit + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": { + # "page": 1, + # "ps": 3, + # "total": 2, + # "items": [ + # { + # "profitId": "216306213226230400", + # "symbol": "btc_usdt", + # "positionSide": "LONG", + # "origQty": "1", + # "triggerPriceType": "LATEST_PRICE", + # "triggerProfitPrice": null, + # "triggerStopPrice": "20000", + # "entryPrice": "0", + # "positionSize": "0", + # "isolatedMargin": "0", + # "executedQty": "0", + # "avgPrice": null, + # "positionType": "ISOLATED", + # "state": "USER_REVOCATION", + # "createdTime": 1681273420039 + # }, + # ] + # } + # } + # + orders = [] + resultDict = self.safe_dict(response, 'result') + if resultDict is not None: + orders = self.safe_list(resultDict, 'items', []) + else: + orders = self.safe_list(response, 'result') + return self.parse_orders(orders, market, since, limit) + + async def fetch_open_orders(self, symbol: str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all unfilled currently open orders + + https://doc.xt.com/#orderopenOrderGet + https://doc.xt.com/#futures_ordergetOrders + https://doc.xt.com/#futures_entrustgetPlan + https://doc.xt.com/#futures_entrustgetProfit + + :param str [symbol]: unified market symbol of the market the orders were made in + :param int [since]: timestamp in ms of the earliest order + :param int [limit]: the maximum number of open order structures to retrieve + :param dict params: extra parameters specific to the xt api endpoint + :param bool [params.trigger]: if the order is a trigger order or not + :param bool [params.stopLossTakeProfit]: if the order is a stop-loss or take-profit order + :returns dict[]: a list of `order structures ` + """ + return await self.fetch_orders_by_status('open', symbol, since, limit, params) + + async def fetch_closed_orders(self, symbol: str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches information on multiple closed orders made by the user + + https://doc.xt.com/#orderhistoryOrderGet + https://doc.xt.com/#futures_ordergetOrders + https://doc.xt.com/#futures_entrustgetPlan + https://doc.xt.com/#futures_entrustgetProfit + + :param str [symbol]: unified market symbol of the market the orders were made in + :param int [since]: timestamp in ms of the earliest order + :param int [limit]: the maximum number of order structures to retrieve + :param dict params: extra parameters specific to the xt api endpoint + :param bool [params.trigger]: if the order is a trigger order or not + :param bool [params.stopLossTakeProfit]: if the order is a stop-loss or take-profit order + :returns dict[]: a list of `order structures ` + """ + return await self.fetch_orders_by_status('closed', symbol, since, limit, params) + + async def fetch_canceled_orders(self, symbol: str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches information on multiple canceled orders made by the user + + https://doc.xt.com/#orderhistoryOrderGet + https://doc.xt.com/#futures_ordergetOrders + https://doc.xt.com/#futures_entrustgetPlan + https://doc.xt.com/#futures_entrustgetProfit + + :param str [symbol]: unified market symbol of the market the orders were made in + :param int [since]: timestamp in ms of the earliest order + :param int [limit]: the maximum number of order structures to retrieve + :param dict params: extra parameters specific to the xt api endpoint + :param bool [params.trigger]: if the order is a trigger order or not + :param bool [params.stopLossTakeProfit]: if the order is a stop-loss or take-profit order + :returns dict: a list of `order structures ` + """ + return await self.fetch_orders_by_status('canceled', symbol, since, limit, params) + + async def cancel_order(self, id: str, symbol: str = None, params={}): + """ + cancels an open order + + https://doc.xt.com/#orderorderDel + https://doc.xt.com/#futures_ordercancel + https://doc.xt.com/#futures_entrustcancelPlan + https://doc.xt.com/#futures_entrustcancelProfit + + :param str id: order id + :param str [symbol]: unified symbol of the market the order was made in + :param dict params: extra parameters specific to the xt api endpoint + :param bool [params.trigger]: if the order is a trigger order or not + :param bool [params.stopLossTakeProfit]: if the order is a stop-loss or take-profit order + :returns dict: An `order structure ` + """ + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + request = {} + type = None + subType = None + response = None + type, params = self.handle_market_type_and_params('cancelOrder', market, params) + subType, params = self.handle_sub_type_and_params('cancelOrder', market, params) + trigger = self.safe_value_2(params, 'trigger', 'stop') + stopLossTakeProfit = self.safe_value(params, 'stopLossTakeProfit') + if trigger: + request['entrustId'] = id + elif stopLossTakeProfit: + request['profitId'] = id + else: + request['orderId'] = id + if trigger: + params = self.omit(params, ['trigger', 'stop']) + if subType == 'inverse': + response = await self.privateInversePostFutureTradeV1EntrustCancelPlan(self.extend(request, params)) + else: + response = await self.privateLinearPostFutureTradeV1EntrustCancelPlan(self.extend(request, params)) + elif stopLossTakeProfit: + params = self.omit(params, 'stopLossTakeProfit') + if subType == 'inverse': + response = await self.privateInversePostFutureTradeV1EntrustCancelProfitStop(self.extend(request, params)) + else: + response = await self.privateLinearPostFutureTradeV1EntrustCancelProfitStop(self.extend(request, params)) + elif subType == 'inverse': + response = await self.privateInversePostFutureTradeV1OrderCancel(self.extend(request, params)) + elif (subType == 'linear') or (type == 'swap') or (type == 'future'): + response = await self.privateLinearPostFutureTradeV1OrderCancel(self.extend(request, params)) + else: + response = await self.privateSpotDeleteOrderOrderId(self.extend(request, params)) + # + # spot + # + # { + # "rc": 0, + # "mc": "SUCCESS", + # "ma": [], + # "result": { + # "cancelId": "208322474307982720" + # } + # } + # + # swap and future + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": "208319789679471616" + # } + # + isContractResponse = ((subType is not None) or (type == 'swap') or (type == 'future')) + order = response if isContractResponse else self.safe_value(response, 'result', {}) + return self.parse_order(order, market) + + async def cancel_all_orders(self, symbol: str = None, params={}): + """ + cancel all open orders in a market + + https://doc.xt.com/#orderopenOrderDel + https://doc.xt.com/#futures_ordercancelBatch + https://doc.xt.com/#futures_entrustcancelPlanBatch + https://doc.xt.com/#futures_entrustcancelProfitBatch + + :param str [symbol]: unified market symbol of the market to cancel orders in + :param dict params: extra parameters specific to the xt api endpoint + :param bool [params.trigger]: if the order is a trigger order or not + :param bool [params.stopLossTakeProfit]: if the order is a stop-loss or take-profit order + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + request = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + type = None + subType = None + response = None + type, params = self.handle_market_type_and_params('cancelAllOrders', market, params) + subType, params = self.handle_sub_type_and_params('cancelAllOrders', market, params) + trigger = self.safe_value_2(params, 'trigger', 'stop') + stopLossTakeProfit = self.safe_value(params, 'stopLossTakeProfit') + if trigger: + params = self.omit(params, ['trigger', 'stop']) + if subType == 'inverse': + response = await self.privateInversePostFutureTradeV1EntrustCancelAllPlan(self.extend(request, params)) + else: + response = await self.privateLinearPostFutureTradeV1EntrustCancelAllPlan(self.extend(request, params)) + elif stopLossTakeProfit: + params = self.omit(params, 'stopLossTakeProfit') + if subType == 'inverse': + response = await self.privateInversePostFutureTradeV1EntrustCancelAllProfitStop(self.extend(request, params)) + else: + response = await self.privateLinearPostFutureTradeV1EntrustCancelAllProfitStop(self.extend(request, params)) + elif subType == 'inverse': + response = await self.privateInversePostFutureTradeV1OrderCancelAll(self.extend(request, params)) + elif (subType == 'linear') or (type == 'swap') or (type == 'future'): + response = await self.privateLinearPostFutureTradeV1OrderCancelAll(self.extend(request, params)) + else: + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('cancelAllOrders', params) + marginOrSpotRequest = 'LEVER' if (marginMode is not None) else 'SPOT' + request['bizType'] = marginOrSpotRequest + response = await self.privateSpotDeleteOpenOrder(self.extend(request, params)) + # + # spot and margin + # + # { + # "rc": 0, + # "mc": "SUCCESS", + # "ma": [], + # "result": null + # } + # + # swap and future + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": True + # } + # + return [ + self.safe_order(response), + ] + + async def cancel_orders(self, ids: List[str], symbol: str = None, params={}) -> List[Order]: + """ + cancel multiple orders + + https://doc.xt.com/#orderbatchOrderDel + + :param str[] ids: order ids + :param str [symbol]: unified market symbol of the market to cancel orders in + :param dict params: extra parameters specific to the xt api endpoint + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + request = { + 'orderIds': ids, + } + market = None + if symbol is not None: + market = self.market(symbol) + subType = None + subType, params = self.handle_sub_type_and_params('cancelOrders', market, params) + if subType is not None: + raise NotSupported(self.id + ' cancelOrders() does not support swap and future orders, only spot orders are accepted') + response = await self.privateSpotDeleteBatchOrder(self.extend(request, params)) + # + # spot + # + # { + # "rc": 0, + # "mc": "SUCCESS", + # "ma": [], + # "result": null + # } + # + return [ + self.safe_order(response), + ] + + def parse_order(self, order, market=None): + # + # spot: createOrder + # + # { + # "orderId": "204371980095156544" + # } + # + # spot: cancelOrder + # + # { + # "cancelId": "208322474307982720" + # } + # + # swap and future: createOrder, cancelOrder, editOrder + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": "206410760006650176" + # } + # + # spot: fetchOrder, fetchOrders, fetchOpenOrders, fetchClosedOrders, fetchCanceledOrders, fetchOrdersByStatus + # + # { + # "symbol": "btc_usdt", + # "orderId": "207505997850909952", + # "clientOrderId": null, + # "baseCurrency": "btc", + # "quoteCurrency": "usdt", + # "side": "BUY", + # "type": "LIMIT", + # "timeInForce": "GTC", + # "price": "20000.00", + # "origQty": "0.001000", + # "origQuoteQty": "20.00", + # "executedQty": "0.000000", + # "leavingQty": "0.001000", + # "tradeBase": "0.000000", + # "tradeQuote": "0.00", + # "avgPrice": null, + # "fee": null, + # "feeCurrency": null, + # "closed": False, + # "state": "NEW", + # "time": 1679175285162, + # "updatedTime": 1679175285255 + # } + # + # swap and future: fetchOrder, fetchOrders, fetchOpenOrders, fetchClosedOrders, fetchCanceledOrders, fetchOrdersByStatus + # + # { + # "orderId": "207519546930995456", + # "clientOrderId": null, + # "symbol": "btc_usdt", + # "orderType": "LIMIT", + # "orderSide": "BUY", + # "positionSide": "LONG", + # "timeInForce": "GTC", + # "closePosition": False, + # "price": "20000", + # "origQty": "100", + # "avgPrice": "0", + # "executedQty": "0", + # "marginFrozen": "4.12", + # "remark": null, + # "triggerProfitPrice": null, + # "triggerStopPrice": null, + # "sourceId": null, + # "sourceType": "DEFAULT", + # "forceClose": False, + # "closeProfit": null, + # "state": "CANCELED", + # "createdTime": 1679178515689, + # "updatedTime": 1679180096172 + # } + # + # trigger: fetchOrder, fetchOrders, fetchOpenOrders, fetchClosedOrders, fetchCanceledOrders, fetchOrdersByStatus + # + # { + # "entrustId": "216300248132756992", + # "symbol": "btc_usdt", + # "entrustType": "STOP", + # "orderSide": "SELL", + # "positionSide": "SHORT", + # "timeInForce": "GTC", + # "closePosition": null, + # "price": "20000", + # "origQty": "1", + # "stopPrice": "19000", + # "triggerPriceType": "LATEST_PRICE", + # "state": "NOT_TRIGGERED", + # "marketOrderLevel": null, + # "createdTime": 1681271998064, + # "updatedTime": 1681271998064, + # "ordinary": False + # } + # + # stop-loss and take-profit: fetchOrder, fetchOpenOrders, fetchClosedOrders, fetchCanceledOrders, fetchOrdersByStatus + # + # { + # "profitId": "216306213226230400", + # "symbol": "btc_usdt", + # "positionSide": "LONG", + # "origQty": "1", + # "triggerPriceType": "LATEST_PRICE", + # "triggerProfitPrice": null, + # "triggerStopPrice": "20000", + # "entryPrice": null, + # "positionSize": null, + # "isolatedMargin": null, + # "executedQty": null, + # "avgPrice": null, + # "positionType": "ISOLATED", + # "state": "NOT_TRIGGERED", + # "createdTime": 1681273420039 + # } + # + # spot editOrder + # + # { + # "orderId": "484203027161892224", + # "modifyId": "484203544105344000", + # "clientModifyId": null + # } + # + marketId = self.safe_string(order, 'symbol') + marketType = ('result' in order) or 'contract' if ('positionSide' in order) else 'spot' + market = self.safe_market(marketId, market, None, marketType) + symbol = self.safe_symbol(marketId, market, None, marketType) + timestamp = self.safe_integer_2(order, 'time', 'createdTime') + quantity = self.safe_number(order, 'origQty') + amount = quantity if (marketType == 'spot') else Precise.string_mul(self.number_to_string(quantity), self.number_to_string(market['contractSize'])) + filledQuantity = self.safe_number(order, 'executedQty') + filled = filledQuantity if (marketType == 'spot') else Precise.string_mul(self.number_to_string(filledQuantity), self.number_to_string(market['contractSize'])) + lastUpdatedTimestamp = self.safe_integer(order, 'updatedTime') + return self.safe_order({ + 'info': order, + 'id': self.safe_string_n(order, ['orderId', 'result', 'cancelId', 'entrustId', 'profitId']), + 'clientOrderId': self.safe_string_2(order, 'clientOrderId', 'clientModifyId'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': lastUpdatedTimestamp, + 'lastUpdateTimestamp': lastUpdatedTimestamp, + 'symbol': symbol, + 'type': self.safe_string_lower_2(order, 'type', 'orderType'), + 'timeInForce': self.safe_string(order, 'timeInForce'), + 'postOnly': None, + 'side': self.safe_string_lower_2(order, 'side', 'orderSide'), + 'price': self.safe_number(order, 'price'), + 'triggerPrice': self.safe_number(order, 'stopPrice'), + 'stopLoss': self.safe_number(order, 'triggerStopPrice'), + 'takeProfit': self.safe_number(order, 'triggerProfitPrice'), + 'amount': amount, + 'filled': filled, + 'remaining': self.safe_number(order, 'leavingQty'), + 'cost': None, + 'average': self.safe_number(order, 'avgPrice'), + 'status': self.parse_order_status(self.safe_string(order, 'state')), + 'fee': { + 'currency': self.safe_currency_code(self.safe_string(order, 'feeCurrency')), + 'cost': self.safe_number(order, 'fee'), + }, + 'trades': None, + }, market) + + def parse_order_status(self, status): + statuses = { + 'NEW': 'open', + 'PARTIALLY_FILLED': 'open', + 'FILLED': 'closed', + 'CANCELED': 'canceled', + 'REJECTED': 'rejected', + 'EXPIRED': 'expired', + 'UNFINISHED': 'open', + 'NOT_TRIGGERED': 'open', + 'TRIGGERING': 'open', + 'TRIGGERED': 'closed', + 'USER_REVOCATION': 'canceled', + 'PLATFORM_REVOCATION': 'rejected', + 'HISTORY': 'expired', + } + return self.safe_string(statuses, status, status) + + async def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + + https://doc.xt.com/#futures_usergetBalanceBill + + :param str [code]: unified currency code + :param int [since]: timestamp in ms of the earliest ledger entry + :param int [limit]: max number of ledger entries to return + :param dict params: extra parameters specific to the xt api endpoint + :returns dict: a `ledger structure ` + """ + await self.load_markets() + request = {} + currency = None + if code is not None: + currency = self.currency(code) + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + type = None + subType = None + response = None + type, params = self.handle_market_type_and_params('fetchLedger', None, params) + subType, params = self.handle_sub_type_and_params('fetchLedger', None, params) + if subType == 'inverse': + response = await self.privateInverseGetFutureUserV1BalanceBills(self.extend(request, params)) + elif (subType == 'linear') or (type == 'swap') or (type == 'future'): + response = await self.privateLinearGetFutureUserV1BalanceBills(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchLedger() does not support spot transactions, only swap and future wallet transactions are supported') + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": { + # "hasPrev": False, + # "hasNext": False, + # "items": [ + # { + # "id": "207260567109387524", + # "coin": "usdt", + # "symbol": "btc_usdt", + # "type": "FEE", + # "amount": "-0.0213", + # "side": "SUB", + # "afterAmount": null, + # "createdTime": 1679116769914 + # }, + # ] + # } + # } + # + data = self.safe_value(response, 'result', {}) + ledger = self.safe_value(data, 'items', []) + return self.parse_ledger(ledger, currency, since, limit) + + def parse_ledger_entry(self, item, currency=None) -> LedgerEntry: + # + # { + # "id": "207260567109387524", + # "coin": "usdt", + # "symbol": "btc_usdt", + # "type": "FEE", + # "amount": "-0.0213", + # "side": "SUB", + # "afterAmount": null, + # "createdTime": 1679116769914 + # } + # + side = self.safe_string(item, 'side') + direction = 'in' if (side == 'ADD') else 'out' + currencyId = self.safe_string(item, 'coin') + currency = self.safe_currency(currencyId, currency) + timestamp = self.safe_integer(item, 'createdTime') + return self.safe_ledger_entry({ + 'info': item, + 'id': self.safe_string(item, 'id'), + 'direction': direction, + 'account': None, + 'referenceId': None, + 'referenceAccount': None, + 'type': self.parse_ledger_entry_type(self.safe_string(item, 'type')), + 'currency': self.safe_currency_code(currencyId, currency), + 'amount': self.safe_number(item, 'amount'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'before': None, + 'after': self.safe_number(item, 'afterAmount'), + 'status': None, + 'fee': { + 'currency': None, + 'cost': None, + }, + }, currency) + + def parse_ledger_entry_type(self, type): + ledgerType = { + 'EXCHANGE': 'transfer', + 'CLOSE_POSITION': 'trade', + 'TAKE_OVER': 'trade', + 'MERGE': 'trade', + 'QIANG_PING_MANAGER': 'fee', + 'FUND': 'fee', + 'FEE': 'fee', + 'ADL': 'auto-deleveraging', + } + return self.safe_string(ledgerType, type, type) + + async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://doc.xt.com/#deposit_withdrawaldepositAddressGet + + :param str code: unified currency code + :param dict params: extra parameters specific to the xt api endpoint + :param str params['network']: required network id + :returns dict: an `address structure ` + """ + await self.load_markets() + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + currency = self.currency(code) + networkId = self.network_code_to_id(networkCode, code) + self.check_required_argument('fetchDepositAddress', networkId, 'network') + request = { + 'currency': currency['id'], + 'chain': networkId, + } + response = await self.privateSpotGetDepositAddress(self.extend(request, params)) + # + # { + # "rc": 0, + # "mc": "SUCCESS", + # "ma": [], + # "result": { + # "address": "0x7f7173cf29d3846d20ca5a3aec1120b93dbd157a", + # "memo": "" + # } + # } + # + result = self.safe_value(response, 'result', {}) + return self.parse_deposit_address(result, currency) + + def parse_deposit_address(self, depositAddress, currency=None) -> DepositAddress: + # + # { + # "address": "0x7f7173cf29d3846d20ca5a3aec1120b93dbd157a", + # "memo": "" + # } + # + address = self.safe_string(depositAddress, 'address') + self.check_address(address) + return { + 'info': depositAddress, + 'currency': self.safe_currency_code(None, currency), + 'network': None, + 'address': address, + 'tag': self.safe_string(depositAddress, 'memo'), + } + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all deposits made to an account + + https://doc.xt.com/#deposit_withdrawalhistoryDepositGet + + :param str [code]: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of transaction structures to retrieve + :param dict params: extra parameters specific to the xt api endpoint + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + request = {} + currency = None + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit # default 10, max 200 + response = await self.privateSpotGetDepositHistory(self.extend(request, params)) + # + # { + # "rc": 0, + # "mc": "SUCCESS", + # "ma": [], + # "result": { + # "hasPrev": False, + # "hasNext": False, + # "items": [ + # { + # "id": 170368702, + # "currency": "usdt", + # "chain": "Ethereum", + # "memo": "", + # "status": "SUCCESS", + # "amount": "31.792528", + # "confirmations": 12, + # "transactionId": "0x90b8487c258b81b85e15e461b1839c49d4d8e6e9de4c1adb658cd47d4f5c5321", + # "address": "0x7f7172cf29d3846d30ca5a3aec1120b92dbd150b", + # "fromAddr": "0x7830c87c02e56aff27fa9ab1241711331fa86f58", + # "createdTime": 1678491442000 + # }, + # ] + # } + # } + # + data = self.safe_value(response, 'result', {}) + deposits = self.safe_value(data, 'items', []) + return self.parse_transactions(deposits, currency, since, limit, params) + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all withdrawals made from an account + + https://doc.xt.com/#deposit_withdrawalwithdrawHistory + + :param str [code]: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of transaction structures to retrieve + :param dict params: extra parameters specific to the xt api endpoint + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + request = {} + currency = None + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit # default 10, max 200 + response = await self.privateSpotGetWithdrawHistory(self.extend(request, params)) + # + # { + # "rc": 0, + # "mc": "SUCCESS", + # "ma": [], + # "result": { + # "hasPrev": False, + # "hasNext": False, + # "items": [ + # { + # "id": 950898, + # "currency": "usdt", + # "chain": "Tron", + # "address": "TGB2vxTjiqraVZBy7YHXF8V3CSMVhQKcaf", + # "memo": "", + # "status": "SUCCESS", + # "amount": "5", + # "fee": "2", + # "confirmations": 6, + # "transactionId": "c36e230b879842b1d7afd19d15ee1a866e26eaa0626e367d6f545d2932a15156", + # "createdTime": 1680049062000 + # } + # ] + # } + # } + # + data = self.safe_value(response, 'result', {}) + withdrawals = self.safe_value(data, 'items', []) + return self.parse_transactions(withdrawals, currency, since, limit, params) + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://doc.xt.com/#deposit_withdrawalwithdraw + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str [tag]: + :param dict params: extra parameters specific to the xt api endpoint + :returns dict: a `transaction structure ` + """ + self.check_address(address) + await self.load_markets() + currency = self.currency(code) + tag, params = self.handle_withdraw_tag_and_params(tag, params) + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + networkIdsByCodes = self.safe_value(self.options, 'networks', {}) + networkId = self.safe_string_2(networkIdsByCodes, networkCode, code, code) + request = { + 'currency': currency['id'], + 'chain': networkId, + 'amount': self.currency_to_precision(code, amount), + 'address': address, + } + if tag is not None: + request['memo'] = tag + response = await self.privateSpotPostWithdraw(self.extend(request, params)) + # + # { + # "rc": 0, + # "mc": "SUCCESS", + # "ma": [], + # "result": { + # "id": 950898 + # } + # } + # + result = self.safe_value(response, 'result', {}) + return self.parse_transaction(result, currency) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fetchDeposits + # + # { + # "id": 170368702, + # "currency": "usdt", + # "chain": "Ethereum", + # "memo": "", + # "status": "SUCCESS", + # "amount": "31.792528", + # "confirmations": 12, + # "transactionId": "0x90b8487c258b81b85e15e461b1839c49d4d8e6e9de4c1adb658cd47d4f5c5321", + # "address": "0x7f7172cf29d3846d30ca5a3aec1120b92dbd150b", + # "fromAddr": "0x7830c87c02e56aff27fa9ab1241711331fa86f58", + # "createdTime": 1678491442000 + # } + # + # fetchWithdrawals + # + # { + # "id": 950898, + # "currency": "usdt", + # "chain": "Tron", + # "address": "TGB2vxTjiqraVZBy7YHXF8V3CSMVhQKcaf", + # "memo": "", + # "status": "SUCCESS", + # "amount": "5", + # "fee": "2", + # "confirmations": 6, + # "transactionId": "c36e230b879842b1d7afd19d15ee1a866e26eaa0626e367d6f545d2932a15156", + # "createdTime": 1680049062000 + # } + # + # withdraw + # + # { + # "id": 950898 + # } + # + type = 'deposit' if ('fromAddr' in transaction) else 'withdraw' + timestamp = self.safe_integer(transaction, 'createdTime') + address = self.safe_string(transaction, 'address') + memo = self.safe_string(transaction, 'memo') + currencyCode = self.safe_currency_code(self.safe_string(transaction, 'currency'), currency) + fee = self.safe_number(transaction, 'fee') + feeCurrency = currencyCode if (fee is not None) else None + networkId = self.safe_string(transaction, 'chain') + return { + 'info': transaction, + 'id': self.safe_string(transaction, 'id'), + 'txid': self.safe_string(transaction, 'transactionId'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'updated': None, + 'addressFrom': self.safe_string(transaction, 'fromAddr'), + 'addressTo': address, + 'address': address, + 'tagFrom': None, + 'tagTo': None, + 'tag': memo, + 'type': type, + 'amount': self.safe_number(transaction, 'amount'), + 'currency': currencyCode, + 'network': self.network_id_to_code(networkId, currencyCode), + 'status': self.parse_transaction_status(self.safe_string(transaction, 'status')), + 'comment': memo, + 'fee': { + 'currency': feeCurrency, + 'cost': fee, + 'rate': None, + }, + 'internal': None, + } + + def parse_transaction_status(self, status): + statuses = { + 'SUBMIT': 'pending', + 'REVIEW': 'pending', + 'AUDITED': 'pending', + 'PENDING': 'pending', + 'CANCEL': 'canceled', + 'FAIL': 'failed', + 'SUCCESS': 'ok', + } + return self.safe_string(statuses, status, status) + + async def set_leverage(self, leverage: int, symbol: str = None, params={}): + """ + set the level of leverage for a market + + https://doc.xt.com/#futures_useradjustLeverage + + :param float leverage: the rate of leverage + :param str symbol: unified market symbol + :param dict params: extra parameters specific to the xt api endpoint + :param str params['positionSide']: 'LONG' or 'SHORT' + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setLeverage() requires a symbol argument') + positionSide = self.safe_string(params, 'positionSide') + self.check_required_argument('setLeverage', positionSide, 'positionSide', ['LONG', 'SHORT']) + if (leverage < 1) or (leverage > 125): + raise BadRequest(self.id + ' setLeverage() leverage should be between 1 and 125') + await self.load_markets() + market = self.market(symbol) + if not (market['contract']): + raise BadSymbol(self.id + ' setLeverage() supports contract markets only') + request = { + 'symbol': market['id'], + 'positionSide': positionSide, + 'leverage': leverage, + } + subType = None + subType, params = self.handle_sub_type_and_params('setLeverage', market, params) + response = None + if subType == 'inverse': + response = await self.privateInversePostFutureUserV1PositionAdjustLeverage(self.extend(request, params)) + else: + response = await self.privateLinearPostFutureUserV1PositionAdjustLeverage(self.extend(request, params)) + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": null + # } + # + return response + + async def add_margin(self, symbol: str, amount: float, params={}): + """ + add margin to a position + + https://doc.xt.com/#futures_useradjustMargin + + :param str symbol: unified market symbol + :param float amount: amount of margin to add + :param dict params: extra parameters specific to the xt api endpoint + :param str params['positionSide']: 'LONG' or 'SHORT' + :returns dict: a `margin structure ` + """ + return await self.modify_margin_helper(symbol, amount, 'ADD', params) + + async def reduce_margin(self, symbol: str, amount: float, params={}): + """ + remove margin from a position + + https://doc.xt.com/#futures_useradjustMargin + + :param str symbol: unified market symbol + :param float amount: the amount of margin to remove + :param dict params: extra parameters specific to the xt api endpoint + :param str params['positionSide']: 'LONG' or 'SHORT' + :returns dict: a `margin structure ` + """ + return await self.modify_margin_helper(symbol, amount, 'SUB', params) + + async def modify_margin_helper(self, symbol: str, amount, addOrReduce, params={}) -> MarginModification: + positionSide = self.safe_string(params, 'positionSide') + self.check_required_argument('setLeverage', positionSide, 'positionSide', ['LONG', 'SHORT']) + await self.load_markets() + market = self.market(symbol) + request = { + 'symbol': market['id'], + 'margin': amount, + 'type': addOrReduce, + 'positionSide': positionSide, + } + subType = None + subType, params = self.handle_sub_type_and_params('modifyMarginHelper', market, params) + response = None + if subType == 'inverse': + response = await self.privateInversePostFutureUserV1PositionMargin(self.extend(request, params)) + else: + response = await self.privateLinearPostFutureUserV1PositionMargin(self.extend(request, params)) + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": null + # } + # + return self.parse_margin_modification(response, market) + + def parse_margin_modification(self, data, market=None) -> MarginModification: + return { + 'info': data, + 'type': None, + 'amount': None, + 'code': None, + 'symbol': self.safe_symbol(None, market), + 'status': None, + 'marginMode': None, + 'total': None, + 'timestamp': None, + 'datetime': None, + } + + async def fetch_leverage_tiers(self, symbols: List[str] = None, params={}) -> LeverageTiers: + """ + retrieve information on the maximum leverage for different trade sizes + + https://doc.xt.com/#futures_quotesgetLeverageBrackets + + :param str [symbols]: a list of unified market symbols + :param dict params: extra parameters specific to the xt api endpoint + :returns dict: a dictionary of `leverage tiers structures ` + """ + await self.load_markets() + subType = None + subType, params = self.handle_sub_type_and_params('fetchLeverageTiers', None, params) + response = None + if subType == 'inverse': + response = await self.publicInverseGetFutureMarketV1PublicLeverageBracketList(params) + else: + response = await self.publicLinearGetFutureMarketV1PublicLeverageBracketList(params) + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": [ + # { + # "symbol": "rad_usdt", + # "leverageBrackets": [ + # { + # "symbol": "rad_usdt", + # "bracket": 1, + # "maxNominalValue": "5000", + # "maintMarginRate": "0.025", + # "startMarginRate": "0.05", + # "maxStartMarginRate": null, + # "maxLeverage": "20", + # "minLeverage": "1" + # }, + # ] + # }, + # ] + # } + # + data = self.safe_value(response, 'result', []) + symbols = self.market_symbols(symbols) + return self.parse_leverage_tiers(data, symbols, 'symbol') + + def parse_leverage_tiers(self, response, symbols=None, marketIdKey=None) -> LeverageTiers: + # + # { + # "symbol": "rad_usdt", + # "leverageBrackets": [ + # { + # "symbol": "rad_usdt", + # "bracket": 1, + # "maxNominalValue": "5000", + # "maintMarginRate": "0.025", + # "startMarginRate": "0.05", + # "maxStartMarginRate": null, + # "maxLeverage": "20", + # "minLeverage": "1" + # }, + # ] + # } + # + result = {} + for i in range(0, len(response)): + entry = response[i] + marketId = self.safe_string(entry, 'symbol') + market = self.safe_market(marketId, None, '_', 'contract') + symbol = self.safe_symbol(marketId, market) + if symbols is not None: + if self.in_array(symbol, symbols): + result[symbol] = self.parse_market_leverage_tiers(entry, market) + else: + result[symbol] = self.parse_market_leverage_tiers(response[i], market) + return result + + async def fetch_market_leverage_tiers(self, symbol: str, params={}) -> List[LeverageTier]: + """ + retrieve information on the maximum leverage for different trade sizes of a single market + + https://doc.xt.com/#futures_quotesgetLeverageBracket + + :param str symbol: unified market symbol + :param dict params: extra parameters specific to the xt api endpoint + :returns dict: a `leverage tiers structure ` + """ + await self.load_markets() + market = self.market(symbol) + request = { + 'symbol': market['id'], + } + subType = None + subType, params = self.handle_sub_type_and_params('fetchMarketLeverageTiers', market, params) + response = None + if subType == 'inverse': + response = await self.publicInverseGetFutureMarketV1PublicLeverageBracketDetail(self.extend(request, params)) + else: + response = await self.publicLinearGetFutureMarketV1PublicLeverageBracketDetail(self.extend(request, params)) + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": { + # "symbol": "btc_usdt", + # "leverageBrackets": [ + # { + # "symbol": "btc_usdt", + # "bracket": 1, + # "maxNominalValue": "500000", + # "maintMarginRate": "0.004", + # "startMarginRate": "0.008", + # "maxStartMarginRate": null, + # "maxLeverage": "125", + # "minLeverage": "1" + # }, + # ] + # } + # } + # + data = self.safe_value(response, 'result', {}) + return self.parse_market_leverage_tiers(data, market) + + def parse_market_leverage_tiers(self, info, market=None) -> List[LeverageTier]: + # + # { + # "symbol": "rad_usdt", + # "leverageBrackets": [ + # { + # "symbol": "rad_usdt", + # "bracket": 1, + # "maxNominalValue": "5000", + # "maintMarginRate": "0.025", + # "startMarginRate": "0.05", + # "maxStartMarginRate": null, + # "maxLeverage": "20", + # "minLeverage": "1" + # }, + # ] + # } + # + tiers = [] + brackets = self.safe_value(info, 'leverageBrackets', []) + for i in range(0, len(brackets)): + tier = brackets[i] + marketId = self.safe_string(info, 'symbol') + market = self.safe_market(marketId, market, '_', 'contract') + tiers.append({ + 'tier': self.safe_integer(tier, 'bracket'), + 'symbol': self.safe_symbol(marketId, market, '_', 'contract'), + 'currency': market['settle'], + 'minNotional': self.safe_number(brackets[i - 1], 'maxNominalValue', 0), + 'maxNotional': self.safe_number(tier, 'maxNominalValue'), + 'maintenanceMarginRate': self.safe_number(tier, 'maintMarginRate'), + 'maxLeverage': self.safe_number(tier, 'maxLeverage'), + 'info': tier, + }) + return tiers + + async def fetch_funding_rate_history(self, symbol: str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical funding rates + + https://doc.xt.com/#futures_quotesgetFundingRateRecord + + :param str [symbol]: unified symbol of the market to fetch the funding rate history for + :param int [since]: timestamp in ms of the earliest funding rate to fetch + :param int [limit]: the maximum amount of [funding rate structures] to fetch + :param dict params: extra parameters specific to the xt api endpoint + :param bool params['paginate']: True/false whether to use the pagination helper to aumatically paginate through the results + :returns dict[]: a list of `funding rate structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchFundingRateHistory() requires a symbol argument') + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchFundingRateHistory', 'paginate') + if paginate: + return await self.fetch_paginated_call_cursor('fetchFundingRateHistory', symbol, since, limit, params, 'id', 'id', 1, 200) + market = self.market(symbol) + if not market['swap']: + raise BadSymbol(self.id + ' fetchFundingRateHistory() supports swap contracts only') + request = { + 'symbol': market['id'], + } + if limit is not None: + request['limit'] = limit + else: + request['limit'] = 200 # max + subType = None + subType, params = self.handle_sub_type_and_params('fetchFundingRateHistory', market, params) + response = None + if subType == 'inverse': + response = await self.publicInverseGetFutureMarketV1PublicQFundingRateRecord(self.extend(request, params)) + else: + response = await self.publicLinearGetFutureMarketV1PublicQFundingRateRecord(self.extend(request, params)) + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": { + # "hasPrev": False, + # "hasNext": True, + # "items": [ + # { + # "id": "210441653482221888", + # "symbol": "btc_usdt", + # "fundingRate": "0.000057", + # "createdTime": 1679875200000, + # "collectionInternal": 28800 + # }, + # ] + # } + # } + # + result = self.safe_value(response, 'result', {}) + items = self.safe_value(result, 'items', []) + rates = [] + for i in range(0, len(items)): + entry = items[i] + marketId = self.safe_string(entry, 'symbol') + symbolInner = self.safe_symbol(marketId, market) + timestamp = self.safe_integer(entry, 'createdTime') + rates.append({ + 'info': entry, + 'symbol': symbolInner, + 'fundingRate': self.safe_number(entry, 'fundingRate'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + sorted = self.sort_by(rates, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, market['symbol'], since, limit) + + async def fetch_funding_interval(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate interval + + https://doc.xt.com/#futures_quotesgetFundingRate + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + return await self.fetch_funding_rate(symbol, params) + + async def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate + + https://doc.xt.com/#futures_quotesgetFundingRate + + :param str symbol: unified market symbol + :param dict params: extra parameters specific to the xt api endpoint + :returns dict: a `funding rate structure ` + """ + await self.load_markets() + market = self.market(symbol) + if not market['swap']: + raise BadSymbol(self.id + ' fetchFundingRate() supports swap contracts only') + request = { + 'symbol': market['id'], + } + subType = None + subType, params = self.handle_sub_type_and_params('fetchFundingRate', market, params) + response = None + if subType == 'inverse': + response = await self.publicInverseGetFutureMarketV1PublicQFundingRate(self.extend(request, params)) + else: + response = await self.publicLinearGetFutureMarketV1PublicQFundingRate(self.extend(request, params)) + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": { + # "symbol": "btc_usdt", + # "fundingRate": "0.000086", + # "nextCollectionTime": 1680307200000, + # "collectionInternal": 8 + # } + # } + # + result = self.safe_value(response, 'result', {}) + return self.parse_funding_rate(result, market) + + def parse_funding_rate(self, contract, market=None) -> FundingRate: + # + # { + # "symbol": "btc_usdt", + # "fundingRate": "0.000086", + # "nextCollectionTime": 1680307200000, + # "collectionInternal": 8 + # } + # + marketId = self.safe_string(contract, 'symbol') + symbol = self.safe_symbol(marketId, market, '_', 'swap') + timestamp = self.safe_integer(contract, 'nextCollectionTime') + interval = self.safe_string(contract, 'collectionInternal') + if interval is not None: + interval = interval + 'h' + return { + 'info': contract, + 'symbol': symbol, + 'markPrice': None, + 'indexPrice': None, + 'interestRate': None, + 'estimatedSettlePrice': None, + 'timestamp': None, + 'datetime': None, + 'fundingRate': self.safe_number(contract, 'fundingRate'), + 'fundingTimestamp': timestamp, + 'fundingDatetime': self.iso8601(timestamp), + 'nextFundingRate': None, + 'nextFundingTimestamp': None, + 'nextFundingDatetime': None, + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': interval, + } + + async def fetch_funding_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch the funding history + + https://doc.xt.com/#futures_usergetFunding + + :param str symbol: unified market symbol + :param int [since]: the starting timestamp in milliseconds + :param int [limit]: the number of entries to return + :param dict params: extra parameters specific to the xt api endpoint + :returns dict[]: a list of `funding history structures ` + """ + await self.load_markets() + market = self.market(symbol) + if not market['swap']: + raise BadSymbol(self.id + ' fetchFundingHistory() supports swap contracts only') + request = { + 'symbol': market['id'], + } + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + subType = None + subType, params = self.handle_sub_type_and_params('fetchFundingHistory', market, params) + response = None + if subType == 'inverse': + response = await self.privateInverseGetFutureUserV1BalanceFundingRateList(self.extend(request, params)) + else: + response = await self.privateLinearGetFutureUserV1BalanceFundingRateList(self.extend(request, params)) + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": { + # "hasPrev": False, + # "hasNext": False, + # "items": [ + # { + # "id": "210804044057280512", + # "symbol": "btc_usdt", + # "cast": "-0.0013", + # "coin": "usdt", + # "positionSide": "SHORT", + # "createdTime": 1679961600653 + # }, + # ] + # } + # } + # + data = self.safe_value(response, 'result', {}) + items = self.safe_value(data, 'items', []) + result = [] + for i in range(0, len(items)): + entry = items[i] + result.append(self.parse_funding_history(entry, market)) + sorted = self.sort_by(result, 'timestamp') + return self.filter_by_since_limit(sorted, since, limit) + + def parse_funding_history(self, contract, market=None): + # + # { + # "id": "210804044057280512", + # "symbol": "btc_usdt", + # "cast": "-0.0013", + # "coin": "usdt", + # "positionSide": "SHORT", + # "createdTime": 1679961600653 + # } + # + marketId = self.safe_string(contract, 'symbol') + symbol = self.safe_symbol(marketId, market, '_', 'swap') + currencyId = self.safe_string(contract, 'coin') + code = self.safe_currency_code(currencyId) + timestamp = self.safe_integer(contract, 'createdTime') + return { + 'info': contract, + 'symbol': symbol, + 'code': code, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': self.safe_string(contract, 'id'), + 'amount': self.safe_number(contract, 'cast'), + } + + async def fetch_position(self, symbol: str, params={}): + """ + fetch data on a single open contract trade position + + https://doc.xt.com/#futures_usergetPosition + + :param str symbol: unified market symbol of the market the position is held in + :param dict params: extra parameters specific to the xt api endpoint + :returns dict: a `position structure ` + """ + await self.load_markets() + market = self.market(symbol) + request = { + 'symbol': market['id'], + } + subType = None + subType, params = self.handle_sub_type_and_params('fetchPosition', market, params) + response = None + if subType == 'inverse': + response = await self.privateInverseGetFutureUserV1PositionList(self.extend(request, params)) + else: + response = await self.privateLinearGetFutureUserV1PositionList(self.extend(request, params)) + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": [ + # { + # "symbol": "btc_usdt", + # "positionType": "ISOLATED", + # "positionSide": "SHORT", + # "contractType": "PERPETUAL", + # "positionSize": "10", + # "closeOrderSize": "0", + # "availableCloseSize": "10", + # "entryPrice": "27060", + # "openOrderSize": "0", + # "isolatedMargin": "1.0824", + # "openOrderMarginFrozen": "0", + # "realizedProfit": "-0.00130138", + # "autoMargin": False, + # "leverage": 25 + # }, + # ] + # } + # + positions = self.safe_value(response, 'result', []) + for i in range(0, len(positions)): + entry = positions[i] + marketId = self.safe_string(entry, 'symbol') + marketInner = self.safe_market(marketId, None, None, 'contract') + positionSize = self.safe_string(entry, 'positionSize') + if positionSize != '0': + return self.parse_position(entry, marketInner) + return None + + async def fetch_positions(self, symbols: List[str] = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://doc.xt.com/#futures_usergetPosition + + :param str [symbols]: list of unified market symbols, not supported with xt + :param dict params: extra parameters specific to the xt api endpoint + :returns dict[]: a list of `position structure ` + """ + await self.load_markets() + subType = None + subType, params = self.handle_sub_type_and_params('fetchPositions', None, params) + response = None + if subType == 'inverse': + response = await self.privateInverseGetFutureUserV1PositionList(params) + else: + response = await self.privateLinearGetFutureUserV1PositionList(params) + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": [ + # { + # "symbol": "btc_usdt", + # "positionType": "ISOLATED", + # "positionSide": "SHORT", + # "contractType": "PERPETUAL", + # "positionSize": "10", + # "closeOrderSize": "0", + # "availableCloseSize": "10", + # "entryPrice": "27060", + # "openOrderSize": "0", + # "isolatedMargin": "1.0824", + # "openOrderMarginFrozen": "0", + # "realizedProfit": "-0.00130138", + # "autoMargin": False, + # "leverage": 25 + # }, + # ] + # } + # + positions = self.safe_value(response, 'result', []) + result = [] + for i in range(0, len(positions)): + entry = positions[i] + marketId = self.safe_string(entry, 'symbol') + marketInner = self.safe_market(marketId, None, None, 'contract') + result.append(self.parse_position(entry, marketInner)) + return self.filter_by_array_positions(result, 'symbol', symbols, False) + + def parse_position(self, position, market=None): + # + # { + # "symbol": "btc_usdt", + # "positionType": "ISOLATED", + # "positionSide": "SHORT", + # "contractType": "PERPETUAL", + # "positionSize": "10", + # "closeOrderSize": "0", + # "availableCloseSize": "10", + # "entryPrice": "27060", + # "openOrderSize": "0", + # "isolatedMargin": "1.0824", + # "openOrderMarginFrozen": "0", + # "realizedProfit": "-0.00130138", + # "autoMargin": False, + # "leverage": 25 + # } + # + marketId = self.safe_string(position, 'symbol') + market = self.safe_market(marketId, market, None, 'contract') + symbol = self.safe_symbol(marketId, market, None, 'contract') + positionType = self.safe_string(position, 'positionType') + marginMode = 'cross' if (positionType == 'CROSSED') else 'isolated' + collateral = self.safe_number(position, 'isolatedMargin') + return self.safe_position({ + 'info': position, + 'id': None, + 'symbol': symbol, + 'timestamp': None, + 'datetime': None, + 'hedged': None, + 'side': self.safe_string_lower(position, 'positionSide'), + 'contracts': self.safe_number(position, 'positionSize'), + 'contractSize': market['contractSize'], + 'entryPrice': self.safe_number(position, 'entryPrice'), + 'markPrice': None, + 'notional': None, + 'leverage': self.safe_integer(position, 'leverage'), + 'collateral': collateral, + 'initialMargin': collateral, + 'maintenanceMargin': None, + 'initialMarginPercentage': None, + 'maintenanceMarginPercentage': None, + 'unrealizedPnl': None, + 'liquidationPrice': None, + 'marginMode': marginMode, + 'percentage': None, + 'marginRatio': None, + }) + + async def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://doc.xt.com/#transfersubTransferPost + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account to transfer from - spot, swap, leverage, finance + :param str toAccount: account to transfer to - spot, swap, leverage, finance + :param dict params: extra parameters specific to the whitebit api endpoint + :returns dict: a `transfer structure ` + """ + await self.load_markets() + currency = self.currency(code) + accountsByType = self.safe_value(self.options, 'accountsById') + fromAccountId = self.safe_string(accountsByType, fromAccount, fromAccount) + toAccountId = self.safe_string(accountsByType, toAccount, toAccount) + amountString = self.currency_to_precision(code, amount) + request = { + 'bizId': self.uuid(), + 'currency': currency['id'], + 'amount': amountString, + 'from': fromAccountId, + 'to': toAccountId, + } + response = await self.privateSpotPostBalanceTransfer(self.extend(request, params)) + # + # { + # info: {rc: '0', mc: 'SUCCESS', ma: [], result: '226971333791398656'}, + # id: '226971333791398656', + # timestamp: None, + # datetime: None, + # currency: None, + # amount: None, + # fromAccount: None, + # toAccount: None, + # status: None + # } + # + return self.parse_transfer(response, currency) + + def parse_transfer(self, transfer, currency=None): + return { + 'info': transfer, + 'id': self.safe_string(transfer, 'result'), + 'timestamp': None, + 'datetime': None, + 'currency': None, + 'amount': None, + 'fromAccount': None, + 'toAccount': None, + 'status': None, + } + + async def set_margin_mode(self, marginMode: str, symbol: Str = None, params={}): + """ + set margin mode to 'cross' or 'isolated' + + https://doc.xt.com/#futures_userchangePositionType + + :param str marginMode: 'cross' or 'isolated' + :param str [symbol]: required + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.positionSide]: *required* "long" or "short" + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setMarginMode() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + if market['spot']: + raise BadSymbol(self.id + ' setMarginMode() supports contract markets only') + marginMode = marginMode.lower() + if marginMode != 'isolated' and marginMode != 'cross': + raise BadRequest(self.id + ' setMarginMode() marginMode argument should be isolated or cross') + if marginMode == 'cross': + marginMode = 'CROSSED' + else: + marginMode = 'ISOLATED' + posSide = self.safe_string_upper(params, 'positionSide') + if posSide is None: + raise ArgumentsRequired(self.id + ' setMarginMode() requires a positionSide parameter, either "LONG" or "SHORT"') + request: dict = { + 'positionType': marginMode, + 'positionSide': posSide, + 'symbol': market['id'], + } + response = await self.privateLinearPostFutureUserV1PositionChangeType(self.extend(request, params)) + # + # { + # "error": { + # "code": "", + # "msg": "" + # }, + # "msgInfo": "", + # "result": {}, + # "returnCode": 0 + # } + # + return response # unify return type + + async def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}) -> Order: + """ + cancels an order and places a new order + + https://doc.xt.com/#orderorderUpdate + https://doc.xt.com/#futures_orderupdate + https://doc.xt.com/#futures_entrustupdateProfit + + :param str id: order id + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of the currency you want to trade in units of the base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.stopLoss]: price to set a stop-loss on an open position + :param float [params.takeProfit]: price to set a take-profit on an open position + :returns dict: an `order structure ` + """ + if amount is None: + raise ArgumentsRequired(self.id + ' editOrder() requires an amount argument') + await self.load_markets() + market = self.market(symbol) + request = {} + stopLoss = self.safe_number_2(params, 'stopLoss', 'triggerStopPrice') + takeProfit = self.safe_number_2(params, 'takeProfit', 'triggerProfitPrice') + params = self.omit(params, ['stopLoss', 'takeProfit']) + isStopLoss = (stopLoss is not None) + isTakeProfit = (takeProfit is not None) + if isStopLoss or isTakeProfit: + request['profitId'] = id + else: + request['orderId'] = id + request['price'] = self.price_to_precision(symbol, price) + response = None + if market['swap']: + if isStopLoss: + request['triggerStopPrice'] = self.price_to_precision(symbol, stopLoss) + elif takeProfit is not None: + request['triggerProfitPrice'] = self.price_to_precision(symbol, takeProfit) + else: + request['origQty'] = self.amount_to_precision(symbol, amount) + subType = None + subType, params = self.handle_sub_type_and_params('editOrder', market, params) + if subType == 'inverse': + if isStopLoss or isTakeProfit: + response = await self.privateInversePostFutureTradeV1EntrustUpdateProfitStop(self.extend(request, params)) + else: + response = await self.privateInversePostFutureTradeV1OrderUpdate(self.extend(request, params)) + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": "483869474947826752" + # } + # + else: + if isStopLoss or isTakeProfit: + response = await self.privateLinearPostFutureTradeV1EntrustUpdateProfitStop(self.extend(request, params)) + else: + response = await self.privateLinearPostFutureTradeV1OrderUpdate(self.extend(request, params)) + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": "483869474947826752" + # } + # + else: + request['quantity'] = self.amount_to_precision(symbol, amount) + response = await self.privateSpotPutOrderOrderId(self.extend(request, params)) + # + # { + # "rc": 0, + # "mc": "SUCCESS", + # "ma": [], + # "result": { + # "orderId": "484203027161892224", + # "modifyId": "484203544105344000", + # "clientModifyId": null + # } + # } + # + result = response if (market['swap']) else self.safe_dict(response, 'result', {}) + return self.parse_order(result, market) + + def handle_errors(self, code, reason, url, method, headers, body, response, requestHeaders, requestBody): + # + # spot: error + # + # { + # "rc": 1, + # "mc": "AUTH_103", + # "ma": [], + # "result": null + # } + # + # spot: success + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": [] + # } + # + # swap and future: error + # + # { + # "returnCode": 1, + # "msgInfo": "failure", + # "error": { + # "code": "403", + # "msg": "invalid signature" + # }, + # "result": null + # } + # + # swap and future: success + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": null + # } + # + # other: + # + # { + # "rc": 0, + # "mc": "SUCCESS", + # "ma": [], + # "result": {} + # } + # + # {"returnCode":1,"msgInfo":"failure","error":{"code":"insufficient_balance","msg":"insufficient balance","args":[]},"result":null} + # + # + status = self.safe_string_upper_2(response, 'msgInfo', 'mc') + if status is not None and status != 'SUCCESS': + feedback = self.id + ' ' + body + error = self.safe_value(response, 'error', {}) + spotErrorCode = self.safe_string(response, 'mc') + errorCode = self.safe_string(error, 'code', spotErrorCode) + spotMessage = self.safe_string(response, 'msgInfo') + message = self.safe_string(error, 'msg', spotMessage) + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + raise ExchangeError(feedback) + return None + + def sign(self, path, api=[], method='GET', params={}, headers=None, body=None): + signed = api[0] == 'private' + endpoint = api[1] + request = '/' + self.implode_params(path, params) + payload = None + if (endpoint == 'spot') or (endpoint == 'user'): + if signed: + payload = '/' + self.version + request + else: + payload = '/' + self.version + '/public' + request + else: + payload = request + url = self.urls['api'][endpoint] + payload + query = self.omit(params, self.extract_params(path)) + urlencoded = self.urlencode(self.keysort(query)) + headers = { + 'Content-Type': 'application/json', + } + if signed: + self.check_required_credentials() + defaultRecvWindow = self.safe_string(self.options, 'recvWindow') + recvWindow = self.safe_string(query, 'recvWindow', defaultRecvWindow) + timestamp = self.number_to_string(self.nonce()) + body = query + if (payload == '/v4/order') or (payload == '/future/trade/v1/order/create') or (payload == '/future/trade/v1/entrust/create-plan') or (payload == '/future/trade/v1/entrust/create-profit') or (payload == '/future/trade/v1/order/create-batch'): + id = 'CCXT' + if payload.find('future') > -1: + body['clientMedia'] = id + else: + body['media'] = id + isUndefinedBody = ((method == 'GET') or (path == 'order/{orderId}') or (path == 'ws-token')) + if (method == 'PUT') and (endpoint == 'spot'): + isUndefinedBody = False + body = None if isUndefinedBody else self.json(body) + payloadString = None + if (endpoint == 'spot') or (endpoint == 'user'): + payloadString = 'xt-validate-algorithms=HmacSHA256&xt-validate-appkey=' + self.apiKey + '&xt-validate-recvwindow=' + recvWindow + '&xt-validate-t' + 'imestamp=' + timestamp + if isUndefinedBody: + if urlencoded: + url += '?' + urlencoded + payloadString += '#' + method + '#' + payload + '#' + self.rawencode(self.keysort(query)) + else: + payloadString += '#' + method + '#' + payload + else: + payloadString += '#' + method + '#' + payload + '#' + body + headers['xt-validate-algorithms'] = 'HmacSHA256' + headers['xt-validate-recvwindow'] = recvWindow + else: + payloadString = 'xt-validate-appkey=' + self.apiKey + '&xt-validate-t' + 'imestamp=' + timestamp # we can't glue timestamp, breaks in php + if method == 'GET': + if urlencoded: + url += '?' + urlencoded + payloadString += '#' + payload + '#' + urlencoded + else: + payloadString += '#' + payload + else: + payloadString += '#' + payload + '#' + body + signature = self.hmac(self.encode(payloadString), self.encode(self.secret), hashlib.sha256) + headers['xt-validate-appkey'] = self.apiKey + headers['xt-validate-timestamp'] = timestamp + headers['xt-validate-signature'] = signature + else: + if urlencoded: + url += '?' + urlencoded + return {'url': url, 'method': method, 'body': body, 'headers': headers} diff --git a/ccxt/async_support/yobit.py b/ccxt/async_support/yobit.py new file mode 100644 index 0000000..102ea5a --- /dev/null +++ b/ccxt/async_support/yobit.py @@ -0,0 +1,1418 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.yobit import ImplicitAPI +import asyncio +import hashlib +from ccxt.base.types import Any, Balances, DepositAddress, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, OrderBooks, Trade, TradingFees, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import DDoSProtection +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.errors import InvalidNonce +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class yobit(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(yobit, self).describe(), { + 'id': 'yobit', + 'name': 'YoBit', + 'countries': ['RU'], + 'rateLimit': 2000, # responses are cached every 2 seconds + 'version': '3', + 'pro': False, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelOrder': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createDepositAddress': True, + 'createMarketOrder': False, + 'createOrder': True, + 'createReduceOnlyOrder': False, + 'createStopLimitOrder': False, + 'createStopMarketOrder': False, + 'createStopOrder': False, + 'fetchAllGreeks': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': False, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': False, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrice': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrderBooks': True, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': True, + 'fetchTransactions': False, + 'fetchTransfer': False, + 'fetchTransfers': False, + 'fetchUnderlyingAssets': False, + 'fetchVolatilityHistory': False, + 'fetchWithdrawals': False, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'repayMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'transfer': False, + 'withdraw': True, + 'ws': False, + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/27766910-cdcbfdae-5eea-11e7-9859-03fea873272d.jpg', + 'api': { + 'public': 'https://yobit.net/api', + 'private': 'https://yobit.net/tapi', + }, + 'www': 'https://www.yobit.net', + 'doc': 'https://www.yobit.net/en/api/', + 'fees': 'https://www.yobit.net/en/fees/', + }, + 'api': { + 'public': { + 'get': { + 'depth/{pair}': 1, + 'info': 1, + 'ticker/{pair}': 1, + 'trades/{pair}': 1, + }, + }, + 'private': { + 'post': { + 'ActiveOrders': 1, + 'CancelOrder': 1, + 'GetDepositAddress': 1, + 'getInfo': 1, + 'OrderInfo': 1, + 'Trade': 1, + 'TradeHistory': 1, + 'WithdrawCoinsToAddress': 1, + }, + }, + }, + 'fees': { + 'trading': { + 'maker': 0.002, + 'taker': 0.002, + }, + 'funding': { + 'withdraw': {}, + }, + }, + 'commonCurrencies': { + 'AIR': 'AirCoin', + 'ANI': 'ANICoin', + 'ANT': 'AntsCoin', # what is self, a coin for ants? + 'ATMCHA': 'ATM', + 'ASN': 'Ascension', + 'AST': 'Astral', + 'ATM': 'Autumncoin', + 'AUR': 'AuroraCoin', + 'BAB': 'Babel', + 'BAN': 'BANcoin', + 'BCC': 'BCH', + 'BCS': 'BitcoinStake', + 'BITS': 'Bitstar', + 'BLN': 'Bulleon', + 'BNS': 'Benefit Bonus Coin', + 'BOT': 'BOTcoin', + 'BON': 'BONES', + 'BPC': 'BitcoinPremium', + 'BST': 'BitStone', + 'BTS': 'Bitshares2', + 'CAT': 'BitClave', + 'CBC': 'CryptoBossCoin', + 'CMT': 'CometCoin', + 'COIN': 'Coin.com', + 'COV': 'Coven Coin', + 'COVX': 'COV', + 'CPC': 'Capricoin', + 'CREDIT': 'Creditbit', + 'CS': 'CryptoSpots', + 'DCT': 'Discount', + 'DFT': 'DraftCoin', + 'DGD': 'DarkGoldCoin', + 'DIRT': 'DIRTY', + 'DROP': 'FaucetCoin', + 'DSH': 'DASH', + 'EGC': 'EverGreenCoin', + 'EGG': 'EggCoin', + 'EKO': 'EkoCoin', + 'ENTER': 'ENTRC', + 'EPC': 'ExperienceCoin', + 'ESC': 'EdwardSnowden', + 'EUROPE': 'EUROP', + 'EXT': 'LifeExtension', + 'FUND': 'FUNDChains', + 'FUNK': 'FUNKCoin', + 'FX': 'FCoin', + 'GCC': 'GlobalCryptocurrency', + 'GEN': 'Genstake', + 'GENE': 'Genesiscoin', + 'GMR': 'Gimmer', + 'GOLD': 'GoldMint', + 'GOT': 'Giotto Coin', + 'GSX': 'GlowShares', + 'GT': 'GTcoin', + 'HTML5': 'HTML', + 'HYPERX': 'HYPER', + 'ICN': 'iCoin', + 'INSANE': 'INSN', + 'JNT': 'JointCoin', + 'JPC': 'JupiterCoin', + 'JWL': 'Jewels', + 'KNC': 'KingN Coin', + 'LBTCX': 'LiteBitcoin', + 'LIZI': 'LiZi', + 'LOC': 'LocoCoin', + 'LOCX': 'LOC', + 'LUNYR': 'LUN', + 'LUN': 'LunarCoin', # they just change the ticker if it is already taken + 'LUNA': 'Luna Coin', + 'MASK': 'Yobit MASK', + 'MDT': 'Midnight', + 'MEME': 'Memez Token', # conflict with Meme Inu / Degenerator Meme + 'MIS': 'MIScoin', + 'MM': 'MasterMint', # conflict with MilliMeter + 'NAV': 'NavajoCoin', + 'NBT': 'NiceBytes', + 'OMG': 'OMGame', + 'ONX': 'Onix', + 'PAC': '$PAC', + 'PLAY': 'PlayCoin', + 'PIVX': 'Darknet', + 'PURE': 'PurePOS', + 'PUTIN': 'PutinCoin', + 'SPACE': 'Spacecoin', + 'STK': 'StakeCoin', + 'SUB': 'Subscriptio', + 'PAY': 'EPAY', + 'PLC': 'Platin Coin', + 'RAI': 'RaiderCoin', + 'RCN': 'RCoin', + 'REP': 'Republicoin', + 'RUR': 'RUB', + 'SBTC': 'Super Bitcoin', + 'SMC': 'SmartCoin', + 'SOLO': 'SoloCoin', + 'SOUL': 'SoulCoin', + 'STAR': 'StarCoin', + 'SUPER': 'SuperCoin', + 'TNS': 'Transcodium', + 'TTC': 'TittieCoin', + 'UNI': 'Universe', + 'UST': 'Uservice', + 'VOL': 'VolumeCoin', + 'XIN': 'XINCoin', + 'XMT': 'SummitCoin', + 'XRA': 'Ratecoin', + 'BCHN': 'BSV', + }, + 'options': { + 'maxUrlLength': 2048, + 'fetchOrdersRequiresSymbol': True, + 'networks': { + 'ETH': 'ERC20', + 'TRX': 'TRC20', + 'BSC': 'BEP20', + }, + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'exact': { + '803': InvalidOrder, # "Count could not be less than 0.001."(selling below minAmount) + '804': InvalidOrder, # "Count could not be more than 10000."(buying above maxAmount) + '805': InvalidOrder, # "price could not be less than X."(minPrice violation on buy & sell) + '806': InvalidOrder, # "price could not be more than X."(maxPrice violation on buy & sell) + '807': InvalidOrder, # "cost could not be less than X."(minCost violation on buy & sell) + '831': InsufficientFunds, # "Not enougth X to create buy order."(buying with balance.quote < order.cost) + '832': InsufficientFunds, # "Not enougth X to create sell order."(selling with balance.base < order.amount) + '833': OrderNotFound, # "Order with id X was not found."(cancelling non-existent, closed and cancelled order) + }, + 'broad': { + 'Invalid pair name': ExchangeError, # {"success":0,"error":"Invalid pair name: btc_eth"} + 'invalid api key': AuthenticationError, + 'invalid sign': AuthenticationError, + 'api key dont have trade permission': AuthenticationError, + 'invalid parameter': InvalidOrder, + 'invalid order': InvalidOrder, + 'The given order has already been cancelled': InvalidOrder, + 'Requests too often': DDoSProtection, + 'not available': ExchangeNotAvailable, + 'data unavailable': ExchangeNotAvailable, + 'external service unavailable': ExchangeNotAvailable, + 'Total transaction amount': InvalidOrder, # {"success": 0, "error": "Total transaction amount is less than minimal total: 0.00010000"} + 'The given order has already been closed and cannot be cancelled': InvalidOrder, + 'Insufficient funds': InsufficientFunds, + 'invalid key': AuthenticationError, + 'invalid nonce': InvalidNonce, # {"success":0,"error":"invalid nonce(has already been used)"}' + 'Total order amount is less than minimal amount': InvalidOrder, + 'Rate Limited': RateLimitExceeded, + }, + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': False, + 'triggerDirection': False, + 'triggerPriceType': None, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': False, + 'FOK': False, + 'PO': False, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': False, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 100000, # todo + 'untilDays': 100000, # todo + 'symbolRequired': True, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOrders': None, + 'fetchClosedOrders': None, + 'fetchOHLCV': None, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'orders': {}, # orders cache / emulation + }) + + def parse_balance(self, response) -> Balances: + balances = self.safe_dict(response, 'return', {}) + timestamp = self.safe_integer(balances, 'server_time') + result: dict = { + 'info': response, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + free = self.safe_dict(balances, 'funds', {}) + total = self.safe_dict(balances, 'funds_incl_orders', {}) + currencyIds = list(self.extend(free, total).keys()) + for i in range(0, len(currencyIds)): + currencyId = currencyIds[i] + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(free, currencyId) + account['total'] = self.safe_string(total, currencyId) + result[code] = account + return self.safe_balance(result) + + async def fetch_balance(self, params={}) -> Balances: + """ + + https://yobit.net/en/api + + query for balance and get the amount of funds available for trading or funds locked in orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + response = await self.privatePostGetInfo(params) + # + # { + # "success":1, + # "return":{ + # "funds":{ + # "ltc":22, + # "nvc":423.998, + # "ppc":10, + # }, + # "funds_incl_orders":{ + # "ltc":32, + # "nvc":523.998, + # "ppc":20, + # }, + # "rights":{ + # "info":1, + # "trade":0, + # "withdraw":0 + # }, + # "transaction_count":0, + # "open_orders":1, + # "server_time":1418654530 + # } + # } + # + return self.parse_balance(response) + + async def fetch_markets(self, params={}) -> List[Market]: + """ + + https://yobit.net/en/api + + retrieves data on all markets for yobit + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = await self.publicGetInfo(params) + # + # { + # "server_time":1615856752, + # "pairs":{ + # "ltc_btc":{ + # "decimal_places":8, + # "min_price":0.00000001, + # "max_price":10000, + # "min_amount":0.0001, + # "min_total":0.0001, + # "hidden":0, + # "fee":0.2, + # "fee_buyer":0.2, + # "fee_seller":0.2 + # }, + # }, + # } + # + markets = self.safe_dict(response, 'pairs', {}) + keys = list(markets.keys()) + result = [] + for i in range(0, len(keys)): + id = keys[i] + market = markets[id] + baseId, quoteId = id.split('_') + base = baseId.upper() + quote = quoteId.upper() + base = self.safe_currency_code(base) + quote = self.safe_currency_code(quote) + hidden = self.safe_integer(market, 'hidden') + feeString = self.safe_string(market, 'fee') + feeString = Precise.string_div(feeString, '100') + # yobit maker = taker + result.append({ + 'id': id, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': (hidden == 0), + 'contract': False, + 'linear': None, + 'inverse': None, + 'taker': self.parse_number(feeString), + 'maker': self.parse_number(feeString), + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(self.parse_precision(self.safe_string(market, 'decimal_places'))), + 'price': self.parse_number(self.parse_precision(self.safe_string(market, 'decimal_places'))), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(market, 'min_amount'), + 'max': self.safe_number(market, 'max_amount'), + }, + 'price': { + 'min': self.safe_number(market, 'min_price'), + 'max': self.safe_number(market, 'max_price'), + }, + 'cost': { + 'min': self.safe_number(market, 'min_total'), + 'max': None, + }, + }, + 'created': None, + 'info': market, + }) + return result + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + + https://yobit.net/en/api + + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + if limit is not None: + request['limit'] = limit # default = 150, max = 2000 + response = await self.publicGetDepthPair(self.extend(request, params)) + market_id_in_reponse = (market['id'] in response) + if not market_id_in_reponse: + raise ExchangeError(self.id + ' ' + market['symbol'] + ' order book is empty or not available') + orderbook = response[market['id']] + return self.parse_order_book(orderbook, symbol) + + async def fetch_order_books(self, symbols: Strings = None, limit: Int = None, params={}) -> OrderBooks: + """ + + https://yobit.net/en/api + + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data for multiple markets + :param str[]|None symbols: list of unified market symbols, all symbols fetched if None, default is None + :param int [limit]: max number of entries per orderbook to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `order book structures ` indexed by market symbol + """ + await self.load_markets() + ids = None + if symbols is None: + ids = '-'.join(self.ids) + # max URL length is 2083 symbols, including http schema, hostname, tld, etc... + if len(ids) > 2048: + numIds = len(self.ids) + raise ExchangeError(self.id + ' fetchOrderBooks() has ' + str(numIds) + ' symbols exceeding max URL length, you are required to specify a list of symbols in the first argument to fetchOrderBooks') + else: + ids = self.market_ids(symbols) + ids = '-'.join(ids) + request: dict = { + 'pair': ids, + # 'ignore_invalid': True, + } + if limit is not None: + request['limit'] = limit + response = await self.publicGetDepthPair(self.extend(request, params)) + result: dict = {} + ids = list(response.keys()) + for i in range(0, len(ids)): + id = ids[i] + symbol = self.safe_symbol(id) + result[symbol] = self.parse_order_book(response[id], symbol) + return result + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "high": 0.03497582, + # "low": 0.03248474, + # "avg": 0.03373028, + # "vol": 120.11485715062999, + # "vol_cur": 3572.24914074, + # "last": 0.0337611, + # "buy": 0.0337442, + # "sell": 0.03377798, + # "updated": 1537522009 + # } + # + timestamp = self.safe_timestamp(ticker, 'updated') + last = self.safe_string(ticker, 'last') + return self.safe_ticker({ + 'symbol': self.safe_symbol(None, market), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': self.safe_string(ticker, 'buy'), + 'bidVolume': None, + 'ask': self.safe_string(ticker, 'sell'), + 'askVolume': None, + 'vwap': None, + 'open': None, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': self.safe_string(ticker, 'avg'), + 'baseVolume': self.safe_string(ticker, 'vol_cur'), + 'quoteVolume': self.safe_string(ticker, 'vol'), + 'info': ticker, + }, market) + + async def fetch_tickers_helper(self, idsString: str, params={}) -> Tickers: + request: dict = { + 'pair': idsString, + } + tickers = await self.publicGetTickerPair(self.extend(request, params)) + result: dict = {} + keys = list(tickers.keys()) + for k in range(0, len(keys)): + id = keys[k] + ticker = tickers[id] + market = self.safe_market(id) + symbol = market['symbol'] + result[symbol] = self.parse_ticker(ticker, market) + return result + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + + https://yobit.net/en/api + + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :param dict [params.all]: you can set to `true` for convenience to fetch all tickers from self exchange by sending multiple requests + :returns dict: a dictionary of `ticker structures ` + """ + allSymbols = None + allSymbols, params = self.handle_param_bool(params, 'all', False) + if symbols is None and not allSymbols: + raise ArgumentsRequired(self.id + ' fetchTickers() requires "symbols" argument or use `params["all"] = True` to send multiple requests for all markets') + await self.load_markets() + promises = [] + maxLength = self.safe_integer(self.options, 'maxUrlLength', 2048) + # max URL length is 2048 symbols, including http schema, hostname, tld, etc... + lenghtOfBaseUrl = 40 # safe space for the url including api-base and endpoint dir is 30 chars + if allSymbols: + symbols = self.symbols + ids = '' + for i in range(0, len(self.ids)): + id = self.ids[i] + prefix = '' if (ids == '') else '-' + ids += prefix + id + if len(ids) > maxLength: + promises.append(self.fetch_tickers_helper(ids, params)) + ids = '' + if ids != '': + promises.append(self.fetch_tickers_helper(ids, params)) + else: + symbols = self.market_symbols(symbols) + ids = self.market_ids(symbols) + idsLength: number = len(ids) + idsString = '-'.join(ids) + actualLength = len(idsString) + lenghtOfBaseUrl + if actualLength > maxLength: + raise ArgumentsRequired(self.id + ' fetchTickers() is being requested for ' + str(idsLength) + ' markets(which has an URL length of ' + str(actualLength) + ' characters), but it exceedes max URL length(' + str(maxLength) + '), please pass limisted symbols array to fetchTickers to fit in one request') + promises.append(self.fetch_tickers_helper(idsString, params)) + resultAll = await asyncio.gather(*promises) + finalResult = {} + for i in range(0, len(resultAll)): + result = self.filter_by_array_tickers(resultAll[i], 'symbol', symbols) + finalResult = self.extend(finalResult, result) + return finalResult + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + + https://yobit.net/en/api + + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + tickers = await self.fetch_tickers([symbol], params) + return tickers[symbol] + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(public) + # + # { + # "type":"bid", + # "price":0.14046179, + # "amount":0.001, + # "tid":200256901, + # "timestamp":1649861004 + # } + # + # fetchMyTrades(private) + # + # { + # "pair":"doge_usdt", + # "type":"sell", + # "amount":139, + # "rate":0.139, + # "order_id":"2101103631773172", + # "is_your_order":1, + # "timestamp":"1649861561" + # } + # + timestamp = self.safe_timestamp(trade, 'timestamp') + side = self.safe_string(trade, 'type') + if side == 'ask': + side = 'sell' + elif side == 'bid': + side = 'buy' + priceString = self.safe_string_2(trade, 'rate', 'price') + id = self.safe_string_2(trade, 'trade_id', 'tid') + order = self.safe_string(trade, 'order_id') + marketId = self.safe_string(trade, 'pair') + symbol = self.safe_symbol(marketId, market) + amountString = self.safe_string(trade, 'amount') + # arguments for calculateFee(need to be numbers) + price = self.parse_number(priceString) + amount = self.parse_number(amountString) + type = 'limit' # all trades are still limit trades + fee = None + feeCostString = self.safe_number(trade, 'commission') + if feeCostString is not None: + feeCurrencyId = self.safe_string(trade, 'commissionCurrency') + feeCurrencyCode = self.safe_currency_code(feeCurrencyId) + fee = { + 'cost': feeCostString, + 'currency': feeCurrencyCode, + } + isYourOrder = self.safe_string(trade, 'is_your_order') + if isYourOrder is not None: + if fee is None: + feeInNumbers = self.calculate_fee(symbol, type, side, amount, price, 'taker') + fee = { + 'currency': self.safe_string(feeInNumbers, 'currency'), + 'cost': self.safe_string(feeInNumbers, 'cost'), + 'rate': self.safe_string(feeInNumbers, 'rate'), + } + return self.safe_trade({ + 'id': id, + 'order': order, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'type': type, + 'side': side, + 'takerOrMaker': None, + 'price': priceString, + 'amount': amountString, + 'cost': None, + 'fee': fee, + 'info': trade, + }, market) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + + https://yobit.net/en/api + + get the list of most recent trades for a particular symbol + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + if limit is not None: + request['limit'] = limit + response = await self.publicGetTradesPair(self.extend(request, params)) + # + # { + # "doge_usdt": [ + # { + # "type":"ask", + # "price":0.13956743, + # "amount":0.0008, + # "tid":200256900, + # "timestamp":1649860521 + # }, + # ] + # } + # + if isinstance(response, list): + numElements = len(response) + if numElements == 0: + return [] + result = self.safe_list(response, market['id'], []) + return self.parse_trades(result, market, since, limit) + + async def fetch_trading_fees(self, params={}) -> TradingFees: + """ + + https://yobit.net/en/api + + fetch the trading fees for multiple markets + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + await self.load_markets() + response = await self.publicGetInfo(params) + # + # { + # "server_time":1615856752, + # "pairs":{ + # "ltc_btc":{ + # "decimal_places":8, + # "min_price":0.00000001, + # "max_price":10000, + # "min_amount":0.0001, + # "min_total":0.0001, + # "hidden":0, + # "fee":0.2, + # "fee_buyer":0.2, + # "fee_seller":0.2 + # }, + # ... + # }, + # } + # + pairs = self.safe_dict(response, 'pairs', {}) + marketIds = list(pairs.keys()) + result: dict = {} + for i in range(0, len(marketIds)): + marketId = marketIds[i] + pair = self.safe_dict(pairs, marketId, {}) + symbol = self.safe_symbol(marketId, None, '_') + takerString = self.safe_string(pair, 'fee_buyer') + makerString = self.safe_string(pair, 'fee_seller') + taker = self.parse_number(Precise.string_div(takerString, '100')) + maker = self.parse_number(Precise.string_div(makerString, '100')) + result[symbol] = { + 'info': pair, + 'symbol': symbol, + 'taker': taker, + 'maker': maker, + 'percentage': True, + 'tierBased': False, + } + return result + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + + https://yobit.net/en/api + + create a trade order + :param str symbol: unified symbol of the market to create an order in + :param str type: must be 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + if type == 'market': + raise ExchangeError(self.id + ' createOrder() allows limit orders only') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + 'type': side, + 'amount': self.amount_to_precision(symbol, amount), + 'rate': self.price_to_precision(symbol, price), + } + response = await self.privatePostTrade(self.extend(request, params)) + # + # { + # "success":1, + # "return": { + # "received":0, + # "remains":10, + # "order_id":1101103635125179, + # "funds": { + # "usdt":27.84756553, + # "usdttrc20":0, + # "doge":19.98327206 + # }, + # "funds_incl_orders": { + # "usdt":30.35256553, + # "usdttrc20":0, + # "doge":19.98327206 + # }, + # "server_time":1650114256 + # } + # } + # + result = self.safe_dict(response, 'return') + return self.parse_order(result, market) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + + https://yobit.net/en/api + + cancels an open order + :param str id: order id + :param str symbol: not used by yobit cancelOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + request: dict = { + 'order_id': int(id), + } + response = await self.privatePostCancelOrder(self.extend(request, params)) + # + # { + # "success":1, + # "return": { + # "order_id":1101103632552304, + # "funds": { + # "usdt":30.71055443, + # "usdttrc20":0, + # "doge":9.98327206 + # }, + # "funds_incl_orders": { + # "usdt":31.81275443, + # "usdttrc20":0, + # "doge":9.98327206 + # }, + # "server_time":1649918298 + # } + # } + # + result = self.safe_dict(response, 'return', {}) + return self.parse_order(result) + + def parse_order_status(self, status: Str): + statuses: dict = { + '0': 'open', + '1': 'closed', + '2': 'canceled', + '3': 'open', # or partially-filled and canceled? https://github.com/ccxt/ccxt/issues/1594 + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder(private) + # + # { + # "received":0, + # "remains":10, + # "order_id":1101103635125179, + # "funds": { + # "usdt":27.84756553, + # "usdttrc20":0, + # "doge":19.98327206 + # }, + # "funds_incl_orders": { + # "usdt":30.35256553, + # "usdttrc20":0, + # "doge":19.98327206 + # }, + # "server_time":1650114256 + # } + # + # fetchOrder(private) + # + # { + # "id: "1101103635103335", # id-field is manually added in fetchOrder() from exchange response id-order dictionary structure + # "pair":"doge_usdt", + # "type":"buy", + # "start_amount":10, + # "amount":10, + # "rate":0.05, + # "timestamp_created":"1650112553", + # "status":0 + # } + # + # fetchOpenOrders(private) + # + # { + # "id":"1101103635103335", # id-field is manually added in fetchOpenOrders() from exchange response id-order dictionary structure + # "pair":"doge_usdt", + # "type":"buy", + # "amount":10, + # "rate":0.05, + # "timestamp_created":"1650112553", + # "status":0 + # } + # + # cancelOrder(private) + # + # { + # "order_id":1101103634000197, + # "funds": { + # "usdt":31.81275443, + # "usdttrc20":0, + # "doge":9.98327206 + # }, + # "funds_incl_orders": { + # "usdt":31.81275443, + # "usdttrc20":0, + # "doge":9.98327206 + # } + # } + # + id = self.safe_string_2(order, 'id', 'order_id') + status = self.parse_order_status(self.safe_string(order, 'status', 'open')) + if id == '0': + id = self.safe_string(order, 'init_order_id') + status = 'closed' + timestamp = self.safe_timestamp_2(order, 'timestamp_created', 'server_time') + marketId = self.safe_string(order, 'pair') + symbol = self.safe_symbol(marketId, market) + amount = self.safe_string(order, 'start_amount') + remaining = self.safe_string_2(order, 'amount', 'remains') + filled = self.safe_string(order, 'received', '0.0') + price = self.safe_string(order, 'rate') + fee = None + type = 'limit' + side = self.safe_string(order, 'type') + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': None, + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'type': type, + 'timeInForce': None, + 'postOnly': None, + 'side': side, + 'price': price, + 'triggerPrice': None, + 'cost': None, + 'amount': amount, + 'remaining': remaining, + 'filled': filled, + 'status': status, + 'fee': fee, + 'average': None, + 'trades': None, + }, market) + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + + https://yobit.net/en/api + + fetches information on an order made by the user + :param str id: order id + :param str symbol: not used by yobit fetchOrder + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + request: dict = { + 'order_id': int(id), + } + response = await self.privatePostOrderInfo(self.extend(request, params)) + id = str(id) + orders = self.safe_dict(response, 'return', {}) + # + # { + # "success":1, + # "return": { + # "1101103635103335": { + # "pair":"doge_usdt", + # "type":"buy", + # "start_amount":10, + # "amount":10, + # "rate":0.05, + # "timestamp_created":"1650112553", + # "status":0 + # } + # } + # } + # + return self.parse_order(self.extend({'id': id}, orders[id])) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + + https://yobit.net/en/api + + fetch all unfilled currently open orders + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOpenOrders() requires a symbol argument') + await self.load_markets() + request: dict = {} + market = None + if symbol is not None: + marketInner = self.market(symbol) + request['pair'] = marketInner['id'] + response = await self.privatePostActiveOrders(self.extend(request, params)) + # + # { + # "success":1, + # "return": { + # "1101103634006799": { + # "pair":"doge_usdt", + # "type":"buy", + # "amount":10, + # "rate":0.1, + # "timestamp_created":"1650034937", + # "status":0 + # }, + # "1101103634006738": { + # "pair":"doge_usdt", + # "type":"buy", + # "amount":10, + # "rate":0.1, + # "timestamp_created":"1650034932", + # "status":0 + # } + # } + # } + # + result = self.safe_dict(response, 'return', {}) + return self.parse_orders(result, market, since, limit) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + + https://yobit.net/en/api + + fetch all trades made by the user + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMyTrades() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + # some derived classes use camelcase notation for request fields + request: dict = { + # 'from': 123456789, # trade ID, from which the display starts numerical 0(test result: liqui ignores self field) + # 'count': 1000, # the number of trades for display numerical, default = 1000 + # 'from_id': trade ID, from which the display starts numerical 0 + # 'end_id': trade ID on which the display ends numerical ∞ + # 'order': 'ASC', # sorting, default = DESC(test result: liqui ignores self field, most recent trade always goes last) + # 'since': 1234567890, # UTC start time, default = 0(test result: liqui ignores self field) + # 'end': 1234567890, # UTC end time, default = ∞(test result: liqui ignores self field) + 'pair': market['id'], + } + if limit is not None: + request['count'] = limit + if since is not None: + request['since'] = self.parse_to_int(since / 1000) + response = await self.privatePostTradeHistory(self.extend(request, params)) + # + # { + # "success":1, + # "return": { + # "200257004": { + # "pair":"doge_usdt", + # "type":"sell", + # "amount":139, + # "rate":0.139, + # "order_id":"2101103631773172", + # "is_your_order":1, + # "timestamp":"1649861561" + # } + # } + # } + # + trades = self.safe_dict(response, 'return', {}) + ids = list(trades.keys()) + result = [] + for i in range(0, len(ids)): + id = self.safe_string(ids, i) + trade = self.parse_trade(self.extend(trades[id], { + 'trade_id': id, + }), market) + result.append(trade) + return self.filter_by_symbol_since_limit(result, market['symbol'], since, limit) + + async def create_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + + https://yobit.net/en/api + + create a currency deposit address + :param str code: unified currency code of the currency for the deposit address + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + request: dict = { + 'need_new': 1, + } + response = await self.fetch_deposit_address(code, self.extend(request, params)) + address = self.safe_string(response, 'address') + self.check_address(address) + return { + 'currency': code, + 'address': address, + 'tag': None, + 'network': None, + 'info': response['info'], + } + + async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://yobit.net/en/api + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + await self.load_markets() + currency = self.currency(code) + currencyId = currency['id'] + networks = self.safe_dict(self.options, 'networks', {}) + network = self.safe_string_upper(params, 'network') # self line allows the user to specify either ERC20 or ETH + network = self.safe_string(networks, network, network) # handle ERC20>ETH alias + if network is not None: + if network != 'ERC20': + currencyId = currencyId + network.lower() + params = self.omit(params, 'network') + request: dict = { + 'coinName': currencyId, + 'need_new': 0, + } + response = await self.privatePostGetDepositAddress(self.extend(request, params)) + address = self.safe_string(response['return'], 'address') + self.check_address(address) + return { + 'info': response, + 'currency': code, + 'network': None, + 'address': address, + 'tag': None, + } + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + + https://yobit.net/en/api + + make a withdrawal + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.check_address(address) + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'coinName': currency['id'], + 'amount': amount, + 'address': address, + } + # no docs on the tag, yet... + if tag is not None: + raise ExchangeError(self.id + ' withdraw() does not support the tag argument yet due to a lack of docs on withdrawing with tag/memo on behalf of the exchange.') + response = await self.privatePostWithdrawCoinsToAddress(self.extend(request, params)) + return { + 'info': response, + 'id': None, + 'txid': None, + 'type': None, + 'currency': None, + 'network': None, + 'amount': None, + 'status': None, + 'timestamp': None, + 'datetime': None, + 'address': None, + 'addressFrom': None, + 'addressTo': None, + 'tag': None, + 'tagFrom': None, + 'tagTo': None, + 'updated': None, + 'comment': None, + 'fee': { + 'currency': None, + 'cost': None, + 'rate': None, + }, + } + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = self.urls['api'][api] + query = self.omit(params, self.extract_params(path)) + if api == 'private': + self.check_required_credentials() + nonce = self.nonce() + body = self.urlencode(self.extend({ + 'nonce': nonce, + 'method': path, + }, query)) + signature = self.hmac(self.encode(body), self.encode(self.secret), hashlib.sha512) + headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Key': self.apiKey, + 'Sign': signature, + } + elif api == 'public': + url += '/' + self.version + '/' + self.implode_params(path, params) + if query: + url += '?' + self.urlencode(query) + else: + url += '/' + self.implode_params(path, params) + if method == 'GET': + if query: + url += '?' + self.urlencode(query) + else: + if query: + body = self.json(query) + headers = { + 'Content-Type': 'application/json', + } + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None # fallback to default error handler + if 'success' in response: + # + # 1 - Liqui only returns the integer 'success' key from their private API + # + # {"success": 1, ...} httpCode == 200 + # {"success": 0, ...} httpCode == 200 + # + # 2 - However, exchanges derived from Liqui, can return non-integers + # + # It can be a numeric string + # {"sucesss": "1", ...} + # {"sucesss": "0", ...}, httpCode >= 200(can be 403, 502, etc) + # + # Or just a string + # {"success": "true", ...} + # {"success": "false", ...}, httpCode >= 200 + # + # Or a boolean + # {"success": True, ...} + # {"success": False, ...}, httpCode >= 200 + # + # 3 - Oversimplified, Python PEP8 forbids comparison operator(==) of different types + # + # 4 - We do not want to copy-paste and duplicate the code of self handler to other exchanges derived from Liqui + # + # To cover points 1, 2, 3 and 4 combined self handler should work like self: + # + success = self.safe_value(response, 'success') # don't replace with safeBool here + if isinstance(success, str): + if (success == 'true') or (success == '1'): + success = True + else: + success = False + if not success: + code = self.safe_string(response, 'code') + message = self.safe_string(response, 'error') + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], code, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + raise ExchangeError(feedback) # unknown message + return None diff --git a/ccxt/async_support/zaif.py b/ccxt/async_support/zaif.py new file mode 100644 index 0000000..6a33e34 --- /dev/null +++ b/ccxt/async_support/zaif.py @@ -0,0 +1,802 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.zaif import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currency, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Ticker, Trade, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import BadRequest +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class zaif(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(zaif, self).describe(), { + 'id': 'zaif', + 'name': 'Zaif', + 'countries': ['JP'], + # 10 requests per second = 1000ms / 10 = 100ms between requests(public market endpoints) + 'rateLimit': 100, + 'version': '1', + 'has': { + 'CORS': None, + 'spot': True, + 'margin': None, # has but unimplemented + 'swap': False, + 'future': False, + 'option': False, + 'cancelOrder': True, + 'createMarketOrder': False, + 'createOrder': True, + 'fetchBalance': True, + 'fetchClosedOrders': True, + 'fetchCurrencies': False, + 'fetchFundingHistory': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchIndexOHLCV': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenOrders': True, + 'fetchOrderBook': True, + 'fetchPremiumIndexOHLCV': False, + 'fetchTicker': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': False, + 'withdraw': True, + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/27766927-39ca2ada-5eeb-11e7-972f-1b4199518ca6.jpg', + 'api': { + 'rest': 'https://api.zaif.jp', + }, + 'www': 'https://zaif.jp', + 'doc': [ + 'https://techbureau-api-document.readthedocs.io/ja/latest/index.html', + 'https://corp.zaif.jp/api-docs', + 'https://corp.zaif.jp/api-docs/api_links', + 'https://www.npmjs.com/package/zaif.jp', + 'https://github.com/you21979/node-zaif', + ], + 'fees': 'https://zaif.jp/fee?lang=en', + }, + 'fees': { + 'trading': { + 'percentage': True, + 'taker': self.parse_number('0.001'), + 'maker': self.parse_number('0'), + }, + }, + 'api': { + 'public': { + 'get': { + 'depth/{pair}': 1, + 'currencies/{pair}': 1, + 'currencies/all': 1, + 'currency_pairs/{pair}': 1, + 'currency_pairs/all': 1, + 'last_price/{pair}': 1, + 'ticker/{pair}': 1, + 'trades/{pair}': 1, + }, + }, + 'private': { + 'post': { + 'active_orders': 5, # 10 in 5 seconds = 2 per second => cost = 10 / 2 = 5 + 'cancel_order': 5, + 'deposit_history': 5, + 'get_id_info': 5, + 'get_info': 10, # 10 in 10 seconds = 1 per second => cost = 10 / 1 = 10 + 'get_info2': 5, # 20 in 10 seconds = 2 per second => cost = 10 / 2 = 5 + 'get_personal_info': 5, + 'trade': 5, + 'trade_history': 50, # 12 in 60 seconds = 0.2 per second => cost = 10 / 0.2 = 50 + 'withdraw': 5, + 'withdraw_history': 5, + }, + }, + 'ecapi': { + 'post': { + 'createInvoice': 1, # unverified + 'getInvoice': 1, + 'getInvoiceIdsByOrderNumber': 1, + 'cancelInvoice': 1, + }, + }, + 'tlapi': { + 'post': { + 'get_positions': 66, # 10 in 60 seconds = 0.166 per second => cost = 10 / 0.166 = 66 + 'position_history': 66, # 10 in 60 seconds + 'active_positions': 5, # 20 in 10 seconds + 'create_position': 33, # 3 in 10 seconds = 0.3 per second => cost = 10 / 0.3 = 33 + 'change_position': 33, # 3 in 10 seconds + 'cancel_position': 33, # 3 in 10 seconds + }, + }, + 'fapi': { + 'get': { + 'groups/{group_id}': 1, # testing + 'last_price/{group_id}/{pair}': 1, + 'ticker/{group_id}/{pair}': 1, + 'trades/{group_id}/{pair}': 1, + 'depth/{group_id}/{pair}': 1, + }, + }, + }, + 'options': { + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': True, # todo + 'triggerPrice': True, # todo implement + 'triggerDirection': False, + 'triggerPriceType': None, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': False, + 'FOK': False, + 'PO': False, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': True, # todo implement + 'marketBuyByCost': False, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': None, # todo + 'fetchOrder': None, + 'fetchOpenOrders': { + 'marginMode': True, # todo + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, # todo + 'fetchClosedOrders': { + 'marginMode': True, # todo + 'limit': 1000, + 'daysBack': 100000, # todo + 'daysBackCanceled': 1, # todo + 'untilDays': 100000, # todo + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': None, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'exact': { + 'unsupported currency_pair': BadRequest, # {"error": "unsupported currency_pair"} + }, + 'broad': { + }, + }, + }) + + async def fetch_markets(self, params={}) -> List[Market]: + """ + + https://zaif-api-document.readthedocs.io/ja/latest/PublicAPI.html#id12 + + retrieves data on all markets for zaif + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + markets = await self.publicGetCurrencyPairsAll(params) + # + # [ + # { + # "aux_unit_point": 0, + # "item_japanese": "\u30d3\u30c3\u30c8\u30b3\u30a4\u30f3", + # "aux_unit_step": 5.0, + # "description": "\u30d3\u30c3\u30c8\u30b3\u30a4\u30f3\u30fb\u65e5\u672c\u5186\u306e\u53d6\u5f15\u3092\u884c\u3046\u3053\u3068\u304c\u3067\u304d\u307e\u3059", + # "item_unit_min": 0.001, + # "event_number": 0, + # "currency_pair": "btc_jpy", + # "is_token": False, + # "aux_unit_min": 5.0, + # "aux_japanese": "\u65e5\u672c\u5186", + # "id": 1, + # "item_unit_step": 0.0001, + # "name": "BTC/JPY", + # "seq": 0, + # "title": "BTC/JPY" + # } + # ] + # + return self.parse_markets(markets) + + def parse_market(self, market: dict) -> Market: + id = self.safe_string(market, 'currency_pair') + name = self.safe_string(market, 'name') + baseId, quoteId = name.split('/') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + symbol = base + '/' + quote + return { + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': None, + 'swap': False, + 'future': False, + 'option': False, + 'active': None, # can trade or not + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.safe_number(market, 'item_unit_step'), + 'price': self.parse_number(self.parse_precision(self.safe_string(market, 'aux_unit_point'))), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(market, 'item_unit_min'), + 'max': None, + }, + 'price': { + 'min': self.safe_number(market, 'aux_unit_min'), + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + 'info': market, + } + + def parse_balance(self, response) -> Balances: + balances = self.safe_value(response, 'return', {}) + deposit = self.safe_value(balances, 'deposit') + result: dict = { + 'info': response, + 'timestamp': None, + 'datetime': None, + } + funds = self.safe_value(balances, 'funds', {}) + currencyIds = list(funds.keys()) + for i in range(0, len(currencyIds)): + currencyId = currencyIds[i] + code = self.safe_currency_code(currencyId) + balance = self.safe_string(funds, currencyId) + account = self.account() + account['free'] = balance + account['total'] = balance + if deposit is not None: + if currencyId in deposit: + account['total'] = self.safe_string(deposit, currencyId) + result[code] = account + return self.safe_balance(result) + + async def fetch_balance(self, params={}) -> Balances: + """ + + https://zaif-api-document.readthedocs.io/ja/latest/TradingAPI.html#id10 + + query for balance and get the amount of funds available for trading or funds locked in orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + response = await self.privatePostGetInfo(params) + return self.parse_balance(response) + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + + https://zaif-api-document.readthedocs.io/ja/latest/PublicAPI.html#id34 + + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + response = await self.publicGetDepthPair(self.extend(request, params)) + return self.parse_order_book(response, market['symbol']) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "last": 9e-08, + # "high": 1e-07, + # "low": 9e-08, + # "vwap": 0.0, + # "volume": 135250.0, + # "bid": 9e-08, + # "ask": 1e-07 + # } + # + symbol = self.safe_symbol(None, market) + vwap = self.safe_string(ticker, 'vwap') + baseVolume = self.safe_string(ticker, 'volume') + quoteVolume = Precise.string_mul(baseVolume, vwap) + last = self.safe_string(ticker, 'last') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': None, + 'datetime': None, + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': self.safe_string(ticker, 'bid'), + 'bidVolume': None, + 'ask': self.safe_string(ticker, 'ask'), + 'askVolume': None, + 'vwap': vwap, + 'open': None, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'info': ticker, + }, market) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + + https://zaif-api-document.readthedocs.io/ja/latest/PublicAPI.html#id22 + + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + ticker = await self.publicGetTickerPair(self.extend(request, params)) + # + # { + # "last": 9e-08, + # "high": 1e-07, + # "low": 9e-08, + # "vwap": 0.0, + # "volume": 135250.0, + # "bid": 9e-08, + # "ask": 1e-07 + # } + # + return self.parse_ticker(ticker, market) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(public) + # + # { + # "date": 1648559414, + # "price": 5880375.0, + # "amount": 0.017, + # "tid": 176126557, + # "currency_pair": "btc_jpy", + # "trade_type": "ask" + # } + # + side = self.safe_string(trade, 'trade_type') + side = 'buy' if (side == 'bid') else 'sell' + timestamp = self.safe_timestamp(trade, 'date') + id = self.safe_string_2(trade, 'id', 'tid') + priceString = self.safe_string(trade, 'price') + amountString = self.safe_string(trade, 'amount') + marketId = self.safe_string(trade, 'currency_pair') + symbol = self.safe_symbol(marketId, market, '_') + return self.safe_trade({ + 'id': id, + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'type': None, + 'side': side, + 'order': None, + 'takerOrMaker': None, + 'price': priceString, + 'amount': amountString, + 'cost': None, + 'fee': None, + }, market) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + + https://zaif-api-document.readthedocs.io/ja/latest/PublicAPI.html#id28 + + get the list of most recent trades for a particular symbol + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + response = await self.publicGetTradesPair(self.extend(request, params)) + # + # [ + # { + # "date": 1648559414, + # "price": 5880375.0, + # "amount": 0.017, + # "tid": 176126557, + # "currency_pair": "btc_jpy", + # "trade_type": "ask" + # }, ... + # ] + # + numTrades = len(response) + if numTrades == 1: + firstTrade = response[0] + if not firstTrade: + response = [] + return self.parse_trades(response, market, since, limit) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + + https://zaif-api-document.readthedocs.io/ja/latest/MarginTradingAPI.html#id23 + + create a trade order + :param str symbol: unified symbol of the market to create an order in + :param str type: must be 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + if type != 'limit': + raise ExchangeError(self.id + ' createOrder() allows limit orders only') + market = self.market(symbol) + request: dict = { + 'currency_pair': market['id'], + 'action': 'bid' if (side == 'buy') else 'ask', + 'amount': amount, + 'price': price, + } + response = await self.privatePostTrade(self.extend(request, params)) + return self.safe_order({ + 'info': response, + 'id': str(response['return']['order_id']), + }, market) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + + https://zaif-api-document.readthedocs.io/ja/latest/TradingAPI.html#id37 + + cancels an open order + :param str id: order id + :param str symbol: not used by zaif cancelOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + request: dict = { + 'order_id': id, + } + response = await self.privatePostCancelOrder(self.extend(request, params)) + # + # { + # "success": 1, + # "return": { + # "order_id": 184, + # "funds": { + # "jpy": 15320, + # "btc": 1.392, + # "mona": 2600, + # "kaori": 0.1 + # } + # } + # } + # + data = self.safe_dict(response, 'return') + return self.parse_order(data) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # { + # "currency_pair": "btc_jpy", + # "action": "ask", + # "amount": 0.03, + # "price": 56000, + # "timestamp": 1402021125, + # "comment" : "demo" + # } + # + # cancelOrder + # + # { + # "order_id": 184, + # "funds": { + # "jpy": 15320, + # "btc": 1.392, + # "mona": 2600, + # "kaori": 0.1 + # } + # } + # + side = self.safe_string(order, 'action') + side = 'buy' if (side == 'bid') else 'sell' + timestamp = self.safe_timestamp(order, 'timestamp') + marketId = self.safe_string(order, 'currency_pair') + symbol = self.safe_symbol(marketId, market, '_') + price = self.safe_string(order, 'price') + amount = self.safe_string(order, 'amount') + id = self.safe_string_2(order, 'id', 'order_id') + return self.safe_order({ + 'id': id, + 'clientOrderId': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'status': 'open', + 'symbol': symbol, + 'type': 'limit', + 'timeInForce': None, + 'postOnly': None, + 'side': side, + 'price': price, + 'triggerPrice': None, + 'cost': None, + 'amount': amount, + 'filled': None, + 'remaining': None, + 'trades': None, + 'fee': None, + 'info': order, + 'average': None, + }, market) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + + https://zaif-api-document.readthedocs.io/ja/latest/MarginTradingAPI.html#id28 + + fetch all unfilled currently open orders + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + market: Market = None + request: dict = { + # 'is_token': False, + # 'is_token_both': False, + } + if symbol is not None: + market = self.market(symbol) + request['currency_pair'] = market['id'] + response = await self.privatePostActiveOrders(self.extend(request, params)) + return self.parse_orders(response['return'], market, since, limit) + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + + https://zaif-api-document.readthedocs.io/ja/latest/TradingAPI.html#id24 + + fetches information on multiple closed orders made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + market: Market = None + request: dict = { + # 'from': 0, + # 'count': 1000, + # 'from_id': 0, + # 'end_id': 1000, + # 'order': 'DESC', + # 'since': 1503821051, + # 'end': 1503821051, + # 'is_token': False, + } + if symbol is not None: + market = self.market(symbol) + request['currency_pair'] = market['id'] + response = await self.privatePostTradeHistory(self.extend(request, params)) + return self.parse_orders(response['return'], market, since, limit) + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + + https://zaif-api-document.readthedocs.io/ja/latest/TradingAPI.html#id41 + + make a withdrawal + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.check_address(address) + await self.load_markets() + currency = self.currency(code) + if code == 'JPY': + raise ExchangeError(self.id + ' withdraw() does not allow ' + code + ' withdrawals') + request: dict = { + 'currency': currency['id'], + 'amount': amount, + 'address': address, + # 'message': 'Hi!', # XEM and others + # 'opt_fee': 0.003, # BTC and MONA only + } + if tag is not None: + request['message'] = tag + result = await self.privatePostWithdraw(self.extend(request, params)) + # + # { + # "success": 1, + # "return": { + # "id": 23634, + # "fee": 0.001, + # "txid":, + # "funds": { + # "jpy": 15320, + # "btc": 1.392, + # "xem": 100.2, + # "mona": 2600 + # } + # } + # } + # + returnData = self.safe_dict(result, 'return') + return self.parse_transaction(returnData, currency) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # { + # "id": 23634, + # "fee": 0.001, + # "txid":, + # "funds": { + # "jpy": 15320, + # "btc": 1.392, + # "xem": 100.2, + # "mona": 2600 + # } + # } + # + currency = self.safe_currency(None, currency) + fee = None + feeCost = self.safe_value(transaction, 'fee') + if feeCost is not None: + fee = { + 'cost': feeCost, + 'currency': currency['code'], + } + return { + 'id': self.safe_string(transaction, 'id'), + 'txid': self.safe_string(transaction, 'txid'), + 'timestamp': None, + 'datetime': None, + 'network': None, + 'addressFrom': None, + 'address': None, + 'addressTo': None, + 'amount': None, + 'type': None, + 'currency': currency['code'], + 'status': None, + 'updated': None, + 'tagFrom': None, + 'tag': None, + 'tagTo': None, + 'comment': None, + 'internal': None, + 'fee': fee, + 'info': transaction, + } + + def custom_nonce(self): + num = self.number_to_string(self.milliseconds() / 1000) + nonce = float(num) + return format(nonce, '.8f') + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = self.urls['api']['rest'] + '/' + if api == 'public': + url += 'api/' + self.version + '/' + self.implode_params(path, params) + elif api == 'fapi': + url += 'fapi/' + self.version + '/' + self.implode_params(path, params) + else: + self.check_required_credentials() + if api == 'ecapi': + url += 'ecapi' + elif api == 'tlapi': + url += 'tlapi' + else: + url += 'tapi' + nonce = self.custom_nonce() + body = self.urlencode(self.extend({ + 'method': path, + 'nonce': nonce, + }, params)) + headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Key': self.apiKey, + 'Sign': self.hmac(self.encode(body), self.encode(self.secret), hashlib.sha512), + } + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None + # + # {"error": "unsupported currency_pair"} + # + feedback = self.id + ' ' + body + error = self.safe_string(response, 'error') + if error is not None: + self.throw_exactly_matched_exception(self.exceptions['exact'], error, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], error, feedback) + raise ExchangeError(feedback) # unknown message + success = self.safe_bool(response, 'success', True) + if not success: + raise ExchangeError(feedback) + return None diff --git a/ccxt/async_support/zonda.py b/ccxt/async_support/zonda.py new file mode 100644 index 0000000..60cc0da --- /dev/null +++ b/ccxt/async_support/zonda.py @@ -0,0 +1,1956 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.zonda import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currency, DepositAddress, Int, LedgerEntry, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, Transaction, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import AccountSuspended +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import OrderImmediatelyFillable +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import OnMaintenance +from ccxt.base.errors import InvalidNonce +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class zonda(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(zonda, self).describe(), { + 'id': 'zonda', + 'name': 'Zonda', + 'countries': ['EE'], # Estonia + 'rateLimit': 1000, + 'has': { + 'CORS': True, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelAllOrders': False, + 'cancelOrder': True, + 'cancelOrders': False, + 'closeAllPositions': False, + 'closePosition': False, + 'createDepositAddress': False, + 'createOrder': True, + 'createReduceOnlyOrder': False, + 'fetchAllGreeks': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': False, + 'fetchDeposit': False, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': True, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': None, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLedger': True, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrice': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrder': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrderBook': True, + 'fetchOrderBooks': False, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': False, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': False, + 'fetchTransactionFee': False, + 'fetchTransactionFees': False, + 'fetchTransactions': None, + 'fetchTransfer': False, + 'fetchUnderlyingAssets': False, + 'fetchVolatilityHistory': False, + 'fetchWithdrawal': False, + 'fetchWithdrawals': None, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'repayMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'transfer': True, + 'withdraw': True, + }, + 'timeframes': { + '1m': '60', + '3m': '180', + '5m': '300', + '15m': '900', + '30m': '1800', + '1h': '3600', + '2h': '7200', + '4h': '14400', + '6h': '21600', + '12h': '43200', + '1d': '86400', + '3d': '259200', + '1w': '604800', + }, + 'hostname': 'zondacrypto.exchange', + 'urls': { + 'referral': 'https://auth.zondaglobal.com/ref/jHlbB4mIkdS1', + 'logo': 'https://user-images.githubusercontent.com/1294454/159202310-a0e38007-5e7c-4ba9-a32f-c8263a0291fe.jpg', + 'www': 'https://zondaglobal.com', + 'api': { + 'public': 'https://{hostname}/API/Public', + 'private': 'https://{hostname}/API/Trading/tradingApi.php', + 'v1_01Public': 'https://api.{hostname}/rest', + 'v1_01Private': 'https://api.{hostname}/rest', + }, + 'doc': [ + 'https://docs.zondacrypto.exchange/', + 'https://github.com/BitBayNet/API', + ], + 'support': 'https://zondaglobal.com/en/helpdesk/zonda-exchange', + 'fees': 'https://zondaglobal.com/legal/zonda-exchange/fees', + }, + 'api': { + 'public': { + 'get': [ + '{id}/all', + '{id}/market', + '{id}/orderbook', + '{id}/ticker', + '{id}/trades', + ], + }, + 'private': { + 'post': [ + 'info', + 'trade', + 'cancel', + 'orderbook', + 'orders', + 'transfer', + 'withdraw', + 'history', + 'transactions', + ], + }, + 'v1_01Public': { + 'get': [ + 'trading/ticker', + 'trading/ticker/{symbol}', + 'trading/stats', + 'trading/stats/{symbol}', + 'trading/orderbook/{symbol}', + 'trading/transactions/{symbol}', + 'trading/candle/history/{symbol}/{resolution}', + ], + }, + 'v1_01Private': { + 'get': [ + 'api_payments/deposits/crypto/addresses', + 'payments/withdrawal/{detailId}', + 'payments/deposit/{detailId}', + 'trading/offer', + 'trading/stop/offer', + 'trading/config/{symbol}', + 'trading/history/transactions', + 'balances/BITBAY/history', + 'balances/BITBAY/balance', + 'fiat_cantor/rate/{baseId}/{quoteId}', + 'fiat_cantor/history', + 'client_payments/v2/customer/crypto/{currency}/channels/deposit', + 'client_payments/v2/customer/crypto/{currency}/channels/withdrawal', + 'client_payments/v2/customer/crypto/deposit/fee', + 'client_payments/v2/customer/crypto/withdrawal/fee', + ], + 'post': [ + 'trading/offer/{symbol}', + 'trading/stop/offer/{symbol}', + 'trading/config/{symbol}', + 'balances/BITBAY/balance', + 'balances/BITBAY/balance/transfer/{source}/{destination}', + 'fiat_cantor/exchange', + 'api_payments/withdrawals/crypto', + 'api_payments/withdrawals/fiat', + 'client_payments/v2/customer/crypto/deposit', + 'client_payments/v2/customer/crypto/withdrawal', + ], + 'delete': [ + 'trading/offer/{symbol}/{id}/{side}/{price}', + 'trading/stop/offer/{symbol}/{id}/{side}/{price}', + ], + 'put': [ + 'balances/BITBAY/balance/{id}', + ], + }, + }, + 'fees': { + 'trading': { + 'maker': self.parse_number('0.0'), + 'taker': self.parse_number('0.001'), + 'percentage': True, + 'tierBased': False, + }, + 'fiat': { + 'maker': self.parse_number('0.0030'), + 'taker': self.parse_number('0.0043'), + 'percentage': True, + 'tierBased': True, + 'tiers': { + 'taker': [ + [self.parse_number('0.0043'), self.parse_number('0')], + [self.parse_number('0.0042'), self.parse_number('1250')], + [self.parse_number('0.0041'), self.parse_number('3750')], + [self.parse_number('0.0040'), self.parse_number('7500')], + [self.parse_number('0.0039'), self.parse_number('10000')], + [self.parse_number('0.0038'), self.parse_number('15000')], + [self.parse_number('0.0037'), self.parse_number('20000')], + [self.parse_number('0.0036'), self.parse_number('25000')], + [self.parse_number('0.0035'), self.parse_number('37500')], + [self.parse_number('0.0034'), self.parse_number('50000')], + [self.parse_number('0.0033'), self.parse_number('75000')], + [self.parse_number('0.0032'), self.parse_number('100000')], + [self.parse_number('0.0031'), self.parse_number('150000')], + [self.parse_number('0.0030'), self.parse_number('200000')], + [self.parse_number('0.0029'), self.parse_number('250000')], + [self.parse_number('0.0028'), self.parse_number('375000')], + [self.parse_number('0.0027'), self.parse_number('500000')], + [self.parse_number('0.0026'), self.parse_number('625000')], + [self.parse_number('0.0025'), self.parse_number('875000')], + ], + 'maker': [ + [self.parse_number('0.0030'), self.parse_number('0')], + [self.parse_number('0.0029'), self.parse_number('1250')], + [self.parse_number('0.0028'), self.parse_number('3750')], + [self.parse_number('0.0028'), self.parse_number('7500')], + [self.parse_number('0.0027'), self.parse_number('10000')], + [self.parse_number('0.0026'), self.parse_number('15000')], + [self.parse_number('0.0025'), self.parse_number('20000')], + [self.parse_number('0.0025'), self.parse_number('25000')], + [self.parse_number('0.0024'), self.parse_number('37500')], + [self.parse_number('0.0023'), self.parse_number('50000')], + [self.parse_number('0.0023'), self.parse_number('75000')], + [self.parse_number('0.0022'), self.parse_number('100000')], + [self.parse_number('0.0021'), self.parse_number('150000')], + [self.parse_number('0.0021'), self.parse_number('200000')], + [self.parse_number('0.0020'), self.parse_number('250000')], + [self.parse_number('0.0019'), self.parse_number('375000')], + [self.parse_number('0.0018'), self.parse_number('500000')], + [self.parse_number('0.0018'), self.parse_number('625000')], + [self.parse_number('0.0017'), self.parse_number('875000')], + ], + }, + }, + 'funding': { + 'withdraw': {}, + }, + }, + 'options': { + 'fetchTickerMethod': 'v1_01PublicGetTradingTickerSymbol', # or v1_01PublicGetTradingStatsSymbol + 'fetchTickersMethod': 'v1_01PublicGetTradingTicker', # or v1_01PublicGetTradingStats + 'fiatCurrencies': ['EUR', 'USD', 'GBP', 'PLN'], + 'transfer': { + 'fillResponseFromRequest': True, + }, + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, # todo remove + 'triggerDirection': False, + 'triggerPriceType': None, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': None, + 'daysBack': 100000, # todo + 'untilDays': 100000, # todo + 'symbolRequired': False, + }, + 'fetchOrder': None, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 100, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': None, # todo + 'fetchOHLCV': { + 'limit': None, + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + '400': ExchangeError, # At least one parameter wasn't set + '401': InvalidOrder, # Invalid order type + '402': InvalidOrder, # No orders with specified currencies + '403': InvalidOrder, # Invalid payment currency name + '404': InvalidOrder, # Error. Wrong transaction type + '405': InvalidOrder, # Order with self id doesn't exist + '406': InsufficientFunds, # No enough money or crypto + # code 407 not specified are not specified in their docs + '408': InvalidOrder, # Invalid currency name + '501': AuthenticationError, # Invalid public key + '502': AuthenticationError, # Invalid sign + '503': InvalidNonce, # Invalid moment parameter. Request time doesn't match current server time + '504': ExchangeError, # Invalid method + '505': AuthenticationError, # Key has no permission for self action + '506': AccountSuspended, # Account locked. Please contact with customer service + # codes 507 and 508 are not specified in their docs + '509': ExchangeError, # The BIC/SWIFT is required for self currency + '510': BadSymbol, # Invalid market name + 'FUNDS_NOT_SUFFICIENT': InsufficientFunds, + 'OFFER_FUNDS_NOT_EXCEEDING_MINIMUMS': InvalidOrder, + 'OFFER_NOT_FOUND': OrderNotFound, + 'OFFER_WOULD_HAVE_BEEN_PARTIALLY_FILLED': OrderImmediatelyFillable, + 'ACTION_LIMIT_EXCEEDED': RateLimitExceeded, + 'UNDER_MAINTENANCE': OnMaintenance, + 'REQUEST_TIMESTAMP_TOO_OLD': InvalidNonce, + 'PERMISSIONS_NOT_SUFFICIENT': PermissionDenied, + 'INVALID_STOP_RATE': InvalidOrder, + 'TIMEOUT': ExchangeError, + 'RESPONSE_TIMEOUT': ExchangeError, + 'ACTION_BLOCKED': PermissionDenied, + 'INVALID_HASH_SIGNATURE': AuthenticationError, + }, + 'commonCurrencies': { + 'GGC': 'Global Game Coin', + }, + }) + + async def fetch_markets(self, params={}) -> List[Market]: + """ + + https://docs.zondacrypto.exchange/reference/ticker-1 + + retrieves data on all markets for zonda + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = await self.v1_01PublicGetTradingTicker(params) + # + # { + # "status": "Ok", + # "items": { + # "BSV-USD": { + # "market": { + # "code": "BSV-USD", + # "first": {currency: "BSV", minOffer: "0.00035", scale: 8}, + # "second": {currency: "USD", minOffer: "5", scale: 2} + # }, + # "time": "1557569762154", + # "highestBid": "52.31", + # "lowestAsk": "62.99", + # "rate": "63", + # "previousRate": "51.21", + # }, + # }, + # } + # + items = self.safe_value(response, 'items', {}) + markets = list(items.values()) + return self.parse_markets(markets) + + def parse_market(self, item) -> Market: + market = self.safe_value(item, 'market', {}) + id = self.safe_string(market, 'code') + first = self.safe_value(market, 'first', {}) + second = self.safe_value(market, 'second', {}) + baseId = self.safe_string(first, 'currency') + quoteId = self.safe_string(second, 'currency') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + fees = self.safe_value(self.fees, 'trading', {}) + fiatCurrencies = self.safe_value(self.options, 'fiatCurrencies', []) + if self.in_array(base, fiatCurrencies) or self.in_array(quote, fiatCurrencies): + fees = self.safe_value(self.fees, 'fiat', {}) + # todo: check that the limits have ben interpreted correctly + return { + 'id': id, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': None, + 'contract': False, + 'linear': None, + 'inverse': None, + 'taker': self.safe_number(fees, 'taker'), + 'maker': self.safe_number(fees, 'maker'), + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'optionType': None, + 'strike': None, + 'precision': { + 'amount': self.parse_number(self.parse_precision(self.safe_string(first, 'scale'))), + 'price': self.parse_number(self.parse_precision(self.safe_string(second, 'scale'))), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(first, 'minOffer'), + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + 'info': item, + } + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + + https://docs.zondacrypto.exchange/reference/active-orders + + fetch all unfilled currently open orders + :param str symbol: not used by zonda fetchOpenOrders + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + request: dict = {} + # todo pair + response = await self.v1_01PrivateGetTradingOffer(self.extend(request, params)) + items = self.safe_list(response, 'items', []) + return self.parse_orders(items, None, since, limit, {'status': 'open'}) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # { + # "market": "ETH-EUR", + # "offerType": "Sell", + # "id": "93d3657b-d616-11e9-9248-0242ac110005", + # "currentAmount": "0.04", + # "lockedAmount": "0.04", + # "rate": "280", + # "startAmount": "0.04", + # "time": "1568372806924", + # "postOnly": False, + # "hidden": False, + # "mode": "limit", + # "receivedAmount": "0.0", + # "firstBalanceId": "5b816c3e-437c-4e43-9bef-47814ae7ebfc", + # "secondBalanceId": "ab43023b-4079-414c-b340-056e3430a3af" + # } + # + # cancelOrder + # + # { + # status: "Ok", + # errors: [] + # } + # + marketId = self.safe_string(order, 'market') + symbol = self.safe_symbol(marketId, market, '-') + timestamp = self.safe_integer(order, 'time') + amount = self.safe_string(order, 'startAmount') + remaining = self.safe_string(order, 'currentAmount') + postOnly = self.safe_value(order, 'postOnly') + return self.safe_order({ + 'id': self.safe_string(order, 'id'), + 'clientOrderId': None, + 'info': order, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'status': None, + 'symbol': symbol, + 'type': self.safe_string(order, 'mode'), + 'timeInForce': None, + 'postOnly': postOnly, + 'side': self.safe_string_lower(order, 'offerType'), + 'price': self.safe_string(order, 'rate'), + 'triggerPrice': None, + 'amount': amount, + 'cost': None, + 'filled': None, + 'remaining': remaining, + 'average': None, + 'fee': None, + 'trades': None, + }, market) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + + https://docs.zondacrypto.exchange/reference/transactions-history + + fetch all trades made by the user + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + request: dict = {} + if symbol: + markets = [self.market_id(symbol)] + symbol = self.symbol(symbol) + request['markets'] = markets + query: dict = {'query': self.json(self.extend(request, params))} + response = await self.v1_01PrivateGetTradingHistoryTransactions(query) + # + # { + # "status": "Ok", + # "totalRows": "67", + # "items": [ + # { + # "id": "b54659a0-51b5-42a0-80eb-2ac5357ccee2", + # "market": "BTC-EUR", + # "time": "1541697096247", + # "amount": "0.00003", + # "rate": "4341.44", + # "initializedBy": "Sell", + # "wasTaker": False, + # "userAction": "Buy", + # "offerId": "bd19804a-6f89-4a69-adb8-eb078900d006", + # "commissionValue": null + # }, + # ] + # } + # + items = self.safe_value(response, 'items') + result = self.parse_trades(items, None, since, limit) + if symbol is None: + return result + return self.filter_by_symbol(result, symbol) + + def parse_balance(self, response) -> Balances: + balances = self.safe_value(response, 'balances') + if balances is None: + raise ExchangeError(self.id + ' empty balance response ' + self.json(response)) + result: dict = {'info': response} + for i in range(0, len(balances)): + balance = balances[i] + currencyId = self.safe_string(balance, 'currency') + code = self.safe_currency_code(currencyId) + account = self.account() + account['used'] = self.safe_string(balance, 'lockedFunds') + account['free'] = self.safe_string(balance, 'availableFunds') + result[code] = account + return self.safe_balance(result) + + async def fetch_balance(self, params={}) -> Balances: + """ + + https://docs.zondacrypto.exchange/reference/list-of-wallets + + query for balance and get the amount of funds available for trading or funds locked in orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + response = await self.v1_01PrivateGetBalancesBITBAYBalance(params) + return self.parse_balance(response) + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + + https://docs.zondacrypto.exchange/reference/orderbook-2 + + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = await self.v1_01PublicGetTradingOrderbookSymbol(self.extend(request, params)) + # + # { + # "status":"Ok", + # "sell":[ + # {"ra":"43988.93","ca":"0.00100525","sa":"0.00100525","pa":"0.00100525","co":1}, + # {"ra":"43988.94","ca":"0.00114136","sa":"0.00114136","pa":"0.00114136","co":1}, + # {"ra":"43989","ca":"0.010578","sa":"0.010578","pa":"0.010578","co":1}, + # ], + # "buy":[ + # {"ra":"42157.33","ca":"2.83147881","sa":"2.83147881","pa":"2.83147881","co":2}, + # {"ra":"42096.0","ca":"0.00011878","sa":"0.00011878","pa":"0.00011878","co":1}, + # {"ra":"42022.0","ca":"0.00011899","sa":"0.00011899","pa":"0.00011899","co":1}, + # ], + # "timestamp":"1642299886122", + # "seqNo":"27641254" + # } + # + rawBids = self.safe_value(response, 'buy', []) + rawAsks = self.safe_value(response, 'sell', []) + timestamp = self.safe_integer(response, 'timestamp') + return { + 'symbol': market['symbol'], + 'bids': self.parse_bids_asks(rawBids, 'ra', 'ca'), + 'asks': self.parse_bids_asks(rawAsks, 'ra', 'ca'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'nonce': self.safe_integer(response, 'seqNo'), + } + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # version 1 + # + # { + # "m": "ETH-PLN", + # "h": "13485.13", + # "l": "13100.01", + # "v": "126.10710939", + # "r24h": "13332.72" + # } + # + # version 2 + # + # { + # "market": { + # "code": "ADA-USDT", + # "first": { + # "currency": "ADA", + # "minOffer": "0.2", + # "scale": "6" + # }, + # "second": { + # "currency": "USDT", + # "minOffer": "0.099", + # "scale": "6" + # }, + # "amountPrecision": "6", + # "pricePrecision": "6", + # "ratePrecision": "6" + # }, + # "time": "1655812661202", + # "highestBid": "0.492", + # "lowestAsk": "0.499389", + # "rate": "0.50588", + # "previousRate": "0.504981" + # } + # + tickerMarket = self.safe_value(ticker, 'market') + marketId = self.safe_string_2(tickerMarket, 'code', 'm') + market = self.safe_market(marketId, market) + timestamp = self.safe_integer(ticker, 'time') + rate = self.safe_value(ticker, 'rate') + return self.safe_ticker({ + 'symbol': self.safe_symbol(marketId, market), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'h'), + 'low': self.safe_string(ticker, 'l'), + 'bid': self.safe_number(ticker, 'highestBid'), + 'bidVolume': None, + 'ask': self.safe_number(ticker, 'lowestAsk'), + 'askVolume': None, + 'vwap': None, + 'open': self.safe_string(ticker, 'r24h'), + 'close': rate, + 'last': rate, + 'previousClose': self.safe_value(ticker, 'previousRate'), + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': self.safe_string(ticker, 'v'), + 'quoteVolume': None, + 'info': ticker, + }, market) + + async def fetch_ticker(self, symbol: str, params={}): + """ + v1_01PublicGetTradingTickerSymbol retrieves timestamp, datetime, bid, ask, close, last, previousClose, v1_01PublicGetTradingStatsSymbol retrieves high, low, volume and opening price of an asset + + https://docs.zondacrypto.exchange/reference/market-statistics + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.method]: v1_01PublicGetTradingTickerSymbol(default) or v1_01PublicGetTradingStatsSymbol + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + method = 'v1_01PublicGetTradingTickerSymbol' + defaultMethod = self.safe_string(self.options, 'fetchTickerMethod', method) + fetchTickerMethod = self.safe_string_2(params, 'method', 'fetchTickerMethod', defaultMethod) + response = None + if fetchTickerMethod == method: + response = await self.v1_01PublicGetTradingTickerSymbol(self.extend(request, params)) + # + # { + # "status": "Ok", + # "ticker": { + # "market": { + # "code": "ADA-USDT", + # "first": { + # "currency": "ADA", + # "minOffer": "0.21", + # "scale": 6 + # }, + # "second": { + # "currency": "USDT", + # "minOffer": "0.099", + # "scale": 6 + # }, + # "amountPrecision": 6, + # "pricePrecision": 6, + # "ratePrecision": 6 + # }, + # "time": "1655810976780", + # "highestBid": "0.498543", + # "lowestAsk": "0.50684", + # "rate": "0.50588", + # "previousRate": "0.504981" + # } + # } + # + elif fetchTickerMethod == 'v1_01PublicGetTradingStatsSymbol': + response = await self.v1_01PublicGetTradingStatsSymbol(self.extend(request, params)) + # + # { + # "status": "Ok", + # "stats": { + # "m": "BTC-USDT", + # "h": "28800", + # "l": "26703.950101", + # "v": "6.72932396", + # "r24h": "27122.2" + # } + # } + # + else: + raise BadRequest(self.id + ' fetchTicker params["method"] must be "v1_01PublicGetTradingTickerSymbol" or "v1_01PublicGetTradingStatsSymbol"') + stats = self.safe_value_2(response, 'ticker', 'stats') + return self.parse_ticker(stats, market) + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + @ignore + v1_01PublicGetTradingTicker retrieves timestamp, datetime, bid, ask, close, last, previousClose for each market, v1_01PublicGetTradingStats retrieves high, low, volume and opening price of each market + + https://docs.zondacrypto.exchange/reference/market-statistics + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.method]: v1_01PublicGetTradingTicker(default) or v1_01PublicGetTradingStats + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + method = 'v1_01PublicGetTradingTicker' + defaultMethod = self.safe_string(self.options, 'fetchTickersMethod', method) + fetchTickersMethod = self.safe_string_2(params, 'method', 'fetchTickersMethod', defaultMethod) + response = None + if fetchTickersMethod == method: + response = await self.v1_01PublicGetTradingTicker(params) + # + # { + # "status": "Ok", + # "items": { + # "DAI-PLN": { + # "market": { + # "code": "DAI-PLN", + # "first": { + # "currency": "DAI", + # "minOffer": "0.99", + # "scale": 8 + # }, + # "second": { + # "currency": "PLN", + # "minOffer": "5", + # "scale": 2 + # }, + # "amountPrecision": 8, + # "pricePrecision": 2, + # "ratePrecision": 2 + # }, + # "time": "1655810825137", + # "highestBid": "4.42", + # "lowestAsk": "4.44", + # "rate": "4.44", + # "previousRate": "4.43" + # }, + # ... + # } + # } + # + elif fetchTickersMethod == 'v1_01PublicGetTradingStats': + response = await self.v1_01PublicGetTradingStats(params) + # + # { + # "status": "Ok", + # "items": { + # "DAI-PLN": { + # "m": "DAI-PLN", + # "h": "4.41", + # "l": "4.37", + # "v": "8.71068087", + # "r24h": "4.36" + # }, + # ... + # } + # } + # + else: + raise BadRequest(self.id + ' fetchTickers params["method"] must be "v1_01PublicGetTradingTicker" or "v1_01PublicGetTradingStats"') + items = self.safe_dict(response, 'items') + return self.parse_tickers(items, symbols) + + async def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + + https://docs.zondacrypto.exchange/reference/operations-history + + :param str [code]: unified currency code, default is None + :param int [since]: timestamp in ms of the earliest ledger entry, default is None + :param int [limit]: max number of ledger entries to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ledger structure ` + """ + balanceCurrencies = [] + if code is not None: + currency = self.currency(code) + balanceCurrencies.append(currency['id']) + request: dict = { + 'balanceCurrencies': balanceCurrencies, + } + if since is not None: + request['fromTime'] = since + if limit is not None: + request['limit'] = limit + request = self.extend(request, params) + response = await self.v1_01PrivateGetBalancesBITBAYHistory({'query': self.json(request)}) + items = response['items'] + return self.parse_ledger(items, None, since, limit) + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + # + # FUNDS_MIGRATION + # { + # "historyId": "84ea7a29-7da5-4de5-b0c0-871e83cad765", + # "balance": { + # "id": "821ec166-cb88-4521-916c-f4eb44db98df", + # "currency": "LTC", + # "type": "CRYPTO", + # "userId": "a34d361d-7bad-49c1-888e-62473b75d877", + # "name": "LTC" + # }, + # "detailId": null, + # "time": 1506128252968, + # "type": "FUNDS_MIGRATION", + # "value": 0.0009957, + # "fundsBefore": {"total": 0, "available": 0, "locked": 0}, + # "fundsAfter": {"total": 0.0009957, "available": 0.0009957, "locked": 0}, + # "change": {"total": 0.0009957, "available": 0.0009957, "locked": 0} + # } + # + # CREATE_BALANCE + # { + # "historyId": "d0fabd8d-9107-4b5e-b9a6-3cab8af70d49", + # "balance": { + # "id": "653ffcf2-3037-4ebe-8e13-d5ea1a01d60d", + # "currency": "BTG", + # "type": "CRYPTO", + # "userId": "a34d361d-7bad-49c1-888e-62473b75d877", + # "name": "BTG" + # }, + # "detailId": null, + # "time": 1508895244751, + # "type": "CREATE_BALANCE", + # "value": 0, + # "fundsBefore": {"total": null, "available": null, "locked": null}, + # "fundsAfter": {"total": 0, "available": 0, "locked": 0}, + # "change": {"total": 0, "available": 0, "locked": 0} + # } + # + # BITCOIN_GOLD_FORK + # { + # "historyId": "2b4d52d3-611c-473d-b92c-8a8d87a24e41", + # "balance": { + # "id": "653ffcf2-3037-4ebe-8e13-d5ea1a01d60d", + # "currency": "BTG", + # "type": "CRYPTO", + # "userId": "a34d361d-7bad-49c1-888e-62473b75d877", + # "name": "BTG" + # }, + # "detailId": null, + # "time": 1508895244778, + # "type": "BITCOIN_GOLD_FORK", + # "value": 0.00453512, + # "fundsBefore": {"total": 0, "available": 0, "locked": 0}, + # "fundsAfter": {"total": 0.00453512, "available": 0.00453512, "locked": 0}, + # "change": {"total": 0.00453512, "available": 0.00453512, "locked": 0} + # } + # + # ADD_FUNDS + # { + # "historyId": "3158236d-dae5-4a5d-81af-c1fa4af340fb", + # "balance": { + # "id": "3a7e7a1e-0324-49d5-8f59-298505ebd6c7", + # "currency": "BTC", + # "type": "CRYPTO", + # "userId": "a34d361d-7bad-49c1-888e-62473b75d877", + # "name": "BTC" + # }, + # "detailId": "8e83a960-e737-4380-b8bb-259d6e236faa", + # "time": 1520631178816, + # "type": "ADD_FUNDS", + # "value": 0.628405, + # "fundsBefore": {"total": 0.00453512, "available": 0.00453512, "locked": 0}, + # "fundsAfter": {"total": 0.63294012, "available": 0.63294012, "locked": 0}, + # "change": {"total": 0.628405, "available": 0.628405, "locked": 0} + # } + # + # TRANSACTION_PRE_LOCKING + # { + # "historyId": "e7d19e0f-03b3-46a8-bc72-dde72cc24ead", + # "balance": { + # "id": "3a7e7a1e-0324-49d5-8f59-298505ebd6c7", + # "currency": "BTC", + # "type": "CRYPTO", + # "userId": "a34d361d-7bad-49c1-888e-62473b75d877", + # "name": "BTC" + # }, + # "detailId": null, + # "time": 1520706403868, + # "type": "TRANSACTION_PRE_LOCKING", + # "value": -0.1, + # "fundsBefore": {"total": 0.63294012, "available": 0.63294012, "locked": 0}, + # "fundsAfter": {"total": 0.63294012, "available": 0.53294012, "locked": 0.1}, + # "change": {"total": 0, "available": -0.1, "locked": 0.1} + # } + # + # TRANSACTION_POST_OUTCOME + # { + # "historyId": "c4010825-231d-4a9c-8e46-37cde1f7b63c", + # "balance": { + # "id": "3a7e7a1e-0324-49d5-8f59-298505ebd6c7", + # "currency": "BTC", + # "type": "CRYPTO", + # "userId": "a34d361d-7bad-49c1-888e-62473b75d877", + # "name": "BTC" + # }, + # "detailId": "bf2876bc-b545-4503-96c8-ef4de8233876", + # "time": 1520706404032, + # "type": "TRANSACTION_POST_OUTCOME", + # "value": -0.01771415, + # "fundsBefore": {"total": 0.63294012, "available": 0.53294012, "locked": 0.1}, + # "fundsAfter": {"total": 0.61522597, "available": 0.53294012, "locked": 0.08228585}, + # "change": {"total": -0.01771415, "available": 0, "locked": -0.01771415} + # } + # + # TRANSACTION_POST_INCOME + # { + # "historyId": "7f18b7af-b676-4125-84fd-042e683046f6", + # "balance": { + # "id": "ab43023b-4079-414c-b340-056e3430a3af", + # "currency": "EUR", + # "type": "FIAT", + # "userId": "a34d361d-7bad-49c1-888e-62473b75d877", + # "name": "EUR" + # }, + # "detailId": "f5fcb274-0cc7-4385-b2d3-bae2756e701f", + # "time": 1520706404035, + # "type": "TRANSACTION_POST_INCOME", + # "value": 628.78, + # "fundsBefore": {"total": 0, "available": 0, "locked": 0}, + # "fundsAfter": {"total": 628.78, "available": 628.78, "locked": 0}, + # "change": {"total": 628.78, "available": 628.78, "locked": 0} + # } + # + # TRANSACTION_COMMISSION_OUTCOME + # { + # "historyId": "843177fa-61bc-4cbf-8be5-b029d856c93b", + # "balance": { + # "id": "ab43023b-4079-414c-b340-056e3430a3af", + # "currency": "EUR", + # "type": "FIAT", + # "userId": "a34d361d-7bad-49c1-888e-62473b75d877", + # "name": "EUR" + # }, + # "detailId": "f5fcb274-0cc7-4385-b2d3-bae2756e701f", + # "time": 1520706404050, + # "type": "TRANSACTION_COMMISSION_OUTCOME", + # "value": -2.71, + # "fundsBefore": {"total": 766.06, "available": 766.06, "locked": 0}, + # "fundsAfter": {"total": 763.35,"available": 763.35, "locked": 0}, + # "change": {"total": -2.71, "available": -2.71, "locked": 0} + # } + # + # TRANSACTION_OFFER_COMPLETED_RETURN + # { + # "historyId": "cac69b04-c518-4dc5-9d86-e76e91f2e1d2", + # "balance": { + # "id": "3a7e7a1e-0324-49d5-8f59-298505ebd6c7", + # "currency": "BTC", + # "type": "CRYPTO", + # "userId": "a34d361d-7bad-49c1-888e-62473b75d877", + # "name": "BTC" + # }, + # "detailId": null, + # "time": 1520714886425, + # "type": "TRANSACTION_OFFER_COMPLETED_RETURN", + # "value": 0.00000196, + # "fundsBefore": {"total": 0.00941208, "available": 0.00941012, "locked": 0.00000196}, + # "fundsAfter": {"total": 0.00941208, "available": 0.00941208, "locked": 0}, + # "change": {"total": 0, "available": 0.00000196, "locked": -0.00000196} + # } + # + # WITHDRAWAL_LOCK_FUNDS + # { + # "historyId": "03de2271-66ab-4960-a786-87ab9551fc14", + # "balance": { + # "id": "3a7e7a1e-0324-49d5-8f59-298505ebd6c7", + # "currency": "BTC", + # "type": "CRYPTO", + # "userId": "a34d361d-7bad-49c1-888e-62473b75d877", + # "name": "BTC" + # }, + # "detailId": "6ad3dc72-1d6d-4ec2-8436-ca43f85a38a6", + # "time": 1522245654481, + # "type": "WITHDRAWAL_LOCK_FUNDS", + # "value": -0.8, + # "fundsBefore": {"total": 0.8, "available": 0.8, "locked": 0}, + # "fundsAfter": {"total": 0.8, "available": 0, "locked": 0.8}, + # "change": {"total": 0, "available": -0.8, "locked": 0.8} + # } + # + # WITHDRAWAL_SUBTRACT_FUNDS + # { + # "historyId": "b0308c89-5288-438d-a306-c6448b1a266d", + # "balance": { + # "id": "3a7e7a1e-0324-49d5-8f59-298505ebd6c7", + # "currency": "BTC", + # "type": "CRYPTO", + # "userId": "a34d361d-7bad-49c1-888e-62473b75d877", + # "name": "BTC" + # }, + # "detailId": "6ad3dc72-1d6d-4ec2-8436-ca43f85a38a6", + # "time": 1522246526186, + # "type": "WITHDRAWAL_SUBTRACT_FUNDS", + # "value": -0.8, + # "fundsBefore": {"total": 0.8, "available": 0, "locked": 0.8}, + # "fundsAfter": {"total": 0, "available": 0, "locked": 0}, + # "change": {"total": -0.8, "available": 0, "locked": -0.8} + # } + # + # TRANSACTION_OFFER_ABORTED_RETURN + # { + # "historyId": "b1a3c075-d403-4e05-8f32-40512cdd88c0", + # "balance": { + # "id": "3a7e7a1e-0324-49d5-8f59-298505ebd6c7", + # "currency": "BTC", + # "type": "CRYPTO", + # "userId": "a34d361d-7bad-49c1-888e-62473b75d877", + # "name": "BTC" + # }, + # "detailId": null, + # "time": 1522512298662, + # "type": "TRANSACTION_OFFER_ABORTED_RETURN", + # "value": 0.0564931, + # "fundsBefore": {"total": 0.44951311, "available": 0.39302001, "locked": 0.0564931}, + # "fundsAfter": {"total": 0.44951311, "available": 0.44951311, "locked": 0}, + # "change": {"total": 0, "available": 0.0564931, "locked": -0.0564931} + # } + # + # WITHDRAWAL_UNLOCK_FUNDS + # { + # "historyId": "0ed569a2-c330-482e-bb89-4cb553fb5b11", + # "balance": { + # "id": "3a7e7a1e-0324-49d5-8f59-298505ebd6c7", + # "currency": "BTC", + # "type": "CRYPTO", + # "userId": "a34d361d-7bad-49c1-888e-62473b75d877", + # "name": "BTC" + # }, + # "detailId": "0c7be256-c336-4111-bee7-4eb22e339700", + # "time": 1527866360785, + # "type": "WITHDRAWAL_UNLOCK_FUNDS", + # "value": 0.05045, + # "fundsBefore": {"total": 0.86001578, "available": 0.80956578, "locked": 0.05045}, + # "fundsAfter": {"total": 0.86001578, "available": 0.86001578, "locked": 0}, + # "change": {"total": 0, "available": 0.05045, "locked": -0.05045} + # } + # + # TRANSACTION_COMMISSION_RETURN + # { + # "historyId": "07c89c27-46f1-4d7a-8518-b73798bf168a", + # "balance": { + # "id": "ab43023b-4079-414c-b340-056e3430a3af", + # "currency": "EUR", + # "type": "FIAT", + # "userId": "a34d361d-7bad-49c1-888e-62473b75d877", + # "name": "EUR" + # }, + # "detailId": null, + # "time": 1528304043063, + # "type": "TRANSACTION_COMMISSION_RETURN", + # "value": 0.6, + # "fundsBefore": {"total": 0, "available": 0, "locked": 0}, + # "fundsAfter": {"total": 0.6, "available": 0.6, "locked": 0}, + # "change": {"total": 0.6, "available": 0.6, "locked": 0} + # } + # + timestamp = self.safe_integer(item, 'time') + balance = self.safe_value(item, 'balance', {}) + currencyId = self.safe_string(balance, 'currency') + currency = self.safe_currency(currencyId, currency) + change = self.safe_value(item, 'change', {}) + amount = self.safe_string(change, 'total') + direction = 'in' + if Precise.string_lt(amount, '0'): + direction = 'out' + amount = Precise.string_neg(amount) + # there are 2 undocumented api calls: (v1_01PrivateGetPaymentsDepositDetailId and v1_01PrivateGetPaymentsWithdrawalDetailId) + # that can be used to enrich the transfers with txid, address etc(you need to use info.detailId parameter) + fundsBefore = self.safe_value(item, 'fundsBefore', {}) + fundsAfter = self.safe_value(item, 'fundsAfter', {}) + return self.safe_ledger_entry({ + 'info': item, + 'id': self.safe_string(item, 'historyId'), + 'direction': direction, + 'account': None, + 'referenceId': self.safe_string(item, 'detailId'), + 'referenceAccount': None, + 'type': self.parse_ledger_entry_type(self.safe_string(item, 'type')), + 'currency': self.safe_currency_code(currencyId), + 'amount': self.parse_number(amount), + 'before': self.safe_number(fundsBefore, 'total'), + 'after': self.safe_number(fundsAfter, 'total'), + 'status': 'ok', + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'fee': None, + }, currency) + + def parse_ledger_entry_type(self, type): + types: dict = { + 'ADD_FUNDS': 'transaction', + 'BITCOIN_GOLD_FORK': 'transaction', + 'CREATE_BALANCE': 'transaction', + 'FUNDS_MIGRATION': 'transaction', + 'WITHDRAWAL_LOCK_FUNDS': 'transaction', + 'WITHDRAWAL_SUBTRACT_FUNDS': 'transaction', + 'WITHDRAWAL_UNLOCK_FUNDS': 'transaction', + 'TRANSACTION_COMMISSION_OUTCOME': 'fee', + 'TRANSACTION_COMMISSION_RETURN': 'fee', + 'TRANSACTION_OFFER_ABORTED_RETURN': 'trade', + 'TRANSACTION_OFFER_COMPLETED_RETURN': 'trade', + 'TRANSACTION_POST_INCOME': 'trade', + 'TRANSACTION_POST_OUTCOME': 'trade', + 'TRANSACTION_PRE_LOCKING': 'trade', + } + return self.safe_string(types, type, type) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # [ + # "1582399800000", + # { + # "o": "0.0001428", + # "c": "0.0001428", + # "h": "0.0001428", + # "l": "0.0001428", + # "v": "4", + # "co": "1" + # } + # ] + # + first = self.safe_value(ohlcv, 1, {}) + return [ + self.safe_integer(ohlcv, 0), + self.safe_number(first, 'o'), + self.safe_number(first, 'h'), + self.safe_number(first, 'l'), + self.safe_number(first, 'c'), + self.safe_number(first, 'v'), + ] + + async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + + https://docs.zondacrypto.exchange/reference/candles-chart + + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + tradingSymbol = market['baseId'] + '-' + market['quoteId'] + request: dict = { + 'symbol': tradingSymbol, + 'resolution': self.safe_string(self.timeframes, timeframe, timeframe), + # 'from': 1574709092000, # unix timestamp in milliseconds, required + # 'to': 1574709092000, # unix timestamp in milliseconds, required + } + if limit is None: + limit = 100 + else: + limit = min(limit, 11000) # supports up to 11k candles diapason + duration = self.parse_timeframe(timeframe) + timerange = limit * duration * 1000 + if since is None: + request['to'] = self.milliseconds() + request['from'] = request['to'] - timerange + else: + request['from'] = since + request['to'] = self.sum(request['from'], timerange) + response = await self.v1_01PublicGetTradingCandleHistorySymbolResolution(self.extend(request, params)) + # + # { + # "status":"Ok", + # "items":[ + # ["1591503060000",{"o":"0.02509572","c":"0.02509438","h":"0.02509664","l":"0.02509438","v":"0.02082165","co":"17"}], + # ["1591503120000",{"o":"0.02509606","c":"0.02509515","h":"0.02509606","l":"0.02509487","v":"0.04971703","co":"13"}], + # ["1591503180000",{"o":"0.02509532","c":"0.02509589","h":"0.02509589","l":"0.02509454","v":"0.01332236","co":"7"}], + # ] + # } + # + items = self.safe_list(response, 'items', []) + return self.parse_ohlcvs(items, market, timeframe, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # createOrder trades + # + # { + # "rate": "0.02195928", + # "amount": "0.00167952" + # } + # + # fetchMyTrades(private) + # + # { + # "amount": "0.29285199", + # "commissionValue": "0.00125927", + # "id": "11c8203a-a267-11e9-b698-0242ac110007", + # "initializedBy": "Buy", + # "market": "ETH-EUR", + # "offerId": "11c82038-a267-11e9-b698-0242ac110007", + # "rate": "277", + # "time": "1562689917517", + # "userAction": "Buy", + # "wasTaker": True, + # } + # + # fetchTrades(public) + # + # { + # "id": "df00b0da-e5e0-11e9-8c19-0242ac11000a", + # "t": "1570108958831", + # "a": "0.04776653", + # "r": "0.02145854", + # "ty": "Sell" + # } + # + timestamp = self.safe_integer_2(trade, 'time', 't') + side = self.safe_string_lower_2(trade, 'userAction', 'ty') + wasTaker = self.safe_value(trade, 'wasTaker') + takerOrMaker: Str = None + if wasTaker is not None: + takerOrMaker = 'taker' if wasTaker else 'maker' + priceString = self.safe_string_2(trade, 'rate', 'r') + amountString = self.safe_string_2(trade, 'amount', 'a') + feeCostString = self.safe_string(trade, 'commissionValue') + marketId = self.safe_string(trade, 'market') + market = self.safe_market(marketId, market, '-') + symbol = market['symbol'] + fee = None + if feeCostString is not None: + feeCurrency = market['base'] if (side == 'buy') else market['quote'] + fee = { + 'currency': feeCurrency, + 'cost': feeCostString, + } + order = self.safe_string(trade, 'offerId') + # todo: check self logic + type: Str = None + if order is not None: + type = 'limit' if order else 'market' + return self.safe_trade({ + 'id': self.safe_string(trade, 'id'), + 'order': order, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'type': type, + 'side': side, + 'price': priceString, + 'amount': amountString, + 'cost': None, + 'takerOrMaker': takerOrMaker, + 'fee': fee, + 'info': trade, + }, market) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + + https://docs.zondacrypto.exchange/reference/last-transactions + + get the list of most recent trades for a particular symbol + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + tradingSymbol = market['baseId'] + '-' + market['quoteId'] + request: dict = { + 'symbol': tradingSymbol, + } + if since is not None: + request['fromTime'] = since - 1 # result does not include exactly `since` time therefore decrease by 1 + if limit is not None: + request['limit'] = limit # default - 10, max - 300 + response = await self.v1_01PublicGetTradingTransactionsSymbol(self.extend(request, params)) + items = self.safe_list(response, 'items') + return self.parse_trades(items, market, since, limit) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://docs.zondacrypto.exchange/reference/new-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + tradingSymbol = market['baseId'] + '-' + market['quoteId'] + amount = float(self.amount_to_precision(symbol, amount)) + request: dict = { + 'symbol': tradingSymbol, + 'offerType': side.upper(), + 'amount': amount, + } + stopLossPrice = self.safe_value_2(params, 'stopPrice', 'stopLossPrice') + isStopLossPrice = stopLossPrice is not None + isLimitOrder = type == 'limit' + isMarketOrder = type == 'market' + isStopLimit = (type == 'stop-limit') or (isLimitOrder and isStopLossPrice) + isStopMarket = type == 'stop-market' or (isMarketOrder and isStopLossPrice) + isStopOrder = isStopLimit or isStopMarket + if isLimitOrder or isStopLimit: + request['rate'] = self.price_to_precision(symbol, price) + request['mode'] = 'stop-limit' if isStopLimit else 'limit' + elif isMarketOrder or isStopMarket: + request['mode'] = 'stop-market' if isStopMarket else 'market' + else: + raise ExchangeError(self.id + ' createOrder() invalid type') + params = self.omit(params, ['stopPrice', 'stopLossPrice']) + response = None + if isStopOrder: + if not isStopLossPrice: + raise ExchangeError(self.id + ' createOrder() zonda requires `triggerPrice` parameter for stop-limit or stop-market orders') + request['stopRate'] = self.price_to_precision(symbol, stopLossPrice) + response = await self.v1_01PrivatePostTradingStopOfferSymbol(self.extend(request, params)) + else: + response = await self.v1_01PrivatePostTradingOfferSymbol(self.extend(request, params)) + # + # unfilled(open order) + # + # { + # "status": "Ok", + # "completed": False, # can deduce status from here + # "offerId": "ce9cc72e-d61c-11e9-9248-0242ac110005", + # "transactions": [], # can deduce order info from here + # } + # + # filled(closed order) + # + # { + # "status": "Ok", + # "offerId": "942a4a3e-e922-11e9-8c19-0242ac11000a", + # "completed": True, + # "transactions": [ + # { + # "rate": "0.02195928", + # "amount": "0.00167952" + # }, + # { + # "rate": "0.02195928", + # "amount": "0.00167952" + # }, + # { + # "rate": "0.02196207", + # "amount": "0.27704177" + # } + # ] + # } + # + # partially-filled(open order) + # + # { + # "status": "Ok", + # "offerId": "d0ebefab-f4d7-11e9-8c19-0242ac11000a", + # "completed": False, + # "transactions": [ + # { + # "rate": "0.02106404", + # "amount": "0.0019625" + # }, + # { + # "rate": "0.02106404", + # "amount": "0.0019625" + # }, + # { + # "rate": "0.02105901", + # "amount": "0.00975256" + # } + # ] + # } + # + id = self.safe_string_2(response, 'offerId', 'stopOfferId') + completed = self.safe_bool(response, 'completed', False) + status = 'closed' if completed else 'open' + transactions = self.safe_value(response, 'transactions') + return self.safe_order({ + 'id': id, + 'info': response, + 'timestamp': None, + 'datetime': None, + 'lastTradeTimestamp': None, + 'status': status, + 'symbol': symbol, + 'type': type, + 'side': side, + 'price': price, + 'amount': amount, + 'cost': None, + 'filled': None, + 'remaining': None, + 'average': None, + 'fee': None, + 'trades': transactions, + 'clientOrderId': None, + }) + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + + https://docs.zondacrypto.exchange/reference/cancel-order + + cancels an open order + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + side = self.safe_string(params, 'side') + if side is None: + raise ExchangeError(self.id + ' cancelOrder() requires a `side` parameter("buy" or "sell")') + price = self.safe_value(params, 'price') + if price is None: + raise ExchangeError(self.id + ' cancelOrder() requires a `price` parameter(float or string)') + await self.load_markets() + market = self.market(symbol) + tradingSymbol = market['baseId'] + '-' + market['quoteId'] + request: dict = { + 'symbol': tradingSymbol, + 'id': id, + 'side': side, + 'price': price, + } + response = await self.v1_01PrivateDeleteTradingOfferSymbolIdSidePrice(self.extend(request, params)) + # {status: "Fail", errors: ["NOT_RECOGNIZED_OFFER_TYPE"]} -- if required params are missing + # {status: "Ok", errors: []} + return self.parse_order(response) + + def is_fiat(self, currency: str) -> bool: + fiatCurrencies: dict = { + 'USD': True, + 'EUR': True, + 'PLN': True, + } + return self.safe_bool(fiatCurrencies, currency, False) + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + # + # { + # "address": "33u5YAEhQbYfjHHPsfMfCoSdEjfwYjVcBE", + # "currency": "BTC", + # "balanceId": "5d5d19e7-2265-49c7-af9a-047bcf384f21", + # "balanceEngine": "BITBAY", + # "tag": null + # } + # + currencyId = self.safe_string(depositAddress, 'currency') + address = self.safe_string(depositAddress, 'address') + self.check_address(address) + return { + 'info': depositAddress, + 'currency': self.safe_currency_code(currencyId, currency), + 'network': None, + 'address': address, + 'tag': self.safe_string(depositAddress, 'tag'), + } + + async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + + https://docs.zondacrypto.exchange/reference/deposit-addresses-for-crypto + + fetch the deposit address for a currency associated with self account + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.walletId]: Wallet id to filter deposit adresses. + :returns dict: an `address structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + } + response = await self.v1_01PrivateGetApiPaymentsDepositsCryptoAddresses(self.extend(request, params)) + # + # { + # "status": "Ok", + # "data": [{ + # "address": "33u5YAEhQbYfjHHPsfMfCoSdEjfwYjVcBE", + # "currency": "BTC", + # "balanceId": "5d5d19e7-2265-49c7-af9a-047bcf384f21", + # "balanceEngine": "BITBAY", + # "tag": null + # } + # ] + # } + # + data = self.safe_value(response, 'data') + first = self.safe_dict(data, 0) + return self.parse_deposit_address(first, currency) + + async def fetch_deposit_addresses(self, codes: Strings = None, params={}) -> List[DepositAddress]: + """ + + https://docs.zondacrypto.exchange/reference/deposit-addresses-for-crypto + + fetch deposit addresses for multiple currencies and chain types + :param str[]|None codes: zonda does not support filtering filtering by multiple codes and will ignore self parameter. + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `address structures ` + """ + await self.load_markets() + response = await self.v1_01PrivateGetApiPaymentsDepositsCryptoAddresses(params) + # + # { + # "status": "Ok", + # "data": [{ + # "address": "33u5YAEhQbYfjHHPsfMfCoSdEjfwYjVcBE", + # "currency": "BTC", + # "balanceId": "5d5d19e7-2265-49c7-af9a-047bcf384f21", + # "balanceEngine": "BITBAY", + # "tag": null + # } + # ] + # } + # + data = self.safe_list(response, 'data') + return self.parse_deposit_addresses(data, codes) + + async def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + + https://docs.zondacrypto.exchange/reference/internal-transfer + + transfer currency internally between wallets on the same account + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account to transfer from + :param str toAccount: account to transfer to + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transfer structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'source': fromAccount, + 'destination': toAccount, + 'currency': code, + 'funds': self.currency_to_precision(code, amount), + } + response = await self.v1_01PrivatePostBalancesBITBAYBalanceTransferSourceDestination(self.extend(request, params)) + # + # { + # "status": "Ok", + # "from": { + # "id": "ad9397c5-3bd9-4372-82ba-22da6a90cb56", + # "userId": "4bc43956-423f-47fd-9faa-acd37c58ed9f", + # "availableFunds": 0.01803472, + # "totalFunds": 0.01804161, + # "lockedFunds": 0.00000689, + # "currency": "BTC", + # "type": "CRYPTO", + # "name": "BTC", + # "balanceEngine": "BITBAY" + # }, + # "to": { + # "id": "01931d52-536b-4ca5-a9f4-be28c86d0cc3", + # "userId": "4bc43956-423f-47fd-9faa-acd37c58ed9f", + # "availableFunds": 0.0001, + # "totalFunds": 0.0001, + # "lockedFunds": 0, + # "currency": "BTC", + # "type": "CRYPTO", + # "name": "Prowizja", + # "balanceEngine": "BITBAY" + # }, + # "errors": null + # } + # + transfer = self.parse_transfer(response, currency) + transferOptions = self.safe_value(self.options, 'transfer', {}) + fillResponseFromRequest = self.safe_bool(transferOptions, 'fillResponseFromRequest', True) + if fillResponseFromRequest: + transfer['amount'] = amount + return transfer + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + # + # { + # "status": "Ok", + # "from": { + # "id": "ad9397c5-3bd9-4372-82ba-22da6a90cb56", + # "userId": "4bc43956-423f-47fd-9faa-acd37c58ed9f", + # "availableFunds": 0.01803472, + # "totalFunds": 0.01804161, + # "lockedFunds": 0.00000689, + # "currency": "BTC", + # "type": "CRYPTO", + # "name": "BTC", + # "balanceEngine": "BITBAY" + # }, + # "to": { + # "id": "01931d52-536b-4ca5-a9f4-be28c86d0cc3", + # "userId": "4bc43956-423f-47fd-9faa-acd37c58ed9f", + # "availableFunds": 0.0001, + # "totalFunds": 0.0001, + # "lockedFunds": 0, + # "currency": "BTC", + # "type": "CRYPTO", + # "name": "Prowizja", + # "balanceEngine": "BITBAY" + # }, + # "errors": null + # } + # + status = self.safe_string(transfer, 'status') + fromAccount = self.safe_value(transfer, 'from', {}) + fromId = self.safe_string(fromAccount, 'id') + to = self.safe_value(transfer, 'to', {}) + toId = self.safe_string(to, 'id') + currencyId = self.safe_string(fromAccount, 'currency') + return { + 'info': transfer, + 'id': None, + 'timestamp': None, + 'datetime': None, + 'currency': self.safe_currency_code(currencyId, currency), + 'amount': None, + 'fromAccount': fromId, + 'toAccount': toId, + 'status': self.parse_transfer_status(status), + } + + def parse_transfer_status(self, status: Str) -> Str: + statuses: dict = { + 'Ok': 'ok', + 'Fail': 'failed', + } + return self.safe_string(statuses, status, status) + + async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + + https://docs.zondacrypto.exchange/reference/crypto-withdrawal-1 + + make a withdrawal + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.check_address(address) + await self.load_markets() + response = None + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + 'amount': amount, + 'address': address, + # request['balanceId'] = params['balanceId'] # Wallet id used for withdrawal. If not provided, any BITBAY wallet with sufficient funds is used. If BITBAYPAY wallet should be used parameter must be explicitly specified. + } + if self.is_fiat(code): + # request['swift'] = params['swift'] # Bank identifier, if required. + response = await self.v1_01PrivatePostApiPaymentsWithdrawalsFiat(self.extend(request, params)) + else: + if tag is not None: + request['tag'] = tag + response = await self.v1_01PrivatePostApiPaymentsWithdrawalsCrypto(self.extend(request, params)) + # + # { + # "status": "Ok", + # "data": { + # "id": "65e01087-afb0-4ab2-afdb-cc925e360296" + # } + # } + # + data = self.safe_dict(response, 'data') + return self.parse_transaction(data, currency) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # withdraw + # + # { + # "id": "65e01087-afb0-4ab2-afdb-cc925e360296" + # } + # + currency = self.safe_currency(None, currency) + return { + 'id': self.safe_string(transaction, 'id'), + 'txid': None, + 'timestamp': None, + 'datetime': None, + 'network': None, + 'addressFrom': None, + 'address': None, + 'addressTo': None, + 'amount': None, + 'type': None, + 'currency': currency['code'], + 'status': None, + 'updated': None, + 'tagFrom': None, + 'tag': None, + 'tagTo': None, + 'comment': None, + 'internal': None, + 'fee': None, + 'info': transaction, + } + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = self.implode_hostname(self.urls['api'][api]) + if api == 'public': + query = self.omit(params, self.extract_params(path)) + url += '/' + self.implode_params(path, params) + '.json' + if query: + url += '?' + self.urlencode(query) + elif api == 'v1_01Public': + query = self.omit(params, self.extract_params(path)) + url += '/' + self.implode_params(path, params) + if query: + url += '?' + self.urlencode(query) + elif api == 'v1_01Private': + self.check_required_credentials() + query = self.omit(params, self.extract_params(path)) + url += '/' + self.implode_params(path, params) + nonce = str(self.milliseconds()) + payload: Str = None + if method != 'POST': + if query: + url += '?' + self.urlencode(query) + payload = self.apiKey + nonce + elif body is None: + body = self.json(query) + payload = self.apiKey + nonce + body + headers = { + 'Request-Timestamp': nonce, + 'Operation-Id': self.uuid(), + 'API-Key': self.apiKey, + 'API-Hash': self.hmac(self.encode(payload), self.encode(self.secret), hashlib.sha512), + 'Content-Type': 'application/json', + } + else: + self.check_required_credentials() + body = self.urlencode(self.extend({ + 'method': path, + 'moment': self.nonce(), + }, params)) + headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + 'API-Key': self.apiKey, + 'API-Hash': self.hmac(self.encode(body), self.encode(self.secret), hashlib.sha512), + } + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None # fallback to default error handler + if 'code' in response: + # + # bitbay returns the integer "success": 1 key from their private API + # or an integer "code" value from 0 to 510 and an error message + # + # {"success": 1, ...} + # {'code': 502, "message": "Invalid sign"} + # {'code': 0, "message": "offer funds not exceeding minimums"} + # + # 400 At least one parameter wasn't set + # 401 Invalid order type + # 402 No orders with specified currencies + # 403 Invalid payment currency name + # 404 Error. Wrong transaction type + # 405 Order with self id doesn't exist + # 406 No enough money or crypto + # 408 Invalid currency name + # 501 Invalid public key + # 502 Invalid sign + # 503 Invalid moment parameter. Request time doesn't match current server time + # 504 Invalid method + # 505 Key has no permission for self action + # 506 Account locked. Please contact with customer service + # 509 The BIC/SWIFT is required for self currency + # 510 Invalid market name + # + code = self.safe_string(response, 'code') # always an integer + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions, code, feedback) + raise ExchangeError(feedback) + elif 'status' in response: + # + # {"status":"Fail","errors":["OFFER_FUNDS_NOT_EXCEEDING_MINIMUMS"]} + # + status = self.safe_string(response, 'status') + if status == 'Fail': + errors = self.safe_value(response, 'errors') + feedback = self.id + ' ' + body + for i in range(0, len(errors)): + error = errors[i] + self.throw_exactly_matched_exception(self.exceptions, error, feedback) + raise ExchangeError(feedback) + return None diff --git a/ccxt/backpack.py b/ccxt/backpack.py new file mode 100644 index 0000000..d271e29 --- /dev/null +++ b/ccxt/backpack.py @@ -0,0 +1,2230 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.backpack import ImplicitAPI +from ccxt.base.types import Any, Balances, Bool, Currencies, Currency, DepositAddress, Int, Market, MarketType, Num, Order, OrderBook, OrderRequest, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, Trade, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import OperationRejected +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OperationFailed +from ccxt.base.errors import NetworkError +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.errors import RequestTimeout +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class backpack(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(backpack, self).describe(), { + 'id': 'backpack', + 'name': 'Backpack', + 'countries': ['JP'], # Japan + 'rateLimit': 50, # 20 times per second + 'version': 'v1', + 'certified': False, + 'pro': True, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': True, + 'swap': True, + 'future': False, + 'option': False, + 'addMargin': False, + 'cancelAllOrders': True, + 'cancelAllOrdersAfter': False, + 'cancelOrder': True, + 'cancelOrders': False, + 'cancelWithdraw': False, + 'closePosition': False, + 'createConvertTrade': False, # todo + 'createDepositAddress': False, + 'createLimitBuyOrder': True, + 'createLimitOrder': True, + 'createLimitSellOrder': True, + 'createMarketBuyOrder': True, + 'createMarketBuyOrderWithCost': True, + 'createMarketOrder': True, + 'createMarketOrderWithCost': True, + 'createMarketSellOrder': True, + 'createMarketSellOrderWithCost': True, + 'createOrder': True, + 'createOrders': True, + 'createOrderWithTakeProfitAndStopLoss': True, + 'createPostOnlyOrder': True, + 'createReduceOnlyOrder': True, + 'createStopLossOrder': False, + 'createTakeProfitOrder': False, + 'createTrailingAmountOrder': False, + 'createTrailingPercentOrder': False, + 'createTriggerOrder': True, + 'fetchAccounts': False, + 'fetchAllGreeks': False, + 'fetchBalance': True, + 'fetchCanceledAndClosedOrders': False, + 'fetchCanceledOrders': False, + 'fetchClosedOrder': False, + 'fetchClosedOrders': False, + 'fetchConvertCurrencies': False, + 'fetchConvertQuote': False, + 'fetchConvertTrade': False, + 'fetchConvertTradeHistory': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': True, + 'fetchDeposits': True, + 'fetchDepositsWithdrawals': False, + 'fetchDepositWithdrawFees': False, + 'fetchFundingHistory': True, + 'fetchFundingRate': True, + 'fetchFundingRateHistory': True, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': True, + 'fetchLedger': False, + 'fetchLeverage': False, + 'fetchLeverageTiers': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': True, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': True, + 'fetchOpenInterestHistory': True, + 'fetchOpenOrder': True, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': False, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchOrderTrades': False, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': True, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchStatus': True, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': False, + 'fetchTransactions': False, + 'fetchTransfers': False, + 'fetchVolatilityHistory': False, + 'fetchWithdrawals': True, + 'reduceMargin': False, + 'sandbox': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'transfer': False, + 'withdraw': True, + }, + 'timeframes': { + '1m': '1m', + '3m': '3m', + '5m': '5m', + '15': '15m', + '30': '30m', + '1h': '1h', + '2h': '2h', + '4h': '4h', + '6h': '6h', + '8h': '8h', + '12h': '12h', + '1d': '1d', + '3d': '3d', + '1w': '1w', + '1M': '1month', + }, + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/cc04c278-679f-4554-9f72-930dd632b80f', + 'api': { + 'public': 'https://api.backpack.exchange', + 'private': 'https://api.backpack.exchange', + }, + 'www': 'https://backpack.exchange/', + 'doc': 'https://docs.backpack.exchange/', + 'referral': 'https://backpack.exchange/join/ccxt', + }, + 'api': { + 'public': { + 'get': { + 'api/v1/assets': 1, # done + 'api/v1/collateral': 1, # not used + 'api/v1/borrowLend/markets': 1, + 'api/v1/borrowLend/markets/history': 1, + 'api/v1/markets': 1, # done + 'api/v1/market': 1, # not used + 'api/v1/ticker': 1, # done + 'api/v1/tickers': 1, # done + 'api/v1/depth': 1, # done + 'api/v1/klines': 1, # done + 'api/v1/markPrices': 1, # done + 'api/v1/openInterest': 1, # done + 'api/v1/fundingRates': 1, # done + 'api/v1/status': 1, # done + 'api/v1/ping': 1, # todo check if it is needed for ws + 'api/v1/time': 1, # done + 'api/v1/wallets': 1, # not used + 'api/v1/trades': 1, # done + 'api/v1/trades/history': 1, # done + }, + }, + 'private': { + 'get': { + 'api/v1/account': 1, # todo fetchTradingFee + 'api/v1/account/limits/borrow': 1, # not used + 'api/v1/account/limits/order': 1, # not used + 'api/v1/account/limits/withdrawal': 1, # not used + 'api/v1/borrowLend/positions': 1, # todo fetchBorrowInterest + 'api/v1/capital': 1, # done + 'api/v1/capital/collateral': 1, # not used + 'wapi/v1/capital/deposits': 1, # done + 'wapi/v1/capital/deposit/address': 1, # done + 'wapi/v1/capital/withdrawals': 1, # todo complete after withdrawal + 'api/v1/position': 1, # done but todo check if all is right + 'wapi/v1/history/borrowLend': 1, # not used + 'wapi/v1/history/interest': 1, # not used + 'wapi/v1/history/borrowLend/positions': 1, # not used + 'wapi/v1/history/dust': 1, # not used + 'wapi/v1/history/fills': 1, # done + 'wapi/v1/history/funding': 1, # done + 'wapi/v1/history/orders': 1, # done + 'wapi/v1/history/rfq': 1, + 'wapi/v1/history/quote': 1, + 'wapi/v1/history/settlement': 1, + 'wapi/v1/history/strategies': 1, + 'api/v1/order': 1, # done + 'api/v1/orders': 1, # done + }, + 'post': { + 'api/v1/account/convertDust': 1, + 'api/v1/borrowLend': 1, # todo borrowCrossMargin + 'wapi/v1/capital/withdrawals': 1, # todo complete after withdrawal + 'api/v1/order': 1, # done + 'api/v1/orders': 1, # done + 'api/v1/rfq': 1, + 'api/v1/rfq/accept': 1, + 'api/v1/rfq/refresh': 1, + 'api/v1/rfq/cancel': 1, + 'api/v1/rfq/quote': 1, + }, + 'delete': { + 'api/v1/order': 1, # done + 'api/v1/orders': 1, # done + }, + 'patch': { + 'api/v1/account': 1, + }, + }, + }, + 'features': { + 'default': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': True, + 'takeProfitPrice': True, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'GTC': True, + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': True, + 'selfTradePrevention': { + 'EXPIRE_MAKER': True, + 'EXPIRE_TAKER': True, + 'EXPIRE_BOTH': True, + 'NONE': False, + }, + 'iceberg': False, + }, + 'createOrders': { + 'max': 20, + }, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': None, + 'untilDays': None, + 'symbolRequired': False, + }, + 'fetchOrder': None, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 1000, + 'trigger': True, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': None, + 'untilDays': None, + 'trigger': True, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchClosedOrders': None, + 'fetchOHLCV': { + 'paginate': False, + 'limit': 1000, + }, + }, + 'spot': { + 'extends': 'default', + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + }, + 'precisionMode': TICK_SIZE, + 'options': { + 'instructions': { + 'api/v1/account': { + 'GET': 'accountQuery', + 'PATCH': 'accountUpdate', + }, + 'api/v1/capital': { + 'GET': 'balanceQuery', + }, + 'api/v1/account/limits/borrow': { + 'GET': 'maxBorrowQuantity', + }, + 'api/v1/account/limits/order': { + 'GET': 'maxOrderQuantity', + }, + 'api/v1/account/limits/withdrawal': { + 'GET': 'maxWithdrawalQuantity', + }, + 'api/v1/borrowLend/positions': { + 'GET': 'borrowLendPositionQuery', + }, + 'api/v1/borrowLend': { + 'POST': 'borrowLendExecute', + }, + 'wapi/v1/history/borrowLend/positions': { + 'GET': 'borrowPositionHistoryQueryAll', + }, + 'wapi/v1/history/borrowLend': { + 'GET': 'borrowHistoryQueryAll', + }, + 'wapi/v1/history/dust': { + 'GET': 'dustHistoryQueryAll', + }, + 'api/v1/capital/collateral': { + 'GET': 'collateralQuery', + }, + 'wapi/v1/capital/deposit/address': { + 'GET': 'depositAddressQuery', + }, + 'wapi/v1/capital/deposits': { + 'GET': 'depositQueryAll', + }, + 'wapi/v1/history/fills': { + 'GET': 'fillHistoryQueryAll', + }, + 'wapi/v1/history/funding': { + 'GET': 'fundingHistoryQueryAll', + }, + 'wapi/v1/history/interest': { + 'GET': 'interestHistoryQueryAll', + }, + 'api/v1/order': { + 'GET': 'orderQuery', + 'POST': 'orderExecute', + 'DELETE': 'orderCancel', + }, + 'api/v1/orders': { + 'GET': 'orderQueryAll', + 'POST': 'orderExecute', + 'DELETE': 'orderCancelAll', + }, + 'wapi/v1/history/orders': { + 'GET': 'orderHistoryQueryAll', + }, + 'wapi/v1/history/pnl': { + 'GET': 'pnlHistoryQueryAll', + }, + 'wapi/v1/history/rfq': { + 'GET': 'rfqHistoryQueryAll', + }, + 'wapi/v1/history/quote': { + 'GET': 'quoteHistoryQueryAll', + }, + 'wapi/v1/history/settlement': { + 'GET': 'settlementHistoryQueryAll', + }, + 'api/v1/position': { + 'GET': 'positionQuery', + }, + 'api/v1/rfq/quote': { + 'POST': 'quoteSubmit', + }, + 'wapi/v1/history/strategies': { + 'GET': 'strategyHistoryQueryAll', + }, + 'wapi/v1/capital/withdrawals': { + 'GET': 'withdrawalQueryAll', + 'POST': 'withdraw', + }, + }, + 'recvWindow': 5000, # default is 5000, max is 60000 + 'brokerId': '', + 'currencyIdsListForParseMarket': None, + 'broker': '', + 'timeDifference': 0, # the difference between system clock and the exchange server clock in milliseconds + 'adjustForTimeDifference': False, # controls the adjustment logic upon instantiation + 'networks': { + 'APT': 'Aptos', + 'ARB': 'Arbitrum', + 'AVAX': 'Avalanche', + 'BASE': 'Base', + 'BERA': 'Berachain', + 'BTC': 'Bitcoin', + 'BCH': 'BitcoinCash', + 'BSC': 'Bsc', + 'ADA': 'Cardano', + 'DOGE': 'Dogecoin', + 'ECLIPSE': 'Eclipse', + 'EQUALSMONEY': 'EqualsMoney', + 'ERC20': 'Ethereum', + 'HYP': 'Hyperliquid', + 'LTC': 'Litecoin', + 'OPTIMISM': 'Optimism', + 'MATIC': 'Polygon', + 'SEI': 'Sei', + 'SUI': 'Sui', + 'SOL': 'Solana', + 'STORY': 'Story', + 'TRC20': 'Tron', + 'XRP': 'XRP', + }, + 'networksById': { + 'aptos': 'APT', + 'arbitrum': 'ARB', + 'avalanche': 'AVAX', + 'base': 'BASE', + 'berachain': 'BERA', + 'bitcoin': 'BTC', + 'bitcoincash': 'BCH', + 'bsc': 'BSC', + 'cardano': 'ADA', + 'dogecoin': 'DOGE', + 'eclipse': 'ECLIPSE', + 'equalsmoney': 'EQUALSMONEY', + 'ethereum': 'ERC20', + 'hyperliquid': 'HYP', + 'litecoin': 'LTC', + 'optimism': 'OPTIMISM', + 'polygon': 'MATIC', + 'sei': 'SEI', + 'sui': 'SUI', + 'solana': 'SOL', + 'story': 'STORY', + 'tron': 'TRC20', + 'xrp': 'XRP', + }, + }, + 'commonCurrencies': {}, + 'exceptions': { + 'exact': { + 'INVALID_CLIENT_REQUEST': BadRequest, + 'INVALID_ORDER': InvalidOrder, + 'ACCOUNT_LIQUIDATING': BadRequest, + 'BORROW_LIMIT': BadRequest, + 'BORROW_REQUIRES_LEND_REDEEM': BadRequest, + 'FORBIDDEN': OperationRejected, + 'INSUFFICIENT_FUNDS': InsufficientFunds, + 'INSUFFICIENT_MARGIN': InsufficientFunds, + 'INSUFFICIENT_SUPPLY': InsufficientFunds, + 'INVALID_ASSET': BadRequest, + 'INVALID_MARKET': BadSymbol, + 'INVALID_PRICE': BadRequest, + 'INVALID_POSITION_ID': BadRequest, + 'INVALID_QUANTITY': BadRequest, + 'INVALID_RANGE': BadRequest, + 'INVALID_SIGNATURE': AuthenticationError, + 'INVALID_SOURCE': BadRequest, + 'INVALID_SYMBOL': BadSymbol, + 'INVALID_TWO_FACTOR_CODE': BadRequest, + 'LEND_LIMIT': BadRequest, + 'LEND_REQUIRES_BORROW_REPAY': BadRequest, + 'MAINTENANCE': ExchangeError, + 'MAX_LEVERAGE_REACHED': InsufficientFunds, + 'NOT_IMPLEMENTED': OperationFailed, + 'ORDER_LIMIT': OperationRejected, + 'POSITION_LIMIT': OperationRejected, + 'PRECONDITION_FAILED': OperationFailed, + 'RESOURCE_NOT_FOUND': ExchangeNotAvailable, + 'SERVER_ERROR': NetworkError, + 'TIMEOUT': RequestTimeout, + 'TOO_MANY_REQUESTS': RateLimitExceeded, + 'TRADING_PAUSED': ExchangeNotAvailable, + 'UNAUTHORIZED': AuthenticationError, + }, + # Bad Request parse request payload error: failed to parse "MarketSymbol": Invalid market symbol(occurred while parsing "OrderExecutePayload") + # failed to parse parameter `interval`: failed to parse "KlineInterval": Expect a valid enumeration value. + 'broad': {}, + }, + }) + + def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://docs.backpack.exchange/#tag/Assets + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = self.publicGetApiV1Assets(params) + # + # [ + # { + # "coingeckoId": "jito-governance-token", + # "displayName": "Jito", + # "symbol": "JTO", + # "tokens": [ + # { + # "blockchain": "Solana", + # "contractAddress": "jtojtomepa8beP8AuQc6eXt5FriJwfFMwQx2v2f9mCL", + # "depositEnabled": True, + # "displayName": "Jito", + # "maximumWithdrawal": null, + # "minimumDeposit": "0.29", + # "minimumWithdrawal": "0.58", + # "withdrawEnabled": True, + # "withdrawalFee": "0.29" + # } + # ] + # } + # ... + # ] + # + result: dict = {} + for i in range(0, len(response)): + currecy = response[i] + currencyId = self.safe_string(currecy, 'symbol') + code = self.safe_currency_code(currencyId) + networks = self.safe_list(currecy, 'tokens', []) + parsedNetworks: dict = {} + for j in range(0, len(networks)): + network = networks[j] + networkId = self.safe_string(network, 'blockchain') + networkIdLowerCase = self.safe_string_lower(network, 'blockchain') + networkCode = self.network_id_to_code(networkIdLowerCase) + parsedNetworks[networkCode] = { + 'id': networkId, + 'network': networkCode, + 'limits': { + 'withdraw': { + 'min': self.safe_number(network, 'minimumWithdrawal'), + 'max': self.parse_number(self.omit_zero(self.safe_string(network, 'maximumWithdrawal'))), + }, + 'deposit': { + 'min': self.safe_number(network, 'minimumDeposit'), + 'max': None, + }, + }, + 'active': None, + 'deposit': self.safe_bool(network, 'depositEnabled'), + 'withdraw': self.safe_bool(network, 'withdrawEnabled'), + 'fee': self.safe_number(network, 'withdrawalFee'), + 'precision': None, + 'info': network, + } + active = None + deposit = None + withdraw = None + if self.is_empty(parsedNetworks): # if networks are not provided + active = False + deposit = False + withdraw = False + result[code] = self.safe_currency_structure({ + 'id': currencyId, + 'code': code, + 'precision': None, + 'type': 'crypto', # todo check if it is always crypto + 'name': self.safe_string(currecy, 'displayName'), + 'active': active, + 'deposit': deposit, + 'withdraw': withdraw, + 'fee': None, + 'limits': { + 'deposit': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + }, + 'networks': parsedNetworks, + 'info': currecy, + }) + return result + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for bitbank + + https://docs.backpack.exchange/#tag/Markets/operation/get_markets + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + if self.options['adjustForTimeDifference']: + self.load_time_difference() + response = self.publicGetApiV1Markets(params) + return self.parse_markets(response) + + def parse_market(self, market: dict) -> Market: + # + # [ + # { + # "baseSymbol": "SOL", + # "createdAt": "2025-01-21T06:34:54.691858", + # "filters": { + # "price": { + # "borrowmarketFeeMaxMultiplier": null, + # "borrowmarketFeeMinMultiplier": null, + # "maxImpactMultiplier": "1.03", + # "maxMultiplier": "1.25", + # "maxPrice": null, + # "meanMarkPriceBand": { + # "maxMultiplier": "1.15", + # "minMultiplier": "0.9" + # }, + # "meanPremiumBand": null, + # "minImpactMultiplier": "0.97", + # "minMultiplier": "0.75", + # "minPrice": "0.01", + # "tickSize": "0.01" + # }, + # "quantity": { + # "maxQuantity": null, + # "minQuantity": "0.01", + # "stepSize": "0.01" + # } + # }, + # "fundingInterval": 28800000, + # "fundingRateLowerBound": null, + # "fundingRateUpperBound": null, + # "imfFunction": null, + # "marketType": "SPOT", + # "mmfFunction": null, + # "openInterestLimit": "0", + # "orderBookState": "Open", + # "quoteSymbol": "USDC", + # "symbol": "SOL_USDC" + # }, + # { + # "baseSymbol": "SOL", + # "createdAt": "2025-01-21T06:34:54.691858", + # "filters": { + # "price": { + # "borrowEntryFeeMaxMultiplier": null, + # "borrowEntryFeeMinMultiplier": null, + # "maxImpactMultiplier": "1.03", + # "maxMultiplier": "1.25", + # "maxPrice": "1000", + # "meanMarkPriceBand": { + # "maxMultiplier": "1.1", + # "minMultiplier": "0.9" + # }, + # "meanPremiumBand": { + # "tolerancePct": "0.05" + # }, + # "minImpactMultiplier": "0.97", + # "minMultiplier": "0.75", + # "minPrice": "0.01", + # "tickSize": "0.01" + # }, + # "quantity": { + # "maxQuantity": null, + # "minQuantity": "0.01", + # "stepSize": "0.01" + # } + # }, + # "fundingInterval": "28800000", + # "fundingRateLowerBound": "-100", + # "fundingRateUpperBound": "100", + # "imfFunction": { + # "base": "0.02", + # "factor": "0.0001275", + # "type": "sqrt" + # }, + # "marketType": "PERP", + # "mmfFunction": { + # "base": "0.0125", + # "factor": "0.0000765", + # "type": "sqrt" + # }, + # "openInterestLimit": "4000000", + # "orderBookState": "Open", + # "quoteSymbol": "USDC", + # "symbol": "SOL_USDC_PERP" + # } + # ] + # + id = self.safe_string(market, 'symbol') + baseId = self.safe_string(market, 'baseSymbol') + quoteId = self.safe_string(market, 'quoteSymbol') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + symbol = base + '/' + quote + filters = self.safe_dict(market, 'filters', {}) + priceFilter = self.safe_dict(filters, 'price', {}) + maxPrice = self.safe_number(priceFilter, 'maxPrice') + minPrice = self.safe_number(priceFilter, 'minPrice') + pricePrecision = self.safe_number(priceFilter, 'tickSize') + quantityFilter = self.safe_dict(filters, 'quantity', {}) + maxQuantity = self.safe_number(quantityFilter, 'maxQuantity') + minQuantity = self.safe_number(quantityFilter, 'minQuantity') + amountPrecision = self.safe_number(quantityFilter, 'stepSize') + type: MarketType + typeOfMarket = self.parse_market_type(self.safe_string(market, 'marketType')) + linear: Bool = None + inverse: Bool = None + settle: Str = None + settleId: Str = None + contractSize: Num = None + if typeOfMarket == 'spot': + type = 'spot' + elif typeOfMarket == 'swap': + type = 'swap' + linear = True + inverse = False + settleId = self.safe_string(market, 'quoteSymbol') + settle = self.safe_currency_code(settleId) + symbol += ':' + settle + contractSize = 1 + orderBookState = self.safe_string(market, 'orderBookState') + return self.safe_market_structure({ + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': type, + 'spot': type == 'spot', + 'margin': type == 'spot', # todo check if margin is supported for all markets + 'swap': type == 'swap', + 'future': False, + 'option': False, + 'active': orderBookState == 'Open', + 'contract': type != 'spot', + 'linear': linear, + 'inverse': inverse, + 'taker': None, # todo check commission + 'maker': None, # todo check commission + 'contractSize': contractSize, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': amountPrecision, + 'price': pricePrecision, + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': minQuantity, + 'max': maxQuantity, + }, + 'price': { + 'min': minPrice, + 'max': maxPrice, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': self.parse8601(self.safe_string(market, 'createdAt')), + 'info': market, + }) + + def parse_market_type(self, type): + types = { + 'SPOT': 'spot', + 'PERP': 'swap', + # current types are described in the docs, but the exchange returns only 'SPOT' and 'PERP' + # 'IPERP': 'swap', + # 'DATED': 'swap', + # 'PREDICTION': 'swap', + # 'RFQ': 'swap', + } + return self.safe_string(types, type, type) + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + + https://docs.backpack.exchange/#tag/Markets/operation/get_tickers + + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + request: dict = {} + response = self.publicGetApiV1Tickers(self.extend(request, params)) + tickers = self.parse_tickers(response) + return self.filter_by_array_tickers(tickers, 'symbol', symbols) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://docs.backpack.exchange/#tag/Markets/operation/get_ticker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = self.publicGetApiV1Ticker(self.extend(request, params)) + return self.parse_ticker(response, market) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # fetchTicker/fetchTickers + # + # { + # "firstPrice": "327.38", + # "high": "337.99", + # "lastPrice": "317.14", + # "low": "300.01", + # "priceChange": "-10.24", + # "priceChangePercent": "-0.031279", + # "quoteVolume": "21584.32278", + # "symbol": "AAVE_USDC", + # "trades": "245", + # "volume": "65.823" + # }, ... + # + marketId = self.safe_string(ticker, 'symbol') + market = self.safe_market(marketId, market) + symbol = self.safe_symbol(marketId, market) + open = self.safe_string(ticker, 'firstPrice') + last = self.safe_string(ticker, 'lastPrice') + high = self.safe_string(ticker, 'high') + low = self.safe_string(ticker, 'low') + baseVolume = self.safe_string(ticker, 'volume') + quoteVolume = self.safe_string(ticker, 'quoteVolume') + percentage = self.safe_string(ticker, 'priceChangePercent') + change = self.safe_string(ticker, 'priceChange') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': None, + 'datetime': None, + 'high': high, + 'low': low, + 'bid': None, + 'bidVolume': None, + 'ask': None, + 'askVolume': None, + 'vwap': None, + 'open': open, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': change, + 'percentage': percentage, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'markPrice': None, + 'indexPrice': None, + 'info': ticker, + }, market) + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://docs.backpack.exchange/#tag/Markets/operation/get_depth + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return(default 100, max 200) + :param dict [params]: extra parameters specific to the bitteam api endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = self.publicGetApiV1Depth(self.extend(request, params)) + # + # { + # "asks": [ + # ["118318.3","0.00633"], + # ["118567.2","0.08450"] + # ], + # "bids": [ + # ["1.0","0.38647"], + # ["12.9","1.00000"] + # ], + # "lastUpdateId":"1504999670", + # "timestamp":1753102447307501 + # } + # + microseconds = self.safe_integer(response, 'timestamp') + timestamp = self.parse_to_int(microseconds / 1000) + orderbook = self.parse_order_book(response, symbol, timestamp) + orderbook['nonce'] = self.safe_integer(response, 'lastUpdateId') + return orderbook + + def fetch_ohlcv(self, symbol: str, timeframe='1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://docs.backpack.exchange/#tag/Markets/operation/get_klines + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in seconds of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch(default 100) + :param dict [params]: extra parameters specific to the bitteam api endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + market = self.market(symbol) + interval = self.safe_string(self.timeframes, timeframe, timeframe) + request: dict = { + 'symbol': market['id'], + 'interval': interval, + } + until: Int = None + until, params = self.handle_option_and_params(params, 'fetchOHLCV', 'until') + if until is not None: + request['endTime'] = self.parse_to_int(until / 1000) # convert milliseconds to seconds + defaultLimit = 100 + if since is None: + if limit is None: + limit = defaultLimit + duration = self.parse_timeframe(timeframe) + endTime = self.parse_to_int(until / 1000) if until else self.seconds() + startTime = endTime - (limit * duration) + request['startTime'] = startTime + else: + request['startTime'] = self.parse_to_int(since / 1000) # convert milliseconds to seconds + price = self.safe_string(params, 'price') + if price is not None: + request['priceType'] = self.capitalize(price) + params = self.omit(params, 'price') + response = self.publicGetApiV1Klines(self.extend(request, params)) + return self.parse_ohlcvs(response, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # [ + # { + # "close": "118294.6", + # "end": "2025-07-19 13:12:00", + # "high": "118297.6", + # "low": "118237.5", + # "open": "118238", + # "quoteVolume": "4106.558156", + # "start": "2025-07-19 13:09:00", + # "trades": "12", + # "volume": "0.03473" + # }, + # ... + # ] + # + return [ + self.parse8601(self.safe_string(ohlcv, 'start')), + self.safe_number(ohlcv, 'open'), + self.safe_number(ohlcv, 'high'), + self.safe_number(ohlcv, 'low'), + self.safe_number(ohlcv, 'close'), + self.safe_number(ohlcv, 'volume'), + ] + + def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate + + https://docs.backpack.exchange/#tag/Markets/operation/get_mark_prices + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + self.load_markets() + market = self.market(symbol) + if market['spot']: + raise BadRequest(self.id + ' fetchFundingRate() symbol does not support market ' + symbol) + request: dict = { + 'symbol': market['id'], + } + response = self.publicGetApiV1MarkPrices(self.extend(request, params)) + data = self.safe_dict(response, 0, {}) + return self.parse_funding_rate(data, market) + + def parse_funding_rate(self, contract, market: Market = None) -> FundingRate: + # + # { + # "fundingRate": "0.0001", + # "indexPrice": "118333.18643195", + # "markPrice": "118343.51853741", + # "nextFundingTimestamp": 1753113600000, + # "symbol": "BTC_USDC_PERP" + # } + # + marketId = self.safe_string(contract, 'symbol') + market = self.safe_market(marketId, market) + symbol = self.safe_symbol(marketId, market) + nextFundingTimestamp = self.safe_integer(contract, 'nextFundingTimestamp') + return { + 'info': contract, + 'symbol': symbol, + 'markPrice': self.safe_number(contract, 'markPrice'), + 'indexPrice': self.safe_number(contract, 'indexPrice'), + 'interestRate': None, + 'estimatedSettlePrice': None, + 'timestamp': None, + 'datetime': None, + 'fundingRate': self.safe_number(contract, 'fundingRate'), + 'fundingTimestamp': None, + 'fundingDatetime': None, + 'nextFundingRate': None, + 'nextFundingTimestamp': nextFundingTimestamp, + 'nextFundingDatetime': self.iso8601(nextFundingTimestamp), + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': '1h', + } + + def fetch_open_interest(self, symbol: str, params={}): + """ + Retrieves the open interest of a derivative trading pair + + https://docs.backpack.exchange/#tag/Markets/operation/get_open_interest + + :param str symbol: Unified CCXT market symbol + :param dict [params]: exchange specific parameters + :returns dict} an open interest structure{@link https://docs.ccxt.com/#/?id=interest-history-structure: + """ + self.load_markets() + market = self.market(symbol) + if market['spot']: + raise BadRequest(self.id + ' fetchOpenInterest() symbol does not support market ' + symbol) + request: dict = { + 'symbol': market['id'], + } + response = self.publicGetApiV1OpenInterest(self.extend(request, params)) + interest = self.safe_dict(response, 0, {}) + return self.parse_open_interest(interest, market) + + def parse_open_interest(self, interest, market: Market = None): + # + # [ + # { + # "openInterest": "1273.85214", + # "symbol": "BTC_USDC_PERP", + # "timestamp":1753105735301 + # } + # ] + # + timestamp = self.safe_integer(interest, 'timestamp') + openInterest = self.safe_number(interest, 'openInterest') + return self.safe_open_interest({ + 'symbol': market['symbol'], + 'openInterestAmount': None, + 'openInterestValue': openInterest, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'info': interest, + }, market) + + def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical funding rate prices + + https://docs.backpack.exchange/#tag/Markets/operation/get_funding_interval_rates + + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: timestamp in ms of the earliest funding rate to fetch + :param int [limit]: the maximum amount of funding rate structures + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `funding rate structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchFundingRateHistory() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['limit'] = min(limit, 1000) # api maximum 1000 + response = self.publicGetApiV1FundingRates(self.extend(request, params)) + # + # [ + # { + # "fundingRate": "0.0001", + # "intervalEndTimestamp": "2025-07-22T00:00:00", + # "symbol": "BTC_USDC_PERP" + # } + # ] + # + rates = [] + for i in range(0, len(response)): + rate = response[i] + datetime = self.safe_string(rate, 'intervalEndTimestamp') + timestamp = self.parse8601(datetime) + rates.append({ + 'info': rate, + 'symbol': market['symbol'], + 'fundingRate': self.safe_number(rate, 'fundingRate'), + 'timestamp': timestamp, + 'datetime': datetime, + }) + sorted = self.sort_by(rates, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, market['symbol'], since, limit) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://docs.backpack.exchange/#tag/Trades/operation/get_recent_trades + https://docs.backpack.exchange/#tag/Trades/operation/get_historical_trades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.offset]: the number of trades to skip, default is 0 + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['limit'] = min(limit, 1000) # api maximum 1000 + response = None + offset = self.safe_integer(params, 'offset') + if offset is not None: + response = self.publicGetApiV1TradesHistory(self.extend(request, params)) + else: + response = self.publicGetApiV1Trades(self.extend(request, params)) + return self.parse_trades(response, market, since, limit) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://docs.backpack.exchange/#tag/History/operation/get_fills + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve(default 100, max 1000) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch trades for + :param str [params.fillType]: 'User'(default) 'BookLiquidation' or 'Adl' or 'Backstop' or 'Liquidation' or 'AllLiquidation' or 'CollateralConversion' or 'CollateralConversionAndSpotLiquidation' + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if since is not None: + request['from'] = since + if limit is not None: + request['limit'] = limit + until = self.safe_integer(params, 'until') + if until is not None: + params = self.omit(params, ['until']) + request['to'] = until + fillType = self.safe_string(params, 'fillType') + if fillType is None: + request['fillType'] = 'User' # default + response = self.privateGetWapiV1HistoryFills(self.extend(request, params)) + return self.parse_trades(response, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades + # { + # "id": 8721564, + # "isBuyerMaker": False, + # "price": "117427.6", + # "quantity": "0.00016", + # "quoteQuantity": "18.788416", + # "timestamp": 1753123916818 + # } + # + # fetchMyTrades + # { + # "clientId": null, + # "fee": "0.004974", + # "feeSymbol": "USDC", + # "isMaker": False, + # "orderId": "4238907375", + # "price": "3826.15", + # "quantity": "0.0026", + # "side": "Bid", + # "symbol": "ETH_USDC_PERP", + # "systemOrderType": null, + # "timestamp": "2025-07-27T17:39:00.092", + # "tradeId": 9748827 + # } + # + id = self.safe_string_2(trade, 'id', 'tradeId') + marketId = self.safe_string(trade, 'symbol') + market = self.safe_market(marketId, market) + price = self.safe_string(trade, 'price') + amount = self.safe_string(trade, 'quantity') + isMaker = self.safe_bool(trade, 'isMaker') + takerOrMaker = 'maker' if isMaker else 'taker' + orderId = self.safe_string(trade, 'orderId') + side = self.parse_order_side(self.safe_string(trade, 'side')) + fee = None + feeAmount = self.safe_string(trade, 'fee') + timestamp = self.safe_integer(trade, 'timestamp') + if feeAmount is not None: + # if fetchMyTrades + datetime = self.safe_string(trade, 'timestamp') + timestamp = self.parse8601(datetime) + feeSymbol = self.safe_currency_code(self.safe_string(trade, 'feeSymbol')) + if feeAmount is not None: + fee = { + 'cost': feeAmount, + 'currency': feeSymbol, + 'rate': None, + } + return self.safe_trade({ + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'id': id, + 'order': orderId, + 'type': None, + 'side': side, + 'takerOrMaker': takerOrMaker, + 'price': price, + 'amount': amount, + 'cost': None, + 'fee': fee, + }, market) + + def fetch_status(self, params={}): + """ + the latest known information on the availability of the exchange API + + https://docs.backpack.exchange/#tag/System/operation/get_status + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `status structure ` + """ + response = self.publicGetApiV1Status(params) + # + # { + # "message":null, + # "status":"Ok" + # } + # + status = self.safe_string(response, 'status') + return { + 'status': status.lower(), + 'updated': None, + 'eta': None, + 'url': None, + 'info': response, + } + + def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://developer-pro.bitmart.com/en/spot/#get-system-time + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = self.publicGetApiV1Time(params) + # + # 1753131712992 + # + return self.safe_integer(response, 0, self.milliseconds()) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://docs.backpack.exchange/#tag/Capital/operation/get_balances + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + response = self.privateGetApiV1Capital(params) + return self.parse_balance(response) + + def parse_balance(self, response) -> Balances: + # + # { + # "USDC": { + # "available": "120", + # "locked": "0", + # "staked": "0" + # } + # } + # + balanceKeys = list(response.keys()) + result: dict = {} + for i in range(0, len(balanceKeys)): + id = balanceKeys[i] + code = self.safe_currency_code(id) + balance = response[id] + account = self.account() + locked = self.safe_string(balance, 'locked') + staked = self.safe_string(balance, 'staked') + used = Precise.string_add(locked, staked) + account['free'] = self.safe_string(balance, 'available') + account['used'] = used + result[code] = account + return self.safe_balance(result) + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://docs.backpack.exchange/#tag/Capital/operation/get_deposits + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch entries for + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + request: dict = { + } + currency: Currency = None + if code is not None: + currency = self.currency(code) + if since is not None: + request['from'] = since + if limit is not None: + request['limit'] = limit # default 100, max 1000 + until: Int = None + until, params = self.handle_option_and_params(params, 'fetchDeposits', 'until') + if until is not None: + request['endTime'] = until + response = self.privateGetWapiV1CapitalDeposits(self.extend(request, params)) + return self.parse_transactions(response, currency, since, limit) + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://docs.backpack.exchange/#tag/Capital/operation/get_withdrawals + + :param str code: unified currency code of the currency transferred + :param int [since]: the earliest time in ms to fetch transfers for(default 24 hours ago) + :param int [limit]: the maximum number of transfer structures to retrieve(default 50, max 200) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch transfers for(default time now) + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + request: dict = {} + currency: Currency = None + if code is not None: + currency = self.currency(code) + if since is not None: + request['from'] = since + if limit is not None: + request['limit'] = limit + until: Int = None + until, params = self.handle_option_and_params(params, 'fetchWithdrawals', 'until') + if until is not None: + request['to'] = until + response = self.privateGetWapiV1CapitalWithdrawals(self.extend(request, params)) + return self.parse_transactions(response, currency, since, limit) + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://docs.backpack.exchange/#tag/Capital/operation/request_withdrawal + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str params['network']: the network to withdraw on(mandatory) + :returns dict: a `transaction structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'symbol': currency['id'], + 'quantity': self.number_to_string(amount), + 'address': address, + } + if tag is not None: + request['clientId'] = tag # memo or tag + networkCode, query = self.handle_network_code_and_params(params) + networkId = self.network_code_to_id(networkCode) + if networkId is None: + raise BadRequest(self.id + ' withdraw() requires a network parameter') + request['blockchain'] = networkId + response = self.privatePostWapiV1CapitalWithdrawals(self.extend(request, query)) + return self.parse_transaction(response, currency) + + def parse_transaction(self, transaction, currency: Currency = None) -> Transaction: + # + # fetchDeposits + # [ + # { + # "createdAt": "2025-07-23T13:55:54.267", + # "fiatAmount": null, + # "fiatCurrency": null, + # "fromAddress": "0x2e3ab3e88a7dbdc763aadf5b28c18fb085af420a", + # "id": 6695353, + # "institutionBic": null, + # "platformMemo": null, + # "quantity": "120", + # "source": "ethereum", + # "status": "confirmed", + # "symbol": "USDC", + # "toAddress": "0xfBe7CbfCde93c8a4204a4be6B56732Eb32690170", + # "transactionHash": "0x58edaac415398d617b34c6673fffcaf0024990d5700565030119db5cbf3765d1" + # } + # ] + # + # withdraw + # { + # "accountIdentifier": null, + # "bankIdentifier": null, + # "bankName": null, + # "blockchain": "Ethereum", + # "clientId": null, + # "createdAt": "2025-08-13T19:27:13.817", + # "fee": "3", + # "fiatFee": null, + # "fiatState": null, + # "fiatSymbol": null, + # "id": 5479929, + # "identifier": null, + # "isInternal": False, + # "providerId": null, + # "quantity": "10", + # "status": "pending", + # "subaccountId": null, + # "symbol": "USDC", + # "toAddress": "0x0ad42b8e602c2d3d475ae52d678cf63d84ab2749", + # "transactionHash": null, + # "triggerAt": null + # } + # + # fetchWithdrawals + # [ + # { + # "accountIdentifier": null, + # "bankIdentifier": null, + # "bankName": null, + # "blockchain": "Ethereum", + # "clientId": null, + # "createdAt": "2025-08-13T19:27:13.817", + # "fee": "3", + # "fiatFee": null, + # "fiatState": null, + # "fiatSymbol": null, + # "id": 5479929, + # "identifier": null, + # "isInternal": False, + # "providerId": null, + # "quantity": "10", + # "status": "confirmed", + # "subaccountId": null, + # "symbol": "USDC", + # "toAddress": "0x0ad42b8e602c2d3d475ae52d678cf63d84ab2749", + # "transactionHash": "0x658b6d082af4afa0d3cf85caf344ff7c19d980117726bf193b00d8850f8746a1", + # "triggerAt": null + # } + # ] + # + status = self.parse_transaction_status(self.safe_string(transaction, 'status')) + id = self.safe_string(transaction, 'id') + txid = self.safe_string(transaction, 'transactionHash') + coin = self.safe_string(transaction, 'symbol') + code = self.safe_currency_code(coin, currency) + timestamp = self.parse8601(self.safe_string(transaction, 'createdAt')) + amount = self.safe_number(transaction, 'quantity') + networkId = self.safe_string_lower_2(transaction, 'source', 'blockchain') + network = self.network_id_to_code(networkId) + addressTo = self.safe_string(transaction, 'toAddress') + addressFrom = self.safe_string(transaction, 'fromAddress') + tag = self.safe_string(transaction, 'platformMemo') + feeCost = self.safe_number(transaction, 'fee') + internal = self.safe_bool(transaction, 'isInternal', False) + fee = None + if feeCost is not None: + fee = { + 'cost': feeCost, + 'currency': code, + } + return { + 'info': transaction, + 'id': id, + 'txid': txid, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': network, + 'address': None, + 'addressTo': addressTo, + 'addressFrom': addressFrom, + 'tag': tag, + 'tagTo': None, + 'tagFrom': None, + 'type': None, + 'amount': amount, + 'currency': code, + 'status': status, + 'updated': None, + 'internal': internal, + 'comment': None, + 'fee': fee, + } + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'cancelled': 'cancelled', + 'confirmed': 'ok', + 'declined': 'declined', + 'expired': 'expired', + 'initiated': 'initiated', + 'pending': 'pending', + 'refunded': 'refunded', + 'information required': 'pending', + } + return self.safe_string(statuses, status, status) + + def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://docs.backpack.exchange/#tag/Capital/operation/get_deposit_address + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.networkCode]: the network to fetch the deposit address(mandatory) + :returns dict: an `address structure ` + """ + self.load_markets() + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + if networkCode is None: + raise ArgumentsRequired(self.id + ' fetchDepositAddress() requires a network parameter, see https://docs.ccxt.com/#/?id=network-codes') + currency = self.currency(code) + request: dict = { + 'blockchain': self.network_code_to_id(networkCode), + } + response = self.privateGetWapiV1CapitalDepositAddress(self.extend(request, params)) + return self.parse_deposit_address(response, currency) + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + # + # { + # "address": "0xfBe7CbfCde93c8a4204a4be6B56732Eb32690170" + # } + # + address = self.safe_string(depositAddress, 'address') + currencyId = self.safe_string(depositAddress, 'currency') + currency = self.safe_currency(currencyId, currency) + return { + 'info': depositAddress, + 'currency': currency['code'], + 'network': None, # network is not returned by the API + 'address': address, + 'tag': None, + } + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}) -> Order: + """ + create a trade order + + https://docs.backpack.exchange/#tag/Order/operation/execute_order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fullfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.cost]: *market orders only* the cost of the order in units of the quote currency(could be used instead of amount) + :param int [params.clientOrderId]: a unique id for the order + :param boolean [params.postOnly]: True to place a post only order + :param str [params.timeInForce]: 'GTC', 'IOC', 'FOK' or 'PO' + :param bool [params.reduceOnly]: *contract only* Indicates if self order is to reduce the size of a position + :param str [params.selfTradePrevention]: one of EXPIRE_MAKER, EXPIRE_TAKER or EXPIRE_BOTH + :param bool [params.autoLend]: *spot margin only* if True then the order can lend + :param bool [params.autoLendRedeem]: *spot margin only* if True then the order can redeem a lend if required + :param bool [params.autoBorrow]: *spot margin only* if True then the order can borrow + :param bool [params.autoBorrowRepay]: *spot margin only* if True then the order can repay a borrow + :param float [params.triggerPrice]: the price that a trigger order is triggered at + :param dict [params.takeProfit]: *swap markets only - takeProfit object in params* containing the triggerPrice at which the attached take profit order will be triggered + :param float [params.takeProfit.triggerPrice]: take profit trigger price + :param float [params.takeProfit.price]: take profit order price(if not provided the order will be a market order) + :param dict [params.stopLoss]: *swap markets only - stopLoss object in params* containing the triggerPrice at which the attached stop loss order will be triggered + :param float [params.stopLoss.triggerPrice]: stop loss trigger price + :param float [params.stopLoss.price]: stop loss order price(if not provided the order will be a market order) + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + orderRequest = self.create_order_request(symbol, type, side, amount, price, params) + response = self.privatePostApiV1Order(orderRequest) + return self.parse_order(response, market) + + def create_orders(self, orders: List[OrderRequest], params={}): + """ + create a list of trade orders + + https://docs.backpack.exchange/#tag/Order/operation/execute_order_batch + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + ordersRequests = [] + for i in range(0, len(orders)): + rawOrder = orders[i] + marketId = self.safe_string(rawOrder, 'symbol') + type = self.safe_string(rawOrder, 'type') + side = self.safe_string(rawOrder, 'side') + amount = self.safe_number(rawOrder, 'amount') + price = self.safe_number(rawOrder, 'price') + orderParams = self.safe_dict(rawOrder, 'params', {}) + extendedParams = self.extend(orderParams, params) # the request does not accept extra params since it's a list, so we're extending each order with the common params + orderRequest = self.create_order_request(marketId, type, side, amount, price, extendedParams) + ordersRequests.append(orderRequest) + response = self.privatePostApiV1Orders(ordersRequests) + return self.parse_orders(response) + + def create_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'side': self.encode_order_side(side), + 'orderType': self.capitalize(type), + } + triggerPrice = self.safe_string(params, 'triggerPrice') + isTriggerOrder = triggerPrice is not None + quantityKey = 'triggerQuantity' if isTriggerOrder else 'quantity' + # handle basic limit/market order types + if type == 'limit': + request['price'] = self.price_to_precision(symbol, price) + request[quantityKey] = self.amount_to_precision(symbol, amount) + elif type == 'market': + cost = self.safe_string_2(params, 'cost', 'quoteQuantity') + if cost is not None: + request['quoteQuantity'] = self.cost_to_precision(symbol, cost) + params = self.omit(params, ['cost', 'quoteQuantity']) + else: + request[quantityKey] = self.amount_to_precision(symbol, amount) + # trigger orders + if isTriggerOrder: + request['triggerPrice'] = self.price_to_precision(symbol, triggerPrice) + params = self.omit(params, 'triggerPrice') + clientOrderId = self.safe_integer(params, 'clientOrderId') # the exchange requires uint + if clientOrderId is not None: + request['clientId'] = clientOrderId + params = self.omit(params, 'clientOrderId') + postOnly = False + postOnly, params = self.handle_post_only(type == 'market', False, params) + if postOnly: + params['postOnly'] = True + takeProfit = self.safe_dict(params, 'takeProfit') + if takeProfit is not None: + takeProfitTriggerPrice = self.safe_string(takeProfit, 'triggerPrice') + if takeProfitTriggerPrice is not None: + request['takeProfitTriggerPrice'] = self.price_to_precision(symbol, takeProfitTriggerPrice) + takeProfitPrice = self.safe_string(takeProfit, 'price') + if takeProfitPrice is not None: + request['takeProfitLimitPrice'] = self.price_to_precision(symbol, takeProfitPrice) + params = self.omit(params, 'takeProfit') + stopLoss = self.safe_dict(params, 'stopLoss') + if stopLoss is not None: + stopLossTriggerPrice = self.safe_string(stopLoss, 'triggerPrice') + if stopLossTriggerPrice is not None: + request['stopLossTriggerPrice'] = self.price_to_precision(symbol, stopLossTriggerPrice) + stopLossPrice = self.safe_string(stopLoss, 'price') + if stopLossPrice is not None: + request['stopLossLimitPrice'] = self.price_to_precision(symbol, stopLossPrice) + params = self.omit(params, 'stopLoss') + selfTradePrevention = None + selfTradePrevention, params = self.handle_option_and_params(params, 'createOrder', 'selfTradePrevention') + if selfTradePrevention is not None: + if selfTradePrevention == 'EXPIRE_MAKER': + request['selfTradePrevention'] = 'RejectMaker' + elif selfTradePrevention == 'EXPIRE_TAKER': + request['selfTradePrevention'] = 'RejectTaker' + elif selfTradePrevention == 'EXPIRE_BOTH': + request['selfTradePrevention'] = 'RejectBoth' + return self.extend(request, params) + + def encode_order_side(self, side): + sides: dict = { + 'buy': 'Bid', + 'sell': 'Ask', + } + return self.safe_string(sides, side, side) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all unfilled currently open orders + + https://docs.backpack.exchange/#tag/Order/operation/get_open_orders + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + response = self.privateGetApiV1Orders(self.extend(request, params)) + return self.parse_orders(response, market, since, limit) + + def fetch_open_order(self, id: str, symbol: Str = None, params={}): + """ + fetch an open order by it's id + + https://docs.backpack.exchange/#tag/Order/operation/get_order + + :param str id: order id + :param str symbol: not used by hollaex fetchOpenOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOpenOrder() requires a symbol argument') + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'orderId': id, + } + response = self.privateGetApiV1Order(self.extend(request, params)) + return self.parse_order(response) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://docs.backpack.exchange/#tag/Order/operation/cancel_order + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + market = self.market(symbol) + request: dict = { + 'orderId': id, + 'symbol': market['id'], + } + response = self.privateDeleteApiV1Order(self.extend(request, params)) + return self.parse_order(response) + + def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders + + https://docs.backpack.exchange/#tag/Order/operation/cancel_open_orders + + :param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + self.load_markets() + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = self.privateDeleteApiV1Orders(self.extend(request, params)) + return self.parse_orders(response, market) + + def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://docs.backpack.exchange/#tag/History/operation/get_order_history + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of orde structures to retrieve(default 100, max 1000) + :param dict [params]: extra parameters specific to the bitteam api endpoint + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if limit is not None: + request['limit'] = limit + response = self.privateGetWapiV1HistoryOrders(self.extend(request, params)) + return self.parse_orders(response, market, since, limit) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # { + # "clientId": null, + # "createdAt": 1753624283415, + # "executedQuantity": "0.001", + # "executedQuoteQuantity": "3.81428", + # "id": "4227701917", + # "orderType": "Market", + # "quantity": "0.001", + # "quoteQuantity": "3.82", + # "reduceOnly": null, + # "relatedOrderId": null, + # "selfTradePrevention": "RejectTaker", + # "side": "Bid", + # "status": "Filled", + # "stopLossLimitPrice": null, + # "stopLossTriggerBy": null, + # "stopLossTriggerPrice": null, + # "strategyId": null, + # "symbol": "ETH_USDC", + # "takeProfitLimitPrice": null, + # "takeProfitTriggerBy": null, + # "takeProfitTriggerPrice": null, + # "timeInForce": "GTC", + # "triggerBy": null, + # "triggerPrice": null, + # "triggerQuantity": null, + # "triggeredAt": null + # } + # + # fetchOpenOrders + # { + # "clientId": 123456789, + # "createdAt": 1753626206762, + # "executedQuantity": "0", + # "executedQuoteQuantity": "0", + # "id": "4228978331", + # "orderType": "Limit", + # "postOnly": True, + # "price": "3000", + # "quantity": "0.001", + # "reduceOnly": null, + # "relatedOrderId": null, + # "selfTradePrevention": "RejectTaker", + # "side": "Bid", + # "status": "New", + # "stopLossLimitPrice": null, + # "stopLossTriggerBy": null, + # "stopLossTriggerPrice": null, + # "strategyId": null, + # "symbol": "ETH_USDC", + # "takeProfitLimitPrice": null, + # "takeProfitTriggerBy": null, + # "takeProfitTriggerPrice": null, + # "timeInForce": "GTC", + # "triggerBy": null, + # "triggerPrice": null, + # "triggerQuantity": null, + # "triggeredAt": null + # } + # + # fetchOrders + # { + # "clientId": null, + # "createdAt": "2025-07-27T18:05:40.897", + # "executedQuantity": "0", + # "executedQuoteQuantity": "0", + # "expiryReason": null, + # "id": "4239996998", + # "orderType": "Limit", + # "postOnly": False, + # "price": "4500", + # "quantity": null, + # "quoteQuantity": null, + # "selfTradePrevention": "RejectTaker", + # "side": "Ask", + # "status": "Cancelled", + # "stopLossLimitPrice": null, + # "stopLossTriggerBy": null, + # "stopLossTriggerPrice": null, + # "strategyId": null, + # "symbol": "ETH_USDC", + # "systemOrderType": null, + # "takeProfitLimitPrice": null, + # "takeProfitTriggerBy": null, + # "takeProfitTriggerPrice": null, + # "timeInForce": "GTC", + # "triggerBy": null, + # "triggerPrice": "4300", + # "triggerQuantity": "0.001" + # } + # + timestamp = self.safe_integer(order, 'createdAt') + timestamp2 = self.parse8601(self.safe_string(order, 'createdAt')) + if timestamp2 is not None: + timestamp = timestamp2 + id = self.safe_string(order, 'id') + clientOrderId = self.safe_string(order, 'clientId') + symbol = self.safe_symbol(self.safe_string(order, 'symbol'), market) + type = self.safe_string_lower(order, 'orderType') + timeInForce = self.safe_string(order, 'timeInForce') + side = self.parse_order_side(self.safe_string(order, 'side')) + amount = self.safe_string_2(order, 'quantity', 'triggerQuantity') + price = self.safe_string(order, 'price') + cost = self.safe_string(order, 'executedQuoteQuantity') + status = self.parse_order_status(self.safe_string(order, 'status')) + triggerPrice = self.safe_string(order, 'triggerPrice') + filled = self.safe_string(order, 'executedQuantity') + reduceOnly = self.safe_bool(order, 'reduceOnly') + postOnly = self.safe_bool(order, 'postOnly') + stopLossPrice = self.safe_string_2(order, 'stopLossLimitPrice', 'stopLossTriggerPrice') + takeProfitPrice = self.safe_string_2(order, 'takeProfitLimitPrice', 'takeProfitTriggerPrice') + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'symbol': symbol, + 'type': type, + 'timeInForce': timeInForce, + 'postOnly': postOnly, + 'reduceOnly': reduceOnly, + 'side': side, + 'price': price, + 'triggerPrice': triggerPrice, + 'stopLossPrice': stopLossPrice, + 'takeProfitPrice': takeProfitPrice, + 'amount': amount, + 'cost': cost, + 'average': None, + 'filled': filled, + 'remaining': None, + 'status': status, + 'fee': None, + 'trades': None, + }, market) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'New': 'open', + 'Filled': 'closed', + 'Cancelled': 'canceled', + 'Expired': 'canceled', + 'PartiallyFilled': 'open', + 'TriggerPending': 'open', + 'TriggerFailed': 'rejected', + } + return self.safe_string(statuses, status, status) + + def parse_order_side(self, side: Str): + sides: dict = { + 'Bid': 'buy', + 'Ask': 'sell', + } + return self.safe_string(sides, side, side) + + def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://docs.backpack.exchange/#tag/Futures/operation/get_positions + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + self.load_markets() + response = self.privateGetApiV1Position(params) + positions = self.parse_positions(response) + if self.is_empty(symbols): + return positions + symbols = self.market_symbols(symbols) + return self.filter_by_array_positions(positions, 'symbol', symbols, False) + + def parse_position(self, position: dict, market: Market = None): + # + # fetchPositions + # { + # "breakEvenPrice": "3831.3630555555555555555555556", + # "cumulativeFundingPayment": "-0.009218", + # "cumulativeInterest": "0", + # "entryPrice": "3826.8888888888888888888888889", + # "estLiquidationPrice": "0", + # "imf": "0.02", + # "imfFunction": { + # "base": "0.02", + # "factor": "0.0000935", + # "type": "sqrt" + # }, + # "markPrice": "3787.46813304", + # "mmf": "0.0125", + # "mmfFunction": { + # "base": "0.0125", + # "factor": "0.0000561", + # "type": "sqrt" + # }, + # "netCost": "13.7768", + # "netExposureNotional": "13.634885278944", + # "netExposureQuantity": "0.0036", + # "netQuantity": "0.0036", + # "pnlRealized": "0", + # "pnlUnrealized": "-0.141914", + # "positionId": "4238420454", + # "subaccountId": null, + # "symbol": "ETH_USDC_PERP", + # "userId":1813870 + # } + # + # + id = self.safe_string(position, 'positionId') + marketId = self.safe_string(position, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + entryPrice = self.safe_string(position, 'entryPrice') + markPrice = self.safe_string(position, 'markPrice') + netCost = self.safe_string(position, 'netCost') + hedged = False + side = 'long' + if Precise.string_lt(netCost, '0'): + side = 'short' + if netCost is None: + hedged = None + side = None + unrealizedPnl = self.safe_string(position, 'pnlUnrealized') + realizedPnl = self.safe_string(position, 'pnlRealized') + liquidationPrice = self.safe_string(position, 'estLiquidationPrice') + return self.safe_position({ + 'info': position, + 'id': id, + 'symbol': symbol, + 'timestamp': self.parse8601(self.safe_string(position, 'timestamp')), + 'datetime': self.iso8601(self.parse8601(self.safe_string(position, 'timestamp'))), + 'lastUpdateTimestamp': None, + 'hedged': hedged, + 'side': side, + 'contracts': self.safe_string(position, 'netExposureQuantity'), + 'contractSize': None, + 'entryPrice': entryPrice, + 'markPrice': markPrice, + 'lastPrice': None, + 'notional': Precise.string_abs(netCost), + 'leverage': None, + 'collateral': None, + 'initialMargin': None, + 'initialMarginPercentage': self.safe_string(position, 'imf'), + 'maintenanceMargin': None, + 'maintenanceMarginPercentage': self.safe_string(position, 'mmf'), + 'realizedPnl': realizedPnl, + 'unrealizedPnl': unrealizedPnl, + 'liquidationPrice': liquidationPrice, + 'marginMode': None, + 'marginRatio': None, + 'percentage': None, + 'stopLossPrice': None, + 'takeProfitPrice': None, + }) + + def fetch_funding_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches the history of funding payments + + https://docs.backpack.exchange/#tag/History/operation/get_funding_payments + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch(default 24 hours ago) + :param int [limit]: the maximum amount of trades to fetch(default 200, max 500) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest trade to fetch(default now) + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if limit is not None: + request['limit'] = limit + response = self.privateGetWapiV1HistoryFunding(self.extend(request, params)) + return self.parse_incomes(response, market, since, limit) + + def parse_income(self, income, market: Market = None): + # + # { + # "fundingRate": "0.0001", + # "intervalEndTimestamp": "2025-08-01T16:00:00", + # "quantity": "-0.001301", + # "subaccountId": 0, + # "symbol": "ETH_USDC_PERP", + # "userId": 1813870 + # } + # + marketId = self.safe_string(income, 'symbol') + symbol = self.safe_symbol(marketId, market) + amount = self.safe_number(income, 'quantity') + id = self.safe_string(income, 'userId') + timestamp = self.parse8601(self.safe_string(income, 'intervalEndTimestamp')) + rate = self.safe_number(income, 'fundingRate') + return { + 'info': income, + 'symbol': symbol, + 'code': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': id, + 'amount': amount, + 'rate': rate, + } + + def nonce(self): + return self.milliseconds() - self.options['timeDifference'] + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + endpoint = '/' + path + url = self.urls['api'][api] + sortedParams = params if isinstance(params, list) else self.keysort(params) + if api == 'private': + self.check_required_credentials() + ts = str(self.nonce()) + recvWindow = self.safe_string_2(self.options, 'recvWindow', 'X-Window', '5000') + optionInstructions = self.safe_dict(self.options, 'instructions', {}) + optionPathInstructions = self.safe_dict(optionInstructions, path, {}) + instruction = self.safe_string(optionPathInstructions, method, '') + payload = '' + if (path == 'api/v1/orders') and (method == 'POST'): # for createOrders + payload = self.generate_batch_payload(sortedParams, ts, recvWindow, instruction) + else: + queryString = self.urlencode(sortedParams) + if len(queryString) > 0: + queryString += '&' + payload = 'instruction=' + instruction + '&' + queryString + 'timestamp=' + ts + '&window=' + recvWindow + secretBytes = self.base64_to_binary(self.secret) + seed = self.array_slice(secretBytes, 0, 32) + signature = self.eddsa(self.encode(payload), seed, 'ed25519') + headers = { + 'X-Timestamp': ts, + 'X-Window': recvWindow, + 'X-API-Key': self.apiKey, + 'X-Signature': signature, + 'X-Broker-Id': '1400', + } + if method != 'GET': + body = self.json(sortedParams) + headers['Content-Type'] = 'application/json' + if method == 'GET': + query = self.urlencode(sortedParams) + if len(query) != 0: + endpoint += '?' + query + url += endpoint + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def generate_batch_payload(self, params, ts, recvWindow, instruction): + payload = '' + for i in range(0, len(params)): + order = self.safe_dict(params, i, {}) + sortedOrder = self.keysort(order) + orderQuery = self.urlencode(sortedOrder) + payload += 'instruction=' + instruction + '&' + orderQuery + '&' + if i == (len(params) - 1): + payload += 'timestamp=' + ts + '&window=' + recvWindow + return payload + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None # fallback to default error handler + # + # {"code":"INVALID_ORDER","message":"Invalid order"} + # {"code":"INVALID_CLIENT_REQUEST","message":"Must specify both `triggerPrice` and `triggerQuantity` or neither"} + # + errorCode = self.safe_string(response, 'code') + message = self.safe_string(response, 'message') + if errorCode is not None: + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + raise ExchangeError(feedback) # unknown message + return None diff --git a/ccxt/base/__init__.py b/ccxt/base/__init__.py new file mode 100644 index 0000000..0e8a333 --- /dev/null +++ b/ccxt/base/__init__.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- + +# MIT License +# Copyright (c) 2017 Igor Kroitor +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from ccxt.base import errors +from ccxt.base import exchange +from ccxt.base import decimal_to_precision + +__all__ = exchange.__all__ + decimal_to_precision.__all__ + errors.__all__ # noqa: F405 diff --git a/ccxt/base/__pycache__/__init__.cpython-311.pyc b/ccxt/base/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..2cc3f76 Binary files /dev/null and b/ccxt/base/__pycache__/__init__.cpython-311.pyc differ diff --git a/ccxt/base/__pycache__/__init__.cpython-36.pyc b/ccxt/base/__pycache__/__init__.cpython-36.pyc new file mode 100644 index 0000000..ffaeeaa Binary files /dev/null and b/ccxt/base/__pycache__/__init__.cpython-36.pyc differ diff --git a/ccxt/base/__pycache__/decimal_to_precision.cpython-311.pyc b/ccxt/base/__pycache__/decimal_to_precision.cpython-311.pyc new file mode 100644 index 0000000..d5b2d07 Binary files /dev/null and b/ccxt/base/__pycache__/decimal_to_precision.cpython-311.pyc differ diff --git a/ccxt/base/__pycache__/decimal_to_precision.cpython-36.pyc b/ccxt/base/__pycache__/decimal_to_precision.cpython-36.pyc new file mode 100644 index 0000000..710ee07 Binary files /dev/null and b/ccxt/base/__pycache__/decimal_to_precision.cpython-36.pyc differ diff --git a/ccxt/base/__pycache__/errors.cpython-311.pyc b/ccxt/base/__pycache__/errors.cpython-311.pyc new file mode 100644 index 0000000..d61fd7c Binary files /dev/null and b/ccxt/base/__pycache__/errors.cpython-311.pyc differ diff --git a/ccxt/base/__pycache__/errors.cpython-36.pyc b/ccxt/base/__pycache__/errors.cpython-36.pyc new file mode 100644 index 0000000..e445ffe Binary files /dev/null and b/ccxt/base/__pycache__/errors.cpython-36.pyc differ diff --git a/ccxt/base/__pycache__/exchange.cpython-311.pyc b/ccxt/base/__pycache__/exchange.cpython-311.pyc new file mode 100644 index 0000000..542f1af Binary files /dev/null and b/ccxt/base/__pycache__/exchange.cpython-311.pyc differ diff --git a/ccxt/base/__pycache__/exchange.cpython-36.pyc b/ccxt/base/__pycache__/exchange.cpython-36.pyc new file mode 100644 index 0000000..a1ba4cd Binary files /dev/null and b/ccxt/base/__pycache__/exchange.cpython-36.pyc differ diff --git a/ccxt/base/__pycache__/precise.cpython-311.pyc b/ccxt/base/__pycache__/precise.cpython-311.pyc new file mode 100644 index 0000000..f5aacc6 Binary files /dev/null and b/ccxt/base/__pycache__/precise.cpython-311.pyc differ diff --git a/ccxt/base/__pycache__/precise.cpython-36.pyc b/ccxt/base/__pycache__/precise.cpython-36.pyc new file mode 100644 index 0000000..a7ef5e2 Binary files /dev/null and b/ccxt/base/__pycache__/precise.cpython-36.pyc differ diff --git a/ccxt/base/__pycache__/types.cpython-311.pyc b/ccxt/base/__pycache__/types.cpython-311.pyc new file mode 100644 index 0000000..1572613 Binary files /dev/null and b/ccxt/base/__pycache__/types.cpython-311.pyc differ diff --git a/ccxt/base/__pycache__/types.cpython-36.pyc b/ccxt/base/__pycache__/types.cpython-36.pyc new file mode 100644 index 0000000..96edca0 Binary files /dev/null and b/ccxt/base/__pycache__/types.cpython-36.pyc differ diff --git a/ccxt/base/decimal_to_precision.py b/ccxt/base/decimal_to_precision.py new file mode 100644 index 0000000..9f8acb0 --- /dev/null +++ b/ccxt/base/decimal_to_precision.py @@ -0,0 +1,178 @@ +import decimal +import numbers +import itertools +import re + +__all__ = [ + 'TRUNCATE', + 'ROUND', + 'ROUND_UP', + 'ROUND_DOWN', + 'DECIMAL_PLACES', + 'SIGNIFICANT_DIGITS', + 'TICK_SIZE', + 'NO_PADDING', + 'PAD_WITH_ZERO', + 'decimal_to_precision', +] + + +# rounding mode +TRUNCATE = 0 +ROUND = 1 +ROUND_UP = 2 +ROUND_DOWN = 3 + +# digits counting mode +DECIMAL_PLACES = 2 +SIGNIFICANT_DIGITS = 3 +TICK_SIZE = 4 + +# padding mode +NO_PADDING = 5 +PAD_WITH_ZERO = 6 + + +def decimal_to_precision(n, rounding_mode=ROUND, precision=None, counting_mode=DECIMAL_PLACES, padding_mode=NO_PADDING): + assert precision is not None, 'precision should not be None' + + if isinstance(precision, str): + precision = float(precision) + assert isinstance(precision, float) or isinstance(precision, decimal.Decimal) or isinstance(precision, numbers.Integral), 'precision has an invalid number' + + if counting_mode == TICK_SIZE: + assert precision > 0, 'negative or zero precision can not be used with TICK_SIZE precisionMode' + else: + assert isinstance(precision, numbers.Integral) + + assert rounding_mode in [TRUNCATE, ROUND] + assert counting_mode in [DECIMAL_PLACES, SIGNIFICANT_DIGITS, TICK_SIZE] + assert padding_mode in [NO_PADDING, PAD_WITH_ZERO] + # end of checks + + context = decimal.getcontext() + + if counting_mode != TICK_SIZE: + precision = min(context.prec - 2, precision) + + # all default except decimal.Underflow (raised when a number is rounded to zero) + context.traps[decimal.Underflow] = True + context.rounding = decimal.ROUND_HALF_UP # rounds 0.5 away from zero + + dec = decimal.Decimal(str(n)) + precision_dec = decimal.Decimal(str(precision)) + string = '{:f}'.format(dec) # convert to string using .format to avoid engineering notation + precise = None + + def power_of_10(x): + return decimal.Decimal('10') ** (-x) + + if precision < 0: + if counting_mode == TICK_SIZE: + raise ValueError('TICK_SIZE cant be used with negative numPrecisionDigits') + to_nearest = power_of_10(precision) + if rounding_mode == ROUND: + return "{:f}".format(to_nearest * decimal.Decimal(decimal_to_precision(dec / to_nearest, rounding_mode, 0, DECIMAL_PLACES, padding_mode))) + elif rounding_mode == TRUNCATE: + return decimal_to_precision(dec - dec % to_nearest, rounding_mode, 0, DECIMAL_PLACES, padding_mode) + + if counting_mode == TICK_SIZE: + # python modulo with negative numbers behaves different than js/php, so use abs first + missing = abs(dec) % precision_dec + if missing != 0: + if rounding_mode == ROUND: + if dec > 0: + if missing >= precision_dec / 2: + dec = dec - missing + precision_dec + else: + dec = dec - missing + else: + if missing >= precision_dec / 2: + dec = dec + missing - precision_dec + else: + dec = dec + missing + elif rounding_mode == TRUNCATE: + if dec < 0: + dec = dec + missing + else: + dec = dec - missing + parts = re.sub(r'0+$', '', '{:f}'.format(precision_dec)).split('.') + if len(parts) > 1: + new_precision = len(parts[1]) + else: + match = re.search(r'0+$', parts[0]) + if match is None: + new_precision = 0 + else: + new_precision = - len(match.group(0)) + return decimal_to_precision('{:f}'.format(dec), ROUND, new_precision, DECIMAL_PLACES, padding_mode) + + if rounding_mode == ROUND: + if counting_mode == DECIMAL_PLACES: + precise = '{:f}'.format(dec.quantize(power_of_10(precision))) # ROUND_HALF_EVEN is default context + elif counting_mode == SIGNIFICANT_DIGITS: + q = precision - dec.adjusted() - 1 + sigfig = power_of_10(q) + if q < 0: + string_to_precision = string[:precision] + # string_to_precision is '' when we have zero precision + below = sigfig * decimal.Decimal(string_to_precision if string_to_precision else '0') + above = below + sigfig + precise = '{:f}'.format(min((below, above), key=lambda x: abs(x - dec))) + else: + precise = '{:f}'.format(dec.quantize(sigfig)) + if precise.startswith('-0') and all(c in '0.' for c in precise[1:]): + precise = precise[1:] + + elif rounding_mode == TRUNCATE: + # Slice a string + if counting_mode == DECIMAL_PLACES: + before, after = string.split('.') if '.' in string else (string, '') + precise = before + '.' + after[:precision] + elif counting_mode == SIGNIFICANT_DIGITS: + if precision == 0: + return '0' + dot = string.index('.') if '.' in string else len(string) + start = dot - dec.adjusted() + end = start + precision + # need to clarify these conditionals + if dot >= end: + end -= 1 + if precision >= len(string.replace('.', '')): + precise = string + else: + precise = string[:end].ljust(dot, '0') + if precise.startswith('-0') and all(c in '0.' for c in precise[1:]): + precise = precise[1:] + precise = precise.rstrip('.') + + if padding_mode == NO_PADDING: + return precise.rstrip('0').rstrip('.') if '.' in precise else precise + elif padding_mode == PAD_WITH_ZERO: + if '.' in precise: + if counting_mode == DECIMAL_PLACES: + before, after = precise.split('.') + return before + '.' + after.ljust(precision, '0') + + elif counting_mode == SIGNIFICANT_DIGITS: + fsfg = len(list(itertools.takewhile(lambda x: x == '.' or x == '0', precise))) + if '.' in precise[fsfg:]: + precision += 1 + return precise[:fsfg] + precise[fsfg:].rstrip('0').ljust(precision, '0') + else: + if counting_mode == SIGNIFICANT_DIGITS: + if precision > len(precise): + return precise + '.' + (precision - len(precise)) * '0' + elif counting_mode == DECIMAL_PLACES: + if precision > 0: + return precise + '.' + precision * '0' + return precise + + +def number_to_string(x): + # avoids scientific notation for too large and too small numbers + if x is None: + return None + d = decimal.Decimal(str(x)) + formatted = '{:f}'.format(d) + return formatted.rstrip('0').rstrip('.') if '.' in formatted else formatted diff --git a/ccxt/base/errors.py b/ccxt/base/errors.py new file mode 100644 index 0000000..3664ea7 --- /dev/null +++ b/ccxt/base/errors.py @@ -0,0 +1,273 @@ +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code +# EDIT THE CORRESPONDENT .ts FILE INSTEAD + +error_hierarchy = { + 'BaseError': { + 'ExchangeError': { + 'AuthenticationError': { + 'PermissionDenied': { + 'AccountNotEnabled': {}, + }, + 'AccountSuspended': {}, + }, + 'ArgumentsRequired': {}, + 'BadRequest': { + 'BadSymbol': {}, + }, + 'OperationRejected': { + 'NoChange': { + 'MarginModeAlreadySet': {}, + }, + 'MarketClosed': {}, + 'ManualInteractionNeeded': {}, + 'RestrictedLocation': {}, + }, + 'InsufficientFunds': {}, + 'InvalidAddress': { + 'AddressPending': {}, + }, + 'InvalidOrder': { + 'OrderNotFound': {}, + 'OrderNotCached': {}, + 'OrderImmediatelyFillable': {}, + 'OrderNotFillable': {}, + 'DuplicateOrderId': {}, + 'ContractUnavailable': {}, + }, + 'NotSupported': {}, + 'InvalidProxySettings': {}, + 'ExchangeClosedByUser': {}, + }, + 'OperationFailed': { + 'NetworkError': { + 'DDoSProtection': {}, + 'RateLimitExceeded': {}, + 'ExchangeNotAvailable': { + 'OnMaintenance': {}, + }, + 'InvalidNonce': { + 'ChecksumError': {}, + }, + 'RequestTimeout': {}, + }, + 'BadResponse': { + 'NullResponse': {}, + }, + 'CancelPending': {}, + }, + 'UnsubscribeError': {}, + }, +} + + +class BaseError(Exception): + pass + + +class ExchangeError(BaseError): + pass + + +class AuthenticationError(ExchangeError): + pass + + +class PermissionDenied(AuthenticationError): + pass + + +class AccountNotEnabled(PermissionDenied): + pass + + +class AccountSuspended(AuthenticationError): + pass + + +class ArgumentsRequired(ExchangeError): + pass + + +class BadRequest(ExchangeError): + pass + + +class BadSymbol(BadRequest): + pass + + +class OperationRejected(ExchangeError): + pass + + +class NoChange(OperationRejected): + pass + + +class MarginModeAlreadySet(NoChange): + pass + + +class MarketClosed(OperationRejected): + pass + + +class ManualInteractionNeeded(OperationRejected): + pass + + +class RestrictedLocation(OperationRejected): + pass + + +class InsufficientFunds(ExchangeError): + pass + + +class InvalidAddress(ExchangeError): + pass + + +class AddressPending(InvalidAddress): + pass + + +class InvalidOrder(ExchangeError): + pass + + +class OrderNotFound(InvalidOrder): + pass + + +class OrderNotCached(InvalidOrder): + pass + + +class OrderImmediatelyFillable(InvalidOrder): + pass + + +class OrderNotFillable(InvalidOrder): + pass + + +class DuplicateOrderId(InvalidOrder): + pass + + +class ContractUnavailable(InvalidOrder): + pass + + +class NotSupported(ExchangeError): + pass + + +class InvalidProxySettings(ExchangeError): + pass + + +class ExchangeClosedByUser(ExchangeError): + pass + + +class OperationFailed(BaseError): + pass + + +class NetworkError(OperationFailed): + pass + + +class DDoSProtection(NetworkError): + pass + + +class RateLimitExceeded(NetworkError): + pass + + +class ExchangeNotAvailable(NetworkError): + pass + + +class OnMaintenance(ExchangeNotAvailable): + pass + + +class InvalidNonce(NetworkError): + pass + + +class ChecksumError(InvalidNonce): + pass + + +class RequestTimeout(NetworkError): + pass + + +class BadResponse(OperationFailed): + pass + + +class NullResponse(BadResponse): + pass + + +class CancelPending(OperationFailed): + pass + + +class UnsubscribeError(BaseError): + pass + + +__all__ = [ + 'error_hierarchy', + 'BaseError', + 'ExchangeError', + 'AuthenticationError', + 'PermissionDenied', + 'AccountNotEnabled', + 'AccountSuspended', + 'ArgumentsRequired', + 'BadRequest', + 'BadSymbol', + 'OperationRejected', + 'NoChange', + 'MarginModeAlreadySet', + 'MarketClosed', + 'ManualInteractionNeeded', + 'RestrictedLocation', + 'InsufficientFunds', + 'InvalidAddress', + 'AddressPending', + 'InvalidOrder', + 'OrderNotFound', + 'OrderNotCached', + 'OrderImmediatelyFillable', + 'OrderNotFillable', + 'DuplicateOrderId', + 'ContractUnavailable', + 'NotSupported', + 'InvalidProxySettings', + 'ExchangeClosedByUser', + 'OperationFailed', + 'NetworkError', + 'DDoSProtection', + 'RateLimitExceeded', + 'ExchangeNotAvailable', + 'OnMaintenance', + 'InvalidNonce', + 'ChecksumError', + 'RequestTimeout', + 'BadResponse', + 'NullResponse', + 'CancelPending', + 'UnsubscribeError' +] diff --git a/ccxt/base/exchange.py b/ccxt/base/exchange.py new file mode 100644 index 0000000..b051a49 --- /dev/null +++ b/ccxt/base/exchange.py @@ -0,0 +1,7447 @@ +# -*- coding: utf-8 -*- + +"""Base exchange class""" + +# ----------------------------------------------------------------------------- + +__version__ = '4.5.18' + +# ----------------------------------------------------------------------------- + +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import NetworkError +from ccxt.base.errors import NotSupported +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import DDoSProtection +from ccxt.base.errors import RequestTimeout +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.errors import InvalidAddress +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import NullResponse +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import OperationFailed +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadResponse +from ccxt.base.errors import InvalidProxySettings +from ccxt.base.errors import UnsubscribeError + +# ----------------------------------------------------------------------------- + +from ccxt.base.decimal_to_precision import decimal_to_precision +from ccxt.base.decimal_to_precision import DECIMAL_PLACES, TICK_SIZE, NO_PADDING, TRUNCATE, ROUND, ROUND_UP, ROUND_DOWN, SIGNIFICANT_DIGITS +from ccxt.base.decimal_to_precision import number_to_string +from ccxt.base.precise import Precise +from ccxt.base.types import ConstructorArgs, BalanceAccount, Currency, IndexType, OrderSide, OrderType, Trade, OrderRequest, Market, MarketType, Str, Num, Strings, CancellationRequest, Bool, Order + +# ----------------------------------------------------------------------------- + +# rsa jwt signing +from cryptography.hazmat import backends +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import padding, ed25519 +# from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature +from cryptography.hazmat.primitives.serialization import load_pem_private_key + +# ----------------------------------------------------------------------------- + +# ecdsa signing +from ccxt.static_dependencies import ecdsa +from ccxt.static_dependencies import keccak + +try: + import coincurve +except ImportError: + coincurve = None + +# eddsa signing +try: + import axolotl_curve25519 as eddsa +except ImportError: + eddsa = None + +# eth signing +from ccxt.static_dependencies.ethereum import abi +from ccxt.static_dependencies.ethereum import account +from ccxt.static_dependencies.msgpack import packb + +# starknet +from ccxt.static_dependencies.starknet.ccxt_utils import get_private_key_from_eth_signature +from ccxt.static_dependencies.starknet.hash.address import compute_address +from ccxt.static_dependencies.starknet.hash.selector import get_selector_from_name +from ccxt.static_dependencies.starknet.hash.utils import message_signature, private_to_stark_key +from ccxt.static_dependencies.starknet.utils.typed_data import TypedData as TypedDataDataclass +try: + import apexpro.zklink_sdk as zklink_sdk +except ImportError: + zklink_sdk = None + +# ----------------------------------------------------------------------------- + +__all__ = [ + 'Exchange', +] + +# ----------------------------------------------------------------------------- + +import types +import logging +import base64 +import binascii +import calendar +import collections +import datetime +from email.utils import parsedate +# import functools +import gzip +import hashlib +import hmac +import io + +# load orjson if available, otherwise default to json +orjson = None +try: + import orjson as orjson +except ImportError: + pass + +import json +import math +import random +from numbers import Number +import re +from requests import Session +from requests.utils import default_user_agent +from requests.exceptions import HTTPError, Timeout, TooManyRedirects, RequestException, ConnectionError as requestsConnectionError +# import socket +from ssl import SSLError +# import sys +import time +import uuid +import zlib +from decimal import Decimal +from time import mktime +from wsgiref.handlers import format_date_time +import urllib.parse as _urlencode +from typing import Any, List +from ccxt.base.types import Int + +# ----------------------------------------------------------------------------- + +class SafeJSONEncoder(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, Exception): + return {"name": obj.__class__.__name__} + try: + return super().default(obj) + except TypeError: + return f"TypeError: Object of type {type(obj).__name__} is not JSON serializable" + +class Exchange(object): + """Base exchange class""" + id = None + name = None + countries = None + version = None + certified = False # if certified by the CCXT dev team + pro = False # if it is integrated with CCXT Pro for WebSocket support + alias = False # whether this exchange is an alias to another exchange + # rate limiter settings + enableRateLimit = True + rateLimit = 2000 # milliseconds = seconds * 1000 + timeout = 10000 # milliseconds = seconds * 1000 + asyncio_loop = None + aiohttp_proxy = None + ssl_context = None + trust_env = False + aiohttp_trust_env = False + requests_trust_env = False + session = None # Session () by default + tcp_connector = None # aiohttp.TCPConnector + aiohttp_socks_connector = None + socks_proxy_sessions = None + verify = True # SSL verification + validateServerSsl = True + validateClientSsl = False + logger = None # logging.getLogger(__name__) by default + verbose = False + markets = None + symbols = None + codes = None + timeframes = {} + tokenBucket = None + + fees = { + 'trading': { + 'tierBased': None, + 'percentage': None, + 'taker': None, + 'maker': None, + }, + 'funding': { + 'tierBased': None, + 'percentage': None, + 'withdraw': {}, + 'deposit': {}, + }, + } + loaded_fees = { + 'trading': { + 'percentage': True, + }, + 'funding': { + 'withdraw': {}, + 'deposit': {}, + }, + } + ids = None + urls = None + api = None + parseJsonResponse = True + throttler = None + + # PROXY & USER-AGENTS (see "examples/proxy-usage" file for explanation) + proxy = None # for backwards compatibility + proxyUrl = None + proxy_url = None + proxyUrlCallback = None + proxy_url_callback = None + httpProxy = None + http_proxy = None + httpProxyCallback = None + http_proxy_callback = None + httpsProxy = None + https_proxy = None + httpsProxyCallback = None + https_proxy_callback = None + socksProxy = None + socks_proxy = None + socksProxyCallback = None + socks_proxy_callback = None + userAgent = None + user_agent = None + wsProxy = None + ws_proxy = None + wssProxy = None + wss_proxy = None + wsSocksProxy = None + ws_socks_proxy = None + # + userAgents = { + 'chrome': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36', + 'chrome39': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.71 Safari/537.36', + 'chrome100': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.75 Safari/537.36', + } + headers = None + returnResponseHeaders = False + origin = '*' # CORS origin + MAX_VALUE = float('inf') + # + proxies = None + + hostname = None # in case of inaccessibility of the "main" domain + apiKey = '' + secret = '' + password = '' + uid = '' + accountId = None + privateKey = '' # a "0x"-prefixed hexstring private key for a wallet + walletAddress = '' # the wallet address "0x"-prefixed hexstring + token = '' # reserved for HTTP auth in some cases + twofa = None + markets_by_id = None + currencies_by_id = None + + precision = None + exceptions = None + limits = { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': None, + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + } + + httpExceptions = { + '422': ExchangeError, + '418': DDoSProtection, + '429': RateLimitExceeded, + '404': ExchangeNotAvailable, + '409': ExchangeNotAvailable, + '410': ExchangeNotAvailable, + '451': ExchangeNotAvailable, + '500': ExchangeNotAvailable, + '501': ExchangeNotAvailable, + '502': ExchangeNotAvailable, + '520': ExchangeNotAvailable, + '521': ExchangeNotAvailable, + '522': ExchangeNotAvailable, + '525': ExchangeNotAvailable, + '526': ExchangeNotAvailable, + '400': ExchangeNotAvailable, + '403': ExchangeNotAvailable, + '405': ExchangeNotAvailable, + '503': ExchangeNotAvailable, + '530': ExchangeNotAvailable, + '408': RequestTimeout, + '504': RequestTimeout, + '401': AuthenticationError, + '407': AuthenticationError, + '511': AuthenticationError, + } + balance = None + liquidations = None + orderbooks = None + orders = None + triggerOrders = None + myLiquidations = None + myTrades = None + trades = None + transactions = None + ohlcvs = None + tickers = None + fundingRates = None + bidsasks = None + base_currencies = None + quote_currencies = None + currencies = {} + options = None # Python does not allow to define properties in run-time with setattr + isSandboxModeEnabled = False + accounts = None + positions = None + + status = None + + requiredCredentials = { + 'apiKey': True, + 'secret': True, + 'uid': False, + 'accountId': False, + 'login': False, + 'password': False, + 'twofa': False, # 2-factor authentication (one-time password key) + 'privateKey': False, # a "0x"-prefixed hexstring private key for a wallet + 'walletAddress': False, # the wallet address "0x"-prefixed hexstring + 'token': False, # reserved for HTTP auth in some cases + } + + # API method metainfo + has = {} + features = {} + precisionMode = DECIMAL_PLACES + paddingMode = NO_PADDING + minFundingAddressLength = 1 # used in check_address + substituteCommonCurrencyCodes = True + quoteJsonNumbers = True + number: Num = float # or str (a pointer to a class) + handleContentTypeApplicationZip = False + # whether fees should be summed by currency code + reduceFees = True + lastRestRequestTimestamp = 0 + lastRestPollTimestamp = 0 + restRequestQueue = None + restPollerLoopIsRunning = False + rateLimitTokens = 16 + rateLimitMaxTokens = 16 + rateLimitUpdateTime = 0 + enableLastHttpResponse = True + enableLastJsonResponse = False + enableLastResponseHeaders = True + last_http_response = None + last_json_response = None + last_response_headers = None + last_request_body = None + last_request_url = None + last_request_headers = None + + requiresEddsa = False + base58_encoder = None + base58_decoder = None + # no lower case l or upper case I, O + base58_alphabet = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' + + commonCurrencies = { + 'XBT': 'BTC', + 'BCC': 'BCH', + 'BCHSV': 'BSV', + } + synchronous = True + + def __init__(self, config: ConstructorArgs = {}): + self.aiohttp_trust_env = self.aiohttp_trust_env or self.trust_env + self.requests_trust_env = self.requests_trust_env or self.trust_env + + self.precision = dict() if self.precision is None else self.precision + self.limits = dict() if self.limits is None else self.limits + self.exceptions = dict() if self.exceptions is None else self.exceptions + self.headers = dict() if self.headers is None else self.headers + self.balance = dict() if self.balance is None else self.balance + self.orderbooks = dict() if self.orderbooks is None else self.orderbooks + self.fundingRates = dict() if self.fundingRates is None else self.fundingRates + self.tickers = dict() if self.tickers is None else self.tickers + self.bidsasks = dict() if self.bidsasks is None else self.bidsasks + self.trades = dict() if self.trades is None else self.trades + self.transactions = dict() if self.transactions is None else self.transactions + self.ohlcvs = dict() if self.ohlcvs is None else self.ohlcvs + self.liquidations = dict() if self.liquidations is None else self.liquidations + self.myLiquidations = dict() if self.myLiquidations is None else self.myLiquidations + self.currencies = dict() if self.currencies is None else self.currencies + self.options = self.get_default_options() if self.options is None else self.options # Python does not allow to define properties in run-time with setattr + self.decimal_to_precision = decimal_to_precision + self.number_to_string = number_to_string + + # version = '.'.join(map(str, sys.version_info[:3])) + # self.userAgent = { + # 'User-Agent': 'ccxt/' + __version__ + ' (+https://github.com/ccxt/ccxt) Python/' + version + # } + + self.origin = self.uuid() + self.userAgent = default_user_agent() + + settings = self.deep_extend(self.describe(), config) + + for key in settings: + if hasattr(self, key) and isinstance(getattr(self, key), dict): + setattr(self, key, self.deep_extend(getattr(self, key), settings[key])) + else: + setattr(self, key, settings[key]) + + self.after_construct() + + if self.safe_bool(config, 'sandbox') or self.safe_bool(config, 'testnet'): + self.set_sandbox_mode(True) + + # convert all properties from underscore notation foo_bar to camelcase notation fooBar + cls = type(self) + for name in dir(self): + if name[0] != '_' and name[-1] != '_' and '_' in name: + parts = name.split('_') + # fetch_ohlcv → fetchOHLCV (not fetchOhlcv!) + exceptions = {'ohlcv': 'OHLCV', 'le': 'LE', 'be': 'BE'} + camelcase = parts[0] + ''.join(exceptions.get(i, self.capitalize(i)) for i in parts[1:]) + attr = getattr(self, name) + if isinstance(attr, types.MethodType): + setattr(cls, camelcase, getattr(cls, name)) + else: + if hasattr(self, camelcase): + if attr is not None: + setattr(self, camelcase, attr) + else: + setattr(self, camelcase, attr) + + if not self.session and self.synchronous: + self.session = Session() + self.session.trust_env = self.requests_trust_env + self.logger = self.logger if self.logger else logging.getLogger(__name__) + + def __del__(self): + if self.session: + try: + self.session.close() + except Exception as e: + pass + + def __repr__(self): + return 'ccxt.' + ('async_support.' if self.asyncio_loop else '') + self.id + '()' + + def __str__(self): + return self.name + + def init_throttler(self, cost=None): + # stub in sync + pass + + def throttle(self, cost=None): + now = float(self.milliseconds()) + elapsed = now - self.lastRestRequestTimestamp + cost = 1 if cost is None else cost + sleep_time = self.rateLimit * cost + if elapsed < sleep_time: + delay = sleep_time - elapsed + time.sleep(delay / 1000.0) + + @staticmethod + def gzip_deflate(response, text): + encoding = response.info().get('Content-Encoding') + if encoding in ('gzip', 'x-gzip', 'deflate'): + if encoding == 'deflate': + return zlib.decompress(text, -zlib.MAX_WBITS) + else: + return gzip.GzipFile('', 'rb', 9, io.BytesIO(text)).read() + return text + + def prepare_request_headers(self, headers=None): + headers = headers or {} + if self.session: + headers.update(self.session.headers) + headers.update(self.headers) + userAgent = self.userAgent if self.userAgent is not None else self.user_agent + if userAgent: + if type(userAgent) is str: + headers.update({'User-Agent': userAgent}) + elif (type(userAgent) is dict) and ('User-Agent' in userAgent): + headers.update(userAgent) + headers.update({'Accept-Encoding': 'gzip, deflate'}) + return self.set_headers(headers) + + def log(self, *args): + print(*args) + + def on_rest_response(self, code, reason, url, method, response_headers, response_body, request_headers, request_body): + return response_body.strip() + + def on_json_response(self, response_body): + if self.quoteJsonNumbers and orjson is None: + return json.loads(response_body, parse_float=str, parse_int=str) + else: + if orjson: + return orjson.loads(response_body) + return json.loads(response_body) + + def fetch(self, url, method='GET', headers=None, body=None): + """Perform a HTTP request and return decoded JSON data""" + + # ##### PROXY & HEADERS ##### + request_headers = self.prepare_request_headers(headers) + # proxy-url + proxyUrl = self.check_proxy_url_settings(url, method, headers, body) + if proxyUrl is not None: + request_headers.update({'Origin': self.origin}) + url = proxyUrl + self.url_encoder_for_proxy_url(url) + # proxy agents + proxies = None # set default + httpProxy, httpsProxy, socksProxy = self.check_proxy_settings(url, method, headers, body) + if httpProxy: + proxies = {} + proxies['http'] = httpProxy + elif httpsProxy: + proxies = {} + proxies['https'] = httpsProxy + elif socksProxy: + proxies = {} + # https://stackoverflow.com/a/15661226/2377343 + proxies['http'] = socksProxy + proxies['https'] = socksProxy + proxyAgentSet = proxies is not None + self.check_conflicting_proxies(proxyAgentSet, proxyUrl) + # specifically for async-python, there is ".proxies" property maintained + if (self.proxies is not None): + if (proxyAgentSet or proxyUrl): + raise ExchangeError(self.id + ' you have conflicting proxy settings - use either .proxies or http(s)Proxy / socksProxy / proxyUrl') + proxies = self.proxies + # log + if self.verbose: + self.log("\nfetch Request:", self.id, method, url, "RequestHeaders:", request_headers, "RequestBody:", body) + self.logger.debug("%s %s, Request: %s %s", method, url, request_headers, body) + # end of proxies & headers + + request_body = body + if body: + body = body.encode() + + self.session.cookies.clear() + + http_response = None + http_status_code = None + http_status_text = None + json_response = None + try: + response = self.session.request( + method, + url, + data=body, + headers=request_headers, + timeout=(self.timeout / 1000), + proxies=proxies, + verify=self.verify and self.validateServerSsl + ) + # does not try to detect encoding + response.encoding = 'utf-8' + headers = response.headers + http_status_code = response.status_code + http_status_text = response.reason + http_response = self.on_rest_response(http_status_code, http_status_text, url, method, headers, response.text, request_headers, request_body) + json_response = self.parse_json(http_response) + # FIXME remove last_x_responses from subclasses + if self.enableLastHttpResponse: + self.last_http_response = http_response + if self.enableLastJsonResponse: + self.last_json_response = json_response + if self.enableLastResponseHeaders: + self.last_response_headers = headers + if self.verbose: + self.log("\nfetch Response:", self.id, method, url, http_status_code, "ResponseHeaders:", headers, "ResponseBody:", http_response) + self.logger.debug("%s %s, Response: %s %s %s", method, url, http_status_code, headers, http_response) + if json_response and not isinstance(json_response, list) and self.returnResponseHeaders: + json_response['responseHeaders'] = headers + response.raise_for_status() + + except Timeout as e: + details = ' '.join([self.id, method, url]) + raise RequestTimeout(details) from e + + except TooManyRedirects as e: + details = ' '.join([self.id, method, url]) + raise ExchangeError(details) from e + + except SSLError as e: + details = ' '.join([self.id, method, url]) + raise ExchangeError(details) from e + + except HTTPError as e: + details = ' '.join([self.id, method, url]) + skip_further_error_handling = self.handle_errors(http_status_code, http_status_text, url, method, headers, http_response, json_response, request_headers, request_body) + if not skip_further_error_handling: + self.handle_http_status_code(http_status_code, http_status_text, url, method, http_response) + raise ExchangeError(details) from e + + except requestsConnectionError as e: + error_string = str(e) + details = ' '.join([self.id, method, url]) + if 'Read timed out' in error_string: + raise RequestTimeout(details) from e + else: + raise NetworkError(details) from e + + except ConnectionResetError as e: + error_string = str(e) + details = ' '.join([self.id, method, url]) + raise NetworkError(details) from e + + except RequestException as e: # base exception class + error_string = str(e) + if ('Missing dependencies for SOCKS support' in error_string): + raise NotSupported(self.id + ' - to use SOCKS proxy with ccxt, you might need "pysocks" module that can be installed by "pip install pysocks"') + details = ' '.join([self.id, method, url]) + if any(x in error_string for x in ['ECONNRESET', 'Connection aborted.', 'Connection broken:']): + raise NetworkError(details) from e + else: + raise ExchangeError(details) from e + + self.handle_errors(http_status_code, http_status_text, url, method, headers, http_response, json_response, request_headers, request_body) + if json_response is not None: + return json_response + elif self.is_text_response(headers): + return http_response + else: + if isinstance(response.content, bytes): + return response.content.decode('utf8') + return response.content + + def parse_json(self, http_response): + try: + if Exchange.is_json_encoded_object(http_response): + return self.on_json_response(http_response) + except ValueError: # superclass of JsonDecodeError (python2) + pass + + def is_text_response(self, headers): + # https://github.com/ccxt/ccxt/issues/5302 + content_type = headers.get('Content-Type', '') + return content_type.startswith('application/json') or content_type.startswith('text/') + + @staticmethod + def key_exists(dictionary, key): + if hasattr(dictionary, '__getitem__') and not isinstance(dictionary, str): + if isinstance(dictionary, list) and type(key) is not int: + return False + try: + value = dictionary[key] + return value is not None and value != '' + except LookupError: + return False + return False + + @staticmethod + def safe_float(dictionary, key, default_value=None): + value = default_value + try: + if Exchange.key_exists(dictionary, key): + value = float(dictionary[key]) + except ValueError as e: + value = default_value + return value + + @staticmethod + def safe_string(dictionary, key, default_value=None): + return str(dictionary[key]) if Exchange.key_exists(dictionary, key) else default_value + + @staticmethod + def safe_string_lower(dictionary, key, default_value=None): + if Exchange.key_exists(dictionary, key): + return str(dictionary[key]).lower() + else: + return default_value.lower() if default_value is not None else default_value + + @staticmethod + def safe_string_upper(dictionary, key, default_value=None): + if Exchange.key_exists(dictionary, key): + return str(dictionary[key]).upper() + else: + return default_value.upper() if default_value is not None else default_value + + @staticmethod + def safe_integer(dictionary, key, default_value=None): + if not Exchange.key_exists(dictionary, key): + return default_value + value = dictionary[key] + try: + # needed to avoid breaking on "100.0" + # https://stackoverflow.com/questions/1094717/convert-a-string-to-integer-with-decimal-in-python#1094721 + return int(float(value)) + except ValueError: + return default_value + except TypeError: + return default_value + + @staticmethod + def safe_integer_product(dictionary, key, factor, default_value=None): + if not Exchange.key_exists(dictionary, key): + return default_value + value = dictionary[key] + if isinstance(value, Number): + return int(value * factor) + elif isinstance(value, str): + try: + return int(float(value) * factor) + except ValueError: + pass + return default_value + + @staticmethod + def safe_timestamp(dictionary, key, default_value=None): + return Exchange.safe_integer_product(dictionary, key, 1000, default_value) + + @staticmethod + def safe_value(dictionary, key, default_value=None): + return dictionary[key] if Exchange.key_exists(dictionary, key) else default_value + + # we're not using safe_floats with a list argument as we're trying to save some cycles here + # we're not using safe_float_3 either because those cases are too rare to deserve their own optimization + + @staticmethod + def safe_float_2(dictionary, key1, key2, default_value=None): + return Exchange.safe_either(Exchange.safe_float, dictionary, key1, key2, default_value) + + @staticmethod + def safe_string_2(dictionary, key1, key2, default_value=None): + return Exchange.safe_either(Exchange.safe_string, dictionary, key1, key2, default_value) + + @staticmethod + def safe_string_lower_2(dictionary, key1, key2, default_value=None): + return Exchange.safe_either(Exchange.safe_string_lower, dictionary, key1, key2, default_value) + + @staticmethod + def safe_string_upper_2(dictionary, key1, key2, default_value=None): + return Exchange.safe_either(Exchange.safe_string_upper, dictionary, key1, key2, default_value) + + @staticmethod + def safe_integer_2(dictionary, key1, key2, default_value=None): + return Exchange.safe_either(Exchange.safe_integer, dictionary, key1, key2, default_value) + + @staticmethod + def safe_integer_product_2(dictionary, key1, key2, factor, default_value=None): + value = Exchange.safe_integer_product(dictionary, key1, factor) + return value if value is not None else Exchange.safe_integer_product(dictionary, key2, factor, default_value) + + @staticmethod + def safe_timestamp_2(dictionary, key1, key2, default_value=None): + return Exchange.safe_integer_product_2(dictionary, key1, key2, 1000, default_value) + + @staticmethod + def safe_value_2(dictionary, key1, key2, default_value=None): + return Exchange.safe_either(Exchange.safe_value, dictionary, key1, key2, default_value) + + # safe_method_n methods family + + @staticmethod + def safe_float_n(dictionary, key_list, default_value=None): + value = Exchange.get_object_value_from_key_list(dictionary, key_list) + if value is None: + return default_value + try: + value = float(value) + except ValueError as e: + value = default_value + return value + + @staticmethod + def safe_string_n(dictionary, key_list, default_value=None): + value = Exchange.get_object_value_from_key_list(dictionary, key_list) + return str(value) if value is not None else default_value + + @staticmethod + def safe_string_lower_n(dictionary, key_list, default_value=None): + value = Exchange.get_object_value_from_key_list(dictionary, key_list) + if value is not None: + return str(value).lower() + elif default_value is None: + return default_value + else: + return default_value.lower() + + @staticmethod + def safe_string_upper_n(dictionary, key_list, default_value=None): + value = Exchange.get_object_value_from_key_list(dictionary, key_list) + if value is not None: + return str(value).upper() + elif default_value is None: + return default_value + else: + return default_value.upper() + + @staticmethod + def safe_integer_n(dictionary, key_list, default_value=None): + value = Exchange.get_object_value_from_key_list(dictionary, key_list) + if value is None: + return default_value + try: + # needed to avoid breaking on "100.0" + # https://stackoverflow.com/questions/1094717/convert-a-string-to-integer-with-decimal-in-python#1094721 + return int(float(value)) + except ValueError: + return default_value + except TypeError: + return default_value + + @staticmethod + def safe_integer_product_n(dictionary, key_list, factor, default_value=None): + if dictionary is None: + return default_value + value = Exchange.get_object_value_from_key_list(dictionary, key_list) + if value is None: + return default_value + if isinstance(value, Number): + return int(value * factor) + elif isinstance(value, str): + try: + return int(float(value) * factor) + except ValueError: + pass + return default_value + + @staticmethod + def safe_timestamp_n(dictionary, key_list, default_value=None): + return Exchange.safe_integer_product_n(dictionary, key_list, 1000, default_value) + + @staticmethod + def safe_value_n(dictionary, key_list, default_value=None): + if dictionary is None: + return default_value + value = Exchange.get_object_value_from_key_list(dictionary, key_list) + return value if value is not None else default_value + + @staticmethod + def get_object_value_from_key_list(dictionary_or_list, key_list): + isDataArray = isinstance(dictionary_or_list, list) + isDataDict = isinstance(dictionary_or_list, dict) + for key in key_list: + if isDataDict: + if key in dictionary_or_list and dictionary_or_list[key] is not None and dictionary_or_list[key] != '': + return dictionary_or_list[key] + elif isDataArray and not isinstance(key, str): + if (key < len(dictionary_or_list)) and (dictionary_or_list[key] is not None) and (dictionary_or_list[key] != ''): + return dictionary_or_list[key] + return None + + @staticmethod + def safe_either(method, dictionary, key1, key2, default_value=None): + """A helper-wrapper for the safe_value_2() family.""" + value = method(dictionary, key1) + return value if value is not None else method(dictionary, key2, default_value) + + @staticmethod + def truncate(num, precision=0): + """Deprecated, use decimal_to_precision instead""" + if precision > 0: + decimal_precision = math.pow(10, precision) + return math.trunc(num * decimal_precision) / decimal_precision + return int(Exchange.truncate_to_string(num, precision)) + + @staticmethod + def truncate_to_string(num, precision=0): + """Deprecated, todo: remove references from subclasses""" + if precision > 0: + parts = ('{0:.%df}' % precision).format(Decimal(num)).split('.') + decimal_digits = parts[1][:precision].rstrip('0') + decimal_digits = decimal_digits if len(decimal_digits) else '0' + return parts[0] + '.' + decimal_digits + return ('%d' % num) + + @staticmethod + def uuid22(length=22): + return format(random.getrandbits(length * 4), 'x') + + @staticmethod + def uuid16(length=16): + return format(random.getrandbits(length * 4), 'x') + + @staticmethod + def uuid(): + return str(uuid.uuid4()) + + @staticmethod + def uuidv1(): + return str(uuid.uuid1()).replace('-', '') + + @staticmethod + def capitalize(string): # first character only, rest characters unchanged + # the native pythonic .capitalize() method lowercases all other characters + # which is an unwanted behaviour, therefore we use this custom implementation + # check it yourself: print('foobar'.capitalize(), 'fooBar'.capitalize()) + if len(string) > 1: + return "%s%s" % (string[0].upper(), string[1:]) + return string.upper() + + @staticmethod + def strip(string): + return string.strip() + + @staticmethod + def keysort(dictionary): + return collections.OrderedDict(sorted(dictionary.items(), key=lambda t: t[0])) + + @staticmethod + def sort(array): + return sorted(array) + + @staticmethod + def extend(*args): + if args is not None: + result = None + if type(args[0]) is collections.OrderedDict: + result = collections.OrderedDict() + else: + result = {} + for arg in args: + result.update(arg) + return result + return {} + + @staticmethod + def deep_extend(*args): + result = None + for arg in args: + if isinstance(arg, dict): + if not isinstance(result, dict): + result = {} + for key in arg: + result[key] = Exchange.deep_extend(result[key] if key in result else None, arg[key]) + else: + result = arg + return result + + @staticmethod + def filter_by(array, key, value=None): + array = Exchange.to_array(array) + return list(filter(lambda x: x[key] == value, array)) + + @staticmethod + def filterBy(array, key, value=None): + return Exchange.filter_by(array, key, value) + + @staticmethod + def group_by(array, key): + result = {} + array = Exchange.to_array(array) + array = [entry for entry in array if (key in entry) and (entry[key] is not None)] + for entry in array: + if entry[key] not in result: + result[entry[key]] = [] + result[entry[key]].append(entry) + return result + + @staticmethod + def groupBy(array, key): + return Exchange.group_by(array, key) + + + @staticmethod + def index_by_safe(array, key): + return Exchange.index_by(array, key) # wrapper for go + + @staticmethod + def index_by(array, key): + result = {} + if type(array) is dict: + array = Exchange.keysort(array).values() + is_int_key = isinstance(key, int) + for element in array: + if ((is_int_key and (key < len(element))) or (key in element)) and (element[key] is not None): + k = element[key] + result[k] = element + return result + + @staticmethod + def sort_by(array, key, descending=False, default=0): + return sorted(array, key=lambda k: k[key] if k[key] is not None else default, reverse=descending) + + @staticmethod + def sort_by_2(array, key1, key2, descending=False): + return sorted(array, key=lambda k: (k[key1] if k[key1] is not None else "", k[key2] if k[key2] is not None else ""), reverse=descending) + + @staticmethod + def array_concat(a, b): + return a + b + + @staticmethod + def in_array(needle, haystack): + return needle in haystack + + @staticmethod + def is_empty(object): + return not object + + @staticmethod + def extract_params(string): + return re.findall(r'{([\w-]+)}', string) + + @staticmethod + def implode_params(string, params): + if isinstance(params, dict): + for key in params: + if not isinstance(params[key], list): + string = string.replace('{' + key + '}', str(params[key])) + return string + + @staticmethod + def urlencode(params={}, doseq=False, sort=False): + newParams = params.copy() + for key, value in params.items(): + if isinstance(value, bool): + newParams[key] = 'true' if value else 'false' + return _urlencode.urlencode(newParams, doseq, quote_via=_urlencode.quote) + + @staticmethod + def urlencode_with_array_repeat(params={}): + return re.sub(r'%5B\d*%5D', '', Exchange.urlencode(params, True)) + + @staticmethod + def urlencode_nested(params): + result = {} + + def _encode_params(params, p_key=None): + encode_params = {} + if isinstance(params, dict): + for key in params: + encode_key = '{}[{}]'.format(p_key, key) + encode_params[encode_key] = params[key] + elif isinstance(params, (list, tuple)): + for offset, value in enumerate(params): + encode_key = '{}[{}]'.format(p_key, offset) + encode_params[encode_key] = value + else: + result[p_key] = params + for key in encode_params: + value = encode_params[key] + _encode_params(value, key) + if isinstance(params, dict): + for key in params: + _encode_params(params[key], key) + return _urlencode.urlencode(result, quote_via=_urlencode.quote) + + @staticmethod + def rawencode(params={}, sort=False): + return _urlencode.unquote(Exchange.urlencode(params)) + + @staticmethod + def encode_uri_component(uri, safe="~()*!.'"): + return _urlencode.quote(uri, safe=safe) + + @staticmethod + def omit(d, *args): + if isinstance(d, dict): + result = d.copy() + for arg in args: + if type(arg) is list: + for key in arg: + if key in result: + del result[key] + else: + if arg in result: + del result[arg] + return result + return d + + @staticmethod + def unique(array): + return list(set(array)) + + @staticmethod + def pluck(array, key): + return [ + element[key] + for element in array + if (key in element) and (element[key] is not None) + ] + + @staticmethod + def sum(*args): + return sum([arg for arg in args if isinstance(arg, (float, int))]) + + @staticmethod + def ordered(array): + return collections.OrderedDict(array) + + @staticmethod + def aggregate(bidasks): + ordered = Exchange.ordered({}) + for [price, volume, *_] in bidasks: + if volume > 0: + ordered[price] = (ordered[price] if price in ordered else 0) + volume + result = [] + items = list(ordered.items()) + for price, volume in items: + result.append([price, volume]) + return result + + @staticmethod + def sec(): + return Exchange.seconds() + + @staticmethod + def msec(): + return Exchange.milliseconds() + + @staticmethod + def usec(): + return Exchange.microseconds() + + @staticmethod + def seconds(): + return int(time.time()) + + @staticmethod + def milliseconds(): + return int(time.time() * 1000) + + @staticmethod + def microseconds(): + return int(time.time() * 1000000) + + @staticmethod + def iso8601(timestamp=None): + if timestamp is None: + return timestamp + if not isinstance(timestamp, int): + return None + if int(timestamp) < 0: + return None + + try: + utc = datetime.datetime.fromtimestamp(timestamp // 1000, datetime.timezone.utc) + return utc.strftime('%Y-%m-%dT%H:%M:%S.%f')[:-6] + "{:03d}".format(int(timestamp) % 1000) + 'Z' + except (TypeError, OverflowError, OSError): + return None + + @staticmethod + def rfc2616(self, timestamp=None): + if timestamp is None: + ts = datetime.datetime.now() + else: + ts = timestamp + stamp = mktime(ts.timetuple()) + return format_date_time(stamp) + + @staticmethod + def dmy(timestamp, infix='-'): + utc_datetime = datetime.datetime.fromtimestamp(int(round(timestamp / 1000)), datetime.timezone.utc) + return utc_datetime.strftime('%m' + infix + '%d' + infix + '%Y') + + @staticmethod + def ymd(timestamp, infix='-', fullYear=True): + year_format = '%Y' if fullYear else '%y' + utc_datetime = datetime.datetime.fromtimestamp(int(round(timestamp / 1000)), datetime.timezone.utc) + return utc_datetime.strftime(year_format + infix + '%m' + infix + '%d') + + @staticmethod + def yymmdd(timestamp, infix=''): + return Exchange.ymd(timestamp, infix, False) + + @staticmethod + def yyyymmdd(timestamp, infix='-'): + return Exchange.ymd(timestamp, infix, True) + + @staticmethod + def ymdhms(timestamp, infix=' '): + utc_datetime = datetime.datetime.fromtimestamp(int(round(timestamp / 1000)), datetime.timezone.utc) + return utc_datetime.strftime('%Y-%m-%d' + infix + '%H:%M:%S') + + @staticmethod + def parse_date(timestamp=None): + if timestamp is None: + return timestamp + if not isinstance(timestamp, str): + return None + if 'GMT' in timestamp: + try: + string = ''.join([str(value).zfill(2) for value in parsedate(timestamp)[:6]]) + '.000Z' + dt = datetime.datetime.strptime(string, "%Y%m%d%H%M%S.%fZ") + return calendar.timegm(dt.utctimetuple()) * 1000 + except (TypeError, OverflowError, OSError): + return None + else: + return Exchange.parse8601(timestamp) + + @staticmethod + def parse8601(timestamp=None): + if timestamp is None: + return timestamp + yyyy = '([0-9]{4})-?' + mm = '([0-9]{2})-?' + dd = '([0-9]{2})(?:T|[\\s])?' + h = '([0-9]{2}):?' + m = '([0-9]{2}):?' + s = '([0-9]{2})' + ms = '(\\.[0-9]{1,3})?' + tz = '(?:(\\+|\\-)([0-9]{2})\\:?([0-9]{2})|Z)?' + regex = r'' + yyyy + mm + dd + h + m + s + ms + tz + try: + match = re.search(regex, timestamp, re.IGNORECASE) + if match is None: + return None + yyyy, mm, dd, h, m, s, ms, sign, hours, minutes = match.groups() + ms = ms or '.000' + ms = (ms + '00')[0:4] + msint = int(ms[1:]) + sign = sign or '' + sign = int(sign + '1') * -1 + hours = int(hours or 0) * sign + minutes = int(minutes or 0) * sign + offset = datetime.timedelta(hours=hours, minutes=minutes) + string = yyyy + mm + dd + h + m + s + ms + 'Z' + dt = datetime.datetime.strptime(string, "%Y%m%d%H%M%S.%fZ") + dt = dt + offset + return calendar.timegm(dt.utctimetuple()) * 1000 + msint + except (TypeError, OverflowError, OSError, ValueError): + return None + + @staticmethod + def hash(request, algorithm='md5', digest='hex'): + if algorithm == 'keccak': + binary = bytes(keccak.SHA3(request)) + else: + h = hashlib.new(algorithm, request) + binary = h.digest() + if digest == 'base64': + return Exchange.binary_to_base64(binary) + elif digest == 'hex': + return Exchange.binary_to_base16(binary) + return binary + + @staticmethod + def hmac(request, secret, algorithm=hashlib.sha256, digest='hex'): + h = hmac.new(secret, request, algorithm) + binary = h.digest() + if digest == 'hex': + return Exchange.binary_to_base16(binary) + elif digest == 'base64': + return Exchange.binary_to_base64(binary) + return binary + + @staticmethod + def binary_concat(*args): + result = bytes() + for arg in args: + result = result + arg + return result + + @staticmethod + def binary_concat_array(array): + result = bytes() + for element in array: + result = result + element + return result + + @staticmethod + def urlencode_base64(s): + return Exchange.decode(base64.urlsafe_b64encode(s)).replace('=', '') + + @staticmethod + def binary_to_base64(s): + return Exchange.decode(base64.standard_b64encode(s)) + + @staticmethod + def base64_to_binary(s): + return base64.standard_b64decode(s) + + @staticmethod + def string_to_base64(s): + return Exchange.binary_to_base64(Exchange.encode(s)) + + @staticmethod + def base64_to_string(s): + return Exchange.decode(base64.b64decode(s)) + + @staticmethod + def jwt(request, secret, algorithm='sha256', is_rsa=False, opts={}): + algos = { + 'sha256': hashlib.sha256, + 'sha384': hashlib.sha384, + 'sha512': hashlib.sha512, + } + alg = ('RS' if is_rsa else 'HS') + algorithm[3:] + if 'alg' in opts and opts['alg'] is not None: + alg = opts['alg'] + header_opts = { + 'alg': alg, + 'typ': 'JWT', + } + if 'kid' in opts and opts['kid'] is not None: + header_opts['kid'] = opts['kid'] + if 'nonce' in opts and opts['nonce'] is not None: + header_opts['nonce'] = opts['nonce'] + header = Exchange.encode(Exchange.json(header_opts)) + encoded_header = Exchange.urlencode_base64(header) + encoded_data = Exchange.urlencode_base64(Exchange.encode(Exchange.json(request))) + token = encoded_header + '.' + encoded_data + algoType = alg[0:2] + if is_rsa or algoType == 'RS': + signature = Exchange.base64_to_binary(Exchange.rsa(token, Exchange.decode(secret), algorithm)) + elif algoType == 'ES': + rawSignature = Exchange.ecdsa(token, secret, 'p256', algorithm) + signature = Exchange.base16_to_binary(rawSignature['r'].rjust(64, "0") + rawSignature['s'].rjust(64, "0")) + elif algoType == 'Ed': + signature = Exchange.eddsa(token.encode('utf-8'), secret, 'ed25519', True) + # here the signature is already a urlencoded base64-encoded string + return token + '.' + signature + else: + signature = Exchange.hmac(Exchange.encode(token), secret, algos[algorithm], 'binary') + return token + '.' + Exchange.urlencode_base64(signature) + + @staticmethod + def rsa(request, secret, alg='sha256'): + algorithms = { + "sha256": hashes.SHA256(), + "sha384": hashes.SHA384(), + "sha512": hashes.SHA512(), + } + algorithm = algorithms[alg] + priv_key = load_pem_private_key(Exchange.encode(secret), None, backends.default_backend()) + return Exchange.binary_to_base64(priv_key.sign(Exchange.encode(request), padding.PKCS1v15(), algorithm)) + + @staticmethod + def eth_abi_encode(types, args): + return abi.encode(types, args) + + @staticmethod + def eth_encode_structured_data(domain, messageTypes, message): + encodedData = account.messages.encode_typed_data(domain, messageTypes, message) + return Exchange.binary_concat(b"\x19\x01", encodedData.header, encodedData.body) + + @staticmethod + def retrieve_stark_account (signature, accountClassHash, accountProxyClassHash): + privateKey = get_private_key_from_eth_signature(signature) + publicKey = private_to_stark_key(privateKey) + calldata = [ + int(accountClassHash, 16), + get_selector_from_name("initialize"), + 2, + publicKey, + 0, + ] + + address = compute_address( + class_hash=int(accountProxyClassHash, 16), + constructor_calldata=calldata, + salt=publicKey, + ) + return { + 'privateKey': privateKey, + 'publicKey': publicKey, + 'address': hex(address) + } + + @staticmethod + def starknet_encode_structured_data (domain, messageTypes, messageData, address): + types = list(messageTypes.keys()) + if len(types) > 1: + raise NotSupported('starknetEncodeStructuredData only support single type') + + request = { + 'domain': domain, + 'primaryType': types[0], + 'types': Exchange.extend({ + 'StarkNetDomain': [ + {'name': "name", 'type': "felt"}, + {'name': "chainId", 'type': "felt"}, + {'name': "version", 'type': "felt"}, + ], + }, messageTypes), + 'message': messageData, + } + typedDataClass = TypedDataDataclass.from_dict(request) + msgHash = typedDataClass.message_hash(int(address, 16)) + return msgHash + + @staticmethod + def starknet_sign (msg_hash, pri): + # // TODO: unify to ecdsa + r, s = message_signature(msg_hash, pri) + return Exchange.json([hex(r), hex(s)]) + + @staticmethod + def packb(o): + return packb(o) + + @staticmethod + def int_to_base16(num): + return "%0.2X" % num + + @staticmethod + def random_bytes(length): + return format(random.getrandbits(length * 8), 'x') + + @staticmethod + def ecdsa(request, secret, algorithm='p256', hash=None, fixed_length=False): + """ + ECDSA signing with support for multiple algorithms and coincurve for SECP256K1. + Args: + request: The message to sign + secret: The private key (hex string or PEM format) + algorithm: The elliptic curve algorithm ('p192', 'p224', 'p256', 'p384', 'p521', 'secp256k1') + hash: The hash function to use (defaults to algorithm-specific hash) + fixed_length: Whether to ensure fixed-length signatures (for deterministic signing) + Note: coincurve produces non-deterministic signatures + Returns: + dict: {'r': r_value, 's': s_value, 'v': v_value} + Note: + If coincurve is not available or fails for SECP256K1, the method automatically + falls back to the standard ecdsa implementation. + """ + # your welcome - frosty00 + algorithms = { + 'p192': [ecdsa.NIST192p, 'sha256'], + 'p224': [ecdsa.NIST224p, 'sha256'], + 'p256': [ecdsa.NIST256p, 'sha256'], + 'p384': [ecdsa.NIST384p, 'sha384'], + 'p521': [ecdsa.NIST521p, 'sha512'], + 'secp256k1': [ecdsa.SECP256k1, 'sha256'], + } + if algorithm not in algorithms: + raise ArgumentsRequired(algorithm + ' is not a supported algorithm') + # Use coincurve for SECP256K1 if available + if algorithm == 'secp256k1' and coincurve is not None: + try: + return Exchange._ecdsa_secp256k1_coincurve(request, secret, hash, fixed_length) + except Exception: + # If coincurve fails, fall back to ecdsa implementation + pass + # Fall back to original ecdsa implementation for other algorithms or when deterministic signing is needed + curve_info = algorithms[algorithm] + hash_function = getattr(hashlib, curve_info[1]) + encoded_request = Exchange.encode(request) + if hash is not None: + digest = Exchange.hash(encoded_request, hash, 'binary') + else: + digest = base64.b16decode(encoded_request, casefold=True) + if isinstance(secret, str): + secret = Exchange.encode(secret) + if secret.find(b'-----BEGIN EC PRIVATE KEY-----') > -1: + key = ecdsa.SigningKey.from_pem(secret, hash_function) + else: + key = ecdsa.SigningKey.from_string(base64.b16decode(secret, + casefold=True), curve=curve_info[0]) + r_binary, s_binary, v = key.sign_digest_deterministic(digest, hashfunc=hash_function, + sigencode=ecdsa.util.sigencode_strings_canonize) + r_int, s_int = ecdsa.util.sigdecode_strings((r_binary, s_binary), key.privkey.order) + counter = 0 + minimum_size = (1 << (8 * 31)) - 1 + half_order = key.privkey.order / 2 + while fixed_length and (r_int > half_order or r_int <= minimum_size or s_int <= minimum_size): + r_binary, s_binary, v = key.sign_digest_deterministic(digest, hashfunc=hash_function, + sigencode=ecdsa.util.sigencode_strings_canonize, + extra_entropy=Exchange.number_to_le(counter, 32)) + r_int, s_int = ecdsa.util.sigdecode_strings((r_binary, s_binary), key.privkey.order) + counter += 1 + r, s = Exchange.decode(base64.b16encode(r_binary)).lower(), Exchange.decode(base64.b16encode(s_binary)).lower() + return { + 'r': r, + 's': s, + 'v': v, + } + + + @staticmethod + def _ecdsa_secp256k1_coincurve(request, secret, hash=None, fixed_length=False): + """ + Use coincurve library for SECP256K1 ECDSA signing. + This method provides faster SECP256K1 signing using the coincurve library, + which is a Python binding to libsecp256k1. This implementation produces + deterministic signatures (RFC 6979) using coincurve's sign_recoverable method. + Args: + request: The message to sign + secret: The private key (hex string or PEM format) + hash: The hash function to use + fixed_length: Not used in coincurve implementation (signatures are always fixed length) + Returns: + dict: {'r': r_value, 's': s_value, 'v': v_value} + Raises: + ArgumentsRequired: If coincurve library is not available + """ + encoded_request = Exchange.encode(request) + if hash is not None: + digest = Exchange.hash(encoded_request, hash, 'binary') + else: + digest = base64.b16decode(encoded_request, casefold=True) + if isinstance(secret, str): + secret = Exchange.encode(secret) + # Handle PEM format + if secret.find(b'-----BEGIN EC PRIVATE KEY-----') > -1: + secret = base64.b16decode(secret.replace(b'-----BEGIN EC PRIVATE KEY-----', b'').replace(b'-----END EC PRIVATE KEY-----', b'').replace(b'\n', b'').replace(b'\r', b''), casefold=True) + else: + # Assume hex format + secret = base64.b16decode(secret, casefold=True) + # Create coincurve PrivateKey + private_key = coincurve.PrivateKey(secret) + # Sign the digest using sign_recoverable which produces deterministic signatures (RFC 6979) + # The signature format is: 32 bytes r + 32 bytes s + 1 byte recovery_id (v) + signature = private_key.sign_recoverable(digest, hasher=None) + # Extract r, s, and v from the recoverable signature (65 bytes total) + r_binary = signature[:32] + s_binary = signature[32:64] + v = signature[64] + # Convert to hex strings + r = Exchange.decode(base64.b16encode(r_binary)).lower() + s = Exchange.decode(base64.b16encode(s_binary)).lower() + return { + 'r': r, + 's': s, + 'v': v, + } + + def binary_to_urlencoded_base64(data: bytes) -> str: + encoded = base64.urlsafe_b64encode(data).decode("utf-8") + return encoded.rstrip("=") + + @staticmethod + def eddsa(request, secret, curve='ed25519', url_encode=False): + if isinstance(secret, str): + secret = Exchange.encode(secret) + private_key = ed25519.Ed25519PrivateKey.from_private_bytes(secret) if len(secret) == 32 else load_pem_private_key(secret, None) + signature = private_key.sign(request) + if url_encode: + return Exchange.binary_to_urlencoded_base64(signature) + + return Exchange.binary_to_base64(signature) + + @staticmethod + def axolotl(request, secret, curve='ed25519'): + random = b'\x00' * 64 + request = base64.b16decode(request, casefold=True) + secret = base64.b16decode(secret, casefold=True) + signature = eddsa.calculateSignature(random, secret, request) + return Exchange.binary_to_base58(signature) + + @staticmethod + def json(data, params=None): + if orjson: + return orjson.dumps(data).decode('utf-8') + return json.dumps(data, separators=(',', ':'), cls=SafeJSONEncoder) + + @staticmethod + def is_json_encoded_object(input): + return (isinstance(input, str) and + (len(input) >= 2) and + ((input[0] == '{') or (input[0] == '['))) + + @staticmethod + def encode(string): + return string.encode('utf-8') + + @staticmethod + def decode(string): + return string.decode('utf-8') + + @staticmethod + def to_array(value): + return list(value.values()) if type(value) is dict else value + + @staticmethod + def check_required_version(required_version, error=True): + result = True + [major1, minor1, patch1] = required_version.split('.') + [major2, minor2, patch2] = __version__.split('.') + int_major1 = int(major1) + int_minor1 = int(minor1) + int_patch1 = int(patch1) + int_major2 = int(major2) + int_minor2 = int(minor2) + int_patch2 = int(patch2) + if int_major1 > int_major2: + result = False + if int_major1 == int_major2: + if int_minor1 > int_minor2: + result = False + elif int_minor1 == int_minor2 and int_patch1 > int_patch2: + result = False + if not result: + if error: + raise NotSupported('Your current version of CCXT is ' + __version__ + ', a newer version ' + required_version + ' is required, please, upgrade your version of CCXT') + else: + return error + return result + + def precision_from_string(self, str): + # support string formats like '1e-4' + if 'e' in str or 'E' in str: + numStr = re.sub(r'\d\.?\d*[eE]', '', str) + return int(numStr) * -1 + # support integer formats (without dot) like '1', '10' etc [Note: bug in decimalToPrecision, so this should not be used atm] + # if not ('.' in str): + # return len(str) * -1 + # default strings like '0.0001' + parts = re.sub(r'0+$', '', str).split('.') + return len(parts[1]) if len(parts) > 1 else 0 + + def map_to_safe_map(self, dictionary): + return dictionary # wrapper for go + + def safe_map_to_map(self, dictionary): + return dictionary # wrapper for go + + def load_markets(self, reload=False, params={}): + """ + Loads and prepares the markets for trading. + + Args: + reload (bool): If True, the markets will be reloaded from the exchange. + params (dict): Additional exchange-specific parameters for the request. + + Returns: + dict: A dictionary of markets. + + Raises: + Exception: If the markets cannot be loaded or prepared. + + Notes: + It ensures that the markets are only loaded once, even if called multiple times. + If the markets are already loaded and `reload` is False or not provided, it returns the existing markets. + If a reload is in progress, it waits for completion before returning. + If an error occurs during loading or preparation, an exception is raised. + """ + if not reload: + if self.markets: + if not self.markets_by_id: + return self.set_markets(self.markets) + return self.markets + currencies = None + if self.has['fetchCurrencies'] is True: + currencies = self.fetch_currencies() + self.options['cachedCurrencies'] = currencies + markets = self.fetch_markets(params) + if 'cachedCurrencies' in self.options: + del self.options['cachedCurrencies'] + return self.set_markets(markets, currencies) + + def fetch_markets(self, params={}): + # markets are returned as a list + # currencies are returned as a dict + # this is for historical reasons + # and may be changed for consistency later + return self.to_array(self.markets) + + def fetch_currencies(self, params={}): + # markets are returned as a list + # currencies are returned as a dict + # this is for historical reasons + # and may be changed for consistency later + return self.currencies + + def fetch_fees(self): + trading = {} + funding = {} + if self.has['fetchTradingFees']: + trading = self.fetch_trading_fees() + if self.has['fetchFundingFees']: + funding = self.fetch_funding_fees() + return { + 'trading': trading, + 'funding': funding, + } + + @staticmethod + def parse_timeframe(timeframe): + amount = int(timeframe[0:-1]) + unit = timeframe[-1] + if 'y' == unit: + scale = 60 * 60 * 24 * 365 + elif 'M' == unit: + scale = 60 * 60 * 24 * 30 + elif 'w' == unit: + scale = 60 * 60 * 24 * 7 + elif 'd' == unit: + scale = 60 * 60 * 24 + elif 'h' == unit: + scale = 60 * 60 + elif 'm' == unit: + scale = 60 + elif 's' == unit: + scale = 1 + else: + raise NotSupported('timeframe unit {} is not supported'.format(unit)) + return amount * scale + + @staticmethod + def round_timeframe(timeframe, timestamp, direction=ROUND_DOWN): + ms = Exchange.parse_timeframe(timeframe) * 1000 + # Get offset based on timeframe in milliseconds + offset = timestamp % ms + return timestamp - offset + (ms if direction == ROUND_UP else 0) + + def vwap(self, baseVolume, quoteVolume): + return (quoteVolume / baseVolume) if (quoteVolume is not None) and (baseVolume is not None) and (baseVolume > 0) else None + + def check_required_dependencies(self): + if self.requiresEddsa and eddsa is None: + raise NotSupported(self.id + ' Eddsa functionality requires python-axolotl-curve25519, install with `pip install python-axolotl-curve25519==0.4.1.post2`: https://github.com/tgalal/python-axolotl-curve25519') + + def privateKeyToAddress(self, privateKey): + private_key_bytes = base64.b16decode(Exchange.encode(privateKey), True) + public_key_bytes = ecdsa.SigningKey.from_string(private_key_bytes, curve=ecdsa.SECP256k1).verifying_key.to_string() + public_key_hash = keccak.SHA3(public_key_bytes) + return '0x' + Exchange.decode(base64.b16encode(public_key_hash))[-40:].lower() + + @staticmethod + def remove0x_prefix(value): + if value[:2] == '0x': + return value[2:] + return value + + @staticmethod + def totp(key): + def hex_to_dec(n): + return int(n, base=16) + + def base32_to_bytes(n): + missing_padding = len(n) % 8 + padding = 8 - missing_padding if missing_padding > 0 else 0 + padded = n.upper() + ('=' * padding) + return base64.b32decode(padded) # throws an error if the key is invalid + + epoch = int(time.time()) // 30 + hmac_res = Exchange.hmac(epoch.to_bytes(8, 'big'), base32_to_bytes(key.replace(' ', '')), hashlib.sha1, 'hex') + offset = hex_to_dec(hmac_res[-1]) * 2 + otp = str(hex_to_dec(hmac_res[offset: offset + 8]) & 0x7fffffff) + return otp[-6:] + + @staticmethod + def number_to_le(n, size): + return int(n).to_bytes(size, 'little') + + @staticmethod + def number_to_be(n, size): + return int(n).to_bytes(size, 'big') + + @staticmethod + def base16_to_binary(s): + return base64.b16decode(s, True) + + @staticmethod + def binary_to_base16(s): + return Exchange.decode(base64.b16encode(s)).lower() + + def sleep(self, milliseconds): + return time.sleep(milliseconds / 1000) + + @staticmethod + def base58_to_binary(s): + """encodes a base58 string to as a big endian integer""" + if Exchange.base58_decoder is None: + Exchange.base58_decoder = {} + Exchange.base58_encoder = {} + for i, c in enumerate(Exchange.base58_alphabet): + Exchange.base58_decoder[c] = i + Exchange.base58_encoder[i] = c + result = 0 + for i in range(len(s)): + result *= 58 + result += Exchange.base58_decoder[s[i]] + return result.to_bytes((result.bit_length() + 7) // 8, 'big') + + @staticmethod + def binary_to_base58(b): + if Exchange.base58_encoder is None: + Exchange.base58_decoder = {} + Exchange.base58_encoder = {} + for i, c in enumerate(Exchange.base58_alphabet): + Exchange.base58_decoder[c] = i + Exchange.base58_encoder[i] = c + result = 0 + # undo decimal_to_bytes + for byte in b: + result *= 0x100 + result += byte + string = [] + while result > 0: + result, next_character = divmod(result, 58) + string.append(Exchange.base58_encoder[next_character]) + string.reverse() + return ''.join(string) + + def parse_number(self, value, default=None): + if value is None: + return default + else: + try: + return self.number(value) + except Exception: + return default + + def omit_zero(self, string_number): + try: + if string_number is None or string_number == '': + return None + if float(string_number) == 0: + return None + return string_number + except Exception: + return string_number + + def check_order_arguments(self, market, type, side, amount, price, params): + if price is None: + if type == 'limit': + raise ArgumentsRequired(self.id + ' create_order() requires a price argument for a limit order') + if amount <= 0: + raise InvalidOrder(self.id + ' create_order() amount should be above 0') + + def handle_http_status_code(self, code, reason, url, method, body): + codeAsString = str(code) + if codeAsString in self.httpExceptions: + ErrorClass = self.httpExceptions[codeAsString] + raise ErrorClass(self.id + ' ' + method + ' ' + url + ' ' + codeAsString + ' ' + reason + ' ' + body) + + @staticmethod + def crc32(string, signed=False): + unsigned = binascii.crc32(string.encode('utf8')) + if signed and (unsigned >= 0x80000000): + return unsigned - 0x100000000 + else: + return unsigned + + def clone(self, obj): + return obj if isinstance(obj, list) else self.extend(obj) + + # def delete_key_from_dictionary(self, dictionary, key): + # newDictionary = self.clone(dictionary) + # del newDictionary[key] + # return newDictionary + + # def set_object_property(obj, prop, value): + # obj[prop] = value + + def convert_to_big_int(self, value): + return int(value) if isinstance(value, str) else value + + def string_to_chars_array(self, value): + return list(value) + + def value_is_defined(self, value): + return value is not None + + def array_slice(self, array, first, second=None): + return array[first:second] if second else array[first:] + + def get_property(self, obj, property, defaultValue=None): + return getattr(obj, property) if hasattr(obj, property) else defaultValue + + def set_property(self, obj, property, value): + setattr(obj, property, value) + + def un_camel_case(self, str): + return re.sub('(?!^)([A-Z]+)', r'_\1', str).lower() + + def fix_stringified_json_members(self, content): + # when stringified json has members with their values also stringified, like: + # '{"code":0, "data":{"order":{"orderId":1742968678528512345,"symbol":"BTC-USDT", "takeProfit":"{\"type\":\"TAKE_PROFIT\",\"stopPrice\":43320.1}","reduceOnly":false}}}' + # we can fix with below manipulations + # @ts-ignore + modifiedContent = content.replace('\\', '') + modifiedContent = modifiedContent.replace('"{', '{') + modifiedContent = modifiedContent.replace('}"', '}') + return modifiedContent + + def extend_exchange_options(self, newOptions): + self.options = self.extend(self.options, newOptions) + + def create_safe_dictionary(self): + return {} + + def convert_to_safe_dictionary(self, dictionary): + return dictionary + + def rand_number(self, size): + return int(''.join([str(random.randint(0, 9)) for _ in range(size)])) + + def binary_length(self, binary): + return len(binary) + + def get_zk_contract_signature_obj(self, seeds: str, params={}): + if zklink_sdk is None: + raise Exception('zklink_sdk is not installed, please do pip3 install apexomni-arm or apexomni-x86-mac or apexomni-x86-windows-linux') + + slotId = self.safe_string(params, 'slotId') + nonceInt = int(self.remove0x_prefix(self.hash(self.encode(slotId), 'sha256', 'hex')), 16) + + maxUint64 = 18446744073709551615 + maxUint32 = 4294967295 + + slotId = (nonceInt % maxUint64) / maxUint32 + nonce = nonceInt % maxUint32 + accountId = int(self.safe_string(params, 'accountId'), 10) % maxUint32 + + priceStr = (Decimal(self.safe_string(params, 'price')) * Decimal(10) ** Decimal('18')).quantize(Decimal(0), rounding='ROUND_DOWN') + sizeStr = (Decimal(self.safe_string(params, 'size')) * Decimal(10) ** Decimal('18')).quantize(Decimal(0), rounding='ROUND_DOWN') + + takerFeeRateStr = (Decimal(self.safe_string(params, 'takerFeeRate')) * Decimal(10000)).quantize(Decimal(0), rounding='ROUND_UP') + makerFeeRateStr = (Decimal(self.safe_string(params, 'makerFeeRate')) * Decimal(10000)).quantize(Decimal(0), rounding='ROUND_UP') + + builder = zklink_sdk.ContractBuilder( + int(accountId), int(0), int(slotId), int(nonce), int(self.safe_number(params, 'pairId')), + sizeStr.__str__(), priceStr.__str__(), self.safe_string(params, 'direction') == "BUY", + int(takerFeeRateStr), int(makerFeeRateStr), False) + + + tx = zklink_sdk.Contract(builder) + seedsByte = bytes.fromhex(seeds.removeprefix('0x')) + signerSeed = zklink_sdk.ZkLinkSigner().new_from_seed(seedsByte) + auth_data = signerSeed.sign_musig(tx.get_bytes()) + signature = auth_data.signature + return signature + + def get_zk_transfer_signature_obj(self, seeds: str, params={}): + if zklink_sdk is None: + raise Exception('zklink_sdk is not installed, please do pip3 install apexomni-arm or apexomni-x86-mac or apexomni-x86-windows-linux') + + nonce = self.safe_string(params, 'nonce', '0') + if self.safe_bool(params, 'isContract'): + formattedUint32 = '4294967295' + formattedNonce = int(self.remove0x_prefix(self.hash(self.encode(nonce), 'sha256', 'hex')), 16) + nonce = Precise.string_mod(str(formattedNonce), formattedUint32) + + tx_builder = zklink_sdk.TransferBuilder( + int(self.safe_number(params, 'zkAccountId', 0)), + self.safe_string(params, 'receiverAddress'), + int(self.safe_number(params, 'subAccountId', 0)), + int(self.safe_number(params, 'receiverSubAccountId', 0)), + int(self.safe_number(params, 'tokenId', 0)), + self.safe_string(params, 'amount', '0'), + self.safe_string(params, 'fee', '0'), + self.parse_to_int(nonce), + int(self.safe_number(params, 'timestampSeconds', 0)) + ) + + tx = zklink_sdk.Transfer(tx_builder) + seedsByte = bytes.fromhex(seeds.removeprefix('0x')) + signerSeed = zklink_sdk.ZkLinkSigner().new_from_seed(seedsByte) + auth_data = signerSeed.sign_musig(tx.get_bytes()) + signature = auth_data.signature + return signature + + def is_binary_message(self, message): + return isinstance(message, bytes) or isinstance(message, bytearray) + + # ######################################################################## + # ######################################################################## + # ######################################################################## + # ######################################################################## + # ######## ######## ######## + # ######## ######## ######## + # ######## ######## ######## + # ######## ######## ######## + # ######## ######################## ######################## + # ######## ######################## ######################## + # ######## ######################## ######################## + # ######## ######################## ######################## + # ######## ######## ######## + # ######## ######## ######## + # ######## ######## ######## + # ######## ######## ######## + # ######################################################################## + # ######################################################################## + # ######################################################################## + # ######################################################################## + # ######## ######## ######## ######## + # ######## ######## ######## ######## + # ######## ######## ######## ######## + # ######## ######## ######## ######## + # ################ ######################## ################ + # ################ ######################## ################ + # ################ ######################## ################ + # ################ ######################## ################ + # ######## ######## ################ ################ + # ######## ######## ################ ################ + # ######## ######## ################ ################ + # ######## ######## ################ ################ + # ######################################################################## + # ######################################################################## + # ######################################################################## + # ######################################################################## + + # METHODS BELOW THIS LINE ARE TRANSPILED FROM TYPESCRIPT + + def describe(self) -> Any: + return { + 'id': None, + 'name': None, + 'countries': None, + 'enableRateLimit': True, + 'rateLimit': 2000, # milliseconds = seconds * 1000 + 'timeout': self.timeout, # milliseconds = seconds * 1000 + 'certified': False, # if certified by the CCXT dev team + 'pro': False, # if it is integrated with CCXT Pro for WebSocket support + 'alias': False, # whether self exchange is an alias to another exchange + 'dex': False, + 'has': { + 'publicAPI': True, + 'privateAPI': True, + 'CORS': None, + 'sandbox': None, + 'spot': None, + 'margin': None, + 'swap': None, + 'future': None, + 'option': None, + 'addMargin': None, + 'borrowCrossMargin': None, + 'borrowIsolatedMargin': None, + 'borrowMargin': None, + 'cancelAllOrders': None, + 'cancelAllOrdersWs': None, + 'cancelOrder': True, + 'cancelOrderWithClientOrderId': None, + 'cancelOrderWs': None, + 'cancelOrders': None, + 'cancelOrdersWithClientOrderId': None, + 'cancelOrdersWs': None, + 'closeAllPositions': None, + 'closePosition': None, + 'createDepositAddress': None, + 'createLimitBuyOrder': None, + 'createLimitBuyOrderWs': None, + 'createLimitOrder': True, + 'createLimitOrderWs': None, + 'createLimitSellOrder': None, + 'createLimitSellOrderWs': None, + 'createMarketBuyOrder': None, + 'createMarketBuyOrderWs': None, + 'createMarketBuyOrderWithCost': None, + 'createMarketBuyOrderWithCostWs': None, + 'createMarketOrder': True, + 'createMarketOrderWs': True, + 'createMarketOrderWithCost': None, + 'createMarketOrderWithCostWs': None, + 'createMarketSellOrder': None, + 'createMarketSellOrderWs': None, + 'createMarketSellOrderWithCost': None, + 'createMarketSellOrderWithCostWs': None, + 'createOrder': True, + 'createOrderWs': None, + 'createOrders': None, + 'createOrderWithTakeProfitAndStopLoss': None, + 'createOrderWithTakeProfitAndStopLossWs': None, + 'createPostOnlyOrder': None, + 'createPostOnlyOrderWs': None, + 'createReduceOnlyOrder': None, + 'createReduceOnlyOrderWs': None, + 'createStopLimitOrder': None, + 'createStopLimitOrderWs': None, + 'createStopLossOrder': None, + 'createStopLossOrderWs': None, + 'createStopMarketOrder': None, + 'createStopMarketOrderWs': None, + 'createStopOrder': None, + 'createStopOrderWs': None, + 'createTakeProfitOrder': None, + 'createTakeProfitOrderWs': None, + 'createTrailingAmountOrder': None, + 'createTrailingAmountOrderWs': None, + 'createTrailingPercentOrder': None, + 'createTrailingPercentOrderWs': None, + 'createTriggerOrder': None, + 'createTriggerOrderWs': None, + 'deposit': None, + 'editOrder': 'emulated', + 'editOrderWithClientOrderId': None, + 'editOrders': None, + 'editOrderWs': None, + 'fetchAccounts': None, + 'fetchBalance': True, + 'fetchBalanceWs': None, + 'fetchBidsAsks': None, + 'fetchBorrowInterest': None, + 'fetchBorrowRate': None, + 'fetchBorrowRateHistories': None, + 'fetchBorrowRateHistory': None, + 'fetchBorrowRates': None, + 'fetchBorrowRatesPerSymbol': None, + 'fetchCanceledAndClosedOrders': None, + 'fetchCanceledOrders': None, + 'fetchClosedOrder': None, + 'fetchClosedOrders': None, + 'fetchClosedOrdersWs': None, + 'fetchConvertCurrencies': None, + 'fetchConvertQuote': None, + 'fetchConvertTrade': None, + 'fetchConvertTradeHistory': None, + 'fetchCrossBorrowRate': None, + 'fetchCrossBorrowRates': None, + 'fetchCurrencies': 'emulated', + 'fetchCurrenciesWs': 'emulated', + 'fetchDeposit': None, + 'fetchDepositAddress': None, + 'fetchDepositAddresses': None, + 'fetchDepositAddressesByNetwork': None, + 'fetchDeposits': None, + 'fetchDepositsWithdrawals': None, + 'fetchDepositsWs': None, + 'fetchDepositWithdrawFee': None, + 'fetchDepositWithdrawFees': None, + 'fetchFundingHistory': None, + 'fetchFundingRate': None, + 'fetchFundingRateHistory': None, + 'fetchFundingInterval': None, + 'fetchFundingIntervals': None, + 'fetchFundingRates': None, + 'fetchGreeks': None, + 'fetchIndexOHLCV': None, + 'fetchIsolatedBorrowRate': None, + 'fetchIsolatedBorrowRates': None, + 'fetchMarginAdjustmentHistory': None, + 'fetchIsolatedPositions': None, + 'fetchL2OrderBook': True, + 'fetchL3OrderBook': None, + 'fetchLastPrices': None, + 'fetchLedger': None, + 'fetchLedgerEntry': None, + 'fetchLeverage': None, + 'fetchLeverages': None, + 'fetchLeverageTiers': None, + 'fetchLiquidations': None, + 'fetchLongShortRatio': None, + 'fetchLongShortRatioHistory': None, + 'fetchMarginMode': None, + 'fetchMarginModes': None, + 'fetchMarketLeverageTiers': None, + 'fetchMarkets': True, + 'fetchMarketsWs': None, + 'fetchMarkOHLCV': None, + 'fetchMyLiquidations': None, + 'fetchMySettlementHistory': None, + 'fetchMyTrades': None, + 'fetchMyTradesWs': None, + 'fetchOHLCV': None, + 'fetchOHLCVWs': None, + 'fetchOpenInterest': None, + 'fetchOpenInterests': None, + 'fetchOpenInterestHistory': None, + 'fetchOpenOrder': None, + 'fetchOpenOrders': None, + 'fetchOpenOrdersWs': None, + 'fetchOption': None, + 'fetchOptionChain': None, + 'fetchOrder': None, + 'fetchOrderWithClientOrderId': None, + 'fetchOrderBook': True, + 'fetchOrderBooks': None, + 'fetchOrderBookWs': None, + 'fetchOrders': None, + 'fetchOrdersByStatus': None, + 'fetchOrdersWs': None, + 'fetchOrderTrades': None, + 'fetchOrderWs': None, + 'fetchPosition': None, + 'fetchPositionHistory': None, + 'fetchPositionsHistory': None, + 'fetchPositionWs': None, + 'fetchPositionMode': None, + 'fetchPositions': None, + 'fetchPositionsWs': None, + 'fetchPositionsForSymbol': None, + 'fetchPositionsForSymbolWs': None, + 'fetchPositionsRisk': None, + 'fetchPremiumIndexOHLCV': None, + 'fetchSettlementHistory': None, + 'fetchStatus': None, + 'fetchTicker': True, + 'fetchTickerWs': None, + 'fetchTickers': None, + 'fetchMarkPrices': None, + 'fetchTickersWs': None, + 'fetchTime': None, + 'fetchTrades': True, + 'fetchTradesWs': None, + 'fetchTradingFee': None, + 'fetchTradingFees': None, + 'fetchTradingFeesWs': None, + 'fetchTradingLimits': None, + 'fetchTransactionFee': None, + 'fetchTransactionFees': None, + 'fetchTransactions': None, + 'fetchTransfer': None, + 'fetchTransfers': None, + 'fetchUnderlyingAssets': None, + 'fetchVolatilityHistory': None, + 'fetchWithdrawAddresses': None, + 'fetchWithdrawal': None, + 'fetchWithdrawals': None, + 'fetchWithdrawalsWs': None, + 'fetchWithdrawalWhitelist': None, + 'reduceMargin': None, + 'repayCrossMargin': None, + 'repayIsolatedMargin': None, + 'setLeverage': None, + 'setMargin': None, + 'setMarginMode': None, + 'setPositionMode': None, + 'signIn': None, + 'transfer': None, + 'watchBalance': None, + 'watchMyTrades': None, + 'watchOHLCV': None, + 'watchOHLCVForSymbols': None, + 'watchOrderBook': None, + 'watchBidsAsks': None, + 'watchOrderBookForSymbols': None, + 'watchOrders': None, + 'watchOrdersForSymbols': None, + 'watchPosition': None, + 'watchPositions': None, + 'watchStatus': None, + 'watchTicker': None, + 'watchTickers': None, + 'watchTrades': None, + 'watchTradesForSymbols': None, + 'watchLiquidations': None, + 'watchLiquidationsForSymbols': None, + 'watchMyLiquidations': None, + 'unWatchOrders': None, + 'unWatchTrades': None, + 'unWatchTradesForSymbols': None, + 'unWatchOHLCVForSymbols': None, + 'unWatchOrderBookForSymbols': None, + 'unWatchPositions': None, + 'unWatchOrderBook': None, + 'unWatchTickers': None, + 'unWatchMyTrades': None, + 'unWatchTicker': None, + 'unWatchOHLCV': None, + 'watchMyLiquidationsForSymbols': None, + 'withdraw': None, + 'ws': None, + }, + 'urls': { + 'logo': None, + 'api': None, + 'www': None, + 'doc': None, + 'fees': None, + }, + 'api': None, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + 'uid': False, + 'accountId': False, + 'login': False, + 'password': False, + 'twofa': False, # 2-factor authentication(one-time password key) + 'privateKey': False, # a "0x"-prefixed hexstring private key for a wallet + 'walletAddress': False, # the wallet address "0x"-prefixed hexstring + 'token': False, # reserved for HTTP auth in some cases + }, + 'markets': None, # to be filled manually or by fetchMarkets + 'currencies': {}, # to be filled manually or by fetchMarkets + 'timeframes': None, # redefine if the exchange has.fetchOHLCV + 'fees': { + 'trading': { + 'tierBased': None, + 'percentage': None, + 'taker': None, + 'maker': None, + }, + 'funding': { + 'tierBased': None, + 'percentage': None, + 'withdraw': {}, + 'deposit': {}, + }, + }, + 'status': { + 'status': 'ok', + 'updated': None, + 'eta': None, + 'url': None, + }, + 'exceptions': None, + 'httpExceptions': { + '422': ExchangeError, + '418': DDoSProtection, + '429': RateLimitExceeded, + '404': ExchangeNotAvailable, + '409': ExchangeNotAvailable, + '410': ExchangeNotAvailable, + '451': ExchangeNotAvailable, + '500': ExchangeNotAvailable, + '501': ExchangeNotAvailable, + '502': ExchangeNotAvailable, + '520': ExchangeNotAvailable, + '521': ExchangeNotAvailable, + '522': ExchangeNotAvailable, + '525': ExchangeNotAvailable, + '526': ExchangeNotAvailable, + '400': ExchangeNotAvailable, + '403': ExchangeNotAvailable, + '405': ExchangeNotAvailable, + '503': ExchangeNotAvailable, + '530': ExchangeNotAvailable, + '408': RequestTimeout, + '504': RequestTimeout, + '401': AuthenticationError, + '407': AuthenticationError, + '511': AuthenticationError, + }, + 'commonCurrencies': { + 'XBT': 'BTC', + 'BCHSV': 'BSV', + }, + 'precisionMode': TICK_SIZE, + 'paddingMode': NO_PADDING, + 'limits': { + 'leverage': {'min': None, 'max': None}, + 'amount': {'min': None, 'max': None}, + 'price': {'min': None, 'max': None}, + 'cost': {'min': None, 'max': None}, + }, + } + + def safe_bool_n(self, dictionaryOrList, keys: List[IndexType], defaultValue: bool = None): + """ + @ignore + safely extract boolean value from dictionary or list + :returns bool | None: + """ + value = self.safe_value_n(dictionaryOrList, keys, defaultValue) + if isinstance(value, bool): + return value + return defaultValue + + def safe_bool_2(self, dictionary, key1: IndexType, key2: IndexType, defaultValue: bool = None): + """ + @ignore + safely extract boolean value from dictionary or list + :returns bool | None: + """ + return self.safe_bool_n(dictionary, [key1, key2], defaultValue) + + def safe_bool(self, dictionary, key: IndexType, defaultValue: bool = None): + """ + @ignore + safely extract boolean value from dictionary or list + :returns bool | None: + """ + return self.safe_bool_n(dictionary, [key], defaultValue) + + def safe_dict_n(self, dictionaryOrList, keys: List[IndexType], defaultValue: dict = None): + """ + @ignore + safely extract a dictionary from dictionary or list + :returns dict | None: + """ + value = self.safe_value_n(dictionaryOrList, keys, defaultValue) + if value is None: + return defaultValue + if (isinstance(value, dict)): + if not isinstance(value, list): + return value + return defaultValue + + def safe_dict(self, dictionary, key: IndexType, defaultValue: dict = None): + """ + @ignore + safely extract a dictionary from dictionary or list + :returns dict | None: + """ + return self.safe_dict_n(dictionary, [key], defaultValue) + + def safe_dict_2(self, dictionary, key1: IndexType, key2: str, defaultValue: dict = None): + """ + @ignore + safely extract a dictionary from dictionary or list + :returns dict | None: + """ + return self.safe_dict_n(dictionary, [key1, key2], defaultValue) + + def safe_list_n(self, dictionaryOrList, keys: List[IndexType], defaultValue: List[Any] = None): + """ + @ignore + safely extract an Array from dictionary or list + :returns Array | None: + """ + value = self.safe_value_n(dictionaryOrList, keys, defaultValue) + if value is None: + return defaultValue + if isinstance(value, list): + return value + return defaultValue + + def safe_list_2(self, dictionaryOrList, key1: IndexType, key2: str, defaultValue: List[Any] = None): + """ + @ignore + safely extract an Array from dictionary or list + :returns Array | None: + """ + return self.safe_list_n(dictionaryOrList, [key1, key2], defaultValue) + + def safe_list(self, dictionaryOrList, key: IndexType, defaultValue: List[Any] = None): + """ + @ignore + safely extract an Array from dictionary or list + :returns Array | None: + """ + return self.safe_list_n(dictionaryOrList, [key], defaultValue) + + def handle_deltas(self, orderbook, deltas): + for i in range(0, len(deltas)): + self.handle_delta(orderbook, deltas[i]) + + def handle_delta(self, bookside, delta): + raise NotSupported(self.id + ' handleDelta not supported yet') + + def handle_deltas_with_keys(self, bookSide: Any, deltas, priceKey: IndexType = 0, amountKey: IndexType = 1, countOrIdKey: IndexType = 2): + for i in range(0, len(deltas)): + bidAsk = self.parse_bid_ask(deltas[i], priceKey, amountKey, countOrIdKey) + bookSide.storeArray(bidAsk) + + def get_cache_index(self, orderbook, deltas): + # return the first index of the cache that can be applied to the orderbook or -1 if not possible. + return -1 + + def arrays_concat(self, arraysOfArrays: List[Any]): + result = [] + for i in range(0, len(arraysOfArrays)): + result = self.array_concat(result, arraysOfArrays[i]) + return result + + def find_timeframe(self, timeframe, timeframes=None): + if timeframes is None: + timeframes = self.timeframes + keys = list(timeframes.keys()) + for i in range(0, len(keys)): + key = keys[i] + if timeframes[key] == timeframe: + return key + return None + + def check_proxy_url_settings(self, url: Str = None, method: Str = None, headers=None, body=None): + usedProxies = [] + proxyUrl = None + if self.proxyUrl is not None: + usedProxies.append('proxyUrl') + proxyUrl = self.proxyUrl + if self.proxy_url is not None: + usedProxies.append('proxy_url') + proxyUrl = self.proxy_url + if self.proxyUrlCallback is not None: + usedProxies.append('proxyUrlCallback') + proxyUrl = self.proxyUrlCallback(url, method, headers, body) + if self.proxy_url_callback is not None: + usedProxies.append('proxy_url_callback') + proxyUrl = self.proxy_url_callback(url, method, headers, body) + # backwards-compatibility + if self.proxy is not None: + usedProxies.append('proxy') + if callable(self.proxy): + proxyUrl = self.proxy(url, method, headers, body) + else: + proxyUrl = self.proxy + length = len(usedProxies) + if length > 1: + joinedProxyNames = ','.join(usedProxies) + raise InvalidProxySettings(self.id + ' you have multiple conflicting proxy settings(' + joinedProxyNames + '), please use only one from : proxyUrl, proxy_url, proxyUrlCallback, proxy_url_callback') + return proxyUrl + + def url_encoder_for_proxy_url(self, targetUrl: str): + # to be overriden + includesQuery = targetUrl.find('?') >= 0 + finalUrl = self.encode_uri_component(targetUrl) if includesQuery else targetUrl + return finalUrl + + def check_proxy_settings(self, url: Str = None, method: Str = None, headers=None, body=None): + usedProxies = [] + httpProxy = None + httpsProxy = None + socksProxy = None + # httpProxy + isHttpProxyDefined = self.value_is_defined(self.httpProxy) + isHttp_proxy_defined = self.value_is_defined(self.http_proxy) + if isHttpProxyDefined or isHttp_proxy_defined: + usedProxies.append('httpProxy') + httpProxy = self.httpProxy if isHttpProxyDefined else self.http_proxy + ishttpProxyCallbackDefined = self.value_is_defined(self.httpProxyCallback) + ishttp_proxy_callback_defined = self.value_is_defined(self.http_proxy_callback) + if ishttpProxyCallbackDefined or ishttp_proxy_callback_defined: + usedProxies.append('httpProxyCallback') + httpProxy = self.httpProxyCallback(url, method, headers, body) if ishttpProxyCallbackDefined else self.http_proxy_callback(url, method, headers, body) + # httpsProxy + isHttpsProxyDefined = self.value_is_defined(self.httpsProxy) + isHttps_proxy_defined = self.value_is_defined(self.https_proxy) + if isHttpsProxyDefined or isHttps_proxy_defined: + usedProxies.append('httpsProxy') + httpsProxy = self.httpsProxy if isHttpsProxyDefined else self.https_proxy + ishttpsProxyCallbackDefined = self.value_is_defined(self.httpsProxyCallback) + ishttps_proxy_callback_defined = self.value_is_defined(self.https_proxy_callback) + if ishttpsProxyCallbackDefined or ishttps_proxy_callback_defined: + usedProxies.append('httpsProxyCallback') + httpsProxy = self.httpsProxyCallback(url, method, headers, body) if ishttpsProxyCallbackDefined else self.https_proxy_callback(url, method, headers, body) + # socksProxy + isSocksProxyDefined = self.value_is_defined(self.socksProxy) + isSocks_proxy_defined = self.value_is_defined(self.socks_proxy) + if isSocksProxyDefined or isSocks_proxy_defined: + usedProxies.append('socksProxy') + socksProxy = self.socksProxy if isSocksProxyDefined else self.socks_proxy + issocksProxyCallbackDefined = self.value_is_defined(self.socksProxyCallback) + issocks_proxy_callback_defined = self.value_is_defined(self.socks_proxy_callback) + if issocksProxyCallbackDefined or issocks_proxy_callback_defined: + usedProxies.append('socksProxyCallback') + socksProxy = self.socksProxyCallback(url, method, headers, body) if issocksProxyCallbackDefined else self.socks_proxy_callback(url, method, headers, body) + # check + length = len(usedProxies) + if length > 1: + joinedProxyNames = ','.join(usedProxies) + raise InvalidProxySettings(self.id + ' you have multiple conflicting proxy settings(' + joinedProxyNames + '), please use only one from: httpProxy, httpsProxy, httpProxyCallback, httpsProxyCallback, socksProxy, socksProxyCallback') + return [httpProxy, httpsProxy, socksProxy] + + def check_ws_proxy_settings(self): + usedProxies = [] + wsProxy = None + wssProxy = None + wsSocksProxy = None + # ws proxy + isWsProxyDefined = self.value_is_defined(self.wsProxy) + is_ws_proxy_defined = self.value_is_defined(self.ws_proxy) + if isWsProxyDefined or is_ws_proxy_defined: + usedProxies.append('wsProxy') + wsProxy = self.wsProxy if (isWsProxyDefined) else self.ws_proxy + # wss proxy + isWssProxyDefined = self.value_is_defined(self.wssProxy) + is_wss_proxy_defined = self.value_is_defined(self.wss_proxy) + if isWssProxyDefined or is_wss_proxy_defined: + usedProxies.append('wssProxy') + wssProxy = self.wssProxy if (isWssProxyDefined) else self.wss_proxy + # ws socks proxy + isWsSocksProxyDefined = self.value_is_defined(self.wsSocksProxy) + is_ws_socks_proxy_defined = self.value_is_defined(self.ws_socks_proxy) + if isWsSocksProxyDefined or is_ws_socks_proxy_defined: + usedProxies.append('wsSocksProxy') + wsSocksProxy = self.wsSocksProxy if (isWsSocksProxyDefined) else self.ws_socks_proxy + # check + length = len(usedProxies) + if length > 1: + joinedProxyNames = ','.join(usedProxies) + raise InvalidProxySettings(self.id + ' you have multiple conflicting proxy settings(' + joinedProxyNames + '), please use only one from: wsProxy, wssProxy, wsSocksProxy') + return [wsProxy, wssProxy, wsSocksProxy] + + def check_conflicting_proxies(self, proxyAgentSet, proxyUrlSet): + if proxyAgentSet and proxyUrlSet: + raise InvalidProxySettings(self.id + ' you have multiple conflicting proxy settings, please use only one from : proxyUrl, httpProxy, httpsProxy, socksProxy') + + def check_address(self, address: Str = None): + if address is None: + raise InvalidAddress(self.id + ' address is None') + # check the address is not the same letter like 'aaaaa' nor too short nor has a space + uniqChars = (self.unique(self.string_to_chars_array(address))) + length = len(uniqChars) # py transpiler trick + if length == 1 or len(address) < self.minFundingAddressLength or address.find(' ') > -1: + raise InvalidAddress(self.id + ' address is invalid or has less than ' + str(self.minFundingAddressLength) + ' characters: "' + str(address) + '"') + return address + + def find_message_hashes(self, client, element: str): + result = [] + messageHashes = list(client.futures.keys()) + for i in range(0, len(messageHashes)): + messageHash = messageHashes[i] + if messageHash.find(element) >= 0: + result.append(messageHash) + return result + + def filter_by_limit(self, array: List[object], limit: Int = None, key: IndexType = 'timestamp', fromStart: bool = False): + if self.value_is_defined(limit): + arrayLength = len(array) + if arrayLength > 0: + ascending = True + if (key in array[0]): + first = array[0][key] + last = array[arrayLength - 1][key] + if first is not None and last is not None: + ascending = first <= last # True if array is sorted in ascending order based on 'timestamp' + if fromStart: + if limit > arrayLength: + limit = arrayLength + # array = self.array_slice(array, 0, limit) if ascending else self.array_slice(array, -limit) + if ascending: + array = self.array_slice(array, 0, limit) + else: + array = self.array_slice(array, -limit) + else: + # array = self.array_slice(array, -limit) if ascending else self.array_slice(array, 0, limit) + if ascending: + array = self.array_slice(array, -limit) + else: + array = self.array_slice(array, 0, limit) + return array + + def filter_by_since_limit(self, array: List[object], since: Int = None, limit: Int = None, key: IndexType = 'timestamp', tail=False): + sinceIsDefined = self.value_is_defined(since) + parsedArray = self.to_array(array) + result = parsedArray + if sinceIsDefined: + result = [] + for i in range(0, len(parsedArray)): + entry = parsedArray[i] + value = self.safe_value(entry, key) + if value and (value >= since): + result.append(entry) + if tail and limit is not None: + return self.array_slice(result, -limit) + # if the user provided a 'since' argument + # we want to limit the result starting from the 'since' + shouldFilterFromStart = not tail and sinceIsDefined + return self.filter_by_limit(result, limit, key, shouldFilterFromStart) + + def filter_by_value_since_limit(self, array: List[object], field: IndexType, value=None, since: Int = None, limit: Int = None, key='timestamp', tail=False): + valueIsDefined = self.value_is_defined(value) + sinceIsDefined = self.value_is_defined(since) + parsedArray = self.to_array(array) + result = parsedArray + # single-pass filter for both symbol and since + if valueIsDefined or sinceIsDefined: + result = [] + for i in range(0, len(parsedArray)): + entry = parsedArray[i] + entryFiledEqualValue = entry[field] == value + firstCondition = entryFiledEqualValue if valueIsDefined else True + entryKeyValue = self.safe_value(entry, key) + entryKeyGESince = (entryKeyValue) and (since is not None) and (entryKeyValue >= since) + secondCondition = entryKeyGESince if sinceIsDefined else True + if firstCondition and secondCondition: + result.append(entry) + if tail and limit is not None: + return self.array_slice(result, -limit) + return self.filter_by_limit(result, limit, key, sinceIsDefined) + + def set_sandbox_mode(self, enabled: bool): + """ + set the sandbox mode for the exchange + :param boolean enabled: True to enable sandbox mode, False to disable it + """ + if enabled: + if 'test' in self.urls: + if isinstance(self.urls['api'], str): + self.urls['apiBackup'] = self.urls['api'] + self.urls['api'] = self.urls['test'] + else: + self.urls['apiBackup'] = self.clone(self.urls['api']) + self.urls['api'] = self.clone(self.urls['test']) + else: + raise NotSupported(self.id + ' does not have a sandbox URL') + # set flag + self.isSandboxModeEnabled = True + elif 'apiBackup' in self.urls: + if isinstance(self.urls['api'], str): + self.urls['api'] = self.urls['apiBackup'] + else: + self.urls['api'] = self.clone(self.urls['apiBackup']) + newUrls = self.omit(self.urls, 'apiBackup') + self.urls = newUrls + # set flag + self.isSandboxModeEnabled = False + + def enable_demo_trading(self, enable: bool): + """ + enables or disables demo trading mode + :param boolean [enable]: True if demo trading should be enabled, False otherwise + """ + if self.isSandboxModeEnabled: + raise NotSupported(self.id + ' demo trading does not support in sandbox environment. Please check https://www.binance.com/en/support/faq/detail/9be58f73e5e14338809e3b705b9687dd to see the differences') + if enable: + self.urls['apiBackupDemoTrading'] = self.urls['api'] + self.urls['api'] = self.urls['demo'] + elif 'apiBackupDemoTrading' in self.urls: + self.urls['api'] = self.urls['apiBackupDemoTrading'] + newUrls = self.omit(self.urls, 'apiBackupDemoTrading') + self.urls = newUrls + self.options['enableDemoTrading'] = enable + + def sign(self, path, api: Any = 'public', method='GET', params={}, headers: Any = None, body: Any = None): + return {} + + def fetch_accounts(self, params={}): + raise NotSupported(self.id + ' fetchAccounts() is not supported yet') + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' fetchTrades() is not supported yet') + + def fetch_trades_ws(self, symbol: str, since: Int = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' fetchTradesWs() is not supported yet') + + def watch_liquidations(self, symbol: str, since: Int = None, limit: Int = None, params={}): + if self.has['watchLiquidationsForSymbols']: + return self.watch_liquidations_for_symbols([symbol], since, limit, params) + raise NotSupported(self.id + ' watchLiquidations() is not supported yet') + + def watch_liquidations_for_symbols(self, symbols: List[str], since: Int = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' watchLiquidationsForSymbols() is not supported yet') + + def watch_my_liquidations(self, symbol: str, since: Int = None, limit: Int = None, params={}): + if self.has['watchMyLiquidationsForSymbols']: + return self.watch_my_liquidations_for_symbols([symbol], since, limit, params) + raise NotSupported(self.id + ' watchMyLiquidations() is not supported yet') + + def watch_my_liquidations_for_symbols(self, symbols: List[str], since: Int = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' watchMyLiquidationsForSymbols() is not supported yet') + + def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' watchTrades() is not supported yet') + + def un_watch_orders(self, symbol: Str = None, params={}): + raise NotSupported(self.id + ' unWatchOrders() is not supported yet') + + def un_watch_trades(self, symbol: str, params={}): + raise NotSupported(self.id + ' unWatchTrades() is not supported yet') + + def watch_trades_for_symbols(self, symbols: List[str], since: Int = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' watchTradesForSymbols() is not supported yet') + + def un_watch_trades_for_symbols(self, symbols: List[str], params={}): + raise NotSupported(self.id + ' unWatchTradesForSymbols() is not supported yet') + + def watch_my_trades_for_symbols(self, symbols: List[str], since: Int = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' watchMyTradesForSymbols() is not supported yet') + + def watch_orders_for_symbols(self, symbols: List[str], since: Int = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' watchOrdersForSymbols() is not supported yet') + + def watch_ohlcv_for_symbols(self, symbolsAndTimeframes: List[List[str]], since: Int = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' watchOHLCVForSymbols() is not supported yet') + + def un_watch_ohlcv_for_symbols(self, symbolsAndTimeframes: List[List[str]], params={}): + raise NotSupported(self.id + ' unWatchOHLCVForSymbols() is not supported yet') + + def watch_order_book_for_symbols(self, symbols: List[str], limit: Int = None, params={}): + raise NotSupported(self.id + ' watchOrderBookForSymbols() is not supported yet') + + def un_watch_order_book_for_symbols(self, symbols: List[str], params={}): + raise NotSupported(self.id + ' unWatchOrderBookForSymbols() is not supported yet') + + def un_watch_positions(self, symbols: Strings = None, params={}): + raise NotSupported(self.id + ' unWatchPositions() is not supported yet') + + def un_watch_ticker(self, symbol: str, params={}): + raise NotSupported(self.id + ' unWatchTicker() is not supported yet') + + def un_watch_mark_price(self, symbol: str, params={}): + raise NotSupported(self.id + ' unWatchMarkPrice() is not supported yet') + + def un_watch_mark_prices(self, symbols: Strings = None, params={}): + raise NotSupported(self.id + ' unWatchMarkPrices() is not supported yet') + + def fetch_deposit_addresses(self, codes: Strings = None, params={}): + raise NotSupported(self.id + ' fetchDepositAddresses() is not supported yet') + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}): + raise NotSupported(self.id + ' fetchOrderBook() is not supported yet') + + def fetch_order_book_ws(self, symbol: str, limit: Int = None, params={}): + raise NotSupported(self.id + ' fetchOrderBookWs() is not supported yet') + + def fetch_margin_mode(self, symbol: str, params={}): + if self.has['fetchMarginModes']: + marginModes = self.fetch_margin_modes([symbol], params) + return self.safe_dict(marginModes, symbol) + else: + raise NotSupported(self.id + ' fetchMarginMode() is not supported yet') + + def fetch_margin_modes(self, symbols: Strings = None, params={}): + raise NotSupported(self.id + ' fetchMarginModes() is not supported yet') + + def fetch_rest_order_book_safe(self, symbol, limit=None, params={}): + fetchSnapshotMaxRetries = self.handle_option('watchOrderBook', 'maxRetries', 3) + for i in range(0, fetchSnapshotMaxRetries): + try: + orderBook = self.fetch_order_book(symbol, limit, params) + return orderBook + except Exception as e: + if (i + 1) == fetchSnapshotMaxRetries: + raise e + return None + + def watch_order_book(self, symbol: str, limit: Int = None, params={}): + raise NotSupported(self.id + ' watchOrderBook() is not supported yet') + + def un_watch_order_book(self, symbol: str, params={}): + raise NotSupported(self.id + ' unWatchOrderBook() is not supported yet') + + def fetch_time(self, params={}): + raise NotSupported(self.id + ' fetchTime() is not supported yet') + + def fetch_trading_limits(self, symbols: Strings = None, params={}): + raise NotSupported(self.id + ' fetchTradingLimits() is not supported yet') + + def parse_currency(self, rawCurrency: dict): + raise NotSupported(self.id + ' parseCurrency() is not supported yet') + + def parse_currencies(self, rawCurrencies): + result = {} + arr = self.to_array(rawCurrencies) + for i in range(0, len(arr)): + parsed = self.parse_currency(arr[i]) + code = parsed['code'] + result[code] = parsed + return result + + def parse_market(self, market: dict): + raise NotSupported(self.id + ' parseMarket() is not supported yet') + + def parse_markets(self, markets): + result = [] + for i in range(0, len(markets)): + result.append(self.parse_market(markets[i])) + return result + + def parse_ticker(self, ticker: dict, market: Market = None): + raise NotSupported(self.id + ' parseTicker() is not supported yet') + + def parse_deposit_address(self, depositAddress, currency: Currency = None): + raise NotSupported(self.id + ' parseDepositAddress() is not supported yet') + + def parse_trade(self, trade: dict, market: Market = None): + raise NotSupported(self.id + ' parseTrade() is not supported yet') + + def parse_transaction(self, transaction: dict, currency: Currency = None): + raise NotSupported(self.id + ' parseTransaction() is not supported yet') + + def parse_transfer(self, transfer: dict, currency: Currency = None): + raise NotSupported(self.id + ' parseTransfer() is not supported yet') + + def parse_account(self, account: dict): + raise NotSupported(self.id + ' parseAccount() is not supported yet') + + def parse_ledger_entry(self, item: dict, currency: Currency = None): + raise NotSupported(self.id + ' parseLedgerEntry() is not supported yet') + + def parse_order(self, order: dict, market: Market = None): + raise NotSupported(self.id + ' parseOrder() is not supported yet') + + def fetch_cross_borrow_rates(self, params={}): + raise NotSupported(self.id + ' fetchCrossBorrowRates() is not supported yet') + + def fetch_isolated_borrow_rates(self, params={}): + raise NotSupported(self.id + ' fetchIsolatedBorrowRates() is not supported yet') + + def parse_market_leverage_tiers(self, info, market: Market = None): + raise NotSupported(self.id + ' parseMarketLeverageTiers() is not supported yet') + + def fetch_leverage_tiers(self, symbols: Strings = None, params={}): + raise NotSupported(self.id + ' fetchLeverageTiers() is not supported yet') + + def parse_position(self, position: dict, market: Market = None): + raise NotSupported(self.id + ' parsePosition() is not supported yet') + + def parse_funding_rate_history(self, info, market: Market = None): + raise NotSupported(self.id + ' parseFundingRateHistory() is not supported yet') + + def parse_borrow_interest(self, info: dict, market: Market = None): + raise NotSupported(self.id + ' parseBorrowInterest() is not supported yet') + + def parse_isolated_borrow_rate(self, info: dict, market: Market = None): + raise NotSupported(self.id + ' parseIsolatedBorrowRate() is not supported yet') + + def parse_ws_trade(self, trade: dict, market: Market = None): + raise NotSupported(self.id + ' parseWsTrade() is not supported yet') + + def parse_ws_order(self, order: dict, market: Market = None): + raise NotSupported(self.id + ' parseWsOrder() is not supported yet') + + def parse_ws_order_trade(self, trade: dict, market: Market = None): + raise NotSupported(self.id + ' parseWsOrderTrade() is not supported yet') + + def parse_ws_ohlcv(self, ohlcv, market: Market = None): + return self.parse_ohlcv(ohlcv, market) + + def fetch_funding_rates(self, symbols: Strings = None, params={}): + raise NotSupported(self.id + ' fetchFundingRates() is not supported yet') + + def fetch_funding_intervals(self, symbols: Strings = None, params={}): + raise NotSupported(self.id + ' fetchFundingIntervals() is not supported yet') + + def watch_funding_rate(self, symbol: str, params={}): + raise NotSupported(self.id + ' watchFundingRate() is not supported yet') + + def watch_funding_rates(self, symbols: List[str], params={}): + raise NotSupported(self.id + ' watchFundingRates() is not supported yet') + + def watch_funding_rates_for_symbols(self, symbols: List[str], params={}): + return self.watch_funding_rates(symbols, params) + + def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}): + raise NotSupported(self.id + ' transfer() is not supported yet') + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}): + raise NotSupported(self.id + ' withdraw() is not supported yet') + + def create_deposit_address(self, code: str, params={}): + raise NotSupported(self.id + ' createDepositAddress() is not supported yet') + + def set_leverage(self, leverage: int, symbol: Str = None, params={}): + raise NotSupported(self.id + ' setLeverage() is not supported yet') + + def fetch_leverage(self, symbol: str, params={}): + if self.has['fetchLeverages']: + leverages = self.fetch_leverages([symbol], params) + return self.safe_dict(leverages, symbol) + else: + raise NotSupported(self.id + ' fetchLeverage() is not supported yet') + + def fetch_leverages(self, symbols: Strings = None, params={}): + raise NotSupported(self.id + ' fetchLeverages() is not supported yet') + + def set_position_mode(self, hedged: bool, symbol: Str = None, params={}): + raise NotSupported(self.id + ' setPositionMode() is not supported yet') + + def add_margin(self, symbol: str, amount: float, params={}): + raise NotSupported(self.id + ' addMargin() is not supported yet') + + def reduce_margin(self, symbol: str, amount: float, params={}): + raise NotSupported(self.id + ' reduceMargin() is not supported yet') + + def set_margin(self, symbol: str, amount: float, params={}): + raise NotSupported(self.id + ' setMargin() is not supported yet') + + def fetch_long_short_ratio(self, symbol: str, timeframe: Str = None, params={}): + raise NotSupported(self.id + ' fetchLongShortRatio() is not supported yet') + + def fetch_long_short_ratio_history(self, symbol: Str = None, timeframe: Str = None, since: Int = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' fetchLongShortRatioHistory() is not supported yet') + + def fetch_margin_adjustment_history(self, symbol: Str = None, type: Str = None, since: Num = None, limit: Num = None, params={}): + """ + fetches the history of margin added or reduced from contract isolated positions + :param str [symbol]: unified market symbol + :param str [type]: "add" or "reduce" + :param int [since]: timestamp in ms of the earliest change to fetch + :param int [limit]: the maximum amount of changes to fetch + :param dict params: extra parameters specific to the exchange api endpoint + :returns dict[]: a list of `margin structures ` + """ + raise NotSupported(self.id + ' fetchMarginAdjustmentHistory() is not supported yet') + + def set_margin_mode(self, marginMode: str, symbol: Str = None, params={}): + raise NotSupported(self.id + ' setMarginMode() is not supported yet') + + def fetch_deposit_addresses_by_network(self, code: str, params={}): + raise NotSupported(self.id + ' fetchDepositAddressesByNetwork() is not supported yet') + + def fetch_open_interest_history(self, symbol: str, timeframe: str = '1h', since: Int = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' fetchOpenInterestHistory() is not supported yet') + + def fetch_open_interest(self, symbol: str, params={}): + raise NotSupported(self.id + ' fetchOpenInterest() is not supported yet') + + def fetch_open_interests(self, symbols: Strings = None, params={}): + raise NotSupported(self.id + ' fetchOpenInterests() is not supported yet') + + def sign_in(self, params={}): + raise NotSupported(self.id + ' signIn() is not supported yet') + + def fetch_payment_methods(self, params={}): + raise NotSupported(self.id + ' fetchPaymentMethods() is not supported yet') + + def parse_to_int(self, number): + # Solve Common intmisuse ex: int((since / str(1000))) + # using a number which is not valid in ts + stringifiedNumber = self.number_to_string(number) + convertedNumber = float(stringifiedNumber) + return int(convertedNumber) + + def parse_to_numeric(self, number): + stringVersion = self.number_to_string(number) # self will convert 1.0 and 1 to "1" and 1.1 to "1.1" + # keep self in mind: + # in JS: 1 == 1.0 is True + # in Python: 1 == 1.0 is True + # in PHP: 1 == 1.0 is True, but 1 == 1.0 is False. + if stringVersion.find('.') >= 0: + return float(stringVersion) + return int(stringVersion) + + def is_round_number(self, value: float): + # self method is similar to isInteger, but self is more loyal and does not check for types. + # i.e. isRoundNumber(1.000) returns True, while isInteger(1.000) returns False + res = self.parse_to_numeric((value % 1)) + return res == 0 + + def safe_number_omit_zero(self, obj: object, key: IndexType, defaultValue: Num = None): + value = self.safe_string(obj, key) + final = self.parse_number(self.omit_zero(value)) + return defaultValue if (final is None) else final + + def safe_integer_omit_zero(self, obj: object, key: IndexType, defaultValue: Int = None): + timestamp = self.safe_integer(obj, key, defaultValue) + if timestamp is None or timestamp == 0: + return None + return timestamp + + def after_construct(self): + # networks + self.create_networks_by_id_object() + self.features_generator() + # init predefined markets if any + if self.markets: + self.set_markets(self.markets) + # init the request rate limiter + self.init_rest_rate_limiter() + # sanbox mode + isSandbox = self.safe_bool_2(self.options, 'sandbox', 'testnet', False) + if isSandbox: + self.set_sandbox_mode(isSandbox) + + def init_rest_rate_limiter(self): + if self.rateLimit is None or (self.id is not None and self.rateLimit == -1): + raise ExchangeError(self.id + '.rateLimit property is not configured') + refillRate = self.MAX_VALUE + if self.rateLimit > 0: + refillRate = 1 / self.rateLimit + defaultBucket = { + 'delay': 0.001, + 'capacity': 1, + 'cost': 1, + 'maxCapacity': self.safe_integer(self.options, 'maxRequestsQueue', 1000), + 'refillRate': refillRate, + } + existingBucket = {} if (self.tokenBucket is None) else self.tokenBucket + self.tokenBucket = self.extend(defaultBucket, existingBucket) + self.init_throttler() + + def features_generator(self): + # + # in the exchange-specific features can be something like self, where we support 'string' aliases too: + # + # { + # 'my' : { + # 'createOrder' : {...}, + # }, + # 'swap': { + # 'linear': { + # 'extends': my', + # }, + # }, + # } + # + if self.features is None: + return + # reconstruct + initialFeatures = self.features + self.features = {} + unifiedMarketTypes = ['spot', 'swap', 'future', 'option'] + subTypes = ['linear', 'inverse'] + # atm only support basic methods, eg: 'createOrder', 'fetchOrder', 'fetchOrders', 'fetchMyTrades' + for i in range(0, len(unifiedMarketTypes)): + marketType = unifiedMarketTypes[i] + # if marketType is not filled for self exchange, don't add that in `features` + if not (marketType in initialFeatures): + self.features[marketType] = None + else: + if marketType == 'spot': + self.features[marketType] = self.features_mapper(initialFeatures, marketType, None) + else: + self.features[marketType] = {} + for j in range(0, len(subTypes)): + subType = subTypes[j] + self.features[marketType][subType] = self.features_mapper(initialFeatures, marketType, subType) + + def features_mapper(self, initialFeatures: Any, marketType: Str, subType: Str = None): + featuresObj = initialFeatures[marketType][subType] if (subType is not None) else initialFeatures[marketType] + # if exchange does not have that market-type(eg. future>inverse) + if featuresObj is None: + return None + extendsStr: Str = self.safe_string(featuresObj, 'extends') + if extendsStr is not None: + featuresObj = self.omit(featuresObj, 'extends') + extendObj = self.features_mapper(initialFeatures, extendsStr) + featuresObj = self.deep_extend(extendObj, featuresObj) + # + # ### corrections ### + # + # createOrder + if 'createOrder' in featuresObj: + value = self.safe_dict(featuresObj['createOrder'], 'attachedStopLossTakeProfit') + featuresObj['createOrder']['stopLoss'] = value + featuresObj['createOrder']['takeProfit'] = value + if marketType == 'spot': + # default 'hedged': False + featuresObj['createOrder']['hedged'] = False + # default 'leverage': False + if not ('leverage' in featuresObj['createOrder']): + featuresObj['createOrder']['leverage'] = False + # default 'GTC' to True + if self.safe_bool(featuresObj['createOrder']['timeInForce'], 'GTC') is None: + featuresObj['createOrder']['timeInForce']['GTC'] = True + # other methods + keys = list(featuresObj.keys()) + for i in range(0, len(keys)): + key = keys[i] + featureBlock = featuresObj[key] + if not self.in_array(key, ['sandbox']) and featureBlock is not None: + # default "symbolRequired" to False to all methods(except `createOrder`) + if not ('symbolRequired' in featureBlock): + featureBlock['symbolRequired'] = self.in_array(key, ['createOrder', 'createOrders', 'fetchOHLCV']) + return featuresObj + + def feature_value(self, symbol: str, methodName: Str = None, paramName: Str = None, defaultValue: Any = None): + """ + self method is a very deterministic to help users to know what feature is supported by the exchange + :param str [symbol]: unified symbol + :param str [methodName]: view currently supported methods: https://docs.ccxt.com/#/README?id=features + :param str [paramName]: unified param value, like: `triggerPrice`, `stopLoss.triggerPrice`(check docs for supported param names) + :param dict [defaultValue]: return default value if no result found + :returns dict: returns feature value + """ + market = self.market(symbol) + return self.feature_value_by_type(market['type'], market['subType'], methodName, paramName, defaultValue) + + def feature_value_by_type(self, marketType: str, subType: Str, methodName: Str = None, paramName: Str = None, defaultValue: Any = None): + """ + self method is a very deterministic to help users to know what feature is supported by the exchange + :param str [marketType]: supported only: "spot", "swap", "future" + :param str [subType]: supported only: "linear", "inverse" + :param str [methodName]: view currently supported methods: https://docs.ccxt.com/#/README?id=features + :param str [paramName]: unified param value(check docs for supported param names) + :param dict [defaultValue]: return default value if no result found + :returns dict: returns feature value + """ + # if exchange does not yet have features manually implemented + if self.features is None: + return defaultValue + if marketType is None: + return defaultValue # marketType is required + # if marketType(e.g. 'option') does not exist in features + if not (marketType in self.features): + return defaultValue # unsupported marketType, check "exchange.features" for details + # if marketType dict None + if self.features[marketType] is None: + return defaultValue + methodsContainer = self.features[marketType] + if subType is None: + if marketType != 'spot': + return defaultValue # subType is required for non-spot markets + else: + if not (subType in self.features[marketType]): + return defaultValue # unsupported subType, check "exchange.features" for details + # if subType dict None + if self.features[marketType][subType] is None: + return defaultValue + methodsContainer = self.features[marketType][subType] + # if user wanted only marketType and didn't provide methodName, eg: featureIsSupported('spot') + if methodName is None: + return defaultValue if (defaultValue is not None) else methodsContainer + if not (methodName in methodsContainer): + return defaultValue # unsupported method, check "exchange.features" for details') + methodDict = methodsContainer[methodName] + if methodDict is None: + return defaultValue + # if user wanted only method and didn't provide `paramName`, eg: featureIsSupported('swap', 'linear', 'createOrder') + if paramName is None: + return defaultValue if (defaultValue is not None) else methodDict + splited = paramName.split('.') # can be only parent key(`stopLoss`) or with child(`stopLoss.triggerPrice`) + parentKey = splited[0] + subKey = self.safe_string(splited, 1) + if not (parentKey in methodDict): + return defaultValue # unsupported paramName, check "exchange.features" for details') + dictionary = self.safe_dict(methodDict, parentKey) + if dictionary is None: + # if the value is not dictionary but a scalar value(or None), return + return methodDict[parentKey] + else: + # return, when calling without subKey eg: featureValueByType('spot', None, 'createOrder', 'stopLoss') + if subKey is None: + return methodDict[parentKey] + # raise an exception for unsupported subKey + if not (subKey in methodDict[parentKey]): + return defaultValue # unsupported subKey, check "exchange.features" for details + return methodDict[parentKey][subKey] + + def orderbook_checksum_message(self, symbol: Str): + return symbol + ' = False' + + def create_networks_by_id_object(self): + # automatically generate network-id-to-code mappings + networkIdsToCodesGenerated = self.invert_flat_string_dictionary(self.safe_value(self.options, 'networks', {})) # invert defined networks dictionary + self.options['networksById'] = self.extend(networkIdsToCodesGenerated, self.safe_value(self.options, 'networksById', {})) # support manually overriden "networksById" dictionary too + + def get_default_options(self): + return { + 'defaultNetworkCodeReplacements': { + 'ETH': {'ERC20': 'ETH'}, + 'TRX': {'TRC20': 'TRX'}, + 'CRO': {'CRC20': 'CRONOS'}, + 'BRC20': {'BRC20': 'BTC'}, + }, + } + + def safe_ledger_entry(self, entry: object, currency: Currency = None): + currency = self.safe_currency(None, currency) + direction = self.safe_string(entry, 'direction') + before = self.safe_string(entry, 'before') + after = self.safe_string(entry, 'after') + amount = self.safe_string(entry, 'amount') + if amount is not None: + if before is None and after is not None: + before = Precise.string_sub(after, amount) + elif before is not None and after is None: + after = Precise.string_add(before, amount) + if before is not None and after is not None: + if direction is None: + if Precise.string_gt(before, after): + direction = 'out' + if Precise.string_gt(after, before): + direction = 'in' + fee = self.safe_value(entry, 'fee') + if fee is not None: + fee['cost'] = self.safe_number(fee, 'cost') + timestamp = self.safe_integer(entry, 'timestamp') + info = self.safe_dict(entry, 'info', {}) + return { + 'id': self.safe_string(entry, 'id'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'direction': direction, + 'account': self.safe_string(entry, 'account'), + 'referenceId': self.safe_string(entry, 'referenceId'), + 'referenceAccount': self.safe_string(entry, 'referenceAccount'), + 'type': self.safe_string(entry, 'type'), + 'currency': currency['code'], + 'amount': self.parse_number(amount), + 'before': self.parse_number(before), + 'after': self.parse_number(after), + 'status': self.safe_string(entry, 'status'), + 'fee': fee, + 'info': info, + } + + def safe_currency_structure(self, currency: object): + # derive data from networks: deposit, withdraw, active, fee, limits, precision + networks = self.safe_dict(currency, 'networks', {}) + keys = list(networks.keys()) + length = len(keys) + if length != 0: + for i in range(0, length): + key = keys[i] + network = networks[key] + deposit = self.safe_bool(network, 'deposit') + currencyDeposit = self.safe_bool(currency, 'deposit') + if currencyDeposit is None or deposit: + currency['deposit'] = deposit + withdraw = self.safe_bool(network, 'withdraw') + currencyWithdraw = self.safe_bool(currency, 'withdraw') + if currencyWithdraw is None or withdraw: + currency['withdraw'] = withdraw + # set network 'active' to False if D or W is disabled + active = self.safe_bool(network, 'active') + if active is None: + if deposit and withdraw: + currency['networks'][key]['active'] = True + elif deposit is not None and withdraw is not None: + currency['networks'][key]['active'] = False + active = self.safe_bool(currency['networks'][key], 'active') # dict might have been updated on above lines, so access directly instead of `network` variable + currencyActive = self.safe_bool(currency, 'active') + if currencyActive is None or active: + currency['active'] = active + # find lowest fee(which is more desired) + fee = self.safe_string(network, 'fee') + feeMain = self.safe_string(currency, 'fee') + if feeMain is None or Precise.string_lt(fee, feeMain): + currency['fee'] = self.parse_number(fee) + # find lowest precision(which is more desired) + precision = self.safe_string(network, 'precision') + precisionMain = self.safe_string(currency, 'precision') + if precisionMain is None or Precise.string_gt(precision, precisionMain): + currency['precision'] = self.parse_number(precision) + # limits + limits = self.safe_dict(network, 'limits') + limitsMain = self.safe_dict(currency, 'limits') + if limitsMain is None: + currency['limits'] = {} + # deposits + limitsDeposit = self.safe_dict(limits, 'deposit') + limitsDepositMain = self.safe_dict(limitsMain, 'deposit') + if limitsDepositMain is None: + currency['limits']['deposit'] = {} + limitsDepositMin = self.safe_string(limitsDeposit, 'min') + limitsDepositMax = self.safe_string(limitsDeposit, 'max') + limitsDepositMinMain = self.safe_string(limitsDepositMain, 'min') + limitsDepositMaxMain = self.safe_string(limitsDepositMain, 'max') + # find min + if limitsDepositMinMain is None or Precise.string_lt(limitsDepositMin, limitsDepositMinMain): + currency['limits']['deposit']['min'] = self.parse_number(limitsDepositMin) + # find max + if limitsDepositMaxMain is None or Precise.string_gt(limitsDepositMax, limitsDepositMaxMain): + currency['limits']['deposit']['max'] = self.parse_number(limitsDepositMax) + # withdrawals + limitsWithdraw = self.safe_dict(limits, 'withdraw') + limitsWithdrawMain = self.safe_dict(limitsMain, 'withdraw') + if limitsWithdrawMain is None: + currency['limits']['withdraw'] = {} + limitsWithdrawMin = self.safe_string(limitsWithdraw, 'min') + limitsWithdrawMax = self.safe_string(limitsWithdraw, 'max') + limitsWithdrawMinMain = self.safe_string(limitsWithdrawMain, 'min') + limitsWithdrawMaxMain = self.safe_string(limitsWithdrawMain, 'max') + # find min + if limitsWithdrawMinMain is None or Precise.string_lt(limitsWithdrawMin, limitsWithdrawMinMain): + currency['limits']['withdraw']['min'] = self.parse_number(limitsWithdrawMin) + # find max + if limitsWithdrawMaxMain is None or Precise.string_gt(limitsWithdrawMax, limitsWithdrawMaxMain): + currency['limits']['withdraw']['max'] = self.parse_number(limitsWithdrawMax) + return self.extend({ + 'info': None, + 'id': None, + 'numericId': None, + 'code': None, + 'precision': None, + 'type': None, + 'name': None, + 'active': None, + 'deposit': None, + 'withdraw': None, + 'fee': None, + 'fees': {}, + 'networks': {}, + 'limits': { + 'deposit': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + }, + }, currency) + + def safe_market_structure(self, market: dict = None): + cleanStructure = { + 'id': None, + 'lowercaseId': None, + 'symbol': None, + 'base': None, + 'quote': None, + 'settle': None, + 'baseId': None, + 'quoteId': None, + 'settleId': None, + 'type': None, + 'spot': None, + 'margin': None, + 'swap': None, + 'future': None, + 'option': None, + 'index': None, + 'active': None, + 'contract': None, + 'linear': None, + 'inverse': None, + 'subType': None, + 'taker': None, + 'maker': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': None, + 'price': None, + 'cost': None, + 'base': None, + 'quote': None, + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': None, + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'marginModes': { + 'cross': None, + 'isolated': None, + }, + 'created': None, + 'info': None, + } + if market is not None: + result = self.extend(cleanStructure, market) + # set None swap/future/etc + if result['spot']: + if result['contract'] is None: + result['contract'] = False + if result['swap'] is None: + result['swap'] = False + if result['future'] is None: + result['future'] = False + if result['option'] is None: + result['option'] = False + if result['index'] is None: + result['index'] = False + return result + return cleanStructure + + def set_markets(self, markets, currencies=None): + values = [] + self.markets_by_id = self.create_safe_dictionary() + # handle marketId conflicts + # we insert spot markets first + marketValues = self.sort_by(self.to_array(markets), 'spot', True, True) + for i in range(0, len(marketValues)): + value = marketValues[i] + if value['id'] in self.markets_by_id: + marketsByIdArray = (self.markets_by_id[value['id']]) + marketsByIdArray.append(value) + self.markets_by_id[value['id']] = marketsByIdArray + else: + self.markets_by_id[value['id']] = [value] + market = self.deep_extend(self.safe_market_structure(), { + 'precision': self.precision, + 'limits': self.limits, + }, self.fees['trading'], value) + if market['linear']: + market['subType'] = 'linear' + elif market['inverse']: + market['subType'] = 'inverse' + else: + market['subType'] = None + values.append(market) + self.markets = self.map_to_safe_map(self.index_by(values, 'symbol')) + marketsSortedBySymbol = self.keysort(self.markets) + marketsSortedById = self.keysort(self.markets_by_id) + self.symbols = list(marketsSortedBySymbol.keys()) + self.ids = list(marketsSortedById.keys()) + numCurrencies = 0 + if currencies is not None: + keys = list(currencies.keys()) + numCurrencies = len(keys) + if numCurrencies > 0: + # currencies is always None when called in constructor but not when called from loadMarkets + self.currencies = self.map_to_safe_map(self.deep_extend(self.currencies, currencies)) + else: + baseCurrencies = [] + quoteCurrencies = [] + for i in range(0, len(values)): + market = values[i] + defaultCurrencyPrecision = 8 if (self.precisionMode == DECIMAL_PLACES) else self.parse_number('1e-8') + marketPrecision = self.safe_dict(market, 'precision', {}) + if 'base' in market: + currency = self.safe_currency_structure({ + 'id': self.safe_string_2(market, 'baseId', 'base'), + 'numericId': self.safe_integer(market, 'baseNumericId'), + 'code': self.safe_string(market, 'base'), + 'precision': self.safe_value_2(marketPrecision, 'base', 'amount', defaultCurrencyPrecision), + }) + baseCurrencies.append(currency) + if 'quote' in market: + currency = self.safe_currency_structure({ + 'id': self.safe_string_2(market, 'quoteId', 'quote'), + 'numericId': self.safe_integer(market, 'quoteNumericId'), + 'code': self.safe_string(market, 'quote'), + 'precision': self.safe_value_2(marketPrecision, 'quote', 'price', defaultCurrencyPrecision), + }) + quoteCurrencies.append(currency) + baseCurrencies = self.sort_by(baseCurrencies, 'code', False, '') + quoteCurrencies = self.sort_by(quoteCurrencies, 'code', False, '') + self.baseCurrencies = self.map_to_safe_map(self.index_by(baseCurrencies, 'code')) + self.quoteCurrencies = self.map_to_safe_map(self.index_by(quoteCurrencies, 'code')) + allCurrencies = self.array_concat(baseCurrencies, quoteCurrencies) + groupedCurrencies = self.group_by(allCurrencies, 'code') + codes = list(groupedCurrencies.keys()) + resultingCurrencies = [] + for i in range(0, len(codes)): + code = codes[i] + groupedCurrenciesCode = self.safe_list(groupedCurrencies, code, []) + highestPrecisionCurrency = self.safe_value(groupedCurrenciesCode, 0) + for j in range(1, len(groupedCurrenciesCode)): + currentCurrency = groupedCurrenciesCode[j] + if self.precisionMode == TICK_SIZE: + highestPrecisionCurrency = currentCurrency if (currentCurrency['precision'] < highestPrecisionCurrency['precision']) else highestPrecisionCurrency + else: + highestPrecisionCurrency = currentCurrency if (currentCurrency['precision'] > highestPrecisionCurrency['precision']) else highestPrecisionCurrency + resultingCurrencies.append(highestPrecisionCurrency) + sortedCurrencies = self.sort_by(resultingCurrencies, 'code') + self.currencies = self.map_to_safe_map(self.deep_extend(self.currencies, self.index_by(sortedCurrencies, 'code'))) + self.currencies_by_id = self.index_by_safe(self.currencies, 'id') + currenciesSortedByCode = self.keysort(self.currencies) + self.codes = list(currenciesSortedByCode.keys()) + return self.markets + + def set_markets_from_exchange(self, sourceExchange): + # Validate that both exchanges are of the same type + if self.id != sourceExchange.id: + raise ArgumentsRequired(self.id + ' shareMarkets() can only share markets with exchanges of the same type(got ' + sourceExchange['id'] + ')') + # Validate that source exchange has loaded markets + if not sourceExchange.markets: + raise ExchangeError('setMarketsFromExchange() source exchange must have loaded markets first. Can call by using loadMarkets function') + # Set all market-related data + self.markets = sourceExchange.markets + self.markets_by_id = sourceExchange.markets_by_id + self.symbols = sourceExchange.symbols + self.ids = sourceExchange.ids + self.currencies = sourceExchange.currencies + self.currencies_by_id = sourceExchange.currencies_by_id + self.baseCurrencies = sourceExchange.baseCurrencies + self.quoteCurrencies = sourceExchange.quoteCurrencies + self.codes = sourceExchange.codes + # check marketHelperProps + sourceExchangeHelpers = self.safe_list(sourceExchange.options, 'marketHelperProps', []) + for i in range(0, len(sourceExchangeHelpers)): + helper = sourceExchangeHelpers[i] + if sourceExchange.options[helper] is not None: + self.options[helper] = sourceExchange.options[helper] + return self + + def get_describe_for_extended_ws_exchange(self, currentRestInstance: Any, parentRestInstance: Any, wsBaseDescribe: dict): + extendedRestDescribe = self.deep_extend(parentRestInstance.describe(), currentRestInstance.describe()) + superWithRestDescribe = self.deep_extend(extendedRestDescribe, wsBaseDescribe) + return superWithRestDescribe + + def safe_balance(self, balance: dict): + balances = self.omit(balance, ['info', 'timestamp', 'datetime', 'free', 'used', 'total']) + codes = list(balances.keys()) + balance['free'] = {} + balance['used'] = {} + balance['total'] = {} + debtBalance = {} + for i in range(0, len(codes)): + code = codes[i] + total = self.safe_string(balance[code], 'total') + free = self.safe_string(balance[code], 'free') + used = self.safe_string(balance[code], 'used') + debt = self.safe_string(balance[code], 'debt') + if (total is None) and (free is not None) and (used is not None): + total = Precise.string_add(free, used) + if (free is None) and (total is not None) and (used is not None): + free = Precise.string_sub(total, used) + if (used is None) and (total is not None) and (free is not None): + used = Precise.string_sub(total, free) + balance[code]['free'] = self.parse_number(free) + balance[code]['used'] = self.parse_number(used) + balance[code]['total'] = self.parse_number(total) + balance['free'][code] = balance[code]['free'] + balance['used'][code] = balance[code]['used'] + balance['total'][code] = balance[code]['total'] + if debt is not None: + balance[code]['debt'] = self.parse_number(debt) + debtBalance[code] = balance[code]['debt'] + debtBalanceArray = list(debtBalance.keys()) + length = len(debtBalanceArray) + if length: + balance['debt'] = debtBalance + return balance + + def safe_order(self, order: dict, market: Market = None): + # parses numbers + # * it is important pass the trades rawTrades + amount = self.omit_zero(self.safe_string(order, 'amount')) + remaining = self.safe_string(order, 'remaining') + filled = self.safe_string(order, 'filled') + cost = self.safe_string(order, 'cost') + average = self.omit_zero(self.safe_string(order, 'average')) + price = self.omit_zero(self.safe_string(order, 'price')) + lastTradeTimeTimestamp = self.safe_integer(order, 'lastTradeTimestamp') + symbol = self.safe_string(order, 'symbol') + side = self.safe_string(order, 'side') + status = self.safe_string(order, 'status') + parseFilled = (filled is None) + parseCost = (cost is None) + parseLastTradeTimeTimestamp = (lastTradeTimeTimestamp is None) + fee = self.safe_value(order, 'fee') + parseFee = (fee is None) + parseFees = self.safe_value(order, 'fees') is None + parseSymbol = symbol is None + parseSide = side is None + shouldParseFees = parseFee or parseFees + fees = self.safe_list(order, 'fees', []) + trades = [] + isTriggerOrSLTpOrder = ((self.safe_string(order, 'triggerPrice') is not None or (self.safe_string(order, 'stopLossPrice') is not None)) or (self.safe_string(order, 'takeProfitPrice') is not None)) + if parseFilled or parseCost or shouldParseFees: + rawTrades = self.safe_value(order, 'trades', trades) + # oldNumber = self.number + # we parse trades here! + # i don't think self is needed anymore + # self.number = str + firstTrade = self.safe_value(rawTrades, 0) + # parse trades if they haven't already been parsed + tradesAreParsed = ((firstTrade is not None) and ('info' in firstTrade) and ('id' in firstTrade)) + if not tradesAreParsed: + trades = self.parse_trades(rawTrades, market) + else: + trades = rawTrades + # self.number = oldNumber; why parse trades if you read the value using `safeString` ? + tradesLength = 0 + isArray = isinstance(trades, list) + if isArray: + tradesLength = len(trades) + if isArray and (tradesLength > 0): + # move properties that are defined in trades up into the order + if order['symbol'] is None: + order['symbol'] = trades[0]['symbol'] + if order['side'] is None: + order['side'] = trades[0]['side'] + if order['type'] is None: + order['type'] = trades[0]['type'] + if order['id'] is None: + order['id'] = trades[0]['order'] + if parseFilled: + filled = '0' + if parseCost: + cost = '0' + for i in range(0, len(trades)): + trade = trades[i] + tradeAmount = self.safe_string(trade, 'amount') + if parseFilled and (tradeAmount is not None): + filled = Precise.string_add(filled, tradeAmount) + tradeCost = self.safe_string(trade, 'cost') + if parseCost and (tradeCost is not None): + cost = Precise.string_add(cost, tradeCost) + if parseSymbol: + symbol = self.safe_string(trade, 'symbol') + if parseSide: + side = self.safe_string(trade, 'side') + tradeTimestamp = self.safe_value(trade, 'timestamp') + if parseLastTradeTimeTimestamp and (tradeTimestamp is not None): + if lastTradeTimeTimestamp is None: + lastTradeTimeTimestamp = tradeTimestamp + else: + lastTradeTimeTimestamp = max(lastTradeTimeTimestamp, tradeTimestamp) + if shouldParseFees: + tradeFees = self.safe_value(trade, 'fees') + if tradeFees is not None: + for j in range(0, len(tradeFees)): + tradeFee = tradeFees[j] + fees.append(self.extend({}, tradeFee)) + else: + tradeFee = self.safe_value(trade, 'fee') + if tradeFee is not None: + fees.append(self.extend({}, tradeFee)) + if shouldParseFees: + reducedFees = self.reduce_fees_by_currency(fees) if self.reduceFees else fees + reducedLength = len(reducedFees) + for i in range(0, reducedLength): + reducedFees[i]['cost'] = self.safe_number(reducedFees[i], 'cost') + if 'rate' in reducedFees[i]: + reducedFees[i]['rate'] = self.safe_number(reducedFees[i], 'rate') + if not parseFee and (reducedLength == 0): + # copy fee to avoid modification by reference + feeCopy = self.deep_extend(fee) + feeCopy['cost'] = self.safe_number(feeCopy, 'cost') + if 'rate' in feeCopy: + feeCopy['rate'] = self.safe_number(feeCopy, 'rate') + reducedFees.append(feeCopy) + order['fees'] = reducedFees + if parseFee and (reducedLength == 1): + order['fee'] = reducedFees[0] + if amount is None: + # ensure amount = filled + remaining + if filled is not None and remaining is not None: + amount = Precise.string_add(filled, remaining) + elif status == 'closed': + amount = filled + if filled is None: + if amount is not None and remaining is not None: + filled = Precise.string_sub(amount, remaining) + elif status == 'closed' and amount is not None: + filled = amount + if remaining is None: + if amount is not None and filled is not None: + remaining = Precise.string_sub(amount, filled) + elif status == 'closed': + remaining = '0' + # ensure that the average field is calculated correctly + inverse = self.safe_bool(market, 'inverse', False) + contractSize = self.number_to_string(self.safe_value(market, 'contractSize', 1)) + # inverse + # price = filled * contract size / cost + # + # linear + # price = cost / (filled * contract size) + if average is None: + if (filled is not None) and (cost is not None) and Precise.string_gt(filled, '0'): + filledTimesContractSize = Precise.string_mul(filled, contractSize) + if inverse: + average = Precise.string_div(filledTimesContractSize, cost) + else: + average = Precise.string_div(cost, filledTimesContractSize) + # similarly + # inverse + # cost = filled * contract size / price + # + # linear + # cost = filled * contract size * price + costPriceExists = (average is not None) or (price is not None) + if parseCost and (filled is not None) and costPriceExists: + multiplyPrice = None + if average is None: + multiplyPrice = price + else: + multiplyPrice = average + # contract trading + filledTimesContractSize = Precise.string_mul(filled, contractSize) + if inverse: + cost = Precise.string_div(filledTimesContractSize, multiplyPrice) + else: + cost = Precise.string_mul(filledTimesContractSize, multiplyPrice) + # support for market orders + orderType = self.safe_value(order, 'type') + emptyPrice = (price is None) or Precise.string_equals(price, '0') + if emptyPrice and (orderType == 'market'): + price = average + # we have trades with string values at self point so we will mutate them + for i in range(0, len(trades)): + entry = trades[i] + entry['amount'] = self.safe_number(entry, 'amount') + entry['price'] = self.safe_number(entry, 'price') + entry['cost'] = self.safe_number(entry, 'cost') + tradeFee = self.safe_dict(entry, 'fee', {}) + tradeFee['cost'] = self.safe_number(tradeFee, 'cost') + if 'rate' in tradeFee: + tradeFee['rate'] = self.safe_number(tradeFee, 'rate') + entryFees = self.safe_list(entry, 'fees', []) + for j in range(0, len(entryFees)): + entryFees[j]['cost'] = self.safe_number(entryFees[j], 'cost') + entry['fees'] = entryFees + entry['fee'] = tradeFee + timeInForce = self.safe_string(order, 'timeInForce') + postOnly = self.safe_value(order, 'postOnly') + # timeInForceHandling + if timeInForce is None: + if not isTriggerOrSLTpOrder and (self.safe_string(order, 'type') == 'market'): + timeInForce = 'IOC' + # allow postOnly override + if postOnly: + timeInForce = 'PO' + elif postOnly is None: + # timeInForce is not None here + postOnly = timeInForce == 'PO' + timestamp = self.safe_integer(order, 'timestamp') + lastUpdateTimestamp = self.safe_integer(order, 'lastUpdateTimestamp') + datetime = self.safe_string(order, 'datetime') + if datetime is None: + datetime = self.iso8601(timestamp) + triggerPrice = self.parse_number(self.safe_string_2(order, 'triggerPrice', 'stopPrice')) + takeProfitPrice = self.parse_number(self.safe_string(order, 'takeProfitPrice')) + stopLossPrice = self.parse_number(self.safe_string(order, 'stopLossPrice')) + return self.extend(order, { + 'id': self.safe_string(order, 'id'), + 'clientOrderId': self.safe_string(order, 'clientOrderId'), + 'timestamp': timestamp, + 'datetime': datetime, + 'symbol': symbol, + 'type': self.safe_string(order, 'type'), + 'side': side, + 'lastTradeTimestamp': lastTradeTimeTimestamp, + 'lastUpdateTimestamp': lastUpdateTimestamp, + 'price': self.parse_number(price), + 'amount': self.parse_number(amount), + 'cost': self.parse_number(cost), + 'average': self.parse_number(average), + 'filled': self.parse_number(filled), + 'remaining': self.parse_number(remaining), + 'timeInForce': timeInForce, + 'postOnly': postOnly, + 'trades': trades, + 'reduceOnly': self.safe_value(order, 'reduceOnly'), + 'stopPrice': triggerPrice, # ! deprecated, use triggerPrice instead + 'triggerPrice': triggerPrice, + 'takeProfitPrice': takeProfitPrice, + 'stopLossPrice': stopLossPrice, + 'status': status, + 'fee': self.safe_value(order, 'fee'), + }) + + def parse_orders(self, orders: object, market: Market = None, since: Int = None, limit: Int = None, params={}): + # + # the value of orders is either a dict or a list + # + # dict + # + # { + # 'id1': {...}, + # 'id2': {...}, + # 'id3': {...}, + # ... + # } + # + # list + # + # [ + # {'id': 'id1', ...}, + # {'id': 'id2', ...}, + # {'id': 'id3', ...}, + # ... + # ] + # + results = [] + if isinstance(orders, list): + for i in range(0, len(orders)): + parsed = self.parse_order(orders[i], market) # don't inline self call + order = self.extend(parsed, params) + results.append(order) + else: + ids = list(orders.keys()) + for i in range(0, len(ids)): + id = ids[i] + idExtended = self.extend({'id': id}, orders[id]) + parsedOrder = self.parse_order(idExtended, market) # don't inline these calls + order = self.extend(parsedOrder, params) + results.append(order) + results = self.sort_by(results, 'timestamp') + symbol = market['symbol'] if (market is not None) else None + return self.filter_by_symbol_since_limit(results, symbol, since, limit) + + def calculate_fee_with_rate(self, symbol: str, type: str, side: str, amount: float, price: float, takerOrMaker='taker', feeRate: Num = None, params={}): + if type == 'market' and takerOrMaker == 'maker': + raise ArgumentsRequired(self.id + ' calculateFee() - you have provided incompatible arguments - "market" type order can not be "maker". Change either the "type" or the "takerOrMaker" argument to calculate the fee.') + market = self.markets[symbol] + feeSide = self.safe_string(market, 'feeSide', 'quote') + useQuote = None + if feeSide == 'get': + # the fee is always in the currency you get + useQuote = side == 'sell' + elif feeSide == 'give': + # the fee is always in the currency you give + useQuote = side == 'buy' + else: + # the fee is always in feeSide currency + useQuote = feeSide == 'quote' + cost = self.number_to_string(amount) + key = None + if useQuote: + priceString = self.number_to_string(price) + cost = Precise.string_mul(cost, priceString) + key = 'quote' + else: + key = 'base' + # for derivatives, the fee is in 'settle' currency + if not market['spot']: + key = 'settle' + # even if `takerOrMaker` argument was set to 'maker', for 'market' orders we should forcefully override it to 'taker' + if type == 'market': + takerOrMaker = 'taker' + rate = self.number_to_string(feeRate) if (feeRate is not None) else self.safe_string(market, takerOrMaker) + cost = Precise.string_mul(cost, rate) + return { + 'type': takerOrMaker, + 'currency': market[key], + 'rate': self.parse_number(rate), + 'cost': self.parse_number(cost), + } + + def calculate_fee(self, symbol: str, type: str, side: str, amount: float, price: float, takerOrMaker='taker', params={}): + """ + calculates the presumptive fee that would be charged for an order + :param str symbol: unified market symbol + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade, in units of the base currency on most exchanges, or number of contracts + :param float price: the price for the order to be filled at, in units of the quote currency + :param str takerOrMaker: 'taker' or 'maker' + :param dict params: + :returns dict: contains the rate, the percentage multiplied to the order amount to obtain the fee amount, and cost, the total value of the fee in units of the quote currency, for the order + """ + return self.calculate_fee_with_rate(symbol, type, side, amount, price, takerOrMaker, None, params) + + def safe_liquidation(self, liquidation: dict, market: Market = None): + contracts = self.safe_string(liquidation, 'contracts') + contractSize = self.safe_string(market, 'contractSize') + price = self.safe_string(liquidation, 'price') + baseValue = self.safe_string(liquidation, 'baseValue') + quoteValue = self.safe_string(liquidation, 'quoteValue') + if (baseValue is None) and (contracts is not None) and (contractSize is not None) and (price is not None): + baseValue = Precise.string_mul(contracts, contractSize) + if (quoteValue is None) and (baseValue is not None) and (price is not None): + quoteValue = Precise.string_mul(baseValue, price) + liquidation['contracts'] = self.parse_number(contracts) + liquidation['contractSize'] = self.parse_number(contractSize) + liquidation['price'] = self.parse_number(price) + liquidation['baseValue'] = self.parse_number(baseValue) + liquidation['quoteValue'] = self.parse_number(quoteValue) + return liquidation + + def safe_trade(self, trade: dict, market: Market = None): + amount = self.safe_string(trade, 'amount') + price = self.safe_string(trade, 'price') + cost = self.safe_string(trade, 'cost') + if cost is None: + # contract trading + contractSize = self.safe_string(market, 'contractSize') + multiplyPrice = price + if contractSize is not None: + inverse = self.safe_bool(market, 'inverse', False) + if inverse: + multiplyPrice = Precise.string_div('1', price) + multiplyPrice = Precise.string_mul(multiplyPrice, contractSize) + cost = Precise.string_mul(multiplyPrice, amount) + resultFee, resultFees = self.parsed_fee_and_fees(trade) + trade['fee'] = resultFee + trade['fees'] = resultFees + trade['amount'] = self.parse_number(amount) + trade['price'] = self.parse_number(price) + trade['cost'] = self.parse_number(cost) + return trade + + def create_ccxt_trade_id(self, timestamp=None, side=None, amount=None, price=None, takerOrMaker=None): + # self approach is being used by multiple exchanges(mexc, woo, coinsbit, dydx, ...) + id = None + if timestamp is not None: + id = self.number_to_string(timestamp) + if side is not None: + id += '-' + side + if amount is not None: + id += '-' + self.number_to_string(amount) + if price is not None: + id += '-' + self.number_to_string(price) + if takerOrMaker is not None: + id += '-' + takerOrMaker + return id + + def parsed_fee_and_fees(self, container: Any): + fee = self.safe_dict(container, 'fee') + fees = self.safe_list(container, 'fees') + feeDefined = fee is not None + feesDefined = fees is not None + # parsing only if at least one of them is defined + shouldParseFees = (feeDefined or feesDefined) + if shouldParseFees: + if feeDefined: + fee = self.parse_fee_numeric(fee) + if not feesDefined: + # just set it directly, no further processing needed. + fees = [fee] + # 'fees' were set, so reparse them + reducedFees = self.reduce_fees_by_currency(fees) if self.reduceFees else fees + reducedLength = len(reducedFees) + for i in range(0, reducedLength): + reducedFees[i] = self.parse_fee_numeric(reducedFees[i]) + fees = reducedFees + if reducedLength == 1: + fee = reducedFees[0] + elif reducedLength == 0: + fee = None + # in case `fee & fees` are None, set `fees` array + if fee is None: + fee = { + 'cost': None, + 'currency': None, + } + if fees is None: + fees = [] + return [fee, fees] + + def parse_fee_numeric(self, fee: Any): + fee['cost'] = self.safe_number(fee, 'cost') # ensure numeric + if 'rate' in fee: + fee['rate'] = self.safe_number(fee, 'rate') + return fee + + def find_nearest_ceiling(self, arr: List[float], providedValue: float): + # i.e. findNearestCeiling([10, 30, 50], 23) returns 30 + length = len(arr) + for i in range(0, length): + current = arr[i] + if providedValue <= current: + return current + return arr[length - 1] + + def invert_flat_string_dictionary(self, dict): + reversed = {} + keys = list(dict.keys()) + for i in range(0, len(keys)): + key = keys[i] + value = dict[key] + if isinstance(value, str): + reversed[value] = key + return reversed + + def reduce_fees_by_currency(self, fees): + # + # self function takes a list of fee structures having the following format + # + # string = True + # + # [ + # {'currency': 'BTC', 'cost': '0.1'}, + # {'currency': 'BTC', 'cost': '0.2' }, + # {'currency': 'BTC', 'cost': '0.2', 'rate': '0.00123'}, + # {'currency': 'BTC', 'cost': '0.4', 'rate': '0.00123'}, + # {'currency': 'BTC', 'cost': '0.5', 'rate': '0.00456'}, + # {'currency': 'USDT', 'cost': '12.3456'}, + # ] + # + # string = False + # + # [ + # {'currency': 'BTC', 'cost': 0.1}, + # {'currency': 'BTC', 'cost': 0.2}, + # {'currency': 'BTC', 'cost': 0.2, 'rate': 0.00123}, + # {'currency': 'BTC', 'cost': 0.4, 'rate': 0.00123}, + # {'currency': 'BTC', 'cost': 0.5, 'rate': 0.00456}, + # {'currency': 'USDT', 'cost': 12.3456}, + # ] + # + # and returns a reduced fee list, where fees are summed per currency and rate(if any) + # + # string = True + # + # [ + # {'currency': 'BTC', 'cost': '0.4' }, + # {'currency': 'BTC', 'cost': '0.6', 'rate': '0.00123'}, + # {'currency': 'BTC', 'cost': '0.5', 'rate': '0.00456'}, + # {'currency': 'USDT', 'cost': '12.3456'}, + # ] + # + # string = False + # + # [ + # {'currency': 'BTC', 'cost': 0.3 }, + # {'currency': 'BTC', 'cost': 0.6, 'rate': 0.00123}, + # {'currency': 'BTC', 'cost': 0.5, 'rate': 0.00456}, + # {'currency': 'USDT', 'cost': 12.3456}, + # ] + # + reduced = {} + for i in range(0, len(fees)): + fee = fees[i] + code = self.safe_string(fee, 'currency') + feeCurrencyCode = code if (code is not None) else str(i) + if feeCurrencyCode is not None: + rate = self.safe_string(fee, 'rate') + cost = self.safe_string(fee, 'cost') + if cost is None: + # omit None cost, does not make sense, however, don't omit '0' costs, still make sense + continue + if not (feeCurrencyCode in reduced): + reduced[feeCurrencyCode] = {} + rateKey = '' if (rate is None) else rate + if rateKey in reduced[feeCurrencyCode]: + reduced[feeCurrencyCode][rateKey]['cost'] = Precise.string_add(reduced[feeCurrencyCode][rateKey]['cost'], cost) + else: + reduced[feeCurrencyCode][rateKey] = { + 'currency': code, + 'cost': cost, + } + if rate is not None: + reduced[feeCurrencyCode][rateKey]['rate'] = rate + result = [] + feeValues = list(reduced.values()) + for i in range(0, len(feeValues)): + reducedFeeValues = list(feeValues[i].values()) + result = self.array_concat(result, reducedFeeValues) + return result + + def safe_ticker(self, ticker: dict, market: Market = None): + open = self.omit_zero(self.safe_string(ticker, 'open')) + close = self.omit_zero(self.safe_string_2(ticker, 'close', 'last')) + change = self.omit_zero(self.safe_string(ticker, 'change')) + percentage = self.omit_zero(self.safe_string(ticker, 'percentage')) + average = self.omit_zero(self.safe_string(ticker, 'average')) + vwap = self.safe_string(ticker, 'vwap') + baseVolume = self.safe_string(ticker, 'baseVolume') + quoteVolume = self.safe_string(ticker, 'quoteVolume') + if vwap is None: + vwap = Precise.string_div(self.omit_zero(quoteVolume), baseVolume) + # calculate open + if change is not None: + if close is None and average is not None: + close = Precise.string_add(average, Precise.string_div(change, '2')) + if open is None and close is not None: + open = Precise.string_sub(close, change) + elif percentage is not None: + if close is None and average is not None: + openAddClose = Precise.string_mul(average, '2') + # openAddClose = open * (1 + (100 + percentage)/100) + denominator = Precise.string_add('2', Precise.string_div(percentage, '100')) + calcOpen = open if (open is not None) else Precise.string_div(openAddClose, denominator) + close = Precise.string_mul(calcOpen, Precise.string_add('1', Precise.string_div(percentage, '100'))) + if open is None and close is not None: + open = Precise.string_div(close, Precise.string_add('1', Precise.string_div(percentage, '100'))) + # change + if change is None: + if close is not None and open is not None: + change = Precise.string_sub(close, open) + elif close is not None and percentage is not None: + change = Precise.string_mul(Precise.string_div(percentage, '100'), Precise.string_div(close, '100')) + elif open is not None and percentage is not None: + change = Precise.string_mul(open, Precise.string_div(percentage, '100')) + # calculate things according to "open"(similar can be done with "close") + if open is not None: + # percentage(using change) + if percentage is None and change is not None: + percentage = Precise.string_mul(Precise.string_div(change, open), '100') + # close(using change) + if close is None and change is not None: + close = Precise.string_add(open, change) + # close(using average) + if close is None and average is not None: + close = Precise.string_mul(average, '2') + # average + if average is None and close is not None: + precision = 18 + if market is not None and self.is_tick_precision(): + marketPrecision = self.safe_dict(market, 'precision') + precisionPrice = self.safe_string(marketPrecision, 'price') + if precisionPrice is not None: + precision = self.precision_from_string(precisionPrice) + average = Precise.string_div(Precise.string_add(open, close), '2', precision) + # timestamp and symbol operations don't belong in safeTicker + # they should be done in the derived classes + closeParsed = self.parse_number(self.omit_zero(close)) + return self.extend(ticker, { + 'bid': self.parse_number(self.omit_zero(self.safe_string(ticker, 'bid'))), + 'bidVolume': self.safe_number(ticker, 'bidVolume'), + 'ask': self.parse_number(self.omit_zero(self.safe_string(ticker, 'ask'))), + 'askVolume': self.safe_number(ticker, 'askVolume'), + 'high': self.parse_number(self.omit_zero(self.safe_string(ticker, 'high'))), + 'low': self.parse_number(self.omit_zero(self.safe_string(ticker, 'low'))), + 'open': self.parse_number(self.omit_zero(open)), + 'close': closeParsed, + 'last': closeParsed, + 'change': self.parse_number(change), + 'percentage': self.parse_number(percentage), + 'average': self.parse_number(average), + 'vwap': self.parse_number(vwap), + 'baseVolume': self.parse_number(baseVolume), + 'quoteVolume': self.parse_number(quoteVolume), + 'previousClose': self.safe_number(ticker, 'previousClose'), + 'indexPrice': self.safe_number(ticker, 'indexPrice'), + 'markPrice': self.safe_number(ticker, 'markPrice'), + }) + + def fetch_borrow_rate(self, code: str, amount: float, params={}): + raise NotSupported(self.id + ' fetchBorrowRate is deprecated, please use fetchCrossBorrowRate or fetchIsolatedBorrowRate instead') + + def repay_cross_margin(self, code: str, amount: float, params={}): + raise NotSupported(self.id + ' repayCrossMargin is not support yet') + + def repay_isolated_margin(self, symbol: str, code: str, amount: float, params={}): + raise NotSupported(self.id + ' repayIsolatedMargin is not support yet') + + def borrow_cross_margin(self, code: str, amount: float, params={}): + raise NotSupported(self.id + ' borrowCrossMargin is not support yet') + + def borrow_isolated_margin(self, symbol: str, code: str, amount: float, params={}): + raise NotSupported(self.id + ' borrowIsolatedMargin is not support yet') + + def borrow_margin(self, code: str, amount: float, symbol: Str = None, params={}): + raise NotSupported(self.id + ' borrowMargin is deprecated, please use borrowCrossMargin or borrowIsolatedMargin instead') + + def repay_margin(self, code: str, amount: float, symbol: Str = None, params={}): + raise NotSupported(self.id + ' repayMargin is deprecated, please use repayCrossMargin or repayIsolatedMargin instead') + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}): + message = '' + if self.has['fetchTrades']: + message = '. If you want to build OHLCV candles from trade executions data, visit https://github.com/ccxt/ccxt/tree/master/examples/ and see "build-ohlcv-bars" file' + raise NotSupported(self.id + ' fetchOHLCV() is not supported yet' + message) + + def fetch_ohlcv_ws(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}): + message = '' + if self.has['fetchTradesWs']: + message = '. If you want to build OHLCV candles from trade executions data, visit https://github.com/ccxt/ccxt/tree/master/examples/ and see "build-ohlcv-bars" file' + raise NotSupported(self.id + ' fetchOHLCVWs() is not supported yet. Try using fetchOHLCV instead.' + message) + + def watch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' watchOHLCV() is not supported yet') + + def convert_trading_view_to_ohlcv(self, ohlcvs: List[List[float]], timestamp='t', open='o', high='h', low='l', close='c', volume='v', ms=False): + result = [] + timestamps = self.safe_list(ohlcvs, timestamp, []) + opens = self.safe_list(ohlcvs, open, []) + highs = self.safe_list(ohlcvs, high, []) + lows = self.safe_list(ohlcvs, low, []) + closes = self.safe_list(ohlcvs, close, []) + volumes = self.safe_list(ohlcvs, volume, []) + for i in range(0, len(timestamps)): + result.append([ + self.safe_integer(timestamps, i) if ms else self.safe_timestamp(timestamps, i), + self.safe_value(opens, i), + self.safe_value(highs, i), + self.safe_value(lows, i), + self.safe_value(closes, i), + self.safe_value(volumes, i), + ]) + return result + + def convert_ohlcv_to_trading_view(self, ohlcvs: List[List[float]], timestamp='t', open='o', high='h', low='l', close='c', volume='v', ms=False): + result = {} + result[timestamp] = [] + result[open] = [] + result[high] = [] + result[low] = [] + result[close] = [] + result[volume] = [] + for i in range(0, len(ohlcvs)): + ts = ohlcvs[i][0] if ms else self.parse_to_int(ohlcvs[i][0] / 1000) + resultTimestamp = result[timestamp] + resultTimestamp.append(ts) + resultOpen = result[open] + resultOpen.append(ohlcvs[i][1]) + resultHigh = result[high] + resultHigh.append(ohlcvs[i][2]) + resultLow = result[low] + resultLow.append(ohlcvs[i][3]) + resultClose = result[close] + resultClose.append(ohlcvs[i][4]) + resultVolume = result[volume] + resultVolume.append(ohlcvs[i][5]) + return result + + def fetch_web_endpoint(self, method, endpointMethod, returnAsJson, startRegex=None, endRegex=None): + errorMessage = '' + options = self.safe_value(self.options, method, {}) + muteOnFailure = self.safe_bool(options, 'webApiMuteFailure', True) + try: + # if it was not explicitly disabled, then don't fetch + if self.safe_bool(options, 'webApiEnable', True) is not True: + return None + maxRetries = self.safe_value(options, 'webApiRetries', 10) + response = None + retry = 0 + shouldBreak = False + while(retry < maxRetries): + try: + response = getattr(self, endpointMethod)({}) + shouldBreak = True + break + except Exception as e: + retry = retry + 1 + if retry == maxRetries: + raise e + if shouldBreak: + break # self is needed because of GO + content = response + if startRegex is not None: + splitted_by_start = content.split(startRegex) + content = splitted_by_start[1] # we need second part after start + if endRegex is not None: + splitted_by_end = content.split(endRegex) + content = splitted_by_end[0] # we need first part after start + if returnAsJson and (isinstance(content, str)): + jsoned = self.parse_json(content.strip()) # content should be trimmed before json parsing + if jsoned: + return jsoned # if parsing was not successfull, exception should be thrown + else: + raise BadResponse('could not parse the response into json') + else: + return content + except Exception as e: + errorMessage = self.id + ' ' + method + '() failed to fetch correct data from website. Probably webpage markup has been changed, breaking the page custom parser.' + if muteOnFailure: + return None + else: + raise BadResponse(errorMessage) + + def market_ids(self, symbols: Strings = None): + if symbols is None: + return symbols + result = [] + for i in range(0, len(symbols)): + result.append(self.market_id(symbols[i])) + return result + + def currency_ids(self, codes: Strings = None): + if codes is None: + return codes + result = [] + for i in range(0, len(codes)): + result.append(self.currency_id(codes[i])) + return result + + def markets_for_symbols(self, symbols: Strings = None): + if symbols is None: + return symbols + result = [] + for i in range(0, len(symbols)): + result.append(self.market(symbols[i])) + return result + + def market_symbols(self, symbols: Strings = None, type: Str = None, allowEmpty=True, sameTypeOnly=False, sameSubTypeOnly=False): + if symbols is None: + if not allowEmpty: + raise ArgumentsRequired(self.id + ' empty list of symbols is not supported') + return symbols + symbolsLength = len(symbols) + if symbolsLength == 0: + if not allowEmpty: + raise ArgumentsRequired(self.id + ' empty list of symbols is not supported') + return symbols + result = [] + marketType = None + isLinearSubType = None + for i in range(0, len(symbols)): + market = self.market(symbols[i]) + if sameTypeOnly and (marketType is not None): + if market['type'] != marketType: + raise BadRequest(self.id + ' symbols must be of the same type, either ' + marketType + ' or ' + market['type'] + '.') + if sameSubTypeOnly and (isLinearSubType is not None): + if market['linear'] != isLinearSubType: + raise BadRequest(self.id + ' symbols must be of the same subType, either linear or inverse.') + if type is not None and market['type'] != type: + raise BadRequest(self.id + ' symbols must be of the same type ' + type + '. If the type is incorrect you can change it in options or the params of the request') + marketType = market['type'] + if not market['spot']: + isLinearSubType = market['linear'] + symbol = self.safe_string(market, 'symbol', symbols[i]) + result.append(symbol) + return result + + def market_codes(self, codes: Strings = None): + if codes is None: + return codes + result = [] + for i in range(0, len(codes)): + result.append(self.common_currency_code(codes[i])) + return result + + def parse_bids_asks(self, bidasks, priceKey: IndexType = 0, amountKey: IndexType = 1, countOrIdKey: IndexType = 2): + bidasks = self.to_array(bidasks) + result = [] + for i in range(0, len(bidasks)): + result.append(self.parse_bid_ask(bidasks[i], priceKey, amountKey, countOrIdKey)) + return result + + def fetch_l2_order_book(self, symbol: str, limit: Int = None, params={}): + orderbook = self.fetch_order_book(symbol, limit, params) + return self.extend(orderbook, { + 'asks': self.sort_by(self.aggregate(orderbook['asks']), 0), + 'bids': self.sort_by(self.aggregate(orderbook['bids']), 0, True), + }) + + def filter_by_symbol(self, objects, symbol: Str = None): + if symbol is None: + return objects + result = [] + for i in range(0, len(objects)): + objectSymbol = self.safe_string(objects[i], 'symbol') + if objectSymbol == symbol: + result.append(objects[i]) + return result + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + if isinstance(ohlcv, list): + return [ + self.safe_integer(ohlcv, 0), # timestamp + self.safe_number(ohlcv, 1), # open + self.safe_number(ohlcv, 2), # high + self.safe_number(ohlcv, 3), # low + self.safe_number(ohlcv, 4), # close + self.safe_number(ohlcv, 5), # volume + ] + return ohlcv + + def network_code_to_id(self, networkCode: str, currencyCode: Str = None): + """ + @ignore + tries to convert the provided networkCode(which is expected to be an unified network code) to a network id. In order to achieve self, derived class needs to have 'options->networks' defined. + :param str networkCode: unified network code + :param str currencyCode: unified currency code, but self argument is not required by default, unless there is an exchange(like huobi) that needs an override of the method to be able to pass currencyCode argument additionally + :returns str|None: exchange-specific network id + """ + if networkCode is None: + return None + networkIdsByCodes = self.safe_value(self.options, 'networks', {}) + networkId = self.safe_string(networkIdsByCodes, networkCode) + # for example, if 'ETH' is passed for networkCode, but 'ETH' key not defined in `options->networks` object + if networkId is None: + if currencyCode is None: + currencies = list(self.currencies.values()) + for i in range(0, len(currencies)): + currency = currencies[i] + networks = self.safe_dict(currency, 'networks') + network = self.safe_dict(networks, networkCode) + networkId = self.safe_string(network, 'id') + if networkId is not None: + break + else: + # if currencyCode was provided, then we try to find if that currencyCode has a replacement(i.e. ERC20 for ETH) or is in the currency + defaultNetworkCodeReplacements = self.safe_value(self.options, 'defaultNetworkCodeReplacements', {}) + if currencyCode in defaultNetworkCodeReplacements: + # if there is a replacement for the passed networkCode, then we use it to find network-id in `options->networks` object + replacementObject = defaultNetworkCodeReplacements[currencyCode] # i.e. {'ERC20': 'ETH'} + keys = list(replacementObject.keys()) + for i in range(0, len(keys)): + key = keys[i] + value = replacementObject[key] + # if value matches to provided unified networkCode, then we use it's key to find network-id in `options->networks` object + if value == networkCode: + networkId = self.safe_string(networkIdsByCodes, key) + break + else: + # serach for network inside currency + currency = self.safe_dict(self.currencies, currencyCode) + networks = self.safe_dict(currency, 'networks') + network = self.safe_dict(networks, networkCode) + networkId = self.safe_string(network, 'id') + # if it wasn't found, we just set the provided value to network-id + if networkId is None: + networkId = networkCode + return networkId + + def network_id_to_code(self, networkId: Str = None, currencyCode: Str = None): + """ + @ignore + tries to convert the provided exchange-specific networkId to an unified network Code. In order to achieve self, derived class needs to have "options['networksById']" defined. + :param str networkId: exchange specific network id/title, like: TRON, Trc-20, usdt-erc20, etc + :param str|None currencyCode: unified currency code, but self argument is not required by default, unless there is an exchange(like huobi) that needs an override of the method to be able to pass currencyCode argument additionally + :returns str|None: unified network code + """ + if networkId is None: + return None + networkCodesByIds = self.safe_dict(self.options, 'networksById', {}) + networkCode = self.safe_string(networkCodesByIds, networkId, networkId) + # replace mainnet network-codes(i.e. ERC20->ETH) + if currencyCode is not None: + defaultNetworkCodeReplacements = self.safe_dict(self.options, 'defaultNetworkCodeReplacements', {}) + if currencyCode in defaultNetworkCodeReplacements: + replacementObject = self.safe_dict(defaultNetworkCodeReplacements, currencyCode, {}) + networkCode = self.safe_string(replacementObject, networkCode, networkCode) + return networkCode + + def handle_network_code_and_params(self, params): + networkCodeInParams = self.safe_string_2(params, 'networkCode', 'network') + if networkCodeInParams is not None: + params = self.omit(params, ['networkCode', 'network']) + # if it was not defined by user, we should not set it from 'defaultNetworks', because handleNetworkCodeAndParams is for only request-side and thus we do not fill it with anything. We can only use 'defaultNetworks' after parsing response-side + return [networkCodeInParams, params] + + def default_network_code(self, currencyCode: str): + defaultNetworkCode = None + defaultNetworks = self.safe_dict(self.options, 'defaultNetworks', {}) + if currencyCode in defaultNetworks: + # if currency had set its network in "defaultNetworks", use it + defaultNetworkCode = defaultNetworks[currencyCode] + else: + # otherwise, try to use the global-scope 'defaultNetwork' value(even if that network is not supported by currency, it doesn't make any problem, self will be just used "at first" if currency supports self network at all) + defaultNetwork = self.safe_string(self.options, 'defaultNetwork') + if defaultNetwork is not None: + defaultNetworkCode = defaultNetwork + return defaultNetworkCode + + def select_network_code_from_unified_networks(self, currencyCode, networkCode, indexedNetworkEntries): + return self.select_network_key_from_networks(currencyCode, networkCode, indexedNetworkEntries, True) + + def select_network_id_from_raw_networks(self, currencyCode, networkCode, indexedNetworkEntries): + return self.select_network_key_from_networks(currencyCode, networkCode, indexedNetworkEntries, False) + + def select_network_key_from_networks(self, currencyCode, networkCode, indexedNetworkEntries, isIndexedByUnifiedNetworkCode=False): + # self method is used against raw & unparse network entries, which are just indexed by network id + chosenNetworkId = None + availableNetworkIds = list(indexedNetworkEntries.keys()) + responseNetworksLength = len(availableNetworkIds) + if networkCode is not None: + if responseNetworksLength == 0: + raise NotSupported(self.id + ' - ' + networkCode + ' network did not return any result for ' + currencyCode) + else: + # if networkCode was provided by user, we should check it after response, referenced exchange doesn't support network-code during request + networkIdOrCode = networkCode if isIndexedByUnifiedNetworkCode else self.network_code_to_id(networkCode, currencyCode) + if networkIdOrCode in indexedNetworkEntries: + chosenNetworkId = networkIdOrCode + else: + raise NotSupported(self.id + ' - ' + networkIdOrCode + ' network was not found for ' + currencyCode + ', use one of ' + ', '.join(availableNetworkIds)) + else: + if responseNetworksLength == 0: + raise NotSupported(self.id + ' - no networks were returned for ' + currencyCode) + else: + # if networkCode was not provided by user, then we try to use the default network(if it was defined in "defaultNetworks"), otherwise, we just return the first network entry + defaultNetworkCode = self.default_network_code(currencyCode) + defaultNetworkId = defaultNetworkCode if isIndexedByUnifiedNetworkCode else self.network_code_to_id(defaultNetworkCode, currencyCode) + if defaultNetworkId in indexedNetworkEntries: + return defaultNetworkId + raise NotSupported(self.id + ' - can not determine the default network, please pass param["network"] one from : ' + ', '.join(availableNetworkIds)) + return chosenNetworkId + + def safe_number_2(self, dictionary: object, key1: IndexType, key2: IndexType, d=None): + value = self.safe_string_2(dictionary, key1, key2) + return self.parse_number(value, d) + + def parse_order_book(self, orderbook: object, symbol: str, timestamp: Int = None, bidsKey='bids', asksKey='asks', priceKey: IndexType = 0, amountKey: IndexType = 1, countOrIdKey: IndexType = 2): + bids = self.parse_bids_asks(self.safe_value(orderbook, bidsKey, []), priceKey, amountKey, countOrIdKey) + asks = self.parse_bids_asks(self.safe_value(orderbook, asksKey, []), priceKey, amountKey, countOrIdKey) + return { + 'symbol': symbol, + 'bids': self.sort_by(bids, 0, True), + 'asks': self.sort_by(asks, 0), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'nonce': None, + } + + def parse_ohlcvs(self, ohlcvs: List[object], market: Any = None, timeframe: str = '1m', since: Int = None, limit: Int = None, tail: Bool = False): + results = [] + for i in range(0, len(ohlcvs)): + results.append(self.parse_ohlcv(ohlcvs[i], market)) + sorted = self.sort_by(results, 0) + return self.filter_by_since_limit(sorted, since, limit, 0, tail) + + def parse_leverage_tiers(self, response: Any, symbols: List[str] = None, marketIdKey=None): + # marketIdKey should only be None when response is a dictionary. + symbols = self.market_symbols(symbols) + tiers = {} + symbolsLength = 0 + if symbols is not None: + symbolsLength = len(symbols) + noSymbols = (symbols is None) or (symbolsLength == 0) + if isinstance(response, list): + for i in range(0, len(response)): + item = response[i] + id = self.safe_string(item, marketIdKey) + market = self.safe_market(id, None, None, 'swap') + symbol = market['symbol'] + contract = self.safe_bool(market, 'contract', False) + if contract and (noSymbols or self.in_array(symbol, symbols)): + tiers[symbol] = self.parse_market_leverage_tiers(item, market) + else: + keys = list(response.keys()) + for i in range(0, len(keys)): + marketId = keys[i] + item = response[marketId] + market = self.safe_market(marketId, None, None, 'swap') + symbol = market['symbol'] + contract = self.safe_bool(market, 'contract', False) + if contract and (noSymbols or self.in_array(symbol, symbols)): + tiers[symbol] = self.parse_market_leverage_tiers(item, market) + return tiers + + def load_trading_limits(self, symbols: Strings = None, reload=False, params={}): + if self.has['fetchTradingLimits']: + if reload or not ('limitsLoaded' in self.options): + response = self.fetch_trading_limits(symbols) + for i in range(0, len(symbols)): + symbol = symbols[i] + self.markets[symbol] = self.deep_extend(self.markets[symbol], response[symbol]) + self.options['limitsLoaded'] = self.milliseconds() + return self.markets + + def safe_position(self, position: dict): + # simplified version of: /pull/12765/ + unrealizedPnlString = self.safe_string(position, 'unrealisedPnl') + initialMarginString = self.safe_string(position, 'initialMargin') + # + # PERCENTAGE + # + percentage = self.safe_value(position, 'percentage') + if (percentage is None) and (unrealizedPnlString is not None) and (initialMarginString is not None): + # was done in all implementations( aax, btcex, bybit, deribit, ftx, gate, kucoinfutures, phemex ) + percentageString = Precise.string_mul(Precise.string_div(unrealizedPnlString, initialMarginString, 4), '100') + position['percentage'] = self.parse_number(percentageString) + # if contractSize is None get from market + contractSize = self.safe_number(position, 'contractSize') + symbol = self.safe_string(position, 'symbol') + market = None + if symbol is not None: + market = self.safe_value(self.markets, symbol) + if contractSize is None and market is not None: + contractSize = self.safe_number(market, 'contractSize') + position['contractSize'] = contractSize + return position + + def parse_positions(self, positions: List[Any], symbols: List[str] = None, params={}): + symbols = self.market_symbols(symbols) + positions = self.to_array(positions) + result = [] + for i in range(0, len(positions)): + position = self.extend(self.parse_position(positions[i], None), params) + result.append(position) + return self.filter_by_array_positions(result, 'symbol', symbols, False) + + def parse_accounts(self, accounts: List[Any], params={}): + accounts = self.to_array(accounts) + result = [] + for i in range(0, len(accounts)): + account = self.extend(self.parse_account(accounts[i]), params) + result.append(account) + return result + + def parse_trades_helper(self, isWs: bool, trades: List[Any], market: Market = None, since: Int = None, limit: Int = None, params={}): + trades = self.to_array(trades) + result = [] + for i in range(0, len(trades)): + parsed = None + if isWs: + parsed = self.parse_ws_trade(trades[i], market) + else: + parsed = self.parse_trade(trades[i], market) + trade = self.extend(parsed, params) + result.append(trade) + result = self.sort_by_2(result, 'timestamp', 'id') + symbol = market['symbol'] if (market is not None) else None + return self.filter_by_symbol_since_limit(result, symbol, since, limit) + + def parse_trades(self, trades: List[Any], market: Market = None, since: Int = None, limit: Int = None, params={}): + return self.parse_trades_helper(False, trades, market, since, limit, params) + + def parse_ws_trades(self, trades: List[Any], market: Market = None, since: Int = None, limit: Int = None, params={}): + return self.parse_trades_helper(True, trades, market, since, limit, params) + + def parse_transactions(self, transactions: List[Any], currency: Currency = None, since: Int = None, limit: Int = None, params={}): + transactions = self.to_array(transactions) + result = [] + for i in range(0, len(transactions)): + transaction = self.extend(self.parse_transaction(transactions[i], currency), params) + result.append(transaction) + result = self.sort_by(result, 'timestamp') + code = currency['code'] if (currency is not None) else None + return self.filter_by_currency_since_limit(result, code, since, limit) + + def parse_transfers(self, transfers: List[Any], currency: Currency = None, since: Int = None, limit: Int = None, params={}): + transfers = self.to_array(transfers) + result = [] + for i in range(0, len(transfers)): + transfer = self.extend(self.parse_transfer(transfers[i], currency), params) + result.append(transfer) + result = self.sort_by(result, 'timestamp') + code = currency['code'] if (currency is not None) else None + return self.filter_by_currency_since_limit(result, code, since, limit) + + def parse_ledger(self, data, currency: Currency = None, since: Int = None, limit: Int = None, params={}): + result = [] + arrayData = self.to_array(data) + for i in range(0, len(arrayData)): + itemOrItems = self.parse_ledger_entry(arrayData[i], currency) + if isinstance(itemOrItems, list): + for j in range(0, len(itemOrItems)): + result.append(self.extend(itemOrItems[j], params)) + else: + result.append(self.extend(itemOrItems, params)) + result = self.sort_by(result, 'timestamp') + code = currency['code'] if (currency is not None) else None + return self.filter_by_currency_since_limit(result, code, since, limit) + + def nonce(self): + return self.seconds() + + def set_headers(self, headers): + return headers + + def currency_id(self, code: str): + currency = self.safe_dict(self.currencies, code) + if currency is None: + currency = self.safe_currency(code) + if currency is not None: + return currency['id'] + return code + + def market_id(self, symbol: str): + market = self.market(symbol) + if market is not None: + return market['id'] + return symbol + + def symbol(self, symbol: str): + market = self.market(symbol) + return self.safe_string(market, 'symbol', symbol) + + def handle_param_string(self, params: object, paramName: str, defaultValue: Str = None): + value = self.safe_string(params, paramName, defaultValue) + if value is not None: + params = self.omit(params, paramName) + return [value, params] + + def handle_param_string_2(self, params: object, paramName1: str, paramName2: str, defaultValue: Str = None): + value = self.safe_string_2(params, paramName1, paramName2, defaultValue) + if value is not None: + params = self.omit(params, [paramName1, paramName2]) + return [value, params] + + def handle_param_integer(self, params: object, paramName: str, defaultValue: Int = None): + value = self.safe_integer(params, paramName, defaultValue) + if value is not None: + params = self.omit(params, paramName) + return [value, params] + + def handle_param_integer_2(self, params: object, paramName1: str, paramName2: str, defaultValue: Int = None): + value = self.safe_integer_2(params, paramName1, paramName2, defaultValue) + if value is not None: + params = self.omit(params, [paramName1, paramName2]) + return [value, params] + + def handle_param_bool(self, params: object, paramName: str, defaultValue: Bool = None): + value = self.safe_bool(params, paramName, defaultValue) + if value is not None: + params = self.omit(params, paramName) + return [value, params] + + def handle_param_bool_2(self, params: object, paramName1: str, paramName2: str, defaultValue: Bool = None): + value = self.safe_bool_2(params, paramName1, paramName2, defaultValue) + if value is not None: + params = self.omit(params, [paramName1, paramName2]) + return [value, params] + + def handle_request_network(self, params: dict, request: dict, exchangeSpecificKey: str, currencyCode: Str = None, isRequired: bool = False): + """ + :param dict params: - extra parameters + :param dict request: - existing dictionary of request + :param str exchangeSpecificKey: - the key for chain id to be set in request + :param dict currencyCode: - (optional) existing dictionary of request + :param boolean isRequired: - (optional) whether that param is required to be present + :returns dict[]: - returns [request, params] where request is the modified request object and params is the modified params object + """ + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + if networkCode is not None: + request[exchangeSpecificKey] = self.network_code_to_id(networkCode, currencyCode) + elif isRequired: + raise ArgumentsRequired(self.id + ' - "network" param is required for self request') + return [request, params] + + def resolve_path(self, path, params): + return [ + self.implode_params(path, params), + self.omit(params, self.extract_params(path)), + ] + + def get_list_from_object_values(self, objects, key: IndexType): + newArray = objects + if not isinstance(objects, list): + newArray = self.to_array(objects) + results = [] + for i in range(0, len(newArray)): + results.append(newArray[i][key]) + return results + + def get_symbols_for_market_type(self, marketType: Str = None, subType: Str = None, symbolWithActiveStatus: bool = True, symbolWithUnknownStatus: bool = True): + filteredMarkets = self.markets + if marketType is not None: + filteredMarkets = self.filter_by(filteredMarkets, 'type', marketType) + if subType is not None: + self.check_required_argument('getSymbolsForMarketType', subType, 'subType', ['linear', 'inverse', 'quanto']) + filteredMarkets = self.filter_by(filteredMarkets, 'subType', subType) + activeStatuses = [] + if symbolWithActiveStatus: + activeStatuses.append(True) + if symbolWithUnknownStatus: + activeStatuses.append(None) + filteredMarkets = self.filter_by_array(filteredMarkets, 'active', activeStatuses, False) + return self.get_list_from_object_values(filteredMarkets, 'symbol') + + def filter_by_array(self, objects, key: IndexType, values=None, indexed=True): + objects = self.to_array(objects) + # return all of them if no values were passed + if values is None or not values: + # return self.index_by(objects, key) if indexed else objects + if indexed: + return self.index_by(objects, key) + else: + return objects + results = [] + for i in range(0, len(objects)): + if self.in_array(objects[i][key], values): + results.append(objects[i]) + # return self.index_by(results, key) if indexed else results + if indexed: + return self.index_by(results, key) + return results + + def fetch2(self, path, api: Any = 'public', method='GET', params={}, headers: Any = None, body: Any = None, config={}): + if self.enableRateLimit: + cost = self.calculate_rate_limiter_cost(api, method, path, params, config) + self.throttle(cost) + retries = None + retries, params = self.handle_option_and_params(params, path, 'maxRetriesOnFailure', 0) + retryDelay = None + retryDelay, params = self.handle_option_and_params(params, path, 'maxRetriesOnFailureDelay', 0) + self.lastRestRequestTimestamp = self.milliseconds() + request = self.sign(path, api, method, params, headers, body) + self.last_request_headers = request['headers'] + self.last_request_body = request['body'] + self.last_request_url = request['url'] + for i in range(0, retries + 1): + try: + return self.fetch(request['url'], request['method'], request['headers'], request['body']) + except Exception as e: + if isinstance(e, OperationFailed): + if i < retries: + if self.verbose: + self.log('Request failed with the error: ' + str(e) + ', retrying ' + (i + str(1)) + ' of ' + str(retries) + '...') + if (retryDelay is not None) and (retryDelay != 0): + self.sleep(retryDelay) + else: + raise e + else: + raise e + return None # self line is never reached, but exists for c# value return requirement + + def request(self, path, api: Any = 'public', method='GET', params={}, headers: Any = None, body: Any = None, config={}): + return self.fetch2(path, api, method, params, headers, body, config) + + def load_accounts(self, reload=False, params={}): + if reload: + self.accounts = self.fetch_accounts(params) + else: + if self.accounts: + return self.accounts + else: + self.accounts = self.fetch_accounts(params) + self.accountsById = self.index_by(self.accounts, 'id') + return self.accounts + + def build_ohlcvc(self, trades: List[Trade], timeframe: str = '1m', since: float = 0, limit: float = 2147483647): + # given a sorted arrays of trades(recent last) and a timeframe builds an array of OHLCV candles + # note, default limit value(2147483647) is max int32 value + ms = self.parse_timeframe(timeframe) * 1000 + ohlcvs = [] + i_timestamp = 0 + # open = 1 + i_high = 2 + i_low = 3 + i_close = 4 + i_volume = 5 + i_count = 6 + tradesLength = len(trades) + oldest = min(tradesLength, limit) + options = self.safe_dict(self.options, 'buildOHLCVC', {}) + skipZeroPrices = self.safe_bool(options, 'skipZeroPrices', True) + for i in range(0, oldest): + trade = trades[i] + ts = trade['timestamp'] + price = trade['price'] + if ts < since: + continue + openingTime = int(math.floor(ts / ms)) * ms # shift to the edge of m/h/d(but not M) + if openingTime < since: # we don't need bars, that have opening time earlier than requested + continue + ohlcv_length = len(ohlcvs) + candle = ohlcv_length - 1 + if skipZeroPrices and not (price > 0) and not (price < 0): + continue + isFirstCandle = candle == -1 + if isFirstCandle or openingTime >= self.sum(ohlcvs[candle][i_timestamp], ms): + # moved to a new timeframe -> create a new candle from opening trade + ohlcvs.append([ + openingTime, # timestamp + price, # O + price, # H + price, # L + price, # C + trade['amount'], # V + 1, # count + ]) + else: + # still processing the same timeframe -> update opening trade + ohlcvs[candle][i_high] = max(ohlcvs[candle][i_high], price) + ohlcvs[candle][i_low] = min(ohlcvs[candle][i_low], price) + ohlcvs[candle][i_close] = price + ohlcvs[candle][i_volume] = self.sum(ohlcvs[candle][i_volume], trade['amount']) + ohlcvs[candle][i_count] = self.sum(ohlcvs[candle][i_count], 1) + return ohlcvs + + def parse_trading_view_ohlcv(self, ohlcvs, market=None, timeframe='1m', since: Int = None, limit: Int = None): + result = self.convert_trading_view_to_ohlcv(ohlcvs) + return self.parse_ohlcvs(result, market, timeframe, since, limit) + + def edit_limit_buy_order(self, id: str, symbol: str, amount: float, price: Num = None, params={}): + return self.edit_limit_order(id, symbol, 'buy', amount, price, params) + + def edit_limit_sell_order(self, id: str, symbol: str, amount: float, price: Num = None, params={}): + return self.edit_limit_order(id, symbol, 'sell', amount, price, params) + + def edit_limit_order(self, id: str, symbol: str, side: OrderSide, amount: float, price: Num = None, params={}): + return self.edit_order(id, symbol, 'limit', side, amount, price, params) + + def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + self.cancel_order(id, symbol) + return self.create_order(symbol, type, side, amount, price, params) + + def edit_order_with_client_order_id(self, clientOrderId: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + return self.edit_order('', symbol, type, side, amount, price, self.extend({'clientOrderId': clientOrderId}, params)) + + def edit_order_ws(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + self.cancel_order_ws(id, symbol) + return self.create_order_ws(symbol, type, side, amount, price, params) + + def fetch_position(self, symbol: str, params={}): + raise NotSupported(self.id + ' fetchPosition() is not supported yet') + + def fetch_position_ws(self, symbol: str, params={}): + raise NotSupported(self.id + ' fetchPositionWs() is not supported yet') + + def watch_position(self, symbol: Str = None, params={}): + raise NotSupported(self.id + ' watchPosition() is not supported yet') + + def watch_positions(self, symbols: Strings = None, since: Int = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' watchPositions() is not supported yet') + + def watch_position_for_symbols(self, symbols: Strings = None, since: Int = None, limit: Int = None, params={}): + return self.watch_positions(symbols, since, limit, params) + + def fetch_positions_for_symbol(self, symbol: str, params={}): + """ + fetches all open positions for specific symbol, unlike fetchPositions(which is designed to work with multiple symbols) so self method might be preffered for one-market position, because of less rate-limit consumption and speed + :param str symbol: unified market symbol + :param dict params: extra parameters specific to the endpoint + :returns dict[]: a list of `position structure ` with maximum 3 items - possible one position for "one-way" mode, and possible two positions(long & short) for "two-way"(a.k.a. hedge) mode + """ + raise NotSupported(self.id + ' fetchPositionsForSymbol() is not supported yet') + + def fetch_positions_for_symbol_ws(self, symbol: str, params={}): + """ + fetches all open positions for specific symbol, unlike fetchPositions(which is designed to work with multiple symbols) so self method might be preffered for one-market position, because of less rate-limit consumption and speed + :param str symbol: unified market symbol + :param dict params: extra parameters specific to the endpoint + :returns dict[]: a list of `position structure ` with maximum 3 items - possible one position for "one-way" mode, and possible two positions(long & short) for "two-way"(a.k.a. hedge) mode + """ + raise NotSupported(self.id + ' fetchPositionsForSymbol() is not supported yet') + + def fetch_positions(self, symbols: Strings = None, params={}): + raise NotSupported(self.id + ' fetchPositions() is not supported yet') + + def fetch_positions_ws(self, symbols: Strings = None, params={}): + raise NotSupported(self.id + ' fetchPositions() is not supported yet') + + def fetch_positions_risk(self, symbols: Strings = None, params={}): + raise NotSupported(self.id + ' fetchPositionsRisk() is not supported yet') + + def fetch_bids_asks(self, symbols: Strings = None, params={}): + raise NotSupported(self.id + ' fetchBidsAsks() is not supported yet') + + def fetch_borrow_interest(self, code: Str = None, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' fetchBorrowInterest() is not supported yet') + + def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' fetchLedger() is not supported yet') + + def fetch_ledger_entry(self, id: str, code: Str = None, params={}): + raise NotSupported(self.id + ' fetchLedgerEntry() is not supported yet') + + def parse_bid_ask(self, bidask, priceKey: IndexType = 0, amountKey: IndexType = 1, countOrIdKey: IndexType = 2): + price = self.safe_number(bidask, priceKey) + amount = self.safe_number(bidask, amountKey) + countOrId = self.safe_integer(bidask, countOrIdKey) + bidAsk = [price, amount] + if countOrId is not None: + bidAsk.append(countOrId) + return bidAsk + + def safe_currency(self, currencyId: Str, currency: Currency = None): + if (currencyId is None) and (currency is not None): + return currency + if (self.currencies_by_id is not None) and (currencyId in self.currencies_by_id) and (self.currencies_by_id[currencyId] is not None): + return self.currencies_by_id[currencyId] + code = currencyId + if currencyId is not None: + code = self.common_currency_code(currencyId.upper()) + return self.safe_currency_structure({ + 'id': currencyId, + 'code': code, + 'precision': None, + }) + + def safe_market(self, marketId: Str = None, market: Market = None, delimiter: Str = None, marketType: Str = None): + result = self.safe_market_structure({ + 'symbol': marketId, + 'marketId': marketId, + }) + if marketId is not None: + if (self.markets_by_id is not None) and (marketId in self.markets_by_id): + markets = self.markets_by_id[marketId] + numMarkets = len(markets) + if numMarkets == 1: + return markets[0] + else: + if marketType is None: + if market is None: + raise ArgumentsRequired(self.id + ' safeMarket() requires a fourth argument for ' + marketId + ' to disambiguate between different markets with the same market id') + else: + marketType = market['type'] + for i in range(0, len(markets)): + currentMarket = markets[i] + if currentMarket[marketType]: + return currentMarket + elif delimiter is not None and delimiter != '': + parts = marketId.split(delimiter) + partsLength = len(parts) + if partsLength == 2: + result['baseId'] = self.safe_string(parts, 0) + result['quoteId'] = self.safe_string(parts, 1) + result['base'] = self.safe_currency_code(result['baseId']) + result['quote'] = self.safe_currency_code(result['quoteId']) + result['symbol'] = result['base'] + '/' + result['quote'] + return result + else: + return result + if market is not None: + return market + return result + + def market_or_null(self, symbol: str): + if symbol is None: + return None + return self.market(symbol) + + def check_required_credentials(self, error=True): + """ + @ignore + :param boolean error: raise an error that a credential is required if True + :returns boolean: True if all required credentials have been set, otherwise False or an error is thrown is param error=true + """ + keys = list(self.requiredCredentials.keys()) + for i in range(0, len(keys)): + key = keys[i] + if self.requiredCredentials[key] and not getattr(self, key): + if error: + raise AuthenticationError(self.id + ' requires "' + key + '" credential') + else: + return False + return True + + def oath(self): + if self.twofa is not None: + return self.totp(self.twofa) + else: + raise ExchangeError(self.id + ' exchange.twofa has not been set for 2FA Two-Factor Authentication') + + def fetch_balance(self, params={}): + raise NotSupported(self.id + ' fetchBalance() is not supported yet') + + def fetch_balance_ws(self, params={}): + raise NotSupported(self.id + ' fetchBalanceWs() is not supported yet') + + def parse_balance(self, response): + raise NotSupported(self.id + ' parseBalance() is not supported yet') + + def watch_balance(self, params={}): + raise NotSupported(self.id + ' watchBalance() is not supported yet') + + def fetch_partial_balance(self, part, params={}): + balance = self.fetch_balance(params) + return balance[part] + + def fetch_free_balance(self, params={}): + return self.fetch_partial_balance('free', params) + + def fetch_used_balance(self, params={}): + return self.fetch_partial_balance('used', params) + + def fetch_total_balance(self, params={}): + return self.fetch_partial_balance('total', params) + + def fetch_status(self, params={}): + raise NotSupported(self.id + ' fetchStatus() is not supported yet') + + def fetch_transaction_fee(self, code: str, params={}): + if not self.has['fetchTransactionFees']: + raise NotSupported(self.id + ' fetchTransactionFee() is not supported yet') + return self.fetch_transaction_fees([code], params) + + def fetch_transaction_fees(self, codes: Strings = None, params={}): + raise NotSupported(self.id + ' fetchTransactionFees() is not supported yet') + + def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}): + raise NotSupported(self.id + ' fetchDepositWithdrawFees() is not supported yet') + + def fetch_deposit_withdraw_fee(self, code: str, params={}): + if not self.has['fetchDepositWithdrawFees']: + raise NotSupported(self.id + ' fetchDepositWithdrawFee() is not supported yet') + fees = self.fetch_deposit_withdraw_fees([code], params) + return self.safe_value(fees, code) + + def get_supported_mapping(self, key, mapping={}): + if key in mapping: + return mapping[key] + else: + raise NotSupported(self.id + ' ' + key + ' does not have a value in mapping') + + def fetch_cross_borrow_rate(self, code: str, params={}): + self.load_markets() + if not self.has['fetchBorrowRates']: + raise NotSupported(self.id + ' fetchCrossBorrowRate() is not supported yet') + borrowRates = self.fetch_cross_borrow_rates(params) + rate = self.safe_value(borrowRates, code) + if rate is None: + raise ExchangeError(self.id + ' fetchCrossBorrowRate() could not find the borrow rate for currency code ' + code) + return rate + + def fetch_isolated_borrow_rate(self, symbol: str, params={}): + self.load_markets() + if not self.has['fetchBorrowRates']: + raise NotSupported(self.id + ' fetchIsolatedBorrowRate() is not supported yet') + borrowRates = self.fetch_isolated_borrow_rates(params) + rate = self.safe_dict(borrowRates, symbol) + if rate is None: + raise ExchangeError(self.id + ' fetchIsolatedBorrowRate() could not find the borrow rate for market symbol ' + symbol) + return rate + + def handle_option_and_params(self, params: object, methodName: str, optionName: str, defaultValue=None): + # This method can be used to obtain method specific properties, i.e: self.handle_option_and_params(params, 'fetchPosition', 'marginMode', 'isolated') + defaultOptionName = 'default' + self.capitalize(optionName) # we also need to check the 'defaultXyzWhatever' + # check if params contain the key + value = self.safe_value_2(params, optionName, defaultOptionName) + if value is not None: + params = self.omit(params, [optionName, defaultOptionName]) + else: + # handle routed methods like "watchTrades > watchTradesForSymbols"(or "watchTicker > watchTickers") + methodName, params = self.handle_param_string(params, 'callerMethodName', methodName) + # check if exchange has properties for self method + exchangeWideMethodOptions = self.safe_value(self.options, methodName) + if exchangeWideMethodOptions is not None: + # check if the option is defined inside self method's props + value = self.safe_value_2(exchangeWideMethodOptions, optionName, defaultOptionName) + if value is None: + # if it's still None, check if global exchange-wide option exists + value = self.safe_value_2(self.options, optionName, defaultOptionName) + # if it's still None, use the default value + value = value if (value is not None) else defaultValue + return [value, params] + + def handle_option_and_params_2(self, params: object, methodName1: str, optionName1: str, optionName2: str, defaultValue=None): + value = None + value, params = self.handle_option_and_params(params, methodName1, optionName1) + if value is not None: + # omit optionName2 too from params + params = self.omit(params, optionName2) + return [value, params] + # if still None, try optionName2 + value2 = None + value2, params = self.handle_option_and_params(params, methodName1, optionName2, defaultValue) + return [value2, params] + + def handle_option(self, methodName: str, optionName: str, defaultValue=None): + res = self.handle_option_and_params({}, methodName, optionName, defaultValue) + return self.safe_value(res, 0) + + def handle_market_type_and_params(self, methodName: str, market: Market = None, params={}, defaultValue=None): + """ + @ignore + @param methodName the method calling handleMarketTypeAndParams + :param Market market: + :param dict params: + :param str [params.type]: type assigned by user + :param str [params.defaultType]: same.type + :param str [defaultValue]: assigned programatically in the method calling handleMarketTypeAndParams + :returns [str, dict]: the market type and params with type and defaultType omitted + """ + # type from param + type = self.safe_string_2(params, 'defaultType', 'type') + if type is not None: + params = self.omit(params, ['defaultType', 'type']) + return [type, params] + # type from market + if market is not None: + return [market['type'], params] + # type from default-argument + if defaultValue is not None: + return [defaultValue, params] + methodOptions = self.safe_dict(self.options, methodName) + if methodOptions is not None: + if isinstance(methodOptions, str): + return [methodOptions, params] + else: + typeFromMethod = self.safe_string_2(methodOptions, 'defaultType', 'type') + if typeFromMethod is not None: + return [typeFromMethod, params] + defaultType = self.safe_string_2(self.options, 'defaultType', 'type', 'spot') + return [defaultType, params] + + def handle_sub_type_and_params(self, methodName: str, market=None, params={}, defaultValue=None): + subType = None + # if set in params, it takes precedence + subTypeInParams = self.safe_string_2(params, 'subType', 'defaultSubType') + # avoid omitting if it's not present + if subTypeInParams is not None: + subType = subTypeInParams + params = self.omit(params, ['subType', 'defaultSubType']) + else: + # at first, check from market object + if market is not None: + if market['linear']: + subType = 'linear' + elif market['inverse']: + subType = 'inverse' + # if it was not defined in market object + if subType is None: + values = self.handle_option_and_params({}, methodName, 'subType', defaultValue) # no need to re-test params here + subType = values[0] + return [subType, params] + + def handle_margin_mode_and_params(self, methodName: str, params={}, defaultValue=None): + """ + @ignore + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Array: the marginMode in lowercase by params["marginMode"], params["defaultMarginMode"] self.options["marginMode"] or self.options["defaultMarginMode"] + """ + return self.handle_option_and_params(params, methodName, 'marginMode', defaultValue) + + def throw_exactly_matched_exception(self, exact, string, message): + if string is None: + return + if string in exact: + raise exact[string](message) + + def throw_broadly_matched_exception(self, broad, string, message): + broadKey = self.find_broadly_matched_key(broad, string) + if broadKey is not None: + raise broad[broadKey](message) + + def find_broadly_matched_key(self, broad, string): + # a helper for matching error strings exactly vs broadly + keys = list(broad.keys()) + for i in range(0, len(keys)): + key = keys[i] + if string is not None: # #issues/12698 + if string.find(key) >= 0: + return key + return None + + def handle_errors(self, statusCode: int, statusText: str, url: str, method: str, responseHeaders: dict, responseBody: str, response, requestHeaders, requestBody): + # it is a stub method that must be overrided in the derived exchange classes + # raise NotSupported(self.id + ' handleErrors() not implemented yet') + return None + + def calculate_rate_limiter_cost(self, api, method, path, params, config={}): + return self.safe_value(config, 'cost', 1) + + def fetch_ticker(self, symbol: str, params={}): + if self.has['fetchTickers']: + self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + tickers = self.fetch_tickers([symbol], params) + ticker = self.safe_dict(tickers, symbol) + if ticker is None: + raise NullResponse(self.id + ' fetchTickers() could not find a ticker for ' + symbol) + else: + return ticker + else: + raise NotSupported(self.id + ' fetchTicker() is not supported yet') + + def fetch_mark_price(self, symbol: str, params={}): + if self.has['fetchMarkPrices']: + self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + tickers = self.fetch_mark_prices([symbol], params) + ticker = self.safe_dict(tickers, symbol) + if ticker is None: + raise NullResponse(self.id + ' fetchMarkPrices() could not find a ticker for ' + symbol) + else: + return ticker + else: + raise NotSupported(self.id + ' fetchMarkPrices() is not supported yet') + + def fetch_ticker_ws(self, symbol: str, params={}): + if self.has['fetchTickersWs']: + self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + tickers = self.fetch_tickers_ws([symbol], params) + ticker = self.safe_dict(tickers, symbol) + if ticker is None: + raise NullResponse(self.id + ' fetchTickerWs() could not find a ticker for ' + symbol) + else: + return ticker + else: + raise NotSupported(self.id + ' fetchTickerWs() is not supported yet') + + def watch_ticker(self, symbol: str, params={}): + raise NotSupported(self.id + ' watchTicker() is not supported yet') + + def fetch_tickers(self, symbols: Strings = None, params={}): + raise NotSupported(self.id + ' fetchTickers() is not supported yet') + + def fetch_mark_prices(self, symbols: Strings = None, params={}): + raise NotSupported(self.id + ' fetchMarkPrices() is not supported yet') + + def fetch_tickers_ws(self, symbols: Strings = None, params={}): + raise NotSupported(self.id + ' fetchTickers() is not supported yet') + + def fetch_order_books(self, symbols: Strings = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' fetchOrderBooks() is not supported yet') + + def watch_bids_asks(self, symbols: Strings = None, params={}): + raise NotSupported(self.id + ' watchBidsAsks() is not supported yet') + + def watch_tickers(self, symbols: Strings = None, params={}): + raise NotSupported(self.id + ' watchTickers() is not supported yet') + + def un_watch_tickers(self, symbols: Strings = None, params={}): + raise NotSupported(self.id + ' unWatchTickers() is not supported yet') + + def fetch_order(self, id: str, symbol: Str = None, params={}): + raise NotSupported(self.id + ' fetchOrder() is not supported yet') + + def fetch_order_with_client_order_id(self, clientOrderId: str, symbol: Str = None, params={}): + """ + create a market order by providing the symbol, side and cost + :param str clientOrderId: client order Id + :param str symbol: unified symbol of the market to create an order in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + extendedParams = self.extend(params, {'clientOrderId': clientOrderId}) + return self.fetch_order('', symbol, extendedParams) + + def fetch_order_ws(self, id: str, symbol: Str = None, params={}): + raise NotSupported(self.id + ' fetchOrderWs() is not supported yet') + + def fetch_order_status(self, id: str, symbol: Str = None, params={}): + # TODO: TypeScript: change method signature by replacing + # Promise with Promise. + order = self.fetch_order(id, symbol, params) + return order['status'] + + def fetch_unified_order(self, order, params={}): + return self.fetch_order(self.safe_string(order, 'id'), self.safe_string(order, 'symbol'), params) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + raise NotSupported(self.id + ' createOrder() is not supported yet') + + def create_convert_trade(self, id: str, fromCode: str, toCode: str, amount: Num = None, params={}): + raise NotSupported(self.id + ' createConvertTrade() is not supported yet') + + def fetch_convert_trade(self, id: str, code: Str = None, params={}): + raise NotSupported(self.id + ' fetchConvertTrade() is not supported yet') + + def fetch_convert_trade_history(self, code: Str = None, since: Int = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' fetchConvertTradeHistory() is not supported yet') + + def fetch_position_mode(self, symbol: Str = None, params={}): + raise NotSupported(self.id + ' fetchPositionMode() is not supported yet') + + def create_trailing_amount_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, trailingAmount: Num = None, trailingTriggerPrice: Num = None, params={}): + """ + create a trailing order by providing the symbol, type, side, amount, price and trailingAmount + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency, or number of contracts + :param float [price]: the price for the order to be filled at, in units of the quote currency, ignored in market orders + :param float trailingAmount: the quote amount to trail away from the current market price + :param float [trailingTriggerPrice]: the price to activate a trailing order, default uses the price argument + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + if trailingAmount is None: + raise ArgumentsRequired(self.id + ' createTrailingAmountOrder() requires a trailingAmount argument') + params['trailingAmount'] = trailingAmount + if trailingTriggerPrice is not None: + params['trailingTriggerPrice'] = trailingTriggerPrice + if self.has['createTrailingAmountOrder']: + return self.create_order(symbol, type, side, amount, price, params) + raise NotSupported(self.id + ' createTrailingAmountOrder() is not supported yet') + + def create_trailing_amount_order_ws(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, trailingAmount: Num = None, trailingTriggerPrice: Num = None, params={}): + """ + create a trailing order by providing the symbol, type, side, amount, price and trailingAmount + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency, or number of contracts + :param float [price]: the price for the order to be filled at, in units of the quote currency, ignored in market orders + :param float trailingAmount: the quote amount to trail away from the current market price + :param float [trailingTriggerPrice]: the price to activate a trailing order, default uses the price argument + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + if trailingAmount is None: + raise ArgumentsRequired(self.id + ' createTrailingAmountOrderWs() requires a trailingAmount argument') + params['trailingAmount'] = trailingAmount + if trailingTriggerPrice is not None: + params['trailingTriggerPrice'] = trailingTriggerPrice + if self.has['createTrailingAmountOrderWs']: + return self.create_order_ws(symbol, type, side, amount, price, params) + raise NotSupported(self.id + ' createTrailingAmountOrderWs() is not supported yet') + + def create_trailing_percent_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, trailingPercent: Num = None, trailingTriggerPrice: Num = None, params={}): + """ + create a trailing order by providing the symbol, type, side, amount, price and trailingPercent + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency, or number of contracts + :param float [price]: the price for the order to be filled at, in units of the quote currency, ignored in market orders + :param float trailingPercent: the percent to trail away from the current market price + :param float [trailingTriggerPrice]: the price to activate a trailing order, default uses the price argument + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + if trailingPercent is None: + raise ArgumentsRequired(self.id + ' createTrailingPercentOrder() requires a trailingPercent argument') + params['trailingPercent'] = trailingPercent + if trailingTriggerPrice is not None: + params['trailingTriggerPrice'] = trailingTriggerPrice + if self.has['createTrailingPercentOrder']: + return self.create_order(symbol, type, side, amount, price, params) + raise NotSupported(self.id + ' createTrailingPercentOrder() is not supported yet') + + def create_trailing_percent_order_ws(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, trailingPercent: Num = None, trailingTriggerPrice: Num = None, params={}): + """ + create a trailing order by providing the symbol, type, side, amount, price and trailingPercent + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency, or number of contracts + :param float [price]: the price for the order to be filled at, in units of the quote currency, ignored in market orders + :param float trailingPercent: the percent to trail away from the current market price + :param float [trailingTriggerPrice]: the price to activate a trailing order, default uses the price argument + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + if trailingPercent is None: + raise ArgumentsRequired(self.id + ' createTrailingPercentOrderWs() requires a trailingPercent argument') + params['trailingPercent'] = trailingPercent + if trailingTriggerPrice is not None: + params['trailingTriggerPrice'] = trailingTriggerPrice + if self.has['createTrailingPercentOrderWs']: + return self.create_order_ws(symbol, type, side, amount, price, params) + raise NotSupported(self.id + ' createTrailingPercentOrderWs() is not supported yet') + + def create_market_order_with_cost(self, symbol: str, side: OrderSide, cost: float, params={}): + """ + create a market order by providing the symbol, side and cost + :param str symbol: unified symbol of the market to create an order in + :param str side: 'buy' or 'sell' + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + if self.has['createMarketOrderWithCost'] or (self.has['createMarketBuyOrderWithCost'] and self.has['createMarketSellOrderWithCost']): + return self.create_order(symbol, 'market', side, cost, 1, params) + raise NotSupported(self.id + ' createMarketOrderWithCost() is not supported yet') + + def create_market_buy_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market buy order by providing the symbol and cost + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + if self.options['createMarketBuyOrderRequiresPrice'] or self.has['createMarketBuyOrderWithCost']: + return self.create_order(symbol, 'market', 'buy', cost, 1, params) + raise NotSupported(self.id + ' createMarketBuyOrderWithCost() is not supported yet') + + def create_market_sell_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market sell order by providing the symbol and cost + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + if self.options['createMarketSellOrderRequiresPrice'] or self.has['createMarketSellOrderWithCost']: + return self.create_order(symbol, 'market', 'sell', cost, 1, params) + raise NotSupported(self.id + ' createMarketSellOrderWithCost() is not supported yet') + + def create_market_order_with_cost_ws(self, symbol: str, side: OrderSide, cost: float, params={}): + """ + create a market order by providing the symbol, side and cost + :param str symbol: unified symbol of the market to create an order in + :param str side: 'buy' or 'sell' + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + if self.has['createMarketOrderWithCostWs'] or (self.has['createMarketBuyOrderWithCostWs'] and self.has['createMarketSellOrderWithCostWs']): + return self.create_order_ws(symbol, 'market', side, cost, 1, params) + raise NotSupported(self.id + ' createMarketOrderWithCostWs() is not supported yet') + + def create_trigger_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, triggerPrice: Num = None, params={}): + """ + create a trigger stop order(type 1) + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency or the number of contracts + :param float [price]: the price to fulfill the order, in units of the quote currency, ignored in market orders + :param float triggerPrice: the price to trigger the stop order, in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + if triggerPrice is None: + raise ArgumentsRequired(self.id + ' createTriggerOrder() requires a triggerPrice argument') + params['triggerPrice'] = triggerPrice + if self.has['createTriggerOrder']: + return self.create_order(symbol, type, side, amount, price, params) + raise NotSupported(self.id + ' createTriggerOrder() is not supported yet') + + def create_trigger_order_ws(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, triggerPrice: Num = None, params={}): + """ + create a trigger stop order(type 1) + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency or the number of contracts + :param float [price]: the price to fulfill the order, in units of the quote currency, ignored in market orders + :param float triggerPrice: the price to trigger the stop order, in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + if triggerPrice is None: + raise ArgumentsRequired(self.id + ' createTriggerOrderWs() requires a triggerPrice argument') + params['triggerPrice'] = triggerPrice + if self.has['createTriggerOrderWs']: + return self.create_order_ws(symbol, type, side, amount, price, params) + raise NotSupported(self.id + ' createTriggerOrderWs() is not supported yet') + + def create_stop_loss_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, stopLossPrice: Num = None, params={}): + """ + create a trigger stop loss order(type 2) + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency or the number of contracts + :param float [price]: the price to fulfill the order, in units of the quote currency, ignored in market orders + :param float stopLossPrice: the price to trigger the stop loss order, in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + if stopLossPrice is None: + raise ArgumentsRequired(self.id + ' createStopLossOrder() requires a stopLossPrice argument') + params['stopLossPrice'] = stopLossPrice + if self.has['createStopLossOrder']: + return self.create_order(symbol, type, side, amount, price, params) + raise NotSupported(self.id + ' createStopLossOrder() is not supported yet') + + def create_stop_loss_order_ws(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, stopLossPrice: Num = None, params={}): + """ + create a trigger stop loss order(type 2) + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency or the number of contracts + :param float [price]: the price to fulfill the order, in units of the quote currency, ignored in market orders + :param float stopLossPrice: the price to trigger the stop loss order, in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + if stopLossPrice is None: + raise ArgumentsRequired(self.id + ' createStopLossOrderWs() requires a stopLossPrice argument') + params['stopLossPrice'] = stopLossPrice + if self.has['createStopLossOrderWs']: + return self.create_order_ws(symbol, type, side, amount, price, params) + raise NotSupported(self.id + ' createStopLossOrderWs() is not supported yet') + + def create_take_profit_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, takeProfitPrice: Num = None, params={}): + """ + create a trigger take profit order(type 2) + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency or the number of contracts + :param float [price]: the price to fulfill the order, in units of the quote currency, ignored in market orders + :param float takeProfitPrice: the price to trigger the take profit order, in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + if takeProfitPrice is None: + raise ArgumentsRequired(self.id + ' createTakeProfitOrder() requires a takeProfitPrice argument') + params['takeProfitPrice'] = takeProfitPrice + if self.has['createTakeProfitOrder']: + return self.create_order(symbol, type, side, amount, price, params) + raise NotSupported(self.id + ' createTakeProfitOrder() is not supported yet') + + def create_take_profit_order_ws(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, takeProfitPrice: Num = None, params={}): + """ + create a trigger take profit order(type 2) + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency or the number of contracts + :param float [price]: the price to fulfill the order, in units of the quote currency, ignored in market orders + :param float takeProfitPrice: the price to trigger the take profit order, in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + if takeProfitPrice is None: + raise ArgumentsRequired(self.id + ' createTakeProfitOrderWs() requires a takeProfitPrice argument') + params['takeProfitPrice'] = takeProfitPrice + if self.has['createTakeProfitOrderWs']: + return self.create_order_ws(symbol, type, side, amount, price, params) + raise NotSupported(self.id + ' createTakeProfitOrderWs() is not supported yet') + + def create_order_with_take_profit_and_stop_loss(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, takeProfit: Num = None, stopLoss: Num = None, params={}): + """ + create an order with a stop loss or take profit attached(type 3) + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency or the number of contracts + :param float [price]: the price to fulfill the order, in units of the quote currency, ignored in market orders + :param float [takeProfit]: the take profit price, in units of the quote currency + :param float [stopLoss]: the stop loss price, in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.takeProfitType]: *not available on all exchanges* 'limit' or 'market' + :param str [params.stopLossType]: *not available on all exchanges* 'limit' or 'market' + :param str [params.takeProfitPriceType]: *not available on all exchanges* 'last', 'mark' or 'index' + :param str [params.stopLossPriceType]: *not available on all exchanges* 'last', 'mark' or 'index' + :param float [params.takeProfitLimitPrice]: *not available on all exchanges* limit price for a limit take profit order + :param float [params.stopLossLimitPrice]: *not available on all exchanges* stop loss for a limit stop loss order + :param float [params.takeProfitAmount]: *not available on all exchanges* the amount for a take profit + :param float [params.stopLossAmount]: *not available on all exchanges* the amount for a stop loss + :returns dict: an `order structure ` + """ + params = self.set_take_profit_and_stop_loss_params(symbol, type, side, amount, price, takeProfit, stopLoss, params) + if self.has['createOrderWithTakeProfitAndStopLoss']: + return self.create_order(symbol, type, side, amount, price, params) + raise NotSupported(self.id + ' createOrderWithTakeProfitAndStopLoss() is not supported yet') + + def set_take_profit_and_stop_loss_params(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, takeProfit: Num = None, stopLoss: Num = None, params={}): + if (takeProfit is None) and (stopLoss is None): + raise ArgumentsRequired(self.id + ' createOrderWithTakeProfitAndStopLoss() requires either a takeProfit or stopLoss argument') + if takeProfit is not None: + params['takeProfit'] = { + 'triggerPrice': takeProfit, + } + if stopLoss is not None: + params['stopLoss'] = { + 'triggerPrice': stopLoss, + } + takeProfitType = self.safe_string(params, 'takeProfitType') + takeProfitPriceType = self.safe_string(params, 'takeProfitPriceType') + takeProfitLimitPrice = self.safe_string(params, 'takeProfitLimitPrice') + takeProfitAmount = self.safe_string(params, 'takeProfitAmount') + stopLossType = self.safe_string(params, 'stopLossType') + stopLossPriceType = self.safe_string(params, 'stopLossPriceType') + stopLossLimitPrice = self.safe_string(params, 'stopLossLimitPrice') + stopLossAmount = self.safe_string(params, 'stopLossAmount') + if takeProfitType is not None: + params['takeProfit']['type'] = takeProfitType + if takeProfitPriceType is not None: + params['takeProfit']['priceType'] = takeProfitPriceType + if takeProfitLimitPrice is not None: + params['takeProfit']['price'] = self.parse_to_numeric(takeProfitLimitPrice) + if takeProfitAmount is not None: + params['takeProfit']['amount'] = self.parse_to_numeric(takeProfitAmount) + if stopLossType is not None: + params['stopLoss']['type'] = stopLossType + if stopLossPriceType is not None: + params['stopLoss']['priceType'] = stopLossPriceType + if stopLossLimitPrice is not None: + params['stopLoss']['price'] = self.parse_to_numeric(stopLossLimitPrice) + if stopLossAmount is not None: + params['stopLoss']['amount'] = self.parse_to_numeric(stopLossAmount) + params = self.omit(params, ['takeProfitType', 'takeProfitPriceType', 'takeProfitLimitPrice', 'takeProfitAmount', 'stopLossType', 'stopLossPriceType', 'stopLossLimitPrice', 'stopLossAmount']) + return params + + def create_order_with_take_profit_and_stop_loss_ws(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, takeProfit: Num = None, stopLoss: Num = None, params={}): + """ + create an order with a stop loss or take profit attached(type 3) + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency or the number of contracts + :param float [price]: the price to fulfill the order, in units of the quote currency, ignored in market orders + :param float [takeProfit]: the take profit price, in units of the quote currency + :param float [stopLoss]: the stop loss price, in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.takeProfitType]: *not available on all exchanges* 'limit' or 'market' + :param str [params.stopLossType]: *not available on all exchanges* 'limit' or 'market' + :param str [params.takeProfitPriceType]: *not available on all exchanges* 'last', 'mark' or 'index' + :param str [params.stopLossPriceType]: *not available on all exchanges* 'last', 'mark' or 'index' + :param float [params.takeProfitLimitPrice]: *not available on all exchanges* limit price for a limit take profit order + :param float [params.stopLossLimitPrice]: *not available on all exchanges* stop loss for a limit stop loss order + :param float [params.takeProfitAmount]: *not available on all exchanges* the amount for a take profit + :param float [params.stopLossAmount]: *not available on all exchanges* the amount for a stop loss + :returns dict: an `order structure ` + """ + params = self.set_take_profit_and_stop_loss_params(symbol, type, side, amount, price, takeProfit, stopLoss, params) + if self.has['createOrderWithTakeProfitAndStopLossWs']: + return self.create_order_ws(symbol, type, side, amount, price, params) + raise NotSupported(self.id + ' createOrderWithTakeProfitAndStopLossWs() is not supported yet') + + def create_orders(self, orders: List[OrderRequest], params={}): + raise NotSupported(self.id + ' createOrders() is not supported yet') + + def edit_orders(self, orders: List[OrderRequest], params={}): + raise NotSupported(self.id + ' editOrders() is not supported yet') + + def create_order_ws(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + raise NotSupported(self.id + ' createOrderWs() is not supported yet') + + def cancel_order(self, id: str, symbol: Str = None, params={}): + raise NotSupported(self.id + ' cancelOrder() is not supported yet') + + def cancel_order_with_client_order_id(self, clientOrderId: str, symbol: Str = None, params={}): + """ + create a market order by providing the symbol, side and cost + :param str clientOrderId: client order Id + :param str symbol: unified symbol of the market to create an order in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + extendedParams = self.extend(params, {'clientOrderId': clientOrderId}) + return self.cancel_order('', symbol, extendedParams) + + def cancel_order_ws(self, id: str, symbol: Str = None, params={}): + raise NotSupported(self.id + ' cancelOrderWs() is not supported yet') + + def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): + raise NotSupported(self.id + ' cancelOrders() is not supported yet') + + def cancel_orders_with_client_order_ids(self, clientOrderIds: List[str], symbol: Str = None, params={}): + """ + create a market order by providing the symbol, side and cost + :param str[] clientOrderIds: client order Ids + :param str symbol: unified symbol of the market to create an order in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + extendedParams = self.extend(params, {'clientOrderIds': clientOrderIds}) + return self.cancel_orders([], symbol, extendedParams) + + def cancel_orders_ws(self, ids: List[str], symbol: Str = None, params={}): + raise NotSupported(self.id + ' cancelOrdersWs() is not supported yet') + + def cancel_all_orders(self, symbol: Str = None, params={}): + raise NotSupported(self.id + ' cancelAllOrders() is not supported yet') + + def cancel_all_orders_after(self, timeout: Int, params={}): + raise NotSupported(self.id + ' cancelAllOrdersAfter() is not supported yet') + + def cancel_orders_for_symbols(self, orders: List[CancellationRequest], params={}): + raise NotSupported(self.id + ' cancelOrdersForSymbols() is not supported yet') + + def cancel_all_orders_ws(self, symbol: Str = None, params={}): + raise NotSupported(self.id + ' cancelAllOrdersWs() is not supported yet') + + def cancel_unified_order(self, order: Order, params={}): + return self.cancel_order(self.safe_string(order, 'id'), self.safe_string(order, 'symbol'), params) + + def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + if self.has['fetchOpenOrders'] and self.has['fetchClosedOrders']: + raise NotSupported(self.id + ' fetchOrders() is not supported yet, consider using fetchOpenOrders() and fetchClosedOrders() instead') + raise NotSupported(self.id + ' fetchOrders() is not supported yet') + + def fetch_orders_ws(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' fetchOrdersWs() is not supported yet') + + def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' fetchOrderTrades() is not supported yet') + + def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' watchOrders() is not supported yet') + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + if self.has['fetchOrders']: + orders = self.fetch_orders(symbol, since, limit, params) + return self.filter_by(orders, 'status', 'open') + raise NotSupported(self.id + ' fetchOpenOrders() is not supported yet') + + def fetch_open_orders_ws(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + if self.has['fetchOrdersWs']: + orders = self.fetch_orders_ws(symbol, since, limit, params) + return self.filter_by(orders, 'status', 'open') + raise NotSupported(self.id + ' fetchOpenOrdersWs() is not supported yet') + + def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + if self.has['fetchOrders']: + orders = self.fetch_orders(symbol, since, limit, params) + return self.filter_by(orders, 'status', 'closed') + raise NotSupported(self.id + ' fetchClosedOrders() is not supported yet') + + def fetch_canceled_and_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' fetchCanceledAndClosedOrders() is not supported yet') + + def fetch_closed_orders_ws(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + if self.has['fetchOrdersWs']: + orders = self.fetch_orders_ws(symbol, since, limit, params) + return self.filter_by(orders, 'status', 'closed') + raise NotSupported(self.id + ' fetchClosedOrdersWs() is not supported yet') + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' fetchMyTrades() is not supported yet') + + def fetch_my_liquidations(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' fetchMyLiquidations() is not supported yet') + + def fetch_liquidations(self, symbol: str, since: Int = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' fetchLiquidations() is not supported yet') + + def fetch_my_trades_ws(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' fetchMyTradesWs() is not supported yet') + + def watch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' watchMyTrades() is not supported yet') + + def fetch_greeks(self, symbol: str, params={}): + raise NotSupported(self.id + ' fetchGreeks() is not supported yet') + + def fetch_all_greeks(self, symbols: Strings = None, params={}): + raise NotSupported(self.id + ' fetchAllGreeks() is not supported yet') + + def fetch_option_chain(self, code: str, params={}): + raise NotSupported(self.id + ' fetchOptionChain() is not supported yet') + + def fetch_option(self, symbol: str, params={}): + raise NotSupported(self.id + ' fetchOption() is not supported yet') + + def fetch_convert_quote(self, fromCode: str, toCode: str, amount: Num = None, params={}): + raise NotSupported(self.id + ' fetchConvertQuote() is not supported yet') + + def fetch_deposits_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch history of deposits and withdrawals + :param str [code]: unified currency code for the currency of the deposit/withdrawals, default is None + :param int [since]: timestamp in ms of the earliest deposit/withdrawal, default is None + :param int [limit]: max number of deposit/withdrawals to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `transaction structures ` + """ + raise NotSupported(self.id + ' fetchDepositsWithdrawals() is not supported yet') + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' fetchDeposits() is not supported yet') + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' fetchWithdrawals() is not supported yet') + + def fetch_deposits_ws(self, code: Str = None, since: Int = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' fetchDepositsWs() is not supported yet') + + def fetch_withdrawals_ws(self, code: Str = None, since: Int = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' fetchWithdrawalsWs() is not supported yet') + + def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' fetchFundingRateHistory() is not supported yet') + + def fetch_funding_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + raise NotSupported(self.id + ' fetchFundingHistory() is not supported yet') + + def close_position(self, symbol: str, side: OrderSide = None, params={}): + raise NotSupported(self.id + ' closePosition() is not supported yet') + + def close_all_positions(self, params={}): + raise NotSupported(self.id + ' closeAllPositions() is not supported yet') + + def fetch_l3_order_book(self, symbol: str, limit: Int = None, params={}): + raise BadRequest(self.id + ' fetchL3OrderBook() is not supported yet') + + def parse_last_price(self, price, market: Market = None): + raise NotSupported(self.id + ' parseLastPrice() is not supported yet') + + def fetch_deposit_address(self, code: str, params={}): + if self.has['fetchDepositAddresses']: + depositAddresses = self.fetch_deposit_addresses([code], params) + depositAddress = self.safe_value(depositAddresses, code) + if depositAddress is None: + raise InvalidAddress(self.id + ' fetchDepositAddress() could not find a deposit address for ' + code + ', make sure you have created a corresponding deposit address in your wallet on the exchange website') + else: + return depositAddress + elif self.has['fetchDepositAddressesByNetwork']: + network = self.safe_string(params, 'network') + params = self.omit(params, 'network') + addressStructures = self.fetch_deposit_addresses_by_network(code, params) + if network is not None: + return self.safe_dict(addressStructures, network) + else: + keys = list(addressStructures.keys()) + key = self.safe_string(keys, 0) + return self.safe_dict(addressStructures, key) + else: + raise NotSupported(self.id + ' fetchDepositAddress() is not supported yet') + + def account(self) -> BalanceAccount: + return { + 'free': None, + 'used': None, + 'total': None, + } + + def common_currency_code(self, code: str): + if not self.substituteCommonCurrencyCodes: + return code + return self.safe_string(self.commonCurrencies, code, code) + + def currency(self, code: str): + keys = list(self.currencies.keys()) + numCurrencies = len(keys) + if numCurrencies == 0: + raise ExchangeError(self.id + ' currencies not loaded') + if isinstance(code, str): + if code in self.currencies: + return self.currencies[code] + elif code in self.currencies_by_id: + return self.currencies_by_id[code] + raise ExchangeError(self.id + ' does not have currency code ' + code) + + def market(self, symbol: str): + if self.markets is None: + raise ExchangeError(self.id + ' markets not loaded') + if symbol in self.markets: + return self.markets[symbol] + elif symbol in self.markets_by_id: + markets = self.markets_by_id[symbol] + defaultType = self.safe_string_2(self.options, 'defaultType', 'defaultSubType', 'spot') + for i in range(0, len(markets)): + market = markets[i] + if market[defaultType]: + return market + return markets[0] + elif (symbol.endswith('-C')) or (symbol.endswith('-P')) or (symbol.startswith('C-')) or (symbol.startswith('P-')): + return self.create_expired_option_market(symbol) + raise BadSymbol(self.id + ' does not have market symbol ' + symbol) + + def create_expired_option_market(self, symbol: str): + raise NotSupported(self.id + ' createExpiredOptionMarket() is not supported yet') + + def is_leveraged_currency(self, currencyCode, checkBaseCoin: Bool = False, existingCurrencies: dict = None): + leverageSuffixes = [ + '2L', '2S', '3L', '3S', '4L', '4S', '5L', '5S', # Leveraged Tokens(LT) + 'UP', 'DOWN', # exchange-specific(e.g. BLVT) + 'BULL', 'BEAR', # similar + ] + for i in range(0, len(leverageSuffixes)): + leverageSuffix = leverageSuffixes[i] + if currencyCode.endswith(leverageSuffix): + if not checkBaseCoin: + return True + else: + # check if base currency is inside dict + baseCurrencyCode = currencyCode.replace(leverageSuffix, '') + if baseCurrencyCode in existingCurrencies: + return True + return False + + def handle_withdraw_tag_and_params(self, tag, params): + if (tag is not None) and (isinstance(tag, dict)): + params = self.extend(tag, params) + tag = None + if tag is None: + tag = self.safe_string(params, 'tag') + if tag is not None: + params = self.omit(params, 'tag') + return [tag, params] + + def create_limit_order(self, symbol: str, side: OrderSide, amount: float, price: float, params={}): + return self.create_order(symbol, 'limit', side, amount, price, params) + + def create_limit_order_ws(self, symbol: str, side: OrderSide, amount: float, price: float, params={}): + return self.create_order_ws(symbol, 'limit', side, amount, price, params) + + def create_market_order(self, symbol: str, side: OrderSide, amount: float, price: Num = None, params={}): + return self.create_order(symbol, 'market', side, amount, price, params) + + def create_market_order_ws(self, symbol: str, side: OrderSide, amount: float, price: Num = None, params={}): + return self.create_order_ws(symbol, 'market', side, amount, price, params) + + def create_limit_buy_order(self, symbol: str, amount: float, price: float, params={}): + return self.create_order(symbol, 'limit', 'buy', amount, price, params) + + def create_limit_buy_order_ws(self, symbol: str, amount: float, price: float, params={}): + return self.create_order_ws(symbol, 'limit', 'buy', amount, price, params) + + def create_limit_sell_order(self, symbol: str, amount: float, price: float, params={}): + return self.create_order(symbol, 'limit', 'sell', amount, price, params) + + def create_limit_sell_order_ws(self, symbol: str, amount: float, price: float, params={}): + return self.create_order_ws(symbol, 'limit', 'sell', amount, price, params) + + def create_market_buy_order(self, symbol: str, amount: float, params={}): + return self.create_order(symbol, 'market', 'buy', amount, None, params) + + def create_market_buy_order_ws(self, symbol: str, amount: float, params={}): + return self.create_order_ws(symbol, 'market', 'buy', amount, None, params) + + def create_market_sell_order(self, symbol: str, amount: float, params={}): + return self.create_order(symbol, 'market', 'sell', amount, None, params) + + def create_market_sell_order_ws(self, symbol: str, amount: float, params={}): + return self.create_order_ws(symbol, 'market', 'sell', amount, None, params) + + def cost_to_precision(self, symbol: str, cost): + if cost is None: + return None + market = self.market(symbol) + return self.decimal_to_precision(cost, TRUNCATE, market['precision']['price'], self.precisionMode, self.paddingMode) + + def price_to_precision(self, symbol: str, price): + if price is None: + return None + market = self.market(symbol) + result = self.decimal_to_precision(price, ROUND, market['precision']['price'], self.precisionMode, self.paddingMode) + if result == '0': + raise InvalidOrder(self.id + ' price of ' + market['symbol'] + ' must be greater than minimum price precision of ' + self.number_to_string(market['precision']['price'])) + return result + + def amount_to_precision(self, symbol: str, amount): + if amount is None: + return None + market = self.market(symbol) + result = self.decimal_to_precision(amount, TRUNCATE, market['precision']['amount'], self.precisionMode, self.paddingMode) + if result == '0': + raise InvalidOrder(self.id + ' amount of ' + market['symbol'] + ' must be greater than minimum amount precision of ' + self.number_to_string(market['precision']['amount'])) + return result + + def fee_to_precision(self, symbol: str, fee): + if fee is None: + return None + market = self.market(symbol) + return self.decimal_to_precision(fee, ROUND, market['precision']['price'], self.precisionMode, self.paddingMode) + + def currency_to_precision(self, code: str, fee, networkCode=None): + currency = self.currencies[code] + precision = self.safe_value(currency, 'precision') + if networkCode is not None: + networks = self.safe_dict(currency, 'networks', {}) + networkItem = self.safe_dict(networks, networkCode, {}) + precision = self.safe_value(networkItem, 'precision', precision) + if precision is None: + return self.force_string(fee) + else: + roundingMode = self.safe_integer(self.options, 'currencyToPrecisionRoundingMode', ROUND) + return self.decimal_to_precision(fee, roundingMode, precision, self.precisionMode, self.paddingMode) + + def force_string(self, value): + if not isinstance(value, str): + return self.number_to_string(value) + return value + + def is_tick_precision(self): + return self.precisionMode == TICK_SIZE + + def is_decimal_precision(self): + return self.precisionMode == DECIMAL_PLACES + + def is_significant_precision(self): + return self.precisionMode == SIGNIFICANT_DIGITS + + def safe_number(self, obj, key: IndexType, defaultNumber: Num = None): + value = self.safe_string(obj, key) + return self.parse_number(value, defaultNumber) + + def safe_number_n(self, obj: object, arr: List[IndexType], defaultNumber: Num = None): + value = self.safe_string_n(obj, arr) + return self.parse_number(value, defaultNumber) + + def parse_precision(self, precision: str): + """ + @ignore + :param str precision: The number of digits to the right of the decimal + :returns str: a string number equal to 1e-precision + """ + if precision is None: + return None + precisionNumber = int(precision) + if precisionNumber == 0: + return '1' + if precisionNumber > 0: + parsedPrecision = '0.' + for i in range(0, precisionNumber - 1): + parsedPrecision = parsedPrecision + '0' + return parsedPrecision + '1' + else: + parsedPrecision = '1' + for i in range(0, precisionNumber * -1 - 1): + parsedPrecision = parsedPrecision + '0' + return parsedPrecision + '0' + + def integer_precision_to_amount(self, precision: Str): + """ + @ignore + handles positive & negative numbers too. parsePrecision() does not handle negative numbers, but self method handles + :param str precision: The number of digits to the right of the decimal + :returns str: a string number equal to 1e-precision + """ + if precision is None: + return None + if Precise.string_ge(precision, '0'): + return self.parse_precision(precision) + else: + positivePrecisionString = Precise.string_abs(precision) + positivePrecision = int(positivePrecisionString) + parsedPrecision = '1' + for i in range(0, positivePrecision - 1): + parsedPrecision = parsedPrecision + '0' + return parsedPrecision + '0' + + def load_time_difference(self, params={}): + serverTime = self.fetch_time(params) + after = self.milliseconds() + self.options['timeDifference'] = after - serverTime + return self.options['timeDifference'] + + def implode_hostname(self, url: str): + return self.implode_params(url, {'hostname': self.hostname}) + + def fetch_market_leverage_tiers(self, symbol: str, params={}): + if self.has['fetchLeverageTiers']: + market = self.market(symbol) + if not market['contract']: + raise BadSymbol(self.id + ' fetchMarketLeverageTiers() supports contract markets only') + tiers = self.fetch_leverage_tiers([symbol]) + return self.safe_value(tiers, symbol) + else: + raise NotSupported(self.id + ' fetchMarketLeverageTiers() is not supported yet') + + def create_post_only_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + if not self.has['createPostOnlyOrder']: + raise NotSupported(self.id + ' createPostOnlyOrder() is not supported yet') + query = self.extend(params, {'postOnly': True}) + return self.create_order(symbol, type, side, amount, price, query) + + def create_post_only_order_ws(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + if not self.has['createPostOnlyOrderWs']: + raise NotSupported(self.id + ' createPostOnlyOrderWs() is not supported yet') + query = self.extend(params, {'postOnly': True}) + return self.create_order_ws(symbol, type, side, amount, price, query) + + def create_reduce_only_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + if not self.has['createReduceOnlyOrder']: + raise NotSupported(self.id + ' createReduceOnlyOrder() is not supported yet') + query = self.extend(params, {'reduceOnly': True}) + return self.create_order(symbol, type, side, amount, price, query) + + def create_reduce_only_order_ws(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + if not self.has['createReduceOnlyOrderWs']: + raise NotSupported(self.id + ' createReduceOnlyOrderWs() is not supported yet') + query = self.extend(params, {'reduceOnly': True}) + return self.create_order_ws(symbol, type, side, amount, price, query) + + def create_stop_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, triggerPrice: Num = None, params={}): + if not self.has['createStopOrder']: + raise NotSupported(self.id + ' createStopOrder() is not supported yet') + if triggerPrice is None: + raise ArgumentsRequired(self.id + ' create_stop_order() requires a stopPrice argument') + query = self.extend(params, {'stopPrice': triggerPrice}) + return self.create_order(symbol, type, side, amount, price, query) + + def create_stop_order_ws(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, triggerPrice: Num = None, params={}): + if not self.has['createStopOrderWs']: + raise NotSupported(self.id + ' createStopOrderWs() is not supported yet') + if triggerPrice is None: + raise ArgumentsRequired(self.id + ' createStopOrderWs() requires a stopPrice argument') + query = self.extend(params, {'stopPrice': triggerPrice}) + return self.create_order_ws(symbol, type, side, amount, price, query) + + def create_stop_limit_order(self, symbol: str, side: OrderSide, amount: float, price: float, triggerPrice: float, params={}): + if not self.has['createStopLimitOrder']: + raise NotSupported(self.id + ' createStopLimitOrder() is not supported yet') + query = self.extend(params, {'stopPrice': triggerPrice}) + return self.create_order(symbol, 'limit', side, amount, price, query) + + def create_stop_limit_order_ws(self, symbol: str, side: OrderSide, amount: float, price: float, triggerPrice: float, params={}): + if not self.has['createStopLimitOrderWs']: + raise NotSupported(self.id + ' createStopLimitOrderWs() is not supported yet') + query = self.extend(params, {'stopPrice': triggerPrice}) + return self.create_order_ws(symbol, 'limit', side, amount, price, query) + + def create_stop_market_order(self, symbol: str, side: OrderSide, amount: float, triggerPrice: float, params={}): + if not self.has['createStopMarketOrder']: + raise NotSupported(self.id + ' createStopMarketOrder() is not supported yet') + query = self.extend(params, {'stopPrice': triggerPrice}) + return self.create_order(symbol, 'market', side, amount, None, query) + + def create_stop_market_order_ws(self, symbol: str, side: OrderSide, amount: float, triggerPrice: float, params={}): + if not self.has['createStopMarketOrderWs']: + raise NotSupported(self.id + ' createStopMarketOrderWs() is not supported yet') + query = self.extend(params, {'stopPrice': triggerPrice}) + return self.create_order_ws(symbol, 'market', side, amount, None, query) + + def safe_currency_code(self, currencyId: Str, currency: Currency = None): + currency = self.safe_currency(currencyId, currency) + return currency['code'] + + def filter_by_symbol_since_limit(self, array, symbol: Str = None, since: Int = None, limit: Int = None, tail=False): + return self.filter_by_value_since_limit(array, 'symbol', symbol, since, limit, 'timestamp', tail) + + def filter_by_currency_since_limit(self, array, code=None, since: Int = None, limit: Int = None, tail=False): + return self.filter_by_value_since_limit(array, 'currency', code, since, limit, 'timestamp', tail) + + def filter_by_symbols_since_limit(self, array, symbols: List[str] = None, since: Int = None, limit: Int = None, tail=False): + result = self.filter_by_array(array, 'symbol', symbols, False) + return self.filter_by_since_limit(result, since, limit, 'timestamp', tail) + + def parse_last_prices(self, pricesData, symbols: List[str] = None, params={}): + # + # the value of tickers is either a dict or a list + # + # dict + # + # { + # 'marketId1': {...}, + # 'marketId2': {...}, + # ... + # } + # + # list + # + # [ + # {'market': 'marketId1', ...}, + # {'market': 'marketId2', ...}, + # ... + # ] + # + results = [] + if isinstance(pricesData, list): + for i in range(0, len(pricesData)): + priceData = self.extend(self.parse_last_price(pricesData[i]), params) + results.append(priceData) + else: + marketIds = list(pricesData.keys()) + for i in range(0, len(marketIds)): + marketId = marketIds[i] + market = self.safe_market(marketId) + priceData = self.extend(self.parse_last_price(pricesData[marketId], market), params) + results.append(priceData) + symbols = self.market_symbols(symbols) + return self.filter_by_array(results, 'symbol', symbols) + + def parse_tickers(self, tickers, symbols: Strings = None, params={}): + # + # the value of tickers is either a dict or a list + # + # + # dict + # + # { + # 'marketId1': {...}, + # 'marketId2': {...}, + # 'marketId3': {...}, + # ... + # } + # + # list + # + # [ + # {'market': 'marketId1', ...}, + # {'market': 'marketId2', ...}, + # {'market': 'marketId3', ...}, + # ... + # ] + # + results = [] + if isinstance(tickers, list): + for i in range(0, len(tickers)): + parsedTicker = self.parse_ticker(tickers[i]) + ticker = self.extend(parsedTicker, params) + results.append(ticker) + else: + marketIds = list(tickers.keys()) + for i in range(0, len(marketIds)): + marketId = marketIds[i] + market = self.safe_market(marketId) + parsed = self.parse_ticker(tickers[marketId], market) + ticker = self.extend(parsed, params) + results.append(ticker) + symbols = self.market_symbols(symbols) + return self.filter_by_array(results, 'symbol', symbols) + + def parse_deposit_addresses(self, addresses, codes: Strings = None, indexed=True, params={}): + result = [] + for i in range(0, len(addresses)): + address = self.extend(self.parse_deposit_address(addresses[i]), params) + result.append(address) + if codes is not None: + result = self.filter_by_array(result, 'currency', codes, False) + if indexed: + result = self.filter_by_array(result, 'currency', None, indexed) + return result + + def parse_borrow_interests(self, response, market: Market = None): + interests = [] + for i in range(0, len(response)): + row = response[i] + interests.append(self.parse_borrow_interest(row, market)) + return interests + + def parse_borrow_rate(self, info, currency: Currency = None): + raise NotSupported(self.id + ' parseBorrowRate() is not supported yet') + + def parse_borrow_rate_history(self, response, code: Str, since: Int, limit: Int): + result = [] + for i in range(0, len(response)): + item = response[i] + borrowRate = self.parse_borrow_rate(item) + result.append(borrowRate) + sorted = self.sort_by(result, 'timestamp') + return self.filter_by_currency_since_limit(sorted, code, since, limit) + + def parse_isolated_borrow_rates(self, info: Any): + result = {} + for i in range(0, len(info)): + item = info[i] + borrowRate = self.parse_isolated_borrow_rate(item) + symbol = self.safe_string(borrowRate, 'symbol') + result[symbol] = borrowRate + return result + + def parse_funding_rate_histories(self, response, market=None, since: Int = None, limit: Int = None): + rates = [] + for i in range(0, len(response)): + entry = response[i] + rates.append(self.parse_funding_rate_history(entry, market)) + sorted = self.sort_by(rates, 'timestamp') + symbol = None if (market is None) else market['symbol'] + return self.filter_by_symbol_since_limit(sorted, symbol, since, limit) + + def safe_symbol(self, marketId: Str, market: Market = None, delimiter: Str = None, marketType: Str = None): + market = self.safe_market(marketId, market, delimiter, marketType) + return market['symbol'] + + def parse_funding_rate(self, contract: str, market: Market = None): + raise NotSupported(self.id + ' parseFundingRate() is not supported yet') + + def parse_funding_rates(self, response, symbols: Strings = None): + fundingRates = {} + for i in range(0, len(response)): + entry = response[i] + parsed = self.parse_funding_rate(entry) + fundingRates[parsed['symbol']] = parsed + return self.filter_by_array(fundingRates, 'symbol', symbols) + + def parse_long_short_ratio(self, info: dict, market: Market = None): + raise NotSupported(self.id + ' parseLongShortRatio() is not supported yet') + + def parse_long_short_ratio_history(self, response, market=None, since: Int = None, limit: Int = None): + rates = [] + for i in range(0, len(response)): + entry = response[i] + rates.append(self.parse_long_short_ratio(entry, market)) + sorted = self.sort_by(rates, 'timestamp') + symbol = None if (market is None) else market['symbol'] + return self.filter_by_symbol_since_limit(sorted, symbol, since, limit) + + def handle_trigger_prices_and_params(self, symbol, params, omitParams=True): + # + triggerPrice = self.safe_string_2(params, 'triggerPrice', 'stopPrice') + triggerPriceStr: Str = None + stopLossPrice = self.safe_string(params, 'stopLossPrice') + stopLossPriceStr: Str = None + takeProfitPrice = self.safe_string(params, 'takeProfitPrice') + takeProfitPriceStr: Str = None + # + if triggerPrice is not None: + if omitParams: + params = self.omit(params, ['triggerPrice', 'stopPrice']) + triggerPriceStr = self.price_to_precision(symbol, float(triggerPrice)) + if stopLossPrice is not None: + if omitParams: + params = self.omit(params, 'stopLossPrice') + stopLossPriceStr = self.price_to_precision(symbol, float(stopLossPrice)) + if takeProfitPrice is not None: + if omitParams: + params = self.omit(params, 'takeProfitPrice') + takeProfitPriceStr = self.price_to_precision(symbol, float(takeProfitPrice)) + return [triggerPriceStr, stopLossPriceStr, takeProfitPriceStr, params] + + def handle_trigger_direction_and_params(self, params, exchangeSpecificKey: Str = None, allowEmpty: Bool = False): + """ + @ignore + :returns [str, dict]: the trigger-direction value and omited params + """ + triggerDirection = self.safe_string(params, 'triggerDirection') + exchangeSpecificDefined = (exchangeSpecificKey is not None) and (exchangeSpecificKey in params) + if triggerDirection is not None: + params = self.omit(params, 'triggerDirection') + # raise exception if: + # A) if provided value is not unified(support old "up/down" strings too) + # B) if exchange specific "trigger direction key"(eg. "stopPriceSide") was not provided + if not self.in_array(triggerDirection, ['ascending', 'descending', 'up', 'down', 'above', 'below']) and not exchangeSpecificDefined and not allowEmpty: + raise ArgumentsRequired(self.id + ' createOrder() : trigger orders require params["triggerDirection"] to be either "ascending" or "descending"') + # if old format was provided, overwrite to new + if triggerDirection == 'up' or triggerDirection == 'above': + triggerDirection = 'ascending' + elif triggerDirection == 'down' or triggerDirection == 'below': + triggerDirection = 'descending' + return [triggerDirection, params] + + def handle_trigger_and_params(self, params): + isTrigger = self.safe_bool_2(params, 'trigger', 'stop') + if isTrigger: + params = self.omit(params, ['trigger', 'stop']) + return [isTrigger, params] + + def is_trigger_order(self, params): + # for backwards compatibility + return self.handle_trigger_and_params(params) + + def is_post_only(self, isMarketOrder: bool, exchangeSpecificParam, params={}): + """ + @ignore + :param str type: Order type + :param boolean exchangeSpecificParam: exchange specific postOnly + :param dict [params]: exchange specific params + :returns boolean: True if a post only order, False otherwise + """ + timeInForce = self.safe_string_upper(params, 'timeInForce') + postOnly = self.safe_bool_2(params, 'postOnly', 'post_only', False) + # we assume timeInForce is uppercase from safeStringUpper(params, 'timeInForce') + ioc = timeInForce == 'IOC' + fok = timeInForce == 'FOK' + timeInForcePostOnly = timeInForce == 'PO' + postOnly = postOnly or timeInForcePostOnly or exchangeSpecificParam + if postOnly: + if ioc or fok: + raise InvalidOrder(self.id + ' postOnly orders cannot have timeInForce equal to ' + timeInForce) + elif isMarketOrder: + raise InvalidOrder(self.id + ' market orders cannot be postOnly') + else: + return True + else: + return False + + def handle_post_only(self, isMarketOrder: bool, exchangeSpecificPostOnlyOption: bool, params: Any = {}): + """ + @ignore + :param str type: Order type + :param boolean exchangeSpecificBoolean: exchange specific postOnly + :param dict [params]: exchange specific params + :returns Array: + """ + timeInForce = self.safe_string_upper(params, 'timeInForce') + postOnly = self.safe_bool(params, 'postOnly', False) + ioc = timeInForce == 'IOC' + fok = timeInForce == 'FOK' + po = timeInForce == 'PO' + postOnly = postOnly or po or exchangeSpecificPostOnlyOption + if postOnly: + if ioc or fok: + raise InvalidOrder(self.id + ' postOnly orders cannot have timeInForce equal to ' + timeInForce) + elif isMarketOrder: + raise InvalidOrder(self.id + ' market orders cannot be postOnly') + else: + if po: + params = self.omit(params, 'timeInForce') + params = self.omit(params, 'postOnly') + return [True, params] + return [False, params] + + def fetch_last_prices(self, symbols: Strings = None, params={}): + raise NotSupported(self.id + ' fetchLastPrices() is not supported yet') + + def fetch_trading_fees(self, params={}): + raise NotSupported(self.id + ' fetchTradingFees() is not supported yet') + + def fetch_trading_fees_ws(self, params={}): + raise NotSupported(self.id + ' fetchTradingFeesWs() is not supported yet') + + def fetch_trading_fee(self, symbol: str, params={}): + if not self.has['fetchTradingFees']: + raise NotSupported(self.id + ' fetchTradingFee() is not supported yet') + fees = self.fetch_trading_fees(params) + return self.safe_dict(fees, symbol) + + def fetch_convert_currencies(self, params={}): + raise NotSupported(self.id + ' fetchConvertCurrencies() is not supported yet') + + def parse_open_interest(self, interest, market: Market = None): + raise NotSupported(self.id + ' parseOpenInterest() is not supported yet') + + def parse_open_interests(self, response, symbols: Strings = None): + result = {} + for i in range(0, len(response)): + entry = response[i] + parsed = self.parse_open_interest(entry) + result[parsed['symbol']] = parsed + return self.filter_by_array(result, 'symbol', symbols) + + def parse_open_interests_history(self, response, market=None, since: Int = None, limit: Int = None): + interests = [] + for i in range(0, len(response)): + entry = response[i] + interest = self.parse_open_interest(entry, market) + interests.append(interest) + sorted = self.sort_by(interests, 'timestamp') + symbol = self.safe_string(market, 'symbol') + return self.filter_by_symbol_since_limit(sorted, symbol, since, limit) + + def fetch_funding_rate(self, symbol: str, params={}): + if self.has['fetchFundingRates']: + self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + if not market['contract']: + raise BadSymbol(self.id + ' fetchFundingRate() supports contract markets only') + rates = self.fetch_funding_rates([symbol], params) + rate = self.safe_value(rates, symbol) + if rate is None: + raise NullResponse(self.id + ' fetchFundingRate() returned no data for ' + symbol) + else: + return rate + else: + raise NotSupported(self.id + ' fetchFundingRate() is not supported yet') + + def fetch_funding_interval(self, symbol: str, params={}): + if self.has['fetchFundingIntervals']: + self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + if not market['contract']: + raise BadSymbol(self.id + ' fetchFundingInterval() supports contract markets only') + rates = self.fetch_funding_intervals([symbol], params) + rate = self.safe_value(rates, symbol) + if rate is None: + raise NullResponse(self.id + ' fetchFundingInterval() returned no data for ' + symbol) + else: + return rate + else: + raise NotSupported(self.id + ' fetchFundingInterval() is not supported yet') + + def fetch_mark_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}): + """ + fetches historical mark price candlestick data containing the open, high, low, and close price of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns float[][]: A list of candles ordered, open, high, low, close, None + """ + if self.has['fetchMarkOHLCV']: + request: dict = { + 'price': 'mark', + } + return self.fetch_ohlcv(symbol, timeframe, since, limit, self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchMarkOHLCV() is not supported yet') + + def fetch_index_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}): + """ + fetches historical index price candlestick data containing the open, high, low, and close price of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + @returns {} A list of candles ordered, open, high, low, close, None + """ + if self.has['fetchIndexOHLCV']: + request: dict = { + 'price': 'index', + } + return self.fetch_ohlcv(symbol, timeframe, since, limit, self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchIndexOHLCV() is not supported yet') + + def fetch_premium_index_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}): + """ + fetches historical premium index price candlestick data containing the open, high, low, and close price of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns float[][]: A list of candles ordered, open, high, low, close, None + """ + if self.has['fetchPremiumIndexOHLCV']: + request: dict = { + 'price': 'premiumIndex', + } + return self.fetch_ohlcv(symbol, timeframe, since, limit, self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchPremiumIndexOHLCV() is not supported yet') + + def handle_time_in_force(self, params={}): + """ + @ignore + Must add timeInForce to self.options to use self method + :returns str: returns the exchange specific value for timeInForce + """ + timeInForce = self.safe_string_upper(params, 'timeInForce') # supported values GTC, IOC, PO + if timeInForce is not None: + exchangeValue = self.safe_string(self.options['timeInForce'], timeInForce) + if exchangeValue is None: + raise ExchangeError(self.id + ' does not support timeInForce "' + timeInForce + '"') + return exchangeValue + return None + + def convert_type_to_account(self, account): + """ + @ignore + Must add accountsByType to self.options to use self method + :param str account: key for account name in self.options['accountsByType'] + :returns: the exchange specific account name or the isolated margin id for transfers + """ + accountsByType = self.safe_dict(self.options, 'accountsByType', {}) + lowercaseAccount = account.lower() + if lowercaseAccount in accountsByType: + return accountsByType[lowercaseAccount] + elif (account in self.markets) or (account in self.markets_by_id): + market = self.market(account) + return market['id'] + else: + return account + + def check_required_argument(self, methodName: str, argument, argumentName, options=[]): + """ + @ignore + :param str methodName: the name of the method that the argument is being checked for + :param str argument: the argument's actual value provided + :param str argumentName: the name of the argument being checked(for logging purposes) + :param str[] options: a list of options that the argument can be + :returns None: + """ + optionsLength = len(options) + if (argument is None) or ((optionsLength > 0) and (not(self.in_array(argument, options)))): + messageOptions = ', '.join(options) + message = self.id + ' ' + methodName + '() requires a ' + argumentName + ' argument' + if messageOptions != '': + message += ', one of ' + '(' + messageOptions + ')' + raise ArgumentsRequired(message) + + def check_required_margin_argument(self, methodName: str, symbol: Str, marginMode: str): + """ + @ignore + :param str symbol: unified symbol of the market + :param str methodName: name of the method that requires a symbol + :param str marginMode: is either 'isolated' or 'cross' + """ + if (marginMode == 'isolated') and (symbol is None): + raise ArgumentsRequired(self.id + ' ' + methodName + '() requires a symbol argument for isolated margin') + elif (marginMode == 'cross') and (symbol is not None): + raise ArgumentsRequired(self.id + ' ' + methodName + '() cannot have a symbol argument for cross margin') + + def parse_deposit_withdraw_fees(self, response, codes: Strings = None, currencyIdKey=None): + """ + @ignore + :param object[]|dict response: unparsed response from the exchange + :param str[]|None codes: the unified currency codes to fetch transactions fees for, returns all currencies when None + :param str currencyIdKey: *should only be None when response is a dictionary* the object key that corresponds to the currency id + :returns dict: objects with withdraw and deposit fees, indexed by currency codes + """ + depositWithdrawFees = {} + isArray = isinstance(response, list) + responseKeys = response + if not isArray: + responseKeys = list(response.keys()) + for i in range(0, len(responseKeys)): + entry = responseKeys[i] + dictionary = entry if isArray else response[entry] + currencyId = self.safe_string(dictionary, currencyIdKey) if isArray else entry + currency = self.safe_currency(currencyId) + code = self.safe_string(currency, 'code') + if (codes is None) or (self.in_array(code, codes)): + depositWithdrawFees[code] = self.parse_deposit_withdraw_fee(dictionary, currency) + return depositWithdrawFees + + def parse_deposit_withdraw_fee(self, fee, currency: Currency = None): + raise NotSupported(self.id + ' parseDepositWithdrawFee() is not supported yet') + + def deposit_withdraw_fee(self, info): + return { + 'info': info, + 'withdraw': { + 'fee': None, + 'percentage': None, + }, + 'deposit': { + 'fee': None, + 'percentage': None, + }, + 'networks': {}, + } + + def assign_default_deposit_withdraw_fees(self, fee, currency=None): + """ + @ignore + Takes a depositWithdrawFee structure and assigns the default values for withdraw and deposit + :param dict fee: A deposit withdraw fee structure + :param dict currency: A currency structure, the response from self.currency() + :returns dict: A deposit withdraw fee structure + """ + networkKeys = list(fee['networks'].keys()) + numNetworks = len(networkKeys) + if numNetworks == 1: + fee['withdraw'] = fee['networks'][networkKeys[0]]['withdraw'] + fee['deposit'] = fee['networks'][networkKeys[0]]['deposit'] + return fee + currencyCode = self.safe_string(currency, 'code') + for i in range(0, numNetworks): + network = networkKeys[i] + if network == currencyCode: + fee['withdraw'] = fee['networks'][networkKeys[i]]['withdraw'] + fee['deposit'] = fee['networks'][networkKeys[i]]['deposit'] + return fee + + def parse_income(self, info, market: Market = None): + raise NotSupported(self.id + ' parseIncome() is not supported yet') + + def parse_incomes(self, incomes, market=None, since: Int = None, limit: Int = None): + """ + @ignore + parses funding fee info from exchange response + :param dict[] incomes: each item describes once instance of currency being received or paid + :param dict market: ccxt market + :param int [since]: when defined, the response items are filtered to only include items after self timestamp + :param int [limit]: limits the number of items in the response + :returns dict[]: an array of `funding history structures ` + """ + result = [] + for i in range(0, len(incomes)): + entry = incomes[i] + parsed = self.parse_income(entry, market) + result.append(parsed) + sorted = self.sort_by(result, 'timestamp') + symbol = self.safe_string(market, 'symbol') + return self.filter_by_symbol_since_limit(sorted, symbol, since, limit) + + def get_market_from_symbols(self, symbols: Strings = None): + if symbols is None: + return None + firstMarket = self.safe_string(symbols, 0) + market = self.market(firstMarket) + return market + + def parse_ws_ohlcvs(self, ohlcvs: List[object], market: Any = None, timeframe: str = '1m', since: Int = None, limit: Int = None): + results = [] + for i in range(0, len(ohlcvs)): + results.append(self.parse_ws_ohlcv(ohlcvs[i], market)) + return results + + def fetch_transactions(self, code: Str = None, since: Int = None, limit: Int = None, params={}): + """ + @deprecated + *DEPRECATED* use fetchDepositsWithdrawals instead + :param str code: unified currency code for the currency of the deposit/withdrawals, default is None + :param int [since]: timestamp in ms of the earliest deposit/withdrawal, default is None + :param int [limit]: max number of deposit/withdrawals to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `transaction structures ` + """ + if self.has['fetchDepositsWithdrawals']: + return self.fetch_deposits_withdrawals(code, since, limit, params) + else: + raise NotSupported(self.id + ' fetchTransactions() is not supported yet') + + def filter_by_array_positions(self, objects, key: IndexType, values=None, indexed=True): + """ + @ignore + Typed wrapper for filterByArray that returns a list of positions + """ + return self.filter_by_array(objects, key, values, indexed) + + def filter_by_array_tickers(self, objects, key: IndexType, values=None, indexed=True): + """ + @ignore + Typed wrapper for filterByArray that returns a dictionary of tickers + """ + return self.filter_by_array(objects, key, values, indexed) + + def create_ohlcv_object(self, symbol: str, timeframe: str, data): + res = {} + res[symbol] = {} + res[symbol][timeframe] = data + return res + + def handle_max_entries_per_request_and_params(self, method: str, maxEntriesPerRequest: Int = None, params={}): + newMaxEntriesPerRequest = None + newMaxEntriesPerRequest, params = self.handle_option_and_params(params, method, 'maxEntriesPerRequest') + if (newMaxEntriesPerRequest is not None) and (newMaxEntriesPerRequest != maxEntriesPerRequest): + maxEntriesPerRequest = newMaxEntriesPerRequest + if maxEntriesPerRequest is None: + maxEntriesPerRequest = 1000 # default to 1000 + return [maxEntriesPerRequest, params] + + def fetch_paginated_call_dynamic(self, method: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}, maxEntriesPerRequest: Int = None, removeRepeated=True): + maxCalls = None + maxCalls, params = self.handle_option_and_params(params, method, 'paginationCalls', 10) + maxRetries = None + maxRetries, params = self.handle_option_and_params(params, method, 'maxRetries', 3) + paginationDirection = None + paginationDirection, params = self.handle_option_and_params(params, method, 'paginationDirection', 'backward') + paginationTimestamp = None + removeRepeatedOption = removeRepeated + removeRepeatedOption, params = self.handle_option_and_params(params, method, 'removeRepeated', removeRepeated) + calls = 0 + result = [] + errors = 0 + until = self.safe_integer_n(params, ['until', 'untill', 'till']) # do not omit it from params here + maxEntriesPerRequest, params = self.handle_max_entries_per_request_and_params(method, maxEntriesPerRequest, params) + if (paginationDirection == 'forward'): + if since is None: + raise ArgumentsRequired(self.id + ' pagination requires a since argument when paginationDirection set to forward') + paginationTimestamp = since + while((calls < maxCalls)): + calls += 1 + try: + if paginationDirection == 'backward': + # do it backwards, starting from the last + # UNTIL filtering is required in order to work + if paginationTimestamp is not None: + params['until'] = paginationTimestamp - 1 + response = getattr(self, method)(symbol, None, maxEntriesPerRequest, params) + responseLength = len(response) + if self.verbose: + backwardMessage = 'Dynamic pagination call ' + self.number_to_string(calls) + ' method ' + method + ' response length ' + self.number_to_string(responseLength) + if paginationTimestamp is not None: + backwardMessage += ' timestamp ' + self.number_to_string(paginationTimestamp) + self.log(backwardMessage) + if responseLength == 0: + break + errors = 0 + result = self.array_concat(result, response) + firstElement = self.safe_value(response, 0) + paginationTimestamp = self.safe_integer_2(firstElement, 'timestamp', 0) + if (since is not None) and (paginationTimestamp <= since): + break + else: + # do it forwards, starting from the since + response = getattr(self, method)(symbol, paginationTimestamp, maxEntriesPerRequest, params) + responseLength = len(response) + if self.verbose: + forwardMessage = 'Dynamic pagination call ' + self.number_to_string(calls) + ' method ' + method + ' response length ' + self.number_to_string(responseLength) + if paginationTimestamp is not None: + forwardMessage += ' timestamp ' + self.number_to_string(paginationTimestamp) + self.log(forwardMessage) + if responseLength == 0: + break + errors = 0 + result = self.array_concat(result, response) + last = self.safe_value(response, responseLength - 1) + paginationTimestamp = self.safe_integer(last, 'timestamp') + 1 + if (until is not None) and (paginationTimestamp >= until): + break + except Exception as e: + errors += 1 + if errors > maxRetries: + raise e + uniqueResults = result + if removeRepeatedOption: + uniqueResults = self.remove_repeated_elements_from_array(result) + key = 0 if (method == 'fetchOHLCV') else 'timestamp' + return self.filter_by_since_limit(uniqueResults, since, limit, key) + + def safe_deterministic_call(self, method: str, symbol: Str = None, since: Int = None, limit: Int = None, timeframe: Str = None, params={}): + maxRetries = None + maxRetries, params = self.handle_option_and_params(params, method, 'maxRetries', 3) + errors = 0 + while(errors <= maxRetries): + try: + if timeframe and method != 'fetchFundingRateHistory': + return getattr(self, method)(symbol, timeframe, since, limit, params) + else: + return getattr(self, method)(symbol, since, limit, params) + except Exception as e: + if isinstance(e, RateLimitExceeded): + raise e # if we are rate limited, we should not retry and fail fast + errors += 1 + if errors > maxRetries: + raise e + return [] + + def fetch_paginated_call_deterministic(self, method: str, symbol: Str = None, since: Int = None, limit: Int = None, timeframe: Str = None, params={}, maxEntriesPerRequest=None): + maxCalls = None + maxCalls, params = self.handle_option_and_params(params, method, 'paginationCalls', 10) + maxEntriesPerRequest, params = self.handle_max_entries_per_request_and_params(method, maxEntriesPerRequest, params) + current = self.milliseconds() + tasks = [] + time = self.parse_timeframe(timeframe) * 1000 + step = time * maxEntriesPerRequest + currentSince = current - (maxCalls * step) - 1 + if since is not None: + currentSince = max(currentSince, since) + else: + currentSince = max(currentSince, 1241440531000) # avoid timestamps older than 2009 + until = self.safe_integer_2(params, 'until', 'till') # do not omit it here + if until is not None: + requiredCalls = int(math.ceil((until - since)) / step) + if requiredCalls > maxCalls: + raise BadRequest(self.id + ' the number of required calls is greater than the max number of calls allowed, either increase the paginationCalls or decrease the since-until gap. Current paginationCalls limit is ' + str(maxCalls) + ' required calls is ' + str(requiredCalls)) + for i in range(0, maxCalls): + if (until is not None) and (currentSince >= until): + break + if currentSince >= current: + break + tasks.append(self.safe_deterministic_call(method, symbol, currentSince, maxEntriesPerRequest, timeframe, params)) + currentSince = self.sum(currentSince, step) - 1 + results = tasks + result = [] + for i in range(0, len(results)): + result = self.array_concat(result, results[i]) + uniqueResults = self.remove_repeated_elements_from_array(result) + key = 0 if (method == 'fetchOHLCV') else 'timestamp' + return self.filter_by_since_limit(uniqueResults, since, limit, key) + + def fetch_paginated_call_cursor(self, method: str, symbol: Str = None, since=None, limit=None, params={}, cursorReceived=None, cursorSent=None, cursorIncrement=None, maxEntriesPerRequest=None): + maxCalls = None + maxCalls, params = self.handle_option_and_params(params, method, 'paginationCalls', 10) + maxRetries = None + maxRetries, params = self.handle_option_and_params(params, method, 'maxRetries', 3) + maxEntriesPerRequest, params = self.handle_max_entries_per_request_and_params(method, maxEntriesPerRequest, params) + cursorValue = None + i = 0 + errors = 0 + result = [] + timeframe = self.safe_string(params, 'timeframe') + params = self.omit(params, 'timeframe') # reading the timeframe from the method arguments to avoid changing the signature + while(i < maxCalls): + try: + if cursorValue is not None: + if cursorIncrement is not None: + cursorValue = self.parse_to_int(cursorValue) + cursorIncrement + params[cursorSent] = cursorValue + response = None + if method == 'fetchAccounts': + response = getattr(self, method)(params) + elif method == 'getLeverageTiersPaginated' or method == 'fetchPositions': + response = getattr(self, method)(symbol, params) + elif method == 'fetchOpenInterestHistory': + response = getattr(self, method)(symbol, timeframe, since, maxEntriesPerRequest, params) + else: + response = getattr(self, method)(symbol, since, maxEntriesPerRequest, params) + errors = 0 + responseLength = len(response) + if self.verbose: + cursorString = '' if (cursorValue is None) else cursorValue + iteration = (i + 1) + cursorMessage = 'Cursor pagination call ' + str(iteration) + ' method ' + method + ' response length ' + str(responseLength) + ' cursor ' + cursorString + self.log(cursorMessage) + if responseLength == 0: + break + result = self.array_concat(result, response) + last = self.safe_dict(response, responseLength - 1) + # cursorValue = self.safe_value(last['info'], cursorReceived) + cursorValue = None # search for the cursor + for j in range(0, responseLength): + index = responseLength - j - 1 + entry = self.safe_dict(response, index) + info = self.safe_dict(entry, 'info') + cursor = self.safe_value(info, cursorReceived) + if cursor is not None: + cursorValue = cursor + break + if cursorValue is None: + break + lastTimestamp = self.safe_integer(last, 'timestamp') + if lastTimestamp is not None and lastTimestamp < since: + break + except Exception as e: + errors += 1 + if errors > maxRetries: + raise e + i += 1 + sorted = self.sort_cursor_paginated_result(result) + key = 0 if (method == 'fetchOHLCV') else 'timestamp' + return self.filter_by_since_limit(sorted, since, limit, key) + + def fetch_paginated_call_incremental(self, method: str, symbol: Str = None, since=None, limit=None, params={}, pageKey=None, maxEntriesPerRequest=None): + maxCalls = None + maxCalls, params = self.handle_option_and_params(params, method, 'paginationCalls', 10) + maxRetries = None + maxRetries, params = self.handle_option_and_params(params, method, 'maxRetries', 3) + maxEntriesPerRequest, params = self.handle_max_entries_per_request_and_params(method, maxEntriesPerRequest, params) + i = 0 + errors = 0 + result = [] + while(i < maxCalls): + try: + params[pageKey] = i + 1 + response = getattr(self, method)(symbol, since, maxEntriesPerRequest, params) + errors = 0 + responseLength = len(response) + if self.verbose: + iteration = (i + str(1)) + incrementalMessage = 'Incremental pagination call ' + iteration + ' method ' + method + ' response length ' + str(responseLength) + self.log(incrementalMessage) + if responseLength == 0: + break + result = self.array_concat(result, response) + except Exception as e: + errors += 1 + if errors > maxRetries: + raise e + i += 1 + sorted = self.sort_cursor_paginated_result(result) + key = 0 if (method == 'fetchOHLCV') else 'timestamp' + return self.filter_by_since_limit(sorted, since, limit, key) + + def sort_cursor_paginated_result(self, result): + first = self.safe_value(result, 0) + if first is not None: + if 'timestamp' in first: + return self.sort_by(result, 'timestamp', True) + if 'id' in first: + return self.sort_by(result, 'id', True) + return result + + def remove_repeated_elements_from_array(self, input, fallbackToTimestamp: bool = True): + uniqueDic = {} + uniqueResult = [] + for i in range(0, len(input)): + entry = input[i] + uniqValue = self.safe_string_n(entry, ['id', 'timestamp', 0]) if fallbackToTimestamp else self.safe_string(entry, 'id') + if uniqValue is not None and not (uniqValue in uniqueDic): + uniqueDic[uniqValue] = 1 + uniqueResult.append(entry) + valuesLength = len(uniqueResult) + if valuesLength > 0: + return uniqueResult + return input + + def remove_repeated_trades_from_array(self, input): + uniqueResult = {} + for i in range(0, len(input)): + entry = input[i] + id = self.safe_string(entry, 'id') + if id is None: + price = self.safe_string(entry, 'price') + amount = self.safe_string(entry, 'amount') + timestamp = self.safe_string(entry, 'timestamp') + side = self.safe_string(entry, 'side') + # unique trade identifier + id = 't_' + str(timestamp) + '_' + side + '_' + price + '_' + amount + if id is not None and not (id in uniqueResult): + uniqueResult[id] = entry + values = list(uniqueResult.values()) + return values + + def remove_keys_from_dict(self, dict: dict, removeKeys: List[str]): + keys = list(dict.keys()) + newDict = {} + for i in range(0, len(keys)): + key = keys[i] + if not self.in_array(key, removeKeys): + newDict[key] = dict[key] + return newDict + + def handle_until_option(self, key: str, request, params, multiplier=1): + until = self.safe_integer_2(params, 'until', 'till') + if until is not None: + request[key] = self.parse_to_int(until * multiplier) + params = self.omit(params, ['until', 'till']) + return [request, params] + + def safe_open_interest(self, interest: dict, market: Market = None): + symbol = self.safe_string(interest, 'symbol') + if symbol is None: + symbol = self.safe_string(market, 'symbol') + return self.extend(interest, { + 'symbol': symbol, + 'baseVolume': self.safe_number(interest, 'baseVolume'), # deprecated + 'quoteVolume': self.safe_number(interest, 'quoteVolume'), # deprecated + 'openInterestAmount': self.safe_number(interest, 'openInterestAmount'), + 'openInterestValue': self.safe_number(interest, 'openInterestValue'), + 'timestamp': self.safe_integer(interest, 'timestamp'), + 'datetime': self.safe_string(interest, 'datetime'), + 'info': self.safe_value(interest, 'info'), + }) + + def parse_liquidation(self, liquidation, market: Market = None): + raise NotSupported(self.id + ' parseLiquidation() is not supported yet') + + def parse_liquidations(self, liquidations: List[dict], market: Market = None, since: Int = None, limit: Int = None): + """ + @ignore + parses liquidation info from the exchange response + :param dict[] liquidations: each item describes an instance of a liquidation event + :param dict market: ccxt market + :param int [since]: when defined, the response items are filtered to only include items after self timestamp + :param int [limit]: limits the number of items in the response + :returns dict[]: an array of `liquidation structures ` + """ + result = [] + for i in range(0, len(liquidations)): + entry = liquidations[i] + parsed = self.parse_liquidation(entry, market) + result.append(parsed) + sorted = self.sort_by(result, 'timestamp') + symbol = self.safe_string(market, 'symbol') + return self.filter_by_symbol_since_limit(sorted, symbol, since, limit) + + def parse_greeks(self, greeks: dict, market: Market = None): + raise NotSupported(self.id + ' parseGreeks() is not supported yet') + + def parse_all_greeks(self, greeks, symbols: Strings = None, params={}): + # + # the value of greeks is either a dict or a list + # + results = [] + if isinstance(greeks, list): + for i in range(0, len(greeks)): + parsedTicker = self.parse_greeks(greeks[i]) + greek = self.extend(parsedTicker, params) + results.append(greek) + else: + marketIds = list(greeks.keys()) + for i in range(0, len(marketIds)): + marketId = marketIds[i] + market = self.safe_market(marketId) + parsed = self.parse_greeks(greeks[marketId], market) + greek = self.extend(parsed, params) + results.append(greek) + symbols = self.market_symbols(symbols) + return self.filter_by_array(results, 'symbol', symbols) + + def parse_option(self, chain: dict, currency: Currency = None, market: Market = None): + raise NotSupported(self.id + ' parseOption() is not supported yet') + + def parse_option_chain(self, response: List[object], currencyKey: Str = None, symbolKey: Str = None): + optionStructures = {} + for i in range(0, len(response)): + info = response[i] + currencyId = self.safe_string(info, currencyKey) + currency = self.safe_currency(currencyId) + marketId = self.safe_string(info, symbolKey) + market = self.safe_market(marketId, None, None, 'option') + optionStructures[market['symbol']] = self.parse_option(info, currency, market) + return optionStructures + + def parse_margin_modes(self, response: List[object], symbols: List[str] = None, symbolKey: Str = None, marketType: MarketType = None): + marginModeStructures = {} + if marketType is None: + marketType = 'swap' # default to swap + for i in range(0, len(response)): + info = response[i] + marketId = self.safe_string(info, symbolKey) + market = self.safe_market(marketId, None, None, marketType) + if (symbols is None) or self.in_array(market['symbol'], symbols): + marginModeStructures[market['symbol']] = self.parse_margin_mode(info, market) + return marginModeStructures + + def parse_margin_mode(self, marginMode: dict, market: Market = None): + raise NotSupported(self.id + ' parseMarginMode() is not supported yet') + + def parse_leverages(self, response: List[object], symbols: List[str] = None, symbolKey: Str = None, marketType: MarketType = None): + leverageStructures = {} + if marketType is None: + marketType = 'swap' # default to swap + for i in range(0, len(response)): + info = response[i] + marketId = self.safe_string(info, symbolKey) + market = self.safe_market(marketId, None, None, marketType) + if (symbols is None) or self.in_array(market['symbol'], symbols): + leverageStructures[market['symbol']] = self.parse_leverage(info, market) + return leverageStructures + + def parse_leverage(self, leverage: dict, market: Market = None): + raise NotSupported(self.id + ' parseLeverage() is not supported yet') + + def parse_conversions(self, conversions: List[Any], code: Str = None, fromCurrencyKey: Str = None, toCurrencyKey: Str = None, since: Int = None, limit: Int = None, params={}): + conversions = self.to_array(conversions) + result = [] + fromCurrency = None + toCurrency = None + for i in range(0, len(conversions)): + entry = conversions[i] + fromId = self.safe_string(entry, fromCurrencyKey) + toId = self.safe_string(entry, toCurrencyKey) + if fromId is not None: + fromCurrency = self.safe_currency(fromId) + if toId is not None: + toCurrency = self.safe_currency(toId) + conversion = self.extend(self.parse_conversion(entry, fromCurrency, toCurrency), params) + result.append(conversion) + sorted = self.sort_by(result, 'timestamp') + currency = None + if code is not None: + currency = self.safe_currency(code) + code = currency['code'] + if code is None: + return self.filter_by_since_limit(sorted, since, limit) + fromConversion = self.filter_by(sorted, 'fromCurrency', code) + toConversion = self.filter_by(sorted, 'toCurrency', code) + both = self.array_concat(fromConversion, toConversion) + return self.filter_by_since_limit(both, since, limit) + + def parse_conversion(self, conversion: dict, fromCurrency: Currency = None, toCurrency: Currency = None): + raise NotSupported(self.id + ' parseConversion() is not supported yet') + + def convert_expire_date(self, date: str): + # parse YYMMDD to datetime string + year = date[0:2] + month = date[2:4] + day = date[4:6] + reconstructedDate = '20' + year + '-' + month + '-' + day + 'T00:00:00Z' + return reconstructedDate + + def convert_expire_date_to_market_id_date(self, date: str): + # parse 240119 to 19JAN24 + year = date[0:2] + monthRaw = date[2:4] + month = None + day = date[4:6] + if monthRaw == '01': + month = 'JAN' + elif monthRaw == '02': + month = 'FEB' + elif monthRaw == '03': + month = 'MAR' + elif monthRaw == '04': + month = 'APR' + elif monthRaw == '05': + month = 'MAY' + elif monthRaw == '06': + month = 'JUN' + elif monthRaw == '07': + month = 'JUL' + elif monthRaw == '08': + month = 'AUG' + elif monthRaw == '09': + month = 'SEP' + elif monthRaw == '10': + month = 'OCT' + elif monthRaw == '11': + month = 'NOV' + elif monthRaw == '12': + month = 'DEC' + reconstructedDate = day + month + year + return reconstructedDate + + def convert_market_id_expire_date(self, date: str): + # parse 03JAN24 to 240103. + monthMappping = { + 'JAN': '01', + 'FEB': '02', + 'MAR': '03', + 'APR': '04', + 'MAY': '05', + 'JUN': '06', + 'JUL': '07', + 'AUG': '08', + 'SEP': '09', + 'OCT': '10', + 'NOV': '11', + 'DEC': '12', + } + # if exchange omits first zero and provides i.e. '3JAN24' instead of '03JAN24' + if len(date) == 6: + date = '0' + date + year = date[0:2] + monthName = date[2:5] + month = self.safe_string(monthMappping, monthName) + day = date[5:7] + reconstructedDate = day + month + year + return reconstructedDate + + def fetch_position_history(self, symbol: str, since: Int = None, limit: Int = None, params={}): + """ + fetches the history of margin added or reduced from contract isolated positions + :param str [symbol]: unified market symbol + :param int [since]: timestamp in ms of the position + :param int [limit]: the maximum amount of candles to fetch, default=1000 + :param dict params: extra parameters specific to the exchange api endpoint + :returns dict[]: a list of `position structures ` + """ + if self.has['fetchPositionsHistory']: + positions = self.fetch_positions_history([symbol], since, limit, params) + return positions + else: + raise NotSupported(self.id + ' fetchPositionHistory() is not supported yet') + + def fetch_positions_history(self, symbols: Strings = None, since: Int = None, limit: Int = None, params={}): + """ + fetches the history of margin added or reduced from contract isolated positions + :param str [symbol]: unified market symbol + :param int [since]: timestamp in ms of the position + :param int [limit]: the maximum amount of candles to fetch, default=1000 + :param dict params: extra parameters specific to the exchange api endpoint + :returns dict[]: a list of `position structures ` + """ + raise NotSupported(self.id + ' fetchPositionsHistory() is not supported yet') + + def parse_margin_modification(self, data: dict, market: Market = None): + raise NotSupported(self.id + ' parseMarginModification() is not supported yet') + + def parse_margin_modifications(self, response: List[object], symbols: Strings = None, symbolKey: Str = None, marketType: MarketType = None): + marginModifications = [] + for i in range(0, len(response)): + info = response[i] + marketId = self.safe_string(info, symbolKey) + market = self.safe_market(marketId, None, None, marketType) + if (symbols is None) or self.in_array(market['symbol'], symbols): + marginModifications.append(self.parse_margin_modification(info, market)) + return marginModifications + + def fetch_transfer(self, id: str, code: Str = None, params={}): + """ + fetches a transfer + :param str id: transfer id + :param [str] code: unified currency code + :param dict params: extra parameters specific to the exchange api endpoint + :returns dict: a `transfer structure ` + """ + raise NotSupported(self.id + ' fetchTransfer() is not supported yet') + + def fetch_transfers(self, code: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches a transfer + :param str id: transfer id + :param int [since]: timestamp in ms of the earliest transfer to fetch + :param int [limit]: the maximum amount of transfers to fetch + :param dict params: extra parameters specific to the exchange api endpoint + :returns dict: a `transfer structure ` + """ + raise NotSupported(self.id + ' fetchTransfers() is not supported yet') + + def un_watch_ohlcv(self, symbol: str, timeframe: str = '1m', params={}): + """ + watches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + raise NotSupported(self.id + ' unWatchOHLCV() is not supported yet') + + def watch_mark_price(self, symbol: str, params={}): + """ + watches a mark price for a specific market + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + raise NotSupported(self.id + ' watchMarkPrice() is not supported yet') + + def watch_mark_prices(self, symbols: Strings = None, params={}): + """ + watches the mark price for all markets + :param str[] symbols: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + raise NotSupported(self.id + ' watchMarkPrices() is not supported yet') + + def withdraw_ws(self, code: str, amount: float, address: str, tag: Str = None, params={}): + """ + make a withdrawal + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the bitvavo api endpoint + :returns dict: a `transaction structure ` + """ + raise NotSupported(self.id + ' withdrawWs() is not supported yet') + + def un_watch_my_trades(self, symbol: Str = None, params={}): + """ + unWatches information on multiple trades made by the user + :param str symbol: unified market symbol of the market orders were made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + raise NotSupported(self.id + ' unWatchMyTrades() is not supported yet') + + def create_orders_ws(self, orders: List[OrderRequest], params={}): + """ + create a list of trade orders + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + raise NotSupported(self.id + ' createOrdersWs() is not supported yet') + + def fetch_orders_by_status_ws(self, status: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + raise NotSupported(self.id + ' fetchOrdersByStatusWs() is not supported yet') + + def un_watch_bids_asks(self, symbols: Strings = None, params={}): + """ + unWatches best bid & ask for symbols + :param str[] symbols: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + raise NotSupported(self.id + ' unWatchBidsAsks() is not supported yet') + + def clean_unsubscription(self, client, subHash: str, unsubHash: str, subHashIsPrefix=False): + if unsubHash in client.subscriptions: + del client.subscriptions[unsubHash] + if not subHashIsPrefix: + if subHash in client.subscriptions: + del client.subscriptions[subHash] + if subHash in client.futures: + error = UnsubscribeError(self.id + ' ' + subHash) + client.reject(error, subHash) + else: + clientSubscriptions = list(client.subscriptions.keys()) + for i in range(0, len(clientSubscriptions)): + sub = clientSubscriptions[i] + if sub.startswith(subHash): + del client.subscriptions[sub] + clientFutures = list(client.futures.keys()) + for i in range(0, len(clientFutures)): + future = clientFutures[i] + if future.startswith(subHash): + error = UnsubscribeError(self.id + ' ' + future) + client.reject(error, future) + client.resolve(True, unsubHash) + + def clean_cache(self, subscription: dict): + topic = self.safe_string(subscription, 'topic') + symbols = self.safe_list(subscription, 'symbols', []) + symbolsLength = len(symbols) + if topic == 'ohlcv': + symbolsAndTimeframes = self.safe_list(subscription, 'symbolsAndTimeframes', []) + for i in range(0, len(symbolsAndTimeframes)): + symbolAndTimeFrame = symbolsAndTimeframes[i] + symbol = self.safe_string(symbolAndTimeFrame, 0) + timeframe = self.safe_string(symbolAndTimeFrame, 1) + if (self.ohlcvs is not None) and (symbol in self.ohlcvs): + if timeframe in self.ohlcvs[symbol]: + del self.ohlcvs[symbol][timeframe] + elif symbolsLength > 0: + for i in range(0, len(symbols)): + symbol = symbols[i] + if topic == 'trades': + if symbol in self.trades: + del self.trades[symbol] + elif topic == 'orderbook': + if symbol in self.orderbooks: + del self.orderbooks[symbol] + elif topic == 'ticker': + if symbol in self.tickers: + del self.tickers[symbol] + elif topic == 'bidsasks': + if symbol in self.bidsasks: + del self.bidsasks[symbol] + else: + if topic == 'myTrades' and (self.myTrades is not None): + self.myTrades = None + elif topic == 'orders' and (self.orders is not None): + self.orders = None + elif topic == 'positions' and (self.positions is not None): + self.positions = None + clients = list(self.clients.values()) + for i in range(0, len(clients)): + client = clients[i] + futures = client.futures + if (futures is not None) and ('fetchPositionsSnapshot' in futures): + del futures['fetchPositionsSnapshot'] + elif (topic == 'ticker' or topic == 'markPrice') and (self.tickers is not None): + tickerSymbols = list(self.tickers.keys()) + for i in range(0, len(tickerSymbols)): + tickerSymbol = tickerSymbols[i] + if tickerSymbol in self.tickers: + del self.tickers[tickerSymbol] + elif topic == 'bidsasks' and (self.bidsasks is not None): + bidsaskSymbols = list(self.bidsasks.keys()) + for i in range(0, len(bidsaskSymbols)): + bidsaskSymbol = bidsaskSymbols[i] + if bidsaskSymbol in self.bidsasks: + del self.bidsasks[bidsaskSymbol] diff --git a/ccxt/base/precise.py b/ccxt/base/precise.py new file mode 100644 index 0000000..f6b89a1 --- /dev/null +++ b/ccxt/base/precise.py @@ -0,0 +1,297 @@ +# Author: carlo.revelli@berkeley.edu +# +# Precise +# Representation +# Expanding +# CCXT +# Internal +# Scientific +# Exponents +# +# (╯°□°)╯︵ ┻━┻ + + +class Precise: + def __init__(self, number, decimals=None): + if decimals is None: + modifier = 0 + number = number.lower() + if 'e' in number: + number, modifier = number.split('e') + modifier = int(modifier) + decimal_index = number.find('.') + if decimal_index > -1: + self.decimals = len(number) - decimal_index - 1 + self.integer = int(number.replace('.', '')) + else: + self.decimals = 0 + self.integer = int(number) + self.decimals = self.decimals - modifier + else: + self.integer = number + self.decimals = decimals + self.base = 10 + + def __add__(self, other): + return self.add(other) + + def __sub__(self, other): + return self.sub(other) + + def __mul__(self, other): + return self.mul(other) + + def __truediv__(self, other): + return self.div(other) + + def __mod__(self, other): + return self.mod(other) + + def __neg__(self): + return self.neg() + + def __abs__(self): + return self.abs() + + def __min__(self, other): + return self.min(other) + + def __max__(self, other): + return self.max(other) + + def __lt__(self, other): + return self.lt(other) + + def __le__(self, other): + return self.le(other) + + def __gt__(self, other): + return self.gt(other) + + def __ge__(self, other): + return self.ge(other) + + def __eq__(self, other): + if isinstance(other, str): + # Allow comparisons with Precise("5") == "5" + return str(self) == other + return self.equals(other) + + def mul(self, other): + integer_result = self.integer * other.integer + return Precise(integer_result, self.decimals + other.decimals) + + def div(self, other, precision=18): + distance = precision - self.decimals + other.decimals + if distance == 0: + numerator = self.integer + elif distance < 0: + exponent = self.base ** -distance + numerator = self.integer // exponent + else: + exponent = self.base ** distance + numerator = self.integer * exponent + result, mod = divmod(numerator, other.integer) + # python floors negative numbers down instead of truncating + # if mod is zero it will be floored to itself so we do not add one + result = result + 1 if result < 0 and mod else result + return Precise(result, precision) + + def add(self, other): + if self.decimals == other.decimals: + integer_result = self.integer + other.integer + return Precise(integer_result, self.decimals) + else: + smaller, bigger = [other, self] if self.decimals > other.decimals else [self, other] + exponent = bigger.decimals - smaller.decimals + normalised = smaller.integer * (self.base ** exponent) + result = normalised + bigger.integer + return Precise(result, bigger.decimals) + + def sub(self, other): + negative = Precise(-other.integer, other.decimals) + return self.add(negative) + + def abs(self): + return Precise(abs(self.integer), self.decimals) + + def neg(self): + return Precise(-self.integer, self.decimals) + + def mod(self, other): + rationizerNumberator = max(-self.decimals + other.decimals, 0) + numerator = self.integer * (self.base ** rationizerNumberator) + rationizerDenominator = max(-other.decimals + self.decimals, 0) + denominator = other.integer * (self.base ** rationizerDenominator) + result = numerator % denominator + return Precise(result, rationizerDenominator + other.decimals) + + def orn(self, other): + integer_result = self.integer | other.integer + return Precise(integer_result, self.decimals) + + def min(self, other): + return self if self.lt(other) else other + + def max(self, other): + return self if self.gt(other) else other + + def gt(self, other): + add = self.sub(other) + return add.integer > 0 + + def ge(self, other): + add = self.sub(other) + return add.integer >= 0 + + def lt(self, other): + return other.gt(self) + + def le(self, other): + return other.ge(self) + + def reduce(self): + string = str(self.integer) + start = len(string) - 1 + if start == 0: + if string == "0": + self.decimals = 0 + return self + for i in range(start, -1, -1): + if string[i] != '0': + break + difference = start - i + if difference == 0: + return self + self.decimals -= difference + self.integer = int(string[:i + 1]) + + def equals(self, other): + self.reduce() + other.reduce() + return self.decimals == other.decimals and self.integer == other.integer + + def __str__(self): + self.reduce() + sign = '-' if self.integer < 0 else '' + integer_array = list(str(abs(self.integer)).rjust(self.decimals, '0')) + index = len(integer_array) - self.decimals + if index == 0: + item = '0.' + elif self.decimals < 0: + item = '0' * (-self.decimals) + elif self.decimals == 0: + item = '' + else: + item = '.' + integer_array.insert(index, item) + return sign + ''.join(integer_array) + + def __repr__(self): + return "Precise(" + str(self) + ")" + + def __float__(self): + return float(str(self)) + + @staticmethod + def string_mul(string1, string2): + if string1 is None or string2 is None: + return None + return str(Precise(string1).mul(Precise(string2))) + + @staticmethod + def string_div(string1, string2, precision=18): + if string1 is None or string2 is None: + return None + string2_precise = Precise(string2) + if string2_precise.integer == 0: + return None + return str(Precise(string1).div(string2_precise, precision)) + + @staticmethod + def string_add(string1, string2): + if string1 is None and string2 is None: + return None + if string1 is None: + return string2 + elif string2 is None: + return string1 + return str(Precise(string1).add(Precise(string2))) + + @staticmethod + def string_sub(string1, string2): + if string1 is None or string2 is None: + return None + return str(Precise(string1).sub(Precise(string2))) + + @staticmethod + def string_abs(string): + if string is None: + return None + return str(Precise(string).abs()) + + @staticmethod + def string_neg(string): + if string is None: + return None + return str(Precise(string).neg()) + + @staticmethod + def string_mod(string1, string2): + if string1 is None or string2 is None: + return None + return str(Precise(string1).mod(Precise(string2))) + + @staticmethod + def string_or(string1, string2): + if string1 is None or string2 is None: + return None + return str(Precise(string1).orn(Precise(string2))) + + @staticmethod + def string_equals(string1, string2): + if string1 is None or string2 is None: + return None + return Precise(string1).equals(Precise(string2)) + + @staticmethod + def string_eq(string1, string2): + if string1 is None or string2 is None: + return None + return Precise(string1).equals(Precise(string2)) + + @staticmethod + def string_min(string1, string2): + if string1 is None or string2 is None: + return None + return str(Precise(string1).min(Precise(string2))) + + @staticmethod + def string_max(string1, string2): + if string1 is None or string2 is None: + return None + return str(Precise(string1).max(Precise(string2))) + + @staticmethod + def string_gt(string1, string2): + if string1 is None or string2 is None: + return None + return Precise(string1).gt(Precise(string2)) + + @staticmethod + def string_ge(string1, string2): + if string1 is None or string2 is None: + return None + return Precise(string1).ge(Precise(string2)) + + @staticmethod + def string_lt(string1, string2): + if string1 is None or string2 is None: + return None + return Precise(string1).lt(Precise(string2)) + + @staticmethod + def string_le(string1, string2): + if string1 is None or string2 is None: + return None + return Precise(string1).le(Precise(string2)) diff --git a/ccxt/base/types.py b/ccxt/base/types.py new file mode 100644 index 0000000..fdd28c1 --- /dev/null +++ b/ccxt/base/types.py @@ -0,0 +1,612 @@ +import sys +import types +from typing import Union, List, Optional, Any as PythonAny +from decimal import Decimal + + +if sys.version_info >= (3, 8): + from typing import TypedDict, Literal, Dict +else: + from typing import Dict + from typing_extensions import Literal + TypedDict = Dict + +if sys.version_info >= (3, 11): + from typing import NotRequired +else: + from typing_extensions import NotRequired + + +OrderSide = Literal['buy', 'sell'] +OrderType = Literal['limit', 'market'] +PositionSide = Literal['long', 'short'] +Any = PythonAny + + +class Entry: + def __init__(self, path, api, method, config): + self.name = None + self.path = path + self.api = api + self.method = method + self.config = config + + def unbound_method(_self, params={}): + return _self.request(self.path, self.api, self.method, params, config=self.config) + + self.unbound_method = unbound_method + + def __get__(self, instance, owner): + if instance is None: + return self.unbound_method + else: + return types.MethodType(self.unbound_method, instance) + + def __set_name__(self, owner, name): + self.name = name + + +IndexType = Union[str, int] +Num = Union[None, str, float, int, Decimal] +Str = Optional[str] +Strings = Optional[List[str]] +Int = Optional[int] +Bool = Optional[bool] +MarketType = Literal['spot', 'margin', 'swap', 'future', 'option'] +SubType = Literal['linear', 'inverse'] + + +class FeeInterface(TypedDict): + currency: Str + cost: Num + rate: NotRequired[Num] + + +Fee = Optional[FeeInterface] + + +class TradingFeeInterface(TypedDict): + info: Dict[str, Any] + symbol: Str + maker: Num + taker: Num + percentage: Bool + tierBased: Bool + + +class Balance(TypedDict): + free: Num + used: Num + total: Num + debt: NotRequired[Num] + + +class BalanceAccount(TypedDict): + free: Str + used: Str + total: Str + + +class Account(TypedDict): + id: Str + type: Str + code: Str + info: Dict[str, Any] + + +class Trade(TypedDict): + info: Dict[str, Any] + amount: Num + datetime: Str + id: Str + order: Str + price: Num + timestamp: Int + type: Str + side: Str + symbol: Str + takerOrMaker: Str + cost: Num + fee: Fee + + +class Position(TypedDict): + info: Dict[str, Any] + symbol: Str + id: Str + timestamp: Int + datetime: Str + contracts: Num + contractSize: Num + side: Str + notional: Num + leverage: Num + unrealizedPnl: Num + realizedPnl: Num + collateral: Num + entryPrice: Num + markPrice: Num + liquidationPrice: Num + hedged: bool + maintenanceMargin: Num + initialMargin: Num + initialMarginPercentage: Num + marginMode: Str + marginRatio: Num + lastUpdateTimestamp: Int + lastPrice: Num + percentage: Num + stopLossPrice: Num + takeProfitPrice: Num + +class OrderRequest(TypedDict): + symbol: Str + type: Str + side: Str + amount: Union[None, float] + price: Union[None, float] + params: Dict[str, Any] + + +class CancellationRequest(TypedDict): + id: Str + symbol: Str + clientOrderId: Str + + +class Order(TypedDict): + info: Dict[str, Any] + id: Str + clientOrderId: Str + datetime: Str + timestamp: Int + lastTradeTimestamp: Int + lastUpdateTimestamp: Int + status: Str + symbol: Str + type: Str + timeInForce: Str + side: OrderSide + price: Num + average: Num + amount: Num + filled: Num + remaining: Num + stopPrice: Num + takeProfitPrice: Num + stopLossPrice: Num + cost: Num + trades: List[Trade] + reduceOnly: Bool + postOnly: Bool + fee: Fee + + +class Liquidation(TypedDict): + info: Dict[str, Any] + symbol: Str + timestamp: Int + datetime: Str + price: Num + baseValue: Num + quoteValue: Num + side: OrderSide + contracts: Num + contractSize: Num + + +class FundingHistory(TypedDict): + info: Dict[str, Any] + symbol: Str + code: Str + timestamp: Int + datetime: Str + id: Str + amount: Num + + +class Balances(Dict[str, Balance]): + datetime: Str + timestamp: Int + + +class OrderBook(TypedDict): + asks: List[List[Num]] + bids: List[List[Num]] + datetime: Str + timestamp: Int + nonce: Int + symbol: Str + + +class Transaction(TypedDict): + info: Dict[str, any] + id: Str + txid: Str + timestamp: Int + datetime: Str + address: Str + addressFrom: Str + addressTo: Str + tag: Str + tagFrom: Str + tagTo: Str + type: Str + amount: Num + currency: Str + status: Str + updated: Int + fee: Fee + network: Str + comment: Str + internal: Bool + + +class TransferEntry(TypedDict): + info: Dict[str, any] + id: Str + timestamp: Int + datetime: Str + currency: Str + amount: Num + fromAccount: Str + toAccount: Str + status: Str + + +class Ticker(TypedDict): + info: Dict[str, Any] + symbol: Str + timestamp: Int + datetime: Str + high: Num + low: Num + bid: Num + bidVolume: Num + ask: Num + askVolume: Num + vwap: Num + open: Num + close: Num + last: Num + previousClose: Num + change: Num + percentage: Num + average: Num + quoteVolume: Num + baseVolume: Num + markPrice: Num + indexPrice: Num + + +Tickers = Dict[str, Ticker] + +OrderBooks = Dict[str, OrderBook] +class MarginMode(TypedDict): + info: Dict[str, Any] + symbol: Str + marginMode: Str + + +MarginModes = Dict[str, MarginMode] + + +class Leverage(TypedDict): + info: Dict[str, Any] + symbol: Str + marginMode: Str + longLeverage: Num + shortLeverage: Num + + +Leverages = Dict[str, Leverage] + + +class Greeks(TypedDict): + symbol: Str + timestamp: Int + datetime: Str + delta: Num + gamma: Num + theta: Num + vega: Num + rho: Num + vanna: Num + volga: Num + charm: Num + bidSize: Num + askSize: Num + bidImpliedVolatility: Num + askImpliedVolatility: Num + markImpliedVolatility: Num + bidPrice: Num + askPrice: Num + markPrice: Num + lastPrice: Num + underlyingPrice: Num + info: Dict[str, Any] + + +class Conversion(TypedDict): + info: Dict[str, Any] + timestamp: Int + datetime: Str + id: Str + fromCurrency: Str + fromAmount: Num + toCurrency: Str + toAmount: Num + price: Num + fee: Num + + +class Option(TypedDict): + info: Dict[str, Any] + currency: Str + symbol: Str + timestamp: Int + datetime: Str + impliedVolatility: Num + openInterest: Num + bidPrice: Num + askPrice: Num + midPrice: Num + markPrice: Num + lastPrice: Num + underlyingPrice: Num + change: Num + percentage: Num + baseVolume: Num + quoteVolume: Num + + +OptionChain = Dict[str, Option] + +class MarketMarginModes(TypedDict): + cross: bool + isolated: bool + +class MinMax(TypedDict): + min: Num + max: Num + +class MarketLimits(TypedDict): + amount: Optional[MinMax] + cost: Optional[MinMax] + leverage: Optional[MinMax] + price: Optional[MinMax] + market: Optional[MinMax] + +class MarketInterface(TypedDict): + info: Dict[str, Any] + id: Str + symbol: Str + base: Str + quote: Str + baseId: Str + quoteId: Str + active: Bool + type: Str + subType: Str + spot: bool + margin: bool + marginModes: MarketMarginModes + swap: bool + future: bool + option: bool + contract: bool + settle: Str + settleId: Str + contractSize: Num + linear: bool + inverse: bool + expiry: Num + expiryDatetime: Str + strike: Num + optionType: Str + taker: Num + maker: Num + percentage: bool + tierBased: bool + feeSide: Str + precision: Any + limits: MarketLimits + created: Int + +class Limit(TypedDict): + min: Num + max: Num + + +class CurrencyLimits(TypedDict): + amount: Limit + withdraw: Limit + + +class CurrencyInterface(TypedDict): + id: Str + code: Str + numericId: Int + precision: Num + type: Str + margin: Bool + name: Str + active: Bool + deposit: Bool + withdraw: Bool + fee: Num + limits: CurrencyLimits + networks: Dict[str, any] + info: any + + +class LastPrice(TypedDict): + symbol: Str + timestamp: Int + datetime: Str + price: Num + side: OrderSide + info: Dict[str, Any] + + +class MarginModification(TypedDict): + info: Dict[str, any] + symbol: str + type: Optional[Literal['add', 'reduce', 'set']] + marginMode: Optional[Literal['isolated', 'cross']] + amount: Optional[float] + code: Str + status: Str + timestamp: Int + datetime: Str + + +class CrossBorrowRate(TypedDict): + info: Dict[str, any] + currency: Str + rate: float + period: Optional[float] + timestamp: Int + datetime: Str + + +class IsolatedBorrowRate(TypedDict): + info: Dict[str, any] + symbol: str + base: str + baseRate: float + quote: str + quoteRate: float + period: Int + timestamp: Int + datetime: Str + + +class FundingRate(TypedDict): + symbol: Str + timestamp: Int + fundingRate: Num + datetime: Str + markPrice: Num + indexPrice: Num + interestRate: Num + estimatedSettlePrice: Num + fundingTimestamp: Int + fundingDatetime: Str + nextFundingTimestamp: Int + nextFundingDatetime: Str + nextFundingRate: Num + previousFundingTimestamp: Int + previousFundingDatetime: Str + previousFundingRate: Num + info: Dict[str, Any] + interval: Str + +class OpenInterest(TypedDict): + symbol: Str + openInterestAmount: Num + openInterestValue: Num + baseVolume: Num + quoteVolume: Num + timestamp: Int + datetime: Str + info: Dict[str, Any] + +class LeverageTier: + tier: Num + symbol: Str + currency: Str + minNotional: Num + maxNotional: Num + maintenanceMarginRate: Num + maxLeverage: Num + info: Dict[str, Any] + + +class LedgerEntry: + id: Str + info: Any + timestamp: Int + datetime: Str + direction: Str + account: Str + referenceId: Str + referenceAccount: Str + type: Str + currency: Str + amount: Str + before: float + after: float + status: Str + fee: Fee + + +class DepositAddress: + info: Any + currency: Str + network: Optional[Str] + address: Str + tag: Optional[Str] + + +class LongShortRatio: + info: Any + symbol: Str + timestamp: Optional[Int] + datetime: Optional[Str] + timeframe: Optional[Str] + longShortRatio: float + + +class BorrowInterest: + info: Any + symbol: Optional[Str] + currency: Optional[Str] + interest: Optional[Num] + interestRate: Optional[Num] + amountBorrowed: Optional[Num] + marginMode: Optional[Str] + timestamp: Optional[Int] + datetime: Optional[Str] + + +FundingRates = Dict[Str, FundingRate] +OpenInterests = Dict[Str, OpenInterest] +LastPrices = Dict[Str, LastPrice] +Currencies = Dict[Str, CurrencyInterface] +TradingFees = Dict[Str, TradingFeeInterface] +IsolatedBorrowRates = Dict[Str, IsolatedBorrowRate] +CrossBorrowRates = Dict[Str, CrossBorrowRate] +LeverageTiers = Dict[Str, List[LeverageTier]] + +Market = Optional[MarketInterface] +Currency = Optional[CurrencyInterface] + +class ConstructorArgs(TypedDict, total=False): + apiKey: str + secret: str + passphrase: str + password: str + privateKey: str + walletAddress: str + uid: str + verbose: bool + testnet: bool + sandbox: bool # redudant but kept for backwards compatibility + options: Dict[str, Any] + enableRateLimit: bool + httpsProxy: str + socksProxy: str + wssProxy: str + proxy: str + rateLimit: Num + commonCurrencies: Dict[str, Any] + userAgent: str + userAgents: Dict[str, Any] + timeout: Num + markets: Dict[str, Any] + currencies: Dict[str, Any] + hostname: str + urls: Dict[str, Any] + headers: Dict[str, Any] + session: Any diff --git a/ccxt/bequant.py b/ccxt/bequant.py new file mode 100644 index 0000000..c67a7e5 --- /dev/null +++ b/ccxt/bequant.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.hitbtc import hitbtc +from ccxt.abstract.bequant import ImplicitAPI +from ccxt.base.types import Any + + +class bequant(hitbtc, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(bequant, self).describe(), { + 'id': 'bequant', + 'name': 'Bequant', + 'pro': True, + 'countries': ['MT'], # Malta + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/0583ef1f-29fe-4b7c-8189-63565a0e2867', + 'api': { + # v3 + 'public': 'https://api.bequant.io/api/3', + 'private': 'https://api.bequant.io/api/3', + }, + 'www': 'https://bequant.io', + 'doc': [ + 'https://api.bequant.io/', + ], + 'fees': [ + 'https://bequant.io/fees-and-limits', + ], + 'referral': 'https://bequant.io/referral/dd104e3bee7634ec', + }, + }) diff --git a/ccxt/bigone.py b/ccxt/bigone.py new file mode 100644 index 0000000..81f01bf --- /dev/null +++ b/ccxt/bigone.py @@ -0,0 +1,2267 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.bigone import ImplicitAPI +from ccxt.base.types import Any, Balances, Currencies, Currency, DepositAddress, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, Transaction, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import NotSupported +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class bigone(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(bigone, self).describe(), { + 'id': 'bigone', + 'name': 'BigONE', + 'countries': ['CN'], + 'version': 'v3', + 'rateLimit': 20, # 500 requests per 10 seconds + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': True, + 'future': None, # has but unimplemented + 'option': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'createMarketBuyOrderWithCost': True, + 'createMarketOrderWithCost': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createPostOnlyOrder': True, + 'createStopLimitOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'fetchAllGreeks': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchFundingHistory': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchMarkets': True, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': False, + 'fetchTransactionFees': False, + 'fetchVolatilityHistory': False, + 'fetchWithdrawals': True, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'transfer': True, + 'withdraw': True, + }, + 'timeframes': { + '1m': 'min1', + '5m': 'min5', + '15m': 'min15', + '30m': 'min30', + '1h': 'hour1', + '3h': 'hour3', + '4h': 'hour4', + '6h': 'hour6', + '12h': 'hour12', + '1d': 'day1', + '1w': 'week1', + '1M': 'month1', + }, + 'hostname': 'big.one', # or 'bigone.com' + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/4e5cfd53-98cc-4b90-92cd-0d7b512653d1', + 'api': { + 'public': 'https://{hostname}/api/v3', + 'private': 'https://{hostname}/api/v3/viewer', + 'contractPublic': 'https://{hostname}/api/contract/v2', + 'contractPrivate': 'https://{hostname}/api/contract/v2', + 'webExchange': 'https://{hostname}/api/', + }, + 'www': 'https://big.one', + 'doc': 'https://open.big.one/docs/api.html', + 'fees': 'https://bigone.zendesk.com/hc/en-us/articles/115001933374-BigONE-Fee-Policy', + 'referral': 'https://b1.run/users/new?code=D3LLBVFT', + }, + 'api': { + 'public': { + 'get': [ + 'ping', + 'asset_pairs', + 'asset_pairs/{asset_pair_name}/depth', + 'asset_pairs/{asset_pair_name}/trades', + 'asset_pairs/{asset_pair_name}/ticker', + 'asset_pairs/{asset_pair_name}/candles', + 'asset_pairs/tickers', + ], + }, + 'private': { + 'get': [ + 'accounts', + 'fund/accounts', + 'assets/{asset_symbol}/address', + 'orders', + 'orders/{id}', + 'orders/multi', + 'trades', + 'withdrawals', + 'deposits', + ], + 'post': [ + 'orders', + 'orders/{id}/cancel', + 'orders/cancel', + 'withdrawals', + 'transfer', + ], + }, + 'contractPublic': { + 'get': [ + 'symbols', + 'instruments', + 'depth@{symbol}/snapshot', + 'instruments/difference', + 'instruments/prices', + ], + }, + 'contractPrivate': { + 'get': [ + 'accounts', + 'orders/{id}', + 'orders', + 'orders/opening', + 'orders/count', + 'orders/opening/count', + 'trades', + 'trades/count', + ], + 'post': [ + 'orders', + 'orders/batch', + ], + 'put': [ + 'positions/{symbol}/margin', + 'positions/{symbol}/risk-limit', + ], + 'delete': [ + 'orders/{id}', + 'orders/batch', + ], + }, + 'webExchange': { + 'get': [ + 'v3/assets', + ], + }, + }, + 'fees': { + 'trading': { + 'maker': self.parse_number('0.001'), + 'taker': self.parse_number('0.001'), + }, + 'funding': { + 'withdraw': {}, + }, + }, + 'options': { + 'createMarketBuyOrderRequiresPrice': True, + 'accountsByType': { + 'spot': 'SPOT', + 'fund': 'FUND', + 'funding': 'FUND', + 'future': 'CONTRACT', + 'swap': 'CONTRACT', + }, + 'transfer': { + 'fillResponseFromRequest': True, + }, + 'exchangeMillisecondsCorrection': -100, + 'fetchCurrencies': { + 'webApiEnable': True, # fetches from WEB + 'webApiRetries': 5, + 'webApiMuteFailure': True, + }, + 'defaultNetwork': 'ERC20', + 'defaultNetworks': { + 'USDT': 'TRC20', + }, + 'networks': { + 'ABBC': 'ABBC', + 'ACA': 'Acala', + 'AE': 'Aeternity', + 'ALGO': 'Algorand', + 'APT': 'Aptos', + 'AR': 'Arweave', + 'ASTR': 'Astar', + 'AVAXC': 'Avax', + 'AVAXX': 'AvaxChain', + 'BEAM': 'Beam', + 'BEP20': 'BinanceSmartChain', + 'BITCI': 'BitciChain', + 'BTC': 'Bitcoin', + 'BCH': 'BitcoinCash', + 'BSV': 'BitcoinSV', + 'CELO': 'Celo', + 'CKKB': 'CKB', + 'ATOM': 'Cosmos', + 'CRC20': 'CRO', + 'DASH': 'Dash', + 'DOGE': 'Dogecoin', + 'XEC': 'ECash', + 'EOS': 'EOS', + 'ETH': 'Ethereum', + 'ETC': 'EthereumClassic', + 'ETHW': 'EthereumPow', + 'FTM': 'Fantom', + 'FIL': 'Filecoin', + 'FSN': 'Fusion', + 'GRIN': 'Grin', + 'ONE': 'Harmony', + 'HRC20': 'Hecochain', + 'HBAR': 'Hedera', + 'HNT': 'Helium', + 'ZEN': 'Horizen', + 'IOST': 'IOST', + 'IRIS': 'IRIS', + 'KLAY': 'Klaytn', + 'KSM': 'Kusama', + 'LTC': 'Litecoin', + 'XMR': 'Monero', + 'GLMR': 'Moonbeam', + 'NEAR': 'Near', + 'NEO': 'Neo', + 'NEON3': 'NeoN3', + 'OASIS': 'Oasis', + 'OKC': 'Okexchain', + 'ONT': 'Ontology', + 'OPTIMISM': 'Optimism', + 'DOT': 'Polkadot', + 'MATIC': 'Polygon', + 'QTUM': 'Qtum', + 'REI': 'REI', + 'XRP': 'Ripple', + 'SGB': 'SGB', + 'SDN': 'Shiden', + 'SOL': 'Solana', + 'XLM': 'Stellar', + 'TERA': 'Tera', + 'XTZ': 'Tezos', + 'TRC20': 'Tron', + 'VET': 'Vechain', + 'VSYS': 'VSystems', + 'WAX': 'WAX', + 'ZEC': 'Zcash', + # todo: uncomment after consensus + # 'BITSHARES_OLD': 'Bitshares', + # 'BITSHARES_NEW': 'NewBitshares', + # 'MOBILECOIN': 'Mobilecoin', + # 'LBRY': 'Lbry', + # 'ZEEPIN': 'Zeepin', + # 'WAYFCOIN': 'Wayfcoin', + # 'UCACOIN': 'Ucacoin', + # 'VANILLACASH': 'Vcash', + # 'LAMDEN': 'Lamden', + # 'GXSHARES': 'Gxshares', + # 'ICP': 'Dfinity', + # 'CLOVER': 'Clover', + # 'CLASSZZ': 'Classzz', + # 'CLASSZZ_V2': 'ClasszzV2', + # 'CHAINX_V2': 'ChainxV2', + # 'BITCOINDIAMON': 'BitcoinDiamond', + # 'BITCOINGOLD': 'BitcoinGold', + # 'BUTTRUSTSYSTEM': 'BitTrustSystem', + # 'BYTOM_V2': 'BytomV2', + # 'LIBONOMY': 'Libonomy', + # 'TERRACLASSIC': 'Terra', + # 'TERRA': 'Terra2', + # 'SUPERBITCOIN': 'SuperBitcoin', + # 'SIACLASSIC': 'Sia', + # 'SIACOIN': 'SiaCore', + # 'PARALLELFINANCE': 'Parallel', + # 'PLCULTIMA': 'Plcu', + # 'PLCULTIMA2': 'Plcu2', + # undetermined: XinFin, YAS, Ycash + }, + }, + 'features': { + 'default': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerPriceType': None, + 'triggerDirection': True, # todo implement + 'stopLossPrice': False, # todo by trigger + 'takeProfitPrice': False, # todo by trigger + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': False, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyRequiresPrice': True, + 'marketBuyByCost': True, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, # todo: implement + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 200, + 'daysBack': None, + 'untilDays': None, + 'symbolRequired': True, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 200, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': 200, + 'daysBack': None, + 'untilDays': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 200, + 'daysBack': None, + 'daysBackCanceled': None, + 'untilDays': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOHLCV': { + 'limit': 500, + }, + }, + 'spot': { + 'extends': 'default', + }, + 'forDerivatives': { + 'extends': 'default', + 'createOrder': { + # todo: implement + 'triggerPriceType': { + 'mark': True, + 'index': True, + 'last': True, + }, + }, + 'fetchOrders': { + 'daysBack': 100000, + 'untilDays': 100000, + }, + 'fetchClosedOrders': { + 'daysBack': 100000, + 'untilDays': 100000, + }, + }, + 'swap': { + 'linear': { + 'extends': 'forDerivatives', + }, + 'inverse': { + 'extends': 'forDerivatives', + }, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'exact': { + '10001': BadRequest, # syntax error + '10005': ExchangeError, # internal error + "Amount's scale must greater than AssetPair's base scale": InvalidOrder, + "Price mulit with amount should larger than AssetPair's min_quote_value": InvalidOrder, + '10007': BadRequest, # parameter error, {"code":10007,"message":"Amount's scale must greater than AssetPair's base scale"} + '10011': ExchangeError, # system error + '10013': BadSymbol, # {"code":10013,"message":"Resource not found"} + '10014': InsufficientFunds, # {"code":10014,"message":"Insufficient funds"} + '10403': PermissionDenied, # permission denied + '10429': RateLimitExceeded, # too many requests + '40004': AuthenticationError, # {"code":40004,"message":"invalid jwt"} + '40103': AuthenticationError, # invalid otp code + '40104': AuthenticationError, # invalid asset pin code + '40301': PermissionDenied, # {"code":40301,"message":"Permission denied withdrawal create"} + '40302': ExchangeError, # already requested + '40601': ExchangeError, # resource is locked + '40602': ExchangeError, # resource is depleted + '40603': InsufficientFunds, # insufficient resource + '40604': InvalidOrder, # {"code":40604,"message":"Price exceed the maximum order price"} + '40605': InvalidOrder, # {"code":40605,"message":"Price less than the minimum order price"} + '40120': InvalidOrder, # Order is in trading + '40121': InvalidOrder, # Order is already cancelled or filled + '60100': BadSymbol, # {"code":60100,"message":"Asset pair is suspended"} + }, + 'broad': { + }, + }, + 'commonCurrencies': { + 'CRE': 'Cybereits', + 'FXT': 'FXTTOKEN', + 'FREE': 'FreeRossDAO', + 'MBN': 'Mobilian Coin', + 'ONE': 'BigONE Token', + }, + }) + + def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + # we use undocumented link(possible, less informative alternative is : https://big.one/api/uc/v3/assets/accounts) + data = self.fetch_web_endpoint('fetchCurrencies', 'webExchangeGetV3Assets', True) + if data is None: + return {} + # + # { + # "code": "0", + # "message": "", + # "data": [ + # { + # "uuid": "17082d1c-0195-4fb6-8779-2cdbcb9eeb3c", + # "symbol": "USDT", + # "name": "TetherUS", + # "scale": 12, + # "is_fiat": False, + # "is_transfer_enabled": True, + # "transfer_scale": 12, + # "binding_gateways": [ + # { + # "guid": "07efc37f-d1ec-4bc9-8339-a745256ea2ba", + # "is_deposit_enabled": True, + # "gateway_name": "Ethereum", + # "min_withdrawal_amount": "0.000001", + # "withdrawal_fee": "5.71", + # "is_withdrawal_enabled": True, + # "min_deposit_amount": "0.000001", + # "is_memo_required": False, + # "withdrawal_scale": 6, + # "scale": 12 + # }, + # { + # "guid": "4e387a9a-a480-40a3-b4ae-ed1773c2db5a", + # "is_deposit_enabled": True, + # "gateway_name": "BinanceSmartChain", + # "min_withdrawal_amount": "10", + # "withdrawal_fee": "5", + # "is_withdrawal_enabled": False, + # "min_deposit_amount": "1", + # "is_memo_required": False, + # "withdrawal_scale": 8, + # "scale": 12 + # } + # ] + # }, + # ... + # ], + # } + # + currenciesData = self.safe_list(data, 'data', []) + result: dict = {} + for i in range(0, len(currenciesData)): + currency = currenciesData[i] + id = self.safe_string(currency, 'symbol') + code = self.safe_currency_code(id) + name = self.safe_string(currency, 'name') + networks: dict = {} + chains = self.safe_list(currency, 'binding_gateways', []) + currencyMaxPrecision = self.parse_precision(self.safe_string_2(currency, 'withdrawal_scale', 'scale')) + for j in range(0, len(chains)): + chain = chains[j] + networkId = self.safe_string(chain, 'gateway_name') + networkCode = self.network_id_to_code(networkId) + deposit = self.safe_bool(chain, 'is_deposit_enabled') + withdraw = self.safe_bool(chain, 'is_withdrawal_enabled') + minDepositAmount = self.safe_string(chain, 'min_deposit_amount') + minWithdrawalAmount = self.safe_string(chain, 'min_withdrawal_amount') + withdrawalFee = self.safe_string(chain, 'withdrawal_fee') + precision = self.parse_precision(self.safe_string_2(chain, 'withdrawal_scale', 'scale')) + networks[networkCode] = { + 'id': networkId, + 'network': networkCode, + 'margin': None, + 'deposit': deposit, + 'withdraw': withdraw, + 'active': None, + 'fee': self.parse_number(withdrawalFee), + 'precision': self.parse_number(precision), + 'limits': { + 'deposit': { + 'min': minDepositAmount, + 'max': None, + }, + 'withdraw': { + 'min': minWithdrawalAmount, + 'max': None, + }, + }, + 'info': chain, + } + chainLength = len(chains) + type: Str = None + if self.safe_bool(currency, 'is_fiat'): + type = 'fiat' + elif chainLength == 0: + if self.is_leveraged_currency(id): + type = 'leveraged' + else: + type = 'other' + else: + type = 'crypto' + result[code] = self.safe_currency_structure({ + 'id': id, + 'code': code, + 'info': currency, + 'name': name, + 'type': type, + 'active': None, + 'deposit': None, + 'withdraw': None, + 'fee': None, + 'precision': self.parse_number(currencyMaxPrecision), + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + }, + 'networks': networks, + }) + return result + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for bigone + + https://open.big.one/docs/spot_asset_pair.html + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + promises = [self.publicGetAssetPairs(params), self.contractPublicGetSymbols(params)] + promisesResult = promises + response = promisesResult[0] + contractResponse = promisesResult[1] + # + # { + # "code":0, + # "data":[ + # { + # "id":"01e48809-b42f-4a38-96b1-c4c547365db1", + # "name":"PCX-BTC", + # "quote_scale":7, + # "quote_asset":{ + # "id":"0df9c3c3-255a-46d7-ab82-dedae169fba9", + # "symbol":"BTC", + # "name":"Bitcoin", + # }, + # "base_asset":{ + # "id":"405484f7-4b03-4378-a9c1-2bd718ecab51", + # "symbol":"PCX", + # "name":"ChainX", + # }, + # "base_scale":3, + # "min_quote_value":"0.0001", + # "max_quote_value":"35" + # }, + # ] + # } + # + # + # [ + # { + # "baseCurrency": "BTC", + # "multiplier": 1, + # "enable": True, + # "priceStep": 0.5, + # "maxRiskLimit": 1000, + # "pricePrecision": 1, + # "maintenanceMargin": 0.00500, + # "symbol": "BTCUSD", + # "valuePrecision": 4, + # "minRiskLimit": 100, + # "riskLimit": 100, + # "isInverse": True, + # "riskStep": 1, + # "settleCurrency": "BTC", + # "baseName": "Bitcoin", + # "feePrecision": 8, + # "priceMin": 0.5, + # "priceMax": 1E+6, + # "initialMargin": 0.01000, + # "quoteCurrency": "USD" + # }, + # ... + # ] + # + markets = self.safe_list(response, 'data', []) + result = [] + for i in range(0, len(markets)): + market = markets[i] + baseAsset = self.safe_dict(market, 'base_asset', {}) + quoteAsset = self.safe_dict(market, 'quote_asset', {}) + baseId = self.safe_string(baseAsset, 'symbol') + quoteId = self.safe_string(quoteAsset, 'symbol') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + result.append(self.safe_market_structure({ + 'id': self.safe_string(market, 'name'), + 'uuid': self.safe_string(market, 'id'), + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': True, + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(self.parse_precision(self.safe_string(market, 'base_scale'))), + 'price': self.parse_number(self.parse_precision(self.safe_string(market, 'quote_scale'))), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': None, + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': self.safe_number(market, 'min_quote_value'), + 'max': self.safe_number(market, 'max_quote_value'), + }, + }, + 'created': None, + 'info': market, + })) + for i in range(0, len(contractResponse)): + market = contractResponse[i] + baseId = self.safe_string(market, 'baseCurrency') + quoteId = self.safe_string(market, 'quoteCurrency') + settleId = self.safe_string(market, 'settleCurrency') + marketId = self.safe_string(market, 'symbol') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + settle = self.safe_currency_code(settleId) + inverse = self.safe_bool(market, 'isInverse') + result.append(self.safe_market_structure({ + 'id': marketId, + 'symbol': base + '/' + quote + ':' + settle, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': 'swap', + 'spot': False, + 'margin': False, + 'swap': True, + 'future': False, + 'option': False, + 'active': self.safe_bool(market, 'enable'), + 'contract': True, + 'linear': not inverse, + 'inverse': inverse, + 'contractSize': self.safe_number(market, 'multiplier'), + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(self.parse_precision(self.safe_string(market, 'valuePrecision'))), + 'price': self.parse_number(self.parse_precision(self.safe_string(market, 'pricePrecision'))), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': None, + 'max': None, + }, + 'price': { + 'min': self.safe_number(market, 'priceMin'), + 'max': self.safe_number(market, 'priceMax'), + }, + 'cost': { + 'min': self.safe_number(market, 'initialMargin'), + 'max': None, + }, + }, + 'info': market, + })) + return result + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # spot + # + # { + # "asset_pair_name": "ETH-BTC", + # "bid": { + # "price": "0.021593", + # "order_count": 1, + # "quantity": "0.20936" + # }, + # "ask": { + # "price": "0.021613", + # "order_count": 1, + # "quantity": "2.87064" + # }, + # "open": "0.021795", + # "high": "0.021795", + # "low": "0.021471", + # "close": "0.021613", + # "volume": "117078.90431", + # "daily_change": "-0.000182" + # } + # + # contract + # + # { + # "usdtPrice": 1.00031998, + # "symbol": "BTCUSD", + # "btcPrice": 34700.4, + # "ethPrice": 1787.83, + # "nextFundingRate": 0.00010, + # "fundingRate": 0.00010, + # "latestPrice": 34708.5, + # "last24hPriceChange": 0.0321, + # "indexPrice": 34700.4, + # "volume24h": 261319063, + # "turnover24h": 8204.129380685496, + # "nextFundingTime": 1698285600000, + # "markPrice": 34702.4646738, + # "last24hMaxPrice": 35127.5, + # "volume24hInUsd": 0.0, + # "openValue": 32.88054722085945, + # "last24hMinPrice": 33552.0, + # "openInterest": 1141372.0 + # } + # + marketType = 'spot' if ('asset_pair_name' in ticker) else 'swap' + marketId = self.safe_string_2(ticker, 'asset_pair_name', 'symbol') + symbol = self.safe_symbol(marketId, market, '-', marketType) + close = self.safe_string_2(ticker, 'close', 'latestPrice') + bid = self.safe_dict(ticker, 'bid', {}) + ask = self.safe_dict(ticker, 'ask', {}) + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': None, + 'datetime': None, + 'high': self.safe_string_2(ticker, 'high', 'last24hMaxPrice'), + 'low': self.safe_string_2(ticker, 'low', 'last24hMinPrice'), + 'bid': self.safe_string(bid, 'price'), + 'bidVolume': self.safe_string(bid, 'quantity'), + 'ask': self.safe_string(ask, 'price'), + 'askVolume': self.safe_string(ask, 'quantity'), + 'vwap': None, + 'open': self.safe_string(ticker, 'open'), + 'close': close, + 'last': close, + 'previousClose': None, + 'change': self.safe_string_2(ticker, 'daily_change', 'last24hPriceChange'), + 'percentage': None, + 'average': None, + 'baseVolume': self.safe_string_2(ticker, 'volume', 'volume24h'), + 'quoteVolume': self.safe_string(ticker, 'volume24hInUsd'), + 'markPrice': self.safe_string(ticker, 'markPrice'), + 'indexPrice': self.safe_string(ticker, 'indexPrice'), + 'info': ticker, + }, market) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://open.big.one/docs/spot_tickers.html + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + type = None + type, params = self.handle_market_type_and_params('fetchTicker', market, params) + if type == 'spot': + request: dict = { + 'asset_pair_name': market['id'], + } + response = self.publicGetAssetPairsAssetPairNameTicker(self.extend(request, params)) + # + # { + # "code":0, + # "data":{ + # "asset_pair_name":"ETH-BTC", + # "bid":{"price":"0.021593","order_count":1,"quantity":"0.20936"}, + # "ask":{"price":"0.021613","order_count":1,"quantity":"2.87064"}, + # "open":"0.021795", + # "high":"0.021795", + # "low":"0.021471", + # "close":"0.021613", + # "volume":"117078.90431", + # "daily_change":"-0.000182" + # } + # } + # + ticker = self.safe_dict(response, 'data', {}) + return self.parse_ticker(ticker, market) + else: + tickers = self.fetch_tickers([symbol], params) + return self.safe_value(tickers, symbol) + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://open.big.one/docs/spot_tickers.html + + :param str[] [symbols]: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + market = None + symbol = self.safe_string(symbols, 0) + if symbol is not None: + market = self.market(symbol) + type = None + type, params = self.handle_market_type_and_params('fetchTickers', market, params) + isSpot = type == 'spot' + request: dict = {} + symbols = self.market_symbols(symbols) + data = None + if isSpot: + if symbols is not None: + ids = self.market_ids(symbols) + request['pair_names'] = ','.join(ids) + response = self.publicGetAssetPairsTickers(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "asset_pair_name": "PCX-BTC", + # "bid": { + # "price": "0.000234", + # "order_count": 1, + # "quantity": "0.518" + # }, + # "ask": { + # "price": "0.0002348", + # "order_count": 1, + # "quantity": "2.348" + # }, + # "open": "0.0002343", + # "high": "0.0002348", + # "low": "0.0002162", + # "close": "0.0002348", + # "volume": "12887.016", + # "daily_change": "0.0000005" + # }, + # ... + # ] + # } + # + data = self.safe_list(response, 'data', []) + else: + data = self.contractPublicGetInstruments(params) + # + # [ + # { + # "usdtPrice": 1.00031998, + # "symbol": "BTCUSD", + # "btcPrice": 34700.4, + # "ethPrice": 1787.83, + # "nextFundingRate": 0.00010, + # "fundingRate": 0.00010, + # "latestPrice": 34708.5, + # "last24hPriceChange": 0.0321, + # "indexPrice": 34700.4, + # "volume24h": 261319063, + # "turnover24h": 8204.129380685496, + # "nextFundingTime": 1698285600000, + # "markPrice": 34702.4646738, + # "last24hMaxPrice": 35127.5, + # "volume24hInUsd": 0.0, + # "openValue": 32.88054722085945, + # "last24hMinPrice": 33552.0, + # "openInterest": 1141372.0 + # } + # ... + # ] + # + tickers = self.parse_tickers(data, symbols) + return self.filter_by_array_tickers(tickers, 'symbol', symbols) + + def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://open.big.one/docs/spot_ping.html + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = self.publicGetPing(params) + # + # { + # "data": { + # "timestamp": 1527665262168391000 + # } + # } + # + data = self.safe_dict(response, 'data', {}) + timestamp = self.safe_integer(data, 'Timestamp') + return self.parse_to_int(timestamp / 1000000) + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://open.big.one/docs/contract_misc.html#get-orderbook-snapshot + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + response = None + if market['contract']: + request: dict = { + 'symbol': market['id'], + } + response = self.contractPublicGetDepthSymbolSnapshot(self.extend(request, params)) + # + # { + # bids: { + # '20000': '20', + # ... + # '34552': '64851', + # '34526.5': '59594', + # ... + # '34551.5': '29711' + # }, + # asks: { + # '34557': '34395', + # ... + # '40000': '20', + # '34611.5': '56024', + # ... + # '34578.5': '66367' + # }, + # to: '59737174', + # lastPrice: '34554.5', + # bestPrices: { + # ask: '34557.0', + # bid: '34552.0' + # }, + # from: '0' + # } + # + return self.parse_contract_order_book(response, market['symbol'], limit) + else: + request: dict = { + 'asset_pair_name': market['id'], + } + if limit is not None: + request['limit'] = limit # default 50, max 200 + response = self.publicGetAssetPairsAssetPairNameDepth(self.extend(request, params)) + # + # { + # "code":0, + # "data": { + # "asset_pair_name": "EOS-BTC", + # "bids": [ + # {"price": "42", "order_count": 4, "quantity": "23.33363711"} + # ], + # "asks": [ + # {"price": "45", "order_count": 2, "quantity": "4193.3283464"} + # ] + # } + # } + # + orderbook = self.safe_dict(response, 'data', {}) + return self.parse_order_book(orderbook, market['symbol'], None, 'bids', 'asks', 'price', 'quantity') + + def parse_contract_bids_asks(self, bidsAsks): + bidsAsksKeys = list(bidsAsks.keys()) + result = [] + for i in range(0, len(bidsAsksKeys)): + price = bidsAsksKeys[i] + amount = bidsAsks[price] + result.append([self.parse_number(price), self.parse_number(amount)]) + return result + + def parse_contract_order_book(self, orderbook: object, symbol: str, limit: Int = None) -> OrderBook: + responseBids = self.safe_value(orderbook, 'bids') + responseAsks = self.safe_value(orderbook, 'asks') + bids = self.parse_contract_bids_asks(responseBids) + asks = self.parse_contract_bids_asks(responseAsks) + return { + 'symbol': symbol, + 'bids': self.filter_by_limit(self.sort_by(bids, 0, True), limit), + 'asks': self.filter_by_limit(self.sort_by(asks, 0), limit), + 'timestamp': None, + 'datetime': None, + 'nonce': None, + } + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(public) + # + # { + # "id": 38199941, + # "price": "3378.67", + # "amount": "0.019812", + # "taker_side": "ASK", + # "created_at": "2019-01-29T06:05:56Z" + # } + # + # fetchMyTrades(private) + # + # { + # "id": 10854280, + # "asset_pair_name": "XIN-USDT", + # "price": "70", + # "amount": "1", + # "taker_side": "ASK", + # "maker_order_id": 58284908, + # "taker_order_id": 58284909, + # "maker_fee": "0.0008", + # "taker_fee": "0.07", + # "side": "SELF_TRADING", + # "inserted_at": "2019-04-16T12:00:01Z" + # }, + # + # { + # "id": 10854263, + # "asset_pair_name": "XIN-USDT", + # "price": "75.7", + # "amount": "12.743149", + # "taker_side": "BID", + # "maker_order_id": null, + # "taker_order_id": 58284888, + # "maker_fee": null, + # "taker_fee": "0.0025486298", + # "side": "BID", + # "inserted_at": "2019-04-15T06:20:57Z" + # } + # + timestamp = self.parse8601(self.safe_string_2(trade, 'created_at', 'inserted_at')) + priceString = self.safe_string(trade, 'price') + amountString = self.safe_string(trade, 'amount') + marketId = self.safe_string(trade, 'asset_pair_name') + market = self.safe_market(marketId, market, '-') + side = self.safe_string(trade, 'side') + takerSide = self.safe_string(trade, 'taker_side') + takerOrMaker: Str = None + if (takerSide is not None) and (side is not None) and (side != 'SELF_TRADING'): + takerOrMaker = 'taker' if (takerSide == side) else 'maker' + if side is None: + # taker side is not related to buy/sell side + # the following code is probably a mistake + side = 'sell' if (takerSide == 'ASK') else 'buy' + else: + if side == 'BID': + side = 'buy' + elif side == 'ASK': + side = 'sell' + makerOrderId = self.safe_string(trade, 'maker_order_id') + takerOrderId = self.safe_string(trade, 'taker_order_id') + orderId: Str = None + if makerOrderId is not None: + orderId = makerOrderId + elif takerOrderId is not None: + orderId = takerOrderId + id = self.safe_string(trade, 'id') + result: dict = { + 'id': id, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'order': orderId, + 'type': 'limit', + 'side': side, + 'takerOrMaker': takerOrMaker, + 'price': priceString, + 'amount': amountString, + 'cost': None, + 'info': trade, + } + makerCurrencyCode: str + takerCurrencyCode: str + if takerOrMaker is not None: + if side == 'buy': + if takerOrMaker == 'maker': + makerCurrencyCode = market['base'] + takerCurrencyCode = market['quote'] + else: + makerCurrencyCode = market['quote'] + takerCurrencyCode = market['base'] + else: + if takerOrMaker == 'maker': + makerCurrencyCode = market['quote'] + takerCurrencyCode = market['base'] + else: + makerCurrencyCode = market['base'] + takerCurrencyCode = market['quote'] + elif side == 'SELF_TRADING': + if takerSide == 'BID': + makerCurrencyCode = market['quote'] + takerCurrencyCode = market['base'] + elif takerSide == 'ASK': + makerCurrencyCode = market['base'] + takerCurrencyCode = market['quote'] + makerFeeCost = self.safe_string(trade, 'maker_fee') + takerFeeCost = self.safe_string(trade, 'taker_fee') + if makerFeeCost is not None: + if takerFeeCost is not None: + result['fees'] = [ + {'cost': makerFeeCost, 'currency': makerCurrencyCode}, + {'cost': takerFeeCost, 'currency': takerCurrencyCode}, + ] + else: + result['fee'] = {'cost': makerFeeCost, 'currency': makerCurrencyCode} + elif takerFeeCost is not None: + result['fee'] = {'cost': takerFeeCost, 'currency': takerCurrencyCode} + else: + result['fee'] = None + return self.safe_trade(result, market) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://open.big.one/docs/spot_asset_pair_trade.html + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + if market['contract']: + raise NotSupported(self.id + ' fetchTrades() can only fetch trades for spot markets') + request: dict = { + 'asset_pair_name': market['id'], + } + response = self.publicGetAssetPairsAssetPairNameTrades(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "id": 38199941, + # "price": "3378.67", + # "amount": "0.019812", + # "taker_side": "ASK", + # "created_at": "2019-01-29T06:05:56Z" + # }, + # { + # "id": 38199934, + # "price": "3376.14", + # "amount": "0.019384", + # "taker_side": "ASK", + # "created_at": "2019-01-29T06:05:40Z" + # } + # ] + # } + # + trades = self.safe_list(response, 'data', []) + return self.parse_trades(trades, market, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # { + # "close": "0.021562", + # "high": "0.021563", + # "low": "0.02156", + # "open": "0.021563", + # "time": "2019-11-21T07:54:00Z", + # "volume": "59.84376" + # } + # + return [ + self.parse8601(self.safe_string(ohlcv, 'time')), + self.safe_number(ohlcv, 'open'), + self.safe_number(ohlcv, 'high'), + self.safe_number(ohlcv, 'low'), + self.safe_number(ohlcv, 'close'), + self.safe_number(ohlcv, 'volume'), + ] + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://open.big.one/docs/spot_asset_pair_candle.html + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the earliest candle to fetch + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + market = self.market(symbol) + if market['contract']: + raise NotSupported(self.id + ' fetchOHLCV() can only fetch ohlcvs for spot markets') + until = self.safe_integer(params, 'until') + untilIsDefined = (until is not None) + sinceIsDefined = (since is not None) + if limit is None: + limit = 500 if (sinceIsDefined and untilIsDefined) else 100 # default 100, max 500, if since and limit defined then fetch all the candles between them unless it exceeds the max of 500 + request: dict = { + 'asset_pair_name': market['id'], + 'period': self.safe_string(self.timeframes, timeframe, timeframe), + 'limit': limit, + } + if sinceIsDefined: + # start = self.parse_to_int(since / 1000) + duration = self.parse_timeframe(timeframe) + endByLimit = self.sum(since, limit * duration * 1000) + if untilIsDefined: + request['time'] = self.iso8601(min(endByLimit, until + 1)) + else: + request['time'] = self.iso8601(endByLimit) + elif untilIsDefined: + request['time'] = self.iso8601(until + 1) + params = self.omit(params, 'until') + response = self.publicGetAssetPairsAssetPairNameCandles(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "close": "0.021656", + # "high": "0.021658", + # "low": "0.021652", + # "open": "0.021652", + # "time": "2019-11-21T09:30:00Z", + # "volume": "53.08664" + # }, + # { + # "close": "0.021652", + # "high": "0.021656", + # "low": "0.021652", + # "open": "0.021656", + # "time": "2019-11-21T09:29:00Z", + # "volume": "88.39861" + # }, + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_ohlcvs(data, market, timeframe, since, limit) + + def parse_balance(self, response) -> Balances: + result: dict = { + 'info': response, + 'timestamp': None, + 'datetime': None, + } + balances = self.safe_list(response, 'data', []) + for i in range(0, len(balances)): + balance = balances[i] + symbol = self.safe_string(balance, 'asset_symbol') + code = self.safe_currency_code(symbol) + account = self.account() + account['total'] = self.safe_string(balance, 'balance') + account['used'] = self.safe_string(balance, 'locked_balance') + result[code] = account + return self.safe_balance(result) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://open.big.one/docs/fund_accounts.html + https://open.big.one/docs/spot_accounts.html + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + type = self.safe_string(params, 'type', '') + params = self.omit(params, 'type') + response = None + if type == 'funding' or type == 'fund': + response = self.privateGetFundAccounts(params) + else: + response = self.privateGetAccounts(params) + # + # { + # "code":0, + # "data":[ + # {"asset_symbol":"NKC","balance":"0","locked_balance":"0"}, + # {"asset_symbol":"UBTC","balance":"0","locked_balance":"0"}, + # {"asset_symbol":"READ","balance":"0","locked_balance":"0"}, + # ], + # } + # + return self.parse_balance(response) + + def parse_type(self, type: str): + types: dict = { + 'STOP_LIMIT': 'limit', + 'STOP_MARKET': 'market', + 'LIMIT': 'limit', + 'MARKET': 'market', + } + return self.safe_string(types, type, type) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # { + # "id": "42154072251", + # "asset_pair_name": "SOL-USDT", + # "price": "20", + # "amount": "0.5", + # "filled_amount": "0", + # "avg_deal_price": "0", + # "side": "ASK", + # "state": "PENDING", + # "created_at": "2023-09-13T03:42:00Z", + # "updated_at": "2023-09-13T03:42:00Z", + # "type": "LIMIT", + # "stop_price": "0", + # "immediate_or_cancel": False, + # "post_only": False, + # "client_order_id": '' + # } + # + id = self.safe_string(order, 'id') + marketId = self.safe_string(order, 'asset_pair_name') + symbol = self.safe_symbol(marketId, market, '-') + timestamp = self.parse8601(self.safe_string(order, 'created_at')) + side = self.safe_string(order, 'side') + if side == 'BID': + side = 'buy' + else: + side = 'sell' + triggerPrice = self.safe_string(order, 'stop_price') + if Precise.string_eq(triggerPrice, '0'): + triggerPrice = None + immediateOrCancel = self.safe_bool(order, 'immediate_or_cancel') + timeInForce = None + if immediateOrCancel: + timeInForce = 'IOC' + type = self.parse_type(self.safe_string(order, 'type')) + price = self.safe_string(order, 'price') + amount = None + filled = None + cost = None + if type == 'market' and side == 'buy': + cost = self.safe_string(order, 'filled_amount') + else: + amount = self.safe_string(order, 'amount') + filled = self.safe_string(order, 'filled_amount') + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': self.safe_string(order, 'client_order_id'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': self.parse8601(self.safe_string(order, 'updated_at')), + 'symbol': symbol, + 'type': type, + 'timeInForce': timeInForce, + 'postOnly': self.safe_bool(order, 'post_only'), + 'side': side, + 'price': price, + 'triggerPrice': triggerPrice, + 'amount': amount, + 'cost': cost, + 'average': self.safe_string(order, 'avg_deal_price'), + 'filled': filled, + 'remaining': None, + 'status': self.parse_order_status(self.safe_string(order, 'state')), + 'fee': None, + 'trades': None, + }, market) + + def create_market_buy_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market buy order by providing the symbol and cost + + https://open.big.one/docs/spot_orders.html#create-order + + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' createMarketBuyOrderWithCost() supports spot orders only') + params['createMarketBuyOrderRequiresPrice'] = False + return self.create_order(symbol, 'market', 'buy', cost, None, params) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://open.big.one/docs/spot_orders.html#create-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: the price at which a trigger order is triggered at + :param bool [params.postOnly]: if True, the order will only be posted to the order book and not executed immediately + :param str [params.timeInForce]: "GTC", "IOC", or "PO" + :param float [params.cost]: *spot market buy only* the quote quantity that can be used alternative for the amount + + EXCHANGE SPECIFIC PARAMETERS + :param str [params.operator]: *stop order only* GTE or LTE(default) + :param str [params.client_order_id]: must match ^[a-zA-Z0-9-_]{1,36}$ self regex. client_order_id is unique in 24 hours, If created 24 hours later and the order closed, it will be released and can be reused + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + isBuy = (side == 'buy') + requestSide = 'BID' if isBuy else 'ASK' + uppercaseType = type.upper() + isLimit = uppercaseType == 'LIMIT' + exchangeSpecificParam = self.safe_bool(params, 'post_only', False) + postOnly = None + postOnly, params = self.handle_post_only((uppercaseType == 'MARKET'), exchangeSpecificParam, params) + triggerPrice = self.safe_string_n(params, ['triggerPrice', 'stopPrice', 'stop_price']) + request: dict = { + 'asset_pair_name': market['id'], # asset pair name BTC-USDT, required + 'side': requestSide, # order side one of "ASK"/"BID", required + 'amount': self.amount_to_precision(symbol, amount), # order amount, string, required + # "price": self.price_to_precision(symbol, price), # order price, string, required + # "operator": "GTE", # stop orders only, GTE greater than and equal, LTE less than and equal + # "immediate_or_cancel": False, # limit orders only, must be False when post_only is True + # "post_only": False, # limit orders only, must be False when immediate_or_cancel is True + } + if isLimit or (uppercaseType == 'STOP_LIMIT'): + request['price'] = self.price_to_precision(symbol, price) + if isLimit: + timeInForce = self.safe_string(params, 'timeInForce') + if timeInForce == 'IOC': + request['immediate_or_cancel'] = True + if postOnly: + request['post_only'] = True + request['amount'] = self.amount_to_precision(symbol, amount) + else: + if isBuy: + createMarketBuyOrderRequiresPrice = True + createMarketBuyOrderRequiresPrice, params = self.handle_option_and_params(params, 'createOrder', 'createMarketBuyOrderRequiresPrice', True) + cost = self.safe_number(params, 'cost') + params = self.omit(params, 'cost') + if createMarketBuyOrderRequiresPrice: + if (price is None) and (cost is None): + raise InvalidOrder(self.id + ' createOrder() requires the price argument for market buy orders to calculate the total cost to spend(amount * price), alternatively set the createMarketBuyOrderRequiresPrice option or param to False and pass the cost to spend in the amount argument') + else: + amountString = self.number_to_string(amount) + priceString = self.number_to_string(price) + quoteAmount = self.parse_to_numeric(Precise.string_mul(amountString, priceString)) + costRequest = cost if (cost is not None) else quoteAmount + request['amount'] = self.cost_to_precision(symbol, costRequest) + else: + request['amount'] = self.cost_to_precision(symbol, amount) + else: + request['amount'] = self.amount_to_precision(symbol, amount) + if triggerPrice is not None: + request['stop_price'] = self.price_to_precision(symbol, triggerPrice) + request['operator'] = 'GTE' if isBuy else 'LTE' + if isLimit: + uppercaseType = 'STOP_LIMIT' + elif uppercaseType == 'MARKET': + uppercaseType = 'STOP_MARKET' + request['type'] = uppercaseType + clientOrderId = self.safe_string(params, 'clientOrderId') + if clientOrderId is not None: + request['client_order_id'] = clientOrderId + params = self.omit(params, ['stop_price', 'stopPrice', 'triggerPrice', 'timeInForce', 'clientOrderId']) + response = self.privatePostOrders(self.extend(request, params)) + # + # { + # "id": 10, + # "asset_pair_name": "EOS-BTC", + # "price": "10.00", + # "amount": "10.00", + # "filled_amount": "9.0", + # "avg_deal_price": "12.0", + # "side": "ASK", + # "state": "FILLED", + # "created_at":"2019-01-29T06:05:56Z", + # "updated_at":"2019-01-29T06:05:56Z" + # } + # + order = self.safe_dict(response, 'data') + return self.parse_order(order, market) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://open.big.one/docs/spot_orders.html#cancel-order + + :param str id: order id + :param str symbol: Not used by bigone cancelOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + request: dict = {'id': id} + response = self.privatePostOrdersIdCancel(self.extend(request, params)) + # { + # "id": 10, + # "asset_pair_name": "EOS-BTC", + # "price": "10.00", + # "amount": "10.00", + # "filled_amount": "9.0", + # "avg_deal_price": "12.0", + # "side": "ASK", + # "state": "CANCELLED", + # "created_at":"2019-01-29T06:05:56Z", + # "updated_at":"2019-01-29T06:05:56Z" + # } + order = self.safe_dict(response, 'data') + return self.parse_order(order) + + def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders + + https://open.big.one/docs/spot_orders.html#cancel-all-orders + + :param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'asset_pair_name': market['id'], + } + response = self.privatePostOrdersCancel(self.extend(request, params)) + # + # { + # "code":0, + # "data": { + # "cancelled":[ + # 58272370, + # 58272377 + # ], + # "failed": [] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + cancelled = self.safe_list(data, 'cancelled', []) + failed = self.safe_list(data, 'failed', []) + result = [] + for i in range(0, len(cancelled)): + orderId = cancelled[i] + result.append(self.safe_order({ + 'info': orderId, + 'id': orderId, + 'status': 'canceled', + })) + for i in range(0, len(failed)): + orderId = failed[i] + result.append(self.safe_order({ + 'info': orderId, + 'id': orderId, + 'status': 'failed', + })) + return result + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://open.big.one/docs/spot_orders.html#get-one-order + + :param str id: the order id + :param str symbol: not used by bigone fetchOrder + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + request: dict = {'id': id} + response = self.privateGetOrdersId(self.extend(request, params)) + order = self.safe_dict(response, 'data', {}) + return self.parse_order(order) + + def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://open.big.one/docs/spot_orders.html#get-user-orders-in-one-asset-pair + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrders() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'asset_pair_name': market['id'], + # 'page_token': 'dxzef', # request page after self page token + # 'side': 'ASK', # 'ASK' or 'BID', optional + # 'state': 'FILLED', # 'CANCELLED', 'FILLED', 'PENDING' + # 'limit' 20, # default 20, max 200 + } + if limit is not None: + request['limit'] = limit # default 20, max 200 + response = self.privateGetOrders(self.extend(request, params)) + # + # { + # "code":0, + # "data": [ + # { + # "id": 10, + # "asset_pair_name": "ETH-BTC", + # "price": "10.00", + # "amount": "10.00", + # "filled_amount": "9.0", + # "avg_deal_price": "12.0", + # "side": "ASK", + # "state": "FILLED", + # "created_at":"2019-01-29T06:05:56Z", + # "updated_at":"2019-01-29T06:05:56Z", + # }, + # ], + # "page_token":"dxzef", + # } + # + orders = self.safe_list(response, 'data', []) + return self.parse_orders(orders, market, since, limit) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://open.big.one/docs/spot_trade.html#trades-of-user + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMyTrades() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'asset_pair_name': market['id'], + # 'page_token': 'dxzef', # request page after self page token + } + if limit is not None: + request['limit'] = limit # default 20, max 200 + response = self.privateGetTrades(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "id": 10854280, + # "asset_pair_name": "XIN-USDT", + # "price": "70", + # "amount": "1", + # "taker_side": "ASK", + # "maker_order_id": 58284908, + # "taker_order_id": 58284909, + # "maker_fee": "0.0008", + # "taker_fee": "0.07", + # "side": "SELF_TRADING", + # "inserted_at": "2019-04-16T12:00:01Z" + # }, + # { + # "id": 10854263, + # "asset_pair_name": "XIN-USDT", + # "price": "75.7", + # "amount": "12.743149", + # "taker_side": "BID", + # "maker_order_id": null, + # "taker_order_id": 58284888, + # "maker_fee": null, + # "taker_fee": "0.0025486298", + # "side": "BID", + # "inserted_at": "2019-04-15T06:20:57Z" + # } + # ], + # "page_token":"dxfv" + # } + # + trades = self.safe_list(response, 'data', []) + return self.parse_trades(trades, market, since, limit) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'PENDING': 'open', + 'FILLED': 'closed', + 'CANCELLED': 'canceled', + } + return self.safe_string(statuses, status) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://open.big.one/docs/spot_orders.html#get-user-orders-in-one-asset-pair + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + request: dict = { + 'state': 'PENDING', + } + return self.fetch_orders(symbol, since, limit, self.extend(request, params)) + + def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://open.big.one/docs/spot_orders.html#get-user-orders-in-one-asset-pair + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + request: dict = { + 'state': 'FILLED', + } + return self.fetch_orders(symbol, since, limit, self.extend(request, params)) + + def nonce(self): + exchangeTimeCorrection = self.safe_integer(self.options, 'exchangeMillisecondsCorrection', 0) * 1000000 + return self.sum(self.microseconds() * 1000, exchangeTimeCorrection) + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + query = self.omit(params, self.extract_params(path)) + baseUrl = self.implode_hostname(self.urls['api'][api]) + url = baseUrl + '/' + self.implode_params(path, params) + headers = {} + if api == 'public' or api == 'webExchange' or api == 'contractPublic': + if query: + url += '?' + self.urlencode(query) + else: + self.check_required_credentials() + nonce = str(self.nonce()) + request: dict = { + 'type': 'OpenAPIV2', + 'sub': self.apiKey, + 'nonce': nonce, + # 'recv_window': '30', # default 30 + } + token = self.jwt(request, self.encode(self.secret), 'sha256') + headers['Authorization'] = 'Bearer ' + token + if method == 'GET': + if query: + url += '?' + self.urlencode(query) + elif method == 'POST': + headers['Content-Type'] = 'application/json' + body = self.json(query) + headers['User-Agent'] = 'ccxt/' + self.id + '-' + self.version + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://open.big.one/docs/spot_deposit.html#get-deposite-address-of-one-asset-of-user + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'asset_symbol': currency['id'], + } + networkCode, paramsOmitted = self.handle_network_code_and_params(params) + response = self.privateGetAssetsAssetSymbolAddress(self.extend(request, paramsOmitted)) + # + # the actual response format is not the same documented one + # the data key contains an array in the actual response + # + # { + # "code":0, + # "message":"", + # "data":[ + # { + # "id":5521878, + # "chain":"Bitcoin", + # "value":"1GbmyKoikhpiQVZ1C9sbF17mTyvBjeobVe", + # "memo":"" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + dataLength = len(data) + if dataLength < 1: + raise ExchangeError(self.id + ' fetchDepositAddress() returned empty address response') + chainsIndexedById = self.index_by(data, 'chain') + selectedNetworkId = self.select_network_id_from_raw_networks(code, networkCode, chainsIndexedById) + addressObject = self.safe_dict(chainsIndexedById, selectedNetworkId, {}) + address = self.safe_string(addressObject, 'value') + tag = self.safe_string(addressObject, 'memo') + self.check_address(address) + return { + 'info': response, + 'currency': code, + 'network': self.network_id_to_code(selectedNetworkId), + 'address': address, + 'tag': tag, + } + + def parse_transaction_status(self, status: Str): + statuses: dict = { + # what are other statuses here? + 'WITHHOLD': 'ok', # deposits + 'UNCONFIRMED': 'pending', + 'CONFIRMED': 'ok', # withdrawals + 'COMPLETED': 'ok', + 'PENDING': 'pending', + } + return self.safe_string(statuses, status, status) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fetchDeposits + # + # { + # "amount": "25.0", + # "asset_symbol": "BTS" + # "confirms": 100, + # "id": 5, + # "inserted_at": "2018-02-16T11:39:58.000Z", + # "is_internal": False, + # "kind": "default", + # "memo": "", + # "state": "WITHHOLD", + # "txid": "72e03037d144dae3d32b68b5045462b1049a0755", + # "updated_at": "2018-11-09T10:20:09.000Z", + # } + # + # fetchWithdrawals + # + # { + # "amount": "5", + # "asset_symbol": "ETH", + # "completed_at": "2018-03-15T16:13:45.610463Z", + # "customer_id": "10", + # "id": 10, + # "inserted_at": "2018-03-15T16:13:45.610463Z", + # "is_internal": True, + # "note": "2018-03-15T16:13:45.610463Z", + # "state": "CONFIRMED", + # "target_address": "0x4643bb6b393ac20a6175c713175734a72517c63d6f7" + # "txid": "0x4643bb6b393ac20a6175c713175734a72517c63d6f73a3ca90a15356f2e967da0", + # } + # + # withdraw + # + # { + # "id":1077391, + # "customer_id":1082679, + # "amount":"21.9000000000000000", + # "txid":"", + # "is_internal":false, + # "kind":"on_chain", + # "state":"PENDING", + # "inserted_at":"2020-06-03T00:50:57+00:00", + # "updated_at":"2020-06-03T00:50:57+00:00", + # "memo":"", + # "target_address":"rDYtYT3dBeuw376rvHqoZBKW3UmvguoBAf", + # "fee":"0.1000000000000000", + # "asset_symbol":"XRP" + # } + # + currencyId = self.safe_string(transaction, 'asset_symbol') + code = self.safe_currency_code(currencyId) + id = self.safe_string(transaction, 'id') + amount = self.safe_number(transaction, 'amount') + status = self.parse_transaction_status(self.safe_string(transaction, 'state')) + timestamp = self.parse8601(self.safe_string(transaction, 'inserted_at')) + updated = self.parse8601(self.safe_string_2(transaction, 'updated_at', 'completed_at')) + txid = self.safe_string(transaction, 'txid') + address = self.safe_string(transaction, 'target_address') + tag = self.safe_string(transaction, 'memo') + type = 'withdrawal' if ('customer_id' in transaction) else 'deposit' + internal = self.safe_bool(transaction, 'is_internal') + return { + 'info': transaction, + 'id': id, + 'txid': txid, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': None, + 'addressFrom': None, + 'address': None, + 'addressTo': address, + 'tagFrom': None, + 'tag': tag, + 'tagTo': None, + 'type': type, + 'amount': amount, + 'currency': code, + 'status': status, + 'updated': updated, + 'fee': None, + 'comment': None, + 'internal': internal, + } + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://open.big.one/docs/spot_deposit.html#deposit-of-user + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + request: dict = { + # 'page_token': 'dxzef', # request page after self page token + # 'limit': 50, # optional, default 50 + # 'kind': 'string', # optional - air_drop, big_holder_dividend, default, eosc_to_eos, internal, equally_airdrop, referral_mining, one_holder_dividend, single_customer, snapshotted_airdrop, trade_mining + # 'asset_symbol': 'BTC', # optional + } + currency = None + if code is not None: + currency = self.currency(code) + request['asset_symbol'] = currency['id'] + if limit is not None: + request['limit'] = limit # default 50 + response = self.privateGetDeposits(self.extend(request, params)) + # + # { + # "code": 0, + # "page_token": "NQ==", + # "data": [ + # { + # "id": 5, + # "amount": "25.0", + # "confirms": 100, + # "txid": "72e03037d144dae3d32b68b5045462b1049a0755", + # "is_internal": False, + # "inserted_at": "2018-02-16T11:39:58.000Z", + # "updated_at": "2018-11-09T10:20:09.000Z", + # "kind": "default", + # "memo": "", + # "state": "WITHHOLD", + # "asset_symbol": "BTS" + # } + # ] + # } + # + deposits = self.safe_list(response, 'data', []) + return self.parse_transactions(deposits, currency, since, limit) + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://open.big.one/docs/spot_withdrawal.html#get-withdrawals-of-user + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + request: dict = { + # 'page_token': 'dxzef', # request page after self page token + # 'limit': 50, # optional, default 50 + # 'kind': 'string', # optional - air_drop, big_holder_dividend, default, eosc_to_eos, internal, equally_airdrop, referral_mining, one_holder_dividend, single_customer, snapshotted_airdrop, trade_mining + # 'asset_symbol': 'BTC', # optional + } + currency = None + if code is not None: + currency = self.currency(code) + request['asset_symbol'] = currency['id'] + if limit is not None: + request['limit'] = limit # default 50 + response = self.privateGetWithdrawals(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "id": 10, + # "customer_id": "10", + # "asset_symbol": "ETH", + # "amount": "5", + # "state": "CONFIRMED", + # "note": "2018-03-15T16:13:45.610463Z", + # "txid": "0x4643bb6b393ac20a6175c713175734a72517c63d6f73a3ca90a15356f2e967da0", + # "completed_at": "2018-03-15T16:13:45.610463Z", + # "inserted_at": "2018-03-15T16:13:45.610463Z", + # "is_internal": True, + # "target_address": "0x4643bb6b393ac20a6175c713175734a72517c63d6f7" + # } + # ], + # "page_token":"dxvf" + # } + # + withdrawals = self.safe_list(response, 'data', []) + return self.parse_transactions(withdrawals, currency, since, limit) + + def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://open.big.one/docs/spot_transfer.html#transfer-of-user + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: 'SPOT', 'FUND', or 'CONTRACT' + :param str toAccount: 'SPOT', 'FUND', or 'CONTRACT' + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transfer structure ` + """ + self.load_markets() + currency = self.currency(code) + accountsByType = self.safe_dict(self.options, 'accountsByType', {}) + fromId = self.safe_string(accountsByType, fromAccount, fromAccount) + toId = self.safe_string(accountsByType, toAccount, toAccount) + guid = self.safe_string(params, 'guid', self.uuid()) + request: dict = { + 'symbol': currency['id'], + 'amount': self.currency_to_precision(code, amount), + 'from': fromId, + 'to': toId, + 'guid': guid, + # 'type': type, # NORMAL, MASTER_TO_SUB, SUB_TO_MASTER, SUB_INTERNAL, default is NORMAL + # 'sub_acccunt': '', # when type is NORMAL, it should be empty, and when type is others it is required + } + response = self.privatePostTransfer(self.extend(request, params)) + # + # { + # "code": 0, + # "data": null + # } + # + transfer = self.parse_transfer(response, currency) + transferOptions = self.safe_dict(self.options, 'transfer', {}) + fillResponseFromRequest = self.safe_bool(transferOptions, 'fillResponseFromRequest', True) + if fillResponseFromRequest: + transfer['fromAccount'] = fromAccount + transfer['toAccount'] = toAccount + transfer['amount'] = amount + transfer['id'] = guid + return transfer + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + # + # { + # "code": 0, + # "data": null + # } + # + code = self.safe_string(transfer, 'code') + return { + 'info': transfer, + 'id': None, + 'timestamp': None, + 'datetime': None, + 'currency': None, + 'amount': None, + 'fromAccount': None, + 'toAccount': None, + 'status': self.parse_transfer_status(code), + } + + def parse_transfer_status(self, status: Str) -> Str: + statuses: dict = { + '0': 'ok', + } + return self.safe_string(statuses, status, 'failed') + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://open.big.one/docs/spot_withdrawal.html#create-withdrawal-of-user + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.load_markets() + currency = self.currency(code) + request: dict = { + 'symbol': currency['id'], + 'target_address': address, + 'amount': self.currency_to_precision(code, amount), + } + if tag is not None: + request['memo'] = tag + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + if networkCode is not None: + request['gateway_name'] = self.network_code_to_id(networkCode) + # requires write permission on the wallet + response = self.privatePostWithdrawals(self.extend(request, params)) + # + # { + # "code":0, + # "message":"", + # "data":{ + # "id":1077391, + # "customer_id":1082679, + # "amount":"21.9000000000000000", + # "txid":"", + # "is_internal":false, + # "kind":"on_chain", + # "state":"PENDING", + # "inserted_at":"2020-06-03T00:50:57+00:00", + # "updated_at":"2020-06-03T00:50:57+00:00", + # "memo":"", + # "target_address":"rDYtYT3dBeuw376rvHqoZBKW3UmvguoBAf", + # "fee":"0.1000000000000000", + # "asset_symbol":"XRP" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_transaction(data, currency) + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None # fallback to default error handler + # + # {"code":10013,"message":"Resource not found"} + # {"code":40004,"message":"invalid jwt"} + # + code = self.safe_string(response, 'code') + message = self.safe_string(response, 'message') + if (code != '0') and (code is not None): + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], code, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + raise ExchangeError(feedback) # unknown message + return None diff --git a/ccxt/binance.py b/ccxt/binance.py new file mode 100644 index 0000000..3e22052 --- /dev/null +++ b/ccxt/binance.py @@ -0,0 +1,13486 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.binance import ImplicitAPI +import hashlib +import json +from ccxt.base.types import Any, Balances, BorrowInterest, Conversion, CrossBorrowRate, Currencies, Currency, DepositAddress, Greeks, Int, IsolatedBorrowRate, IsolatedBorrowRates, LedgerEntry, Leverage, Leverages, LeverageTier, LeverageTiers, LongShortRatio, MarginMode, MarginModes, MarginModification, Market, Num, Option, Order, OrderBook, OrderRequest, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, FundingRates, Trade, TradingFeeInterface, TradingFees, Transaction, MarketInterface, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import AccountSuspended +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import OperationRejected +from ccxt.base.errors import MarginModeAlreadySet +from ccxt.base.errors import MarketClosed +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import OrderImmediatelyFillable +from ccxt.base.errors import OrderNotFillable +from ccxt.base.errors import NotSupported +from ccxt.base.errors import OperationFailed +from ccxt.base.errors import DDoSProtection +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import OnMaintenance +from ccxt.base.errors import InvalidNonce +from ccxt.base.errors import RequestTimeout +from ccxt.base.errors import BadResponse +from ccxt.base.decimal_to_precision import TRUNCATE +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class binance(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(binance, self).describe(), { + 'id': 'binance', + 'name': 'Binance', + 'countries': [], # Japan + 'rateLimit': 50, + 'certified': True, + 'pro': True, + # new metainfo2 interface + 'has': { + 'CORS': None, + 'spot': True, + 'margin': True, + 'swap': True, + 'future': True, + 'option': True, + 'addMargin': True, + 'borrowCrossMargin': True, + 'borrowIsolatedMargin': True, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'cancelOrders': True, # contract only + 'closeAllPositions': False, + 'closePosition': False, # exchange specific closePosition parameter for binance createOrder is not synonymous with how CCXT uses closePositions + 'createConvertTrade': True, + 'createDepositAddress': False, + 'createLimitBuyOrder': True, + 'createLimitSellOrder': True, + 'createMarketBuyOrder': True, + 'createMarketBuyOrderWithCost': True, + 'createMarketOrderWithCost': True, + 'createMarketSellOrder': True, + 'createMarketSellOrderWithCost': True, + 'createOrder': True, + 'createOrders': True, + 'createOrderWithTakeProfitAndStopLoss': False, + 'createPostOnlyOrder': True, + 'createReduceOnlyOrder': True, + 'createStopLimitOrder': True, + 'createStopLossOrder': True, + 'createStopMarketOrder': False, + 'createStopOrder': True, + 'createTakeProfitOrder': True, + 'createTrailingPercentOrder': True, + 'createTriggerOrder': True, + 'editOrder': True, + 'editOrders': True, + 'fetchAccounts': None, + 'fetchAllGreeks': True, + 'fetchBalance': True, + 'fetchBidsAsks': True, + 'fetchBorrowInterest': True, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': True, + 'fetchCanceledAndClosedOrders': 'emulated', + 'fetchCanceledOrders': 'emulated', + 'fetchClosedOrder': False, + 'fetchClosedOrders': 'emulated', + 'fetchConvertCurrencies': True, + 'fetchConvertQuote': True, + 'fetchConvertTrade': True, + 'fetchConvertTradeHistory': True, + 'fetchCrossBorrowRate': True, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDeposit': False, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchDepositsWithdrawals': False, + 'fetchDepositWithdrawFee': 'emulated', + 'fetchDepositWithdrawFees': True, + 'fetchFundingHistory': True, + 'fetchFundingInterval': 'emulated', + 'fetchFundingIntervals': True, + 'fetchFundingRate': True, + 'fetchFundingRateHistory': True, + 'fetchFundingRates': True, + 'fetchGreeks': True, + 'fetchIndexOHLCV': True, + 'fetchIsolatedBorrowRate': 'emulated', + 'fetchIsolatedBorrowRates': True, + 'fetchL3OrderBook': False, + 'fetchLastPrices': True, + 'fetchLedger': True, + 'fetchLedgerEntry': True, + 'fetchLeverage': 'emulated', + 'fetchLeverages': True, + 'fetchLeverageTiers': True, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': True, + 'fetchMarginAdjustmentHistory': True, + 'fetchMarginMode': True, + 'fetchMarginModes': True, + 'fetchMarketLeverageTiers': 'emulated', + 'fetchMarkets': True, + 'fetchMarkOHLCV': True, + 'fetchMarkPrice': True, + 'fetchMarkPrices': True, + 'fetchMyLiquidations': True, + 'fetchMySettlementHistory': True, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': True, + 'fetchOpenInterestHistory': True, + 'fetchOpenOrder': True, + 'fetchOpenOrders': True, + 'fetchOption': True, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrderBooks': False, + 'fetchOrders': True, + 'fetchOrderTrades': True, + 'fetchPosition': True, + 'fetchPositionHistory': False, + 'fetchPositionMode': True, + 'fetchPositions': True, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': True, + 'fetchPremiumIndexOHLCV': True, + 'fetchSettlementHistory': True, + 'fetchStatus': True, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': True, + 'fetchTradingFees': True, + 'fetchTradingLimits': 'emulated', + 'fetchTransactionFee': 'emulated', + 'fetchTransactionFees': True, + 'fetchTransactions': False, + 'fetchTransfer': False, + 'fetchTransfers': True, + 'fetchUnderlyingAssets': False, + 'fetchVolatilityHistory': False, + 'fetchWithdrawAddresses': False, + 'fetchWithdrawal': False, + 'fetchWithdrawals': True, + 'fetchWithdrawalWhitelist': False, + 'reduceMargin': True, + 'repayCrossMargin': True, + 'repayIsolatedMargin': True, + 'sandbox': True, + 'setLeverage': True, + 'setMargin': False, + 'setMarginMode': True, + 'setPositionMode': True, + 'signIn': False, + 'transfer': True, + 'withdraw': True, + }, + 'timeframes': { + '1s': '1s', # spot only for now + '1m': '1m', + '3m': '3m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1h', + '2h': '2h', + '4h': '4h', + '6h': '6h', + '8h': '8h', + '12h': '12h', + '1d': '1d', + '3d': '3d', + '1w': '1w', + '1M': '1M', + }, + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/e9419b93-ccb0-46aa-9bff-c883f096274b', + 'test': { + 'dapiPublic': 'https://testnet.binancefuture.com/dapi/v1', + 'dapiPrivate': 'https://testnet.binancefuture.com/dapi/v1', + 'dapiPrivateV2': 'https://testnet.binancefuture.com/dapi/v2', + 'fapiPublic': 'https://testnet.binancefuture.com/fapi/v1', + 'fapiPublicV2': 'https://testnet.binancefuture.com/fapi/v2', + 'fapiPublicV3': 'https://testnet.binancefuture.com/fapi/v3', + 'fapiPrivate': 'https://testnet.binancefuture.com/fapi/v1', + 'fapiPrivateV2': 'https://testnet.binancefuture.com/fapi/v2', + 'fapiPrivateV3': 'https://testnet.binancefuture.com/fapi/v3', + 'public': 'https://testnet.binance.vision/api/v3', + 'private': 'https://testnet.binance.vision/api/v3', + 'v1': 'https://testnet.binance.vision/api/v1', + }, + 'demo': { + 'dapiPublic': 'https://demo-dapi.binance.com/dapi/v1', + 'dapiPrivate': 'https://demo-dapi.binance.com/dapi/v1', + 'dapiPrivateV2': 'https://demo-dapi.binance.com/dapi/v2', + 'fapiPublic': 'https://demo-fapi.binance.com/fapi/v1', + 'fapiPublicV2': 'https://demo-fapi.binance.com/fapi/v2', + 'fapiPublicV3': 'https://demo-fapi.binance.com/fapi/v3', + 'fapiPrivate': 'https://demo-fapi.binance.com/fapi/v1', + 'fapiPrivateV2': 'https://demo-fapi.binance.com/fapi/v2', + 'fapiPrivateV3': 'https://demo-fapi.binance.com/fapi/v3', + 'public': 'https://demo-api.binance.com/api/v3', + 'private': 'https://demo-api.binance.com/api/v3', + 'v1': 'https://demo-api.binance.com/api/v1', + }, + 'api': { + 'sapi': 'https://api.binance.com/sapi/v1', + 'sapiV2': 'https://api.binance.com/sapi/v2', + 'sapiV3': 'https://api.binance.com/sapi/v3', + 'sapiV4': 'https://api.binance.com/sapi/v4', + 'dapiPublic': 'https://dapi.binance.com/dapi/v1', + 'dapiPrivate': 'https://dapi.binance.com/dapi/v1', + 'eapiPublic': 'https://eapi.binance.com/eapi/v1', + 'eapiPrivate': 'https://eapi.binance.com/eapi/v1', + 'dapiPrivateV2': 'https://dapi.binance.com/dapi/v2', + 'dapiData': 'https://dapi.binance.com/futures/data', + 'fapiPublic': 'https://fapi.binance.com/fapi/v1', + 'fapiPublicV2': 'https://fapi.binance.com/fapi/v2', + 'fapiPublicV3': 'https://fapi.binance.com/fapi/v3', + 'fapiPrivate': 'https://fapi.binance.com/fapi/v1', + 'fapiPrivateV2': 'https://fapi.binance.com/fapi/v2', + 'fapiPrivateV3': 'https://fapi.binance.com/fapi/v3', + 'fapiData': 'https://fapi.binance.com/futures/data', + 'public': 'https://api.binance.com/api/v3', + 'private': 'https://api.binance.com/api/v3', + 'v1': 'https://api.binance.com/api/v1', + 'papi': 'https://papi.binance.com/papi/v1', + 'papiV2': 'https://papi.binance.com/papi/v2', + }, + 'www': 'https://www.binance.com', + 'referral': { + 'url': 'https://accounts.binance.com/register?ref=CCXTCOM', + 'discount': 0.1, + }, + 'doc': [ + 'https://developers.binance.com/en', + ], + 'api_management': 'https://www.binance.com/en/usercenter/settings/api-management', + 'fees': 'https://www.binance.com/en/fee/schedule', + }, + 'api': { + # the API structure below will need 3-layer apidefs + 'sapi': { + # IP(sapi) request rate limit of 12 000 per minute + # 1 IP(sapi) => cost = 0.1 =>(1000 / (50 * 0.1)) * 60 = 12000 + # 10 IP(sapi) => cost = 1 + # UID(sapi) request rate limit of 180 000 per minute + # 1 UID(sapi) => cost = 0.006667 =>(1000 / (50 * 0.006667)) * 60 = 180000 + 'get': { + # copy trading + 'copyTrading/futures/userStatus': 2, + 'copyTrading/futures/leadSymbol': 2, + 'system/status': 0.1, + # these endpoints require self.apiKey + 'accountSnapshot': 240, # Weight(IP): 2400 => cost = 0.1 * 2400 = 240 + 'account/info': 0.1, + 'margin/asset': 1, # Weight(IP): 10 => cost = 0.1 * 10 = 1 + 'margin/pair': 1, + 'margin/allAssets': 0.1, + 'margin/allPairs': 0.1, + 'margin/priceIndex': 1, + # these endpoints require self.apiKey + self.secret + 'spot/delist-schedule': 10, + 'asset/assetDividend': 1, + 'asset/dribblet': 0.1, + 'asset/transfer': 0.1, + 'asset/assetDetail': 0.1, + 'asset/tradeFee': 0.1, + 'asset/ledger-transfer/cloud-mining/queryByPage': 4.0002, # Weight(UID): 600 => cost = 0.006667 * 600 = 4.0002 + 'asset/convert-transfer/queryByPage': 0.033335, + 'asset/wallet/balance': 6, # Weight(IP): 60 => cost = 0.1 * 60 = 6 + 'asset/custody/transfer-history': 6, # Weight(IP): 60 => cost = 0.1 * 60 = 6 + 'margin/borrow-repay': 1, + 'margin/loan': 1, + 'margin/repay': 1, + 'margin/account': 1, + 'margin/transfer': 0.1, + 'margin/interestHistory': 0.1, + 'margin/forceLiquidationRec': 0.1, + 'margin/order': 1, + 'margin/openOrders': 1, + 'margin/allOrders': 20, # Weight(IP): 200 => cost = 0.1 * 200 = 20 + 'margin/myTrades': 1, + 'margin/maxBorrowable': 5, # Weight(IP): 50 => cost = 0.1 * 50 = 5 + 'margin/maxTransferable': 5, + 'margin/tradeCoeff': 1, + 'margin/isolated/transfer': 0.1, + 'margin/isolated/account': 1, + 'margin/isolated/pair': 1, + 'margin/isolated/allPairs': 1, + 'margin/isolated/accountLimit': 0.1, + 'margin/interestRateHistory': 0.1, + 'margin/orderList': 1, + 'margin/allOrderList': 20, # Weight(IP): 200 => cost = 0.1 * 200 = 20 + 'margin/openOrderList': 1, + 'margin/crossMarginData': {'cost': 0.1, 'noCoin': 0.5}, + 'margin/isolatedMarginData': {'cost': 0.1, 'noCoin': 1}, + 'margin/isolatedMarginTier': 0.1, + 'margin/rateLimit/order': 2, + 'margin/dribblet': 0.1, + 'margin/dust': 20.001, # Weight(UID): 3000 => cost = 0.006667 * 3000 = 20 + 'margin/crossMarginCollateralRatio': 10, + 'margin/exchange-small-liability': 0.6667, + 'margin/exchange-small-liability-history': 0.6667, + 'margin/next-hourly-interest-rate': 0.6667, + 'margin/capital-flow': 10, # Weight(IP): 100 => cost = 0.1 * 100 = 10 + 'margin/delist-schedule': 10, # Weight(IP): 100 => cost = 0.1 * 100 = 10 + 'margin/available-inventory': 0.3334, # Weight(UID): 50 => cost = 0.006667 * 50 = 0.3334 + 'margin/leverageBracket': 0.1, # Weight(IP): 1 => cost = 0.1 * 1 = 0.1 + 'loan/vip/loanable/data': 40, # Weight(IP): 400 => cost = 0.1 * 400 = 40 + 'loan/vip/collateral/data': 40, # Weight(IP): 400 => cost = 0.1 * 400 = 40 + 'loan/vip/request/data': 2.6668, # Weight(UID): 400 => cost = 0.006667 * 400 = 2.6668 + 'loan/vip/request/interestRate': 2.6668, # Weight(UID): 400 => cost = 0.006667 * 400 = 2.6668 + 'loan/income': 40.002, # Weight(UID): 6000 => cost = 0.006667 * 6000 = 40.002 + 'loan/ongoing/orders': 40, # Weight(IP): 400 => cost = 0.1 * 400 = 40 + 'loan/ltv/adjustment/history': 40, # Weight(IP): 400 => cost = 0.1 * 400 = 40 + 'loan/borrow/history': 40, # Weight(IP): 400 => cost = 0.1 * 400 = 40 + 'loan/repay/history': 40, # Weight(IP): 400 => cost = 0.1 * 400 = 40 + 'loan/loanable/data': 40, # Weight(IP): 400 => cost = 0.1 * 400 = 40 + 'loan/collateral/data': 40, # Weight(IP): 400 => cost = 0.1 * 400 = 40 + 'loan/repay/collateral/rate': 600, # Weight(IP): 6000 => cost = 0.1 * 6000 = 600 + 'loan/flexible/ongoing/orders': 30, # TODO: Deprecating at 2024-04-24 03:00(UTC) + 'loan/flexible/borrow/history': 40, # Weight(IP): 400 => cost = 0.1 * 400 = 40, check flexible rate loans order history before 2024-02-27 08:00(UTC) + 'loan/flexible/repay/history': 40, # Weight(IP): 400 => cost = 0.1 * 400 = 40, check flexible rate loans order history before 2024-02-27 08:00(UTC) + 'loan/flexible/ltv/adjustment/history': 40, # Weight(IP): 400 => cost = 0.1 * 400 = 40, check flexible rate loans order history before 2024-02-27 08:00(UTC) + 'loan/vip/ongoing/orders': 40, # Weight(IP): 400 => cost = 0.1 * 400 = 40 + 'loan/vip/repay/history': 40, # Weight(IP): 400 => cost = 0.1 * 400 = 40 + 'loan/vip/collateral/account': 600, # Weight(IP): 6000 => cost = 0.1 * 6000 = 600 + 'fiat/orders': 600.03, # Weight(UID): 90000 => cost = 0.006667 * 90000 = 600.03 + 'fiat/payments': 0.1, + 'futures/transfer': 1, + 'futures/histDataLink': 0.1, # Weight(IP): 1 => cost = 0.1 * 1 = 0.1 + 'rebate/taxQuery': 80.004, # Weight(UID): 12000 => cost = 0.006667 * 12000 = 80.004 + 'capital/config/getall': 1, # get networks for withdrawing USDT ERC20 vs USDT Omni + 'capital/deposit/address': 1, + 'capital/deposit/address/list': 1, + 'capital/deposit/hisrec': 0.1, + 'capital/deposit/subAddress': 0.1, + 'capital/deposit/subHisrec': 0.1, + 'capital/withdraw/history': 2, # Weight(UID): 18000 + (Additional: 10 requests per second => cost = ( 1000 / rateLimit ) / 10 = 2 + 'capital/withdraw/address/list': 10, + 'capital/contract/convertible-coins': 4.0002, # Weight(UID): 600 => cost = 0.006667 * 600 = 4.0002 + 'convert/tradeFlow': 20.001, # Weight(UID): 3000 => cost = 0.006667 * 3000 = 20.001 + 'convert/exchangeInfo': 50, + 'convert/assetInfo': 10, + 'convert/orderStatus': 0.6667, + 'convert/limit/queryOpenOrders': 20.001, # Weight(UID): 3000 => cost = 0.006667 * 3000 = 20.001 + 'account/status': 0.1, + 'account/apiTradingStatus': 0.1, + 'account/apiRestrictions/ipRestriction': 0.1, + 'bnbBurn': 0.1, + 'sub-account/futures/account': 1, + 'sub-account/futures/accountSummary': 0.1, + 'sub-account/futures/positionRisk': 1, + 'sub-account/futures/internalTransfer': 0.1, + 'sub-account/list': 0.1, + 'sub-account/margin/account': 1, + 'sub-account/margin/accountSummary': 1, + 'sub-account/spotSummary': 0.1, + 'sub-account/status': 1, + 'sub-account/sub/transfer/history': 0.1, + 'sub-account/transfer/subUserHistory': 0.1, + 'sub-account/universalTransfer': 0.1, + 'sub-account/apiRestrictions/ipRestriction/thirdPartyList': 1, + 'sub-account/transaction-statistics': 0.40002, # Weight(UID): 60 => cost = 0.006667 * 60 = 0.40002 + 'sub-account/subAccountApi/ipRestriction': 20.001, # Weight(UID): 3000 => cost = 0.006667 * 3000 = 20.001 + 'managed-subaccount/asset': 0.1, + 'managed-subaccount/accountSnapshot': 240, + 'managed-subaccount/queryTransLogForInvestor': 0.1, + 'managed-subaccount/queryTransLogForTradeParent': 0.40002, # Weight(UID): 60 => cost = 0.006667 * 60 = 0.40002 + 'managed-subaccount/fetch-future-asset': 0.40002, # Weight(UID): 60 => cost = 0.006667 * 60 = 0.40002 + 'managed-subaccount/marginAsset': 0.1, + 'managed-subaccount/info': 0.40002, # Weight(UID): 60 => cost = 0.006667 * 60 = 0.40002 + 'managed-subaccount/deposit/address': 0.006667, # Weight(UID): 1 => cost = 0.006667 * 1 = 0.006667 + 'managed-subaccount/query-trans-log': 0.40002, + # lending endpoints + 'lending/daily/product/list': 0.1, + 'lending/daily/userLeftQuota': 0.1, + 'lending/daily/userRedemptionQuota': 0.1, + 'lending/daily/token/position': 0.1, + 'lending/union/account': 0.1, + 'lending/union/purchaseRecord': 0.1, + 'lending/union/redemptionRecord': 0.1, + 'lending/union/interestHistory': 0.1, + 'lending/project/list': 0.1, + 'lending/project/position/list': 0.1, + # eth-staking + 'eth-staking/eth/history/stakingHistory': 15, # Weight(IP): 150 => cost = 0.1 * 150 = 15 + 'eth-staking/eth/history/redemptionHistory': 15, # Weight(IP): 150 => cost = 0.1 * 150 = 15 + 'eth-staking/eth/history/rewardsHistory': 15, # Weight(IP): 150 => cost = 0.1 * 150 = 15 + 'eth-staking/eth/quota': 15, # Weight(IP): 150 => cost = 0.1 * 150 = 15 + 'eth-staking/eth/history/rateHistory': 15, # Weight(IP): 150 => cost = 0.1 * 150 = 15 + 'eth-staking/account': 15, # Weight(IP): 150 => cost = 0.1 * 150 = 15 + 'eth-staking/wbeth/history/wrapHistory': 15, # Weight(IP): 150 => cost = 0.1 * 150 = 15 + 'eth-staking/wbeth/history/unwrapHistory': 15, # Weight(IP): 150 => cost = 0.1 * 150 = 15 + 'eth-staking/eth/history/wbethRewardsHistory': 15, # Weight(IP): 150 => cost = 0.1 * 150 = 15 + 'sol-staking/sol/history/stakingHistory': 15, + 'sol-staking/sol/history/redemptionHistory': 15, + 'sol-staking/sol/history/bnsolRewardsHistory': 15, + 'sol-staking/sol/history/rateHistory': 15, + 'sol-staking/account': 15, + 'sol-staking/sol/quota': 15, + # mining endpoints + 'mining/pub/algoList': 0.1, + 'mining/pub/coinList': 0.1, + 'mining/worker/detail': 0.5, # Weight(IP): 5 => cost = 0.1 * 5 = 0.5 + 'mining/worker/list': 0.5, + 'mining/payment/list': 0.5, + 'mining/statistics/user/status': 0.5, + 'mining/statistics/user/list': 0.5, + 'mining/payment/uid': 0.5, + # liquid swap endpoints + 'bswap/pools': 0.1, + 'bswap/liquidity': {'cost': 0.1, 'noPoolId': 1}, + 'bswap/liquidityOps': 20.001, # Weight(UID): 3000 => cost = 0.006667 * 3000 = 20.001 + 'bswap/quote': 1.00005, # Weight(UID): 150 => cost = 0.006667 * 150 = 1.00005 + 'bswap/swap': 20.001, # Weight(UID): 3000 => cost = 0.006667 * 3000 = 20.001 + 'bswap/poolConfigure': 1.00005, # Weight(UID): 150 => cost = 0.006667 * 150 = 1.00005 + 'bswap/addLiquidityPreview': 1.00005, # Weight(UID): 150 => cost = 0.006667 * 150 = 1.00005 + 'bswap/removeLiquidityPreview': 1.00005, # Weight(UID): 150 => cost = 0.006667 * 150 = 1.00005 + 'bswap/unclaimedRewards': 6.667, # Weight(UID): 1000 => cost = 0.006667 * 1000 = 6.667 + 'bswap/claimedHistory': 6.667, # Weight(UID): 1000 => cost = 0.006667 * 1000 = 6.667 + # leveraged token endpoints + 'blvt/tokenInfo': 0.1, + 'blvt/subscribe/record': 0.1, + 'blvt/redeem/record': 0.1, + 'blvt/userLimit': 0.1, + # broker api TODO(NOT IN DOCS) + 'apiReferral/ifNewUser': 1, + 'apiReferral/customization': 1, + 'apiReferral/userCustomization': 1, + 'apiReferral/rebate/recentRecord': 1, + 'apiReferral/rebate/historicalRecord': 1, + 'apiReferral/kickback/recentRecord': 1, + 'apiReferral/kickback/historicalRecord': 1, + # brokerage API TODO https://binance-docs.github.io/Brokerage-API/General/ does not state ratelimits + 'broker/subAccountApi': 1, + 'broker/subAccount': 1, + 'broker/subAccountApi/commission/futures': 1, + 'broker/subAccountApi/commission/coinFutures': 1, + 'broker/info': 1, + 'broker/transfer': 1, + 'broker/transfer/futures': 1, + 'broker/rebate/recentRecord': 1, + 'broker/rebate/historicalRecord': 1, + 'broker/subAccount/bnbBurn/status': 1, + 'broker/subAccount/depositHist': 1, + 'broker/subAccount/spotSummary': 1, + 'broker/subAccount/marginSummary': 1, + 'broker/subAccount/futuresSummary': 1, + 'broker/rebate/futures/recentRecord': 1, + 'broker/subAccountApi/ipRestriction': 1, + 'broker/universalTransfer': 1, + # v2 not supported yet + # GET /sapi/v2/broker/subAccount/futuresSummary + 'account/apiRestrictions': 0.1, + # c2c / p2p + 'c2c/orderMatch/listUserOrderHistory': 0.1, + # nft endpoints + 'nft/history/transactions': 20.001, # Weight(UID): 3000 => cost = 0.006667 * 3000 = 20.001 + 'nft/history/deposit': 20.001, + 'nft/history/withdraw': 20.001, + 'nft/user/getAsset': 20.001, + 'pay/transactions': 20.001, + 'giftcard/verify': 0.1, + 'giftcard/cryptography/rsa-public-key': 0.1, + 'giftcard/buyCode/token-limit': 0.1, + 'algo/spot/openOrders': 0.1, + 'algo/spot/historicalOrders': 0.1, + 'algo/spot/subOrders': 0.1, + 'algo/futures/openOrders': 0.1, + 'algo/futures/historicalOrders': 0.1, + 'algo/futures/subOrders': 0.1, + 'portfolio/account': 0.1, + 'portfolio/collateralRate': 5, + 'portfolio/pmLoan': 3.3335, + 'portfolio/interest-history': 0.6667, + 'portfolio/asset-index-price': 0.1, + 'portfolio/repay-futures-switch': 3, # Weight(IP): 30 => cost = 0.1 * 30 = 3 + 'portfolio/margin-asset-leverage': 5, # Weight(IP): 50 => cost = 0.1 * 50 = 5 + 'portfolio/balance': 2, + 'portfolio/negative-balance-exchange-record': 2, + 'portfolio/pmloan-history': 5, + 'portfolio/earn-asset-balance': 150, # Weight(IP): 1500 => cost = 0.1 * 1500 = 150 + # staking + 'staking/productList': 0.1, + 'staking/position': 0.1, + 'staking/stakingRecord': 0.1, + 'staking/personalLeftQuota': 0.1, + 'lending/auto-invest/target-asset/list': 0.1, # Weight(IP): 1 => cost = 0.1 * 1 = 0.1 + 'lending/auto-invest/target-asset/roi/list': 0.1, + 'lending/auto-invest/all/asset': 0.1, + 'lending/auto-invest/source-asset/list': 0.1, + 'lending/auto-invest/plan/list': 0.1, + 'lending/auto-invest/plan/id': 0.1, + 'lending/auto-invest/history/list': 0.1, + 'lending/auto-invest/index/info': 0.1, # Weight(IP): 1 => cost = 0.1 * 1 = 0.1 + 'lending/auto-invest/index/user-summary': 0.1, # Weight(IP): 1 => cost = 0.1 * 1 = 0.1 + 'lending/auto-invest/one-off/status': 0.1, # Weight(IP): 1 => cost = 0.1 * 1 = 0.1 + 'lending/auto-invest/redeem/history': 0.1, # Weight(IP): 1 => cost = 0.1 * 1 = 0.1 + 'lending/auto-invest/rebalance/history': 0.1, # Weight(IP): 1 => cost = 0.1 * 1 = 0.1 + # simple earn + 'simple-earn/flexible/list': 15, + 'simple-earn/locked/list': 15, + 'simple-earn/flexible/personalLeftQuota': 15, + 'simple-earn/locked/personalLeftQuota': 15, + 'simple-earn/flexible/subscriptionPreview': 15, + 'simple-earn/locked/subscriptionPreview': 15, + 'simple-earn/flexible/history/rateHistory': 15, + 'simple-earn/flexible/position': 15, + 'simple-earn/locked/position': 15, + 'simple-earn/account': 15, + 'simple-earn/flexible/history/subscriptionRecord': 15, + 'simple-earn/locked/history/subscriptionRecord': 15, + 'simple-earn/flexible/history/redemptionRecord': 15, + 'simple-earn/locked/history/redemptionRecord': 15, + 'simple-earn/flexible/history/rewardsRecord': 15, + 'simple-earn/locked/history/rewardsRecord': 15, + 'simple-earn/flexible/history/collateralRecord': 0.1, + # Convert + 'dci/product/list': 0.1, + 'dci/product/positions': 0.1, + 'dci/product/accounts': 0.1, + }, + 'post': { + 'asset/dust': 0.06667, # Weight(UID): 10 => cost = 0.006667 * 10 = 0.06667 + 'asset/dust-btc': 0.1, + 'asset/transfer': 6.0003, # Weight(UID): 900 => cost = 0.006667 * 900 = 6.0003 + 'asset/get-funding-asset': 0.1, + 'asset/convert-transfer': 0.033335, + 'account/disableFastWithdrawSwitch': 0.1, + 'account/enableFastWithdrawSwitch': 0.1, + # 'account/apiRestrictions/ipRestriction': 1, discontinued + # 'account/apiRestrictions/ipRestriction/ipList': 1, discontinued + 'capital/withdraw/apply': 4.0002, # Weight(UID): 600 => cost = 0.006667 * 600 = 4.0002 + 'capital/contract/convertible-coins': 4.0002, + 'capital/deposit/credit-apply': 0.1, # Weight(IP): 1 => cost = 0.1 * 1 = 0.1 + 'margin/borrow-repay': 20.001, + 'margin/transfer': 4.0002, + 'margin/loan': 20.001, # Weight(UID): 3000 => cost = 0.006667 * 3000 = 20.001 + 'margin/repay': 20.001, + 'margin/order': 0.040002, # Weight(UID): 6 => cost = 0.006667 * 6 = 0.040002 + 'margin/order/oco': 0.040002, + 'margin/dust': 20.001, # Weight(UID): 3000 => cost = 0.006667 * 3000 = 20.001 + 'margin/exchange-small-liability': 20.001, + # 'margin/isolated/create': 1, discontinued + 'margin/isolated/transfer': 4.0002, # Weight(UID): 600 => cost = 0.006667 * 600 = 4.0002 + 'margin/isolated/account': 2.0001, # Weight(UID): 300 => cost = 0.006667 * 300 = 2.0001 + 'margin/max-leverage': 300, # Weight(IP): 3000 => cost = 0.1 * 3000 = 300 + 'bnbBurn': 0.1, + 'sub-account/virtualSubAccount': 0.1, + 'sub-account/margin/transfer': 4.0002, # Weight(UID): 600 => cost = 0.006667 * 600 = 4.0002 + 'sub-account/margin/enable': 0.1, + 'sub-account/futures/enable': 0.1, + 'sub-account/futures/transfer': 0.1, + 'sub-account/futures/internalTransfer': 0.1, + 'sub-account/transfer/subToSub': 0.1, + 'sub-account/transfer/subToMaster': 0.1, + 'sub-account/universalTransfer': 0.1, + 'sub-account/options/enable': 0.1, + 'managed-subaccount/deposit': 0.1, + 'managed-subaccount/withdraw': 0.1, + 'userDataStream': 0.1, + 'userDataStream/isolated': 0.1, + 'futures/transfer': 0.1, + # lending + 'lending/customizedFixed/purchase': 0.1, + 'lending/daily/purchase': 0.1, + 'lending/daily/redeem': 0.1, + # liquid swap endpoints + 'bswap/liquidityAdd': 60, # Weight(UID): 1000 + (Additional: 1 request every 3 seconds = 0.333 requests per second) => cost = ( 1000 / rateLimit ) / 0.333 = 60.0000006 + 'bswap/liquidityRemove': 60, # Weight(UID): 1000 + (Additional: 1 request every three seconds) + 'bswap/swap': 60, # Weight(UID): 1000 + (Additional: 1 request every three seconds) + 'bswap/claimRewards': 6.667, # Weight(UID): 1000 => cost = 0.006667 * 1000 = 6.667 + # leveraged token endpoints + 'blvt/subscribe': 0.1, + 'blvt/redeem': 0.1, + # brokerage API TODO: NO MENTION OF RATELIMITS IN BROKERAGE DOCS + 'apiReferral/customization': 1, + 'apiReferral/userCustomization': 1, + 'apiReferral/rebate/historicalRecord': 1, + 'apiReferral/kickback/historicalRecord': 1, + 'broker/subAccount': 1, + 'broker/subAccount/margin': 1, + 'broker/subAccount/futures': 1, + 'broker/subAccountApi': 1, + 'broker/subAccountApi/permission': 1, + 'broker/subAccountApi/commission': 1, + 'broker/subAccountApi/commission/futures': 1, + 'broker/subAccountApi/commission/coinFutures': 1, + 'broker/transfer': 1, + 'broker/transfer/futures': 1, + 'broker/rebate/historicalRecord': 1, + 'broker/subAccount/bnbBurn/spot': 1, + 'broker/subAccount/bnbBurn/marginInterest': 1, + 'broker/subAccount/blvt': 1, + 'broker/subAccountApi/ipRestriction': 1, + 'broker/subAccountApi/ipRestriction/ipList': 1, + 'broker/universalTransfer': 1, + 'broker/subAccountApi/permission/universalTransfer': 1, + 'broker/subAccountApi/permission/vanillaOptions': 1, + # + 'giftcard/createCode': 0.1, + 'giftcard/redeemCode': 0.1, + 'giftcard/buyCode': 0.1, + 'algo/spot/newOrderTwap': 20.001, + 'algo/futures/newOrderVp': 20.001, + 'algo/futures/newOrderTwap': 20.001, + # staking + 'staking/purchase': 0.1, + 'staking/redeem': 0.1, + 'staking/setAutoStaking': 0.1, + # eth-staking + 'eth-staking/eth/stake': 15, # Weight(IP): 150 => cost = 0.1 * 150 = 15 + 'eth-staking/eth/redeem': 15, # Weight(IP): 150 => cost = 0.1 * 150 = 15 + 'eth-staking/wbeth/wrap': 15, # Weight(IP): 150 => cost = 0.1 * 150 = 15 + 'sol-staking/sol/stake': 15, + 'sol-staking/sol/redeem': 15, + # mining endpoints + 'mining/hash-transfer/config': 0.5, # Weight(IP): 5 => cost = 0.1 * 5 = 0.5 + 'mining/hash-transfer/config/cancel': 0.5, # Weight(IP): 5 => cost = 0.1 * 5 = 0.5 + 'portfolio/repay': 20.001, + 'loan/vip/renew': 40.002, # Weight(UID): 6000 => cost = 0.006667 * 6000 = 40.002 + 'loan/vip/borrow': 40.002, + 'loan/borrow': 40.002, + 'loan/repay': 40.002, + 'loan/adjust/ltv': 40.002, + 'loan/customize/margin_call': 40.002, + 'loan/flexible/repay': 40.002, # TODO: Deprecating at 2024-04-24 03:00(UTC) + 'loan/flexible/adjust/ltv': 40.002, # TODO: Deprecating at 2024-04-24 03:00(UTC) + 'loan/vip/repay': 40.002, + 'convert/getQuote': 1.3334, # Weight(UID): 200 => cost = 0.006667 * 200 = 1.3334 + 'convert/acceptQuote': 3.3335, # Weight(UID): 500 => cost = 0.006667 * 500 = 3.3335 + 'convert/limit/placeOrder': 3.3335, # Weight(UID): 500 => cost = 0.006667 * 500 = 3.3335 + 'convert/limit/cancelOrder': 1.3334, # Weight(UID): 200 => cost = 0.006667 * 200 = 1.3334 + 'portfolio/auto-collection': 150, # Weight(IP): 1500 => cost = 0.1 * 1500 = 150 + 'portfolio/asset-collection': 6, # Weight(IP): 60 => cost = 0.1 * 60 = 6 + 'portfolio/bnb-transfer': 150, # Weight(IP): 1500 => cost = 0.1 * 1500 = 150 + 'portfolio/repay-futures-switch': 150, # Weight(IP): 1500 => cost = 0.1 * 1500 = 150 + 'portfolio/repay-futures-negative-balance': 150, # Weight(IP): 1500 => cost = 0.1 * 1500 = 150 + 'portfolio/mint': 20, + 'portfolio/redeem': 20, + 'portfolio/earn-asset-transfer': 150, # Weight(IP): 1500 => cost = 0.1 * 1500 = 150 + 'lending/auto-invest/plan/add': 0.1, # Weight(IP): 1 => cost = 0.1 * 1 = 0.1 + 'lending/auto-invest/plan/edit': 0.1, # Weight(IP): 1 => cost = 0.1 * 1 = 0.1 + 'lending/auto-invest/plan/edit-status': 0.1, # Weight(IP): 1 => cost = 0.1 * 1 = 0.1 + 'lending/auto-invest/one-off': 0.1, # Weight(IP): 1 => cost = 0.1 * 1 = 0.1 + 'lending/auto-invest/redeem': 0.1, # Weight(IP): 1 => cost = 0.1 * 1 = 0.1 + # simple earn + 'simple-earn/flexible/subscribe': 0.1, + 'simple-earn/locked/subscribe': 0.1, + 'simple-earn/flexible/redeem': 0.1, + 'simple-earn/locked/redeem': 0.1, + 'simple-earn/flexible/setAutoSubscribe': 15, + 'simple-earn/locked/setAutoSubscribe': 15, + 'simple-earn/locked/setRedeemOption': 5, + # convert + 'dci/product/subscribe': 0.1, + 'dci/product/auto_compound/edit': 0.1, + }, + 'put': { + 'userDataStream': 0.1, + 'userDataStream/isolated': 0.1, + }, + 'delete': { + # 'account/apiRestrictions/ipRestriction/ipList': 1, discontinued + 'margin/openOrders': 0.1, + 'margin/order': 0.006667, # Weight(UID): 1 => cost = 0.006667 + 'margin/orderList': 0.006667, + 'margin/isolated/account': 2.0001, # Weight(UID): 300 => cost = 0.006667 * 300 = 2.0001 + 'userDataStream': 0.1, + 'userDataStream/isolated': 0.1, + # brokerage API TODO NO MENTION OF RATELIMIT IN BROKERAGE DOCS + 'broker/subAccountApi': 1, + 'broker/subAccountApi/ipRestriction/ipList': 1, + 'algo/spot/order': 0.1, + 'algo/futures/order': 0.1, + 'sub-account/subAccountApi/ipRestriction/ipList': 20.001, # Weight(UID): 3000 => cost = 0.006667 * 3000 = 20.001 + }, + }, + 'sapiV2': { + 'get': { + 'eth-staking/account': 15, # Weight(IP): 150 => cost = 0.1 * 150 = 15 + 'sub-account/futures/account': 0.1, + 'sub-account/futures/accountSummary': 1, + 'sub-account/futures/positionRisk': 0.1, + 'loan/flexible/ongoing/orders': 30, # Weight(IP): 300 => cost = 0.1 * 300 = 30 + 'loan/flexible/borrow/history': 40, # Weight(IP): 400 => cost = 0.1 * 400 = 40 + 'loan/flexible/repay/history': 40, # Weight(IP): 400 => cost = 0.1 * 400 = 40 + 'loan/flexible/ltv/adjustment/history': 40, # Weight(IP): 400 => cost = 0.1 * 400 = 40 + 'loan/flexible/loanable/data': 40, # Weight(IP): 400 => cost = 0.1 * 400 = 40 + 'loan/flexible/collateral/data': 40, # Weight(IP): 400 => cost = 0.1 * 400 = 40 + 'portfolio/account': 2, + }, + 'post': { + 'eth-staking/eth/stake': 15, # Weight(IP): 150 => cost = 0.1 * 150 = 15 + 'sub-account/subAccountApi/ipRestriction': 20.001, # Weight(UID): 3000 => cost = 0.006667 * 3000 = 20.001 + 'loan/flexible/borrow': 40.002, # Weight(UID): 6000 => cost = 0.006667 * 6000 = 40.002 + 'loan/flexible/repay': 40.002, # Weight(UID): 6000 => cost = 0.006667 * 6000 = 40.002 + 'loan/flexible/adjust/ltv': 40.002, # Weight(UID): 6000 => cost = 0.006667 * 6000 = 40.002 + }, + }, + 'sapiV3': { + 'get': { + 'sub-account/assets': 0.40002, # Weight(UID): 60 => cost = 0.006667 * 60 = 0.40002 + }, + 'post': { + 'asset/getUserAsset': 0.5, + }, + }, + 'sapiV4': { + 'get': { + 'sub-account/assets': 0.40002, # Weight(UID): 60 => cost = 0.006667 * 60 = 0.40002 + }, + }, + 'dapiPublic': { + 'get': { + 'ping': 1, + 'time': 1, + 'exchangeInfo': 1, + 'depth': {'cost': 2, 'byLimit': [[50, 2], [100, 5], [500, 10], [1000, 20]]}, + 'trades': 5, + 'historicalTrades': 20, + 'aggTrades': 20, + 'premiumIndex': 10, + 'fundingRate': 1, + 'klines': {'cost': 1, 'byLimit': [[99, 1], [499, 2], [1000, 5], [10000, 10]]}, + 'continuousKlines': {'cost': 1, 'byLimit': [[99, 1], [499, 2], [1000, 5], [10000, 10]]}, + 'indexPriceKlines': {'cost': 1, 'byLimit': [[99, 1], [499, 2], [1000, 5], [10000, 10]]}, + 'markPriceKlines': {'cost': 1, 'byLimit': [[99, 1], [499, 2], [1000, 5], [10000, 10]]}, + 'premiumIndexKlines': {'cost': 1, 'byLimit': [[99, 1], [499, 2], [1000, 5], [10000, 10]]}, + 'ticker/24hr': {'cost': 1, 'noSymbol': 40}, + 'ticker/price': {'cost': 1, 'noSymbol': 2}, + 'ticker/bookTicker': {'cost': 2, 'noSymbol': 5}, + 'constituents': 2, + 'openInterest': 1, + 'fundingInfo': 1, + }, + }, + 'dapiData': { + 'get': { + 'delivery-price': 1, + 'openInterestHist': 1, + 'topLongShortAccountRatio': 1, + 'topLongShortPositionRatio': 1, + 'globalLongShortAccountRatio': 1, + 'takerBuySellVol': 1, + 'basis': 1, + }, + }, + 'dapiPrivate': { + 'get': { + 'positionSide/dual': 30, + 'orderAmendment': 1, + 'order': 1, + 'openOrder': 1, + 'openOrders': {'cost': 1, 'noSymbol': 5}, + 'allOrders': {'cost': 20, 'noSymbol': 40}, + 'balance': 1, + 'account': 5, + 'positionMargin/history': 1, + 'positionRisk': 1, + 'userTrades': {'cost': 20, 'noSymbol': 40}, + 'income': 20, + 'leverageBracket': 1, + 'forceOrders': {'cost': 20, 'noSymbol': 50}, + 'adlQuantile': 5, + 'commissionRate': 20, + 'income/asyn': 5, + 'income/asyn/id': 5, + 'trade/asyn': 0.5, + 'trade/asyn/id': 0.5, + 'order/asyn': 0.5, + 'order/asyn/id': 0.5, + 'pmExchangeInfo': 0.5, # Weight(IP): 5 => cost = 0.1 * 5 = 0.5 + 'pmAccountInfo': 0.5, # Weight(IP): 5 => cost = 0.1 * 5 = 0.5 + }, + 'post': { + 'positionSide/dual': 1, + 'order': 4, + 'batchOrders': 5, + 'countdownCancelAll': 10, + 'leverage': 1, + 'marginType': 1, + 'positionMargin': 1, + 'listenKey': 1, + }, + 'put': { + 'listenKey': 1, + 'order': 1, + 'batchOrders': 5, + }, + 'delete': { + 'order': 1, + 'allOpenOrders': 1, + 'batchOrders': 5, + 'listenKey': 1, + }, + }, + 'dapiPrivateV2': { + 'get': { + 'leverageBracket': 1, + }, + }, + 'fapiPublic': { + 'get': { + 'ping': 1, + 'time': 1, + 'exchangeInfo': 1, + 'depth': {'cost': 2, 'byLimit': [[50, 2], [100, 5], [500, 10], [1000, 20]]}, + 'trades': 5, + 'historicalTrades': 20, + 'aggTrades': 20, + 'klines': {'cost': 1, 'byLimit': [[99, 1], [499, 2], [1000, 5], [10000, 10]]}, + 'continuousKlines': {'cost': 1, 'byLimit': [[99, 1], [499, 2], [1000, 5], [10000, 10]]}, + 'markPriceKlines': {'cost': 1, 'byLimit': [[99, 1], [499, 2], [1000, 5], [10000, 10]]}, + 'indexPriceKlines': {'cost': 1, 'byLimit': [[99, 1], [499, 2], [1000, 5], [10000, 10]]}, + 'premiumIndexKlines': {'cost': 1, 'byLimit': [[99, 1], [499, 2], [1000, 5], [10000, 10]]}, + 'fundingRate': 1, + 'fundingInfo': 1, + 'premiumIndex': 1, + 'ticker/24hr': {'cost': 1, 'noSymbol': 40}, + 'ticker/price': {'cost': 1, 'noSymbol': 2}, + 'ticker/bookTicker': {'cost': 1, 'noSymbol': 2}, + 'openInterest': 1, + 'indexInfo': 1, + 'assetIndex': {'cost': 1, 'noSymbol': 10}, + 'constituents': 2, + 'apiTradingStatus': {'cost': 1, 'noSymbol': 10}, + 'lvtKlines': 1, + 'convert/exchangeInfo': 4, + 'insuranceBalance': 1, + }, + }, + 'fapiData': { + 'get': { + 'delivery-price': 1, + 'openInterestHist': 1, + 'topLongShortAccountRatio': 1, + 'topLongShortPositionRatio': 1, + 'globalLongShortAccountRatio': 1, + 'takerlongshortRatio': 1, + 'basis': 1, + }, + }, + 'fapiPrivate': { + 'get': { + 'forceOrders': {'cost': 20, 'noSymbol': 50}, + 'allOrders': 5, + 'openOrder': 1, + 'openOrders': {'cost': 1, 'noSymbol': 40}, + 'order': 1, + 'account': 5, + 'balance': 5, + 'leverageBracket': 1, + 'positionMargin/history': 1, + 'positionRisk': 5, + 'positionSide/dual': 30, + 'userTrades': 5, + 'income': 30, + 'commissionRate': 20, + 'rateLimit/order': 1, + 'apiTradingStatus': 1, + 'multiAssetsMargin': 30, + # broker endpoints + 'apiReferral/ifNewUser': 1, + 'apiReferral/customization': 1, + 'apiReferral/userCustomization': 1, + 'apiReferral/traderNum': 1, + 'apiReferral/overview': 1, + 'apiReferral/tradeVol': 1, + 'apiReferral/rebateVol': 1, + 'apiReferral/traderSummary': 1, + 'adlQuantile': 5, + 'pmAccountInfo': 5, + 'orderAmendment': 1, + 'income/asyn': 1000, + 'income/asyn/id': 10, + 'order/asyn': 1000, + 'order/asyn/id': 10, + 'trade/asyn': 1000, + 'trade/asyn/id': 10, + 'feeBurn': 1, + 'symbolConfig': 5, + 'accountConfig': 5, + 'convert/orderStatus': 5, + }, + 'post': { + 'batchOrders': 5, + 'positionSide/dual': 1, + 'positionMargin': 1, + 'marginType': 1, + 'order': 4, + 'leverage': 1, + 'listenKey': 1, + 'countdownCancelAll': 10, + 'multiAssetsMargin': 1, + # broker endpoints + 'apiReferral/customization': 1, + 'apiReferral/userCustomization': 1, + 'feeBurn': 1, + 'convert/getQuote': 200, # 360 requests per hour + 'convert/acceptQuote': 20, + }, + 'put': { + 'listenKey': 1, + 'order': 1, + 'batchOrders': 5, + }, + 'delete': { + 'batchOrders': 1, + 'order': 1, + 'allOpenOrders': 1, + 'listenKey': 1, + }, + }, + 'fapiPublicV2': { + 'get': { + 'ticker/price': 0, + }, + }, + 'fapiPrivateV2': { + 'get': { + 'account': 1, + 'balance': 1, + 'positionRisk': 1, + }, + }, + 'fapiPublicV3': { + 'get': {}, + }, + 'fapiPrivateV3': { + 'get': { + 'account': 1, + 'balance': 1, + 'positionRisk': 1, + }, + }, + 'eapiPublic': { + 'get': { + 'ping': 1, + 'time': 1, + 'exchangeInfo': 1, + 'index': 1, + 'ticker': 5, + 'mark': 5, + 'depth': 1, + 'klines': 1, + 'trades': 5, + 'historicalTrades': 20, + 'exerciseHistory': 3, + 'openInterest': 3, + }, + }, + 'eapiPrivate': { + 'get': { + 'account': 3, + 'position': 5, + 'openOrders': {'cost': 1, 'noSymbol': 40}, + 'historyOrders': 3, + 'userTrades': 5, + 'exerciseRecord': 5, + 'bill': 1, + 'income/asyn': 5, + 'income/asyn/id': 5, + 'marginAccount': 3, + 'mmp': 1, + 'countdownCancelAll': 1, + 'order': 1, + 'block/order/orders': 5, + 'block/order/execute': 5, + 'block/user-trades': 5, + 'blockTrades': 5, + }, + 'post': { + 'order': 1, + 'batchOrders': 5, + 'listenKey': 1, + 'mmpSet': 1, + 'mmpReset': 1, + 'countdownCancelAll': 1, + 'countdownCancelAllHeartBeat': 10, + 'block/order/create': 5, + 'block/order/execute': 5, + }, + 'put': { + 'listenKey': 1, + 'block/order/create': 5, + }, + 'delete': { + 'order': 1, + 'batchOrders': 1, + 'allOpenOrders': 1, + 'allOpenOrdersByUnderlying': 1, + 'listenKey': 1, + 'block/order/create': 5, + }, + }, + 'public': { + # IP(api) request rate limit of 6000 per minute + # 1 IP(api) => cost = 0.2 =>(1000 / (50 * 0.2)) * 60 = 6000 + 'get': { + 'ping': 0.2, # Weight(IP): 1 => cost = 0.2 * 1 = 0.2 + 'time': 0.2, + 'depth': {'cost': 1, 'byLimit': [[100, 1], [500, 5], [1000, 10], [5000, 50]]}, + 'trades': 2, # Weight(IP): 10 => cost = 0.2 * 10 = 2 + 'aggTrades': 0.4, + 'historicalTrades': 2, # Weight(IP): 10 => cost = 0.2 * 10 = 2 + 'klines': 0.4, + 'uiKlines': 0.4, + 'ticker/24hr': {'cost': 0.4, 'noSymbol': 16}, + 'ticker': {'cost': 0.4, 'noSymbol': 16}, + 'ticker/tradingDay': 0.8, + 'ticker/price': {'cost': 0.4, 'noSymbol': 0.8}, + 'ticker/bookTicker': {'cost': 0.4, 'noSymbol': 0.8}, + 'exchangeInfo': 4, # Weight(IP): 20 => cost = 0.2 * 20 = 4 + 'avgPrice': 0.4, + }, + 'put': { + 'userDataStream': 0.4, + }, + 'post': { + 'userDataStream': 0.4, + }, + 'delete': { + 'userDataStream': 0.4, + }, + }, + 'private': { + 'get': { + 'allOrderList': 4, # oco Weight(IP): 20 => cost = 0.2 * 20 = 4 + 'openOrderList': 1.2, # oco Weight(IP): 6 => cost = 0.2 * 6 = 1.2 + 'orderList': 0.8, # oco + 'order': 0.8, + 'openOrders': {'cost': 1.2, 'noSymbol': 16}, + 'allOrders': 4, + 'account': 4, + 'myTrades': 4, + 'rateLimit/order': 8, # Weight(IP): 40 => cost = 0.2 * 40 = 8 + 'myPreventedMatches': 4, # Weight(IP): 20 => cost = 0.2 * 20 = 4 + 'myAllocations': 4, + 'account/commission': 4, + }, + 'post': { + 'order/oco': 0.2, + 'orderList/oco': 0.2, + 'orderList/oto': 0.2, + 'orderList/otoco': 0.2, + 'sor/order': 0.2, + 'sor/order/test': 0.2, + 'order': 0.2, + 'order/cancelReplace': 0.2, + 'order/test': 0.2, + }, + 'delete': { + 'openOrders': 0.2, + 'orderList': 0.2, # oco + 'order': 0.2, + }, + }, + 'papi': { + # IP(papi) request rate limit of 6000 per minute + # 1 IP(papi) => cost = 0.2 =>(1000 / (50 * 0.2)) * 60 = 6000 + # Order(papi) request rate limit of 1200 per minute + # 1 Order(papi) => cost = 1 =>(1000 / (50 * 1)) * 60 = 1200 + 'get': { + 'ping': 0.2, + 'um/order': 1, + 'um/openOrder': 1, + 'um/openOrders': {'cost': 1, 'noSymbol': 40}, + 'um/allOrders': 5, + 'cm/order': 1, + 'cm/openOrder': 1, + 'cm/openOrders': {'cost': 1, 'noSymbol': 40}, + 'cm/allOrders': 20, + 'um/conditional/openOrder': 1, + 'um/conditional/openOrders': {'cost': 1, 'noSymbol': 40}, + 'um/conditional/orderHistory': 1, + 'um/conditional/allOrders': {'cost': 1, 'noSymbol': 40}, + 'cm/conditional/openOrder': 1, + 'cm/conditional/openOrders': {'cost': 1, 'noSymbol': 40}, + 'cm/conditional/orderHistory': 1, + 'cm/conditional/allOrders': 40, + 'margin/order': 10, + 'margin/openOrders': 5, + 'margin/allOrders': 100, + 'margin/orderList': 5, + 'margin/allOrderList': 100, + 'margin/openOrderList': 5, + 'margin/myTrades': 5, + 'balance': 4, + 'account': 4, + 'margin/maxBorrowable': 1, + 'margin/maxWithdraw': 1, + 'um/positionRisk': 1, + 'cm/positionRisk': 0.2, + 'um/positionSide/dual': 6, + 'cm/positionSide/dual': 6, + 'um/userTrades': 5, + 'cm/userTrades': 20, + 'um/leverageBracket': 0.2, + 'cm/leverageBracket': 0.2, + 'margin/forceOrders': 1, + 'um/forceOrders': {'cost': 20, 'noSymbol': 50}, + 'cm/forceOrders': {'cost': 20, 'noSymbol': 50}, + 'um/apiTradingStatus': {'cost': 0.2, 'noSymbol': 2}, + 'um/commissionRate': 4, + 'cm/commissionRate': 4, + 'margin/marginLoan': 2, + 'margin/repayLoan': 2, + 'margin/marginInterestHistory': 0.2, + 'portfolio/interest-history': 10, + 'um/income': 6, + 'cm/income': 6, + 'um/account': 1, + 'cm/account': 1, + 'repay-futures-switch': 6, + 'um/adlQuantile': 5, + 'cm/adlQuantile': 5, + 'um/trade/asyn': 300, + 'um/trade/asyn/id': 2, + 'um/order/asyn': 300, + 'um/order/asyn/id': 2, + 'um/income/asyn': 300, + 'um/income/asyn/id': 2, + 'um/orderAmendment': 1, + 'cm/orderAmendment': 1, + 'um/feeBurn': 30, + 'um/accountConfig': 1, + 'um/symbolConfig': 1, + 'cm/accountConfig': 1, + 'cm/symbolConfig': 1, + 'rateLimit/order': 1, + }, + 'post': { + 'um/order': 1, + 'um/conditional/order': 1, + 'cm/order': 1, + 'cm/conditional/order': 1, + 'margin/order': 1, + 'marginLoan': 100, + 'repayLoan': 100, + 'margin/order/oco': 1, + 'um/leverage': 0.2, + 'cm/leverage': 0.2, + 'um/positionSide/dual': 0.2, + 'cm/positionSide/dual': 0.2, + 'auto-collection': 150, + 'bnb-transfer': 150, + 'repay-futures-switch': 150, + 'repay-futures-negative-balance': 150, + 'listenKey': 0.2, + 'asset-collection': 6, + 'margin/repay-debt': 3000, + 'um/feeBurn': 1, + }, + 'put': { + 'listenKey': 0.2, + 'um/order': 1, + 'cm/order': 1, + }, + 'delete': { + 'um/order': 1, + 'um/conditional/order': 1, + 'um/allOpenOrders': 1, + 'um/conditional/allOpenOrders': 1, + 'cm/order': 1, + 'cm/conditional/order': 1, + 'cm/allOpenOrders': 1, + 'cm/conditional/allOpenOrders': 1, + 'margin/order': 2, + 'margin/allOpenOrders': 5, + 'margin/orderList': 2, + 'listenKey': 0.2, + }, + }, + 'papiV2': { + 'get': { + 'um/account': 1, + }, + }, + }, + 'fees': { + 'trading': { + 'feeSide': 'get', + 'tierBased': False, + 'percentage': True, + 'taker': self.parse_number('0.001'), + 'maker': self.parse_number('0.001'), + }, + 'linear': { + 'trading': { + 'feeSide': 'quote', + 'tierBased': True, + 'percentage': True, + 'taker': self.parse_number('0.000500'), + 'maker': self.parse_number('0.000200'), + 'tiers': { + 'taker': [ + [self.parse_number('0'), self.parse_number('0.000400')], + [self.parse_number('250'), self.parse_number('0.000400')], + [self.parse_number('2500'), self.parse_number('0.000350')], + [self.parse_number('7500'), self.parse_number('0.000320')], + [self.parse_number('22500'), self.parse_number('0.000300')], + [self.parse_number('50000'), self.parse_number('0.000270')], + [self.parse_number('100000'), self.parse_number('0.000250')], + [self.parse_number('200000'), self.parse_number('0.000220')], + [self.parse_number('400000'), self.parse_number('0.000200')], + [self.parse_number('750000'), self.parse_number('0.000170')], + ], + 'maker': [ + [self.parse_number('0'), self.parse_number('0.000200')], + [self.parse_number('250'), self.parse_number('0.000160')], + [self.parse_number('2500'), self.parse_number('0.000140')], + [self.parse_number('7500'), self.parse_number('0.000120')], + [self.parse_number('22500'), self.parse_number('0.000100')], + [self.parse_number('50000'), self.parse_number('0.000080')], + [self.parse_number('100000'), self.parse_number('0.000060')], + [self.parse_number('200000'), self.parse_number('0.000040')], + [self.parse_number('400000'), self.parse_number('0.000020')], + [self.parse_number('750000'), self.parse_number('0')], + ], + }, + }, + }, + 'inverse': { + 'trading': { + 'feeSide': 'base', + 'tierBased': True, + 'percentage': True, + 'taker': self.parse_number('0.000500'), + 'maker': self.parse_number('0.000100'), + 'tiers': { + 'taker': [ + [self.parse_number('0'), self.parse_number('0.000500')], + [self.parse_number('250'), self.parse_number('0.000450')], + [self.parse_number('2500'), self.parse_number('0.000400')], + [self.parse_number('7500'), self.parse_number('0.000300')], + [self.parse_number('22500'), self.parse_number('0.000250')], + [self.parse_number('50000'), self.parse_number('0.000240')], + [self.parse_number('100000'), self.parse_number('0.000240')], + [self.parse_number('200000'), self.parse_number('0.000240')], + [self.parse_number('400000'), self.parse_number('0.000240')], + [self.parse_number('750000'), self.parse_number('0.000240')], + ], + 'maker': [ + [self.parse_number('0'), self.parse_number('0.000100')], + [self.parse_number('250'), self.parse_number('0.000080')], + [self.parse_number('2500'), self.parse_number('0.000050')], + [self.parse_number('7500'), self.parse_number('0.0000030')], + [self.parse_number('22500'), self.parse_number('0')], + [self.parse_number('50000'), self.parse_number('-0.000050')], + [self.parse_number('100000'), self.parse_number('-0.000060')], + [self.parse_number('200000'), self.parse_number('-0.000070')], + [self.parse_number('400000'), self.parse_number('-0.000080')], + [self.parse_number('750000'), self.parse_number('-0.000090')], + ], + }, + }, + }, + 'option': {}, + }, + 'currencies': { + 'BNFCR': self.safe_currency_structure({'id': 'BNFCR', 'code': 'BNFCR', 'precision': self.parse_number('0.001')}), + }, + 'commonCurrencies': { + 'BCC': 'BCC', # kept for backward-compatibility https://github.com/ccxt/ccxt/issues/4848 + 'YOYO': 'YOYOW', + }, + 'precisionMode': TICK_SIZE, + # exchange-specific options + 'options': { + 'sandboxMode': False, + 'fetchMargins': True, + 'fetchMarkets': { + 'types': [ + 'spot', # allows CORS in browsers + 'linear', # allows CORS in browsers + 'inverse', # allows CORS in browsers + # 'option', # does not allow CORS, enable outside of the browser only + ], + }, + 'loadAllOptions': False, + 'fetchCurrencies': True, # self is a private call and it requires API keys + # 'fetchTradesMethod': 'publicGetAggTrades', # publicGetTrades, publicGetHistoricalTrades, eapiPublicGetTrades + # 'repayCrossMarginMethod': 'papiPostRepayLoan', # papiPostMarginRepayDebt + 'defaultTimeInForce': 'GTC', # 'GTC' = Good To Cancel(default), 'IOC' = Immediate Or Cancel + 'defaultType': 'spot', # 'spot', 'future', 'margin', 'delivery', 'option' + 'defaultSubType': None, # 'linear', 'inverse' + 'hasAlreadyAuthenticatedSuccessfully': False, + 'warnOnFetchOpenOrdersWithoutSymbol': True, + 'currencyToPrecisionRoundingMode': TRUNCATE, + # not an error + # https://github.com/ccxt/ccxt/issues/11268 + # https://github.com/ccxt/ccxt/pull/11624 + # POST https://fapi.binance.com/fapi/v1/marginType 400 Bad Request + # binanceusdm + 'throwMarginModeAlreadySet': False, + 'fetchPositions': 'positionRisk', # or 'account' or 'option' + 'recvWindow': 10 * 1000, # 10 sec + 'timeDifference': 0, # the difference between system clock and Binance clock + 'adjustForTimeDifference': False, # controls the adjustment logic upon instantiation + 'newOrderRespType': { + 'market': 'FULL', # 'ACK' for order id, 'RESULT' for full order or 'FULL' for order with fills + 'limit': 'FULL', # we change it from 'ACK' by default to 'FULL'(returns immediately if limit is not hit) + }, + 'quoteOrderQty': True, # whether market orders support amounts in quote currency + 'broker': { + 'spot': 'x-TKT5PX2F', + 'margin': 'x-TKT5PX2F', + 'future': 'x-cvBPrNm9', + 'delivery': 'x-xcKtGhcu', + 'swap': 'x-cvBPrNm9', + 'option': 'x-xcKtGhcu', + 'inverse': 'x-xcKtGhcu', + }, + 'accountsByType': { + 'main': 'MAIN', + 'spot': 'MAIN', + 'funding': 'FUNDING', + 'margin': 'MARGIN', + 'cross': 'MARGIN', + 'future': 'UMFUTURE', # backwards compatibility + 'delivery': 'CMFUTURE', # backwards compatbility + 'linear': 'UMFUTURE', + 'swap': 'UMFUTURE', + 'inverse': 'CMFUTURE', + 'option': 'OPTION', + }, + 'accountsById': { + 'MAIN': 'spot', + 'FUNDING': 'funding', + 'MARGIN': 'margin', + 'UMFUTURE': 'linear', + 'CMFUTURE': 'inverse', + 'OPTION': 'option', + }, + 'networks': { + 'ERC20': 'ETH', + 'TRC20': 'TRX', + 'BEP2': 'BNB', + 'BEP20': 'BSC', + 'OMNI': 'OMNI', + 'EOS': 'EOS', + 'SPL': 'SOL', # temporarily keep support for SPL(old name) + 'SOL': 'SOL', # we shouldn't rename SOL + }, + 'networksById': { + 'SOL': 'SOL', # temporary fix for SPL definition + }, + 'impliedNetworks': { + 'ETH': {'ERC20': 'ETH'}, + 'TRX': {'TRC20': 'TRX'}, + }, + 'legalMoney': { + 'MXN': True, + 'UGX': True, + 'SEK': True, + 'CHF': True, + 'VND': True, + 'AED': True, + 'DKK': True, + 'KZT': True, + 'HUF': True, + 'PEN': True, + 'PHP': True, + 'USD': True, + 'TRY': True, + 'EUR': True, + 'NGN': True, + 'PLN': True, + 'BRL': True, + 'ZAR': True, + 'KES': True, + 'ARS': True, + 'RUB': True, + 'AUD': True, + 'NOK': True, + 'CZK': True, + 'GBP': True, + 'UAH': True, + 'GHS': True, + 'HKD': True, + 'CAD': True, + 'INR': True, + 'JPY': True, + 'NZD': True, + }, + 'legalMoneyCurrenciesById': { + 'BUSD': 'USD', + }, + 'defaultWithdrawPrecision': 0.00000001, + }, + 'features': { + 'spot': { + 'sandbox': True, + 'fetchCurrencies': { + 'private': True, + }, + 'createOrder': { + 'marginMode': True, + 'triggerPrice': True, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': True, + 'takeProfitPrice': True, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': True, + 'leverage': False, + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': { + 'EXPIRE_MAKER': True, + 'EXPIRE_TAKER': True, + 'EXPIRE_BOTH': True, + 'NONE': True, + }, + 'trailing': False, # todo: self is different from standard trailing https://github.com/binance/binance-spot-api-docs/blob/master/faqs/trailing-stop-faq.md + 'icebergAmount': True, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': None, + 'untilDays': 1, # days between start-end + 'symbolRequired': True, + }, + 'fetchOrder': { + 'marginMode': True, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOpenOrders': { + 'marginMode': True, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': True, + 'limit': 1000, + 'daysBack': None, + 'untilDays': 10000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchClosedOrders': { + 'marginMode': True, + 'limit': 1000, + 'daysBack': None, + 'daysBackCanceled': None, + 'untilDays': 10000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOHLCV': { + 'limit': 1000, + }, + }, + 'forDerivatives': { + 'sandbox': True, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerPriceType': { + 'mark': True, + 'last': True, + 'index': False, + }, + 'stopLossPrice': True, + 'takeProfitPrice': True, + 'attachedStopLossTakeProfit': None, # not supported + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': True, + # 'GTX': True, + }, + 'hedged': True, + # exchange-supported features + 'selfTradePrevention': True, + 'trailing': True, + 'iceberg': False, + 'leverage': False, + 'marketBuyRequiresPrice': False, + 'marketBuyByCost': True, + }, + 'createOrders': { + 'max': 5, + }, + 'fetchMyTrades': { + 'marginMode': False, + 'daysBack': None, + 'limit': 1000, + 'untilDays': 7, + 'symbolRequired': True, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOpenOrders': { + 'marginMode': True, + 'limit': 500, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': True, + 'limit': 1000, + 'daysBack': 90, + 'untilDays': 7, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchClosedOrders': { + 'marginMode': True, + 'limit': 1000, + 'daysBack': 90, + 'daysBackCanceled': 3, + 'untilDays': 7, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOHLCV': { + 'limit': 1500, + }, + }, + 'swap': { + 'linear': { + 'extends': 'forDerivatives', + }, + 'inverse': { + 'extends': 'forDerivatives', + }, + }, + 'future': { + 'linear': { + 'extends': 'forDerivatives', + }, + 'inverse': { + 'extends': 'forDerivatives', + }, + }, + }, + 'exceptions': { + 'spot': { + 'exact': { + # + # 1xxx + # + '-1004': OperationFailed, # {"code":-1004,"msg":"Server is busy, please wait and try again"} + '-1008': OperationFailed, # undocumented, but mentioned: This is sent whenever the servers are overloaded with requests. + '-1099': AuthenticationError, # {"code":-1099,"msg":"Not found, authenticated, or authorized"} + '-1108': BadRequest, # undocumented, but mentioned: This error will occur if a value to a parameter being sent was too large, potentially causing overflow + '-1131': BadRequest, # {"code":-1131,"msg":"recvWindow must be less than 60000"} + '-1134': BadRequest, # strategyType was less than 1000000. + '-1135': BadRequest, # undocumented, but mentioned: This error code will occur if a parameter requiring a JSON object is invalid. + '-1145': BadRequest, # cancelRestrictions has to be either ONLY_NEW or ONLY_PARTIALLY_FILLED. + '-1151': BadSymbol, # Symbol is present multiple times in the list. + # + # 2xxx + # + '-2008': AuthenticationError, # undocumented, Invalid Api-Key ID + '-2016': OperationRejected, # {"code":-2016,"msg":"No trading window could be found for the symbol. Try ticker/24hrs instead."} + '-2021': BadResponse, # This code is sent when either the cancellation of the order failed or the new order placement failed but not both. + '-2022': BadResponse, # This code is sent when both the cancellation of the order failed and the new order placement failed. + '-2026': InvalidOrder, # Order was canceled or expired with no executed qty over 90 days ago and has been archived. + # + # 3xxx(these errors are available only for spot atm) + # + '-3000': OperationFailed, # {"code":-3000,"msg":"Internal server error."} + '-3001': AuthenticationError, # {"code":-3001,"msg":"Please enable 2FA first."} + '-3002': BadSymbol, # {"code":-3002,"msg":"We don't have self asset."} + '-3003': BadRequest, # {"code":-3003,"msg":"Margin account does not exist."} + '-3004': OperationRejected, # {"code":-3004,"msg":"Trade not allowed."} + '-3005': BadRequest, # {"code":-3005,"msg":"Transferring out not allowed. Transfer out amount exceeds max amount."} + '-3006': BadRequest, # {"code":-3006,"msg":"Your borrow amount has exceed maximum borrow amount."} + '-3007': OperationFailed, # {"code":-3007,"msg":"You have pending transaction, please try again later.."} + '-3008': BadRequest, # {"code":-3008,"msg":"Borrow not allowed. Your borrow amount has exceed maximum borrow amount."} + '-3009': OperationRejected, # {"code":-3009,"msg":"This asset are not allowed to transfer into margin account currently."} + '-3010': BadRequest, # {"code":-3010,"msg":"Repay not allowed. Repay amount exceeds borrow amount."} + '-3011': BadRequest, # {"code":-3011,"msg":"Your input date is invalid."} + '-3012': OperationRejected, # {"code":-3012,"msg":"Borrow is banned for self asset."} + '-3013': BadRequest, # {"code":-3013,"msg":"Borrow amount less than minimum borrow amount."} + '-3014': AccountSuspended, # {"code":-3014,"msg":"Borrow is banned for self account."} + '-3015': BadRequest, # {"code":-3015,"msg":"Repay amount exceeds borrow amount."} + '-3016': BadRequest, # {"code":-3016,"msg":"Repay amount less than minimum repay amount."} + '-3017': OperationRejected, # {"code":-3017,"msg":"This asset are not allowed to transfer into margin account currently."} + '-3018': AccountSuspended, # {"code":-3018,"msg":"Transferring in has been banned for self account."} + '-3019': AccountSuspended, # {"code":-3019,"msg":"Transferring out has been banned for self account."} + '-3020': BadRequest, # {"code":-3020,"msg":"Transfer out amount exceeds max amount."} + '-3021': BadRequest, # {"code":-3021,"msg":"Margin account are not allowed to trade self trading pair."} + '-3022': AccountSuspended, # {"code":-3022,"msg":"You account's trading is banned."} + '-3023': OperationRejected, # {"code":-3023,"msg":"You can't transfer out/place order under current margin level."} + '-3024': OperationRejected, # {"code":-3024,"msg":"The unpaid debt is too small after self repayment."} + '-3025': BadRequest, # {"code":-3025,"msg":"Your input date is invalid."} + '-3026': BadRequest, # {"code":-3026,"msg":"Your input param is invalid."} + '-3027': BadSymbol, # {"code":-3027,"msg":"Not a valid margin asset."} + '-3028': BadSymbol, # {"code":-3028,"msg":"Not a valid margin pair."} + '-3029': OperationFailed, # {"code":-3029,"msg":"Transfer failed."} + '-3036': AccountSuspended, # {"code":-3036,"msg":"This account is not allowed to repay."} + '-3037': OperationFailed, # {"code":-3037,"msg":"PNL is clearing. Wait a second."} + '-3038': BadRequest, # {"code":-3038,"msg":"Listen key not found."} + '-3041': InsufficientFunds, # {"code":-3041,"msg":"Balance is not enough"} + '-3042': BadRequest, # {"code":-3042,"msg":"PriceIndex not available for self margin pair."} + '-3043': PermissionDenied, # {"code":-3043,"msg":"Transferring in not allowed."} + '-3044': OperationFailed, # {"code":-3044,"msg":"System busy."} + '-3045': OperationRejected, # {"code":-3045,"msg":"The system doesn't have enough asset now."} + '-3999': PermissionDenied, # {"code":-3999,"msg":"This function is only available for invited users."} + # + # 4xxx(different from contract markets) + # + '-4000': ExchangeError, # override commons + '-4001': BadRequest, # {"code":-4001 ,"msg":"Invalid operation."} + '-4002': BadRequest, # {"code":-4002 ,"msg":"Invalid get."} + '-4003': BadRequest, # {"code":-4003 ,"msg":"Your input email is invalid."} + '-4004': AuthenticationError, # {"code":-4004,"msg":"You don't login or auth."} + '-4005': RateLimitExceeded, # {"code":-4005 ,"msg":"Too many new requests."} + '-4006': BadRequest, # {"code":-4006 ,"msg":"Support main account only."} + '-4007': PermissionDenied, # {"code":-4007 ,"msg":"Address validation is not passed."} + '-4008': PermissionDenied, # {"code":-4008 ,"msg":"Address tag validation is not passed."} + '-4009': ExchangeError, # undocumented + '-4010': PermissionDenied, # {"code":-4010 ,"msg":"White list mail has been confirmed."} # [TODO] possible bug: it should probably be "has not been confirmed" + '-4011': BadRequest, # {"code":-4011 ,"msg":"White list mail is invalid."} + '-4012': PermissionDenied, # {"code":-4012 ,"msg":"White list is not opened."} + '-4013': AuthenticationError, # {"code":-4013 ,"msg":"2FA is not opened."} + '-4014': OperationRejected, # {"code":-4014 ,"msg":"Withdraw is not allowed within 2 min login."} + '-4015': PermissionDenied, # {"code":-4015 ,"msg":"Withdraw is limited."} + '-4016': PermissionDenied, # {"code":-4016 ,"msg":"Within 24 hours after password modification, withdrawal is prohibited."} + '-4017': PermissionDenied, # {"code":-4017 ,"msg":"Within 24 hours after the release of 2FA, withdrawal is prohibited."} + '-4018': BadSymbol, # {"code":-4018,"msg":"We don't have self asset."} + '-4019': BadRequest, # {"code":-4019,"msg":"Current asset is not open for withdrawal."} + '-4020': ExchangeError, # override commons + '-4021': BadRequest, # {"code":-4021,"msg":"Asset withdrawal must be an %s multiple of %s."} + '-4022': BadRequest, # {"code":-4022,"msg":"Not less than the minimum pick-up quantity %s."} + '-4023': OperationRejected, # {"code":-4023,"msg":"Within 24 hours, the withdrawal exceeds the maximum amount."} + '-4024': InsufficientFunds, # {"code":-4024,"msg":"You don't have self asset."} + '-4025': InsufficientFunds, # {"code":-4025,"msg":"The number of hold asset is less than zero."} + '-4026': InsufficientFunds, # {"code":-4026,"msg":"You have insufficient balance."} + '-4027': OperationFailed, # {"code":-4027,"msg":"Failed to obtain tranId."} + '-4028': BadRequest, # {"code":-4028,"msg":"The amount of withdrawal must be greater than the Commission."} + '-4029': BadRequest, # {"code":-4029,"msg":"The withdrawal record does not exist."} + '-4030': BadResponse, # {"code":-4030,"msg":"Confirmation of successful asset withdrawal. [TODO] possible bug in docs"} + '-4031': OperationFailed, # {"code":-4031,"msg":"Cancellation failed."} + '-4032': OperationRejected, # {"code":-4032,"msg":"Withdraw verification exception."} + '-4033': BadRequest, # {"code":-4033,"msg":"Illegal address."} + '-4034': OperationRejected, # {"code":-4034,"msg":"The address is suspected of fake."} + '-4035': PermissionDenied, # {"code":-4035,"msg":"This address is not on the whitelist. Please join and try again."} + '-4036': PermissionDenied, # {"code":-4036,"msg":"The new address needs to be withdrawn in {0} hours."} + '-4037': OperationFailed, # {"code":-4037,"msg":"Re-sending Mail failed."} + '-4038': OperationFailed, # {"code":-4038,"msg":"Please try again in 5 minutes."} + '-4039': PermissionDenied, # {"code":-4039,"msg":"The user does not exist."} + '-4040': OperationRejected, # {"code":-4040,"msg":"This address not charged."} + '-4041': OperationFailed, # {"code":-4041,"msg":"Please try again in one minute."} + '-4042': OperationRejected, # {"code":-4042,"msg":"This asset cannot get deposit address again."} + '-4043': OperationRejected, # {"code":-4043,"msg":"More than 100 recharge addresses were used in 24 hours."} + '-4044': PermissionDenied, # {"code":-4044,"msg":"This is a blacklist country."} + '-4045': OperationFailed, # {"code":-4045,"msg":"Failure to acquire assets."} + '-4046': AuthenticationError, # {"code":-4046,"msg":"Agreement not confirmed."} + '-4047': BadRequest, # {"code":-4047,"msg":"Time interval must be within 0-90 days"} + '-4048': ExchangeError, # override commons + '-4049': ExchangeError, # override commons + '-4050': ExchangeError, # override commons + '-4051': ExchangeError, # override commons + '-4052': ExchangeError, # override commons + '-4053': ExchangeError, # override commons + '-4054': ExchangeError, # override commons + '-4055': ExchangeError, # override commons + '-4056': ExchangeError, # override commons + '-4057': ExchangeError, # override commons + '-4058': ExchangeError, # override commons + '-4059': ExchangeError, # override commons + '-4060': OperationFailed, # As your deposit has not reached the required block confirmations, we have temporarily locked {0} asset + '-4061': ExchangeError, # override commons + '-4062': ExchangeError, # override commons + '-4063': ExchangeError, # override commons + '-4064': ExchangeError, # override commons + '-4065': ExchangeError, # override commons + '-4066': ExchangeError, # override commons + '-4067': ExchangeError, # override commons + '-4068': ExchangeError, # override commons + '-4069': ExchangeError, # override commons + '-4070': ExchangeError, # override commons + '-4071': ExchangeError, # override commons + '-4072': ExchangeError, # override commons + '-4073': ExchangeError, # override commons + '-4074': ExchangeError, # override commons + '-4075': ExchangeError, # override commons + '-4076': ExchangeError, # override commons + '-4077': ExchangeError, # override commons + '-4078': ExchangeError, # override commons + '-4079': ExchangeError, # override commons + '-4080': ExchangeError, # override commons + '-4081': ExchangeError, # override commons + '-4082': ExchangeError, # override commons + '-4083': ExchangeError, # override commons + '-4084': ExchangeError, # override commons + '-4085': ExchangeError, # override commons + '-4086': ExchangeError, # override commons + '-4087': ExchangeError, # override commons + '-4088': ExchangeError, # override commons + '-4089': ExchangeError, # override commons + '-4091': ExchangeError, # override commons + '-4092': ExchangeError, # override commons + '-4093': ExchangeError, # override commons + '-4094': ExchangeError, # override commons + '-4095': ExchangeError, # override commons + '-4096': ExchangeError, # override commons + '-4097': ExchangeError, # override commons + '-4098': ExchangeError, # override commons + '-4099': ExchangeError, # override commons + '-4101': ExchangeError, # override commons + '-4102': ExchangeError, # override commons + '-4103': ExchangeError, # override commons + '-4104': ExchangeError, # override commons + '-4105': ExchangeError, # override commons + '-4106': ExchangeError, # override commons + '-4107': ExchangeError, # override commons + '-4108': ExchangeError, # override commons + '-4109': ExchangeError, # override commons + '-4110': ExchangeError, # override commons + '-4112': ExchangeError, # override commons + '-4113': ExchangeError, # override commons + '-4114': ExchangeError, # override commons + '-4115': ExchangeError, # override commons + '-4116': ExchangeError, # override commons + '-4117': ExchangeError, # override commons + '-4118': ExchangeError, # override commons + '-4119': ExchangeError, # override commons + '-4120': ExchangeError, # override commons + '-4121': ExchangeError, # override commons + '-4122': ExchangeError, # override commons + '-4123': ExchangeError, # override commons + '-4124': ExchangeError, # override commons + '-4125': ExchangeError, # override commons + '-4126': ExchangeError, # override commons + '-4127': ExchangeError, # override commons + '-4128': ExchangeError, # override commons + '-4129': ExchangeError, # override commons + '-4130': ExchangeError, # override commons + '-4131': ExchangeError, # override commons + '-4132': ExchangeError, # override commons + '-4133': ExchangeError, # override commons + '-4134': ExchangeError, # override commons + '-4135': ExchangeError, # override commons + '-4136': ExchangeError, # override commons + '-4137': ExchangeError, # override commons + '-4138': ExchangeError, # override commons + '-4139': ExchangeError, # override commons + '-4141': ExchangeError, # override commons + '-4142': ExchangeError, # override commons + '-4143': ExchangeError, # override commons + '-4144': ExchangeError, # override commons + '-4145': ExchangeError, # override commons + '-4146': ExchangeError, # override commons + '-4147': ExchangeError, # override commons + '-4148': ExchangeError, # override commons + '-4149': ExchangeError, # override commons + '-4150': ExchangeError, # override commons + # + # 5xxx + # + '-5001': BadRequest, # Don't allow transfer to micro assets. + '-5002': InsufficientFunds, # You have insufficient balance. + '-5003': InsufficientFunds, # You don't have self asset. + '-5004': OperationRejected, # The residual balances of %s have exceeded 0.001BTC, Please re-choose. + '-5005': OperationRejected, # The residual balances of %s is too low, Please re-choose. + '-5006': OperationRejected, # Only transfer once in 24 hours. + '-5007': BadRequest, # Quantity must be greater than zero. + '-5008': OperationRejected, # Insufficient amount of returnable assets. + '-5009': BadSymbol, # Product does not exist. + '-5010': OperationFailed, # Asset transfer fail. + '-5011': BadRequest, # future account not exists. + '-5012': OperationFailed, # Asset transfer is in pending. + '-5013': InsufficientFunds, # {"code":-5013,"msg":"Asset transfer failed: insufficient balance""} # undocumented + '-5021': BadRequest, # This parent sub have no relation + '-5022': BadRequest, # future account or sub relation not exists. + # + # 6xxx + # + '-6001': BadSymbol, # Daily product not exists. + '-6003': PermissionDenied, # Product not exist or you don't have permission + '-6004': BadRequest, # Product not in purchase status + '-6005': BadRequest, # Smaller than min purchase limit + '-6006': BadRequest, # Redeem amount error + '-6007': OperationRejected, # Not in redeem time + '-6008': OperationRejected, # Product not in redeem status + '-6009': RateLimitExceeded, # Request frequency too high + '-6011': OperationRejected, # Exceeding the maximum num allowed to purchase per user + '-6012': InsufficientFunds, # Balance not enough + '-6013': BadResponse, # Purchasing failed + '-6014': OperationRejected, # Exceed up-limit allowed to purchased + '-6015': BadRequest, # Empty request body + '-6016': BadRequest, # Parameter err + '-6017': PermissionDenied, # Not in whitelist + '-6018': InsufficientFunds, # Asset not enough + '-6019': OperationRejected, # Need confirm + '-6020': BadRequest, # Project not exists + # + # 7xxx + # + '-7001': BadRequest, # Date range is not supported. + '-7002': BadRequest, # Data request type is not supported. + # + # 1xxxx + # + '-10001': OperationFailed, # The system is under maintenance, please try again later. + '-10002': BadRequest, # Invalid input parameters. + '-10005': BadResponse, # No records found. + '-10007': BadRequest, # This coin is not loanable + '-10008': BadRequest, # This coin is not loanable + '-10009': BadRequest, # This coin can not be used. + '-10010': BadRequest, # This coin can not be used. + '-10011': InsufficientFunds, # Insufficient spot assets. + '-10012': BadRequest, # Invalid repayment amount. + '-10013': InsufficientFunds, # Insufficient collateral amount. + '-10015': OperationFailed, # Collateral deduction failed. + '-10016': OperationFailed, # Failed to provide loan. + '-10017': OperationRejected, # {"code":-10017,"msg":"Repay amount should not be larger than liability."} + '-10018': BadRequest, # Invalid repayment amount. + '-10019': BadRequest, # Configuration does not exists. + '-10020': BadRequest, # User ID does not exist. + '-10021': InvalidOrder, # Order does not exist. + '-10022': BadRequest, # Invalid adjustment amount. + '-10023': OperationFailed, # Failed to adjust LTV. + '-10024': BadRequest, # LTV adjustment not supported. + '-10025': OperationFailed, # Repayment failed. + '-10026': BadRequest, # Invalid parameter. + '-10028': BadRequest, # Invalid parameter. + '-10029': OperationRejected, # Loan amount is too small. + '-10030': OperationRejected, # Loan amount is too much. + '-10031': OperationRejected, # Individual loan quota reached. + '-10032': OperationFailed, # Repayment is temporarily unavailable. + '-10034': OperationRejected, # Repay with collateral is not available currently, please try to repay with borrowed coin. + '-10039': OperationRejected, # Repayment amount is too small. + '-10040': OperationRejected, # Repayment amount is too large. + '-10041': OperationFailed, # Due to high demand, there are currently insufficient loanable assets for {0}. Please adjust your borrow amount or try again tomorrow. + '-10042': BadSymbol, # asset %s is not supported + '-10043': OperationRejected, # {0} borrowing is currently not supported. + '-10044': OperationRejected, # Collateral amount has reached the limit. Please reduce your collateral amount or try with other collaterals. + '-10045': OperationRejected, # The loan coin does not support collateral repayment. Please try again later. + '-10046': OperationRejected, # Collateral Adjustment exceeds the maximum limit. Please try again. + '-10047': PermissionDenied, # This coin is currently not supported in your location due to local regulations. + '-11008': OperationRejected, # undocumented: Exceeding the account’s maximum borrowable limit + '-12014': RateLimitExceeded, # More than 1 request in 2 seconds + # BLVT + '-13000': OperationRejected, # Redeption of the token is forbiden now + '-13001': OperationRejected, # Exceeds individual 24h redemption limit of the token + '-13002': OperationRejected, # Exceeds total 24h redemption limit of the token + '-13003': PermissionDenied, # Subscription of the token is forbiden now + '-13004': OperationRejected, # Exceeds individual 24h subscription limit of the token + '-13005': OperationRejected, # Exceeds total 24h subscription limit of the token + '-13006': OperationRejected, # Subscription amount is too small + '-13007': PermissionDenied, # The Agreement is not signed + # 18xxx - BINANCE CODE + '-18002': OperationRejected, # The total amount of codes you created has exceeded the 24-hour limit, please try again after UTC 0 + '-18003': OperationRejected, # Too many codes created in 24 hours, please try again after UTC 0 + '-18004': OperationRejected, # Too many invalid redeem attempts in 24 hours, please try again after UTC 0 + '-18005': PermissionDenied, # Too many invalid verify attempts, please try later + '-18006': OperationRejected, # The amount is too small, please re-enter + '-18007': OperationRejected, # This token is not currently supported, please re-enter + # + # 2xxxx + # + # 21xxx - PORTFOLIO MARGIN(documented in spot docs) + '-21001': BadRequest, # Request ID is not a Portfolio Margin Account. + '-21002': BadRequest, # Portfolio Margin Account doesn't support transfer from margin to futures. + '-21003': BadResponse, # Fail to retrieve margin assets. + '-21004': OperationRejected, # User doesn’t have portfolio margin bankruptcy loan + '-21005': InsufficientFunds, # User’s spot wallet doesn’t have enough BUSD to repay portfolio margin bankruptcy loan + '-21006': OperationFailed, # User had portfolio margin bankruptcy loan repayment in process + '-21007': OperationFailed, # User failed to repay portfolio margin bankruptcy loan since liquidation was in process + # + # misc + # + '-32603': BadRequest, # undocumented, Filter failure: LOT_SIZE & precision + '400002': BadRequest, # undocumented, {“status”: “FAIL”, “code”: “400002”, “errorMessage”: “Signature for self request is not valid.”} + '100001003': AuthenticationError, # undocumented, {"code":100001003,"msg":"Verification failed"} + '200003903': AuthenticationError, # undocumented, {"code":200003903,"msg":"Your identity verification has been rejected. Please complete identity verification again."} + }, + }, + 'linear': { + 'exact': { + # + # 1xxx + # + '-1005': PermissionDenied, # {"code":-1005,"msg":"No such IP has been white listed"} + '-1008': OperationFailed, # -1008 SERVER_BUSY: Server is currently overloaded with other requests. Please try again in a few minutes. + '-1011': PermissionDenied, # {"code":-1011,"msg":"This IP cannot access self route."} + '-1023': BadRequest, # {"code":-1023,"msg":"Start time is greater than end time."} + '-1099': AuthenticationError, # {"code":-1099,"msg":"Not found, authenticated, or authorized"} + '-1109': PermissionDenied, # {"code":-1109,"msg":"Invalid account."} + '-1110': BadRequest, # {"code":-1110,"msg":"Invalid symbolType."} + '-1113': BadRequest, # {"code":-1113,"msg":"Withdrawal amount must be negative."} + '-1122': BadRequest, # INVALID_SYMBOL_STATUS + '-1126': BadSymbol, # ASSET_NOT_SUPPORTED + '-1136': BadRequest, # {"code":-1136,"msg":"Invalid newOrderRespType"} + # + # 2xxx + # + '-2012': OperationFailed, # CANCEL_ALL_FAIL + '-2016': OperationRejected, # {"code":-2016,"msg":"No trading window could be found for the symbol. Try ticker/24hrs instead."} + '-2017': PermissionDenied, # API Keys are locked on self account. + '-2018': InsufficientFunds, # {"code":-2018,"msg":"Balance is insufficient"} + '-2019': InsufficientFunds, # {"code":-2019,"msg":"Margin is insufficient."} + '-2020': OperationFailed, # {"code":-2020,"msg":"Unable to fill."} + '-2021': OrderImmediatelyFillable, # {"code":-2021,"msg":"Order would immediately trigger."} + '-2022': InvalidOrder, # {"code":-2022,"msg":"ReduceOnly Order is rejected."} + '-2023': OperationFailed, # {"code":-2023,"msg":"User in liquidation mode now."} + '-2024': InsufficientFunds, # {"code":-2024,"msg":"Position is not sufficient."} + '-2025': OperationRejected, # {"code":-2025,"msg":"Reach max open order limit."} + '-2026': InvalidOrder, # {"code":-2026,"msg":"This OrderType is not supported when reduceOnly."} + '-2027': OperationRejected, # {"code":-2027,"msg":"Exceeded the maximum allowable position at current leverage."} + '-2028': OperationRejected, # {"code":-2028,"msg":"Leverage is smaller than permitted: insufficient margin balance"} + # + # 4xxx + # + '-4063': BadRequest, # INVALID_OPTIONS_REQUEST_TYPE + '-4064': BadRequest, # INVALID_OPTIONS_TIME_FRAME + '-4065': BadRequest, # INVALID_OPTIONS_AMOUNT + '-4066': BadRequest, # INVALID_OPTIONS_EVENT_TYPE + '-4069': BadRequest, # Position INVALID_OPTIONS_PREMIUM_FEE + '-4070': BadRequest, # Client options id is not valid. + '-4071': BadRequest, # Invalid options direction + '-4072': OperationRejected, # premium fee is not updated, reject order + '-4073': BadRequest, # OPTIONS_PREMIUM_INPUT_LESS_THAN_ZERO + '-4074': OperationRejected, # Order amount is bigger than upper boundary or less than 0, reject order + '-4075': BadRequest, # output premium fee is less than 0, reject order + '-4076': OperationRejected, # original fee is too much higher than last fee + '-4077': OperationRejected, # place order amount has reached to limit, reject order + '-4078': OperationFailed, # options internal error + '-4079': BadRequest, # invalid options id + '-4080': PermissionDenied, # user not found with id: %s + '-4081': BadRequest, # OPTIONS_NOT_FOUND + '-4085': BadRequest, # Invalid notional limit coefficient + '-4087': PermissionDenied, # User can only place reduce only order + '-4088': PermissionDenied, # User can not place order currently + '-4114': BadRequest, # INVALID_CLIENT_TRAN_ID_LEN + '-4115': BadRequest, # DUPLICATED_CLIENT_TRAN_ID + '-4116': InvalidOrder, # DUPLICATED_CLIENT_ORDER_ID + '-4117': OperationRejected, # STOP_ORDER_TRIGGERING + '-4118': OperationRejected, # REDUCE_ONLY_MARGIN_CHECK_FAILED + '-4131': OperationRejected, # The counterparty's best price does not meet the PERCENT_PRICE filter limit + '-4140': BadRequest, # Invalid symbol status for opening position + '-4141': OperationRejected, # Symbol is closed + '-4144': BadSymbol, # Invalid pair + '-4164': InvalidOrder, # {"code":-4164,"msg":"Order's notional must be no smaller than 20(unless you choose reduce only)."}, + '-4136': InvalidOrder, # {"code":-4136,"msg":"Target strategy invalid for orderType TRAILING_STOP_MARKET,closePosition True"} + '-4165': BadRequest, # Invalid time interval + '-4167': BadRequest, # Unable to adjust to Multi-Assets mode with symbols of USDⓈ-M Futures under isolated-margin mode. + '-4168': BadRequest, # Unable to adjust to isolated-margin mode under the Multi-Assets mode. + '-4169': OperationRejected, # Unable to adjust Multi-Assets Mode with insufficient margin balance in USDⓈ-M Futures + '-4170': OperationRejected, # Unable to adjust Multi-Assets Mode with open orders in USDⓈ-M Futures + '-4171': OperationRejected, # Adjusted asset mode is currently set and does not need to be adjusted repeatedly + '-4172': OperationRejected, # Unable to adjust Multi-Assets Mode with a negative wallet balance of margin available asset in USDⓈ-M Futures account. + '-4183': BadRequest, # Price is higher than stop price multiplier cap. + '-4184': BadRequest, # Price is lower than stop price multiplier floor. + '-4192': PermissionDenied, # Trade forbidden due to Cooling-off Period. + '-4202': PermissionDenied, # Intermediate Personal Verification is required for adjusting leverage over 20x + '-4203': PermissionDenied, # More than 20x leverage is available one month after account registration. + '-4205': PermissionDenied, # More than 20x leverage is available %s days after Futures account registration. + '-4206': PermissionDenied, # hasattr(self, Users) country has limited adjust leverage. + '-4208': OperationRejected, # Current symbol leverage cannot exceed 20 when using position limit adjustment service. + '-4209': OperationRejected, # Leverage adjustment failed. Current symbol max leverage limit is %sx + '-4210': BadRequest, # Stop price is higher than price multiplier cap + '-4211': BadRequest, # Stop price is lower than price multiplier floor + '-4400': PermissionDenied, # Futures Trading Quantitative Rules violated, only reduceOnly order is allowed, please try again later. + '-4401': PermissionDenied, # Compliance restricted account permission: can only place reduceOnly order. + '-4402': PermissionDenied, # Dear user, our Terms of Use and compliance with local regulations, self feature is currently not available in your region. + '-4403': PermissionDenied, # Dear user, our Terms of Use and compliance with local regulations, the leverage can only up to %sx in your region + # + # 5xxx + # + '-5021': OrderNotFillable, # Due to the order could not be filled immediately, the FOK order has been rejected. + '-5022': OrderNotFillable, # Due to the order could not be executed, the Post Only order will be rejected. + '-5024': OperationRejected, # Symbol is not in trading status. Order amendment is not permitted. + '-5025': OperationRejected, # Only limit order is supported. + '-5026': OperationRejected, # Exceed maximum modify order limit. + '-5027': OperationRejected, # No need to modify the order. + '-5028': BadRequest, # Timestamp for self request is outside of the ME recvWindow. + '-5037': BadRequest, # Invalid price match + '-5038': BadRequest, # Price match only supports order type: LIMIT, STOP AND TAKE_PROFIT + '-5039': BadRequest, # Invalid self trade prevention mode + '-5040': BadRequest, # The goodTillDate timestamp must be greater than the current time plus 600 seconds and smaller than 253402300799000 + '-5041': OperationFailed, # No depth matches self BBO order + }, + }, + 'inverse': { + 'exact': { + # + # 1xxx + # + '-1005': PermissionDenied, # {"code":-1005,"msg":"No such IP has been white listed"} + '-1011': PermissionDenied, # {"code":-1011,"msg":"This IP cannot access self route."} + '-1023': BadRequest, # {"code":-1023,"msg":"Start time is greater than end time."} + '-1109': AuthenticationError, # {"code":-1109,"msg":"Invalid account."} + '-1110': BadSymbol, # {"code":-1110,"msg":"Invalid symbolType."} + '-1113': BadRequest, # {"code":-1113,"msg":"Withdrawal amount must be negative."} + '-1128': BadRequest, # {"code":-1128,"msg":"Combination of optional parameters invalid."} + '-1136': BadRequest, # {"code":-1136,"msg":"Invalid newOrderRespType"} + # + # 2xxx + # + '-2016': OperationRejected, # {"code":-2016,"msg":"No trading window could be found for the symbol. Try ticker/24hrs instead."} + '-2018': InsufficientFunds, # {"code":-2018,"msg":"Balance is insufficient"} + '-2019': InsufficientFunds, # {"code":-2019,"msg":"Margin is insufficient."} + '-2020': OperationFailed, # {"code":-2020,"msg":"Unable to fill."} + '-2021': OrderImmediatelyFillable, # {"code":-2021,"msg":"Order would immediately trigger."} + '-2022': InvalidOrder, # {"code":-2022,"msg":"ReduceOnly Order is rejected."} + '-2023': OperationFailed, # {"code":-2023,"msg":"User in liquidation mode now."} + '-2024': BadRequest, # {"code":-2024,"msg":"Position is not sufficient."} + '-2025': OperationRejected, # {"code":-2025,"msg":"Reach max open order limit."} + '-2026': InvalidOrder, # {"code":-2026,"msg":"This OrderType is not supported when reduceOnly."} + '-2027': OperationRejected, # {"code":-2027,"msg":"Exceeded the maximum allowable position at current leverage."} + '-2028': OperationRejected, # {"code":-2028,"msg":"Leverage is smaller than permitted: insufficient margin balance"} + # + # 4xxx + # + '-4086': BadRequest, # Invalid price spread threshold. + '-4087': BadSymbol, # Invalid pair + '-4088': BadRequest, # Invalid time interval + '-4089': PermissionDenied, # User can only place reduce only order. + '-4090': PermissionDenied, # User can not place order currently. + '-4110': BadRequest, # clientTranId is not valid + '-4111': BadRequest, # clientTranId is duplicated. + '-4112': OperationRejected, # ReduceOnly Order Failed. Please check your existing position and open orders. + '-4113': OperationRejected, # The counterparty's best price does not meet the PERCENT_PRICE filter limit. + '-4150': OperationRejected, # Leverage reduction is not supported in Isolated Margin Mode with open positions. + '-4151': BadRequest, # Price is higher than stop price multiplier cap. + '-4152': BadRequest, # Price is lower than stop price multiplier floor. + '-4154': BadRequest, # Stop price is higher than price multiplier cap. + '-4155': BadRequest, # Stop price is lower than price multiplier floor + '-4178': BadRequest, # Order's notional must be no smaller than one(unless you choose reduce only) + '-4188': BadRequest, # Timestamp for self request is outside of the ME recvWindow. + '-4192': PermissionDenied, # Trade forbidden due to Cooling-off Period. + '-4194': PermissionDenied, # Intermediate Personal Verification is required for adjusting leverage over 20x. + '-4195': PermissionDenied, # More than 20x leverage is available one month after account registration. + '-4196': BadRequest, # Only limit order is supported. + '-4197': OperationRejected, # No need to modify the order. + '-4198': OperationRejected, # Exceed maximum modify order limit. + '-4199': BadRequest, # Symbol is not in trading status. Order amendment is not permitted. + '-4200': PermissionDenied, # More than 20x leverage is available %s days after Futures account registration. + '-4201': PermissionDenied, # Users in your location/country can only access a maximum leverage of %s + '-4202': OperationRejected, # Current symbol leverage cannot exceed 20 when using position limit adjustment service. + }, + }, + 'option': { + 'exact': { + # + # 1xxx + # + '-1003': ExchangeError, # override common + '-1004': ExchangeError, # override common + '-1006': ExchangeError, # override common + '-1007': ExchangeError, # override common + '-1008': RateLimitExceeded, # TOO_MANY_REQUEST + '-1010': ExchangeError, # override common + '-1013': ExchangeError, # override common + '-1108': ExchangeError, # override common + '-1112': ExchangeError, # override common + '-1114': ExchangeError, # override common + '-1128': BadSymbol, # BAD_CONTRACT + '-1129': BadSymbol, # BAD_CURRENCY + '-1131': BadRequest, # {"code":-1131,"msg":"recvWindow must be less than 60000"} + # + # 2xxx + # + '-2011': ExchangeError, # override common + '-2018': InsufficientFunds, # BALANCE_NOT_SUFFICIENT + '-2027': InsufficientFunds, # OPTION_MARGIN_NOT_SUFFICIENT + # + # 3xxx + # + '-3029': OperationFailed, # {"code":-3029,"msg":"Transfer failed."} + # + # 4xxx + # + # -4001 inherited + # -4002 inherited + # -4003 inherited + # -4004 inherited + # -4005 inherited + '-4006': ExchangeError, # override commons + '-4007': ExchangeError, # override commons + '-4008': ExchangeError, # override commons + '-4009': ExchangeError, # override commons + '-4010': ExchangeError, # override commons + '-4011': ExchangeError, # override commons + '-4012': ExchangeError, # override commons + # -4013 inherited + '-4014': ExchangeError, # override commons + '-4015': ExchangeError, # override commons + '-4016': ExchangeError, # override commons + '-4017': ExchangeError, # override commons + '-4018': ExchangeError, # override commons + '-4019': ExchangeError, # override commons + '-4020': ExchangeError, # override commons + '-4021': ExchangeError, # override commons + '-4022': ExchangeError, # override commons + '-4023': ExchangeError, # override commons + '-4024': ExchangeError, # override commons + '-4025': ExchangeError, # override commons + '-4026': ExchangeError, # override commons + '-4027': ExchangeError, # override commons + '-4028': ExchangeError, # override commons + # -4029 inherited + # -4030 inherited + '-4031': ExchangeError, # override commons + '-4032': ExchangeError, # override commons + '-4033': ExchangeError, # override commons + '-4034': ExchangeError, # override commons + '-4035': ExchangeError, # override commons + '-4036': ExchangeError, # override commons + '-4037': ExchangeError, # override commons + '-4038': ExchangeError, # override commons + '-4039': ExchangeError, # override commons + '-4040': ExchangeError, # override commons + '-4041': ExchangeError, # override commons + '-4042': ExchangeError, # override commons + '-4043': ExchangeError, # override commons + '-4044': ExchangeError, # override commons + '-4045': ExchangeError, # override commons + '-4046': ExchangeError, # override commons + '-4047': ExchangeError, # override commons + '-4048': ExchangeError, # override commons + '-4049': ExchangeError, # override commons + '-4050': ExchangeError, # override commons + '-4051': ExchangeError, # override commons + '-4052': ExchangeError, # override commons + '-4053': ExchangeError, # override commons + '-4054': ExchangeError, # override commons + # -4055 inherited + '-4056': ExchangeError, # override commons + '-4057': ExchangeError, # override commons + '-4058': ExchangeError, # override commons + '-4059': ExchangeError, # override commons + '-4060': ExchangeError, # override commons + '-4061': ExchangeError, # override commons + '-4062': ExchangeError, # override commons + '-4063': ExchangeError, # override commons + '-4064': ExchangeError, # override commons + '-4065': ExchangeError, # override commons + '-4066': ExchangeError, # override commons + '-4067': ExchangeError, # override commons + '-4068': ExchangeError, # override commons + '-4069': ExchangeError, # override commons + '-4070': ExchangeError, # override commons + '-4071': ExchangeError, # override commons + '-4072': ExchangeError, # override commons + '-4073': ExchangeError, # override commons + '-4074': ExchangeError, # override commons + '-4075': ExchangeError, # override commons + '-4076': ExchangeError, # override commons + '-4077': ExchangeError, # override commons + '-4078': ExchangeError, # override commons + '-4079': ExchangeError, # override commons + '-4080': ExchangeError, # override commons + '-4081': ExchangeError, # override commons + '-4082': ExchangeError, # override commons + '-4083': ExchangeError, # override commons + '-4084': ExchangeError, # override commons + '-4085': ExchangeError, # override commons + '-4086': ExchangeError, # override commons + '-4087': ExchangeError, # override commons + '-4088': ExchangeError, # override commons + '-4089': ExchangeError, # override commons + '-4091': ExchangeError, # override commons + '-4092': ExchangeError, # override commons + '-4093': ExchangeError, # override commons + '-4094': ExchangeError, # override commons + '-4095': ExchangeError, # override commons + '-4096': ExchangeError, # override commons + '-4097': ExchangeError, # override commons + '-4098': ExchangeError, # override commons + '-4099': ExchangeError, # override commons + '-4101': ExchangeError, # override commons + '-4102': ExchangeError, # override commons + '-4103': ExchangeError, # override commons + '-4104': ExchangeError, # override commons + '-4105': ExchangeError, # override commons + '-4106': ExchangeError, # override commons + '-4107': ExchangeError, # override commons + '-4108': ExchangeError, # override commons + '-4109': ExchangeError, # override commons + '-4110': ExchangeError, # override commons + '-4112': ExchangeError, # override commons + '-4113': ExchangeError, # override commons + '-4114': ExchangeError, # override commons + '-4115': ExchangeError, # override commons + '-4116': ExchangeError, # override commons + '-4117': ExchangeError, # override commons + '-4118': ExchangeError, # override commons + '-4119': ExchangeError, # override commons + '-4120': ExchangeError, # override commons + '-4121': ExchangeError, # override commons + '-4122': ExchangeError, # override commons + '-4123': ExchangeError, # override commons + '-4124': ExchangeError, # override commons + '-4125': ExchangeError, # override commons + '-4126': ExchangeError, # override commons + '-4127': ExchangeError, # override commons + '-4128': ExchangeError, # override commons + '-4129': ExchangeError, # override commons + '-4130': ExchangeError, # override commons + '-4131': ExchangeError, # override commons + '-4132': ExchangeError, # override commons + '-4133': ExchangeError, # override commons + '-4134': ExchangeError, # override commons + '-4135': ExchangeError, # override commons + '-4136': ExchangeError, # override commons + '-4137': ExchangeError, # override commons + '-4138': ExchangeError, # override commons + '-4139': ExchangeError, # override commons + '-4141': ExchangeError, # override commons + '-4142': ExchangeError, # override commons + '-4143': ExchangeError, # override commons + '-4144': ExchangeError, # override commons + '-4145': ExchangeError, # override commons + '-4146': ExchangeError, # override commons + '-4147': ExchangeError, # override commons + '-4148': ExchangeError, # override commons + '-4149': ExchangeError, # override commons + '-4150': ExchangeError, # override commons + # + # 2xxxx + # + '-20121': ExchangeError, # override commons + '-20124': ExchangeError, # override commons + '-20130': ExchangeError, # override commons + '-20132': ExchangeError, # override commons + '-20194': ExchangeError, # override commons + '-20195': ExchangeError, # override commons + '-20196': ExchangeError, # override commons + '-20198': ExchangeError, # override commons + '-20204': ExchangeError, # override commons + }, + }, + 'portfolioMargin': { + 'exact': { + # + # 10xx General Server or Network Issues + # + '-1000': OperationFailed, # An unknown error occured while processing the request. + '-1001': ExchangeError, # Internal error; unable to process your request. Please try again. + '-1002': PermissionDenied, # You are not authorized to execute self request. + '-1003': RateLimitExceeded, # Too many requests use the websocket for live updates to avoid polling the API. + '-1004': BadRequest, # This IP is already on the white list. + '-1005': PermissionDenied, # No such IP has been white listed. + '-1006': BadResponse, # An unexpected response was received from the message bus. Execution status unknown. + '-1007': BadResponse, # Timeout waiting for response from backend server. Send status unknown, execution status unknown. + '-1008': OperationFailed, # WS Spot server is currently overloaded with other requests. Please try again in a few minutes. + '-1010': ExchangeError, # ERROR_MSG_RECEIVED + '-1011': PermissionDenied, # This IP cannot access self route. + '-1013': ExchangeError, # INVALID_MESSAGE. + '-1014': InvalidOrder, # Unsupported order combination. + '-1015': InvalidOrder, # Too many new orders. + '-1016': NotSupported, # This service is no longer available. + '-1020': NotSupported, # This operation is not supported. + '-1021': BadRequest, # Timestamp for self request is outside of the recvWindow 1000ms ahead of the servers time. + '-1022': BadRequest, # Signature for self request is not valid. + '-1023': BadRequest, # Start time is greater than end time + '-1099': OperationFailed, # WS not found authenticated or authorized + # + # 11xx Request Issues + # + '-1100': BadRequest, # Illegal characters found in a parameter. + '-1101': BadRequest, # Too many parameters sent for self endpoint. + '-1102': BadRequest, # A mandatory parameter was not sent, was empty/null, or malformed. + '-1103': BadRequest, # An unknown parameter was sent. + '-1104': BadRequest, # Not all sent parameters were read. + '-1105': BadRequest, # A parameter was empty. + '-1106': BadRequest, # A parameter was sent when not required. + '-1108': BadRequest, # Invalid asset. + '-1109': BadRequest, # Invalid account. + '-1110': BadSymbol, # Invalid symbolType. + '-1111': BadRequest, # Precision is over the maximum defined for self asset. + '-1112': BadRequest, # No orders on book for symbol. + '-1113': BadRequest, # Withdrawal amount must be negative. + '-1114': BadRequest, # TimeInForce parameter sent when not required. + '-1115': BadRequest, # Invalid timeInForce. + '-1116': BadRequest, # Invalid orderType. + '-1117': BadRequest, # Invalid side. + '-1118': BadRequest, # New client order ID was empty. + '-1119': BadRequest, # Original client order ID was empty. + '-1120': BadRequest, # Invalid interval. + '-1121': BadSymbol, # Invalid symbol. + '-1125': BadRequest, # This listenKey does not exist. + '-1127': BadRequest, # Lookup interval is too big. + '-1128': BadRequest, # Combination of optional parameters invalid. + '-1130': BadRequest, # Invalid data sent for a parameter. + '-1131': BadRequest, # WS recvWindow must be less than 60000 + '-1134': BadRequest, # WS strategyType was less than 1000000. + '-1136': BadRequest, # Invalid newOrderRespType. + '-1145': BadRequest, # WS cancelRestrictions has to be either ONLY_NEW or ONLY_PARTIALLY_FILLED. + '-1151': BadRequest, # WS Symbol is present multiple times in the list. + # + # 20xx Processing Issues + # + '-2010': InvalidOrder, # NEW_ORDER_REJECTED + '-2011': OperationRejected, # CANCEL_REJECTED + '-2013': OrderNotFound, # Order does not exist. + '-2014': OperationRejected, # API-key format invalid. + '-2015': OperationRejected, # Invalid API-key, IP, or permissions for action. + '-2016': OperationFailed, # No trading window could be found for the symbol. Try ticker/24hrs instead. + '-2018': OperationFailed, # Balance is insufficient. + '-2019': OperationFailed, # Margin is insufficient. + '-2020': OrderNotFillable, # Unable to fill. + '-2021': OrderImmediatelyFillable, # Order would immediately trigger. + '-2022': InvalidOrder, # ReduceOnly Order is rejected. + '-2023': OperationFailed, # User in liquidation mode now. + '-2024': OperationRejected, # Position is not sufficient. + '-2025': OperationRejected, # Reach max open order limit. + '-2026': InvalidOrder, # This OrderType is not supported when reduceOnly. + '-2027': OperationRejected, # Exceeded the maximum allowable position at current leverage. + '-2028': OperationRejected, # Leverage is smaller than permitted: insufficient margin balance. + # + # 4xxx Filters and other issues + # + '-4000': BadRequest, # Invalid order status. + '-4001': BadRequest, # Price less than 0. + '-4002': BadRequest, # Price greater than max price. + '-4003': BadRequest, # Quantity less than zero. + '-4004': BadRequest, # Quantity less than min quantity. + '-4005': BadRequest, # Quantity greater than max quantity. + '-4006': BadRequest, # Stop price less than zero. + '-4007': BadRequest, # Stop price greater than max price. + '-4008': BadRequest, # Tick size less than zero. + '-4009': BadRequest, # Max price less than min price. + '-4010': BadRequest, # Max qty less than min qty. + '-4011': BadRequest, # Step size less than zero. + '-4012': BadRequest, # Max mum orders less than zero. + '-4013': BadRequest, # Price less than min price. + '-4014': BadRequest, # Price not increased by tick size. + '-4015': BadRequest, # Client order id is not valid. + '-4016': BadRequest, # Price is higher than mark price multiplier cap. + '-4017': BadRequest, # Multiplier up less than zero. + '-4018': BadRequest, # Multiplier down less than zero. + '-4019': BadRequest, # Composite scale too large. + '-4020': BadRequest, # Target strategy invalid for orderType '%s',reduceOnly '%b'. + '-4021': BadRequest, # Invalid depth limit. + '-4022': BadRequest, # market status sent is not valid. + '-4023': BadRequest, # Qty not increased by step size. + '-4024': BadRequest, # Price is lower than mark price multiplier floor. + '-4025': BadRequest, # Multiplier decimal less than zero. + '-4026': BadRequest, # Commission invalid. + '-4027': BadRequest, # Invalid account type. + '-4028': BadRequest, # Invalid leverage + '-4029': BadRequest, # Tick size precision is invalid. + '-4030': BadRequest, # Step size precision is invalid. + '-4031': BadRequest, # Invalid parameter working type + '-4032': BadRequest, # Exceed maximum cancel order size. + '-4033': BadRequest, # Insurance account not found. + '-4044': BadRequest, # Balance Type is invalid. + '-4045': BadRequest, # Reach max stop order limit. + '-4046': BadRequest, # No need to change margin type. + '-4047': BadRequest, # Margin type cannot be changed if there exists open orders. + '-4048': BadRequest, # Margin type cannot be changed if there exists position. + '-4049': BadRequest, # Add margin only support for isolated position. + '-4050': BadRequest, # Cross balance insufficient. + '-4051': BadRequest, # Isolated balance insufficient. + '-4052': BadRequest, # No need to change auto add margin. + '-4053': BadRequest, # Auto add margin only support for isolated position. + '-4054': BadRequest, # Cannot add position margin: position is 0. + '-4055': BadRequest, # Amount must be positive. + '-4056': PermissionDenied, # Invalid api key type. + '-4057': PermissionDenied, # Invalid api public key + '-4058': BadRequest, # maxPrice and priceDecimal too large,please check. + '-4059': BadRequest, # No need to change position side. + '-4060': BadRequest, # Invalid position side. + '-4061': InvalidOrder, # Order's position side does not match user's setting. + '-4062': BadRequest, # Invalid or improper reduceOnly value. + '-4063': BadRequest, # Invalid options request type + '-4064': BadRequest, # Invalid options time frame + '-4065': BadRequest, # Invalid options amount + '-4066': BadRequest, # Invalid options event type + '-4067': BadRequest, # Position side cannot be changed if there exists open orders. + '-4068': BadRequest, # Position side cannot be changed if there exists position. + '-4069': BadRequest, # Invalid options premium fee + '-4070': BadRequest, # Client options id is not valid. + '-4071': BadRequest, # Invalid options direction + '-4072': OperationRejected, # premium fee is not updated, reject order + '-4073': BadRequest, # input premium fee is less than 0, reject order + '-4074': BadRequest, # Order amount is bigger than upper boundary or less than 0, reject order + '-4075': BadRequest, # output premium fee is less than 0, reject order + '-4076': OperationRejected, # original fee is too much higher than last fee + '-4077': OperationRejected, # place order amount has reached to limit, reject order + '-4078': OperationFailed, # options internal error + '-4079': BadRequest, # invalid options id + '-4080': PermissionDenied, # user not found + '-4081': BadRequest, # options not found + '-4082': BadRequest, # Invalid number of batch place orders. + '-4083': BadRequest, # Fail to place batch orders. + '-4084': NotSupported, # Method is not allowed currently. Upcoming soon. + '-4085': BadRequest, # Invalid notional limit coefficient + '-4086': BadRequest, # Invalid price spread threshold + '-4087': PermissionDenied, # User can only place reduce only order + '-4088': PermissionDenied, # User can not place order currently + '-4104': BadRequest, # Invalid contract type + '-4114': BadRequest, # clientTranId is not valid + '-4115': BadRequest, # clientTranId is duplicated + '-4118': OperationRejected, # ReduceOnly Order Failed. Please check your existing position and open orders + '-4131': OperationRejected, # The counterparty's best price does not meet the PERCENT_PRICE filter limit + '-4135': BadRequest, # Invalid activation price + '-4137': BadRequest, # Quantity must be zero with closePosition equals True + '-4138': BadRequest, # Reduce only must be True with closePosition equals True + '-4139': BadRequest, # Order type can not be market if it's unable to cancel + '-4140': OrderImmediatelyFillable, # Invalid symbol status for opening position + '-4141': BadRequest, # Symbol is closed + '-4142': OrderImmediatelyFillable, # REJECT: take profit or stop order will be triggered immediately + '-4144': BadSymbol, # Invalid pair + '-4161': OperationRejected, # Leverage reduction is not supported in Isolated Margin Mode with open positions + '-4164': InvalidOrder, # Order's notional must be no smaller than 5.0(unless you choose reduce only) + '-4165': BadRequest, # Invalid time interval + '-4183': InvalidOrder, # Price is higher than stop price multiplier cap. + '-4184': InvalidOrder, # Price is lower than stop price multiplier floor. + '-4408': InvalidOrder, # This symbol is in reduce only mode due to regulation requirements. Please upgrade to Binance Credits Trading Mode. + # + # 5xxx Order Execution Issues + # + '-5021': OrderNotFillable, # Due to the order could not be filled immediately, the FOK order has been rejected. + '-5022': OrderNotFillable, # Due to the order could not be executed, the Post Only order will be rejected. + '-5028': OperationFailed, # The requested timestamp is outside the recvWindow of the matching engine + '-5041': RateLimitExceeded, # Time out for too many requests from self account queueing at the same time. + }, + }, + 'exact': { + # error codes to cover ALL market types(however, specific market type might have override) + # + # 1xxx + # + '-1000': OperationFailed, # {"code":-1000,"msg":"An unknown error occured while processing the request."} + '-1001': OperationFailed, # {"code":-1001,"msg":"'Internal error; unable to process your request. Please try again.'"} + '-1002': AuthenticationError, # {"code":-1002,"msg":"'You are not authorized to execute self request.'"} + '-1003': RateLimitExceeded, # {"code":-1003,"msg":"Too much request weight used, current limit is 1200 request weight per 1 MINUTE. Please use the websocket for live updates to avoid polling the API."} + '-1004': OperationRejected, # DUPLICATE_IP : This IP is already on the white list + '-1006': OperationFailed, # {"code":-1006,"msg":"An unexpected response was received from the message bus. Execution status unknown."} + '-1007': RequestTimeout, # {"code":-1007,"msg":"Timeout waiting for response from backend server. Send status unknown; execution status unknown."} + '-1010': OperationFailed, # {"code":-1010,"msg":"ERROR_MSG_RECEIVED."} + '-1013': BadRequest, # INVALID_MESSAGE + '-1014': InvalidOrder, # {"code":-1014,"msg":"Unsupported order combination."} + '-1015': RateLimitExceeded, # {"code":-1015,"msg":"'Too many new orders; current limit is %s orders per %s.'"} + '-1016': BadRequest, # {"code":-1016,"msg":"'This service is no longer available.',"} + '-1020': BadRequest, # {"code":-1020,"msg":"'This operation is not supported.'"} + '-1021': InvalidNonce, # {"code":-1021,"msg":"'your time is ahead of server'"} + '-1022': AuthenticationError, # {"code":-1022,"msg":"Signature for self request is not valid."} + '-1100': BadRequest, # {"code":-1100,"msg":"createOrder(symbol, 1, asdf) -> 'Illegal characters found in parameter 'price'"} + '-1101': BadRequest, # {"code":-1101,"msg":"Too many parameters; expected %s and received %s."} + '-1102': BadRequest, # {"code":-1102,"msg":"Param %s or %s must be sent, but both were empty"} + '-1103': BadRequest, # {"code":-1103,"msg":"An unknown parameter was sent."} + '-1104': BadRequest, # {"code":-1104,"msg":"Not all sent parameters were read, read 8 parameters but was sent 9"} + '-1105': BadRequest, # {"code":-1105,"msg":"Parameter %s was empty."} + '-1106': BadRequest, # {"code":-1106,"msg":"Parameter %s sent when not required."} + '-1108': BadSymbol, # {"code":-1108,"msg":"Invalid asset."} + '-1111': BadRequest, # {"code":-1111,"msg":"Precision is over the maximum defined for self asset."} + '-1112': OperationFailed, # {"code":-1112,"msg":"No orders on book for symbol."} + '-1114': BadRequest, # {"code":-1114,"msg":"TimeInForce parameter sent when not required."} + '-1115': BadRequest, # {"code":-1115,"msg":"Invalid timeInForce."} + '-1116': BadRequest, # {"code":-1116,"msg":"Invalid orderType."} + '-1117': BadRequest, # {"code":-1117,"msg":"Invalid side."} + '-1118': BadRequest, # {"code":-1118,"msg":"New client order ID was empty."} + '-1119': BadRequest, # {"code":-1119,"msg":"Original client order ID was empty."} + '-1120': BadRequest, # {"code":-1120,"msg":"Invalid interval."} + '-1121': BadSymbol, # {"code":-1121,"msg":"Invalid symbol."} + '-1125': AuthenticationError, # {"code":-1125,"msg":"This listenKey does not exist."} + '-1127': BadRequest, # {"code":-1127,"msg":"More than %s hours between startTime and endTime."} + '-1128': BadRequest, # {"code":-1128,"msg":"Combination of optional parameters invalid."} + '-1130': BadRequest, # {"code":-1130,"msg":"Data sent for paramter %s is not valid."} + # + # 2xxx + # + '-2010': InvalidOrder, # NEW_ORDER_REJECTED + '-2011': OrderNotFound, # {"code":-2011,"msg":"cancelOrder(1, 'BTC/USDT') -> 'UNKNOWN_ORDER'"} + '-2013': OrderNotFound, # {"code":-2013,"msg":"fetchOrder(1, 'BTC/USDT') -> 'Order does not exist'"} + '-2014': AuthenticationError, # {"code":-2014,"msg":"API-key format invalid."} + '-2015': AuthenticationError, # {"code":-2015,"msg":"Invalid API-key, IP, or permissions for action."} + # + # 4xxx(common for linear, inverse, pm) + # + '-4000': InvalidOrder, # INVALID_ORDER_STATUS + '-4001': BadRequest, # PRICE_LESS_THAN_ZERO + '-4002': BadRequest, # PRICE_GREATER_THAN_MAX_PRICE + '-4003': BadRequest, # QTY_LESS_THAN_ZERO + '-4004': BadRequest, # QTY_LESS_THAN_MIN_QTY + '-4005': BadRequest, # QTY_GREATER_THAN_MAX_QTY + '-4006': BadRequest, # STOP_PRICE_LESS_THAN_ZERO + '-4007': BadRequest, # STOP_PRICE_GREATER_THAN_MAX_PRICE + '-4008': BadRequest, # TICK SIZE LESS THAN ZERO + '-4009': BadRequest, # MAX_PRICE_LESS_THAN_MIN_PRICE + '-4010': BadRequest, # MAX_QTY_LESS_THAN_MIN_QTY + '-4011': BadRequest, # STEP_SIZE_LESS_THAN_ZERO + '-4012': BadRequest, # MAX_NUM_ORDERS_LESS_THAN_ZERO + '-4013': BadRequest, # PRICE_LESS_THAN_MIN_PRICE + '-4014': BadRequest, # PRICE NOT INCREASED BY TICK SIZE + '-4015': BadRequest, # Client order id is not valid + '-4016': BadRequest, # Price is higher than mark price multiplier cap. + '-4017': BadRequest, # MULTIPLIER_UP_LESS_THAN_ZERO + '-4018': BadRequest, # MULTIPLIER_DOWN_LESS_THAN_ZERO + '-4019': OperationRejected, # COMPOSITE_SCALE_OVERFLOW + '-4020': BadRequest, # TARGET_STRATEGY_INVALID + '-4021': BadRequest, # INVALID_DEPTH_LIMIT + '-4022': BadRequest, # WRONG_MARKET_STATUS + '-4023': BadRequest, # QTY_NOT_INCREASED_BY_STEP_SIZE + '-4024': BadRequest, # PRICE_LOWER_THAN_MULTIPLIER_DOWN + '-4025': BadRequest, # MULTIPLIER_DECIMAL_LESS_THAN_ZERO + '-4026': BadRequest, # COMMISSION_INVALID + '-4027': BadRequest, # INVALID_ACCOUNT_TYPE + '-4028': BadRequest, # INVALID_LEVERAGE + '-4029': BadRequest, # INVALID_TICK SIZE_PRECISION + '-4030': BadRequest, # INVALID_STEP_SIZE_PRECISION + '-4031': BadRequest, # INVALID_WORKING_TYPE + '-4032': OperationRejected, # EXCEED_MAX_CANCEL_ORDER_SIZE(or Invalid parameter working type: %s) + '-4033': BadRequest, # INSURANCE_ACCOUNT_NOT_FOUND + '-4044': BadRequest, # INVALID_BALANCE_TYPE + '-4045': OperationRejected, # MAX_STOP_ORDER_EXCEEDED + '-4046': OperationRejected, # NO_NEED_TO_CHANGE_MARGIN_TYPE + '-4047': OperationRejected, # Margin type cannot be changed if there exists open orders. + '-4048': OperationRejected, # Margin type cannot be changed if there exists position. + '-4049': BadRequest, # Add margin only support for isolated position. + '-4050': InsufficientFunds, # Cross balance insufficient + '-4051': InsufficientFunds, # Isolated balance insufficient. + '-4052': OperationRejected, # No need to change auto add margin. + '-4053': BadRequest, # Auto add margin only support for isolated position. + '-4054': OperationRejected, # Cannot add position margin: position is 0. + '-4055': BadRequest, # Amount must be positive. + '-4056': AuthenticationError, # INVALID_API_KEY_TYPE + '-4057': AuthenticationError, # INVALID_RSA_PUBLIC_KEY: Invalid api public key + '-4058': BadRequest, # MAX_PRICE_TOO_LARGE + '-4059': OperationRejected, # NO_NEED_TO_CHANGE_POSITION_SIDE + '-4060': BadRequest, # INVALID_POSITION_SIDE + '-4061': OperationRejected, # POSITION_SIDE_NOT_MATCH: Order's position side does not match user's setting. + '-4062': BadRequest, # REDUCE_ONLY_CONFLICT: Invalid or improper reduceOnly value. + '-4067': OperationRejected, # Position side cannot be changed if there exists open orders. + '-4068': OperationRejected, # Position side cannot be changed if there exists position. + '-4082': BadRequest, # Invalid number of batch place orders. + '-4083': OperationRejected, # PLACE_BATCH_ORDERS_FAIL : Fail to place batch orders. + '-4084': BadRequest, # UPCOMING_METHOD : Method is not allowed currently. Upcoming soon. + '-4086': BadRequest, # Invalid price spread threshold. + '-4104': BadRequest, # INVALID_CONTRACT_TYPE + '-4135': BadRequest, # Invalid activation price + '-4137': BadRequest, # Quantity must be zero with closePosition equals True + '-4138': BadRequest, # Reduce only must be True with closePosition equals True + '-4139': BadRequest, # Order type can not be market if it's unable to cancel + '-4142': OrderImmediatelyFillable, # REJECT: take profit or stop order will be triggered immediately + # + # 2xxxx + # + # 20xxx - spot & futures algo(TBD for OPTIONS & PORTFOLIO MARGIN) + '-20121': BadSymbol, # Invalid symbol. + '-20124': BadRequest, # Invalid algo id or it has been completed. + '-20130': BadRequest, # Invalid data sent for a parameter + '-20132': BadRequest, # The client algo id is duplicated + '-20194': BadRequest, # Duration is too short to execute all required quantity. + '-20195': BadRequest, # The total size is too small. + '-20196': BadRequest, # The total size is too large. + '-20198': OperationRejected, # Reach the max open orders allowed. + '-20204': BadRequest, # The notional of USD is less or more than the limit. + # + # strings + # + 'System is under maintenance.': OnMaintenance, # {"code":1,"msg":"System is under maintenance."} + 'System abnormality': OperationFailed, # {"code":-1000,"msg":"System abnormality"} + 'You are not authorized to execute self request.': PermissionDenied, # {"msg":"You are not authorized to execute self request."} + 'API key does not exist': AuthenticationError, + 'Order would trigger immediately.': OrderImmediatelyFillable, + 'Stop price would trigger immediately.': OrderImmediatelyFillable, # {"code":-2010,"msg":"Stop price would trigger immediately."} + 'Order would immediately match and take.': OrderImmediatelyFillable, # {"code":-2010,"msg":"Order would immediately match and take."} + 'Account has insufficient balance for requested action.': InsufficientFunds, + 'Rest API trading is not enabled.': PermissionDenied, + 'This account may not place or cancel orders.': PermissionDenied, + "You don't have permission.": PermissionDenied, # {"msg":"You don't have permission.","success":false} + 'Market is closed.': MarketClosed, # {"code":-1013,"msg":"Market is closed."} + 'Too many requests. Please try again later.': RateLimitExceeded, # {"msg":"Too many requests. Please try again later.","success":false} + 'This action is disabled on self account.': AccountSuspended, # {"code":-2011,"msg":"This action is disabled on self account."} + 'Limit orders require GTC for self phase.': BadRequest, + 'This order type is not hasattr(self, possible) trading phase.': BadRequest, + 'This type of sub-account exceeds the maximum number limit': OperationRejected, # {"code":-9000,"msg":"This type of sub-account exceeds the maximum number limit"} + 'This symbol is restricted for self account.': PermissionDenied, + 'This symbol is not permitted for self account.': PermissionDenied, # {"code":-2010,"msg":"This symbol is not permitted for self account."} + }, + 'broad': { + 'has no operation privilege': PermissionDenied, + 'MAX_POSITION': BadRequest, # {"code":-2010,"msg":"Filter failure: MAX_POSITION"} + }, + }, + }) + + def is_inverse(self, type: str, subType: Str = None) -> bool: + if subType is None: + return(type == 'delivery') + else: + return subType == 'inverse' + + def is_linear(self, type: str, subType: Str = None) -> bool: + if subType is None: + return(type == 'future') or (type == 'swap') + else: + return subType == 'linear' + + def set_sandbox_mode(self, enable: bool): + super(binance, self).set_sandbox_mode(enable) + self.options['sandboxMode'] = enable + + def create_expired_option_market(self, symbol: str): + # support expired option contracts + settle = 'USDT' + optionParts = symbol.split('-') + symbolBase = symbol.split('/') + base = None + if symbol.find('/') > -1: + base = self.safe_string(symbolBase, 0) + else: + base = self.safe_string(optionParts, 0) + expiry = self.safe_string(optionParts, 1) + strike = self.safe_integer(optionParts, 2) + strikeAsString = self.safe_string(optionParts, 2) + optionType = self.safe_string(optionParts, 3) + datetime = self.convert_expire_date(expiry) + timestamp = self.parse8601(datetime) + return { + 'id': base + '-' + expiry + '-' + strikeAsString + '-' + optionType, + 'symbol': base + '/' + settle + ':' + settle + '-' + expiry + '-' + strikeAsString + '-' + optionType, + 'base': base, + 'quote': settle, + 'baseId': base, + 'quoteId': settle, + 'active': None, + 'type': 'option', + 'linear': None, + 'inverse': None, + 'spot': False, + 'swap': False, + 'future': False, + 'option': True, + 'margin': False, + 'contract': True, + 'contractSize': None, + 'expiry': timestamp, + 'expiryDatetime': datetime, + 'optionType': 'call' if (optionType == 'C') else 'put', + 'strike': strike, + 'settle': settle, + 'settleId': settle, + 'precision': { + 'amount': None, + 'price': None, + }, + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'info': None, + } + + def market(self, symbol: str) -> MarketInterface: + if self.markets is None: + raise ExchangeError(self.id + ' markets not loaded') + # defaultType has legacy support on binance + defaultType = self.safe_string(self.options, 'defaultType') + defaultSubType = self.safe_string(self.options, 'defaultSubType') + isLegacyLinear = defaultType == 'future' + isLegacyInverse = defaultType == 'delivery' + isLegacy = isLegacyLinear or isLegacyInverse + if isinstance(symbol, str): + if symbol in self.markets: + market = self.markets[symbol] + # begin diff + if isLegacy and market['spot']: + settle = market['quote'] if isLegacyLinear else market['base'] + futuresSymbol = symbol + ':' + settle + if futuresSymbol in self.markets: + return self.markets[futuresSymbol] + else: + return market + # end diff + elif symbol in self.markets_by_id: + markets = self.markets_by_id[symbol] + # begin diff + if isLegacyLinear: + defaultType = 'linear' + elif isLegacyInverse: + defaultType = 'inverse' + elif defaultType is None: + defaultType = defaultSubType + # end diff + for i in range(0, len(markets)): + market = markets[i] + if market[defaultType]: + return market + return markets[0] + elif (symbol.find('/') > -1) and (symbol.find(':') < 0): + if (defaultType is not None) and (defaultType != 'spot'): + # support legacy symbols + base, quote = symbol.split('/') + settle = base if (quote == 'USD') else quote + futuresSymbol = symbol + ':' + settle + if futuresSymbol in self.markets: + return self.markets[futuresSymbol] + elif (symbol.find('-C') > -1) or (symbol.find('-P') > -1): # both exchange-id and unified symbols are supported self way regardless of the defaultType + return self.create_expired_option_market(symbol) + raise BadSymbol(self.id + ' does not have market symbol ' + symbol) + + def safe_market(self, marketId: Str = None, market: Market = None, delimiter: Str = None, marketType: Str = None) -> MarketInterface: + isOption = (marketId is not None) and ((marketId.find('-C') > -1) or (marketId.find('-P') > -1)) + if isOption and not (marketId in self.markets_by_id): + # handle expired option contracts + return self.create_expired_option_market(marketId) + return super(binance, self).safe_market(marketId, market, delimiter, marketType) + + def cost_to_precision(self, symbol, cost): + return self.decimal_to_precision(cost, TRUNCATE, self.markets[symbol]['precision']['quote'], self.precisionMode, self.paddingMode) + + def nonce(self): + return self.milliseconds() - self.options['timeDifference'] + + def enable_demo_trading(self, enable: bool): + """ + enables or disables demo trading mode + + https://www.binance.com/en/support/faq/detail/9be58f73e5e14338809e3b705b9687dd + https://demo.binance.com/en/my/settings/api-management + + :param boolean [enable]: True if demo trading should be enabled, False otherwise + """ + if self.isSandboxModeEnabled: + raise NotSupported(self.id + ' demo trading is not supported in the sandbox environment. Please check https://www.binance.com/en/support/faq/detail/9be58f73e5e14338809e3b705b9687dd to see the differences') + if enable: + self.urls['apiBackupDemoTrading'] = self.urls['api'] + self.urls['api'] = self.urls['demo'] + elif 'apiBackupDemoTrading' in self.urls: + self.urls['api'] = self.urls['apiBackupDemoTrading'] + newUrls = self.omit(self.urls, 'apiBackupDemoTrading') + self.urls = newUrls + self.options['enableDemoTrading'] = enable + + def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://developers.binance.com/docs/binance-spot-api-docs/rest-api/general-endpoints#check-server-time # spot + https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/Check-Server-Time # swap + https://developers.binance.com/docs/derivatives/coin-margined-futures/market-data/rest-api/Check-Server-time # future + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.subType]: "linear" or "inverse" + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + defaultType = self.safe_string_2(self.options, 'fetchTime', 'defaultType', 'spot') + type = self.safe_string(params, 'type', defaultType) + query = self.omit(params, 'type') + subType = None + subType, params = self.handle_sub_type_and_params('fetchTime', None, params) + response = None + if self.is_linear(type, subType): + response = self.fapiPublicGetTime(query) + elif self.is_inverse(type, subType): + response = self.dapiPublicGetTime(query) + else: + response = self.publicGetTime(query) + return self.safe_integer(response, 'serverTime') + + def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://developers.binance.com/docs/wallet/capital/all-coins-info + https://developers.binance.com/docs/margin_trading/market-data/Get-All-Margin-Assets + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + fetchCurrenciesEnabled = self.safe_bool(self.options, 'fetchCurrencies') + if not fetchCurrenciesEnabled: + return {} + # self endpoint requires authentication + # while fetchCurrencies is a public API method by design + # therefore we check the keys here + # and fallback to generating the currencies from the markets + if not self.check_required_credentials(False): + return {} + # sandbox/testnet does not support sapi endpoints + apiBackup = self.safe_value(self.urls, 'apiBackup') + if apiBackup is not None: + return {} + # demotrading does not support sapi endpoints + if self.safe_bool(self.options, 'enableDemoTrading', False): + return {} + promises = [self.sapiGetCapitalConfigGetall(params)] + fetchMargins = self.safe_bool(self.options, 'fetchMargins', False) + if fetchMargins: + promises.append(self.sapiGetMarginAllPairs(params)) + results = promises + responseCurrencies = results[0] + marginablesById = None + if fetchMargins: + responseMarginables = results[1] + marginablesById = self.index_by(responseMarginables, 'assetName') + result: dict = {} + for i in range(0, len(responseCurrencies)): + # + # { + # "coin": "LINK", + # "depositAllEnable": True, + # "withdrawAllEnable": True, + # "name": "ChainLink", + # "free": "0", + # "locked": "0", + # "freeze": "0", + # "withdrawing": "0", + # "ipoing": "0", + # "ipoable": "0", + # "storage": "0", + # "isLegalMoney": False, + # "trading": True, + # "networkList": [ + # { + # "network": "BSC", + # "coin": "LINK", + # "withdrawIntegerMultiple": "0.00000001", + # "isDefault": False, + # "depositEnable": True, + # "withdrawEnable": True, + # "depositDesc": "", + # "withdrawDesc": "", + # "specialTips": "", + # "specialWithdrawTips": "The network you have selected is BSC. Please ensure that the withdrawal address supports the Binance Smart Chain network. You will lose your assets if the chosen platform does not support retrievals.", + # "name": "BNB Smart Chain(BEP20)", + # "resetAddressStatus": False, + # "addressRegex": "^(0x)[0-9A-Fa-f]{40}$", + # "addressRule": "", + # "memoRegex": "", + # "withdrawFee": "0.012", + # "withdrawMin": "0.024", + # "withdrawMax": "9999999999.99999999", + # "minConfirm": "15", + # "unLockConfirm": "0", + # "sameAddress": False, + # "estimatedArrivalTime": "5", + # "busy": False, + # "country": "AE,BINANCE_BAHRAIN_BSC" + # }, + # { + # "network": "BNB", + # "coin": "LINK", + # "withdrawIntegerMultiple": "0.00000001", + # "isDefault": False, + # "depositEnable": True, + # "withdrawEnable": True, + # "depositDesc": "", + # "withdrawDesc": "", + # "specialTips": "Both a MEMO and an Address are required to successfully deposit your LINK BEP2 tokens to Binance.", + # "specialWithdrawTips": "", + # "name": "BNB Beacon Chain(BEP2)", + # "resetAddressStatus": False, + # "addressRegex": "^(bnb1)[0-9a-z]{38}$", + # "addressRule": "", + # "memoRegex": "^[0-9A-Za-z\\-_]{1,120}$", + # "withdrawFee": "0.003", + # "withdrawMin": "0.01", + # "withdrawMax": "10000000000", + # "minConfirm": "1", + # "unLockConfirm": "0", + # "sameAddress": True, + # "estimatedArrivalTime": "5", + # "busy": False, + # "country": "AE,BINANCE_BAHRAIN_BSC" + # }, + # { + # "network": "ETH", + # "coin": "LINK", + # "withdrawIntegerMultiple": "0.00000001", + # "isDefault": True, + # "depositEnable": True, + # "withdrawEnable": True, + # "depositDesc": "", + # "withdrawDesc": "", + # "name": "Ethereum(ERC20)", + # "resetAddressStatus": False, + # "addressRegex": "^(0x)[0-9A-Fa-f]{40}$", + # "addressRule": "", + # "memoRegex": "", + # "withdrawFee": "0.55", + # "withdrawMin": "1.1", + # "withdrawMax": "10000000000", + # "minConfirm": "12", + # "unLockConfirm": "0", + # "sameAddress": False, + # "estimatedArrivalTime": "5", + # "busy": False, + # "country": "AE,BINANCE_BAHRAIN_BSC" + # } + # ] + # } + # + entry = responseCurrencies[i] + id = self.safe_string(entry, 'coin') + name = self.safe_string(entry, 'name') + code = self.safe_currency_code(id) + isFiat = self.safe_bool(entry, 'isLegalMoney') + minPrecision = None + isWithdrawEnabled = True + isDepositEnabled = True + networkList = self.safe_list(entry, 'networkList', []) + fees: dict = {} + fee = None + networks: dict = {} + for j in range(0, len(networkList)): + networkItem = networkList[j] + network = self.safe_string(networkItem, 'network') + networkCode = self.network_id_to_code(network) + isETF = (network == 'ETF') # e.g. BTCUP, ETHDOWN + # name = self.safe_string(networkItem, 'name') + withdrawFee = self.safe_number(networkItem, 'withdrawFee') + depositEnable = self.safe_bool(networkItem, 'depositEnable') + withdrawEnable = self.safe_bool(networkItem, 'withdrawEnable') + isDepositEnabled = isDepositEnabled or depositEnable + isWithdrawEnabled = isWithdrawEnabled or withdrawEnable + fees[network] = withdrawFee + isDefault = self.safe_bool(networkItem, 'isDefault') + if isDefault or (fee is None): + fee = withdrawFee + # todo: default networks in "setMarkets" overload + # if isDefault: + # self.options['defaultNetworkCodesForCurrencies'][code] = networkCode + # } + precisionTick = self.safe_string(networkItem, 'withdrawIntegerMultiple') + withdrawPrecision = precisionTick + # avoid zero values, which are mostly from fiat or leveraged tokens or some abandoned coins : https://github.com/ccxt/ccxt/pull/14902#issuecomment-1271636731 + if not Precise.string_eq(precisionTick, '0'): + minPrecision = precisionTick if (minPrecision is None) else Precise.string_min(minPrecision, precisionTick) + else: + if not isFiat and not isETF: + # non-fiat and non-ETF currency, there are many cases when precision is set to zero(probably bug, we've reported to binance already) + # in such cases, we can set default precision of 8(which is in UI for such coins) + withdrawPrecision = self.omit_zero(self.safe_string(networkItem, 'withdrawInternalMin')) + if withdrawPrecision is None: + withdrawPrecision = self.safe_string(self.options, 'defaultWithdrawPrecision') + networks[networkCode] = { + 'info': networkItem, + 'id': network, + 'network': networkCode, + 'active': depositEnable and withdrawEnable, + 'deposit': depositEnable, + 'withdraw': withdrawEnable, + 'fee': withdrawFee, + 'precision': self.parse_number(withdrawPrecision), + 'limits': { + 'withdraw': { + 'min': self.safe_number(networkItem, 'withdrawMin'), + 'max': self.safe_number(networkItem, 'withdrawMax'), + }, + 'deposit': { + 'min': self.safe_number(networkItem, 'depositDust'), + 'max': None, + }, + }, + } + trading = self.safe_bool(entry, 'trading') + active = (isWithdrawEnabled and isDepositEnabled and trading) + marginEntry = self.safe_dict(marginablesById, id, {}) + # + # { + # assetName: "BTC", + # assetFullName: "Bitcoin", + # isBorrowable: True, + # isMortgageable: True, + # userMinBorrow: "0", + # userMinRepay: "0", + # } + # + result[code] = { + 'id': id, + 'name': name, + 'code': code, + 'type': 'fiat' if isFiat else 'crypto', + 'precision': self.parse_number(minPrecision), + 'info': entry, + 'active': active, + 'deposit': isDepositEnabled, + 'withdraw': isWithdrawEnabled, + 'networks': networks, + 'fee': fee, + 'fees': fees, + 'limits': self.limits, + 'margin': self.safe_bool(marginEntry, 'isBorrowable'), + } + return result + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for binance + + https://developers.binance.com/docs/binance-spot-api-docs/rest-api/general-endpoints#exchange-information # spot + https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/Exchange-Information # swap + https://developers.binance.com/docs/derivatives/coin-margined-futures/market-data/rest-api/Exchange-Information # future + https://developers.binance.com/docs/derivatives/option/market-data/Exchange-Information # option + https://developers.binance.com/docs/margin_trading/market-data/Get-All-Cross-Margin-Pairs # cross margin + https://developers.binance.com/docs/margin_trading/market-data/Get-All-Isolated-Margin-Symbol # isolated margin + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + promisesRaw = [] + rawFetchMarkets = None + defaultTypes = ['spot', 'linear', 'inverse'] + fetchMarketsOptions = self.safe_dict(self.options, 'fetchMarkets') + if fetchMarketsOptions is not None: + rawFetchMarkets = self.safe_list(fetchMarketsOptions, 'types', defaultTypes) + else: + # for backward-compatibility + rawFetchMarkets = self.safe_list(self.options, 'fetchMarkets', defaultTypes) + # handle loadAllOptions option + loadAllOptions = self.safe_bool(self.options, 'loadAllOptions', False) + if loadAllOptions: + if not self.in_array('option', rawFetchMarkets): + rawFetchMarkets.append('option') + sandboxMode = self.safe_bool(self.options, 'sandboxMode', False) + demoMode = self.safe_bool(self.options, 'enableDemoTrading', False) + isDemoEnv = demoMode or sandboxMode + fetchMarkets = [] + for i in range(0, len(rawFetchMarkets)): + type = rawFetchMarkets[i] + if type == 'option' and isDemoEnv: + continue + fetchMarkets.append(type) + fetchMargins = self.safe_bool(self.options, 'fetchMargins', False) + for i in range(0, len(fetchMarkets)): + marketType = fetchMarkets[i] + if marketType == 'spot': + promisesRaw.append(self.publicGetExchangeInfo(params)) + if fetchMargins and self.check_required_credentials(False) and not isDemoEnv: + promisesRaw.append(self.sapiGetMarginAllPairs(params)) + promisesRaw.append(self.sapiGetMarginIsolatedAllPairs(params)) + elif marketType == 'linear': + promisesRaw.append(self.fapiPublicGetExchangeInfo(params)) + elif marketType == 'inverse': + promisesRaw.append(self.dapiPublicGetExchangeInfo(params)) + elif marketType == 'option': + promisesRaw.append(self.eapiPublicGetExchangeInfo(params)) + else: + raise ExchangeError(self.id + ' fetchMarkets() self.options fetchMarkets "' + marketType + '" is not a supported market type') + results = promisesRaw + markets = [] + self.options['crossMarginPairsData'] = [] + self.options['isolatedMarginPairsData'] = [] + for i in range(0, len(results)): + res = self.safe_value(results, i) + if fetchMargins and isinstance(res, list): + keysList = list(self.index_by(res, 'symbol').keys()) + length = len(self.options['crossMarginPairsData']) + # first one is the cross-margin promise + if length == 0: + self.options['crossMarginPairsData'] = keysList + else: + self.options['isolatedMarginPairsData'] = keysList + else: + resultMarkets = self.safe_list_2(res, 'symbols', 'optionSymbols', []) + markets = self.array_concat(markets, resultMarkets) + # + # spot / margin + # + # { + # "timezone":"UTC", + # "serverTime":1575416692969, + # "rateLimits":[ + # {"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":1200}, + # {"rateLimitType":"ORDERS","interval":"SECOND","intervalNum":10,"limit":100}, + # {"rateLimitType":"ORDERS","interval":"DAY","intervalNum":1,"limit":200000} + # ], + # "exchangeFilters":[], + # "symbols":[ + # { + # "symbol":"ETHBTC", + # "status":"TRADING", + # "baseAsset":"ETH", + # "baseAssetPrecision":8, + # "quoteAsset":"BTC", + # "quotePrecision":8, + # "baseCommissionPrecision":8, + # "quoteCommissionPrecision":8, + # "orderTypes":["LIMIT","LIMIT_MAKER","MARKET","STOP_LOSS_LIMIT","TAKE_PROFIT_LIMIT"], + # "icebergAllowed":true, + # "ocoAllowed":true, + # "quoteOrderQtyMarketAllowed":true, + # "allowTrailingStop":false, + # "isSpotTradingAllowed":true, + # "isMarginTradingAllowed":true, + # "filters":[ + # {"filterType":"PRICE_FILTER","minPrice":"0.00000100","maxPrice":"100000.00000000","tickSize":"0.00000100"}, + # {"filterType":"PERCENT_PRICE","multiplierUp":"5","multiplierDown":"0.2","avgPriceMins":5}, + # {"filterType":"LOT_SIZE","minQty":"0.00100000","maxQty":"100000.00000000","stepSize":"0.00100000"}, + # {"filterType":"MIN_NOTIONAL","minNotional":"0.00010000","applyToMarket":true,"avgPriceMins":5}, + # {"filterType":"ICEBERG_PARTS","limit":10}, + # {"filterType":"MARKET_LOT_SIZE","minQty":"0.00000000","maxQty":"63100.00000000","stepSize":"0.00000000"}, + # {"filterType":"MAX_NUM_ORDERS","maxNumOrders":200}, + # {"filterType":"MAX_NUM_ALGO_ORDERS","maxNumAlgoOrders":5} + # ], + # "permissions":["SPOT","MARGIN"]} + # }, + # ], + # } + # + # cross & isolated pairs response: + # + # [ + # { + # symbol: "BTCUSDT", + # base: "BTC", + # quote: "USDT", + # isMarginTrade: True, + # isBuyAllowed: True, + # isSellAllowed: True, + # id: "376870555451677893", # doesn't exist in isolated + # }, + # ] + # + # futures/usdt-margined(fapi) + # + # { + # "timezone":"UTC", + # "serverTime":1575417244353, + # "rateLimits":[ + # {"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":1200}, + # {"rateLimitType":"ORDERS","interval":"MINUTE","intervalNum":1,"limit":1200} + # ], + # "exchangeFilters":[], + # "symbols":[ + # { + # "symbol":"BTCUSDT", + # "status":"TRADING", + # "maintMarginPercent":"2.5000", + # "requiredMarginPercent":"5.0000", + # "baseAsset":"BTC", + # "quoteAsset":"USDT", + # "pricePrecision":2, + # "quantityPrecision":3, + # "baseAssetPrecision":8, + # "quotePrecision":8, + # "filters":[ + # {"minPrice":"0.01","maxPrice":"100000","filterType":"PRICE_FILTER","tickSize":"0.01"}, + # {"stepSize":"0.001","filterType":"LOT_SIZE","maxQty":"1000","minQty":"0.001"}, + # {"stepSize":"0.001","filterType":"MARKET_LOT_SIZE","maxQty":"1000","minQty":"0.001"}, + # {"limit":200,"filterType":"MAX_NUM_ORDERS"}, + # {"multiplierDown":"0.8500","multiplierUp":"1.1500","multiplierDecimal":"4","filterType":"PERCENT_PRICE"} + # ], + # "orderTypes":["LIMIT","MARKET","STOP"], + # "timeInForce":["GTC","IOC","FOK","GTX"] + # } + # ] + # } + # + # delivery/coin-margined(dapi) + # + # { + # "timezone": "UTC", + # "serverTime": 1597667052958, + # "rateLimits": [ + # {"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":6000}, + # {"rateLimitType":"ORDERS","interval":"MINUTE","intervalNum":1,"limit":6000} + # ], + # "exchangeFilters": [], + # "symbols": [ + # { + # "symbol": "BTCUSD_200925", + # "pair": "BTCUSD", + # "contractType": "CURRENT_QUARTER", + # "deliveryDate": 1601020800000, + # "onboardDate": 1590739200000, + # "contractStatus": "TRADING", + # "contractSize": 100, + # "marginAsset": "BTC", + # "maintMarginPercent": "2.5000", + # "requiredMarginPercent": "5.0000", + # "baseAsset": "BTC", + # "quoteAsset": "USD", + # "pricePrecision": 1, + # "quantityPrecision": 0, + # "baseAssetPrecision": 8, + # "quotePrecision": 8, + # "equalQtyPrecision": 4, + # "filters": [ + # {"minPrice":"0.1","maxPrice":"100000","filterType":"PRICE_FILTER","tickSize":"0.1"}, + # {"stepSize":"1","filterType":"LOT_SIZE","maxQty":"100000","minQty":"1"}, + # {"stepSize":"0","filterType":"MARKET_LOT_SIZE","maxQty":"100000","minQty":"1"}, + # {"limit":200,"filterType":"MAX_NUM_ORDERS"}, + # {"multiplierDown":"0.9500","multiplierUp":"1.0500","multiplierDecimal":"4","filterType":"PERCENT_PRICE"} + # ], + # "orderTypes": ["LIMIT","MARKET","STOP","STOP_MARKET","TAKE_PROFIT","TAKE_PROFIT_MARKET","TRAILING_STOP_MARKET"], + # "timeInForce": ["GTC","IOC","FOK","GTX"] + # }, + # { + # "symbol": "BTCUSD_PERP", + # "pair": "BTCUSD", + # "contractType": "PERPETUAL", + # "deliveryDate": 4133404800000, + # "onboardDate": 1596006000000, + # "contractStatus": "TRADING", + # "contractSize": 100, + # "marginAsset": "BTC", + # "maintMarginPercent": "2.5000", + # "requiredMarginPercent": "5.0000", + # "baseAsset": "BTC", + # "quoteAsset": "USD", + # "pricePrecision": 1, + # "quantityPrecision": 0, + # "baseAssetPrecision": 8, + # "quotePrecision": 8, + # "equalQtyPrecision": 4, + # "filters": [ + # {"minPrice":"0.1","maxPrice":"100000","filterType":"PRICE_FILTER","tickSize":"0.1"}, + # {"stepSize":"1","filterType":"LOT_SIZE","maxQty":"100000","minQty":"1"}, + # {"stepSize":"1","filterType":"MARKET_LOT_SIZE","maxQty":"100000","minQty":"1"}, + # {"limit":200,"filterType":"MAX_NUM_ORDERS"}, + # {"multiplierDown":"0.8500","multiplierUp":"1.1500","multiplierDecimal":"4","filterType":"PERCENT_PRICE"} + # ], + # "orderTypes": ["LIMIT","MARKET","STOP","STOP_MARKET","TAKE_PROFIT","TAKE_PROFIT_MARKET","TRAILING_STOP_MARKET"], + # "timeInForce": ["GTC","IOC","FOK","GTX"] + # } + # ] + # } + # + # options(eapi) + # + # { + # "timezone": "UTC", + # "serverTime": 1675912490405, + # "optionContracts": [ + # { + # "id": 1, + # "baseAsset": "SOL", + # "quoteAsset": "USDT", + # "underlying": "SOLUSDT", + # "settleAsset": "USDT" + # }, + # ... + # ], + # "optionAssets": [ + # {"id":1,"name":"USDT"} + # ], + # "optionSymbols": [ + # { + # "contractId": 3, + # "expiryDate": 1677225600000, + # "filters": [ + # {"filterType":"PRICE_FILTER","minPrice":"724.6","maxPrice":"919.2","tickSize":"0.1"}, + # {"filterType":"LOT_SIZE","minQty":"0.01","maxQty":"1001","stepSize":"0.01"} + # ], + # "id": 2474, + # "symbol": "ETH-230224-800-C", + # "side": "CALL", + # "strikePrice": "800.00000000", + # "underlying": "ETHUSDT", + # "unit": 1, + # "makerFeeRate": "0.00020000", + # "takerFeeRate": "0.00020000", + # "minQty": "0.01", + # "maxQty": "1000", + # "initialMargin": "0.15000000", + # "maintenanceMargin": "0.07500000", + # "minInitialMargin": "0.10000000", + # "minMaintenanceMargin": "0.05000000", + # "priceScale": 1, + # "quantityScale": 2, + # "quoteAsset": "USDT" + # }, + # ... + # ], + # "rateLimits": [ + # {"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":400}, + # {"rateLimitType":"ORDERS","interval":"MINUTE","intervalNum":1,"limit":100}, + # {"rateLimitType":"ORDERS","interval":"SECOND","intervalNum":10,"limit":30} + # ] + # } + # + if self.options['adjustForTimeDifference']: + self.load_time_difference() + result = [] + for i in range(0, len(markets)): + result.append(self.parse_market(markets[i])) + return result + + def parse_market(self, market: dict) -> Market: + swap = False + future = False + option = False + underlying = self.safe_string(market, 'underlying') + id = self.safe_string(market, 'symbol') + optionParts = id.split('-') + optionBase = self.safe_string(optionParts, 0) + lowercaseId = self.safe_string_lower(market, 'symbol') + baseId = self.safe_string(market, 'baseAsset', optionBase) + quoteId = self.safe_string(market, 'quoteAsset') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + contractType = self.safe_string(market, 'contractType') + contract = ('contractType' in market) + expiry = self.safe_integer_2(market, 'deliveryDate', 'expiryDate') + settleId = self.safe_string(market, 'marginAsset') + if (contractType == 'PERPETUAL') or (expiry == 4133404800000): # some swap markets do not have contract type, eg: BTCST + expiry = None + swap = True + elif underlying is not None: + contract = True + option = True + settleId = 'USDT' if (settleId is None) else settleId + elif expiry is not None: + future = True + settle = self.safe_currency_code(settleId) + spot = not contract + filters = self.safe_list(market, 'filters', []) + filtersByType = self.index_by(filters, 'filterType') + status = self.safe_string_2(market, 'status', 'contractStatus') + contractSize = None + fees = self.fees + linear = None + inverse = None + symbol = base + '/' + quote + strike = None + if contract: + if swap: + symbol = symbol + ':' + settle + elif future: + symbol = symbol + ':' + settle + '-' + self.yymmdd(expiry) + elif option: + strike = self.number_to_string(self.parse_to_numeric(self.safe_string(market, 'strikePrice'))) + symbol = symbol + ':' + settle + '-' + self.yymmdd(expiry) + '-' + strike + '-' + self.safe_string(optionParts, 3) + contractSize = self.safe_number_2(market, 'contractSize', 'unit', self.parse_number('1')) + linear = settle == quote + inverse = settle == base + feesType = 'linear' if linear else 'inverse' + fees = self.safe_dict(self.fees, feesType, {}) + active = (status == 'TRADING') + if spot: + permissions = self.safe_list(market, 'permissions', []) + for j in range(0, len(permissions)): + if permissions[j] == 'TRD_GRP_003': + active = False + break + isMarginTradingAllowed = self.safe_bool(market, 'isMarginTradingAllowed', False) + marginModes = None + if spot: + hasCrossMargin = self.in_array(id, self.options['crossMarginPairsData']) + hasIsolatedMargin = self.in_array(id, self.options['isolatedMarginPairsData']) + marginModes = { + 'cross': hasCrossMargin, + 'isolated': hasIsolatedMargin, + } + elif linear or inverse: + marginModes = { + 'cross': True, + 'isolated': True, + } + unifiedType = None + if spot: + unifiedType = 'spot' + elif swap: + unifiedType = 'swap' + elif future: + unifiedType = 'future' + elif option: + unifiedType = 'option' + active = None + parsedStrike = None + if strike is not None: + parsedStrike = self.parse_to_numeric(strike) + entry = { + 'id': id, + 'lowercaseId': lowercaseId, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': unifiedType, + 'spot': spot, + 'margin': spot and isMarginTradingAllowed, + 'marginModes': marginModes, + 'swap': swap, + 'future': future, + 'option': option, + 'active': active, + 'contract': contract, + 'linear': linear, + 'inverse': inverse, + 'taker': fees['trading']['taker'], + 'maker': fees['trading']['maker'], + 'contractSize': contractSize, + 'expiry': expiry, + 'expiryDatetime': self.iso8601(expiry), + 'strike': parsedStrike, + 'optionType': self.safe_string_lower(market, 'side'), + 'precision': { + 'amount': self.parse_number(self.parse_precision(self.safe_string_2(market, 'quantityPrecision', 'quantityScale'))), + 'price': self.parse_number(self.parse_precision(self.safe_string_2(market, 'pricePrecision', 'priceScale'))), + 'base': self.parse_number(self.parse_precision(self.safe_string(market, 'baseAssetPrecision'))), + 'quote': self.parse_number(self.parse_precision(self.safe_string(market, 'quotePrecision'))), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(market, 'minQty'), + 'max': self.safe_number(market, 'maxQty'), + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'info': market, + 'created': self.safe_integer(market, 'onboardDate'), # present in inverse & linear apis + } + if 'PRICE_FILTER' in filtersByType: + filter = self.safe_dict(filtersByType, 'PRICE_FILTER', {}) + # PRICE_FILTER reports zero values for maxPrice + # since they updated filter types in November 2018 + # https://github.com/ccxt/ccxt/issues/4286 + # therefore limits['price']['max'] doesn't have any meaningful value except None + entry['limits']['price'] = { + 'min': self.safe_number(filter, 'minPrice'), + 'max': self.safe_number(filter, 'maxPrice'), + } + entry['precision']['price'] = self.safe_number(filter, 'tickSize') + if 'LOT_SIZE' in filtersByType: + filter = self.safe_dict(filtersByType, 'LOT_SIZE', {}) + entry['precision']['amount'] = self.safe_number(filter, 'stepSize') + entry['limits']['amount'] = { + 'min': self.safe_number(filter, 'minQty'), + 'max': self.safe_number(filter, 'maxQty'), + } + if 'MARKET_LOT_SIZE' in filtersByType: + filter = self.safe_dict(filtersByType, 'MARKET_LOT_SIZE', {}) + entry['limits']['market'] = { + 'min': self.safe_number(filter, 'minQty'), + 'max': self.safe_number(filter, 'maxQty'), + } + if ('MIN_NOTIONAL' in filtersByType) or ('NOTIONAL' in filtersByType): # notional added in 12/04/23 to spot testnet + filter = self.safe_dict_2(filtersByType, 'MIN_NOTIONAL', 'NOTIONAL', {}) + entry['limits']['cost']['min'] = self.safe_number_2(filter, 'minNotional', 'notional') + entry['limits']['cost']['max'] = self.safe_number(filter, 'maxNotional') + return entry + + def parse_balance_helper(self, entry): + account = self.account() + account['used'] = self.safe_string(entry, 'locked') + account['free'] = self.safe_string(entry, 'free') + interest = self.safe_string(entry, 'interest') + debt = self.safe_string(entry, 'borrowed') + account['debt'] = Precise.string_add(debt, interest) + return account + + def parse_balance_custom(self, response, type=None, marginMode=None, isPortfolioMargin=False) -> Balances: + result = { + 'info': response, + } + timestamp = None + isolated = marginMode == 'isolated' + cross = (type == 'margin') or (marginMode == 'cross') + if isPortfolioMargin: + for i in range(0, len(response)): + entry = response[i] + account = self.account() + currencyId = self.safe_string(entry, 'asset') + code = self.safe_currency_code(currencyId) + if type == 'linear': + account['free'] = self.safe_string(entry, 'umWalletBalance') + account['used'] = self.safe_string(entry, 'umUnrealizedPNL') + elif type == 'inverse': + account['free'] = self.safe_string(entry, 'cmWalletBalance') + account['used'] = self.safe_string(entry, 'cmUnrealizedPNL') + elif cross: + borrowed = self.safe_string(entry, 'crossMarginBorrowed') + interest = self.safe_string(entry, 'crossMarginInterest') + account['debt'] = Precise.string_add(borrowed, interest) + account['free'] = self.safe_string(entry, 'crossMarginFree') + account['used'] = self.safe_string(entry, 'crossMarginLocked') + account['total'] = self.safe_string(entry, 'crossMarginAsset') + else: + usedLinear = self.safe_string(entry, 'umUnrealizedPNL') + usedInverse = self.safe_string(entry, 'cmUnrealizedPNL') + totalUsed = Precise.string_add(usedLinear, usedInverse) + totalWalletBalance = self.safe_string(entry, 'totalWalletBalance') + account['total'] = Precise.string_add(totalUsed, totalWalletBalance) + result[code] = account + elif not isolated and ((type == 'spot') or cross): + timestamp = self.safe_integer(response, 'updateTime') + balances = self.safe_list_2(response, 'balances', 'userAssets', []) + for i in range(0, len(balances)): + balance = balances[i] + currencyId = self.safe_string(balance, 'asset') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(balance, 'free') + account['used'] = self.safe_string(balance, 'locked') + if cross: + debt = self.safe_string(balance, 'borrowed') + interest = self.safe_string(balance, 'interest') + account['debt'] = Precise.string_add(debt, interest) + result[code] = account + elif isolated: + assets = self.safe_list(response, 'assets') + for i in range(0, len(assets)): + asset = assets[i] + marketId = self.safe_string(asset, 'symbol') + symbol = self.safe_symbol(marketId, None, None, 'spot') + base = self.safe_dict(asset, 'baseAsset', {}) + quote = self.safe_dict(asset, 'quoteAsset', {}) + baseCode = self.safe_currency_code(self.safe_string(base, 'asset')) + quoteCode = self.safe_currency_code(self.safe_string(quote, 'asset')) + subResult: dict = {} + subResult[baseCode] = self.parse_balance_helper(base) + subResult[quoteCode] = self.parse_balance_helper(quote) + result[symbol] = self.safe_balance(subResult) + elif type == 'savings': + positionAmountVos = self.safe_list(response, 'positionAmountVos', []) + for i in range(0, len(positionAmountVos)): + entry = positionAmountVos[i] + currencyId = self.safe_string(entry, 'asset') + code = self.safe_currency_code(currencyId) + account = self.account() + usedAndTotal = self.safe_string(entry, 'amount') + account['total'] = usedAndTotal + account['used'] = usedAndTotal + result[code] = account + elif type == 'funding': + for i in range(0, len(response)): + entry = response[i] + account = self.account() + currencyId = self.safe_string(entry, 'asset') + code = self.safe_currency_code(currencyId) + account['free'] = self.safe_string(entry, 'free') + frozen = self.safe_string(entry, 'freeze') + withdrawing = self.safe_string(entry, 'withdrawing') + locked = self.safe_string(entry, 'locked') + account['used'] = Precise.string_add(frozen, Precise.string_add(locked, withdrawing)) + result[code] = account + else: + balances = response + if not isinstance(response, list): + balances = self.safe_list(response, 'assets', []) + for i in range(0, len(balances)): + balance = balances[i] + currencyId = self.safe_string(balance, 'asset') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(balance, 'availableBalance') + account['used'] = self.safe_string(balance, 'initialMargin') + account['total'] = self.safe_string_2(balance, 'marginBalance', 'balance') + result[code] = account + result['timestamp'] = timestamp + result['datetime'] = self.iso8601(timestamp) + return result if isolated else self.safe_balance(result) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://developers.binance.com/docs/binance-spot-api-docs/rest-api/account-endpoints#account-information-user_data # spot + https://developers.binance.com/docs/margin_trading/account/Query-Cross-Margin-Account-Details # cross margin + https://developers.binance.com/docs/margin_trading/account/Query-Isolated-Margin-Account-Info # isolated margin + https://developers.binance.com/docs/wallet/asset/funding-wallet # funding + https://developers.binance.com/docs/derivatives/usds-margined-futures/account/rest-api/Futures-Account-Balance-V2 # swap + https://developers.binance.com/docs/derivatives/coin-margined-futures/account/rest-api/Futures-Account-Balance # future + https://developers.binance.com/docs/derivatives/option/account/Option-Account-Information # option + https://developers.binance.com/docs/derivatives/portfolio-margin/account/Account-Balance # portfolio margin + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: 'future', 'delivery', 'savings', 'funding', or 'spot' or 'papi' + :param str [params.marginMode]: 'cross' or 'isolated', for margin trading, uses self.options.defaultMarginMode if not passed, defaults to None/None/None + :param str[]|None [params.symbols]: unified market symbols, only used in isolated margin mode + :param boolean [params.portfolioMargin]: set to True if you would like to fetch the balance for a portfolio margin account + :param str [params.subType]: 'linear' or 'inverse' + :returns dict: a `balance structure ` + """ + self.load_markets() + defaultType = self.safe_string_2(self.options, 'fetchBalance', 'defaultType', 'spot') + type = self.safe_string(params, 'type', defaultType) + subType = None + subType, params = self.handle_sub_type_and_params('fetchBalance', None, params) + isPortfolioMargin = None + isPortfolioMargin, params = self.handle_option_and_params_2(params, 'fetchBalance', 'papi', 'portfolioMargin', False) + marginMode = None + query = None + marginMode, query = self.handle_margin_mode_and_params('fetchBalance', params) + query = self.omit(query, 'type') + response = None + request: dict = {} + if isPortfolioMargin or (type == 'papi'): + if self.is_linear(type, subType): + type = 'linear' + elif self.is_inverse(type, subType): + type = 'inverse' + isPortfolioMargin = True + response = self.papiGetBalance(self.extend(request, query)) + elif self.is_linear(type, subType): + type = 'linear' + useV2 = None + useV2, params = self.handle_option_and_params(params, 'fetchBalance', 'useV2', False) + params = self.extend(request, query) + if not useV2: + response = self.fapiPrivateV3GetAccount(params) + else: + response = self.fapiPrivateV2GetAccount(params) + elif self.is_inverse(type, subType): + type = 'inverse' + response = self.dapiPrivateGetAccount(self.extend(request, query)) + elif marginMode == 'isolated': + paramSymbols = self.safe_list(params, 'symbols') + query = self.omit(query, 'symbols') + if paramSymbols is not None: + symbols = '' + if isinstance(paramSymbols, list): + symbols = self.market_id(paramSymbols[0]) + for i in range(1, len(paramSymbols)): + symbol = paramSymbols[i] + id = self.market_id(symbol) + symbols += ',' + id + else: + symbols = paramSymbols + request['symbols'] = symbols + response = self.sapiGetMarginIsolatedAccount(self.extend(request, query)) + elif (type == 'margin') or (marginMode == 'cross'): + response = self.sapiGetMarginAccount(self.extend(request, query)) + elif type == 'savings': + response = self.sapiGetLendingUnionAccount(self.extend(request, query)) + elif type == 'funding': + response = self.sapiPostAssetGetFundingAsset(self.extend(request, query)) + else: + response = self.privateGetAccount(self.extend(request, query)) + # + # spot + # + # { + # "makerCommission": 10, + # "takerCommission": 10, + # "buyerCommission": 0, + # "sellerCommission": 0, + # "canTrade": True, + # "canWithdraw": True, + # "canDeposit": True, + # "updateTime": 1575357359602, + # "accountType": "MARGIN", + # "balances": [ + # {asset: "BTC", free: "0.00219821", locked: "0.00000000" }, + # ] + # } + # + # margin(cross) + # + # { + # "borrowEnabled":true, + # "marginLevel":"999.00000000", + # "totalAssetOfBtc":"0.00000000", + # "totalLiabilityOfBtc":"0.00000000", + # "totalNetAssetOfBtc":"0.00000000", + # "tradeEnabled":true, + # "transferEnabled":true, + # "userAssets":[ + # {"asset":"MATIC","borrowed":"0.00000000","free":"0.00000000","interest":"0.00000000","locked":"0.00000000","netAsset":"0.00000000"}, + # {"asset":"VET","borrowed":"0.00000000","free":"0.00000000","interest":"0.00000000","locked":"0.00000000","netAsset":"0.00000000"}, + # {"asset":"USDT","borrowed":"0.00000000","free":"0.00000000","interest":"0.00000000","locked":"0.00000000","netAsset":"0.00000000"} + # ], + # } + # + # margin(isolated) + # + # { + # "info": { + # "assets": [ + # { + # "baseAsset": { + # "asset": "1INCH", + # "borrowEnabled": True, + # "borrowed": "0", + # "free": "0", + # "interest": "0", + # "locked": "0", + # "netAsset": "0", + # "netAssetOfBtc": "0", + # "repayEnabled": True, + # "totalAsset": "0" + # }, + # "quoteAsset": { + # "asset": "USDT", + # "borrowEnabled": True, + # "borrowed": "0", + # "free": "11", + # "interest": "0", + # "locked": "0", + # "netAsset": "11", + # "netAssetOfBtc": "0.00054615", + # "repayEnabled": True, + # "totalAsset": "11" + # }, + # "symbol": "1INCHUSDT", + # "isolatedCreated": True, + # "marginLevel": "999", + # "marginLevelStatus": "EXCESSIVE", + # "marginRatio": "5", + # "indexPrice": "0.59184331", + # "liquidatePrice": "0", + # "liquidateRate": "0", + # "tradeEnabled": True, + # "enabled": True + # }, + # ] + # } + # } + # + # futures(fapi) + # + # fapiPrivateV3GetAccount + # + # { + # "feeTier":0, + # "canTrade":true, + # "canDeposit":true, + # "canWithdraw":true, + # "updateTime":0, + # "totalInitialMargin":"0.00000000", + # "totalMaintMargin":"0.00000000", + # "totalWalletBalance":"0.00000000", + # "totalUnrealizedProfit":"0.00000000", + # "totalMarginBalance":"0.00000000", + # "totalPositionInitialMargin":"0.00000000", + # "totalOpenOrderInitialMargin":"0.00000000", + # "totalCrossWalletBalance":"0.00000000", + # "totalCrossUnPnl":"0.00000000", + # "availableBalance":"0.00000000", + # "maxWithdrawAmount":"0.00000000", + # "assets":[ + # { + # "asset":"BNB", + # "walletBalance":"0.01000000", + # "unrealizedProfit":"0.00000000", + # "marginBalance":"0.01000000", + # "maintMargin":"0.00000000", + # "initialMargin":"0.00000000", + # "positionInitialMargin":"0.00000000", + # "openOrderInitialMargin":"0.00000000", + # "maxWithdrawAmount":"0.01000000", + # "crossWalletBalance":"0.01000000", + # "crossUnPnl":"0.00000000", + # "availableBalance":"0.01000000" + # } + # ], + # "positions":[ + # { + # "symbol":"BTCUSDT", + # "initialMargin":"0", + # "maintMargin":"0", + # "unrealizedProfit":"0.00000000", + # "positionInitialMargin":"0", + # "openOrderInitialMargin":"0", + # "leverage":"21", + # "isolated":false, + # "entryPrice":"0.00000", + # "maxNotional":"5000000", + # "positionSide":"BOTH" + # }, + # ] + # } + # + # fapiPrivateV2GetBalance + # + # [ + # { + # "accountAlias":"FzFzXquXXqoC", + # "asset":"BNB", + # "balance":"0.01000000", + # "crossWalletBalance":"0.01000000", + # "crossUnPnl":"0.00000000", + # "availableBalance":"0.01000000", + # "maxWithdrawAmount":"0.01000000" + # } + # ] + # + # binance pay + # + # [ + # { + # "asset": "BUSD", + # "free": "1129.83", + # "locked": "0", + # "freeze": "0", + # "withdrawing": "0" + # } + # ] + # + # portfolio margin + # + # [ + # { + # "asset": "USDT", + # "totalWalletBalance": "66.9923261", + # "crossMarginAsset": "35.9697141", + # "crossMarginBorrowed": "0.0", + # "crossMarginFree": "35.9697141", + # "crossMarginInterest": "0.0", + # "crossMarginLocked": "0.0", + # "umWalletBalance": "31.022612", + # "umUnrealizedPNL": "0.0", + # "cmWalletBalance": "0.0", + # "cmUnrealizedPNL": "0.0", + # "updateTime": 0, + # "negativeBalance": "0.0" + # }, + # ] + # + return self.parse_balance_custom(response, type, marginMode, isPortfolioMargin) + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://developers.binance.com/docs/binance-spot-api-docs/rest-api/market-data-endpoints#order-book # spot + https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/Order-Book # swap + https://developers.binance.com/docs/derivatives/coin-margined-futures/market-data/rest-api/Order-Book # future + https://developers.binance.com/docs/derivatives/option/market-data/Order-Book # option + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['limit'] = limit # default 100, max 5000, see https://github.com/binance/binance-spot-api-docs/blob/master/rest-api.md#order-book + response = None + if market['option']: + response = self.eapiPublicGetDepth(self.extend(request, params)) + elif market['linear']: + response = self.fapiPublicGetDepth(self.extend(request, params)) + elif market['inverse']: + response = self.dapiPublicGetDepth(self.extend(request, params)) + else: + response = self.publicGetDepth(self.extend(request, params)) + # + # future + # + # { + # "lastUpdateId":333598053905, + # "E":1618631511986, + # "T":1618631511964, + # "bids":[ + # ["2493.56","20.189"], + # ["2493.54","1.000"], + # ["2493.51","0.005"] + # ], + # "asks":[ + # ["2493.57","0.877"], + # ["2493.62","0.063"], + # ["2493.71","12.054"], + # ] + # } + # + # options(eapi) + # + # { + # "bids": [ + # ["108.7","16.08"], + # ["106","21.29"], + # ["82.4","0.02"] + # ], + # "asks": [ + # ["111.4","19.52"], + # ["119.9","17.6"], + # ["141.2","31"] + # ], + # "T": 1676771382078, + # "u": 1015939 + # } + # + timestamp = self.safe_integer(response, 'T') + orderbook = self.parse_order_book(response, symbol, timestamp) + orderbook['nonce'] = self.safe_integer_2(response, 'lastUpdateId', 'u') + return orderbook + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # markPrices + # + # { + # "symbol": "BTCUSDT", + # "markPrice": "11793.63104561", # mark price + # "indexPrice": "11781.80495970", # index price + # "estimatedSettlePrice": "11781.16138815", # Estimated Settle Price, only useful in the last hour before the settlement starts + # "lastFundingRate": "0.00038246", # This is the lastest estimated funding rate + # "nextFundingTime": 1597392000000, + # "interestRate": "0.00010000", + # "time": 1597370495002 + # } + # + # spot - ticker + # + # { + # "symbol": "BTCUSDT", + # "priceChange": "-188.18000000", + # "priceChangePercent": "-0.159", + # "weightedAvgPrice": "118356.64734074", + # "lastPrice": "118449.03000000", + # "prevClosePrice": "118637.22000000", # field absent in rolling ticker + # "lastQty": "0.00731000", # field absent in rolling ticker + # "bidPrice": "118449.02000000", # field absent in rolling ticker + # "bidQty": "7.15931000", # field absent in rolling ticker + # "askPrice": "118449.03000000", # field absent in rolling ticker + # "askQty": "0.09592000", # field absent in rolling ticker + # "openPrice": "118637.21000000", + # "highPrice": "119273.36000000", + # "lowPrice": "117427.50000000", + # "volume": "14741.41491000", + # "quoteVolume": "1744744445.80640740", + # "openTime": "1753701474013", + # "closeTime": "1753787874013", + # "firstId": "5116031635", + # "lastId": "5117964946", + # "count": "1933312" + # } + # + # usdm tickers + # + # { + # "symbol": "SUSDT", + # "priceChange": "-0.0229000", + # "priceChangePercent": "-6.777", + # "weightedAvgPrice": "0.3210035", + # "lastPrice": "0.3150000", + # "lastQty": "16", + # "openPrice": "0.3379000", + # "highPrice": "0.3411000", + # "lowPrice": "0.3071000", + # "volume": "120588225", + # "quoteVolume": "38709237.2289000", + # "openTime": "1753701720000", + # "closeTime": "1753788172414", + # "firstId": "72234973", + # "lastId": "72423677", + # "count": "188700" + # } + # + # coinm + # + # { + # "baseVolume": "214549.95171161", + # "closeTime": "1621965286847", + # "count": "1283779", + # "firstId": "152560106", + # "highPrice": "39938.3", + # "lastId": "153843955", + # "lastPrice": "37993.4", + # "lastQty": "1", + # "lowPrice": "36457.2", + # "openPrice": "37783.4", + # "openTime": "1621878840000", + # "pair": "BTCUSD", + # "priceChange": "210.0", + # "priceChangePercent": "0.556", + # "symbol": "BTCUSD_PERP", + # "volume": "81990451", + # "weightedAvgPrice": "38215.08713747" + # } + # + # eapi: fetchTicker, fetchTickers + # + # { + # "symbol": "ETH-230510-1825-C", + # "priceChange": "-5.1", + # "priceChangePercent": "-0.1854", + # "lastPrice": "22.4", + # "lastQty": "0", + # "open": "27.5", + # "high": "34.1", + # "low": "22.4", + # "volume": "6.83", + # "amount": "201.44", + # "bidPrice": "21.9", + # "askPrice": "22.4", + # "openTime": 1683614771898, + # "closeTime": 1683695017784, + # "firstTradeId": 12, + # "tradeCount": 22, + # "strikePrice": "1825", + # "exercisePrice": "1845.95341176" + # } + # + # spot bidsAsks + # + # { + # "symbol":"ETHBTC", + # "bidPrice":"0.07466800", + # "bidQty":"5.31990000", + # "askPrice":"0.07466900", + # "askQty":"10.93540000" + # } + # + # usdm bidsAsks + # + # { + # "symbol":"BTCUSDT", + # "bidPrice":"21321.90", + # "bidQty":"33.592", + # "askPrice":"21322.00", + # "askQty":"1.427", + # "time":"1673899207538" + # } + # + # coinm bidsAsks + # + # { + # "symbol":"BTCUSD_PERP", + # "pair":"BTCUSD", + # "bidPrice":"21301.2", + # "bidQty":"188", + # "askPrice":"21301.3", + # "askQty":"10302", + # "time":"1673899278514" + # } + # + timestamp = self.safe_integer_2(ticker, 'closeTime', 'time') + marketType = None + if ('time' in ticker): + marketType = 'contract' + if marketType is None: + marketType = 'spot' if ('bidQty' in ticker) else 'contract' + marketId = self.safe_string(ticker, 'symbol') + symbol = self.safe_symbol(marketId, market, None, marketType) + last = self.safe_string(ticker, 'lastPrice') + wAvg = self.safe_string(ticker, 'weightedAvgPrice') + isCoinm = ('baseVolume' in ticker) + baseVolume = None + quoteVolume = None + if isCoinm: + baseVolume = self.safe_string(ticker, 'baseVolume') + # 'volume' field in inverse markets is not quoteVolume, but traded amount(per contracts) + quoteVolume = Precise.string_mul(baseVolume, wAvg) + else: + baseVolume = self.safe_string(ticker, 'volume') + quoteVolume = self.safe_string_2(ticker, 'quoteVolume', 'amount') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string_2(ticker, 'highPrice', 'high'), + 'low': self.safe_string_2(ticker, 'lowPrice', 'low'), + 'bid': self.safe_string(ticker, 'bidPrice'), + 'bidVolume': self.safe_string(ticker, 'bidQty'), + 'ask': self.safe_string(ticker, 'askPrice'), + 'askVolume': self.safe_string(ticker, 'askQty'), + 'vwap': wAvg, + 'open': self.safe_string_2(ticker, 'openPrice', 'open'), + 'close': last, + 'last': last, + 'previousClose': self.safe_string(ticker, 'prevClosePrice'), # previous day close + 'change': self.safe_string(ticker, 'priceChange'), + 'percentage': self.safe_string(ticker, 'priceChangePercent'), + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'markPrice': self.safe_string(ticker, 'markPrice'), + 'indexPrice': self.safe_string(ticker, 'indexPrice'), + 'info': ticker, + }, market) + + def fetch_status(self, params={}): + """ + the latest known information on the availability of the exchange API + + https://developers.binance.com/docs/wallet/others/system-status + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `status structure ` + """ + response = self.sapiGetSystemStatus(params) + # + # { + # "status": 0, # 0: normal,1:system maintenance + # "msg": "normal" # "normal", "system_maintenance" + # } + # + statusRaw = self.safe_string(response, 'status') + return { + 'status': self.safe_string({'0': 'ok', '1': 'maintenance'}, statusRaw, statusRaw), + 'updated': None, + 'eta': None, + 'url': None, + 'info': response, + } + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://developers.binance.com/docs/binance-spot-api-docs/rest-api/market-data-endpoints#24hr-ticker-price-change-statistics # spot + https://developers.binance.com/docs/binance-spot-api-docs/rest-api/market-data-endpoints#rolling-window-price-change-statistics # spot + https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/24hr-Ticker-Price-Change-Statistics # swap + https://developers.binance.com/docs/derivatives/coin-margined-futures/market-data/rest-api/24hr-Ticker-Price-Change-Statistics # future + https://developers.binance.com/docs/derivatives/option/market-data/24hr-Ticker-Price-Change-Statistics # option + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.rolling]:(spot only) default False, if True, uses the rolling 24 hour ticker endpoint /api/v3/ticker + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = None + if market['option']: + response = self.eapiPublicGetTicker(self.extend(request, params)) + elif market['linear']: + response = self.fapiPublicGetTicker24hr(self.extend(request, params)) + elif market['inverse']: + response = self.dapiPublicGetTicker24hr(self.extend(request, params)) + else: + rolling = self.safe_bool(params, 'rolling', False) + params = self.omit(params, 'rolling') + if rolling: + response = self.publicGetTicker(self.extend(request, params)) + else: + response = self.publicGetTicker24hr(self.extend(request, params)) + if isinstance(response, list): + firstTicker = self.safe_dict(response, 0, {}) + return self.parse_ticker(firstTicker, market) + return self.parse_ticker(response, market) + + def fetch_bids_asks(self, symbols: Strings = None, params={}): + """ + fetches the bid and ask price and volume for multiple markets + + https://developers.binance.com/docs/binance-spot-api-docs/rest-api/market-data-endpoints#symbol-order-book-ticker # spot + https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/Symbol-Order-Book-Ticker # swap + https://developers.binance.com/docs/derivatives/coin-margined-futures/market-data/rest-api/Symbol-Order-Book-Ticker # future + + :param str[]|None symbols: unified symbols of the markets to fetch the bids and asks for, all markets are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.subType]: "linear" or "inverse" + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + symbols = self.market_symbols(symbols, None, True, True, True) + market = self.get_market_from_symbols(symbols) + type = None + type, params = self.handle_market_type_and_params('fetchBidsAsks', market, params) + subType = None + subType, params = self.handle_sub_type_and_params('fetchBidsAsks', market, params) + response = None + if self.is_linear(type, subType): + response = self.fapiPublicGetTickerBookTicker(params) + elif self.is_inverse(type, subType): + response = self.dapiPublicGetTickerBookTicker(params) + elif type == 'spot': + request: dict = {} + if symbols is not None: + request['symbols'] = self.json(self.market_ids(symbols)) + response = self.publicGetTickerBookTicker(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchBidsAsks() does not support ' + type + ' markets yet') + return self.parse_tickers(response, symbols) + + def fetch_last_prices(self, symbols: Strings = None, params={}): + """ + fetches the last price for multiple markets + + https://developers.binance.com/docs/binance-spot-api-docs/rest-api/market-data-endpoints#symbol-price-ticker # spot + https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/Symbol-Price-Ticker # swap + https://developers.binance.com/docs/derivatives/coin-margined-futures/market-data/rest-api/Symbol-Price-Ticker # future + + :param str[]|None symbols: unified symbols of the markets to fetch the last prices + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.subType]: "linear" or "inverse" + :returns dict: a dictionary of lastprices structures + """ + self.load_markets() + symbols = self.market_symbols(symbols, None, True, True, True) + market = self.get_market_from_symbols(symbols) + type = None + type, params = self.handle_market_type_and_params('fetchLastPrices', market, params) + subType = None + subType, params = self.handle_sub_type_and_params('fetchLastPrices', market, params) + response = None + if self.is_linear(type, subType): + response = self.fapiPublicV2GetTickerPrice(params) + # + # [ + # { + # "symbol": "LTCBTC", + # "price": "4.00000200" + # "time": 1589437530011 + # }, + # ... + # ] + # + elif self.is_inverse(type, subType): + response = self.dapiPublicGetTickerPrice(params) + # + # [ + # { + # "symbol": "BTCUSD_200626", + # "ps": "9647.8", + # "price": "9647.8", + # "time": 1591257246176 + # } + # ] + # + elif type == 'spot': + response = self.publicGetTickerPrice(params) + # + # [ + # { + # "symbol": "LTCBTC", + # "price": "4.00000200" + # }, + # ... + # ] + # + else: + raise NotSupported(self.id + ' fetchLastPrices() does not support ' + type + ' markets yet') + return self.parse_last_prices(response, symbols) + + def parse_last_price(self, entry, market: Market = None): + # + # spot + # + # { + # "symbol": "LTCBTC", + # "price": "4.00000200" + # } + # + # usdm(swap/future) + # + # { + # "symbol": "BTCUSDT", + # "price": "6000.01", + # "time": 1589437530011 # Transaction time + # } + # + # + # coinm(swap/future) + # + # { + # "symbol": "BTCUSD_200626", # symbol("BTCUSD_200626", "BTCUSD_PERP", etc..) + # "ps": "BTCUSD", # pair + # "price": "9647.8", + # "time": 1591257246176 + # } + # + timestamp = self.safe_integer(entry, 'time') + type = 'spot' if (timestamp is None) else 'swap' + marketId = self.safe_string(entry, 'symbol') + market = self.safe_market(marketId, market, None, type) + return { + 'symbol': market['symbol'], + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'price': self.safe_number_omit_zero(entry, 'price'), + 'side': None, + 'info': entry, + } + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://developers.binance.com/docs/binance-spot-api-docs/rest-api/market-data-endpoints#24hr-ticker-price-change-statistics # spot + https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/24hr-Ticker-Price-Change-Statistics # swap + https://developers.binance.com/docs/derivatives/coin-margined-futures/market-data/rest-api/24hr-Ticker-Price-Change-Statistics # future + https://developers.binance.com/docs/derivatives/option/market-data/24hr-Ticker-Price-Change-Statistics # option + + :param str[] [symbols]: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.subType]: "linear" or "inverse" + :param str [params.type]: 'spot', 'option', use params["subType"] for swap and future markets + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + symbols = self.market_symbols(symbols, None, True, True, True) + market = self.get_market_from_symbols(symbols) + type = None + type, params = self.handle_market_type_and_params('fetchTickers', market, params) + subType = None + subType, params = self.handle_sub_type_and_params('fetchTickers', market, params) + response = None + if self.is_linear(type, subType): + response = self.fapiPublicGetTicker24hr(params) + elif self.is_inverse(type, subType): + response = self.dapiPublicGetTicker24hr(params) + elif type == 'spot': + rolling = self.safe_bool(params, 'rolling', False) + params = self.omit(params, 'rolling') + if rolling: + symbols = self.market_symbols(symbols) + request: dict = { + 'symbols': self.json(self.market_ids(symbols)), + } + response = self.publicGetTicker(self.extend(request, params)) + # parseTicker is not able to handle marketType for spot-rolling ticker fields, so we need custom parsing + return self.parse_tickers_for_rolling(response, symbols) + else: + request: dict = {} + if symbols is not None: + request['symbols'] = self.json(self.market_ids(symbols)) + response = self.publicGetTicker24hr(self.extend(request, params)) + elif type == 'option': + response = self.eapiPublicGetTicker(params) + else: + raise NotSupported(self.id + ' fetchTickers() does not support ' + type + ' markets yet') + return self.parse_tickers(response, symbols) + + def parse_tickers_for_rolling(self, response, symbols): + results = [] + for i in range(0, len(response)): + marketId = self.safe_string(response[i], 'symbol') + tickerMarket = self.safe_market(marketId, None, None, 'spot') + parsedTicker = self.parse_ticker(response[i]) + parsedTicker['symbol'] = tickerMarket['symbol'] + results.append(parsedTicker) + return self.filter_by_array(results, 'symbol', symbols) + + def fetch_mark_price(self, symbol: str, params={}) -> Ticker: + """ + fetches mark price for the market + + https://developers.binance.com/docs/derivatives/coin-margined-futures/market-data/rest-api/Index-Price-and-Mark-Price + https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/Mark-Price + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.subType]: "linear" or "inverse" + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + market = self.market(symbol) + type = None + type, params = self.handle_market_type_and_params('fetchMarkPrice', market, params, 'swap') + subType = None + subType, params = self.handle_sub_type_and_params('fetchMarkPrice', market, params, 'linear') + request = { + 'symbol': market['id'], + } + response = None + if self.is_linear(type, subType): + response = self.fapiPublicGetPremiumIndex(self.extend(request, params)) + elif self.is_inverse(type, subType): + response = self.dapiPublicGetPremiumIndex(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchMarkPrice() does not support ' + type + ' markets yet') + if isinstance(response, list): + return self.parse_ticker(self.safe_dict(response, 0, {}), market) + return self.parse_ticker(response, market) + + def fetch_mark_prices(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches mark prices for multiple markets + + https://developers.binance.com/docs/derivatives/coin-margined-futures/market-data/rest-api/Index-Price-and-Mark-Price + https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/Mark-Price + + :param str[] [symbols]: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.subType]: "linear" or "inverse" + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + symbols = self.market_symbols(symbols, None, True, True, True) + market = self.get_market_from_symbols(symbols) + type = None + type, params = self.handle_market_type_and_params('fetchMarkPrices', market, params, 'swap') + subType = None + subType, params = self.handle_sub_type_and_params('fetchMarkPrices', market, params, 'linear') + response = None + if self.is_linear(type, subType): + response = self.fapiPublicGetPremiumIndex(params) + elif self.is_inverse(type, subType): + response = self.dapiPublicGetPremiumIndex(params) + else: + raise NotSupported(self.id + ' fetchMarkPrices() does not support ' + type + ' markets yet') + return self.parse_tickers(response, symbols) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # when api method = publicGetKlines or fapiPublicGetKlines or dapiPublicGetKlines + # [ + # 1591478520000, # open time + # "0.02501300", # open + # "0.02501800", # high + # "0.02500000", # low + # "0.02500000", # close + # "22.19000000", # volume + # 1591478579999, # close time + # "0.55490906", # quote asset volume, base asset volume for dapi + # 40, # number of trades + # "10.92900000", # taker buy base asset volume + # "0.27336462", # taker buy quote asset volume + # "0" # ignore + # ] + # + # when api method = fapiPublicGetMarkPriceKlines or fapiPublicGetIndexPriceKlines + # [ + # [ + # 1591256460000, # Open time + # "9653.29201333", # Open + # "9654.56401333", # High + # "9653.07367333", # Low + # "9653.07367333", # Close(or latest price) + # "0", # Ignore + # 1591256519999, # Close time + # "0", # Ignore + # 60, # Number of bisic data + # "0", # Ignore + # "0", # Ignore + # "0" # Ignore + # ] + # ] + # + # options + # + # { + # "open": "32.2", + # "high": "32.2", + # "low": "32.2", + # "close": "32.2", + # "volume": "0", + # "interval": "5m", + # "tradeCount": 0, + # "takerVolume": "0", + # "takerAmount": "0", + # "amount": "0", + # "openTime": 1677096900000, + # "closeTime": 1677097200000 + # } + # + inverse = self.safe_bool(market, 'inverse') + volumeIndex = 7 if inverse else 5 + return [ + self.safe_integer_2(ohlcv, 0, 'openTime'), + self.safe_number_2(ohlcv, 1, 'open'), + self.safe_number_2(ohlcv, 2, 'high'), + self.safe_number_2(ohlcv, 3, 'low'), + self.safe_number_2(ohlcv, 4, 'close'), + self.safe_number_2(ohlcv, volumeIndex, 'volume'), + ] + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://developers.binance.com/docs/binance-spot-api-docs/rest-api/market-data-endpoints#klinecandlestick-data + https://developers.binance.com/docs/derivatives/option/market-data/Kline-Candlestick-Data + https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/Kline-Candlestick-Data + https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/Index-Price-Kline-Candlestick-Data + https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/Mark-Price-Kline-Candlestick-Data + https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/Premium-Index-Kline-Data + https://developers.binance.com/docs/derivatives/coin-margined-futures/market-data/rest-api/Kline-Candlestick-Data + https://developers.binance.com/docs/derivatives/coin-margined-futures/market-data/rest-api/Index-Price-Kline-Candlestick-Data + https://developers.binance.com/docs/derivatives/coin-margined-futures/market-data/rest-api/Mark-Price-Kline-Candlestick-Data + https://developers.binance.com/docs/derivatives/coin-margined-futures/market-data/rest-api/Premium-Index-Kline-Data + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.price]: "mark" or "index" for mark price and index price candles + :param int [params.until]: timestamp in ms of the latest candle to fetch + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate', False) + if paginate: + return self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params, 1000) + market = self.market(symbol) + # binance docs say that the default limit 500, max 1500 for futures, max 1000 for spot markets + # the reality is that the time range wider than 500 candles won't work right + defaultLimit = 500 + maxLimit = 1500 + price = self.safe_string(params, 'price') + until = self.safe_integer(params, 'until') + params = self.omit(params, ['price', 'until']) + if since is not None and until is not None and limit is None: + limit = maxLimit + limit = defaultLimit if (limit is None) else min(limit, maxLimit) + request: dict = { + 'interval': self.safe_string(self.timeframes, timeframe, timeframe), + 'limit': limit, + } + marketId = market['id'] + if price == 'index': + parts = marketId.split('_') + pair = self.safe_string(parts, 0) + request['pair'] = pair # Index price takes self argument instead of symbol + else: + request['symbol'] = marketId + # duration = self.parse_timeframe(timeframe) + if since is not None: + request['startTime'] = since + # + # It didn't work before without the endTime + # https://github.com/ccxt/ccxt/issues/8454 + # + if market['inverse']: + if since > 0: + duration = self.parse_timeframe(timeframe) + endTime = self.sum(since, limit * duration * 1000 - 1) + now = self.milliseconds() + request['endTime'] = min(now, endTime) + if until is not None: + request['endTime'] = until + response = None + if market['option']: + response = self.eapiPublicGetKlines(self.extend(request, params)) + elif price == 'mark': + if market['inverse']: + response = self.dapiPublicGetMarkPriceKlines(self.extend(request, params)) + else: + response = self.fapiPublicGetMarkPriceKlines(self.extend(request, params)) + elif price == 'index': + if market['inverse']: + response = self.dapiPublicGetIndexPriceKlines(self.extend(request, params)) + else: + response = self.fapiPublicGetIndexPriceKlines(self.extend(request, params)) + elif price == 'premiumIndex': + if market['inverse']: + response = self.dapiPublicGetPremiumIndexKlines(self.extend(request, params)) + else: + response = self.fapiPublicGetPremiumIndexKlines(self.extend(request, params)) + elif market['linear']: + response = self.fapiPublicGetKlines(self.extend(request, params)) + elif market['inverse']: + response = self.dapiPublicGetKlines(self.extend(request, params)) + else: + response = self.publicGetKlines(self.extend(request, params)) + # + # [ + # [1591478520000,"0.02501300","0.02501800","0.02500000","0.02500000","22.19000000",1591478579999,"0.55490906",40,"10.92900000","0.27336462","0"], + # [1591478580000,"0.02499600","0.02500900","0.02499400","0.02500300","21.34700000",1591478639999,"0.53370468",24,"7.53800000","0.18850725","0"], + # [1591478640000,"0.02500800","0.02501100","0.02500300","0.02500800","154.14200000",1591478699999,"3.85405839",97,"5.32300000","0.13312641","0"], + # ] + # + # options(eapi) + # + # [ + # { + # "open": "32.2", + # "high": "32.2", + # "low": "32.2", + # "close": "32.2", + # "volume": "0", + # "interval": "5m", + # "tradeCount": 0, + # "takerVolume": "0", + # "takerAmount": "0", + # "amount": "0", + # "openTime": 1677096900000, + # "closeTime": 1677097200000 + # } + # ] + # + candles = self.parse_ohlcvs(response, market, timeframe, since, limit) + return candles + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + if 'isDustTrade' in trade: + return self.parse_dust_trade(trade, market) + # + # aggregate trades + # https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md#compressedaggregate-trades-list + # + # { + # "a": 26129, # Aggregate tradeId + # "p": "0.01633102", # Price + # "q": "4.70443515", # Quantity + # "f": 27781, # First tradeId + # "l": 27781, # Last tradeId + # "T": 1498793709153, # Timestamp + # "m": True, # Was the buyer the maker? + # "M": True # Was the trade the best price match? + # } + # + # REST: aggregate trades for swap & future(both linear and inverse) + # + # { + # "a": "269772814", + # "p": "25864.1", + # "q": "3", + # "f": "662149354", + # "l": "662149355", + # "T": "1694209776022", + # "m": False, + # } + # + # recent public trades and old public trades + # https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md#recent-trades-list + # https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md#old-trade-lookup-market_data + # + # { + # "id": 28457, + # "price": "4.00000100", + # "qty": "12.00000000", + # "time": 1499865549590, + # "isBuyerMaker": True, + # "isBestMatch": True + # } + # + # private trades + # https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md#account-trade-list-user_data + # + # { + # "symbol": "BNBBTC", + # "id": 28457, + # "orderId": 100234, + # "price": "4.00000100", + # "qty": "12.00000000", + # "commission": "10.10000000", + # "commissionAsset": "BNB", + # "time": 1499865549590, + # "isBuyer": True, + # "isMaker": False, + # "isBestMatch": True + # } + # + # futures trades + # + # { + # "accountId": 20, + # "buyer": False, + # "commission": "-0.07819010", + # "commissionAsset": "USDT", + # "counterPartyId": 653, + # "id": 698759, + # "maker": False, + # "orderId": 25851813, + # "price": "7819.01", + # "qty": "0.002", + # "quoteQty": "0.01563", + # "realizedPnl": "-0.91539999", + # "side": "SELL", + # "symbol": "BTCUSDT", + # "time": 1569514978020 + # } + # { + # "symbol": "BTCUSDT", + # "id": 477128891, + # "orderId": 13809777875, + # "side": "SELL", + # "price": "38479.55", + # "qty": "0.001", + # "realizedPnl": "-0.00009534", + # "marginAsset": "USDT", + # "quoteQty": "38.47955", + # "commission": "-0.00076959", + # "commissionAsset": "USDT", + # "time": 1612733566708, + # "positionSide": "BOTH", + # "maker": True, + # "buyer": False + # } + # + # {respType: FULL} + # + # { + # "price": "4000.00000000", + # "qty": "1.00000000", + # "commission": "4.00000000", + # "commissionAsset": "USDT", + # "tradeId": "1234", + # } + # + # options: fetchMyTrades + # + # { + # "id": 1125899906844226012, + # "tradeId": 73, + # "orderId": 4638761100843040768, + # "symbol": "ETH-230211-1500-C", + # "price": "18.70000000", + # "quantity": "-0.57000000", + # "fee": "0.17305890", + # "realizedProfit": "-3.53400000", + # "side": "SELL", + # "type": "LIMIT", + # "volatility": "0.30000000", + # "liquidity": "MAKER", + # "time": 1676085216845, + # "priceScale": 1, + # "quantityScale": 2, + # "optionSide": "CALL", + # "quoteAsset": "USDT" + # } + # + # options: fetchTrades + # + # { + # "id": 1, + # "symbol": "ETH-230216-1500-C", + # "price": "35.5", + # "qty": "0.03", + # "quoteQty": "1.065", + # "side": 1, + # "time": 1676366446072 + # } + # + # fetchMyTrades: linear portfolio margin + # + # { + # "symbol": "BTCUSDT", + # "id": 4575108247, + # "orderId": 261942655610, + # "side": "SELL", + # "price": "47263.40", + # "qty": "0.010", + # "realizedPnl": "27.38400000", + # "marginAsset": "USDT", + # "quoteQty": "472.63", + # "commission": "0.18905360", + # "commissionAsset": "USDT", + # "time": 1707530039409, + # "buyer": False, + # "maker": False, + # "positionSide": "LONG" + # } + # + # fetchMyTrades: inverse portfolio margin + # + # { + # "symbol": "ETHUSD_PERP", + # "id": 701907838, + # "orderId": 71548909034, + # "pair": "ETHUSD", + # "side": "SELL", + # "price": "2498.15", + # "qty": "1", + # "realizedPnl": "0.00012517", + # "marginAsset": "ETH", + # "baseQty": "0.00400296", + # "commission": "0.00000160", + # "commissionAsset": "ETH", + # "time": 1707530317519, + # "positionSide": "LONG", + # "buyer": False, + # "maker": False + # } + # + # fetchMyTrades: spot margin portfolio margin + # + # { + # "symbol": "ADAUSDT", + # "id": 470227543, + # "orderId": 4421170947, + # "price": "0.53880000", + # "qty": "10.00000000", + # "quoteQty": "5.38800000", + # "commission": "0.00538800", + # "commissionAsset": "USDT", + # "time": 1707545780522, + # "isBuyer": False, + # "isMaker": False, + # "isBestMatch": True + # } + # + timestamp = self.safe_integer_2(trade, 'T', 'time') + amount = self.safe_string_2(trade, 'q', 'qty') + amount = self.safe_string(trade, 'quantity', amount) + marketId = self.safe_string(trade, 'symbol') + isSpotTrade = ('isIsolated' in trade) or ('M' in trade) or ('orderListId' in trade) or ('isMaker' in trade) + marketType = 'spot' if isSpotTrade else 'contract' + market = self.safe_market(marketId, market, None, marketType) + symbol = market['symbol'] + side = None + buyerMaker = self.safe_bool_2(trade, 'm', 'isBuyerMaker') + takerOrMaker = None + if buyerMaker is not None: + side = 'sell' if buyerMaker else 'buy' # self is reversed intentionally + elif 'side' in trade: + side = self.safe_string_lower(trade, 'side') + else: + if 'isBuyer' in trade: + side = 'buy' if trade['isBuyer'] else 'sell' # self is a True side + fee = None + if 'commission' in trade: + fee = { + 'cost': self.safe_string(trade, 'commission'), + 'currency': self.safe_currency_code(self.safe_string(trade, 'commissionAsset')), + } + if 'isMaker' in trade: + takerOrMaker = 'maker' if trade['isMaker'] else 'taker' + if 'maker' in trade: + takerOrMaker = 'maker' if trade['maker'] else 'taker' + if ('optionSide' in trade) or market['option']: + settle = self.safe_currency_code(self.safe_string(trade, 'quoteAsset', 'USDT')) + takerOrMaker = self.safe_string_lower(trade, 'liquidity') + if 'fee' in trade: + fee = { + 'cost': self.safe_string(trade, 'fee'), + 'currency': settle, + } + if (side != 'buy') and (side != 'sell'): + side = 'buy' if (side == '1') else 'sell' + if 'optionSide' in trade: + if side != 'buy': + amount = Precise.string_mul('-1', amount) + return self.safe_trade({ + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'id': self.safe_string_n(trade, ['t', 'a', 'tradeId', 'id']), + 'order': self.safe_string(trade, 'orderId'), + 'type': self.safe_string_lower(trade, 'type'), + 'side': side, + 'takerOrMaker': takerOrMaker, + 'price': self.safe_string_2(trade, 'p', 'price'), + 'amount': amount, + 'cost': self.safe_string_2(trade, 'quoteQty', 'baseQty'), + 'fee': fee, + }, market) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + Default fetchTradesMethod + + https://developers.binance.com/docs/binance-spot-api-docs/rest-api/market-data-endpoints#compressedaggregate-trades-list # publicGetAggTrades(spot) + https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/Compressed-Aggregate-Trades-List # fapiPublicGetAggTrades(swap) + https://developers.binance.com/docs/derivatives/coin-margined-futures/market-data/rest-api/Compressed-Aggregate-Trades-List # dapiPublicGetAggTrades(future) + https://developers.binance.com/docs/derivatives/option/market-data/Recent-Trades-List # eapiPublicGetTrades(option) + + Other fetchTradesMethod + + https://developers.binance.com/docs/binance-spot-api-docs/rest-api/market-data-endpoints#recent-trades-list # publicGetTrades(spot) + https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/Recent-Trades-List # fapiPublicGetTrades(swap) + https://developers.binance.com/docs/derivatives/coin-margined-futures/market-data/rest-api/Recent-Trades-List # dapiPublicGetTrades(future) + https://developers.binance.com/docs/binance-spot-api-docs/rest-api/market-data-endpoints#old-trade-lookup # publicGetHistoricalTrades(spot) + https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/Old-Trades-Lookup # fapiPublicGetHistoricalTrades(swap) + https://developers.binance.com/docs/derivatives/coin-margined-futures/market-data/rest-api/Old-Trades-Lookup # dapiPublicGetHistoricalTrades(future) + https://developers.binance.com/docs/derivatives/option/market-data/Old-Trades-Lookup # eapiPublicGetHistoricalTrades(option) + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: only used when fetchTradesMethod is 'publicGetAggTrades', 'fapiPublicGetAggTrades', or 'dapiPublicGetAggTrades' + :param int [limit]: default 500, max 1000 + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: only used when fetchTradesMethod is 'publicGetAggTrades', 'fapiPublicGetAggTrades', or 'dapiPublicGetAggTrades' + :param int [params.fetchTradesMethod]: 'publicGetAggTrades'(spot default), 'fapiPublicGetAggTrades'(swap default), 'dapiPublicGetAggTrades'(future default), 'eapiPublicGetTrades'(option default), 'publicGetTrades', 'fapiPublicGetTrades', 'dapiPublicGetTrades', 'publicGetHistoricalTrades', 'fapiPublicGetHistoricalTrades', 'dapiPublicGetHistoricalTrades', 'eapiPublicGetHistoricalTrades' + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + + EXCHANGE SPECIFIC PARAMETERS + :param int [params.fromId]: trade id to fetch from, default gets most recent trades, not used when fetchTradesMethod is 'publicGetTrades', 'fapiPublicGetTrades', 'dapiPublicGetTrades', or 'eapiPublicGetTrades' + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchTrades', 'paginate') + if paginate: + return self.fetch_paginated_call_dynamic('fetchTrades', symbol, since, limit, params) + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + # 'fromId': 123, # ID to get aggregate trades from INCLUSIVE. + # 'startTime': 456, # Timestamp in ms to get aggregate trades from INCLUSIVE. + # 'endTime': 789, # Timestamp in ms to get aggregate trades until INCLUSIVE. + # 'limit': 500, # default = 500, maximum = 1000 + } + if not market['option']: + if since is not None: + request['startTime'] = since + # https://github.com/ccxt/ccxt/issues/6400 + # https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md#compressedaggregate-trades-list + request['endTime'] = self.sum(since, 3600000) + until = self.safe_integer(params, 'until') + if until is not None: + request['endTime'] = until + method = self.safe_string(self.options, 'fetchTradesMethod') + method = self.safe_string_2(params, 'fetchTradesMethod', 'method', method) + if limit is not None: + isFutureOrSwap = (market['swap'] or market['future']) + isHistoricalEndpoint = (method is not None) and (method.find('GetHistoricalTrades') >= 0) + maxLimitForContractHistorical = 500 if isHistoricalEndpoint else 1000 + request['limit'] = min(limit, maxLimitForContractHistorical) if isFutureOrSwap else limit # default = 500, maximum = 1000 + params = self.omit(params, ['until', 'fetchTradesMethod']) + response = None + if market['option'] or method == 'eapiPublicGetTrades': + response = self.eapiPublicGetTrades(self.extend(request, params)) + elif market['linear'] or method == 'fapiPublicGetAggTrades': + response = self.fapiPublicGetAggTrades(self.extend(request, params)) + elif market['inverse'] or method == 'dapiPublicGetAggTrades': + response = self.dapiPublicGetAggTrades(self.extend(request, params)) + else: + response = self.publicGetAggTrades(self.extend(request, params)) + # + # Caveats: + # - default limit(500) applies only if no other parameters set, trades up + # to the maximum limit may be returned to satisfy other parameters + # - if both limit and time window is set and time window contains more + # trades than the limit then the last trades from the window are returned + # - "tradeId" accepted and returned by self method is "aggregate" trade id + # which is different from actual trade id + # - setting both fromId and time window results in error + # + # aggregate trades + # + # [ + # { + # "a": 26129, # Aggregate tradeId + # "p": "0.01633102", # Price + # "q": "4.70443515", # Quantity + # "f": 27781, # First tradeId + # "l": 27781, # Last tradeId + # "T": 1498793709153, # Timestamp + # "m": True, # Was the buyer the maker? + # "M": True # Was the trade the best price match? + # } + # ] + # + # inverse(swap & future) + # + # [ + # { + # "a": "269772814", + # "p": "25864.1", + # "q": "3", + # "f": "662149354", + # "l": "662149355", + # "T": "1694209776022", + # "m": False, + # }, + # ] + # + # recent public trades and historical public trades + # + # [ + # { + # "id": 28457, + # "price": "4.00000100", + # "qty": "12.00000000", + # "time": 1499865549590, + # "isBuyerMaker": True, + # "isBestMatch": True + # } + # ] + # + # options(eapi) + # + # [ + # { + # "id": 1, + # "symbol": "ETH-230216-1500-C", + # "price": "35.5", + # "qty": "0.03", + # "quoteQty": "1.065", + # "side": 1, + # "time": 1676366446072 + # }, + # ] + # + return self.parse_trades(response, market, since, limit) + + def edit_spot_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + @ignore + edit a trade order + + https://developers.binance.com/docs/binance-spot-api-docs/rest-api/trading-endpoints#cancel-an-existing-order-and-send-a-new-order-trade + + :param str id: cancel order id + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' or 'STOP_LOSS' or 'STOP_LOSS_LIMIT' or 'TAKE_PROFIT' or 'TAKE_PROFIT_LIMIT' or 'STOP' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated', for spot margin trading + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' editSpotOrder() does not support ' + market['type'] + ' orders') + payload = self.edit_spot_order_request(id, symbol, type, side, amount, price, params) + response = self.privatePostOrderCancelReplace(payload) + # + # spot + # + # { + # "cancelResult": "SUCCESS", + # "newOrderResult": "SUCCESS", + # "cancelResponse": { + # "symbol": "BTCUSDT", + # "origClientOrderId": "web_3f6286480b194b079870ac75fb6978b7", + # "orderId": 16383156620, + # "orderListId": -1, + # "clientOrderId": "Azt6foVTTgHPNhqBf41TTt", + # "price": "14000.00000000", + # "origQty": "0.00110000", + # "executedQty": "0.00000000", + # "cummulativeQuoteQty": "0.00000000", + # "status": "CANCELED", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "side": "BUY" + # }, + # "newOrderResponse": { + # "symbol": "BTCUSDT", + # "orderId": 16383176297, + # "orderListId": -1, + # "clientOrderId": "x-TKT5PX2F22ecb58eb9074fb1be018c", + # "transactTime": 1670891847932, + # "price": "13500.00000000", + # "origQty": "0.00085000", + # "executedQty": "0.00000000", + # "cummulativeQuoteQty": "0.00000000", + # "status": "NEW", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "side": "BUY", + # "fills": [] + # } + # } + # + data = self.safe_dict(response, 'newOrderResponse') + return self.parse_order(data, market) + + def edit_spot_order_request(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + @ignore + helper function to build request for editSpotOrder + :param str id: order id to be edited + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' or 'STOP_LOSS' or 'STOP_LOSS_LIMIT' or 'TAKE_PROFIT' or 'TAKE_PROFIT_LIMIT' or 'STOP' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict params: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated', for spot margin trading + :returns dict: request to be sent to the exchange + """ + market = self.market(symbol) + clientOrderId = self.safe_string_n(params, ['newClientOrderId', 'clientOrderId', 'origClientOrderId']) + request: dict = { + 'symbol': market['id'], + 'side': side.upper(), + } + initialUppercaseType = type.upper() + uppercaseType = initialUppercaseType + postOnly = self.is_post_only(initialUppercaseType == 'MARKET', initialUppercaseType == 'LIMIT_MAKER', params) + if postOnly: + uppercaseType = 'LIMIT_MAKER' + triggerPrice = self.safe_number_2(params, 'stopPrice', 'triggerPrice') + if triggerPrice is not None: + if uppercaseType == 'MARKET': + uppercaseType = 'STOP_LOSS' + elif uppercaseType == 'LIMIT': + uppercaseType = 'STOP_LOSS_LIMIT' + request['type'] = uppercaseType + validOrderTypes = self.safe_list(market['info'], 'orderTypes') + if not self.in_array(uppercaseType, validOrderTypes): + if initialUppercaseType != uppercaseType: + raise InvalidOrder(self.id + ' triggerPrice parameter is not allowed for ' + symbol + ' ' + type + ' orders') + else: + raise InvalidOrder(self.id + ' ' + type + ' is not a valid order type for the ' + symbol + ' market') + if clientOrderId is None: + broker = self.safe_dict(self.options, 'broker') + if broker is not None: + brokerId = self.safe_string(broker, 'spot') + if brokerId is not None: + request['newClientOrderId'] = brokerId + self.uuid22() + else: + request['newClientOrderId'] = clientOrderId + request['newOrderRespType'] = self.safe_value(self.options['newOrderRespType'], type, 'RESULT') # 'ACK' for order id, 'RESULT' for full order or 'FULL' for order with fills + timeInForceIsRequired = False + priceIsRequired = False + triggerPriceIsRequired = False + quantityIsRequired = False + if uppercaseType == 'MARKET': + quoteOrderQty = self.safe_bool(self.options, 'quoteOrderQty', True) + if quoteOrderQty: + quoteOrderQtyNew = self.safe_value_2(params, 'quoteOrderQty', 'cost') + precision = market['precision']['price'] + if quoteOrderQtyNew is not None: + request['quoteOrderQty'] = self.decimal_to_precision(quoteOrderQtyNew, TRUNCATE, precision, self.precisionMode) + elif price is not None: + amountString = self.number_to_string(amount) + priceString = self.number_to_string(price) + quoteOrderQuantity = Precise.string_mul(amountString, priceString) + request['quoteOrderQty'] = self.decimal_to_precision(quoteOrderQuantity, TRUNCATE, precision, self.precisionMode) + else: + quantityIsRequired = True + else: + quantityIsRequired = True + elif uppercaseType == 'LIMIT': + priceIsRequired = True + timeInForceIsRequired = True + quantityIsRequired = True + elif (uppercaseType == 'STOP_LOSS') or (uppercaseType == 'TAKE_PROFIT'): + triggerPriceIsRequired = True + quantityIsRequired = True + elif (uppercaseType == 'STOP_LOSS_LIMIT') or (uppercaseType == 'TAKE_PROFIT_LIMIT'): + quantityIsRequired = True + triggerPriceIsRequired = True + priceIsRequired = True + timeInForceIsRequired = True + elif uppercaseType == 'LIMIT_MAKER': + priceIsRequired = True + quantityIsRequired = True + if quantityIsRequired: + request['quantity'] = self.amount_to_precision(symbol, amount) + if priceIsRequired: + if price is None: + raise InvalidOrder(self.id + ' editOrder() requires a price argument for a ' + type + ' order') + request['price'] = self.price_to_precision(symbol, price) + if timeInForceIsRequired and (self.safe_string(params, 'timeInForce') is None): + request['timeInForce'] = self.options['defaultTimeInForce'] # 'GTC' = Good To Cancel(default), 'IOC' = Immediate Or Cancel + if triggerPriceIsRequired: + if triggerPrice is None: + raise InvalidOrder(self.id + ' editOrder() requires a triggerPrice extra param for a ' + type + ' order') + else: + request['stopPrice'] = self.price_to_precision(symbol, triggerPrice) + request['cancelReplaceMode'] = 'STOP_ON_FAILURE' # If the cancel request fails, the new order placement will not be attempted. + cancelId = self.safe_string_2(params, 'cancelNewClientOrderId', 'cancelOrigClientOrderId') + if cancelId is None: + request['cancelOrderId'] = id # user can provide either cancelOrderId, cancelOrigClientOrderId or cancelOrigClientOrderId + # remove timeInForce from params because PO is only used by self.is_post_only and it's not a valid value for Binance + if self.safe_string(params, 'timeInForce') == 'PO': + params = self.omit(params, ['timeInForce']) + params = self.omit(params, ['quoteOrderQty', 'cost', 'stopPrice', 'newClientOrderId', 'clientOrderId', 'postOnly']) + return self.extend(request, params) + + def edit_contract_order_request(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + market = self.market(symbol) + if not market['contract']: + raise NotSupported(self.id + ' editContractOrder() does not support ' + market['type'] + ' orders') + request: dict = { + 'symbol': market['id'], + 'side': side.upper(), + 'orderId': id, + 'quantity': self.amount_to_precision(symbol, amount), + } + clientOrderId = self.safe_string_n(params, ['newClientOrderId', 'clientOrderId', 'origClientOrderId']) + if price is not None: + request['price'] = self.price_to_precision(symbol, price) + if clientOrderId is not None: + request['origClientOrderId'] = clientOrderId + params = self.omit(params, ['clientOrderId', 'newClientOrderId']) + return request + + def edit_contract_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + edit a trade order + + https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Modify-Order + https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/rest-api/Modify-Order + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Modify-UM-Order + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Modify-CM-Order + + :param str id: cancel order id + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.portfolioMargin]: set to True if you would like to edit an order in a portfolio margin account + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + isPortfolioMargin = None + isPortfolioMargin, params = self.handle_option_and_params_2(params, 'editContractOrder', 'papi', 'portfolioMargin', False) + if market['linear'] or isPortfolioMargin: + if (price is None) and not ('priceMatch' in params): + raise ArgumentsRequired(self.id + ' editOrder() requires a price argument for portfolio margin and linear orders') + request = self.edit_contract_order_request(id, symbol, type, side, amount, price, params) + response = None + if market['linear']: + if isPortfolioMargin: + response = self.papiPutUmOrder(self.extend(request, params)) + else: + response = self.fapiPrivatePutOrder(self.extend(request, params)) + elif market['inverse']: + if isPortfolioMargin: + response = self.papiPutCmOrder(self.extend(request, params)) + else: + response = self.dapiPrivatePutOrder(self.extend(request, params)) + # + # swap and future + # + # { + # "orderId": 151007482392, + # "symbol": "BTCUSDT", + # "status": "NEW", + # "clientOrderId": "web_pCCGp9AIHjziKLlpGpXI", + # "price": "25000", + # "avgPrice": "0.00000", + # "origQty": "0.001", + # "executedQty": "0", + # "cumQty": "0", + # "cumQuote": "0", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "reduceOnly": False, + # "closePosition": False, + # "side": "BUY", + # "positionSide": "BOTH", + # "stopPrice": "0", + # "workingType": "CONTRACT_PRICE", + # "priceProtect": False, + # "origType": "LIMIT", + # "updateTime": 1684300587845 + # } + # + return self.parse_order(response, market) + + def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + """ + edit a trade order + + https://developers.binance.com/docs/binance-spot-api-docs/rest-api/trading-endpoints#cancel-an-existing-order-and-send-a-new-order-trade + https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Modify-Order + https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/rest-api/Modify-Order + + :param str id: cancel order id + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + if market['option']: + raise NotSupported(self.id + ' editOrder() does not support ' + market['type'] + ' orders') + if market['spot']: + return self.edit_spot_order(id, symbol, type, side, amount, price, params) + else: + return self.edit_contract_order(id, symbol, type, side, amount, price, params) + + def edit_orders(self, orders: List[OrderRequest], params={}): + """ + edit a list of trade orders + + https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Modify-Multiple-Orders + https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/rest-api/Modify-Multiple-Orders + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + ordersRequests = [] + orderSymbols = [] + for i in range(0, len(orders)): + rawOrder = orders[i] + marketId = self.safe_string(rawOrder, 'symbol') + orderSymbols.append(marketId) + id = self.safe_string(rawOrder, 'id') + type = self.safe_string(rawOrder, 'type') + side = self.safe_string(rawOrder, 'side') + amount = self.safe_value(rawOrder, 'amount') + price = self.safe_value(rawOrder, 'price') + orderParams = self.safe_dict(rawOrder, 'params', {}) + isPortfolioMargin = None + isPortfolioMargin, orderParams = self.handle_option_and_params_2(orderParams, 'editOrders', 'papi', 'portfolioMargin', False) + if isPortfolioMargin: + raise NotSupported(self.id + ' editOrders() does not support portfolio margin orders') + orderRequest = self.edit_contract_order_request(id, marketId, type, side, amount, price, orderParams) + ordersRequests.append(orderRequest) + orderSymbols = self.market_symbols(orderSymbols, None, False, True, True) + market = self.market(orderSymbols[0]) + if market['spot'] or market['option']: + raise NotSupported(self.id + ' editOrders() does not support ' + market['type'] + ' orders') + response = None + request: dict = { + 'batchOrders': ordersRequests, + } + request = self.extend(request, params) + if market['linear']: + response = self.fapiPrivatePutBatchOrders(request) + elif market['inverse']: + response = self.dapiPrivatePutBatchOrders(request) + # + # [ + # { + # "code": -4005, + # "msg": "Quantity greater than max quantity." + # }, + # { + # "orderId": 650640530, + # "symbol": "LTCUSDT", + # "status": "NEW", + # "clientOrderId": "x-xcKtGhcu32184eb13585491289bbaf", + # "price": "54.00", + # "avgPrice": "0.00", + # "origQty": "0.100", + # "executedQty": "0.000", + # "cumQty": "0.000", + # "cumQuote": "0.00000", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "reduceOnly": False, + # "closePosition": False, + # "side": "BUY", + # "positionSide": "BOTH", + # "stopPrice": "0.00", + # "workingType": "CONTRACT_PRICE", + # "priceProtect": False, + # "origType": "LIMIT", + # "priceMatch": "NONE", + # "selfTradePreventionMode": "NONE", + # "goodTillDate": 0, + # "updateTime": 1698073926929 + # } + # ] + # + return self.parse_orders(response) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'NEW': 'open', + 'PARTIALLY_FILLED': 'open', + 'ACCEPTED': 'open', + 'FILLED': 'closed', + 'CANCELED': 'canceled', + 'CANCELLED': 'canceled', + 'PENDING_CANCEL': 'canceling', # currently unused + 'REJECTED': 'rejected', + 'EXPIRED': 'expired', + 'EXPIRED_IN_MATCH': 'expired', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # spot + # + # { + # "symbol": "LTCBTC", + # "orderId": 1, + # "clientOrderId": "myOrder1", + # "price": "0.1", + # "origQty": "1.0", + # "executedQty": "0.0", + # "cummulativeQuoteQty": "0.0", + # "status": "NEW", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "side": "BUY", + # "stopPrice": "0.0", + # "icebergQty": "0.0", + # "time": 1499827319559, + # "updateTime": 1499827319559, + # "isWorking": True + # } + # + # spot: editOrder + # + # { + # "symbol": "BTCUSDT", + # "orderId": 16383176297, + # "orderListId": -1, + # "clientOrderId": "x-TKT5PX2F22ecb58eb9074fb1be018c", + # "transactTime": 1670891847932, + # "price": "13500.00000000", + # "origQty": "0.00085000", + # "executedQty": "0.00000000", + # "cummulativeQuoteQty": "0.00000000", + # "status": "NEW", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "side": "BUY", + # "fills": [] + # } + # + # swap and future: editOrder + # + # { + # "orderId": 151007482392, + # "symbol": "BTCUSDT", + # "status": "NEW", + # "clientOrderId": "web_pCCGp9AIHjziKLlpGpXI", + # "price": "25000", + # "avgPrice": "0.00000", + # "origQty": "0.001", + # "executedQty": "0", + # "cumQty": "0", + # "cumQuote": "0", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "reduceOnly": False, + # "closePosition": False, + # "side": "BUY", + # "positionSide": "BOTH", + # "stopPrice": "0", + # "workingType": "CONTRACT_PRICE", + # "priceProtect": False, + # "origType": "LIMIT", + # "updateTime": 1684300587845 + # } + # + # futures + # + # { + # "symbol": "BTCUSDT", + # "orderId": 1, + # "clientOrderId": "myOrder1", + # "price": "0.1", + # "origQty": "1.0", + # "executedQty": "1.0", + # "cumQuote": "10.0", + # "status": "NEW", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "side": "BUY", + # "stopPrice": "0.0", + # "updateTime": 1499827319559 + # } + # + # createOrder with {"newOrderRespType": "FULL"} + # + # { + # "symbol": "BTCUSDT", + # "orderId": 5403233939, + # "orderListId": -1, + # "clientOrderId": "x-TKT5PX2F5e669e75b6c14f69a2c43e", + # "transactTime": 1617151923742, + # "price": "0.00000000", + # "origQty": "0.00050000", + # "executedQty": "0.00050000", + # "cummulativeQuoteQty": "29.47081500", + # "status": "FILLED", + # "timeInForce": "GTC", + # "type": "MARKET", + # "side": "BUY", + # "fills": [ + # { + # "price": "58941.63000000", + # "qty": "0.00050000", + # "commission": "0.00007050", + # "commissionAsset": "BNB", + # "tradeId": 737466631 + # } + # ] + # } + # + # delivery + # + # { + # "orderId": "18742727411", + # "symbol": "ETHUSD_PERP", + # "pair": "ETHUSD", + # "status": "FILLED", + # "clientOrderId": "x-xcKtGhcu3e2d1503fdd543b3b02419", + # "price": "0", + # "avgPrice": "4522.14", + # "origQty": "1", + # "executedQty": "1", + # "cumBase": "0.00221134", + # "timeInForce": "GTC", + # "type": "MARKET", + # "reduceOnly": False, + # "closePosition": False, + # "side": "SELL", + # "positionSide": "BOTH", + # "stopPrice": "0", + # "workingType": "CONTRACT_PRICE", + # "priceProtect": False, + # "origType": "MARKET", + # "time": "1636061952660", + # "updateTime": "1636061952660" + # } + # + # option: createOrder, fetchOrder, fetchOpenOrders, fetchOrders + # + # { + # "orderId": 4728833085436977152, + # "symbol": "ETH-230211-1500-C", + # "price": "10.0", + # "quantity": "1.00", + # "executedQty": "0.00", + # "fee": "0", + # "side": "BUY", + # "type": "LIMIT", + # "timeInForce": "GTC", + # "reduceOnly": False, + # "postOnly": False, + # "createTime": 1676083034462, + # "updateTime": 1676083034462, + # "status": "ACCEPTED", + # "avgPrice": "0", + # "source": "API", + # "clientOrderId": "", + # "priceScale": 1, + # "quantityScale": 2, + # "optionSide": "CALL", + # "quoteAsset": "USDT", + # "lastTrade": {"id":"69","time":"1676084430567","price":"24.9","qty":"1.00"}, + # "mmp": False + # } + # + # cancelOrders/createOrders + # + # { + # "code": -4005, + # "msg": "Quantity greater than max quantity." + # } + # + # createOrder, fetchOpenOrders, fetchOrder, cancelOrder, fetchOrders: portfolio margin linear swap and future + # + # { + # "symbol": "BTCUSDT", + # "side": "BUY", + # "executedQty": "0.000", + # "orderId": 258649539704, + # "goodTillDate": 0, + # "avgPrice": "0", + # "origQty": "0.010", + # "clientOrderId": "x-xcKtGhcu02573c6f15e544e990057b", + # "positionSide": "BOTH", + # "cumQty": "0.000", + # "updateTime": 1707110415436, + # "type": "LIMIT", + # "reduceOnly": False, + # "price": "35000.00", + # "cumQuote": "0.00000", + # "selfTradePreventionMode": "NONE", + # "timeInForce": "GTC", + # "status": "NEW" + # } + # + # createOrder, fetchOpenOrders, fetchOrder, cancelOrder, fetchOrders: portfolio margin inverse swap and future + # + # { + # "symbol": "ETHUSD_PERP", + # "side": "BUY", + # "cumBase": "0", + # "executedQty": "0", + # "orderId": 71275227732, + # "avgPrice": "0.00", + # "origQty": "1", + # "clientOrderId": "x-xcKtGhcuca5af3acfb5044198c5398", + # "positionSide": "BOTH", + # "cumQty": "0", + # "updateTime": 1707110994334, + # "type": "LIMIT", + # "pair": "ETHUSD", + # "reduceOnly": False, + # "price": "2000", + # "timeInForce": "GTC", + # "status": "NEW" + # } + # + # createOrder, fetchOpenOrders, fetchOpenOrder: portfolio margin linear swap and future conditional + # + # { + # "newClientStrategyId": "x-xcKtGhcu27f109953d6e4dc0974006", + # "strategyId": 3645916, + # "strategyStatus": "NEW", + # "strategyType": "STOP", + # "origQty": "0.010", + # "price": "35000.00", + # "reduceOnly": False, + # "side": "BUY", + # "positionSide": "BOTH", + # "stopPrice": "45000.00", + # "symbol": "BTCUSDT", + # "timeInForce": "GTC", + # "bookTime": 1707112625879, + # "updateTime": 1707112625879, + # "workingType": "CONTRACT_PRICE", + # "priceProtect": False, + # "goodTillDate": 0, + # "selfTradePreventionMode": "NONE" + # } + # + # createOrder, fetchOpenOrders: portfolio margin inverse swap and future conditional + # + # { + # "newClientStrategyId": "x-xcKtGhcuc6b86f053bb34933850739", + # "strategyId": 1423462, + # "strategyStatus": "NEW", + # "strategyType": "STOP", + # "origQty": "1", + # "price": "2000", + # "reduceOnly": False, + # "side": "BUY", + # "positionSide": "BOTH", + # "stopPrice": "3000", + # "symbol": "ETHUSD_PERP", + # "timeInForce": "GTC", + # "bookTime": 1707113098840, + # "updateTime": 1707113098840, + # "workingType": "CONTRACT_PRICE", + # "priceProtect": False + # } + # + # createOrder, cancelAllOrders, cancelOrder: portfolio margin spot margin + # + # { + # "clientOrderId": "x-TKT5PX2Fe9ef29d8346440f0b28b86", + # "cummulativeQuoteQty": "0.00000000", + # "executedQty": "0.00000000", + # "fills": [], + # "orderId": 24684460474, + # "origQty": "0.00100000", + # "price": "35000.00000000", + # "selfTradePreventionMode": "EXPIRE_MAKER", + # "side": "BUY", + # "status": "NEW", + # "symbol": "BTCUSDT", + # "timeInForce": "GTC", + # "transactTime": 1707113538870, + # "type": "LIMIT" + # } + # + # fetchOpenOrders, fetchOrder, fetchOrders: portfolio margin spot margin + # + # { + # "symbol": "BTCUSDT", + # "orderId": 24700763749, + # "clientOrderId": "x-TKT5PX2F6f724c2a4af6425f98c7b6", + # "price": "35000.00000000", + # "origQty": "0.00100000", + # "executedQty": "0.00000000", + # "cummulativeQuoteQty": "0.00000000", + # "status": "NEW", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "side": "BUY", + # "stopPrice": "0.00000000", + # "icebergQty": "0.00000000", + # "time": 1707199187679, + # "updateTime": 1707199187679, + # "isWorking": True, + # "accountId": 200180970, + # "selfTradePreventionMode": "EXPIRE_MAKER", + # "preventedMatchId": null, + # "preventedQuantity": null + # } + # + # cancelOrder: portfolio margin linear and inverse swap conditional + # + # { + # "strategyId": 3733211, + # "newClientStrategyId": "x-xcKtGhcuaf166172ed504cd1bc0396", + # "strategyType": "STOP", + # "strategyStatus": "CANCELED", + # "origQty": "0.010", + # "price": "35000.00", + # "reduceOnly": False, + # "side": "BUY", + # "positionSide": "BOTH", + # "stopPrice": "50000.00", # ignored with trailing orders + # "symbol": "BTCUSDT", + # "timeInForce": "GTC", + # "activatePrice": null, # only return with trailing orders + # "priceRate": null, # only return with trailing orders + # "bookTime": 1707270098774, + # "updateTime": 1707270119261, + # "workingType": "CONTRACT_PRICE", + # "priceProtect": False, + # "goodTillDate": 0, + # "selfTradePreventionMode": "NONE" + # } + # + # fetchOrders: portfolio margin linear and inverse swap conditional + # + # { + # "newClientStrategyId": "x-xcKtGhcuaf166172ed504cd1bc0396", + # "strategyId": 3733211, + # "strategyStatus": "CANCELLED", + # "strategyType": "STOP", + # "origQty": "0.010", + # "price": "35000", + # "orderId": 0, + # "reduceOnly": False, + # "side": "BUY", + # "positionSide": "BOTH", + # "stopPrice": "50000", + # "symbol": "BTCUSDT", + # "type": "LIMIT", + # "bookTime": 1707270098774, + # "updateTime": 1707270119261, + # "timeInForce": "GTC", + # "triggerTime": 0, + # "workingType": "CONTRACT_PRICE", + # "priceProtect": False, + # "goodTillDate": 0, + # "selfTradePreventionMode": "NONE" + # } + # + # fetchOpenOrder: linear swap + # + # { + # "orderId": 3697213934, + # "symbol": "BTCUSDT", + # "status": "NEW", + # "clientOrderId": "x-xcKtGhcufb20c5a7761a4aa09aa156", + # "price": "33000.00", + # "avgPrice": "0.00000", + # "origQty": "0.010", + # "executedQty": "0.000", + # "cumQuote": "0.00000", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "reduceOnly": False, + # "closePosition": False, + # "side": "BUY", + # "positionSide": "BOTH", + # "stopPrice": "0.00", + # "workingType": "CONTRACT_PRICE", + # "priceProtect": False, + # "origType": "LIMIT", + # "priceMatch": "NONE", + # "selfTradePreventionMode": "NONE", + # "goodTillDate": 0, + # "time": 1707892893502, + # "updateTime": 1707892893515 + # } + # + # fetchOpenOrder: inverse swap + # + # { + # "orderId": 597368542, + # "symbol": "BTCUSD_PERP", + # "pair": "BTCUSD", + # "status": "NEW", + # "clientOrderId": "x-xcKtGhcubbde7ba93b1a4ab881eff3", + # "price": "35000", + # "avgPrice": "0", + # "origQty": "1", + # "executedQty": "0", + # "cumBase": "0", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "reduceOnly": False, + # "closePosition": False, + # "side": "BUY", + # "positionSide": "BOTH", + # "stopPrice": "0", + # "workingType": "CONTRACT_PRICE", + # "priceProtect": False, + # "origType": "LIMIT", + # "time": 1707893453199, + # "updateTime": 1707893453199 + # } + # + # fetchOpenOrder: linear portfolio margin + # + # { + # "orderId": 264895013409, + # "symbol": "BTCUSDT", + # "status": "NEW", + # "clientOrderId": "x-xcKtGhcu6278f1adbdf14f74ab432e", + # "price": "35000", + # "avgPrice": "0", + # "origQty": "0.010", + # "executedQty": "0", + # "cumQuote": "0", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "reduceOnly": False, + # "side": "BUY", + # "positionSide": "LONG", + # "origType": "LIMIT", + # "time": 1707893839364, + # "updateTime": 1707893839364, + # "goodTillDate": 0, + # "selfTradePreventionMode": "NONE" + # } + # + # fetchOpenOrder: inverse portfolio margin + # + # { + # "orderId": 71790316950, + # "symbol": "ETHUSD_PERP", + # "pair": "ETHUSD", + # "status": "NEW", + # "clientOrderId": "x-xcKtGhcuec11030474204ab08ba2c2", + # "price": "2500", + # "avgPrice": "0", + # "origQty": "1", + # "executedQty": "0", + # "cumBase": "0", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "reduceOnly": False, + # "side": "BUY", + # "positionSide": "LONG", + # "origType": "LIMIT", + # "time": 1707894181694, + # "updateTime": 1707894181694 + # } + # + # fetchOpenOrder: inverse portfolio margin conditional + # + # { + # "newClientStrategyId": "x-xcKtGhcu2da9c765294b433994ffce", + # "strategyId": 1423501, + # "strategyStatus": "NEW", + # "strategyType": "STOP", + # "origQty": "1", + # "price": "2500", + # "reduceOnly": False, + # "side": "BUY", + # "positionSide": "LONG", + # "stopPrice": "4000", + # "symbol": "ETHUSD_PERP", + # "bookTime": 1707894782679, + # "updateTime": 1707894782679, + # "timeInForce": "GTC", + # "workingType": "CONTRACT_PRICE", + # "priceProtect": False + # } + # + code = self.safe_string(order, 'code') + if code is not None: + # cancelOrders/createOrders might have a partial success + return self.safe_order({'info': order, 'status': 'rejected'}, market) + status = self.parse_order_status(self.safe_string_2(order, 'status', 'strategyStatus')) + marketId = self.safe_string(order, 'symbol') + isContract = ('positionSide' in order) or ('cumQuote' in order) + marketType = 'contract' if isContract else 'spot' + symbol = self.safe_symbol(marketId, market, None, marketType) + filled = self.safe_string(order, 'executedQty', '0') + timestamp = self.safe_integer_n(order, ['time', 'createTime', 'workingTime', 'transactTime', 'updateTime']) # order of the keys matters here + lastTradeTimestamp = None + if ('transactTime' in order) or ('updateTime' in order): + timestampValue = self.safe_integer_2(order, 'updateTime', 'transactTime') + if status == 'open': + if Precise.string_gt(filled, '0'): + lastTradeTimestamp = timestampValue + elif status == 'closed': + lastTradeTimestamp = timestampValue + lastUpdateTimestamp = self.safe_integer_2(order, 'transactTime', 'updateTime') + average = self.safe_string(order, 'avgPrice') + price = self.safe_string(order, 'price') + amount = self.safe_string_2(order, 'origQty', 'quantity') + # - Spot/Margin market: cummulativeQuoteQty + # - Futures market: cumQuote. + # Note self is not the actual cost, since Binance futures uses leverage to calculate margins. + cost = self.safe_string_2(order, 'cummulativeQuoteQty', 'cumQuote') + cost = self.safe_string(order, 'cumBase', cost) + type = self.safe_string_lower(order, 'type') + side = self.safe_string_lower(order, 'side') + fills = self.safe_list(order, 'fills', []) + timeInForce = self.safe_string(order, 'timeInForce') + if timeInForce == 'GTX': + # GTX means "Good Till Crossing" and is an equivalent way of saying Post Only + timeInForce = 'PO' + postOnly = (type == 'limit_maker') or (timeInForce == 'PO') + if type == 'limit_maker': + type = 'limit' + stopPriceString = self.safe_string(order, 'stopPrice') + triggerPrice = self.parse_number(self.omit_zero(stopPriceString)) + feeCost = self.safe_number(order, 'fee') + fee = None + if feeCost is not None: + fee = { + 'currency': self.safe_string(order, 'quoteAsset'), + 'cost': feeCost, + 'rate': None, + } + return self.safe_order({ + 'info': order, + 'id': self.safe_string_2(order, 'strategyId', 'orderId'), + 'clientOrderId': self.safe_string_2(order, 'clientOrderId', 'newClientStrategyId'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': lastTradeTimestamp, + 'lastUpdateTimestamp': lastUpdateTimestamp, + 'symbol': symbol, + 'type': type, + 'timeInForce': timeInForce, + 'postOnly': postOnly, + 'reduceOnly': self.safe_bool(order, 'reduceOnly'), + 'side': side, + 'price': price, + 'triggerPrice': triggerPrice, + 'amount': amount, + 'cost': cost, + 'average': average, + 'filled': filled, + 'remaining': None, + 'status': status, + 'fee': fee, + 'trades': fills, + }, market) + + def create_orders(self, orders: List[OrderRequest], params={}): + """ + *contract only* create a list of trade orders + + https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/rest-api/Place-Multiple-Orders + https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Place-Multiple-Orders + https://developers.binance.com/docs/derivatives/option/trade/Place-Multiple-Orders + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + ordersRequests = [] + orderSymbols = [] + for i in range(0, len(orders)): + rawOrder = orders[i] + marketId = self.safe_string(rawOrder, 'symbol') + orderSymbols.append(marketId) + type = self.safe_string(rawOrder, 'type') + side = self.safe_string(rawOrder, 'side') + amount = self.safe_value(rawOrder, 'amount') + price = self.safe_value(rawOrder, 'price') + orderParams = self.safe_dict(rawOrder, 'params', {}) + orderRequest = self.create_order_request(marketId, type, side, amount, price, orderParams) + ordersRequests.append(orderRequest) + orderSymbols = self.market_symbols(orderSymbols, None, False, True, True) + market = self.market(orderSymbols[0]) + if market['spot']: + raise NotSupported(self.id + ' createOrders() does not support ' + market['type'] + ' orders') + response = None + request: dict = { + 'batchOrders': ordersRequests, + } + request = self.extend(request, params) + if market['linear']: + response = self.fapiPrivatePostBatchOrders(request) + elif market['option']: + response = self.eapiPrivatePostBatchOrders(request) + else: + response = self.dapiPrivatePostBatchOrders(request) + # + # [ + # { + # "code": -4005, + # "msg": "Quantity greater than max quantity." + # }, + # { + # "orderId": 650640530, + # "symbol": "LTCUSDT", + # "status": "NEW", + # "clientOrderId": "x-xcKtGhcu32184eb13585491289bbaf", + # "price": "54.00", + # "avgPrice": "0.00", + # "origQty": "0.100", + # "executedQty": "0.000", + # "cumQty": "0.000", + # "cumQuote": "0.00000", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "reduceOnly": False, + # "closePosition": False, + # "side": "BUY", + # "positionSide": "BOTH", + # "stopPrice": "0.00", + # "workingType": "CONTRACT_PRICE", + # "priceProtect": False, + # "origType": "LIMIT", + # "priceMatch": "NONE", + # "selfTradePreventionMode": "NONE", + # "goodTillDate": 0, + # "updateTime": 1698073926929 + # } + # ] + # + return self.parse_orders(response) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://developers.binance.com/docs/binance-spot-api-docs/rest-api/trading-endpoints#new-order-trade + https://developers.binance.com/docs/binance-spot-api-docs/testnet/rest-api/trading-endpoints#test-new-order-trade + https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/New-Order + https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/rest-api + https://developers.binance.com/docs/derivatives/option/trade/New-Order + https://developers.binance.com/docs/binance-spot-api-docs/rest-api/trading-endpoints#sor + https://developers.binance.com/docs/binance-spot-api-docs/testnet/rest-api/trading-endpoints#sor + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/New-UM-Order + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/New-CM-Order + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/New-Margin-Order + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/New-UM-Conditional-Order + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/New-CM-Conditional-Order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' or 'STOP_LOSS' or 'STOP_LOSS_LIMIT' or 'TAKE_PROFIT' or 'TAKE_PROFIT_LIMIT' or 'STOP' + :param str side: 'buy' or 'sell' + :param float amount: how much of you want to trade in units of the base currency + :param float [price]: the price that the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.reduceOnly]: for swap and future reduceOnly is a string 'true' or 'false' that cant be sent with close position set to True or in hedge mode. For spot margin and option reduceOnly is a boolean. + :param str [params.marginMode]: 'cross' or 'isolated', for spot margin trading + :param boolean [params.sor]: *spot only* whether to use SOR(Smart Order Routing) or not, default is False + :param boolean [params.test]: *spot only* whether to use the test endpoint or not, default is False + :param float [params.trailingPercent]: the percent to trail away from the current market price + :param float [params.trailingTriggerPrice]: the price to trigger a trailing order, default uses the price argument + :param float [params.triggerPrice]: the price that a trigger order is triggered at + :param float [params.stopLossPrice]: the price that a stop loss order is triggered at + :param float [params.takeProfitPrice]: the price that a take profit order is triggered at + :param boolean [params.portfolioMargin]: set to True if you would like to create an order in a portfolio margin account + :param str [params.selfTradePrevention]: set unified value for stp, one of NONE, EXPIRE_MAKER, EXPIRE_TAKER or EXPIRE_BOTH + :param float [params.icebergAmount]: set iceberg amount for limit orders + :param str [params.stopLossOrTakeProfit]: 'stopLoss' or 'takeProfit', required for spot trailing orders + :param str [params.positionSide]: *swap and portfolio margin only* "BOTH" for one-way mode, "LONG" for buy side of hedged mode, "SHORT" for sell side of hedged mode + :param bool [params.hedged]: *swap and portfolio margin only* True for hedged mode, False for one way mode, default is False + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + # don't handle/omit params here, omitting happens inside createOrderRequest + marketType = self.safe_string(params, 'type', market['type']) + marginMode = self.safe_string(params, 'marginMode') + porfolioOptionsValue = self.safe_bool_2(self.options, 'papi', 'portfolioMargin', False) + isPortfolioMargin = self.safe_bool_2(params, 'papi', 'portfolioMargin', porfolioOptionsValue) + triggerPrice = self.safe_string_2(params, 'triggerPrice', 'stopPrice') + stopLossPrice = self.safe_string(params, 'stopLossPrice') + takeProfitPrice = self.safe_string(params, 'takeProfitPrice') + trailingPercent = self.safe_string_2(params, 'trailingPercent', 'callbackRate') + isTrailingPercentOrder = trailingPercent is not None + isStopLoss = stopLossPrice is not None + isTakeProfit = takeProfitPrice is not None + isConditional = (triggerPrice is not None) or isTrailingPercentOrder or isStopLoss or isTakeProfit + sor = self.safe_bool_2(params, 'sor', 'SOR', False) + test = self.safe_bool(params, 'test', False) + params = self.omit(params, ['sor', 'SOR', 'test']) + # if isPortfolioMargin: + # params['portfolioMargin'] = isPortfolioMargin + # } + request = self.create_order_request(symbol, type, side, amount, price, params) + response = None + if market['option']: + response = self.eapiPrivatePostOrder(request) + elif sor: + if test: + response = self.privatePostSorOrderTest(request) + else: + response = self.privatePostSorOrder(request) + elif market['linear']: + if isPortfolioMargin: + if isConditional: + response = self.papiPostUmConditionalOrder(request) + else: + response = self.papiPostUmOrder(request) + else: + response = self.fapiPrivatePostOrder(request) + elif market['inverse']: + if isPortfolioMargin: + if isConditional: + response = self.papiPostCmConditionalOrder(request) + else: + response = self.papiPostCmOrder(request) + else: + response = self.dapiPrivatePostOrder(request) + elif marketType == 'margin' or marginMode is not None or isPortfolioMargin: + if isPortfolioMargin: + response = self.papiPostMarginOrder(request) + else: + response = self.sapiPostMarginOrder(request) + else: + if test: + response = self.privatePostOrderTest(request) + else: + response = self.privatePostOrder(request) + return self.parse_order(response, market) + + def create_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + @ignore + helper function to build the request + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency + :param float [price]: the price that the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: request to be sent to the exchange + """ + market = self.market(symbol) + marketType = self.safe_string(params, 'type', market['type']) + clientOrderId = self.safe_string_2(params, 'newClientOrderId', 'clientOrderId') + initialUppercaseType = type.upper() + isMarketOrder = initialUppercaseType == 'MARKET' + isLimitOrder = initialUppercaseType == 'LIMIT' + request: dict = { + 'symbol': market['id'], + 'side': side.upper(), + } + isPortfolioMargin = None + isPortfolioMargin, params = self.handle_option_and_params_2(params, 'createOrder', 'papi', 'portfolioMargin', False) + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('createOrder', params) + reduceOnly = self.safe_bool(params, 'reduceOnly', False) + if reduceOnly: + if marketType == 'margin' or (not market['contract'] and (marginMode is not None)): + params = self.omit(params, 'reduceOnly') + request['sideEffectType'] = 'AUTO_REPAY' + triggerPrice = self.safe_string_2(params, 'triggerPrice', 'stopPrice') + stopLossPrice = self.safe_string(params, 'stopLossPrice', triggerPrice) # fallback to stopLoss + takeProfitPrice = self.safe_string(params, 'takeProfitPrice') + trailingDelta = self.safe_string(params, 'trailingDelta') + trailingTriggerPrice = self.safe_string_2(params, 'trailingTriggerPrice', 'activationPrice') + trailingPercent = self.safe_string_n(params, ['trailingPercent', 'callbackRate', 'trailingDelta']) + priceMatch = self.safe_string(params, 'priceMatch') + isTrailingPercentOrder = trailingPercent is not None + isStopLoss = stopLossPrice is not None or trailingDelta is not None + isTakeProfit = takeProfitPrice is not None + isTriggerOrder = triggerPrice is not None + isConditional = isTriggerOrder or isTrailingPercentOrder or isStopLoss or isTakeProfit + isPortfolioMarginConditional = (isPortfolioMargin and isConditional) + isPriceMatch = priceMatch is not None + priceRequiredForTrailing = True + uppercaseType = type.upper() + stopPrice = None + if isTrailingPercentOrder: + if market['swap']: + uppercaseType = 'TRAILING_STOP_MARKET' + request['callbackRate'] = trailingPercent + if trailingTriggerPrice is not None: + request['activationPrice'] = self.price_to_precision(symbol, trailingTriggerPrice) + else: + if (uppercaseType != 'STOP_LOSS') and (uppercaseType != 'TAKE_PROFIT') and (uppercaseType != 'STOP_LOSS_LIMIT') and (uppercaseType != 'TAKE_PROFIT_LIMIT'): + stopLossOrTakeProfit = self.safe_string(params, 'stopLossOrTakeProfit') + params = self.omit(params, 'stopLossOrTakeProfit') + if (stopLossOrTakeProfit != 'stopLoss') and (stopLossOrTakeProfit != 'takeProfit'): + raise InvalidOrder(self.id + symbol + ' trailingPercent orders require a stopLossOrTakeProfit parameter of either stopLoss or takeProfit') + if isMarketOrder: + if stopLossOrTakeProfit == 'stopLoss': + uppercaseType = 'STOP_LOSS' + elif stopLossOrTakeProfit == 'takeProfit': + uppercaseType = 'TAKE_PROFIT' + else: + if stopLossOrTakeProfit == 'stopLoss': + uppercaseType = 'STOP_LOSS_LIMIT' + elif stopLossOrTakeProfit == 'takeProfit': + uppercaseType = 'TAKE_PROFIT_LIMIT' + if (uppercaseType == 'STOP_LOSS') or (uppercaseType == 'TAKE_PROFIT'): + priceRequiredForTrailing = False + if trailingTriggerPrice is not None: + stopPrice = self.price_to_precision(symbol, trailingTriggerPrice) + trailingPercentConverted = Precise.string_mul(trailingPercent, '100') + request['trailingDelta'] = trailingPercentConverted + elif isStopLoss: + stopPrice = stopLossPrice + if isMarketOrder: + # spot STOP_LOSS market orders are not a valid order type + uppercaseType = 'STOP_MARKET' if market['contract'] else 'STOP_LOSS' + elif isLimitOrder: + uppercaseType = 'STOP' if market['contract'] else 'STOP_LOSS_LIMIT' + elif isTakeProfit: + stopPrice = takeProfitPrice + if isMarketOrder: + # spot TAKE_PROFIT market orders are not a valid order type + uppercaseType = 'TAKE_PROFIT_MARKET' if market['contract'] else 'TAKE_PROFIT' + elif isLimitOrder: + uppercaseType = 'TAKE_PROFIT' if market['contract'] else 'TAKE_PROFIT_LIMIT' + if market['option']: + if type == 'market': + raise InvalidOrder(self.id + ' ' + type + ' is not a valid order type for the ' + symbol + ' market') + else: + validOrderTypes = self.safe_list(market['info'], 'orderTypes') + if not self.in_array(uppercaseType, validOrderTypes): + if initialUppercaseType != uppercaseType: + raise InvalidOrder(self.id + ' triggerPrice parameter is not allowed for ' + symbol + ' ' + type + ' orders') + else: + raise InvalidOrder(self.id + ' ' + type + ' is not a valid order type for the ' + symbol + ' market') + clientOrderIdRequest = 'newClientStrategyId' if isPortfolioMarginConditional else 'newClientOrderId' + if clientOrderId is None: + broker = self.safe_dict(self.options, 'broker', {}) + defaultId = 'x-xcKtGhcu' if (market['contract']) else 'x-TKT5PX2F' + idMarketType = 'spot' + if market['contract']: + idMarketType = 'swap' if (market['swap'] and market['linear']) else 'inverse' + brokerId = self.safe_string(broker, idMarketType, defaultId) + request[clientOrderIdRequest] = brokerId + self.uuid22() + else: + request[clientOrderIdRequest] = clientOrderId + postOnly = None + if not isPortfolioMargin: + postOnly = self.is_post_only(isMarketOrder, initialUppercaseType == 'LIMIT_MAKER', params) + if market['spot'] or marketType == 'margin': + # only supported for spot/margin api(all margin markets are spot markets) + if postOnly: + uppercaseType = 'LIMIT_MAKER' + if marginMode == 'isolated': + request['isIsolated'] = True + else: + postOnly = self.is_post_only(isMarketOrder, initialUppercaseType == 'LIMIT_MAKER', params) + if postOnly: + if not market['contract']: + uppercaseType = 'LIMIT_MAKER' + else: + request['timeInForce'] = 'GTX' + # handle newOrderRespType response type + if ((marketType == 'spot') or (marketType == 'margin')) and not isPortfolioMargin: + request['newOrderRespType'] = self.safe_string(self.options['newOrderRespType'], type, 'FULL') # 'ACK' for order id, 'RESULT' for full order or 'FULL' for order with fills + else: + # swap, futures and options + request['newOrderRespType'] = 'RESULT' # "ACK", "RESULT", default "ACK" + typeRequest = 'strategyType' if isPortfolioMarginConditional else 'type' + request[typeRequest] = uppercaseType + # additional required fields depending on the order type + closePosition = self.safe_bool(params, 'closePosition', False) + timeInForceIsRequired = False + priceIsRequired = False + triggerPriceIsRequired = False + quantityIsRequired = False + # + # spot/margin + # + # LIMIT timeInForce, quantity, price + # MARKET quantity or quoteOrderQty + # STOP_LOSS quantity, stopPrice + # STOP_LOSS_LIMIT timeInForce, quantity, price, stopPrice + # TAKE_PROFIT quantity, stopPrice + # TAKE_PROFIT_LIMIT timeInForce, quantity, price, stopPrice + # LIMIT_MAKER quantity, price + # + # futures + # + # LIMIT timeInForce, quantity, price + # MARKET quantity + # STOP/TAKE_PROFIT quantity, price, stopPrice + # STOP_MARKET stopPrice + # TAKE_PROFIT_MARKET stopPrice + # TRAILING_STOP_MARKET callbackRate + # + if uppercaseType == 'MARKET': + if market['spot']: + quoteOrderQty = self.safe_bool(self.options, 'quoteOrderQty', True) + if quoteOrderQty: + quoteOrderQtyNew = self.safe_string_2(params, 'quoteOrderQty', 'cost') + precision = market['precision']['price'] + if quoteOrderQtyNew is not None: + request['quoteOrderQty'] = self.decimal_to_precision(quoteOrderQtyNew, TRUNCATE, precision, self.precisionMode) + elif price is not None: + amountString = self.number_to_string(amount) + priceString = self.number_to_string(price) + quoteOrderQuantity = Precise.string_mul(amountString, priceString) + request['quoteOrderQty'] = self.decimal_to_precision(quoteOrderQuantity, TRUNCATE, precision, self.precisionMode) + else: + quantityIsRequired = True + else: + quantityIsRequired = True + else: + quantityIsRequired = True + elif uppercaseType == 'LIMIT': + priceIsRequired = True + timeInForceIsRequired = True + quantityIsRequired = True + elif (uppercaseType == 'STOP_LOSS') or (uppercaseType == 'TAKE_PROFIT'): + triggerPriceIsRequired = True + quantityIsRequired = True + if (market['linear'] or market['inverse']) and priceRequiredForTrailing: + priceIsRequired = True + elif (uppercaseType == 'STOP_LOSS_LIMIT') or (uppercaseType == 'TAKE_PROFIT_LIMIT'): + quantityIsRequired = True + triggerPriceIsRequired = True + priceIsRequired = True + timeInForceIsRequired = True + elif uppercaseType == 'LIMIT_MAKER': + priceIsRequired = True + quantityIsRequired = True + elif uppercaseType == 'STOP': + quantityIsRequired = True + triggerPriceIsRequired = True + priceIsRequired = True + elif (uppercaseType == 'STOP_MARKET') or (uppercaseType == 'TAKE_PROFIT_MARKET'): + if not closePosition: + quantityIsRequired = True + triggerPriceIsRequired = True + elif uppercaseType == 'TRAILING_STOP_MARKET': + if not closePosition: + quantityIsRequired = True + if trailingPercent is None: + raise InvalidOrder(self.id + ' createOrder() requires a trailingPercent param for a ' + type + ' order') + if quantityIsRequired: + marketAmountPrecision = self.safe_string(market['precision'], 'amount') + isPrecisionAvailable = (marketAmountPrecision is not None) + if isPrecisionAvailable: + request['quantity'] = self.amount_to_precision(symbol, amount) + else: + request['quantity'] = self.parse_to_numeric(amount) # some options don't have the precision available + if priceIsRequired and not isPriceMatch: + if price is None: + raise InvalidOrder(self.id + ' createOrder() requires a price argument for a ' + type + ' order') + pricePrecision = self.safe_string(market['precision'], 'price') + isPricePrecisionAvailable = (pricePrecision is not None) + if isPricePrecisionAvailable: + request['price'] = self.price_to_precision(symbol, price) + else: + request['price'] = self.parse_to_numeric(price) # some options don't have the precision available + if triggerPriceIsRequired: + if market['contract']: + if stopPrice is None: + raise InvalidOrder(self.id + ' createOrder() requires a triggerPrice extra param for a ' + type + ' order') + else: + # check for delta price + if trailingDelta is None and stopPrice is None and trailingPercent is None: + raise InvalidOrder(self.id + ' createOrder() requires a triggerPrice, trailingDelta or trailingPercent param for a ' + type + ' order') + if stopPrice is not None: + request['stopPrice'] = self.price_to_precision(symbol, stopPrice) + if timeInForceIsRequired and (self.safe_string(params, 'timeInForce') is None) and (self.safe_string(request, 'timeInForce') is None): + request['timeInForce'] = self.safe_string(self.options, 'defaultTimeInForce') # 'GTC' = Good To Cancel(default), 'IOC' = Immediate Or Cancel + if not isPortfolioMargin and market['contract'] and postOnly: + request['timeInForce'] = 'GTX' + # remove timeInForce from params because PO is only used by self.is_post_only and it's not a valid value for Binance + if self.safe_string(params, 'timeInForce') == 'PO': + params = self.omit(params, 'timeInForce') + hedged = self.safe_bool(params, 'hedged', False) + if not market['spot'] and not market['option'] and hedged: + if reduceOnly: + params = self.omit(params, 'reduceOnly') + side = 'sell' if (side == 'buy') else 'buy' + request['positionSide'] = 'LONG' if (side == 'buy') else 'SHORT' + # unified stp + selfTradePrevention = None + selfTradePrevention, params = self.handle_option_and_params(params, 'createOrder', 'selfTradePrevention') + if selfTradePrevention is not None: + if market['spot']: + request['selfTradePreventionMode'] = selfTradePrevention.upper() # binance enums exactly match the unified ccxt enums(but needs uppercase) + # unified iceberg + icebergAmount = self.safe_number(params, 'icebergAmount') + if icebergAmount is not None: + if market['spot']: + request['icebergQty'] = self.amount_to_precision(symbol, icebergAmount) + requestParams = self.omit(params, ['type', 'newClientOrderId', 'clientOrderId', 'postOnly', 'stopLossPrice', 'takeProfitPrice', 'stopPrice', 'triggerPrice', 'trailingTriggerPrice', 'trailingPercent', 'quoteOrderQty', 'cost', 'test', 'hedged', 'icebergAmount']) + return self.extend(request, requestParams) + + def create_market_order_with_cost(self, symbol: str, side: OrderSide, cost: float, params={}): + """ + create a market order by providing the symbol, side and cost + + https://developers.binance.com/docs/binance-spot-api-docs/rest-api/trading-endpoints#new-order-trade + + :param str symbol: unified symbol of the market to create an order in + :param str side: 'buy' or 'sell' + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' createMarketOrderWithCost() supports spot orders only') + req = { + 'cost': cost, + } + return self.create_order(symbol, 'market', side, cost, None, self.extend(req, params)) + + def create_market_buy_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market buy order by providing the symbol and cost + + https://developers.binance.com/docs/binance-spot-api-docs/rest-api/trading-endpoints#new-order-trade + + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' createMarketBuyOrderWithCost() supports spot orders only') + req = { + 'cost': cost, + } + return self.create_order(symbol, 'market', 'buy', cost, None, self.extend(req, params)) + + def create_market_sell_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market sell order by providing the symbol and cost + + https://developers.binance.com/docs/binance-spot-api-docs/rest-api/trading-endpoints#new-order-trade + + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' createMarketSellOrderWithCost() supports spot orders only') + params['quoteOrderQty'] = cost + return self.create_order(symbol, 'market', 'sell', cost, None, params) + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://developers.binance.com/docs/binance-spot-api-docs/rest-api/trading-endpoints#query-order-user_data + https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Query-Order + https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/rest-api/Query-Order + https://developers.binance.com/docs/derivatives/option/trade/Query-Single-Order + https://developers.binance.com/docs/margin_trading/trade/Query-Margin-Account-Order + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Query-UM-Order + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Query-CM-Order + + :param str id: the order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated', for spot margin trading + :param boolean [params.portfolioMargin]: set to True if you would like to fetch an order in a portfolio margin account + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrder() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + defaultType = self.safe_string_2(self.options, 'fetchOrder', 'defaultType', 'spot') + type = self.safe_string(params, 'type', defaultType) + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchOrder', params) + isPortfolioMargin = None + isPortfolioMargin, params = self.handle_option_and_params_2(params, 'fetchOrder', 'papi', 'portfolioMargin', False) + request: dict = { + 'symbol': market['id'], + } + clientOrderId = self.safe_string_2(params, 'origClientOrderId', 'clientOrderId') + if clientOrderId is not None: + if market['option']: + request['clientOrderId'] = clientOrderId + else: + request['origClientOrderId'] = clientOrderId + else: + request['orderId'] = id + params = self.omit(params, ['type', 'clientOrderId', 'origClientOrderId']) + response = None + if market['option']: + response = self.eapiPrivateGetOrder(self.extend(request, params)) + elif market['linear']: + if isPortfolioMargin: + response = self.papiGetUmOrder(self.extend(request, params)) + else: + response = self.fapiPrivateGetOrder(self.extend(request, params)) + elif market['inverse']: + if isPortfolioMargin: + response = self.papiGetCmOrder(self.extend(request, params)) + else: + response = self.dapiPrivateGetOrder(self.extend(request, params)) + elif (type == 'margin') or (marginMode is not None) or isPortfolioMargin: + if isPortfolioMargin: + response = self.papiGetMarginOrder(self.extend(request, params)) + else: + if marginMode == 'isolated': + request['isIsolated'] = True + response = self.sapiGetMarginOrder(self.extend(request, params)) + else: + response = self.privateGetOrder(self.extend(request, params)) + return self.parse_order(response, market) + + def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://developers.binance.com/docs/binance-spot-api-docs/rest-api/trading-endpoints#all-orders-user_data + https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/All-Orders + https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/rest-api/All-Orders + https://developers.binance.com/docs/derivatives/option/trade/Query-Option-Order-History + https://developers.binance.com/docs/margin_trading/trade/Query-Margin-Account-All-Orders + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Query-All-UM-Orders + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Query-All-CM-Orders + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Query-All-UM-Conditional-Orders + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Query-All-CM-Conditional-Orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated', for spot margin trading + :param int [params.until]: the latest time in ms to fetch orders for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param boolean [params.portfolioMargin]: set to True if you would like to fetch orders in a portfolio margin account + :param boolean [params.trigger]: set to True if you would like to fetch portfolio margin account trigger or conditional orders + :returns Order[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrders() requires a symbol argument') + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOrders', 'paginate') + if paginate: + return self.fetch_paginated_call_dynamic('fetchOrders', symbol, since, limit, params) + market = self.market(symbol) + defaultType = self.safe_string_2(self.options, 'fetchOrders', 'defaultType', market['type']) + type = self.safe_string(params, 'type', defaultType) + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchOrders', params) + isPortfolioMargin = None + isPortfolioMargin, params = self.handle_option_and_params_2(params, 'fetchOrders', 'papi', 'portfolioMargin', False) + isConditional = self.safe_bool_n(params, ['stop', 'trigger', 'conditional']) + params = self.omit(params, ['stop', 'trigger', 'conditional', 'type']) + request: dict = { + 'symbol': market['id'], + } + request, params = self.handle_until_option('endTime', request, params) + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + response = None + if market['option']: + response = self.eapiPrivateGetHistoryOrders(self.extend(request, params)) + elif market['linear']: + if isPortfolioMargin: + if isConditional: + response = self.papiGetUmConditionalAllOrders(self.extend(request, params)) + else: + response = self.papiGetUmAllOrders(self.extend(request, params)) + else: + response = self.fapiPrivateGetAllOrders(self.extend(request, params)) + elif market['inverse']: + if isPortfolioMargin: + if isConditional: + response = self.papiGetCmConditionalAllOrders(self.extend(request, params)) + else: + response = self.papiGetCmAllOrders(self.extend(request, params)) + else: + response = self.dapiPrivateGetAllOrders(self.extend(request, params)) + else: + if isPortfolioMargin: + response = self.papiGetMarginAllOrders(self.extend(request, params)) + elif type == 'margin' or marginMode is not None: + if marginMode == 'isolated': + request['isIsolated'] = True + response = self.sapiGetMarginAllOrders(self.extend(request, params)) + else: + response = self.privateGetAllOrders(self.extend(request, params)) + # + # spot + # + # [ + # { + # "symbol": "LTCBTC", + # "orderId": 1, + # "clientOrderId": "myOrder1", + # "price": "0.1", + # "origQty": "1.0", + # "executedQty": "0.0", + # "cummulativeQuoteQty": "0.0", + # "status": "NEW", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "side": "BUY", + # "stopPrice": "0.0", + # "icebergQty": "0.0", + # "time": 1499827319559, + # "updateTime": 1499827319559, + # "isWorking": True + # } + # ] + # + # futures + # + # [ + # { + # "symbol": "BTCUSDT", + # "orderId": 1, + # "clientOrderId": "myOrder1", + # "price": "0.1", + # "origQty": "1.0", + # "executedQty": "1.0", + # "cumQuote": "10.0", + # "status": "NEW", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "side": "BUY", + # "stopPrice": "0.0", + # "updateTime": 1499827319559 + # } + # ] + # + # options + # + # [ + # { + # "orderId": 4728833085436977152, + # "symbol": "ETH-230211-1500-C", + # "price": "10.0", + # "quantity": "1.00", + # "executedQty": "0.00", + # "fee": "0", + # "side": "BUY", + # "type": "LIMIT", + # "timeInForce": "GTC", + # "reduceOnly": False, + # "postOnly": False, + # "createTime": 1676083034462, + # "updateTime": 1676083034462, + # "status": "ACCEPTED", + # "avgPrice": "0", + # "source": "API", + # "clientOrderId": "", + # "priceScale": 1, + # "quantityScale": 2, + # "optionSide": "CALL", + # "quoteAsset": "USDT", + # "lastTrade": {"id":"69","time":"1676084430567","price":"24.9","qty":"1.00"}, + # "mmp": False + # } + # ] + # + # inverse portfolio margin + # + # [ + # { + # "orderId": 71328442983, + # "symbol": "ETHUSD_PERP", + # "pair": "ETHUSD", + # "status": "CANCELED", + # "clientOrderId": "x-xcKtGhcu4b3e3d8515dd4dc5ba9ccc", + # "price": "2000", + # "avgPrice": "0.00", + # "origQty": "1", + # "executedQty": "0", + # "cumBase": "0", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "reduceOnly": False, + # "side": "BUY", + # "origType": "LIMIT", + # "time": 1707197843046, + # "updateTime": 1707197941373, + # "positionSide": "BOTH" + # }, + # ] + # + # linear portfolio margin + # + # [ + # { + # "orderId": 259235347005, + # "symbol": "BTCUSDT", + # "status": "CANCELED", + # "clientOrderId": "x-xcKtGhcu402881c9103f42bdb4183b", + # "price": "35000", + # "avgPrice": "0.00000", + # "origQty": "0.010", + # "executedQty": "0", + # "cumQuote": "0", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "reduceOnly": False, + # "side": "BUY", + # "origType": "LIMIT", + # "time": 1707194702167, + # "updateTime": 1707197804748, + # "positionSide": "BOTH", + # "selfTradePreventionMode": "NONE", + # "goodTillDate": 0 + # }, + # ] + # + # conditional portfolio margin + # + # [ + # { + # "newClientStrategyId": "x-xcKtGhcuaf166172ed504cd1bc0396", + # "strategyId": 3733211, + # "strategyStatus": "CANCELLED", + # "strategyType": "STOP", + # "origQty": "0.010", + # "price": "35000", + # "orderId": 0, + # "reduceOnly": False, + # "side": "BUY", + # "positionSide": "BOTH", + # "stopPrice": "50000", + # "symbol": "BTCUSDT", + # "type": "LIMIT", + # "bookTime": 1707270098774, + # "updateTime": 1707270119261, + # "timeInForce": "GTC", + # "triggerTime": 0, + # "workingType": "CONTRACT_PRICE", + # "priceProtect": False, + # "goodTillDate": 0, + # "selfTradePreventionMode": "NONE" + # }, + # ] + # + # spot margin portfolio margin + # + # [ + # { + # "symbol": "BTCUSDT", + # "orderId": 24684460474, + # "clientOrderId": "x-TKT5PX2Fe9ef29d8346440f0b28b86", + # "price": "35000.00000000", + # "origQty": "0.00100000", + # "executedQty": "0.00000000", + # "cummulativeQuoteQty": "0.00000000", + # "status": "CANCELED", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "side": "BUY", + # "stopPrice": "0.00000000", + # "icebergQty": "0.00000000", + # "time": 1707113538870, + # "updateTime": 1707113797688, + # "isWorking": True, + # "accountId": 200180970, + # "selfTradePreventionMode": "EXPIRE_MAKER", + # "preventedMatchId": null, + # "preventedQuantity": null + # }, + # ] + # + return self.parse_orders(response, market, since, limit) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://developers.binance.com/docs/binance-spot-api-docs/rest-api/trading-endpoints#current-open-orders-user_data + https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Current-All-Open-Orders + https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/rest-api/Current-All-Open-Orders + https://developers.binance.com/docs/derivatives/option/trade/Query-Current-Open-Option-Orders + https://developers.binance.com/docs/margin_trading/trade/Query-Margin-Account-Open-Orders + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Query-All-Current-UM-Open-Orders + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Query-All-Current-UM-Open-Conditional-Orders + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Query-All-Current-CM-Open-Orders + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Query-All-Current-CM-Open-Conditional-Orders + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated', for spot margin trading + :param boolean [params.portfolioMargin]: set to True if you would like to fetch open orders in the portfolio margin account + :param boolean [params.trigger]: set to True if you would like to fetch portfolio margin account conditional orders + :param str [params.subType]: "linear" or "inverse" + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + market = None + type = None + request: dict = {} + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchOpenOrders', params) + isPortfolioMargin = None + isPortfolioMargin, params = self.handle_option_and_params_2(params, 'fetchOpenOrders', 'papi', 'portfolioMargin', False) + isConditional = self.safe_bool_n(params, ['stop', 'trigger', 'conditional']) + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + defaultType = self.safe_string_2(self.options, 'fetchOpenOrders', 'defaultType', 'spot') + marketType = market['type'] if ('type' in market) else defaultType + type = self.safe_string(params, 'type', marketType) + elif self.options['warnOnFetchOpenOrdersWithoutSymbol']: + raise ExchangeError(self.id + ' fetchOpenOrders() WARNING: fetching open orders without specifying a symbol has stricter rate limits(10 times more for spot, 40 times more for other markets) compared to requesting with symbol argument. To acknowledge self warning, set ' + self.id + '.options["warnOnFetchOpenOrdersWithoutSymbol"] = False to suppress self warning message.') + else: + defaultType = self.safe_string_2(self.options, 'fetchOpenOrders', 'defaultType', 'spot') + type = self.safe_string(params, 'type', defaultType) + subType = None + subType, params = self.handle_sub_type_and_params('fetchOpenOrders', market, params) + params = self.omit(params, ['type', 'stop', 'trigger', 'conditional']) + response = None + if type == 'option': + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + response = self.eapiPrivateGetOpenOrders(self.extend(request, params)) + elif self.is_linear(type, subType): + if isPortfolioMargin: + if isConditional: + response = self.papiGetUmConditionalOpenOrders(self.extend(request, params)) + else: + response = self.papiGetUmOpenOrders(self.extend(request, params)) + else: + response = self.fapiPrivateGetOpenOrders(self.extend(request, params)) + elif self.is_inverse(type, subType): + if isPortfolioMargin: + if isConditional: + response = self.papiGetCmConditionalOpenOrders(self.extend(request, params)) + else: + response = self.papiGetCmOpenOrders(self.extend(request, params)) + else: + response = self.dapiPrivateGetOpenOrders(self.extend(request, params)) + elif type == 'margin' or marginMode is not None or isPortfolioMargin: + if isPortfolioMargin: + response = self.papiGetMarginOpenOrders(self.extend(request, params)) + else: + if marginMode == 'isolated': + request['isIsolated'] = True + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOpenOrders() requires a symbol argument for isolated markets') + response = self.sapiGetMarginOpenOrders(self.extend(request, params)) + else: + response = self.privateGetOpenOrders(self.extend(request, params)) + return self.parse_orders(response, market, since, limit) + + def fetch_open_order(self, id: str, symbol: Str = None, params={}): + """ + fetch an open order by the id + + https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Query-Current-Open-Order + https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/rest-api/Query-Current-Open-Order + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Query-Current-UM-Open-Order + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Query-Current-UM-Open-Conditional-Order + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Query-Current-CM-Open-Order + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Query-Current-CM-Open-Conditional-Order + + :param str id: order id + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.trigger]: set to True if you would like to fetch portfolio margin account stop or conditional orders + :param boolean [params.portfolioMargin]: set to True if you would like to fetch for a portfolio margin account + :returns dict: an `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOpenOrder() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + isPortfolioMargin = None + isPortfolioMargin, params = self.handle_option_and_params_2(params, 'fetchOpenOrder', 'papi', 'portfolioMargin', False) + isConditional = self.safe_bool_n(params, ['stop', 'trigger', 'conditional']) + params = self.omit(params, ['stop', 'trigger', 'conditional']) + isPortfolioMarginConditional = (isPortfolioMargin and isConditional) + orderIdRequest = 'strategyId' if isPortfolioMarginConditional else 'orderId' + request[orderIdRequest] = id + response = None + if market['linear']: + if isPortfolioMargin: + if isConditional: + response = self.papiGetUmConditionalOpenOrder(self.extend(request, params)) + else: + response = self.papiGetUmOpenOrder(self.extend(request, params)) + else: + response = self.fapiPrivateGetOpenOrder(self.extend(request, params)) + elif market['inverse']: + if isPortfolioMargin: + if isConditional: + response = self.papiGetCmConditionalOpenOrder(self.extend(request, params)) + else: + response = self.papiGetCmOpenOrder(self.extend(request, params)) + else: + response = self.dapiPrivateGetOpenOrder(self.extend(request, params)) + else: + if market['option']: + raise NotSupported(self.id + ' fetchOpenOrder() does not support option markets') + elif market['spot']: + raise NotSupported(self.id + ' fetchOpenOrder() does not support spot markets') + # + # linear swap + # + # { + # "orderId": 3697213934, + # "symbol": "BTCUSDT", + # "status": "NEW", + # "clientOrderId": "x-xcKtGhcufb20c5a7761a4aa09aa156", + # "price": "33000.00", + # "avgPrice": "0.00000", + # "origQty": "0.010", + # "executedQty": "0.000", + # "cumQuote": "0.00000", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "reduceOnly": False, + # "closePosition": False, + # "side": "BUY", + # "positionSide": "BOTH", + # "stopPrice": "0.00", + # "workingType": "CONTRACT_PRICE", + # "priceProtect": False, + # "origType": "LIMIT", + # "priceMatch": "NONE", + # "selfTradePreventionMode": "NONE", + # "goodTillDate": 0, + # "time": 1707892893502, + # "updateTime": 1707892893515 + # } + # + # inverse swap + # + # { + # "orderId": 597368542, + # "symbol": "BTCUSD_PERP", + # "pair": "BTCUSD", + # "status": "NEW", + # "clientOrderId": "x-xcKtGhcubbde7ba93b1a4ab881eff3", + # "price": "35000", + # "avgPrice": "0", + # "origQty": "1", + # "executedQty": "0", + # "cumBase": "0", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "reduceOnly": False, + # "closePosition": False, + # "side": "BUY", + # "positionSide": "BOTH", + # "stopPrice": "0", + # "workingType": "CONTRACT_PRICE", + # "priceProtect": False, + # "origType": "LIMIT", + # "time": 1707893453199, + # "updateTime": 1707893453199 + # } + # + # linear portfolio margin + # + # { + # "orderId": 264895013409, + # "symbol": "BTCUSDT", + # "status": "NEW", + # "clientOrderId": "x-xcKtGhcu6278f1adbdf14f74ab432e", + # "price": "35000", + # "avgPrice": "0", + # "origQty": "0.010", + # "executedQty": "0", + # "cumQuote": "0", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "reduceOnly": False, + # "side": "BUY", + # "positionSide": "LONG", + # "origType": "LIMIT", + # "time": 1707893839364, + # "updateTime": 1707893839364, + # "goodTillDate": 0, + # "selfTradePreventionMode": "NONE" + # } + # + # inverse portfolio margin + # + # { + # "orderId": 71790316950, + # "symbol": "ETHUSD_PERP", + # "pair": "ETHUSD", + # "status": "NEW", + # "clientOrderId": "x-xcKtGhcuec11030474204ab08ba2c2", + # "price": "2500", + # "avgPrice": "0", + # "origQty": "1", + # "executedQty": "0", + # "cumBase": "0", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "reduceOnly": False, + # "side": "BUY", + # "positionSide": "LONG", + # "origType": "LIMIT", + # "time": 1707894181694, + # "updateTime": 1707894181694 + # } + # + # linear portfolio margin conditional + # + # { + # "newClientStrategyId": "x-xcKtGhcu2205fde44418483ca21874", + # "strategyId": 4084339, + # "strategyStatus": "NEW", + # "strategyType": "STOP", + # "origQty": "0.010", + # "price": "35000", + # "reduceOnly": False, + # "side": "BUY", + # "positionSide": "LONG", + # "stopPrice": "60000", + # "symbol": "BTCUSDT", + # "bookTime": 1707894490094, + # "updateTime": 1707894490094, + # "timeInForce": "GTC", + # "workingType": "CONTRACT_PRICE", + # "priceProtect": False, + # "goodTillDate": 0, + # "selfTradePreventionMode": "NONE" + # } + # + # inverse portfolio margin conditional + # + # { + # "newClientStrategyId": "x-xcKtGhcu2da9c765294b433994ffce", + # "strategyId": 1423501, + # "strategyStatus": "NEW", + # "strategyType": "STOP", + # "origQty": "1", + # "price": "2500", + # "reduceOnly": False, + # "side": "BUY", + # "positionSide": "LONG", + # "stopPrice": "4000", + # "symbol": "ETHUSD_PERP", + # "bookTime": 1707894782679, + # "updateTime": 1707894782679, + # "timeInForce": "GTC", + # "workingType": "CONTRACT_PRICE", + # "priceProtect": False + # } + # + return self.parse_order(response, market) + + def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://developers.binance.com/docs/binance-spot-api-docs/rest-api/trading-endpoints#all-orders-user_data + https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/All-Orders + https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/rest-api/All-Orders + https://developers.binance.com/docs/derivatives/option/trade/Query-Option-Order-History + https://developers.binance.com/docs/margin_trading/trade/Query-Margin-Account-All-Orders + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Query-All-UM-Orders + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Query-All-CM-Orders + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Query-All-UM-Conditional-Orders + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Query-All-CM-Conditional-Orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param boolean [params.portfolioMargin]: set to True if you would like to fetch orders in a portfolio margin account + :param boolean [params.trigger]: set to True if you would like to fetch portfolio margin account trigger or conditional orders + :returns Order[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchClosedOrders() requires a symbol argument') + orders = self.fetch_orders(symbol, since, None, params) + filteredOrders = self.filter_by(orders, 'status', 'closed') + return self.filter_by_since_limit(filteredOrders, since, limit) + + def fetch_canceled_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches information on multiple canceled orders made by the user + + https://developers.binance.com/docs/binance-spot-api-docs/rest-api/trading-endpoints#all-orders-user_data + https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/All-Orders + https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/rest-api/All-Orders + https://developers.binance.com/docs/derivatives/option/trade/Query-Option-Order-History + https://developers.binance.com/docs/margin_trading/trade/Query-Margin-Account-All-Orders + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Query-All-UM-Orders + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Query-All-CM-Orders + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Query-All-UM-Conditional-Orders + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Query-All-CM-Conditional-Orders + + :param str symbol: unified market symbol of the market the orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param boolean [params.portfolioMargin]: set to True if you would like to fetch orders in a portfolio margin account + :param boolean [params.trigger]: set to True if you would like to fetch portfolio margin account trigger or conditional orders + :returns dict[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchCanceledOrders() requires a symbol argument') + orders = self.fetch_orders(symbol, since, None, params) + filteredOrders = self.filter_by(orders, 'status', 'canceled') + return self.filter_by_since_limit(filteredOrders, since, limit) + + def fetch_canceled_and_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple canceled orders made by the user + + https://developers.binance.com/docs/binance-spot-api-docs/rest-api/trading-endpoints#all-orders-user_data + https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/All-Orders + https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/rest-api/All-Orders + https://developers.binance.com/docs/derivatives/option/trade/Query-Option-Order-History + https://developers.binance.com/docs/margin_trading/trade/Query-Margin-Account-All-Orders + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Query-All-UM-Orders + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Query-All-CM-Orders + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Query-All-UM-Conditional-Orders + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Query-All-CM-Conditional-Orders + + :param str symbol: unified market symbol of the market the orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param boolean [params.portfolioMargin]: set to True if you would like to fetch orders in a portfolio margin account + :param boolean [params.trigger]: set to True if you would like to fetch portfolio margin account trigger or conditional orders + :returns dict[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchCanceledAndClosedOrders() requires a symbol argument') + orders = self.fetch_orders(symbol, since, None, params) + canceledOrders = self.filter_by(orders, 'status', 'canceled') + closedOrders = self.filter_by(orders, 'status', 'closed') + filteredOrders = self.array_concat(canceledOrders, closedOrders) + sortedOrders = self.sort_by(filteredOrders, 'timestamp') + return self.filter_by_since_limit(sortedOrders, since, limit) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://developers.binance.com/docs/binance-spot-api-docs/rest-api/trading-endpoints#cancel-order-trade + https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Cancel-Order + https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/rest-api/Cancel-Order + https://developers.binance.com/docs/derivatives/option/trade/Cancel-Option-Order + https://developers.binance.com/docs/margin_trading/trade/Margin-Account-Cancel-Order + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Cancel-UM-Order + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Cancel-CM-Order + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Cancel-UM-Conditional-Order + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Cancel-CM-Conditional-Order + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Cancel-Margin-Account-Order + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.portfolioMargin]: set to True if you would like to cancel an order in a portfolio margin account + :param boolean [params.trigger]: set to True if you would like to cancel a portfolio margin account conditional order + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + defaultType = self.safe_string_2(self.options, 'cancelOrder', 'defaultType', 'spot') + type = self.safe_string(params, 'type', defaultType) + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('cancelOrder', params) + isPortfolioMargin = None + isPortfolioMargin, params = self.handle_option_and_params_2(params, 'cancelOrder', 'papi', 'portfolioMargin', False) + isConditional = self.safe_bool_n(params, ['stop', 'trigger', 'conditional']) + request: dict = { + 'symbol': market['id'], + } + clientOrderId = self.safe_string_n(params, ['origClientOrderId', 'clientOrderId', 'newClientStrategyId']) + if clientOrderId is not None: + if market['option']: + request['clientOrderId'] = clientOrderId + else: + if isPortfolioMargin and isConditional: + request['newClientStrategyId'] = clientOrderId + else: + request['origClientOrderId'] = clientOrderId + else: + if isPortfolioMargin and isConditional: + request['strategyId'] = id + else: + request['orderId'] = id + params = self.omit(params, ['type', 'origClientOrderId', 'clientOrderId', 'newClientStrategyId', 'stop', 'trigger', 'conditional']) + response = None + if market['option']: + response = self.eapiPrivateDeleteOrder(self.extend(request, params)) + elif market['linear']: + if isPortfolioMargin: + if isConditional: + response = self.papiDeleteUmConditionalOrder(self.extend(request, params)) + else: + response = self.papiDeleteUmOrder(self.extend(request, params)) + else: + response = self.fapiPrivateDeleteOrder(self.extend(request, params)) + elif market['inverse']: + if isPortfolioMargin: + if isConditional: + response = self.papiDeleteCmConditionalOrder(self.extend(request, params)) + else: + response = self.papiDeleteCmOrder(self.extend(request, params)) + else: + response = self.dapiPrivateDeleteOrder(self.extend(request, params)) + elif (type == 'margin') or (marginMode is not None) or isPortfolioMargin: + if isPortfolioMargin: + response = self.papiDeleteMarginOrder(self.extend(request, params)) + else: + if marginMode == 'isolated': + request['isIsolated'] = True + response = self.sapiDeleteMarginOrder(self.extend(request, params)) + else: + response = self.privateDeleteOrder(self.extend(request, params)) + return self.parse_order(response, market) + + def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders in a market + + https://developers.binance.com/docs/binance-spot-api-docs/rest-api/trading-endpoints#cancel-all-open-orders-on-a-symbol-trade + https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Cancel-All-Open-Orders + https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/rest-api/Cancel-All-Open-Orders + https://developers.binance.com/docs/derivatives/option/trade/Cancel-all-Option-orders-on-specific-symbol + https://developers.binance.com/docs/margin_trading/trade/Margin-Account-Cancel-All-Open-Orders + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Cancel-All-UM-Open-Orders + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Cancel-All-UM-Open-Conditional-Orders + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Cancel-All-CM-Open-Orders + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Cancel-All-CM-Open-Conditional-Orders + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Cancel-Margin-Account-All-Open-Orders-on-a-Symbol + + :param str symbol: unified market symbol of the market to cancel orders in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated', for spot margin trading + :param boolean [params.portfolioMargin]: set to True if you would like to cancel orders in a portfolio margin account + :param boolean [params.trigger]: set to True if you would like to cancel portfolio margin account conditional orders + :returns dict[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelAllOrders() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + isPortfolioMargin = None + isPortfolioMargin, params = self.handle_option_and_params_2(params, 'cancelAllOrders', 'papi', 'portfolioMargin', False) + isConditional = self.safe_bool_n(params, ['stop', 'trigger', 'conditional']) + type = self.safe_string(params, 'type', market['type']) + params = self.omit(params, ['type', 'stop', 'trigger', 'conditional']) + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('cancelAllOrders', params) + response = None + if market['option']: + response = self.eapiPrivateDeleteAllOpenOrders(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "success" + # } + # + elif market['linear']: + if isPortfolioMargin: + if isConditional: + response = self.papiDeleteUmConditionalAllOpenOrders(self.extend(request, params)) + # + # { + # "code": "200", + # "msg": "The operation of cancel all conditional open order is done." + # } + # + else: + response = self.papiDeleteUmAllOpenOrders(self.extend(request, params)) + # + # { + # "code": 200, + # "msg": "The operation of cancel all open order is done." + # } + # + else: + response = self.fapiPrivateDeleteAllOpenOrders(self.extend(request, params)) + # + # { + # "code": 200, + # "msg": "The operation of cancel all open order is done." + # } + # + elif market['inverse']: + if isPortfolioMargin: + if isConditional: + response = self.papiDeleteCmConditionalAllOpenOrders(self.extend(request, params)) + # + # { + # "code": "200", + # "msg": "The operation of cancel all conditional open order is done." + # } + # + else: + response = self.papiDeleteCmAllOpenOrders(self.extend(request, params)) + # + # { + # "code": 200, + # "msg": "The operation of cancel all open order is done." + # } + # + else: + response = self.dapiPrivateDeleteAllOpenOrders(self.extend(request, params)) + # + # { + # "code": 200, + # "msg": "The operation of cancel all open order is done." + # } + # + elif (type == 'margin') or (marginMode is not None) or isPortfolioMargin: + if isPortfolioMargin: + response = self.papiDeleteMarginAllOpenOrders(self.extend(request, params)) + else: + if marginMode == 'isolated': + request['isIsolated'] = True + response = self.sapiDeleteMarginOpenOrders(self.extend(request, params)) + # + # [ + # { + # "symbol": "BTCUSDT", + # "isIsolated": True, # if isolated margin + # "origClientOrderId": "E6APeyTJvkMvLMYMqu1KQ4", + # "orderId": 11, + # "orderListId": -1, + # "clientOrderId": "pXLV6Hz6mprAcVYpVMTGgx", + # "price": "0.089853", + # "origQty": "0.178622", + # "executedQty": "0.000000", + # "cummulativeQuoteQty": "0.000000", + # "status": "CANCELED", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "side": "BUY", + # "selfTradePreventionMode": "NONE" + # }, + # ... + # ] + # + else: + response = self.privateDeleteOpenOrders(self.extend(request, params)) + # + # [ + # { + # "symbol": "ADAUSDT", + # "origClientOrderId": "x-TKT5PX2F662cde7a90114475b86e21", + # "orderId": 3935107, + # "orderListId": -1, + # "clientOrderId": "bqM2w1oTlugfRAjnTIFBE8", + # "transactTime": 1720589016657, + # "price": "0.35000000", + # "origQty": "30.00000000", + # "executedQty": "0.00000000", + # "cummulativeQuoteQty": "0.00000000", + # "status": "CANCELED", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "side": "BUY", + # "selfTradePreventionMode": "EXPIRE_MAKER" + # } + # ] + # + if isinstance(response, list): + return self.parse_orders(response, market) + else: + return [ + self.safe_order({ + 'info': response, + }), + ] + + def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): + """ + cancel multiple orders + + https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Cancel-Multiple-Orders + https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/rest-api/Cancel-Multiple-Orders + + :param str[] ids: order ids + :param str [symbol]: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str[] [params.clientOrderIds]: alternative to ids, array of client order ids + + EXCHANGE SPECIFIC PARAMETERS + :param str[] [params.origClientOrderIdList]: max length 10 e.g. ["my_id_1","my_id_2"], encode the double quotes. No space after comma + :param int[] [params.recvWindow]: + :returns dict: an list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrders() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + if not market['contract']: + raise BadRequest(self.id + ' cancelOrders is only supported for swap markets.') + request: dict = { + 'symbol': market['id'], + # 'orderidlist': ids, + } + origClientOrderIdList = self.safe_list_2(params, 'origClientOrderIdList', 'clientOrderIds') + if origClientOrderIdList is not None: + params = self.omit(params, ['clientOrderIds']) + request['origClientOrderIdList'] = origClientOrderIdList + else: + request['orderidlist'] = ids + response = None + if market['linear']: + response = self.fapiPrivateDeleteBatchOrders(self.extend(request, params)) + elif market['inverse']: + response = self.dapiPrivateDeleteBatchOrders(self.extend(request, params)) + # + # [ + # { + # "clientOrderId": "myOrder1", + # "cumQty": "0", + # "cumQuote": "0", + # "executedQty": "0", + # "orderId": 283194212, + # "origQty": "11", + # "origType": "TRAILING_STOP_MARKET", + # "price": "0", + # "reduceOnly": False, + # "side": "BUY", + # "positionSide": "SHORT", + # "status": "CANCELED", + # "stopPrice": "9300", # please ignore when order type is TRAILING_STOP_MARKET + # "closePosition": False, # if Close-All + # "symbol": "BTCUSDT", + # "timeInForce": "GTC", + # "type": "TRAILING_STOP_MARKET", + # "activatePrice": "9020", # activation price, only return with TRAILING_STOP_MARKET order + # "priceRate": "0.3", # callback rate, only return with TRAILING_STOP_MARKET order + # "updateTime": 1571110484038, + # "workingType": "CONTRACT_PRICE", + # "priceProtect": False, # if conditional order trigger is protected + # "priceMatch": "NONE", # price match mode + # "selfTradePreventionMode": "NONE", # self trading preventation mode + # "goodTillDate": 0 # order pre-set auot cancel time for TIF GTD order + # }, + # { + # "code": -2011, + # "msg": "Unknown order sent." + # } + # ] + # + return self.parse_orders(response, market) + + def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all the trades made from a single order + + https://developers.binance.com/docs/binance-spot-api-docs/rest-api/account-endpoints#account-trade-list-user_data + https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Account-Trade-List + https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/rest-api/Account-Trade-List + https://developers.binance.com/docs/margin_trading/trade/Query-Margin-Account-Trade-List + + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrderTrades() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + type = self.safe_string(params, 'type', market['type']) + params = self.omit(params, 'type') + if type != 'spot': + raise NotSupported(self.id + ' fetchOrderTrades() supports spot markets only') + request: dict = { + 'orderId': id, + } + return self.fetch_my_trades(symbol, since, limit, self.extend(request, params)) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://developers.binance.com/docs/binance-spot-api-docs/rest-api/account-endpoints#account-trade-list-user_data + https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Account-Trade-List + https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/rest-api/Account-Trade-List + https://developers.binance.com/docs/margin_trading/trade/Query-Margin-Account-Trade-List + https://developers.binance.com/docs/derivatives/option/trade/Account-Trade-List + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/UM-Account-Trade-List + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/CM-Account-Trade-List + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param int [params.until]: the latest time in ms to fetch entries for + :param boolean [params.portfolioMargin]: set to True if you would like to fetch trades for a portfolio margin account + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchMyTrades', 'paginate') + if paginate: + return self.fetch_paginated_call_dynamic('fetchMyTrades', symbol, since, limit, params) + request: dict = {} + market = None + type = None + marginMode = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + type, params = self.handle_market_type_and_params('fetchMyTrades', market, params) + endTime = self.safe_integer_2(params, 'until', 'endTime') + if since is not None: + startTime = since + request['startTime'] = startTime + # If startTime and endTime are both not sent, then the last 7 days' data will be returned. + # The time between startTime and endTime cannot be longer than 7 days. + # The parameter fromId cannot be sent with startTime or endTime. + currentTimestamp = self.milliseconds() + oneWeek = 7 * 24 * 60 * 60 * 1000 + if (currentTimestamp - startTime) >= oneWeek: + if (endTime is None) and market['linear']: + endTime = self.sum(startTime, oneWeek) + endTime = min(endTime, currentTimestamp) + if endTime is not None: + request['endTime'] = endTime + params = self.omit(params, ['endTime', 'until']) + if limit is not None: + if (type == 'option') or market['contract']: + limit = min(limit, 1000) # above 1000, returns error + request['limit'] = limit + response = None + if type == 'option': + response = self.eapiPrivateGetUserTrades(self.extend(request, params)) + else: + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMyTrades() requires a symbol argument') + marginMode, params = self.handle_margin_mode_and_params('fetchMyTrades', params) + isPortfolioMargin = None + isPortfolioMargin, params = self.handle_option_and_params_2(params, 'fetchMyTrades', 'papi', 'portfolioMargin', False) + if type == 'spot' or type == 'margin': + if isPortfolioMargin: + response = self.papiGetMarginMyTrades(self.extend(request, params)) + elif (type == 'margin') or (marginMode is not None): + if marginMode == 'isolated': + request['isIsolated'] = True + response = self.sapiGetMarginMyTrades(self.extend(request, params)) + else: + response = self.privateGetMyTrades(self.extend(request, params)) + elif market['linear']: + if isPortfolioMargin: + response = self.papiGetUmUserTrades(self.extend(request, params)) + else: + response = self.fapiPrivateGetUserTrades(self.extend(request, params)) + elif market['inverse']: + if isPortfolioMargin: + response = self.papiGetCmUserTrades(self.extend(request, params)) + else: + response = self.dapiPrivateGetUserTrades(self.extend(request, params)) + # + # spot trade + # + # [ + # { + # "symbol": "BNBBTC", + # "id": 28457, + # "orderId": 100234, + # "price": "4.00000100", + # "qty": "12.00000000", + # "commission": "10.10000000", + # "commissionAsset": "BNB", + # "time": 1499865549590, + # "isBuyer": True, + # "isMaker": False, + # "isBestMatch": True, + # } + # ] + # + # futures trade + # + # [ + # { + # "accountId": 20, + # "buyer": False, + # "commission": "-0.07819010", + # "commissionAsset": "USDT", + # "counterPartyId": 653, + # "id": 698759, + # "maker": False, + # "orderId": 25851813, + # "price": "7819.01", + # "qty": "0.002", + # "quoteQty": "0.01563", + # "realizedPnl": "-0.91539999", + # "side": "SELL", + # "symbol": "BTCUSDT", + # "time": 1569514978020 + # } + # ] + # + # options(eapi) + # + # [ + # { + # "id": 1125899906844226012, + # "tradeId": 73, + # "orderId": 4638761100843040768, + # "symbol": "ETH-230211-1500-C", + # "price": "18.70000000", + # "quantity": "-0.57000000", + # "fee": "0.17305890", + # "realizedProfit": "-3.53400000", + # "side": "SELL", + # "type": "LIMIT", + # "volatility": "0.30000000", + # "liquidity": "MAKER", + # "time": 1676085216845, + # "priceScale": 1, + # "quantityScale": 2, + # "optionSide": "CALL", + # "quoteAsset": "USDT" + # } + # ] + # + # linear portfolio margin + # + # [ + # { + # "symbol": "BTCUSDT", + # "id": 4575108247, + # "orderId": 261942655610, + # "side": "SELL", + # "price": "47263.40", + # "qty": "0.010", + # "realizedPnl": "27.38400000", + # "marginAsset": "USDT", + # "quoteQty": "472.63", + # "commission": "0.18905360", + # "commissionAsset": "USDT", + # "time": 1707530039409, + # "buyer": False, + # "maker": False, + # "positionSide": "LONG" + # } + # ] + # + # inverse portfolio margin + # + # [ + # { + # "symbol": "ETHUSD_PERP", + # "id": 701907838, + # "orderId": 71548909034, + # "pair": "ETHUSD", + # "side": "SELL", + # "price": "2498.15", + # "qty": "1", + # "realizedPnl": "0.00012517", + # "marginAsset": "ETH", + # "baseQty": "0.00400296", + # "commission": "0.00000160", + # "commissionAsset": "ETH", + # "time": 1707530317519, + # "positionSide": "LONG", + # "buyer": False, + # "maker": False + # } + # ] + # + # spot margin portfolio margin + # + # [ + # { + # "symbol": "ADAUSDT", + # "id": 470227543, + # "orderId": 4421170947, + # "price": "0.53880000", + # "qty": "10.00000000", + # "quoteQty": "5.38800000", + # "commission": "0.00538800", + # "commissionAsset": "USDT", + # "time": 1707545780522, + # "isBuyer": False, + # "isMaker": False, + # "isBestMatch": True + # } + # ] + # + return self.parse_trades(response, market, since, limit) + + def fetch_my_dust_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all dust trades made by the user + + https://developers.binance.com/docs/wallet/asset/dust-log + + :param str symbol: not used by binance fetchMyDustTrades() + :param int [since]: the earliest time in ms to fetch my dust trades for + :param int [limit]: the maximum number of dust trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: 'spot' or 'margin', default spot + :returns dict[]: a list of `trade structures ` + """ + # + # Binance provides an opportunity to trade insignificant(i.e. non-tradable and non-withdrawable) + # token leftovers(of any asset) into `BNB` coin which in turn can be used to pay trading fees with it. + # The corresponding trades history is called the `Dust Log` and can be requested via the following end-point: + # https://github.com/binance-exchange/binance-official-api-docs/blob/master/wapi-api.md#dustlog-user_data + # + self.load_markets() + request: dict = {} + if since is not None: + request['startTime'] = since + request['endTime'] = self.sum(since, 7776000000) + accountType = self.safe_string_upper(params, 'type') + params = self.omit(params, 'type') + if accountType is not None: + request['accountType'] = accountType + response = self.sapiGetAssetDribblet(self.extend(request, params)) + # { + # "total": "4", + # "userAssetDribblets": [ + # { + # "operateTime": "1627575731000", + # "totalServiceChargeAmount": "0.00001453", + # "totalTransferedAmount": "0.00072693", + # "transId": "70899815863", + # "userAssetDribbletDetails": [ + # { + # "fromAsset": "LTC", + # "amount": "0.000006", + # "transferedAmount": "0.00000267", + # "serviceChargeAmount": "0.00000005", + # "operateTime": "1627575731000", + # "transId": "70899815863" + # }, + # { + # "fromAsset": "GBP", + # "amount": "0.15949157", + # "transferedAmount": "0.00072426", + # "serviceChargeAmount": "0.00001448", + # "operateTime": "1627575731000", + # "transId": "70899815863" + # } + # ] + # }, + # ] + # } + results = self.safe_list(response, 'userAssetDribblets', []) + rows = self.safe_integer(response, 'total', 0) + data = [] + for i in range(0, rows): + logs = self.safe_list(results[i], 'userAssetDribbletDetails', []) + for j in range(0, len(logs)): + logs[j]['isDustTrade'] = True + data.append(logs[j]) + trades = self.parse_trades(data, None, since, limit) + return self.filter_by_since_limit(trades, since, limit) + + def parse_dust_trade(self, trade, market: Market = None): + # + # { + # "fromAsset": "USDT", + # "amount": "0.009669", + # "transferedAmount": "0.00002992", + # "serviceChargeAmount": "0.00000059", + # "operateTime": "1628076010000", + # "transId": "71416578712", + # "isDustTrade": True + # } + # + orderId = self.safe_string(trade, 'transId') + timestamp = self.safe_integer(trade, 'operateTime') + currencyId = self.safe_string(trade, 'fromAsset') + tradedCurrency = self.safe_currency_code(currencyId) + bnb = self.currency('BNB') + earnedCurrency = bnb['code'] + applicantSymbol = earnedCurrency + '/' + tradedCurrency + tradedCurrencyIsQuote = False + if applicantSymbol in self.markets: + tradedCurrencyIsQuote = True + feeCostString = self.safe_string(trade, 'serviceChargeAmount') + fee = { + 'currency': earnedCurrency, + 'cost': self.parse_number(feeCostString), + } + symbol = None + amountString = None + costString = None + side = None + if tradedCurrencyIsQuote: + symbol = applicantSymbol + amountString = self.safe_string(trade, 'transferedAmount') + costString = self.safe_string(trade, 'amount') + side = 'buy' + else: + symbol = tradedCurrency + '/' + earnedCurrency + amountString = self.safe_string(trade, 'amount') + costString = self.safe_string(trade, 'transferedAmount') + side = 'sell' + priceString = None + if costString is not None: + if amountString: + priceString = Precise.string_div(costString, amountString) + id = None + amount = self.parse_number(amountString) + price = self.parse_number(priceString) + cost = self.parse_number(costString) + type = None + takerOrMaker = None + return { + 'id': id, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'order': orderId, + 'type': type, + 'takerOrMaker': takerOrMaker, + 'side': side, + 'amount': amount, + 'price': price, + 'cost': cost, + 'fee': fee, + 'info': trade, + } + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://developers.binance.com/docs/wallet/capital/deposite-history + https://developers.binance.com/docs/fiat/rest-api/Get-Fiat-Deposit-Withdraw-History + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.fiat]: if True, only fiat deposits will be returned + :param int [params.until]: the latest time in ms to fetch entries for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchDeposits', 'paginate') + if paginate: + return self.fetch_paginated_call_dynamic('fetchDeposits', code, since, limit, params) + currency = None + response = None + request: dict = {} + legalMoney = self.safe_dict(self.options, 'legalMoney', {}) + fiatOnly = self.safe_bool(params, 'fiat', False) + params = self.omit(params, 'fiatOnly') + until = self.safe_integer(params, 'until') + params = self.omit(params, 'until') + if fiatOnly or (code in legalMoney): + if code is not None: + currency = self.currency(code) + request['transactionType'] = 0 + if since is not None: + request['beginTime'] = since + if until is not None: + request['endTime'] = until + raw = self.sapiGetFiatOrders(self.extend(request, params)) + response = self.safe_list(raw, 'data', []) + # { + # "code": "000000", + # "message": "success", + # "data": [ + # { + # "orderNo": "25ced37075c1470ba8939d0df2316e23", + # "fiatCurrency": "EUR", + # "indicatedAmount": "15.00", + # "amount": "15.00", + # "totalFee": "0.00", + # "method": "card", + # "status": "Failed", + # "createTime": 1627501026000, + # "updateTime": 1627501027000 + # } + # ], + # "total": 1, + # "success": True + # } + else: + if code is not None: + currency = self.currency(code) + request['coin'] = currency['id'] + if since is not None: + request['startTime'] = since + # max 3 months range https://github.com/ccxt/ccxt/issues/6495 + endTime = self.sum(since, 7776000000) + if until is not None: + endTime = min(endTime, until) + request['endTime'] = endTime + if limit is not None: + request['limit'] = limit + response = self.sapiGetCapitalDepositHisrec(self.extend(request, params)) + # [ + # { + # "amount": "0.01844487", + # "coin": "BCH", + # "network": "BCH", + # "status": 1, + # "address": "1NYxAJhW2281HK1KtJeaENBqHeygA88FzR", + # "addressTag": "", + # "txId": "bafc5902504d6504a00b7d0306a41154cbf1d1b767ab70f3bc226327362588af", + # "insertTime": 1610784980000, + # "transferType": 0, + # "confirmTimes": "2/2" + # }, + # { + # "amount": "4500", + # "coin": "USDT", + # "network": "BSC", + # "status": 1, + # "address": "0xc9c923c87347ca0f3451d6d308ce84f691b9f501", + # "addressTag": "", + # "txId": "Internal transfer 51376627901", + # "insertTime": 1618394381000, + # "transferType": 1, + # "confirmTimes": "1/15" + # } + # ] + for i in range(0, len(response)): + response[i]['type'] = 'deposit' + return self.parse_transactions(response, currency, since, limit) + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://developers.binance.com/docs/wallet/capital/withdraw-history + https://developers.binance.com/docs/fiat/rest-api/Get-Fiat-Deposit-Withdraw-History + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.fiat]: if True, only fiat withdrawals will be returned + :param int [params.until]: the latest time in ms to fetch withdrawals for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchWithdrawals', 'paginate') + if paginate: + return self.fetch_paginated_call_dynamic('fetchWithdrawals', code, since, limit, params) + legalMoney = self.safe_dict(self.options, 'legalMoney', {}) + fiatOnly = self.safe_bool(params, 'fiat', False) + params = self.omit(params, 'fiatOnly') + request: dict = {} + until = self.safe_integer(params, 'until') + if until is not None: + params = self.omit(params, 'until') + request['endTime'] = until + response = None + currency = None + if fiatOnly or (code in legalMoney): + if code is not None: + currency = self.currency(code) + request['transactionType'] = 1 + if since is not None: + request['beginTime'] = since + raw = self.sapiGetFiatOrders(self.extend(request, params)) + response = self.safe_list(raw, 'data', []) + # { + # "code": "000000", + # "message": "success", + # "data": [ + # { + # "orderNo": "CJW706452266115170304", + # "fiatCurrency": "GBP", + # "indicatedAmount": "10001.50", + # "amount": "100.00", + # "totalFee": "1.50", + # "method": "bank transfer", + # "status": "Successful", + # "createTime": 1620037745000, + # "updateTime": 1620038480000 + # }, + # { + # "orderNo": "CJW706287492781891584", + # "fiatCurrency": "GBP", + # "indicatedAmount": "10001.50", + # "amount": "100.00", + # "totalFee": "1.50", + # "method": "bank transfer", + # "status": "Successful", + # "createTime": 1619998460000, + # "updateTime": 1619998823000 + # } + # ], + # "total": 39, + # "success": True + # } + else: + if code is not None: + currency = self.currency(code) + request['coin'] = currency['id'] + if since is not None: + request['startTime'] = since + # max 3 months range https://github.com/ccxt/ccxt/issues/6495 + request['endTime'] = self.sum(since, 7776000000) + if limit is not None: + request['limit'] = limit + response = self.sapiGetCapitalWithdrawHistory(self.extend(request, params)) + # [ + # { + # "id": "69e53ad305124b96b43668ceab158a18", + # "amount": "28.75", + # "transactionFee": "0.25", + # "coin": "XRP", + # "status": 6, + # "address": "r3T75fuLjX51mmfb5Sk1kMNuhBgBPJsjza", + # "addressTag": "101286922", + # "txId": "19A5B24ED0B697E4F0E9CD09FCB007170A605BC93C9280B9E6379C5E6EF0F65A", + # "applyTime": "2021-04-15 12:09:16", + # "network": "XRP", + # "transferType": 0 + # }, + # { + # "id": "9a67628b16ba4988ae20d329333f16bc", + # "amount": "20", + # "transactionFee": "20", + # "coin": "USDT", + # "status": 6, + # "address": "0x0AB991497116f7F5532a4c2f4f7B1784488628e1", + # "txId": "0x77fbf2cf2c85b552f0fd31fd2e56dc95c08adae031d96f3717d8b17e1aea3e46", + # "applyTime": "2021-04-15 12:06:53", + # "network": "ETH", + # "transferType": 0 + # }, + # { + # "id": "a7cdc0afbfa44a48bd225c9ece958fe2", + # "amount": "51", + # "transactionFee": "1", + # "coin": "USDT", + # "status": 6, + # "address": "TYDmtuWL8bsyjvcauUTerpfYyVhFtBjqyo", + # "txId": "168a75112bce6ceb4823c66726ad47620ad332e69fe92d9cb8ceb76023f9a028", + # "applyTime": "2021-04-13 12:46:59", + # "network": "TRX", + # "transferType": 0 + # } + # ] + for i in range(0, len(response)): + response[i]['type'] = 'withdrawal' + return self.parse_transactions(response, currency, since, limit) + + def parse_transaction_status_by_type(self, status, type=None): + if type is None: + return status + statusesByType: dict = { + 'deposit': { + '0': 'pending', + '1': 'ok', + '6': 'ok', + # Fiat + # Processing, Failed, Successful, Finished, Refunding, Refunded, Refund Failed, Order Partial credit Stopped + 'Processing': 'pending', + 'Failed': 'failed', + 'Successful': 'ok', + 'Refunding': 'canceled', + 'Refunded': 'canceled', + 'Refund Failed': 'failed', + }, + 'withdrawal': { + '0': 'pending', # Email Sent + '1': 'canceled', # Cancelled(different from 1 = ok in deposits) + '2': 'pending', # Awaiting Approval + '3': 'failed', # Rejected + '4': 'pending', # Processing + '5': 'failed', # Failure + '6': 'ok', # Completed + # Fiat + # Processing, Failed, Successful, Finished, Refunding, Refunded, Refund Failed, Order Partial credit Stopped + 'Processing': 'pending', + 'Failed': 'failed', + 'Successful': 'ok', + 'Refunding': 'canceled', + 'Refunded': 'canceled', + 'Refund Failed': 'failed', + }, + } + statuses = self.safe_dict(statusesByType, type, {}) + return self.safe_string(statuses, status, status) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fetchDeposits + # + # { + # "amount": "4500", + # "coin": "USDT", + # "network": "BSC", + # "status": 1, + # "address": "0xc9c923c87347ca0f3451d6d308ce84f691b9f501", + # "addressTag": "", + # "txId": "Internal transfer 51376627901", + # "insertTime": 1618394381000, + # "transferType": 1, + # "confirmTimes": "1/15" + # } + # + # fetchWithdrawals + # + # { + # "id": "69e53ad305124b96b43668ceab158a18", + # "amount": "28.75", + # "transactionFee": "0.25", + # "coin": "XRP", + # "status": 6, + # "address": "r3T75fuLjX51mmfb5Sk1kMNuhBgBPJsjza", + # "addressTag": "101286922", + # "txId": "19A5B24ED0B697E4F0E9CD09FCB007170A605BC93C9280B9E6379C5E6EF0F65A", + # "applyTime": "2021-04-15 12:09:16", + # "network": "XRP", + # "transferType": 0 + # } + # + # fiat transaction + # withdraw + # { + # "orderNo": "CJW684897551397171200", + # "fiatCurrency": "GBP", + # "indicatedAmount": "29.99", + # "amount": "28.49", + # "totalFee": "1.50", + # "method": "bank transfer", + # "status": "Successful", + # "createTime": 1614898701000, + # "updateTime": 1614898820000 + # } + # + # deposit + # { + # "orderNo": "25ced37075c1470ba8939d0df2316e23", + # "fiatCurrency": "EUR", + # "transactionType": 0, + # "indicatedAmount": "15.00", + # "amount": "15.00", + # "totalFee": "0.00", + # "method": "card", + # "status": "Failed", + # "createTime": "1627501026000", + # "updateTime": "1627501027000" + # } + # + # withdraw + # + # {id: "9a67628b16ba4988ae20d329333f16bc"} + # + id = self.safe_string_2(transaction, 'id', 'orderNo') + address = self.safe_string(transaction, 'address') + tag = self.safe_string(transaction, 'addressTag') # set but unused + if tag is not None: + if len(tag) < 1: + tag = None + txid = self.safe_string(transaction, 'txId') + if (txid is not None) and (txid.find('Internal transfer ') >= 0): + txid = txid[18:] + currencyId = self.safe_string_2(transaction, 'coin', 'fiatCurrency') + code = self.safe_currency_code(currencyId, currency) + timestamp = None + timestamp = self.safe_integer_2(transaction, 'insertTime', 'createTime') + if timestamp is None: + timestamp = self.parse8601(self.safe_string(transaction, 'applyTime')) + updated = self.safe_integer_2(transaction, 'successTime', 'updateTime') + type = self.safe_string(transaction, 'type') + if type is None: + txType = self.safe_string(transaction, 'transactionType') + if txType is not None: + type = 'deposit' if (txType == '0') else 'withdrawal' + legalMoneyCurrenciesById = self.safe_dict(self.options, 'legalMoneyCurrenciesById') + code = self.safe_string(legalMoneyCurrenciesById, code, code) + status = self.parse_transaction_status_by_type(self.safe_string(transaction, 'status'), type) + amount = self.safe_number(transaction, 'amount') + feeCost = self.safe_number_2(transaction, 'transactionFee', 'totalFee') + fee = None + if feeCost is not None: + fee = {'currency': code, 'cost': feeCost} + internalInteger = self.safe_integer(transaction, 'transferType') + internal = None + if internalInteger is not None: + internal = True if (internalInteger != 0) else False + network = self.safe_string(transaction, 'network') + return { + 'info': transaction, + 'id': id, + 'txid': txid, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': network, + 'address': address, + 'addressTo': address, + 'addressFrom': None, + 'tag': tag, + 'tagTo': tag, + 'tagFrom': None, + 'type': type, + 'amount': amount, + 'currency': code, + 'status': status, + 'updated': updated, + 'internal': internal, + 'comment': None, + 'fee': fee, + } + + def parse_transfer_status(self, status: Str) -> Str: + statuses: dict = { + 'CONFIRMED': 'ok', + } + return self.safe_string(statuses, status, status) + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + # + # transfer + # + # { + # "tranId":13526853623 + # } + # + # fetchTransfers + # + # { + # "timestamp": 1614640878000, + # "asset": "USDT", + # "amount": "25", + # "type": "MAIN_UMFUTURE", + # "status": "CONFIRMED", + # "tranId": 43000126248 + # } + # + # { + # "orderType": "C2C", # Enum:PAY(C2B Merchant Acquiring Payment), PAY_REFUND(C2B Merchant Acquiring Payment,refund), C2C(C2C Transfer Payment),CRYPTO_BOX(Crypto box), CRYPTO_BOX_RF(Crypto Box, refund), C2C_HOLDING(Transfer to new Binance user), C2C_HOLDING_RF(Transfer to new Binance user,refund), PAYOUT(B2C Disbursement Payment), REMITTANCE(Send cash) + # "transactionId": "M_P_71505104267788288", + # "transactionTime": 1610090460133, #trade timestamp + # "amount": "23.72469206", #order amount(up to 8 decimal places), positive is income, negative is expenditure + # "currency": "BNB", + # "walletType": 1, #main wallet type, 1 for funding wallet, 2 for spot wallet, 3 for fiat wallet, 4 or 6 for card payment, 5 for earn wallet + # "walletTypes": [1,2], #array format,there are multiple values when using combination payment + # "fundsDetail": [ # details + # { + # "currency": "USDT", #asset + # "amount": "1.2", + # "walletAssetCost":[ #details of asset cost per wallet + # {"1":"0.6"}, + # {"2":"0.6"} + # ] + # }, + # { + # "currency": "ETH", + # "amount": "0.0001", + # "walletAssetCost":[ + # {"1":"0.00005"}, + # {"2":"0.00005"} + # ] + # } + # ], + # "payerInfo":{ + # "name":"Jack", #nickname or merchant name + # "type":"USER", #account type,USER for personal,MERCHANT for merchant + # "binanceId":"12345678", #binance uid + # "accountId":"67736251" #binance pay id + # }, + # "receiverInfo":{ + # "name":"Alan", #nickname or merchant name + # "type":"MERCHANT", #account type,USER for personal,MERCHANT for merchant + # "email":"alan@binance.com", #email + # "binanceId":"34355667", #binance uid + # "accountId":"21326891", #binance pay id + # "countryCode":"1", #International area code + # "phoneNumber":"8057651210", + # "mobileCode":"US", #country code + # "extend":[ #extension field + # "institutionName": "", + # "cardNumber": "", + # "digitalWalletId": "" + # ] + # } + # } + id = self.safe_string_2(transfer, 'tranId', 'transactionId') + currencyId = self.safe_string_2(transfer, 'asset', 'currency') + code = self.safe_currency_code(currencyId, currency) + amount = self.safe_number(transfer, 'amount') + type = self.safe_string(transfer, 'type') + fromAccount = None + toAccount = None + accountsById = self.safe_dict(self.options, 'accountsById', {}) + if type is not None: + parts = type.split('_') + fromAccount = self.safe_value(parts, 0) + toAccount = self.safe_value(parts, 1) + fromAccount = self.safe_string(accountsById, fromAccount, fromAccount) + toAccount = self.safe_string(accountsById, toAccount, toAccount) + walletType = self.safe_integer(transfer, 'walletType') + if walletType is not None: + payer = self.safe_dict(transfer, 'payerInfo', {}) + receiver = self.safe_dict(transfer, 'receiverInfo', {}) + fromAccount = self.safe_string(payer, 'accountId') + toAccount = self.safe_string(receiver, 'accountId') + timestamp = self.safe_integer_2(transfer, 'timestamp', 'transactionTime') + status = self.parse_transfer_status(self.safe_string(transfer, 'status')) + return { + 'info': transfer, + 'id': id, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'currency': code, + 'amount': amount, + 'fromAccount': fromAccount, + 'toAccount': toAccount, + 'status': status, + } + + def parse_income(self, income, market: Market = None): + # + # { + # "symbol": "ETHUSDT", + # "incomeType": "FUNDING_FEE", + # "income": "0.00134317", + # "asset": "USDT", + # "time": "1621584000000", + # "info": "FUNDING_FEE", + # "tranId": "4480321991774044580", + # "tradeId": "" + # } + # + marketId = self.safe_string(income, 'symbol') + currencyId = self.safe_string(income, 'asset') + timestamp = self.safe_integer(income, 'time') + return { + 'info': income, + 'symbol': self.safe_symbol(marketId, market, None, 'swap'), + 'code': self.safe_currency_code(currencyId), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': self.safe_string(income, 'tranId'), + 'amount': self.safe_number(income, 'income'), + } + + def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://developers.binance.com/docs/wallet/asset/user-universal-transfer + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account to transfer from + :param str toAccount: account to transfer to + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: exchange specific transfer type + :param str [params.symbol]: the unified symbol, required for isolated margin transfers + :returns dict: a `transfer structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'asset': currency['id'], + 'amount': self.currency_to_precision(code, amount), + } + request['type'] = self.safe_string(params, 'type') + params = self.omit(params, 'type') + if request['type'] is None: + symbol = self.safe_string(params, 'symbol') + market = None + if symbol is not None: + market = self.market(symbol) + params = self.omit(params, 'symbol') + fromId = self.convert_type_to_account(fromAccount).upper() + toId = self.convert_type_to_account(toAccount).upper() + isolatedSymbol = None + if market is not None: + isolatedSymbol = market['id'] + if fromId == 'ISOLATED': + if symbol is None: + raise ArgumentsRequired(self.id + ' transfer() requires params["symbol"] when fromAccount is ' + fromAccount) + if toId == 'ISOLATED': + if symbol is None: + raise ArgumentsRequired(self.id + ' transfer() requires params["symbol"] when toAccount is ' + toAccount) + accountsById = self.safe_dict(self.options, 'accountsById', {}) + fromIsolated = not (fromId in accountsById) + toIsolated = not (toId in accountsById) + if fromIsolated and (market is None): + isolatedSymbol = fromId # allow user provide symbol from/to account + if toIsolated and (market is None): + isolatedSymbol = toId + if fromIsolated or toIsolated: # Isolated margin transfer + fromFuture = fromId == 'UMFUTURE' or fromId == 'CMFUTURE' + toFuture = toId == 'UMFUTURE' or toId == 'CMFUTURE' + fromSpot = fromId == 'MAIN' + toSpot = toId == 'MAIN' + funding = fromId == 'FUNDING' or toId == 'FUNDING' + option = fromId == 'OPTION' or toId == 'OPTION' + prohibitedWithIsolated = fromFuture or toFuture or funding or option + if (fromIsolated or toIsolated) and prohibitedWithIsolated: + raise BadRequest(self.id + ' transfer() does not allow transfers between ' + fromAccount + ' and ' + toAccount) + elif toSpot and fromIsolated: + fromId = 'ISOLATED_MARGIN' + request['fromSymbol'] = isolatedSymbol + elif fromSpot and toIsolated: + toId = 'ISOLATED_MARGIN' + request['toSymbol'] = isolatedSymbol + else: + if fromIsolated and toIsolated: + request['fromSymbol'] = fromId + request['toSymbol'] = toId + fromId = 'ISOLATEDMARGIN' + toId = 'ISOLATEDMARGIN' + else: + if fromIsolated: + request['fromSymbol'] = isolatedSymbol + fromId = 'ISOLATEDMARGIN' + if toIsolated: + request['toSymbol'] = isolatedSymbol + toId = 'ISOLATEDMARGIN' + request['type'] = fromId + '_' + toId + else: + request['type'] = fromId + '_' + toId + response = self.sapiPostAssetTransfer(self.extend(request, params)) + # + # { + # "tranId":13526853623 + # } + # + return self.parse_transfer(response, currency) + + def fetch_transfers(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[TransferEntry]: + """ + fetch a history of internal transfers made on an account + + https://developers.binance.com/docs/wallet/asset/query-user-universal-transfer + + :param str code: unified currency code of the currency transferred + :param int [since]: the earliest time in ms to fetch transfers for + :param int [limit]: the maximum number of transfers structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch transfers for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param boolean [params.internal]: default False, when True will fetch pay trade history + :returns dict[]: a list of `transfer structures ` + """ + self.load_markets() + internal = self.safe_bool(params, 'internal') + params = self.omit(params, 'internal') + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchTransfers', 'paginate') + if paginate and not internal: + return self.fetch_paginated_call_dynamic('fetchTransfers', code, since, limit, params) + currency = None + if code is not None: + currency = self.currency(code) + request: dict = {} + limitKey = 'limit' + if not internal: + defaultType = self.safe_string_2(self.options, 'fetchTransfers', 'defaultType', 'spot') + fromAccount = self.safe_string(params, 'fromAccount', defaultType) + defaultTo = 'spot' if (fromAccount == 'future') else 'future' + toAccount = self.safe_string(params, 'toAccount', defaultTo) + type = self.safe_string(params, 'type') + accountsByType = self.safe_dict(self.options, 'accountsByType', {}) + fromId = self.safe_string(accountsByType, fromAccount) + toId = self.safe_string(accountsByType, toAccount) + if type is None: + if fromId is None: + keys = list(accountsByType.keys()) + raise ExchangeError(self.id + ' fromAccount parameter must be one of ' + ', '.join(keys)) + if toId is None: + keys = list(accountsByType.keys()) + raise ExchangeError(self.id + ' toAccount parameter must be one of ' + ', '.join(keys)) + type = fromId + '_' + toId + request['type'] = type + limitKey = 'size' + if limit is not None: + request[limitKey] = limit + if since is not None: + request['startTime'] = since + until = self.safe_integer(params, 'until') + if until is not None: + params = self.omit(params, 'until') + request['endTime'] = until + response = None + if internal: + response = self.sapiGetPayTransactions(self.extend(request, params)) + # + # { + # "code": "000000", + # "message": "success", + # "data": [ + # { + # "orderType": "C2C", # Enum:PAY(C2B Merchant Acquiring Payment), PAY_REFUND(C2B Merchant Acquiring Payment,refund), C2C(C2C Transfer Payment),CRYPTO_BOX(Crypto box), CRYPTO_BOX_RF(Crypto Box, refund), C2C_HOLDING(Transfer to new Binance user), C2C_HOLDING_RF(Transfer to new Binance user,refund), PAYOUT(B2C Disbursement Payment), REMITTANCE(Send cash) + # "transactionId": "M_P_71505104267788288", + # "transactionTime": 1610090460133, #trade timestamp + # "amount": "23.72469206", #order amount(up to 8 decimal places), positive is income, negative is expenditure + # "currency": "BNB", + # "walletType": 1, #main wallet type, 1 for funding wallet, 2 for spot wallet, 3 for fiat wallet, 4 or 6 for card payment, 5 for earn wallet + # "walletTypes": [1,2], #array format,there are multiple values when using combination payment + # "fundsDetail": [ # details + # { + # "currency": "USDT", #asset + # "amount": "1.2", + # "walletAssetCost":[ #details of asset cost per wallet + # {"1":"0.6"}, + # {"2":"0.6"} + # ] + # }, + # { + # "currency": "ETH", + # "amount": "0.0001", + # "walletAssetCost":[ + # {"1":"0.00005"}, + # {"2":"0.00005"} + # ] + # } + # ], + # "payerInfo":{ + # "name":"Jack", #nickname or merchant name + # "type":"USER", #account type,USER for personal,MERCHANT for merchant + # "binanceId":"12345678", #binance uid + # "accountId":"67736251" #binance pay id + # }, + # "receiverInfo":{ + # "name":"Alan", #nickname or merchant name + # "type":"MERCHANT", #account type,USER for personal,MERCHANT for merchant + # "email":"alan@binance.com", #email + # "binanceId":"34355667", #binance uid + # "accountId":"21326891", #binance pay id + # "countryCode":"1", #International area code + # "phoneNumber":"8057651210", + # "mobileCode":"US", #country code + # "extend":[ #extension field + # "institutionName": "", + # "cardNumber": "", + # "digitalWalletId": "" + # ] + # } + # } + # ], + # "success": True + # } + # + else: + response = self.sapiGetAssetTransfer(self.extend(request, params)) + # + # { + # "total": 3, + # "rows": [ + # { + # "timestamp": 1614640878000, + # "asset": "USDT", + # "amount": "25", + # "type": "MAIN_UMFUTURE", + # "status": "CONFIRMED", + # "tranId": 43000126248 + # }, + # ] + # } + # + rows = self.safe_list_2(response, 'rows', 'data', []) + return self.parse_transfers(rows, currency, since, limit) + + def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://developers.binance.com/docs/wallet/capital/deposite-address + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.network]: network for fetch deposit address + :returns dict: an `address structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'coin': currency['id'], + # 'network': 'ETH', # 'BSC', 'XMR', you can get network and isDefault in networkList in the response of sapiGetCapitalConfigDetail + } + networks = self.safe_dict(self.options, 'networks', {}) + network = self.safe_string_upper(params, 'network') # self line allows the user to specify either ERC20 or ETH + network = self.safe_string(networks, network, network) # handle ERC20>ETH alias + if network is not None: + request['network'] = network + params = self.omit(params, 'network') + # has support for the 'network' parameter + response = self.sapiGetCapitalDepositAddress(self.extend(request, params)) + # + # { + # "currency": "XRP", + # "address": "rEb8TK3gBgk5auZkwc6sHnwrGVJH8DuaLh", + # "tag": "108618262", + # "info": { + # "coin": "XRP", + # "address": "rEb8TK3gBgk5auZkwc6sHnwrGVJH8DuaLh", + # "tag": "108618262", + # "url": "https://bithomp.com/explorer/rEb8TK3gBgk5auZkwc6sHnwrGVJH8DuaLh" + # } + # } + # + return self.parse_deposit_address(response, currency) + + def parse_deposit_address(self, response, currency: Currency = None) -> DepositAddress: + # + # { + # "coin": "XRP", + # "address": "rEb8TK3gBgk5auZkwc6sHnwrGVJH8DuaLh", + # "tag": "108618262", + # "url": "https://bithomp.com/explorer/rEb8TK3gBgk5auZkwc6sHnwrGVJH8DuaLh" + # } + # + url = self.safe_string(response, 'url') + address = self.safe_string(response, 'address') + currencyId = self.safe_string(response, 'currency') + code = self.safe_currency_code(currencyId, currency) + # deposit-address endpoint provides only network url(not network ID/CODE) + # so we should map the url to network(their data is inside currencies) + networkCode = self.get_network_code_by_network_url(code, url) + tag = self.safe_string(response, 'tag', '') + if len(tag) == 0: + tag = None + self.check_address(address) + return { + 'info': response, + 'currency': code, + 'network': networkCode, + 'address': address, + 'tag': tag, + } + + def fetch_transaction_fees(self, codes: Strings = None, params={}): + """ + @deprecated + please use fetchDepositWithdrawFees instead + + https://developers.binance.com/docs/wallet/capital/all-coins-info + + :param str[]|None codes: not used by binance fetchTransactionFees() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `fee structures ` + """ + self.load_markets() + response = self.sapiGetCapitalConfigGetall(params) + # + # [ + # { + # "coin": "BAT", + # "depositAllEnable": True, + # "withdrawAllEnable": True, + # "name": "Basic Attention Token", + # "free": "0", + # "locked": "0", + # "freeze": "0", + # "withdrawing": "0", + # "ipoing": "0", + # "ipoable": "0", + # "storage": "0", + # "isLegalMoney": False, + # "trading": True, + # "networkList": [ + # { + # "network": "BNB", + # "coin": "BAT", + # "withdrawIntegerMultiple": "0.00000001", + # "isDefault": False, + # "depositEnable": True, + # "withdrawEnable": True, + # "depositDesc": '', + # "withdrawDesc": '', + # "specialTips": "The name of self asset is Basic Attention Token(BAT). Both a MEMO and an Address are required to successfully deposit your BEP2 tokens to Binance.", + # "name": "BEP2", + # "resetAddressStatus": False, + # "addressRegex": "^(bnb1)[0-9a-z]{38}$", + # "memoRegex": "^[0-9A-Za-z\\-_]{1,120}$", + # "withdrawFee": "0.27", + # "withdrawMin": "0.54", + # "withdrawMax": "10000000000", + # "minConfirm": "1", + # "unLockConfirm": "0" + # }, + # { + # "network": "BSC", + # "coin": "BAT", + # "withdrawIntegerMultiple": "0.00000001", + # "isDefault": False, + # "depositEnable": True, + # "withdrawEnable": True, + # "depositDesc": '', + # "withdrawDesc": '', + # "specialTips": "The name of self asset is Basic Attention Token. Please ensure you are depositing Basic Attention Token(BAT) tokens under the contract address ending in 9766e.", + # "name": "BEP20(BSC)", + # "resetAddressStatus": False, + # "addressRegex": "^(0x)[0-9A-Fa-f]{40}$", + # "memoRegex": '', + # "withdrawFee": "0.27", + # "withdrawMin": "0.54", + # "withdrawMax": "10000000000", + # "minConfirm": "15", + # "unLockConfirm": "0" + # }, + # { + # "network": "ETH", + # "coin": "BAT", + # "withdrawIntegerMultiple": "0.00000001", + # "isDefault": True, + # "depositEnable": True, + # "withdrawEnable": True, + # "depositDesc": '', + # "withdrawDesc": '', + # "specialTips": "The name of self asset is Basic Attention Token. Please ensure you are depositing Basic Attention Token(BAT) tokens under the contract address ending in 887ef.", + # "name": "ERC20", + # "resetAddressStatus": False, + # "addressRegex": "^(0x)[0-9A-Fa-f]{40}$", + # "memoRegex": '', + # "withdrawFee": "27", + # "withdrawMin": "54", + # "withdrawMax": "10000000000", + # "minConfirm": "12", + # "unLockConfirm": "0" + # } + # ] + # } + # ] + # + withdrawFees: dict = {} + for i in range(0, len(response)): + entry = response[i] + currencyId = self.safe_string(entry, 'coin') + code = self.safe_currency_code(currencyId) + networkList = self.safe_list(entry, 'networkList', []) + withdrawFees[code] = {} + for j in range(0, len(networkList)): + networkEntry = networkList[j] + networkId = self.safe_string(networkEntry, 'network') + networkCode = self.safe_currency_code(networkId) + fee = self.safe_number(networkEntry, 'withdrawFee') + withdrawFees[code][networkCode] = fee + return { + 'withdraw': withdrawFees, + 'deposit': {}, + 'info': response, + } + + def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}): + """ + fetch deposit and withdraw fees + + https://developers.binance.com/docs/wallet/capital/all-coins-info + + :param str[]|None codes: not used by binance fetchDepositWithdrawFees() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `fee structures ` + """ + self.load_markets() + response = self.sapiGetCapitalConfigGetall(params) + # + # [ + # { + # "coin": "BAT", + # "depositAllEnable": True, + # "withdrawAllEnable": True, + # "name": "Basic Attention Token", + # "free": "0", + # "locked": "0", + # "freeze": "0", + # "withdrawing": "0", + # "ipoing": "0", + # "ipoable": "0", + # "storage": "0", + # "isLegalMoney": False, + # "trading": True, + # "networkList": [ + # { + # "network": "BNB", + # "coin": "BAT", + # "withdrawIntegerMultiple": "0.00000001", + # "isDefault": False, + # "depositEnable": True, + # "withdrawEnable": True, + # "depositDesc": '', + # "withdrawDesc": '', + # "specialTips": "The name of self asset is Basic Attention Token(BAT). Both a MEMO and an Address are required to successfully deposit your BEP2 tokens to Binance.", + # "name": "BEP2", + # "resetAddressStatus": False, + # "addressRegex": "^(bnb1)[0-9a-z]{38}$", + # "memoRegex": "^[0-9A-Za-z\\-_]{1,120}$", + # "withdrawFee": "0.27", + # "withdrawMin": "0.54", + # "withdrawMax": "10000000000", + # "minConfirm": "1", + # "unLockConfirm": "0" + # }, + # ... + # ] + # } + # ] + # + return self.parse_deposit_withdraw_fees(response, codes, 'coin') + + def parse_deposit_withdraw_fee(self, fee, currency: Currency = None): + # + # { + # "coin": "BAT", + # "depositAllEnable": True, + # "withdrawAllEnable": True, + # "name": "Basic Attention Token", + # "free": "0", + # "locked": "0", + # "freeze": "0", + # "withdrawing": "0", + # "ipoing": "0", + # "ipoable": "0", + # "storage": "0", + # "isLegalMoney": False, + # "trading": True, + # "networkList": [ + # { + # "network": "BNB", + # "coin": "BAT", + # "withdrawIntegerMultiple": "0.00000001", + # "isDefault": False, + # "depositEnable": True, + # "withdrawEnable": True, + # "depositDesc": '', + # "withdrawDesc": '', + # "specialTips": "The name of self asset is Basic Attention Token(BAT). Both a MEMO and an Address are required to successfully deposit your BEP2 tokens to Binance.", + # "name": "BEP2", + # "resetAddressStatus": False, + # "addressRegex": "^(bnb1)[0-9a-z]{38}$", + # "memoRegex": "^[0-9A-Za-z\\-_]{1,120}$", + # "withdrawFee": "0.27", + # "withdrawMin": "0.54", + # "withdrawMax": "10000000000", + # "minConfirm": "1", + # "unLockConfirm": "0" + # }, + # ... + # ] + # } + # + networkList = self.safe_list(fee, 'networkList', []) + result = self.deposit_withdraw_fee(fee) + for j in range(0, len(networkList)): + networkEntry = networkList[j] + networkId = self.safe_string(networkEntry, 'network') + networkCode = self.network_id_to_code(networkId) + withdrawFee = self.safe_number(networkEntry, 'withdrawFee') + isDefault = self.safe_bool(networkEntry, 'isDefault') + if isDefault is True: + result['withdraw'] = { + 'fee': withdrawFee, + 'percentage': None, + } + result['networks'][networkCode] = { + 'withdraw': { + 'fee': withdrawFee, + 'percentage': None, + }, + 'deposit': { + 'fee': None, + 'percentage': None, + }, + } + return result + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://developers.binance.com/docs/wallet/capital/withdraw + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.check_address(address) + self.load_markets() + currency = self.currency(code) + request: dict = { + 'coin': currency['id'], + 'address': address, + # issue sapiGetCapitalConfigGetall() to get networks for withdrawing USDT ERC20 vs USDT Omni + # 'network': 'ETH', # 'BTC', 'TRX', etc, optional + } + if tag is not None: + request['addressTag'] = tag + networks = self.safe_dict(self.options, 'networks', {}) + network = self.safe_string_upper(params, 'network') # self line allows the user to specify either ERC20 or ETH + network = self.safe_string(networks, network, network) # handle ERC20>ETH alias + if network is not None: + request['network'] = network + params = self.omit(params, 'network') + request['amount'] = self.currency_to_precision(code, amount, network) + response = self.sapiPostCapitalWithdrawApply(self.extend(request, params)) + # {id: '9a67628b16ba4988ae20d329333f16bc'} + return self.parse_transaction(response, currency) + + def parse_trading_fee(self, fee: dict, market: Market = None) -> TradingFeeInterface: + # + # spot + # [ + # { + # "symbol": "BTCUSDT", + # "makerCommission": "0.001", + # "takerCommission": "0.001" + # } + # ] + # + # swap + # { + # "symbol": "BTCUSD_PERP", + # "makerCommissionRate": "0.00015", # 0.015% + # "takerCommissionRate": "0.00040" # 0.040% + # } + # + marketId = self.safe_string(fee, 'symbol') + symbol = self.safe_symbol(marketId, market, None, 'spot') + return { + 'info': fee, + 'symbol': symbol, + 'maker': self.safe_number_2(fee, 'makerCommission', 'makerCommissionRate'), + 'taker': self.safe_number_2(fee, 'takerCommission', 'takerCommissionRate'), + 'percentage': None, + 'tierBased': None, + } + + def fetch_trading_fee(self, symbol: str, params={}) -> TradingFeeInterface: + """ + fetch the trading fees for a market + + https://developers.binance.com/docs/wallet/asset/trade-fee + https://developers.binance.com/docs/derivatives/usds-margined-futures/account/rest-api/User-Commission-Rate + https://developers.binance.com/docs/derivatives/coin-margined-futures/account/rest-api/User-Commission-Rate + https://developers.binance.com/docs/derivatives/portfolio-margin/account/Get-User-Commission-Rate-for-UM + https://developers.binance.com/docs/derivatives/portfolio-margin/account/Get-User-Commission-Rate-for-CM + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.portfolioMargin]: set to True if you would like to fetch trading fees in a portfolio margin account + :param str [params.subType]: "linear" or "inverse" + :returns dict: a `fee structure ` + """ + self.load_markets() + market = self.market(symbol) + type = market['type'] + subType = None + subType, params = self.handle_sub_type_and_params('fetchTradingFee', market, params) + isPortfolioMargin = None + isPortfolioMargin, params = self.handle_option_and_params_2(params, 'fetchTradingFee', 'papi', 'portfolioMargin', False) + isLinear = self.is_linear(type, subType) + isInverse = self.is_inverse(type, subType) + request: dict = { + 'symbol': market['id'], + } + response = None + if isLinear: + if isPortfolioMargin: + response = self.papiGetUmCommissionRate(self.extend(request, params)) + else: + response = self.fapiPrivateGetCommissionRate(self.extend(request, params)) + elif isInverse: + if isPortfolioMargin: + response = self.papiGetCmCommissionRate(self.extend(request, params)) + else: + response = self.dapiPrivateGetCommissionRate(self.extend(request, params)) + else: + response = self.sapiGetAssetTradeFee(self.extend(request, params)) + # + # spot + # + # [ + # { + # "symbol": "BTCUSDT", + # "makerCommission": "0.001", + # "takerCommission": "0.001" + # } + # ] + # + # swap + # + # { + # "symbol": "BTCUSD_PERP", + # "makerCommissionRate": "0.00015", # 0.015% + # "takerCommissionRate": "0.00040" # 0.040% + # } + # + data = response + if isinstance(data, list): + data = self.safe_dict(data, 0, {}) + return self.parse_trading_fee(data, market) + + def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://developers.binance.com/docs/wallet/asset/trade-fee + https://developers.binance.com/docs/derivatives/usds-margined-futures/account/rest-api/Account-Information-V2 + https://developers.binance.com/docs/derivatives/coin-margined-futures/account/rest-api/Account-Information + https://developers.binance.com/docs/derivatives/usds-margined-futures/account/rest-api/Account-Config + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.subType]: "linear" or "inverse" + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + self.load_markets() + type = None + type, params = self.handle_market_type_and_params('fetchTradingFees', None, params) + subType = None + subType, params = self.handle_sub_type_and_params('fetchTradingFees', None, params, 'linear') + isSpotOrMargin = (type == 'spot') or (type == 'margin') + isLinear = self.is_linear(type, subType) + isInverse = self.is_inverse(type, subType) + response = None + if isSpotOrMargin: + response = self.sapiGetAssetTradeFee(params) + elif isLinear: + response = self.fapiPrivateGetAccountConfig(params) + elif isInverse: + response = self.dapiPrivateGetAccount(params) + # + # sapi / spot + # + # [ + # { + # "symbol": "ZRXBNB", + # "makerCommission": "0.001", + # "takerCommission": "0.001" + # }, + # { + # "symbol": "ZRXBTC", + # "makerCommission": "0.001", + # "takerCommission": "0.001" + # }, + # ] + # + # fapi / future / linear + # + # { + # "feeTier": 0, # account commisssion tier + # "canTrade": True, # if can trade + # "canDeposit": True, # if can transfer in asset + # "canWithdraw": True, # if can transfer out asset + # "updateTime": 0, + # "totalInitialMargin": "0.00000000", # total initial margin required with current mark price(useless with isolated positions), only for USDT asset + # "totalMaintMargin": "0.00000000", # total maintenance margin required, only for USDT asset + # "totalWalletBalance": "23.72469206", # total wallet balance, only for USDT asset + # "totalUnrealizedProfit": "0.00000000", # total unrealized profit, only for USDT asset + # "totalMarginBalance": "23.72469206", # total margin balance, only for USDT asset + # "totalPositionInitialMargin": "0.00000000", # initial margin required for positions with current mark price, only for USDT asset + # "totalOpenOrderInitialMargin": "0.00000000", # initial margin required for open orders with current mark price, only for USDT asset + # "totalCrossWalletBalance": "23.72469206", # crossed wallet balance, only for USDT asset + # "totalCrossUnPnl": "0.00000000", # unrealized profit of crossed positions, only for USDT asset + # "availableBalance": "23.72469206", # available balance, only for USDT asset + # "maxWithdrawAmount": "23.72469206" # maximum amount for transfer out, only for USDT asset + # ... + # } + # + # dapi / delivery / inverse + # + # { + # "canDeposit": True, + # "canTrade": True, + # "canWithdraw": True, + # "feeTier": 2, + # "updateTime": 0 + # } + # + if isSpotOrMargin: + # + # [ + # { + # "symbol": "ZRXBNB", + # "makerCommission": "0.001", + # "takerCommission": "0.001" + # }, + # { + # "symbol": "ZRXBTC", + # "makerCommission": "0.001", + # "takerCommission": "0.001" + # }, + # ] + # + result: dict = {} + for i in range(0, len(response)): + fee = self.parse_trading_fee(response[i]) + symbol = fee['symbol'] + result[symbol] = fee + return result + elif isLinear: + # + # { + # "feeTier": 0, # account commisssion tier + # "canTrade": True, # if can trade + # "canDeposit": True, # if can transfer in asset + # "canWithdraw": True, # if can transfer out asset + # "updateTime": 0, + # "totalInitialMargin": "0.00000000", # total initial margin required with current mark price(useless with isolated positions), only for USDT asset + # "totalMaintMargin": "0.00000000", # total maintenance margin required, only for USDT asset + # "totalWalletBalance": "23.72469206", # total wallet balance, only for USDT asset + # "totalUnrealizedProfit": "0.00000000", # total unrealized profit, only for USDT asset + # "totalMarginBalance": "23.72469206", # total margin balance, only for USDT asset + # "totalPositionInitialMargin": "0.00000000", # initial margin required for positions with current mark price, only for USDT asset + # "totalOpenOrderInitialMargin": "0.00000000", # initial margin required for open orders with current mark price, only for USDT asset + # "totalCrossWalletBalance": "23.72469206", # crossed wallet balance, only for USDT asset + # "totalCrossUnPnl": "0.00000000", # unrealized profit of crossed positions, only for USDT asset + # "availableBalance": "23.72469206", # available balance, only for USDT asset + # "maxWithdrawAmount": "23.72469206" # maximum amount for transfer out, only for USDT asset + # ... + # } + # + symbols = list(self.markets.keys()) + result: dict = {} + feeTier = self.safe_integer(response, 'feeTier') + feeTiers = self.fees['linear']['trading']['tiers'] + maker = feeTiers['maker'][feeTier][1] + taker = feeTiers['taker'][feeTier][1] + for i in range(0, len(symbols)): + symbol = symbols[i] + market = self.markets[symbol] + if market['linear']: + result[symbol] = { + 'info': { + 'feeTier': feeTier, + }, + 'symbol': symbol, + 'maker': maker, + 'taker': taker, + } + return result + elif isInverse: + # + # { + # "canDeposit": True, + # "canTrade": True, + # "canWithdraw": True, + # "feeTier": 2, + # "updateTime": 0 + # } + # + symbols = list(self.markets.keys()) + result: dict = {} + feeTier = self.safe_integer(response, 'feeTier') + feeTiers = self.fees['inverse']['trading']['tiers'] + maker = feeTiers['maker'][feeTier][1] + taker = feeTiers['taker'][feeTier][1] + for i in range(0, len(symbols)): + symbol = symbols[i] + market = self.markets[symbol] + if market['inverse']: + result[symbol] = { + 'info': { + 'feeTier': feeTier, + }, + 'symbol': symbol, + 'maker': maker, + 'taker': taker, + } + return result + return None + + def futures_transfer(self, code: str, amount, type, params={}): + """ + @ignore + transfer between futures account + + https://developers.binance.com/docs/derivatives/usds-margined-futures/account/rest-api/New-Future-Account-Transfer + + :param str code: unified currency code + :param float amount: the amount to transfer + :param str type: 1 - transfer from spot account to USDT-Ⓜ futures account, 2 - transfer from USDT-Ⓜ futures account to spot account, 3 - transfer from spot account to COIN-Ⓜ futures account, 4 - transfer from COIN-Ⓜ futures account to spot account + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float params.recvWindow: + :returns dict: a `transfer structure ` + """ + if (type < 1) or (type > 4): + raise ArgumentsRequired(self.id + ' type must be between 1 and 4') + self.load_markets() + currency = self.currency(code) + request: dict = { + 'asset': currency['id'], + 'amount': amount, + 'type': type, + } + response = self.sapiPostFuturesTransfer(self.extend(request, params)) + # + # { + # "tranId": 100000001 + # } + # + return self.parse_transfer(response, currency) + + def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate + + https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/Mark-Price + https://developers.binance.com/docs/derivatives/coin-margined-futures/market-data/rest-api/Index-Price-and-Mark-Price + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = None + if market['linear']: + response = self.fapiPublicGetPremiumIndex(self.extend(request, params)) + elif market['inverse']: + response = self.dapiPublicGetPremiumIndex(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchFundingRate() supports linear and inverse contracts only') + if market['inverse']: + response = response[0] + # + # { + # "symbol": "BTCUSDT", + # "markPrice": "45802.81129892", + # "indexPrice": "45745.47701915", + # "estimatedSettlePrice": "45133.91753671", + # "lastFundingRate": "0.00063521", + # "interestRate": "0.00010000", + # "nextFundingTime": "1621267200000", + # "time": "1621252344001" + # } + # + return self.parse_funding_rate(response, market) + + def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical funding rate prices + + https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/Get-Funding-Rate-History + https://developers.binance.com/docs/derivatives/coin-margined-futures/market-data/rest-api/Get-Funding-Rate-History-of-Perpetual-Futures + + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: timestamp in ms of the earliest funding rate to fetch + :param int [limit]: the maximum amount of `funding rate structures ` to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest funding rate + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param str [params.subType]: "linear" or "inverse" + :returns dict[]: a list of `funding rate structures ` + """ + self.load_markets() + request: dict = {} + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchFundingRateHistory', 'paginate') + if paginate: + return self.fetch_paginated_call_deterministic('fetchFundingRateHistory', symbol, since, limit, '8h', params) + defaultType = self.safe_string_2(self.options, 'fetchFundingRateHistory', 'defaultType', 'future') + type = self.safe_string(params, 'type', defaultType) + market = None + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + request['symbol'] = market['id'] + subType = None + subType, params = self.handle_sub_type_and_params('fetchFundingRateHistory', market, params, 'linear') + params = self.omit(params, 'type') + if since is not None: + request['startTime'] = since + until = self.safe_integer(params, 'until') # unified in milliseconds + endTime = self.safe_integer(params, 'endTime', until) # exchange-specific in milliseconds + params = self.omit(params, ['endTime', 'until']) + if endTime is not None: + request['endTime'] = endTime + if limit is not None: + request['limit'] = limit + response = None + if self.is_linear(type, subType): + response = self.fapiPublicGetFundingRate(self.extend(request, params)) + elif self.is_inverse(type, subType): + response = self.dapiPublicGetFundingRate(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchFundingRateHistory() is not supported for ' + type + ' markets') + # + # { + # "symbol": "BTCUSDT", + # "fundingRate": "0.00063521", + # "fundingTime": "1621267200000", + # } + # + return self.parse_funding_rate_histories(response, market, since, limit) + + def parse_funding_rate_history(self, contract, market: Market = None): + # + # { + # "symbol": "BTCUSDT", + # "fundingRate": "0.00063521", + # "fundingTime": "1621267200000", + # } + # + timestamp = self.safe_integer(contract, 'fundingTime') + return { + 'info': contract, + 'symbol': self.safe_symbol(self.safe_string(contract, 'symbol'), None, None, 'swap'), + 'fundingRate': self.safe_number(contract, 'fundingRate'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + + def fetch_funding_rates(self, symbols: Strings = None, params={}) -> FundingRates: + """ + fetch the funding rate for multiple markets + + https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/Mark-Price + https://developers.binance.com/docs/derivatives/coin-margined-futures/market-data/rest-api/Index-Price-and-Mark-Price + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.subType]: "linear" or "inverse" + :returns dict[]: a list of `funding rate structures `, indexed by market symbols + """ + self.load_markets() + symbols = self.market_symbols(symbols) + defaultType = self.safe_string_2(self.options, 'fetchFundingRates', 'defaultType', 'future') + type = self.safe_string(params, 'type', defaultType) + subType = None + subType, params = self.handle_sub_type_and_params('fetchFundingRates', None, params, 'linear') + query = self.omit(params, 'type') + response = None + if self.is_linear(type, subType): + response = self.fapiPublicGetPremiumIndex(query) + elif self.is_inverse(type, subType): + response = self.dapiPublicGetPremiumIndex(query) + else: + raise NotSupported(self.id + ' fetchFundingRates() supports linear and inverse contracts only') + return self.parse_funding_rates(response, symbols) + + def parse_funding_rate(self, contract, market: Market = None) -> FundingRate: + # ensure it matches with https://www.binance.com/en/futures/funding-history/0 + # + # fetchFundingRate, fetchFundingRates + # + # { + # "symbol": "BTCUSDT", + # "markPrice": "45802.81129892", + # "indexPrice": "45745.47701915", + # "estimatedSettlePrice": "45133.91753671", + # "lastFundingRate": "0.00063521", + # "interestRate": "0.00010000", + # "nextFundingTime": "1621267200000", + # "time": "1621252344001" + # } + # + # fetchFundingInterval, fetchFundingIntervals + # + # { + # "symbol": "BLZUSDT", + # "adjustedFundingRateCap": "0.03000000", + # "adjustedFundingRateFloor": "-0.03000000", + # "fundingIntervalHours": 4, + # "disclaimer": False + # } + # + timestamp = self.safe_integer(contract, 'time') + marketId = self.safe_string(contract, 'symbol') + symbol = self.safe_symbol(marketId, market, None, 'contract') + markPrice = self.safe_number(contract, 'markPrice') + indexPrice = self.safe_number(contract, 'indexPrice') + interestRate = self.safe_number(contract, 'interestRate') + estimatedSettlePrice = self.safe_number(contract, 'estimatedSettlePrice') + fundingRate = self.safe_number(contract, 'lastFundingRate') + fundingTime = self.safe_integer(contract, 'nextFundingTime') + interval = self.safe_string(contract, 'fundingIntervalHours') + intervalString = None + if interval is not None: + intervalString = interval + 'h' + return { + 'info': contract, + 'symbol': symbol, + 'markPrice': markPrice, + 'indexPrice': indexPrice, + 'interestRate': interestRate, + 'estimatedSettlePrice': estimatedSettlePrice, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'fundingRate': fundingRate, + 'fundingTimestamp': fundingTime, + 'fundingDatetime': self.iso8601(fundingTime), + 'nextFundingRate': None, + 'nextFundingTimestamp': None, + 'nextFundingDatetime': None, + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': intervalString, + } + + def parse_account_positions(self, account, filterClosed=False): + positions = self.safe_list(account, 'positions') + assets = self.safe_list(account, 'assets', []) + balances: dict = {} + for i in range(0, len(assets)): + entry = assets[i] + currencyId = self.safe_string(entry, 'asset') + code = self.safe_currency_code(currencyId) + crossWalletBalance = self.safe_string(entry, 'crossWalletBalance') + crossUnPnl = self.safe_string(entry, 'crossUnPnl') + balances[code] = { + 'crossMargin': Precise.string_add(crossWalletBalance, crossUnPnl), + 'crossWalletBalance': crossWalletBalance, + } + result = [] + for i in range(0, len(positions)): + position = positions[i] + marketId = self.safe_string(position, 'symbol') + market = self.safe_market(marketId, None, None, 'contract') + code = market['quote'] if market['linear'] else market['base'] + maintenanceMargin = self.safe_string(position, 'maintMargin') + # check for maintenance margin so empty positions are not returned + isPositionOpen = (maintenanceMargin != '0') and (maintenanceMargin != '0.00000000') + if not filterClosed or isPositionOpen: + # sometimes not all the codes are correctly returned... + if code in balances: + parsed = self.parse_account_position(self.extend(position, { + 'crossMargin': balances[code]['crossMargin'], + 'crossWalletBalance': balances[code]['crossWalletBalance'], + }), market) + result.append(parsed) + return result + + def parse_account_position(self, position, market: Market = None): + # + # usdm + # + # v3(similar for cross & isolated) + # + # { + # "symbol": "WLDUSDT", + # "positionSide": "BOTH", + # "positionAmt": "-849", + # "unrealizedProfit": "11.17920750", + # "notional": "-1992.46079250", + # "isolatedMargin": "0", + # "isolatedWallet": "0", + # "initialMargin": "99.62303962", + # "maintMargin": "11.95476475", + # "updateTime": "1721995760449" + # "leverage": "50", # in v2 + # "entryPrice": "2.34", # in v2 + # "positionInitialMargin": "118.82116614", # in v2 + # "openOrderInitialMargin": "0", # in v2 + # "isolated": False, # in v2 + # "breakEvenPrice": "2.3395788", # in v2 + # "maxNotional": "25000", # in v2 + # "bidNotional": "0", # in v2 + # "askNotional": "0" # in v2 + # } + # + # coinm + # + # { + # "symbol": "BTCUSD_210625", + # "initialMargin": "0.00024393", + # "maintMargin": "0.00002439", + # "unrealizedProfit": "-0.00000163", + # "positionInitialMargin": "0.00024393", + # "openOrderInitialMargin": "0", + # "leverage": "10", + # "isolated": False, + # "positionSide": "BOTH", + # "entryPrice": "41021.20000069", + # "maxQty": "100", + # "notionalValue": "0.00243939", + # "isolatedWallet": "0", + # "crossMargin": "0.314" + # "crossWalletBalance": "34", + # } + # + # linear portfolio margin + # + # { + # "symbol": "CTSIUSDT", + # "initialMargin": "0", + # "maintMargin": "0", + # "unrealizedProfit": "0.00000000", + # "positionInitialMargin": "0", + # "openOrderInitialMargin": "0", + # "leverage": "20", + # "entryPrice": "0.0", + # "maxNotional": "25000", + # "bidNotional": "0", + # "askNotional": "0", + # "positionSide": "SHORT", + # "positionAmt": "0", + # "updateTime": 0, + # "notional": "0", + # "breakEvenPrice": "0.0" + # } + # + # inverse portoflio margin + # + # { + # "symbol": "TRXUSD_PERP", + # "initialMargin": "0", + # "maintMargin": "0", + # "unrealizedProfit": "0.00000000", + # "positionInitialMargin": "0", + # "openOrderInitialMargin": "0", + # "leverage": "20", + # "entryPrice": "0.00000000", + # "positionSide": "SHORT", + # "positionAmt": "0", + # "maxQty": "5000000", + # "updateTime": 0, + # "notionalValue": "0", + # "breakEvenPrice": "0.00000000" + # } + # + marketId = self.safe_string(position, 'symbol') + market = self.safe_market(marketId, market, None, 'contract') + symbol = self.safe_string(market, 'symbol') + leverageString = self.safe_string(position, 'leverage') + leverage = int(leverageString) if (leverageString is not None) else None + initialMarginString = self.safe_string(position, 'initialMargin') + initialMargin = self.parse_number(initialMarginString) + initialMarginPercentageString = None + if leverageString is not None: + initialMarginPercentageString = Precise.string_div('1', leverageString, 8) + rational = self.is_round_number(1000 % leverage) + if not rational: + initialMarginPercentageString = Precise.string_div(Precise.string_add(initialMarginPercentageString, '1e-8'), '1', 8) + # to notionalValue + usdm = ('notional' in position) + maintenanceMarginString = self.safe_string(position, 'maintMargin') + maintenanceMargin = self.parse_number(maintenanceMarginString) + entryPriceString = self.safe_string(position, 'entryPrice') + entryPrice = self.parse_number(entryPriceString) + notionalString = self.safe_string_2(position, 'notional', 'notionalValue') + notionalStringAbs = Precise.string_abs(notionalString) + notional = self.parse_number(notionalStringAbs) + contractsString = self.safe_string(position, 'positionAmt') + contractsStringAbs = Precise.string_abs(contractsString) + if contractsString is None: + entryNotional = Precise.string_mul(Precise.string_mul(leverageString, initialMarginString), entryPriceString) + contractSizeNew = self.safe_string(market, 'contractSize') + contractsString = Precise.string_div(entryNotional, contractSizeNew) + contractsStringAbs = Precise.string_div(Precise.string_add(contractsString, '0.5'), '1', 0) + contracts = self.parse_number(contractsStringAbs) + leverageBrackets = self.safe_dict(self.options, 'leverageBrackets', {}) + leverageBracket = self.safe_list(leverageBrackets, symbol, []) + maintenanceMarginPercentageString = None + for i in range(0, len(leverageBracket)): + bracket = leverageBracket[i] + if Precise.string_lt(notionalStringAbs, bracket[0]): + break + maintenanceMarginPercentageString = bracket[1] + maintenanceMarginPercentage = self.parse_number(maintenanceMarginPercentageString) + unrealizedPnlString = self.safe_string(position, 'unrealizedProfit') + unrealizedPnl = self.parse_number(unrealizedPnlString) + timestamp = self.safe_integer(position, 'updateTime') + if timestamp == 0: + timestamp = None + isolated = self.safe_bool(position, 'isolated') + if isolated is None: + isolatedMarginRaw = self.safe_string(position, 'isolatedMargin') + isolated = not Precise.string_eq(isolatedMarginRaw, '0') + marginMode = None + collateralString = None + walletBalance = None + if isolated: + marginMode = 'isolated' + walletBalance = self.safe_string(position, 'isolatedWallet') + collateralString = Precise.string_add(walletBalance, unrealizedPnlString) + else: + marginMode = 'cross' + walletBalance = self.safe_string(position, 'crossWalletBalance') + collateralString = self.safe_string(position, 'crossMargin') + collateral = self.parse_number(collateralString) + marginRatio = None + side = None + percentage = None + liquidationPriceStringRaw = None + liquidationPrice = None + contractSize = self.safe_value(market, 'contractSize') + contractSizeString = self.number_to_string(contractSize) + if Precise.string_equals(notionalString, '0'): + entryPrice = None + else: + side = 'short' if Precise.string_lt(notionalString, '0') else 'long' + marginRatio = self.parse_number(Precise.string_div(Precise.string_add(Precise.string_div(maintenanceMarginString, collateralString), '5e-5'), '1', 4)) + percentage = self.parse_number(Precise.string_mul(Precise.string_div(unrealizedPnlString, initialMarginString, 4), '100')) + if usdm: + # calculate liquidation price + # + # liquidationPrice = (walletBalance / (contracts * (±1 + mmp))) + (±entryPrice / (±1 + mmp)) + # + # mmp = maintenanceMarginPercentage + # where ± is negative for long and positive for short + # TODO: calculate liquidation price for coinm contracts + onePlusMaintenanceMarginPercentageString = None + entryPriceSignString = entryPriceString + if side == 'short': + onePlusMaintenanceMarginPercentageString = Precise.string_add('1', maintenanceMarginPercentageString) + else: + onePlusMaintenanceMarginPercentageString = Precise.string_add('-1', maintenanceMarginPercentageString) + entryPriceSignString = Precise.string_mul('-1', entryPriceSignString) + leftSide = Precise.string_div(walletBalance, Precise.string_mul(contractsStringAbs, onePlusMaintenanceMarginPercentageString)) + rightSide = Precise.string_div(entryPriceSignString, onePlusMaintenanceMarginPercentageString) + liquidationPriceStringRaw = Precise.string_add(leftSide, rightSide) + else: + # calculate liquidation price + # + # liquidationPrice = (contracts * contractSize(±1 - mmp)) / (±1/entryPrice * contracts * contractSize - walletBalance) + # + onePlusMaintenanceMarginPercentageString = None + entryPriceSignString = entryPriceString + if side == 'short': + onePlusMaintenanceMarginPercentageString = Precise.string_sub('1', maintenanceMarginPercentageString) + else: + onePlusMaintenanceMarginPercentageString = Precise.string_sub('-1', maintenanceMarginPercentageString) + entryPriceSignString = Precise.string_mul('-1', entryPriceSignString) + size = Precise.string_mul(contractsStringAbs, contractSizeString) + leftSide = Precise.string_mul(size, onePlusMaintenanceMarginPercentageString) + rightSide = Precise.string_sub(Precise.string_mul(Precise.string_div('1', entryPriceSignString), size), walletBalance) + liquidationPriceStringRaw = Precise.string_div(leftSide, rightSide) + pricePrecision = self.precision_from_string(self.safe_string(market['precision'], 'price')) + pricePrecisionPlusOne = pricePrecision + 1 + pricePrecisionPlusOneString = str(pricePrecisionPlusOne) + # round half up + rounder = Precise('5e-' + pricePrecisionPlusOneString) + rounderString = str(rounder) + liquidationPriceRoundedString = Precise.string_add(rounderString, liquidationPriceStringRaw) + truncatedLiquidationPrice = Precise.string_div(liquidationPriceRoundedString, '1', pricePrecision) + if truncatedLiquidationPrice[0] == '-': + # user cannot be liquidated + # since he has more collateral than the size of the position + truncatedLiquidationPrice = None + liquidationPrice = self.parse_number(truncatedLiquidationPrice) + positionSide = self.safe_string(position, 'positionSide') + hedged = positionSide != 'BOTH' + return { + 'info': position, + 'id': None, + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'initialMargin': initialMargin, + 'initialMarginPercentage': self.parse_number(initialMarginPercentageString), + 'maintenanceMargin': maintenanceMargin, + 'maintenanceMarginPercentage': maintenanceMarginPercentage, + 'entryPrice': entryPrice, + 'notional': notional, + 'leverage': self.parse_number(leverageString), + 'unrealizedPnl': unrealizedPnl, + 'contracts': contracts, + 'contractSize': contractSize, + 'marginRatio': marginRatio, + 'liquidationPrice': liquidationPrice, + 'markPrice': None, + 'collateral': collateral, + 'marginMode': marginMode, + 'side': side, + 'hedged': hedged, + 'percentage': percentage, + } + + def parse_position_risk(self, position, market: Market = None): + # + # usdm + # + # { + # symbol: "WLDUSDT", + # positionSide: "BOTH", + # positionAmt: "5", + # entryPrice: "2.3483", + # breakEvenPrice: "2.349356735", + # markPrice: "2.39560000", + # unRealizedProfit: "0.23650000", + # liquidationPrice: "0", + # isolatedMargin: "0", + # notional: "11.97800000", + # isolatedWallet: "0", + # updateTime: "1722062678998", + # initialMargin: "2.39560000", # not in v2 + # maintMargin: "0.07186800", # not in v2 + # positionInitialMargin: "2.39560000", # not in v2 + # openOrderInitialMargin: "0", # not in v2 + # adl: "2", # not in v2 + # bidNotional: "0", # not in v2 + # askNotional: "0", # not in v2 + # marginAsset: "USDT", # not in v2 + # # the below fields are only in v2 + # leverage: "5", + # maxNotionalValue: "6000000", + # marginType: "cross", + # isAutoAddMargin: "false", + # isolated: False, + # adlQuantile: "2", + # + # coinm + # + # { + # "symbol": "BTCUSD_PERP", + # "positionAmt": "2", + # "entryPrice": "37643.10000021", + # "markPrice": "38103.05510455", + # "unRealizedProfit": "0.00006413", + # "liquidationPrice": "25119.97445760", + # "leverage": "2", + # "maxQty": "1500", + # "marginType": "isolated", + # "isolatedMargin": "0.00274471", + # "isAutoAddMargin": "false", + # "positionSide": "BOTH", + # "notionalValue": "0.00524892", + # "isolatedWallet": "0.00268058" + # } + # + # inverse portfolio margin + # + # { + # "symbol": "ETHUSD_PERP", + # "positionAmt": "1", + # "entryPrice": "2422.400000007", + # "markPrice": "2424.51267823", + # "unRealizedProfit": "0.0000036", + # "liquidationPrice": "293.57678898", + # "leverage": "100", + # "positionSide": "LONG", + # "updateTime": 1707371941861, + # "maxQty": "15", + # "notionalValue": "0.00412454", + # "breakEvenPrice": "2423.368960034" + # } + # + # linear portfolio margin + # + # { + # "symbol": "BTCUSDT", + # "positionAmt": "0.01", + # "entryPrice": "44525.0", + # "markPrice": "45464.1735922", + # "unRealizedProfit": "9.39173592", + # "liquidationPrice": "38007.16308568", + # "leverage": "100", + # "positionSide": "LONG", + # "updateTime": 1707371879042, + # "maxNotionalValue": "500000.0", + # "notional": "454.64173592", + # "breakEvenPrice": "44542.81" + # } + # + marketId = self.safe_string(position, 'symbol') + market = self.safe_market(marketId, market, None, 'contract') + symbol = self.safe_string(market, 'symbol') + isolatedMarginString = self.safe_string(position, 'isolatedMargin') + leverageBrackets = self.safe_dict(self.options, 'leverageBrackets', {}) + leverageBracket = self.safe_list(leverageBrackets, symbol, []) + notionalString = self.safe_string_2(position, 'notional', 'notionalValue') + notionalStringAbs = Precise.string_abs(notionalString) + maintenanceMarginPercentageString = None + for i in range(0, len(leverageBracket)): + bracket = leverageBracket[i] + if Precise.string_lt(notionalStringAbs, bracket[0]): + break + maintenanceMarginPercentageString = bracket[1] + notional = self.parse_number(notionalStringAbs) + contractsAbs = Precise.string_abs(self.safe_string(position, 'positionAmt')) + contracts = self.parse_number(contractsAbs) + unrealizedPnlString = self.safe_string(position, 'unRealizedProfit') + unrealizedPnl = self.parse_number(unrealizedPnlString) + liquidationPriceString = self.omit_zero(self.safe_string(position, 'liquidationPrice')) + liquidationPrice = self.parse_number(liquidationPriceString) + collateralString = None + marginMode = self.safe_string(position, 'marginType') + if marginMode is None and isolatedMarginString is not None: + marginMode = 'cross' if Precise.string_eq(isolatedMarginString, '0') else 'isolated' + side = None + if Precise.string_gt(notionalString, '0'): + side = 'long' + elif Precise.string_lt(notionalString, '0'): + side = 'short' + entryPriceString = self.safe_string(position, 'entryPrice') + entryPrice = self.parse_number(entryPriceString) + contractSize = self.safe_value(market, 'contractSize') + contractSizeString = self.number_to_string(contractSize) + # to notionalValue + linear = ('notional' in position) + if marginMode == 'cross': + # calculate collateral + precision = self.safe_dict(market, 'precision', {}) + basePrecisionValue = self.safe_string(precision, 'base') + quotePrecisionValue = self.safe_string_2(precision, 'quote', 'price') + precisionIsUndefined = (basePrecisionValue is None) and (quotePrecisionValue is None) + if not precisionIsUndefined: + if linear: + # walletBalance = (liquidationPrice * (±1 + mmp) ± entryPrice) * contracts + onePlusMaintenanceMarginPercentageString = None + entryPriceSignString = entryPriceString + if side == 'short': + onePlusMaintenanceMarginPercentageString = Precise.string_add('1', maintenanceMarginPercentageString) + entryPriceSignString = Precise.string_mul('-1', entryPriceSignString) + else: + onePlusMaintenanceMarginPercentageString = Precise.string_add('-1', maintenanceMarginPercentageString) + inner = Precise.string_mul(liquidationPriceString, onePlusMaintenanceMarginPercentageString) + leftSide = Precise.string_add(inner, entryPriceSignString) + quotePrecision = self.precision_from_string(self.safe_string_2(precision, 'quote', 'price')) + if quotePrecision is not None: + collateralString = Precise.string_div(Precise.string_mul(leftSide, contractsAbs), '1', quotePrecision) + else: + # walletBalance = (contracts * contractSize) * (±1/entryPrice - (±1 - mmp) / liquidationPrice) + onePlusMaintenanceMarginPercentageString = None + entryPriceSignString = entryPriceString + if side == 'short': + onePlusMaintenanceMarginPercentageString = Precise.string_sub('1', maintenanceMarginPercentageString) + else: + onePlusMaintenanceMarginPercentageString = Precise.string_sub('-1', maintenanceMarginPercentageString) + entryPriceSignString = Precise.string_mul('-1', entryPriceSignString) + leftSide = Precise.string_mul(contractsAbs, contractSizeString) + rightSide = Precise.string_sub(Precise.string_div('1', entryPriceSignString), Precise.string_div(onePlusMaintenanceMarginPercentageString, liquidationPriceString)) + basePrecision = self.precision_from_string(self.safe_string(precision, 'base')) + if basePrecision is not None: + collateralString = Precise.string_div(Precise.string_mul(leftSide, rightSide), '1', basePrecision) + else: + collateralString = self.safe_string(position, 'isolatedMargin') + collateralString = '0' if (collateralString is None) else collateralString + collateral = self.parse_number(collateralString) + markPrice = self.parse_number(self.omit_zero(self.safe_string(position, 'markPrice'))) + timestamp = self.safe_integer(position, 'updateTime') + if timestamp == 0: + timestamp = None + maintenanceMarginPercentage = self.parse_number(maintenanceMarginPercentageString) + maintenanceMarginString = Precise.string_mul(maintenanceMarginPercentageString, notionalStringAbs) + if maintenanceMarginString is None: + # for a while, self new value was a backup to the existing calculations, but in future we might prioritize self + maintenanceMarginString = self.safe_string(position, 'maintMargin') + maintenanceMargin = self.parse_number(maintenanceMarginString) + initialMarginString = None + initialMarginPercentageString = None + leverageString = self.safe_string(position, 'leverage') + if leverageString is not None: + leverage = int(leverageString) + rational = self.is_round_number(1000 % leverage) + initialMarginPercentageString = Precise.string_div('1', leverageString, 8) + if not rational: + initialMarginPercentageString = Precise.string_add(initialMarginPercentageString, '1e-8') + unrounded = Precise.string_mul(notionalStringAbs, initialMarginPercentageString) + initialMarginString = Precise.string_div(unrounded, '1', 8) + else: + initialMarginString = self.safe_string(position, 'initialMargin') + unrounded = Precise.string_mul(initialMarginString, '1') + initialMarginPercentageString = Precise.string_div(unrounded, notionalStringAbs, 8) + marginRatio = None + percentage = None + if not Precise.string_equals(collateralString, '0'): + marginRatio = self.parse_number(Precise.string_div(Precise.string_add(Precise.string_div(maintenanceMarginString, collateralString), '5e-5'), '1', 4)) + percentage = self.parse_number(Precise.string_mul(Precise.string_div(unrealizedPnlString, initialMarginString, 4), '100')) + positionSide = self.safe_string(position, 'positionSide') + hedged = positionSide != 'BOTH' + return { + 'info': position, + 'id': None, + 'symbol': symbol, + 'contracts': contracts, + 'contractSize': contractSize, + 'unrealizedPnl': unrealizedPnl, + 'leverage': self.parse_number(leverageString), + 'liquidationPrice': liquidationPrice, + 'collateral': collateral, + 'notional': notional, + 'markPrice': markPrice, + 'entryPrice': entryPrice, + 'timestamp': timestamp, + 'initialMargin': self.parse_number(initialMarginString), + 'initialMarginPercentage': self.parse_number(initialMarginPercentageString), + 'maintenanceMargin': maintenanceMargin, + 'maintenanceMarginPercentage': maintenanceMarginPercentage, + 'marginRatio': marginRatio, + 'datetime': self.iso8601(timestamp), + 'marginMode': marginMode, + 'marginType': marginMode, # deprecated + 'side': side, + 'hedged': hedged, + 'percentage': percentage, + 'stopLossPrice': None, + 'takeProfitPrice': None, + } + + def load_leverage_brackets(self, reload=False, params={}): + self.load_markets() + # by default cache the leverage bracket + # it contains useful stuff like the maintenance margin and initial margin for positions + leverageBrackets = self.safe_dict(self.options, 'leverageBrackets') + if (leverageBrackets is None) or (reload): + defaultType = self.safe_string(self.options, 'defaultType', 'future') + type = self.safe_string(params, 'type', defaultType) + query = self.omit(params, 'type') + subType = None + subType, params = self.handle_sub_type_and_params('loadLeverageBrackets', None, params, 'linear') + isPortfolioMargin = None + isPortfolioMargin, params = self.handle_option_and_params_2(params, 'loadLeverageBrackets', 'papi', 'portfolioMargin', False) + response = None + if self.is_linear(type, subType): + if isPortfolioMargin: + response = self.papiGetUmLeverageBracket(query) + else: + response = self.fapiPrivateGetLeverageBracket(query) + elif self.is_inverse(type, subType): + if isPortfolioMargin: + response = self.papiGetCmLeverageBracket(query) + else: + response = self.dapiPrivateV2GetLeverageBracket(query) + else: + raise NotSupported(self.id + ' loadLeverageBrackets() supports linear and inverse contracts only') + self.options['leverageBrackets'] = self.create_safe_dictionary() + for i in range(0, len(response)): + entry = response[i] + marketId = self.safe_string(entry, 'symbol') + symbol = self.safe_symbol(marketId, None, None, 'contract') + brackets = self.safe_list(entry, 'brackets', []) + result = [] + for j in range(0, len(brackets)): + bracket = brackets[j] + floorValue = self.safe_string_2(bracket, 'notionalFloor', 'qtyFloor') + maintenanceMarginPercentage = self.safe_string(bracket, 'maintMarginRatio') + result.append([floorValue, maintenanceMarginPercentage]) + self.options['leverageBrackets'][symbol] = result + return self.options['leverageBrackets'] + + def fetch_leverage_tiers(self, symbols: Strings = None, params={}) -> LeverageTiers: + """ + retrieve information on the maximum leverage, and maintenance margin for trades of varying trade sizes + + https://developers.binance.com/docs/derivatives/usds-margined-futures/account/rest-api/Notional-and-Leverage-Brackets + https://developers.binance.com/docs/derivatives/coin-margined-futures/account/rest-api/Notional-Bracket-for-Pair + https://developers.binance.com/docs/derivatives/portfolio-margin/account/UM-Notional-and-Leverage-Brackets + https://developers.binance.com/docs/derivatives/portfolio-margin/account/CM-Notional-and-Leverage-Brackets + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.portfolioMargin]: set to True if you would like to fetch the leverage tiers for a portfolio margin account + :param str [params.subType]: "linear" or "inverse" + :returns dict: a dictionary of `leverage tiers structures `, indexed by market symbols + """ + self.load_markets() + type = None + type, params = self.handle_market_type_and_params('fetchLeverageTiers', None, params) + subType = None + subType, params = self.handle_sub_type_and_params('fetchLeverageTiers', None, params, 'linear') + isPortfolioMargin = None + isPortfolioMargin, params = self.handle_option_and_params_2(params, 'fetchLeverageTiers', 'papi', 'portfolioMargin', False) + response = None + if self.is_linear(type, subType): + if isPortfolioMargin: + response = self.papiGetUmLeverageBracket(params) + else: + response = self.fapiPrivateGetLeverageBracket(params) + elif self.is_inverse(type, subType): + if isPortfolioMargin: + response = self.papiGetCmLeverageBracket(params) + else: + response = self.dapiPrivateV2GetLeverageBracket(params) + else: + raise NotSupported(self.id + ' fetchLeverageTiers() supports linear and inverse contracts only') + # + # usdm + # + # [ + # { + # "symbol": "SUSHIUSDT", + # "brackets": [ + # { + # "bracket": 1, + # "initialLeverage": 50, + # "notionalCap": 50000, + # "notionalFloor": 0, + # "maintMarginRatio": 0.01, + # "cum": 0.0 + # }, + # ... + # ] + # } + # ] + # + # coinm + # + # [ + # { + # "symbol":"XRPUSD_210326", + # "brackets":[ + # { + # "bracket":1, + # "initialLeverage":20, + # "qtyCap":500000, + # "qtyFloor":0, + # "maintMarginRatio":0.0185, + # "cum":0.0 + # } + # ] + # } + # ] + # + return self.parse_leverage_tiers(response, symbols, 'symbol') + + def parse_market_leverage_tiers(self, info, market: Market = None) -> List[LeverageTier]: + """ + @ignore + :param dict info: Exchange response for 1 market + :param dict market: CCXT market + """ + # + # { + # "symbol": "SUSHIUSDT", + # "brackets": [ + # { + # "bracket": 1, + # "initialLeverage": 50, + # "notionalCap": 50000, + # "notionalFloor": 0, + # "maintMarginRatio": 0.01, + # "cum": 0.0 + # }, + # ... + # ] + # } + # + marketId = self.safe_string(info, 'symbol') + market = self.safe_market(marketId, market, None, 'contract') + brackets = self.safe_list(info, 'brackets', []) + tiers = [] + for j in range(0, len(brackets)): + bracket = brackets[j] + tiers.append({ + 'tier': self.safe_number(bracket, 'bracket'), + 'symbol': self.safe_symbol(marketId, market), + 'currency': market['quote'], + 'minNotional': self.safe_number_2(bracket, 'notionalFloor', 'qtyFloor'), + 'maxNotional': self.safe_number_2(bracket, 'notionalCap', 'qtyCap'), + 'maintenanceMarginRate': self.safe_number(bracket, 'maintMarginRatio'), + 'maxLeverage': self.safe_number(bracket, 'initialLeverage'), + 'info': bracket, + }) + return tiers + + def fetch_position(self, symbol: str, params={}): + """ + fetch data on an open position + + https://developers.binance.com/docs/derivatives/option/trade/Option-Position-Information + + :param str symbol: unified market symbol of the market the position is held in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `position structure ` + """ + self.load_markets() + market = self.market(symbol) + if not market['option']: + raise NotSupported(self.id + ' fetchPosition() supports option markets only') + request: dict = { + 'symbol': market['id'], + } + response = self.eapiPrivateGetPosition(self.extend(request, params)) + # + # [ + # { + # "entryPrice": "27.70000000", + # "symbol": "ETH-230426-1850-C", + # "side": "LONG", + # "quantity": "0.50000000", + # "reducibleQty": "0.50000000", + # "markValue": "10.250000000", + # "ror": "-0.2599", + # "unrealizedPNL": "-3.600000000", + # "markPrice": "20.5", + # "strikePrice": "1850.00000000", + # "positionCost": "13.85000000", + # "expiryDate": 1682496000000, + # "priceScale": 1, + # "quantityScale": 2, + # "optionSide": "CALL", + # "quoteAsset": "USDT", + # "time": 1682492427106 + # } + # ] + # + return self.parse_position(response[0], market) + + def fetch_option_positions(self, symbols: Strings = None, params={}): + """ + fetch data on open options positions + + https://developers.binance.com/docs/derivatives/option/trade/Option-Position-Information + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structures ` + """ + self.load_markets() + symbols = self.market_symbols(symbols) + request: dict = {} + market = None + if symbols is not None: + symbol = None + if isinstance(symbols, list): + symbolsLength = len(symbols) + if symbolsLength > 1: + raise BadRequest(self.id + ' fetchPositions() symbols argument cannot contain more than 1 symbol') + symbol = symbols[0] + else: + symbol = symbols + market = self.market(symbol) + request['symbol'] = market['id'] + response = self.eapiPrivateGetPosition(self.extend(request, params)) + # + # [ + # { + # "entryPrice": "27.70000000", + # "symbol": "ETH-230426-1850-C", + # "side": "LONG", + # "quantity": "0.50000000", + # "reducibleQty": "0.50000000", + # "markValue": "10.250000000", + # "ror": "-0.2599", + # "unrealizedPNL": "-3.600000000", + # "markPrice": "20.5", + # "strikePrice": "1850.00000000", + # "positionCost": "13.85000000", + # "expiryDate": 1682496000000, + # "priceScale": 1, + # "quantityScale": 2, + # "optionSide": "CALL", + # "quoteAsset": "USDT", + # "time": 1682492427106 + # } + # ] + # + result = [] + for i in range(0, len(response)): + result.append(self.parse_position(response[i], market)) + return self.filter_by_array_positions(result, 'symbol', symbols, False) + + def parse_position(self, position: dict, market: Market = None): + # + # { + # "entryPrice": "27.70000000", + # "symbol": "ETH-230426-1850-C", + # "side": "LONG", + # "quantity": "0.50000000", + # "reducibleQty": "0.50000000", + # "markValue": "10.250000000", + # "ror": "-0.2599", + # "unrealizedPNL": "-3.600000000", + # "markPrice": "20.5", + # "strikePrice": "1850.00000000", + # "positionCost": "13.85000000", + # "expiryDate": 1682496000000, + # "priceScale": 1, + # "quantityScale": 2, + # "optionSide": "CALL", + # "quoteAsset": "USDT", + # "time": 1682492427106 + # } + # + marketId = self.safe_string(position, 'symbol') + market = self.safe_market(marketId, market, None, 'swap') + symbol = market['symbol'] + side = self.safe_string_lower(position, 'side') + quantity = self.safe_string(position, 'quantity') + if side != 'long': + quantity = Precise.string_mul('-1', quantity) + timestamp = self.safe_integer(position, 'time') + return self.safe_position({ + 'info': position, + 'id': None, + 'symbol': symbol, + 'entryPrice': self.safe_number(position, 'entryPrice'), + 'markPrice': self.safe_number(position, 'markPrice'), + 'notional': self.safe_number(position, 'markValue'), + 'collateral': self.safe_number(position, 'positionCost'), + 'unrealizedPnl': self.safe_number(position, 'unrealizedPNL'), + 'side': side, + 'contracts': self.parse_number(quantity), + 'contractSize': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'hedged': None, + 'maintenanceMargin': None, + 'maintenanceMarginPercentage': None, + 'initialMargin': None, + 'initialMarginPercentage': None, + 'leverage': None, + 'liquidationPrice': None, + 'marginRatio': None, + 'marginMode': None, + 'percentage': None, + }) + + def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://developers.binance.com/docs/derivatives/usds-margined-futures/account/rest-api/Account-Information-V2 + https://developers.binance.com/docs/derivatives/coin-margined-futures/account/rest-api/Account-Information + https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Position-Information-V2 + https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/rest-api/Position-Information + https://developers.binance.com/docs/derivatives/option/trade/Option-Position-Information + + :param str[] [symbols]: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param dict [params.params]: extra parameters specific to the exchange API endpoint + :param str [params.method]: method name to call, "positionRisk", "account" or "option", default is "positionRisk" + :param bool [params.useV2]: set to True if you want to use the obsolete endpoint, where some more additional fields were provided + :returns dict[]: a list of `position structure ` + """ + defaultMethod = None + defaultMethod, params = self.handle_option_and_params(params, 'fetchPositions', 'method') + if defaultMethod is None: + options = self.safe_dict(self.options, 'fetchPositions') + if options is None: + defaultMethod = self.safe_string(self.options, 'fetchPositions', 'positionRisk') + else: + defaultMethod = 'positionRisk' + if defaultMethod == 'positionRisk': + return self.fetch_positions_risk(symbols, params) + elif defaultMethod == 'account': + return self.fetch_account_positions(symbols, params) + elif defaultMethod == 'option': + return self.fetch_option_positions(symbols, params) + else: + raise NotSupported(self.id + '.options["fetchPositions"]["method"] or params["method"] = "' + defaultMethod + '" is invalid, please choose between "account", "positionRisk" and "option"') + + def fetch_account_positions(self, symbols: Strings = None, params={}): + """ + @ignore + fetch account positions + + https://developers.binance.com/docs/derivatives/usds-margined-futures/account/rest-api/Account-Information-V2 + https://developers.binance.com/docs/derivatives/coin-margined-futures/account/rest-api/Account-Information + https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Position-Information-V2 + https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/rest-api/Position-Information + https://developers.binance.com/docs/derivatives/usds-margined-futures/account/rest-api/Account-Information-V3 + + :param str[] [symbols]: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.portfolioMargin]: set to True if you would like to fetch positions in a portfolio margin account + :param str [params.subType]: "linear" or "inverse" + :param boolean [params.filterClosed]: set to True if you would like to filter out closed positions, default is False + :param boolean [params.useV2]: set to True if you want to use obsolete endpoint, where some more additional fields were provided + :returns dict: data on account positions + """ + if symbols is not None: + if not isinstance(symbols, list): + raise ArgumentsRequired(self.id + ' fetchPositions() requires an array argument for symbols') + self.load_markets() + self.load_leverage_brackets(False, params) + defaultType = self.safe_string(self.options, 'defaultType', 'future') + type = self.safe_string(params, 'type', defaultType) + params = self.omit(params, 'type') + subType = None + subType, params = self.handle_sub_type_and_params('fetchAccountPositions', None, params, 'linear') + isPortfolioMargin = None + isPortfolioMargin, params = self.handle_option_and_params_2(params, 'fetchAccountPositions', 'papi', 'portfolioMargin', False) + response = None + if self.is_linear(type, subType): + if isPortfolioMargin: + response = self.papiV2GetUmAccount(params) + else: + useV2 = None + useV2, params = self.handle_option_and_params(params, 'fetchAccountPositions', 'useV2', False) + if not useV2: + response = self.fapiPrivateV3GetAccount(params) + else: + response = self.fapiPrivateV2GetAccount(params) + # + # { + # "totalInitialMargin": "99.62112386", + # "totalMaintMargin": "11.95453485", + # "totalWalletBalance": "99.84331553", + # "totalUnrealizedProfit": "11.17675690", + # "totalMarginBalance": "111.02007243", + # "totalPositionInitialMargin": "99.62112386", + # "totalOpenOrderInitialMargin": "0.00000000", + # "totalCrossWalletBalance": "99.84331553", + # "totalCrossUnPnl": "11.17675690", + # "availableBalance": "11.39894857", + # "maxWithdrawAmount": "11.39894857", + # "feeTier": "0", # in v2 + # "canTrade": True, # in v2 + # "canDeposit": True, # in v2 + # "canWithdraw": True, # in v2 + # "feeBurn": True, # in v2 + # "tradeGroupId": "-1",// in v2 + # "updateTime": "0", # in v2 + # "multiAssetsMargin": True # in v2 + # "assets": [ + # { + # "asset": "USDT", + # "walletBalance": "72.72317863", + # "unrealizedProfit": "11.17920750", + # "marginBalance": "83.90238613", + # "maintMargin": "11.95476475", + # "initialMargin": "99.62303962", + # "positionInitialMargin": "99.62303962", + # "openOrderInitialMargin": "0.00000000", + # "crossWalletBalance": "72.72317863", + # "crossUnPnl": "11.17920750", + # "availableBalance": "11.39916777", + # "maxWithdrawAmount": "11.39916777", + # "updateTime": "1721995605338", + # "marginAvailable": True # in v2 + # }, + # ... and some few supported settle currencies: USDC, BTC, ETH, BNB .. + # ], + # "positions": [ + # { + # "symbol": "WLDUSDT", + # "positionSide": "BOTH", + # "positionAmt": "-849", + # "unrealizedProfit": "11.17920750", + # "isolatedMargin": "0", + # "isolatedWallet": "0", + # "notional": "-1992.46079250", + # "initialMargin": "99.62303962", + # "maintMargin": "11.95476475", + # "updateTime": "1721995760449" + # "leverage": "50", # in v2 + # "entryPrice": "2.34", # in v2 + # "positionInitialMargin": "118.82116614", # in v2 + # "openOrderInitialMargin": "0", # in v2 + # "isolated": False, # in v2 + # "breakEvenPrice": "2.3395788", # in v2 + # "maxNotional": "25000", # in v2 + # "bidNotional": "0", # in v2 + # "askNotional": "0" # in v2 + # }, + # ... + # ] + # } + # + elif self.is_inverse(type, subType): + if isPortfolioMargin: + response = self.papiGetCmAccount(params) + else: + response = self.dapiPrivateGetAccount(params) + else: + raise NotSupported(self.id + ' fetchPositions() supports linear and inverse contracts only') + filterClosed = None + filterClosed, params = self.handle_option_and_params(params, 'fetchAccountPositions', 'filterClosed', False) + result = self.parse_account_positions(response, filterClosed) + symbols = self.market_symbols(symbols) + return self.filter_by_array_positions(result, 'symbol', symbols, False) + + def fetch_positions_risk(self, symbols: Strings = None, params={}): + """ + @ignore + fetch positions risk + + https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Position-Information-V2 + https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/rest-api/Position-Information + https://developers.binance.com/docs/derivatives/portfolio-margin/account/Query-UM-Position-Information + https://developers.binance.com/docs/derivatives/portfolio-margin/account/Query-CM-Position-Information + https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Position-Information-V3 + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.portfolioMargin]: set to True if you would like to fetch positions for a portfolio margin account + :param str [params.subType]: "linear" or "inverse" + :param bool [params.useV2]: set to True if you want to use the obsolete endpoint, where some more additional fields were provided + :returns dict: data on the positions risk + """ + if symbols is not None: + if not isinstance(symbols, list): + raise ArgumentsRequired(self.id + ' fetchPositionsRisk() requires an array argument for symbols') + self.load_markets() + self.load_leverage_brackets(False, params) + request: dict = {} + defaultType = 'future' + defaultType = self.safe_string(self.options, 'defaultType', defaultType) + type = self.safe_string(params, 'type', defaultType) + subType = None + subType, params = self.handle_sub_type_and_params('fetchPositionsRisk', None, params, 'linear') + isPortfolioMargin = None + isPortfolioMargin, params = self.handle_option_and_params_2(params, 'fetchPositionsRisk', 'papi', 'portfolioMargin', False) + params = self.omit(params, 'type') + response = None + if self.is_linear(type, subType): + if isPortfolioMargin: + response = self.papiGetUmPositionRisk(self.extend(request, params)) + else: + useV2 = None + useV2, params = self.handle_option_and_params(params, 'fetchPositionsRisk', 'useV2', False) + params = self.extend(request, params) + if not useV2: + response = self.fapiPrivateV3GetPositionRisk(params) + else: + response = self.fapiPrivateV2GetPositionRisk(params) + # + # [ + # { + # symbol: "WLDUSDT", + # positionSide: "BOTH", + # positionAmt: "5", + # entryPrice: "2.3483", + # breakEvenPrice: "2.349356735", + # markPrice: "2.39560000", + # unRealizedProfit: "0.23650000", + # liquidationPrice: "0", + # isolatedMargin: "0", + # notional: "11.97800000", + # isolatedWallet: "0", + # updateTime: "1722062678998", + # initialMargin: "2.39560000", # added in v3 + # maintMargin: "0.07186800", # added in v3 + # positionInitialMargin: "2.39560000", # added in v3 + # openOrderInitialMargin: "0", # added in v3 + # adl: "2", # added in v3 + # bidNotional: "0", # added in v3 + # askNotional: "0", # added in v3 + # marginAsset: "USDT", # added in v3 + # }, + # ] + # + elif self.is_inverse(type, subType): + if isPortfolioMargin: + response = self.papiGetCmPositionRisk(self.extend(request, params)) + else: + response = self.dapiPrivateGetPositionRisk(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchPositionsRisk() supports linear and inverse contracts only') + # ### Response examples ### + # + # For One-way position mode: + # + # [ + # { + # "symbol": "BTCUSDT", + # "positionSide": "BOTH", + # "positionAmt": "0.000", + # "entryPrice": "0.00000", + # "markPrice": "6679.50671178", + # "unRealizedProfit": "0.00000000", + # "liquidationPrice": "0", + # "isolatedMargin": "0.00000000", + # "marginType": "isolated", + # "isAutoAddMargin": "false", + # "leverage": "10", + # "maxNotionalValue": "20000000", + # "updateTime": 0 + # } + # ] + # + # For Hedge position mode: + # + # [ + # { + # "entryPrice": "6563.66500", + # "marginType": "isolated", + # "isAutoAddMargin": "false", + # "isolatedMargin": "15517.54150468", + # "leverage": "10", + # "liquidationPrice": "5930.78", + # "markPrice": "6679.50671178", + # "maxNotionalValue": "20000000", + # "positionSide": "LONG", + # "positionAmt": "20.000", # negative value for 'SHORT' + # "symbol": "BTCUSDT", + # "unRealizedProfit": "2316.83423560" + # "updateTime": 1625474304765 + # }, + # .. second dict is similar, but with `positionSide: SHORT` + # ] + # + # inverse portfolio margin: + # + # [ + # { + # "symbol": "ETHUSD_PERP", + # "positionAmt": "1", + # "entryPrice": "2422.400000007", + # "markPrice": "2424.51267823", + # "unRealizedProfit": "0.0000036", + # "liquidationPrice": "293.57678898", + # "leverage": "100", + # "positionSide": "LONG", + # "updateTime": 1707371941861, + # "maxQty": "15", + # "notionalValue": "0.00412454", + # "breakEvenPrice": "2423.368960034" + # } + # ] + # + # linear portfolio margin: + # + # [ + # { + # "symbol": "BTCUSDT", + # "positionAmt": "0.01", + # "entryPrice": "44525.0", + # "markPrice": "45464.1735922", + # "unRealizedProfit": "9.39173592", + # "liquidationPrice": "38007.16308568", + # "leverage": "100", + # "positionSide": "LONG", + # "updateTime": 1707371879042, + # "maxNotionalValue": "500000.0", + # "notional": "454.64173592", + # "breakEvenPrice": "44542.81" + # } + # ] + # + result = [] + for i in range(0, len(response)): + rawPosition = response[i] + entryPriceString = self.safe_string(rawPosition, 'entryPrice') + if Precise.string_gt(entryPriceString, '0'): + result.append(self.parse_position_risk(response[i])) + symbols = self.market_symbols(symbols) + return self.filter_by_array_positions(result, 'symbol', symbols, False) + + def fetch_funding_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch the history of funding payments paid and received on self account + + https://developers.binance.com/docs/derivatives/usds-margined-futures/account/rest-api/Get-Income-History + https://developers.binance.com/docs/derivatives/coin-margined-futures/account/rest-api/Get-Income-History + https://developers.binance.com/docs/derivatives/portfolio-margin/account/Get-UM-Income-History + https://developers.binance.com/docs/derivatives/portfolio-margin/account/Get-CM-Income-History + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch funding history for + :param int [limit]: the maximum number of funding history structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest funding history entry + :param boolean [params.portfolioMargin]: set to True if you would like to fetch the funding history for a portfolio margin account + :param str [params.subType]: "linear" or "inverse" + :returns dict: a `funding history structure ` + """ + self.load_markets() + market = None + request: dict = { + 'incomeType': 'FUNDING_FEE', # "TRANSFER","WELCOME_BONUS", "REALIZED_PNL","FUNDING_FEE", "COMMISSION" and "INSURANCE_CLEAR" + } + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if not market['swap']: + raise NotSupported(self.id + ' fetchFundingHistory() supports swap contracts only') + subType = None + subType, params = self.handle_sub_type_and_params('fetchFundingHistory', market, params, 'linear') + isPortfolioMargin = None + isPortfolioMargin, params = self.handle_option_and_params_2(params, 'fetchFundingHistory', 'papi', 'portfolioMargin', False) + request, params = self.handle_until_option('endTime', request, params) + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + defaultType = self.safe_string_2(self.options, 'fetchFundingHistory', 'defaultType', 'future') + type = self.safe_string(params, 'type', defaultType) + params = self.omit(params, 'type') + response = None + if self.is_linear(type, subType): + if isPortfolioMargin: + response = self.papiGetUmIncome(self.extend(request, params)) + else: + response = self.fapiPrivateGetIncome(self.extend(request, params)) + elif self.is_inverse(type, subType): + if isPortfolioMargin: + response = self.papiGetCmIncome(self.extend(request, params)) + else: + response = self.dapiPrivateGetIncome(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchFundingHistory() supports linear and inverse contracts only') + return self.parse_incomes(response, market, since, limit) + + def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Change-Initial-Leverage + https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/rest-api/Change-Initial-Leverage + https://developers.binance.com/docs/derivatives/portfolio-margin/account/Change-UM-Initial-Leverage + https://developers.binance.com/docs/derivatives/portfolio-margin/account/Change-CM-Initial-Leverage + + :param float leverage: the rate of leverage + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.portfolioMargin]: set to True if you would like to set the leverage for a trading pair in a portfolio margin account + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setLeverage() requires a symbol argument') + # WARNING: THIS WILL INCREASE LIQUIDATION PRICE FOR OPEN ISOLATED LONG POSITIONS + # AND DECREASE LIQUIDATION PRICE FOR OPEN ISOLATED SHORT POSITIONS + if (leverage < 1) or (leverage > 125): + raise BadRequest(self.id + ' leverage should be between 1 and 125') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'leverage': leverage, + } + isPortfolioMargin = None + isPortfolioMargin, params = self.handle_option_and_params_2(params, 'setLeverage', 'papi', 'portfolioMargin', False) + response = None + if market['linear']: + if isPortfolioMargin: + response = self.papiPostUmLeverage(self.extend(request, params)) + else: + response = self.fapiPrivatePostLeverage(self.extend(request, params)) + elif market['inverse']: + if isPortfolioMargin: + response = self.papiPostCmLeverage(self.extend(request, params)) + else: + response = self.dapiPrivatePostLeverage(self.extend(request, params)) + else: + raise NotSupported(self.id + ' setLeverage() supports linear and inverse contracts only') + return response + + def set_margin_mode(self, marginMode: str, symbol: Str = None, params={}): + """ + set margin mode to 'cross' or 'isolated' + + https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Change-Margin-Type + https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/rest-api/Change-Margin-Type + + :param str marginMode: 'cross' or 'isolated' + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setMarginMode() requires a symbol argument') + # + # {"code": -4048 , "msg": "Margin type cannot be changed if there exists position."} + # + # or + # + # {"code": 200, "msg": "success"} + # + marginMode = marginMode.upper() + if marginMode == 'CROSS': + marginMode = 'CROSSED' + if (marginMode != 'ISOLATED') and (marginMode != 'CROSSED'): + raise BadRequest(self.id + ' marginMode must be either isolated or cross') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'marginType': marginMode, + } + response = None + try: + if market['linear']: + response = self.fapiPrivatePostMarginType(self.extend(request, params)) + elif market['inverse']: + response = self.dapiPrivatePostMarginType(self.extend(request, params)) + else: + raise NotSupported(self.id + ' setMarginMode() supports linear and inverse contracts only') + except Exception as e: + # not an error + # https://github.com/ccxt/ccxt/issues/11268 + # https://github.com/ccxt/ccxt/pull/11624 + # POST https://fapi.binance.com/fapi/v1/marginType 400 Bad Request + # binanceusdm + if isinstance(e, MarginModeAlreadySet): + throwMarginModeAlreadySet = self.safe_bool(self.options, 'throwMarginModeAlreadySet', False) + if throwMarginModeAlreadySet: + raise e + else: + response = {'code': -4046, 'msg': 'No need to change margin type.'} + else: + raise e + return response + + def set_position_mode(self, hedged: bool, symbol: Str = None, params={}): + """ + set hedged to True or False for a market + + https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Change-Position-Mode + https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/rest-api/Change-Position-Mode + https://developers.binance.com/docs/derivatives/portfolio-margin/account/Get-UM-Current-Position-Mode + https://developers.binance.com/docs/derivatives/portfolio-margin/account/Get-CM-Current-Position-Mode + + :param bool hedged: set to True to use dualSidePosition + :param str symbol: not used by binance setPositionMode() + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.portfolioMargin]: set to True if you would like to set the position mode for a portfolio margin account + :param str [params.subType]: "linear" or "inverse" + :returns dict: response from the exchange + """ + market = None + if symbol is not None: + market = self.market(symbol) + type = None + type, params = self.handle_market_type_and_params('setPositionMode', market, params) + subType = None + subType, params = self.handle_sub_type_and_params('setPositionMode', market, params) + isPortfolioMargin = None + isPortfolioMargin, params = self.handle_option_and_params_2(params, 'setPositionMode', 'papi', 'portfolioMargin', False) + dualSidePosition = None + if hedged: + dualSidePosition = 'true' + else: + dualSidePosition = 'false' + request: dict = { + 'dualSidePosition': dualSidePosition, + } + response = None + if self.is_inverse(type, subType): + if isPortfolioMargin: + response = self.papiPostCmPositionSideDual(self.extend(request, params)) + else: + response = self.dapiPrivatePostPositionSideDual(self.extend(request, params)) + elif self.is_linear(type, subType): + if isPortfolioMargin: + response = self.papiPostUmPositionSideDual(self.extend(request, params)) + else: + response = self.fapiPrivatePostPositionSideDual(self.extend(request, params)) + else: + raise BadRequest(self.id + ' setPositionMode() supports linear and inverse contracts only') + # + # { + # "code": 200, + # "msg": "success" + # } + # + return response + + def fetch_leverages(self, symbols: Strings = None, params={}) -> Leverages: + """ + fetch the set leverage for all markets + + https://developers.binance.com/docs/derivatives/usds-margined-futures/account/rest-api/Account-Information-V2 + https://developers.binance.com/docs/derivatives/coin-margined-futures/account/rest-api/Account-Information + https://developers.binance.com/docs/derivatives/portfolio-margin/account/Get-UM-Account-Detail + https://developers.binance.com/docs/derivatives/portfolio-margin/account/Get-CM-Account-Detail + https://developers.binance.com/docs/derivatives/usds-margined-futures/account/rest-api/Symbol-Config + + :param str[] [symbols]: a list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.subType]: "linear" or "inverse" + :returns dict: a list of `leverage structures ` + """ + self.load_markets() + self.load_leverage_brackets(False, params) + type = None + type, params = self.handle_market_type_and_params('fetchLeverages', None, params) + subType = None + subType, params = self.handle_sub_type_and_params('fetchLeverages', None, params, 'linear') + isPortfolioMargin = None + isPortfolioMargin, params = self.handle_option_and_params_2(params, 'fetchLeverages', 'papi', 'portfolioMargin', False) + response = None + if self.is_linear(type, subType): + if isPortfolioMargin: + response = self.papiGetUmAccount(params) + else: + response = self.fapiPrivateGetSymbolConfig(params) + elif self.is_inverse(type, subType): + if isPortfolioMargin: + response = self.papiGetCmAccount(params) + else: + response = self.dapiPrivateGetAccount(params) + else: + raise NotSupported(self.id + ' fetchLeverages() supports linear and inverse contracts only') + leverages = self.safe_list(response, 'positions', []) + if isinstance(response, list): + leverages = response + return self.parse_leverages(leverages, symbols, 'symbol') + + def parse_leverage(self, leverage: dict, market: Market = None) -> Leverage: + marketId = self.safe_string(leverage, 'symbol') + marginModeRaw = self.safe_bool(leverage, 'isolated') + marginMode = None + if marginModeRaw is not None: + marginMode = 'isolated' if marginModeRaw else 'cross' + marginTypeRaw = self.safe_string_lower(leverage, 'marginType') + if marginTypeRaw is not None: + marginMode = 'cross' if (marginTypeRaw == 'crossed') else 'isolated' + side = self.safe_string_lower(leverage, 'positionSide') + longLeverage = None + shortLeverage = None + leverageValue = self.safe_integer(leverage, 'leverage') + if (side is None) or (side == 'both'): + longLeverage = leverageValue + shortLeverage = leverageValue + elif side == 'long': + longLeverage = leverageValue + elif side == 'short': + shortLeverage = leverageValue + return { + 'info': leverage, + 'symbol': self.safe_symbol(marketId, market), + 'marginMode': marginMode, + 'longLeverage': longLeverage, + 'shortLeverage': shortLeverage, + } + + def fetch_settlement_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical settlement records + + https://developers.binance.com/docs/derivatives/option/market-data/Historical-Exercise-Records + + :param str symbol: unified market symbol of the settlement history + :param int [since]: timestamp in ms + :param int [limit]: number of records, default 100, max 100 + :param dict [params]: exchange specific params + :returns dict[]: a list of `settlement history objects ` + """ + self.load_markets() + market = None if (symbol is None) else self.market(symbol) + type = None + type, params = self.handle_market_type_and_params('fetchSettlementHistory', market, params) + if type != 'option': + raise NotSupported(self.id + ' fetchSettlementHistory() supports option markets only') + request: dict = {} + if symbol is not None: + symbol = market['symbol'] + request['underlying'] = market['baseId'] + market['quoteId'] + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + response = self.eapiPublicGetExerciseHistory(self.extend(request, params)) + # + # [ + # { + # "symbol": "ETH-230223-1900-P", + # "strikePrice": "1900", + # "realStrikePrice": "1665.5897334", + # "expiryDate": 1677139200000, + # "strikeResult": "REALISTIC_VALUE_STRICKEN" + # } + # ] + # + settlements = self.parse_settlements(response, market) + sorted = self.sort_by(settlements, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, symbol, since, limit) + + def fetch_my_settlement_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical settlement records of the user + + https://developers.binance.com/docs/derivatives/option/trade/User-Exercise-Record + + :param str symbol: unified market symbol of the settlement history + :param int [since]: timestamp in ms + :param int [limit]: number of records + :param dict [params]: exchange specific params + :returns dict[]: a list of [settlement history objects] + """ + self.load_markets() + market = None if (symbol is None) else self.market(symbol) + type = None + type, params = self.handle_market_type_and_params('fetchMySettlementHistory', market, params) + if type != 'option': + raise NotSupported(self.id + ' fetchMySettlementHistory() supports option markets only') + request: dict = {} + if symbol is not None: + request['symbol'] = market['id'] + symbol = market['symbol'] + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + response = self.eapiPrivateGetExerciseRecord(self.extend(request, params)) + # + # [ + # { + # "id": "1125899906842897036", + # "currency": "USDT", + # "symbol": "BTC-230728-30000-C", + # "exercisePrice": "30000.00000000", + # "markPrice": "29160.71284993", + # "quantity": "1.00000000", + # "amount": "0.00000000", + # "fee": "0.00000000", + # "createDate": 1690531200000, + # "priceScale": 0, + # "quantityScale": 2, + # "optionSide": "CALL", + # "positionSide": "LONG", + # "quoteAsset": "USDT" + # } + # ] + # + settlements = self.parse_settlements(response, market) + sorted = self.sort_by(settlements, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, symbol, since, limit) + + def parse_settlement(self, settlement, market): + # + # fetchSettlementHistory + # + # { + # "symbol": "ETH-230223-1900-P", + # "strikePrice": "1900", + # "realStrikePrice": "1665.5897334", + # "expiryDate": 1677139200000, + # "strikeResult": "REALISTIC_VALUE_STRICKEN" + # } + # + # fetchMySettlementHistory + # + # { + # "id": "1125899906842897036", + # "currency": "USDT", + # "symbol": "BTC-230728-30000-C", + # "exercisePrice": "30000.00000000", + # "markPrice": "29160.71284993", + # "quantity": "1.00000000", + # "amount": "0.00000000", + # "fee": "0.00000000", + # "createDate": 1690531200000, + # "priceScale": 0, + # "quantityScale": 2, + # "optionSide": "CALL", + # "positionSide": "LONG", + # "quoteAsset": "USDT" + # } + # + timestamp = self.safe_integer_2(settlement, 'expiryDate', 'createDate') + marketId = self.safe_string(settlement, 'symbol') + return { + 'info': settlement, + 'symbol': self.safe_symbol(marketId, market), + 'price': self.safe_number_2(settlement, 'realStrikePrice', 'exercisePrice'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + + def parse_settlements(self, settlements, market): + # + # fetchSettlementHistory + # + # [ + # { + # "symbol": "ETH-230223-1900-P", + # "strikePrice": "1900", + # "realStrikePrice": "1665.5897334", + # "expiryDate": 1677139200000, + # "strikeResult": "EXTRINSIC_VALUE_EXPIRED" + # } + # ] + # + # fetchMySettlementHistory + # + # [ + # { + # "id": "1125899906842897036", + # "currency": "USDT", + # "symbol": "BTC-230728-30000-C", + # "exercisePrice": "30000.00000000", + # "markPrice": "29160.71284993", + # "quantity": "1.00000000", + # "amount": "0.00000000", + # "fee": "0.00000000", + # "createDate": 1690531200000, + # "priceScale": 0, + # "quantityScale": 2, + # "optionSide": "CALL", + # "positionSide": "LONG", + # "quoteAsset": "USDT" + # } + # ] + # + result = [] + for i in range(0, len(settlements)): + result.append(self.parse_settlement(settlements[i], market)) + return result + + def fetch_ledger_entry(self, id: str, code: Str = None, params={}) -> LedgerEntry: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + + https://developers.binance.com/docs/derivatives/option/account/Account-Funding-Flow + + :param str id: the identification number of the ledger entry + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ledger structure ` + """ + self.load_markets() + type = None + type, params = self.handle_market_type_and_params('fetchLedgerEntry', None, params) + if type != 'option': + raise BadRequest(self.id + ' fetchLedgerEntry() can only be used for type option') + self.check_required_argument('fetchLedgerEntry', code, 'code') + currency = self.currency(code) + request: dict = { + 'recordId': id, + 'currency': currency['id'], + } + response = self.eapiPrivateGetBill(self.extend(request, params)) + # + # [ + # { + # "id": "1125899906845701870", + # "asset": "USDT", + # "amount": "-0.16518203", + # "type": "FEE", + # "createDate": 1676621042489 + # } + # ] + # + first = self.safe_dict(response, 0, response) + return self.parse_ledger_entry(first, currency) + + def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + + https://developers.binance.com/docs/derivatives/option/account/Account-Funding-Flow + https://developers.binance.com/docs/derivatives/usds-margined-futures/account/rest-api/Get-Income-History + https://developers.binance.com/docs/derivatives/coin-margined-futures/account/rest-api/Get-Income-History + https://developers.binance.com/docs/derivatives/portfolio-margin/account/Get-UM-Income-History + https://developers.binance.com/docs/derivatives/portfolio-margin/account/Get-CM-Income-History + + :param str [code]: unified currency code + :param int [since]: timestamp in ms of the earliest ledger entry + :param int [limit]: max number of ledger entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest ledger entry + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param boolean [params.portfolioMargin]: set to True if you would like to fetch the ledger for a portfolio margin account + :param str [params.subType]: "linear" or "inverse" + :returns dict: a `ledger structure ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchLedger', 'paginate') + if paginate: + return self.fetch_paginated_call_dynamic('fetchLedger', code, since, limit, params, None, False) + type = None + subType = None + currency = None + if code is not None: + currency = self.currency(code) + request: dict = {} + type, params = self.handle_market_type_and_params('fetchLedger', None, params) + subType, params = self.handle_sub_type_and_params('fetchLedger', None, params) + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + until = self.safe_integer(params, 'until') + if until is not None: + params = self.omit(params, 'until') + request['endTime'] = until + isPortfolioMargin = None + isPortfolioMargin, params = self.handle_option_and_params_2(params, 'fetchLedger', 'papi', 'portfolioMargin', False) + response = None + if type == 'option': + self.check_required_argument('fetchLedger', code, 'code') + request['currency'] = currency['id'] + response = self.eapiPrivateGetBill(self.extend(request, params)) + elif self.is_linear(type, subType): + if isPortfolioMargin: + response = self.papiGetUmIncome(self.extend(request, params)) + else: + response = self.fapiPrivateGetIncome(self.extend(request, params)) + elif self.is_inverse(type, subType): + if isPortfolioMargin: + response = self.papiGetCmIncome(self.extend(request, params)) + else: + response = self.dapiPrivateGetIncome(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchLedger() supports contract wallets only') + # + # options(eapi) + # + # [ + # { + # "id": "1125899906845701870", + # "asset": "USDT", + # "amount": "-0.16518203", + # "type": "FEE", + # "createDate": 1676621042489 + # } + # ] + # + # futures(fapi, dapi, papi) + # + # [ + # { + # "symbol": "", + # "incomeType": "TRANSFER", + # "income": "10.00000000", + # "asset": "USDT", + # "time": 1677645250000, + # "info": "TRANSFER", + # "tranId": 131001573082, + # "tradeId": "" + # } + # ] + # + return self.parse_ledger(response, currency, since, limit) + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + # + # options(eapi) + # + # { + # "id": "1125899906845701870", + # "asset": "USDT", + # "amount": "-0.16518203", + # "type": "FEE", + # "createDate": 167662104241 + # } + # + # futures(fapi, dapi, papi) + # + # { + # "symbol": "", + # "incomeType": "TRANSFER", + # "income": "10.00000000", + # "asset": "USDT", + # "time": 1677645250000, + # "info": "TRANSFER", + # "tranId": 131001573082, + # "tradeId": "" + # } + # + amount = self.safe_string_2(item, 'amount', 'income') + direction = None + if Precise.string_le(amount, '0'): + direction = 'out' + amount = Precise.string_mul('-1', amount) + else: + direction = 'in' + currencyId = self.safe_string(item, 'asset') + code = self.safe_currency_code(currencyId, currency) + currency = self.safe_currency(currencyId, currency) + timestamp = self.safe_integer_2(item, 'createDate', 'time') + type = self.safe_string_2(item, 'type', 'incomeType') + return self.safe_ledger_entry({ + 'info': item, + 'id': self.safe_string_2(item, 'id', 'tranId'), + 'direction': direction, + 'account': None, + 'referenceAccount': None, + 'referenceId': self.safe_string(item, 'tradeId'), + 'type': self.parse_ledger_entry_type(type), + 'currency': code, + 'amount': self.parse_number(amount), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'before': None, + 'after': None, + 'status': None, + 'fee': None, + }, currency) + + def parse_ledger_entry_type(self, type): + ledgerType: dict = { + 'FEE': 'fee', + 'FUNDING_FEE': 'fee', + 'OPTIONS_PREMIUM_FEE': 'fee', + 'POSITION_LIMIT_INCREASE_FEE': 'fee', + 'CONTRACT': 'trade', + 'REALIZED_PNL': 'trade', + 'TRANSFER': 'transfer', + 'CROSS_COLLATERAL_TRANSFER': 'transfer', + 'INTERNAL_TRANSFER': 'transfer', + 'COIN_SWAP_DEPOSIT': 'deposit', + 'COIN_SWAP_WITHDRAW': 'withdrawal', + 'OPTIONS_SETTLE_PROFIT': 'settlement', + 'DELIVERED_SETTELMENT': 'settlement', + 'WELCOME_BONUS': 'cashback', + 'CONTEST_REWARD': 'cashback', + 'COMMISSION_REBATE': 'rebate', + 'API_REBATE': 'rebate', + 'REFERRAL_KICKBACK': 'referral', + 'COMMISSION': 'commission', + } + return self.safe_string(ledgerType, type, type) + + def get_network_code_by_network_url(self, currencyCode: str, depositUrl: Str = None) -> Str: + # depositUrl is like : https://bscscan.com/address/0xEF238AB229342849.. + if depositUrl is None: + return None + networkCode = None + currency = self.currency(currencyCode) + networks = self.safe_dict(currency, 'networks', {}) + networkCodes = list(networks.keys()) + for i in range(0, len(networkCodes)): + currentNetworkCode = networkCodes[i] + info = self.safe_dict(networks[currentNetworkCode], 'info', {}) + siteUrl = self.safe_string(info, 'contractAddressUrl') + # check if url matches the field's value + if siteUrl is not None and depositUrl.startswith(self.get_base_domain_from_url(siteUrl)): + networkCode = currentNetworkCode + return networkCode + + def get_base_domain_from_url(self, url: Str) -> Str: + if url is None: + return None + urlParts = url.split('/') + scheme = self.safe_string(urlParts, 0) + if scheme is None: + return None + domain = self.safe_string(urlParts, 2) + if domain is None: + return None + return scheme + '//' + domain + '/' + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + urls = self.urls + if not (api in urls['api']): + raise NotSupported(self.id + ' does not have a testnet/sandbox URL for ' + api + ' endpoints') + url = self.urls['api'][api] + url += '/' + path + if path == 'historicalTrades': + if self.apiKey: + headers = { + 'X-MBX-APIKEY': self.apiKey, + } + else: + raise AuthenticationError(self.id + ' historicalTrades endpoint requires `apiKey` credential') + userDataStream = (path == 'userDataStream') or (path == 'listenKey') + if userDataStream: + if self.apiKey: + # v1 special case for userDataStream + headers = { + 'X-MBX-APIKEY': self.apiKey, + 'Content-Type': 'application/x-www-form-urlencoded', + } + if method != 'GET': + body = self.urlencode(params) + else: + raise AuthenticationError(self.id + ' userDataStream endpoint requires `apiKey` credential') + elif (api == 'private') or (api == 'eapiPrivate') or (api == 'sapi' and path != 'system/status') or (api == 'sapiV2') or (api == 'sapiV3') or (api == 'sapiV4') or (api == 'dapiPrivate') or (api == 'dapiPrivateV2') or (api == 'fapiPrivate') or (api == 'fapiPrivateV2') or (api == 'fapiPrivateV3') or (api == 'papiV2' or api == 'papi' and path != 'ping'): + self.check_required_credentials() + if (url.find('testnet.binancefuture.com') > -1) and self.isSandboxModeEnabled and (not self.safe_bool(self.options, 'disableFuturesSandboxWarning')): + raise NotSupported(self.id + ' testnet/sandbox mode is not supported for futures anymore, please check the deprecation announcement https://t.me/ccxt_announcements/92 and consider using the demo trading instead.') + if method == 'POST' and ((path == 'order') or (path == 'sor/order')): + # inject in implicit API calls + newClientOrderId = self.safe_string(params, 'newClientOrderId') + if newClientOrderId is None: + isSpotOrMargin = (api.find('sapi') > -1 or api == 'private') + marketType = 'spot' if isSpotOrMargin else 'future' + defaultId = 'x-xcKtGhcu' if (not isSpotOrMargin) else 'x-TKT5PX2F' + broker = self.safe_dict(self.options, 'broker', {}) + brokerId = self.safe_string(broker, marketType, defaultId) + params['newClientOrderId'] = brokerId + self.uuid22() + query = None + # handle batchOrders + if (path == 'batchOrders') and ((method == 'POST') or (method == 'PUT')): + batchOrders = self.safe_list(params, 'batchOrders') + checkedBatchOrders = batchOrders + if method == 'POST' and api == 'fapiPrivate': + # check broker id if batchOrders are called with fapiPrivatePostBatchOrders + checkedBatchOrders = [] + for i in range(0, len(batchOrders)): + batchOrder = batchOrders[i] + newClientOrderId = self.safe_string(batchOrder, 'newClientOrderId') + if newClientOrderId is None: + defaultId = 'x-xcKtGhcu' # batchOrders can not be spot or margin + broker = self.safe_dict(self.options, 'broker', {}) + brokerId = self.safe_string(broker, 'future', defaultId) + newClientOrderId = brokerId + self.uuid22() + batchOrder['newClientOrderId'] = newClientOrderId + checkedBatchOrders.append(batchOrder) + queryBatch = (self.json(checkedBatchOrders)) + params['batchOrders'] = queryBatch + defaultRecvWindow = self.safe_integer(self.options, 'recvWindow') + extendedParams = self.extend({ + 'timestamp': self.nonce(), + }, params) + if defaultRecvWindow is not None: + extendedParams['recvWindow'] = defaultRecvWindow + recvWindow = self.safe_integer(params, 'recvWindow') + if recvWindow is not None: + extendedParams['recvWindow'] = recvWindow + if (api == 'sapi') and (path == 'asset/dust'): + query = self.urlencode_with_array_repeat(extendedParams) + elif (path == 'batchOrders') or (path.find('sub-account') >= 0) or (path == 'capital/withdraw/apply') or (path.find('staking') >= 0) or (path.find('simple-earn') >= 0): + if (method == 'DELETE') and (path == 'batchOrders'): + orderidlist = self.safe_list(extendedParams, 'orderidlist', []) + origclientorderidlist = self.safe_list_2(extendedParams, 'origclientorderidlist', 'origClientOrderIdList', []) + extendedParams = self.omit(extendedParams, ['orderidlist', 'origclientorderidlist', 'origClientOrderIdList']) + if 'symbol' in extendedParams: + extendedParams['symbol'] = self.encode_uri_component(extendedParams['symbol']) + query = self.rawencode(extendedParams) + orderidlistLength = len(orderidlist) + origclientorderidlistLength = len(origclientorderidlist) + if orderidlistLength > 0: + query = query + '&' + 'orderidlist=%5B' + '%2C'.join(orderidlist) + '%5D' + if origclientorderidlistLength > 0: + # wrap clientOrderids around "" + newClientOrderIds = [] + for i in range(0, origclientorderidlistLength): + newClientOrderIds.append('%22' + origclientorderidlist[i] + '%22') + query = query + '&' + 'origclientorderidlist=%5B' + '%2C'.join(newClientOrderIds) + '%5D' + else: + query = self.rawencode(extendedParams) + else: + query = self.urlencode(extendedParams) + signature = None + if self.secret.find('PRIVATE KEY') > -1: + if len(self.secret) > 120: + signature = self.encode_uri_component(self.rsa(query, self.secret, 'sha256')) + else: + signature = self.encode_uri_component(self.eddsa(self.encode(query), self.secret, 'ed25519')) + else: + signature = self.hmac(self.encode(query), self.encode(self.secret), hashlib.sha256) + query += '&' + 'signature=' + signature + headers = { + 'X-MBX-APIKEY': self.apiKey, + } + if (method == 'GET') or (method == 'DELETE'): + url += '?' + query + else: + body = query + headers['Content-Type'] = 'application/x-www-form-urlencoded' + else: + if params: + url += '?' + self.urlencode(params) + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def get_exceptions_by_url(self, url: str, exactOrBroad: str): + marketType = None + hostname = self.hostname if (self.hostname is not None) else 'binance.com' + if url.startswith('https://api.' + hostname + '/') or url.startswith('https://testnet.binance.vision'): + marketType = 'spot' + elif url.startswith('https://dapi.' + hostname + '/') or url.startswith('https://testnet.binancefuture.com/dapi'): + marketType = 'inverse' + elif url.startswith('https://fapi.' + hostname + '/') or url.startswith('https://testnet.binancefuture.com/fapi'): + marketType = 'linear' + elif url.startswith('https://eapi.' + hostname + '/'): + marketType = 'option' + elif url.startswith('https://papi.' + hostname + '/'): + marketType = 'portfolioMargin' + if marketType is not None: + exceptionsForMarketType = self.safe_dict(self.exceptions, marketType, {}) + return self.safe_dict(exceptionsForMarketType, exactOrBroad, {}) + return {} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if (code == 418) or (code == 429): + raise DDoSProtection(self.id + ' ' + str(code) + ' ' + reason + ' ' + body) + # error response in a form: {"code": -1013, "msg": "Invalid quantity."} + # following block cointains legacy checks against message patterns in "msg" property + # will switch "code" checks eventually, when we know all of them + if code >= 400: + if body.find('Price * QTY is zero or less') >= 0: + raise InvalidOrder(self.id + ' order cost = amount * price is zero or less ' + body) + if body.find('LOT_SIZE') >= 0: + raise InvalidOrder(self.id + ' order amount should be evenly divisible by lot size ' + body) + if body.find('PRICE_FILTER') >= 0: + raise InvalidOrder(self.id + ' order price is invalid, i.e. exceeds allowed price precision, exceeds min price or max price limits or is invalid value in general, use self.price_to_precision(symbol, amount) ' + body) + if response is None: + return None # fallback to default error handler + # response in format {'msg': 'The coin does not exist.', 'success': True/false} + success = self.safe_bool(response, 'success', True) + if not success: + messageNew = self.safe_string(response, 'msg') + parsedMessage = None + if messageNew is not None: + try: + parsedMessage = json.loads(messageNew) + except Exception as e: + # do nothing + parsedMessage = None + if parsedMessage is not None: + response = parsedMessage + message = self.safe_string(response, 'msg') + if message is not None: + self.throw_exactly_matched_exception(self.get_exceptions_by_url(url, 'exact'), message, self.id + ' ' + message) + self.throw_exactly_matched_exception(self.exceptions['exact'], message, self.id + ' ' + message) + self.throw_broadly_matched_exception(self.get_exceptions_by_url(url, 'broad'), message, self.id + ' ' + message) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, self.id + ' ' + message) + # checks against error codes + error = self.safe_string(response, 'code') + if error is not None: + # https://github.com/ccxt/ccxt/issues/6501 + # https://github.com/ccxt/ccxt/issues/7742 + if (error == '200') or Precise.string_equals(error, '0'): + return None + # a workaround for {"code":-2015,"msg":"Invalid API-key, IP, or permissions for action."} + # despite that their message is very confusing, it is raised by Binance + # on a temporary ban, the API key is valid, but disabled for a while + if (error == '-2015') and self.options['hasAlreadyAuthenticatedSuccessfully']: + raise DDoSProtection(self.id + ' ' + body) + feedback = self.id + ' ' + body + if message == 'No need to change margin type.': + # not an error + # https://github.com/ccxt/ccxt/issues/11268 + # https://github.com/ccxt/ccxt/pull/11624 + # POST https://fapi.binance.com/fapi/v1/marginType 400 Bad Request + # binanceusdm {"code":-4046,"msg":"No need to change margin type."} + raise MarginModeAlreadySet(feedback) + self.throw_exactly_matched_exception(self.get_exceptions_by_url(url, 'exact'), error, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], error, feedback) + raise ExchangeError(feedback) + if not success: + raise ExchangeError(self.id + ' ' + body) + if isinstance(response, list): + # cancelOrders returns an array like self: [{"code":-2011,"msg":"Unknown order sent."}] + arrayLength = len(response) + if arrayLength == 1: # when there's a single error we can throw, otherwise we have a partial success + element = response[0] + errorCode = self.safe_string(element, 'code') + if errorCode is not None: + self.throw_exactly_matched_exception(self.get_exceptions_by_url(url, 'exact'), errorCode, self.id + ' ' + body) + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, self.id + ' ' + body) + return None + + def calculate_rate_limiter_cost(self, api, method, path, params, config={}): + if ('noCoin' in config) and not ('coin' in params): + return config['noCoin'] + elif ('noSymbol' in config) and not ('symbol' in params): + return config['noSymbol'] + elif ('noPoolId' in config) and not ('poolId' in params): + return config['noPoolId'] + elif ('byLimit' in config) and ('limit' in params): + limit = params['limit'] + byLimit = config['byLimit'] + for i in range(0, len(byLimit)): + entry = byLimit[i] + if limit <= entry[0]: + return entry[1] + return self.safe_value(config, 'cost', 1) + + def request(self, path, api='public', method='GET', params={}, headers=None, body=None, config={}): + response = self.fetch2(path, api, method, params, headers, body, config) + # a workaround for {"code":-2015,"msg":"Invalid API-key, IP, or permissions for action."} + if api == 'private': + self.options['hasAlreadyAuthenticatedSuccessfully'] = True + return response + + def modify_margin_helper(self, symbol: str, amount, addOrReduce, params={}): + # used to modify isolated positions + defaultType = self.safe_string(self.options, 'defaultType', 'future') + if defaultType == 'spot': + defaultType = 'future' + type = self.safe_string(params, 'type', defaultType) + if (type == 'margin') or (type == 'spot'): + raise NotSupported(self.id + ' add / reduce margin only supported with type future or delivery') + self.load_markets() + market = self.market(symbol) + amount = self.amount_to_precision(symbol, amount) + request: dict = { + 'type': addOrReduce, + 'symbol': market['id'], + 'amount': amount, + } + response = None + code = None + if market['linear']: + code = market['quote'] + response = self.fapiPrivatePostPositionMargin(self.extend(request, params)) + else: + code = market['base'] + response = self.dapiPrivatePostPositionMargin(self.extend(request, params)) + # + # { + # "code": 200, + # "msg": "Successfully modify position margin.", + # "amount": 0.001, + # "type": 1 + # } + # + return self.extend(self.parse_margin_modification(response, market), { + 'code': code, + }) + + def parse_margin_modification(self, data: dict, market: Market = None) -> MarginModification: + # + # add/reduce margin + # + # { + # "code": 200, + # "msg": "Successfully modify position margin.", + # "amount": 0.001, + # "type": 1 + # } + # + # fetchMarginAdjustmentHistory + # + # { + # symbol: "XRPUSDT", + # type: "1", + # deltaType: "TRADE", + # amount: "2.57148240", + # asset: "USDT", + # time: "1711046271555", + # positionSide: "BOTH", + # clientTranId: "" + # } + # + rawType = self.safe_integer(data, 'type') + errorCode = self.safe_string(data, 'code') + marketId = self.safe_string(data, 'symbol') + timestamp = self.safe_integer(data, 'time') + market = self.safe_market(marketId, market, None, 'swap') + noErrorCode = errorCode is None + success = errorCode == '200' + return { + 'info': data, + 'symbol': market['symbol'], + 'type': 'add' if (rawType == 1) else 'reduce', + 'marginMode': 'isolated', + 'amount': self.safe_number(data, 'amount'), + 'code': self.safe_string(data, 'asset'), + 'total': None, + 'status': 'ok' if (success or noErrorCode) else 'failed', + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + + def reduce_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + remove margin from a position + + https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Modify-Isolated-Position-Margin + https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/rest-api/Modify-Isolated-Position-Margin + + :param str symbol: unified market symbol + :param float amount: the amount of margin to remove + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin structure ` + """ + return self.modify_margin_helper(symbol, amount, 2, params) + + def add_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + add margin + + https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Modify-Isolated-Position-Margin + https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/rest-api/Modify-Isolated-Position-Margin + + :param str symbol: unified market symbol + :param float amount: amount of margin to add + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin structure ` + """ + return self.modify_margin_helper(symbol, amount, 1, params) + + def fetch_cross_borrow_rate(self, code: str, params={}) -> CrossBorrowRate: + """ + fetch the rate of interest to borrow a currency for margin trading + + https://developers.binance.com/docs/margin_trading/borrow-and-repay/Query-Margin-Interest-Rate-History + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `borrow rate structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'asset': currency['id'], + # 'vipLevel': self.safe_integer(params, 'vipLevel'), + } + response = self.sapiGetMarginInterestRateHistory(self.extend(request, params)) + # + # [ + # { + # "asset": "USDT", + # "timestamp": 1638230400000, + # "dailyInterestRate": "0.0006", + # "vipLevel": 0 + # }, + # ] + # + rate = self.safe_dict(response, 0) + return self.parse_borrow_rate(rate) + + def fetch_isolated_borrow_rate(self, symbol: str, params={}) -> IsolatedBorrowRate: + """ + fetch the rate of interest to borrow a currency for margin trading + + https://developers.binance.com/docs/margin_trading/account/Query-Isolated-Margin-Fee-Data + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + + EXCHANGE SPECIFIC PARAMETERS + :param dict [params.vipLevel]: user's current specific margin data will be returned if viplevel is omitted + :returns dict: an `isolated borrow rate structure ` + """ + request: dict = { + 'symbol': symbol, + } + borrowRates = self.fetch_isolated_borrow_rates(self.extend(request, params)) + return self.safe_dict(borrowRates, symbol) + + def fetch_isolated_borrow_rates(self, params={}) -> IsolatedBorrowRates: + """ + fetch the borrow interest rates of all currencies + + https://developers.binance.com/docs/margin_trading/account/Query-Isolated-Margin-Fee-Data + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param dict [params.symbol]: unified market symbol + + EXCHANGE SPECIFIC PARAMETERS + :param dict [params.vipLevel]: user's current specific margin data will be returned if viplevel is omitted + :returns dict: a `borrow rate structure ` + """ + self.load_markets() + request: dict = {} + symbol = self.safe_string(params, 'symbol') + params = self.omit(params, 'symbol') + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + response = self.sapiGetMarginIsolatedMarginData(self.extend(request, params)) + # + # [ + # { + # "vipLevel": 0, + # "symbol": "BTCUSDT", + # "leverage": "10", + # "data": [ + # { + # "coin": "BTC", + # "dailyInterest": "0.00026125", + # "borrowLimit": "270" + # }, + # { + # "coin": "USDT", + # "dailyInterest": "0.000475", + # "borrowLimit": "2100000" + # } + # ] + # } + # ] + # + return self.parse_isolated_borrow_rates(response) + + def fetch_borrow_rate_history(self, code: str, since: Int = None, limit: Int = None, params={}): + """ + retrieves a history of a currencies borrow interest rate at specific time slots + + https://developers.binance.com/docs/margin_trading/borrow-and-repay/Query-Margin-Interest-Rate-History + + :param str code: unified currency code + :param int [since]: timestamp for the earliest borrow rate + :param int [limit]: the maximum number of `borrow rate structures ` to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of `borrow rate structures ` + """ + self.load_markets() + if limit is None: + limit = 93 + elif limit > 93: + # Binance API says the limit is 100, but "Illegal characters found in a parameter." is returned when limit is > 93 + raise BadRequest(self.id + ' fetchBorrowRateHistory() limit parameter cannot exceed 92') + currency = self.currency(code) + request: dict = { + 'asset': currency['id'], + 'limit': limit, + } + if since is not None: + request['startTime'] = since + endTime = self.sum(since, limit * 86400000) - 1 # required when startTime is further than 93 days in the past + now = self.milliseconds() + request['endTime'] = min(endTime, now) # cannot have an endTime later than current time + response = self.sapiGetMarginInterestRateHistory(self.extend(request, params)) + # + # [ + # { + # "asset": "USDT", + # "timestamp": 1638230400000, + # "dailyInterestRate": "0.0006", + # "vipLevel": 0 + # }, + # ] + # + return self.parse_borrow_rate_history(response, code, since, limit) + + def parse_borrow_rate(self, info, currency: Currency = None): + # + # { + # "asset": "USDT", + # "timestamp": 1638230400000, + # "dailyInterestRate": "0.0006", + # "vipLevel": 0 + # } + # + timestamp = self.safe_integer(info, 'timestamp') + currencyId = self.safe_string(info, 'asset') + return { + 'currency': self.safe_currency_code(currencyId, currency), + 'rate': self.safe_number(info, 'dailyInterestRate'), + 'period': 86400000, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'info': info, + } + + def parse_isolated_borrow_rate(self, info: dict, market: Market = None) -> IsolatedBorrowRate: + # + # { + # "vipLevel": 0, + # "symbol": "BTCUSDT", + # "leverage": "10", + # "data": [ + # { + # "coin": "BTC", + # "dailyInterest": "0.00026125", + # "borrowLimit": "270" + # }, + # { + # "coin": "USDT", + # "dailyInterest": "0.000475", + # "borrowLimit": "2100000" + # } + # ] + # } + # + marketId = self.safe_string(info, 'symbol') + market = self.safe_market(marketId, market, None, 'spot') + data = self.safe_list(info, 'data') + baseInfo = self.safe_dict(data, 0) + quoteInfo = self.safe_dict(data, 1) + return { + 'info': info, + 'symbol': self.safe_string(market, 'symbol'), + 'base': self.safe_string(baseInfo, 'coin'), + 'baseRate': self.safe_number(baseInfo, 'dailyInterest'), + 'quote': self.safe_string(quoteInfo, 'coin'), + 'quoteRate': self.safe_number(quoteInfo, 'dailyInterest'), + 'period': 86400000, + 'timestamp': None, + 'datetime': None, + } + + def create_gift_code(self, code: str, amount, params={}): + """ + create gift code + + https://developers.binance.com/docs/gift_card/market-data/Create-a-single-token-gift-card + + :param str code: gift code + :param float amount: amount of currency for the gift + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: The gift code id, code, currency and amount + """ + self.load_markets() + currency = self.currency(code) + # ensure you have enough token in your funding account before calling self code + request: dict = { + 'token': currency['id'], + 'amount': amount, + } + response = self.sapiPostGiftcardCreateCode(self.extend(request, params)) + # + # { + # "code": "000000", + # "message": "success", + # "data": {referenceNo: "0033002404219823", code: "AP6EXTLKNHM6CEX7"}, + # "success": True + # } + # + data = self.safe_dict(response, 'data') + giftcardCode = self.safe_string(data, 'code') + id = self.safe_string(data, 'referenceNo') + return { + 'info': response, + 'id': id, + 'code': giftcardCode, + 'currency': code, + 'amount': amount, + } + + def redeem_gift_code(self, giftcardCode, params={}): + """ + redeem gift code + + https://developers.binance.com/docs/gift_card/market-data/Redeem-a-Binance-Gift-Card + + :param str giftcardCode: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + request: dict = { + 'code': giftcardCode, + } + response = self.sapiPostGiftcardRedeemCode(self.extend(request, params)) + # + # { + # "code": "000000", + # "message": "success", + # "data": { + # "referenceNo": "0033002404219823", + # "identityNo": "10316431732801474560" + # }, + # "success": True + # } + # + return response + + def verify_gift_code(self, id: str, params={}): + """ + verify gift code + + https://developers.binance.com/docs/gift_card/market-data/Verify-Binance-Gift-Card-by-Gift-Card-Number + + :param str id: reference number id + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + request: dict = { + 'referenceNo': id, + } + response = self.sapiGetGiftcardVerify(self.extend(request, params)) + # + # { + # "code": "000000", + # "message": "success", + # "data": {valid: True}, + # "success": True + # } + # + return response + + def fetch_borrow_interest(self, code: Str = None, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[BorrowInterest]: + """ + fetch the interest owed by the user for borrowing currency for margin trading + + https://developers.binance.com/docs/margin_trading/borrow-and-repay/Get-Interest-History + https://developers.binance.com/docs/derivatives/portfolio-margin/account/Get-Margin-BorrowLoan-Interest-History + + :param str [code]: unified currency code + :param str [symbol]: unified market symbol when fetch interest in isolated markets + :param int [since]: the earliest time in ms to fetch borrrow interest for + :param int [limit]: the maximum number of structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.portfolioMargin]: set to True if you would like to fetch the borrow interest in a portfolio margin account + :returns dict[]: a list of `borrow interest structures ` + """ + self.load_markets() + isPortfolioMargin = None + isPortfolioMargin, params = self.handle_option_and_params_2(params, 'fetchBorrowInterest', 'papi', 'portfolioMargin', False) + request: dict = {} + market = None + if code is not None: + currency = self.currency(code) + request['asset'] = currency['id'] + if since is not None: + request['startTime'] = since + if limit is not None: + request['size'] = limit + request, params = self.handle_until_option('endTime', request, params) + response = None + if isPortfolioMargin: + response = self.papiGetMarginMarginInterestHistory(self.extend(request, params)) + else: + if symbol is not None: + market = self.market(symbol) + request['isolatedSymbol'] = market['id'] + response = self.sapiGetMarginInterestHistory(self.extend(request, params)) + # + # spot margin + # + # { + # "rows":[ + # { + # "isolatedSymbol": "BNBUSDT", # isolated symbol, will not be returned for crossed margin + # "asset": "BNB", + # "interest": "0.02414667", + # "interestAccuredTime": 1566813600000, + # "interestRate": "0.01600000", + # "principal": "36.22000000", + # "type": "ON_BORROW" + # } + # ], + # "total": 1 + # } + # + # spot margin portfolio margin + # + # { + # "total": 49, + # "rows": [ + # { + # "txId": 1656187724899910076, + # "interestAccuredTime": 1707541200000, + # "asset": "USDT", + # "rawAsset": "USDT", + # "principal": "0.00011146", + # "interest": "0.00000001", + # "interestRate": "0.00089489", + # "type": "PERIODIC" + # }, + # ] + # } + # + rows = self.safe_list(response, 'rows') + interest = self.parse_borrow_interests(rows, market) + return self.filter_by_currency_since_limit(interest, code, since, limit) + + def parse_borrow_interest(self, info: dict, market: Market = None) -> BorrowInterest: + symbol = self.safe_string(info, 'isolatedSymbol') + timestamp = self.safe_integer(info, 'interestAccuredTime') + marginMode = 'cross' if (symbol is None) else 'isolated' + return { + 'info': info, + 'symbol': symbol, + 'currency': self.safe_currency_code(self.safe_string(info, 'asset')), + 'interest': self.safe_number(info, 'interest'), + 'interestRate': self.safe_number(info, 'interestRate'), + 'amountBorrowed': self.safe_number(info, 'principal'), + 'marginMode': marginMode, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + + def repay_cross_margin(self, code: str, amount, params={}): + """ + repay borrowed margin and interest + + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Margin-Account-Repay + https://developers.binance.com/docs/margin_trading/borrow-and-repay/Margin-Account-Borrow-Repay + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Margin-Account-Repay-Debt + + :param str code: unified currency code of the currency to repay + :param float amount: the amount to repay + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.portfolioMargin]: set to True if you would like to repay margin in a portfolio margin account + :param str [params.repayCrossMarginMethod]: *portfolio margin only* 'papiPostRepayLoan'(default), 'papiPostMarginRepayDebt'(alternative) + :param str [params.specifyRepayAssets]: *portfolio margin papiPostMarginRepayDebt only* specific asset list to repay debt + :returns dict: a `margin loan structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'asset': currency['id'], + 'amount': self.currency_to_precision(code, amount), + } + response = None + isPortfolioMargin = None + isPortfolioMargin, params = self.handle_option_and_params_2(params, 'repayCrossMargin', 'papi', 'portfolioMargin', False) + if isPortfolioMargin: + method = None + method, params = self.handle_option_and_params_2(params, 'repayCrossMargin', 'repayCrossMarginMethod', 'method') + if method == 'papiPostMarginRepayDebt': + response = self.papiPostMarginRepayDebt(self.extend(request, params)) + # + # { + # "asset": "USDC", + # "amount": 10, + # "specifyRepayAssets": null, + # "updateTime": 1727170761267, + # "success": True + # } + # + else: + response = self.papiPostRepayLoan(self.extend(request, params)) + # + # { + # "tranId": 108988250265, + # "clientTag":"" + # } + # + else: + request['isIsolated'] = 'FALSE' + request['type'] = 'REPAY' + response = self.sapiPostMarginBorrowRepay(self.extend(request, params)) + # + # { + # "tranId": 108988250265, + # "clientTag":"" + # } + # + return self.parse_margin_loan(response, currency) + + def repay_isolated_margin(self, symbol: str, code: str, amount, params={}): + """ + repay borrowed margin and interest + + https://developers.binance.com/docs/margin_trading/borrow-and-repay/Margin-Account-Borrow-Repay + + :param str symbol: unified market symbol, required for isolated margin + :param str code: unified currency code of the currency to repay + :param float amount: the amount to repay + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin loan structure ` + """ + self.load_markets() + currency = self.currency(code) + market = self.market(symbol) + request: dict = { + 'asset': currency['id'], + 'amount': self.currency_to_precision(code, amount), + 'symbol': market['id'], + 'isIsolated': 'TRUE', + 'type': 'REPAY', + } + response = self.sapiPostMarginBorrowRepay(self.extend(request, params)) + # + # { + # "tranId": 108988250265, + # "clientTag":"" + # } + # + return self.parse_margin_loan(response, currency) + + def borrow_cross_margin(self, code: str, amount: float, params={}): + """ + create a loan to borrow margin + + https://developers.binance.com/docs/margin_trading/borrow-and-repay/Margin-Account-Borrow-Repay + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Margin-Account-Borrow + + :param str code: unified currency code of the currency to borrow + :param float amount: the amount to borrow + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.portfolioMargin]: set to True if you would like to borrow margin in a portfolio margin account + :returns dict: a `margin loan structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'asset': currency['id'], + 'amount': self.currency_to_precision(code, amount), + } + response = None + isPortfolioMargin = None + isPortfolioMargin, params = self.handle_option_and_params_2(params, 'borrowCrossMargin', 'papi', 'portfolioMargin', False) + if isPortfolioMargin: + response = self.papiPostMarginLoan(self.extend(request, params)) + else: + request['isIsolated'] = 'FALSE' + request['type'] = 'BORROW' + response = self.sapiPostMarginBorrowRepay(self.extend(request, params)) + # + # { + # "tranId": 108988250265, + # "clientTag":"" + # } + # + return self.parse_margin_loan(response, currency) + + def borrow_isolated_margin(self, symbol: str, code: str, amount: float, params={}): + """ + create a loan to borrow margin + + https://developers.binance.com/docs/margin_trading/borrow-and-repay/Margin-Account-Borrow-Repay + + :param str symbol: unified market symbol, required for isolated margin + :param str code: unified currency code of the currency to borrow + :param float amount: the amount to borrow + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin loan structure ` + """ + self.load_markets() + currency = self.currency(code) + market = self.market(symbol) + request: dict = { + 'asset': currency['id'], + 'amount': self.currency_to_precision(code, amount), + 'symbol': market['id'], + 'isIsolated': 'TRUE', + 'type': 'BORROW', + } + response = self.sapiPostMarginBorrowRepay(self.extend(request, params)) + # + # { + # "tranId": 108988250265, + # "clientTag":"" + # } + # + return self.parse_margin_loan(response, currency) + + def parse_margin_loan(self, info, currency: Currency = None): + # + # { + # "tranId": 108988250265, + # "clientTag":"" + # } + # + # repayCrossMargin alternative endpoint + # + # { + # "asset": "USDC", + # "amount": 10, + # "specifyRepayAssets": null, + # "updateTime": 1727170761267, + # "success": True + # } + # + currencyId = self.safe_string(info, 'asset') + timestamp = self.safe_integer(info, 'updateTime') + return { + 'id': self.safe_integer(info, 'tranId'), + 'currency': self.safe_currency_code(currencyId, currency), + 'amount': self.safe_number(info, 'amount'), + 'symbol': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'info': info, + } + + def fetch_open_interest_history(self, symbol: str, timeframe='5m', since: Int = None, limit: Int = None, params={}): + """ + Retrieves the open interest history of a currency + + https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/Open-Interest-Statistics + https://developers.binance.com/docs/derivatives/coin-margined-futures/market-data/rest-api/Open-Interest-Statistics + + :param str symbol: Unified CCXT market symbol + :param str timeframe: "5m","15m","30m","1h","2h","4h","6h","12h", or "1d" + :param int [since]: the time(ms) of the earliest record to retrieve unix timestamp + :param int [limit]: default 30, max 500 + :param dict [params]: exchange specific parameters + :param int [params.until]: the time(ms) of the latest record to retrieve unix timestamp + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict: an array of `open interest structure ` + """ + if timeframe == '1m': + raise BadRequest(self.id + ' fetchOpenInterestHistory cannot use the 1m timeframe') + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOpenInterestHistory', 'paginate', False) + if paginate: + return self.fetch_paginated_call_deterministic('fetchOpenInterestHistory', symbol, since, limit, timeframe, params, 500) + market = self.market(symbol) + request: dict = { + 'period': self.safe_string(self.timeframes, timeframe, timeframe), + } + if limit is not None: + request['limit'] = limit + symbolKey = 'symbol' if market['linear'] else 'pair' + request[symbolKey] = market['id'] + if market['inverse']: + request['contractType'] = self.safe_string(params, 'contractType', 'CURRENT_QUARTER') + if since is not None: + request['startTime'] = since + until = self.safe_integer(params, 'until') # unified in milliseconds + endTime = self.safe_integer(params, 'endTime', until) # exchange-specific in milliseconds + params = self.omit(params, ['endTime', 'until']) + if endTime: + request['endTime'] = endTime + elif since: + if limit is None: + limit = 30 # Exchange default + duration = self.parse_timeframe(timeframe) + request['endTime'] = self.sum(since, duration * limit * 1000) + response = None + if market['inverse']: + response = self.dapiDataGetOpenInterestHist(self.extend(request, params)) + else: + response = self.fapiDataGetOpenInterestHist(self.extend(request, params)) + # + # [ + # { + # "symbol":"BTCUSDT", + # "sumOpenInterest":"75375.61700000", + # "sumOpenInterestValue":"3248828883.71251440", + # "timestamp":1642179900000 + # }, + # ... + # ] + # + return self.parse_open_interests_history(response, market, since, limit) + + def fetch_open_interest(self, symbol: str, params={}): + """ + retrieves the open interest of a contract trading pair + + https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/Open-Interest + https://developers.binance.com/docs/derivatives/coin-margined-futures/market-data/rest-api/Open-Interest + https://developers.binance.com/docs/derivatives/option/market-data/Open-Interest + + :param str symbol: unified CCXT market symbol + :param dict [params]: exchange specific parameters + :returns dict} an open interest structure{@link https://docs.ccxt.com/#/?id=open-interest-structure: + """ + self.load_markets() + market = self.market(symbol) + request: dict = {} + if market['option']: + request['underlyingAsset'] = market['baseId'] + if market['expiry'] is None: + raise NotSupported(self.id + ' fetchOpenInterest does not support ' + symbol) + request['expiration'] = self.yymmdd(market['expiry']) + else: + request['symbol'] = market['id'] + response = None + if market['option']: + response = self.eapiPublicGetOpenInterest(self.extend(request, params)) + elif market['inverse']: + response = self.dapiPublicGetOpenInterest(self.extend(request, params)) + else: + response = self.fapiPublicGetOpenInterest(self.extend(request, params)) + # + # futures(fapi) + # + # { + # "symbol": "ETHUSDT_230331", + # "openInterest": "23581.677", + # "time": 1677356872265 + # } + # + # futures(dapi) + # + # { + # "symbol": "ETHUSD_PERP", + # "pair": "ETHUSD", + # "openInterest": "26542436", + # "contractType": "PERPETUAL", + # "time": 1677360272224 + # } + # + # options(eapi) + # + # [ + # { + # "symbol": "ETH-230225-1625-C", + # "sumOpenInterest": "460.50", + # "sumOpenInterestUsd": "734957.4358092150", + # "timestamp": "1677304860000" + # } + # ] + # + if market['option']: + symbol = market['symbol'] + result = self.parse_open_interests_history(response, market) + for i in range(0, len(result)): + item = result[i] + if item['symbol'] == symbol: + return item + else: + return self.parse_open_interest(response, market) + return None + + def parse_open_interest(self, interest, market: Market = None): + timestamp = self.safe_integer_2(interest, 'timestamp', 'time') + id = self.safe_string(interest, 'symbol') + amount = self.safe_number_2(interest, 'sumOpenInterest', 'openInterest') + value = self.safe_number_2(interest, 'sumOpenInterestValue', 'sumOpenInterestUsd') + # Inverse returns the number of contracts different from the base or quote hasattr(self, volume) case + # compared with https://www.binance.com/en/futures/funding-history/quarterly/4 + return self.safe_open_interest({ + 'symbol': self.safe_symbol(id, market, None, 'contract'), + 'baseVolume': None if market['inverse'] else amount, # deprecated + 'quoteVolume': value, # deprecated + 'openInterestAmount': amount, + 'openInterestValue': value, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'info': interest, + }, market) + + def fetch_my_liquidations(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + retrieves the users liquidated positions + + https://developers.binance.com/docs/margin_trading/trade/Get-Force-Liquidation-Record + https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Users-Force-Orders + https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/rest-api/Users-Force-Orders + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Query-Users-UM-Force-Orders + https://developers.binance.com/docs/derivatives/portfolio-margin/trade/Query-Users-CM-Force-Orders + + :param str [symbol]: unified CCXT market symbol + :param int [since]: the earliest time in ms to fetch liquidations for + :param int [limit]: the maximum number of liquidation structures to retrieve + :param dict [params]: exchange specific parameters for the binance api endpoint + :param int [params.until]: timestamp in ms of the latest liquidation + :param boolean [params.paginate]: *spot only* default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param boolean [params.portfolioMargin]: set to True if you would like to fetch liquidations in a portfolio margin account + :param str [params.type]: "spot" + :param str [params.subType]: "linear" or "inverse" + :returns dict: an array of `liquidation structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchMyLiquidations', 'paginate') + if paginate: + return self.fetch_paginated_call_incremental('fetchMyLiquidations', symbol, since, limit, params, 'current', 100) + market = None + if symbol is not None: + market = self.market(symbol) + type = None + type, params = self.handle_market_type_and_params('fetchMyLiquidations', market, params) + subType = None + subType, params = self.handle_sub_type_and_params('fetchMyLiquidations', market, params, 'linear') + isPortfolioMargin = None + isPortfolioMargin, params = self.handle_option_and_params_2(params, 'fetchMyLiquidations', 'papi', 'portfolioMargin', False) + request: dict = {} + if type != 'spot': + request['autoCloseType'] = 'LIQUIDATION' + if market is not None: + symbolKey = 'isolatedSymbol' if market['spot'] else 'symbol' + if not isPortfolioMargin: + request[symbolKey] = market['id'] + if since is not None: + request['startTime'] = since + if limit is not None: + if type == 'spot': + request['size'] = limit + else: + request['limit'] = limit + request, params = self.handle_until_option('endTime', request, params) + response = None + if type == 'spot': + if isPortfolioMargin: + response = self.papiGetMarginForceOrders(self.extend(request, params)) + else: + response = self.sapiGetMarginForceLiquidationRec(self.extend(request, params)) + elif subType == 'linear': + if isPortfolioMargin: + response = self.papiGetUmForceOrders(self.extend(request, params)) + else: + response = self.fapiPrivateGetForceOrders(self.extend(request, params)) + elif subType == 'inverse': + if isPortfolioMargin: + response = self.papiGetCmForceOrders(self.extend(request, params)) + else: + response = self.dapiPrivateGetForceOrders(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchMyLiquidations() does not support ' + market['type'] + ' markets') + # + # margin + # + # { + # "rows": [ + # { + # "avgPrice": "0.00388359", + # "executedQty": "31.39000000", + # "orderId": 180015097, + # "price": "0.00388110", + # "qty": "31.39000000", + # "side": "SELL", + # "symbol": "BNBBTC", + # "timeInForce": "GTC", + # "isIsolated": True, + # "updatedTime": 1558941374745 + # } + # ], + # "total": 1 + # } + # + # linear + # + # [ + # { + # "orderId": 6071832819, + # "symbol": "BTCUSDT", + # "status": "FILLED", + # "clientOrderId": "autoclose-1596107620040000020", + # "price": "10871.09", + # "avgPrice": "10913.21000", + # "origQty": "0.001", + # "executedQty": "0.001", + # "cumQuote": "10.91321", + # "timeInForce": "IOC", + # "type": "LIMIT", + # "reduceOnly": False, + # "closePosition": False, + # "side": "SELL", + # "positionSide": "BOTH", + # "stopPrice": "0", + # "workingType": "CONTRACT_PRICE", + # "origType": "LIMIT", + # "time": 1596107620044, + # "updateTime": 1596107620087 + # }, + # ] + # + # inverse + # + # [ + # { + # "orderId": 165123080, + # "symbol": "BTCUSD_200925", + # "pair": "BTCUSD", + # "status": "FILLED", + # "clientOrderId": "autoclose-1596542005017000006", + # "price": "11326.9", + # "avgPrice": "11326.9", + # "origQty": "1", + # "executedQty": "1", + # "cumBase": "0.00882854", + # "timeInForce": "IOC", + # "type": "LIMIT", + # "reduceOnly": False, + # "closePosition": False, + # "side": "SELL", + # "positionSide": "BOTH", + # "stopPrice": "0", + # "workingType": "CONTRACT_PRICE", + # "priceProtect": False, + # "origType": "LIMIT", + # "time": 1596542005019, + # "updateTime": 1596542005050 + # }, + # ] + # + liquidations = self.safe_list(response, 'rows', response) + return self.parse_liquidations(liquidations, market, since, limit) + + def parse_liquidation(self, liquidation, market: Market = None): + # + # margin + # + # { + # "avgPrice": "0.00388359", + # "executedQty": "31.39000000", + # "orderId": 180015097, + # "price": "0.00388110", + # "qty": "31.39000000", + # "side": "SELL", + # "symbol": "BNBBTC", + # "timeInForce": "GTC", + # "isIsolated": True, + # "updatedTime": 1558941374745 + # } + # + # linear + # + # { + # "orderId": 6071832819, + # "symbol": "BTCUSDT", + # "status": "FILLED", + # "clientOrderId": "autoclose-1596107620040000020", + # "price": "10871.09", + # "avgPrice": "10913.21000", + # "origQty": "0.001", + # "executedQty": "0.002", + # "cumQuote": "10.91321", + # "timeInForce": "IOC", + # "type": "LIMIT", + # "reduceOnly": False, + # "closePosition": False, + # "side": "SELL", + # "positionSide": "BOTH", + # "stopPrice": "0", + # "workingType": "CONTRACT_PRICE", + # "origType": "LIMIT", + # "time": 1596107620044, + # "updateTime": 1596107620087 + # } + # + # inverse + # + # { + # "orderId": 165123080, + # "symbol": "BTCUSD_200925", + # "pair": "BTCUSD", + # "status": "FILLED", + # "clientOrderId": "autoclose-1596542005017000006", + # "price": "11326.9", + # "avgPrice": "11326.9", + # "origQty": "1", + # "executedQty": "1", + # "cumBase": "0.00882854", + # "timeInForce": "IOC", + # "type": "LIMIT", + # "reduceOnly": False, + # "closePosition": False, + # "side": "SELL", + # "positionSide": "BOTH", + # "stopPrice": "0", + # "workingType": "CONTRACT_PRICE", + # "priceProtect": False, + # "origType": "LIMIT", + # "time": 1596542005019, + # "updateTime": 1596542005050 + # } + # + marketId = self.safe_string(liquidation, 'symbol') + timestamp = self.safe_integer_2(liquidation, 'updatedTime', 'updateTime') + return self.safe_liquidation({ + 'info': liquidation, + 'symbol': self.safe_symbol(marketId, market), + 'contracts': self.safe_number(liquidation, 'executedQty'), + 'contractSize': self.safe_number(market, 'contractSize'), + 'price': self.safe_number(liquidation, 'avgPrice'), + 'side': self.safe_string_lower(liquidation, 'side'), + 'baseValue': self.safe_number(liquidation, 'cumBase'), + 'quoteValue': self.safe_number(liquidation, 'cumQuote'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + + def fetch_greeks(self, symbol: str, params={}) -> Greeks: + """ + fetches an option contracts greeks, financial metrics used to measure the factors that affect the price of an options contract + + https://developers.binance.com/docs/derivatives/option/market-data/Option-Mark-Price + + :param str symbol: unified symbol of the market to fetch greeks for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `greeks structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = self.eapiPublicGetMark(self.extend(request, params)) + # + # [ + # { + # "symbol": "BTC-231229-40000-C", + # "markPrice": "2012", + # "bidIV": "0.60236275", + # "askIV": "0.62267244", + # "markIV": "0.6125176", + # "delta": "0.39111646", + # "theta": "-32.13948531", + # "gamma": "0.00004656", + # "vega": "51.70062218", + # "highPriceLimit": "6474", + # "lowPriceLimit": "5" + # } + # ] + # + return self.parse_greeks(response[0], market) + + def fetch_all_greeks(self, symbols: Strings = None, params={}) -> List[Greeks]: + """ + fetches all option contracts greeks, financial metrics used to measure the factors that affect the price of an options contract + + https://developers.binance.com/docs/derivatives/option/market-data/Option-Mark-Price + + :param str[] [symbols]: unified symbols of the markets to fetch greeks for, all markets are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `greeks structure ` + """ + self.load_markets() + symbols = self.market_symbols(symbols, None, True, True, True) + request: dict = {} + market = None + if symbols is not None: + symbolsLength = len(symbols) + if symbolsLength == 1: + market = self.market(symbols[0]) + request['symbol'] = market['id'] + response = self.eapiPublicGetMark(self.extend(request, params)) + # + # [ + # { + # "symbol": "BTC-231229-40000-C", + # "markPrice": "2012", + # "bidIV": "0.60236275", + # "askIV": "0.62267244", + # "markIV": "0.6125176", + # "delta": "0.39111646", + # "theta": "-32.13948531", + # "gamma": "0.00004656", + # "vega": "51.70062218", + # "highPriceLimit": "6474", + # "lowPriceLimit": "5" + # } + # ] + # + return self.parse_all_greeks(response, symbols) + + def parse_greeks(self, greeks: dict, market: Market = None) -> Greeks: + # + # { + # "symbol": "BTC-231229-40000-C", + # "markPrice": "2012", + # "bidIV": "0.60236275", + # "askIV": "0.62267244", + # "markIV": "0.6125176", + # "delta": "0.39111646", + # "theta": "-32.13948531", + # "gamma": "0.00004656", + # "vega": "51.70062218", + # "highPriceLimit": "6474", + # "lowPriceLimit": "5" + # } + # + marketId = self.safe_string(greeks, 'symbol') + symbol = self.safe_symbol(marketId, market) + return { + 'symbol': symbol, + 'timestamp': None, + 'datetime': None, + 'delta': self.safe_number(greeks, 'delta'), + 'gamma': self.safe_number(greeks, 'gamma'), + 'theta': self.safe_number(greeks, 'theta'), + 'vega': self.safe_number(greeks, 'vega'), + 'rho': None, + 'bidSize': None, + 'askSize': None, + 'bidImpliedVolatility': self.safe_number(greeks, 'bidIV'), + 'askImpliedVolatility': self.safe_number(greeks, 'askIV'), + 'markImpliedVolatility': self.safe_number(greeks, 'markIV'), + 'bidPrice': None, + 'askPrice': None, + 'markPrice': self.safe_number(greeks, 'markPrice'), + 'lastPrice': None, + 'underlyingPrice': None, + 'info': greeks, + } + + def fetch_trading_limits(self, symbols: Strings = None, params={}): + # self method should not be called directly, use loadTradingLimits() instead + markets = self.fetch_markets() + tradingLimits: dict = {} + for i in range(0, len(markets)): + market = markets[i] + symbol = market['symbol'] + if (symbols is None) or (self.in_array(symbol, symbols)): + tradingLimits[symbol] = market['limits']['amount'] + return tradingLimits + + def fetch_position_mode(self, symbol: Str = None, params={}): + """ + fetchs the position mode, hedged or one way, hedged for binance is set identically for all linear markets or all inverse markets + + https://developers.binance.com/docs/derivatives/usds-margined-futures/account/rest-api/Get-Current-Position-Mode + https://developers.binance.com/docs/derivatives/coin-margined-futures/account/rest-api/Get-Current-Position-Mode + + :param str symbol: unified symbol of the market to fetch the order book for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.subType]: "linear" or "inverse" + :returns dict: an object detailing whether the market is in hedged or one-way mode + """ + market = None + if symbol is not None: + market = self.market(symbol) + subType = None + subType, params = self.handle_sub_type_and_params('fetchPositionMode', market, params) + response = None + if subType == 'linear': + response = self.fapiPrivateGetPositionSideDual(params) + elif subType == 'inverse': + response = self.dapiPrivateGetPositionSideDual(params) + else: + raise BadRequest(self.id + ' fetchPositionMode requires either a symbol argument or params["subType"]') + # + # { + # dualSidePosition: False + # } + # + dualSidePosition = self.safe_bool(response, 'dualSidePosition') + return { + 'info': response, + 'hedged': dualSidePosition, + } + + def fetch_margin_modes(self, symbols: Strings = None, params={}) -> MarginModes: + """ + fetches margin modes("isolated" or "cross") that the market for the symbol in in, with symbol=None all markets for a subType(linear/inverse) are returned + + https://developers.binance.com/docs/derivatives/coin-margined-futures/account/rest-api/Account-Information + https://developers.binance.com/docs/derivatives/usds-margined-futures/account/rest-api/Account-Information-V2 + https://developers.binance.com/docs/derivatives/usds-margined-futures/account/rest-api/Symbol-Config + + :param str[] symbols: unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.subType]: "linear" or "inverse" + :returns dict: a list of `margin mode structures ` + """ + self.load_markets() + market = None + if symbols is not None: + symbols = self.market_symbols(symbols) + market = self.market(symbols[0]) + subType = None + subType, params = self.handle_sub_type_and_params('fetchMarginMode', market, params) + response = None + if subType == 'linear': + response = self.fapiPrivateGetSymbolConfig(params) + # + # [ + # { + # "symbol": "BTCUSDT", + # "marginType": "CROSSED", + # "isAutoAddMargin": "false", + # "leverage": 21, + # "maxNotionalValue": "1000000", + # } + # ] + # + elif subType == 'inverse': + response = self.dapiPrivateGetAccount(params) + # + # { + # feeTier: '0', + # canTrade: True, + # canDeposit: True, + # canWithdraw: True, + # updateTime: '0', + # assets: [ + # { + # asset: 'APT', + # walletBalance: '0.00000000', + # unrealizedProfit: '0.00000000', + # marginBalance: '0.00000000', + # maintMargin: '0.00000000', + # initialMargin: '0.00000000', + # positionInitialMargin: '0.00000000', + # openOrderInitialMargin: '0.00000000', + # maxWithdrawAmount: '0.00000000', + # crossWalletBalance: '0.00000000', + # crossUnPnl: '0.00000000', + # availableBalance: '0.00000000', + # updateTime: '0' + # }, + # ... + # ], + # positions: [ + # { + # symbol: 'BCHUSD_240329', + # initialMargin: '0', + # maintMargin: '0', + # unrealizedProfit: '0.00000000', + # positionInitialMargin: '0', + # openOrderInitialMargin: '0', + # leverage: '20', + # isolated: False, + # positionSide: 'BOTH', + # entryPrice: '0.00000000', + # maxQty: '1000', + # notionalValue: '0', + # isolatedWallet: '0', + # updateTime: '0', + # positionAmt: '0', + # breakEvenPrice: '0.00000000' + # }, + # ... + # ] + # } + # + else: + raise BadRequest(self.id + ' fetchMarginModes() supports linear and inverse subTypes only') + assets = self.safe_list(response, 'positions', []) + if isinstance(response, list): + assets = response + return self.parse_margin_modes(assets, symbols, 'symbol', 'swap') + + def fetch_margin_mode(self, symbol: str, params={}) -> MarginMode: + """ + fetches the margin mode of a specific symbol + + https://developers.binance.com/docs/derivatives/usds-margined-futures/account/rest-api/Symbol-Config + https://developers.binance.com/docs/derivatives/coin-margined-futures/account/rest-api/Account-Information + + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.subType]: "linear" or "inverse" + :returns dict: a `margin mode structure ` + """ + self.load_markets() + market = self.market(symbol) + subType = None + subType, params = self.handle_sub_type_and_params('fetchMarginMode', market, params) + response = None + if subType == 'linear': + request: dict = { + 'symbol': market['id'], + } + response = self.fapiPrivateGetSymbolConfig(self.extend(request, params)) + # + # [ + # { + # "symbol": "BTCUSDT", + # "marginType": "CROSSED", + # "isAutoAddMargin": "false", + # "leverage": 21, + # "maxNotionalValue": "1000000", + # } + # ] + # + elif subType == 'inverse': + fetchMarginModesResponse = self.fetch_margin_modes([symbol], params) + return fetchMarginModesResponse[symbol] + else: + raise BadRequest(self.id + ' fetchMarginMode() supports linear and inverse subTypes only') + return self.parse_margin_mode(response[0], market) + + def parse_margin_mode(self, marginMode: dict, market=None) -> MarginMode: + marketId = self.safe_string(marginMode, 'symbol') + market = self.safe_market(marketId, market) + marginModeRaw = self.safe_bool(marginMode, 'isolated') + reMarginMode = None + if marginModeRaw is not None: + reMarginMode = 'isolated' if marginModeRaw else 'cross' + marginTypeRaw = self.safe_string_lower(marginMode, 'marginType') + if marginTypeRaw is not None: + reMarginMode = 'cross' if (marginTypeRaw == 'crossed') else 'isolated' + return { + 'info': marginMode, + 'symbol': market['symbol'], + 'marginMode': reMarginMode, + } + + def fetch_option(self, symbol: str, params={}) -> Option: + """ + fetches option data that is commonly found in an option chain + + https://developers.binance.com/docs/derivatives/option/market-data/24hr-Ticker-Price-Change-Statistics + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `option chain structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = self.eapiPublicGetTicker(self.extend(request, params)) + # + # [ + # { + # "symbol": "BTC-241227-80000-C", + # "priceChange": "0", + # "priceChangePercent": "0", + # "lastPrice": "2750", + # "lastQty": "0", + # "open": "2750", + # "high": "2750", + # "low": "2750", + # "volume": "0", + # "amount": "0", + # "bidPrice": "4880", + # "askPrice": "0", + # "openTime": 0, + # "closeTime": 0, + # "firstTradeId": 0, + # "tradeCount": 0, + # "strikePrice": "80000", + # "exercisePrice": "63944.09893617" + # } + # ] + # + chain = self.safe_dict(response, 0, {}) + return self.parse_option(chain, None, market) + + def parse_option(self, chain: dict, currency: Currency = None, market: Market = None) -> Option: + # + # { + # "symbol": "BTC-241227-80000-C", + # "priceChange": "0", + # "priceChangePercent": "0", + # "lastPrice": "2750", + # "lastQty": "0", + # "open": "2750", + # "high": "2750", + # "low": "2750", + # "volume": "0", + # "amount": "0", + # "bidPrice": "4880", + # "askPrice": "0", + # "openTime": 0, + # "closeTime": 0, + # "firstTradeId": 0, + # "tradeCount": 0, + # "strikePrice": "80000", + # "exercisePrice": "63944.09893617" + # } + # + marketId = self.safe_string(chain, 'symbol') + market = self.safe_market(marketId, market) + return { + 'info': chain, + 'currency': None, + 'symbol': market['symbol'], + 'timestamp': None, + 'datetime': None, + 'impliedVolatility': None, + 'openInterest': None, + 'bidPrice': self.safe_number(chain, 'bidPrice'), + 'askPrice': self.safe_number(chain, 'askPrice'), + 'midPrice': None, + 'markPrice': None, + 'lastPrice': self.safe_number(chain, 'lastPrice'), + 'underlyingPrice': self.safe_number(chain, 'exercisePrice'), + 'change': self.safe_number(chain, 'priceChange'), + 'percentage': self.safe_number(chain, 'priceChangePercent'), + 'baseVolume': self.safe_number(chain, 'volume'), + 'quoteVolume': None, + } + + def fetch_margin_adjustment_history(self, symbol: Str = None, type: Str = None, since: Num = None, limit: Num = None, params={}) -> List[MarginModification]: + """ + fetches the history of margin added or reduced from contract isolated positions + + https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Get-Position-Margin-Change-History + https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/rest-api/Get-Position-Margin-Change-History + + :param str symbol: unified market symbol + :param str [type]: "add" or "reduce" + :param int [since]: timestamp in ms of the earliest change to fetch + :param int [limit]: the maximum amount of changes to fetch + :param dict params: extra parameters specific to the exchange api endpoint + :param int [params.until]: timestamp in ms of the latest change to fetch + :returns dict[]: a list of `margin structures ` + """ + self.load_markets() + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMarginAdjustmentHistory() requires a symbol argument') + market = self.market(symbol) + until = self.safe_integer(params, 'until') + params = self.omit(params, 'until') + request: dict = { + 'symbol': market['id'], + } + if type is not None: + request['type'] = 1 if (type == 'add') else 2 + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + if until is not None: + request['endTime'] = until + response = None + if market['linear']: + response = self.fapiPrivateGetPositionMarginHistory(self.extend(request, params)) + elif market['inverse']: + response = self.dapiPrivateGetPositionMarginHistory(self.extend(request, params)) + else: + raise BadRequest(self.id + ' fetchMarginAdjustmentHistory() is not supported for markets of type ' + market['type']) + # + # [ + # { + # symbol: "XRPUSDT", + # type: "1", + # deltaType: "TRADE", + # amount: "2.57148240", + # asset: "USDT", + # time: "1711046271555", + # positionSide: "BOTH", + # clientTranId: "" + # } + # ... + # ] + # + modifications = self.parse_margin_modifications(response) + return self.filter_by_symbol_since_limit(modifications, symbol, since, limit) + + def fetch_convert_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies that can be converted + + https://developers.binance.com/docs/convert/market-data/Query-order-quantity-precision-per-asset + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + self.load_markets() + response = self.sapiGetConvertAssetInfo(params) + # + # [ + # { + # "asset": "BTC", + # "fraction": 8 + # }, + # ] + # + result: dict = {} + for i in range(0, len(response)): + entry = response[i] + id = self.safe_string(entry, 'asset') + code = self.safe_currency_code(id) + result[code] = { + 'info': entry, + 'id': id, + 'code': code, + 'networks': None, + 'type': None, + 'name': None, + 'active': None, + 'deposit': None, + 'withdraw': None, + 'fee': None, + 'precision': self.parse_number(self.parse_precision(self.safe_string(entry, 'fraction'))), + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + 'deposit': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + } + return result + + def fetch_convert_quote(self, fromCode: str, toCode: str, amount: Num = None, params={}) -> Conversion: + """ + fetch a quote for converting from one currency to another + + https://developers.binance.com/docs/convert/trade/Send-quote-request + + :param str fromCode: the currency that you want to sell and convert from + :param str toCode: the currency that you want to buy and convert into + :param float amount: how much you want to trade in units of the from currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.walletType]: either 'SPOT' or 'FUNDING', the default is 'SPOT' + :returns dict: a `conversion structure ` + """ + if amount is None: + raise ArgumentsRequired(self.id + ' fetchConvertQuote() requires an amount argument') + self.load_markets() + request: dict = { + 'fromAsset': fromCode, + 'toAsset': toCode, + 'fromAmount': amount, + } + response = self.sapiPostConvertGetQuote(self.extend(request, params)) + # + # { + # "quoteId":"12415572564", + # "ratio":"38163.7", + # "inverseRatio":"0.0000262", + # "validTimestamp":1623319461670, + # "toAmount":"3816.37", + # "fromAmount":"0.1" + # } + # + fromCurrency = self.currency(fromCode) + toCurrency = self.currency(toCode) + return self.parse_conversion(response, fromCurrency, toCurrency) + + def create_convert_trade(self, id: str, fromCode: str, toCode: str, amount: Num = None, params={}) -> Conversion: + """ + convert from one currency to another + + https://developers.binance.com/docs/convert/trade/Accept-Quote + + :param str id: the id of the trade that you want to make + :param str fromCode: the currency that you want to sell and convert from + :param str toCode: the currency that you want to buy and convert into + :param float [amount]: how much you want to trade in units of the from currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `conversion structure ` + """ + self.load_markets() + request: dict = {} + response = None + if (fromCode == 'BUSD') or (toCode == 'BUSD'): + if amount is None: + raise ArgumentsRequired(self.id + ' createConvertTrade() requires an amount argument') + request['clientTranId'] = id + request['asset'] = fromCode + request['targetAsset'] = toCode + request['amount'] = amount + response = self.sapiPostAssetConvertTransfer(self.extend(request, params)) + # + # { + # "tranId": 118263407119, + # "status": "S" + # } + # + else: + request['quoteId'] = id + response = self.sapiPostConvertAcceptQuote(self.extend(request, params)) + # + # { + # "orderId":"933256278426274426", + # "createTime":1623381330472, + # "orderStatus":"PROCESS" + # } + # + fromCurrency = self.currency(fromCode) + toCurrency = self.currency(toCode) + return self.parse_conversion(response, fromCurrency, toCurrency) + + def fetch_convert_trade(self, id: str, code: Str = None, params={}) -> Conversion: + """ + fetch the data for a conversion trade + + https://developers.binance.com/docs/convert/trade/Order-Status + + :param str id: the id of the trade that you want to fetch + :param str [code]: the unified currency code of the conversion trade + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `conversion structure ` + """ + self.load_markets() + request: dict = {} + response = None + if code == 'BUSD': + msInDay = 86400000 + now = self.milliseconds() + if code is not None: + currency = self.currency(code) + request['asset'] = currency['id'] + request['tranId'] = id + request['startTime'] = now - msInDay + request['endTime'] = now + response = self.sapiGetAssetConvertTransferQueryByPage(self.extend(request, params)) + # + # { + # "total": 3, + # "rows": [ + # { + # "tranId": 118263615991, + # "type": 244, + # "time": 1664442078000, + # "deductedAsset": "BUSD", + # "deductedAmount": "1", + # "targetAsset": "USDC", + # "targetAmount": "1", + # "status": "S", + # "accountType": "MAIN" + # }, + # ] + # } + # + else: + request['orderId'] = id + response = self.sapiGetConvertOrderStatus(self.extend(request, params)) + # + # { + # "orderId":933256278426274426, + # "orderStatus":"SUCCESS", + # "fromAsset":"BTC", + # "fromAmount":"0.00054414", + # "toAsset":"USDT", + # "toAmount":"20", + # "ratio":"36755", + # "inverseRatio":"0.00002721", + # "createTime":1623381330472 + # } + # + data = response + if code == 'BUSD': + rows = self.safe_list(response, 'rows', []) + data = self.safe_dict(rows, 0, {}) + fromCurrencyId = self.safe_string_2(data, 'deductedAsset', 'fromAsset') + toCurrencyId = self.safe_string_2(data, 'targetAsset', 'toAsset') + fromCurrency = None + toCurrency = None + if fromCurrencyId is not None: + fromCurrency = self.currency(fromCurrencyId) + if toCurrencyId is not None: + toCurrency = self.currency(toCurrencyId) + return self.parse_conversion(data, fromCurrency, toCurrency) + + def fetch_convert_trade_history(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Conversion]: + """ + fetch the users history of conversion trades + + https://developers.binance.com/docs/convert/trade/Get-Convert-Trade-History + + :param str [code]: the unified currency code + :param int [since]: the earliest time in ms to fetch conversions for + :param int [limit]: the maximum number of conversion structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest conversion to fetch + :returns dict[]: a list of `conversion structures ` + """ + self.load_markets() + request: dict = {} + msInThirtyDays = 2592000000 + now = self.milliseconds() + if since is not None: + request['startTime'] = since + else: + request['startTime'] = now - msInThirtyDays + endTime = self.safe_integer_2(params, 'endTime', 'until') + if endTime is not None: + request['endTime'] = endTime + else: + request['endTime'] = now + params = self.omit(params, 'until') + response = None + responseQuery = None + fromCurrencyKey = None + toCurrencyKey = None + if code == 'BUSD': + currency = self.currency(code) + request['asset'] = currency['id'] + if limit is not None: + request['size'] = limit + fromCurrencyKey = 'deductedAsset' + toCurrencyKey = 'targetAsset' + responseQuery = 'rows' + response = self.sapiGetAssetConvertTransferQueryByPage(self.extend(request, params)) + # + # { + # "total": 3, + # "rows": [ + # { + # "tranId": 118263615991, + # "type": 244, + # "time": 1664442078000, + # "deductedAsset": "BUSD", + # "deductedAmount": "1", + # "targetAsset": "USDC", + # "targetAmount": "1", + # "status": "S", + # "accountType": "MAIN" + # }, + # ] + # } + # + else: + if (request['endTime'] - request['startTime']) > msInThirtyDays: + raise BadRequest(self.id + ' fetchConvertTradeHistory() the max interval between startTime and endTime is 30 days.') + if limit is not None: + request['limit'] = limit + fromCurrencyKey = 'fromAsset' + toCurrencyKey = 'toAsset' + responseQuery = 'list' + response = self.sapiGetConvertTradeFlow(self.extend(request, params)) + # + # { + # "list": [ + # { + # "quoteId": "f3b91c525b2644c7bc1e1cd31b6e1aa6", + # "orderId": 940708407462087195, + # "orderStatus": "SUCCESS", + # "fromAsset": "USDT", + # "fromAmount": "20", + # "toAsset": "BNB", + # "toAmount": "0.06154036", + # "ratio": "0.00307702", + # "inverseRatio": "324.99", + # "createTime": 1624248872184 + # } + # ], + # "startTime": 1623824139000, + # "endTime": 1626416139000, + # "limit": 100, + # "moreData": False + # } + # + rows = self.safe_list(response, responseQuery, []) + return self.parse_conversions(rows, code, fromCurrencyKey, toCurrencyKey, since, limit) + + def parse_conversion(self, conversion: dict, fromCurrency: Currency = None, toCurrency: Currency = None) -> Conversion: + # + # fetchConvertQuote + # + # { + # "quoteId":"12415572564", + # "ratio":"38163.7", + # "inverseRatio":"0.0000262", + # "validTimestamp":1623319461670, + # "toAmount":"3816.37", + # "fromAmount":"0.1" + # } + # + # createConvertTrade + # + # { + # "orderId":"933256278426274426", + # "createTime":1623381330472, + # "orderStatus":"PROCESS" + # } + # + # createConvertTrade BUSD + # + # { + # "tranId": 118263407119, + # "status": "S" + # } + # + # fetchConvertTrade, fetchConvertTradeHistory BUSD + # + # { + # "tranId": 118263615991, + # "type": 244, + # "time": 1664442078000, + # "deductedAsset": "BUSD", + # "deductedAmount": "1", + # "targetAsset": "USDC", + # "targetAmount": "1", + # "status": "S", + # "accountType": "MAIN" + # } + # + # fetchConvertTrade + # + # { + # "orderId":933256278426274426, + # "orderStatus":"SUCCESS", + # "fromAsset":"BTC", + # "fromAmount":"0.00054414", + # "toAsset":"USDT", + # "toAmount":"20", + # "ratio":"36755", + # "inverseRatio":"0.00002721", + # "createTime":1623381330472 + # } + # + # fetchConvertTradeHistory + # + # { + # "quoteId": "f3b91c525b2644c7bc1e1cd31b6e1aa6", + # "orderId": 940708407462087195, + # "orderStatus": "SUCCESS", + # "fromAsset": "USDT", + # "fromAmount": "20", + # "toAsset": "BNB", + # "toAmount": "0.06154036", + # "ratio": "0.00307702", + # "inverseRatio": "324.99", + # "createTime": 1624248872184 + # } + # + timestamp = self.safe_integer_n(conversion, ['time', 'validTimestamp', 'createTime']) + fromCur = self.safe_string_2(conversion, 'deductedAsset', 'fromAsset') + fromCode = self.safe_currency_code(fromCur, fromCurrency) + to = self.safe_string_2(conversion, 'targetAsset', 'toAsset') + toCode = self.safe_currency_code(to, toCurrency) + return { + 'info': conversion, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': self.safe_string_n(conversion, ['tranId', 'orderId', 'quoteId']), + 'fromCurrency': fromCode, + 'fromAmount': self.safe_number_2(conversion, 'deductedAmount', 'fromAmount'), + 'toCurrency': toCode, + 'toAmount': self.safe_number_2(conversion, 'targetAmount', 'toAmount'), + 'price': None, + 'fee': None, + } + + def fetch_funding_intervals(self, symbols: Strings = None, params={}) -> FundingRates: + """ + fetch the funding rate interval for multiple markets + + https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/Get-Funding-Rate-Info + https://developers.binance.com/docs/derivatives/coin-margined-futures/market-data/rest-api/Get-Funding-Info + + :param str[] [symbols]: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.subType]: "linear" or "inverse" + :returns dict[]: a list of `funding rate structures ` + """ + self.load_markets() + market = None + if symbols is not None: + symbols = self.market_symbols(symbols) + market = self.market(symbols[0]) + type = 'swap' + subType = None + subType, params = self.handle_sub_type_and_params('fetchFundingIntervals', market, params, 'linear') + response = None + if self.is_linear(type, subType): + response = self.fapiPublicGetFundingInfo(params) + elif self.is_inverse(type, subType): + response = self.dapiPublicGetFundingInfo(params) + else: + raise NotSupported(self.id + ' fetchFundingIntervals() supports linear and inverse swap contracts only') + # + # [ + # { + # "symbol": "BLZUSDT", + # "adjustedFundingRateCap": "0.03000000", + # "adjustedFundingRateFloor": "-0.03000000", + # "fundingIntervalHours": 4, + # "disclaimer": False + # }, + # ] + # + return self.parse_funding_rates(response, symbols) + + def fetch_long_short_ratio_history(self, symbol: Str = None, timeframe: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LongShortRatio]: + """ + fetches the long short ratio history for a unified market symbol + + https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/Long-Short-Ratio + https://developers.binance.com/docs/derivatives/coin-margined-futures/market-data/rest-api/Long-Short-Ratio + + :param str symbol: unified symbol of the market to fetch the long short ratio for + :param str [timeframe]: the period for the ratio, default is 24 hours + :param int [since]: the earliest time in ms to fetch ratios for + :param int [limit]: the maximum number of long short ratio structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest ratio to fetch + :returns dict[]: an array of `long short ratio structures ` + """ + self.load_markets() + market = self.market(symbol) + if timeframe is None: + timeframe = '1d' + request: dict = { + 'period': timeframe, + } + request, params = self.handle_until_option('endTime', request, params) + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + subType = None + subType, params = self.handle_sub_type_and_params('fetchLongShortRatioHistory', market, params) + response = None + if subType == 'linear': + request['symbol'] = market['id'] + response = self.fapiDataGetGlobalLongShortAccountRatio(self.extend(request, params)) + # + # [ + # { + # "symbol": "BTCUSDT", + # "longAccount": "0.4558", + # "longShortRatio": "0.8376", + # "shortAccount": "0.5442", + # "timestamp": 1726790400000 + # }, + # ] + # + elif subType == 'inverse': + request['pair'] = market['info']['pair'] + response = self.dapiDataGetGlobalLongShortAccountRatio(self.extend(request, params)) + # + # [ + # { + # "longAccount": "0.7262", + # "longShortRatio": "2.6523", + # "shortAccount": "0.2738", + # "pair": "BTCUSD", + # "timestamp": 1726790400000 + # }, + # ] + # + else: + raise BadRequest(self.id + ' fetchLongShortRatioHistory() supports linear and inverse subTypes only') + return self.parse_long_short_ratio_history(response, market) + + def parse_long_short_ratio(self, info: dict, market: Market = None) -> LongShortRatio: + # + # linear + # + # { + # "symbol": "BTCUSDT", + # "longAccount": "0.4558", + # "longShortRatio": "0.8376", + # "shortAccount": "0.5442", + # "timestamp": 1726790400000 + # } + # + # inverse + # + # { + # "longAccount": "0.7262", + # "longShortRatio": "2.6523", + # "shortAccount": "0.2738", + # "pair": "BTCUSD", + # "timestamp": 1726790400000 + # } + # + marketId = self.safe_string(info, 'symbol') + timestamp = self.safe_integer_omit_zero(info, 'timestamp') + return { + 'info': info, + 'symbol': self.safe_symbol(marketId, market, None, 'contract'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'timeframe': None, + 'longShortRatio': self.safe_number(info, 'longShortRatio'), + } diff --git a/ccxt/binancecoinm.py b/ccxt/binancecoinm.py new file mode 100644 index 0000000..c7a76e6 --- /dev/null +++ b/ccxt/binancecoinm.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.binance import binance +from ccxt.abstract.binancecoinm import ImplicitAPI +from ccxt.base.types import Any + + +class binancecoinm(binance, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(binancecoinm, self).describe(), { + 'id': 'binancecoinm', + 'name': 'Binance COIN-M', + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/387cfc4e-5f33-48cd-8f5c-cd4854dabf0c', + 'doc': [ + 'https://binance-docs.github.io/apidocs/delivery/en/', + 'https://binance-docs.github.io/apidocs/spot/en', + 'https://developers.binance.com/en', + ], + }, + 'has': { + 'CORS': None, + 'spot': False, + 'margin': False, + 'swap': True, + 'future': True, + 'option': None, + 'createStopMarketOrder': True, + }, + 'options': { + 'fetchMarkets': { + 'types': [ + 'inverse', + ], + }, + 'defaultSubType': 'inverse', + 'leverageBrackets': None, + }, + }) + + def transfer_in(self, code: str, amount, params={}): + # transfer from spot wallet to coinm futures wallet + return self.futuresTransfer(code, amount, 3, params) + + def transfer_out(self, code: str, amount, params={}): + # transfer from coinm futures wallet to spot wallet + return self.futuresTransfer(code, amount, 4, params) diff --git a/ccxt/binanceus.py b/ccxt/binanceus.py new file mode 100644 index 0000000..a22adc5 --- /dev/null +++ b/ccxt/binanceus.py @@ -0,0 +1,255 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.binance import binance +from ccxt.abstract.binanceus import ImplicitAPI +from ccxt.base.types import Any + + +class binanceus(binance, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(binanceus, self).describe(), { + 'id': 'binanceus', + 'name': 'Binance US', + 'countries': ['US'], # US + 'hostname': 'binance.us', + 'rateLimit': 50, # 1200 req per min + 'certified': False, + 'pro': True, + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/a9667919-b632-4d52-a832-df89f8a35e8c', + 'api': { + 'web': 'https://www.binance.us', + 'public': 'https://api.binance.us/api/v3', + 'private': 'https://api.binance.us/api/v3', + 'sapi': 'https://api.binance.us/sapi/v1', + 'sapiV2': 'https://api.binance.us/sapi/v2', + 'sapiV3': 'https://api.binance.us/sapi/v3', + }, + 'www': 'https://www.binance.us', + 'referral': 'https://www.binance.us/?ref=35005074', + 'doc': 'https://github.com/binance-us/binance-official-api-docs', + 'fees': 'https://www.binance.us/en/fee/schedule', + }, + 'fees': { + 'trading': { + 'tierBased': True, + 'percentage': True, + 'taker': self.parse_number('0.001'), # 0.1% trading fee, zero fees for all trading pairs before November 1. + 'maker': self.parse_number('0.001'), # 0.1% trading fee, zero fees for all trading pairs before November 1. + }, + }, + 'options': { + 'fetchMarkets': { + 'types': ['spot'], + }, + 'defaultType': 'spot', + 'fetchMargins': False, + 'quoteOrderQty': False, + }, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'closeAllPositions': False, + 'closePosition': False, + 'createReduceOnlyOrder': False, + 'createStopLossOrder': False, + 'createTakeProfitOrder': False, + 'fetchAllGreeks': False, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLeverage': False, + 'fetchLeverageTiers': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkOHLCV': False, + 'fetchMarkPrice': False, + 'fetchMarkPrices': False, + 'fetchMySettlementHistory': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchUnderlyingAssets': False, + 'fetchVolatilityHistory': False, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + }, + 'api': { + 'public': { + 'get': { + 'ping': 1, + 'time': 1, + 'exchangeInfo': 10, + 'trades': 1, + 'historicalTrades': 5, + 'aggTrades': 1, + 'depth': {'cost': 1, 'byLimit': [[100, 1], [500, 5], [1000, 10], [5000, 50]]}, + 'klines': 1, + 'ticker/price': {'cost': 1, 'noSymbol': 2}, + 'avgPrice': 1, + 'ticker/bookTicker': {'cost': 1, 'noSymbol': 2}, + 'ticker/24hr': {'cost': 1, 'noSymbol': 40}, + 'ticker': {'cost': 2, 'noSymbol': 100}, + }, + }, + 'private': { + 'get': { + 'account': 10, + 'rateLimit/order': 20, + 'order': 2, + 'openOrders': {'cost': 3, 'noSymbol': 40}, + 'myTrades': 10, + 'myPreventedMatches': 10, # with ID it has weight 1, but we don't have that complex handling yet + 'allOrders': 10, + 'orderList': 2, + 'allOrderList': 10, + 'openOrderList': 3, + }, + 'post': { + 'order': 1, + 'order/test': 1, + 'order/cancelReplace': 1, + 'order/oco': 1, + }, + 'delete': { + 'order': 1, + 'openOrders': 1, + 'orderList': 1, + }, + }, + 'sapi': { + 'get': { + 'system/status': 1, + 'asset/assetDistributionHistory': 1, + 'asset/query/trading-fee': 1, + 'asset/query/trading-volume': 1, + 'sub-account/spotSummary': 1, + 'sub-account/status': 1, + 'otc/coinPairs': 1, + 'otc/orders/{orderId}': 1, + 'otc/orders': 1, + 'ocbs/orders': 1, + 'capital/config/getall': 1, + 'capital/withdraw/history': 1, + 'fiatpayment/query/withdraw/history': 1, + 'capital/deposit/address': 1, + 'capital/deposit/hisrec': 1, + 'fiatpayment/query/deposit/history': 1, + 'capital/sub-account/deposit/address': 1, + 'capital/sub-account/deposit/history': 1, + 'asset/query/dust-logs': 1, + 'asset/query/dust-assets': 1, + 'marketing/referral/reward/history': 1, + 'staking/asset': 1, + 'staking/stakingBalance': 1, + 'staking/history': 1, + 'staking/stakingRewardsHistory': 1, + 'custodian/balance': 1, + 'custodian/supportedAssetList': 1, + 'custodian/walletTransferHistory': 1, + 'custodian/custodianTransferHistory': 1, + 'custodian/openOrders': 1, + 'custodian/order': 1, + 'custodian/orderHistory': 1, + 'custodian/tradeHistory': 1, + 'custodian/settlementSetting': 1, + 'custodian/settlementHistory': 1, + 'cl/transferHistory': 1, + 'apipartner/checkEligibility': 1, + 'apipartner/rebateHistory': 1, + }, + 'post': { + 'otc/quotes': 1, + 'otc/orders': 1, + 'fiatpayment/withdraw/apply': 1, + 'capital/withdraw/apply': 1, + 'asset/dust': 10, + 'staking/stake': 1, + 'staking/unstake': 1, + 'custodian/walletTransfer': 1, + 'custodian/custodianTransfer': 1, + 'custodian/undoTransfer': 1, + 'custodian/order': 1, + 'custodian/ocoOrder': 1, + 'cl/transfer': 1, + }, + 'delete': { + 'custodian/cancelOrder': 1, + 'custodian/cancelOrdersBySymbol': 1, + 'custodian/cancelOcoOrder': 1, + }, + }, + 'sapiV2': { + 'get': { + 'cl/account': 10, + 'cl/alertHistory': 1, + }, + }, + 'sapiV3': { + 'get': { + 'accountStatus': 1, + 'apiTradingStatus': 1, + 'sub-account/list': 1, + 'sub-account/transfer/history': 1, + 'sub-account/assets': 1, + }, + 'post': { + 'sub-account/transfer': 1, + }, + }, + }, + 'features': { + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + }) diff --git a/ccxt/binanceusdm.py b/ccxt/binanceusdm.py new file mode 100644 index 0000000..bc465ed --- /dev/null +++ b/ccxt/binanceusdm.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.binance import binance +from ccxt.abstract.binanceusdm import ImplicitAPI +from ccxt.base.types import Any +from ccxt.base.errors import InvalidOrder + + +class binanceusdm(binance, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(binanceusdm, self).describe(), { + 'id': 'binanceusdm', + 'name': 'Binance USDⓈ-M', + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/871cbea7-eebb-4b28-b260-c1c91df0487a', + 'doc': [ + 'https://binance-docs.github.io/apidocs/futures/en/', + 'https://binance-docs.github.io/apidocs/spot/en', + 'https://developers.binance.com/en', + ], + }, + 'has': { + 'CORS': None, + 'spot': False, + 'margin': False, + 'swap': True, + 'future': True, + 'option': None, + 'createStopMarketOrder': True, + }, + 'options': { + 'fetchMarkets': { + 'types': ['linear'], + }, + 'defaultSubType': 'linear', + # https://www.binance.com/en/support/faq/360033162192 + # tier amount, maintenance margin, initial margin, + 'leverageBrackets': None, + 'marginTypes': {}, + 'marginModes': {}, + }, + # https://binance-docs.github.io/apidocs/futures/en/#error-codes + # https://developers.binance.com/docs/derivatives/usds-margined-futures/error-code + 'exceptions': { + 'exact': { + '-5021': InvalidOrder, # {"code":-5021,"msg":"Due to the order could not be filled immediately, the FOK order has been rejected."} + '-5022': InvalidOrder, # {"code":-5022,"msg":"Due to the order could not be executed, the Post Only order will be rejected."} + '-5028': InvalidOrder, # {"code":-5028,"msg":"Timestamp for self request is outside of the ME recvWindow."} + }, + }, + }) + + def transfer_in(self, code: str, amount, params={}): + # transfer from spot wallet to usdm futures wallet + return self.futuresTransfer(code, amount, 1, params) + + def transfer_out(self, code: str, amount, params={}): + # transfer from usdm futures wallet to spot wallet + return self.futuresTransfer(code, amount, 2, params) diff --git a/ccxt/bingx.py b/ccxt/bingx.py new file mode 100644 index 0000000..fd27e33 --- /dev/null +++ b/ccxt/bingx.py @@ -0,0 +1,6417 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.bingx import ImplicitAPI +import hashlib +import numbers +from ccxt.base.types import Any, Balances, Currencies, Currency, DepositAddress, Int, Leverage, MarginMode, MarginModification, Market, Num, Order, OrderBook, OrderRequest, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, FundingRates, Trade, TradingFeeInterface, Transaction, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import AccountSuspended +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import NotSupported +from ccxt.base.errors import OperationFailed +from ccxt.base.errors import DDoSProtection +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class bingx(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(bingx, self).describe(), { + 'id': 'bingx', + 'name': 'BingX', + 'countries': ['US'], # North America, Canada, the EU, Hong Kong and Taiwan + 'rateLimit': 100, + 'version': 'v1', + 'certified': True, + 'pro': True, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': True, + 'future': False, + 'option': False, + 'addMargin': True, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelAllOrders': True, + 'cancelAllOrdersAfter': True, + 'cancelOrder': True, + 'cancelOrders': True, + 'closeAllPositions': True, + 'closePosition': True, + 'createMarketBuyOrderWithCost': True, + 'createMarketOrderWithCost': True, + 'createMarketSellOrderWithCost': True, + 'createOrder': True, + 'createOrders': True, + 'createOrderWithTakeProfitAndStopLoss': True, + 'createReduceOnlyOrder': True, + 'createStopLossOrder': True, + 'createStopOrder': True, + 'createTakeProfitOrder': True, + 'createTrailingAmountOrder': True, + 'createTrailingPercentOrder': True, + 'createTriggerOrder': True, + 'editOrder': True, + 'fetchAllGreeks': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchCanceledOrders': True, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': True, + 'fetchDeposits': True, + 'fetchDepositWithdrawFee': 'emulated', + 'fetchDepositWithdrawFees': True, + 'fetchFundingRate': True, + 'fetchFundingRateHistory': True, + 'fetchFundingRates': True, + 'fetchGreeks': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchLeverage': True, + 'fetchLiquidations': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': True, + 'fetchMarkets': True, + 'fetchMarkOHLCV': True, + 'fetchMarkPrice': True, + 'fetchMarkPrices': True, + 'fetchMyLiquidations': True, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': True, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchPosition': True, + 'fetchPositionHistory': False, + 'fetchPositionMode': True, + 'fetchPositions': True, + 'fetchPositionsHistory': True, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': True, + 'fetchTransfers': True, + 'fetchVolatilityHistory': False, + 'fetchWithdrawals': True, + 'reduceMargin': True, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'sandbox': True, + 'setLeverage': True, + 'setMargin': True, + 'setMarginMode': True, + 'setPositionMode': True, + 'transfer': True, + }, + 'hostname': 'bingx.com', + 'urls': { + 'logo': 'https://github-production-user-asset-6210df.s3.amazonaws.com/1294454/253675376-6983b72e-4999-4549-b177-33b374c195e3.jpg', + 'api': { + 'fund': 'https://open-api.{hostname}/openApi', + 'spot': 'https://open-api.{hostname}/openApi', + 'swap': 'https://open-api.{hostname}/openApi', + 'contract': 'https://open-api.{hostname}/openApi', + 'wallets': 'https://open-api.{hostname}/openApi', + 'user': 'https://open-api.{hostname}/openApi', + 'subAccount': 'https://open-api.{hostname}/openApi', + 'account': 'https://open-api.{hostname}/openApi', + 'copyTrading': 'https://open-api.{hostname}/openApi', + 'cswap': 'https://open-api.{hostname}/openApi', + 'api': 'https://open-api.{hostname}/openApi', + }, + 'test': { + 'swap': 'https://open-api-vst.{hostname}/openApi', # only swap is really "test" but since the API keys are the same, we want to keep all the functionalities when the user enables the sandboxmode + }, + 'www': 'https://bingx.com/', + 'doc': 'https://bingx-api.github.io/docs/', + 'referral': 'https://bingx.com/invite/OHETOM', + }, + 'fees': { + 'tierBased': True, + 'spot': { + 'feeSide': 'get', + 'maker': self.parse_number('0.001'), + 'taker': self.parse_number('0.001'), + }, + 'swap': { + 'feeSide': 'quote', + 'maker': self.parse_number('0.0002'), + 'taker': self.parse_number('0.0005'), + }, + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + }, + 'api': { + 'fund': { + 'v1': { + 'private': { + 'get': { + 'account/balance': 1, + }, + }, + }, + }, + 'spot': { + 'v1': { + 'public': { + 'get': { + 'server/time': 1, + 'common/symbols': 1, + 'market/trades': 1, + 'market/depth': 1, + 'market/kline': 1, + 'ticker/24hr': 1, + 'ticker/price': 1, + 'ticker/bookTicker': 1, + }, + }, + 'private': { + 'get': { + 'trade/query': 1, + 'trade/openOrders': 1, + 'trade/historyOrders': 1, + 'trade/myTrades': 2, + 'user/commissionRate': 5, + 'account/balance': 2, + 'oco/orderList': 5, + 'oco/openOrderList': 5, + 'oco/historyOrderList': 5, + }, + 'post': { + 'trade/order': 2, + 'trade/cancel': 2, + 'trade/batchOrders': 5, + 'trade/order/cancelReplace': 5, + 'trade/cancelOrders': 5, + 'trade/cancelOpenOrders': 5, + 'trade/cancelAllAfter': 5, + 'oco/order': 5, + 'oco/cancel': 5, + }, + }, + }, + 'v2': { + 'public': { + 'get': { + 'market/depth': 1, + 'market/kline': 1, + }, + }, + }, + 'v3': { + 'private': { + 'get': { + 'get/asset/transfer': 1, + 'asset/transfer': 1, + 'capital/deposit/hisrec': 1, + 'capital/withdraw/history': 1, + }, + 'post': { + 'post/asset/transfer': 5, + }, + }, + }, + }, + 'swap': { + 'v1': { + 'public': { + 'get': { + 'ticker/price': 1, + 'market/historicalTrades': 1, + 'market/markPriceKlines': 1, + 'trade/multiAssetsRules': 1, + 'tradingRules': 1, + }, + }, + 'private': { + 'get': { + 'positionSide/dual': 5, + 'trade/batchCancelReplace': 5, + 'trade/fullOrder': 2, + 'maintMarginRatio': 2, + 'trade/positionHistory': 2, + 'positionMargin/history': 2, + 'twap/openOrders': 5, + 'twap/historyOrders': 5, + 'twap/orderDetail': 5, + 'trade/assetMode': 5, + 'user/marginAssets': 5, + }, + 'post': { + 'trade/cancelReplace': 2, + 'positionSide/dual': 5, + 'trade/batchCancelReplace': 5, + 'trade/closePosition': 2, + 'trade/getVst': 5, + 'twap/order': 5, + 'twap/cancelOrder': 5, + 'trade/assetMode': 5, + 'trade/reverse': 5, + 'trade/autoAddMargin': 5, + }, + }, + }, + 'v2': { + 'public': { + 'get': { + 'server/time': 1, + 'quote/contracts': 1, + 'quote/price': 1, + 'quote/depth': 1, + 'quote/trades': 1, + 'quote/premiumIndex': 1, + 'quote/fundingRate': 1, + 'quote/klines': 1, + 'quote/openInterest': 1, + 'quote/ticker': 1, + 'quote/bookTicker': 1, + }, + }, + 'private': { + 'get': { + 'user/balance': 2, + 'user/positions': 2, + 'user/income': 2, + 'trade/openOrders': 2, + 'trade/openOrder': 2, + 'trade/order': 2, + 'trade/marginType': 5, + 'trade/leverage': 2, + 'trade/forceOrders': 1, + 'trade/allOrders': 2, + 'trade/allFillOrders': 2, + 'trade/fillHistory': 2, + 'user/income/export': 2, + 'user/commissionRate': 2, + 'quote/bookTicker': 1, + }, + 'post': { + 'trade/order': 2, + 'trade/batchOrders': 2, + 'trade/closeAllPositions': 2, + 'trade/cancelAllAfter': 5, + 'trade/marginType': 5, + 'trade/leverage': 5, + 'trade/positionMargin': 5, + 'trade/order/test': 2, + }, + 'delete': { + 'trade/order': 2, + 'trade/batchOrders': 2, + 'trade/allOpenOrders': 2, + }, + }, + }, + 'v3': { + 'public': { + 'get': { + 'quote/klines': 1, + }, + }, + 'private': { + 'get': { + 'user/balance': 2, + }, + }, + }, + }, + 'cswap': { + 'v1': { + 'public': { + 'get': { + 'market/contracts': 1, + 'market/premiumIndex': 1, + 'market/openInterest': 1, + 'market/klines': 1, + 'market/depth': 1, + 'market/ticker': 1, + }, + }, + 'private': { + 'get': { + 'trade/leverage': 2, + 'trade/forceOrders': 2, + 'trade/allFillOrders': 2, + 'trade/openOrders': 2, + 'trade/orderDetail': 2, + 'trade/orderHistory': 2, + 'trade/marginType': 2, + 'user/commissionRate': 2, + 'user/positions': 2, + 'user/balance': 2, + }, + 'post': { + 'trade/order': 2, + 'trade/leverage': 2, + 'trade/allOpenOrders': 2, + 'trade/closeAllPositions': 2, + 'trade/marginType': 2, + 'trade/positionMargin': 2, + }, + 'delete': { + 'trade/allOpenOrders': 2, # post method in doc + 'trade/cancelOrder': 2, + }, + }, + }, + }, + 'contract': { + 'v1': { + 'private': { + 'get': { + 'allPosition': 2, + 'allOrders': 2, + 'balance': 2, + }, + }, + }, + }, + 'wallets': { + 'v1': { + 'private': { + 'get': { + 'capital/config/getall': 5, + 'capital/deposit/address': 5, + 'capital/innerTransfer/records': 1, + 'capital/subAccount/deposit/address': 5, + 'capital/deposit/subHisrec': 2, + 'capital/subAccount/innerTransfer/records': 1, + 'capital/deposit/riskRecords': 5, + }, + 'post': { + 'capital/withdraw/apply': 5, + 'capital/innerTransfer/apply': 5, + 'capital/subAccountInnerTransfer/apply': 2, + 'capital/deposit/createSubAddress': 2, + }, + }, + }, + }, + 'subAccount': { + 'v1': { + 'private': { + 'get': { + 'list': 10, + 'assets': 2, + 'allAccountBalance': 2, + }, + 'post': { + 'create': 10, + 'apiKey/create': 2, + 'apiKey/edit': 2, + 'apiKey/del': 2, + 'updateStatus': 10, + }, + }, + }, + }, + 'account': { + 'v1': { + 'private': { + 'get': { + 'uid': 1, + 'apiKey/query': 2, + 'account/apiPermissions': 5, + 'allAccountBalance': 2, + }, + 'post': { + 'innerTransfer/authorizeSubAccount': 1, + }, + }, + }, + 'transfer': { + 'v1': { + 'private': { + 'get': { + 'subAccount/asset/transferHistory': 1, + }, + 'post': { + 'subAccount/transferAsset/supportCoins': 1, + 'subAccount/transferAsset': 1, + }, + }, + }, + }, + }, + 'user': { + 'auth': { + 'private': { + 'post': { + 'userDataStream': 2, + }, + 'put': { + 'userDataStream': 2, + }, + 'delete': { + 'userDataStream': 2, + }, + }, + }, + }, + 'copyTrading': { + 'v1': { + 'private': { + 'get': { + 'swap/trace/currentTrack': 2, + }, + 'post': { + 'swap/trace/closeTrackOrder': 2, + 'swap/trace/setTPSL': 2, + 'spot/trader/sellOrder': 10, + }, + }, + }, + }, + 'api': { + 'v3': { + 'private': { + 'get': { + 'asset/transfer': 1, + 'asset/transferRecord': 5, + 'capital/deposit/hisrec': 1, + 'capital/withdraw/history': 1, + }, + 'post': { + 'post/asset/transfer': 1, + }, + }, + }, + 'asset': { + 'v1': { + 'private': { + 'post': { + 'transfer': 5, + }, + }, + 'public': { + 'get': { + 'transfer/supportCoins': 5, + }, + }, + }, + }, + }, + 'agent': { + 'v1': { + 'private': { + 'get': { + 'account/inviteAccountList': 5, + 'reward/commissionDataList': 5, + 'account/inviteRelationCheck': 5, + 'asset/depositDetailList': 5, + 'reward/third/commissionDataList': 5, + 'asset/partnerData': 5, + 'commissionDataList/referralCode': 5, + 'account/superiorCheck': 5, + }, + }, + }, + }, + }, + 'timeframes': { + '1m': '1m', + '3m': '3m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1h', + '2h': '2h', + '4h': '4h', + '6h': '6h', + '12h': '12h', + '1d': '1d', + '3d': '3d', + '1w': '1w', + '1M': '1M', + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'exact': { + '400': BadRequest, + '401': AuthenticationError, + '403': PermissionDenied, + '404': BadRequest, + '429': DDoSProtection, + '418': PermissionDenied, + '500': ExchangeError, + '504': ExchangeError, + '100001': AuthenticationError, + '100412': AuthenticationError, + '100202': InsufficientFunds, + '100204': BadRequest, + '100400': BadRequest, + '100410': OperationFailed, # {"code":100410,"msg":"The current system is busy, please try again later"} + '100421': BadSymbol, # {"code":100421,"msg":"This pair is currently restricted from API trading","debugMsg":""} + '100440': ExchangeError, + '100500': OperationFailed, # {"code":100500,"msg":"The current system is busy, please try again later","debugMsg":""} + '100503': ExchangeError, + '80001': BadRequest, + '80012': InsufficientFunds, # {"code":80012,"msg":"{\"Code\":101253,\"Msg\":\"margin is not enough\"}} + '80014': BadRequest, + '80016': OrderNotFound, + '80017': OrderNotFound, + '100414': AccountSuspended, # {"code":100414,"msg":"Code: 100414, Msg: risk control check fail,code(1)","debugMsg":""} + '100419': PermissionDenied, # {"code":100419,"msg":"IP does not match IP whitelist","success":false,"timestamp":1705274099347} + '100437': BadRequest, # {"code":100437,"msg":"The withdrawal amount is lower than the minimum limit, please re-enter.","timestamp":1689258588845} + '101204': InsufficientFunds, # {"code":101204,"msg":"","data":{}} + '110425': InvalidOrder, # {"code":110425,"msg":"Please ensure that the minimum nominal value of the order placed must be greater than 2u","data":{}} + 'Insufficient assets': InsufficientFunds, # {"transferErrorMsg":"Insufficient assets"} + 'illegal transferType': BadRequest, # {"transferErrorMsg":"illegal transferType"} + }, + 'broad': {}, + }, + 'commonCurrencies': { + 'SNOW': 'Snowman', # Snowman vs SnowSwap conflict + 'OMNI': 'OmniCat', + 'NAP': '$NAP', # NAP on SOL = SNAP + 'TRUMP': 'TRUMPMAGA', + 'TRUMPSOL': 'TRUMP', + }, + 'options': { + 'defaultType': 'spot', + 'accountsByType': { + 'funding': 'fund', + 'spot': 'spot', + 'future': 'stdFutures', + 'swap': 'USDTMPerp', + 'linear': 'USDTMPerp', + 'inverse': 'coinMPerp', + }, + 'accountsById': { + 'fund': 'funding', + 'spot': 'spot', + 'stdFutures': 'future', + 'USDTMPerp': 'linear', + 'coinMPerp': 'inverse', + }, + 'recvWindow': 5 * 1000, # 5 sec + 'broker': 'CCXT', + 'defaultNetworks': { + 'ETH': 'ETH', + 'USDT': 'ERC20', + 'USDC': 'ERC20', + 'BTC': 'BTC', + 'LTC': 'LTC', + }, + 'networks': { + 'ARBITRUM': 'ARB', + 'MATIC': 'POLYGON', + 'ZKSYNC': 'ZKSYNCERA', + 'AVAXC': 'AVAX-C', + 'HBAR': 'HEDERA', + }, + }, + 'features': { + 'defaultForLinear': { + 'sandbox': True, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerPriceType': { + 'last': True, + 'mark': True, + 'index': True, + }, + 'triggerDirection': False, + 'stopLossPrice': True, + 'takeProfitPrice': True, + 'attachedStopLossTakeProfit': { + 'triggerPriceType': { + 'last': True, + 'mark': True, + 'index': True, + }, + 'price': True, + }, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': True, + 'trailing': True, + 'leverage': False, + 'marketBuyRequiresPrice': False, + 'marketBuyByCost': True, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': { + 'max': 5, + }, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 512, # 512 days for 'allFillOrders', 1000 days for 'fillOrders' + 'daysBack': 30, # 30 for 'allFillOrders', 7 for 'fillHistory' + 'untilDays': 30, # 30 for 'allFillOrders', 7 for 'fillHistory' + 'symbolRequired': True, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 20000, # since epoch + 'untilDays': 7, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': None, + 'daysBackCanceled': None, + 'untilDays': 7, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOHLCV': { + 'limit': 1440, + }, + }, + 'defaultForInverse': { + 'extends': 'defaultForLinear', + 'fetchMyTrades': { + 'limit': 1000, + 'daysBack': None, + 'untilDays': None, + }, + 'fetchOrders': None, + }, + # + 'spot': { + 'extends': 'defaultForLinear', + 'fetchCurrencies': { + 'private': True, + }, + 'createOrder': { + 'triggerPriceType': None, + 'attachedStopLossTakeProfit': None, + 'trailing': False, + }, + 'fetchMyTrades': { + 'limit': 1000, + 'daysBack': 1, + 'untilDays': 1, + }, + 'fetchOrders': None, + 'fetchClosedOrders': { + 'limit': 100, + 'untilDays': None, + }, + }, + 'swap': { + 'linear': { + 'extends': 'defaultForLinear', + }, + 'inverse': { + 'extends': 'defaultForInverse', + }, + }, + 'defaultForFuture': { + 'extends': 'defaultForLinear', + 'fetchOrders': None, + }, + 'future': { + 'linear': { + 'extends': 'defaultForFuture', + }, + 'inverse': { + 'extends': 'defaultForFuture', + }, + }, + }, + }) + + def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the bingx server + + https://bingx-api.github.io/docs/#/swapV2/base-info.html#Get%20Server%20Time + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the bingx server + """ + response = self.swapV2PublicGetServerTime(params) + # + # { + # "code": 0, + # "msg": "", + # "data": { + # "serverTime": 1675319535362 + # } + # } + # + data = self.safe_dict(response, 'data') + return self.safe_integer(data, 'serverTime') + + def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://bingx-api.github.io/docs/#/common/account-api.html#All%20Coins + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + if not self.check_required_credentials(False): + return {} + isSandbox = self.safe_bool(self.options, 'sandboxMode', False) + if isSandbox: + return {} + response = self.walletsV1PrivateGetCapitalConfigGetall(params) + # + # { + # "code": 0, + # "timestamp": 1702623271476, + # "data": [ + # { + # "coin": "BTC", + # "name": "BTC", + # "networkList": [ + # { + # "name": "BTC", + # "network": "BTC", + # "isDefault": True, + # "minConfirm": 2, + # "withdrawEnable": True, + # "depositEnable": True, + # "withdrawFee": "0.0006", + # "withdrawMax": "1.17522", + # "withdrawMin": "0.0005", + # "depositMin": "0.0002" + # }, + # { + # "name": "BTC", + # "network": "BEP20", + # "isDefault": False, + # "minConfirm": 15, + # "withdrawEnable": True, + # "depositEnable": True, + # "withdrawFee": "0.0000066", + # "withdrawMax": "1.17522", + # "withdrawMin": "0.0000066", + # "depositMin": "0.0002" + # } + # ] + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + result: dict = {} + for i in range(0, len(data)): + entry = data[i] + currencyId = self.safe_string(entry, 'coin') + code = self.safe_currency_code(currencyId) + name = self.safe_string(entry, 'name') + networkList = self.safe_list(entry, 'networkList') + networks: dict = {} + for j in range(0, len(networkList)): + rawNetwork = networkList[j] + network = self.safe_string(rawNetwork, 'network') + networkCode = self.network_id_to_code(network) + limits: dict = { + 'withdraw': { + 'min': self.safe_number(rawNetwork, 'withdrawMin'), + 'max': self.safe_number(rawNetwork, 'withdrawMax'), + }, + 'deposit': { + 'min': self.safe_number(rawNetwork, 'depositMin'), + 'max': None, + }, + } + precision = self.parse_number(self.parse_precision(self.safe_string(rawNetwork, 'withdrawPrecision'))) + networks[networkCode] = { + 'info': rawNetwork, + 'id': network, + 'network': networkCode, + 'fee': self.safe_number(rawNetwork, 'withdrawFee'), + 'active': None, + 'deposit': self.safe_bool(rawNetwork, 'depositEnable'), + 'withdraw': self.safe_bool(rawNetwork, 'withdrawEnable'), + 'precision': precision, + 'limits': limits, + } + if not (code in result): # the exchange could return the same currency with different networks + result[code] = { + 'info': entry, + 'code': code, + 'id': currencyId, + 'precision': None, + 'name': name, + 'active': None, + 'deposit': None, + 'withdraw': None, + 'networks': networks, + 'fee': None, + 'limits': None, + 'type': 'crypto', # only cryptos now + } + else: + existing = result[code] + existingNetworks = self.safe_dict(existing, 'networks', {}) + newNetworkCodes = list(networks.keys()) + for j in range(0, len(newNetworkCodes)): + newNetworkCode = newNetworkCodes[j] + if not (newNetworkCode in existingNetworks): + existingNetworks[newNetworkCode] = networks[newNetworkCode] + result[code]['networks'] = existingNetworks + codes = list(result.keys()) + for i in range(0, len(codes)): + code = codes[i] + currency = result[code] + result[code] = self.safe_currency_structure(currency) + return result + + def fetch_spot_markets(self, params) -> List[Market]: + response = self.spotV1PublicGetCommonSymbols(params) + # + # { + # "code": 0, + # "msg": "", + # "debugMsg": "", + # "data": { + # "symbols": [ + # { + # "symbol": "GEAR-USDT", + # "minQty": 735, # deprecated + # "maxQty": 2941177, # deprecated. + # "minNotional": 5, + # "maxNotional": 20000, + # "status": 1, + # "tickSize": 0.000001, + # "stepSize": 1, + # "apiStateSell": True, + # "apiStateBuy": True, + # "timeOnline": 0, + # "offTime": 0, + # "maintainTime": 0 + # }, + # ... + # ] + # } + # } + # + data = self.safe_dict(response, 'data') + markets = self.safe_list(data, 'symbols', []) + return self.parse_markets(markets) + + def fetch_swap_markets(self, params): + response = self.swapV2PublicGetQuoteContracts(params) + # + # { + # "code": 0, + # "msg": "", + # "data": [ + # { + # "contractId": "100", + # "symbol": "BTC-USDT", + # "size": "0.0001", + # "quantityPrecision": "4", + # "pricePrecision": "1", + # "feeRate": "0.0005", + # "makerFeeRate": "0.0002", + # "takerFeeRate": "0.0005", + # "tradeMinLimit": "0", + # "tradeMinQuantity": "0.0001", + # "tradeMinUSDT": "2", + # "maxLongLeverage": "125", + # "maxShortLeverage": "125", + # "currency": "USDT", + # "asset": "BTC", + # "status": "1", + # "apiStateOpen": "true", + # "apiStateClose": "true", + # "ensureTrigger": True, + # "triggerFeeRate": "0.00020000" + # }, + # ... + # ] + # } + # + markets = self.safe_list(response, 'data', []) + return self.parse_markets(markets) + + def fetch_inverse_swap_markets(self, params): + response = self.cswapV1PublicGetMarketContracts(params) + # + # { + # "code": 0, + # "msg": "", + # "timestamp": 1720074487610, + # "data": [ + # { + # "symbol": "BNB-USD", + # "pricePrecision": 2, + # "minTickSize": "10", + # "minTradeValue": "10", + # "minQty": "1.00000000", + # "status": 1, + # "timeOnline": 1713175200000 + # }, + # ] + # } + # + markets = self.safe_list(response, 'data', []) + return self.parse_markets(markets) + + def parse_market(self, market: dict) -> Market: + id = self.safe_string(market, 'symbol') + symbolParts = id.split('-') + baseId = symbolParts[0] + quoteId = symbolParts[1] + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + currency = self.safe_string(market, 'currency') + checkIsInverse = False + checkIsLinear = True + minTickSize = self.safe_number(market, 'minTickSize') + if minTickSize is not None: + # inverse swap market + currency = baseId + checkIsInverse = True + checkIsLinear = False + settle = self.safe_currency_code(currency) + pricePrecision = self.safe_number(market, 'tickSize') + if pricePrecision is None: + pricePrecision = self.parse_number(self.parse_precision(self.safe_string(market, 'pricePrecision'))) + quantityPrecision = self.safe_number(market, 'stepSize') + if quantityPrecision is None: + quantityPrecision = self.parse_number(self.parse_precision(self.safe_string(market, 'quantityPrecision'))) + type = 'swap' if (settle is not None) else 'spot' + spot = type == 'spot' + swap = type == 'swap' + symbol = base + '/' + quote + if settle is not None: + symbol += ':' + settle + fees = self.safe_dict(self.fees, type, {}) + contractSize = self.parse_number('1') if (swap) else None + isActive = False + if (self.safe_string(market, 'apiStateOpen') == 'true') and (self.safe_string(market, 'apiStateClose') == 'true'): + isActive = True # swap active + elif self.safe_bool(market, 'apiStateSell') and self.safe_bool(market, 'apiStateBuy') and (self.safe_string(market, 'status') == '1'): + isActive = True # spot active + isInverse = None if (spot) else checkIsInverse + isLinear = None if (spot) else checkIsLinear + minAmount = None + if not spot: + minAmount = self.safe_number_2(market, 'minQty', 'tradeMinQuantity') + timeOnline = self.safe_integer(market, 'timeOnline') + if timeOnline == 0: + timeOnline = None + return self.safe_market_structure({ + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': currency, + 'type': type, + 'spot': spot, + 'margin': False, + 'swap': swap, + 'future': False, + 'option': False, + 'active': isActive, + 'contract': swap, + 'linear': isLinear, + 'inverse': isInverse, + 'taker': self.safe_number(fees, 'taker'), + 'maker': self.safe_number(fees, 'maker'), + 'feeSide': self.safe_string(fees, 'feeSide'), + 'contractSize': contractSize, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': quantityPrecision, + 'price': pricePrecision, + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': minAmount, + 'max': None, + }, + 'price': { + 'min': minTickSize, + 'max': None, + }, + 'cost': { + 'min': self.safe_number_n(market, ['minNotional', 'tradeMinUSDT', 'minTradeValue']), + 'max': self.safe_number(market, 'maxNotional'), + }, + }, + 'created': timeOnline, + 'info': market, + }) + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for bingx + + https://bingx-api.github.io/docs/#/spot/market-api.html#Query%20Symbols + https://bingx-api.github.io/docs/#/swapV2/market-api.html#Contract%20Information + https://bingx-api.github.io/docs/#/en-us/cswap/market-api.html#Contract%20Information + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + requests = [self.fetch_swap_markets(params)] + isSandbox = self.safe_bool(self.options, 'sandboxMode', False) + if not isSandbox: + requests.append(self.fetch_inverse_swap_markets(params)) + requests.append(self.fetch_spot_markets(params)) # sandbox is swap only + promises = requests + linearSwapMarkets = self.safe_list(promises, 0, []) + inverseSwapMarkets = self.safe_list(promises, 1, []) + spotMarkets = self.safe_list(promises, 2, []) + swapMarkets = self.array_concat(linearSwapMarkets, inverseSwapMarkets) + return self.array_concat(spotMarkets, swapMarkets) + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://bingx-api.github.io/docs/#/swapV2/market-api.html#K-Line%20Data + https://bingx-api.github.io/docs/#/spot/market-api.html#Candlestick%20chart%20data + https://bingx-api.github.io/docs/#/swapV2/market-api.html#%20K-Line%20Data + https://bingx-api.github.io/docs/#/en-us/swapV2/market-api.html#Mark%20Price%20Kline/Candlestick%20Data + https://bingx-api.github.io/docs/#/en-us/cswap/market-api.html#Get%20K-line%20Data + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest candle to fetch + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate', False) + if paginate: + return self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params, 1440) + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + request['interval'] = self.safe_string(self.timeframes, timeframe, timeframe) + if since is not None: + request['startTime'] = max(since - 1, 0) + if limit is not None: + request['limit'] = limit + until = self.safe_integer_2(params, 'until', 'endTime') + if until is not None: + params = self.omit(params, ['until']) + request['endTime'] = until + response = None + if market['spot']: + response = self.spotV1PublicGetMarketKline(self.extend(request, params)) + else: + if market['inverse']: + response = self.cswapV1PublicGetMarketKlines(self.extend(request, params)) + else: + price = self.safe_string(params, 'price') + params = self.omit(params, 'price') + if price == 'mark': + response = self.swapV1PublicGetMarketMarkPriceKlines(self.extend(request, params)) + else: + response = self.swapV3PublicGetQuoteKlines(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "data": [ + # { + # "open": "19396.8", + # "close": "19394.4", + # "high": "19397.5", + # "low": "19385.7", + # "volume": "110.05", + # "time": 1666583700000 + # }, + # ... + # ] + # } + # + # fetchMarkOHLCV + # + # { + # "code": 0, + # "msg": "", + # "data": [ + # { + # "open": "42191.7", + # "close": "42189.5", + # "high": "42196.5", + # "low": "42189.5", + # "volume": "0.00", + # "openTime": 1706508840000, + # "closeTime": 1706508840000 + # } + # ] + # } + # + ohlcvs = self.safe_value(response, 'data', []) + if not isinstance(ohlcvs, list): + ohlcvs = [ohlcvs] + return self.parse_ohlcvs(ohlcvs, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # { + # "open": "19394.4", + # "close": "19379.0", + # "high": "19394.4", + # "low": "19368.3", + # "volume": "167.44", + # "time": 1666584000000 + # } + # + # fetchMarkOHLCV + # + # { + # "open": "42191.7", + # "close": "42189.5", + # "high": "42196.5", + # "low": "42189.5", + # "volume": "0.00", + # "openTime": 1706508840000, + # "closeTime": 1706508840000 + # } + # spot + # [ + # 1691402580000, + # 29093.61, + # 29093.93, + # 29087.73, + # 29093.24, + # 0.59, + # 1691402639999, + # 17221.07 + # ] + # + if isinstance(ohlcv, list): + return [ + self.safe_integer(ohlcv, 0), + self.safe_number(ohlcv, 1), + self.safe_number(ohlcv, 2), + self.safe_number(ohlcv, 3), + self.safe_number(ohlcv, 4), + self.safe_number(ohlcv, 5), + ] + return [ + self.safe_integer_2(ohlcv, 'time', 'closeTime'), + self.safe_number(ohlcv, 'open'), + self.safe_number(ohlcv, 'high'), + self.safe_number(ohlcv, 'low'), + self.safe_number(ohlcv, 'close'), + self.safe_number(ohlcv, 'volume'), + ] + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://bingx-api.github.io/docs/#/spot/market-api.html#Query%20transaction%20records + https://bingx-api.github.io/docs/#/swapV2/market-api.html#The%20latest%20Trade%20of%20a%20Trading%20Pair + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['limit'] = min(limit, 100) # avoid API exception "limit should less than 100" + response = None + marketType = None + marketType, params = self.handle_market_type_and_params('fetchTrades', market, params) + if marketType == 'spot': + response = self.spotV1PublicGetMarketTrades(self.extend(request, params)) + else: + response = self.swapV2PublicGetQuoteTrades(self.extend(request, params)) + # + # spot + # + # { + # "code": 0, + # "data": [ + # { + # "id": 43148253, + # "price": 25714.71, + # "qty": 1.674571, + # "time": 1655085975589, + # "buyerMaker": False + # } + # ] + # } + # + # swap + # + # { + # "code":0, + # "msg":"", + # "data":[ + # { + # "time": 1672025549368, + # "isBuyerMaker": True, + # "price": "16885.0", + # "qty": "3.3002", + # "quoteQty": "55723.87" + # }, + # ... + # ] + # } + # + trades = self.safe_list(response, 'data', []) + return self.parse_trades(trades, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # spot fetchTrades + # + # { + # "id": 43148253, + # "price": 25714.71, + # "qty": 1.674571, + # "time": 1655085975589, + # "buyerMaker": False + # } + # + # spot fetchMyTrades + # + # { + # "symbol": "LTC-USDT", + # "id": 36237072, + # "orderId": 1674069326895775744, + # "price": "85.891", + # "qty": "0.0582", + # "quoteQty": "4.9988562000000005", + # "commission": -0.00005820000000000001, + # "commissionAsset": "LTC", + # "time": 1687964205000, + # "isBuyer": True, + # "isMaker": False + # } + # + # swap fetchTrades + # + # { + # "time": 1672025549368, + # "isBuyerMaker": True, + # "price": "16885.0", + # "qty": "3.3002", + # "quoteQty": "55723.87" + # } + # + # swap fetchMyTrades + # + # { + # "volume": "0.1", + # "price": "106.75", + # "amount": "10.6750", + # "commission": "-0.0053", + # "currency": "USDT", + # "orderId": "1676213270274379776", + # "liquidatedPrice": "0.00", + # "liquidatedMarginRatio": "0.00", + # "filledTime": "2023-07-04T20:56:01.000+0800" + # } + # + # ws spot + # + # { + # "E": 1690214529432, + # "T": 1690214529386, + # "e": "trade", + # "m": True, + # "p": "29110.19", + # "q": "0.1868", + # "s": "BTC-USDT", + # "t": "57903921" + # } + # + # ws linear swap + # + # { + # "q": "0.0421", + # "p": "29023.5", + # "T": 1690221401344, + # "m": False, + # "s": "BTC-USDT" + # } + # + # ws inverse swap + # + # { + # "e": "trade", + # "E": 1722920589665, + # "s": "BTC-USD", + # "t": "39125001", + # "p": "55360.0", + # "q": "1", + # "T": 1722920589582, + # "m": False + # } + # + # inverse swap fetchMyTrades + # + # { + # "orderId": "1817441228670648320", + # "symbol": "SOL-USD", + # "type": "MARKET", + # "side": "BUY", + # "positionSide": "LONG", + # "tradeId": "97244554", + # "volume": "2", + # "tradePrice": "182.652", + # "amount": "20.00000000", + # "realizedPnl": "0.00000000", + # "commission": "-0.00005475", + # "currency": "SOL", + # "buyer": True, + # "maker": False, + # "tradeTime": 1722146730000 + # } + # + time = self.safe_integer_n(trade, ['time', 'filledTm', 'T', 'tradeTime']) + datetimeId = self.safe_string(trade, 'filledTm') + if datetimeId is not None: + time = self.parse8601(datetimeId) + if time == 0: + time = None + cost = self.safe_string(trade, 'quoteQty') + # type = 'spot' if (cost is None) else 'swap'; self is not reliable + currencyId = self.safe_string_n(trade, ['currency', 'N', 'commissionAsset']) + currencyCode = self.safe_currency_code(currencyId) + m = self.safe_bool(trade, 'm') + marketId = self.safe_string_2(trade, 's', 'symbol') + isBuyerMaker = self.safe_bool_n(trade, ['buyerMaker', 'isBuyerMaker', 'maker']) + takeOrMaker = None + if (isBuyerMaker is not None) or (m is not None): + takeOrMaker = 'maker' if (isBuyerMaker or m) else 'taker' + side = self.safe_string_lower_2(trade, 'side', 'S') + if side is None: + if (isBuyerMaker is not None) or (m is not None): + side = 'sell' if (isBuyerMaker or m) else 'buy' + takeOrMaker = 'taker' + isBuyer = self.safe_bool(trade, 'isBuyer') + if isBuyer is not None: + side = 'buy' if isBuyer else 'sell' + isMaker = self.safe_bool(trade, 'isMaker') + if isMaker is not None: + takeOrMaker = 'maker' if isMaker else 'taker' + amount = self.safe_string_n(trade, ['qty', 'amount', 'q']) + if (market is not None) and market['swap'] and ('volume' in trade): + # private trade returns num of contracts instead of base currency(as the order-related methods do) + contractSize = self.safe_string(market['info'], 'tradeMinQuantity') + volume = self.safe_string(trade, 'volume') + amount = Precise.string_mul(volume, contractSize) + return self.safe_trade({ + 'id': self.safe_string_n(trade, ['id', 't']), + 'info': trade, + 'timestamp': time, + 'datetime': self.iso8601(time), + 'symbol': self.safe_symbol(marketId, market, '-'), + 'order': self.safe_string_2(trade, 'orderId', 'i'), + 'type': self.safe_string_lower(trade, 'o'), + 'side': self.parse_order_side(side), + 'takerOrMaker': takeOrMaker, + 'price': self.safe_string_n(trade, ['price', 'p', 'tradePrice']), + 'amount': amount, + 'cost': cost, + 'fee': { + 'cost': self.parse_number(Precise.string_abs(self.safe_string_2(trade, 'commission', 'n'))), + 'currency': currencyCode, + }, + }, market) + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://bingx-api.github.io/docs/#/spot/market-api.html#Query%20depth%20information + https://bingx-api.github.io/docs/#/swapV2/market-api.html#Get%20Market%20Depth + https://bingx-api.github.io/docs/#/en-us/cswap/market-api.html#Query%20Depth%20Data + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['limit'] = limit + response = None + marketType = None + marketType, params = self.handle_market_type_and_params('fetchOrderBook', market, params) + if marketType == 'spot': + response = self.spotV1PublicGetMarketDepth(self.extend(request, params)) + else: + if market['inverse']: + response = self.cswapV1PublicGetMarketDepth(self.extend(request, params)) + else: + response = self.swapV2PublicGetQuoteDepth(self.extend(request, params)) + # + # spot + # + # { + # "code":0, + # "timestamp":1743240504535, + # "data":{ + # "bids":[ + # ["83775.39","1.981875"], + # ["83775.38","0.001076"], + # ["83775.34","0.254716"], + # ], + # "asks":[ + # ["83985.40","0.000013"], + # ["83980.00","0.000011"], + # ["83975.70","0.000061000000000000005"], + # ], + # "ts":1743240504535, + # "lastUpdateId":13565639906 + # } + # } + # + # + # linear swap + # + # { + # "code":0, + # "msg":"", + # "data":{ + # "T":1743240836255, + # "bids":[ + # ["83760.7","7.0861"], + # ["83760.6","0.0044"], + # ["83757.7","1.9526"], + # ], + # "asks":[ + # ["83784.3","8.3531"], + # ["83782.8","23.7289"], + # ["83780.1","18.0617"], + # ], + # "bidsCoin":[ + # ["83760.7","0.0007"], + # ["83760.6","0.0000"], + # ["83757.7","0.0002"], + # ], + # "asksCoin":[ + # ["83784.3","0.0008"], + # ["83782.8","0.0024"], + # ["83780.1","0.0018"], + # ] + # } + # } + # + # inverse swap + # + # { + # "code":0, + # "msg":"", + # "timestamp":1743240979146, + # "data":{ + # "T":1743240978691, + # "bids":[ + # ["83611.4","241.0"], + # ["83611.3","1.0"], + # ["83602.9","666.0"], + # ], + # "asks":[ + # ["83645.0","4253.0"], + # ["83640.5","3188.0"], + # ["83636.0","5540.0"], + # ] + # } + # } + # + orderbook = self.safe_dict(response, 'data', {}) + nonce = self.safe_integer(orderbook, 'lastUpdateId') + timestamp = self.safe_integer_2(orderbook, 'T', 'ts') + result = self.parse_order_book(orderbook, market['symbol'], timestamp, 'bids', 'asks', 0, 1) + result['nonce'] = nonce + return result + + def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate + + https://bingx-api.github.io/docs/#/swapV2/market-api.html#Current%20Funding%20Rate + https://bingx-api.github.io/docs/#/en-us/cswap/market-api.html#Price%20&%20Current%20Funding%20Rate + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = None + if market['inverse']: + response = self.cswapV1PublicGetMarketPremiumIndex(self.extend(request, params)) + else: + response = self.swapV2PublicGetQuotePremiumIndex(self.extend(request, params)) + # + # { + # "code":0, + # "msg":"", + # "data":[ + # { + # "symbol": "BTC-USDT", + # "markPrice": "16884.5", + # "indexPrice": "16886.9", + # "lastFundingRate": "0.0001", + # "nextFundingTime": 1672041600000 + # }, + # ... + # ] + # } + # + data = self.safe_dict(response, 'data') + return self.parse_funding_rate(data, market) + + def fetch_funding_rates(self, symbols: Strings = None, params={}) -> FundingRates: + """ + fetch the current funding rate for multiple symbols + + https://bingx-api.github.io/docs/#/swapV2/market-api.html#Current%20Funding%20Rate + + :param str[] [symbols]: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `funding rate structures ` + """ + self.load_markets() + symbols = self.market_symbols(symbols, 'swap', True) + response = self.swapV2PublicGetQuotePremiumIndex(self.extend(params)) + data = self.safe_list(response, 'data', []) + return self.parse_funding_rates(data, symbols) + + def parse_funding_rate(self, contract, market: Market = None) -> FundingRate: + # + # { + # "symbol": "BTC-USDT", + # "markPrice": "16884.5", + # "indexPrice": "16886.9", + # "lastFundingRate": "0.0001", + # "nextFundingTime": 1672041600000 + # } + # + marketId = self.safe_string(contract, 'symbol') + nextFundingTimestamp = self.safe_integer(contract, 'nextFundingTime') + return { + 'info': contract, + 'symbol': self.safe_symbol(marketId, market, '-', 'swap'), + 'markPrice': self.safe_number(contract, 'markPrice'), + 'indexPrice': self.safe_number(contract, 'indexPrice'), + 'interestRate': None, + 'estimatedSettlePrice': None, + 'timestamp': None, + 'datetime': None, + 'fundingRate': self.safe_number(contract, 'lastFundingRate'), + 'fundingTimestamp': None, + 'fundingDatetime': None, + 'nextFundingRate': None, + 'nextFundingTimestamp': nextFundingTimestamp, + 'nextFundingDatetime': self.iso8601(nextFundingTimestamp), + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': None, + } + + def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical funding rate prices + + https://bingx-api.github.io/docs/#/swapV2/market-api.html#Funding%20Rate%20History + + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: timestamp in ms of the earliest funding rate to fetch + :param int [limit]: the maximum amount of `funding rate structures ` to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest funding rate to fetch + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `funding rate structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchFundingRateHistory() requires a symbol argument') + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchFundingRateHistory', 'paginate') + if paginate: + return self.fetch_paginated_call_deterministic('fetchFundingRateHistory', symbol, since, limit, '8h', params) + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + until = self.safe_integer_2(params, 'until', 'startTime') + if until is not None: + params = self.omit(params, ['until']) + request['startTime'] = until + response = self.swapV2PublicGetQuoteFundingRate(self.extend(request, params)) + # + # { + # "code":0, + # "msg":"", + # "data":[ + # { + # "symbol": "BTC-USDT", + # "fundingRate": "0.0001", + # "fundingTime": 1585684800000 + # }, + # ... + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_funding_rate_histories(data, market, since, limit) + + def parse_funding_rate_history(self, contract, market: Market = None): + # + # { + # "symbol": "BTC-USDT", + # "fundingRate": "0.0001", + # "fundingTime": 1585684800000 + # } + # + timestamp = self.safe_integer(contract, 'fundingTime') + return { + 'info': contract, + 'symbol': self.safe_symbol(self.safe_string(contract, 'symbol'), market, '-', 'swap'), + 'fundingRate': self.safe_number(contract, 'fundingRate'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + + def fetch_open_interest(self, symbol: str, params={}): + """ + retrieves the open interest of a trading pair + + https://bingx-api.github.io/docs/#/swapV2/market-api.html#Get%20Swap%20Open%20Positions + https://bingx-api.github.io/docs/#/en-us/cswap/market-api.html#Get%20Swap%20Open%20Positions + + :param str symbol: unified CCXT market symbol + :param dict [params]: exchange specific parameters + :returns dict} an open interest structure{@link https://docs.ccxt.com/#/?id=open-interest-structure: + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = None + if market['inverse']: + response = self.cswapV1PublicGetMarketOpenInterest(self.extend(request, params)) + else: + response = self.swapV2PublicGetQuoteOpenInterest(self.extend(request, params)) + # + # linear swap + # + # { + # "code": 0, + # "msg": "", + # "data": { + # "openInterest": "3289641547.10", + # "symbol": "BTC-USDT", + # "time": 1672026617364 + # } + # } + # + # inverse swap + # + # { + # "code": 0, + # "msg": "", + # "timestamp": 1720328247986, + # "data": [ + # { + # "symbol": "BTC-USD", + # "openInterest": "749.1160", + # "timestamp": 1720310400000 + # } + # ] + # } + # + result: dict = {} + if market['inverse']: + data = self.safe_list(response, 'data', []) + result = self.safe_dict(data, 0, {}) + else: + result = self.safe_dict(response, 'data', {}) + return self.parse_open_interest(result, market) + + def parse_open_interest(self, interest, market: Market = None): + # + # linear swap + # + # { + # "openInterest": "3289641547.10", + # "symbol": "BTC-USDT", + # "time": 1672026617364 + # } + # + # inverse swap + # + # { + # "symbol": "BTC-USD", + # "openInterest": "749.1160", + # "timestamp": 1720310400000 + # } + # + timestamp = self.safe_integer_2(interest, 'time', 'timestamp') + id = self.safe_string(interest, 'symbol') + symbol = self.safe_symbol(id, market, '-', 'swap') + openInterest = self.safe_number(interest, 'openInterest') + return self.safe_open_interest({ + 'symbol': symbol, + 'baseVolume': None, + 'quoteVolume': None, # deprecated + 'openInterestAmount': None, + 'openInterestValue': openInterest, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'info': interest, + }, market) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://bingx-api.github.io/docs/#/en-us/swapV2/market-api.html#Get%20Ticker + https://bingx-api.github.io/docs/#/en-us/spot/market-api.html#24-hour%20price%20changes + https://bingx-api.github.io/docs/#/en-us/cswap/market-api.html#Query%2024-Hour%20Price%20Change + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = None + if market['spot']: + response = self.spotV1PublicGetTicker24hr(self.extend(request, params)) + else: + if market['inverse']: + response = self.cswapV1PublicGetMarketTicker(self.extend(request, params)) + else: + response = self.swapV2PublicGetQuoteTicker(self.extend(request, params)) + # + # spot and swap + # + # { + # "code": 0, + # "msg": "", + # "timestamp": 1720647285296, + # "data": [ + # { + # "symbol": "SOL-USD", + # "priceChange": "-2.418", + # "priceChangePercent": "-1.6900%", + # "lastPrice": "140.574", + # "lastQty": "1", + # "highPrice": "146.190", + # "lowPrice": "138.586", + # "volume": "1464648.00", + # "quoteVolume": "102928.12", + # "openPrice": "142.994", + # "closeTime": "1720647284976", + # "bidPrice": "140.573", + # "bidQty": "372", + # "askPrice": "140.577", + # "askQty": "58" + # } + # ] + # } + # + data = self.safe_list(response, 'data') + if data is not None: + first = self.safe_dict(data, 0, {}) + return self.parse_ticker(first, market) + dataDict = self.safe_dict(response, 'data', {}) + return self.parse_ticker(dataDict, market) + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://bingx-api.github.io/docs/#/en-us/swapV2/market-api.html#Get%20Ticker + https://bingx-api.github.io/docs/#/en-us/spot/market-api.html#24-hour%20price%20changes + https://bingx-api.github.io/docs/#/en-us/cswap/market-api.html#Query%2024-Hour%20Price%20Change + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + market = None + if symbols is not None: + symbols = self.market_symbols(symbols) + firstSymbol = self.safe_string(symbols, 0) + if firstSymbol is not None: + market = self.market(firstSymbol) + type = None + type, params = self.handle_market_type_and_params('fetchTickers', market, params) + subType = None + subType, params = self.handle_sub_type_and_params('fetchTickers', market, params) + response = None + if type == 'spot': + response = self.spotV1PublicGetTicker24hr(params) + else: + if subType == 'inverse': + response = self.cswapV1PublicGetMarketTicker(params) + else: + response = self.swapV2PublicGetQuoteTicker(params) + # + # spot and swap + # + # { + # "code": 0, + # "msg": "", + # "timestamp": 1720647285296, + # "data": [ + # { + # "symbol": "SOL-USD", + # "priceChange": "-2.418", + # "priceChangePercent": "-1.6900%", + # "lastPrice": "140.574", + # "lastQty": "1", + # "highPrice": "146.190", + # "lowPrice": "138.586", + # "volume": "1464648.00", + # "quoteVolume": "102928.12", + # "openPrice": "142.994", + # "closeTime": "1720647284976", + # "bidPrice": "140.573", + # "bidQty": "372", + # "askPrice": "140.577", + # "askQty": "58" + # }, + # ... + # ] + # } + # + tickers = self.safe_list(response, 'data') + return self.parse_tickers(tickers, symbols) + + def fetch_mark_price(self, symbol: str, params={}) -> Ticker: + """ + fetches mark prices for the market + + https://bingx-api.github.io/docs/#/en-us/swapV2/market-api.html#Mark%20Price%20and%20Funding%20Rate + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + market = self.market(symbol) + subType = None + subType, params = self.handle_sub_type_and_params('fetchMarkPrice', market, params, 'linear') + request = { + 'symbol': market['id'], + } + response = None + if subType == 'inverse': + response = self.cswapV1PublicGetMarketPremiumIndex(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "timestamp": 1728577213289, + # "data": [ + # { + # "symbol": "ETH-USD", + # "lastFundingRate": "0.0001", + # "markPrice": "2402.68", + # "indexPrice": "2404.92", + # "nextFundingTime": 1728604800000 + # } + # ] + # } + # + else: + response = self.swapV2PublicGetQuotePremiumIndex(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "data": { + # "symbol": "ETH-USDT", + # "markPrice": "2408.40", + # "indexPrice": "2409.62", + # "lastFundingRate": "0.00009900", + # "nextFundingTime": 1728604800000 + # } + # } + # + if isinstance(response['data'], list): + return self.parse_ticker(self.safe_dict(response['data'], 0, {}), market) + return self.parse_ticker(response['data'], market) + + def fetch_mark_prices(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches mark prices for multiple markets + + https://bingx-api.github.io/docs/#/en-us/swapV2/market-api.html#Mark%20Price%20and%20Funding%20Rate + + :param str[] [symbols]: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + market = None + if symbols is not None: + symbols = self.market_symbols(symbols) + firstSymbol = self.safe_string(symbols, 0) + if firstSymbol is not None: + market = self.market(firstSymbol) + subType = None + subType, params = self.handle_sub_type_and_params('fetchMarkPrices', market, params, 'linear') + response = None + if subType == 'inverse': + response = self.cswapV1PublicGetMarketPremiumIndex(params) + else: + response = self.swapV2PublicGetQuotePremiumIndex(params) + # + # spot and swap + # + # { + # "code": 0, + # "msg": "", + # "timestamp": 1720647285296, + # "data": [ + # { + # "symbol": "SOL-USD", + # "priceChange": "-2.418", + # "priceChangePercent": "-1.6900%", + # "lastPrice": "140.574", + # "lastQty": "1", + # "highPrice": "146.190", + # "lowPrice": "138.586", + # "volume": "1464648.00", + # "quoteVolume": "102928.12", + # "openPrice": "142.994", + # "closeTime": "1720647284976", + # "bidPrice": "140.573", + # "bidQty": "372", + # "askPrice": "140.577", + # "askQty": "58" + # }, + # ... + # ] + # } + # + tickers = self.safe_list(response, 'data') + return self.parse_tickers(tickers, symbols) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # mark price + # { + # "symbol": "string", + # "lastFundingRate": "string", + # "markPrice": "string", + # "indexPrice": "string", + # "nextFundingTime": "int64" + # } + # + # spot + # { + # "symbol": "BTC-USDT", + # "openPrice": "26032.08", + # "highPrice": "26178.86", + # "lowPrice": "25968.18", + # "lastPrice": "26113.60", + # "volume": "1161.79", + # "quoteVolume": "30288466.44", + # "openTime": "1693081020762", + # "closeTime": "1693167420762", + # added 2023-11-10: + # "bidPrice": 16726.0, + # "bidQty": 0.05, + # "askPrice": 16726.0, + # "askQty": 0.05, + # } + # swap + # + # { + # "symbol": "BTC-USDT", + # "priceChange": "52.5", + # "priceChangePercent": "0.31%", # they started to add the percent sign in value + # "lastPrice": "16880.5", + # "lastQty": "2.2238", # only present in swap! + # "highPrice": "16897.5", + # "lowPrice": "16726.0", + # "volume": "245870.1692", + # "quoteVolume": "4151395117.73", + # "openPrice": "16832.0", + # "openTime": 1672026667803, + # "closeTime": 1672026648425, + # added 2023-11-10: + # "bidPrice": 16726.0, + # "bidQty": 0.05, + # "askPrice": 16726.0, + # "askQty": 0.05, + # } + # + marketId = self.safe_string(ticker, 'symbol') + lastQty = self.safe_string(ticker, 'lastQty') + # in spot markets, lastQty is not present + # it's(bad, but) the only way we can check the tickers origin + type = 'spot' if (lastQty is None) else 'swap' + market = self.safe_market(marketId, market, None, type) + symbol = market['symbol'] + open = self.safe_string(ticker, 'openPrice') + high = self.safe_string(ticker, 'highPrice') + low = self.safe_string(ticker, 'lowPrice') + close = self.safe_string(ticker, 'lastPrice') + quoteVolume = self.safe_string(ticker, 'quoteVolume') + baseVolume = self.safe_string(ticker, 'volume') + percentage = self.safe_string(ticker, 'priceChangePercent') + if percentage is not None: + percentage = percentage.replace('%', '') + change = self.safe_string(ticker, 'priceChange') + ts = self.safe_integer(ticker, 'closeTime') + if ts == 0: + ts = None + datetime = self.iso8601(ts) + bid = self.safe_string(ticker, 'bidPrice') + bidVolume = self.safe_string(ticker, 'bidQty') + ask = self.safe_string(ticker, 'askPrice') + askVolume = self.safe_string(ticker, 'askQty') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': ts, + 'datetime': datetime, + 'high': high, + 'low': low, + 'bid': bid, + 'bidVolume': bidVolume, + 'ask': ask, + 'askVolume': askVolume, + 'vwap': None, + 'open': open, + 'close': close, + 'last': None, + 'previousClose': None, + 'change': change, + 'percentage': percentage, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'markPrice': self.safe_string(ticker, 'markPrice'), + 'indexPrice': self.safe_string(ticker, 'indexPrice'), + 'info': ticker, + }, market) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://bingx-api.github.io/docs/#/spot/trade-api.html#Query%20Assets + https://bingx-api.github.io/docs/#/en-us/swapV2/account-api.html#Query%20account%20data + https://bingx-api.github.io/docs/#/standard/contract-interface.html#Query%20standard%20contract%20balance + https://bingx-api.github.io/docs/#/en-us/cswap/trade-api.html#Query%20Account%20Assets + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.standard]: whether to fetch standard contract balances + :param str [params.type]: the type of balance to fetch(spot, swap, funding) default is `spot` + :returns dict: a `balance structure ` + """ + self.load_markets() + response = None + standard = None + standard, params = self.handle_option_and_params(params, 'fetchBalance', 'standard', False) + subType = None + subType, params = self.handle_sub_type_and_params('fetchBalance', None, params) + marketType, marketTypeQuery = self.handle_market_type_and_params('fetchBalance', None, params) + if standard: + response = self.contractV1PrivateGetBalance(marketTypeQuery) + # + # { + # "code": 0, + # "timestamp": 1721192833454, + # "data": [ + # { + # "asset": "USDT", + # "balance": "4.72644300000000000000", + # "crossWalletBalance": "4.72644300000000000000", + # "crossUnPnl": "0", + # "availableBalance": "4.72644300000000000000", + # "maxWithdrawAmount": "4.72644300000000000000", + # "marginAvailable": False, + # "updateTime": 1721192833443 + # }, + # ] + # } + # + elif (marketType == 'funding') or (marketType == 'fund'): + response = self.fundV1PrivateGetAccountBalance(marketTypeQuery) + # { + # code: '0', + # timestamp: '1754906016631', + # data: { + # assets: [ + # { + # asset: 'USDT', + # free: '44.37692200000000237300', + # locked: '0.00000000000000000000' + # } + # ] + # } + # } + elif marketType == 'spot': + response = self.spotV1PrivateGetAccountBalance(marketTypeQuery) + # + # { + # "code": 0, + # "msg": "", + # "debugMsg": "", + # "data": { + # "balances": [ + # { + # "asset": "USDT", + # "free": "45.733046995800514", + # "locked": "0" + # }, + # ] + # } + # } + # + else: + if subType == 'inverse': + response = self.cswapV1PrivateGetUserBalance(marketTypeQuery) + # + # { + # "code": 0, + # "msg": "", + # "timestamp": 1721191833813, + # "data": [ + # { + # "asset": "SOL", + # "balance": "0.35707951", + # "equity": "0.35791051", + # "unrealizedProfit": "0.00083099", + # "availableMargin": "0.35160653", + # "usedMargin": "0.00630397", + # "freezedMargin": "0", + # "shortUid": "12851936" + # } + # ] + # } + # + else: + response = self.swapV3PrivateGetUserBalance(marketTypeQuery) + # + # { + # "code": 0, + # "msg": "", + # "data": [ + # { + # "userId": "116***295", + # "asset": "USDT", + # "balance": "194.8212", + # "equity": "196.7431", + # "unrealizedProfit": "1.9219", + # "realisedProfit": "-109.2504", + # "availableMargin": "193.7609", + # "usedMargin": "1.0602", + # "freezedMargin": "0.0000", + # "shortUid": "12851936" + # } + # ] + # } + return self.parse_balance(response) + + def parse_balance(self, response) -> Balances: + # + # standard + # + # { + # "code": 0, + # "timestamp": 1721192833454, + # "data": [ + # { + # "asset": "USDT", + # "balance": "4.72644300000000000000", + # "crossWalletBalance": "4.72644300000000000000", + # "crossUnPnl": "0", + # "availableBalance": "4.72644300000000000000", + # "maxWithdrawAmount": "4.72644300000000000000", + # "marginAvailable": False, + # "updateTime": 1721192833443 + # }, + # ] + # } + # + # spot + # + # { + # "code": 0, + # "msg": "", + # "debugMsg": "", + # "data": { + # "balances": [ + # { + # "asset": "USDT", + # "free": "45.733046995800514", + # "locked": "0" + # }, + # ] + # } + # } + # + # inverse swap + # + # { + # "code": 0, + # "msg": "", + # "timestamp": 1721191833813, + # "data": [ + # { + # "asset": "SOL", + # "balance": "0.35707951", + # "equity": "0.35791051", + # "unrealizedProfit": "0.00083099", + # "availableMargin": "0.35160653", + # "usedMargin": "0.00630397", + # "freezedMargin": "0", + # "shortUid": "12851936" + # } + # ] + # } + # + # linear swap + # + # { + # "code": 0, + # "msg": "", + # "data": [ + # { + # "userId": "116***295", + # "asset": "USDT", + # "balance": "194.8212", + # "equity": "196.7431", + # "unrealizedProfit": "1.9219", + # "realisedProfit": "-109.2504", + # "availableMargin": "193.7609", + # "usedMargin": "1.0602", + # "freezedMargin": "0.0000", + # "shortUid": "12851936" + # } + # ] + # } + # + result: dict = {'info': response} + contractBalances = self.safe_list(response, 'data') + firstContractBalances = self.safe_dict(contractBalances, 0) + isContract = firstContractBalances is not None + spotData = self.safe_dict(response, 'data', {}) + spotBalances = self.safe_list_2(spotData, 'balances', 'assets', []) + if isContract: + for i in range(0, len(contractBalances)): + balance = contractBalances[i] + currencyId = self.safe_string(balance, 'asset') + if currencyId is None: # linear v3 returns empty asset + break + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string_2(balance, 'availableMargin', 'availableBalance') + account['used'] = self.safe_string(balance, 'usedMargin') + account['total'] = self.safe_string(balance, 'maxWithdrawAmount') + result[code] = account + else: + for i in range(0, len(spotBalances)): + balance = spotBalances[i] + currencyId = self.safe_string(balance, 'asset') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(balance, 'free') + account['used'] = self.safe_string(balance, 'locked') + result[code] = account + return self.safe_balance(result) + + def fetch_position_history(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Position]: + """ + fetches historical positions + + https://bingx-api.github.io/docs/#/en-us/swapV2/trade-api.html#Query%20Position%20History + + :param str symbol: unified contract symbol + :param int [since]: the earliest time in ms to fetch positions for + :param int [limit]: the maximum amount of records to fetch + :param dict [params]: extra parameters specific to the exchange api endpoint + :param int [params.until]: the latest time in ms to fetch positions for + :returns dict[]: a list of `position structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['pageSize'] = limit + if since is not None: + request['startTs'] = since + request, params = self.handle_until_option('endTs', request, params) + response = None + if market['linear']: + response = self.swapV1PrivateGetTradePositionHistory(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchPositionHistory() is not supported for inverse swap positions') + # + # { + # "code": 0, + # "msg": "", + # "data": { + # "positionHistory": [ + # { + # "positionId": "1861675561156571136", + # "symbol": "LTC-USDT", + # "isolated": False, + # "positionSide": "LONG", + # "openTime": 1732693017000, + # "updateTime": 1733310292000, + # "avgPrice": "95.18", + # "avgClosePrice": "129.48", + # "realisedProfit": "102.89", + # "netProfit": "99.63", + # "positionAmt": "30.0", + # "closePositionAmt": "30.0", + # "leverage": 6, + # "closeAllPositions": True, + # "positionCommission": "-0.33699650000000003", + # "totalFunding": "-2.921461693902908" + # }, + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + records = self.safe_list(data, 'positionHistory', []) + positions = self.parse_positions(records) + return self.filter_by_symbol_since_limit(positions, symbol, since, limit) + + def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://bingx-api.github.io/docs/#/en-us/swapV2/account-api.html#Query%20position%20data + https://bingx-api.github.io/docs/#/en-us/standard/contract-interface.html#position + https://bingx-api.github.io/docs/#/en-us/cswap/trade-api.html#Query%20warehouse + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.standard]: whether to fetch standard contract positions + :returns dict[]: a list of `position structures ` + """ + self.load_markets() + symbols = self.market_symbols(symbols) + standard = None + standard, params = self.handle_option_and_params(params, 'fetchPositions', 'standard', False) + response = None + if standard: + response = self.contractV1PrivateGetAllPosition(params) + else: + market = None + if symbols is not None: + symbols = self.market_symbols(symbols) + firstSymbol = self.safe_string(symbols, 0) + if firstSymbol is not None: + market = self.market(firstSymbol) + subType = None + subType, params = self.handle_sub_type_and_params('fetchPositions', market, params) + if subType == 'inverse': + response = self.cswapV1PrivateGetUserPositions(params) + # + # { + # "code": 0, + # "msg": "", + # "timestamp": 0, + # "data": [ + # { + # "symbol": "SOL-USD", + # "positionId": "1813080351385337856", + # "positionSide": "LONG", + # "isolated": False, + # "positionAmt": "1", + # "availableAmt": "1", + # "unrealizedProfit": "-0.00009074", + # "initialMargin": "0.00630398", + # "liquidationPrice": 23.968303426677032, + # "avgPrice": "158.63", + # "leverage": 10, + # "markPrice": "158.402", + # "riskRate": "0.00123783", + # "maxMarginReduction": "0", + # "updateTime": 1721107015848 + # } + # ] + # } + # + else: + response = self.swapV2PrivateGetUserPositions(params) + # + # { + # "code": 0, + # "msg": "", + # "data": [ + # { + # "positionId": "1792480725958881280", + # "symbol": "LTC-USDT", + # "currency": "USDT", + # "positionAmt": "0.1", + # "availableAmt": "0.1", + # "positionSide": "LONG", + # "isolated": False, + # "avgPrice": "83.53", + # "initialMargin": "1.3922", + # "margin": "0.3528", + # "leverage": 6, + # "unrealizedProfit": "-1.0393", + # "realisedProfit": "-0.2119", + # "liquidationPrice": 0, + # "pnlRatio": "-0.7465", + # "maxMarginReduction": "0.0000", + # "riskRate": "0.0008", + # "markPrice": "73.14", + # "positionValue": "7.3136", + # "onlyOnePosition": True, + # "updateTime": 1721088016688 + # } + # ] + # } + # + positions = self.safe_list(response, 'data', []) + return self.parse_positions(positions, symbols) + + def fetch_position(self, symbol: str, params={}): + """ + fetch data on a single open contract trade position + + https://bingx-api.github.io/docs/#/en-us/swapV2/account-api.html#Query%20position%20data + https://bingx-api.github.io/docs/#/en-us/cswap/trade-api.html#Query%20warehouse + + :param str symbol: unified market symbol of the market the position is held in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `position structure ` + """ + self.load_markets() + market = self.market(symbol) + if not market['swap']: + raise BadRequest(self.id + ' fetchPosition() supports swap markets only') + request: dict = { + 'symbol': market['id'], + } + response = None + if market['inverse']: + response = self.cswapV1PrivateGetUserPositions(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "timestamp": 0, + # "data": [ + # { + # "symbol": "SOL-USD", + # "positionId": "1813080351385337856", + # "positionSide": "LONG", + # "isolated": False, + # "positionAmt": "1", + # "availableAmt": "1", + # "unrealizedProfit": "-0.00009074", + # "initialMargin": "0.00630398", + # "liquidationPrice": 23.968303426677032, + # "avgPrice": "158.63", + # "leverage": 10, + # "markPrice": "158.402", + # "riskRate": "0.00123783", + # "maxMarginReduction": "0", + # "updateTime": 1721107015848 + # } + # ] + # } + # + else: + response = self.swapV2PrivateGetUserPositions(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "data": [ + # { + # "positionId": "1792480725958881280", + # "symbol": "LTC-USDT", + # "currency": "USDT", + # "positionAmt": "0.1", + # "availableAmt": "0.1", + # "positionSide": "LONG", + # "isolated": False, + # "avgPrice": "83.53", + # "initialMargin": "1.3922", + # "margin": "0.3528", + # "leverage": 6, + # "unrealizedProfit": "-1.0393", + # "realisedProfit": "-0.2119", + # "liquidationPrice": 0, + # "pnlRatio": "-0.7465", + # "maxMarginReduction": "0.0000", + # "riskRate": "0.0008", + # "markPrice": "73.14", + # "positionValue": "7.3136", + # "onlyOnePosition": True, + # "updateTime": 1721088016688 + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + first = self.safe_dict(data, 0, {}) + return self.parse_position(first, market) + + def parse_position(self, position: dict, market: Market = None): + # + # inverse swap + # + # { + # "symbol": "SOL-USD", + # "positionId": "1813080351385337856", + # "positionSide": "LONG", + # "isolated": False, + # "positionAmt": "1", + # "availableAmt": "1", + # "unrealizedProfit": "-0.00009074", + # "initialMargin": "0.00630398", + # "liquidationPrice": 23.968303426677032, + # "avgPrice": "158.63", + # "leverage": 10, + # "markPrice": "158.402", + # "riskRate": "0.00123783", + # "maxMarginReduction": "0", + # "updateTime": 1721107015848 + # } + # + # linear swap + # + # { + # "positionId": "1792480725958881280", + # "symbol": "LTC-USDT", + # "currency": "USDT", + # "positionAmt": "0.1", + # "availableAmt": "0.1", + # "positionSide": "LONG", + # "isolated": False, + # "avgPrice": "83.53", + # "initialMargin": "1.3922", + # "margin": "0.3528", + # "leverage": 6, + # "unrealizedProfit": "-1.0393", + # "realisedProfit": "-0.2119", + # "liquidationPrice": 0, + # "pnlRatio": "-0.7465", + # "maxMarginReduction": "0.0000", + # "riskRate": "0.0008", + # "markPrice": "73.14", + # "positionValue": "7.3136", + # "onlyOnePosition": True, + # "updateTime": 1721088016688 + # } + # + # standard position + # + # { + # "currentPrice": "82.91", + # "symbol": "LTC/USDT", + # "initialMargin": "5.00000000000000000000", + # "unrealizedProfit": "-0.26464500", + # "leverage": "20.000000000", + # "isolated": True, + # "entryPrice": "83.13", + # "positionSide": "LONG", + # "positionAmt": "1.20365912", + # } + # + # linear swap fetchPositionHistory + # + # { + # "positionId": "1861675561156571136", + # "symbol": "LTC-USDT", + # "isolated": False, + # "positionSide": "LONG", + # "openTime": 1732693017000, + # "updateTime": 1733310292000, + # "avgPrice": "95.18", + # "avgClosePrice": "129.48", + # "realisedProfit": "102.89", + # "netProfit": "99.63", + # "positionAmt": "30.0", + # "closePositionAmt": "30.0", + # "leverage": 6, + # "closeAllPositions": True, + # "positionCommission": "-0.33699650000000003", + # "totalFunding": "-2.921461693902908" + # } + # + marketId = self.safe_string(position, 'symbol', '') + marketId = marketId.replace('/', '-') # standard return different format + isolated = self.safe_bool(position, 'isolated') + marginMode = None + if isolated is not None: + marginMode = 'isolated' if isolated else 'cross' + timestamp = self.safe_integer(position, 'openTime') + return self.safe_position({ + 'info': position, + 'id': self.safe_string(position, 'positionId'), + 'symbol': self.safe_symbol(marketId, market, '-', 'swap'), + 'notional': self.safe_number(position, 'positionValue'), + 'marginMode': marginMode, + 'liquidationPrice': None, + 'entryPrice': self.safe_number_2(position, 'avgPrice', 'entryPrice'), + 'unrealizedPnl': self.safe_number(position, 'unrealizedProfit'), + 'realizedPnl': self.safe_number(position, 'realisedProfit'), + 'percentage': None, + 'contracts': self.safe_number(position, 'positionAmt'), + 'contractSize': None, + 'markPrice': self.safe_number(position, 'markPrice'), + 'lastPrice': None, + 'side': self.safe_string_lower(position, 'positionSide'), + 'hedged': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastUpdateTimestamp': self.safe_integer(position, 'updateTime'), + 'maintenanceMargin': None, + 'maintenanceMarginPercentage': None, + 'collateral': None, + 'initialMargin': self.safe_number(position, 'initialMargin'), + 'initialMarginPercentage': None, + 'leverage': self.safe_number(position, 'leverage'), + 'marginRatio': None, + 'stopLossPrice': None, + 'takeProfitPrice': None, + }) + + def create_market_order_with_cost(self, symbol: str, side: OrderSide, cost: float, params={}): + """ + create a market order by providing the symbol, side and cost + :param str symbol: unified symbol of the market to create an order in + :param str side: 'buy' or 'sell' + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + params['quoteOrderQty'] = cost + return self.create_order(symbol, 'market', side, cost, None, params) + + def create_market_buy_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market buy order by providing the symbol and cost + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + params['quoteOrderQty'] = cost + return self.create_order(symbol, 'market', 'buy', cost, None, params) + + def create_market_sell_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market sell order by providing the symbol and cost + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + params['quoteOrderQty'] = cost + return self.create_order(symbol, 'market', 'sell', cost, None, params) + + def create_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + @ignore + helper function to build request + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: request to be sent to the exchange + """ + market = self.market(symbol) + postOnly = None + marketType = None + marketType, params = self.handle_market_type_and_params('createOrder', market, params) + type = type.upper() + request: dict = { + 'symbol': market['id'], + 'type': type, + 'side': side.upper(), + } + isMarketOrder = type == 'MARKET' + isSpot = marketType == 'spot' + isTwapOrder = type == 'TWAP' + if isTwapOrder and isSpot: + raise BadSymbol(self.id + ' createOrder() twap order supports swap contracts only') + stopLossPrice = self.safe_string(params, 'stopLossPrice') + takeProfitPrice = self.safe_string(params, 'takeProfitPrice') + triggerPrice = self.safe_string_2(params, 'stopPrice', 'triggerPrice') + isTriggerOrder = triggerPrice is not None + isStopLossPriceOrder = stopLossPrice is not None + isTakeProfitPriceOrder = takeProfitPrice is not None + exchangeClientOrderId = 'newClientOrderId' if isSpot else 'clientOrderID' + clientOrderId = self.safe_string_2(params, exchangeClientOrderId, 'clientOrderId') + if clientOrderId is not None: + request[exchangeClientOrderId] = clientOrderId + timeInForce = self.safe_string_upper(params, 'timeInForce') + postOnly, params = self.handle_post_only(isMarketOrder, timeInForce == 'PostOnly', params) + if postOnly or (timeInForce == 'PostOnly'): + request['timeInForce'] = 'PostOnly' + elif timeInForce == 'IOC': + request['timeInForce'] = 'IOC' + elif timeInForce == 'GTC': + request['timeInForce'] = 'GTC' + if isSpot: + cost = self.safe_string_2(params, 'cost', 'quoteOrderQty') + params = self.omit(params, 'cost') + if cost is not None: + request['quoteOrderQty'] = self.parse_to_numeric(self.cost_to_precision(symbol, cost)) + else: + if isMarketOrder and (price is not None): + # keep the legacy behavior, to avoid breaking the old spot-market-buying code + calculatedCost = Precise.string_mul(self.number_to_string(amount), self.number_to_string(price)) + request['quoteOrderQty'] = self.parse_to_numeric(calculatedCost) + else: + request['quantity'] = self.parse_to_numeric(self.amount_to_precision(symbol, amount)) + if not isMarketOrder: + request['price'] = self.parse_to_numeric(self.price_to_precision(symbol, price)) + if triggerPrice is not None: + if isMarketOrder and self.safe_string(request, 'quoteOrderQty') is None: + raise ArgumentsRequired(self.id + ' createOrder() requires the cost parameter(or the amount + price) for placing spot market-buy trigger orders') + request['stopPrice'] = self.price_to_precision(symbol, triggerPrice) + if type == 'LIMIT': + request['type'] = 'TRIGGER_LIMIT' + elif type == 'MARKET': + request['type'] = 'TRIGGER_MARKET' + elif (stopLossPrice is not None) or (takeProfitPrice is not None): + stopTakePrice = stopLossPrice if (stopLossPrice is not None) else takeProfitPrice + if type == 'LIMIT': + request['type'] = 'TAKE_STOP_LIMIT' + elif type == 'MARKET': + request['type'] = 'TAKE_STOP_MARKET' + request['stopPrice'] = self.parse_to_numeric(self.price_to_precision(symbol, stopTakePrice)) + else: + if isTwapOrder: + twapRequest: dict = { + 'symbol': request['symbol'], + 'side': request['side'], + 'positionSide': 'LONG' if (side == 'buy') else 'SHORT', + 'triggerPrice': self.parse_to_numeric(self.price_to_precision(symbol, triggerPrice)), + 'totalAmount': self.parse_to_numeric(self.amount_to_precision(symbol, amount)), + } + # { + # "symbol": "LTC-USDT", + # "side": "BUY", + # "positionSide": "LONG", + # "priceType": "constant", + # "priceVariance": "10", + # "triggerPrice": "120", + # "interval": 8, + # "amountPerOrder": "0.5", + # "totalAmount": "1" + # } + return self.extend(twapRequest, params) + if timeInForce == 'FOK': + request['timeInForce'] = 'FOK' + trailingAmount = self.safe_string(params, 'trailingAmount') + trailingPercent = self.safe_string_2(params, 'trailingPercent', 'priceRate') + trailingType = self.safe_string(params, 'trailingType', 'TRAILING_STOP_MARKET') + isTrailingAmountOrder = trailingAmount is not None + isTrailingPercentOrder = trailingPercent is not None + isTrailing = isTrailingAmountOrder or isTrailingPercentOrder + stopLoss = self.safe_value(params, 'stopLoss') + takeProfit = self.safe_value(params, 'takeProfit') + isStopLoss = stopLoss is not None + isTakeProfit = takeProfit is not None + if ((type == 'LIMIT') or (type == 'TRIGGER_LIMIT') or (type == 'STOP') or (type == 'TAKE_PROFIT')) and not isTrailing: + request['price'] = self.parse_to_numeric(self.price_to_precision(symbol, price)) + reduceOnly = self.safe_bool(params, 'reduceOnly', False) + if isTriggerOrder: + request['stopPrice'] = self.parse_to_numeric(self.price_to_precision(symbol, triggerPrice)) + if isMarketOrder or (type == 'TRIGGER_MARKET'): + request['type'] = 'TRIGGER_MARKET' + elif (type == 'LIMIT') or (type == 'TRIGGER_LIMIT'): + request['type'] = 'TRIGGER_LIMIT' + elif isStopLossPriceOrder or isTakeProfitPriceOrder: + # This can be used to set the stop loss and take profit, but the position needs to be opened first + reduceOnly = True + if isStopLossPriceOrder: + request['stopPrice'] = self.parse_to_numeric(self.price_to_precision(symbol, stopLossPrice)) + if isMarketOrder or (type == 'STOP_MARKET'): + request['type'] = 'STOP_MARKET' + elif (type == 'LIMIT') or (type == 'STOP'): + request['type'] = 'STOP' + elif isTakeProfitPriceOrder: + request['stopPrice'] = self.parse_to_numeric(self.price_to_precision(symbol, takeProfitPrice)) + if isMarketOrder or (type == 'TAKE_PROFIT_MARKET'): + request['type'] = 'TAKE_PROFIT_MARKET' + elif (type == 'LIMIT') or (type == 'TAKE_PROFIT'): + request['type'] = 'TAKE_PROFIT' + elif isTrailing: + request['type'] = trailingType + if isTrailingAmountOrder: + request['price'] = self.parse_to_numeric(trailingAmount) + elif isTrailingPercentOrder: + requestTrailingPercent = Precise.string_div(trailingPercent, '100') + request['priceRate'] = self.parse_to_numeric(requestTrailingPercent) + if isStopLoss or isTakeProfit: + stringifiedAmount = self.number_to_string(amount) + if isStopLoss: + slTriggerPrice = self.safe_string_2(stopLoss, 'triggerPrice', 'stopPrice', stopLoss) + slWorkingType = self.safe_string(stopLoss, 'workingType', 'MARK_PRICE') + slType = self.safe_string(stopLoss, 'type', 'STOP_MARKET') + slRequest: dict = { + 'stopPrice': self.parse_to_numeric(self.price_to_precision(symbol, slTriggerPrice)), + 'workingType': slWorkingType, + 'type': slType, + } + slPrice = self.safe_string(stopLoss, 'price') + if slPrice is not None: + slRequest['price'] = self.parse_to_numeric(self.price_to_precision(symbol, slPrice)) + slQuantity = self.safe_string(stopLoss, 'quantity', stringifiedAmount) + slRequest['quantity'] = self.parse_to_numeric(self.amount_to_precision(symbol, slQuantity)) + request['stopLoss'] = self.json(slRequest) + if isTakeProfit: + tkTriggerPrice = self.safe_string_2(takeProfit, 'triggerPrice', 'stopPrice', takeProfit) + tkWorkingType = self.safe_string(takeProfit, 'workingType', 'MARK_PRICE') + tpType = self.safe_string(takeProfit, 'type', 'TAKE_PROFIT_MARKET') + tpRequest: dict = { + 'stopPrice': self.parse_to_numeric(self.price_to_precision(symbol, tkTriggerPrice)), + 'workingType': tkWorkingType, + 'type': tpType, + } + slPrice = self.safe_string(takeProfit, 'price') + if slPrice is not None: + tpRequest['price'] = self.parse_to_numeric(self.price_to_precision(symbol, slPrice)) + tkQuantity = self.safe_string(takeProfit, 'quantity', stringifiedAmount) + tpRequest['quantity'] = self.parse_to_numeric(self.amount_to_precision(symbol, tkQuantity)) + request['takeProfit'] = self.json(tpRequest) + positionSide = None + hedged = self.safe_bool(params, 'hedged', False) + if hedged: + params = self.omit(params, 'reduceOnly') + if reduceOnly: + positionSide = 'SHORT' if (side == 'buy') else 'LONG' + else: + positionSide = 'LONG' if (side == 'buy') else 'SHORT' + else: + positionSide = 'BOTH' + request['positionSide'] = positionSide + amountReq = amount + if not market['inverse']: + amountReq = self.parse_to_numeric(self.amount_to_precision(symbol, amount)) + request['quantity'] = amountReq # precision not available for inverse contracts + params = self.omit(params, ['hedged', 'triggerPrice', 'stopLossPrice', 'takeProfitPrice', 'trailingAmount', 'trailingPercent', 'trailingType', 'takeProfit', 'stopLoss', 'clientOrderId']) + return self.extend(request, params) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://bingx-api.github.io/docs/#/en-us/swapV2/trade-api.html#Trade%20order + https://bingx-api.github.io/docs/#/en-us/spot/trade-api.html#Create%20an%20Order + https://bingx-api.github.io/docs/#/en-us/cswap/trade-api.html#Trade%20order + https://bingx-api.github.io/docs/#/en-us/swapV2/trade-api.html#Place%20TWAP%20Order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.clientOrderId]: a unique id for the order + :param bool [params.postOnly]: True to place a post only order + :param str [params.timeInForce]: spot supports 'PO', 'GTC' and 'IOC', swap supports 'PO', 'GTC', 'IOC' and 'FOK' + :param bool [params.reduceOnly]: *swap only* True or False whether the order is reduce only + :param float [params.triggerPrice]: triggerPrice at which the attached take profit / stop loss order will be triggered + :param float [params.stopLossPrice]: stop loss trigger price + :param float [params.takeProfitPrice]: take profit trigger price + :param float [params.cost]: the quote quantity that can be used alternative for the amount + :param float [params.trailingAmount]: *swap only* the quote amount to trail away from the current market price + :param float [params.trailingPercent]: *swap only* the percent to trail away from the current market price + :param dict [params.takeProfit]: *takeProfit object in params* containing the triggerPrice at which the attached take profit order will be triggered + :param float [params.takeProfit.triggerPrice]: take profit trigger price + :param dict [params.stopLoss]: *stopLoss object in params* containing the triggerPrice at which the attached stop loss order will be triggered + :param float [params.stopLoss.triggerPrice]: stop loss trigger price + :param boolean [params.test]: *swap only* whether to use the test endpoint or not, default is False + :param str [params.positionSide]: *contracts only* "BOTH" for one way mode, "LONG" for buy side of hedged mode, "SHORT" for sell side of hedged mode + :param boolean [params.hedged]: *swap only* whether the order is in hedged mode or one way mode + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + test = self.safe_bool(params, 'test', False) + params = self.omit(params, 'test') + request = self.create_order_request(symbol, type, side, amount, price, params) + response = None + if market['swap']: + if test: + response = self.swapV2PrivatePostTradeOrderTest(request) + elif market['inverse']: + response = self.cswapV1PrivatePostTradeOrder(request) + elif type == 'twap': + response = self.swapV1PrivatePostTwapOrder(request) + else: + response = self.swapV2PrivatePostTradeOrder(request) + else: + response = self.spotV1PrivatePostTradeOrder(request) + # + # spot + # + # { + # "code": 0, + # "msg": "", + # "data": { + # "symbol": "XRP-USDT", + # "orderId": 1514090846268424192, + # "transactTime": 1649822362855, + # "price": "0.5", + # "origQty": "10", + # "executedQty": "0", + # "cummulativeQuoteQty": "0", + # "status": "PENDING", + # "type": "LIMIT", + # "side": "BUY" + # } + # } + # + # linear swap + # + # { + # "code": 0, + # "msg": "", + # "data": { + # "order": { + # "symbol": "BTC-USDT", + # "orderId": 1709036527545438208, + # "side": "BUY", + # "positionSide": "LONG", + # "type": "TRIGGER_LIMIT", + # "clientOrderID": "", + # "workingType": "" + # } + # } + # } + # + # inverse swap + # + # { + # "orderId": 1809841379603398656, + # "symbol": "SOL-USD", + # "positionSide": "LONG", + # "side": "BUY", + # "type": "LIMIT", + # "price": 100, + # "quantity": 1, + # "stopPrice": 0, + # "workingType": "", + # "timeInForce": "" + # } + # + # twap order + # + # { + # "code": 0, + # "msg": "", + # "timestamp": 1732693774386, + # "data": { + # "mainOrderId": "4633860139993029715" + # } + # } + # + if isinstance(response, str): + # broken api engine : order-ids are too long numbers(i.e. 1742930526912864656) + # and json.loadscan not handle them in JS, so we have to use .parseJson + # however, when order has an attached SL/TP, their value types need extra parsing + response = self.fix_stringified_json_members(response) + response = self.parse_json(response) + data = self.safe_dict(response, 'data', {}) + result: dict = {} + if market['swap']: + if market['inverse']: + result = response + else: + result = self.safe_dict(data, 'order', data) + else: + result = data + return self.parse_order(result, market) + + def create_orders(self, orders: List[OrderRequest], params={}): + """ + create a list of trade orders + + https://bingx-api.github.io/docs/#/spot/trade-api.html#Batch%20Placing%20Orders + https://bingx-api.github.io/docs/#/swapV2/trade-api.html#Bulk%20order + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.sync]: *spot only* if True, multiple orders are ordered serially and all orders do not require the same symbol/side/type + :returns dict: an `order structure ` + """ + self.load_markets() + ordersRequests = [] + marketIds = [] + for i in range(0, len(orders)): + rawOrder = orders[i] + marketId = self.safe_string(rawOrder, 'symbol') + type = self.safe_string(rawOrder, 'type') + marketIds.append(marketId) + side = self.safe_string(rawOrder, 'side') + amount = self.safe_number(rawOrder, 'amount') + price = self.safe_number(rawOrder, 'price') + orderParams = self.safe_dict(rawOrder, 'params', {}) + orderRequest = self.create_order_request(marketId, type, side, amount, price, orderParams) + ordersRequests.append(orderRequest) + symbols = self.market_symbols(marketIds, None, False, True, True) + symbolsLength = len(symbols) + market = self.market(symbols[0]) + request: dict = {} + response = None + if market['swap']: + if symbolsLength > 5: + raise InvalidOrder(self.id + ' createOrders() can not create more than 5 orders at once for swap markets') + request['batchOrders'] = self.json(ordersRequests) + response = self.swapV2PrivatePostTradeBatchOrders(request) + else: + sync = self.safe_bool(params, 'sync', False) + if sync: + request['sync'] = True + request['data'] = self.json(ordersRequests) + response = self.spotV1PrivatePostTradeBatchOrders(request) + # + # spot + # + # { + # "code": 0, + # "msg": "", + # "debugMsg": "", + # "data": { + # "orders": [ + # { + # "symbol": "BTC-USDT", + # "orderId": 1720661389564968960, + # "transactTime": 1699072618272, + # "price": "25000", + # "origQty": "0.0002", + # "executedQty": "0", + # "cummulativeQuoteQty": "0", + # "status": "PENDING", + # "type": "LIMIT", + # "side": "BUY" + # }, + # ] + # } + # } + # + # swap + # + # { + # "code": 0, + # "msg": "", + # "data": { + # "orders": [ + # { + # "symbol": "BTC-USDT", + # "orderId": 1720657081994006528, + # "side": "BUY", + # "positionSide": "LONG", + # "type": "LIMIT", + # "clientOrderID": "", + # "workingType": "" + # }, + # ] + # } + # } + # + if isinstance(response, str): + # broken api engine : order-ids are too long numbers(i.e. 1742930526912864656) + # and json.loadscan not handle them in JS, so we have to use .parseJson + # however, when order has an attached SL/TP, their value types need extra parsing + response = self.fix_stringified_json_members(response) + response = self.parse_json(response) + data = self.safe_dict(response, 'data', {}) + result = self.safe_list(data, 'orders', []) + return self.parse_orders(result, market) + + def parse_order_side(self, side): + sides: dict = { + 'BUY': 'buy', + 'SELL': 'sell', + 'SHORT': 'sell', + 'LONG': 'buy', + 'ask': 'sell', + 'bid': 'buy', + } + return self.safe_string(sides, side, side) + + def parse_order_type(self, type: Str): + types: dict = { + 'trigger_market': 'market', + 'trigger_limit': 'limit', + 'stop_limit': 'limit', + 'stop_market': 'market', + 'take_profit_market': 'market', + 'stop': 'limit', + } + return self.safe_string(types, type, type) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # spot + # createOrder, createOrders, cancelOrder + # + # { + # "symbol": "XRP-USDT", + # "orderId": 1514090846268424192, + # "transactTime": 1649822362855, + # "price": "0.5", + # "origQty": "10", + # "executedQty": "0", + # "cummulativeQuoteQty": "0", + # "status": "PENDING", + # "type": "LIMIT", + # "side": "BUY" + # } + # + # fetchOrder + # + # { + # "symbol": "ETH-USDT", + # "orderId": "1660602123001266176", + # "price": "1700", + # "origQty": "0.003", + # "executedQty": "0", + # "cummulativeQuoteQty": "0", + # "status": "PENDING", + # "type": "LIMIT", + # "side": "BUY", + # "time": "1684753373276", + # "updateTime": "1684753373276", + # "origQuoteOrderQty": "0", + # "fee": "0", + # "feeAsset": "ETH" + # } + # + # fetchOpenOrders, fetchClosedOrders + # + # { + # "symbol": "XRP-USDT", + # "orderId": 1514073325788200960, + # "price": "0.5", + # "StopPrice": "0", + # "origQty": "20", + # "executedQty": "10", + # "cummulativeQuoteQty": "5", + # "status": "PENDING", + # "type": "LIMIT", + # "side": "BUY", + # "time": 1649818185647, + # "updateTime": 1649818185647, + # "origQuoteOrderQty": "0" + # "fee": "-0.01" + # } + # + # + # linear swap + # createOrder, createOrders + # + # { + # "symbol": "BTC-USDT", + # "orderId": 1590973236294713344, + # "side": "BUY", + # "positionSide": "LONG", + # "type": "LIMIT" + # } + # + # inverse swap createOrder + # + # { + # "orderId": 1809841379603398656, + # "symbol": "SOL-USD", + # "positionSide": "LONG", + # "side": "BUY", + # "type": "LIMIT", + # "price": 100, + # "quantity": 1, + # "stopPrice": 0, + # "workingType": "", + # "timeInForce": "" + # } + # + # fetchOrder, fetchOpenOrders, fetchClosedOrders + # + # { + # "symbol": "BTC-USDT", + # "orderId": 1709036527545438208, + # "side": "BUY", + # "positionSide": "LONG", + # "type": "TRIGGER_LIMIT", + # "origQty": "0.0010", + # "price": "22000.0", + # "executedQty": "0.0000", + # "avgPrice": "0.0", + # "cumQuote": "", + # "stopPrice": "23000.0", + # "profit": "", + # "commission": "", + # "status": "NEW", + # "time": 1696301035187, + # "updateTime": 1696301035187, + # "clientOrderId": "", + # "leverage": "", + # "takeProfit": "", + # "stopLoss": "", + # "advanceAttr": 0, + # "positionID": 0, + # "takeProfitEntrustPrice": 0, + # "stopLossEntrustPrice": 0, + # "orderType": "", + # "workingType": "MARK_PRICE" + # } + # with tp and sl + # { + # orderId: 1741440894764281900, + # symbol: 'LTC-USDT', + # positionSide: 'LONG', + # side: 'BUY', + # type: 'MARKET', + # price: 0, + # quantity: 1, + # stopPrice: 0, + # workingType: 'MARK_PRICE', + # clientOrderID: '', + # timeInForce: 'GTC', + # priceRate: 0, + # stopLoss: '{"stopPrice":50,"workingType":"MARK_PRICE","type":"STOP_MARKET","quantity":1}', + # takeProfit: '{"stopPrice":150,"workingType":"MARK_PRICE","type":"TAKE_PROFIT_MARKET","quantity":1}', + # reduceOnly: False + # } + # + # editOrder(swap) + # + # { + # cancelResult: 'true', + # cancelMsg: '', + # cancelResponse: { + # cancelClientOrderId: '', + # cancelOrderId: '1755336244265705472', + # symbol: 'SOL-USDT', + # orderId: '1755336244265705472', + # side: 'SELL', + # positionSide: 'SHORT', + # type: 'LIMIT', + # origQty: '1', + # price: '100.000', + # executedQty: '0', + # avgPrice: '0.000', + # cumQuote: '0', + # stopPrice: '', + # profit: '0.0000', + # commission: '0.000000', + # status: 'PENDING', + # time: '1707339747860', + # updateTime: '1707339747860', + # clientOrderId: '', + # leverage: '20X', + # workingType: 'MARK_PRICE', + # onlyOnePosition: False, + # reduceOnly: False + # }, + # replaceResult: 'true', + # replaceMsg: '', + # newOrderResponse: { + # orderId: '1755338440612995072', + # symbol: 'SOL-USDT', + # positionSide: 'SHORT', + # side: 'SELL', + # type: 'LIMIT', + # price: '99', + # quantity: '2', + # stopPrice: '0', + # workingType: 'MARK_PRICE', + # clientOrderID: '', + # timeInForce: 'GTC', + # priceRate: '0', + # stopLoss: '', + # takeProfit: '', + # reduceOnly: False + # } + # } + # + # editOrder(spot) + # + # { + # cancelResult: {code: '0', msg: '', result: True}, + # openResult: {code: '0', msg: '', result: True}, + # orderOpenResponse: { + # symbol: 'SOL-USDT', + # orderId: '1755334007697866752', + # transactTime: '1707339214620', + # price: '99', + # stopPrice: '0', + # origQty: '0.2', + # executedQty: '0', + # cummulativeQuoteQty: '0', + # status: 'PENDING', + # type: 'LIMIT', + # side: 'SELL', + # clientOrderID: '' + # }, + # orderCancelResponse: { + # symbol: 'SOL-USDT', + # orderId: '1755117055251480576', + # price: '100', + # stopPrice: '0', + # origQty: '0.2', + # executedQty: '0', + # cummulativeQuoteQty: '0', + # status: 'CANCELED', + # type: 'LIMIT', + # side: 'SELL' + # } + # } + # + # stop loss order + # + # { + # "symbol": "ETH-USDT", + # "orderId": "1792461744476422144", + # "price": "2775.65", + # "StopPrice": "2778.42", + # "origQty": "0.032359", + # "executedQty": "0", + # "cummulativeQuoteQty": "0", + # "status": "NEW", + # "type": "TAKE_STOP_LIMIT", + # "side": "SELL", + # "time": "1716191156868", + # "updateTime": "1716191156868", + # "origQuoteOrderQty": "0", + # "fee": "0", + # "feeAsset": "USDT", + # "clientOrderID": "" + # } + # + # inverse swap cancelAllOrders, cancelOrder, fetchOrder, fetchOpenOrders, fetchClosedOrders, fetchCanceledOrders + # + # { + # "symbol": "SOL-USD", + # "orderId": "1809845251327672320", + # "side": "BUY", + # "positionSide": "LONG", + # "type": "LIMIT", + # "quantity": 1, + # "origQty": "0", + # "price": "90", + # "executedQty": "0", + # "avgPrice": "0", + # "cumQuote": "0", + # "stopPrice": "", + # "profit": "0.0000", + # "commission": "0.000000", + # "status": "CANCELLED", + # "time": 1720335707872, + # "updateTime": 1720335707912, + # "clientOrderId": "", + # "leverage": "", + # "takeProfit": { + # "type": "", + # "quantity": 0, + # "stopPrice": 0, + # "price": 0, + # "workingType": "", + # "stopGuaranteed": "" + # }, + # "stopLoss": { + # "type": "", + # "quantity": 0, + # "stopPrice": 0, + # "price": 0, + # "workingType": "", + # "stopGuaranteed": "" + # }, + # "advanceAttr": 0, + # "positionID": 0, + # "takeProfitEntrustPrice": 0, + # "stopLossEntrustPrice": 0, + # "orderType": "", + # "workingType": "" + # } + # + info = order + newOrder = self.safe_dict_2(order, 'newOrderResponse', 'orderOpenResponse') + if newOrder is not None: + order = newOrder + positionSide = self.safe_string_2(order, 'positionSide', 'ps') + marketType = 'spot' if (positionSide is None) else 'swap' + marketId = self.safe_string_2(order, 'symbol', 's') + if market is None: + market = self.safe_market(marketId, None, None, marketType) + side = self.safe_string_lower_2(order, 'side', 'S') + timestamp = self.safe_integer_n(order, ['time', 'transactTime', 'E', 'createdTime']) + lastTradeTimestamp = self.safe_integer_2(order, 'updateTime', 'T') + statusId = self.safe_string_upper_n(order, ['status', 'X', 'orderStatus']) + feeCurrencyCode = self.safe_string_2(order, 'feeAsset', 'N') + feeCost = self.safe_string_n(order, ['fee', 'commission', 'n']) + if (feeCurrencyCode is None): + if market['spot']: + if side == 'buy': + feeCurrencyCode = market['base'] + else: + feeCurrencyCode = market['quote'] + else: + feeCurrencyCode = market['quote'] + stopLoss = self.safe_value(order, 'stopLoss') + stopLossPrice = None + if (stopLoss is not None) and (stopLoss != ''): + stopLossPrice = self.omit_zero(self.safe_string(stopLoss, 'stopLoss')) + if (stopLoss is not None) and ((not isinstance(stopLoss, numbers.Real))) and (stopLoss != ''): + # stopLoss: '{"stopPrice":50,"workingType":"MARK_PRICE","type":"STOP_MARKET","quantity":1}', + if isinstance(stopLoss, str): + stopLoss = self.parse_json(stopLoss) + stopLossPrice = self.omit_zero(self.safe_string(stopLoss, 'stopPrice')) + takeProfit = self.safe_value(order, 'takeProfit') + takeProfitPrice = None + if takeProfit is not None and (takeProfit != ''): + takeProfitPrice = self.omit_zero(self.safe_string(takeProfit, 'takeProfit')) + if (takeProfit is not None) and ((not isinstance(takeProfit, numbers.Real))) and (takeProfit != ''): + # takeProfit: '{"stopPrice":150,"workingType":"MARK_PRICE","type":"TAKE_PROFIT_MARKET","quantity":1}', + if isinstance(takeProfit, str): + takeProfit = self.parse_json(takeProfit) + takeProfitPrice = self.omit_zero(self.safe_string(takeProfit, 'stopPrice')) + rawType = self.safe_string_lower_2(order, 'type', 'o') + stopPrice = self.omit_zero(self.safe_string_2(order, 'StopPrice', 'stopPrice')) + triggerPrice = stopPrice + if stopPrice is not None: + if (rawType.find('stop') > -1) and (stopLossPrice is None): + stopLossPrice = stopPrice + triggerPrice = None + if (rawType.find('take') > -1) and (takeProfitPrice is None): + takeProfitPrice = stopPrice + triggerPrice = None + return self.safe_order({ + 'info': info, + 'id': self.safe_string_n(order, ['orderId', 'i', 'mainOrderId']), + 'clientOrderId': self.safe_string_n(order, ['clientOrderID', 'clientOrderId', 'origClientOrderId', 'c']), + 'symbol': self.safe_symbol(marketId, market, '-', marketType), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': lastTradeTimestamp, + 'lastUpdateTimestamp': self.safe_integer(order, 'updateTime'), + 'type': self.parse_order_type(rawType), + 'timeInForce': self.safe_string(order, 'timeInForce'), + 'postOnly': None, + 'side': self.parse_order_side(side), + 'price': self.safe_string_2(order, 'price', 'p'), + 'triggerPrice': triggerPrice, + 'stopLossPrice': stopLossPrice, + 'takeProfitPrice': takeProfitPrice, + 'average': self.safe_string_2(order, 'avgPrice', 'ap'), + 'cost': self.safe_string(order, 'cummulativeQuoteQty'), + 'amount': self.safe_string_n(order, ['origQty', 'q', 'quantity', 'totalAmount']), + 'filled': self.safe_string_2(order, 'executedQty', 'z'), + 'remaining': None, + 'status': self.parse_order_status(statusId), + 'fee': { + 'currency': feeCurrencyCode, + 'cost': Precise.string_abs(feeCost), + }, + 'trades': None, + 'reduceOnly': self.safe_bool_2(order, 'reduceOnly', 'ro'), + }, market) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'NEW': 'open', + 'PENDING': 'open', + 'PARTIALLY_FILLED': 'open', + 'RUNNING': 'open', + 'FILLED': 'closed', + 'CANCELED': 'canceled', + 'CANCELLED': 'canceled', + 'FAILED': 'canceled', + } + return self.safe_string(statuses, status, status) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://bingx-api.github.io/docs/#/en-us/spot/trade-api.html#Cancel%20Order + https://bingx-api.github.io/docs/#/en-us/swapV2/trade-api.html#Cancel%20Order + https://bingx-api.github.io/docs/#/en-us/cswap/trade-api.html#Cancel%20an%20Order + https://bingx-api.github.io/docs/#/en-us/swapV2/trade-api.html#Cancel%20TWAP%20Order + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.clientOrderId]: a unique id for the order + :returns dict: an `order structure ` + """ + self.load_markets() + isTwapOrder = self.safe_bool(params, 'twap', False) + params = self.omit(params, 'twap') + response = None + market = None + if isTwapOrder: + twapRequest: dict = { + 'mainOrderId': id, + } + response = self.swapV1PrivatePostTwapCancelOrder(self.extend(twapRequest, params)) + # + # { + # "code": 0, + # "msg": "", + # "timestamp": 1702731661854, + # "data": { + # "symbol": "BNB-USDT", + # "side": "BUY", + # "positionSide": "LONG", + # "priceType": "constant", + # "priceVariance": "2000", + # "triggerPrice": "68000", + # "interval": 8, + # "amountPerOrder": "0.111", + # "totalAmount": "0.511", + # "orderStatus": "Running", + # "executedQty": "0.1", + # "duration": 800, + # "maxDuration": 9000, + # "createdTime": 1702731661854, + # "updateTime": 1702731661854 + # } + # } + # + else: + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + clientOrderId = self.safe_string_2(params, 'clientOrderId', 'clientOrderID') + params = self.omit(params, ['clientOrderId']) + if clientOrderId is not None: + request['clientOrderID'] = clientOrderId + else: + request['orderId'] = id + type = None + subType = None + type, params = self.handle_market_type_and_params('cancelOrder', market, params) + subType, params = self.handle_sub_type_and_params('cancelOrder', market, params) + if type == 'spot': + response = self.spotV1PrivatePostTradeCancel(self.extend(request, params)) + else: + if subType == 'inverse': + response = self.cswapV1PrivateDeleteTradeCancelOrder(self.extend(request, params)) + else: + response = self.swapV2PrivateDeleteTradeOrder(self.extend(request, params)) + # + # spot + # + # { + # "code": 0, + # "msg": "", + # "data": { + # "symbol": "XRP-USDT", + # "orderId": 1514090846268424192, + # "price": "0.5", + # "origQty": "10", + # "executedQty": "0", + # "cummulativeQuoteQty": "0", + # "status": "CANCELED", + # "type": "LIMIT", + # "side": "BUY" + # } + # } + # + # inverse swap + # + # { + # "code": 0, + # "msg": "", + # "data": { + # "order": { + # "symbol": "SOL-USD", + # "orderId": "1816002957423951872", + # "side": "BUY", + # "positionSide": "Long", + # "type": "Pending", + # "quantity": 0, + # "origQty": "0", + # "price": "150", + # "executedQty": "0", + # "avgPrice": "0", + # "cumQuote": "0", + # "stopPrice": "", + # "profit": "0.0000", + # "commission": "0.000000", + # "status": "CANCELLED", + # "time": 1721803819410, + # "updateTime": 1721803819427, + # "clientOrderId": "", + # "leverage": "", + # "takeProfit": { + # "type": "", + # "quantity": 0, + # "stopPrice": 0, + # "price": 0, + # "workingType": "", + # "stopGuaranteed": "" + # }, + # "stopLoss": { + # "type": "", + # "quantity": 0, + # "stopPrice": 0, + # "price": 0, + # "workingType": "", + # "stopGuaranteed": "" + # }, + # "advanceAttr": 0, + # "positionID": 0, + # "takeProfitEntrustPrice": 0, + # "stopLossEntrustPrice": 0, + # "orderType": "", + # "workingType": "" + # } + # } + # } + # + # linear swap + # + # { + # "code": 0, + # "msg": "", + # "data": { + # "order": { + # "symbol": "LINK-USDT", + # "orderId": 1597783850786750464, + # "side": "BUY", + # "positionSide": "LONG", + # "type": "TRIGGER_MARKET", + # "origQty": "5.0", + # "price": "5.0000", + # "executedQty": "0.0", + # "avgPrice": "0.0000", + # "cumQuote": "0", + # "stopPrice": "5.0000", + # "profit": "", + # "commission": "", + # "status": "CANCELLED", + # "time": 1669776330000, + # "updateTime": 1669776330000 + # } + # } + # } + # + data = self.safe_dict(response, 'data', {}) + order = self.safe_dict(data, 'order', data) + return self.parse_order(order, market) + + def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders + + https://bingx-api.github.io/docs/#/en-us/spot/trade-api.html#Cancel%20orders%20by%20symbol + https://bingx-api.github.io/docs/#/swapV2/trade-api.html#Cancel%20All%20Orders + https://bingx-api.github.io/docs/#/en-us/cswap/trade-api.html#Cancel%20all%20orders + + :param str [symbol]: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelAllOrders() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = None + if market['spot']: + response = self.spotV1PrivatePostTradeCancelOpenOrders(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "debugMsg": "", + # "data": { + # "orders": [{ + # "symbol": "ADA-USDT", + # "orderId": 1740659971369992192, + # "transactTime": 1703840651730, + # "price": 5, + # "stopPrice": 0, + # "origQty": 10, + # "executedQty": 0, + # "cummulativeQuoteQty": 0, + # "status": "CANCELED", + # "type": "LIMIT", + # "side": "SELL" + # }] + # } + # } + # + elif market['swap']: + if market['inverse']: + response = self.cswapV1PrivateDeleteTradeAllOpenOrders(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "timestamp": 1720501468364, + # "data": { + # "success": [ + # { + # "symbol": "SOL-USD", + # "orderId": "1809845251327672320", + # "side": "BUY", + # "positionSide": "LONG", + # "type": "LIMIT", + # "quantity": 1, + # "origQty": "0", + # "price": "90", + # "executedQty": "0", + # "avgPrice": "0", + # "cumQuote": "0", + # "stopPrice": "", + # "profit": "0.0000", + # "commission": "0.000000", + # "status": "CANCELLED", + # "time": 1720335707872, + # "updateTime": 1720335707912, + # "clientOrderId": "", + # "leverage": "", + # "takeProfit": { + # "type": "", + # "quantity": 0, + # "stopPrice": 0, + # "price": 0, + # "workingType": "", + # "stopGuaranteed": "" + # }, + # "stopLoss": { + # "type": "", + # "quantity": 0, + # "stopPrice": 0, + # "price": 0, + # "workingType": "", + # "stopGuaranteed": "" + # }, + # "advanceAttr": 0, + # "positionID": 0, + # "takeProfitEntrustPrice": 0, + # "stopLossEntrustPrice": 0, + # "orderType": "", + # "workingType": "" + # } + # ], + # "failed": null + # } + # } + # + else: + response = self.swapV2PrivateDeleteTradeAllOpenOrders(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "data": { + # "success": [ + # { + # "symbol": "LINK-USDT", + # "orderId": 1597783835095859200, + # "side": "BUY", + # "positionSide": "LONG", + # "type": "TRIGGER_LIMIT", + # "origQty": "5.0", + # "price": "9.0000", + # "executedQty": "0.0", + # "avgPrice": "0.0000", + # "cumQuote": "0", + # "stopPrice": "9.5000", + # "profit": "", + # "commission": "", + # "status": "NEW", + # "time": 1669776326000, + # "updateTime": 1669776326000 + # } + # ], + # "failed": null + # } + # } + # + else: + raise BadRequest(self.id + ' cancelAllOrders is only supported for spot and swap markets.') + data = self.safe_dict(response, 'data', {}) + orders = self.safe_list_2(data, 'success', 'orders', []) + return self.parse_orders(orders) + + def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): + """ + cancel multiple orders + + https://bingx-api.github.io/docs/#/swapV2/trade-api.html#Cancel%20a%20Batch%20of%20Orders + https://bingx-api.github.io/docs/#/spot/trade-api.html#Cancel%20a%20Batch%20of%20Orders + + :param str[] ids: order ids + :param str symbol: unified market symbol, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str[] [params.clientOrderIds]: client order ids + :returns dict: an list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrders() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + clientOrderIds = self.safe_value(params, 'clientOrderIds') + params = self.omit(params, 'clientOrderIds') + idsToParse = ids + areClientOrderIds = (clientOrderIds is not None) + if areClientOrderIds: + idsToParse = clientOrderIds + parsedIds = [] + for i in range(0, len(idsToParse)): + id = idsToParse[i] + stringId = str(id) + parsedIds.append(stringId) + response = None + if market['spot']: + spotReqKey = 'clientOrderIDs' if areClientOrderIds else 'orderIds' + request[spotReqKey] = ','.join(parsedIds) + response = self.spotV1PrivatePostTradeCancelOrders(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "debugMsg": "", + # "data": { + # "orders": [ + # { + # "symbol": "SOL-USDT", + # "orderId": 1795970045910614016, + # "transactTime": 1717027601111, + # "price": "180.25", + # "stopPrice": "0", + # "origQty": "0.03", + # "executedQty": "0", + # "cummulativeQuoteQty": "0", + # "status": "CANCELED", + # "type": "LIMIT", + # "side": "SELL", + # "clientOrderID": "" + # }, + # ... + # ] + # } + # } + # + else: + if areClientOrderIds: + request['clientOrderIDList'] = self.json(parsedIds) + else: + request['orderIdList'] = parsedIds + response = self.swapV2PrivateDeleteTradeBatchOrders(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "data": { + # "success": [ + # { + # "symbol": "LINK-USDT", + # "orderId": 1597783850786750464, + # "side": "BUY", + # "positionSide": "LONG", + # "type": "TRIGGER_MARKET", + # "origQty": "5.0", + # "price": "5.5710", + # "executedQty": "0.0", + # "avgPrice": "0.0000", + # "cumQuote": "0", + # "stopPrice": "5.0000", + # "profit": "0.0000", + # "commission": "0.000000", + # "status": "CANCELLED", + # "time": 1669776330000, + # "updateTime": 1672370837000 + # } + # ], + # "failed": null + # } + # } + # + data = self.safe_dict(response, 'data', {}) + success = self.safe_list_2(data, 'success', 'orders', []) + return self.parse_orders(success) + + def cancel_all_orders_after(self, timeout: Int, params={}): + """ + dead man's switch, cancel all orders after the given timeout + + https://bingx-api.github.io/docs/#/en-us/spot/trade-api.html#Cancel%20all%20orders%20in%20countdown + https://bingx-api.github.io/docs/#/en-us/swapV2/trade-api.html#Cancel%20all%20orders%20in%20countdown + + :param number timeout: time in milliseconds, 0 represents cancel the timer + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: spot or swap market + :returns dict: the api result + """ + self.load_markets() + isActive = (timeout > 0) + request: dict = { + 'type': 'ACTIVATE' if (isActive) else 'CLOSE', + 'timeOut': (self.parse_to_int(timeout / 1000)) if (isActive) else 0, + } + response = None + type = None + type, params = self.handle_market_type_and_params('cancelAllOrdersAfter', None, params) + if type == 'spot': + response = self.spotV1PrivatePostTradeCancelAllAfter(self.extend(request, params)) + elif type == 'swap': + response = self.swapV2PrivatePostTradeCancelAllAfter(self.extend(request, params)) + else: + raise NotSupported(self.id + ' cancelAllOrdersAfter() is not supported for ' + type + ' markets') + # + # { + # code: '0', + # msg: '', + # data: { + # triggerTime: '1712645434', + # status: 'ACTIVATED', + # note: 'All your perpetual pending orders will be closed automatically at 2024-04-09 06:50:34 UTC(+0),before that you can cancel the timer, or self.extend triggerTime time by self request' + # } + # } + # + return response + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://bingx-api.github.io/docs/#/en-us/spot/trade-api.html#Query%20Order%20details + https://bingx-api.github.io/docs/#/en-us/swapV2/trade-api.html#Query%20Order%20details + https://bingx-api.github.io/docs/#/en-us/cswap/trade-api.html#Query%20Order + https://bingx-api.github.io/docs/#/en-us/swapV2/trade-api.html#TWAP%20Order%20Details + + :param str id: the order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.twap]: if fetching twap order + :returns dict: an `order structure ` + """ + self.load_markets() + isTwapOrder = self.safe_bool(params, 'twap', False) + params = self.omit(params, 'twap') + response = None + market = None + if isTwapOrder: + twapRequest: dict = { + 'mainOrderId': id, + } + response = self.swapV1PrivateGetTwapOrderDetail(self.extend(twapRequest, params)) + # + # { + # "code": 0, + # "msg": "success cancel order", + # "timestamp": 1732760856617, + # "data": { + # "symbol": "LTC-USDT", + # "mainOrderId": "5596903086063901779", + # "side": "BUY", + # "positionSide": "LONG", + # "priceType": "constant", + # "priceVariance": "10.00", + # "triggerPrice": "120.00", + # "interval": 8, + # "amountPerOrder": "0.5", + # "totalAmount": "1.0", + # "orderStatus": "Filled", + # "executedQty": "1.0", + # "duration": 16, + # "maxDuration": 86400, + # "createdTime": 1732693017000, + # "updateTime": 1732693033000 + # } + # } + # + else: + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrder() requires a symbol argument') + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'orderId': id, + } + type = None + subType = None + type, params = self.handle_market_type_and_params('fetchOrder', market, params) + subType, params = self.handle_sub_type_and_params('fetchOrder', market, params) + if type == 'spot': + response = self.spotV1PrivateGetTradeQuery(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "data": { + # "symbol": "XRP-USDT", + # "orderId": 1514087361158316032, + # "price": "0.5", + # "origQty": "10", + # "executedQty": "0", + # "cummulativeQuoteQty": "0", + # "status": "CANCELED", + # "type": "LIMIT", + # "side": "BUY", + # "time": 1649821532000, + # "updateTime": 1649821543000, + # "origQuoteOrderQty": "0", + # "fee": "0", + # "feeAsset": "XRP" + # } + # } + # + else: + if subType == 'inverse': + response = self.cswapV1PrivateGetTradeOrderDetail(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "data": { + # "order": { + # "symbol": "SOL-USD", + # "orderId": "1816342420721254400", + # "side": "BUY", + # "positionSide": "Long", + # "type": "LIMIT", + # "quantity": 1, + # "origQty": "", + # "price": "150", + # "executedQty": "0", + # "avgPrice": "0.000", + # "cumQuote": "", + # "stopPrice": "", + # "profit": "0.0000", + # "commission": "0.0000", + # "status": "Pending", + # "time": 1721884753767, + # "updateTime": 1721884753786, + # "clientOrderId": "", + # "leverage": "", + # "takeProfit": { + # "type": "TAKE_PROFIT", + # "quantity": 0, + # "stopPrice": 0, + # "price": 0, + # "workingType": "MARK_PRICE", + # "stopGuaranteed": "" + # }, + # "stopLoss": { + # "type": "STOP", + # "quantity": 0, + # "stopPrice": 0, + # "price": 0, + # "workingType": "MARK_PRICE", + # "stopGuaranteed": "" + # }, + # "advanceAttr": 0, + # "positionID": 0, + # "takeProfitEntrustPrice": 0, + # "stopLossEntrustPrice": 0, + # "orderType": "", + # "workingType": "MARK_PRICE" + # } + # } + # } + # + else: + response = self.swapV2PrivateGetTradeOrder(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "data": { + # "order": { + # "symbol": "BTC-USDT", + # "orderId": 1597597642269917184, + # "side": "SELL", + # "positionSide": "LONG", + # "type": "TAKE_PROFIT_MARKET", + # "origQty": "1.0000", + # "price": "0.0", + # "executedQty": "0.0000", + # "avgPrice": "0.0", + # "cumQuote": "", + # "stopPrice": "16494.0", + # "profit": "", + # "commission": "", + # "status": "FILLED", + # "time": 1669731935000, + # "updateTime": 1669752524000 + # } + # } + # } + # + data = self.safe_dict(response, 'data', {}) + order = self.safe_dict(data, 'order', data) + return self.parse_order(order, market) + + def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://bingx-api.github.io/docs/#/en-us/swapV2/trade-api.html#All%20Orders + https://bingx-api.github.io/docs/#/en-us/swapV2/trade-api.html#Query%20Order%20history(returns less fields than above) + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch entries for + :param int [params.orderId]: Only return subsequent orders, and return the latest order by default + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + type = None + type, params = self.handle_market_type_and_params('fetchOrders', market, params) + if type != 'swap': + raise NotSupported(self.id + ' fetchOrders() is only supported for swap markets') + if limit is not None: + request['limit'] = limit + if since is not None: + request['startTime'] = since + request, params = self.handle_until_option('endTime', request, params) + response = self.swapV1PrivateGetTradeFullOrder(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "data": { + # "orders": [ + # { + # "symbol": "PYTH-USDT", + # "orderId": 1736007506620112100, + # "side": "SELL", + # "positionSide": "SHORT", + # "type": "LIMIT", + # "origQty": "33", + # "price": "0.3916", + # "executedQty": "33", + # "avgPrice": "0.3916", + # "cumQuote": "13", + # "stopPrice": "", + # "profit": "0.0000", + # "commission": "-0.002585", + # "status": "FILLED", + # "time": 1702731418000, + # "updateTime": 1702731470000, + # "clientOrderId": "", + # "leverage": "15X", + # "takeProfit": { + # "type": "TAKE_PROFIT", + # "quantity": 0, + # "stopPrice": 0, + # "price": 0, + # "workingType": "" + # }, + # "stopLoss": { + # "type": "STOP", + # "quantity": 0, + # "stopPrice": 0, + # "price": 0, + # "workingType": "" + # }, + # "advanceAttr": 0, + # "positionID": 0, + # "takeProfitEntrustPrice": 0, + # "stopLossEntrustPrice": 0, + # "orderType": "", + # "workingType": "MARK_PRICE", + # "stopGuaranteed": False, + # "triggerOrderId": 1736012449498123500 + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + orders = self.safe_list(data, 'orders', []) + return self.parse_orders(orders, market, since, limit) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://bingx-api.github.io/docs/#/en-us/spot/trade-api.html#Current%20Open%20Orders + https://bingx-api.github.io/docs/#/en-us/swapV2/trade-api.html#Current%20All%20Open%20Orders + https://bingx-api.github.io/docs/#/en-us/cswap/trade-api.html#Query%20all%20current%20pending%20orders + https://bingx-api.github.io/docs/#/en-us/swapV2/trade-api.html#Query%20TWAP%20Entrusted%20Order + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.twap]: if fetching twap open orders + :returns dict[]: a list of `order structures ` + """ + self.load_markets() + market = None + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + type = None + subType = None + response = None + type, params = self.handle_market_type_and_params('fetchOpenOrders', market, params) + subType, params = self.handle_sub_type_and_params('fetchOpenOrders', market, params) + if type == 'spot': + response = self.spotV1PrivateGetTradeOpenOrders(self.extend(request, params)) + else: + isTwapOrder = self.safe_bool(params, 'twap', False) + params = self.omit(params, 'twap') + if isTwapOrder: + response = self.swapV1PrivateGetTwapOpenOrders(self.extend(request, params)) + elif subType == 'inverse': + response = self.cswapV1PrivateGetTradeOpenOrders(self.extend(request, params)) + else: + response = self.swapV2PrivateGetTradeOpenOrders(self.extend(request, params)) + # + # spot + # + # { + # "code": 0, + # "msg": "", + # "data": { + # "orders": [ + # { + # "symbol": "XRP-USDT", + # "orderId": 1514073325788200960, + # "price": "0.5", + # "origQty": "20", + # "executedQty": "0", + # "cummulativeQuoteQty": "0", + # "status": "PENDING", + # "type": "LIMIT", + # "side": "BUY", + # "time": 1649818185647, + # "updateTime": 1649818185647, + # "origQuoteOrderQty": "0" + # } + # ] + # } + # } + # + # inverse swap + # + # { + # "code": 0, + # "msg": "", + # "data": { + # "orders": [ + # { + # "symbol": "SOL-USD", + # "orderId": "1816013900044320768", + # "side": "BUY", + # "positionSide": "Long", + # "type": "LIMIT", + # "quantity": 1, + # "origQty": "", + # "price": "150", + # "executedQty": "0", + # "avgPrice": "0.000", + # "cumQuote": "", + # "stopPrice": "", + # "profit": "0.0000", + # "commission": "0.0000", + # "status": "Pending", + # "time": 1721806428334, + # "updateTime": 1721806428352, + # "clientOrderId": "", + # "leverage": "", + # "takeProfit": { + # "type": "TAKE_PROFIT", + # "quantity": 0, + # "stopPrice": 0, + # "price": 0, + # "workingType": "MARK_PRICE", + # "stopGuaranteed": "" + # }, + # "stopLoss": { + # "type": "STOP", + # "quantity": 0, + # "stopPrice": 0, + # "price": 0, + # "workingType": "MARK_PRICE", + # "stopGuaranteed": "" + # }, + # "advanceAttr": 0, + # "positionID": 0, + # "takeProfitEntrustPrice": 0, + # "stopLossEntrustPrice": 0, + # "orderType": "", + # "workingType": "MARK_PRICE" + # } + # ] + # } + # } + # + # linear swap + # + # { + # "code": 0, + # "msg": "", + # "data": { + # "orders": [ + # { + # "symbol": "LINK-USDT", + # "orderId": 1585839271162413056, + # "side": "BUY", + # "positionSide": "LONG", + # "type": "TRIGGER_MARKET", + # "origQty": "5.0", + # "price": "9", + # "executedQty": "0.0", + # "avgPrice": "0", + # "cumQuote": "0", + # "stopPrice": "5", + # "profit": "0.0000", + # "commission": "0.000000", + # "status": "CANCELLED", + # "time": 1667631605000, + # "updateTime": 1667631605000 + # }, + # ] + # } + # } + # + # twap + # + # { + # "code": 0, + # "msg": "", + # "timestamp": 1702731661854, + # "data": { + # "list": [ + # { + # "symbol": "BNB-USDT", + # "side": "BUY", + # "positionSide": "LONG", + # "priceType": "constant", + # "priceVariance": "2000", + # "triggerPrice": "68000", + # "interval": 8, + # "amountPerOrder": "0.111", + # "totalAmount": "0.511", + # "orderStatus": "Running", + # "executedQty": "0.1", + # "duration": 800, + # "maxDuration": 9000, + # "createdTime": 1702731661854, + # "updateTime": 1702731661854 + # } + # ], + # "total": 1 + # } + # } + # + data = self.safe_dict(response, 'data', {}) + orders = self.safe_list_2(data, 'orders', 'list', []) + return self.parse_orders(orders, market, since, limit) + + def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://bingx-api.github.io/docs/#/en-us/spot/trade-api.html#Query%20Order%20history + https://bingx-api.github.io/docs/#/en-us/swapV2/trade-api.html#Query%20Order%20history + https://bingx-api.github.io/docs/#/en-us/cswap/trade-api.html#User's%20History%20Orders + https://bingx-api.github.io/docs/#/standard/contract-interface.html#Historical%20order + + :param str symbol: unified market symbol of the closed orders + :param int [since]: timestamp in ms of the earliest order + :param int [limit]: the max number of closed orders to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch orders for + :param boolean [params.standard]: whether to fetch standard contract orders + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + orders = self.fetch_canceled_and_closed_orders(symbol, since, limit, params) + return self.filter_by(orders, 'status', 'closed') + + def fetch_canceled_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches information on multiple canceled orders made by the user + + https://bingx-api.github.io/docs/#/en-us/spot/trade-api.html#Query%20Order%20history + https://bingx-api.github.io/docs/#/en-us/swapV2/trade-api.html#Query%20Order%20history + https://bingx-api.github.io/docs/#/en-us/cswap/trade-api.html#User's%20History%20Orders + https://bingx-api.github.io/docs/#/standard/contract-interface.html#Historical%20order + + :param str symbol: unified market symbol of the canceled orders + :param int [since]: timestamp in ms of the earliest order + :param int [limit]: the max number of canceled orders to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch orders for + :param boolean [params.standard]: whether to fetch standard contract orders + :returns dict: a list of `order structures ` + """ + self.load_markets() + orders = self.fetch_canceled_and_closed_orders(symbol, since, limit, params) + return self.filter_by(orders, 'status', 'canceled') + + def fetch_canceled_and_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches information on multiple closed orders made by the user + + https://bingx-api.github.io/docs/#/en-us/spot/trade-api.html#Query%20Order%20history + https://bingx-api.github.io/docs/#/en-us/swapV2/trade-api.html#Query%20Order%20history + https://bingx-api.github.io/docs/#/en-us/cswap/trade-api.html#User's%20History%20Orders + https://bingx-api.github.io/docs/#/standard/contract-interface.html#Historical%20order + https://bingx-api.github.io/docs/#/en-us/swapV2/trade-api.html#Query%20TWAP%20Historical%20Orders + + :param str [symbol]: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch orders for + :param boolean [params.standard]: whether to fetch standard contract orders + :param boolean [params.twap]: if fetching twap orders + :returns dict[]: a list of `order structures ` + """ + self.load_markets() + market = None + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + type = None + subType = None + standard = None + response = None + type, params = self.handle_market_type_and_params('fetchClosedOrders', market, params) + subType, params = self.handle_sub_type_and_params('fetchClosedOrders', market, params) + standard, params = self.handle_option_and_params(params, 'fetchClosedOrders', 'standard', False) + if standard: + response = self.contractV1PrivateGetAllOrders(self.extend(request, params)) + elif type == 'spot': + if limit is not None: + request['pageSize'] = limit + response = self.spotV1PrivateGetTradeHistoryOrders(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "data": { + # "orders": [ + # { + # "symbol": "XRP-USDT", + # "orderId": 1514073325788200960, + # "price": "0.5", + # "origQty": "20", + # "executedQty": "0", + # "cummulativeQuoteQty": "0", + # "status": "PENDING", + # "type": "LIMIT", + # "side": "BUY", + # "time": 1649818185647, + # "updateTime": 1649818185647, + # "origQuoteOrderQty": "0" + # } + # ] + # } + # } + # + else: + isTwapOrder = self.safe_bool(params, 'twap', False) + params = self.omit(params, 'twap') + if isTwapOrder: + request['pageIndex'] = 1 + request['pageSize'] = 100 if (limit is None) else limit + request['startTime'] = 1 if (since is None) else since + until = self.safe_integer(params, 'until', self.milliseconds()) + params = self.omit(params, 'until') + request['endTime'] = until + response = self.swapV1PrivateGetTwapHistoryOrders(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "timestamp": 1702731661854, + # "data": { + # "list": [ + # { + # "symbol": "BNB-USDT", + # "side": "BUY", + # "positionSide": "LONG", + # "priceType": "constant", + # "priceVariance": "2000", + # "triggerPrice": "68000", + # "interval": 8, + # "amountPerOrder": "0.111", + # "totalAmount": "0.511", + # "orderStatus": "Running", + # "executedQty": "0.1", + # "duration": 800, + # "maxDuration": 9000, + # "createdTime": 1702731661854, + # "updateTime": 1702731661854 + # } + # ], + # "total": 1 + # } + # } + # + elif subType == 'inverse': + response = self.cswapV1PrivateGetTradeOrderHistory(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "data": { + # "orders": [ + # { + # "symbol": "SOL-USD", + # "orderId": "1816002957423951872", + # "side": "BUY", + # "positionSide": "LONG", + # "type": "LIMIT", + # "quantity": 1, + # "origQty": "10.00000000", + # "price": "150.000", + # "executedQty": "0.00000000", + # "avgPrice": "0.000", + # "cumQuote": "", + # "stopPrice": "0.000", + # "profit": "0.0000", + # "commission": "0.000000", + # "status": "Filled", + # "time": 1721803819000, + # "updateTime": 1721803856000, + # "clientOrderId": "", + # "leverage": "", + # "takeProfit": { + # "type": "", + # "quantity": 0, + # "stopPrice": 0, + # "price": 0, + # "workingType": "", + # "stopGuaranteed": "" + # }, + # "stopLoss": { + # "type": "", + # "quantity": 0, + # "stopPrice": 0, + # "price": 0, + # "workingType": "", + # "stopGuaranteed": "" + # }, + # "advanceAttr": 0, + # "positionID": 0, + # "takeProfitEntrustPrice": 0, + # "stopLossEntrustPrice": 0, + # "orderType": "", + # "workingType": "MARK_PRICE" + # }, + # ] + # } + # } + # + else: + response = self.swapV2PrivateGetTradeAllOrders(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "data": { + # "orders": [ + # { + # "symbol": "LINK-USDT", + # "orderId": 1585839271162413056, + # "side": "BUY", + # "positionSide": "LONG", + # "type": "TRIGGER_MARKET", + # "origQty": "5.0", + # "price": "9", + # "executedQty": "0.0", + # "avgPrice": "0", + # "cumQuote": "0", + # "stopPrice": "5", + # "profit": "0.0000", + # "commission": "0.000000", + # "status": "CANCELLED", + # "time": 1667631605000, + # "updateTime": 1667631605000 + # }, + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + orders = self.safe_list_2(data, 'orders', 'list', []) + return self.parse_orders(orders, market, since, limit) + + def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://bingx-api.github.io/docs/#/en-us/common/account-api.html#Asset%20Transfer%20New + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account to transfer from(spot, swap, futures, or funding) + :param str toAccount: account to transfer to(spot, swap(linear or inverse), future, or funding) + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transfer structure ` + """ + self.load_markets() + currency = self.currency(code) + accountsByType = self.safe_dict(self.options, 'accountsByType', {}) + subType = None + subType, params = self.handle_sub_type_and_params('transfer', None, params) + fromId = self.safe_string(accountsByType, fromAccount, fromAccount) + toId = self.safe_string(accountsByType, toAccount, toAccount) + if fromId == 'swap': + if subType == 'inverse': + fromId = 'coinMPerp' + else: + fromId = 'USDTMPerp' + if toId == 'swap': + if subType == 'inverse': + toId = 'coinMPerp' + else: + toId = 'USDTMPerp' + request: dict = { + 'fromAccount': fromId, + 'toAccount': toId, + 'asset': currency['id'], + 'amount': self.currency_to_precision(code, amount), + } + response = self.apiAssetV1PrivatePostTransfer(self.extend(request, params)) + # + # { + # "tranId": 1933130865269936128, + # "transferId": "1051450703949464903736" + # } + # + return { + 'info': response, + 'id': self.safe_string(response, 'transferId'), + 'timestamp': None, + 'datetime': None, + 'currency': code, + 'amount': amount, + 'fromAccount': fromAccount, + 'toAccount': toAccount, + 'status': None, + } + + def fetch_transfers(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[TransferEntry]: + """ + fetch a history of internal transfers made on an account + + https://bingx-api.github.io/docs/#/en-us/common/account-api.html#Asset%20transfer%20records%20new + + :param str [code]: unified currency code of the currency transferred + :param int [since]: the earliest time in ms to fetch transfers for + :param int [limit]: the maximum number of transfers structures to retrieve(default 10, max 100) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str params.fromAccount:(mandatory) transfer from(spot, swap(linear or inverse), future, or funding) + :param str params.toAccount:(mandatory) transfer to(spot, swap(linear or inverse), future, or funding) + :param boolean [params.paginate]: whether to paginate the results(default False) + :returns dict[]: a list of `transfer structures ` + """ + self.load_markets() + request: dict = {} + currency = None + if code is not None: + currency = self.currency(code) + accountsByType = self.safe_dict(self.options, 'accountsByType', {}) + fromAccount = self.safe_string(params, 'fromAccount') + toAccount = self.safe_string(params, 'toAccount') + fromId = self.safe_string(accountsByType, fromAccount, fromAccount) + toId = self.safe_string(accountsByType, toAccount, toAccount) + if fromId is None or toId is None: + raise ExchangeError(self.id + ' fromAccount & toAccount parameters are required') + if fromAccount is not None: + request['fromAccount'] = fromId + if toAccount is not None: + request['toAccount'] = toId + params = self.omit(params, ['fromAccount', 'toAccount']) + maxLimit = 100 + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchTransfers', 'paginate', False) + if paginate: + return self.fetch_paginated_call_dynamic('fetchTransfers', None, since, limit, params, maxLimit) + if since is not None: + request['startTime'] = since + if limit is not None: + request['pageSize'] = limit + request, params = self.handle_until_option('endTime', request, params) + response = self.apiV3PrivateGetAssetTransferRecord(self.extend(request, params)) + # + # { + # "total": 2, + # "rows": [ + # { + # "asset": "LTC", + # "amount": "0.05000000000000000000", + # "status": "CONFIRMED", + # "transferId": "1051461075661819338791", + # "timestamp": 1752202092000, + # "fromAccount": "spot", + # "toAccount": "USDTMPerp" + # } + # ] + # } + # + rows = self.safe_list(response, 'rows', []) + return self.parse_transfers(rows, currency, since, limit) + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + tranId = self.safe_string(transfer, 'transferId') + timestamp = self.safe_integer(transfer, 'timestamp') + currencyId = self.safe_string(transfer, 'asset') + currencyCode = self.safe_currency_code(currencyId, currency) + status = self.safe_string(transfer, 'status') + accountsById = self.safe_dict(self.options, 'accountsById', {}) + fromId = self.safe_string(transfer, 'fromAccount') + toId = self.safe_string(transfer, 'toAccount') + fromAccount = self.safe_string(accountsById, fromId, fromId) + toAccount = self.safe_string(accountsById, toId, toId) + return { + 'info': transfer, + 'id': tranId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'currency': currencyCode, + 'amount': self.safe_number(transfer, 'amount'), + 'fromAccount': fromAccount, + 'toAccount': toAccount, + 'status': self.parse_transfer_status(status), + } + + def parse_transfer_status(self, status: Str) -> str: + statuses: dict = { + 'CONFIRMED': 'ok', + } + return self.safe_string(statuses, status, status) + + def fetch_deposit_addresses_by_network(self, code: str, params={}) -> List[DepositAddress]: + """ + fetch the deposit addresses for a currency associated with self account + + https://bingx-api.github.io/docs/#/en-us/common/wallet-api.html#Query%20Main%20Account%20Deposit%20Address + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary `address structures `, indexed by the network + """ + self.load_markets() + currency = self.currency(code) + defaultRecvWindow = self.safe_integer(self.options, 'recvWindow') + recvWindow = self.safe_integer(self.parse_params, 'recvWindow', defaultRecvWindow) + request: dict = { + 'coin': currency['id'], + 'offset': 0, + 'limit': 1000, + 'recvWindow': recvWindow, + } + response = self.walletsV1PrivateGetCapitalDepositAddress(self.extend(request, params)) + # + # { + # "code": "0", + # "timestamp": "1695200226859", + # "data": { + # "data": [ + # { + # "coinId": "799", + # "coin": "USDT", + # "network": "BEP20", + # "address": "6a7eda2817462dabb6493277a2cfe0f5c3f2550b", + # "tag": '' + # } + # ], + # "total": "1" + # } + # } + # + data = self.safe_list(self.safe_dict(response, 'data'), 'data') + parsed = self.parse_deposit_addresses(data, [currency['code']], False) + return self.index_by(parsed, 'network') + + def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://bingx-api.github.io/docs/#/en-us/common/wallet-api.html#Query%20Main%20Account%20Deposit%20Address + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.network]: The chain of currency. This only apply for multi-chain currency, and there is no need for single chain currency + :returns dict: an `address structure ` + """ + network = self.safe_string(params, 'network') + params = self.omit(params, ['network']) + addressStructures = self.fetch_deposit_addresses_by_network(code, params) + if network is not None: + return self.safe_dict(addressStructures, network) + else: + options = self.safe_dict(self.options, 'defaultNetworks') + defaultNetworkForCurrency = self.safe_string(options, code) + if defaultNetworkForCurrency is not None: + return self.safe_dict(addressStructures, defaultNetworkForCurrency) + else: + keys = list(addressStructures.keys()) + key = self.safe_string(keys, 0) + return self.safe_dict(addressStructures, key) + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + # + # { + # "coinId":"4", + # "coin":"USDT", + # "network":"OMNI", + # "address":"1HXyx8HVQRY7Nhqz63nwnRB7SpS9xQPzLN", + # "addressWithPrefix":"1HXyx8HVQRY7Nhqz63nwnRB7SpS9xQPzLN" + # } + # + tag = self.safe_string(depositAddress, 'tag') + currencyId = self.safe_string(depositAddress, 'coin') + currency = self.safe_currency(currencyId, currency) + code = currency['code'] + address = self.safe_string(depositAddress, 'addressWithPrefix') + networkdId = self.safe_string(depositAddress, 'network') + networkCode = self.network_id_to_code(networkdId, code) + self.check_address(address) + return { + 'info': depositAddress, + 'currency': code, + 'network': networkCode, + 'address': address, + 'tag': tag, + } + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://bingx-api.github.io/docs/#/spot/account-api.html#Deposit%20History(supporting%20network) + + :param str [code]: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + request: dict = { + } + currency = None + if code is not None: + currency = self.currency(code) + request['coin'] = currency['id'] + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit # default 1000 + response = self.spotV3PrivateGetCapitalDepositHisrec(self.extend(request, params)) + # + # [ + # { + # "amount":"0.00999800", + # "coin":"PAXG", + # "network":"ETH", + # "status":1, + # "address":"0x788cabe9236ce061e5a892e1a59395a81fc8d62c", + # "addressTag":"", + # "txId":"0xaad4654a3234aa6118af9b4b335f5ae81c360b2394721c019b5d1e75328b09f3", + # "insertTime":1599621997000, + # "transferType":0, + # "unlockConfirm":"12/12", # confirm times for unlocking + # "confirmTimes":"12/12" + # }, + # ] + # + return self.parse_transactions(response, currency, since, limit) + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://bingx-api.github.io/docs/#/spot/account-api.html#Withdraw%20History%20(supporting%20network) + + :param str [code]: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + request: dict = { + } + currency = None + if code is not None: + currency = self.currency(code) + request['coin'] = currency['id'] + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit # default 1000 + response = self.spotV3PrivateGetCapitalWithdrawHistory(self.extend(request, params)) + # + # [ + # { + # "address": "0x94df8b352de7f46f64b01d3666bf6e936e44ce60", + # "amount": "8.91000000", + # "applyTime": "2019-10-12 11:12:02", + # "coin": "USDT", + # "id": "b6ae22b3aa844210a7041aee7589627c", + # "withdrawOrderId": "WITHDRAWtest123", + # "network": "ETH", + # "transferType": 0 + # "status": 6, + # "transactionFee": "0.004", + # "confirmNo":3, + # "info": "The address is not valid. Please confirm with the recipient", + # "txId": "0xb5ef8c13b968a406cc62a93a8bd80f9e9a906ef1b3fcf20a2e48573c17659268" + # }, + # ] + # + return self.parse_transactions(response, currency, since, limit) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fetchDeposits + # + # { + # "amount":"0.00999800", + # "coin":"PAXG", + # "network":"ETH", + # "status":1, + # "address":"0x788cabe9236ce061e5a892e1a59395a81fc8d62c", + # "addressTag":"", + # "txId":"0xaad4654a3234aa6118af9b4b335f5ae81c360b2394721c019b5d1e75328b09f3", + # "insertTime":1599621997000, + # "transferType":0, + # "unlockConfirm":"12/12", # confirm times for unlocking + # "confirmTimes":"12/12" + # } + # + # fetchWithdrawals + # + # { + # "address": "0x94df8b352de7f46f64b01d3666bf6e936e44ce60", + # "amount": "8.91000000", + # "applyTime": "2019-10-12 11:12:02", + # "coin": "USDT", + # "id": "b6ae22b3aa844210a7041aee7589627c", + # "withdrawOrderId": "WITHDRAWtest123", + # "network": "ETH", + # "transferType": 0 + # "status": 6, + # "transactionFee": "0.004", + # "confirmNo":3, + # "info": "The address is not valid. Please confirm with the recipient", + # "txId": "0xb5ef8c13b968a406cc62a93a8bd80f9e9a906ef1b3fcf20a2e48573c17659268" + # } + # + # withdraw + # + # { + # "code":0, + # "timestamp":1705274263621, + # "data":{ + # "id":"1264246141278773252" + # } + # } + # + # parse withdraw-type output first... + # + data = self.safe_value(transaction, 'data') + dataId = None if (data is None) else self.safe_string(data, 'id') + id = self.safe_string(transaction, 'id', dataId) + address = self.safe_string(transaction, 'address') + tag = self.safe_string(transaction, 'addressTag') + timestamp = self.safe_integer_2(transaction, 'insertTime', 'timestamp') + datetime = self.iso8601(timestamp) + if timestamp is None: + datetime = self.safe_string(transaction, 'applyTime') + timestamp = self.parse8601(datetime) + network = self.safe_string(transaction, 'network') + currencyId = self.safe_string(transaction, 'coin') + code = self.safe_currency_code(currencyId, currency) + if (code is not None) and (code != network) and code.find(network) >= 0: + if network is not None: + code = code.replace(network, '') + rawType = self.safe_string(transaction, 'transferType') + type = 'deposit' if (rawType == '0') else 'withdrawal' + return { + 'info': transaction, + 'id': id, + 'txid': self.safe_string(transaction, 'txId'), + 'type': type, + 'currency': code, + 'network': self.network_id_to_code(network), + 'amount': self.safe_number(transaction, 'amount'), + 'status': self.parse_transaction_status(self.safe_string(transaction, 'status')), + 'timestamp': timestamp, + 'datetime': datetime, + 'address': address, + 'addressFrom': None, + 'addressTo': address, + 'tag': tag, + 'tagFrom': tag, + 'tagTo': None, + 'updated': None, + 'comment': self.safe_string(transaction, 'info'), + 'fee': { + 'currency': code, + 'cost': self.safe_number(transaction, 'transactionFee'), + 'rate': None, + }, + 'internal': None, + } + + def parse_transaction_status(self, status: str): + statuses: dict = { + '0': 'pending', + '1': 'ok', + '10': 'pending', + '20': 'rejected', + '30': 'ok', + '40': 'rejected', + '50': 'ok', + '60': 'pending', + '70': 'rejected', + '2': 'pending', + '3': 'rejected', + '4': 'pending', + '5': 'rejected', + '6': 'ok', + } + return self.safe_string(statuses, status, status) + + def set_margin_mode(self, marginMode: str, symbol: Str = None, params={}): + """ + set margin mode to 'cross' or 'isolated' + + https://bingx-api.github.io/docs/#/en-us/swapV2/trade-api.html#Change%20Margin%20Type + https://bingx-api.github.io/docs/#/en-us/cswap/trade-api.html#Set%20Margin%20Type + + :param str marginMode: 'cross' or 'isolated' + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setMarginMode() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + if market['type'] != 'swap': + raise BadSymbol(self.id + ' setMarginMode() supports swap contracts only') + marginMode = marginMode.upper() + if marginMode == 'CROSS': + marginMode = 'CROSSED' + if marginMode != 'ISOLATED' and marginMode != 'CROSSED': + raise BadRequest(self.id + ' setMarginMode() marginMode argument should be isolated or cross') + request: dict = { + 'symbol': market['id'], + 'marginType': marginMode, + } + subType = None + subType, params = self.handle_sub_type_and_params('setMarginMode', market, params) + if subType == 'inverse': + return self.cswapV1PrivatePostTradeMarginType(self.extend(request, params)) + else: + return self.swapV2PrivatePostTradeMarginType(self.extend(request, params)) + + def add_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + request: dict = { + 'type': 1, + } + return self.set_margin(symbol, amount, self.extend(request, params)) + + def reduce_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + request: dict = { + 'type': 2, + } + return self.set_margin(symbol, amount, self.extend(request, params)) + + def set_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + Either adds or reduces margin in an isolated position in order to set the margin to a specific value + + https://bingx-api.github.io/docs/#/swapV2/trade-api.html#Adjust%20isolated%20margin + + :param str symbol: unified market symbol of the market to set margin in + :param float amount: the amount to set the margin to + :param dict [params]: parameters specific to the bingx api endpoint + :returns dict: A `margin structure ` + """ + type = self.safe_integer(params, 'type') # 1 increase margin 2 decrease margin + if type is None: + raise ArgumentsRequired(self.id + ' setMargin() requires a type parameter either 1(increase margin) or 2(decrease margin)') + if not self.in_array(type, [1, 2]): + raise ArgumentsRequired(self.id + ' setMargin() requires a type parameter either 1(increase margin) or 2(decrease margin)') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'amount': self.amount_to_precision(market['symbol'], amount), + 'type': type, + } + response = self.swapV2PrivatePostTradePositionMargin(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "amount": 1, + # "type": 1 + # } + # + return self.parse_margin_modification(response, market) + + def parse_margin_modification(self, data: dict, market: Market = None) -> MarginModification: + # + # { + # "code": 0, + # "msg": "", + # "amount": 1, + # "type": 1 + # } + # + type = self.safe_string(data, 'type') + return { + 'info': data, + 'symbol': self.safe_string(market, 'symbol'), + 'type': 'add' if (type == '1') else 'reduce', + 'marginMode': 'isolated', + 'amount': self.safe_number(data, 'amount'), + 'total': self.safe_number(data, 'margin'), + 'code': self.safe_string(market, 'settle'), + 'status': None, + 'timestamp': None, + 'datetime': None, + } + + def fetch_leverage(self, symbol: str, params={}) -> Leverage: + """ + fetch the set leverage for a market + + https://bingx-api.github.io/docs/#/swapV2/trade-api.html#Query%20Leverage + https://bingx-api.github.io/docs/#/en-us/cswap/trade-api.html#Query%20Leverage + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `leverage structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = None + if market['inverse']: + response = self.cswapV1PrivateGetTradeLeverage(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "timestamp": 1720683803391, + # "data": { + # "symbol": "SOL-USD", + # "longLeverage": 5, + # "shortLeverage": 5, + # "maxLongLeverage": 50, + # "maxShortLeverage": 50, + # "availableLongVol": "4000000", + # "availableShortVol": "4000000" + # } + # } + # + else: + response = self.swapV2PrivateGetTradeLeverage(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "data": { + # "longLeverage": 5, + # "shortLeverage": 5, + # "maxLongLeverage": 125, + # "maxShortLeverage": 125, + # "availableLongVol": "0.0000", + # "availableShortVol": "0.0000", + # "availableLongVal": "0.0", + # "availableShortVal": "0.0", + # "maxPositionLongVal": "0.0", + # "maxPositionShortVal": "0.0" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_leverage(data, market) + + def parse_leverage(self, leverage: dict, market: Market = None) -> Leverage: + # + # linear swap + # + # { + # "longLeverage": 5, + # "shortLeverage": 5, + # "maxLongLeverage": 125, + # "maxShortLeverage": 125, + # "availableLongVol": "0.0000", + # "availableShortVol": "0.0000", + # "availableLongVal": "0.0", + # "availableShortVal": "0.0", + # "maxPositionLongVal": "0.0", + # "maxPositionShortVal": "0.0" + # } + # + # inverse swap + # + # { + # "symbol": "SOL-USD", + # "longLeverage": 5, + # "shortLeverage": 5, + # "maxLongLeverage": 50, + # "maxShortLeverage": 50, + # "availableLongVol": "4000000", + # "availableShortVol": "4000000" + # } + # + marketId = self.safe_string(leverage, 'symbol') + return { + 'info': leverage, + 'symbol': self.safe_symbol(marketId, market), + 'marginMode': None, + 'longLeverage': self.safe_integer(leverage, 'longLeverage'), + 'shortLeverage': self.safe_integer(leverage, 'shortLeverage'), + } + + def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://bingx-api.github.io/docs/#/swapV2/trade-api.html#Switch%20Leverage + https://bingx-api.github.io/docs/#/en-us/cswap/trade-api.html#Modify%20Leverage + + :param float leverage: the rate of leverage + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.side]: hedged: ['long' or 'short']. one way: ['both'] + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setLeverage() requires a symbol argument') + side = self.safe_string_upper(params, 'side') + self.check_required_argument('setLeverage', side, 'side', ['LONG', 'SHORT', 'BOTH']) + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'side': side, + 'leverage': leverage, + } + if market['inverse']: + return self.cswapV1PrivatePostTradeLeverage(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "timestamp": 1720725058059, + # "data": { + # "symbol": "SOL-USD", + # "longLeverage": 10, + # "shortLeverage": 5, + # "maxLongLeverage": 50, + # "maxShortLeverage": 50, + # "availableLongVol": "4000000", + # "availableShortVol": "4000000" + # } + # } + # + else: + return self.swapV2PrivatePostTradeLeverage(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "data": { + # "leverage": 10, + # "symbol": "BTC-USDT", + # "availableLongVol": "0.0000", + # "availableShortVol": "0.0000", + # "availableLongVal": "0.0", + # "availableShortVal": "0.0", + # "maxPositionLongVal": "0.0", + # "maxPositionShortVal": "0.0" + # } + # } + # + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://bingx-api.github.io/docs/#/en-us/spot/trade-api.html#Query%20transaction%20details + https://bingx-api.github.io/docs/#/en-us/swapV2/trade-api.html#Query%20historical%20transaction%20orders + https://bingx-api.github.io/docs/#/en-us/swapV2/trade-api.html#Query%20historical%20transaction%20details + https://bingx-api.github.io/docs/#/en-us/cswap/trade-api.html#Query%20Order%20Trade%20Detail + + :param str [symbol]: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms for the ending date filter, default is None + :param str params['trandingUnit']: COIN(directly represent assets such and ETH) or CONT(represents the number of contract sheets) + :param str params['orderId']: the order id required for inverse swap + :returns dict[]: a list of `trade structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMyTrades() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = {} + fills = None + response = None + subType = None + subType, params = self.handle_sub_type_and_params('fetchMyTrades', market, params) + if subType == 'inverse': + orderId = self.safe_string(params, 'orderId') + if orderId is None: + raise ArgumentsRequired(self.id + ' fetchMyTrades() requires an orderId argument for inverse swap trades') + response = self.cswapV1PrivateGetTradeAllFillOrders(self.extend(request, params)) + fills = self.safe_list(response, 'data', []) + # + # { + # "code": 0, + # "msg": "", + # "timestamp": 1722147756019, + # "data": [ + # { + # "orderId": "1817441228670648320", + # "symbol": "SOL-USD", + # "type": "MARKET", + # "side": "BUY", + # "positionSide": "LONG", + # "tradeId": "97244554", + # "volume": "2", + # "tradePrice": "182.652", + # "amount": "20.00000000", + # "realizedPnl": "0.00000000", + # "commission": "-0.00005475", + # "currency": "SOL", + # "buyer": True, + # "maker": False, + # "tradeTime": 1722146730000 + # } + # ] + # } + # + else: + request['symbol'] = market['id'] + now = self.milliseconds() + if since is not None: + startTimeReq = 'startTime' if market['spot'] else 'startTs' + request[startTimeReq] = since + elif market['swap']: + request['startTs'] = now - 30 * 24 * 60 * 60 * 1000 # 30 days for swap + until = self.safe_integer(params, 'until') + params = self.omit(params, 'until') + if until is not None: + endTimeReq = 'endTime' if market['spot'] else 'endTs' + request[endTimeReq] = until + elif market['swap']: + request['endTs'] = now + if market['spot']: + if limit is not None: + request['limit'] = limit # default 500, maximum 1000 + response = self.spotV1PrivateGetTradeMyTrades(self.extend(request, params)) + data = self.safe_dict(response, 'data', {}) + fills = self.safe_list(data, 'fills', []) + # + # { + # "code": 0, + # "msg": "", + # "debugMsg": "", + # "data": { + # "fills": [ + # { + # "symbol": "LTC-USDT", + # "id": 36237072, + # "orderId": 1674069326895775744, + # "price": "85.891", + # "qty": "0.0582", + # "quoteQty": "4.9988562000000005", + # "commission": -0.00005820000000000001, + # "commissionAsset": "LTC", + # "time": 1687964205000, + # "isBuyer": True, + # "isMaker": False + # } + # ] + # } + # } + # + else: + tradingUnit = self.safe_string_upper(params, 'tradingUnit', 'CONT') + params = self.omit(params, 'tradingUnit') + request['tradingUnit'] = tradingUnit + response = self.swapV2PrivateGetTradeAllFillOrders(self.extend(request, params)) + data = self.safe_dict(response, 'data', {}) + fills = self.safe_list(data, 'fill_orders', []) + # + # { + # "code": "0", + # "msg": '', + # "data": {fill_orders: [ + # { + # "volume": "0.1", + # "price": "106.75", + # "amount": "10.6750", + # "commission": "-0.0053", + # "currency": "USDT", + # "orderId": "1676213270274379776", + # "liquidatedPrice": "0.00", + # "liquidatedMarginRatio": "0.00", + # "filledTime": "2023-07-04T20:56:01.000+0800" + # } + # ] + # } + # } + # + return self.parse_trades(fills, market, since, limit, params) + + def parse_deposit_withdraw_fee(self, fee, currency: Currency = None): + # + # currencie structure + # + networks = self.safe_dict(fee, 'networks', {}) + networkCodes = list(networks.keys()) + networksLength = len(networkCodes) + result: dict = { + 'info': networks, + 'withdraw': { + 'fee': None, + 'percentage': None, + }, + 'deposit': { + 'fee': None, + 'percentage': None, + }, + 'networks': {}, + } + if networksLength != 0: + for i in range(0, networksLength): + networkCode = networkCodes[i] + network = networks[networkCode] + result['networks'][networkCode] = { + 'deposit': {'fee': None, 'percentage': None}, + 'withdraw': {'fee': self.safe_number(network, 'fee'), 'percentage': False}, + } + if networksLength == 1: + result['withdraw']['fee'] = self.safe_number(network, 'withdrawFee') + result['withdraw']['percentage'] = False + return result + + def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}): + """ + fetch deposit and withdraw fees + + https://bingx-api.github.io/docs/#/common/account-api.html#All%20Coins'%20Information + + :param str[]|None codes: list of unified currency codes + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `fee structures ` + """ + self.load_markets() + response = self.fetch_currencies(params) + depositWithdrawFees: dict = {} + responseCodes = list(response.keys()) + for i in range(0, len(responseCodes)): + code = responseCodes[i] + if (codes is None) or (self.in_array(code, codes)): + entry = response[code] + depositWithdrawFees[code] = self.parse_deposit_withdraw_fee(entry) + return depositWithdrawFees + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://bingx-api.github.io/docs/#/en-us/spot/wallet-api.html#Withdraw + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str [tag]: + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.walletType]: 1 fund(funding) account, 2 standard account, 3 perpetual account, 15 spot account + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.check_address(address) + self.load_markets() + currency = self.currency(code) + defaultWalletType = 15 # spot + walletType = None + walletType, params = self.handle_option_and_params_2(params, 'withdraw', 'type', 'walletType', defaultWalletType) + walletTypes = { + 'funding': 1, + 'fund': 1, + 'standard': 2, + 'perpetual': 3, + 'spot': 15, + } + walletType = self.safe_integer(walletTypes, walletType, defaultWalletType) + request: dict = { + 'coin': currency['id'], + 'address': address, + 'amount': self.currency_to_precision(code, amount), + 'walletType': walletType, + } + network = self.safe_string_upper(params, 'network') + if network is not None: + request['network'] = self.network_code_to_id(network) + if tag is not None: + request['addressTag'] = tag + params = self.omit(params, ['walletType', 'network']) + response = self.walletsV1PrivatePostCapitalWithdrawApply(self.extend(request, params)) + data = self.safe_value(response, 'data') + # { + # "code":0, + # "timestamp":1689258953651, + # "data":{ + # "id":"1197073063359000577" + # } + # } + return self.parse_transaction(data) + + def parse_params(self, params): + # sortedParams = self.keysort(params) + rawKeys = list(params.keys()) + keys = self.sort(rawKeys) + for i in range(0, len(keys)): + key = keys[i] + value = params[key] + if isinstance(value, list): + arrStr = '[' + for j in range(0, len(value)): + arrayElement = value[j] + if j > 0: + arrStr += ',' + arrStr += str(arrayElement) + arrStr += ']' + params[key] = arrStr + return params + + def fetch_my_liquidations(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + retrieves the users liquidated positions + + https://bingx-api.github.io/docs/#/swapV2/trade-api.html#User's%20Force%20Orders + https://bingx-api.github.io/docs/#/en-us/cswap/trade-api.html#Query%20force%20orders + + :param str [symbol]: unified CCXT market symbol + :param int [since]: the earliest time in ms to fetch liquidations for + :param int [limit]: the maximum number of liquidation structures to retrieve + :param dict [params]: exchange specific parameters for the bingx api endpoint + :param int [params.until]: timestamp in ms of the latest liquidation + :returns dict: an array of `liquidation structures ` + """ + self.load_markets() + request: dict = { + 'autoCloseType': 'LIQUIDATION', + } + request, params = self.handle_until_option('endTime', request, params) + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + subType = None + subType, params = self.handle_sub_type_and_params('fetchMyLiquidations', market, params) + response = None + liquidations = None + if subType == 'inverse': + response = self.cswapV1PrivateGetTradeForceOrders(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "timestamp": 1721280071678, + # "data": [ + # { + # "orderId": "string", + # "symbol": "string", + # "type": "string", + # "side": "string", + # "positionSide": "string", + # "price": "string", + # "quantity": "float64", + # "stopPrice": "string", + # "workingType": "string", + # "status": "string", + # "time": "int64", + # "avgPrice": "string", + # "executedQty": "string", + # "profit": "string", + # "commission": "string", + # "updateTime": "string" + # } + # ] + # } + # + liquidations = self.safe_list(response, 'data', []) + else: + response = self.swapV2PrivateGetTradeForceOrders(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "data": { + # "orders": [ + # { + # "time": "int64", + # "symbol": "string", + # "side": "string", + # "type": "string", + # "positionSide": "string", + # "cumQuote": "string", + # "status": "string", + # "stopPrice": "string", + # "price": "string", + # "origQty": "string", + # "avgPrice": "string", + # "executedQty": "string", + # "orderId": "int64", + # "profit": "string", + # "commission": "string", + # "workingType": "string", + # "updateTime": "int64" + # }, + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + liquidations = self.safe_list(data, 'orders', []) + return self.parse_liquidations(liquidations, market, since, limit) + + def parse_liquidation(self, liquidation, market: Market = None): + # + # { + # "time": "int64", + # "symbol": "string", + # "side": "string", + # "type": "string", + # "positionSide": "string", + # "cumQuote": "string", + # "status": "string", + # "stopPrice": "string", + # "price": "string", + # "origQty": "string", + # "avgPrice": "string", + # "executedQty": "string", + # "orderId": "int64", + # "profit": "string", + # "commission": "string", + # "workingType": "string", + # "updateTime": "int64" + # } + # + marketId = self.safe_string(liquidation, 'symbol') + timestamp = self.safe_integer(liquidation, 'time') + contractsString = self.safe_string(liquidation, 'executedQty') + contractSizeString = self.safe_string(market, 'contractSize') + priceString = self.safe_string(liquidation, 'avgPrice') + baseValueString = Precise.string_mul(contractsString, contractSizeString) + quoteValueString = Precise.string_mul(baseValueString, priceString) + return self.safe_liquidation({ + 'info': liquidation, + 'symbol': self.safe_symbol(marketId, market), + 'contracts': self.parse_number(contractsString), + 'contractSize': self.parse_number(contractSizeString), + 'price': self.parse_number(priceString), + 'baseValue': self.parse_number(baseValueString), + 'quoteValue': self.parse_number(quoteValueString), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + + def close_position(self, symbol: str, side: OrderSide = None, params={}) -> Order: + """ + closes open positions for a market + + https://bingx-api.github.io/docs/#/en-us/swapV2/trade-api.html#One-Click%20Close%20All%20Positions + https://bingx-api.github.io/docs/#/en-us/cswap/trade-api.html#Close%20all%20positions%20in%20bulk + + :param str symbol: Unified CCXT market symbol + :param str [side]: not used by bingx + :param dict [params]: extra parameters specific to the bingx api endpoint + :param str|None [params.positionId]: the id of the position you would like to close + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + positionId = self.safe_string(params, 'positionId') + request: dict = {} + response = None + if positionId is not None: + response = self.swapV1PrivatePostTradeClosePosition(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "timestamp": 1710992264190, + # "data": { + # "orderId": 1770656007907930112, + # "positionId": "1751667128353910784", + # "symbol": "LTC-USDT", + # "side": "Ask", + # "type": "MARKET", + # "positionSide": "Long", + # "origQty": "0.2" + # } + # } + # + else: + request['symbol'] = market['id'] + if market['inverse']: + response = self.cswapV1PrivatePostTradeCloseAllPositions(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "timestamp": 1720771601428, + # "data": { + # "success": ["1811673520637231104"], + # "failed": null + # } + # } + # + else: + response = self.swapV2PrivatePostTradeCloseAllPositions(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "data": { + # "success": [ + # 1727686766700486656, + # ], + # "failed": null + # } + # } + # + data = self.safe_dict(response, 'data') + return self.parse_order(data, market) + + def close_all_positions(self, params={}) -> List[Position]: + """ + closes open positions for a market + + https://bingx-api.github.io/docs/#/en-us/swapV2/trade-api.html#One-Click%20Close%20All%20Positions + https://bingx-api.github.io/docs/#/en-us/cswap/trade-api.html#Close%20all%20positions%20in%20bulk + + :param dict [params]: extra parameters specific to the bingx api endpoint + :param str [params.recvWindow]: request valid time window value + :returns dict[]: `a list of position structures ` + """ + self.load_markets() + defaultRecvWindow = self.safe_integer(self.options, 'recvWindow') + recvWindow = self.safe_integer(self.parse_params, 'recvWindow', defaultRecvWindow) + marketType = None + marketType, params = self.handle_market_type_and_params('closeAllPositions', None, params) + subType = None + subType, params = self.handle_sub_type_and_params('closeAllPositions', None, params) + if marketType == 'margin': + raise BadRequest(self.id + ' closePositions() cannot be used for ' + marketType + ' markets') + request: dict = { + 'recvWindow': recvWindow, + } + response = None + if subType == 'inverse': + response = self.cswapV1PrivatePostTradeCloseAllPositions(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "timestamp": 1720771601428, + # "data": { + # "success": ["1811673520637231104"], + # "failed": null + # } + # } + # + else: + response = self.swapV2PrivatePostTradeCloseAllPositions(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "data": { + # "success": [ + # 1727686766700486656, + # 1727686767048613888 + # ], + # "failed": null + # } + # } + # + data = self.safe_dict(response, 'data', {}) + success = self.safe_list(data, 'success', []) + positions = [] + for i in range(0, len(success)): + position = self.parse_position({'positionId': success[i]}) + positions.append(position) + return positions + + def fetch_position_mode(self, symbol: Str = None, params={}): + """ + fetchs the position mode, hedged or one way, hedged for binance is set identically for all linear markets or all inverse markets + + https://bingx-api.github.io/docs/#/en-us/swapV2/trade-api.html#Get%20Position%20Mode + + :param str symbol: unified symbol of the market to fetch the order book for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an object detailing whether the market is in hedged or one-way mode + """ + response = self.swapV1PrivateGetPositionSideDual(params) + # + # { + # "code": "0", + # "msg": "", + # "timeStamp": "1709002057516", + # "data": { + # "dualSidePosition": "false" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + dualSidePosition = self.safe_string(data, 'dualSidePosition') + return { + 'info': response, + 'hedged': (dualSidePosition == 'true'), + } + + def set_position_mode(self, hedged: bool, symbol: Str = None, params={}): + """ + set hedged to True or False for a market + + https://bingx-api.github.io/docs/#/en-us/swapV2/trade-api.html#Set%20Position%20Mode + + :param bool hedged: set to True to use dualSidePosition + :param str symbol: not used by bingx setPositionMode() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + dualSidePosition = None + if hedged: + dualSidePosition = 'true' + else: + dualSidePosition = 'false' + request: dict = { + 'dualSidePosition': dualSidePosition, + } + # + # { + # code: '0', + # msg: '', + # timeStamp: '1703327432734', + # data: {dualSidePosition: 'false'} + # } + # + return self.swapV1PrivatePostPositionSideDual(self.extend(request, params)) + + def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}) -> Order: + """ + cancels an order and places a new order + + https://bingx-api.github.io/docs/#/en-us/spot/trade-api.html#Cancel%20order%20and%20place%20a%20new%20order # spot + https://bingx-api.github.io/docs/#/en-us/swapV2/trade-api.html#Cancel%20an%20order%20and%20then%20Place%20a%20new%20order # swap + + :param str id: order id + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of the currency you want to trade in units of the base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.triggerPrice]: Trigger price used for TAKE_STOP_LIMIT, TAKE_STOP_MARKET, TRIGGER_LIMIT, TRIGGER_MARKET order types. + :param dict [params.takeProfit]: *takeProfit object in params* containing the triggerPrice at which the attached take profit order will be triggered + :param float [params.takeProfit.triggerPrice]: take profit trigger price + :param dict [params.stopLoss]: *stopLoss object in params* containing the triggerPrice at which the attached stop loss order will be triggered + :param float [params.stopLoss.triggerPrice]: stop loss trigger price + + EXCHANGE SPECIFIC PARAMETERS + :param str [params.cancelClientOrderID]: the user-defined id of the order to be canceled, 1-40 characters, different orders cannot use the same clientOrderID, only supports a query range of 2 hours + :param str [params.cancelRestrictions]: cancel orders with specified status, NEW: New order, PENDING: Pending order, PARTIALLY_FILLED: Partially filled + :param str [params.cancelReplaceMode]: STOP_ON_FAILURE - if the cancel order fails, it will not continue to place a new order, ALLOW_FAILURE - regardless of whether the cancel order succeeds or fails, it will continue to place a new order + :param float [params.quoteOrderQty]: order amount + :param str [params.newClientOrderId]: custom order id consisting of letters, numbers, and _, 1-40 characters, different orders cannot use the same newClientOrderId. + :param str [params.positionSide]: *contract only* position direction, required for single position, for both long and short positions only LONG or SHORT can be chosen, defaults to LONG if empty + :param str [params.reduceOnly]: *contract only* True or False, default=false for single position mode. self parameter is not accepted for both long and short positions mode + :param float [params.priceRate]: *contract only* for type TRAILING_STOP_Market or TRAILING_TP_SL, Max = 1 + :param str [params.workingType]: *contract only* StopPrice trigger price types, MARK_PRICE(default), CONTRACT_PRICE, or INDEX_PRICE + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + request = self.create_order_request(symbol, type, side, amount, price, params) + request['cancelOrderId'] = id + request['cancelReplaceMode'] = 'STOP_ON_FAILURE' + response = None + if market['swap']: + response = self.swapV1PrivatePostTradeCancelReplace(self.extend(request, params)) + # + # { + # code: '0', + # msg: '', + # data: { + # cancelResult: 'true', + # cancelMsg: '', + # cancelResponse: { + # cancelClientOrderId: '', + # cancelOrderId: '1755336244265705472', + # symbol: 'SOL-USDT', + # orderId: '1755336244265705472', + # side: 'SELL', + # positionSide: 'SHORT', + # type: 'LIMIT', + # origQty: '1', + # price: '100.000', + # executedQty: '0', + # avgPrice: '0.000', + # cumQuote: '0', + # stopPrice: '', + # profit: '0.0000', + # commission: '0.000000', + # status: 'PENDING', + # time: '1707339747860', + # updateTime: '1707339747860', + # clientOrderId: '', + # leverage: '20X', + # workingType: 'MARK_PRICE', + # onlyOnePosition: False, + # reduceOnly: False + # }, + # replaceResult: 'true', + # replaceMsg: '', + # newOrderResponse: { + # orderId: '1755338440612995072', + # symbol: 'SOL-USDT', + # positionSide: 'SHORT', + # side: 'SELL', + # type: 'LIMIT', + # price: '99', + # quantity: '2', + # stopPrice: '0', + # workingType: 'MARK_PRICE', + # clientOrderID: '', + # timeInForce: 'GTC', + # priceRate: '0', + # stopLoss: '', + # takeProfit: '', + # reduceOnly: False + # } + # } + # } + # + else: + response = self.spotV1PrivatePostTradeOrderCancelReplace(self.extend(request, params)) + # + # { + # code: '0', + # msg: '', + # debugMsg: '', + # data: { + # cancelResult: {code: '0', msg: '', result: True}, + # openResult: {code: '0', msg: '', result: True}, + # orderOpenResponse: { + # symbol: 'SOL-USDT', + # orderId: '1755334007697866752', + # transactTime: '1707339214620', + # price: '99', + # stopPrice: '0', + # origQty: '0.2', + # executedQty: '0', + # cummulativeQuoteQty: '0', + # status: 'PENDING', + # type: 'LIMIT', + # side: 'SELL', + # clientOrderID: '' + # }, + # orderCancelResponse: { + # symbol: 'SOL-USDT', + # orderId: '1755117055251480576', + # price: '100', + # stopPrice: '0', + # origQty: '0.2', + # executedQty: '0', + # cummulativeQuoteQty: '0', + # status: 'CANCELED', + # type: 'LIMIT', + # side: 'SELL' + # } + # } + # } + # + data = self.safe_dict(response, 'data') + return self.parse_order(data, market) + + def fetch_margin_mode(self, symbol: str, params={}) -> MarginMode: + """ + fetches the margin mode of the trading pair + + https://bingx-api.github.io/docs/#/en-us/swapV2/trade-api.html#Query%20Margin%20Type + https://bingx-api.github.io/docs/#/en-us/cswap/trade-api.html#Query%20Margin%20Type + + :param str symbol: unified symbol of the market to fetch the margin mode for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin mode structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + subType = None + response = None + subType, params = self.handle_sub_type_and_params('fetchMarginMode', market, params) + if subType == 'inverse': + response = self.cswapV1PrivateGetTradeMarginType(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "timestamp": 1721966069132, + # "data": { + # "symbol": "SOL-USD", + # "marginType": "CROSSED" + # } + # } + # + else: + response = self.swapV2PrivateGetTradeMarginType(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "data": { + # "marginType": "CROSSED" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_margin_mode(data, market) + + def parse_margin_mode(self, marginMode: dict, market=None) -> MarginMode: + marketId = self.safe_string(marginMode, 'symbol') + marginType = self.safe_string_lower(marginMode, 'marginType') + marginType = 'cross' if (marginType == 'crossed') else marginType + return { + 'info': marginMode, + 'symbol': self.safe_symbol(marketId, market, '-', 'swap'), + 'marginMode': marginType, + } + + def fetch_trading_fee(self, symbol: str, params={}) -> TradingFeeInterface: + """ + fetch the trading fees for a market + + https://bingx-api.github.io/docs/#/en-us/spot/trade-api.html#Query%20Trading%20Commission%20Rate + https://bingx-api.github.io/docs/#/en-us/swapV2/account-api.html#Query%20Trading%20Commission%20Rate + https://bingx-api.github.io/docs/#/en-us/cswap/trade-api.html#Query%20Trade%20Commission%20Rate + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `fee structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = None + commission: dict = {} + data = self.safe_dict(response, 'data', {}) + if market['spot']: + response = self.spotV1PrivateGetUserCommissionRate(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "debugMsg": "", + # "data": { + # "takerCommissionRate": 0.001, + # "makerCommissionRate": 0.001 + # } + # } + # + commission = data + else: + if market['inverse']: + response = self.cswapV1PrivateGetUserCommissionRate(params) + # + # { + # "code": 0, + # "msg": "", + # "timestamp": 1721365261438, + # "data": { + # "takerCommissionRate": "0.0005", + # "makerCommissionRate": "0.0002" + # } + # } + # + commission = data + else: + response = self.swapV2PrivateGetUserCommissionRate(params) + # + # { + # "code": 0, + # "msg": "", + # "data": { + # "commission": { + # "takerCommissionRate": 0.0005, + # "makerCommissionRate": 0.0002 + # } + # } + # } + # + commission = self.safe_dict(data, 'commission', {}) + return self.parse_trading_fee(commission, market) + + def parse_trading_fee(self, fee: dict, market: Market = None) -> TradingFeeInterface: + # + # { + # "takerCommissionRate": 0.001, + # "makerCommissionRate": 0.001 + # } + # + symbol = market['symbol'] if (market is not None) else None + return { + 'info': fee, + 'symbol': symbol, + 'maker': self.safe_number(fee, 'makerCommissionRate'), + 'taker': self.safe_number(fee, 'takerCommissionRate'), + 'percentage': False, + 'tierBased': False, + } + + def custom_encode(self, params): + # sortedParams = self.keysort(params) + rawKeys = list(params.keys()) + keys = self.sort(rawKeys) + adjustedValue = None + result = None + for i in range(0, len(keys)): + key = keys[i] + value = params[key] + if isinstance(value, list): + arrStr = None + for j in range(0, len(value)): + arrayElement = value[j] + isString = (isinstance(arrayElement, str)) + if isString: + if j > 0: + arrStr += ',' + '"' + str(arrayElement) + '"' + else: + arrStr = '"' + str(arrayElement) + '"' + else: + if j > 0: + arrStr += ',' + str(arrayElement) + else: + arrStr = str(arrayElement) + adjustedValue = '[' + arrStr + ']' + value = adjustedValue + if i == 0: + result = key + '=' + value + else: + result += '&' + key + '=' + value + return result + + def sign(self, path, section='public', method='GET', params={}, headers=None, body=None): + type = section[0] + version = section[1] + access = section[2] + isSandbox = self.safe_bool(self.options, 'sandboxMode', False) + if isSandbox and (type != 'swap'): + raise NotSupported(self.id + ' does not have a testnet/sandbox URL for ' + type + ' endpoints') + url = self.implode_hostname(self.urls['api'][type]) + path = self.implode_params(path, params) + versionIsTransfer = (version == 'transfer') + versionIsAsset = (version == 'asset') + if versionIsTransfer or versionIsAsset: + if versionIsTransfer: + type = 'account/transfer' + else: + type = 'api/asset' + version = section[2] + access = section[3] + if path != 'account/apiPermissions': + if type == 'spot' and version == 'v3': + url += '/api' + else: + url += '/' + type + url += '/' + version + '/' + path + params = self.omit(params, self.extract_params(path)) + params['timestamp'] = self.nonce() + params = self.keysort(params) + if access == 'public': + if params: + url += '?' + self.urlencode(params) + elif access == 'private': + self.check_required_credentials() + isJsonContentType = (((type == 'subAccount') or (type == 'account/transfer')) and (method == 'POST')) + parsedParams = None + encodeRequest = None + if isJsonContentType: + encodeRequest = self.custom_encode(params) + else: + parsedParams = self.parse_params(params) + encodeRequest = self.rawencode(parsedParams, True) + signature = self.hmac(self.encode(encodeRequest), self.encode(self.secret), hashlib.sha256) + headers = { + 'X-BX-APIKEY': self.apiKey, + 'X-SOURCE-KEY': self.safe_string(self.options, 'broker', 'CCXT'), + } + if isJsonContentType: + headers['Content-Type'] = 'application/json' + params['signature'] = signature + body = self.json(params) + else: + query = self.urlencode(parsedParams, True) + url += '?' + query + '&' + 'signature=' + signature + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def nonce(self): + return self.milliseconds() + + def set_sandbox_mode(self, enable: bool): + super(bingx, self).set_sandbox_mode(enable) + self.options['sandboxMode'] = enable + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None # fallback to default error handler + # + # { + # "code": 80014, + # "msg": "Invalid parameters, err:Key: 'GetTickerRequest.Symbol' Error:Field validation for "Symbol" failed on the "len=0|endswith=-USDT" tag", + # "data": { + # } + # } + # + code = self.safe_string(response, 'code') + message = self.safe_string(response, 'msg') + transferErrorMsg = self.safe_string(response, 'transferErrorMsg') # handling with errors from transfer endpoint + if (transferErrorMsg is not None) or (code is not None and code != '0'): + if transferErrorMsg is not None: + message = transferErrorMsg + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], code, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + raise ExchangeError(feedback) # unknown message + return None diff --git a/ccxt/bit2c.py b/ccxt/bit2c.py new file mode 100644 index 0000000..25b65a6 --- /dev/null +++ b/ccxt/bit2c.py @@ -0,0 +1,986 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.bit2c import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currency, DepositAddress, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Ticker, Trade, TradingFees +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import NotSupported +from ccxt.base.errors import InvalidNonce +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class bit2c(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(bit2c, self).describe(), { + 'id': 'bit2c', + 'name': 'Bit2C', + 'countries': ['IL'], # Israel + 'rateLimit': 3000, + 'pro': False, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelAllOrders': False, + 'cancelOrder': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createOrder': True, + 'createOrderWithTakeProfitAndStopLoss': False, + 'createOrderWithTakeProfitAndStopLossWs': False, + 'createReduceOnlyOrder': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkOHLCV': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': True, + 'fetchTransfer': False, + 'fetchTransfers': False, + 'fetchUnderlyingAssets': False, + 'fetchVolatilityHistory': False, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'repayMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'transfer': False, + 'ws': False, + }, + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/db0bce50-6842-4c09-a1d5-0c87d22118aa', + 'api': { + 'rest': 'https://bit2c.co.il', + }, + 'www': 'https://www.bit2c.co.il', + 'referral': 'https://bit2c.co.il/Aff/63bfed10-e359-420c-ab5a-ad368dab0baf', + 'doc': [ + 'https://www.bit2c.co.il/home/api', + 'https://github.com/OferE/bit2c', + ], + }, + 'api': { + 'public': { + 'get': [ + 'Exchanges/{pair}/Ticker', + 'Exchanges/{pair}/orderbook', + 'Exchanges/{pair}/trades', + 'Exchanges/{pair}/lasttrades', + ], + }, + 'private': { + 'post': [ + 'Merchant/CreateCheckout', + 'Funds/AddCoinFundsRequest', + 'Order/AddFund', + 'Order/AddOrder', + 'Order/GetById', + 'Order/AddOrderMarketPriceBuy', + 'Order/AddOrderMarketPriceSell', + 'Order/CancelOrder', + 'Order/AddCoinFundsRequest', + 'Order/AddStopOrder', + 'Payment/GetMyId', + 'Payment/Send', + 'Payment/Pay', + ], + 'get': [ + 'Account/Balance', + 'Account/Balance/v2', + 'Order/MyOrders', + 'Order/GetById', + 'Order/AccountHistory', + 'Order/OrderHistory', + ], + }, + }, + 'markets': { + 'BTC/NIS': self.safe_market_structure({'id': 'BtcNis', 'symbol': 'BTC/NIS', 'base': 'BTC', 'quote': 'NIS', 'baseId': 'Btc', 'quoteId': 'Nis', 'type': 'spot', 'spot': True}), + 'ETH/NIS': self.safe_market_structure({'id': 'EthNis', 'symbol': 'ETH/NIS', 'base': 'ETH', 'quote': 'NIS', 'baseId': 'Eth', 'quoteId': 'Nis', 'type': 'spot', 'spot': True}), + 'LTC/NIS': self.safe_market_structure({'id': 'LtcNis', 'symbol': 'LTC/NIS', 'base': 'LTC', 'quote': 'NIS', 'baseId': 'Ltc', 'quoteId': 'Nis', 'type': 'spot', 'spot': True}), + 'USDC/NIS': self.safe_market_structure({'id': 'UsdcNis', 'symbol': 'USDC/NIS', 'base': 'USDC', 'quote': 'NIS', 'baseId': 'Usdc', 'quoteId': 'Nis', 'type': 'spot', 'spot': True}), + }, + 'fees': { + 'trading': { + 'tierBased': True, + 'percentage': True, + 'maker': self.parse_number('0.025'), + 'taker': self.parse_number('0.03'), + 'tiers': { + 'taker': [ + [self.parse_number('0'), self.parse_number('0.03')], + [self.parse_number('20000'), self.parse_number('0.0275')], + [self.parse_number('50000'), self.parse_number('0.025')], + [self.parse_number('75000'), self.parse_number('0.0225')], + [self.parse_number('100000'), self.parse_number('0.02')], + [self.parse_number('250000'), self.parse_number('0.015')], + [self.parse_number('500000'), self.parse_number('0.0125')], + [self.parse_number('750000'), self.parse_number('0.01')], + [self.parse_number('1000000'), self.parse_number('0.008')], + [self.parse_number('2000000'), self.parse_number('0.006')], + [self.parse_number('3000000'), self.parse_number('0.004')], + [self.parse_number('4000000'), self.parse_number('0.002')], + ], + 'maker': [ + [self.parse_number('0'), self.parse_number('0.025')], + [self.parse_number('20000'), self.parse_number('0.0225')], + [self.parse_number('50000'), self.parse_number('0.02')], + [self.parse_number('75000'), self.parse_number('0.0175')], + [self.parse_number('100000'), self.parse_number('0.015')], + [self.parse_number('250000'), self.parse_number('0.01')], + [self.parse_number('500000'), self.parse_number('0.0075')], + [self.parse_number('750000'), self.parse_number('0.005')], + [self.parse_number('1000000'), self.parse_number('0.004')], + [self.parse_number('2000000'), self.parse_number('0.003')], + [self.parse_number('3000000'), self.parse_number('0.002')], + [self.parse_number('4000000'), self.parse_number('0.001')], + ], + }, + }, + }, + 'options': { + 'fetchTradesMethod': 'public_get_exchanges_pair_trades', + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': False, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': False, + 'FOK': False, + 'PO': False, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyRequiresPrice': False, + 'marketBuyByCost': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 30, + 'untilDays': 30, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOrders': None, + 'fetchClosedOrders': None, # todo implement + 'fetchOHLCV': None, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'exact': { + 'Please provide valid APIkey': AuthenticationError, # {"error" : "Please provide valid APIkey"} + 'No order found.': OrderNotFound, # {"Error" : "No order found."} + }, + 'broad': { + # {"error": "Please provide valid nonce in Request Nonce(1598218490) is not bigger than last nonce(1598218490)."} + # {"error": "Please provide valid nonce in Request UInt64.TryParse failed for nonce :"} + 'Please provide valid nonce': InvalidNonce, + 'please approve new terms of use on site': PermissionDenied, # {"error" : "please approve new terms of use on site."} + }, + }, + }) + + def parse_balance(self, response) -> Balances: + result: dict = { + 'info': response, + 'timestamp': None, + 'datetime': None, + } + codes = list(self.currencies.keys()) + for i in range(0, len(codes)): + code = codes[i] + account = self.account() + currency = self.currency(code) + uppercase = currency['id'].upper() + if uppercase in response: + account['free'] = self.safe_string(response, 'AVAILABLE_' + uppercase) + account['total'] = self.safe_string(response, uppercase) + result[code] = account + return self.safe_balance(result) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://bit2c.co.il/home/api#balance + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + response = self.privateGetAccountBalanceV2(params) + # + # { + # "AVAILABLE_NIS": 0.0, + # "NIS": 0.0, + # "LOCKED_NIS": 0.0, + # "AVAILABLE_BTC": 0.0, + # "BTC": 0.0, + # "LOCKED_BTC": 0.0, + # "AVAILABLE_ETH": 0.0, + # "ETH": 0.0, + # "LOCKED_ETH": 0.0, + # "AVAILABLE_BCHSV": 0.0, + # "BCHSV": 0.0, + # "LOCKED_BCHSV": 0.0, + # "AVAILABLE_BCHABC": 0.0, + # "BCHABC": 0.0, + # "LOCKED_BCHABC": 0.0, + # "AVAILABLE_LTC": 0.0, + # "LTC": 0.0, + # "LOCKED_LTC": 0.0, + # "AVAILABLE_ETC": 0.0, + # "ETC": 0.0, + # "LOCKED_ETC": 0.0, + # "AVAILABLE_BTG": 0.0, + # "BTG": 0.0, + # "LOCKED_BTG": 0.0, + # "AVAILABLE_GRIN": 0.0, + # "GRIN": 0.0, + # "LOCKED_GRIN": 0.0, + # "Fees": { + # "BtcNis": {"FeeMaker": 1.0, "FeeTaker": 1.0}, + # "EthNis": {"FeeMaker": 1.0, "FeeTaker": 1.0}, + # "BchabcNis": {"FeeMaker": 1.0, "FeeTaker": 1.0}, + # "LtcNis": {"FeeMaker": 1.0, "FeeTaker": 1.0}, + # "EtcNis": {"FeeMaker": 1.0, "FeeTaker": 1.0}, + # "BtgNis": {"FeeMaker": 1.0, "FeeTaker": 1.0}, + # "LtcBtc": {"FeeMaker": 1.0, "FeeTaker": 1.0}, + # "BchsvNis": {"FeeMaker": 1.0, "FeeTaker": 1.0}, + # "GrinNis": {"FeeMaker": 1.0, "FeeTaker": 1.0} + # } + # } + # + return self.parse_balance(response) + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://bit2c.co.il/home/api#orderb + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + orderbook = self.publicGetExchangesPairOrderbook(self.extend(request, params)) + return self.parse_order_book(orderbook, symbol) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + symbol = self.safe_symbol(None, market) + averagePrice = self.safe_string(ticker, 'av') + baseVolume = self.safe_string(ticker, 'a') + last = self.safe_string(ticker, 'll') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': None, + 'datetime': None, + 'high': None, + 'low': None, + 'bid': self.safe_string(ticker, 'h'), + 'bidVolume': None, + 'ask': self.safe_string(ticker, 'l'), + 'askVolume': None, + 'vwap': None, + 'open': None, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': averagePrice, + 'baseVolume': baseVolume, + 'quoteVolume': None, + 'info': ticker, + }, market) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://bit2c.co.il/home/api#ticker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + response = self.publicGetExchangesPairTicker(self.extend(request, params)) + return self.parse_ticker(response, market) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://bit2c.co.il/home/api#transactions + https://bit2c.co.il/home/api#trades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + method = self.options['fetchTradesMethod'] # public_get_exchanges_pair_trades or public_get_exchanges_pair_lasttrades + request: dict = { + 'pair': market['id'], + } + if since is not None: + request['date'] = self.parse_to_int(since) + if limit is not None: + request['limit'] = limit # max 100000 + response = None + if method == 'public_get_exchanges_pair_trades': + response = self.publicGetExchangesPairTrades(self.extend(request, params)) + else: + response = self.publicGetExchangesPairLasttrades(self.extend(request, params)) + # + # [ + # {"date":1651785980,"price":127975.68,"amount":0.3750321,"isBid":true,"tid":1261018}, + # {"date":1651785980,"price":127987.70,"amount":0.0389527820303982335802581029,"isBid":true,"tid":1261020}, + # {"date":1651786701,"price":128084.03,"amount":0.0015614749161156156626239821,"isBid":true,"tid":1261022}, + # ] + # + if isinstance(response, str): + raise ExchangeError(response) + return self.parse_trades(response, market, since, limit) + + def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://bit2c.co.il/home/api#balance + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + self.load_markets() + response = self.privateGetAccountBalance(params) + # + # { + # "AVAILABLE_NIS": 0.0, + # "NIS": 0.0, + # "LOCKED_NIS": 0.0, + # "AVAILABLE_BTC": 0.0, + # "BTC": 0.0, + # "LOCKED_BTC": 0.0, + # ... + # "Fees": { + # "BtcNis": {"FeeMaker": 1.0, "FeeTaker": 1.0}, + # "EthNis": {"FeeMaker": 1.0, "FeeTaker": 1.0}, + # ... + # } + # } + # + fees = self.safe_value(response, 'Fees', {}) + keys = list(fees.keys()) + result: dict = {} + for i in range(0, len(keys)): + marketId = keys[i] + symbol = self.safe_symbol(marketId) + fee = self.safe_value(fees, marketId) + makerString = self.safe_string(fee, 'FeeMaker') + takerString = self.safe_string(fee, 'FeeTaker') + maker = self.parse_number(Precise.string_div(makerString, '100')) + taker = self.parse_number(Precise.string_div(takerString, '100')) + result[symbol] = { + 'info': fee, + 'symbol': symbol, + 'taker': taker, + 'maker': maker, + 'percentage': True, + 'tierBased': True, + } + return result + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://bit2c.co.il/home/api#addo + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + method = 'privatePostOrderAddOrder' + market = self.market(symbol) + request: dict = { + 'Amount': amount, + 'Pair': market['id'], + } + if type == 'market': + method += 'MarketPrice' + self.capitalize(side) + else: + request['Price'] = price + amountString = self.number_to_string(amount) + priceString = self.number_to_string(price) + request['Total'] = self.parse_to_numeric(Precise.string_mul(amountString, priceString)) + request['IsBid'] = (side == 'buy') + response = getattr(self, method)(self.extend(request, params)) + return self.parse_order(response, market) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://bit2c.co.il/home/api#cancelo + + :param str id: order id + :param str symbol: Not used by bit2c cancelOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + request: dict = { + 'id': id, + } + response = self.privatePostOrderCancelOrder(self.extend(request, params)) + return self.parse_order(response) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://bit2c.co.il/home/api#geto + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOpenOrders() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + response = self.privateGetOrderMyOrders(self.extend(request, params)) + orders = self.safe_value(response, market['id'], {}) + asks = self.safe_value(orders, 'ask', []) + bids = self.safe_list(orders, 'bid', []) + return self.parse_orders(self.array_concat(asks, bids), market, since, limit) + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://bit2c.co.il/home/api#getoid + + :param str id: the order id + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'id': id, + } + response = self.privateGetOrderGetById(self.extend(request, params)) + # + # { + # "pair": "BtcNis", + # "status": "Completed", + # "created": 1666689837, + # "type": 0, + # "order_type": 0, + # "amount": 0.00000000, + # "price": 50000.00000000, + # "stop": 0, + # "id": 10951473, + # "initialAmount": 2.00000000 + # } + # + return self.parse_order(response, market) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder + # { + # "OrderResponse": {"pair": "BtcNis", "HasError": False, "Error": "", "Message": ""}, + # "NewOrder": { + # "created": 1505531577, + # "type": 0, + # "order_type": 0, + # "status_type": 0, + # "amount": 0.01, + # "price": 10000, + # "stop": 0, + # "id": 9244416, + # "initialAmount": None, + # }, + # } + # fetchOrder, fetchOpenOrders + # { + # "pair": "BtcNis", + # "status": "Completed", + # "created": 1535555837, + # "type": 0, + # "order_type": 0, + # "amount": 0.00000000, + # "price": 120000.00000000, + # "stop": 0, + # "id": 10555173, + # "initialAmount": 2.00000000 + # } + # + orderUnified = None + isNewOrder = False + if 'NewOrder' in order: + orderUnified = order['NewOrder'] + isNewOrder = True + else: + orderUnified = order + id = self.safe_string(orderUnified, 'id') + symbol = self.safe_symbol(None, market) + timestamp = self.safe_integer_product(orderUnified, 'created', 1000) + # status field vary between responses + # bit2c status type: + # 0 = New + # 1 = Open + # 5 = Completed + status: str + if isNewOrder: + tempStatus = self.safe_integer(orderUnified, 'status_type') + if tempStatus == 0 or tempStatus == 1: + status = 'open' + elif tempStatus == 5: + status = 'closed' + else: + tempStatus = self.safe_string(orderUnified, 'status') + if tempStatus == 'New' or tempStatus == 'Open': + status = 'open' + elif tempStatus == 'Completed': + status = 'closed' + # bit2c order type: + # 0 = LMT, 1 = MKT + type = self.safe_string(orderUnified, 'order_type') + if type == '0': + type = 'limit' + elif type == '1': + type = 'market' + # bit2c side: + # 0 = buy, 1 = sell + side = self.safe_string(orderUnified, 'type') + if side == '0': + side = 'buy' + elif side == '1': + side = 'sell' + price = self.safe_string(orderUnified, 'price') + amount = None + remaining = None + if isNewOrder: + amount = self.safe_string(orderUnified, 'amount') # NOTE:'initialAmount' is currently not set on new order + remaining = self.safe_string(orderUnified, 'amount') + else: + amount = self.safe_string(orderUnified, 'initialAmount') + remaining = self.safe_string(orderUnified, 'amount') + return self.safe_order({ + 'id': id, + 'clientOrderId': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'status': status, + 'symbol': symbol, + 'type': type, + 'timeInForce': None, + 'postOnly': None, + 'side': side, + 'price': price, + 'triggerPrice': None, + 'amount': amount, + 'filled': None, + 'remaining': remaining, + 'cost': None, + 'trades': None, + 'fee': None, + 'info': order, + 'average': None, + }, market) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://bit2c.co.il/home/api#orderh + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = None + request: dict = {} + if limit is not None: + request['take'] = limit + request['take'] = limit + if since is not None: + request['toTime'] = self.yyyymmdd(self.milliseconds(), '.') + request['fromTime'] = self.yyyymmdd(since, '.') + if symbol is not None: + market = self.market(symbol) + request['pair'] = market['id'] + response = self.privateGetOrderOrderHistory(self.extend(request, params)) + # + # [ + # { + # "ticks":1574767951, + # "created":"26/11/19 13:32", + # "action":1, + # "price":"1000", + # "pair":"EthNis", + # "reference":"EthNis|10867390|10867377", + # "fee":"0.5", + # "feeAmount":"0.08", + # "feeCoin":"₪", + # "firstAmount":"-0.015", + # "firstAmountBalance":"9", + # "secondAmount":"14.93", + # "secondAmountBalance":"130,233.28", + # "firstCoin":"ETH", + # "secondCoin":"₪" + # }, + # { + # "ticks":1574767951, + # "created":"26/11/19 13:32", + # "action":0, + # "price":"1000", + # "pair":"EthNis", + # "reference":"EthNis|10867390|10867377", + # "fee":"0.5", + # "feeAmount":"0.08", + # "feeCoin":"₪", + # "firstAmount":"0.015", + # "firstAmountBalance":"9.015", + # "secondAmount":"-15.08", + # "secondAmountBalance":"130,218.35", + # "firstCoin":"ETH", + # "secondCoin":"₪" + # } + # ] + # + return self.parse_trades(response, market, since, limit) + + def remove_comma_from_value(self, str): + newString = '' + strParts = str.split(',') + for i in range(0, len(strParts)): + newString += strParts[i] + return newString + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # public fetchTrades + # + # { + # "date":1651785980, + # "price":127975.68, + # "amount":0.3750321, + # "isBid":true, + # "tid":1261018 + # } + # + # private fetchMyTrades + # + # { + # "ticks":1574767951, + # "created":"26/11/19 13:32", + # "action":1, + # "price":"1,000", + # "pair":"EthNis", + # "reference":"EthNis|10867390|10867377", + # "fee":"0.5", + # "feeAmount":"0.08", + # "feeCoin":"₪", + # "firstAmount":"-0.015", + # "firstAmountBalance":"9", + # "secondAmount":"14.93", + # "secondAmountBalance":"130,233.28", + # "firstCoin":"ETH", + # "secondCoin":"₪" + # "isMaker": True, + # } + # + timestamp: Int + id: Str + price = None + amount = None + orderId = None + fee = None + side: str + makerOrTaker = None + reference = self.safe_string(trade, 'reference') + if reference is not None: + id = reference + timestamp = self.safe_timestamp(trade, 'ticks') + price = self.safe_string(trade, 'price') + price = self.remove_comma_from_value(price) + amount = self.safe_string(trade, 'firstAmount') + reference_parts = reference.split('|') # reference contains 'pair|orderId_by_taker|orderId_by_maker' + marketId = self.safe_string(trade, 'pair') + market = self.safe_market(marketId, market) + market = self.safe_market(reference_parts[0], market) + isMaker = self.safe_value(trade, 'isMaker') + makerOrTaker = 'maker' if isMaker else 'taker' + orderId = reference_parts[2] if isMaker else reference_parts[1] + action = self.safe_integer(trade, 'action') + if action == 0: + side = 'buy' + else: + side = 'sell' + feeCost = self.safe_string(trade, 'feeAmount') + if feeCost is not None: + fee = { + 'cost': feeCost, + 'currency': 'NIS', + } + else: + timestamp = self.safe_timestamp(trade, 'date') + id = self.safe_string(trade, 'tid') + price = self.safe_string(trade, 'price') + amount = self.safe_string(trade, 'amount') + side = self.safe_value(trade, 'isBid') + if side is not None: + if side: + side = 'buy' + else: + side = 'sell' + market = self.safe_market(None, market) + return self.safe_trade({ + 'info': trade, + 'id': id, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'order': orderId, + 'type': None, + 'side': side, + 'takerOrMaker': makerOrTaker, + 'price': price, + 'amount': amount, + 'cost': None, + 'fee': fee, + }, market) + + def is_fiat(self, code): + return code == 'NIS' + + def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://bit2c.co.il/home/api#addc + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + self.load_markets() + currency = self.currency(code) + if self.is_fiat(code): + raise NotSupported(self.id + ' fetchDepositAddress() does not support fiat currencies') + request: dict = { + 'Coin': currency['id'], + } + response = self.privatePostFundsAddCoinFundsRequest(self.extend(request, params)) + # + # { + # "address": "0xf14b94518d74aff2b1a6d3429471bcfcd3881d42", + # "hasTx": False + # } + # + return self.parse_deposit_address(response, currency) + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + # + # { + # "address": "0xf14b94518d74aff2b1a6d3429471bcfcd3881d42", + # "hasTx": False + # } + # + address = self.safe_string(depositAddress, 'address') + self.check_address(address) + code = self.safe_currency_code(None, currency) + return { + 'info': depositAddress, + 'currency': code, + 'network': None, + 'address': address, + 'tag': None, + } + + def nonce(self): + return self.milliseconds() + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = self.urls['api']['rest'] + '/' + self.implode_params(path, params) + if api == 'public': + url += '.json' + else: + self.check_required_credentials() + nonce = self.nonce() + query = self.extend({ + 'nonce': nonce, + }, params) + auth = self.urlencode(query) + if method == 'GET': + if query: + url += '?' + auth + else: + body = auth + signature = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha512, 'base64') + headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + 'key': self.apiKey, + 'sign': signature, + } + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None # fallback to default error handler + # + # {"error" : "please approve new terms of use on site."} + # {"error": "Please provide valid nonce in Request Nonce(1598218490) is not bigger than last nonce(1598218490)."} + # {"Error" : "No order found."} + # + error = self.safe_string(response, 'error') + if error is None: + error = self.safe_string(response, 'Error') + if error is not None: + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], error, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], error, feedback) + raise ExchangeError(feedback) # unknown message + return None diff --git a/ccxt/bitbank.py b/ccxt/bitbank.py new file mode 100644 index 0000000..ee05511 --- /dev/null +++ b/ccxt/bitbank.py @@ -0,0 +1,1130 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.bitbank import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currency, DepositAddress, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Ticker, Trade, TradingFees, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import InvalidNonce +from ccxt.base.decimal_to_precision import TICK_SIZE + + +class bitbank(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(bitbank, self).describe(), { + 'id': 'bitbank', + 'name': 'bitbank', + 'countries': ['JP'], + 'version': 'v1', + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelAllOrders': False, + 'cancelOrder': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createOrder': True, + 'createOrderWithTakeProfitAndStopLoss': False, + 'createOrderWithTakeProfitAndStopLossWs': False, + 'createReduceOnlyOrder': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': False, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkOHLCV': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': True, + 'fetchTransfer': False, + 'fetchTransfers': False, + 'fetchVolatilityHistory': False, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'transfer': False, + 'withdraw': True, + }, + 'timeframes': { + '1m': '1min', + '5m': '5min', + '15m': '15min', + '30m': '30min', + '1h': '1hour', + '4h': '4hour', + '8h': '8hour', + '12h': '12hour', + '1d': '1day', + '1w': '1week', + }, + 'hostname': 'bitbank.cc', + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/9d616de0-8a88-4468-8e38-d269acab0348', + 'api': { + 'public': 'https://public.{hostname}', + 'private': 'https://api.{hostname}', + 'markets': 'https://api.{hostname}', + }, + 'www': 'https://bitbank.cc/', + 'doc': 'https://docs.bitbank.cc/', + 'fees': 'https://bitbank.cc/docs/fees/', + }, + 'api': { + 'public': { + 'get': [ + '{pair}/ticker', + 'tickers', + 'tickers_jpy', + '{pair}/depth', + '{pair}/transactions', + '{pair}/transactions/{yyyymmdd}', + '{pair}/candlestick/{candletype}/{yyyymmdd}', + '{pair}/circuit_break_info', + ], + }, + 'private': { + 'get': [ + 'user/assets', + 'user/spot/order', + 'user/spot/active_orders', + 'user/margin/positions', + 'user/spot/trade_history', + 'user/deposit_history', + 'user/unconfirmed_deposits', + 'user/deposit_originators', + 'user/withdrawal_account', + 'user/withdrawal_history', + 'spot/status', + 'spot/pairs', + ], + 'post': [ + 'user/spot/order', + 'user/spot/cancel_order', + 'user/spot/cancel_orders', + 'user/spot/orders_info', + 'user/confirm_deposits', + 'user/confirm_deposits_all', + 'user/request_withdrawal', + ], + }, + 'markets': { + 'get': [ + 'spot/pairs', + ], + }, + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, # todo implement + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': False, + 'FOK': False, + 'PO': True, # todo: implement + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyRequiresPrice': False, + 'marketBuyByCost': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': None, + 'untilDays': None, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 1000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': None, + 'fetchOHLCV': { + 'limit': 1000, + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'exact': { + '20001': AuthenticationError, + '20002': AuthenticationError, + '20003': AuthenticationError, + '20005': AuthenticationError, + '20004': InvalidNonce, + '40020': InvalidOrder, + '40021': InvalidOrder, + '40025': ExchangeError, + '40013': OrderNotFound, + '40014': OrderNotFound, + '50008': PermissionDenied, + '50009': OrderNotFound, + '50010': OrderNotFound, + '60001': InsufficientFunds, + '60005': InvalidOrder, + }, + }, + }) + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for bitbank + + https://github.com/bitbankinc/bitbank-api-docs/blob/38d6d7c6f486c793872fd4b4087a0d090a04cd0a/rest-api.md#get-all-pairs-info + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = self.marketsGetSpotPairs(params) + # + # { + # "success": 1, + # "data": { + # "pairs": [ + # { + # "name": "btc_jpy", + # "base_asset": "btc", + # "quote_asset": "jpy", + # "maker_fee_rate_base": "0", + # "taker_fee_rate_base": "0", + # "maker_fee_rate_quote": "-0.0002", + # "taker_fee_rate_quote": "0.0012", + # "unit_amount": "0.0001", + # "limit_max_amount": "1000", + # "market_max_amount": "10", + # "market_allowance_rate": "0.2", + # "price_digits": 0, + # "amount_digits": 4, + # "is_enabled": True, + # "stop_order": False, + # "stop_order_and_cancel": False + # } + # ] + # } + # } + # + data = self.safe_value(response, 'data') + pairs = self.safe_value(data, 'pairs', []) + return self.parse_markets(pairs) + + def parse_market(self, entry) -> Market: + id = self.safe_string(entry, 'name') + baseId = self.safe_string(entry, 'base_asset') + quoteId = self.safe_string(entry, 'quote_asset') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + return { + 'id': id, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': self.safe_value(entry, 'is_enabled'), + 'contract': False, + 'linear': None, + 'inverse': None, + 'taker': self.safe_number(entry, 'taker_fee_rate_quote'), + 'maker': self.safe_number(entry, 'maker_fee_rate_quote'), + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(self.parse_precision(self.safe_string(entry, 'amount_digits'))), + 'price': self.parse_number(self.parse_precision(self.safe_string(entry, 'price_digits'))), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(entry, 'unit_amount'), + 'max': self.safe_number(entry, 'limit_max_amount'), + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + 'info': entry, + } + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + symbol = self.safe_symbol(None, market) + timestamp = self.safe_integer(ticker, 'timestamp') + last = self.safe_string(ticker, 'last') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': self.safe_string(ticker, 'buy'), + 'bidVolume': None, + 'ask': self.safe_string(ticker, 'sell'), + 'askVolume': None, + 'vwap': None, + 'open': None, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': self.safe_string(ticker, 'vol'), + 'quoteVolume': None, + 'info': ticker, + }, market) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://github.com/bitbankinc/bitbank-api-docs/blob/38d6d7c6f486c793872fd4b4087a0d090a04cd0a/public-api.md#ticker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + response = self.publicGetPairTicker(self.extend(request, params)) + data = self.safe_dict(response, 'data', {}) + return self.parse_ticker(data, market) + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://github.com/bitbankinc/bitbank-api-docs/blob/38d6d7c6f486c793872fd4b4087a0d090a04cd0a/public-api.md#depth + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + response = self.publicGetPairDepth(self.extend(request, params)) + orderbook = self.safe_value(response, 'data', {}) + timestamp = self.safe_integer(orderbook, 'timestamp') + return self.parse_order_book(orderbook, market['symbol'], timestamp) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades + # + # { + # "transaction_id": "1143247037", + # "side": "buy", + # "price": "3836025", + # "amount": "0.0005", + # "executed_at": "1694249441593" + # } + # + timestamp = self.safe_integer(trade, 'executed_at') + market = self.safe_market(None, market) + priceString = self.safe_string(trade, 'price') + amountString = self.safe_string(trade, 'amount') + id = self.safe_string_2(trade, 'transaction_id', 'trade_id') + takerOrMaker = self.safe_string(trade, 'maker_taker') + fee = None + feeCostString = self.safe_string(trade, 'fee_amount_quote') + if feeCostString is not None: + fee = { + 'currency': market['quote'], + 'cost': feeCostString, + } + orderId = self.safe_string(trade, 'order_id') + type = self.safe_string(trade, 'type') + side = self.safe_string(trade, 'side') + return self.safe_trade({ + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'id': id, + 'order': orderId, + 'type': type, + 'side': side, + 'takerOrMaker': takerOrMaker, + 'price': priceString, + 'amount': amountString, + 'cost': None, + 'fee': fee, + 'info': trade, + }, market) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://github.com/bitbankinc/bitbank-api-docs/blob/38d6d7c6f486c793872fd4b4087a0d090a04cd0a/public-api.md#transactions + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + response = self.publicGetPairTransactions(self.extend(request, params)) + data = self.safe_value(response, 'data', {}) + trades = self.safe_list(data, 'transactions', []) + return self.parse_trades(trades, market, since, limit) + + def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://github.com/bitbankinc/bitbank-api-docs/blob/38d6d7c6f486c793872fd4b4087a0d090a04cd0a/rest-api.md#get-all-pairs-info + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + self.load_markets() + response = self.marketsGetSpotPairs(params) + # + # { + # "success": "1", + # "data": { + # "pairs": [ + # { + # "name": "btc_jpy", + # "base_asset": "btc", + # "quote_asset": "jpy", + # "maker_fee_rate_base": "0", + # "taker_fee_rate_base": "0", + # "maker_fee_rate_quote": "-0.0002", + # "taker_fee_rate_quote": "0.0012", + # "unit_amount": "0.0001", + # "limit_max_amount": "1000", + # "market_max_amount": "10", + # "market_allowance_rate": "0.2", + # "price_digits": "0", + # "amount_digits": "4", + # "is_enabled": True, + # "stop_order": False, + # "stop_order_and_cancel": False + # }, + # ... + # ] + # } + # } + # + data = self.safe_value(response, 'data', {}) + pairs = self.safe_value(data, 'pairs', []) + result: dict = {} + for i in range(0, len(pairs)): + pair = pairs[i] + marketId = self.safe_string(pair, 'name') + market = self.safe_market(marketId) + symbol = market['symbol'] + result[symbol] = { + 'info': pair, + 'symbol': symbol, + 'maker': self.safe_number(pair, 'maker_fee_rate_quote'), + 'taker': self.safe_number(pair, 'taker_fee_rate_quote'), + 'percentage': True, + 'tierBased': False, + } + return result + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # [ + # "0.02501786", + # "0.02501786", + # "0.02501786", + # "0.02501786", + # "0.0000", + # 1591488000000 + # ] + # + return [ + self.safe_integer(ohlcv, 5), + self.safe_number(ohlcv, 0), + self.safe_number(ohlcv, 1), + self.safe_number(ohlcv, 2), + self.safe_number(ohlcv, 3), + self.safe_number(ohlcv, 4), + ] + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://github.com/bitbankinc/bitbank-api-docs/blob/38d6d7c6f486c793872fd4b4087a0d090a04cd0a/public-api.md#candlestick + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + if since is None: + if limit is None: + limit = 1000 # it doesn't have any defaults, might return 200, might 2000(i.e. https://public.bitbank.cc/btc_jpy/candlestick/4hour/2020) + duration = self.parse_timeframe(timeframe) + since = self.milliseconds() - duration * 1000 * limit + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + 'candletype': self.safe_string(self.timeframes, timeframe, timeframe), + 'yyyymmdd': self.yyyymmdd(since, ''), + } + response = self.publicGetPairCandlestickCandletypeYyyymmdd(self.extend(request, params)) + # + # { + # "success":1, + # "data":{ + # "candlestick":[ + # { + # "type":"5min", + # "ohlcv":[ + # ["0.02501786","0.02501786","0.02501786","0.02501786","0.0000",1591488000000], + # ["0.02501747","0.02501953","0.02501747","0.02501953","0.3017",1591488300000], + # ["0.02501762","0.02501762","0.02500392","0.02500392","0.1500",1591488600000], + # ] + # } + # ], + # "timestamp":1591508668190 + # } + # } + # + data = self.safe_value(response, 'data', {}) + candlestick = self.safe_value(data, 'candlestick', []) + first = self.safe_value(candlestick, 0, {}) + ohlcv = self.safe_list(first, 'ohlcv', []) + return self.parse_ohlcvs(ohlcv, market, timeframe, since, limit) + + def parse_balance(self, response) -> Balances: + result: dict = { + 'info': response, + 'timestamp': None, + 'datetime': None, + } + data = self.safe_value(response, 'data', {}) + assets = self.safe_value(data, 'assets', []) + for i in range(0, len(assets)): + balance = assets[i] + currencyId = self.safe_string(balance, 'asset') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(balance, 'free_amount') + account['used'] = self.safe_string(balance, 'locked_amount') + account['total'] = self.safe_string(balance, 'onhand_amount') + result[code] = account + return self.safe_balance(result) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://github.com/bitbankinc/bitbank-api-docs/blob/38d6d7c6f486c793872fd4b4087a0d090a04cd0a/rest-api.md#assets + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + response = self.privateGetUserAssets(params) + # + # { + # "success": "1", + # "data": { + # "assets": [ + # { + # "asset": "jpy", + # "amount_precision": "4", + # "onhand_amount": "0.0000", + # "locked_amount": "0.0000", + # "free_amount": "0.0000", + # "stop_deposit": False, + # "stop_withdrawal": False, + # "withdrawal_fee": { + # "threshold": "30000.0000", + # "under": "550.0000", + # "over": "770.0000" + # } + # }, + # { + # "asset": "btc", + # "amount_precision": "8", + # "onhand_amount": "0.00000000", + # "locked_amount": "0.00000000", + # "free_amount": "0.00000000", + # "stop_deposit": False, + # "stop_withdrawal": False, + # "withdrawal_fee": "0.00060000" + # }, + # ] + # } + # } + # + return self.parse_balance(response) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'UNFILLED': 'open', + 'PARTIALLY_FILLED': 'open', + 'FULLY_FILLED': 'closed', + 'CANCELED_UNFILLED': 'canceled', + 'CANCELED_PARTIALLY_FILLED': 'canceled', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + id = self.safe_string(order, 'order_id') + marketId = self.safe_string(order, 'pair') + market = self.safe_market(marketId, market) + timestamp = self.safe_integer(order, 'ordered_at') + price = self.safe_string(order, 'price') + amount = self.safe_string(order, 'start_amount') + filled = self.safe_string(order, 'executed_amount') + remaining = self.safe_string(order, 'remaining_amount') + average = self.safe_string(order, 'average_price') + status = self.parse_order_status(self.safe_string(order, 'status')) + type = self.safe_string_lower(order, 'type') + side = self.safe_string_lower(order, 'side') + return self.safe_order({ + 'id': id, + 'clientOrderId': None, + 'datetime': self.iso8601(timestamp), + 'timestamp': timestamp, + 'lastTradeTimestamp': None, + 'status': status, + 'symbol': market['symbol'], + 'type': type, + 'timeInForce': None, + 'postOnly': None, + 'side': side, + 'price': price, + 'triggerPrice': None, + 'cost': None, + 'average': average, + 'amount': amount, + 'filled': filled, + 'remaining': remaining, + 'trades': None, + 'fee': None, + 'info': order, + }, market) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://github.com/bitbankinc/bitbank-api-docs/blob/38d6d7c6f486c793872fd4b4087a0d090a04cd0a/rest-api.md#create-new-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + 'amount': self.amount_to_precision(symbol, amount), + 'side': side, + 'type': type, + } + if type == 'limit': + request['price'] = self.price_to_precision(symbol, price) + response = self.privatePostUserSpotOrder(self.extend(request, params)) + data = self.safe_dict(response, 'data') + return self.parse_order(data, market) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://github.com/bitbankinc/bitbank-api-docs/blob/38d6d7c6f486c793872fd4b4087a0d090a04cd0a/rest-api.md#cancel-order + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'order_id': id, + 'pair': market['id'], + } + response = self.privatePostUserSpotCancelOrder(self.extend(request, params)) + # + # { + # "success": 1, + # "data": { + # "order_id": 0, + # "pair": "string", + # "side": "string", + # "type": "string", + # "start_amount": "string", + # "remaining_amount": "string", + # "executed_amount": "string", + # "price": "string", + # "post_only": False, + # "average_price": "string", + # "ordered_at": 0, + # "expire_at": 0, + # "canceled_at": 0, + # "triggered_at": 0, + # "trigger_price": "string", + # "status": "string" + # } + # } + # + data = self.safe_value(response, 'data') + return self.parse_order(data) + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://github.com/bitbankinc/bitbank-api-docs/blob/38d6d7c6f486c793872fd4b4087a0d090a04cd0a/rest-api.md#fetch-order-information + + :param str id: the order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'order_id': id, + 'pair': market['id'], + } + response = self.privateGetUserSpotOrder(self.extend(request, params)) + # + # { + # "success": 1, + # "data": { + # "order_id": 0, + # "pair": "string", + # "side": "string", + # "type": "string", + # "start_amount": "string", + # "remaining_amount": "string", + # "executed_amount": "string", + # "price": "string", + # "post_only": False, + # "average_price": "string", + # "ordered_at": 0, + # "expire_at": 0, + # "triggered_at": 0, + # "triger_price": "string", + # "status": "string" + # } + # } + # + data = self.safe_dict(response, 'data') + return self.parse_order(data, market) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://github.com/bitbankinc/bitbank-api-docs/blob/38d6d7c6f486c793872fd4b4087a0d090a04cd0a/rest-api.md#fetch-active-orders + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + if limit is not None: + request['count'] = limit + if since is not None: + request['since'] = self.parse_to_int(since / 1000) + response = self.privateGetUserSpotActiveOrders(self.extend(request, params)) + data = self.safe_value(response, 'data', {}) + orders = self.safe_list(data, 'orders', []) + return self.parse_orders(orders, market, since, limit) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://github.com/bitbankinc/bitbank-api-docs/blob/38d6d7c6f486c793872fd4b4087a0d090a04cd0a/rest-api.md#fetch-trade-history + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['pair'] = market['id'] + if limit is not None: + request['count'] = limit + if since is not None: + request['since'] = self.parse_to_int(since / 1000) + response = self.privateGetUserSpotTradeHistory(self.extend(request, params)) + data = self.safe_value(response, 'data', {}) + trades = self.safe_list(data, 'trades', []) + return self.parse_trades(trades, market, since, limit) + + def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://github.com/bitbankinc/bitbank-api-docs/blob/38d6d7c6f486c793872fd4b4087a0d090a04cd0a/rest-api.md#get-withdrawal-accounts + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'asset': currency['id'], + } + response = self.privateGetUserWithdrawalAccount(self.extend(request, params)) + data = self.safe_value(response, 'data', {}) + # Not sure about self if there could be more than one account... + accounts = self.safe_value(data, 'accounts', []) + firstAccount = self.safe_value(accounts, 0, {}) + address = self.safe_string(firstAccount, 'address') + return { + 'info': response, + 'currency': currency['code'], + 'network': None, + 'address': address, + 'tag': None, + } + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://github.com/bitbankinc/bitbank-api-docs/blob/38d6d7c6f486c793872fd4b4087a0d090a04cd0a/rest-api.md#new-withdrawal-request + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + if not ('uuid' in params): + raise ExchangeError(self.id + ' uuid is required for withdrawal') + self.load_markets() + currency = self.currency(code) + request: dict = { + 'asset': currency['id'], + 'amount': amount, + } + response = self.privatePostUserRequestWithdrawal(self.extend(request, params)) + # + # { + # "success": 1, + # "data": { + # "uuid": "string", + # "asset": "btc", + # "amount": 0, + # "account_uuid": "string", + # "fee": 0, + # "status": "DONE", + # "label": "string", + # "txid": "string", + # "address": "string", + # "requested_at": 0 + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_transaction(data, currency) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # withdraw + # + # { + # "uuid": "string", + # "asset": "btc", + # "amount": 0, + # "account_uuid": "string", + # "fee": 0, + # "status": "DONE", + # "label": "string", + # "txid": "string", + # "address": "string", + # "requested_at": 0 + # } + # + txid = self.safe_string(transaction, 'txid') + currency = self.safe_currency(None, currency) + return { + 'id': txid, + 'txid': txid, + 'timestamp': None, + 'datetime': None, + 'network': None, + 'addressFrom': None, + 'address': None, + 'addressTo': None, + 'amount': None, + 'type': None, + 'currency': currency['code'], + 'status': None, + 'updated': None, + 'tagFrom': None, + 'tag': None, + 'tagTo': None, + 'comment': None, + 'internal': None, + 'fee': None, + 'info': transaction, + } + + def nonce(self): + return self.milliseconds() + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + query = self.omit(params, self.extract_params(path)) + url = self.implode_hostname(self.urls['api'][api]) + '/' + if (api == 'public') or (api == 'markets'): + url += self.implode_params(path, params) + if query: + url += '?' + self.urlencode(query) + else: + self.check_required_credentials() + nonce = str(self.nonce()) + auth = nonce + url += self.version + '/' + self.implode_params(path, params) + if method == 'POST': + body = self.json(query) + auth += body + else: + auth += '/' + self.version + '/' + path + if query: + query = self.urlencode(query) + url += '?' + query + auth += '?' + query + headers = { + 'Content-Type': 'application/json', + 'ACCESS-KEY': self.apiKey, + 'ACCESS-NONCE': nonce, + 'ACCESS-SIGNATURE': self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256), + } + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None + success = self.safe_integer(response, 'success') + data = self.safe_value(response, 'data') + if not success or not data: + errorMessages: dict = { + '10000': 'URL does not exist', + '10001': 'A system error occurred. Please contact support', + '10002': 'Invalid JSON format. Please check the contents of transmission', + '10003': 'A system error occurred. Please contact support', + '10005': 'A timeout error occurred. Please wait for a while and try again', + '20001': 'API authentication failed', + '20002': 'Illegal API key', + '20003': 'API key does not exist', + '20004': 'API Nonce does not exist', + '20005': 'API signature does not exist', + '20011': 'Two-step verification failed', + '20014': 'SMS authentication failed', + '30001': 'Please specify the order quantity', + '30006': 'Please specify the order ID', + '30007': 'Please specify the order ID array', + '30009': 'Please specify the stock', + '30012': 'Please specify the order price', + '30013': 'Trade Please specify either', + '30015': 'Please specify the order type', + '30016': 'Please specify asset name', + '30019': 'Please specify uuid', + '30039': 'Please specify the amount to be withdrawn', + '40001': 'The order quantity is invalid', + '40006': 'Count value is invalid', + '40007': 'End time is invalid', + '40008': 'end_id Value is invalid', + '40009': 'The from_id value is invalid', + '40013': 'The order ID is invalid', + '40014': 'The order ID array is invalid', + '40015': 'Too many specified orders', + '40017': 'Incorrect issue name', + '40020': 'The order price is invalid', + '40021': 'The trading classification is invalid', + '40022': 'Start date is invalid', + '40024': 'The order type is invalid', + '40025': 'Incorrect asset name', + '40028': 'uuid is invalid', + '40048': 'The amount of withdrawal is illegal', + '50003': 'Currently, self account is in a state where you can not perform the operation you specified. Please contact support', + '50004': 'Currently, self account is temporarily registered. Please try again after registering your account', + '50005': 'Currently, self account is locked. Please contact support', + '50006': 'Currently, self account is locked. Please contact support', + '50008': 'User identification has not been completed', + '50009': 'Your order does not exist', + '50010': 'Can not cancel specified order', + '50011': 'API not found', + '60001': 'The number of possessions is insufficient', + '60002': 'It exceeds the quantity upper limit of the tender buying order', + '60003': 'The specified quantity exceeds the limit', + '60004': 'The specified quantity is below the threshold', + '60005': 'The specified price is above the limit', + '60006': 'The specified price is below the lower limit', + '70001': 'A system error occurred. Please contact support', + '70002': 'A system error occurred. Please contact support', + '70003': 'A system error occurred. Please contact support', + '70004': 'We are unable to accept orders transaction is currently suspended', + '70005': 'Order can not be accepted because purchase order is currently suspended', + '70006': 'We can not accept orders because we are currently unsubscribed ', + '70009': 'We are currently temporarily restricting orders to be carried out. Please use the limit order.', + '70010': 'We are temporarily raising the minimum order quantity system load is now rising.', + } + code = self.safe_string(data, 'code') + message = self.safe_string(errorMessages, code, 'Error') + self.throw_exactly_matched_exception(self.exceptions['exact'], code, message) + raise ExchangeError(self.id + ' ' + self.json(response)) + return None diff --git a/ccxt/bitbns.py b/ccxt/bitbns.py new file mode 100644 index 0000000..ce9b6e3 --- /dev/null +++ b/ccxt/bitbns.py @@ -0,0 +1,1229 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.bitbns import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currency, DepositAddress, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import OrderNotFound +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class bitbns(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(bitbns, self).describe(), { + 'id': 'bitbns', + 'name': 'Bitbns', + 'countries': ['IN'], # India + 'rateLimit': 1000, + 'certified': False, + 'version': 'v2', + # new metainfo interface + 'has': { + 'CORS': None, + 'spot': True, + 'margin': None, # has but unimplemented + 'swap': False, + 'future': False, + 'option': None, # coming soon + 'cancelAllOrders': False, + 'cancelOrder': True, + 'createOrder': True, + 'createStopOrder': True, + 'createTriggerOrder': True, + 'fechCurrencies': False, + 'fetchBalance': True, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchFundingHistory': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchIndexOHLCV': False, + 'fetchMarginMode': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMyTrades': True, + 'fetchOHLCV': False, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchPositionMode': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchStatus': True, + 'fetchTicker': 'emulated', + 'fetchTickers': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': False, + 'fetchTransfer': False, + 'fetchTransfers': False, + 'fetchWithdrawal': False, + 'fetchWithdrawals': True, + 'transfer': False, + 'withdraw': False, + }, + 'hostname': 'bitbns.com', + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/a5b9a562-cdd8-4bea-9fa7-fd24c1dad3d9', + 'api': { + 'www': 'https://{hostname}', + 'v1': 'https://api.{hostname}/api/trade/v1', + 'v2': 'https://api.{hostname}/api/trade/v2', + }, + 'www': 'https://bitbns.com', + 'referral': 'https://ref.bitbns.com/1090961', + 'doc': [ + 'https://bitbns.com/trade/#/api-trading/', + ], + 'fees': 'https://bitbns.com/fees', + }, + 'api': { + 'www': { + 'get': [ + 'order/fetchMarkets', + 'order/fetchTickers', + 'order/fetchOrderbook', + 'order/getTickerWithVolume', + 'exchangeData/ohlc', # ?coin=${coin_name}&page=${page} + 'exchangeData/orderBook', + 'exchangeData/tradedetails', + ], + }, + 'v1': { + 'get': [ + 'platform/status', + 'tickers', + 'orderbook/sell/{symbol}', + 'orderbook/buy/{symbol}', + ], + 'post': [ + 'currentCoinBalance/EVERYTHING', + 'getApiUsageStatus/USAGE', + 'getOrderSocketToken/USAGE', + 'currentCoinBalance/{symbol}', + 'orderStatus/{symbol}', + 'depositHistory/{symbol}', + 'withdrawHistory/{symbol}', + 'withdrawHistoryAll/{symbol}', + 'depositHistoryAll/{symbol}', + 'listOpenOrders/{symbol}', + 'listOpenStopOrders/{symbol}', + 'getCoinAddress/{symbol}', + 'placeSellOrder/{symbol}', + 'placeBuyOrder/{symbol}', + 'buyStopLoss/{symbol}', + 'sellStopLoss/{symbol}', + 'cancelOrder/{symbol}', + 'cancelStopLossOrder/{symbol}', + 'listExecutedOrders/{symbol}', + 'placeMarketOrder/{symbol}', + 'placeMarketOrderQnty/{symbol}', + ], + }, + 'v2': { + 'post': [ + 'orders', + 'cancel', + 'getordersnew', + 'marginOrders', + ], + }, + }, + 'fees': { + 'trading': { + 'feeSide': 'quote', + 'tierBased': False, + 'percentage': True, + 'taker': self.parse_number('0.0025'), + 'maker': self.parse_number('0.0025'), + }, + }, + 'precisionMode': TICK_SIZE, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, # todo with triggerPrice + 'takeProfitPrice': False, # todo with triggerPrice + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': False, + 'FOK': False, + 'PO': False, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, # todo recheck + 'leverage': False, + 'marketBuyRequiresPrice': False, + 'marketBuyByCost': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': None, + 'daysBack': None, + 'untilDays': None, + 'symbolRequired': True, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': True, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOrders': None, + 'fetchClosedOrders': None, + # todo: implement fetchOHLCV + 'fetchOHLCV': { + 'limit': 100, + }, + }, + # todo: implement swap methods + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'exceptions': { + 'exact': { + '400': BadRequest, # {"msg":"Invalid Request","status":-1,"code":400} + '409': BadSymbol, # {"data":"","status":0,"error":"coin name not supplied or not yet supported","code":409} + '416': InsufficientFunds, # {"data":"Oops ! Not sufficient currency to sell","status":0,"error":null,"code":416} + '417': OrderNotFound, # {"data":[],"status":0,"error":"Nothing to show","code":417} + }, + 'broad': {}, + }, + }) + + def fetch_status(self, params={}): + """ + the latest known information on the availability of the exchange API + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `status structure ` + """ + response = self.v1GetPlatformStatus(params) + # + # { + # "data":{ + # "BTC":{"status":1}, + # "ETH":{"status":1}, + # "XRP":{"status":1}, + # }, + # "status":1, + # "error":null, + # "code":200 + # } + # + statusRaw = self.safe_string(response, 'status') + return { + 'status': self.safe_string({'1': 'ok'}, statusRaw, statusRaw), + 'updated': None, + 'eta': None, + 'url': None, + 'info': response, + } + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for bitbns + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = self.wwwGetOrderFetchMarkets(params) + # + # [ + # { + # "id":"BTC", + # "symbol":"BTC/INR", + # "base":"BTC", + # "quote":"INR", + # "baseId":"BTC", + # "quoteId":"", + # "active":true, + # "limits":{ + # "amount":{"min":"0.00017376","max":20}, + # "price":{"min":2762353.2359999996,"max":6445490.883999999}, + # "cost":{"min":800,"max":128909817.67999998} + # }, + # "precision":{ + # "amount":8, + # "price":2 + # }, + # "info":{} + # }, + # ] + # + result = [] + for i in range(0, len(response)): + market = response[i] + id = self.safe_string(market, 'id') + baseId = self.safe_string(market, 'base') + quoteId = self.safe_string(market, 'quote') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + marketPrecision = self.safe_dict(market, 'precision', {}) + marketLimits = self.safe_dict(market, 'limits', {}) + amountLimits = self.safe_dict(marketLimits, 'amount', {}) + priceLimits = self.safe_dict(marketLimits, 'price', {}) + costLimits = self.safe_dict(marketLimits, 'cost', {}) + usdt = (quoteId == 'USDT') + # INR markets don't need a _INR prefix + uppercaseId = (baseId + '_' + quoteId) if usdt else baseId + result.append({ + 'id': id, + 'uppercaseId': uppercaseId, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': self.safe_bool(market, 'active'), + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(self.parse_precision(self.safe_string(marketPrecision, 'amount'))), + 'price': self.parse_number(self.parse_precision(self.safe_string(marketPrecision, 'price'))), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(amountLimits, 'min'), + 'max': self.safe_number(amountLimits, 'max'), + }, + 'price': { + 'min': self.safe_number(priceLimits, 'min'), + 'max': self.safe_number(priceLimits, 'max'), + }, + 'cost': { + 'min': self.safe_number(costLimits, 'min'), + 'max': self.safe_number(costLimits, 'max'), + }, + }, + 'created': None, + 'info': market, + }) + return result + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['limit'] = limit # default 100, max 5000, see https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md#order-book + response = self.wwwGetOrderFetchOrderbook(self.extend(request, params)) + # + # { + # "bids":[ + # [49352.04,0.843948], + # [49352.03,0.742048], + # [49349.78,0.686239], + # ], + # "asks":[ + # [49443.59,0.065137], + # [49444.63,0.098211], + # [49449.01,0.066309], + # ], + # "timestamp":1619172786577, + # "datetime":"2021-04-23T10:13:06.577Z", + # "nonce":"" + # } + # + timestamp = self.safe_integer(response, 'timestamp') + return self.parse_order_book(response, market['symbol'], timestamp) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "symbol":"BTC/INR", + # "info":{ + # "highest_buy_bid":4368494.31, + # "lowest_sell_bid":4374835.09, + # "last_traded_price":4374835.09, + # "yes_price":4531016.27, + # "volume":{"max":"4569119.23","min":"4254552.13","volume":62.17722344} + # }, + # "timestamp":1619100020845, + # "datetime":1619100020845, + # "high":"4569119.23", + # "low":"4254552.13", + # "bid":4368494.31, + # "bidVolume":"", + # "ask":4374835.09, + # "askVolume":"", + # "vwap":"", + # "open":4531016.27, + # "close":4374835.09, + # "last":4374835.09, + # "baseVolume":62.17722344, + # "quoteVolume":"", + # "previousClose":"", + # "change":-156181.1799999997, + # "percentage":-3.446934874943623, + # "average":4452925.68 + # } + # + timestamp = self.safe_integer(ticker, 'timestamp') + marketId = self.safe_string(ticker, 'symbol') + symbol = self.safe_symbol(marketId, market) + last = self.safe_string(ticker, 'last') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': self.safe_string(ticker, 'bid'), + 'bidVolume': self.safe_string(ticker, 'bidVolume'), + 'ask': self.safe_string(ticker, 'ask'), + 'askVolume': self.safe_string(ticker, 'askVolume'), + 'vwap': self.safe_string(ticker, 'vwap'), + 'open': self.safe_string(ticker, 'open'), + 'close': last, + 'last': last, + 'previousClose': self.safe_string(ticker, 'previousClose'), # previous day close + 'change': self.safe_string(ticker, 'change'), + 'percentage': self.safe_string(ticker, 'percentage'), + 'average': self.safe_string(ticker, 'average'), + 'baseVolume': self.safe_string(ticker, 'baseVolume'), + 'quoteVolume': self.safe_string(ticker, 'quoteVolume'), + 'info': ticker, + }, market) + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + response = self.wwwGetOrderFetchTickers(params) + # + # { + # "BTC/INR":{ + # "symbol":"BTC/INR", + # "info":{ + # "highest_buy_bid":4368494.31, + # "lowest_sell_bid":4374835.09, + # "last_traded_price":4374835.09, + # "yes_price":4531016.27, + # "volume":{"max":"4569119.23","min":"4254552.13","volume":62.17722344} + # }, + # "timestamp":1619100020845, + # "datetime":1619100020845, + # "high":"4569119.23", + # "low":"4254552.13", + # "bid":4368494.31, + # "bidVolume":"", + # "ask":4374835.09, + # "askVolume":"", + # "vwap":"", + # "open":4531016.27, + # "close":4374835.09, + # "last":4374835.09, + # "baseVolume":62.17722344, + # "quoteVolume":"", + # "previousClose":"", + # "change":-156181.1799999997, + # "percentage":-3.446934874943623, + # "average":4452925.68 + # } + # } + # + return self.parse_tickers(response, symbols) + + def parse_balance(self, response) -> Balances: + timestamp = None + result: dict = { + 'info': response, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + data = self.safe_dict(response, 'data', {}) + keys = list(data.keys()) + for i in range(0, len(keys)): + key = keys[i] + parts = key.split('availableorder') + numParts = len(parts) + if numParts > 1: + currencyId = self.safe_string(parts, 1) + # note that "Money" stands for INR - the only fiat in bitbns + account = self.account() + account['free'] = self.safe_string(data, key) + account['used'] = self.safe_string(data, 'inorder' + currencyId) + if currencyId == 'Money': + currencyId = 'INR' + code = self.safe_currency_code(currencyId) + result[code] = account + return self.safe_balance(result) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + response = self.v1PostCurrentCoinBalanceEVERYTHING(params) + # + # { + # "data":{ + # "availableorderMoney":12.34, # INR + # "availableorderBTC":0, + # "availableorderXRP":0, + # "inorderMoney":0, # INR + # "inorderBTC":0, + # "inorderXRP":0, + # "inorderNEO":0, + # }, + # "status":1, + # "error":null, + # "code":200 + # } + # + # note that "Money" stands for INR - the only fiat in bitbns + return self.parse_balance(response) + + def parse_status(self, status): + statuses: dict = { + '-1': 'cancelled', + '0': 'open', + '1': 'open', + '2': 'done', + # 'PARTIALLY_FILLED': 'open', + # 'FILLED': 'closed', + # 'CANCELED': 'canceled', + # 'PENDING_CANCEL': 'canceling', # currently unused + # 'REJECTED': 'rejected', + # 'EXPIRED': 'expired', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder + # + # { + # "data": "Successfully placed bid to purchase currency", + # "status": 1, + # "error": null, + # "id": 5424475, + # "code": 200 + # } + # + # fetchOpenOrders, fetchOrder + # + # { + # "entry_id": 5424475, + # "btc": 0.01, + # "rate": 2000, + # "time": "2021-04-25T17:05:42.000Z", + # "type": 0, + # "status": 0 + # "t_rate": 0.45, # only stop orders + # "trail": 0 # only stop orders + # } + # + # cancelOrder + # + # { + # "data": "Successfully cancelled the order", + # "status": 1, + # "error": null, + # "code": 200 + # } + # + id = self.safe_string_2(order, 'id', 'entry_id') + datetime = self.safe_string(order, 'time') + triggerPrice = self.safe_string(order, 't_rate') + side = self.safe_string(order, 'type') + if side == '0': + side = 'buy' + elif side == '1': + side = 'sell' + data = self.safe_string(order, 'data') + status = self.safe_string(order, 'status') + if data == 'Successfully cancelled the order': + status = 'cancelled' + else: + status = self.parse_status(status) + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': None, + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + 'lastTradeTimestamp': None, + 'symbol': self.safe_string(market, 'symbol'), + 'timeInForce': None, + 'postOnly': None, + 'side': side, + 'price': self.safe_string(order, 'rate'), + 'triggerPrice': triggerPrice, + 'amount': self.safe_string(order, 'btc'), + 'cost': None, + 'average': None, + 'filled': None, + 'remaining': None, + 'status': status, + 'fee': { + 'cost': None, + 'currency': None, + 'rate': None, + }, + 'trades': None, + }, market) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://docs.bitbns.com/bitbns/rest-endpoints/order-apis/version-2/place-orders + https://docs.bitbns.com/bitbns/rest-endpoints/order-apis/version-1/market-orders-quantity # market orders + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: the price at which a trigger order is triggered at + + EXCHANGE SPECIFIC PARAMETERS + :param float [params.target_rate]: *requires params.trail_rate when set, type must be 'limit'* a bracket order is placed when set + :param float [params.trail_rate]: *requires params.target_rate when set, type must be 'limit'* a bracket order is placed when set + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + triggerPrice = self.safe_string_n(params, ['triggerPrice', 'stopPrice', 't_rate']) + targetRate = self.safe_string(params, 'target_rate') + trailRate = self.safe_string(params, 'trail_rate') + params = self.omit(params, ['triggerPrice', 'stopPrice', 'trail_rate', 'target_rate', 't_rate']) + request: dict = { + 'side': side.upper(), + 'symbol': market['uppercaseId'], + 'quantity': self.amount_to_precision(symbol, amount), + # 'target_rate': self.price_to_precision(symbol, targetRate), + # 't_rate': self.price_to_precision(symbol, stopPrice), + # 'trail_rate': self.price_to_precision(symbol, trailRate), + } + method = 'v2PostOrders' + if type == 'limit': + request['rate'] = self.price_to_precision(symbol, price) + else: + method = 'v1PostPlaceMarketOrderQntySymbol' + request['market'] = market['quoteId'] + if triggerPrice is not None: + request['t_rate'] = self.price_to_precision(symbol, triggerPrice) + if targetRate is not None: + request['target_rate'] = self.price_to_precision(symbol, targetRate) + if trailRate is not None: + request['trail_rate'] = self.price_to_precision(symbol, trailRate) + response = getattr(self, method)(self.extend(request, params)) + # + # { + # "data":"Successfully placed bid to purchase currency", + # "status":1, + # "error":null, + # "id":5424475, + # "code":200 + # } + # + return self.parse_order(response, market) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://docs.bitbns.com/bitbns/rest-endpoints/order-apis/version-2/cancel-orders + https://docs.bitbns.com/bitbns/rest-endpoints/order-apis/version-1/cancel-stop-loss-orders + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: True if cancelling a trigger order + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + isTrigger = self.safe_bool_2(params, 'trigger', 'stop') + params = self.omit(params, ['trigger', 'stop']) + request: dict = { + 'entry_id': id, + 'symbol': market['uppercaseId'], + } + response = None + tail = 'StopLossOrder' if isTrigger else 'Order' + quoteSide = 'usdtcancel' if (market['quoteId'] == 'USDT') else 'cancel' + quoteSide += tail + request['side'] = quoteSide + response = self.v2PostCancel(self.extend(request, params)) + return self.parse_order(response, market) + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://docs.bitbns.com/bitbns/rest-endpoints/order-apis/version-1/order-status + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrder() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'entry_id': id, + } + trigger = self.safe_bool_2(params, 'trigger', 'stop') + if trigger: + raise BadRequest(self.id + ' fetchOrder cannot fetch stop orders') + response = self.v1PostOrderStatusSymbol(self.extend(request, params)) + # + # { + # "data":[ + # { + # "entry_id":5424475, + # "btc":0.01, + # "rate":2000, + # "time":"2021-04-25T17:05:42.000Z", + # "type":0, + # "status":0, + # "total":0.01, + # "avg_cost":null, + # "side":"BUY", + # "amount":0.01, + # "remaining":0.01, + # "filled":0, + # "cost":null, + # "fee":0.05 + # } + # ], + # "status":1, + # "error":null, + # "code":200 + # } + # + data = self.safe_list(response, 'data', []) + first = self.safe_dict(data, 0) + return self.parse_order(first, market) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://docs.bitbns.com/bitbns/rest-endpoints/order-apis/version-2/order-status-limit + https://docs.bitbns.com/bitbns/rest-endpoints/order-apis/version-2/order-status-limit/order-status-stop-limit + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: True if fetching trigger orders + :returns Order[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOpenOrders() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + isTrigger = self.safe_bool_2(params, 'trigger', 'stop') + params = self.omit(params, ['trigger', 'stop']) + quoteSide = 'usdtListOpen' if (market['quoteId'] == 'USDT') else 'listOpen' + request: dict = { + 'symbol': market['uppercaseId'], + 'page': 0, + 'side': (quoteSide + 'StopOrders') if isTrigger else (quoteSide + 'Orders'), + } + response = self.v2PostGetordersnew(self.extend(request, params)) + # + # { + # "data":[ + # { + # "entry_id":5424475, + # "btc":0.01, + # "rate":2000, + # "time":"2021-04-25T17:05:42.000Z", + # "type":0, + # "status":0 + # "t_rate":0.45, # only stop orders + # "type":1, # only stop orders + # "trail":0 # only stop orders + # } + # ], + # "status":1, + # "error":null, + # "code":200 + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_orders(data, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchMyTrades + # + # { + # "type": "BTC Sell order executed", + # "typeI": 6, + # "crypto": 5000, + # "amount": 35.4, + # "rate": 709800, + # "date": "2020-05-22T15:05:34.000Z", + # "unit": "INR", + # "factor": 100000000, + # "fee": 0.09, + # "delh_btc": -5000, + # "delh_inr": 0, + # "del_btc": 0, + # "del_inr": 35.4, + # "id": "2938823" + # } + # + # fetchTrades + # + # { + # "tradeId":"1909151", + # "price":"61904.6300", + # "quote_volume":1618.05, + # "base_volume":0.02607254, + # "timestamp":1634548602000, + # "type":"buy" + # } + # + market = self.safe_market(None, market) + orderId = self.safe_string_2(trade, 'id', 'tradeId') + timestamp = self.parse8601(self.safe_string(trade, 'date')) + timestamp = self.safe_integer(trade, 'timestamp', timestamp) + priceString = self.safe_string_2(trade, 'rate', 'price') + amountString = self.safe_string(trade, 'amount') + side = self.safe_string_lower(trade, 'type') + if side is not None: + if side.find('buy') >= 0: + side = 'buy' + elif side.find('sell') >= 0: + side = 'sell' + factor = self.safe_string(trade, 'factor') + costString = None + if factor is not None: + amountString = Precise.string_div(amountString, factor) + else: + amountString = self.safe_string(trade, 'base_volume') + costString = self.safe_string(trade, 'quote_volume') + symbol = market['symbol'] + fee = None + feeCostString = self.safe_string(trade, 'fee') + if feeCostString is not None: + feeCurrencyCode = market['quote'] + fee = { + 'cost': feeCostString, + 'currency': feeCurrencyCode, + } + return self.safe_trade({ + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'id': orderId, + 'order': orderId, + 'type': None, + 'side': side, + 'takerOrMaker': None, + 'price': priceString, + 'amount': amountString, + 'cost': costString, + 'fee': fee, + }, market) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMyTrades() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'page': 0, + } + if since is not None: + request['since'] = self.iso8601(since) + response = self.v1PostListExecutedOrdersSymbol(self.extend(request, params)) + # + # { + # "data": [ + # { + # "type": "BTC Sell order executed", + # "typeI": 6, + # "crypto": 5000, + # "amount": 35.4, + # "rate": 709800, + # "date": "2020-05-22T15:05:34.000Z", + # "unit": "INR", + # "factor": 100000000, + # "fee": 0.09, + # "delh_btc": -5000, + # "delh_inr": 0, + # "del_btc": 0, + # "del_inr": 35.4, + # "id": "2938823" + # }, + # { + # "type": "BTC Sell order executed", + # "typeI": 6, + # "crypto": 195000, + # "amount": 1380.58, + # "rate": 709765.5, + # "date": "2020-05-22T15:05:34.000Z", + # "unit": "INR", + # "factor": 100000000, + # "fee": 3.47, + # "delh_btc": -195000, + # "delh_inr": 0, + # "del_btc": 0, + # "del_inr": 1380.58, + # "id": "2938823" + # } + # ], + # "status": 1, + # "error": null, + # "code": 200 + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_trades(data, market, since, limit) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchTrades() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'coin': market['baseId'], + 'market': market['quoteId'], + } + response = self.wwwGetExchangeDataTradedetails(self.extend(request, params)) + # + # [ + # {"tradeId":"1909151","price":"61904.6300","quote_volume":1618.05,"base_volume":0.02607254,"timestamp":1634548602000,"type":"buy"}, + # {"tradeId":"1909153","price":"61893.9000","quote_volume":16384.42,"base_volume":0.26405767,"timestamp":1634548999000,"type":"sell"}, + # {"tradeId":"1909155","price":"61853.1100","quote_volume":2304.37,"base_volume":0.03716263,"timestamp":1634549670000,"type":"sell"} + # } + # + return self.parse_trades(response, market, since, limit) + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + if code is None: + raise ArgumentsRequired(self.id + ' fetchDeposits() requires a currency code argument') + self.load_markets() + currency = self.currency(code) + request: dict = { + 'symbol': currency['id'], + 'page': 0, + } + response = self.v1PostDepositHistorySymbol(self.extend(request, params)) + # + # { + # "data":[ + # { + # "type":"USDT deposited", + # "typeI":1, + # "amount":100, + # "date":"2021-04-24T14:56:04.000Z", + # "unit":"USDT", + # "factor":100, + # "fee":0, + # "delh_btc":0, + # "delh_inr":0, + # "rate":0, + # "del_btc":10000, + # "del_inr":0 + # } + # ], + # "status":1, + # "error":null, + # "code":200 + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_transactions(data, currency, since, limit) + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + if code is None: + raise ArgumentsRequired(self.id + ' fetchWithdrawals() requires a currency code argument') + self.load_markets() + currency = self.currency(code) + request: dict = { + 'symbol': currency['id'], + 'page': 0, + } + response = self.v1PostWithdrawHistorySymbol(self.extend(request, params)) + # + # ... + # + data = self.safe_list(response, 'data', []) + return self.parse_transactions(data, currency, since, limit) + + def parse_transaction_status_by_type(self, status, type=None): + statusesByType: dict = { + 'deposit': { + '0': 'pending', + '1': 'ok', + }, + 'withdrawal': { + '0': 'pending', # Email Sent + '1': 'canceled', # Cancelled(different from 1 = ok in deposits) + '2': 'pending', # Awaiting Approval + '3': 'failed', # Rejected + '4': 'pending', # Processing + '5': 'failed', # Failure + '6': 'ok', # Completed + }, + } + statuses = self.safe_dict(statusesByType, type, {}) + return self.safe_string(statuses, status, status) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fetchDeposits + # + # { + # "type":"USDT deposited", + # "typeI":1, + # "amount":100, + # "date":"2021-04-24T14:56:04.000Z", + # "unit":"USDT", + # "factor":100, + # "fee":0, + # "delh_btc":0, + # "delh_inr":0, + # "rate":0, + # "del_btc":10000, + # "del_inr":0 + # } + # + # fetchWithdrawals + # + # ... + # + currencyId = self.safe_string(transaction, 'unit') + code = self.safe_currency_code(currencyId, currency) + timestamp = self.parse8601(self.safe_string_2(transaction, 'date', 'timestamp')) + type = self.safe_string(transaction, 'type') + expTime = self.safe_string(transaction, 'expTime', '') + status = None + if type is not None: + if type.find('deposit') >= 0: + type = 'deposit' + status = 'ok' + elif type.find('withdraw') >= 0 or expTime.find('withdraw') >= 0: + type = 'withdrawal' + # status = self.parse_transaction_status_by_type(self.safe_string(transaction, 'status'), type) + amount = self.safe_number(transaction, 'amount') + feeCost = self.safe_number(transaction, 'fee') + fee = None + if feeCost is not None: + fee = {'currency': code, 'cost': feeCost} + return { + 'info': transaction, + 'id': None, + 'txid': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': None, + 'address': None, + 'addressTo': None, + 'addressFrom': None, + 'tag': None, + 'tagTo': None, + 'tagFrom': None, + 'type': type, + 'amount': amount, + 'currency': code, + 'status': status, + 'updated': None, + 'comment': None, + 'internal': None, + 'fee': fee, + } + + def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'symbol': currency['id'], + } + response = self.v1PostGetCoinAddressSymbol(self.extend(request, params)) + # + # { + # "data":{ + # "token":"0x680dee9edfff0c397736e10b017cf6a0aee4ba31", + # "expiry":"2022-04-24 22:30:11" + # }, + # "status":1, + # "error":null + # } + # + data = self.safe_dict(response, 'data', {}) + address = self.safe_string(data, 'token') + tag = self.safe_string(data, 'tag') + self.check_address(address) + return { + 'info': response, + 'currency': code, + 'network': None, + 'address': address, + 'tag': tag, + } + + def nonce(self): + return self.milliseconds() + + def sign(self, path, api='www', method='GET', params={}, headers=None, body=None): + urls = self.urls + if not (api in urls['api']): + raise ExchangeError(self.id + ' does not have a testnet/sandbox URL for ' + api + ' endpoints') + if api != 'www': + self.check_required_credentials() + headers = { + 'X-BITBNS-APIKEY': self.apiKey, + } + baseUrl = self.implode_hostname(self.urls['api'][api]) + url = baseUrl + '/' + self.implode_params(path, params) + query = self.omit(params, self.extract_params(path)) + nonce = str(self.nonce()) + if method == 'GET': + if query: + url += '?' + self.urlencode(query) + elif method == 'POST': + if query: + body = self.json(query) + else: + body = '{}' + auth: dict = { + 'timeStamp_nonce': nonce, + 'body': body, + } + payload = self.string_to_base64(self.json(auth)) + signature = self.hmac(self.encode(payload), self.encode(self.secret), hashlib.sha512) + headers['X-BITBNS-PAYLOAD'] = payload + headers['X-BITBNS-SIGNATURE'] = signature + headers['Content-Type'] = 'application/x-www-form-urlencoded' + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None # fallback to default error handler + # + # {"msg":"Invalid Request","status":-1,"code":400} + # {"data":[],"status":0,"error":"Nothing to show","code":417} + # + code = self.safe_string(response, 'code') + message = self.safe_string(response, 'msg') + error = (code is not None) and (code != '200') and (code != '204') + if error or (message is not None): + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], code, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + raise ExchangeError(feedback) # unknown message + return None diff --git a/ccxt/bitfinex.py b/ccxt/bitfinex.py new file mode 100644 index 0000000..099476b --- /dev/null +++ b/ccxt/bitfinex.py @@ -0,0 +1,3796 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.bitfinex import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currencies, Currency, DepositAddress, Int, LedgerEntry, MarginModification, Market, Num, Order, OrderBook, OrderRequest, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, FundingRates, Trade, TradingFees, Transaction, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import NotSupported +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.errors import OnMaintenance +from ccxt.base.errors import InvalidNonce +from ccxt.base.decimal_to_precision import ROUND +from ccxt.base.decimal_to_precision import TRUNCATE +from ccxt.base.decimal_to_precision import DECIMAL_PLACES +from ccxt.base.decimal_to_precision import SIGNIFICANT_DIGITS +from ccxt.base.precise import Precise + + +class bitfinex(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(bitfinex, self).describe(), { + 'id': 'bitfinex', + 'name': 'Bitfinex', + 'countries': ['VG'], + 'version': 'v2', + 'certified': False, + 'pro': True, + # new metainfo interface + 'has': { + 'CORS': None, + 'spot': True, + 'margin': True, + 'swap': True, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'cancelOrders': True, + 'createDepositAddress': True, + 'createLimitOrder': True, + 'createMarketOrder': True, + 'createOrder': True, + 'createPostOnlyOrder': True, + 'createReduceOnlyOrder': True, + 'createStopLimitOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'createTrailingAmountOrder': True, + 'createTrailingPercentOrder': False, + 'createTriggerOrder': True, + 'editOrder': True, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchClosedOrder': True, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDepositsWithdrawals': True, + 'fetchFundingHistory': False, + 'fetchFundingRate': 'emulated', # emulated in exchange + 'fetchFundingRateHistory': True, + 'fetchFundingRates': True, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchLedger': True, + 'fetchLeverage': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': True, + 'fetchMarginMode': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkOHLCV': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': True, + 'fetchOpenInterestHistory': True, + 'fetchOpenInterests': True, + 'fetchOpenOrder': True, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrderBooks': False, + 'fetchOrderTrades': True, + 'fetchPosition': False, + 'fetchPositionMode': False, + 'fetchPositions': True, + 'fetchPremiumIndexOHLCV': False, + 'fetchStatus': True, + 'fetchTickers': True, + 'fetchTime': False, + 'fetchTradingFee': False, + 'fetchTradingFees': True, + 'fetchTransactionFees': None, + 'fetchTransactions': 'emulated', + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'setLeverage': False, + 'setMargin': True, + 'setMarginMode': False, + 'setPositionMode': False, + 'signIn': False, + 'transfer': True, + 'withdraw': True, + }, + 'timeframes': { + '1m': '1m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1h', + '3h': '3h', + '4h': '4h', + '6h': '6h', + '12h': '12h', + '1d': '1D', + '1w': '7D', + '2w': '14D', + '1M': '1M', + }, + # cheapest endpoint is 240 requests per minute => ~ 4 requests per second =>( 1000ms / 4 ) = 250ms between requests on average + 'rateLimit': 250, + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/4a8e947f-ab46-481a-a8ae-8b20e9b03178', + 'api': { + 'v1': 'https://api.bitfinex.com', + 'public': 'https://api-pub.bitfinex.com', + 'private': 'https://api.bitfinex.com', + }, + 'www': 'https://www.bitfinex.com', + 'doc': [ + 'https://docs.bitfinex.com/v2/docs/', + 'https://github.com/bitfinexcom/bitfinex-api-node', + ], + 'fees': 'https://www.bitfinex.com/fees', + }, + 'api': { + 'public': { + 'get': { + 'conf/{config}': 2.7, # 90 requests a minute, 90/60 = 1.5, 1000 / (250 * 2.66) = 1.503, use 2.7 instead of 2.66 to ensure rateLimitExceeded is not triggered + 'conf/pub:{action}:{object}': 2.7, + 'conf/pub:{action}:{object}:{detail}': 2.7, + 'conf/pub:map:{object}': 2.7, + 'conf/pub:map:{object}:{detail}': 2.7, + 'conf/pub:map:currency:{detail}': 2.7, + 'conf/pub:map:currency:sym': 2.7, # maps symbols to their API symbols, BAB > BCH + 'conf/pub:map:currency:label': 2.7, # verbose friendly names, BNT > Bancor + 'conf/pub:map:currency:unit': 2.7, # maps symbols to unit of measure where applicable + 'conf/pub:map:currency:undl': 2.7, # maps derivatives symbols to their underlying currency + 'conf/pub:map:currency:pool': 2.7, # maps symbols to underlying network/protocol they operate on + 'conf/pub:map:currency:explorer': 2.7, # maps symbols to their recognised block explorer URLs + 'conf/pub:map:currency:tx:fee': 2.7, # maps currencies to their withdrawal fees https://github.com/ccxt/ccxt/issues/7745 + 'conf/pub:map:tx:method': 2.7, + 'conf/pub:list:{object}': 2.7, + 'conf/pub:list:{object}:{detail}': 2.7, + 'conf/pub:list:currency': 2.7, + 'conf/pub:list:pair:exchange': 2.7, + 'conf/pub:list:pair:margin': 2.7, + 'conf/pub:list:pair:futures': 2.7, + 'conf/pub:list:competitions': 2.7, + 'conf/pub:info:{object}': 2.7, + 'conf/pub:info:{object}:{detail}': 2.7, + 'conf/pub:info:pair': 2.7, + 'conf/pub:info:pair:futures': 2.7, + 'conf/pub:info:tx:status': 2.7, # [deposit, withdrawal] statuses 1 = active, 0 = maintenance + 'conf/pub:fees': 2.7, + 'platform/status': 8, # 30 requests per minute = 0.5 requests per second =>( 1000ms / rateLimit ) / 0.5 = 8 + 'tickers': 2.7, # 90 requests a minute = 1.5 requests per second =>( 1000 / rateLimit ) / 1.5 = 2.666666666 + 'ticker/{symbol}': 2.7, + 'tickers/hist': 2.7, + 'trades/{symbol}/hist': 2.7, + 'book/{symbol}/{precision}': 1, # 240 requests a minute + 'book/{symbol}/P0': 1, + 'book/{symbol}/P1': 1, + 'book/{symbol}/P2': 1, + 'book/{symbol}/P3': 1, + 'book/{symbol}/R0': 1, + 'stats1/{key}:{size}:{symbol}:{side}/{section}': 2.7, + 'stats1/{key}:{size}:{symbol}:{side}/last': 2.7, + 'stats1/{key}:{size}:{symbol}:{side}/hist': 2.7, + 'stats1/{key}:{size}:{symbol}/{section}': 2.7, + 'stats1/{key}:{size}:{symbol}/last': 2.7, + 'stats1/{key}:{size}:{symbol}/hist': 2.7, + 'stats1/{key}:{size}:{symbol}:long/last': 2.7, + 'stats1/{key}:{size}:{symbol}:long/hist': 2.7, + 'stats1/{key}:{size}:{symbol}:short/last': 2.7, + 'stats1/{key}:{size}:{symbol}:short/hist': 2.7, + 'candles/trade:{timeframe}:{symbol}:{period}/{section}': 2.7, + 'candles/trade:{timeframe}:{symbol}/{section}': 2.7, + 'candles/trade:{timeframe}:{symbol}/last': 2.7, + 'candles/trade:{timeframe}:{symbol}/hist': 2.7, + 'status/{type}': 2.7, + 'status/deriv': 2.7, + 'status/deriv/{symbol}/hist': 2.7, + 'liquidations/hist': 80, # 3 requests a minute = 0.05 requests a second =>( 1000ms / rateLimit ) / 0.05 = 80 + 'rankings/{key}:{timeframe}:{symbol}/{section}': 2.7, + 'rankings/{key}:{timeframe}:{symbol}/hist': 2.7, + 'pulse/hist': 2.7, + 'pulse/profile/{nickname}': 2.7, + 'funding/stats/{symbol}/hist': 10, # ratelimit not in docs + 'ext/vasps': 1, + }, + 'post': { + 'calc/trade/avg': 2.7, + 'calc/fx': 2.7, + }, + }, + 'private': { + 'post': { + # 'auth/r/orders/{symbol}/new', # outdated + # 'auth/r/stats/perf:{timeframe}/hist', # outdated + 'auth/r/wallets': 2.7, + 'auth/r/wallets/hist': 2.7, + 'auth/r/orders': 2.7, + 'auth/r/orders/{symbol}': 2.7, + 'auth/w/order/submit': 2.7, + 'auth/w/order/update': 2.7, + 'auth/w/order/cancel': 2.7, + 'auth/w/order/multi': 2.7, + 'auth/w/order/cancel/multi': 2.7, + 'auth/r/orders/{symbol}/hist': 2.7, + 'auth/r/orders/hist': 2.7, + 'auth/r/order/{symbol}:{id}/trades': 2.7, + 'auth/r/trades/{symbol}/hist': 2.7, + 'auth/r/trades/hist': 2.7, + 'auth/r/ledgers/{currency}/hist': 2.7, + 'auth/r/ledgers/hist': 2.7, + 'auth/r/info/margin/{key}': 2.7, + 'auth/r/info/margin/base': 2.7, + 'auth/r/info/margin/sym_all': 2.7, + 'auth/r/positions': 2.7, + 'auth/w/position/claim': 2.7, + 'auth/w/position/increase:': 2.7, + 'auth/r/position/increase/info': 2.7, + 'auth/r/positions/hist': 2.7, + 'auth/r/positions/audit': 2.7, + 'auth/r/positions/snap': 2.7, + 'auth/w/deriv/collateral/set': 2.7, + 'auth/w/deriv/collateral/limits': 2.7, + 'auth/r/funding/offers': 2.7, + 'auth/r/funding/offers/{symbol}': 2.7, + 'auth/w/funding/offer/submit': 2.7, + 'auth/w/funding/offer/cancel': 2.7, + 'auth/w/funding/offer/cancel/all': 2.7, + 'auth/w/funding/close': 2.7, + 'auth/w/funding/auto': 2.7, + 'auth/w/funding/keep': 2.7, + 'auth/r/funding/offers/{symbol}/hist': 2.7, + 'auth/r/funding/offers/hist': 2.7, + 'auth/r/funding/loans': 2.7, + 'auth/r/funding/loans/hist': 2.7, + 'auth/r/funding/loans/{symbol}': 2.7, + 'auth/r/funding/loans/{symbol}/hist': 2.7, + 'auth/r/funding/credits': 2.7, + 'auth/r/funding/credits/hist': 2.7, + 'auth/r/funding/credits/{symbol}': 2.7, + 'auth/r/funding/credits/{symbol}/hist': 2.7, + 'auth/r/funding/trades/{symbol}/hist': 2.7, + 'auth/r/funding/trades/hist': 2.7, + 'auth/r/info/funding/{key}': 2.7, + 'auth/r/info/user': 2.7, + 'auth/r/summary': 2.7, + 'auth/r/logins/hist': 2.7, + 'auth/r/permissions': 2.7, + 'auth/w/token': 2.7, + 'auth/r/audit/hist': 2.7, + 'auth/w/transfer': 2.7, # ratelimit not in docs... + 'auth/w/deposit/address': 24, # 10 requests a minute = 0.166 requests per second =>( 1000ms / rateLimit ) / 0.166 = 24 + 'auth/w/deposit/invoice': 24, # ratelimit not in docs + 'auth/w/withdraw': 24, # ratelimit not in docs + 'auth/r/movements/{currency}/hist': 2.7, + 'auth/r/movements/hist': 2.7, + 'auth/r/alerts': 5.34, # 45 requests a minute = 0.75 requests per second =>( 1000ms / rateLimit ) / 0.749 => 5.34 + 'auth/w/alert/set': 2.7, + 'auth/w/alert/price:{symbol}:{price}/del': 2.7, + 'auth/w/alert/{type}:{symbol}:{price}/del': 2.7, + 'auth/calc/order/avail': 2.7, + 'auth/w/settings/set': 2.7, + 'auth/r/settings': 2.7, + 'auth/w/settings/del': 2.7, + 'auth/r/pulse/hist': 2.7, + 'auth/w/pulse/add': 16, # 15 requests a minute = 0.25 requests per second =>( 1000ms / rateLimit ) / 0.25 => 16 + 'auth/w/pulse/del': 2.7, + }, + }, + }, + 'fees': { + 'trading': { + 'feeSide': 'get', + 'percentage': True, + 'tierBased': True, + 'maker': self.parse_number('0.001'), + 'taker': self.parse_number('0.002'), + 'tiers': { + 'taker': [ + [self.parse_number('0'), self.parse_number('0.002')], + [self.parse_number('500000'), self.parse_number('0.002')], + [self.parse_number('1000000'), self.parse_number('0.002')], + [self.parse_number('2500000'), self.parse_number('0.002')], + [self.parse_number('5000000'), self.parse_number('0.002')], + [self.parse_number('7500000'), self.parse_number('0.002')], + [self.parse_number('10000000'), self.parse_number('0.0018')], + [self.parse_number('15000000'), self.parse_number('0.0016')], + [self.parse_number('20000000'), self.parse_number('0.0014')], + [self.parse_number('25000000'), self.parse_number('0.0012')], + [self.parse_number('30000000'), self.parse_number('0.001')], + ], + 'maker': [ + [self.parse_number('0'), self.parse_number('0.001')], + [self.parse_number('500000'), self.parse_number('0.0008')], + [self.parse_number('1000000'), self.parse_number('0.0006')], + [self.parse_number('2500000'), self.parse_number('0.0004')], + [self.parse_number('5000000'), self.parse_number('0.0002')], + [self.parse_number('7500000'), self.parse_number('0')], + [self.parse_number('10000000'), self.parse_number('0')], + [self.parse_number('15000000'), self.parse_number('0')], + [self.parse_number('20000000'), self.parse_number('0')], + [self.parse_number('25000000'), self.parse_number('0')], + [self.parse_number('30000000'), self.parse_number('0')], + ], + }, + }, + 'funding': { + 'withdraw': {}, + }, + }, + 'precisionMode': SIGNIFICANT_DIGITS, + 'options': { + 'precision': 'R0', # P0, P1, P2, P3, P4, R0 + # convert 'EXCHANGE MARKET' to lowercase 'market' + # convert 'EXCHANGE LIMIT' to lowercase 'limit' + # everything else remains uppercase + 'exchangeTypes': { + 'MARKET': 'market', + 'EXCHANGE MARKET': 'market', + 'LIMIT': 'limit', + 'EXCHANGE LIMIT': 'limit', + # 'STOP': None, + 'EXCHANGE STOP': 'market', + # 'TRAILING STOP': None, + # 'EXCHANGE TRAILING STOP': None, + # 'FOK': None, + 'EXCHANGE FOK': 'limit', + # 'STOP LIMIT': None, + 'EXCHANGE STOP LIMIT': 'limit', + # 'IOC': None, + 'EXCHANGE IOC': 'limit', + }, + # convert 'market' to 'EXCHANGE MARKET' + # convert 'limit' 'EXCHANGE LIMIT' + # everything else remains + 'orderTypes': { + 'market': 'EXCHANGE MARKET', + 'limit': 'EXCHANGE LIMIT', + }, + 'fiat': { + 'USD': 'USD', + 'EUR': 'EUR', + 'JPY': 'JPY', + 'GBP': 'GBP', + 'CHN': 'CHN', + }, + # actually the correct names unlike the v1 + # we don't want to self.extend self with accountsByType in v1 + 'v2AccountsByType': { + 'spot': 'exchange', + 'exchange': 'exchange', + 'funding': 'funding', + 'margin': 'margin', + 'derivatives': 'margin', + 'future': 'margin', + 'swap': 'margin', + }, + 'withdraw': { + 'includeFee': False, + }, + 'networks': { + 'BTC': 'BITCOIN', + 'LTC': 'LITECOIN', + 'ERC20': 'ETHEREUM', + 'OMNI': 'TETHERUSO', + 'LIQUID': 'TETHERUSL', + 'TRC20': 'TETHERUSX', + 'EOS': 'TETHERUSS', + 'AVAX': 'TETHERUSDTAVAX', + 'SOL': 'TETHERUSDTSOL', + 'ALGO': 'TETHERUSDTALG', + 'BCH': 'TETHERUSDTBCH', + 'KSM': 'TETHERUSDTKSM', + 'DVF': 'TETHERUSDTDVF', + 'OMG': 'TETHERUSDTOMG', + }, + 'networksById': { + 'TETHERUSE': 'ERC20', + }, + }, + 'features': { + 'default': { + 'sandbox': False, + 'createOrder': { + 'marginMode': True, + 'triggerPrice': True, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': True, + 'takeProfitPrice': True, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': True, # todo: implement + 'leverage': True, # todo: implement + 'marketBuyRequiresPrice': False, + 'marketBuyByCost': True, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': { + 'max': 75, + }, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 2500, + 'daysBack': None, + 'untilDays': 100000, # todo: implement + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': None, + 'daysBack': None, + 'daysBackCanceled': None, + 'untilDays': 100000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 10000, + }, + }, + 'spot': { + 'extends': 'default', + }, + 'swap': { + 'linear': { + 'extends': 'default', + }, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'exceptions': { + 'exact': { + '11010': RateLimitExceeded, + '10001': PermissionDenied, # api_key: permission invalid(#10001) + '10020': BadRequest, + '10100': AuthenticationError, + '10114': InvalidNonce, + '20060': OnMaintenance, + # {"code":503,"error":"temporarily_unavailable","error_description":"Sorry, the service is temporarily unavailable. See https://www.bitfinex.com/ for more info."} + 'temporarily_unavailable': ExchangeNotAvailable, + }, + 'broad': { + 'available balance is only': InsufficientFunds, + 'not enough exchange balance': InsufficientFunds, + 'Order not found': OrderNotFound, + 'symbol: invalid': BadSymbol, + }, + }, + 'commonCurrencies': { + 'UST': 'USDT', + 'EUTF0': 'EURT', + 'USTF0': 'USDT', + 'ALG': 'ALGO', # https://github.com/ccxt/ccxt/issues/6034 + 'AMP': 'AMPL', + 'ATO': 'ATOM', # https://github.com/ccxt/ccxt/issues/5118 + 'BCHABC': 'XEC', + 'BCHN': 'BCH', + 'DAT': 'DATA', + 'DOG': 'MDOGE', + 'DSH': 'DASH', + 'EDO': 'PNT', + 'EUS': 'EURS', + 'EUT': 'EURT', + 'HTX': 'HT', + 'IDX': 'ID', + 'IOT': 'IOTA', + 'IQX': 'IQ', + 'LUNA': 'LUNC', + 'LUNA2': 'LUNA', + 'MNA': 'MANA', + 'ORS': 'ORS Group', # conflict with Origin Sport #3230 + 'PAS': 'PASS', + 'QSH': 'QASH', + 'QTM': 'QTUM', + 'RBT': 'RBTC', + 'SNG': 'SNGLS', + 'STJ': 'STORJ', + 'TERRAUST': 'USTC', + 'TSD': 'TUSD', + 'YGG': 'YEED', # conflict with Yield Guild Games + 'YYW': 'YOYOW', + 'UDC': 'USDC', + 'VSY': 'VSYS', + 'WAX': 'WAXP', + 'XCH': 'XCHF', + 'ZBT': 'ZB', + }, + }) + + def is_fiat(self, code): + return(code in self.options['fiat']) + + def get_currency_id(self, code): + return 'f' + code + + def get_currency_name(self, code): + # temporary fix for transpiler recognition, even though self is in parent class + if code in self.options['currencyNames']: + return self.options['currencyNames'][code] + raise NotSupported(self.id + ' ' + code + ' not supported for withdrawal') + + def amount_to_precision(self, symbol, amount): + # https://docs.bitfinex.com/docs/introduction#amount-precision + # The amount field allows up to 8 decimals. + # Anything exceeding self will be rounded to the 8th decimal. + symbol = self.safe_symbol(symbol) + return self.decimal_to_precision(amount, TRUNCATE, self.markets[symbol]['precision']['amount'], DECIMAL_PLACES) + + def price_to_precision(self, symbol, price): + symbol = self.safe_symbol(symbol) + price = self.decimal_to_precision(price, ROUND, self.markets[symbol]['precision']['price'], self.precisionMode) + # https://docs.bitfinex.com/docs/introduction#price-precision + # The precision level of all trading prices is based on significant figures. + # All pairs on Bitfinex use up to 5 significant digits and up to 8 decimals(e.g. 1.2345, 123.45, 1234.5, 0.00012345). + # Prices submit with a precision larger than 5 will be cut by the API. + return self.decimal_to_precision(price, TRUNCATE, 8, DECIMAL_PLACES) + + def fetch_status(self, params={}): + """ + the latest known information on the availability of the exchange API + + https://docs.bitfinex.com/reference/rest-public-platform-status + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `status structure ` + """ + # + # [1] # operative + # [0] # maintenance + # + response = self.publicGetPlatformStatus(params) + statusRaw = self.safe_string(response, 0) + return { + 'status': self.safe_string({'0': 'maintenance', '1': 'ok'}, statusRaw, statusRaw), + 'updated': None, + 'eta': None, + 'url': None, + 'info': response, + } + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for bitfinex + + https://docs.bitfinex.com/reference/rest-public-conf + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + spotMarketsInfoPromise = self.publicGetConfPubInfoPair(params) + futuresMarketsInfoPromise = self.publicGetConfPubInfoPairFutures(params) + marginIdsPromise = self.publicGetConfPubListPairMargin(params) + spotMarketsInfo, futuresMarketsInfo, marginIds = [spotMarketsInfoPromise, futuresMarketsInfoPromise, marginIdsPromise] + spotMarketsInfo = self.safe_list(spotMarketsInfo, 0, []) + futuresMarketsInfo = self.safe_list(futuresMarketsInfo, 0, []) + markets = self.array_concat(spotMarketsInfo, futuresMarketsInfo) + marginIds = self.safe_value(marginIds, 0, []) + # + # [ + # "1INCH:USD", + # [ + # null, + # null, + # null, + # "2.0", + # "100000.0", + # null, + # null, + # null, + # null, + # null, + # null, + # null + # ] + # ] + # + result = [] + for i in range(0, len(markets)): + pair = markets[i] + id = self.safe_string_upper(pair, 0) + market = self.safe_value(pair, 1, {}) + spot = True + if id.find('F0') >= 0: + spot = False + swap = not spot + baseId = None + quoteId = None + if id.find(':') >= 0: + parts = id.split(':') + baseId = parts[0] + quoteId = parts[1] + else: + baseId = id[0:3] + quoteId = id[3:6] + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + splitBase = base.split('F0') + splitQuote = quote.split('F0') + base = self.safe_string(splitBase, 0) + quote = self.safe_string(splitQuote, 0) + symbol = base + '/' + quote + baseId = self.get_currency_id(baseId) + quoteId = self.get_currency_id(quoteId) + settle = None + settleId = None + if swap: + settle = quote + settleId = quote + symbol = symbol + ':' + settle + minOrderSizeString = self.safe_string(market, 3) + maxOrderSizeString = self.safe_string(market, 4) + margin = False + if spot and self.in_array(id, marginIds): + margin = True + result.append({ + 'id': 't' + id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': 'spot' if spot else 'swap', + 'spot': spot, + 'margin': margin, + 'swap': swap, + 'future': False, + 'option': False, + 'active': True, + 'contract': swap, + 'linear': True if swap else None, + 'inverse': False if swap else None, + 'contractSize': self.parse_number('1') if swap else None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': int('8'), # https://github.com/ccxt/ccxt/issues/7310 + 'price': int('5'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.parse_number(minOrderSizeString), + 'max': self.parse_number(maxOrderSizeString), + }, + 'price': { + 'min': self.parse_number('1e-8'), + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': None, # todo: the api needs revision for extra params & endpoints for possibility of returning a timestamp for self + 'info': market, + }) + return result + + def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://docs.bitfinex.com/reference/rest-public-conf + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + labels = [ + 'pub:list:currency', + 'pub:map:currency:sym', # maps symbols to their API symbols, BAB > BCH + 'pub:map:currency:label', # verbose friendly names, BNT > Bancor + 'pub:map:currency:unit', # maps symbols to unit of measure where applicable + 'pub:map:currency:undl', # maps derivatives symbols to their underlying currency + 'pub:map:currency:pool', # maps symbols to underlying network/protocol they operate on + 'pub:map:currency:explorer', # maps symbols to their recognised block explorer URLs + 'pub:map:currency:tx:fee', # maps currencies to their withdrawal fees https://github.com/ccxt/ccxt/issues/7745, + 'pub:map:tx:method', # maps withdrawal/deposit methods to their API symbols + 'pub:info:tx:status', # maps withdrawal/deposit statuses, coins: 1 = enabled, 0 = maintenance + ] + config = ','.join(labels) + request: dict = { + 'config': config, + } + response = self.publicGetConfConfig(self.extend(request, params)) + # + # [ + # + # a list of symbols + # ["AAA","ABS","ADA"], + # + # # sym + # # maps symbols to their API symbols, BAB > BCH + # [ + # ["BAB", "BCH"], + # ["CNHT", "CNHt"], + # ["DSH", "DASH"], + # ["IOT", "IOTA"], + # ["LES", "LEO-EOS"], + # ["LET", "LEO-ERC20"], + # ["STJ", "STORJ"], + # ["TSD", "TUSD"], + # ["UDC", "USDC"], + # ["USK", "USDK"], + # ["UST", "USDt"], + # ["USTF0", "USDt0"], + # ["XCH", "XCHF"], + # ["YYW", "YOYOW"], + # # ... + # ], + # # label + # # verbose friendly names, BNT > Bancor + # [ + # ["BAB", "Bitcoin Cash"], + # ["BCH", "Bitcoin Cash"], + # ["LEO", "Unus Sed LEO"], + # ["LES", "Unus Sed LEO(EOS)"], + # ["LET", "Unus Sed LEO(ERC20)"], + # # ... + # ], + # # unit + # # maps symbols to unit of measure where applicable + # [ + # ["IOT", "Mi|MegaIOTA"], + # ], + # # undl + # # maps derivatives symbols to their underlying currency + # [ + # ["USTF0", "UST"], + # ["BTCF0", "BTC"], + # ["ETHF0", "ETH"], + # ], + # # pool + # # maps symbols to underlying network/protocol they operate on + # [ + # ['SAN', 'ETH'], ['OMG', 'ETH'], ['AVT', 'ETH'], ["EDO", "ETH"], + # ['ESS', 'ETH'], ['ATD', 'EOS'], ['ADD', 'EOS'], ["MTO", "EOS"], + # ['PNK', 'ETH'], ['BAB', 'BCH'], ['WLO', 'XLM'], ["VLD", "ETH"], + # ['BTT', 'TRX'], ['IMP', 'ETH'], ['SCR', 'ETH'], ["GNO", "ETH"], + # # ... + # ], + # # explorer + # # maps symbols to their recognised block explorer URLs + # [ + # [ + # "AIO", + # [ + # "https://mainnet.aion.network", + # "https://mainnet.aion.network/#/account/VAL", + # "https://mainnet.aion.network/#/transaction/VAL" + # ] + # ], + # # ... + # ], + # # fee + # # maps currencies to their withdrawal fees + # [ + # ["AAA",[0,0]], + # ["ABS",[0,131.3]], + # ["ADA",[0,0.3]], + # ], + # # deposit/withdrawal data + # [ + # ["BITCOIN", 1, 1, null, null, null, null, 0, 0, null, null, 3], + # ... + # ] + # ] + # + indexed: dict = { + 'sym': self.index_by(self.safe_list(response, 1, []), 0), + 'label': self.index_by(self.safe_list(response, 2, []), 0), + 'unit': self.index_by(self.safe_list(response, 3, []), 0), + 'undl': self.index_by(self.safe_list(response, 4, []), 0), + 'pool': self.index_by(self.safe_list(response, 5, []), 0), + 'explorer': self.index_by(self.safe_list(response, 6, []), 0), + 'fees': self.index_by(self.safe_list(response, 7, []), 0), + 'networks': self.safe_list(response, 8, []), + 'statuses': self.index_by(self.safe_list(response, 9, []), 0), + } + indexedNetworks: dict = {} + for i in range(0, len(indexed['networks'])): + networkObj = indexed['networks'][i] + networkId = self.safe_string(networkObj, 0) + valuesList = self.safe_list(networkObj, 1) + networkName = self.safe_string(valuesList, 0) + # for GOlang transpiler, do with "safe" method + networksList = self.safe_list(indexedNetworks, networkName, []) + networksList.append(networkId) + indexedNetworks[networkName] = networksList + ids = self.safe_list(response, 0, []) + result: dict = {} + for i in range(0, len(ids)): + id = ids[i] + if id.endswith('F0'): + # we get a lot of F0 currencies, skip those + continue + code = self.safe_currency_code(id) + label = self.safe_list(indexed['label'], id, []) + name = self.safe_string(label, 1) + pool = self.safe_list(indexed['pool'], id, []) + rawType = self.safe_string(pool, 1) + isCryptoCoin = (rawType is not None) or (id in indexed['explorer']) # "hacky" solution + type = None + if isCryptoCoin: + type = 'crypto' + feeValues = self.safe_list(indexed['fees'], id, []) + fees = self.safe_list(feeValues, 1, []) + fee = self.safe_number(fees, 1) + undl = self.safe_list(indexed['undl'], id, []) + precision = '8' # default precision, todo: fix "magic constants" + fid = 'f' + id + dwStatuses = self.safe_list(indexed['statuses'], id, []) + depositEnabled = self.safe_integer(dwStatuses, 1) == 1 + withdrawEnabled = self.safe_integer(dwStatuses, 2) == 1 + networks: dict = {} + netwokIds = self.safe_list(indexedNetworks, id, []) + for j in range(0, len(netwokIds)): + networkId = netwokIds[j] + network = self.network_id_to_code(networkId) + networks[network] = { + 'info': networkId, + 'id': networkId.lower(), + 'network': networkId, + 'active': None, + 'deposit': None, + 'withdraw': None, + 'fee': None, + 'precision': None, + 'limits': { + 'withdraw': { + 'min': None, + 'max': None, + }, + }, + } + result[code] = self.safe_currency_structure({ + 'id': fid, + 'uppercaseId': id, + 'code': code, + 'info': [id, label, pool, feeValues, undl], + 'type': type, + 'name': name, + 'active': True, + 'deposit': depositEnabled, + 'withdraw': withdrawEnabled, + 'fee': fee, + 'precision': int(precision), + 'limits': { + 'amount': { + 'min': self.parse_number(self.parse_precision(precision)), + 'max': None, + }, + 'withdraw': { + 'min': fee, + 'max': None, + }, + }, + 'networks': networks, + }) + return result + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://docs.bitfinex.com/reference/rest-auth-wallets + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + # self api call does not return the 'used' amount - use the v1 version instead(which also returns zero balances) + # there is a difference between self and the v1 api, namely trading wallet is called margin in v2 + self.load_markets() + accountsByType = self.safe_value(self.options, 'v2AccountsByType', {}) + requestedType = self.safe_string(params, 'type', 'exchange') + accountType = self.safe_string(accountsByType, requestedType, requestedType) + if accountType is None: + keys = list(accountsByType.keys()) + raise ExchangeError(self.id + ' fetchBalance() type parameter must be one of ' + ', '.join(keys)) + isDerivative = requestedType == 'derivatives' + query = self.omit(params, 'type') + response = self.privatePostAuthRWallets(query) + result: dict = {'info': response} + for i in range(0, len(response)): + balance = response[i] + account = self.account() + interest = self.safe_string(balance, 3) + if interest != '0': + account['debt'] = interest + type = self.safe_string(balance, 0) + currencyId = self.safe_string_lower(balance, 1, '') + start = len(currencyId) - 2 + isDerivativeCode = currencyId[start:] == 'f0' + # self will only filter the derivative codes if the requestedType is 'derivatives' + derivativeCondition = (not isDerivative or isDerivativeCode) + if (accountType == type) and derivativeCondition: + code = self.safe_currency_code(currencyId) + account['total'] = self.safe_string(balance, 2) + account['free'] = self.safe_string(balance, 4) + result[code] = account + return self.safe_balance(result) + + def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://docs.bitfinex.com/reference/rest-auth-transfer + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account to transfer from + :param str toAccount: account to transfer to + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transfer structure ` + """ + # transferring between derivatives wallet and regular wallet is not documented in their API + # however we support it in CCXT(from just looking at web inspector) + self.load_markets() + accountsByType = self.safe_value(self.options, 'v2AccountsByType', {}) + fromId = self.safe_string(accountsByType, fromAccount) + if fromId is None: + keys = list(accountsByType.keys()) + raise ArgumentsRequired(self.id + ' transfer() fromAccount must be one of ' + ', '.join(keys)) + toId = self.safe_string(accountsByType, toAccount) + if toId is None: + keys = list(accountsByType.keys()) + raise ArgumentsRequired(self.id + ' transfer() toAccount must be one of ' + ', '.join(keys)) + currency = self.currency(code) + fromCurrencyId = self.convert_derivatives_id(currency, fromAccount) + toCurrencyId = self.convert_derivatives_id(currency, toAccount) + requestedAmount = self.currency_to_precision(code, amount) + # self request is slightly different from v1 fromAccount -> from + request: dict = { + 'amount': requestedAmount, + 'currency': fromCurrencyId, + 'currency_to': toCurrencyId, + 'from': fromId, + 'to': toId, + } + response = self.privatePostAuthWTransfer(self.extend(request, params)) + # + # [ + # 1616451183763, + # "acc_tf", + # null, + # null, + # [ + # 1616451183763, + # "exchange", + # "margin", + # null, + # "UST", + # "UST", + # null, + # 1 + # ], + # null, + # "SUCCESS", + # "1.0 Tether USDt transfered from Exchange to Margin" + # ] + # + error = self.safe_string(response, 0) + if error == 'error': + message = self.safe_string(response, 2, '') + # same message v1 + self.throw_exactly_matched_exception(self.exceptions['exact'], message, self.id + ' ' + message) + raise ExchangeError(self.id + ' ' + message) + return self.parse_transfer({'result': response}, currency) + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + # + # transfer + # + # [ + # 1616451183763, + # "acc_tf", + # null, + # null, + # [ + # 1616451183763, + # "exchange", + # "margin", + # null, + # "UST", + # "UST", + # null, + # 1 + # ], + # null, + # "SUCCESS", + # "1.0 Tether USDt transfered from Exchange to Margin" + # ] + # + result = self.safe_list(transfer, 'result') + timestamp = self.safe_integer(result, 0) + info = self.safe_value(result, 4) + fromAccount = self.safe_string(info, 1) + toAccount = self.safe_string(info, 2) + currencyId = self.safe_string(info, 5) + status = self.safe_string(result, 6) + return { + 'id': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'status': self.parse_transfer_status(status), + 'amount': self.safe_number(info, 7), + 'currency': self.safe_currency_code(currencyId, currency), + 'fromAccount': fromAccount, + 'toAccount': toAccount, + 'info': result, + } + + def parse_transfer_status(self, status: Str) -> Str: + statuses: dict = { + 'SUCCESS': 'ok', + 'ERROR': 'failed', + 'FAILURE': 'failed', + } + return self.safe_string(statuses, status, status) + + def convert_derivatives_id(self, currency, type): + # there is a difference between self and the v1 api, namely trading wallet is called margin in v2 + # { + # "id": "fUSTF0", + # "code": "USTF0", + # "info": ['USTF0', [], [], [], ["USTF0", "UST"]], + info = self.safe_value(currency, 'info') + transferId = self.safe_string(info, 0) + underlying = self.safe_value(info, 4, []) + currencyId = None + if type == 'derivatives': + currencyId = self.safe_string(underlying, 0, transferId) + start = len(currencyId) - 2 + isDerivativeCode = currencyId[start:] == 'F0' + if not isDerivativeCode: + currencyId = currencyId + 'F0' + elif type != 'margin': + currencyId = self.safe_string(underlying, 1, transferId) + else: + currencyId = transferId + return currencyId + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://docs.bitfinex.com/reference/rest-public-book + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return, bitfinex only allows 1, 25, or 100 + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + precision = self.safe_value(self.options, 'precision', 'R0') + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'precision': precision, + } + if limit is not None: + request['len'] = limit + fullRequest = self.extend(request, params) + orderbook = self.publicGetBookSymbolPrecision(fullRequest) + timestamp = self.milliseconds() + result: dict = { + 'symbol': market['symbol'], + 'bids': [], + 'asks': [], + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'nonce': None, + } + priceIndex = 1 if (fullRequest['precision'] == 'R0') else 0 + for i in range(0, len(orderbook)): + order = orderbook[i] + price = self.safe_number(order, priceIndex) + signedAmount = self.safe_string(order, 2) + amount = Precise.string_abs(signedAmount) + side = 'bids' if Precise.string_gt(signedAmount, '0') else 'asks' + resultSide = result[side] + resultSide.append([price, self.parse_number(amount)]) + result['bids'] = self.sort_by(result['bids'], 0, True) + result['asks'] = self.sort_by(result['asks'], 0) + return result + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # on trading pairs(ex. tBTCUSD) + # + # [ + # SYMBOL, # self index is not present in singular-ticker + # BID, + # BID_SIZE, + # ASK, + # ASK_SIZE, + # DAILY_CHANGE, + # DAILY_CHANGE_RELATIVE, + # LAST_PRICE, + # VOLUME, + # HIGH, + # LOW + # ] + # + # + # on funding currencies(ex. fUSD) + # + # [ + # SYMBOL, # self index is not present in singular-ticker + # FRR, + # BID, + # BID_PERIOD, + # BID_SIZE, + # ASK, + # ASK_PERIOD, + # ASK_SIZE, + # DAILY_CHANGE, + # DAILY_CHANGE_RELATIVE, + # LAST_PRICE, + # VOLUME, + # HIGH, + # LOW, + # _PLACEHOLDER, + # _PLACEHOLDER, + # FRR_AMOUNT_AVAILABLE + # ] + # + length = len(ticker) + isFetchTicker = (length == 10) or (length == 16) + symbol: Str = None + minusIndex = 0 + isFundingCurrency = False + if isFetchTicker: + minusIndex = 1 + isFundingCurrency = (length == 16) + else: + marketId = self.safe_string(ticker, 0) + market = self.safe_market(marketId, market) + isFundingCurrency = (length == 17) + symbol = self.safe_symbol(None, market) + last: Str = None + bid: Str = None + ask: Str = None + change: Str = None + percentage: Str = None + volume: Str = None + high: Str = None + low: Str = None + if isFundingCurrency: + # per api docs, they are different array type + last = self.safe_string(ticker, 10 - minusIndex) + bid = self.safe_string(ticker, 2 - minusIndex) + ask = self.safe_string(ticker, 5 - minusIndex) + change = self.safe_string(ticker, 8 - minusIndex) + percentage = self.safe_string(ticker, 9 - minusIndex) + volume = self.safe_string(ticker, 11 - minusIndex) + high = self.safe_string(ticker, 12 - minusIndex) + low = self.safe_string(ticker, 13 - minusIndex) + else: + # on trading pairs(ex. tBTCUSD or tHMSTR:USD) + last = self.safe_string(ticker, 7 - minusIndex) + bid = self.safe_string(ticker, 1 - minusIndex) + ask = self.safe_string(ticker, 3 - minusIndex) + change = self.safe_string(ticker, 5 - minusIndex) + percentage = self.safe_string(ticker, 6 - minusIndex) + percentage = Precise.string_mul(percentage, '100') + volume = self.safe_string(ticker, 8 - minusIndex) + high = self.safe_string(ticker, 9 - minusIndex) + low = self.safe_string(ticker, 10 - minusIndex) + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': None, + 'datetime': None, + 'high': high, + 'low': low, + 'bid': bid, + 'bidVolume': None, + 'ask': ask, + 'askVolume': None, + 'vwap': None, + 'open': None, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': change, + 'percentage': percentage, + 'average': None, + 'baseVolume': volume, + 'quoteVolume': None, + 'info': ticker, + }, market) + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://docs.bitfinex.com/reference/rest-public-tickers + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + symbols = self.market_symbols(symbols) + request: dict = {} + if symbols is not None: + ids = self.market_ids(symbols) + request['symbols'] = ','.join(ids) + else: + request['symbols'] = 'ALL' + tickers = self.publicGetTickers(self.extend(request, params)) + # + # [ + # # on trading pairs(ex. tBTCUSD) + # [ + # SYMBOL, + # BID, + # BID_SIZE, + # ASK, + # ASK_SIZE, + # DAILY_CHANGE, + # DAILY_CHANGE_RELATIVE, + # LAST_PRICE, + # VOLUME, + # HIGH, + # LOW + # ], + # # on funding currencies(ex. fUSD) + # [ + # SYMBOL, + # FRR, + # BID, + # BID_PERIOD, + # BID_SIZE, + # ASK, + # ASK_PERIOD, + # ASK_SIZE, + # DAILY_CHANGE, + # DAILY_CHANGE_RELATIVE, + # LAST_PRICE, + # VOLUME, + # HIGH, + # LOW, + # _PLACEHOLDER, + # _PLACEHOLDER, + # FRR_AMOUNT_AVAILABLE + # ], + # ... + # ] + # + return self.parse_tickers(tickers, symbols) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://docs.bitfinex.com/reference/rest-public-ticker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + ticker = self.publicGetTickerSymbol(self.extend(request, params)) + return self.parse_ticker(ticker, market) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(public) + # + # [ + # ID, + # MTS, # timestamp + # AMOUNT, + # PRICE + # ] + # + # fetchMyTrades(private) + # + # [ + # ID, + # PAIR, + # MTS_CREATE, + # ORDER_ID, + # EXEC_AMOUNT, + # EXEC_PRICE, + # ORDER_TYPE, + # ORDER_PRICE, + # MAKER, + # FEE, + # FEE_CURRENCY, + # ... + # ] + # + tradeList = self.safe_list(trade, 'result', []) + tradeLength = len(tradeList) + isPrivate = (tradeLength > 5) + id = self.safe_string(tradeList, 0) + amountIndex = 4 if isPrivate else 2 + side = None + amountString = self.safe_string(tradeList, amountIndex) + priceIndex = 5 if isPrivate else 3 + priceString = self.safe_string(tradeList, priceIndex) + if amountString[0] == '-': + side = 'sell' + amountString = Precise.string_abs(amountString) + else: + side = 'buy' + orderId = None + takerOrMaker = None + type = None + fee = None + symbol = self.safe_symbol(None, market) + timestampIndex = 2 if isPrivate else 1 + timestamp = self.safe_integer(tradeList, timestampIndex) + if isPrivate: + marketId = tradeList[1] + symbol = self.safe_symbol(marketId) + orderId = self.safe_string(tradeList, 3) + maker = self.safe_integer(tradeList, 8) + takerOrMaker = 'maker' if (maker == 1) else 'taker' + feeCostString = self.safe_string(tradeList, 9) + feeCostString = Precise.string_neg(feeCostString) + feeCurrencyId = self.safe_string(tradeList, 10) + feeCurrency = self.safe_currency_code(feeCurrencyId) + fee = { + 'cost': feeCostString, + 'currency': feeCurrency, + } + orderType = tradeList[6] + type = self.safe_string(self.options['exchangeTypes'], orderType) + return self.safe_trade({ + 'id': id, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'order': orderId, + 'side': side, + 'type': type, + 'takerOrMaker': takerOrMaker, + 'price': priceString, + 'amount': amountString, + 'cost': None, + 'fee': fee, + 'info': tradeList, + }, market) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://docs.bitfinex.com/reference/rest-public-trades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch, default 120, max 10000 + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param int [params.until]: the latest time in ms to fetch entries for + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchTrades', 'paginate') + if paginate: + return self.fetch_paginated_call_dynamic('fetchTrades', symbol, since, limit, params, 10000) + market = self.market(symbol) + sort = '-1' + request: dict = { + 'symbol': market['id'], + } + if since is not None: + request['start'] = since + sort = '1' + if limit is not None: + request['limit'] = min(limit, 10000) # default 120, max 10000 + request['sort'] = sort + request, params = self.handle_until_option('end', request, params) + response = self.publicGetTradesSymbolHist(self.extend(request, params)) + # + # [ + # [ + # ID, + # MTS, # timestamp + # AMOUNT, + # PRICE + # ] + # ] + # + trades = self.sort_by(response, 1) + tradesList = [] + for i in range(0, len(trades)): + tradesList.append({'result': trades[i]}) # convert to array of dicts to match parseOrder signature + return self.parse_trades(tradesList, market, None, limit) + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = 100, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://docs.bitfinex.com/reference/rest-public-candles + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch, default 100 max 10000 + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + :param int [params.until]: timestamp in ms of the latest candle to fetch + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate') + if paginate: + return self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params, 10000) + market = self.market(symbol) + if limit is None: + limit = 10000 + else: + limit = min(limit, 10000) + request: dict = { + 'symbol': market['id'], + 'timeframe': self.safe_string(self.timeframes, timeframe, timeframe), + 'sort': 1, + 'limit': limit, + } + if since is not None: + request['start'] = since + request, params = self.handle_until_option('end', request, params) + response = self.publicGetCandlesTradeTimeframeSymbolHist(self.extend(request, params)) + # + # [ + # [1591503840000,0.025069,0.025068,0.025069,0.025068,1.97828998], + # [1591504500000,0.025065,0.025065,0.025065,0.025065,1.0164], + # [1591504620000,0.025062,0.025062,0.025062,0.025062,0.5], + # ] + # + return self.parse_ohlcvs(response, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # [ + # 1457539800000, + # 0.02594, + # 0.02594, + # 0.02594, + # 0.02594, + # 0.1 + # ] + # + return [ + self.safe_integer(ohlcv, 0), + self.safe_number(ohlcv, 1), + self.safe_number(ohlcv, 3), + self.safe_number(ohlcv, 4), + self.safe_number(ohlcv, 2), + self.safe_number(ohlcv, 5), + ] + + def parse_order_status(self, status: Str): + if status is None: + return status + parts = status.split(' ') + state = self.safe_string(parts, 0) + statuses: dict = { + 'ACTIVE': 'open', + 'PARTIALLY': 'open', + 'EXECUTED': 'closed', + 'CANCELED': 'canceled', + 'INSUFFICIENT': 'canceled', + 'POSTONLY CANCELED': 'canceled', + 'RSN_DUST': 'rejected', + 'RSN_PAUSE': 'rejected', + 'IOC CANCELED': 'canceled', + 'FILLORKILL CANCELED': 'canceled', + } + return self.safe_string(statuses, state, status) + + def parse_order_flags(self, flags): + # flags can be added to each other... + flagValues: dict = { + '1024': ['reduceOnly'], + '4096': ['postOnly'], + '5120': ['reduceOnly', 'postOnly'], + # '64': 'hidden', # The hidden order option ensures an order does not appear in the order book + # '512': 'close', # Close position if position present. + # '16384': 'OCO', # The one cancels other order option allows you to place a pair of orders stipulating that if one order is executed fully or partially, then the other is automatically canceled. + # '524288': 'No Var Rates' # Excludes variable rate funding offers from matching against self order, if on margin + } + return self.safe_value(flagValues, flags, None) + + def parse_time_in_force(self, orderType): + orderTypes: dict = { + 'EXCHANGE IOC': 'IOC', + 'EXCHANGE FOK': 'FOK', + 'IOC': 'IOC', # Margin + 'FOK': 'FOK', # Margin + } + return self.safe_string(orderTypes, orderType, 'GTC') + + def parse_order(self, order: dict, market: Market = None) -> Order: + orderList = self.safe_list(order, 'result') + id = self.safe_string(orderList, 0) + marketId = self.safe_string(orderList, 3) + symbol = self.safe_symbol(marketId) + # https://github.com/ccxt/ccxt/issues/6686 + # timestamp = self.safe_timestamp(orderObject, 5) + timestamp = self.safe_integer(orderList, 5) + remaining = Precise.string_abs(self.safe_string(orderList, 6)) + signedAmount = self.safe_string(orderList, 7) + amount = Precise.string_abs(signedAmount) + side = 'sell' if Precise.string_lt(signedAmount, '0') else 'buy' + orderType = self.safe_string(orderList, 8) + type = self.safe_string(self.safe_value(self.options, 'exchangeTypes'), orderType) + timeInForce = self.parse_time_in_force(orderType) + rawFlags = self.safe_string(orderList, 12) + flags = self.parse_order_flags(rawFlags) + postOnly = False + if flags is not None: + for i in range(0, len(flags)): + if flags[i] == 'postOnly': + postOnly = True + price = self.safe_string(orderList, 16) + triggerPrice = None + if (orderType == 'EXCHANGE STOP') or (orderType == 'EXCHANGE STOP LIMIT'): + price = None + triggerPrice = self.safe_string(orderList, 16) + if orderType == 'EXCHANGE STOP LIMIT': + price = self.safe_string(orderList, 19) + status = None + statusString = self.safe_string(orderList, 13) + if statusString is not None: + parts = statusString.split(' @ ') + status = self.parse_order_status(self.safe_string(parts, 0)) + average = self.safe_string(orderList, 17) + clientOrderId = self.safe_string(orderList, 2) + return self.safe_order({ + 'info': orderList, + 'id': id, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'symbol': symbol, + 'type': type, + 'timeInForce': timeInForce, + 'postOnly': postOnly, + 'side': side, + 'price': price, + 'triggerPrice': triggerPrice, + 'amount': amount, + 'cost': None, + 'average': average, + 'filled': None, + 'remaining': remaining, + 'status': status, + 'fee': None, + 'trades': None, + }, market) + + def create_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + @ignore + helper function to build an order request + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency + :param float [price]: the price of the order, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: The price at which a trigger order is triggered at + :param str [params.timeInForce]: "GTC", "IOC", "FOK", or "PO" + :param bool [params.postOnly]: + :param bool [params.reduceOnly]: Ensures that the executed order does not flip the opened position. + :param int [params.flags]: additional order parameters: 4096(Post Only), 1024(Reduce Only), 16384(OCO), 64(Hidden), 512(Close), 524288(No Var Rates) + :param int [params.lev]: leverage for a derivative order, supported by derivative symbol orders only. The value should be between 1 and 100 inclusive. + :param str [params.price_traling]: The trailing price for a trailing stop order + :param str [params.price_aux_limit]: Order price for stop limit orders + :param str [params.price_oco_stop]: OCO stop price + :returns dict: an `order structure ` + """ + market = self.market(symbol) + amountString = self.amount_to_precision(symbol, amount) + amountString = amountString if (side == 'buy') else Precise.string_neg(amountString) + request: dict = { + 'symbol': market['id'], + 'amount': amountString, + } + triggerPrice = self.safe_string_2(params, 'stopPrice', 'triggerPrice') + trailingAmount = self.safe_string(params, 'trailingAmount') + timeInForce = self.safe_string(params, 'timeInForce') + postOnlyParam = self.safe_bool(params, 'postOnly', False) + reduceOnly = self.safe_bool(params, 'reduceOnly', False) + clientOrderId = self.safe_value_2(params, 'cid', 'clientOrderId') + orderType = type.upper() + if trailingAmount is not None: + orderType = 'TRAILING STOP' + request['price_trailing'] = trailingAmount + elif triggerPrice is not None: + # request['price'] is taken for stop orders + request['price'] = self.price_to_precision(symbol, triggerPrice) + if type == 'limit': + orderType = 'STOP LIMIT' + request['price_aux_limit'] = self.price_to_precision(symbol, price) + else: + orderType = 'STOP' + ioc = (timeInForce == 'IOC') + fok = (timeInForce == 'FOK') + postOnly = (postOnlyParam or (timeInForce == 'PO')) + if (ioc or fok) and (price is None): + raise InvalidOrder(self.id + ' createOrder() requires a price argument with IOC and FOK orders') + if (ioc or fok) and (type == 'market'): + raise InvalidOrder(self.id + ' createOrder() does not allow market IOC and FOK orders') + if (type != 'market') and (triggerPrice is None): + request['price'] = self.price_to_precision(symbol, price) + if ioc: + orderType = 'IOC' + elif fok: + orderType = 'FOK' + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('createOrder', params) + if market['spot'] and (marginMode is None): + # The EXCHANGE prefix is only required for non margin spot markets + orderType = 'EXCHANGE ' + orderType + request['type'] = orderType + # flag values may be summed to combine flags + flags = 0 + if postOnly: + flags = self.sum(flags, 4096) + if reduceOnly: + flags = self.sum(flags, 1024) + if flags != 0: + request['flags'] = flags + if clientOrderId is not None: + request['cid'] = clientOrderId + params = self.omit(params, ['triggerPrice', 'stopPrice', 'timeInForce', 'postOnly', 'reduceOnly', 'trailingAmount', 'clientOrderId']) + return self.extend(request, params) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create an order on the exchange + + https://docs.bitfinex.com/reference/rest-auth-submit-order + + :param str symbol: unified CCXT market symbol + :param str type: 'limit' or 'market' + :param str side: 'buy' or 'sell' + :param float amount: the amount of currency to trade + :param float [price]: price of the order + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: the price that triggers a trigger order + :param str [params.timeInForce]: "GTC", "IOC", "FOK", or "PO" + :param boolean [params.postOnly]: set to True if you want to make a post only order + :param boolean [params.reduceOnly]: indicates that the order is to reduce the size of a position + :param int [params.flags]: additional order parameters: 4096(Post Only), 1024(Reduce Only), 16384(OCO), 64(Hidden), 512(Close), 524288(No Var Rates) + :param int [params.lev]: leverage for a derivative order, supported by derivative symbol orders only. The value should be between 1 and 100 inclusive. + :param str [params.price_aux_limit]: order price for stop limit orders + :param str [params.price_oco_stop]: OCO stop price + :param str [params.trailingAmount]: *swap only* the quote amount to trail away from the current market price + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + request = self.create_order_request(symbol, type, side, amount, price, params) + response = self.privatePostAuthWOrderSubmit(request) + # + # [ + # 1653325121, # Timestamp in milliseconds + # "on-req", # Purpose of notification('on-req', 'oc-req', "uca", 'fon-req', "foc-req") + # null, # unique ID of the message + # null, + # [ + # [ + # 95412102131, # Order ID + # null, # Group ID + # 1653325121798, # Client Order ID + # "tDOGE:UST", # Market ID + # 1653325121798, # Millisecond timestamp of creation + # 1653325121798, # Millisecond timestamp of update + # -10, # Amount(Positive means buy, negative means sell) + # -10, # Original amount + # "EXCHANGE LIMIT", # Type of the order: LIMIT, EXCHANGE LIMIT, MARKET, EXCHANGE MARKET, STOP, EXCHANGE STOP, STOP LIMIT, EXCHANGE STOP LIMIT, TRAILING STOP, EXCHANGE TRAILING STOP, FOK, EXCHANGE FOK, IOC, EXCHANGE IOC. + # null, # Previous order type(stop-limit orders are converted to limit orders so for them previous type is always STOP) + # null, # Millisecond timestamp of Time-In-Force: automatic order cancellation + # null, # _PLACEHOLDER + # 4096, # Flags, see parseOrderFlags() + # "ACTIVE", # Order Status, see parseOrderStatus() + # null, # _PLACEHOLDER + # null, # _PLACEHOLDER + # 0.071, # Price(Stop Price for stop-limit orders, Limit Price for limit orders) + # 0, # Average Price + # 0, # Trailing Price + # 0, # Auxiliary Limit price(for STOP LIMIT) + # null, # _PLACEHOLDER + # null, # _PLACEHOLDER + # null, # _PLACEHOLDER + # 0, # Hidden(0 if False, 1 if True) + # 0, # Placed ID(If another order caused self order to be placed(OCO) self will be that other order's ID) + # null, # _PLACEHOLDER + # null, # _PLACEHOLDER + # null, # _PLACEHOLDER + # "API>BFX", # Routing, indicates origin of action: BFX, ETHFX, API>BFX, API>ETHFX + # null, # _PLACEHOLDER + # null, # _PLACEHOLDER + # {"$F7":1} # additional meta information about the order( $F7 = IS_POST_ONLY(0 if False, 1 if True), $F33 = Leverage(int)) + # ] + # ], + # null, # CODE(work in progress) + # "SUCCESS", # Status of the request + # "Submitting 1 orders." # Message + # ] + # + status = self.safe_string(response, 6) + if status != 'SUCCESS': + errorCode = response[5] + errorText = response[7] + raise ExchangeError(self.id + ' ' + response[6] + ': ' + errorText + '(#' + errorCode + ')') + orders = self.safe_list(response, 4, []) + order = self.safe_list(orders, 0) + newOrder = {'result': order} + return self.parse_order(newOrder, market) + + def create_orders(self, orders: List[OrderRequest], params={}): + """ + create a list of trade orders + + https://docs.bitfinex.com/reference/rest-auth-order-multi + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + ordersRequests = [] + for i in range(0, len(orders)): + rawOrder = orders[i] + symbol = self.safe_string(rawOrder, 'symbol') + type = self.safe_string(rawOrder, 'type') + side = self.safe_string(rawOrder, 'side') + amount = self.safe_number(rawOrder, 'amount') + price = self.safe_number(rawOrder, 'price') + orderParams = self.safe_dict(rawOrder, 'params', {}) + orderRequest = self.create_order_request(symbol, type, side, amount, price, orderParams) + ordersRequests.append(['on', orderRequest]) + request: dict = { + 'ops': ordersRequests, + } + response = self.privatePostAuthWOrderMulti(request) + # + # [ + # 1706762515553, + # "ox_multi-req", + # null, + # null, + # [ + # [ + # 1706762515, + # "on-req", + # null, + # null, + # [ + # [139567428547,null,1706762515551,"tBTCUST",1706762515551,1706762515551,0.0001,0.0001,"EXCHANGE LIMIT",null,null,null,0,"ACTIVE",null,null,35000,0,0,0,null,null,null,0,0,null,null,null,"API>BFX",null,null,{}] + # ], + # null, + # "SUCCESS", + # "Submitting 1 orders." + # ], + # ], + # null, + # "SUCCESS", + # "Submitting 2 order operations." + # ] + # + results = [] + data = self.safe_list(response, 4, []) + for i in range(0, len(data)): + entry = data[i] + individualOrder = entry[4] + results.append({'result': individualOrder[0]}) + return self.parse_orders(results) + + def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders + + https://docs.bitfinex.com/reference/rest-auth-cancel-orders-multiple + + :param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + self.load_markets() + request: dict = { + 'all': 1, + } + response = self.privatePostAuthWOrderCancelMulti(self.extend(request, params)) + orders = self.safe_list(response, 4, []) + ordersList = [] + for i in range(0, len(orders)): + ordersList.append({'result': orders[i]}) + return self.parse_orders(ordersList) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://docs.bitfinex.com/reference/rest-auth-cancel-order + + :param str id: order id + :param str symbol: Not used by bitfinex cancelOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + cid = self.safe_value_2(params, 'cid', 'clientOrderId') # client order id + request = None + market = None + if symbol is not None: + market = self.market(symbol) + if cid is not None: + cidDate = self.safe_value(params, 'cidDate') # client order id date + if cidDate is None: + raise InvalidOrder(self.id + " canceling an order by clientOrderId('cid') requires both 'cid' and 'cid_date'('YYYY-MM-DD')") + request = { + 'cid': cid, + 'cid_date': cidDate, + } + params = self.omit(params, ['cid', 'clientOrderId']) + else: + request = { + 'id': int(id), + } + response = self.privatePostAuthWOrderCancel(self.extend(request, params)) + order = self.safe_value(response, 4) + newOrder = {'result': order} + return self.parse_order(newOrder, market) + + def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): + """ + cancel multiple orders at the same time + + https://docs.bitfinex.com/reference/rest-auth-cancel-orders-multiple + + :param str[] ids: order ids + :param str symbol: unified market symbol, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an array of `order structures ` + """ + self.load_markets() + numericIds = [] + for i in range(0, len(ids)): + # numericIds[i] = self.parse_to_numeric(ids[i]) + numericIds.append(self.parse_to_numeric(ids[i])) + request: dict = { + 'id': numericIds, + } + market = None + if symbol is not None: + market = self.market(symbol) + response = self.privatePostAuthWOrderCancelMulti(self.extend(request, params)) + # + # [ + # 1706740198811, + # "oc_multi-req", + # null, + # null, + # [ + # [ + # 139530205057, + # null, + # 1706740132275, + # "tBTCF0:USTF0", + # 1706740132276, + # 1706740132276, + # 0.0001, + # 0.0001, + # "LIMIT", + # null, + # null, + # null, + # 0, + # "ACTIVE", + # null, + # null, + # 39000, + # 0, + # 0, + # 0, + # null, + # null, + # null, + # 0, + # 0, + # null, + # null, + # null, + # "API>BFX", + # null, + # null, + # { + # "lev": 10, + # "$F33": 10 + # } + # ], + # ], + # null, + # "SUCCESS", + # "Submitting 2 order cancellations." + # ] + # + orders = self.safe_list(response, 4, []) + ordersList = [] + for i in range(0, len(orders)): + ordersList.append({'result': orders[i]}) + return self.parse_orders(ordersList, market) + + def fetch_open_order(self, id: str, symbol: Str = None, params={}): + """ + fetch an open order by it's id + + https://docs.bitfinex.com/reference/rest-auth-retrieve-orders + https://docs.bitfinex.com/reference/rest-auth-retrieve-orders-by-symbol + + :param str id: order id + :param str symbol: unified market symbol, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + request: dict = { + 'id': [int(id)], + } + orders = self.fetch_open_orders(symbol, None, None, self.extend(request, params)) + order = self.safe_value(orders, 0) + if order is None: + raise OrderNotFound(self.id + ' order ' + id + ' not found') + return order + + def fetch_closed_order(self, id: str, symbol: Str = None, params={}): + """ + fetch an open order by it's id + + https://docs.bitfinex.com/reference/rest-auth-retrieve-orders + https://docs.bitfinex.com/reference/rest-auth-retrieve-orders-by-symbol + + :param str id: order id + :param str symbol: unified market symbol, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + request: dict = { + 'id': [int(id)], + } + orders = self.fetch_closed_orders(symbol, None, None, self.extend(request, params)) + order = self.safe_value(orders, 0) + if order is None: + raise OrderNotFound(self.id + ' order ' + id + ' not found') + return order + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://docs.bitfinex.com/reference/rest-auth-retrieve-orders + https://docs.bitfinex.com/reference/rest-auth-retrieve-orders-by-symbol + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + request: dict = {} + market = None + response = None + if symbol is None: + response = self.privatePostAuthROrders(self.extend(request, params)) + else: + market = self.market(symbol) + request['symbol'] = market['id'] + response = self.privatePostAuthROrdersSymbol(self.extend(request, params)) + # + # [ + # [ + # 95408916206, # Order ID + # null, # Group Order ID + # 1653322349926, # Client Order ID + # "tDOGE:UST", # Market ID + # 1653322349926, # Created Timestamp in milliseconds + # 1653322349927, # Updated Timestamp in milliseconds + # -10, # Amount remaining(Positive means buy, negative means sell) + # -10, # Original amount + # "EXCHANGE LIMIT", # Order type + # null, # Previous Order Type + # null, # _PLACEHOLDER + # null, # _PLACEHOLDER + # 0, # Flags, see parseOrderFlags() + # "ACTIVE", # Order Status, see parseOrderStatus() + # null, # _PLACEHOLDER + # null, # _PLACEHOLDER + # 0.11, # Price + # 0, # Average Price + # 0, # Trailing Price + # 0, # Auxiliary Limit price(for STOP LIMIT) + # null, # _PLACEHOLDER + # null, # _PLACEHOLDER + # null, # _PLACEHOLDER + # 0, # Hidden(0 if False, 1 if True) + # 0, # Placed ID(If another order caused self order to be placed(OCO) self will be that other order's ID) + # null, # _PLACEHOLDER + # null, # _PLACEHOLDER + # null, # _PLACEHOLDER + # "API>BFX", # Routing, indicates origin of action: BFX, ETHFX, API>BFX, API>ETHFX + # null, # _PLACEHOLDER + # null, # _PLACEHOLDER + # {"$F7":1} # additional meta information about the order( $F7 = IS_POST_ONLY(0 if False, 1 if True), $F33 = Leverage(int)) + # ], + # ] + # + ordersList = [] + for i in range(0, len(response)): + ordersList.append({'result': response[i]}) + return self.parse_orders(ordersList, market, since, limit) + + def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://docs.bitfinex.com/reference/rest-auth-retrieve-orders + https://docs.bitfinex.com/reference/rest-auth-retrieve-orders-by-symbol + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch entries for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Order[]: a list of `order structures ` + """ + # returns the most recent closed or canceled orders up to circa two weeks ago + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchClosedOrders', 'paginate') + if paginate: + return self.fetch_paginated_call_dynamic('fetchClosedOrders', symbol, since, limit, params) + request: dict = {} + if since is not None: + request['start'] = since + if limit is not None: + request['limit'] = limit # default 25, max 2500 + request, params = self.handle_until_option('end', request, params) + market = None + response = None + if symbol is None: + response = self.privatePostAuthROrdersHist(self.extend(request, params)) + else: + market = self.market(symbol) + request['symbol'] = market['id'] + response = self.privatePostAuthROrdersSymbolHist(self.extend(request, params)) + # + # [ + # [ + # 95412102131, # Order ID + # null, # Group Order ID + # 1653325121798, # Client Order ID + # "tDOGE:UST", # Market ID + # 1653325122000, # Created Timestamp in milliseconds + # 1653325122000, # Updated Timestamp in milliseconds + # -10, # Amount remaining(Positive means buy, negative means sell) + # -10, # Original amount + # "EXCHANGE LIMIT", # Order type + # null, # Previous Order Type + # null, # Millisecond timestamp of Time-In-Force: automatic order cancellation + # null, # _PLACEHOLDER + # "4096", # Flags, see parseOrderFlags() + # "POSTONLY CANCELED", # Order Status, see parseOrderStatus() + # null, # _PLACEHOLDER + # null, # _PLACEHOLDER + # 0.071, # Price + # 0, # Average Price + # 0, # Trailing Price + # 0, # Auxiliary Limit price(for STOP LIMIT) + # null, # _PLACEHOLDER + # null, # _PLACEHOLDER + # null, # _PLACEHOLDER + # 0, # Notify(0 if False, 1 if True) + # 0, # Hidden(0 if False, 1 if True) + # null, # Placed ID(If another order caused self order to be placed(OCO) self will be that other order's ID) + # null, # _PLACEHOLDER + # null, # _PLACEHOLDER + # "API>BFX", # Routing, indicates origin of action: BFX, ETHFX, API>BFX, API>ETHFX + # null, # _PLACEHOLDER + # null, # _PLACEHOLDER + # {"_$F7":1} # additional meta information about the order( _$F7 = IS_POST_ONLY(0 if False, 1 if True), _$F33 = Leverage(int)) + # ] + # ] + # + ordersList = [] + for i in range(0, len(response)): + ordersList.append({'result': response[i]}) + return self.parse_orders(ordersList, market, since, limit) + + def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all the trades made from a single order + + https://docs.bitfinex.com/reference/rest-auth-order-trades + + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrderTrades() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + orderId = int(id) + request: dict = { + 'id': orderId, + 'symbol': market['id'], + } + # valid for trades upto 10 days old + response = self.privatePostAuthROrderSymbolIdTrades(self.extend(request, params)) + tradesList = [] + for i in range(0, len(response)): + tradesList.append({'result': response[i]}) # convert to array of dicts to match parseOrder signature + return self.parse_trades(tradesList, market, since, limit) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://docs.bitfinex.com/reference/rest-auth-trades + https://docs.bitfinex.com/reference/rest-auth-trades-by-symbol + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = None + request: dict = { + 'end': self.milliseconds(), + } + if since is not None: + request['start'] = since + if limit is not None: + request['limit'] = limit # default 25, max 1000 + response = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + response = self.privatePostAuthRTradesSymbolHist(self.extend(request, params)) + else: + response = self.privatePostAuthRTradesHist(self.extend(request, params)) + tradesList = [] + for i in range(0, len(response)): + tradesList.append({'result': response[i]}) # convert to array of dicts to match parseOrder signature + return self.parse_trades(tradesList, market, since, limit) + + def create_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + create a currency deposit address + + https://docs.bitfinex.com/reference/rest-auth-deposit-address + + :param str code: unified currency code of the currency for the deposit address + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + self.load_markets() + request: dict = { + 'op_renew': 1, + } + return self.fetch_deposit_address(code, self.extend(request, params)) + + def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://docs.bitfinex.com/reference/rest-auth-deposit-address + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + self.load_markets() + currency = self.currency(code) + # if not provided explicitly we will try to match using the currency name + network = self.safe_string(params, 'network', code) + currencyNetworks = self.safe_value(currency, 'networks', {}) + currencyNetwork = self.safe_value(currencyNetworks, network) + networkId = self.safe_string(currencyNetwork, 'id') + if networkId is None: + raise ArgumentsRequired(self.id + " fetchDepositAddress() could not find a network for '" + code + "'. You can specify it by providing the 'network' value inside params") + wallet = self.safe_string(params, 'wallet', 'exchange') # 'exchange', 'margin', 'funding' and also old labels 'exchange', 'trading', 'deposit', respectively + params = self.omit(params, 'network', 'wallet') + request: dict = { + 'method': networkId, + 'wallet': wallet, + 'op_renew': 0, # a value of 1 will generate a new address + } + response = self.privatePostAuthWDepositAddress(self.extend(request, params)) + # + # [ + # 1582269616687, # MTS Millisecond Time Stamp of the update + # "acc_dep", # TYPE Purpose of notification "acc_dep" for account deposit + # null, # MESSAGE_ID unique ID of the message + # null, # not documented + # [ + # null, # PLACEHOLDER + # "BITCOIN", # METHOD Method of deposit + # "BTC", # CURRENCY_CODE Currency code of new address + # null, # PLACEHOLDER + # "1BC9PZqpUmjyEB54uggn8TFKj49zSDYzqG", # ADDRESS + # null, # POOL_ADDRESS + # ], + # null, # CODE null or integer work in progress + # "SUCCESS", # STATUS Status of the notification, SUCCESS, ERROR, FAILURE + # "success", # TEXT Text of the notification + # ] + # + result = self.safe_value(response, 4, []) + poolAddress = self.safe_string(result, 5) + address = self.safe_string(result, 4) if (poolAddress is None) else poolAddress + tag = None if (poolAddress is None) else self.safe_string(result, 4) + self.check_address(address) + return { + 'currency': code, + 'address': address, + 'tag': tag, + 'network': None, + 'info': response, + } + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'SUCCESS': 'ok', + 'COMPLETED': 'ok', + 'ERROR': 'failed', + 'FAILURE': 'failed', + 'CANCELED': 'canceled', + 'PENDING APPROVAL': 'pending', + 'PENDING': 'pending', + 'PENDING REVIEW': 'pending', + 'PENDING CANCELLATION': 'pending', + 'SENDING': 'pending', + 'USER APPROVED': 'pending', + } + return self.safe_string(statuses, status, status) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # withdraw + # + # [ + # 1582271520931, # MTS Millisecond Time Stamp of the update + # "acc_wd-req", # TYPE Purpose of notification "acc_wd-req" account withdrawal request + # null, # MESSAGE_ID unique ID of the message + # null, # not documented + # [ + # 0, # WITHDRAWAL_ID Unique Withdrawal ID + # null, # PLACEHOLDER + # "bitcoin", # METHOD Method of withdrawal + # null, # PAYMENT_ID Payment ID if relevant + # "exchange", # WALLET Sending wallet + # 1, # AMOUNT Amount of Withdrawal less fee + # null, # PLACEHOLDER + # null, # PLACEHOLDER + # 0.0004, # WITHDRAWAL_FEE Fee on withdrawal + # ], + # null, # CODE null or integer Work in progress + # "SUCCESS", # STATUS Status of the notification, it may vary over time SUCCESS, ERROR, FAILURE + # "Invalid bitcoin address(abcdef)", # TEXT Text of the notification + # ] + # + # fetchDepositsWithdrawals + # + # [ + # 13293039, # ID + # "ETH", # CURRENCY + # "ETHEREUM", # CURRENCY_NAME + # null, + # null, + # 1574175052000, # MTS_STARTED + # 1574181326000, # MTS_UPDATED + # null, + # null, + # "CANCELED", # STATUS + # null, + # null, + # -0.24, # AMOUNT, negative for withdrawals + # -0.00135, # FEES + # null, + # null, + # "0x38110e0Fc932CB2BE...........", # DESTINATION_ADDRESS + # null, + # null, + # null, + # "0x523ec8945500.....................................", # TRANSACTION_ID + # "Purchase of 100 pizzas", # WITHDRAW_TRANSACTION_NOTE, might also be: null + # ] + # + transactionLength = len(transaction) + timestamp = None + updated = None + code = None + amount = None + id = None + status = None + tag = None + type = None + feeCost = None + txid = None + addressTo = None + network = None + comment = None + if transactionLength == 8: + data = self.safe_value(transaction, 4, []) + timestamp = self.safe_integer(transaction, 0) + if currency is not None: + code = currency['code'] + feeCost = self.safe_string(data, 8) + if feeCost is not None: + feeCost = Precise.string_abs(feeCost) + amount = self.safe_number(data, 5) + id = self.safe_integer(data, 0) + status = 'ok' + if id == 0: + id = None + status = 'failed' + tag = self.safe_string(data, 3) + type = 'withdrawal' + networkId = self.safe_string(data, 2) + network = self.network_id_to_code(networkId.upper()) # withdraw returns in lowercase + elif transactionLength == 22: + id = self.safe_string(transaction, 0) + currencyId = self.safe_string(transaction, 1) + code = self.safe_currency_code(currencyId, currency) + networkId = self.safe_string(transaction, 2) + network = self.network_id_to_code(networkId) + timestamp = self.safe_integer(transaction, 5) + updated = self.safe_integer(transaction, 6) + status = self.parse_transaction_status(self.safe_string(transaction, 9)) + signedAmount = self.safe_string(transaction, 12) + amount = Precise.string_abs(signedAmount) + if signedAmount is not None: + if Precise.string_lt(signedAmount, '0'): + type = 'withdrawal' + else: + type = 'deposit' + feeCost = self.safe_string(transaction, 13) + if feeCost is not None: + feeCost = Precise.string_abs(feeCost) + addressTo = self.safe_string(transaction, 16) + txid = self.safe_string(transaction, 20) + comment = self.safe_string(transaction, 21) + return { + 'info': transaction, + 'id': id, + 'txid': txid, + 'type': type, + 'currency': code, + 'network': network, + 'amount': self.parse_number(amount), + 'status': status, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'address': addressTo, # self is actually the tag for XRP transfers(the address is missing) + 'addressFrom': None, + 'addressTo': addressTo, + 'tag': tag, # refix it properly for the tag from description + 'tagFrom': None, + 'tagTo': tag, + 'updated': updated, + 'comment': comment, + 'internal': None, + 'fee': { + 'currency': code, + 'cost': self.parse_number(feeCost), + 'rate': None, + }, + } + + def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://docs.bitfinex.com/reference/rest-auth-summary + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + self.load_markets() + response = self.privatePostAuthRSummary(params) + # + # Response Spec: + # [ + # PLACEHOLDER, + # PLACEHOLDER, + # PLACEHOLDER, + # PLACEHOLDER, + # [ + # [ + # MAKER_FEE, + # MAKER_FEE, + # MAKER_FEE, + # PLACEHOLDER, + # PLACEHOLDER, + # DERIV_REBATE + # ], + # [ + # TAKER_FEE_TO_CRYPTO, + # TAKER_FEE_TO_STABLE, + # TAKER_FEE_TO_FIAT, + # PLACEHOLDER, + # PLACEHOLDER, + # DERIV_TAKER_FEE + # ] + # ], + # PLACEHOLDER, + # PLACEHOLDER, + # PLACEHOLDER, + # PLACEHOLDER, + # { + # LEO_LEV, + # LEO_AMOUNT_AVG + # } + # ] + # + # Example response: + # + # [ + # null, + # null, + # null, + # null, + # [ + # [0.001, 0.001, 0.001, null, null, 0.0002], + # [0.002, 0.002, 0.002, null, null, 0.00065] + # ], + # [ + # [ + # { + # "curr": "Total(USD)", + # "vol": "0", + # "vol_safe": "0", + # "vol_maker": "0", + # "vol_BFX": "0", + # "vol_BFX_safe": "0", + # "vol_BFX_maker": "0" + # } + # ], + # {}, + # 0 + # ], + # [null, {}, 0], + # null, + # null, + # {leo_lev: "0", leo_amount_avg: "0"} + # ] + # + result: dict = {} + fiat = self.safe_value(self.options, 'fiat', {}) + feeData = self.safe_value(response, 4, []) + makerData = self.safe_value(feeData, 0, []) + takerData = self.safe_value(feeData, 1, []) + makerFee = self.safe_number(makerData, 0) + makerFeeFiat = self.safe_number(makerData, 2) + makerFeeDeriv = self.safe_number(makerData, 5) + takerFee = self.safe_number(takerData, 0) + takerFeeFiat = self.safe_number(takerData, 2) + takerFeeDeriv = self.safe_number(takerData, 5) + for i in range(0, len(self.symbols)): + symbol = self.symbols[i] + market = self.market(symbol) + fee = { + 'info': response, + 'symbol': symbol, + 'percentage': True, + 'tierBased': True, + } + if market['quote'] in fiat: + fee['maker'] = makerFeeFiat + fee['taker'] = takerFeeFiat + elif market['contract']: + fee['maker'] = makerFeeDeriv + fee['taker'] = takerFeeDeriv + else: # TODO check if stable coin + fee['maker'] = makerFee + fee['taker'] = takerFee + result[symbol] = fee + return result + + def fetch_deposits_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch history of deposits and withdrawals + + https://docs.bitfinex.com/reference/movement-info + https://docs.bitfinex.com/reference/rest-auth-movements + + :param str [code]: unified currency code for the currency of the deposit/withdrawals, default is None + :param int [since]: timestamp in ms of the earliest deposit/withdrawal, default is None + :param int [limit]: max number of deposit/withdrawals to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `transaction structure ` + """ + self.load_markets() + currency = None + request: dict = {} + if since is not None: + request['start'] = since + if limit is not None: + request['limit'] = limit # max 1000 + response = None + if code is not None: + currency = self.currency(code) + request['currency'] = currency['uppercaseId'] + response = self.privatePostAuthRMovementsCurrencyHist(self.extend(request, params)) + else: + response = self.privatePostAuthRMovementsHist(self.extend(request, params)) + # + # [ + # [ + # 13293039, # ID + # "ETH", # CURRENCY + # "ETHEREUM", # CURRENCY_NAME + # null, + # null, + # 1574175052000, # MTS_STARTED + # 1574181326000, # MTS_UPDATED + # null, + # null, + # "CANCELED", # STATUS + # null, + # null, + # -0.24, # AMOUNT, negative for withdrawals + # -0.00135, # FEES + # null, + # null, + # "0x38110e0Fc932CB2BE...........", # DESTINATION_ADDRESS + # null, + # null, + # null, + # "0x523ec8945500.....................................", # TRANSACTION_ID + # "Purchase of 100 pizzas", # WITHDRAW_TRANSACTION_NOTE, might also be: null + # ] + # ] + # + return self.parse_transactions(response, currency, since, limit) + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://docs.bitfinex.com/reference/rest-auth-withdraw + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + self.check_address(address) + self.load_markets() + currency = self.currency(code) + # if not provided explicitly we will try to match using the currency name + network = self.safe_string(params, 'network', code) + params = self.omit(params, 'network') + currencyNetworks = self.safe_value(currency, 'networks', {}) + currencyNetwork = self.safe_value(currencyNetworks, network) + networkId = self.safe_string(currencyNetwork, 'id') + if networkId is None: + raise ArgumentsRequired(self.id + " withdraw() could not find a network for '" + code + "'. You can specify it by providing the 'network' value inside params") + wallet = self.safe_string(params, 'wallet', 'exchange') # 'exchange', 'margin', 'funding' and also old labels 'exchange', 'trading', 'deposit', respectively + params = self.omit(params, 'network', 'wallet') + request: dict = { + 'method': networkId, + 'wallet': wallet, + 'amount': self.number_to_string(amount), + 'address': address, + } + if tag is not None: + request['payment_id'] = tag + withdrawOptions = self.safe_value(self.options, 'withdraw', {}) + includeFee = self.safe_bool(withdrawOptions, 'includeFee', False) + if includeFee: + request['fee_deduct'] = 1 + response = self.privatePostAuthWWithdraw(self.extend(request, params)) + # + # [ + # 1582271520931, # MTS Millisecond Time Stamp of the update + # "acc_wd-req", # TYPE Purpose of notification "acc_wd-req" account withdrawal request + # null, # MESSAGE_ID unique ID of the message + # null, # not documented + # [ + # 0, # WITHDRAWAL_ID Unique Withdrawal ID + # null, # PLACEHOLDER + # "bitcoin", # METHOD Method of withdrawal + # null, # PAYMENT_ID Payment ID if relevant + # "exchange", # WALLET Sending wallet + # 1, # AMOUNT Amount of Withdrawal less fee + # null, # PLACEHOLDER + # null, # PLACEHOLDER + # 0.0004, # WITHDRAWAL_FEE Fee on withdrawal + # ], + # null, # CODE null or integer Work in progress + # "SUCCESS", # STATUS Status of the notification, it may vary over time SUCCESS, ERROR, FAILURE + # "Invalid bitcoin address(abcdef)", # TEXT Text of the notification + # ] + # + # in case of failure: + # + # [ + # "error", + # 10001, + # "Momentary balance check. Please wait few seconds and try the transfer again." + # ] + # + statusMessage = self.safe_string(response, 0) + if statusMessage == 'error': + feedback = self.id + ' ' + response + message = self.safe_string(response, 2, '') + # same message v1 + self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + raise ExchangeError(feedback) # unknown message + text = self.safe_string(response, 7) + if text != 'success': + self.throw_broadly_matched_exception(self.exceptions['broad'], text, text) + return self.parse_transaction(response, currency) + + def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://docs.bitfinex.com/reference/rest-auth-positions + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + self.load_markets() + symbols = self.market_symbols(symbols) + response = self.privatePostAuthRPositions(params) + # + # [ + # [ + # "tBTCUSD", # SYMBOL + # "ACTIVE", # STATUS + # 0.0195, # AMOUNT + # 8565.0267019, # BASE_PRICE + # 0, # MARGIN_FUNDING + # 0, # MARGIN_FUNDING_TYPE + # -0.33455568705000516, # PL + # -0.0003117550117425625, # PL_PERC + # 7045.876419249083, # PRICE_LIQ + # 3.0673001895895604, # LEVERAGE + # null, # _PLACEHOLDER + # 142355652, # POSITION_ID + # 1574002216000, # MTS_CREATE + # 1574002216000, # MTS_UPDATE + # null, # _PLACEHOLDER + # 0, # TYPE + # null, # _PLACEHOLDER + # 0, # COLLATERAL + # 0, # COLLATERAL_MIN + # # META + # { + # "reason":"TRADE", + # "order_id":34271018124, + # "liq_stage":null, + # "trade_price":"8565.0267019", + # "trade_amount":"0.0195", + # "order_id_oppo":34277498022 + # } + # ] + # ] + # + positionsList = [] + for i in range(0, len(response)): + positionsList.append({'result': response[i]}) + return self.parse_positions(positionsList, symbols) + + def parse_position(self, position: dict, market: Market = None): + # + # [ + # "tBTCUSD", # SYMBOL + # "ACTIVE", # STATUS + # 0.0195, # AMOUNT + # 8565.0267019, # BASE_PRICE + # 0, # MARGIN_FUNDING + # 0, # MARGIN_FUNDING_TYPE + # -0.33455568705000516, # PL + # -0.0003117550117425625, # PL_PERC + # 7045.876419249083, # PRICE_LIQ + # 3.0673001895895604, # LEVERAGE + # null, # _PLACEHOLDER + # 142355652, # POSITION_ID + # 1574002216000, # MTS_CREATE + # 1574002216000, # MTS_UPDATE + # null, # _PLACEHOLDER + # 0, # TYPE + # null, # _PLACEHOLDER + # 0, # COLLATERAL + # 0, # COLLATERAL_MIN + # # META + # { + # "reason": "TRADE", + # "order_id": 34271018124, + # "liq_stage": null, + # "trade_price": "8565.0267019", + # "trade_amount": "0.0195", + # "order_id_oppo": 34277498022 + # } + # ] + # + positionList = self.safe_list(position, 'result') + marketId = self.safe_string(positionList, 0) + amount = self.safe_string(positionList, 2) + timestamp = self.safe_integer(positionList, 12) + meta = self.safe_string(positionList, 19) + tradePrice = self.safe_string(meta, 'trade_price') + tradeAmount = self.safe_string(meta, 'trade_amount') + return self.safe_position({ + 'info': positionList, + 'id': self.safe_string(positionList, 11), + 'symbol': self.safe_symbol(marketId, market), + 'notional': self.parse_number(amount), + 'marginMode': 'isolated', # derivatives use isolated, margin uses cross, https://support.bitfinex.com/hc/en-us/articles/360035475374-Derivatives-Trading-on-Bitfinex + 'liquidationPrice': self.safe_number(positionList, 8), + 'entryPrice': self.safe_number(positionList, 3), + 'unrealizedPnl': self.safe_number(positionList, 6), + 'percentage': self.safe_number(positionList, 7), + 'contracts': None, + 'contractSize': None, + 'markPrice': None, + 'lastPrice': None, + 'side': 'long' if Precise.string_gt(amount, '0') else 'short', + 'hedged': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastUpdateTimestamp': self.safe_integer(positionList, 13), + 'maintenanceMargin': self.safe_number(positionList, 18), + 'maintenanceMarginPercentage': None, + 'collateral': self.safe_number(positionList, 17), + 'initialMargin': self.parse_number(Precise.string_mul(tradeAmount, tradePrice)), + 'initialMarginPercentage': None, + 'leverage': self.safe_number(positionList, 9), + 'marginRatio': None, + 'stopLossPrice': None, + 'takeProfitPrice': None, + }) + + def nonce(self): + return self.milliseconds() + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + request = '/' + self.implode_params(path, params) + query = self.omit(params, self.extract_params(path)) + if api == 'v1': + request = api + request + else: + request = self.version + request + url = self.urls['api'][api] + '/' + request + if api == 'public': + if query: + url += '?' + self.urlencode(query) + if api == 'private': + self.check_required_credentials() + nonce = str(self.nonce()) + body = self.json(query) + auth = '/api/' + request + nonce + body + signature = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha384) + headers = { + 'bfx-nonce': nonce, + 'bfx-apikey': self.apiKey, + 'bfx-signature': signature, + 'Content-Type': 'application/json', + } + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, statusCode, statusText, url, method, headers, body, response, requestHeaders, requestBody): + # ["error", 11010, "ratelimit: error"] + if response is not None: + if not isinstance(response, list): + message = self.safe_string_2(response, 'message', 'error') + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + raise ExchangeError(self.id + ' ' + body) + elif response == '': + raise ExchangeError(self.id + ' returned empty response') + if statusCode == 429: + raise RateLimitExceeded(self.id + ' ' + body) + if statusCode == 500: + # See https://docs.bitfinex.com/docs/abbreviations-glossary#section-errorinfo-codes + errorCode = self.safe_string(response, 1, '') + errorText = self.safe_string(response, 2, '') + feedback = self.id + ' ' + errorText + self.throw_broadly_matched_exception(self.exceptions['broad'], errorText, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], errorText, feedback) + raise ExchangeError(self.id + ' ' + errorText + '(#' + errorCode + ')') + return response + + def parse_ledger_entry_type(self, type: Str): + if type is None: + return None + elif type.find('fee') >= 0 or type.find('charged') >= 0: + return 'fee' + elif type.find('rebate') >= 0: + return 'rebate' + elif type.find('deposit') >= 0 or type.find('withdrawal') >= 0: + return 'transaction' + elif type.find('transfer') >= 0: + return 'transfer' + elif type.find('payment') >= 0: + return 'payout' + elif type.find('exchange') >= 0 or type.find('position') >= 0: + return 'trade' + else: + return type + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + # + # [ + # [ + # 2531822314, # ID: Ledger identifier + # "USD", # CURRENCY: The symbol of the currency(ex. "BTC") + # null, # PLACEHOLDER + # 1573521810000, # MTS: Timestamp in milliseconds + # null, # PLACEHOLDER + # 0.01644445, # AMOUNT: Amount of funds moved + # 0, # BALANCE: New balance + # null, # PLACEHOLDER + # "Settlement @ 185.79 on wallet margin" # DESCRIPTION: Description of ledger transaction + # ] + # ] + # + itemList = self.safe_list(item, 'result', []) + type = None + id = self.safe_string(itemList, 0) + currencyId = self.safe_string(itemList, 1) + code = self.safe_currency_code(currencyId, currency) + currency = self.safe_currency(currencyId, currency) + timestamp = self.safe_integer(itemList, 3) + amount = self.safe_number(itemList, 5) + after = self.safe_number(itemList, 6) + description = self.safe_string(itemList, 8) + if description is not None: + parts = description.split(' @ ') + first = self.safe_string_lower(parts, 0) + type = self.parse_ledger_entry_type(first) + return self.safe_ledger_entry({ + 'info': item, + 'id': id, + 'direction': None, + 'account': None, + 'referenceId': id, + 'referenceAccount': None, + 'type': type, + 'currency': code, + 'amount': amount, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'before': None, + 'after': after, + 'status': None, + 'fee': None, + }, currency) + + def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + + https://docs.bitfinex.com/reference/rest-auth-ledgers + + :param str [code]: unified currency code, default is None + :param int [since]: timestamp in ms of the earliest ledger entry, default is None + :param int [limit]: max number of ledger entries to return, default is None, max is 2500 + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest ledger entry + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict: a `ledger structure ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchLedger', 'paginate') + if paginate: + return self.fetch_paginated_call_dynamic('fetchLedger', code, since, limit, params, 2500) + currency = None + request: dict = {} + if since is not None: + request['start'] = since + if limit is not None: + request['limit'] = limit + request, params = self.handle_until_option('end', request, params) + response = None + if code is not None: + currency = self.currency(code) + request['currency'] = currency['uppercaseId'] + response = self.privatePostAuthRLedgersCurrencyHist(self.extend(request, params)) + else: + response = self.privatePostAuthRLedgersHist(self.extend(request, params)) + # + # [ + # [ + # 2531822314, # ID: Ledger identifier + # "USD", # CURRENCY: The symbol of the currency(ex. "BTC") + # null, # PLACEHOLDER + # 1573521810000, # MTS: Timestamp in milliseconds + # null, # PLACEHOLDER + # 0.01644445, # AMOUNT: Amount of funds moved + # 0, # BALANCE: New balance + # null, # PLACEHOLDER + # "Settlement @ 185.79 on wallet margin" # DESCRIPTION: Description of ledger transaction + # ] + # ] + # + ledgerObjects = [] + for i in range(0, len(response)): + item = response[i] + ledgerObjects.append({'result': item}) + return self.parse_ledger(ledgerObjects, currency, since, limit) + + def fetch_funding_rates(self, symbols: Strings = None, params={}) -> FundingRates: + """ + fetch the current funding rate for multiple symbols + + https://docs.bitfinex.com/reference/rest-public-derivatives-status + + :param str[] symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `funding rate structures ` + """ + if symbols is None: + raise ArgumentsRequired(self.id + ' fetchFundingRates() requires a symbols argument') + self.load_markets() + marketIds = self.market_ids(symbols) + request: dict = { + 'keys': ','.join(marketIds), + } + response = self.publicGetStatusDeriv(self.extend(request, params)) + # + # [ + # [ + # "tBTCF0:USTF0", + # 1691165059000, + # null, + # 29297.851276225, + # 29277.5, + # null, + # 36950860.76010306, + # null, + # 1691193600000, + # 0.00000527, + # 82, + # null, + # 0.00014548, + # null, + # null, + # 29278.8925, + # null, + # null, + # 9636.07644994, + # null, + # null, + # null, + # 0.0005, + # 0.0025 + # ] + # ] + # + return self.parse_funding_rates(response, symbols) + + def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical funding rate prices + + https://docs.bitfinex.com/reference/rest-public-derivatives-status-history + + :param str symbol: unified market symbol + :param int [since]: timestamp in ms of the earliest funding rate entry + :param int [limit]: max number of funding rate entrys to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest funding rate + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict: a `funding rate structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchFundingRateHistory() requires a symbol argument') + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchFundingRateHistory', 'paginate') + if paginate: + return self.fetch_paginated_call_deterministic('fetchFundingRateHistory', symbol, since, limit, '8h', params, 5000) + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if since is not None: + request['start'] = since + request, params = self.handle_until_option('end', request, params) + response = self.publicGetStatusDerivSymbolHist(self.extend(request, params)) + # + # [ + # [ + # "tBTCF0:USTF0", + # 1691165059000, + # null, + # 29297.851276225, + # 29277.5, + # null, + # 36950860.76010306, + # null, + # 1691193600000, + # 0.00000527, + # 82, + # null, + # 0.00014548, + # null, + # null, + # 29278.8925, + # null, + # null, + # 9636.07644994, + # null, + # null, + # null, + # 0.0005, + # 0.0025 + # ] + # ] + # + rates = [] + for i in range(0, len(response)): + fr = response[i] + rate = self.parse_funding_rate_history(fr, market) + rates.append(rate) + reversedArray = [] + rawRates = self.filter_by_symbol_since_limit(rates, symbol, since, limit) + ratesLength = len(rawRates) + for i in range(0, ratesLength): + index = ratesLength - i - 1 + valueAtIndex = rawRates[index] + reversedArray.append(valueAtIndex) + return reversedArray + + def parse_funding_rate(self, contract, market: Market = None) -> FundingRate: + # + # [ + # "tBTCF0:USTF0", + # 1691165059000, + # null, + # 29297.851276225, + # 29277.5, + # null, + # 36950860.76010306, + # null, + # 1691193600000, + # 0.00000527, + # 82, + # null, + # 0.00014548, + # null, + # null, + # 29278.8925, + # null, + # null, + # 9636.07644994, + # null, + # null, + # null, + # 0.0005, + # 0.0025 + # ] + # + marketId = self.safe_string(contract, 0) + timestamp = self.safe_integer(contract, 1) + nextFundingTimestamp = self.safe_integer(contract, 8) + return { + 'info': contract, + 'symbol': self.safe_symbol(marketId, market), + 'markPrice': self.safe_number(contract, 15), + 'indexPrice': self.safe_number(contract, 3), + 'interestRate': None, + 'estimatedSettlePrice': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'fundingRate': self.safe_number(contract, 12), + 'fundingTimestamp': None, + 'fundingDatetime': None, + 'nextFundingRate': self.safe_number(contract, 9), + 'nextFundingTimestamp': nextFundingTimestamp, + 'nextFundingDatetime': self.iso8601(nextFundingTimestamp), + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': None, + } + + def parse_funding_rate_history(self, contract, market: Market = None): + # + # [ + # 1691165494000, + # null, + # 29278.95838065, + # 29260.5, + # null, + # 36950860.76010305, + # null, + # 1691193600000, + # 0.00001449, + # 222, + # null, + # 0.00014548, + # null, + # null, + # 29260.005, + # null, + # null, + # 9635.86484562, + # null, + # null, + # null, + # 0.0005, + # 0.0025 + # ] + # + timestamp = self.safe_integer(contract, 0) + nextFundingTimestamp = self.safe_integer(contract, 7) + return { + 'info': contract, + 'symbol': self.safe_symbol(None, market), + 'markPrice': self.safe_number(contract, 14), + 'indexPrice': self.safe_number(contract, 2), + 'interestRate': None, + 'estimatedSettlePrice': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'fundingRate': self.safe_number(contract, 11), + 'fundingTimestamp': None, + 'fundingDatetime': None, + 'nextFundingRate': self.safe_number(contract, 8), + 'nextFundingTimestamp': nextFundingTimestamp, + 'nextFundingDatetime': self.iso8601(nextFundingTimestamp), + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + } + + def fetch_open_interests(self, symbols: Strings = None, params={}): + """ + Retrieves the open interest for a list of symbols + + https://docs.bitfinex.com/reference/rest-public-derivatives-status + + :param str[] [symbols]: a list of unified CCXT market symbols + :param dict [params]: exchange specific parameters + :returns dict[]: a list of `open interest structures ` + """ + self.load_markets() + symbols = self.market_symbols(symbols) + marketIds = ['ALL'] + if symbols is not None: + marketIds = self.market_ids(symbols) + request: dict = { + 'keys': ','.join(marketIds), + } + response = self.publicGetStatusDeriv(self.extend(request, params)) + # + # [ + # [ + # "tXRPF0:USTF0", # market id + # 1706256986000, # millisecond timestamp + # null, + # 0.512705, # derivative mid price + # 0.512395, # underlying spot mid price + # null, + # 37671483.04, # insurance fund balance + # null, + # 1706284800000, # timestamp of next funding + # 0.00002353, # accrued funding for next period + # 317, # next funding step + # null, + # 0, # current funding + # null, + # null, + # 0.5123016, # mark price + # null, + # null, + # 2233562.03115, # open interest in contracts + # null, + # null, + # null, + # 0.0005, # average spread without funding payment + # 0.0025 # funding payment cap + # ] + # ] + # + return self.parse_open_interests(response, symbols) + + def fetch_open_interest(self, symbol: str, params={}): + """ + retrieves the open interest of a contract trading pair + + https://docs.bitfinex.com/reference/rest-public-derivatives-status + + :param str symbol: unified CCXT market symbol + :param dict [params]: exchange specific parameters + :returns dict: an `open interest structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'keys': market['id'], + } + response = self.publicGetStatusDeriv(self.extend(request, params)) + # + # [ + # [ + # "tXRPF0:USTF0", # market id + # 1706256986000, # millisecond timestamp + # null, + # 0.512705, # derivative mid price + # 0.512395, # underlying spot mid price + # null, + # 37671483.04, # insurance fund balance + # null, + # 1706284800000, # timestamp of next funding + # 0.00002353, # accrued funding for next period + # 317, # next funding step + # null, + # 0, # current funding + # null, + # null, + # 0.5123016, # mark price + # null, + # null, + # 2233562.03115, # open interest in contracts + # null, + # null, + # null, + # 0.0005, # average spread without funding payment + # 0.0025 # funding payment cap + # ] + # ] + # + oi = self.safe_list(response, 0) + return self.parse_open_interest(oi, market) + + def fetch_open_interest_history(self, symbol: str, timeframe='1m', since: Int = None, limit: Int = None, params={}): + """ + retrieves the open interest history of a currency + + https://docs.bitfinex.com/reference/rest-public-derivatives-status-history + + :param str symbol: unified CCXT market symbol + :param str timeframe: the time period of each row of data, not used by bitfinex + :param int [since]: the time in ms of the earliest record to retrieve unix timestamp + :param int [limit]: the number of records in the response + :param dict [params]: exchange specific parameters + :param int [params.until]: the time in ms of the latest record to retrieve unix timestamp + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns: An array of `open interest structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOpenInterestHistory', 'paginate') + if paginate: + return self.fetch_paginated_call_deterministic('fetchOpenInterestHistory', symbol, since, limit, '8h', params, 5000) + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if since is not None: + request['start'] = since + if limit is not None: + request['limit'] = limit + request, params = self.handle_until_option('end', request, params) + response = self.publicGetStatusDerivSymbolHist(self.extend(request, params)) + # + # [ + # [ + # 1706295191000, # timestamp + # null, + # 42152.425382, # derivative mid price + # 42133, # spot mid price + # null, + # 37671589.7853521, # insurance fund balance + # null, + # 1706313600000, # timestamp of next funding + # 0.00018734, # accrued funding for next period + # 3343, # next funding step + # null, + # 0.00007587, # current funding + # null, + # null, + # 42134.1, # mark price + # null, + # null, + # 5775.20348804, # open interest number of contracts + # null, + # null, + # null, + # 0.0005, # average spread without funding payment + # 0.0025 # funding payment cap + # ], + # ] + # + return self.parse_open_interests_history(response, market, since, limit) + + def parse_open_interest(self, interest, market: Market = None): + # + # fetchOpenInterest: + # + # [ + # "tXRPF0:USTF0", # market id + # 1706256986000, # millisecond timestamp + # null, + # 0.512705, # derivative mid price + # 0.512395, # underlying spot mid price + # null, + # 37671483.04, # insurance fund balance + # null, + # 1706284800000, # timestamp of next funding + # 0.00002353, # accrued funding for next period + # 317, # next funding step + # null, + # 0, # current funding + # null, + # null, + # 0.5123016, # mark price + # null, + # null, + # 2233562.03115, # open interest in contracts + # null, + # null, + # null, + # 0.0005, # average spread without funding payment + # 0.0025 # funding payment cap + # ] + # + # fetchOpenInterestHistory: + # + # [ + # 1706295191000, # timestamp + # null, + # 42152.425382, # derivative mid price + # 42133, # spot mid price + # null, + # 37671589.7853521, # insurance fund balance + # null, + # 1706313600000, # timestamp of next funding + # 0.00018734, # accrued funding for next period + # 3343, # next funding step + # null, + # 0.00007587, # current funding + # null, + # null, + # 42134.1, # mark price + # null, + # null, + # 5775.20348804, # open interest number of contracts + # null, + # null, + # null, + # 0.0005, # average spread without funding payment + # 0.0025 # funding payment cap + # ] + # + interestLength = len(interest) + openInterestIndex = 17 if (interestLength == 23) else 18 + timestamp = self.safe_integer(interest, 1) + marketId = self.safe_string(interest, 0) + return self.safe_open_interest({ + 'symbol': self.safe_symbol(marketId, market, None, 'swap'), + 'openInterestAmount': self.safe_number(interest, openInterestIndex), + 'openInterestValue': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'info': interest, + }, market) + + def fetch_liquidations(self, symbol: str, since: Int = None, limit: Int = None, params={}): + """ + retrieves the public liquidations of a trading pair + + https://docs.bitfinex.com/reference/rest-public-liquidations + + :param str symbol: unified CCXT market symbol + :param int [since]: the earliest time in ms to fetch liquidations for + :param int [limit]: the maximum number of liquidation structures to retrieve + :param dict [params]: exchange specific parameters + :param int [params.until]: timestamp in ms of the latest liquidation + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict: an array of `liquidation structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchLiquidations', 'paginate') + if paginate: + return self.fetch_paginated_call_deterministic('fetchLiquidations', symbol, since, limit, '8h', params, 500) + market = self.market(symbol) + request: dict = {} + if since is not None: + request['start'] = since + if limit is not None: + request['limit'] = limit + request, params = self.handle_until_option('end', request, params) + response = self.publicGetLiquidationsHist(self.extend(request, params)) + # + # [ + # [ + # [ + # "pos", + # 171085137, + # 1706395919788, + # null, + # "tAVAXF0:USTF0", + # -8, + # 32.868, + # null, + # 1, + # 1, + # null, + # 33.255 + # ] + # ], + # ] + # + return self.parse_liquidations(response, market, since, limit) + + def parse_liquidation(self, liquidation, market: Market = None): + # + # [ + # [ + # "pos", + # 171085137, # position id + # 1706395919788, # timestamp + # null, + # "tAVAXF0:USTF0", # market id + # -8, # amount in contracts + # 32.868, # base price + # null, + # 1, + # 1, + # null, + # 33.255 # acquired price + # ] + # ] + # + entry = liquidation[0] + timestamp = self.safe_integer(entry, 2) + marketId = self.safe_string(entry, 4) + contracts = Precise.string_abs(self.safe_string(entry, 5)) + contractSize = self.safe_string(market, 'contractSize') + baseValue = Precise.string_mul(contracts, contractSize) + price = self.safe_string(entry, 11) + sideFlag = self.safe_integer(entry, 8) + side = 'buy' if (sideFlag == 1) else 'sell' + return self.safe_liquidation({ + 'info': entry, + 'symbol': self.safe_symbol(marketId, market, None, 'contract'), + 'contracts': self.parse_number(contracts), + 'contractSize': self.parse_number(contractSize), + 'price': self.parse_number(price), + 'side': side, + 'baseValue': self.parse_number(baseValue), + 'quoteValue': self.parse_number(Precise.string_mul(baseValue, price)), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + + def set_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + either adds or reduces margin in a swap position in order to set the margin to a specific value + + https://docs.bitfinex.com/reference/rest-auth-deriv-pos-collateral-set + + :param str symbol: unified market symbol of the market to set margin in + :param float amount: the amount to set the margin to + :param dict [params]: parameters specific to the exchange API endpoint + :returns dict: A `margin structure ` + """ + self.load_markets() + market = self.market(symbol) + if not market['swap']: + raise NotSupported(self.id + ' setMargin() only support swap markets') + request: dict = { + 'symbol': market['id'], + 'collateral': self.parse_to_numeric(amount), + } + response = self.privatePostAuthWDerivCollateralSet(self.extend(request, params)) + # + # [ + # [ + # 1 + # ] + # ] + # + data = self.safe_value(response, 0) + return self.parse_margin_modification(data, market) + + def parse_margin_modification(self, data, market=None) -> MarginModification: + # + # setMargin + # + # [ + # [ + # 1 + # ] + # ] + # + marginStatusRaw = data[0] + marginStatus = 'ok' if (marginStatusRaw == 1) else 'failed' + return { + 'info': data, + 'symbol': market['symbol'], + 'type': None, + 'marginMode': 'isolated', + 'amount': None, + 'total': None, + 'code': None, + 'status': marginStatus, + 'timestamp': None, + 'datetime': None, + } + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://docs.bitfinex.com/reference/rest-auth-retrieve-orders + https://docs.bitfinex.com/reference/rest-auth-retrieve-orders-by-symbol + + :param str id: the order id + :param str [symbol]: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + request: dict = { + 'id': [self.parse_to_numeric(id)], + } + market = None + response = None + if symbol is None: + response = self.privatePostAuthROrders(self.extend(request, params)) + else: + market = self.market(symbol) + request['symbol'] = market['id'] + response = self.privatePostAuthROrdersSymbol(self.extend(request, params)) + # + # [ + # [ + # 139658969116, + # null, + # 1706843908637, + # "tBTCUST", + # 1706843908637, + # 1706843908638, + # 0.0001, + # 0.0001, + # "EXCHANGE LIMIT", + # null, + # null, + # null, + # 0, + # "ACTIVE", + # null, + # null, + # 35000, + # 0, + # 0, + # 0, + # null, + # null, + # null, + # 0, + # 0, + # null, + # null, + # null, + # "API>BFX", + # null, + # null, + # {} + # ] + # ] + # + order = self.safe_list(response, 0) + newOrder = {'result': order} + return self.parse_order(newOrder, market) + + def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + """ + edit a trade order + + https://docs.bitfinex.com/reference/rest-auth-update-order + + :param str id: edit order id + :param str symbol: unified symbol of the market to edit an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: the price that triggers a trigger order + :param boolean [params.postOnly]: set to True if you want to make a post only order + :param boolean [params.reduceOnly]: indicates that the order is to reduce the size of a position + :param int [params.flags]: additional order parameters: 4096(Post Only), 1024(Reduce Only), 16384(OCO), 64(Hidden), 512(Close), 524288(No Var Rates) + :param int [params.leverage]: leverage for a derivative order, supported by derivative symbol orders only, the value should be between 1 and 100 inclusive + :param int [params.clientOrderId]: a unique client order id for the order + :param float [params.trailingAmount]: *swap only* the quote amount to trail away from the current market price + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'id': self.parse_to_numeric(id), + } + if amount is not None: + amountString = self.amount_to_precision(symbol, amount) + amountString = amountString if (side == 'buy') else Precise.string_neg(amountString) + request['amount'] = amountString + triggerPrice = self.safe_string_2(params, 'stopPrice', 'triggerPrice') + trailingAmount = self.safe_string(params, 'trailingAmount') + timeInForce = self.safe_string(params, 'timeInForce') + postOnlyParam = self.safe_bool(params, 'postOnly', False) + reduceOnly = self.safe_bool(params, 'reduceOnly', False) + clientOrderId = self.safe_integer_2(params, 'cid', 'clientOrderId') + if trailingAmount is not None: + request['price_trailing'] = trailingAmount + elif triggerPrice is not None: + # request['price'] is taken for stop orders + request['price'] = self.price_to_precision(symbol, triggerPrice) + if type == 'limit': + request['price_aux_limit'] = self.price_to_precision(symbol, price) + postOnly = (postOnlyParam or (timeInForce == 'PO')) + if (type != 'market') and (triggerPrice is None): + request['price'] = self.price_to_precision(symbol, price) + # flag values may be summed to combine flags + flags = 0 + if postOnly: + flags = self.sum(flags, 4096) + if reduceOnly: + flags = self.sum(flags, 1024) + if flags != 0: + request['flags'] = flags + if clientOrderId is not None: + request['cid'] = clientOrderId + leverage = self.safe_integer_2(params, 'leverage', 'lev') + if leverage is not None: + request['lev'] = leverage + params = self.omit(params, ['triggerPrice', 'stopPrice', 'timeInForce', 'postOnly', 'reduceOnly', 'trailingAmount', 'clientOrderId', 'leverage']) + response = self.privatePostAuthWOrderUpdate(self.extend(request, params)) + # + # [ + # 1706845376402, + # "ou-req", + # null, + # null, + # [ + # 139658969116, + # null, + # 1706843908637, + # "tBTCUST", + # 1706843908637, + # 1706843908638, + # 0.0002, + # 0.0002, + # "EXCHANGE LIMIT", + # null, + # null, + # null, + # 0, + # "ACTIVE", + # null, + # null, + # 35000, + # 0, + # 0, + # 0, + # null, + # null, + # null, + # 0, + # 0, + # null, + # null, + # null, + # "API>BFX", + # null, + # null, + # {} + # ], + # null, + # "SUCCESS", + # "Submitting update to exchange limit buy order for 0.0002 BTC." + # ] + # + status = self.safe_string(response, 6) + if status != 'SUCCESS': + errorCode = response[5] + errorText = response[7] + raise ExchangeError(self.id + ' ' + response[6] + ': ' + errorText + '(#' + errorCode + ')') + order = self.safe_list(response, 4, []) + newOrder = {'result': order} + return self.parse_order(newOrder, market) diff --git a/ccxt/bitflyer.py b/ccxt/bitflyer.py new file mode 100644 index 0000000..092be3c --- /dev/null +++ b/ccxt/bitflyer.py @@ -0,0 +1,1171 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.bitflyer import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currency, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Position, Str, Strings, Ticker, FundingRate, Trade, TradingFeeInterface, Transaction, MarketInterface +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import OnMaintenance +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class bitflyer(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(bitflyer, self).describe(), { + 'id': 'bitflyer', + 'name': 'bitFlyer', + 'countries': ['JP'], + 'version': 'v1', + 'rateLimit': 1000, # their nonce-timestamp is in seconds... + 'hostname': 'bitflyer.com', # or bitflyer.com + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': None, # has but not fully implemented + 'future': None, # has but not fully implemented + 'option': False, + 'cancelAllOrders': None, # https://lightning.bitflyer.com/docs?lang=en#cancel-all-orders + 'cancelOrder': True, + 'createOrder': True, + 'fetchBalance': True, + 'fetchClosedOrders': 'emulated', + 'fetchCurrencies': False, + 'fetchDeposits': True, + 'fetchFundingRate': True, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchMarginMode': False, + 'fetchMarkets': True, + 'fetchMyTrades': True, + 'fetchOpenOrders': 'emulated', + 'fetchOrder': 'emulated', + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchPositionMode': False, + 'fetchPositions': True, + 'fetchTicker': True, + 'fetchTrades': True, + 'fetchTradingFee': True, + 'fetchTradingFees': False, + 'fetchTransfer': False, + 'fetchTransfers': False, + 'fetchWithdrawals': True, + 'transfer': False, + 'withdraw': True, + }, + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/d0217747-e54d-4533-8416-0d553dca74bb', + 'api': { + 'rest': 'https://api.{hostname}', + }, + 'www': 'https://bitflyer.com', + 'doc': 'https://lightning.bitflyer.com/docs?lang=en', + }, + 'api': { + 'public': { + 'get': [ + 'getmarkets/usa', # new(wip) + 'getmarkets/eu', # new(wip) + 'getmarkets', # or 'markets' + 'getboard', # ... + 'getticker', + 'getexecutions', + 'gethealth', + 'getboardstate', + 'getchats', + 'getfundingrate', + ], + }, + 'private': { + 'get': [ + 'getpermissions', + 'getbalance', + 'getbalancehistory', + 'getcollateral', + 'getcollateralhistory', + 'getcollateralaccounts', + 'getaddresses', + 'getcoinins', + 'getcoinouts', + 'getbankaccounts', + 'getdeposits', + 'getwithdrawals', + 'getchildorders', + 'getparentorders', + 'getparentorder', + 'getexecutions', + 'getpositions', + 'gettradingcommission', + ], + 'post': [ + 'sendcoin', + 'withdraw', + 'sendchildorder', + 'cancelchildorder', + 'sendparentorder', + 'cancelparentorder', + 'cancelallchildorders', + ], + }, + }, + 'fees': { + 'trading': { + 'maker': self.parse_number('0.002'), + 'taker': self.parse_number('0.002'), + }, + }, + 'precisionMode': TICK_SIZE, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': False, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': True, # todo implement + }, + 'hedged': False, + 'trailing': False, # todo recheck + 'leverage': False, + 'marketBuyRequiresPrice': False, + 'marketBuyByCost': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, + 'daysBack': None, + 'untilDays': None, + 'symbolRequired': True, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 100, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': None, + 'untilDays': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': None, + 'daysBackCanceled': None, + 'untilDays': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOHLCV': None, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'exceptions': { + 'exact': { + '-2': OnMaintenance, # {"status":-2,"error_message":"Under maintenance","data":null} + }, + }, + }) + + def parse_expiry_date(self, expiry): + day = expiry[0:2] + monthName = expiry[2:5] + year = expiry[5:9] + months: dict = { + 'JAN': '01', + 'FEB': '02', + 'MAR': '03', + 'APR': '04', + 'MAY': '05', + 'JUN': '06', + 'JUL': '07', + 'AUG': '08', + 'SEP': '09', + 'OCT': '10', + 'NOV': '11', + 'DEC': '12', + } + month = self.safe_string(months, monthName) + return self.parse8601(year + '-' + month + '-' + day + 'T00:00:00Z') + + def safe_market(self, marketId: Str = None, market: Market = None, delimiter: Str = None, marketType: Str = None) -> MarketInterface: + # Bitflyer has a different type of conflict in markets, because + # some of their ids(ETH/BTC and BTC/JPY) are duplicated in US, EU and JP. + # Since they're the same we just need to return one + return super(bitflyer, self).safe_market(marketId, market, delimiter, 'spot') + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for bitflyer + + https://lightning.bitflyer.com/docs?lang=en#market-list + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + jp_markets = self.publicGetGetmarkets(params) + # + # [ + # # spot + # {"product_code": "BTC_JPY", "market_type": "Spot"}, + # {"product_code": "BCH_BTC", "market_type": "Spot"}, + # # forex swap + # {"product_code": "FX_BTC_JPY", "market_type": "FX"}, + # + # # future + # { + # "product_code": "BTCJPY11FEB2022", + # "alias": "BTCJPY_MAT1WK", + # "market_type": "Futures", + # }, + # ] + # + us_markets = self.publicGetGetmarketsUsa(params) + # + # [ + # {"product_code": "BTC_USD", "market_type": "Spot"}, + # {"product_code": "BTC_JPY", "market_type": "Spot"}, + # ] + # + eu_markets = self.publicGetGetmarketsEu(params) + # + # [ + # {"product_code": "BTC_EUR", "market_type": "Spot"}, + # {"product_code": "BTC_JPY", "market_type": "Spot"}, + # ] + # + markets = self.array_concat(jp_markets, us_markets) + markets = self.array_concat(markets, eu_markets) + result = [] + for i in range(0, len(markets)): + market = markets[i] + id = self.safe_string(market, 'product_code') + currencies = id.split('_') + marketType = self.safe_string(market, 'market_type') + swap = (marketType == 'FX') + future = (marketType == 'Futures') + spot = not swap and not future + type = 'spot' + settle = None + baseId = None + quoteId = None + expiry = None + if spot: + baseId = self.safe_string(currencies, 0) + quoteId = self.safe_string(currencies, 1) + elif swap: + type = 'swap' + baseId = self.safe_string(currencies, 1) + quoteId = self.safe_string(currencies, 2) + elif future: + alias = self.safe_string(market, 'alias') + if alias is None: + # no alias: + # {product_code: 'BTCJPY11MAR2022', market_type: 'Futures'} + # TODO self will break if there are products with 4 chars + baseId = id[0:3] + quoteId = id[3:6] + # last 9 chars are expiry date + expiryDate = id[-9:] + expiry = self.parse_expiry_date(expiryDate) + else: + splitAlias = alias.split('_') + currencyIds = self.safe_string(splitAlias, 0) + baseId = currencyIds[0:-3] + quoteId = currencyIds[-3:] + splitId = id.split(currencyIds) + expiryDate = self.safe_string(splitId, 1) + expiry = self.parse_expiry_date(expiryDate) + type = 'future' + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + symbol = base + '/' + quote + taker = self.fees['trading']['taker'] + maker = self.fees['trading']['maker'] + contract = swap or future + if contract: + maker = 0.0 + taker = 0.0 + settle = 'JPY' + symbol = symbol + ':' + settle + if future: + symbol = symbol + '-' + self.yymmdd(expiry) + result.append({ + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': type, + 'spot': spot, + 'margin': False, + 'swap': swap, + 'future': future, + 'option': False, + 'active': True, + 'contract': contract, + 'linear': None if spot else True, + 'inverse': None if spot else False, + 'taker': taker, + 'maker': maker, + 'contractSize': None, + 'expiry': expiry, + 'expiryDatetime': self.iso8601(expiry), + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': None, + 'price': None, + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': None, + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + 'info': market, + }) + return result + + def parse_balance(self, response) -> Balances: + result: dict = {'info': response} + for i in range(0, len(response)): + balance = response[i] + currencyId = self.safe_string(balance, 'currency_code') + code = self.safe_currency_code(currencyId) + account = self.account() + account['total'] = self.safe_string(balance, 'amount') + account['free'] = self.safe_string(balance, 'available') + result[code] = account + return self.safe_balance(result) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://lightning.bitflyer.com/docs?lang=en#get-account-asset-balance + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + response = self.privateGetGetbalance(params) + # + # [ + # { + # "currency_code": "JPY", + # "amount": 1024078, + # "available": 508000 + # }, + # { + # "currency_code": "BTC", + # "amount": 10.24, + # "available": 4.12 + # }, + # { + # "currency_code": "ETH", + # "amount": 20.48, + # "available": 16.38 + # } + # ] + # + return self.parse_balance(response) + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://lightning.bitflyer.com/docs?lang=en#order-book + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'product_code': market['id'], + } + orderbook = self.publicGetGetboard(self.extend(request, params)) + return self.parse_order_book(orderbook, market['symbol'], None, 'bids', 'asks', 'price', 'size') + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + symbol = self.safe_symbol(None, market) + timestamp = self.parse8601(self.safe_string(ticker, 'timestamp')) + last = self.safe_string(ticker, 'ltp') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': None, + 'low': None, + 'bid': self.safe_string(ticker, 'best_bid'), + 'bidVolume': None, + 'ask': self.safe_string(ticker, 'best_ask'), + 'askVolume': None, + 'vwap': None, + 'open': None, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': self.safe_string(ticker, 'volume_by_product'), + 'quoteVolume': None, + 'info': ticker, + }, market) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://lightning.bitflyer.com/docs?lang=en#ticker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'product_code': market['id'], + } + response = self.publicGetGetticker(self.extend(request, params)) + return self.parse_ticker(response, market) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(public) v1 + # + # { + # "id":2278466664, + # "side":"SELL", + # "price":56810.7, + # "size":0.08798, + # "exec_date":"2021-11-19T11:46:39.323", + # "buy_child_order_acceptance_id":"JRF20211119-114209-236525", + # "sell_child_order_acceptance_id":"JRF20211119-114639-236919" + # } + # + # fetchMyTrades + # + # { + # "id": 37233, + # "side": "BUY", + # "price": 33470, + # "size": 0.01, + # "exec_date": "2015-07-07T09:57:40.397", + # "child_order_id": "JOR20150707-060559-021935", + # "child_order_acceptance_id": "JRF20150707-060559-396699" + # "commission": 0, + # }, + # + side = self.safe_string_lower(trade, 'side') + if side is not None: + if len(side) < 1: + side = None + order = None + if side is not None: + idInner = side + '_child_order_acceptance_id' + if idInner in trade: + order = trade[idInner] + if order is None: + order = self.safe_string(trade, 'child_order_acceptance_id') + timestamp = self.parse8601(self.safe_string(trade, 'exec_date')) + priceString = self.safe_string(trade, 'price') + amountString = self.safe_string(trade, 'size') + id = self.safe_string(trade, 'id') + market = self.safe_market(None, market) + return self.safe_trade({ + 'id': id, + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'order': order, + 'type': None, + 'side': side, + 'takerOrMaker': None, + 'price': priceString, + 'amount': amountString, + 'cost': None, + 'fee': None, + }, market) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://lightning.bitflyer.com/docs?lang=en#list-executions + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'product_code': market['id'], + } + if limit is not None: + request['count'] = limit + response = self.publicGetGetexecutions(self.extend(request, params)) + # + # [ + # { + # "id": 39287, + # "side": "BUY", + # "price": 31690, + # "size": 27.04, + # "exec_date": "2015-07-08T02:43:34.823", + # "buy_child_order_acceptance_id": "JRF20150707-200203-452209", + # "sell_child_order_acceptance_id": "JRF20150708-024334-060234" + # }, + # ] + # + return self.parse_trades(response, market, since, limit) + + def fetch_trading_fee(self, symbol: str, params={}) -> TradingFeeInterface: + """ + fetch the trading fees for a market + + https://lightning.bitflyer.com/docs?lang=en#get-trading-commission + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `fee structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'product_code': market['id'], + } + response = self.privateGetGettradingcommission(self.extend(request, params)) + # + # { + # commission_rate: '0.0020' + # } + # + fee = self.safe_number(response, 'commission_rate') + return { + 'info': response, + 'symbol': market['symbol'], + 'maker': fee, + 'taker': fee, + 'percentage': None, + 'tierBased': None, + } + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://lightning.bitflyer.com/docs?lang=en#send-a-new-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + request: dict = { + 'product_code': self.market_id(symbol), + 'child_order_type': type.upper(), + 'side': side.upper(), + 'price': price, + 'size': amount, + } + result = self.privatePostSendchildorder(self.extend(request, params)) + # {"status": - 200, "error_message": "Insufficient funds", "data": null} + id = self.safe_string(result, 'child_order_acceptance_id') + return self.safe_order({ + 'id': id, + 'info': result, + }) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://lightning.bitflyer.com/docs?lang=en#cancel-order + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + self.load_markets() + request: dict = { + 'product_code': self.market_id(symbol), + 'child_order_acceptance_id': id, + } + response = self.privatePostCancelchildorder(self.extend(request, params)) + # + # 200 OK. + # + return self.safe_order({ + 'info': response, + }) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'ACTIVE': 'open', + 'COMPLETED': 'closed', + 'CANCELED': 'canceled', + 'EXPIRED': 'canceled', + 'REJECTED': 'canceled', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + timestamp = self.parse8601(self.safe_string(order, 'child_order_date')) + price = self.safe_string(order, 'price') + amount = self.safe_string(order, 'size') + filled = self.safe_string(order, 'executed_size') + remaining = self.safe_string(order, 'outstanding_size') + status = self.parse_order_status(self.safe_string(order, 'child_order_state')) + type = self.safe_string_lower(order, 'child_order_type') + side = self.safe_string_lower(order, 'side') + marketId = self.safe_string(order, 'product_code') + symbol = self.safe_symbol(marketId, market) + fee = None + feeCost = self.safe_number(order, 'total_commission') + if feeCost is not None: + fee = { + 'cost': feeCost, + 'currency': None, + 'rate': None, + } + id = self.safe_string(order, 'child_order_acceptance_id') + return self.safe_order({ + 'id': id, + 'clientOrderId': None, + 'info': order, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'status': status, + 'symbol': symbol, + 'type': type, + 'timeInForce': None, + 'postOnly': None, + 'side': side, + 'price': price, + 'triggerPrice': None, + 'cost': None, + 'amount': amount, + 'filled': filled, + 'remaining': remaining, + 'fee': fee, + 'average': None, + 'trades': None, + }, market) + + def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = 100, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://lightning.bitflyer.com/docs?lang=en#list-orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrders() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'product_code': market['id'], + 'count': limit, + } + response = self.privateGetGetchildorders(self.extend(request, params)) + orders = self.parse_orders(response, market, since, limit) + if symbol is not None: + orders = self.filter_by(orders, 'symbol', symbol) + return orders + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = 100, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://lightning.bitflyer.com/docs?lang=en#list-orders + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + request: dict = { + 'child_order_state': 'ACTIVE', + } + return self.fetch_orders(symbol, since, limit, self.extend(request, params)) + + def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = 100, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://lightning.bitflyer.com/docs?lang=en#list-orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + request: dict = { + 'child_order_state': 'COMPLETED', + } + return self.fetch_orders(symbol, since, limit, self.extend(request, params)) + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://lightning.bitflyer.com/docs?lang=en#list-orders + + :param str id: the order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrder() requires a symbol argument') + orders = self.fetch_orders(symbol) + ordersById = self.index_by(orders, 'id') + if id in ordersById: + return ordersById[id] + raise OrderNotFound(self.id + ' No order found with id ' + id) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://lightning.bitflyer.com/docs?lang=en#list-executions + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMyTrades() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'product_code': market['id'], + } + if limit is not None: + request['count'] = limit + response = self.privateGetGetexecutions(self.extend(request, params)) + # + # [ + # { + # "id": 37233, + # "side": "BUY", + # "price": 33470, + # "size": 0.01, + # "exec_date": "2015-07-07T09:57:40.397", + # "child_order_id": "JOR20150707-060559-021935", + # "child_order_acceptance_id": "JRF20150707-060559-396699" + # "commission": 0, + # }, + # ] + # + return self.parse_trades(response, market, since, limit) + + def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://lightning.bitflyer.com/docs?lang=en#get-open-interest-summary + + :param str[] symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + if symbols is None: + raise ArgumentsRequired(self.id + ' fetchPositions() requires a `symbols` argument, exactly one symbol in an array') + self.load_markets() + request: dict = { + 'product_code': self.market_ids(symbols), + } + response = self.privateGetGetpositions(self.extend(request, params)) + # + # [ + # { + # "product_code": "FX_BTC_JPY", + # "side": "BUY", + # "price": 36000, + # "size": 10, + # "commission": 0, + # "swap_point_accumulate": -35, + # "require_collateral": 120000, + # "open_date": "2015-11-03T10:04:45.011", + # "leverage": 3, + # "pnl": 965, + # "sfd": -0.5 + # } + # ] + # + # todo unify parsePosition/parsePositions + return response + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://lightning.bitflyer.com/docs?lang=en#withdrawing-funds + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + self.check_address(address) + self.load_markets() + if code != 'JPY' and code != 'USD' and code != 'EUR': + raise ExchangeError(self.id + ' allows withdrawing JPY, USD, EUR only, ' + code + ' is not supported') + currency = self.currency(code) + request: dict = { + 'currency_code': currency['id'], + 'amount': amount, + # 'bank_account_id': 1234, + } + response = self.privatePostWithdraw(self.extend(request, params)) + # + # { + # "message_id": "69476620-5056-4003-bcbe-42658a2b041b" + # } + # + return self.parse_transaction(response, currency) + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://lightning.bitflyer.com/docs?lang=en#get-crypto-assets-deposit-history + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + currency = None + request: dict = {} + if code is not None: + currency = self.currency(code) + if limit is not None: + request['count'] = limit # default 100 + response = self.privateGetGetcoinins(self.extend(request, params)) + # + # [ + # { + # "id": 100, + # "order_id": "CDP20151227-024141-055555", + # "currency_code": "BTC", + # "amount": 0.00002, + # "address": "1WriteySQufKZ2pVuM1oMhPrTtTVFq35j", + # "tx_hash": "9f92ee65a176bb9545f7becb8706c50d07d4cee5ffca34d8be3ef11d411405ae", + # "status": "COMPLETED", + # "event_date": "2015-11-27T08:59:20.301" + # } + # ] + # + return self.parse_transactions(response, currency, since, limit) + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://lightning.bitflyer.com/docs?lang=en#get-crypto-assets-transaction-history + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + currency = None + request: dict = {} + if code is not None: + currency = self.currency(code) + if limit is not None: + request['count'] = limit # default 100 + response = self.privateGetGetcoinouts(self.extend(request, params)) + # + # [ + # { + # "id": 500, + # "order_id": "CWD20151224-014040-077777", + # "currency_code": "BTC", + # "amount": 0.1234, + # "address": "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", + # "tx_hash": "724c07dfd4044abcb390b0412c3e707dd5c4f373f0a52b3bd295ce32b478c60a", + # "fee": 0.0005, + # "additional_fee": 0.0001, + # "status": "COMPLETED", + # "event_date": "2015-12-24T01:40:40.397" + # } + # ] + # + return self.parse_transactions(response, currency, since, limit) + + def parse_deposit_status(self, status): + statuses: dict = { + 'PENDING': 'pending', + 'COMPLETED': 'ok', + } + return self.safe_string(statuses, status, status) + + def parse_withdrawal_status(self, status): + statuses: dict = { + 'PENDING': 'pending', + 'COMPLETED': 'ok', + } + return self.safe_string(statuses, status, status) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fetchDeposits + # + # { + # "id": 100, + # "order_id": "CDP20151227-024141-055555", + # "currency_code": "BTC", + # "amount": 0.00002, + # "address": "1WriteySQufKZ2pVuM1oMhPrTtTVFq35j", + # "tx_hash": "9f92ee65a176bb9545f7becb8706c50d07d4cee5ffca34d8be3ef11d411405ae", + # "status": "COMPLETED", + # "event_date": "2015-11-27T08:59:20.301" + # } + # + # fetchWithdrawals + # + # { + # "id": 500, + # "order_id": "CWD20151224-014040-077777", + # "currency_code": "BTC", + # "amount": 0.1234, + # "address": "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", + # "tx_hash": "724c07dfd4044abcb390b0412c3e707dd5c4f373f0a52b3bd295ce32b478c60a", + # "fee": 0.0005, + # "additional_fee": 0.0001, + # "status": "COMPLETED", + # "event_date": "2015-12-24T01:40:40.397" + # } + # + # withdraw + # + # { + # "message_id": "69476620-5056-4003-bcbe-42658a2b041b" + # } + # + id = self.safe_string_2(transaction, 'id', 'message_id') + address = self.safe_string(transaction, 'address') + currencyId = self.safe_string(transaction, 'currency_code') + code = self.safe_currency_code(currencyId, currency) + timestamp = self.parse8601(self.safe_string(transaction, 'event_date')) + amount = self.safe_number(transaction, 'amount') + txId = self.safe_string(transaction, 'tx_hash') + rawStatus = self.safe_string(transaction, 'status') + type = None + status = None + fee = None + if 'fee' in transaction: + type = 'withdrawal' + status = self.parse_withdrawal_status(rawStatus) + feeCost = self.safe_string(transaction, 'fee') + additionalFee = self.safe_string(transaction, 'additional_fee') + fee = {'currency': code, 'cost': self.parse_number(Precise.string_add(feeCost, additionalFee))} + else: + type = 'deposit' + status = self.parse_deposit_status(rawStatus) + return { + 'info': transaction, + 'id': id, + 'txid': txId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': None, + 'address': address, + 'addressTo': address, + 'addressFrom': None, + 'tag': None, + 'tagTo': None, + 'tagFrom': None, + 'type': type, + 'amount': amount, + 'currency': code, + 'status': status, + 'updated': None, + 'comment': None, + 'internal': None, + 'fee': fee, + } + + def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate + + https://lightning.bitflyer.com/docs#funding-rate + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'product_code': market['id'], + } + response = self.publicGetGetfundingrate(self.extend(request, params)) + # + # { + # "current_funding_rate": -0.003750000000 + # "next_funding_rate_settledate": "2024-04-15T13:00:00" + # } + # + return self.parse_funding_rate(response, market) + + def parse_funding_rate(self, contract, market: Market = None) -> FundingRate: + # + # { + # "current_funding_rate": -0.003750000000 + # "next_funding_rate_settledate": "2024-04-15T13:00:00" + # } + # + nextFundingDatetime = self.safe_string(contract, 'next_funding_rate_settledate') + nextFundingTimestamp = self.parse8601(nextFundingDatetime) + return { + 'info': contract, + 'symbol': self.safe_string(market, 'symbol'), + 'markPrice': None, + 'indexPrice': None, + 'interestRate': None, + 'estimatedSettlePrice': None, + 'timestamp': None, + 'datetime': None, + 'fundingRate': None, + 'fundingTimestamp': None, + 'fundingDatetime': None, + 'nextFundingRate': self.safe_number(contract, 'current_funding_rate'), + 'nextFundingTimestamp': nextFundingTimestamp, + 'nextFundingDatetime': self.iso8601(nextFundingTimestamp), + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': None, + } + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + request = '/' + self.version + '/' + if api == 'private': + request += 'me/' + request += path + if method == 'GET': + if params: + request += '?' + self.urlencode(params) + baseUrl = self.implode_hostname(self.urls['api']['rest']) + url = baseUrl + request + if api == 'private': + self.check_required_credentials() + nonce = str(self.nonce()) + auth = ''.join([nonce, method, request]) + if params: + if method != 'GET': + body = self.json(params) + auth += body + headers = { + 'ACCESS-KEY': self.apiKey, + 'ACCESS-TIMESTAMP': nonce, + 'ACCESS-SIGN': self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256), + 'Content-Type': 'application/json', + } + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None # fallback to the default error handler + feedback = self.id + ' ' + body + # i.e. {"status":-2,"error_message":"Under maintenance","data":null} + errorMessage = self.safe_string(response, 'error_message') + statusCode = self.safe_integer(response, 'status') + if errorMessage is not None: + self.throw_exactly_matched_exception(self.exceptions['exact'], statusCode, feedback) + raise ExchangeError(feedback) + return None diff --git a/ccxt/bitget.py b/ccxt/bitget.py new file mode 100644 index 0000000..5e5bbd7 --- /dev/null +++ b/ccxt/bitget.py @@ -0,0 +1,10517 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.bitget import ImplicitAPI +import hashlib +import json +from ccxt.base.types import Any, Balances, BorrowInterest, Conversion, CrossBorrowRate, Currencies, Currency, DepositAddress, FundingHistory, Int, IsolatedBorrowRate, LedgerEntry, Leverage, LeverageTier, Liquidation, LongShortRatio, MarginMode, MarginModification, Market, Num, Order, OrderBook, OrderRequest, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, FundingRates, Trade, TradingFeeInterface, TradingFees, Transaction, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import AccountSuspended +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidAddress +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import NotSupported +from ccxt.base.errors import DDoSProtection +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.errors import OnMaintenance +from ccxt.base.errors import InvalidNonce +from ccxt.base.errors import RequestTimeout +from ccxt.base.errors import CancelPending +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class bitget(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(bitget, self).describe(), { + 'id': 'bitget', + 'name': 'Bitget', + 'countries': ['SG'], + 'version': 'v2', + 'rateLimit': 50, # up to 3000 requests per 5 minutes ≈ 600 requests per minute ≈ 10 requests per second ≈ 100 ms + 'certified': True, + 'pro': True, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': True, + 'swap': True, + 'future': True, + 'option': False, + 'addMargin': True, + 'borrowCrossMargin': True, + 'borrowIsolatedMargin': True, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'cancelOrders': True, + 'closeAllPositions': True, + 'closePosition': True, + 'createConvertTrade': True, + 'createDepositAddress': False, + 'createMarketBuyOrderWithCost': True, + 'createMarketOrderWithCost': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createOrders': True, + 'createOrderWithTakeProfitAndStopLoss': True, + 'createPostOnlyOrder': True, + 'createReduceOnlyOrder': False, + 'createStopLimitOrder': True, + 'createStopLossOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'createTakeProfitOrder': True, + 'createTrailingAmountOrder': False, + 'createTrailingPercentOrder': True, + 'createTriggerOrder': True, + 'editOrder': True, + 'fetchAccounts': False, + 'fetchBalance': True, + 'fetchBorrowInterest': True, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchCanceledAndClosedOrders': True, + 'fetchCanceledOrders': True, + 'fetchClosedOrders': True, + 'fetchConvertCurrencies': True, + 'fetchConvertQuote': True, + 'fetchConvertTrade': False, + 'fetchConvertTradeHistory': True, + 'fetchCrossBorrowRate': True, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDeposit': False, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchDepositsWithdrawals': False, + 'fetchDepositWithdrawFee': 'emulated', + 'fetchDepositWithdrawFees': True, + 'fetchFundingHistory': True, + 'fetchFundingInterval': True, + 'fetchFundingIntervals': True, + 'fetchFundingRate': True, + 'fetchFundingRateHistory': True, + 'fetchFundingRates': True, + 'fetchIndexOHLCV': True, + 'fetchIsolatedBorrowRate': True, + 'fetchIsolatedBorrowRates': False, + 'fetchLedger': True, + 'fetchLeverage': True, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': True, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': True, + 'fetchMarketLeverageTiers': True, + 'fetchMarkets': True, + 'fetchMarkOHLCV': True, + 'fetchMarkPrice': True, + 'fetchMyLiquidations': True, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': True, + 'fetchOpenInterestHistory': False, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrderBooks': False, + 'fetchOrders': False, + 'fetchOrderTrades': False, + 'fetchPosition': True, + 'fetchPositionHistory': 'emulated', + 'fetchPositionMode': False, + 'fetchPositions': True, + 'fetchPositionsHistory': True, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchStatus': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': True, + 'fetchTradingFees': True, + 'fetchTransactions': False, + 'fetchTransfer': False, + 'fetchTransfers': True, + 'fetchWithdrawAddresses': False, + 'fetchWithdrawal': False, + 'fetchWithdrawals': True, + 'reduceMargin': True, + 'repayCrossMargin': True, + 'repayIsolatedMargin': True, + 'setLeverage': True, + 'setMargin': False, + 'setMarginMode': True, + 'setPositionMode': True, + 'signIn': False, + 'transfer': True, + 'withdraw': True, + }, + 'timeframes': { + '1m': '1m', + '3m': '3m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1h', + '2h': '2h', + '4h': '4h', + '6h': '6h', + '12h': '12h', + '1d': '1d', + '3d': '3d', + '1w': '1w', + '1M': '1m', + }, + 'hostname': 'bitget.com', + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/fbaa10cc-a277-441d-a5b7-997dd9a87658', + 'api': { + 'spot': 'https://api.{hostname}', + 'mix': 'https://api.{hostname}', + 'user': 'https://api.{hostname}', + 'p2p': 'https://api.{hostname}', + 'broker': 'https://api.{hostname}', + 'margin': 'https://api.{hostname}', + 'common': 'https://api.{hostname}', + 'tax': 'https://api.{hostname}', + 'convert': 'https://api.{hostname}', + 'copy': 'https://api.{hostname}', + 'earn': 'https://api.{hostname}', + 'uta': 'https://api.{hostname}', + }, + 'www': 'https://www.bitget.com', + 'doc': [ + 'https://www.bitget.com/api-doc/common/intro', + 'https://www.bitget.com/api-doc/spot/intro', + 'https://www.bitget.com/api-doc/contract/intro', + 'https://www.bitget.com/api-doc/broker/intro', + 'https://www.bitget.com/api-doc/margin/intro', + 'https://www.bitget.com/api-doc/copytrading/intro', + 'https://www.bitget.com/api-doc/earn/intro', + 'https://bitgetlimited.github.io/apidoc/en/mix', + 'https://bitgetlimited.github.io/apidoc/en/spot', + 'https://bitgetlimited.github.io/apidoc/en/broker', + 'https://bitgetlimited.github.io/apidoc/en/margin', + ], + 'fees': 'https://www.bitget.cc/zh-CN/rate?tab=1', + 'referral': 'https://www.bitget.com/expressly?languageType=0&channelCode=ccxt&vipCode=tg9j', + }, + 'api': { + 'public': { + 'common': { + 'get': { + 'v2/public/annoucements': 1, + 'v2/public/time': 1, + }, + }, + 'spot': { + 'get': { + 'spot/v1/notice/queryAllNotices': 1, # 20 times/1s(IP) => 20/20 = 1 + 'spot/v1/public/time': 1, + 'spot/v1/public/currencies': 6.6667, # 3 times/1s(IP) => 20/3 = 6.6667 + 'spot/v1/public/products': 1, + 'spot/v1/public/product': 1, + 'spot/v1/market/ticker': 1, + 'spot/v1/market/tickers': 1, + 'spot/v1/market/fills': 2, # 10 times/1s(IP) => 20/10 = 2 + 'spot/v1/market/fills-history': 2, + 'spot/v1/market/candles': 1, + 'spot/v1/market/depth': 1, + 'spot/v1/market/spot-vip-level': 2, + 'spot/v1/market/merge-depth': 1, + 'spot/v1/market/history-candles': 1, + 'spot/v1/public/loan/coinInfos': 2, # 10 times/1s(IP) => 20/10 = 2 + 'spot/v1/public/loan/hour-interest': 2, # 10 times/1s(IP) => 20/10 = 2 + 'v2/spot/public/coins': 6.6667, + 'v2/spot/public/symbols': 1, + 'v2/spot/market/vip-fee-rate': 2, + 'v2/spot/market/tickers': 1, + 'v2/spot/market/merge-depth': 1, + 'v2/spot/market/orderbook': 1, + 'v2/spot/market/candles': 1, + 'v2/spot/market/history-candles': 1, + 'v2/spot/market/fills': 2, + 'v2/spot/market/fills-history': 2, + }, + }, + 'mix': { + 'get': { + 'mix/v1/market/contracts': 1, + 'mix/v1/market/depth': 1, + 'mix/v1/market/ticker': 1, + 'mix/v1/market/tickers': 1, + 'mix/v1/market/contract-vip-level': 2, + 'mix/v1/market/fills': 1, + 'mix/v1/market/fills-history': 2, + 'mix/v1/market/candles': 1, + 'mix/v1/market/index': 1, + 'mix/v1/market/funding-time': 1, + 'mix/v1/market/history-fundRate': 1, + 'mix/v1/market/current-fundRate': 1, + 'mix/v1/market/open-interest': 1, + 'mix/v1/market/mark-price': 1, + 'mix/v1/market/symbol-leverage': 1, + 'mix/v1/market/queryPositionLever': 1, + 'mix/v1/market/open-limit': 1, + 'mix/v1/market/history-candles': 1, + 'mix/v1/market/history-index-candles': 1, + 'mix/v1/market/history-mark-candles': 1, + 'mix/v1/market/merge-depth': 1, + 'v2/mix/market/vip-fee-rate': 2, + 'v2/mix/market/merge-depth': 1, + 'v2/mix/market/ticker': 1, + 'v2/mix/market/tickers': 1, + 'v2/mix/market/fills': 1, + 'v2/mix/market/fills-history': 2, + 'v2/mix/market/candles': 1, + 'v2/mix/market/history-candles': 1, + 'v2/mix/market/history-index-candles': 1, + 'v2/mix/market/history-mark-candles': 1, + 'v2/mix/market/open-interest': 1, + 'v2/mix/market/funding-time': 1, + 'v2/mix/market/symbol-price': 1, + 'v2/mix/market/history-fund-rate': 1, + 'v2/mix/market/current-fund-rate': 1, + 'v2/mix/market/contracts': 1, + 'v2/mix/market/query-position-lever': 2, + 'v2/mix/market/account-long-short': 20, + }, + }, + 'margin': { + 'get': { + 'margin/v1/cross/public/interestRateAndLimit': 2, # 10 times/1s(IP) => 20/10 = 2 + 'margin/v1/isolated/public/interestRateAndLimit': 2, # 10 times/1s(IP) => 20/10 = 2 + 'margin/v1/cross/public/tierData': 2, # 10 times/1s(IP) => 20/10 = 2 + 'margin/v1/isolated/public/tierData': 2, # 10 times/1s(IP) => 20/10 = 2 + 'margin/v1/public/currencies': 1, # 20 times/1s(IP) => 20/20 = 1 + 'v2/margin/currencies': 2, + 'v2/margin/market/long-short-ratio': 20, + }, + }, + 'earn': { + 'get': { + 'v2/earn/loan/public/coinInfos': 2, + 'v2/earn/loan/public/hour-interest': 2, + }, + }, + 'uta': { + 'get': { + 'v3/market/instruments': 1, + 'v3/market/tickers': 1, + 'v3/market/orderbook': 1, + 'v3/market/fills': 1, + 'v3/market/open-interest': 1, + 'v3/market/candles': 1, + 'v3/market/history-candles': 1, + 'v3/market/current-fund-rate': 1, + 'v3/market/history-fund-rate': 1, + 'v3/market/risk-reserve': 1, + 'v3/market/discount-rate': 1, + 'v3/market/margin-loans': 1, + 'v3/market/position-tier': 1, + 'v3/market/oi-limit': 2, + }, + }, + }, + 'private': { + 'spot': { + 'get': { + 'spot/v1/wallet/deposit-address': 4, + 'spot/v1/wallet/withdrawal-list': 1, + 'spot/v1/wallet/deposit-list': 1, + 'spot/v1/account/getInfo': 20, + 'spot/v1/account/assets': 2, + 'spot/v1/account/assets-lite': 2, # 10 times/1s(UID) => 20/10 = 2 + 'spot/v1/account/transferRecords': 1, # 20 times/1s(UID) => 20/20 = 1 + 'spot/v1/convert/currencies': 2, + 'spot/v1/convert/convert-record': 2, + 'spot/v1/loan/ongoing-orders': 2, # 10 times/1s(UID) => 20/10 = 2 + 'spot/v1/loan/repay-history': 2, # 10 times/1s(UID) => 20/10 = 2 + 'spot/v1/loan/revise-history': 2, # 10 times/1s(UID) => 20/10 = 2 + 'spot/v1/loan/borrow-history': 2, # 10 times/1s(UID) => 20/10 = 2 + 'spot/v1/loan/debts': 2, # 10 times/1s(UID) => 20/10 = 2 + 'v2/spot/trade/orderInfo': 1, + 'v2/spot/trade/unfilled-orders': 1, + 'v2/spot/trade/history-orders': 1, + 'v2/spot/trade/fills': 2, + 'v2/spot/trade/current-plan-order': 1, + 'v2/spot/trade/history-plan-order': 1, + 'v2/spot/account/info': 20, + 'v2/spot/account/assets': 2, + 'v2/spot/account/subaccount-assets': 2, + 'v2/spot/account/bills': 2, + 'v2/spot/account/transferRecords': 1, + 'v2/account/funding-assets': 2, + 'v2/account/bot-assets': 2, + 'v2/account/all-account-balance': 20, + 'v2/spot/wallet/deposit-address': 2, + 'v2/spot/wallet/deposit-records': 2, + 'v2/spot/wallet/withdrawal-records': 2, + }, + 'post': { + 'spot/v1/wallet/transfer': 4, + 'spot/v1/wallet/transfer-v2': 4, + 'spot/v1/wallet/subTransfer': 10, + 'spot/v1/wallet/withdrawal': 4, + 'spot/v1/wallet/withdrawal-v2': 4, + 'spot/v1/wallet/withdrawal-inner': 4, + 'spot/v1/wallet/withdrawal-inner-v2': 4, + 'spot/v1/account/sub-account-spot-assets': 200, + 'spot/v1/account/bills': 2, + 'spot/v1/trade/orders': 2, + 'spot/v1/trade/batch-orders': 4, + 'spot/v1/trade/cancel-order': 2, + 'spot/v1/trade/cancel-order-v2': 2, + 'spot/v1/trade/cancel-symbol-order': 2, + 'spot/v1/trade/cancel-batch-orders': 4, + 'spot/v1/trade/cancel-batch-orders-v2': 4, + 'spot/v1/trade/orderInfo': 1, + 'spot/v1/trade/open-orders': 1, + 'spot/v1/trade/history': 1, + 'spot/v1/trade/fills': 1, + 'spot/v1/plan/placePlan': 1, + 'spot/v1/plan/modifyPlan': 1, + 'spot/v1/plan/cancelPlan': 1, + 'spot/v1/plan/currentPlan': 1, + 'spot/v1/plan/historyPlan': 1, + 'spot/v1/plan/batchCancelPlan': 2, # 10 times/1s(UID) => 20/10 = 2 + 'spot/v1/convert/quoted-price': 4, + 'spot/v1/convert/trade': 4, + 'spot/v1/loan/borrow': 2, # 10 times/1s(UID) => 20/10 = 2 + 'spot/v1/loan/repay': 2, # 10 times/1s(UID) => 20/10 = 2 + 'spot/v1/loan/revise-pledge': 2, # 10 times/1s(UID) => 20/10 = 2 + 'spot/v1/trace/order/orderCurrentList': 2, # 10 times/1s(UID) => 20/10 = 2 + 'spot/v1/trace/order/orderHistoryList': 2, # 10 times/1s(UID) => 20/10 = 2 + 'spot/v1/trace/order/closeTrackingOrder': 2, # 10 times/1s(UID) => 20/10 = 2 + 'spot/v1/trace/order/updateTpsl': 2, # 10 times/1s(UID) => 20/10 = 2 + 'spot/v1/trace/order/followerEndOrder': 2, # 10 times/1s(UID) => 20/10 = 2 + 'spot/v1/trace/order/spotInfoList': 2, # 10 times/1s(UID) => 20/10 = 2 + 'spot/v1/trace/config/getTraderSettings': 2, # 10 times/1s(UID) => 20/10 = 2 + 'spot/v1/trace/config/getFollowerSettings': 2, # 10 times/1s(UID) => 20/10 = 2 + 'spot/v1/trace/user/myTraders': 2, # 10 times/1s(UID) => 20/10 = 2 + 'spot/v1/trace/config/setFollowerConfig': 2, # 10 times/1s(UID) => 20/10 = 2 + 'spot/v1/trace/user/myFollowers': 2, # 10 times/1s(UID) => 20/10 = 2 + 'spot/v1/trace/config/setProductCode': 2, # 10 times/1s(UID) => 20/10 = 2 + 'spot/v1/trace/user/removeTrader': 2, # 10 times/1s(UID) => 20/10 = 2 + 'spot/v1/trace/getRemovableFollower': 2, + 'spot/v1/trace/user/removeFollower': 2, + 'spot/v1/trace/profit/totalProfitInfo': 2, # 10 times/1s(UID) => 20/10 = 2 + 'spot/v1/trace/profit/totalProfitList': 2, # 10 times/1s(UID) => 20/10 = 2 + 'spot/v1/trace/profit/profitHisList': 2, # 10 times/1s(UID) => 20/10 = 2 + 'spot/v1/trace/profit/profitHisDetailList': 2, # 10 times/1s(UID) => 20/10 = 2 + 'spot/v1/trace/profit/waitProfitDetailList': 2, # 10 times/1s(UID) => 20/10 = 2 + 'spot/v1/trace/user/getTraderInfo': 2, # 10 times/1s(UID) => 20/10 = 2 + 'v2/spot/trade/place-order': 2, + 'v2/spot/trade/cancel-order': 2, + 'v2/spot/trade/batch-orders': 20, + 'v2/spot/trade/batch-cancel-order': 2, + 'v2/spot/trade/cancel-symbol-order': 4, + 'v2/spot/trade/place-plan-order': 1, + 'v2/spot/trade/modify-plan-order': 1, + 'v2/spot/trade/cancel-plan-order': 1, + 'v2/spot/trade/cancel-replace-order': 2, + 'v2/spot/trade/batch-cancel-plan-order': 2, + 'v2/spot/wallet/transfer': 2, + 'v2/spot/wallet/subaccount-transfer': 2, + 'v2/spot/wallet/withdrawal': 2, + 'v2/spot/wallet/cancel-withdrawal': 2, + 'v2/spot/wallet/modify-deposit-account': 2, + }, + }, + 'mix': { + 'get': { + 'mix/v1/account/account': 2, + 'mix/v1/account/accounts': 2, + 'mix/v1/position/singlePosition': 2, + 'mix/v1/position/singlePosition-v2': 2, + 'mix/v1/position/allPosition': 4, # 5 times/1s(UID) => 20/5 = 4 + 'mix/v1/position/allPosition-v2': 4, # 5 times/1s(UID) => 20/5 = 4 + 'mix/v1/position/history-position': 1, + 'mix/v1/account/accountBill': 2, + 'mix/v1/account/accountBusinessBill': 4, + 'mix/v1/order/current': 1, # 20 times/1s(UID) => 20/20 = 1 + 'mix/v1/order/marginCoinCurrent': 1, # 20 times/1s(UID) => 20/20 = 1 + 'mix/v1/order/history': 2, + 'mix/v1/order/historyProductType': 4, # 5 times/1s(UID) => 20/5 = 4 + 'mix/v1/order/detail': 2, + 'mix/v1/order/fills': 2, + 'mix/v1/order/allFills': 2, + 'mix/v1/plan/currentPlan': 1, # 20 times/1s(UID) => 20/20 = 1 + 'mix/v1/plan/historyPlan': 2, + 'mix/v1/trace/currentTrack': 2, + 'mix/v1/trace/followerOrder': 2, + 'mix/v1/trace/followerHistoryOrders': 2, + 'mix/v1/trace/historyTrack': 2, + 'mix/v1/trace/summary': 1, # 20 times/1s(UID) => 20/20 = 1 + 'mix/v1/trace/profitSettleTokenIdGroup': 1, # 20 times/1s(UID) => 20/20 = 1 + 'mix/v1/trace/profitDateGroupList': 1, # 20 times/1s(UID) => 20/20 = 1 + 'mix/v1/trade/profitDateList': 2, + 'mix/v1/trace/waitProfitDateList': 1, # 20 times/1s(UID) => 20/20 = 1 + 'mix/v1/trace/traderSymbols': 1, # 20 times/1s(UID) => 20/20 = 1 + 'mix/v1/trace/traderList': 2, + 'mix/v1/trace/traderDetail': 2, # 10 times/1s(UID) => 20/10 = 2 + 'mix/v1/trace/queryTraceConfig': 2, + 'v2/mix/account/account': 2, + 'v2/mix/account/accounts': 2, + 'v2/mix/account/sub-account-assets': 200, + 'v2/mix/account/open-count': 2, + 'v2/mix/account/bill': 2, + 'v2/mix/market/query-position-lever': 2, + 'v2/mix/position/single-position': 2, + 'v2/mix/position/all-position': 4, + 'v2/mix/position/history-position': 1, + 'v2/mix/order/detail': 2, + 'v2/mix/order/fills': 2, + 'v2/mix/order/fill-history': 2, + 'v2/mix/order/orders-pending': 2, + 'v2/mix/order/orders-history': 2, + 'v2/mix/order/orders-plan-pending': 2, + 'v2/mix/order/orders-plan-history': 2, + 'v2/mix/market/position-long-short': 20, + }, + 'post': { + 'mix/v1/account/sub-account-contract-assets': 200, # 0.1 times/1s(UID) => 20/0.1 = 200 + 'mix/v1/account/open-count': 1, + 'mix/v1/account/setLeverage': 4, # 5 times/1s(UID) => 20/5 = 4 + 'mix/v1/account/setMargin': 4, # 5 times/1s(UID) => 20/5 = 4 + 'mix/v1/account/setMarginMode': 4, # 5 times/1s(UID) => 20/5 = 4 + 'mix/v1/account/setPositionMode': 4, # 5 times/1s(UID) => 20/5 = 4 + 'mix/v1/order/placeOrder': 2, + 'mix/v1/order/batch-orders': 2, + 'mix/v1/order/cancel-order': 2, + 'mix/v1/order/cancel-batch-orders': 2, + 'mix/v1/order/modifyOrder': 2, # 10 times/1s(UID) => 20/10 = 2 + 'mix/v1/order/cancel-symbol-orders': 2, + 'mix/v1/order/cancel-all-orders': 2, + 'mix/v1/order/close-all-positions': 20, + 'mix/v1/plan/placePlan': 2, + 'mix/v1/plan/modifyPlan': 2, + 'mix/v1/plan/modifyPlanPreset': 2, + 'mix/v1/plan/placeTPSL': 2, + 'mix/v1/plan/placeTrailStop': 2, + 'mix/v1/plan/placePositionsTPSL': 2, + 'mix/v1/plan/modifyTPSLPlan': 2, + 'mix/v1/plan/cancelPlan': 2, + 'mix/v1/plan/cancelSymbolPlan': 2, + 'mix/v1/plan/cancelAllPlan': 2, + 'mix/v1/trace/closeTrackOrder': 2, + 'mix/v1/trace/modifyTPSL': 2, # 10 times/1s(UID) => 20/10 = 2 + 'mix/v1/trace/closeTrackOrderBySymbol': 2, + 'mix/v1/trace/setUpCopySymbols': 2, + 'mix/v1/trace/followerSetBatchTraceConfig': 2, + 'mix/v1/trace/followerCloseByTrackingNo': 2, + 'mix/v1/trace/followerCloseByAll': 2, + 'mix/v1/trace/followerSetTpsl': 2, + 'mix/v1/trace/cancelCopyTrader': 4, # 5 times/1s(UID) => 20/5 = 4 + 'mix/v1/trace/traderUpdateConfig': 2, # 10 times/1s(UID) => 20/10 = 2 + 'mix/v1/trace/myTraderList': 2, # 10 times/1s(UID) => 20/10 = 2 + 'mix/v1/trace/myFollowerList': 2, # 10 times/1s(UID) => 20/10 = 2 + 'mix/v1/trace/removeFollower': 2, # 10 times/1s(UID) => 20/10 = 2 + 'mix/v1/trace/public/getFollowerConfig': 2, # 10 times/1s(UID) => 20/10 = 2 + 'mix/v1/trace/report/order/historyList': 2, # 10 times/1s(IP) => 20/10 = 2 + 'mix/v1/trace/report/order/currentList': 2, # 10 times/1s(IP) => 20/10 = 2 + 'mix/v1/trace/queryTraderTpslRatioConfig': 2, # 10 times/1s(UID) => 20/10 = 2 + 'mix/v1/trace/traderUpdateTpslRatioConfig': 2, # 10 times/1s(UID) => 20/10 = 2 + 'v2/mix/account/set-leverage': 4, + 'v2/mix/account/set-margin': 4, + 'v2/mix/account/set-margin-mode': 4, + 'v2/mix/account/set-position-mode': 4, + 'v2/mix/order/place-order': 2, + 'v2/mix/order/click-backhand': 20, + 'v2/mix/order/batch-place-order': 20, + 'v2/mix/order/modify-order': 2, + 'v2/mix/order/cancel-order': 2, + 'v2/mix/order/batch-cancel-orders': 2, + 'v2/mix/order/close-positions': 20, + 'v2/mix/order/place-tpsl-order': 2, + 'v2/mix/order/place-plan-order': 2, + 'v2/mix/order/modify-tpsl-order': 2, + 'v2/mix/order/modify-plan-order': 2, + 'v2/mix/order/cancel-plan-order': 2, + }, + }, + 'user': { + 'get': { + 'user/v1/fee/query': 2, + 'user/v1/sub/virtual-list': 2, + 'user/v1/sub/virtual-api-list': 2, + 'user/v1/tax/spot-record': 1, + 'user/v1/tax/future-record': 1, + 'user/v1/tax/margin-record': 1, + 'user/v1/tax/p2p-record': 1, + 'v2/user/virtual-subaccount-list': 2, + 'v2/user/virtual-subaccount-apikey-list': 2, + }, + 'post': { + 'user/v1/sub/virtual-create': 4, + 'user/v1/sub/virtual-modify': 4, + 'user/v1/sub/virtual-api-batch-create': 20, # 1 times/1s(UID) => 20/1 = 20 + 'user/v1/sub/virtual-api-create': 4, + 'user/v1/sub/virtual-api-modify': 4, + 'v2/user/create-virtual-subaccount': 4, + 'v2/user/modify-virtual-subaccount': 4, + 'v2/user/batch-create-subaccount-and-apikey': 20, + 'v2/user/create-virtual-subaccount-apikey': 4, + 'v2/user/modify-virtual-subaccount-apikey': 4, + }, + }, + 'p2p': { + 'get': { + 'p2p/v1/merchant/merchantList': 2, # 10 times/1s(UID) => 20/10 = 2 + 'p2p/v1/merchant/merchantInfo': 2, # 10 times/1s(UID) => 20/10 = 2 + 'p2p/v1/merchant/advList': 2, # 10 times/1s(UID) => 20/10 = 2 + 'p2p/v1/merchant/orderList': 2, # 10 times/1s(UID) => 20/10 = 2 + 'v2/p2p/merchantList': 2, + 'v2/p2p/merchantInfo': 2, + 'v2/p2p/orderList': 2, + 'v2/p2p/advList': 2, + }, + }, + 'broker': { + 'get': { + 'broker/v1/account/info': 2, # 10 times/1s(UID) => 20/10 = 2 + 'broker/v1/account/sub-list': 20, # 1 times/1s(UID) => 20/1 = 20 + 'broker/v1/account/sub-email': 20, # 1 times/1s(UID) => 20/1 = 20 + 'broker/v1/account/sub-spot-assets': 2, # 10 times/1s(UID) => 20/10 = 2 + 'broker/v1/account/sub-future-assets': 2, # 10 times/1s(UID) => 20/10 = 2 + 'broker/v1/account/subaccount-transfer': 1, # unknown + 'broker/v1/account/subaccount-deposit': 1, # unknown + 'broker/v1/account/subaccount-withdrawal': 1, # unknown + 'broker/v1/account/sub-api-list': 2, # 10 times/1s(UID) => 20/10 = 2 + 'v2/broker/account/info': 2, + 'v2/broker/account/subaccount-list': 20, + 'v2/broker/account/subaccount-email': 2, + 'v2/broker/account/subaccount-spot-assets': 2, + 'v2/broker/account/subaccount-future-assets': 2, + 'v2/broker/manage/subaccount-apikey-list': 2, + }, + 'post': { + 'broker/v1/account/sub-create': 20, # 1 times/1s(UID) => 20/1 = 20 + 'broker/v1/account/sub-modify': 20, # 1 times/1s(UID) => 20/1 = 20 + 'broker/v1/account/sub-modify-email': 20, # 1 times/1s(UID) => 20/1 = 20 + 'broker/v1/account/sub-address': 2, # 10 times/1s(UID) => 20/10 = 2 + 'broker/v1/account/sub-withdrawal': 2, # 10 times/1s(UID) => 20/10 = 2 + 'broker/v1/account/sub-auto-transfer': 4, # 5 times/1s(UID) => 20/5 = 4 + 'broker/v1/account/sub-api-create': 2, # 10 times/1s(UID) => 20/10 = 2 + 'broker/v1/account/sub-api-modify': 2, # 10 times/1s(UID) => 20/10 = 2 + 'v2/broker/account/modify-subaccount-email': 2, + 'v2/broker/account/create-subaccount': 20, + 'v2/broker/account/modify-subaccount': 20, + 'v2/broker/account/subaccount-address': 2, + 'v2/broker/account/subaccount-withdrawal': 2, + 'v2/broker/account/set-subaccount-autotransfer': 2, + 'v2/broker/manage/create-subaccount-apikey': 2, + 'v2/broker/manage/modify-subaccount-apikey': 2, + }, + }, + 'margin': { + 'get': { + 'margin/v1/cross/account/riskRate': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/cross/account/maxTransferOutAmount': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/isolated/account/maxTransferOutAmount': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/isolated/order/openOrders': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/isolated/order/history': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/isolated/order/fills': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/isolated/loan/list': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/isolated/repay/list': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/isolated/interest/list': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/isolated/liquidation/list': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/isolated/fin/list': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/cross/order/openOrders': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/cross/order/history': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/cross/order/fills': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/cross/loan/list': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/cross/repay/list': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/cross/interest/list': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/cross/liquidation/list': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/cross/fin/list': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/cross/account/assets': 2, # 10 times/1s(IP) => 20/10 = 2 + 'margin/v1/isolated/account/assets': 2, # 10 times/1s(IP) => 20/10 = 2 + 'v2/margin/crossed/borrow-history': 2, + 'v2/margin/crossed/repay-history': 2, + 'v2/margin/crossed/interest-history': 2, + 'v2/margin/crossed/liquidation-history': 2, + 'v2/margin/crossed/financial-records': 2, + 'v2/margin/crossed/account/assets': 2, + 'v2/margin/crossed/account/risk-rate': 2, + 'v2/margin/crossed/account/max-borrowable-amount': 2, + 'v2/margin/crossed/account/max-transfer-out-amount': 2, + 'v2/margin/crossed/interest-rate-and-limit': 2, + 'v2/margin/crossed/tier-data': 2, + 'v2/margin/crossed/open-orders': 2, + 'v2/margin/crossed/history-orders': 2, + 'v2/margin/crossed/fills': 2, + 'v2/margin/isolated/borrow-history': 2, + 'v2/margin/isolated/repay-history': 2, + 'v2/margin/isolated/interest-history': 2, + 'v2/margin/isolated/liquidation-history': 2, + 'v2/margin/isolated/financial-records': 2, + 'v2/margin/isolated/account/assets': 2, + 'v2/margin/isolated/account/risk-rate': 2, + 'v2/margin/isolated/account/max-borrowable-amount': 2, + 'v2/margin/isolated/account/max-transfer-out-amount': 2, + 'v2/margin/isolated/interest-rate-and-limit': 2, + 'v2/margin/isolated/tier-data': 2, + 'v2/margin/isolated/open-orders': 2, + 'v2/margin/isolated/history-orders': 2, + 'v2/margin/isolated/fills': 2, + }, + 'post': { + 'margin/v1/cross/account/borrow': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/isolated/account/borrow': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/cross/account/repay': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/isolated/account/repay': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/isolated/account/riskRate': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/cross/account/maxBorrowableAmount': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/isolated/account/maxBorrowableAmount': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/isolated/account/flashRepay': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/isolated/account/queryFlashRepayStatus': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/cross/account/flashRepay': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/cross/account/queryFlashRepayStatus': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/isolated/order/placeOrder': 4, # 5 times/1s(UID) => 20/5 = 4 + 'margin/v1/isolated/order/batchPlaceOrder': 4, # 5 times/1s(UID) => 20/5 = 4 + 'margin/v1/isolated/order/cancelOrder': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/isolated/order/batchCancelOrder': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/cross/order/placeOrder': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/cross/order/batchPlaceOrder': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/cross/order/cancelOrder': 2, # 10 times/1s(UID) => 20/10 = 2 + 'margin/v1/cross/order/batchCancelOrder': 2, # 10 times/1s(UID) => 20/10 = 2 + 'v2/margin/crossed/account/borrow': 2, + 'v2/margin/crossed/account/repay': 2, + 'v2/margin/crossed/account/flash-repay': 2, + 'v2/margin/crossed/account/query-flash-repay-status': 2, + 'v2/margin/crossed/place-order': 2, + 'v2/margin/crossed/batch-place-order': 2, + 'v2/margin/crossed/cancel-order': 2, + 'v2/margin/crossed/batch-cancel-order': 2, + 'v2/margin/isolated/account/borrow': 2, + 'v2/margin/isolated/account/repay': 2, + 'v2/margin/isolated/account/flash-repay': 2, + 'v2/margin/isolated/account/query-flash-repay-status': 2, + 'v2/margin/isolated/place-order': 2, + 'v2/margin/isolated/batch-place-order': 2, + 'v2/margin/isolated/cancel-order': 2, + 'v2/margin/isolated/batch-cancel-order': 2, + }, + }, + 'copy': { + 'get': { + 'v2/copy/mix-trader/order-current-track': 2, + 'v2/copy/mix-trader/order-history-track': 2, + 'v2/copy/mix-trader/order-total-detail': 2, + 'v2/copy/mix-trader/profit-history-summarys': 1, + 'v2/copy/mix-trader/profit-history-details': 1, + 'v2/copy/mix-trader/profit-details': 1, + 'v2/copy/mix-trader/profits-group-coin-date': 1, + 'v2/copy/mix-trader/config-query-symbols': 1, + 'v2/copy/mix-trader/config-query-followers': 2, + 'v2/copy/mix-follower/query-current-orders': 2, + 'v2/copy/mix-follower/query-history-orders': 1, + 'v2/copy/mix-follower/query-settings': 2, + 'v2/copy/mix-follower/query-traders': 2, + 'v2/copy/mix-follower/query-quantity-limit': 2, + 'v2/copy/mix-broker/query-traders': 2, + 'v2/copy/mix-broker/query-history-traces': 2, + 'v2/copy/mix-broker/query-current-traces': 2, + 'v2/copy/spot-trader/profit-summarys': 2, + 'v2/copy/spot-trader/profit-history-details': 2, + 'v2/copy/spot-trader/profit-details': 2, + 'v2/copy/spot-trader/order-total-detail': 2, + 'v2/copy/spot-trader/order-history-track': 2, + 'v2/copy/spot-trader/order-current-track': 2, + 'v2/copy/spot-trader/config-query-settings': 2, + 'v2/copy/spot-trader/config-query-followers': 2, + 'v2/copy/spot-follower/query-traders': 2, + 'v2/copy/spot-follower/query-trader-symbols': 2, + 'v2/copy/spot-follower/query-settings': 2, + 'v2/copy/spot-follower/query-history-orders': 2, + 'v2/copy/spot-follower/query-current-orders': 2, + }, + 'post': { + 'v2/copy/mix-trader/order-modify-tpsl': 2, + 'v2/copy/mix-trader/order-close-positions': 2, + 'v2/copy/mix-trader/config-setting-symbols': 2, + 'v2/copy/mix-trader/config-setting-base': 2, + 'v2/copy/mix-trader/config-remove-follower': 2, + 'v2/copy/mix-follower/setting-tpsl': 1, + 'v2/copy/mix-follower/settings': 2, + 'v2/copy/mix-follower/close-positions': 2, + 'v2/copy/mix-follower/cancel-trader': 4, + 'v2/copy/spot-trader/order-modify-tpsl': 2, + 'v2/copy/spot-trader/order-close-tracking': 2, + 'v2/copy/spot-trader/config-setting-symbols': 2, + 'v2/copy/spot-trader/config-remove-follower': 2, + 'v2/copy/spot-follower/stop-order': 2, + 'v2/copy/spot-follower/settings': 2, + 'v2/copy/spot-follower/setting-tpsl': 2, + 'v2/copy/spot-follower/order-close-tracking': 2, + 'v2/copy/spot-follower/cancel-trader': 2, + }, + }, + 'tax': { + 'get': { + 'v2/tax/spot-record': 20, + 'v2/tax/future-record': 20, + 'v2/tax/margin-record': 20, + 'v2/tax/p2p-record': 20, + }, + }, + 'convert': { + 'get': { + 'v2/convert/currencies': 2, + 'v2/convert/quoted-price': 2, + 'v2/convert/convert-record': 2, + 'v2/convert/bgb-convert-coin-list': 2, + 'v2/convert/bgb-convert-records': 2, + }, + 'post': { + 'v2/convert/trade': 2, + 'v2/convert/bgb-convert': 2, + }, + }, + 'earn': { + 'get': { + 'v2/earn/savings/product': 2, + 'v2/earn/savings/account': 2, + 'v2/earn/savings/assets': 2, + 'v2/earn/savings/records': 2, + 'v2/earn/savings/subscribe-info': 2, + 'v2/earn/savings/subscribe-result': 2, + 'v2/earn/savings/redeem-result': 2, + 'v2/earn/sharkfin/product': 2, + 'v2/earn/sharkfin/account': 2, + 'v2/earn/sharkfin/assets': 2, + 'v2/earn/sharkfin/records': 2, + 'v2/earn/sharkfin/subscribe-info': 2, + 'v2/earn/sharkfin/subscribe-result': 4, + 'v2/earn/loan/ongoing-orders': 2, + 'v2/earn/loan/repay-history': 2, + 'v2/earn/loan/revise-history': 2, + 'v2/earn/loan/borrow-history': 2, + 'v2/earn/loan/debts': 2, + 'v2/earn/loan/reduces': 2, + 'v2/earn/account/assets': 2, + }, + 'post': { + 'v2/earn/savings/subscribe': 2, + 'v2/earn/savings/redeem': 2, + 'v2/earn/sharkfin/subscribe': 2, + 'v2/earn/loan/borrow': 2, + 'v2/earn/loan/repay': 2, + 'v2/earn/loan/revise-pledge': 2, + }, + }, + 'common': { + 'get': { + 'v2/common/trade-rate': 2, + }, + }, + 'uta': { + 'get': { + 'v3/account/assets': 1, + 'v3/account/settings': 1, + 'v3/account/deposit-records': 2, + 'v3/account/financial-records': 1, + 'v3/account/repayable-coins': 2, + 'v3/account/payment-coins': 2, + 'v3/account/convert-records': 1, + 'v3/account/transferable-coins': 2, + 'v3/account/sub-transfer-record': 4, + 'v3/ins-loan/transfered': 6.6667, + 'v3/ins-loan/symbols': 6.6667, + 'v3/ins-loan/risk-unit': 6.6667, + 'v3/ins-loan/repaid-history': 6.6667, + 'v3/ins-loan/product-infos': 6.6667, + 'v3/ins-loan/loan-order': 6.6667, + 'v3/ins-loan/ltv-convert': 6.6667, + 'v3/ins-loan/ensure-coins-convert': 6.6667, + 'v3/position/current-position': 1, + 'v3/position/history-position': 1, + 'v3/trade/order-info': 1, + 'v3/trade/unfilled-orders': 1, + 'v3/trade/unfilled-strategy-orders': 1, + 'v3/trade/history-orders': 1, + 'v3/trade/history-strategy-orders': 1, + 'v3/trade/fills': 1, + 'v3/user/sub-list': 2, + 'v3/user/sub-api-list': 2, + }, + 'post': { + 'v3/account/set-leverage': 2, + 'v3/account/set-hold-mode': 2, + 'v3/account/repay': 4, + 'v3/account/transfer': 4, + 'v3/account/sub-transfer': 4, + 'v3/account/max-open-available': 4, + 'v3/ins-loan/bind-uid': 6.6667, + 'v3/trade/place-order': 2, + 'v3/trade/place-strategy-order': 2, + 'v3/trade/modify-order': 2, + 'v3/trade/modify-strategy-order': 2, + 'v3/trade/cancel-order': 2, + 'v3/trade/cancel-strategy-order': 2, + 'v3/trade/place-batch': 4, + 'v3/trade/batch-modify-order': 2, + 'v3/trade/cancel-batch': 4, + 'v3/trade/cancel-symbol-order': 4, + 'v3/trade/close-positions': 4, + 'v3/user/create-sub': 2, + 'v3/user/freeze-sub': 2, + 'v3/user/create-sub-api': 2, + 'v3/user/update-sub-api': 2, + 'v3/user/delete-sub-api': 2, + }, + }, + }, + }, + 'fees': { + 'spot': { + 'taker': self.parse_number('0.002'), + 'maker': self.parse_number('0.002'), + }, + 'swap': { + 'taker': self.parse_number('0.0006'), + 'maker': self.parse_number('0.0004'), + }, + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + 'password': True, + }, + 'exceptions': { + # http error codes + # 400 Bad Request — Invalid request format + # 401 Unauthorized — Invalid API Key + # 403 Forbidden — You do not have access to the requested resource + # 404 Not Found + # 500 Internal Server Error — We had a problem with our server + 'exact': { + '1': ExchangeError, # {"code": 1, "message": "System error"} + # undocumented + 'failure to get a peer from the ring-balancer': ExchangeNotAvailable, # {"message": "failure to get a peer from the ring-balancer"} + '4010': PermissionDenied, # {"code": 4010, "message": "For the security of your funds, withdrawals are not permitted within 24 hours after changing fund password / mobile number / Google Authenticator settings "} + # common + # '0': ExchangeError, # 200 successful,when the order placement / cancellation / operation is successful + '4001': ExchangeError, # no data received in 30s + '4002': ExchangeError, # Buffer full. cannot write data + '40020': BadRequest, # {"code":"40020","msg":"Parameter orderId error","requestTime":1754305078588,"data":null} + # -------------------------------------------------------- + '30001': AuthenticationError, # {"code": 30001, "message": 'request header "OK_ACCESS_KEY" cannot be blank'} + '30002': AuthenticationError, # {"code": 30002, "message": 'request header "OK_ACCESS_SIGN" cannot be blank'} + '30003': AuthenticationError, # {"code": 30003, "message": 'request header "OK_ACCESS_TIMESTAMP" cannot be blank'} + '30004': AuthenticationError, # {"code": 30004, "message": 'request header "OK_ACCESS_PASSPHRASE" cannot be blank'} + '30005': InvalidNonce, # {"code": 30005, "message": "invalid OK_ACCESS_TIMESTAMP"} + '30006': AuthenticationError, # {"code": 30006, "message": "invalid OK_ACCESS_KEY"} + '30007': BadRequest, # {"code": 30007, "message": 'invalid Content_Type, please use "application/json" format'} + '30008': RequestTimeout, # {"code": 30008, "message": "timestamp request expired"} + '30009': ExchangeError, # {"code": 30009, "message": "system error"} + '30010': AuthenticationError, # {"code": 30010, "message": "API validation failed"} + '30011': PermissionDenied, # {"code": 30011, "message": "invalid IP"} + '30012': AuthenticationError, # {"code": 30012, "message": "invalid authorization"} + '30013': AuthenticationError, # {"code": 30013, "message": "invalid sign"} + '30014': DDoSProtection, # {"code": 30014, "message": "request too frequent"} + '30015': AuthenticationError, # {"code": 30015, "message": 'request header "OK_ACCESS_PASSPHRASE" incorrect'} + '30016': ExchangeError, # {"code": 30015, "message": "you are using v1 apiKey, please use v1 endpoint. If you would like to use v3 endpoint, please subscribe to v3 apiKey"} + '30017': ExchangeError, # {"code": 30017, "message": "apikey's broker id does not match"} + '30018': ExchangeError, # {"code": 30018, "message": "apikey's domain does not match"} + '30019': ExchangeNotAvailable, # {"code": 30019, "message": "Api is offline or unavailable"} + '30020': BadRequest, # {"code": 30020, "message": "body cannot be blank"} + '30021': BadRequest, # {"code": 30021, "message": "Json data format error"}, {"code": 30021, "message": "json data format error"} + '30022': PermissionDenied, # {"code": 30022, "message": "Api has been frozen"} + '30023': BadRequest, # {"code": 30023, "message": "{0} parameter cannot be blank"} + '30024': BadSymbol, # {"code":30024,"message":"\"instrument_id\" is an invalid parameter"} + '30025': BadRequest, # {"code": 30025, "message": "{0} parameter category error"} + '30026': DDoSProtection, # {"code": 30026, "message": "requested too frequent"} + '30027': AuthenticationError, # {"code": 30027, "message": "login failure"} + '30028': PermissionDenied, # {"code": 30028, "message": "unauthorized execution"} + '30029': AccountSuspended, # {"code": 30029, "message": "account suspended"} + '30030': ExchangeError, # {"code": 30030, "message": "endpoint request failed. Please try again"} + '30031': BadRequest, # {"code": 30031, "message": "token does not exist"} + '30032': BadSymbol, # {"code": 30032, "message": "pair does not exist"} + '30033': BadRequest, # {"code": 30033, "message": "exchange domain does not exist"} + '30034': ExchangeError, # {"code": 30034, "message": "exchange ID does not exist"} + '30035': ExchangeError, # {"code": 30035, "message": "trading is not hasattr(self, supported) website"} + '30036': ExchangeError, # {"code": 30036, "message": "no relevant data"} + '30037': ExchangeNotAvailable, # {"code": 30037, "message": "endpoint is offline or unavailable"} + # '30038': AuthenticationError, # {"code": 30038, "message": "user does not exist"} + '30038': OnMaintenance, # {"client_oid":"","code":"30038","error_code":"30038","error_message":"Matching engine is being upgraded. Please try in about 1 minute.","message":"Matching engine is being upgraded. Please try in about 1 minute.","order_id":"-1","result":false} + # futures + '32001': AccountSuspended, # {"code": 32001, "message": "futures account suspended"} + '32002': PermissionDenied, # {"code": 32002, "message": "futures account does not exist"} + '32003': CancelPending, # {"code": 32003, "message": "canceling, please wait"} + '32004': ExchangeError, # {"code": 32004, "message": "you have no unfilled orders"} + '32005': InvalidOrder, # {"code": 32005, "message": "max order quantity"} + '32006': InvalidOrder, # {"code": 32006, "message": "the order price or trigger price exceeds USD 1 million"} + '32007': InvalidOrder, # {"code": 32007, "message": "leverage level must be the same for orders on the same side of the contract"} + '32008': InvalidOrder, # {"code": 32008, "message": "Max. positions to open(cross margin)"} + '32009': InvalidOrder, # {"code": 32009, "message": "Max. positions to open(fixed margin)"} + '32010': ExchangeError, # {"code": 32010, "message": "leverage cannot be changed with open positions"} + '32011': ExchangeError, # {"code": 32011, "message": "futures status error"} + '32012': ExchangeError, # {"code": 32012, "message": "futures order update error"} + '32013': ExchangeError, # {"code": 32013, "message": "token type is blank"} + '32014': ExchangeError, # {"code": 32014, "message": "your number of contracts closing is larger than the number of contracts available"} + '32015': ExchangeError, # {"code": 32015, "message": "margin ratio is lower than 100% before opening positions"} + '32016': ExchangeError, # {"code": 32016, "message": "margin ratio is lower than 100% after opening position"} + '32017': ExchangeError, # {"code": 32017, "message": "no BBO"} + '32018': ExchangeError, # {"code": 32018, "message": "the order quantity is less than 1, please try again"} + '32019': ExchangeError, # {"code": 32019, "message": "the order price deviates from the price of the previous minute by more than 3%"} + '32020': ExchangeError, # {"code": 32020, "message": "the price is not in the range of the price limit"} + '32021': ExchangeError, # {"code": 32021, "message": "leverage error"} + '32022': ExchangeError, # {"code": 32022, "message": "self function is not supported in your country or region according to the regulations"} + '32023': ExchangeError, # {"code": 32023, "message": "self account has outstanding loan"} + '32024': ExchangeError, # {"code": 32024, "message": "order cannot be placed during delivery"} + '32025': ExchangeError, # {"code": 32025, "message": "order cannot be placed during settlement"} + '32026': ExchangeError, # {"code": 32026, "message": "your account is restricted from opening positions"} + '32027': ExchangeError, # {"code": 32027, "message": "cancelled over 20 orders"} + '32028': AccountSuspended, # {"code": 32028, "message": "account is suspended and liquidated"} + '32029': ExchangeError, # {"code": 32029, "message": "order info does not exist"} + '32030': InvalidOrder, # The order cannot be cancelled + '32031': ArgumentsRequired, # client_oid or order_id is required. + '32038': AuthenticationError, # User does not exist + '32040': ExchangeError, # User have open contract orders or position + '32044': ExchangeError, # {"code": 32044, "message": "The margin ratio after submitting self order is lower than the minimum requirement({0}) for your tier."} + '32045': ExchangeError, # str of commission over 1 million + '32046': ExchangeError, # Each user can hold up to 10 trade plans at the same time + '32047': ExchangeError, # system error + '32048': InvalidOrder, # Order strategy track range error + '32049': ExchangeError, # Each user can hold up to 10 track plans at the same time + '32050': InvalidOrder, # Order strategy rang error + '32051': InvalidOrder, # Order strategy ice depth error + '32052': ExchangeError, # str of commission over 100 thousand + '32053': ExchangeError, # Each user can hold up to 6 ice plans at the same time + '32057': ExchangeError, # The order price is zero. Market-close-all function cannot be executed + '32054': ExchangeError, # Trade not allow + '32055': InvalidOrder, # cancel order error + '32056': ExchangeError, # iceberg per order average should between {0}-{1} contracts + '32058': ExchangeError, # Each user can hold up to 6 initiative plans at the same time + '32059': InvalidOrder, # Total amount should exceed per order amount + '32060': InvalidOrder, # Order strategy type error + '32061': InvalidOrder, # Order strategy initiative limit error + '32062': InvalidOrder, # Order strategy initiative range error + '32063': InvalidOrder, # Order strategy initiative rate error + '32064': ExchangeError, # Time Stringerval of orders should set between 5-120s + '32065': ExchangeError, # Close amount exceeds the limit of Market-close-all(999 for BTC, and 9999 for the rest tokens) + '32066': ExchangeError, # You have open orders. Please cancel all open orders before changing your leverage level. + '32067': ExchangeError, # Account equity < required hasattr(self, margin) setting. Please adjust your leverage level again. + '32068': ExchangeError, # The margin for self position will fall short of the required hasattr(self, margin) setting. Please adjust your leverage level or increase your margin to proceed. + '32069': ExchangeError, # Target leverage level too low. Your account balance is insufficient to cover the margin required. Please adjust the leverage level again. + '32070': ExchangeError, # Please check open position or unfilled order + '32071': ExchangeError, # Your current liquidation mode does not support self action. + '32072': ExchangeError, # The highest available margin for your order’s tier is {0}. Please edit your margin and place a new order. + '32073': ExchangeError, # The action does not apply to the token + '32074': ExchangeError, # The number of contracts of your position, open orders, and the current order has exceeded the maximum order limit of self asset. + '32075': ExchangeError, # Account risk rate breach + '32076': ExchangeError, # Liquidation of the holding position(s) at market price will require cancellation of all pending close orders of the contracts. + '32077': ExchangeError, # Your margin for self asset in futures account is insufficient and the position has been taken over for liquidation.(You will not be able to place orders, close positions, transfer funds, or add margin during self period of time. Your account will be restored after the liquidation is complete.) + '32078': ExchangeError, # Please cancel all open orders before switching the liquidation mode(Please cancel all open orders before switching the liquidation mode) + '32079': ExchangeError, # Your open positions are at high risk.(Please add margin or reduce positions before switching the mode) + '32080': ExchangeError, # Funds cannot be transferred out within 30 minutes after futures settlement + '32083': ExchangeError, # The number of contracts should be a positive multiple of %%. Please place your order again + # token and margin trading + '33001': PermissionDenied, # {"code": 33001, "message": "margin account for self pair is not enabled yet"} + '33002': AccountSuspended, # {"code": 33002, "message": "margin account for self pair is suspended"} + '33003': InsufficientFunds, # {"code": 33003, "message": "no loan balance"} + '33004': ExchangeError, # {"code": 33004, "message": "loan amount cannot be smaller than the minimum limit"} + '33005': ExchangeError, # {"code": 33005, "message": "repayment amount must exceed 0"} + '33006': ExchangeError, # {"code": 33006, "message": "loan order not found"} + '33007': ExchangeError, # {"code": 33007, "message": "status not found"} + '33008': InsufficientFunds, # {"code": 33008, "message": "loan amount cannot exceed the maximum limit"} + '33009': ExchangeError, # {"code": 33009, "message": "user ID is blank"} + '33010': ExchangeError, # {"code": 33010, "message": "you cannot cancel an order during session 2 of call auction"} + '33011': ExchangeError, # {"code": 33011, "message": "no new market data"} + '33012': ExchangeError, # {"code": 33012, "message": "order cancellation failed"} + '33013': InvalidOrder, # {"code": 33013, "message": "order placement failed"} + '33014': OrderNotFound, # {"code": 33014, "message": "order does not exist"} + '33015': InvalidOrder, # {"code": 33015, "message": "exceeded maximum limit"} + '33016': ExchangeError, # {"code": 33016, "message": "margin trading is not open for self token"} + '33017': InsufficientFunds, # {"code": 33017, "message": "insufficient balance"} + '33018': ExchangeError, # {"code": 33018, "message": "self parameter must be smaller than 1"} + '33020': ExchangeError, # {"code": 33020, "message": "request not supported"} + '33021': BadRequest, # {"code": 33021, "message": "token and the pair do not match"} + '33022': InvalidOrder, # {"code": 33022, "message": "pair and the order do not match"} + '33023': ExchangeError, # {"code": 33023, "message": "you can only place market orders during call auction"} + '33024': InvalidOrder, # {"code": 33024, "message": "trading amount too small"} + '33025': InvalidOrder, # {"code": 33025, "message": "base token amount is blank"} + '33026': ExchangeError, # {"code": 33026, "message": "transaction completed"} + '33027': InvalidOrder, # {"code": 33027, "message": "cancelled order or order cancelling"} + '33028': InvalidOrder, # {"code": 33028, "message": "the decimal places of the trading price exceeded the limit"} + '33029': InvalidOrder, # {"code": 33029, "message": "the decimal places of the trading size exceeded the limit"} + '33034': ExchangeError, # {"code": 33034, "message": "You can only place limit order after Call Auction has started"} + '33035': ExchangeError, # This type of order cannot be canceled(This type of order cannot be canceled) + '33036': ExchangeError, # Exceeding the limit of entrust order + '33037': ExchangeError, # The buy order price should be lower than 130% of the trigger price + '33038': ExchangeError, # The sell order price should be higher than 70% of the trigger price + '33039': ExchangeError, # The limit of callback rate is 0 < x <= 5% + '33040': ExchangeError, # The trigger price of a buy order should be lower than the latest transaction price + '33041': ExchangeError, # The trigger price of a sell order should be higher than the latest transaction price + '33042': ExchangeError, # The limit of price variance is 0 < x <= 1% + '33043': ExchangeError, # The total amount must be larger than 0 + '33044': ExchangeError, # The average amount should be 1/1000 * total amount <= x <= total amount + '33045': ExchangeError, # The price should not be 0, including trigger price, order price, and price limit + '33046': ExchangeError, # Price variance should be 0 < x <= 1% + '33047': ExchangeError, # Sweep ratio should be 0 < x <= 100% + '33048': ExchangeError, # Per order limit: Total amount/1000 < x <= Total amount + '33049': ExchangeError, # Total amount should be X > 0 + '33050': ExchangeError, # Time interval should be 5 <= x <= 120s + '33051': ExchangeError, # cancel order number not higher limit: plan and track entrust no more than 10, ice and time entrust no more than 6 + '33059': BadRequest, # {"code": 33059, "message": "client_oid or order_id is required"} + '33060': BadRequest, # {"code": 33060, "message": "Only fill in either parameter client_oid or order_id"} + '33061': ExchangeError, # Value of a single market price order cannot exceed 100,000 USD + '33062': ExchangeError, # The leverage ratio is too high. The borrowed position has exceeded the maximum position of self leverage ratio. Please readjust the leverage ratio + '33063': ExchangeError, # Leverage multiple is too low, there is insufficient margin in the account, please readjust the leverage ratio + '33064': ExchangeError, # The setting of the leverage ratio cannot be less than 2, please readjust the leverage ratio + '33065': ExchangeError, # Leverage ratio exceeds maximum leverage ratio, please readjust leverage ratio + # account + '21009': ExchangeError, # Funds cannot be transferred out within 30 minutes after swap settlement(Funds cannot be transferred out within 30 minutes after swap settlement) + '34001': PermissionDenied, # {"code": 34001, "message": "withdrawal suspended"} + '34002': InvalidAddress, # {"code": 34002, "message": "please add a withdrawal address"} + '34003': ExchangeError, # {"code": 34003, "message": "sorry, self token cannot be withdrawn to xx at the moment"} + '34004': ExchangeError, # {"code": 34004, "message": "withdrawal fee is smaller than minimum limit"} + '34005': ExchangeError, # {"code": 34005, "message": "withdrawal fee exceeds the maximum limit"} + '34006': ExchangeError, # {"code": 34006, "message": "withdrawal amount is lower than the minimum limit"} + '34007': ExchangeError, # {"code": 34007, "message": "withdrawal amount exceeds the maximum limit"} + '34008': InsufficientFunds, # {"code": 34008, "message": "insufficient balance"} + '34009': ExchangeError, # {"code": 34009, "message": "your withdrawal amount exceeds the daily limit"} + '34010': ExchangeError, # {"code": 34010, "message": "transfer amount must be larger than 0"} + '34011': ExchangeError, # {"code": 34011, "message": "conditions not met"} + '34012': ExchangeError, # {"code": 34012, "message": "the minimum withdrawal amount for NEO is 1, and the amount must be an integer"} + '34013': ExchangeError, # {"code": 34013, "message": "please transfer"} + '34014': ExchangeError, # {"code": 34014, "message": "transfer limited"} + '34015': ExchangeError, # {"code": 34015, "message": "subaccount does not exist"} + '34016': PermissionDenied, # {"code": 34016, "message": "transfer suspended"} + '34017': AccountSuspended, # {"code": 34017, "message": "account suspended"} + '34018': AuthenticationError, # {"code": 34018, "message": "incorrect trades password"} + '34019': PermissionDenied, # {"code": 34019, "message": "please bind your email before withdrawal"} + '34020': PermissionDenied, # {"code": 34020, "message": "please bind your funds password before withdrawal"} + '34021': InvalidAddress, # {"code": 34021, "message": "Not verified address"} + '34022': ExchangeError, # {"code": 34022, "message": "Withdrawals are not available for sub accounts"} + '34023': PermissionDenied, # {"code": 34023, "message": "Please enable futures trading before transferring your funds"} + '34026': ExchangeError, # transfer too frequently(transfer too frequently) + '34036': ExchangeError, # Parameter is incorrect, please refer to API documentation + '34037': ExchangeError, # Get the sub-account balance interface, account type is not supported + '34038': ExchangeError, # Since your C2C transaction is unusual, you are restricted from fund transfer. Please contact our customer support to cancel the restriction + '34039': ExchangeError, # You are now restricted from transferring out your funds due to abnormal trades on C2C Market. Please transfer your fund on our website or app instead to verify your identity + # swap + '35001': ExchangeError, # {"code": 35001, "message": "Contract does not exist"} + '35002': ExchangeError, # {"code": 35002, "message": "Contract settling"} + '35003': ExchangeError, # {"code": 35003, "message": "Contract paused"} + '35004': ExchangeError, # {"code": 35004, "message": "Contract pending settlement"} + '35005': AuthenticationError, # {"code": 35005, "message": "User does not exist"} + '35008': InvalidOrder, # {"code": 35008, "message": "Risk ratio too high"} + '35010': InvalidOrder, # {"code": 35010, "message": "Position closing too large"} + '35012': InvalidOrder, # {"code": 35012, "message": "Incorrect order size"} + '35014': InvalidOrder, # {"code": 35014, "message": "Order price is not within limit"} + '35015': InvalidOrder, # {"code": 35015, "message": "Invalid leverage level"} + '35017': ExchangeError, # {"code": 35017, "message": "Open orders exist"} + '35019': InvalidOrder, # {"code": 35019, "message": "Order size too large"} + '35020': InvalidOrder, # {"code": 35020, "message": "Order price too high"} + '35021': InvalidOrder, # {"code": 35021, "message": "Order size exceeded current tier limit"} + '35022': ExchangeError, # {"code": 35022, "message": "Contract status error"} + '35024': ExchangeError, # {"code": 35024, "message": "Contract not initialized"} + '35025': InsufficientFunds, # {"code": 35025, "message": "No account balance"} + '35026': ExchangeError, # {"code": 35026, "message": "Contract settings not initialized"} + '35029': OrderNotFound, # {"code": 35029, "message": "Order does not exist"} + '35030': InvalidOrder, # {"code": 35030, "message": "Order size too large"} + '35031': InvalidOrder, # {"code": 35031, "message": "Cancel order size too large"} + '35032': ExchangeError, # {"code": 35032, "message": "Invalid user status"} + '35037': ExchangeError, # No last traded price in cache + '35039': ExchangeError, # {"code": 35039, "message": "Open order quantity exceeds limit"} + '35040': InvalidOrder, # {"error_message":"Invalid order type","result":"true","error_code":"35040","order_id":"-1"} + '35044': ExchangeError, # {"code": 35044, "message": "Invalid order status"} + '35046': InsufficientFunds, # {"code": 35046, "message": "Negative account balance"} + '35047': InsufficientFunds, # {"code": 35047, "message": "Insufficient account balance"} + '35048': ExchangeError, # {"code": 35048, "message": "User contract is frozen and liquidating"} + '35049': InvalidOrder, # {"code": 35049, "message": "Invalid order type"} + '35050': InvalidOrder, # {"code": 35050, "message": "Position settings are blank"} + '35052': InsufficientFunds, # {"code": 35052, "message": "Insufficient cross margin"} + '35053': ExchangeError, # {"code": 35053, "message": "Account risk too high"} + '35055': InsufficientFunds, # {"code": 35055, "message": "Insufficient account balance"} + '35057': ExchangeError, # {"code": 35057, "message": "No last traded price"} + '35058': ExchangeError, # {"code": 35058, "message": "No limit"} + '35059': BadRequest, # {"code": 35059, "message": "client_oid or order_id is required"} + '35060': BadRequest, # {"code": 35060, "message": "Only fill in either parameter client_oid or order_id"} + '35061': BadRequest, # {"code": 35061, "message": "Invalid instrument_id"} + '35062': InvalidOrder, # {"code": 35062, "message": "Invalid match_price"} + '35063': InvalidOrder, # {"code": 35063, "message": "Invalid order_size"} + '35064': InvalidOrder, # {"code": 35064, "message": "Invalid client_oid"} + '35066': InvalidOrder, # Order interval error + '35067': InvalidOrder, # Time-weighted order ratio error + '35068': InvalidOrder, # Time-weighted order range error + '35069': InvalidOrder, # Time-weighted single transaction limit error + '35070': InvalidOrder, # Algo order type error + '35071': InvalidOrder, # Order total must be larger than single order limit + '35072': InvalidOrder, # Maximum 6 unfulfilled time-weighted orders can be held at the same time + '35073': InvalidOrder, # Order price is 0. Market-close-all not available + '35074': InvalidOrder, # Iceberg order single transaction average error + '35075': InvalidOrder, # Failed to cancel order + '35076': InvalidOrder, # LTC 20x leverage. Not allowed to open position + '35077': InvalidOrder, # Maximum 6 unfulfilled iceberg orders can be held at the same time + '35078': InvalidOrder, # Order amount exceeded 100,000 + '35079': InvalidOrder, # Iceberg order price variance error + '35080': InvalidOrder, # Callback rate error + '35081': InvalidOrder, # Maximum 10 unfulfilled trail orders can be held at the same time + '35082': InvalidOrder, # Trail order callback rate error + '35083': InvalidOrder, # Each user can only hold a maximum of 10 unfulfilled stop-limit orders at the same time + '35084': InvalidOrder, # Order amount exceeded 1 million + '35085': InvalidOrder, # Order amount is not in the correct range + '35086': InvalidOrder, # Price exceeds 100 thousand + '35087': InvalidOrder, # Price exceeds 100 thousand + '35088': InvalidOrder, # Average amount error + '35089': InvalidOrder, # Price exceeds 100 thousand + '35090': ExchangeError, # No stop-limit orders available for cancelation + '35091': ExchangeError, # No trail orders available for cancellation + '35092': ExchangeError, # No iceberg orders available for cancellation + '35093': ExchangeError, # No trail orders available for cancellation + '35094': ExchangeError, # Stop-limit order last traded price error + '35095': BadRequest, # Instrument_id error + '35096': ExchangeError, # Algo order status error + '35097': ExchangeError, # Order status and order ID cannot exist at the same time + '35098': ExchangeError, # An order status or order ID must exist + '35099': ExchangeError, # Algo order ID error + # option + '36001': BadRequest, # Invalid underlying index. + '36002': BadRequest, # Instrument does not exist. + '36005': ExchangeError, # Instrument status is invalid. + '36101': AuthenticationError, # Account does not exist. + '36102': PermissionDenied, # Account status is invalid. + '36103': AccountSuspended, # Account is suspended due to ongoing liquidation. + '36104': PermissionDenied, # Account is not enabled for options trading. + '36105': PermissionDenied, # Please enable the account for option contract. + '36106': AccountSuspended, # Funds cannot be transferred in or out, is suspended. + '36107': PermissionDenied, # Funds cannot be transferred out within 30 minutes after option exercising or settlement. + '36108': InsufficientFunds, # Funds cannot be transferred in or out, of the account is less than zero. + '36109': PermissionDenied, # Funds cannot be transferred in or out during option exercising or settlement. + '36201': PermissionDenied, # New order function is blocked. + '36202': PermissionDenied, # Account does not have permission to short option. + '36203': InvalidOrder, # Invalid format for client_oid. + '36204': ExchangeError, # Invalid format for request_id. + '36205': BadRequest, # Instrument id does not match underlying index. + '36206': BadRequest, # Order_id and client_oid can not be used at the same time. + '36207': InvalidOrder, # Either order price or fartouch price must be present. + '36208': InvalidOrder, # Either order price or size must be present. + '36209': InvalidOrder, # Either order_id or client_oid must be present. + '36210': InvalidOrder, # Either order_ids or client_oids must be present. + '36211': InvalidOrder, # Exceeding max batch size for order submission. + '36212': InvalidOrder, # Exceeding max batch size for oder cancellation. + '36213': InvalidOrder, # Exceeding max batch size for order amendment. + '36214': ExchangeError, # Instrument does not have valid bid/ask quote. + '36216': OrderNotFound, # Order does not exist. + '36217': InvalidOrder, # Order submission failed. + '36218': InvalidOrder, # Order cancellation failed. + '36219': InvalidOrder, # Order amendment failed. + '36220': InvalidOrder, # Order is pending cancel. + '36221': InvalidOrder, # Order qty is not valid multiple of lot size. + '36222': InvalidOrder, # Order price is breaching highest buy limit. + '36223': InvalidOrder, # Order price is breaching lowest sell limit. + '36224': InvalidOrder, # Exceeding max order size. + '36225': InvalidOrder, # Exceeding max open order count for instrument. + '36226': InvalidOrder, # Exceeding max open order count for underlying. + '36227': InvalidOrder, # Exceeding max open size across all orders for underlying + '36228': InvalidOrder, # Exceeding max available qty for instrument. + '36229': InvalidOrder, # Exceeding max available qty for underlying. + '36230': InvalidOrder, # Exceeding max position limit for underlying. + # -------------------------------------------------------- + # swap + '400': BadRequest, # Bad Request + '401': AuthenticationError, # Unauthorized access + '403': PermissionDenied, # Access prohibited + '404': BadRequest, # Request address does not exist + '405': BadRequest, # The HTTP Method is not supported + '415': BadRequest, # The current media type is not supported + '429': DDoSProtection, # Too many requests + '500': ExchangeNotAvailable, # System busy + '1001': RateLimitExceeded, # The request is too frequent and has been throttled + '1002': ExchangeError, # {0} verifications within 24 hours + '1003': ExchangeError, # You failed more than {0} times today, the current operation is locked, please try again in 24 hours + # '00000': ExchangeError, # success + '40001': AuthenticationError, # ACCESS_KEY cannot be empty + '40002': AuthenticationError, # SECRET_KEY cannot be empty + '40003': AuthenticationError, # Signature cannot be empty + '40004': InvalidNonce, # Request timestamp expired + '40005': InvalidNonce, # Invalid ACCESS_TIMESTAMP + '40006': AuthenticationError, # Invalid ACCESS_KEY + '40007': BadRequest, # Invalid Content_Type + '40008': InvalidNonce, # Request timestamp expired + '40009': AuthenticationError, # sign signature error + '40010': AuthenticationError, # sign signature error + '40011': AuthenticationError, # ACCESS_PASSPHRASE cannot be empty + '40012': AuthenticationError, # apikey/password is incorrect + '40013': ExchangeError, # User status is abnormal + '40014': PermissionDenied, # Incorrect permissions + '40015': ExchangeError, # System is abnormal, please try again later + '40016': PermissionDenied, # The user must bind the phone or Google + '40017': ExchangeError, # Parameter verification failed + '40018': PermissionDenied, # Invalid IP + '40019': BadRequest, # {"code":"40019","msg":"Parameter QLCUSDT_SPBL cannot be empty","requestTime":1679196063659,"data":null} + '40031': AccountSuspended, # The account has been cancelled and cannot be used again + '40037': AuthenticationError, # Apikey does not exist + '40102': BadRequest, # Contract configuration does not exist, please check the parameters + '40103': BadRequest, # Request method cannot be empty + '40104': ExchangeError, # Lever adjustment failure + '40105': ExchangeError, # Abnormal access to current price limit data + '40106': ExchangeError, # Abnormal get next settlement time + '40107': ExchangeError, # Abnormal access to index price data + '40108': InvalidOrder, # Wrong order quantity + '40109': OrderNotFound, # The data of the order cannot be found, please confirm the order number + '40200': OnMaintenance, # Server upgrade, please try again later + '40201': InvalidOrder, # Order number cannot be empty + '40202': ExchangeError, # User information cannot be empty + '40203': BadRequest, # The amount of adjustment margin cannot be empty or negative + '40204': BadRequest, # Adjustment margin type cannot be empty + '40205': BadRequest, # Adjusted margin type data is wrong + '40206': BadRequest, # The direction of the adjustment margin cannot be empty + '40207': BadRequest, # The adjustment margin data is wrong + '40208': BadRequest, # The accuracy of the adjustment margin amount is incorrect + '40209': BadRequest, # The current page number is wrong, please confirm + '40300': ExchangeError, # User does not exist + '40301': PermissionDenied, # Permission has not been obtained yet. If you need to use it, please contact customer service + '40302': BadRequest, # Parameter abnormality + '40303': BadRequest, # Can only query up to 20,000 data + '40304': BadRequest, # Parameter type is abnormal + '40305': BadRequest, # Client_oid length is not greater than 50, and cannot be Martian characters + '40306': ExchangeError, # Batch processing orders can only process up to 20 + '40308': OnMaintenance, # The contract is being temporarily maintained + '40309': BadSymbol, # The contract has been removed + '40400': ExchangeError, # Status check abnormal + '40401': ExchangeError, # The operation cannot be performed + '40402': BadRequest, # The opening direction cannot be empty + '40403': BadRequest, # Wrong opening direction format + '40404': BadRequest, # Whether to enable automatic margin call parameters cannot be empty + '40405': BadRequest, # Whether to enable the automatic margin call parameter type is wrong + '40406': BadRequest, # Whether to enable automatic margin call parameters is of unknown type + '40407': ExchangeError, # The query direction is not the direction entrusted by the plan + '40408': ExchangeError, # Wrong time range + '40409': ExchangeError, # Time format error + '40500': InvalidOrder, # Client_oid check error + '40501': ExchangeError, # Channel name error + '40502': ExchangeError, # If it is a copy user, you must pass the copy to whom + '40503': ExchangeError, # With the single type + '40504': ExchangeError, # Platform code must pass + '40505': ExchangeError, # Not the same type + '40506': AuthenticationError, # Platform signature error + '40507': AuthenticationError, # Api signature error + '40508': ExchangeError, # KOL is not authorized + '40509': ExchangeError, # Abnormal copy end + '40600': ExchangeError, # Copy function suspended + '40601': ExchangeError, # Followers cannot be KOL + '40602': ExchangeError, # The number of copies has reached the limit and cannot process the request + '40603': ExchangeError, # Abnormal copy end + '40604': ExchangeNotAvailable, # Server is busy, please try again later + '40605': ExchangeError, # Copy type, the copy number must be passed + '40606': ExchangeError, # The type of document number is wrong + '40607': ExchangeError, # Document number must be passed + '40608': ExchangeError, # No documented products currently supported + '40609': ExchangeError, # The contract product does not support copying + '40700': BadRequest, # Cursor parameters are incorrect + '40701': ExchangeError, # KOL is not authorized + '40702': ExchangeError, # Unauthorized copying user + '40703': ExchangeError, # Bill inquiry start and end time cannot be empty + '40704': ExchangeError, # Can only check the data of the last three months + '40705': BadRequest, # The start and end time cannot exceed 90 days + '40706': InvalidOrder, # Wrong order price + '40707': BadRequest, # Start time is greater than end time + '40708': BadRequest, # Parameter verification is abnormal + '40709': ExchangeError, # There is no hasattr(self, position) position, and no automatic margin call can be set + '40710': ExchangeError, # Abnormal account status + '40711': InsufficientFunds, # Insufficient contract account balance + '40712': InsufficientFunds, # Insufficient margin + '40713': ExchangeError, # Cannot exceed the maximum transferable margin amount + '40714': ExchangeError, # No direct margin call is allowed + '40762': InsufficientFunds, # {"code":"40762","msg":"The order amount exceeds the balance","requestTime":1716572156622,"data":null} + '40768': OrderNotFound, # Order does not exist + '40808': InvalidOrder, # {"code":"40808","msg":"Parameter verification exception size checkBDScale error value=2293.577 checkScale=2","requestTime":1725638500052,"data":null} + '41103': InvalidOrder, # {"code":"41103","msg":"param price scale error error","requestTime":1725635883561,"data":null} + '41114': OnMaintenance, # {"code":"41114","msg":"The current trading pair is under maintenance, please refer to the official announcement for the opening time","requestTime":1679196062544,"data":null} + '43011': InvalidOrder, # The parameter does not meet the specification executePrice <= 0 + '43001': OrderNotFound, + '43012': InsufficientFunds, # {"code":"43012","msg":"Insufficient balance","requestTime":1711648951774,"data":null} + '43025': InvalidOrder, # Plan order does not exist + '43115': OnMaintenance, # {"code":"43115","msg":"The current trading pair is opening soon, please refer to the official announcement for the opening time","requestTime":1688907202434,"data":null} + '45110': InvalidOrder, # {"code":"45110","msg":"less than the minimum amount 5 USDT","requestTime":1669911118932,"data":null} + '40774': InvalidOrder, # {"code":"40774","msg":"The order type for unilateral position must also be the unilateral position type.","requestTime":1758709764409,"data":null} + '45122': InvalidOrder, # {"code":"45122","msg":"Short position stop loss price please > mark price 106.86","requestTime":1758709970499,"data":null} + # spot + 'invalid sign': AuthenticationError, + 'invalid currency': BadSymbol, # invalid trading pair + 'invalid symbol': BadSymbol, + 'invalid period': BadRequest, # invalid Kline type + 'invalid user': ExchangeError, + 'invalid amount': InvalidOrder, + 'invalid type': InvalidOrder, # {"status":"error","ts":1595700344504,"err_code":"invalid-parameter","err_msg":"invalid type"} + 'invalid orderId': InvalidOrder, + 'invalid record': ExchangeError, + 'invalid accountId': BadRequest, + 'invalid address': BadRequest, + 'accesskey not None': AuthenticationError, # {"status":"error","ts":1595704360508,"err_code":"invalid-parameter","err_msg":"accesskey not null"} + 'illegal accesskey': AuthenticationError, + 'sign not null': AuthenticationError, + 'req_time is too much difference from server time': InvalidNonce, + 'permissions not right': PermissionDenied, # {"status":"error","ts":1595704490084,"err_code":"invalid-parameter","err_msg":"permissions not right"} + 'illegal sign invalid': AuthenticationError, # {"status":"error","ts":1595684716042,"err_code":"invalid-parameter","err_msg":"illegal sign invalid"} + 'user locked': AccountSuspended, + 'Request Frequency Is Too High': RateLimitExceeded, + 'more than a daily rate of cash': BadRequest, + 'more than the maximum daily withdrawal amount': BadRequest, + 'need to bind email or mobile': ExchangeError, + 'user forbid': PermissionDenied, + 'User Prohibited Cash Withdrawal': PermissionDenied, + 'Cash Withdrawal Is Less Than The Minimum Value': BadRequest, + 'Cash Withdrawal Is More Than The Maximum Value': BadRequest, + 'the account with in 24 hours ban coin': PermissionDenied, + 'order cancel fail': BadRequest, # {"status":"error","ts":1595703343035,"err_code":"bad-request","err_msg":"order cancel fail"} + 'base symbol error': BadSymbol, + 'base date error': ExchangeError, + 'api signature not valid': AuthenticationError, + 'gateway internal error': ExchangeError, + 'audit failed': ExchangeError, + 'order queryorder invalid': BadRequest, + 'market no need price': InvalidOrder, + 'limit need price': InvalidOrder, + 'userid not equal to account_id': ExchangeError, + 'your balance is low': InsufficientFunds, # {"status":"error","ts":1595594160149,"err_code":"invalid-parameter","err_msg":"invalid size, valid range: [1,2000]"} + 'address invalid cointype': ExchangeError, + 'system exception': ExchangeError, # {"status":"error","ts":1595711862763,"err_code":"system exception","err_msg":"system exception"} + '50003': ExchangeError, # No record + '50004': BadSymbol, # The transaction pair is currently not supported or has been suspended + '50006': PermissionDenied, # The account is forbidden to withdraw. If you have any questions, please contact customer service. + '50007': PermissionDenied, # The account is forbidden to withdraw within 24 hours. If you have any questions, please contact customer service. + '50008': RequestTimeout, # network timeout + '50009': RateLimitExceeded, # The operation is too frequent, please try again later + '50010': ExchangeError, # The account is abnormally frozen. If you have any questions, please contact customer service. + '50014': InvalidOrder, # The transaction amount under minimum limits + '50015': InvalidOrder, # The transaction amount exceed maximum limits + '50016': InvalidOrder, # The price can't be higher than the current price + '50017': InvalidOrder, # Price under minimum limits + '50018': InvalidOrder, # The price exceed maximum limits + '50019': InvalidOrder, # The amount under minimum limits + '50020': InsufficientFunds, # Insufficient balance + '50021': InvalidOrder, # Price is under minimum limits + '50026': InvalidOrder, # Market price parameter error + 'invalid order query time': ExchangeError, # start time is greater than end time; or the time interval between start time and end time is greater than 48 hours + 'invalid start time': BadRequest, # start time is a date 30 days ago; or start time is a date in the future + 'invalid end time': BadRequest, # end time is a date 30 days ago; or end time is a date in the future + '20003': ExchangeError, # operation failed, {"status":"error","ts":1595730308979,"err_code":"bad-request","err_msg":"20003"} + '01001': ExchangeError, # order failed, {"status":"fail","err_code":"01001","err_msg":"系统异常,请稍后重试"} + '43111': PermissionDenied, # {"code":"43111","msg":"参数错误 address not in address book","requestTime":1665394201164,"data":null} + }, + 'broad': { + 'invalid size, valid range': ExchangeError, + }, + }, + 'precisionMode': TICK_SIZE, + 'commonCurrencies': { + 'APX': 'AstroPepeX', + 'DEGEN': 'DegenReborn', + 'EVA': 'Evadore', # conflict with EverValue Coin + 'JADE': 'Jade Protocol', + 'OMNI': 'omni', # conflict with Omni Network + 'TONCOIN': 'TON', + }, + 'options': { + 'uta': False, + 'timeDifference': 0, # the difference between system clock and Binance clock + 'adjustForTimeDifference': False, # controls the adjustment logic upon instantiation + 'timeframes': { + 'spot': { + '1m': '1min', + '5m': '5min', + '3m': '3min', + '15m': '15min', + '30m': '30min', + '1h': '1h', + '4h': '4h', + '6h': '6Hutc', + '12h': '12Hutc', + '1d': '1Dutc', + '3d': '3Dutc', + '1w': '1Wutc', + '1M': '1Mutc', + }, + 'swap': { + '1m': '1m', + '3m': '3m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1H', + '2h': '2H', + '4h': '4H', + '6h': '6Hutc', + '12h': '12Hutc', + '1d': '1Dutc', + '3d': '3Dutc', + '1w': '1Wutc', + '1M': '1Mutc', + }, + 'uta': { + '1m': '1m', + '3m': '3m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1H', + '2h': '2H', + '4h': '4H', + '6h': '6H', + '12h': '12H', + '1d': '1D', + }, + }, + 'fetchMarkets': { + 'types': ['spot', 'swap'], # there is future markets but they use the same endpoints + }, + 'defaultType': 'spot', # 'spot', 'swap', 'future' + 'defaultSubType': 'linear', # 'linear', 'inverse' + 'createMarketBuyOrderRequiresPrice': True, + 'broker': 'p4sve', + 'withdraw': { + 'fillResponseFromRequest': True, + }, + 'fetchOHLCV': { + # ### Timeframe settings ### + # after testing, the below values are real ones, because the values provided by API DOCS are wrong + # so, start timestamp should be within these thresholds to be able to call "recent" candles endpoint + 'maxRecentDaysPerTimeframe': { + '1m': 30, + '3m': 30, + '5m': 30, + '15m': 30, + '30m': 30, + '1h': 60, + '2h': 120, + '4h': 240, + '6h': 360, + '12h': 720, + '1d': 1440, + '3d': 1440 * 3, + '1w': 1440 * 7, + '1M': 1440 * 30, + }, + 'spot': { + 'maxLimitPerTimeframe': { + '1d': 300, + '3d': 100, + '1w': 100, + '1M': 100, + }, + 'method': 'publicSpotGetV2SpotMarketCandles', # publicSpotGetV2SpotMarketCandles or publicSpotGetV2SpotMarketHistoryCandles + }, + 'swap': { + 'maxLimitPerTimeframe': { + '4h': 540, + '6h': 360, + '12h': 180, + '1d': 90, + '3d': 30, + '1w': 13, + '1M': 4, + }, + 'method': 'publicMixGetV2MixMarketCandles', # publicMixGetV2MixMarketCandles or publicMixGetV2MixMarketHistoryCandles or publicMixGetV2MixMarketHistoryIndexCandles or publicMixGetV2MixMarketHistoryMarkCandles + }, + }, + 'fetchTrades': { + 'spot': { + 'method': 'publicSpotGetV2SpotMarketFillsHistory', # or publicSpotGetV2SpotMarketFills + }, + 'swap': { + 'method': 'publicMixGetV2MixMarketFillsHistory', # or publicMixGetV2MixMarketFills + }, + }, + 'fetchFundingRate': { + 'method': 'publicMixGetV2MixMarketCurrentFundRate', # or publicMixGetV2MixMarketFundingTime + }, + 'accountsByType': { + 'spot': 'spot', + 'cross': 'crossed_margin', + 'isolated': 'isolated_margin', + 'swap': 'usdt_futures', + 'usdc_swap': 'usdc_futures', + 'future': 'coin_futures', + 'p2p': 'p2p', + }, + 'accountsById': { + 'spot': 'spot', + 'crossed_margin': 'cross', + 'isolated_margin': 'isolated', + 'usdt_futures': 'swap', + 'usdc_futures': 'usdc_swap', + 'coin_futures': 'future', + 'p2p': 'p2p', + }, + 'sandboxMode': False, + 'networks': { + # 'TRX': 'TRX', # different code for mainnet + 'TRC20': 'TRC20', + # 'ETH': 'ETH', # different code for mainnet + 'ERC20': 'ERC20', + 'BEP20': 'BSC', + # 'BEP20': 'BEP20', # different for BEP20 + 'ATOM': 'ATOM', + 'ACA': 'AcalaToken', + 'APT': 'Aptos', + 'ARBONE': 'ArbitrumOne', + 'ARBNOVA': 'ArbitrumNova', + 'AVAXC': 'C-Chain', + 'AVAXX': 'X-Chain', + 'AR': 'Arweave', + 'BCH': 'BCH', + 'BCHA': 'BCHA', + 'BITCI': 'BITCI', + 'BTC': 'BTC', + 'CELO': 'CELO', + 'CSPR': 'CSPR', + 'ADA': 'Cardano', + 'CHZ': 'ChilizChain', + 'CRC20': 'CronosChain', + 'DOGE': 'DOGE', + 'DOT': 'DOT', + 'EOS': 'EOS', + 'ETHF': 'ETHFAIR', + 'ETHW': 'ETHW', + 'ETC': 'ETC', + 'EGLD': 'Elrond', + 'FIL': 'FIL', + 'FIO': 'FIO', + 'FTM': 'Fantom', + 'HRC20': 'HECO', + 'ONE': 'Harmony', + 'HNT': 'Helium', + 'ICP': 'ICP', + 'IOTX': 'IoTeX', + 'KARDIA': 'KAI', + 'KAVA': 'KAVA', + 'KDA': 'KDA', + 'KLAY': 'Klaytn', + 'KSM': 'Kusama', + 'LAT': 'LAT', + 'LTC': 'LTC', + 'MINA': 'MINA', + 'MOVR': 'MOVR', + 'METIS': 'MetisToken', + 'GLMR': 'Moonbeam', + 'NEAR': 'NEARProtocol', + 'NULS': 'NULS', + 'OASYS': 'OASYS', + 'OASIS': 'ROSE', + 'OMNI': 'OMNI', + 'ONT': 'Ontology', + 'OPTIMISM': 'Optimism', + 'OSMO': 'Osmosis', + 'POKT': 'PocketNetwork', + 'MATIC': 'Polygon', + 'QTUM': 'QTUM', + 'REEF': 'REEF', + 'SOL': 'SOL', + 'SYS': 'SYS', # SyscoinNEVM is different + 'SXP': 'Solar', + 'XYM': 'Symbol', + 'TON': 'TON', + 'TT': 'TT', + 'TLOS': 'Telos', + 'THETA': 'ThetaToken', + 'VITE': 'VITE', + 'WAVES': 'WAVES', + 'WAX': 'WAXP', + 'WEMIX': 'WEMIXMainnet', + 'XDC': 'XDCNetworkXDC', + 'XRP': 'XRP', + 'FET': 'FETCH', + 'NEM': 'NEM', + 'REI': 'REINetwork', + 'ZIL': 'ZIL', + 'ABBC': 'ABBCCoin', + 'RSK': 'RSK', + 'AZERO': 'AZERO', + 'TRC10': 'TRC10', + 'JUNO': 'JUNO', + # undetected: USDSP, more info at https://www.bitget.com/v1/spot/public/coinChainList + # todo: uncomment below after unification + # 'TERRACLASSIC': 'Terra', # tbd, that network id is also assigned to TERRANEW network + # 'CUBENETWORK': 'CUBE', + # 'CADUCEUS': 'CMP', + # 'CONFLUX': 'CFX', # CFXeSpace is different + # 'CERE': 'CERE', + # 'CANTO': 'CANTO', + 'ZKSYNC': 'zkSyncEra', + 'STARKNET': 'Starknet', + 'VIC': 'VICTION', + }, + 'networksById': { + }, + 'fetchPositions': { + 'method': 'privateMixGetV2MixPositionAllPosition', # or privateMixGetV2MixPositionHistoryPosition + }, + 'defaultTimeInForce': 'GTC', # 'GTC' = Good To Cancel(default), 'IOC' = Immediate Or Cancel + # fiat currencies on deposit page + 'fiatCurrencies': ['EUR', 'VND', 'PLN', 'CZK', 'HUF', 'DKK', 'AUD', 'CAD', 'NOK', 'SEK', 'CHF', 'MXN', 'COP', 'ARS', 'GBP', 'BRL', 'UAH', 'ZAR'], + }, + 'features': { + 'spot': { + 'sandbox': True, + 'createOrder': { + 'marginMode': True, + 'triggerPrice': True, + 'triggerPriceType': { + 'last': True, + 'mark': True, + 'index': False, # not on spot + }, + 'triggerDirection': False, + 'stopLossPrice': True, # todo: not yet implemented in spot + 'takeProfitPrice': True, # todo: not yet implemented in spot + 'attachedStopLossTakeProfit': { + 'triggerPriceType': { + 'last': False, + 'mark': False, + 'index': False, + }, + 'price': True, + }, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'marketBuyRequiresPrice': True, + 'marketBuyByCost': True, + # exchange-supported features + # 'selfTradePrevention': True, + # 'twap': False, + # 'iceberg': False, + # 'oco': False, + }, + 'createOrders': { + 'max': 50, + }, + 'fetchMyTrades': { + 'marginMode': True, + 'limit': 100, + 'daysBack': None, + 'untilDays': 90, + 'symbolRequired': True, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOpenOrders': { + 'marginMode': True, + 'limit': 100, + 'trigger': True, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': { + 'marginMode': True, + 'limit': 100, + 'daysBack': None, + 'daysBackCanceled': None, + 'untilDays': 90, + 'trigger': True, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 200, # variable timespans for recent endpoint, 200 for historical + }, + }, + 'forPerps': { + 'extends': 'spot', + 'createOrder': { + 'triggerPrice': True, + 'triggerPriceType': { + 'last': True, + 'mark': True, + 'index': False, # not on spot + }, + 'triggerDirection': False, + 'stopLossPrice': True, + 'takeProfitPrice': True, + 'attachedStopLossTakeProfit': { + 'triggerPriceType': { + 'last': True, + 'mark': True, + 'index': True, + }, + 'price': False, + }, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': True, + 'trailing': True, + 'marketBuyRequiresPrice': False, + 'marketBuyByCost': False, + # exchange-supported features + # 'selfTradePrevention': True, + # 'trailing': True, + # 'twap': False, + # 'iceberg': False, + # 'oco': False, + }, + 'fetchMyTrades': { + 'untilDays': 7, + }, + 'fetchClosedOrders': { + 'trailing': True, + }, + }, + 'swap': { + 'linear': { + 'extends': 'forPerps', + }, + 'inverse': { + 'extends': 'forPerps', + }, + }, + 'future': { + 'linear': { + 'extends': 'forPerps', + }, + 'inverse': { + 'extends': 'forPerps', + }, + }, + }, + }) + + def set_sandbox_mode(self, enabled: bool): + """ + enables or disables demo trading mode, if enabled will send PAPTRADING=1 in headers + @param enabled + """ + self.options['sandboxMode'] = enabled + + def enable_demo_trading(self, enabled: bool): + """ + enables or disables demo trading mode, if enabled will send PAPTRADING=1 in headers + @param enabled + """ + self.set_sandbox_mode(enabled) + + def handle_product_type_and_params(self, market=None, params={}): + subType = None + subType, params = self.handle_sub_type_and_params('handleProductTypeAndParams', None, params) + defaultProductType = None + if (subType is not None) and (market is None): + # set default only if subType is defined and market is not defined, since there is also USDC productTypes which are also linear + # sandboxMode = self.safe_bool(self.options, 'sandboxMode', False) + # if sandboxMode: + # defaultProductType = 'SUSDT-FUTURES' if (subType == 'linear') else 'SCOIN-FUTURES' + # else: + defaultProductType = 'USDT-FUTURES' if (subType == 'linear') else 'COIN-FUTURES' + # } + productType = self.safe_string_2(params, 'productType', 'category', defaultProductType) + if (productType is None) and (market is not None): + settle = market['settle'] + if market['spot']: + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('handleProductTypeAndParams', params) + if marginMode is not None: + productType = 'MARGIN' + else: + productType = 'SPOT' + elif settle == 'USDT': + productType = 'USDT-FUTURES' + elif settle == 'USDC': + productType = 'USDC-FUTURES' + elif settle == 'SUSDT': + productType = 'SUSDT-FUTURES' + elif settle == 'SUSDC': + productType = 'SUSDC-FUTURES' + elif (settle == 'SBTC') or (settle == 'SETH') or (settle == 'SEOS'): + productType = 'SCOIN-FUTURES' + else: + productType = 'COIN-FUTURES' + if productType is None: + raise ArgumentsRequired(self.id + ' requires a productType param, one of "USDT-FUTURES", "USDC-FUTURES", "COIN-FUTURES", "SUSDT-FUTURES", "SUSDC-FUTURES", "SCOIN-FUTURES" or for uta only "SPOT"') + params = self.omit(params, ['productType', 'category']) + return [productType, params] + + def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://www.bitget.com/api-doc/common/public/Get-Server-Time + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = self.publicCommonGetV2PublicTime(params) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700111073740, + # "data": { + # "serverTime": "1700111073740" + # } + # } + # + data = self.safe_value(response, 'data', {}) + return self.safe_integer(data, 'serverTime') + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for bitget + + https://www.bitget.com/api-doc/spot/market/Get-Symbols + https://www.bitget.com/api-doc/contract/market/Get-All-Symbols-Contracts + https://www.bitget.com/api-doc/margin/common/support-currencies + https://www.bitget.com/api-doc/uta/public/Instruments + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :returns dict[]: an array of objects representing market data + """ + if self.options['adjustForTimeDifference']: + self.load_time_difference() + uta = None + uta, params = self.handle_option_and_params(params, 'fetchMarkets', 'uta', False) + if uta: + return self.fetch_uta_markets(params) + else: + return self.fetch_default_markets(params) + + def fetch_default_markets(self, params) -> List[Market]: + types = None + fetchMarketsOptions = self.safe_dict(self.options, 'fetchMarkets') + defaultMarkets = ['spot', 'swap'] + if fetchMarketsOptions is not None: + types = self.safe_list(fetchMarketsOptions, 'types', defaultMarkets) + else: + # for backward-compatibility + types = self.safe_list(self.options, 'fetchMarkets', defaultMarkets) + promises = [] + fetchMargins = False + for i in range(0, len(types)): + type = types[i] + if (type == 'swap') or (type == 'future'): + subTypes = ['USDT-FUTURES', 'COIN-FUTURES', 'USDC-FUTURES', 'SUSDT-FUTURES', 'SCOIN-FUTURES', 'SUSDC-FUTURES'] + for j in range(0, len(subTypes)): + promises.append(self.publicMixGetV2MixMarketContracts(self.extend(params, { + 'productType': subTypes[j], + }))) + elif type == 'spot': + promises.append(self.publicSpotGetV2SpotPublicSymbols(params)) + fetchMargins = True + promises.append(self.publicMarginGetV2MarginCurrencies(params)) + else: + raise NotSupported(self.id + ' does not support ' + type + ' market') + results = promises + markets = [] + self.options['crossMarginPairsData'] = [] + self.options['isolatedMarginPairsData'] = [] + for i in range(0, len(results)): + res = self.safe_dict(results, i) + data = self.safe_list(res, 'data', []) + firstData = self.safe_dict(data, 0, {}) + isBorrowable = self.safe_bool(firstData, 'isBorrowable') + if fetchMargins and isBorrowable is not None: + keysList = list(self.index_by(data, 'symbol').keys()) + self.options['crossMarginPairsData'] = keysList + self.options['isolatedMarginPairsData'] = keysList + else: + markets = self.array_concat(markets, data) + # + # spot + # + # { + # "symbol": "TRXUSDT", + # "baseCoin": "TRX", + # "quoteCoin": "USDT", + # "minTradeAmount": "0", + # "maxTradeAmount": "10000000000", + # "takerFeeRate": "0.002", + # "makerFeeRate": "0.002", + # "pricePrecision": "6", + # "quantityPrecision": "4", + # "quotePrecision": "6", + # "status": "online", + # "minTradeUSDT": "5", + # "buyLimitPriceRatio": "0.05", + # "sellLimitPriceRatio": "0.05" + # } + # + # swap and future + # + # { + # "symbol": "BTCUSDT", + # "baseCoin": "BTC", + # "quoteCoin": "USDT", + # "buyLimitPriceRatio": "0.01", + # "sellLimitPriceRatio": "0.01", + # "feeRateUpRatio": "0.005", + # "makerFeeRate": "0.0002", + # "takerFeeRate": "0.0006", + # "openCostUpRatio": "0.01", + # "supportMarginCoins": ["USDT"], + # "minTradeNum": "0.001", + # "priceEndStep": "1", + # "volumePlace": "3", + # "pricePlace": "1", + # "sizeMultiplier": "0.001", + # "symbolType": "perpetual", + # "minTradeUSDT": "5", + # "maxSymbolOrderNum": "200", + # "maxProductOrderNum": "400", + # "maxPositionNum": "150", + # "symbolStatus": "normal", + # "offTime": "-1", + # "limitOpenTime": "-1", + # "deliveryTime": "", + # "deliveryStartTime": "", + # "deliveryPeriod": "", + # "launchTime": "", + # "fundInterval": "8", + # "minLever": "1", + # "maxLever": "125", + # "posLimit": "0.05", + # "maintainTime": "" + # } + # + result = [] + for i in range(0, len(markets)): + market = markets[i] + marketId = self.safe_string(market, 'symbol') + quoteId = self.safe_string(market, 'quoteCoin') + baseId = self.safe_string(market, 'baseCoin') + quote = self.safe_currency_code(quoteId) + base = self.safe_currency_code(baseId) + supportMarginCoins = self.safe_value(market, 'supportMarginCoins', []) + settleId = None + if self.in_array(baseId, supportMarginCoins): + settleId = baseId + elif self.in_array(quoteId, supportMarginCoins): + settleId = quoteId + else: + settleId = self.safe_string(supportMarginCoins, 0) + settle = self.safe_currency_code(settleId) + symbol = base + '/' + quote + type = None + swap = False + spot = False + future = False + contract = False + pricePrecision = None + amountPrecision = None + linear = None + inverse = None + expiry = None + expiryDatetime = None + symbolType = self.safe_string(market, 'symbolType') + marginModes = None + isMarginTradingAllowed = False + if symbolType is None: + type = 'spot' + spot = True + pricePrecision = self.parse_number(self.parse_precision(self.safe_string(market, 'pricePrecision'))) + amountPrecision = self.parse_number(self.parse_precision(self.safe_string(market, 'quantityPrecision'))) + hasCrossMargin = self.in_array(marketId, self.options['crossMarginPairsData']) + hasIsolatedMargin = self.in_array(marketId, self.options['isolatedMarginPairsData']) + marginModes = { + 'cross': hasCrossMargin, + 'isolated': hasIsolatedMargin, + } + isMarginTradingAllowed = hasCrossMargin or hasIsolatedMargin + else: + if symbolType == 'perpetual': + type = 'swap' + swap = True + symbol = symbol + ':' + settle + elif symbolType == 'delivery': + expiry = self.safe_integer(market, 'deliveryTime') + expiryDatetime = self.iso8601(expiry) + expiryParts = expiryDatetime.split('-') + yearPart = self.safe_string(expiryParts, 0) + dayPart = self.safe_string(expiryParts, 2) + year = yearPart[2:4] + month = self.safe_string(expiryParts, 1) + day = dayPart[0:2] + expiryString = year + month + day + type = 'future' + future = True + symbol = symbol + ':' + settle + '-' + expiryString + contract = True + inverse = (base == settle) + linear = not inverse + priceDecimals = self.safe_integer(market, 'pricePlace') + amountDecimals = self.safe_integer(market, 'volumePlace') + priceStep = self.safe_string(market, 'priceEndStep') + amountStep = self.safe_string(market, 'sizeMultiplier') + precise = Precise(priceStep) + precise.decimals = max(precise.decimals, priceDecimals) + precise.reduce() + priceString = str(precise) + pricePrecision = self.parse_number(priceString) + preciseAmount = Precise(amountStep) + preciseAmount.decimals = max(preciseAmount.decimals, amountDecimals) + preciseAmount.reduce() + amountString = str(preciseAmount) + amountPrecision = self.parse_number(amountString) + marginModes = { + 'cross': True, + 'isolated': True, + } + status = self.safe_string_2(market, 'status', 'symbolStatus') + active = None + if status is not None: + active = ((status == 'online') or (status == 'normal')) + minCost = None + if quote == 'USDT': + minCost = self.safe_number(market, 'minTradeUSDT') + contractSize = 1 if contract else None + result.append(self.safe_market_structure({ + 'id': marketId, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': type, + 'spot': spot, + 'margin': spot and isMarginTradingAllowed, + 'marginModes': marginModes, + 'swap': swap, + 'future': future, + 'option': False, + 'active': active, + 'contract': contract, + 'linear': linear, + 'inverse': inverse, + 'taker': self.safe_number(market, 'takerFeeRate'), + 'maker': self.safe_number(market, 'makerFeeRate'), + 'contractSize': contractSize, + 'expiry': expiry, + 'expiryDatetime': expiryDatetime, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': amountPrecision, + 'price': pricePrecision, + }, + 'limits': { + 'leverage': { + 'min': self.safe_number(market, 'minLever'), + 'max': self.safe_number(market, 'maxLever'), + }, + 'amount': { + 'min': self.safe_number_2(market, 'minTradeNum', 'minTradeAmount'), + 'max': self.safe_number(market, 'maxTradeAmount'), + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': minCost, + 'max': None, + }, + }, + 'created': self.safe_integer(market, 'launchTime'), + 'info': market, + })) + return result + + def fetch_uta_markets(self, params) -> List[Market]: + subTypes = ['SPOT', 'USDT-FUTURES', 'COIN-FUTURES', 'USDC-FUTURES'] + promises = [] + for i in range(0, len(subTypes)): + req = self.extend(params, { + 'category': subTypes[i], + }) + promises.append(self.publicUtaGetV3MarketInstruments(req)) + results = promises + markets = [] + for i in range(0, len(results)): + res = self.safe_dict(results, i) + data = self.safe_list(res, 'data', []) + markets = self.array_concat(markets, data) + # + # spot uta + # + # { + # "symbol": "BTCUSDT", + # "category": "SPOT", + # "baseCoin": "BTC", + # "quoteCoin": "USDT", + # "buyLimitPriceRatio": "0.05", + # "sellLimitPriceRatio": "0.05", + # "minOrderQty": "0.000001", + # "maxOrderQty": "0", + # "pricePrecision": "2", + # "quantityPrecision": "6", + # "quotePrecision": "8", + # "minOrderAmount": "1", + # "maxSymbolOrderNum": "400", + # "maxProductOrderNum": "400", + # "status": "online", + # "maintainTime": "" + # } + # + # margin uta + # + # { + # "symbol": "BTCUSDC", + # "category": "MARGIN", + # "baseCoin": "BTC", + # "quoteCoin": "USDC", + # "buyLimitPriceRatio": "0.05", + # "sellLimitPriceRatio": "0.05", + # "minOrderQty": "0.00001", + # "maxOrderQty": "0", + # "pricePrecision": "2", + # "quantityPrecision": "5", + # "quotePrecision": "7", + # "minOrderAmount": "1", + # "maxSymbolOrderNum": "400", + # "maxProductOrderNum": "400", + # "status": "online", + # "maintainTime": "", + # "isIsolatedBaseBorrowable": "NO", + # "isIsolatedQuotedBorrowable": "NO", + # "warningRiskRatio": "0.8", + # "liquidationRiskRatio": "1", + # "maxCrossedLeverage": "3", + # "maxIsolatedLeverage": "0", + # "userMinBorrow": "0.00000001", + # "areaSymbol": "no" + # } + # + # swap and future uta + # + # { + # "symbol": "BTCPERP", + # "category": "USDC-FUTURES", + # "baseCoin": "BTC", + # "quoteCoin": "USDC", + # "buyLimitPriceRatio": "0.02", + # "sellLimitPriceRatio": "0.02", + # "feeRateUpRatio": "0.005", + # "makerFeeRate": "0.0002", + # "takerFeeRate": "0.0006", + # "openCostUpRatio": "0.01", + # "minOrderQty": "0.0001", + # "maxOrderQty": "", + # "pricePrecision": "1", + # "quantityPrecision": "4", + # "quotePrecision": null, + # "priceMultiplier": "0.5", + # "quantityMultiplier": "0.0001", + # "type": "perpetual", + # "minOrderAmount": "5", + # "maxSymbolOrderNum": "200", + # "maxProductOrderNum": "1000", + # "maxPositionNum": "150", + # "status": "online", + # "offTime": "-1", + # "limitOpenTime": "-1", + # "deliveryTime": "", + # "deliveryStartTime": "", + # "deliveryPeriod": "", + # "launchTime": "", + # "fundInterval": "8", + # "minLeverage": "1", + # "maxLeverage": "125", + # "maintainTime": "" + # } + # + result = [] + for i in range(0, len(markets)): + market = markets[i] + category = self.safe_string(market, 'category') + marketId = self.safe_string(market, 'symbol') + quoteId = self.safe_string(market, 'quoteCoin') + baseId = self.safe_string(market, 'baseCoin') + quote = self.safe_currency_code(quoteId) + base = self.safe_currency_code(baseId) + settleId = None + settle = None + if category == 'USDT-FUTURES': + settleId = 'USDT' + elif category == 'USDC-FUTURES': + settleId = 'USDC' + elif category == 'COIN-FUTURES': + settleId = base + if settleId is not None: + settle = self.safe_currency_code(settleId) + symbol = base + '/' + quote + type = None + swap = False + spot = False + future = False + contract = False + pricePrecision = None + amountPrecision = None + linear = None + inverse = None + expiry = None + expiryDatetime = None + symbolType = self.safe_string(market, 'type') + marginModes = None + isMarginTradingAllowed = False + isUtaMargin = (category == 'MARGIN') + if isUtaMargin or (category == 'SPOT'): + type = 'spot' + spot = True + if isUtaMargin: + isolatedBase = self.safe_string(market, 'isIsolatedBaseBorrowable') + isolatedQuote = self.safe_string(market, 'isIsolatedQuotedBorrowable') + isolated = (isolatedBase == 'YES') or (isolatedQuote == 'YES') + maxCrossLeverage = self.safe_string(market, 'maxCrossedLeverage') + cross = (maxCrossLeverage != '0') + marginModes = { + 'cross': cross, + 'isolated': isolated, + } + isMarginTradingAllowed = True + else: + if symbolType == 'perpetual': + type = 'swap' + swap = True + symbol = symbol + ':' + settle + elif symbolType == 'delivery': + expiry = self.safe_integer(market, 'deliveryTime') + expiryDatetime = self.iso8601(expiry) + expiryParts = expiryDatetime.split('-') + yearPart = self.safe_string(expiryParts, 0) + dayPart = self.safe_string(expiryParts, 2) + year = yearPart[2:4] + month = self.safe_string(expiryParts, 1) + day = dayPart[0:2] + expiryString = year + month + day + type = 'future' + future = True + symbol = symbol + ':' + settle + '-' + expiryString + contract = True + inverse = (base == settle) + linear = not inverse + marginModes = { + 'cross': True, + 'isolated': True, + } + pricePrecision = self.parse_number(self.parse_precision(self.safe_string(market, 'pricePrecision'))) + amountPrecision = self.parse_number(self.parse_precision(self.safe_string(market, 'quantityPrecision'))) + status = self.safe_string(market, 'status') + active = None + if status is not None: + active = ((status == 'online') or (status == 'normal')) + contractSize = 1 if contract else None + result.append(self.safe_market_structure({ + 'id': marketId, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': type, + 'spot': spot, + 'margin': spot and isMarginTradingAllowed, + 'marginModes': marginModes, + 'swap': swap, + 'future': future, + 'option': False, + 'active': active, + 'contract': contract, + 'linear': linear, + 'inverse': inverse, + 'taker': self.safe_number(market, 'takerFeeRate'), + 'maker': self.safe_number(market, 'makerFeeRate'), + 'contractSize': contractSize, + 'expiry': expiry, + 'expiryDatetime': expiryDatetime, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': amountPrecision, + 'price': pricePrecision, + }, + 'limits': { + 'leverage': { + 'min': self.safe_number(market, 'minLeverage'), + 'max': self.safe_number(market, 'maxLeverage'), + }, + 'amount': { + 'min': self.safe_number(market, 'minOrderQty'), + 'max': self.safe_number(market, 'maxOrderQty'), + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': self.safe_integer(market, 'launchTime'), + 'info': market, + })) + return result + + def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://www.bitget.com/api-doc/spot/market/Get-Coin-List + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = self.publicSpotGetV2SpotPublicCoins(params) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": "1746195617812", + # "data": [ + # { + # "coinId": "1456", + # "coin": "NEIROETH", + # "transfer": "false", + # "chains": [ + # { + # "chain": "ERC20", + # "needTag": "false", + # "withdrawable": "true", + # "rechargeable": "true", + # "withdrawFee": "44.91017965", + # "extraWithdrawFee": "0", + # "depositConfirm": "12", + # "withdrawConfirm": "64", + # "minDepositAmount": "0.06", + # "minWithdrawAmount": "60", + # "browserUrl": "https://etherscan.io/tx/", + # "contractAddress": "0xee2a03aa6dacf51c18679c516ad5283d8e7c2637", + # "withdrawStep": "0", + # "withdrawMinScale": "8", + # "congestion": "normal" + # } + # ], + # "areaCoin": "no" + # }, + # ... + # + result: dict = {} + data = self.safe_value(response, 'data', []) + fiatCurrencies = self.safe_list(self.options, 'fiatCurrencies', []) + for i in range(0, len(data)): + entry = data[i] + id = self.safe_string(entry, 'coin') # we don't use 'coinId' has no use. it is 'coin' field that needs to be used in currency related endpoints(deposit, withdraw, etc..) + code = self.safe_currency_code(id) + chains = self.safe_value(entry, 'chains', []) + networks: dict = {} + withdraw = None + deposit = None + chainsLength = len(chains) + if chainsLength == 0: + withdraw = False + deposit = False + for j in range(0, chainsLength): + chain = chains[j] + networkId = self.safe_string(chain, 'chain') + network = self.network_id_to_code(networkId, code) + network = network.upper() + withdrawable = (self.safe_string(chain, 'withdrawable') == 'true') + rechargeable = (self.safe_string(chain, 'rechargeable') == 'true') + withdraw = withdrawable if (withdraw is None) else (withdraw or withdrawable) + deposit = rechargeable if (deposit is None) else (deposit or rechargeable) + networks[network] = { + 'info': chain, + 'id': networkId, + 'network': network, + 'limits': { + 'withdraw': { + 'min': self.safe_number(chain, 'minWithdrawAmount'), + 'max': None, + }, + 'deposit': { + 'min': self.safe_number(chain, 'minDepositAmount'), + 'max': None, + }, + }, + 'active': None, + 'withdraw': withdrawable, + 'deposit': rechargeable, + 'fee': self.safe_number(chain, 'withdrawFee'), + 'precision': self.parse_number(self.parse_precision(self.safe_string(chain, 'withdrawMinScale'))), + } + active = withdraw and deposit + isFiat = self.in_array(code, fiatCurrencies) + result[code] = self.safe_currency_structure({ + 'info': entry, + 'id': id, + 'code': code, + 'networks': networks, + 'type': 'fiat' if isFiat else 'crypto', + 'name': None, + 'active': active, + 'deposit': deposit, + 'withdraw': withdraw, + 'fee': None, + 'precision': None, + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + 'deposit': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + }) + return result + + def fetch_market_leverage_tiers(self, symbol: str, params={}) -> List[LeverageTier]: + """ + retrieve information on the maximum leverage, and maintenance margin for trades of varying trade sizes for a single market + + https://www.bitget.com/api-doc/contract/position/Get-Query-Position-Lever + https://www.bitget.com/api-doc/margin/cross/account/Cross-Tier-Data + https://www.bitget.com/api-doc/margin/isolated/account/Isolated-Tier-Data + https://www.bitget.com/api-doc/uta/public/Get-Position-Tier-Data + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: for spot margin 'cross' or 'isolated', default is 'isolated' + :param str [params.code]: required for cross spot margin + :param str [params.productType]: *contract and uta only* 'USDT-FUTURES', 'USDC-FUTURES', 'COIN-FUTURES', 'SUSDT-FUTURES', 'SUSDC-FUTURES' or 'SCOIN-FUTURES' + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :returns dict: a `leverage tiers structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = {} + response = None + marginMode = None + productType = None + uta = None + marginMode, params = self.handle_margin_mode_and_params('fetchMarketLeverageTiers', params, 'isolated') + productType, params = self.handle_product_type_and_params(market, params) + uta, params = self.handle_option_and_params(params, 'fetchMarketLeverageTiers', 'uta', False) + if uta: + if productType == 'SPOT': + if marginMode is not None: + productType = 'MARGIN' + request['symbol'] = market['id'] + request['category'] = productType + response = self.publicUtaGetV3MarketPositionTier(self.extend(request, params)) + elif (market['swap']) or (market['future']): + request['productType'] = productType + request['symbol'] = market['id'] + response = self.publicMixGetV2MixMarketQueryPositionLever(self.extend(request, params)) + elif marginMode == 'isolated': + request['symbol'] = market['id'] + response = self.privateMarginGetV2MarginIsolatedTierData(self.extend(request, params)) + elif marginMode == 'cross': + code = self.safe_string(params, 'code') + if code is None: + raise ArgumentsRequired(self.id + ' fetchMarketLeverageTiers() requires a code argument') + params = self.omit(params, 'code') + currency = self.currency(code) + request['coin'] = currency['id'] + response = self.privateMarginGetV2MarginCrossedTierData(self.extend(request, params)) + else: + raise BadRequest(self.id + ' fetchMarketLeverageTiers() symbol does not support market ' + market['symbol']) + # + # swap and future + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700290724614, + # "data": [ + # { + # "symbol": "BTCUSDT", + # "level": "1", + # "startUnit": "0", + # "endUnit": "150000", + # "leverage": "125", + # "keepMarginRate": "0.004" + # }, + # ] + # } + # + # isolated + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700291531894, + # "data": [ + # { + # "tier": "1", + # "symbol": "BTCUSDT", + # "leverage": "10", + # "baseCoin": "BTC", + # "quoteCoin": "USDT", + # "baseMaxBorrowableAmount": "2", + # "quoteMaxBorrowableAmount": "24000", + # "maintainMarginRate": "0.05", + # "initRate": "0.1111" + # }, + # ] + # } + # + # cross + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700291818831, + # "data": [ + # { + # "tier": "1", + # "leverage": "3", + # "coin": "BTC", + # "maxBorrowableAmount": "26", + # "maintainMarginRate": "0.1" + # } + # ] + # } + # + # uta + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1752735673127, + # "data": [ + # { + # "tier": "1", + # "minTierValue": "0", + # "maxTierValue": "150000", + # "leverage": "125", + # "mmr": "0.004" + # }, + # ] + # } + # + result = self.safe_value(response, 'data', []) + return self.parse_market_leverage_tiers(result, market) + + def parse_market_leverage_tiers(self, info, market: Market = None) -> List[LeverageTier]: + # + # swap and future + # + # { + # "symbol": "BTCUSDT", + # "level": "1", + # "startUnit": "0", + # "endUnit": "150000", + # "leverage": "125", + # "keepMarginRate": "0.004" + # } + # + # isolated + # + # { + # "tier": "1", + # "symbol": "BTCUSDT", + # "leverage": "10", + # "baseCoin": "BTC", + # "quoteCoin": "USDT", + # "baseMaxBorrowableAmount": "2", + # "quoteMaxBorrowableAmount": "24000", + # "maintainMarginRate": "0.05", + # "initRate": "0.1111" + # } + # + # cross + # + # { + # "tier": "1", + # "leverage": "3", + # "coin": "BTC", + # "maxBorrowableAmount": "26", + # "maintainMarginRate": "0.1" + # } + # + # uta + # + # { + # "tier": "1", + # "minTierValue": "0", + # "maxTierValue": "150000", + # "leverage": "125", + # "mmr": "0.004" + # } + # + tiers = [] + minNotional = 0 + for i in range(0, len(info)): + item = info[i] + minimumNotional = self.safe_number_2(item, 'startUnit', 'minTierValue') + if minimumNotional is not None: + minNotional = minimumNotional + maxNotional = self.safe_number_n(item, ['endUnit', 'maxBorrowableAmount', 'baseMaxBorrowableAmount', 'maxTierValue']) + marginCurrency = self.safe_string_2(item, 'coin', 'baseCoin') + currencyId = marginCurrency if (marginCurrency is not None) else market['base'] + marketId = self.safe_string(item, 'symbol') + tiers.append({ + 'tier': self.safe_integer_2(item, 'level', 'tier'), + 'symbol': self.safe_symbol(marketId, market), + 'currency': self.safe_currency_code(currencyId), + 'minNotional': minNotional, + 'maxNotional': maxNotional, + 'maintenanceMarginRate': self.safe_number_n(item, ['keepMarginRate', 'maintainMarginRate', 'mmr']), + 'maxLeverage': self.safe_number(item, 'leverage'), + 'info': item, + }) + minNotional = maxNotional + return tiers + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://www.bitget.com/api-doc/spot/account/Get-Deposit-Record + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: end time in milliseconds + :param str [params.idLessThan]: return records with id less than the provided value + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchDeposits', 'paginate') + if paginate: + return self.fetch_paginated_call_cursor('fetchDeposits', None, since, limit, params, 'idLessThan', 'idLessThan', None, 100) + if since is None: + since = self.milliseconds() - 7776000000 # 90 days + request: dict = { + 'startTime': since, + 'endTime': self.milliseconds(), + } + currency = None + if code is not None: + currency = self.currency(code) + request['coin'] = currency['id'] + if limit is not None: + request['limit'] = limit + request, params = self.handle_until_option('endTime', request, params) + response = self.privateSpotGetV2SpotWalletDepositRecords(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700528340608, + # "data": [ + # { + # "orderId": "1083832260799930368", + # "tradeId": "35bf0e588a42b25c71a9d45abe7308cabdeec6b7b423910b9bd4743d3a9a9efa", + # "coin": "BTC", + # "type": "deposit", + # "size": "0.00030000", + # "status": "success", + # "toAddress": "1BfZh7JESJGBUszCGeZnzxbVVvBycbJSbA", + # "dest": "on_chain", + # "chain": "BTC", + # "fromAddress": null, + # "cTime": "1694131668281", + # "uTime": "1694131680247" + # } + # ] + # } + # + rawTransactions = self.safe_list(response, 'data', []) + return self.parse_transactions(rawTransactions, None, since, limit) + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://www.bitget.com/api-doc/spot/account/Wallet-Withdrawal + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.chain]: the blockchain network the withdrawal is taking place on + :returns dict: a `transaction structure ` + """ + self.check_address(address) + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + if networkCode is None: + raise ArgumentsRequired(self.id + ' withdraw() requires a "network" parameter') + self.load_markets() + currency = self.currency(code) + networkId = self.network_code_to_id(networkCode) + request: dict = { + 'coin': currency['id'], + 'address': address, + 'chain': networkId, + 'size': self.currency_to_precision(code, amount, networkCode), + 'transferType': 'on_chain', + } + if tag is not None: + request['tag'] = tag + response = self.privateSpotPostV2SpotWalletWithdrawal(self.extend(request, params)) + # + # { + # "code":"00000", + # "msg":"success", + # "requestTime":1696784219602, + # "data": { + # "orderId":"1094957867615789056", + # "clientOid":"64f1e4ce842041d296b4517df1b5c2d7" + # } + # } + # + data = self.safe_value(response, 'data', {}) + result = self.parse_transaction(data, currency) + result['type'] = 'withdrawal' + withdrawOptions = self.safe_value(self.options, 'withdraw', {}) + fillResponseFromRequest = self.safe_bool(withdrawOptions, 'fillResponseFromRequest', True) + if fillResponseFromRequest: + result['currency'] = code + result['amount'] = amount + result['tag'] = tag + result['address'] = address + result['addressTo'] = address + result['network'] = networkCode + return result + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://www.bitget.com/api-doc/spot/account/Get-Withdraw-Record + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: end time in milliseconds + :param str [params.idLessThan]: return records with id less than the provided value + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchWithdrawals', 'paginate') + if paginate: + return self.fetch_paginated_call_cursor('fetchWithdrawals', None, since, limit, params, 'idLessThan', 'idLessThan', None, 100) + currency = None + if code is not None: + currency = self.currency(code) + if since is None: + since = self.milliseconds() - 7776000000 # 90 days + request: dict = { + 'startTime': since, + 'endTime': self.milliseconds(), + } + if currency is not None: + request['coin'] = currency['id'] + request, params = self.handle_until_option('endTime', request, params) + if limit is not None: + request['limit'] = limit + response = self.privateSpotGetV2SpotWalletWithdrawalRecords(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700528340608, + # "data": [ + # { + # "orderId": "1083832260799930368", + # "tradeId": "35bf0e588a42b25c71a9d45abe7308cabdeec6b7b423910b9bd4743d3a9a9efa", + # "clientOid": "123", + # "coin": "BTC", + # "type": "withdraw", + # "size": "0.00030000", + # "fee": "-1.0000000", + # "status": "success", + # "toAddress": "1BfZh7JESJGBUszCGeZnzxbVVvBycbJSbA", + # "dest": "on_chain", + # "chain": "BTC", + # "confirm": "100", + # "fromAddress": null, + # "cTime": "1694131668281", + # "uTime": "1694131680247" + # } + # ] + # } + # + rawTransactions = self.safe_list(response, 'data', []) + return self.parse_transactions(rawTransactions, currency, since, limit) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fetchDeposits + # + # { + # "orderId": "1083832260799930368", + # "tradeId": "35bf0e588a42b25c71a9d45abe7308cabdeec6b7b423910b9bd4743d3a9a9efa", + # "coin": "BTC", + # "type": "deposit", + # "size": "0.00030000", + # "status": "success", + # "toAddress": "1BfZh7JESJGBUszCGeZnzxbVVvBycbJSbA", + # "dest": "on_chain", + # "chain": "BTC", + # "fromAddress": null, + # "cTime": "1694131668281", + # "uTime": "1694131680247" + # } + # + # fetchWithdrawals + # + # { + # "orderId": "1083832260799930368", + # "tradeId": "35bf0e588a42b25c71a9d45abe7308cabdeec6b7b423910b9bd4743d3a9a9efa", + # "clientOid": "123", + # "coin": "BTC", + # "type": "withdraw", + # "size": "0.00030000", + # "fee": "-1.0000000", + # "status": "success", + # "toAddress": "1BfZh7JESJGBUszCGeZnzxbVVvBycbJSbA", + # "dest": "on_chain", + # "chain": "BTC", + # "confirm": "100", + # "fromAddress": null, + # "cTime": "1694131668281", + # "uTime": "1694131680247" + # } + # + currencyId = self.safe_string(transaction, 'coin') + code = self.safe_currency_code(currencyId, currency) + timestamp = self.safe_integer(transaction, 'cTime') + networkId = self.safe_string(transaction, 'chain') + status = self.safe_string(transaction, 'status') + tag = self.safe_string(transaction, 'tag') + feeCostString = self.safe_string(transaction, 'fee') + feeCostAbsString = None + if feeCostString is not None: + feeCostAbsString = Precise.string_abs(feeCostString) + fee = None + amountString = self.safe_string(transaction, 'size') + if feeCostAbsString is not None: + fee = {'currency': code, 'cost': self.parse_number(feeCostAbsString)} + amountString = Precise.string_sub(amountString, feeCostAbsString) + return { + 'id': self.safe_string(transaction, 'orderId'), + 'info': transaction, + 'txid': self.safe_string(transaction, 'tradeId'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': self.network_id_to_code(networkId), + 'addressFrom': self.safe_string(transaction, 'fromAddress'), + 'address': self.safe_string(transaction, 'toAddress'), + 'addressTo': self.safe_string(transaction, 'toAddress'), + 'amount': self.parse_number(amountString), + 'type': self.safe_string(transaction, 'type'), + 'currency': code, + 'status': self.parse_transaction_status(status), + 'updated': self.safe_integer(transaction, 'uTime'), + 'tagFrom': None, + 'tag': tag, + 'tagTo': tag, + 'comment': None, + 'internal': None, + 'fee': fee, + } + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'success': 'ok', + 'Pending': 'pending', + 'pending_review': 'pending', + 'pending_review_fail': 'failed', + 'reject': 'failed', + } + return self.safe_string(statuses, status, status) + + def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://www.bitget.com/api-doc/spot/account/Get-Deposit-Address + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + self.load_markets() + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + currency = self.currency(code) + request: dict = { + 'coin': currency['id'], + } + if networkCode is not None: + request['chain'] = self.network_code_to_id(networkCode, code) + response = self.privateSpotGetV2SpotWalletDepositAddress(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700532244807, + # "data": { + # "coin": "BTC", + # "address": "1BfZh7JESJGBUszCGeZnzxbVVvBycbJSbA", + # "chain": "", + # "tag": null, + # "url": "https://blockchair.com/bitcoin/transaction/" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_deposit_address(data, currency) + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + # + # { + # "coin": "BTC", + # "address": "1BfZh7JESJGBUszCGeZnzxbVVvBycbJSbA", + # "chain": "", + # "tag": null, + # "url": "https://blockchair.com/bitcoin/transaction/" + # } + # + currencyId = self.safe_string(depositAddress, 'coin') + networkId = self.safe_string(depositAddress, 'chain') + parsedCurrency = self.safe_currency_code(currencyId, currency) + network = None + if networkId is not None: + network = self.network_id_to_code(networkId, parsedCurrency) + return { + 'info': depositAddress, + 'currency': parsedCurrency, + 'network': network, + 'address': self.safe_string(depositAddress, 'address'), + 'tag': self.safe_string(depositAddress, 'tag'), + } + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://www.bitget.com/api-doc/spot/market/Get-Orderbook + https://www.bitget.com/api-doc/contract/market/Get-Merge-Depth + https://www.bitget.com/api-doc/uta/public/OrderBook + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['limit'] = limit + productType = None + productType, params = self.handle_product_type_and_params(market, params) + response = None + uta = None + uta, params = self.handle_option_and_params(params, 'fetchOrderBook', 'uta', False) + if uta: + request['category'] = productType + response = self.publicUtaGetV3MarketOrderbook(self.extend(request, params)) + elif market['spot']: + response = self.publicSpotGetV2SpotMarketOrderbook(self.extend(request, params)) + else: + request['productType'] = productType + response = self.publicMixGetV2MixMarketMergeDepth(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1645854610294, + # "data": { + # "asks": [["39102", "11.026"]], + # "bids": [['39100.5', "1.773"]], + # "ts": "1645854610294" + # } + # } + # + # uta + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1750329437753, + # "data": { + # "a": [[104992.60, 0.018411]], + # "b":[[104927.40, 0.229914]], + # "ts": "1750329437763" + # } + # } + # + data = self.safe_value(response, 'data', {}) + bidsKey = 'b' if uta else 'bids' + asksKey = 'a' if uta else 'asks' + timestamp = self.safe_integer(data, 'ts') + return self.parse_order_book(data, market['symbol'], timestamp, bidsKey, asksKey) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "symbol": "BTCUSDT", + # "price": "26242", + # "indexPrice": "34867", + # "markPrice": "25555", + # "ts": "1695793390482" + # } + # + # spot + # + # { + # "open": "37202.46", + # "symbol": "BTCUSDT", + # "high24h": "37744.75", + # "low24h": "36666", + # "lastPr": "37583.69", + # "quoteVolume": "519127705.303", + # "baseVolume": "13907.0386", + # "usdtVolume": "519127705.302908", + # "ts": "1700532903261", + # "bidPr": "37583.68", + # "askPr": "37583.69", + # "bidSz": "0.0007", + # "askSz": "0.0829", + # "openUtc": "37449.4", + # "changeUtc24h": "0.00359", + # "change24h": "0.00321" + # } + # + # swap and future + # + # { + # "symbol": "BTCUSDT", + # "lastPr": "104823.8", + # "askPr": "104823.8", + # "bidPr": "104823.5", + # "bidSz": "0.703", + # "askSz": "13.894", + # "high24h": "105289.3", + # "low24h": "103447.9", + # "ts": "1750332210370", + # "change24h": "0.00471", + # "baseVolume": "79089.5675", + # "quoteVolume": "8274870921.80485", + # "usdtVolume": "8274870921.80485", + # "openUtc": "104833", + # "changeUtc24h": "-0.00009", + # "indexPrice": "104881.953125", + # "fundingRate": "-0.000014", + # "holdingAmount": "7452.6421", + # "deliveryStartTime": null, + # "deliveryTime": null, + # "deliveryStatus": "", + # "open24h": "104332.3", + # "markPrice": "104824.2" + # } + # + # spot uta + # + # { + # "category": "SPOT", + # "symbol": "BTCUSDT", + # "ts": "1750330651972", + # "lastPrice": "104900.2", + # "openPrice24h": "104321.2", + # "highPrice24h": "107956.8", + # "lowPrice24h": "103600.1", + # "ask1Price": "104945.8", + # "bid1Price": "104880.6", + # "bid1Size": "0.266534", + # "ask1Size": "0.014001", + # "price24hPcnt": "0.00555", + # "volume24h": "355.941109", + # "turnover24h": "37302936.008134" + # } + # + # swap and future uta + # + # { + # "category": "USDT-FUTURES", + # "symbol": "BTCUSDT", + # "ts": "1750332730472", + # "lastPrice": "104738", + # "openPrice24h": "104374", + # "highPrice24h": "105289.3", + # "lowPrice24h": "103447.9", + # "ask1Price": "104738", + # "bid1Price": "104737.7", + # "bid1Size": "2.036", + # "ask1Size": "8.094", + # "price24hPcnt": "0.00349", + # "volume24h": "79101.6477", + # "turnover24h": "8276293391.45973", + # "indexPrice": "104785.956168", + # "markPrice": "104738", + # "fundingRate": "-0.000007", + # "openInterest": "7465.5938", + # "deliveryStartTime": "", + # "deliveryTime": "", + # "deliveryStatus": "" + # } + # + marketId = self.safe_string(ticker, 'symbol') + close = self.safe_string_2(ticker, 'lastPr', 'lastPrice') + timestamp = self.safe_integer_omit_zero(ticker, 'ts') # exchange bitget provided 0 + change = self.safe_string(ticker, 'change24h') + category = self.safe_string(ticker, 'category') + markPrice = self.safe_string(ticker, 'markPrice') + marketType: str + if (markPrice is not None) and (category != 'SPOT'): + marketType = 'contract' + else: + marketType = 'spot' + percentage = self.safe_string(ticker, 'price24hPcnt') + if percentage is None: + percentage = Precise.string_mul(change, '100') + return self.safe_ticker({ + 'symbol': self.safe_symbol(marketId, market, None, marketType), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string_2(ticker, 'high24h', 'highPrice24h'), + 'low': self.safe_string_2(ticker, 'low24h', 'lowPrice24h'), + 'bid': self.safe_string_2(ticker, 'bidPr', 'bid1Price'), + 'bidVolume': self.safe_string_2(ticker, 'bidSz', 'bid1Size'), + 'ask': self.safe_string_2(ticker, 'askPr', 'ask1Price'), + 'askVolume': self.safe_string_2(ticker, 'askSz', 'ask1Size'), + 'vwap': None, + 'open': self.safe_string_n(ticker, ['open', 'open24h', 'openPrice24h']), + 'close': close, + 'last': close, + 'previousClose': None, + 'change': change, + 'percentage': percentage, + 'average': None, + 'baseVolume': self.safe_string_2(ticker, 'baseVolume', 'volume24h'), + 'quoteVolume': self.safe_string_2(ticker, 'quoteVolume', 'turnover24h'), + 'indexPrice': self.safe_string(ticker, 'indexPrice'), + 'markPrice': markPrice, + 'info': ticker, + }, market) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://www.bitget.com/api-doc/spot/market/Get-Tickers + https://www.bitget.com/api-doc/contract/market/Get-Ticker + https://www.bitget.com/api-doc/uta/public/Tickers + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + productType = None + productType, params = self.handle_product_type_and_params(market, params) + response = None + uta = None + uta, params = self.handle_option_and_params(params, 'fetchTicker', 'uta', False) + if uta: + request['category'] = productType + response = self.publicUtaGetV3MarketTickers(self.extend(request, params)) + elif market['spot']: + response = self.publicSpotGetV2SpotMarketTickers(self.extend(request, params)) + else: + request['productType'] = productType + response = self.publicMixGetV2MixMarketTicker(self.extend(request, params)) + # + # spot + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700532903782, + # "data": [ + # { + # "open": "37202.46", + # "symbol": "BTCUSDT", + # "high24h": "37744.75", + # "low24h": "36666", + # "lastPr": "37583.69", + # "quoteVolume": "519127705.303", + # "baseVolume": "13907.0386", + # "usdtVolume": "519127705.302908", + # "ts": "1700532903261", + # "bidPr": "37583.68", + # "askPr": "37583.69", + # "bidSz": "0.0007", + # "askSz": "0.0829", + # "openUtc": "37449.4", + # "changeUtc24h": "0.00359", + # "change24h": "0.00321" + # } + # ] + # } + # + # swap and future + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1750332210369, + # "data": [ + # { + # "symbol": "BTCUSDT", + # "lastPr": "104823.8", + # "askPr": "104823.8", + # "bidPr": "104823.5", + # "bidSz": "0.703", + # "askSz": "13.894", + # "high24h": "105289.3", + # "low24h": "103447.9", + # "ts": "1750332210370", + # "change24h": "0.00471", + # "baseVolume": "79089.5675", + # "quoteVolume": "8274870921.80485", + # "usdtVolume": "8274870921.80485", + # "openUtc": "104833", + # "changeUtc24h": "-0.00009", + # "indexPrice": "104881.953125", + # "fundingRate": "-0.000014", + # "holdingAmount": "7452.6421", + # "deliveryStartTime": null, + # "deliveryTime": null, + # "deliveryStatus": "", + # "open24h": "104332.3", + # "markPrice": "104824.2" + # } + # ] + # } + # + # spot uta + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1750330653575, + # "data": [ + # { + # "category": "SPOT", + # "symbol": "BTCUSDT", + # "ts": "1750330651972", + # "lastPrice": "104900.2", + # "openPrice24h": "104321.2", + # "highPrice24h": "107956.8", + # "lowPrice24h": "103600.1", + # "ask1Price": "104945.8", + # "bid1Price": "104880.6", + # "bid1Size": "0.266534", + # "ask1Size": "0.014001", + # "price24hPcnt": "0.00555", + # "volume24h": "355.941109", + # "turnover24h": "37302936.008134" + # } + # ] + # } + # + # swap and future uta + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1750332731203, + # "data": [ + # { + # "category": "USDT-FUTURES", + # "symbol": "BTCUSDT", + # "ts": "1750332730472", + # "lastPrice": "104738", + # "openPrice24h": "104374", + # "highPrice24h": "105289.3", + # "lowPrice24h": "103447.9", + # "ask1Price": "104738", + # "bid1Price": "104737.7", + # "bid1Size": "2.036", + # "ask1Size": "8.094", + # "price24hPcnt": "0.00349", + # "volume24h": "79101.6477", + # "turnover24h": "8276293391.45973", + # "indexPrice": "104785.956168", + # "markPrice": "104738", + # "fundingRate": "-0.000007", + # "openInterest": "7465.5938", + # "deliveryStartTime": "", + # "deliveryTime": "", + # "deliveryStatus": "" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_ticker(data[0], market) + + def fetch_mark_price(self, symbol: str, params={}) -> Ticker: + """ + fetches the mark price for a specific market + + https://www.bitget.com/api-doc/contract/market/Get-Symbol-Price + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = None + if market['spot']: + raise NotSupported(self.id + ' fetchMarkPrice() is not supported for spot markets') + else: + productType = None + productType, params = self.handle_product_type_and_params(market, params) + request['productType'] = productType + response = self.publicMixGetV2MixMarketSymbolPrice(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + return self.parse_ticker(data[0], market) + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://www.bitget.com/api-doc/spot/market/Get-Tickers + https://www.bitget.com/api-doc/contract/market/Get-All-Symbol-Ticker + https://www.bitget.com/api-doc/uta/public/Tickers + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :param str [params.subType]: *contract only* 'linear', 'inverse' + :param str [params.productType]: *contract only* 'USDT-FUTURES', 'USDC-FUTURES', 'COIN-FUTURES', 'SUSDT-FUTURES', 'SUSDC-FUTURES' or 'SCOIN-FUTURES' + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + market = None + if symbols is not None: + symbol = self.safe_value(symbols, 0) + market = self.market(symbol) + response = None + request: dict = {} + type = None + type, params = self.handle_market_type_and_params('fetchTickers', market, params) + # Calls like `.fetchTickers(None, {subType:'inverse'})` should be supported for self exchange, so + # as "options.defaultSubType" is also set in exchange options, we should consider `params.subType` + # with higher priority and only default to spot, if `subType` is not set in params + passedSubType = self.safe_string(params, 'subType') + productType = None + productType, params = self.handle_product_type_and_params(market, params) + # only if passedSubType and productType is None, then use spot + uta = None + uta, params = self.handle_option_and_params(params, 'fetchTickers', 'uta', False) + if uta: + symbolsLength = len(symbols) + if (symbols is not None) and (symbolsLength == 1): + request['symbol'] = market['id'] + request['category'] = productType + response = self.publicUtaGetV3MarketTickers(self.extend(request, params)) + elif type == 'spot' and passedSubType is None: + response = self.publicSpotGetV2SpotMarketTickers(self.extend(request, params)) + else: + request['productType'] = productType + response = self.publicMixGetV2MixMarketTickers(self.extend(request, params)) + # + # spot + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700532903782, + # "data": [ + # { + # "open": "37202.46", + # "symbol": "BTCUSDT", + # "high24h": "37744.75", + # "low24h": "36666", + # "lastPr": "37583.69", + # "quoteVolume": "519127705.303", + # "baseVolume": "13907.0386", + # "usdtVolume": "519127705.302908", + # "ts": "1700532903261", + # "bidPr": "37583.68", + # "askPr": "37583.69", + # "bidSz": "0.0007", + # "askSz": "0.0829", + # "openUtc": "37449.4", + # "changeUtc24h": "0.00359", + # "change24h": "0.00321" + # } + # ] + # } + # + # swap and future + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700533773477, + # "data": [ + # { + # "open": "14.9776", + # "symbol": "LINKUSDT", + # "high24h": "15.3942", + # "low24h": "14.3457", + # "lastPr": "14.3748", + # "quoteVolume": "7008612.4299", + # "baseVolume": "469908.8523", + # "usdtVolume": "7008612.42986561", + # "ts": "1700533772309", + # "bidPr": "14.375", + # "askPr": "14.3769", + # "bidSz": "50.004", + # "askSz": "0.7647", + # "openUtc": "14.478", + # "changeUtc24h": "-0.00713", + # "change24h": "-0.04978" + # }, + # ] + # } + # + # spot uta + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1750330653575, + # "data": [ + # { + # "category": "SPOT", + # "symbol": "BTCUSDT", + # "ts": "1750330651972", + # "lastPrice": "104900.2", + # "openPrice24h": "104321.2", + # "highPrice24h": "107956.8", + # "lowPrice24h": "103600.1", + # "ask1Price": "104945.8", + # "bid1Price": "104880.6", + # "bid1Size": "0.266534", + # "ask1Size": "0.014001", + # "price24hPcnt": "0.00555", + # "volume24h": "355.941109", + # "turnover24h": "37302936.008134" + # } + # ] + # } + # + # swap and future uta + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1750332731203, + # "data": [ + # { + # "category": "USDT-FUTURES", + # "symbol": "BTCUSDT", + # "ts": "1750332730472", + # "lastPrice": "104738", + # "openPrice24h": "104374", + # "highPrice24h": "105289.3", + # "lowPrice24h": "103447.9", + # "ask1Price": "104738", + # "bid1Price": "104737.7", + # "bid1Size": "2.036", + # "ask1Size": "8.094", + # "price24hPcnt": "0.00349", + # "volume24h": "79101.6477", + # "turnover24h": "8276293391.45973", + # "indexPrice": "104785.956168", + # "markPrice": "104738", + # "fundingRate": "-0.000007", + # "openInterest": "7465.5938", + # "deliveryStartTime": "", + # "deliveryTime": "", + # "deliveryStatus": "" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_tickers(data, symbols) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # spot, swap and future: fetchTrades + # + # { + # "tradeId": "1075199767891652609", + # "price": "29376.5", + # "size": "6.035", + # "side": "Buy", + # "ts": "1692073521000", + # "symbol": "BTCUSDT" + # } + # + # spot: fetchMyTrades + # + # { + # "userId": "7264631750", + # "symbol": "BTCUSDT", + # "orderId": "1098394344925597696", + # "tradeId": "1098394344974925824", + # "orderType": "market", + # "side": "sell", + # "priceAvg": "28467.68", + # "size": "0.0002", + # "amount": "5.693536", + # "feeDetail": { + # "deduction": "no", + # "feeCoin": "USDT", + # "totalDeductionFee": "", + # "totalFee": "-0.005693536" + # }, + # "tradeScope": "taker", + # "cTime": "1697603539699", + # "uTime": "1697603539754" + # } + # + # spot margin: fetchMyTrades + # + # { + # "orderId": "1099353730455318528", + # "tradeId": "1099353730627092481", + # "orderType": "market", + # "side": "sell", + # "priceAvg": "29543.7", + # "size": "0.0001", + # "amount": "2.95437", + # "tradeScope": "taker", + # "feeDetail": { + # "deduction": "no", + # "feeCoin": "USDT", + # "totalDeductionFee": "0", + # "totalFee": "-0.00295437" + # }, + # "cTime": "1697832275063", + # "uTime": "1697832275150" + # } + # + # swap and future: fetchMyTrades + # + # { + # "tradeId": "1111468664328269825", + # "symbol": "BTCUSDT", + # "orderId": "1111468664264753162", + # "price": "37271.4", + # "baseVolume": "0.001", + # "feeDetail": [ + # { + # "deduction": "no", + # "feeCoin": "USDT", + # "totalDeductionFee": null, + # "totalFee": "-0.02236284" + # } + # ], + # "side": "buy", + # "quoteVolume": "37.2714", + # "profit": "-0.0007", + # "enterPointSource": "web", + # "tradeSide": "close", + # "posMode": "hedge_mode", + # "tradeScope": "taker", + # "cTime": "1700720700342" + # } + # + # uta fetchTrades + # + # { + # "execId": "1319896716324937729", + # "price": "105909.1", + # "size": "6.3090", + # "side": "sell", + # "ts": "1750413820344" + # } + # + # uta fetchMyTrades + # + # { + # "execId": "1322441401010528257", + # "orderId": "1322441400976261120", + # "category": "USDT-FUTURES", + # "symbol": "BTCUSDT", + # "orderType": "market", + # "side": "sell", + # "execPrice": "107005.4", + # "execQty": "0.0001", + # "execValue": "10.7005", + # "tradeScope": "taker", + # "feeDetail": [{ + # "feeCoin": "USDT", + # "fee":"0.00642032" + # }], + # "createdTime": "1751020520451", + # "updatedTime": "1751020520458", + # "execPnl": "0.00017" + # } + # + marketId = self.safe_string(trade, 'symbol') + symbol = self.safe_symbol(marketId, market) + timestamp = self.safe_integer_n(trade, ['cTime', 'ts', 'createdTime']) + fee = None + feeDetail = self.safe_value(trade, 'feeDetail') + posMode = self.safe_string(trade, 'posMode') + category = self.safe_string(trade, 'category') + isFeeStructure = (posMode is not None) or (category is not None) + feeStructure = feeDetail[0] if isFeeStructure else feeDetail + if feeStructure is not None: + currencyCode = self.safe_currency_code(self.safe_string(feeStructure, 'feeCoin')) + fee = { + 'currency': currencyCode, + } + feeCostString = self.safe_string_2(feeStructure, 'totalFee', 'fee') + deduction = self.safe_string(feeStructure, 'deduction') is True if 'yes' else False + if deduction: + fee['cost'] = feeCostString + else: + fee['cost'] = Precise.string_neg(feeCostString) + return self.safe_trade({ + 'info': trade, + 'id': self.safe_string_2(trade, 'tradeId', 'execId'), + 'order': self.safe_string(trade, 'orderId'), + 'symbol': symbol, + 'side': self.safe_string_lower(trade, 'side'), + 'type': self.safe_string(trade, 'orderType'), + 'takerOrMaker': self.safe_string(trade, 'tradeScope'), + 'price': self.safe_string_n(trade, ['priceAvg', 'price', 'execPrice']), + 'amount': self.safe_string_n(trade, ['baseVolume', 'size', 'execQty']), + 'cost': self.safe_string_n(trade, ['quoteVolume', 'amount', 'execValue']), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'fee': fee, + }, market) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://www.bitget.com/api-doc/spot/market/Get-Recent-Trades + https://www.bitget.com/api-doc/spot/market/Get-Market-Trades + https://www.bitget.com/api-doc/contract/market/Get-Recent-Fills + https://www.bitget.com/api-doc/contract/market/Get-Fills-History + https://www.bitget.com/api-doc/uta/public/Fills + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :param int [params.until]: *only applies to publicSpotGetV2SpotMarketFillsHistory and publicMixGetV2MixMarketFillsHistory* the latest time in ms to fetch trades for + :param boolean [params.paginate]: *only applies to publicSpotGetV2SpotMarketFillsHistory and publicMixGetV2MixMarketFillsHistory* default False, when True will automatically paginate by calling self endpoint multiple times + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchTrades', 'paginate') + if paginate: + return self.fetch_paginated_call_cursor('fetchTrades', symbol, since, limit, params, 'idLessThan', 'idLessThan') + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + uta = None + uta, params = self.handle_option_and_params(params, 'fetchTrades', 'uta', False) + if limit is not None: + if uta: + request['limit'] = min(limit, 100) + elif market['contract']: + request['limit'] = min(limit, 1000) + else: + request['limit'] = limit + options = self.safe_value(self.options, 'fetchTrades', {}) + response = None + productType = None + productType, params = self.handle_product_type_and_params(market, params) + if uta: + if productType == 'SPOT': + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchTrades', params) + if marginMode is not None: + productType = 'MARGIN' + request['category'] = productType + response = self.publicUtaGetV3MarketFills(self.extend(request, params)) + elif market['spot']: + spotOptions = self.safe_value(options, 'spot', {}) + defaultSpotMethod = self.safe_string(spotOptions, 'method', 'publicSpotGetV2SpotMarketFillsHistory') + spotMethod = self.safe_string(params, 'method', defaultSpotMethod) + params = self.omit(params, 'method') + if spotMethod == 'publicSpotGetV2SpotMarketFillsHistory': + request, params = self.handle_until_option('endTime', request, params) + if since is not None: + request['startTime'] = since + response = self.publicSpotGetV2SpotMarketFillsHistory(self.extend(request, params)) + elif spotMethod == 'publicSpotGetV2SpotMarketFills': + response = self.publicSpotGetV2SpotMarketFills(self.extend(request, params)) + else: + swapOptions = self.safe_value(options, 'swap', {}) + defaultSwapMethod = self.safe_string(swapOptions, 'method', 'publicMixGetV2MixMarketFillsHistory') + swapMethod = self.safe_string(params, 'method', defaultSwapMethod) + params = self.omit(params, 'method') + request['productType'] = productType + if swapMethod == 'publicMixGetV2MixMarketFillsHistory': + request, params = self.handle_until_option('endTime', request, params) + if since is not None: + request['startTime'] = since + response = self.publicMixGetV2MixMarketFillsHistory(self.extend(request, params)) + elif swapMethod == 'publicMixGetV2MixMarketFills': + response = self.publicMixGetV2MixMarketFills(self.extend(request, params)) + # + # spot + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1692073693562, + # "data": [ + # { + # "symbol": "BTCUSDT_SPBL", + # "tradeId": "1075200479040323585", + # "side": "Sell", + # "price": "29381.54", + # "size": "0.0056", + # "ts": "1692073691000" + # }, + # ] + # } + # + # swap + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1692073522689, + # "data": [ + # { + # "tradeId": "1075199767891652609", + # "price": "29376.5", + # "size": "6.035", + # "side": "Buy", + # "ts": "1692073521000", + # "symbol": "BTCUSDT_UMCBL" + # }, + # ] + # } + # + # uta + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1750413823980, + # "data": [ + # { + # "execId": "1319896716324937729", + # "price": "105909.1", + # "size": "6.3090", + # "side": "sell", + # "ts": "1750413820344" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_trades(data, market, since, limit) + + def fetch_trading_fee(self, symbol: str, params={}) -> TradingFeeInterface: + """ + fetch the trading fees for a market + + https://www.bitget.com/api-doc/common/public/Get-Trade-Rate + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'isolated' or 'cross', for finding the fee rate of spot margin trading pairs + :returns dict: a `fee structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchTradingFee', params) + if market['spot']: + if marginMode is not None: + request['businessType'] = 'margin' + else: + request['businessType'] = 'spot' + else: + request['businessType'] = 'mix' + response = self.privateCommonGetV2CommonTradeRate(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700549524887, + # "data": { + # "makerFeeRate": "0.001", + # "takerFeeRate": "0.001" + # } + # } + # + data = self.safe_value(response, 'data', {}) + return self.parse_trading_fee(data, market) + + def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://www.bitget.com/api-doc/spot/market/Get-Symbols + https://www.bitget.com/api-doc/contract/market/Get-All-Symbols-Contracts + https://www.bitget.com/api-doc/margin/common/support-currencies + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.productType]: *contract only* 'USDT-FUTURES', 'USDC-FUTURES', 'COIN-FUTURES', 'SUSDT-FUTURES', 'SUSDC-FUTURES' or 'SCOIN-FUTURES' + :param boolean [params.margin]: set to True for spot margin + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + self.load_markets() + response = None + marginMode = None + marketType = None + marginMode, params = self.handle_margin_mode_and_params('fetchTradingFees', params) + marketType, params = self.handle_market_type_and_params('fetchTradingFees', None, params) + if marketType == 'spot': + margin = self.safe_bool(params, 'margin', False) + params = self.omit(params, 'margin') + if (marginMode is not None) or margin: + response = self.publicMarginGetV2MarginCurrencies(params) + else: + response = self.publicSpotGetV2SpotPublicSymbols(params) + elif (marketType == 'swap') or (marketType == 'future'): + productType = None + productType, params = self.handle_product_type_and_params(None, params) + params['productType'] = productType + response = self.publicMixGetV2MixMarketContracts(params) + else: + raise NotSupported(self.id + ' does not support ' + marketType + ' market') + # + # spot and margin + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700102364653, + # "data": [ + # { + # "symbol": "TRXUSDT", + # "baseCoin": "TRX", + # "quoteCoin": "USDT", + # "minTradeAmount": "0", + # "maxTradeAmount": "10000000000", + # "takerFeeRate": "0.002", + # "makerFeeRate": "0.002", + # "pricePrecision": "6", + # "quantityPrecision": "4", + # "quotePrecision": "6", + # "status": "online", + # "minTradeUSDT": "5", + # "buyLimitPriceRatio": "0.05", + # "sellLimitPriceRatio": "0.05" + # }, + # ] + # } + # + # swap and future + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700102364709, + # "data": [ + # { + # "symbol": "BTCUSDT", + # "baseCoin": "BTC", + # "quoteCoin": "USDT", + # "buyLimitPriceRatio": "0.01", + # "sellLimitPriceRatio": "0.01", + # "feeRateUpRatio": "0.005", + # "makerFeeRate": "0.0002", + # "takerFeeRate": "0.0006", + # "openCostUpRatio": "0.01", + # "supportMarginCoins": ["USDT"], + # "minTradeNum": "0.001", + # "priceEndStep": "1", + # "volumePlace": "3", + # "pricePlace": "1", + # "sizeMultiplier": "0.001", + # "symbolType": "perpetual", + # "minTradeUSDT": "5", + # "maxSymbolOrderNum": "200", + # "maxProductOrderNum": "400", + # "maxPositionNum": "150", + # "symbolStatus": "normal", + # "offTime": "-1", + # "limitOpenTime": "-1", + # "deliveryTime": "", + # "deliveryStartTime": "", + # "deliveryPeriod": "", + # "launchTime": "", + # "fundInterval": "8", + # "minLever": "1", + # "maxLever": "125", + # "posLimit": "0.05", + # "maintainTime": "" + # }, + # ] + # } + # + data = self.safe_value(response, 'data', []) + result: dict = {} + for i in range(0, len(data)): + entry = data[i] + marketId = self.safe_string(entry, 'symbol') + symbol = self.safe_symbol(marketId, None, None, marketType) + market = self.market(symbol) + fee = self.parse_trading_fee(entry, market) + result[symbol] = fee + return result + + def parse_trading_fee(self, data, market: Market = None): + marketId = self.safe_string(data, 'symbol') + return { + 'info': data, + 'symbol': self.safe_symbol(marketId, market), + 'maker': self.safe_number(data, 'makerFeeRate'), + 'taker': self.safe_number(data, 'takerFeeRate'), + 'percentage': None, + 'tierBased': None, + } + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # [ + # "1645911960000", + # "39406", + # "39407", + # "39374.5", + # "39379", + # "35.526", + # "1399132.341" + # ] + # + inverse = self.safe_bool(market, 'inverse') + volumeIndex = 6 if inverse else 5 + return [ + self.safe_integer(ohlcv, 0), + self.safe_number(ohlcv, 1), + self.safe_number(ohlcv, 2), + self.safe_number(ohlcv, 3), + self.safe_number(ohlcv, 4), + self.safe_number(ohlcv, volumeIndex), + ] + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://www.bitget.com/api-doc/spot/market/Get-Candle-Data + https://www.bitget.com/api-doc/spot/market/Get-History-Candle-Data + https://www.bitget.com/api-doc/contract/market/Get-Candle-Data + https://www.bitget.com/api-doc/contract/market/Get-History-Candle-Data + https://www.bitget.com/api-doc/contract/market/Get-History-Index-Candle-Data + https://www.bitget.com/api-doc/contract/market/Get-History-Mark-Candle-Data + https://www.bitget.com/api-doc/uta/public/Get-Candle-Data + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :param int [params.until]: timestamp in ms of the latest candle to fetch + :param boolean [params.useHistoryEndpoint]: whether to force to use historical endpoint(it has max limit of 200) + :param boolean [params.useHistoryEndpointForPagination]: whether to force to use historical endpoint for pagination(default True) + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param str [params.price]: *swap only* "mark"(to fetch mark price candles) or "index"(to fetch index price candles) + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + defaultLimit = 100 # default 100, max 1000 + maxLimitForRecentEndpoint = 1000 + maxLimitForHistoryEndpoint = 200 # note, max 1000 bars are supported for "recent-candles" endpoint, but "historical-candles" support only max 200 + useHistoryEndpoint = self.safe_bool(params, 'useHistoryEndpoint', False) + useHistoryEndpointForPagination = self.safe_bool(params, 'useHistoryEndpointForPagination', True) + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate') + if paginate: + limitForPagination = maxLimitForHistoryEndpoint if useHistoryEndpointForPagination else maxLimitForRecentEndpoint + return self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params, limitForPagination) + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + marketType = None + timeframes = None + uta = None + uta, params = self.handle_option_and_params(params, 'fetchOHLCV', 'uta', False) + if uta: + timeframes = self.options['timeframes']['uta'] + request['interval'] = self.safe_string(timeframes, timeframe, timeframe) + else: + marketType = 'spot' if market['spot'] else 'swap' + timeframes = self.options['timeframes'][marketType] + request['granularity'] = self.safe_string(timeframes, timeframe, timeframe) + msInDay = 86400000 + now = self.milliseconds() + duration = self.parse_timeframe(timeframe) * 1000 + until = self.safe_integer(params, 'until') + limitDefined = limit is not None + sinceDefined = since is not None + untilDefined = until is not None + params = self.omit(params, ['until']) + # retrievable periods listed here: + # - https://www.bitget.com/api-doc/spot/market/Get-Candle-Data#request-parameters + # - https://www.bitget.com/api-doc/contract/market/Get-Candle-Data#description + key = 'spot' if market['spot'] else 'swap' + ohlcOptions = self.safe_dict(self.options['fetchOHLCV'], key, {}) + maxLimitPerTimeframe = self.safe_dict(ohlcOptions, 'maxLimitPerTimeframe', {}) + maxLimitForThisTimeframe = self.safe_integer(maxLimitPerTimeframe, timeframe, limit) + recentEndpointDaysMap = self.safe_dict(self.options['fetchOHLCV'], 'maxRecentDaysPerTimeframe', {}) + recentEndpointAvailableDays = self.safe_integer(recentEndpointDaysMap, timeframe) + recentEndpointBoundaryTs = now - (recentEndpointAvailableDays - 1) * msInDay + if limitDefined: + limit = min(limit, maxLimitForRecentEndpoint) + limit = min(limit, maxLimitForThisTimeframe) + else: + limit = defaultLimit + limitMultipliedDuration = limit * duration + # exchange aligns from endTime, so it's important, not startTime + # startTime is supported only on "recent" endpoint, not on "historical" endpoint + calculatedStartTime = None + calculatedEndTime = None + if sinceDefined: + calculatedStartTime = since + request['startTime'] = since + if not untilDefined: + calculatedEndTime = self.sum(calculatedStartTime, limitMultipliedDuration) + if calculatedEndTime > now: + calculatedEndTime = now + request['endTime'] = calculatedEndTime + if untilDefined: + calculatedEndTime = until + if calculatedEndTime > now: + calculatedEndTime = now + request['endTime'] = calculatedEndTime + if not sinceDefined: + calculatedStartTime = calculatedEndTime - limitMultipliedDuration + # we do not need to set "startTime" here + # if historical endpoint is needed, we should re-set the variables + historicalEndpointNeeded = False + if (calculatedStartTime is not None and calculatedStartTime <= recentEndpointBoundaryTs) or useHistoryEndpoint: + historicalEndpointNeeded = True + # only for "historical-candles" - ensure we use correct max limit + limit = min(limit, maxLimitForHistoryEndpoint) + limitMultipliedDuration = limit * duration + calculatedStartTime = calculatedEndTime - limitMultipliedDuration + request['startTime'] = calculatedStartTime + # for contract, maximum 90 days allowed between start-end times + if not market['spot']: + maxDistanceDaysForContracts = 90 + # only correct if request is larger + if calculatedEndTime - calculatedStartTime > maxDistanceDaysForContracts * msInDay: + calculatedEndTime = self.sum(calculatedStartTime, maxDistanceDaysForContracts * msInDay) + request['endTime'] = calculatedEndTime + # we need to set limit to safely cover the period + request['limit'] = limit + # make request + response = None + productType = None + priceType = None + priceType, params = self.handle_param_string(params, 'price') + productType, params = self.handle_product_type_and_params(market, params) + if uta: + if priceType is not None: + if priceType == 'mark': + request['type'] = 'MARK' + elif priceType == 'index': + request['type'] = 'INDEX' + request['category'] = productType + response = self.publicUtaGetV3MarketCandles(self.extend(request, params)) + elif market['spot']: + # checks if we need history endpoint + if historicalEndpointNeeded: + response = self.publicSpotGetV2SpotMarketHistoryCandles(self.extend(request, params)) + else: + if not limitDefined: + request['limit'] = 1000 + limit = 1000 + response = self.publicSpotGetV2SpotMarketCandles(self.extend(request, params)) + else: + request['productType'] = productType + extended = self.extend(request, params) + if not historicalEndpointNeeded and (priceType == 'mark' or priceType == 'index'): + if not limitDefined: + extended['limit'] = 1000 + limit = 1000 + # Recent endpoint for mark/index prices + # https://www.bitget.com/api-doc/contract/market/Get-Candle-Data + response = self.publicMixGetV2MixMarketCandles(self.extend({'kLineType': priceType}, extended)) + elif priceType == 'mark': + response = self.publicMixGetV2MixMarketHistoryMarkCandles(extended) + elif priceType == 'index': + response = self.publicMixGetV2MixMarketHistoryIndexCandles(extended) + else: + if historicalEndpointNeeded: + response = self.publicMixGetV2MixMarketHistoryCandles(extended) + else: + if not limitDefined: + extended['limit'] = 1000 + limit = 1000 + response = self.publicMixGetV2MixMarketCandles(extended) + if response == '': + return [] # happens when a new token is listed + # [["1645911960000","39406","39407","39374.5","39379","35.526","1399132.341"]] + data = self.safe_list(response, 'data', response) + return self.parse_ohlcvs(data, market, timeframe, since, limit) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://www.bitget.com/api-doc/spot/account/Get-Account-Assets + https://www.bitget.com/api-doc/contract/account/Get-Account-List + https://www.bitget.com/api-doc/margin/cross/account/Get-Cross-Assets + https://www.bitget.com/api-doc/margin/isolated/account/Get-Isolated-Assets + https://bitgetlimited.github.io/apidoc/en/margin/#get-cross-assets + https://bitgetlimited.github.io/apidoc/en/margin/#get-isolated-assets + https://www.bitget.com/api-doc/uta/account/Get-Account + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.productType]: *contract only* 'USDT-FUTURES', 'USDC-FUTURES', 'COIN-FUTURES', 'SUSDT-FUTURES', 'SUSDC-FUTURES' or 'SCOIN-FUTURES' + :param str [params.uta]: set to True for the unified trading account(uta), defaults to False + :returns dict: a `balance structure ` + """ + self.load_markets() + request: dict = {} + marketType = None + marginMode = None + response = None + uta = None + uta, params = self.handle_option_and_params(params, 'fetchBalance', 'uta', False) + marketType, params = self.handle_market_type_and_params('fetchBalance', None, params) + marginMode, params = self.handle_margin_mode_and_params('fetchBalance', params) + if uta: + response = self.privateUtaGetV3AccountAssets(self.extend(request, params)) + results = self.safe_dict(response, 'data', {}) + assets = self.safe_list(results, 'assets', []) + return self.parse_uta_balance(assets) + elif (marketType == 'swap') or (marketType == 'future'): + productType = None + productType, params = self.handle_product_type_and_params(None, params) + request['productType'] = productType + response = self.privateMixGetV2MixAccountAccounts(self.extend(request, params)) + elif marginMode == 'isolated': + response = self.privateMarginGetV2MarginIsolatedAccountAssets(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": "1759829170717", + # "data": [ + # { + # "symbol": "BTCUSDT", + # "coin": "USDT", + # "totalAmount": "0.000001", + # "available": "0.000001", + # "frozen": "0", + # "borrow": "0", + # "interest": "0", + # "net": "0.000001", + # "coupon": "0", + # "cTime": "1759826434145", + # "uTime": "1759826434146" + # }, + # ] + # } + # + elif marginMode == 'cross': + response = self.privateMarginGetV2MarginCrossedAccountAssets(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": "1759828519501", + # "data": [ + # { + # "coin": "USDT", + # "totalAmount": "0.01", + # "available": "0.01", + # "frozen": "0", + # "borrow": "0", + # "interest": "0", + # "net": "0.01", + # "coupon": "0", + # "cTime": "1759828511592", + # "uTime": "1759828511592" + # } + # ] + # } + # + elif marketType == 'spot': + response = self.privateSpotGetV2SpotAccountAssets(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchBalance() does not support ' + marketType + ' accounts') + # spot + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700623852854, + # "data": [ + # { + # "coin": "USDT", + # "available": "0.00000000", + # "limitAvailable": "0", + # "frozen": "0.00000000", + # "locked": "0.00000000", + # "uTime": "1699937566000" + # } + # ] + # } + # + # swap + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700625127294, + # "data": [ + # { + # "marginCoin": "USDT", + # "locked": "0", + # "available": "0", + # "crossedMaxAvailable": "0", + # "isolatedMaxAvailable": "0", + # "maxTransferOut": "0", + # "accountEquity": "0", + # "usdtEquity": "0.000000005166", + # "btcEquity": "0", + # "crossedRiskRate": "0", + # "unrealizedPL": "0", + # "coupon": "0", + # "crossedUnrealizedPL": null, + # "isolatedUnrealizedPL": null + # } + # ] + # } + # + # uta + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1749980065089, + # "data": { + # "accountEquity": "11.13919278", + # "usdtEquity": "11.13921165", + # "btcEquity": "0.00011256", + # "unrealisedPnl": "0", + # "usdtUnrealisedPnl": "0", + # "btcUnrealizedPnl": "0", + # "effEquity": "6.19299777", + # "mmr": "0", + # "imr": "0", + # "mgnRatio": "0", + # "positionMgnRatio": "0", + # "assets": [ + # { + # "coin": "USDT", + # "equity": "6.19300826", + # "usdValue": "6.19299777", + # "balance": "6.19300826", + # "available": "6.19300826", + # "debt": "0", + # "locked": "0" + # } + # ] + # } + # } + # + data = self.safe_value(response, 'data', []) + return self.parse_balance(data) + + def parse_uta_balance(self, balance) -> Balances: + result: dict = {'info': balance} + # + # { + # "coin": "USDT", + # "equity": "6.19300826", + # "usdValue": "6.19299777", + # "balance": "6.19300826", + # "available": "6.19300826", + # "debt": "0", + # "locked": "0" + # } + # + for i in range(0, len(balance)): + entry = balance[i] + account = self.account() + currencyId = self.safe_string(entry, 'coin') + code = self.safe_currency_code(currencyId) + account['debt'] = self.safe_string(entry, 'debt') + account['used'] = self.safe_string(entry, 'locked') + account['free'] = self.safe_string(entry, 'available') + account['total'] = self.safe_string(entry, 'balance') + result[code] = account + return self.safe_balance(result) + + def parse_balance(self, balance) -> Balances: + result: dict = {'info': balance} + # + # spot + # + # { + # "coin": "USDT", + # "available": "0.00000000", + # "limitAvailable": "0", + # "frozen": "0.00000000", + # "locked": "0.00000000", + # "uTime": "1699937566000" + # } + # + # swap + # + # { + # "marginCoin": "USDT", + # "locked": "0", + # "available": "0", + # "crossedMaxAvailable": "0", + # "isolatedMaxAvailable": "0", + # "maxTransferOut": "0", + # "accountEquity": "0", + # "usdtEquity": "0.000000005166", + # "btcEquity": "0", + # "crossedRiskRate": "0", + # "unrealizedPL": "0", + # "coupon": "0", + # "crossedUnrealizedPL": null, + # "isolatedUnrealizedPL": null + # } + # + # cross & isolated margin + # + # { + # "coin": "USDT", + # "totalAmount": "0.01", + # "available": "0.01", + # "frozen": "0", + # "borrow": "0", + # "interest": "0", + # "net": "0.01", + # "coupon": "0", + # "cTime": "1759828511592", + # "uTime": "1759828511592" + # # "symbol": "BTCUSDT" # only for isolated margin + # } + # + for i in range(0, len(balance)): + entry = balance[i] + account = self.account() + currencyId = self.safe_string_2(entry, 'marginCoin', 'coin') + code = self.safe_currency_code(currencyId) + borrow = self.safe_string(entry, 'borrow') + if borrow is not None: + interest = self.safe_string(entry, 'interest') + account['free'] = self.safe_string(entry, 'transferable') + account['total'] = self.safe_string(entry, 'totalAmount') + account['debt'] = Precise.string_add(borrow, interest) + else: + # Use transferable instead of available for swap and margin https://github.com/ccxt/ccxt/pull/19127 + spotAccountFree = self.safe_string(entry, 'available') + contractAccountFree = self.safe_string(entry, 'maxTransferOut') + if contractAccountFree is not None: + account['free'] = contractAccountFree + account['total'] = self.safe_string(entry, 'accountEquity') + else: + account['free'] = spotAccountFree + frozen = self.safe_string(entry, 'frozen') + locked = self.safe_string(entry, 'locked') + account['used'] = Precise.string_add(frozen, locked) + result[code] = account + return self.safe_balance(result) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'new': 'open', + 'init': 'open', + 'not_trigger': 'open', + 'partial_fill': 'open', + 'partially_fill': 'open', + 'partially_filled': 'open', + 'triggered': 'closed', + 'full_fill': 'closed', + 'filled': 'closed', + 'fail_trigger': 'rejected', + 'cancel': 'canceled', + 'cancelled': 'canceled', + 'canceled': 'canceled', + 'live': 'open', + 'fail_execute': 'rejected', + 'executed': 'closed', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder, editOrder, closePosition + # + # { + # "clientOid": "abe95dbe-6081-4a6f-a2d3-ae49601cd479", + # "orderId": null + # } + # + # createOrders + # + # [ + # { + # "orderId": "1111397214281175046", + # "clientOid": "766d3fc3-7321-4406-a689-15c9987a2e75" + # }, + # { + # "orderId": "", + # "clientOid": "d1b75cb3-cc15-4ede-ad4c-3937396f75ab", + # "errorMsg": "less than the minimum amount 5 USDT", + # "errorCode": "45110" + # }, + # ] + # + # spot, swap, future, spot margin and uta: cancelOrder, cancelOrders, cancelAllOrders + # + # { + # "orderId": "1098758604547850241", + # "clientOid": "1098758604585598977" + # } + # + # spot trigger: cancelOrder + # + # { + # "result": "success" + # } + # + # spot: fetchOrder, fetchOpenOrders, fetchCanceledAndClosedOrders + # + # { + # "userId": "7264631750", + # "symbol": "BTCUSDT", + # "orderId": "1111499608327360513", + # "clientOid": "d0d4dad5-18d0-4869-a074-ec40bb47cba6", + # "size": "0.0002000000000000", # COST for 'buy market' order! AMOUNT in all other cases + # "price": "0", # in fetchOrder: 0 for market order, otherwise limit price(field not present in fetchOpenOrders + # "orderType": "limit", + # "side": "buy", + # "status": "live", + # "basePrice": "0", + # "priceAvg": "25000.0000000000000000", # 0 if nothing filled + # "baseVolume": "0.0000000000000000", # 0 if nothing filled + # "quoteVolume": "0.0000000000000000", # 0 if nothing filled + # "enterPointSource": "WEB", + # "orderSource": "normal", + # "cTime": "1700728077966", + # "uTime": "1700728077966" + # "feeDetail": "{\\"newFees\\":{\\"c\\":0,\\"d\\":0,\\"deduction\\":false,\\"r\\":-0.0064699886,\\"t\\":-0.0064699886,\\"totalDeductionFee\\":0},\\"USDT\\":{\\"deduction\\":false,\\"feeCoinCode\\":\\"USDT\\",\\"totalDeductionFee\\":0,\\"totalFee\\":-0.0064699886000000}}", # might not be present in fetchOpenOrders + # "triggerPrice": null, + # "tpslType": "normal", + # "quoteCoin": "USDT", # not present in fetchOpenOrders + # "baseCoin": "DOT", # not present in fetchOpenOrders + # "cancelReason": "", # not present in fetchOpenOrders + # } + # + # spot trigger: fetchOpenOrders, fetchCanceledAndClosedOrders + # + # { + # "orderId": "1111503385931620352", + # "clientOid": "1111503385910648832", + # "symbol": "BTCUSDT", + # "size": "0.0002", + # "planType": "AMOUNT", + # "executePrice": "25000", + # "triggerPrice": "26000", + # "status": "live", + # "orderType": "limit", + # "side": "buy", + # "triggerType": "fill_price", + # "enterPointSource": "API", + # "cTime": "1700728978617", + # "uTime": "1700728978617" + # } + # + # spot margin: fetchOpenOrders, fetchCanceledAndClosedOrders + # + # { + # "symbol": "BTCUSDT", + # "orderType": "limit", + # "enterPointSource": "WEB", + # "orderId": "1111506377509580801", + # "clientOid": "2043a3b59a60445f9d9f7365bf3e960c", + # "loanType": "autoLoanAndRepay", + # "price": "25000", + # "side": "buy", + # "status": "live", + # "baseSize": "0.0002", + # "quoteSize": "5", + # "priceAvg": "0", + # "size": "0", + # "amount": "0", + # "force": "gtc", + # "cTime": "1700729691866", + # "uTime": "1700729691866" + # } + # + # swap and future: fetchOrder, fetchOpenOrders, fetchCanceledAndClosedOrders + # + # { + # "symbol": "BTCUSDT", + # "size": "0.001", + # "orderId": "1111465253393825792", + # "clientOid": "1111465253431574529", + # "baseVolume": "0", + # "fee": "0", + # "price": "27000", + # "priceAvg": "", + # "state": "live", + # # "status": "live", # key for fetchOpenOrders, fetchClosedOrders + # "side": "buy", + # "force": "gtc", + # "totalProfits": "0", + # "posSide": "long", + # "marginCoin": "USDT", + # "quoteVolume": "0", + # "leverage": "20", + # "marginMode": "crossed", + # "enterPointSource": "API", + # "tradeSide": "open", + # "posMode": "hedge_mode", + # "orderType": "limit", + # "orderSource": "normal", + # "presetStopSurplusPrice": "", + # "presetStopLossPrice": "", + # "reduceOnly": "NO", + # "cTime": "1700719887120", + # "uTime": "1700719887120" + # + # for swap trigger order, the additional below fields are present: + # + # "planType": "normal_plan", + # "callbackRatio": "", + # "triggerPrice": "24000", + # "triggerType": "mark_price", + # "planStatus": "live", + # "stopSurplusTriggerPrice": "", + # "stopSurplusExecutePrice": "", + # "stopSurplusTriggerType": "fill_price", + # "stopLossTriggerPrice": "", + # "stopLossExecutePrice": "", + # "stopLossTriggerType": "fill_price", + # } + # + # uta: fetchOrder, fetchOpenOrders, fetchCanceledAndClosedOrders + # + # { + # "orderId": "1320244799629316096", + # "clientOid": "1320244799633510400", + # "category": "USDT-FUTURES", + # "symbol": "BTCUSDT", + # "orderType": "limit", + # "side": "buy", + # "price": "50000", + # "qty": "0.001", + # "amount": "0", + # "cumExecQty": "0", + # "cumExecValue": "0", + # "avgPrice": "0", + # "timeInForce": "gtc", + # "orderStatus": "live", + # "posSide": "long", + # "holdMode": "hedge_mode", + # "reduceOnly": "NO", + # "feeDetail": [{ + # "feeCoin": "", + # "fee": "" + # }], + # "createdTime": "1750496809871", + # "updatedTime": "1750496809886", + # "cancelReason": "", + # "execType": "normal", + # "stpMode": "none", + # "tpTriggerBy": null, + # "slTriggerBy": null, + # "takeProfit": null, + # "stopLoss": null, + # "tpOrderType": null, + # "slOrderType": null, + # "tpLimitPrice": null, + # "slLimitPrice": null + # } + # + # uta trigger: fetchClosedOrders, fetchCanceledOrders + # + # { + # "orderId": "1330984742276198400", + # "clientOid": "1330984742276198400", + # "symbol": "BTCUSDT", + # "category": "USDT-FUTURES", + # "qty": "0.001", + # "posSide": "long", + # "tpTriggerBy": "market", + # "slTriggerBy": "mark", + # "takeProfit": "", + # "stopLoss": "112000", + # "tpOrderType": "market", + # "slOrderType": "limit", + # "tpLimitPrice": "", + # "slLimitPrice": "111000", + # "createdTime": "1753057411736", + # "updatedTime": "1753058267412" + # } + # + errorMessage = self.safe_string(order, 'errorMsg') + if errorMessage is not None: + return self.safe_order({ + 'info': order, + 'id': self.safe_string(order, 'orderId'), + 'clientOrderId': self.safe_string_2(order, 'clientOrderId', 'clientOid'), + 'status': 'rejected', + }, market) + posSide = self.safe_string(order, 'posSide') + isContractOrder = (posSide is not None) + marketType = 'contract' if isContractOrder else 'spot' + if market is not None: + marketType = market['type'] + marketId = self.safe_string(order, 'symbol') + market = self.safe_market(marketId, market, None, marketType) + timestamp = self.safe_integer_n(order, ['cTime', 'ctime', 'createdTime']) + updateTimestamp = self.safe_integer_2(order, 'uTime', 'updatedTime') + rawStatus = self.safe_string_n(order, ['status', 'state', 'orderStatus', 'planStatus']) + fee = None + feeCostString = self.safe_string(order, 'fee') + if feeCostString is not None: + # swap + fee = { + 'cost': self.parse_number(Precise.string_neg(feeCostString)), + 'currency': market['settle'], + } + feeDetail = self.safe_value(order, 'feeDetail') + uta = self.safe_string(order, 'category') is not None + if uta: + feeResult = self.safe_dict(feeDetail, 0, {}) + utaFee = self.safe_string(feeResult, 'fee') + fee = { + 'cost': self.parse_number(Precise.string_neg(utaFee)), + 'currency': market['settle'], + } + else: + if feeDetail is not None: + parsedFeeDetail = json.loads(feeDetail) + feeValues = list(parsedFeeDetail.values()) + feeObject = None + for i in range(0, len(feeValues)): + feeValue = feeValues[i] + if self.safe_value(feeValue, 'feeCoinCode') is not None: + feeObject = feeValue + break + fee = { + 'cost': self.parse_number(Precise.string_neg(self.safe_string(feeObject, 'totalFee'))), + 'currency': self.safe_currency_code(self.safe_string(feeObject, 'feeCoinCode')), + } + postOnly = None + timeInForce = self.safe_string_upper_2(order, 'force', 'timeInForce') + if timeInForce == 'POST_ONLY': + postOnly = True + timeInForce = 'PO' + reduceOnly = None + reduceOnlyRaw = self.safe_string(order, 'reduceOnly') + if reduceOnlyRaw is not None: + reduceOnly = False if (reduceOnlyRaw == 'NO') else True + price = None + average = None + basePrice = self.safe_string(order, 'basePrice') + if basePrice is not None: + # for spot fetchOpenOrders, the price is priceAvg and the filled price is basePrice + price = self.safe_string(order, 'priceAvg') + average = self.safe_string(order, 'basePrice') + else: + price = self.safe_string_n(order, ['price', 'executePrice', 'slLimitPrice', 'tpLimitPrice']) + average = self.safe_string(order, 'priceAvg') + size = None + filled = None + baseSize = self.safe_string(order, 'baseSize') + if baseSize is not None: + # for spot margin fetchOpenOrders, the order size is baseSize and the filled amount is size + size = baseSize + filled = self.safe_string(order, 'size') + else: + size = self.safe_string_2(order, 'size', 'qty') + filled = self.safe_string_2(order, 'baseVolume', 'cumExecQty') + side = self.safe_string(order, 'side') + posMode = self.safe_string(order, 'posMode') + if posMode == 'hedge_mode' and reduceOnly: + side = 'sell' if (side == 'buy') else 'buy' + # on bitget hedge mode if the position is long the side is always buy, and if the position is short the side is always sell + # so the side of the reduceOnly order is inversed + orderType = self.safe_string(order, 'orderType') + isBuyMarket = (side == 'buy') and (orderType == 'market') + if market['spot'] and isBuyMarket: + # in top comment, for 'buy market' the 'size' field is COST, not AMOUNT + size = self.safe_string(order, 'baseVolume') + return self.safe_order({ + 'info': order, + 'id': self.safe_string_2(order, 'orderId', 'data'), + 'clientOrderId': self.safe_string_2(order, 'clientOrderId', 'clientOid'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': updateTimestamp, + 'lastUpdateTimestamp': updateTimestamp, + 'symbol': market['symbol'], + 'type': orderType, + 'side': side, + 'price': price, + 'amount': size, + 'cost': self.safe_string_2(order, 'quoteVolume', 'quoteSize'), + 'average': average, + 'filled': filled, + 'remaining': None, + 'timeInForce': timeInForce, + 'postOnly': postOnly, + 'reduceOnly': reduceOnly, + 'triggerPrice': self.safe_number(order, 'triggerPrice'), + 'takeProfitPrice': self.safe_number_n(order, ['presetStopSurplusPrice', 'stopSurplusTriggerPrice', 'takeProfit']), + 'stopLossPrice': self.safe_number_n(order, ['presetStopLossPrice', 'stopLossTriggerPrice', 'stopLoss']), + 'status': self.parse_order_status(rawStatus), + 'fee': fee, + 'trades': None, + }, market) + + def create_market_buy_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market buy order by providing the symbol and cost + + https://www.bitget.com/api-doc/spot/trade/Place-Order + https://www.bitget.com/api-doc/margin/cross/trade/Cross-Place-Order + https://www.bitget.com/api-doc/margin/isolated/trade/Isolated-Place-Order + + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' createMarketBuyOrderWithCost() supports spot orders only') + req = { + 'createMarketBuyOrderRequiresPrice': False, + } + return self.create_order(symbol, 'market', 'buy', cost, None, self.extend(req, params)) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://www.bitget.com/api-doc/spot/trade/Place-Order + https://www.bitget.com/api-doc/spot/plan/Place-Plan-Order + https://www.bitget.com/api-doc/contract/trade/Place-Order + https://www.bitget.com/api-doc/contract/plan/Place-Tpsl-Order + https://www.bitget.com/api-doc/contract/plan/Place-Plan-Order + https://www.bitget.com/api-doc/margin/cross/trade/Cross-Place-Order + https://www.bitget.com/api-doc/margin/isolated/trade/Isolated-Place-Order + https://www.bitget.com/api-doc/uta/trade/Place-Order + https://www.bitget.com/api-doc/uta/strategy/Place-Strategy-Order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.cost]: *spot only* how much you want to trade in units of the quote currency, for market buy orders only + :param float [params.triggerPrice]: *swap only* The price at which a trigger order is triggered at + :param float [params.stopLossPrice]: *swap only* The price at which a stop loss order is triggered at + :param float [params.takeProfitPrice]: *swap only* The price at which a take profit order is triggered at + :param dict [params.takeProfit]: *takeProfit object in params* containing the triggerPrice at which the attached take profit order will be triggered(perpetual swap markets only) + :param float [params.takeProfit.triggerPrice]: *swap only* take profit trigger price + :param dict [params.stopLoss]: *stopLoss object in params* containing the triggerPrice at which the attached stop loss order will be triggered(perpetual swap markets only) + :param float [params.stopLoss.triggerPrice]: *swap only* stop loss trigger price + :param str [params.timeInForce]: "GTC", "IOC", "FOK", or "PO" + :param str [params.marginMode]: 'isolated' or 'cross' for spot margin trading + :param str [params.loanType]: *spot margin only* 'normal', 'autoLoan', 'autoRepay', or 'autoLoanAndRepay' default is 'normal' + :param str [params.holdSide]: *contract stopLossPrice, takeProfitPrice only* Two-way position: ('long' or 'short'), one-way position: ('buy' or 'sell') + :param float [params.stopLoss.price]: *swap only* the execution price for a stop loss attached to a trigger order + :param float [params.takeProfit.price]: *swap only* the execution price for a take profit attached to a trigger order + :param str [params.stopLoss.type]: *swap only* the type for a stop loss attached to a trigger order, 'fill_price', 'index_price' or 'mark_price', default is 'mark_price' + :param str [params.takeProfit.type]: *swap only* the type for a take profit attached to a trigger order, 'fill_price', 'index_price' or 'mark_price', default is 'mark_price' + :param str [params.trailingPercent]: *swap and future only* the percent to trail away from the current market price, rate can not be greater than 10 + :param str [params.trailingTriggerPrice]: *swap and future only* the price to trigger a trailing stop order, default uses the price argument + :param str [params.triggerType]: *swap and future only* 'fill_price', 'mark_price' or 'index_price' + :param boolean [params.oneWayMode]: *swap and future only* required to set self to True in one_way_mode and you can leave self in hedge_mode, can adjust the mode using the setPositionMode() method + :param bool [params.hedged]: *swap and future only* True for hedged mode, False for one way mode, default is False + :param bool [params.reduceOnly]: True or False whether the order is reduce-only + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :param str [params.posSide]: *uta only* hedged two-way position side, long or short + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + marginParams = self.handle_margin_mode_and_params('createOrder', params) + marginMode = marginParams[0] + triggerPrice = self.safe_value_2(params, 'stopPrice', 'triggerPrice') + stopLossTriggerPrice = self.safe_value(params, 'stopLossPrice') + takeProfitTriggerPrice = self.safe_value(params, 'takeProfitPrice') + trailingPercent = self.safe_string_2(params, 'trailingPercent', 'callbackRatio') + isTrailingPercentOrder = trailingPercent is not None + isTriggerOrder = triggerPrice is not None + isStopLossTriggerOrder = stopLossTriggerPrice is not None + isTakeProfitTriggerOrder = takeProfitTriggerPrice is not None + isStopLossOrTakeProfitTrigger = isStopLossTriggerOrder or isTakeProfitTriggerOrder + response = None + uta = None + uta, params = self.handle_option_and_params(params, 'createOrder', 'uta', False) + if uta: + request = self.create_uta_order_request(symbol, type, side, amount, price, params) + if isStopLossOrTakeProfitTrigger: + response = self.privateUtaPostV3TradePlaceStrategyOrder(request) + else: + response = self.privateUtaPostV3TradePlaceOrder(request) + else: + request = self.create_order_request(symbol, type, side, amount, price, params) + if market['spot']: + if isTriggerOrder: + response = self.privateSpotPostV2SpotTradePlacePlanOrder(request) + elif marginMode == 'isolated': + response = self.privateMarginPostV2MarginIsolatedPlaceOrder(request) + elif marginMode == 'cross': + response = self.privateMarginPostV2MarginCrossedPlaceOrder(request) + else: + response = self.privateSpotPostV2SpotTradePlaceOrder(request) + else: + if isTriggerOrder or isTrailingPercentOrder: + response = self.privateMixPostV2MixOrderPlacePlanOrder(request) + elif isStopLossOrTakeProfitTrigger: + response = self.privateMixPostV2MixOrderPlaceTpslOrder(request) + else: + response = self.privateMixPostV2MixOrderPlaceOrder(request) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1645932209602, + # "data": { + # "orderId": "881669078313766912", + # "clientOid": "iauIBf#a45b595f96474d888d0ada" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data, market) + + def create_uta_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + market = self.market(symbol) + productType = None + productType, params = self.handle_product_type_and_params(market, params) + if productType == 'SPOT': + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('createOrder', params) + if marginMode is not None: + productType = 'MARGIN' + request: dict = { + 'category': productType, + 'symbol': market['id'], + 'qty': self.amount_to_precision(symbol, amount), + 'side': side, + } + clientOrderId = self.safe_string_2(params, 'clientOid', 'clientOrderId') + if clientOrderId is not None: + request['clientOid'] = clientOrderId + params = self.omit(params, 'clientOrderId') + stopLossTriggerPrice = self.safe_number(params, 'stopLossPrice') + takeProfitTriggerPrice = self.safe_number(params, 'takeProfitPrice') + stopLoss = self.safe_value(params, 'stopLoss') + takeProfit = self.safe_value(params, 'takeProfit') + isStopLoss = stopLoss is not None + isTakeProfit = takeProfit is not None + isStopLossTrigger = stopLossTriggerPrice is not None + isTakeProfitTrigger = takeProfitTriggerPrice is not None + isStopLossOrTakeProfitTrigger = isStopLossTrigger or isTakeProfitTrigger + if isStopLossOrTakeProfitTrigger: + if isStopLossTrigger: + slType = self.safe_string(params, 'slTriggerBy', 'mark') + request['slTriggerBy'] = slType + request['stopLoss'] = self.price_to_precision(symbol, stopLossTriggerPrice) + if price is not None: + request['slLimitPrice'] = self.price_to_precision(symbol, price) + request['slOrderType'] = self.safe_string(params, 'slOrderType', 'limit') + else: + request['slOrderType'] = self.safe_string(params, 'slOrderType', 'market') + elif isTakeProfitTrigger: + tpType = self.safe_string(params, 'tpTriggerBy', 'mark') + request['tpTriggerBy'] = tpType + request['takeProfit'] = self.price_to_precision(symbol, takeProfitTriggerPrice) + if price is not None: + request['tpLimitPrice'] = self.price_to_precision(symbol, price) + request['tpOrderType'] = self.safe_string(params, 'tpOrderType', 'limit') + else: + request['tpOrderType'] = self.safe_string(params, 'tpOrderType', 'market') + params = self.omit(params, ['stopLossPrice', 'takeProfitPrice']) + else: + if isStopLoss: + slTriggerPrice = self.safe_number_2(stopLoss, 'triggerPrice', 'stopPrice') + slLimitPrice = self.safe_number(stopLoss, 'price') + request['stopLoss'] = self.price_to_precision(symbol, slTriggerPrice) + if slLimitPrice is not None: + request['slLimitPrice'] = self.price_to_precision(symbol, slLimitPrice) + request['slOrderType'] = self.safe_string(params, 'slOrderType', 'limit') + else: + request['slOrderType'] = self.safe_string(params, 'slOrderType', 'market') + if isTakeProfit: + tpTriggerPrice = self.safe_number_2(takeProfit, 'triggerPrice', 'stopPrice') + tpLimitPrice = self.safe_number(takeProfit, 'price') + request['takeProfit'] = self.price_to_precision(symbol, tpTriggerPrice) + if tpLimitPrice is not None: + request['tpLimitPrice'] = self.price_to_precision(symbol, tpLimitPrice) + request['tpOrderType'] = self.safe_string(params, 'tpOrderType', 'limit') + else: + request['tpOrderType'] = self.safe_string(params, 'tpOrderType', 'market') + isMarketOrder = type == 'market' + if not isMarketOrder: + request['price'] = self.price_to_precision(symbol, price) + request['orderType'] = type + exchangeSpecificTifParam = self.safe_string(params, 'timeInForce') + postOnly = None + postOnly, params = self.handle_post_only(isMarketOrder, exchangeSpecificTifParam == 'post_only', params) + defaultTimeInForce = self.safe_string_upper(self.options, 'defaultTimeInForce') + timeInForce = self.safe_string_upper(params, 'timeInForce', defaultTimeInForce) + if postOnly: + request['timeInForce'] = 'post_only' + elif timeInForce == 'GTC': + request['timeInForce'] = 'gtc' + elif timeInForce == 'FOK': + request['timeInForce'] = 'fok' + elif timeInForce == 'IOC': + request['timeInForce'] = 'ioc' + reduceOnly = self.safe_bool(params, 'reduceOnly', False) + hedged = None + hedged, params = self.handle_param_bool(params, 'hedged', False) + if reduceOnly: + if hedged or isStopLossOrTakeProfitTrigger: + reduceOnlyPosSide = 'long' if (side == 'sell') else 'short' + request['posSide'] = reduceOnlyPosSide + elif not isStopLossOrTakeProfitTrigger: + request['reduceOnly'] = 'yes' + else: + if hedged: + posSide = 'long' if (side == 'buy') else 'short' + request['posSide'] = posSide + params = self.omit(params, ['stopLoss', 'takeProfit', 'postOnly', 'reduceOnly', 'hedged']) + return self.extend(request, params) + + def create_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + market = self.market(symbol) + marketType = None + marginMode = None + marketType, params = self.handle_market_type_and_params('createOrder', market, params) + marginMode, params = self.handle_margin_mode_and_params('createOrder', params) + request: dict = { + 'symbol': market['id'], + 'orderType': type, + } + hedged = None + hedged, params = self.handle_param_bool(params, 'hedged', False) + # backward compatibility for `oneWayMode` + oneWayMode = None + oneWayMode, params = self.handle_param_bool(params, 'oneWayMode') + if oneWayMode is not None: + hedged = not oneWayMode + isMarketOrder = type == 'market' + triggerPrice = self.safe_value_2(params, 'stopPrice', 'triggerPrice') + stopLossTriggerPrice = self.safe_value(params, 'stopLossPrice') + takeProfitTriggerPrice = self.safe_value(params, 'takeProfitPrice') + stopLoss = self.safe_value(params, 'stopLoss') + takeProfit = self.safe_value(params, 'takeProfit') + isTriggerOrder = triggerPrice is not None + isStopLossTriggerOrder = stopLossTriggerPrice is not None + isTakeProfitTriggerOrder = takeProfitTriggerPrice is not None + isStopLoss = stopLoss is not None + isTakeProfit = takeProfit is not None + isStopLossOrTakeProfitTrigger = isStopLossTriggerOrder or isTakeProfitTriggerOrder + isStopLossOrTakeProfit = isStopLoss or isTakeProfit + trailingTriggerPrice = self.safe_string(params, 'trailingTriggerPrice', self.number_to_string(price)) + trailingPercent = self.safe_string_2(params, 'trailingPercent', 'callbackRatio') + isTrailingPercentOrder = trailingPercent is not None + if self.sum(isTriggerOrder, isStopLossTriggerOrder, isTakeProfitTriggerOrder, isTrailingPercentOrder) > 1: + raise ExchangeError(self.id + ' createOrder() params can only contain one of triggerPrice, stopLossPrice, takeProfitPrice, trailingPercent') + if type == 'limit': + request['price'] = self.price_to_precision(symbol, price) + triggerPriceType = self.safe_string_2(params, 'triggerPriceType', 'triggerType', 'mark_price') + reduceOnly = self.safe_bool(params, 'reduceOnly', False) + clientOrderId = self.safe_string_2(params, 'clientOid', 'clientOrderId') + exchangeSpecificTifParam = self.safe_string_2(params, 'force', 'timeInForce') + postOnly = None + postOnly, params = self.handle_post_only(isMarketOrder, exchangeSpecificTifParam == 'post_only', params) + defaultTimeInForce = self.safe_string_upper(self.options, 'defaultTimeInForce') + timeInForce = self.safe_string_upper(params, 'timeInForce', defaultTimeInForce) + if postOnly: + request['force'] = 'post_only' + elif timeInForce == 'GTC': + request['force'] = 'GTC' + elif timeInForce == 'FOK': + request['force'] = 'FOK' + elif timeInForce == 'IOC': + request['force'] = 'IOC' + params = self.omit(params, ['stopPrice', 'triggerType', 'stopLossPrice', 'takeProfitPrice', 'stopLoss', 'takeProfit', 'postOnly', 'reduceOnly', 'clientOrderId', 'trailingPercent', 'trailingTriggerPrice']) + if (marketType == 'swap') or (marketType == 'future'): + request['marginCoin'] = market['settleId'] + request['size'] = self.amount_to_precision(symbol, amount) + productType = None + productType, params = self.handle_product_type_and_params(market, params) + request['productType'] = productType + if clientOrderId is not None: + request['clientOid'] = clientOrderId + if isTriggerOrder or isStopLossOrTakeProfitTrigger or isTrailingPercentOrder: + request['triggerType'] = triggerPriceType + if isTrailingPercentOrder: + if not isMarketOrder: + raise BadRequest(self.id + ' createOrder() bitget trailing orders must be market orders') + if trailingTriggerPrice is None: + raise ArgumentsRequired(self.id + ' createOrder() bitget trailing orders must have a trailingTriggerPrice param') + request['planType'] = 'track_plan' + request['triggerPrice'] = self.price_to_precision(symbol, trailingTriggerPrice) + request['callbackRatio'] = trailingPercent + elif isTriggerOrder: + request['planType'] = 'normal_plan' + request['triggerPrice'] = self.price_to_precision(symbol, triggerPrice) + if price is not None: + request['executePrice'] = self.price_to_precision(symbol, price) + if isStopLoss: + slTriggerPrice = self.safe_string_2(stopLoss, 'triggerPrice', 'stopPrice') + request['stopLossTriggerPrice'] = self.price_to_precision(symbol, slTriggerPrice) + slPrice = self.safe_string(stopLoss, 'price') + request['stopLossExecutePrice'] = self.price_to_precision(symbol, slPrice) + slType = self.safe_string(stopLoss, 'type', 'mark_price') + request['stopLossTriggerType'] = slType + if isTakeProfit: + tpTriggerPrice = self.safe_string_2(takeProfit, 'triggerPrice', 'stopPrice') + request['stopSurplusTriggerPrice'] = self.price_to_precision(symbol, tpTriggerPrice) + tpPrice = self.safe_string(takeProfit, 'price') + request['stopSurplusExecutePrice'] = self.price_to_precision(symbol, tpPrice) + tpType = self.safe_string(takeProfit, 'type', 'mark_price') + request['stopSurplusTriggerType'] = tpType + elif isStopLossOrTakeProfitTrigger: + if not isMarketOrder: + raise ExchangeError(self.id + ' createOrder() bitget stopLoss or takeProfit orders must be market orders') + if hedged: + request['holdSide'] = 'long' if (side == 'sell') else 'short' + else: + request['holdSide'] = 'buy' if (side == 'sell') else 'sell' + if isStopLossTriggerOrder: + request['triggerPrice'] = self.price_to_precision(symbol, stopLossTriggerPrice) + request['planType'] = 'pos_loss' + elif isTakeProfitTriggerOrder: + request['triggerPrice'] = self.price_to_precision(symbol, takeProfitTriggerPrice) + request['planType'] = 'pos_profit' + else: + if isStopLoss: + slTriggerPrice = self.safe_value_2(stopLoss, 'triggerPrice', 'stopPrice') + request['presetStopLossPrice'] = self.price_to_precision(symbol, slTriggerPrice) + if isTakeProfit: + tpTriggerPrice = self.safe_value_2(takeProfit, 'triggerPrice', 'stopPrice') + request['presetStopSurplusPrice'] = self.price_to_precision(symbol, tpTriggerPrice) + if not isStopLossOrTakeProfitTrigger: + if marginMode is None: + marginMode = 'cross' + marginModeRequest = 'crossed' if (marginMode == 'cross') else 'isolated' + request['marginMode'] = marginModeRequest + requestSide = side + if reduceOnly: + if not hedged: + request['reduceOnly'] = 'YES' + else: + # on bitget hedge mode if the position is long the side is always buy, and if the position is short the side is always sell + requestSide = 'sell' if (side == 'buy') else 'buy' + request['tradeSide'] = 'Close' + else: + if hedged: + request['tradeSide'] = 'Open' + request['side'] = requestSide + elif marketType == 'spot': + if isStopLossOrTakeProfitTrigger or isStopLossOrTakeProfit: + raise InvalidOrder(self.id + ' createOrder() does not support stop loss/take profit orders on spot markets, only swap markets') + request['side'] = side + quantity = None + planType = None + createMarketBuyOrderRequiresPrice = True + createMarketBuyOrderRequiresPrice, params = self.handle_option_and_params(params, 'createOrder', 'createMarketBuyOrderRequiresPrice', True) + if isMarketOrder and (side == 'buy'): + planType = 'total' + cost = self.safe_number(params, 'cost') + params = self.omit(params, 'cost') + if cost is not None: + quantity = self.cost_to_precision(symbol, cost) + elif createMarketBuyOrderRequiresPrice: + if price is None: + raise InvalidOrder(self.id + ' createOrder() requires the price argument for market buy orders to calculate the total cost to spend(amount * price), alternatively set the createMarketBuyOrderRequiresPrice option or param to False and pass the cost to spend in the amount argument') + else: + amountString = self.number_to_string(amount) + priceString = self.number_to_string(price) + quoteAmount = Precise.string_mul(amountString, priceString) + quantity = self.cost_to_precision(symbol, quoteAmount) + else: + quantity = self.cost_to_precision(symbol, amount) + else: + planType = 'amount' + quantity = self.amount_to_precision(symbol, amount) + if clientOrderId is not None: + request['clientOid'] = clientOrderId + if marginMode is not None: + request['loanType'] = 'normal' + if isMarketOrder and (side == 'buy'): + request['quoteSize'] = quantity + else: + request['baseSize'] = quantity + else: + if quantity is not None: + request['size'] = quantity + if triggerPrice is not None: + request['planType'] = planType + request['triggerType'] = triggerPriceType + request['triggerPrice'] = self.price_to_precision(symbol, triggerPrice) + if price is not None: + request['executePrice'] = self.price_to_precision(symbol, price) + else: + raise NotSupported(self.id + ' createOrder() does not support ' + marketType + ' orders') + return self.extend(request, params) + + def create_uta_orders(self, orders: List[OrderRequest], params={}): + self.load_markets() + ordersRequests = [] + symbol = None + marginMode = None + for i in range(0, len(orders)): + rawOrder = orders[i] + marketId = self.safe_string(rawOrder, 'symbol') + if symbol is None: + symbol = marketId + else: + if symbol != marketId: + raise BadRequest(self.id + ' createOrders() requires all orders to have the same symbol') + type = self.safe_string(rawOrder, 'type') + side = self.safe_string(rawOrder, 'side') + amount = self.safe_value(rawOrder, 'amount') + price = self.safe_value(rawOrder, 'price') + orderParams = self.safe_value(rawOrder, 'params', {}) + marginResult = self.handle_margin_mode_and_params('createOrders', orderParams) + currentMarginMode = marginResult[0] + if currentMarginMode is not None: + if marginMode is None: + marginMode = currentMarginMode + else: + if marginMode != currentMarginMode: + raise BadRequest(self.id + ' createOrders() requires all orders to have the same margin mode(isolated or cross)') + orderRequest = self.create_uta_order_request(marketId, type, side, amount, price, orderParams) + ordersRequests.append(orderRequest) + market = self.market(symbol) + response = self.privateUtaPostV3TradePlaceBatch(ordersRequests) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1752810184560, + # "data": [ + # { + # "orderId": "1329947796441513984", + # "clientOid": "1329947796483457024" + # }, + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_orders(data, market) + + def create_orders(self, orders: List[OrderRequest], params={}): + """ + create a list of trade orders(all orders should be of the same symbol) + + https://www.bitget.com/api-doc/spot/trade/Batch-Place-Orders + https://www.bitget.com/api-doc/contract/trade/Batch-Order + https://www.bitget.com/api-doc/margin/isolated/trade/Isolated-Batch-Order + https://www.bitget.com/api-doc/margin/cross/trade/Cross-Batch-Order + https://www.bitget.com/api-doc/uta/trade/Place-Batch + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the api endpoint + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :returns dict: an `order structure ` + """ + self.load_markets() + uta = None + uta, params = self.handle_option_and_params(params, 'createOrders', 'uta', False) + if uta: + return self.create_uta_orders(orders, params) + ordersRequests = [] + symbol = None + marginMode = None + for i in range(0, len(orders)): + rawOrder = orders[i] + marketId = self.safe_string(rawOrder, 'symbol') + if symbol is None: + symbol = marketId + else: + if symbol != marketId: + raise BadRequest(self.id + ' createOrders() requires all orders to have the same symbol') + type = self.safe_string(rawOrder, 'type') + side = self.safe_string(rawOrder, 'side') + amount = self.safe_value(rawOrder, 'amount') + price = self.safe_value(rawOrder, 'price') + orderParams = self.safe_value(rawOrder, 'params', {}) + marginResult = self.handle_margin_mode_and_params('createOrders', orderParams) + currentMarginMode = marginResult[0] + if currentMarginMode is not None: + if marginMode is None: + marginMode = currentMarginMode + else: + if marginMode != currentMarginMode: + raise BadRequest(self.id + ' createOrders() requires all orders to have the same margin mode(isolated or cross)') + orderRequest = self.create_order_request(marketId, type, side, amount, price, orderParams) + ordersRequests.append(orderRequest) + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'orderList': ordersRequests, + } + response = None + if (market['swap']) or (market['future']): + if marginMode is None: + marginMode = 'cross' + marginModeRequest = 'crossed' if (marginMode == 'cross') else 'isolated' + request['marginMode'] = marginModeRequest + request['marginCoin'] = market['settleId'] + productType = None + productType, params = self.handle_product_type_and_params(market, params) + request['productType'] = productType + response = self.privateMixPostV2MixOrderBatchPlaceOrder(request) + elif marginMode == 'isolated': + response = self.privateMarginPostV2MarginIsolatedBatchPlaceOrder(request) + elif marginMode == 'cross': + response = self.privateMarginPostV2MarginCrossedBatchPlaceOrder(request) + else: + response = self.privateSpotPostV2SpotTradeBatchOrders(request) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700703539416, + # "data": { + # "successList": [ + # { + # "orderId": "1111397214281175046", + # "clientOid": "766d3fc3-7321-4406-a689-15c9987a2e75" + # }, + # ], + # "failureList": [ + # { + # "orderId": "", + # "clientOid": "d1b75cb3-cc15-4ede-ad4c-3937396f75ab", + # "errorMsg": "less than the minimum amount 5 USDT", + # "errorCode": "45110" + # }, + # ] + # } + # } + # + data = self.safe_value(response, 'data', {}) + failure = self.safe_value(data, 'failureList', []) + orderInfo = self.safe_value(data, 'successList', []) + both = self.array_concat(orderInfo, failure) + return self.parse_orders(both, market) + + def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + """ + edit a trade order + + https://www.bitget.com/api-doc/spot/plan/Modify-Plan-Order + https://www.bitget.com/api-doc/spot/trade/Cancel-Replace-Order + https://www.bitget.com/api-doc/contract/trade/Modify-Order + https://www.bitget.com/api-doc/contract/plan/Modify-Tpsl-Order + https://www.bitget.com/api-doc/contract/plan/Modify-Plan-Order + https://www.bitget.com/api-doc/uta/trade/Modify-Order + https://www.bitget.com/api-doc/uta/strategy/Modify-Strategy-Order + + :param str id: cancel order id + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: the price that a trigger order is triggered at + :param float [params.stopLossPrice]: *swap only* The price at which a stop loss order is triggered at + :param float [params.takeProfitPrice]: *swap only* The price at which a take profit order is triggered at + :param dict [params.takeProfit]: *takeProfit object in params* containing the triggerPrice at which the attached take profit order will be triggered(perpetual swap markets only) + :param float [params.takeProfit.triggerPrice]: *swap only* take profit trigger price + :param dict [params.stopLoss]: *stopLoss object in params* containing the triggerPrice at which the attached stop loss order will be triggered(perpetual swap markets only) + :param float [params.stopLoss.triggerPrice]: *swap only* stop loss trigger price + :param float [params.stopLoss.price]: *swap only* the execution price for a stop loss attached to a trigger order + :param float [params.takeProfit.price]: *swap only* the execution price for a take profit attached to a trigger order + :param str [params.stopLoss.type]: *swap only* the type for a stop loss attached to a trigger order, 'fill_price', 'index_price' or 'mark_price', default is 'mark_price' + :param str [params.takeProfit.type]: *swap only* the type for a take profit attached to a trigger order, 'fill_price', 'index_price' or 'mark_price', default is 'mark_price' + :param str [params.trailingPercent]: *swap and future only* the percent to trail away from the current market price, rate can not be greater than 10 + :param str [params.trailingTriggerPrice]: *swap and future only* the price to trigger a trailing stop order, default uses the price argument + :param str [params.newTriggerType]: *swap and future only* 'fill_price', 'mark_price' or 'index_price' + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + # 'orderId': id, + } + clientOrderId = self.safe_string_2(params, 'clientOrderId', 'clientOid') + if clientOrderId is not None: + params = self.omit(params, ['clientOrderId']) + request['clientOid'] = clientOrderId + else: + request['orderId'] = id + isMarketOrder = type == 'market' + triggerPrice = self.safe_value_2(params, 'stopPrice', 'triggerPrice') + isTriggerOrder = triggerPrice is not None + stopLossPrice = self.safe_value(params, 'stopLossPrice') + isStopLossOrder = stopLossPrice is not None + takeProfitPrice = self.safe_value(params, 'takeProfitPrice') + isTakeProfitOrder = takeProfitPrice is not None + stopLoss = self.safe_value(params, 'stopLoss') + takeProfit = self.safe_value(params, 'takeProfit') + isStopLoss = stopLoss is not None + isTakeProfit = takeProfit is not None + trailingTriggerPrice = self.safe_string(params, 'trailingTriggerPrice', self.number_to_string(price)) + trailingPercent = self.safe_string_2(params, 'trailingPercent', 'newCallbackRatio') + isTrailingPercentOrder = trailingPercent is not None + if self.sum(isTriggerOrder, isStopLossOrder, isTakeProfitOrder, isTrailingPercentOrder) > 1: + raise ExchangeError(self.id + ' editOrder() params can only contain one of triggerPrice, stopLossPrice, takeProfitPrice, trailingPercent') + params = self.omit(params, ['stopPrice', 'triggerType', 'stopLossPrice', 'takeProfitPrice', 'stopLoss', 'takeProfit', 'clientOrderId', 'trailingTriggerPrice', 'trailingPercent']) + response = None + productType = None + uta = None + productType, params = self.handle_product_type_and_params(market, params) + uta, params = self.handle_option_and_params(params, 'editOrder', 'uta', False) + if uta: + if amount is not None: + request['qty'] = self.amount_to_precision(symbol, amount) + if isStopLossOrder or isTakeProfitOrder: + if isStopLossOrder: + slType = self.safe_string(params, 'slTriggerBy', 'mark') + request['slTriggerBy'] = slType + request['stopLoss'] = self.price_to_precision(symbol, stopLossPrice) + if price is not None: + request['slLimitPrice'] = self.price_to_precision(symbol, price) + request['slOrderType'] = self.safe_string(params, 'slOrderType', 'limit') + else: + request['slOrderType'] = self.safe_string(params, 'slOrderType', 'market') + elif isTakeProfitOrder: + tpType = self.safe_string(params, 'tpTriggerBy', 'mark') + request['tpTriggerBy'] = tpType + request['takeProfit'] = self.price_to_precision(symbol, takeProfitPrice) + if price is not None: + request['tpLimitPrice'] = self.price_to_precision(symbol, price) + request['tpOrderType'] = self.safe_string(params, 'tpOrderType', 'limit') + else: + request['tpOrderType'] = self.safe_string(params, 'tpOrderType', 'market') + params = self.omit(params, ['stopLossPrice', 'takeProfitPrice']) + response = self.privateUtaPostV3TradeModifyStrategyOrder(self.extend(request, params)) + else: + if price is not None: + request['price'] = self.price_to_precision(symbol, price) + response = self.privateUtaPostV3TradeModifyOrder(self.extend(request, params)) + elif market['spot']: + cost = self.safe_string(params, 'cost') + params = self.omit(params, 'cost') + editMarketBuyOrderRequiresPrice = self.safe_bool(self.options, 'editMarketBuyOrderRequiresPrice', True) + if (editMarketBuyOrderRequiresPrice or (cost is not None)) and isMarketOrder and (side == 'buy'): + if price is None and cost is None: + raise InvalidOrder(self.id + ' editOrder() requires price argument for market buy orders on spot markets to calculate the total amount to spend(amount * price), alternatively provide `cost` in the params') + else: + amountString = self.number_to_string(amount) + priceString = self.number_to_string(price) + finalCost = (Precise.string_mul(amountString, priceString)) if (cost is None) else cost + request['size'] = self.price_to_precision(symbol, finalCost) + else: + request['size'] = self.amount_to_precision(symbol, amount) + request['orderType'] = type + if triggerPrice is not None: + request['triggerPrice'] = self.price_to_precision(symbol, triggerPrice) + request['executePrice'] = self.price_to_precision(symbol, price) + else: + request['price'] = self.price_to_precision(symbol, price) + if triggerPrice is not None: + response = self.privateSpotPostV2SpotTradeModifyPlanOrder(self.extend(request, params)) + else: + request['symbol'] = market['id'] + response = self.privateSpotPostV2SpotTradeCancelReplaceOrder(self.extend(request, params)) + else: + if (not market['swap']) and (not market['future']): + raise NotSupported(self.id + ' editOrder() does not support ' + market['type'] + ' orders') + request['symbol'] = market['id'] + request['productType'] = productType + if not isTakeProfitOrder and not isStopLossOrder: + request['newSize'] = self.amount_to_precision(symbol, amount) + if (price is not None) and not isTrailingPercentOrder: + request['newPrice'] = self.price_to_precision(symbol, price) + if isTrailingPercentOrder: + if not isMarketOrder: + raise BadRequest(self.id + ' editOrder() bitget trailing orders must be market orders') + if trailingTriggerPrice is not None: + request['newTriggerPrice'] = self.price_to_precision(symbol, trailingTriggerPrice) + request['newCallbackRatio'] = trailingPercent + response = self.privateMixPostV2MixOrderModifyPlanOrder(self.extend(request, params)) + elif isTakeProfitOrder or isStopLossOrder: + request['marginCoin'] = market['settleId'] + request['size'] = self.amount_to_precision(symbol, amount) + if price is not None: + request['executePrice'] = self.price_to_precision(symbol, price) + if isStopLossOrder: + request['triggerPrice'] = self.price_to_precision(symbol, stopLossPrice) + elif isTakeProfitOrder: + request['triggerPrice'] = self.price_to_precision(symbol, takeProfitPrice) + response = self.privateMixPostV2MixOrderModifyTpslOrder(self.extend(request, params)) + elif isTriggerOrder: + request['newTriggerPrice'] = self.price_to_precision(symbol, triggerPrice) + if isStopLoss: + slTriggerPrice = self.safe_number_2(stopLoss, 'triggerPrice', 'stopPrice') + request['newStopLossTriggerPrice'] = self.price_to_precision(symbol, slTriggerPrice) + slPrice = self.safe_number(stopLoss, 'price') + request['newStopLossExecutePrice'] = self.price_to_precision(symbol, slPrice) + slType = self.safe_string(stopLoss, 'type', 'mark_price') + request['newStopLossTriggerType'] = slType + if isTakeProfit: + tpTriggerPrice = self.safe_number_2(takeProfit, 'triggerPrice', 'stopPrice') + request['newSurplusTriggerPrice'] = self.price_to_precision(symbol, tpTriggerPrice) + tpPrice = self.safe_number(takeProfit, 'price') + request['newStopSurplusExecutePrice'] = self.price_to_precision(symbol, tpPrice) + tpType = self.safe_string(takeProfit, 'type', 'mark_price') + request['newStopSurplusTriggerType'] = tpType + response = self.privateMixPostV2MixOrderModifyPlanOrder(self.extend(request, params)) + else: + defaultNewClientOrderId = self.uuid() + newClientOrderId = self.safe_string_2(params, 'newClientOid', 'newClientOrderId', defaultNewClientOrderId) + params = self.omit(params, 'newClientOrderId') + request['newClientOid'] = newClientOrderId + if isStopLoss: + slTriggerPrice = self.safe_value_2(stopLoss, 'triggerPrice', 'stopPrice') + request['newPresetStopLossPrice'] = self.price_to_precision(symbol, slTriggerPrice) + if isTakeProfit: + tpTriggerPrice = self.safe_value_2(takeProfit, 'triggerPrice', 'stopPrice') + request['newPresetStopSurplusPrice'] = self.price_to_precision(symbol, tpTriggerPrice) + response = self.privateMixPostV2MixOrderModifyOrder(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700708275737, + # "data": { + # "clientOid": "abe95dbe-6081-4a6f-a2d3-ae49601cd459", + # "orderId": null + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data, market) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://www.bitget.com/api-doc/spot/trade/Cancel-Order + https://www.bitget.com/api-doc/spot/plan/Cancel-Plan-Order + https://www.bitget.com/api-doc/contract/trade/Cancel-Order + https://www.bitget.com/api-doc/contract/plan/Cancel-Plan-Order + https://www.bitget.com/api-doc/margin/cross/trade/Cross-Cancel-Order + https://www.bitget.com/api-doc/margin/isolated/trade/Isolated-Cancel-Order + https://www.bitget.com/api-doc/uta/trade/Cancel-Order + https://www.bitget.com/api-doc/uta/strategy/Cancel-Strategy-Order + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'isolated' or 'cross' for spot margin trading + :param boolean [params.trigger]: set to True for canceling trigger orders + :param str [params.planType]: *swap only* either profit_plan, loss_plan, normal_plan, pos_profit, pos_loss, moving_plan or track_plan + :param boolean [params.trailing]: set to True if you want to cancel a trailing order + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :param str [params.clientOrderId]: the clientOrderId of the order, id does not need to be provided if clientOrderId is provided + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + marginMode = None + response = None + marginMode, params = self.handle_margin_mode_and_params('cancelOrder', params) + request: dict = {} + trailing = self.safe_value(params, 'trailing') + trigger = self.safe_value_2(params, 'stop', 'trigger') + params = self.omit(params, ['stop', 'trigger', 'trailing']) + if not (market['spot'] and trigger): + request['symbol'] = market['id'] + uta = None + uta, params = self.handle_option_and_params(params, 'cancelOrder', 'uta', False) + isPlanOrder = trigger or trailing + isContract = market['swap'] or market['future'] + isContractTriggerEndpoint = isContract and isPlanOrder and not uta + clientOrderId = self.safe_string_2(params, 'clientOrderId', 'clientOid') + if isContractTriggerEndpoint: + orderIdList = [] + orderId: dict = {} + if clientOrderId is not None: + params = self.omit(params, 'clientOrderId') + orderId['clientOid'] = clientOrderId + else: + orderId['orderId'] = id + orderIdList.append(orderId) + request['orderIdList'] = orderIdList + else: + if clientOrderId is not None: + params = self.omit(params, 'clientOrderId') + request['clientOid'] = clientOrderId + else: + request['orderId'] = id + if uta: + if trigger: + response = self.privateUtaPostV3TradeCancelStrategyOrder(self.extend(request, params)) + else: + response = self.privateUtaPostV3TradeCancelOrder(self.extend(request, params)) + elif (market['swap']) or (market['future']): + productType = None + productType, params = self.handle_product_type_and_params(market, params) + request['productType'] = productType + if trailing: + planType = self.safe_string(params, 'planType', 'track_plan') + request['planType'] = planType + response = self.privateMixPostV2MixOrderCancelPlanOrder(self.extend(request, params)) + elif trigger: + response = self.privateMixPostV2MixOrderCancelPlanOrder(self.extend(request, params)) + else: + response = self.privateMixPostV2MixOrderCancelOrder(self.extend(request, params)) + elif market['spot']: + if marginMode is not None: + if marginMode == 'isolated': + response = self.privateMarginPostV2MarginIsolatedCancelOrder(self.extend(request, params)) + elif marginMode == 'cross': + response = self.privateMarginPostV2MarginCrossedCancelOrder(self.extend(request, params)) + else: + if trigger: + response = self.privateSpotPostV2SpotTradeCancelPlanOrder(self.extend(request, params)) + else: + response = self.privateSpotPostV2SpotTradeCancelOrder(self.extend(request, params)) + else: + raise NotSupported(self.id + ' cancelOrder() does not support ' + market['type'] + ' orders') + # + # spot, swap, future and spot margin + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1697690413177, + # "data": { + # "orderId": "1098758604547850241", + # "clientOid": "1098758604585598977" + # } + # } + # + # swap trigger + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700711311791, + # "data": { + # "successList": [ + # { + # "clientOid": "1111428059067125760", + # "orderId": "1111428059067125761" + # } + # ], + # "failureList": [] + # } + # } + # + # spot trigger + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700711728063, + # "data": { + # "result": "success" + # } + # } + # + # uta trigger + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": "1753058267399", + # "data": null + # } + # + data = self.safe_value(response, 'data', {}) + order = None + if isContractTriggerEndpoint: + orderInfo = self.safe_value(data, 'successList', []) + order = orderInfo[0] + else: + if uta and trigger: + order = response + else: + order = data + return self.parse_order(order, market) + + def cancel_uta_orders(self, ids, symbol: Str = None, params={}): + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrders() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + productType = None + productType, params = self.handle_product_type_and_params(market, params) + requestList = [] + for i in range(0, len(ids)): + individualId = ids[i] + order: dict = { + 'orderId': individualId, + 'symbol': market['id'], + 'category': productType, + } + requestList.append(order) + response = self.privateUtaPostV3TradeCancelBatch(requestList) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1752813731517, + # "data": [ + # { + # "orderId": "1329948909442023424", + # "clientOid": "1329948909446217728" + # }, + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_orders(data, market) + + def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): + """ + cancel multiple orders + + https://www.bitget.com/api-doc/spot/trade/Batch-Cancel-Orders + https://www.bitget.com/api-doc/contract/trade/Batch-Cancel-Orders + https://www.bitget.com/api-doc/contract/plan/Cancel-Plan-Order + https://www.bitget.com/api-doc/margin/cross/trade/Cross-Batch-Cancel-Order + https://www.bitget.com/api-doc/margin/isolated/trade/Isolated-Batch-Cancel-Orders + https://www.bitget.com/api-doc/uta/trade/Cancel-Batch + + :param str[] ids: order ids + :param str symbol: unified market symbol, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'isolated' or 'cross' for spot margin trading + :param boolean [params.trigger]: *contract only* set to True for canceling trigger orders + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :returns dict: an array of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrders() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + uta = None + uta, params = self.handle_option_and_params(params, 'cancelOrders', 'uta', False) + if uta: + return self.cancel_uta_orders(ids, symbol, params) + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('cancelOrders', params) + trigger = self.safe_value_2(params, 'stop', 'trigger') + params = self.omit(params, ['stop', 'trigger']) + orderIdList = [] + for i in range(0, len(ids)): + individualId = ids[i] + orderId: dict = { + 'orderId': individualId, + } + orderIdList.append(orderId) + request: dict = { + 'symbol': market['id'], + } + if market['spot'] and (marginMode is None): + request['orderList'] = orderIdList + else: + request['orderIdList'] = orderIdList + response = None + if market['spot']: + if marginMode is not None: + if marginMode == 'cross': + response = self.privateMarginPostV2MarginCrossedBatchCancelOrder(self.extend(request, params)) + else: + response = self.privateMarginPostV2MarginIsolatedBatchCancelOrder(self.extend(request, params)) + else: + response = self.privateSpotPostV2SpotTradeBatchCancelOrder(self.extend(request, params)) + else: + productType = None + productType, params = self.handle_product_type_and_params(market, params) + request['productType'] = productType + if trigger: + response = self.privateMixPostV2MixOrderCancelPlanOrder(self.extend(request, params)) + else: + response = self.privateMixPostV2MixOrderBatchCancelOrders(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": "1680008815965", + # "data": { + # "successList": [ + # { + # "orderId": "1024598257429823488", + # "clientOid": "876493ce-c287-4bfc-9f4a-8b1905881313" + # }, + # ], + # "failureList": [] + # } + # } + # + data = self.safe_value(response, 'data', {}) + orders = self.safe_list(data, 'successList', []) + return self.parse_orders(orders, market) + + def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders + + https://www.bitget.com/api-doc/spot/trade/Cancel-Symbol-Orders + https://www.bitget.com/api-doc/spot/plan/Batch-Cancel-Plan-Order + https://www.bitget.com/api-doc/contract/trade/Batch-Cancel-Orders + https://www.bitget.com/api-doc/margin/cross/trade/Cross-Batch-Cancel-Order + https://www.bitget.com/api-doc/margin/isolated/trade/Isolated-Batch-Cancel-Orders + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'isolated' or 'cross' for spot margin trading + :param boolean [params.trigger]: *contract only* set to True for canceling trigger orders + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :returns dict[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelAllOrders() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('cancelAllOrders', params) + productType = None + productType, params = self.handle_product_type_and_params(market, params) + request: dict = { + 'symbol': market['id'], + } + trigger = self.safe_bool_2(params, 'stop', 'trigger') + params = self.omit(params, ['stop', 'trigger']) + response = None + uta = None + uta, params = self.handle_option_and_params(params, 'cancelAllOrders', 'uta', False) + if uta: + if productType == 'SPOT': + if marginMode is not None: + productType = 'MARGIN' + request['category'] = productType + response = self.privateUtaPostV3TradeCancelSymbolOrder(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1750751578138, + # "data": { + # "list": [ + # { + # "orderId": "1321313242969427968", + # "clientOid": "1321313242969427969" + # } + # ] + # } + # } + # + elif market['spot']: + if marginMode is not None: + raise NotSupported(self.id + ' cancelAllOrders() does not support margin markets, you can use cancelOrders() instead') + else: + if trigger: + stopRequest: dict = { + 'symbolList': [market['id']], + } + response = self.privateSpotPostV2SpotTradeBatchCancelPlanOrder(self.extend(stopRequest, params)) + else: + response = self.privateSpotPostV2SpotTradeCancelSymbolOrder(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700716953996, + # "data": { + # "symbol": "BTCUSDT" + # } + # } + # + timestamp = self.safe_integer(response, 'requestTime') + responseData = self.safe_dict(response, 'data') + marketId = self.safe_string(responseData, 'symbol') + return [ + self.safe_order({ + 'info': response, + 'symbol': self.safe_symbol(marketId, None, None, 'spot'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }), + ] + else: + request['productType'] = productType + if trigger: + response = self.privateMixPostV2MixOrderCancelPlanOrder(self.extend(request, params)) + else: + response = self.privateMixPostV2MixOrderBatchCancelOrders(self.extend(request, params)) + # { + # "code": "00000", + # "msg": "success", + # "requestTime": "1680008815965", + # "data": { + # "successList": [ + # { + # "orderId": "1024598257429823488", + # "clientOid": "876493ce-c287-4bfc-9f4a-8b1905881313" + # }, + # ], + # "failureList": [] + # } + # } + data = self.safe_dict(response, 'data') + resultList = self.safe_list_n(data, ['resultList', 'successList', 'list']) + failureList = self.safe_list_2(data, 'failure', 'failureList') + responseList = None + if (resultList is not None) and (failureList is not None): + responseList = self.array_concat(resultList, failureList) + else: + responseList = resultList + return self.parse_orders(responseList) + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://www.bitget.com/api-doc/spot/trade/Get-Order-Info + https://www.bitget.com/api-doc/contract/trade/Get-Order-Details + https://www.bitget.com/api-doc/uta/trade/Get-Order-Details + + :param str id: the order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :param str [params.clientOrderId]: the clientOrderId of the order, id does not need to be provided if clientOrderId is provided + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrder() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + # 'orderId': id, + } + clientOrderId = self.safe_string_2(params, 'clientOrderId', 'clientOid') + if clientOrderId is not None: + params = self.omit(params, ['clientOrderId']) + request['clientOid'] = clientOrderId + else: + request['orderId'] = id + response = None + uta = None + uta, params = self.handle_option_and_params(params, 'fetchOrder', 'uta', False) + if uta: + response = self.privateUtaGetV3TradeOrderInfo(self.extend(request, params)) + elif market['spot']: + response = self.privateSpotGetV2SpotTradeOrderInfo(self.extend(request, params)) + elif market['swap'] or market['future']: + request['symbol'] = market['id'] + productType = None + productType, params = self.handle_product_type_and_params(market, params) + request['productType'] = productType + response = self.privateMixGetV2MixOrderDetail(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchOrder() does not support ' + market['type'] + ' orders') + # + # spot + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700719076263, + # "data": [ + # { + # "userId": "7264631750", + # "symbol": "BTCUSDT", + # "orderId": "1111461743123927040", + # "clientOid": "63f95110-93b5-4309-8f77-46339f1bcf3c", + # "price": "25000.0000000000000000", + # "size": "0.0002000000000000", + # "orderType": "limit", + # "side": "buy", + # "status": "live", + # "priceAvg": "0", + # "baseVolume": "0.0000000000000000", + # "quoteVolume": "0.0000000000000000", + # "enterPointSource": "API", + # "feeDetail": "", + # "orderSource": "normal", + # "cTime": "1700719050198", + # "uTime": "1700719050198" + # } + # ] + # } + # + # swap and future + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700719918781, + # "data": { + # "symbol": "BTCUSDT", + # "size": "0.001", + # "orderId": "1111465253393825792", + # "clientOid": "1111465253431574529", + # "baseVolume": "0", + # "fee": "0", + # "price": "27000", + # "priceAvg": "", + # "state": "live", + # "side": "buy", + # "force": "gtc", + # "totalProfits": "0", + # "posSide": "long", + # "marginCoin": "USDT", + # "presetStopSurplusPrice": "", + # "presetStopLossPrice": "", + # "quoteVolume": "0", + # "orderType": "limit", + # "leverage": "20", + # "marginMode": "crossed", + # "reduceOnly": "NO", + # "enterPointSource": "API", + # "tradeSide": "open", + # "posMode": "hedge_mode", + # "orderSource": "normal", + # "cTime": "1700719887120", + # "uTime": "1700719887120" + # } + # } + # + # uta + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1750496858333, + # "data": { + # "orderId": "1320244799629316096", + # "clientOid": "1320244799633510400", + # "category": "USDT-FUTURES", + # "symbol": "BTCUSDT", + # "orderType": "limit", + # "side": "buy", + # "price": "50000", + # "qty": "0.001", + # "amount": "0", + # "cumExecQty": "0", + # "cumExecValue": "0", + # "avgPrice": "0", + # "timeInForce": "gtc", + # "orderStatus": "live", + # "posSide": "long", + # "holdMode": "hedge_mode", + # "reduceOnly": "NO", + # "feeDetail": [{ + # "feeCoin": "", + # "fee": "" + # }], + # "createdTime": "1750496809871", + # "updatedTime": "1750496809886", + # "cancelReason": "", + # "execType": "normal", + # "stpMode": "none", + # "tpTriggerBy": null, + # "slTriggerBy": null, + # "takeProfit": null, + # "stopLoss": null, + # "tpOrderType": null, + # "slOrderType": null, + # "tpLimitPrice": null, + # "slLimitPrice": null + # } + # } + # + if not uta and (isinstance(response, str)): + response = json.loads(response) + data = self.safe_dict(response, 'data') + if (data is not None): + if not isinstance(data, list): + return self.parse_order(data, market) + dataList = self.safe_list(response, 'data', []) + dataListLength = len(dataList) + if dataListLength == 0: + raise OrderNotFound(self.id + ' fetchOrder() could not find order id ' + id + ' in ' + self.json(response)) + first = self.safe_dict(dataList, 0, {}) + return self.parse_order(first, market) + # first = self.safe_dict(data, 0, data) + # return self.parse_order(first, market) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://www.bitget.com/api-doc/spot/trade/Get-Unfilled-Orders + https://www.bitget.com/api-doc/spot/plan/Get-Current-Plan-Order + https://www.bitget.com/api-doc/contract/trade/Get-Orders-Pending + https://www.bitget.com/api-doc/contract/plan/get-orders-plan-pending + https://www.bitget.com/api-doc/margin/cross/trade/Get-Cross-Open-Orders + https://www.bitget.com/api-doc/margin/isolated/trade/Isolated-Open-Orders + https://www.bitget.com/api-doc/uta/strategy/Get-Unfilled-Strategy-Orders + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch orders for + :param str [params.planType]: *contract stop only* 'normal_plan': average trigger order, 'profit_loss': opened tp/sl orders, 'track_plan': trailing stop order, default is 'normal_plan' + :param boolean [params.trigger]: set to True for fetching trigger orders + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param str [params.isPlan]: *swap only* 'plan' for stop orders and 'profit_loss' for tp/sl orders, default is 'plan' + :param boolean [params.trailing]: set to True if you want to fetch trailing orders + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + market = None + type = None + request: dict = {} + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchOpenOrders', params) + uta = None + uta, params = self.handle_option_and_params(params, 'fetchOpenOrders', 'uta', False) + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + defaultType = self.safe_string_2(self.options, 'fetchOpenOrders', 'defaultType', 'spot') + marketType = market['type'] if ('type' in market) else defaultType + type = self.safe_string(params, 'type', marketType) + else: + defaultType = self.safe_string_2(self.options, 'fetchOpenOrders', 'defaultType', 'spot') + type = self.safe_string(params, 'type', defaultType) + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOpenOrders', 'paginate') + if paginate: + cursorReceived = None + cursorSent = None + if uta: + cursorReceived = 'cursor' + cursorSent = 'cursor' + elif type == 'spot': + if marginMode is not None: + cursorReceived = 'minId' + cursorSent = 'idLessThan' + else: + cursorReceived = 'endId' + cursorSent = 'idLessThan' + return self.fetch_paginated_call_cursor('fetchOpenOrders', symbol, since, limit, params, cursorReceived, cursorSent) + response = None + trailing = self.safe_bool(params, 'trailing') + trigger = self.safe_bool_2(params, 'stop', 'trigger') + planTypeDefined = self.safe_string(params, 'planType') is not None + isTrigger = (trigger or planTypeDefined) + request, params = self.handle_until_option('endTime', request, params) + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + if not uta and ((type == 'swap') or (type == 'future') or (marginMode is not None)): + clientOrderId = self.safe_string_2(params, 'clientOid', 'clientOrderId') + params = self.omit(params, 'clientOrderId') + if clientOrderId is not None: + request['clientOid'] = clientOrderId + productType = None + productType, params = self.handle_product_type_and_params(market, params) + params = self.omit(params, ['type', 'stop', 'trigger', 'trailing']) + if uta: + if type == 'spot': + if marginMode is not None: + productType = 'MARGIN' + request['category'] = productType + if trigger: + response = self.privateUtaGetV3TradeUnfilledStrategyOrders(self.extend(request, params)) + else: + response = self.privateUtaGetV3TradeUnfilledOrders(self.extend(request, params)) + elif type == 'spot': + if marginMode is not None: + if since is None: + since = self.milliseconds() - 7776000000 + request['startTime'] = since + if marginMode == 'isolated': + response = self.privateMarginGetV2MarginIsolatedOpenOrders(self.extend(request, params)) + elif marginMode == 'cross': + response = self.privateMarginGetV2MarginCrossedOpenOrders(self.extend(request, params)) + else: + if trigger: + response = self.privateSpotGetV2SpotTradeCurrentPlanOrder(self.extend(request, params)) + else: + response = self.privateSpotGetV2SpotTradeUnfilledOrders(self.extend(request, params)) + else: + request['productType'] = productType + if trailing: + planType = self.safe_string(params, 'planType', 'track_plan') + request['planType'] = planType + response = self.privateMixGetV2MixOrderOrdersPlanPending(self.extend(request, params)) + elif isTrigger: + planType = self.safe_string(params, 'planType', 'normal_plan') + request['planType'] = planType + response = self.privateMixGetV2MixOrderOrdersPlanPending(self.extend(request, params)) + else: + response = self.privateMixGetV2MixOrderOrdersPending(self.extend(request, params)) + # + # spot + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700728123994, + # "data": [ + # { + # "userId": "7264631750", + # "symbol": "BTCUSDT", + # "orderId": "1111499608327360513", + # "clientOid": "d0d4dad5-18d0-4869-a074-ec40bb47cba6", + # "priceAvg": "25000.0000000000000000", + # "size": "0.0002000000000000", + # "orderType": "limit", + # "side": "buy", + # "status": "live", + # "basePrice": "0", + # "baseVolume": "0.0000000000000000", + # "quoteVolume": "0.0000000000000000", + # "enterPointSource": "WEB", + # "orderSource": "normal", + # "cTime": "1700728077966", + # "uTime": "1700728077966" + # } + # ] + # } + # + # spot stop + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700729361609, + # "data": { + # "nextFlag": False, + # "idLessThan": "1111503385931620352", + # "orderList": [ + # { + # "orderId": "1111503385931620352", + # "clientOid": "1111503385910648832", + # "symbol": "BTCUSDT", + # "size": "0.0002", + # "planType": "AMOUNT", + # "executePrice": "25000", + # "triggerPrice": "26000", + # "status": "live", + # "orderType": "limit", + # "side": "buy", + # "triggerType": "fill_price", + # "enterPointSource": "API", + # "cTime": "1700728978617", + # "uTime": "1700728978617" + # } + # ] + # } + # } + # + # spot margin + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700729887686, + # "data": { + # "orderList": [ + # { + # "symbol": "BTCUSDT", + # "orderType": "limit", + # "enterPointSource": "WEB", + # "orderId": "1111506377509580801", + # "clientOid": "2043a3b59a60445f9d9f7365bf3e960c", + # "loanType": "autoLoanAndRepay", + # "price": "25000", + # "side": "buy", + # "status": "live", + # "baseSize": "0.0002", + # "quoteSize": "5", + # "priceAvg": "0", + # "size": "0", + # "amount": "0", + # "force": "gtc", + # "cTime": "1700729691866", + # "uTime": "1700729691866" + # } + # ], + # "maxId": "1111506377509580801", + # "minId": "1111506377509580801" + # } + # } + # + # swap and future + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700725609065, + # "data": { + # "entrustedList": [ + # { + # "symbol": "BTCUSDT", + # "size": "0.002", + # "orderId": "1111488897767604224", + # "clientOid": "1111488897805352960", + # "baseVolume": "0", + # "fee": "0", + # "price": "25000", + # "priceAvg": "", + # "status": "live", + # "side": "buy", + # "force": "gtc", + # "totalProfits": "0", + # "posSide": "long", + # "marginCoin": "USDT", + # "quoteVolume": "0", + # "leverage": "20", + # "marginMode": "crossed", + # "enterPointSource": "web", + # "tradeSide": "open", + # "posMode": "hedge_mode", + # "orderType": "limit", + # "orderSource": "normal", + # "presetStopSurplusPrice": "", + # "presetStopLossPrice": "", + # "reduceOnly": "NO", + # "cTime": "1700725524378", + # "uTime": "1700725524378" + # } + # ], + # "endId": "1111488897767604224" + # } + # } + # + # swap and future stop + # + # { + # "code": "00000",\ + # "msg": "success", + # "requestTime": 1700726417495, + # "data": { + # "entrustedList": [ + # { + # "planType": "normal_plan", + # "symbol": "BTCUSDT", + # "size": "0.001", + # "orderId": "1111491399869075457", + # "clientOid": "1111491399869075456", + # "price": "27000", + # "callbackRatio": "", + # "triggerPrice": "24000", + # "triggerType": "mark_price", + # "planStatus": "live", + # "side": "buy", + # "posSide": "long", + # "marginCoin": "USDT", + # "marginMode": "crossed", + # "enterPointSource": "API", + # "tradeSide": "open", + # "posMode": "hedge_mode", + # "orderType": "limit", + # "stopSurplusTriggerPrice": "", + # "stopSurplusExecutePrice": "", + # "stopSurplusTriggerType": "fill_price", + # "stopLossTriggerPrice": "", + # "stopLossExecutePrice": "", + # "stopLossTriggerType": "fill_price", + # "cTime": "1700726120917", + # "uTime": "1700726120917" + # } + # ], + # "endId": "1111491399869075457" + # } + # } + # + # uta + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1750753395850, + # "data": { + # "list": [ + # { + # "orderId": "1321320757371228160", + # "clientOid": "1321320757371228161", + # "category": "USDT-FUTURES", + # "symbol": "BTCUSDT", + # "orderType": "limit", + # "side": "buy", + # "price": "50000", + # "qty": "0.001", + # "amount": "0", + # "cumExecQty": "0", + # "cumExecValue": "0", + # "avgPrice": "0", + # "timeInForce": "gtc", + # "orderStatus": "live", + # "posSide": "long", + # "holdMode": "hedge_mode", + # "reduceOnly": "NO", + # "feeDetail": [ + # { + # "feeCoin": "", + # "fee": "" + # } + # ], + # "createdTime": "1750753338186", + # "updatedTime": "1750753338203", + # "stpMode": "none", + # "tpTriggerBy": null, + # "slTriggerBy": null, + # "takeProfit": null, + # "stopLoss": null, + # "tpOrderType": null, + # "slOrderType": null, + # "tpLimitPrice": null, + # "slLimitPrice": null + # } + # ], + # "cursor": "1321320757371228160" + # } + # } + # + # uta trigger + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1753057527060, + # "data": [ + # { + # "orderId": "1330984742276198400", + # "clientOid": "1330984742276198400", + # "symbol": "BTCUSDT", + # "category": "USDT-FUTURES", + # "qty": "0.001", + # "posSide": "long", + # "tpTriggerBy": "market", + # "slTriggerBy": "mark", + # "takeProfit": "", + # "stopLoss":"114000", + # "tpOrderType": "market", + # "slOrderType": "limit", + # "tpLimitPrice": "", + # "slLimitPrice": "113000", + # "createdTime": "1753057411736", + # "updatedTime": "1753057411747" + # } + # ] + # } + # + data = self.safe_value(response, 'data') + if uta: + result = None + if trigger: + result = self.safe_list(response, 'data', []) + else: + result = self.safe_list(data, 'list', []) + return self.parse_orders(result, market, since, limit) + elif type == 'spot': + if (marginMode is not None) or trigger: + resultList = self.safe_list(data, 'orderList', []) + return self.parse_orders(resultList, market, since, limit) + else: + result = self.safe_list(data, 'entrustedList', []) + return self.parse_orders(result, market, since, limit) + return self.parse_orders(data, market, since, limit) + + def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://www.bitget.com/api-doc/spot/trade/Get-History-Orders + https://www.bitget.com/api-doc/spot/plan/Get-History-Plan-Order + https://www.bitget.com/api-doc/contract/trade/Get-Orders-History + https://www.bitget.com/api-doc/contract/plan/orders-plan-history + https://www.bitget.com/api-doc/margin/cross/trade/Get-Cross-Order-History + https://www.bitget.com/api-doc/margin/isolated/trade/Get-Isolated-Order-History + https://www.bitget.com/api-doc/uta/trade/Get-Order-History + + :param str symbol: unified market symbol of the closed orders + :param int [since]: timestamp in ms of the earliest order + :param int [limit]: the max number of closed orders to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch orders for + :param str [params.planType]: *contract stop only* 'normal_plan': average trigger order, 'profit_loss': opened tp/sl orders, 'track_plan': trailing stop order, default is 'normal_plan' + :param boolean [params.trigger]: set to True for fetching trigger orders + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param str [params.isPlan]: *swap only* 'plan' for stop orders and 'profit_loss' for tp/sl orders, default is 'plan' + :param boolean [params.trailing]: set to True if you want to fetch trailing orders + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + orders = self.fetch_canceled_and_closed_orders(symbol, since, limit, params) + return self.filter_by(orders, 'status', 'closed') + + def fetch_canceled_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches information on multiple canceled orders made by the user + + https://www.bitget.com/api-doc/spot/trade/Get-History-Orders + https://www.bitget.com/api-doc/spot/plan/Get-History-Plan-Order + https://www.bitget.com/api-doc/contract/trade/Get-Orders-History + https://www.bitget.com/api-doc/contract/plan/orders-plan-history + https://www.bitget.com/api-doc/margin/cross/trade/Get-Cross-Order-History + https://www.bitget.com/api-doc/margin/isolated/trade/Get-Isolated-Order-History + https://www.bitget.com/api-doc/uta/trade/Get-Order-History + + :param str symbol: unified market symbol of the canceled orders + :param int [since]: timestamp in ms of the earliest order + :param int [limit]: the max number of canceled orders to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch orders for + :param str [params.planType]: *contract stop only* 'normal_plan': average trigger order, 'profit_loss': opened tp/sl orders, 'track_plan': trailing stop order, default is 'normal_plan' + :param boolean [params.trigger]: set to True for fetching trigger orders + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param str [params.isPlan]: *swap only* 'plan' for stop orders and 'profit_loss' for tp/sl orders, default is 'plan' + :param boolean [params.trailing]: set to True if you want to fetch trailing orders + :returns dict: a list of `order structures ` + """ + self.load_markets() + orders = self.fetch_canceled_and_closed_orders(symbol, since, limit, params) + return self.filter_by(orders, 'status', 'canceled') + + def fetch_canceled_and_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + + https://www.bitget.com/api-doc/spot/trade/Get-History-Orders + https://www.bitget.com/api-doc/spot/plan/Get-History-Plan-Order + https://www.bitget.com/api-doc/contract/trade/Get-Orders-History + https://www.bitget.com/api-doc/contract/plan/orders-plan-history + https://www.bitget.com/api-doc/margin/cross/trade/Get-Cross-Order-History + https://www.bitget.com/api-doc/margin/isolated/trade/Get-Isolated-Order-History + https://www.bitget.com/api-doc/uta/trade/Get-Order-History + https://www.bitget.com/api-doc/uta/strategy/Get-History-Strategy-Orders + + fetches information on multiple canceled and closed orders made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch orders for + :param str [params.planType]: *contract stop only* 'normal_plan': average trigger order, 'profit_loss': opened tp/sl orders, 'track_plan': trailing stop order, default is 'normal_plan' + :param boolean [params.trigger]: set to True for fetching trigger orders + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param str [params.isPlan]: *swap only* 'plan' for stop orders and 'profit_loss' for tp/sl orders, default is 'plan' + :param boolean [params.trailing]: set to True if you want to fetch trailing orders + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :returns Order[]: a list of `order structures ` + """ + uta = None + uta, params = self.handle_option_and_params(params, 'fetchCanceledAndClosedOrders', 'uta', False) + if uta: + return self.fetch_uta_canceled_and_closed_orders(symbol, since, limit, params) + self.load_markets() + market = None + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + marketType = None + marketType, params = self.handle_market_type_and_params('fetchCanceledAndClosedOrders', market, params) + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchCanceledAndClosedOrders', params) + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchCanceledAndClosedOrders', 'paginate') + if paginate: + cursorReceived = None + if marketType == 'spot': + if marginMode is not None: + cursorReceived = 'minId' + else: + cursorReceived = 'endId' + return self.fetch_paginated_call_cursor('fetchCanceledAndClosedOrders', symbol, since, limit, params, cursorReceived, 'idLessThan') + response = None + trailing = self.safe_bool(params, 'trailing') + trigger = self.safe_bool_2(params, 'stop', 'trigger') + params = self.omit(params, ['stop', 'trigger', 'trailing']) + request, params = self.handle_until_option('endTime', request, params) + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + if (marketType == 'swap') or (marketType == 'future') or (marginMode is not None): + clientOrderId = self.safe_string_2(params, 'clientOid', 'clientOrderId') + params = self.omit(params, 'clientOrderId') + if clientOrderId is not None: + request['clientOid'] = clientOrderId + now = self.milliseconds() + if marketType == 'spot': + if marginMode is not None: + if since is None: + since = now - 7776000000 + request['startTime'] = since + if marginMode == 'isolated': + response = self.privateMarginGetV2MarginIsolatedHistoryOrders(self.extend(request, params)) + elif marginMode == 'cross': + response = self.privateMarginGetV2MarginCrossedHistoryOrders(self.extend(request, params)) + elif trigger: + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchCanceledAndClosedOrders() requires a symbol argument') + endTime = self.safe_integer_n(params, ['endTime', 'until']) + params = self.omit(params, ['until']) + if since is None: + since = now - 7776000000 + request['startTime'] = since + if endTime is None: + request['endTime'] = now + response = self.privateSpotGetV2SpotTradeHistoryPlanOrder(self.extend(request, params)) + else: + response = self.privateSpotGetV2SpotTradeHistoryOrders(self.extend(request, params)) + else: + productType = None + productType, params = self.handle_product_type_and_params(market, params) + request['productType'] = productType + planTypeDefined = self.safe_string(params, 'planType') is not None + if trailing: + planType = self.safe_string(params, 'planType', 'track_plan') + request['planType'] = planType + response = self.privateMixGetV2MixOrderOrdersPlanHistory(self.extend(request, params)) + elif trigger or planTypeDefined: + planType = self.safe_string(params, 'planType', 'normal_plan') + request['planType'] = planType + response = self.privateMixGetV2MixOrderOrdersPlanHistory(self.extend(request, params)) + else: + response = self.privateMixGetV2MixOrderOrdersHistory(self.extend(request, params)) + # + # spot + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700791085380, + # "data": [ + # { + # "userId": "7264631750", + # "symbol": "BTCUSDT", + # "orderId": "1111499608327360513", + # "clientOid": "d0d4dad5-18d0-4869-a074-ec40bb47cba6", + # "price": "25000.0000000000000000", + # "size": "0.0002000000000000", + # "orderType": "limit", + # "side": "buy", + # "status": "cancelled", + # "priceAvg": "0", + # "baseVolume": "0.0000000000000000", + # "quoteVolume": "0.0000000000000000", + # "enterPointSource": "WEB", + # "feeDetail": "", + # "orderSource": "normal", + # "cTime": "1700728077966", + # "uTime": "1700728911471" + # }, + # ] + # } + # + # spot stop + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700792099146, + # "data": { + # "nextFlag": False, + # "idLessThan": "1098757597417775104", + # "orderList": [ + # { + # "orderId": "1111503385931620352", + # "clientOid": "1111503385910648832", + # "symbol": "BTCUSDT", + # "size": "0.0002", + # "planType": "AMOUNT", + # "executePrice": "25000", + # "triggerPrice": "26000", + # "status": "cancelled", + # "orderType": "limit", + # "side": "buy", + # "triggerType": "fill_price", + # "enterPointSource": "API", + # "cTime": "1700728978617", + # "uTime": "1700729666868" + # }, + # ] + # } + # } + # + # spot margin + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700792381435, + # "data": { + # "orderList": [ + # { + # "symbol": "BTCUSDT", + # "orderType": "limit", + # "enterPointSource": "WEB", + # "orderId": "1111456274707001345", + # "clientOid": "41e428dd305a4f668671b7f1ed00dc50", + # "loanType": "autoLoanAndRepay", + # "price": "27000", + # "side": "buy", + # "status": "cancelled", + # "baseSize": "0.0002", + # "quoteSize": "5.4", + # "priceAvg": "0", + # "size": "0", + # "amount": "0", + # "force": "gtc", + # "cTime": "1700717746427", + # "uTime": "1700717780636" + # }, + # ], + # "maxId": "1111456274707001345", + # "minId": "1098396464990269440" + # } + # } + # + # swap and future + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700792674673, + # "data": { + # "entrustedList": [ + # { + # "symbol": "BTCUSDT", + # "size": "0.002", + # "orderId": "1111498800817143808", + # "clientOid": "1111498800850698240", + # "baseVolume": "0", + # "fee": "0", + # "price": "25000", + # "priceAvg": "", + # "status": "canceled", + # "side": "buy", + # "force": "gtc", + # "totalProfits": "0", + # "posSide": "long", + # "marginCoin": "USDT", + # "quoteVolume": "0", + # "leverage": "20", + # "marginMode": "crossed", + # "enterPointSource": "web", + # "tradeSide": "open", + # "posMode": "hedge_mode", + # "orderType": "limit", + # "orderSource": "normal", + # "presetStopSurplusPrice": "", + # "presetStopLossPrice": "", + # "reduceOnly": "NO", + # "cTime": "1700727885449", + # "uTime": "1700727944563" + # }, + # ], + # "endId": "1098397008323575809" + # } + # } + # + # swap and future stop + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700792938359, + # "data": { + # "entrustedList": [ + # { + # "planType": "normal_plan", + # "symbol": "BTCUSDT", + # "size": "0.001", + # "orderId": "1111491399869075457", + # "clientOid": "1111491399869075456", + # "planStatus": "cancelled", + # "price": "27000", + # "feeDetail": null, + # "baseVolume": "0", + # "callbackRatio": "", + # "triggerPrice": "24000", + # "triggerType": "mark_price", + # "side": "buy", + # "posSide": "long", + # "marginCoin": "USDT", + # "marginMode": "crossed", + # "enterPointSource": "API", + # "tradeSide": "open", + # "posMode": "hedge_mode", + # "orderType": "limit", + # "stopSurplusTriggerPrice": "", + # "stopSurplusExecutePrice": "", + # "stopSurplusTriggerType": "fill_price", + # "stopLossTriggerPrice": "", + # "stopLossExecutePrice": "", + # "stopLossTriggerType": "fill_price", + # "cTime": "1700726120917", + # "uTime": "1700727879652" + # }, + # ], + # "endId": "1098760007867502593" + # } + # } + # + data = self.safe_value(response, 'data', {}) + if marketType == 'spot': + if (marginMode is not None) or trigger: + return self.parse_orders(self.safe_value(data, 'orderList', []), market, since, limit) + else: + return self.parse_orders(self.safe_value(data, 'entrustedList', []), market, since, limit) + if isinstance(response, str): + response = json.loads(response) + orders = self.safe_list(response, 'data', []) + return self.parse_orders(orders, market, since, limit) + + def fetch_uta_canceled_and_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + productType = None + productType, params = self.handle_product_type_and_params(market, params) + if productType == 'SPOT': + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchCanceledAndClosedOrders', params) + if marginMode is not None: + productType = 'MARGIN' + request: dict = { + 'category': productType, + } + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchCanceledAndClosedOrders', 'paginate') + if paginate: + return self.fetch_paginated_call_cursor('fetchCanceledAndClosedOrders', symbol, since, limit, params, 'cursor', 'cursor') + request, params = self.handle_until_option('endTime', request, params) + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + response = None + trigger = self.safe_bool_2(params, 'stop', 'trigger') + params = self.omit(params, ['stop', 'trigger']) + if trigger: + response = self.privateUtaGetV3TradeHistoryStrategyOrders(self.extend(request, params)) + else: + response = self.privateUtaGetV3TradeHistoryOrders(self.extend(request, params)) + # + # uta + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1752531592855, + # "data": { + # "list": [ + # { + # "orderId": "1322441400976261120", + # "clientOid": "1322441400976261121", + # "category": "USDT-FUTURES", + # "symbol": "BTCUSDT", + # "orderType": "market", + # "side": "sell", + # "price": "0", + # "qty": "0.0001", + # "amount": "0", + # "cumExecQty": "0.0001", + # "cumExecValue": "10.7005", + # "avgPrice": "107005.4", + # "timeInForce": "gtc", + # "orderStatus": "filled", + # "posSide": "long", + # "holdMode": "hedge_mode", + # "reduceOnly": "NO", + # "feeDetail": [ + # { + # "feeCoin": "USDT", + # "fee": "0.00642032" + # } + # ], + # "createdTime": "1751020520442", + # "updatedTime": "1751020520457", + # "cancelReason": "", + # "execType": "normal", + # "stpMode": "none", + # "tpTriggerBy": null, + # "slTriggerBy": null, + # "takeProfit": null, + # "stopLoss": null, + # "tpOrderType": null, + # "slOrderType": null, + # "tpLimitPrice": null, + # "slLimitPrice": null + # }, + # ], + # "cursor": "1322441328637100035" + # } + # } + # + # uta trigger + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1753058447920, + # "data": { + # "list": [ + # { + # "orderId": "1330984742276198400", + # "clientOid": "1330984742276198400", + # "symbol": "BTCUSDT", + # "category": "USDT-FUTURES", + # "qty": "0.001", + # "posSide": "long", + # "tpTriggerBy": "market", + # "slTriggerBy": "mark", + # "takeProfit": "", + # "stopLoss": "112000", + # "tpOrderType": "market", + # "slOrderType": "limit", + # "tpLimitPrice": "", + # "slLimitPrice": "111000", + # "createdTime": "1753057411736", + # "updatedTime": "1753058267412" + # }, + # ], + # "cursor": 1330960754317619202 + # } + # } + # + data = self.safe_dict(response, 'data', {}) + orders = self.safe_list(data, 'list', []) + return self.parse_orders(orders, market, since, limit) + + def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + + https://www.bitget.com/api-doc/spot/account/Get-Account-Bills + https://www.bitget.com/api-doc/contract/account/Get-Account-Bill + + :param str [code]: unified currency code, default is None + :param int [since]: timestamp in ms of the earliest ledger entry, default is None + :param int [limit]: max number of ledger entries to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: end time in ms + :param str [params.symbol]: *contract only* unified market symbol + :param str [params.productType]: *contract only* 'USDT-FUTURES', 'USDC-FUTURES', 'COIN-FUTURES', 'SUSDT-FUTURES', 'SUSDC-FUTURES' or 'SCOIN-FUTURES' + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict: a `ledger structure ` + """ + self.load_markets() + symbol = self.safe_string(params, 'symbol') + params = self.omit(params, 'symbol') + market = None + if symbol is not None: + market = self.market(symbol) + marketType = None + marketType, params = self.handle_market_type_and_params('fetchLedger', market, params) + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchLedger', 'paginate') + if paginate: + cursorReceived = None + if marketType != 'spot': + cursorReceived = 'endId' + return self.fetch_paginated_call_cursor('fetchLedger', symbol, since, limit, params, cursorReceived, 'idLessThan') + currency = None + request: dict = {} + if code is not None: + currency = self.currency(code) + request['coin'] = currency['id'] + request, params = self.handle_until_option('endTime', request, params) + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + response = None + if marketType == 'spot': + response = self.privateSpotGetV2SpotAccountBills(self.extend(request, params)) + else: + if symbol is not None: + request['symbol'] = market['id'] + productType = None + productType, params = self.handle_product_type_and_params(market, params) + request['productType'] = productType + response = self.privateMixGetV2MixAccountBill(self.extend(request, params)) + # + # spot + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700795836415, + # "data": [ + # { + # "billId": "1111506298997215233", + # "coin": "USDT", + # "groupType": "transfer", + # "businessType": "transfer_out", + # "size": "-11.64958799", + # "balance": "0.00000000", + # "fees": "0.00000000", + # "cTime": "1700729673028" + # }, + # ] + # } + # + # swap and future + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700795977890, + # "data": { + # "bills": [ + # { + # "billId": "1111499428100472833", + # "symbol": "", + # "amount": "-11.64958799", + # "fee": "0", + # "feeByCoupon": "", + # "businessType": "trans_to_exchange", + # "coin": "USDT", + # "cTime": "1700728034996" + # }, + # ], + # "endId": "1098396773329305606" + # } + # } + # + data = self.safe_value(response, 'data') + if (marketType == 'swap') or (marketType == 'future'): + bills = self.safe_value(data, 'bills', []) + return self.parse_ledger(bills, currency, since, limit) + return self.parse_ledger(data, currency, since, limit) + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + # + # spot + # + # { + # "billId": "1111506298997215233", + # "coin": "USDT", + # "groupType": "transfer", + # "businessType": "transfer_out", + # "size": "-11.64958799", + # "balance": "0.00000000", + # "fees": "0.00000000", + # "cTime": "1700729673028" + # } + # + # swap and future + # + # { + # "billId": "1111499428100472833", + # "symbol": "", + # "amount": "-11.64958799", + # "fee": "0", + # "feeByCoupon": "", + # "businessType": "trans_to_exchange", + # "coin": "USDT", + # "cTime": "1700728034996" + # } + # + currencyId = self.safe_string(item, 'coin') + code = self.safe_currency_code(currencyId, currency) + currency = self.safe_currency(currencyId, currency) + timestamp = self.safe_integer(item, 'cTime') + after = self.safe_number(item, 'balance') + fee = self.safe_number_2(item, 'fees', 'fee') + amountRaw = self.safe_string_2(item, 'size', 'amount') + amount = self.parse_number(Precise.string_abs(amountRaw)) + direction = 'in' + if amountRaw.find('-') >= 0: + direction = 'out' + return self.safe_ledger_entry({ + 'info': item, + 'id': self.safe_string(item, 'billId'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'direction': direction, + 'account': None, + 'referenceId': None, + 'referenceAccount': None, + 'type': self.parse_ledger_type(self.safe_string(item, 'businessType')), + 'currency': code, + 'amount': amount, + 'before': None, + 'after': after, + 'status': None, + 'fee': { + 'currency': code, + 'cost': fee, + }, + }, currency) + + def parse_ledger_type(self, type): + types: dict = { + 'trans_to_cross': 'transfer', + 'trans_from_cross': 'transfer', + 'trans_to_exchange': 'transfer', + 'trans_from_exchange': 'transfer', + 'trans_to_isolated': 'transfer', + 'trans_from_isolated': 'transfer', + 'trans_to_contract': 'transfer', + 'trans_from_contract': 'transfer', + 'trans_to_otc': 'transfer', + 'trans_from_otc': 'transfer', + 'open_long': 'trade', + 'close_long': 'trade', + 'open_short': 'trade', + 'close_short': 'trade', + 'force_close_long': 'trade', + 'force_close_short': 'trade', + 'burst_long_loss_query': 'trade', + 'burst_short_loss_query': 'trade', + 'force_buy': 'trade', + 'force_sell': 'trade', + 'burst_buy': 'trade', + 'burst_sell': 'trade', + 'delivery_long': 'settlement', + 'delivery_short': 'settlement', + 'contract_settle_fee': 'fee', + 'append_margin': 'transaction', + 'adjust_down_lever_append_margin': 'transaction', + 'reduce_margin': 'transaction', + 'auto_append_margin': 'transaction', + 'cash_gift_issue': 'cashback', + 'cash_gift_recycle': 'cashback', + 'bonus_issue': 'rebate', + 'bonus_recycle': 'rebate', + 'bonus_expired': 'rebate', + 'transfer_in': 'transfer', + 'transfer_out': 'transfer', + 'deposit': 'deposit', + 'withdraw': 'withdrawal', + 'buy': 'trade', + 'sell': 'trade', + } + return self.safe_string(types, type, type) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + fetch all trades made by the user + + https://www.bitget.com/api-doc/spot/trade/Get-Fills + https://www.bitget.com/api-doc/contract/trade/Get-Order-Fills + https://www.bitget.com/api-doc/margin/cross/trade/Get-Cross-Order-Fills + https://www.bitget.com/api-doc/margin/isolated/trade/Get-Isolated-Transaction-Details + https://www.bitget.com/api-doc/uta/trade/Get-Order-Fills + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch trades for + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Trade[]: a list of `trade structures ` + """ + uta = None + uta, params = self.handle_option_and_params(params, 'fetchMyTrades', 'uta', False) + if not uta and (symbol is None): + raise ArgumentsRequired(self.id + ' fetchMyTrades() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = {} + request, params = self.handle_until_option('endTime', request, params) + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + paginate = False + marginMode = None + paginate, params = self.handle_option_and_params(params, 'fetchMyTrades', 'paginate') + marginMode, params = self.handle_margin_mode_and_params('fetchMyTrades', params) + if paginate: + cursorReceived = None + cursorSent = None + if uta: + cursorReceived = 'cursor' + cursorSent = 'cursor' + elif market['spot']: + if marginMode is not None: + cursorReceived = 'minId' + cursorSent = 'idLessThan' + else: + cursorReceived = 'endId' + cursorSent = 'idLessThan' + return self.fetch_paginated_call_cursor('fetchMyTrades', symbol, since, limit, params, cursorReceived, cursorSent) + response = None + if uta: + response = self.privateUtaGetV3TradeFills(self.extend(request, params)) + else: + request['symbol'] = market['id'] + if market['spot']: + if marginMode is not None: + if since is None: + request['startTime'] = self.milliseconds() - 7776000000 + if marginMode == 'isolated': + response = self.privateMarginGetV2MarginIsolatedFills(self.extend(request, params)) + elif marginMode == 'cross': + response = self.privateMarginGetV2MarginCrossedFills(self.extend(request, params)) + else: + response = self.privateSpotGetV2SpotTradeFills(self.extend(request, params)) + else: + productType = None + productType, params = self.handle_product_type_and_params(market, params) + request['productType'] = productType + response = self.privateMixGetV2MixOrderFills(self.extend(request, params)) + # + # spot + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700802995406, + # "data": [ + # { + # "userId": "7264631751", + # "symbol": "BTCUSDT", + # "orderId": "1098394344925597696", + # "tradeId": "1098394344974925824", + # "orderType": "market", + # "side": "sell", + # "priceAvg": "28467.68", + # "size": "0.0002", + # "amount": "5.693536", + # "feeDetail": { + # "deduction": "no", + # "feeCoin": "USDT", + # "totalDeductionFee": "", + # "totalFee": "-0.005693536" + # }, + # "tradeScope": "taker", + # "cTime": "1697603539699", + # "uTime": "1697603539754" + # } + # ] + # } + # + # spot margin + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700803176399, + # "data": { + # "fills": [ + # { + # "orderId": "1099353730455318528", + # "tradeId": "1099353730627092481", + # "orderType": "market", + # "side": "sell", + # "priceAvg": "29543.7", + # "size": "0.0001", + # "amount": "2.95437", + # "tradeScope": "taker", + # "feeDetail": { + # "deduction": "no", + # "feeCoin": "USDT", + # "totalDeductionFee": "0", + # "totalFee": "-0.00295437" + # }, + # "cTime": "1697832275063", + # "uTime": "1697832275150" + # }, + # ], + # "minId": "1099353591699161118", + # "maxId": "1099353730627092481" + # } + # } + # + # swap and future + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700803357487, + # "data": { + # "fillList": [ + # { + # "tradeId": "1111468664328269825", + # "symbol": "BTCUSDT", + # "orderId": "1111468664264753162", + # "price": "37271.4", + # "baseVolume": "0.001", + # "feeDetail": [ + # { + # "deduction": "no", + # "feeCoin": "USDT", + # "totalDeductionFee": null, + # "totalFee": "-0.02236284" + # } + # ], + # "side": "buy", + # "quoteVolume": "37.2714", + # "profit": "-0.0007", + # "enterPointSource": "web", + # "tradeSide": "close", + # "posMode": "hedge_mode", + # "tradeScope": "taker", + # "cTime": "1700720700342" + # }, + # ], + # "endId": "1099351587643699201" + # } + # } + # + # uta + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1751099666579, + # "data": { + # "list": [ + # { + # "execId": "1322441401010528257", + # "orderId": "1322441400976261120", + # "category": "USDT-FUTURES", + # "symbol": "BTCUSDT", + # "orderType": "market", + # "side": "sell", + # "execPrice": "107005.4", + # "execQty": "0.0001", + # "execValue": "10.7005", + # "tradeScope": "taker", + # "feeDetail": [{ + # "feeCoin": "USDT", + # "fee":"0.00642032" + # }], + # "createdTime": "1751020520451", + # "updatedTime": "1751020520458", + # "execPnl": "0.00017" + # }, + # ], + # "cursor": "1322061241878880257" + # } + # } + # + data = self.safe_value(response, 'data') + if uta: + fills = self.safe_list(data, 'list', []) + return self.parse_trades(fills, market, since, limit) + elif (market['swap'] or (market['future'])): + fills = self.safe_list(data, 'fillList', []) + return self.parse_trades(fills, market, since, limit) + elif marginMode is not None: + fills = self.safe_list(data, 'fills', []) + return self.parse_trades(fills, market, since, limit) + return self.parse_trades(data, market, since, limit) + + def fetch_position(self, symbol: str, params={}): + """ + fetch data on a single open contract trade position + + https://www.bitget.com/api-doc/contract/position/get-single-position + https://www.bitget.com/api-doc/uta/trade/Get-Position + + :param str symbol: unified market symbol of the market the position is held in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :returns dict: a `position structure ` + """ + self.load_markets() + market = self.market(symbol) + productType = None + productType, params = self.handle_product_type_and_params(market, params) + request: dict = { + 'symbol': market['id'], + } + response = None + uta = None + result = None + uta, params = self.handle_option_and_params(params, 'fetchPosition', 'uta', False) + if uta: + request['category'] = productType + response = self.privateUtaGetV3PositionCurrentPosition(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1750929905423, + # "data": { + # "list": [ + # { + # "category": "USDT-FUTURES", + # "symbol": "BTCUSDT", + # "marginCoin": "USDT", + # "holdMode": "hedge_mode", + # "posSide": "long", + # "marginMode": "crossed", + # "positionBalance": "5.435199", + # "available": "0.001", + # "frozen": "0", + # "total": "0.001", + # "leverage": "20", + # "curRealisedPnl": "0", + # "avgPrice": "107410.3", + # "positionStatus": "normal", + # "unrealisedPnl": "0.0047", + # "liquidationPrice": "0", + # "mmr": "0.004", + # "profitRate": "0.0008647337475591", + # "markPrice": "107415.3", + # "breakEvenPrice": "107539.2", + # "totalFunding": "0", + # "openFeeTotal": "-0.06444618", + # "closeFeeTotal": "0", + # "createdTime": "1750495670699", + # "updatedTime": "1750929883465" + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + result = self.safe_list(data, 'list', []) + else: + request['marginCoin'] = market['settleId'] + request['productType'] = productType + response = self.privateMixGetV2MixPositionSinglePosition(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700807531673, + # "data": [ + # { + # "marginCoin": "USDT", + # "symbol": "BTCUSDT", + # "holdSide": "long", + # "openDelegateSize": "0", + # "marginSize": "3.73555", + # "available": "0.002", + # "locked": "0", + # "total": "0.002", + # "leverage": "20", + # "achievedProfits": "0", + # "openPriceAvg": "37355.5", + # "marginMode": "crossed", + # "posMode": "hedge_mode", + # "unrealizedPL": "0.007", + # "liquidationPrice": "31724.970702417", + # "keepMarginRate": "0.004", + # "markPrice": "37359", + # "marginRatio": "0.029599540355", + # "cTime": "1700807507275" + # } + # ] + # } + # + result = self.safe_list(response, 'data', []) + first = self.safe_dict(result, 0, {}) + return self.parse_position(first, market) + + def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://www.bitget.com/api-doc/contract/position/get-all-position + https://www.bitget.com/api-doc/contract/position/Get-History-Position + https://www.bitget.com/api-doc/uta/trade/Get-Position + + :param str[] [symbols]: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginCoin]: the settle currency of the positions, needs to match the productType + :param str [params.productType]: 'USDT-FUTURES', 'USDC-FUTURES', 'COIN-FUTURES', 'SUSDT-FUTURES', 'SUSDC-FUTURES' or 'SCOIN-FUTURES' + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param boolean [params.useHistoryEndpoint]: default False, when True will use the historic endpoint to fetch positions + :param str [params.method]: either(default) 'privateMixGetV2MixPositionAllPosition', 'privateMixGetV2MixPositionHistoryPosition', or 'privateUtaGetV3PositionCurrentPosition' + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :returns dict[]: a list of `position structure ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchPositions', 'paginate') + if paginate: + return self.fetch_paginated_call_cursor('fetchPositions', None, None, None, params, 'endId', 'idLessThan') + method = None + useHistoryEndpoint = self.safe_bool(params, 'useHistoryEndpoint', False) + if useHistoryEndpoint: + method = 'privateMixGetV2MixPositionHistoryPosition' + else: + method, params = self.handle_option_and_params(params, 'fetchPositions', 'method', 'privateMixGetV2MixPositionAllPosition') + market = None + if symbols is not None: + first = self.safe_string(symbols, 0) + # symbols can be None or [] + if first is not None: + market = self.market(first) + productType = None + productType, params = self.handle_product_type_and_params(market, params) + request: dict = {} + response = None + isHistory = False + uta = None + uta, params = self.handle_option_and_params(params, 'fetchPositions', 'uta', False) + if uta: + request['category'] = productType + response = self.privateUtaGetV3PositionCurrentPosition(self.extend(request, params)) + elif method == 'privateMixGetV2MixPositionAllPosition': + marginCoin = self.safe_string(params, 'marginCoin', 'USDT') + if market is not None: + marginCoin = market['settleId'] + elif productType == 'USDT-FUTURES': + marginCoin = 'USDT' + elif productType == 'USDC-FUTURES': + marginCoin = 'USDC' + elif productType == 'SUSDT-FUTURES': + marginCoin = 'SUSDT' + elif productType == 'SUSDC-FUTURES': + marginCoin = 'SUSDC' + elif (productType == 'SCOIN-FUTURES') or (productType == 'COIN-FUTURES'): + if marginCoin is None: + raise ArgumentsRequired(self.id + ' fetchPositions() requires a marginCoin parameter that matches the productType') + request['marginCoin'] = marginCoin + request['productType'] = productType + response = self.privateMixGetV2MixPositionAllPosition(self.extend(request, params)) + else: + isHistory = True + if market is not None: + request['symbol'] = market['id'] + request['productType'] = productType + response = self.privateMixGetV2MixPositionHistoryPosition(self.extend(request, params)) + # + # privateMixGetV2MixPositionAllPosition + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700807810221, + # "data": [ + # { + # "marginCoin": "USDT", + # "symbol": "BTCUSDT", + # "holdSide": "long", + # "openDelegateSize": "0", + # "marginSize": "3.73555", + # "available": "0.002", + # "locked": "0", + # "total": "0.002", + # "leverage": "20", + # "achievedProfits": "0", + # "openPriceAvg": "37355.5", + # "marginMode": "crossed", + # "posMode": "hedge_mode", + # "unrealizedPL": "0.03", + # "liquidationPrice": "31725.023602417", + # "keepMarginRate": "0.004", + # "markPrice": "37370.5", + # "marginRatio": "0.029550120396", + # "cTime": "1700807507275" + # } + # ] + # } + # + # privateMixGetV2MixPositionHistoryPosition + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700808051002, + # "data": { + # "list": [ + # { + # "symbol": "BTCUSDT", + # "marginCoin": "USDT", + # "holdSide": "long", + # "openAvgPrice": "37272.1", + # "closeAvgPrice": "37271.4", + # "marginMode": "crossed", + # "openTotalPos": "0.001", + # "closeTotalPos": "0.001", + # "pnl": "-0.0007", + # "netProfit": "-0.0454261", + # "totalFunding": "0", + # "openFee": "-0.02236326", + # "closeFee": "-0.02236284", + # "utime": "1700720700400", + # "ctime": "1700720651684" + # }, + # ], + # "endId": "1099351653866962944" + # } + # } + # + # privateUtaGetV3PositionCurrentPosition + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1750929905423, + # "data": { + # "list": [ + # { + # "category": "USDT-FUTURES", + # "symbol": "BTCUSDT", + # "marginCoin": "USDT", + # "holdMode": "hedge_mode", + # "posSide": "long", + # "marginMode": "crossed", + # "positionBalance": "5.435199", + # "available": "0.001", + # "frozen": "0", + # "total": "0.001", + # "leverage": "20", + # "curRealisedPnl": "0", + # "avgPrice": "107410.3", + # "positionStatus": "normal", + # "unrealisedPnl": "0.0047", + # "liquidationPrice": "0", + # "mmr": "0.004", + # "profitRate": "0.0008647337475591", + # "markPrice": "107415.3", + # "breakEvenPrice": "107539.2", + # "totalFunding": "0", + # "openFeeTotal": "-0.06444618", + # "closeFeeTotal": "0", + # "createdTime": "1750495670699", + # "updatedTime": "1750929883465" + # } + # ] + # } + # } + # + position = [] + if uta or isHistory: + data = self.safe_dict(response, 'data', {}) + position = self.safe_list(data, 'list', []) + else: + position = self.safe_list(response, 'data', []) + result = [] + for i in range(0, len(position)): + result.append(self.parse_position(position[i], market)) + symbols = self.market_symbols(symbols) + return self.filter_by_array_positions(result, 'symbol', symbols, False) + + def parse_position(self, position: dict, market: Market = None): + # + # fetchPosition + # + # { + # "marginCoin": "USDT", + # "symbol": "BTCUSDT", + # "holdSide": "long", + # "openDelegateSize": "0", + # "marginSize": "3.73555", + # "available": "0.002", + # "locked": "0", + # "total": "0.002", + # "leverage": "20", + # "achievedProfits": "0", + # "openPriceAvg": "37355.5", + # "marginMode": "crossed", + # "posMode": "hedge_mode", + # "unrealizedPL": "0.007", + # "liquidationPrice": "31724.970702417", + # "keepMarginRate": "0.004", + # "markPrice": "37359", + # "marginRatio": "0.029599540355", + # "cTime": "1700807507275" + # } + # + # uta: fetchPosition + # + # { + # "category": "USDT-FUTURES", + # "symbol": "BTCUSDT", + # "marginCoin": "USDT", + # "holdMode": "hedge_mode", + # "posSide": "long", + # "marginMode": "crossed", + # "positionBalance": "5.435199", + # "available": "0.001", + # "frozen": "0", + # "total": "0.001", + # "leverage": "20", + # "curRealisedPnl": "0", + # "avgPrice": "107410.3", + # "positionStatus": "normal", + # "unrealisedPnl": "0.0047", + # "liquidationPrice": "0", + # "mmr": "0.004", + # "profitRate": "0.0008647337475591", + # "markPrice": "107415.3", + # "breakEvenPrice": "107539.2", + # "totalFunding": "0", + # "openFeeTotal": "-0.06444618", + # "closeFeeTotal": "0", + # "createdTime": "1750495670699", + # "updatedTime": "1750929883465" + # } + # + # fetchPositions: privateMixGetV2MixPositionAllPosition + # + # { + # "marginCoin": "USDT", + # "symbol": "BTCUSDT", + # "holdSide": "long", + # "openDelegateSize": "0", + # "marginSize": "3.73555", + # "available": "0.002", + # "locked": "0", + # "total": "0.002", + # "leverage": "20", + # "achievedProfits": "0", + # "openPriceAvg": "37355.5", + # "marginMode": "crossed", + # "posMode": "hedge_mode", + # "unrealizedPL": "0.03", + # "liquidationPrice": "31725.023602417", + # "keepMarginRate": "0.004", + # "markPrice": "37370.5", + # "marginRatio": "0.029550120396", + # "cTime": "1700807507275" + # } + # + # fetchPositionsHistory: privateMixGetV2MixPositionHistoryPosition + # + # { + # "symbol": "BTCUSDT", + # "marginCoin": "USDT", + # "holdSide": "long", + # "openAvgPrice": "37272.1", + # "closeAvgPrice": "37271.4", + # "marginMode": "crossed", + # "openTotalPos": "0.001", + # "closeTotalPos": "0.001", + # "pnl": "-0.0007", + # "netProfit": "-0.0454261", + # "totalFunding": "0", + # "openFee": "-0.02236326", + # "closeFee": "-0.02236284", + # "utime": "1700720700400", + # "ctime": "1700720651684" + # } + # + # closeAllPositions + # + # { + # "orderId": "1120923953904893955", + # "clientOid": "1120923953904893956" + # } + # + # uta: fetchPositionsHistory + # + # { + # "positionId": "1322441328637100049", + # "category": "USDT-FUTURES", + # "symbol": "BTCUSDT", + # "marginCoin": "USDT", + # "holdMode": "hedge_mode", + # "posSide": "long", + # "marginMode": "crossed", + # "openPriceAvg": "107003.7", + # "closePriceAvg": "107005.4", + # "openTotalPos": "0.0001", + # "closeTotalPos": "0.0001", + # "cumRealisedPnl": "0.00017", + # "netProfit": "-0.01267055", + # "totalFunding": "0", + # "openFeeTotal": "-0.00642022", + # "closeFeeTotal": "-0.00642032", + # "createdTime": "1751020503195", + # "updatedTime": "1751020520458" + # } + # + marketId = self.safe_string(position, 'symbol') + market = self.safe_market(marketId, market, None, 'contract') + symbol = market['symbol'] + timestamp = self.safe_integer_n(position, ['cTime', 'ctime', 'createdTime']) + marginMode = self.safe_string(position, 'marginMode') + collateral = None + initialMargin = None + unrealizedPnl = self.safe_string_2(position, 'unrealizedPL', 'unrealisedPnl') + rawCollateral = self.safe_string_2(position, 'marginSize', 'positionBalance') + if marginMode == 'isolated': + collateral = Precise.string_add(rawCollateral, unrealizedPnl) + elif marginMode == 'crossed': + marginMode = 'cross' + initialMargin = rawCollateral + holdMode = self.safe_string_2(position, 'posMode', 'holdMode') + hedged = None + if holdMode == 'hedge_mode': + hedged = True + elif holdMode == 'one_way_mode': + hedged = False + side = self.safe_string_2(position, 'holdSide', 'posSide') + leverage = self.safe_string(position, 'leverage') + contractSizeNumber = self.safe_value(market, 'contractSize') + contractSize = self.number_to_string(contractSizeNumber) + baseAmount = self.safe_string_2(position, 'total', 'openTotalPos') + entryPrice = self.safe_string_n(position, ['openPriceAvg', 'openAvgPrice', 'avgPrice']) + maintenanceMarginPercentage = self.safe_string(position, 'keepMarginRate') + openNotional = Precise.string_mul(entryPrice, baseAmount) + if initialMargin is None: + initialMargin = Precise.string_div(openNotional, leverage) + contracts = self.parse_number(Precise.string_div(baseAmount, contractSize)) + if contracts is None: + contracts = self.safe_number(position, 'closeTotalPos') + markPrice = self.safe_string(position, 'markPrice') + notional = Precise.string_mul(baseAmount, markPrice) + initialMarginPercentage = Precise.string_div(initialMargin, notional) + liquidationPrice = self.parse_number(self.omit_zero(self.safe_string(position, 'liquidationPrice'))) + calcTakerFeeRate = '0.0006' + calcTakerFeeMult = '0.9994' + if (liquidationPrice is None) and (marginMode == 'isolated') and Precise.string_gt(baseAmount, '0'): + signedMargin = Precise.string_div(rawCollateral, baseAmount) + signedMmp = maintenanceMarginPercentage + if side == 'short': + signedMargin = Precise.string_neg(signedMargin) + signedMmp = Precise.string_neg(signedMmp) + mmrMinusOne = Precise.string_sub('1', signedMmp) + numerator = Precise.string_sub(entryPrice, signedMargin) + if side == 'long': + mmrMinusOne = Precise.string_mul(mmrMinusOne, calcTakerFeeMult) + else: + numerator = Precise.string_mul(numerator, calcTakerFeeMult) + liquidationPrice = self.parse_number(Precise.string_div(numerator, mmrMinusOne)) + feeToClose = Precise.string_mul(notional, calcTakerFeeRate) + maintenanceMargin = Precise.string_add(Precise.string_mul(maintenanceMarginPercentage, notional), feeToClose) + percentage = Precise.string_mul(Precise.string_div(unrealizedPnl, initialMargin, 4), '100') + return self.safe_position({ + 'info': position, + 'id': self.safe_string_2(position, 'orderId', 'positionId'), + 'symbol': symbol, + 'notional': self.parse_number(notional), + 'marginMode': marginMode, + 'liquidationPrice': liquidationPrice, + 'entryPrice': self.parse_number(entryPrice), + 'unrealizedPnl': self.parse_number(unrealizedPnl), + 'realizedPnl': self.safe_number_n(position, ['pnl', 'curRealisedPnl', 'cumRealisedPnl']), + 'percentage': self.parse_number(percentage), + 'contracts': contracts, + 'contractSize': contractSizeNumber, + 'markPrice': self.parse_number(markPrice), + 'lastPrice': self.safe_number_2(position, 'closeAvgPrice', 'closePriceAvg'), + 'side': side, + 'hedged': hedged, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastUpdateTimestamp': self.safe_integer_2(position, 'utime', 'updatedTime'), + 'maintenanceMargin': self.parse_number(maintenanceMargin), + 'maintenanceMarginPercentage': self.parse_number(maintenanceMarginPercentage), + 'collateral': self.parse_number(collateral), + 'initialMargin': self.parse_number(initialMargin), + 'initialMarginPercentage': self.parse_number(initialMarginPercentage), + 'leverage': self.parse_number(leverage), + 'marginRatio': self.safe_number_2(position, 'marginRatio', 'mmr'), + 'stopLossPrice': None, + 'takeProfitPrice': None, + }) + + def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical funding rate prices + + https://www.bitget.com/api-doc/contract/market/Get-History-Funding-Rate + https://www.bitget.com/api-doc/uta/public/Get-History-Funding-Rate + + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: timestamp in ms of the earliest funding rate to fetch + :param int [limit]: the maximum amount of funding rate structures to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `funding rate structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchFundingRateHistory() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + productType = None + uta = None + response = None + result = None + productType, params = self.handle_product_type_and_params(market, params) + uta, params = self.handle_option_and_params(params, 'fetchFundingRateHistory', 'uta', False) + if uta: + if limit is not None: + request['limit'] = limit + request['category'] = productType + response = self.publicUtaGetV3MarketHistoryFundRate(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1750435113658, + # "data": { + # "resultList": [ + # { + # "symbol": "BTCUSDT", + # "fundingRate": "-0.000017", + # "fundingRateTimestamp": "1750431600000" + # }, + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + result = self.safe_list(data, 'resultList', []) + else: + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchFundingRateHistory', 'paginate') + if paginate: + return self.fetch_paginated_call_incremental('fetchFundingRateHistory', symbol, since, limit, params, 'pageNo', 100) + if limit is not None: + request['pageSize'] = limit + request['productType'] = productType + response = self.publicMixGetV2MixMarketHistoryFundRate(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1652406728393, + # "data": [ + # { + # "symbol": "BTCUSDT", + # "fundingRate": "-0.0003", + # "fundingTime": "1652396400000" + # }, + # ] + # } + # + result = self.safe_list(response, 'data', []) + rates = [] + for i in range(0, len(result)): + entry = result[i] + marketId = self.safe_string(entry, 'symbol') + symbolInner = self.safe_symbol(marketId, market) + timestamp = self.safe_integer_2(entry, 'fundingTime', 'fundingRateTimestamp') + rates.append({ + 'info': entry, + 'symbol': symbolInner, + 'fundingRate': self.safe_number(entry, 'fundingRate'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + sorted = self.sort_by(rates, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, market['symbol'], since, limit) + + def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate + + https://www.bitget.com/api-doc/contract/market/Get-Current-Funding-Rate + https://www.bitget.com/api-doc/contract/market/Get-Symbol-Next-Funding-Time + https://www.bitget.com/api-doc/uta/public/Get-Current-Funding-Rate + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :param str [params.method]: either(default) 'publicMixGetV2MixMarketCurrentFundRate' or 'publicMixGetV2MixMarketFundingTime' + :returns dict: a `funding rate structure ` + """ + self.load_markets() + market = self.market(symbol) + if not market['swap']: + raise BadSymbol(self.id + ' fetchFundingRate() supports swap contracts only') + productType = None + productType, params = self.handle_product_type_and_params(market, params) + request: dict = { + 'symbol': market['id'], + } + uta = None + response = None + uta, params = self.handle_option_and_params(params, 'fetchFundingRate', 'uta', False) + if uta: + response = self.publicUtaGetV3MarketCurrentFundRate(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1750897372153, + # "data": [ + # { + # "symbol": "BTCUSDT", + # "fundingRate": "0.00001", + # "fundingRateInterval": "8", + # "nextUpdate": "1750924800000", + # "minFundingRate": "-0.003", + # "maxFundingRate": "0.003" + # } + # ] + # } + # + else: + request['productType'] = productType + method = None + method, params = self.handle_option_and_params(params, 'fetchFundingRate', 'method', 'publicMixGetV2MixMarketCurrentFundRate') + if method == 'publicMixGetV2MixMarketCurrentFundRate': + response = self.publicMixGetV2MixMarketCurrentFundRate(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1745500709429, + # "data": [ + # { + # "symbol": "BTCUSDT", + # "fundingRate": "-0.000013", + # "fundingRateInterval": "8", + # "nextUpdate": "1745510400000", + # "minFundingRate": "-0.003", + # "maxFundingRate": "0.003" + # } + # ] + # } + # + elif method == 'publicMixGetV2MixMarketFundingTime': + response = self.publicMixGetV2MixMarketFundingTime(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1745402092428, + # "data": [ + # { + # "symbol": "BTCUSDT", + # "nextFundingTime": "1745424000000", + # "ratePeriod": "8" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_funding_rate(data[0], market) + + def fetch_funding_rates(self, symbols: Strings = None, params={}) -> FundingRates: + """ + fetch the current funding rates for all markets + + https://www.bitget.com/api-doc/contract/market/Get-All-Symbol-Ticker + + :param str[] [symbols]: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.subType]: *contract only* 'linear', 'inverse' + :param str [params.productType]: *contract only* 'USDT-FUTURES', 'USDC-FUTURES', 'COIN-FUTURES', 'SUSDT-FUTURES', 'SUSDC-FUTURES' or 'SCOIN-FUTURES' + :param str [params.method]: either(default) 'publicMixGetV2MixMarketTickers' or 'publicMixGetV2MixMarketCurrentFundRate' + :returns dict: a dictionary of `funding rate structures `, indexed by market symbols + """ + self.load_markets() + market = None + if symbols is not None: + symbol = self.safe_value(symbols, 0) + market = self.market(symbol) + request: dict = {} + productType = None + productType, params = self.handle_product_type_and_params(market, params) + method = 'publicMixGetV2MixMarketTickers' + method, params = self.handle_option_and_params(params, 'fetchFundingRates', 'method', method) + response = None + request['productType'] = productType + if method == 'publicMixGetV2MixMarketTickers': + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700533773477, + # "data": [ + # { + # "symbol": "BTCUSD", + # "lastPr": "29904.5", + # "askPr": "29904.5", + # "bidPr": "29903.5", + # "bidSz": "0.5091", + # "askSz": "2.2694", + # "high24h": "0", + # "low24h": "0", + # "ts": "1695794271400", + # "change24h": "0", + # "baseVolume": "0", + # "quoteVolume": "0", + # "usdtVolume": "0", + # "openUtc": "0", + # "changeUtc24h": "0", + # "indexPrice": "29132.353333", + # "fundingRate": "-0.0007", + # "holdingAmount": "125.6844", + # "deliveryStartTime": null, + # "deliveryTime": null, + # "deliveryStatus": "delivery_normal", + # "open24h": "0", + # "markPrice": "12345" + # }, + # ] + # } + response = self.publicMixGetV2MixMarketTickers(self.extend(request, params)) + elif method == 'publicMixGetV2MixMarketCurrentFundRate': + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime":1761659449917, + # "data":[ + # { + # "symbol": "BTCUSDT", + # "fundingRate": "-0.000024", + # "fundingRateInterval": "8", + # "nextUpdate": "1761667200000", + # "minFundingRate": "-0.003", + # "maxFundingRate": "0.003" + # } + # ] + # } + # + response = self.publicMixGetV2MixMarketCurrentFundRate(self.extend(request, params)) + symbols = self.market_symbols(symbols) + data = self.safe_list(response, 'data', []) + return self.parse_funding_rates(data, symbols) + + def fetch_funding_intervals(self, symbols: Strings = None, params={}) -> FundingRates: + """ + fetch the funding rate interval for multiple markets + + https://www.bitget.com/api-doc/contract/market/Get-All-Symbol-Ticker + + :param str[] [symbols]: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.productType]: 'USDT-FUTURES'(default), 'USDC-FUTURES', 'COIN-FUTURES', 'SUSDT-FUTURES', 'SUSDC-FUTURES' or 'SCOIN-FUTURES' + :returns dict[]: a list of `funding rate structures ` + """ + self.load_markets() + params = self.extend({'method': 'publicMixGetV2MixMarketCurrentFundRate'}, params) + return self.fetch_funding_rates(symbols, params) + + def parse_funding_rate(self, contract, market: Market = None) -> FundingRate: + # + # fetchFundingRate: publicMixGetV2MixMarketCurrentFundRate, publicUtaGetV3MarketCurrentFundRate + # + # { + # "symbol": "BTCUSDT", + # "fundingRate": "-0.000013", + # "fundingRateInterval": "8", + # "nextUpdate": "1745510400000", + # "minFundingRate": "-0.003", + # "maxFundingRate": "0.003" + # } + # + # fetchFundingRate: publicMixGetV2MixMarketFundingTime + # + # { + # "symbol": "BTCUSDT", + # "nextFundingTime": "1745424000000", + # "ratePeriod": "8" + # } + # + # fetchFundingInterval + # + # { + # "symbol": "BTCUSDT", + # "nextFundingTime": "1727942400000", + # "ratePeriod": "8" + # } + # + # fetchFundingRates + # + # { + # "symbol": "BTCUSD", + # "lastPr": "29904.5", + # "askPr": "29904.5", + # "bidPr": "29903.5", + # "bidSz": "0.5091", + # "askSz": "2.2694", + # "high24h": "0", + # "low24h": "0", + # "ts": "1695794271400", + # "change24h": "0", + # "baseVolume": "0", + # "quoteVolume": "0", + # "usdtVolume": "0", + # "openUtc": "0", + # "changeUtc24h": "0", + # "indexPrice": "29132.353333", + # "fundingRate": "-0.0007", + # "holdingAmount": "125.6844", + # "deliveryStartTime": null, + # "deliveryTime": null, + # "deliveryStatus": "delivery_normal", + # "open24h": "0", + # "markPrice": "12345" + # } + # + marketId = self.safe_string(contract, 'symbol') + symbol = self.safe_symbol(marketId, market, None, 'swap') + fundingTimestamp = self.safe_integer_2(contract, 'nextFundingTime', 'nextUpdate') + interval = self.safe_string_2(contract, 'ratePeriod', 'fundingRateInterval') + timestamp = self.safe_integer(contract, 'ts') + markPrice = self.safe_number(contract, 'markPrice') + indexPrice = self.safe_number(contract, 'indexPrice') + intervalString = None + if interval is not None: + intervalString = interval + 'h' + return { + 'info': contract, + 'symbol': symbol, + 'markPrice': markPrice, + 'indexPrice': indexPrice, + 'interestRate': None, + 'estimatedSettlePrice': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'fundingRate': self.safe_number(contract, 'fundingRate'), + 'fundingTimestamp': fundingTimestamp, + 'fundingDatetime': self.iso8601(fundingTimestamp), + 'nextFundingRate': None, + 'nextFundingTimestamp': None, + 'nextFundingDatetime': None, + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': intervalString, + } + + def fetch_funding_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[FundingHistory]: + """ + fetch the funding history + + https://www.bitget.com/api-doc/contract/account/Get-Account-Bill + + :param str symbol: unified market symbol + :param int [since]: the starting timestamp in milliseconds + :param int [limit]: the number of entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch funding history for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `funding history structures ` + """ + self.load_markets() + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchFundingHistory() requires a symbol argument') + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchFundingHistory', 'paginate') + if paginate: + return self.fetch_paginated_call_cursor('fetchFundingHistory', symbol, since, limit, params, 'endId', 'idLessThan') + market = self.market(symbol) + if not market['swap']: + raise BadSymbol(self.id + ' fetchFundingHistory() supports swap contracts only') + productType = None + productType, params = self.handle_product_type_and_params(market, params) + request: dict = { + 'symbol': market['id'], + 'marginCoin': market['settleId'], + 'businessType': 'contract_settle_fee', + 'productType': productType, + } + request, params = self.handle_until_option('endTime', request, params) + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + response = self.privateMixGetV2MixAccountBill(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700795977890, + # "data": { + # "bills": [ + # { + # "billId": "1111499428100472833", + # "symbol": "BTCUSDT", + # "amount": "-0.004992", + # "fee": "0", + # "feeByCoupon": "", + # "businessType": "contract_settle_fee", + # "coin": "USDT", + # "cTime": "1700728034996" + # }, + # ], + # "endId": "1098396773329305606" + # } + # } + # + data = self.safe_value(response, 'data', {}) + result = self.safe_value(data, 'bills', []) + return self.parse_funding_histories(result, market, since, limit) + + def parse_funding_history(self, contract, market: Market = None): + # + # { + # "billId": "1111499428100472833", + # "symbol": "BTCUSDT", + # "amount": "-0.004992", + # "fee": "0", + # "feeByCoupon": "", + # "businessType": "contract_settle_fee", + # "coin": "USDT", + # "cTime": "1700728034996" + # } + # + marketId = self.safe_string(contract, 'symbol') + currencyId = self.safe_string(contract, 'coin') + timestamp = self.safe_integer(contract, 'cTime') + return { + 'info': contract, + 'symbol': self.safe_symbol(marketId, market, None, 'swap'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'code': self.safe_currency_code(currencyId), + 'amount': self.safe_number(contract, 'amount'), + 'id': self.safe_string(contract, 'billId'), + } + + def parse_funding_histories(self, contracts, market=None, since: Int = None, limit: Int = None) -> List[FundingHistory]: + result = [] + for i in range(0, len(contracts)): + contract = contracts[i] + business = self.safe_string(contract, 'businessType') + if business != 'contract_settle_fee': + continue + result.append(self.parse_funding_history(contract, market)) + sorted = self.sort_by(result, 'timestamp') + symbol = None + if market is not None: + symbol = market['symbol'] + return self.filter_by_symbol_since_limit(sorted, symbol, since, limit) + + def modify_margin_helper(self, symbol: str, amount, type, params={}) -> MarginModification: + self.load_markets() + holdSide = self.safe_string(params, 'holdSide') + market = self.market(symbol) + productType = None + productType, params = self.handle_product_type_and_params(market, params) + request: dict = { + 'symbol': market['id'], + 'marginCoin': market['settleId'], + 'amount': self.amount_to_precision(symbol, amount), # positive value for adding margin, negative for reducing + 'holdSide': holdSide, # long or short + 'productType': productType, + } + params = self.omit(params, 'holdSide') + response = self.privateMixPostV2MixAccountSetMargin(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700813444618, + # "data": "" + # } + # + return self.extend(self.parse_margin_modification(response, market), { + 'amount': self.parse_number(amount), + 'type': type, + }) + + def parse_margin_modification(self, data: dict, market: Market = None) -> MarginModification: + # + # addMargin/reduceMargin + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700813444618, + # "data": "" + # } + # + errorCode = self.safe_string(data, 'code') + status = 'ok' if (errorCode == '00000') else 'failed' + return { + 'info': data, + 'symbol': market['symbol'], + 'type': None, + 'marginMode': 'isolated', + 'amount': None, + 'total': None, + 'code': market['settle'], + 'status': status, + 'timestamp': None, + 'datetime': None, + } + + def reduce_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + remove margin from a position + + https://www.bitget.com/api-doc/contract/account/Change-Margin + + :param str symbol: unified market symbol + :param float amount: the amount of margin to remove + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin structure ` + """ + if amount > 0: + raise BadRequest(self.id + ' reduceMargin() amount parameter must be a negative value') + holdSide = self.safe_string(params, 'holdSide') + if holdSide is None: + raise ArgumentsRequired(self.id + ' reduceMargin() requires a holdSide parameter, either long or short') + return self.modify_margin_helper(symbol, amount, 'reduce', params) + + def add_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + add margin + + https://www.bitget.com/api-doc/contract/account/Change-Margin + + :param str symbol: unified market symbol + :param float amount: the amount of margin to add + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin structure ` + """ + holdSide = self.safe_string(params, 'holdSide') + if holdSide is None: + raise ArgumentsRequired(self.id + ' addMargin() requires a holdSide parameter, either long or short') + return self.modify_margin_helper(symbol, amount, 'add', params) + + def fetch_leverage(self, symbol: str, params={}) -> Leverage: + """ + fetch the set leverage for a market + + https://www.bitget.com/api-doc/contract/account/Get-Single-Account + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `leverage structure ` + """ + self.load_markets() + market = self.market(symbol) + productType = None + productType, params = self.handle_product_type_and_params(market, params) + request: dict = { + 'symbol': market['id'], + 'marginCoin': market['settleId'], + 'productType': productType, + } + response = self.privateMixGetV2MixAccountAccount(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1709366911964, + # "data": { + # "marginCoin": "USDT", + # "locked": "0", + # "available": "0", + # "crossedMaxAvailable": "0", + # "isolatedMaxAvailable": "0", + # "maxTransferOut": "0", + # "accountEquity": "0", + # "usdtEquity": "0.000000009166", + # "btcEquity": "0", + # "crossedRiskRate": "0", + # "crossedMarginLeverage": 20, + # "isolatedLongLever": 20, + # "isolatedShortLever": 20, + # "marginMode": "crossed", + # "posMode": "hedge_mode", + # "unrealizedPL": "0", + # "coupon": "0", + # "crossedUnrealizedPL": "0", + # "isolatedUnrealizedPL": "" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_leverage(data, market) + + def parse_leverage(self, leverage: dict, market: Market = None) -> Leverage: + isCrossMarginMode = self.safe_string(leverage, 'marginMode') == 'crossed' + longLevKey = 'crossedMarginLeverage' if isCrossMarginMode else 'isolatedLongLever' + shortLevKey = 'crossedMarginLeverage' if isCrossMarginMode else 'isolatedShortLever' + return { + 'info': leverage, + 'symbol': market['symbol'], + 'marginMode': 'cross' if isCrossMarginMode else 'isolated', + 'longLeverage': self.safe_integer(leverage, longLevKey), + 'shortLeverage': self.safe_integer(leverage, shortLevKey), + } + + def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://www.bitget.com/api-doc/contract/account/Change-Leverage + https://www.bitget.com/api-doc/uta/account/Change-Leverage + + :param int leverage: the rate of leverage + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.holdSide]: *isolated only* position direction, 'long' or 'short' + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :param boolean [params.posSide]: required for uta isolated margin, long or short + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setLeverage() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + productType = None + productType, params = self.handle_product_type_and_params(market, params) + request: dict = { + 'symbol': market['id'], + 'leverage': self.number_to_string(leverage), + } + uta = None + response = None + uta, params = self.handle_option_and_params(params, 'setLeverage', 'uta', False) + if uta: + if productType == 'SPOT': + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchTrades', params) + if marginMode is not None: + productType = 'MARGIN' + request['coin'] = market['settleId'] + request['category'] = productType + response = self.privateUtaPostV3AccountSetLeverage(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1752815940833, + # "data": "success" + # } + # + else: + request['marginCoin'] = market['settleId'] + request['productType'] = productType + response = self.privateMixPostV2MixAccountSetLeverage(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700864711517, + # "data": { + # "symbol": "BTCUSDT", + # "marginCoin": "USDT", + # "longLeverage": "25", + # "shortLeverage": "25", + # "crossMarginLeverage": "25", + # "marginMode": "crossed" + # } + # } + # + return response + + def set_margin_mode(self, marginMode: str, symbol: Str = None, params={}): + """ + set margin mode to 'cross' or 'isolated' + + https://www.bitget.com/api-doc/contract/account/Change-Margin-Mode + + :param str marginMode: 'cross' or 'isolated' + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setMarginMode() requires a symbol argument') + marginMode = marginMode.lower() + if marginMode == 'cross': + marginMode = 'crossed' + if (marginMode != 'isolated') and (marginMode != 'crossed'): + raise ArgumentsRequired(self.id + ' setMarginMode() marginMode must be either isolated or crossed(cross)') + self.load_markets() + market = self.market(symbol) + productType = None + productType, params = self.handle_product_type_and_params(market, params) + request: dict = { + 'symbol': market['id'], + 'marginCoin': market['settleId'], + 'marginMode': marginMode, + 'productType': productType, + } + response = self.privateMixPostV2MixAccountSetMarginMode(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700865205552, + # "data": { + # "symbol": "BTCUSDT", + # "marginCoin": "USDT", + # "longLeverage": "20", + # "shortLeverage": "3", + # "marginMode": "isolated" + # } + # } + # + return response + + def set_position_mode(self, hedged: bool, symbol: Str = None, params={}): + """ + set hedged to True or False for a market + + https://www.bitget.com/api-doc/contract/account/Change-Hold-Mode + https://www.bitget.com/api-doc/uta/account/Change-Position-Mode + + :param bool hedged: set to True to use dualSidePosition + :param str symbol: not used by bitget setPositionMode() + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.productType]: required if not uta and symbol is None: 'USDT-FUTURES', 'USDC-FUTURES', 'COIN-FUTURES', 'SUSDT-FUTURES', 'SUSDC-FUTURES' or 'SCOIN-FUTURES' + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :returns dict: response from the exchange + """ + self.load_markets() + posMode = 'hedge_mode' if hedged else 'one_way_mode' + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + productType = None + uta = None + response = None + productType, params = self.handle_product_type_and_params(market, params) + uta, params = self.handle_option_and_params(params, 'setPositionMode', 'uta', False) + if uta: + request['holdMode'] = posMode + response = self.privateUtaPostV3AccountSetHoldMode(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1752816734592, + # "data": "success" + # } + # + else: + request['posMode'] = posMode + request['productType'] = productType + response = self.privateMixPostV2MixAccountSetPositionMode(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700865608009, + # "data": { + # "posMode": "hedge_mode" + # } + # } + # + return response + + def fetch_open_interest(self, symbol: str, params={}): + """ + retrieves the open interest of a contract trading pair + + https://www.bitget.com/api-doc/contract/market/Get-Open-Interest + https://www.bitget.com/api-doc/uta/public/Get-Open-Interest + + :param str symbol: unified CCXT market symbol + :param dict [params]: exchange specific parameters + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :returns dict} an open interest structure{@link https://docs.ccxt.com/#/?id=open-interest-structure: + """ + self.load_markets() + market = self.market(symbol) + if not market['contract']: + raise BadRequest(self.id + ' fetchOpenInterest() supports contract markets only') + productType = None + productType, params = self.handle_product_type_and_params(market, params) + request: dict = { + 'symbol': market['id'], + } + uta = None + response = None + uta, params = self.handle_option_and_params(params, 'fetchOpenInterest', 'uta', False) + if uta: + request['category'] = productType + response = self.publicUtaGetV3MarketOpenInterest(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1751101221545, + # "data": { + # "list": [ + # { + # "symbol": "BTCUSDT", + # "openInterest": "18166.3583" + # } + # ], + # "ts": "1751101220993" + # } + # } + # + else: + request['productType'] = productType + response = self.publicMixGetV2MixMarketOpenInterest(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700866041022, + # "data": { + # "openInterestList": [ + # { + # "symbol": "BTCUSDT", + # "size": "52234.134" + # } + # ], + # "ts": "1700866041023" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_open_interest(data, market) + + def parse_open_interest(self, interest, market: Market = None): + # + # default + # + # { + # "openInterestList": [ + # { + # "symbol": "BTCUSDT", + # "size": "52234.134" + # } + # ], + # "ts": "1700866041023" + # } + # + # uta + # + # { + # "list": [ + # { + # "symbol": "BTCUSDT", + # "openInterest": "18166.3583" + # } + # ], + # "ts": "1751101220993" + # } + # + data = self.safe_list_2(interest, 'openInterestList', 'list', []) + timestamp = self.safe_integer(interest, 'ts') + marketId = self.safe_string(data[0], 'symbol') + return self.safe_open_interest({ + 'symbol': self.safe_symbol(marketId, market, None, 'contract'), + 'openInterestAmount': self.safe_number_2(data[0], 'size', 'openInterest'), + 'openInterestValue': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'info': interest, + }, market) + + def fetch_transfers(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[TransferEntry]: + """ + fetch a history of internal transfers made on an account + + https://www.bitget.com/api-doc/spot/account/Get-Account-TransferRecords + + :param str code: unified currency code of the currency transferred + :param int [since]: the earliest time in ms to fetch transfers for + :param int [limit]: the maximum number of transfers structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch entries for + :returns dict[]: a list of `transfer structures ` + """ + if code is None: + raise ArgumentsRequired(self.id + ' fetchTransfers() requires a code argument') + self.load_markets() + type = None + type, params = self.handle_market_type_and_params('fetchTransfers', None, params) + fromAccount = self.safe_string(params, 'fromAccount', type) + params = self.omit(params, 'fromAccount') + accountsByType = self.safe_value(self.options, 'accountsByType', {}) + type = self.safe_string(accountsByType, fromAccount) + currency = self.currency(code) + request: dict = { + 'coin': currency['id'], + 'fromType': type, + } + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + request, params = self.handle_until_option('endTime', request, params) + response = self.privateSpotGetV2SpotAccountTransferRecords(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700873854651, + # "data": [ + # { + # "coin": "USDT", + # "status": "Successful", + # "toType": "crossed_margin", + # "toSymbol": "", + # "fromType": "spot", + # "fromSymbol": "", + # "size": "11.64958799", + # "ts": "1700729673028", + # "clientOid": "1111506298504744960", + # "transferId": "24930940" + # }, + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_transfers(data, currency, since, limit) + + def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://www.bitget.com/api-doc/spot/account/Wallet-Transfer + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account to transfer from + :param str toAccount: account to transfer to + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.symbol]: unified CCXT market symbol, required when transferring to or from an account type that is a leveraged position-by-position account + :param str [params.clientOid]: custom id + :returns dict: a `transfer structure ` + """ + self.load_markets() + currency = self.currency(code) + accountsByType = self.safe_value(self.options, 'accountsByType', {}) + fromType = self.safe_string(accountsByType, fromAccount) + toType = self.safe_string(accountsByType, toAccount) + request: dict = { + 'fromType': fromType, + 'toType': toType, + 'amount': amount, + 'coin': currency['id'], + } + symbol = self.safe_string(params, 'symbol') + params = self.omit(params, 'symbol') + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + response = self.privateSpotPostV2SpotWalletTransfer(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700874302021, + # "data": { + # "transferId": "1112112916581847040", + # "clientOrderId": null + # } + # } + # + data = self.safe_value(response, 'data', {}) + data['ts'] = self.safe_integer(response, 'requestTime') + return self.parse_transfer(data, currency) + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + # + # transfer + # + # { + # "transferId": "1112112916581847040", + # "clientOrderId": null, + # "ts": 1700874302021 + # } + # + # fetchTransfers + # + # { + # "coin": "USDT", + # "status": "Successful", + # "toType": "crossed_margin", + # "toSymbol": "", + # "fromType": "spot", + # "fromSymbol": "", + # "size": "11.64958799", + # "ts": "1700729673028", + # "clientOid": "1111506298504744960", + # "transferId": "24930940" + # } + # + timestamp = self.safe_integer(transfer, 'ts') + status = self.safe_string_lower(transfer, 'status') + currencyId = self.safe_string(transfer, 'coin') + fromAccountRaw = self.safe_string(transfer, 'fromType') + accountsById = self.safe_value(self.options, 'accountsById', {}) + fromAccount = self.safe_string(accountsById, fromAccountRaw, fromAccountRaw) + toAccountRaw = self.safe_string(transfer, 'toType') + toAccount = self.safe_string(accountsById, toAccountRaw, toAccountRaw) + return { + 'info': transfer, + 'id': self.safe_string(transfer, 'transferId'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'currency': self.safe_currency_code(currencyId, currency), + 'amount': self.safe_number(transfer, 'size'), + 'fromAccount': fromAccount, + 'toAccount': toAccount, + 'status': self.parse_transfer_status(status), + } + + def parse_transfer_status(self, status: Str) -> Str: + statuses: dict = { + 'successful': 'ok', + } + return self.safe_string(statuses, status, status) + + def parse_deposit_withdraw_fee(self, fee, currency: Currency = None): + # + # { + # "chains": [ + # { + # "browserUrl": "https://blockchair.com/bitcoin/transaction/", + # "chain": "BTC", + # "depositConfirm": "1", + # "extraWithdrawFee": "0", + # "minDepositAmount": "0.0001", + # "minWithdrawAmount": "0.005", + # "needTag": "false", + # "rechargeable": "true", + # "withdrawConfirm": "1", + # "withdrawFee": "0.0004", + # "withdrawable": "true" + # }, + # ], + # "coin": "BTC", + # "coinId": "1", + # "transfer": "true"" + # } + # + chains = self.safe_value(fee, 'chains', []) + chainsLength = len(chains) + result: dict = { + 'info': fee, + 'withdraw': { + 'fee': None, + 'percentage': None, + }, + 'deposit': { + 'fee': None, + 'percentage': None, + }, + 'networks': {}, + } + for i in range(0, chainsLength): + chain = chains[i] + networkId = self.safe_string(chain, 'chain') + currencyCode = self.safe_string(currency, 'code') + networkCode = self.network_id_to_code(networkId, currencyCode) + result['networks'][networkCode] = { + 'deposit': {'fee': None, 'percentage': None}, + 'withdraw': {'fee': self.safe_number(chain, 'withdrawFee'), 'percentage': False}, + } + if chainsLength == 1: + result['withdraw']['fee'] = self.safe_number(chain, 'withdrawFee') + result['withdraw']['percentage'] = False + return result + + def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}): + """ + fetch deposit and withdraw fees + + https://www.bitget.com/api-doc/spot/market/Get-Coin-List + + :param str[]|None codes: list of unified currency codes + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `fee structures ` + """ + self.load_markets() + response = self.publicSpotGetV2SpotPublicCoins(params) + # + # { + # "code": "00000", + # "data": [ + # { + # "chains": [ + # { + # "browserUrl": "https://blockchair.com/bitcoin/transaction/", + # "chain": "BTC", + # "depositConfirm": "1", + # "extraWithdrawFee": "0", + # "minDepositAmount": "0.0001", + # "minWithdrawAmount": "0.005", + # "needTag": "false", + # "rechargeable": "true", + # "withdrawConfirm": "1", + # "withdrawFee": "0.0004", + # "withdrawable": "true" + # }, + # ], + # "coin": "BTC", + # "coinId": "1", + # "transfer": "true"" + # } + # ], + # "msg": "success", + # "requestTime": "1700120731773" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_deposit_withdraw_fees(data, codes, 'coin') + + def borrow_cross_margin(self, code: str, amount: float, params={}): + """ + create a loan to borrow margin + + https://www.bitget.com/api-doc/margin/cross/account/Cross-Borrow + + :param str code: unified currency code of the currency to borrow + :param str amount: the amount to borrow + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin loan structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'coin': currency['id'], + 'borrowAmount': self.currency_to_precision(code, amount), + } + response = self.privateMarginPostV2MarginCrossedAccountBorrow(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700876470931, + # "data": { + # "loanId": "1112122013642272769", + # "coin": "USDT", + # "borrowAmount": "4" + # } + # } + # + data = self.safe_value(response, 'data', {}) + return self.parse_margin_loan(data, currency) + + def borrow_isolated_margin(self, symbol: str, code: str, amount: float, params={}): + """ + create a loan to borrow margin + + https://www.bitget.com/api-doc/margin/isolated/account/Isolated-Borrow + + :param str symbol: unified market symbol + :param str code: unified currency code of the currency to borrow + :param str amount: the amount to borrow + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin loan structure ` + """ + self.load_markets() + currency = self.currency(code) + market = self.market(symbol) + request: dict = { + 'coin': currency['id'], + 'borrowAmount': self.currency_to_precision(code, amount), + 'symbol': market['id'], + } + response = self.privateMarginPostV2MarginIsolatedAccountBorrow(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700877255605, + # "data": { + # "loanId": "1112125304879067137", + # "symbol": "BTCUSDT", + # "coin": "USDT", + # "borrowAmount": "4" + # } + # } + # + data = self.safe_value(response, 'data', {}) + return self.parse_margin_loan(data, currency, market) + + def repay_isolated_margin(self, symbol: str, code: str, amount, params={}): + """ + repay borrowed margin and interest + + https://www.bitget.com/api-doc/margin/isolated/account/Isolated-Repay + + :param str symbol: unified market symbol + :param str code: unified currency code of the currency to repay + :param str amount: the amount to repay + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin loan structure ` + """ + self.load_markets() + currency = self.currency(code) + market = self.market(symbol) + request: dict = { + 'coin': currency['id'], + 'repayAmount': self.currency_to_precision(code, amount), + 'symbol': market['id'], + } + response = self.privateMarginPostV2MarginIsolatedAccountRepay(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700877518012, + # "data": { + # "remainDebtAmount": "0", + # "repayId": "1112126405439270912", + # "symbol": "BTCUSDT", + # "coin": "USDT", + # "repayAmount": "8.000137" + # } + # } + # + data = self.safe_value(response, 'data', {}) + return self.parse_margin_loan(data, currency, market) + + def repay_cross_margin(self, code: str, amount, params={}): + """ + repay borrowed margin and interest + + https://www.bitget.com/api-doc/margin/cross/account/Cross-Repay + + :param str code: unified currency code of the currency to repay + :param str amount: the amount to repay + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin loan structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'coin': currency['id'], + 'repayAmount': self.currency_to_precision(code, amount), + } + response = self.privateMarginPostV2MarginCrossedAccountRepay(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700876704885, + # "data": { + # "remainDebtAmount": "0", + # "repayId": "1112122994945830912", + # "coin": "USDT", + # "repayAmount": "4.00006834" + # } + # } + # + data = self.safe_value(response, 'data', {}) + return self.parse_margin_loan(data, currency) + + def parse_margin_loan(self, info, currency: Currency = None, market: Market = None): + # + # isolated: borrowMargin + # + # { + # "loanId": "1112125304879067137", + # "symbol": "BTCUSDT", + # "coin": "USDT", + # "borrowAmount": "4" + # } + # + # cross: borrowMargin + # + # { + # "loanId": "1112122013642272769", + # "coin": "USDT", + # "borrowAmount": "4" + # } + # + # isolated: repayMargin + # + # { + # "remainDebtAmount": "0", + # "repayId": "1112126405439270912", + # "symbol": "BTCUSDT", + # "coin": "USDT", + # "repayAmount": "8.000137" + # } + # + # cross: repayMargin + # + # { + # "remainDebtAmount": "0", + # "repayId": "1112122994945830912", + # "coin": "USDT", + # "repayAmount": "4.00006834" + # } + # + currencyId = self.safe_string(info, 'coin') + marketId = self.safe_string(info, 'symbol') + symbol = None + if marketId is not None: + symbol = self.safe_symbol(marketId, market, None, 'spot') + return { + 'id': self.safe_string_2(info, 'loanId', 'repayId'), + 'currency': self.safe_currency_code(currencyId, currency), + 'amount': self.safe_number_2(info, 'borrowAmount', 'repayAmount'), + 'symbol': symbol, + 'timestamp': None, + 'datetime': None, + 'info': info, + } + + def fetch_my_liquidations(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Liquidation]: + """ + retrieves the users liquidated positions + + https://www.bitget.com/api-doc/margin/cross/record/Get-Cross-Liquidation-Records + https://www.bitget.com/api-doc/margin/isolated/record/Get-Isolated-Liquidation-Records + + :param str [symbol]: unified CCXT market symbol + :param int [since]: the earliest time in ms to fetch liquidations for + :param int [limit]: the maximum number of liquidation structures to retrieve + :param dict [params]: exchange specific parameters for the bitget api endpoint + :param int [params.until]: timestamp in ms of the latest liquidation + :param str [params.marginMode]: 'cross' or 'isolated' default value is 'cross' + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict: an array of `liquidation structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchMyLiquidations', 'paginate') + if paginate: + return self.fetch_paginated_call_cursor('fetchMyLiquidations', symbol, since, limit, params, 'minId', 'idLessThan') + market = None + if symbol is not None: + market = self.market(symbol) + type = None + type, params = self.handle_market_type_and_params('fetchMyLiquidations', market, params) + if type != 'spot': + raise NotSupported(self.id + ' fetchMyLiquidations() supports spot margin markets only') + request: dict = {} + request, params = self.handle_until_option('endTime', request, params) + if since is not None: + request['startTime'] = since + else: + request['startTime'] = self.milliseconds() - 7776000000 + if limit is not None: + request['limit'] = limit + response = None + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchMyLiquidations', params, 'cross') + if marginMode == 'isolated': + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMyLiquidations() requires a symbol argument') + request['symbol'] = market['id'] + response = self.privateMarginGetV2MarginIsolatedLiquidationHistory(self.extend(request, params)) + elif marginMode == 'cross': + response = self.privateMarginGetV2MarginCrossedLiquidationHistory(self.extend(request, params)) + # + # isolated + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1698114119193, + # "data": { + # "resultList": [ + # { + # "liqId": "123", + # "symbol": "BTCUSDT", + # "liqStartTime": "1653453245342", + # "liqEndTime": "16312423423432", + # "liqRiskRatio": "1.01", + # "totalAssets": "1242.34", + # "totalDebt": "1100", + # "liqFee": "1.2", + # "uTime": "1668134458717", + # "cTime": "1653453245342" + # } + # ], + # "maxId": "0", + # "minId": "0" + # } + # } + # + # cross + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1698114119193, + # "data": { + # "resultList": [ + # { + # "liqId": "123", + # "liqStartTime": "1653453245342", + # "liqEndTime": "16312423423432", + # "liqRiskRatio": "1.01", + # "totalAssets": "1242.34", + # "totalDebt": "1100", + # "LiqFee": "1.2", + # "uTime": "1668134458717", + # "cTime": "1653453245342" + # } + # ], + # "maxId": "0", + # "minId": "0" + # } + # } + # + data = self.safe_value(response, 'data', {}) + liquidations = self.safe_list(data, 'resultList', []) + return self.parse_liquidations(liquidations, market, since, limit) + + def parse_liquidation(self, liquidation, market: Market = None): + # + # isolated + # + # { + # "liqId": "123", + # "symbol": "BTCUSDT", + # "liqStartTime": "1653453245342", + # "liqEndTime": "16312423423432", + # "liqRiskRatio": "1.01", + # "totalAssets": "1242.34", + # "totalDebt": "1100", + # "liqFee": "1.2", + # "uTime": "1692690126000" + # "cTime": "1653453245342" + # } + # + # cross + # + # { + # "liqId": "123", + # "liqStartTime": "1653453245342", + # "liqEndTime": "16312423423432", + # "liqRiskRatio": "1.01", + # "totalAssets": "1242.34", + # "totalDebt": "1100", + # "LiqFee": "1.2", + # "uTime": "1692690126000" + # "cTime": "1653453245342" + # } + # + marketId = self.safe_string(liquidation, 'symbol') + timestamp = self.safe_integer(liquidation, 'liqEndTime') + liquidationFee = self.safe_string_2(liquidation, 'LiqFee', 'liqFee') + totalDebt = self.safe_string(liquidation, 'totalDebt') + quoteValueString = Precise.string_add(liquidationFee, totalDebt) + return self.safe_liquidation({ + 'info': liquidation, + 'symbol': self.safe_symbol(marketId, market), + 'contracts': None, + 'contractSize': None, + 'price': None, + 'baseValue': None, + 'quoteValue': self.parse_number(quoteValueString), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + + def fetch_isolated_borrow_rate(self, symbol: str, params={}) -> IsolatedBorrowRate: + """ + fetch the rate of interest to borrow a currency for margin trading + + https://www.bitget.com/api-doc/margin/isolated/account/Isolated-Margin-Interest-Rate-And-Max-Borrowable-Amount + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `isolated borrow rate structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = self.privateMarginGetV2MarginIsolatedInterestRateAndLimit(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700878692567, + # "data": [ + # { + # "symbol": "BTCUSDT", + # "leverage": "10", + # "baseCoin": "BTC", + # "baseTransferable": True, + # "baseBorrowable": True, + # "baseDailyInterestRate": "0.00007", + # "baseAnnuallyInterestRate": "0.02555", + # "baseMaxBorrowableAmount": "27", + # "baseVipList": [ + # {"level":"0","dailyInterestRate":"0.00007","limit":"27","annuallyInterestRate":"0.02555","discountRate":"1"}, + # {"level":"1","dailyInterestRate":"0.0000679","limit":"27.81","annuallyInterestRate":"0.0247835","discountRate":"0.97"}, + # {"level":"2","dailyInterestRate":"0.0000644","limit":"29.16","annuallyInterestRate":"0.023506","discountRate":"0.92"}, + # {"level":"3","dailyInterestRate":"0.0000602","limit":"31.32","annuallyInterestRate":"0.021973","discountRate":"0.86"}, + # {"level":"4","dailyInterestRate":"0.0000525","limit":"35.91","annuallyInterestRate":"0.0191625","discountRate":"0.75"}, + # {"level":"5","dailyInterestRate":"0.000042","limit":"44.82","annuallyInterestRate":"0.01533","discountRate":"0.6"} + # ], + # "quoteCoin": "USDT", + # "quoteTransferable": True, + # "quoteBorrowable": True, + # "quoteDailyInterestRate": "0.00041095", + # "quoteAnnuallyInterestRate": "0.14999675", + # "quoteMaxBorrowableAmount": "300000", + # "quoteList": [ + # {"level":"0","dailyInterestRate":"0.00041095","limit":"300000","annuallyInterestRate":"0.14999675","discountRate":"1"}, + # {"level":"1","dailyInterestRate":"0.00039863","limit":"309000","annuallyInterestRate":"0.14549995","discountRate":"0.97"}, + # {"level":"2","dailyInterestRate":"0.00037808","limit":"324000","annuallyInterestRate":"0.1379992","discountRate":"0.92"}, + # {"level":"3","dailyInterestRate":"0.00035342","limit":"348000","annuallyInterestRate":"0.1289983","discountRate":"0.86"}, + # {"level":"4","dailyInterestRate":"0.00030822","limit":"399000","annuallyInterestRate":"0.1125003","discountRate":"0.75"}, + # {"level":"5","dailyInterestRate":"0.00024657","limit":"498000","annuallyInterestRate":"0.08999805","discountRate":"0.6"} + # ] + # } + # ] + # } + # + timestamp = self.safe_integer(response, 'requestTime') + data = self.safe_value(response, 'data', []) + first = self.safe_value(data, 0, {}) + first['timestamp'] = timestamp + return self.parse_isolated_borrow_rate(first, market) + + def parse_isolated_borrow_rate(self, info: dict, market: Market = None) -> IsolatedBorrowRate: + # + # { + # "symbol": "BTCUSDT", + # "leverage": "10", + # "baseCoin": "BTC", + # "baseTransferable": True, + # "baseBorrowable": True, + # "baseDailyInterestRate": "0.00007", + # "baseAnnuallyInterestRate": "0.02555", + # "baseMaxBorrowableAmount": "27", + # "baseVipList": [ + # {"level":"0","dailyInterestRate":"0.00007","limit":"27","annuallyInterestRate":"0.02555","discountRate":"1"}, + # {"level":"1","dailyInterestRate":"0.0000679","limit":"27.81","annuallyInterestRate":"0.0247835","discountRate":"0.97"}, + # {"level":"2","dailyInterestRate":"0.0000644","limit":"29.16","annuallyInterestRate":"0.023506","discountRate":"0.92"}, + # {"level":"3","dailyInterestRate":"0.0000602","limit":"31.32","annuallyInterestRate":"0.021973","discountRate":"0.86"}, + # {"level":"4","dailyInterestRate":"0.0000525","limit":"35.91","annuallyInterestRate":"0.0191625","discountRate":"0.75"}, + # {"level":"5","dailyInterestRate":"0.000042","limit":"44.82","annuallyInterestRate":"0.01533","discountRate":"0.6"} + # ], + # "quoteCoin": "USDT", + # "quoteTransferable": True, + # "quoteBorrowable": True, + # "quoteDailyInterestRate": "0.00041095", + # "quoteAnnuallyInterestRate": "0.14999675", + # "quoteMaxBorrowableAmount": "300000", + # "quoteList": [ + # {"level":"0","dailyInterestRate":"0.00041095","limit":"300000","annuallyInterestRate":"0.14999675","discountRate":"1"}, + # {"level":"1","dailyInterestRate":"0.00039863","limit":"309000","annuallyInterestRate":"0.14549995","discountRate":"0.97"}, + # {"level":"2","dailyInterestRate":"0.00037808","limit":"324000","annuallyInterestRate":"0.1379992","discountRate":"0.92"}, + # {"level":"3","dailyInterestRate":"0.00035342","limit":"348000","annuallyInterestRate":"0.1289983","discountRate":"0.86"}, + # {"level":"4","dailyInterestRate":"0.00030822","limit":"399000","annuallyInterestRate":"0.1125003","discountRate":"0.75"}, + # {"level":"5","dailyInterestRate":"0.00024657","limit":"498000","annuallyInterestRate":"0.08999805","discountRate":"0.6"} + # ] + # } + # + marketId = self.safe_string(info, 'symbol') + symbol = self.safe_symbol(marketId, market, None, 'spot') + baseId = self.safe_string(info, 'baseCoin') + quoteId = self.safe_string(info, 'quoteCoin') + timestamp = self.safe_integer(info, 'timestamp') + return { + 'symbol': symbol, + 'base': self.safe_currency_code(baseId), + 'baseRate': self.safe_number(info, 'baseDailyInterestRate'), + 'quote': self.safe_currency_code(quoteId), + 'quoteRate': self.safe_number(info, 'quoteDailyInterestRate'), + 'period': 86400000, # 1-Day + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'info': info, + } + + def fetch_cross_borrow_rate(self, code: str, params={}) -> CrossBorrowRate: + """ + fetch the rate of interest to borrow a currency for margin trading + + https://www.bitget.com/api-doc/margin/cross/account/Get-Cross-Margin-Interest-Rate-And-Borrowable + https://www.bitget.com/api-doc/uta/public/Get-Margin-Loans + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :returns dict: a `borrow rate structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'coin': currency['id'], + } + uta = None + response = None + result = None + uta, params = self.handle_option_and_params(params, 'fetchCrossBorrowRate', 'uta', False) + if uta: + response = self.publicUtaGetV3MarketMarginLoans(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1752817798893, + # "data": { + # "dailyInterest": "0.00100008", + # "annualInterest": "0.3650292", + # "limit": "100" + # } + # } + # + result = self.safe_dict(response, 'data', {}) + else: + response = self.privateMarginGetV2MarginCrossedInterestRateAndLimit(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700879047861, + # "data": [ + # { + # "coin": "BTC", + # "leverage": "3", + # "transferable": True, + # "borrowable": True, + # "dailyInterestRate": "0.00007", + # "annualInterestRate": "0.02555", + # "maxBorrowableAmount": "26", + # "vipList": [ + # {"level":"0","limit":"26","dailyInterestRate":"0.00007","annualInterestRate":"0.02555","discountRate":"1"}, + # {"level":"1","limit":"26.78","dailyInterestRate":"0.0000679","annualInterestRate":"0.0247835","discountRate":"0.97"}, + # {"level":"2","limit":"28.08","dailyInterestRate":"0.0000644","annualInterestRate":"0.023506","discountRate":"0.92"}, + # {"level":"3","limit":"30.16","dailyInterestRate":"0.0000602","annualInterestRate":"0.021973","discountRate":"0.86"}, + # {"level":"4","limit":"34.58","dailyInterestRate":"0.0000525","annualInterestRate":"0.0191625","discountRate":"0.75"}, + # {"level":"5","limit":"43.16","dailyInterestRate":"0.000042","annualInterestRate":"0.01533","discountRate":"0.6"} + # ] + # } + # ] + # } + # + data = self.safe_value(response, 'data', []) + result = self.safe_value(data, 0, {}) + timestamp = self.safe_integer(response, 'requestTime') + result['timestamp'] = timestamp + return self.parse_borrow_rate(result, currency) + + def parse_borrow_rate(self, info, currency: Currency = None): + # + # default + # + # { + # "coin": "BTC", + # "leverage": "3", + # "transferable": True, + # "borrowable": True, + # "dailyInterestRate": "0.00007", + # "annualInterestRate": "0.02555", + # "maxBorrowableAmount": "26", + # "vipList": [ + # {"level":"0","limit":"26","dailyInterestRate":"0.00007","annualInterestRate":"0.02555","discountRate":"1"}, + # {"level":"1","limit":"26.78","dailyInterestRate":"0.0000679","annualInterestRate":"0.0247835","discountRate":"0.97"}, + # {"level":"2","limit":"28.08","dailyInterestRate":"0.0000644","annualInterestRate":"0.023506","discountRate":"0.92"}, + # {"level":"3","limit":"30.16","dailyInterestRate":"0.0000602","annualInterestRate":"0.021973","discountRate":"0.86"}, + # {"level":"4","limit":"34.58","dailyInterestRate":"0.0000525","annualInterestRate":"0.0191625","discountRate":"0.75"}, + # {"level":"5","limit":"43.16","dailyInterestRate":"0.000042","annualInterestRate":"0.01533","discountRate":"0.6"} + # ] + # } + # + # uta + # + # { + # "dailyInterest": "0.00100008", + # "annualInterest": "0.3650292", + # "limit": "100" + # } + # + currencyId = self.safe_string(info, 'coin') + timestamp = self.safe_integer(info, 'timestamp') + return { + 'currency': self.safe_currency_code(currencyId, currency), + 'rate': self.safe_number_2(info, 'dailyInterestRate', 'dailyInterest'), + 'period': 86400000, # 1-Day + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'info': info, + } + + def fetch_borrow_interest(self, code: Str = None, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[BorrowInterest]: + """ + fetch the interest owed by the user for borrowing currency for margin trading + + https://www.bitget.com/api-doc/margin/cross/record/Get-Cross-Interest-Records + https://www.bitget.com/api-doc/margin/isolated/record/Get-Isolated-Interest-Records + + :param str [code]: unified currency code + :param str [symbol]: unified market symbol when fetching interest in isolated markets + :param int [since]: the earliest time in ms to fetch borrow interest for + :param int [limit]: the maximum number of structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `borrow interest structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchBorrowInterest', 'paginate') + if paginate: + return self.fetch_paginated_call_cursor('fetchBorrowInterest', symbol, since, limit, params, 'minId', 'idLessThan') + market = None + if symbol is not None: + market = self.market(symbol) + request: dict = {} + currency = None + if code is not None: + currency = self.currency(code) + request['coin'] = currency['id'] + if since is not None: + request['startTime'] = since + else: + request['startTime'] = self.milliseconds() - 7776000000 + if limit is not None: + request['limit'] = limit + response = None + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchBorrowInterest', params, 'cross') + if marginMode == 'isolated': + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchBorrowInterest() requires a symbol argument') + request['symbol'] = market['id'] + response = self.privateMarginGetV2MarginIsolatedInterestHistory(self.extend(request, params)) + elif marginMode == 'cross': + response = self.privateMarginGetV2MarginCrossedInterestHistory(self.extend(request, params)) + # + # isolated + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700879935189, + # "data": { + # "resultList": [ + # { + # "interestId": "1112125304879067137", + # "interestCoin": "USDT", + # "dailyInterestRate": "0.00041095", + # "loanCoin": "USDT", + # "interestAmount": "0.0000685", + # "interstType": "first", + # "symbol": "BTCUSDT", + # "cTime": "1700877255648", + # "uTime": "1700877255648" + # }, + # ], + # "maxId": "1112125304879067137", + # "minId": "1100138015672119298" + # } + # } + # + # cross + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1700879597044, + # "data": { + # "resultList": [ + # { + # "interestId": "1112122013642272769", + # "interestCoin": "USDT", + # "dailyInterestRate": "0.00041", + # "loanCoin": "USDT", + # "interestAmount": "0.00006834", + # "interstType": "first", + # "cTime": "1700876470957", + # "uTime": "1700876470957" + # }, + # ], + # "maxId": "1112122013642272769", + # "minId": "1096917004629716993" + # } + # } + # + data = self.safe_value(response, 'data', {}) + rows = self.safe_value(data, 'resultList', []) + interest = self.parse_borrow_interests(rows, market) + return self.filter_by_currency_since_limit(interest, code, since, limit) + + def parse_borrow_interest(self, info: dict, market: Market = None) -> BorrowInterest: + # + # isolated + # + # { + # "interestId": "1112125304879067137", + # "interestCoin": "USDT", + # "dailyInterestRate": "0.00041095", + # "loanCoin": "USDT", + # "interestAmount": "0.0000685", + # "interstType": "first", + # "symbol": "BTCUSDT", + # "cTime": "1700877255648", + # "uTime": "1700877255648" + # } + # + # cross + # + # { + # "interestId": "1112122013642272769", + # "interestCoin": "USDT", + # "dailyInterestRate": "0.00041", + # "loanCoin": "USDT", + # "interestAmount": "0.00006834", + # "interstType": "first", + # "cTime": "1700876470957", + # "uTime": "1700876470957" + # } + # + marketId = self.safe_string(info, 'symbol') + market = self.safe_market(marketId, market) + marginMode = 'isolated' if (marketId is not None) else 'cross' + timestamp = self.safe_integer(info, 'cTime') + return { + 'info': info, + 'symbol': self.safe_string(market, 'symbol'), + 'currency': self.safe_currency_code(self.safe_string(info, 'interestCoin')), + 'interest': self.safe_number(info, 'interestAmount'), + 'interestRate': self.safe_number(info, 'dailyInterestRate'), + 'amountBorrowed': None, + 'marginMode': marginMode, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + + def close_position(self, symbol: str, side: OrderSide = None, params={}) -> Order: + """ + closes an open position for a market + + https://www.bitget.com/api-doc/contract/trade/Flash-Close-Position + https://www.bitget.com/api-doc/uta/trade/Close-All-Positions + + :param str symbol: unified CCXT market symbol + :param str [side]: one-way mode: 'buy' or 'sell', hedge-mode: 'long' or 'short' + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :returns dict: An `order structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + productType = None + uta = None + response = None + productType, params = self.handle_product_type_and_params(market, params) + uta, params = self.handle_option_and_params(params, 'closePosition', 'uta', False) + if uta: + if side is not None: + request['posSide'] = side + request['category'] = productType + response = self.privateUtaPostV3TradeClosePositions(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1751020218384, + # "data": { + # "list": [ + # { + # "orderId": "1322440134099320832", + # "clientOid": "1322440134099320833" + # } + # ] + # } + # } + # + else: + if side is not None: + request['holdSide'] = side + request['productType'] = productType + response = self.privateMixPostV2MixOrderClosePositions(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1702975017017, + # "data": { + # "successList": [ + # { + # "orderId": "1120923953904893955", + # "clientOid": "1120923953904893956" + # } + # ], + # "failureList": [], + # "result": False + # } + # } + # + data = self.safe_value(response, 'data', {}) + order = self.safe_list_2(data, 'successList', 'list', []) + return self.parse_order(order[0], market) + + def close_all_positions(self, params={}) -> List[Position]: + """ + closes all open positions for a market type + + https://www.bitget.com/api-doc/contract/trade/Flash-Close-Position + https://www.bitget.com/api-doc/uta/trade/Close-All-Positions + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.productType]: 'USDT-FUTURES', 'USDC-FUTURES', 'COIN-FUTURES', 'SUSDT-FUTURES', 'SUSDC-FUTURES' or 'SCOIN-FUTURES' + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :returns dict[]: A list of `position structures ` + """ + self.load_markets() + request: dict = {} + productType = None + uta = None + response = None + productType, params = self.handle_product_type_and_params(None, params) + uta, params = self.handle_option_and_params(params, 'closeAllPositions', 'uta', False) + if uta: + request['category'] = productType + response = self.privateUtaPostV3TradeClosePositions(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1751020218384, + # "data": { + # "list": [ + # { + # "orderId": "1322440134099320832", + # "clientOid": "1322440134099320833" + # } + # ] + # } + # } + # + else: + request['productType'] = productType + response = self.privateMixPostV2MixOrderClosePositions(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1702975017017, + # "data": { + # "successList": [ + # { + # "orderId": "1120923953904893955", + # "clientOid": "1120923953904893956" + # } + # ], + # "failureList": [], + # "result": False + # } + # } + # + data = self.safe_value(response, 'data', {}) + orderInfo = self.safe_list_2(data, 'successList', 'list', []) + return self.parse_positions(orderInfo, None, params) + + def fetch_margin_mode(self, symbol: str, params={}) -> MarginMode: + """ + fetches the margin mode of a trading pair + + https://www.bitget.com/api-doc/contract/account/Get-Single-Account + + :param str symbol: unified symbol of the market to fetch the margin mode for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin mode structure ` + """ + self.load_markets() + market = self.market(symbol) + productType = None + productType, params = self.handle_product_type_and_params(market, params) + request: dict = { + 'symbol': market['id'], + 'marginCoin': market['settleId'], + 'productType': productType, + } + response = self.privateMixGetV2MixAccountAccount(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1709791216652, + # "data": { + # "marginCoin": "USDT", + # "locked": "0", + # "available": "19.88811074", + # "crossedMaxAvailable": "19.88811074", + # "isolatedMaxAvailable": "19.88811074", + # "maxTransferOut": "19.88811074", + # "accountEquity": "19.88811074", + # "usdtEquity": "19.888110749166", + # "btcEquity": "0.000302183391", + # "crossedRiskRate": "0", + # "crossedMarginLeverage": 20, + # "isolatedLongLever": 20, + # "isolatedShortLever": 20, + # "marginMode": "crossed", + # "posMode": "hedge_mode", + # "unrealizedPL": "0", + # "coupon": "0", + # "crossedUnrealizedPL": "0", + # "isolatedUnrealizedPL": "" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_margin_mode(data, market) + + def parse_margin_mode(self, marginMode: dict, market=None) -> MarginMode: + marginType = self.safe_string(marginMode, 'marginMode') + marginType = 'cross' if (marginType == 'crossed') else marginType + return { + 'info': marginMode, + 'symbol': market['symbol'], + 'marginMode': marginType, + } + + def fetch_positions_history(self, symbols: Strings = None, since: Int = None, limit: Int = None, params={}) -> List[Position]: + """ + fetches historical positions + + https://www.bitget.com/api-doc/contract/position/Get-History-Position + https://www.bitget.com/api-doc/uta/trade/Get-Position-History + + :param str[] [symbols]: unified contract symbols + :param int [since]: timestamp in ms of the earliest position to fetch, default=3 months ago, max range for params["until"] - since is 3 months + :param int [limit]: the maximum amount of records to fetch, default=20, max=100 + :param dict params: extra parameters specific to the exchange api endpoint + :param int [params.until]: timestamp in ms of the latest position to fetch, max range for params["until"] - since is 3 months + :param str [params.productType]: USDT-FUTURES(default), COIN-FUTURES, USDC-FUTURES, SUSDT-FUTURES, SCOIN-FUTURES, or SUSDC-FUTURES + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :returns dict[]: a list of `position structures ` + """ + self.load_markets() + request: dict = {} + market = None + productType = None + uta = None + response = None + if symbols is not None: + symbolsLength = len(symbols) + if symbolsLength > 0: + market = self.market(symbols[0]) + request['symbol'] = market['id'] + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + request, params = self.handle_until_option('endTime', request, params) + productType, params = self.handle_product_type_and_params(market, params) + uta, params = self.handle_option_and_params(params, 'fetchPositionsHistory', 'uta', False) + if uta: + request['category'] = productType + response = self.privateUtaGetV3PositionHistoryPosition(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1751020950427, + # "data": { + # "list": [ + # { + # "positionId": "1322441328637100049", + # "category": "USDT-FUTURES", + # "symbol": "BTCUSDT", + # "marginCoin": "USDT", + # "holdMode": "hedge_mode", + # "posSide": "long", + # "marginMode": "crossed", + # "openPriceAvg": "107003.7", + # "closePriceAvg": "107005.4", + # "openTotalPos": "0.0001", + # "closeTotalPos": "0.0001", + # "cumRealisedPnl": "0.00017", + # "netProfit": "-0.01267055", + # "totalFunding": "0", + # "openFeeTotal": "-0.00642022", + # "closeFeeTotal": "-0.00642032", + # "createdTime": "1751020503195", + # "updatedTime": "1751020520458" + # }, + # ], + # "cursor": "1322440134158041089" + # } + # } + # + else: + response = self.privateMixGetV2MixPositionHistoryPosition(self.extend(request, params)) + # + # { + # code: '00000', + # msg: 'success', + # requestTime: '1712794148791', + # data: { + # list: [ + # { + # symbol: 'XRPUSDT', + # marginCoin: 'USDT', + # holdSide: 'long', + # openAvgPrice: '0.64967', + # closeAvgPrice: '0.58799', + # marginMode: 'isolated', + # openTotalPos: '10', + # closeTotalPos: '10', + # pnl: '-0.62976205', + # netProfit: '-0.65356802', + # totalFunding: '-0.01638', + # openFee: '-0.00389802', + # closeFee: '-0.00352794', + # ctime: '1709590322199', + # utime: '1709667583395' + # }, + # ... + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + responseList = self.safe_list(data, 'list', []) + positions = self.parse_positions(responseList, symbols, params) + return self.filter_by_since_limit(positions, since, limit) + + def fetch_convert_quote(self, fromCode: str, toCode: str, amount: Num = None, params={}) -> Conversion: + """ + fetch a quote for converting from one currency to another + + https://www.bitget.com/api-doc/common/convert/Get-Quoted-Price + + :param str fromCode: the currency that you want to sell and convert from + :param str toCode: the currency that you want to buy and convert into + :param float [amount]: how much you want to trade in units of the from currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `conversion structure ` + """ + self.load_markets() + request: dict = { + 'fromCoin': fromCode, + 'toCoin': toCode, + 'fromCoinSize': self.number_to_string(amount), + } + response = self.privateConvertGetV2ConvertQuotedPrice(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1712121940158, + # "data": { + # "fromCoin": "USDT", + # "fromCoinSize": "5", + # "cnvtPrice": "0.9993007892377704", + # "toCoin": "USDC", + # "toCoinSize": "4.99650394", + # "traceId": "1159288930228187140", + # "fee": "0" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + fromCurrencyId = self.safe_string(data, 'fromCoin', fromCode) + fromCurrency = self.currency(fromCurrencyId) + toCurrencyId = self.safe_string(data, 'toCoin', toCode) + toCurrency = self.currency(toCurrencyId) + return self.parse_conversion(data, fromCurrency, toCurrency) + + def create_convert_trade(self, id: str, fromCode: str, toCode: str, amount: Num = None, params={}) -> Conversion: + """ + convert from one currency to another + + https://www.bitget.com/api-doc/common/convert/Trade + + :param str id: the id of the trade that you want to make + :param str fromCode: the currency that you want to sell and convert from + :param str toCode: the currency that you want to buy and convert into + :param float amount: how much you want to trade in units of the from currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str params['price']: the price of the conversion, obtained from fetchConvertQuote() + :param str params['toAmount']: the amount you want to trade in units of the toCurrency, obtained from fetchConvertQuote() + :returns dict: a `conversion structure ` + """ + self.load_markets() + price = self.safe_string_2(params, 'price', 'cnvtPrice') + if price is None: + raise ArgumentsRequired(self.id + ' createConvertTrade() requires a price parameter') + toAmount = self.safe_string_2(params, 'toAmount', 'toCoinSize') + if toAmount is None: + raise ArgumentsRequired(self.id + ' createConvertTrade() requires a toAmount parameter') + params = self.omit(params, ['price', 'toAmount']) + request: dict = { + 'traceId': id, + 'fromCoin': fromCode, + 'toCoin': toCode, + 'fromCoinSize': self.number_to_string(amount), + 'toCoinSize': toAmount, + 'cnvtPrice': price, + } + response = self.privateConvertPostV2ConvertTrade(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1712123746203, + # "data": { + # "cnvtPrice": "0.99940076", + # "toCoin": "USDC", + # "toCoinSize": "4.99700379", + # "ts": "1712123746217" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + toCurrencyId = self.safe_string(data, 'toCoin', toCode) + toCurrency = self.currency(toCurrencyId) + return self.parse_conversion(data, None, toCurrency) + + def fetch_convert_trade_history(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Conversion]: + """ + fetch the users history of conversion trades + + https://www.bitget.com/api-doc/common/convert/Get-Convert-Record + + :param str [code]: the unified currency code + :param int [since]: the earliest time in ms to fetch conversions for + :param int [limit]: the maximum number of conversion structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `conversion structures ` + """ + self.load_markets() + request: dict = {} + msInDay = 86400000 + now = self.milliseconds() + if since is not None: + request['startTime'] = since + else: + request['startTime'] = now - msInDay + endTime = self.safe_string_2(params, 'endTime', 'until') + if endTime is not None: + request['endTime'] = endTime + else: + request['endTime'] = now + if limit is not None: + request['limit'] = limit + params = self.omit(params, 'until') + response = self.privateConvertGetV2ConvertConvertRecord(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1712124371799, + # "data": { + # "dataList": [ + # { + # "id": "1159296505255219205", + # "fromCoin": "USDT", + # "fromCoinSize": "5", + # "cnvtPrice": "0.99940076", + # "toCoin": "USDC", + # "toCoinSize": "4.99700379", + # "ts": "1712123746217", + # "fee": "0" + # } + # ], + # "endId": "1159296505255219205" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + dataList = self.safe_list(data, 'dataList', []) + return self.parse_conversions(dataList, code, 'fromCoin', 'toCoin', since, limit) + + def parse_conversion(self, conversion: dict, fromCurrency: Currency = None, toCurrency: Currency = None) -> Conversion: + # + # fetchConvertQuote + # + # { + # "fromCoin": "USDT", + # "fromCoinSize": "5", + # "cnvtPrice": "0.9993007892377704", + # "toCoin": "USDC", + # "toCoinSize": "4.99650394", + # "traceId": "1159288930228187140", + # "fee": "0" + # } + # + # createConvertTrade + # + # { + # "cnvtPrice": "0.99940076", + # "toCoin": "USDC", + # "toCoinSize": "4.99700379", + # "ts": "1712123746217" + # } + # + # fetchConvertTradeHistory + # + # { + # "id": "1159296505255219205", + # "fromCoin": "USDT", + # "fromCoinSize": "5", + # "cnvtPrice": "0.99940076", + # "toCoin": "USDC", + # "toCoinSize": "4.99700379", + # "ts": "1712123746217", + # "fee": "0" + # } + # + timestamp = self.safe_integer(conversion, 'ts') + fromCoin = self.safe_string(conversion, 'fromCoin') + fromCode = self.safe_currency_code(fromCoin, fromCurrency) + to = self.safe_string(conversion, 'toCoin') + toCode = self.safe_currency_code(to, toCurrency) + return { + 'info': conversion, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': self.safe_string_2(conversion, 'id', 'traceId'), + 'fromCurrency': fromCode, + 'fromAmount': self.safe_number(conversion, 'fromCoinSize'), + 'toCurrency': toCode, + 'toAmount': self.safe_number(conversion, 'toCoinSize'), + 'price': self.safe_number(conversion, 'cnvtPrice'), + 'fee': self.safe_number(conversion, 'fee'), + } + + def fetch_convert_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies that can be converted + + https://www.bitget.com/api-doc/common/convert/Get-Convert-Currencies + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + self.load_markets() + response = self.privateConvertGetV2ConvertCurrencies(params) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1712121755897, + # "data": [ + # { + # "coin": "BTC", + # "available": "0.00009850", + # "maxAmount": "0.756266", + # "minAmount": "0.00001" + # }, + # ] + # } + # + result: dict = {} + data = self.safe_list(response, 'data', []) + for i in range(0, len(data)): + entry = data[i] + id = self.safe_string(entry, 'coin') + code = self.safe_currency_code(id) + result[code] = { + 'info': entry, + 'id': id, + 'code': code, + 'networks': None, + 'type': None, + 'name': None, + 'active': None, + 'deposit': None, + 'withdraw': self.safe_number(entry, 'available'), + 'fee': None, + 'precision': None, + 'limits': { + 'amount': { + 'min': self.safe_number(entry, 'minAmount'), + 'max': self.safe_number(entry, 'maxAmount'), + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + 'deposit': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + } + return result + + def fetch_funding_interval(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate interval + + https://www.bitget.com/api-doc/contract/market/Get-Symbol-Next-Funding-Time + https://www.bitget.com/api-doc/uta/public/Get-Current-Funding-Rate + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :returns dict: a `funding rate structure ` + """ + self.load_markets() + market = self.market(symbol) + productType = None + productType, params = self.handle_product_type_and_params(market, params) + request: dict = { + 'symbol': market['id'], + } + response = None + uta = None + uta, params = self.handle_option_and_params(params, 'fetchFundingInterval', 'uta', False) + if uta: + response = self.publicUtaGetV3MarketCurrentFundRate(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1752880157959, + # "data": [ + # { + # "symbol": "BTCUSDT", + # "fundingRate": "0.0001", + # "fundingRateInterval": "8", + # "nextUpdate": "1752883200000", + # "minFundingRate": "-0.003", + # "maxFundingRate": "0.003" + # } + # ] + # } + # + else: + request['productType'] = productType + response = self.publicMixGetV2MixMarketFundingTime(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1727930153888, + # "data": [ + # { + # "symbol": "BTCUSDT", + # "nextFundingTime": "1727942400000", + # "ratePeriod": "8" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + first = self.safe_dict(data, 0, {}) + return self.parse_funding_rate(first, market) + + def fetch_long_short_ratio_history(self, symbol: Str = None, timeframe: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LongShortRatio]: + """ + fetches the long short ratio history for a unified market symbol + + https://www.bitget.com/api-doc/common/apidata/Margin-Ls-Ratio + https://www.bitget.com/api-doc/common/apidata/Account-Long-Short + + :param str symbol: unified symbol of the market to fetch the long short ratio for + :param str [timeframe]: the period for the ratio + :param int [since]: the earliest time in ms to fetch ratios for + :param int [limit]: the maximum number of long short ratio structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of `long short ratio structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if timeframe is not None: + request['period'] = timeframe + response = None + if market['swap'] or market['future']: + response = self.publicMixGetV2MixMarketAccountLongShort(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1729321233281, + # "data": [ + # { + # "longAccountRatio": "0.58", + # "shortAccountRatio": "0.42", + # "longShortAccountRatio": "0.0138", + # "ts": "1729312200000" + # }, + # ] + # } + # + else: + response = self.publicMarginGetV2MarginMarketLongShortRatio(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1729306974712, + # "data": [ + # { + # "longShortRatio": "40.66", + # "ts": "1729306800000" + # }, + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_long_short_ratio_history(data, market) + + def parse_long_short_ratio(self, info: dict, market: Market = None) -> LongShortRatio: + marketId = self.safe_string(info, 'symbol') + timestamp = self.safe_integer_omit_zero(info, 'ts') + return { + 'info': info, + 'symbol': self.safe_symbol(marketId, market, None, 'contract'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'timeframe': None, + 'longShortRatio': self.safe_number_2(info, 'longShortRatio', 'longShortAccountRatio'), + } + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if not response: + return None # fallback to default error handler + # + # spot + # + # {"code":"00000","msg":"success","requestTime":1713294492511,"data":[...]}" + # + # {"status":"fail","err_code":"01001","err_msg":"系统异常,请稍后重试"} + # {"status":"error","ts":1595594160149,"err_code":"invalid-parameter","err_msg":"invalid size, valid range: [1,2000]"} + # {"status":"error","ts":1595684716042,"err_code":"invalid-parameter","err_msg":"illegal sign invalid"} + # {"status":"error","ts":1595700216275,"err_code":"bad-request","err_msg":"your balance is low!"} + # {"status":"error","ts":1595700344504,"err_code":"invalid-parameter","err_msg":"invalid type"} + # {"status":"error","ts":1595703343035,"err_code":"bad-request","err_msg":"order cancel fail"} + # {"status":"error","ts":1595704360508,"err_code":"invalid-parameter","err_msg":"accesskey not null"} + # {"status":"error","ts":1595704490084,"err_code":"invalid-parameter","err_msg":"permissions not right"} + # {"status":"error","ts":1595711862763,"err_code":"system exception","err_msg":"system exception"} + # {"status":"error","ts":1595730308979,"err_code":"bad-request","err_msg":"20003"} + # + # swap + # + # {"code":"40015","msg":"","requestTime":1595698564931,"data":null} + # {"code":"40017","msg":"Order id must not be blank","requestTime":1595702477835,"data":null} + # {"code":"40017","msg":"Order Type must not be blank","requestTime":1595698516162,"data":null} + # {"code":"40301","msg":"","requestTime":1595667662503,"data":null} + # {"code":"40017","msg":"Contract code must not be blank","requestTime":1595703151651,"data":null} + # {"code":"40108","msg":"","requestTime":1595885064600,"data":null} + # {"order_id":"513468410013679613","client_oid":null,"symbol":"ethusd","result":false,"err_code":"order_no_exist_error","err_msg":"订单不存在!"} + # + message = self.safe_string_2(response, 'err_msg', 'msg') + feedback = self.id + ' ' + body + nonEmptyMessage = ((message is not None) and (message != '') and (message != 'success')) + if nonEmptyMessage: + self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + errorCode = self.safe_string_2(response, 'code', 'err_code') + nonZeroErrorCode = (errorCode is not None) and (errorCode != '00000') + if nonZeroErrorCode: + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + if nonZeroErrorCode or nonEmptyMessage: + raise ExchangeError(feedback) # unknown message + return None + + def nonce(self): + return self.milliseconds() - self.options['timeDifference'] + + def sign(self, path, api=[], method='GET', params={}, headers=None, body=None): + signed = api[0] == 'private' + endpoint = api[1] + pathPart = '/api' + request = '/' + self.implode_params(path, params) + payload = pathPart + request + url = self.implode_hostname(self.urls['api'][endpoint]) + payload + query = self.omit(params, self.extract_params(path)) + if not signed and (method == 'GET'): + keys = list(query.keys()) + keysLength = len(keys) + if keysLength > 0: + url = url + '?' + self.urlencode(query) + if signed: + self.check_required_credentials() + timestamp = str(self.nonce()) + auth = timestamp + method + payload + if method == 'POST': + body = self.json(params) + auth += body + else: + if params: + queryInner = '?' + self.urlencode(self.keysort(params)) + # check #21169 pr + if queryInner.find('%24') > -1: + queryInner = queryInner.replace('%24', '$') + url += queryInner + auth += queryInner + signature = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256, 'base64') + broker = self.safe_string(self.options, 'broker') + headers = { + 'ACCESS-KEY': self.apiKey, + 'ACCESS-SIGN': signature, + 'ACCESS-TIMESTAMP': timestamp, + 'ACCESS-PASSPHRASE': self.password, + 'X-CHANNEL-API-CODE': broker, + } + if method == 'POST': + headers['Content-Type'] = 'application/json' + sandboxMode = self.safe_bool_2(self.options, 'sandboxMode', 'sandbox', False) + if sandboxMode and (path != 'v2/public/time') and (path != 'v3/market/current-fund-rate'): + # https://github.com/ccxt/ccxt/issues/25252#issuecomment-2662742336 + if headers is None: + headers = {} + productType = self.safe_string(params, 'productType') + if (productType != 'SCOIN-FUTURES') and (productType != 'SUSDT-FUTURES') and (productType != 'SUSDC-FUTURES'): + headers['PAPTRADING'] = '1' + return {'url': url, 'method': method, 'body': body, 'headers': headers} diff --git a/ccxt/bithumb.py b/ccxt/bithumb.py new file mode 100644 index 0000000..a74d567 --- /dev/null +++ b/ccxt/bithumb.py @@ -0,0 +1,1226 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.bithumb import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currency, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, Transaction, MarketInterface +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import InvalidAddress +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.decimal_to_precision import TRUNCATE +from ccxt.base.decimal_to_precision import DECIMAL_PLACES +from ccxt.base.decimal_to_precision import SIGNIFICANT_DIGITS +from ccxt.base.precise import Precise + + +class bithumb(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(bithumb, self).describe(), { + 'id': 'bithumb', + 'name': 'Bithumb', + 'countries': ['KR'], # South Korea + 'rateLimit': 500, + 'pro': True, + 'has': { + 'CORS': True, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelOrder': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createMarketOrder': True, + 'createOrder': True, + 'createOrderWithTakeProfitAndStopLoss': False, + 'createOrderWithTakeProfitAndStopLossWs': False, + 'createReduceOnlyOrder': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': False, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTrades': True, + 'fetchTransfer': False, + 'fetchTransfers': False, + 'fetchVolatilityHistory': False, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'transfer': False, + 'withdraw': True, + }, + 'hostname': 'bithumb.com', + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/c9e0eefb-4777-46b9-8f09-9d7f7c4af82d', + 'api': { + 'public': 'https://api.{hostname}/public', + 'private': 'https://api.{hostname}', + }, + 'www': 'https://www.bithumb.com', + 'doc': 'https://apidocs.bithumb.com', + 'fees': 'https://en.bithumb.com/customer_support/info_fee', + }, + 'api': { + 'public': { + 'get': [ + 'ticker/ALL_{quoteId}', + 'ticker/{baseId}_{quoteId}', + 'orderbook/ALL_{quoteId}', + 'orderbook/{baseId}_{quoteId}', + 'transaction_history/{baseId}_{quoteId}', + 'network-info', + 'assetsstatus/multichain/ALL', + 'assetsstatus/multichain/{currency}', + 'withdraw/minimum/ALL', + 'withdraw/minimum/{currency}', + 'assetsstatus/ALL', + 'assetsstatus/{baseId}', + 'candlestick/{baseId}_{quoteId}/{interval}', + ], + }, + 'private': { + 'post': [ + 'info/account', + 'info/balance', + 'info/wallet_address', + 'info/ticker', + 'info/orders', + 'info/user_transactions', + 'info/order_detail', + 'trade/place', + 'trade/cancel', + 'trade/btc_withdrawal', + 'trade/krw_deposit', + 'trade/krw_withdrawal', + 'trade/market_buy', + 'trade/market_sell', + 'trade/stop_limit', + ], + }, + }, + 'fees': { + 'trading': { + 'maker': self.parse_number('0.0025'), + 'taker': self.parse_number('0.0025'), + }, + }, + 'precisionMode': SIGNIFICANT_DIGITS, + # todo: update to v2 apis + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': False, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': False, + 'FOK': False, + 'PO': False, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyRequiresPrice': False, + 'marketBuyByCost': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': None, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 1000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOrders': None, + 'fetchClosedOrders': None, + 'fetchOHLCV': { + 'limit': 1000, + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'exceptions': { + 'Bad Request(SSL)': BadRequest, + 'Bad Request(Bad Method)': BadRequest, + 'Bad Request.(Auth Data)': AuthenticationError, # {"status": "5100", "message": "Bad Request.(Auth Data)"} + 'Not Member': AuthenticationError, + 'Invalid Apikey': AuthenticationError, # {"status":"5300","message":"Invalid Apikey"} + 'Method Not Allowed.(Access IP)': PermissionDenied, + 'Method Not Allowed.(BTC Adress)': InvalidAddress, + 'Method Not Allowed.(Access)': PermissionDenied, + 'Database Fail': ExchangeNotAvailable, + 'Invalid Parameter': BadRequest, + '5600': ExchangeError, + 'Unknown Error': ExchangeError, + 'After May 23th, recent_transactions is no longer, hence users will not be able to connect to recent_transactions': ExchangeError, # {"status":"5100","message":"After May 23th, recent_transactions is no longer, hence users will not be able to connect to recent_transactions"} + }, + 'timeframes': { + '1m': '1m', + '3m': '3m', + '5m': '5m', + '10m': '10m', + '30m': '30m', + '1h': '1h', + '6h': '6h', + '12h': '12h', + '1d': '24h', + }, + 'options': { + 'quoteCurrencies': { + 'BTC': { + 'limits': { + 'cost': { + 'min': 0.0002, + 'max': 100, + }, + }, + }, + 'KRW': { + 'limits': { + 'cost': { + 'min': 500, + 'max': 5000000000, + }, + }, + }, + 'USDT': { + 'limits': { + 'cost': { + 'min': None, + 'max': None, + }, + }, + }, + }, + }, + 'commonCurrencies': { + 'ALT': 'ArchLoot', + 'FTC': 'FTC2', + 'SOC': 'Soda Coin', + }, + }) + + def safe_market(self, marketId: Str = None, market: Market = None, delimiter: Str = None, marketType: Str = None) -> MarketInterface: + # bithumb has a different type of conflict in markets, because + # their ids are the base currency(BTC for instance), so we can have + # multiple "BTC" ids representing the different markets(BTC/ETH, "BTC/DOGE", etc) + # since they're the same we just need to return one + return super(bithumb, self).safe_market(marketId, market, delimiter, 'spot') + + def amount_to_precision(self, symbol, amount): + return self.decimal_to_precision(amount, TRUNCATE, self.markets[symbol]['precision']['amount'], DECIMAL_PLACES) + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for bithumb + + https://apidocs.bithumb.com/v1.2.0/reference/%ED%98%84%EC%9E%AC%EA%B0%80-%EC%A0%95%EB%B3%B4-%EC%A1%B0%ED%9A%8C-all + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + result = [] + quoteCurrencies = self.safe_dict(self.options, 'quoteCurrencies', {}) + quotes = list(quoteCurrencies.keys()) + promises = [] + for i in range(0, len(quotes)): + request = { + 'quoteId': quotes[i], + } + promises.append(self.publicGetTickerALLQuoteId(self.extend(request, params))) + # + # { + # "status": "0000", + # "data": { + # "ETH": { + # "opening_price": "0.05153399", + # "closing_price": "0.05145144", + # "min_price": "0.05145144", + # "max_price": "0.05160781", + # "units_traded": "6.541124172077830855", + # "acc_trade_value": "0.33705472498492329997697755", + # "prev_closing_price": "0.0515943", + # "units_traded_24H": "43.368879902677400513", + # "acc_trade_value_24H": "2.24165339555398079994373342", + # "fluctate_24H": "-0.00018203", + # "fluctate_rate_24H": "-0.35" + # }, + # "XRP": { + # "opening_price": "0.00000918", + # "closing_price": "0.0000092", + # "min_price": "0.00000918", + # "max_price": "0.0000092", + # "units_traded": "6516.949363", + # "acc_trade_value": "0.0598792533602796", + # "prev_closing_price": "0.00000916", + # "units_traded_24H": "229161.50354738", + # "acc_trade_value_24H": "2.0446589371637117", + # "fluctate_24H": "0.00000049", + # "fluctate_rate_24H": "5.63" + # }, + # ... + # "date": "1721675913145" + # } + # } + # + results = promises + for i in range(0, len(quotes)): + quote = quotes[i] + quoteId = quote + response = results[i] + data = self.safe_dict(response, 'data') + extension = self.safe_dict(quoteCurrencies, quote, {}) + currencyIds = list(data.keys()) + for j in range(0, len(currencyIds)): + currencyId = currencyIds[j] + if currencyId == 'date': + continue + market = data[currencyId] + base = self.safe_currency_code(currencyId) + active = True + if isinstance(market, list): + numElements = len(market) + if numElements == 0: + active = False + entry = self.deep_extend({ + 'id': currencyId, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': currencyId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': active, + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDateTime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': int('4'), + 'price': int('4'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': None, + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': {}, # set via options + }, + 'created': None, + 'info': market, + }, extension) + result.append(entry) + return result + + def parse_balance(self, response) -> Balances: + result: dict = {'info': response} + balances = self.safe_dict(response, 'data') + codes = list(self.currencies.keys()) + for i in range(0, len(codes)): + code = codes[i] + account = self.account() + currency = self.currency(code) + lowerCurrencyId = self.safe_string_lower(currency, 'id') + account['total'] = self.safe_string(balances, 'total_' + lowerCurrencyId) + account['used'] = self.safe_string(balances, 'in_use_' + lowerCurrencyId) + account['free'] = self.safe_string(balances, 'available_' + lowerCurrencyId) + result[code] = account + return self.safe_balance(result) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://apidocs.bithumb.com/v1.2.0/reference/%EB%B3%B4%EC%9C%A0%EC%9E%90%EC%82%B0-%EC%A1%B0%ED%9A%8C + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + request: dict = { + 'currency': 'ALL', + } + response = self.privatePostInfoBalance(self.extend(request, params)) + return self.parse_balance(response) + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://apidocs.bithumb.com/v1.2.0/reference/%ED%98%B8%EA%B0%80-%EC%A0%95%EB%B3%B4-%EC%A1%B0%ED%9A%8C + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'baseId': market['baseId'], + 'quoteId': market['quoteId'], + } + if limit is not None: + request['count'] = limit # default 30, max 30 + response = self.publicGetOrderbookBaseIdQuoteId(self.extend(request, params)) + # + # { + # "status":"0000", + # "data":{ + # "timestamp":"1587621553942", + # "payment_currency":"KRW", + # "order_currency":"BTC", + # "bids":[ + # {"price":"8652000","quantity":"0.0043"}, + # {"price":"8651000","quantity":"0.0049"}, + # {"price":"8650000","quantity":"8.4791"}, + # ], + # "asks":[ + # {"price":"8654000","quantity":"0.119"}, + # {"price":"8655000","quantity":"0.254"}, + # {"price":"8658000","quantity":"0.119"}, + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + timestamp = self.safe_integer(data, 'timestamp') + return self.parse_order_book(data, symbol, timestamp, 'bids', 'asks', 'price', 'quantity') + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # fetchTicker, fetchTickers + # + # { + # "opening_price":"227100", + # "closing_price":"228400", + # "min_price":"222300", + # "max_price":"230000", + # "units_traded":"82618.56075337", + # "acc_trade_value":"18767376138.6031", + # "prev_closing_price":"227100", + # "units_traded_24H":"151871.13484676", + # "acc_trade_value_24H":"34247610416.8974", + # "fluctate_24H":"8700", + # "fluctate_rate_24H":"3.96", + # "date":"1587710327264", # fetchTickers inject self + # } + # + timestamp = self.safe_integer(ticker, 'date') + symbol = self.safe_symbol(None, market) + open = self.safe_string(ticker, 'opening_price') + close = self.safe_string(ticker, 'closing_price') + baseVolume = self.safe_string(ticker, 'units_traded_24H') + quoteVolume = self.safe_string(ticker, 'acc_trade_value_24H') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'max_price'), + 'low': self.safe_string(ticker, 'min_price'), + 'bid': self.safe_string(ticker, 'buy_price'), + 'bidVolume': None, + 'ask': self.safe_string(ticker, 'sell_price'), + 'askVolume': None, + 'vwap': None, + 'open': open, + 'close': close, + 'last': close, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'info': ticker, + }, market) + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://apidocs.bithumb.com/v1.2.0/reference/%ED%98%84%EC%9E%AC%EA%B0%80-%EC%A0%95%EB%B3%B4-%EC%A1%B0%ED%9A%8C-all + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + result: dict = {} + quoteCurrencies = self.safe_dict(self.options, 'quoteCurrencies', {}) + quotes = list(quoteCurrencies.keys()) + promises = [] + for i in range(0, len(quotes)): + request: dict = { + 'quoteId': quotes[i], + } + promises.append(self.publicGetTickerALLQuoteId(self.extend(request, params))) + responses = promises + for i in range(0, len(quotes)): + quote = quotes[i] + response = responses[i] + # + # { + # "status":"0000", + # "data":{ + # "BTC":{ + # "opening_price":"9045000", + # "closing_price":"9132000", + # "min_price":"8938000", + # "max_price":"9168000", + # "units_traded":"4619.79967497", + # "acc_trade_value":"42021363832.5187", + # "prev_closing_price":"9041000", + # "units_traded_24H":"8793.5045804", + # "acc_trade_value_24H":"78933458515.4962", + # "fluctate_24H":"530000", + # "fluctate_rate_24H":"6.16" + # }, + # "date":"1587710878669" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + timestamp = self.safe_integer(data, 'date') + tickers = self.omit(data, 'date') + currencyIds = list(tickers.keys()) + for j in range(0, len(currencyIds)): + currencyId = currencyIds[j] + ticker = data[currencyId] + base = self.safe_currency_code(currencyId) + symbol = base + '/' + quote + market = self.safe_market(symbol) + ticker['date'] = timestamp + result[symbol] = self.parse_ticker(ticker, market) + return self.filter_by_array_tickers(result, 'symbol', symbols) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://apidocs.bithumb.com/v1.2.0/reference/%ED%98%84%EC%9E%AC%EA%B0%80-%EC%A0%95%EB%B3%B4-%EC%A1%B0%ED%9A%8C + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'baseId': market['baseId'], + 'quoteId': market['quoteId'], + } + response = self.publicGetTickerBaseIdQuoteId(self.extend(request, params)) + # + # { + # "status":"0000", + # "data":{ + # "opening_price":"227100", + # "closing_price":"228400", + # "min_price":"222300", + # "max_price":"230000", + # "units_traded":"82618.56075337", + # "acc_trade_value":"18767376138.6031", + # "prev_closing_price":"227100", + # "units_traded_24H":"151871.13484676", + # "acc_trade_value_24H":"34247610416.8974", + # "fluctate_24H":"8700", + # "fluctate_rate_24H":"3.96", + # "date":"1587710327264" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_ticker(data, market) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # [ + # 1576823400000, # 기준 시간 + # "8284000", # 시가 + # "8286000", # 종가 + # "8289000", # 고가 + # "8276000", # 저가 + # "15.41503692" # 거래량 + # ] + # + return [ + self.safe_integer(ohlcv, 0), + self.safe_number(ohlcv, 1), + self.safe_number(ohlcv, 3), + self.safe_number(ohlcv, 4), + self.safe_number(ohlcv, 2), + self.safe_number(ohlcv, 5), + ] + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://apidocs.bithumb.com/v1.2.0/reference/candlestick-rest-api + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'baseId': market['baseId'], + 'quoteId': market['quoteId'], + 'interval': self.safe_string(self.timeframes, timeframe, timeframe), + } + response = self.publicGetCandlestickBaseIdQuoteIdInterval(self.extend(request, params)) + # + # { + # "status": "0000", + # "data": { + # [ + # 1576823400000, # 기준 시간 + # "8284000", # 시가 + # "8286000", # 종가 + # "8289000", # 고가 + # "8276000", # 저가 + # "15.41503692" # 거래량 + # ], + # [ + # 1576824000000, # 기준 시간 + # "8284000", # 시가 + # "8281000", # 종가 + # "8289000", # 고가 + # "8275000", # 저가 + # "6.19584467" # 거래량 + # ], + # } + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_ohlcvs(data, market, timeframe, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(public) + # + # { + # "transaction_date":"2020-04-23 22:21:46", + # "type":"ask", + # "units_traded":"0.0125", + # "price":"8667000", + # "total":"108337" + # } + # + # fetchOrder(private) + # + # { + # "transaction_date": "1572497603902030", + # "price": "8601000", + # "units": "0.005", + # "fee_currency": "KRW", + # "fee": "107.51", + # "total": "43005" + # } + # + # a workaround for their bug in date format, hours are not 0-padded + timestamp = None + transactionDatetime = self.safe_string(trade, 'transaction_date') + if transactionDatetime is not None: + parts = transactionDatetime.split(' ') + numParts = len(parts) + if numParts > 1: + transactionDate = parts[0] + transactionTime = parts[1] + if len(transactionTime) < 8: + transactionTime = '0' + transactionTime + timestamp = self.parse8601(transactionDate + ' ' + transactionTime) + else: + timestamp = self.safe_integer_product(trade, 'transaction_date', 0.001) + if timestamp is not None: + timestamp -= 9 * 3600000 # they report UTC + 9 hours, server in Korean timezone + type = None + side = self.safe_string(trade, 'type') + side = 'sell' if (side == 'ask') else 'buy' + id = self.safe_string(trade, 'cont_no') + market = self.safe_market(None, market) + priceString = self.safe_string(trade, 'price') + amountString = self.fix_comma_number(self.safe_string_2(trade, 'units_traded', 'units')) + costString = self.safe_string(trade, 'total') + fee = None + feeCostString = self.safe_string(trade, 'fee') + if feeCostString is not None: + feeCurrencyId = self.safe_string(trade, 'fee_currency') + feeCurrencyCode = self.common_currency_code(feeCurrencyId) + fee = { + 'cost': feeCostString, + 'currency': feeCurrencyCode, + } + return self.safe_trade({ + 'id': id, + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'order': None, + 'type': type, + 'side': side, + 'takerOrMaker': None, + 'price': priceString, + 'amount': amountString, + 'cost': costString, + 'fee': fee, + }, market) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://apidocs.bithumb.com/v1.2.0/reference/%EC%B5%9C%EA%B7%BC-%EC%B2%B4%EA%B2%B0-%EB%82%B4%EC%97%AD + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'baseId': market['baseId'], + 'quoteId': market['quoteId'], + } + if limit is not None: + request['count'] = limit # default 20, max 100 + response = self.publicGetTransactionHistoryBaseIdQuoteId(self.extend(request, params)) + # + # { + # "status":"0000", + # "data":[ + # { + # "transaction_date":"2020-04-23 22:21:46", + # "type":"ask", + # "units_traded":"0.0125", + # "price":"8667000", + # "total":"108337" + # }, + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_trades(data, market, since, limit) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://apidocs.bithumb.com/v1.2.0/reference/%EC%A7%80%EC%A0%95%EA%B0%80-%EC%A3%BC%EB%AC%B8%ED%95%98%EA%B8%B0 + https://apidocs.bithumb.com/v1.2.0/reference/%EC%8B%9C%EC%9E%A5%EA%B0%80-%EB%A7%A4%EC%88%98%ED%95%98%EA%B8%B0 + https://apidocs.bithumb.com/v1.2.0/reference/%EC%8B%9C%EC%9E%A5%EA%B0%80-%EB%A7%A4%EB%8F%84%ED%95%98%EA%B8%B0 + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'order_currency': market['id'], + 'payment_currency': market['quote'], + 'units': amount, + } + method = 'privatePostTradePlace' + if type == 'limit': + request['price'] = price + request['type'] = 'bid' if (side == 'buy') else 'ask' + else: + method = 'privatePostTradeMarket' + self.capitalize(side) + response = getattr(self, method)(self.extend(request, params)) + id = self.safe_string(response, 'order_id') + if id is None: + raise InvalidOrder(self.id + ' createOrder() did not return an order id') + return self.safe_order({ + 'info': response, + 'symbol': symbol, + 'type': type, + 'side': side, + 'id': id, + }, market) + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://apidocs.bithumb.com/v1.2.0/reference/%EA%B1%B0%EB%9E%98-%EC%A3%BC%EB%AC%B8%EB%82%B4%EC%97%AD-%EC%83%81%EC%84%B8-%EC%A1%B0%ED%9A%8C + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrder() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'order_id': id, + 'count': 1, + 'order_currency': market['base'], + 'payment_currency': market['quote'], + } + response = self.privatePostInfoOrderDetail(self.extend(request, params)) + # + # { + # "status": "0000", + # "data": { + # "order_date": "1603161798539254", + # "type": "ask", + # "order_status": "Cancel", + # "order_currency": "BTC", + # "payment_currency": "KRW", + # "watch_price": "0", + # "order_price": "13344000", + # "order_qty": "0.0125", + # "cancel_date": "1603161803809993", + # "cancel_type": "사용자취소", + # "contract": [ + # { + # "transaction_date": "1603161799976383", + # "price": "13344000", + # "units": "0.0015", + # "fee_currency": "KRW", + # "fee": "0", + # "total": "20016" + # } + # ], + # } + # } + # + data = self.safe_dict(response, 'data') + return self.parse_order(self.extend(data, {'order_id': id}), market) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'Pending': 'open', + 'Completed': 'closed', + 'Cancel': 'canceled', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # + # fetchOrder + # + # { + # "transaction_date": "1572497603668315", + # "type": "bid", + # "order_status": "Completed", # Completed, Cancel ... + # "order_currency": "BTC", + # "payment_currency": "KRW", + # "watch_price": "0", # present in Cancel order + # "order_price": "8601000", + # "order_qty": "0.007", + # "cancel_date": "", # filled in Cancel order + # "cancel_type": "", # filled in Cancel order, i.e. 사용자취소 + # "contract": [ + # { + # "transaction_date": "1572497603902030", + # "price": "8601000", + # "units": "0.005", + # "fee_currency": "KRW", + # "fee": "107.51", + # "total": "43005" + # }, + # ] + # } + # + # fetchOpenOrders + # + # { + # "order_currency": "BTC", + # "payment_currency": "KRW", + # "order_id": "C0101000007408440032", + # "order_date": "1571728739360570", + # "type": "bid", + # "units": "5.0", + # "units_remaining": "5.0", + # "price": "501000", + # } + # + timestamp = self.safe_integer_product(order, 'order_date', 0.001) + sideProperty = self.safe_string_2(order, 'type', 'side') + side = 'buy' if (sideProperty == 'bid') else 'sell' + status = self.parse_order_status(self.safe_string(order, 'order_status')) + price = self.safe_string_2(order, 'order_price', 'price') + type = 'limit' + if Precise.string_equals(price, '0'): + type = 'market' + amount = self.fix_comma_number(self.safe_string_2(order, 'order_qty', 'units')) + remaining = self.fix_comma_number(self.safe_string(order, 'units_remaining')) + if remaining is None: + if status == 'closed': + remaining = '0' + elif status != 'canceled': + remaining = amount + symbol = None + baseId = self.safe_string(order, 'order_currency') + quoteId = self.safe_string(order, 'payment_currency') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + if (base is not None) and (quote is not None): + symbol = base + '/' + quote + if symbol is None: + market = self.safe_market(None, market) + symbol = market['symbol'] + id = self.safe_string(order, 'order_id') + rawTrades = self.safe_list(order, 'contract', []) + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'symbol': symbol, + 'type': type, + 'timeInForce': None, + 'postOnly': None, + 'side': side, + 'price': price, + 'triggerPrice': None, + 'amount': amount, + 'cost': None, + 'average': None, + 'filled': None, + 'remaining': remaining, + 'status': status, + 'fee': None, + 'trades': rawTrades, + }, market) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://apidocs.bithumb.com/v1.2.0/reference/%EA%B1%B0%EB%9E%98-%EC%A3%BC%EB%AC%B8%EB%82%B4%EC%97%AD-%EC%A1%B0%ED%9A%8C + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOpenOrders() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + if limit is None: + limit = 100 + request: dict = { + 'count': limit, + 'order_currency': market['base'], + 'payment_currency': market['quote'], + } + if since is not None: + request['after'] = since + response = self.privatePostInfoOrders(self.extend(request, params)) + # + # { + # "status": "0000", + # "data": [ + # { + # "order_currency": "BTC", + # "payment_currency": "KRW", + # "order_id": "C0101000007408440032", + # "order_date": "1571728739360570", + # "type": "bid", + # "units": "5.0", + # "units_remaining": "5.0", + # "price": "501000", + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_orders(data, market, since, limit) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://apidocs.bithumb.com/v1.2.0/reference/%EC%A3%BC%EB%AC%B8-%EC%B7%A8%EC%86%8C%ED%95%98%EA%B8%B0 + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + side_in_params = ('side' in params) + if not side_in_params: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a `side` parameter(sell or buy)') + market = self.market(symbol) + side = 'bid' if (params['side'] == 'buy') else 'ask' + params = self.omit(params, ['side', 'currency']) + # https://github.com/ccxt/ccxt/issues/6771 + request: dict = { + 'order_id': id, + 'type': side, + 'order_currency': market['base'], + 'payment_currency': market['quote'], + } + response = self.privatePostTradeCancel(self.extend(request, params)) + # + # { + # 'status': 'string', + # } + # + return self.safe_order({ + 'info': response, + }) + + def cancel_unified_order(self, order: Order, params={}): + request: dict = { + 'side': order['side'], + } + return self.cancel_order(order['id'], order['symbol'], self.extend(request, params)) + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://apidocs.bithumb.com/v1.2.0/reference/%EC%BD%94%EC%9D%B8-%EC%B6%9C%EA%B8%88%ED%95%98%EA%B8%B0-%EA%B0%9C%EC%9D%B8 + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.check_address(address) + self.load_markets() + currency = self.currency(code) + request: dict = { + 'units': amount, + 'address': address, + 'currency': currency['id'], + } + if code == 'XRP' or code == 'XMR' or code == 'EOS' or code == 'STEEM' or code == 'TON': + destination = self.safe_string(params, 'destination') + if (tag is None) and (destination is None): + raise ArgumentsRequired(self.id + ' ' + code + ' withdraw() requires a tag argument or an extra destination param') + elif tag is not None: + request['destination'] = tag + response = self.privatePostTradeBtcWithdrawal(self.extend(request, params)) + # + # {"status" : "0000"} + # + return self.parse_transaction(response, currency) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # withdraw + # + # {"status" : "0000"} + # + currency = self.safe_currency(None, currency) + return { + 'id': None, + 'txid': None, + 'timestamp': None, + 'datetime': None, + 'network': None, + 'addressFrom': None, + 'address': None, + 'addressTo': None, + 'amount': None, + 'type': None, + 'currency': currency['code'], + 'status': None, + 'updated': None, + 'tagFrom': None, + 'tag': None, + 'tagTo': None, + 'comment': None, + 'internal': None, + 'fee': None, + 'info': transaction, + } + + def fix_comma_number(self, numberStr): + # some endpoints need self https://github.com/ccxt/ccxt/issues/11031 + if numberStr is None: + return None + finalNumberStr = numberStr + while(finalNumberStr.find(',') > -1): + finalNumberStr = finalNumberStr.replace(',', '') + return finalNumberStr + + def nonce(self): + return self.milliseconds() + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + endpoint = '/' + self.implode_params(path, params) + url = self.implode_hostname(self.urls['api'][api]) + endpoint + query = self.omit(params, self.extract_params(path)) + if api == 'public': + if query: + url += '?' + self.urlencode(query) + else: + self.check_required_credentials() + body = self.urlencode(self.extend({ + 'endpoint': endpoint, + }, query)) + nonce = str(self.nonce()) + auth = endpoint + "\0" + body + "\0" + nonce # eslint-disable-line quotes + signature = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha512) + signature64 = self.string_to_base64(signature) + headers = { + 'Accept': 'application/json', + 'Content-Type': 'application/x-www-form-urlencoded', + 'Api-Key': self.apiKey, + 'Api-Sign': signature64, + 'Api-Nonce': nonce, + } + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None # fallback to default error handler + if 'status' in response: + # + # {"status":"5100","message":"After May 23th, recent_transactions is no longer, hence users will not be able to connect to recent_transactions"} + # + status = self.safe_string(response, 'status') + message = self.safe_string(response, 'message') + if status is not None: + if status == '0000': + return None # no error + elif message == '거래 진행중인 내역이 존재하지 않습니다.': + # https://github.com/ccxt/ccxt/issues/9017 + return None # no error + feedback = self.id + ' ' + message + self.throw_exactly_matched_exception(self.exceptions, status, feedback) + self.throw_exactly_matched_exception(self.exceptions, message, feedback) + raise ExchangeError(feedback) + return None diff --git a/ccxt/bitmart.py b/ccxt/bitmart.py new file mode 100644 index 0000000..b7fa8f4 --- /dev/null +++ b/ccxt/bitmart.py @@ -0,0 +1,5360 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.bitmart import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, BorrowInterest, Currencies, Currency, DepositAddress, FundingHistory, Int, IsolatedBorrowRate, IsolatedBorrowRates, LedgerEntry, Market, Num, Order, OrderBook, OrderRequest, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, Trade, TradingFeeInterface, Transaction, MarketInterface, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import AccountSuspended +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidAddress +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import NotSupported +from ccxt.base.errors import NetworkError +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.errors import OnMaintenance +from ccxt.base.errors import InvalidNonce +from ccxt.base.decimal_to_precision import TRUNCATE +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class bitmart(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(bitmart, self).describe(), { + 'id': 'bitmart', + 'name': 'BitMart', + 'countries': ['US', 'CN', 'HK', 'KR'], + # 150 per 5 seconds = 30 per second + # rateLimit = 1000ms / 30 ~= 33.334 + 'rateLimit': 33.34, + 'version': 'v2', + 'certified': True, + 'pro': True, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': True, + 'swap': True, + 'future': False, + 'option': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': True, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'cancelOrders': True, + 'createMarketBuyOrderWithCost': True, + 'createMarketOrderWithCost': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createOrders': True, + 'createPostOnlyOrder': True, + 'createReduceOnlyOrder': True, + 'createStopLimitOrder': False, + 'createStopMarketOrder': False, + 'createStopOrder': False, + 'createTrailingPercentOrder': True, + 'fetchBalance': True, + 'fetchBorrowInterest': True, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchCanceledOrders': True, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDeposit': True, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchDepositWithdrawFee': True, + 'fetchDepositWithdrawFees': False, + 'fetchFundingHistory': True, + 'fetchFundingRate': True, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchIsolatedBorrowRate': True, + 'fetchIsolatedBorrowRates': True, + 'fetchLedger': True, + 'fetchLiquidations': False, + 'fetchMarginMode': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': True, + 'fetchMyLiquidations': True, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': True, + 'fetchOpenInterestHistory': False, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': False, + 'fetchOrderTrades': True, + 'fetchPosition': True, + 'fetchPositionMode': True, + 'fetchPositions': True, + 'fetchStatus': True, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': True, + 'fetchTradingFees': False, + 'fetchTransactionFee': True, + 'fetchTransactionFees': False, + 'fetchTransfer': False, + 'fetchTransfers': True, + 'fetchWithdrawAddresses': True, + 'fetchWithdrawAddressesByNetwork': False, + 'fetchWithdrawal': True, + 'fetchWithdrawals': True, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': True, + 'setLeverage': True, + 'setMarginMode': False, + 'setPositionMode': True, + 'transfer': True, + 'withdraw': True, + }, + 'hostname': 'bitmart.com', # bitmart.info, bitmart.news for Hong Kong users + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/0623e9c4-f50e-48c9-82bd-65c3908c3a14', + 'api': { + 'spot': 'https://api-cloud.{hostname}', + 'swap': 'https://api-cloud-v2.{hostname}', # bitmart.info for Hong Kong users + }, + 'www': 'https://www.bitmart.com/', + 'doc': 'https://developer-pro.bitmart.com/', + 'referral': { + 'url': 'http://www.bitmart.com/?r=rQCFLh', + 'discount': 0.3, + }, + 'fees': 'https://www.bitmart.com/fee/en', + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + 'uid': True, + }, + 'api': { + 'public': { + 'get': { + 'system/time': 3, # 10 times/sec => 30/10 = 3 + 'system/service': 3, + # spot markets + 'spot/v1/currencies': 7.5, + 'spot/v1/symbols': 7.5, + 'spot/v1/symbols/details': 5, + 'spot/quotation/v3/tickers': 6, # 10 times/2 sec = 5/s => 30/5 = 6 + 'spot/quotation/v3/ticker': 4, # 15 times/2 sec = 7.5/s => 30/7.5 = 4 + 'spot/quotation/v3/lite-klines': 5, # should be 4 but errors + 'spot/quotation/v3/klines': 7, # should be 6 but errors + 'spot/quotation/v3/books': 4, # 15 times/2 sec = 7.5/s => 30/7.5 = 4 + 'spot/quotation/v3/trades': 4, # 15 times/2 sec = 7.5/s => 30/7.5 = 4 + 'spot/v1/ticker': 5, + 'spot/v2/ticker': 30, + 'spot/v1/ticker_detail': 5, # 12 times/2 sec = 6/s => 30/6 = 5 + 'spot/v1/steps': 30, + 'spot/v1/symbols/kline': 6, # should be 5 but errors + 'spot/v1/symbols/book': 5, + 'spot/v1/symbols/trades': 5, + # contract markets + 'contract/v1/tickers': 15, + 'contract/public/details': 5, + 'contract/public/depth': 5, + 'contract/public/open-interest': 30, + 'contract/public/funding-rate': 30, + 'contract/public/funding-rate-history': 30, + 'contract/public/kline': 6, # should be 5 but errors + 'account/v1/currencies': 30, + 'contract/public/markprice-kline': 5, # 6 times per 1 second + }, + }, + 'private': { + 'get': { + # sub-account + 'account/sub-account/v1/transfer-list': 7.5, + 'account/sub-account/v1/transfer-history': 7.5, + 'account/sub-account/main/v1/wallet': 5, + 'account/sub-account/main/v1/subaccount-list': 7.5, + 'account/contract/sub-account/main/v1/wallet': 5, + 'account/contract/sub-account/main/v1/transfer-list': 7.5, + 'account/contract/sub-account/v1/transfer-history': 7.5, + # account + 'account/v1/wallet': 5, + 'account/v1/currencies': 30, + 'spot/v1/wallet': 5, + 'account/v1/deposit/address': 30, + 'account/v1/withdraw/charge': 32, # should be 30 but errors + 'account/v2/deposit-withdraw/history': 7.5, + 'account/v1/deposit-withdraw/detail': 7.5, + 'account/v1/withdraw/address/list': 30, # 2 times per 2 seconds + # order + 'spot/v1/order_detail': 1, + 'spot/v2/orders': 5, + 'spot/v1/trades': 5, + # newer order endpoint + 'spot/v2/trades': 4, + 'spot/v3/orders': 5, + 'spot/v2/order_detail': 1, + # margin + 'spot/v1/margin/isolated/borrow_record': 1, + 'spot/v1/margin/isolated/repay_record': 1, + 'spot/v1/margin/isolated/pairs': 30, + 'spot/v1/margin/isolated/account': 5, + 'spot/v1/trade_fee': 30, + 'spot/v1/user_fee': 30, + # broker + 'spot/v1/broker/rebate': 1, + # contract + 'contract/private/assets-detail': 5, + 'contract/private/order': 1.2, + 'contract/private/order-history': 10, + 'contract/private/position': 10, + 'contract/private/position-v2': 10, + 'contract/private/get-open-orders': 1.2, + 'contract/private/current-plan-order': 1.2, + 'contract/private/trades': 10, + 'contract/private/position-risk': 10, + 'contract/private/affilate/rebate-list': 10, + 'contract/private/affilate/trade-list': 10, + 'contract/private/transaction-history': 10, + 'contract/private/get-position-mode': 1, + }, + 'post': { + # sub-account endpoints + 'account/sub-account/main/v1/sub-to-main': 30, + 'account/sub-account/sub/v1/sub-to-main': 30, + 'account/sub-account/main/v1/main-to-sub': 30, + 'account/sub-account/sub/v1/sub-to-sub': 30, + 'account/sub-account/main/v1/sub-to-sub': 30, + 'account/contract/sub-account/main/v1/sub-to-main': 7.5, + 'account/contract/sub-account/main/v1/main-to-sub': 7.5, + 'account/contract/sub-account/sub/v1/sub-to-main': 7.5, + # account + 'account/v1/withdraw/apply': 7.5, + # transaction and trading + 'spot/v1/submit_order': 1, + 'spot/v1/batch_orders': 1, + 'spot/v2/cancel_order': 1, + 'spot/v1/cancel_orders': 15, + 'spot/v4/query/order': 1, # 60 times/2 sec = 30/s => 30/30 = 1 + 'spot/v4/query/client-order': 1, # 60 times/2 sec = 30/s => 30/30 = 1 + 'spot/v4/query/open-orders': 5, # 12 times/2 sec = 6/s => 30/6 = 5 + 'spot/v4/query/history-orders': 5, # 12 times/2 sec = 6/s => 30/6 = 5 + 'spot/v4/query/trades': 5, # 12 times/2 sec = 6/s => 30/6 = 5 + 'spot/v4/query/order-trades': 5, # 12 times/2 sec = 6/s => 30/6 = 5 + 'spot/v4/cancel_orders': 3, + 'spot/v4/cancel_all': 90, + 'spot/v4/batch_orders': 3, + # newer endpoint + 'spot/v3/cancel_order': 1, + 'spot/v2/batch_orders': 1, + 'spot/v2/submit_order': 1, + # margin + 'spot/v1/margin/submit_order': 1.5, # 20 times per second + 'spot/v1/margin/isolated/borrow': 30, + 'spot/v1/margin/isolated/repay': 30, + 'spot/v1/margin/isolated/transfer': 30, + # contract + 'account/v1/transfer-contract-list': 60, + 'account/v1/transfer-contract': 60, + 'contract/private/submit-order': 2.5, + 'contract/private/cancel-order': 1.5, + 'contract/private/cancel-orders': 30, + 'contract/private/submit-plan-order': 2.5, + 'contract/private/cancel-plan-order': 1.5, + 'contract/private/submit-leverage': 2.5, + 'contract/private/submit-tp-sl-order': 2.5, + 'contract/private/modify-plan-order': 2.5, + 'contract/private/modify-preset-plan-order': 2.5, + 'contract/private/modify-limit-order': 2.5, + 'contract/private/modify-tp-sl-order': 2.5, + 'contract/private/submit-trail-order': 2.5, # weight is not provided by the exchange, is set order + 'contract/private/cancel-trail-order': 1.5, # weight is not provided by the exchange, is set order + 'contract/private/set-position-mode': 1, + }, + }, + }, + 'timeframes': { + '1m': 1, + '3m': 3, + '5m': 5, + '15m': 15, + '30m': 30, + '45m': 45, + '1h': 60, + '2h': 120, + '3h': 180, + '4h': 240, + '1d': 1440, + '1w': 10080, + '1M': 43200, + }, + 'fees': { + 'trading': { + 'tierBased': True, + 'percentage': True, + 'taker': self.parse_number('0.0040'), + 'maker': self.parse_number('0.0035'), + 'tiers': { + 'taker': [ + [self.parse_number('0'), self.parse_number('0.0020')], + [self.parse_number('10'), self.parse_number('0.18')], + [self.parse_number('50'), self.parse_number('0.0016')], + [self.parse_number('250'), self.parse_number('0.0014')], + [self.parse_number('1000'), self.parse_number('0.0012')], + [self.parse_number('5000'), self.parse_number('0.0010')], + [self.parse_number('25000'), self.parse_number('0.0008')], + [self.parse_number('50000'), self.parse_number('0.0006')], + ], + 'maker': [ + [self.parse_number('0'), self.parse_number('0.001')], + [self.parse_number('10'), self.parse_number('0.0009')], + [self.parse_number('50'), self.parse_number('0.0008')], + [self.parse_number('250'), self.parse_number('0.0007')], + [self.parse_number('1000'), self.parse_number('0.0006')], + [self.parse_number('5000'), self.parse_number('0.0005')], + [self.parse_number('25000'), self.parse_number('0.0004')], + [self.parse_number('50000'), self.parse_number('0.0003')], + ], + }, + }, + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'exact': { + # general errors + '30000': ExchangeError, # 404, Not found + '30001': AuthenticationError, # 401, Header X-BM-KEY is empty + '30002': AuthenticationError, # 401, Header X-BM-KEY not found + '30003': AccountSuspended, # 401, Header X-BM-KEY has frozen + '30004': AuthenticationError, # 401, Header X-BM-SIGN is empty + '30005': AuthenticationError, # 401, Header X-BM-SIGN is wrong + '30006': AuthenticationError, # 401, Header X-BM-TIMESTAMP is empty + '30007': AuthenticationError, # 401, Header X-BM-TIMESTAMP range. Within a minute + '30008': AuthenticationError, # 401, Header X-BM-TIMESTAMP invalid format + '30010': PermissionDenied, # 403, IP is forbidden. We recommend enabling IP whitelist for API trading. After that reauth your account + '30011': AuthenticationError, # 403, Header X-BM-KEY over expire time + '30012': AuthenticationError, # 403, Header X-BM-KEY is forbidden to request it + '30013': RateLimitExceeded, # 429, Request too many requests + '30014': ExchangeNotAvailable, # 503, Service unavailable + '30016': OnMaintenance, # 200, Service maintenance, the function is temporarily unavailable + '30017': RateLimitExceeded, # 418, Your account request is temporarily rejected due to violation of current limiting rules + '30018': BadRequest, # 503, Request Body requires JSON format + '30019': PermissionDenied, # 200, You do not have the permissions to perform self operation + # funding account & sub account errors + '60000': BadRequest, # 400, Invalid request(maybe the body is empty, or the int parameter passes string data) + '60001': BadRequest, # 400, Asset account type does not exist + '60002': BadRequest, # 400, currency does not exist + '60003': ExchangeError, # 400, Currency has been closed recharge channel, if there is any problem, please consult customer service + '60004': ExchangeError, # 400, Currency has been closed withdraw channel, if there is any problem, please consult customer service + '60005': ExchangeError, # 400, Minimum amount is %s + '60006': ExchangeError, # 400, Maximum withdraw precision is %d + '60007': InvalidAddress, # 400, Only withdrawals from added addresses are allowed + '60008': InsufficientFunds, # 400, Balance not enough + '60009': ExchangeError, # 400, Beyond the limit + '60010': ExchangeError, # 400, Withdraw id or deposit id not found + '60011': InvalidAddress, # 400, Address is not valid + '60012': ExchangeError, # 400, This action is not hasattr(self, supported) currency(If IOTA, HLX recharge and withdraw calls are prohibited) + '60020': PermissionDenied, # 403, Your account is not allowed to recharge + '60021': PermissionDenied, # 403, Your account is not allowed to withdraw + '60022': PermissionDenied, # 403, No withdrawals for 24 hours + '60026': PermissionDenied, # 403, Sub-account does not have permission to operate + '60027': PermissionDenied, # 403, Only supports sub-account calls + '60028': AccountSuspended, # 403, Account is disabled for security reasons, please contact customer service + '60029': AccountSuspended, # 403, The account is frozen by the master account, please contact the master account to unfreeze the account + '60030': BadRequest, # 405, Method Not Allowed + '60031': BadRequest, # 415, Unsupported Media Type + '60050': ExchangeError, # 500, User account not found + '60051': ExchangeError, # 500, Internal Server Error + '61001': InsufficientFunds, # {"message":"Balance not enough","code":61001,"trace":"b85ea1f8-b9af-4001-ac5f-9e061fe93d78","data":{}} + '61003': BadRequest, # 400, {"message":"sub-account not found","code":61003,"trace":"b35ec2fd-0bc9-4ef2-a3c0-6f78d4f335a4","data":{}} + '61004': BadRequest, # 400, Duplicate requests(such an existing requestNo) + '61005': BadRequest, # 403, Asset transfer between accounts is not available + '61006': NotSupported, # 403, The sub-account api only supports organization accounts + '61007': ExchangeError, # 403, Please complete your institution verification to enable withdrawal function. + '61008': ExchangeError, # 403, Suspend transfer out + # spot public errors + '70000': ExchangeError, # 200, no data + '70001': BadRequest, # 200, request param can not be null + '70002': BadSymbol, # 200, symbol is invalid + '70003': NetworkError, # {"code":70003,"trace":"81a9d57b63be4819b65d3065e6a4682b.105.17105295323593915","message":"net error, please try later","data":null} + '71001': BadRequest, # 200, after is invalid + '71002': BadRequest, # 200, before is invalid + '71003': BadRequest, # 200, request after or before is invalid + '71004': BadRequest, # 200, request kline count limit + '71005': BadRequest, # 200, request step error + # spot & margin errors + '50000': BadRequest, # 400, Bad Request + '50001': BadSymbol, # 400, Symbol not found + '50002': BadRequest, # 400, From Or To format error + '50003': BadRequest, # 400, Step format error + '50004': BadRequest, # 400, Kline size over 500 + '50005': OrderNotFound, # 400, Order Id not found + '50006': InvalidOrder, # 400, Minimum size is %s + '50007': InvalidOrder, # 400, Maximum size is %s + '50008': InvalidOrder, # 400, Minimum price is %s + '50009': InvalidOrder, # 400, Minimum count*price is %s + '50010': InvalidOrder, # 400, RequestParam size is required + '50011': InvalidOrder, # 400, RequestParam price is required + '50012': InvalidOrder, # 400, RequestParam notional is required + '50013': InvalidOrder, # 400, Maximum limit*offset is %d + '50014': BadRequest, # 400, RequestParam limit is required + '50015': BadRequest, # 400, Minimum limit is 1 + '50016': BadRequest, # 400, Maximum limit is %d + '50017': BadRequest, # 400, RequestParam offset is required + '50018': BadRequest, # 400, Minimum offset is 1 + '50019': ExchangeError, # 400, Invalid status. validate status is [1=Failed, 2=Success, 3=Frozen Failed, 4=Frozen Success, 5=Partially Filled, 6=Fully Fulled, 7=Canceling, 8=Canceled] '50020': InsufficientFunds, # 400, Balance not enough + '50020': InsufficientFunds, # 400, Balance not enough + '50021': BadRequest, # 400, Invalid %s + '50022': ExchangeNotAvailable, # 400, Service unavailable + '50023': BadSymbol, # 400, This Symbol can't place order by api + '50024': BadRequest, # 400, Order book size over 200 + '50025': BadRequest, # 400, Maximum price is %s + '50026': BadRequest, # 400, The buy order price cannot be higher than the open price + '50027': BadRequest, # 400, The sell order price cannot be lower than the open price + '50028': BadRequest, # 400, Missing parameters + '50029': InvalidOrder, # 400, {"message":"param not match : size * price >=1000","code":50029,"trace":"f931f030-b692-401b-a0c5-65edbeadc598","data":{}} + '50030': OrderNotFound, # 400, {"message":"Order is already canceled","code":50030,"trace":"8d6f64ee-ad26-45a4-9efd-1080f9fca1fa","data":{}} + '50031': OrderNotFound, # 400, Order is already completed + '50032': OrderNotFound, # 400, {"message":"Order does not exist","code":50032,"trace":"8d6b482d-4bf2-4e6c-aab2-9dcd22bf2481","data":{}} + '50033': InvalidOrder, # 400, The order quantity should be greater than 0 and less than or equal to 10 + # below Error codes used interchangeably for both failed postOnly and IOC orders depending on market price and order side + '50034': InvalidOrder, # 400, {"message":"The price is high and there is no matching depth","code":50034,"trace":"ebfae59a-ba69-4735-86b2-0ed7b9ca14ea","data":{}} + '50035': InvalidOrder, # 400, {"message":"The price is low and there is no matching depth","code":50035,"trace":"677f01c7-8b88-4346-b097-b4226c75c90e","data":{}} + '50036': ExchangeError, # 400, Cancel failed, order is not revocable status + '50037': BadRequest, # 400, The maximum length of clientOrderId cannot exceed 32 + '50038': BadRequest, # 400, ClientOrderId only allows a combination of numbers and letters + '50039': BadRequest, # 400, Order_id and clientOrderId cannot be empty at the same time + '50040': BadSymbol, # 400, Symbol Not Available + '50041': ExchangeError, # 400, Out of query time range + '50042': BadRequest, # 400, clientOrderId is duplicate + '51000': BadSymbol, # 400, Currency not found + '51001': ExchangeError, # 400, Margin Account not Opened + '51002': ExchangeError, # 400, Margin Account Not Available + '51003': ExchangeError, # 400, Account Limit + '51004': InsufficientFunds, # 400, {"message":"Exceed the maximum number of borrows available.","code":51004,"trace":"4030b753-9beb-44e6-8352-1633c5edcd47","data":{}} + '51005': InvalidOrder, # 400, Less than the minimum borrowable amount + '51006': InvalidOrder, # 400, Exceeds the amount to be repaid + '51007': BadRequest, # 400, order_mode not found + '51008': ExchangeError, # 400, Operation is limited, please try again later + '51009': InvalidOrder, # 400, Parameter mismatch: limit order/market order quantity should be greater than the minimum number of should buy/sell + '51010': InvalidOrder, # 400, Parameter mismatch: limit order price should be greater than the minimum buy price + '51011': InvalidOrder, # 400, {"message":"param not match : size * price >=5","code":51011,"trace":"525e1d27bfd34d60b2d90ba13a7c0aa9.74.16696421352220797","data":{}} + '51012': InvalidOrder, # 400, Parameter mismatch: limit order price should be greater than the minimum buy price + '51013': InvalidOrder, # 400, Parameter mismatch: Limit order quantity * price should be greater than the minimum transaction amount + '51014': InvalidOrder, # 400, Participation mismatch: the number of market order buy orders should be greater than the minimum buyable amount + '51015': InvalidOrder, # 400, Parameter mismatch: the price of market order buy order placed is too small + '52000': BadRequest, # 400, Unsupported OrderMode Type + '52001': BadRequest, # 400, Unsupported Trade Type + '52002': BadRequest, # 400, Unsupported Side Type + '52003': BadRequest, # 400, Unsupported Query State Type + '52004': BadRequest, # 400, End time must be greater than or equal to Start time + '53000': AccountSuspended, # 403, Your account is frozen due to security policies. Please contact customer service + '53001': AccountSuspended, # 403, {"message":"Your kyc country is restricted. Please contact customer service.","code":53001,"trace":"8b445940-c123-4de9-86d7-73c5be2e7a24","data":{}} + '53002': PermissionDenied, # 403, Your account has not yet completed the kyc advanced certification, please complete first + '53003': PermissionDenied, # 403 No permission, please contact the main account + '53005': PermissionDenied, # 403 Don't have permission to access the interface + '53006': PermissionDenied, # 403 Please complete your personal verification(Starter) + '53007': PermissionDenied, # 403 Please complete your personal verification(Advanced) + '53008': PermissionDenied, # 403 Services is not available in your countries and areas + '53009': PermissionDenied, # 403 Your account has not yet completed the qr code certification, please complete first + '53010': PermissionDenied, # 403 This account is restricted from borrowing + '57001': BadRequest, # 405, Method Not Allowed + '58001': BadRequest, # 415, Unsupported Media Type + '59001': ExchangeError, # 500, User account not found + '59002': ExchangeError, # 500, Internal Server Error + '59003': ExchangeError, # 500, Spot wallet call fail + '59004': ExchangeError, # 500, Margin wallet service call exception + '59005': PermissionDenied, # 500, Margin wallet service restricted + '59006': ExchangeError, # 500, Transfer fail + '59007': ExchangeError, # 500, Get symbol risk data fail + '59008': ExchangeError, # 500, Trading order failure + '59009': ExchangeError, # 500, Loan success,but trading order failure + '59010': InsufficientFunds, # 500, Insufficient loan amount. + '59011': ExchangeError, # 500, The Get Wallet Balance service call fail, please try again later + # contract errors + '40001': ExchangeError, # 400, Cloud account not found + '40002': ExchangeError, # 400, out_trade_no not found + '40003': ExchangeError, # 400, out_trade_no already existed + '40004': ExchangeError, # 400, Cloud account count limit + '40005': ExchangeError, # 400, Transfer vol precision error + '40006': PermissionDenied, # 400, Invalid ip error + '40007': BadRequest, # 400, Parse parameter error + '40008': InvalidNonce, # 400, Check nonce error + '40009': BadRequest, # 400, Check ver error + '40010': BadRequest, # 400, Not found func error + '40011': BadRequest, # 400, Invalid request + '40012': ExchangeError, # 500, System error + '40013': ExchangeError, # 400, Access too often" CLIENT_TIME_INVALID, "Please check your system time. + '40014': BadSymbol, # 400, This contract is offline + '40015': BadSymbol, # 400, This contract's exchange has been paused + '40016': InvalidOrder, # 400, This order would trigger user position liquidate + '40017': InvalidOrder, # 400, It is not possible to open and close simultaneously in the same position + '40018': InvalidOrder, # 400, Your position is closed + '40019': ExchangeError, # 400, Your position is in liquidation delegating + '40020': InvalidOrder, # 400, Your position volume is not enough + '40021': ExchangeError, # 400, The position is not exsit + '40022': ExchangeError, # 400, The position is not isolated + '40023': ExchangeError, # 400, The position would liquidate when sub margin + '40024': ExchangeError, # 400, The position would be warnning of liquidation when sub margin + '40025': ExchangeError, # 400, The position’s margin shouldn’t be lower than the base limit + '40026': ExchangeError, # 400, You cross margin position is in liquidation delegating + '40027': InsufficientFunds, # 400, You contract account available balance not enough + '40028': PermissionDenied, # 400, Your plan order's count is more than system maximum limit. + '40029': InvalidOrder, # 400, The order's leverage is too large. + '40030': InvalidOrder, # 400, The order's leverage is too small. + '40031': InvalidOrder, # 400, The deviation between current price and trigger price is too large. + '40032': InvalidOrder, # 400, The plan order's life cycle is too long. + '40033': InvalidOrder, # 400, The plan order's life cycle is too short. + '40034': BadSymbol, # 400, This contract is not found + '40035': OrderNotFound, # 400, The order is not exist + '40036': InvalidOrder, # 400, The order status is invalid + '40037': OrderNotFound, # 400, The order id is not exist + '40038': BadRequest, # 400, The k-line step is invalid + '40039': BadRequest, # 400, The timestamp is invalid + '40040': InvalidOrder, # 400, The order leverage is invalid + '40041': InvalidOrder, # 400, The order side is invalid + '40042': InvalidOrder, # 400, The order type is invalid + '40043': InvalidOrder, # 400, The order precision is invalid + '40044': InvalidOrder, # 400, The order range is invalid + '40045': InvalidOrder, # 400, The order open type is invalid + '40046': PermissionDenied, # 403, The account is not opened futures + '40047': PermissionDenied, # 403, Services is not available in you countries and areas + '40048': InvalidOrder, # 403, ClientOrderId only allows a combination of numbers and letters + '40049': InvalidOrder, # 403, The maximum length of clientOrderId cannot exceed 32 + '40050': InvalidOrder, # 403, Client OrderId duplicated with existing orders + }, + 'broad': { + 'You contract account available balance not enough': InsufficientFunds, + 'you contract account available balance not enough': InsufficientFunds, + }, + }, + 'commonCurrencies': { + '$GM': 'GOLDMINER', + '$HERO': 'Step Hero', + '$PAC': 'PAC', + 'BP': 'BEYOND', + 'GDT': 'Gorilla Diamond', + 'GLD': 'Goldario', + 'MVP': 'MVP Coin', + 'TRU': 'Truebit', # conflict with TrueFi + }, + 'options': { + 'defaultNetworks': { + 'USDT': 'TRC20', + 'BTC': 'BTC', + 'ETH': 'ERC20', + }, + 'timeDifference': 0, # the difference between system clock and exchange clock + 'adjustForTimeDifference': False, # controls the adjustment logic upon instantiation + 'networks': { + 'ERC20': 'ERC20', + 'SOL': 'SOL', + 'BTC': 'BTC', + 'TRC20': 'TRC20', + # todo: should be TRX after unification + # 'TRC20': ['TRC20', 'trc20', 'TRON'], # todo: after unification i.e. TRON is returned from fetchDepositAddress + # 'ERC20': ['ERC20', 'ERC-20', 'ERC20 '], # todo: after unification + 'OMNI': 'OMNI', + 'XLM': 'XLM', + 'EOS': 'EOS', + 'NEO': 'NEO', + 'BTM': 'BTM', + 'BCH': 'BCH', + 'LTC': 'LTC', + 'BSV': 'BSV', + 'XRP': 'XRP', + # 'VECHAIN': ['VET', 'Vechain'], # todo: after unification + 'PLEX': 'PLEX', + 'XCH': 'XCH', + # 'AVALANCHE_C': ['AVAX', 'AVAX-C'], # todo: after unification + 'NEAR': 'NEAR', + 'FIO': 'FIO', + 'SCRT': 'SCRT', + 'IOTX': 'IOTX', + 'ALGO': 'ALGO', + 'ATOM': 'ATOM', + 'DOT': 'DOT', + 'ADA': 'ADA', + 'DOGE': 'DOGE', + 'XYM': 'XYM', + 'GLMR': 'GLMR', + 'MOVR': 'MOVR', + 'ZIL': 'ZIL', + 'INJ': 'INJ', + 'KSM': 'KSM', + 'ZEC': 'ZEC', + 'NAS': 'NAS', + 'POLYGON': 'MATIC', + 'HRC20': 'HECO', + 'XDC': 'XDC', + 'ONE': 'ONE', + 'LAT': 'LAT', + 'CSPR': 'Casper', + 'ICP': 'Computer', + 'XTZ': 'XTZ', + 'MINA': 'MINA', + 'BEP20': 'BSC_BNB', + 'THETA': 'THETA', + 'AKT': 'AKT', + 'AR': 'AR', + 'CELO': 'CELO', + 'FIL': 'FIL', + 'NULS': 'NULS', + 'ETC': 'ETC', + 'DASH': 'DASH', + 'DGB': 'DGB', + 'BEP2': 'BEP2', + 'GRIN': 'GRIN', + 'WAVES': 'WAVES', + 'ABBC': 'ABBC', + 'ACA': 'ACA', + 'QTUM': 'QTUM', + 'PAC': 'PAC', + # 'TERRACLASSIC': 'LUNC', # TBD + # 'TERRA': 'Terra', # TBD + # 'HEDERA': ['HBAR', 'Hedera', 'Hedera Mainnet'], # todo: after unification + 'TLOS': 'TLOS', + 'KARDIA': 'KardiaChain', + 'FUSE': 'FUSE', + 'TRC10': 'TRC10', + 'FIRO': 'FIRO', + 'FTM': 'Fantom', + # 'KLAYTN': ['klaytn', 'KLAY', 'Klaytn'], # todo: after unification + # 'ELROND': ['EGLD', 'Elrond eGold', 'MultiversX'], # todo: after unification + 'EVER': 'EVER', + 'KAVA': 'KAVA', + 'HYDRA': 'HYDRA', + 'PLCU': 'PLCU', + 'BRISE': 'BRISE', + # 'CRC20': ['CRO', 'CRO_Chain'], # todo: after unification + # 'CONFLUX': ['CFX eSpace', 'CFX'], # todo: after unification + 'OPTIMISM': 'OPTIMISM', + 'REEF': 'REEF', + 'SYS': 'SYS', # NEVM is different + 'VITE': 'VITE', + 'STX': 'STX', + 'SXP': 'SXP', + 'BITCI': 'BITCI', + # 'ARBITRUM': ['ARBI', 'Arbitrum'], # todo: after unification + 'XRD': 'XRD', + 'ASTR': 'ASTAR', + 'ZEN': 'HORIZEN', + 'LTO': 'LTO', + 'ETHW': 'ETHW', + 'ETHF': 'ETHF', + 'IOST': 'IOST', + # 'CHILIZ': ['CHZ', 'CHILIZ'], # todo: after unification + 'APT': 'APT', + # 'FLOW': ['FLOW', 'Flow'], # todo: after unification + 'ONT': 'ONT', + 'EVMOS': 'EVMOS', + 'XMR': 'XMR', + 'OASYS': 'OAS', + 'OSMO': 'OSMO', + 'OMAX': 'OMAX Chain', + 'DESO': 'DESO', + 'BFIC': 'BFIC', + 'OHO': 'OHO', + 'CS': 'CS', + 'CHEQ': 'CHEQ', + 'NODL': 'NODL', + 'NEM': 'XEM', + 'FRA': 'FRA', + 'ERGO': 'ERG', + # todo: below will be uncommented after unification + # 'BITCOINHD': 'BHD', + # 'CRUST': 'CRU', + # 'MINTME': 'MINTME', + # 'ZENITH': 'ZENITH', + # 'ZENIQ': 'ZENIQ', # "ZEN-20" is different + # 'BITCOINVAULT': 'BTCV', + # 'MOBILECOIN': 'MBX', + # 'PINETWORK': 'PI', + # 'PI': 'PI', + # 'REBUS': 'REBUS', + # 'XODEX': 'XODEX', + # 'ULTRONGLOW': 'UTG' + # 'QIBLOCKCHAIN': 'QIE', + # 'XIDEN': 'XDEN', + # 'PHAETON': 'PHAE', + # 'REDLIGHT': 'REDLC', + # 'VERITISE': 'VTS', + # 'VERIBLOCK': 'VBK', + # 'RAMESTTA': 'RAMA', + # 'BITICA': 'BDCC', + # 'CROWNSOVEREIGN': 'CSOV', + # 'DRAC': 'DRC20', + # 'QCHAIN': 'QDT', + # 'KINGARU': 'KRU', + # 'PROOFOFMEMES': 'POM', + # 'CUBE': 'CUBE', + # 'CADUCEUS': 'CMP', + # 'VEIL': 'VEIL', + # 'ENERGYWEB': 'EWT', + # 'CYPHERIUM': 'CPH', + # 'LBRY': 'LBC', + # 'ETHERCOIN': 'ETE', + # undetermined chains: + # LEX(for LexThum), TAYCAN(for TRICE), SFL(probably TAYCAN), OMNIA(for APEX), NAC(for NAC), KAG(Kinesis), CEM(crypto emergency), XVM(for Venidium), NEVM(for NEVM), IGT20(for IGNITE), FILM(FILMCredits), CC(CloudCoin), MERGE(MERGE), LTNM(Bitcoin latinum), PLUGCN( PlugChain), DINGO(dingo), LED(LEDGIS), AVAT(AVAT), VSOL(Vsolidus), EPIC(EPIC cash), NFC(netflowcoin), mrx(Metrix Coin), Idena(idena network), PKT(PKT Cash), BondDex(BondDex), XBN(XBN), KALAM(Kalamint), REV(RChain), KRC20(MyDeFiPet), ARC20(Hurricane Token), GMD(Coop network), BERS(Berith), ZEBI(Zebi), BRC(Baer Chain), DAPS(DAPS Coin), APL(Gold Secured Currency), NDAU(NDAU), WICC(WICC), UPG(Unipay God), TSL(TreasureSL), MXW(Maxonrow), CLC(Cifculation), SMH(SMH Coin), XIN(CPCoin), RDD(ReddCoin), OK(Okcash), KAR(KAR), CCX(ConcealNetwork), + }, + 'networksById': { + 'ETH': 'ERC20', + 'Ethereum': 'ERC20', + 'USDT': 'OMNI', # the default USDT network for bitmart is OMNI + 'Bitcoin': 'BTC', + }, + 'defaultType': 'spot', # 'spot', 'swap' + 'fetchBalance': { + 'type': 'spot', # 'spot', 'swap', 'account' + }, + 'accountsByType': { + 'spot': 'spot', + 'swap': 'swap', + }, + 'createMarketBuyOrderRequiresPrice': True, + 'brokerId': 'CCXTxBitmart000', + }, + 'features': { + 'default': { + 'sandbox': False, + 'createOrder': { + 'marginMode': True, + 'triggerPrice': False, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': False, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'marketBuyRequiresPrice': False, # todo: https://developer-pro.bitmart.com/en/spot/#new-order-v2-signed + 'marketBuyByCost': True, + 'leverage': True, # todo: implement + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': { + 'max': 10, + }, + 'fetchMyTrades': { + 'marginMode': True, + 'limit': 200, + 'daysBack': None, + 'untilDays': 99999, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': True, + 'limit': 200, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': { + 'marginMode': True, + 'limit': 200, + 'daysBack': None, + 'daysBackCanceled': None, + 'untilDays': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 1000, # variable timespans for recent endpoint, 200 for historical + }, + }, + 'forDerivatives': { + 'extends': 'default', + 'createOrder': { + 'marginMode': True, + 'triggerPrice': True, + 'triggerPriceType': { + 'last': True, + 'mark': True, + 'index': False, + }, + 'triggerDirection': True, # todo: implementation broken + 'stopLossPrice': True, + 'takeProfitPrice': True, + 'attachedStopLossTakeProfit': { + 'triggerPriceType': { + 'last': True, + 'mark': True, + 'index': False, + }, + 'price': False, + }, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': True, + 'marketBuyRequiresPrice': True, + 'marketBuyByCost': True, + # exchange-supported features + # 'selfTradePrevention': True, + # 'twap': False, + # 'iceberg': False, + # 'oco': False, + }, + 'fetchMyTrades': { + 'marginMode': True, + 'limit': None, + 'daysBack': None, + 'untilDays': 99999, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': True, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 100, + 'trigger': True, + 'trailing': False, + }, + 'fetchClosedOrders': { + 'marginMode': True, + 'limit': 200, + 'daysBack': None, + 'daysBackCanceled': None, + 'untilDays': None, + 'trigger': False, + 'trailing': False, + }, + 'fetchOHLCV': { + 'limit': 500, + }, + }, + 'spot': { + 'extends': 'default', + }, + 'swap': { + 'linear': { + 'extends': 'forDerivatives', + }, + 'inverse': { + 'extends': 'forDerivatives', + }, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + }) + + def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://developer-pro.bitmart.com/en/spot/#get-system-time + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = self.publicGetSystemTime(params) + # + # { + # "message":"OK", + # "code":1000, + # "trace":"c4e5e5b7-fe9f-4191-89f7-53f6c5bf9030", + # "data":{ + # "server_time":1599843709578 + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.safe_integer(data, 'server_time') + + def fetch_status(self, params={}): + """ + the latest known information on the availability of the exchange API + + https://developer-pro.bitmart.com/en/spot/#get-system-service-status + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `status structure ` + """ + options = self.safe_dict(self.options, 'fetchStatus', {}) + defaultType = self.safe_string(self.options, 'defaultType') + type = self.safe_string(options, 'type', defaultType) + type = self.safe_string(params, 'type', type) + params = self.omit(params, 'type') + response = self.publicGetSystemService(params) + # + # { + # "message": "OK", + # "code": 1000, + # "trace": "1d3f28b0-763e-4f78-90c4-5e3ad19dc595", + # "data": { + # "service": [ + # { + # "title": "Spot API Stop", + # "service_type": "spot", + # "status": 2, + # "start_time": 1648639069125, + # "end_time": 1648639069125 + # }, + # { + # "title": "Contract API Stop", + # "service_type": "contract", + # "status": 2, + # "start_time": 1648639069125, + # "end_time": 1648639069125 + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + services = self.safe_list(data, 'service', []) + servicesByType = self.index_by(services, 'service_type') + if type == 'swap': + type = 'contract' + service = self.safe_string(servicesByType, type) + status = None + eta = None + if service is not None: + statusCode = self.safe_integer(service, 'status') + if statusCode == 2: + status = 'ok' + else: + status = 'maintenance' + eta = self.safe_integer(service, 'end_time') + return { + 'status': status, + 'updated': None, + 'eta': eta, + 'url': None, + 'info': response, + } + + def fetch_spot_markets(self, params={}) -> List[MarketInterface]: + response = self.publicGetSpotV1SymbolsDetails(params) + # + # { + # "message":"OK", + # "code":1000, + # "trace":"a67c9146-086d-4d3f-9897-5636a9bb26e1", + # "data":{ + # "symbols":[ + # { + # "symbol": "BTC_USDT", + # "symbol_id": 53, + # "base_currency": "BTC", + # "quote_currency": "USDT", + # "base_min_size": "0.000010000000000000000000000000", + # "base_max_size": "100000000.000000000000000000000000000000", + # "price_min_precision": -1, + # "price_max_precision": 2, + # "quote_increment": "0.00001", # Api docs says "The minimum order quantity is also the minimum order quantity increment", however I think they mistakenly use the term 'order quantity' + # "expiration": "NA", + # "min_buy_amount": "5.000000000000000000000000000000", + # "min_sell_amount": "5.000000000000000000000000000000", + # "trade_status": "trading" + # }, + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + symbols = self.safe_list(data, 'symbols', []) + result = [] + fees = self.fees['trading'] + for i in range(0, len(symbols)): + market = symbols[i] + id = self.safe_string(market, 'symbol') + numericId = self.safe_integer(market, 'symbol_id') + baseId = self.safe_string(market, 'base_currency') + quoteId = self.safe_string(market, 'quote_currency') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + symbol = base + '/' + quote + minBuyCost = self.safe_string(market, 'min_buy_amount') + minSellCost = self.safe_string(market, 'min_sell_amount') + minCost = Precise.string_max(minBuyCost, minSellCost) + baseMinSize = self.safe_number(market, 'base_min_size') + result.append(self.safe_market_structure({ + 'id': id, + 'numericId': numericId, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': self.safe_string_lower_2(market, 'status', 'trade_status') == 'trading', + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'maker': fees['maker'], + 'taker': fees['taker'], + 'precision': { + 'amount': baseMinSize, + 'price': self.parse_number(self.parse_precision(self.safe_string(market, 'price_max_precision'))), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': baseMinSize, + 'max': self.safe_number(market, 'base_max_size'), + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': self.parse_number(minCost), + 'max': None, + }, + }, + 'created': None, + 'info': market, + })) + return result + + def fetch_contract_markets(self, params={}) -> List[MarketInterface]: + response = self.publicGetContractPublicDetails(params) + # + # { + # "code": 1000, + # "message": "Ok", + # "data": { + # "symbols": [ + # { + # "symbol": "BTCUSDT", + # "product_type": 1, + # "open_timestamp": 1645977600000, + # "expire_timestamp": 0, + # "settle_timestamp": 0, + # "base_currency": "BTC", + # "quote_currency": "USDT", + # "last_price": "63547.4", + # "volume_24h": "110938430", + # "turnover_24h": "7004836342.6944", + # "index_price": "63587.85404255", + # "index_name": "BTCUSDT", + # "contract_size": "0.001", + # "min_leverage": "1", + # "max_leverage": "100", + # "price_precision": "0.1", + # "vol_precision": "1", + # "max_volume": "1000000", + # "min_volume": "1", + # "funding_rate": "0.0000801", + # "expected_funding_rate": "-0.0000035", + # "open_interest": "278214", + # "open_interest_value": "17555316.9355496", + # "high_24h": "64109.4", + # "low_24h": "61857.6", + # "change_24h": "0.0239264900886327", + # "funding_time": 1726819200000 + # }, + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + symbols = self.safe_list(data, 'symbols', []) + result = [] + fees = self.fees['trading'] + for i in range(0, len(symbols)): + market = symbols[i] + id = self.safe_string(market, 'symbol') + baseId = self.safe_string(market, 'base_currency') + quoteId = self.safe_string(market, 'quote_currency') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + settleId = 'USDT' # self is bitmart's ID for usdt + settle = self.safe_currency_code(settleId) + symbol = base + '/' + quote + ':' + settle + productType = self.safe_integer(market, 'product_type') + isSwap = (productType == 1) + isFutures = (productType == 2) + expiry = self.safe_integer(market, 'expire_timestamp') + if not isFutures and (expiry == 0): + expiry = None + result.append(self.safe_market_structure({ + 'id': id, + 'numericId': None, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': 'swap' if isSwap else 'future', + 'spot': False, + 'margin': False, + 'swap': isSwap, + 'future': isFutures, + 'option': False, + 'active': self.safe_string_lower(market, 'status') == 'trading', + 'contract': True, + 'linear': True, + 'inverse': False, + 'contractSize': self.safe_number(market, 'contract_size'), + 'expiry': expiry, + 'expiryDatetime': self.iso8601(expiry), + 'strike': None, + 'optionType': None, + 'maker': fees['maker'], + 'taker': fees['taker'], + 'precision': { + 'amount': self.safe_number(market, 'vol_precision'), + 'price': self.safe_number(market, 'price_precision'), + }, + 'limits': { + 'leverage': { + 'min': self.safe_number(market, 'min_leverage'), + 'max': self.safe_number(market, 'max_leverage'), + }, + 'amount': { + 'min': self.safe_number(market, 'min_volume'), + 'max': self.safe_number(market, 'max_volume'), + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': self.safe_integer(market, 'open_timestamp'), + 'info': market, + })) + return result + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for bitmart + + https://developer-pro.bitmart.com/en/spot/#get-trading-pair-details-v1 + https://developer-pro.bitmart.com/en/futuresv2/#get-contract-details + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + if self.options['adjustForTimeDifference']: + self.load_time_difference() + spot = self.fetch_spot_markets(params) + contract = self.fetch_contract_markets(params) + return self.array_concat(spot, contract) + + def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://developer-pro.bitmart.com/en/spot/#get-currency-list-v1 + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = self.publicGetAccountV1Currencies(params) + # + # { + # "message": "OK", + # "code": 1000, + # "trace": "619294ecef584282b26a3be322b1e01f.66.17403093228242229", + # "data": { + # "currencies": [ + # { + # "currency": "BTC", + # "name": "Bitcoin", + # "recharge_minsize": '0.00000001', + # "contract_address": null, + # "network": "BTC", + # "withdraw_enabled": True, + # "deposit_enabled": True, + # "withdraw_minsize": "0.0003", + # "withdraw_minfee": "9.61", + # "withdraw_fee_estimate": "9.61", + # "withdraw_fee": "0.0001" + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + currencies = self.safe_list(data, 'currencies', []) + result = {} + for i in range(0, len(currencies)): + currency = currencies[i] + fullId = self.safe_string(currency, 'currency') + currencyId = fullId + networkId = self.safe_string(currency, 'network') + isNtf = (fullId.find('NFT') >= 0) + if not isNtf: + parts = fullId.split('-') + currencyId = self.safe_string(parts, 0) + second = self.safe_string(parts, 1) + if second is not None: + networkId = second.upper() + currencyCode = self.safe_currency_code(currencyId) + entry = self.safe_dict(result, currencyCode) + if entry is None: + entry = { + 'info': currency, + 'id': currencyId, + 'code': currencyCode, + 'precision': None, + 'name': self.safe_string(currency, 'name'), + 'deposit': None, + 'withdraw': None, + 'active': None, + 'networks': {}, + 'type': 'other' if isNtf else 'crypto', + } + networkCode = self.network_id_to_code(networkId) + withdraw = self.safe_bool(currency, 'withdraw_enabled') + deposit = self.safe_bool(currency, 'deposit_enabled') + entry['networks'][networkCode] = { + 'info': currency, + 'id': networkId, + 'code': networkCode, + 'withdraw': withdraw, + 'deposit': deposit, + 'active': withdraw and deposit, + 'fee': self.safe_number(currency, 'withdraw_fee'), + 'limits': { + 'withdraw': { + 'min': self.safe_number(currency, 'withdraw_minsize'), + 'max': None, + }, + 'deposit': { + 'min': None, + 'max': None, + }, + }, + } + result[currencyCode] = entry + keys = list(result.keys()) + for i in range(0, len(keys)): + key = keys[i] + currency = result[key] + result[key] = self.safe_currency_structure(currency) + return result + + def get_currency_id_from_code_and_network(self, currencyCode: Str, networkCode: Str) -> Str: + if networkCode is None: + networkCode = self.default_network_code(currencyCode) # use default network code if not provided + currency = self.currency(currencyCode) + id = currency['id'] + idFromNetwork: Str = None + networks = self.safe_dict(currency, 'networks', {}) + networkInfo: dict = {} + if networkCode is None: + # network code is not provided and there is no default network code + network = self.safe_dict(networks, currencyCode) # trying to find network that has the same code + if network is None: + # use the first network in the networks list if there is no network code with the same code + keys = list(networks.keys()) + length = len(keys) + if length > 0: + network = self.safe_value(networks, keys[0]) + networkInfo = self.safe_dict(network, 'info', {}) + idFromNetwork = self.safe_string(networkInfo, 'currency') # use currency name from network + else: + providedOrDefaultNetwork = self.safe_dict(networks, networkCode) + if providedOrDefaultNetwork is not None: + networkInfo = self.safe_dict(providedOrDefaultNetwork, 'info', {}) + idFromNetwork = self.safe_string(networkInfo, 'currency') # use currency name from network + else: + id += '-' + self.network_code_to_id(networkCode, currencyCode) # use concatenated currency id and network code if network is not found + return idFromNetwork if (idFromNetwork is not None) else id + + def fetch_transaction_fee(self, code: str, params={}): + """ + @deprecated + please use fetchDepositWithdrawFee instead + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.network]: the network code of the currency + :returns dict: a `fee structure ` + """ + self.load_markets() + currency = self.currency(code) + network: Str = None + network, params = self.handle_network_code_and_params(params) + request: dict = { + 'currency': self.get_currency_id_from_code_and_network(currency['code'], network), + } + response = self.privateGetAccountV1WithdrawCharge(self.extend(request, params)) + # + # { + # "message": "OK", + # "code": "1000", + # "trace": "3ecc0adf-91bd-4de7-aca1-886c1122f54f", + # "data": { + # "today_available_withdraw_BTC": "100.0000", + # "min_withdraw": "0.005", + # "withdraw_precision": "8", + # "withdraw_fee": "0.000500000000000000000000000000" + # } + # } + # + data = response['data'] + withdrawFees: dict = {} + withdrawFees[code] = self.safe_number(data, 'withdraw_fee') + return { + 'info': response, + 'withdraw': withdrawFees, + 'deposit': {}, + } + + def parse_deposit_withdraw_fee(self, fee, currency: Currency = None): + # + # { + # "today_available_withdraw_BTC": "100.0000", + # "min_withdraw": "0.005", + # "withdraw_precision": "8", + # "withdraw_fee": "0.000500000000000000000000000000" + # } + # + return { + 'info': fee, + 'withdraw': { + 'fee': self.safe_number(fee, 'withdraw_fee'), + 'percentage': None, + }, + 'deposit': { + 'fee': None, + 'percentage': None, + }, + 'networks': {}, + } + + def fetch_deposit_withdraw_fee(self, code: str, params={}): + """ + fetch the fee for deposits and withdrawals + + https://developer-pro.bitmart.com/en/spot/#withdraw-quota-keyed + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.network]: the network code of the currency + :returns dict: a `fee structure ` + """ + self.load_markets() + network: Str = None + network, params = self.handle_network_code_and_params(params) + request: dict = { + 'currency': self.get_currency_id_from_code_and_network(code, network), + } + response = self.privateGetAccountV1WithdrawCharge(self.extend(request, params)) + # + # { + # "message": "OK", + # "code": "1000", + # "trace": "3ecc0adf-91bd-4de7-aca1-886c1122f54f", + # "data": { + # "today_available_withdraw_BTC": "100.0000", + # "min_withdraw": "0.005", + # "withdraw_precision": "8", + # "withdraw_fee": "0.000500000000000000000000000000" + # } + # } + # + data = response['data'] + return self.parse_deposit_withdraw_fee(data) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # spot(REST) fetchTickers + # + # { + # 'result': [ + # "AFIN_USDT", # symbol + # "0.001047", # last + # "11110", # v_24h + # "11.632170", # qv_24h + # "0.001048", # open_24h + # "0.001048", # high_24h + # "0.001047", # low_24h + # "-0.00095", # price_change_24h + # "0.001029", # bid_px + # "5555", # bid_sz + # "0.001041", # ask_px + # "5297", # ask_sz + # "1717122550482" # timestamp + # ] + # } + # + # spot(REST) fetchTicker + # + # { + # "symbol": "BTC_USDT", + # "last": "68500.00", + # "v_24h": "10491.65490", + # "qv_24h": "717178990.42", + # "open_24h": "68149.75", + # "high_24h": "69499.99", + # "low_24h": "67132.40", + # "fluctuation": "0.00514", + # "bid_px": "68500", + # "bid_sz": "0.00162", + # "ask_px": "68500.01", + # "ask_sz": "0.01722", + # "ts": "1717131391671" + # } + # + # spot(WS) + # + # { + # "symbol":"BTC_USDT", + # "last_price":"146.24", + # "open_24h":"147.17", + # "high_24h":"147.48", + # "low_24h":"143.88", + # "base_volume_24h":"117387.58", # NOT base, but quote currencynot !! + # "s_t": 1610936002 + # } + # + # swap + # + # { + # "symbol": "BTCUSDT", + # "product_type": 1, + # "open_timestamp": 1645977600000, + # "expire_timestamp": 0, + # "settle_timestamp": 0, + # "base_currency": "BTC", + # "quote_currency": "USDT", + # "last_price": "63547.4", + # "volume_24h": "110938430", + # "turnover_24h": "7004836342.6944", + # "index_price": "63587.85404255", + # "index_name": "BTCUSDT", + # "contract_size": "0.001", + # "min_leverage": "1", + # "max_leverage": "100", + # "price_precision": "0.1", + # "vol_precision": "1", + # "max_volume": "1000000", + # "min_volume": "1", + # "funding_rate": "0.0000801", + # "expected_funding_rate": "-0.0000035", + # "open_interest": "278214", + # "open_interest_value": "17555316.9355496", + # "high_24h": "64109.4", + # "low_24h": "61857.6", + # "change_24h": "0.0239264900886327", + # "funding_time": 1726819200000 + # } + # + result = self.safe_list(ticker, 'result', []) + average = self.safe_string_2(ticker, 'avg_price', 'index_price') + marketId = self.safe_string_2(ticker, 'symbol', 'contract_symbol') + timestamp = self.safe_integer_2(ticker, 'timestamp', 'ts') + last = self.safe_string_2(ticker, 'last_price', 'last') + percentage = self.safe_string_2(ticker, 'price_change_percent_24h', 'change_24h') + change = self.safe_string(ticker, 'fluctuation') + high = self.safe_string_2(ticker, 'high_24h', 'high_price') + low = self.safe_string_2(ticker, 'low_24h', 'low_price') + bid = self.safe_string_2(ticker, 'best_bid', 'bid_px') + bidVolume = self.safe_string_2(ticker, 'best_bid_size', 'bid_sz') + ask = self.safe_string_2(ticker, 'best_ask', 'ask_px') + askVolume = self.safe_string_2(ticker, 'best_ask_size', 'ask_sz') + open = self.safe_string(ticker, 'open_24h') + baseVolume = self.safe_string_n(ticker, ['base_volume_24h', 'v_24h', 'volume_24h']) + quoteVolume = self.safe_string_lower_n(ticker, ['quote_volume_24h', 'qv_24h', 'turnover_24h']) + listMarketId = self.safe_string(result, 0) + if listMarketId is not None: + marketId = listMarketId + timestamp = self.safe_integer(result, 12) + high = self.safe_string(result, 5) + low = self.safe_string(result, 6) + bid = self.safe_string(result, 8) + bidVolume = self.safe_string(result, 9) + ask = self.safe_string(result, 10) + askVolume = self.safe_string(result, 11) + open = self.safe_string(result, 4) + last = self.safe_string(result, 1) + change = self.safe_string(result, 7) + baseVolume = self.safe_string(result, 2) + quoteVolume = self.safe_string_lower(result, 3) + market = self.safe_market(marketId, market) + symbol = market['symbol'] + if timestamp is None: + # ticker from WS has a different field(in seconds) + timestamp = self.safe_integer_product(ticker, 's_t', 1000) + if percentage is None: + percentage = Precise.string_mul(change, '100') + if quoteVolume is None: + if baseVolume is None: + # self is swap + quoteVolume = self.safe_string(ticker, 'volume_24h', quoteVolume) + else: + # self is a ticker from websockets + # contrary to name and documentation, base_volume_24h is actually the quote volume + quoteVolume = baseVolume + baseVolume = None + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': high, + 'low': low, + 'bid': bid, + 'bidVolume': bidVolume, + 'ask': ask, + 'askVolume': askVolume, + 'vwap': None, + 'open': open, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': percentage, + 'average': average, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'indexPrice': self.safe_string(ticker, 'index_price'), + 'info': ticker, + }, market) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://developer-pro.bitmart.com/en/spot/#get-ticker-of-a-trading-pair-v3 + https://developer-pro.bitmart.com/en/futuresv2/#get-contract-details + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = {} + response = None + if market['swap']: + request['symbol'] = market['id'] + response = self.publicGetContractPublicDetails(self.extend(request, params)) + # + # { + # "code": 1000, + # "message": "Ok", + # "data": { + # "symbols": [ + # { + # "symbol": "BTCUSDT", + # "product_type": 1, + # "open_timestamp": 1645977600000, + # "expire_timestamp": 0, + # "settle_timestamp": 0, + # "base_currency": "BTC", + # "quote_currency": "USDT", + # "last_price": "63547.4", + # "volume_24h": "110938430", + # "turnover_24h": "7004836342.6944", + # "index_price": "63587.85404255", + # "index_name": "BTCUSDT", + # "contract_size": "0.001", + # "min_leverage": "1", + # "max_leverage": "100", + # "price_precision": "0.1", + # "vol_precision": "1", + # "max_volume": "1000000", + # "min_volume": "1", + # "funding_rate": "0.0000801", + # "expected_funding_rate": "-0.0000035", + # "open_interest": "278214", + # "open_interest_value": "17555316.9355496", + # "high_24h": "64109.4", + # "low_24h": "61857.6", + # "change_24h": "0.0239264900886327", + # "funding_time": 1726819200000 + # }, + # ] + # } + # } + # + elif market['spot']: + request['symbol'] = market['id'] + response = self.publicGetSpotQuotationV3Ticker(self.extend(request, params)) + # + # { + # "code": 1000, + # "trace": "f2194c2c202d2.99.1717535", + # "message": "success", + # "data": { + # "symbol": "BTC_USDT", + # "last": "68500.00", + # "v_24h": "10491.65490", + # "qv_24h": "717178990.42", + # "open_24h": "68149.75", + # "high_24h": "69499.99", + # "low_24h": "67132.40", + # "fluctuation": "0.00514", + # "bid_px": "68500", + # "bid_sz": "0.00162", + # "ask_px": "68500.01", + # "ask_sz": "0.01722", + # "ts": "1717131391671" + # } + # } + # + else: + raise NotSupported(self.id + ' fetchTicker() does not support ' + market['type'] + ' markets, only spot and swap markets are accepted') + # fails in naming for contract tickers 'contract_symbol' + tickers = [] + ticker: dict = {} + if market['spot']: + ticker = self.safe_dict(response, 'data', {}) + else: + data = self.safe_dict(response, 'data', {}) + tickers = self.safe_list(data, 'symbols', []) + ticker = self.safe_dict(tickers, 0, {}) + return self.parse_ticker(ticker, market) + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://developer-pro.bitmart.com/en/spot/#get-ticker-of-all-pairs-v3 + https://developer-pro.bitmart.com/en/futuresv2/#get-contract-details + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + symbols = self.market_symbols(symbols) + type = None + market = None + if symbols is not None: + symbol = self.safe_string(symbols, 0) + market = self.market(symbol) + type, params = self.handle_market_type_and_params('fetchTickers', market, params) + response = None + if type == 'spot': + response = self.publicGetSpotQuotationV3Tickers(params) + # + # { + # "code": 1000, + # "trace": "17c5e5d9ac49f9b71efca2bed55f1a.105.171225637482393", + # "message": "success", + # "data": [ + # [ + # "AFIN_USDT", + # "0.001047", + # "11110", + # "11.632170", + # "0.001048", + # "0.001048", + # "0.001047", + # "-0.00095", + # "0.001029", + # "5555", + # "0.001041", + # "5297", + # "1717122550482" + # ], + # ] + # } + # + elif type == 'swap': + response = self.publicGetContractPublicDetails(params) + # + # { + # "code": 1000, + # "message": "Ok", + # "data": { + # "symbols": [ + # { + # "symbol": "BTCUSDT", + # "product_type": 1, + # "open_timestamp": 1645977600000, + # "expire_timestamp": 0, + # "settle_timestamp": 0, + # "base_currency": "BTC", + # "quote_currency": "USDT", + # "last_price": "63547.4", + # "volume_24h": "110938430", + # "turnover_24h": "7004836342.6944", + # "index_price": "63587.85404255", + # "index_name": "BTCUSDT", + # "contract_size": "0.001", + # "min_leverage": "1", + # "max_leverage": "100", + # "price_precision": "0.1", + # "vol_precision": "1", + # "max_volume": "1000000", + # "min_volume": "1", + # "funding_rate": "0.0000801", + # "expected_funding_rate": "-0.0000035", + # "open_interest": "278214", + # "open_interest_value": "17555316.9355496", + # "high_24h": "64109.4", + # "low_24h": "61857.6", + # "change_24h": "0.0239264900886327", + # "funding_time": 1726819200000 + # }, + # ] + # } + # } + # + else: + raise NotSupported(self.id + ' fetchTickers() does not support ' + type + ' markets, only spot and swap markets are accepted') + tickers = [] + if type == 'spot': + tickers = self.safe_list(response, 'data', []) + else: + data = self.safe_dict(response, 'data', {}) + tickers = self.safe_list(data, 'symbols', []) + result: dict = {} + for i in range(0, len(tickers)): + ticker: dict = {} + if type == 'spot': + ticker = self.parse_ticker({'result': tickers[i]}) + else: + ticker = self.parse_ticker(tickers[i]) + symbol = ticker['symbol'] + result[symbol] = ticker + return self.filter_by_array_tickers(result, 'symbol', symbols) + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://developer-pro.bitmart.com/en/spot/#get-depth-v3 + https://developer-pro.bitmart.com/en/futuresv2/#get-market-depth + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = None + if market['spot']: + if limit is not None: + request['limit'] = limit # default 35, max 50 + response = self.publicGetSpotQuotationV3Books(self.extend(request, params)) + elif market['swap']: + response = self.publicGetContractPublicDepth(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchOrderBook() does not support ' + market['type'] + ' markets, only spot and swap markets are accepted') + # + # spot + # + # { + # "code": 1000, + # "message": "success", + # "data": { + # "ts": "1695264191808", + # "symbol": "BTC_USDT", + # "asks": [ + # ["26942.57","0.06492"], + # ["26942.73","0.05447"], + # ["26943.00","0.07154"] + # ], + # "bids": [ + # ["26942.45","0.00074"], + # ["26941.53","0.00371"], + # ["26940.94","0.08992"] + # ] + # }, + # "trace": "430a7f69581d4258a8e4b424dfb10782.73.16952341919017619" + # } + # + # swap + # + # { + # "code": 1000, + # "message": "Ok", + # "data": { + # "asks": [ + # ["26938.3","3499","3499"], + # ["26938.5","14702","18201"], + # ["26938.6","20457","38658"] + # ], + # "bids": [ + # ["26938.2","20","20"], + # ["26937.9","1913","1933"], + # ["26937.8","2588","4521"] + # ], + # "timestamp": 1695264383999, + # "symbol": "BTCUSDT" + # }, + # "trace": "4cad855074664097ac6ba5258c47305d.72.16952643834721135" + # } + # + data = self.safe_dict(response, 'data', {}) + timestamp = self.safe_integer_2(data, 'ts', 'timestamp') + return self.parse_order_book(data, market['symbol'], timestamp) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # public fetchTrades spot( amount = count * price ) + # + # [ + # "BTC_USDT", # symbol + # "1717212457302", # timestamp + # "67643.11", # price + # "0.00106", # size + # "sell" # side + # ] + # + # spot: fetchMyTrades + # + # { + # "tradeId":"182342999769370687", + # "orderId":"183270218784142990", + # "clientOrderId":"183270218784142990", + # "symbol":"ADA_USDT", + # "side":"buy", + # "orderMode":"spot", + # "type":"market", + # "price":"0.245948", + # "size":"20.71", + # "notional":"5.09358308", + # "fee":"0.00509358", + # "feeCoinName":"USDT", + # "tradeRole":"taker", + # "createTime":1695658457836, + # } + # + # swap: fetchMyTrades + # + # { + # "order_id": "230930336848609", + # "trade_id": "6212604014", + # "symbol": "BTCUSDT", + # "side": 3, + # "price": "26910.4", + # "vol": "1", + # "exec_type": "Taker", + # "profit": False, + # "create_time": 1695961596692, + # "realised_profit": "-0.0003", + # "paid_fees": "0.01614624" + # } + # + # ws swap + # + # { + # 'fee': '-0.000044502', + # 'feeCcy': 'USDT', + # 'fillPrice': '74.17', + # 'fillQty': '1', + # 'lastTradeID': 6802340762 + # } + # + timestamp = self.safe_integer_n(trade, ['createTime', 'create_time', 1]) + isPublic = self.safe_string(trade, 0) + isPublicTrade = (isPublic is not None) + amount = None + cost = None + type = None + side = None + if isPublicTrade: + amount = self.safe_string_2(trade, 'count', 3) + cost = self.safe_string(trade, 'amount') + side = self.safe_string_2(trade, 'type', 4) + else: + amount = self.safe_string_n(trade, ['size', 'vol', 'fillQty']) + cost = self.safe_string(trade, 'notional') + type = self.safe_string(trade, 'type') + side = self.parse_order_side(self.safe_string(trade, 'side')) + marketId = self.safe_string_2(trade, 'symbol', 0) + market = self.safe_market(marketId, market) + feeCostString = self.safe_string_2(trade, 'fee', 'paid_fees') + fee = None + if feeCostString is not None: + feeCurrencyId = self.safe_string(trade, 'feeCoinName') + feeCurrencyCode = self.safe_currency_code(feeCurrencyId) + if feeCurrencyCode is None: + feeCurrencyCode = market['base'] if (side == 'buy') else market['quote'] + fee = { + 'cost': feeCostString, + 'currency': feeCurrencyCode, + } + return self.safe_trade({ + 'info': trade, + 'id': self.safe_string_n(trade, ['tradeId', 'trade_id', 'lastTradeID']), + 'order': self.safe_string_2(trade, 'orderId', 'order_id'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'type': type, + 'side': side, + 'price': self.safe_string_n(trade, ['price', 'fillPrice', 2]), + 'amount': amount, + 'cost': cost, + 'takerOrMaker': self.safe_string_lower_2(trade, 'tradeRole', 'exec_type'), + 'fee': fee, + }, market) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get a list of the most recent trades for a particular symbol + + https://developer-pro.bitmart.com/en/spot/#get-recent-trades-v3 + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum number of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' fetchTrades() does not support ' + market['type'] + ' orders, only spot orders are accepted') + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['limit'] = limit + response = self.publicGetSpotQuotationV3Trades(self.extend(request, params)) + # + # { + # "code": 1000, + # "trace": "58031f9a5bd.111.17117", + # "message": "success", + # "data": [ + # [ + # "BTC_USDT", + # "1717212457302", + # "67643.11", + # "0.00106", + # "sell" + # ], + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_trades(data, market, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # spot + # [ + # "1699512060", # timestamp + # "36746.49", # open + # "36758.71", # high + # "36736.13", # low + # "36755.99", # close + # "2.83965", # base volume + # "104353.57" # quote volume + # ] + # + # swap + # { + # "low_price": "20090.3", + # "high_price": "20095.5", + # "open_price": "20092.6", + # "close_price": "20091.4", + # "volume": "8748", + # "timestamp": 1665002281 + # } + # + # ws + # [ + # 1631056350, # timestamp + # "46532.83", # open + # "46555.71", # high + # "46511.41", # low + # "46555.71", # close + # "0.25", # volume + # ] + # + # ws swap + # { + # "symbol":"BTCUSDT", + # "o":"146.24", + # "h":"146.24", + # "l":"146.24", + # "c":"146.24", + # "v":"146" + # } + # + if isinstance(ohlcv, list): + return [ + self.safe_timestamp(ohlcv, 0), + self.safe_number(ohlcv, 1), + self.safe_number(ohlcv, 2), + self.safe_number(ohlcv, 3), + self.safe_number(ohlcv, 4), + self.safe_number(ohlcv, 5), + ] + else: + return [ + self.safe_timestamp_2(ohlcv, 'timestamp', 'ts'), + self.safe_number_2(ohlcv, 'open_price', 'o'), + self.safe_number_2(ohlcv, 'high_price', 'h'), + self.safe_number_2(ohlcv, 'low_price', 'l'), + self.safe_number_2(ohlcv, 'close_price', 'c'), + self.safe_number_2(ohlcv, 'volume', 'v'), + ] + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://developer-pro.bitmart.com/en/spot/#get-history-k-line-v3 + https://developer-pro.bitmart.com/en/futuresv2/#get-k-line + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp of the latest candle in ms + :param boolean [params.paginate]: *spot only* default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate', False) + if paginate: + return self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params, 200) + market = self.market(symbol) + duration = self.parse_timeframe(timeframe) + parsedTimeframe = self.safe_integer(self.timeframes, timeframe) + request: dict = { + 'symbol': market['id'], + } + if parsedTimeframe is not None: + request['step'] = parsedTimeframe + else: + request['step'] = timeframe + if market['spot']: + request, params = self.handle_until_option('before', request, params, 0.001) + if limit is not None: + request['limit'] = limit + if since is not None: + request['after'] = self.parse_to_int((since / 1000)) - 1 + else: + maxLimit = 500 + if limit is None: + limit = maxLimit + limit = min(maxLimit, limit) + now = self.parse_to_int(self.milliseconds() / 1000) + if since is None: + start = now - limit * duration + request['start_time'] = start + request['end_time'] = now + else: + start = self.parse_to_int((since / 1000)) - 1 + end = self.sum(start, limit * duration) + request['start_time'] = start + request['end_time'] = min(end, now) + request, params = self.handle_until_option('end_time', request, params, 0.001) + response = None + if market['swap']: + price = self.safe_string(params, 'price') + if price == 'mark': + params = self.omit(params, 'price') + response = self.publicGetContractPublicMarkpriceKline(self.extend(request, params)) + else: + response = self.publicGetContractPublicKline(self.extend(request, params)) + else: + response = self.publicGetSpotQuotationV3Klines(self.extend(request, params)) + # + # spot + # + # { + # "code": 1000, + # "message": "success", + # "data": [ + # ["1699512060","36746.49","36758.71","36736.13","36755.99","2.83965","104353.57"], + # ["1699512120","36756.00","36758.70","36737.14","36737.63","1.96070","72047.10"], + # ["1699512180","36737.63","36740.45","36737.62","36740.44","0.63194","23217.62"] + # ], + # "trace": "6591fc7b508845359d5fa442e3b3a4fb.72.16995122398750695" + # } + # + # swap + # + # { + # "code": 1000, + # "message": "Ok", + # "data": [ + # { + # "low_price": "20090.3", + # "high_price": "20095.5", + # "open_price": "20092.6", + # "close_price": "20091.4", + # "volume": "8748", + # "timestamp": 1665002281 + # }, + # ... + # ], + # "trace": "96c989db-e0f5-46f5-bba6-60cfcbde699b" + # } + # + ohlcv = self.safe_list(response, 'data', []) + return self.parse_ohlcvs(ohlcv, market, timeframe, since, limit) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + + https://developer-pro.bitmart.com/en/spot/#account-trade-list-v4-signed + https://developer-pro.bitmart.com/en/futuresv2/#get-order-trade-keyed + + fetch all trades made by the user + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch trades for + :param boolean [params.marginMode]: *spot* whether to fetch trades for margin orders or spot orders, defaults to spot orders(only isolated margin orders are supported) + :param str [params.stpMode]: self-trade prevention only for spot, defaults to none, ['none', 'cancel_maker', 'cancel_taker', 'cancel_both'] + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = None + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + type = None + response = None + type, params = self.handle_market_type_and_params('fetchMyTrades', market, params) + until = self.safe_integer_n(params, ['until', 'endTime', 'end_time']) + params = self.omit(params, ['until']) + if type == 'spot': + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchMyTrades', params) + if marginMode == 'isolated': + request['orderMode'] = 'iso_margin' + options = self.safe_dict(self.options, 'fetchMyTrades', {}) + maxLimit = 200 + defaultLimit = self.safe_integer(options, 'limit', maxLimit) + if limit is None: + limit = defaultLimit + request['limit'] = min(limit, maxLimit) + if since is not None: + request['startTime'] = since + if until is not None: + request['endTime'] = until + response = self.privatePostSpotV4QueryTrades(self.extend(request, params)) + elif type == 'swap': + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMyTrades() requires a symbol argument') + if since is not None: + request['start_time'] = since + if until is not None: + request['end_time'] = until + response = self.privateGetContractPrivateTrades(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchMyTrades() does not support ' + type + ' orders, only spot and swap orders are accepted') + # + # spot + # + # { + # "code":1000, + # "message":"success", + # "data":[ + # { + # "tradeId":"182342999769370687", + # "orderId":"183270218784142990", + # "clientOrderId":"183270218784142990", + # "symbol":"ADA_USDT", + # "side":"buy", + # "orderMode":"spot", + # "type":"market", + # "price":"0.245948", + # "size":"20.71", + # "notional":"5.09358308", + # "fee":"0.00509358", + # "feeCoinName":"USDT", + # "tradeRole":"taker", + # "createTime":1695658457836, + # "updateTime":1695658457836 + # } + # ], + # "trace":"fbaee9e0e2f5442fba5b3262fc86b0ac.65.16956593456523085" + # } + # + # swap + # + # { + # "code": 1000, + # "message": "Ok", + # "data": [ + # { + # "order_id": "230930336848609", + # "trade_id": "6212604014", + # "symbol": "BTCUSDT", + # "side": 3, + # "price": "26910.4", + # "vol": "1", + # "exec_type": "Taker", + # "profit": False, + # "create_time": 1695961596692, + # "realised_profit": "-0.0003", + # "paid_fees": "0.01614624" + # }, + # ], + # "trace": "4cad855074634097ac6ba5257c47305d.62.16959616054873723" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_trades(data, market, since, limit) + + def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + + https://developer-pro.bitmart.com/en/spot/#order-trade-list-v4-signed + + fetch all the trades made from a single order + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.stpMode]: self-trade prevention only for spot, defaults to none, ['none', 'cancel_maker', 'cancel_taker', 'cancel_both'] + :returns dict[]: a list of `trade structures ` + """ + self.load_markets() + request: dict = { + 'orderId': id, + } + response = self.privatePostSpotV4QueryOrderTrades(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + return self.parse_trades(data, None, since, limit) + + def custom_parse_balance(self, response, marketType) -> Balances: + data = self.safe_dict(response, 'data', {}) + wallet = None + if marketType == 'swap': + wallet = self.safe_list(response, 'data', []) + elif marketType == 'margin': + wallet = self.safe_list(data, 'symbols', []) + else: + wallet = self.safe_list(data, 'wallet', []) + result = {'info': response} + if marketType == 'margin': + for i in range(0, len(wallet)): + entry = wallet[i] + marketId = self.safe_string(entry, 'symbol') + symbol = self.safe_symbol(marketId, None, '_') + base = self.safe_dict(entry, 'base', {}) + quote = self.safe_dict(entry, 'quote', {}) + baseCode = self.safe_currency_code(self.safe_string(base, 'currency')) + quoteCode = self.safe_currency_code(self.safe_string(quote, 'currency')) + subResult: dict = {} + subResult[baseCode] = self.parse_balance_helper(base) + subResult[quoteCode] = self.parse_balance_helper(quote) + result[symbol] = self.safe_balance(subResult) + return result + else: + for i in range(0, len(wallet)): + balance = wallet[i] + currencyId = self.safe_string_2(balance, 'id', 'currency') + currencyId = self.safe_string(balance, 'coin_code', currencyId) + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string_2(balance, 'available', 'available_balance') + account['used'] = self.safe_string_n(balance, ['unAvailable', 'frozen', 'frozen_balance']) + result[code] = account + return self.safe_balance(result) + + def parse_balance_helper(self, entry): + account = self.account() + account['used'] = self.safe_string(entry, 'frozen') + account['free'] = self.safe_string(entry, 'available') + account['total'] = self.safe_string(entry, 'total_asset') + debt = self.safe_string(entry, 'borrow_unpaid') + interest = self.safe_string(entry, 'interest_unpaid') + account['debt'] = Precise.string_add(debt, interest) + return account + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://developer-pro.bitmart.com/en/spot/#get-spot-wallet-balance-keyed + https://developer-pro.bitmart.com/en/futuresv2/#get-contract-assets-keyed + https://developer-pro.bitmart.com/en/spot/#get-account-balance-keyed + https://developer-pro.bitmart.com/en/spot/#get-margin-account-details-isolated-keyed + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + marketType = None + marketType, params = self.handle_market_type_and_params('fetchBalance', None, params) + marginMode = self.safe_string(params, 'marginMode') + isMargin = self.safe_bool(params, 'margin', False) + params = self.omit(params, ['margin', 'marginMode']) + if marginMode is not None or isMargin: + marketType = 'margin' + response = None + if marketType == 'spot': + response = self.privateGetSpotV1Wallet(params) + elif marketType == 'swap': + response = self.privateGetContractPrivateAssetsDetail(params) + elif marketType == 'account': + response = self.privateGetAccountV1Wallet(params) + elif marketType == 'margin': + response = self.privateGetSpotV1MarginIsolatedAccount(params) + else: + raise NotSupported(self.id + ' fetchBalance() does not support ' + marketType + ' markets, only spot, swap and account and margin markets are accepted') + # + # spot + # + # { + # "message":"OK", + # "code":1000, + # "trace":"39069916-72f9-44c7-acde-2ad5afd21cad", + # "data":{ + # "wallet":[ + # {"id":"BTC","name":"Bitcoin","available":"0.00000062","frozen":"0.00000000"}, + # {"id":"ETH","name":"Ethereum","available":"0.00002277","frozen":"0.00000000"}, + # {"id":"BMX","name":"BitMart Token","available":"0.00000000","frozen":"0.00000000"} + # ] + # } + # } + # + # account + # + # { + # "message":"OK", + # "code":1000, + # "trace":"5c3b7fc7-93b2-49ef-bb59-7fdc56915b59", + # "data":{ + # "wallet":[ + # {"currency":"BTC","name":"Bitcoin","available":"0.00000062","frozen":"0.00000000","available_usd_valuation":null}, + # {"currency":"ETH","name":"Ethereum","available":"0.00002277","frozen":"0.00000000","available_usd_valuation":null} + # ] + # } + # } + # + # swap + # + # { + # "code": 1000, + # "message": "Ok", + # "data": [ + # { + # "currency": "USDT", + # "available_balance": "0", + # "frozen_balance": "0", + # "unrealized": "0", + # "equity": "0", + # "position_deposit": "0" + # }, + # ... + # ], + # "trace": "f9da3a39-cf45-42e7-914d-294f565dfc33" + # } + # + # margin + # + # { + # "message": "OK", + # "code": 1000, + # "trace": "61dd6ab265c04064b72d8bc9b205f741.71.16701055600915302", + # "data": { + # "symbols": [ + # { + # "symbol": "BTC_USDT", + # "risk_rate": "999.00", + # "risk_level": "1", + # "buy_enabled": False, + # "sell_enabled": False, + # "liquidate_price": null, + # "liquidate_rate": "1.15", + # "base": { + # "currency": "BTC", + # "borrow_enabled": True, + # "borrowed": "0.00000000", + # "available": "0.00000000", + # "frozen": "0.00000000", + # "net_asset": "0.00000000", + # "net_assetBTC": "0.00000000", + # "total_asset": "0.00000000", + # "borrow_unpaid": "0.00000000", + # "interest_unpaid": "0.00000000" + # }, + # "quote": { + # "currency": "USDT", + # "borrow_enabled": True, + # "borrowed": "0.00000000", + # "available": "20.00000000", + # "frozen": "0.00000000", + # "net_asset": "20.00000000", + # "net_assetBTC": "0.00118008", + # "total_asset": "20.00000000", + # "borrow_unpaid": "0.00000000", + # "interest_unpaid": "0.00000000" + # } + # } + # ] + # } + # } + # + return self.custom_parse_balance(response, marketType) + + def parse_trading_fee(self, fee: dict, market: Market = None) -> TradingFeeInterface: + # + # { + # "symbol": "ETH_USDT", + # "taker_fee_rate": "0.0025", + # "maker_fee_rate": "0.0025" + # } + # + marketId = self.safe_string(fee, 'symbol') + symbol = self.safe_symbol(marketId) + return { + 'info': fee, + 'symbol': symbol, + 'maker': self.safe_number(fee, 'maker_fee_rate'), + 'taker': self.safe_number(fee, 'taker_fee_rate'), + 'percentage': None, + 'tierBased': None, + } + + def fetch_trading_fee(self, symbol: str, params={}) -> TradingFeeInterface: + """ + fetch the trading fees for a market + + https://developer-pro.bitmart.com/en/spot/#get-actual-trade-fee-rate-keyed + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `fee structure ` + """ + self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' fetchTradingFee() does not support ' + market['type'] + ' orders, only spot orders are accepted') + request: dict = { + 'symbol': market['id'], + } + response = self.privateGetSpotV1TradeFee(self.extend(request, params)) + # + # { + # "message": "OK", + # "code": "1000", + # "trace": "5a6f1e40-37fe-4849-a494-03279fadcc62", + # "data": { + # "symbol": "ETH_USDT", + # "taker_fee_rate": "0.0025", + # "maker_fee_rate": "0.0025" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_trading_fee(data) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder, editOrder + # + # { + # "order_id": 2707217580 + # } + # + # swap + # "data": { + # "order_id": 231116359426639, + # "price": "market price" + # }, + # + # cancelOrder + # + # "2707217580" # order id + # + # spot fetchOrder, fetchOrdersByStatus, fetchOpenOrders, fetchClosedOrders + # + # { + # "order_id":1736871726781, + # "symbol":"BTC_USDT", + # "create_time":1591096004000, + # "side":"sell", + # "type":"market", # limit, market, limit_maker, ioc + # "price":"0.00", + # "price_avg":"0.00", + # "size":"0.02000", + # "notional":"0.00000000", + # "filled_notional":"0.00000000", + # "filled_size":"0.00000", + # "status":"8" + # } + # + # spot v4 + # { + # "orderId" : "118100034543076010", + # "clientOrderId" : "118100034543076010", + # "symbol" : "BTC_USDT", + # "side" : "buy", + # "orderMode" : "spot", + # "type" : "limit", + # "state" : "filled", + # "price" : "48800.00", + # "priceAvg" : "39999.00", + # "size" : "0.10000", + # "filledSize" : "0.10000", + # "notional" : "4880.00000000", + # "filledNotional" : "3999.90000000", + # "createTime" : 1681701557927, + # "updateTime" : 1681701559408 + # } + # + # swap: fetchOrder, fetchOpenOrders, fetchClosedOrders + # + # { + # "order_id": "230935812485489", + # "client_order_id": "", + # "price": "24000", + # "size": "1", + # "symbol": "BTCUSDT", + # "state": 2, + # "side": 1, + # "type": "limit", + # "leverage": "10", + # "open_type": "isolated", + # "deal_avg_price": "0", + # "deal_size": "0", + # "create_time": 1695702258629, + # "update_time": 1695702258642, + # "activation_price_type": 0, + # "activation_price": "", + # "callback_rate": "" + # } + # + id = None + if isinstance(order, str): + id = order + order = {} + id = self.safe_string_2(order, 'order_id', 'orderId', id) + timestamp = self.safe_integer_2(order, 'create_time', 'createTime') + marketId = self.safe_string(order, 'symbol') + symbol = self.safe_symbol(marketId, market) + market = self.safe_market(symbol, market) + orderType = self.safe_string(market, 'type', 'spot') + type = self.safe_string(order, 'type') + timeInForce = None + postOnly = None + if type == 'limit_maker': + type = 'limit' + postOnly = True + timeInForce = 'PO' + if type == 'ioc': + type = 'limit' + timeInForce = 'IOC' + priceString = self.safe_string(order, 'price') + if priceString == 'market price': + priceString = None + trailingActivationPrice = self.safe_number(order, 'activation_price') + return self.safe_order({ + 'id': id, + 'clientOrderId': self.safe_string_2(order, 'client_order_id', 'clientOrderId'), + 'info': order, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': self.safe_integer(order, 'update_time'), + 'symbol': symbol, + 'type': type, + 'timeInForce': timeInForce, + 'postOnly': postOnly, + 'side': self.parse_order_side(self.safe_string(order, 'side')), + 'price': self.omit_zero(priceString), + 'triggerPrice': trailingActivationPrice, + 'amount': self.omit_zero(self.safe_string(order, 'size')), + 'cost': self.safe_string_2(order, 'filled_notional', 'filledNotional'), + 'average': self.safe_string_n(order, ['price_avg', 'priceAvg', 'deal_avg_price']), + 'filled': self.safe_string_n(order, ['filled_size', 'filledSize', 'deal_size']), + 'remaining': None, + 'status': self.parse_order_status_by_type(orderType, self.safe_string_2(order, 'status', 'state')), + 'fee': None, + 'trades': None, + }, market) + + def parse_order_side(self, side): + sides: dict = { + '1': 'buy', + '2': 'buy', + '3': 'sell', + '4': 'sell', + } + return self.safe_string(sides, side, side) + + def parse_order_status_by_type(self, type, status): + statusesByType: dict = { + 'spot': { + '1': 'rejected', # Order failure + '2': 'open', # Placing order + '3': 'rejected', # Order failure, Freeze failure + '4': 'open', # Order success, Pending for fulfilment + '5': 'open', # Partially filled + '6': 'closed', # Fully filled + '7': 'canceled', # Canceling + '8': 'canceled', # Canceled + 'new': 'open', + 'partially_filled': 'open', + 'filled': 'closed', + 'partially_canceled': 'canceled', + }, + 'swap': { + '1': 'open', # Submitting + '2': 'open', # Commissioned + '4': 'closed', # Completed + }, + } + statuses = self.safe_dict(statusesByType, type, {}) + return self.safe_string(statuses, status, status) + + def create_market_buy_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market buy order by providing the symbol and cost + + https://developer-pro.bitmart.com/en/spot/#new-order-v2-signed + + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' createMarketBuyOrderWithCost() supports spot orders only') + params['createMarketBuyOrderRequiresPrice'] = False + return self.create_order(symbol, 'market', 'buy', cost, None, params) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://developer-pro.bitmart.com/en/spot/#new-order-v2-signed + https://developer-pro.bitmart.com/en/spot/#new-margin-order-v1-signed + https://developer-pro.bitmart.com/en/futuresv2/#submit-order-signed + https://developer-pro.bitmart.com/en/futuresv2/#submit-plan-order-signed + https://developer-pro.bitmart.com/en/futuresv2/#submit-tp-sl-order-signed + https://developer-pro.bitmart.com/en/futuresv2/#submit-trail-order-signed + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market', 'limit' or 'trailing' for swap markets only + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' + :param str [params.leverage]: *swap only* leverage level + :param str [params.clientOrderId]: client order id of the order + :param boolean [params.reduceOnly]: *swap only* reduce only + :param boolean [params.postOnly]: make sure the order is posted to the order book and not matched immediately + :param str [params.triggerPrice]: *swap only* the price to trigger a stop order + :param int [params.price_type]: *swap only* 1: last price, 2: fair price, default is 1 + :param int [params.price_way]: *swap only* 1: price way long, 2: price way short + :param int [params.activation_price_type]: *swap trailing order only* 1: last price, 2: fair price, default is 1 + :param str [params.trailingPercent]: *swap only* the percent to trail away from the current market price, min 0.1 max 5 + :param str [params.trailingTriggerPrice]: *swap only* the price to trigger a trailing order, default uses the price argument + :param str [params.stopLossPrice]: *swap only* the price to trigger a stop-loss order + :param str [params.takeProfitPrice]: *swap only* the price to trigger a take-profit order + :param int [params.plan_category]: *swap tp/sl only* 1: tp/sl, 2: position tp/sl, default is 1 + :param str [params.stpMode]: self-trade prevention only for spot, defaults to none, ['none', 'cancel_maker', 'cancel_taker', 'cancel_both'] + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + result = self.handle_margin_mode_and_params('createOrder', params) + marginMode = self.safe_string(result, 0) + triggerPrice = self.safe_string_n(params, ['triggerPrice', 'stopPrice', 'trigger_price']) + stopLossPrice = self.safe_string(params, 'stopLossPrice') + takeProfitPrice = self.safe_string(params, 'takeProfitPrice') + isStopLoss = stopLossPrice is not None + isTakeProfit = takeProfitPrice is not None + isTriggerOrder = triggerPrice is not None + response = None + if market['spot']: + spotRequest = self.create_spot_order_request(symbol, type, side, amount, price, params) + if marginMode == 'isolated': + response = self.privatePostSpotV1MarginSubmitOrder(spotRequest) + else: + response = self.privatePostSpotV2SubmitOrder(spotRequest) + else: + swapRequest = self.create_swap_order_request(symbol, type, side, amount, price, params) + activationPrice = self.safe_string(swapRequest, 'activation_price') + if activationPrice is not None: + # if type is trailing + response = self.privatePostContractPrivateSubmitTrailOrder(swapRequest) + elif isTriggerOrder: + response = self.privatePostContractPrivateSubmitPlanOrder(swapRequest) + elif isStopLoss or isTakeProfit: + response = self.privatePostContractPrivateSubmitTpSlOrder(swapRequest) + else: + response = self.privatePostContractPrivateSubmitOrder(swapRequest) + # + # spot and margin + # + # { + # "code": 1000, + # "trace":"886fb6ae-456b-4654-b4e0-d681ac05cea1", + # "message": "OK", + # "data": { + # "order_id": 2707217580 + # } + # } + # + # swap + # {"code":1000,"message":"Ok","data":{"order_id":231116359426639,"price":"market price"},"trace":"7f9c94e10f9d4513bc08a7bfc2a5559a.62.16996369620521911"} + # + data = self.safe_dict(response, 'data', {}) + order = self.parse_order(data, market) + order['type'] = type + order['side'] = side + order['amount'] = amount + order['price'] = price + return order + + def create_orders(self, orders: List[OrderRequest], params={}): + """ + create a list of trade orders + + https://developer-pro.bitmart.com/en/spot/#new-batch-order-v4-signed + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.stpMode]: self-trade prevention only for spot, defaults to none, ['none', 'cancel_maker', 'cancel_taker', 'cancel_both'] + :returns dict: an `order structure ` + """ + self.load_markets() + ordersRequests = [] + symbol = None + market = None + for i in range(0, len(orders)): + rawOrder = orders[i] + marketId = self.safe_string(rawOrder, 'symbol') + market = self.market(marketId) + if not market['spot']: + raise NotSupported(self.id + ' createOrders() supports spot orders only') + if symbol is None: + symbol = marketId + else: + if symbol != marketId: + raise BadRequest(self.id + ' createOrders() requires all orders to have the same symbol') + type = self.safe_string(rawOrder, 'type') + side = self.safe_string(rawOrder, 'side') + amount = self.safe_value(rawOrder, 'amount') + price = self.safe_value(rawOrder, 'price') + orderParams = self.safe_dict(rawOrder, 'params', {}) + orderRequest = self.create_spot_order_request(marketId, type, side, amount, price, orderParams) + orderRequest = self.omit(orderRequest, ['symbol']) # not needed because it goes in the outter object + ordersRequests.append(orderRequest) + request: dict = { + 'symbol': market['id'], + 'orderParams': ordersRequests, + } + response = self.privatePostSpotV4BatchOrders(request) + # + # { + # "message": "OK", + # "code": 1000, + # "trace": "5fc697fb817a4b5396284786a9b2609a.263.17022620476480263", + # "data": { + # "code": 0, + # "msg": "success", + # "data": { + # "orderIds": [ + # "212751308355553320" + # ] + # } + # } + # } + # + data = self.safe_dict(response, 'data', {}) + innderData = self.safe_dict(data, 'data', {}) + orderIds = self.safe_list(innderData, 'orderIds', []) + parsedOrders = [] + for i in range(0, len(orderIds)): + orderId = orderIds[i] + order = self.safe_order({'id': orderId}, market) + parsedOrders.append(order) + return parsedOrders + + def create_swap_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + @ignore + create a trade order + https://developer-pro.bitmart.com/en/futuresv2/#submit-order-signed + https://developer-pro.bitmart.com/en/futuresv2/#submit-plan-order-signed + https://developer-pro.bitmart.com/en/futuresv2/#submit-tp-sl-order-signed + https://developer-pro.bitmart.com/en/futuresv2/#submit-trail-order-signed + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market', 'limit', 'trailing', 'stop_loss', or 'take_profit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.leverage]: leverage level + :param boolean [params.reduceOnly]: *swap only* reduce only + :param str [params.marginMode]: 'cross' or 'isolated', default is 'cross' + :param str [params.clientOrderId]: client order id of the order + :param str [params.triggerPrice]: *swap only* the price to trigger a stop order + :param int [params.price_type]: *swap only* 1: last price, 2: fair price, default is 1 + :param int [params.price_way]: *swap only* 1: price way long, 2: price way short + :param int [params.activation_price_type]: *swap trailing order only* 1: last price, 2: fair price, default is 1 + :param str [params.trailingPercent]: *swap only* the percent to trail away from the current market price, min 0.1 max 5 + :param str [params.trailingTriggerPrice]: *swap only* the price to trigger a trailing order, default uses the price argument + :param str [params.stopLossPrice]: *swap only* the price to trigger a stop-loss order + :param str [params.takeProfitPrice]: *swap only* the price to trigger a take-profit order + :param int [params.plan_category]: *swap tp/sl only* 1: tp/sl, 2: position tp/sl, default is 1 + :returns dict: an `order structure ` + """ + market = self.market(symbol) + stopLossPrice = self.safe_string(params, 'stopLossPrice') + takeProfitPrice = self.safe_string(params, 'takeProfitPrice') + isStopLoss = stopLossPrice is not None + isTakeProfit = takeProfitPrice is not None + if isStopLoss: + type = 'stop_loss' + elif isTakeProfit: + type = 'take_profit' + request: dict = { + 'symbol': market['id'], + 'size': int(self.amount_to_precision(symbol, amount)), + } + timeInForce = self.safe_string(params, 'timeInForce') + mode = self.safe_integer(params, 'mode') # only for swap + isMarketOrder = type == 'market' + postOnly = None + reduceOnly = self.safe_bool(params, 'reduceOnly') + isExchangeSpecificPo = (mode == 4) + postOnly, params = self.handle_post_only(isMarketOrder, isExchangeSpecificPo, params) + ioc = ((timeInForce == 'IOC') or (mode == 3)) + isLimitOrder = (type == 'limit') or postOnly or ioc + if timeInForce == 'GTC': + request['mode'] = 1 + elif timeInForce == 'FOK': + request['mode'] = 2 + elif timeInForce == 'IOC': + request['mode'] = 3 + if postOnly: + request['mode'] = 4 + triggerPrice = self.safe_string_n(params, ['triggerPrice', 'stopPrice', 'trigger_price']) + isTriggerOrder = triggerPrice is not None + trailingTriggerPrice = self.safe_string_2(params, 'trailingTriggerPrice', 'activation_price', self.number_to_string(price)) + trailingPercent = self.safe_string_2(params, 'trailingPercent', 'callback_rate') + isTrailingPercentOrder = trailingPercent is not None + if isLimitOrder: + request['price'] = self.price_to_precision(symbol, price) + elif type == 'trailing' or isTrailingPercentOrder: + type = 'trailing' + request['callback_rate'] = trailingPercent + request['activation_price'] = self.price_to_precision(symbol, trailingTriggerPrice) + request['activation_price_type'] = self.safe_integer(params, 'activation_price_type', 1) + if isTriggerOrder: + if isLimitOrder or price is not None: + request['executive_price'] = self.price_to_precision(symbol, price) + request['trigger_price'] = self.price_to_precision(symbol, triggerPrice) + request['price_type'] = self.safe_integer(params, 'price_type', 1) + if side == 'buy': + if reduceOnly: + request['price_way'] = 2 + else: + request['price_way'] = 1 + elif side == 'sell': + if reduceOnly: + request['price_way'] = 1 + else: + request['price_way'] = 2 + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('createOrder', params, 'cross') + if isStopLoss or isTakeProfit: + reduceOnly = True + request['price_type'] = self.safe_integer(params, 'price_type', 1) + request['executive_price'] = self.price_to_precision(symbol, price) + if isStopLoss: + request['trigger_price'] = self.price_to_precision(symbol, stopLossPrice) + else: + request['trigger_price'] = self.price_to_precision(symbol, takeProfitPrice) + else: + request['open_type'] = marginMode + if side == 'buy': + if reduceOnly: + request['side'] = 2 # buy close short + else: + request['side'] = 1 # buy open long + elif side == 'sell': + if reduceOnly: + request['side'] = 3 # sell close long + else: + request['side'] = 4 # sell open short + clientOrderId = self.safe_string(params, 'clientOrderId') + if clientOrderId is not None: + params = self.omit(params, 'clientOrderId') + request['client_order_id'] = clientOrderId + leverage = self.safe_integer(params, 'leverage') + params = self.omit(params, ['timeInForce', 'postOnly', 'reduceOnly', 'leverage', 'trailingTriggerPrice', 'trailingPercent', 'triggerPrice', 'stopPrice', 'stopLossPrice', 'takeProfitPrice']) + if leverage is not None: + request['leverage'] = self.number_to_string(leverage) + elif isTriggerOrder: + request['leverage'] = '1' # for plan orders leverage is required, if not available default to 1 + if type != 'trailing': + request['type'] = type + return self.extend(request, params) + + def create_spot_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + @ignore + create a spot order request + https://developer-pro.bitmart.com/en/spot/#new-order-v2-signed + https://developer-pro.bitmart.com/en/spot/#new-margin-order-v1-signed + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' + :returns dict: an `order structure ` + """ + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'side': side, + 'type': type, + } + timeInForce = self.safe_string(params, 'timeInForce') + if timeInForce == 'FOK': + raise InvalidOrder(self.id + ' createOrder() only accepts timeInForce parameter values of IOC or PO') + mode = self.safe_integer(params, 'mode') # only for swap + isMarketOrder = type == 'market' + postOnly = None + isExchangeSpecificPo = (type == 'limit_maker') or (mode == 4) + postOnly, params = self.handle_post_only(isMarketOrder, isExchangeSpecificPo, params) + params = self.omit(params, ['timeInForce', 'postOnly']) + ioc = ((timeInForce == 'IOC') or (type == 'ioc')) + isLimitOrder = (type == 'limit') or postOnly or ioc + # method = 'privatePostSpotV2SubmitOrder' + if isLimitOrder: + request['size'] = self.amount_to_precision(symbol, amount) + request['price'] = self.price_to_precision(symbol, price) + elif isMarketOrder: + # for market buy it requires the amount of quote currency to spend + if side == 'buy': + notional = self.safe_string_2(params, 'cost', 'notional') + params = self.omit(params, 'cost') + createMarketBuyOrderRequiresPrice = True + createMarketBuyOrderRequiresPrice, params = self.handle_option_and_params(params, 'createOrder', 'createMarketBuyOrderRequiresPrice', True) + if createMarketBuyOrderRequiresPrice: + if (price is None) and (notional is None): + raise InvalidOrder(self.id + ' createOrder() requires the price argument for market buy orders to calculate the total cost to spend(amount * price), alternatively set the createMarketBuyOrderRequiresPrice option or param to False and pass the cost to spend in the amount argument or in the "notional" extra parameter(the exchange-specific behaviour)') + else: + amountString = self.number_to_string(amount) + priceString = self.number_to_string(price) + notional = Precise.string_mul(amountString, priceString) + else: + notional = self.number_to_string(amount) if (notional is None) else notional + request['notional'] = self.decimal_to_precision(notional, TRUNCATE, market['precision']['price'], self.precisionMode) + elif side == 'sell': + request['size'] = self.amount_to_precision(symbol, amount) + if postOnly: + request['type'] = 'limit_maker' + if ioc: + request['type'] = 'ioc' + clientOrderId = self.safe_string(params, 'clientOrderId') + if clientOrderId is not None: + params = self.omit(params, 'clientOrderId') + request['client_order_id'] = clientOrderId + return self.extend(request, params) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://developer-pro.bitmart.com/en/futuresv2/#cancel-order-signed + https://developer-pro.bitmart.com/en/spot/#cancel-order-v3-signed + https://developer-pro.bitmart.com/en/futuresv2/#cancel-plan-order-signed + https://developer-pro.bitmart.com/en/futuresv2/#cancel-order-signed + https://developer-pro.bitmart.com/en/futuresv2/#cancel-trail-order-signed + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.clientOrderId]: *spot only* the client order id of the order to cancel + :param boolean [params.trigger]: *swap only* whether the order is a trigger order + :param boolean [params.trailing]: *swap only* whether the order is a stop order + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + clientOrderId = self.safe_string_2(params, 'clientOrderId', 'client_order_id') + if clientOrderId is not None: + request['client_order_id'] = clientOrderId + else: + request['order_id'] = str(id) + params = self.omit(params, ['clientOrderId']) + response = None + if market['spot']: + response = self.privatePostSpotV3CancelOrder(self.extend(request, params)) + else: + trigger = self.safe_bool_2(params, 'stop', 'trigger') + trailing = self.safe_bool(params, 'trailing') + params = self.omit(params, ['stop', 'trigger']) + if trigger: + response = self.privatePostContractPrivateCancelPlanOrder(self.extend(request, params)) + elif trailing: + response = self.privatePostContractPrivateCancelTrailOrder(self.extend(request, params)) + else: + response = self.privatePostContractPrivateCancelOrder(self.extend(request, params)) + # swap + # {"code":1000,"message":"Ok","trace":"7f9c94e10f9d4513bc08a7bfc2a5559a.55.16959817848001851"} + # + # spot + # + # { + # "code": 1000, + # "trace":"886fb6ae-456b-4654-b4e0-d681ac05cea1", + # "message": "OK", + # "data": { + # "result": True + # } + # } + # + # spot alternative + # + # { + # "code": 1000, + # "trace":"886fb6ae-456b-4654-b4e0-d681ac05cea1", + # "message": "OK", + # "data": True + # } + # + if market['swap']: + return self.safe_order({'info': response}) + data = self.safe_value(response, 'data') + if data is True: + return self.safe_order({'id': id}, market) + succeeded = self.safe_value(data, 'succeed') + if succeeded is not None: + id = self.safe_string(succeeded, 0) + if id is None: + raise InvalidOrder(self.id + ' cancelOrder() failed to cancel ' + symbol + ' order id ' + id) + else: + result = self.safe_value(data, 'result') + if not result: + raise InvalidOrder(self.id + ' cancelOrder() ' + symbol + ' order id ' + id + ' is filled or canceled') + order = self.safe_order({'id': id, 'symbol': market['symbol'], 'info': {}}, market) + return order + + def cancel_orders(self, ids: List[str], symbol: Str = None, params={}) -> List[Order]: + """ + cancel multiple orders + + https://developer-pro.bitmart.com/en/spot/#cancel-batch-order-v4-signed + + :param str[] ids: order ids + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str[] [params.clientOrderIds]: client order ids + :returns dict: an list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrders() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' cancelOrders() does not support ' + market['type'] + ' orders, only spot orders are accepted') + clientOrderIds = self.safe_list(params, 'clientOrderIds') + params = self.omit(params, ['clientOrderIds']) + request: dict = { + 'symbol': market['id'], + } + if clientOrderIds is not None: + request['clientOrderIds'] = clientOrderIds + else: + request['orderIds'] = ids + response = self.privatePostSpotV4CancelOrders(self.extend(request, params)) + # + # { + # "message": "OK", + # "code": 1000, + # "trace": "c4edbce860164203954f7c3c81d60fc6.309.17022669632770001", + # "data": { + # "successIds": [ + # "213055379155243012" + # ], + # "failIds": [], + # "totalCount": 1, + # "successCount": 1, + # "failedCount": 0 + # } + # } + # + data = self.safe_dict(response, 'data', {}) + allOrders = [] + successIds = self.safe_list(data, 'successIds', []) + for i in range(0, len(successIds)): + id = successIds[i] + allOrders.append(self.safe_order({'id': id, 'status': 'canceled'}, market)) + failIds = self.safe_list(data, 'failIds', []) + for i in range(0, len(failIds)): + id = failIds[i] + allOrders.append(self.safe_order({'id': id, 'status': 'failed'}, market)) + return allOrders + + def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders in a market + + https://developer-pro.bitmart.com/en/spot/#cancel-all-order-v4-signed + https://developer-pro.bitmart.com/en/futuresv2/#cancel-all-orders-signed + + :param str symbol: unified market symbol of the market to cancel orders in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.side]: *spot only* 'buy' or 'sell' + :returns dict[]: a list of `order structures ` + """ + self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + response = None + type = None + type, params = self.handle_market_type_and_params('cancelAllOrders', market, params) + if type == 'spot': + response = self.privatePostSpotV4CancelAll(self.extend(request, params)) + elif type == 'swap': + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelAllOrders() requires a symbol argument') + response = self.privatePostContractPrivateCancelOrders(self.extend(request, params)) + # + # spot + # + # { + # "code": 1000, + # "trace":"886fb6ae-456b-4654-b4e0-d681ac05cea1", + # "message": "OK", + # "data": {} + # } + # + # swap + # + # { + # "code": 1000, + # "message": "Ok", + # "trace": "7f9c94e10f9d4513bc08a7bfc2a5559a.70.16954131323145323" + # } + # + return [self.safe_order({'info': response})] + + def fetch_orders_by_status(self, status, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrdersByStatus() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' fetchOrdersByStatus() does not support ' + market['type'] + ' orders, only spot orders are accepted') + request: dict = { + 'symbol': market['id'], + 'offset': 1, # max offset * limit < 500 + 'N': 100, # max limit is 100 + } + if status == 'open': + request['status'] = 9 + elif status == 'closed': + request['status'] = 6 + elif status == 'canceled': + request['status'] = 8 + else: + request['status'] = status + response = self.privateGetSpotV3Orders(self.extend(request, params)) + # + # spot + # + # { + # "message":"OK", + # "code":1000, + # "trace":"70e7d427-7436-4fb8-8cdd-97e1f5eadbe9", + # "data":{ + # "current_page":1, + # "orders":[ + # { + # "order_id":2147601241, + # "symbol":"BTC_USDT", + # "create_time":1591099963000, + # "side":"sell", + # "type":"limit", + # "price":"9000.00", + # "price_avg":"0.00", + # "size":"1.00000", + # "notional":"9000.00000000", + # "filled_notional":"0.00000000", + # "filled_size":"0.00000", + # "status":"4" + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + orders = self.safe_list(data, 'orders', []) + return self.parse_orders(orders, market, since, limit) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + + https://developer-pro.bitmart.com/en/spot/#current-open-orders-v4-signed + https://developer-pro.bitmart.com/en/futuresv2/#get-all-open-orders-keyed + https://developer-pro.bitmart.com/en/futuresv2/#get-all-current-plan-orders-keyed + + fetch all unfilled currently open orders + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.marginMode]: *spot* whether to fetch trades for margin orders or spot orders, defaults to spot orders(only isolated margin orders are supported) + :param int [params.until]: *spot* the latest time in ms to fetch orders for + :param str [params.type]: *swap* order type, 'limit' or 'market' + :param str [params.order_state]: *swap* the order state, 'all' or 'partially_filled', default is 'all' + :param str [params.orderType]: *swap only* 'limit', 'market', or 'trailing' + :param boolean [params.trailing]: *swap only* set to True if you want to fetch trailing orders + :param boolean [params.trigger]: *swap only* set to True if you want to fetch trigger orders + :param str [params.stpMode]: self-trade prevention only for spot, defaults to none, ['none', 'cancel_maker', 'cancel_taker', 'cancel_both'] + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + market = None + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + type = None + response = None + type, params = self.handle_market_type_and_params('fetchOpenOrders', market, params) + if type == 'spot': + if limit is not None: + request['limit'] = min(limit, 200) + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchOpenOrders', params) + if marginMode == 'isolated': + request['orderMode'] = 'iso_margin' + if since is not None: + request['startTime'] = since + until = self.safe_integer_2(params, 'until', 'endTime') + if until is not None: + params = self.omit(params, ['endTime']) + request['endTime'] = until + response = self.privatePostSpotV4QueryOpenOrders(self.extend(request, params)) + elif type == 'swap': + if limit is not None: + request['limit'] = min(limit, 100) + isTrigger = self.safe_bool_2(params, 'stop', 'trigger') + params = self.omit(params, ['stop', 'trigger']) + if isTrigger: + response = self.privateGetContractPrivateCurrentPlanOrder(self.extend(request, params)) + else: + trailing = self.safe_bool(params, 'trailing', False) + orderType = self.safe_string(params, 'orderType') + params = self.omit(params, ['orderType', 'trailing']) + if trailing: + orderType = 'trailing' + if orderType is not None: + request['type'] = orderType + response = self.privateGetContractPrivateGetOpenOrders(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchOpenOrders() does not support ' + type + ' orders, only spot and swap orders are accepted') + # + # spot + # + # { + # "code": 1000, + # "message": "success", + # "data": [ + # { + # "orderId": "183299373022163211", + # "clientOrderId": "183299373022163211", + # "symbol": "BTC_USDT", + # "side": "buy", + # "orderMode": "spot", + # "type": "limit", + # "state": "new", + # "price": "25000.00", + # "priceAvg": "0.00", + # "size": "0.00020", + # "filledSize": "0.00000", + # "notional": "5.00000000", + # "filledNotional": "0.00000000", + # "createTime": 1695703703338, + # "updateTime": 1695703703359 + # } + # ], + # "trace": "15f11d48e3234c81a2e786cr2e7a38e6.71.16957022303515933" + # } + # + # swap + # + # { + # "code": 1000, + # "message": "Ok", + # "data": [ + # { + # "order_id": "230935812485489", + # "client_order_id": "", + # "price": "24000", + # "size": "1", + # "symbol": "BTCUSDT", + # "state": 2, + # "side": 1, + # "type": "limit", + # "leverage": "10", + # "open_type": "isolated", + # "deal_avg_price": "0", + # "deal_size": "0", + # "create_time": 1695702258629, + # "update_time": 1695702258642 + # } + # ], + # "trace": "7f9d94g10f9d4513bc08a7rfc3a5559a.71.16957022303515933" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_orders(data, market, since, limit) + + def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + + https://developer-pro.bitmart.com/en/spot/#account-orders-v4-signed + https://developer-pro.bitmart.com/en/futuresv2/#get-order-history-keyed + + fetches information on multiple closed orders made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest entry + :param str [params.marginMode]: *spot only* 'cross' or 'isolated', for margin trading + :param str [params.stpMode]: self-trade prevention only for spot, defaults to none, ['none', 'cancel_maker', 'cancel_taker', 'cancel_both'] + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + market = None + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + type = None + type, params = self.handle_market_type_and_params('fetchClosedOrders', market, params) + if type != 'spot': + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchClosedOrders() requires a symbol argument') + if since is not None: + startTimeKey = 'startTime' if (type == 'spot') else 'start_time' + request[startTimeKey] = since + endTimeKey = 'endTime' if (type == 'spot') else 'end_time' + until = self.safe_integer_2(params, 'until', endTimeKey) + if until is not None: + params = self.omit(params, ['until']) + request[endTimeKey] = until + response = None + if type == 'spot': + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchClosedOrders', params) + if marginMode == 'isolated': + request['orderMode'] = 'iso_margin' + response = self.privatePostSpotV4QueryHistoryOrders(self.extend(request, params)) + else: + response = self.privateGetContractPrivateOrderHistory(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + return self.parse_orders(data, market, since, limit) + + def fetch_canceled_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches information on multiple canceled orders made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: timestamp in ms of the earliest order, default is None + :param int [limit]: max number of orders to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `order structures ` + """ + return self.fetch_orders_by_status('canceled', symbol, since, limit, params) + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://developer-pro.bitmart.com/en/spot/#query-order-by-id-v4-signed + https://developer-pro.bitmart.com/en/spot/#query-order-by-clientorderid-v4-signed + https://developer-pro.bitmart.com/en/futuresv2/#get-order-detail-keyed + + :param str id: the id of the order + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.clientOrderId]: *spot* fetch the order by client order id instead of order id + :param str [params.orderType]: *swap only* 'limit', 'market', 'liquidate', 'bankruptcy', 'adl' or 'trailing' + :param boolean [params.trailing]: *swap only* set to True if you want to fetch a trailing order + :param str [params.stpMode]: self-trade prevention only for spot, defaults to none, ['none', 'cancel_maker', 'cancel_taker', 'cancel_both'] + :returns dict: An `order structure ` + """ + self.load_markets() + request: dict = {} + type = None + market = None + response = None + if symbol is not None: + market = self.market(symbol) + type, params = self.handle_market_type_and_params('fetchOrder', market, params) + if type == 'spot': + clientOrderId = self.safe_string(params, 'clientOrderId') + if not clientOrderId: + request['orderId'] = id + if clientOrderId is not None: + response = self.privatePostSpotV4QueryClientOrder(self.extend(request, params)) + else: + response = self.privatePostSpotV4QueryOrder(self.extend(request, params)) + elif type == 'swap': + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrder() requires a symbol argument') + trailing = self.safe_bool(params, 'trailing', False) + orderType = self.safe_string(params, 'orderType') + params = self.omit(params, ['orderType', 'trailing']) + if trailing: + orderType = 'trailing' + if orderType is not None: + request['type'] = orderType + request['symbol'] = market['id'] + request['order_id'] = id + response = self.privateGetContractPrivateOrder(self.extend(request, params)) + # + # spot + # + # { + # "code": 1000, + # "message": "success", + # "data": { + # "orderId": "183347420821295423", + # "clientOrderId": "183347420821295423", + # "symbol": "BTC_USDT", + # "side": "buy", + # "orderMode": "spot", + # "type": "limit", + # "state": "new", + # "price": "24000.00", + # "priceAvg": "0.00", + # "size": "0.00022", + # "filledSize": "0.00000", + # "notional": "5.28000000", + # "filledNotional": "0.00000000", + # "createTime": 1695783014734, + # "updateTime": 1695783014762 + # }, + # "trace": "ce3e6422c8b44d5fag855348a68693ed.63.14957831547451715" + # } + # + # swap + # + # { + # "code": 1000, + # "message": "Ok", + # "data": { + # "order_id": "230927283405028", + # "client_order_id": "", + # "price": "23000", + # "size": "1", + # "symbol": "BTCUSDT", + # "state": 2, + # "side": 1, + # "type": "limit", + # "leverage": "10", + # "open_type": "isolated", + # "deal_avg_price": "0", + # "deal_size": "0", + # "create_time": 1695783433600, + # "update_time": 1695783433613 + # }, + # "trace": "4cad855075664097af6ba5257c47605d.63.14957831547451715" + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data, market) + + def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://developer-pro.bitmart.com/en/spot/#deposit-address-keyed + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + self.load_markets() + currency = self.currency(code) + network: Str = None + network, params = self.handle_network_code_and_params(params) + request: dict = { + 'currency': self.get_currency_id_from_code_and_network(code, network), + } + response = self.privateGetAccountV1DepositAddress(self.extend(request, params)) + # + # { + # "message": "OK", + # "code": 1000, + # "trace": "0e6edd79-f77f-4251-abe5-83ba75d06c1a", + # "data": { + # currency: 'ETH', + # chain: 'Ethereum', + # address: '0x99B5EEc2C520f86F0F62F05820d28D05D36EccCf', + # address_memo: '' + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_deposit_address(data, currency) + + def parse_deposit_address(self, depositAddress, currency=None) -> DepositAddress: + # + # fetchDepositAddress + # { + # currency: 'ETH', + # chain: 'Ethereum', + # address: '0x99B5EEc2C520f86F0F62F05820d28D05D36EccCf', + # address_memo: '' + # } + # + # fetchWithdrawAddress + # { + # "currency": "ETH", + # "network": "ETH", + # "address": "0x1121", + # "memo": "12", + # "remark": "12", + # "addressType": 0, + # "verifyStatus": 0 + # } + # + currencyId = self.safe_string(depositAddress, 'currency') + network = self.safe_string_2(depositAddress, 'chain', 'network') + if currencyId.find('NFT') < 0: + parts = currencyId.split('-') + currencyId = self.safe_string(parts, 0) + secondPart = self.safe_string(parts, 1) + if secondPart is not None: + network = secondPart + address = self.safe_string(depositAddress, 'address') + currency = self.safe_currency(currencyId, currency) + self.check_address(address) + return { + 'info': depositAddress, + 'currency': self.safe_string(currency, 'code'), + 'network': self.network_id_to_code(network), + 'address': address, + 'tag': self.safe_string_2(depositAddress, 'address_memo', 'memo'), + } + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://developer-pro.bitmart.com/en/spot/#withdraw-signed + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.network]: the network name for self withdrawal + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.check_address(address) + self.load_markets() + currency = self.currency(code) + network: Str = None + network, params = self.handle_network_code_and_params(params) + request: dict = { + 'currency': self.get_currency_id_from_code_and_network(code, network), + 'amount': amount, + 'destination': 'To Digital Address', # To Digital Address, To Binance, To OKEX + 'address': address, + } + if tag is not None: + request['address_memo'] = tag + response = self.privatePostAccountV1WithdrawApply(self.extend(request, params)) + # + # { + # "code": 1000, + # "trace":"886fb6ae-456b-4654-b4e0-d681ac05cea1", + # "message": "OK", + # "data": { + # "withdraw_id": "121212" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + transaction = self.parse_transaction(data, currency) + return self.extend(transaction, { + 'code': code, + 'address': address, + 'tag': tag, + }) + + def fetch_transactions_by_type(self, type, code: Str = None, since: Int = None, limit: Int = None, params={}): + self.load_markets() + if limit is None: + limit = 1000 # max 1000 + request: dict = { + 'operation_type': type, # deposit or withdraw + 'N': limit, + } + currency = None + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + if since is not None: + request['startTime'] = since + until = self.safe_integer(params, 'until') + if until is not None: + params = self.omit(params, 'until') + request['endTime'] = until + response = self.privateGetAccountV2DepositWithdrawHistory(self.extend(request, params)) + # + # { + # "message":"OK", + # "code":1000, + # "trace":"142bf92a-fc50-4689-92b6-590886f90b97", + # "data":{ + # "records":[ + # { + # "withdraw_id":"1679952", + # "deposit_id":"", + # "operation_type":"withdraw", + # "currency":"BMX", + # "apply_time":1588867374000, + # "arrival_amount":"59.000000000000", + # "fee":"1.000000000000", + # "status":0, + # "address":"0xe57b69a8776b37860407965B73cdFFBDFe668Bb5", + # "address_memo":"", + # "tx_id":"" + # }, + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + records = self.safe_list(data, 'records', []) + return self.parse_transactions(records, currency, since, limit) + + def fetch_deposit(self, id: str, code: Str = None, params={}): + """ + fetch information on a deposit + + https://developer-pro.bitmart.com/en/spot/#get-a-deposit-or-withdraw-detail-keyed + + :param str id: deposit id + :param str code: not used by bitmart fetchDeposit() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + self.load_markets() + request: dict = { + 'id': id, + } + response = self.privateGetAccountV1DepositWithdrawDetail(self.extend(request, params)) + # + # { + # "message":"OK", + # "code":1000, + # "trace":"f7f74924-14da-42a6-b7f2-d3799dd9a612", + # "data":{ + # "record":{ + # "withdraw_id":"", + # "deposit_id":"1679952", + # "operation_type":"deposit", + # "currency":"BMX", + # "apply_time":1588867374000, + # "arrival_amount":"59.000000000000", + # "fee":"1.000000000000", + # "status":0, + # "address":"0xe57b69a8776b37860407965B73cdFFBDFe668Bb5", + # "address_memo":"", + # "tx_id":"" + # } + # } + # } + # + data = self.safe_dict(response, 'data', {}) + record = self.safe_dict(data, 'record', {}) + return self.parse_transaction(record) + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://developer-pro.bitmart.com/en/spot/#get-deposit-and-withdraw-history-keyed + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + return self.fetch_transactions_by_type('deposit', code, since, limit, params) + + def fetch_withdrawal(self, id: str, code: Str = None, params={}): + """ + fetch data on a currency withdrawal via the withdrawal id + + https://developer-pro.bitmart.com/en/spot/#get-a-deposit-or-withdraw-detail-keyed + + :param str id: withdrawal id + :param str code: not used by bitmart.fetchWithdrawal + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + self.load_markets() + request: dict = { + 'id': id, + } + response = self.privateGetAccountV1DepositWithdrawDetail(self.extend(request, params)) + # + # { + # "message":"OK", + # "code":1000, + # "trace":"f7f74924-14da-42a6-b7f2-d3799dd9a612", + # "data":{ + # "record":{ + # "withdraw_id":"1679952", + # "deposit_id":"", + # "operation_type":"withdraw", + # "currency":"BMX", + # "apply_time":1588867374000, + # "arrival_amount":"59.000000000000", + # "fee":"1.000000000000", + # "status":0, + # "address":"0xe57b69a8776b37860407965B73cdFFBDFe668Bb5", + # "address_memo":"", + # "tx_id":"" + # } + # } + # } + # + data = self.safe_dict(response, 'data', {}) + record = self.safe_dict(data, 'record', {}) + return self.parse_transaction(record) + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://developer-pro.bitmart.com/en/spot/#get-deposit-and-withdraw-history-keyed + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + return self.fetch_transactions_by_type('withdraw', code, since, limit, params) + + def parse_transaction_status(self, status: Str): + statuses: dict = { + '0': 'pending', # Create + '1': 'pending', # Submitted, waiting for withdrawal + '2': 'pending', # Processing + '3': 'ok', # Success + '4': 'canceled', # Cancel + '5': 'failed', # Fail + } + return self.safe_string(statuses, status, status) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # withdraw + # + # { + # "withdraw_id": "121212" + # } + # + # fetchDeposits, fetchWithdrawals, fetchWithdrawal + # + # { + # "withdraw_id":"1679952", + # "deposit_id":"", + # "operation_type":"withdraw", + # "currency":"BMX", + # "apply_time":1588867374000, + # "arrival_amount":"59.000000000000", + # "fee":"1.000000000000", + # "status":0, + # "address":"0xe57b69a8776b37860407965B73cdFFBDFe668Bb5", + # "address_memo":"", + # "tx_id":"" + # } + # + id = None + withdrawId = self.safe_string(transaction, 'withdraw_id') + depositId = self.safe_string(transaction, 'deposit_id') + type = None + if (withdrawId is not None) and (withdrawId != ''): + type = 'withdraw' + id = withdrawId + elif (depositId is not None) and (depositId != ''): + type = 'deposit' + id = depositId + amount = self.safe_number(transaction, 'arrival_amount') + timestamp = self.safe_integer(transaction, 'apply_time') + currencyId = self.safe_string(transaction, 'currency') + networkId: Str = None + if currencyId is not None: + if currencyId.find('NFT') < 0: + parts = currencyId.split('-') + currencyId = self.safe_string(parts, 0) + networkId = self.safe_string(parts, 1) + code = self.safe_currency_code(currencyId, currency) + status = self.parse_transaction_status(self.safe_string(transaction, 'status')) + feeCost = self.safe_number(transaction, 'fee') + fee = None + if feeCost is not None: + fee = { + 'cost': feeCost, + 'currency': code, + } + txid = self.safe_string(transaction, 'tx_id') + address = self.safe_string(transaction, 'address') + tag = self.safe_string(transaction, 'address_memo') + return { + 'info': transaction, + 'id': id, + 'currency': code, + 'amount': amount, + 'network': self.network_id_to_code(networkId), + 'address': address, + 'addressFrom': None, + 'addressTo': None, + 'tag': tag, + 'tagFrom': None, + 'tagTo': None, + 'status': status, + 'type': type, + 'updated': None, + 'txid': txid, + 'internal': None, + 'comment': None, + 'timestamp': timestamp if (timestamp != 0) else None, + 'datetime': self.iso8601(timestamp) if (timestamp != 0) else None, + 'fee': fee, + } + + def repay_isolated_margin(self, symbol: str, code: str, amount, params={}): + """ + repay borrowed margin and interest + + https://developer-pro.bitmart.com/en/spot/#margin-repay-isolated-signed + + :param str symbol: unified market symbol + :param str code: unified currency code of the currency to repay + :param str amount: the amount to repay + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin loan structure ` + """ + self.load_markets() + market = self.market(symbol) + currency = self.currency(code) + request: dict = { + 'symbol': market['id'], + 'currency': currency['id'], + 'amount': self.currency_to_precision(code, amount), + } + response = self.privatePostSpotV1MarginIsolatedRepay(self.extend(request, params)) + # + # { + # "message": "OK", + # "code": 1000, + # "trace": "b0a60b4c-e986-4b54-a190-8f7c05ddf685", + # "data": { + # "repay_id": "2afcc16d99bd4707818c5a355dc89bed" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + transaction = self.parse_margin_loan(data, currency) + return self.extend(transaction, { + 'amount': amount, + 'symbol': symbol, + }) + + def borrow_isolated_margin(self, symbol: str, code: str, amount: float, params={}): + """ + create a loan to borrow margin + + https://developer-pro.bitmart.com/en/spot/#margin-borrow-isolated-signed + + :param str symbol: unified market symbol + :param str code: unified currency code of the currency to borrow + :param str amount: the amount to borrow + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin loan structure ` + """ + self.load_markets() + market = self.market(symbol) + currency = self.currency(code) + request: dict = { + 'symbol': market['id'], + 'currency': currency['id'], + 'amount': self.currency_to_precision(code, amount), + } + response = self.privatePostSpotV1MarginIsolatedBorrow(self.extend(request, params)) + # + # { + # "message": "OK", + # "code": 1000, + # "trace": "e6fda683-181e-4e78-ac9c-b27c4c8ba035", + # "data": { + # "borrow_id": "629a7177a4ed4cf09869c6a4343b788c" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + transaction = self.parse_margin_loan(data, currency) + return self.extend(transaction, { + 'amount': amount, + 'symbol': symbol, + }) + + def parse_margin_loan(self, info, currency: Currency = None): + # + # borrowMargin + # + # { + # "borrow_id": "629a7177a4ed4cf09869c6a4343b788c", + # } + # + # repayMargin + # + # { + # "repay_id": "2afcc16d99bd4707818c5a355dc89bed", + # } + # + return { + 'id': self.safe_string_2(info, 'borrow_id', 'repay_id'), + 'currency': self.safe_currency_code(None, currency), + 'amount': None, + 'symbol': None, + 'timestamp': None, + 'datetime': None, + 'info': info, + } + + def fetch_isolated_borrow_rate(self, symbol: str, params={}) -> IsolatedBorrowRate: + """ + fetch the rate of interest to borrow a currency for margin trading + + https://developer-pro.bitmart.com/en/spot/#get-trading-pair-borrowing-rate-and-amount-keyed + + :param str symbol: unified symbol of the market to fetch the borrow rate for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `isolated borrow rate structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = self.privateGetSpotV1MarginIsolatedPairs(self.extend(request, params)) + # + # { + # "message": "OK", + # "code": 1000, + # "trace": "0985a130-a5ae-4fc1-863f-4704e214f585", + # "data": { + # "symbols": [ + # { + # "symbol": "BTC_USDT", + # "max_leverage": "5", + # "symbol_enabled": True, + # "base": { + # "currency": "BTC", + # "daily_interest": "0.00055000", + # "hourly_interest": "0.00002291", + # "max_borrow_amount": "2.00000000", + # "min_borrow_amount": "0.00000001", + # "borrowable_amount": "0.00670810" + # }, + # "quote": { + # "currency": "USDT", + # "daily_interest": "0.00055000", + # "hourly_interest": "0.00002291", + # "max_borrow_amount": "50000.00000000", + # "min_borrow_amount": "0.00000001", + # "borrowable_amount": "135.12575038" + # } + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + symbols = self.safe_list(data, 'symbols', []) + borrowRate = self.safe_dict(symbols, 0, []) + return self.parse_isolated_borrow_rate(borrowRate, market) + + def parse_isolated_borrow_rate(self, info: dict, market: Market = None) -> IsolatedBorrowRate: + # + # { + # "symbol": "BTC_USDT", + # "max_leverage": "5", + # "symbol_enabled": True, + # "base": { + # "currency": "BTC", + # "daily_interest": "0.00055000", + # "hourly_interest": "0.00002291", + # "max_borrow_amount": "2.00000000", + # "min_borrow_amount": "0.00000001", + # "borrowable_amount": "0.00670810" + # }, + # "quote": { + # "currency": "USDT", + # "daily_interest": "0.00055000", + # "hourly_interest": "0.00002291", + # "max_borrow_amount": "50000.00000000", + # "min_borrow_amount": "0.00000001", + # "borrowable_amount": "135.12575038" + # } + # } + # + marketId = self.safe_string(info, 'symbol') + symbol = self.safe_symbol(marketId, market) + baseData = self.safe_dict(info, 'base', {}) + quoteData = self.safe_dict(info, 'quote', {}) + baseId = self.safe_string(baseData, 'currency') + quoteId = self.safe_string(quoteData, 'currency') + return { + 'symbol': symbol, + 'base': self.safe_currency_code(baseId), + 'baseRate': self.safe_number(baseData, 'hourly_interest'), + 'quote': self.safe_currency_code(quoteId), + 'quoteRate': self.safe_number(quoteData, 'hourly_interest'), + 'period': 3600000, # 1-Hour + 'timestamp': None, + 'datetime': None, + 'info': info, + } + + def fetch_isolated_borrow_rates(self, params={}) -> IsolatedBorrowRates: + """ + fetch the borrow interest rates of all currencies, currently only works for isolated margin + + https://developer-pro.bitmart.com/en/spot/#get-trading-pair-borrowing-rate-and-amount-keyed + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `isolated borrow rate structures ` + """ + self.load_markets() + response = self.privateGetSpotV1MarginIsolatedPairs(params) + # + # { + # "message": "OK", + # "code": 1000, + # "trace": "0985a130-a5ae-4fc1-863f-4704e214f585", + # "data": { + # "symbols": [ + # { + # "symbol": "BTC_USDT", + # "max_leverage": "5", + # "symbol_enabled": True, + # "base": { + # "currency": "BTC", + # "daily_interest": "0.00055000", + # "hourly_interest": "0.00002291", + # "max_borrow_amount": "2.00000000", + # "min_borrow_amount": "0.00000001", + # "borrowable_amount": "0.00670810" + # }, + # "quote": { + # "currency": "USDT", + # "daily_interest": "0.00055000", + # "hourly_interest": "0.00002291", + # "max_borrow_amount": "50000.00000000", + # "min_borrow_amount": "0.00000001", + # "borrowable_amount": "135.12575038" + # } + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + symbols = self.safe_list(data, 'symbols', []) + return self.parse_isolated_borrow_rates(symbols) + + def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account, currently only supports transfer between spot and margin + + https://developer-pro.bitmart.com/en/spot/#margin-asset-transfer-signed + https://developer-pro.bitmart.com/en/futuresv2/#transfer-signed + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account to transfer from + :param str toAccount: account to transfer to + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transfer structure ` + """ + self.load_markets() + currency = self.currency(code) + amountToPrecision = self.currency_to_precision(code, amount) + request: dict = { + 'amount': amountToPrecision, + 'currency': currency['id'], + } + fromId = self.convert_type_to_account(fromAccount) + toId = self.convert_type_to_account(toAccount) + if fromAccount == 'spot': + if toAccount == 'margin': + request['side'] = 'in' + request['symbol'] = toId + elif toAccount == 'swap': + request['type'] = 'spot_to_contract' + elif toAccount == 'spot': + if fromAccount == 'margin': + request['side'] = 'out' + request['symbol'] = fromId + elif fromAccount == 'swap': + request['type'] = 'contract_to_spot' + else: + raise ArgumentsRequired(self.id + ' transfer() requires either fromAccount or toAccount to be spot') + response = None + if (fromAccount == 'margin') or (toAccount == 'margin'): + response = self.privatePostSpotV1MarginIsolatedTransfer(self.extend(request, params)) + elif (fromAccount == 'swap') or (toAccount == 'swap'): + response = self.privatePostAccountV1TransferContract(self.extend(request, params)) + # + # margin + # + # { + # "message": "OK", + # "code": 1000, + # "trace": "b26cecec-ef5a-47d9-9531-2bd3911d3d55", + # "data": { + # "transfer_id": "ca90d97a621e47d49774f19af6b029f5" + # } + # } + # + # swap + # + # { + # "message": "OK", + # "code": 1000, + # "trace": "4cad858074667097ac6ba5257c57305d.68.16953302431189455", + # "data": { + # "currency": "USDT", + # "amount": "5" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.extend(self.parse_transfer(data, currency), { + 'status': self.parse_transfer_status(self.safe_string_2(response, 'code', 'message')), + }) + + def parse_transfer_status(self, status: Str) -> Str: + statuses: dict = { + '1000': 'ok', + 'OK': 'ok', + 'FINISHED': 'ok', + } + return self.safe_string(statuses, status, status) + + def parse_transfer_to_account(self, type): + types: dict = { + 'contract_to_spot': 'spot', + 'spot_to_contract': 'swap', + } + return self.safe_string(types, type, type) + + def parse_transfer_from_account(self, type): + types: dict = { + 'contract_to_spot': 'swap', + 'spot_to_contract': 'spot', + } + return self.safe_string(types, type, type) + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + # + # margin + # + # { + # "transfer_id": "ca90d97a621e47d49774f19af6b029f5" + # } + # + # swap + # + # { + # "currency": "USDT", + # "amount": "5" + # } + # + # fetchTransfers + # + # { + # "transfer_id": "902463535961567232", + # "currency": "USDT", + # "amount": "5", + # "type": "contract_to_spot", + # "state": "FINISHED", + # "timestamp": 1695330539565 + # } + # + currencyId = self.safe_string(transfer, 'currency') + timestamp = self.safe_integer(transfer, 'timestamp') + return { + 'id': self.safe_string(transfer, 'transfer_id'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'currency': self.safe_currency_code(currencyId, currency), + 'amount': self.safe_number(transfer, 'amount'), + 'fromAccount': self.parse_transfer_from_account(self.safe_string(transfer, 'type')), + 'toAccount': self.parse_transfer_to_account(self.safe_string(transfer, 'type')), + 'status': self.parse_transfer_status(self.safe_string(transfer, 'state')), + } + + def fetch_transfers(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[TransferEntry]: + """ + fetch a history of internal transfers made on an account, only transfers between spot and swap are supported + + https://developer-pro.bitmart.com/en/futuresv2/#get-transfer-list-signed + + :param str code: unified currency code of the currency transferred + :param int [since]: the earliest time in ms to fetch transfers for + :param int [limit]: the maximum number of transfer structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.page]: the required number of pages, default is 1, max is 1000 + :param int [params.until]: the latest time in ms to fetch transfers for + :returns dict[]: a list of `transfer structures ` + """ + self.load_markets() + if limit is None: + limit = 10 + request: dict = { + 'page': self.safe_integer(params, 'page', 1), # default is 1, max is 1000 + 'limit': limit, # default is 10, max is 100 + } + currency = None + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + if since is not None: + request['time_start'] = since + if limit is not None: + request['limit'] = limit + until = self.safe_integer(params, 'until') # unified in milliseconds + endTime = self.safe_integer(params, 'time_end', until) # exchange-specific in milliseconds + params = self.omit(params, ['until']) + if endTime is not None: + request['time_end'] = endTime + response = self.privatePostAccountV1TransferContractList(self.extend(request, params)) + # + # { + # "message": "OK", + # "code": 1000, + # "trace": "7f9d93e10f9g4513bc08a7btc2a5559a.69.16953325693032193", + # "data": { + # "records": [ + # { + # "transfer_id": "902463535961567232", + # "currency": "USDT", + # "amount": "5", + # "type": "contract_to_spot", + # "state": "FINISHED", + # "timestamp": 1695330539565 + # }, + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + records = self.safe_list(data, 'records', []) + return self.parse_transfers(records, currency, since, limit) + + def fetch_borrow_interest(self, code: Str = None, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[BorrowInterest]: + """ + fetch the interest owed by the user for borrowing currency for margin trading + + https://developer-pro.bitmart.com/en/spot/#get-borrow-record-isolated-keyed + + :param str code: unified currency code + :param str symbol: unified market symbol when fetch interest in isolated markets + :param int [since]: the earliest time in ms to fetch borrrow interest for + :param int [limit]: the maximum number of structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `borrow interest structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchBorrowInterest() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['N'] = limit + if since is not None: + request['start_time'] = since + response = self.privateGetSpotV1MarginIsolatedBorrowRecord(self.extend(request, params)) + # + # { + # "message": "OK", + # "code": 1000, + # "trace": "8ea27a2a-4aba-49fa-961d-43a0137b0ef3", + # "data": { + # "records": [ + # { + # "borrow_id": "1659045283903rNvJnuRTJNL5J53n", + # "symbol": "BTC_USDT", + # "currency": "USDT", + # "borrow_amount": "100.00000000", + # "daily_interest": "0.00055000", + # "hourly_interest": "0.00002291", + # "interest_amount": "0.00229166", + # "create_time": 1659045284000 + # }, + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + rows = self.safe_list(data, 'records', []) + interest = self.parse_borrow_interests(rows, market) + return self.filter_by_currency_since_limit(interest, code, since, limit) + + def parse_borrow_interest(self, info: dict, market: Market = None) -> BorrowInterest: + # + # { + # "borrow_id": "1657664327844Lk5eJJugXmdHHZoe", + # "symbol": "BTC_USDT", + # "currency": "USDT", + # "borrow_amount": "20.00000000", + # "daily_interest": "0.00055000", + # "hourly_interest": "0.00002291", + # "interest_amount": "0.00045833", + # "create_time": 1657664329000 + # } + # + marketId = self.safe_string(info, 'symbol') + market = self.safe_market(marketId, market) + timestamp = self.safe_integer(info, 'create_time') + return { + 'info': info, + 'symbol': self.safe_string(market, 'symbol'), + 'currency': self.safe_currency_code(self.safe_string(info, 'currency')), + 'interest': self.safe_number(info, 'interest_amount'), + 'interestRate': self.safe_number(info, 'hourly_interest'), + 'amountBorrowed': self.safe_number(info, 'borrow_amount'), + 'marginMode': 'isolated', + 'timestamp': timestamp, # borrow creation time + 'datetime': self.iso8601(timestamp), + } + + def fetch_open_interest(self, symbol: str, params={}): + """ + Retrieves the open interest of a currency + + https://developer-pro.bitmart.com/en/futuresv2/#get-futures-openinterest + + :param str symbol: Unified CCXT market symbol + :param dict [params]: exchange specific parameters + :returns dict} an open interest structure{@link https://docs.ccxt.com/#/?id=open-interest-structure: + """ + self.load_markets() + market = self.market(symbol) + if not market['contract']: + raise BadRequest(self.id + ' fetchOpenInterest() supports contract markets only') + request: dict = { + 'symbol': market['id'], + } + response = self.publicGetContractPublicOpenInterest(self.extend(request, params)) + # + # { + # "code": 1000, + # "message": "Ok", + # "data": { + # "timestamp": 1694657502415, + # "symbol": "BTCUSDT", + # "open_interest": "265231.721368593081729069", + # "open_interest_value": "7006353.83988919" + # }, + # "trace": "7f9c94e10f9d4513bc08a7bfc2a5559a.72.16946575108274991" + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_open_interest(data, market) + + def parse_open_interest(self, interest, market: Market = None): + # + # { + # "timestamp": 1694657502415, + # "symbol": "BTCUSDT", + # "open_interest": "265231.721368593081729069", + # "open_interest_value": "7006353.83988919" + # } + # + timestamp = self.safe_integer(interest, 'timestamp') + id = self.safe_string(interest, 'symbol') + return self.safe_open_interest({ + 'symbol': self.safe_symbol(id, market), + 'openInterestAmount': self.safe_number(interest, 'open_interest'), + 'openInterestValue': self.safe_number(interest, 'open_interest_value'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'info': interest, + }, market) + + def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://developer-pro.bitmart.com/en/futuresv2/#submit-leverage-signed + + :param float leverage: the rate of leverage + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'isolated' or 'cross' + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setLeverage() requires a symbol argument') + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('setLeverage', params) + self.check_required_argument('setLeverage', marginMode, 'marginMode', ['isolated', 'cross']) + self.load_markets() + market = self.market(symbol) + if not market['swap']: + raise BadSymbol(self.id + ' setLeverage() supports swap contracts only') + request: dict = { + 'symbol': market['id'], + 'leverage': str(leverage), + 'open_type': marginMode, + } + return self.privatePostContractPrivateSubmitLeverage(self.extend(request, params)) + + def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate + + https://developer-pro.bitmart.com/en/futuresv2/#get-current-funding-rate + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + self.load_markets() + market = self.market(symbol) + if not market['swap']: + raise BadSymbol(self.id + ' fetchFundingRate() supports swap contracts only') + request: dict = { + 'symbol': market['id'], + } + response = self.publicGetContractPublicFundingRate(self.extend(request, params)) + # + # { + # "code": 1000, + # "message": "Ok", + # "data": { + # "symbol": "BTCUSDT", + # "expected_rate": "-0.0000238", + # "rate_value": "0.000009601106", + # "funding_time": 1761292800000, + # "funding_upper_limit": "0.0375", + # "funding_lower_limit": "-0.0375", + # "timestamp": 1761291544336 + # }, + # "trace": "64b7a589-e1e-4ac2-86b1-41058757421" + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_funding_rate(data, market) + + def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical funding rate prices + + https://developer-pro.bitmart.com/en/futuresv2/#get-funding-rate-history + + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: not sent to exchange api, exchange api always returns the most recent data, only used to filter exchange response + :param int [limit]: the maximum amount of funding rate structures to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `funding rate structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchFundingRateHistory() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['limit'] = limit + response = self.publicGetContractPublicFundingRateHistory(self.extend(request, params)) + # + # { + # "code": 1000, + # "message": "Ok", + # "data": { + # "list": [ + # { + # "symbol": "BTCUSDT", + # "funding_rate": "0.000091412174", + # "funding_time": "1734336000000" + # }, + # ] + # }, + # "trace": "fg73d949fgfdf6a40c8fc7f5ae6738.54.345345345345" + # } + # + data = self.safe_dict(response, 'data', {}) + result = self.safe_list(data, 'list', []) + rates = [] + for i in range(0, len(result)): + entry = result[i] + marketId = self.safe_string(entry, 'symbol') + symbolInner = self.safe_symbol(marketId, market, '-', 'swap') + timestamp = self.safe_integer(entry, 'funding_time') + rates.append({ + 'info': entry, + 'symbol': symbolInner, + 'fundingRate': self.safe_number(entry, 'funding_rate'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + sorted = self.sort_by(rates, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, market['symbol'], since, limit) + + def parse_funding_rate(self, contract, market: Market = None) -> FundingRate: + # + # { + # "symbol": "BTCUSDT", + # "expected_rate": "-0.0000238", + # "rate_value": "0.000009601106", + # "funding_time": 1761292800000, + # "funding_upper_limit": "0.0375", + # "funding_lower_limit": "-0.0375", + # "timestamp": 1761291544336 + # } + # + marketId = self.safe_string(contract, 'symbol') + timestamp = self.safe_integer(contract, 'timestamp') + fundingTimestamp = self.safe_integer(contract, 'funding_time') + return { + 'info': contract, + 'symbol': self.safe_symbol(marketId, market), + 'markPrice': None, + 'indexPrice': None, + 'interestRate': None, + 'estimatedSettlePrice': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'fundingRate': self.safe_number(contract, 'expected_rate'), + 'fundingTimestamp': fundingTimestamp, + 'fundingDatetime': self.iso8601(fundingTimestamp), + 'nextFundingRate': None, + 'nextFundingTimestamp': None, + 'nextFundingDatetime': None, + 'previousFundingRate': self.safe_number(contract, 'rate_value'), + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': None, + } + + def fetch_position(self, symbol: str, params={}): + """ + fetch data on a single open contract trade position + + https://developer-pro.bitmart.com/en/futuresv2/#get-current-position-keyed + + :param str symbol: unified market symbol of the market the position is held in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `position structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = self.privateGetContractPrivatePosition(self.extend(request, params)) + # + # { + # "code": 1000, + # "message": "Ok", + # "data": [ + # { + # "symbol": "BTCUSDT", + # "leverage": "10", + # "timestamp": 1696392515269, + # "current_fee": "0.0014250028", + # "open_timestamp": 1696392256998, + # "current_value": "27.4039", + # "mark_price": "27.4039", + # "position_value": "27.4079", + # "position_cross": "3.75723474", + # "maintenance_margin": "0.1370395", + # "close_vol": "0", + # "close_avg_price": "0", + # "open_avg_price": "27407.9", + # "entry_price": "27407.9", + # "current_amount": "1", + # "unrealized_value": "-0.004", + # "realized_value": "-0.01644474", + # "position_type": 1 + # } + # ], + # "trace":"4cad855074664097ac5ba5257c47305d.67.16963925142065945" + # } + # + data = self.safe_list(response, 'data', []) + first = self.safe_dict(data, 0, {}) + return self.parse_position(first, market) + + def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open contract positions + + https://developer-pro.bitmart.com/en/futuresv2/#get-current-position-keyed + https://developer-pro.bitmart.com/en/futuresv2/#get-current-position-v2-keyed + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structures ` + """ + self.load_markets() + market = None + symbolsLength = None + if symbols is not None: + symbolsLength = len(symbols) + first = self.safe_string(symbols, 0) + market = self.market(first) + request: dict = {} + if symbolsLength == 1: + # only supports symbols or sending one symbol + request['symbol'] = market['id'] + response = self.privateGetContractPrivatePositionV2(self.extend(request, params)) + # + # { + # "code": 1000, + # "message": "Ok", + # "data": [ + # { + # "symbol": "BTCUSDT", + # "leverage": "10", + # "timestamp": 1696392515269, + # "current_fee": "0.0014250028", + # "open_timestamp": 1696392256998, + # "current_value": "27.4039", + # "mark_price": "27.4039", + # "position_value": "27.4079", + # "position_cross": "3.75723474", + # "maintenance_margin": "0.1370395", + # "close_vol": "0", + # "close_avg_price": "0", + # "open_avg_price": "27407.9", + # "entry_price": "27407.9", + # "current_amount": "1", + # "unrealized_value": "-0.004", + # "realized_value": "-0.01644474", + # "position_type": 1 + # }, + # ], + # "trace":"4cad855074664097ac5ba5257c47305d.67.16963925142065945" + # } + # + positions = self.safe_list(response, 'data', []) + result = [] + for i in range(0, len(positions)): + result.append(self.parse_position(positions[i])) + symbols = self.market_symbols(symbols) + return self.filter_by_array_positions(result, 'symbol', symbols, False) + + def parse_position(self, position: dict, market: Market = None): + # + # { + # "symbol": "BTCUSDT", + # "leverage": "10", + # "timestamp": 1696392515269, + # "current_fee": "0.0014250028", + # "open_timestamp": 1696392256998, + # "current_value": "27.4039", + # "mark_price": "27.4039", + # "position_value": "27.4079", + # "position_cross": "3.75723474", + # "maintenance_margin": "0.1370395", + # "close_vol": "0", + # "close_avg_price": "0", + # "open_avg_price": "27407.9", + # "entry_price": "27407.9", + # "current_amount": "1", + # "unrealized_value": "-0.004", + # "realized_value": "-0.01644474", + # "position_type": 1 + # } + # + marketId = self.safe_string(position, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + timestamp = self.safe_integer(position, 'timestamp') + side = self.safe_integer(position, 'position_type') + maintenanceMargin = self.safe_string(position, 'maintenance_margin') + notional = self.safe_string(position, 'current_value') + collateral = self.safe_string(position, 'position_cross') + maintenanceMarginPercentage = Precise.string_div(maintenanceMargin, notional) + marginRatio = Precise.string_div(maintenanceMargin, collateral) + return self.safe_position({ + 'info': position, + 'id': None, + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastUpdateTimestamp': None, + 'hedged': None, + 'side': 'long' if (side == 1) else 'short', + 'contracts': self.safe_number(position, 'current_amount'), + 'contractSize': self.safe_number(market, 'contractSize'), + 'entryPrice': self.safe_number(position, 'entry_price'), + 'markPrice': self.safe_number(position, 'mark_price'), + 'lastPrice': None, + 'notional': self.parse_number(notional), + 'leverage': self.safe_number(position, 'leverage'), + 'collateral': self.parse_number(collateral), + 'initialMargin': None, + 'initialMarginPercentage': None, + 'maintenanceMargin': self.parse_number(maintenanceMargin), + 'maintenanceMarginPercentage': self.parse_number(maintenanceMarginPercentage), + 'unrealizedPnl': self.safe_number(position, 'unrealized_value'), + 'realizedPnl': self.safe_number(position, 'realized_value'), + 'liquidationPrice': None, + 'marginMode': None, + 'percentage': None, + 'marginRatio': self.parse_number(marginRatio), + 'stopLossPrice': None, + 'takeProfitPrice': None, + }) + + def fetch_my_liquidations(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + retrieves the users liquidated positions + + https://developer-pro.bitmart.com/en/futuresv2/#get-order-history-keyed + + :param str symbol: unified CCXT market symbol + :param int [since]: the earliest time in ms to fetch liquidations for + :param int [limit]: the maximum number of liquidation structures to retrieve + :param dict [params]: exchange specific parameters for the bitmart api endpoint + :param int [params.until]: timestamp in ms of the latest liquidation + :returns dict: an array of `liquidation structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMyLiquidations() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + if not market['swap']: + raise NotSupported(self.id + ' fetchMyLiquidations() supports swap markets only') + request: dict = { + 'symbol': market['id'], + } + if since is not None: + request['start_time'] = since + request, params = self.handle_until_option('end_time', request, params) + response = self.privateGetContractPrivateOrderHistory(self.extend(request, params)) + # + # { + # "code": 1000, + # "message": "Ok", + # "data": [ + # { + # "order_id": "231007865458273", + # "client_order_id": "", + # "price": "27407.9", + # "size": "1", + # "symbol": "BTCUSDT", + # "state": 4, + # "side": 3, + # "type": "liquidate", + # "leverage": "10", + # "open_type": "isolated", + # "deal_avg_price": "27422.6", + # "deal_size": "1", + # "create_time": 1696405864011, + # "update_time": 1696405864045 + # }, + # ], + # "trace": "4cad855074664097ac6ba4257c47305d.71.16965658195443021" + # } + # + data = self.safe_list(response, 'data', []) + result = [] + for i in range(0, len(data)): + entry = data[i] + checkLiquidation = self.safe_string(entry, 'type') + if checkLiquidation == 'liquidate': + result.append(entry) + return self.parse_liquidations(result, market, since, limit) + + def parse_liquidation(self, liquidation, market: Market = None): + # + # { + # "order_id": "231007865458273", + # "client_order_id": "", + # "price": "27407.9", + # "size": "1", + # "symbol": "BTCUSDT", + # "state": 4, + # "side": 3, + # "type": "market", + # "leverage": "10", + # "open_type": "isolated", + # "deal_avg_price": "27422.6", + # "deal_size": "1", + # "create_time": 1696405864011, + # "update_time": 1696405864045 + # } + # + marketId = self.safe_string(liquidation, 'symbol') + timestamp = self.safe_integer(liquidation, 'update_time') + contractsString = self.safe_string(liquidation, 'deal_size') + contractSizeString = self.safe_string(market, 'contractSize') + priceString = self.safe_string(liquidation, 'deal_avg_price') + baseValueString = Precise.string_mul(contractsString, contractSizeString) + quoteValueString = Precise.string_mul(baseValueString, priceString) + return self.safe_liquidation({ + 'info': liquidation, + 'symbol': self.safe_symbol(marketId, market), + 'contracts': self.parse_number(contractsString), + 'contractSize': self.parse_number(contractSizeString), + 'price': self.parse_number(priceString), + 'baseValue': self.parse_number(baseValueString), + 'quoteValue': self.parse_number(quoteValueString), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + + def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}) -> Order: + """ + edits an open order + + https://developer-pro.bitmart.com/en/futuresv2/#modify-plan-order-signed + https://developer-pro.bitmart.com/en/futuresv2/#modify-tp-sl-order-signed + https://developer-pro.bitmart.com/en/futuresv2/#modify-preset-plan-order-signed + https://developer-pro.bitmart.com/en/futuresv2/#modify-limit-order-signed + + :param str id: order id + :param str symbol: unified symbol of the market to edit an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float [amount]: how much you want to trade in units of the base currency + :param float [price]: the price to fulfill the order, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.triggerPrice]: *swap only* the price to trigger a stop order + :param str [params.stopLossPrice]: *swap only* the price to trigger a stop-loss order + :param str [params.takeProfitPrice]: *swap only* the price to trigger a take-profit order + :param str [params.stopLoss.triggerPrice]: *swap only* the price to trigger a preset stop-loss order + :param str [params.takeProfit.triggerPrice]: *swap only* the price to trigger a preset take-profit order + :param str [params.clientOrderId]: client order id of the order + :param int [params.price_type]: *swap only* 1: last price, 2: fair price, default is 1 + :param int [params.plan_category]: *swap tp/sl only* 1: tp/sl, 2: position tp/sl, default is 1 + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + if not market['swap']: + raise NotSupported(self.id + ' editOrder() does not support ' + market['type'] + ' markets, only swap markets are supported') + stopLossPrice = self.safe_string(params, 'stopLossPrice') + takeProfitPrice = self.safe_string(params, 'takeProfitPrice') + triggerPrice = self.safe_string_n(params, ['triggerPrice', 'stopPrice', 'trigger_price']) + stopLoss = self.safe_dict(params, 'stopLoss', {}) + takeProfit = self.safe_dict(params, 'takeProfit', {}) + presetStopLoss = self.safe_string(stopLoss, 'triggerPrice') + presetTakeProfit = self.safe_string(takeProfit, 'triggerPrice') + isTriggerOrder = triggerPrice is not None + isStopLoss = stopLossPrice is not None + isTakeProfit = takeProfitPrice is not None + isPresetStopLoss = presetStopLoss is not None + isPresetTakeProfit = presetTakeProfit is not None + isLimitOrder = (type == 'limit') + request: dict = { + 'symbol': market['id'], + } + clientOrderId = self.safe_string(params, 'clientOrderId') + if clientOrderId is not None: + params = self.omit(params, 'clientOrderId') + request['client_order_id'] = clientOrderId + if id is not None: + request['order_id'] = id + params = self.omit(params, ['triggerPrice', 'stopPrice', 'stopLossPrice', 'takeProfitPrice', 'stopLoss', 'takeProfit']) + response = None + if isTriggerOrder or isStopLoss or isTakeProfit: + request['price_type'] = self.safe_integer(params, 'price_type', 1) + if price is not None: + request['executive_price'] = self.price_to_precision(symbol, price) + if isTriggerOrder: + request['type'] = type + request['trigger_price'] = self.price_to_precision(symbol, triggerPrice) + response = self.privatePostContractPrivateModifyPlanOrder(self.extend(request, params)) + # + # { + # "code": 1000, + # "message": "Ok", + # "data": { + # "order_id": "3000023150003503" + # }, + # "trace": "324523453245.108.1734567125596324575" + # } + # + elif isStopLoss or isTakeProfit: + request['category'] = type + if isStopLoss: + request['trigger_price'] = self.price_to_precision(symbol, stopLossPrice) + else: + request['trigger_price'] = self.price_to_precision(symbol, takeProfitPrice) + response = self.privatePostContractPrivateModifyTpSlOrder(self.extend(request, params)) + # + # { + # "code": 1000, + # "message": "Ok", + # "data": { + # "order_id": "3000023150003480" + # }, + # "trace": "23452345.104.1724536582682345459" + # } + # + elif isPresetStopLoss or isPresetTakeProfit: + if isPresetStopLoss: + request['preset_stop_loss_price_type'] = self.safe_integer(params, 'price_type', 1) + request['preset_stop_loss_price'] = self.price_to_precision(symbol, presetStopLoss) + else: + request['preset_take_profit_price_type'] = self.safe_integer(params, 'price_type', 1) + request['preset_take_profit_price'] = self.price_to_precision(symbol, presetTakeProfit) + response = self.privatePostContractPrivateModifyPresetPlanOrder(self.extend(request, params)) + # + # { + # "code": 1000, + # "message": "Ok", + # "data": { + # "order_id": "3000023150003496" + # }, + # "trace": "a5c3234534534a836bc476a203.123452.172716624359200197" + # } + # + elif isLimitOrder: + request['order_id'] = self.parse_to_int(id) # reparse id self endpoint is the only one requiring it + if amount is not None: + request['size'] = self.amount_to_precision(symbol, amount) + if price is not None: + request['price'] = self.price_to_precision(symbol, price) + response = self.privatePostContractPrivateModifyLimitOrder(self.extend(request, params)) + else: + raise NotSupported(self.id + ' editOrder() only supports limit, trigger, stop loss and take profit orders') + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data, market) + + def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + + https://developer-pro.bitmart.com/en/futuresv2/#get-transaction-history-keyed + + :param str [code]: unified currency code + :param int [since]: timestamp in ms of the earliest ledger entry + :param int [limit]: max number of ledger entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest ledger entry + :returns dict[]: a list of `ledger structures ` + """ + self.load_markets() + currency = None + if code is not None: + currency = self.currency(code) + request: dict = {} + request, params = self.handle_until_option('end_time', request, params) + transactionsRequest = self.fetch_transactions_request(0, None, since, limit, params) + response = self.privateGetContractPrivateTransactionHistory(transactionsRequest) + # + # { + # "code": 1000, + # "message": "Ok", + # "data": [ + # { + # "time": "1734422402121", + # "type": "Funding Fee", + # "amount": "-0.00008253", + # "asset": "USDT", + # "symbol": "LTCUSDT", + # "tran_id": "1734422402121", + # "flow_type": 3 + # }, + # ], + # "trace": "4cd11f83c71egfhfgh842790f07241e.23.173442343427772866" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_ledger(data, currency, since, limit) + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + # + # { + # "time": "1734422402121", + # "type": "Funding Fee", + # "amount": "-0.00008253", + # "asset": "USDT", + # "symbol": "LTCUSDT", + # "tran_id": "1734422402121", + # "flow_type": 3 + # } + # + amount = self.safe_string(item, 'amount') + direction = None + if Precise.string_le(amount, '0'): + direction = 'out' + amount = Precise.string_mul('-1', amount) + else: + direction = 'in' + currencyId = self.safe_string(item, 'asset') + currency = self.safe_currency(currencyId, currency) + timestamp = self.safe_integer(item, 'time') + type = self.safe_string(item, 'type') + return self.safe_ledger_entry({ + 'info': item, + 'id': self.safe_string(item, 'tran_id'), + 'direction': direction, + 'account': None, + 'referenceAccount': None, + 'referenceId': self.safe_string(item, 'tradeId'), + 'type': self.parse_ledger_entry_type(type), + 'currency': currency['code'], + 'amount': self.parse_number(amount), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'before': None, + 'after': None, + 'status': None, + 'fee': None, + }, currency) + + def parse_ledger_entry_type(self, type): + ledgerType: dict = { + 'Commission Fee': 'fee', + 'Funding Fee': 'fee', + 'Realized PNL': 'trade', + 'Transfer': 'transfer', + 'Liquidation Clearance': 'settlement', + } + return self.safe_string(ledgerType, type, type) + + def fetch_transactions_request(self, flowType: Int = None, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + request: dict = {} + if flowType is not None: + request['flow_type'] = flowType + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if since is not None: + request['start_time'] = since + if limit is not None: + request['page_size'] = limit + request, params = self.handle_until_option('end_time', request, params) + return self.extend(request, params) + + def fetch_funding_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[FundingHistory]: + """ + fetch the history of funding payments paid and received on self account + + https://developer-pro.bitmart.com/en/futuresv2/#get-transaction-history-keyed + + :param str [symbol]: unified market symbol + :param int [since]: the starting timestamp in milliseconds + :param int [limit]: the number of entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch funding history for + :returns dict[]: a list of `funding history structures ` + """ + self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + request: dict = {} + request, params = self.handle_until_option('end_time', request, params) + transactionsRequest = self.fetch_transactions_request(3, symbol, since, limit, params) + response = self.privateGetContractPrivateTransactionHistory(transactionsRequest) + # + # { + # "code": 1000, + # "message": "Ok", + # "data": [ + # { + # "time": "1734422402121", + # "type": "Funding Fee", + # "amount": "-0.00008253", + # "asset": "USDT", + # "symbol": "LTCUSDT", + # "tran_id": "1734422402121", + # "flow_type": 3 + # }, + # ], + # "trace": "4cd11f83c71egfhfgh842790f07241e.23.173442343427772866" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_funding_histories(data, market, since, limit) + + def parse_funding_history(self, contract, market: Market = None): + # + # { + # "time": "1734422402121", + # "type": "Funding Fee", + # "amount": "-0.00008253", + # "asset": "USDT", + # "symbol": "LTCUSDT", + # "tran_id": "1734422402121", + # "flow_type": 3 + # } + # + marketId = self.safe_string(contract, 'symbol') + currencyId = self.safe_string(contract, 'asset') + timestamp = self.safe_integer(contract, 'time') + return { + 'info': contract, + 'symbol': self.safe_symbol(marketId, market, None, 'swap'), + 'code': self.safe_currency_code(currencyId), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': self.safe_string(contract, 'tran_id'), + 'amount': self.safe_number(contract, 'amount'), + } + + def parse_funding_histories(self, contracts, market=None, since: Int = None, limit: Int = None) -> List[FundingHistory]: + result = [] + for i in range(0, len(contracts)): + contract = contracts[i] + result.append(self.parse_funding_history(contract, market)) + sorted = self.sort_by(result, 'timestamp') + return self.filter_by_since_limit(sorted, since, limit) + + def fetch_withdraw_addresses(self, code: str, note=None, networkCode=None, params={}): + self.load_markets() + codes = None + if code is not None: + currency = self.currency(code) + code = currency['code'] + codes = [code] + response = self.privateGetAccountV1WithdrawAddressList(params) + # + # { + # "message": "OK", + # "code": 1000, + # "trace": "0e6edd79-f77f-4251-abe5-83ba75d06c1a", + # "data": { + # "list": [ + # { + # "currency": "ETH", + # "network": "ETH", + # "address": "0x1121", + # "memo": "12", + # "remark": "12", + # "addressType": 0, + # "verifyStatus": 0 + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + list = self.safe_list(data, 'list', []) + allAddresses = self.parse_deposit_addresses(list, codes, False) + addresses = [] + for i in range(0, len(allAddresses)): + address = allAddresses[i] + noteMatch = (note is None) or (address['note'] == note) + networkMatch = (networkCode is None) or (address['network'] == networkCode) + if noteMatch and networkMatch: + addresses.append(address) + return addresses + + def set_position_mode(self, hedged: bool, symbol: Str = None, params={}): + """ + set hedged to True or False for a market + + https://developer-pro.bitmart.com/en/futuresv2/#submit-leverage-signed + + :param bool hedged: set to True to use dualSidePosition + :param str symbol: not used by bingx setPositionMode() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + self.load_markets() + positionMode = None + if hedged: + positionMode = 'hedge_mode' + else: + positionMode = 'one_way_mode' + request: dict = { + 'position_mode': positionMode, + } + # + # { + # "code": 1000, + # "trace": "0cc6f4c4-8b8c-4253-8e90-8d3195aa109c", + # "message": "Ok", + # "data": { + # "position_mode":"one_way_mode" + # } + # } + # + return self.privatePostContractPrivateSetPositionMode(self.extend(request, params)) + + def fetch_position_mode(self, symbol: Str = None, params={}): + """ + fetchs the position mode, hedged or one way, hedged for binance is set identically for all linear markets or all inverse markets + + https://developer-pro.bitmart.com/en/futuresv2/#get-position-mode-keyed + + :param str symbol: not used + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an object detailing whether the market is in hedged or one-way mode + """ + response = self.privateGetContractPrivateGetPositionMode(params) + # + # { + # "code": 1000, + # "trace": "0cc6f4c4-8b8c-4253-8e90-8d3195aa109c", + # "message": "Ok", + # "data": { + # "position_mode":"one_way_mode" + # } + # } + # + data = self.safe_dict(response, 'data') + positionMode = self.safe_string(data, 'position_mode') + return { + 'info': response, + 'hedged': (positionMode == 'hedge_mode'), + } + + def nonce(self): + return self.milliseconds() - self.options['timeDifference'] + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + parts = path.split('/') + # to do: refactor api endpoints with spot/swap sections + category = self.safe_string(parts, 0, 'spot') + market = 'spot' if (category == 'spot' or category == 'account') else 'swap' + baseUrl = self.implode_hostname(self.urls['api'][market]) + url = baseUrl + '/' + self.implode_params(path, params) + query = self.omit(params, self.extract_params(path)) + queryString = '' + getOrDelete = (method == 'GET') or (method == 'DELETE') + if getOrDelete: + if query: + queryString = self.urlencode(query) + url += '?' + queryString + if api == 'private': + self.check_required_credentials() + timestamp = str(self.nonce()) + brokerId = self.safe_string(self.options, 'brokerId', 'CCXTxBitmart000') + headers = { + 'X-BM-KEY': self.apiKey, + 'X-BM-TIMESTAMP': timestamp, + 'X-BM-BROKER-ID': brokerId, + 'Content-Type': 'application/json', + } + if not getOrDelete: + body = self.json(query) + queryString = body + auth = timestamp + '#' + self.uid + '#' + queryString + signature = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256) + headers['X-BM-SIGN'] = signature + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None + # + # spot + # + # {"message":"Bad Request [to is empty]","code":50000,"trace":"f9d46e1b-4edb-4d07-a06e-4895fb2fc8fc","data":{}} + # {"message":"Bad Request [from is empty]","code":50000,"trace":"579986f7-c93a-4559-926b-06ba9fa79d76","data":{}} + # {"message":"Kline size over 500","code":50004,"trace":"d625caa8-e8ca-4bd2-b77c-958776965819","data":{}} + # {"message":"Balance not enough","code":50020,"trace":"7c709d6a-3292-462c-98c5-32362540aeef","data":{}} + # {"code":40012,"message":"You contract account available balance not enough.","trace":"..."} + # + # contract + # + # {"errno":"OK","message":"INVALID_PARAMETER","code":49998,"trace":"eb5ebb54-23cd-4de2-9064-e090b6c3b2e3","data":null} + # + message = self.safe_string_lower(response, 'message') + isErrorMessage = (message is not None) and (message != 'ok') and (message != 'success') + errorCode = self.safe_string(response, 'code') + isErrorCode = (errorCode is not None) and (errorCode != '1000') + if isErrorCode or isErrorMessage: + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], errorCode, feedback) + raise ExchangeError(feedback) # unknown message + return None diff --git a/ccxt/bitmex.py b/ccxt/bitmex.py new file mode 100644 index 0000000..5bb2648 --- /dev/null +++ b/ccxt/bitmex.py @@ -0,0 +1,2941 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.bitmex import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currencies, Currency, DepositAddress, Int, LedgerEntry, Leverage, Leverages, Market, MarketType, Num, Order, OrderBook, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, FundingRates, Trade, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import DDoSProtection +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class bitmex(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(bitmex, self).describe(), { + 'id': 'bitmex', + 'name': 'BitMEX', + 'countries': ['SC'], # Seychelles + 'version': 'v1', + 'userAgent': None, + # cheapest endpoints are 10 requests per second(trading) + # 10 per second => rateLimit = 1000ms / 10 = 100ms + # 120 per minute => 2 per second => weight = 5(authenticated) + # 30 per minute => 0.5 per second => weight = 20(unauthenticated) + 'rateLimit': 100, + 'certified': True, + 'pro': True, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': True, + 'future': True, + 'option': False, + 'addMargin': None, + 'cancelAllOrders': True, + 'cancelAllOrdersAfter': True, + 'cancelOrder': True, + 'cancelOrders': True, + 'closeAllPositions': False, + 'closePosition': True, + 'createOrder': True, + 'createReduceOnlyOrder': True, + 'createStopOrder': True, + 'createTrailingAmountOrder': True, + 'createTriggerOrder': True, + 'editOrder': True, + 'fetchBalance': True, + 'fetchClosedOrders': True, + 'fetchCurrencies': True, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDepositsWithdrawals': 'emulated', + 'fetchDepositWithdrawFee': 'emulated', + 'fetchDepositWithdrawFees': True, + 'fetchFundingHistory': False, + 'fetchFundingRate': 'emulated', # emulated in exchange + 'fetchFundingRateHistory': True, + 'fetchFundingRates': True, + 'fetchIndexOHLCV': False, + 'fetchLedger': True, + 'fetchLeverage': 'emulated', + 'fetchLeverages': True, + 'fetchLeverageTiers': False, + 'fetchLiquidations': True, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMyLiquidations': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositions': True, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTrades': True, + 'fetchTransactions': 'emulated', + 'fetchTransfer': False, + 'fetchTransfers': False, + 'index': True, + 'reduceMargin': None, + 'sandbox': True, + 'setLeverage': True, + 'setMargin': None, + 'setMarginMode': True, + 'setPositionMode': False, + 'transfer': False, + 'withdraw': True, + }, + 'timeframes': { + '1m': '1m', + '5m': '5m', + '1h': '1h', + '1d': '1d', + }, + 'urls': { + 'test': { + 'public': 'https://testnet.bitmex.com', + 'private': 'https://testnet.bitmex.com', + }, + 'logo': 'https://github.com/user-attachments/assets/c78425ab-78d5-49d6-bd14-db7734798f04', + 'api': { + 'public': 'https://www.bitmex.com', + 'private': 'https://www.bitmex.com', + }, + 'www': 'https://www.bitmex.com', + 'doc': [ + 'https://www.bitmex.com/app/apiOverview', + 'https://github.com/BitMEX/api-connectors/tree/master/official-http', + ], + 'fees': 'https://www.bitmex.com/app/fees', + 'referral': { + 'url': 'https://www.bitmex.com/app/register/NZTR1q', + 'discount': 0.1, + }, + }, + 'api': { + 'public': { + 'get': { + 'announcement': 5, + 'announcement/urgent': 5, + 'chat': 5, + 'chat/channels': 5, + 'chat/connected': 5, + 'chat/pinned': 5, + 'funding': 5, + 'guild': 5, + 'instrument': 5, + 'instrument/active': 5, + 'instrument/activeAndIndices': 5, + 'instrument/activeIntervals': 5, + 'instrument/compositeIndex': 5, + 'instrument/indices': 5, + 'instrument/usdVolume': 5, + 'insurance': 5, + 'leaderboard': 5, + 'liquidation': 5, + 'orderBook/L2': 5, + 'porl/nonce': 5, + 'quote': 5, + 'quote/bucketed': 5, + 'schema': 5, + 'schema/websocketHelp': 5, + 'settlement': 5, + 'stats': 5, + 'stats/history': 5, + 'stats/historyUSD': 5, + 'trade': 5, + 'trade/bucketed': 5, + 'wallet/assets': 5, + 'wallet/networks': 5, + }, + }, + 'private': { + 'get': { + 'address': 5, + 'apiKey': 5, + 'execution': 5, + 'execution/tradeHistory': 5, + 'globalNotification': 5, + 'leaderboard/name': 5, + 'order': 5, + 'porl/snapshots': 5, + 'position': 5, + 'user': 5, + 'user/affiliateStatus': 5, + 'user/checkReferralCode': 5, + 'user/commission': 5, + 'user/csa': 5, + 'user/depositAddress': 5, + 'user/executionHistory': 5, + 'user/getWalletTransferAccounts': 5, + 'user/margin': 5, + 'user/quoteFillRatio': 5, + 'user/quoteValueRatio': 5, + 'user/staking': 5, + 'user/staking/instruments': 5, + 'user/staking/tiers': 5, + 'user/tradingVolume': 5, + 'user/unstakingRequests': 5, + 'user/wallet': 5, + 'user/walletHistory': 5, + 'user/walletSummary': 5, + 'userAffiliates': 5, + 'userEvent': 5, + }, + 'post': { + 'address': 5, + 'chat': 5, + 'guild': 5, + 'guild/archive': 5, + 'guild/join': 5, + 'guild/kick': 5, + 'guild/leave': 5, + 'guild/sharesTrades': 5, + 'order': 1, + 'order/cancelAllAfter': 5, + 'order/closePosition': 5, + 'position/isolate': 1, + 'position/leverage': 1, + 'position/riskLimit': 5, + 'position/transferMargin': 1, + 'user/addSubaccount': 5, + 'user/cancelWithdrawal': 5, + 'user/communicationToken': 5, + 'user/confirmEmail': 5, + 'user/confirmWithdrawal': 5, + 'user/logout': 5, + 'user/preferences': 5, + 'user/requestWithdrawal': 5, + 'user/unstakingRequests': 5, + 'user/updateSubaccount': 5, + 'user/walletTransfer': 5, + }, + 'put': { + 'guild': 5, + 'order': 1, + }, + 'delete': { + 'order': 1, + 'order/all': 1, + 'user/unstakingRequests': 5, + }, + }, + }, + 'exceptions': { + 'exact': { + 'Invalid API Key.': AuthenticationError, + 'This key is disabled.': PermissionDenied, + 'Access Denied': PermissionDenied, + 'Duplicate clOrdID': InvalidOrder, + 'orderQty is invalid': InvalidOrder, + 'Invalid price': InvalidOrder, + 'Invalid stopPx for ordType': InvalidOrder, + 'Account is restricted': PermissionDenied, # {"error":{"message":"Account is restricted","name":"HTTPError"}} + }, + 'broad': { + 'Signature not valid': AuthenticationError, + 'overloaded': ExchangeNotAvailable, + 'Account has insufficient Available Balance': InsufficientFunds, + 'Service unavailable': ExchangeNotAvailable, # {"error":{"message":"Service unavailable","name":"HTTPError"}} + 'Server Error': ExchangeError, # {"error":{"message":"Server Error","name":"HTTPError"}} + 'Unable to cancel order due to existing state': InvalidOrder, + 'We require all new traders to verify': PermissionDenied, # {"message":"We require all new traders to verify their identity before their first deposit. Please visit bitmex.com/verify to complete the process.","name":"HTTPError"} + }, + }, + 'precisionMode': TICK_SIZE, + 'options': { + # https://blog.bitmex.com/api_announcement/deprecation-of-api-nonce-header/ + # https://github.com/ccxt/ccxt/issues/4789 + 'api-expires': 5, # in seconds + 'fetchOHLCVOpenTimestamp': True, + 'oldPrecision': False, + 'networks': { + 'BTC': 'btc', + 'ERC20': 'eth', + 'BEP20': 'bsc', + 'TRC20': 'tron', + 'AVAXC': 'avax', + 'NEAR': 'near', + 'XTZ': 'xtz', + 'DOT': 'dot', + 'SOL': 'sol', + 'ADA': 'ada', + }, + }, + 'features': { + 'default': { + 'sandbox': True, + 'createOrder': { + 'marginMode': True, + 'triggerPrice': True, + 'triggerPriceType': { + 'last': True, + 'mark': True, + }, + 'triggerDirection': True, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': True, + 'marketBuyRequiresPrice': False, + 'marketBuyByCost': False, + # exchange-supported features + # 'selfTradePrevention': True, + # 'twap': False, + # 'iceberg': False, + # 'oco': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 500, + 'daysBack': None, + 'untilDays': 1000000, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 500, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': 500, + 'daysBack': None, + 'untilDays': 1000000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 500, + 'daysBack': None, + 'daysBackCanceled': None, + 'untilDays': 1000000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 10000, + }, + }, + 'spot': { + 'extends': 'default', + 'createOrder': { + 'triggerPriceType': { + 'index': False, + }, + }, + }, + 'derivatives': { + 'extends': 'default', + 'createOrder': { + 'triggerPriceType': { + 'index': True, + }, + }, + }, + 'swap': { + 'linear': { + 'extends': 'derivatives', + }, + 'inverse': { + 'extends': 'derivatives', + }, + }, + 'future': { + 'linear': { + 'extends': 'derivatives', + }, + 'inverse': { + 'extends': 'derivatives', + }, + }, + }, + 'commonCurrencies': { + 'USDt': 'USDT', + 'XBt': 'BTC', + 'XBT': 'BTC', + 'Gwei': 'ETH', + 'GWEI': 'ETH', + 'LAMP': 'SOL', + 'LAMp': 'SOL', + }, + }) + + def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://www.bitmex.com/api/explorer/#not /Wallet/Wallet_getAssetsConfig + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = self.publicGetWalletAssets(params) + # + # { + # "XBt": { + # "asset": "XBT", + # "currency": "XBt", + # "majorCurrency": "XBT", + # "name": "Bitcoin", + # "currencyType": "Crypto", + # "scale": "8", + # # "mediumPrecision": "8", + # # "shorterPrecision": "4", + # # "symbol": "₿", + # # "tickLog": "0", + # # "weight": "1", + # "enabled": True, + # "isMarginCurrency": True, + # "minDepositAmount": "10000", + # "minWithdrawalAmount": "1000", + # "maxWithdrawalAmount": "100000000000000", + # "networks": [ + # { + # "asset": "btc", + # "tokenAddress": "", + # "depositEnabled": True, + # "withdrawalEnabled": True, + # "withdrawalFee": "20000", + # "minFee": "20000", + # "maxFee": "10000000" + # } + # ] + # }, + # } + # + result: dict = {} + for i in range(0, len(response)): + currency = response[i] + asset = self.safe_string(currency, 'asset') + code = self.safe_currency_code(asset) + id = self.safe_string(currency, 'currency') + name = self.safe_string(currency, 'name') + chains = self.safe_value(currency, 'networks', []) + depositEnabled = False + withdrawEnabled = False + networks: dict = {} + scale = self.safe_string(currency, 'scale') + precisionString = self.parse_precision(scale) + precision = self.parse_number(precisionString) + for j in range(0, len(chains)): + chain = chains[j] + networkId = self.safe_string(chain, 'asset') + network = self.network_id_to_code(networkId) + withdrawalFeeRaw = self.safe_string(chain, 'withdrawalFee') + withdrawalFee = self.parse_number(Precise.string_mul(withdrawalFeeRaw, precisionString)) + isDepositEnabled = self.safe_bool(chain, 'depositEnabled', False) + isWithdrawEnabled = self.safe_bool(chain, 'withdrawalEnabled', False) + active = (isDepositEnabled and isWithdrawEnabled) + if isDepositEnabled: + depositEnabled = True + if isWithdrawEnabled: + withdrawEnabled = True + networks[network] = { + 'info': chain, + 'id': networkId, + 'network': network, + 'active': active, + 'deposit': isDepositEnabled, + 'withdraw': isWithdrawEnabled, + 'fee': withdrawalFee, + 'precision': None, + 'limits': { + 'withdraw': { + 'min': None, + 'max': None, + }, + 'deposit': { + 'min': None, + 'max': None, + }, + }, + } + currencyEnabled = self.safe_value(currency, 'enabled') + currencyActive = currencyEnabled or (depositEnabled or withdrawEnabled) + minWithdrawalString = self.safe_string(currency, 'minWithdrawalAmount') + minWithdrawal = self.parse_number(Precise.string_mul(minWithdrawalString, precisionString)) + maxWithdrawalString = self.safe_string(currency, 'maxWithdrawalAmount') + maxWithdrawal = self.parse_number(Precise.string_mul(maxWithdrawalString, precisionString)) + minDepositString = self.safe_string(currency, 'minDepositAmount') + minDeposit = self.parse_number(Precise.string_mul(minDepositString, precisionString)) + isCrypto = self.safe_string(currency, 'currencyType') == 'Crypto' + result[code] = { + 'id': id, + 'code': code, + 'info': currency, + 'name': name, + 'active': currencyActive, + 'deposit': depositEnabled, + 'withdraw': withdrawEnabled, + 'fee': None, + 'precision': precision, + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': minWithdrawal, + 'max': maxWithdrawal, + }, + 'deposit': { + 'min': minDeposit, + 'max': None, + }, + }, + 'networks': networks, + 'type': 'crypto' if isCrypto else 'other', + } + return result + + def convert_from_real_amount(self, code, amount): + currency = self.currency(code) + precision = self.safe_string(currency, 'precision') + amountString = self.number_to_string(amount) + finalAmount = Precise.string_div(amountString, precision) + return self.parse_number(finalAmount) + + def convert_to_real_amount(self, code: Str, amount: Str): + if code is None: + return amount + elif amount is None: + return None + currency = self.currency(code) + precision = self.safe_string(currency, 'precision') + return Precise.string_mul(amount, precision) + + def amount_to_precision(self, symbol, amount): + symbol = self.safe_symbol(symbol) + market = self.market(symbol) + oldPrecision = self.safe_value(self.options, 'oldPrecision') + if market['spot'] and not oldPrecision: + amount = self.convert_from_real_amount(market['base'], amount) + return super(bitmex, self).amount_to_precision(symbol, amount) + + def convert_from_raw_quantity(self, symbol, rawQuantity, currencySide='base'): + if self.safe_value(self.options, 'oldPrecision'): + return self.parse_number(rawQuantity) + symbol = self.safe_symbol(symbol) + marketExists = self.in_array(symbol, self.symbols) + if not marketExists: + return self.parse_number(rawQuantity) + market = self.market(symbol) + if market['spot']: + return self.parse_number(self.convert_to_real_amount(market[currencySide], rawQuantity)) + return self.parse_number(rawQuantity) + + def convert_from_raw_cost(self, symbol, rawQuantity): + return self.convert_from_raw_quantity(symbol, rawQuantity, 'quote') + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for bitmex + + https://www.bitmex.com/api/explorer/#not /Instrument/Instrument_getActive + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = self.publicGetInstrumentActive(params) + # + # [ + # { + # "symbol": "LTCUSDT", + # "rootSymbol": "LTC", + # "state": "Open", + # "typ": "FFWCSX", + # "listing": "2021-11-10T04:00:00.000Z", + # "front": "2021-11-10T04:00:00.000Z", + # "expiry": null, + # "settle": null, + # "listedSettle": null, + # "relistInterval": null, + # "inverseLeg": "", + # "sellLeg": "", + # "buyLeg": "", + # "optionStrikePcnt": null, + # "optionStrikeRound": null, + # "optionStrikePrice": null, + # "optionMultiplier": null, + # "positionCurrency": "LTC", # can be empty for spot markets + # "underlying": "LTC", + # "quoteCurrency": "USDT", + # "underlyingSymbol": "LTCT=", # can be empty for spot markets + # "reference": "BMEX", + # "referenceSymbol": ".BLTCT", # can be empty for spot markets + # "calcInterval": null, + # "publishInterval": null, + # "publishTime": null, + # "maxOrderQty": 1000000000, + # "maxPrice": 1000000, + # "lotSize": 1000, + # "tickSize": 0.01, + # "multiplier": 100, + # "settlCurrency": "USDt", # can be empty for spot markets + # "underlyingToPositionMultiplier": 10000, + # "underlyingToSettleMultiplier": null, + # "quoteToSettleMultiplier": 1000000, + # "isQuanto": False, + # "isInverse": False, + # "initMargin": 0.03, + # "maintMargin": 0.015, + # "riskLimit": 1000000000000, # can be null for spot markets + # "riskStep": 1000000000000, # can be null for spot markets + # "limit": null, + # "capped": False, + # "taxed": True, + # "deleverage": True, + # "makerFee": -0.0001, + # "takerFee": 0.0005, + # "settlementFee": 0, + # "insuranceFee": 0, + # "fundingBaseSymbol": ".LTCBON8H", # can be empty for spot markets + # "fundingQuoteSymbol": ".USDTBON8H", # can be empty for spot markets + # "fundingPremiumSymbol": ".LTCUSDTPI8H", # can be empty for spot markets + # "fundingTimestamp": "2022-01-14T20:00:00.000Z", + # "fundingInterval": "2000-01-01T08:00:00.000Z", + # "fundingRate": 0.0001, + # "indicativeFundingRate": 0.0001, + # "rebalanceTimestamp": null, + # "rebalanceInterval": null, + # "openingTimestamp": "2022-01-14T17:00:00.000Z", + # "closingTimestamp": "2022-01-14T18:00:00.000Z", + # "sessionInterval": "2000-01-01T01:00:00.000Z", + # "prevClosePrice": 138.511, + # "limitDownPrice": null, + # "limitUpPrice": null, + # "bankruptLimitDownPrice": null, + # "bankruptLimitUpPrice": null, + # "prevTotalVolume": 12699024000, + # "totalVolume": 12702160000, + # "volume": 3136000, + # "volume24h": 114251000, + # "prevTotalTurnover": 232418052349000, + # "totalTurnover": 232463353260000, + # "turnover": 45300911000, + # "turnover24h": 1604331340000, + # "homeNotional24h": 11425.1, + # "foreignNotional24h": 1604331.3400000003, + # "prevPrice24h": 135.48, + # "vwap": 140.42165, + # "highPrice": 146.42, + # "lowPrice": 135.08, + # "lastPrice": 144.36, + # "lastPriceProtected": 144.36, + # "lastTickDirection": "MinusTick", + # "lastChangePcnt": 0.0655, + # "bidPrice": 143.75, + # "midPrice": 143.855, + # "askPrice": 143.96, + # "impactBidPrice": 143.75, + # "impactMidPrice": 143.855, + # "impactAskPrice": 143.96, + # "hasLiquidity": True, + # "openInterest": 38103000, + # "openValue": 547963053300, + # "fairMethod": "FundingRate", + # "fairBasisRate": 0.1095, + # "fairBasis": 0.004, + # "fairPrice": 143.811, + # "markMethod": "FairPrice", + # "markPrice": 143.811, + # "indicativeTaxRate": null, + # "indicativeSettlePrice": 143.807, + # "optionUnderlyingPrice": null, + # "settledPriceAdjustmentRate": null, + # "settledPrice": null, + # "timestamp": "2022-01-14T17:49:55.000Z" + # } + # ] + # + return self.parse_markets(response) + + def parse_market(self, market: dict) -> Market: + id = self.safe_string(market, 'symbol') + baseId = self.safe_string(market, 'underlying') + quoteId = self.safe_string(market, 'quoteCurrency') + settleId = self.safe_string(market, 'settlCurrency') + settle = self.safe_currency_code(settleId) + # 'positionCurrency' may be empty("", currently returns for ETHUSD) + # so let's take the settlCurrency first and then adjust if needed + typ = self.safe_string(market, 'typ') # type definitions at: https://www.bitmex.com/api/explorer/#not /Instrument/Instrument_get + type: MarketType + swap = False + spot = False + future = False + if typ == 'FFWCSX': + type = 'swap' + swap = True + elif typ == 'IFXXXP': + type = 'spot' + spot = True + elif typ == 'FFCCSX': + type = 'future' + future = True + elif typ == 'FFICSX': + # prediction markets(without any volume) + quoteId = baseId + baseId = self.safe_string(market, 'rootSymbol') + type = 'future' + future = True + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + contract = swap or future + contractSize = None + isInverse = self.safe_value(market, 'isInverse') # self is True when BASE and SETTLE are same, i.e. BTC/XXX:BTC + isQuanto = self.safe_value(market, 'isQuanto') # self is True when BASE and SETTLE are different, i.e. AXS/XXX:BTC + linear = (not isInverse and not isQuanto) if contract else None + status = self.safe_string(market, 'state') + active = status == 'Open' # Open, Settled, Unlisted + expiry = None + expiryDatetime = None + symbol = None + if spot: + symbol = base + '/' + quote + elif contract: + symbol = base + '/' + quote + ':' + settle + if linear: + multiplierString = self.safe_string_2(market, 'underlyingToPositionMultiplier', 'underlyingToSettleMultiplier') + contractSize = self.parse_number(Precise.string_div('1', multiplierString)) + else: + multiplierString = Precise.string_abs(self.safe_string(market, 'multiplier')) + contractSize = self.parse_number(multiplierString) + expiryDatetime = self.safe_string(market, 'expiry') + expiry = self.parse8601(expiryDatetime) + if expiry is not None: + symbol = symbol + '-' + self.yymmdd(expiry) + else: + # for index/exotic markets, default to id + symbol = id + positionId = self.safe_string_2(market, 'positionCurrency', 'underlying') + position = self.safe_currency_code(positionId) + positionIsQuote = (position == quote) + maxOrderQty = self.safe_number(market, 'maxOrderQty') + initMargin = self.safe_string(market, 'initMargin', '1') + maxLeverage = self.parse_number(Precise.string_div('1', initMargin)) + # subtype should be None for spot markets + if spot: + isInverse = None + isQuanto = None + linear = None + return { + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': type, + 'spot': spot, + 'margin': False, + 'swap': swap, + 'future': future, + 'option': False, + 'active': active, + 'contract': contract, + 'linear': linear, + 'inverse': isInverse, + 'quanto': isQuanto, + 'taker': self.safe_number(market, 'takerFee'), + 'maker': self.safe_number(market, 'makerFee'), + 'contractSize': contractSize, + 'expiry': expiry, + 'expiryDatetime': expiryDatetime, + 'strike': self.safe_number(market, 'optionStrikePrice'), + 'optionType': None, + 'precision': { + 'amount': self.safe_number(market, 'lotSize'), + 'price': self.safe_number(market, 'tickSize'), + }, + 'limits': { + 'leverage': { + 'min': self.parse_number('1') if contract else None, + 'max': maxLeverage if contract else None, + }, + 'amount': { + 'min': None, + 'max': None if positionIsQuote else maxOrderQty, + }, + 'price': { + 'min': None, + 'max': self.safe_number(market, 'maxPrice'), + }, + 'cost': { + 'min': None, + 'max': maxOrderQty if positionIsQuote else None, + }, + }, + 'created': None, # 'listing' field is buggy, e.g. 2200-02-01T00:00:00.000Z + 'info': market, + } + + def parse_balance(self, response) -> Balances: + # + # [ + # { + # "account":1455728, + # "currency":"XBt", + # "riskLimit":1000000000000, + # "prevState":"", + # "state":"", + # "action":"", + # "amount":263542, + # "pendingCredit":0, + # "pendingDebit":0, + # "confirmedDebit":0, + # "prevRealisedPnl":0, + # "prevUnrealisedPnl":0, + # "grossComm":0, + # "grossOpenCost":0, + # "grossOpenPremium":0, + # "grossExecCost":0, + # "grossMarkValue":0, + # "riskValue":0, + # "taxableMargin":0, + # "initMargin":0, + # "maintMargin":0, + # "sessionMargin":0, + # "targetExcessMargin":0, + # "varMargin":0, + # "realisedPnl":0, + # "unrealisedPnl":0, + # "indicativeTax":0, + # "unrealisedProfit":0, + # "syntheticMargin":null, + # "walletBalance":263542, + # "marginBalance":263542, + # "marginBalancePcnt":1, + # "marginLeverage":0, + # "marginUsedPcnt":0, + # "excessMargin":263542, + # "excessMarginPcnt":1, + # "availableMargin":263542, + # "withdrawableMargin":263542, + # "timestamp":"2020-08-03T12:01:01.246Z", + # "grossLastValue":0, + # "commission":null + # } + # ] + # + result: dict = {'info': response} + for i in range(0, len(response)): + balance = response[i] + currencyId = self.safe_string(balance, 'currency') + code = self.safe_currency_code(currencyId) + account = self.account() + free = self.safe_string(balance, 'availableMargin') + total = self.safe_string(balance, 'marginBalance') + account['free'] = self.convert_to_real_amount(code, free) + account['total'] = self.convert_to_real_amount(code, total) + result[code] = account + return self.safe_balance(result) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://www.bitmex.com/api/explorer/#not /User/User_getMargin + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + request: dict = { + 'currency': 'all', + } + response = self.privateGetUserMargin(self.extend(request, params)) + # + # [ + # { + # "account":1455728, + # "currency":"XBt", + # "riskLimit":1000000000000, + # "prevState":"", + # "state":"", + # "action":"", + # "amount":263542, + # "pendingCredit":0, + # "pendingDebit":0, + # "confirmedDebit":0, + # "prevRealisedPnl":0, + # "prevUnrealisedPnl":0, + # "grossComm":0, + # "grossOpenCost":0, + # "grossOpenPremium":0, + # "grossExecCost":0, + # "grossMarkValue":0, + # "riskValue":0, + # "taxableMargin":0, + # "initMargin":0, + # "maintMargin":0, + # "sessionMargin":0, + # "targetExcessMargin":0, + # "varMargin":0, + # "realisedPnl":0, + # "unrealisedPnl":0, + # "indicativeTax":0, + # "unrealisedProfit":0, + # "syntheticMargin":null, + # "walletBalance":263542, + # "marginBalance":263542, + # "marginBalancePcnt":1, + # "marginLeverage":0, + # "marginUsedPcnt":0, + # "excessMargin":263542, + # "excessMarginPcnt":1, + # "availableMargin":263542, + # "withdrawableMargin":263542, + # "timestamp":"2020-08-03T12:01:01.246Z", + # "grossLastValue":0, + # "commission":null + # } + # ] + # + return self.parse_balance(response) + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://www.bitmex.com/api/explorer/#not /OrderBook/OrderBook_getL2 + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['depth'] = limit + response = self.publicGetOrderBookL2(self.extend(request, params)) + result: dict = { + 'symbol': symbol, + 'bids': [], + 'asks': [], + 'timestamp': None, + 'datetime': None, + 'nonce': None, + } + for i in range(0, len(response)): + order = response[i] + side = 'asks' if (order['side'] == 'Sell') else 'bids' + amount = self.convert_from_raw_quantity(symbol, self.safe_string(order, 'size')) + price = self.safe_number(order, 'price') + # https://github.com/ccxt/ccxt/issues/4926 + # https://github.com/ccxt/ccxt/issues/4927 + # the exchange sometimes returns null price in the orderbook + if price is not None: + resultSide = result[side] + resultSide.append([price, amount]) + result['bids'] = self.sort_by(result['bids'], 0, True) + result['asks'] = self.sort_by(result['asks'], 0) + return result + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://www.bitmex.com/api/explorer/#not /Order/Order_getOrders + + :param str id: the order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + filter: dict = { + 'filter': { + 'orderID': id, + }, + } + response = self.fetch_orders(symbol, None, None, self.deep_extend(filter, params)) + numResults = len(response) + if numResults == 1: + return response[0] + raise OrderNotFound(self.id + ': The order ' + id + ' not found.') + + def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + + https://www.bitmex.com/api/explorer/#not /Order/Order_getOrders + + fetches information on multiple orders made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the earliest time in ms to fetch orders for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOrders', 'paginate') + if paginate: + return self.fetch_paginated_call_dynamic('fetchOrders', symbol, since, limit, params, 100) + market = None + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if since is not None: + request['startTime'] = self.iso8601(since) + if limit is not None: + request['count'] = limit + until = self.safe_integer_2(params, 'until', 'endTime') + if until is not None: + params = self.omit(params, ['until']) + request['endTime'] = self.iso8601(until) + request = self.deep_extend(request, params) + # why the hassle? urlencode in python is kinda broken for nested dicts. + # E.g. self.urlencode({"filter": {"open": True}}) will return "filter={'open':+True}" + # Bitmex doesn't like that. Hence resorting to self hack. + if 'filter' in request: + request['filter'] = self.json(request['filter']) + response = self.privateGetOrder(request) + return self.parse_orders(response, market, since, limit) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://www.bitmex.com/api/explorer/#not /Order/Order_getOrders + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + request: dict = { + 'filter': { + 'open': True, + }, + } + return self.fetch_orders(symbol, since, limit, self.deep_extend(request, params)) + + def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://www.bitmex.com/api/explorer/#not /Order/Order_getOrders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + # Bitmex barfs if you set 'open': False in the filter... + orders = self.fetch_orders(symbol, since, limit, params) + return self.filter_by_array(orders, 'status', ['closed', 'canceled'], False) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://www.bitmex.com/api/explorer/#not /Execution/Execution_getTradeHistory + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchMyTrades', 'paginate') + if paginate: + return self.fetch_paginated_call_dynamic('fetchMyTrades', symbol, since, limit, params, 100) + market = None + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if since is not None: + request['startTime'] = self.iso8601(since) + if limit is not None: + request['count'] = min(500, limit) + until = self.safe_integer_2(params, 'until', 'endTime') + if until is not None: + params = self.omit(params, ['until']) + request['endTime'] = self.iso8601(until) + request = self.deep_extend(request, params) + # why the hassle? urlencode in python is kinda broken for nested dicts. + # E.g. self.urlencode({"filter": {"open": True}}) will return "filter={'open':+True}" + # Bitmex doesn't like that. Hence resorting to self hack. + if 'filter' in request: + request['filter'] = self.json(request['filter']) + response = self.privateGetExecutionTradeHistory(request) + # + # [ + # { + # "execID": "string", + # "orderID": "string", + # "clOrdID": "string", + # "clOrdLinkID": "string", + # "account": 0, + # "symbol": "string", + # "side": "string", + # "lastQty": 0, + # "lastPx": 0, + # "underlyingLastPx": 0, + # "lastMkt": "string", + # "lastLiquidityInd": "string", + # "simpleOrderQty": 0, + # "orderQty": 0, + # "price": 0, + # "displayQty": 0, + # "stopPx": 0, + # "pegOffsetValue": 0, + # "pegPriceType": "string", + # "currency": "string", + # "settlCurrency": "string", + # "execType": "string", + # "ordType": "string", + # "timeInForce": "string", + # "execInst": "string", + # "contingencyType": "string", + # "exDestination": "string", + # "ordStatus": "string", + # "triggered": "string", + # "workingIndicator": True, + # "ordRejReason": "string", + # "simpleLeavesQty": 0, + # "leavesQty": 0, + # "simpleCumQty": 0, + # "cumQty": 0, + # "avgPx": 0, + # "commission": 0, + # "tradePublishIndicator": "string", + # "multiLegReportingType": "string", + # "text": "string", + # "trdMatchID": "string", + # "execCost": 0, + # "execComm": 0, + # "homeNotional": 0, + # "foreignNotional": 0, + # "transactTime": "2019-03-05T12:47:02.762Z", + # "timestamp": "2019-03-05T12:47:02.762Z" + # } + # ] + # + return self.parse_trades(response, market, since, limit) + + def parse_ledger_entry_type(self, type): + types: dict = { + 'Withdrawal': 'transaction', + 'RealisedPNL': 'margin', + 'UnrealisedPNL': 'margin', + 'Deposit': 'transaction', + 'Transfer': 'transfer', + 'AffiliatePayout': 'referral', + 'SpotTrade': 'trade', + } + return self.safe_string(types, type, type) + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + # + # { + # "transactID": "69573da3-7744-5467-3207-89fd6efe7a47", + # "account": 24321, + # "currency": "XBt", + # "transactType": "Withdrawal", # "AffiliatePayout", "Transfer", "Deposit", "RealisedPNL", ... + # "amount": -1000000, + # "fee": 300000, + # "transactStatus": "Completed", # "Canceled", ... + # "address": "1Ex4fkF4NhQaQdRWNoYpqiPbDBbq18Kdd9", + # "tx": "3BMEX91ZhhKoWtsH9QRb5dNXnmnGpiEetA", + # "text": "", + # "transactTime": "2017-03-21T20:05:14.388Z", + # "walletBalance": 0, # balance after + # "marginBalance": null, + # "timestamp": "2017-03-22T13:09:23.514Z" + # } + # + # ButMEX returns the unrealized pnl from the wallet history endpoint. + # The unrealized pnl transaction has an empty timestamp. + # It is not related to historical pnl it has status set to "Pending". + # Therefore it's not a part of the history at all. + # https://github.com/ccxt/ccxt/issues/6047 + # + # { + # "transactID":"00000000-0000-0000-0000-000000000000", + # "account":121210, + # "currency":"XBt", + # "transactType":"UnrealisedPNL", + # "amount":-5508, + # "fee":0, + # "transactStatus":"Pending", + # "address":"XBTUSD", + # "tx":"", + # "text":"", + # "transactTime":null, # ←---------------------------- null + # "walletBalance":139198767, + # "marginBalance":139193259, + # "timestamp":null # ←---------------------------- null + # } + # + id = self.safe_string(item, 'transactID') + account = self.safe_string(item, 'account') + referenceId = self.safe_string(item, 'tx') + referenceAccount = None + type = self.parse_ledger_entry_type(self.safe_string(item, 'transactType')) + currencyId = self.safe_string(item, 'currency') + code = self.safe_currency_code(currencyId, currency) + currency = self.safe_currency(currencyId, currency) + amountString = self.safe_string(item, 'amount') + amount = self.convert_to_real_amount(code, amountString) + timestamp = self.parse8601(self.safe_string(item, 'transactTime')) + if timestamp is None: + # https://github.com/ccxt/ccxt/issues/6047 + # set the timestamp to zero, 1970 Jan 1 00:00:00 + # for unrealized pnl and other transactions without a timestamp + timestamp = 0 # see comments above + fee = None + feeCost = self.safe_string(item, 'fee') + if feeCost is not None: + feeCost = self.convert_to_real_amount(code, feeCost) + fee = { + 'cost': self.parse_number(feeCost), + 'currency': code, + } + after = self.safe_string(item, 'walletBalance') + if after is not None: + after = self.convert_to_real_amount(code, after) + before = self.parse_number(Precise.string_sub(self.number_to_string(after), self.number_to_string(amount))) + direction = None + if Precise.string_lt(amountString, '0'): + direction = 'out' + amount = self.convert_to_real_amount(code, Precise.string_abs(amountString)) + else: + direction = 'in' + status = self.parse_transaction_status(self.safe_string(item, 'transactStatus')) + return self.safe_ledger_entry({ + 'info': item, + 'id': id, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'direction': direction, + 'account': account, + 'referenceId': referenceId, + 'referenceAccount': referenceAccount, + 'type': type, + 'currency': code, + 'amount': self.parse_number(amount), + 'before': before, + 'after': self.parse_number(after), + 'status': status, + 'fee': fee, + }, currency) + + def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + + https://www.bitmex.com/api/explorer/#not /User/User_getWalletHistory + + :param str [code]: unified currency code, default is None + :param int [since]: timestamp in ms of the earliest ledger entry, default is None + :param int [limit]: max number of ledger entries to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ledger structure ` + """ + self.load_markets() + request: dict = { + # 'start': 123, + } + # + # if since is not None: + # # date-based pagination not supported + # } + # + if limit is not None: + request['count'] = limit + currency = None + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + response = self.privateGetUserWalletHistory(self.extend(request, params)) + # + # [ + # { + # "transactID": "69573da3-7744-5467-3207-89fd6efe7a47", + # "account": 24321, + # "currency": "XBt", + # "transactType": "Withdrawal", # "AffiliatePayout", "Transfer", "Deposit", "RealisedPNL", ... + # "amount": -1000000, + # "fee": 300000, + # "transactStatus": "Completed", # "Canceled", ... + # "address": "1Ex4fkF4NhQaQdRWNoYpqiPbDBbq18Kdd9", + # "tx": "3BMEX91ZhhKoWtsH9QRb5dNXnmnGpiEetA", + # "text": "", + # "transactTime": "2017-03-21T20:05:14.388Z", + # "walletBalance": 0, # balance after + # "marginBalance": null, + # "timestamp": "2017-03-22T13:09:23.514Z" + # } + # ] + # + return self.parse_ledger(response, currency, since, limit) + + def fetch_deposits_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch history of deposits and withdrawals + + https://www.bitmex.com/api/explorer/#not /User/User_getWalletHistory + + :param str [code]: unified currency code for the currency of the deposit/withdrawals, default is None + :param int [since]: timestamp in ms of the earliest deposit/withdrawal, default is None + :param int [limit]: max number of deposit/withdrawals to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `transaction structure ` + """ + self.load_markets() + request: dict = { + 'currency': 'all', + # 'start': 123, + } + # + # if since is not None: + # # date-based pagination not supported + # } + # + currency = None + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + if limit is not None: + request['count'] = limit + response = self.privateGetUserWalletHistory(self.extend(request, params)) + transactions = self.filter_by_array(response, 'transactType', ['Withdrawal', 'Deposit'], False) + return self.parse_transactions(transactions, currency, since, limit) + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'Confirmed': 'pending', + 'Canceled': 'canceled', + 'Completed': 'ok', + 'Pending': 'pending', + } + return self.safe_string(statuses, status, status) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # { + # "transactID": "ffe699c2-95ee-4c13-91f9-0faf41daec25", + # "account": 123456, + # "currency": "XBt", + # "network":'', # "tron" for USDt, etc... + # "transactType": "Withdrawal", + # "amount": -100100000, + # "fee": 100000, + # "transactStatus": "Completed", + # "address": "385cR5DM96n1HvBDMzLHPYcw89fZAXULJP", + # "tx": "3BMEXabcdefghijklmnopqrstuvwxyz123", + # "text": '', + # "transactTime": "2019-01-02T01:00:00.000Z", + # "walletBalance": 99900000, # self field might be inexistent + # "marginBalance": None, # self field might be inexistent + # "timestamp": "2019-01-02T13:00:00.000Z" + # } + # + currencyId = self.safe_string(transaction, 'currency') + currency = self.safe_currency(currencyId, currency) + # For deposits, transactTime == timestamp + # For withdrawals, transactTime is submission, timestamp is processed + transactTime = self.parse8601(self.safe_string(transaction, 'transactTime')) + timestamp = self.parse8601(self.safe_string(transaction, 'timestamp')) + type = self.safe_string_lower(transaction, 'transactType') + # Deposits have no from address or to address, withdrawals have both + address = None + addressFrom = None + addressTo = None + if type == 'withdrawal': + address = self.safe_string(transaction, 'address') + addressFrom = self.safe_string(transaction, 'tx') + addressTo = address + elif type == 'deposit': + addressTo = self.safe_string(transaction, 'address') + addressFrom = self.safe_string(transaction, 'tx') + amountString = self.safe_string(transaction, 'amount') + amountStringAbs = Precise.string_abs(amountString) + amount = self.convert_to_real_amount(currency['code'], amountStringAbs) + feeCostString = self.safe_string(transaction, 'fee') + feeCost = self.convert_to_real_amount(currency['code'], feeCostString) + status = self.safe_string(transaction, 'transactStatus') + if status is not None: + status = self.parse_transaction_status(status) + return { + 'info': transaction, + 'id': self.safe_string(transaction, 'transactID'), + 'txid': self.safe_string(transaction, 'tx'), + 'type': type, + 'currency': currency['code'], + 'network': self.network_id_to_code(self.safe_string(transaction, 'network'), currency['code']), + 'amount': self.parse_number(amount), + 'status': status, + 'timestamp': transactTime, + 'datetime': self.iso8601(transactTime), + 'address': address, + 'addressFrom': addressFrom, + 'addressTo': addressTo, + 'tag': None, + 'tagFrom': None, + 'tagTo': None, + 'updated': timestamp, + 'internal': None, + 'comment': None, + 'fee': { + 'currency': currency['code'], + 'cost': self.parse_number(feeCost), + 'rate': None, + }, + } + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://www.bitmex.com/api/explorer/#not /Instrument/Instrument_get + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = self.publicGetInstrument(self.extend(request, params)) + ticker = self.safe_value(response, 0) + if ticker is None: + raise BadSymbol(self.id + ' fetchTicker() symbol ' + symbol + ' not found') + return self.parse_ticker(ticker, market) + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://www.bitmex.com/api/explorer/#not /Instrument/Instrument_getActiveAndIndices + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + symbols = self.market_symbols(symbols) + response = self.publicGetInstrumentActiveAndIndices(params) + # same response "fetchMarkets" + result: dict = {} + for i in range(0, len(response)): + ticker = self.parse_ticker(response[i]) + symbol = self.safe_string(ticker, 'symbol') + if symbol is not None: + result[symbol] = ticker + return self.filter_by_array_tickers(result, 'symbol', symbols) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # see response sample under "fetchMarkets" because same endpoint is being used here + marketId = self.safe_string(ticker, 'symbol') + symbol = self.safe_symbol(marketId, market) + timestamp = self.parse8601(self.safe_string(ticker, 'timestamp')) + open = self.safe_string(ticker, 'prevPrice24h') + last = self.safe_string(ticker, 'lastPrice') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'highPrice'), + 'low': self.safe_string(ticker, 'lowPrice'), + 'bid': self.safe_string(ticker, 'bidPrice'), + 'bidVolume': None, + 'ask': self.safe_string(ticker, 'askPrice'), + 'askVolume': None, + 'vwap': self.safe_string(ticker, 'vwap'), + 'open': open, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': self.safe_string(ticker, 'homeNotional24h'), + 'quoteVolume': self.safe_string(ticker, 'foreignNotional24h'), + 'markPrice': self.safe_string(ticker, 'markPrice'), + 'info': ticker, + }, market) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # { + # "timestamp":"2015-09-25T13:38:00.000Z", + # "symbol":"XBTUSD", + # "open":237.45, + # "high":237.45, + # "low":237.45, + # "close":237.45, + # "trades":0, + # "volume":0, + # "vwap":null, + # "lastSize":null, + # "turnover":0, + # "homeNotional":0, + # "foreignNotional":0 + # } + # + marketId = self.safe_string(ohlcv, 'symbol') + market = self.safe_market(marketId, market) + volume = self.convert_from_raw_quantity(market['symbol'], self.safe_string(ohlcv, 'volume')) + return [ + self.parse8601(self.safe_string(ohlcv, 'timestamp')), + self.safe_number(ohlcv, 'open'), + self.safe_number(ohlcv, 'high'), + self.safe_number(ohlcv, 'low'), + self.safe_number(ohlcv, 'close'), + volume, + ] + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://www.bitmex.com/api/explorer/#not /Trade/Trade_getBucketed + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate') + if paginate: + return self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params) + # send JSON key/value pairs, such as {"key": "value"} + # filter by individual fields and do advanced queries on timestamps + # filter: Dict = {'key': 'value'} + # send a bare series(e.g. XBU) to nearest expiring contract in that series + # you can also send a timeframe, e.g. XBU:monthly + # timeframes: daily, weekly, monthly, quarterly, and biquarterly + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'binSize': self.safe_string(self.timeframes, timeframe, timeframe), + 'partial': True, # True == include yet-incomplete current bins + # 'filter': filter, # filter by individual fields and do advanced queries + # 'columns': [], # will return all columns if omitted + # 'start': 0, # starting point for results(wtf?) + # 'reverse': False, # True == newest first + # 'endTime': '', # ending date filter for results + } + if limit is not None: + request['count'] = limit # default 100, max 500 + until = self.safe_integer(params, 'until') + if until is not None: + params = self.omit(params, ['until']) + request['endTime'] = self.iso8601(until) + duration = self.parse_timeframe(timeframe) * 1000 + fetchOHLCVOpenTimestamp = self.safe_bool(self.options, 'fetchOHLCVOpenTimestamp', True) + # if since is not set, they will return candles starting from 2017-01-01 + if since is not None: + timestamp = since + if fetchOHLCVOpenTimestamp: + timestamp = self.sum(timestamp, duration) + startTime = self.iso8601(timestamp) + request['startTime'] = startTime # starting date filter for results + else: + request['reverse'] = True + response = self.publicGetTradeBucketed(self.extend(request, params)) + # + # [ + # {"timestamp":"2015-09-25T13:38:00.000Z","symbol":"XBTUSD","open":237.45,"high":237.45,"low":237.45,"close":237.45,"trades":0,"volume":0,"vwap":null,"lastSize":null,"turnover":0,"homeNotional":0,"foreignNotional":0}, + # {"timestamp":"2015-09-25T13:39:00.000Z","symbol":"XBTUSD","open":237.45,"high":237.45,"low":237.45,"close":237.45,"trades":0,"volume":0,"vwap":null,"lastSize":null,"turnover":0,"homeNotional":0,"foreignNotional":0}, + # {"timestamp":"2015-09-25T13:40:00.000Z","symbol":"XBTUSD","open":237.45,"high":237.45,"low":237.45,"close":237.45,"trades":0,"volume":0,"vwap":null,"lastSize":null,"turnover":0,"homeNotional":0,"foreignNotional":0} + # ] + # + result = self.parse_ohlcvs(response, market, timeframe, since, limit) + if fetchOHLCVOpenTimestamp: + # bitmex returns the candle's close timestamp - https://github.com/ccxt/ccxt/issues/4446 + # we can emulate the open timestamp by shifting all the timestamps one place + # so the previous close becomes the current open, and we drop the first candle + for i in range(0, len(result)): + result[i][0] = result[i][0] - duration + return result + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(public) + # + # { + # "timestamp": "2018-08-28T00:00:02.735Z", + # "symbol": "XBTUSD", + # "side": "Buy", + # "size": 2000, + # "price": 6906.5, + # "tickDirection": "PlusTick", + # "trdMatchID": "b9a42432-0a46-6a2f-5ecc-c32e9ca4baf8", + # "grossValue": 28958000, + # "homeNotional": 0.28958, + # "foreignNotional": 2000 + # } + # + # fetchMyTrades(private) + # + # { + # "execID": "string", + # "orderID": "string", + # "clOrdID": "string", + # "clOrdLinkID": "string", + # "account": 0, + # "symbol": "string", + # "side": "string", + # "lastQty": 0, + # "lastPx": 0, + # "underlyingLastPx": 0, + # "lastMkt": "string", + # "lastLiquidityInd": "string", + # "simpleOrderQty": 0, + # "orderQty": 0, + # "price": 0, + # "displayQty": 0, + # "stopPx": 0, + # "pegOffsetValue": 0, + # "pegPriceType": "string", + # "currency": "string", + # "settlCurrency": "string", + # "execType": "string", + # "ordType": "string", + # "timeInForce": "string", + # "execInst": "string", + # "contingencyType": "string", + # "exDestination": "string", + # "ordStatus": "string", + # "triggered": "string", + # "workingIndicator": True, + # "ordRejReason": "string", + # "simpleLeavesQty": 0, + # "leavesQty": 0, + # "simpleCumQty": 0, + # "cumQty": 0, + # "avgPx": 0, + # "commission": 0, + # "tradePublishIndicator": "string", + # "multiLegReportingType": "string", + # "text": "string", + # "trdMatchID": "string", + # "execCost": 0, + # "execComm": 0, + # "homeNotional": 0, + # "foreignNotional": 0, + # "transactTime": "2019-03-05T12:47:02.762Z", + # "timestamp": "2019-03-05T12:47:02.762Z" + # } + # + marketId = self.safe_string(trade, 'symbol') + symbol = self.safe_symbol(marketId, market) + timestamp = self.parse8601(self.safe_string(trade, 'timestamp')) + priceString = self.safe_string_2(trade, 'avgPx', 'price') + amountString = self.convert_from_raw_quantity(symbol, self.safe_string_2(trade, 'size', 'lastQty')) + execCost = self.number_to_string(self.convert_from_raw_cost(symbol, self.safe_string(trade, 'execCost'))) + id = self.safe_string(trade, 'trdMatchID') + order = self.safe_string(trade, 'orderID') + side = self.safe_string_lower(trade, 'side') + # price * amount doesn't work for all symbols(e.g. XBT, ETH) + fee = None + feeCostString = self.number_to_string(self.convert_from_raw_cost(symbol, self.safe_string(trade, 'execComm'))) + if feeCostString is not None: + currencyId = self.safe_string_2(trade, 'settlCurrency', 'currency') + fee = { + 'cost': feeCostString, + 'currency': self.safe_currency_code(currencyId), + 'rate': self.safe_string(trade, 'commission'), + } + # Trade or Funding + execType = self.safe_string(trade, 'execType') + takerOrMaker = None + if feeCostString is not None and execType == 'Trade': + takerOrMaker = 'maker' if Precise.string_lt(feeCostString, '0') else 'taker' + type = self.safe_string_lower(trade, 'ordType') + return self.safe_trade({ + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'id': id, + 'order': order, + 'type': type, + 'takerOrMaker': takerOrMaker, + 'side': side, + 'price': priceString, + 'cost': Precise.string_abs(execCost), + 'amount': amountString, + 'fee': fee, + }, market) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'New': 'open', + 'PartiallyFilled': 'open', + 'Filled': 'closed', + 'DoneForDay': 'open', + 'Canceled': 'canceled', + 'PendingCancel': 'open', + 'PendingNew': 'open', + 'Rejected': 'rejected', + 'Expired': 'expired', + 'Stopped': 'open', + 'Untriggered': 'open', + 'Triggered': 'open', + } + return self.safe_string(statuses, status, status) + + def parse_time_in_force(self, timeInForce: Str): + timeInForces: dict = { + 'Day': 'Day', + 'GoodTillCancel': 'GTC', + 'ImmediateOrCancel': 'IOC', + 'FillOrKill': 'FOK', + } + return self.safe_string(timeInForces, timeInForce, timeInForce) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # { + # "orderID":"56222c7a-9956-413a-82cf-99f4812c214b", + # "clOrdID":"", + # "clOrdLinkID":"", + # "account":1455728, + # "symbol":"XBTUSD", + # "side":"Sell", + # "simpleOrderQty":null, + # "orderQty":1, + # "price":40000, + # "displayQty":null, + # "stopPx":null, + # "pegOffsetValue":null, + # "pegPriceType":"", + # "currency":"USD", + # "settlCurrency":"XBt", + # "ordType":"Limit", + # "timeInForce":"GoodTillCancel", + # "execInst":"", + # "contingencyType":"", + # "exDestination":"XBME", + # "ordStatus":"New", + # "triggered":"", + # "workingIndicator":true, + # "ordRejReason":"", + # "simpleLeavesQty":null, + # "leavesQty":1, + # "simpleCumQty":null, + # "cumQty":0, + # "avgPx":null, + # "multiLegReportingType":"SingleSecurity", + # "text":"Submitted via API.", + # "transactTime":"2021-01-02T21:38:49.246Z", + # "timestamp":"2021-01-02T21:38:49.246Z" + # } + # + marketId = self.safe_string(order, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + qty = self.safe_string(order, 'orderQty') + cost = None + amount = None + isInverse = False + if marketId is None: + defaultSubType = self.safe_string(self.options, 'defaultSubType', 'linear') + isInverse = (defaultSubType == 'inverse') + else: + isInverse = self.safe_bool(market, 'inverse', False) + if isInverse: + cost = self.convert_from_raw_quantity(symbol, qty) + else: + amount = self.convert_from_raw_quantity(symbol, qty) + average = self.safe_string(order, 'avgPx') + filled = None + cumQty = self.number_to_string(self.convert_from_raw_quantity(symbol, self.safe_string(order, 'cumQty'))) + if isInverse: + filled = Precise.string_div(cumQty, average) + else: + filled = cumQty + execInst = self.safe_string(order, 'execInst') + postOnly = None + if execInst is not None: + postOnly = (execInst == 'ParticipateDoNotInitiate') + timestamp = self.parse8601(self.safe_string(order, 'timestamp')) + triggerPrice = self.safe_number(order, 'stopPx') + remaining = self.safe_string(order, 'leavesQty') + return self.safe_order({ + 'info': order, + 'id': self.safe_string(order, 'orderID'), + 'clientOrderId': self.safe_string(order, 'clOrdID'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': self.parse8601(self.safe_string(order, 'transactTime')), + 'symbol': symbol, + 'type': self.safe_string_lower(order, 'ordType'), + 'timeInForce': self.parse_time_in_force(self.safe_string(order, 'timeInForce')), + 'postOnly': postOnly, + 'side': self.safe_string_lower(order, 'side'), + 'price': self.safe_string(order, 'price'), + 'triggerPrice': triggerPrice, + 'amount': amount, + 'cost': cost, + 'average': average, + 'filled': filled, + 'remaining': self.convert_from_raw_quantity(symbol, remaining), + 'status': self.parse_order_status(self.safe_string(order, 'ordStatus')), + 'fee': None, + 'trades': None, + }, market) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://www.bitmex.com/api/explorer/#not /Trade/Trade_get + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchTrades', 'paginate') + if paginate: + return self.fetch_paginated_call_dynamic('fetchTrades', symbol, since, limit, params) + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if since is not None: + request['startTime'] = self.iso8601(since) + else: + # by default reverse=false, i.e. trades are fetched since the time of market inception(year 2015 for XBTUSD) + request['reverse'] = True + if limit is not None: + request['count'] = min(limit, 1000) # api maximum 1000 + until = self.safe_integer_2(params, 'until', 'endTime') + if until is not None: + params = self.omit(params, ['until']) + request['endTime'] = self.iso8601(until) + response = self.publicGetTrade(self.extend(request, params)) + # + # [ + # { + # "timestamp": "2018-08-28T00:00:02.735Z", + # "symbol": "XBTUSD", + # "side": "Buy", + # "size": 2000, + # "price": 6906.5, + # "tickDirection": "PlusTick", + # "trdMatchID": "b9a42432-0a46-6a2f-5ecc-c32e9ca4baf8", + # "grossValue": 28958000, + # "homeNotional": 0.28958, + # "foreignNotional": 2000 + # }, + # { + # "timestamp": "2018-08-28T00:00:03.778Z", + # "symbol": "XBTUSD", + # "side": "Sell", + # "size": 1000, + # "price": 6906, + # "tickDirection": "MinusTick", + # "trdMatchID": "0d4f1682-5270-a800-569b-4a0eb92db97c", + # "grossValue": 14480000, + # "homeNotional": 0.1448, + # "foreignNotional": 1000 + # }, + # ] + # + return self.parse_trades(response, market, since, limit) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://www.bitmex.com/api/explorer/#not /Order/Order_new + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param dict [params.triggerPrice]: the price at which a trigger order is triggered at + :param dict [params.triggerDirection]: the direction whenever the trigger happens with relation to price - 'ascending' or 'descending' + :param float [params.trailingAmount]: the quote amount to trail away from the current market price + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + orderType = self.capitalize(type) + reduceOnly = self.safe_value(params, 'reduceOnly') + if reduceOnly is not None: + if (not market['swap']) and (not market['future']): + raise InvalidOrder(self.id + ' createOrder() does not support reduceOnly for ' + market['type'] + ' orders, reduceOnly orders are supported for swap and future markets only') + brokerId = self.safe_string(self.options, 'brokerId', 'CCXT') + qty = self.parse_to_int(self.amount_to_precision(symbol, amount)) + request: dict = { + 'symbol': market['id'], + 'side': self.capitalize(side), + 'orderQty': qty, # lot size multiplied by the number of contracts + 'ordType': orderType, + 'text': brokerId, + } + # support for unified trigger format + triggerPrice = self.safe_number_n(params, ['triggerPrice', 'stopPx', 'stopPrice']) + trailingAmount = self.safe_string_2(params, 'trailingAmount', 'pegOffsetValue') + isTriggerOrder = triggerPrice is not None + isTrailingAmountOrder = trailingAmount is not None + if isTriggerOrder or isTrailingAmountOrder: + triggerDirection = self.safe_string(params, 'triggerDirection') + triggerAbove = ((triggerDirection == 'ascending') or (triggerDirection == 'above')) + if (type == 'limit') or (type == 'market'): + self.check_required_argument('createOrder', triggerDirection, 'triggerDirection', ['above', 'below']) + if type == 'limit': + if side == 'buy': + orderType = 'StopLimit' if triggerAbove else 'LimitIfTouched' + else: + orderType = 'LimitIfTouched' if triggerAbove else 'StopLimit' + elif type == 'market': + if side == 'buy': + orderType = 'Stop' if triggerAbove else 'MarketIfTouched' + else: + orderType = 'MarketIfTouched' if triggerAbove else 'Stop' + if isTrailingAmountOrder: + isStopSellOrder = (side == 'sell') and ((orderType == 'Stop') or (orderType == 'StopLimit')) + isBuyIfTouchedOrder = (side == 'buy') and ((orderType == 'MarketIfTouched') or (orderType == 'LimitIfTouched')) + if isStopSellOrder or isBuyIfTouchedOrder: + trailingAmount = '-' + trailingAmount + request['pegOffsetValue'] = self.parse_to_numeric(trailingAmount) + request['pegPriceType'] = 'TrailingStopPeg' + else: + if triggerPrice is None: + # if exchange specific trigger types were provided + raise ArgumentsRequired(self.id + ' createOrder() requires a triggerPrice parameter for the ' + orderType + ' order type') + request['stopPx'] = self.parse_to_numeric(self.price_to_precision(symbol, triggerPrice)) + request['ordType'] = orderType + params = self.omit(params, ['triggerPrice', 'stopPrice', 'stopPx', 'triggerDirection', 'trailingAmount']) + if (orderType == 'Limit') or (orderType == 'StopLimit') or (orderType == 'LimitIfTouched'): + request['price'] = self.parse_to_numeric(self.price_to_precision(symbol, price)) + clientOrderId = self.safe_string_2(params, 'clOrdID', 'clientOrderId') + if clientOrderId is not None: + request['clOrdID'] = clientOrderId + params = self.omit(params, ['clOrdID', 'clientOrderId']) + response = self.privatePostOrder(self.extend(request, params)) + return self.parse_order(response, market) + + def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + self.load_markets() + request: dict = {} + trailingAmount = self.safe_string_2(params, 'trailingAmount', 'pegOffsetValue') + isTrailingAmountOrder = trailingAmount is not None + if isTrailingAmountOrder: + triggerDirection = self.safe_string(params, 'triggerDirection') + triggerAbove = ((triggerDirection == 'ascending') or (triggerDirection == 'above')) + if (type == 'limit') or (type == 'market'): + self.check_required_argument('createOrder', triggerDirection, 'triggerDirection', ['above', 'below']) + orderType = None + if type == 'limit': + if side == 'buy': + orderType = 'StopLimit' if triggerAbove else 'LimitIfTouched' + else: + orderType = 'LimitIfTouched' if triggerAbove else 'StopLimit' + elif type == 'market': + if side == 'buy': + orderType = 'Stop' if triggerAbove else 'MarketIfTouched' + else: + orderType = 'MarketIfTouched' if triggerAbove else 'Stop' + isStopSellOrder = (side == 'sell') and ((orderType == 'Stop') or (orderType == 'StopLimit')) + isBuyIfTouchedOrder = (side == 'buy') and ((orderType == 'MarketIfTouched') or (orderType == 'LimitIfTouched')) + if isStopSellOrder or isBuyIfTouchedOrder: + trailingAmount = '-' + trailingAmount + request['pegOffsetValue'] = self.parse_to_numeric(trailingAmount) + params = self.omit(params, ['triggerDirection', 'trailingAmount']) + origClOrdID = self.safe_string_2(params, 'origClOrdID', 'clientOrderId') + if origClOrdID is not None: + request['origClOrdID'] = origClOrdID + clientOrderId = self.safe_string(params, 'clOrdID', 'clientOrderId') + if clientOrderId is not None: + request['clOrdID'] = clientOrderId + params = self.omit(params, ['origClOrdID', 'clOrdID', 'clientOrderId']) + else: + request['orderID'] = id + if amount is not None: + qty = self.parse_to_int(self.amount_to_precision(symbol, amount)) + request['orderQty'] = qty + if price is not None: + request['price'] = price + brokerId = self.safe_string(self.options, 'brokerId', 'CCXT') + request['text'] = brokerId + response = self.privatePutOrder(self.extend(request, params)) + return self.parse_order(response) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://www.bitmex.com/api/explorer/#not /Order/Order_cancel + + :param str id: order id + :param str symbol: not used by bitmex cancelOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + # https://github.com/ccxt/ccxt/issues/6507 + clientOrderId = self.safe_value_2(params, 'clOrdID', 'clientOrderId') + request: dict = {} + if clientOrderId is None: + request['orderID'] = id + else: + request['clOrdID'] = clientOrderId + params = self.omit(params, ['clOrdID', 'clientOrderId']) + response = self.privateDeleteOrder(self.extend(request, params)) + order = self.safe_value(response, 0, {}) + error = self.safe_string(order, 'error') + if error is not None: + if error.find('Unable to cancel order due to existing state') >= 0: + raise OrderNotFound(self.id + ' cancelOrder() failed: ' + error) + return self.parse_order(order) + + def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): + """ + cancel multiple orders + + https://www.bitmex.com/api/explorer/#not /Order/Order_cancel + + :param str[] ids: order ids + :param str symbol: not used by bitmex cancelOrders() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an list of `order structures ` + """ + # return self.cancel_order(ids, symbol, params) + self.load_markets() + # https://github.com/ccxt/ccxt/issues/6507 + clientOrderId = self.safe_value_2(params, 'clOrdID', 'clientOrderId') + request: dict = {} + if clientOrderId is None: + request['orderID'] = ids + else: + request['clOrdID'] = clientOrderId + params = self.omit(params, ['clOrdID', 'clientOrderId']) + response = self.privateDeleteOrder(self.extend(request, params)) + return self.parse_orders(response) + + def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders + + https://www.bitmex.com/api/explorer/#not /Order/Order_cancelAll + + :param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + response = self.privateDeleteOrderAll(self.extend(request, params)) + # + # [ + # { + # "orderID": "string", + # "clOrdID": "string", + # "clOrdLinkID": "string", + # "account": 0, + # "symbol": "string", + # "side": "string", + # "simpleOrderQty": 0, + # "orderQty": 0, + # "price": 0, + # "displayQty": 0, + # "stopPx": 0, + # "pegOffsetValue": 0, + # "pegPriceType": "string", + # "currency": "string", + # "settlCurrency": "string", + # "ordType": "string", + # "timeInForce": "string", + # "execInst": "string", + # "contingencyType": "string", + # "exDestination": "string", + # "ordStatus": "string", + # "triggered": "string", + # "workingIndicator": True, + # "ordRejReason": "string", + # "simpleLeavesQty": 0, + # "leavesQty": 0, + # "simpleCumQty": 0, + # "cumQty": 0, + # "avgPx": 0, + # "multiLegReportingType": "string", + # "text": "string", + # "transactTime": "2020-06-01T09:36:35.290Z", + # "timestamp": "2020-06-01T09:36:35.290Z" + # } + # ] + # + return self.parse_orders(response, market) + + def cancel_all_orders_after(self, timeout: Int, params={}): + """ + dead man's switch, cancel all orders after the given timeout + + https://www.bitmex.com/api/explorer/#not /Order/Order_cancelAllAfter + + :param number timeout: time in milliseconds, 0 represents cancel the timer + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: the api result + """ + self.load_markets() + request: dict = { + 'timeout': self.parse_to_int(timeout / 1000) if (timeout > 0) else 0, + } + response = self.privatePostOrderCancelAllAfter(self.extend(request, params)) + # + # { + # now: '2024-04-09T09:01:56.560Z', + # cancelTime: '2024-04-09T09:01:56.660Z' + # } + # + return response + + def fetch_leverages(self, symbols: Strings = None, params={}) -> Leverages: + """ + fetch the set leverage for all contract markets + + https://www.bitmex.com/api/explorer/#not /Position/Position_get + + :param str[] [symbols]: a list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `leverage structures ` + """ + self.load_markets() + leverages = self.fetch_positions(symbols, params) + return self.parse_leverages(leverages, symbols, 'symbol') + + def parse_leverage(self, leverage: dict, market: Market = None) -> Leverage: + marketId = self.safe_string(leverage, 'symbol') + return { + 'info': leverage, + 'symbol': self.safe_symbol(marketId, market), + 'marginMode': self.safe_string_lower(leverage, 'marginMode'), + 'longLeverage': self.safe_integer(leverage, 'leverage'), + 'shortLeverage': self.safe_integer(leverage, 'leverage'), + } + + def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://www.bitmex.com/api/explorer/#not /Position/Position_get + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + self.load_markets() + response = self.privateGetPosition(params) + # + # [ + # { + # "account": 0, + # "symbol": "string", + # "currency": "string", + # "underlying": "string", + # "quoteCurrency": "string", + # "commission": 0, + # "initMarginReq": 0, + # "maintMarginReq": 0, + # "riskLimit": 0, + # "leverage": 0, + # "crossMargin": True, + # "deleveragePercentile": 0, + # "rebalancedPnl": 0, + # "prevRealisedPnl": 0, + # "prevUnrealisedPnl": 0, + # "prevClosePrice": 0, + # "openingTimestamp": "2020-11-09T06:53:59.892Z", + # "openingQty": 0, + # "openingCost": 0, + # "openingComm": 0, + # "openOrderBuyQty": 0, + # "openOrderBuyCost": 0, + # "openOrderBuyPremium": 0, + # "openOrderSellQty": 0, + # "openOrderSellCost": 0, + # "openOrderSellPremium": 0, + # "execBuyQty": 0, + # "execBuyCost": 0, + # "execSellQty": 0, + # "execSellCost": 0, + # "execQty": 0, + # "execCost": 0, + # "execComm": 0, + # "currentTimestamp": "2020-11-09T06:53:59.893Z", + # "currentQty": 0, + # "currentCost": 0, + # "currentComm": 0, + # "realisedCost": 0, + # "unrealisedCost": 0, + # "grossOpenCost": 0, + # "grossOpenPremium": 0, + # "grossExecCost": 0, + # "isOpen": True, + # "markPrice": 0, + # "markValue": 0, + # "riskValue": 0, + # "homeNotional": 0, + # "foreignNotional": 0, + # "posState": "string", + # "posCost": 0, + # "posCost2": 0, + # "posCross": 0, + # "posInit": 0, + # "posComm": 0, + # "posLoss": 0, + # "posMargin": 0, + # "posMaint": 0, + # "posAllowance": 0, + # "taxableMargin": 0, + # "initMargin": 0, + # "maintMargin": 0, + # "sessionMargin": 0, + # "targetExcessMargin": 0, + # "varMargin": 0, + # "realisedGrossPnl": 0, + # "realisedTax": 0, + # "realisedPnl": 0, + # "unrealisedGrossPnl": 0, + # "longBankrupt": 0, + # "shortBankrupt": 0, + # "taxBase": 0, + # "indicativeTaxRate": 0, + # "indicativeTax": 0, + # "unrealisedTax": 0, + # "unrealisedPnl": 0, + # "unrealisedPnlPcnt": 0, + # "unrealisedRoePcnt": 0, + # "simpleQty": 0, + # "simpleCost": 0, + # "simpleValue": 0, + # "simplePnl": 0, + # "simplePnlPcnt": 0, + # "avgCostPrice": 0, + # "avgEntryPrice": 0, + # "breakEvenPrice": 0, + # "marginCallPrice": 0, + # "liquidationPrice": 0, + # "bankruptPrice": 0, + # "timestamp": "2020-11-09T06:53:59.894Z", + # "lastPrice": 0, + # "lastValue": 0 + # } + # ] + # + results = self.parse_positions(response, symbols) + return self.filter_by_array_positions(results, 'symbol', symbols, False) + + def parse_position(self, position: dict, market: Market = None): + # + # { + # "account": 9371654, + # "symbol": "ETHUSDT", + # "currency": "USDt", + # "underlying": "ETH", + # "quoteCurrency": "USDT", + # "commission": 0.00075, + # "initMarginReq": 0.3333333333333333, + # "maintMarginReq": 0.01, + # "riskLimit": 1000000000000, + # "leverage": 3, + # "crossMargin": False, + # "deleveragePercentile": 1, + # "rebalancedPnl": 0, + # "prevRealisedPnl": 0, + # "prevUnrealisedPnl": 0, + # "prevClosePrice": 2053.738, + # "openingTimestamp": "2022-05-21T04:00:00.000Z", + # "openingQty": 0, + # "openingCost": 0, + # "openingComm": 0, + # "openOrderBuyQty": 0, + # "openOrderBuyCost": 0, + # "openOrderBuyPremium": 0, + # "openOrderSellQty": 0, + # "openOrderSellCost": 0, + # "openOrderSellPremium": 0, + # "execBuyQty": 2000, + # "execBuyCost": 39260000, + # "execSellQty": 0, + # "execSellCost": 0, + # "execQty": 2000, + # "execCost": 39260000, + # "execComm": 26500, + # "currentTimestamp": "2022-05-21T04:35:16.397Z", + # "currentQty": 2000, + # "currentCost": 39260000, + # "currentComm": 26500, + # "realisedCost": 0, + # "unrealisedCost": 39260000, + # "grossOpenCost": 0, + # "grossOpenPremium": 0, + # "grossExecCost": 39260000, + # "isOpen": True, + # "markPrice": 1964.195, + # "markValue": 39283900, + # "riskValue": 39283900, + # "homeNotional": 0.02, + # "foreignNotional": -39.2839, + # "posState": "", + # "posCost": 39260000, + # "posCost2": 39260000, + # "posCross": 0, + # "posInit": 13086667, + # "posComm": 39261, + # "posLoss": 0, + # "posMargin": 13125928, + # "posMaint": 435787, + # "posAllowance": 0, + # "taxableMargin": 0, + # "initMargin": 0, + # "maintMargin": 13149828, + # "sessionMargin": 0, + # "targetExcessMargin": 0, + # "varMargin": 0, + # "realisedGrossPnl": 0, + # "realisedTax": 0, + # "realisedPnl": -26500, + # "unrealisedGrossPnl": 23900, + # "longBankrupt": 0, + # "shortBankrupt": 0, + # "taxBase": 0, + # "indicativeTaxRate": null, + # "indicativeTax": 0, + # "unrealisedTax": 0, + # "unrealisedPnl": 23900, + # "unrealisedPnlPcnt": 0.0006, + # "unrealisedRoePcnt": 0.0018, + # "simpleQty": null, + # "simpleCost": null, + # "simpleValue": null, + # "simplePnl": null, + # "simplePnlPcnt": null, + # "avgCostPrice": 1963, + # "avgEntryPrice": 1963, + # "breakEvenPrice": 1964.35, + # "marginCallPrice": 1328.5, + # "liquidationPrice": 1328.5, + # "bankruptPrice": 1308.7, + # "timestamp": "2022-05-21T04:35:16.397Z", + # "lastPrice": 1964.195, + # "lastValue": 39283900 + # } + # + market = self.safe_market(self.safe_string(position, 'symbol'), market) + symbol = market['symbol'] + datetime = self.safe_string(position, 'timestamp') + crossMargin = self.safe_value(position, 'crossMargin') + marginMode = 'cross' if (crossMargin is True) else 'isolated' + notionalString = Precise.string_abs(self.safe_string_2(position, 'foreignNotional', 'homeNotional')) + settleCurrencyCode = self.safe_string(market, 'settle') + maintenanceMargin = self.convert_to_real_amount(settleCurrencyCode, self.safe_string(position, 'maintMargin')) + unrealisedPnl = self.convert_to_real_amount(settleCurrencyCode, self.safe_string(position, 'unrealisedPnl')) + contracts = self.parse_number(Precise.string_abs(self.safe_string(position, 'currentQty'))) + contractSize = self.safe_number(market, 'contractSize') + side = None + homeNotional = self.safe_string(position, 'homeNotional') + if homeNotional is not None: + if homeNotional[0] == '-': + side = 'short' + else: + side = 'long' + return self.safe_position({ + 'info': position, + 'id': self.safe_string(position, 'account'), + 'symbol': symbol, + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + 'lastUpdateTimestamp': None, + 'hedged': None, + 'side': side, + 'contracts': contracts, + 'contractSize': contractSize, + 'entryPrice': self.safe_number(position, 'avgEntryPrice'), + 'markPrice': self.safe_number(position, 'markPrice'), + 'lastPrice': None, + 'notional': self.parse_number(notionalString), + 'leverage': self.safe_number(position, 'leverage'), + 'collateral': None, + 'initialMargin': self.safe_number(position, 'initMargin'), + 'initialMarginPercentage': self.safe_number(position, 'initMarginReq'), + 'maintenanceMargin': maintenanceMargin, + 'maintenanceMarginPercentage': self.safe_number(position, 'maintMarginReq'), + 'unrealizedPnl': unrealisedPnl, + 'liquidationPrice': self.safe_number(position, 'liquidationPrice'), + 'marginMode': marginMode, + 'marginRatio': None, + 'percentage': self.safe_number(position, 'unrealisedPnlPcnt'), + 'stopLossPrice': None, + 'takeProfitPrice': None, + }) + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://www.bitmex.com/api/explorer/#not /User/User_requestWithdrawal + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.check_address(address) + self.load_markets() + currency = self.currency(code) + qty = self.convert_from_real_amount(code, amount) + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + request: dict = { + 'currency': currency['id'], + 'amount': qty, + 'address': address, + 'network': self.network_code_to_id(networkCode, currency['code']), + # 'otpToken': '123456', # requires if two-factor auth(OTP) is enabled + # 'fee': 0.001, # bitcoin network fee + } + if self.twofa is not None: + request['otpToken'] = self.totp(self.twofa) + response = self.privatePostUserRequestWithdrawal(self.extend(request, params)) + # + # { + # "transactID": "3aece414-bb29-76c8-6c6d-16a477a51a1e", + # "account": 1403035, + # "currency": "USDt", + # "network": "tron", + # "transactType": "Withdrawal", + # "amount": -11000000, + # "fee": 1000000, + # "transactStatus": "Pending", + # "address": "TAf5JxcAQQsC2Nm2zu21XE2iDtnisxPo1x", + # "tx": "", + # "text": "", + # "transactTime": "2022-12-16T07:37:06.500Z", + # "timestamp": "2022-12-16T07:37:06.500Z", + # } + # + return self.parse_transaction(response, currency) + + def fetch_funding_rates(self, symbols: Strings = None, params={}) -> FundingRates: + """ + fetch the funding rate for multiple markets + + https://www.bitmex.com/api/explorer/#not /Instrument/Instrument_getActiveAndIndices + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `funding rate structures `, indexed by market symbols + """ + self.load_markets() + response = self.publicGetInstrumentActiveAndIndices(params) + # same response "fetchMarkets" + filteredResponse = [] + for i in range(0, len(response)): + item = response[i] + marketId = self.safe_string(item, 'symbol') + market = self.safe_market(marketId) + swap = self.safe_bool(market, 'swap', False) + if swap: + filteredResponse.append(item) + symbols = self.market_symbols(symbols) + result = self.parse_funding_rates(filteredResponse) + return self.filter_by_array(result, 'symbol', symbols) + + def parse_funding_rate(self, contract, market: Market = None) -> FundingRate: + # see response sample under "fetchMarkets" because same endpoint is being used here + datetime = self.safe_string(contract, 'timestamp') + marketId = self.safe_string(contract, 'symbol') + fundingDatetime = self.safe_string(contract, 'fundingTimestamp') + return { + 'info': contract, + 'symbol': self.safe_symbol(marketId, market), + 'markPrice': self.safe_number(contract, 'markPrice'), + 'indexPrice': None, + 'interestRate': None, + 'estimatedSettlePrice': self.safe_number(contract, 'indicativeSettlePrice'), + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + 'fundingRate': self.safe_number(contract, 'fundingRate'), + 'fundingTimestamp': self.parse8601(fundingDatetime), + 'fundingDatetime': fundingDatetime, + 'nextFundingRate': self.safe_number(contract, 'indicativeFundingRate'), + 'nextFundingTimestamp': None, + 'nextFundingDatetime': None, + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': None, + } + + def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + Fetches the history of funding rates + + https://www.bitmex.com/api/explorer/#not /Funding/Funding_get + + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: timestamp in ms of the earliest funding rate to fetch + :param int [limit]: the maximum amount of `funding rate structures ` to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms for ending date filter + :param bool [params.reverse]: if True, will sort results newest first + :param int [params.start]: starting point for results + :param str [params.columns]: array of column names to fetch in info, if omitted, will return all columns + :param str [params.filter]: generic table filter, send json key/value pairs, such as {"key": "value"}, you can key on individual fields, and do more advanced querying on timestamps, see the `timestamp docs ` for more details + :returns dict[]: a list of `funding rate structures ` + """ + self.load_markets() + request: dict = {} + market = None + if symbol in self.currencies: + code = self.currency(symbol) + request['symbol'] = code['id'] + elif symbol is not None: + splitSymbol = symbol.split(':') + splitSymbolLength = len(splitSymbol) + timeframes = ['nearest', 'daily', 'weekly', 'monthly', 'quarterly', 'biquarterly', 'perpetual'] + if (splitSymbolLength > 1) and self.in_array(splitSymbol[1], timeframes): + code = self.currency(splitSymbol[0]) + symbol = code['id'] + ':' + splitSymbol[1] + request['symbol'] = symbol + else: + market = self.market(symbol) + request['symbol'] = market['id'] + if since is not None: + request['startTime'] = self.iso8601(since) + if limit is not None: + request['count'] = limit + until = self.safe_integer(params, 'until') + params = self.omit(params, ['until']) + if until is not None: + request['endTime'] = self.iso8601(until) + if (since is None) and (until is None): + request['reverse'] = True + response = self.publicGetFunding(self.extend(request, params)) + # + # [ + # { + # "timestamp": "2016-05-07T12:00:00.000Z", + # "symbol": "ETHXBT", + # "fundingInterval": "2000-01-02T00:00:00.000Z", + # "fundingRate": 0.0010890000000000001, + # "fundingRateDaily": 0.0010890000000000001 + # } + # ] + # + return self.parse_funding_rate_histories(response, market, since, limit) + + def parse_funding_rate_history(self, info, market: Market = None): + # + # { + # "timestamp": "2016-05-07T12:00:00.000Z", + # "symbol": "ETHXBT", + # "fundingInterval": "2000-01-02T00:00:00.000Z", + # "fundingRate": 0.0010890000000000001, + # "fundingRateDaily": 0.0010890000000000001 + # } + # + marketId = self.safe_string(info, 'symbol') + datetime = self.safe_string(info, 'timestamp') + return { + 'info': info, + 'symbol': self.safe_symbol(marketId, market), + 'fundingRate': self.safe_number(info, 'fundingRate'), + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + } + + def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://www.bitmex.com/api/explorer/#not /Position/Position_updateLeverage + + :param float leverage: the rate of leverage + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setLeverage() requires a symbol argument') + if (leverage < 0.01) or (leverage > 100): + raise BadRequest(self.id + ' leverage should be between 0.01 and 100') + self.load_markets() + market = self.market(symbol) + if market['type'] != 'swap' and market['type'] != 'future': + raise BadSymbol(self.id + ' setLeverage() supports future and swap contracts only') + request: dict = { + 'symbol': market['id'], + 'leverage': leverage, + } + return self.privatePostPositionLeverage(self.extend(request, params)) + + def set_margin_mode(self, marginMode: str, symbol: Str = None, params={}): + """ + set margin mode to 'cross' or 'isolated' + + https://www.bitmex.com/api/explorer/#not /Position/Position_isolateMargin + + :param str marginMode: 'cross' or 'isolated' + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setMarginMode() requires a symbol argument') + marginMode = marginMode.lower() + if marginMode != 'isolated' and marginMode != 'cross': + raise BadRequest(self.id + ' setMarginMode() marginMode argument should be isolated or cross') + self.load_markets() + market = self.market(symbol) + if (market['type'] != 'swap') and (market['type'] != 'future'): + raise BadSymbol(self.id + ' setMarginMode() supports swap and future contracts only') + enabled = False if (marginMode == 'cross') else True + request: dict = { + 'symbol': market['id'], + 'enabled': enabled, + } + return self.privatePostPositionIsolate(self.extend(request, params)) + + def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://www.bitmex.com/api/explorer/#not /User/User_getDepositAddress + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.network]: deposit chain, can view all chains via self.publicGetWalletAssets, default is eth, unless the currency has a default chain within self.options['networks'] + :returns dict: an `address structure ` + """ + self.load_markets() + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + if networkCode is None: + raise ArgumentsRequired(self.id + ' fetchDepositAddress requires params["network"]') + currency = self.currency(code) + params = self.omit(params, 'network') + request: dict = { + 'currency': currency['id'], + 'network': self.network_code_to_id(networkCode, currency['code']), + } + response = self.privateGetUserDepositAddress(self.extend(request, params)) + # + # '"bc1qmex3puyrzn2gduqcnlu70c2uscpyaa9nm2l2j9le2lt2wkgmw33sy7ndjg"' + # + return { + 'info': response, + 'currency': code, + 'network': networkCode, + 'address': response.replace('"', '').replace('"', ''), # Done twice because some languages only replace the first instance + 'tag': None, + } + + def parse_deposit_withdraw_fee(self, fee, currency: Currency = None): + # + # { + # "asset": "XBT", + # "currency": "XBt", + # "majorCurrency": "XBT", + # "name": "Bitcoin", + # "currencyType": "Crypto", + # "scale": "8", + # "enabled": True, + # "isMarginCurrency": True, + # "minDepositAmount": "10000", + # "minWithdrawalAmount": "1000", + # "maxWithdrawalAmount": "100000000000000", + # "networks": [ + # { + # "asset": "btc", + # "tokenAddress": '', + # "depositEnabled": True, + # "withdrawalEnabled": True, + # "withdrawalFee": "20000", + # "minFee": "20000", + # "maxFee": "10000000" + # } + # ] + # } + # + networks = self.safe_value(fee, 'networks', []) + networksLength = len(networks) + result: dict = { + 'info': fee, + 'withdraw': { + 'fee': None, + 'percentage': None, + }, + 'deposit': { + 'fee': None, + 'percentage': None, + }, + 'networks': {}, + } + if networksLength != 0: + scale = self.safe_string(fee, 'scale') + precision = self.parse_precision(scale) + for i in range(0, networksLength): + network = networks[i] + networkId = self.safe_string(network, 'asset') + currencyCode = self.safe_string(currency, 'code') + networkCode = self.network_id_to_code(networkId, currencyCode) + withdrawalFeeId = self.safe_string(network, 'withdrawalFee') + withdrawalFee = self.parse_number(Precise.string_mul(withdrawalFeeId, precision)) + result['networks'][networkCode] = { + 'deposit': {'fee': None, 'percentage': None}, + 'withdraw': {'fee': withdrawalFee, 'percentage': False}, + } + if networksLength == 1: + result['withdraw']['fee'] = withdrawalFee + result['withdraw']['percentage'] = False + return result + + def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}): + """ + fetch deposit and withdraw fees + + https://www.bitmex.com/api/explorer/#not /Wallet/Wallet_getAssetsConfig + + :param str[]|None codes: list of unified currency codes + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `fee structures ` + """ + self.load_markets() + assets = self.publicGetWalletAssets(params) + # + # [ + # { + # "asset": "XBT", + # "currency": "XBt", + # "majorCurrency": "XBT", + # "name": "Bitcoin", + # "currencyType": "Crypto", + # "scale": "8", + # "enabled": True, + # "isMarginCurrency": True, + # "minDepositAmount": "10000", + # "minWithdrawalAmount": "1000", + # "maxWithdrawalAmount": "100000000000000", + # "networks": [ + # { + # "asset": "btc", + # "tokenAddress": '', + # "depositEnabled": True, + # "withdrawalEnabled": True, + # "withdrawalFee": "20000", + # "minFee": "20000", + # "maxFee": "10000000" + # } + # ] + # }, + # ... + # ] + # + return self.parse_deposit_withdraw_fees(assets, codes, 'asset') + + def calculate_rate_limiter_cost(self, api, method, path, params, config={}): + isAuthenticated = self.check_required_credentials(False) + cost = self.safe_value(config, 'cost', 1) + if cost != 1: # trading endpoints + if isAuthenticated: + return cost + else: + return 20 + return cost + + def fetch_liquidations(self, symbol: str, since: Int = None, limit: Int = None, params={}): + """ + retrieves the public liquidations of a trading pair + + https://www.bitmex.com/api/explorer/#not /Liquidation/Liquidation_get + + :param str symbol: unified CCXT market symbol + :param int [since]: the earliest time in ms to fetch liquidations for + :param int [limit]: the maximum number of liquidation structures to retrieve + :param dict [params]: exchange specific parameters for the bitmex api endpoint + :param int [params.until]: timestamp in ms of the latest liquidation + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict: an array of `liquidation structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchLiquidations', 'paginate') + if paginate: + return self.fetch_paginated_call_dynamic('fetchLiquidations', symbol, since, limit, params) + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if since is not None: + request['startTime'] = since + if limit is not None: + request['count'] = limit + request, params = self.handle_until_option('endTime', request, params) + response = self.publicGetLiquidation(self.extend(request, params)) + # + # [ + # { + # "orderID": "string", + # "symbol": "string", + # "side": "string", + # "price": 0, + # "leavesQty": 0 + # } + # ] + # + return self.parse_liquidations(response, market, since, limit) + + def parse_liquidation(self, liquidation, market: Market = None): + # + # { + # "orderID": "string", + # "symbol": "string", + # "side": "string", + # "price": 0, + # "leavesQty": 0 + # } + # + marketId = self.safe_string(liquidation, 'symbol') + return self.safe_liquidation({ + 'info': liquidation, + 'symbol': self.safe_symbol(marketId, market), + 'contracts': None, + 'contractSize': self.safe_number(market, 'contractSize'), + 'price': self.safe_number(liquidation, 'price'), + 'side': self.safe_string_lower(liquidation, 'side'), + 'baseValue': None, + 'quoteValue': None, + 'timestamp': None, + 'datetime': None, + }) + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None + if code == 429: + raise DDoSProtection(self.id + ' ' + body) + if code >= 400: + error = self.safe_value(response, 'error', {}) + message = self.safe_string(error, 'message') + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + if code == 400: + raise BadRequest(feedback) + raise ExchangeError(feedback) # unknown message + return None + + def nonce(self): + return self.milliseconds() + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + query = '/api/' + self.version + '/' + path + if method == 'GET': + if params: + query += '?' + self.urlencode(params) + else: + format = self.safe_string(params, '_format') + if format is not None: + query += '?' + self.urlencode({'_format': format}) + params = self.omit(params, '_format') + url = self.urls['api'][api] + query + isAuthenticated = self.check_required_credentials(False) + if api == 'private' or (api == 'public' and isAuthenticated): + self.check_required_credentials() + auth = method + query + expires = self.safe_integer(self.options, 'api-expires') + headers = { + 'Content-Type': 'application/json', + 'api-key': self.apiKey, + } + expires = self.sum(self.seconds(), expires) + stringExpires = str(expires) + auth += stringExpires + headers['api-expires'] = stringExpires + if method == 'POST' or method == 'PUT' or method == 'DELETE': + if params: + body = self.json(params) + auth += body + headers['api-signature'] = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256) + return {'url': url, 'method': method, 'body': body, 'headers': headers} diff --git a/ccxt/bitopro.py b/ccxt/bitopro.py new file mode 100644 index 0000000..5358fde --- /dev/null +++ b/ccxt/bitopro.py @@ -0,0 +1,1830 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.bitopro import ImplicitAPI +import hashlib +import math +from ccxt.base.types import Any, Balances, Currencies, Currency, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, TradingFees, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class bitopro(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(bitopro, self).describe(), { + 'id': 'bitopro', + 'name': 'BitoPro', + 'countries': ['TW'], # Taiwan + 'version': 'v3', + 'rateLimit': 100, + 'pro': True, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'cancelOrders': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createOrder': True, + 'createOrderWithTakeProfitAndStopLoss': False, + 'createOrderWithTakeProfitAndStopLossWs': False, + 'createReduceOnlyOrder': False, + 'createStopOrder': True, + 'createTriggerOrder': True, + 'editOrder': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': False, + 'fetchDeposits': True, + 'fetchDepositsWithdrawals': False, + 'fetchDepositWithdrawFee': 'emulated', + 'fetchDepositWithdrawFees': True, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': False, + 'fetchOrderTrades': False, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': False, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': True, + 'fetchTransactionFees': False, + 'fetchTransactions': False, + 'fetchTransfer': False, + 'fetchTransfers': False, + 'fetchVolatilityHistory': False, + 'fetchWithdrawal': True, + 'fetchWithdrawals': True, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'transfer': False, + 'withdraw': True, + }, + 'timeframes': { + '1m': '1m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1h', + '3h': '3h', + '6h': '6h', + '12h': '12h', + '1d': '1d', + '1w': '1w', + '1M': '1M', + }, + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/affc6337-b95a-44bf-aacd-04f9722364f6', + 'api': { + 'rest': 'https://api.bitopro.com/v3', + }, + 'www': 'https://www.bitopro.com', + 'doc': [ + 'https://github.com/bitoex/bitopro-offical-api-docs/blob/master/v3-1/rest-1/rest.md', + ], + 'fees': 'https://www.bitopro.com/fees', + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + }, + 'api': { + 'public': { + 'get': { + 'order-book/{pair}': 1, + 'tickers': 1, + 'tickers/{pair}': 1, + 'trades/{pair}': 1, + 'provisioning/currencies': 1, + 'provisioning/trading-pairs': 1, + 'provisioning/limitations-and-fees': 1, + 'trading-history/{pair}': 1, + 'price/otc/{currency}': 1, + }, + }, + 'private': { + 'get': { + 'accounts/balance': 1, + 'orders/history': 1, + 'orders/all/{pair}': 1, + 'orders/trades/{pair}': 1, + 'orders/{pair}/{orderId}': 1, + 'wallet/withdraw/{currency}/{serial}': 1, + 'wallet/withdraw/{currency}/id/{id}': 1, + 'wallet/depositHistory/{currency}': 1, + 'wallet/withdrawHistory/{currency}': 1, + 'orders/open': 1, + }, + 'post': { + 'orders/{pair}': 1 / 2, # 1200/m => 20/s => 10/20 = 1/2 + 'orders/batch': 20 / 3, # 90/m => 1.5/s => 10/1.5 = 20/3 + 'wallet/withdraw/{currency}': 10, # 60/m => 1/s => 10/1 = 10 + }, + 'put': { + 'orders': 5, # 2/s => 10/2 = 5 + }, + 'delete': { + 'orders/{pair}/{id}': 2 / 3, # 900/m => 15/s => 10/15 = 2/3 + 'orders/all': 5, # 2/s => 10/2 = 5 + 'orders/{pair}': 5, # 2/s => 10/2 = 5 + }, + }, + }, + 'fees': { + 'trading': { + 'tierBased': True, + 'percentage': True, + 'maker': self.parse_number('0.001'), + 'taker': self.parse_number('0.002'), + 'tiers': { + 'taker': [ + [self.parse_number('0'), self.parse_number('0.002')], + [self.parse_number('3000000'), self.parse_number('0.00194')], + [self.parse_number('5000000'), self.parse_number('0.0015')], + [self.parse_number('30000000'), self.parse_number('0.0014')], + [self.parse_number('300000000'), self.parse_number('0.0013')], + [self.parse_number('550000000'), self.parse_number('0.0012')], + [self.parse_number('1300000000'), self.parse_number('0.0011')], + ], + 'maker': [ + [self.parse_number('0'), self.parse_number('0.001')], + [self.parse_number('3000000'), self.parse_number('0.00097')], + [self.parse_number('5000000'), self.parse_number('0.0007')], + [self.parse_number('30000000'), self.parse_number('0.0006')], + [self.parse_number('300000000'), self.parse_number('0.0005')], + [self.parse_number('550000000'), self.parse_number('0.0004')], + [self.parse_number('1300000000'), self.parse_number('0.0003')], + ], + }, + }, + }, + 'options': { + 'networks': { + 'ERC20': 'ERC20', + 'ETH': 'ERC20', + 'TRX': 'TRX', + 'TRC20': 'TRX', + 'BEP20': 'BSC', + 'BSC': 'BSC', + }, + 'fiatCurrencies': ['TWD'], # the only fiat currency for exchange + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerPriceType': None, + 'triggerDirection': True, # todo implement + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': False, + 'FOK': False, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyRequiresPrice': False, + 'marketBuyByCost': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 100000, + 'untilDays': 100000, + 'symbolRequired': True, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + # todo: implement through fetchOrders + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 100000, + 'untilDays': 100000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 100000, + 'daysBackCanceled': 1, + 'untilDays': 10000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOHLCV': { + 'limit': 1000, + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'exact': { + 'Unsupported currency.': BadRequest, # {"error":"Unsupported currency."} + 'Unsupported order type': BadRequest, # {"error":"Unsupported order type"} + 'Invalid body': BadRequest, # {"error":"Invalid body"} + 'Invalid Signature': AuthenticationError, # {"error":"Invalid Signature"} + 'Address not in whitelist.': BadRequest, + }, + 'broad': { + 'Invalid amount': InvalidOrder, # {"error":"Invalid amount 0.0000000001, decimal limit is 8."} + 'Balance for ': InsufficientFunds, # {"error":"Balance for eth not enough, only has 0, but ordered 0.01."} + 'Invalid ': BadRequest, # {"error":"Invalid price -1."} + 'Wrong parameter': BadRequest, # {"error":"Wrong parameter: from"} + }, + }, + 'commonCurrencies': { + }, + }) + + def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/public/get_currency_info.md + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = self.publicGetProvisioningCurrencies(params) + currencies = self.safe_list(response, 'data', []) + # + # { + # "data":[ + # { + # "currency":"eth", + # "withdrawFee":"0.007", + # "minWithdraw":"0.001", + # "maxWithdraw":"1000", + # "maxDailyWithdraw":"2000", + # "withdraw":true, + # "deposit":true, + # "depositConfirmation":"12" + # } + # ] + # } + # + result: dict = {} + fiatCurrencies = self.safe_list(self.options, 'fiatCurrencies', []) + for i in range(0, len(currencies)): + currency = currencies[i] + currencyId = self.safe_string(currency, 'currency') + code = self.safe_currency_code(currencyId) + deposit = self.safe_bool(currency, 'deposit') + withdraw = self.safe_bool(currency, 'withdraw') + fee = self.safe_number(currency, 'withdrawFee') + withdrawMin = self.safe_number(currency, 'minWithdraw') + withdrawMax = self.safe_number(currency, 'maxWithdraw') + limits: dict = { + 'withdraw': { + 'min': withdrawMin, + 'max': withdrawMax, + }, + 'amount': { + 'min': None, + 'max': None, + }, + } + isFiat = self.in_array(code, fiatCurrencies) + result[code] = { + 'id': currencyId, + 'code': code, + 'info': currency, + 'type': 'fiat' if isFiat else 'crypto', + 'name': None, + 'active': deposit and withdraw, + 'deposit': deposit, + 'withdraw': withdraw, + 'fee': fee, + 'precision': None, + 'limits': limits, + 'networks': None, + } + return result + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for bitopro + + https://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/public/get_trading_pair_info.md + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = self.publicGetProvisioningTradingPairs() + markets = self.safe_list(response, 'data', []) + # + # { + # "data":[ + # { + # "pair":"shib_twd", + # "base":"shib", + # "quote":"twd", + # "basePrecision":"8", + # "quotePrecision":"6", + # "minLimitBaseAmount":"100000", + # "maxLimitBaseAmount":"5500000000", + # "minMarketBuyQuoteAmount":"1000", + # "orderOpenLimit":"200", + # "maintain":false, + # "orderBookQuotePrecision":"6", + # "orderBookQuoteScaleLevel":"5" + # } + # ] + # } + # + return self.parse_markets(markets) + + def parse_market(self, market: dict) -> Market: + active = not self.safe_bool(market, 'maintain') + id = self.safe_string(market, 'pair') + uppercaseId = id.upper() + baseId = self.safe_string(market, 'base') + quoteId = self.safe_string(market, 'quote') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + symbol = base + '/' + quote + limits: dict = { + 'amount': { + 'min': self.safe_number(market, 'minLimitBaseAmount'), + 'max': self.safe_number(market, 'maxLimitBaseAmount'), + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + 'leverage': { + 'min': None, + 'max': None, + }, + } + return { + 'id': id, + 'uppercaseId': uppercaseId, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'baseId': base, + 'quoteId': quote, + 'settle': None, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'limits': limits, + 'precision': { + 'price': self.parse_number(self.parse_precision(self.safe_string(market, 'quotePrecision'))), + 'amount': self.parse_number(self.parse_precision(self.safe_string(market, 'basePrecision'))), + }, + 'active': active, + 'created': None, + 'info': market, + } + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "pair":"btc_twd", + # "lastPrice":"1182449.00000000", + # "isBuyer":false, + # "priceChange24hr":"-1.99", + # "volume24hr":"9.13089740", + # "high24hr":"1226097.00000000", + # "low24hr":"1181000.00000000" + # } + # + marketId = self.safe_string(ticker, 'pair') + market = self.safe_market(marketId, market) + symbol = self.safe_string(market, 'symbol') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': None, + 'datetime': None, + 'high': self.safe_string(ticker, 'high24hr'), + 'low': self.safe_string(ticker, 'low24hr'), + 'bid': None, + 'bidVolume': None, + 'ask': None, + 'askVolume': None, + 'vwap': None, + 'open': None, + 'close': self.safe_string(ticker, 'lastPrice'), + 'last': self.safe_string(ticker, 'lastPrice'), + 'previousClose': None, + 'change': None, + 'percentage': self.safe_string(ticker, 'priceChange24hr'), + 'average': None, + 'baseVolume': self.safe_string(ticker, 'volume24hr'), + 'quoteVolume': None, + 'info': ticker, + }, market) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/public/get_ticker_data.md + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + response = self.publicGetTickersPair(self.extend(request, params)) + ticker = self.safe_dict(response, 'data', {}) + # + # { + # "data":{ + # "pair":"btc_twd", + # "lastPrice":"1182449.00000000", + # "isBuyer":false, + # "priceChange24hr":"-1.99", + # "volume24hr":"9.13089740", + # "high24hr":"1226097.00000000", + # "low24hr":"1181000.00000000" + # } + # } + # + return self.parse_ticker(ticker, market) + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/public/get_ticker_data.md + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + response = self.publicGetTickers() + tickers = self.safe_list(response, 'data', []) + # + # { + # "data":[ + # { + # "pair":"xrp_twd", + # "lastPrice":"21.26110000", + # "isBuyer":false, + # "priceChange24hr":"-6.53", + # "volume24hr":"102846.47084802", + # "high24hr":"23.24460000", + # "low24hr":"21.13730000" + # } + # ] + # } + # + return self.parse_tickers(tickers, symbols) + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/public/get_orderbook_data.md + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + if limit is not None: + request['limit'] = limit + response = self.publicGetOrderBookPair(self.extend(request, params)) + # + # { + # "bids":[ + # { + # "price":"1175271", + # "amount":"0.00022804", + # "count":1, + # "total":"0.00022804" + # } + # ], + # "asks":[ + # { + # "price":"1176906", + # "amount":"0.0496", + # "count":1, + # "total":"0.0496" + # } + # ] + # } + # + return self.parse_order_book(response, market['symbol'], None, 'bids', 'asks', 'price', 'amount') + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades + # { + # "timestamp":1644651458, + # "price":"1180785.00000000", + # "amount":"0.00020000", + # "isBuyer":false + # } + # + # fetchMyTrades + # { + # "tradeId":"5685030251", + # "orderId":"9669168142", + # "price":"11821.8", + # "action":"SELL", + # "baseAmount":"0.01", + # "quoteAmount":"118.218", + # "fee":"0.236436", + # "feeSymbol":"BNB", + # "isTaker":true, + # "timestamp":1644905714862, + # "createdTimestamp":1644905714862 + # } + # + id = self.safe_string(trade, 'tradeId') + orderId = self.safe_string(trade, 'orderId') + timestamp = None + if id is None: + timestamp = self.safe_timestamp(trade, 'timestamp') + else: + timestamp = self.safe_integer(trade, 'timestamp') + marketId = self.safe_string(trade, 'pair') + market = self.safe_market(marketId, market) + symbol = self.safe_string(market, 'symbol') + price = self.safe_string(trade, 'price') + type = self.safe_string_lower(trade, 'type') + side = self.safe_string_lower(trade, 'action') + if side is None: + isBuyer = self.safe_bool(trade, 'isBuyer') + if isBuyer: + side = 'buy' + else: + side = 'sell' + amount = self.safe_string(trade, 'amount') + if amount is None: + amount = self.safe_string(trade, 'baseAmount') + fee = None + feeAmount = self.safe_string(trade, 'fee') + feeSymbol = self.safe_currency_code(self.safe_string(trade, 'feeSymbol')) + if feeAmount is not None: + fee = { + 'cost': feeAmount, + 'currency': feeSymbol, + 'rate': None, + } + isTaker = self.safe_bool(trade, 'isTaker') + takerOrMaker = None + if isTaker is not None: + if isTaker: + takerOrMaker = 'taker' + else: + takerOrMaker = 'maker' + return self.safe_trade({ + 'id': id, + 'info': trade, + 'order': orderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'takerOrMaker': takerOrMaker, + 'type': type, + 'side': side, + 'price': price, + 'amount': amount, + 'cost': None, + 'fee': fee, + }, market) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/public/get_trades_data.md + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + response = self.publicGetTradesPair(self.extend(request, params)) + trades = self.safe_list(response, 'data', []) + # + # { + # "data":[ + # { + # "timestamp":1644651458, + # "price":"1180785.00000000", + # "amount":"0.00020000", + # "isBuyer":false + # } + # ] + # } + # + return self.parse_trades(trades, market, since, limit) + + def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/public/get_limitations_and_fees.md + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + self.load_markets() + response = self.publicGetProvisioningLimitationsAndFees(params) + tradingFeeRate = self.safe_dict(response, 'tradingFeeRate', {}) + first = self.safe_value(tradingFeeRate, 0) + # + # { + # "tradingFeeRate":[ + # { + # "rank":0, + # "twdVolumeSymbol":"\u003c", + # "twdVolume":"3000000", + # "bitoAmountSymbol":"\u003c", + # "bitoAmount":"7500", + # "makerFee":"0.001", + # "takerFee":"0.002", + # "makerBitoFee":"0.0008", + # "takerBitoFee":"0.0016" + # } + # ], + # "orderFeesAndLimitations":[ + # { + # "pair":"BTC/TWD", + # "minimumOrderAmount":"0.0001", + # "minimumOrderAmountBase":"BTC", + # "minimumOrderNumberOfDigits":"0" + # } + # ], + # "restrictionsOfWithdrawalFees":[ + # { + # "currency":"TWD", + # "fee":"15", + # "minimumTradingAmount":"100", + # "maximumTradingAmount":"1000000", + # "dailyCumulativeMaximumAmount":"2000000", + # "remarks":"", + # "protocol":"" + # } + # ], + # "cryptocurrencyDepositFeeAndConfirmation":[ + # { + # "currency":"TWD", + # "generalDepositFees":"0", + # "blockchainConfirmationRequired":"" + # } + # ], + # "ttCheckFeesAndLimitationsLevel1":[ + # { + # "currency":"TWD", + # "redeemDailyCumulativeMaximumAmount":"", + # "generateMinimumTradingAmount":"", + # "generateMaximumTradingAmount":"", + # "generateDailyCumulativeMaximumAmount":"" + # } + # ], + # "ttCheckFeesAndLimitationsLevel2":[ + # { + # "currency":"TWD", + # "redeemDailyCumulativeMaximumAmount":"20000000", + # "generateMinimumTradingAmount":"30", + # "generateMaximumTradingAmount":"10000000", + # "generateDailyCumulativeMaximumAmount":"10000000" + # } + # ] + # } + # + result: dict = {} + maker = self.safe_number(first, 'makerFee') + taker = self.safe_number(first, 'takerFee') + for i in range(0, len(self.symbols)): + symbol = self.symbols[i] + result[symbol] = { + 'info': first, + 'symbol': symbol, + 'maker': maker, + 'taker': taker, + 'percentage': True, + 'tierBased': True, + } + return result + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + return [ + self.safe_integer(ohlcv, 'timestamp'), + self.safe_number(ohlcv, 'open'), + self.safe_number(ohlcv, 'high'), + self.safe_number(ohlcv, 'low'), + self.safe_number(ohlcv, 'close'), + self.safe_number(ohlcv, 'volume'), + ] + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/public/get_ohlc_data.md + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + market = self.market(symbol) + resolution = self.safe_string(self.timeframes, timeframe, timeframe) + request: dict = { + 'pair': market['id'], + 'resolution': resolution, + } + # we need to have a limit argument because "to" and "from" are required + if limit is None: + limit = 500 + else: + limit = min(limit, 75000) # supports slightly more than 75k candles atm, but limit here to avoid errors + timeframeInSeconds = self.parse_timeframe(timeframe) + alignedSince = None + if since is None: + request['to'] = self.seconds() + request['from'] = request['to'] - (limit * timeframeInSeconds) + else: + timeframeInMilliseconds = timeframeInSeconds * 1000 + alignedSince = int(math.floor(since / timeframeInMilliseconds)) * timeframeInMilliseconds + request['from'] = int(math.floor(since / 1000)) + request['to'] = self.sum(request['from'], limit * timeframeInSeconds) + response = self.publicGetTradingHistoryPair(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + # + # { + # "data":[ + # { + # "timestamp":1644581100000, + # "open":"1214737", + # "high":"1215110", + # "low":"1214737", + # "close":"1215110", + # "volume":"0.08423959" + # } + # ] + # } + # + sparse = self.parse_ohlcvs(data, market, timeframe, since, limit) + return self.insert_missing_candles(sparse, timeframeInSeconds, alignedSince, limit) + + def insert_missing_candles(self, candles, distance, since, limit): + # the exchange doesn't send zero volume candles so we emulate them instead + # otherwise sending a limit arg leads to unexpected results + length = len(candles) + if length == 0: + return candles + result = [] + copyFrom = candles[0] + timestamp = None + if since is None: + timestamp = copyFrom[0] + else: + timestamp = since + i = 0 + candleLength = len(candles) + resultLength = 0 + while((resultLength < limit) and (i < candleLength)): + candle = candles[i] + if candle[0] == timestamp: + result.append(candle) + i = self.sum(i, 1) + else: + copy = self.array_concat([], copyFrom) + copy[0] = timestamp + # set open, high, low to close + copy[1] = copy[4] + copy[2] = copy[4] + copy[3] = copy[4] + copy[5] = self.parse_number('0') + result.append(copy) + timestamp = self.sum(timestamp, distance * 1000) + resultLength = len(result) + copyFrom = result[resultLength - 1] + return result + + def parse_balance(self, response) -> Balances: + # + # [{ + # "currency":"twd", + # "amount":"0", + # "available":"0", + # "stake":"0", + # "tradable":true + # }] + # + result: dict = { + 'info': response, + } + for i in range(0, len(response)): + balance = response[i] + currencyId = self.safe_string(balance, 'currency') + code = self.safe_currency_code(currencyId) + amount = self.safe_string(balance, 'amount') + available = self.safe_string(balance, 'available') + account: dict = { + 'free': available, + 'total': amount, + } + result[code] = account + return self.safe_balance(result) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/private/get_account_balance.md + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + response = self.privateGetAccountsBalance(params) + balances = self.safe_list(response, 'data', []) + # + # { + # "data":[ + # { + # "currency":"twd", + # "amount":"0", + # "available":"0", + # "stake":"0", + # "tradable":true + # } + # ] + # } + # + return self.parse_balance(balances) + + def parse_order_status(self, status: Str): + statuses: dict = { + '-1': 'open', + '0': 'open', + '1': 'open', + '2': 'closed', + '3': 'closed', + '4': 'canceled', + '6': 'canceled', + } + return self.safe_string(statuses, status, None) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder + # { + # "orderId": "2220595581", + # "timestamp": "1644896744886", + # "action": "SELL", + # "amount": "0.01", + # "price": "15000", + # "timeInForce": "GTC" + # } + # + # fetchOrder + # { + # "id":"8777138788", + # "pair":"bnb_twd", + # "price":"16000", + # "avgExecutionPrice":"0", + # "action":"SELL", + # "type":"LIMIT", + # "timestamp":1644899002598, + # "status":4, + # "originalAmount":"0.01", + # "remainingAmount":"0.01", + # "executedAmount":"0", + # "fee":"0", + # "feeSymbol":"twd", + # "bitoFee":"0", + # "total":"0", + # "seq":"BNBTWD548774666", + # "timeInForce":"GTC", + # "createdTimestamp":1644898944074, + # "updatedTimestamp":1644899002598 + # } + # + id = self.safe_string_2(order, 'id', 'orderId') + timestamp = self.safe_integer_2(order, 'timestamp', 'createdTimestamp') + side = self.safe_string(order, 'action') + side = side.lower() + amount = self.safe_string_2(order, 'amount', 'originalAmount') + price = self.safe_string(order, 'price') + marketId = self.safe_string(order, 'pair') + market = self.safe_market(marketId, market, '_') + symbol = self.safe_string(market, 'symbol') + orderStatus = self.safe_string(order, 'status') + status = self.parse_order_status(orderStatus) + type = self.safe_string_lower(order, 'type') + average = self.safe_string(order, 'avgExecutionPrice') + filled = self.safe_string(order, 'executedAmount') + remaining = self.safe_string(order, 'remainingAmount') + timeInForce = self.safe_string(order, 'timeInForce') + postOnly = None + if timeInForce == 'POST_ONLY': + postOnly = True + fee = None + feeAmount = self.safe_string(order, 'fee') + feeSymbol = self.safe_currency_code(self.safe_string(order, 'feeSymbol')) + if Precise.string_gt(feeAmount, '0'): + fee = { + 'currency': feeSymbol, + 'cost': feeAmount, + } + return self.safe_order({ + 'id': id, + 'clientOrderId': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': self.safe_integer(order, 'updatedTimestamp'), + 'symbol': symbol, + 'type': type, + 'timeInForce': timeInForce, + 'postOnly': postOnly, + 'side': side, + 'price': price, + 'triggerPrice': None, + 'amount': amount, + 'cost': None, + 'average': average, + 'filled': filled, + 'remaining': remaining, + 'status': status, + 'fee': fee, + 'trades': None, + 'info': order, + }, market) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/private/create_an_order.md + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param dict [params.triggerPrice]: the price at which a trigger order is triggered at + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'type': type, + 'pair': market['id'], + 'action': side, + 'amount': self.amount_to_precision(symbol, amount), + 'timestamp': self.milliseconds(), + } + orderType = type.upper() + if orderType == 'LIMIT': + request['price'] = self.price_to_precision(symbol, price) + if orderType == 'STOP_LIMIT': + request['price'] = self.price_to_precision(symbol, price) + triggerPrice = self.safe_value_2(params, 'triggerPrice', 'stopPrice') + params = self.omit(params, ['triggerPrice', 'stopPrice']) + if triggerPrice is None: + raise InvalidOrder(self.id + ' createOrder() requires a triggerPrice parameter for ' + orderType + ' orders') + else: + request['stopPrice'] = self.price_to_precision(symbol, triggerPrice) + condition = self.safe_string(params, 'condition') + if condition is None: + raise InvalidOrder(self.id + ' createOrder() requires a condition parameter for ' + orderType + ' orders') + else: + request['condition'] = condition + postOnly = self.is_post_only(orderType == 'MARKET', None, params) + if postOnly: + request['timeInForce'] = 'POST_ONLY' + response = self.privatePostOrdersPair(self.extend(request, params)) + # + # { + # "orderId": "2220595581", + # "timestamp": "1644896744886", + # "action": "SELL", + # "amount": "0.01", + # "price": "15000", + # "timeInForce": "GTC" + # } + # + return self.parse_order(response, market) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/private/cancel_an_order.md + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'id': id, + 'pair': market['id'], + } + response = self.privateDeleteOrdersPairId(self.extend(request, params)) + # + # { + # "orderId":"8777138788", + # "action":"SELL", + # "timestamp":1644899002465, + # "price":"16000", + # "amount":"0.01" + # } + # + return self.parse_order(response, market) + + def parse_cancel_orders(self, data): + dataKeys = list(data.keys()) + orders = [] + for i in range(0, len(dataKeys)): + marketId = dataKeys[i] + orderIds = data[marketId] + for j in range(0, len(orderIds)): + orders.append(self.safe_order({ + 'info': orderIds[j], + 'id': orderIds[j], + 'symbol': self.safe_symbol(marketId), + })) + return orders + + def cancel_orders(self, ids: List[str], symbol: Str = None, params={}) -> List[Order]: + """ + cancel multiple orders + + https://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/private/cancel_batch_orders.md + + :param str[] ids: order ids + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrders() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + id = market['uppercaseId'] + request: dict = {} + request[id] = ids + response = self.privatePutOrders(self.extend(request, params)) + # + # { + # "data":{ + # "BNB_TWD":[ + # "5236347105", + # "359488711" + # ] + # } + # } + # + data = self.safe_dict(response, 'data') + return self.parse_cancel_orders(data) + + def cancel_all_orders(self, symbol: Str = None, params={}) -> List[Order]: + """ + cancel all open orders + + https://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/private/cancel_all_orders.md + + :param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + self.load_markets() + request: dict = { + # 'pair': market['id'], # optional + } + response = None + if symbol is not None: + market = self.market(symbol) + request['pair'] = market['id'] + response = self.privateDeleteOrdersPair(self.extend(request, params)) + else: + response = self.privateDeleteOrdersAll(self.extend(request, params)) + data = self.safe_dict(response, 'data', {}) + # + # { + # "data":{ + # "BNB_TWD":[ + # "9515988421", + # "4639130027" + # ] + # } + # } + # + return self.parse_cancel_orders(data) + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/private/get_an_order_data.md + + :param str id: the order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrder() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'orderId': id, + 'pair': market['id'], + } + response = self.privateGetOrdersPairOrderId(self.extend(request, params)) + # + # { + # "id":"8777138788", + # "pair":"bnb_twd", + # "price":"16000", + # "avgExecutionPrice":"0", + # "action":"SELL", + # "type":"LIMIT", + # "timestamp":1644899002598, + # "status":4, + # "originalAmount":"0.01", + # "remainingAmount":"0.01", + # "executedAmount":"0", + # "fee":"0", + # "feeSymbol":"twd", + # "bitoFee":"0", + # "total":"0", + # "seq":"BNBTWD548774666", + # "timeInForce":"GTC", + # "createdTimestamp":1644898944074, + # "updatedTimestamp":1644899002598 + # } + # + return self.parse_order(response, market) + + def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/private/get_orders_data.md + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrders() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + # 'startTimestamp': 0, + # 'endTimestamp': 0, + # 'statusKind': '', + # 'orderId': '', + } + if since is not None: + request['startTimestamp'] = since + if limit is not None: + request['limit'] = limit + response = self.privateGetOrdersAllPair(self.extend(request, params)) + orders = self.safe_list(response, 'data', []) + if orders is None: + orders = [] + # + # { + # "data":[ + # { + # "id":"2220595581", + # "pair":"bnb_twd", + # "price":"15000", + # "avgExecutionPrice":"0", + # "action":"SELL", + # "type":"LIMIT", + # "createdTimestamp":1644896744886, + # "updatedTimestamp":1644898706236, + # "status":4, + # "originalAmount":"0.01", + # "remainingAmount":"0.01", + # "executedAmount":"0", + # "fee":"0", + # "feeSymbol":"twd", + # "bitoFee":"0", + # "total":"0", + # "seq":"BNBTWD8540871774", + # "timeInForce":"GTC" + # } + # ] + # } + # + return self.parse_orders(orders, market, since, limit) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all unfilled currently open orders + + https://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/private/get_open_orders_data.md + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['pair'] = market['id'] + response = self.privateGetOrdersOpen(self.extend(request, params)) + orders = self.safe_list(response, 'data', []) + return self.parse_orders(orders, market, since, limit) + + def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/private/get_orders_data.md + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + request: dict = { + 'statusKind': 'DONE', + } + return self.fetch_orders(symbol, since, limit, self.extend(request, params)) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/private/get_trades_data.md + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMyTrades() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + response = self.privateGetOrdersTradesPair(self.extend(request, params)) + trades = self.safe_list(response, 'data', []) + # + # { + # "data":[ + # { + # "tradeId":"5685030251", + # "orderId":"9669168142", + # "price":"11821.8", + # "action":"SELL", + # "baseAmount":"0.01", + # "quoteAmount":"118.218", + # "fee":"0.236436", + # "feeSymbol":"BNB", + # "isTaker":true, + # "timestamp":1644905714862, + # "createdTimestamp":1644905714862 + # } + # ] + # } + # + return self.parse_trades(trades, market, since, limit) + + def parse_transaction_status(self, status: Str): + states: dict = { + 'COMPLETE': 'ok', + 'INVALID': 'failed', + 'PROCESSING': 'pending', + 'WAIT_PROCESS': 'pending', + 'FAILED': 'failed', + 'EXPIRED': 'failed', + 'CANCELLED': 'failed', + 'EMAIL_VERIFICATION': 'pending', + 'WAIT_CONFIRMATION': 'pending', + } + return self.safe_string(states, status, status) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fetchDeposits + # + # { + # "serial": "20220214X766799", + # "timestamp": "1644833015053", + # "address": "bnb1xml62k5a9dcewgc542fha75fyxdcp0zv8eqfsh", + # "amount": "0.20000000", + # "fee": "0.00000000", + # "total": "0.20000000", + # "status": "COMPLETE", + # "txid": "A3CC4F6828CC752B9F3737F48B5826B9EC2857040CB5141D0CC955F7E53DB6D9", + # "message": "778553959", + # "protocol": "MAIN", + # "id": "2905906537" + # } + # + # fetchWithdrawals or fetchWithdraw + # + # { + # "serial": "20220215BW14069838", + # "timestamp": "1644907716044", + # "address": "TKrwMaZaGiAvtXCFT41xHuusNcs4LPWS7w", + # "amount": "8.00000000", + # "fee": "2.00000000", + # "total": "10.00000000", + # "status": "COMPLETE", + # "txid": "50bf250c71a582f40cf699fb58bab978437ea9bdf7259ff8072e669aab30c32b", + # "protocol": "TRX", + # "id": "9925310345" + # } + # + # withdraw + # + # { + # "serial": "20220215BW14069838", + # "currency": "USDT", + # "protocol": "TRX", + # "address": "TKrwMaZaGiAvtXCFT41xHuusNcs4LPWS7w", + # "amount": "8", + # "fee": "2", + # "total": "10" + # } + # + currencyId = self.safe_string(transaction, 'coin') + code = self.safe_currency_code(currencyId, currency) + timestamp = self.safe_integer(transaction, 'timestamp') + address = self.safe_string(transaction, 'address') + tag = self.safe_string(transaction, 'message') + status = self.safe_string(transaction, 'status') + networkId = self.safe_string(transaction, 'protocol') + if networkId == 'MAIN': + networkId = code + return { + 'info': transaction, + 'id': self.safe_string(transaction, 'serial'), + 'txid': self.safe_string(transaction, 'txid'), + 'type': None, + 'currency': code, + 'network': self.network_id_to_code(networkId), + 'amount': self.safe_number(transaction, 'total'), + 'status': self.parse_transaction_status(status), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'address': address, + 'addressFrom': None, + 'addressTo': address, + 'tag': tag, + 'tagFrom': None, + 'tagTo': tag, + 'updated': None, + 'comment': None, + 'internal': None, + 'fee': { + 'currency': code, + 'cost': self.safe_number(transaction, 'fee'), + 'rate': None, + }, + } + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/private/get_deposit_invoices_data.md + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + if code is None: + raise ArgumentsRequired(self.id + ' fetchDeposits() requires the code argument') + self.load_markets() + currency = self.safe_currency(code) + request: dict = { + 'currency': currency['id'], + # 'endTimestamp': 0, + # 'id': '', + # 'statuses': '', # 'ROCESSING,COMPLETE,INVALID,WAIT_PROCESS,CANCELLED,FAILED' + } + if since is not None: + request['startTimestamp'] = since + if limit is not None: + request['limit'] = limit + response = self.privateGetWalletDepositHistoryCurrency(self.extend(request, params)) + result = self.safe_list(response, 'data', []) + # + # { + # "data":[ + # { + # "serial":"20220214X766799", + # "timestamp":"1644833015053", + # "address":"bnb1xml62k5a9dcewgc542fha75fyxdcp0zv8eqfsh", + # "amount":"0.20000000", + # "fee":"0.00000000", + # "total":"0.20000000", + # "status":"COMPLETE", + # "txid":"A3CC4F6828CC752B9F3737F48B5826B9EC2857040CB5141D0CC955F7E53DB6D9", + # "message":"778553959", + # "protocol":"MAIN", + # "id":"2905906537" + # } + # ] + # } + # + return self.parse_transactions(result, currency, since, limit, {'type': 'deposit'}) + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/private/get_withdraw_invoices_data.md + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + if code is None: + raise ArgumentsRequired(self.id + ' fetchWithdrawals() requires the code argument') + self.load_markets() + currency = self.safe_currency(code) + request: dict = { + 'currency': currency['id'], + # 'endTimestamp': 0, + # 'id': '', + # 'statuses': '', # 'PROCESSING,COMPLETE,EXPIRED,INVALID,WAIT_PROCESS,WAIT_CONFIRMATION,EMAIL_VERIFICATION,CANCELLED' + } + if since is not None: + request['startTimestamp'] = since + if limit is not None: + request['limit'] = limit + response = self.privateGetWalletWithdrawHistoryCurrency(self.extend(request, params)) + result = self.safe_list(response, 'data', []) + # + # { + # "data":[ + # { + # "serial":"20220215BW14069838", + # "timestamp":"1644907716044", + # "address":"TKrwMaZaGiAvtXCFT41xHuusNcs4LPWS7w", + # "amount":"8.00000000", + # "fee":"2.00000000", + # "total":"10.00000000", + # "status":"COMPLETE", + # "txid":"50bf250c71a582f40cf699fb58bab978437ea9bdf7259ff8072e669aab30c32b", + # "protocol":"TRX", + # "id":"9925310345" + # } + # ] + # } + # + return self.parse_transactions(result, currency, since, limit, {'type': 'withdrawal'}) + + def fetch_withdrawal(self, id: str, code: Str = None, params={}): + """ + fetch data on a currency withdrawal via the withdrawal id + + https://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/private/get_an_withdraw_invoice_data.md + + :param str id: withdrawal id + :param str code: unified currency code of the currency withdrawn, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + if code is None: + raise ArgumentsRequired(self.id + ' fetchWithdrawal() requires the code argument') + self.load_markets() + currency = self.safe_currency(code) + request: dict = { + 'serial': id, + 'currency': currency['id'], + } + response = self.privateGetWalletWithdrawCurrencySerial(self.extend(request, params)) + result = self.safe_dict(response, 'data', {}) + # + # { + # "data":{ + # "serial":"20220215BW14069838", + # "address":"TKrwMaZaGiAvtXCFT41xHuusNcs4LPWS7w", + # "amount":"8.00000000", + # "fee":"2.00000000", + # "total":"10.00000000", + # "status":"COMPLETE", + # "txid":"50bf250c71a582f40cf699fb58bab978437ea9bdf7259ff8072e669aab30c32b", + # "protocol":"TRX", + # "id":"9925310345", + # "timestamp":"1644907716044" + # } + # } + # + return self.parse_transaction(result, currency) + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/private/create_an_withdraw_invoice.md + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.load_markets() + self.check_address(address) + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + 'amount': self.number_to_string(amount), + 'address': address, + } + if 'network' in params: + networks = self.safe_dict(self.options, 'networks', {}) + requestedNetwork = self.safe_string_upper(params, 'network') + params = self.omit(params, ['network']) + networkId = self.safe_string(networks, requestedNetwork) + if networkId is None: + raise ExchangeError(self.id + ' invalid network ' + requestedNetwork) + request['protocol'] = networkId + if tag is not None: + request['message'] = tag + response = self.privatePostWalletWithdrawCurrency(self.extend(request, params)) + result = self.safe_dict(response, 'data', {}) + # + # { + # "data":{ + # "serial":"20220215BW14069838", + # "currency":"USDT", + # "protocol":"TRX", + # "address":"TKrwMaZaGiAvtXCFT41xHuusNcs4LPWS7w", + # "amount":"8", + # "fee":"2", + # "total":"10" + # } + # } + # + return self.parse_transaction(result, currency) + + def parse_deposit_withdraw_fee(self, fee, currency: Currency = None): + # { + # "currency":"eth", + # "withdrawFee":"0.007", + # "minWithdraw":"0.001", + # "maxWithdraw":"1000", + # "maxDailyWithdraw":"2000", + # "withdraw":true, + # "deposit":true, + # "depositConfirmation":"12" + # } + return { + 'info': fee, + 'withdraw': { + 'fee': self.safe_number(fee, 'withdrawFee'), + 'percentage': False, + }, + 'deposit': { + 'fee': None, + 'percentage': None, + }, + 'networks': {}, + } + + def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}): + """ + fetch deposit and withdraw fees + + https://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/public/get_currency_info.md + + :param str[]|None codes: list of unified currency codes + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `fee structures ` + """ + self.load_markets() + response = self.publicGetProvisioningCurrencies(params) + # + # { + # "data":[ + # { + # "currency":"eth", + # "withdrawFee":"0.007", + # "minWithdraw":"0.001", + # "maxWithdraw":"1000", + # "maxDailyWithdraw":"2000", + # "withdraw":true, + # "deposit":true, + # "depositConfirmation":"12" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_deposit_withdraw_fees(data, codes, 'currency') + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = '/' + self.implode_params(path, params) + query = self.omit(params, self.extract_params(path)) + if headers is None: + headers = {} + headers['X-BITOPRO-API'] = 'ccxt' + if api == 'private': + self.check_required_credentials() + if method == 'POST' or method == 'PUT': + body = self.json(params) + payload = self.string_to_base64(body) + signature = self.hmac(self.encode(payload), self.encode(self.secret), hashlib.sha384) + headers['X-BITOPRO-APIKEY'] = self.apiKey + headers['X-BITOPRO-PAYLOAD'] = payload + headers['X-BITOPRO-SIGNATURE'] = signature + elif method == 'GET' or method == 'DELETE': + if query: + url += '?' + self.urlencode(query) + nonce = self.milliseconds() + rawData: dict = { + 'nonce': nonce, + } + data = self.json(rawData) + payload = self.string_to_base64(data) + signature = self.hmac(self.encode(payload), self.encode(self.secret), hashlib.sha384) + headers['X-BITOPRO-APIKEY'] = self.apiKey + headers['X-BITOPRO-PAYLOAD'] = payload + headers['X-BITOPRO-SIGNATURE'] = signature + elif api == 'public' and method == 'GET': + if query: + url += '?' + self.urlencode(query) + url = self.urls['api']['rest'] + url + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None # fallback to the default error handler + if code >= 200 and code < 300: + return None + feedback = self.id + ' ' + body + error = self.safe_string(response, 'error') + self.throw_exactly_matched_exception(self.exceptions['exact'], error, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], error, feedback) + raise ExchangeError(feedback) # unknown message diff --git a/ccxt/bitrue.py b/ccxt/bitrue.py new file mode 100644 index 0000000..cf78e94 --- /dev/null +++ b/ccxt/bitrue.py @@ -0,0 +1,3195 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.bitrue import ImplicitAPI +import hashlib +import json +from ccxt.base.types import Any, Balances, Currencies, Currency, Int, MarginModification, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, Transaction, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import AccountSuspended +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import OrderImmediatelyFillable +from ccxt.base.errors import NotSupported +from ccxt.base.errors import DDoSProtection +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.errors import OnMaintenance +from ccxt.base.errors import InvalidNonce +from ccxt.base.decimal_to_precision import TRUNCATE +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class bitrue(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(bitrue, self).describe(), { + 'id': 'bitrue', + 'name': 'Bitrue', + 'countries': ['SG'], # Singapore, Malta + 'rateLimit': 10, + 'certified': False, + 'version': 'v1', + 'pro': True, + # new metainfo interface + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': True, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createMarketBuyOrderWithCost': True, + 'createMarketOrderWithCost': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createOrderWithTakeProfitAndStopLoss': False, + 'createOrderWithTakeProfitAndStopLossWs': False, + 'createReduceOnlyOrder': True, + 'createStopLimitOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'fetchBalance': True, + 'fetchBidsAsks': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': False, + 'fetchDeposits': True, + 'fetchDepositsWithdrawals': False, + 'fetchDepositWithdrawFee': 'emulated', + 'fetchDepositWithdrawFees': True, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': False, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchStatus': True, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': False, + 'fetchTransactionFees': False, + 'fetchTransactions': False, + 'fetchTransfers': True, + 'fetchVolatilityHistory': False, + 'fetchWithdrawals': True, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'setLeverage': True, + 'setMargin': True, + 'setMarginMode': False, + 'setPositionMode': False, + 'transfer': True, + 'withdraw': True, + }, + 'timeframes': { + '1m': '1m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1H', + '2h': '2H', + '4h': '4H', + '1d': '1D', + '1w': '1W', + }, + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/67abe346-1273-461a-bd7c-42fa32907c8e', + 'api': { + 'spot': 'https://www.bitrue.com/api', + 'fapi': 'https://fapi.bitrue.com/fapi', + 'dapi': 'https://fapi.bitrue.com/dapi', + 'kline': 'https://www.bitrue.com/kline-api', + }, + 'www': 'https://www.bitrue.com', + 'referral': 'https://www.bitrue.com/affiliate/landing?cn=600000&inviteCode=EZWETQE', + 'doc': [ + 'https://github.com/Bitrue-exchange/bitrue-official-api-docs', + 'https://www.bitrue.com/api-docs', + ], + 'fees': 'https://bitrue.zendesk.com/hc/en-001/articles/4405479952537', + }, + # from spotV1PublicGetExchangeInfo: + # general 25000 weight in 1 minute per IP. = 416.66 per second a weight of 0.24 for 1 + # orders 750 weight in 6 seconds per IP. = 125 per second a weight of 0.8 for 1 + # orders 200 weight in 10 seconds per User. = 20 per second a weight of 5 for 1 + # withdraw 3000 weight in 1 hour per User. = 0.833 per second a weight of 120 for 1 + # withdraw 1000 weight in 1 day per User. = 0.011574 per second a weight of 8640 for 1 + 'api': { + 'spot': { + 'kline': { + 'public': { + 'get': { + 'public.json': 0.24, + 'public{currency}.json': 0.24, + }, + }, + }, + 'v1': { + 'public': { + 'get': { + 'ping': 0.24, + 'time': 0.24, + 'exchangeInfo': 0.24, + 'depth': {'cost': 1, 'byLimit': [[100, 0.24], [500, 1.2], [1000, 2.4]]}, + 'trades': 0.24, + 'historicalTrades': 1.2, + 'aggTrades': 0.24, + 'ticker/24hr': {'cost': 0.24, 'noSymbol': 9.6}, + 'ticker/price': 0.24, + 'ticker/bookTicker': 0.24, + 'market/kline': 0.24, + }, + }, + 'private': { + 'get': { + 'order': 5, + 'openOrders': 5, + 'allOrders': 25, + 'account': 25, + 'myTrades': 25, + 'etf/net-value/{symbol}': 0.24, + 'withdraw/history': 120, + 'deposit/history': 120, + }, + 'post': { + 'order': 5, + 'withdraw/commit': 120, + }, + 'delete': { + 'order': 5, + }, + }, + }, + 'v2': { + 'private': { + 'get': { + 'myTrades': 1.2, + }, + }, + }, + }, + 'fapi': { + 'v1': { + 'public': { + 'get': { + 'ping': 0.24, + 'time': 0.24, + 'contracts': 0.24, + 'depth': 0.24, + 'ticker': 0.24, + 'klines': 0.24, + }, + }, + }, + 'v2': { + 'private': { + 'get': { + 'myTrades': 5, + 'openOrders': 5, + 'order': 5, + 'account': 5, + 'leverageBracket': 5, + 'commissionRate': 5, + 'futures_transfer_history': 5, + 'forceOrdersHistory': 5, + }, + 'post': { + 'positionMargin': 5, + 'level_edit': 5, + 'cancel': 5, + 'order': 25, + 'allOpenOrders': 5, + 'futures_transfer': 5, + }, + }, + }, + }, + 'dapi': { + 'v1': { + 'public': { + 'get': { + 'ping': 0.24, + 'time': 0.24, + 'contracts': 0.24, + 'depth': 0.24, + 'ticker': 0.24, + 'klines': 0.24, + }, + }, + }, + 'v2': { + 'private': { + 'get': { + 'myTrades': 5, + 'openOrders': 5, + 'order': 5, + 'account': 5, + 'leverageBracket': 5, + 'commissionRate': 5, + 'futures_transfer_history': 5, + 'forceOrdersHistory': 5, + }, + 'post': { + 'positionMargin': 5, + 'level_edit': 5, + 'cancel': 5, + 'order': 5, + 'allOpenOrders': 5, + 'futures_transfer': 5, + }, + }, + }, + }, + }, + 'fees': { + 'trading': { + 'feeSide': 'get', + 'tierBased': False, + 'percentage': True, + 'taker': self.parse_number('0.00098'), + 'maker': self.parse_number('0.00098'), + }, + 'future': { + 'trading': { + 'feeSide': 'quote', + 'tierBased': True, + 'percentage': True, + 'taker': self.parse_number('0.000400'), + 'maker': self.parse_number('0.000200'), + 'tiers': { + 'taker': [ + [self.parse_number('0'), self.parse_number('0.000400')], + [self.parse_number('250'), self.parse_number('0.000400')], + [self.parse_number('2500'), self.parse_number('0.000350')], + [self.parse_number('7500'), self.parse_number('0.000320')], + [self.parse_number('22500'), self.parse_number('0.000300')], + [self.parse_number('50000'), self.parse_number('0.000270')], + [self.parse_number('100000'), self.parse_number('0.000250')], + [self.parse_number('200000'), self.parse_number('0.000220')], + [self.parse_number('400000'), self.parse_number('0.000200')], + [self.parse_number('750000'), self.parse_number('0.000170')], + ], + 'maker': [ + [self.parse_number('0'), self.parse_number('0.000200')], + [self.parse_number('250'), self.parse_number('0.000160')], + [self.parse_number('2500'), self.parse_number('0.000140')], + [self.parse_number('7500'), self.parse_number('0.000120')], + [self.parse_number('22500'), self.parse_number('0.000100')], + [self.parse_number('50000'), self.parse_number('0.000080')], + [self.parse_number('100000'), self.parse_number('0.000060')], + [self.parse_number('200000'), self.parse_number('0.000040')], + [self.parse_number('400000'), self.parse_number('0.000020')], + [self.parse_number('750000'), self.parse_number('0')], + ], + }, + }, + }, + 'delivery': { + 'trading': { + 'feeSide': 'base', + 'tierBased': True, + 'percentage': True, + 'taker': self.parse_number('0.000500'), + 'maker': self.parse_number('0.000100'), + 'tiers': { + 'taker': [ + [self.parse_number('0'), self.parse_number('0.000500')], + [self.parse_number('250'), self.parse_number('0.000450')], + [self.parse_number('2500'), self.parse_number('0.000400')], + [self.parse_number('7500'), self.parse_number('0.000300')], + [self.parse_number('22500'), self.parse_number('0.000250')], + [self.parse_number('50000'), self.parse_number('0.000240')], + [self.parse_number('100000'), self.parse_number('0.000240')], + [self.parse_number('200000'), self.parse_number('0.000240')], + [self.parse_number('400000'), self.parse_number('0.000240')], + [self.parse_number('750000'), self.parse_number('0.000240')], + ], + 'maker': [ + [self.parse_number('0'), self.parse_number('0.000100')], + [self.parse_number('250'), self.parse_number('0.000080')], + [self.parse_number('2500'), self.parse_number('0.000050')], + [self.parse_number('7500'), self.parse_number('0.0000030')], + [self.parse_number('22500'), self.parse_number('0')], + [self.parse_number('50000'), self.parse_number('-0.000050')], + [self.parse_number('100000'), self.parse_number('-0.000060')], + [self.parse_number('200000'), self.parse_number('-0.000070')], + [self.parse_number('400000'), self.parse_number('-0.000080')], + [self.parse_number('750000'), self.parse_number('-0.000090')], + ], + }, + }, + }, + }, + # exchange-specific options + 'options': { + 'createMarketBuyOrderRequiresPrice': True, + 'fetchMarkets': { + 'types': ['spot', 'linear', 'inverse'], + }, + # 'fetchTradesMethod': 'publicGetAggTrades', # publicGetTrades, publicGetHistoricalTrades + 'fetchMyTradesMethod': 'v2PrivateGetMyTrades', # spotV1PrivateGetMyTrades + 'hasAlreadyAuthenticatedSuccessfully': False, + 'currencyToPrecisionRoundingMode': TRUNCATE, + 'recvWindow': 5 * 1000, # 5 sec, binance default + 'timeDifference': 0, # the difference between system clock and Binance clock + 'adjustForTimeDifference': False, # controls the adjustment logic upon instantiation + 'parseOrderToPrecision': False, # force amounts and costs in parseOrder to precision + 'newOrderRespType': { + 'market': 'FULL', # 'ACK' for order id, 'RESULT' for full order or 'FULL' for order with fills + 'limit': 'FULL', # we change it from 'ACK' by default to 'FULL'(returns immediately if limit is not hit) + }, + 'networks': { + 'ERC20': 'ETH', + 'TRC20': 'TRX', + 'AETERNITY': 'Aeternity', + 'AION': 'AION', + 'ALGO': 'Algorand', + 'ASK': 'ASK', + 'ATOM': 'ATOM', + 'AVAXC': 'AVAX C-Chain', + 'BCH': 'BCH', + 'BEP2': 'BEP2', + 'BEP20': 'BEP20', + 'Bitcoin': 'Bitcoin', + 'BRP20': 'BRP20', + 'ADA': 'Cardano', + 'CASINOCOIN': 'CasinoCoin', + 'CASINOCOIN-XRPL': 'CasinoCoin XRPL', + 'CONTENTOS': 'Contentos', + 'DASH': 'Dash', + 'DECOIN': 'Decoin', + 'DFI': 'DeFiChain', + 'DGB': 'DGB', + 'DIVI': 'Divi', + 'DOGE': 'dogecoin', + 'EOS': 'EOS', + 'ETC': 'ETC', + 'FILECOIN': 'Filecoin', + 'FREETON': 'FREETON', + 'HBAR': 'HBAR', + 'HEDERA': 'Hedera Hashgraph', + 'HRC20': 'HRC20', + 'ICON': 'ICON', + 'ICP': 'ICP', + 'IGNIS': 'Ignis', + 'INTERNETCOMPUTER': 'Internet Computer', + 'IOTA': 'IOTA', + 'KAVA': 'KAVA', + 'KSM': 'KSM', + 'LTC': 'LiteCoin', + 'LUNA': 'Luna', + 'MATIC': 'MATIC', + 'MOBILECOIN': 'Mobile Coin', + 'MONACOIN': 'MonaCoin', + 'XMR': 'Monero', + 'NEM': 'NEM', + 'NEP5': 'NEP5', + 'OMNI': 'OMNI', + 'PAC': 'PAC', + 'DOT': 'Polkadot', + 'RAVEN': 'Ravencoin', + 'SAFEX': 'Safex', + 'SOL': 'SOLANA', + 'SGB': 'Songbird', + 'XML': 'Stellar Lumens', + 'XYM': 'Symbol', + 'XTZ': 'Tezos', + 'theta': 'theta', + 'THETA': 'THETA', + 'VECHAIN': 'VeChain', + 'WANCHAIN': 'Wanchain', + 'XINFIN': 'XinFin Network', + 'XRP': 'XRP', + 'XRPL': 'XRPL', + 'ZIL': 'ZIL', + }, + 'defaultType': 'spot', + 'timeframes': { + 'spot': { + '1m': '1m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1H', + '2h': '2H', + '4h': '4H', + '12h': '12H', + '1d': '1D', + '1w': '1W', + }, + 'future': { + '1m': '1min', + '5m': '5min', + '15m': '15min', + '30m': '30min', + '1h': '1h', + '1d': '1day', + '1w': '1week', + '1M': '1month', + }, + }, + 'accountsByType': { + 'spot': 'wallet', + 'future': 'contract', + 'swap': 'contract', + 'funding': 'wallet', + 'fund': 'wallet', + 'contract': 'contract', + }, + }, + 'commonCurrencies': { + 'MIM': 'MIM Swarm', + }, + 'precisionMode': TICK_SIZE, + 'features': { + 'default': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerPriceType': None, + 'triggerDirection': None, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyRequiresPrice': True, # todo revise + 'marketBuyByCost': True, + 'selfTradePrevention': False, + 'iceberg': True, # todo implement + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 100000, + 'untilDays': 100000, + 'symbolRequired': True, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOrders': None, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 90, + 'daysBackCanceled': 1, + 'untilDays': 90, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOHLCV': { + 'limit': 1440, + }, + }, + 'spot': { + 'extends': 'default', + }, + 'forDerivatives': { + 'extends': 'default', + 'createOrder': { + 'marginMode': True, + 'leverage': True, + 'marketBuyRequiresPrice': False, + 'marketBuyByCost': False, + }, + 'fetchOHLCV': { + 'limit': 300, + }, + 'fetchClosedOrders': None, + }, + 'swap': { + 'linear': { + 'extends': 'forDerivatives', + }, + 'inverse': { + 'extends': 'forDerivatives', + }, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'exceptions': { + 'exact': { + 'System is under maintenance.': OnMaintenance, # {"code":1,"msg":"System is under maintenance."} + 'System abnormality': ExchangeError, # {"code":-1000,"msg":"System abnormality"} + 'You are not authorized to execute self request.': PermissionDenied, # {"msg":"You are not authorized to execute self request."} + 'API key does not exist': AuthenticationError, + 'Order would trigger immediately.': OrderImmediatelyFillable, + 'Stop price would trigger immediately.': OrderImmediatelyFillable, # {"code":-2010,"msg":"Stop price would trigger immediately."} + 'Order would immediately match and take.': OrderImmediatelyFillable, # {"code":-2010,"msg":"Order would immediately match and take."} + 'Account has insufficient balance for requested action.': InsufficientFunds, + 'Rest API trading is not enabled.': ExchangeNotAvailable, + "You don't have permission.": PermissionDenied, # {"msg":"You don't have permission.","success":false} + 'Market is closed.': ExchangeNotAvailable, # {"code":-1013,"msg":"Market is closed."} + 'Too many requests. Please try again later.': DDoSProtection, # {"msg":"Too many requests. Please try again later.","success":false} + '-1000': ExchangeNotAvailable, # {"code":-1000,"msg":"An unknown error occured while processing the request."} + '-1001': ExchangeNotAvailable, # 'Internal error; unable to process your request. Please try again.' + '-1002': AuthenticationError, # 'You are not authorized to execute self request.' + '-1003': RateLimitExceeded, # {"code":-1003,"msg":"Too much request weight used, current limit is 1200 request weight per 1 MINUTE. Please use the websocket for live updates to avoid polling the API."} + '-1013': InvalidOrder, # createOrder -> 'invalid quantity'/'invalid price'/MIN_NOTIONAL + '-1015': RateLimitExceeded, # 'Too many new orders; current limit is %s orders per %s.' + '-1016': ExchangeNotAvailable, # 'This service is no longer available.', + '-1020': BadRequest, # 'This operation is not supported.' + '-1021': InvalidNonce, # 'your time is ahead of server' + '-1022': AuthenticationError, # {"code":-1022,"msg":"Signature for self request is not valid."} + '-1100': BadRequest, # createOrder(symbol, 1, asdf) -> 'Illegal characters found in parameter 'price' + '-1101': BadRequest, # Too many parameters; expected %s and received %s. + '-1102': BadRequest, # Param %s or %s must be sent, but both were empty # {"code":-1102,"msg":"timestamp IllegalArgumentException.","data":null} + '-1103': BadRequest, # An unknown parameter was sent. + '-1104': BadRequest, # Not all sent parameters were read, read 8 parameters but was sent 9 + '-1105': BadRequest, # Parameter %s was empty. + '-1106': BadRequest, # Parameter %s sent when not required. + '-1111': BadRequest, # Precision is over the maximum defined for self asset. + '-1112': InvalidOrder, # No orders on book for symbol. + '-1114': BadRequest, # TimeInForce parameter sent when not required. + '-1115': BadRequest, # Invalid timeInForce. + '-1116': BadRequest, # Invalid orderType. + '-1117': BadRequest, # Invalid side. + '-1166': InvalidOrder, # {"code":"-1166","msg":"The leverage value of the order is inconsistent with the user contract configuration 5","data":null} + '-1118': BadRequest, # New client order ID was empty. + '-1119': BadRequest, # Original client order ID was empty. + '-1120': BadRequest, # Invalid interval. + '-1121': BadSymbol, # Invalid symbol. + '-1125': AuthenticationError, # This listenKey does not exist. + '-1127': BadRequest, # More than %s hours between startTime and endTime. + '-1128': BadRequest, # {"code":-1128,"msg":"Combination of optional parameters invalid."} + '-1130': BadRequest, # Data sent for paramter %s is not valid. + '-1131': BadRequest, # recvWindow must be less than 60000 + '-1160': InvalidOrder, # {"code":"-1160","msg":"Minimum order amount 10","data":null} + '-1156': InvalidOrder, # {"code":"-1156","msg":"The number of closed positions exceeds the total number of positions","data":null} + '-2008': AuthenticationError, # {"code":-2008,"msg":"Invalid Api-Key ID."} + '-2010': ExchangeError, # generic error code for createOrder -> 'Account has insufficient balance for requested action.', {"code":-2010,"msg":"Rest API trading is not enabled."}, etc... + '-2011': OrderNotFound, # cancelOrder(1, 'BTC/USDT') -> 'UNKNOWN_ORDER' + '-2013': OrderNotFound, # fetchOrder(1, 'BTC/USDT') -> 'Order does not exist' + '-2014': AuthenticationError, # {"code":-2014, "msg": "API-key format invalid."} + '-2015': AuthenticationError, # "Invalid API-key, IP, or permissions for action." + '-2017': InsufficientFunds, # {code":"-2017","msg":"Insufficient balance","data":null} + '-2019': InsufficientFunds, # {"code":-2019,"msg":"Margin is insufficient."} + '-3005': InsufficientFunds, # {"code":-3005,"msg":"Transferring out not allowed. Transfer out amount exceeds max amount."} + '-3006': InsufficientFunds, # {"code":-3006,"msg":"Your borrow amount has exceed maximum borrow amount."} + '-3008': InsufficientFunds, # {"code":-3008,"msg":"Borrow not allowed. Your borrow amount has exceed maximum borrow amount."} + '-3010': ExchangeError, # {"code":-3010,"msg":"Repay not allowed. Repay amount exceeds borrow amount."} + '-3015': ExchangeError, # {"code":-3015,"msg":"Repay amount exceeds borrow amount."} + '-3022': AccountSuspended, # You account's trading is banned. + '-4028': BadRequest, # {"code":-4028,"msg":"Leverage 100 is not valid"} + '-3020': InsufficientFunds, # {"code":-3020,"msg":"Transfer out amount exceeds max amount."} + '-3041': InsufficientFunds, # {"code":-3041,"msg":"Balance is not enough"} + '-5013': InsufficientFunds, # Asset transfer failed: insufficient balance" + '-11008': InsufficientFunds, # {"code":-11008,"msg":"Exceeding the account's maximum borrowable limit."} + '-4051': InsufficientFunds, # {"code":-4051,"msg":"Isolated balance insufficient."} + }, + 'broad': { + 'Insufficient account balance': InsufficientFunds, # {"code":-2010,"msg":"Insufficient account balance.","data":null} + 'has no operation privilege': PermissionDenied, + 'MAX_POSITION': InvalidOrder, # {"code":-2010,"msg":"Filter failure: MAX_POSITION"} + }, + }, + }) + + def nonce(self): + return self.milliseconds() - self.options['timeDifference'] + + def fetch_status(self, params={}): + """ + the latest known information on the availability of the exchange API + + https://github.com/Bitrue-exchange/Spot-official-api-docs#test-connectivity + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `status structure ` + """ + response = self.spotV1PublicGetPing(params) + # + # empty means working status. + # + # {} + # + keys = list(response.keys()) + keysLength = len(keys) + formattedStatus = 'maintenance' if keysLength else 'ok' + return { + 'status': formattedStatus, + 'updated': None, + 'eta': None, + 'url': None, + 'info': response, + } + + def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://github.com/Bitrue-exchange/Spot-official-api-docs#check-server-time + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = self.spotV1PublicGetTime(params) + # + # { + # "serverTime":1635467280514 + # } + # + return self.safe_integer(response, 'serverTime') + + def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = self.spotV1PublicGetExchangeInfo(params) + # + # { + # "timezone":"CTT", + # "serverTime":1635464889117, + # "rateLimits":[ + # {"rateLimitType":"REQUESTS_WEIGHT","interval":"MINUTES","limit":6000}, + # {"rateLimitType":"ORDERS","interval":"SECONDS","limit":150}, + # {"rateLimitType":"ORDERS","interval":"DAYS","limit":288000}, + # ], + # "exchangeFilters":[], + # "symbols":[ + # { + # "symbol":"SHABTC", + # "status":"TRADING", + # "baseAsset":"sha", + # "baseAssetPrecision":0, + # "quoteAsset":"btc", + # "quotePrecision":10, + # "orderTypes":["MARKET","LIMIT"], + # "icebergAllowed":false, + # "filters":[ + # {"filterType":"PRICE_FILTER","minPrice":"0.00000001349","maxPrice":"0.00000017537","priceScale":10}, + # {"filterType":"LOT_SIZE","minQty":"1.0","minVal":"0.00020","maxQty":"1000000000","volumeScale":0}, + # ], + # "defaultPrice":"0.0000006100", + # }, + # ], + # "coins":[ + # { + # "coin": "near", + # "coinFulName": "NEAR Protocol", + # "chains": ["BEP20",], + # "chainDetail": [ + # { + # "chain": "BEP20", + # "enableWithdraw": True, + # "enableDeposit": True, + # "withdrawFee": "0.2000", + # "minWithdraw": "5.0000", + # "maxWithdraw": "1000000000000000.0000", + # }, + # ], + # }, + # ], + # } + # + result: dict = {} + coins = self.safe_list(response, 'coins', []) + for i in range(0, len(coins)): + currency = coins[i] + id = self.safe_string(currency, 'coin') + name = self.safe_string(currency, 'coinFulName') + code = self.safe_currency_code(id) + networkDetails = self.safe_list(currency, 'chainDetail', []) + networks: dict = {} + for j in range(0, len(networkDetails)): + entry = networkDetails[j] + networkId = self.safe_string(entry, 'chain') + network = self.network_id_to_code(networkId, code) + networks[network] = { + 'info': entry, + 'id': networkId, + 'network': network, + 'deposit': self.safe_bool(entry, 'enableDeposit'), + 'withdraw': self.safe_bool(entry, 'enableWithdraw'), + 'active': None, + 'fee': self.safe_number(entry, 'withdrawFee'), + 'precision': None, + 'limits': { + 'withdraw': { + 'min': self.safe_number(entry, 'minWithdraw'), + 'max': self.safe_number(entry, 'maxWithdraw'), + }, + }, + } + result[code] = self.safe_currency_structure({ + 'id': id, + 'name': name, + 'code': code, + 'precision': None, + 'info': currency, + 'active': None, + 'deposit': None, + 'withdraw': None, + 'networks': networks, + 'fee': None, + 'fees': None, + 'type': 'crypto', + 'limits': { + 'withdraw': { + 'min': None, + 'max': None, + }, + }, + }) + return result + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for bitrue + + https://github.com/Bitrue-exchange/Spot-official-api-docs#exchangeInfo_endpoint + https://www.bitrue.com/api-docs#current-open-contract + https://www.bitrue.com/api_docs_includes_file/delivery.html#current-open-contract + + :param dict [params]: extra parameters specific to the exchange api endpoint + :returns dict[]: an array of objects representing market data + """ + promisesRaw = [] + types = None + defaultTypes = ['spot', 'linear', 'inverse'] + fetchMarketsOptions = self.safe_dict(self.options, 'fetchMarkets') + if fetchMarketsOptions is not None: + types = self.safe_list(fetchMarketsOptions, 'types', defaultTypes) + else: + # for backward-compatibility + types = self.safe_list(self.options, 'fetchMarkets', defaultTypes) + for i in range(0, len(types)): + marketType = types[i] + if marketType == 'spot': + promisesRaw.append(self.spotV1PublicGetExchangeInfo(params)) + elif marketType == 'linear': + promisesRaw.append(self.fapiV1PublicGetContracts(params)) + elif marketType == 'inverse': + promisesRaw.append(self.dapiV1PublicGetContracts(params)) + else: + raise ExchangeError(self.id + ' fetchMarkets() self.options fetchMarkets "' + marketType + '" is not a supported market type') + promises = promisesRaw + spotMarkets = self.safe_value(self.safe_value(promises, 0), 'symbols', []) + futureMarkets = self.safe_value(promises, 1) + deliveryMarkets = self.safe_value(promises, 2) + markets = spotMarkets + markets = self.array_concat(markets, futureMarkets) + markets = self.array_concat(markets, deliveryMarkets) + # + # spot + # + # { + # "timezone":"CTT", + # "serverTime":1635464889117, + # "rateLimits":[ + # {"rateLimitType":"REQUESTS_WEIGHT","interval":"MINUTES","limit":6000}, + # {"rateLimitType":"ORDERS","interval":"SECONDS","limit":150}, + # {"rateLimitType":"ORDERS","interval":"DAYS","limit":288000}, + # ], + # "exchangeFilters":[], + # "symbols":[ + # { + # "symbol":"SHABTC", + # "status":"TRADING", + # "baseAsset":"sha", + # "baseAssetPrecision":0, + # "quoteAsset":"btc", + # "quotePrecision":10, + # "orderTypes":["MARKET","LIMIT"], + # "icebergAllowed":false, + # "filters":[ + # {"filterType":"PRICE_FILTER","minPrice":"0.00000001349","maxPrice":"0.00000017537","priceScale":10}, + # {"filterType":"LOT_SIZE","minQty":"1.0","minVal":"0.00020","maxQty":"1000000000","volumeScale":0}, + # ], + # "defaultPrice":"0.0000006100", + # }, + # ], + # "coins":[ + # { + # "coin":"sbr", + # "coinFulName":"Saber", + # "enableWithdraw":true, + # "enableDeposit":true, + # "chains":["SOLANA"], + # "withdrawFee":"2.0", + # "minWithdraw":"5.0", + # "maxWithdraw":"1000000000000000", + # }, + # ], + # } + # + # swap / delivery + # + # [ + # { + # "symbol": "H-HT-USDT", + # "pricePrecision": 8, + # "side": 1, + # "maxMarketVolume": 100000, + # "multiplier": 6, + # "minOrderVolume": 1, + # "maxMarketMoney": 10000000, + # "type": "H", # E: perpetual contract, S: test contract, others are mixed contract + # "maxLimitVolume": 1000000, + # "maxValidOrder": 20, + # "multiplierCoin": "HT", + # "minOrderMoney": 0.001, + # "maxLimitMoney": 1000000, + # "status": 1 + # } + # ] + # + if self.options['adjustForTimeDifference']: + self.load_time_difference() + return self.parse_markets(markets) + + def parse_market(self, market: dict) -> Market: + id = self.safe_string(market, 'symbol') + lowercaseId = self.safe_string_lower(market, 'symbol') + side = self.safe_integer(market, 'side') # 1 linear, 0 inverse, None spot + type = None + isLinear = None + isInverse = None + if side is None: + type = 'spot' + else: + type = 'swap' + isLinear = (side == 1) + isInverse = (side == 0) + isContract = (type != 'spot') + baseId = self.safe_string(market, 'baseAsset') + quoteId = self.safe_string(market, 'quoteAsset') + settleId = None + settle = None + if isContract: + symbolSplit = id.split('-') + baseId = self.safe_string(symbolSplit, 1) + quoteId = self.safe_string(symbolSplit, 2) + if isLinear: + settleId = quoteId + else: + settleId = baseId + settle = self.safe_currency_code(settleId) + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + symbol = base + '/' + quote + if settle is not None: + symbol += ':' + settle + filters = self.safe_list(market, 'filters', []) + filtersByType = self.index_by(filters, 'filterType') + status = self.safe_string(market, 'status') + priceFilter = self.safe_dict(filtersByType, 'PRICE_FILTER', {}) + amountFilter = self.safe_dict(filtersByType, 'LOT_SIZE', {}) + defaultPricePrecision = self.safe_string(market, 'pricePrecision') + defaultAmountPrecision = self.safe_string(market, 'quantityPrecision') + pricePrecision = self.safe_string(priceFilter, 'priceScale', defaultPricePrecision) + amountPrecision = self.safe_string(amountFilter, 'volumeScale', defaultAmountPrecision) + multiplier = self.safe_string(market, 'multiplier') + maxQuantity = self.safe_number(amountFilter, 'maxQty') + if maxQuantity is None: + maxQuantity = self.safe_number(market, 'maxValidOrder') + minCost = self.safe_number(amountFilter, 'minVal') + if minCost is None: + minCost = self.safe_number(market, 'minOrderMoney') + return { + 'id': id, + 'lowercaseId': lowercaseId, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': type, + 'spot': (type == 'spot'), + 'margin': False, + 'swap': isContract, + 'future': False, + 'option': False, + 'active': (status == 'TRADING'), + 'contract': isContract, + 'linear': isLinear, + 'inverse': isInverse, + 'contractSize': self.parse_number(Precise.string_abs(multiplier)), + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(self.parse_precision(amountPrecision)), + 'price': self.parse_number(self.parse_precision(pricePrecision)), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(amountFilter, 'minQty'), + 'max': maxQuantity, + }, + 'price': { + 'min': self.safe_number(priceFilter, 'minPrice'), + 'max': self.safe_number(priceFilter, 'maxPrice'), + }, + 'cost': { + 'min': minCost, + 'max': None, + }, + }, + 'created': None, + 'info': market, + } + + def parse_balance(self, response) -> Balances: + # + # spot + # + # { + # "makerCommission":0, + # "takerCommission":0, + # "buyerCommission":0, + # "sellerCommission":0, + # "updateTime":null, + # "balances":[ + # {"asset":"sbr","free":"0","locked":"0"}, + # {"asset":"ksm","free":"0","locked":"0"}, + # {"asset":"neo3s","free":"0","locked":"0"}, + # ], + # "canTrade":false, + # "canWithdraw":false, + # "canDeposit":false + # } + # + # swap + # + # { + # "account":[ + # { + # "marginCoin":"USDT", + # "coinPrecious":4, + # "accountNormal":1010.4043400372839856, + # "accountLock":2.9827889600000006, + # "partPositionNormal":0, + # "totalPositionNormal":0, + # "achievedAmount":0, + # "unrealizedAmount":0, + # "totalMarginRate":0, + # "totalEquity":1010.4043400372839856, + # "partEquity":0, + # "totalCost":0, + # "sumMarginRate":0, + # "sumOpenRealizedAmount":0, + # "canUseTrialFund":0, + # "sumMaintenanceMargin":null, + # "futureModel":null, + # "positionVos":[] + # } + # ] + # } + # + result: dict = { + 'info': response, + } + timestamp = self.safe_integer(response, 'updateTime') + balances = self.safe_value_2(response, 'balances', 'account', []) + for i in range(0, len(balances)): + balance = balances[i] + currencyId = self.safe_string_2(balance, 'asset', 'marginCoin') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string_2(balance, 'free', 'accountNormal') + account['used'] = self.safe_string_2(balance, 'locked', 'accountLock') + result[code] = account + result['timestamp'] = timestamp + result['datetime'] = self.iso8601(timestamp) + return self.safe_balance(result) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://github.com/Bitrue-exchange/Spot-official-api-docs#account-information-user_data + https://www.bitrue.com/api-docs#account-information-v2-user_data-hmac-sha256 + https://www.bitrue.com/api_docs_includes_file/delivery.html#account-information-v2-user_data-hmac-sha256 + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: 'future', 'delivery', 'spot', 'swap' + :param str [params.subType]: 'linear', 'inverse' + :returns dict: a `balance structure ` + """ + self.load_markets() + type = None + type, params = self.handle_market_type_and_params('fetchBalance', None, params) + subType = None + subType, params = self.handle_sub_type_and_params('fetchBalance', None, params) + response = None + result = None + if type == 'swap': + if subType is not None and subType == 'inverse': + response = self.dapiV2PrivateGetAccount(params) + result = self.safe_dict(response, 'data', {}) + # + # { + # "code":"0", + # "msg":"Success", + # "data":{ + # "account":[ + # { + # "marginCoin":"USD", + # "coinPrecious":4, + # "accountNormal":1010.4043400372839856, + # "accountLock":2.9827889600000006, + # "partPositionNormal":0, + # "totalPositionNormal":0, + # "achievedAmount":0, + # "unrealizedAmount":0, + # "totalMarginRate":0, + # "totalEquity":1010.4043400372839856, + # "partEquity":0, + # "totalCost":0, + # "sumMarginRate":0, + # "sumOpenRealizedAmount":0, + # "canUseTrialFund":0, + # "sumMaintenanceMargin":null, + # "futureModel":null, + # "positionVos":[] + # } + # ] + # } + # } + # + else: + response = self.fapiV2PrivateGetAccount(params) + result = self.safe_dict(response, 'data', {}) + # + # { + # "code":"0", + # "msg":"Success", + # "data":{ + # "account":[ + # { + # "marginCoin":"USDT", + # "coinPrecious":4, + # "accountNormal":1010.4043400372839856, + # "accountLock":2.9827889600000006, + # "partPositionNormal":0, + # "totalPositionNormal":0, + # "achievedAmount":0, + # "unrealizedAmount":0, + # "totalMarginRate":0, + # "totalEquity":1010.4043400372839856, + # "partEquity":0, + # "totalCost":0, + # "sumMarginRate":0, + # "sumOpenRealizedAmount":0, + # "canUseTrialFund":0, + # "sumMaintenanceMargin":null, + # "futureModel":null, + # "positionVos":[] + # } + # ] + # } + # } + # + else: + response = self.spotV1PrivateGetAccount(params) + result = response + # + # { + # "makerCommission":0, + # "takerCommission":0, + # "buyerCommission":0, + # "sellerCommission":0, + # "updateTime":null, + # "balances":[ + # {"asset":"sbr","free":"0","locked":"0"}, + # {"asset":"ksm","free":"0","locked":"0"}, + # {"asset":"neo3s","free":"0","locked":"0"}, + # ], + # "canTrade":false, + # "canWithdraw":false, + # "canDeposit":false + # } + # + return self.parse_balance(result) + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://github.com/Bitrue-exchange/Spot-official-api-docs#order-book + https://www.bitrue.com/api-docs#order-book + https://www.bitrue.com/api_docs_includes_file/delivery.html#order-book + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + response = None + if market['swap']: + request: dict = { + 'contractName': market['id'], + } + if limit is not None: + if limit > 100: + limit = 100 + request['limit'] = limit # default 100, max 100, see https://www.bitrue.com/api-docs#order-book + if market['linear']: + response = self.fapiV1PublicGetDepth(self.extend(request, params)) + elif market['inverse']: + response = self.dapiV1PublicGetDepth(self.extend(request, params)) + elif market['spot']: + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + if limit > 1000: + limit = 1000 + request['limit'] = limit # default 100, max 1000, see https://github.com/Bitrue-exchange/bitrue-official-api-docs#order-book + response = self.spotV1PublicGetDepth(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchOrderBook only support spot & swap markets') + # + # spot + # + # { + # "lastUpdateId":1635474910177, + # "bids":[ + # ["61436.84","0.05",[]], + # ["61435.77","0.0124",[]], + # ["61434.88","0.012",[]], + # ], + # "asks":[ + # ["61452.46","0.0001",[]], + # ["61452.47","0.0597",[]], + # ["61452.76","0.0713",[]], + # ] + # } + # + # swap + # + # { + # "asks": [[34916.5, 2582], [34916.6, 2193], [34916.7, 2629], [34916.8, 3478], [34916.9, 2718]], + # "bids": [[34916.4, 92065], [34916.3, 25703], [34916.2, 37259], [34916.1, 26446], [34916, 44456]], + # "time": 1699338305000 + # } + # + timestamp = self.safe_integer_2(response, 'time', 'lastUpdateId') + orderbook = self.parse_order_book(response, symbol, timestamp) + orderbook['nonce'] = self.safe_integer(response, 'lastUpdateId') + return orderbook + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # fetchBidsAsks + # + # { + # "symbol": "LTCBTC", + # "bidPrice": "4.00000000", + # "bidQty": "431.00000000", + # "askPrice": "4.00000200", + # "askQty": "9.00000000" + # } + # + # fetchTicker + # + # { + # "symbol": "BNBBTC", + # "priceChange": "0.000248", + # "priceChangePercent": "3.5500", + # "weightedAvgPrice": null, + # "prevClosePrice": null, + # "lastPrice": "0.007226", + # "lastQty": null, + # "bidPrice": "0.007208", + # "askPrice": "0.007240", + # "openPrice": "0.006978", + # "highPrice": "0.007295", + # "lowPrice": "0.006935", + # "volume": "11749.86", + # "quoteVolume": "84.1066211", + # "openTime": 0, + # "closeTime": 0, + # "firstId": 0, + # "lastId": 0, + # "count": 0 + # } + # + symbol = self.safe_symbol(None, market) + last = self.safe_string_2(ticker, 'lastPrice', 'last') + timestamp = self.safe_integer(ticker, 'time') + percentage = None + if market['swap']: + percentage = Precise.string_mul(self.safe_string(ticker, 'rose'), '100') + else: + percentage = self.safe_string(ticker, 'priceChangePercent') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string_2(ticker, 'highPrice', 'high'), + 'low': self.safe_string_2(ticker, 'lowPrice', 'low'), + 'bid': self.safe_string_2(ticker, 'bidPrice', 'buy'), + 'bidVolume': self.safe_string(ticker, 'bidQty'), + 'ask': self.safe_string_2(ticker, 'askPrice', 'sell'), + 'askVolume': self.safe_string(ticker, 'askQty'), + 'vwap': self.safe_string(ticker, 'weightedAvgPrice'), + 'open': self.safe_string(ticker, 'openPrice'), + 'close': last, + 'last': last, + 'previousClose': None, + 'change': self.safe_string(ticker, 'priceChange'), + 'percentage': percentage, + 'average': None, + 'baseVolume': self.safe_string_2(ticker, 'volume', 'vol'), + 'quoteVolume': self.safe_string(ticker, 'quoteVolume'), + 'info': ticker, + }, market) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://github.com/Bitrue-exchange/Spot-official-api-docs#24hr-ticker-price-change-statistics + https://www.bitrue.com/api-docs#ticker + https://www.bitrue.com/api_docs_includes_file/delivery.html#ticker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + response = None + data = None + if market['swap']: + request: dict = { + 'contractName': market['id'], + } + if market['linear']: + response = self.fapiV1PublicGetTicker(self.extend(request, params)) + elif market['inverse']: + response = self.dapiV1PublicGetTicker(self.extend(request, params)) + data = response + elif market['spot']: + request: dict = { + 'symbol': market['id'], + } + response = self.spotV1PublicGetTicker24hr(self.extend(request, params)) + data = self.safe_dict(response, 0, {}) + else: + raise NotSupported(self.id + ' fetchTicker only support spot & swap markets') + # + # spot + # + # [{ + # symbol: 'BTCUSDT', + # priceChange: '105.20', + # priceChangePercent: '0.3000', + # weightedAvgPrice: null, + # prevClosePrice: null, + # lastPrice: '34905.21', + # lastQty: null, + # bidPrice: '34905.21', + # askPrice: '34905.22', + # openPrice: '34800.01', + # highPrice: '35276.33', + # lowPrice: '34787.51', + # volume: '12549.6481', + # quoteVolume: '439390492.917', + # openTime: '0', + # closeTime: '0', + # firstId: '0', + # lastId: '0', + # count: '0' + # }] + # + # swap + # + # { + # "high": "35296", + # "vol": "779308354", + # "last": "34884.1", + # "low": "34806.7", + # "buy": 34883.9, + # "sell": 34884, + # "rose": "-0.0027957315", + # "time": 1699348013000 + # } + # + return self.parse_ticker(data, market) + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://www.bitrue.com/api_docs_includes_file/spot/index.html#kline-data + https://www.bitrue.com/api_docs_includes_file/futures/index.html#kline-candlestick-data + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch transfers for + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + market = self.market(symbol) + timeframes = self.safe_dict(self.options, 'timeframes', {}) + response = None + data = None + if market['swap']: + timeframesFuture = self.safe_dict(timeframes, 'future', {}) + request: dict = { + 'contractName': market['id'], + # 1min / 5min / 15min / 30min / 1h / 1day / 1week / 1month + 'interval': self.safe_string(timeframesFuture, timeframe, '1min'), + } + if limit is not None: + request['limit'] = limit + if market['linear']: + response = self.fapiV1PublicGetKlines(self.extend(request, params)) + elif market['inverse']: + response = self.dapiV1PublicGetKlines(self.extend(request, params)) + data = response + elif market['spot']: + timeframesSpot = self.safe_dict(timeframes, 'spot', {}) + request: dict = { + 'symbol': market['id'], + # 1m / 5m / 15m / 30m / 1H / 2H / 4H / 12H / 1D / 1W + 'scale': self.safe_string(timeframesSpot, timeframe, '1m'), + } + if limit is not None: + request['limit'] = limit + until = self.safe_integer(params, 'until') + if until is not None: + params = self.omit(params, 'until') + request['fromIdx'] = until + response = self.spotV1PublicGetMarketKline(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + else: + raise NotSupported(self.id + ' fetchOHLCV only support spot & swap markets') + # + # spot + # + # { + # "symbol":"BTCUSDT", + # "scale":"KLINE_1MIN", + # "data":[ + # { + # "i":"1660825020", + # "a":"93458.778", + # "v":"3.9774", + # "c":"23494.99", + # "h":"23509.63", + # "l":"23491.93", + # "o":"23508.34" + # } + # ] + # } + # + # swap + # + # [ + # { + # "high": "35360.7", + # "vol": "110288", + # "low": "35347.9", + # "idx": 1699411680000, + # "close": "35347.9", + # "open": "35349.4" + # } + # ] + # + return self.parse_ohlcvs(data, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # spot + # + # { + # "i":"1660825020", + # "a":"93458.778", + # "v":"3.9774", + # "c":"23494.99", + # "h":"23509.63", + # "l":"23491.93", + # "o":"23508.34" + # } + # + # swap + # + # { + # "high": "35360.7", + # "vol": "110288", + # "low": "35347.9", + # "idx": 1699411680000, + # "close": "35347.9", + # "open": "35349.4" + # } + # + timestamp = self.safe_timestamp(ohlcv, 'i') + if timestamp is None: + timestamp = self.safe_integer(ohlcv, 'idx') + return [ + timestamp, + self.safe_number_2(ohlcv, 'o', 'open'), + self.safe_number_2(ohlcv, 'h', 'high'), + self.safe_number_2(ohlcv, 'l', 'low'), + self.safe_number_2(ohlcv, 'c', 'close'), + self.safe_number_2(ohlcv, 'v', 'vol'), + ] + + def fetch_bids_asks(self, symbols: Strings = None, params={}): + """ + fetches the bid and ask price and volume for multiple markets + + https://github.com/Bitrue-exchange/Spot-official-api-docs#symbol-order-book-ticker + https://www.bitrue.com/api-docs#ticker + https://www.bitrue.com/api_docs_includes_file/delivery.html#ticker + + :param str[]|None symbols: unified symbols of the markets to fetch the bids and asks for, all markets are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + symbols = self.market_symbols(symbols, None, False) + first = self.safe_string(symbols, 0) + market = self.market(first) + response = None + if market['swap']: + request: dict = { + 'contractName': market['id'], + } + if market['linear']: + response = self.fapiV1PublicGetTicker(self.extend(request, params)) + elif market['inverse']: + response = self.dapiV1PublicGetTicker(self.extend(request, params)) + elif market['spot']: + request: dict = { + 'symbol': market['id'], + } + response = self.spotV1PublicGetTickerBookTicker(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchBidsAsks only support spot & swap markets') + # + # spot + # + # { + # "symbol": "LTCBTC", + # "bidPrice": "4.00000000", + # "bidQty": "431.00000000", + # "askPrice": "4.00000200", + # "askQty": "9.00000000" + # } + # + # swap + # + # { + # "high": "35296", + # "vol": "779308354", + # "last": "34884.1", + # "low": "34806.7", + # "buy": 34883.9, + # "sell": 34884, + # "rose": "-0.0027957315", + # "time": 1699348013000 + # } + # + data: dict = {} + data[market['id']] = response + return self.parse_tickers(data, symbols) + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://github.com/Bitrue-exchange/Spot-official-api-docs#24hr-ticker-price-change-statistics + https://www.bitrue.com/api-docs#ticker + https://www.bitrue.com/api_docs_includes_file/delivery.html#ticker + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + symbols = self.market_symbols(symbols) + response = None + data = None + request: dict = {} + type = None + if symbols is not None: + first = self.safe_string(symbols, 0) + market = self.market(first) + if market['swap']: + raise NotSupported(self.id + ' fetchTickers does not support swap markets, please use fetchTicker instead') + elif market['spot']: + response = self.spotV1PublicGetTicker24hr(self.extend(request, params)) + data = response + else: + raise NotSupported(self.id + ' fetchTickers only support spot & swap markets') + else: + type, params = self.handle_market_type_and_params('fetchTickers', None, params) + if type != 'spot': + raise NotSupported(self.id + ' fetchTickers only support spot when symbols are not proved') + response = self.spotV1PublicGetTicker24hr(self.extend(request, params)) + data = response + # + # spot + # + # [{ + # symbol: 'BTCUSDT', + # priceChange: '105.20', + # priceChangePercent: '0.3000', + # weightedAvgPrice: null, + # prevClosePrice: null, + # lastPrice: '34905.21', + # lastQty: null, + # bidPrice: '34905.21', + # askPrice: '34905.22', + # openPrice: '34800.01', + # highPrice: '35276.33', + # lowPrice: '34787.51', + # volume: '12549.6481', + # quoteVolume: '439390492.917', + # openTime: '0', + # closeTime: '0', + # firstId: '0', + # lastId: '0', + # count: '0' + # }] + # + # swap + # + # { + # "high": "35296", + # "vol": "779308354", + # "last": "34884.1", + # "low": "34806.7", + # "buy": 34883.9, + # "sell": 34884, + # "rose": "-0.0027957315", + # "time": 1699348013000 + # } + # + # the exchange returns market ids with an underscore from the tickers endpoint + # the market ids do not have an underscore, so it has to be removed + # https://github.com/ccxt/ccxt/issues/13856 + tickers: dict = {} + for i in range(0, len(data)): + ticker = self.safe_dict(data, i, {}) + market = self.safe_market(self.safe_string(ticker, 'symbol')) + tickers[market['id']] = ticker + return self.parse_tickers(tickers, symbols) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades + # + # { + # "id": 28457, + # "price": "4.00000100", + # "qty": "12.00000000", + # "time": 1499865549590, # Actual timestamp of trade + # "isBuyerMaker": True, + # "isBestMatch": True + # } + # + # fetchTrades - spot + # + # { + # "symbol":"USDCUSDT", + # "id":20725156, + # "orderId":2880918576, + # "origClientOrderId":null, + # "price":"0.9996000000000000", + # "qty":"100.0000000000000000", + # "commission":null, + # "commissionAssert":null, + # "time":1635558511000, + # "isBuyer":false, + # "isMaker":false, + # "isBestMatch":true + # } + # + # fetchTrades - future + # + # { + # "tradeId":12, + # "price":0.9, + # "qty":1, + # "amount":9, + # "contractName":"E-SAND-USDT", + # "side":"BUY", + # "fee":"0.0018", + # "bidId":1558124009467904992, + # "askId":1558124043827644908, + # "bidUserId":10294, + # "askUserId":10467, + # "isBuyer":true, + # "isMaker":true, + # "ctime":1678426306000 + # } + # + timestamp = self.safe_integer_2(trade, 'ctime', 'time') + priceString = self.safe_string(trade, 'price') + amountString = self.safe_string(trade, 'qty') + marketId = self.safe_string_2(trade, 'symbol', 'contractName') + symbol = self.safe_symbol(marketId, market) + orderId = self.safe_string(trade, 'orderId') + id = self.safe_string_2(trade, 'id', 'tradeId') + side = None + buyerMaker = self.safe_bool(trade, 'isBuyerMaker') # ignore "m" until Bitrue fixes api + isBuyer = self.safe_bool(trade, 'isBuyer') + if buyerMaker is not None: + side = 'sell' if buyerMaker else 'buy' + if isBuyer is not None: + side = 'buy' if isBuyer else 'sell' # self is a True side + fee = None + if 'commission' in trade: + fee = { + 'cost': self.safe_string_2(trade, 'commission', 'fee'), + 'currency': self.safe_currency_code(self.safe_string(trade, 'commissionAssert')), + } + takerOrMaker = None + isMaker = self.safe_bool(trade, 'isMaker') + if isMaker is not None: + takerOrMaker = 'maker' if isMaker else 'taker' + return self.safe_trade({ + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'id': id, + 'order': orderId, + 'type': None, + 'side': side, + 'takerOrMaker': takerOrMaker, + 'price': priceString, + 'amount': amountString, + 'cost': None, + 'fee': fee, + }, market) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://github.com/Bitrue-exchange/Spot-official-api-docs#recent-trades-list + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + response = None + if market['spot']: + request: dict = { + 'symbol': market['id'], + # 'limit': 100, # default 100, max = 1000 + } + if limit is not None: + request['limit'] = limit # default 100, max 1000 + response = self.spotV1PublicGetTrades(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchTrades only support spot markets') + # + # spot + # + # [ + # { + # "id": 28457, + # "price": "4.00000100", + # "qty": "12.00000000", + # "time": 1499865549590, + # "isBuyerMaker": True, + # "isBestMatch": True + # } + # ] + # + return self.parse_trades(response, market, since, limit) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'INIT': 'open', + 'PENDING_CREATE': 'open', + 'NEW': 'open', + 'PARTIALLY_FILLED': 'open', + 'FILLED': 'closed', + 'CANCELED': 'canceled', + 'PENDING_CANCEL': 'canceling', # currently unused + 'REJECTED': 'rejected', + 'EXPIRED': 'expired', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder - spot + # + # { + # "symbol":"USDCUSDT", + # "orderId":2878854881, + # "clientOrderId":"", + # "transactTime":1635551031276 + # } + # + # createOrder - future + # + # { + # "orderId":1690615676032452985, + # } + # + # fetchOrders - spot + # + # { + # "symbol":"USDCUSDT", + # "orderId":"2878854881", + # "clientOrderId":"", + # "price":"1.1000000000000000", + # "origQty":"100.0000000000000000", + # "executedQty":"0.0000000000000000", + # "cummulativeQuoteQty":"0.0000000000000000", + # "status":"NEW", + # "timeInForce":"", + # "type":"LIMIT", + # "side":"SELL", + # "stopPrice":"", + # "icebergQty":"", + # "time":1635551031000, + # "updateTime":1635551031000, + # "isWorking":false + # } + # + # fetchOrders - future + # + # { + # "orderId":1917641, + # "price":100, + # "origQty":10, + # "origAmount":10, + # "executedQty":1, + # "avgPrice":10000, + # "status":"INIT", + # "type":"LIMIT", + # "side":"BUY", + # "action":"OPEN", + # "transactTime":1686716571425 + # "clientOrderId":4949299210 + # } + # + status = self.parse_order_status(self.safe_string_2(order, 'status', 'orderStatus')) + marketId = self.safe_string(order, 'symbol') + symbol = self.safe_symbol(marketId, market) + filled = self.safe_string(order, 'executedQty') + timestamp = None + lastTradeTimestamp = None + if 'time' in order: + timestamp = self.safe_integer(order, 'time') + elif 'transactTime' in order: + timestamp = self.safe_integer(order, 'transactTime') + elif 'updateTime' in order: + if status == 'open': + if Precise.string_gt(filled, '0'): + lastTradeTimestamp = self.safe_integer(order, 'updateTime') + else: + timestamp = self.safe_integer(order, 'updateTime') + average = self.safe_string(order, 'avgPrice') + price = self.safe_string(order, 'price') + amount = self.safe_string(order, 'origQty') + # - Spot/Margin market: cummulativeQuoteQty + # - Futures market: cumQuote. + # Note self is not the actual cost, since Binance futures uses leverage to calculate margins. + cost = self.safe_string_2(order, 'cummulativeQuoteQty', 'cumQuote') + id = self.safe_string(order, 'orderId') + type = self.safe_string_lower(order, 'type') + side = self.safe_string_lower(order, 'side') + fills = self.safe_list(order, 'fills', []) + clientOrderId = self.safe_string(order, 'clientOrderId') + timeInForce = self.safe_string(order, 'timeInForce') + postOnly = (type == 'limit_maker') or (timeInForce == 'GTX') or (type == 'post_only') + if type == 'limit_maker': + type = 'limit' + triggerPrice = self.parse_number(self.omit_zero(self.safe_string(order, 'stopPrice'))) + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': lastTradeTimestamp, + 'symbol': symbol, + 'type': type, + 'timeInForce': timeInForce, + 'postOnly': postOnly, + 'side': side, + 'price': price, + 'triggerPrice': triggerPrice, + 'amount': amount, + 'cost': cost, + 'average': average, + 'filled': filled, + 'remaining': None, + 'status': status, + 'fee': None, + 'trades': fills, + }, market) + + def create_market_buy_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market buy order by providing the symbol and cost + + https://www.bitrue.com/api-docs#new-order-trade-hmac-sha256 + https://www.bitrue.com/api_docs_includes_file/delivery.html#new-order-trade-hmac-sha256 + + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + if not market['swap']: + raise NotSupported(self.id + ' createMarketBuyOrderWithCost() supports swap orders only') + params['createMarketBuyOrderRequiresPrice'] = False + return self.create_order(symbol, 'market', 'buy', cost, None, params) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://www.bitrue.com/api_docs_includes_file/spot/index.html#new-order-trade + https://www.bitrue.com/api_docs_includes_file/futures/index.html#new-order-trade-hmac-sha256 + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: *spot only* the price at which a trigger order is triggered at + :param str [params.clientOrderId]: a unique id for the order, automatically generated if not sent + :param decimal [params.leverage]: in future order, the leverage value of the order should consistent with the user contract configuration, default is 1 + :param str [params.timeInForce]: 'fok', 'ioc' or 'po' + :param bool [params.postOnly]: default False + :param bool [params.reduceOnly]: default False + EXCHANGE SPECIFIC PARAMETERS + :param decimal [params.icebergQty]: + :param long [params.recvWindow]: + :param float [params.cost]: *swap market buy only* the quote quantity that can be used alternative for the amount + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + response = None + data = None + uppercaseType = type.upper() + request: dict = { + 'side': side.upper(), + 'type': uppercaseType, + # 'timeInForce': '', + # 'price': self.price_to_precision(symbol, price), + # 'newClientOrderId': clientOrderId, # automatically generated if not sent + # 'stopPrice': self.price_to_precision(symbol, 'stopPrice'), + # 'icebergQty': self.amount_to_precision(symbol, icebergQty), + } + if uppercaseType == 'LIMIT': + if price is None: + raise InvalidOrder(self.id + ' createOrder() requires a price argument') + request['price'] = self.price_to_precision(symbol, price) + if market['swap']: + isMarket = uppercaseType == 'MARKET' + timeInForce = self.safe_string_lower(params, 'timeInForce') + postOnly = self.is_post_only(isMarket, None, params) + if postOnly: + request['type'] = 'POST_ONLY' + elif timeInForce == 'fok': + request['type'] = 'FOK' + elif timeInForce == 'ioc': + request['type'] = 'IOC' + request['contractName'] = market['id'] + createMarketBuyOrderRequiresPrice = True + createMarketBuyOrderRequiresPrice, params = self.handle_option_and_params(params, 'createOrder', 'createMarketBuyOrderRequiresPrice', True) + if isMarket and (side == 'buy') and createMarketBuyOrderRequiresPrice: + cost = self.safe_string(params, 'cost') + params = self.omit(params, 'cost') + if price is None and cost is None: + raise InvalidOrder(self.id + ' createOrder() requires the price argument with swap market buy orders to calculate total order cost(amount to spend), where cost = amount * price. Supply a price argument to createOrder() call if you want the cost to be calculated for you from price and amount, or, alternatively, add .options["createMarketBuyOrderRequiresPrice"] = False to supply the cost in the amount argument(the exchange-specific behaviour)') + else: + amountString = self.number_to_string(amount) + priceString = self.number_to_string(price) + quoteAmount = Precise.string_mul(amountString, priceString) + requestAmount = cost if (cost is not None) else quoteAmount + request['amount'] = self.cost_to_precision(symbol, requestAmount) + request['volume'] = self.cost_to_precision(symbol, requestAmount) + else: + request['amount'] = self.parse_to_numeric(amount) + request['volume'] = self.parse_to_numeric(amount) + request['positionType'] = 1 + reduceOnly = self.safe_value_2(params, 'reduceOnly', 'reduce_only') + request['open'] = 'CLOSE' if reduceOnly else 'OPEN' + leverage = self.safe_string(params, 'leverage', '1') + request['leverage'] = self.parse_to_numeric(leverage) + params = self.omit(params, ['leverage', 'reduceOnly', 'reduce_only', 'timeInForce']) + if market['linear']: + response = self.fapiV2PrivatePostOrder(self.extend(request, params)) + elif market['inverse']: + response = self.dapiV2PrivatePostOrder(self.extend(request, params)) + data = self.safe_dict(response, 'data', {}) + elif market['spot']: + request['symbol'] = market['id'] + request['quantity'] = self.amount_to_precision(symbol, amount) + validOrderTypes = self.safe_value(market['info'], 'orderTypes') + if not self.in_array(uppercaseType, validOrderTypes): + raise InvalidOrder(self.id + ' ' + type + ' is not a valid order type in market ' + symbol) + clientOrderId = self.safe_string_2(params, 'newClientOrderId', 'clientOrderId') + if clientOrderId is not None: + params = self.omit(params, ['newClientOrderId', 'clientOrderId']) + request['newClientOrderId'] = clientOrderId + triggerPrice = self.safe_value_2(params, 'triggerPrice', 'stopPrice') + if triggerPrice is not None: + params = self.omit(params, ['triggerPrice', 'stopPrice']) + request['stopPrice'] = self.price_to_precision(symbol, triggerPrice) + response = self.spotV1PrivatePostOrder(self.extend(request, params)) + data = response + else: + raise NotSupported(self.id + ' createOrder only support spot & swap markets') + # + # spot + # + # { + # "symbol": "BTCUSDT", + # "orderId": 307650651173648896, + # "orderIdStr": "307650651173648896", + # "clientOrderId": "6gCrw2kRUAF9CvJDGP16IP", + # "transactTime": 1507725176595 + # } + # + # swap + # + # { + # "code": "0", + # "msg": "Success", + # "data": { + # "orderId": 1690615676032452985 + # } + # } + # + return self.parse_order(data, market) + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://www.bitrue.com/api_docs_includes_file/spot/index.html#query-order-user_data + https://www.bitrue.com/api_docs_includes_file/futures/index.html#query-order-user_data-hmac-sha256 + + :param str id: the order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrder() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + origClientOrderId = self.safe_value_2(params, 'origClientOrderId', 'clientOrderId') + params = self.omit(params, ['origClientOrderId', 'clientOrderId']) + response = None + data = None + request: dict = {} + if origClientOrderId is None: + request['orderId'] = id + else: + if market['swap']: + request['clientOrderId'] = origClientOrderId + else: + request['origClientOrderId'] = origClientOrderId + if market['swap']: + request['contractName'] = market['id'] + if market['linear']: + response = self.fapiV2PrivateGetOrder(self.extend(request, params)) + elif market['inverse']: + response = self.dapiV2PrivateGetOrder(self.extend(request, params)) + data = self.safe_dict(response, 'data', {}) + elif market['spot']: + request['orderId'] = id # spot market id is mandatory + request['symbol'] = market['id'] + response = self.spotV1PrivateGetOrder(self.extend(request, params)) + data = response + else: + raise NotSupported(self.id + ' fetchOrder only support spot & swap markets') + # + # spot + # + # { + # "symbol": "LTCBTC", + # "orderId": 1, + # "clientOrderId": "myOrder1", + # "price": "0.1", + # "origQty": "1.0", + # "executedQty": "0.0", + # "cummulativeQuoteQty": "0.0", + # "status": "NEW", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "side": "BUY", + # "stopPrice": "0.0", + # "icebergQty": "0.0", + # "time": 1499827319559, + # "updateTime": 1499827319559, + # "isWorking": True + # } + # + # swap + # + # { + # "code":0, + # "msg":"success", + # "data":{ + # "orderId":1917641, + # "price":100, + # "origQty":10, + # "origAmount":10, + # "executedQty":1, + # "avgPrice":10000, + # "status":"INIT", + # "type":"LIMIT", + # "side":"BUY", + # "action":"OPEN", + # "transactTime":1686716571425 + # "clientOrderId":4949299210 + # } + # } + # + return self.parse_order(data, market) + + def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://www.bitrue.com/api_docs_includes_file/spot/index.html#all-orders-user_data + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchClosedOrders() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' fetchClosedOrders only support spot markets') + request: dict = { + 'symbol': market['id'], + # 'orderId': 123445, # long + # 'startTime': since, + # 'endTime': self.milliseconds(), + # 'limit': limit, # default 100, max 1000 + } + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit # default 100, max 1000 + response = self.spotV1PrivateGetAllOrders(self.extend(request, params)) + # + # [ + # { + # "symbol": "LTCBTC", + # "orderId": 1, + # "clientOrderId": "myOrder1", + # "price": "0.1", + # "origQty": "1.0", + # "executedQty": "0.0", + # "cummulativeQuoteQty": "0.0", + # "status": "NEW", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "side": "BUY", + # "stopPrice": "0.0", + # "icebergQty": "0.0", + # "time": 1499827319559, + # "updateTime": 1499827319559, + # "isWorking": True + # } + # ] + # + return self.parse_orders(response, market, since, limit) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://www.bitrue.com/api_docs_includes_file/spot/index.html#current-open-orders-user_data + https://www.bitrue.com/api_docs_includes_file/futures/index.html#cancel-all-open-orders-trade-hmac-sha256 + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOpenOrders() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + response = None + data = None + request: dict = {} + if market['swap']: + request['contractName'] = market['id'] + if market['linear']: + response = self.fapiV2PrivateGetOpenOrders(self.extend(request, params)) + elif market['inverse']: + response = self.dapiV2PrivateGetOpenOrders(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + elif market['spot']: + request['symbol'] = market['id'] + response = self.spotV1PrivateGetOpenOrders(self.extend(request, params)) + data = response + else: + raise NotSupported(self.id + ' fetchOpenOrders only support spot & swap markets') + # + # spot + # + # [ + # { + # "symbol":"USDCUSDT", + # "orderId":"2878854881", + # "clientOrderId":"", + # "price":"1.1000000000000000", + # "origQty":"100.0000000000000000", + # "executedQty":"0.0000000000000000", + # "cummulativeQuoteQty":"0.0000000000000000", + # "status":"NEW", + # "timeInForce":"", + # "type":"LIMIT", + # "side":"SELL", + # "stopPrice":"", + # "icebergQty":"", + # "time":1635551031000, + # "updateTime":1635551031000, + # "isWorking":false + # } + # ] + # + # swap + # + # { + # "code": "0", + # "msg": "Success", + # "data": [{ + # "orderId": 1917641, + # "clientOrderId": "2488514315", + # "price": 100, + # "origQty": 10, + # "origAmount": 10, + # "executedQty": 1, + # "avgPrice": 12451, + # "status": "INIT", + # "type": "LIMIT", + # "side": "BUY", + # "action": "OPEN", + # "transactTime": 1686717303975 + # } + # ] + # } + # + return self.parse_orders(data, market, since, limit) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://github.com/Bitrue-exchange/Spot-official-api-docs#cancel-order-trade + https://www.bitrue.com/api-docs#cancel-order-trade-hmac-sha256 + https://www.bitrue.com/api_docs_includes_file/delivery.html#cancel-order-trade-hmac-sha256 + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + origClientOrderId = self.safe_value_2(params, 'origClientOrderId', 'clientOrderId') + params = self.omit(params, ['origClientOrderId', 'clientOrderId']) + response = None + data = None + request: dict = {} + if origClientOrderId is None: + request['orderId'] = id + else: + if market['swap']: + request['clientOrderId'] = origClientOrderId + else: + request['origClientOrderId'] = origClientOrderId + if market['swap']: + request['contractName'] = market['id'] + if market['linear']: + response = self.fapiV2PrivatePostCancel(self.extend(request, params)) + elif market['inverse']: + response = self.dapiV2PrivatePostCancel(self.extend(request, params)) + data = self.safe_dict(response, 'data', {}) + elif market['spot']: + request['symbol'] = market['id'] + response = self.spotV1PrivateDeleteOrder(self.extend(request, params)) + data = response + else: + raise NotSupported(self.id + ' cancelOrder only support spot & swap markets') + # + # spot + # + # { + # "symbol": "LTCBTC", + # "origClientOrderId": "myOrder1", + # "orderId": 1, + # "clientOrderId": "cancelMyOrder1" + # } + # + # swap + # + # { + # "code": "0", + # "msg": "Success", + # "data": { + # "orderId": 1690615847831143159 + # } + # } + # + return self.parse_order(data, market) + + def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders in a market + + https://www.bitrue.com/api-docs#cancel-all-open-orders-trade-hmac-sha256 + https://www.bitrue.com/api_docs_includes_file/delivery.html#cancel-all-open-orders-trade-hmac-sha256 + + :param str symbol: unified market symbol of the market to cancel orders in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated', for spot margin trading + :returns dict[]: a list of `order structures ` + """ + self.load_markets() + market = self.market(symbol) + response = None + data = None + if market['swap']: + request: dict = { + 'contractName': market['id'], + } + if market['linear']: + response = self.fapiV2PrivatePostAllOpenOrders(self.extend(request, params)) + elif market['inverse']: + response = self.dapiV2PrivatePostAllOpenOrders(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + else: + raise NotSupported(self.id + ' cancelAllOrders only support future markets') + # + # swap + # + # { + # 'code': '0', + # 'msg': 'Success', + # 'data': null + # } + # + return self.parse_orders(data, market) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://www.bitrue.com/api_docs_includes_file/spot/index.html#account-trade-list-user_data + https://www.bitrue.com/api_docs_includes_file/futures/index.html#account-trade-list-user_data-hmac-sha256 + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMyTrades() requires a symbol argument') + market = self.market(symbol) + response = None + data = None + request: dict = {} + if since is not None: + request['startTime'] = since + if limit is not None: + if limit > 1000: + limit = 1000 + request['limit'] = limit + if market['swap']: + request['contractName'] = market['id'] + if market['linear']: + response = self.fapiV2PrivateGetMyTrades(self.extend(request, params)) + elif market['inverse']: + response = self.dapiV2PrivateGetMyTrades(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + elif market['spot']: + request['symbol'] = market['id'] + response = self.spotV2PrivateGetMyTrades(self.extend(request, params)) + data = response + else: + raise NotSupported(self.id + ' fetchMyTrades only support spot & swap markets') + # + # spot + # + # [ + # { + # "symbol":"USDCUSDT", + # "id":20725156, + # "orderId":2880918576, + # "origClientOrderId":null, + # "price":"0.9996000000000000", + # "qty":"100.0000000000000000", + # "commission":null, + # "commissionAssert":null, + # "time":1635558511000, + # "isBuyer":false, + # "isMaker":false, + # "isBestMatch":true + # } + # ] + # + # swap + # + # { + # "code":"0", + # "msg":"Success", + # "data":[ + # { + # "tradeId":12, + # "price":0.9, + # "qty":1, + # "amount":9, + # "contractName":"E-SAND-USDT", + # "side":"BUY", + # "fee":"0.0018", + # "bidId":1558124009467904992, + # "askId":1558124043827644908, + # "bidUserId":10294, + # "askUserId":10467, + # "isBuyer":true, + # "isMaker":true, + # "ctime":1678426306000 + # } + # ] + # } + # + return self.parse_trades(data, market, since, limit) + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://github.com/Bitrue-exchange/Spot-official-api-docs#deposit-history--withdraw_data + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + if code is None: + raise ArgumentsRequired(self.id + ' fetchDeposits() requires a code argument') + self.load_markets() + currency = self.currency(code) + request: dict = { + 'coin': currency['id'], + 'status': 1, # 0 init, 1 finished, default 0 + # 'offset': 0, + # 'limit': limit, # default 10, max 1000 + # 'startTime': since, + # 'endTime': self.milliseconds(), + } + if since is not None: + request['startTime'] = since + # request['endTime'] = self.sum(since, 7776000000) + if limit is not None: + request['limit'] = limit + response = self.spotV1PrivateGetDepositHistory(self.extend(request, params)) + # + # { + # "code":200, + # "msg":"succ", + # "data":[ + # { + # "id":2659137, + # "symbol":"USDC", + # "amount":"200.0000000000000000", + # "fee":"0.0E-15", + # "createdAt":1635503169000, + # "updatedAt":1635503202000, + # "addressFrom":"0x2faf487a4414fe77e2327f0bf4ae2a264a776ad2", + # "addressTo":"0x190ceccb1f8bfbec1749180f0ba8922b488d865b", + # "txid":"0x9970aec41099ac385568859517308707bc7d716df8dabae7b52f5b17351c3ed0", + # "confirmations":5, + # "status":0, + # "tagType":null, + # }, + # { + # "id":2659137, + # "symbol": "XRP", + # "amount": "20.0000000000000000", + # "fee": "0.0E-15", + # "createdAt": 1544669393000, + # "updatedAt": 1544669413000, + # "addressFrom": "", + # "addressTo": "raLPjTYeGezfdb6crXZzcC8RkLBEwbBHJ5_18113641", + # "txid": "515B23E1F9864D3AF7F5B4C4FCBED784BAE861854FAB95F4031922B6AAEFC7AC", + # "confirmations": 7, + # "status": 1, + # "tagType": "Tag" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_transactions(data, currency, since, limit) + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://github.com/Bitrue-exchange/Spot-official-api-docs#withdraw-history--withdraw_data + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + if code is None: + raise ArgumentsRequired(self.id + ' fetchWithdrawals() requires a code argument') + self.load_markets() + currency = self.currency(code) + request: dict = { + 'coin': currency['id'], + 'status': 5, # 0 init, 5 finished, 6 canceled, default 0 + # 'offset': 0, + # 'limit': limit, # default 10, max 1000 + # 'startTime': since, + # 'endTime': self.milliseconds(), + } + if since is not None: + request['startTime'] = since + # request['endTime'] = self.sum(since, 7776000000) + if limit is not None: + request['limit'] = limit + response = self.spotV1PrivateGetWithdrawHistory(self.extend(request, params)) + # + # { + # "code": 200, + # "msg": "succ", + # "data": [ + # { + # "id": 183745, + # "symbol": "usdt_erc20", + # "amount": "8.4000000000000000", + # "fee": "1.6000000000000000", + # "payAmount": "0.0000000000000000", + # "createdAt": 1595336441000, + # "updatedAt": 1595336576000, + # "addressFrom": "", + # "addressTo": "0x2edfae3878d7b6db70ce4abed177ab2636f60c83", + # "txid": "", + # "confirmations": 0, + # "status": 6, + # "tagType": null + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_transactions(data, currency) + + def parse_transaction_status_by_type(self, status, type=None): + statusesByType: dict = { + 'deposit': { + '0': 'pending', + '1': 'ok', + }, + 'withdrawal': { + '0': 'pending', # Email Sent + '5': 'ok', # Failure + '6': 'canceled', + }, + } + statuses = self.safe_dict(statusesByType, type, {}) + return self.safe_string(statuses, status, status) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fetchDeposits + # + # { + # "symbol": "XRP", + # "amount": "261.3361000000000000", + # "fee": "0.0E-15", + # "createdAt": 1548816979000, + # "updatedAt": 1548816999000, + # "addressFrom": "", + # "addressTo": "raLPjTYeGezfdb6crXZzcC8RkLBEwbBHJ5_18113641", + # "txid": "86D6EB68A7A28938BCE06BD348F8C07DEF500C5F7FE92069EF8C0551CE0F2C7D", + # "confirmations": 8, + # "status": 1, + # "tagType": "Tag" + # }, + # { + # "symbol": "XRP", + # "amount": "20.0000000000000000", + # "fee": "0.0E-15", + # "createdAt": 1544669393000, + # "updatedAt": 1544669413000, + # "addressFrom": "", + # "addressTo": "raLPjTYeGezfdb6crXZzcC8RkLBEwbBHJ5_18113641", + # "txid": "515B23E1F9864D3AF7F5B4C4FCBED784BAE861854FAB95F4031922B6AAEFC7AC", + # "confirmations": 7, + # "status": 1, + # "tagType": "Tag" + # } + # + # fetchWithdrawals + # + # { + # "id": 183745, + # "symbol": "usdt_erc20", + # "amount": "8.4000000000000000", + # "fee": "1.6000000000000000", + # "payAmount": "0.0000000000000000", + # "createdAt": 1595336441000, + # "updatedAt": 1595336576000, + # "addressFrom": "", + # "addressTo": "0x2edfae3878d7b6db70ce4abed177ab2636f60c83", + # "txid": "", + # "confirmations": 0, + # "status": 6, + # "tagType": null + # } + # + # withdraw + # + # { + # "msg": null, + # "amount": 1000, + # "fee": 1, + # "ctime": null, + # "coin": "usdt_erc20", + # "withdrawId": 1156423, + # "addressTo": "0x2edfae3878d7b6db70ce4abed177ab2636f60c83" + # } + # + id = self.safe_string_2(transaction, 'id', 'withdrawId') + tagType = self.safe_string(transaction, 'tagType') + addressTo = self.safe_string(transaction, 'addressTo') + addressFrom = self.safe_string(transaction, 'addressFrom') + tagTo = None + tagFrom = None + if tagType is not None: + if addressTo is not None: + parts = addressTo.split('_') + addressTo = self.safe_string(parts, 0) + tagTo = self.safe_string(parts, 1) + if addressFrom is not None: + parts = addressFrom.split('_') + addressFrom = self.safe_string(parts, 0) + tagFrom = self.safe_string(parts, 1) + txid = self.safe_string(transaction, 'txid') + timestamp = self.safe_integer(transaction, 'createdAt') + updated = self.safe_integer(transaction, 'updatedAt') + payAmount = ('payAmount' in transaction) + ctime = ('ctime' in transaction) + type = 'withdrawal' if (payAmount or ctime) else 'deposit' + status = self.parse_transaction_status_by_type(self.safe_string(transaction, 'status'), type) + amount = self.safe_number(transaction, 'amount') + network = None + currencyId = self.safe_string_2(transaction, 'symbol', 'coin') + if currencyId is not None: + parts = currencyId.split('_') + currencyId = self.safe_string(parts, 0) + networkId = self.safe_string(parts, 1) + if networkId is not None: + network = networkId.upper() + code = self.safe_currency_code(currencyId, currency) + feeCost = self.safe_number(transaction, 'fee') + fee = None + if feeCost is not None: + fee = {'currency': code, 'cost': feeCost} + return { + 'info': transaction, + 'id': id, + 'txid': txid, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': network, + 'address': addressTo, + 'addressTo': addressTo, + 'addressFrom': addressFrom, + 'tag': tagTo, + 'tagTo': tagTo, + 'tagFrom': tagFrom, + 'type': type, + 'amount': amount, + 'currency': code, + 'status': status, + 'updated': updated, + 'internal': False, + 'comment': None, + 'fee': fee, + } + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://github.com/Bitrue-exchange/Spot-official-api-docs#withdraw-commit--withdraw_data + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.check_address(address) + self.load_markets() + currency = self.currency(code) + request: dict = { + 'coin': currency['id'], + 'amount': amount, + 'addressTo': address, + # 'chainName': chainName, # 'ERC20', 'TRC20', 'SOL' + # 'addressMark': '', # mark of address + # 'addrType': '', # type of address + # 'tag': tag, + } + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + if networkCode is not None: + request['chainName'] = self.network_code_to_id(networkCode) + if tag is not None: + request['tag'] = tag + response = self.spotV1PrivatePostWithdrawCommit(self.extend(request, params)) + # + # { + # "code": 200, + # "msg": "succ", + # "data": { + # "msg": null, + # "amount": 1000, + # "fee": 1, + # "ctime": null, + # "coin": "usdt_erc20", + # "withdrawId": 1156423, + # "addressTo": "0x2edfae3878d7b6db70ce4abed177ab2636f60c83" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_transaction(data, currency) + + def parse_deposit_withdraw_fee(self, fee, currency: Currency = None): + # + # { + # "coin": "adx", + # "coinFulName": "Ambire AdEx", + # "chains": ["BSC"], + # "chainDetail": [[Object]] + # } + # + chainDetails = self.safe_list(fee, 'chainDetail', []) + chainDetailLength = len(chainDetails) + result: dict = { + 'info': fee, + 'withdraw': { + 'fee': None, + 'percentage': None, + }, + 'deposit': { + 'fee': None, + 'percentage': None, + }, + 'networks': {}, + } + if chainDetailLength != 0: + for i in range(0, chainDetailLength): + chainDetail = chainDetails[i] + networkId = self.safe_string(chainDetail, 'chain') + currencyCode = self.safe_string(currency, 'code') + networkCode = self.network_id_to_code(networkId, currencyCode) + result['networks'][networkCode] = { + 'deposit': {'fee': None, 'percentage': None}, + 'withdraw': {'fee': self.safe_number(chainDetail, 'withdrawFee'), 'percentage': False}, + } + if chainDetailLength == 1: + result['withdraw']['fee'] = self.safe_number(chainDetail, 'withdrawFee') + result['withdraw']['percentage'] = False + return result + + def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}): + """ + fetch deposit and withdraw fees + + https://github.com/Bitrue-exchange/Spot-official-api-docs#exchangeInfo_endpoint + + :param str[]|None codes: list of unified currency codes + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `fee structures ` + """ + self.load_markets() + response = self.spotV1PublicGetExchangeInfo(params) + coins = self.safe_list(response, 'coins') + return self.parse_deposit_withdraw_fees(coins, codes, 'coin') + + def parse_transfer(self, transfer, currency=None): + # + # fetchTransfers + # + # { + # 'transferType': 'wallet_to_contract', + # 'symbol': 'USDT', + # 'amount': 1.0, + # 'status': 1, + # 'ctime': 1685404575000 + # } + # + # transfer + # + # {} + # + transferType = self.safe_string(transfer, 'transferType') + fromAccount = None + toAccount = None + if transferType is not None: + accountSplit = transferType.split('_to_') + fromAccount = self.safe_string(accountSplit, 0) + toAccount = self.safe_string(accountSplit, 1) + timestamp = self.safe_integer(transfer, 'ctime') + return { + 'info': transfer, + 'id': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'currency': self.safe_string(currency, 'code'), + 'amount': self.safe_number(transfer, 'amount'), + 'fromAccount': fromAccount, + 'toAccount': toAccount, + 'status': 'ok', + } + + def fetch_transfers(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[TransferEntry]: + """ + fetch a history of internal transfers made on an account + + https://www.bitrue.com/api-docs#get-future-account-transfer-history-list-user_data-hmac-sha256 + https://www.bitrue.com/api_docs_includes_file/delivery.html#get-future-account-transfer-history-list-user_data-hmac-sha256 + + :param str code: unified currency code of the currency transferred + :param int [since]: the earliest time in ms to fetch transfers for + :param int [limit]: the maximum number of transfers structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch transfers for + :param str [params.type]: transfer type wallet_to_contract or contract_to_wallet + :returns dict[]: a list of `transfer structures ` + """ + self.load_markets() + type = self.safe_string_2(params, 'type', 'transferType') + request: dict = { + 'transferType': type, + } + currency = None + if code is not None: + currency = self.currency(code) + request['coinSymbol'] = currency['id'] + if since is not None: + request['beginTime'] = since + if limit is not None: + if limit > 200: + limit = 200 + request['limit'] = limit + until = self.safe_integer(params, 'until') + if until is not None: + params = self.omit(params, 'until') + request['endTime'] = until + response = self.fapiV2PrivateGetFuturesTransferHistory(self.extend(request, params)) + # + # { + # 'code': '0', + # 'msg': 'Success', + # 'data': [{ + # 'transferType': 'wallet_to_contract', + # 'symbol': 'USDT', + # 'amount': 1.0, + # 'status': 1, + # 'ctime': 1685404575000 + # }] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_transfers(data, currency, since, limit) + + def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://www.bitrue.com/api-docs#new-future-account-transfer-user_data-hmac-sha256 + https://www.bitrue.com/api_docs_includes_file/delivery.html#user-commission-rate-user_data-hmac-sha256 + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account to transfer from + :param str toAccount: account to transfer to + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transfer structure ` + """ + self.load_markets() + currency = self.currency(code) + accountTypes = self.safe_dict(self.options, 'accountsByType', {}) + fromId = self.safe_string(accountTypes, fromAccount, fromAccount) + toId = self.safe_string(accountTypes, toAccount, toAccount) + request: dict = { + 'coinSymbol': currency['id'], + 'amount': self.currency_to_precision(code, amount), + 'transferType': fromId + '_to_' + toId, + } + response = self.fapiV2PrivatePostFuturesTransfer(self.extend(request, params)) + # + # { + # 'code': '0', + # 'msg': 'Success', + # 'data': null + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_transfer(data, currency) + + def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://www.bitrue.com/api-docs#change-initial-leverage-trade-hmac-sha256 + https://www.bitrue.com/api_docs_includes_file/delivery.html#change-initial-leverage-trade-hmac-sha256 + + :param float leverage: the rate of leverage + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setLeverage() requires a symbol argument') + if (leverage < 1) or (leverage > 125): + raise BadRequest(self.id + ' leverage should be between 1 and 125') + self.load_markets() + market = self.market(symbol) + response = None + request: dict = { + 'contractName': market['id'], + 'leverage': leverage, + } + if not market['swap']: + raise NotSupported(self.id + ' setLeverage only support swap markets') + if market['linear']: + response = self.fapiV2PrivatePostLevelEdit(self.extend(request, params)) + elif market['inverse']: + response = self.dapiV2PrivatePostLevelEdit(self.extend(request, params)) + return response + + def parse_margin_modification(self, data, market=None) -> MarginModification: + # + # setMargin + # + # { + # "code": 0, + # "msg": "success" + # "data": null + # } + # + return { + 'info': data, + 'symbol': market['symbol'], + 'type': None, + 'marginMode': 'isolated', + 'amount': None, + 'total': None, + 'code': None, + 'status': None, + 'timestamp': None, + 'datetime': None, + } + + def set_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + Either adds or reduces margin in an isolated position in order to set the margin to a specific value + + https://www.bitrue.com/api-docs#modify-isolated-position-margin-trade-hmac-sha256 + https://www.bitrue.com/api_docs_includes_file/delivery.html#modify-isolated-position-margin-trade-hmac-sha256 + + :param str symbol: unified market symbol of the market to set margin in + :param float amount: the amount to set the margin to + :param dict [params]: parameters specific to the exchange API endpoint + :returns dict: A `margin structure ` + """ + self.load_markets() + market = self.market(symbol) + if not market['swap']: + raise NotSupported(self.id + ' setMargin only support swap markets') + response = None + request: dict = { + 'contractName': market['id'], + 'amount': self.parse_to_numeric(amount), + } + if market['linear']: + response = self.fapiV2PrivatePostPositionMargin(self.extend(request, params)) + elif market['inverse']: + response = self.dapiV2PrivatePostPositionMargin(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "success" + # "data": null + # } + # + return self.parse_margin_modification(response, market) + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + type = self.safe_string(api, 0) + version = self.safe_string(api, 1) + access = self.safe_string(api, 2) + url = None + if (type == 'api' and version == 'kline') or (type == 'open' and path.find('listenKey') >= 0): + url = self.urls['api'][type] + else: + url = self.urls['api'][type] + '/' + version + url = url + '/' + self.implode_params(path, params) + params = self.omit(params, self.extract_params(path)) + if access == 'private': + self.check_required_credentials() + recvWindow = self.safe_integer(self.options, 'recvWindow', 5000) + if type == 'spot' or type == 'open': + query = self.urlencode(self.extend({ + 'timestamp': self.nonce(), + 'recvWindow': recvWindow, + }, params)) + signature = self.hmac(self.encode(query), self.encode(self.secret), hashlib.sha256) + query += '&' + 'signature=' + signature + headers = { + 'X-MBX-APIKEY': self.apiKey, + } + if (method == 'GET') or (method == 'DELETE'): + url += '?' + query + else: + body = query + headers['Content-Type'] = 'application/x-www-form-urlencoded' + else: + timestamp = str(self.nonce()) + signPath = None + if type == 'fapi': + signPath = '/fapi' + elif type == 'dapi': + signPath = '/dapi' + signPath = signPath + '/' + version + '/' + path + signMessage = timestamp + method + signPath + if method == 'GET': + keys = list(params.keys()) + keysLength = len(keys) + if keysLength > 0: + signMessage += '?' + self.urlencode(params) + signature = self.hmac(self.encode(signMessage), self.encode(self.secret), hashlib.sha256) + headers = { + 'X-CH-APIKEY': self.apiKey, + 'X-CH-SIGN': signature, + 'X-CH-TS': timestamp, + } + url += '?' + self.urlencode(params) + else: + query = self.extend({ + 'recvWindow': recvWindow, + }, params) + body = self.json(query) + signMessage += body + signature = self.hmac(self.encode(signMessage), self.encode(self.secret), hashlib.sha256) + headers = { + 'Content-Type': 'application/json', + 'X-CH-APIKEY': self.apiKey, + 'X-CH-SIGN': signature, + 'X-CH-TS': timestamp, + } + else: + if params: + url += '?' + self.urlencode(params) + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if (code == 418) or (code == 429): + raise DDoSProtection(self.id + ' ' + str(code) + ' ' + reason + ' ' + body) + # error response in a form: {"code": -1013, "msg": "Invalid quantity."} + # following block cointains legacy checks against message patterns in "msg" property + # will switch "code" checks eventually, when we know all of them + if code >= 400: + if body.find('Price * QTY is zero or less') >= 0: + raise InvalidOrder(self.id + ' order cost = amount * price is zero or less ' + body) + if body.find('LOT_SIZE') >= 0: + raise InvalidOrder(self.id + ' order amount should be evenly divisible by lot size ' + body) + if body.find('PRICE_FILTER') >= 0: + raise InvalidOrder(self.id + ' order price is invalid, i.e. exceeds allowed price precision, exceeds min price or max price limits or is invalid float value in general, use self.price_to_precision(symbol, amount) ' + body) + if response is None: + return None # fallback to default error handler + # check success value for wapi endpoints + # response in format {'msg': 'The coin does not exist.', 'success': True/false} + success = self.safe_bool(response, 'success', True) + if not success: + messageInner = self.safe_string(response, 'msg') + parsedMessage = None + if messageInner is not None: + try: + parsedMessage = json.loads(messageInner) + except Exception as e: + # do nothing + parsedMessage = None + if parsedMessage is not None: + response = parsedMessage + message = self.safe_string(response, 'msg') + if message is not None: + self.throw_exactly_matched_exception(self.exceptions['exact'], message, self.id + ' ' + message) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, self.id + ' ' + message) + # checks against error codes + error = self.safe_string(response, 'code') + if error is not None: + # https://github.com/ccxt/ccxt/issues/6501 + # https://github.com/ccxt/ccxt/issues/7742 + if (error == '200') or Precise.string_equals(error, '0'): + return None + # a workaround for {"code":-2015,"msg":"Invalid API-key, IP, or permissions for action."} + # despite that their message is very confusing, it is raised by Binance + # on a temporary ban, the API key is valid, but disabled for a while + if (error == '-2015') and self.options['hasAlreadyAuthenticatedSuccessfully']: + raise DDoSProtection(self.id + ' temporary banned: ' + body) + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], error, feedback) + raise ExchangeError(feedback) + if not success: + raise ExchangeError(self.id + ' ' + body) + return None + + def calculate_rate_limiter_cost(self, api, method, path, params, config={}): + if ('noSymbol' in config) and not ('symbol' in params): + return config['noSymbol'] + elif ('byLimit' in config) and ('limit' in params): + limit = params['limit'] + byLimit = config['byLimit'] + for i in range(0, len(byLimit)): + entry = byLimit[i] + if limit <= entry[0]: + return entry[1] + return self.safe_value(config, 'cost', 1) diff --git a/ccxt/bitso.py b/ccxt/bitso.py new file mode 100644 index 0000000..1a6e84c --- /dev/null +++ b/ccxt/bitso.py @@ -0,0 +1,1816 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.bitso import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currency, DepositAddress, Int, LedgerEntry, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Trade, TradingFees, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import NotSupported +from ccxt.base.errors import InvalidNonce +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class bitso(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(bitso, self).describe(), { + 'id': 'bitso', + 'name': 'Bitso', + 'countries': ['MX'], # Mexico + 'rateLimit': 2000, # 30 requests per minute + 'version': 'v3', + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'cancelOrders': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createDepositAddress': False, + 'createOrder': True, + 'createOrderWithTakeProfitAndStopLoss': False, + 'createOrderWithTakeProfitAndStopLossWs': False, + 'createReduceOnlyOrder': False, + 'fetchAccounts': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': False, + 'fetchDeposit': True, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchDepositsWithdrawals': False, + 'fetchDepositWithdrawFee': 'emulated', + 'fetchDepositWithdrawFees': True, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLedger': True, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrderTrades': True, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTickers': False, + 'fetchTime': False, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': True, + 'fetchTransactionFee': False, + 'fetchTransactionFees': True, + 'fetchTransactions': False, + 'fetchTransfer': False, + 'fetchTransfers': False, + 'fetchVolatilityHistory': False, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'transfer': False, + 'withdraw': True, + }, + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/178c8e56-9054-4107-b192-5e5053d4f975', + 'api': { + 'rest': 'https://bitso.com/api', + }, + 'test': { + 'rest': 'https://stage.bitso.com/api', + }, + 'www': 'https://bitso.com', + 'doc': 'https://bitso.com/api_info', + 'fees': 'https://bitso.com/fees', + 'referral': 'https://bitso.com/?ref=itej', + }, + 'precisionMode': TICK_SIZE, + 'options': { + 'precision': { + 'XRP': 0.000001, + 'MXN': 0.01, + 'TUSD': 0.01, + }, + 'defaultPrecision': 0.00000001, + 'networks': { + 'TRC20': 'trx', + 'ERC20': 'erc20', + 'BEP20': 'bsc', + 'BEP2': 'bep2', + }, + }, + 'timeframes': { + '1m': '60', + '5m': '300', + '15m': '900', + '30m': '1800', + '1h': '3600', + '4h': '14400', + '12h': '43200', + '1d': '86400', + '1w': '604800', + }, + 'api': { + 'public': { + 'get': [ + 'available_books', + 'ticker', + 'order_book', + 'trades', + 'ohlc', + ], + }, + 'private': { + 'get': [ + 'account_status', + 'balance', + 'fees', + 'fundings', + 'fundings/{fid}', + 'funding_destination', + 'kyc_documents', + 'ledger', + 'ledger/trades', + 'ledger/fees', + 'ledger/fundings', + 'ledger/withdrawals', + 'mx_bank_codes', + 'open_orders', + 'order_trades/{oid}', + 'orders/{oid}', + 'user_trades', + 'user_trades/{tid}', + 'withdrawals/', + 'withdrawals/{wid}', + ], + 'post': [ + 'bitcoin_withdrawal', + 'debit_card_withdrawal', + 'ether_withdrawal', + 'orders', + 'phone_number', + 'phone_verification', + 'phone_withdrawal', + 'spei_withdrawal', + 'ripple_withdrawal', + 'bcash_withdrawal', + 'litecoin_withdrawal', + ], + 'delete': [ + 'orders', + 'orders/{oid}', + 'orders/all', + ], + }, + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, # todo implementation + 'triggerPriceType': None, + 'triggerDirection': None, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': None, + # todo: implementation for TIF + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyRequiresPrice': False, + 'marketBuyByCost': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, + 'daysBack': None, + 'untilDays': None, + 'symbolRequired': True, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 500, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOrders': None, + 'fetchClosedOrders': None, + 'fetchOHLCV': { + 'limit': 300, + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'exceptions': { + '0201': AuthenticationError, # Invalid Nonce or Invalid Credentials + '104': InvalidNonce, # Cannot perform request - nonce must be higher than 1520307203724237 + '0304': BadRequest, # {"success":false,"error":{"code":"0304","message":"The field time_bucket() is either invalid or missing"}} + }, + }) + + def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + :param str [code]: unified currency code, default is None + :param int [since]: timestamp in ms of the earliest ledger entry, default is None + :param int [limit]: max number of ledger entries to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ledger structure ` + """ + request: dict = {} + if limit is not None: + request['limit'] = limit + response = self.privateGetLedger(self.extend(request, params)) + # + # { + # "success": True, + # "payload": [{ + # "eid": "2510b3e2bc1c87f584500a18084f35ed", + # "created_at": "2022-06-08T12:21:42+0000", + # "balance_updates": [{ + # "amount": "0.00080000", + # "currency": "btc" + # }], + # "operation": "funding", + # "details": { + # "network": "btc", + # "method": "btc", + # "method_name": "Bitcoin", + # "asset": "btc", + # "protocol": "btc", + # "integration": "bitgo-v2", + # "fid": "6112c6369100d6ecceb7f54f17cf0511" + # } + # }] + # } + # + payload = self.safe_value(response, 'payload', []) + currency = self.safe_currency(code) + return self.parse_ledger(payload, currency, since, limit) + + def parse_ledger_entry_type(self, type): + types: dict = { + 'funding': 'transaction', + 'withdrawal': 'transaction', + 'trade': 'trade', + 'fee': 'fee', + } + return self.safe_string(types, type, type) + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + # + # { + # "eid": "2510b3e2bc1c87f584500a18084f35ed", + # "created_at": "2022-06-08T12:21:42+0000", + # "balance_updates": [{ + # "amount": "0.00080000", + # "currency": "btc" + # }], + # "operation": "funding", + # "details": { + # "network": "btc", + # "method": "btc", + # "method_name": "Bitcoin", + # "asset": "btc", + # "protocol": "btc", + # "integration": "bitgo-v2", + # "fid": "6112c6369100d6ecceb7f54f17cf0511" + # } + # } + # + # trade + # { + # "eid": "8976c6053f078f704f037d82a813678a", + # "created_at": "2022-06-08T17:01:48+0000", + # "balance_updates": [{ + # "amount": "59.21320500", + # "currency": "mxn" + # }, + # { + # "amount": "-0.00010000", + # "currency": "btc" + # } + # ], + # "operation": "trade", + # "details": { + # "tid": "72145428", + # "oid": "JO5TZmMZjzjlZDyT" + # } + # } + # + # fee + # { + # "eid": "cbbb3c8d4e41723d25d2850dcb7c3c74", + # "created_at": "2022-06-08T17:01:48+0000", + # "balance_updates": [{ + # "amount": "-0.38488583", + # "currency": "mxn" + # }], + # "operation": "fee", + # "details": { + # "tid": "72145428", + # "oid": "JO5TZmMZjzjlZDyT" + # } + # } + operation = self.safe_string(item, 'operation') + type = self.parse_ledger_entry_type(operation) + balanceUpdates = self.safe_value(item, 'balance_updates', []) + firstBalance = self.safe_value(balanceUpdates, 0, {}) + direction = None + fee = None + amount = self.safe_string(firstBalance, 'amount') + currencyId = self.safe_string(firstBalance, 'currency') + code = self.safe_currency_code(currencyId, currency) + currency = self.safe_currency(currencyId, currency) + details = self.safe_value(item, 'details', {}) + referenceId = self.safe_string_2(details, 'fid', 'wid') + if referenceId is None: + referenceId = self.safe_string(details, 'tid') + if operation == 'funding': + direction = 'in' + elif operation == 'withdrawal': + direction = 'out' + elif operation == 'trade': + direction = None + elif operation == 'fee': + direction = 'out' + cost = Precise.string_abs(amount) + fee = { + 'cost': cost, + 'currency': currency, + } + timestamp = self.parse8601(self.safe_string(item, 'created_at')) + return self.safe_ledger_entry({ + 'info': item, + 'id': self.safe_string(item, 'eid'), + 'direction': direction, + 'account': None, + 'referenceId': referenceId, + 'referenceAccount': None, + 'type': type, + 'currency': code, + 'amount': amount, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'before': None, + 'after': None, + 'status': 'ok', + 'fee': fee, + }, currency) + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for bitso + + https://docs.bitso.com/bitso-api/docs/list-available-books + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = self.publicGetAvailableBooks(params) + # + # { + # "success":true, + # "payload":[ + # { + # "book":"btc_mxn", + # "minimum_price":"500", + # "maximum_price":"10000000", + # "minimum_amount":"0.00005", + # "maximum_amount":"500", + # "minimum_value":"5", + # "maximum_value":"10000000", + # "tick_size":"0.01", + # "fees":{ + # "flat_rate":{"maker":"0.500","taker":"0.650"}, + # "structure":[ + # {"volume":"1500000","maker":"0.00500","taker":"0.00650"}, + # {"volume":"2000000","maker":"0.00490","taker":"0.00637"}, + # {"volume":"5000000","maker":"0.00480","taker":"0.00624"}, + # {"volume":"7000000","maker":"0.00440","taker":"0.00572"}, + # {"volume":"10000000","maker":"0.00420","taker":"0.00546"}, + # {"volume":"15000000","maker":"0.00400","taker":"0.00520"}, + # {"volume":"35000000","maker":"0.00370","taker":"0.00481"}, + # {"volume":"50000000","maker":"0.00300","taker":"0.00390"}, + # {"volume":"150000000","maker":"0.00200","taker":"0.00260"}, + # {"volume":"250000000","maker":"0.00100","taker":"0.00130"}, + # {"volume":"9999999999","maker":"0.00000","taker":"0.00130"}, + # ] + # } + # }, + # ] + # } + markets = self.safe_value(response, 'payload', []) + result = [] + for i in range(0, len(markets)): + market = markets[i] + id = self.safe_string(market, 'book') + baseId, quoteId = id.split('_') + base = baseId.upper() + quote = quoteId.upper() + base = self.safe_currency_code(base) + quote = self.safe_currency_code(quote) + fees = self.safe_value(market, 'fees', {}) + flatRate = self.safe_value(fees, 'flat_rate', {}) + takerString = self.safe_string(flatRate, 'taker') + makerString = self.safe_string(flatRate, 'maker') + taker = self.parse_number(Precise.string_div(takerString, '100')) + maker = self.parse_number(Precise.string_div(makerString, '100')) + feeTiers = self.safe_value(fees, 'structure', []) + fee = { + 'taker': taker, + 'maker': maker, + 'percentage': True, + 'tierBased': True, + } + takerFees = [] + makerFees = [] + for j in range(0, len(feeTiers)): + tier = feeTiers[j] + volume = self.safe_number(tier, 'volume') + takerFee = self.safe_number(tier, 'taker') + makerFee = self.safe_number(tier, 'maker') + takerFees.append([volume, takerFee]) + makerFees.append([volume, makerFee]) + if j == 0: + fee['taker'] = takerFee + fee['maker'] = makerFee + tiers: dict = { + 'taker': takerFees, + 'maker': makerFees, + } + fee['tiers'] = tiers + # TODO: precisions can be also set from https://bitso.com/api/v3/catalogues ->available_currency_conversions->currencies(or ->currencies->metadata) or https://bitso.com/api/v3/get_exchange_rates/mxn + defaultPricePrecision = self.safe_number(self.options['precision'], quote, self.options['defaultPrecision']) + result.append(self.extend({ + 'id': id, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': None, + 'contract': False, + 'linear': None, + 'inverse': None, + 'taker': taker, + 'maker': maker, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.safe_number(self.options['precision'], base, self.options['defaultPrecision']), + 'price': self.safe_number(market, 'tick_size', defaultPricePrecision), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(market, 'minimum_amount'), + 'max': self.safe_number(market, 'maximum_amount'), + }, + 'price': { + 'min': self.safe_number(market, 'minimum_price'), + 'max': self.safe_number(market, 'maximum_price'), + }, + 'cost': { + 'min': self.safe_number(market, 'minimum_value'), + 'max': self.safe_number(market, 'maximum_value'), + }, + }, + 'created': None, + 'info': market, + }, fee)) + return result + + def parse_balance(self, response) -> Balances: + payload = self.safe_value(response, 'payload', {}) + balances = self.safe_value(payload, 'balances', []) + result: dict = { + 'info': response, + 'timestamp': None, + 'datetime': None, + } + for i in range(0, len(balances)): + balance = balances[i] + currencyId = self.safe_string(balance, 'currency') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(balance, 'available') + account['used'] = self.safe_string(balance, 'locked') + account['total'] = self.safe_string(balance, 'total') + result[code] = account + return self.safe_balance(result) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://docs.bitso.com/bitso-api/docs/get-account-balance + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + response = self.privateGetBalance(params) + # + # { + # "success": True, + # "payload": { + # "balances": [ + # { + # "currency": "bat", + # "available": "0.00000000", + # "locked": "0.00000000", + # "total": "0.00000000", + # "pending_deposit": "0.00000000", + # "pending_withdrawal": "0.00000000" + # }, + # { + # "currency": "bch", + # "available": "0.00000000", + # "locked": "0.00000000", + # "total": "0.00000000", + # "pending_deposit": "0.00000000", + # "pending_withdrawal": "0.00000000" + # }, + # ], + # }, + # } + # + return self.parse_balance(response) + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://docs.bitso.com/bitso-api/docs/list-order-book + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'book': market['id'], + } + response = self.publicGetOrderBook(self.extend(request, params)) + orderbook = self.safe_value(response, 'payload') + timestamp = self.parse8601(self.safe_string(orderbook, 'updated_at')) + return self.parse_order_book(orderbook, market['symbol'], timestamp, 'bids', 'asks', 'price', 'amount') + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "high":"37446.85", + # "last":"36599.54", + # "created_at":"2022-01-28T12:06:11+00:00", + # "book":"btc_usdt", + # "volume":"7.29075419", + # "vwap":"36579.1564400307", + # "low":"35578.52", + # "ask":"36574.76", + # "bid":"36538.22", + # "change_24":"-105.64" + # } + # + symbol = self.safe_symbol(None, market) + timestamp = self.parse8601(self.safe_string(ticker, 'created_at')) + vwap = self.safe_string(ticker, 'vwap') + baseVolume = self.safe_string(ticker, 'volume') + quoteVolume = Precise.string_mul(baseVolume, vwap) + last = self.safe_string(ticker, 'last') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': self.safe_string(ticker, 'bid'), + 'bidVolume': None, + 'ask': self.safe_string(ticker, 'ask'), + 'askVolume': None, + 'vwap': vwap, + 'open': None, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'info': ticker, + }, market) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://docs.bitso.com/bitso-api/docs/ticker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'book': market['id'], + } + response = self.publicGetTicker(self.extend(request, params)) + ticker = self.safe_value(response, 'payload') + # + # { + # "success":true, + # "payload":{ + # "high":"37446.85", + # "last":"37051.96", + # "created_at":"2022-01-28T17:03:29+00:00", + # "book":"btc_usdt", + # "volume":"6.16176186", + # "vwap":"36582.6293169472", + # "low":"35578.52", + # "ask":"37083.62", + # "bid":"37039.66", + # "change_24":"478.45" + # } + # } + # + return self.parse_ticker(ticker, market) + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'book': market['id'], + 'time_bucket': self.safe_string(self.timeframes, timeframe, timeframe), + } + if since is not None: + request['start'] = since + if limit is not None: + duration = self.parse_timeframe(timeframe) + request['end'] = self.sum(since, duration * limit * 1000) + elif limit is not None: + now = self.milliseconds() + request['end'] = now + request['start'] = now - self.parse_timeframe(timeframe) * 1000 * limit + response = self.publicGetOhlc(self.extend(request, params)) + # + # { + # "success":true, + # "payload": [ + # { + # "bucket_start_time":1648219140000, + # "first_trade_time":1648219154990, + # "last_trade_time":1648219189442, + # "first_rate":"44958.60", + # "last_rate":"44979.88", + # "min_rate":"44957.33", + # "max_rate":"44979.88", + # "trade_count":8, + # "volume":"0.00082814", + # "vwap":"44965.02" + # }, + # ] + # } + # + payload = self.safe_list(response, 'payload', []) + return self.parse_ohlcvs(payload, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # { + # "bucket_start_time":1648219140000, + # "first_trade_time":1648219154990, + # "last_trade_time":1648219189441, + # "first_rate":"44958.60", + # "last_rate":"44979.88", + # "min_rate":"44957.33", + # "max_rate":"44979.88", + # "trade_count":8, + # "volume":"0.00082814", + # "vwap":"44965.02" + # }, + # + return [ + self.safe_integer(ohlcv, 'bucket_start_time'), + self.safe_number(ohlcv, 'first_rate'), + self.safe_number(ohlcv, 'max_rate'), + self.safe_number(ohlcv, 'min_rate'), + self.safe_number(ohlcv, 'last_rate'), + self.safe_number(ohlcv, 'volume'), + ] + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(public) + # + # { + # "book": "btc_usdt", + # "created_at": "2021-11-24T12:14:53+0000", + # "amount": "0.00026562", + # "maker_side": "sell", + # "price": "56471.55", + # "tid": "52557338" + # } + # + # fetchMyTrades(private) + # + # { + # "book": "btc_usdt", + # "created_at": "2021-11-24T12:31:03+0000", + # "minor": "11.30356000", + # "major": "-0.00020000", + # "fees_amount": "0.01119052", + # "fees_currency": "usdt", + # "minor_currency": "usdt", + # "major_currency": "btc", + # "oid": "djTzMIWx2Vi3iMjl", + # "tid": "52559051", + # "price": "56517.80", + # "side": "sell", + # "maker_side": "buy" + # } + # + # fetchOrderTrades(private) + # + # { + # "book": "btc_usdt", + # "created_at": "2021-11-24T12:30:52+0000", + # "minor": "-11.33047916", + # "major": "0.00020020", + # "fees_amount": "0.00000020", + # "fees_currency": "btc", + # "minor_currency": "usdt", + # "major_currency": "btc", + # "oid": "O0D2zcljjjQF5xlG", + # "tid": "52559030", + # "price": "56595.80", + # "side": "buy", + # "maker_side": "sell" + # } + # + timestamp = self.parse8601(self.safe_string(trade, 'created_at')) + marketId = self.safe_string(trade, 'book') + symbol = self.safe_symbol(marketId, market, '_') + side = self.safe_string(trade, 'side') + makerSide = self.safe_string(trade, 'maker_side') + takerOrMaker = None + if side is not None: + if side == makerSide: + takerOrMaker = 'maker' + else: + takerOrMaker = 'taker' + else: + if makerSide == 'buy': + side = 'sell' + else: + side = 'buy' + amount = self.safe_string_2(trade, 'amount', 'major') + if amount is not None: + amount = Precise.string_abs(amount) + fee = None + feeCost = self.safe_string(trade, 'fees_amount') + if feeCost is not None: + feeCurrencyId = self.safe_string(trade, 'fees_currency') + feeCurrency = self.safe_currency_code(feeCurrencyId) + fee = { + 'cost': feeCost, + 'currency': feeCurrency, + } + cost = self.safe_string(trade, 'minor') + if cost is not None: + cost = Precise.string_abs(cost) + price = self.safe_string(trade, 'price') + orderId = self.safe_string(trade, 'oid') + id = self.safe_string(trade, 'tid') + return self.safe_trade({ + 'id': id, + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'order': orderId, + 'type': None, + 'side': side, + 'takerOrMaker': takerOrMaker, + 'price': price, + 'amount': amount, + 'cost': cost, + 'fee': fee, + }, market) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://docs.bitso.com/bitso-api/docs/list-trades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'book': market['id'], + } + response = self.publicGetTrades(self.extend(request, params)) + return self.parse_trades(response['payload'], market, since, limit) + + def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://docs.bitso.com/bitso-api/docs/list-fees + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + self.load_markets() + response = self.privateGetFees(params) + # + # { + # "success": True, + # "payload": { + # "fees": [ + # { + # "book": "btc_mxn", + # "fee_percent": "0.6500", + # "fee_decimal": "0.00650000", + # "taker_fee_percent": "0.6500", + # "taker_fee_decimal": "0.00650000", + # "maker_fee_percent": "0.5000", + # "maker_fee_decimal": "0.00500000", + # "volume_currency": "mxn", + # "current_volume": "0.00", + # "next_volume": "1500000.00", + # "next_maker_fee_percent": "0.490", + # "next_taker_fee_percent": "0.637", + # "nextVolume": "1500000.00", + # "nextFee": "0.490", + # "nextTakerFee": "0.637" + # }, + # ... + # ], + # "deposit_fees": [ + # { + # "currency": "btc", + # "method": "rewards", + # "fee": "0.00", + # "is_fixed": False + # }, + # ... + # ], + # "withdrawal_fees": { + # "ada": "0.20958100", + # "bch": "0.00009437", + # "ars": "0", + # "btc": "0.00001209", + # ... + # } + # } + # } + # + payload = self.safe_value(response, 'payload', {}) + fees = self.safe_value(payload, 'fees', []) + result: dict = {} + for i in range(0, len(fees)): + fee = fees[i] + marketId = self.safe_string(fee, 'book') + symbol = self.safe_symbol(marketId, None, '_') + result[symbol] = { + 'info': fee, + 'symbol': symbol, + 'maker': self.safe_number(fee, 'maker_fee_decimal'), + 'taker': self.safe_number(fee, 'taker_fee_decimal'), + 'percentage': True, + 'tierBased': True, + } + return result + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = 25, params={}): + """ + fetch all trades made by the user + + https://docs.bitso.com/bitso-api/docs/user-trades + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + # the don't support fetching trades starting from a date yet + # use the `marker` extra param for that + # self is not a typo, the variable name is 'marker'(don't confuse with 'market') + markerInParams = ('marker' in params) + # warn the user with an exception if the user wants to filter + # starting from since timestamp, but does not set the trade id with an extra 'marker' param + if (since is not None) and not markerInParams: + raise ExchangeError(self.id + ' fetchMyTrades() does not support fetching trades starting from a timestamp with the `since` argument, use the `marker` extra param to filter starting from an integer trade id') + # convert it to an integer unconditionally + if markerInParams: + params = self.extend(params, { + 'marker': int(params['marker']), + }) + request: dict = { + 'book': market['id'], + 'limit': limit, # default = 25, max = 100 + # 'sort': 'desc', # default = desc + # 'marker': id, # integer id to start from + } + response = self.privateGetUserTrades(self.extend(request, params)) + return self.parse_trades(response['payload'], market, since, limit) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://docs.bitso.com/bitso-api/docs/place-an-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'book': market['id'], + 'side': side, + 'type': type, + 'major': self.amount_to_precision(market['symbol'], amount), + } + if type == 'limit': + request['price'] = self.price_to_precision(market['symbol'], price) + response = self.privatePostOrders(self.extend(request, params)) + id = self.safe_string(response['payload'], 'oid') + return self.safe_order({ + 'info': response, + 'id': id, + }, market) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://docs.bitso.com/bitso-api/docs/cancel-an-order + + :param str id: order id + :param str symbol: not used by bitso cancelOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + request: dict = { + 'oid': id, + } + response = self.privateDeleteOrdersOid(self.extend(request, params)) + # + # { + # "success": True, + # "payload": ["yWTQGxDMZ0VimZgZ"] + # } + # + payload = self.safe_list(response, 'payload', []) + orderId = self.safe_string(payload, 0) + return self.safe_order({ + 'info': response, + 'id': orderId, + }) + + def cancel_orders(self, ids: List[str], symbol: Str = None, params={}) -> List[Order]: + """ + cancel multiple orders + + https://docs.bitso.com/bitso-api/docs/cancel-an-order + + :param str[] ids: order ids + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an list of `order structures ` + """ + if not isinstance(ids, list): + raise ArgumentsRequired(self.id + ' cancelOrders() ids argument should be an array') + market = None + if symbol is not None: + market = self.market(symbol) + oids = ','.join(ids) + request: dict = { + 'oids': oids, + } + response = self.privateDeleteOrders(self.extend(request, params)) + # + # { + # "success": True, + # "payload": ["yWTQGxDMZ0VimZgZ"] + # } + # + payload = self.safe_value(response, 'payload', []) + orders = [] + for i in range(0, len(payload)): + id = payload[i] + orders.append(self.parse_order(id, market)) + return orders + + def cancel_all_orders(self, symbol: Str = None, params={}) -> List[Order]: + """ + cancel all open orders + + https://docs.bitso.com/bitso-api/docs/cancel-an-order + + :param None symbol: bitso does not support canceling orders for only a specific market + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + if symbol is not None: + raise NotSupported(self.id + ' cancelAllOrders() deletes all orders for user, it does not support filtering by symbol.') + response = self.privateDeleteOrdersAll(params) + # + # { + # "success": True, + # "payload": ["NWUZUYNT12ljwzDT", "kZUkZmQ2TTjkkYTY"] + # } + # + payload = self.safe_value(response, 'payload', []) + canceledOrders = [] + for i in range(0, len(payload)): + order = self.parse_order(payload[i]) + canceledOrders.append(order) + return canceledOrders + + def parse_order_status(self, status: Str): + statuses: dict = { + 'partial-fill': 'open', # self is a common substitution in ccxt + 'partially filled': 'open', + 'queued': 'open', + 'completed': 'closed', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # + # canceledOrder + # yWTQGxDMZ0VimZgZ + # + id = None + if isinstance(order, str): + id = order + else: + id = self.safe_string(order, 'oid') + side = self.safe_string(order, 'side') + status = self.parse_order_status(self.safe_string(order, 'status')) + marketId = self.safe_string(order, 'book') + symbol = self.safe_symbol(marketId, market, '_') + orderType = self.safe_string(order, 'type') + timestamp = self.parse8601(self.safe_string(order, 'created_at')) + price = self.safe_string(order, 'price') + amount = self.safe_string(order, 'original_amount') + remaining = self.safe_string(order, 'unfilled_amount') + clientOrderId = self.safe_string(order, 'client_id') + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'symbol': symbol, + 'type': orderType, + 'timeInForce': None, + 'postOnly': None, + 'side': side, + 'price': price, + 'triggerPrice': None, + 'amount': amount, + 'cost': None, + 'remaining': remaining, + 'filled': None, + 'status': status, + 'fee': None, + 'average': None, + 'trades': None, + }, market) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = 25, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://docs.bitso.com/bitso-api/docs/list-open-orders + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + market = self.market(symbol) + # the don't support fetching trades starting from a date yet + # use the `marker` extra param for that + # self is not a typo, the variable name is 'marker'(don't confuse with 'market') + markerInParams = ('marker' in params) + # warn the user with an exception if the user wants to filter + # starting from since timestamp, but does not set the trade id with an extra 'marker' param + if (since is not None) and not markerInParams: + raise ExchangeError(self.id + ' fetchOpenOrders() does not support fetching orders starting from a timestamp with the `since` argument, use the `marker` extra param to filter starting from an integer trade id') + # convert it to an integer unconditionally + if markerInParams: + params = self.extend(params, { + 'marker': int(params['marker']), + }) + request: dict = { + 'book': market['id'], + 'limit': limit, # default = 25, max = 100 + # 'sort': 'desc', # default = desc + # 'marker': id, # integer id to start from + } + response = self.privateGetOpenOrders(self.extend(request, params)) + orders = self.parse_orders(response['payload'], market, since, limit) + return orders + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://docs.bitso.com/bitso-api/docs/look-up-orders + + :param str id: the order id + :param str symbol: not used by bitso fetchOrder + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + response = self.privateGetOrdersOid({ + 'oid': id, + }) + payload = self.safe_value(response, 'payload') + if isinstance(payload, list): + numOrders = len(response['payload']) + if numOrders == 1: + return self.parse_order(payload[0]) + raise OrderNotFound(self.id + ': The order ' + id + ' not found.') + + def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all the trades made from a single order + + https://docs.bitso.com/bitso-api/docs/list-user-trades + + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'oid': id, + } + response = self.privateGetOrderTradesOid(self.extend(request, params)) + return self.parse_trades(response['payload'], market) + + def fetch_deposit(self, id: str, code: Str = None, params={}): + """ + fetch information on a deposit + + https://docs.bitso.com/bitso-payouts-funding/docs/fundings + + :param str id: deposit id + :param str code: bitso does not support filtering by currency code and will ignore self argument + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + self.load_markets() + request: dict = { + 'fid': id, + } + response = self.privateGetFundingsFid(self.extend(request, params)) + # + # { + # "success": True, + # "payload": [{ + # "fid": "6112c6369100d6ecceb7f54f17cf0511", + # "status": "complete", + # "created_at": "2022-06-08T12:02:49+0000", + # "currency": "btc", + # "method": "btc", + # "method_name": "Bitcoin", + # "amount": "0.00080000", + # "asset": "btc", + # "network": "btc", + # "protocol": "btc", + # "integration": "bitgo-v2", + # "details": { + # "receiving_address": "3N2vbcYKhogs6RoTb4eYCUJ3beRSqLgSif", + # "tx_hash": "327f3838531f211485ec59f9d0a119fea1595591e274d942b2c10b9b8262eb1d", + # "confirmations": "4" + # } + # }] + # } + # + transactions = self.safe_value(response, 'payload', []) + first = self.safe_dict(transactions, 0, {}) + return self.parse_transaction(first) + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://docs.bitso.com/bitso-payouts-funding/docs/fundings + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + currency = None + if code is not None: + currency = self.currency(code) + response = self.privateGetFundings(params) + # + # { + # "success": True, + # "payload": [{ + # "fid": "6112c6369100d6ecceb7f54f17cf0511", + # "status": "complete", + # "created_at": "2022-06-08T12:02:49+0000", + # "currency": "btc", + # "method": "btc", + # "method_name": "Bitcoin", + # "amount": "0.00080000", + # "asset": "btc", + # "network": "btc", + # "protocol": "btc", + # "integration": "bitgo-v2", + # "details": { + # "receiving_address": "3N2vbcYKhogs6RoTb4eYCUJ3beRSqLgSif", + # "tx_hash": "327f3838531f211485ec59f9d0a119fea1595591e274d942b2c10b9b8262eb1d", + # "confirmations": "4" + # } + # }] + # } + # + transactions = self.safe_list(response, 'payload', []) + return self.parse_transactions(transactions, currency, since, limit, params) + + def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'fund_currency': currency['id'], + } + response = self.privateGetFundingDestination(self.extend(request, params)) + address = self.safe_string(response['payload'], 'account_identifier') + tag = None + if address.find('?dt=') >= 0: + parts = address.split('?dt=') + address = self.safe_string(parts, 0) + tag = self.safe_string(parts, 1) + self.check_address(address) + return { + 'info': response, + 'currency': code, + 'network': None, + 'address': address, + 'tag': tag, + } + + def fetch_transaction_fees(self, codes: Strings = None, params={}): + """ + @deprecated + please use fetchDepositWithdrawFees instead + + https://docs.bitso.com/bitso-api/docs/list-fees + + :param str[]|None codes: list of unified currency codes + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `fee structures ` + """ + self.load_markets() + response = self.privateGetFees(params) + # + # { + # "success": True, + # "payload": { + # "fees": [ + # { + # "book": "btc_mxn", + # "fee_percent": "0.6500", + # "fee_decimal": "0.00650000", + # "taker_fee_percent": "0.6500", + # "taker_fee_decimal": "0.00650000", + # "maker_fee_percent": "0.5000", + # "maker_fee_decimal": "0.00500000", + # "volume_currency": "mxn", + # "current_volume": "0.00", + # "next_volume": "1500000.00", + # "next_maker_fee_percent": "0.490", + # "next_taker_fee_percent": "0.637", + # "nextVolume": "1500000.00", + # "nextFee": "0.490", + # "nextTakerFee": "0.637" + # }, + # ... + # ], + # "deposit_fees": [ + # { + # "currency": "btc", + # "method": "rewards", + # "fee": "0.00", + # "is_fixed": False + # }, + # ... + # ], + # "withdrawal_fees": { + # "ada": "0.20958100", + # "bch": "0.00009437", + # "ars": "0", + # "btc": "0.00001209", + # ... + # } + # } + # } + # + result: dict = {} + payload = self.safe_value(response, 'payload', {}) + depositFees = self.safe_value(payload, 'deposit_fees', []) + for i in range(0, len(depositFees)): + depositFee = depositFees[i] + currencyId = self.safe_string(depositFee, 'currency') + code = self.safe_currency_code(currencyId) + if (codes is not None) and not self.in_array(code, codes): + continue + result[code] = { + 'deposit': self.safe_number(depositFee, 'fee'), + 'withdraw': None, + 'info': { + 'deposit': depositFee, + 'withdraw': None, + }, + } + withdrawalFees = self.safe_value(payload, 'withdrawal_fees', []) + currencyIds = list(withdrawalFees.keys()) + for i in range(0, len(currencyIds)): + currencyId = currencyIds[i] + code = self.safe_currency_code(currencyId) + if (codes is not None) and not self.in_array(code, codes): + continue + result[code] = { + 'deposit': self.safe_value(result[code], 'deposit'), + 'withdraw': self.safe_number(withdrawalFees, currencyId), + 'info': { + 'deposit': self.safe_value(result[code]['info'], 'deposit'), + 'withdraw': self.safe_number(withdrawalFees, currencyId), + }, + } + return result + + def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}): + """ + fetch deposit and withdraw fees + + https://docs.bitso.com/bitso-api/docs/list-fees + + :param str[]|None codes: list of unified currency codes + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `fee structures ` + """ + self.load_markets() + response = self.privateGetFees(params) + # + # { + # "success": True, + # "payload": { + # "fees": [ + # { + # "book": "btc_mxn", + # "fee_percent": "0.6500", + # "fee_decimal": "0.00650000", + # "taker_fee_percent": "0.6500", + # "taker_fee_decimal": "0.00650000", + # "maker_fee_percent": "0.5000", + # "maker_fee_decimal": "0.00500000", + # "volume_currency": "mxn", + # "current_volume": "0.00", + # "next_volume": "1500000.00", + # "next_maker_fee_percent": "0.490", + # "next_taker_fee_percent": "0.637", + # "nextVolume": "1500000.00", + # "nextFee": "0.490", + # "nextTakerFee": "0.637" + # }, + # ... + # ], + # "deposit_fees": [ + # { + # "currency": "btc", + # "method": "rewards", + # "fee": "0.00", + # "is_fixed": False + # }, + # ... + # ], + # "withdrawal_fees": { + # "ada": "0.20958100", + # "bch": "0.00009437", + # "ars": "0", + # "btc": "0.00001209", + # ... + # } + # } + # } + # + payload = self.safe_list(response, 'payload', []) + return self.parse_deposit_withdraw_fees(payload, codes) + + def parse_deposit_withdraw_fees(self, response, codes=None, currencyIdKey=None): + # + # { + # "fees": [ + # { + # "book": "btc_mxn", + # "fee_percent": "0.6500", + # "fee_decimal": "0.00650000", + # "taker_fee_percent": "0.6500", + # "taker_fee_decimal": "0.00650000", + # "maker_fee_percent": "0.5000", + # "maker_fee_decimal": "0.00500000", + # "volume_currency": "mxn", + # "current_volume": "0.00", + # "next_volume": "1500000.00", + # "next_maker_fee_percent": "0.490", + # "next_taker_fee_percent": "0.637", + # "nextVolume": "1500000.00", + # "nextFee": "0.490", + # "nextTakerFee": "0.637" + # }, + # ... + # ], + # "deposit_fees": [ + # { + # "currency": "btc", + # "method": "rewards", + # "fee": "0.00", + # "is_fixed": False + # }, + # ... + # ], + # "withdrawal_fees": { + # "ada": "0.20958100", + # "bch": "0.00009437", + # "ars": "0", + # "btc": "0.00001209", + # ... + # } + # } + # + result: dict = {} + depositResponse = self.safe_value(response, 'deposit_fees', []) + withdrawalResponse = self.safe_value(response, 'withdrawal_fees', []) + for i in range(0, len(depositResponse)): + entry = depositResponse[i] + currencyId = self.safe_string(entry, 'currency') + code = self.safe_currency_code(currencyId) + if (codes is None) or (code in codes): + result[code] = { + 'deposit': { + 'fee': self.safe_number(entry, 'fee'), + 'percentage': not self.safe_value(entry, 'is_fixed'), + }, + 'withdraw': { + 'fee': None, + 'percentage': None, + }, + 'networks': {}, + 'info': entry, + } + withdrawalKeys = list(withdrawalResponse.keys()) + for i in range(0, len(withdrawalKeys)): + currencyId = withdrawalKeys[i] + code = self.safe_currency_code(currencyId) + if (codes is None) or (code in codes): + withdrawFee = self.parse_number(withdrawalResponse[currencyId]) + resultValue = self.safe_value(result, code) + if resultValue is None: + result[code] = self.deposit_withdraw_fee({}) + result[code]['withdraw']['fee'] = withdrawFee + result[code]['info'][code] = withdrawFee + return result + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.check_address(address) + self.load_markets() + methods: dict = { + 'BTC': 'Bitcoin', + 'ETH': 'Ether', + 'XRP': 'Ripple', + 'BCH': 'Bcash', + 'LTC': 'Litecoin', + } + currency = self.currency(code) + method = methods[code] if (code in methods) else None + if method is None: + raise ExchangeError(self.id + ' not valid withdraw coin: ' + code) + request: dict = { + 'amount': amount, + 'address': address, + 'destination_tag': tag, + } + classMethod = 'privatePost' + method + 'Withdrawal' + response = getattr(self, classMethod)(self.extend(request, params)) + # + # { + # "success": True, + # "payload": [ + # { + # "wid": "c5b8d7f0768ee91d3b33bee648318688", + # "status": "pending", + # "created_at": "2016-04-08T17:52:31.000+00:00", + # "currency": "btc", + # "method": "Bitcoin", + # "amount": "0.48650929", + # "details": { + # "withdrawal_address": "18MsnATiNiKLqUHDTRKjurwMg7inCrdNEp", + # "tx_hash": "d4f28394693e9fb5fffcaf730c11f32d1922e5837f76ca82189d3bfe30ded433" + # } + # }, + # ] + # } + # + payload = self.safe_value(response, 'payload', []) + first = self.safe_dict(payload, 0) + return self.parse_transaction(first, currency) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # deposit + # { + # "fid": "6112c6369100d6ecceb7f54f17cf0511", + # "status": "complete", + # "created_at": "2022-06-08T12:02:49+0000", + # "currency": "btc", + # "method": "btc", + # "method_name": "Bitcoin", + # "amount": "0.00080000", + # "asset": "btc", + # "network": "btc", + # "protocol": "btc", + # "integration": "bitgo-v2", + # "details": { + # "receiving_address": "3NmvbcYKhogs6RoTb4eYCUJ3beRSqLgSif", + # "tx_hash": "327f3838531f611485ec59f9d0a119fea1595591e274d942b2c10b9b8262eb1d", + # "confirmations": "4" + # } + # } + # + # withdraw + # + # { + # "wid": "c5b8d7f0768ee91d3b33bee648318688", + # "status": "pending", + # "created_at": "2016-04-08T17:52:31.000+00:00", + # "currency": "btc", + # "method": "Bitcoin", + # "amount": "0.48650929", + # "details": { + # "withdrawal_address": "18MsnATiNiKLqUHDTRKjurwMg7inCrdNEp", + # "tx_hash": "d4f28394693e9fb5fffcaf730c11f32d1922e5837f76ca82189d3bfe30ded433" + # } + # } + # + currencyId = self.safe_string_2(transaction, 'currency', 'asset') + currency = self.safe_currency(currencyId, currency) + details = self.safe_value(transaction, 'details', {}) + datetime = self.safe_string(transaction, 'created_at') + withdrawalAddress = self.safe_string(details, 'withdrawal_address') + receivingAddress = self.safe_string(details, 'receiving_address') + networkId = self.safe_string_2(transaction, 'network', 'method') + status = self.safe_string(transaction, 'status') + withdrawId = self.safe_string(transaction, 'wid') + networkCode = self.network_id_to_code(networkId) + networkCodeUpper = networkCode.upper() if (networkCode is not None) else None + return { + 'id': self.safe_string_2(transaction, 'wid', 'fid'), + 'txid': self.safe_string(details, 'tx_hash'), + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + 'network': networkCodeUpper, + 'addressFrom': receivingAddress, + 'address': withdrawalAddress if (withdrawalAddress is not None) else receivingAddress, + 'addressTo': withdrawalAddress, + 'amount': self.safe_number(transaction, 'amount'), + 'type': 'deposit' if (withdrawId is None) else 'withdrawal', + 'currency': self.safe_currency_code(currencyId, currency), + 'status': self.parse_transaction_status(status), + 'updated': None, + 'tagFrom': None, + 'tag': None, + 'tagTo': None, + 'comment': None, + 'internal': None, + 'fee': None, + 'info': transaction, + } + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'pending': 'pending', + 'in_progress': 'pending', + 'complete': 'ok', + 'failed': 'failed', + } + return self.safe_string(statuses, status, status) + + def nonce(self): + return self.milliseconds() + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + endpoint = '/' + self.version + '/' + self.implode_params(path, params) + query = self.omit(params, self.extract_params(path)) + if method == 'GET' or method == 'DELETE': + if query: + endpoint += '?' + self.urlencode(query) + url = self.urls['api']['rest'] + endpoint + if api == 'private': + self.check_required_credentials() + nonce = str(self.nonce()) + endpoint = '/api' + endpoint + request = ''.join([nonce, method, endpoint]) + if method != 'GET' and method != 'DELETE': + if query: + body = self.json(query) + request += body + signature = self.hmac(self.encode(request), self.encode(self.secret), hashlib.sha256) + auth = self.apiKey + ':' + nonce + ':' + signature + headers = { + 'Authorization': 'Bitso ' + auth, + # 'Content-Type': 'application/json', + } + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None # fallback to default error handler + if 'success' in response: + # + # {"success":false,"error":{"code":104,"message":"Cannot perform request - nonce must be higher than 1520307203724237"}} + # + success = self.safe_bool(response, 'success', False) + if isinstance(success, str): + if (success == 'true') or (success == '1'): + success = True + else: + success = False + if not success: + feedback = self.id + ' ' + self.json(response) + error = self.safe_value(response, 'error') + if error is None: + raise ExchangeError(feedback) + code = self.safe_string(error, 'code') + self.throw_exactly_matched_exception(self.exceptions, code, feedback) + raise ExchangeError(feedback) + return None diff --git a/ccxt/bitstamp.py b/ccxt/bitstamp.py new file mode 100644 index 0000000..01f5cd9 --- /dev/null +++ b/ccxt/bitstamp.py @@ -0,0 +1,2337 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.bitstamp import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currencies, Currency, DepositAddress, Int, LedgerEntry, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, TradingFeeInterface, TradingFees, Transaction, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import AccountSuspended +from ccxt.base.errors import BadRequest +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidAddress +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import NotSupported +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.errors import OnMaintenance +from ccxt.base.errors import InvalidNonce +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class bitstamp(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(bitstamp, self).describe(), { + 'id': 'bitstamp', + 'name': 'Bitstamp', + 'countries': ['GB'], + # 8000 requests per 10 minutes = 8000 / 600 = 13.33333333 requests per second => 1000ms / 13.33333333 = 75ms between requests on average + 'rateLimit': 75, + 'version': 'v2', + 'userAgent': self.userAgents['chrome'], + 'pro': True, + 'has': { + 'CORS': True, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createOrder': True, + 'createOrderWithTakeProfitAndStopLoss': False, + 'createOrderWithTakeProfitAndStopLossWs': False, + 'createReduceOnlyOrder': False, + 'createStopLimitOrder': False, + 'createStopMarketOrder': False, + 'createStopOrder': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDepositsWithdrawals': True, + 'fetchDepositWithdrawFee': 'emulated', + 'fetchDepositWithdrawFees': True, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLedger': True, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTrades': True, + 'fetchTradingFee': True, + 'fetchTradingFees': True, + 'fetchTransactionFees': True, + 'fetchTransactions': 'emulated', + 'fetchVolatilityHistory': False, + 'fetchWithdrawals': True, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'transfer': True, + 'withdraw': True, + }, + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/d5480572-1fee-43cb-b900-d38c522d0024', + 'api': { + 'public': 'https://www.bitstamp.net/api', + 'private': 'https://www.bitstamp.net/api', + }, + 'www': 'https://www.bitstamp.net', + 'doc': 'https://www.bitstamp.net/api', + }, + 'timeframes': { + '1m': '60', + '3m': '180', + '5m': '300', + '15m': '900', + '30m': '1800', + '1h': '3600', + '2h': '7200', + '4h': '14400', + '6h': '21600', + '12h': '43200', + '1d': '86400', + '1w': '259200', + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + }, + 'api': { + 'public': { + 'get': { + 'ohlc/{pair}/': 1, + 'order_book/{pair}/': 1, + 'ticker/': 1, + 'ticker_hour/{pair}/': 1, + 'ticker/{pair}/': 1, + 'transactions/{pair}/': 1, + 'trading-pairs-info/': 1, + 'currencies/': 1, + 'eur_usd/': 1, + 'travel_rule/vasps/': 1, + }, + }, + 'private': { + 'get': { + 'travel_rule/contacts/': 1, + 'contacts/{contact_uuid}/': 1, + 'earn/subscriptions/': 1, + 'earn/transactions/': 1, + }, + 'post': { + 'account_balances/': 1, + 'account_balances/{currency}/': 1, + 'balance/': 1, + 'balance/{pair}/': 1, + 'bch_withdrawal/': 1, + 'bch_address/': 1, + 'user_transactions/': 1, + 'user_transactions/{pair}/': 1, + 'crypto-transactions/': 1, + 'open_order': 1, + 'open_orders/all/': 1, + 'open_orders/{pair}/': 1, + 'order_status/': 1, + 'cancel_order/': 1, + 'cancel_all_orders/': 1, + 'cancel_all_orders/{pair}/': 1, + 'buy/{pair}/': 1, + 'buy/market/{pair}/': 1, + 'buy/instant/{pair}/': 1, + 'sell/{pair}/': 1, + 'sell/market/{pair}/': 1, + 'sell/instant/{pair}/': 1, + 'transfer-to-main/': 1, + 'transfer-from-main/': 1, + 'my_trading_pairs/': 1, + 'fees/trading/': 1, + 'fees/trading/{market_symbol}': 1, + 'fees/withdrawal/': 1, + 'fees/withdrawal/{currency}/': 1, + 'withdrawal-requests/': 1, + 'withdrawal/open/': 1, + 'withdrawal/status/': 1, + 'withdrawal/cancel/': 1, + 'liquidation_address/new/': 1, + 'liquidation_address/info/': 1, + 'btc_unconfirmed/': 1, + 'websockets_token/': 1, + # individual coins + 'btc_withdrawal/': 1, + 'btc_address/': 1, + 'ripple_withdrawal/': 1, + 'ripple_address/': 1, + 'ltc_withdrawal/': 1, + 'ltc_address/': 1, + 'eth_withdrawal/': 1, + 'eth_address/': 1, + 'xrp_withdrawal/': 1, + 'xrp_address/': 1, + 'xlm_withdrawal/': 1, + 'xlm_address/': 1, + 'pax_withdrawal/': 1, + 'pax_address/': 1, + 'link_withdrawal/': 1, + 'link_address/': 1, + 'usdc_withdrawal/': 1, + 'usdc_address/': 1, + 'omg_withdrawal/': 1, + 'omg_address/': 1, + 'dai_withdrawal/': 1, + 'dai_address/': 1, + 'knc_withdrawal/': 1, + 'knc_address/': 1, + 'mkr_withdrawal/': 1, + 'mkr_address/': 1, + 'zrx_withdrawal/': 1, + 'zrx_address/': 1, + 'gusd_withdrawal/': 1, + 'gusd_address/': 1, + 'aave_withdrawal/': 1, + 'aave_address/': 1, + 'bat_withdrawal/': 1, + 'bat_address/': 1, + 'uma_withdrawal/': 1, + 'uma_address/': 1, + 'snx_withdrawal/': 1, + 'snx_address/': 1, + 'uni_withdrawal/': 1, + 'uni_address/': 1, + 'yfi_withdrawal/': 1, + 'yfi_address/': 1, + 'audio_withdrawal/': 1, + 'audio_address/': 1, + 'crv_withdrawal/': 1, + 'crv_address/': 1, + 'algo_withdrawal/': 1, + 'algo_address/': 1, + 'comp_withdrawal/': 1, + 'comp_address/': 1, + 'grt_withdrawal/': 1, + 'grt_address/': 1, + 'usdt_withdrawal/': 1, + 'usdt_address/': 1, + 'eurt_withdrawal/': 1, + 'eurt_address/': 1, + 'matic_withdrawal/': 1, + 'matic_address/': 1, + 'sushi_withdrawal/': 1, + 'sushi_address/': 1, + 'chz_withdrawal/': 1, + 'chz_address/': 1, + 'enj_withdrawal/': 1, + 'enj_address/': 1, + 'alpha_withdrawal/': 1, + 'alpha_address/': 1, + 'ftt_withdrawal/': 1, + 'ftt_address/': 1, + 'storj_withdrawal/': 1, + 'storj_address/': 1, + 'axs_withdrawal/': 1, + 'axs_address/': 1, + 'sand_withdrawal/': 1, + 'sand_address/': 1, + 'hbar_withdrawal/': 1, + 'hbar_address/': 1, + 'rgt_withdrawal/': 1, + 'rgt_address/': 1, + 'fet_withdrawal/': 1, + 'fet_address/': 1, + 'skl_withdrawal/': 1, + 'skl_address/': 1, + 'cel_withdrawal/': 1, + 'cel_address/': 1, + 'sxp_withdrawal/': 1, + 'sxp_address/': 1, + 'ada_withdrawal/': 1, + 'ada_address/': 1, + 'slp_withdrawal/': 1, + 'slp_address/': 1, + 'ftm_withdrawal/': 1, + 'ftm_address/': 1, + 'perp_withdrawal/': 1, + 'perp_address/': 1, + 'dydx_withdrawal/': 1, + 'dydx_address/': 1, + 'gala_withdrawal/': 1, + 'gala_address/': 1, + 'shib_withdrawal/': 1, + 'shib_address/': 1, + 'amp_withdrawal/': 1, + 'amp_address/': 1, + 'sgb_withdrawal/': 1, + 'sgb_address/': 1, + 'avax_withdrawal/': 1, + 'avax_address/': 1, + 'wbtc_withdrawal/': 1, + 'wbtc_address/': 1, + 'ctsi_withdrawal/': 1, + 'ctsi_address/': 1, + 'cvx_withdrawal/': 1, + 'cvx_address/': 1, + 'imx_withdrawal/': 1, + 'imx_address/': 1, + 'nexo_withdrawal/': 1, + 'nexo_address/': 1, + 'ust_withdrawal/': 1, + 'ust_address/': 1, + 'ant_withdrawal/': 1, + 'ant_address/': 1, + 'gods_withdrawal/': 1, + 'gods_address/': 1, + 'rad_withdrawal/': 1, + 'rad_address/': 1, + 'band_withdrawal/': 1, + 'band_address/': 1, + 'inj_withdrawal/': 1, + 'inj_address/': 1, + 'rly_withdrawal/': 1, + 'rly_address/': 1, + 'rndr_withdrawal/': 1, + 'rndr_address/': 1, + 'vega_withdrawal/': 1, + 'vega_address/': 1, + '1inch_withdrawal/': 1, + '1inch_address/': 1, + 'ens_withdrawal/': 1, + 'ens_address/': 1, + 'mana_withdrawal/': 1, + 'mana_address/': 1, + 'lrc_withdrawal/': 1, + 'lrc_address/': 1, + 'ape_withdrawal/': 1, + 'ape_address/': 1, + 'mpl_withdrawal/': 1, + 'mpl_address/': 1, + 'euroc_withdrawal/': 1, + 'euroc_address/': 1, + 'sol_withdrawal/': 1, + 'sol_address/': 1, + 'dot_withdrawal/': 1, + 'dot_address/': 1, + 'near_withdrawal/': 1, + 'near_address/': 1, + 'doge_withdrawal/': 1, + 'doge_address/': 1, + 'flr_withdrawal/': 1, + 'flr_address/': 1, + 'dgld_withdrawal/': 1, + 'dgld_address/': 1, + 'ldo_withdrawal/': 1, + 'ldo_address/': 1, + 'travel_rule/contacts/': 1, + 'earn/subscribe/': 1, + 'earn/subscriptions/setting/': 1, + 'earn/unsubscribe': 1, + 'wecan_withdrawal/': 1, + 'wecan_address/': 1, + 'trac_withdrawal/': 1, + 'trac_address/': 1, + 'eurcv_withdrawal/': 1, + 'eurcv_address/': 1, + 'pyusd_withdrawal/': 1, + 'pyusd_address/': 1, + 'lmwr_withdrawal/': 1, + 'lmwr_address/': 1, + 'pepe_withdrawal/': 1, + 'pepe_address/': 1, + 'blur_withdrawal/': 1, + 'blur_address/': 1, + 'vext_withdrawal/': 1, + 'vext_address/': 1, + 'cspr_withdrawal/': 1, + 'cspr_address/': 1, + 'vchf_withdrawal/': 1, + 'vchf_address/': 1, + 'veur_withdrawal/': 1, + 'veur_address/': 1, + 'truf_withdrawal/': 1, + 'truf_address/': 1, + 'wif_withdrawal/': 1, + 'wif_address/': 1, + 'smt_withdrawal/': 1, + 'smt_address/': 1, + 'sui_withdrawal/': 1, + 'sui_address/': 1, + 'jup_withdrawal/': 1, + 'jup_address/': 1, + 'ondo_withdrawal/': 1, + 'ondo_address/': 1, + 'boba_withdrawal/': 1, + 'boba_address/': 1, + 'pyth_withdrawal/': 1, + 'pyth_address/': 1, + }, + }, + }, + 'fees': { + 'trading': { + 'tierBased': True, + 'percentage': True, + 'taker': self.parse_number('0.004'), + 'maker': self.parse_number('0.004'), + 'tiers': { + 'taker': [ + [self.parse_number('0'), self.parse_number('0.004')], + [self.parse_number('10000'), self.parse_number('0.003')], + [self.parse_number('100000'), self.parse_number('0.002')], + [self.parse_number('500000'), self.parse_number('0.0018')], + [self.parse_number('1500000'), self.parse_number('0.0016')], + [self.parse_number('5000000'), self.parse_number('0.0012')], + [self.parse_number('20000000'), self.parse_number('0.001')], + [self.parse_number('50000000'), self.parse_number('0.0008')], + [self.parse_number('100000000'), self.parse_number('0.0006')], + [self.parse_number('250000000'), self.parse_number('0.0005')], + [self.parse_number('1000000000'), self.parse_number('0.0003')], + ], + 'maker': [ + [self.parse_number('0'), self.parse_number('0.003')], + [self.parse_number('10000'), self.parse_number('0.002')], + [self.parse_number('100000'), self.parse_number('0.001')], + [self.parse_number('500000'), self.parse_number('0.0008')], + [self.parse_number('1500000'), self.parse_number('0.0006')], + [self.parse_number('5000000'), self.parse_number('0.0003')], + [self.parse_number('20000000'), self.parse_number('0.002')], + [self.parse_number('50000000'), self.parse_number('0.0001')], + [self.parse_number('100000000'), self.parse_number('0')], + [self.parse_number('250000000'), self.parse_number('0')], + [self.parse_number('1000000000'), self.parse_number('0')], + ], + }, + }, + 'funding': { + 'tierBased': False, + 'percentage': False, + 'withdraw': {}, + 'deposit': { + 'BTC': 0, + 'BCH': 0, + 'LTC': 0, + 'ETH': 0, + 'XRP': 0, + 'XLM': 0, + 'PAX': 0, + 'USD': 7.5, + 'EUR': 0, + }, + }, + }, + 'precisionMode': TICK_SIZE, + 'commonCurrencies': { + 'UST': 'USTC', + }, + # exchange-specific options + 'options': { + 'networksById': { + 'bitcoin-cash': 'BCH', + 'bitcoin': 'BTC', + 'ethereum': 'ERC20', + 'litecoin': 'LTC', + 'stellar': 'XLM', + 'xrpl': 'XRP', + 'tron': 'TRC20', + 'algorand': 'ALGO', + 'flare': 'FLR', + 'hedera': 'HBAR', + 'cardana': 'ADA', + 'songbird': 'FLR', + 'avalanche-c-chain': 'AVAX', + 'solana': 'SOL', + 'polkadot': 'DOT', + 'near': 'NEAR', + 'doge': 'DOGE', + 'sui': 'SUI', + 'casper': 'CSRP', + }, + }, + 'exceptions': { + 'exact': { + 'No permission found': PermissionDenied, + 'API key not found': AuthenticationError, + 'IP address not allowed': PermissionDenied, + 'Invalid nonce': InvalidNonce, + 'Invalid signature': AuthenticationError, + 'Authentication failed': AuthenticationError, + 'Missing key, signature and nonce parameters': AuthenticationError, + 'Wrong API key format': AuthenticationError, + 'Your account is frozen': PermissionDenied, + 'Please update your profile with your FATCA information, before using API.': PermissionDenied, + 'Order not found.': OrderNotFound, + 'Price is more than 20% below market price.': InvalidOrder, + "Bitstamp.net is under scheduled maintenance. We'll be back soon.": OnMaintenance, # {"error": "Bitstamp.net is under scheduled maintenance. We'll be back soon."} + 'Order could not be placed.': ExchangeNotAvailable, # Order could not be placed(perhaps due to internal error or trade halt). Please retry placing order. + 'Invalid offset.': BadRequest, + 'Trading is currently unavailable for your account.': AccountSuspended, # {"status": "error", "reason": {"__all__": ["Trading is currently unavailable for your account."]}, "response_code": "403.004"} + }, + 'broad': { + 'Minimum order size is': InvalidOrder, # Minimum order size is 5.0 EUR. + 'Check your account balance for details.': InsufficientFunds, # You have only 0.00100000 BTC available. Check your account balance for details. + 'Ensure self value has at least': InvalidAddress, # Ensure self value has at least 25 characters(it has 4). + 'Ensure that there are no more than': InvalidOrder, # {"status": "error", "reason": {"amount": ["Ensure that there are no more than 0 decimal places."], "__all__": [""]}} + }, + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': False, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': True, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': False, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': None, + 'untilDays': 30, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': None, + 'fetchOHLCV': { + 'limit': 1000, + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + }) + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for bitstamp + + https://www.bitstamp.net/api/#tag/Market-info/operation/GetTradingPairsInfo + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = self.fetch_markets_from_cache(params) + # + # [ + # { + # "trading": "Enabled", + # "base_decimals": 8, + # "url_symbol": "btcusd", + # "name": "BTC/USD", + # "instant_and_market_orders": "Enabled", + # "minimum_order": "20.0 USD", + # "counter_decimals": 2, + # "description": "Bitcoin / U.S. dollar" + # } + # ] + # + result = [] + for i in range(0, len(response)): + market = response[i] + name = self.safe_string(market, 'name') + base, quote = name.split('/') + baseId = base.lower() + quoteId = quote.lower() + base = self.safe_currency_code(base) + quote = self.safe_currency_code(quote) + minimumOrder = self.safe_string(market, 'minimum_order') + parts = minimumOrder.split(' ') + status = self.safe_string(market, 'trading') + result.append({ + 'id': self.safe_string(market, 'url_symbol'), + 'marketId': baseId + '_' + quoteId, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'future': False, + 'swap': False, + 'option': False, + 'active': (status == 'Enabled'), + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(self.parse_precision(self.safe_string(market, 'base_decimals'))), + 'price': self.parse_number(self.parse_precision(self.safe_string(market, 'counter_decimals'))), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': None, + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': self.safe_number(parts, 0), + 'max': None, + }, + }, + 'created': None, + 'info': market, + }) + return result + + def construct_currency_object(self, id, code, name, precision, minCost, originalPayload): + currencyType = 'crypto' + description = self.describe() + if self.is_fiat(code): + currencyType = 'fiat' + tickSize = self.parse_number(self.parse_precision(self.number_to_string(precision))) + return { + 'id': id, + 'code': code, + 'info': originalPayload, # the original payload + 'type': currencyType, + 'name': name, + 'active': True, + 'deposit': None, + 'withdraw': None, + 'fee': self.safe_number(description['fees']['funding']['withdraw'], code), + 'precision': tickSize, + 'limits': { + 'amount': { + 'min': tickSize, + 'max': None, + }, + 'price': { + 'min': tickSize, + 'max': None, + }, + 'cost': { + 'min': minCost, + 'max': None, + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + }, + 'networks': {}, + } + + def fetch_markets_from_cache(self, params={}): + # self method is now redundant + # currencies are now fetched before markets + options = self.safe_value(self.options, 'fetchMarkets', {}) + timestamp = self.safe_integer(options, 'timestamp') + expires = self.safe_integer(options, 'expires', 1000) + now = self.milliseconds() + if (timestamp is None) or ((now - timestamp) > expires): + response = self.publicGetTradingPairsInfo(params) + self.options['fetchMarkets'] = self.extend(options, { + 'response': response, + 'timestamp': now, + }) + return self.safe_value(self.options['fetchMarkets'], 'response') + + def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://www.bitstamp.net/api/#tag/Market-info/operation/GetTradingPairsInfo + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = self.fetch_markets_from_cache(params) + # + # [ + # { + # "trading": "Enabled", + # "base_decimals": 8, + # "url_symbol": "btcusd", + # "name": "BTC/USD", + # "instant_and_market_orders": "Enabled", + # "minimum_order": "20.0 USD", + # "counter_decimals": 2, + # "description": "Bitcoin / U.S. dollar" + # }, + # ] + # + result: dict = {} + for i in range(0, len(response)): + market = response[i] + name = self.safe_string(market, 'name') + base, quote = name.split('/') + baseId = base.lower() + quoteId = quote.lower() + base = self.safe_currency_code(base) + quote = self.safe_currency_code(quote) + description = self.safe_string(market, 'description') + baseDescription, quoteDescription = description.split(' / ') + minimumOrder = self.safe_string(market, 'minimum_order') + parts = minimumOrder.split(' ') + cost = parts[0] + if not (base in result): + baseDecimals = self.safe_integer(market, 'base_decimals') + result[base] = self.construct_currency_object(baseId, base, baseDescription, baseDecimals, None, market) + if not (quote in result): + counterDecimals = self.safe_integer(market, 'counter_decimals') + result[quote] = self.construct_currency_object(quoteId, quote, quoteDescription, counterDecimals, self.parse_number(cost), market) + return result + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://www.bitstamp.net/api/#tag/Order-book/operation/GetOrderBook + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + response = self.publicGetOrderBookPair(self.extend(request, params)) + # + # { + # "timestamp": "1583652948", + # "microtimestamp": "1583652948955826", + # "bids": [ + # ["8750.00", "1.33685271"], + # ["8749.39", "0.07700000"], + # ["8746.98", "0.07400000"], + # ] + # "asks": [ + # ["8754.10", "1.51995636"], + # ["8754.71", "1.40000000"], + # ["8754.72", "2.50000000"], + # ] + # } + # + microtimestamp = self.safe_integer(response, 'microtimestamp') + timestamp = self.parse_to_int(microtimestamp / 1000) + orderbook = self.parse_order_book(response, market['symbol'], timestamp) + orderbook['nonce'] = microtimestamp + return orderbook + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "timestamp": "1686068944", + # "high": "26252", + # "last": "26216", + # "bid": "26208", + # "vwap": "25681", + # "volume": "3563.13819902", + # "low": "25350", + # "ask": "26211", + # "open": "25730", + # "open_24": "25895", + # "percent_change_24": "1.24", + # "pair": "BTC/USD" + # } + # + marketId = self.safe_string(ticker, 'pair') + symbol = self.safe_symbol(marketId, market, None) + timestamp = self.safe_timestamp(ticker, 'timestamp') + vwap = self.safe_string(ticker, 'vwap') + baseVolume = self.safe_string(ticker, 'volume') + quoteVolume = Precise.string_mul(baseVolume, vwap) + last = self.safe_string(ticker, 'last') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': self.safe_string(ticker, 'bid'), + 'bidVolume': None, + 'ask': self.safe_string(ticker, 'ask'), + 'askVolume': None, + 'vwap': vwap, + 'open': self.safe_string(ticker, 'open'), + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'info': ticker, + }, market) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://www.bitstamp.net/api/#tag/Tickers/operation/GetMarketTicker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + ticker = self.publicGetTickerPair(self.extend(request, params)) + # + # { + # "timestamp": "1686068944", + # "high": "26252", + # "last": "26216", + # "bid": "26208", + # "vwap": "25681", + # "volume": "3563.13819902", + # "low": "25350", + # "ask": "26211", + # "open": "25730", + # "open_24": "25895", + # "percent_change_24": "1.24" + # } + # + return self.parse_ticker(ticker, market) + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://www.bitstamp.net/api/#tag/Tickers/operation/GetCurrencyPairTickers + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + response = self.publicGetTicker(params) + # + # { + # "timestamp": "1686068944", + # "high": "26252", + # "last": "26216", + # "bid": "26208", + # "vwap": "25681", + # "volume": "3563.13819902", + # "low": "25350", + # "ask": "26211", + # "open": "25730", + # "open_24": "25895", + # "percent_change_24": "1.24", + # "pair": "BTC/USD" + # } + # + return self.parse_tickers(response, symbols) + + def get_currency_id_from_transaction(self, transaction): + # + # { + # "fee": "0.00000000", + # "btc_usd": "0.00", + # "datetime": XXX, + # "usd": 0.0, + # "btc": 0.0, + # "eth": "0.05000000", + # "type": "0", + # "id": XXX, + # "eur": 0.0 + # } + # + currencyId = self.safe_string_lower(transaction, 'currency') + if currencyId is not None: + return currencyId + transaction = self.omit(transaction, [ + 'fee', + 'price', + 'datetime', + 'type', + 'status', + 'id', + ]) + ids = list(transaction.keys()) + for i in range(0, len(ids)): + id = ids[i] + if id.find('_') < 0: + value = self.safe_integer(transaction, id) + if (value is not None) and (value != 0): + return id + return None + + def get_market_from_trade(self, trade): + trade = self.omit(trade, [ + 'fee', + 'price', + 'datetime', + 'tid', + 'type', + 'order_id', + 'side', + ]) + currencyIds = list(trade.keys()) + numCurrencyIds = len(currencyIds) + if numCurrencyIds > 2: + raise ExchangeError(self.id + ' getMarketFromTrade() too many keys: ' + self.json(currencyIds) + ' in the trade: ' + self.json(trade)) + if numCurrencyIds == 2: + marketId = currencyIds[0] + currencyIds[1] + if marketId in self.markets_by_id: + return self.safe_market(marketId) + marketId = currencyIds[1] + currencyIds[0] + if marketId in self.markets_by_id: + return self.safe_market(marketId) + return None + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(public) + # + # { + # "date": "1637845199", + # "tid": "209895701", + # "amount": "0.00500000", + # "type": "0", # Transaction type: 0 - buy; 1 - sell + # "price": "4451.25" + # } + # + # fetchMyTrades, trades returned within fetchOrder(private) + # + # { + # "fee": "0.11128", + # "eth_usdt": 4451.25, + # "datetime": "2021-11-25 12:59:59.322000", + # "usdt": "-22.26", + # "order_id": 1429545880227846, + # "usd": 0, + # "btc": 0, + # "eth": "0.00500000", + # "type": "2", # Transaction type: 0 - deposit; 1 - withdrawal; 2 - market trade; 14 - sub account transfer; 25 - credited with staked assets; 26 - sent assets to staking; 27 - staking reward; 32 - referral reward; 35 - inter account transfer. + # "id": 209895701, + # "eur": 0 + # } + # + # from fetchOrder(private) + # + # { + # "fee": "0.11128", + # "price": "4451.25000000", + # "datetime": "2021-11-25 12:59:59.322000", + # "usdt": "22.25625000", + # "tid": 209895701, + # "eth": "0.00500000", + # "type": 2 # Transaction type: 0 - deposit; 1 - withdrawal; 2 - market trade + # } + # + id = self.safe_string_2(trade, 'id', 'tid') + symbol = None + side = None + priceString = self.safe_string(trade, 'price') + amountString = self.safe_string(trade, 'amount') + orderId = self.safe_string(trade, 'order_id') + type = None + costString = self.safe_string(trade, 'cost') + rawMarketId = None + if market is None: + keys = list(trade.keys()) + for i in range(0, len(keys)): + currentKey = keys[i] + if currentKey != 'order_id' and currentKey.find('_') >= 0: + rawMarketId = currentKey + market = self.safe_market(rawMarketId, market, '_') + # if the market is still not defined + # try to deduce it from used keys + if market is None: + market = self.get_market_from_trade(trade) + feeCostString = self.safe_string(trade, 'fee') + feeCurrency = market['quote'] + priceId = rawMarketId if (rawMarketId is not None) else market['marketId'] + priceString = self.safe_string(trade, priceId, priceString) + amountString = self.safe_string(trade, market['baseId'], amountString) + costString = self.safe_string(trade, market['quoteId'], costString) + symbol = market['symbol'] + datetimeString = self.safe_string_2(trade, 'date', 'datetime') + timestamp = None + if datetimeString is not None: + if datetimeString.find(' ') >= 0: + # iso8601 + timestamp = self.parse8601(datetimeString) + else: + # string unix epoch in seconds + timestamp = int(datetimeString) + timestamp = timestamp * 1000 + # if it is a private trade + if 'id' in trade: + if amountString is not None: + isAmountNeg = Precise.string_lt(amountString, '0') + if isAmountNeg: + side = 'sell' + amountString = Precise.string_neg(amountString) + else: + side = 'buy' + else: + side = self.safe_string(trade, 'type') + if side == '1': + side = 'sell' + elif side == '0': + side = 'buy' + else: + side = None + if costString is not None: + costString = Precise.string_abs(costString) + fee = None + if feeCostString is not None: + fee = { + 'cost': feeCostString, + 'currency': feeCurrency, + } + return self.safe_trade({ + 'id': id, + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'order': orderId, + 'type': type, + 'side': side, + 'takerOrMaker': None, + 'price': priceString, + 'amount': amountString, + 'cost': costString, + 'fee': fee, + }, market) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://www.bitstamp.net/api/#tag/Transactions-public/operation/GetTransactions + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + 'time': 'hour', + } + response = self.publicGetTransactionsPair(self.extend(request, params)) + # + # [ + # { + # "date": "1551814435", + # "tid": "83581898", + # "price": "0.03532850", + # "type": "1", + # "amount": "0.85945907" + # }, + # { + # "date": "1551814434", + # "tid": "83581896", + # "price": "0.03532851", + # "type": "1", + # "amount": "11.34130961" + # }, + # ] + # + return self.parse_trades(response, market, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # { + # "high": "9064.77", + # "timestamp": "1593961440", + # "volume": "18.49436608", + # "low": "9040.87", + # "close": "9064.77", + # "open": "9040.87" + # } + # + return [ + self.safe_timestamp(ohlcv, 'timestamp'), + self.safe_number(ohlcv, 'open'), + self.safe_number(ohlcv, 'high'), + self.safe_number(ohlcv, 'low'), + self.safe_number(ohlcv, 'close'), + self.safe_number(ohlcv, 'volume'), + ] + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://www.bitstamp.net/api/#tag/Market-info/operation/GetOHLCData + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + 'step': self.safe_string(self.timeframes, timeframe, timeframe), + } + duration = self.parse_timeframe(timeframe) + if limit is None: + if since is None: + request['limit'] = 1000 # we need to specify an allowed amount of `limit` if no `since` is set and there is no default limit by exchange + else: + limit = 1000 + start = self.parse_to_int(since / 1000) + request['start'] = start + request['end'] = self.sum(start, duration * (limit - 1)) + request['limit'] = limit + else: + if since is not None: + start = self.parse_to_int(since / 1000) + request['start'] = start + request['end'] = self.sum(start, duration * (limit - 1)) + request['limit'] = min(limit, 1000) # min 1, max 1000 + response = self.publicGetOhlcPair(self.extend(request, params)) + # + # { + # "data": { + # "pair": "BTC/USD", + # "ohlc": [ + # {"high": "9064.77", "timestamp": "1593961440", "volume": "18.49436608", "low": "9040.87", "close": "9064.77", "open": "9040.87"}, + # {"high": "9071.59", "timestamp": "1593961500", "volume": "3.48631711", "low": "9058.76", "close": "9061.07", "open": "9064.66"}, + # {"high": "9067.33", "timestamp": "1593961560", "volume": "0.04142833", "low": "9061.94", "close": "9061.94", "open": "9067.33"}, + # ], + # } + # } + # + data = self.safe_value(response, 'data', {}) + ohlc = self.safe_list(data, 'ohlc', []) + return self.parse_ohlcvs(ohlc, market, timeframe, since, limit) + + def parse_balance(self, response) -> Balances: + result: dict = { + 'info': response, + 'timestamp': None, + 'datetime': None, + } + if response is None: + response = [] + for i in range(0, len(response)): + currencyBalance = response[i] + currencyId = self.safe_string(currencyBalance, 'currency') + currencyCode = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(currencyBalance, 'available') + account['used'] = self.safe_string(currencyBalance, 'reserved') + account['total'] = self.safe_string(currencyBalance, 'total') + result[currencyCode] = account + return self.safe_balance(result) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://www.bitstamp.net/api/#tag/Account-balances/operation/GetAccountBalances + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + response = self.privatePostAccountBalances(params) + # + # [ + # { + # "currency": "usdt", + # "total": "7.00000", + # "available": "7.00000", + # "reserved": "0.00000" + # }, + # ... + # ] + # + return self.parse_balance(response) + + def fetch_trading_fee(self, symbol: str, params={}) -> TradingFeeInterface: + """ + fetch the trading fees for a market + + https://www.bitstamp.net/api/#tag/Fees/operation/GetTradingFeesForCurrency + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `fee structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'market_symbol': market['id'], + } + response = self.privatePostFeesTrading(self.extend(request, params)) + # + # [ + # { + # "currency_pair": "btcusd", + # "fees": + # { + # "maker": "0.15000", + # "taker": "0.16000" + # }, + # "market": "btcusd" + # } + # ... + # ] + # + tradingFeesByMarketId = self.index_by(response, 'currency_pair') + tradingFee = self.safe_dict(tradingFeesByMarketId, market['id']) + return self.parse_trading_fee(tradingFee, market) + + def parse_trading_fee(self, fee: dict, market: Market = None) -> TradingFeeInterface: + marketId = self.safe_string(fee, 'market') + fees = self.safe_dict(fee, 'fees', {}) + return { + 'info': fee, + 'symbol': self.safe_symbol(marketId, market), + 'maker': self.safe_number(fees, 'maker'), + 'taker': self.safe_number(fees, 'taker'), + 'percentage': None, + 'tierBased': None, + } + + def parse_trading_fees(self, fees): + result: dict = {'info': fees} + for i in range(0, len(fees)): + fee = self.parse_trading_fee(fees[i]) + symbol = fee['symbol'] + result[symbol] = fee + return result + + def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://www.bitstamp.net/api/#tag/Fees/operation/GetAllTradingFees + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + self.load_markets() + response = self.privatePostFeesTrading(params) + # + # [ + # { + # "currency_pair": "btcusd", + # "fees": + # { + # "maker": "0.15000", + # "taker": "0.16000" + # }, + # "market": "btcusd" + # } + # ... + # ] + # + return self.parse_trading_fees(response) + + def fetch_transaction_fees(self, codes: Strings = None, params={}): + """ + @deprecated + please use fetchDepositWithdrawFees instead + + https://www.bitstamp.net/api/#tag/Fees + + :param str[]|None codes: list of unified currency codes + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `fee structures ` + """ + self.load_markets() + response = self.privatePostFeesWithdrawal(params) + # + # [ + # { + # "currency": "btc", + # "fee": "0.00015000", + # "network": "bitcoin" + # } + # ... + # ] + # + return self.parse_transaction_fees(response) + + def parse_transaction_fees(self, response, codes=None): + result: dict = {} + currencies = self.index_by(response, 'currency') + ids = list(currencies.keys()) + for i in range(0, len(ids)): + id = ids[i] + fees = self.safe_value(response, i, {}) + code = self.safe_currency_code(id) + if (codes is not None) and not self.in_array(code, codes): + continue + result[code] = { + 'withdraw_fee': self.safe_number(fees, 'fee'), + 'deposit': {}, + 'info': self.safe_dict(currencies, id), + } + return result + + def fetch_deposit_withdraw_fees(self, codes=None, params={}): + """ + fetch deposit and withdraw fees + + https://www.bitstamp.net/api/#tag/Fees/operation/GetAllWithdrawalFees + + :param str[]|None codes: list of unified currency codes + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `fee structures ` + """ + self.load_markets() + response = self.privatePostFeesWithdrawal(params) + # + # [ + # { + # "currency": "btc", + # "fee": "0.00015000", + # "network": "bitcoin" + # } + # ... + # ] + # + responseByCurrencyId = self.group_by(response, 'currency') + return self.parse_deposit_withdraw_fees(responseByCurrencyId, codes) + + def parse_deposit_withdraw_fee(self, fee, currency=None): + result = self.deposit_withdraw_fee(fee) + for j in range(0, len(fee)): + networkEntry = fee[j] + networkId = self.safe_string(networkEntry, 'network') + networkCode = self.network_id_to_code(networkId) + withdrawFee = self.safe_number(networkEntry, 'fee') + result['withdraw'] = { + 'fee': withdrawFee, + 'percentage': None, + } + result['networks'][networkCode] = { + 'withdraw': { + 'fee': withdrawFee, + 'percentage': None, + }, + 'deposit': { + 'fee': None, + 'percentage': None, + }, + } + return result + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://www.bitstamp.net/api/#tag/Orders/operation/OpenInstantBuyOrder + https://www.bitstamp.net/api/#tag/Orders/operation/OpenMarketBuyOrder + https://www.bitstamp.net/api/#tag/Orders/operation/OpenLimitBuyOrder + https://www.bitstamp.net/api/#tag/Orders/operation/OpenInstantSellOrder + https://www.bitstamp.net/api/#tag/Orders/operation/OpenMarketSellOrder + https://www.bitstamp.net/api/#tag/Orders/operation/OpenLimitSellOrder + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + 'amount': self.amount_to_precision(symbol, amount), + } + clientOrderId = self.safe_string_2(params, 'client_order_id', 'clientOrderId') + if clientOrderId is not None: + request['client_order_id'] = clientOrderId + params = self.omit(params, ['clientOrderId']) + response = None + capitalizedSide = self.capitalize(side) + if type == 'market': + if capitalizedSide == 'Buy': + response = self.privatePostBuyMarketPair(self.extend(request, params)) + else: + response = self.privatePostSellMarketPair(self.extend(request, params)) + elif type == 'instant': + if capitalizedSide == 'Buy': + response = self.privatePostBuyInstantPair(self.extend(request, params)) + else: + response = self.privatePostSellInstantPair(self.extend(request, params)) + else: + request['price'] = self.price_to_precision(symbol, price) + if capitalizedSide == 'Buy': + response = self.privatePostBuyPair(self.extend(request, params)) + else: + response = self.privatePostSellPair(self.extend(request, params)) + order = self.parse_order(response, market) + order['type'] = type + return order + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://www.bitstamp.net/api/#tag/Orders/operation/CancelOrder + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + request: dict = { + 'id': id, + } + response = self.privatePostCancelOrder(self.extend(request, params)) + # + # { + # "id": 1453282316578816, + # "amount": "0.02035278", + # "price": "2100.45", + # "type": 0, + # "market": "BTC/USD" + # } + # + return self.parse_order(response) + + def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders + + https://www.bitstamp.net/api/#tag/Orders/operation/CancelAllOrders + https://www.bitstamp.net/api/#tag/Orders/operation/CancelOrdersForMarket + + :param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + self.load_markets() + market = None + request: dict = {} + response = None + if symbol is not None: + market = self.market(symbol) + request['pair'] = market['id'] + response = self.privatePostCancelAllOrdersPair(self.extend(request, params)) + else: + response = self.privatePostCancelAllOrders(self.extend(request, params)) + # + # { + # "canceled": [ + # { + # "id": 1453282316578816, + # "amount": "0.02035278", + # "price": "2100.45", + # "type": 0, + # "currency_pair": "BTC/USD", + # "market": "BTC/USD" + # } + # ], + # "success": True + # } + # + canceled = self.safe_list(response, 'canceled') + return self.parse_orders(canceled) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'In Queue': 'open', + 'Open': 'open', + 'Finished': 'closed', + 'Canceled': 'canceled', + } + return self.safe_string(statuses, status, status) + + def fetch_order_status(self, id: str, symbol: Str = None, params={}): + self.load_markets() + clientOrderId = self.safe_value_2(params, 'client_order_id', 'clientOrderId') + request: dict = {} + if clientOrderId is not None: + request['client_order_id'] = clientOrderId + params = self.omit(params, ['client_order_id', 'clientOrderId']) + else: + request['id'] = id + response = self.privatePostOrderStatus(self.extend(request, params)) + return self.parse_order_status(self.safe_string(response, 'status')) + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://www.bitstamp.net/api/#tag/Orders/operation/GetOrderStatus + + :param str id: the order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + clientOrderId = self.safe_value_2(params, 'client_order_id', 'clientOrderId') + request: dict = {} + if clientOrderId is not None: + request['client_order_id'] = clientOrderId + params = self.omit(params, ['client_order_id', 'clientOrderId']) + else: + request['id'] = id + response = self.privatePostOrderStatus(self.extend(request, params)) + # + # { + # "status": "Finished", + # "id": 1429545880227846, + # "amount_remaining": "0.00000000", + # "transactions": [ + # { + # "fee": "0.11128", + # "price": "4451.25000000", + # "datetime": "2021-11-25 12:59:59.322000", + # "usdt": "22.25625000", + # "tid": 209895701, + # "eth": "0.00500000", + # "type": 2 + # } + # ] + # } + # + return self.parse_order(response, market) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://www.bitstamp.net/api/#tag/Transactions-private/operation/GetUserTransactions + https://www.bitstamp.net/api/#tag/Transactions-private/operation/GetUserTransactionsForMarket + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + request: dict = {} + method = 'privatePostUserTransactions' + market = None + if symbol is not None: + market = self.market(symbol) + request['pair'] = market['id'] + method += 'Pair' + if limit is not None: + request['limit'] = limit + response = getattr(self, method)(self.extend(request, params)) + result = self.filter_by(response, 'type', '2') + return self.parse_trades(result, market, since, limit) + + def fetch_deposits_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch history of deposits and withdrawals + + https://www.bitstamp.net/api/#tag/Transactions-private/operation/GetUserTransactions + + :param str [code]: unified currency code for the currency of the deposit/withdrawals, default is None + :param int [since]: timestamp in ms of the earliest deposit/withdrawal, default is None + :param int [limit]: max number of deposit/withdrawals to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `transaction structure ` + """ + self.load_markets() + request: dict = {} + if limit is not None: + request['limit'] = limit + response = self.privatePostUserTransactions(self.extend(request, params)) + # + # [ + # { + # "fee": "0.00000000", + # "btc_usd": "0.00", + # "id": 1234567894, + # "usd": 0, + # "btc": 0, + # "datetime": "2018-09-08 09:00:31", + # "type": "1", + # "xrp": "-20.00000000", + # "eur": 0, + # }, + # { + # "fee": "0.00000000", + # "btc_usd": "0.00", + # "id": 1134567891, + # "usd": 0, + # "btc": 0, + # "datetime": "2018-09-07 18:47:52", + # "type": "0", + # "xrp": "20.00000000", + # "eur": 0, + # }, + # ] + # + currency = None + if code is not None: + currency = self.currency(code) + transactions = self.filter_by_array(response, 'type', ['0', '1'], False) + return self.parse_transactions(transactions, currency, since, limit) + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://www.bitstamp.net/api/#tag/Withdrawals/operation/GetWithdrawalRequests + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + request: dict = {} + if since is not None: + request['timedelta'] = self.milliseconds() - since + else: + request['timedelta'] = 50000000 # use max bitstamp approved value + response = self.privatePostWithdrawalRequests(self.extend(request, params)) + # + # [ + # { + # "status": 2, + # "datetime": "2018-10-17 10:58:13", + # "currency": "BTC", + # "amount": "0.29669259", + # "address": "aaaaa", + # "type": 1, + # "id": 111111, + # "transaction_id": "xxxx", + # }, + # { + # "status": 2, + # "datetime": "2018-10-17 10:55:17", + # "currency": "ETH", + # "amount": "1.11010664", + # "address": "aaaa", + # "type": 16, + # "id": 222222, + # "transaction_id": "xxxxx", + # }, + # ] + # + return self.parse_transactions(response, None, since, limit) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fetchDepositsWithdrawals + # + # { + # "fee": "0.00000000", + # "btc_usd": "0.00", + # "id": 1234567894, + # "usd": 0, + # "btc": 0, + # "datetime": "2018-09-08 09:00:31", + # "type": "1", + # "xrp": "-20.00000000", + # "eur": 0, + # } + # + # fetchWithdrawals + # + # { + # "status": 2, + # "datetime": "2018-10-17 10:58:13", + # "currency": "BTC", + # "amount": "0.29669259", + # "address": "aaaaa", + # "type": 1, + # "id": 111111, + # "transaction_id": "xxxx", + # } + # + # { + # "id": 3386432, + # "type": 14, + # "amount": "863.21332500", + # "status": 2, + # "address": "rE1sdh25BJQ3qFwngiTBwaq3zPGGYcrjp1?dt=1455", + # "currency": "XRP", + # "datetime": "2018-01-05 15:27:55", + # "transaction_id": "001743B03B0C79BA166A064AC0142917B050347B4CB23BA2AB4B91B3C5608F4C" + # } + # + timestamp = self.parse8601(self.safe_string(transaction, 'datetime')) + currencyId = self.get_currency_id_from_transaction(transaction) + code = self.safe_currency_code(currencyId, currency) + feeCost = self.safe_string(transaction, 'fee') + feeCurrency = None + amount = None + if 'amount' in transaction: + amount = self.safe_string(transaction, 'amount') + elif currency is not None: + amount = self.safe_string(transaction, currency['id'], amount) + feeCurrency = currency['code'] + elif (code is not None) and (currencyId is not None): + amount = self.safe_string(transaction, currencyId, amount) + feeCurrency = code + if amount is not None: + # withdrawals have a negative amount + amount = Precise.string_abs(amount) + status = 'ok' + if 'status' in transaction: + status = self.parse_transaction_status(self.safe_string(transaction, 'status')) + type = None + if 'type' in transaction: + # from fetchDepositsWithdrawals + rawType = self.safe_string(transaction, 'type') + if rawType == '0': + type = 'deposit' + elif rawType == '1': + type = 'withdrawal' + else: + # from fetchWithdrawals + type = 'withdrawal' + tag = None + address = self.safe_string(transaction, 'address') + if address is not None: + # dt(destination tag) is embedded into the address field + addressParts = address.split('?dt=') + numParts = len(addressParts) + if numParts > 1: + address = addressParts[0] + tag = addressParts[1] + fee = { + 'currency': None, + 'cost': None, + 'rate': None, + } + if feeCost is not None: + fee = { + 'currency': feeCurrency, + 'cost': feeCost, + 'rate': None, + } + return { + 'info': transaction, + 'id': self.safe_string(transaction, 'id'), + 'txid': self.safe_string(transaction, 'transaction_id'), + 'type': type, + 'currency': code, + 'network': None, + 'amount': self.parse_number(amount), + 'status': status, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'address': address, + 'addressFrom': None, + 'addressTo': address, + 'tag': tag, + 'tagFrom': None, + 'tagTo': tag, + 'updated': None, + 'comment': None, + 'internal': None, + 'fee': fee, + } + + def parse_transaction_status(self, status: Str): + # + # withdrawals: + # 0(open), 1(in process), 2(finished), 3(canceled) or 4(failed). + # + statuses: dict = { + '0': 'pending', # Open + '1': 'pending', # In process + '2': 'ok', # Finished + '3': 'canceled', # Canceled + '4': 'failed', # Failed + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # from fetch order: + # {status: "Finished", + # "id": 731693945, + # "client_order_id": '', + # "transactions": + # [{fee: "0.000019", + # "price": "0.00015803", + # "datetime": "2018-01-07 10:45:34.132551", + # "btc": "0.0079015000000000", + # "tid": 42777395, + # "type": 2, + # "xrp": "50.00000000"}]} + # + # partially filled order: + # {"id": 468646390, + # "client_order_id": "", + # "status": "Canceled", + # "transactions": [{ + # "eth": "0.23000000", + # "fee": "0.09", + # "tid": 25810126, + # "usd": "69.8947000000000000", + # "type": 2, + # "price": "303.89000000", + # "datetime": "2017-11-11 07:22:20.710567" + # }]} + # + # from create order response: + # { + # "price": "0.00008012", + # "client_order_id": '', + # "currency_pair": "XRP/BTC", + # "datetime": "2019-01-31 21:23:36", + # "amount": "15.00000000", + # "type": "0", + # "id": "2814205012" + # } + # + # cancelOrder + # + # { + # "id": 1453282316578816, + # "amount": "0.02035278", + # "price": "2100.45", + # "type": 0, + # "market": "BTC/USD" + # } + # + id = self.safe_string(order, 'id') + clientOrderId = self.safe_string(order, 'client_order_id') + side = self.safe_string(order, 'type') + if side is not None: + side = 'sell' if (side == '1') else 'buy' + # there is no timestamp from fetchOrder + timestamp = self.parse8601(self.safe_string(order, 'datetime')) + marketId = self.safe_string_lower(order, 'currency_pair') + symbol = self.safe_symbol(marketId, market, '/') + status = self.parse_order_status(self.safe_string(order, 'status')) + amount = self.safe_string(order, 'amount') + transactions = self.safe_value(order, 'transactions', []) + price = self.safe_string(order, 'price') + return self.safe_order({ + 'id': id, + 'clientOrderId': clientOrderId, + 'datetime': self.iso8601(timestamp), + 'timestamp': timestamp, + 'lastTradeTimestamp': None, + 'status': status, + 'symbol': symbol, + 'type': None, + 'timeInForce': None, + 'postOnly': None, + 'side': side, + 'price': price, + 'triggerPrice': None, + 'cost': None, + 'amount': amount, + 'filled': None, + 'remaining': None, + 'trades': transactions, + 'fee': None, + 'info': order, + 'average': None, + }, market) + + def parse_ledger_entry_type(self, type): + types: dict = { + '0': 'transaction', + '1': 'transaction', + '2': 'trade', + '14': 'transfer', + } + return self.safe_string(types, type, type) + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + # + # [ + # { + # "fee": "0.00000000", + # "btc_usd": "0.00", + # "id": 1234567894, + # "usd": 0, + # "btc": 0, + # "datetime": "2018-09-08 09:00:31", + # "type": "1", + # "xrp": "-20.00000000", + # "eur": 0, + # }, + # { + # "fee": "0.00000000", + # "btc_usd": "0.00", + # "id": 1134567891, + # "usd": 0, + # "btc": 0, + # "datetime": "2018-09-07 18:47:52", + # "type": "0", + # "xrp": "20.00000000", + # "eur": 0, + # }, + # ] + # + type = self.parse_ledger_entry_type(self.safe_string(item, 'type')) + if type == 'trade': + parsedTrade = self.parse_trade(item) + market = None + keys = list(item.keys()) + for i in range(0, len(keys)): + if keys[i].find('_') >= 0: + marketId = keys[i].replace('_', '') + market = self.safe_market(marketId, market) + # if the market is still not defined + # try to deduce it from used keys + if market is None: + market = self.get_market_from_trade(item) + direction = 'in' if (parsedTrade['side'] == 'buy') else 'out' + return self.safe_ledger_entry({ + 'info': item, + 'id': parsedTrade['id'], + 'timestamp': parsedTrade['timestamp'], + 'datetime': parsedTrade['datetime'], + 'direction': direction, + 'account': None, + 'referenceId': parsedTrade['order'], + 'referenceAccount': None, + 'type': type, + 'currency': market['base'], + 'amount': parsedTrade['amount'], + 'before': None, + 'after': None, + 'status': 'ok', + 'fee': parsedTrade['fee'], + }, currency) + else: + parsedTransaction = self.parse_transaction(item, currency) + direction = None + if 'amount' in item: + amount = self.safe_string(item, 'amount') + direction = 'in' if Precise.string_gt(amount, '0') else 'out' + elif ('currency' in parsedTransaction) and parsedTransaction['currency'] is not None: + currencyCode = self.safe_string(parsedTransaction, 'currency') + currency = self.currency(currencyCode) + amount = self.safe_string(item, currency['id']) + direction = 'in' if Precise.string_gt(amount, '0') else 'out' + return self.safe_ledger_entry({ + 'info': item, + 'id': parsedTransaction['id'], + 'timestamp': parsedTransaction['timestamp'], + 'datetime': parsedTransaction['datetime'], + 'direction': direction, + 'account': None, + 'referenceId': parsedTransaction['txid'], + 'referenceAccount': None, + 'type': type, + 'currency': parsedTransaction['currency'], + 'amount': parsedTransaction['amount'], + 'before': None, + 'after': None, + 'status': parsedTransaction['status'], + 'fee': parsedTransaction['fee'], + }, currency) + + def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + + https://www.bitstamp.net/api/#tag/Transactions-private/operation/GetUserTransactions + + :param str [code]: unified currency code, default is None + :param int [since]: timestamp in ms of the earliest ledger entry, default is None + :param int [limit]: max number of ledger entries to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ledger structure ` + """ + self.load_markets() + request: dict = {} + if limit is not None: + request['limit'] = limit + response = self.privatePostUserTransactions(self.extend(request, params)) + currency = None + if code is not None: + currency = self.currency(code) + return self.parse_ledger(response, currency, since, limit) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://www.bitstamp.net/api/#tag/Orders/operation/GetAllOpenOrders + https://www.bitstamp.net/api/#tag/Orders/operation/GetOpenOrdersForMarket + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + market = None + self.load_markets() + if symbol is not None: + market = self.market(symbol) + response = self.privatePostOpenOrdersAll(params) + # + # [ + # { + # "price": "0.00008012", + # "currency_pair": "XRP/BTC", + # "client_order_id": '', + # "datetime": "2019-01-31 21:23:36", + # "amount": "15.00000000", + # "type": "0", + # "id": "2814205012", + # } + # ] + # + return self.parse_orders(response, market, since, limit, { + 'status': 'open', + 'type': 'limit', + }) + + def get_currency_name(self, code): + """ + @ignore + :param str code: Unified currency code + :returns str: lowercase version of code + """ + return code.lower() + + def is_fiat(self, code): + return code == 'USD' or code == 'EUR' or code == 'GBP' + + def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://www.bitstamp.net/api/#tag/Deposits/operation/GetCryptoDepositAddress + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + if self.is_fiat(code): + raise NotSupported(self.id + ' fiat fetchDepositAddress() for ' + code + ' is not supported!') + name = self.get_currency_name(code) + method = 'privatePost' + self.capitalize(name) + 'Address' + response = getattr(self, method)(params) + address = self.safe_string(response, 'address') + tag = self.safe_string_2(response, 'memo_id', 'destination_tag') + self.check_address(address) + return { + 'info': response, + 'currency': code, + 'network': None, + 'address': address, + 'tag': tag, + } + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://www.bitstamp.net/api/#tag/Withdrawals/operation/RequestFiatWithdrawal + https://www.bitstamp.net/api/#tag/Withdrawals/operation/RequestCryptoWithdrawal + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + # For fiat withdrawals please provide all required additional parameters in the 'params' + # Check https://www.bitstamp.net/api/ under 'Open bank withdrawal' for list and description. + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.load_markets() + self.check_address(address) + request: dict = { + 'amount': amount, + } + currency = None + method = None + if not self.is_fiat(code): + name = self.get_currency_name(code) + method = 'privatePost' + self.capitalize(name) + 'Withdrawal' + if code == 'XRP': + if tag is not None: + request['destination_tag'] = tag + elif code == 'XLM' or code == 'HBAR': + if tag is not None: + request['memo_id'] = tag + request['address'] = address + else: + method = 'privatePostWithdrawalOpen' + currency = self.currency(code) + request['iban'] = address + request['account_currency'] = currency['id'] + response = getattr(self, method)(self.extend(request, params)) + return self.parse_transaction(response, currency) + + def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://www.bitstamp.net/api/#tag/Sub-account/operation/TransferFromMainToSub + https://www.bitstamp.net/api/#tag/Sub-account/operation/TransferFromSubToMain + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account to transfer from + :param str toAccount: account to transfer to + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transfer structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'amount': self.parse_to_numeric(self.currency_to_precision(code, amount)), + 'currency': currency['id'].upper(), + } + response = None + if fromAccount == 'main': + request['subAccount'] = toAccount + response = self.privatePostTransferFromMain(self.extend(request, params)) + elif toAccount == 'main': + request['subAccount'] = fromAccount + response = self.privatePostTransferToMain(self.extend(request, params)) + else: + raise BadRequest(self.id + ' transfer() only supports from or to main') + # + # {status: 'ok'} + # + transfer = self.parse_transfer(response, currency) + transfer['amount'] = amount + transfer['fromAccount'] = fromAccount + transfer['toAccount'] = toAccount + return transfer + + def parse_transfer(self, transfer, currency=None): + # + # {status: 'ok'} + # + status = self.safe_string(transfer, 'status') + return { + 'info': transfer, + 'id': None, + 'timestamp': None, + 'datetime': None, + 'currency': currency['code'], + 'amount': None, + 'fromAccount': None, + 'toAccount': None, + 'status': self.parse_transfer_status(status), + } + + def parse_transfer_status(self, status: Str) -> Str: + statuses: dict = { + 'ok': 'ok', + 'error': 'failed', + } + return self.safe_string(statuses, status, status) + + def nonce(self): + return self.milliseconds() + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = self.urls['api'][api] + '/' + url += self.version + '/' + url += self.implode_params(path, params) + query = self.omit(params, self.extract_params(path)) + if api == 'public': + if query: + url += '?' + self.urlencode(query) + else: + self.check_required_credentials() + xAuth = 'BITSTAMP ' + self.apiKey + xAuthNonce = self.uuid() + xAuthTimestamp = str(self.milliseconds()) + xAuthVersion = 'v2' + contentType = '' + headers = { + 'X-Auth': xAuth, + 'X-Auth-Nonce': xAuthNonce, + 'X-Auth-Timestamp': xAuthTimestamp, + 'X-Auth-Version': xAuthVersion, + } + if method == 'POST': + if query: + body = self.urlencode(query) + contentType = 'application/x-www-form-urlencoded' + headers['Content-Type'] = contentType + else: + # sending an empty POST request will trigger + # an API0020 error returned by the exchange + # therefore for empty requests we send a dummy object + # https://github.com/ccxt/ccxt/issues/6846 + body = self.urlencode({'foo': 'bar'}) + contentType = 'application/x-www-form-urlencoded' + headers['Content-Type'] = contentType + authBody = body if body else '' + auth = xAuth + method + url.replace('https://', '') + contentType + xAuthNonce + xAuthTimestamp + xAuthVersion + authBody + signature = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256) + headers['X-Auth-Signature'] = signature + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None + # + # {"error": "No permission found"} # fetchDepositAddress returns self on apiKeys that don't have the permission required + # {"status": "error", "reason": {"__all__": ["Minimum order size is 5.0 EUR."]}} + # reuse of a nonce gives: {status: 'error', reason: 'Invalid nonce', code: 'API0004'} + # + status = self.safe_string(response, 'status') + error = self.safe_value(response, 'error') + if (status == 'error') or (error is not None): + errors = [] + if isinstance(error, str): + errors.append(error) + elif error is not None: + keys = list(error.keys()) + for i in range(0, len(keys)): + key = keys[i] + value = self.safe_value(error, key) + if isinstance(value, list): + errors = self.array_concat(errors, value) + else: + errors.append(value) + reasonInner = self.safe_value(response, 'reason', {}) + if isinstance(reasonInner, str): + errors.append(reasonInner) + else: + all = self.safe_value(reasonInner, '__all__', []) + for i in range(0, len(all)): + errors.append(all[i]) + code = self.safe_string(response, 'code') + if code == 'API0005': + raise AuthenticationError(self.id + ' invalid signature, use the uid for the main account if you have subaccounts') + feedback = self.id + ' ' + body + for i in range(0, len(errors)): + value = errors[i] + self.throw_exactly_matched_exception(self.exceptions['exact'], value, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], value, feedback) + raise ExchangeError(feedback) + return None diff --git a/ccxt/bitteam.py b/ccxt/bitteam.py new file mode 100644 index 0000000..ce23c99 --- /dev/null +++ b/ccxt/bitteam.py @@ -0,0 +1,2381 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.bitteam import ImplicitAPI +from ccxt.base.types import Any, Balances, Currencies, Currency, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class bitteam(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(bitteam, self).describe(), { + 'id': 'bitteam', + 'name': 'BIT.TEAM', + 'countries': ['UK'], + 'version': 'v2.0.6', + 'rateLimit': 1, # the exchange has no rate limit + 'certified': False, + 'pro': False, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'cancelOrders': False, + 'closeAllPositions': False, + 'closePosition': False, + 'createDepositAddress': False, + 'createOrder': True, + 'createOrderWithTakeProfitAndStopLoss': False, + 'createOrderWithTakeProfitAndStopLossWs': False, + 'createPostOnlyOrder': False, + 'createReduceOnlyOrder': False, + 'createStopLimitOrder': False, + 'createStopMarketOrder': False, + 'createStopOrder': False, + 'deposit': False, + 'editOrder': False, + 'fetchAccounts': False, + 'fetchBalance': True, + 'fetchBidsAsks': False, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchCanceledOrders': True, + 'fetchClosedOrder': False, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDeposit': False, + 'fetchDepositAddress': False, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': False, + 'fetchDepositsWithdrawals': True, + 'fetchDepositWithdrawFee': False, + 'fetchDepositWithdrawFees': False, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchL3OrderBook': False, + 'fetchLedger': False, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrder': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrderBooks': False, + 'fetchOrders': True, + 'fetchOrderTrades': False, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchStatus': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': False, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': False, + 'fetchTradingLimits': False, + 'fetchTransactionFee': False, + 'fetchTransactionFees': False, + 'fetchTransactions': True, + 'fetchTransfers': False, + 'fetchVolatilityHistory': False, + 'fetchWithdrawal': False, + 'fetchWithdrawals': False, + 'fetchWithdrawalWhitelist': False, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'repayMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'signIn': False, + 'transfer': False, + 'withdraw': False, + 'ws': False, + }, + 'timeframes': { + '1m': '1', + '5m': '5', + '15m': '15', + '1h': '60', + '1d': '1D', + }, + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/b41b5e0d-98e5-4bd3-8a6e-aeb230a4a135', + 'api': { + 'history': 'https://history.bit.team', + 'public': 'https://bit.team', + 'private': 'https://bit.team', + }, + 'www': 'https://bit.team/', + 'referral': 'https://bit.team/auth/sign-up?ref=bitboy2023', + 'doc': [ + 'https://bit.team/trade/api/documentation', + ], + }, + 'api': { + 'history': { + 'get': { + 'api/tw/history/{pairName}/{resolution}': 1, + }, + }, + 'public': { + 'get': { + 'trade/api/asset': 1, # not unified + 'trade/api/currencies': 1, + 'trade/api/orderbooks/{symbol}': 1, # not unified + 'trade/api/orders': 1, # not unified + 'trade/api/pair/{name}': 1, + 'trade/api/pairs': 1, # not unified + 'trade/api/pairs/precisions': 1, # not unified + 'trade/api/rates': 1, # not unified + 'trade/api/trade/{id}': 1, # not unified + 'trade/api/trades': 1, # not unified + 'trade/api/ccxt/pairs': 1, + 'trade/api/cmc/assets': 1, + 'trade/api/cmc/orderbook/{pair}': 1, + 'trade/api/cmc/summary': 1, + 'trade/api/cmc/ticker': 1, # not unified + 'trade/api/cmc/trades/{pair}': 1, + }, + }, + 'private': { + 'get': { + 'trade/api/ccxt/balance': 1, + 'trade/api/ccxt/order/{id}': 1, + 'trade/api/ccxt/ordersOfUser': 1, + 'trade/api/ccxt/tradesOfUser': 1, + 'trade/api/transactionsOfUser': 1, + }, + 'post': { + 'trade/api/ccxt/cancel-all-order': 1, + 'trade/api/ccxt/cancelorder': 1, + 'trade/api/ccxt/ordercreate': 1, + }, + }, + }, + 'fees': { + 'trading': { + 'feeSide': 'get', + 'tierBased': False, + 'percentage': True, + 'taker': self.parse_number('0.002'), + 'maker': self.parse_number('0.002'), + }, + }, + 'precisionMode': TICK_SIZE, + # exchange-specific options + 'options': { + 'networksById': { + 'Ethereum': 'ERC20', + 'ethereum': 'ERC20', + 'Tron': 'TRC20', + 'tron': 'TRC20', + 'Binance': 'BSC', + 'binance': 'BSC', + 'Binance Smart Chain': 'BSC', + 'bscscan': 'BSC', + 'Bitcoin': 'BTC', + 'bitcoin': 'BTC', + 'Litecoin': 'LTC', + 'litecoin': 'LTC', + 'Polygon': 'POLYGON', + 'polygon': 'POLYGON', + 'PRIZM': 'PRIZM', + 'Decimal': 'Decimal', + 'ufobject': 'ufobject', + 'tonchain': 'tonchain', + }, + 'currenciesValuedInUsd': { + 'USDT': True, + 'BUSD': True, + }, + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': False, + 'triggerPriceType': None, + 'triggerDirection': None, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': False, + 'FOK': False, + 'PO': False, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyRequiresPrice': False, + 'marketBuyByCost': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 100000, + 'untilDays': 100000, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 100, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': True, + 'limit': 100, + 'daysBack': None, + 'untilDays': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': None, + 'daysBackCanceled': None, + 'untilDays': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 1000, + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'exceptions': { + 'exact': { + '400002': BadSymbol, # {"ok":false,"code":400002,"message":"An order cannot be created on a deactivated pair"} + '401000': AuthenticationError, # {"ok":false,"code":401000,"data": {},"message": "Missing authentication"} + '403002': BadRequest, # {"ok":false,"code":403002,"data":{},"message":"Order cannot be deleted, status does not match"} + '404200': BadSymbol, # {"ok":false,"code":404200,"data":{},"message":"Pair was not found"} + }, + 'broad': { + 'is not allowed': BadRequest, # {"message":"\"createdAt\" is not allowed","path":["createdAt"],"type":"object.unknown","context":{"child":"createdAt","label":"createdAt","value":"DESC","key":"createdAt"}} + 'Insufficient funds': InsufficientFunds, # {"ok":false,"code":450000,"data":null,"message":"Insufficient funds"} + 'Invalid request params input': BadRequest, # {"ok":false,"code":400000,"data":{},"message":"Invalid request params input"} + 'must be a number': BadRequest, # [ExchangeError] bitteam {"message":"\"currency\" must be a number","path":["currency"],"type":"number.base","context":{"label":"currency","value":"adsf","key":"currency"}} + 'must be a string': BadRequest, # {"message":"\"pairId\" must be a string","path":["pairId"],"type":"string.base","context":{"label":"pairId","value":87,"key":"pairId"}} + 'must be of type': BadRequest, # {"message":"\"order\" must be of type object","path":["order"],"type":"object.base","context":{"type":"object","label":"order","value":"107218781","key":"order"}} + 'must be one of': BadRequest, # {"message":"\"resolution\" must be one of [1, 5, 15, 60, 1D]","path":["resolution"],"type":"any.only","context":{"valids":["1","5","15","60","1D"],"label":"resolution","value":"1d","key":"resolution"}} + 'Order not found': OrderNotFound, # {"ok":false,"code":404300,"data":{},"message":"Order not found"} + 'Pair with pair name': BadSymbol, # {"ok":false,"code":404000,"data":{"pairName":"ETH_USasdf"},"msg":"Pair with pair name ETH_USasdf was not found"} + 'pairName': BadSymbol, # {"message":"\"pairName\" length must be at least 7 characters long","path":["pairName"],"type":"string.min","context":{"limit":7,"value":"ETH_US","label":"pairName","key":"pairName"}} + 'Service Unavailable': ExchangeNotAvailable, # {"message":"Service Unavailable","code":403000,"ok":false} + 'Symbol ': BadSymbol, # {"ok":false,"code":404000,"data":{},"message":"Symbol asdfasdfas was not found"} + }, + }, + }) + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for bitteam + + https://bit.team/trade/api/documentation#/CCXT/getTradeApiCcxtPairs + + :param dict [params]: extra parameters specific to the exchange api endpoint + :returns dict[]: an array of objects representing market data + """ + response = self.publicGetTradeApiCcxtPairs(params) + # + # { + # "ok": True, + # "result": { + # "count": 28, + # "pairs": [ + # { + # "id": 2, + # "name": "eth_usdt", + # "baseAssetId": 2, + # "quoteAssetId": 3, + # "fullName": "ETH USDT", + # "description": "ETH USDT", + # "lastBuy": 1964.665001, + # "lastSell": 1959.835005, + # "lastPrice": 1964.665001, + # "change24": 1.41, + # "volume24": 28.22627543, + # "volume24USD": 55662.35636401598, + # "active": True, + # "baseStep": 8, + # "quoteStep": 6, + # "status": 1, + # "settings": { + # "limit_usd": "0.1", + # "price_max": "10000000000000", + # "price_min": "1", + # "price_tick": "1", + # "pricescale": 10000, + # "lot_size_max": "1000000000000000", + # "lot_size_min": "1", + # "lot_size_tick": "1", + # "price_view_min": 6, + # "default_slippage": 10, + # "lot_size_view_min": 6 + # }, + # "updateId": "50620", + # "timeStart": "2021-01-28T09:19:30.706Z", + # "makerFee": 200, + # "takerFee": 200, + # "quoteVolume24": 54921.93404134529, + # "lowPrice24": 1919.355, + # "highPrice24": 1971.204995 + # }, + # { + # "id": 27, + # "name": "ltc_usdt", + # "baseAssetId": 13, + # "quoteAssetId": 3, + # "fullName": "LTC USDT", + # "description": "This is LTC USDT", + # "lastBuy": 53.14, + # "lastSell": 53.58, + # "lastPrice": 53.58, + # "change24": -6.72, + # "volume24": 0, + # "volume24USD": null, + # "active": True, + # "baseStep": 8, + # "quoteStep": 6, + # "status": 0, + # "settings": { + # "limit_usd": "0.1", + # "price_max": "1000000000000", + # "price_min": "1", + # "price_tick": "1", + # "pricescale": 10000, + # "lot_size_max": "1000000000000", + # "lot_size_min": "1", + # "lot_size_tick": "1", + # "price_view_min": 6, + # "default_slippage": 10, + # "lot_size_view_min": 6 + # }, + # "updateId": "30", + # "timeStart": "2021-10-13T12:11:05.359Z", + # "makerFee": 200, + # "takerFee": 200, + # "quoteVolume24": 0, + # "lowPrice24": null, + # "highPrice24": null + # } + # ] + # } + # } + # + result = self.safe_value(response, 'result', {}) + markets = self.safe_value(result, 'pairs', []) + return self.parse_markets(markets) + + def parse_market(self, market: dict) -> Market: + id = self.safe_string(market, 'name') + numericId = self.safe_integer(market, 'id') + parts = id.split('_') + baseId = self.safe_string(parts, 0) + quoteId = self.safe_string(parts, 1) + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + active = self.safe_value(market, 'active') + timeStart = self.safe_string(market, 'timeStart') + created = self.parse8601(timeStart) + minCost = None + currenciesValuedInUsd = self.safe_value(self.options, 'currenciesValuedInUsd', {}) + quoteInUsd = self.safe_bool(currenciesValuedInUsd, quote, False) + if quoteInUsd: + settings = self.safe_value(market, 'settings', {}) + minCost = self.safe_number(settings, 'limit_usd') + return self.safe_market_structure({ + 'id': id, + 'numericId': numericId, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': active, + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(self.parse_precision(self.safe_string(market, 'baseStep'))), + 'price': self.parse_number(self.parse_precision(self.safe_string(market, 'quoteStep'))), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': None, + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': minCost, + 'max': None, + }, + }, + 'created': created, + 'info': market, + }) + + def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://bit.team/trade/api/documentation#/PUBLIC/getTradeApiCurrencies + + :param dict [params]: extra parameters specific to the bitteam api endpoint + :returns dict: an associative dictionary of currencies + """ + response = self.publicGetTradeApiCurrencies(params) + # + # { + # "ok": True, + # "result": { + # "count": 24, + # "currencies": [ + # { + # "txLimits": { + # "minDeposit": "0.0001", + # "minWithdraw": "0.02", + # "maxWithdraw": "10000", + # "withdrawCommissionPercentage": "NaN", + # "withdrawCommissionFixed": "0.005" + # }, + # "id": 2, + # "status": 1, + # "symbol": "eth", + # "title": "Ethereum", + # "logoURL": "https://ethereum.org/static/6b935ac0e6194247347855dc3d328e83/34ca5/eth-diamond-black.png", + # "isDiscount": False, + # "address": "https://ethereum.org/", + # "description": "Ethereum ETH", + # "decimals": 18, + # "blockChain": "Ethereum", + # "precision": 8, + # "currentRate": null, + # "active": True, + # "timeStart": "2021-01-28T08:57:41.719Z", + # "type": "crypto", + # "typeNetwork": "internalGW", + # "idSorting": 2, + # "links": [ + # { + # "tx": "https://etherscan.io/tx/", + # "address": "https://etherscan.io/address/", + # "blockChain": "Ethereum" + # } + # ] + # }, + # { + # "txLimits": { + # "minDeposit": "0.001", + # "minWithdraw": "1", + # "maxWithdraw": "100000", + # "withdrawCommissionPercentage": "NaN", + # "withdrawCommissionFixed": { + # "Tron": "2", + # "Binance": "2", + # "Ethereum": "20" + # } + # }, + # "id": 3, + # "status": 1, + # "symbol": "usdt", + # "title": "Tether USD", + # "logoURL": "https://cryptologos.cc/logos/tether-usdt-logo.png?v=010", + # "isDiscount": False, + # "address": "https://tether.to/", + # "description": "Tether USD", + # "decimals": 6, + # "blockChain": "", + # "precision": 6, + # "currentRate": null, + # "active": True, + # "timeStart": "2021-01-28T09:04:17.170Z", + # "type": "crypto", + # "typeNetwork": "internalGW", + # "idSorting": 0, + # "links": [ + # { + # "tx": "https://etherscan.io/tx/", + # "address": "https://etherscan.io/address/", + # "blockChain": "Ethereum" + # }, + # { + # "tx": "https://tronscan.org/#/transaction/", + # "address": "https://tronscan.org/#/address/", + # "blockChain": "Tron" + # }, + # { + # "tx": "https://bscscan.com/tx/", + # "address": "https://bscscan.com/address/", + # "blockChain": "Binance" + # } + # ] + # } + # ] + # } + # } + # + responseResult = self.safe_value(response, 'result', {}) + currencies = self.safe_value(responseResult, 'currencies', []) + # usding another endpoint to fetch statuses of deposits and withdrawals + statusesResponse = self.publicGetTradeApiCmcAssets() + # + # { + # "ZNX": { + # "name": "ZeNeX Coin", + # "unified_cryptoasset_id": 30, + # "withdrawStatus": True, + # "depositStatus": True, + # "min_withdraw": 0.00001, + # "max_withdraw": 10000 + # }, + # "USDT": { + # "name": "Tether USD", + # "unified_cryptoasset_id": 3, + # "withdrawStatus": True, + # "depositStatus": True, + # "min_withdraw": 1, + # "max_withdraw": 100000 + # }, + # } + # + statusesResponse = self.index_by(statusesResponse, 'unified_cryptoasset_id') + result: dict = {} + for i in range(0, len(currencies)): + currency = currencies[i] + id = self.safe_string(currency, 'symbol') + numericId = self.safe_integer(currency, 'id') + code = self.safe_currency_code(id) + active = self.safe_bool(currency, 'active', False) + precision = self.parse_number(self.parse_precision(self.safe_string(currency, 'precision'))) + txLimits = self.safe_value(currency, 'txLimits', {}) + minWithdraw = self.safe_string(txLimits, 'minWithdraw') + maxWithdraw = self.safe_string(txLimits, 'maxWithdraw') + minDeposit = self.safe_string(txLimits, 'minDeposit') + fee = None + withdrawCommissionFixed = self.safe_value(txLimits, 'withdrawCommissionFixed', {}) + feesByNetworkId: dict = {} + blockChain = self.safe_string(currency, 'blockChain') + # if only one blockChain + if (blockChain is not None) and (blockChain != ''): + fee = self.parse_number(withdrawCommissionFixed) + feesByNetworkId[blockChain] = fee + else: + feesByNetworkId = withdrawCommissionFixed + statuses = self.safe_value(statusesResponse, numericId, {}) + deposit = self.safe_value(statuses, 'depositStatus') + withdraw = self.safe_value(statuses, 'withdrawStatus') + networkIds = list(feesByNetworkId.keys()) + networks: dict = {} + networkPrecision = self.parse_number(self.parse_precision(self.safe_string(currency, 'decimals'))) + typeRaw = self.safe_string(currency, 'type') + for j in range(0, len(networkIds)): + networkId = networkIds[j] + networkCode = self.network_id_to_code(networkId, code) + networkFee = self.safe_number(feesByNetworkId, networkId) + networks[networkCode] = { + 'id': networkId, + 'network': networkCode, + 'deposit': deposit, + 'withdraw': withdraw, + 'active': active, + 'fee': networkFee, + 'precision': networkPrecision, + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': self.parse_number(minWithdraw), + 'max': self.parse_number(maxWithdraw), + }, + 'deposit': { + 'min': self.parse_number(minDeposit), + 'max': None, + }, + }, + 'info': currency, + } + result[code] = { + 'id': id, + 'numericId': numericId, + 'code': code, + 'name': code, + 'info': currency, + 'active': active, + 'deposit': deposit, + 'withdraw': withdraw, + 'fee': fee, + 'precision': precision, + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': self.parse_number(minWithdraw), + 'max': self.parse_number(maxWithdraw), + }, + 'deposit': { + 'min': self.parse_number(minDeposit), + 'max': None, + }, + }, + 'type': typeRaw, # 'crypto' or 'fiat' + 'networks': networks, + } + return result + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the bitteam api endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + market = self.market(symbol) + resolution = self.safe_string(self.timeframes, timeframe, timeframe) + request: dict = { + 'pairName': market['id'], + 'resolution': resolution, + } + response = self.historyGetApiTwHistoryPairNameResolution(self.extend(request, params)) + # + # { + # "ok": True, + # "result": { + # "count": 364, + # "data": [ + # { + # "t": 1669593600, + # "o": 16211.259266, + # "h": 16476.985001, + # "l": 16023.714999, + # "c": 16430.636894, + # "v": 2.60150368999999 + # }, + # { + # "t": 1669680000, + # "o": 16430.636894, + # "h": 17065.229582, + # "l": 16346.114155, + # "c": 16882.297736, + # "v": 3.0872548400000115 + # }, + # ... + # ] + # } + # } + # + result = self.safe_value(response, 'result', {}) + data = self.safe_list(result, 'data', []) + return self.parse_ohlcvs(data, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # { + # "t": 1669680000, + # "o": 16430.636894, + # "h": 17065.229582, + # "l": 16346.114155, + # "c": 16882.297736, + # "v": 3.0872548400000115 + # }, + # + return [ + self.safe_timestamp(ohlcv, 't'), + self.safe_number(ohlcv, 'o'), + self.safe_number(ohlcv, 'h'), + self.safe_number(ohlcv, 'l'), + self.safe_number(ohlcv, 'c'), + self.safe_number(ohlcv, 'v'), + ] + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://bit.team/trade/api/documentation#/CMC/getTradeApiCmcOrderbookPair + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return(default 100, max 200) + :param dict [params]: extra parameters specific to the bitteam api endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + response = self.publicGetTradeApiCmcOrderbookPair(self.extend(request, params)) + # + # { + # "timestamp": 1701166703285, + # "bids": [ + # [ + # 2019.334988, + # 0.09048525 + # ], + # [ + # 1999.860002, + # 0.0225 + # ], + # ... + # ], + # "asks": [ + # [ + # 2019.334995, + # 0.00899078 + # ], + # [ + # 2019.335013, + # 0.09833052 + # ], + # ... + # ] + # } + # + timestamp = self.safe_integer(response, 'timestamp') + orderbook = self.parse_order_book(response, symbol, timestamp) + return orderbook + + def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://bit.team/trade/api/documentation#/PRIVATE/getTradeApiCcxtOrdersofuser + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of orde structures to retrieve(default 10) + :param dict [params]: extra parameters specific to the bitteam api endpoint + :param str [params.type]: the status of the order - 'active', 'closed', 'cancelled', 'all', 'history'(default 'all') + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + type = self.safe_string(params, 'type', 'all') + request: dict = { + 'type': type, + } + market = None + if symbol is not None: + market = self.market(symbol) + request['pair'] = market['id'] + if limit is not None: + request['limit'] = limit + response = self.privateGetTradeApiCcxtOrdersOfUser(self.extend(request, params)) + # + # { + # "ok": True, + # "result": { + # "count": 3, + # "orders": [ + # { + # "id": 106733026, + # "orderId": null, + # "userId": 21639, + # "pair": "btc_usdt", + # "pairId": 22, + # "quantity": "0.00001", + # "price": "40", + # "executedPrice": "0", + # "fee": null, + # "orderCid": null, + # "executed": "0", + # "expires": null, + # "baseDecimals": 8, + # "quoteDecimals": 6, + # "timestamp": 1700594804, + # "status": "inactive", + # "side": "buy", + # "type": "limit", + # "createdAt": "2023-11-21T19:26:43.868Z", + # "updatedAt": "2023-11-21T19:26:43.868Z" + # }, + # { + # "id": 106733308, + # "orderId": "13074362", + # "userId": 21639, + # "pair": "btc_usdt", + # "pairId": 22, + # "quantity": "0.00001", + # "price": "50000", + # "executedPrice": "37017.495008", + # "fee": { + # "amount": "0.00000002", + # "symbol": "btc", + # "userId": 21639, + # "decimals": 8, + # "symbolId": 11 + # }, + # "orderCid": null, + # "executed": "0.00001", + # "expires": null, + # "baseDecimals": 8, + # "quoteDecimals": 6, + # "timestamp": 1700594959, + # "status": "executed", + # "side": "buy", + # "type": "limit", + # "createdAt": "2023-11-21T19:29:19.946Z", + # "updatedAt": "2023-11-21T19:29:19.946Z" + # }, + # { + # "id": 106734455, + # "orderId": "13248984", + # "userId": 21639, + # "pair": "eth_usdt", + # "pairId": 2, + # "quantity": "0.001", + # "price": "1750", + # "executedPrice": "0", + # "fee": null, + # "orderCid": null, + # "executed": "0", + # "expires": null, + # "baseDecimals": 18, + # "quoteDecimals": 6, + # "timestamp": 1700595523, + # "status": "accepted", + # "side": "buy", + # "type": "limit", + # "createdAt": "2023-11-21T19:38:43.530Z", + # "updatedAt": "2023-11-21T19:38:43.530Z" + # } + # ] + # } + # } + # + result = self.safe_value(response, 'result', {}) + orders = self.safe_list(result, 'orders', []) + return self.parse_orders(orders, market, since, limit) + + def fetch_order(self, id: str, symbol: Str = None, params={}) -> Order: + """ + fetches information on an order + + https://bit.team/trade/api/documentation#/PRIVATE/getTradeApiCcxtOrderId + + :param int|str id: order id + :param str symbol: not used by bitteam fetchOrder() + :param dict [params]: extra parameters specific to the bitteam api endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + request: dict = { + 'id': id, + } + market = None + if symbol is not None: + market = self.market(symbol) + response = self.privateGetTradeApiCcxtOrderId(self.extend(request, params)) + # + # { + # "ok": True, + # "result": { + # "id": 106494347, + # "orderId": "13214332", + # "userId": 15912, + # "pair": "eth_usdt", + # "pairId": 2, + # "quantity": "0.00448598", + # "price": "2015.644995", + # "executedPrice": "2015.644995", + # "fee": { + # "amount": "0", + # "symbol": "eth", + # "userId": 15912, + # "decimals": 18, + # "symbolId": 2, + # "discountAmount": "0", + # "discountSymbol": "btt", + # "discountDecimals": 18, + # "discountSymbolId": 5 + # }, + # "orderCid": null, + # "executed": "0.00448598", + # "expires": null, + # "baseDecimals": 18, + # "quoteDecimals": 6, + # "timestamp": 1700470476, + # "status": "executed", + # "side": "buy", + # "type": "limit", + # "stopPrice": null, + # "slippage": null + # } + # } + # + result = self.safe_dict(response, 'result') + return self.parse_order(result, market) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://bit.team/trade/api/documentation#/PRIVATE/getTradeApiCcxtOrdersofuser + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open order structures to retrieve(default 10) + :param dict [params]: extra parameters specific to the bitteam api endpoint + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + request: dict = { + 'type': 'active', + } + return self.fetch_orders(symbol, since, limit, self.extend(request, params)) + + def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://bit.team/trade/api/documentation#/PRIVATE/getTradeApiCcxtOrdersofuser + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of closed order structures to retrieve(default 10) + :param dict [params]: extra parameters specific to the bitteam api endpoint + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + request: dict = { + 'type': 'closed', + } + return self.fetch_orders(symbol, since, limit, self.extend(request, params)) + + def fetch_canceled_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches information on multiple canceled orders made by the user + + https://bit.team/trade/api/documentation#/PRIVATE/getTradeApiCcxtOrdersofuser + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of canceled order structures to retrieve(default 10) + :param dict [params]: extra parameters specific to the bitteam api endpoint + :returns dict: a list of `order structures ` + """ + self.load_markets() + request: dict = { + 'type': 'cancelled', + } + return self.fetch_orders(symbol, since, limit, self.extend(request, params)) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://bit.team/trade/api/documentation#/PRIVATE/postTradeApiCcxtOrdercreate + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the bitteam api endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pairId': str(market['numericId']), + 'type': type, + 'side': side, + 'amount': self.amount_to_precision(symbol, amount), + } + if type == 'limit': + if price is None: + raise ArgumentsRequired(self.id + ' createOrder() requires a price argument for a ' + type + ' order') + else: + request['price'] = self.price_to_precision(symbol, price) + response = self.privatePostTradeApiCcxtOrdercreate(self.extend(request, params)) + # + # { + # "ok": True, + # "result": { + # "id": 106733308, + # "userId": 21639, + # "quantity": "0.00001", + # "pair": "btc_usdt", + # "side": "buy", + # "price": "50000", + # "executed": "0", + # "executedPrice": "0", + # "status": "created", + # "baseDecimals": 8, + # "quoteDecimals": 6, + # "pairId": 22, + # "type": "limit", + # "stopPrice": null, + # "slippage": null, + # "timestamp": "1700594959" + # } + # } + # + order = self.safe_dict(response, 'result', {}) + return self.parse_order(order, market) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://bit.team/trade/api/documentation#/PRIVATE/postTradeApiCcxtCancelorder + + :param str id: order id + :param str symbol: not used by bitteam cancelOrder() + :param dict [params]: extra parameters specific to the bitteam api endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + request: dict = { + 'id': id, + } + response = self.privatePostTradeApiCcxtCancelorder(self.extend(request, params)) + # + # { + # "ok": True, + # "result": { + # "message": "The request to cancel your order was received" + # } + # } + # + result = self.safe_dict(response, 'result', {}) + return self.parse_order(result) + + def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel open orders of market + + https://bit.team/trade/api/documentation#/PRIVATE/postTradeApiCcxtCancelallorder + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the bitteam api endpoint + :returns dict[]: a list of `order structures ` + """ + self.load_markets() + market = None + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['pairId'] = str(market['numericId']) + else: + request['pairId'] = '0' # '0' for all markets + response = self.privatePostTradeApiCcxtCancelAllOrder(self.extend(request, params)) + # + # { + # "ok": True, + # "result": { + # "message":"The request to cancel all your orders was received" + # } + # } + # + result = self.safe_value(response, 'result', {}) + orders = [result] + return self.parse_orders(orders, market) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # fetchOrders + # { + # "id": 106733308, + # "orderId": "13074362", + # "userId": 21639, + # "pair": "btc_usdt", + # "pairId": 22, + # "quantity": "0.00001", + # "price": "50000", + # "executedPrice": "37017.495008", + # "fee": { + # "amount": "0.00000002", + # "symbol": "btc", + # "userId": 21639, + # "decimals": 8, + # "symbolId": 11 + # }, + # "orderCid": null, + # "executed": "0.00001", + # "expires": null, + # "baseDecimals": 8, + # "quoteDecimals": 6, + # "timestamp": 1700594959, + # "status": "executed", + # "side": "buy", + # "type": "limit", + # "createdAt": "2023-11-21T19:29:19.946Z", + # "updatedAt": "2023-11-21T19:29:19.946Z" + # }, + # + # fetchOrder + # { + # "id": 106494347, + # "orderId": "13214332", + # "userId": 15912, + # "pair": "eth_usdt", + # "pairId": 2, + # "quantity": "0.00448598", + # "price": "2015.644995", + # "executedPrice": "2015.644995", + # "fee": { + # "amount": "0", + # "symbol": "eth", + # "userId": 15912, + # "decimals": 18, + # "symbolId": 2, + # "discountAmount": "0", + # "discountSymbol": "btt", + # "discountDecimals": 18, + # "discountSymbolId": 5 + # }, + # "orderCid": null, + # "executed": "0.00448598", + # "expires": null, + # "baseDecimals": 18, + # "quoteDecimals": 6, + # "timestamp": 1700470476, + # "status": "executed", + # "side": "buy", + # "type": "limit", + # "stopPrice": null, + # "slippage": null + # } + # + # createOrder + # { + # "id": 106733308, + # "userId": 21639, + # "quantity": "0.00001", + # "pair": "btc_usdt", + # "side": "buy", + # "price": "50000", + # "executed": "0", + # "executedPrice": "0", + # "status": "created", + # "baseDecimals": 8, + # "quoteDecimals": 6, + # "pairId": 22, + # "type": "limit", + # "stopPrice": null, + # "slippage": null, + # "timestamp": "1700594959" + # } + # + id = self.safe_string(order, 'id') + marketId = self.safe_string(order, 'pair') + market = self.safe_market(marketId, market) + clientOrderId = self.safe_string(order, 'orderCid') + timestamp = None + createdAt = self.safe_string(order, 'createdAt') + if createdAt is not None: + timestamp = self.parse8601(createdAt) + else: + timestamp = self.safe_timestamp(order, 'timestamp') + updatedAt = self.safe_string(order, 'updatedAt') + lastUpdateTimestamp = self.parse8601(updatedAt) + status = self.parse_order_status(self.safe_string(order, 'status')) + type = self.parse_order_type(self.safe_string(order, 'type')) + side = self.safe_string(order, 'side') + feeRaw = self.safe_value(order, 'fee') + price = self.safe_string(order, 'price') + amount = self.safe_string(order, 'quantity') + filled = self.safe_string(order, 'executed') + fee = None + if feeRaw is not None: + feeCost = self.safe_string(feeRaw, 'amount') + feeCurrencyId = self.safe_string(feeRaw, 'symbol') + fee = { + 'currency': self.safe_currency_code(feeCurrencyId), + 'cost': feeCost, + 'rate': None, + } + return self.safe_order({ + 'id': id, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'lastUpdateTimestamp': lastUpdateTimestamp, + 'status': status, + 'symbol': market['symbol'], + 'type': type, + 'timeInForce': 'GTC', + 'side': side, + 'price': price, + 'triggerPrice': self.safe_string(order, 'stopPrice'), + 'average': None, + 'amount': amount, + 'cost': None, + 'filled': filled, + 'remaining': None, + 'fee': fee, + 'trades': None, + 'info': order, + 'postOnly': False, + }, market) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'accepted': 'open', + 'executed': 'closed', + 'cancelled': 'canceled', + 'partiallyCancelled': 'canceled', + 'delete': 'rejected', + 'inactive': 'rejected', + 'executing': 'open', + 'created': 'open', + } + return self.safe_string(statuses, status, status) + + def parse_order_type(self, status): + statuses: dict = { + 'market': 'market', + 'limit': 'limit', + } + return self.safe_string(statuses, status, status) + + def parse_value_to_pricision(self, valueObject, valueKey, preciseObject, precisionKey): + valueRawString = self.safe_string(valueObject, valueKey) + precisionRawString = self.safe_string(preciseObject, precisionKey) + if valueRawString is None or precisionRawString is None: + return None + precisionString = self.parse_precision(precisionRawString) + return Precise.string_mul(valueRawString, precisionString) + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical calculations with the information calculated over the past 24 hours each market + + https://bit.team/trade/api/documentation#/CMC/getTradeApiCmcSummary + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the bitteam api endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + response = self.publicGetTradeApiCmcSummary() + # + # [ + # { + # "trading_pairs": "BTC_USDT", + # "base_currency": "BTC", + # "quote_currency": "USDT", + # "last_price": 37669.955001, + # "lowest_ask": 37670.055, + # "highest_bid": 37669.955, + # "base_volume": 6.81156888, + # "quote_volume": 257400.516878529, + # "price_change_percent_24h": -0.29, + # "highest_price_24h": 38389.994463, + # "lowest_price_24h": 37574.894999 + # }, + # { + # "trading_pairs": "BNB_USDT", + # "base_currency": "BNB", + # "quote_currency": "USDT", + # "last_price": 233.525142, + # "lowest_ask": 233.675, + # "highest_bid": 233.425, + # "base_volume": 245.0199339, + # "quote_volume": 57356.91823827642, + # "price_change_percent_24h": -0.32, + # "highest_price_24h": 236.171123, + # "lowest_price_24h": 231.634637 + # }, + # ... + # ] + # + tickers = [] + if not isinstance(response, list): + response = [] + for i in range(0, len(response)): + rawTicker = response[i] + ticker = self.parse_ticker(rawTicker) + tickers.append(ticker) + return self.filter_by_array_tickers(tickers, 'symbol', symbols) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://bit.team/trade/api/documentation#/PUBLIC/getTradeApiPairName + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the bitteam api endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'name': market['id'], + } + response = self.publicGetTradeApiPairName(self.extend(request, params)) + # + # { + # "ok": True, + # "result": { + # "pair": { + # "id": 2, + # "name": "eth_usdt", + # "baseAssetId": 2, + # "quoteAssetId": 3, + # "fullName": "ETH USDT", + # "description": "ETH USDT", + # "lastBuy": "1976.715012", + # "lastSell": "1971.995006", + # "lastPrice": "1976.715012", + # "change24": "1.02", + # "volume24": 24.0796457, + # "volume24USD": 44282.347995912205, + # "active": True, + # "baseStep": 8, + # "quoteStep": 6, + # "status": 1, + # "settings": { + # "limit_usd": "0.1", + # "price_max": "10000000000000", + # "price_min": "1", + # "price_tick": "1", + # "pricescale": 10000, + # "lot_size_max": "1000000000000000", + # "lot_size_min": "1", + # "lot_size_tick": "1", + # "price_view_min": 6, + # "default_slippage": 10, + # "lot_size_view_min": 6 + # }, + # "asks": [ + # { + # "price": "1976.405003", + # "quantity": "0.0051171", + # "amount": "10.1134620408513" + # }, + # { + # "price": "1976.405013", + # "quantity": "0.09001559", + # "amount": "177.90726332415267" + # }, + # { + # "price": "2010.704988", + # "quantity": "0.00127892", + # "amount": "2.57153082325296" + # } + # ], + # "bids": [ + # { + # "price": "1976.404988", + # "quantity": "0.09875861", + # "amount": "195.18700941194668" + # }, + # { + # "price": "1905.472973", + # "quantity": "0.00263591", + # "amount": "5.02265526426043" + # }, + # { + # "price": "1904.274973", + # "quantity": "0.09425304", + # "amount": "179.48370520116792" + # } + # ], + # "updateId": "78", + # "timeStart": "2021-01-28T09:19:30.706Z", + # "makerFee": 200, + # "takerFee": 200, + # "quoteVolume24": 49125.1374009045, + # "lowPrice24": 1966.704999, + # "highPrice24": 2080.354997, + # "baseCurrency": { + # "id": 2, + # "status": 1, + # "symbol": "eth", + # "title": "Ethereum", + # "logoURL": "https://ethereum.org/static/6b935ac0e6194247347855dc3d328e83/34ca5/eth-diamond-black.png", + # "isDiscount": False, + # "address": "https://ethereum.org/", + # "description": "Ethereum ETH", + # "decimals": 18, + # "blockChain": "Ethereum", + # "precision": 8, + # "currentRate": null, + # "active": True, + # "timeStart": "2021-01-28T08:57:41.719Z", + # "txLimits": { + # "minDeposit": "100000000000000", + # "maxWithdraw": "10000000000000000000000", + # "minWithdraw": "20000000000000000", + # "withdrawCommissionFixed": "5000000000000000", + # "withdrawCommissionPercentage": "NaN" + # }, + # "type": "crypto", + # "typeNetwork": "internalGW", + # "icon": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzAiIGhlaWdodD0iMzAiIHZpZXdCb3g9IjAgMCAzMCAzMCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTAgMTVDMCA2LjcxNTczIDYuNzE1NzMgMCAxNSAwVjBDMjMuMjg0MyAwIDMwIDYuNzE1NzMgMzAgMTVWMTVDMzAgMjMuMjg0MyAyMy4yODQzIDMwIDE1IDMwVjMwQzYuNzE1NzMgMzAgMCAyMy4yODQzIDAgMTVWMTVaIiBmaWxsPSJibGFjayIvPgo8cGF0aCBkPSJNMTQuOTU1NyAxOS45NzM5TDkgMTYuMzUwOUwxNC45NTIxIDI1TDIwLjkxMDkgMTYuMzUwOUwxNC45NTIxIDE5Ljk3MzlIMTQuOTU1N1pNMTUuMDQ0MyA1TDkuMDkwOTUgMTUuMTg1M0wxNS4wNDQzIDE4LjgxNDZMMjEgMTUuMTg5MUwxNS4wNDQzIDVaIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K", + # "idSorting": 2, + # "links": [ + # { + # "tx": "https://etherscan.io/tx/", + # "address": "https://etherscan.io/address/", + # "blockChain": "Ethereum" + # } + # ], + # "clientTxLimits": { + # "minDeposit": "0.0001", + # "minWithdraw": "0.02", + # "maxWithdraw": "10000", + # "withdrawCommissionPercentage": "NaN", + # "withdrawCommissionFixed": "0.005" + # } + # }, + # "quoteCurrency": { + # "id": 3, + # "status": 1, + # "symbol": "usdt", + # "title": "Tether USD", + # "logoURL": "https://cryptologos.cc/logos/tether-usdt-logo.png?v=010", + # "isDiscount": False, + # "address": "https://tether.to/", + # "description": "Tether USD", + # "decimals": 6, + # "blockChain": "", + # "precision": 6, + # "currentRate": null, + # "active": True, + # "timeStart": "2021-01-28T09:04:17.170Z", + # "txLimits": { + # "minDeposit": "1000", + # "maxWithdraw": "100000000000", + # "minWithdraw": "1000000", + # "withdrawCommissionFixed": { + # "Tron": "2000000", + # "Binance": "2000000000000000000", + # "Ethereum": "20000000" + # }, + # "withdrawCommissionPercentage": "NaN" + # }, + # "type": "crypto", + # "typeNetwork": "internalGW", + # "icon": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzAiIGhlaWdodD0iMzAiIHZpZXdCb3g9IjAgMCAzMCAzMCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTAgMTVDMCA2LjcxNTczIDYuNzE1NzMgMCAxNSAwVjBDMjMuMjg0MyAwIDMwIDYuNzE1NzMgMzAgMTVWMTVDMzAgMjMuMjg0MyAyMy4yODQzIDMwIDE1IDMwVjMwQzYuNzE1NzMgMzAgMCAyMy4yODQzIDAgMTVWMTVaIiBmaWxsPSIjNkZBNjg4Ii8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMjMgN0g3VjExSDEzVjEyLjA2MkM4Ljk5MjAyIDEyLjMxNDYgNiAxMy4zMTAyIDYgMTQuNUM2IDE1LjY4OTggOC45OTIwMiAxNi42ODU0IDEzIDE2LjkzOFYyM0gxN1YxNi45MzhDMjEuMDA4IDE2LjY4NTQgMjQgMTUuNjg5OCAyNCAxNC41QzI0IDEzLjMxMDIgMjEuMDA4IDEyLjMxNDYgMTcgMTIuMDYyVjExSDIzVjdaTTcuNSAxNC41QzcuNSAxMy40NjA2IDkuMzMzMzMgMTIuMzY4IDEzIDEyLjA3NTZWMTUuNUgxN1YxMi4wNzU5QzIwLjkzODQgMTIuMzkyNyAyMi41IDEzLjYzMzkgMjIuNSAxNC41QzIyLjUgMTUuMzIyIDIwLjAwMDggMTUuODA2MSAxNyAxNS45NTI1QzE1LjcwODIgMTYuMDQ2MiAxMy43OTUxIDE1Ljk4MjYgMTMgMTUuOTM5MUM5Ljk5OTIxIDE1Ljc1NTkgNy41IDE1LjE4MDkgNy41IDE0LjVaIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K", + # "idSorting": 0, + # "links": [ + # { + # "tx": "https://etherscan.io/tx/", + # "address": "https://etherscan.io/address/", + # "blockChain": "Ethereum" + # }, + # { + # "tx": "https://tronscan.org/#/transaction/", + # "address": "https://tronscan.org/#/address/", + # "blockChain": "Tron" + # }, + # { + # "tx": "https://bscscan.com/tx/", + # "address": "https://bscscan.com/address/", + # "blockChain": "Binance" + # } + # ], + # "clientTxLimits": { + # "minDeposit": "0.001", + # "minWithdraw": "1", + # "maxWithdraw": "100000", + # "withdrawCommissionPercentage": "NaN", + # "withdrawCommissionFixed": { + # "Tron": "2", + # "Binance": "2", + # "Ethereum": "20" + # } + # } + # }, + # "quantities": { + # "asks": "5.58760757", + # "bids": "2226.98663823032198" + # } + # } + # } + # } + # + result = self.safe_value(response, 'result', {}) + pair = self.safe_dict(result, 'pair', {}) + return self.parse_ticker(pair, market) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # fetchTicker + # { + # "id": 2, + # "name": "eth_usdt", + # "baseAssetId": 2, + # "quoteAssetId": 3, + # "fullName": "ETH USDT", + # "description": "ETH USDT", + # "lastBuy": "1976.715012", + # "lastSell": "1971.995006", + # "lastPrice": "1976.715012", + # "change24": "1.02", + # "volume24": 24.0796457, + # "volume24USD": 44282.347995912205, + # "active": True, + # "baseStep": 8, + # "quoteStep": 6, + # "status": 1, + # "asks": [ + # { + # "price": "1976.405003", + # "quantity": "0.0051171", + # "amount": "10.1134620408513" + # }, + # { + # "price": "1976.405013", + # "quantity": "0.09001559", + # "amount": "177.90726332415267" + # }, + # { + # "price": "2010.704988", + # "quantity": "0.00127892", + # "amount": "2.57153082325296" + # } + # ... + # ], + # "bids": [ + # { + # "price": "1976.404988", + # "quantity": "0.09875861", + # "amount": "195.18700941194668" + # }, + # { + # "price": "1905.472973", + # "quantity": "0.00263591", + # "amount": "5.02265526426043" + # }, + # { + # "price": "1904.274973", + # "quantity": "0.09425304", + # "amount": "179.48370520116792" + # } + # ... + # ], + # "updateId": "78", + # "timeStart": "2021-01-28T09:19:30.706Z", + # "makerFee": 200, + # "takerFee": 200, + # "quoteVolume24": 49125.1374009045, + # "lowPrice24": 1966.704999, + # "highPrice24": 2080.354997, + # ... + # } + # + # fetchTickers + # { + # "trading_pairs": "BTC_USDT", + # "base_currency": "BTC", + # "quote_currency": "USDT", + # "last_price": 37669.955001, + # "lowest_ask": 37670.055, + # "highest_bid": 37669.955, + # "base_volume": 6.81156888, + # "quote_volume": 257400.516878529, + # "price_change_percent_24h": -0.29, + # "highest_price_24h": 38389.994463, + # "lowest_price_24h": 37574.894999 + # } + marketId = self.safe_string_lower(ticker, 'trading_pairs') + market = self.safe_market(marketId, market) + bestBidPrice = None + bestAskPrice = None + bestBidVolume = None + bestAskVolume = None + bids = self.safe_value(ticker, 'bids') + asks = self.safe_value(ticker, 'asks') + if (bids is not None) and (isinstance(bids, list)) and (asks is not None) and (isinstance(asks, list)): + bestBid = self.safe_value(bids, 0, {}) + bestBidPrice = self.safe_string(bestBid, 'price') + bestBidVolume = self.safe_string(bestBid, 'quantity') + bestAsk = self.safe_value(asks, 0, {}) + bestAskPrice = self.safe_string(bestAsk, 'price') + bestAskVolume = self.safe_string(bestAsk, 'quantity') + else: + bestBidPrice = self.safe_string(ticker, 'highest_bid') + bestAskPrice = self.safe_string(ticker, 'lowest_ask') + baseVolume = self.safe_string_2(ticker, 'volume24', 'base_volume') + quoteVolume = self.safe_string_2(ticker, 'quoteVolume24', 'quote_volume') + high = self.safe_string_2(ticker, 'highPrice24', 'highest_price_24h') + low = self.safe_string_2(ticker, 'lowPrice24', 'lowest_price_24h') + close = self.safe_string_2(ticker, 'lastPrice', 'last_price') + changePcnt = self.safe_string_2(ticker, 'change24', 'price_change_percent_24h') + return self.safe_ticker({ + 'symbol': market['symbol'], + 'timestamp': None, + 'datetime': None, + 'open': None, + 'high': high, + 'low': low, + 'close': close, + 'bid': bestBidPrice, + 'bidVolume': bestBidVolume, + 'ask': bestAskPrice, + 'askVolume': bestAskVolume, + 'vwap': None, + 'previousClose': None, + 'change': None, + 'percentage': changePcnt, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'info': ticker, + }, market) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://bit.team/trade/api/documentation#/CMC/getTradeApiCmcTradesPair + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the bitteam api endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + response = self.publicGetTradeApiCmcTradesPair(self.extend(request, params)) + # + # [ + # { + # "trade_id": 34970337, + # "price": 37769.994793, + # "base_volume": 0.00119062, + # "quote_volume": 44.96971120044166, + # "timestamp": 1700827234000, + # "type": "buy" + # }, + # { + # "trade_id": 34970347, + # "price": 37769.634497, + # "base_volume": 0.00104009, + # "quote_volume": 39.28381914398473, + # "timestamp": 1700827248000, + # "type": "buy" + # }, + # ... + # ] + # + return self.parse_trades(response, market, since, limit) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://bit.team/trade/api/documentation#/PRIVATE/getTradeApiCcxtTradesofuser + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve(default 10) + :param dict [params]: extra parameters specific to the bitteam api endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['pairId'] = market['numericId'] + if limit is not None: + request['limit'] = limit + response = self.privateGetTradeApiCcxtTradesOfUser(self.extend(request, params)) + # + # { + # "ok": True, + # "result": { + # "count": 3, + # "trades": [ + # { + # "id": 34880724, + # "tradeId": "4368041", + # "makerOrderId": 106742914, + # "takerOrderId": 106761614, + # "pairId": 2, + # "quantity": "0.00955449", + # "price": "1993.674994", + # "isBuyerMaker": True, + # "baseDecimals": 18, + # "quoteDecimals": 6, + # "side": "sell", + # "timestamp": 1700615250, + # "rewarded": True, + # "makerUserId": 21639, + # "takerUserId": 15913, + # "baseCurrencyId": 2, + # "quoteCurrencyId": 3, + # "feeMaker": { + # "amount": "0.0000191", + # "symbol": "eth", + # "userId": 21639, + # "decimals": 18, + # "symbolId": 2 + # }, + # "feeTaker": { + # "amount": "0", + # "symbol": "usdt", + # "userId": 15913, + # "decimals": 6, + # "symbolId": 3, + # "discountAmount": "0", + # "discountSymbol": "btt", + # "discountDecimals": 18, + # "discountSymbolId": 5 + # }, + # "pair": "eth_usdt", + # "createdAt": "2023-11-22T01:07:30.593Z", + # "updatedAt": "2023-11-22T01:10:00.117Z", + # "isCurrentSide": "maker" + # }, + # { + # "id": 34875793, + # "tradeId": "4368010", + # "makerOrderId": 106742914, + # "takerOrderId": 106745926, + # "pairId": 2, + # "quantity": "0.0027193", + # "price": "1993.674994", + # "isBuyerMaker": True, + # "baseDecimals": 18, + # "quoteDecimals": 6, + # "side": "sell", + # "timestamp": 1700602983, + # "rewarded": True, + # "makerUserId": 21639, + # "takerUserId": 15912, + # "baseCurrencyId": 2, + # "quoteCurrencyId": 3, + # "feeMaker": { + # "amount": "0.00000543", + # "symbol": "eth", + # "userId": 21639, + # "decimals": 18, + # "symbolId": 2 + # }, + # "feeTaker": { + # "amount": "0", + # "symbol": "usdt", + # "userId": 15912, + # "decimals": 6, + # "symbolId": 3, + # "discountAmount": "0", + # "discountSymbol": "btt", + # "discountDecimals": 18, + # "discountSymbolId": 5 + # }, + # "pair": "eth_usdt", + # "createdAt": "2023-11-21T21:43:02.758Z", + # "updatedAt": "2023-11-21T21:45:00.147Z", + # "isCurrentSide": "maker" + # }, + # { + # "id": 34871727, + # "tradeId": "3441840", + # "makerOrderId": 106733299, + # "takerOrderId": 106733308, + # "pairId": 22, + # "quantity": "0.00001", + # "price": "37017.495008", + # "isBuyerMaker": False, + # "baseDecimals": 8, + # "quoteDecimals": 6, + # "side": "buy", + # "timestamp": 1700594960, + # "rewarded": True, + # "makerUserId": 15909, + # "takerUserId": 21639, + # "baseCurrencyId": 11, + # "quoteCurrencyId": 3, + # "feeMaker": { + # "amount": "0", + # "symbol": "usdt", + # "userId": 15909, + # "decimals": 6, + # "symbolId": 3, + # "discountAmount": "0", + # "discountSymbol": "btt", + # "discountDecimals": 18, + # "discountSymbolId": 5 + # }, + # "feeTaker": { + # "amount": "0.00000002", + # "symbol": "btc", + # "userId": 21639, + # "decimals": 8, + # "symbolId": 11 + # }, + # "pair": "btc_usdt", + # "createdAt": "2023-11-21T19:29:20.092Z", + # "updatedAt": "2023-11-21T19:30:00.159Z" + # "isCurrentSide": "taker" + # } + # ] + # } + # } + # + result = self.safe_value(response, 'result', {}) + trades = self.safe_list(result, 'trades', []) + return self.parse_trades(trades, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades + # { + # "trade_id": 34970337, + # "price": 37769.994793, + # "base_volume": 0.00119062, + # "quote_volume": 44.96971120044166, + # "timestamp": 1700827234000, + # "type": "buy" + # }, + # + # fetchMyTrades + # { + # "id": 34875793, + # "tradeId": "4368010", + # "makerOrderId": 106742914, + # "takerOrderId": 106745926, + # "pairId": 2, + # "quantity": "0.0027193", + # "price": "1993.674994", + # "isBuyerMaker": True, + # "baseDecimals": 18, + # "quoteDecimals": 6, + # "side": "sell", + # "timestamp": 1700602983, + # "rewarded": True, + # "makerUserId": 21639, + # "takerUserId": 15912, + # "baseCurrencyId": 2, + # "quoteCurrencyId": 3, + # "feeMaker": { + # "amount": "0.00000543", + # "symbol": "eth", + # "userId": 21639, + # "decimals": 18, + # "symbolId": 2 + # }, + # "feeTaker": { + # "amount": "0", + # "symbol": "usdt", + # "userId": 15912, + # "decimals": 6, + # "symbolId": 3, + # "discountAmount": "0", + # "discountSymbol": "btt", + # "discountDecimals": 18, + # "discountSymbolId": 5 + # }, + # "pair": "eth_usdt", + # "createdAt": "2023-11-21T21:43:02.758Z", + # "updatedAt": "2023-11-21T21:45:00.147Z", + # "isCurrentSide": "maker" + # } + # + marketId = self.safe_string(trade, 'pair') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + id = self.safe_string_2(trade, 'id', 'trade_id') + price = self.safe_string(trade, 'price') + amount = self.safe_string_2(trade, 'quantity', 'base_volume') + cost = self.safe_string(trade, 'quote_volume') + takerOrMaker = self.safe_string(trade, 'isCurrentSide') + timestamp = self.safe_string(trade, 'timestamp') + if takerOrMaker is not None: + timestamp = Precise.string_mul(timestamp, '1000') + # the exchange returns the side of the taker + side = self.safe_string_2(trade, 'side', 'type') + feeInfo = None + order = None + if takerOrMaker == 'maker': + if side == 'sell': + side = 'buy' + elif side == 'buy': + side = 'sell' + order = self.safe_string(trade, 'makerOrderId') + feeInfo = self.safe_value(trade, 'feeMaker', {}) + elif takerOrMaker == 'taker': + order = self.safe_string(trade, 'takerOrderId') + feeInfo = self.safe_value(trade, 'feeTaker', {}) + feeCurrencyId = self.safe_string(feeInfo, 'symbol') + feeCost = self.safe_string(feeInfo, 'amount') + fee = { + 'currency': self.safe_currency_code(feeCurrencyId), + 'cost': feeCost, + } + intTs = self.parse_to_int(timestamp) + return self.safe_trade({ + 'id': id, + 'order': order, + 'timestamp': intTs, + 'datetime': self.iso8601(intTs), + 'symbol': symbol, + 'type': None, + 'side': side, + 'takerOrMaker': takerOrMaker, + 'price': price, + 'amount': amount, + 'cost': cost, + 'fee': fee, + 'info': trade, + }, market) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://bit.team/trade/api/documentation#/PRIVATE/getTradeApiCcxtBalance + + :param dict [params]: extra parameters specific to the betteam api endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + response = self.privateGetTradeApiCcxtBalance(params) + return self.parse_balance(response) + + def parse_balance(self, response) -> Balances: + # + # { + # "ok": True, + # "result": { + # "free": { + # "USDT": "0", + # "DEL": "0", + # "BTC": "0", + # ... + # }, + # "used": { + # "USDT": "0", + # "DEL": "0", + # "BTC": "0", + # ... + # }, + # "total": { + # "USDT": "0", + # "DEL": "0", + # "BTC": "0", + # ... + # }, + # "USDT": { + # "free": "0", + # "used": "0", + # "total": "0", + # }, + # "DEL": { + # "free": "0", + # "used": "0", + # "total": "0", + # }, + # "BTC": { + # "free": "0", + # "used": "0", + # "total": "0", + # } + # ... + # } + # } + # + timestamp = self.milliseconds() + balance: dict = { + 'info': response, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + result = self.safe_value(response, 'result', {}) + balanceByCurrencies = self.omit(result, ['free', 'used', 'total']) + rawCurrencyIds = list(balanceByCurrencies.keys()) + for i in range(0, len(rawCurrencyIds)): + rawCurrencyId = rawCurrencyIds[i] + currencyBalance = self.safe_value(result, rawCurrencyId) + free = self.safe_string(currencyBalance, 'free') + used = self.safe_string(currencyBalance, 'used') + total = self.safe_string(currencyBalance, 'total') + currencyCode = self.safe_currency_code(rawCurrencyId.lower()) + balance[currencyCode] = { + 'free': free, + 'used': used, + 'total': total, + } + return self.safe_balance(balance) + + def fetch_deposits_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch history of deposits and withdrawals from external wallets and between CoinList Pro trading account and CoinList wallet + + https://bit.team/trade/api/documentation#/PRIVATE/getTradeApiTransactionsofuser + + :param str [code]: unified currency code for the currency of the deposit/withdrawals + :param int [since]: timestamp in ms of the earliest deposit/withdrawal + :param int [limit]: max number of deposit/withdrawals to return(default 10) + :param dict [params]: extra parameters specific to the bitteam api endpoint + :returns dict: a list of `transaction structure ` + """ + self.load_markets() + currency = None + request: dict = {} + if code is not None: + currency = self.currency(code) + request['currency'] = currency['numericId'] + if limit is not None: + request['limit'] = limit + response = self.privateGetTradeApiTransactionsOfUser(self.extend(request, params)) + # + # { + # "ok": True, + # "result": { + # "count": 2, + # "transactions": [ + # { + # "id": 1329686, + # "orderId": "2f060ad5-30f7-4f2b-ac5f-1bb8f5fd34dc", + # "transactionCoreId": "561863", + # "userId": 21639, + # "recipient": "0x9050dfA063D1bE7cA711c750b18D51fDD13e90Ee", + # "sender": "0x6894a93B6fea044584649278621723cac51443Cd", + # "symbolId": 2, + # "CommissionId": 17571, + # "amount": "44000000000000000", + # "params": {}, + # "reason": null, + # "timestamp": 1700715341743, + # "status": "approving", + # "statusDescription": null, + # "type": "withdraw", + # "message": null, + # "blockChain": "", + # "before": null, + # "after": null, + # "currency": { + # "symbol": "eth", + # "decimals": 18, + # "blockChain": "Ethereum", + # "links": [ + # { + # "tx": "https://etherscan.io/tx/", + # "address": "https://etherscan.io/address/", + # "blockChain": "Ethereum" + # } + # ] + # } + # }, + # { + # "id": 1329229, + # "orderId": null, + # "transactionCoreId": "561418", + # "userId": 21639, + # "recipient": "0x7d6a797f2406e06b2f9b41d067df324affa315dd", + # "sender": null, + # "symbolId": 3, + # "CommissionId": null, + # "amount": "100000000", + # "params": { + # "tx_id": "0x2253823c828d838acd983fe6a348fb0e034efe3874b081871d8b80da76ec758b" + # }, + # "reason": null, + # "timestamp": 1700594180417, + # "status": "success", + # "statusDescription": null, + # "type": "deposit", + # "message": null, + # "blockChain": "Ethereum", + # "before": 0, + # "after": 100000000, + # "currency": { + # "symbol": "usdt", + # "decimals": 6, + # "blockChain": "", + # "links": [ + # { + # "tx": "https://etherscan.io/tx/", + # "address": "https://etherscan.io/address/", + # "blockChain": "Ethereum" + # }, + # { + # "tx": "https://tronscan.org/#/transaction/", + # "address": "https://tronscan.org/#/address/", + # "blockChain": "Tron" + # }, + # { + # "tx": "https://bscscan.com/tx/", + # "address": "https://bscscan.com/address/", + # "blockChain": "Binance" + # } + # ] + # } + # } + # ] + # } + # } + # + result = self.safe_value(response, 'result', {}) + transactions = self.safe_list(result, 'transactions', []) + return self.parse_transactions(transactions, currency, since, limit) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # { + # "id": 1329229, + # "orderId": null, + # "transactionCoreId": "561418", + # "userId": 21639, + # "recipient": "0x7d6a797f2406e06b2f9b41d067df324affa315dd", + # "sender": null, + # "symbolId": 3, + # "CommissionId": null, + # "amount": "100000000", + # "params": { + # "tx_id": "0x2253823c828d838acd983fe6a348fb0e034efe3874b081871d8b80da76ec758b" + # }, + # "reason": null, + # "timestamp": 1700594180417, + # "status": "success", + # "statusDescription": null, + # "type": "deposit", + # "message": null, + # "blockChain": "Ethereum", + # "before": 0, + # "after": 100000000, + # "currency": { + # "symbol": "usdt", + # "decimals": 6, + # "blockChain": "", + # "links": [ + # { + # "tx": "https://etherscan.io/tx/", + # "address": "https://etherscan.io/address/", + # "blockChain": "Ethereum" + # }, + # { + # "tx": "https://tronscan.org/#/transaction/", + # "address": "https://tronscan.org/#/address/", + # "blockChain": "Tron" + # }, + # { + # "tx": "https://bscscan.com/tx/", + # "address": "https://bscscan.com/address/", + # "blockChain": "Binance" + # } + # ] + # } + # } + # + currencyObject = self.safe_value(transaction, 'currency') + currencyId = self.safe_string(currencyObject, 'symbol') + code = self.safe_currency_code(currencyId, currency) + id = self.safe_string(transaction, 'id') + params = self.safe_value(transaction, 'params') + txid = self.safe_string(params, 'tx_id') + timestamp = self.safe_integer(transaction, 'timestamp') + networkId = self.safe_string(transaction, 'blockChain') + if networkId is None: + links = self.safe_value(currencyObject, 'links', []) + blockChain = self.safe_value(links, 0, {}) + networkId = self.safe_string(blockChain, 'blockChain') + addressFrom = self.safe_string(transaction, 'sender') + addressTo = self.safe_string(transaction, 'recipient') + tag = self.safe_string(transaction, 'message') + type = self.parse_transaction_type(self.safe_string(transaction, 'type')) + amount = self.parse_value_to_pricision(transaction, 'amount', currencyObject, 'decimals') + status = self.parse_transaction_status(self.safe_value(transaction, 'status')) + return { + 'info': transaction, + 'id': id, + 'txid': txid, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': self.network_id_to_code(networkId), + 'addressFrom': addressFrom, + 'address': None, + 'addressTo': addressTo, + 'tagFrom': None, + 'tag': tag, + 'tagTo': None, + 'type': type, + 'amount': self.parse_number(amount), + 'currency': code, + 'status': status, + 'updated': None, + 'fee': None, + 'comment': self.safe_string(transaction, 'description'), + 'internal': False, + } + + def parse_transaction_type(self, type): + types: dict = { + 'deposit': 'deposit', + 'withdraw': 'withdrawal', + } + return self.safe_string(types, type, type) + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'approving': 'pending', + 'success': 'ok', + } + return self.safe_string(statuses, status, status) + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + request = self.omit(params, self.extract_params(path)) + endpoint = '/' + self.implode_params(path, params) + url = self.urls['api'][api] + endpoint + query = self.urlencode(request) + if api == 'private': + self.check_required_credentials() + if method == 'POST': + body = self.json(request) + elif len(query) != 0: + url += '?' + query + auth = self.apiKey + ':' + self.secret + auth64 = self.string_to_base64(auth) + signature = 'Basic ' + auth64 + headers = { + 'Authorization': signature, + 'Content-Type': 'application/json', + } + elif len(query) != 0: + url += '?' + query + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None + if code != 200: + if code == 404: + if (url.find('/ccxt/order/') >= 0) and (method == 'GET'): + parts = url.split('/order/') + orderId = self.safe_string(parts, 1) + raise OrderNotFound(self.id + ' order ' + orderId + ' not found') + if url.find('/cmc/orderbook/') >= 0: + parts = url.split('/cmc/orderbook/') + symbolId = self.safe_string(parts, 1) + raise BadSymbol(self.id + ' symbolId ' + symbolId + ' not found') + feedback = self.id + ' ' + body + message = self.safe_string(response, 'message') + responseCode = self.safe_string(response, 'code') + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], responseCode, feedback) + raise ExchangeError(feedback) + return None diff --git a/ccxt/bittrade.py b/ccxt/bittrade.py new file mode 100644 index 0000000..1eae04e --- /dev/null +++ b/ccxt/bittrade.py @@ -0,0 +1,1929 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.bittrade import ImplicitAPI +import hashlib +from ccxt.base.types import Account, Any, Balances, Currencies, Currency, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import NotSupported +from ccxt.base.errors import NetworkError +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.errors import OnMaintenance +from ccxt.base.errors import RequestTimeout +from ccxt.base.decimal_to_precision import TRUNCATE +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class bittrade(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(bittrade, self).describe(), { + 'id': 'bittrade', + 'name': 'BitTrade', + 'countries': ['JP'], + 'rateLimit': 100, + 'userAgent': self.userAgents['chrome39'], + 'certified': False, + 'version': 'v1', + 'hostname': 'api-cloud.bittrade.co.jp', + 'pro': True, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': None, + 'swap': False, + 'future': False, + 'option': False, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'cancelOrders': True, + 'createMarketBuyOrderWithCost': True, + 'createMarketOrderWithCost': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createStopLimitOrder': False, + 'createStopMarketOrder': False, + 'createStopOrder': False, + 'fetchAccounts': True, + 'fetchBalance': True, + 'fetchClosedOrders': True, + 'fetchCurrencies': True, + 'fetchDepositAddress': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchFundingHistory': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchIndexOHLCV': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchOrderTrades': True, + 'fetchPremiumIndexOHLCV': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingLimits': True, + 'fetchWithdrawals': True, + 'withdraw': True, + }, + 'timeframes': { + '1m': '1min', + '5m': '5min', + '15m': '15min', + '30m': '30min', + '1h': '60min', + '4h': '4hour', + '1d': '1day', + '1w': '1week', + '1M': '1mon', + '1y': '1year', + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/85734211-85755480-b705-11ea-8b35-0b7f1db33a2f.jpg', + 'api': { + 'market': 'https://{hostname}', + 'public': 'https://{hostname}', + 'private': 'https://{hostname}', + 'v2Public': 'https://{hostname}', + 'v2Private': 'https://{hostname}', + }, + 'www': 'https://www.bittrade.co.jp', + 'referral': 'https://www.bittrade.co.jp/register/?invite_code=znnq3', + 'doc': 'https://api-doc.bittrade.co.jp', + 'fees': 'https://www.bittrade.co.jp/ja-jp/support/fee', + }, + 'api': { + 'v2Public': { + 'get': { + 'reference/currencies': 1, # 币链参考信息 + 'market-status': 1, # 获取当前市场状态 + }, + }, + 'v2Private': { + 'get': { + 'account/ledger': 1, + 'account/withdraw/quota': 1, + 'account/withdraw/address': 1, # 提币地址查询(限母用户可用) + 'account/deposit/address': 1, + 'account/repayment': 5, # 还币交易记录查询 + 'reference/transact-fee-rate': 1, + 'account/asset-valuation': 0.2, # 获取账户资产估值 + 'point/account': 5, # 点卡余额查询 + 'sub-user/user-list': 1, # 获取子用户列表 + 'sub-user/user-state': 1, # 获取特定子用户的用户状态 + 'sub-user/account-list': 1, # 获取特定子用户的账户列表 + 'sub-user/deposit-address': 1, # 子用户充币地址查询 + 'sub-user/query-deposit': 1, # 子用户充币记录查询 + 'user/api-key': 1, # 母子用户API key信息查询 + 'user/uid': 1, # 母子用户获取用户UID + 'algo-orders/opening': 1, # 查询未触发OPEN策略委托 + 'algo-orders/history': 1, # 查询策略委托历史 + 'algo-orders/specific': 1, # 查询特定策略委托 + 'c2c/offers': 1, # 查询借入借出订单 + 'c2c/offer': 1, # 查询特定借入借出订单及其交易记录 + 'c2c/transactions': 1, # 查询借入借出交易记录 + 'c2c/repayment': 1, # 查询还币交易记录 + 'c2c/account': 1, # 查询账户余额 + 'etp/reference': 1, # 基础参考信息 + 'etp/transactions': 5, # 获取杠杆ETP申赎记录 + 'etp/transaction': 5, # 获取特定杠杆ETP申赎记录 + 'etp/rebalance': 1, # 获取杠杆ETP调仓记录 + 'etp/limit': 1, # 获取ETP持仓限额 + }, + 'post': { + 'account/transfer': 1, + 'account/repayment': 5, # 归还借币(全仓逐仓通用) + 'point/transfer': 5, # 点卡划转 + 'sub-user/management': 1, # 冻结/解冻子用户 + 'sub-user/creation': 1, # 子用户创建 + 'sub-user/tradable-market': 1, # 设置子用户交易权限 + 'sub-user/transferability': 1, # 设置子用户资产转出权限 + 'sub-user/api-key-generation': 1, # 子用户API key创建 + 'sub-user/api-key-modification': 1, # 修改子用户API key + 'sub-user/api-key-deletion': 1, # 删除子用户API key + 'sub-user/deduct-mode': 1, # 设置子用户手续费抵扣模式 + 'algo-orders': 1, # 策略委托下单 + 'algo-orders/cancel-all-after': 1, # 自动撤销订单 + 'algo-orders/cancellation': 1, # 策略委托(触发前)撤单 + 'c2c/offer': 1, # 借入借出下单 + 'c2c/cancellation': 1, # 借入借出撤单 + 'c2c/cancel-all': 1, # 撤销所有借入借出订单 + 'c2c/repayment': 1, # 还币 + 'c2c/transfer': 1, # 资产划转 + 'etp/creation': 5, # 杠杆ETP换入 + 'etp/redemption': 5, # 杠杆ETP换出 + 'etp/{transactId}/cancel': 10, # 杠杆ETP单个撤单 + 'etp/batch-cancel': 50, # 杠杆ETP批量撤单 + }, + }, + 'market': { + 'get': { + 'history/kline': 1, # 获取K线数据 + 'detail/merged': 1, # 获取聚合行情(Ticker) + 'depth': 1, # 获取 Market Depth 数据 + 'trade': 1, # 获取 Trade Detail 数据 + 'history/trade': 1, # 批量获取最近的交易记录 + 'detail': 1, # 获取 Market Detail 24小时成交量数据 + 'tickers': 1, + 'etp': 1, # 获取杠杆ETP实时净值 + }, + }, + 'public': { + 'get': { + 'common/symbols': 1, # 查询系统支持的所有交易对 + 'common/currencys': 1, # 查询系统支持的所有币种 + 'common/timestamp': 1, # 查询系统当前时间 + 'common/exchange': 1, # order limits + 'settings/currencys': 1, # ?language=en-US + }, + }, + 'private': { + 'get': { + 'account/accounts': 0.2, # 查询当前用户的所有账户(即account-id) + 'account/accounts/{id}/balance': 0.2, # 查询指定账户的余额 + 'account/accounts/{sub-uid}': 1, + 'account/history': 4, + 'cross-margin/loan-info': 1, + 'margin/loan-info': 1, # 查询借币币息率及额度 + 'fee/fee-rate/get': 1, + 'order/openOrders': 0.4, + 'order/orders': 0.4, + 'order/orders/{id}': 0.4, # 查询某个订单详情 + 'order/orders/{id}/matchresults': 0.4, # 查询某个订单的成交明细 + 'order/orders/getClientOrder': 0.4, + 'order/history': 1, # 查询当前委托、历史委托 + 'order/matchresults': 1, # 查询当前成交、历史成交 + # 'dw/withdraw-virtual/addresses', # 查询虚拟币提现地址(Deprecated) + 'query/deposit-withdraw': 1, + # 'margin/loan-info', # duplicate + 'margin/loan-orders': 0.2, # 借贷订单 + 'margin/accounts/balance': 0.2, # 借贷账户详情 + 'cross-margin/loan-orders': 1, # 查询借币订单 + 'cross-margin/accounts/balance': 1, # 借币账户详情 + 'points/actions': 1, + 'points/orders': 1, + 'subuser/aggregate-balance': 10, + 'stable-coin/exchange_rate': 1, + 'stable-coin/quote': 1, + }, + 'post': { + 'account/transfer': 1, # 资产划转(该节点为母用户和子用户进行资产划转的通用接口。) + 'futures/transfer': 1, + 'order/batch-orders': 0.4, + 'order/orders/place': 0.2, # 创建并执行一个新订单(一步下单, 推荐使用) + 'order/orders/submitCancelClientOrder': 0.2, + 'order/orders/batchCancelOpenOrders': 0.4, + # 'order/orders', # 创建一个新的订单请求 (仅创建订单,不执行下单) + # 'order/orders/{id}/place', # 执行一个订单 (仅执行已创建的订单) + 'order/orders/{id}/submitcancel': 0.2, # 申请撤销一个订单请求 + 'order/orders/batchcancel': 0.4, # 批量撤销订单 + # 'dw/balance/transfer', # 资产划转 + 'dw/withdraw/api/create': 1, # 申请提现虚拟币 + # 'dw/withdraw-virtual/create', # 申请提现虚拟币 + # 'dw/withdraw-virtual/{id}/place', # 确认申请虚拟币提现(Deprecated) + 'dw/withdraw-virtual/{id}/cancel': 1, # 申请取消提现虚拟币 + 'dw/transfer-in/margin': 10, # 现货账户划入至借贷账户 + 'dw/transfer-out/margin': 10, # 借贷账户划出至现货账户 + 'margin/orders': 10, # 申请借贷 + 'margin/orders/{id}/repay': 10, # 归还借贷 + 'cross-margin/transfer-in': 1, # 资产划转 + 'cross-margin/transfer-out': 1, # 资产划转 + 'cross-margin/orders': 1, # 申请借币 + 'cross-margin/orders/{id}/repay': 1, # 归还借币 + 'stable-coin/exchange': 1, + 'subuser/transfer': 10, + }, + }, + }, + 'fees': { + 'trading': { + 'feeSide': 'get', + 'tierBased': False, + 'percentage': True, + 'maker': self.parse_number('0.002'), + 'taker': self.parse_number('0.002'), + }, + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, # todo: implement + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': False, + 'FOK': False, + 'PO': False, + 'GTD': False, + }, + 'hedged': False, + 'selfTradePrevention': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 120, + 'untilDays': 2, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, # todo + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': None, + 'daysBack': None, # todo + 'untilDays': None, # todo + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': None, + 'daysBack': None, # todo + 'daysBackCanceled': None, # todo + 'untilDays': None, # todo + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 2000, + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'broad': { + 'contract is restricted of closing positions on API. Please contact customer service': OnMaintenance, + 'maintain': OnMaintenance, + }, + 'exact': { + # err-code + 'bad-request': BadRequest, + 'base-date-limit-error': BadRequest, # {"status":"error","err-code":"base-date-limit-error","err-msg":"date less than system limit","data":null} + 'api-not-support-temp-addr': PermissionDenied, # {"status":"error","err-code":"api-not-support-temp-addr","err-msg":"API withdrawal does not support temporary addresses","data":null} + 'timeout': RequestTimeout, # {"ts":1571653730865,"status":"error","err-code":"timeout","err-msg":"Request Timeout"} + 'gateway-internal-error': ExchangeNotAvailable, # {"status":"error","err-code":"gateway-internal-error","err-msg":"Failed to load data. Try again later.","data":null} + 'account-frozen-balance-insufficient-error': InsufficientFunds, # {"status":"error","err-code":"account-frozen-balance-insufficient-error","err-msg":"trade account balance is not enough, left: `0.0027`","data":null} + 'invalid-amount': InvalidOrder, # eg "Paramemter `amount` is invalid." + 'order-limitorder-amount-min-error': InvalidOrder, # limit order amount error, min: `0.001` + 'order-limitorder-amount-max-error': InvalidOrder, # market order amount error, max: `1000000` + 'order-marketorder-amount-min-error': InvalidOrder, # market order amount error, min: `0.01` + 'order-limitorder-price-min-error': InvalidOrder, # limit order price error + 'order-limitorder-price-max-error': InvalidOrder, # limit order price error + 'order-holding-limit-failed': InvalidOrder, # {"status":"error","err-code":"order-holding-limit-failed","err-msg":"Order failed, exceeded the holding limit of self currency","data":null} + 'order-orderprice-precision-error': InvalidOrder, # {"status":"error","err-code":"order-orderprice-precision-error","err-msg":"order price precision error, scale: `4`","data":null} + 'order-etp-nav-price-max-error': InvalidOrder, # {"status":"error","err-code":"order-etp-nav-price-max-error","err-msg":"Order price cannot be higher than 5% of NAV","data":null} + 'order-orderstate-error': OrderNotFound, # canceling an already canceled order + 'order-queryorder-invalid': OrderNotFound, # querying a non-existent order + 'order-update-error': ExchangeNotAvailable, # undocumented error + 'api-signature-check-failed': AuthenticationError, + 'api-signature-not-valid': AuthenticationError, # {"status":"error","err-code":"api-signature-not-valid","err-msg":"Signature not valid: Incorrect Access key [Access key错误]","data":null} + 'base-record-invalid': OrderNotFound, # https://github.com/ccxt/ccxt/issues/5750 + 'base-symbol-trade-disabled': BadSymbol, # {"status":"error","err-code":"base-symbol-trade-disabled","err-msg":"Trading is disabled for self symbol","data":null} + 'base-symbol-error': BadSymbol, # {"status":"error","err-code":"base-symbol-error","err-msg":"The symbol is invalid","data":null} + 'system-maintenance': OnMaintenance, # {"status": "error", "err-code": "system-maintenance", "err-msg": "System is in maintenance!", "data": null} + # err-msg + 'invalid symbol': BadSymbol, # {"ts":1568813334794,"status":"error","err-code":"invalid-parameter","err-msg":"invalid symbol"} + 'symbol trade not open now': BadSymbol, # {"ts":1576210479343,"status":"error","err-code":"invalid-parameter","err-msg":"symbol trade not open now"}, + 'invalid-address': BadRequest, # {"status":"error","err-code":"invalid-address","err-msg":"Invalid address.","data":null}, + 'base-currency-chain-error': BadRequest, # {"status":"error","err-code":"base-currency-chain-error","err-msg":"The current currency chain does not exist","data":null}, + 'dw-insufficient-balance': InsufficientFunds, # {"status":"error","err-code":"dw-insufficient-balance","err-msg":"Insufficient balance. You can only transfer `12.3456` at most.","data":null} + }, + }, + 'options': { + 'defaultNetwork': 'ERC20', + 'networks': { + 'ETH': 'erc20', + 'TRX': 'trc20', + 'HRC20': 'hrc20', + 'HECO': 'hrc20', + 'HT': 'hrc20', + 'ALGO': 'algo', + 'OMNI': '', + }, + # https://github.com/ccxt/ccxt/issues/5376 + 'fetchOrdersByStatesMethod': 'private_get_order_orders', # 'private_get_order_history' # https://github.com/ccxt/ccxt/pull/5392 + 'fetchOpenOrdersMethod': 'fetch_open_orders_v1', # 'fetch_open_orders_v2' # https://github.com/ccxt/ccxt/issues/5388 + 'createMarketBuyOrderRequiresPrice': True, + 'fetchMarketsMethod': 'publicGetCommonSymbols', + 'fetchBalanceMethod': 'privateGetAccountAccountsIdBalance', + 'createOrderMethod': 'privatePostOrderOrdersPlace', + 'currencyToPrecisionRoundingMode': TRUNCATE, + 'language': 'en-US', + 'broker': { + 'id': 'AA03022abc', + }, + }, + 'commonCurrencies': { + # https://github.com/ccxt/ccxt/issues/6081 + # https://github.com/ccxt/ccxt/issues/3365 + # https://github.com/ccxt/ccxt/issues/2873 + 'GET': 'Themis', # conflict with GET(Guaranteed Entrance Token, GET Protocol) + 'GTC': 'Game.com', # conflict with Gitcoin and Gastrocoin + 'HIT': 'HitChain', + # https://github.com/ccxt/ccxt/issues/7399 + # https://coinmarketcap.com/currencies/pnetwork/ + # https://coinmarketcap.com/currencies/penta/markets/ + # https://en.cryptonomist.ch/blog/eidoo/the-edo-to-pnt-upgrade-what-you-need-to-know-updated/ + 'PNT': 'Penta', + 'SBTC': 'Super Bitcoin', + 'BIFI': 'Bitcoin File', # conflict with Beefy.Finance https://github.com/ccxt/ccxt/issues/8706 + }, + }) + + def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = self.publicGetCommonTimestamp(params) + return self.safe_integer(response, 'data') + + def fetch_trading_limits(self, symbols: Strings = None, params={}): + # self method should not be called directly, use loadTradingLimits() instead + # by default it will try load withdrawal fees of all currencies(with separate requests) + # however if you define symbols = ['ETH/BTC', 'LTC/BTC'] in args it will only load those + self.load_markets() + if symbols is None: + symbols = self.symbols + result: dict = {} + for i in range(0, len(symbols)): + symbol = symbols[i] + result[symbol] = self.fetch_trading_limits_by_id(self.market_id(symbol), params) + return result + + def fetch_trading_limits_by_id(self, id: str, params={}): + request: dict = { + 'symbol': id, + } + response = self.publicGetCommonExchange(self.extend(request, params)) + # + # {status: "ok", + # "data": { symbol: "aidocbtc", + # "buy-limit-must-less-than": 1.1, + # "sell-limit-must-greater-than": 0.9, + # "limit-order-must-greater-than": 1, + # "limit-order-must-less-than": 5000000, + # "market-buy-order-must-greater-than": 0.0001, + # "market-buy-order-must-less-than": 100, + # "market-sell-order-must-greater-than": 1, + # "market-sell-order-must-less-than": 500000, + # "circuit-break-when-greater-than": 10000, + # "circuit-break-when-less-than": 10, + # "market-sell-order-rate-must-less-than": 0.1, + # "market-buy-order-rate-must-less-than": 0.1 }} + # + return self.parse_trading_limits(self.safe_value(response, 'data', {})) + + def parse_trading_limits(self, limits, symbol: Str = None, params={}): + # + # { symbol: "aidocbtc", + # "buy-limit-must-less-than": 1.1, + # "sell-limit-must-greater-than": 0.9, + # "limit-order-must-greater-than": 1, + # "limit-order-must-less-than": 5000000, + # "market-buy-order-must-greater-than": 0.0001, + # "market-buy-order-must-less-than": 100, + # "market-sell-order-must-greater-than": 1, + # "market-sell-order-must-less-than": 500000, + # "circuit-break-when-greater-than": 10000, + # "circuit-break-when-less-than": 10, + # "market-sell-order-rate-must-less-than": 0.1, + # "market-buy-order-rate-must-less-than": 0.1 } + # + return { + 'info': limits, + 'limits': { + 'amount': { + 'min': self.safe_number(limits, 'limit-order-must-greater-than'), + 'max': self.safe_number(limits, 'limit-order-must-less-than'), + }, + }, + } + + def cost_to_precision(self, symbol, cost): + return self.decimal_to_precision(cost, TRUNCATE, self.markets[symbol]['precision']['cost'], self.precisionMode) + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for huobijp + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + method = self.options['fetchMarketsMethod'] + response = getattr(self, method)(params) + # + # { + # "status": "ok", + # "data": [ + # { + # "base-currency": "xrp", + # "quote-currency": "btc", + # "price-precision": 9, + # "amount-precision": 2, + # "symbol-partition": "default", + # "symbol": "xrpbtc", + # "state": "online", + # "value-precision": 8, + # "min-order-amt": 1, + # "max-order-amt": 5000000, + # "min-order-value": 0.0001, + # "limit-order-min-order-amt": 1, + # "limit-order-max-order-amt": 5000000, + # "limit-order-max-buy-amt": 5000000, + # "limit-order-max-sell-amt": 5000000, + # "sell-market-min-order-amt": 1, + # "sell-market-max-order-amt": 500000, + # "buy-market-max-order-value": 100, + # "leverage-ratio": 5, + # "super-margin-leverage-ratio": 3, + # "api-trading": "enabled", + # "tags": "" + # } + # ... + # ] + # } + # + markets = self.safe_value(response, 'data', []) + numMarkets = len(markets) + if numMarkets < 1: + raise NetworkError(self.id + ' fetchMarkets() returned empty response: ' + self.json(markets)) + result = [] + for i in range(0, len(markets)): + market = markets[i] + baseId = self.safe_string(market, 'base-currency') + quoteId = self.safe_string(market, 'quote-currency') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + state = self.safe_string(market, 'state') + leverageRatio = self.safe_string(market, 'leverage-ratio', '1') + superLeverageRatio = self.safe_string(market, 'super-margin-leverage-ratio', '1') + margin = Precise.string_gt(leverageRatio, '1') or Precise.string_gt(superLeverageRatio, '1') + fee = self.parse_number('0') if (base == 'OMG') else self.parse_number('0.002') + result.append({ + 'id': baseId + quoteId, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': margin, + 'swap': False, + 'future': False, + 'option': False, + 'active': (state == 'online'), + 'contract': False, + 'linear': None, + 'inverse': None, + 'taker': fee, + 'maker': fee, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'price': self.parse_number(self.parse_precision(self.safe_string(market, 'price-precision'))), + 'amount': self.parse_number(self.parse_precision(self.safe_string(market, 'amount-precision'))), + 'cost': self.parse_number(self.parse_precision(self.safe_string(market, 'value-precision'))), + }, + 'limits': { + 'leverage': { + 'min': self.parse_number('1'), + 'max': self.parse_number(leverageRatio), + 'superMax': self.parse_number(superLeverageRatio), + }, + 'amount': { + 'min': self.safe_number(market, 'min-order-amt'), + 'max': self.safe_number(market, 'max-order-amt'), + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': self.safe_number(market, 'min-order-value'), + 'max': None, + }, + }, + 'created': None, + 'info': market, + }) + return result + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # fetchTicker + # + # { + # "amount": 26228.672978342216, + # "open": 9078.95, + # "close": 9146.86, + # "high": 9155.41, + # "id": 209988544334, + # "count": 265846, + # "low": 8988.0, + # "version": 209988544334, + # "ask": [9146.87, 0.156134], + # "vol": 2.3822168242201668E8, + # "bid": [9146.86, 0.080758], + # } + # + # fetchTickers + # { + # "symbol": "bhdht", + # "open": 2.3938, + # "high": 2.4151, + # "low": 2.3323, + # "close": 2.3909, + # "amount": 628.992, + # "vol": 1493.71841095, + # "count": 2088, + # "bid": 2.3643, + # "bidSize": 0.7136, + # "ask": 2.4061, + # "askSize": 0.4156 + # } + # + symbol = self.safe_symbol(None, market) + timestamp = self.safe_integer(ticker, 'ts') + bid = None + bidVolume = None + ask = None + askVolume = None + if 'bid' in ticker: + if isinstance(ticker['bid'], list): + bid = self.safe_string(ticker['bid'], 0) + bidVolume = self.safe_string(ticker['bid'], 1) + else: + bid = self.safe_string(ticker, 'bid') + bidVolume = self.safe_string(ticker, 'bidSize') + if 'ask' in ticker: + if isinstance(ticker['ask'], list): + ask = self.safe_string(ticker['ask'], 0) + askVolume = self.safe_string(ticker['ask'], 1) + else: + ask = self.safe_string(ticker, 'ask') + askVolume = self.safe_string(ticker, 'askSize') + open = self.safe_string(ticker, 'open') + close = self.safe_string(ticker, 'close') + baseVolume = self.safe_string(ticker, 'amount') + quoteVolume = self.safe_string(ticker, 'vol') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': bid, + 'bidVolume': bidVolume, + 'ask': ask, + 'askVolume': askVolume, + 'vwap': None, + 'open': open, + 'close': close, + 'last': close, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'info': ticker, + }, market) + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'type': 'step0', + } + response = self.marketGetDepth(self.extend(request, params)) + # + # { + # "status": "ok", + # "ch": "market.btcusdt.depth.step0", + # "ts": 1583474832790, + # "tick": { + # "bids": [ + # [9100.290000000000000000, 0.200000000000000000], + # [9099.820000000000000000, 0.200000000000000000], + # [9099.610000000000000000, 0.205000000000000000], + # ], + # "asks": [ + # [9100.640000000000000000, 0.005904000000000000], + # [9101.010000000000000000, 0.287311000000000000], + # [9101.030000000000000000, 0.012121000000000000], + # ], + # "ts":1583474832008, + # "version":104999698780 + # } + # } + # + if 'tick' in response: + if not response['tick']: + raise BadSymbol(self.id + ' fetchOrderBook() returned empty response: ' + self.json(response)) + tick = self.safe_value(response, 'tick') + timestamp = self.safe_integer(tick, 'ts', self.safe_integer(response, 'ts')) + result = self.parse_order_book(tick, symbol, timestamp) + result['nonce'] = self.safe_integer(tick, 'version') + return result + raise ExchangeError(self.id + ' fetchOrderBook() returned unrecognized response: ' + self.json(response)) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = self.marketGetDetailMerged(self.extend(request, params)) + # + # { + # "status": "ok", + # "ch": "market.btcusdt.detail.merged", + # "ts": 1583494336669, + # "tick": { + # "amount": 26228.672978342216, + # "open": 9078.95, + # "close": 9146.86, + # "high": 9155.41, + # "id": 209988544334, + # "count": 265846, + # "low": 8988.0, + # "version": 209988544334, + # "ask": [9146.87, 0.156134], + # "vol": 2.3822168242201668E8, + # "bid": [9146.86, 0.080758], + # } + # } + # + ticker = self.parse_ticker(response['tick'], market) + timestamp = self.safe_integer(response, 'ts') + ticker['timestamp'] = timestamp + ticker['datetime'] = self.iso8601(timestamp) + return ticker + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + symbols = self.market_symbols(symbols) + response = self.marketGetTickers(params) + tickers = self.safe_value(response, 'data', []) + timestamp = self.safe_integer(response, 'ts') + result: dict = {} + for i in range(0, len(tickers)): + marketId = self.safe_string(tickers[i], 'symbol') + market = self.safe_market(marketId) + symbol = market['symbol'] + ticker = self.parse_ticker(tickers[i], market) + ticker['timestamp'] = timestamp + ticker['datetime'] = self.iso8601(timestamp) + result[symbol] = ticker + return self.filter_by_array_tickers(result, 'symbol', symbols) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(public) + # + # { + # "amount": 0.010411000000000000, + # "trade-id": 102090736910, + # "ts": 1583497692182, + # "id": 10500517034273194594947, + # "price": 9096.050000000000000000, + # "direction": "sell" + # } + # + # fetchMyTrades(private) + # + # { + # "symbol": "swftcbtc", + # "fee-currency": "swftc", + # "filled-fees": "0", + # "source": "spot-api", + # "id": 83789509854000, + # "type": "buy-limit", + # "order-id": 83711103204909, + # 'filled-points': "0.005826843283532154", + # "fee-deduct-currency": "ht", + # 'filled-amount': "45941.53", + # "price": "0.0000001401", + # "created-at": 1597933260729, + # "match-id": 100087455560, + # "role": "maker", + # "trade-id": 100050305348 + # }, + # + marketId = self.safe_string(trade, 'symbol') + symbol = self.safe_symbol(marketId, market) + timestamp = self.safe_integer_2(trade, 'ts', 'created-at') + order = self.safe_string(trade, 'order-id') + side = self.safe_string(trade, 'direction') + type = self.safe_string(trade, 'type') + if type is not None: + typeParts = type.split('-') + side = typeParts[0] + type = typeParts[1] + takerOrMaker = self.safe_string(trade, 'role') + price = self.safe_string(trade, 'price') + amount = self.safe_string_2(trade, 'filled-amount', 'amount') + cost = Precise.string_mul(price, amount) + fee = None + feeCost = self.safe_string(trade, 'filled-fees') + feeCurrency = self.safe_currency_code(self.safe_string(trade, 'fee-currency')) + filledPoints = self.safe_string(trade, 'filled-points') + if filledPoints is not None: + if (feeCost is None) or (Precise.string_eq(feeCost, '0.0')): + feeCost = filledPoints + feeCurrency = self.safe_currency_code(self.safe_string(trade, 'fee-deduct-currency')) + if feeCost is not None: + fee = { + 'cost': feeCost, + 'currency': feeCurrency, + } + tradeId = self.safe_string_2(trade, 'trade-id', 'tradeId') + id = self.safe_string(trade, 'id', tradeId) + return self.safe_trade({ + 'info': trade, + 'id': id, + 'symbol': symbol, + 'order': order, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'type': type, + 'side': side, + 'takerOrMaker': takerOrMaker, + 'price': price, + 'amount': amount, + 'cost': cost, + 'fee': fee, + }) + + def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all the trades made from a single order + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + self.load_markets() + request: dict = { + 'id': id, + } + response = self.privateGetOrderOrdersIdMatchresults(self.extend(request, params)) + return self.parse_trades(response['data'], None, since, limit) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = None + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if limit is not None: + request['size'] = limit # 1-100 orders, default is 100 + if since is not None: + request['start-time'] = since # a date within 120 days from today + # request['end-time'] = self.sum(since, 172800000) # 48 hours window + response = self.privateGetOrderMatchresults(self.extend(request, params)) + return self.parse_trades(response['data'], market, since, limit) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = 1000, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['size'] = min(limit, 2000) + response = self.marketGetHistoryTrade(self.extend(request, params)) + # + # { + # "status": "ok", + # "ch": "market.btcusdt.trade.detail", + # "ts": 1583497692365, + # "data": [ + # { + # "id": 105005170342, + # "ts": 1583497692182, + # "data": [ + # { + # "amount": 0.010411000000000000, + # "trade-id": 102090736910, + # "ts": 1583497692182, + # "id": 10500517034273194594947, + # "price": 9096.050000000000000000, + # "direction": "sell" + # } + # ] + # }, + # # ... + # ] + # } + # + data = self.safe_value(response, 'data', []) + result = [] + for i in range(0, len(data)): + trades = self.safe_value(data[i], 'data', []) + for j in range(0, len(trades)): + trade = self.parse_trade(trades[j], market) + result.append(trade) + result = self.sort_by(result, 'timestamp') + return self.filter_by_symbol_since_limit(result, market['symbol'], since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # { + # "amount":1.2082, + # "open":0.025096, + # "close":0.025095, + # "high":0.025096, + # "id":1591515300, + # "count":6, + # "low":0.025095, + # "vol":0.0303205097 + # } + # + return [ + self.safe_timestamp(ohlcv, 'id'), + self.safe_number(ohlcv, 'open'), + self.safe_number(ohlcv, 'high'), + self.safe_number(ohlcv, 'low'), + self.safe_number(ohlcv, 'close'), + self.safe_number(ohlcv, 'amount'), + ] + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = 1000, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'period': self.safe_string(self.timeframes, timeframe, timeframe), + } + if limit is not None: + request['size'] = min(limit, 2000) + response = self.marketGetHistoryKline(self.extend(request, params)) + # + # { + # "status":"ok", + # "ch":"market.ethbtc.kline.1min", + # "ts":1591515374371, + # "data":[ + # {"amount":0.0,"open":0.025095,"close":0.025095,"high":0.025095,"id":1591515360,"count":0,"low":0.025095,"vol":0.0}, + # {"amount":1.2082,"open":0.025096,"close":0.025095,"high":0.025096,"id":1591515300,"count":6,"low":0.025095,"vol":0.0303205097}, + # {"amount":0.0648,"open":0.025096,"close":0.025096,"high":0.025096,"id":1591515240,"count":2,"low":0.025096,"vol":0.0016262208}, + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_ohlcvs(data, market, timeframe, since, limit) + + def fetch_accounts(self, params={}) -> List[Account]: + """ + fetch all the accounts associated with a profile + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `account structures ` indexed by the account type + """ + self.load_markets() + response = self.privateGetAccountAccounts(params) + return response['data'] + + def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + request: dict = { + 'language': self.options['language'], + } + response = self.publicGetSettingsCurrencys(self.extend(request, params)) + # + # { + # "status":"ok", + # "data":[ + # { + # "currency-addr-with-tag":false, + # "fast-confirms":12, + # "safe-confirms":12, + # "currency-type":"eth", + # "quote-currency":true, + # "withdraw-enable-timestamp":1609430400000, + # "deposit-enable-timestamp":1609430400000, + # "currency-partition":"all", + # "support-sites":["OTC","INSTITUTION","MINEPOOL"], + # "withdraw-precision":6, + # "visible-assets-timestamp":1508839200000, + # "deposit-min-amount":"1", + # "withdraw-min-amount":"10", + # "show-precision":"8", + # "tags":"", + # "weight":23, + # "full-name":"Tether USDT", + # "otc-enable":1, + # "visible":true, + # "white-enabled":false, + # "country-disabled":false, + # "deposit-enabled":true, + # "withdraw-enabled":true, + # "name":"usdt", + # "state":"online", + # "display-name":"USDT", + # "suspend-withdraw-desc":null, + # "withdraw-desc":"Minimum withdrawal amount: 10 USDT(ERC20). not >______ Balances: + balances = self.safe_value(response['data'], 'list', []) + result: dict = {'info': response} + for i in range(0, len(balances)): + balance = balances[i] + currencyId = self.safe_string(balance, 'currency') + code = self.safe_currency_code(currencyId) + account = None + if code in result: + account = result[code] + else: + account = self.account() + if balance['type'] == 'trade': + account['free'] = self.safe_string(balance, 'balance') + if balance['type'] == 'frozen': + account['used'] = self.safe_string(balance, 'balance') + result[code] = account + return self.safe_balance(result) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + self.load_accounts() + method = self.options['fetchBalanceMethod'] + request: dict = { + 'id': self.accounts[0]['id'], + } + response = getattr(self, method)(self.extend(request, params)) + return self.parse_balance(response) + + def fetch_orders_by_states(self, states, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + self.load_markets() + request: dict = { + 'states': states, + } + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + method = self.safe_string(self.options, 'fetchOrdersByStatesMethod', 'private_get_order_orders') + response = getattr(self, method)(self.extend(request, params)) + # + # {"status": "ok", + # "data": [{ id: 13997833014, + # "symbol": "ethbtc", + # "account-id": 3398321, + # "amount": "0.045000000000000000", + # "price": "0.034014000000000000", + # "created-at": 1545836976871, + # "type": "sell-limit", + # "field-amount": "0.045000000000000000", + # "field-cash-amount": "0.001530630000000000", + # "field-fees": "0.000003061260000000", + # "finished-at": 1545837948214, + # "source": "spot-api", + # "state": "filled", + # "canceled-at": 0 } ]} + # + return self.parse_orders(response['data'], market, since, limit) + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + request: dict = { + 'id': id, + } + response = self.privateGetOrderOrdersId(self.extend(request, params)) + order = self.safe_dict(response, 'data') + return self.parse_order(order) + + def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + return self.fetch_orders_by_states('pre-submitted,submitted,partial-filled,filled,partial-canceled,canceled', symbol, since, limit, params) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + method = self.safe_string(self.options, 'fetchOpenOrdersMethod', 'fetch_open_orders_v1') + return getattr(self, method)(symbol, since, limit, params) + + def fetch_open_orders_v1(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOpenOrdersV1() requires a symbol argument') + return self.fetch_orders_by_states('pre-submitted,submitted,partial-filled', symbol, since, limit, params) + + def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + return self.fetch_orders_by_states('filled,partial-canceled,canceled', symbol, since, limit, params) + + def fetch_open_orders_v2(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + accountId = self.safe_string(params, 'account-id') + if accountId is None: + # pick the first account + self.load_accounts() + for i in range(0, len(self.accounts)): + account = self.accounts[i] + if account['type'] == 'spot': + accountId = self.safe_string(account, 'id') + if accountId is not None: + break + request['account-id'] = accountId + if limit is not None: + request['size'] = limit + omitted = self.omit(params, 'account-id') + response = self.privateGetOrderOpenOrders(self.extend(request, omitted)) + # + # { + # "status":"ok", + # "data":[ + # { + # "symbol":"ethusdt", + # "source":"api", + # "amount":"0.010000000000000000", + # "account-id":1528640, + # "created-at":1561597491963, + # "price":"400.000000000000000000", + # "filled-amount":"0.0", + # "filled-cash-amount":"0.0", + # "filled-fees":"0.0", + # "id":38477101630, + # "state":"submitted", + # "type":"sell-limit" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_orders(data, market, since, limit) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'partial-filled': 'open', + 'partial-canceled': 'canceled', + 'filled': 'closed', + 'canceled': 'canceled', + 'submitted': 'open', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # { id: 13997833014, + # "symbol": "ethbtc", + # "account-id": 3398321, + # "amount": "0.045000000000000000", + # "price": "0.034014000000000000", + # "created-at": 1545836976871, + # "type": "sell-limit", + # "field-amount": "0.045000000000000000", # they have fixed it for filled-amount + # "field-cash-amount": "0.001530630000000000", # they have fixed it for filled-cash-amount + # "field-fees": "0.000003061260000000", # they have fixed it for filled-fees + # "finished-at": 1545837948214, + # "source": "spot-api", + # "state": "filled", + # "canceled-at": 0 } + # + # { id: 20395337822, + # "symbol": "ethbtc", + # "account-id": 5685075, + # "amount": "0.001000000000000000", + # "price": "0.0", + # "created-at": 1545831584023, + # "type": "buy-market", + # "field-amount": "0.029100000000000000", # they have fixed it for filled-amount + # "field-cash-amount": "0.000999788700000000", # they have fixed it for filled-cash-amount + # "field-fees": "0.000058200000000000", # they have fixed it for filled-fees + # "finished-at": 1545831584181, + # "source": "spot-api", + # "state": "filled", + # "canceled-at": 0 } + # + id = self.safe_string(order, 'id') + side = None + type = None + status = None + if 'type' in order: + orderType = order['type'].split('-') + side = orderType[0] + type = orderType[1] + status = self.parse_order_status(self.safe_string(order, 'state')) + marketId = self.safe_string(order, 'symbol') + market = self.safe_market(marketId, market) + timestamp = self.safe_integer(order, 'created-at') + clientOrderId = self.safe_string(order, 'client-order-id') + amount = self.safe_string(order, 'amount') + filled = self.safe_string_2(order, 'filled-amount', 'field-amount') # typo in their API, filled amount + price = self.safe_string(order, 'price') + cost = self.safe_string_2(order, 'filled-cash-amount', 'field-cash-amount') # same typo + feeCost = self.safe_string_2(order, 'filled-fees', 'field-fees') # typo in their API, filled fees + fee = None + if feeCost is not None: + feeCurrency = market['quote'] if (side == 'sell') else market['base'] + fee = { + 'cost': feeCost, + 'currency': feeCurrency, + } + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'symbol': market['symbol'], + 'type': type, + 'timeInForce': None, + 'postOnly': None, + 'side': side, + 'price': price, + 'triggerPrice': None, + 'average': None, + 'cost': cost, + 'amount': amount, + 'filled': filled, + 'remaining': None, + 'status': status, + 'fee': fee, + 'trades': None, + }, market) + + def create_market_buy_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market buy order by providing the symbol and cost + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' createMarketBuyOrderWithCost() supports spot orders only') + params['createMarketBuyOrderRequiresPrice'] = False + return self.create_order(symbol, 'market', 'buy', cost, None, params) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + self.load_accounts() + market = self.market(symbol) + request: dict = { + 'account-id': self.accounts[0]['id'], + 'symbol': market['id'], + 'type': side + '-' + type, + } + clientOrderId = self.safe_string_2(params, 'clientOrderId', 'client-order-id') # must be 64 chars max and unique within 24 hours + if clientOrderId is None: + broker = self.safe_value(self.options, 'broker', {}) + brokerId = self.safe_string(broker, 'id') + request['client-order-id'] = brokerId + self.uuid() + else: + request['client-order-id'] = clientOrderId + params = self.omit(params, ['clientOrderId', 'client-order-id']) + if (type == 'market') and (side == 'buy'): + quoteAmount = None + createMarketBuyOrderRequiresPrice = True + createMarketBuyOrderRequiresPrice, params = self.handle_option_and_params(params, 'createOrder', 'createMarketBuyOrderRequiresPrice', True) + cost = self.safe_number(params, 'cost') + params = self.omit(params, 'cost') + if cost is not None: + quoteAmount = self.amount_to_precision(symbol, cost) + elif createMarketBuyOrderRequiresPrice: + if price is None: + raise InvalidOrder(self.id + ' createOrder() requires the price argument for market buy orders to calculate the total cost to spend(amount * price), alternatively set the createMarketBuyOrderRequiresPrice option or param to False and pass the cost to spend in the amount argument') + else: + # despite that cost = amount * price is in quote currency and should have quote precision + # the exchange API requires the cost supplied in 'amount' to be of base precision + # more about it here: + # https://github.com/ccxt/ccxt/pull/4395 + # https://github.com/ccxt/ccxt/issues/7611 + # we use amountToPrecision here because the exchange requires cost in base precision + amountString = self.number_to_string(amount) + priceString = self.number_to_string(price) + quoteAmount = self.amount_to_precision(symbol, Precise.string_mul(amountString, priceString)) + else: + quoteAmount = self.amount_to_precision(symbol, amount) + request['amount'] = quoteAmount + else: + request['amount'] = self.amount_to_precision(symbol, amount) + if type == 'limit' or type == 'ioc' or type == 'limit-maker' or type == 'stop-limit' or type == 'stop-limit-fok': + request['price'] = self.price_to_precision(symbol, price) + method = self.options['createOrderMethod'] + response = getattr(self, method)(self.extend(request, params)) + id = self.safe_string(response, 'data') + return self.safe_order({ + 'info': response, + 'id': id, + 'timestamp': None, + 'datetime': None, + 'lastTradeTimestamp': None, + 'status': None, + 'symbol': symbol, + 'type': type, + 'side': side, + 'price': price, + 'amount': amount, + 'filled': None, + 'remaining': None, + 'cost': None, + 'trades': None, + 'fee': None, + 'clientOrderId': None, + 'average': None, + }, market) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + :param str id: order id + :param str symbol: not used by bittrade cancelOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + response = self.privatePostOrderOrdersIdSubmitcancel({'id': id}) + # + # { + # "status": "ok", + # "data": "10138899000", + # } + # + return self.extend(self.parse_order(response), { + 'id': id, + 'status': 'canceled', + }) + + def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): + """ + cancel multiple orders + :param str[] ids: order ids + :param str symbol: not used by bittrade cancelOrders() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an list of `order structures ` + """ + self.load_markets() + clientOrderIds = self.safe_value_2(params, 'clientOrderIds', 'client-order-ids') + params = self.omit(params, ['clientOrderIds', 'client-order-ids']) + request: dict = {} + if clientOrderIds is None: + request['order-ids'] = ids + else: + request['client-order-ids'] = clientOrderIds + response = self.privatePostOrderOrdersBatchcancel(self.extend(request, params)) + # + # { + # "status": "ok", + # "data": { + # "success": [ + # "5983466" + # ], + # "failed": [ + # { + # "err-msg": "Incorrect order state", + # "order-state": 7, + # "order-id": "", + # "err-code": "order-orderstate-error", + # "client-order-id": "first" + # }, + # { + # "err-msg": "Incorrect order state", + # "order-state": 7, + # "order-id": "", + # "err-code": "order-orderstate-error", + # "client-order-id": "second" + # }, + # { + # "err-msg": "The record is not found.", + # "order-id": "", + # "err-code": "base-not-found", + # "client-order-id": "third" + # } + # ] + # } + # } + # + return self.parse_cancel_orders(response) + + def parse_cancel_orders(self, orders): + # + # { + # "success": [ + # "5983466" + # ], + # "failed": [ + # { + # "err-msg": "Incorrect order state", + # "order-state": 7, + # "order-id": "", + # "err-code": "order-orderstate-error", + # "client-order-id": "first" + # }, + # ... + # ] + # } + # + # { + # "errors": [ + # { + # "order_id": "769206471845261312", + # "err_code": 1061, + # "err_msg": "This order doesnt exist." + # } + # ], + # "successes": "1258075374411399168,1258075393254871040" + # } + # + successes = self.safe_string(orders, 'successes') + success = None + if successes is not None: + success = successes.split(',') + else: + success = self.safe_list(orders, 'success', []) + failed = self.safe_list_2(orders, 'errors', 'failed', []) + result = [] + for i in range(0, len(success)): + order = success[i] + result.append(self.safe_order({ + 'info': order, + 'id': order, + 'status': 'canceled', + })) + for i in range(0, len(failed)): + order = failed[i] + result.append(self.safe_order({ + 'info': order, + 'id': self.safe_string_2(order, 'order-id', 'order_id'), + 'status': 'failed', + 'clientOrderId': self.safe_string(order, 'client-order-id'), + })) + return result + + def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders + :param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + self.load_markets() + request: dict = { + # 'account-id' string False NA The account id used for self cancel Refer to GET /v1/account/accounts + # 'symbol': market['id'], # a list of comma-separated symbols, all symbols by default + # 'types' 'string', buy-market, sell-market, buy-limit, sell-limit, buy-ioc, sell-ioc, buy-stop-limit, sell-stop-limit, buy-limit-fok, sell-limit-fok, buy-stop-limit-fok, sell-stop-limit-fok + # 'side': 'buy', # or 'sell' + # 'size': 100, # the number of orders to cancel 1-100 + } + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + response = self.privatePostOrderOrdersBatchCancelOpenOrders(self.extend(request, params)) + # + # { + # "code": 200, + # "data": { + # "success-count": 2, + # "failed-count": 0, + # "next-id": 5454600 + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return [ + self.safe_order({ + 'info': data, + }), + ] + + def parse_deposit_address(self, depositAddress, currency: Currency = None): + # + # { + # "currency": "usdt", + # "address": "0xf7292eb9ba7bc50358e27f0e025a4d225a64127b", + # "addressTag": "", + # "chain": "usdterc20", # trc20usdt, hrc20usdt, usdt, algousdt + # } + # + address = self.safe_string(depositAddress, 'address') + tag = self.safe_string(depositAddress, 'addressTag') + currencyId = self.safe_string(depositAddress, 'currency') + currency = self.safe_currency(currencyId, currency) + code = self.safe_currency_code(currencyId, currency) + networkId = self.safe_string(depositAddress, 'chain') + networks = self.safe_value(currency, 'networks', {}) + networksById = self.index_by(networks, 'id') + networkValue = self.safe_value(networksById, networkId, networkId) + network = self.safe_string(networkValue, 'network') + self.check_address(address) + return { + 'currency': code, + 'address': address, + 'tag': tag, + 'network': network, + 'info': depositAddress, + } + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + if limit is None or limit > 100: + limit = 100 + self.load_markets() + currency = None + if code is not None: + currency = self.currency(code) + request: dict = { + 'type': 'deposit', + 'from': 0, # From 'id' ... if you want to get results after a particular transaction id, pass the id in params.from + } + if currency is not None: + request['currency'] = currency['id'] + if limit is not None: + request['size'] = limit # max 100 + response = self.privateGetQueryDepositWithdraw(self.extend(request, params)) + # return response + return self.parse_transactions(response['data'], currency, since, limit) + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + if limit is None or limit > 100: + limit = 100 + self.load_markets() + currency = None + if code is not None: + currency = self.currency(code) + request: dict = { + 'type': 'withdraw', + 'from': 0, # From 'id' ... if you want to get results after a particular transaction id, pass the id in params.from + } + if currency is not None: + request['currency'] = currency['id'] + if limit is not None: + request['size'] = limit # max 100 + response = self.privateGetQueryDepositWithdraw(self.extend(request, params)) + # return response + return self.parse_transactions(response['data'], currency, since, limit) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fetchDeposits + # + # { + # "id": 8211029, + # "type": "deposit", + # "currency": "eth", + # "chain": "eth", + # 'tx-hash': "bd315....", + # "amount": 0.81162421, + # "address": "4b8b....", + # 'address-tag": '", + # "fee": 0, + # "state": "safe", + # "created-at": 1542180380965, + # "updated-at": 1542180788077 + # } + # + # fetchWithdrawals + # + # { + # "id": 6908275, + # "type": "withdraw", + # "currency": "btc", + # "chain": "btc", + # 'tx-hash': "c1a1a....", + # "amount": 0.80257005, + # "address": "1QR....", + # 'address-tag": '", + # "fee": 0.0005, + # "state": "confirmed", + # "created-at": 1552107295685, + # "updated-at": 1552108032859 + # } + # + # withdraw + # + # { + # "status": "ok", + # "data": "99562054" + # } + # + timestamp = self.safe_integer(transaction, 'created-at') + code = self.safe_currency_code(self.safe_string(transaction, 'currency')) + type = self.safe_string(transaction, 'type') + if type == 'withdraw': + type = 'withdrawal' + feeCost = self.safe_string(transaction, 'fee') + if feeCost is not None: + feeCost = Precise.string_abs(feeCost) + return { + 'info': transaction, + 'id': self.safe_string_2(transaction, 'id', 'data'), + 'txid': self.safe_string(transaction, 'tx-hash'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': self.safe_string_upper(transaction, 'chain'), + 'address': self.safe_string(transaction, 'address'), + 'addressTo': None, + 'addressFrom': None, + 'tag': self.safe_string(transaction, 'address-tag'), + 'tagTo': None, + 'tagFrom': None, + 'type': type, + 'amount': self.safe_number(transaction, 'amount'), + 'currency': code, + 'status': self.parse_transaction_status(self.safe_string(transaction, 'state')), + 'updated': self.safe_integer(transaction, 'updated-at'), + 'comment': None, + 'internal': None, + 'fee': { + 'currency': code, + 'cost': self.parse_number(feeCost), + 'rate': None, + }, + } + + def parse_transaction_status(self, status: Str): + statuses: dict = { + # deposit statuses + 'unknown': 'failed', + 'confirming': 'pending', + 'confirmed': 'ok', + 'safe': 'ok', + 'orphan': 'failed', + # withdrawal statuses + 'submitted': 'pending', + 'canceled': 'canceled', + 'reexamine': 'pending', + 'reject': 'failed', + 'pass': 'pending', + 'wallet-reject': 'failed', + # 'confirmed': 'ok', # present in deposit statuses + 'confirm-error': 'failed', + 'repealed': 'failed', + 'wallet-transfer': 'pending', + 'pre-transfer': 'pending', + } + return self.safe_string(statuses, status, status) + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.load_markets() + self.check_address(address) + currency = self.currency(code) + request: dict = { + 'address': address, # only supports existing addresses in your withdraw address list + 'amount': amount, + 'currency': currency['id'].lower(), + } + if tag is not None: + request['addr-tag'] = tag # only for XRP? + networks = self.safe_value(self.options, 'networks', {}) + network = self.safe_string_upper(params, 'network') # self line allows the user to specify either ERC20 or ETH + network = self.safe_string_lower(networks, network, network) # handle ETH>ERC20 alias + if network is not None: + # possible chains - usdterc20, trc20usdt, hrc20usdt, usdt, algousdt + if network == 'erc20': + request['chain'] = currency['id'] + network + else: + request['chain'] = network + currency['id'] + params = self.omit(params, 'network') + response = self.privatePostDwWithdrawApiCreate(self.extend(request, params)) + # + # { + # "status": "ok", + # "data": "99562054" + # } + # + return self.parse_transaction(response, currency) + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = '/' + if api == 'market': + url += api + elif (api == 'public') or (api == 'private'): + url += self.version + elif (api == 'v2Public') or (api == 'v2Private'): + url += 'v2' + url += '/' + self.implode_params(path, params) + query = self.omit(params, self.extract_params(path)) + if api == 'private' or api == 'v2Private': + self.check_required_credentials() + timestamp = self.ymdhms(self.milliseconds(), 'T') + request: dict = { + 'SignatureMethod': 'HmacSHA256', + 'SignatureVersion': '2', + 'AccessKeyId': self.apiKey, + 'Timestamp': timestamp, + } + if method != 'POST': + request = self.extend(request, query) + requestSorted = self.keysort(request) + auth = self.urlencode(requestSorted) + # unfortunately, PHP demands double quotes for the escaped newline symbol + # eslint-disable-next-line quotes + payload = "\n".join([method, self.hostname, url, auth]) + signature = self.hmac(self.encode(payload), self.encode(self.secret), hashlib.sha256, 'base64') + auth += '&' + self.urlencode({'Signature': signature}) + url += '?' + auth + if method == 'POST': + body = self.json(query) + headers = { + 'Content-Type': 'application/json', + } + else: + headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + } + else: + if params: + url += '?' + self.urlencode(params) + url = self.implode_params(self.urls['api'][api], { + 'hostname': self.hostname, + }) + url + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None # fallback to default error handler + if 'status' in response: + # + # {"status":"error","err-code":"order-limitorder-amount-min-error","err-msg":"limit order amount error, min: `0.001`","data":null} + # + status = self.safe_string(response, 'status') + if status == 'error': + code = self.safe_string(response, 'err-code') + feedback = self.id + ' ' + body + self.throw_broadly_matched_exception(self.exceptions['broad'], body, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], code, feedback) + message = self.safe_string(response, 'err-msg') + self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) + raise ExchangeError(feedback) + return None diff --git a/ccxt/bitvavo.py b/ccxt/bitvavo.py new file mode 100644 index 0000000..289e323 --- /dev/null +++ b/ccxt/bitvavo.py @@ -0,0 +1,2134 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.bitvavo import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currencies, Currency, DepositAddress, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, TradingFees, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import AccountSuspended +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidAddress +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.errors import OnMaintenance +from ccxt.base.decimal_to_precision import TRUNCATE +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class bitvavo(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(bitvavo, self).describe(), { + 'id': 'bitvavo', + 'name': 'Bitvavo', + 'countries': ['NL'], # Netherlands + 'rateLimit': 60, # 1000 requests per minute + 'version': 'v2', + 'certified': False, + 'pro': True, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createOrder': True, + 'createOrderWithTakeProfitAndStopLoss': False, + 'createOrderWithTakeProfitAndStopLossWs': False, + 'createPostOnlyOrder': False, + 'createReduceOnlyOrder': False, + 'createStopLimitOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'editOrder': True, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchDepositWithdrawFee': 'emulated', + 'fetchDepositWithdrawFees': True, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': True, + 'fetchTransfer': False, + 'fetchTransfers': False, + 'fetchVolatilityHistory': False, + 'fetchWithdrawals': True, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'repayMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'transfer': False, + 'withdraw': True, + }, + 'timeframes': { + '1m': '1m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1h', + '2h': '2h', + '4h': '4h', + '6h': '6h', + '8h': '8h', + '12h': '12h', + '1d': '1d', + }, + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/d213155c-8c71-4701-9bd5-45351febc2a8', + 'api': { + 'public': 'https://api.bitvavo.com', + 'private': 'https://api.bitvavo.com', + }, + 'www': 'https://bitvavo.com/', + 'doc': 'https://docs.bitvavo.com/', + 'fees': 'https://bitvavo.com/en/fees', + 'referral': 'https://bitvavo.com/?a=24F34952F7', + }, + 'api': { + 'public': { + 'get': { + 'time': 1, + 'markets': 1, + 'assets': 1, + '{market}/book': 1, + '{market}/trades': 5, + '{market}/candles': 1, + 'ticker/price': 1, + 'ticker/book': 1, + 'ticker/24h': {'cost': 1, 'noMarket': 25}, + }, + }, + 'private': { + 'get': { + 'account': 1, + 'order': 1, + 'orders': 5, + 'ordersOpen': {'cost': 1, 'noMarket': 25}, + 'trades': 5, + 'balance': 5, + 'deposit': 1, + 'depositHistory': 5, + 'withdrawalHistory': 5, + }, + 'post': { + 'order': 1, + 'withdrawal': 1, + }, + 'put': { + 'order': 1, + }, + 'delete': { + 'order': 1, + 'orders': 1, + }, + }, + }, + 'fees': { + 'trading': { + 'tierBased': True, + 'percentage': True, + 'taker': self.parse_number('0.0025'), + 'maker': self.parse_number('0.002'), + 'tiers': { + 'taker': [ + [self.parse_number('0'), self.parse_number('0.0025')], + [self.parse_number('100000'), self.parse_number('0.0020')], + [self.parse_number('250000'), self.parse_number('0.0016')], + [self.parse_number('500000'), self.parse_number('0.0012')], + [self.parse_number('1000000'), self.parse_number('0.0010')], + [self.parse_number('2500000'), self.parse_number('0.0008')], + [self.parse_number('5000000'), self.parse_number('0.0006')], + [self.parse_number('10000000'), self.parse_number('0.0005')], + [self.parse_number('25000000'), self.parse_number('0.0004')], + ], + 'maker': [ + [self.parse_number('0'), self.parse_number('0.0015')], + [self.parse_number('100000'), self.parse_number('0.0010')], + [self.parse_number('250000'), self.parse_number('0.0008')], + [self.parse_number('500000'), self.parse_number('0.0006')], + [self.parse_number('1000000'), self.parse_number('0.0005')], + [self.parse_number('2500000'), self.parse_number('0.0004')], + [self.parse_number('5000000'), self.parse_number('0.0004')], + [self.parse_number('10000000'), self.parse_number('0.0003')], + [self.parse_number('25000000'), self.parse_number('0.0003')], + ], + }, + }, + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerPriceType': None, + 'triggerDirection': None, + 'stopLossPrice': True, + 'takeProfitPrice': True, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyRequiresPrice': False, + 'marketBuyByCost': True, + 'selfTradePrevention': { + 'EXPIRE_MAKER': False, + 'EXPIRE_TAKER': False, + 'EXPIRE_BOTH': True, + 'NONE': False, + }, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 100000, + 'untilDays': 100000, + 'symbolRequired': True, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOrders': { + 'marginMode': True, + 'limit': 1000, + 'daysBack': 100000, + 'untilDays': 100000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchClosedOrders': None, + 'fetchOHLCV': { + 'limit': 1440, + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'exceptions': { + 'exact': { + '101': ExchangeError, # Unknown error. Operation may or may not have succeeded. + '102': BadRequest, # Invalid JSON. + '103': RateLimitExceeded, # You have been rate limited. Please observe the Bitvavo-Ratelimit-AllowAt header to see when you can send requests again. Failure to respect self limit will result in an IP ban. The default value is 1000 weighted requests per minute. Please contact support if you wish to increase self limit. + '104': RateLimitExceeded, # You have been rate limited by the number of new orders. The default value is 100 new orders per second or 100.000 new orders per day. Please update existing orders instead of cancelling and creating orders. Please contact support if you wish to increase self limit. + '105': PermissionDenied, # Your IP or API key has been banned for not respecting the rate limit. The ban expires at ${expiryInMs}. + '107': ExchangeNotAvailable, # The matching engine is overloaded. Please wait 500ms and resubmit your order. + '108': ExchangeNotAvailable, # The matching engine could not process your order in time. Please consider increasing the access window or resubmit your order. + '109': ExchangeNotAvailable, # The matching engine did not respond in time. Operation may or may not have succeeded. + '110': BadRequest, # Invalid endpoint. Please check url and HTTP method. + '200': BadRequest, # ${param} url parameter is not supported. Please note that parameters are case-sensitive and use body parameters for PUT and POST requests. + '201': BadRequest, # ${param} body parameter is not supported. Please note that parameters are case-sensitive and use url parameters for GET and DELETE requests. + '202': BadRequest, # ${param} order parameter is not supported. Please note that certain parameters are only allowed for market or limit orders. + '203': BadSymbol, # {"errorCode":203,"error":"symbol parameter is required."} + '204': BadRequest, # ${param} parameter is not supported. + '205': BadRequest, # ${param} parameter is invalid. + '206': BadRequest, # Use either ${paramA} or ${paramB}. The usage of both parameters at the same time is not supported. + '210': InvalidOrder, # Amount exceeds the maximum allowed amount(1000000000). + '211': InvalidOrder, # Price exceeds the maximum allowed amount(100000000000). + '212': InvalidOrder, # Amount is below the minimum allowed amount for self asset. + '213': InvalidOrder, # Price is below the minimum allowed amount(0.000000000000001). + '214': InvalidOrder, # Price is too detailed + '215': InvalidOrder, # Price is too detailed. A maximum of 15 digits behind the decimal point are allowed. + '216': InsufficientFunds, # {"errorCode":216,"error":"You do not have sufficient balance to complete self operation."} + '217': InvalidOrder, # {"errorCode":217,"error":"Minimum order size in quote currency is 5 EUR or 0.001 BTC."} + '230': ExchangeError, # The order is rejected by the matching engine. + '231': ExchangeError, # The order is rejected by the matching engine. TimeInForce must be GTC when markets are paused. + '232': BadRequest, # You must change at least one of amount, amountRemaining, price, timeInForce, selfTradePrevention or postOnly. + '233': InvalidOrder, # {"errorCode":233,"error":"Order must be active(status new or partiallyFilled) to allow updating/cancelling."} + '234': InvalidOrder, # Market orders cannot be updated. + '235': ExchangeError, # You can only have 100 open orders on each book. + '236': BadRequest, # You can only update amount or amountRemaining, not both. + '240': OrderNotFound, # {"errorCode":240,"error":"No order found. Please be aware that simultaneously updating the same order may return self error."} + '300': AuthenticationError, # Authentication is required for self endpoint. + '301': AuthenticationError, # {"errorCode":301,"error":"API Key must be of length 64."} + '302': AuthenticationError, # Timestamp is invalid. This must be a timestamp in ms. See Bitvavo-Access-Timestamp header or timestamp parameter for websocket. + '303': AuthenticationError, # Window must be between 100 and 60000 ms. + '304': AuthenticationError, # Request was not received within acceptable window(default 30s, or custom with Bitvavo-Access-Window header) of Bitvavo-Access-Timestamp header(or timestamp parameter for websocket). + # "304": AuthenticationError, # Authentication is required for self endpoint. + '305': AuthenticationError, # {"errorCode":305,"error":"No active API key found."} + '306': AuthenticationError, # No active API key found. Please ensure that you have confirmed the API key by e-mail. + '307': PermissionDenied, # This key does not allow access from self IP. + '308': AuthenticationError, # {"errorCode":308,"error":"The signature length is invalid(HMAC-SHA256 should return a 64 length hexadecimal string)."} + '309': AuthenticationError, # {"errorCode":309,"error":"The signature is invalid."} + '310': PermissionDenied, # This key does not allow trading actions. + '311': PermissionDenied, # This key does not allow showing account information. + '312': PermissionDenied, # This key does not allow withdrawal of funds. + '315': BadRequest, # Websocket connections may not be used in a browser. Please use REST requests for self. + '317': AccountSuspended, # This account is locked. Please contact support. + '400': ExchangeError, # Unknown error. Please contact support with a copy of your request. + '401': ExchangeError, # Deposits for self asset are not available at self time. + '402': PermissionDenied, # You need to verify your identitiy before you can deposit and withdraw digital assets. + '403': PermissionDenied, # You need to verify your phone number before you can deposit and withdraw digital assets. + '404': OnMaintenance, # Could not complete self operation, because our node cannot be reached. Possibly under maintenance. + '405': ExchangeError, # You cannot withdraw digital assets during a cooldown period. This is the result of newly added bank accounts. + '406': BadRequest, # {"errorCode":406,"error":"Your withdrawal is too small."} + '407': ExchangeError, # Internal transfer is not possible. + '408': InsufficientFunds, # {"errorCode":408,"error":"You do not have sufficient balance to complete self operation."} + '409': InvalidAddress, # {"errorCode":409,"error":"This is not a verified bank account."} + '410': ExchangeError, # Withdrawals for self asset are not available at self time. + '411': BadRequest, # You can not transfer assets to yourself. + '412': InvalidAddress, # {"errorCode":412,"error":"eth_address_invalid."} + '413': InvalidAddress, # This address violates the whitelist. + '414': ExchangeError, # You cannot withdraw assets within 2 minutes of logging in. + }, + 'broad': { + 'start parameter is invalid': BadRequest, # {"errorCode":205,"error":"start parameter is invalid."} + 'symbol parameter is invalid': BadSymbol, # {"errorCode":205,"error":"symbol parameter is invalid."} + 'amount parameter is invalid': InvalidOrder, # {"errorCode":205,"error":"amount parameter is invalid."} + 'orderId parameter is invalid': InvalidOrder, # {"errorCode":205,"error":"orderId parameter is invalid."} + }, + }, + 'options': { + 'currencyToPrecisionRoundingMode': TRUNCATE, + 'BITVAVO-ACCESS-WINDOW': 10000, # default 10 sec + 'networks': { + 'ERC20': 'ETH', + 'TRC20': 'TRX', + }, + 'operatorId': None, # self will be required soon for order-related endpoints + 'fiatCurrencies': ['EUR'], # only fiat atm + }, + 'precisionMode': TICK_SIZE, + 'commonCurrencies': { + 'MIOTA': 'IOTA', # https://github.com/ccxt/ccxt/issues/7487 + }, + }) + + def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = self.publicGetTime(params) + # + # {"time": 1590379519148} + # + return self.safe_integer(response, 'time') + + def fetch_markets(self, params={}) -> List[Market]: + """ + + https://docs.bitvavo.com/#tag/General/paths/~1markets/get + + retrieves data on all markets for bitvavo + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = self.publicGetMarkets(params) + # + # { + # "market": "BTC-EUR", + # "status": "trading", + # "base": "BTC", + # "quote": "EUR", + # "pricePrecision": "0", # deprecated, self is mostly 0 across other markets too, which is abnormal, so we ignore self. + # "tickSize": "1.00", + # "minOrderInBaseAsset": "0.00006100", + # "minOrderInQuoteAsset": "5.00", + # "maxOrderInBaseAsset": "1000000000.00000000", + # "maxOrderInQuoteAsset": "1000000000.00", + # "quantityDecimals": "8", + # "notionalDecimals": "2", + # "maxOpenOrders": "100", + # "feeCategory": "A", + # "orderTypes": ["market", "limit", "stopLoss", "stopLossLimit", "takeProfit", "takeProfitLimit"] + # } + # + return self.parse_markets(response) + + def parse_markets(self, markets): + result = [] + fees = self.fees + for i in range(0, len(markets)): + market = markets[i] + id = self.safe_string(market, 'market') + baseId = self.safe_string(market, 'base') + quoteId = self.safe_string(market, 'quote') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + status = self.safe_string(market, 'status') + result.append(self.safe_market_structure({ + 'id': id, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': (status == 'trading'), + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'taker': fees['trading']['taker'], + 'maker': fees['trading']['maker'], + 'precision': { + 'amount': self.parse_number(self.parse_precision(self.safe_string(market, 'quantityDecimals'))), + 'price': self.safe_number(market, 'tickSize'), + 'cost': self.parse_number(self.parse_precision(self.safe_string(market, 'notionalDecimals'))), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(market, 'minOrderInBaseAsset'), + 'max': self.safe_number(market, 'maxOrderInBaseAsset'), + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': self.safe_number(market, 'minOrderInQuoteAsset'), + 'max': self.safe_number(market, 'maxOrderInQuoteAsset'), + }, + }, + 'created': None, + 'info': market, + })) + return result + + def fetch_currencies(self, params={}) -> Currencies: + """ + + https://docs.bitvavo.com/#tag/General/paths/~1assets/get + + fetches all available currencies on an exchange + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = self.publicGetAssets(params) + # + # [ + # { + # "symbol": "USDT", + # "displayTicker": "USDT", + # "name": "Tether", + # "slug": "tether", + # "popularity": -1, + # "decimals": 6, + # "depositFee": "0", + # "depositConfirmations": 64, + # "depositStatus": "OK", + # "withdrawalFee": "3.2", + # "withdrawalMinAmount": "3.2", + # "withdrawalStatus": "OK", + # "networks": [ + # "ETH" + # ], + # "light": { + # "color": "#009393", + # "icon": {"hash": "4ad7c699", "svg": "https://...", "webp16": "https://...", "webp32": "https://...", "webp64": "https://...", "webp128": "https://...", "webp256": "https://...", "png16": "https://...", "png32": "https://...", "png64": "https://...", "png128": "https://...", "png256": "https://..." + # } + # }, + # "dark": { + # "color": "#009393", + # "icon": {"hash": "4ad7c699", "svg": "https://...", "webp16": "https://...", "webp32": "https://...", "webp64": "https://...", "webp128": "https://...", "webp256": "https://...", "png16": "https://...", "png32": "https://...", "png64": "https://...", "png128": "https://...", "png256": "https://..." + # } + # }, + # "visibility": "PUBLIC", + # "message": "" + # }, + # ] + # + return self.parse_currencies_custom(response) + + def parse_currencies_custom(self, currencies): + # + # [ + # { + # "symbol": "USDT", + # "displayTicker": "USDT", + # "name": "Tether", + # "slug": "tether", + # "popularity": -1, + # "decimals": 6, + # "depositFee": "0", + # "depositConfirmations": 64, + # "depositStatus": "OK", + # "withdrawalFee": "3.2", + # "withdrawalMinAmount": "3.2", + # "withdrawalStatus": "OK", + # "networks": [ + # "ETH" + # ], + # "light": { + # "color": "#009393", + # "icon": {"hash": "4ad7c699", "svg": "https://...", "webp16": "https://...", "webp32": "https://...", "webp64": "https://...", "webp128": "https://...", "webp256": "https://...", "png16": "https://...", "png32": "https://...", "png64": "https://...", "png128": "https://...", "png256": "https://..." + # } + # }, + # "dark": { + # "color": "#009393", + # "icon": {"hash": "4ad7c699", "svg": "https://...", "webp16": "https://...", "webp32": "https://...", "webp64": "https://...", "webp128": "https://...", "webp256": "https://...", "png16": "https://...", "png32": "https://...", "png64": "https://...", "png128": "https://...", "png256": "https://..." + # } + # }, + # "visibility": "PUBLIC", + # "message": "" + # }, + # ] + # + fiatCurrencies = self.safe_list(self.options, 'fiatCurrencies', []) + result: dict = {} + for i in range(0, len(currencies)): + currency = currencies[i] + id = self.safe_string(currency, 'symbol') + code = self.safe_currency_code(id) + isFiat = self.in_array(code, fiatCurrencies) + networks: dict = {} + networksArray = self.safe_list(currency, 'networks', []) + deposit = self.safe_string(currency, 'depositStatus') == 'OK' + withdrawal = self.safe_string(currency, 'withdrawalStatus') == 'OK' + active = deposit and withdrawal + withdrawFee = self.safe_number(currency, 'withdrawalFee') + precision = self.safe_string(currency, 'decimals', '8') + minWithdraw = self.safe_number(currency, 'withdrawalMinAmount') + # btw, absolutely all of them have 1 network atm + for j in range(0, len(networksArray)): + networkId = networksArray[j] + networkCode = self.network_id_to_code(networkId) + networks[networkCode] = { + 'info': currency, + 'id': networkId, + 'network': networkCode, + 'active': active, + 'deposit': deposit, + 'withdraw': withdrawal, + 'fee': withdrawFee, + 'precision': self.parse_number(self.parse_precision(precision)), + 'limits': { + 'withdraw': { + 'min': minWithdraw, + 'max': None, + }, + }, + } + result[code] = self.safe_currency_structure({ + 'info': currency, + 'id': id, + 'code': code, + 'name': self.safe_string(currency, 'name'), + 'active': active, + 'deposit': deposit, + 'withdraw': withdrawal, + 'networks': networks, + 'fee': withdrawFee, + 'precision': None, + 'type': 'fiat' if isFiat else 'crypto', + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'deposit': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': minWithdraw, + 'max': None, + }, + }, + }) + return result + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + + https://docs.bitvavo.com/#tag/Market-Data/paths/~1ticker~124h/get + + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + response = self.publicGetTicker24h(self.extend(request, params)) + # + # { + # "market":"ETH-BTC", + # "open":"0.022578", + # "high":"0.023019", + # "low":"0.022572", + # "last":"0.023019", + # "volume":"25.16366324", + # "volumeQuote":"0.57333305", + # "bid":"0.023039", + # "bidSize":"0.53500578", + # "ask":"0.023041", + # "askSize":"0.47859202", + # "timestamp":1590381666900 + # } + # + return self.parse_ticker(response, market) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # fetchTicker + # + # { + # "market":"ETH-BTC", + # "open":"0.022578", + # "high":"0.023019", + # "low":"0.022573", + # "last":"0.023019", + # "volume":"25.16366324", + # "volumeQuote":"0.57333305", + # "bid":"0.023039", + # "bidSize":"0.53500578", + # "ask":"0.023041", + # "askSize":"0.47859202", + # "timestamp":1590381666900 + # } + # + marketId = self.safe_string(ticker, 'market') + symbol = self.safe_symbol(marketId, market, '-') + timestamp = self.safe_integer(ticker, 'timestamp') + last = self.safe_string(ticker, 'last') + baseVolume = self.safe_string(ticker, 'volume') + quoteVolume = self.safe_string(ticker, 'volumeQuote') + open = self.safe_string(ticker, 'open') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': self.safe_string(ticker, 'bid'), + 'bidVolume': self.safe_string(ticker, 'bidSize'), + 'ask': self.safe_string(ticker, 'ask'), + 'askVolume': self.safe_string(ticker, 'askSize'), + 'vwap': None, + 'open': open, + 'close': last, + 'last': last, + 'previousClose': None, # previous day close + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'info': ticker, + }, market) + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + response = self.publicGetTicker24h(params) + # + # [ + # { + # "market":"ADA-BTC", + # "open":"0.0000059595", + # "high":"0.0000059765", + # "low":"0.0000059595", + # "last":"0.0000059765", + # "volume":"2923.172", + # "volumeQuote":"0.01743483", + # "bid":"0.0000059515", + # "bidSize":"1117.630919", + # "ask":"0.0000059585", + # "askSize":"809.999739", + # "timestamp":1590382266324 + # } + # ] + # + return self.parse_tickers(response, symbols) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + + https://docs.bitvavo.com/#tag/Market-Data/paths/~1{market}~1trades/get + + get the list of most recent trades for a particular symbol + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch entries for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchTrades', 'paginate') + if paginate: + return self.fetch_paginated_call_dynamic('fetchTrades', symbol, since, limit, params) + request: dict = { + 'market': market['id'], + # "limit": 500, # default 500, max 1000 + # "start": since, + # "end": self.milliseconds(), + # "tradeIdFrom": "57b1159b-6bf5-4cde-9e2c-6bd6a5678baf", + # "tradeIdTo": "57b1159b-6bf5-4cde-9e2c-6bd6a5678baf", + } + if limit is not None: + request['limit'] = min(limit, 1000) + if since is not None: + request['start'] = since + request, params = self.handle_until_option('end', request, params) + response = self.publicGetMarketTrades(self.extend(request, params)) + # + # [ + # { + # "id":"94154c98-6e8b-4e33-92a8-74e33fc05650", + # "timestamp":1590382761859, + # "amount":"0.06026079", + # "price":"8095.3", + # "side":"buy" + # } + # ] + # + return self.parse_trades(response, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(public) + # + # { + # "id":"94154c98-6e8b-4e33-92a8-74e33fc05650", + # "timestamp":1590382761859, + # "amount":"0.06026079", + # "price":"8095.3", + # "side":"buy" + # } + # + # createOrder, fetchOpenOrders, fetchOrders, editOrder(private) + # + # { + # "id":"b0c86aa5-6ed3-4a2d-ba3a-be9a964220f4", + # "timestamp":1590505649245, + # "amount":"0.249825", + # "price":"183.49", + # "taker":true, + # "fee":"0.12038925", + # "feeCurrency":"EUR", + # "settled":true + # } + # + # fetchMyTrades(private) + # + # { + # "id":"b0c86aa5-6ed3-4a2d-ba3a-be9a964220f4", + # "orderId":"af76d6ce-9f7c-4006-b715-bb5d430652d0", + # "timestamp":1590505649245, + # "market":"ETH-EUR", + # "side":"sell", + # "amount":"0.249825", + # "price":"183.49", + # "taker":true, + # "fee":"0.12038925", + # "feeCurrency":"EUR", + # "settled":true + # } + # + # watchMyTrades(private) + # + # { + # "event": "fill", + # "timestamp": 1590964470132, + # "market": "ETH-EUR", + # "orderId": "85d082e1-eda4-4209-9580-248281a29a9a", + # "fillId": "861d2da5-aa93-475c-8d9a-dce431bd4211", + # "side": "sell", + # "amount": "0.1", + # "price": "211.46", + # "taker": True, + # "fee": "0.056", + # "feeCurrency": "EUR" + # } + # + priceString = self.safe_string(trade, 'price') + amountString = self.safe_string(trade, 'amount') + timestamp = self.safe_integer(trade, 'timestamp') + side = self.safe_string(trade, 'side') + id = self.safe_string_2(trade, 'id', 'fillId') + marketId = self.safe_string(trade, 'market') + symbol = self.safe_symbol(marketId, market, '-') + taker = self.safe_value(trade, 'taker') + takerOrMaker = None + if taker is not None: + takerOrMaker = 'taker' if taker else 'maker' + feeCostString = self.safe_string(trade, 'fee') + fee = None + if feeCostString is not None: + feeCurrencyId = self.safe_string(trade, 'feeCurrency') + feeCurrencyCode = self.safe_currency_code(feeCurrencyId) + fee = { + 'cost': feeCostString, + 'currency': feeCurrencyCode, + } + orderId = self.safe_string(trade, 'orderId') + return self.safe_trade({ + 'info': trade, + 'id': id, + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'order': orderId, + 'type': None, + 'side': side, + 'takerOrMaker': takerOrMaker, + 'price': priceString, + 'amount': amountString, + 'cost': None, + 'fee': fee, + }, market) + + def fetch_trading_fees(self, params={}) -> TradingFees: + """ + + https://docs.bitvavo.com/#tag/Account/paths/~1account/get + + fetch the trading fees for multiple markets + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + self.load_markets() + response = self.privateGetAccount(params) + # + # { + # "fees": { + # "taker": "0.0025", + # "maker": "0.0015", + # "volume": "10000.00" + # } + # } + # + return self.parse_trading_fees(response) + + def parse_trading_fees(self, fees, market=None): + # + # { + # "fees": { + # "taker": "0.0025", + # "maker": "0.0015", + # "volume": "10000.00" + # } + # } + # + feesValue = self.safe_value(fees, 'fees') + maker = self.safe_number(feesValue, 'maker') + taker = self.safe_number(feesValue, 'taker') + result: dict = {} + for i in range(0, len(self.symbols)): + symbol = self.symbols[i] + result[symbol] = { + 'info': fees, + 'symbol': symbol, + 'maker': maker, + 'taker': taker, + 'percentage': True, + 'tierBased': True, + } + return result + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + + https://docs.bitvavo.com/#tag/Market-Data/paths/~1{market}~1book/get + + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + if limit is not None: + request['depth'] = limit + response = self.publicGetMarketBook(self.extend(request, params)) + # + # { + # "market":"BTC-EUR", + # "nonce":35883831, + # "bids":[ + # ["8097.4","0.6229099"], + # ["8097.2","0.64151283"], + # ["8097.1","0.24966294"], + # ], + # "asks":[ + # ["8097.5","1.36916911"], + # ["8098.8","0.33462248"], + # ["8099.3","1.12908646"], + # ] + # } + # + orderbook = self.parse_order_book(response, market['symbol']) + orderbook['nonce'] = self.safe_integer(response, 'nonce') + return orderbook + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # [ + # 1590383700000, + # "8088.5", + # "8088.5", + # "8088.5", + # "8088.5", + # "0.04788623" + # ] + # + return [ + self.safe_integer(ohlcv, 0), + self.safe_number(ohlcv, 1), + self.safe_number(ohlcv, 2), + self.safe_number(ohlcv, 3), + self.safe_number(ohlcv, 4), + self.safe_number(ohlcv, 5), + ] + + def fetch_ohlcv_request(self, symbol: Str, timeframe='1m', since: Int = None, limit: Int = None, params={}): + market = self.market(symbol) + request: dict = { + 'market': market['id'], + 'interval': self.safe_string(self.timeframes, timeframe, timeframe), + # "limit": 1440, # default 1440, max 1440 + # "start": since, + # "end": self.milliseconds(), + } + if since is not None: + # https://github.com/ccxt/ccxt/issues/9227 + duration = self.parse_timeframe(timeframe) + request['start'] = since + if limit is None: + limit = 1440 + else: + limit = min(limit, 1440) + request['end'] = self.sum(since, limit * duration * 1000) + request, params = self.handle_until_option('end', request, params) + if limit is not None: + request['limit'] = limit # default 1440, max 1440 + return self.extend(request, params) + + def fetch_ohlcv(self, symbol: Str, timeframe='1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + + https://docs.bitvavo.com/#tag/Market-Data/paths/~1{market}~1candles/get + + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch entries for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + market = self.market(symbol) + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate') + if paginate: + return self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params, 1440) + request = self.fetch_ohlcv_request(symbol, timeframe, since, limit, params) + response = self.publicGetMarketCandles(request) + # + # [ + # [1590383700000,"8088.5","8088.5","8088.5","8088.5","0.04788623"], + # [1590383580000,"8091.3","8091.5","8091.3","8091.5","0.04931221"], + # [1590383520000,"8090.3","8092.7","8090.3","8092.5","0.04001286"], + # ] + # + return self.parse_ohlcvs(response, market, timeframe, since, limit) + + def parse_balance(self, response) -> Balances: + result: dict = { + 'info': response, + 'timestamp': None, + 'datetime': None, + } + for i in range(0, len(response)): + balance = response[i] + currencyId = self.safe_string(balance, 'symbol') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(balance, 'available') + account['used'] = self.safe_string(balance, 'inOrder') + result[code] = account + return self.safe_balance(result) + + def fetch_balance(self, params={}) -> Balances: + """ + + https://docs.bitvavo.com/#tag/Account/paths/~1balance/get + + query for balance and get the amount of funds available for trading or funds locked in orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + response = self.privateGetBalance(params) + # + # [ + # { + # "symbol": "BTC", + # "available": "1.57593193", + # "inOrder": "0.74832374" + # } + # ] + # + return self.parse_balance(response) + + def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'symbol': currency['id'], + } + response = self.privateGetDeposit(self.extend(request, params)) + # + # { + # "address": "0x449889e3234514c45d57f7c5a571feba0c7ad567", + # "paymentId": "10002653" + # } + # + address = self.safe_string(response, 'address') + tag = self.safe_string(response, 'paymentId') + self.check_address(address) + return { + 'info': response, + 'currency': code, + 'network': None, + 'address': address, + 'tag': tag, + } + + def create_order_request(self, symbol: Str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + market = self.market(symbol) + request: dict = { + 'market': market['id'], + 'side': side, + 'orderType': type, + } + isMarketOrder = (type == 'market') or (type == 'stopLoss') or (type == 'takeProfit') + isLimitOrder = (type == 'limit') or (type == 'stopLossLimit') or (type == 'takeProfitLimit') + timeInForce = self.safe_string(params, 'timeInForce') + triggerPrice = self.safe_string_n(params, ['triggerPrice', 'stopPrice', 'triggerAmount']) + postOnly = self.is_post_only(isMarketOrder, False, params) + stopLossPrice = self.safe_value(params, 'stopLossPrice') # trigger when price crosses from above to below self value + takeProfitPrice = self.safe_value(params, 'takeProfitPrice') # trigger when price crosses from below to above self value + params = self.omit(params, ['timeInForce', 'triggerPrice', 'stopPrice', 'stopLossPrice', 'takeProfitPrice']) + if isMarketOrder: + cost = None + if price is not None: + priceString = self.number_to_string(price) + amountString = self.number_to_string(amount) + quoteAmount = Precise.string_mul(amountString, priceString) + cost = self.parse_number(quoteAmount) + else: + cost = self.safe_number(params, 'cost') + if cost is not None: + precision = self.currency(market['quote'])['precision'] + request['amountQuote'] = self.decimal_to_precision(cost, TRUNCATE, precision, self.precisionMode) + else: + request['amount'] = self.amount_to_precision(symbol, amount) + params = self.omit(params, ['cost']) + elif isLimitOrder: + request['price'] = self.price_to_precision(symbol, price) + request['amount'] = self.amount_to_precision(symbol, amount) + isTakeProfit = (takeProfitPrice is not None) or (type == 'takeProfit') or (type == 'takeProfitLimit') + isStopLoss = (stopLossPrice is not None) or (triggerPrice is not None) and (not isTakeProfit) or (type == 'stopLoss') or (type == 'stopLossLimit') + if isStopLoss: + if stopLossPrice is not None: + triggerPrice = stopLossPrice + request['orderType'] = 'stopLoss' if isMarketOrder else 'stopLossLimit' + elif isTakeProfit: + if takeProfitPrice is not None: + triggerPrice = takeProfitPrice + request['orderType'] = 'takeProfit' if isMarketOrder else 'takeProfitLimit' + if triggerPrice is not None: + request['triggerAmount'] = self.price_to_precision(symbol, triggerPrice) + request['triggerType'] = 'price' + request['triggerReference'] = 'lastTrade' # 'bestBid', 'bestAsk', 'midPrice' + if (timeInForce is not None) and (timeInForce != 'PO'): + request['timeInForce'] = timeInForce + if postOnly: + request['postOnly'] = True + operatorId = None + operatorId, params = self.handle_option_and_params(params, 'createOrder', 'operatorId') + if operatorId is not None: + request['operatorId'] = self.parse_to_int(operatorId) + else: + raise ArgumentsRequired(self.id + ' createOrder() requires an operatorId in params or options, eg: exchange.options[\'operatorId\'] = 1234567890') + selfTradePrevention = None + selfTradePrevention, params = self.handle_option_and_params(params, 'createOrder', 'selfTradePrevention') + if selfTradePrevention is not None: + if selfTradePrevention == 'EXPIRE_BOTH': + request['selfTradePrevention'] = 'cancelBoth' + else: + request['selfTradePrevention'] = selfTradePrevention + return self.extend(request, params) + + def create_order(self, symbol: Str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://docs.bitvavo.com/#tag/Trading-endpoints/paths/~1order/post + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float price: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the bitvavo api endpoint + :param str [params.timeInForce]: "GTC", "IOC", or "PO" + :param float [params.stopPrice]: Alias for triggerPrice + :param float [params.triggerPrice]: The price at which a trigger order is triggered at + :param bool [params.postOnly]: If True, the order will only be posted to the order book and not executed immediately + :param float [params.stopLossPrice]: The price at which a stop loss order is triggered at + :param float [params.takeProfitPrice]: The price at which a take profit order is triggered at + :param str [params.triggerType]: "price" + :param str [params.triggerReference]: "lastTrade", "bestBid", "bestAsk", "midPrice" Only for stop orders: Use self to determine which parameter will trigger the order + :param str [params.selfTradePrevention]: one of EXPIRE_BOTH, cancelOldest, cancelNewest or decrementAndCancel + :param bool [params.disableMarketProtection]: don't cancel if the next fill price is 10% worse than the best fill price + :param bool [params.responseRequired]: Set self to 'false' when only an acknowledgement of success or failure is required, self is faster. + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + request = self.create_order_request(symbol, type, side, amount, price, params) + response = self.privatePostOrder(request) + # + # { + # "orderId":"dec6a640-5b4c-45bc-8d22-3b41c6716630", + # "market":"DOGE-EUR", + # "created":1654789135146, + # "updated":1654789135153, + # "status":"new", + # "side":"buy", + # "orderType":"stopLossLimit", + # "amount":"200", + # "amountRemaining":"200", + # "price":"0.07471", + # "triggerPrice":"0.0747", + # "triggerAmount":"0.0747", + # "triggerType":"price", + # "triggerReference":"lastTrade", + # "onHold":"14.98", + # "onHoldCurrency":"EUR", + # "filledAmount":"0", + # "filledAmountQuote":"0", + # "feePaid":"0", + # "feeCurrency":"EUR", + # "fills":[ # filled with market orders only + # { + # "id":"b0c86aa5-6ed3-4a2d-ba3a-be9a964220f4", + # "timestamp":1590505649245, + # "amount":"0.249825", + # "price":"183.49", + # "taker":true, + # "fee":"0.12038925", + # "feeCurrency":"EUR", + # "settled":true + # } + # ], + # "selfTradePrevention":"decrementAndCancel", + # "visible":true, + # "timeInForce":"GTC", + # "postOnly":false + # } + # + return self.parse_order(response, market) + + def edit_order_request(self, id: str, symbol, type, side, amount=None, price=None, params={}): + request: dict = {} + market = self.market(symbol) + amountRemaining = self.safe_number(params, 'amountRemaining') + triggerPrice = self.safe_string_n(params, ['triggerPrice', 'stopPrice', 'triggerAmount']) + params = self.omit(params, ['amountRemaining', 'triggerPrice', 'stopPrice', 'triggerAmount']) + if price is not None: + request['price'] = self.price_to_precision(symbol, price) + if amount is not None: + request['amount'] = self.amount_to_precision(symbol, amount) + if amountRemaining is not None: + request['amountRemaining'] = self.amount_to_precision(symbol, amountRemaining) + if triggerPrice is not None: + request['triggerAmount'] = self.price_to_precision(symbol, triggerPrice) + request = self.extend(request, params) + if self.is_empty(request): + raise ArgumentsRequired(self.id + ' editOrder() requires an amount argument, or a price argument, or non-empty params') + clientOrderId = self.safe_string(params, 'clientOrderId') + if clientOrderId is None: + request['orderId'] = id + operatorId = None + operatorId, params = self.handle_option_and_params(params, 'editOrder', 'operatorId') + if operatorId is not None: + request['operatorId'] = self.parse_to_int(operatorId) + else: + raise ArgumentsRequired(self.id + ' editOrder() requires an operatorId in params or options, eg: exchange.options[\'operatorId\'] = 1234567890') + request['market'] = market['id'] + return request + + def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + """ + edit a trade order + + https://docs.bitvavo.com/#tag/Orders/paths/~1order/put + + :param str id: cancel order id + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float [amount]: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the bitvavo api endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + request = self.edit_order_request(id, symbol, type, side, amount, price, params) + response = self.privatePutOrder(request) + return self.parse_order(response, market) + + def cancel_order_request(self, id: Str, symbol: Str = None, params={}): + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + clientOrderId = self.safe_string(params, 'clientOrderId') + if clientOrderId is None: + request['orderId'] = id + operatorId = None + operatorId, params = self.handle_option_and_params(params, 'cancelOrder', 'operatorId') + if operatorId is not None: + request['operatorId'] = self.parse_to_int(operatorId) + else: + raise ArgumentsRequired(self.id + ' cancelOrder() requires an operatorId in params or options, eg: exchange.options[\'operatorId\'] = 1234567890') + return self.extend(request, params) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + + https://docs.bitvavo.com/#tag/Orders/paths/~1order/delete + + cancels an open order + + https://docs.bitvavo.com/#tag/Trading-endpoints/paths/~1order/delete + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + market = self.market(symbol) + request = self.cancel_order_request(id, symbol, params) + response = self.privateDeleteOrder(request) + # + # { + # "orderId": "2e7ce7fc-44e2-4d80-a4a7-d079c4750b61" + # } + # + return self.parse_order(response, market) + + def cancel_all_orders(self, symbol: Str = None, params={}): + """ + + https://docs.bitvavo.com/#tag/Orders/paths/~1orders/delete + + cancel all open orders + :param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['market'] = market['id'] + operatorId = None + operatorId, params = self.handle_option_and_params(params, 'cancelAllOrders', 'operatorId') + if operatorId is not None: + request['operatorId'] = self.parse_to_int(operatorId) + else: + raise ArgumentsRequired(self.id + ' canceAllOrders() requires an operatorId in params or options, eg: exchange.options[\'operatorId\'] = 1234567890') + response = self.privateDeleteOrders(self.extend(request, params)) + # + # [ + # { + # "orderId": "1be6d0df-d5dc-4b53-a250-3376f3b393e6" + # } + # ] + # + return self.parse_orders(response, market) + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://docs.bitvavo.com/#tag/Trading-endpoints/paths/~1order/get + + :param str id: the order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrder() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + clientOrderId = self.safe_string(params, 'clientOrderId') + if clientOrderId is None: + request['orderId'] = id + response = self.privateGetOrder(self.extend(request, params)) + # + # { + # "orderId":"af76d6ce-9f7c-4006-b715-bb5d430652d0", + # "market":"ETH-EUR", + # "created":1590505649241, + # "updated":1590505649241, + # "status":"filled", + # "side":"sell", + # "orderType":"market", + # "amount":"0.249825", + # "amountRemaining":"0", + # "onHold":"0", + # "onHoldCurrency":"ETH", + # "filledAmount":"0.249825", + # "filledAmountQuote":"45.84038925", + # "feePaid":"0.12038925", + # "feeCurrency":"EUR", + # "fills":[ + # { + # "id":"b0c86aa5-6ed3-4a2d-ba3a-be9a964220f4", + # "timestamp":1590505649245, + # "amount":"0.249825", + # "price":"183.49", + # "taker":true, + # "fee":"0.12038925", + # "feeCurrency":"EUR", + # "settled":true + # } + # ], + # "selfTradePrevention":"decrementAndCancel", + # "visible":false, + # "disableMarketProtection":false + # } + # + return self.parse_order(response, market) + + def fetch_orders_request(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + market = self.market(symbol) + request: dict = { + 'market': market['id'], + # "limit": 500, + # "start": since, + # "end": self.milliseconds(), + # "orderIdFrom": "af76d6ce-9f7c-4006-b715-bb5d430652d0", + # "orderIdTo": "af76d6ce-9f7c-4006-b715-bb5d430652d0", + } + if since is not None: + request['start'] = since + if limit is not None: + request['limit'] = limit # default 500, max 1000 + request, params = self.handle_until_option('end', request, params) + return self.extend(request, params) + + def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + + https://docs.bitvavo.com/#tag/Trading-endpoints/paths/~1orders/get + + fetches information on multiple orders made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param int [params.until]: the latest time in ms to fetch entries for + :returns Order[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrders() requires a symbol argument') + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOrders', 'paginate') + if paginate: + return self.fetch_paginated_call_dynamic('fetchOrders', symbol, since, limit, params) + market = self.market(symbol) + request = self.fetch_orders_request(symbol, since, limit, params) + response = self.privateGetOrders(request) + # + # [ + # { + # "orderId":"af76d6ce-9f7c-4006-b715-bb5d430652d0", + # "market":"ETH-EUR", + # "created":1590505649241, + # "updated":1590505649241, + # "status":"filled", + # "side":"sell", + # "orderType":"market", + # "amount":"0.249825", + # "amountRemaining":"0", + # "onHold":"0", + # "onHoldCurrency":"ETH", + # "filledAmount":"0.249825", + # "filledAmountQuote":"45.84038925", + # "feePaid":"0.12038925", + # "feeCurrency":"EUR", + # "fills":[ + # { + # "id":"b0c86aa5-6ed3-4a2d-ba3a-be9a964220f4", + # "timestamp":1590505649245, + # "amount":"0.249825", + # "price":"183.49", + # "taker":true, + # "fee":"0.12038925", + # "feeCurrency":"EUR", + # "settled":true + # } + # ], + # "selfTradePrevention":"decrementAndCancel", + # "visible":false, + # "disableMarketProtection":false + # } + # ] + # + return self.parse_orders(response, market, since, limit) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + + https://docs.bitvavo.com/#tag/Trading-endpoints/paths/~1ordersOpen/get + + fetch all unfilled currently open orders + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + request: dict = { + # "market": market["id"], # rate limit 25 without a market, 1 with market specified + } + market = None + if symbol is not None: + market = self.market(symbol) + request['market'] = market['id'] + response = self.privateGetOrdersOpen(self.extend(request, params)) + # + # [ + # { + # "orderId":"af76d6ce-9f7c-4006-b715-bb5d430652d0", + # "market":"ETH-EUR", + # "created":1590505649241, + # "updated":1590505649241, + # "status":"filled", + # "side":"sell", + # "orderType":"market", + # "amount":"0.249825", + # "amountRemaining":"0", + # "onHold":"0", + # "onHoldCurrency":"ETH", + # "filledAmount":"0.249825", + # "filledAmountQuote":"45.84038925", + # "feePaid":"0.12038925", + # "feeCurrency":"EUR", + # "fills":[ + # { + # "id":"b0c86aa5-6ed3-4a2d-ba3a-be9a964220f4", + # "timestamp":1590505649245, + # "amount":"0.249825", + # "price":"183.49", + # "taker":true, + # "fee":"0.12038925", + # "feeCurrency":"EUR", + # "settled":true + # } + # ], + # "selfTradePrevention":"decrementAndCancel", + # "visible":false, + # "disableMarketProtection":false + # } + # ] + # + return self.parse_orders(response, market, since, limit) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'new': 'open', + 'canceled': 'canceled', + 'canceledAuction': 'canceled', + 'canceledSelfTradePrevention': 'canceled', + 'canceledIOC': 'canceled', + 'canceledFOK': 'canceled', + 'canceledMarketProtection': 'canceled', + 'canceledPostOnly': 'canceled', + 'filled': 'closed', + 'partiallyFilled': 'open', + 'expired': 'canceled', + 'rejected': 'canceled', + 'awaitingTrigger': 'open', # https://github.com/ccxt/ccxt/issues/8489 + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # cancelOrder, cancelAllOrders + # + # { + # "orderId": "2e7ce7fc-44e2-4d80-a4a7-d079c4750b61" + # } + # + # createOrder, fetchOrder, fetchOpenOrders, fetchOrders, editOrder + # + # { + # "orderId":"af76d6ce-9f7c-4006-b715-bb5d430652d0", + # "market":"ETH-EUR", + # "created":1590505649241, + # "updated":1590505649241, + # "status":"filled", + # "side":"sell", + # "orderType":"market", + # "amount":"0.249825", + # "amountRemaining":"0", + # "price": "183.49", # limit orders only + # "onHold":"0", + # "onHoldCurrency":"ETH", + # "filledAmount":"0.249825", + # "filledAmountQuote":"45.84038925", + # "feePaid":"0.12038925", + # "feeCurrency":"EUR", + # "fills":[ + # { + # "id":"b0c86aa5-6ed3-4a2d-ba3a-be9a964220f4", + # "timestamp":1590505649245, + # "amount":"0.249825", + # "price":"183.49", + # "taker":true, + # "fee":"0.12038925", + # "feeCurrency":"EUR", + # "settled":true + # } + # ], + # "selfTradePrevention":"decrementAndCancel", + # "visible":false, + # "disableMarketProtection":false + # "timeInForce": "GTC", + # "postOnly": True, + # } + # + id = self.safe_string(order, 'orderId') + timestamp = self.safe_integer(order, 'created') + marketId = self.safe_string(order, 'market') + market = self.safe_market(marketId, market, '-') + symbol = market['symbol'] + status = self.parse_order_status(self.safe_string(order, 'status')) + side = self.safe_string(order, 'side') + type = self.safe_string(order, 'orderType') + price = self.safe_string(order, 'price') + amount = self.safe_string(order, 'amount') + remaining = self.safe_string(order, 'amountRemaining') + filled = self.safe_string(order, 'filledAmount') + cost = self.safe_string(order, 'filledAmountQuote') + if cost is None: + amountQuote = self.safe_string(order, 'amountQuote') + amountQuoteRemaining = self.safe_string(order, 'amountQuoteRemaining') + cost = Precise.string_sub(amountQuote, amountQuoteRemaining) + fee = None + feeCost = self.safe_number(order, 'feePaid') + if feeCost is not None: + feeCurrencyId = self.safe_string(order, 'feeCurrency') + feeCurrencyCode = self.safe_currency_code(feeCurrencyId) + fee = { + 'cost': feeCost, + 'currency': feeCurrencyCode, + } + rawTrades = self.safe_value(order, 'fills', []) + timeInForce = self.safe_string(order, 'timeInForce') + postOnly = self.safe_value(order, 'postOnly') + # https://github.com/ccxt/ccxt/issues/8489 + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'symbol': symbol, + 'type': type, + 'timeInForce': timeInForce, + 'postOnly': postOnly, + 'side': side, + 'price': price, + 'triggerPrice': self.safe_number(order, 'triggerPrice'), + 'amount': amount, + 'cost': cost, + 'average': None, + 'filled': filled, + 'remaining': remaining, + 'status': status, + 'fee': fee, + 'trades': rawTrades, + }, market) + + def fetch_my_trades_request(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + market = self.market(symbol) + request: dict = { + 'market': market['id'], + # "limit": 500, + # "start": since, + # "end": self.milliseconds(), + # "tradeIdFrom": "af76d6ce-9f7c-4006-b715-bb5d430652d0", + # "tradeIdTo": "af76d6ce-9f7c-4006-b715-bb5d430652d0", + } + if since is not None: + request['start'] = since + if limit is not None: + request['limit'] = limit # default 500, max 1000 + request, params = self.handle_until_option('end', request, params) + return self.extend(request, params) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + + https://docs.bitvavo.com/#tag/Trading-endpoints/paths/~1trades/get + + fetch all trades made by the user + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch entries for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Trade[]: a list of `trade structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMyTrades() requires a symbol argument') + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchMyTrades', 'paginate') + if paginate: + return self.fetch_paginated_call_dynamic('fetchMyTrades', symbol, since, limit, params) + market = self.market(symbol) + request = self.fetch_my_trades_request(symbol, since, limit, params) + response = self.privateGetTrades(request) + # + # [ + # { + # "id":"b0c86aa5-6ed3-4a2d-ba3a-be9a964220f4", + # "orderId":"af76d6ce-9f7c-4006-b715-bb5d430652d0", + # "timestamp":1590505649245, + # "market":"ETH-EUR", + # "side":"sell", + # "amount":"0.249825", + # "price":"183.49", + # "taker":true, + # "fee":"0.12038925", + # "feeCurrency":"EUR", + # "settled":true + # } + # ] + # + return self.parse_trades(response, market, since, limit) + + def withdraw_request(self, code: Str, amount, address, tag=None, params={}): + currency = self.currency(code) + request: dict = { + 'symbol': currency['id'], + 'amount': self.currency_to_precision(code, amount), + 'address': address, # address or IBAN + # 'internal': False, # transfer to another Bitvavo user address, no fees + # 'addWithdrawalFee': False, # True = add the fee on top, otherwise the fee is subtracted from the amount + } + if tag is not None: + request['paymentId'] = tag + return self.extend(request, params) + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.check_address(address) + self.load_markets() + currency = self.currency(code) + request = self.withdraw_request(code, amount, address, tag, params) + response = self.privatePostWithdrawal(request) + # + # { + # "success": True, + # "symbol": "BTC", + # "amount": "1.5" + # } + # + return self.parse_transaction(response, currency) + + def fetch_withdrawals_request(self, code: Str = None, since: Int = None, limit: Int = None, params={}): + request: dict = { + # 'symbol': currency['id'], + # 'limit': 500, # default 500, max 1000 + # 'start': since, + # 'end': self.milliseconds(), + } + currency = None + if code is not None: + currency = self.currency(code) + request['symbol'] = currency['id'] + if since is not None: + request['start'] = since + if limit is not None: + request['limit'] = limit # default 500, max 1000 + return self.extend(request, params) + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + + https://docs.bitvavo.com/#tag/Account/paths/~1withdrawalHistory/get + + fetch all withdrawals made from an account + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the bitvavo api endpoint + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + request = self.fetch_withdrawals_request(code, since, limit, params) + currency = None + if code is not None: + currency = self.currency(code) + response = self.privateGetWithdrawalHistory(request) + # + # [ + # { + # "timestamp":1590531212000, + # "symbol":"ETH", + # "amount":"0.091", + # "fee":"0.009", + # "status":"awaiting_bitvavo_inspection", + # "address":"0xe42b309f1eE9F0cbf7f54CcF3bc2159eBfA6735b", + # "paymentId": "10002653", + # "txId": "927b3ea50c5bb52c6854152d305dfa1e27fc01d10464cf10825d96d69d235eb3", + # } + # ] + # + return self.parse_transactions(response, currency, since, limit, {'type': 'withdrawal'}) + + def fetch_deposits_request(self, code: Str = None, since: Int = None, limit: Int = None, params={}): + request: dict = { + # 'symbol': currency['id'], + # 'limit': 500, # default 500, max 1000 + # 'start': since, + # 'end': self.milliseconds(), + } + currency = None + if code is not None: + currency = self.currency(code) + request['symbol'] = currency['id'] + if since is not None: + request['start'] = since + if limit is not None: + request['limit'] = limit # default 500, max 1000 + return self.extend(request, params) + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + + https://docs.bitvavo.com/#tag/Account/paths/~1depositHistory/get + + fetch all deposits made to an account + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the bitvavo api endpoint + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + request = self.fetch_deposits_request(code, since, limit, params) + currency = None + if code is not None: + currency = self.currency(code) + response = self.privateGetDepositHistory(request) + # + # [ + # { + # "timestamp":1590492401000, + # "symbol":"ETH", + # "amount":"0.249825", + # "fee":"0", + # "status":"completed", + # "txId":"0x5167b473fd37811f9ef22364c3d54726a859ef9d98934b3a1e11d7baa8d2c2e2" + # } + # ] + # + return self.parse_transactions(response, currency, since, limit, {'type': 'deposit'}) + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'awaiting_processing': 'pending', + 'awaiting_email_confirmation': 'pending', + 'awaiting_bitvavo_inspection': 'pending', + 'approved': 'pending', + 'sending': 'pending', + 'in_mempool': 'pending', + 'processed': 'pending', + 'completed': 'ok', + 'canceled': 'canceled', + } + return self.safe_string(statuses, status, status) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # withdraw + # + # { + # "success": True, + # "symbol": "BTC", + # "amount": "1.5" + # } + # + # fetchWithdrawals + # + # { + # "timestamp": 1542967486256, + # "symbol": "BTC", + # "amount": "0.99994", + # "address": "BitcoinAddress", + # "paymentId": "10002653", + # "txId": "927b3ea50c5bb52c6854152d305dfa1e27fc01d10464cf10825d96d69d235eb3", + # "fee": "0.00006", + # "status": "awaiting_processing" + # } + # + # fetchDeposits + # + # { + # "timestamp":1590492401000, + # "symbol":"ETH", + # "amount":"0.249825", + # "fee":"0", + # "status":"completed", + # "txId":"0x5167b473fd37811f9ef22364c3d54726a859ef9d98934b3a1e11d7baa8d2c2e2" + # } + # + id = None + timestamp = self.safe_integer(transaction, 'timestamp') + currencyId = self.safe_string(transaction, 'symbol') + code = self.safe_currency_code(currencyId, currency) + status = self.parse_transaction_status(self.safe_string(transaction, 'status')) + amount = self.safe_number(transaction, 'amount') + address = self.safe_string(transaction, 'address') + txid = self.safe_string(transaction, 'txId') + fee = None + feeCost = self.safe_number(transaction, 'fee') + if feeCost is not None: + fee = { + 'cost': feeCost, + 'currency': code, + } + type = None + if ('success' in transaction) or ('address' in transaction): + type = 'withdrawal' + else: + type = 'deposit' + tag = self.safe_string(transaction, 'paymentId') + return { + 'info': transaction, + 'id': id, + 'txid': txid, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'addressFrom': None, + 'address': address, + 'addressTo': address, + 'tagFrom': None, + 'tag': tag, + 'tagTo': tag, + 'type': type, + 'amount': amount, + 'currency': code, + 'status': status, + 'updated': None, + 'fee': fee, + 'network': None, + 'comment': None, + 'internal': None, + } + + def parse_deposit_withdraw_fee(self, fee, currency: Currency = None): + # + # { + # "symbol": "1INCH", + # "name": "1inch", + # "decimals": 8, + # "depositFee": "0", + # "depositConfirmations": 64, + # "depositStatus": "OK", + # "withdrawalFee": "6.1", + # "withdrawalMinAmount": "6.1", + # "withdrawalStatus": "OK", + # "networks": [ + # "ETH" + # ], + # "message": "" + # } + # + result: dict = { + 'info': fee, + 'withdraw': { + 'fee': self.safe_number(fee, 'withdrawalFee'), + 'percentage': False, + }, + 'deposit': { + 'fee': self.safe_number(fee, 'depositFee'), + 'percentage': False, + }, + 'networks': {}, + } + networks = self.safe_value(fee, 'networks') + networkId = self.safe_value(networks, 0) # Bitvavo currently only supports one network per currency + currencyCode = self.safe_string(currency, 'code') + if networkId == 'Mainnet': + networkId = currencyCode + networkCode = self.network_id_to_code(networkId, currencyCode) + result['networks'][networkCode] = { + 'deposit': result['deposit'], + 'withdraw': result['withdraw'], + } + return result + + def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}): + """ + fetch deposit and withdraw fees + + https://docs.bitvavo.com/#tag/General/paths/~1assets/get + + :param str[]|None codes: list of unified currency codes + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `fee structures ` + """ + self.load_markets() + response = self.publicGetAssets(params) + # + # [ + # { + # "symbol": "1INCH", + # "name": "1inch", + # "decimals": 8, + # "depositFee": "0", + # "depositConfirmations": 64, + # "depositStatus": "OK", + # "withdrawalFee": "6.1", + # "withdrawalMinAmount": "6.1", + # "withdrawalStatus": "OK", + # "networks": [ + # "ETH" + # ], + # "message": "" + # }, + # ] + # + return self.parse_deposit_withdraw_fees(response, codes, 'symbol') + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + query = self.omit(params, self.extract_params(path)) + url = '/' + self.version + '/' + self.implode_params(path, params) + getOrDelete = (method == 'GET') or (method == 'DELETE') + if getOrDelete: + if query: + url += '?' + self.urlencode(query) + if api == 'private': + self.check_required_credentials() + payload = '' + if not getOrDelete: + if query: + body = self.json(query) + payload = body + timestamp = str(self.milliseconds()) + auth = timestamp + method + url + payload + signature = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256) + accessWindow = self.safe_string(self.options, 'BITVAVO-ACCESS-WINDOW', '10000') + headers = { + 'BITVAVO-ACCESS-KEY': self.apiKey, + 'BITVAVO-ACCESS-SIGNATURE': signature, + 'BITVAVO-ACCESS-TIMESTAMP': timestamp, + 'BITVAVO-ACCESS-WINDOW': accessWindow, + } + if not getOrDelete: + headers['Content-Type'] = 'application/json' + url = self.urls['api'][api] + url + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None # fallback to default error handler + # + # {"errorCode":308,"error":"The signature length is invalid(HMAC-SHA256 should return a 64 length hexadecimal string)."} + # {"errorCode":203,"error":"symbol parameter is required."} + # {"errorCode":205,"error":"symbol parameter is invalid."} + # + errorCode = self.safe_string(response, 'errorCode') + error = self.safe_string(response, 'error') + if errorCode is not None: + feedback = self.id + ' ' + body + self.throw_broadly_matched_exception(self.exceptions['broad'], error, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + raise ExchangeError(feedback) # unknown message + return None + + def calculate_rate_limiter_cost(self, api, method, path, params, config={}): + if ('noMarket' in config) and not ('market' in params): + return config['noMarket'] + return self.safe_value(config, 'cost', 1) diff --git a/ccxt/blockchaincom.py b/ccxt/blockchaincom.py new file mode 100644 index 0000000..6cb15a8 --- /dev/null +++ b/ccxt/blockchaincom.py @@ -0,0 +1,1219 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.blockchaincom import ImplicitAPI +from ccxt.base.types import Any, Balances, Currency, DepositAddress, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, TradingFees, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import OrderNotFound +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class blockchaincom(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(blockchaincom, self).describe(), { + 'id': 'blockchaincom', + 'secret': None, + 'name': 'Blockchain.com', + 'countries': ['LX'], + 'rateLimit': 500, # prev 1000 + 'version': 'v3', + 'pro': True, + 'has': { + 'CORS': False, + 'spot': True, + 'margin': None, # on exchange but not implemented in CCXT + 'swap': False, + 'future': False, + 'option': False, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'createOrder': True, + 'createStopLimitOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'fetchBalance': True, + 'fetchCanceledOrders': True, + 'fetchClosedOrders': True, + 'fetchCurrencies': False, + 'fetchDeposit': True, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchFundingHistory': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchIndexOHLCV': False, + 'fetchL2OrderBook': True, + 'fetchL3OrderBook': True, + 'fetchLedger': False, + 'fetchMarginMode': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMyTrades': True, + 'fetchOHLCV': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchPositionMode': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTrades': False, + 'fetchTradingFee': False, + 'fetchTradingFees': True, + 'fetchTransfer': False, + 'fetchTransfers': False, + 'fetchWithdrawal': True, + 'fetchWithdrawals': True, + 'fetchWithdrawalWhitelist': True, # fetches exchange specific benficiary-ids needed for withdrawals + 'transfer': False, + 'withdraw': True, + }, + 'timeframes': None, + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/975e3054-3399-4363-bcee-ec3c6d63d4e8', + 'test': { + 'public': 'https://testnet-api.delta.exchange', + 'private': 'https://testnet-api.delta.exchange', + }, + 'api': { + 'public': 'https://api.blockchain.com/v3/exchange', + 'private': 'https://api.blockchain.com/v3/exchange', + }, + 'www': 'https://blockchain.com', + 'doc': [ + 'https://api.blockchain.com/v3', + ], + 'fees': 'https://exchange.blockchain.com/fees', + }, + 'api': { + 'public': { + 'get': { + 'tickers': 1, # fetchTickers + 'tickers/{symbol}': 1, # fetchTicker + 'symbols': 1, # fetchMarkets + 'symbols/{symbol}': 1, # fetchMarket + 'l2/{symbol}': 1, # fetchL2OrderBook + 'l3/{symbol}': 1, # fetchL3OrderBook + }, + }, + 'private': { + 'get': { + 'fees': 1, # fetchFees + 'orders': 1, # fetchOpenOrders, fetchClosedOrders + 'orders/{orderId}': 1, # fetchOrder(id) + 'trades': 1, + 'fills': 1, # fetchMyTrades + 'deposits': 1, # fetchDeposits + 'deposits/{depositId}': 1, # fetchDeposit + 'accounts': 1, # fetchBalance + 'accounts/{account}/{currency}': 1, + 'whitelist': 1, # fetchWithdrawalWhitelist + 'whitelist/{currency}': 1, # fetchWithdrawalWhitelistByCurrency + 'withdrawals': 1, # fetchWithdrawalWhitelist + 'withdrawals/{withdrawalId}': 1, # fetchWithdrawalById + }, + 'post': { + 'orders': 1, # createOrder + 'deposits/{currency}': 1, # fetchDepositAddress by currency(only crypto supported) + 'withdrawals': 1, # withdraw + }, + 'delete': { + 'orders': 1, # cancelOrders + 'orders/{orderId}': 1, # cancelOrder + }, + }, + }, + 'fees': { + 'trading': { + 'feeSide': 'get', + 'tierBased': True, + 'percentage': True, + 'tiers': { + 'taker': [ + [self.parse_number('0'), self.parse_number('0.0045')], + [self.parse_number('10000'), self.parse_number('0.0035')], + [self.parse_number('50000'), self.parse_number('0.0018')], + [self.parse_number('100000'), self.parse_number('0.0018')], + [self.parse_number('500000'), self.parse_number('0.0018')], + [self.parse_number('1000000'), self.parse_number('0.0018')], + [self.parse_number('2500000'), self.parse_number('0.0018')], + [self.parse_number('5000000'), self.parse_number('0.0016')], + [self.parse_number('25000000'), self.parse_number('0.0014')], + [self.parse_number('100000000'), self.parse_number('0.0011')], + [self.parse_number('500000000'), self.parse_number('0.0008')], + [self.parse_number('1000000000'), self.parse_number('0.0006')], + ], + 'maker': [ + [self.parse_number('0'), self.parse_number('0.004')], + [self.parse_number('10000'), self.parse_number('0.0017')], + [self.parse_number('50000'), self.parse_number('0.0015')], + [self.parse_number('100000'), self.parse_number('0.0008')], + [self.parse_number('500000'), self.parse_number('0.0007')], + [self.parse_number('1000000'), self.parse_number('0.0006')], + [self.parse_number('2500000'), self.parse_number('0.0005')], + [self.parse_number('5000000'), self.parse_number('0.0004')], + [self.parse_number('25000000'), self.parse_number('0.0003')], + [self.parse_number('100000000'), self.parse_number('0.0002')], + [self.parse_number('500000000'), self.parse_number('0.0001')], + [self.parse_number('1000000000'), self.parse_number('0')], + ], + }, + }, + }, + 'requiredCredentials': { + 'apiKey': False, + 'secret': True, + }, + 'options': { + 'networks': { + 'ERC20': 'ETH', + 'TRC20': 'TRX', + 'ALGO': 'ALGO', + 'ADA': 'ADA', + 'AR': 'AR', + 'ATOM': 'ATOM', + 'AVAXC': 'AVAX', + 'BCH': 'BCH', + 'BSV': 'BSV', + 'BTC': 'BTC', + # 'BEP20': 'BNB', # todo + 'DCR': 'DCR', + 'DESO': 'DESO', + 'DASH': 'DASH', + 'CELO': 'CELO', + 'CHZ': 'CHZ', + 'MATIC': 'MATIC', + 'SOL': 'SOL', + 'DOGE': 'DOGE', + 'DOT': 'DOT', + 'EOS': 'EOS', + 'ETC': 'ETC', + 'FIL': 'FIL', + 'KAVA': 'KAVA', + 'LTC': 'LTC', + 'IOTA': 'MIOTA', + 'NEAR': 'NEAR', + 'STX': 'STX', + 'XLM': 'XLM', + 'XMR': 'XMR', + 'XRP': 'XRP', + 'XTZ': 'XTZ', + 'ZEC': 'ZEC', + 'ZIL': 'ZIL', + # 'THETA': 'THETA', # todo: possible TFUEL THETA FUEL is also same, but API might have a mistake + # todo: uncomment below after consensus + # 'MOBILECOIN': 'MOB', + # 'KIN': 'KIN', + # 'DIGITALGOLD': 'DGLD', + }, + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': False, + 'GTD': True, # todo implementation + }, + 'hedged': False, + 'leverage': False, + 'marketBuyRequiresPrice': False, + 'marketBuyByCost': False, + 'selfTradePrevention': False, + 'trailing': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 100000, # todo implementation + 'untilDays': 100000, # todo implementation + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'symbolRequired': False, + 'trailing': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 1000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, # todo implement + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 100000, + 'daysBackCanceled': 1, + 'untilDays': 100000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': None, # todo webapi + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'exact': { + '401': AuthenticationError, + '404': OrderNotFound, + }, + 'broad': {}, + }, + }) + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for blockchaincom + + https://api.blockchain.com/v3/#getsymbols + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + # + # "USDC-GBP": { + # "base_currency": "USDC", + # "base_currency_scale": 6, + # "counter_currency": "GBP", + # "counter_currency_scale": 2, + # "min_price_increment": 10000, + # "min_price_increment_scale": 8, + # "min_order_size": 500000000, + # "min_order_size_scale": 8, + # "max_order_size": 0, + # "max_order_size_scale": 8, + # "lot_size": 10000, + # "lot_size_scale": 8, + # "status": "open", + # "id": 68, + # "auction_price": 0, + # "auction_size": 0, + # "auction_time": "", + # "imbalance": 0 + # } + # + markets = self.publicGetSymbols(params) + marketIds = list(markets.keys()) + result = [] + for i in range(0, len(marketIds)): + marketId = marketIds[i] + market = self.safe_value(markets, marketId) + baseId = self.safe_string(market, 'base_currency') + quoteId = self.safe_string(market, 'counter_currency') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + numericId = self.safe_number(market, 'id') + active = None + marketState = self.safe_string(market, 'status') + if marketState == 'open': + active = True + else: + active = False + # price precision + minPriceIncrementString = self.safe_string(market, 'min_price_increment') + minPriceIncrementScaleString = self.safe_string(market, 'min_price_increment_scale') + minPriceScalePrecisionString = self.parse_precision(minPriceIncrementScaleString) + pricePrecisionString = Precise.string_mul(minPriceIncrementString, minPriceScalePrecisionString) + # amount precision + lotSizeString = self.safe_string(market, 'lot_size') + lotSizeScaleString = self.safe_string(market, 'lot_size_scale') + lotSizeScalePrecisionString = self.parse_precision(lotSizeScaleString) + amountPrecisionString = Precise.string_mul(lotSizeString, lotSizeScalePrecisionString) + # minimum order size + minOrderSizeString = self.safe_string(market, 'min_order_size') + minOrderSizeScaleString = self.safe_string(market, 'min_order_size_scale') + minOrderSizeScalePrecisionString = self.parse_precision(minOrderSizeScaleString) + minOrderSizePreciseString = Precise.string_mul(minOrderSizeString, minOrderSizeScalePrecisionString) + minOrderSize = self.parse_number(minOrderSizePreciseString) + # maximum order size + maxOrderSize = None + maxOrderSize = self.safe_string(market, 'max_order_size') + if maxOrderSize != '0': + maxOrderSizeScaleString = self.safe_string(market, 'max_order_size_scale') + maxOrderSizeScalePrecisionString = self.parse_precision(maxOrderSizeScaleString) + maxOrderSizeString = Precise.string_mul(maxOrderSize, maxOrderSizeScalePrecisionString) + maxOrderSize = self.parse_number(maxOrderSizeString) + else: + maxOrderSize = None + result.append({ + 'info': market, + 'id': marketId, + 'numericId': numericId, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': active, + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(amountPrecisionString), + 'price': self.parse_number(pricePrecisionString), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': minOrderSize, + 'max': maxOrderSize, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + }) + return result + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://api.blockchain.com/v3/#getl3orderbook + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + return self.fetch_l3_order_book(symbol, limit, params) + + def fetch_l3_order_book(self, symbol: str, limit: Int = None, params={}): + """ + fetches level 3 information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://api.blockchain.com/v3/#getl3orderbook + + :param str symbol: unified market symbol + :param int [limit]: max number of orders to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order book structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['depth'] = limit + response = self.publicGetL3Symbol(self.extend(request, params)) + return self.parse_order_book(response, market['symbol'], None, 'bids', 'asks', 'px', 'qty') + + def fetch_l2_order_book(self, symbol: str, limit: Int = None, params={}): + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['depth'] = limit + response = self.publicGetL2Symbol(self.extend(request, params)) + return self.parse_order_book(response, market['symbol'], None, 'bids', 'asks', 'px', 'qty') + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "symbol": "BTC-USD", + # "price_24h": 47791.86, + # "volume_24h": 362.88635738, + # "last_trade_price": 47587.75 + # } + # + marketId = self.safe_string(ticker, 'symbol') + symbol = self.safe_symbol(marketId, market, '-') + last = self.safe_string(ticker, 'last_trade_price') + baseVolume = self.safe_string(ticker, 'volume_24h') + open = self.safe_string(ticker, 'price_24h') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': None, + 'datetime': None, + 'high': None, + 'low': None, + 'bid': None, + 'bidVolume': None, + 'ask': None, + 'askVolume': None, + 'vwap': None, + 'open': open, + 'close': None, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': None, + 'info': ticker, + }, market) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://api.blockchain.com/v3/#gettickerbysymbol + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = self.publicGetTickersSymbol(self.extend(request, params)) + return self.parse_ticker(response, market) + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://api.blockchain.com/v3/#gettickers + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + tickers = self.publicGetTickers(params) + return self.parse_tickers(tickers, symbols) + + def parse_order_state(self, state): + states: dict = { + 'OPEN': 'open', + 'REJECTED': 'rejected', + 'FILLED': 'closed', + 'CANCELED': 'canceled', + 'PART_FILLED': 'open', + 'EXPIRED': 'expired', + } + return self.safe_string(states, state, state) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # { + # "clOrdId": "00001", + # "ordType": "MARKET", + # "ordStatus": "FILLED", + # "side": "BUY", + # "symbol": "USDC-USDT", + # "exOrdId": "281775861306290", + # "price": null, + # "text": "Fill", + # "lastShares": "30.0", + # "lastPx": "0.9999", + # "leavesQty": "0.0", + # "cumQty": "30.0", + # "avgPx": "0.9999", + # "timestamp": "1633940339619" + # } + # + clientOrderId = self.safe_string(order, 'clOrdId') + type = self.safe_string_lower(order, 'ordType') + statusId = self.safe_string(order, 'ordStatus') + state = self.parse_order_state(statusId) + side = self.safe_string_lower(order, 'side') + marketId = self.safe_string(order, 'symbol') + symbol = self.safe_symbol(marketId, market, '-') + exchangeOrderId = self.safe_string(order, 'exOrdId') + price = self.safe_string(order, 'price') if (type != 'market') else None + average = self.safe_number(order, 'avgPx') + timestamp = self.safe_integer(order, 'timestamp') + datetime = self.iso8601(timestamp) + filled = self.safe_string(order, 'cumQty') + remaining = self.safe_string(order, 'leavesQty') + result = self.safe_order({ + 'id': exchangeOrderId, + 'clientOrderId': clientOrderId, + 'datetime': datetime, + 'timestamp': timestamp, + 'lastTradeTimestamp': None, + 'status': state, + 'symbol': symbol, + 'type': type, + 'timeInForce': None, + 'side': side, + 'price': price, + 'average': average, + 'amount': None, + 'filled': filled, + 'remaining': remaining, + 'cost': None, + 'trades': [], + 'fees': [], + 'info': order, + }) + return result + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://api.blockchain.com/v3/#createorder + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + orderType = self.safe_string(params, 'ordType', type) + uppercaseOrderType = orderType.upper() + clientOrderId = self.safe_string_2(params, 'clientOrderId', 'clOrdId', self.uuid16()) + params = self.omit(params, ['ordType', 'clientOrderId', 'clOrdId']) + request: dict = { + # 'stopPx' : limit price + # 'timeInForce' : "GTC" for Good Till Cancel, "IOC" for Immediate or Cancel, "FOK" for Fill or Kill, "GTD" Good Till Date + # 'expireDate' : expiry date in the format YYYYMMDD + # 'minQty' : The minimum quantity required for an IOC fill + 'ordType': uppercaseOrderType, + 'symbol': market['id'], + 'side': side.upper(), + 'orderQty': self.amount_to_precision(symbol, amount), + 'clOrdId': clientOrderId, + } + triggerPrice = self.safe_value_n(params, ['triggerPrice', 'stopPx', 'stopPrice']) + params = self.omit(params, ['triggerPrice', 'stopPx', 'stopPrice']) + if uppercaseOrderType == 'STOP' or uppercaseOrderType == 'STOPLIMIT': + if triggerPrice is None: + raise ArgumentsRequired(self.id + ' createOrder() requires a stopPx or triggerPrice param for a ' + uppercaseOrderType + ' order') + if triggerPrice is not None: + if uppercaseOrderType == 'MARKET': + request['ordType'] = 'STOP' + elif uppercaseOrderType == 'LIMIT': + request['ordType'] = 'STOPLIMIT' + priceRequired = False + stopPriceRequired = False + if request['ordType'] == 'LIMIT' or request['ordType'] == 'STOPLIMIT': + priceRequired = True + if request['ordType'] == 'STOP' or request['ordType'] == 'STOPLIMIT': + stopPriceRequired = True + if priceRequired: + request['price'] = self.price_to_precision(symbol, price) + if stopPriceRequired: + request['stopPx'] = self.price_to_precision(symbol, triggerPrice) + response = self.privatePostOrders(self.extend(request, params)) + return self.parse_order(response, market) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://api.blockchain.com/v3/#deleteorder + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + request: dict = { + 'orderId': id, + } + response = self.privateDeleteOrdersOrderId(self.extend(request, params)) + return self.safe_order({ + 'id': id, + 'info': response, + }) + + def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders + + https://api.blockchain.com/v3/#deleteallorders + + :param str symbol: unified market symbol of the market to cancel orders in, all markets are used if None, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an list of `order structures ` + """ + # cancels all open orders if no symbol specified + # cancels all open orders of specified symbol, if symbol is specified + self.load_markets() + request: dict = { + # 'symbol': marketId, + } + if symbol is not None: + marketId = self.market_id(symbol) + request['symbol'] = marketId + response = self.privateDeleteOrders(self.extend(request, params)) + # + # {} + # + return [ + self.safe_order({ + 'info': response, + }), + ] + + def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://api.blockchain.com/v3/#getfees + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + self.load_markets() + response = self.privateGetFees(params) + # + # { + # "makerRate": "0.002", + # "takerRate": "0.004", + # "volumeInUSD": "0.0" + # } + # + makerFee = self.safe_number(response, 'makerRate') + takerFee = self.safe_number(response, 'takerRate') + result: dict = {} + for i in range(0, len(self.symbols)): + symbol = self.symbols[i] + result[symbol] = { + 'info': response, + 'symbol': symbol, + 'maker': makerFee, + 'taker': takerFee, + } + return result + + def fetch_canceled_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches information on multiple canceled orders made by the user + + https://api.blockchain.com/v3/#getorders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: timestamp in ms of the earliest order, default is None + :param int [limit]: max number of orders to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `order structures ` + """ + state = 'CANCELED' + return self.fetch_orders_by_state(state, symbol, since, limit, params) + + def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://api.blockchain.com/v3/#getorders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + state = 'FILLED' + return self.fetch_orders_by_state(state, symbol, since, limit, params) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://api.blockchain.com/v3/#getorders + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + state = 'OPEN' + return self.fetch_orders_by_state(state, symbol, since, limit, params) + + def fetch_orders_by_state(self, state, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + self.load_markets() + request: dict = { + # 'to': unix epoch ms + # 'from': unix epoch ms + 'status': state, + 'limit': 100, + } + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + response = self.privateGetOrders(self.extend(request, params)) + return self.parse_orders(response, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # { + # "exOrdId":281685751028507, + # "tradeId":281685434947633, + # "execId":8847494003, + # "side":"BUY", + # "symbol":"AAVE-USDT", + # "price":405.34, + # "qty":0.1, + # "fee":0.162136, + # "timestamp":1634559249687 + # } + # + orderId = self.safe_string(trade, 'exOrdId') + tradeId = self.safe_string(trade, 'tradeId') + side = self.safe_string(trade, 'side').lower() + marketId = self.safe_string(trade, 'symbol') + priceString = self.safe_string(trade, 'price') + amountString = self.safe_string(trade, 'qty') + timestamp = self.safe_integer(trade, 'timestamp') + datetime = self.iso8601(timestamp) + market = self.safe_market(marketId, market, '-') + symbol = market['symbol'] + fee = None + feeCostString = self.safe_string(trade, 'fee') + if feeCostString is not None: + feeCurrency = market['quote'] + fee = {'cost': feeCostString, 'currency': feeCurrency} + return self.safe_trade({ + 'id': tradeId, + 'timestamp': timestamp, + 'datetime': datetime, + 'symbol': symbol, + 'order': orderId, + 'type': None, + 'side': side, + 'takerOrMaker': None, + 'price': priceString, + 'amount': amountString, + 'cost': None, + 'fee': fee, + 'info': trade, + }, market) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://api.blockchain.com/v3/#getfills + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + request: dict = {} + if limit is not None: + request['limit'] = limit + market = None + if symbol is not None: + request['symbol'] = self.market_id(symbol) + market = self.market(symbol) + trades = self.privateGetFills(self.extend(request, params)) + return self.parse_trades(trades, market, since, limit, params) # need to define + + def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://api.blockchain.com/v3/#getdepositaddress + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + } + response = self.privatePostDepositsCurrency(self.extend(request, params)) + rawAddress = self.safe_string(response, 'address') + tag = None + address = None + if rawAddress is not None: + addressParts = rawAddress.split(';') + # if a tag or memo is used it is separated by a colon in the 'address' value + tag = self.safe_string(addressParts, 0) + address = self.safe_string(addressParts, 1) + return { + 'info': response, + 'currency': currency['code'], + 'network': None, + 'address': address, + 'tag': tag, + } + + def parse_transaction_state(self, state): + states: dict = { + 'COMPLETED': 'ok', # + 'REJECTED': 'failed', + 'PENDING': 'pending', + 'FAILED': 'failed', + 'REFUNDED': 'refunded', + } + return self.safe_string(states, state, state) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # deposit + # + # { + # "depositId":"748e9180-be0d-4a80-e175-0156150efc95", + # "amount":0.009, + # "currency":"ETH", + # "address":"0xEC6B5929D454C8D9546d4221ace969E1810Fa92c", + # "state":"COMPLETED", + # "txHash":"582114562140e51a80b481c2dfebaf62b4ab9769b8ff54820bb67e34d4a3ab0c", + # "timestamp":1633697196241 + # } + # + # withdrawal + # + # { + # "amount":30.0, + # "currency":"USDT", + # "beneficiary":"cab00d11-6e7f-46b7-b453-2e8ef6f101fa", # blockchain specific id + # "withdrawalId":"99df5ef7-eab6-4033-be49-312930fbd1ea", + # "fee":34.005078, + # "state":"COMPLETED", + # "timestamp":1634218452549 + # } + # + type = None + id = None + amount = self.safe_number(transaction, 'amount') + timestamp = self.safe_integer(transaction, 'timestamp') + currencyId = self.safe_string(transaction, 'currency') + code = self.safe_currency_code(currencyId, currency) + state = self.safe_string(transaction, 'state') + if 'depositId' in transaction: + type = 'deposit' + id = self.safe_string(transaction, 'depositId') + elif 'withdrawalId' in transaction: + type = 'withdrawal' + id = self.safe_string(transaction, 'withdrawalId') + feeCost = self.safe_number(transaction, 'fee') if (type == 'withdrawal') else None + fee = None + if feeCost is not None: + fee = {'currency': code, 'cost': feeCost} + address = self.safe_string(transaction, 'address') + txid = self.safe_string(transaction, 'txhash') + return { + 'info': transaction, + 'id': id, + 'txid': txid, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': None, + 'addressFrom': None, + 'address': address, + 'addressTo': address, + 'tagFrom': None, + 'tag': None, + 'tagTo': None, + 'type': type, + 'amount': amount, + 'currency': code, + 'status': self.parse_transaction_state(state), # 'status': 'pending', # 'ok', 'failed', 'canceled', string + 'updated': None, + 'comment': None, + 'internal': None, + 'fee': fee, + } + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://api.blockchain.com/v3/#createwithdrawal + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'amount': amount, + 'currency': currency['id'], + 'beneficiary': address, + 'sendMax': False, + } + response = self.privatePostWithdrawals(self.extend(request, params)) + # + # { + # "amount": "30.0", + # "currency": "USDT", + # "beneficiary": "adcd43fb-9ba6-41f7-8c0d-7013482cb88f", + # "withdrawalId": "99df5ef7-eab6-4033-be49-312930fbd1ea", + # "fee": "34.005078", + # "state": "PENDING", + # "timestamp": "1634218452595" + # }, + # + return self.parse_transaction(response, currency) + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://api.blockchain.com/v3/#getwithdrawals + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + request: dict = { + # 'from' : integer timestamp in ms + # 'to' : integer timestamp in ms + } + if since is not None: + request['from'] = since + currency = None + if code is not None: + currency = self.currency(code) + response = self.privateGetWithdrawals(self.extend(request, params)) + return self.parse_transactions(response, currency, since, limit) + + def fetch_withdrawal(self, id: str, code: Str = None, params={}): + """ + fetch data on a currency withdrawal via the withdrawal id + + https://api.blockchain.com/v3/#getwithdrawalbyid + + :param str id: withdrawal id + :param str code: not used by blockchaincom.fetchWithdrawal + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + self.load_markets() + request: dict = { + 'withdrawalId': id, + } + response = self.privateGetWithdrawalsWithdrawalId(self.extend(request, params)) + return self.parse_transaction(response) + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://api.blockchain.com/v3/#getdeposits + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + request: dict = { + # 'from' : integer timestamp in ms + # 'to' : integer timestap in ms + } + if since is not None: + request['from'] = since + currency = None + if code is not None: + currency = self.currency(code) + response = self.privateGetDeposits(self.extend(request, params)) + return self.parse_transactions(response, currency, since, limit) + + def fetch_deposit(self, id: str, code: Str = None, params={}): + """ + fetch information on a deposit + + https://api.blockchain.com/v3/#getdepositbyid + + :param str id: deposit id + :param str code: not used by blockchaincom fetchDeposit() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + self.load_markets() + depositId = self.safe_string(params, 'depositId', id) + request: dict = { + 'depositId': depositId, + } + deposit = self.privateGetDepositsDepositId(self.extend(request, params)) + return self.parse_transaction(deposit) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://api.blockchain.com/v3/#getaccounts + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + accountName = self.safe_string(params, 'account', 'primary') + params = self.omit(params, 'account') + request: dict = { + 'account': accountName, + } + response = self.privateGetAccounts(self.extend(request, params)) + # + # { + # "primary": [ + # { + # "currency":"ETH", + # "balance":0.009, + # "available":0.009, + # "balance_local":30.82869, + # "available_local":30.82869, + # "rate":3425.41 + # }, + # ... + # ] + # } + # + balances = self.safe_value(response, accountName) + if balances is None: + raise ExchangeError(self.id + ' fetchBalance() could not find the "' + accountName + '" account') + result: dict = {'info': response} + for i in range(0, len(balances)): + entry = balances[i] + currencyId = self.safe_string(entry, 'currency') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(entry, 'available') + account['total'] = self.safe_string(entry, 'balance') + result[code] = account + return self.safe_balance(result) + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://api.blockchain.com/v3/#getorderbyid + + :param str id: the order id + :param str symbol: not used by blockchaincom fetchOrder + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + # note: only works with exchange-order-id + # does not work with clientOrderId + self.load_markets() + request: dict = { + 'orderId': id, + } + response = self.privateGetOrdersOrderId(self.extend(request, params)) + # + # { + # "exOrdId": 11111111, + # "clOrdId": "ABC", + # "ordType": "MARKET", + # "ordStatus": "FILLED", + # "side": "BUY", + # "price": 0.12345, + # "text": "string", + # "symbol": "BTC-USD", + # "lastShares": 0.5678, + # "lastPx": 3500.12, + # "leavesQty": 10, + # "cumQty": 0.123345, + # "avgPx": 345.33, + # "timestamp": 1592830770594 + # } + # + return self.parse_order(response) + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + requestPath = '/' + self.implode_params(path, params) + url = self.urls['api'][api] + requestPath + query = self.omit(params, self.extract_params(path)) + if api == 'public': + if query: + url += '?' + self.urlencode(query) + elif api == 'private': + self.check_required_credentials() + headers = { + 'X-API-Token': self.secret, + } + if (method == 'GET'): + if query: + url += '?' + self.urlencode(query) + else: + body = self.json(query) + headers['Content-Type'] = 'application/json' + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + # {"timestamp":"2021-10-21T15:13:58.837+00:00","status":404,"error":"Not Found","message":"","path":"/orders/505050" + if response is None: + return None + text = self.safe_string(response, 'text') + if text is not None: # if trade currency account is empty returns 200 with rejected order + if text == 'Insufficient Balance': + raise InsufficientFunds(self.id + ' ' + body) + errorCode = self.safe_string(response, 'status') + errorMessage = self.safe_string(response, 'error') + if code is not None: + feedback = self.id + ' ' + self.json(response) + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], errorMessage, feedback) + return None diff --git a/ccxt/blofin.py b/ccxt/blofin.py new file mode 100644 index 0000000..b0717b0 --- /dev/null +++ b/ccxt/blofin.py @@ -0,0 +1,2451 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.blofin import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currency, Int, LedgerEntry, Leverage, Leverages, MarginMode, Market, Num, Order, OrderBook, OrderRequest, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, Trade, TradingFeeInterface, Transaction, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class blofin(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(blofin, self).describe(), { + 'id': 'blofin', + 'name': 'BloFin', + 'countries': ['US'], + 'version': 'v1', + 'rateLimit': 100, + 'pro': True, + 'has': { + 'CORS': None, + 'spot': False, + 'margin': False, + 'swap': True, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowMargin': False, + 'cancelAllOrders': False, + 'cancelOrder': True, + 'cancelOrders': True, + 'closeAllPositions': False, + 'closePosition': True, + 'createDepositAddress': False, + 'createMarketBuyOrderWithCost': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createOrders': True, + 'createOrderWithTakeProfitAndStopLoss': True, + 'createPostOnlyOrder': False, + 'createReduceOnlyOrder': False, + 'createStopLimitOrder': False, + 'createStopLossOrder': True, + 'createStopMarketOrder': False, + 'createStopOrder': False, + 'createTakeProfitOrder': True, + 'createTriggerOrder': True, + 'editOrder': False, + 'fetchAccounts': False, + 'fetchBalance': True, + 'fetchBidsAsks': None, + 'fetchBorrowInterest': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchCanceledOrders': False, + 'fetchClosedOrder': False, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': False, + 'fetchDeposit': False, + 'fetchDepositAddress': False, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchDepositsWithdrawals': False, + 'fetchDepositWithdrawFee': 'emulated', + 'fetchDepositWithdrawFees': False, + 'fetchFundingHistory': True, + 'fetchFundingRate': True, + 'fetchFundingRateHistory': True, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchL3OrderBook': False, + 'fetchLedger': True, + 'fetchLedgerEntry': None, + 'fetchLeverage': True, + 'fetchLeverages': True, + 'fetchLeverageTiers': False, + 'fetchMarginMode': True, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenOrder': None, + 'fetchOpenOrders': True, + 'fetchOrder': None, + 'fetchOrderBook': True, + 'fetchOrderBooks': False, + 'fetchOrders': False, + 'fetchOrderTrades': True, + 'fetchPosition': True, + 'fetchPositionMode': True, + 'fetchPositions': True, + 'fetchPositionsForSymbol': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchStatus': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': False, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': False, + 'fetchTradingLimits': False, + 'fetchTransactionFee': False, + 'fetchTransactionFees': False, + 'fetchTransactions': False, + 'fetchTransfer': False, + 'fetchTransfers': False, + 'fetchUnderlyingAssets': False, + 'fetchVolatilityHistory': False, + 'fetchWithdrawal': False, + 'fetchWithdrawals': True, + 'fetchWithdrawalWhitelist': False, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'setLeverage': True, + 'setMargin': False, + 'setMarginMode': True, + 'setPositionMode': True, + 'signIn': False, + 'transfer': True, + 'withdraw': False, + }, + 'timeframes': { + '1m': '1m', + '3m': '3m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1H', + '2h': '2H', + '4h': '4H', + '6h': '6H', + '8h': '8H', + '12h': '12H', + '1d': '1D', + '3d': '3D', + '1w': '1W', + '1M': '1M', + }, + 'hostname': 'www.blofin.com', + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/518cdf80-f05d-4821-a3e3-d48ceb41d73b', + 'api': { + 'rest': 'https://openapi.blofin.com', + }, + 'test': { + 'rest': 'https://demo-trading-openapi.blofin.com', + }, + 'referral': { + 'url': 'https://blofin.com/register?referral_code=f79EsS', + 'discount': 0.05, + }, + 'www': 'https://www.blofin.com', + 'doc': 'https://blofin.com/docs', + }, + 'api': { + 'public': { + 'get': { + 'market/instruments': 1, + 'market/tickers': 1, + 'market/books': 1, + 'market/trades': 1, + 'market/candles': 1, + 'market/mark-price': 1, + 'market/funding-rate': 1, + 'market/funding-rate-history': 1, + }, + }, + 'private': { + 'get': { + 'asset/balances': 1, + 'trade/orders-pending': 1, + 'trade/fills-history': 1, + 'asset/deposit-history': 1, + 'asset/withdrawal-history': 1, + 'asset/bills': 1, + 'account/balance': 1, + 'account/positions': 1, + 'account/leverage-info': 1, + 'account/margin-mode': 1, + 'account/position-mode': 1, + 'account/batch-leverage-info': 1, + 'trade/orders-tpsl-pending': 1, + 'trade/orders-algo-pending': 1, + 'trade/orders-history': 1, + 'trade/orders-tpsl-history': 1, + 'trade/orders-algo-history': 1, # todo new + 'trade/order/price-range': 1, + 'user/query-apikey': 1, + 'affiliate/basic': 1, + 'copytrading/instruments': 1, + 'copytrading/account/balance': 1, + 'copytrading/account/positions-by-order': 1, + 'copytrading/account/positions-details-by-order': 1, + 'copytrading/account/positions-by-contract': 1, + 'copytrading/account/position-mode': 1, + 'copytrading/account/leverage-info': 1, + 'copytrading/trade/orders-pending': 1, + 'copytrading/trade/pending-tpsl-by-contract': 1, + 'copytrading/trade/position-history-by-order': 1, + 'copytrading/trade/orders-history': 1, + 'copytrading/trade/pending-tpsl-by-order': 1, + }, + 'post': { + 'account/set-margin-mode': 1, + 'account/set-position-mode': 1, + 'trade/order': 1, + 'trade/order-algo': 1, + 'trade/cancel-order': 1, + 'trade/cancel-algo': 1, + 'account/set-leverage': 1, + 'trade/batch-orders': 1, + 'trade/order-tpsl': 1, + 'trade/cancel-batch-orders': 1, + 'trade/cancel-tpsl': 1, + 'trade/close-position': 1, + 'asset/transfer': 1, + 'copytrading/account/set-position-mode': 1, + 'copytrading/account/set-leverage': 1, + 'copytrading/trade/place-order': 1, + 'copytrading/trade/cancel-order': 1, + 'copytrading/trade/place-tpsl-by-contract': 1, + 'copytrading/trade/cancel-tpsl-by-contract': 1, + 'copytrading/trade/place-tpsl-by-order': 1, + 'copytrading/trade/cancel-tpsl-by-order': 1, + 'copytrading/trade/close-position-by-order': 1, + 'copytrading/trade/close-position-by-contract': 1, + }, + }, + }, + 'fees': { + 'swap': { + 'taker': self.parse_number('0.00060'), + 'maker': self.parse_number('0.00020'), + }, + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + 'password': True, + }, + 'features': { + 'default': { + 'sandbox': False, + 'createOrder': { + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'leverage': False, + 'marketBuyRequiresPrice': False, + 'marketBuyByCost': False, + 'selfTradePrevention': False, + 'trailing': False, + 'iceberg': False, + }, + 'createOrders': { + 'max': 10, + }, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 100000, + 'untilDays': 100000, + 'symbolRequired': False, + }, + 'fetchOrder': None, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 100, + 'trigger': True, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 100000, + 'daysBackCanceled': 1, + 'untilDays': 100000, + 'trigger': True, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 1440, + }, + }, + 'spot': { + 'extends': 'default', + 'createOrder': { + 'marginMode': False, + 'triggerPrice': False, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'hedged': False, + }, + }, + 'forDerivatives': { + 'extends': 'default', + 'createOrder': { + 'marginMode': True, + 'triggerPrice': False, # todo + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': True, + 'takeProfitPrice': True, + 'attachedStopLossTakeProfit': { + 'triggerPriceType': None, + 'price': True, + }, + 'hedged': True, + }, + }, + 'swap': { + 'linear': { + 'extends': 'forDerivatives', + }, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'exceptions': { + 'exact': { + '400': BadRequest, # Body can not be empty + '401': AuthenticationError, # Invalid signature + '500': ExchangeError, # Internal Server Error + '404': BadRequest, # not found + '405': BadRequest, # Method Not Allowed + '406': BadRequest, # Not Acceptable + '429': RateLimitExceeded, # Too Many Requests + '152001': BadRequest, # Parameter {} cannot be empty + '152002': BadRequest, # Parameter {} error + '152003': BadRequest, # Either parameter {} or {} is required + '152004': BadRequest, # JSON syntax error + '152005': BadRequest, # Parameter error: wrong or empty + '152006': InvalidOrder, # Batch orders can be placed for up to 20 at once + '152007': InvalidOrder, # Batch orders can only be placed with the same instId and marginMode + '152008': InvalidOrder, # Only the same field is allowed for bulk cancellation of orders, orderId is preferred + '152009': InvalidOrder, # {} must be a combination of numbers, letters, or underscores, and the maximum length of characters is 32 + '150003': InvalidOrder, # clientId already exist + '150004': InvalidOrder, # Insufficient balance. please adjust the amount and try again + '542': InvalidOrder, # Exceeded the maximum order size limit + '102002': InvalidOrder, # Duplicate customized order ID + '102005': InvalidOrder, # Position had been closed + '102014': InvalidOrder, # Limit order exceeds maximum order size limit + '102015': InvalidOrder, # Market order exceeds maximum order size limit + '102022': InvalidOrder, # Failed to place order. You don’t have any positions of self contract. Turn off Reduce-only to continue. + '102037': InvalidOrder, # TP trigger price should be higher than the latest trading price + '102038': InvalidOrder, # SL trigger price should be lower than the latest trading price + '102039': InvalidOrder, # TP trigger price should be lower than the latest trading price + '102040': InvalidOrder, # SL trigger price should be higher than the latest trading price + '102047': InvalidOrder, # Stop loss trigger price should be higher than the order price + '102048': InvalidOrder, # stop loss trigger price must be higher than the best bid price + '102049': InvalidOrder, # Take profit trigger price should be lower than the order price + '102050': InvalidOrder, # stop loss trigger price must be lower than the best ask price + '102051': InvalidOrder, # stop loss trigger price should be lower than the order price + '102052': InvalidOrder, # take profit trigger price should be higher than the order price + '102053': InvalidOrder, # take profit trigger price should be lower than the best bid price + '102054': InvalidOrder, # take profit trigger price should be higher than the best ask price + '102055': InvalidOrder, # stop loss trigger price should be lower than the best ask price + '102064': BadRequest, # Buy price is not within the price limit(Minimum: 310.40; Maximum:1,629.40) + '102065': BadRequest, # Sell price is not within the price limit + '102068': BadRequest, # Cancel failed order has been filled, triggered, canceled or does not exist + '103013': ExchangeError, # Internal error; unable to process your request. Please try again. + 'Order failed. Insufficient USDT margin in account': InsufficientFunds, # Insufficient USDT margin in account + }, + 'broad': { + 'Internal Server Error': ExchangeNotAvailable, # {"code":500,"data":{},"detailMsg":"","error_code":"500","error_message":"Internal Server Error","msg":"Internal Server Error"} + 'server error': ExchangeNotAvailable, # {"code":500,"data":{},"detailMsg":"","error_code":"500","error_message":"server error 1236805249","msg":"server error 1236805249"} + }, + }, + 'httpExceptions': { + '429': ExchangeNotAvailable, # https://github.com/ccxt/ccxt/issues/9612 + }, + 'precisionMode': TICK_SIZE, + 'options': { + 'brokerId': 'ec6dd3a7dd982d0b', + 'accountsByType': { + 'swap': 'futures', + 'funding': 'funding', + 'future': 'futures', + 'copy_trading': 'copy_trading', + 'earn': 'earn', + 'spot': 'spot', + }, + 'accountsById': { + 'funding': 'funding', + 'futures': 'swap', + 'copy_trading': 'copy_trading', + 'earn': 'earn', + 'spot': 'spot', + }, + 'defaultNetwork': 'ERC20', + 'defaultNetworks': { + 'ETH': 'ERC20', + 'BTC': 'BTC', + 'USDT': 'TRC20', + }, + 'networks': { + 'BTC': 'Bitcoin', + 'BEP20': 'BSC', + 'ERC20': 'ERC20', + 'TRC20': 'TRC20', + }, + 'fetchOpenInterestHistory': { + 'timeframes': { + '5m': '5m', + '1h': '1H', + '8h': '8H', + '1d': '1D', + '5M': '5m', + '1H': '1H', + '8H': '8H', + '1D': '1D', + }, + }, + 'fetchOHLCV': { + # 'type': 'Candles', # Candles or HistoryCandles, IndexCandles, MarkPriceCandles + 'timezone': 'UTC', # UTC, HK + }, + 'fetchPositions': { + 'method': 'privateGetAccountPositions', # privateGetAccountPositions or privateGetAccountPositionsHistory + }, + 'createOrder': 'privatePostTradeOrder', # or 'privatePostTradeOrderTpsl' + 'createMarketBuyOrderRequiresPrice': False, + 'fetchMarkets': ['swap'], + 'defaultType': 'swap', + 'fetchLedger': { + 'method': 'privateGetAssetBills', + }, + 'fetchOpenOrders': { + 'method': 'privateGetTradeOrdersPending', + }, + 'cancelOrders': { + 'method': 'privatePostTradeCancelBatchOrders', + }, + 'fetchCanceledOrders': { + 'method': 'privateGetTradeOrdersHistory', # privateGetTradeOrdersTpslHistory + }, + 'fetchClosedOrders': { + 'method': 'privateGetTradeOrdersHistory', # privateGetTradeOrdersTpslHistory + }, + 'withdraw': { + # a funding password credential is required by the exchange for the + # withdraw call(not to be confused with the api password credential) + 'password': None, + 'pwd': None, # password or pwd both work + }, + 'exchangeType': { + 'spot': 'SPOT', + 'swap': 'SWAP', + 'SPOT': 'SPOT', + 'SWAP': 'SWAP', + }, + }, + }) + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for blofin + + https://blofin.com/docs#get-instruments + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = self.publicGetMarketInstruments(params) + data = self.safe_list(response, 'data', []) + return self.parse_markets(data) + + def parse_market(self, market: dict) -> Market: + id = self.safe_string(market, 'instId') + type = self.safe_string_lower(market, 'instType') + spot = (type == 'spot') + future = (type == 'future') + swap = (type == 'swap') + option = (type == 'option') + contract = swap or future + baseId = self.safe_string(market, 'baseCurrency') + quoteId = self.safe_string(market, 'quoteCurrency') + settleId = self.safe_string(market, 'quoteCurrency') + settle = self.safe_currency_code(settleId) + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + symbol = base + '/' + quote + if swap: + symbol = symbol + ':' + settle + expiry = None + strikePrice = None + optionType = None + tickSize = self.safe_string(market, 'tickSize') + fees = self.safe_dict_2(self.fees, type, 'trading', {}) + taker = self.safe_number(fees, 'taker') + maker = self.safe_number(fees, 'maker') + maxLeverage = self.safe_string(market, 'maxLeverage', '100') + maxLeverage = Precise.string_max(maxLeverage, '1') + isActive = (self.safe_string(market, 'state') == 'live') + return self.safe_market_structure({ + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'baseId': baseId, + 'quoteId': quoteId, + 'settle': settle, + 'settleId': settleId, + 'type': type, + 'spot': spot, + 'option': option, + 'margin': spot and (Precise.string_gt(maxLeverage, '1')), + 'swap': swap, + 'future': future, + 'active': isActive, + 'taker': taker, + 'maker': maker, + 'contract': contract, + 'linear': (quoteId == settleId) if contract else None, + 'inverse': (baseId == settleId) if contract else None, + 'contractSize': self.safe_number(market, 'contractValue') if contract else None, + 'expiry': expiry, + 'expiryDatetime': expiry, + 'strike': strikePrice, + 'optionType': optionType, + 'created': self.safe_integer(market, 'listTime'), + 'precision': { + 'amount': self.safe_number(market, 'lotSize'), + 'price': self.parse_number(tickSize), + }, + 'limits': { + 'leverage': { + 'min': self.parse_number('1'), + 'max': self.parse_number(maxLeverage), + }, + 'amount': { + 'min': self.safe_number(market, 'minSize'), + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'info': market, + }) + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://blofin.com/docs#get-order-book + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + } + limit = 50 if (limit is None) else limit + if limit is not None: + request['size'] = limit # max 100 + response = self.publicGetMarketBooks(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "asks": [ + # ["0.07228","4.211619","0","2"], # price, amount, liquidated orders, total open orders + # ["0.0723","299.880364","0","2"], + # ["0.07231","3.72832","0","1"], + # ], + # "bids": [ + # ["0.07221","18.5","0","1"], + # ["0.0722","18.5","0","1"], + # ["0.07219","0.505407","0","1"], + # ], + # "ts": "1621438475342" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + first = self.safe_dict(data, 0, {}) + timestamp = self.safe_integer(first, 'ts') + return self.parse_order_book(first, symbol, timestamp) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # response similar for REST & WS + # + # { + # instId: "ADA-USDT", + # ts: "1707736811486", + # last: "0.5315", + # lastSize: "4", + # askPrice: "0.5318", + # askSize: "248", + # bidPrice: "0.5315", + # bidSize: "63", + # open24h: "0.5555", + # high24h: "0.5563", + # low24h: "0.5315", + # volCurrency24h: "198560100", + # vol24h: "1985601", + # } + # + timestamp = self.safe_integer(ticker, 'ts') + marketId = self.safe_string(ticker, 'instId') + market = self.safe_market(marketId, market, '-') + symbol = market['symbol'] + last = self.safe_string(ticker, 'last') + open = self.safe_string(ticker, 'open24h') + spot = self.safe_bool(market, 'spot', False) + quoteVolume = self.safe_string(ticker, 'volCurrency24h') if spot else None + baseVolume = self.safe_string(ticker, 'vol24h') + high = self.safe_string(ticker, 'high24h') + low = self.safe_string(ticker, 'low24h') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': high, + 'low': low, + 'bid': self.safe_string(ticker, 'bidPrice'), + 'bidVolume': self.safe_string(ticker, 'bidSize'), + 'ask': self.safe_string(ticker, 'askPrice'), + 'askVolume': self.safe_string(ticker, 'askSize'), + 'vwap': None, + 'open': open, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'indexPrice': self.safe_string(ticker, 'indexPrice'), + 'markPrice': self.safe_string(ticker, 'markPrice'), + 'info': ticker, + }, market) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://blofin.com/docs#get-tickers + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + } + response = self.publicGetMarketTickers(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + first = self.safe_dict(data, 0, {}) + return self.parse_ticker(first, market) + + def fetch_mark_price(self, symbol: str, params={}) -> Ticker: + """ + fetches mark price for the market + + https://docs.blofin.com/index.html#get-mark-price + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.subType]: "linear" or "inverse" + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + market = self.market(symbol) + request = { + 'symbol': market['id'], + } + response = self.publicGetMarketMarkPrice(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + first = self.safe_dict(data, 0, {}) + return self.parse_ticker(first, market) + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://blofin.com/docs#get-tickers + + :param str[] [symbols]: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + symbols = self.market_symbols(symbols) + response = self.publicGetMarketTickers(params) + tickers = self.safe_list(response, 'data', []) + return self.parse_tickers(tickers, symbols) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetch trades(response similar for REST & WS) + # + # { + # "tradeId": "3263934920", + # "instId": "LTC-USDT", + # "price": "67.87", + # "size": "1", + # "side": "buy", + # "ts": "1707232020854" + # } + # + # my trades + # { + # "instId": "LTC-USDT", + # "tradeId": "1440847", + # "orderId": "2075705202", + # "fillPrice": "67.850000000000000000", + # "fillSize": "1.000000000000000000", + # "fillPnl": "0.000000000000000000", + # "side": "buy", + # "positionSide": "net", + # "fee": "0.040710000000000000", + # "ts": "1707224678878", + # "brokerId": "" + # } + # + id = self.safe_string(trade, 'tradeId') + marketId = self.safe_string(trade, 'instId') + market = self.safe_market(marketId, market, '-') + symbol = market['symbol'] + timestamp = self.safe_integer(trade, 'ts') + price = self.safe_string_2(trade, 'price', 'fillPrice') + amount = self.safe_string_2(trade, 'size', 'fillSize') + side = self.safe_string(trade, 'side') + orderId = self.safe_string(trade, 'orderId') + feeCost = self.safe_string(trade, 'fee') + fee = None + if feeCost is not None: + fee = { + 'cost': feeCost, + 'currency': market['settle'], + } + return self.safe_trade({ + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'id': id, + 'order': orderId, + 'type': None, + 'takerOrMaker': None, + 'side': side, + 'price': price, + 'amount': amount, + 'cost': None, + 'fee': fee, + }, market) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://blofin.com/docs#get-trades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: *only applies to publicGetMarketHistoryTrades* default False, when True will automatically paginate by calling self endpoint multiple times + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchTrades', 'paginate') + if paginate: + return self.fetch_paginated_call_cursor('fetchTrades', symbol, since, limit, params, 'tradeId', 'after', None, 100) + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + } + response = None + if limit is not None: + request['limit'] = limit # default 100 + method = None + method, params = self.handle_option_and_params(params, 'fetchTrades', 'method', 'publicGetMarketTrades') + if method == 'publicGetMarketTrades': + response = self.publicGetMarketTrades(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + return self.parse_trades(data, market, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # [ + # "1678928760000", # timestamp + # "24341.4", # open + # "24344", # high + # "24313.2", # low + # "24323", # close + # "628", # contract volume + # "2.5819", # base volume + # "62800", # quote volume + # "0" # candlestick state + # ] + # + return [ + self.safe_integer(ohlcv, 0), + self.safe_number(ohlcv, 1), + self.safe_number(ohlcv, 2), + self.safe_number(ohlcv, 3), + self.safe_number(ohlcv, 4), + self.safe_number(ohlcv, 6), + ] + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://blofin.com/docs#get-candlesticks + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest candle to fetch + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + market = self.market(symbol) + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate') + if paginate: + return self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params, 100) + if limit is None: + limit = 100 # default 100, max 100 + request: dict = { + 'instId': market['id'], + 'bar': self.safe_string(self.timeframes, timeframe, timeframe), + 'limit': limit, + } + until = self.safe_integer(params, 'until') + if until is not None: + request['after'] = until + params = self.omit(params, 'until') + response = None + response = self.publicGetMarketCandles(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + return self.parse_ohlcvs(data, market, timeframe, since, limit) + + def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical funding rate prices + + https://blofin.com/docs#get-funding-rate-history + + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: timestamp in ms of the earliest funding rate to fetch + :param int [limit]: the maximum amount of `funding rate structures ` to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param int [params.until]: timestamp in ms of the latest funding rate to fetch + :returns dict[]: a list of `funding rate structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchFundingRateHistory() requires a symbol argument') + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchFundingRateHistory', 'paginate') + if paginate: + return self.fetch_paginated_call_deterministic('fetchFundingRateHistory', symbol, since, limit, '8h', params, 100) + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + } + if since is not None: + request['before'] = max(since - 1, 0) + if limit is not None: + request['limit'] = limit + until = self.safe_integer(params, 'until') + if until is not None: + request['after'] = until + params = self.omit(params, 'until') + response = self.publicGetMarketFundingRateHistory(self.extend(request, params)) + rates = [] + data = self.safe_list(response, 'data', []) + for i in range(0, len(data)): + rate = data[i] + timestamp = self.safe_integer(rate, 'fundingTime') + rates.append({ + 'info': rate, + 'symbol': market['symbol'], + 'fundingRate': self.safe_number(rate, 'fundingRate'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + sorted = self.sort_by(rates, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, market['symbol'], since, limit) + + def parse_funding_rate(self, contract, market: Market = None) -> FundingRate: + # + # { + # "fundingRate": "0.00027815", + # "fundingTime": "1634256000000", + # "instId": "BTC-USD-SWAP", + # } + # + marketId = self.safe_string(contract, 'instId') + symbol = self.safe_symbol(marketId, market) + fundingTime = self.safe_integer(contract, 'fundingTime') + # > The current interest is 0. + return { + 'info': contract, + 'symbol': symbol, + 'markPrice': None, + 'indexPrice': None, + 'interestRate': self.parse_number('0'), + 'estimatedSettlePrice': None, + 'timestamp': None, + 'datetime': None, + 'fundingRate': self.safe_number(contract, 'fundingRate'), + 'fundingTimestamp': fundingTime, + 'fundingDatetime': self.iso8601(fundingTime), + 'nextFundingRate': None, + 'nextFundingTimestamp': None, + 'nextFundingDatetime': None, + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': None, + } + + def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate + + https://blofin.com/docs#get-funding-rate + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + self.load_markets() + market = self.market(symbol) + if not market['swap']: + raise ExchangeError(self.id + ' fetchFundingRate() is only valid for swap markets') + request: dict = { + 'instId': market['id'], + } + response = self.publicGetMarketFundingRate(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "fundingRate": "0.00027815", + # "fundingTime": "1634256000000", + # "instId": "BTC-USD-SWAP", + # } + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + entry = self.safe_dict(data, 0, {}) + return self.parse_funding_rate(entry, market) + + def parse_balance_by_type(self, response): + data = self.safe_list(response, 'data') + if (data is not None) and isinstance(data, list): + return self.parse_funding_balance(response) + else: + return self.parse_balance(response) + + def parse_balance(self, response): + # + # "data" similar for REST & WS + # + # { + # "code": "0", + # "msg": "success", + # "data": { + # "ts": "1697021343571", + # "totalEquity": "10011254.077985990315787910", + # "isolatedEquity": "861.763132108800000000", + # "details": [ + # { + # "currency": "USDT", + # "equity": "10014042.988958415234430699548", + # "balance": "10013119.885958415234430699", + # "ts": "1697021343571", + # "isolatedEquity": "862.003200000000000000048", + # "available": "9996399.4708691159703362725", + # "availableEquity": "9996399.4708691159703362725", + # "frozen": "15805.149672632597427761", + # "orderFrozen": "14920.994472632597427761", + # "equityUsd": "10011254.077985990315787910", + # "isolatedUnrealizedPnl": "-22.151999999999999999952", + # "bonus": "0" # present only in REST + # "unrealizedPnl": "0" # present only in WS + # } + # ] + # } + # } + # + result: dict = {'info': response} + data = self.safe_dict(response, 'data', {}) + timestamp = self.safe_integer(data, 'ts') + details = self.safe_list(data, 'details', []) + for i in range(0, len(details)): + balance = details[i] + currencyId = self.safe_string(balance, 'currency') + code = self.safe_currency_code(currencyId) + account = self.account() + # it may be incorrect to use total, free and used for swap accounts + eq = self.safe_string(balance, 'equity') + availEq = self.safe_string(balance, 'available') + if (eq is None) or (availEq is None): + account['free'] = self.safe_string(balance, 'availableEquity') + account['used'] = self.safe_string(balance, 'frozen') + else: + account['total'] = eq + account['free'] = availEq + result[code] = account + result['timestamp'] = timestamp + result['datetime'] = self.iso8601(timestamp) + return self.safe_balance(result) + + def parse_funding_balance(self, response): + # + # { + # "code": "0", + # "msg": "success", + # "data": [ + # { + # "currency": "USDT", + # "balance": "10012514.919418081548717298", + # "available": "9872132.414278782284622898", + # "frozen": "138556.471805965930761067", + # "bonus": "0" + # } + # ] + # } + # + result: dict = {'info': response} + data = self.safe_list(response, 'data', []) + for i in range(0, len(data)): + balance = data[i] + currencyId = self.safe_string(balance, 'currency') + code = self.safe_currency_code(currencyId) + account = self.account() + # it may be incorrect to use total, free and used for swap accounts + account['total'] = self.safe_string(balance, 'balance') + account['free'] = self.safe_string(balance, 'available') + account['used'] = self.safe_string(balance, 'frozen') + result[code] = account + return self.safe_balance(result) + + def parse_trading_fee(self, fee: dict, market: Market = None) -> TradingFeeInterface: + return { + 'info': fee, + 'symbol': self.safe_symbol(None, market), + # blofin returns the fees values opposed to other exchanges, so the sign needs to be flipped + 'maker': self.parse_number(Precise.string_neg(self.safe_string_2(fee, 'maker', 'makerU'))), + 'taker': self.parse_number(Precise.string_neg(self.safe_string_2(fee, 'taker', 'takerU'))), + 'percentage': None, + 'tierBased': None, + } + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://blofin.com/docs#get-balance + https://blofin.com/docs#get-futures-account-balance + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.accountType]: the type of account to fetch the balance for, either 'funding' or 'futures' or 'copy_trading' or 'earn' + :returns dict: a `balance structure ` + """ + self.load_markets() + accountType = None + accountType, params = self.handle_option_and_params_2(params, 'fetchBalance', 'accountType', 'type') + request: dict = { + } + response = None + if accountType is not None and accountType != 'swap': + options = self.safe_dict(self.options, 'accountsByType', {}) + parsedAccountType = self.safe_string(options, accountType, accountType) + request['accountType'] = parsedAccountType + response = self.privateGetAssetBalances(self.extend(request, params)) + else: + response = self.privateGetAccountBalance(self.extend(request, params)) + return self.parse_balance_by_type(response) + + def create_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + 'side': side, + 'orderType': type, + 'size': self.amount_to_precision(symbol, amount), + 'brokerId': self.safe_string(self.options, 'brokerId', 'ec6dd3a7dd982d0b'), + } + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('createOrder', params, 'cross') + request['marginMode'] = marginMode + triggerPrice = self.safe_string(params, 'triggerPrice') + timeInForce = self.safe_string(params, 'timeInForce', 'GTC') + isHedged = self.safe_bool(params, 'hedged', False) + if isHedged: + request['positionSide'] = 'long' if (side == 'buy') else 'short' + isMarketOrder = type == 'market' + params = self.omit(params, ['timeInForce']) + ioc = (timeInForce == 'IOC') or (type == 'ioc') + marketIOC = (isMarketOrder and ioc) + if isMarketOrder or marketIOC: + request['orderType'] = 'market' + else: + key = 'orderPrice' if (triggerPrice is not None) else 'price' + request[key] = self.price_to_precision(symbol, price) + postOnly = False + postOnly, params = self.handle_post_only(isMarketOrder, type == 'post_only', params) + if postOnly: + request['type'] = 'post_only' + stopLoss = self.safe_dict(params, 'stopLoss') + takeProfit = self.safe_dict(params, 'takeProfit') + params = self.omit(params, ['stopLoss', 'takeProfit', 'hedged']) + isStopLoss = stopLoss is not None + isTakeProfit = takeProfit is not None + if isStopLoss or isTakeProfit: + if isStopLoss: + slTriggerPrice = self.safe_string_2(stopLoss, 'triggerPrice', 'stopPrice') + request['slTriggerPrice'] = self.price_to_precision(symbol, slTriggerPrice) + slOrderPrice = self.safe_string(stopLoss, 'price', '-1') + request['slOrderPrice'] = self.price_to_precision(symbol, slOrderPrice) + if isTakeProfit: + tpTriggerPrice = self.safe_string_2(takeProfit, 'triggerPrice', 'stopPrice') + request['tpTriggerPrice'] = self.price_to_precision(symbol, tpTriggerPrice) + tpPrice = self.safe_string(takeProfit, 'price', '-1') + request['tpOrderPrice'] = self.price_to_precision(symbol, tpPrice) + elif triggerPrice is not None: + request['orderType'] = 'trigger' + request['triggerPrice'] = self.price_to_precision(symbol, triggerPrice) + if isMarketOrder: + request['orderPrice'] = '-1' + return self.extend(request, params) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'canceled': 'canceled', + 'order_failed': 'canceled', + 'live': 'open', + 'partially_filled': 'open', + 'filled': 'closed', + 'effective': 'closed', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # response similar for REST & WS + # + # { + # "orderId": "2075628533", + # "clientOrderId": "", + # "instId": "LTC-USDT", + # "marginMode": "cross", + # "positionSide": "net", + # "side": "buy", + # "orderType": "market", + # "price": "0.000000000000000000", + # "size": "1.000000000000000000", + # "reduceOnly": "true", + # "leverage": "3", + # "state": "filled", + # "filledSize": "1.000000000000000000", + # "pnl": "-0.050000000000000000", + # "averagePrice": "68.110000000000000000", + # "fee": "0.040866000000000000", + # "createTime": "1706891359010", + # "updateTime": "1706891359098", + # "orderCategory": "normal", + # "tpTriggerPrice": null, + # "tpOrderPrice": null, + # "slTriggerPrice": null, + # "slOrderPrice": null, + # "cancelSource": "not_canceled", + # "cancelSourceReason": null, + # "brokerId": "ec6dd3a7dd982d0b" + # "filled_amount": "1.000000000000000000", # filledAmount in "ws" watchOrders + # "cancelSource": "", # only in WS + # "instType": "SWAP", # only in WS + # } + # + id = self.safe_string_n(order, ['tpslId', 'orderId', 'algoId']) + timestamp = self.safe_integer(order, 'createTime') + lastUpdateTimestamp = self.safe_integer(order, 'updateTime') + lastTradeTimestamp = self.safe_integer(order, 'fillTime') + side = self.safe_string(order, 'side') + type = self.safe_string(order, 'orderType') + postOnly = None + timeInForce = None + if type == 'post_only': + postOnly = True + type = 'limit' + elif type == 'fok': + timeInForce = 'FOK' + type = 'limit' + elif type == 'ioc': + timeInForce = 'IOC' + type = 'limit' + elif type == 'conditional': + type = 'trigger' + marketId = self.safe_string(order, 'instId') + market = self.safe_market(marketId, market) + symbol = self.safe_symbol(marketId, market, '-') + filled = self.safe_string(order, 'filledSize') + price = self.safe_string_n(order, ['px', 'price', 'orderPrice']) + average = self.safe_string(order, 'averagePrice') + status = self.parse_order_status(self.safe_string(order, 'state')) + feeCostString = self.safe_string(order, 'fee') + amount = self.safe_string(order, 'size') + leverage = self.safe_string(order, 'leverage', '1') + contractSize = self.safe_string(market, 'contractSize') + baseAmount = Precise.string_mul(contractSize, filled) + cost: Str = None + if average is not None: + cost = Precise.string_mul(average, baseAmount) + cost = Precise.string_div(cost, leverage) + # spot market buy: "sz" can refer either to base currency units or to quote currency units + fee = None + if feeCostString is not None: + feeCostSigned = Precise.string_abs(feeCostString) + feeCurrencyId = self.safe_string(order, 'feeCcy', 'USDT') + feeCurrencyCode = self.safe_currency_code(feeCurrencyId) + fee = { + 'cost': self.parse_number(feeCostSigned), + 'currency': feeCurrencyCode, + } + clientOrderId = self.safe_string(order, 'clientOrderId') + if (clientOrderId is not None) and (len(clientOrderId) < 1): + clientOrderId = None # fix empty clientOrderId string + stopLossTriggerPrice = self.safe_number(order, 'slTriggerPrice') + stopLossPrice = self.safe_number(order, 'slOrderPrice') + takeProfitTriggerPrice = self.safe_number(order, 'tpTriggerPrice') + takeProfitPrice = self.safe_number(order, 'tpOrderPrice') + reduceOnlyRaw = self.safe_string(order, 'reduceOnly') + reduceOnly = (reduceOnlyRaw == 'true') + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': lastTradeTimestamp, + 'lastUpdateTimestamp': lastUpdateTimestamp, + 'symbol': symbol, + 'type': type, + 'timeInForce': timeInForce, + 'postOnly': postOnly, + 'side': side, + 'price': price, + 'stopLossTriggerPrice': stopLossTriggerPrice, + 'takeProfitTriggerPrice': takeProfitTriggerPrice, + 'stopLossPrice': stopLossPrice, + 'takeProfitPrice': takeProfitPrice, + 'average': average, + 'cost': cost, + 'amount': amount, + 'filled': filled, + 'remaining': None, + 'status': status, + 'fee': fee, + 'trades': None, + 'reduceOnly': reduceOnly, + }, market) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}) -> Order: + """ + create a trade order + + https://blofin.com/docs#place-order + https://blofin.com/docs#place-tpsl-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' or 'post_only' or 'ioc' or 'fok' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.triggerPrice]: the trigger price for a trigger order + :param bool [params.reduceOnly]: a mark to reduce the position size for margin, swap and future orders + :param bool [params.postOnly]: True to place a post only order + :param str [params.marginMode]: 'cross' or 'isolated', default is 'cross' + :param float [params.stopLossPrice]: stop loss trigger price(will use privatePostTradeOrderTpsl) + :param float [params.takeProfitPrice]: take profit trigger price(will use privatePostTradeOrderTpsl) + :param str [params.positionSide]: *stopLossPrice/takeProfitPrice orders only* 'long' or 'short' or 'net' default is 'net' + :param boolean [params.hedged]: if True, the positionSide will be set to long/short instead of net, default is False + :param str [params.clientOrderId]: a unique id for the order + :param dict [params.takeProfit]: *takeProfit object in params* containing the triggerPrice at which the attached take profit order will be triggered + :param float [params.takeProfit.triggerPrice]: take profit trigger price + :param float [params.takeProfit.price]: take profit order price(if not provided the order will be a market order) + :param dict [params.stopLoss]: *stopLoss object in params* containing the triggerPrice at which the attached stop loss order will be triggered + :param float [params.stopLoss.triggerPrice]: stop loss trigger price + :param float [params.stopLoss.price]: stop loss order price(if not provided the order will be a market order) + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + tpsl = self.safe_bool(params, 'tpsl', False) + params = self.omit(params, 'tpsl') + method = None + method, params = self.handle_option_and_params(params, 'createOrder', 'method', 'privatePostTradeOrder') + isStopLossPriceDefined = self.safe_string(params, 'stopLossPrice') is not None + isTakeProfitPriceDefined = self.safe_string(params, 'takeProfitPrice') is not None + hasTriggerPrice = self.safe_string(params, 'triggerPrice') is not None + isType2Order = (isStopLossPriceDefined or isTakeProfitPriceDefined) + response = None + reduceOnly = self.safe_bool(params, 'reduceOnly') + if reduceOnly is not None: + params['reduceOnly'] = 'true' if reduceOnly else 'false' + isTpslOrder = tpsl or (method == 'privatePostTradeOrderTpsl') or isType2Order + isTriggerOrder = hasTriggerPrice or (method == 'privatePostTradeOrderAlgo') + if isTpslOrder: + tpslRequest = self.create_tpsl_order_request(symbol, type, side, amount, price, params) + response = self.privatePostTradeOrderTpsl(tpslRequest) + elif isTriggerOrder: + triggerRequest = self.create_order_request(symbol, type, side, amount, price, params) + response = self.privatePostTradeOrderAlgo(triggerRequest) + else: + request = self.create_order_request(symbol, type, side, amount, price, params) + response = self.privatePostTradeOrder(request) + if isTpslOrder or isTriggerOrder: + dataDict = self.safe_dict(response, 'data', {}) + return self.parse_order(dataDict, market) + data = self.safe_list(response, 'data', []) + first = self.safe_dict(data, 0) + order = self.parse_order(first, market) + order['type'] = type + order['side'] = side + return order + + def create_tpsl_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + market = self.market(symbol) + positionSide = self.safe_string(params, 'positionSide', 'net') + request: dict = { + 'instId': market['id'], + 'side': side, + 'positionSide': positionSide, + 'brokerId': self.safe_string(self.options, 'brokerId', 'ec6dd3a7dd982d0b'), + } + if amount is not None: + request['size'] = self.amount_to_precision(symbol, amount) + marginMode = self.safe_string(params, 'marginMode', 'cross') # cross or isolated + if marginMode != 'cross' and marginMode != 'isolated': + raise BadRequest(self.id + ' createTpslOrder() requires a marginMode parameter that must be either cross or isolated') + stopLossPrice = self.safe_string(params, 'stopLossPrice') + takeProfitPrice = self.safe_string(params, 'takeProfitPrice') + if stopLossPrice is not None: + request['slTriggerPrice'] = self.price_to_precision(symbol, stopLossPrice) + if type == 'market': + request['slOrderPrice'] = '-1' + else: + request['slOrderPrice'] = self.price_to_precision(symbol, price) + elif takeProfitPrice is not None: + request['tpTriggerPrice'] = self.price_to_precision(symbol, takeProfitPrice) + if type == 'market': + request['tpOrderPrice'] = '-1' + else: + request['tpOrderPrice'] = self.price_to_precision(symbol, price) + request['marginMode'] = marginMode + params = self.omit(params, ['stopLossPrice', 'takeProfitPrice']) + return self.extend(request, params) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://blofin.com/docs#cancel-order + https://blofin.com/docs#cancel-tpsl-order + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: True if cancelling a trigger/conditional + :param boolean [params.tpsl]: True if cancelling a tpsl order + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + } + isTrigger = self.safe_bool_n(params, ['trigger'], False) + isTpsl = self.safe_bool_2(params, 'tpsl', 'TPSL', False) + clientOrderId = self.safe_string(params, 'clientOrderId') + if clientOrderId is not None: + request['clientOrderId'] = clientOrderId + else: + if not isTrigger and not isTpsl: + request['orderId'] = str(id) + elif isTpsl: + request['tpslId'] = str(id) + elif isTrigger: + request['algoId'] = str(id) + query = self.omit(params, ['orderId', 'clientOrderId', 'stop', 'trigger', 'tpsl']) + if isTpsl: + tpslResponse = self.cancel_orders([id], symbol, params) + first = self.safe_dict(tpslResponse, 0) + return first + elif isTrigger: + triggerResponse = self.privatePostTradeCancelAlgo(self.extend(request, query)) + triggerData = self.safe_dict(triggerResponse, 'data') + return self.parse_order(triggerData, market) + response = self.privatePostTradeCancelOrder(self.extend(request, query)) + data = self.safe_list(response, 'data', []) + order = self.safe_dict(data, 0) + return self.parse_order(order, market) + + def create_orders(self, orders: List[OrderRequest], params={}) -> List[Order]: + """ + create a list of trade orders + + https://blofin.com/docs#place-multiple-orders + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + ordersRequests = [] + for i in range(0, len(orders)): + rawOrder = orders[i] + marketId = self.safe_string(rawOrder, 'symbol') + type = self.safe_string(rawOrder, 'type') + side = self.safe_string(rawOrder, 'side') + amount = self.safe_value(rawOrder, 'amount') + price = self.safe_value(rawOrder, 'price') + orderParams = self.safe_dict(rawOrder, 'params', {}) + extendedParams = self.extend(orderParams, params) # the request does not accept extra params since it's a list, so we're extending each order with the common params + orderRequest = self.create_order_request(marketId, type, side, amount, price, extendedParams) + ordersRequests.append(orderRequest) + response = self.privatePostTradeBatchOrders(ordersRequests) + data = self.safe_list(response, 'data', []) + return self.parse_orders(data) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + Fetch orders that are still open + + https://blofin.com/docs#get-active-orders + https://blofin.com/docs#get-active-tpsl-orders + https://docs.blofin.com/index.html#get-active-algo-orders + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.trigger]: True if fetching trigger or conditional orders + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOpenOrders', 'paginate') + if paginate: + return self.fetch_paginated_call_dynamic('fetchOpenOrders', symbol, since, limit, params) + request: dict = { + } + market = None + if symbol is not None: + market = self.market(symbol) + request['instId'] = market['id'] + if limit is not None: + request['limit'] = limit # default 100, max 100 + isTrigger = self.safe_bool_n(params, ['stop', 'trigger'], False) + isTpSl = self.safe_bool_2(params, 'tpsl', 'TPSL', False) + method: Str = None + method, params = self.handle_option_and_params(params, 'fetchOpenOrders', 'method', 'privateGetTradeOrdersPending') + query = self.omit(params, ['method', 'stop', 'trigger', 'tpsl', 'TPSL']) + response = None + if isTpSl or (method == 'privateGetTradeOrdersTpslPending'): + response = self.privateGetTradeOrdersTpslPending(self.extend(request, query)) + elif isTrigger or (method == 'privateGetTradeOrdersAlgoPending'): + request['orderType'] = 'trigger' + response = self.privateGetTradeOrdersAlgoPending(self.extend(request, query)) + else: + response = self.privateGetTradeOrdersPending(self.extend(request, query)) + data = self.safe_list(response, 'data', []) + return self.parse_orders(data, market, since, limit) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + fetch all trades made by the user + + https://blofin.com/docs#get-trade-history + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: Timestamp in ms of the latest time to retrieve trades for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchMyTrades', 'paginate') + if paginate: + return self.fetch_paginated_call_dynamic('fetchMyTrades', symbol, since, limit, params) + request: dict = { + } + market = None + if symbol is not None: + market = self.market(symbol) + request['instId'] = market['id'] + request, params = self.handle_until_option('end', request, params) + if limit is not None: + request['limit'] = limit # default 100, max 100 + response = self.privateGetTradeFillsHistory(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + return self.parse_trades(data, market, since, limit) + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://blofin.com/docs#get-deposite-history + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch entries for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchDeposits', 'paginate') + if paginate: + return self.fetch_paginated_call_dynamic('fetchDeposits', code, since, limit, params) + request: dict = { + } + currency = None + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + if since is not None: + request['before'] = max(since - 1, 0) + if limit is not None: + request['limit'] = limit # default 100, max 100 + request, params = self.handle_until_option('after', request, params) + response = self.privateGetAssetDepositHistory(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + return self.parse_transactions(data, currency, since, limit, params) + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://blofin.com/docs#get-withdraw-history + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch entries for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchWithdrawals', 'paginate') + if paginate: + return self.fetch_paginated_call_dynamic('fetchWithdrawals', code, since, limit, params) + request: dict = { + } + currency = None + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + if since is not None: + request['before'] = max(since - 1, 0) + if limit is not None: + request['limit'] = limit # default 100, max 100 + request, params = self.handle_until_option('after', request, params) + response = self.privateGetAssetWithdrawalHistory(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + return self.parse_transactions(data, currency, since, limit, params) + + def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + + https://blofin.com/docs#get-funds-transfer-history + + :param str [code]: unified currency code, default is None + :param int [since]: timestamp in ms of the earliest ledger entry, default is None + :param int [limit]: max number of ledger entries to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' + :param int [params.until]: the latest time in ms to fetch entries for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict: a `ledger structure ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchLedger', 'paginate') + if paginate: + return self.fetch_paginated_call_dynamic('fetchLedger', code, since, limit, params) + request: dict = { + } + if limit is not None: + request['limit'] = limit + currency = None + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + request, params = self.handle_until_option('end', request, params) + response = None + response = self.privateGetAssetBills(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + return self.parse_ledger(data, currency, since, limit) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # + # fetchDeposits + # + # { + # "currency": "USDT", + # "chain": "TRC20", + # "address": "TGfJLtnsh3B9EqekFEBZ1nR14QanBUf5Bi", + # "txId": "892f4e0c32268b29b2e541ef30d32a30bbf10f902adcc4b1428319ed7c3758fd", + # "type": "0", + # "amount": "86.975843", + # "state": "1", + # "ts": "1703163304153", + # "tag": null, + # "confirm": "16", + # "depositId": "36c8e2a7ea184a219de72215a696acaf" + # } + # fetchWithdrawals + # { + # "currency": "USDT", + # "chain": "TRC20", + # "address": "TYgB3sVXHPEDQUu288EG1uMFh9Pk2swLgW", + # "txId": "1fd5ac52df414d7ea66194cadd9a5b4d2422c2b9720037f66d98207f9858fd96", + # "type": "0", + # "amount": "9", + # "fee": "1", + # "feeCurrency": "USDT", + # "state": "3", + # "clientId": null, + # "ts": "1707217439351", + # "tag": null, + # "memo": null, + # "withdrawId": "e0768698cfdf4aee8e54654c3775914b" + # } + # + type = None + id = None + withdrawalId = self.safe_string(transaction, 'withdrawId') + depositId = self.safe_string(transaction, 'depositId') + addressTo = self.safe_string(transaction, 'address') + address = addressTo + tagTo = self.safe_string(transaction, 'tag') + if withdrawalId is not None: + type = 'withdrawal' + id = withdrawalId + else: + id = depositId + type = 'deposit' + currencyId = self.safe_string(transaction, 'currency') + code = self.safe_currency_code(currencyId) + amount = self.safe_number(transaction, 'amount') + status = self.parse_transaction_status(self.safe_string(transaction, 'state')) + txid = self.safe_string(transaction, 'txId') + timestamp = self.safe_integer(transaction, 'ts') + feeCurrencyId = self.safe_string(transaction, 'feeCurrency') + feeCode = self.safe_currency_code(feeCurrencyId) + feeCost = self.safe_number(transaction, 'fee') + return { + 'info': transaction, + 'id': id, + 'currency': code, + 'amount': amount, + 'network': None, + 'addressFrom': None, + 'addressTo': addressTo, + 'address': address, + 'tagFrom': None, + 'tagTo': tagTo, + 'tag': tagTo, + 'status': status, + 'type': type, + 'updated': None, + 'txid': txid, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'internal': None, + 'comment': None, + 'fee': { + 'currency': feeCode, + 'cost': feeCost, + }, + } + + def parse_transaction_status(self, status: Str): + statuses: dict = { + '0': 'pending', + '1': 'ok', + '2': 'failed', + '3': 'pending', + } + return self.safe_string(statuses, status, status) + + def parse_ledger_entry_type(self, type): + types: dict = { + '1': 'transfer', # transfer + '2': 'trade', # trade + '3': 'trade', # delivery + '4': 'rebate', # auto token conversion + '5': 'trade', # liquidation + '6': 'transfer', # margin transfer + '7': 'trade', # interest deduction + '8': 'fee', # funding rate + '9': 'trade', # adl + '10': 'trade', # clawback + '11': 'trade', # system token conversion + } + return self.safe_string(types, type, type) + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + currencyId = self.safe_string(item, 'currency') + code = self.safe_currency_code(currencyId, currency) + currency = self.safe_currency(currencyId, currency) + timestamp = self.safe_integer(item, 'ts') + return self.safe_ledger_entry({ + 'info': item, + 'id': self.safe_string(item, 'transferId'), + 'direction': None, + 'account': None, + 'referenceId': self.safe_string(item, 'clientId'), + 'referenceAccount': None, + 'type': self.parse_ledger_entry_type(self.safe_string(item, 'type')), + 'currency': code, + 'amount': self.safe_number(item, 'amount'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'before': None, + 'after': None, + 'status': 'ok', + 'fee': None, + }, currency) + + def parse_ids(self, ids): + """ + @ignore + :param string[]|str ids: order ids + :returns str[]: list of order ids + """ + if isinstance(ids, str): + return ids.split(',') + else: + return ids + + def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): + """ + cancel multiple orders + + https://blofin.com/docs#cancel-multiple-orders + + :param str[] ids: order ids + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: whether the order is a stop/trigger order + :returns dict: an list of `order structures ` + """ + # TODO : the original endpoint signature differs, according to that you can skip individual symbol and assign ids in batch. At self moment, `params` is not being used too. + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrders() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request = [] + options = self.safe_dict(self.options, 'cancelOrders', {}) + defaultMethod = self.safe_string(options, 'method', 'privatePostTradeCancelBatchOrders') + method = self.safe_string(params, 'method', defaultMethod) + clientOrderIds = self.parse_ids(self.safe_value(params, 'clientOrderId')) + tpslIds = self.parse_ids(self.safe_value(params, 'tpslId')) + trigger = self.safe_bool_n(params, ['stop', 'trigger', 'tpsl']) + if trigger: + method = 'privatePostTradeCancelTpsl' + if clientOrderIds is None: + ids = self.parse_ids(ids) + if tpslIds is not None: + for i in range(0, len(tpslIds)): + request.append({ + 'tpslId': tpslIds[i], + 'instId': market['id'], + }) + for i in range(0, len(ids)): + if trigger: + request.append({ + 'tpslId': ids[i], + 'instId': market['id'], + }) + else: + request.append({ + 'orderId': ids[i], + 'instId': market['id'], + }) + else: + for i in range(0, len(clientOrderIds)): + request.append({ + 'instId': market['id'], + 'clientOrderId': clientOrderIds[i], + }) + response = None + if method == 'privatePostTradeCancelTpsl': + response = self.privatePostTradeCancelTpsl(request) # * dont self.extend with params, otherwise ARRAY will be turned into OBJECT + else: + response = self.privatePostTradeCancelBatchOrders(request) # * dont self.extend with params, otherwise ARRAY will be turned into OBJECT + ordersData = self.safe_list(response, 'data', []) + return self.parse_orders(ordersData, market, None, None, params) + + def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://blofin.com/docs#funds-transfer + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account to transfer from(funding, swap, copy_trading, earn) + :param str toAccount: account to transfer to(funding, swap, copy_trading, earn) + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transfer structure ` + """ + self.load_markets() + currency = self.currency(code) + accountsByType = self.safe_dict(self.options, 'accountsByType', {}) + fromId = self.safe_string(accountsByType, fromAccount, fromAccount) + toId = self.safe_string(accountsByType, toAccount, toAccount) + request: dict = { + 'currency': currency['id'], + 'amount': self.currency_to_precision(code, amount), + 'fromAccount': fromId, + 'toAccount': toId, + } + response = self.privatePostAssetTransfer(self.extend(request, params)) + data = self.safe_dict(response, 'data', {}) + return self.parse_transfer(data, currency) + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + id = self.safe_string(transfer, 'transferId') + return { + 'info': transfer, + 'id': id, + 'timestamp': None, + 'datetime': None, + 'currency': None, + 'amount': None, + 'fromAccount': None, + 'toAccount': None, + 'status': None, + } + + def fetch_position(self, symbol: str, params={}) -> Position: + """ + fetch data on a single open contract trade position + + https://blofin.com/docs#get-positions + + :param str symbol: unified market symbol of the market the position is held in, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.instType]: MARGIN, SWAP, FUTURES, OPTION + :returns dict: a `position structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + } + response = self.privateGetAccountPositions(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + position = self.safe_dict(data, 0) + if position is None: + return None + return self.parse_position(position, market) + + def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch data on a single open contract trade position + + https://blofin.com/docs#get-positions + + :param str[] [symbols]: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.instType]: MARGIN, SWAP, FUTURES, OPTION + :returns dict: a `position structure ` + """ + self.load_markets() + symbols = self.market_symbols(symbols) + response = self.privateGetAccountPositions(params) + data = self.safe_list(response, 'data', []) + result = self.parse_positions(data) + return self.filter_by_array_positions(result, 'symbol', symbols, False) + + def parse_position(self, position: dict, market: Market = None): + # + # response similar for REST & WS + # + # { + # instType: 'SWAP', + # instId: 'LTC-USDT', + # marginMode: 'cross', + # positionId: '644159', + # positionSide: 'net', + # positions: '1', + # availablePositions: '1', + # averagePrice: '68.16', + # unrealizedPnl: '0.80631223', + # unrealizedPnlRatio: '0.03548909463028169', + # leverage: '3', + # liquidationPrice: '10.116655172370356435', + # markPrice: '68.96', + # initialMargin: '22.988770743333333333', + # margin: '', # self field might not exist in rest response + # marginRatio: '152.523509620342499273', + # maintenanceMargin: '0.34483156115', + # adl: '4', + # createTime: '1707235776528', + # updateTime: '1707235776528' + # } + # + marketId = self.safe_string(position, 'instId') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + pos = self.safe_string(position, 'positions') + contractsAbs = Precise.string_abs(pos) + side = self.safe_string(position, 'positionSide') + hedged = side != 'net' + contracts = self.parse_number(contractsAbs) + if pos is not None: + if side == 'net': + if Precise.string_gt(pos, '0'): + side = 'long' + elif Precise.string_lt(pos, '0'): + side = 'short' + else: + side = None + contractSize = self.safe_number(market, 'contractSize') + contractSizeString = self.number_to_string(contractSize) + markPriceString = self.safe_string(position, 'markPrice') + notionalString = self.safe_string(position, 'notionalUsd') + if market['inverse']: + notionalString = Precise.string_div(Precise.string_mul(contractsAbs, contractSizeString), markPriceString) + notional = self.parse_number(notionalString) + marginMode = self.safe_string(position, 'marginMode') + initialMarginString = None + entryPriceString = self.safe_string(position, 'averagePrice') + unrealizedPnlString = self.safe_string(position, 'unrealizedPnl') + leverageString = self.safe_string(position, 'leverage') + initialMarginPercentage = None + collateralString = None + if marginMode == 'cross': + initialMarginString = self.safe_string(position, 'initialMargin') + collateralString = Precise.string_add(initialMarginString, unrealizedPnlString) + elif marginMode == 'isolated': + initialMarginPercentage = Precise.string_div('1', leverageString) + collateralString = self.safe_string(position, 'margin') + maintenanceMarginString = self.safe_string(position, 'maintenanceMargin') + maintenanceMargin = self.parse_number(maintenanceMarginString) + maintenanceMarginPercentageString = Precise.string_div(maintenanceMarginString, notionalString) + if initialMarginPercentage is None: + initialMarginPercentage = self.parse_number(Precise.string_div(initialMarginString, notionalString, 4)) + elif initialMarginString is None: + initialMarginString = Precise.string_mul(initialMarginPercentage, notionalString) + rounder = '0.00005' # round to closest 0.01% + maintenanceMarginPercentage = self.parse_number(Precise.string_div(Precise.string_add(maintenanceMarginPercentageString, rounder), '1', 4)) + liquidationPrice = self.safe_number(position, 'liquidationPrice') + percentageString = self.safe_string(position, 'unrealizedPnlRatio') + percentage = self.parse_number(Precise.string_mul(percentageString, '100')) + timestamp = self.safe_integer(position, 'updateTime') + marginRatio = self.parse_number(Precise.string_div(maintenanceMarginString, collateralString, 4)) + return self.safe_position({ + 'info': position, + 'id': None, + 'symbol': symbol, + 'notional': notional, + 'marginMode': marginMode, + 'liquidationPrice': liquidationPrice, + 'entryPrice': self.parse_number(entryPriceString), + 'unrealizedPnl': self.parse_number(unrealizedPnlString), + 'percentage': percentage, + 'contracts': contracts, + 'contractSize': contractSize, + 'markPrice': self.parse_number(markPriceString), + 'lastPrice': None, + 'side': side, + 'hedged': hedged, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastUpdateTimestamp': None, + 'maintenanceMargin': maintenanceMargin, + 'maintenanceMarginPercentage': maintenanceMarginPercentage, + 'collateral': self.parse_number(collateralString), + 'initialMargin': self.parse_number(initialMarginString), + 'initialMarginPercentage': self.parse_number(initialMarginPercentage), + 'leverage': self.parse_number(leverageString), + 'marginRatio': marginRatio, + 'stopLossPrice': None, + 'takeProfitPrice': None, + }) + + def fetch_leverages(self, symbols: Strings = None, params={}) -> Leverages: + """ + fetch the set leverage for all contract markets + + https://docs.blofin.com/index.html#get-multiple-leverage + + :param str[] symbols: a list of unified market symbols, required on blofin + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' + :returns dict: a list of `leverage structures ` + """ + self.load_markets() + if symbols is None: + raise ArgumentsRequired(self.id + ' fetchLeverages() requires a symbols argument') + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchLeverages', params) + if marginMode is None: + marginMode = self.safe_string(params, 'marginMode', 'cross') # cross marginMode + if (marginMode != 'cross') and (marginMode != 'isolated'): + raise BadRequest(self.id + ' fetchLeverages() requires a marginMode parameter that must be either cross or isolated') + symbols = self.market_symbols(symbols) + instIds = '' + for i in range(0, len(symbols)): + entry = symbols[i] + entryMarket = self.market(entry) + if i > 0: + instIds = instIds + ',' + entryMarket['id'] + else: + instIds = instIds + entryMarket['id'] + request: dict = { + 'instId': instIds, + 'marginMode': marginMode, + } + response = self.privateGetAccountBatchLeverageInfo(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "success", + # "data": [ + # { + # "leverage": "3", + # "marginMode": "cross", + # "instId": "BTC-USDT" + # }, + # ] + # } + # + leverages = self.safe_list(response, 'data', []) + return self.parse_leverages(leverages, symbols, 'instId') + + def fetch_leverage(self, symbol: str, params={}) -> Leverage: + """ + fetch the set leverage for a market + + https://docs.blofin.com/index.html#get-leverage + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' + :returns dict: a `leverage structure ` + """ + self.load_markets() + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchLeverage', params) + if marginMode is None: + marginMode = self.safe_string(params, 'marginMode', 'cross') # cross marginMode + if (marginMode != 'cross') and (marginMode != 'isolated'): + raise BadRequest(self.id + ' fetchLeverage() requires a marginMode parameter that must be either cross or isolated') + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + 'marginMode': marginMode, + } + response = self.privateGetAccountLeverageInfo(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "success", + # "data": { + # "leverage": "3", + # "marginMode": "cross", + # "instId": "BTC-USDT" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_leverage(data, market) + + def parse_leverage(self, leverage: dict, market: Market = None) -> Leverage: + marketId = self.safe_string(leverage, 'instId') + leverageValue = self.safe_integer(leverage, 'leverage') + return { + 'info': leverage, + 'symbol': self.safe_symbol(marketId, market), + 'marginMode': self.safe_string_lower(leverage, 'marginMode'), + 'longLeverage': leverageValue, + 'shortLeverage': leverageValue, + } + + def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://blofin.com/docs#set-leverage + + :param int leverage: the rate of leverage + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' + :param str [params.positionSide]: 'long' or 'short' - required for hedged mode in isolated margin + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setLeverage() requires a symbol argument') + # WARNING: THIS WILL INCREASE LIQUIDATION PRICE FOR OPEN ISOLATED LONG POSITIONS + # AND DECREASE LIQUIDATION PRICE FOR OPEN ISOLATED SHORT POSITIONS + if (leverage < 1) or (leverage > 125): + raise BadRequest(self.id + ' setLeverage() leverage should be between 1 and 125') + self.load_markets() + market = self.market(symbol) + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('setLeverage', params, 'cross') + if (marginMode != 'cross') and (marginMode != 'isolated'): + raise BadRequest(self.id + ' setLeverage() requires a marginMode parameter that must be either cross or isolated') + request: dict = { + 'leverage': leverage, + 'marginMode': marginMode, + 'instId': market['id'], + } + response = self.privatePostAccountSetLeverage(self.extend(request, params)) + return response + + def close_position(self, symbol: str, side: OrderSide = None, params={}) -> Order: + """ + closes open positions for a market + + https://blofin.com/docs#close-positions + + :param str symbol: Unified CCXT market symbol + :param str [side]: 'buy' or 'sell', leave in net mode + :param dict [params]: extra parameters specific to the blofin api endpoint + :param str [params.clientOrderId]: a unique identifier for the order + :param str [params.marginMode]: 'cross' or 'isolated', default is 'cross + :param str [params.code]: *required in the case of closing cross MARGIN position for Single-currency margin* margin currency + + EXCHANGE SPECIFIC PARAMETERS + :param boolean [params.autoCxl]: whether any pending orders for closing out needs to be automatically canceled when close position via a market order. False or True, the default is False + :param str [params.tag]: order tag a combination of case-sensitive alphanumerics, all numbers, or all letters of up to 16 characters + :returns dict[]: `A list of position structures ` + """ + self.load_markets() + market = self.market(symbol) + clientOrderId = self.safe_string(params, 'clientOrderId') + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('closePosition', params, 'cross') + request: dict = { + 'instId': market['id'], + 'marginMode': marginMode, + } + if clientOrderId is not None: + request['clientOrderId'] = clientOrderId + response = self.privatePostTradeClosePosition(self.extend(request, params)) + return self.safe_dict(response, 'data') + + def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://blofin.com/docs#get-order-history + https://blofin.com/docs#get-tpsl-order-history + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of orde structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.trigger]: True if fetching trigger or conditional orders + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchClosedOrders', 'paginate') + if paginate: + return self.fetch_paginated_call_dynamic('fetchClosedOrders', symbol, since, limit, params) + request: dict = { + } + market = None + if symbol is not None: + market = self.market(symbol) + request['instId'] = market['id'] + if limit is not None: + request['limit'] = limit # default 100, max 100 + if since is not None: + request['begin'] = since + isTrigger = self.safe_bool_n(params, ['stop', 'trigger', 'tpsl', 'TPSL'], False) + method: Str = None + method, params = self.handle_option_and_params(params, 'fetchOpenOrders', 'method', 'privateGetTradeOrdersHistory') + query = self.omit(params, ['method', 'stop', 'trigger', 'tpsl', 'TPSL']) + response = None + if (isTrigger) or (method == 'privateGetTradeOrdersTpslHistory'): + response = self.privateGetTradeOrdersTpslHistory(self.extend(request, query)) + else: + response = self.privateGetTradeOrdersHistory(self.extend(request, query)) + data = self.safe_list(response, 'data', []) + return self.parse_orders(data, market, since, limit) + + def fetch_margin_mode(self, symbol: str, params={}) -> MarginMode: + """ + fetches the margin mode of a trading pair + + https://docs.blofin.com/index.html#get-margin-mode + + :param str symbol: unified symbol of the market to fetch the margin mode for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin mode structure ` + """ + self.load_markets() + market = self.market(symbol) + response = self.privateGetAccountMarginMode(params) + # + # { + # "code": "0", + # "msg": "success", + # "data": { + # "marginMode": "cross" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_margin_mode(data, market) + + def parse_margin_mode(self, marginMode: dict, market: Market = None) -> MarginMode: + return { + 'info': marginMode, + 'symbol': self.safe_string(market, 'symbol'), + 'marginMode': self.safe_string(marginMode, 'marginMode'), + } + + def set_margin_mode(self, marginMode: str, symbol: Str = None, params={}): + """ + set margin mode to 'cross' or 'isolated' + + https://docs.blofin.com/index.html#set-margin-mode + + :param str marginMode: 'cross' or 'isolated' + :param str [symbol]: unified market symbol(not used in blofin setMarginMode) + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + self.check_required_argument('setMarginMode', marginMode, 'marginMode', ['cross', 'isolated']) + self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + request: dict = { + 'marginMode': marginMode, + } + response = self.privatePostAccountSetMarginMode(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "success", + # "data": { + # "marginMode": "isolated" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_margin_mode(data, market) + + def fetch_position_mode(self, symbol: Str = None, params={}): + """ + fetchs the position mode, hedged or one way + + https://docs.blofin.com/index.html#get-position-mode + + :param str [symbol]: unified symbol of the market to fetch the position mode for(not used in blofin fetchPositionMode) + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an object detailing whether the market is in hedged or one-way mode + """ + response = self.privateGetAccountPositionMode(params) + data = self.safe_dict(response, 'data', {}) + positionMode = self.safe_string(data, 'positionMode') + # + # { + # "code": "0", + # "msg": "success", + # "data": { + # "positionMode": "long_short_mode" + # } + # } + # + return { + 'info': data, + 'hedged': positionMode == 'long_short_mode', + } + + def set_position_mode(self, hedged: bool, symbol: Str = None, params={}): + """ + set hedged to True or False for a market + + https://docs.blofin.com/index.html#set-position-mode + + :param bool hedged: set to True to use hedged mode, False for one-way mode + :param str [symbol]: not used by blofin setPositionMode() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + request: dict = { + 'positionMode': 'long_short_mode' if hedged else 'net_mode', + } + # + # { + # "code": "0", + # "msg": "success", + # "data": { + # "positionMode": "net_mode" + # } + # } + # + return self.privatePostAccountSetPositionMode(self.extend(request, params)) + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None # fallback to default error handler + # + # {"code":"152002","msg":"Parameter bar error."} + # + code = self.safe_string(response, 'code') + message = self.safe_string(response, 'msg') + feedback = self.id + ' ' + body + if code is not None and code != '0': + self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], code, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + raise ExchangeError(feedback) # unknown message + # + # { + # orderId: null, + # clientOrderId: '', + # msg: 'Order failed. Insufficient USDT margin in account', + # code: '103003' + # } + # + data = self.safe_list(response, 'data') + first = self.safe_dict(data, 0) + insideMsg = self.safe_string(first, 'msg') + insideCode = self.safe_string(first, 'code') + if insideCode is not None and insideCode != '0': + self.throw_exactly_matched_exception(self.exceptions['exact'], insideCode, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], insideMsg, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], insideMsg, feedback) + return None + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + request = '/api/' + self.version + '/' + self.implode_params(path, params) + query = self.omit(params, self.extract_params(path)) + url = self.implode_hostname(self.urls['api']['rest']) + request + # type = self.getPathAuthenticationType(path) + if api == 'public': + if not self.is_empty(query): + url += '?' + self.urlencode(query) + elif api == 'private': + self.check_required_credentials() + timestamp = str(self.milliseconds()) + headers = { + 'ACCESS-KEY': self.apiKey, + 'ACCESS-PASSPHRASE': self.password, + 'ACCESS-TIMESTAMP': timestamp, + 'ACCESS-NONCE': timestamp, + } + sign_body = '' + if method == 'GET': + if not self.is_empty(query): + urlencodedQuery = '?' + self.urlencode(query) + url += urlencodedQuery + request += urlencodedQuery + else: + if not self.is_empty(query): + body = self.json(query) + sign_body = body + headers['Content-Type'] = 'application/json' + auth = request + method + timestamp + timestamp + sign_body + signature = self.string_to_base64(self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256)) + headers['ACCESS-SIGN'] = signature + return {'url': url, 'method': method, 'body': body, 'headers': headers} diff --git a/ccxt/btcalpha.py b/ccxt/btcalpha.py new file mode 100644 index 0000000..b12b7a4 --- /dev/null +++ b/ccxt/btcalpha.py @@ -0,0 +1,1030 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.btcalpha import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currency, IndexType, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class btcalpha(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(btcalpha, self).describe(), { + 'id': 'btcalpha', + 'name': 'BTC-Alpha', + 'countries': ['US'], + 'version': 'v1', + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelOrder': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createDepositAddress': False, + 'createOrder': True, + 'createOrderWithTakeProfitAndStopLoss': False, + 'createOrderWithTakeProfitAndStopLossWs': False, + 'createPostOnlyOrder': False, + 'createReduceOnlyOrder': False, + 'createStopLimitOrder': False, + 'createStopMarketOrder': False, + 'createStopOrder': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': False, + 'fetchDeposit': False, + 'fetchDepositAddress': False, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchL2OrderBook': True, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': False, + 'fetchTransfer': False, + 'fetchTransfers': False, + 'fetchVolatilityHistory': False, + 'fetchWithdrawal': False, + 'fetchWithdrawals': True, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'repayMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'transfer': False, + 'withdraw': False, + }, + 'timeframes': { + '5m': '5', + '15m': '15', + '30m': '30', + '1h': '60', + '4h': '240', + '1d': 'D', + }, + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/dce49f3a-61e5-4ba0-a2fe-41d192fd0e5d', + 'api': { + 'rest': 'https://btc-alpha.com/api', + }, + 'www': 'https://btc-alpha.com', + 'doc': 'https://btc-alpha.github.io/api-docs', + 'fees': 'https://btc-alpha.com/fees/', + 'referral': 'https://btc-alpha.com/?r=123788', + }, + 'api': { + 'public': { + 'get': [ + 'currencies/', + 'pairs/', + 'orderbook/{pair_name}', + 'exchanges/', + 'charts/{pair}/{type}/chart/', + 'ticker/', + ], + }, + 'private': { + 'get': [ + 'wallets/', + 'orders/own/', + 'order/{id}/', + 'exchanges/own/', + 'deposits/', + 'withdraws/', + ], + 'post': [ + 'order/', + 'order-cancel/', + ], + }, + }, + 'fees': { + 'trading': { + 'maker': self.parse_number('0.002'), + 'taker': self.parse_number('0.002'), + }, + 'funding': { + 'withdraw': {}, + }, + }, + 'commonCurrencies': { + 'CBC': 'Cashbery', + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': False, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': False, + 'FOK': False, + 'PO': False, + 'GTD': False, + }, + 'hedged': False, + 'leverage': False, + 'marketBuyRequiresPrice': False, + 'marketBuyByCost': False, + 'selfTradePrevention': False, + 'trailing': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, + 'daysBack': None, + 'untilDays': None, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 2000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': 2000, + 'daysBack': None, + 'untilDays': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 2000, + 'daysBack': None, + 'daysBackCanceled': None, + 'untilDays': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 720, + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'exact': {}, + 'broad': { + 'Out of balance': InsufficientFunds, # {"date":1570599531.4814300537,"error":"Out of balance -9.99243661 BTC"} + }, + }, + }) + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for btcalpha + + https://btc-alpha.github.io/api-docs/#list-all-currencies + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = self.publicGetPairs(params) + # + # [ + # { + # "name": "1INCH_USDT", + # "currency1": "1INCH", + # "currency2": "USDT", + # "price_precision": 4, + # "amount_precision": 2, + # "minimum_order_size": "0.01000000", + # "maximum_order_size": "900000.00000000", + # "minimum_order_value": "10.00000000", + # "liquidity_type": 10 + # }, + # ] + # + return self.parse_markets(response) + + def parse_market(self, market: dict) -> Market: + id = self.safe_string(market, 'name') + baseId = self.safe_string(market, 'currency1') + quoteId = self.safe_string(market, 'currency2') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + pricePrecision = self.safe_string(market, 'price_precision') + priceLimit = self.parse_precision(pricePrecision) + amountLimit = self.safe_string(market, 'minimum_order_size') + return { + 'id': id, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': True, + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(self.parse_precision(self.safe_string(market, 'amount_precision'))), + 'price': self.parse_number(self.parse_precision((pricePrecision))), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.parse_number(amountLimit), + 'max': self.safe_number(market, 'maximum_order_size'), + }, + 'price': { + 'min': self.parse_number(priceLimit), + 'max': None, + }, + 'cost': { + 'min': self.parse_number(Precise.string_mul(priceLimit, amountLimit)), + 'max': None, + }, + }, + 'created': None, + 'info': market, + } + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + + https://btc-alpha.github.io/api-docs/#tickers + + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + response = self.publicGetTicker(params) + # + # [ + # { + # "timestamp": "1674658.445272", + # "pair": "BTC_USDT", + # "last": "22476.85", + # "diff": "458.96", + # "vol": "6660.847784", + # "high": "23106.08", + # "low": "22348.29", + # "buy": "22508.46", + # "sell": "22521.11" + # }, + # ... + # ] + # + return self.parse_tickers(response, symbols) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + + https://btc-alpha.github.io/api-docs/#tickers + + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + response = self.publicGetTicker(self.extend(request, params)) + # + # { + # "timestamp": "1674658.445272", + # "pair": "BTC_USDT", + # "last": "22476.85", + # "diff": "458.96", + # "vol": "6660.847784", + # "high": "23106.08", + # "low": "22348.29", + # "buy": "22508.46", + # "sell": "22521.11" + # } + # + return self.parse_ticker(response, market) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "timestamp": "1674658.445272", + # "pair": "BTC_USDT", + # "last": "22476.85", + # "diff": "458.96", + # "vol": "6660.847784", + # "high": "23106.08", + # "low": "22348.29", + # "buy": "22508.46", + # "sell": "22521.11" + # } + # + timestampStr = self.safe_string(ticker, 'timestamp') + timestamp = int(Precise.string_mul(timestampStr, '1000000')) + marketId = self.safe_string(ticker, 'pair') + market = self.safe_market(marketId, market, '_') + last = self.safe_string(ticker, 'last') + return self.safe_ticker({ + 'info': ticker, + 'symbol': market['symbol'], + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': self.safe_string(ticker, 'buy'), + 'bidVolume': None, + 'ask': self.safe_string(ticker, 'sell'), + 'askVolume': None, + 'vwap': None, + 'open': None, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': self.safe_string(ticker, 'diff'), + 'percentage': None, + 'average': None, + 'baseVolume': None, + 'quoteVolume': self.safe_string(ticker, 'vol'), + }, market) + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + + https://btc-alpha.github.io/api-docs/#get-orderbook + + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair_name': market['id'], + } + if limit: + request['limit_sell'] = limit + request['limit_buy'] = limit + response = self.publicGetOrderbookPairName(self.extend(request, params)) + return self.parse_order_book(response, market['symbol'], None, 'buy', 'sell', 'price', 'amount') + + def parse_bids_asks(self, bidasks, priceKey: IndexType = 0, amountKey: IndexType = 1, countOrIdKey: IndexType = 2): + result = [] + for i in range(0, len(bidasks)): + bidask = bidasks[i] + if bidask: + result.append(self.parse_bid_ask(bidask, priceKey, amountKey)) + return result + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(public) + # + # { + # "id": "202203440", + # "timestamp": "1637856276.264215", + # "pair": "AAVE_USDT", + # "price": "320.79900000", + # "amount": "0.05000000", + # "type": "buy" + # } + # + # fetchMyTrades(private) + # + # { + # "id": "202203440", + # "timestamp": "1637856276.264215", + # "pair": "AAVE_USDT", + # "price": "320.79900000", + # "amount": "0.05000000", + # "type": "buy", + # "my_side": "buy" + # } + # + marketId = self.safe_string(trade, 'pair') + market = self.safe_market(marketId, market, '_') + timestampRaw = self.safe_string(trade, 'timestamp') + timestamp = self.parse_to_int(Precise.string_mul(timestampRaw, '1000000')) + priceString = self.safe_string(trade, 'price') + amountString = self.safe_string(trade, 'amount') + id = self.safe_string(trade, 'id') + side = self.safe_string_2(trade, 'my_side', 'type') + return self.safe_trade({ + 'id': id, + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'order': id, + 'type': 'limit', + 'side': side, + 'takerOrMaker': None, + 'price': priceString, + 'amount': amountString, + 'cost': None, + 'fee': None, + }, market) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://btc-alpha.github.io/api-docs/#list-all-exchanges + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = None + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['pair'] = market['id'] + if limit is not None: + request['limit'] = limit + trades = self.publicGetExchanges(self.extend(request, params)) + return self.parse_trades(trades, market, since, limit) + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://btc-alpha.github.io/api-docs/#list-own-deposits + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + currency = None + if code is not None: + currency = self.currency(code) + response = self.privateGetDeposits(params) + # + # [ + # { + # "timestamp": 1485363039.18359, + # "id": 317, + # "currency": "BTC", + # "amount": 530.00000000 + # } + # ] + # + return self.parse_transactions(response, currency, since, limit, {'type': 'deposit'}) + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://btc-alpha.github.io/api-docs/#list-own-made-withdraws + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + currency = None + request: dict = {} + if code is not None: + currency = self.currency(code) + request['currency_id'] = currency['id'] + response = self.privateGetWithdraws(self.extend(request, params)) + # + # [ + # { + # "id": 403, + # "timestamp": 1485363466.868539, + # "currency": "BTC", + # "amount": 0.53000000, + # "status": 20 + # } + # ] + # + return self.parse_transactions(response, currency, since, limit, {'type': 'withdrawal'}) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # deposit + # { + # "timestamp": 1485363039.18359, + # "id": 317, + # "currency": "BTC", + # "amount": 530.00000000 + # } + # + # withdrawal + # { + # "id": 403, + # "timestamp": 1485363466.868539, + # "currency": "BTC", + # "amount": 0.53000000, + # "status": 20 + # } + # + timestamp = self.safe_timestamp(transaction, 'timestamp') + currencyId = self.safe_string(transaction, 'currency') + statusId = self.safe_string(transaction, 'status') + return { + 'id': self.safe_string(transaction, 'id'), + 'info': transaction, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': None, + 'address': None, + 'addressTo': None, + 'addressFrom': None, + 'tag': None, + 'tagTo': None, + 'tagFrom': None, + 'currency': self.safe_currency_code(currencyId, currency), + 'amount': self.safe_number(transaction, 'amount'), + 'txid': None, + 'type': None, + 'status': self.parse_transaction_status(statusId), + 'comment': None, + 'internal': None, + 'fee': None, + 'updated': None, + } + + def parse_transaction_status(self, status: Str): + statuses: dict = { + '10': 'pending', # New + '20': 'pending', # Verified, waiting for approving + '30': 'ok', # Approved by moderator + '40': 'failed', # Refused by moderator. See your email for more details + '50': 'canceled', # Cancelled by user + } + return self.safe_string(statuses, status, status) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # { + # "time":1591296000, + # "open":0.024746, + # "close":0.024728, + # "low":0.024728, + # "high":0.024753, + # "volume":16.624 + # } + # + return [ + self.safe_timestamp(ohlcv, 'time'), + self.safe_number(ohlcv, 'open'), + self.safe_number(ohlcv, 'high'), + self.safe_number(ohlcv, 'low'), + self.safe_number(ohlcv, 'close'), + self.safe_number(ohlcv, 'volume'), + ] + + def fetch_ohlcv(self, symbol: str, timeframe: str = '5m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://btc-alpha.github.io/api-docs/#charts + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + 'type': self.safe_string(self.timeframes, timeframe, timeframe), + } + if limit is not None: + request['limit'] = limit + if since is not None: + request['since'] = self.parse_to_int(since / 1000) + response = self.publicGetChartsPairTypeChart(self.extend(request, params)) + # + # [ + # {"time":1591296000,"open":0.024746,"close":0.024728,"low":0.024728,"high":0.024753,"volume":16.624}, + # {"time":1591295700,"open":0.024718,"close":0.02475,"low":0.024711,"high":0.02475,"volume":31.645}, + # {"time":1591295400,"open":0.024721,"close":0.024717,"low":0.024711,"high":0.02473,"volume":65.071} + # ] + # + return self.parse_ohlcvs(response, market, timeframe, since, limit) + + def parse_balance(self, response) -> Balances: + result: dict = {'info': response} + for i in range(0, len(response)): + balance = response[i] + currencyId = self.safe_string(balance, 'currency') + code = self.safe_currency_code(currencyId) + account = self.account() + account['used'] = self.safe_string(balance, 'reserve') + account['total'] = self.safe_string(balance, 'balance') + result[code] = account + return self.safe_balance(result) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://btc-alpha.github.io/api-docs/#list-own-wallets + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + response = self.privateGetWallets(params) + return self.parse_balance(response) + + def parse_order_status(self, status: Str): + statuses: dict = { + '1': 'open', + '2': 'canceled', + '3': 'closed', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # fetchClosedOrders / fetchOrder + # { + # "id": "923763073", + # "date": "1635451090368", + # "type": "sell", + # "pair": "XRP_USDT", + # "price": "1.00000000", + # "amount": "0.00000000", + # "status": "3", + # "amount_filled": "10.00000000", + # "amount_original": "10.0" + # "trades": [], + # } + # + # createOrder + # { + # "success": True, + # "date": "1635451754.497541", + # "type": "sell", + # "oid": "923776755", + # "price": "1.0", + # "amount": "10.0", + # "amount_filled": "0.0", + # "amount_original": "10.0", + # "trades": [] + # } + # + marketId = self.safe_string(order, 'pair') + market = self.safe_market(marketId, market, '_') + symbol = market['symbol'] + success = self.safe_bool(order, 'success', False) + timestamp = None + if success: + timestamp = self.safe_timestamp(order, 'date') + else: + timestamp = self.safe_integer(order, 'date') + price = self.safe_string(order, 'price') + remaining = self.safe_string(order, 'amount') + filled = self.safe_string(order, 'amount_filled') + amount = self.safe_string(order, 'amount_original') + status = self.parse_order_status(self.safe_string(order, 'status')) + id = self.safe_string_n(order, ['oid', 'id', 'order']) + trades = self.safe_value(order, 'trades') + side = self.safe_string_2(order, 'my_side', 'type') + return self.safe_order({ + 'id': id, + 'clientOrderId': None, + 'datetime': self.iso8601(timestamp), + 'timestamp': timestamp, + 'status': status, + 'symbol': symbol, + 'type': 'limit', + 'timeInForce': None, + 'postOnly': None, + 'side': side, + 'price': price, + 'triggerPrice': None, + 'cost': None, + 'amount': amount, + 'filled': filled, + 'remaining': remaining, + 'trades': trades, + 'fee': None, + 'info': order, + 'lastTradeTimestamp': None, + 'average': None, + }, market) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + + https://btc-alpha.github.io/api-docs/#create-order + + create a trade order + :param str symbol: unified symbol of the market to create an order in + :param str type: 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + if type == 'market': + raise InvalidOrder(self.id + ' only limits orders are supported') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + 'type': side, + 'amount': amount, + 'price': self.price_to_precision(symbol, price), + } + response = self.privatePostOrder(self.extend(request, params)) + if not response['success']: + raise InvalidOrder(self.id + ' ' + self.json(response)) + order = self.parse_order(response, market) + orderAmount = str(order['amount']) + amount = order['amount'] if Precise.string_gt(orderAmount, '0') else amount + order['amount'] = self.parse_number(amount) + return order + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + + https://btc-alpha.github.io/api-docs/#cancel-order + + cancels an open order + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + request: dict = { + 'order': id, + } + response = self.privatePostOrderCancel(self.extend(request, params)) + # + # { + # "order": 63568 + # } + # + return self.parse_order(response) + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + + https://btc-alpha.github.io/api-docs/#retrieve-single-order + + fetches information on an order made by the user + :param str id: the order id + :param str symbol: not used by btcalpha fetchOrder + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + request: dict = { + 'id': id, + } + order = self.privateGetOrderId(self.extend(request, params)) + return self.parse_order(order) + + def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + + https://btc-alpha.github.io/api-docs/#list-own-orders + + fetches information on multiple orders made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['pair'] = market['id'] + if limit is not None: + request['limit'] = limit + orders = self.privateGetOrdersOwn(self.extend(request, params)) + return self.parse_orders(orders, market, since, limit) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://btc-alpha.github.io/api-docs/#list-own-orders + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + request: dict = { + 'status': '1', + } + return self.fetch_orders(symbol, since, limit, self.extend(request, params)) + + def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://btc-alpha.github.io/api-docs/#list-own-orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + request: dict = { + 'status': '3', + } + return self.fetch_orders(symbol, since, limit, self.extend(request, params)) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://btc-alpha.github.io/api-docs/#list-own-exchanges + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['pair'] = market['id'] + if limit is not None: + request['limit'] = limit + trades = self.privateGetExchangesOwn(self.extend(request, params)) + return self.parse_trades(trades, None, since, limit) + + def nonce(self): + return self.milliseconds() + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + query = self.urlencode(self.keysort(self.omit(params, self.extract_params(path)))) + url = self.urls['api']['rest'] + '/' + if path != 'charts/{pair}/{type}/chart/': + url += 'v1/' + url += self.implode_params(path, params) + headers = {'Accept': 'application/json'} + if api == 'public': + if len(query): + url += '?' + query + else: + self.check_required_credentials() + payload = self.apiKey + if method == 'POST': + headers['Content-Type'] = 'application/x-www-form-urlencoded' + body = query + payload += body + elif len(query): + url += '?' + query + headers['X-KEY'] = self.apiKey + headers['X-SIGN'] = self.hmac(self.encode(payload), self.encode(self.secret), hashlib.sha256) + headers['X-NONCE'] = str(self.nonce()) + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None # fallback to default error handler + # + # {"date":1570599531.4814300537,"error":"Out of balance -9.99243661 BTC"} + # + error = self.safe_string(response, 'error') + if error is not None: + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], error, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], error, feedback) + raise ExchangeError(feedback) # unknown error + return None diff --git a/ccxt/btcbox.py b/ccxt/btcbox.py new file mode 100644 index 0000000..7062a95 --- /dev/null +++ b/ccxt/btcbox.py @@ -0,0 +1,813 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.btcbox import ImplicitAPI +import hashlib +import json +from ccxt.base.types import Any, Balances, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import DDoSProtection +from ccxt.base.errors import InvalidNonce +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class btcbox(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(btcbox, self).describe(), { + 'id': 'btcbox', + 'name': 'BtcBox', + 'countries': ['JP'], + 'rateLimit': 1000, + 'version': 'v1', + 'pro': False, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelOrder': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createOrder': True, + 'createOrderWithTakeProfitAndStopLoss': False, + 'createOrderWithTakeProfitAndStopLossWs': False, + 'createPostOnlyOrder': False, + 'createReduceOnlyOrder': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': False, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkOHLCV': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTrades': True, + 'fetchTransfer': False, + 'fetchTransfers': False, + 'fetchVolatilityHistory': False, + 'fetchWithdrawal': False, + 'fetchWithdrawals': False, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'repayMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'transfer': False, + 'withdraw': False, + 'ws': False, + }, + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/1e2cb499-8d0f-4f8f-9464-3c015cfbc76b', + 'api': { + 'rest': 'https://www.btcbox.co.jp/api', + }, + 'www': 'https://www.btcbox.co.jp/', + 'doc': 'https://blog.btcbox.jp/en/archives/8762', + 'fees': 'https://support.btcbox.co.jp/hc/en-us/articles/360001235694-Fees-introduction', + }, + 'api': { + 'public': { + 'get': [ + 'depth', + 'orders', + 'ticker', + 'tickers', + ], + }, + 'private': { + 'post': [ + 'balance', + 'trade_add', + 'trade_cancel', + 'trade_list', + 'trade_view', + 'wallet', + ], + }, + 'webApi': { + 'get': [ + 'ajax/coin/coinInfo', + ], + }, + }, + 'options': { + 'fetchMarkets': { + 'webApiEnable': True, # fetches from WEB + 'webApiRetries': 3, + }, + 'amountPrecision': '0.0001', # exchange has only few pairs and all of them + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': False, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': False, + 'FOK': False, + 'PO': False, + 'GTD': False, + }, + 'hedged': False, + 'leverage': False, + 'marketBuyRequiresPrice': False, + 'marketBuyByCost': False, + 'selfTradePrevention': False, + 'trailing': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': None, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 100, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': None, + 'untilDays': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchClosedOrders': None, + 'fetchOHLCV': None, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + '104': AuthenticationError, + '105': PermissionDenied, + '106': InvalidNonce, + '107': InvalidOrder, # price should be an integer + '200': InsufficientFunds, + '201': InvalidOrder, # amount too small + '202': InvalidOrder, # price should be [0 : 1000000] + '203': OrderNotFound, + '401': OrderNotFound, # cancel canceled, closed or non-existent order + '402': DDoSProtection, + }, + }) + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for ace + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + promise1 = self.publicGetTickers() + promise2 = self.fetch_web_endpoint('fetchMarkets', 'webApiGetAjaxCoinCoinInfo', True) + response1, response2 = [promise1, promise2] + # + result2Data = self.safe_dict(response2, 'data', {}) + marketIds = list(response1.keys()) + markets = [] + for i in range(0, len(marketIds)): + marketId = marketIds[i] + symbolParts = marketId.split('_') + baseCurr = self.safe_string(symbolParts, 0) + quote = self.safe_string(symbolParts, 1) + quoteId = quote.lower() + id = baseCurr.lower() + res = response1[marketId] + symbol = baseCurr + '/' + quote + fee = self.parse_number('0.0005') if (id == 'BTC') else self.parse_number('0.0010') + details = self.safe_dict(result2Data, id, {}) + tradeDetails = self.safe_dict(details, 'trade', {}) + markets.append(self.safe_market_structure({ + 'id': id, + 'uppercaseId': None, + 'symbol': symbol, + 'base': baseCurr, + 'baseId': id, + 'quote': quote, + 'quoteId': quoteId, + 'settle': None, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'taker': fee, + 'maker': fee, + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + 'leverage': { + 'min': None, + 'max': None, + }, + }, + 'precision': { + 'price': self.parse_number(self.parse_precision(self.safe_string(tradeDetails, 'pricedecimal'))), + 'amount': None, + }, + 'active': self.safe_string(tradeDetails, 'enable') == '1', + 'created': None, + 'info': res, + })) + return markets + + def parse_market(self, market: dict) -> Market: + baseId = self.safe_string(market, 'base') + base = self.safe_currency_code(baseId) + quoteId = self.safe_string(market, 'quote') + quote = self.safe_currency_code(quoteId) + symbol = base + '/' + quote + return { + 'id': self.safe_string(market, 'symbol'), + 'uppercaseId': None, + 'symbol': symbol, + 'base': base, + 'baseId': baseId, + 'quote': quote, + 'quoteId': quoteId, + 'settle': None, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'limits': { + 'amount': { + 'min': self.safe_number(market, 'minLimitBaseAmount'), + 'max': self.safe_number(market, 'maxLimitBaseAmount'), + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + 'leverage': { + 'min': None, + 'max': None, + }, + }, + 'precision': { + 'price': self.parse_number(self.parse_precision(self.safe_string(market, 'quotePrecision'))), + 'amount': self.parse_number(self.parse_precision(self.safe_string(market, 'basePrecision'))), + }, + 'active': None, + 'created': None, + 'info': market, + } + + def parse_balance(self, response) -> Balances: + result: dict = {'info': response} + codes = list(self.currencies.keys()) + for i in range(0, len(codes)): + code = codes[i] + currency = self.currency(code) + currencyId = currency['id'] + free = currencyId + '_balance' + if free in response: + account = self.account() + used = currencyId + '_lock' + account['free'] = self.safe_string(response, free) + account['used'] = self.safe_string(response, used) + result[code] = account + return self.safe_balance(result) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://blog.btcbox.jp/en/archives/8762#toc13 + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + response = self.privatePostBalance(params) + return self.parse_balance(response) + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://blog.btcbox.jp/en/archives/8762#toc6 + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = {} + numSymbols = len(self.symbols) + if numSymbols > 1: + request['coin'] = market['baseId'] + response = self.publicGetDepth(self.extend(request, params)) + return self.parse_order_book(response, market['symbol']) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + symbol = self.safe_symbol(None, market) + last = self.safe_string(ticker, 'last') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': None, + 'datetime': None, + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': self.safe_string(ticker, 'buy'), + 'bidVolume': None, + 'ask': self.safe_string(ticker, 'sell'), + 'askVolume': None, + 'vwap': None, + 'open': None, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': self.safe_string(ticker, 'vol'), + 'quoteVolume': self.safe_string(ticker, 'volume'), + 'info': ticker, + }, market) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://blog.btcbox.jp/en/archives/8762#toc5 + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = {} + numSymbols = len(self.symbols) + if numSymbols > 1: + request['coin'] = market['baseId'] + response = self.publicGetTicker(self.extend(request, params)) + return self.parse_ticker(response, market) + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + :param str[] [symbols]: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + response = self.publicGetTickers(params) + return self.parse_tickers(response, symbols) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(public) + # + # { + # "date":"0", + # "price":3, + # "amount":0.1, + # "tid":"1", + # "type":"buy" + # } + # + timestamp = self.safe_timestamp(trade, 'date') + market = self.safe_market(None, market) + id = self.safe_string(trade, 'tid') + priceString = self.safe_string(trade, 'price') + amountString = self.safe_string(trade, 'amount') + type = None + side = self.safe_string(trade, 'type') + return self.safe_trade({ + 'info': trade, + 'id': id, + 'order': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'type': type, + 'side': side, + 'takerOrMaker': None, + 'price': priceString, + 'amount': amountString, + 'cost': None, + 'fee': None, + }, market) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://blog.btcbox.jp/en/archives/8762#toc7 + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = {} + numSymbols = len(self.symbols) + if numSymbols > 1: + request['coin'] = market['baseId'] + response = self.publicGetOrders(self.extend(request, params)) + # + # [ + # { + # "date":"0", + # "price":3, + # "amount":0.1, + # "tid":"1", + # "type":"buy" + # }, + # ] + # + return self.parse_trades(response, market, since, limit) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://blog.btcbox.jp/en/archives/8762#toc18 + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'amount': amount, + 'price': price, + 'type': side, + 'coin': market['baseId'], + } + response = self.privatePostTradeAdd(self.extend(request, params)) + # + # { + # "result":true, + # "id":"11" + # } + # + return self.parse_order(response, market) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://blog.btcbox.jp/en/archives/8762#toc17 + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + # a special case for btcbox – default symbol is BTC/JPY + if symbol is None: + symbol = 'BTC/JPY' + market = self.market(symbol) + request: dict = { + 'id': id, + 'coin': market['baseId'], + } + response = self.privatePostTradeCancel(self.extend(request, params)) + # + # {"result":true, "id":"11"} + # + return self.parse_order(response, market) + + def parse_order_status(self, status: Str): + statuses: dict = { + # TODO: complete list + 'part': 'open', # partially or not at all executed + 'all': 'closed', # fully executed + 'cancelled': 'canceled', + 'closed': 'closed', # never encountered, seems to be bug in the doc + 'no': 'closed', # not clarified in the docs... + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # { + # "id":11, + # "datetime":"2014-10-21 10:47:20", + # "type":"sell", + # "price":42000, + # "amount_original":1.2, + # "amount_outstanding":1.2, + # "status":"closed", + # "trades":[] # no clarification of trade value structure of order endpoint + # } + # + id = self.safe_string(order, 'id') + datetimeString = self.safe_string(order, 'datetime') + timestamp = None + if datetimeString is not None: + timestamp = self.parse8601(order['datetime'] + '+09:00') # Tokyo time + amount = self.safe_string(order, 'amount_original') + remaining = self.safe_string(order, 'amount_outstanding') + price = self.safe_string(order, 'price') + # status is set by fetchOrder method only + status = self.parse_order_status(self.safe_string(order, 'status')) + # fetchOrders do not return status, use heuristic + if status is None: + if Precise.string_equals(remaining, '0'): + status = 'closed' + trades = None # todo: self.parse_trades(order['trades']) + market = self.safe_market(None, market) + side = self.safe_string(order, 'type') + return self.safe_order({ + 'id': id, + 'clientOrderId': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'amount': amount, + 'remaining': remaining, + 'filled': None, + 'side': side, + 'type': None, + 'timeInForce': None, + 'postOnly': None, + 'status': status, + 'symbol': market['symbol'], + 'price': price, + 'triggerPrice': None, + 'cost': None, + 'trades': trades, + 'fee': None, + 'info': order, + 'average': None, + }, market) + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://blog.btcbox.jp/en/archives/8762#toc16 + + :param str id: the order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + # a special case for btcbox – default symbol is BTC/JPY + if symbol is None: + symbol = 'BTC/JPY' + market = self.market(symbol) + request = self.extend({ + 'id': id, + 'coin': market['baseId'], + }, params) + response = self.privatePostTradeView(self.extend(request, params)) + # + # { + # "id":11, + # "datetime":"2014-10-21 10:47:20", + # "type":"sell", + # "price":42000, + # "amount_original":1.2, + # "amount_outstanding":1.2, + # "status":"closed", + # "trades":[] + # } + # + return self.parse_order(response, market) + + def fetch_orders_by_type(self, type, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + self.load_markets() + # a special case for btcbox – default symbol is BTC/JPY + market = self.market(symbol) + request: dict = { + 'type': type, # 'open' or 'all' + 'coin': market['baseId'], + } + response = self.privatePostTradeList(self.extend(request, params)) + # + # [ + # { + # "id":"7", + # "datetime":"2014-10-20 13:27:38", + # "type":"buy", + # "price":42750, + # "amount_original":0.235, + # "amount_outstanding":0.235 + # }, + # ] + # + orders = self.parse_orders(response, market, since, limit) + # status(open/closed/canceled) is None + # btcbox does not return status, but we know it's 'open' queried for open orders + if type == 'open': + for i in range(0, len(orders)): + orders[i]['status'] = 'open' + return orders + + def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://blog.btcbox.jp/en/archives/8762#toc15 + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + return self.fetch_orders_by_type('all', symbol, since, limit, params) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://blog.btcbox.jp/en/archives/8762#toc15 + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + return self.fetch_orders_by_type('open', symbol, since, limit, params) + + def nonce(self): + return self.milliseconds() + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = self.urls['api']['rest'] + '/' + self.version + '/' + path + if api == 'public': + if params: + url += '?' + self.urlencode(params) + elif api == 'webApi': + url = self.urls['www'] + '/' + path + else: + self.check_required_credentials() + nonce = str(self.nonce()) + query = self.extend({ + 'key': self.apiKey, + 'nonce': nonce, + }, params) + request = self.urlencode(query) + secret = self.hash(self.encode(self.secret), 'md5') + query['signature'] = self.hmac(self.encode(request), self.encode(secret), hashlib.sha256) + body = self.urlencode(query) + headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + } + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None # resort to defaultErrorHandler + # typical error response: {"result":false,"code":"401"} + if httpCode >= 400: + return None # resort to defaultErrorHandler + result = self.safe_value(response, 'result') + if result is None or result is True: + return None # either public API(no error codes expected) or success + code = self.safe_value(response, 'code') + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions, code, feedback) + raise ExchangeError(feedback) # unknown message + + def request(self, path, api='public', method='GET', params={}, headers=None, body=None, config={}): + response = self.fetch2(path, api, method, params, headers, body, config) + if isinstance(response, str): + # sometimes the exchange returns whitespace prepended to json + response = self.strip(response) + if not self.is_json_encoded_object(response): + raise ExchangeError(self.id + ' ' + response) + response = json.loads(response) + return response diff --git a/ccxt/btcmarkets.py b/ccxt/btcmarkets.py new file mode 100644 index 0000000..4d853e0 --- /dev/null +++ b/ccxt/btcmarkets.py @@ -0,0 +1,1377 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.btcmarkets import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currency, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Ticker, Trade, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class btcmarkets(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(btcmarkets, self).describe(), { + 'id': 'btcmarkets', + 'name': 'BTC Markets', + 'countries': ['AU'], # Australia + 'rateLimit': 1000, # market data cached for 1 second(trades cached for 2 seconds) + 'version': 'v3', + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelOrder': True, + 'cancelOrders': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createDepositAddress': False, + 'createOrder': True, + 'createOrderWithTakeProfitAndStopLoss': False, + 'createOrderWithTakeProfitAndStopLossWs': False, + 'createPostOnlyOrder': False, + 'createReduceOnlyOrder': False, + 'createTriggerOrder': True, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchClosedOrders': 'emulated', + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': False, + 'fetchDepositAddress': False, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchDepositsWithdrawals': True, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTransactions': 'emulated', + 'fetchVolatilityHistory': False, + 'fetchWithdrawals': True, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'repayMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'withdraw': True, + }, + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/8c8d6907-3873-4cc4-ad20-e22fba28247e', + 'api': { + 'public': 'https://api.btcmarkets.net', + 'private': 'https://api.btcmarkets.net', + }, + 'www': 'https://btcmarkets.net', + 'doc': [ + 'https://api.btcmarkets.net/doc/v3', + 'https://github.com/BTCMarkets/API', + ], + }, + 'api': { + 'public': { + 'get': [ + 'markets', + 'markets/{marketId}/ticker', + 'markets/{marketId}/trades', + 'markets/{marketId}/orderbook', + 'markets/{marketId}/candles', + 'markets/tickers', + 'markets/orderbooks', + 'time', + ], + }, + 'private': { + 'get': [ + 'orders', + 'orders/{id}', + 'batchorders/{ids}', + 'trades', + 'trades/{id}', + 'withdrawals', + 'withdrawals/{id}', + 'deposits', + 'deposits/{id}', + 'transfers', + 'transfers/{id}', + 'addresses', + 'withdrawal-fees', + 'assets', + 'accounts/me/trading-fees', + 'accounts/me/withdrawal-limits', + 'accounts/me/balances', + 'accounts/me/transactions', + 'reports/{id}', + ], + 'post': [ + 'orders', + 'batchorders', + 'withdrawals', + 'reports', + ], + 'delete': [ + 'orders', + 'orders/{id}', + 'batchorders/{ids}', + ], + 'put': [ + 'orders/{id}', + ], + }, + }, + 'timeframes': { + '1m': '1m', + '1h': '1h', + '1d': '1d', + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, # todo: check + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'leverage': False, + 'marketBuyRequiresPrice': False, + 'marketBuyByCost': False, + 'selfTradePrevention': True, # todo: check + 'trailing': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 100000, + 'untilDays': 100000, + 'symbolRequired': True, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 100, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 100000, + 'untilDays': 100000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 100000, + 'daysBackCanceled': 1, + 'untilDays': 100000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 1000, + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'exact': { + 'InsufficientFund': InsufficientFunds, + 'InvalidPrice': InvalidOrder, + 'InvalidAmount': InvalidOrder, + 'MissingArgument': BadRequest, + 'OrderAlreadyCancelled': InvalidOrder, + 'OrderNotFound': OrderNotFound, + 'OrderStatusIsFinal': InvalidOrder, + 'InvalidPaginationParameter': BadRequest, + }, + 'broad': { + }, + }, + 'fees': { + 'percentage': True, + 'tierBased': True, + 'maker': self.parse_number('-0.0005'), + 'taker': self.parse_number('0.0020'), + }, + 'options': { + 'fees': { + 'AUD': { + 'maker': self.parse_number('0.0085'), + 'taker': self.parse_number('0.0085'), + }, + }, + }, + }) + + def fetch_transactions_with_method(self, method, code: Str = None, since: Int = None, limit: Int = None, params={}): + self.load_markets() + request: dict = {} + if limit is not None: + request['limit'] = limit + if since is not None: + request['after'] = since + currency = None + if code is not None: + currency = self.currency(code) + response = getattr(self, method)(self.extend(request, params)) + return self.parse_transactions(response, currency, since, limit) + + def fetch_deposits_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch history of deposits and withdrawals + + https://docs.btcmarkets.net/v3/#tag/Fund-Management-APIs/paths/~1v3~1transfers/get + + :param str [code]: unified currency code for the currency of the deposit/withdrawals, default is None + :param int [since]: timestamp in ms of the earliest deposit/withdrawal, default is None + :param int [limit]: max number of deposit/withdrawals to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `transaction structure ` + """ + return self.fetch_transactions_with_method('privateGetTransfers', code, since, limit, params) + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://docs.btcmarkets.net/v3/#tag/Fund-Management-APIs/paths/~1v3~1deposits/get + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + return self.fetch_transactions_with_method('privateGetDeposits', code, since, limit, params) + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://docs.btcmarkets.net/v3/#tag/Fund-Management-APIs/paths/~1v3~1withdrawals/get + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + return self.fetch_transactions_with_method('privateGetWithdrawals', code, since, limit, params) + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'Accepted': 'pending', + 'Pending Authorization': 'pending', + 'Complete': 'ok', + 'Cancelled': 'cancelled', + 'Failed': 'failed', + } + return self.safe_string(statuses, status, status) + + def parse_transaction_type(self, type): + statuses: dict = { + 'Withdraw': 'withdrawal', + 'Deposit': 'deposit', + } + return self.safe_string(statuses, type, type) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # { + # "id": "6500230339", + # "assetName": "XRP", + # "amount": "500", + # "type": "Deposit", + # "creationTime": "2020-07-27T07:52:08.640000Z", + # "status": "Complete", + # "description": "RIPPLE Deposit, XRP 500", + # "fee": "0", + # "lastUpdate": "2020-07-27T07:52:08.665000Z", + # "paymentDetail": { + # "txId": "lsjflsjdfljsd", + # "address": "kjasfkjsdf?dt=873874545" + # } + # } + # + # { + # "id": "500985282", + # "assetName": "BTC", + # "amount": "0.42570126", + # "type": "Withdraw", + # "creationTime": "2017-07-29T12:49:03.931000Z", + # "status": "Complete", + # "description": "BTC withdraw from [nick-btcmarkets@snowmonkey.co.uk] to Address: 1B9DsnSYQ54VMqFHVJYdGoLMCYzFwrQzsj amount: 0.42570126 fee: 0.00000000", + # "fee": "0.0005", + # "lastUpdate": "2017-07-29T12:52:20.676000Z", + # "paymentDetail": { + # "txId": "fkjdsfjsfljsdfl", + # "address": "a;daddjas;djas" + # } + # } + # + # { + # "id": "505102262", + # "assetName": "XRP", + # "amount": "979.836", + # "type": "Deposit", + # "creationTime": "2017-07-31T08:50:01.053000Z", + # "status": "Complete", + # "description": "Ripple Deposit, X 979.8360", + # "fee": "0", + # "lastUpdate": "2017-07-31T08:50:01.290000Z" + # } + # + timestamp = self.parse8601(self.safe_string(transaction, 'creationTime')) + lastUpdate = self.parse8601(self.safe_string(transaction, 'lastUpdate')) + type = self.parse_transaction_type(self.safe_string_lower(transaction, 'type')) + if type == 'withdraw': + type = 'withdrawal' + cryptoPaymentDetail = self.safe_dict(transaction, 'paymentDetail', {}) + txid = self.safe_string(cryptoPaymentDetail, 'txId') + address = self.safe_string(cryptoPaymentDetail, 'address') + tag = None + if address is not None: + addressParts = address.split('?dt=') + numParts = len(addressParts) + if numParts > 1: + address = addressParts[0] + tag = addressParts[1] + addressTo = address + tagTo = tag + addressFrom = None + tagFrom = None + fee = self.safe_string(transaction, 'fee') + status = self.parse_transaction_status(self.safe_string(transaction, 'status')) + currencyId = self.safe_string(transaction, 'assetName') + code = self.safe_currency_code(currencyId) + amount = self.safe_string(transaction, 'amount') + if fee: + amount = Precise.string_sub(amount, fee) + return { + 'id': self.safe_string(transaction, 'id'), + 'txid': txid, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': None, + 'address': address, + 'addressTo': addressTo, + 'addressFrom': addressFrom, + 'tag': tag, + 'tagTo': tagTo, + 'tagFrom': tagFrom, + 'type': type, + 'amount': self.parse_number(amount), + 'currency': code, + 'status': status, + 'updated': lastUpdate, + 'comment': self.safe_string(transaction, 'description'), + 'internal': None, + 'fee': { + 'currency': code, + 'cost': self.parse_number(fee), + 'rate': None, + }, + 'info': transaction, + } + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for btcmarkets + + https://docs.btcmarkets.net/v3/#tag/Market-Data-APIs/paths/~1v3~1markets/get + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = self.publicGetMarkets(params) + # + # [ + # { + # "marketId":"COMP-AUD", + # "baseAssetName":"COMP", + # "quoteAssetName":"AUD", + # "minOrderAmount":"0.00006", + # "maxOrderAmount":"1000000", + # "amountDecimals":"8", + # "priceDecimals":"2", + # "status": "Online" + # } + # ] + # + return self.parse_markets(response) + + def parse_market(self, market: dict) -> Market: + baseId = self.safe_string(market, 'baseAssetName') + quoteId = self.safe_string(market, 'quoteAssetName') + id = self.safe_string(market, 'marketId') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + symbol = base + '/' + quote + fees = self.safe_value(self.safe_dict(self.options, 'fees', {}), quote, self.fees) + pricePrecision = self.parse_number(self.parse_precision(self.safe_string(market, 'priceDecimals'))) + minAmount = self.safe_number(market, 'minOrderAmount') + maxAmount = self.safe_number(market, 'maxOrderAmount') + status = self.safe_string(market, 'status') + minPrice = None + if quote == 'AUD': + minPrice = pricePrecision + return { + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': (status == 'Online'), + 'contract': False, + 'linear': None, + 'inverse': None, + 'taker': fees['taker'], + 'maker': fees['maker'], + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(self.parse_precision(self.safe_string(market, 'amountDecimals'))), + 'price': pricePrecision, + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': minAmount, + 'max': maxAmount, + }, + 'price': { + 'min': minPrice, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + 'info': market, + } + + def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://docs.btcmarkets.net/v3/#tag/Misc-APIs/paths/~1v3~1time/get + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = self.publicGetTime(params) + # + # { + # "timestamp": "2019-09-01T18:34:27.045000Z" + # } + # + return self.parse8601(self.safe_string(response, 'timestamp')) + + def parse_balance(self, response) -> Balances: + result: dict = {'info': response} + for i in range(0, len(response)): + balance = response[i] + currencyId = self.safe_string(balance, 'assetName') + code = self.safe_currency_code(currencyId) + account = self.account() + account['used'] = self.safe_string(balance, 'locked') + account['total'] = self.safe_string(balance, 'balance') + result[code] = account + return self.safe_balance(result) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://docs.btcmarkets.net/v3/#tag/Account-APIs/paths/~1v3~1accounts~1me~1balances/get + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + response = self.privateGetAccountsMeBalances(params) + return self.parse_balance(response) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # [ + # "2020-09-12T18:30:00.000000Z", + # "14409.45", # open + # "14409.45", # high + # "14403.91", # low + # "14403.91", # close + # "0.01571701" # volume + # ] + # + return [ + self.parse8601(self.safe_string(ohlcv, 0)), + self.safe_number(ohlcv, 1), # open + self.safe_number(ohlcv, 2), # high + self.safe_number(ohlcv, 3), # low + self.safe_number(ohlcv, 4), # close + self.safe_number(ohlcv, 5), # volume + ] + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://docs.btcmarkets.net/v3/#tag/Market-Data-APIs/paths/~1v3~1markets~1{marketId}~1candles/get + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'marketId': market['id'], + 'timeWindow': self.safe_string(self.timeframes, timeframe, timeframe), + # 'from': self.iso8601(since), + # 'to': self.iso8601(self.milliseconds()), + # 'before': 1234567890123, + # 'after': 1234567890123, + # 'limit': limit, # default 10, max 200 + } + if since is not None: + request['from'] = self.iso8601(since) + if limit is not None: + request['limit'] = min(limit, 200) # default is 10, max 200 + response = self.publicGetMarketsMarketIdCandles(self.extend(request, params)) + # + # [ + # ["2020-09-12T18:30:00.000000Z","14409.45","14409.45","14403.91","14403.91","0.01571701"], + # ["2020-09-12T18:21:00.000000Z","14409.45","14409.45","14409.45","14409.45","0.0035"], + # ["2020-09-12T18:03:00.000000Z","14361.37","14361.37","14361.37","14361.37","0.00345221"], + # ] + # + return self.parse_ohlcvs(response, market, timeframe, since, limit) + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://docs.btcmarkets.net/v3/#tag/Market-Data-APIs/paths/~1v3~1markets~1{marketId}~1orderbook/get + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'marketId': market['id'], + } + response = self.publicGetMarketsMarketIdOrderbook(self.extend(request, params)) + # + # { + # "marketId":"BTC-AUD", + # "snapshotId":1599936148941000, + # "asks":[ + # ["14459.45","0.00456475"], + # ["14463.56","2"], + # ["14470.91","0.98"], + # ], + # "bids":[ + # ["14421.01","0.52"], + # ["14421","0.75"], + # ["14418","0.3521"], + # ] + # } + # + timestamp = self.safe_integer_product(response, 'snapshotId', 0.001) + orderbook = self.parse_order_book(response, symbol, timestamp) + orderbook['nonce'] = self.safe_integer(response, 'snapshotId') + return orderbook + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # fetchTicker + # + # { + # "marketId":"BAT-AUD", + # "bestBid":"0.3751", + # "bestAsk":"0.377", + # "lastPrice":"0.3769", + # "volume24h":"56192.97613335", + # "volumeQte24h":"21179.13270465", + # "price24h":"0.0119", + # "pricePct24h":"3.26", + # "low24h":"0.3611", + # "high24h":"0.3799", + # "timestamp":"2020-08-09T18:28:23.280000Z" + # } + # + marketId = self.safe_string(ticker, 'marketId') + market = self.safe_market(marketId, market, '-') + symbol = market['symbol'] + timestamp = self.parse8601(self.safe_string(ticker, 'timestamp')) + last = self.safe_string(ticker, 'lastPrice') + baseVolume = self.safe_string(ticker, 'volume24h') + quoteVolume = self.safe_string(ticker, 'volumeQte24h') + change = self.safe_string(ticker, 'price24h') + percentage = self.safe_string(ticker, 'pricePct24h') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high24h'), + 'low': self.safe_string(ticker, 'low'), + 'bid': self.safe_string(ticker, 'bestBid'), + 'bidVolume': None, + 'ask': self.safe_string(ticker, 'bestAsk'), + 'askVolume': None, + 'vwap': None, + 'open': None, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': change, + 'percentage': percentage, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'info': ticker, + }, market) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://docs.btcmarkets.net/v3/#tag/Market-Data-APIs/paths/~1v3~1markets~1{marketId}~1ticker/get + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'marketId': market['id'], + } + response = self.publicGetMarketsMarketIdTicker(self.extend(request, params)) + # + # { + # "marketId":"BAT-AUD", + # "bestBid":"0.3751", + # "bestAsk":"0.377", + # "lastPrice":"0.3769", + # "volume24h":"56192.97613335", + # "volumeQte24h":"21179.13270465", + # "price24h":"0.0119", + # "pricePct24h":"3.26", + # "low24h":"0.3611", + # "high24h":"0.3799", + # "timestamp":"2020-08-09T18:28:23.280000Z" + # } + # + return self.parse_ticker(response, market) + + def fetch_ticker_2(self, symbol: str, params={}): + self.load_markets() + market = self.market(symbol) + request: dict = { + 'id': market['id'], + } + response = self.publicGetMarketsMarketIdTicker(self.extend(request, params)) + return self.parse_ticker(response, market) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # public fetchTrades + # + # { + # "id":"6191646611", + # "price":"539.98", + # "amount":"0.5", + # "timestamp":"2020-08-09T15:21:05.016000Z", + # "side":"Ask" + # } + # + # private fetchMyTrades + # + # { + # "id": "36014819", + # "marketId": "XRP-AUD", + # "timestamp": "2019-06-25T16:01:02.977000Z", + # "price": "0.67", + # "amount": "1.50533262", + # "side": "Ask", + # "fee": "0.00857285", + # "orderId": "3648306", + # "liquidityType": "Taker", + # "clientOrderId": "48" + # } + # + timestamp = self.parse8601(self.safe_string(trade, 'timestamp')) + marketId = self.safe_string(trade, 'marketId') + market = self.safe_market(marketId, market, '-') + feeCurrencyCode = market['quote'] if (market['quote'] == 'AUD') else market['base'] + side = self.safe_string(trade, 'side') + if side == 'Bid': + side = 'buy' + elif side == 'Ask': + side = 'sell' + id = self.safe_string(trade, 'id') + priceString = self.safe_string(trade, 'price') + amountString = self.safe_string(trade, 'amount') + orderId = self.safe_string(trade, 'orderId') + fee = None + feeCostString = self.safe_string(trade, 'fee') + if feeCostString is not None: + fee = { + 'cost': feeCostString, + 'currency': feeCurrencyCode, + } + takerOrMaker = self.safe_string_lower(trade, 'liquidityType') + return self.safe_trade({ + 'info': trade, + 'id': id, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'order': orderId, + 'symbol': market['symbol'], + 'type': None, + 'side': side, + 'price': priceString, + 'amount': amountString, + 'cost': None, + 'takerOrMaker': takerOrMaker, + 'fee': fee, + }, market) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://docs.btcmarkets.net/v3/#tag/Market-Data-APIs/paths/~1v3~1markets~1{marketId}~1trades/get + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + # 'since': 59868345231, + 'marketId': market['id'], + } + response = self.publicGetMarketsMarketIdTrades(self.extend(request, params)) + # + # [ + # {"id":"6191646611","price":"539.98","amount":"0.5","timestamp":"2020-08-09T15:21:05.016000Z","side":"Ask"}, + # {"id":"6191646610","price":"539.99","amount":"0.5","timestamp":"2020-08-09T15:21:05.015000Z","side":"Ask"}, + # {"id":"6191646590","price":"540","amount":"0.00233785","timestamp":"2020-08-09T15:21:04.171000Z","side":"Bid"}, + # ] + # + return self.parse_trades(response, market, since, limit) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://docs.btcmarkets.net/v3/#tag/Order-Placement-APIs/paths/~1v3~1orders/post + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: the price at which a trigger order is triggered at + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'marketId': market['id'], + # 'price': self.price_to_precision(symbol, price), + 'amount': self.amount_to_precision(symbol, amount), + # 'type': 'Limit', # "Limit", "Market", "Stop Limit", "Stop", "Take Profit" + 'side': 'Bid' if (side == 'buy') else 'Ask', + # 'triggerPrice': self.price_to_precision(symbol, triggerPrice), # required for Stop, Stop Limit, Take Profit orders + # 'targetAmount': self.amount_to_precision(symbol, targetAmount), # target amount when a desired target outcome is required for order execution + # 'timeInForce': 'GTC', # GTC, FOK, IOC + # 'postOnly': False, # boolean if self is a post-only order + # 'selfTrade': 'A', # A = allow, P = prevent + # 'clientOrderId': self.uuid(), + } + lowercaseType = type.lower() + orderTypes = self.safe_value(self.options, 'orderTypes', { + 'limit': 'Limit', + 'market': 'Market', + 'stop': 'Stop', + 'stop limit': 'Stop Limit', + 'take profit': 'Take Profit', + }) + request['type'] = self.safe_string(orderTypes, lowercaseType, type) + priceIsRequired = False + triggerPriceIsRequired = False + if lowercaseType == 'limit': + priceIsRequired = True + # elif lowercaseType == 'market': + # ... + # } + elif lowercaseType == 'stop limit': + triggerPriceIsRequired = True + priceIsRequired = True + elif lowercaseType == 'take profit': + triggerPriceIsRequired = True + elif lowercaseType == 'stop': + triggerPriceIsRequired = True + if priceIsRequired: + if price is None: + raise ArgumentsRequired(self.id + ' createOrder() requires a price argument for a ' + type + 'order') + else: + request['price'] = self.price_to_precision(symbol, price) + if triggerPriceIsRequired: + triggerPrice = self.safe_number(params, 'triggerPrice') + params = self.omit(params, 'triggerPrice') + if triggerPrice is None: + raise ArgumentsRequired(self.id + ' createOrder() requires a triggerPrice parameter for a ' + type + 'order') + else: + request['triggerPrice'] = self.price_to_precision(symbol, triggerPrice) + clientOrderId = self.safe_string(params, 'clientOrderId') + if clientOrderId is not None: + request['clientOrderId'] = clientOrderId + params = self.omit(params, 'clientOrderId') + response = self.privatePostOrders(self.extend(request, params)) + # + # { + # "orderId": "7524", + # "marketId": "BTC-AUD", + # "side": "Bid", + # "type": "Limit", + # "creationTime": "2019-08-30T11:08:21.956000Z", + # "price": "100.12", + # "amount": "1.034", + # "openAmount": "1.034", + # "status": "Accepted", + # "clientOrderId": "1234-5678", + # "timeInForce": "IOC", + # "postOnly": False, + # "selfTrade": "P", + # "triggerAmount": "105", + # "targetAmount": "1000" + # } + # + return self.parse_order(response, market) + + def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): + """ + cancel multiple orders + + https://docs.btcmarkets.net/v3/#tag/Batch-Order-APIs/paths/~1v3~1batchorders~1{ids}/delete + + :param str[] ids: order ids + :param str symbol: not used by btcmarkets cancelOrders() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an list of `order structures ` + """ + self.load_markets() + numericIds = [] + for i in range(0, len(ids)): + # numericIds[i] = int(ids[i]) + numericIds.append(int(ids[i])) + request: dict = { + 'ids': numericIds, + } + response = self.privateDeleteBatchordersIds(self.extend(request, params)) + # + # { + # "cancelOrders": [ + # { + # "orderId": "414186", + # "clientOrderId": "6" + # }, + # ... + # ], + # "unprocessedRequests": [ + # { + # "code": "OrderAlreadyCancelled", + # "message": "order is already cancelled.", + # "requestId": "1" + # } + # ] + # } + # + cancelOrders = self.safe_list(response, 'cancelOrders', []) + unprocessedRequests = self.safe_list(response, 'unprocessedRequests', []) + orders = self.array_concat(cancelOrders, unprocessedRequests) + return self.parse_orders(orders) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://docs.btcmarkets.net/v3/#operation/cancelOrder + + :param str id: order id + :param str symbol: not used by btcmarket cancelOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + request: dict = { + 'id': id, + } + response = self.privateDeleteOrdersId(self.extend(request, params)) + # + # { + # "orderId": "7524", + # "clientOrderId": "123-456" + # } + # + return self.parse_order(response) + + def calculate_fee(self, symbol, type, side, amount, price, takerOrMaker='taker', params={}): + """ + calculates the presumptive fee that would be charged for an order + :param str symbol: unified market symbol + :param str type: not used by btcmarkets.calculateFee + :param str side: not used by btcmarkets.calculateFee + :param float amount: how much you want to trade, in units of the base currency on most exchanges, or number of contracts + :param float price: the price for the order to be filled at, in units of the quote currency + :param str takerOrMaker: 'taker' or 'maker' + :param dict params: + :returns dict: contains the rate, the percentage multiplied to the order amount to obtain the fee amount, and cost, the total value of the fee in units of the quote currency, for the order + """ + market = self.markets[symbol] + currency = None + cost = None + if market['quote'] == 'AUD': + currency = market['quote'] + amountString = self.number_to_string(amount) + priceString = self.number_to_string(price) + otherUnitsAmount = Precise.string_mul(amountString, priceString) + cost = self.cost_to_precision(symbol, otherUnitsAmount) + else: + currency = market['base'] + cost = self.amount_to_precision(symbol, amount) + rate = market[takerOrMaker] + rateCost = Precise.string_mul(self.number_to_string(rate), cost) + return { + 'type': takerOrMaker, + 'currency': currency, + 'rate': rate, + 'cost': float(self.fee_to_precision(symbol, rateCost)), + } + + def parse_order_status(self, status: Str): + statuses: dict = { + 'Accepted': 'open', + 'Placed': 'open', + 'Partially Matched': 'open', + 'Fully Matched': 'closed', + 'Cancelled': 'canceled', + 'Partially Cancelled': 'canceled', + 'Failed': 'rejected', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder + # + # { + # "orderId": "7524", + # "marketId": "BTC-AUD", + # "side": "Bid", + # "type": "Limit", + # "creationTime": "2019-08-30T11:08:21.956000Z", + # "price": "100.12", + # "amount": "1.034", + # "openAmount": "1.034", + # "status": "Accepted", + # "clientOrderId": "1234-5678", + # "timeInForce": "IOC", + # "postOnly": False, + # "selfTrade": "P", + # "triggerAmount": "105", + # "targetAmount": "1000" + # } + # + timestamp = self.parse8601(self.safe_string(order, 'creationTime')) + marketId = self.safe_string(order, 'marketId') + market = self.safe_market(marketId, market, '-') + side = self.safe_string(order, 'side') + if side == 'Bid': + side = 'buy' + elif side == 'Ask': + side = 'sell' + type = self.safe_string_lower(order, 'type') + price = self.safe_string(order, 'price') + amount = self.safe_string(order, 'amount') + remaining = self.safe_string(order, 'openAmount') + status = self.parse_order_status(self.safe_string(order, 'status')) + id = self.safe_string(order, 'orderId') + clientOrderId = self.safe_string(order, 'clientOrderId') + timeInForce = self.safe_string(order, 'timeInForce') + postOnly = self.safe_bool(order, 'postOnly') + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'symbol': market['symbol'], + 'type': type, + 'timeInForce': timeInForce, + 'postOnly': postOnly, + 'side': side, + 'price': price, + 'triggerPrice': self.safe_number(order, 'triggerPrice'), + 'cost': None, + 'amount': amount, + 'filled': None, + 'remaining': remaining, + 'average': None, + 'status': status, + 'trades': None, + 'fee': None, + }, market) + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://docs.btcmarkets.net/v3/#operation/getOrderById + + :param str id: the order id + :param str symbol: not used by btcmarkets fetchOrder + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + request: dict = { + 'id': id, + } + response = self.privateGetOrdersId(self.extend(request, params)) + return self.parse_order(response) + + def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://docs.btcmarkets.net/v3/#operation/listOrders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + request: dict = { + 'status': 'all', + } + market = None + if symbol is not None: + market = self.market(symbol) + request['marketId'] = market['id'] + if since is not None: + request['after'] = since + if limit is not None: + request['limit'] = limit + response = self.privateGetOrders(self.extend(request, params)) + return self.parse_orders(response, market, since, limit) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://docs.btcmarkets.net/v3/#operation/listOrders + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + request: dict = {'status': 'open'} + return self.fetch_orders(symbol, since, limit, self.extend(request, params)) + + def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://docs.btcmarkets.net/v3/#operation/listOrders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + orders = self.fetch_orders(symbol, since, limit, params) + return self.filter_by(orders, 'status', 'closed') + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://docs.btcmarkets.net/v3/#operation/getTrades + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['marketId'] = market['id'] + if since is not None: + request['after'] = since + if limit is not None: + request['limit'] = limit + response = self.privateGetTrades(self.extend(request, params)) + # + # [ + # { + # "id": "36014819", + # "marketId": "XRP-AUD", + # "timestamp": "2019-06-25T16:01:02.977000Z", + # "price": "0.67", + # "amount": "1.50533262", + # "side": "Ask", + # "fee": "0.00857285", + # "orderId": "3648306", + # "liquidityType": "Taker", + # "clientOrderId": "48" + # }, + # { + # "id": "3568960", + # "marketId": "GNT-AUD", + # "timestamp": "2019-06-20T08:44:04.488000Z", + # "price": "0.1362", + # "amount": "0.85", + # "side": "Bid", + # "fee": "0.00098404", + # "orderId": "3543015", + # "liquidityType": "Maker" + # } + # ] + # + return self.parse_trades(response, market, since, limit) + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://docs.btcmarkets.net/v3/#tag/Fund-Management-APIs/paths/~1v3~1withdrawals/post + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency_id': currency['id'], + 'amount': self.currency_to_precision(code, amount), + } + if code != 'AUD': + self.check_address(address) + request['toAddress'] = address + if tag is not None: + request['toAddress'] = address + '?dt=' + tag + response = self.privatePostWithdrawals(self.extend(request, params)) + # + # { + # "id": "4126657", + # "assetName": "XRP", + # "amount": "25", + # "type": "Withdraw", + # "creationTime": "2019-09-04T00:04:10.973000Z", + # "status": "Pending Authorization", + # "description": "XRP withdraw from [me@test.com] to Address: abc amount: 25 fee: 0", + # "fee": "0", + # "lastUpdate": "2019-09-04T00:04:11.018000Z", + # "paymentDetail": { + # "address": "abc" + # } + # } + # + return self.parse_transaction(response, currency) + + def nonce(self): + return self.milliseconds() + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + request = '/' + self.version + '/' + self.implode_params(path, params) + query = self.keysort(self.omit(params, self.extract_params(path))) + if api == 'private': + self.check_required_credentials() + nonce = str(self.nonce()) + secret = self.base64_to_binary(self.secret) + auth = method + request + nonce + if (method == 'GET') or (method == 'DELETE'): + if query: + request += '?' + self.urlencode(query) + else: + body = self.json(query) + auth += body + signature = self.hmac(self.encode(auth), secret, hashlib.sha512, 'base64') + headers = { + 'Accept': 'application/json', + 'Accept-Charset': 'UTF-8', + 'Content-Type': 'application/json', + 'BM-AUTH-APIKEY': self.apiKey, + 'BM-AUTH-TIMESTAMP': nonce, + 'BM-AUTH-SIGNATURE': signature, + } + elif api == 'public': + if query: + request += '?' + self.urlencode(query) + url = self.urls['api'][api] + request + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None # fallback to default error handler + # + # {"code":"UnAuthorized","message":"invalid access token"} + # {"code":"MarketNotFound","message":"invalid marketId"} + # + errorCode = self.safe_string(response, 'code') + message = self.safe_string(response, 'message') + if errorCode is not None: + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + raise ExchangeError(feedback) # unknown message + return None diff --git a/ccxt/btcturk.py b/ccxt/btcturk.py new file mode 100644 index 0000000..a3aa80d --- /dev/null +++ b/ccxt/btcturk.py @@ -0,0 +1,1035 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.btcturk import ImplicitAPI +import hashlib +import math +from ccxt.base.types import Any, Balances, Bool, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import BadRequest +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class btcturk(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(btcturk, self).describe(), { + 'id': 'btcturk', + 'name': 'BTCTurk', + 'countries': ['TR'], # Turkey + 'rateLimit': 100, + 'pro': False, + 'has': { + 'CORS': True, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelOrder': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createDepositAddress': False, + 'createOrder': True, + 'createOrderWithTakeProfitAndStopLoss': False, + 'createOrderWithTakeProfitAndStopLossWs': False, + 'createPostOnlyOrder': False, + 'createReduceOnlyOrder': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': False, + 'fetchDepositAddress': False, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTrades': True, + 'fetchVolatilityHistory': False, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'repayMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'ws': False, + }, + 'timeframes': { + '1m': 1, + '15m': 15, + '30m': 30, + '1h': 60, + '4h': 240, + '1d': '1 d', + '1w': '1 w', + '1y': '1 y', + }, + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/10e0a238-9f60-4b06-9dda-edfc7602f1d6', + 'api': { + 'public': 'https://api.btcturk.com/api/v2', + 'private': 'https://api.btcturk.com/api/v1', + 'graph': 'https://graph-api.btcturk.com/v1', + }, + 'www': 'https://www.btcturk.com', + 'doc': 'https://github.com/BTCTrader/broker-api-docs', + }, + 'api': { + 'public': { + 'get': { + 'orderbook': 1, + 'ticker': 0.1, + 'trades': 1, # ?last=COUNT(max 50) + 'ohlc': 1, + 'server/exchangeinfo': 1, + }, + }, + 'private': { + 'get': { + 'users/balances': 1, + 'openOrders': 1, + 'allOrders': 1, + 'users/transactions/trade': 1, + }, + 'post': { + 'users/transactions/crypto': 1, + 'users/transactions/fiat': 1, + 'order': 1, + 'cancelOrder': 1, + }, + 'delete': { + 'order': 1, + }, + }, + 'graph': { + 'get': { + 'ohlcs': 1, + 'klines/history': 1, + }, + }, + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': False, + 'FOK': False, + 'PO': False, + 'GTD': False, + }, + 'hedged': False, + 'leverage': False, + 'marketBuyRequiresPrice': False, + 'marketBuyByCost': False, + 'selfTradePrevention': False, + 'trailing': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 100000, + 'untilDays': 30, + 'symbolRequired': True, + }, + 'fetchOrder': None, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 100000, + 'untilDays': 30, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchClosedOrders': None, + 'fetchOHLCV': { + 'limit': None, + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'fees': { + 'trading': { + 'maker': self.parse_number('0.0005'), + 'taker': self.parse_number('0.0009'), + }, + }, + 'exceptions': { + 'exact': { + 'FAILED_ORDER_WITH_OPEN_ORDERS': InsufficientFunds, + 'FAILED_LIMIT_ORDER': InvalidOrder, + 'FAILED_MARKET_ORDER': InvalidOrder, + }, + }, + 'precisionMode': TICK_SIZE, + }) + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for btcturk + + https://docs.btcturk.com/public-endpoints/exchange-info + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = self.publicGetServerExchangeinfo(params) + # + # { + # "data": { + # "timeZone": "UTC", + # "serverTime": "1618826678404", + # "symbols": [ + # { + # "id": "1", + # "name": "BTCTRY", + # "nameNormalized": "BTC_TRY", + # "status": "TRADING", + # "numerator": "BTC", + # "denominator": "TRY", + # "numeratorScale": "8", + # "denominatorScale": "2", + # "hasFraction": False, + # "filters": [ + # { + # "filterType": "PRICE_FILTER", + # "minPrice": "0.0000000000001", + # "maxPrice": "10000000", + # "tickSize": "10", + # "minExchangeValue": "99.92", + # "minAmount": null, + # "maxAmount": null + # } + # ], + # "orderMethods": [ + # "MARKET", + # "LIMIT", + # "STOP_MARKET", + # "STOP_LIMIT" + # ], + # "displayFormat": "#,###", + # "commissionFromNumerator": False, + # "order": "1000", + # "priceRounding": False + # }, + # ... + # }, + # ], + # } + # + data = self.safe_dict(response, 'data', {}) + markets = self.safe_list(data, 'symbols', []) + return self.parse_markets(markets) + + def parse_market(self, entry) -> Market: + id = self.safe_string(entry, 'name') + baseId = self.safe_string(entry, 'numerator') + quoteId = self.safe_string(entry, 'denominator') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + filters = self.safe_list(entry, 'filters', []) + minPrice = None + maxPrice = None + minAmount = None + maxAmount = None + minCost = None + for j in range(0, len(filters)): + filter = filters[j] + filterType = self.safe_string(filter, 'filterType') + if filterType == 'PRICE_FILTER': + minPrice = self.safe_number(filter, 'minPrice') + maxPrice = self.safe_number(filter, 'maxPrice') + minAmount = self.safe_number(filter, 'minAmount') + maxAmount = self.safe_number(filter, 'maxAmount') + minCost = self.safe_number(filter, 'minExchangeValue') + status = self.safe_string(entry, 'status') + return { + 'id': id, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': (status == 'TRADING'), + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(self.parse_precision(self.safe_string(entry, 'numeratorScale'))), + 'price': self.parse_number(self.parse_precision(self.safe_string(entry, 'denominatorScale'))), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': minAmount, + 'max': maxAmount, + }, + 'price': { + 'min': minPrice, + 'max': maxPrice, + }, + 'cost': { + 'min': minCost, + 'max': None, + }, + }, + 'created': None, + 'info': entry, + } + + def parse_balance(self, response) -> Balances: + data = self.safe_list(response, 'data', []) + result: dict = { + 'info': response, + 'timestamp': None, + 'datetime': None, + } + for i in range(0, len(data)): + entry = data[i] + currencyId = self.safe_string(entry, 'asset') + code = self.safe_currency_code(currencyId) + account = self.account() + account['total'] = self.safe_string(entry, 'balance') + account['free'] = self.safe_string(entry, 'free') + account['used'] = self.safe_string(entry, 'locked') + result[code] = account + return self.safe_balance(result) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://docs.btcturk.com/private-endpoints/account-balance + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + response = self.privateGetUsersBalances(params) + # + # { + # "data": [ + # { + # "asset": "TRY", + # "assetname": "Türk Lirası", + # "balance": "0", + # "locked": "0", + # "free": "0", + # "orderFund": "0", + # "requestFund": "0", + # "precision": 2 + # } + # ] + # } + # + return self.parse_balance(response) + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://docs.btcturk.com/public-endpoints/orderbook + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pairSymbol': market['id'], + } + response = self.publicGetOrderbook(self.extend(request, params)) + # { + # "data": { + # "timestamp": 1618827901241, + # "bids": [ + # [ + # "460263.00", + # "0.04244000" + # ] + # ] + # } + # } + data = self.safe_dict(response, 'data', {}) + timestamp = self.safe_integer(data, 'timestamp') + return self.parse_order_book(data, market['symbol'], timestamp, 'bids', 'asks', 0, 1) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "pair": "BTCTRY", + # "pairNormalized": "BTC_TRY", + # "timestamp": 1618826361234, + # "last": 462485, + # "high": 473976, + # "low": 444201, + # "bid": 461928, + # "ask": 462485, + # "open": 456915, + # "volume": 917.41368645, + # "average": 462868.29574589, + # "daily": 5570, + # "dailyPercent": 1.22, + # "denominatorSymbol": "TRY", + # "numeratorSymbol": "BTC", + # "order": 1000 + # } + # + marketId = self.safe_string(ticker, 'pair') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + timestamp = self.safe_integer(ticker, 'timestamp') + last = self.safe_string(ticker, 'last') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': self.safe_string(ticker, 'bid'), + 'bidVolume': None, + 'ask': self.safe_string(ticker, 'ask'), + 'askVolume': None, + 'vwap': None, + 'open': self.safe_string(ticker, 'open'), + 'close': last, + 'last': last, + 'previousClose': None, + 'change': self.safe_string(ticker, 'daily'), + 'percentage': self.safe_string(ticker, 'dailyPercent'), + 'average': self.safe_string(ticker, 'average'), + 'baseVolume': self.safe_string(ticker, 'volume'), + 'quoteVolume': None, + 'info': ticker, + }, market) + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://docs.btcturk.com/public-endpoints/ticker + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + response = self.publicGetTicker(params) + tickers = self.safe_list(response, 'data') + return self.parse_tickers(tickers, symbols) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://docs.btcturk.com/public-endpoints/ticker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + tickers = self.fetch_tickers([symbol], params) + return self.safe_value(tickers, symbol) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades + # { + # "pair": "BTCUSDT", + # "pairNormalized": "BTC_USDT", + # "numerator": "BTC", + # "denominator": "USDT", + # "date": "1618916879083", + # "tid": "637545136790672520", + # "price": "55774", + # "amount": "0.27917100", + # "side": "buy" + # } + # + # fetchMyTrades + # { + # "price": "56000", + # "numeratorSymbol": "BTC", + # "denominatorSymbol": "USDT", + # "orderType": "buy", + # "orderId": "2606935102", + # "id": "320874372", + # "timestamp": "1618916479593", + # "amount": "0.00020000", + # "fee": "0", + # "tax": "0" + # } + # + timestamp = self.safe_integer_2(trade, 'date', 'timestamp') + id = self.safe_string_2(trade, 'tid', 'id') + order = self.safe_string(trade, 'orderId') + priceString = self.safe_string(trade, 'price') + amountString = Precise.string_abs(self.safe_string(trade, 'amount')) + marketId = self.safe_string(trade, 'pair') + symbol = self.safe_symbol(marketId, market) + side = self.safe_string_2(trade, 'side', 'orderType') + fee = None + feeAmountString = self.safe_string(trade, 'fee') + if feeAmountString is not None: + feeCurrency = self.safe_string(trade, 'denominatorSymbol') + fee = { + 'cost': Precise.string_abs(feeAmountString), + 'currency': self.safe_currency_code(feeCurrency), + } + return self.safe_trade({ + 'info': trade, + 'id': id, + 'order': order, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'type': None, + 'side': side, + 'takerOrMaker': None, + 'price': priceString, + 'amount': amountString, + 'cost': None, + 'fee': fee, + }, market) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://docs.btcturk.com/public-endpoints/trades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + # maxCount = 50 + request: dict = { + 'pairSymbol': market['id'], + } + if limit is not None: + request['last'] = limit + response = self.publicGetTrades(self.extend(request, params)) + # + # { + # "data": [ + # { + # "pair": "BTCTRY", + # "pairNormalized": "BTC_TRY", + # "numerator": "BTC", + # "denominator": "TRY", + # "date": 1618828421497, + # "tid": "637544252214980918", + # "price": "462585.00", + # "amount": "0.01618411", + # "side": "sell" + # } + # ] + # } + # + data = self.safe_list(response, 'data') + return self.parse_trades(data, market, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # { + # "timestamp": 1661990400, + # "high": 368388.0, + # "open": 368388.0, + # "low": 368388.0, + # "close": 368388.0, + # "volume": 0.00035208, + # } + # + return [ + self.safe_timestamp(ohlcv, 'timestamp'), + self.safe_number(ohlcv, 'open'), + self.safe_number(ohlcv, 'high'), + self.safe_number(ohlcv, 'low'), + self.safe_number(ohlcv, 'close'), + self.safe_number(ohlcv, 'volume'), + ] + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1h', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://docs.btcturk.com/public-endpoints/get-kline-data + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest candle to fetch + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'resolution': self.safe_value(self.timeframes, timeframe, timeframe), # allows the user to pass custom timeframes if needed + } + until = self.safe_integer(params, 'until', self.milliseconds()) + request['to'] = self.parse_to_int((until / 1000)) + if since is not None: + request['from'] = self.parse_to_int(since / 1000) + elif limit is None: # since will also be None + limit = 100 # default value + if limit is not None: + limit = min(limit, 11000) # max 11000 candles diapason can be covered + if timeframe == '1y': # difficult with leap years + raise BadRequest(self.id + ' fetchOHLCV() does not accept a limit parameter when timeframe == "1y"') + seconds = self.parse_timeframe(timeframe) + limitSeconds = seconds * (limit - 1) + if since is not None: + to = self.parse_to_int(since / 1000) + limitSeconds + request['to'] = min(request['to'], to) + else: + request['from'] = self.parse_to_int(0 / 1000) - limitSeconds + response = self.graphGetKlinesHistory(self.extend(request, params)) + # + # { + # "s": "ok", + # "t": [ + # 1661990400, + # 1661990520, + # ... + # ], + # "h": [ + # 368388.0, + # 369090.0, + # ... + # ], + # "o": [ + # 368388.0, + # 368467.0, + # ... + # ], + # "l": [ + # 368388.0, + # 368467.0, + # ... + # ], + # "c": [ + # 368388.0, + # 369090.0, + # ... + # ], + # "v": [ + # 0.00035208, + # 0.2972395, + # ... + # ] + # } + # + return self.parse_ohlcvs(response, market, timeframe, since, limit) + + def parse_ohlcvs(self, ohlcvs, market=None, timeframe='1m', since: Int = None, limit: Int = None, tail: Bool = False): + results = [] + timestamp = self.safe_list(ohlcvs, 't', []) + high = self.safe_list(ohlcvs, 'h', []) + open = self.safe_list(ohlcvs, 'o', []) + low = self.safe_list(ohlcvs, 'l', []) + close = self.safe_list(ohlcvs, 'c', []) + volume = self.safe_list(ohlcvs, 'v', []) + for i in range(0, len(timestamp)): + ohlcv: dict = { + 'timestamp': self.safe_integer(timestamp, i), + 'high': self.safe_number(high, i), + 'open': self.safe_number(open, i), + 'low': self.safe_number(low, i), + 'close': self.safe_number(close, i), + 'volume': self.safe_number(volume, i), + } + results.append(self.parse_ohlcv(ohlcv, market)) + sorted = self.sort_by(results, 0) + return self.filter_by_since_limit(sorted, since, limit, 0, tail) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://docs.btcturk.com/private-endpoints/submit-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'orderType': side, + 'orderMethod': type, + 'pairSymbol': market['id'], + 'quantity': self.amount_to_precision(symbol, amount), + } + if type != 'market': + request['price'] = self.price_to_precision(symbol, price) + if 'clientOrderId' in params: + request['newClientOrderId'] = params['clientOrderId'] + elif not ('newClientOrderId' in params): + request['newClientOrderId'] = self.uuid() + response = self.privatePostOrder(self.extend(request, params)) + data = self.safe_dict(response, 'data') + return self.parse_order(data, market) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://docs.btcturk.com/private-endpoints/cancel-order + + :param str id: order id + :param str symbol: not used by btcturk cancelOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + request: dict = { + 'id': id, + } + response = self.privateDeleteOrder(self.extend(request, params)) + # + # { + # "success": True, + # "message": "SUCCESS", + # "code": 0 + # } + # + return self.safe_order({ + 'info': response, + }) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://docs.btcturk.com/private-endpoints/open-orders + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['pairSymbol'] = market['id'] + response = self.privateGetOpenOrders(self.extend(request, params)) + data = self.safe_dict(response, 'data', {}) + bids = self.safe_list(data, 'bids', []) + asks = self.safe_list(data, 'asks', []) + return self.parse_orders(self.array_concat(bids, asks), market, since, limit) + + def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://docs.btcturk.com/private-endpoints/all-orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pairSymbol': market['id'], + } + if limit is not None: + # default 100 max 1000 + request['last'] = limit + if since is not None: + request['startTime'] = int(math.floor(since / 1000)) + response = self.privateGetAllOrders(self.extend(request, params)) + # { + # "data": [ + # { + # "id": "2606012912", + # "price": "55000", + # "amount": "0.0003", + # "quantity": "0.0003", + # "stopPrice": "0", + # "pairSymbol": "BTCUSDT", + # "pairSymbolNormalized": "BTC_USDT", + # "type": "buy", + # "method": "limit", + # "orderClientId": "2ed187bd-59a8-4875-a212-1b793963b85c", + # "time": "1618913189253", + # "updateTime": "1618913189253", + # "status": "Untouched", + # "leftAmount": "0.0003000000000000" + # } + # ] + # } + data = self.safe_list(response, 'data') + return self.parse_orders(data, market, since, limit) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'Untouched': 'open', + 'Partial': 'open', + 'Canceled': 'canceled', + 'Closed': 'closed', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # fetchOrders / fetchOpenOrders + # { + # "id": 2605984008, + # "price": "55000", + # "amount": "0.00050000", + # "quantity": "0.00050000", + # "stopPrice": "0", + # "pairSymbol": "BTCUSDT", + # "pairSymbolNormalized": "BTC_USDT", + # "type": "buy", + # "method": "limit", + # "orderClientId": "f479bdb6-0965-4f03-95b5-daeb7aa5a3a5", + # "time": 0, + # "updateTime": 1618913083543, + # "status": "Untouched", + # "leftAmount": "0.00050000" + # } + # + # createOrder + # { + # "id": "2606935102", + # "quantity": "0.0002", + # "price": "56000", + # "stopPrice": null, + # "newOrderClientId": "98e5c491-7ed9-462b-9666-93553180fb28", + # "type": "buy", + # "method": "limit", + # "pairSymbol": "BTCUSDT", + # "pairSymbolNormalized": "BTC_USDT", + # "datetime": "1618916479523" + # } + # + id = self.safe_string(order, 'id') + price = self.safe_string(order, 'price') + amountString = self.safe_string_2(order, 'amount', 'quantity') + amount = Precise.string_abs(amountString) + remaining = self.safe_string(order, 'leftAmount') + marketId = self.safe_string(order, 'pairSymbol') + symbol = self.safe_symbol(marketId, market) + side = self.safe_string(order, 'type') + type = self.safe_string(order, 'method') + clientOrderId = self.safe_string(order, 'orderClientId') + timestamp = self.safe_integer_2(order, 'updateTime', 'datetime') + rawStatus = self.safe_string(order, 'status') + status = self.parse_order_status(rawStatus) + return self.safe_order({ + 'info': order, + 'id': id, + 'price': price, + 'amount': amount, + 'remaining': remaining, + 'filled': None, + 'cost': None, + 'average': None, + 'status': status, + 'side': side, + 'type': type, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'fee': None, + }, market) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://docs.btcturk.com/private-endpoints/user-transactions + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + response = self.privateGetUsersTransactionsTrade() + # + # { + # "data": [ + # { + # "price": "56000", + # "numeratorSymbol": "BTC", + # "denominatorSymbol": "USDT", + # "orderType": "buy", + # "orderId": "2606935102", + # "id": "320874372", + # "timestamp": "1618916479593", + # "amount": "0.00020000", + # "fee": "0", + # "tax": "0" + # } + # ], + # "success": True, + # "message": "SUCCESS", + # "code": "0" + # } + # + data = self.safe_list(response, 'data') + return self.parse_trades(data, market, since, limit) + + def nonce(self): + return self.milliseconds() + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + if self.id == 'btctrader': + raise ExchangeError(self.id + ' is an abstract base API for BTCExchange, BTCTurk') + url = self.urls['api'][api] + '/' + path + if (method == 'GET') or (method == 'DELETE'): + if params: + url += '?' + self.urlencode(params) + else: + body = self.json(params) + if api == 'private': + self.check_required_credentials() + nonce = str(self.nonce()) + secret = self.base64_to_binary(self.secret) + auth = self.apiKey + nonce + headers = { + 'X-PCK': self.apiKey, + 'X-Stamp': nonce, + 'X-Signature': self.hmac(self.encode(auth), secret, hashlib.sha256, 'base64'), + 'Content-Type': 'application/json', + } + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + errorCode = self.safe_string(response, 'code', '0') + message = self.safe_string(response, 'message') + output = body if (message is None) else message + self.throw_exactly_matched_exception(self.exceptions['exact'], message, self.id + ' ' + output) + if (errorCode != '0') and (errorCode != 'SUCCESS'): + raise ExchangeError(self.id + ' ' + output) + return None diff --git a/ccxt/bybit.py b/ccxt/bybit.py new file mode 100644 index 0000000..f3eece8 --- /dev/null +++ b/ccxt/bybit.py @@ -0,0 +1,9020 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.bybit import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, BorrowInterest, Conversion, CrossBorrowRate, Currencies, Currency, DepositAddress, FundingHistory, Greeks, Int, LedgerEntry, Leverage, LeverageTier, LeverageTiers, Liquidation, LongShortRatio, Market, Num, Option, OptionChain, Order, OrderBook, OrderRequest, CancellationRequest, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, FundingRates, Trade, TradingFeeInterface, TradingFees, Transaction, MarketInterface, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import AccountSuspended +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import NoChange +from ccxt.base.errors import MarginModeAlreadySet +from ccxt.base.errors import ManualInteractionNeeded +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import NotSupported +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import InvalidNonce +from ccxt.base.errors import RequestTimeout +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class bybit(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(bybit, self).describe(), { + 'id': 'bybit', + 'name': 'Bybit', + 'countries': ['VG'], # British Virgin Islands + 'version': 'v5', + 'userAgent': None, + 'rateLimit': 20, + 'hostname': 'bybit.com', # bybit.com, bytick.com, bybit.nl, bybit.com.hk + 'pro': True, + 'certified': True, + 'has': { + 'CORS': True, + 'spot': True, + 'margin': True, + 'swap': True, + 'future': True, + 'option': True, + 'borrowCrossMargin': True, + 'cancelAllOrders': True, + 'cancelAllOrdersAfter': True, + 'cancelOrder': True, + 'cancelOrders': True, + 'cancelOrdersForSymbols': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createConvertTrade': True, + 'createMarketBuyOrderWithCost': True, + 'createMarketSellOrderWithCost': True, + 'createOrder': True, + 'createOrders': True, + 'createOrderWithTakeProfitAndStopLoss': True, + 'createPostOnlyOrder': True, + 'createReduceOnlyOrder': True, + 'createStopLimitOrder': True, + 'createStopLossOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'createTakeProfitOrder': True, + 'createTrailingAmountOrder': True, + 'createTriggerOrder': True, + 'editOrder': True, + 'editOrders': True, + 'fetchAllGreeks': True, + 'fetchBalance': True, + 'fetchBidsAsks': 'emulated', + 'fetchBorrowInterest': False, # temporarily disabled, doesn't work + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchCanceledAndClosedOrders': True, + 'fetchCanceledOrders': True, + 'fetchClosedOrder': True, + 'fetchClosedOrders': True, + 'fetchConvertCurrencies': True, + 'fetchConvertQuote': True, + 'fetchConvertTrade': True, + 'fetchConvertTradeHistory': True, + 'fetchCrossBorrowRate': True, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDeposit': False, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': True, + 'fetchDeposits': True, + 'fetchDepositWithdrawFee': 'emulated', + 'fetchDepositWithdrawFees': True, + 'fetchFundingHistory': True, + 'fetchFundingRate': 'emulated', # emulated in exchange + 'fetchFundingRateHistory': True, + 'fetchFundingRates': True, + 'fetchGreeks': True, + 'fetchIndexOHLCV': True, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchLedger': True, + 'fetchLeverage': True, + 'fetchLeverageTiers': True, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': True, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarketLeverageTiers': True, + 'fetchMarkets': True, + 'fetchMarkOHLCV': True, + 'fetchMyLiquidations': True, + 'fetchMySettlementHistory': True, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': True, + 'fetchOpenInterestHistory': True, + 'fetchOpenOrder': True, + 'fetchOpenOrders': True, + 'fetchOption': True, + 'fetchOptionChain': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': False, + 'fetchOrderTrades': True, + 'fetchPosition': True, + 'fetchPositionHistory': 'emulated', + 'fetchPositions': True, + 'fetchPositionsHistory': True, + 'fetchPremiumIndexOHLCV': True, + 'fetchSettlementHistory': True, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': True, + 'fetchTradingFees': True, + 'fetchTransactions': False, + 'fetchTransfers': True, + 'fetchUnderlyingAssets': False, + 'fetchVolatilityHistory': True, + 'fetchWithdrawals': True, + 'repayCrossMargin': True, + 'sandbox': True, + 'setLeverage': True, + 'setMarginMode': True, + 'setPositionMode': True, + 'transfer': True, + 'withdraw': True, + }, + 'timeframes': { + '1m': '1', + '3m': '3', + '5m': '5', + '15m': '15', + '30m': '30', + '1h': '60', + '2h': '120', + '4h': '240', + '6h': '360', + '12h': '720', + '1d': 'D', + '1w': 'W', + '1M': 'M', + }, + 'urls': { + 'test': { + 'spot': 'https://api-testnet.{hostname}', + 'futures': 'https://api-testnet.{hostname}', + 'v2': 'https://api-testnet.{hostname}', + 'public': 'https://api-testnet.{hostname}', + 'private': 'https://api-testnet.{hostname}', + }, + 'logo': 'https://github.com/user-attachments/assets/97a5d0b3-de10-423d-90e1-6620960025ed', + 'api': { + 'spot': 'https://api.{hostname}', + 'futures': 'https://api.{hostname}', + 'v2': 'https://api.{hostname}', + 'public': 'https://api.{hostname}', + 'private': 'https://api.{hostname}', + }, + 'demotrading': { + 'spot': 'https://api-demo.{hostname}', + 'futures': 'https://api-demo.{hostname}', + 'v2': 'https://api-demo.{hostname}', + 'public': 'https://api-demo.{hostname}', + 'private': 'https://api-demo.{hostname}', + }, + 'www': 'https://www.bybit.com', + 'doc': [ + 'https://bybit-exchange.github.io/docs/inverse/', + 'https://bybit-exchange.github.io/docs/linear/', + 'https://github.com/bybit-exchange', + ], + 'fees': 'https://help.bybit.com/hc/en-us/articles/360039261154', + 'referral': 'https://www.bybit.com/invite?ref=XDK12WP', + }, + 'api': { + 'public': { + 'get': { + # spot + 'spot/v3/public/symbols': 1, + 'spot/v3/public/quote/depth': 1, + 'spot/v3/public/quote/depth/merged': 1, + 'spot/v3/public/quote/trades': 1, + 'spot/v3/public/quote/kline': 1, + 'spot/v3/public/quote/ticker/24hr': 1, + 'spot/v3/public/quote/ticker/price': 1, + 'spot/v3/public/quote/ticker/bookTicker': 1, + 'spot/v3/public/server-time': 1, + 'spot/v3/public/infos': 1, + 'spot/v3/public/margin-product-infos': 1, + 'spot/v3/public/margin-ensure-tokens': 1, + # data + 'v3/public/time': 1, + 'contract/v3/public/copytrading/symbol/list': 1, + # derivative + 'derivatives/v3/public/order-book/L2': 1, + 'derivatives/v3/public/kline': 1, + 'derivatives/v3/public/tickers': 1, + 'derivatives/v3/public/instruments-info': 1, + 'derivatives/v3/public/mark-price-kline': 1, + 'derivatives/v3/public/index-price-kline': 1, + 'derivatives/v3/public/funding/history-funding-rate': 1, + 'derivatives/v3/public/risk-limit/list': 1, + 'derivatives/v3/public/delivery-price': 1, + 'derivatives/v3/public/recent-trade': 1, + 'derivatives/v3/public/open-interest': 1, + 'derivatives/v3/public/insurance': 1, + # v5 + 'v5/announcements/index': 5, # 10/s = 1000 / (20 * 5) + # market + 'v5/market/time': 5, + 'v5/market/kline': 5, + 'v5/market/mark-price-kline': 5, + 'v5/market/index-price-kline': 5, + 'v5/market/premium-index-price-kline': 5, + 'v5/market/instruments-info': 5, + 'v5/market/orderbook': 5, + 'v5/market/tickers': 5, + 'v5/market/funding/history': 5, + 'v5/market/recent-trade': 5, + 'v5/market/open-interest': 5, + 'v5/market/historical-volatility': 5, + 'v5/market/insurance': 5, + 'v5/market/risk-limit': 5, + 'v5/market/delivery-price': 5, + 'v5/market/account-ratio': 5, + # spot leverage token + 'v5/spot-lever-token/info': 5, + 'v5/spot-lever-token/reference': 5, + # spot margin trade + 'v5/spot-margin-trade/data': 5, + 'v5/spot-margin-trade/collateral': 5, + 'v5/spot-cross-margin-trade/data': 5, + 'v5/spot-cross-margin-trade/pledge-token': 5, + 'v5/spot-cross-margin-trade/borrow-token': 5, + # crypto loan + 'v5/crypto-loan/collateral-data': 5, + 'v5/crypto-loan/loanable-data': 5, + # institutional lending + 'v5/ins-loan/product-infos': 5, + 'v5/ins-loan/ensure-tokens-convert': 5, + # earn + 'v5/earn/product': 5, + }, + }, + 'private': { + 'get': { + 'v5/market/instruments-info': 5, + # Legacy inverse swap + 'v2/private/wallet/fund/records': 25, # 120 per minute = 2 per second => cost = 50 / 2 = 25 + # spot + 'spot/v3/private/order': 2.5, + 'spot/v3/private/open-orders': 2.5, + 'spot/v3/private/history-orders': 2.5, + 'spot/v3/private/my-trades': 2.5, + 'spot/v3/private/account': 2.5, + 'spot/v3/private/reference': 2.5, + 'spot/v3/private/record': 2.5, + 'spot/v3/private/cross-margin-orders': 10, + 'spot/v3/private/cross-margin-account': 10, + 'spot/v3/private/cross-margin-loan-info': 10, + 'spot/v3/private/cross-margin-repay-history': 10, + 'spot/v3/private/margin-loan-infos': 10, + 'spot/v3/private/margin-repaid-infos': 10, + 'spot/v3/private/margin-ltv': 10, + # account + 'asset/v3/private/transfer/inter-transfer/list/query': 50, # 60 per minute = 1 per second => cost = 50 / 1 = 50 + 'asset/v3/private/transfer/sub-member/list/query': 50, + 'asset/v3/private/transfer/sub-member-transfer/list/query': 50, + 'asset/v3/private/transfer/universal-transfer/list/query': 25, + 'asset/v3/private/coin-info/query': 25, # 2/s + 'asset/v3/private/deposit/address/query': 10, + 'contract/v3/private/copytrading/order/list': 30, # 100 req/min = 1000 / (20 * 30) = 1.66666666667/s + 'contract/v3/private/copytrading/position/list': 40, # 75 req/min = 1000 / (20 * 40) = 1.25/s + 'contract/v3/private/copytrading/wallet/balance': 25, # 120 req/min = 1000 / (20 * 25) = 2/s + 'contract/v3/private/position/limit-info': 25, # 120 per minute = 2 per second => cost = 50 / 2 = 25 + 'contract/v3/private/order/unfilled-orders': 1, + 'contract/v3/private/order/list': 1, + 'contract/v3/private/position/list': 1, + 'contract/v3/private/execution/list': 1, + 'contract/v3/private/position/closed-pnl': 1, + 'contract/v3/private/account/wallet/balance': 1, + 'contract/v3/private/account/fee-rate': 1, + 'contract/v3/private/account/wallet/fund-records': 1, + # derivative + 'unified/v3/private/order/unfilled-orders': 1, + 'unified/v3/private/order/list': 1, + 'unified/v3/private/position/list': 1, + 'unified/v3/private/execution/list': 1, + 'unified/v3/private/delivery-record': 1, + 'unified/v3/private/settlement-record': 1, + 'unified/v3/private/account/wallet/balance': 1, + 'unified/v3/private/account/transaction-log': 1, + 'unified/v3/private/account/borrow-history': 1, + 'unified/v3/private/account/borrow-rate': 1, + 'unified/v3/private/account/info': 1, + 'user/v3/private/frozen-sub-member': 10, # 5/s + 'user/v3/private/query-sub-members': 5, # 10/s + 'user/v3/private/query-api': 5, # 10/s + 'user/v3/private/get-member-type': 1, + 'asset/v3/private/transfer/transfer-coin/list/query': 50, + 'asset/v3/private/transfer/account-coin/balance/query': 50, + 'asset/v3/private/transfer/account-coins/balance/query': 25, + 'asset/v3/private/transfer/asset-info/query': 50, + 'asset/v3/public/deposit/allowed-deposit-list/query': 0.17, # 300/s + 'asset/v3/private/deposit/record/query': 10, + 'asset/v3/private/withdraw/record/query': 10, + # v5 + # trade + 'v5/order/realtime': 5, # 10/s => cost = 50 / 10 = 5 + 'v5/order/history': 5, # 10/s => cost = 50 / 10 = 5 + 'v5/order/spot-borrow-check': 1, # 50/s = 1000 / (20 * 1) + # position + 'v5/position/list': 5, # 10/s => cost = 50 / 10 = 5 + 'v5/execution/list': 5, # 10/s => cost = 50 / 10 = 5 + 'v5/position/closed-pnl': 5, # 10/s => cost = 50 / 10 = 5 + 'v5/position/move-history': 5, # 10/s => cost = 50 / 10 = 5 + # pre-upgrade + 'v5/pre-upgrade/order/history': 5, + 'v5/pre-upgrade/execution/list': 5, + 'v5/pre-upgrade/position/closed-pnl': 5, + 'v5/pre-upgrade/account/transaction-log': 5, + 'v5/pre-upgrade/asset/delivery-record': 5, + 'v5/pre-upgrade/asset/settlement-record': 5, + # account + 'v5/account/wallet-balance': 1, + 'v5/account/borrow-history': 1, + 'v5/account/instruments-info': 1, + 'v5/account/collateral-info': 1, + 'v5/asset/coin-greeks': 1, + 'v5/account/fee-rate': 10, # 5/s = 1000 / (20 * 10) + 'v5/account/info': 5, + 'v5/account/transaction-log': 1, + 'v5/account/contract-transaction-log': 1, + 'v5/account/smp-group': 1, + 'v5/account/mmp-state': 5, + 'v5/account/withdrawal': 5, + # asset + 'v5/asset/exchange/query-coin-list': 0.5, # 100/s => cost = 50 / 100 = 0.5 + 'v5/asset/exchange/convert-result-query': 0.5, # 100/s => cost = 50 / 100 = 0.5 + 'v5/asset/exchange/query-convert-history': 0.5, # 100/s => cost = 50 / 100 = 0.5 + 'v5/asset/exchange/order-record': 5, # 10/s => cost = 50 / 10 = 5 + 'v5/asset/delivery-record': 5, + 'v5/asset/settlement-record': 5, + 'v5/asset/transfer/query-asset-info': 50, # 1/s => cost = 50 / 1 = 50 + 'v5/asset/transfer/query-account-coins-balance': 25, # 2/s => cost = 50 / 2 = 25 + 'v5/asset/transfer/query-account-coin-balance': 50, # 1/s => cost = 50 / 1 = 50 + 'v5/asset/transfer/query-transfer-coin-list': 50, # 1/s => cost = 50 / 1 = 50 + 'v5/asset/transfer/query-inter-transfer-list': 50, # 1/s => cost = 50 / 1 = 50 + 'v5/asset/transfer/query-sub-member-list': 50, # 1/s => cost = 50 / 1 = 50 + 'v5/asset/transfer/query-universal-transfer-list': 25, # 2/s => cost = 50 / 2 = 25 + 'v5/asset/deposit/query-allowed-list': 5, + 'v5/asset/deposit/query-record': 10, # 5/s => cost = 50 / 5 = 10 + 'v5/asset/deposit/query-sub-member-record': 10, # 5/s => cost = 50 / 5 = 10 + 'v5/asset/deposit/query-internal-record': 5, + 'v5/asset/deposit/query-address': 10, # 5/s => cost = 50 / 5 = 10 + 'v5/asset/deposit/query-sub-member-address': 10, # 5/s => cost = 50 / 5 = 10 + 'v5/asset/coin/query-info': 28, # should be 25 but exceeds ratelimit unless the weight is 28 or higher + 'v5/asset/withdraw/query-address': 10, + 'v5/asset/withdraw/query-record': 10, # 5/s => cost = 50 / 5 = 10 + 'v5/asset/withdraw/withdrawable-amount': 5, + 'v5/asset/withdraw/vasp/list': 5, + # user + 'v5/user/query-sub-members': 5, # 10/s => cost = 50 / 10 = 5 + 'v5/user/query-api': 5, # 10/s => cost = 50 / 10 = 5 + 'v5/user/sub-apikeys': 5, + 'v5/user/get-member-type': 5, + 'v5/user/aff-customer-info': 5, + 'v5/user/del-submember': 5, + 'v5/user/submembers': 5, + # affilate + 'v5/affiliate/aff-user-list': 5, + # spot leverage token + 'v5/spot-lever-token/order-record': 1, # 50/s => cost = 50 / 50 = 1 + # spot margin trade + 'v5/spot-margin-trade/interest-rate-history': 5, + 'v5/spot-margin-trade/state': 5, + 'v5/spot-margin-trade/max-borrowable': 5, + 'v5/spot-margin-trade/position-tiers': 5, + 'v5/spot-margin-trade/coinstate': 5, + 'v5/spot-margin-trade/repayment-available-amount': 5, + 'v5/spot-cross-margin-trade/loan-info': 1, # 50/s => cost = 50 / 50 = 1 + 'v5/spot-cross-margin-trade/account': 1, # 50/s => cost = 50 / 50 = 1 + 'v5/spot-cross-margin-trade/orders': 1, # 50/s => cost = 50 / 50 = 1 + 'v5/spot-cross-margin-trade/repay-history': 1, # 50/s => cost = 50 / 50 = 1 + # crypto loan + 'v5/crypto-loan/borrowable-collateralisable-number': 5, + 'v5/crypto-loan/ongoing-orders': 5, + 'v5/crypto-loan/repayment-history': 5, + 'v5/crypto-loan/borrow-history': 5, + 'v5/crypto-loan/max-collateral-amount': 5, + 'v5/crypto-loan/adjustment-history': 5, + # institutional lending + 'v5/ins-loan/product-infos': 5, + 'v5/ins-loan/ensure-tokens-convert': 5, + 'v5/ins-loan/loan-order': 5, + 'v5/ins-loan/repaid-history': 5, + 'v5/ins-loan/ltv-convert': 5, + # c2c lending + 'v5/lending/info': 5, + 'v5/lending/history-order': 5, + 'v5/lending/account': 5, + # broker + 'v5/broker/earning-record': 5, # deprecated + 'v5/broker/earnings-info': 5, + 'v5/broker/account-info': 5, + 'v5/broker/asset/query-sub-member-deposit-record': 10, + # earn + 'v5/earn/product': 5, + 'v5/earn/order': 5, + 'v5/earn/position': 5, + 'v5/earn/yield': 5, + 'v5/earn/hourly-yield': 5, + }, + 'post': { + # spot + 'spot/v3/private/order': 2.5, + 'spot/v3/private/cancel-order': 2.5, + 'spot/v3/private/cancel-orders': 2.5, + 'spot/v3/private/cancel-orders-by-ids': 2.5, + 'spot/v3/private/purchase': 2.5, + 'spot/v3/private/redeem': 2.5, + 'spot/v3/private/cross-margin-loan': 10, + 'spot/v3/private/cross-margin-repay': 10, + # account + 'asset/v3/private/transfer/inter-transfer': 150, # 20 per minute = 0.333 per second => cost = 50 / 0.3333 = 150 + 'asset/v3/private/withdraw/create': 300, + 'asset/v3/private/withdraw/cancel': 50, + 'asset/v3/private/transfer/sub-member-transfer': 150, + 'asset/v3/private/transfer/transfer-sub-member-save': 150, + 'asset/v3/private/transfer/universal-transfer': 10, # 5/s + 'user/v3/private/create-sub-member': 10, # 5/s + 'user/v3/private/create-sub-api': 10, # 5/s + 'user/v3/private/update-api': 10, # 5/s + 'user/v3/private/delete-api': 10, # 5/s + 'user/v3/private/update-sub-api': 10, # 5/s + 'user/v3/private/delete-sub-api': 10, # 5/s + # contract + 'contract/v3/private/copytrading/order/create': 30, # 100 req/min = 1000 / (20 * 30) = 1.66666666667/s + 'contract/v3/private/copytrading/order/cancel': 30, + 'contract/v3/private/copytrading/order/close': 30, + 'contract/v3/private/copytrading/position/close': 40, # 75 req/min = 1000 / (20 * 40) = 1.25/s + 'contract/v3/private/copytrading/position/set-leverage': 40, + 'contract/v3/private/copytrading/wallet/transfer': 25, # 120 req/min = 1000 / (20 * 25) = 2/s + 'contract/v3/private/copytrading/order/trading-stop': 2.5, + 'contract/v3/private/order/create': 1, + 'contract/v3/private/order/cancel': 1, + 'contract/v3/private/order/cancel-all': 1, + 'contract/v3/private/order/replace': 1, + 'contract/v3/private/position/set-auto-add-margin': 1, + 'contract/v3/private/position/switch-isolated': 1, + 'contract/v3/private/position/switch-mode': 1, + 'contract/v3/private/position/switch-tpsl-mode': 1, + 'contract/v3/private/position/set-leverage': 1, + 'contract/v3/private/position/trading-stop': 1, + 'contract/v3/private/position/set-risk-limit': 1, + 'contract/v3/private/account/setMarginMode': 1, + # derivative + 'unified/v3/private/order/create': 30, # 100 req/min(shared) = 1000 / (20 * 30) = 1.66666666667/s + 'unified/v3/private/order/replace': 30, + 'unified/v3/private/order/cancel': 30, + 'unified/v3/private/order/create-batch': 30, + 'unified/v3/private/order/replace-batch': 30, + 'unified/v3/private/order/cancel-batch': 30, + 'unified/v3/private/order/cancel-all': 30, + 'unified/v3/private/position/set-leverage': 2.5, + 'unified/v3/private/position/tpsl/switch-mode': 2.5, + 'unified/v3/private/position/set-risk-limit': 2.5, + 'unified/v3/private/position/trading-stop': 2.5, + 'unified/v3/private/account/upgrade-unified-account': 2.5, + 'unified/v3/private/account/setMarginMode': 2.5, + # tax + 'fht/compliance/tax/v3/private/registertime': 50, + 'fht/compliance/tax/v3/private/create': 50, + 'fht/compliance/tax/v3/private/status': 50, + 'fht/compliance/tax/v3/private/url': 50, + # v5 + # trade + 'v5/order/create': 2.5, # 20/s = 1000 / (20 * 2.5) + 'v5/order/amend': 5, # 10/s => cost = 50 / 10 = 5 + 'v5/order/cancel': 2.5, + 'v5/order/cancel-all': 50, # 1/s = 1000 / (20 * 50) + 'v5/order/create-batch': 5, # 10/s => cost = 50 / 10 = 5 + 'v5/order/amend-batch': 5, # 10/s => cost = 50 / 10 = 5 + 'v5/order/cancel-batch': 5, # 10/s => cost = 50 / 10 = 5 + 'v5/order/disconnected-cancel-all': 5, + # position + 'v5/position/set-leverage': 5, # 10/s => cost = 50 / 10 = 5 + 'v5/position/switch-isolated': 5, + 'v5/position/set-tpsl-mode': 5, # 10/s => cost = 50 / 10 = 5 + 'v5/position/switch-mode': 5, + 'v5/position/set-risk-limit': 5, # 10/s => cost = 50 / 10 = 5 + 'v5/position/trading-stop': 5, # 10/s => cost = 50 / 10 = 5 + 'v5/position/set-auto-add-margin': 5, + 'v5/position/add-margin': 5, + 'v5/position/move-positions': 5, + 'v5/position/confirm-pending-mmr': 5, + # account + 'v5/account/upgrade-to-uta': 5, + 'v5/account/quick-repayment': 5, + 'v5/account/set-margin-mode': 5, + 'v5/account/set-hedging-mode': 5, + 'v5/account/mmp-modify': 5, + 'v5/account/mmp-reset': 5, + 'v5/account/borrow': 5, + 'v5/account/repay': 5, + 'v5/account/no-convert-repay': 5, + # asset + 'v5/asset/exchange/quote-apply': 1, # 50/s + 'v5/asset/exchange/convert-execute': 1, # 50/s + 'v5/asset/transfer/inter-transfer': 50, # 1/s => cost = 50 / 1 = 50 + 'v5/asset/transfer/save-transfer-sub-member': 150, # 1/3/s => cost = 50 / 1/3 = 150 + 'v5/asset/transfer/universal-transfer': 10, # 5/s => cost = 50 / 5 = 10 + 'v5/asset/deposit/deposit-to-account': 5, + 'v5/asset/withdraw/create': 50, # 1/s => cost = 50 / 1 = 50 + 'v5/asset/withdraw/cancel': 50, # 1/s => cost = 50 / 1 = 50 + # user + 'v5/user/create-sub-member': 10, # 5/s => cost = 50 / 5 = 10 + 'v5/user/create-sub-api': 10, # 5/s => cost = 50 / 5 = 10 + 'v5/user/frozen-sub-member': 10, # 5/s => cost = 50 / 5 = 10 + 'v5/user/update-api': 10, # 5/s => cost = 50 / 5 = 10 + 'v5/user/update-sub-api': 10, # 5/s => cost = 50 / 5 = 10 + 'v5/user/delete-api': 10, # 5/s => cost = 50 / 5 = 10 + 'v5/user/delete-sub-api': 10, # 5/s => cost = 50 / 5 = 10 + # spot leverage token + 'v5/spot-lever-token/purchase': 2.5, # 20/s => cost = 50 / 20 = 2.5 + 'v5/spot-lever-token/redeem': 2.5, # 20/s => cost = 50 / 20 = 2.5 + # spot margin trade + 'v5/spot-margin-trade/switch-mode': 5, + 'v5/spot-margin-trade/set-leverage': 5, + 'v5/spot-cross-margin-trade/loan': 2.5, # 20/s => cost = 50 / 20 = 2.5 + 'v5/spot-cross-margin-trade/repay': 2.5, # 20/s => cost = 50 / 20 = 2.5 + 'v5/spot-cross-margin-trade/switch': 2.5, # 20/s => cost = 50 / 20 = 2.5 + # crypto loan + 'v5/crypto-loan/borrow': 5, + 'v5/crypto-loan/repay': 5, + 'v5/crypto-loan/adjust-ltv': 5, + # institutional lending + 'v5/ins-loan/association-uid': 5, + # c2c lending + 'v5/lending/purchase': 5, + 'v5/lending/redeem': 5, + 'v5/lending/redeem-cancel': 5, + 'v5/account/set-collateral-switch': 5, + 'v5/account/set-collateral-switch-batch': 5, + # demo trading + 'v5/account/demo-apply-money': 5, + # broker + 'v5/broker/award/info': 5, + 'v5/broker/award/distribute-award': 5, + 'v5/broker/award/distribution-record': 5, + # earn + 'v5/earn/place-order': 5, + }, + }, + }, + 'httpExceptions': { + '403': RateLimitExceeded, # Forbidden -- You request too many times + }, + 'exceptions': { + # Uncodumented explanation of error strings: + # - oc_diff: order cost needed to place self order + # - new_oc: total order cost of open orders including the order you are trying to open + # - ob: order balance - the total cost of current open orders + # - ab: available balance + 'exact': { + '-10009': BadRequest, # {"ret_code":-10009,"ret_msg":"Invalid period!","result":null,"token":null} + '-1004': BadRequest, # {"ret_code":-1004,"ret_msg":"Missing required parameter \u0027symbol\u0027","ext_code":null,"ext_info":null,"result":null} + '-1021': BadRequest, # {"ret_code":-1021,"ret_msg":"Timestamp for self request is outside of the recvWindow.","ext_code":null,"ext_info":null,"result":null} + '-1103': BadRequest, # An unknown parameter was sent. + '-1140': InvalidOrder, # {"ret_code":-1140,"ret_msg":"Transaction amount lower than the minimum.","result":{},"ext_code":"","ext_info":null,"time_now":"1659204910.248576"} + '-1197': InvalidOrder, # {"ret_code":-1197,"ret_msg":"Your order quantity to buy is too large. The filled price may deviate significantly from the market price. Please try again","result":{},"ext_code":"","ext_info":null,"time_now":"1659204531.979680"} + '-2013': InvalidOrder, # {"ret_code":-2013,"ret_msg":"Order does not exist.","ext_code":null,"ext_info":null,"result":null} + '-2015': AuthenticationError, # Invalid API-key, IP, or permissions for action. + '-6017': BadRequest, # Repayment amount has exceeded the total liability + '-6025': BadRequest, # Amount to borrow cannot be lower than the min. amount to borrow(per transaction) + '-6029': BadRequest, # Amount to borrow has exceeded the user's estimated max amount to borrow + '5004': ExchangeError, # {"retCode":5004,"retMsg":"Server Timeout","result":null,"retExtInfo":{},"time":1667577060106} + '7001': BadRequest, # {"retCode":7001,"retMsg":"request params type error"} + '10001': BadRequest, # parameter error + '10002': InvalidNonce, # request expired, check your timestamp and recv_window + '10003': AuthenticationError, # Invalid apikey + '10004': AuthenticationError, # invalid sign + '10005': PermissionDenied, # permission denied for current apikey + '10006': RateLimitExceeded, # too many requests + '10007': AuthenticationError, # api_key not found in your request parameters + '10008': AccountSuspended, # User had been banned + '10009': AuthenticationError, # IP had been banned + '10010': PermissionDenied, # request ip mismatch + '10014': BadRequest, # Request is duplicate + '10016': ExchangeError, # {"retCode":10016,"retMsg":"System error. Please try again later."} + '10017': BadRequest, # request path not found or request method is invalid + '10018': RateLimitExceeded, # exceed ip rate limit + '10020': PermissionDenied, # {"retCode":10020,"retMsg":"your account is not a unified margin account, please update your account","result":null,"retExtInfo":null,"time":1664783731123} + '10024': PermissionDenied, # Compliance rules triggered + '10027': PermissionDenied, # Trading Banned + '10028': PermissionDenied, # The API can only be accessed by unified account users. + '10029': PermissionDenied, # The requested symbol is invalid, please check symbol whitelist + '12137': InvalidOrder, # {"retCode":12137,"retMsg":"Order quantity has too many decimals.","result":{},"retExtInfo":{},"time":1695900943033} + '12201': BadRequest, # {"retCode":12201,"retMsg":"Invalid orderCategory parameter.","result":{},"retExtInfo":null,"time":1666699391220} + '12141': BadRequest, # "retCode":12141,"retMsg":"Duplicate clientOrderId.","result":{},"retExtInfo":{},"time":1686134298989} + '100028': PermissionDenied, # The API cannot be accessed by unified account users. + '110001': OrderNotFound, # Order does not exist + '110003': InvalidOrder, # Order price is out of permissible range + '110004': InsufficientFunds, # Insufficient wallet balance + '110005': InvalidOrder, # position status + '110006': InsufficientFunds, # cannot afford estimated position_margin + '110007': InsufficientFunds, # {"retCode":110007,"retMsg":"ab not enough for new order","result":{},"retExtInfo":{},"time":1668838414793} + '110008': InvalidOrder, # Order has been finished or canceled + '110009': InvalidOrder, # The number of stop orders exceeds maximum limit allowed + '110010': InvalidOrder, # Order already cancelled + '110011': InvalidOrder, # Any adjustments made will trigger immediate liquidation + '110012': InsufficientFunds, # Available balance not enough + '110013': BadRequest, # Due to risk limit, cannot set leverage + '110014': InsufficientFunds, # Available balance not enough to add margin + '110015': BadRequest, # the position is in cross_margin + '110016': InvalidOrder, # Requested quantity of contracts exceeds risk limit, please adjust your risk limit level before trying again + '110017': InvalidOrder, # Reduce-only rule not satisfied + '110018': BadRequest, # userId illegal + '110019': InvalidOrder, # orderId illegal + '110020': InvalidOrder, # number of active orders greater than 500 + '110021': InvalidOrder, # Open Interest exceeded + '110022': InvalidOrder, # qty has been limited, cannot modify the order to add qty + '110023': InvalidOrder, # This contract only supports position reduction operation, please contact customer service for details + '110024': BadRequest, # You have an existing position, so position mode cannot be switched + '110025': NoChange, # Position mode is not modified + '110026': MarginModeAlreadySet, # Cross/isolated margin mode is not modified + '110027': NoChange, # Margin is not modified + '110028': BadRequest, # Open orders exist, so you cannot change position mode + '110029': BadRequest, # Hedge mode is not available for self symbol + '110030': InvalidOrder, # Duplicate orderId + '110031': InvalidOrder, # risk limit info does not exists + '110032': InvalidOrder, # Illegal order + '110033': InvalidOrder, # Margin cannot be set without open position + '110034': InvalidOrder, # There is no net position + '110035': InvalidOrder, # Cancel order is not completed before liquidation + '110036': InvalidOrder, # Cross margin mode is not allowed to change leverage + '110037': InvalidOrder, # User setting list does not have self symbol + '110038': InvalidOrder, # Portfolio margin mode is not allowed to change leverage + '110039': InvalidOrder, # Maintain margin rate is too high, which may trigger liquidation + '110040': InvalidOrder, # Order will trigger forced liquidation, please resubmit the order + '110041': InvalidOrder, # Skip liquidation is not allowed when a position or maker order exists + '110042': InvalidOrder, # Pre-delivery status can only reduce positions + '110043': BadRequest, # Set leverage not modified + '110044': InsufficientFunds, # Insufficient available margin + '110045': InsufficientFunds, # Insufficient wallet balance + '110046': BadRequest, # Any adjustments made will trigger immediate liquidation + '110047': BadRequest, # Risk limit cannot be adjusted due to insufficient available margin + '110048': BadRequest, # Risk limit cannot be adjusted current/expected position value held exceeds the revised risk limit + '110049': BadRequest, # Tick notes can only be numbers + '110050': BadRequest, # Coin is not in the range of selected + '110051': InsufficientFunds, # The user's available balance cannot cover the lowest price of the current market + '110052': InsufficientFunds, # User's available balance is insufficient to set a price + '110053': InsufficientFunds, # The user's available balance cannot cover the current market price and upper limit price + '110054': InvalidOrder, # This position has at least one take profit link order, so the take profit and stop loss mode cannot be switched + '110055': InvalidOrder, # This position has at least one stop loss link order, so the take profit and stop loss mode cannot be switched + '110056': InvalidOrder, # This position has at least one trailing stop link order, so the take profit and stop loss mode cannot be switched + '110057': InvalidOrder, # Conditional order or limit order contains TP/SL related params + '110058': InvalidOrder, # Insufficient number of remaining position size to set take profit and stop loss + '110059': InvalidOrder, # In the case of partial filled of the open order, it is not allowed to modify the take profit and stop loss settings of the open order + '110060': BadRequest, # Under full TP/SL mode, it is not allowed to modify TP/SL + '110061': BadRequest, # Under partial TP/SL mode, TP/SL set more than 20 + '110062': BadRequest, # Institution MMP profile not found. + '110063': ExchangeError, # Settlement in progress! xxx not available for trades. + '110064': InvalidOrder, # The number of contracts modified cannot be less than or equal to the filled quantity + '110065': PermissionDenied, # MMP hasn't yet been enabled for your account. Please contact your BD manager. + '110066': ExchangeError, # No trading is allowed at the current time + '110067': PermissionDenied, # unified account is not support + '110068': PermissionDenied, # Leveraged user trading is not allowed + '110069': PermissionDenied, # Do not allow OTC lending users to trade + '110070': InvalidOrder, # ETP symbols are not allowed to be traded + '110071': ExchangeError, # Sorry, we're revamping the Unified Margin Account! Currently, new upgrades are not supported. If you have any questions, please contact our 24/7 customer support. + '110072': InvalidOrder, # OrderLinkedID is duplicate + '110073': ExchangeError, # Set margin mode failed + '110092': InvalidOrder, # expect Rising, but trigger_price[XXXXX] <= current[XXXXX] + '110093': InvalidOrder, # expect Falling, but trigger_price[XXXXX] >= current[XXXXX] + '110094': InvalidOrder, # Order notional value below the lower limit + '130006': InvalidOrder, # {"ret_code":130006,"ret_msg":"The number of contracts exceeds maximum limit allowed: too large","ext_code":"","ext_info":"","result":null,"time_now":"1658397095.099030","rate_limit_status":99,"rate_limit_reset_ms":1658397095097,"rate_limit":100} + '130021': InsufficientFunds, # {"ret_code":130021,"ret_msg":"orderfix price failed for CannotAffordOrderCost.","ext_code":"","ext_info":"","result":null,"time_now":"1644588250.204878","rate_limit_status":98,"rate_limit_reset_ms":1644588250200,"rate_limit":100} | {"ret_code":130021,"ret_msg":"oc_diff[1707966351], new_oc[1707966351] with ob[....]+AB[....]","ext_code":"","ext_info":"","result":null,"time_now":"1658395300.872766","rate_limit_status":99,"rate_limit_reset_ms":1658395300855,"rate_limit":100} caused issues/9149#issuecomment-1146559498 + '130074': InvalidOrder, # {"ret_code":130074,"ret_msg":"expect Rising, but trigger_price[190000000] \u003c= current[211280000]??LastPrice","ext_code":"","ext_info":"","result":null,"time_now":"1655386638.067076","rate_limit_status":97,"rate_limit_reset_ms":1655386638065,"rate_limit":100} + '131001': InsufficientFunds, # {"retCode":131001,"retMsg":"the available balance is not sufficient to cover the handling fee","result":{},"retExtInfo":{},"time":1666892821245} + '131084': ExchangeError, # Withdraw failed because of Uta Upgrading + '131200': ExchangeError, # Service error + '131201': ExchangeError, # Internal error + '131202': BadRequest, # Invalid memberId + '131203': BadRequest, # Request parameter error + '131204': BadRequest, # Account info error + '131205': BadRequest, # Query transfer error + '131206': ExchangeError, # Fail to transfer + '131207': BadRequest, # Account not exist + '131208': ExchangeError, # Forbid transfer + '131209': BadRequest, # Get subMember relation error + '131210': BadRequest, # Amount accuracy error + '131211': BadRequest, # fromAccountType can't be the same + '131212': InsufficientFunds, # Insufficient balance + '131213': BadRequest, # TransferLTV check error + '131214': BadRequest, # TransferId exist + '131215': BadRequest, # Amount error + '131216': ExchangeError, # Query balance error + '131217': ExchangeError, # Risk check error + '131231': NotSupported, # Transfers into self account are not supported + '131232': NotSupported, # Transfers out self account are not supported + '131002': BadRequest, # Parameter error + '131003': ExchangeError, # Interal error + '131004': AuthenticationError, # KYC needed + '131085': InsufficientFunds, # Withdrawal amount is greater than your availale balance(the deplayed withdrawal is triggered) + '131086': BadRequest, # Withdrawal amount exceeds risk limit(the risk limit of margin trade is triggered) + '131088': BadRequest, # The withdrawal amount exceeds the remaining withdrawal limit of your identity verification level. The current available amount for withdrawal : %s + '131089': BadRequest, # User sensitive operation, withdrawal is prohibited within 24 hours + '131090': ExchangeError, # User withdraw has been banned + '131091': ExchangeError, # Blocked login status does not allow withdrawals + '131092': ExchangeError, # User status is abnormal + '131093': ExchangeError, # The withdrawal address is not in the whitelist + '131094': BadRequest, # UserId is not in the whitelist + '131095': BadRequest, # Withdrawl amount exceeds the 24 hour platform limit + '131096': BadRequest, # Withdraw amount does not satify the lower limit or upper limit + '131097': ExchangeError, # Withdrawal of self currency has been closed + '131098': ExchangeError, # Withdrawal currently is not availble from new address + '131099': ExchangeError, # Hot wallet status can cancel the withdraw + '140001': OrderNotFound, # Order does not exist + '140003': InvalidOrder, # Order price is out of permissible range + '140004': InsufficientFunds, # Insufficient wallet balance + '140005': InvalidOrder, # position status + '140006': InsufficientFunds, # cannot afford estimated position_margin + '140007': InsufficientFunds, # Insufficient available balance + '140008': InvalidOrder, # Order has been finished or canceled + '140009': InvalidOrder, # The number of stop orders exceeds maximum limit allowed + '140010': InvalidOrder, # Order already cancelled + '140011': InvalidOrder, # Any adjustments made will trigger immediate liquidation + '140012': InsufficientFunds, # Available balance not enough + '140013': BadRequest, # Due to risk limit, cannot set leverage + '140014': InsufficientFunds, # Available balance not enough to add margin + '140015': InvalidOrder, # the position is in cross_margin + '140016': InvalidOrder, # Requested quantity of contracts exceeds risk limit, please adjust your risk limit level before trying again + '140017': InvalidOrder, # Reduce-only rule not satisfied + '140018': BadRequest, # userId illegal + '140019': InvalidOrder, # orderId illegal + '140020': InvalidOrder, # number of active orders greater than 500 + '140021': InvalidOrder, # Open Interest exceeded + '140022': InvalidOrder, # qty has been limited, cannot modify the order to add qty + '140023': InvalidOrder, # This contract only supports position reduction operation, please contact customer service for details + '140024': BadRequest, # You have an existing position, so position mode cannot be switched + '140025': BadRequest, # Position mode is not modified + '140026': BadRequest, # Cross/isolated margin mode is not modified + '140027': BadRequest, # Margin is not modified + '140028': InvalidOrder, # Open orders exist, so you cannot change position mode + '140029': BadRequest, # Hedge mode is not available for self symbol + '140030': InvalidOrder, # Duplicate orderId + '140031': BadRequest, # risk limit info does not exists + '140032': InvalidOrder, # Illegal order + '140033': InvalidOrder, # Margin cannot be set without open position + '140034': InvalidOrder, # There is no net position + '140035': InvalidOrder, # Cancel order is not completed before liquidation + '140036': BadRequest, # Cross margin mode is not allowed to change leverage + '140037': InvalidOrder, # User setting list does not have self symbol + '140038': BadRequest, # Portfolio margin mode is not allowed to change leverage + '140039': BadRequest, # Maintain margin rate is too high, which may trigger liquidation + '140040': InvalidOrder, # Order will trigger forced liquidation, please resubmit the order + '140041': InvalidOrder, # Skip liquidation is not allowed when a position or maker order exists + '140042': InvalidOrder, # Pre-delivery status can only reduce positions + '140043': BadRequest, # Set leverage not modified + '140044': InsufficientFunds, # Insufficient available margin + '140045': InsufficientFunds, # Insufficient wallet balance + '140046': BadRequest, # Any adjustments made will trigger immediate liquidation + '140047': BadRequest, # Risk limit cannot be adjusted due to insufficient available margin + '140048': BadRequest, # Risk limit cannot be adjusted current/expected position value held exceeds the revised risk limit + '140049': BadRequest, # Tick notes can only be numbers + '140050': InvalidOrder, # Coin is not in the range of selected + '140051': InsufficientFunds, # The user's available balance cannot cover the lowest price of the current market + '140052': InsufficientFunds, # User's available balance is insufficient to set a price + '140053': InsufficientFunds, # The user's available balance cannot cover the current market price and upper limit price + '140054': InvalidOrder, # This position has at least one take profit link order, so the take profit and stop loss mode cannot be switched + '140055': InvalidOrder, # This position has at least one stop loss link order, so the take profit and stop loss mode cannot be switched + '140056': InvalidOrder, # This position has at least one trailing stop link order, so the take profit and stop loss mode cannot be switched + '140057': InvalidOrder, # Conditional order or limit order contains TP/SL related params + '140058': InvalidOrder, # Insufficient number of remaining position size to set take profit and stop loss + '140059': InvalidOrder, # In the case of partial filled of the open order, it is not allowed to modify the take profit and stop loss settings of the open order + '140060': BadRequest, # Under full TP/SL mode, it is not allowed to modify TP/SL + '140061': BadRequest, # Under partial TP/SL mode, TP/SL set more than 20 + '140062': BadRequest, # Institution MMP profile not found. + '140063': ExchangeError, # Settlement in progress! xxx not available for trades. + '140064': InvalidOrder, # The number of contracts modified cannot be less than or equal to the filled quantity + '140065': PermissionDenied, # MMP hasn't yet been enabled for your account. Please contact your BD manager. + '140066': ExchangeError, # No trading is allowed at the current time + '140067': PermissionDenied, # unified account is not support + '140068': PermissionDenied, # Leveraged user trading is not allowed + '140069': PermissionDenied, # Do not allow OTC lending users to trade + '140070': InvalidOrder, # ETP symbols are not allowed to be traded + '170001': ExchangeError, # Internal error. + '170005': InvalidOrder, # Too many new orders; current limit is %s orders per %s. + '170007': RequestTimeout, # Timeout waiting for response from backend server. + '170010': InvalidOrder, # Purchase failed: Exceed the maximum position limit of leveraged tokens, the current available limit is %s USDT + '170011': InvalidOrder, # "Purchase failed: Exceed the maximum position limit of innovation tokens, + '170019': InvalidOrder, # the current available limit is replaceKey0 USDT" + '170031': ExchangeError, # The feature has been suspended + '170032': ExchangeError, # Network error. Please try again later + '170033': InsufficientFunds, # margin Insufficient account balance + '170034': InsufficientFunds, # Liability over flow in spot leverage trade! + '170035': BadRequest, # Submitted to the system for processing! + '170036': BadRequest, # You haven't enabled Cross Margin Trading yet. To do so, please head to the PC trading site or the Bybit app + '170037': BadRequest, # Cross Margin Trading not yet supported by the selected coin + '170105': BadRequest, # Parameter '%s' was empty. + '170115': InvalidOrder, # Invalid timeInForce. + '170116': InvalidOrder, # Invalid orderType. + '170117': InvalidOrder, # Invalid side. + '170121': InvalidOrder, # Invalid symbol. + '170124': InvalidOrder, # Order amount too large. + '170130': BadRequest, # Data sent for paramter '%s' is not valid. + '170131': InsufficientFunds, # Balance insufficient + '170132': InvalidOrder, # Order price too high. + '170133': InvalidOrder, # Order price lower than the minimum. + '170134': InvalidOrder, # Order price decimal too long. + '170135': InvalidOrder, # Order quantity too large. + '170136': InvalidOrder, # Order quantity lower than the minimum. + '170137': InvalidOrder, # Order volume decimal too long + '170139': InvalidOrder, # Order has been filled. + '170140': InvalidOrder, # Transaction amount lower than the minimum. + '170141': InvalidOrder, # Duplicate clientOrderId + '170142': InvalidOrder, # Order has been canceled + '170143': InvalidOrder, # Cannot be found on order book + '170144': InvalidOrder, # Order has been locked + '170145': InvalidOrder, # This order type does not support cancellation + '170146': InvalidOrder, # Order creation timeout + '170147': InvalidOrder, # Order cancellation timeout + '170148': InvalidOrder, # Market order amount decimal too long + '170149': ExchangeError, # Create order failed + '170150': ExchangeError, # Cancel order failed + '170151': InvalidOrder, # The trading pair is not open yet + '170157': InvalidOrder, # The trading pair is not available for api trading + '170159': InvalidOrder, # Market Order is not supported within the first %s minutes of newly launched pairs due to risk control. + '170190': InvalidOrder, # Cancel order has been finished + '170191': InvalidOrder, # Can not cancel order, please try again later + '170192': InvalidOrder, # Order price cannot be higher than %s . + '170193': InvalidOrder, # Buy order price cannot be higher than %s. + '170194': InvalidOrder, # Sell order price cannot be lower than %s. + '170195': InvalidOrder, # Please note that your order may not be filled + '170196': InvalidOrder, # Please note that your order may not be filled + '170197': InvalidOrder, # Your order quantity to buy is too large. The filled price may deviate significantly from the market price. Please try again + '170198': InvalidOrder, # Your order quantity to sell is too large. The filled price may deviate significantly from the market price. Please try again + '170199': InvalidOrder, # Your order quantity to buy is too large. The filled price may deviate significantly from the nav. Please try again. + '170200': InvalidOrder, # Your order quantity to sell is too large. The filled price may deviate significantly from the nav. Please try again. + '170201': PermissionDenied, # Your account has been restricted for trades. If you have any questions, please email us at support@bybit.com + '170202': InvalidOrder, # Invalid orderFilter parameter. + '170203': InvalidOrder, # Please enter the TP/SL price. + '170204': InvalidOrder, # trigger price cannot be higher than 110% price. + '170206': InvalidOrder, # trigger price cannot be lower than 90% of qty. + '170210': InvalidOrder, # New order rejected. + '170213': OrderNotFound, # Order does not exist. + '170217': InvalidOrder, # Only LIMIT-MAKER order is supported for the current pair. + '170218': InvalidOrder, # The LIMIT-MAKER order is rejected due to invalid price. + '170221': BadRequest, # This coin does not exist. + '170222': RateLimitExceeded, # Too many hasattr(self, requests) time frame. + '170223': InsufficientFunds, # Your Spot Account with Institutional Lending triggers an alert or liquidation. + '170224': PermissionDenied, # You're not a user of the Innovation Zone. + '170226': InsufficientFunds, # Your Spot Account for Margin Trading is being liquidated. + '170227': ExchangeError, # This feature is not supported. + '170228': InvalidOrder, # The purchase amount of each order exceeds the estimated maximum purchase amount. + '170229': InvalidOrder, # The sell quantity per order exceeds the estimated maximum sell quantity. + '170234': ExchangeError, # System Error + '170241': ManualInteractionNeeded, # To proceed with trading, users must read through and confirm that they fully understand the project's risk disclosure document. + '175000': InvalidOrder, # The serialNum is already in use. + '175001': InvalidOrder, # Daily purchase limit has been exceeded. Please try again later. + '175002': InvalidOrder, # There's a large number of purchase orders. Please try again later. + '175003': InsufficientFunds, # Insufficient available balance. Please make a deposit and try again. + '175004': InvalidOrder, # Daily redemption limit has been exceeded. Please try again later. + '175005': InvalidOrder, # There's a large number of redemption orders. Please try again later. + '175006': InsufficientFunds, # Insufficient available balance. Please make a deposit and try again. + '175007': InvalidOrder, # Order not found. + '175008': InvalidOrder, # Purchase period hasn't started yet. + '175009': InvalidOrder, # Purchase amount has exceeded the upper limit. + '175010': PermissionDenied, # You haven't passed the quiz yet! To purchase and/or redeem an LT, please complete the quiz first. + '175012': InvalidOrder, # Redemption period hasn't started yet. + '175013': InvalidOrder, # Redemption amount has exceeded the upper limit. + '175014': InvalidOrder, # Purchase of the LT has been temporarily suspended. + '175015': InvalidOrder, # Redemption of the LT has been temporarily suspended. + '175016': InvalidOrder, # Invalid format. Please check the length and numeric precision. + '175017': InvalidOrder, # Failed to place order:Exceed the maximum position limit of leveraged tokens, the current available limit is XXXX USDT + '175027': ExchangeError, # Subscriptions and redemptions are temporarily unavailable while account upgrade is in progress + '176002': BadRequest, # Query user account info error + '176004': BadRequest, # Query order history start time exceeds end time + '176003': BadRequest, # Query user loan history error + '176006': BadRequest, # Repayment Failed + '176005': BadRequest, # Failed to borrow + '176008': BadRequest, # You haven't enabled Cross Margin Trading yet. To do so + '176007': BadRequest, # User not found + '176010': BadRequest, # Failed to locate the coins to borrow + '176009': BadRequest, # You haven't enabled Cross Margin Trading yet. To do so + '176012': BadRequest, # Pair not available + '176011': BadRequest, # Cross Margin Trading not yet supported by the selected coin + '176014': BadRequest, # Repeated repayment requests + '176013': BadRequest, # Cross Margin Trading not yet supported by the selected pair + '176015': InsufficientFunds, # Insufficient available balance + '176016': BadRequest, # No repayment required + '176017': BadRequest, # Repayment amount has exceeded the total liability + '176018': BadRequest, # Settlement in progress + '176019': BadRequest, # Liquidation in progress + '176020': BadRequest, # Failed to locate repayment history + '176021': BadRequest, # Repeated borrowing requests + '176022': BadRequest, # Coins to borrow not generally available yet + '176023': BadRequest, # Pair to borrow not generally available yet + '176024': BadRequest, # Invalid user status + '176025': BadRequest, # Amount to borrow cannot be lower than the min. amount to borrow(per transaction) + '176026': BadRequest, # Amount to borrow cannot be larger than the max. amount to borrow(per transaction) + '176027': BadRequest, # Amount to borrow cannot be higher than the max. amount to borrow per user + '176028': BadRequest, # Amount to borrow has exceeded Bybit's max. amount to borrow + '176029': BadRequest, # Amount to borrow has exceeded the user's estimated max. amount to borrow + '176030': BadRequest, # Query user loan info error + '176031': BadRequest, # Number of decimals has exceeded the maximum precision + '176034': BadRequest, # The leverage ratio is out of range + '176035': PermissionDenied, # Failed to close the leverage switch during liquidation + '176036': PermissionDenied, # Failed to adjust leverage switch during forced liquidation + '176037': PermissionDenied, # For non-unified transaction users, the operation failed + '176038': BadRequest, # The spot leverage is closed and the current operation is not allowed + '176039': BadRequest, # Borrowing, current operation is not allowed + '176040': BadRequest, # There is a spot leverage order, and the adjustment of the leverage switch failed! + '181000': BadRequest, # category is null + '181001': BadRequest, # category only support linear or option or spot. + '181002': InvalidOrder, # symbol is null. + '181003': InvalidOrder, # side is null. + '181004': InvalidOrder, # side only support Buy or Sell. + '182000': InvalidOrder, # symbol related quote price is null + '181017': BadRequest, # OrderStatus must be final status + '20001': OrderNotFound, # Order not exists + '20003': InvalidOrder, # missing parameter side + '20004': InvalidOrder, # invalid parameter side + '20005': InvalidOrder, # missing parameter symbol + '20006': InvalidOrder, # invalid parameter symbol + '20007': InvalidOrder, # missing parameter order_type + '20008': InvalidOrder, # invalid parameter order_type + '20009': InvalidOrder, # missing parameter qty + '20010': InvalidOrder, # qty must be greater than 0 + '20011': InvalidOrder, # qty must be an integer + '20012': InvalidOrder, # qty must be greater than zero and less than 1 million + '20013': InvalidOrder, # missing parameter price + '20014': InvalidOrder, # price must be greater than 0 + '20015': InvalidOrder, # missing parameter time_in_force + '20016': InvalidOrder, # invalid value for parameter time_in_force + '20017': InvalidOrder, # missing parameter order_id + '20018': InvalidOrder, # invalid date format + '20019': InvalidOrder, # missing parameter stop_px + '20020': InvalidOrder, # missing parameter base_price + '20021': InvalidOrder, # missing parameter stop_order_id + '20022': BadRequest, # missing parameter leverage + '20023': BadRequest, # leverage must be a number + '20031': BadRequest, # leverage must be greater than zero + '20070': BadRequest, # missing parameter margin + '20071': BadRequest, # margin must be greater than zero + '20084': BadRequest, # order_id or order_link_id is required + '30001': BadRequest, # order_link_id is repeated + '30003': InvalidOrder, # qty must be more than the minimum allowed + '30004': InvalidOrder, # qty must be less than the maximum allowed + '30005': InvalidOrder, # price exceeds maximum allowed + '30007': InvalidOrder, # price exceeds minimum allowed + '30008': InvalidOrder, # invalid order_type + '30009': ExchangeError, # no position found + '30010': InsufficientFunds, # insufficient wallet balance + '30011': PermissionDenied, # operation not allowed is undergoing liquidation + '30012': PermissionDenied, # operation not allowed is undergoing ADL + '30013': PermissionDenied, # position is in liq or adl status + '30014': InvalidOrder, # invalid closing order, qty should not greater than size + '30015': InvalidOrder, # invalid closing order, side should be opposite + '30016': ExchangeError, # TS and SL must be cancelled first while closing position + '30017': InvalidOrder, # estimated fill price cannot be lower than current Buy liq_price + '30018': InvalidOrder, # estimated fill price cannot be higher than current Sell liq_price + '30019': InvalidOrder, # cannot attach TP/SL params for non-zero position when placing non-opening position order + '30020': InvalidOrder, # position already has TP/SL params + '30021': InvalidOrder, # cannot afford estimated position_margin + '30022': InvalidOrder, # estimated buy liq_price cannot be higher than current mark_price + '30023': InvalidOrder, # estimated sell liq_price cannot be lower than current mark_price + '30024': InvalidOrder, # cannot set TP/SL/TS for zero-position + '30025': InvalidOrder, # trigger price should bigger than 10% of last price + '30026': InvalidOrder, # price too high + '30027': InvalidOrder, # price set for Take profit should be higher than Last Traded Price + '30028': InvalidOrder, # price set for Stop loss should be between Liquidation price and Last Traded Price + '30029': InvalidOrder, # price set for Stop loss should be between Last Traded Price and Liquidation price + '30030': InvalidOrder, # price set for Take profit should be lower than Last Traded Price + '30031': InsufficientFunds, # insufficient available balance for order cost + '30032': InvalidOrder, # order has been filled or cancelled + '30033': RateLimitExceeded, # The number of stop orders exceeds maximum limit allowed + '30034': OrderNotFound, # no order found + '30035': RateLimitExceeded, # too fast to cancel + '30036': ExchangeError, # the expected position value after order execution exceeds the current risk limit + '30037': InvalidOrder, # order already cancelled + '30041': ExchangeError, # no position found + '30042': InsufficientFunds, # insufficient wallet balance + '30043': InvalidOrder, # operation not allowed is undergoing liquidation + '30044': InvalidOrder, # operation not allowed is undergoing AD + '30045': InvalidOrder, # operation not allowed is not normal status + '30049': InsufficientFunds, # insufficient available balance + '30050': ExchangeError, # any adjustments made will trigger immediate liquidation + '30051': ExchangeError, # due to risk limit, cannot adjust leverage + '30052': ExchangeError, # leverage can not less than 1 + '30054': ExchangeError, # position margin is invalid + '30057': ExchangeError, # requested quantity of contracts exceeds risk limit + '30063': ExchangeError, # reduce-only rule not satisfied + '30067': InsufficientFunds, # insufficient available balance + '30068': ExchangeError, # exit value must be positive + '30074': InvalidOrder, # can't create the stop order, because you expect the order will be triggered when the LastPrice(or IndexPrice、 MarkPrice, determined by trigger_by) is raising to stop_px, but the LastPrice(or IndexPrice、 MarkPrice) is already equal to or greater than stop_px, please adjust base_price or stop_px + '30075': InvalidOrder, # can't create the stop order, because you expect the order will be triggered when the LastPrice(or IndexPrice、 MarkPrice, determined by trigger_by) is falling to stop_px, but the LastPrice(or IndexPrice、 MarkPrice) is already equal to or less than stop_px, please adjust base_price or stop_px + '30078': ExchangeError, # {"ret_code":30078,"ret_msg":"","ext_code":"","ext_info":"","result":null,"time_now":"1644853040.916000","rate_limit_status":73,"rate_limit_reset_ms":1644853040912,"rate_limit":75} + # '30084': BadRequest, # Isolated not modified, see handleErrors below + '33004': AuthenticationError, # apikey already expired + '34026': ExchangeError, # the limit is no change + '34036': BadRequest, # {"ret_code":34036,"ret_msg":"leverage not modified","ext_code":"","ext_info":"","result":null,"time_now":"1652376449.258918","rate_limit_status":74,"rate_limit_reset_ms":1652376449255,"rate_limit":75} + '35015': BadRequest, # {"ret_code":35015,"ret_msg":"Qty not in range","ext_code":"","ext_info":"","result":null,"time_now":"1652277215.821362","rate_limit_status":99,"rate_limit_reset_ms":1652277215819,"rate_limit":100} + '340099': ExchangeError, # Server error + '3400045': ExchangeError, # Set margin mode failed + '3100116': BadRequest, # {"retCode":3100116,"retMsg":"Order quantity below the lower limit 0.01.","result":null,"retExtMap":{"key0":"0.01"}} + '3100198': BadRequest, # {"retCode":3100198,"retMsg":"orderLinkId can not be empty.","result":null,"retExtMap":{}} + '3200300': InsufficientFunds, # {"retCode":3200300,"retMsg":"Insufficient margin balance.","result":null,"retExtMap":{}} + }, + 'broad': { + 'Not supported symbols': BadSymbol, # {"retCode":10001,"retMsg":"Not supported symbols","result":{},"retExtInfo":{},"time":1726147060461} + 'Request timeout': RequestTimeout, # {"retCode":10016,"retMsg":"Request timeout, please try again later","result":{},"retExtInfo":{},"time":1675307914985} + 'unknown orderInfo': OrderNotFound, # {"ret_code":-1,"ret_msg":"unknown orderInfo","ext_code":"","ext_info":"","result":null,"time_now":"1584030414.005545","rate_limit_status":99,"rate_limit_reset_ms":1584030414003,"rate_limit":100} + 'invalid api_key': AuthenticationError, # {"ret_code":10003,"ret_msg":"invalid api_key","ext_code":"","ext_info":"","result":null,"time_now":"1599547085.415797"} + # the below two issues are caused: issues/9149#issuecomment-1146559498, when response is such: {"ret_code":130021,"ret_msg":"oc_diff[1707966351], new_oc[1707966351] with ob[....]+AB[....]","ext_code":"","ext_info":"","result":null,"time_now":"1658395300.872766","rate_limit_status":99,"rate_limit_reset_ms":1658395300855,"rate_limit":100} + 'oc_diff': InsufficientFunds, + 'new_oc': InsufficientFunds, + 'openapi sign params error!': AuthenticationError, # {"retCode":10001,"retMsg":"empty value: apiTimestamp[] apiKey[] apiSignature[xxxxxxxxxxxxxxxxxxxxxxx]: openapi sign params error!","result":null,"retExtInfo":null,"time":1664789597123} + }, + }, + 'precisionMode': TICK_SIZE, + 'options': { + 'usePrivateInstrumentsInfo': False, + 'enableDemoTrading': False, + 'fetchMarkets': { + 'types': ['spot', 'linear', 'inverse', 'option'], + }, + 'enableUnifiedMargin': None, + 'enableUnifiedAccount': None, + 'unifiedMarginStatus': None, + 'createMarketBuyOrderRequiresPrice': False, # only True for classic accounts + 'createUnifiedMarginAccount': False, + 'defaultType': 'swap', # 'swap', 'future', 'option', 'spot' + 'defaultSubType': 'linear', # 'linear', 'inverse' + 'defaultSettle': 'USDT', # USDC for USDC settled markets + 'code': 'BTC', + 'recvWindow': 5 * 1000, # 5 sec default + 'timeDifference': 0, # the difference between system clock and exchange server clock + 'adjustForTimeDifference': False, # controls the adjustment logic upon instantiation + 'loadAllOptions': False, # load all possible option markets, adds signficant load time + 'loadExpiredOptions': False, # loads expired options, to load all possible expired options set loadAllOptions to True + 'brokerId': 'CCXT', + 'accountsByType': { + 'spot': 'SPOT', + 'margin': 'SPOT', + 'future': 'CONTRACT', + 'swap': 'CONTRACT', + 'option': 'OPTION', + 'investment': 'INVESTMENT', + 'unified': 'UNIFIED', + 'funding': 'FUND', + 'fund': 'FUND', + 'contract': 'CONTRACT', + }, + 'accountsById': { + 'SPOT': 'spot', + 'MARGIN': 'spot', + 'CONTRACT': 'contract', + 'OPTION': 'option', + 'INVESTMENT': 'investment', + 'UNIFIED': 'unified', + 'FUND': 'fund', + }, + 'networks': { + 'ERC20': 'ETH', + 'TRC20': 'TRX', + 'BEP20': 'BSC', + 'SOL': 'SOL', + 'ACA': 'ACA', + 'ADA': 'ADA', + 'ALGO': 'ALGO', + 'APT': 'APTOS', + 'AR': 'AR', + 'ARBONE': 'ARBI', + 'AVAXC': 'CAVAX', + 'AVAXX': 'XAVAX', + 'ATOM': 'ATOM', + 'BCH': 'BCH', + 'BEP2': 'BNB', + 'CHZ': 'CHZ', + 'DCR': 'DCR', + 'DGB': 'DGB', + 'DOGE': 'DOGE', + 'DOT': 'DOT', + 'EGLD': 'EGLD', + 'EOS': 'EOS', + 'ETC': 'ETC', + 'ETHF': 'ETHF', + 'ETHW': 'ETHW', + 'FIL': 'FIL', + 'STEP': 'FITFI', + 'FLOW': 'FLOW', + 'FTM': 'FTM', + 'GLMR': 'GLMR', + 'HBAR': 'HBAR', + 'HNT': 'HNT', + 'ICP': 'ICP', + 'ICX': 'ICX', + 'KDA': 'KDA', + 'KLAY': 'KLAY', + 'KMA': 'KMA', + 'KSM': 'KSM', + 'LTC': 'LTC', + # 'TERRA': 'LUNANEW', + # 'TERRACLASSIC': 'LUNA', + 'MATIC': 'MATIC', + 'MINA': 'MINA', + 'MOVR': 'MOVR', + 'NEAR': 'NEAR', + 'NEM': 'NEM', + 'OASYS': 'OAS', + 'OASIS': 'ROSE', + 'OMNI': 'OMNI', + 'ONE': 'ONE', + 'OPTIMISM': 'OP', + 'POKT': 'POKT', + 'QTUM': 'QTUM', + 'RVN': 'RVN', + 'SC': 'SC', + 'SCRT': 'SCRT', + 'STX': 'STX', + 'THETA': 'THETA', + 'TON': 'TON', + 'WAVES': 'WAVES', + 'WAX': 'WAXP', + 'XDC': 'XDC', + 'XEC': 'XEC', + 'XLM': 'XLM', + 'XRP': 'XRP', + 'XTZ': 'XTZ', + 'XYM': 'XYM', + 'ZEN': 'ZEN', + 'ZIL': 'ZIL', + 'ZKSYNC': 'ZKSYNC', + # todo: uncomment after consensus + # 'CADUCEUS': 'CMP', + # 'KON': 'KON', # konpay, "konchain" + # 'AURORA': 'AURORA', + # 'BITCOINGOLD': 'BTG', + }, + 'networksById': { + 'ETH': 'ERC20', + 'TRX': 'TRC20', + 'BSC': 'BEP20', + 'OMNI': 'OMNI', + 'SPL': 'SOL', + }, + 'defaultNetwork': 'ERC20', + 'defaultNetworks': { + 'USDT': 'TRC20', + }, + 'intervals': { + '5m': '5min', + '15m': '15min', + '30m': '30min', + '1h': '1h', + '4h': '4h', + '1d': '1d', + }, + 'useMarkPriceForPositionCollateral': False, # use mark price for position collateral + }, + 'features': { + 'default': { + 'sandbox': True, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerPriceType': { + 'last': True, + 'mark': True, + 'index': True, + }, + 'triggerDirection': True, + 'stopLossPrice': True, + 'takeProfitPrice': True, + 'attachedStopLossTakeProfit': { + 'triggerPriceType': { + 'last': True, + 'mark': True, + 'index': True, + }, + 'price': True, + }, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': True, + 'selfTradePrevention': True, # todo: implement + 'trailing': True, + 'iceberg': False, + 'leverage': False, + 'marketBuyRequiresPrice': False, + 'marketBuyByCost': True, + }, + 'createOrders': { + 'max': 10, + }, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 365 * 2, # 2 years + 'untilDays': 7, # days between start-end + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': True, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 50, + 'trigger': True, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 50, + 'daysBack': 365 * 2, # 2 years + 'daysBackCanceled': 1, + 'untilDays': 7, + 'trigger': True, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 1000, + }, + 'editOrders': { + 'max': 10, + }, + }, + 'spot': { + 'extends': 'default', + 'fetchCurrencies': { + 'private': True, + }, + 'createOrder': { + 'triggerPriceType': None, + 'triggerDirection': False, + 'attachedStopLossTakeProfit': { + 'triggerPriceType': None, + 'price': True, + }, + 'marketBuyRequiresPrice': True, + }, + }, + 'swap': { + 'linear': { + 'extends': 'default', + }, + 'inverse': { + 'extends': 'default', + }, + }, + 'future': { + 'linear': { + 'extends': 'default', + }, + 'inverse': { + 'extends': 'default', + }, + }, + }, + 'fees': { + 'trading': { + 'feeSide': 'get', + 'tierBased': True, + 'percentage': True, + 'taker': 0.00075, + 'maker': 0.0001, + }, + 'funding': { + 'tierBased': False, + 'percentage': False, + 'withdraw': {}, + 'deposit': {}, + }, + }, + }) + + def enable_demo_trading(self, enable: bool): + """ + enables or disables demo trading mode + + https://bybit-exchange.github.io/docs/v5/demo + + :param boolean [enable]: True if demo trading should be enabled, False otherwise + """ + if self.isSandboxModeEnabled: + raise NotSupported(self.id + ' demo trading does not support in sandbox environment') + # enable demo trading in bybit, see: https://bybit-exchange.github.io/docs/v5/demo + if enable: + self.urls['apiBackupDemoTrading'] = self.urls['api'] + self.urls['api'] = self.urls['demotrading'] + elif 'apiBackupDemoTrading' in self.urls: + self.urls['api'] = self.urls['apiBackupDemoTrading'] + newUrls = self.omit(self.urls, 'apiBackupDemoTrading') + self.urls = newUrls + self.options['enableDemoTrading'] = enable + + def nonce(self): + return self.milliseconds() - self.options['timeDifference'] + + def add_pagination_cursor_to_result(self, response): + result = self.safe_dict(response, 'result', {}) + data = self.safe_list_n(result, ['list', 'rows', 'data', 'dataList'], []) + paginationCursor = self.safe_string_2(result, 'nextPageCursor', 'cursor') + dataLength = len(data) + if (paginationCursor is not None) and (dataLength > 0): + first = data[0] + first['nextPageCursor'] = paginationCursor + data[0] = first + return data + + def is_unified_enabled(self, params={}): + """ + + https://bybit-exchange.github.io/docs/v5/user/apikey-info#http-request + https://bybit-exchange.github.io/docs/v5/account/account-info + + returns [enableUnifiedMargin, enableUnifiedAccount] so the user can check if unified account is enabled + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns any: [enableUnifiedMargin, enableUnifiedAccount] + """ + # The API key of user id must own one of permissions will be allowed to call following API endpoints: + # SUB UID: "Account Transfer" + # MASTER UID: "Account Transfer", "Subaccount Transfer", "Withdrawal" + enableUnifiedMargin = self.safe_bool(self.options, 'enableUnifiedMargin') + enableUnifiedAccount = self.safe_bool(self.options, 'enableUnifiedAccount') + if enableUnifiedMargin is None or enableUnifiedAccount is None: + if self.options['enableDemoTrading']: + # info endpoint is not available in demo trading + # so we're assuming UTA is enabled + self.options['enableUnifiedMargin'] = False + self.options['enableUnifiedAccount'] = True + self.options['unifiedMarginStatus'] = 6 + return [self.options['enableUnifiedMargin'], self.options['enableUnifiedAccount']] + rawPromises = [self.privateGetV5UserQueryApi(params), self.privateGetV5AccountInfo(params)] + promises = rawPromises + response = promises[0] + accountInfo = promises[1] + # + # { + # "retCode": 0, + # "retMsg": "", + # "result": { + # "id": "13770661", + # "note": "XXXXXX", + # "apiKey": "XXXXXX", + # "readOnly": 0, + # "secret": "", + # "permissions": { + # "ContractTrade": [...], + # "Spot": [...], + # "Wallet": [...], + # "Options": [...], + # "Derivatives": [...], + # "CopyTrading": [...], + # "BlockTrade": [...], + # "Exchange": [...], + # "NFT": [...], + # }, + # "ips": [...], + # "type": 1, + # "deadlineDay": 83, + # "expiredAt": "2023-05-15T03:21:05Z", + # "createdAt": "2022-10-16T02:24:40Z", + # "unified": 0, + # "uta": 0, + # "userID": 24600000, + # "inviterID": 0, + # "vipLevel": "No VIP", + # "mktMakerLevel": "0", + # "affiliateID": 0, + # "rsaPublicKey": "", + # "isMaster": False + # }, + # "retExtInfo": {}, + # "time": 1676891757649 + # } + # account info + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "marginMode": "REGULAR_MARGIN", + # "updatedTime": "1697078946000", + # "unifiedMarginStatus": 4, + # "dcpStatus": "OFF", + # "timeWindow": 10, + # "smpGroup": 0, + # "isMasterTrader": False, + # "spotHedgingStatus": "OFF" + # } + # } + # + result = self.safe_dict(response, 'result', {}) + accountResult = self.safe_dict(accountInfo, 'result', {}) + self.options['enableUnifiedMargin'] = self.safe_integer(result, 'unified') == 1 + self.options['enableUnifiedAccount'] = self.safe_integer(result, 'uta') == 1 + self.options['unifiedMarginStatus'] = self.safe_integer(accountResult, 'unifiedMarginStatus', 6) # default to uta 2.0 pro if not found + return [self.options['enableUnifiedMargin'], self.options['enableUnifiedAccount']] + + def upgrade_unified_trade_account(self, params={}): + """ + upgrades the account to unified trade account *warning* self is irreversible + + https://bybit-exchange.github.io/docs/v5/account/upgrade-unified-account + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns any: nothing + """ + return self.privatePostV5AccountUpgradeToUta(params) + + def create_expired_option_market(self, symbol: str): + # support expired option contracts + quote = None + settle = None + optionParts = symbol.split('-') + symbolBase = symbol.split('/') + base = None + expiry = None + if symbol.find('/') > -1: + base = self.safe_string(symbolBase, 0) + expiry = self.safe_string(optionParts, 1) + symbolQuoteAndSettle = self.safe_string(symbolBase, 1) + splitQuote = symbolQuoteAndSettle.split(':') + quoteAndSettle = self.safe_string(splitQuote, 0) + quote = quoteAndSettle + settle = quoteAndSettle + else: + base = self.safe_string(optionParts, 0) + expiry = self.convert_market_id_expire_date(self.safe_string(optionParts, 1)) + if symbol.endswith('-USDT'): + quote = 'USDT' + settle = 'USDT' + else: + quote = 'USDC' + settle = 'USDC' + strike = self.safe_string(optionParts, 2) + optionType = self.safe_string(optionParts, 3) + datetime = self.convert_expire_date(expiry) + timestamp = self.parse8601(datetime) + amountPrecision = None + pricePrecision = None + # hard coded amount and price precisions from fetchOptionMarkets + if base == 'BTC': + amountPrecision = self.parse_number('0.01') + pricePrecision = self.parse_number('5') + elif base == 'ETH': + amountPrecision = self.parse_number('0.1') + pricePrecision = self.parse_number('0.1') + elif base == 'SOL': + amountPrecision = self.parse_number('1') + pricePrecision = self.parse_number('0.01') + return { + 'id': base + '-' + self.convert_expire_date_to_market_id_date(expiry) + '-' + strike + '-' + optionType, + 'symbol': base + '/' + quote + ':' + settle + '-' + expiry + '-' + strike + '-' + optionType, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': base, + 'quoteId': quote, + 'settleId': settle, + 'active': False, + 'type': 'option', + 'linear': None, + 'inverse': None, + 'spot': False, + 'swap': False, + 'future': False, + 'option': True, + 'margin': False, + 'contract': True, + 'contractSize': self.parse_number('1'), + 'expiry': timestamp, + 'expiryDatetime': datetime, + 'optionType': 'call' if (optionType == 'C') else 'put', + 'strike': self.parse_number(strike), + 'precision': { + 'amount': amountPrecision, + 'price': pricePrecision, + }, + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'info': None, + } + + def safe_market(self, marketId: Str = None, market: Market = None, delimiter: Str = None, marketType: Str = None) -> MarketInterface: + isOption = (marketId is not None) and ((marketId.find('-C') > -1) or (marketId.find('-P') > -1)) + if isOption and not (marketId in self.markets_by_id): + # handle expired option contracts + return self.create_expired_option_market(marketId) + return super(bybit, self).safe_market(marketId, market, delimiter, marketType) + + def get_bybit_type(self, method, market, params={}): + type = None + type, params = self.handle_market_type_and_params(method, market, params) + subType = None + subType, params = self.handle_sub_type_and_params(method, market, params) + if type == 'option' or type == 'spot': + return [type, params] + return [subType, params] + + def get_amount(self, symbol: str, amount: float): + # some markets like options might not have the precision available + # and we shouldn't crash in those cases + market = self.market(symbol) + emptyPrecisionAmount = (market['precision']['amount'] is None) + amountString = self.number_to_string(amount) + if not emptyPrecisionAmount and (amountString != '0'): + return self.amount_to_precision(symbol, amount) + return amountString + + def get_price(self, symbol: str, price: str): + if price is None: + return price + market = self.market(symbol) + emptyPrecisionPrice = (market['precision']['price'] is None) + if not emptyPrecisionPrice: + return self.price_to_precision(symbol, price) + return price + + def get_cost(self, symbol: str, cost: str): + market = self.market(symbol) + emptyPrecisionPrice = (market['precision']['price'] is None) + if not emptyPrecisionPrice: + return self.cost_to_precision(symbol, cost) + return cost + + def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://bybit-exchange.github.io/docs/v5/market/time + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = self.publicGetV5MarketTime(params) + # + # { + # "retCode": "0", + # "retMsg": "OK", + # "result": { + # "timeSecond": "1666879482", + # "timeNano": "1666879482792685914" + # }, + # "retExtInfo": {}, + # "time": "1666879482792" + # } + # + return self.safe_integer(response, 'time') + + def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://bybit-exchange.github.io/docs/v5/asset/coin-info + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + if not self.check_required_credentials(False): + return {} + if self.options['enableDemoTrading']: + return {} + response = self.privateGetV5AssetCoinQueryInfo(params) + # + # { + # "retCode": 0, + # "retMsg": "", + # "result": { + # "rows": [ + # { + # "name": "BTC", + # "coin": "BTC", + # "remainAmount": "150", + # "chains": [ + # { + # "chainType": "BTC", + # "confirmation": "10000", + # "withdrawFee": "0.0005", + # "depositMin": "0.0005", + # "withdrawMin": "0.001", + # "chain": "BTC", + # "chainDeposit": "1", + # "chainWithdraw": "1", + # "minAccuracy": "8" + # } + # ] + # } + # ] + # }, + # "retExtInfo": {}, + # "time": 1672194582264 + # } + # + data = self.safe_dict(response, 'result', {}) + rows = self.safe_list(data, 'rows', []) + result: dict = {} + for i in range(0, len(rows)): + currency = rows[i] + currencyId = self.safe_string(currency, 'coin') + code = self.safe_currency_code(currencyId) + name = self.safe_string(currency, 'name') + chains = self.safe_list(currency, 'chains', []) + networks: dict = {} + for j in range(0, len(chains)): + chain = chains[j] + networkId = self.safe_string(chain, 'chain') + networkCode = self.network_id_to_code(networkId) + networks[networkCode] = { + 'info': chain, + 'id': networkId, + 'network': networkCode, + 'active': None, + 'deposit': self.safe_integer(chain, 'chainDeposit') == 1, + 'withdraw': self.safe_integer(chain, 'chainWithdraw') == 1, + 'fee': self.safe_number(chain, 'withdrawFee'), + 'precision': self.parse_number(self.parse_precision(self.safe_string(chain, 'minAccuracy'))), + 'limits': { + 'withdraw': { + 'min': self.safe_number(chain, 'withdrawMin'), + 'max': None, + }, + 'deposit': { + 'min': self.safe_number(chain, 'depositMin'), + 'max': None, + }, + }, + } + result[code] = self.safe_currency_structure({ + 'info': currency, + 'code': code, + 'id': currencyId, + 'name': name, + 'active': None, + 'deposit': None, + 'withdraw': None, + 'fee': None, + 'precision': None, + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + 'deposit': { + 'min': None, + 'max': None, + }, + }, + 'networks': networks, + 'type': 'crypto', # atm exchange api provides only cryptos + }) + return result + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for bybit + + https://bybit-exchange.github.io/docs/v5/market/instrument + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + if self.options['adjustForTimeDifference']: + self.load_time_difference() + promisesUnresolved = [] + types = None + defaultTypes = ['spot', 'linear', 'inverse', 'option'] + fetchMarketsOptions = self.safe_dict(self.options, 'fetchMarkets') + if fetchMarketsOptions is not None: + types = self.safe_list(fetchMarketsOptions, 'types', defaultTypes) + else: + # for backward-compatibility + types = self.safe_list(self.options, 'fetchMarkets', defaultTypes) + for i in range(0, len(types)): + marketType = types[i] + if marketType == 'spot': + promisesUnresolved.append(self.fetch_spot_markets(params)) + elif marketType == 'linear': + promisesUnresolved.append(self.fetch_future_markets({'category': 'linear'})) + elif marketType == 'inverse': + promisesUnresolved.append(self.fetch_future_markets({'category': 'inverse'})) + elif marketType == 'option': + promisesUnresolved.append(self.fetch_option_markets({'baseCoin': 'BTC'})) + promisesUnresolved.append(self.fetch_option_markets({'baseCoin': 'ETH'})) + promisesUnresolved.append(self.fetch_option_markets({'baseCoin': 'SOL'})) + else: + raise ExchangeError(self.id + ' fetchMarkets() self.options fetchMarkets "' + marketType + '" is not a supported market type') + promises = promisesUnresolved + spotMarkets = self.safe_list(promises, 0, []) + linearMarkets = self.safe_list(promises, 1, []) + inverseMarkets = self.safe_list(promises, 2, []) + btcOptionMarkets = self.safe_list(promises, 3, []) + ethOptionMarkets = self.safe_list(promises, 4, []) + solOptionMarkets = self.safe_list(promises, 5, []) + futureMarkets = self.array_concat(linearMarkets, inverseMarkets) + optionMarkets = self.array_concat(btcOptionMarkets, ethOptionMarkets) + optionMarkets = self.array_concat(optionMarkets, solOptionMarkets) + derivativeMarkets = self.array_concat(futureMarkets, optionMarkets) + return self.array_concat(spotMarkets, derivativeMarkets) + + def fetch_spot_markets(self, params) -> List[Market]: + request: dict = { + 'category': 'spot', + } + usePrivateInstrumentsInfo = self.safe_bool(self.options, 'usePrivateInstrumentsInfo', False) + response: dict = None + if usePrivateInstrumentsInfo: + response = self.privateGetV5MarketInstrumentsInfo(self.extend(request, params)) + else: + response = self.publicGetV5MarketInstrumentsInfo(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "category": "spot", + # "list": [ + # { + # "symbol": "BTCUSDT", + # "baseCoin": "BTC", + # "quoteCoin": "USDT", + # "innovation": "0", + # "status": "Trading", + # "marginTrading": "both", + # "lotSizeFilter": { + # "basePrecision": "0.000001", + # "quotePrecision": "0.00000001", + # "minOrderQty": "0.00004", + # "maxOrderQty": "63.01197227", + # "minOrderAmt": "1", + # "maxOrderAmt": "100000" + # }, + # "priceFilter": { + # "tickSize": "0.01" + # } + # } + # ] + # }, + # "retExtInfo": {}, + # "time": 1672712468011 + # } + # + responseResult = self.safe_dict(response, 'result', {}) + markets = self.safe_list(responseResult, 'list', []) + result = [] + takerFee = self.parse_number('0.001') + makerFee = self.parse_number('0.001') + for i in range(0, len(markets)): + market = markets[i] + id = self.safe_string(market, 'symbol') + baseId = self.safe_string(market, 'baseCoin') + quoteId = self.safe_string(market, 'quoteCoin') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + symbol = base + '/' + quote + status = self.safe_string(market, 'status') + active = (status == 'Trading') + lotSizeFilter = self.safe_dict(market, 'lotSizeFilter') + priceFilter = self.safe_dict(market, 'priceFilter') + quotePrecision = self.safe_number(lotSizeFilter, 'quotePrecision') + marginTrading = self.safe_string(market, 'marginTrading', 'none') + allowsMargin = marginTrading != 'none' + result.append(self.safe_market_structure({ + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': allowsMargin, + 'swap': False, + 'future': False, + 'option': False, + 'active': active, + 'contract': False, + 'linear': None, + 'inverse': None, + 'taker': takerFee, + 'maker': makerFee, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.safe_number(lotSizeFilter, 'basePrecision'), + 'price': self.safe_number(priceFilter, 'tickSize', quotePrecision), + }, + 'limits': { + 'leverage': { + 'min': self.parse_number('1'), + 'max': None, + }, + 'amount': { + 'min': self.safe_number(lotSizeFilter, 'minOrderQty'), + 'max': self.safe_number(lotSizeFilter, 'maxOrderQty'), + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': self.safe_number(lotSizeFilter, 'minOrderAmt'), + 'max': self.safe_number(lotSizeFilter, 'maxOrderAmt'), + }, + }, + 'created': None, + 'info': market, + })) + return result + + def fetch_future_markets(self, params) -> List[Market]: + params = self.extend(params) + params['limit'] = 1000 # minimize number of requests + preLaunchMarkets = [] + usePrivateInstrumentsInfo = self.safe_bool(self.options, 'usePrivateInstrumentsInfo', False) + response: dict = None + if usePrivateInstrumentsInfo: + response = self.privateGetV5MarketInstrumentsInfo(params) + else: + linearPromises = [ + self.publicGetV5MarketInstrumentsInfo(params), + self.publicGetV5MarketInstrumentsInfo(self.extend(params, {'status': 'PreLaunch'})), + ] + promises = linearPromises + response = self.safe_dict(promises, 0, {}) + preLaunchMarkets = self.safe_dict(promises, 1, {}) + data = self.safe_dict(response, 'result', {}) + markets = self.safe_list(data, 'list', []) + paginationCursor = self.safe_string(data, 'nextPageCursor') + if paginationCursor is not None: + while(paginationCursor is not None): + params['cursor'] = paginationCursor + responseInner: dict = None + if usePrivateInstrumentsInfo: + responseInner = self.privateGetV5MarketInstrumentsInfo(params) + else: + responseInner = self.publicGetV5MarketInstrumentsInfo(params) + dataNew = self.safe_dict(responseInner, 'result', {}) + rawMarkets = self.safe_list(dataNew, 'list', []) + rawMarketsLength = len(rawMarkets) + if rawMarketsLength == 0: + break + markets = self.array_concat(rawMarkets, markets) + paginationCursor = self.safe_string(dataNew, 'nextPageCursor') + # + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "category": "linear", + # "list": [ + # { + # "symbol": "BTCUSDT", + # "contractType": "LinearPerpetual", + # "status": "Trading", + # "baseCoin": "BTC", + # "quoteCoin": "USDT", + # "launchTime": "1585526400000", + # "deliveryTime": "0", + # "deliveryFeeRate": "", + # "priceScale": "2", + # "leverageFilter": { + # "minLeverage": "1", + # "maxLeverage": "100.00", + # "leverageStep": "0.01" + # }, + # "priceFilter": { + # "minPrice": "0.50", + # "maxPrice": "999999.00", + # "tickSize": "0.50" + # }, + # "lotSizeFilter": { + # "maxOrderQty": "100.000", + # "minOrderQty": "0.001", + # "qtyStep": "0.001", + # "postOnlyMaxOrderQty": "1000.000" + # }, + # "unifiedMarginTrade": True, + # "fundingInterval": 480, + # "settleCoin": "USDT" + # } + # ], + # "nextPageCursor": "" + # }, + # "retExtInfo": {}, + # "time": 1672712495660 + # } + # + preLaunchData = self.safe_dict(preLaunchMarkets, 'result', {}) + preLaunchMarketsList = self.safe_list(preLaunchData, 'list', []) + markets = self.array_concat(markets, preLaunchMarketsList) + result = [] + category = self.safe_string(data, 'category') + for i in range(0, len(markets)): + market = markets[i] + if category is None: + category = self.safe_string(market, 'category') + linear = (category == 'linear') + inverse = (category == 'inverse') + contractType = self.safe_string(market, 'contractType') + inverseFutures = (contractType == 'InverseFutures') + linearFutures = (contractType == 'LinearFutures') + linearPerpetual = (contractType == 'LinearPerpetual') + inversePerpetual = (contractType == 'InversePerpetual') + id = self.safe_string(market, 'symbol') + baseId = self.safe_string(market, 'baseCoin') + quoteId = self.safe_string(market, 'quoteCoin') + defaultSettledId = quoteId if linear else baseId + settleId = self.safe_string(market, 'settleCoin', defaultSettledId) + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + settle = None + if linearPerpetual and (settleId == 'USD'): + settle = 'USDC' + else: + settle = self.safe_currency_code(settleId) + symbol = base + '/' + quote + lotSizeFilter = self.safe_dict(market, 'lotSizeFilter', {}) + priceFilter = self.safe_dict(market, 'priceFilter', {}) + leverage = self.safe_dict(market, 'leverageFilter', {}) + status = self.safe_string(market, 'status') + swap = linearPerpetual or inversePerpetual + future = inverseFutures or linearFutures + type = None + if swap: + type = 'swap' + elif future: + type = 'future' + expiry = None + # some swaps have deliveryTime meaning delisting time + if not swap: + expiry = self.omit_zero(self.safe_string(market, 'deliveryTime')) + if expiry is not None: + expiry = int(expiry) + expiryDatetime = self.iso8601(expiry) + symbol = symbol + ':' + settle + if expiry is not None: + symbol = symbol + '-' + self.yymmdd(expiry) + contractSize = self.safe_number_2(lotSizeFilter, 'minTradingQty', 'minOrderQty') if inverse else self.parse_number('1') + result.append(self.safe_market_structure({ + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': type, + 'spot': False, + 'margin': None, + 'swap': swap, + 'future': future, + 'option': False, + 'active': (status == 'Trading'), + 'contract': True, + 'linear': linear, + 'inverse': inverse, + 'taker': self.safe_number(market, 'takerFee', self.parse_number('0.0006')), + 'maker': self.safe_number(market, 'makerFee', self.parse_number('0.0001')), + 'contractSize': contractSize, + 'expiry': expiry, + 'expiryDatetime': expiryDatetime, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.safe_number(lotSizeFilter, 'qtyStep'), + 'price': self.safe_number(priceFilter, 'tickSize'), + }, + 'limits': { + 'leverage': { + 'min': self.safe_number(leverage, 'minLeverage'), + 'max': self.safe_number(leverage, 'maxLeverage'), + }, + 'amount': { + 'min': self.safe_number_2(lotSizeFilter, 'minTradingQty', 'minOrderQty'), + 'max': self.safe_number_2(lotSizeFilter, 'maxTradingQty', 'maxOrderQty'), + }, + 'price': { + 'min': self.safe_number(priceFilter, 'minPrice'), + 'max': self.safe_number(priceFilter, 'maxPrice'), + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': self.safe_integer(market, 'launchTime'), + 'info': market, + })) + return result + + def fetch_option_markets(self, params) -> List[Market]: + request: dict = { + 'category': 'option', + } + usePrivateInstrumentsInfo = self.safe_bool(self.options, 'usePrivateInstrumentsInfo', False) + response: dict = None + if usePrivateInstrumentsInfo: + response = self.privateGetV5MarketInstrumentsInfo(self.extend(request, params)) + else: + response = self.publicGetV5MarketInstrumentsInfo(self.extend(request, params)) + data = self.safe_dict(response, 'result', {}) + markets = self.safe_list(data, 'list', []) + if self.options['loadAllOptions']: + request['limit'] = 1000 + paginationCursor = self.safe_string(data, 'nextPageCursor') + if paginationCursor is not None: + while(paginationCursor is not None): + request['cursor'] = paginationCursor + responseInner: dict = None + if usePrivateInstrumentsInfo: + responseInner = self.privateGetV5MarketInstrumentsInfo(self.extend(request, params)) + else: + responseInner = self.publicGetV5MarketInstrumentsInfo(self.extend(request, params)) + dataNew = self.safe_dict(responseInner, 'result', {}) + rawMarkets = self.safe_list(dataNew, 'list', []) + rawMarketsLength = len(rawMarkets) + if rawMarketsLength == 0: + break + markets = self.array_concat(rawMarkets, markets) + paginationCursor = self.safe_string(dataNew, 'nextPageCursor') + # + # { + # "retCode": 0, + # "retMsg": "success", + # "result": { + # "category": "option", + # "nextPageCursor": "0%2C2", + # "list": [ + # { + # "symbol": "BTC-29DEC23-80000-C", + # "status": "Trading", + # "baseCoin": "BTC", + # "quoteCoin": "USD", + # "settleCoin": "USDC", + # "optionsType": "Call", + # "launchTime": "1688630400000", + # "deliveryTime": "1703836800000", + # "deliveryFeeRate": "0.00015", + # "priceFilter": { + # "minPrice": "5", + # "maxPrice": "10000000", + # "tickSize": "5" + # }, + # "lotSizeFilter": { + # "maxOrderQty": "500", + # "minOrderQty": "0.01", + # "qtyStep": "0.01" + # } + # }, + # ] + # }, + # "retExtInfo": {}, + # "time": 1688873094448 + # } + # + result = [] + for i in range(0, len(markets)): + market = markets[i] + id = self.safe_string(market, 'symbol') + baseId = self.safe_string(market, 'baseCoin') + quoteId = self.safe_string(market, 'quoteCoin') + settleId = self.safe_string(market, 'settleCoin') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + settle = self.safe_currency_code(settleId) + lotSizeFilter = self.safe_dict(market, 'lotSizeFilter', {}) + priceFilter = self.safe_dict(market, 'priceFilter', {}) + status = self.safe_string(market, 'status') + expiry = self.safe_integer(market, 'deliveryTime') + splitId = id.split('-') + strike = self.safe_string(splitId, 2) + optionLetter = self.safe_string(splitId, 3) + isActive = (status == 'Trading') + isInverse = base == settle + if isActive or (self.options['loadAllOptions']) or (self.options['loadExpiredOptions']): + result.append(self.safe_market_structure({ + 'id': id, + 'symbol': base + '/' + quote + ':' + settle + '-' + self.yymmdd(expiry) + '-' + strike + '-' + optionLetter, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': 'option', + 'subType': None, + 'spot': False, + 'margin': False, + 'swap': False, + 'future': False, + 'option': True, + 'active': isActive, + 'contract': True, + 'linear': not isInverse, + 'inverse': isInverse, + 'taker': self.safe_number(market, 'takerFee', self.parse_number('0.0006')), + 'maker': self.safe_number(market, 'makerFee', self.parse_number('0.0001')), + 'contractSize': self.parse_number('1'), + 'expiry': expiry, + 'expiryDatetime': self.iso8601(expiry), + 'strike': self.parse_number(strike), + 'optionType': self.safe_string_lower(market, 'optionsType'), + 'precision': { + 'amount': self.safe_number(lotSizeFilter, 'qtyStep'), + 'price': self.safe_number(priceFilter, 'tickSize'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(lotSizeFilter, 'minOrderQty'), + 'max': self.safe_number(lotSizeFilter, 'maxOrderQty'), + }, + 'price': { + 'min': self.safe_number(priceFilter, 'minPrice'), + 'max': self.safe_number(priceFilter, 'maxPrice'), + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': self.safe_integer(market, 'launchTime'), + 'info': market, + })) + return result + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # spot + # + # { + # "symbol": "BTCUSDT", + # "bid1Price": "20517.96", + # "bid1Size": "2", + # "ask1Price": "20527.77", + # "ask1Size": "1.862172", + # "lastPrice": "20533.13", + # "prevPrice24h": "20393.48", + # "price24hPcnt": "0.0068", + # "highPrice24h": "21128.12", + # "lowPrice24h": "20318.89", + # "turnover24h": "243765620.65899866", + # "volume24h": "11801.27771", + # "usdIndexPrice": "20784.12009279" + # } + # + # linear/inverse + # + # { + # "symbol": "BTCUSD", + # "lastPrice": "16597.00", + # "indexPrice": "16598.54", + # "markPrice": "16596.00", + # "prevPrice24h": "16464.50", + # "price24hPcnt": "0.008047", + # "highPrice24h": "30912.50", + # "lowPrice24h": "15700.00", + # "prevPrice1h": "16595.50", + # "openInterest": "373504107", + # "openInterestValue": "22505.67", + # "turnover24h": "2352.94950046", + # "volume24h": "49337318", + # "fundingRate": "-0.001034", + # "nextFundingTime": "1672387200000", + # "predictedDeliveryPrice": "", + # "basisRate": "", + # "deliveryFeeRate": "", + # "deliveryTime": "0", + # "ask1Size": "1", + # "bid1Price": "16596.00", + # "ask1Price": "16597.50", + # "bid1Size": "1" + # } + # + # option + # + # { + # "symbol": "BTC-30DEC22-18000-C", + # "bid1Price": "0", + # "bid1Size": "0", + # "bid1Iv": "0", + # "ask1Price": "435", + # "ask1Size": "0.66", + # "ask1Iv": "5", + # "lastPrice": "435", + # "highPrice24h": "435", + # "lowPrice24h": "165", + # "markPrice": "0.00000009", + # "indexPrice": "16600.55", + # "markIv": "0.7567", + # "underlyingPrice": "16590.42", + # "openInterest": "6.3", + # "turnover24h": "2482.73", + # "volume24h": "0.15", + # "totalVolume": "99", + # "totalTurnover": "1967653", + # "delta": "0.00000001", + # "gamma": "0.00000001", + # "vega": "0.00000004", + # "theta": "-0.00000152", + # "predictedDeliveryPrice": "0", + # "change24h": "86" + # } + # + isSpot = self.safe_string(ticker, 'openInterestValue') is None + timestamp = self.safe_integer(ticker, 'time') + marketId = self.safe_string(ticker, 'symbol') + type = 'spot' if isSpot else 'contract' + market = self.safe_market(marketId, market, None, type) + symbol = self.safe_symbol(marketId, market, None, type) + last = self.safe_string(ticker, 'lastPrice') + open = self.safe_string(ticker, 'prevPrice24h') + percentage = self.safe_string(ticker, 'price24hPcnt') + percentage = Precise.string_mul(percentage, '100') + quoteVolume = self.safe_string(ticker, 'turnover24h') + baseVolume = self.safe_string(ticker, 'volume24h') + bid = self.safe_string(ticker, 'bid1Price') + ask = self.safe_string(ticker, 'ask1Price') + high = self.safe_string(ticker, 'highPrice24h') + low = self.safe_string(ticker, 'lowPrice24h') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': high, + 'low': low, + 'bid': bid, + 'bidVolume': self.safe_string_2(ticker, 'bidSize', 'bid1Size'), + 'ask': ask, + 'askVolume': self.safe_string_2(ticker, 'askSize', 'ask1Size'), + 'vwap': None, + 'open': open, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': percentage, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'markPrice': self.safe_string(ticker, 'markPrice'), + 'indexPrice': self.safe_string(ticker, 'indexPrice'), + 'info': ticker, + }, market) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://bybit-exchange.github.io/docs/v5/market/tickers + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchTicker() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + # 'baseCoin': '', Base coin. For option only + # 'expDate': '', Expiry date. e.g., 25DEC22. For option only + } + category = None + category, params = self.get_bybit_type('fetchTicker', market, params) + request['category'] = category + response = self.publicGetV5MarketTickers(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "category": "inverse", + # "list": [ + # { + # "symbol": "BTCUSD", + # "lastPrice": "16597.00", + # "indexPrice": "16598.54", + # "markPrice": "16596.00", + # "prevPrice24h": "16464.50", + # "price24hPcnt": "0.008047", + # "highPrice24h": "30912.50", + # "lowPrice24h": "15700.00", + # "prevPrice1h": "16595.50", + # "openInterest": "373504107", + # "openInterestValue": "22505.67", + # "turnover24h": "2352.94950046", + # "volume24h": "49337318", + # "fundingRate": "-0.001034", + # "nextFundingTime": "1672387200000", + # "predictedDeliveryPrice": "", + # "basisRate": "", + # "deliveryFeeRate": "", + # "deliveryTime": "0", + # "ask1Size": "1", + # "bid1Price": "16596.00", + # "ask1Price": "16597.50", + # "bid1Size": "1" + # } + # ] + # }, + # "retExtInfo": {}, + # "time": 1672376496682 + # } + # + result = self.safe_dict(response, 'result', {}) + tickers = self.safe_list(result, 'list', []) + rawTicker = self.safe_dict(tickers, 0) + return self.parse_ticker(rawTicker, market) + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://bybit-exchange.github.io/docs/v5/market/tickers + + :param str[] symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.subType]: *contract only* 'linear', 'inverse' + :param str [params.baseCoin]: *option only* base coin, default is 'BTC' + :returns dict: an array of `ticker structures ` + """ + self.load_markets() + code = self.safe_string_n(params, ['code', 'currency', 'baseCoin']) + market = None + parsedSymbols = None + if symbols is not None: + parsedSymbols = [] + marketTypeInfo = self.handle_market_type_and_params('fetchTickers', None, params) + defaultType = marketTypeInfo[0] # don't omit here + # we can't use marketSymbols here due to the conflicing ids between markets + currentType = None + for i in range(0, len(symbols)): + symbol = symbols[i] + # using safeMarket here because if the user provides for instance BTCUSDT and "type": "spot" in params we should + # infer the market type from the type provided and not from the conflicting id(BTCUSDT might be swap or spot) + isExchangeSpecificSymbol = (symbol.find('/') == -1) + if isExchangeSpecificSymbol: + market = self.safe_market(symbol, None, None, defaultType) + else: + market = self.market(symbol) + if currentType is None: + currentType = market['type'] + elif market['type'] != currentType: + raise BadRequest(self.id + ' fetchTickers can only accept a list of symbols of the same type') + if market['option']: + if code is not None and code != market['base']: + raise BadRequest(self.id + ' fetchTickers the base currency must be the same for all symbols, self endpoint only supports one base currency at a time. Read more about it here: https://bybit-exchange.github.io/docs/v5/market/tickers') + if code is None: + code = market['base'] + params = self.omit(params, ['code', 'currency']) + parsedSymbols.append(market['symbol']) + request: dict = { + # 'symbol': market['id'], + # 'baseCoin': '', # Base coin. For option only + # 'expDate': '', # Expiry date. e.g., 25DEC22. For option only + } + category = None + category, params = self.get_bybit_type('fetchTickers', market, params) + request['category'] = category + if category == 'option': + request['category'] = 'option' + if code is None: + code = 'BTC' + request['baseCoin'] = code + response = self.publicGetV5MarketTickers(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "category": "inverse", + # "list": [ + # { + # "symbol": "BTCUSD", + # "lastPrice": "16597.00", + # "indexPrice": "16598.54", + # "markPrice": "16596.00", + # "prevPrice24h": "16464.50", + # "price24hPcnt": "0.008047", + # "highPrice24h": "30912.50", + # "lowPrice24h": "15700.00", + # "prevPrice1h": "16595.50", + # "openInterest": "373504107", + # "openInterestValue": "22505.67", + # "turnover24h": "2352.94950046", + # "volume24h": "49337318", + # "fundingRate": "-0.001034", + # "nextFundingTime": "1672387200000", + # "predictedDeliveryPrice": "", + # "basisRate": "", + # "deliveryFeeRate": "", + # "deliveryTime": "0", + # "ask1Size": "1", + # "bid1Price": "16596.00", + # "ask1Price": "16597.50", + # "bid1Size": "1" + # } + # ] + # }, + # "retExtInfo": {}, + # "time": 1672376496682 + # } + # + result = self.safe_dict(response, 'result', {}) + tickerList = self.safe_list(result, 'list', []) + return self.parse_tickers(tickerList, parsedSymbols) + + def fetch_bids_asks(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches the bid and ask price and volume for multiple markets + + https://bybit-exchange.github.io/docs/v5/market/tickers + + :param str[]|None symbols: unified symbols of the markets to fetch the bids and asks for, all markets are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.subType]: *contract only* 'linear', 'inverse' + :param str [params.baseCoin]: *option only* base coin, default is 'BTC' + :returns dict: a dictionary of `ticker structures ` + """ + return self.fetch_tickers(symbols, params) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # [ + # "1621162800", + # "49592.43", + # "49644.91", + # "49342.37", + # "49349.42", + # "1451.59", + # "2.4343353100000003" + # ] + # + volumeIndex = 6 if (market['inverse']) else 5 + return [ + self.safe_integer(ohlcv, 0), + self.safe_number(ohlcv, 1), + self.safe_number(ohlcv, 2), + self.safe_number(ohlcv, 3), + self.safe_number(ohlcv, 4), + self.safe_number(ohlcv, volumeIndex), + ] + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://bybit-exchange.github.io/docs/v5/market/kline + https://bybit-exchange.github.io/docs/v5/market/mark-kline + https://bybit-exchange.github.io/docs/v5/market/index-kline + https://bybit-exchange.github.io/docs/v5/market/preimum-index-kline + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch orders for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOHLCV() requires a symbol argument') + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate') + if paginate: + return self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params, 1000) + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is None: + limit = 200 # default is 200 when requested with `since` + if since is not None: + request['start'] = since + if limit is not None: + request['limit'] = limit # max 1000, default 1000 + request, params = self.handle_until_option('end', request, params) + request['interval'] = self.safe_string(self.timeframes, timeframe, timeframe) + response = None + if market['spot']: + request['category'] = 'spot' + response = self.publicGetV5MarketKline(self.extend(request, params)) + else: + price = self.safe_string(params, 'price') + params = self.omit(params, 'price') + if market['linear']: + request['category'] = 'linear' + elif market['inverse']: + request['category'] = 'inverse' + else: + raise NotSupported(self.id + ' fetchOHLCV() is not supported for option markets') + if price == 'mark': + response = self.publicGetV5MarketMarkPriceKline(self.extend(request, params)) + elif price == 'index': + response = self.publicGetV5MarketIndexPriceKline(self.extend(request, params)) + elif price == 'premiumIndex': + response = self.publicGetV5MarketPremiumIndexPriceKline(self.extend(request, params)) + else: + response = self.publicGetV5MarketKline(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "symbol": "BTCUSD", + # "category": "inverse", + # "list": [ + # [ + # "1670608800000", + # "17071", + # "17073", + # "17027", + # "17055.5", + # "268611", + # "15.74462667" + # ], + # [ + # "1670605200000", + # "17071.5", + # "17071.5", + # "17061", + # "17071", + # "4177", + # "0.24469757" + # ], + # [ + # "1670601600000", + # "17086.5", + # "17088", + # "16978", + # "17071.5", + # "6356", + # "0.37288112" + # ] + # ] + # }, + # "retExtInfo": {}, + # "time": 1672025956592 + # } + # + result = self.safe_dict(response, 'result', {}) + ohlcvs = self.safe_list(result, 'list', []) + return self.parse_ohlcvs(ohlcvs, market, timeframe, since, limit) + + def parse_funding_rate(self, ticker, market: Market = None) -> FundingRate: + # + # { + # "symbol": "BTCUSDT", + # "bidPrice": "19255", + # "askPrice": "19255.5", + # "lastPrice": "19255.50", + # "lastTickDirection": "ZeroPlusTick", + # "prevPrice24h": "18634.50", + # "price24hPcnt": "0.033325", + # "highPrice24h": "19675.00", + # "lowPrice24h": "18610.00", + # "prevPrice1h": "19278.00", + # "markPrice": "19255.00", + # "indexPrice": "19260.68", + # "openInterest": "48069.549", + # "turnover24h": "4686694853.047006", + # "volume24h": "243730.252", + # "fundingRate": "0.0001", + # "nextFundingTime": "1663689600000", + # "predictedDeliveryPrice": "", + # "basisRate": "", + # "deliveryFeeRate": "", + # "deliveryTime": "0" + # } + # + timestamp = self.safe_integer(ticker, 'timestamp') # added artificially to avoid changing the signature + ticker = self.omit(ticker, 'timestamp') + marketId = self.safe_string(ticker, 'symbol') + symbol = self.safe_symbol(marketId, market, None, 'swap') + fundingRate = self.safe_number(ticker, 'fundingRate') + fundingTimestamp = self.safe_integer(ticker, 'nextFundingTime') + markPrice = self.safe_number(ticker, 'markPrice') + indexPrice = self.safe_number(ticker, 'indexPrice') + info = self.safe_dict(self.safe_market(marketId, market, None, 'swap'), 'info') + fundingInterval = self.safe_integer(info, 'fundingInterval') + intervalString = None + if fundingInterval is not None: + interval = self.parse_to_int(fundingInterval / 60) + intervalString = str(interval) + 'h' + return { + 'info': ticker, + 'symbol': symbol, + 'markPrice': markPrice, + 'indexPrice': indexPrice, + 'interestRate': None, + 'estimatedSettlePrice': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'fundingRate': fundingRate, + 'fundingTimestamp': fundingTimestamp, + 'fundingDatetime': self.iso8601(fundingTimestamp), + 'nextFundingRate': None, + 'nextFundingTimestamp': None, + 'nextFundingDatetime': None, + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': intervalString, + } + + def fetch_funding_rates(self, symbols: Strings = None, params={}) -> FundingRates: + """ + fetches funding rates for multiple markets + + https://bybit-exchange.github.io/docs/v5/market/tickers + + :param str[] symbols: unified symbols of the markets to fetch the funding rates for, all market funding rates are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `funding rate structures ` + """ + self.load_markets() + market = None + request: dict = {} + if symbols is not None: + symbols = self.market_symbols(symbols) + market = self.market(symbols[0]) + symbolsLength = len(symbols) + if symbolsLength == 1: + request['symbol'] = market['id'] + type = None + type, params = self.handle_market_type_and_params('fetchFundingRates', market, params) + if type != 'swap': + raise NotSupported(self.id + ' fetchFundingRates() does not support ' + type + ' markets') + else: + subType = None + subType, params = self.handle_sub_type_and_params('fetchFundingRates', market, params, 'linear') + request['category'] = subType + response = self.publicGetV5MarketTickers(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "category": "linear", + # "list": [ + # { + # "symbol": "BTCUSDT", + # "bidPrice": "19255", + # "askPrice": "19255.5", + # "lastPrice": "19255.50", + # "lastTickDirection": "ZeroPlusTick", + # "prevPrice24h": "18634.50", + # "price24hPcnt": "0.033325", + # "highPrice24h": "19675.00", + # "lowPrice24h": "18610.00", + # "prevPrice1h": "19278.00", + # "markPrice": "19255.00", + # "indexPrice": "19260.68", + # "openInterest": "48069.549", + # "turnover24h": "4686694853.047006", + # "volume24h": "243730.252", + # "fundingRate": "0.0001", + # "nextFundingTime": "1663689600000", + # "predictedDeliveryPrice": "", + # "basisRate": "", + # "deliveryFeeRate": "", + # "deliveryTime": "0" + # } + # ] + # }, + # "retExtInfo": null, + # "time": 1663670053454 + # } + # + data = self.safe_dict(response, 'result', {}) + tickerList = self.safe_list(data, 'list', []) + timestamp = self.safe_integer(response, 'time') + for i in range(0, len(tickerList)): + tickerList[i]['timestamp'] = timestamp # will be removed inside the parser + return self.parse_funding_rates(tickerList, symbols) + + def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical funding rate prices + + https://bybit-exchange.github.io/docs/v5/market/history-fund-rate + + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: timestamp in ms of the earliest funding rate to fetch + :param int [limit]: the maximum amount of `funding rate structures ` to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest funding rate + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `funding rate structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchFundingRateHistory() requires a symbol argument') + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchFundingRateHistory', 'paginate') + if paginate: + return self.fetch_paginated_call_deterministic('fetchFundingRateHistory', symbol, since, limit, '8h', params, 200) + if limit is None: + limit = 200 + request: dict = { + # 'category': '', # Product type. linear,inverse + # 'symbol': '', # Symbol name + # 'startTime': 0, # The start timestamp(ms) + # 'endTime': 0, # The end timestamp(ms) + 'limit': limit, # Limit for data size per page. [1, 200]. Default: 200 + } + market = self.market(symbol) + symbol = market['symbol'] + request['symbol'] = market['id'] + type = None + type, params = self.get_bybit_type('fetchFundingRateHistory', market, params) + if type == 'spot' or type == 'option': + raise NotSupported(self.id + ' fetchFundingRateHistory() only support linear and inverse market') + request['category'] = type + if since is not None: + request['startTime'] = since + until = self.safe_integer(params, 'until') # unified in milliseconds + endTime = self.safe_integer(params, 'endTime', until) # exchange-specific in milliseconds + params = self.omit(params, ['endTime', 'until']) + if endTime is not None: + request['endTime'] = endTime + else: + if since is not None: + # end time is required when since is not empty + fundingInterval = 60 * 60 * 8 * 1000 + request['endTime'] = since + limit * fundingInterval + response = self.publicGetV5MarketFundingHistory(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "category": "linear", + # "list": [ + # { + # "symbol": "ETHPERP", + # "fundingRate": "0.0001", + # "fundingRateTimestamp": "1672041600000" + # } + # ] + # }, + # "retExtInfo": {}, + # "time": 1672051897447 + # } + # + rates = [] + result = self.safe_dict(response, 'result') + resultList = self.safe_list(result, 'list') + for i in range(0, len(resultList)): + entry = resultList[i] + timestamp = self.safe_integer(entry, 'fundingRateTimestamp') + rates.append({ + 'info': entry, + 'symbol': self.safe_symbol(self.safe_string(entry, 'symbol'), None, None, 'swap'), + 'fundingRate': self.safe_number(entry, 'fundingRate'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + sorted = self.sort_by(rates, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, symbol, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # public https://bybit-exchange.github.io/docs/v5/market/recent-trade + # + # { + # "execId": "666042b4-50c6-58f3-bd9c-89b2088663ff", + # "symbol": "ETHUSD", + # "price": "1162.95", + # "size": "1", + # "side": "Sell", + # "time": "1669191277315", + # "isBlockTrade": False + # } + # + # private trades classic spot https://bybit-exchange.github.io/docs/v5/position/execution + # + # { + # "symbol": "QNTUSDT", + # "orderId": "1538686353240339712", + # "orderLinkId": "", + # "side": "Sell", + # "orderPrice": "", + # "orderQty": "", + # "leavesQty": "", + # "orderType": "Limit", + # "stopOrderType": "", + # "execFee": "0.040919", + # "execId": "2210000000097330907", + # "execPrice": "98.6", + # "execQty": "0.415", + # "execType": "", + # "execValue": "", + # "execTime": "1698161716634", + # "isMaker": True, + # "feeRate": "", + # "tradeIv": "", + # "markIv": "", + # "markPrice": "", + # "indexPrice": "", + # "underlyingPrice": "", + # "blockTradeId": "" + # } + # + # private trades unified https://bybit-exchange.github.io/docs/v5/position/execution + # + # { + # "symbol": "QNTUSDT", + # "orderType": "Limit", + # "underlyingPrice": "", + # "orderLinkId": "1549452573428424449", + # "orderId": "1549452573428424448", + # "stopOrderType": "", + # "execTime": "1699445151998", + # "feeRate": "0.00025", + # "tradeIv": "", + # "blockTradeId": "", + # "markPrice": "", + # "execPrice": "102.8", + # "markIv": "", + # "orderQty": "3.652", + # "orderPrice": "102.8", + # "execValue": "1.028", + # "closedSize": "", + # "execType": "Trade", + # "seq": "19157444346", + # "side": "Buy", + # "indexPrice": "", + # "leavesQty": "3.642", + # "isMaker": True, + # "execFee": "0.0000025", + # "execId": "2210000000101610464", + # "execQty": "0.01", + # "nextPageCursor": "267951%3A0%2C38567%3A0" + # }, + # + # private USDC settled trades + # + # { + # "symbol": "ETHPERP", + # "orderLinkId": "", + # "side": "Buy", + # "orderId": "aad0ee44-ce12-4112-aeee-b7829f6c3a26", + # "execFee": "0.0210", + # "feeRate": "0.000600", + # "blockTradeId": "", + # "tradeTime": "1669196417930", + # "execPrice": "1162.15", + # "lastLiquidityInd": "TAKER", + # "execValue": "34.8645", + # "execType": "Trade", + # "execQty": "0.030", + # "tradeId": "0e94eaf5-b08e-5505-b43f-7f1f30b1ca80" + # } + # + # watchMyTrades execution.fast + # + # { + # "category": "linear", + # "symbol": "ICPUSDT", + # "execId": "3510f361-0add-5c7b-a2e7-9679810944fc", + # "execPrice": "12.015", + # "execQty": "3000", + # "orderId": "443d63fa-b4c3-4297-b7b1-23bca88b04dc", + # "isMaker": False, + # "orderLinkId": "test-00001", + # "side": "Sell", + # "execTime": "1716800399334", + # "seq": 34771365464 + # } + # + # watchMyTrades execution + # + # { + # "category": "linear", + # "symbol": "BTCUSDT", + # "closedSize": "0", + # "execFee": "0.0679239", + # "execId": "135dbae5-cbed-5275-9290-3956bb2ed907", + # "execPrice": "123498", + # "execQty": "0.001", + # "execType": "Trade", + # "execValue": "123.498", + # "feeRate": "0.00055", + # "tradeIv": "", + # "markIv": "", + # "blockTradeId": "", + # "markPrice": "122392", + # "indexPrice": "", + # "underlyingPrice": "", + # "leavesQty": "0", + # "orderId": "aee7453a-a100-465f-857a-3db780e9329a", + # "orderLinkId": "", + # "orderPrice": "123615.9", + # "orderQty": "0.001", + # "orderType": "Market", + # "stopOrderType": "UNKNOWN", + # "side": "Buy", + # "execTime": "1757837580469", + # "isLeverage": "0", + # "isMaker": False, + # "seq": 9517074055, + # "marketUnit": "", + # "execPnl": "0", + # "createType": "CreateByUser", + # "extraFees": [], + # "feeCoin": "USDT" + # } + # + id = self.safe_string_n(trade, ['execId', 'id', 'tradeId']) + marketId = self.safe_string(trade, 'symbol') + marketType = 'contract' if ('createType' in trade) else 'spot' + category = self.safe_string(trade, 'category') + if category is not None: + marketType = 'spot' if (category == 'spot') else 'contract' + if market is not None: + marketType = market['type'] + market = self.safe_market(marketId, market, None, marketType) + symbol = market['symbol'] + amountString = self.safe_string_n(trade, ['execQty', 'orderQty', 'size']) + priceString = self.safe_string_n(trade, ['execPrice', 'orderPrice', 'price']) + costString = self.safe_string(trade, 'execValue') + timestamp = self.safe_integer_n(trade, ['time', 'execTime', 'tradeTime']) + side = self.safe_string_lower(trade, 'side') + if side is None: + isBuyer = self.safe_integer(trade, 'isBuyer') + if isBuyer is not None: + side = 'buy' if isBuyer else 'sell' + isMaker = self.safe_bool(trade, 'isMaker') + takerOrMaker = None + if isMaker is not None: + takerOrMaker = 'maker' if isMaker else 'taker' + else: + lastLiquidityInd = self.safe_string(trade, 'lastLiquidityInd') + if lastLiquidityInd == 'UNKNOWN': + lastLiquidityInd = None + if lastLiquidityInd is not None: + if (lastLiquidityInd == 'TAKER') or (lastLiquidityInd == 'MAKER'): + takerOrMaker = lastLiquidityInd.lower() + else: + takerOrMaker = 'maker' if (lastLiquidityInd == 'AddedLiquidity') else 'taker' + orderType = self.safe_string_lower(trade, 'orderType') + if orderType == 'unknown': + orderType = None + feeCostString = self.safe_string(trade, 'execFee') + fee = None + if feeCostString is not None: + feeRateString = self.safe_string(trade, 'feeRate') + feeCurrencyCode = None + if market['spot']: + if Precise.string_gt(feeCostString, '0'): + if side == 'buy': + feeCurrencyCode = market['base'] + else: + feeCurrencyCode = market['quote'] + else: + if side == 'buy': + feeCurrencyCode = market['quote'] + else: + feeCurrencyCode = market['base'] + else: + feeCurrencyCode = market['base'] if market['inverse'] else market['settle'] + fee = { + 'cost': feeCostString, + 'currency': self.safe_string(trade, 'feeCoin', feeCurrencyCode), + 'rate': feeRateString, + } + return self.safe_trade({ + 'id': id, + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'order': self.safe_string(trade, 'orderId'), + 'type': orderType, + 'side': side, + 'takerOrMaker': takerOrMaker, + 'price': priceString, + 'amount': amountString, + 'cost': costString, + 'fee': fee, + }, market) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://bybit-exchange.github.io/docs/v5/market/recent-trade + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: market type, ['swap', 'option', 'spot'] + :param str [params.subType]: market subType, ['linear', 'inverse'] + :returns Trade[]: a list of `trade structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchTrades() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + # 'baseCoin': '', # Base coin. For option only. If not passed, return BTC data by default + # 'optionType': 'Call', # Option type. Call or Put. For option only + } + if limit is not None: + # spot: [1,60], default: 60. + # others: [1,1000], default: 500 + request['limit'] = limit + type = None + type, params = self.get_bybit_type('fetchTrades', market, params) + request['category'] = type + response = self.publicGetV5MarketRecentTrade(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "category": "spot", + # "list": [ + # { + # "execId": "2100000000007764263", + # "symbol": "BTCUSDT", + # "price": "16618.49", + # "size": "0.00012", + # "side": "Buy", + # "time": "1672052955758", + # "isBlockTrade": False + # } + # ] + # }, + # "retExtInfo": {}, + # "time": 1672053054358 + # } + # + result = self.safe_dict(response, 'result', {}) + trades = self.safe_list(result, 'list', []) + return self.parse_trades(trades, market, since, limit) + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://bybit-exchange.github.io/docs/v5/market/orderbook + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrderBook() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + defaultLimit = 25 + if market['spot']: + # limit: [1, 50]. Default: 1 + defaultLimit = 50 + request['category'] = 'spot' + else: + if market['option']: + # limit: [1, 25]. Default: 1 + request['category'] = 'option' + elif market['linear']: + # limit: [1, 500]. Default: 25 + request['category'] = 'linear' + elif market['inverse']: + # limit: [1, 500]. Default: 25 + request['category'] = 'inverse' + request['limit'] = limit if (limit is not None) else defaultLimit + response = self.publicGetV5MarketOrderbook(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "s": "BTCUSDT", + # "a": [ + # [ + # "16638.64", + # "0.008479" + # ] + # ], + # "b": [ + # [ + # "16638.27", + # "0.305749" + # ] + # ], + # "ts": 1672765737733, + # "u": 5277055 + # }, + # "retExtInfo": {}, + # "time": 1672765737734 + # } + # + result = self.safe_dict(response, 'result', {}) + timestamp = self.safe_integer(result, 'ts') + return self.parse_order_book(result, symbol, timestamp, 'b', 'a') + + def parse_balance(self, response) -> Balances: + # + # cross + # { + # "retCode": 0, + # "retMsg": "success", + # "result": { + # "acctBalanceSum": "0.122995614474732872", + # "debtBalanceSum": "0.011734191124529754", + # "loanAccountList": [ + # { + # "free": "0.001143855", + # "interest": "0", + # "loan": "0", + # "locked": "0", + # "tokenId": "BTC", + # "total": "0.001143855" + # }, + # { + # "free": "200.00005568", + # "interest": "0.0008391", + # "loan": "200", + # "locked": "0", + # "tokenId": "USDT", + # "total": "200.00005568" + # }, + # ], + # "riskRate": "0.0954", + # "status": 1 + # }, + # "retExtInfo": {}, + # "time": 1669843584123 + # } + # + # funding + # { + # "retCode": 0, + # "retMsg": "success", + # "result": { + # "memberId": "533285", + # "accountType": "FUND", + # "balance": [ + # { + # "coin": "USDT", + # "transferBalance": "1010", + # "walletBalance": "1010", + # "bonus": "" + # }, + # { + # "coin": "USDC", + # "transferBalance": "0", + # "walletBalance": "0", + # "bonus": "" + # } + # ] + # }, + # "retExtInfo": {}, + # "time": 1675865290069 + # } + # + # spot & swap + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "list": [ + # { + # "totalEquity": "18070.32797922", + # "accountIMRate": "0.0101", + # "totalMarginBalance": "18070.32797922", + # "totalInitialMargin": "182.60183684", + # "accountType": "UNIFIED", + # "totalAvailableBalance": "17887.72614237", + # "accountMMRate": "0", + # "totalPerpUPL": "-0.11001349", + # "totalWalletBalance": "18070.43799271", + # "accountLTV": "0.017", + # "totalMaintenanceMargin": "0.38106773", + # "coin": [ + # { + # "availableToBorrow": "2.5", + # "bonus": "0", + # "accruedInterest": "0", + # "availableToWithdraw": "0.805994", + # "totalOrderIM": "0", + # "equity": "0.805994", + # "totalPositionMM": "0", + # "usdValue": "12920.95352538", + # "unrealisedPnl": "0", + # "borrowAmount": "0", + # "totalPositionIM": "0", + # "walletBalance": "0.805994", + # "cumRealisedPnl": "0", + # "coin": "BTC" + # } + # ] + # } + # ] + # }, + # "retExtInfo": {}, + # "time": 1672125441042 + # } + # + timestamp = self.safe_integer(response, 'time') + result: dict = { + 'info': response, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + responseResult = self.safe_dict(response, 'result', {}) + currencyList = self.safe_list_n(responseResult, ['loanAccountList', 'list', 'balance']) + if currencyList is None: + # usdc wallet + code = 'USDC' + account = self.account() + account['free'] = self.safe_string(responseResult, 'availableBalance') + account['total'] = self.safe_string(responseResult, 'walletBalance') + result[code] = account + else: + for i in range(0, len(currencyList)): + entry = currencyList[i] + accountType = self.safe_string(entry, 'accountType') + if accountType == 'UNIFIED' or accountType == 'CONTRACT' or accountType == 'SPOT': + coins = self.safe_list(entry, 'coin') + for j in range(0, len(coins)): + account = self.account() + coinEntry = coins[j] + loan = self.safe_string(coinEntry, 'borrowAmount') + interest = self.safe_string(coinEntry, 'accruedInterest') + if (loan is not None) and (interest is not None): + account['debt'] = Precise.string_add(loan, interest) + account['total'] = self.safe_string(coinEntry, 'walletBalance') + free = self.safe_string_2(coinEntry, 'availableToWithdraw', 'free') + if free is not None: + account['free'] = free + else: + locked = self.safe_string(coinEntry, 'locked', '0') + totalPositionIm = self.safe_string(coinEntry, 'totalPositionIM', '0') + totalOrderIm = self.safe_string(coinEntry, 'totalOrderIM', '0') + totalUsed = Precise.string_add(locked, totalPositionIm) + totalUsed = Precise.string_add(totalUsed, totalOrderIm) + account['used'] = totalUsed + # account['used'] = self.safe_string(coinEntry, 'locked') + currencyId = self.safe_string(coinEntry, 'coin') + code = self.safe_currency_code(currencyId) + result[code] = account + else: + account = self.account() + loan = self.safe_string(entry, 'loan') + interest = self.safe_string(entry, 'interest') + if (loan is not None) and (interest is not None): + account['debt'] = Precise.string_add(loan, interest) + account['total'] = self.safe_string_2(entry, 'total', 'walletBalance') + account['free'] = self.safe_string_n(entry, ['free', 'availableBalanceWithoutConvert', 'availableBalance', 'transferBalance']) + account['used'] = self.safe_string(entry, 'locked') + currencyId = self.safe_string_n(entry, ['tokenId', 'coin', 'currencyCoin']) + code = self.safe_currency_code(currencyId) + result[code] = account + return self.safe_balance(result) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://bybit-exchange.github.io/docs/v5/spot-margin-normal/account-info + https://bybit-exchange.github.io/docs/v5/asset/all-balance + https://bybit-exchange.github.io/docs/v5/account/wallet-balance + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: wallet type, ['spot', 'swap', 'funding'] + :returns dict: a `balance structure ` + """ + self.load_markets() + request: dict = {} + enableUnifiedMargin, enableUnifiedAccount = self.is_unified_enabled() + isUnifiedAccount = (enableUnifiedMargin or enableUnifiedAccount) + type = None + # don't use getBybitType here + type, params = self.handle_market_type_and_params('fetchBalance', None, params) + subType = None + subType, params = self.handle_sub_type_and_params('fetchBalance', None, params) + if (type == 'swap') or (type == 'future'): + type = subType + lowercaseRawType = type.lower() if (type is not None) else None + isSpot = (type == 'spot') + isLinear = (type == 'linear') + isInverse = (type == 'inverse') + isFunding = (lowercaseRawType == 'fund') or (lowercaseRawType == 'funding') + if isUnifiedAccount: + unifiedMarginStatus = self.safe_integer(self.options, 'unifiedMarginStatus', 6) + if unifiedMarginStatus < 5: + # it's not uta.20 where inverse are unified + if isInverse: + type = 'contract' + else: + type = 'unified' + else: + type = 'unified' # uta.20 where inverse are unified + else: + if isLinear or isInverse: + type = 'contract' + accountTypes = self.safe_dict(self.options, 'accountsByType', {}) + unifiedType = self.safe_string_upper(accountTypes, type, type) + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchBalance', params) + response = None + if isSpot and (marginMode is not None): + response = self.privateGetV5SpotCrossMarginTradeAccount(self.extend(request, params)) + elif isFunding: + # use self endpoint only we have no other choice + # because it requires transfer permission + request['accountType'] = 'FUND' + response = self.privateGetV5AssetTransferQueryAccountCoinsBalance(self.extend(request, params)) + else: + request['accountType'] = unifiedType + response = self.privateGetV5AccountWalletBalance(self.extend(request, params)) + # + # cross + # { + # "retCode": 0, + # "retMsg": "success", + # "result": { + # "acctBalanceSum": "0.122995614474732872", + # "debtBalanceSum": "0.011734191124529754", + # "loanAccountList": [ + # { + # "free": "0.001143855", + # "interest": "0", + # "loan": "0", + # "locked": "0", + # "tokenId": "BTC", + # "total": "0.001143855" + # }, + # { + # "free": "200.00005568", + # "interest": "0.0008391", + # "loan": "200", + # "locked": "0", + # "tokenId": "USDT", + # "total": "200.00005568" + # }, + # ], + # "riskRate": "0.0954", + # "status": 1 + # }, + # "retExtInfo": {}, + # "time": 1669843584123 + # } + # + # funding + # { + # "retCode": 0, + # "retMsg": "success", + # "result": { + # "memberId": "533285", + # "accountType": "FUND", + # "balance": [ + # { + # "coin": "USDT", + # "transferBalance": "1010", + # "walletBalance": "1010", + # "bonus": "" + # }, + # { + # "coin": "USDC", + # "transferBalance": "0", + # "walletBalance": "0", + # "bonus": "" + # } + # ] + # }, + # "retExtInfo": {}, + # "time": 1675865290069 + # } + # + # spot & swap + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "list": [ + # { + # "totalEquity": "18070.32797922", + # "accountIMRate": "0.0101", + # "totalMarginBalance": "18070.32797922", + # "totalInitialMargin": "182.60183684", + # "accountType": "UNIFIED", + # "totalAvailableBalance": "17887.72614237", + # "accountMMRate": "0", + # "totalPerpUPL": "-0.11001349", + # "totalWalletBalance": "18070.43799271", + # "accountLTV": "0.017", + # "totalMaintenanceMargin": "0.38106773", + # "coin": [ + # { + # "availableToBorrow": "2.5", + # "bonus": "0", + # "accruedInterest": "0", + # "availableToWithdraw": "0.805994", + # "totalOrderIM": "0", + # "equity": "0.805994", + # "totalPositionMM": "0", + # "usdValue": "12920.95352538", + # "unrealisedPnl": "0", + # "borrowAmount": "0", + # "totalPositionIM": "0", + # "walletBalance": "0.805994", + # "cumRealisedPnl": "0", + # "coin": "BTC" + # } + # ] + # } + # ] + # }, + # "retExtInfo": {}, + # "time": 1672125441042 + # } + # + return self.parse_balance(response) + + def parse_order_status(self, status: Str): + statuses: dict = { + # v3 spot + 'NEW': 'open', + 'PARTIALLY_FILLED': 'open', + 'FILLED': 'closed', + 'CANCELED': 'canceled', + 'PENDING_CANCEL': 'open', + 'PENDING_NEW': 'open', + 'REJECTED': 'rejected', + 'PARTIALLY_FILLED_CANCELLED': 'closed', # context: https://github.com/ccxt/ccxt/issues/18685 + # v3 contract / unified margin / unified account + 'Created': 'open', + 'New': 'open', + 'Rejected': 'rejected', # order is triggered but failed upon being placed + 'PartiallyFilled': 'open', + 'PartiallyFilledCanceled': 'closed', # context: https://github.com/ccxt/ccxt/issues/18685 + 'Filled': 'closed', + 'PendingCancel': 'open', + 'Cancelled': 'canceled', + # below self line the status only pertains to conditional orders + 'Untriggered': 'open', + 'Deactivated': 'canceled', + 'Triggered': 'open', + 'Active': 'open', + } + return self.safe_string(statuses, status, status) + + def parse_time_in_force(self, timeInForce: Str): + timeInForces: dict = { + 'GoodTillCancel': 'GTC', + 'ImmediateOrCancel': 'IOC', + 'FillOrKill': 'FOK', + 'PostOnly': 'PO', + } + return self.safe_string(timeInForces, timeInForce, timeInForce) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # v1 for usdc normal account + # { + # "symbol": "BTCPERP", + # "orderType": "Market", + # "orderLinkId": "", + # "orderId": "36190ad3-de08-4b83-9ad3-56942f684b79", + # "cancelType": "UNKNOWN", + # "stopOrderType": "UNKNOWN", + # "orderStatus": "Filled", + # "updateTimeStamp": "1692769133267", + # "takeProfit": "0.0000", + # "cumExecValue": "259.6830", + # "createdAt": "1692769133261", + # "blockTradeId": "", + # "orderPnl": "", + # "price": "24674.7", + # "tpTriggerBy": "UNKNOWN", + # "timeInForce": "ImmediateOrCancel", + # "updatedAt": "1692769133267", + # "basePrice": "0.0", + # "realisedPnl": "0.0000", + # "side": "Sell", + # "triggerPrice": "0.0", + # "cumExecFee": "0.1429", + # "leavesQty": "0.000", + # "cashFlow": "", + # "slTriggerBy": "UNKNOWN", + # "iv": "", + # "closeOnTrigger": "UNKNOWN", + # "cumExecQty": "0.010", + # "reduceOnly": 0, + # "qty": "0.010", + # "stopLoss": "0.0000", + # "triggerBy": "UNKNOWN", + # "orderIM": "" + # } + # + # v5 + # { + # "symbol": "BTCUSDT", + # "orderType": "Market", + # "orderLinkId": "", + # "slLimitPrice": "0", + # "orderId": "f5f2d355-9a11-4af3-9b83-aa1d6ab6ddfe", + # "cancelType": "UNKNOWN", + # "avgPrice": "122529.9", + # "stopOrderType": "", + # "lastPriceOnCreated": "123747.9", + # "orderStatus": "Filled", + # "createType": "CreateByUser", + # "takeProfit": "", + # "cumExecValue": "122.5299", + # "tpslMode": "", + # "smpType": "None", + # "triggerDirection": 0, + # "blockTradeId": "", + # "cumFeeDetail": { + # "USDT": "0.06739145" + # }, + # "rejectReason": "EC_NoError", + # "isLeverage": "", + # "price": "120518", + # "orderIv": "", + # "createdTime": "1757837618905", + # "tpTriggerBy": "", + # "positionIdx": 0, + # "timeInForce": "IOC", + # "leavesValue": "0", + # "updatedTime": "1757837618909", + # "side": "Sell", + # "smpGroup": 0, + # "triggerPrice": "", + # "tpLimitPrice": "0", + # "cumExecFee": "0.06739145", + # "slTriggerBy": "", + # "leavesQty": "0", + # "closeOnTrigger": False, + # "slippageToleranceType": "UNKNOWN", + # "placeType": "", + # "cumExecQty": "0.001", + # "reduceOnly": True, + # "qty": "0.001", + # "stopLoss": "", + # "smpOrderId": "", + # "slippageTolerance": "0", + # "triggerBy": "", + # "extraFees": "" + # } + # + # createOrders failed order + # { + # "category": "linear", + # "symbol": "LTCUSDT", + # "orderId": '', + # "orderLinkId": '', + # "createAt": '', + # "code": "10001", + # "msg": "The number of contracts exceeds maximum limit allowed: too large" + # } + # + code = self.safe_string(order, 'code') + if code is not None: + if code != '0': + category = self.safe_string(order, 'category') + inferredMarketType = 'spot' if (category == 'spot') else 'contract' + return self.safe_order({ + 'info': order, + 'status': 'rejected', + 'id': self.safe_string(order, 'orderId'), + 'clientOrderId': self.safe_string(order, 'orderLinkId'), + 'symbol': self.safe_symbol(self.safe_string(order, 'symbol'), None, None, inferredMarketType), + }) + marketId = self.safe_string(order, 'symbol') + isContract = ('tpslMode' in order) + marketType = None + if market is not None: + marketType = market['type'] + else: + marketType = 'contract' if isContract else 'spot' + market = self.safe_market(marketId, market, None, marketType) + symbol = market['symbol'] + timestamp = self.safe_integer_2(order, 'createdTime', 'createdAt') + marketUnit = self.safe_string(order, 'marketUnit', 'baseCoin') + id = self.safe_string(order, 'orderId') + type = self.safe_string_lower(order, 'orderType') + price = self.safe_string(order, 'price') + amount: Str = None + cost: Str = None + if marketUnit == 'baseCoin': + amount = self.safe_string(order, 'qty') + cost = self.safe_string(order, 'cumExecValue') + else: + cost = self.safe_string(order, 'cumExecValue') + filled = self.safe_string(order, 'cumExecQty') + remaining = self.safe_string(order, 'leavesQty') + lastTradeTimestamp = self.safe_integer_2(order, 'updatedTime', 'updatedAt') + rawStatus = self.safe_string(order, 'orderStatus') + status = self.parse_order_status(rawStatus) + side = self.safe_string_lower(order, 'side') + fee = None + cumFeeDetail = self.safe_dict(order, 'cumFeeDetail', {}) + feeCoins = list(cumFeeDetail.keys()) + feeCoinId = self.safe_string(feeCoins, 0) + if feeCoinId is not None: + fee = { + 'cost': self.safe_number(cumFeeDetail, feeCoinId), + 'currency': feeCoinId, + } + clientOrderId = self.safe_string(order, 'orderLinkId') + if (clientOrderId is not None) and (len(clientOrderId) < 1): + clientOrderId = None + avgPrice = self.omit_zero(self.safe_string(order, 'avgPrice')) + rawTimeInForce = self.safe_string(order, 'timeInForce') + timeInForce = self.parse_time_in_force(rawTimeInForce) + triggerPrice = self.omit_zero(self.safe_string(order, 'triggerPrice')) + reduceOnly = self.safe_bool(order, 'reduceOnly') + takeProfitPrice = self.omit_zero(self.safe_string(order, 'takeProfit')) + stopLossPrice = self.omit_zero(self.safe_string(order, 'stopLoss')) + triggerDirection = self.safe_string(order, 'triggerDirection') + isAscending = (triggerDirection == '1') + isStopOrderType2 = (triggerPrice is not None) and reduceOnly + if (stopLossPrice is None) and isStopOrderType2: + # check if order is stop order type 2 - stopLossPrice + if isAscending and (side == 'buy'): + # stopLoss order against short position + stopLossPrice = triggerPrice + if not isAscending and (side == 'sell'): + # stopLoss order against a long position + stopLossPrice = triggerPrice + if (takeProfitPrice is None) and isStopOrderType2: + # check if order is stop order type 2 - takeProfitPrice + if isAscending and (side == 'sell'): + # takeprofit order against a long position + takeProfitPrice = triggerPrice + if not isAscending and (side == 'buy'): + # takeprofit order against a short position + takeProfitPrice = triggerPrice + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': lastTradeTimestamp, + 'lastUpdateTimestamp': lastTradeTimestamp, + 'symbol': symbol, + 'type': type, + 'timeInForce': timeInForce, + 'postOnly': None, + 'reduceOnly': self.safe_bool(order, 'reduceOnly'), + 'side': side, + 'price': price, + 'triggerPrice': triggerPrice, + 'takeProfitPrice': takeProfitPrice, + 'stopLossPrice': stopLossPrice, + 'amount': amount, + 'cost': cost, + 'average': avgPrice, + 'filled': filled, + 'remaining': remaining, + 'status': status, + 'fee': fee, + 'trades': None, + }, market) + + def create_market_buy_order_with_cost(self, symbol: str, cost: float, params={}) -> Order: + """ + create a market buy order by providing the symbol and cost + + https://bybit-exchange.github.io/docs/v5/order/create-order + + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' createMarketBuyOrderWithCost() supports spot orders only') + req = { + 'cost': cost, + } + return self.create_order(symbol, 'market', 'buy', -1, None, self.extend(req, params)) + + def create_market_sell_order_with_cost(self, symbol: str, cost: float, params={}) -> Order: + """ + create a market sell order by providing the symbol and cost + + https://bybit-exchange.github.io/docs/v5/order/create-order + + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + types = self.is_unified_enabled() + enableUnifiedAccount = types[1] + if not enableUnifiedAccount: + raise NotSupported(self.id + ' createMarketSellOrderWithCost() supports UTA accounts only') + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' createMarketSellOrderWithCost() supports spot orders only') + req = { + 'cost': cost, + } + return self.create_order(symbol, 'market', 'sell', -1, None, self.extend(req, params)) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}) -> Order: + """ + create a trade order + + https://bybit-exchange.github.io/docs/v5/order/create-order + https://bybit-exchange.github.io/docs/v5/position/trading-stop + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.timeInForce]: "GTC", "IOC", "FOK" + :param bool [params.postOnly]: True or False whether the order is post-only + :param bool [params.reduceOnly]: True or False whether the order is reduce-only + :param str [params.positionIdx]: *contracts only* 0 for one-way mode, 1 buy side of hedged mode, 2 sell side of hedged mode + :param bool [params.hedged]: *contracts only* True for hedged mode, False for one way mode, default is False + :param int [params.isLeverage]: *unified spot only* False then spot trading True then margin trading + :param str [params.tpslMode]: *contract only* 'Full' or 'Partial' + :param str [params.mmp]: *option only* market maker protection + :param str [params.triggerDirection]: *contract only* the direction for trigger orders, 'ascending' or 'descending' + :param float [params.triggerPrice]: The price at which a trigger order is triggered at + :param float [params.stopLossPrice]: The price at which a stop loss order is triggered at + :param float [params.takeProfitPrice]: The price at which a take profit order is triggered at + :param dict [params.takeProfit]: *takeProfit object in params* containing the triggerPrice at which the attached take profit order will be triggered + :param float [params.takeProfit.triggerPrice]: take profit trigger price + :param dict [params.stopLoss]: *stopLoss object in params* containing the triggerPrice at which the attached stop loss order will be triggered + :param float [params.stopLoss.triggerPrice]: stop loss trigger price + :param str [params.trailingAmount]: the quote amount to trail away from the current market price + :param str [params.trailingTriggerPrice]: the price to trigger a trailing order, default uses the price argument + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + parts = self.is_unified_enabled() + enableUnifiedAccount = parts[1] + trailingAmount = self.safe_string_2(params, 'trailingAmount', 'trailingStop') + stopLossPrice = self.safe_string(params, 'stopLossPrice') + takeProfitPrice = self.safe_string(params, 'takeProfitPrice') + isTrailingAmountOrder = trailingAmount is not None + isStopLoss = stopLossPrice is not None + isTakeProfit = takeProfitPrice is not None + orderRequest = self.create_order_request(symbol, type, side, amount, price, params, enableUnifiedAccount) + defaultMethod = None + if (isTrailingAmountOrder or isStopLoss or isTakeProfit) and not market['spot']: + defaultMethod = 'privatePostV5PositionTradingStop' + else: + defaultMethod = 'privatePostV5OrderCreate' + method = None + method, params = self.handle_option_and_params(params, 'createOrder', 'method', defaultMethod) + response = None + if method == 'privatePostV5PositionTradingStop': + response = self.privatePostV5PositionTradingStop(orderRequest) + else: + response = self.privatePostV5OrderCreate(orderRequest) # already extended inside createOrderRequest + # + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "orderId": "1321003749386327552", + # "orderLinkId": "spot-test-postonly" + # }, + # "retExtInfo": {}, + # "time": 1672211918471 + # } + # + order = self.safe_dict(response, 'result', {}) + return self.parse_order(order, market) + + def create_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}, isUTA=True): + market = self.market(symbol) + symbol = market['symbol'] + lowerCaseType = type.lower() + if (price is None) and (lowerCaseType == 'limit'): + raise ArgumentsRequired(self.id + ' createOrder requires a price argument for limit orders') + request: dict = { + 'symbol': market['id'], + # 'side': self.capitalize(side), + # 'orderType': self.capitalize(lowerCaseType), # limit or market + # 'timeInForce': 'GTC', # IOC, FOK, PostOnly + # 'takeProfit': 123.45, # take profit price, only take effect upon opening the position + # 'stopLoss': 123.45, # stop loss price, only take effect upon opening the position + # 'reduceOnly': False, # reduce only, required for linear orders + # when creating a closing order, bybit recommends a True value for + # closeOnTrigger to avoid failing due to insufficient available margin + # 'closeOnTrigger': False, required for linear orders + # 'orderLinkId': 'string', # unique client order id, max 36 characters + # 'triggerPrice': 123.46, # trigger price, required for conditional orders + # 'triggerBy': 'MarkPrice', # IndexPrice, MarkPrice, LastPrice + # 'tpTriggerby': 'MarkPrice', # IndexPrice, MarkPrice, LastPrice + # 'slTriggerBy': 'MarkPrice', # IndexPrice, MarkPrice, LastPrice + # 'mmp': False # market maker protection + # 'positionIdx': 0, # Position mode. Unified account has one-way mode only(0) + # 'triggerDirection': 1, # Conditional order param. Used to identify the expected direction of the conditional order. 1: triggered when market price rises to triggerPrice 2: triggered when market price falls to triggerPrice + # Valid for spot only. + # 'isLeverage': 0, # Whether to borrow. 0(default): False, 1: True + # 'orderFilter': 'Order' # Order,tpslOrder. If not passed, Order by default + # Valid for option only. + # 'orderIv': '0', # Implied volatility; parameters are passed according to the real value; for example, for 10%, 0.1 is passed + } + hedged = self.safe_bool(params, 'hedged', False) + reduceOnly = self.safe_bool(params, 'reduceOnly') + triggerPrice = self.safe_value_2(params, 'triggerPrice', 'stopPrice') + stopLossTriggerPrice = self.safe_value(params, 'stopLossPrice') + takeProfitTriggerPrice = self.safe_value(params, 'takeProfitPrice') + stopLoss = self.safe_value(params, 'stopLoss') + takeProfit = self.safe_value(params, 'takeProfit') + trailingTriggerPrice = self.safe_string_2(params, 'trailingTriggerPrice', 'activePrice', self.number_to_string(price)) + trailingAmount = self.safe_string_2(params, 'trailingAmount', 'trailingStop') + isTrailingAmountOrder = trailingAmount is not None + isTriggerOrder = triggerPrice is not None + isStopLossTriggerOrder = stopLossTriggerPrice is not None + isTakeProfitTriggerOrder = takeProfitTriggerPrice is not None + isStopLoss = stopLoss is not None + isTakeProfit = takeProfit is not None + isMarket = lowerCaseType == 'market' + isLimit = lowerCaseType == 'limit' + isBuy = side == 'buy' + defaultMethod = None + if (isTrailingAmountOrder or isStopLossTriggerOrder or isTakeProfitTriggerOrder) and not market['spot']: + defaultMethod = 'privatePostV5PositionTradingStop' + else: + defaultMethod = 'privatePostV5OrderCreate' + method = None + method, params = self.handle_option_and_params(params, 'createOrder', 'method', defaultMethod) + isAlternativeEndpoint = method == 'privatePostV5PositionTradingStop' + amountString = self.get_amount(symbol, amount) + priceString = self.get_price(symbol, self.number_to_string(price)) if (price is not None) else None + if isTrailingAmountOrder or isAlternativeEndpoint: + if isStopLoss or isTakeProfit or isTriggerOrder or market['spot']: + raise InvalidOrder(self.id + ' the API endpoint used only supports contract trailingAmount, stopLossPrice and takeProfitPrice orders') + if isStopLossTriggerOrder or isTakeProfitTriggerOrder: + tpslMode = self.safe_string(params, 'tpslMode', 'Partial') + isFullTpsl = tpslMode == 'Full' + isPartialTpsl = tpslMode == 'Partial' + if isLimit and isFullTpsl: + raise InvalidOrder(self.id + ' tpsl orders with "full" tpslMode only support "market" type') + request['tpslMode'] = tpslMode + if isStopLossTriggerOrder: + request['stopLoss'] = self.get_price(symbol, stopLossTriggerPrice) + if isPartialTpsl: + request['slSize'] = amountString + if isLimit: + request['slOrderType'] = 'Limit' + request['slLimitPrice'] = priceString + elif isTakeProfitTriggerOrder: + request['takeProfit'] = self.get_price(symbol, takeProfitTriggerPrice) + if isPartialTpsl: + request['tpSize'] = amountString + if isLimit: + request['tpOrderType'] = 'Limit' + request['tpLimitPrice'] = priceString + else: + request['side'] = self.capitalize(side) + request['orderType'] = self.capitalize(lowerCaseType) + timeInForce = self.safe_string_lower(params, 'timeInForce') # self is same specific param + postOnly = None + postOnly, params = self.handle_post_only(isMarket, timeInForce == 'postonly', params) + if postOnly: + request['timeInForce'] = 'PostOnly' + elif timeInForce == 'gtc': + request['timeInForce'] = 'GTC' + elif timeInForce == 'fok': + request['timeInForce'] = 'FOK' + elif timeInForce == 'ioc': + request['timeInForce'] = 'IOC' + if market['spot']: + # only works for spot market + if triggerPrice is not None: + request['orderFilter'] = 'StopOrder' + elif isStopLossTriggerOrder or isTakeProfitTriggerOrder: + request['orderFilter'] = 'tpslOrder' + clientOrderId = self.safe_string(params, 'clientOrderId') + if clientOrderId is not None: + request['orderLinkId'] = clientOrderId + elif market['option']: + # mandatory field for options + request['orderLinkId'] = self.uuid16() + if isLimit: + request['price'] = priceString + category = None + category, params = self.get_bybit_type('createOrderRequest', market, params) + request['category'] = category + cost = self.safe_string(params, 'cost') + params = self.omit(params, 'cost') + # if the cost is inferable, let's keep the old logic and ignore marketUnit, to minimize the impact of the changes + isMarketBuyAndCostInferable = (lowerCaseType == 'market') and (side == 'buy') and ((price is not None) or (cost is not None)) + isMarketOrder = lowerCaseType == 'market' + if market['spot'] and isMarketOrder and isUTA and not isMarketBuyAndCostInferable: + # UTA account can specify the cost of the order on both sides + if (cost is not None) or (price is not None): + request['marketUnit'] = 'quoteCoin' + orderCost = None + if cost is not None: + orderCost = cost + else: + quoteAmount = Precise.string_mul(amountString, priceString) + orderCost = quoteAmount + request['qty'] = self.get_cost(symbol, orderCost) + else: + request['marketUnit'] = 'baseCoin' + request['qty'] = amountString + elif market['spot'] and isMarketOrder and (side == 'buy'): + # classic accounts + # for market buy it requires the amount of quote currency to spend + createMarketBuyOrderRequiresPrice = True + createMarketBuyOrderRequiresPrice, params = self.handle_option_and_params(params, 'createOrder', 'createMarketBuyOrderRequiresPrice') + if createMarketBuyOrderRequiresPrice: + if (price is None) and (cost is None): + raise InvalidOrder(self.id + ' createOrder() requires the price argument for market buy orders to calculate the total cost to spend(amount * price), alternatively set the createMarketBuyOrderRequiresPrice option or param to False and pass the cost to spend in the amount argument') + else: + quoteAmount = Precise.string_mul(self.number_to_string(amount), priceString) + costRequest = cost if (cost is not None) else quoteAmount + request['qty'] = self.get_cost(symbol, costRequest) + else: + if cost is not None: + request['qty'] = self.get_cost(symbol, self.number_to_string(cost)) + elif price is not None: + request['qty'] = self.get_cost(symbol, Precise.string_mul(amountString, priceString)) + else: + request['qty'] = amountString + else: + if not isTrailingAmountOrder and not isAlternativeEndpoint: + request['qty'] = amountString + if isTrailingAmountOrder: + if trailingTriggerPrice is not None: + request['activePrice'] = self.get_price(symbol, trailingTriggerPrice) + request['trailingStop'] = trailingAmount + elif isTriggerOrder and not isAlternativeEndpoint: + triggerDirection = self.safe_string(params, 'triggerDirection') + params = self.omit(params, ['triggerPrice', 'stopPrice', 'triggerDirection']) + if market['spot']: + if triggerDirection is not None: + raise NotSupported(self.id + ' createOrder() : trigger order does not support triggerDirection for spot markets yet') + else: + if triggerDirection is None: + raise ArgumentsRequired(self.id + ' stop/trigger orders require a triggerDirection parameter, either "ascending" or "descending" to determine the direction of the trigger.') + isAsending = ((triggerDirection == 'ascending') or (triggerDirection == 'above') or (triggerDirection == '1')) + request['triggerDirection'] = 1 if isAsending else 2 + request['triggerPrice'] = self.get_price(symbol, triggerPrice) + elif (isStopLossTriggerOrder or isTakeProfitTriggerOrder) and not isAlternativeEndpoint: + if isBuy: + request['triggerDirection'] = 1 if isStopLossTriggerOrder else 2 + else: + request['triggerDirection'] = 2 if isStopLossTriggerOrder else 1 + triggerPrice = stopLossTriggerPrice if isStopLossTriggerOrder else takeProfitTriggerPrice + request['triggerPrice'] = self.get_price(symbol, triggerPrice) + request['reduceOnly'] = True + if (isStopLoss or isTakeProfit) and not isAlternativeEndpoint: + if isStopLoss: + slTriggerPrice = self.safe_value_2(stopLoss, 'triggerPrice', 'stopPrice', stopLoss) + request['stopLoss'] = self.get_price(symbol, slTriggerPrice) + slLimitPrice = self.safe_value(stopLoss, 'price') + if slLimitPrice is not None: + request['tpslMode'] = 'Partial' + request['slOrderType'] = 'Limit' + request['slLimitPrice'] = self.get_price(symbol, slLimitPrice) + else: + # for spot market, we need to add self + if market['spot']: + request['slOrderType'] = 'Market' + # for spot market, we need to add self + if market['spot'] and isMarketOrder: + raise InvalidOrder(self.id + ' createOrder(): attached stopLoss is not supported for spot market orders') + if isTakeProfit: + tpTriggerPrice = self.safe_value_2(takeProfit, 'triggerPrice', 'stopPrice', takeProfit) + request['takeProfit'] = self.get_price(symbol, tpTriggerPrice) + tpLimitPrice = self.safe_value(takeProfit, 'price') + if tpLimitPrice is not None: + request['tpslMode'] = 'Partial' + request['tpOrderType'] = 'Limit' + request['tpLimitPrice'] = self.get_price(symbol, tpLimitPrice) + else: + # for spot market, we need to add self + if market['spot']: + request['tpOrderType'] = 'Market' + # for spot market, we need to add self + if market['spot'] and isMarketOrder: + raise InvalidOrder(self.id + ' createOrder(): attached takeProfit is not supported for spot market orders') + if not market['spot'] and hedged: + if reduceOnly: + params = self.omit(params, 'reduceOnly') + side = 'sell' if (side == 'buy') else 'buy' + request['positionIdx'] = 1 if (side == 'buy') else 2 + params = self.omit(params, ['stopPrice', 'timeInForce', 'stopLossPrice', 'takeProfitPrice', 'postOnly', 'clientOrderId', 'triggerPrice', 'stopLoss', 'takeProfit', 'trailingAmount', 'trailingTriggerPrice', 'hedged', 'tpslMode']) + return self.extend(request, params) + + def create_orders(self, orders: List[OrderRequest], params={}) -> List[Order]: + """ + create a list of trade orders + + https://bybit-exchange.github.io/docs/v5/order/batch-place + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + accounts = self.is_unified_enabled() + isUta = accounts[1] + ordersRequests = [] + orderSymbols = [] + for i in range(0, len(orders)): + rawOrder = orders[i] + marketId = self.safe_string(rawOrder, 'symbol') + orderSymbols.append(marketId) + type = self.safe_string(rawOrder, 'type') + side = self.safe_string(rawOrder, 'side') + amount = self.safe_value(rawOrder, 'amount') + price = self.safe_value(rawOrder, 'price') + orderParams = self.safe_dict(rawOrder, 'params', {}) + orderRequest = self.create_order_request(marketId, type, side, amount, price, orderParams, isUta) + del orderRequest['category'] + ordersRequests.append(orderRequest) + symbols = self.market_symbols(orderSymbols, None, False, True, True) + market = self.market(symbols[0]) + unifiedMarginStatus = self.safe_integer(self.options, 'unifiedMarginStatus', 6) + category = None + category, params = self.get_bybit_type('createOrders', market, params) + if (category == 'inverse') and (unifiedMarginStatus < 5): + raise NotSupported(self.id + ' createOrders does not allow inverse orders for non UTA2.0 account') + request: dict = { + 'category': category, + 'request': ordersRequests, + } + response = self.privatePostV5OrderCreateBatch(self.extend(request, params)) + result = self.safe_dict(response, 'result', {}) + data = self.safe_list(result, 'list', []) + retInfo = self.safe_dict(response, 'retExtInfo', {}) + codes = self.safe_list(retInfo, 'list', []) + # self.extend the error with the unsuccessful orders + for i in range(0, len(codes)): + code = codes[i] + retCode = self.safe_integer(code, 'code') + if retCode != 0: + data[i] = self.extend(data[i], code) + # + # { + # "retCode":0, + # "retMsg":"OK", + # "result":{ + # "list":[ + # { + # "category":"linear", + # "symbol":"LTCUSDT", + # "orderId":"", + # "orderLinkId":"", + # "createAt":"" + # }, + # { + # "category":"linear", + # "symbol":"LTCUSDT", + # "orderId":"3c9f65b6-01ad-4ac0-9741-df17e02a4223", + # "orderLinkId":"", + # "createAt":"1698075516029" + # } + # ] + # }, + # "retExtInfo":{ + # "list":[ + # { + # "code":10001, + # "msg":"The number of contracts exceeds maximum limit allowed: too large" + # }, + # { + # "code":0, + # "msg":"OK" + # } + # ] + # }, + # "time":1698075516029 + # } + # + return self.parse_orders(data) + + def edit_order_request(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'orderId': id, + # 'orderLinkId': 'string', # unique client order id, max 36 characters + # 'takeProfit': 123.45, # take profit price, only take effect upon opening the position + # 'stopLoss': 123.45, # stop loss price, only take effect upon opening the position + # 'triggerPrice': 123.45, # trigger price, required for conditional orders + # 'triggerBy': 'MarkPrice', # IndexPrice, MarkPrice, LastPrice + # 'tpTriggerby': 'MarkPrice', # IndexPrice, MarkPrice, LastPrice + # 'slTriggerBy': 'MarkPrice', # IndexPrice, MarkPrice, LastPrice + # Valid for option only. + # 'orderIv': '0', # Implied volatility; parameters are passed according to the real value; for example, for 10%, 0.1 is passed + } + category = None + category, params = self.get_bybit_type('editOrderRequest', market, params) + request['category'] = category + if amount is not None: + request['qty'] = self.get_amount(symbol, amount) + if price is not None: + request['price'] = self.get_price(symbol, self.number_to_string(price)) + triggerPrice = self.safe_string_2(params, 'triggerPrice', 'stopPrice') + stopLossTriggerPrice = self.safe_string(params, 'stopLossPrice') + takeProfitTriggerPrice = self.safe_string(params, 'takeProfitPrice') + stopLoss = self.safe_value(params, 'stopLoss') + takeProfit = self.safe_value(params, 'takeProfit') + isStopLossTriggerOrder = stopLossTriggerPrice is not None + isTakeProfitTriggerOrder = takeProfitTriggerPrice is not None + isStopLoss = stopLoss is not None + isTakeProfit = takeProfit is not None + if isStopLossTriggerOrder or isTakeProfitTriggerOrder: + triggerPrice = stopLossTriggerPrice if isStopLossTriggerOrder else takeProfitTriggerPrice + if triggerPrice is not None: + triggerPriceRequest = triggerPrice if (triggerPrice == '0') else self.get_price(symbol, triggerPrice) + request['triggerPrice'] = triggerPriceRequest + triggerBy = self.safe_string(params, 'triggerBy', 'LastPrice') + request['triggerBy'] = triggerBy + if isStopLoss or isTakeProfit: + if isStopLoss: + slTriggerPrice = self.safe_string_2(stopLoss, 'triggerPrice', 'stopPrice', stopLoss) + stopLossRequest = slTriggerPrice if (slTriggerPrice == '0') else self.get_price(symbol, slTriggerPrice) + request['stopLoss'] = stopLossRequest + slTriggerBy = self.safe_string(params, 'slTriggerBy', 'LastPrice') + request['slTriggerBy'] = slTriggerBy + if isTakeProfit: + tpTriggerPrice = self.safe_string_2(takeProfit, 'triggerPrice', 'stopPrice', takeProfit) + takeProfitRequest = tpTriggerPrice if (tpTriggerPrice == '0') else self.get_price(symbol, tpTriggerPrice) + request['takeProfit'] = takeProfitRequest + tpTriggerBy = self.safe_string(params, 'tpTriggerBy', 'LastPrice') + request['tpTriggerBy'] = tpTriggerBy + clientOrderId = self.safe_string(params, 'clientOrderId') + if clientOrderId is not None: + request['orderLinkId'] = clientOrderId + params = self.omit(params, ['stopPrice', 'stopLossPrice', 'takeProfitPrice', 'triggerPrice', 'clientOrderId', 'stopLoss', 'takeProfit']) + return request + + def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}) -> Order: + """ + edit a trade order + + https://bybit-exchange.github.io/docs/v5/order/amend-order + https://bybit-exchange.github.io/docs/derivatives/unified/replace-order + https://bybit-exchange.github.io/docs/api-explorer/derivatives/trade/contract/replace-order + + :param str id: cancel order id + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float price: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: The price that a trigger order is triggered at + :param float [params.stopLossPrice]: The price that a stop loss order is triggered at + :param float [params.takeProfitPrice]: The price that a take profit order is triggered at + :param dict [params.takeProfit]: *takeProfit object in params* containing the triggerPrice that the attached take profit order will be triggered + :param float [params.takeProfit.triggerPrice]: take profit trigger price + :param dict [params.stopLoss]: *stopLoss object in params* containing the triggerPrice that the attached stop loss order will be triggered + :param float [params.stopLoss.triggerPrice]: stop loss trigger price + :param str [params.triggerBy]: 'IndexPrice', 'MarkPrice' or 'LastPrice', default is 'LastPrice', required if no initial value for triggerPrice + :param str [params.slTriggerBy]: 'IndexPrice', 'MarkPrice' or 'LastPrice', default is 'LastPrice', required if no initial value for stopLoss + :param str [params.tpTriggerby]: 'IndexPrice', 'MarkPrice' or 'LastPrice', default is 'LastPrice', required if no initial value for takeProfit + :returns dict: an `order structure ` + """ + self.load_markets() + if symbol is None: + raise ArgumentsRequired(self.id + ' editOrder() requires a symbol argument') + request = self.edit_order_request(id, symbol, type, side, amount, price, params) + response = self.privatePostV5OrderAmend(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "orderId": "c6f055d9-7f21-4079-913d-e6523a9cfffa", + # "orderLinkId": "linear-004" + # }, + # "retExtInfo": {}, + # "time": 1672217093461 + # } + # + result = self.safe_dict(response, 'result', {}) + return self.safe_order({ + 'info': response, + 'id': self.safe_string(result, 'orderId'), + }) + + def edit_orders(self, orders: List[OrderRequest], params={}) -> List[Order]: + """ + edit a list of trade orders + + https://bybit-exchange.github.io/docs/v5/order/batch-amend + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + ordersRequests = [] + orderSymbols = [] + for i in range(0, len(orders)): + rawOrder = orders[i] + symbol = self.safe_string(rawOrder, 'symbol') + orderSymbols.append(symbol) + id = self.safe_string(rawOrder, 'id') + type = self.safe_string(rawOrder, 'type') + side = self.safe_string(rawOrder, 'side') + amount = self.safe_value(rawOrder, 'amount') + price = self.safe_value(rawOrder, 'price') + orderParams = self.safe_dict(rawOrder, 'params', {}) + orderRequest = self.edit_order_request(id, symbol, type, side, amount, price, orderParams) + del orderRequest['category'] + ordersRequests.append(orderRequest) + orderSymbols = self.market_symbols(orderSymbols, None, False, True, True) + market = self.market(orderSymbols[0]) + unifiedMarginStatus = self.safe_integer(self.options, 'unifiedMarginStatus', 6) + category = None + category, params = self.get_bybit_type('editOrders', market, params) + if (category == 'inverse') and (unifiedMarginStatus < 5): + raise NotSupported(self.id + ' editOrders does not allow inverse orders for non UTA2.0 account') + request: dict = { + 'category': category, + 'request': ordersRequests, + } + response = self.privatePostV5OrderAmendBatch(self.extend(request, params)) + result = self.safe_dict(response, 'result', {}) + data = self.safe_list(result, 'list', []) + retInfo = self.safe_dict(response, 'retExtInfo', {}) + codes = self.safe_list(retInfo, 'list', []) + # self.extend the error with the unsuccessful orders + for i in range(0, len(codes)): + code = codes[i] + retCode = self.safe_integer(code, 'code') + if retCode != 0: + data[i] = self.extend(data[i], code) + # + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "list": [ + # { + # "category": "option", + # "symbol": "ETH-30DEC22-500-C", + # "orderId": "b551f227-7059-4fb5-a6a6-699c04dbd2f2", + # "orderLinkId": "" + # }, + # { + # "category": "option", + # "symbol": "ETH-30DEC22-700-C", + # "orderId": "fa6a595f-1a57-483f-b9d3-30e9c8235a52", + # "orderLinkId": "" + # } + # ] + # }, + # "retExtInfo": { + # "list": [ + # { + # "code": 0, + # "msg": "OK" + # }, + # { + # "code": 0, + # "msg": "OK" + # } + # ] + # }, + # "time": 1672222808060 + # } + # + return self.parse_orders(data) + + def cancel_order_request(self, id: str, symbol: Str = None, params={}): + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + # 'orderLinkId': 'string', + # 'orderId': id, + # conditional orders + # 'orderFilter': '', # Valid for spot only. Order,tpslOrder. If not passed, Order by default + } + if market['spot']: + # only works for spot market + isTrigger = self.safe_bool_2(params, 'stop', 'trigger', False) + params = self.omit(params, ['stop', 'trigger']) + request['orderFilter'] = 'StopOrder' if isTrigger else 'Order' + if id is not None: # The user can also use argument params["orderLinkId"] + request['orderId'] = id + category = None + category, params = self.get_bybit_type('cancelOrderRequest', market, params) + request['category'] = category + return self.extend(request, params) + + def cancel_order(self, id: str, symbol: Str = None, params={}) -> Order: + """ + cancels an open order + + https://bybit-exchange.github.io/docs/v5/order/cancel-order + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: *spot only* whether the order is a trigger order + :param boolean [params.stop]: alias for trigger + :param str [params.orderFilter]: *spot only* 'Order' or 'StopOrder' or 'tpslOrder' + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + requestExtended = self.cancel_order_request(id, symbol, params) + response = self.privatePostV5OrderCancel(requestExtended) + # + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "orderId": "c6f055d9-7f21-4079-913d-e6523a9cfffa", + # "orderLinkId": "linear-004" + # }, + # "retExtInfo": {}, + # "time": 1672217377164 + # } + # + result = self.safe_dict(response, 'result', {}) + return self.parse_order(result, market) + + def cancel_orders(self, ids: List[str], symbol: Str = None, params={}) -> List[Order]: + """ + cancel multiple orders + + https://bybit-exchange.github.io/docs/v5/order/batch-cancel + + :param str[] ids: order ids + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str[] [params.clientOrderIds]: client order ids + :returns dict: an list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrders() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + types = self.is_unified_enabled() + enableUnifiedAccount = types[1] + if not enableUnifiedAccount: + raise NotSupported(self.id + ' cancelOrders() supports UTA accounts only') + category = None + category, params = self.get_bybit_type('cancelOrders', market, params) + if category == 'inverse': + raise NotSupported(self.id + ' cancelOrders does not allow inverse orders') + ordersRequests = [] + clientOrderIds = self.safe_list_2(params, 'clientOrderIds', 'clientOids', []) + params = self.omit(params, ['clientOrderIds', 'clientOids']) + for i in range(0, len(clientOrderIds)): + ordersRequests.append({ + 'symbol': market['id'], + 'orderLinkId': self.safe_string(clientOrderIds, i), + }) + for i in range(0, len(ids)): + ordersRequests.append({ + 'symbol': market['id'], + 'orderId': self.safe_string(ids, i), + }) + request: dict = { + 'category': category, + 'request': ordersRequests, + } + response = self.privatePostV5OrderCancelBatch(self.extend(request, params)) + # + # { + # "retCode": "0", + # "retMsg": "OK", + # "result": { + # "list": [ + # { + # "category": "spot", + # "symbol": "BTCUSDT", + # "orderId": "1636282505818800896", + # "orderLinkId": "1636282505818800897" + # }, + # { + # "category": "spot", + # "symbol": "BTCUSDT", + # "orderId": "1636282505818800898", + # "orderLinkId": "1636282505818800899" + # } + # ] + # }, + # "retExtInfo": { + # "list": [ + # { + # "code": "0", + # "msg": "OK" + # }, + # { + # "code": "0", + # "msg": "OK" + # } + # ] + # }, + # "time": "1709796158501" + # } + # + result = self.safe_dict(response, 'result', {}) + row = self.safe_list(result, 'list', []) + return self.parse_orders(row, market) + + def cancel_all_orders_after(self, timeout: Int, params={}): + """ + dead man's switch, cancel all orders after the given timeout + + https://bybit-exchange.github.io/docs/v5/order/dcp + + :param number timeout: time in milliseconds + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.product]: OPTIONS, DERIVATIVES, SPOT, default is 'DERIVATIVES' + :returns dict: the api result + """ + self.load_markets() + request: dict = { + 'timeWindow': self.parse_to_int(timeout / 1000), + } + type: Str = None + type, params = self.handle_market_type_and_params('cancelAllOrdersAfter', None, params, 'swap') + productMap = { + 'spot': 'SPOT', + 'swap': 'DERIVATIVES', + 'option': 'OPTIONS', + } + product = self.safe_string(productMap, type, type) + request['product'] = product + response = self.privatePostV5OrderDisconnectedCancelAll(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "success" + # } + # + return response + + def cancel_orders_for_symbols(self, orders: List[CancellationRequest], params={}): + """ + cancel multiple orders for multiple symbols + + https://bybit-exchange.github.io/docs/v5/order/batch-cancel + + :param CancellationRequest[] orders: list of order ids with symbol, example [{"id": "a", "symbol": "BTC/USDT"}, {"id": "b", "symbol": "ETH/USDT"}] + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an list of `order structures ` + """ + self.load_markets() + types = self.is_unified_enabled() + enableUnifiedAccount = types[1] + if not enableUnifiedAccount: + raise NotSupported(self.id + ' cancelOrdersForSymbols() supports UTA accounts only') + ordersRequests = [] + category = None + for i in range(0, len(orders)): + order = orders[i] + symbol = self.safe_string(order, 'symbol') + market = self.market(symbol) + currentCategory = None + currentCategory, params = self.get_bybit_type('cancelOrders', market, params) + if currentCategory == 'inverse': + raise NotSupported(self.id + ' cancelOrdersForSymbols does not allow inverse orders') + if (category is not None) and (category != currentCategory): + raise ExchangeError(self.id + ' cancelOrdersForSymbols requires all orders to be of the same category(linear, spot or option))') + category = currentCategory + id = self.safe_string(order, 'id') + clientOrderId = self.safe_string(order, 'clientOrderId') + idKey = 'orderId' + if clientOrderId is not None: + idKey = 'orderLinkId' + orderItem: dict = { + 'symbol': market['id'], + } + orderItem[idKey] = id if (idKey == 'orderId') else clientOrderId + ordersRequests.append(orderItem) + request: dict = { + 'category': category, + 'request': ordersRequests, + } + response = self.privatePostV5OrderCancelBatch(self.extend(request, params)) + # + # { + # "retCode": "0", + # "retMsg": "OK", + # "result": { + # "list": [ + # { + # "category": "spot", + # "symbol": "BTCUSDT", + # "orderId": "1636282505818800896", + # "orderLinkId": "1636282505818800897" + # }, + # { + # "category": "spot", + # "symbol": "BTCUSDT", + # "orderId": "1636282505818800898", + # "orderLinkId": "1636282505818800899" + # } + # ] + # }, + # "retExtInfo": { + # "list": [ + # { + # "code": "0", + # "msg": "OK" + # }, + # { + # "code": "0", + # "msg": "OK" + # } + # ] + # }, + # "time": "1709796158501" + # } + # + result = self.safe_dict(response, 'result', {}) + row = self.safe_list(result, 'list', []) + return self.parse_orders(row, None) + + def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders + + https://bybit-exchange.github.io/docs/v5/order/cancel-all + + :param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: True if trigger order + :param boolean [params.stop]: alias for trigger + :param str [params.type]: market type, ['swap', 'option', 'spot'] + :param str [params.subType]: market subType, ['linear', 'inverse'] + :param str [params.baseCoin]: Base coin. Supports linear, inverse & option + :param str [params.settleCoin]: Settle coin. Supports linear, inverse & option + :returns dict[]: a list of `order structures ` + """ + self.load_markets() + enableUnifiedMargin, enableUnifiedAccount = self.is_unified_enabled() + isUnifiedAccount = (enableUnifiedMargin or enableUnifiedAccount) + market = None + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + type = None + type, params = self.get_bybit_type('cancelAllOrders', market, params) + request['category'] = type + if (type == 'option') and not isUnifiedAccount: + raise NotSupported(self.id + ' cancelAllOrders() Normal Account not support ' + type + ' market') + if (type == 'linear') or (type == 'inverse'): + baseCoin = self.safe_string(params, 'baseCoin') + if symbol is None and baseCoin is None: + defaultSettle = self.safe_string(self.options, 'defaultSettle', 'USDT') + request['settleCoin'] = self.safe_string(params, 'settleCoin', defaultSettle) + isTrigger = self.safe_bool_2(params, 'stop', 'trigger', False) + params = self.omit(params, ['stop', 'trigger']) + if isTrigger: + request['orderFilter'] = 'StopOrder' + response = self.privatePostV5OrderCancelAll(self.extend(request, params)) + # + # linear / inverse / option + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "list": [ + # { + # "orderId": "f6a73e1f-39b5-4dee-af21-1460b2e3b27c", + # "orderLinkId": "a001" + # } + # ] + # }, + # "retExtInfo": {}, + # "time": 1672219780463 + # } + # + # spot + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "success": "1" + # }, + # "retExtInfo": {}, + # "time": 1676962409398 + # } + # + result = self.safe_dict(response, 'result', {}) + orders = self.safe_list(result, 'list') + if not isinstance(orders, list): + return [self.safe_order({'info': response})] + return self.parse_orders(orders, market) + + def fetch_order_classic(self, id: str, symbol: Str = None, params={}) -> Order: + """ + fetches information on an order made by the user *classic accounts only* + + https://bybit-exchange.github.io/docs/v5/order/order-list + + :param str id: the order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrder() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + if market['spot']: + raise NotSupported(self.id + ' fetchOrder() is not supported for spot markets') + request: dict = { + 'orderId': id, + } + result = self.fetch_orders(symbol, None, None, self.extend(request, params)) + length = len(result) + if length == 0: + isTrigger = self.safe_bool_n(params, ['trigger', 'stop'], False) + extra = '' if isTrigger else ' If you are trying to fetch SL/TP conditional order, you might try setting params["trigger"] = True' + raise OrderNotFound('Order ' + str(id) + ' was not found.' + extra) + if length > 1: + raise InvalidOrder(self.id + ' returned more than one order') + return self.safe_value(result, 0) + + def fetch_order(self, id: str, symbol: Str = None, params={}) -> Order: + """ +classic accounts only/ spot not supported* fetches information on an order made by the user *classic accounts only* + + https://bybit-exchange.github.io/docs/v5/order/order-list + + :param str id: the order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param dict [params.acknowledged]: to suppress the warning, set to True + :returns dict: An `order structure ` + """ + self.load_markets() + enableUnifiedMargin, enableUnifiedAccount = self.is_unified_enabled() + isUnifiedAccount = (enableUnifiedMargin or enableUnifiedAccount) + if not isUnifiedAccount: + return self.fetch_order_classic(id, symbol, params) + acknowledge = False + acknowledge, params = self.handle_option_and_params(params, 'fetchOrder', 'acknowledged') + if not acknowledge: + raise ArgumentsRequired(self.id + ' fetchOrder() can only access an order if it is in last 500 orders(of any status) for your account. Set params["acknowledged"] = True to hide self warning. Alternatively, we suggest to use fetchOpenOrder or fetchClosedOrder') + market = self.market(symbol) + marketType = None + marketType, params = self.get_bybit_type('fetchOrder', market, params) + request: dict = { + 'symbol': market['id'], + 'orderId': id, + 'category': marketType, + } + isTrigger = None + isTrigger, params = self.handle_param_bool_2(params, 'trigger', 'stop', False) + if isTrigger: + request['orderFilter'] = 'StopOrder' + response = self.privateGetV5OrderRealtime(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "nextPageCursor": "1321052653536515584%3A1672217748287%2C1321052653536515584%3A1672217748287", + # "category": "spot", + # "list": [ + # { + # "symbol": "ETHUSDT", + # "orderType": "Limit", + # "orderLinkId": "1672217748277652", + # "orderId": "1321052653536515584", + # "cancelType": "UNKNOWN", + # "avgPrice": "", + # "stopOrderType": "tpslOrder", + # "lastPriceOnCreated": "", + # "orderStatus": "Cancelled", + # "takeProfit": "", + # "cumExecValue": "0", + # "triggerDirection": 0, + # "isLeverage": "0", + # "rejectReason": "", + # "price": "1000", + # "orderIv": "", + # "createdTime": "1672217748287", + # "tpTriggerBy": "", + # "positionIdx": 0, + # "timeInForce": "GTC", + # "leavesValue": "500", + # "updatedTime": "1672217748287", + # "side": "Buy", + # "triggerPrice": "1500", + # "cumExecFee": "0", + # "leavesQty": "0", + # "slTriggerBy": "", + # "closeOnTrigger": False, + # "cumExecQty": "0", + # "reduceOnly": False, + # "qty": "0.5", + # "stopLoss": "", + # "triggerBy": "1192.5" + # } + # ] + # }, + # "retExtInfo": {}, + # "time": 1672219526294 + # } + # + result = self.safe_dict(response, 'result', {}) + innerList = self.safe_list(result, 'list', []) + if len(innerList) == 0: + extra = '' if isTrigger else ' If you are trying to fetch SL/TP conditional order, you might try setting params["trigger"] = True' + raise OrderNotFound('Order ' + str(id) + ' was not found.' + extra) + order = self.safe_dict(innerList, 0, {}) + return self.parse_order(order, market) + + def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + res = self.is_unified_enabled() + """ + *classic accounts only/ spot not supported* fetches information on multiple orders made by the user *classic accounts only/ spot not supported* + https://bybit-exchange.github.io/docs/v5/order/order-list + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: True if trigger order + :param boolean [params.stop]: alias for trigger + :param str [params.type]: market type, ['swap', 'option'] + :param str [params.subType]: market subType, ['linear', 'inverse'] + :param str [params.orderFilter]: 'Order' or 'StopOrder' or 'tpslOrder' + :param int [params.until]: the latest time in ms to fetch entries for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Order[]: a list of `order structures ` + """ + enableUnifiedAccount = self.safe_bool(res, 1) + if enableUnifiedAccount: + raise NotSupported(self.id + ' fetchOrders() is not supported after the 5/02 update for UTA accounts, please use fetchOpenOrders, fetchClosedOrders or fetchCanceledOrders') + return self.fetch_orders_classic(symbol, since, limit, params) + + def fetch_orders_classic(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user *classic accounts only* + + https://bybit-exchange.github.io/docs/v5/order/order-list + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: True if trigger order + :param boolean [params.stop]: alias for trigger + :param str [params.type]: market type, ['swap', 'option', 'spot'] + :param str [params.subType]: market subType, ['linear', 'inverse'] + :param str [params.orderFilter]: 'Order' or 'StopOrder' or 'tpslOrder' + :param int [params.until]: the latest time in ms to fetch entries for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOrders', 'paginate') + if paginate: + return self.fetch_paginated_call_cursor('fetchOrders', symbol, since, limit, params, 'nextPageCursor', 'cursor', None, 50) + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + type = None + type, params = self.get_bybit_type('fetchOrders', market, params) + if type == 'spot': + raise NotSupported(self.id + ' fetchOrders() is not supported for spot markets') + request['category'] = type + isTrigger = self.safe_bool_n(params, ['trigger', 'stop'], False) + params = self.omit(params, ['trigger', 'stop']) + if isTrigger: + request['orderFilter'] = 'StopOrder' + if limit is not None: + request['limit'] = limit + if since is not None: + request['startTime'] = since + until = self.safe_integer(params, 'until') # unified in milliseconds + endTime = self.safe_integer(params, 'endTime', until) # exchange-specific in milliseconds + params = self.omit(params, ['endTime', 'until']) + if endTime is not None: + request['endTime'] = endTime + response = self.privateGetV5OrderHistory(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "nextPageCursor": "03234de9-1332-41eb-b805-4a9f42c136a3%3A1672220109387%2C03234de9-1332-41eb-b805-4a9f42c136a3%3A1672220109387", + # "category": "linear", + # "list": [ + # { + # "symbol": "BTCUSDT", + # "orderType": "Limit", + # "orderLinkId": "test-001", + # "orderId": "03234de9-1332-41eb-b805-4a9f42c136a3", + # "cancelType": "CancelByUser", + # "avgPrice": "0", + # "stopOrderType": "UNKNOWN", + # "lastPriceOnCreated": "16656.5", + # "orderStatus": "Cancelled", + # "takeProfit": "", + # "cumExecValue": "0", + # "triggerDirection": 0, + # "blockTradeId": "", + # "rejectReason": "EC_PerCancelRequest", + # "isLeverage": "", + # "price": "18000", + # "orderIv": "", + # "createdTime": "1672220109387", + # "tpTriggerBy": "UNKNOWN", + # "positionIdx": 0, + # "timeInForce": "GoodTillCancel", + # "leavesValue": "0", + # "updatedTime": "1672220114123", + # "side": "Sell", + # "triggerPrice": "", + # "cumExecFee": "0", + # "slTriggerBy": "UNKNOWN", + # "leavesQty": "0", + # "closeOnTrigger": False, + # "cumExecQty": "0", + # "reduceOnly": False, + # "qty": "0.1", + # "stopLoss": "", + # "triggerBy": "UNKNOWN" + # } + # ] + # }, + # "retExtInfo": {}, + # "time": 1672221263862 + # } + # + data = self.add_pagination_cursor_to_result(response) + return self.parse_orders(data, market, since, limit) + + def fetch_closed_order(self, id: str, symbol: Str = None, params={}) -> Order: + """ + fetches information on a closed order made by the user + + https://bybit-exchange.github.io/docs/v5/order/order-list + + :param str id: order id + :param str [symbol]: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: set to True for fetching a closed trigger order + :param boolean [params.stop]: alias for trigger + :param str [params.type]: market type, ['swap', 'option', 'spot'] + :param str [params.subType]: market subType, ['linear', 'inverse'] + :param str [params.orderFilter]: 'Order' or 'StopOrder' or 'tpslOrder' + :returns dict: an `order structure ` + """ + self.load_markets() + request: dict = { + 'orderId': id, + } + result = self.fetch_closed_orders(symbol, None, None, self.extend(request, params)) + length = len(result) + if length == 0: + isTrigger = self.safe_bool_n(params, ['trigger', 'stop'], False) + extra = '' if isTrigger else ' If you are trying to fetch SL/TP conditional order, you might try setting params["trigger"] = True' + raise OrderNotFound('Order ' + str(id) + ' was not found.' + extra) + if length > 1: + raise InvalidOrder(self.id + ' returned more than one order') + return self.safe_value(result, 0) + + def fetch_open_order(self, id: str, symbol: Str = None, params={}) -> Order: + """ + fetches information on an open order made by the user + + https://bybit-exchange.github.io/docs/v5/order/open-order + + :param str id: order id + :param str [symbol]: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: set to True for fetching an open trigger order + :param boolean [params.stop]: alias for trigger + :param str [params.type]: market type, ['swap', 'option', 'spot'] + :param str [params.subType]: market subType, ['linear', 'inverse'] + :param str [params.baseCoin]: Base coin. Supports linear, inverse & option + :param str [params.settleCoin]: Settle coin. Supports linear, inverse & option + :param str [params.orderFilter]: 'Order' or 'StopOrder' or 'tpslOrder' + :returns dict: an `order structure ` + """ + self.load_markets() + request: dict = { + 'orderId': id, + } + result = self.fetch_open_orders(symbol, None, None, self.extend(request, params)) + length = len(result) + if length == 0: + isTrigger = self.safe_bool_n(params, ['trigger', 'stop'], False) + extra = '' if isTrigger else ' If you are trying to fetch SL/TP conditional order, you might try setting params["trigger"] = True' + raise OrderNotFound('Order ' + str(id) + ' was not found.' + extra) + if length > 1: + raise InvalidOrder(self.id + ' returned more than one order') + return self.safe_value(result, 0) + + def fetch_canceled_and_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple canceled and closed orders made by the user + + https://bybit-exchange.github.io/docs/v5/order/order-list + + :param str [symbol]: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: set to True for fetching trigger orders + :param boolean [params.stop]: alias for trigger + :param str [params.type]: market type, ['swap', 'option', 'spot'] + :param str [params.subType]: market subType, ['linear', 'inverse'] + :param str [params.orderFilter]: 'Order' or 'StopOrder' or 'tpslOrder' + :param int [params.until]: the latest time in ms to fetch entries for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchCanceledAndClosedOrders', 'paginate') + if paginate: + return self.fetch_paginated_call_cursor('fetchCanceledAndClosedOrders', symbol, since, limit, params, 'nextPageCursor', 'cursor', None, 50) + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + type = None + type, params = self.get_bybit_type('fetchCanceledAndClosedOrders', market, params) + request['category'] = type + isTrigger = self.safe_bool_n(params, ['trigger', 'stop'], False) + params = self.omit(params, ['trigger', 'stop']) + if isTrigger: + request['orderFilter'] = 'StopOrder' + if limit is not None: + request['limit'] = limit + if since is not None: + request['startTime'] = since + until = self.safe_integer(params, 'until') # unified in milliseconds + endTime = self.safe_integer(params, 'endTime', until) # exchange-specific in milliseconds + params = self.omit(params, ['endTime', 'until']) + if endTime is not None: + request['endTime'] = endTime + response = self.privateGetV5OrderHistory(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "nextPageCursor": "f5f2d355-9a11-4af3-9b83-aa1d6ab6ddfe%3A1757837618905%2Caee7453a-a100-465f-857a-3db780e9329a%3A1757837580469", + # "category": "linear", + # "list": [ + # { + # "symbol": "BTCUSDT", + # "orderType": "Market", + # "orderLinkId": "", + # "slLimitPrice": "0", + # "orderId": "f5f2d355-9a11-4af3-9b83-aa1d6ab6ddfe", + # "cancelType": "UNKNOWN", + # "avgPrice": "122529.9", + # "stopOrderType": "", + # "lastPriceOnCreated": "123747.9", + # "orderStatus": "Filled", + # "createType": "CreateByUser", + # "takeProfit": "", + # "cumExecValue": "122.5299", + # "tpslMode": "", + # "smpType": "None", + # "triggerDirection": 0, + # "blockTradeId": "", + # "cumFeeDetail": { + # "USDT": "0.06739145" + # }, + # "rejectReason": "EC_NoError", + # "isLeverage": "", + # "price": "120518", + # "orderIv": "", + # "createdTime": "1757837618905", + # "tpTriggerBy": "", + # "positionIdx": 0, + # "timeInForce": "IOC", + # "leavesValue": "0", + # "updatedTime": "1757837618909", + # "side": "Sell", + # "smpGroup": 0, + # "triggerPrice": "", + # "tpLimitPrice": "0", + # "cumExecFee": "0.06739145", + # "slTriggerBy": "", + # "leavesQty": "0", + # "closeOnTrigger": False, + # "slippageToleranceType": "UNKNOWN", + # "placeType": "", + # "cumExecQty": "0.001", + # "reduceOnly": True, + # "qty": "0.001", + # "stopLoss": "", + # "smpOrderId": "", + # "slippageTolerance": "0", + # "triggerBy": "", + # "extraFees": "" + # }, + # ] + # }, + # "retExtInfo": {}, + # "time": 1758187806376 + # } + # + data = self.add_pagination_cursor_to_result(response) + return self.parse_orders(data, market, since, limit) + + def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://bybit-exchange.github.io/docs/v5/order/order-list + + :param str [symbol]: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: set to True for fetching closed trigger orders + :param boolean [params.stop]: alias for trigger + :param str [params.type]: market type, ['swap', 'option', 'spot'] + :param str [params.subType]: market subType, ['linear', 'inverse'] + :param str [params.orderFilter]: 'Order' or 'StopOrder' or 'tpslOrder' + :param int [params.until]: the latest time in ms to fetch entries for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + request: dict = { + 'orderStatus': 'Filled', + } + return self.fetch_canceled_and_closed_orders(symbol, since, limit, self.extend(request, params)) + + def fetch_canceled_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple canceled orders made by the user + + https://bybit-exchange.github.io/docs/v5/order/order-list + + :param str [symbol]: unified market symbol of the market orders were made in + :param int [since]: timestamp in ms of the earliest order, default is None + :param int [limit]: max number of orders to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: True if trigger order + :param boolean [params.stop]: alias for trigger + :param str [params.type]: market type, ['swap', 'option', 'spot'] + :param str [params.subType]: market subType, ['linear', 'inverse'] + :param str [params.orderFilter]: 'Order' or 'StopOrder' or 'tpslOrder' + :param int [params.until]: the latest time in ms to fetch entries for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict: a list of `order structures ` + """ + self.load_markets() + request: dict = { + 'orderStatus': 'Cancelled', + } + return self.fetch_canceled_and_closed_orders(symbol, since, limit, self.extend(request, params)) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://bybit-exchange.github.io/docs/v5/order/open-order + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: set to True for fetching open trigger orders + :param boolean [params.stop]: alias for trigger + :param str [params.type]: market type, ['swap', 'option', 'spot'] + :param str [params.subType]: market subType, ['linear', 'inverse'] + :param str [params.baseCoin]: Base coin. Supports linear, inverse & option + :param str [params.settleCoin]: Settle coin. Supports linear, inverse & option + :param str [params.orderFilter]: 'Order' or 'StopOrder' or 'tpslOrder' + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOpenOrders', 'paginate') + if paginate: + return self.fetch_paginated_call_cursor('fetchOpenOrders', symbol, since, limit, params, 'nextPageCursor', 'cursor', None, 50) + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + type = None + type, params = self.get_bybit_type('fetchOpenOrders', market, params) + if type == 'linear' or type == 'inverse': + baseCoin = self.safe_string(params, 'baseCoin') + if symbol is None and baseCoin is None: + defaultSettle = self.safe_string(self.options, 'defaultSettle', 'USDT') + settleCoin = self.safe_string(params, 'settleCoin', defaultSettle) + request['settleCoin'] = settleCoin + request['category'] = type + isTrigger = self.safe_bool_2(params, 'stop', 'trigger', False) + params = self.omit(params, ['stop', 'trigger']) + if isTrigger: + request['orderFilter'] = 'StopOrder' + if limit is not None: + request['limit'] = limit + response = self.privateGetV5OrderRealtime(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "nextPageCursor": "f5f2d355-9a11-4af3-9b83-aa1d6ab6ddfe%3A1757837618905%2Caee7453a-a100-465f-857a-3db780e9329a%3A1757837580469", + # "category": "linear", + # "list": [ + # { + # "symbol": "BTCUSDT", + # "orderType": "Market", + # "orderLinkId": "", + # "slLimitPrice": "0", + # "orderId": "f5f2d355-9a11-4af3-9b83-aa1d6ab6ddfe", + # "cancelType": "UNKNOWN", + # "avgPrice": "122529.9", + # "stopOrderType": "", + # "lastPriceOnCreated": "123747.9", + # "orderStatus": "Filled", + # "createType": "CreateByUser", + # "takeProfit": "", + # "cumExecValue": "122.5299", + # "tpslMode": "", + # "smpType": "None", + # "triggerDirection": 0, + # "blockTradeId": "", + # "cumFeeDetail": { + # "USDT": "0.06739145" + # }, + # "rejectReason": "EC_NoError", + # "isLeverage": "", + # "price": "120518", + # "orderIv": "", + # "createdTime": "1757837618905", + # "tpTriggerBy": "", + # "positionIdx": 0, + # "timeInForce": "IOC", + # "leavesValue": "0", + # "updatedTime": "1757837618909", + # "side": "Sell", + # "smpGroup": 0, + # "triggerPrice": "", + # "tpLimitPrice": "0", + # "cumExecFee": "0.06739145", + # "slTriggerBy": "", + # "leavesQty": "0", + # "closeOnTrigger": False, + # "slippageToleranceType": "UNKNOWN", + # "placeType": "", + # "cumExecQty": "0.001", + # "reduceOnly": True, + # "qty": "0.001", + # "stopLoss": "", + # "smpOrderId": "", + # "slippageTolerance": "0", + # "triggerBy": "", + # "extraFees": "" + # }, + # ] + # }, + # "retExtInfo": {}, + # "time": 1758187806376 + # } + # + data = self.add_pagination_cursor_to_result(response) + return self.parse_orders(data, market, since, limit) + + def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + fetch all the trades made from a single order + + https://bybit-exchange.github.io/docs/v5/position/execution + + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + request: dict = {} + clientOrderId = self.safe_string_2(params, 'clientOrderId', 'orderLinkId') + if clientOrderId is not None: + request['orderLinkId'] = clientOrderId + else: + request['orderId'] = id + params = self.omit(params, ['clientOrderId', 'orderLinkId']) + return self.fetch_my_trades(symbol, since, limit, self.extend(request, params)) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + fetch all trades made by the user + + https://bybit-exchange.github.io/docs/api-explorer/v5/position/execution + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: market type, ['swap', 'option', 'spot'] + :param str [params.subType]: market subType, ['linear', 'inverse'] + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchMyTrades', 'paginate') + if paginate: + return self.fetch_paginated_call_cursor('fetchMyTrades', symbol, since, limit, params, 'nextPageCursor', 'cursor', None, 100) + request: dict = { + 'execType': 'Trade', + } + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + type = None + type, params = self.get_bybit_type('fetchMyTrades', market, params) + request['category'] = type + if limit is not None: + request['limit'] = limit + if since is not None: + request['startTime'] = since + request, params = self.handle_until_option('endTime', request, params) + response = self.privateGetV5ExecutionList(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "nextPageCursor": "132766%3A2%2C132766%3A2", + # "category": "linear", + # "list": [ + # { + # "symbol": "ETHPERP", + # "orderType": "Market", + # "underlyingPrice": "", + # "orderLinkId": "", + # "side": "Buy", + # "indexPrice": "", + # "orderId": "8c065341-7b52-4ca9-ac2c-37e31ac55c94", + # "stopOrderType": "UNKNOWN", + # "leavesQty": "0", + # "execTime": "1672282722429", + # "isMaker": False, + # "execFee": "0.071409", + # "feeRate": "0.0006", + # "execId": "e0cbe81d-0f18-5866-9415-cf319b5dab3b", + # "tradeIv": "", + # "blockTradeId": "", + # "markPrice": "1183.54", + # "execPrice": "1190.15", + # "markIv": "", + # "orderQty": "0.1", + # "orderPrice": "1236.9", + # "execValue": "119.015", + # "execType": "Trade", + # "execQty": "0.1" + # } + # ] + # }, + # "retExtInfo": {}, + # "time": 1672283754510 + # } + # + trades = self.add_pagination_cursor_to_result(response) + return self.parse_trades(trades, market, since, limit) + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + # + # { + # "chainType": "ERC20", + # "addressDeposit": "0xf56297c6717c1d1c42c30324468ed50a9b7402ee", + # "tagDeposit": '', + # "chain": "ETH" + # } + # + address = self.safe_string(depositAddress, 'addressDeposit') + tag = self.safe_string(depositAddress, 'tagDeposit') + code = self.safe_string(currency, 'code') + self.check_address(address) + return { + 'info': depositAddress, + 'currency': code, + 'network': self.network_id_to_code(self.safe_string(depositAddress, 'chain'), code), + 'address': address, + 'tag': tag, + } + + def fetch_deposit_addresses_by_network(self, code: str, params={}) -> List[DepositAddress]: + """ + fetch a dictionary of addresses for a currency, indexed by network + + https://bybit-exchange.github.io/docs/v5/asset/master-deposit-addr + + :param str code: unified currency code of the currency for the deposit address + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `address structures ` indexed by the network + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'coin': currency['id'], + } + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + if networkCode is not None: + request['chainType'] = self.network_code_to_id(networkCode, code) + response = self.privateGetV5AssetDepositQueryAddress(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "success", + # "result": { + # "coin": "USDT", + # "chains": [ + # { + # "chainType": "ERC20", + # "addressDeposit": "0xd9e1cd77afa0e50b452a62fbb68a3340602286c3", + # "tagDeposit": "", + # "chain": "ETH" + # } + # ] + # }, + # "retExtInfo": {}, + # "time": 1672192792860 + # } + # + result = self.safe_dict(response, 'result', {}) + chains = self.safe_list(result, 'chains', []) + coin = self.safe_string(result, 'coin') + currency = self.currency(coin) + parsed = self.parse_deposit_addresses(chains, [currency['code']], False, { + 'currency': currency['code'], + }) + return self.index_by(parsed, 'network') + + def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://bybit-exchange.github.io/docs/v5/asset/master-deposit-addr + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + self.load_markets() + currency = self.currency(code) + networkCode, paramsOmited = self.handle_network_code_and_params(params) + indexedAddresses = self.fetch_deposit_addresses_by_network(code, paramsOmited) + selectedNetworkCode = self.select_network_code_from_unified_networks(currency['code'], networkCode, indexedAddresses) + return indexedAddresses[selectedNetworkCode] + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://bybit-exchange.github.io/docs/v5/asset/deposit-record + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for, default = 30 days before the current time + :param int [limit]: the maximum number of deposits structures to retrieve, default = 50, max = 50 + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch deposits for, default = 30 days after since + EXCHANGE SPECIFIC PARAMETERS + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param str [params.cursor]: used for pagination + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchDeposits', 'paginate') + if paginate: + return self.fetch_paginated_call_cursor('fetchDeposits', code, since, limit, params, 'nextPageCursor', 'cursor', None, 50) + request: dict = { + # 'coin': currency['id'], + # 'limit': 20, # max 50 + # 'cursor': '', + } + currency = None + if code is not None: + currency = self.currency(code) + request['coin'] = currency['id'] + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + request, params = self.handle_until_option('endTime', request, params) + response = self.privateGetV5AssetDepositQueryRecord(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "success", + # "result": { + # "rows": [ + # { + # "coin": "USDT", + # "chain": "ETH", + # "amount": "10000", + # "txID": "skip-notification-scene-test-amount-202212270944-533285-USDT", + # "status": 3, + # "toAddress": "test-amount-address", + # "tag": "", + # "depositFee": "", + # "successAt": "1672134274000", + # "confirmations": "10000", + # "txIndex": "", + # "blockHash": "" + # } + # ], + # "nextPageCursor": "eyJtaW5JRCI6MTA0NjA0MywibWF4SUQiOjEwNDYwNDN9" + # }, + # "retExtInfo": {}, + # "time": 1672191992512 + # } + # + data = self.add_pagination_cursor_to_result(response) + return self.parse_transactions(data, currency, since, limit) + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://bybit-exchange.github.io/docs/v5/asset/withdraw-record + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch entries for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchWithdrawals', 'paginate') + if paginate: + return self.fetch_paginated_call_cursor('fetchWithdrawals', code, since, limit, params, 'nextPageCursor', 'cursor', None, 50) + request: dict = { + # 'coin': currency['id'], + # 'limit': 20, # max 50 + # 'cusor': '', + } + currency = None + if code is not None: + currency = self.currency(code) + request['coin'] = currency['id'] + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + request, params = self.handle_until_option('endTime', request, params) + response = self.privateGetV5AssetWithdrawQueryRecord(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "success", + # "result": { + # "rows": [ + # { + # "coin": "USDT", + # "chain": "ETH", + # "amount": "77", + # "txID": "", + # "status": "SecurityCheck", + # "toAddress": "0x99ced129603abc771c0dabe935c326ff6c86645d", + # "tag": "", + # "withdrawFee": "10", + # "createTime": "1670922217000", + # "updateTime": "1670922217000", + # "withdrawId": "9976", + # "withdrawType": 0 + # }, + # { + # "coin": "USDT", + # "chain": "ETH", + # "amount": "26", + # "txID": "", + # "status": "success", + # "toAddress": "15638072681@163.com", + # "tag": "", + # "withdrawFee": "0", + # "createTime": "1669711121000", + # "updateTime": "1669711380000", + # "withdrawId": "9801", + # "withdrawType": 1 + # } + # ], + # "nextPageCursor": "eyJtaW5JRCI6OTgwMSwibWF4SUQiOjk5NzZ9" + # }, + # "retExtInfo": {}, + # "time": 1672194949928 + # } + # + data = self.add_pagination_cursor_to_result(response) + return self.parse_transactions(data, currency, since, limit) + + def parse_transaction_status(self, status: Str): + statuses: dict = { + # v3 deposit status + '0': 'unknown', + '1': 'pending', + '2': 'processing', + '3': 'ok', + '4': 'fail', + # v3 withdrawal status + 'SecurityCheck': 'pending', + 'Pending': 'pending', + 'success': 'ok', + 'CancelByUser': 'canceled', + 'Reject': 'rejected', + 'Fail': 'failed', + 'BlockchainConfirmed': 'ok', + } + return self.safe_string(statuses, status, status) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fetchWithdrawals + # + # { + # "coin": "USDT", + # "chain": "TRX", + # "amount": "12.34", + # "txID": "de5ea0a2f2e59dc9a714837dd3ddc6d5e151b56ec5d786d351c4f52336f80d3c", + # "status": "success", + # "toAddress": "TQdmFKUoe1Lk2iwZuwRJEHJreTUBoN3BAw", + # "tag": "", + # "withdrawFee": "0.5", + # "createTime": "1665144183000", + # "updateTime": "1665144256000", + # "withdrawId": "8839035" + # } + # + # fetchDeposits + # + # { + # "coin": "USDT", + # "chain": "TRX", + # "amount": "44", + # "txID": "0b038ea12fa1575e2d66693db3c346b700d4b28347afc39f80321cf089acc960", + # "status": "3", + # "toAddress": "TC6NCAC5WSVCCiaD3kWZXyW91ZKKhLm53b", + # "tag": "", + # "depositFee": "", + # "successAt": "1665142507000", + # "confirmations": "100", + # "txIndex": "0", + # "blockHash": "0000000002ac3b1064aee94bca1bd0b58c4c09c65813b084b87a2063d961129e" + # } + # + # withdraw + # + # { + # "id": "9377266" + # } + # + currencyId = self.safe_string(transaction, 'coin') + code = self.safe_currency_code(currencyId, currency) + timestamp = self.safe_integer_2(transaction, 'createTime', 'successAt') + updated = self.safe_integer(transaction, 'updateTime') + status = self.parse_transaction_status(self.safe_string(transaction, 'status')) + feeCost = self.safe_number_2(transaction, 'depositFee', 'withdrawFee') + type = 'deposit' if ('depositFee' in transaction) else 'withdrawal' + fee = None + if feeCost is not None: + fee = { + 'cost': feeCost, + 'currency': code, + } + toAddress = self.safe_string(transaction, 'toAddress') + return { + 'info': transaction, + 'id': self.safe_string_2(transaction, 'id', 'withdrawId'), + 'txid': self.safe_string(transaction, 'txID'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': self.network_id_to_code(self.safe_string(transaction, 'chain')), + 'address': None, + 'addressTo': toAddress, + 'addressFrom': None, + 'tag': self.safe_string(transaction, 'tag'), + 'tagTo': None, + 'tagFrom': None, + 'type': type, + 'amount': self.safe_number(transaction, 'amount'), + 'currency': code, + 'status': status, + 'updated': updated, + 'fee': fee, + 'internal': None, + 'comment': None, + } + + def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + + https://bybit-exchange.github.io/docs/v5/account/transaction-log + https://bybit-exchange.github.io/docs/v5/account/contract-transaction-log + + :param str [code]: unified currency code, default is None + :param int [since]: timestamp in ms of the earliest ledger entry, default is None + :param int [limit]: max number of ledger entries to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param str [params.subType]: if inverse will use v5/account/contract-transaction-log + :returns dict: a `ledger structure ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchLedger', 'paginate') + if paginate: + return self.fetch_paginated_call_cursor('fetchLedger', code, since, limit, params, 'nextPageCursor', 'cursor', None, 50) + request: dict = { + # 'coin': currency['id'], + # 'currency': currency['id'], # alias + # 'start_date': self.iso8601(since), + # 'end_date': self.iso8601(until), + # 'wallet_fund_type': 'Deposit', # Withdraw, RealisedPNL, Commission, Refund, Prize, ExchangeOrderWithdraw, ExchangeOrderDeposit + # 'page': 1, + # 'limit': 20, # max 50 + # v5 transaction log + # 'accountType': '', Account Type. UNIFIED + # 'category': '', Product type. spot,linear,option + # 'currency': '', Currency + # 'baseCoin': '', BaseCoin. e.g., BTC of BTCPERP + # 'type': '', Types of transaction logs + # 'startTime': 0, The start timestamp(ms) + # 'endTime': 0, The end timestamp(ms) + # 'limit': 0, Limit for data size per page. [1, 50]. Default: 20 + # 'cursor': '', Cursor. Used for pagination + } + enableUnified = self.is_unified_enabled() + currency = None + currencyKey = 'coin' + if enableUnified[1]: + currencyKey = 'currency' + if since is not None: + request['startTime'] = since + else: + if since is not None: + request['start_date'] = self.yyyymmdd(since) + if code is not None: + currency = self.currency(code) + request[currencyKey] = currency['id'] + if limit is not None: + request['limit'] = limit + subType = None + subType, params = self.handle_sub_type_and_params('fetchLedger', None, params) + response = None + if enableUnified[1]: + unifiedMarginStatus = self.safe_integer(self.options, 'unifiedMarginStatus', 5) # 3/4 uta 1.0, 5/6 uta 2.0 + if subType == 'inverse' and (unifiedMarginStatus < 5): + response = self.privateGetV5AccountContractTransactionLog(self.extend(request, params)) + else: + response = self.privateGetV5AccountTransactionLog(self.extend(request, params)) + else: + response = self.privateGetV5AccountContractTransactionLog(self.extend(request, params)) + # + # { + # "ret_code": 0, + # "ret_msg": "ok", + # "ext_code": "", + # "result": { + # "data": [ + # { + # "id": 234467, + # "user_id": 1, + # "coin": "BTC", + # "wallet_id": 27913, + # "type": "Realized P&L", + # "amount": "-0.00000006", + # "tx_id": "", + # "address": "BTCUSD", + # "wallet_balance": "0.03000330", + # "exec_time": "2019-12-09T00:00:25.000Z", + # "cross_seq": 0 + # } + # ] + # }, + # "ext_info": null, + # "time_now": "1577481867.115552", + # "rate_limit_status": 119, + # "rate_limit_reset_ms": 1577481867122, + # "rate_limit": 120 + # } + # + # v5 transaction log + # + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "nextPageCursor": "21963%3A1%2C14954%3A1", + # "list": [ + # { + # "symbol": "XRPUSDT", + # "side": "Buy", + # "funding": "-0.003676", + # "orderLinkId": "", + # "orderId": "1672128000-8-592324-1-2", + # "fee": "0.00000000", + # "change": "-0.003676", + # "cashFlow": "0", + # "transactionTime": "1672128000000", + # "type": "SETTLEMENT", + # "feeRate": "0.0001", + # "size": "100", + # "qty": "100", + # "cashBalance": "5086.55825002", + # "currency": "USDT", + # "category": "linear", + # "tradePrice": "0.3676", + # "tradeId": "534c0003-4bf7-486f-aa02-78cee36825e4" + # }, + # { + # "symbol": "XRPUSDT", + # "side": "Buy", + # "funding": "", + # "orderLinkId": "linear-order", + # "orderId": "592b7e41-78fd-42e2-9aa3-91e1835ef3e1", + # "fee": "0.01908720", + # "change": "-0.0190872", + # "cashFlow": "0", + # "transactionTime": "1672121182224", + # "type": "TRADE", + # "feeRate": "0.0006", + # "size": "100", + # "qty": "88", + # "cashBalance": "5086.56192602", + # "currency": "USDT", + # "category": "linear", + # "tradePrice": "0.3615", + # "tradeId": "5184f079-88ec-54c7-8774-5173cafd2b4e" + # }, + # { + # "symbol": "XRPUSDT", + # "side": "Buy", + # "funding": "", + # "orderLinkId": "linear-order", + # "orderId": "592b7e41-78fd-42e2-9aa3-91e1835ef3e1", + # "fee": "0.00260280", + # "change": "-0.0026028", + # "cashFlow": "0", + # "transactionTime": "1672121182224", + # "type": "TRADE", + # "feeRate": "0.0006", + # "size": "12", + # "qty": "12", + # "cashBalance": "5086.58101322", + # "currency": "USDT", + # "category": "linear", + # "tradePrice": "0.3615", + # "tradeId": "8569c10f-5061-5891-81c4-a54929847eb3" + # } + # ] + # }, + # "retExtInfo": {}, + # "time": 1672132481405 + # } + # + data = self.add_pagination_cursor_to_result(response) + return self.parse_ledger(data, currency, since, limit) + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + # + # { + # "id": 234467, + # "user_id": 1, + # "coin": "BTC", + # "wallet_id": 27913, + # "type": "Realized P&L", + # "amount": "-0.00000006", + # "tx_id": "", + # "address": "BTCUSD", + # "wallet_balance": "0.03000330", + # "exec_time": "2019-12-09T00:00:25.000Z", + # "cross_seq": 0 + # } + # + # { + # "symbol": "XRPUSDT", + # "side": "Buy", + # "funding": "", + # "orderLinkId": "linear-order", + # "orderId": "592b7e41-78fd-42e2-9aa3-91e1835ef3e1", + # "fee": "0.00260280", + # "change": "-0.0026028", + # "cashFlow": "0", + # "transactionTime": "1672121182224", + # "type": "TRADE", + # "feeRate": "0.0006", + # "size": "12", + # "qty": "12", + # "cashBalance": "5086.58101322", + # "currency": "USDT", + # "category": "linear", + # "tradePrice": "0.3615", + # "tradeId": "8569c10f-5061-5891-81c4-a54929847eb3" + # } + # + currencyId = self.safe_string_2(item, 'coin', 'currency') + code = self.safe_currency_code(currencyId, currency) + currency = self.safe_currency(currencyId, currency) + amountString = self.safe_string_2(item, 'amount', 'change') + afterString = self.safe_string_2(item, 'wallet_balance', 'cashBalance') + direction = 'out' if Precise.string_lt(amountString, '0') else 'in' + before = None + after = None + amount = None + if afterString is not None and amountString is not None: + difference = amountString if (direction == 'out') else Precise.string_neg(amountString) + before = self.parse_to_numeric(Precise.string_add(afterString, difference)) + after = self.parse_to_numeric(afterString) + amount = self.parse_to_numeric(Precise.string_abs(amountString)) + timestamp = self.parse8601(self.safe_string(item, 'exec_time')) + if timestamp is None: + timestamp = self.safe_integer(item, 'transactionTime') + return self.safe_ledger_entry({ + 'info': item, + 'id': self.safe_string(item, 'id'), + 'direction': direction, + 'account': self.safe_string(item, 'wallet_id'), + 'referenceId': self.safe_string(item, 'tx_id'), + 'referenceAccount': None, + 'type': self.parse_ledger_entry_type(self.safe_string(item, 'type')), + 'currency': code, + 'amount': amount, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'before': before, + 'after': after, + 'status': 'ok', + 'fee': { + 'currency': code, + 'cost': self.safe_number(item, 'fee'), + }, + }, currency) + + def parse_ledger_entry_type(self, type): + types: dict = { + 'Deposit': 'transaction', + 'Withdraw': 'transaction', + 'RealisedPNL': 'trade', + 'Commission': 'fee', + 'Refund': 'cashback', + 'Prize': 'prize', # ? + 'ExchangeOrderWithdraw': 'transaction', + 'ExchangeOrderDeposit': 'transaction', + # v5 + 'TRANSFER_IN': 'transaction', + 'TRANSFER_OUT': 'transaction', + 'TRADE': 'trade', + 'SETTLEMENT': 'trade', + 'DELIVERY': 'trade', + 'LIQUIDATION': 'trade', + 'BONUS': 'Prize', + 'FEE_REFUND': 'cashback', + 'INTEREST': 'transaction', + 'CURRENCY_BUY': 'trade', + 'CURRENCY_SELL': 'trade', + } + return self.safe_string(types, type, type) + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://bybit-exchange.github.io/docs/v5/asset/withdraw + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.accountType]: 'UTA', 'FUND', 'FUND,UTA', and 'SPOT(for classic accounts only) + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + accountType = None + accounts = self.is_unified_enabled() + isUta = accounts[1] + accountType, params = self.handle_option_and_params(params, 'withdraw', 'accountType') + if accountType is None: + accountType = 'UTA' if isUta else 'SPOT' + self.load_markets() + self.check_address(address) + currency = self.currency(code) + request: dict = { + 'coin': currency['id'], + 'amount': self.number_to_string(amount), + 'address': address, + 'timestamp': self.milliseconds(), + 'accountType': accountType, + } + if tag is not None: + request['tag'] = tag + networkCode, query = self.handle_network_code_and_params(params) + networkId = self.network_code_to_id(networkCode) + if networkId is not None: + request['chain'] = networkId.upper() + response = self.privatePostV5AssetWithdrawCreate(self.extend(request, query)) + # + # { + # "retCode": "0", + # "retMsg": "success", + # "result": { + # "id": "9377266" + # }, + # "retExtInfo": {}, + # "time": "1666892894902" + # } + # + result = self.safe_dict(response, 'result', {}) + return self.parse_transaction(result, currency) + + def fetch_position(self, symbol: str, params={}) -> Position: + """ + fetch data on a single open contract trade position + + https://bybit-exchange.github.io/docs/v5/position + + :param str symbol: unified market symbol of the market the position is held in, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `position structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchPosition() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = None + type = None + type, params = self.get_bybit_type('fetchPosition', market, params) + request['category'] = type + response = self.privateGetV5PositionList(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "nextPageCursor": "updateAt%3D1672279322668", + # "category": "linear", + # "list": [ + # { + # "symbol": "XRPUSDT", + # "leverage": "10", + # "avgPrice": "0.3615", + # "liqPrice": "0.0001", + # "riskLimitValue": "200000", + # "takeProfit": "", + # "positionValue": "36.15", + # "tpslMode": "Full", + # "riskId": 41, + # "trailingStop": "0", + # "unrealisedPnl": "-1.83", + # "markPrice": "0.3432", + # "cumRealisedPnl": "0.48805876", + # "positionMM": "0.381021", + # "createdTime": "1672121182216", + # "positionIdx": 0, + # "positionIM": "3.634521", + # "updatedTime": "1672279322668", + # "side": "Buy", + # "bustPrice": "", + # "size": "100", + # "positionStatus": "Normal", + # "stopLoss": "", + # "tradeMode": 0 + # } + # ] + # }, + # "retExtInfo": {}, + # "time": 1672280219169 + # } + # + result = self.safe_dict(response, 'result', {}) + positions = self.safe_list_2(result, 'list', 'dataList', []) + timestamp = self.safe_integer(response, 'time') + first = self.safe_dict(positions, 0, {}) + position = self.parse_position(first, market) + position['timestamp'] = timestamp + position['datetime'] = self.iso8601(timestamp) + return position + + def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://bybit-exchange.github.io/docs/v5/position + + :param str[] symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: market type, ['swap', 'option', 'spot'] + :param str [params.subType]: market subType, ['linear', 'inverse'] + :param str [params.baseCoin]: Base coin. Supports linear, inverse & option + :param str [params.settleCoin]: Settle coin. Supports linear, inverse & option + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times + :returns dict[]: a list of `position structure ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchPositions', 'paginate') + if paginate: + return self.fetch_paginated_call_cursor('fetchPositions', symbols, None, None, params, 'nextPageCursor', 'cursor', None, 200) + symbol = None + if (symbols is not None) and isinstance(symbols, list): + symbolsLength = len(symbols) + if symbolsLength > 1: + raise ArgumentsRequired(self.id + ' fetchPositions() does not accept an array with more than one symbol') + elif symbolsLength == 1: + symbol = symbols[0] + symbols = self.market_symbols(symbols) + elif symbols is not None: + symbol = symbols + symbols = [self.symbol(symbol)] + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + request['symbol'] = market['id'] + type = None + type, params = self.get_bybit_type('fetchPositions', market, params) + if type == 'linear' or type == 'inverse': + baseCoin = self.safe_string(params, 'baseCoin') + if type == 'linear': + if symbol is None and baseCoin is None: + defaultSettle = self.safe_string(self.options, 'defaultSettle', 'USDT') + settleCoin = self.safe_string(params, 'settleCoin', defaultSettle) + request['settleCoin'] = settleCoin + else: + # inverse + if symbol is None and baseCoin is None: + request['category'] = 'inverse' + if self.safe_integer(params, 'limit') is None: + request['limit'] = 200 # max limit + params = self.omit(params, ['type']) + request['category'] = type + response = self.privateGetV5PositionList(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "Success", + # "result": { + # "nextPageCursor": "0%3A1657711949945%2C0%3A1657711949945", + # "category": "linear", + # "list": [ + # { + # "symbol": "ETHUSDT", + # "leverage": "10", + # "updatedTime": 1657711949945, + # "side": "Buy", + # "positionValue": "536.92500000", + # "takeProfit": "", + # "tpslMode": "Full", + # "riskId": 11, + # "trailingStop": "", + # "entryPrice": "1073.85000000", + # "unrealisedPnl": "", + # "markPrice": "1080.65000000", + # "size": "0.5000", + # "positionStatus": "normal", + # "stopLoss": "", + # "cumRealisedPnl": "-0.32215500", + # "positionMM": "2.97456450", + # "createdTime": 1657711949928, + # "positionIdx": 0, + # "positionIM": "53.98243950" + # } + # ] + # }, + # "time": 1657713693182 + # } + # + positions = self.add_pagination_cursor_to_result(response) + results = [] + for i in range(0, len(positions)): + rawPosition = positions[i] + if ('data' in rawPosition) and ('is_valid' in rawPosition): + # futures only + rawPosition = self.safe_dict(rawPosition, 'data') + results.append(self.parse_position(rawPosition)) + return self.filter_by_array_positions(results, 'symbol', symbols, False) + + def parse_position(self, position: dict, market: Market = None) -> Position: + # + # linear swap + # + # { + # "positionIdx": 0, + # "riskId": "11", + # "symbol": "ETHUSDT", + # "side": "Buy", + # "size": "0.10", + # "positionValue": "119.845", + # "entryPrice": "1198.45", + # "tradeMode": 1, + # "autoAddMargin": 0, + # "leverage": "4.2", + # "positionBalance": "28.58931118", + # "liqPrice": "919.10", + # "bustPrice": "913.15", + # "takeProfit": "0.00", + # "stopLoss": "0.00", + # "trailingStop": "0.00", + # "unrealisedPnl": "0.083", + # "createdTime": "1669097244192", + # "updatedTime": "1669413126190", + # "tpSlMode": "Full", + # "riskLimitValue": "900000", + # "activePrice": "0.00" + # } + # + # usdc + # { + # "symbol":"BTCPERP", + # "leverage":"1.00", + # "occClosingFee":"0.0000", + # "liqPrice":"", + # "positionValue":"30.8100", + # "takeProfit":"0.0", + # "riskId":"10001", + # "trailingStop":"0.0000", + # "unrealisedPnl":"0.0000", + # "createdAt":"1652451795305", + # "markPrice":"30809.41", + # "cumRealisedPnl":"0.0000", + # "positionMM":"0.1541", + # "positionIM":"30.8100", + # "updatedAt":"1652451795305", + # "tpSLMode":"UNKNOWN", + # "side":"Buy", + # "bustPrice":"", + # "deleverageIndicator":"0", + # "entryPrice":"30810.0", + # "size":"0.001", + # "sessionRPL":"0.0000", + # "positionStatus":"NORMAL", + # "sessionUPL":"-0.0006", + # "stopLoss":"0.0", + # "orderMargin":"0.0000", + # "sessionAvgPrice":"30810.0" + # } + # + # unified margin + # + # { + # "symbol": "ETHUSDT", + # "leverage": "10", + # "updatedTime": 1657711949945, + # "side": "Buy", + # "positionValue": "536.92500000", + # "takeProfit": "", + # "tpslMode": "Full", + # "riskId": 11, + # "trailingStop": "", + # "entryPrice": "1073.85000000", + # "unrealisedPnl": "", + # "markPrice": "1080.65000000", + # "size": "0.5000", + # "positionStatus": "normal", + # "stopLoss": "", + # "cumRealisedPnl": "-0.32215500", + # "positionMM": "2.97456450", + # "createdTime": 1657711949928, + # "positionIdx": 0, + # "positionIM": "53.98243950" + # } + # + # unified account + # + # { + # "symbol": "XRPUSDT", + # "leverage": "10", + # "avgPrice": "0.3615", + # "liqPrice": "0.0001", + # "riskLimitValue": "200000", + # "takeProfit": "", + # "positionValue": "36.15", + # "tpslMode": "Full", + # "riskId": 41, + # "trailingStop": "0", + # "unrealisedPnl": "-1.83", + # "markPrice": "0.3432", + # "cumRealisedPnl": "0.48805876", + # "positionMM": "0.381021", + # "createdTime": "1672121182216", + # "positionIdx": 0, + # "positionIM": "3.634521", + # "updatedTime": "1672279322668", + # "side": "Buy", + # "bustPrice": "", + # "size": "100", + # "positionStatus": "Normal", + # "stopLoss": "", + # "tradeMode": 0 + # } + # + # fetchPositionsHistory + # + # { + # symbol: 'XRPUSDT', + # orderType: 'Market', + # leverage: '10', + # updatedTime: '1712717265572', + # side: 'Sell', + # orderId: '071749f3-a9fa-427b-b5ca-27b2f52b81de', + # closedPnl: '-0.00049568', + # avgEntryPrice: '0.6045', + # qty: '3', + # cumEntryValue: '1.8135', + # createdTime: '1712717265566', + # orderPrice: '0.5744', + # closedSize: '3', + # avgExitPrice: '0.605', + # execType: 'Trade', + # fillCount: '1', + # cumExitValue: '1.815' + # } + # + closedSize = self.safe_string(position, 'closedSize') + isHistory = (closedSize is not None) + contract = self.safe_string(position, 'symbol') + market = self.safe_market(contract, market, None, 'contract') + size = Precise.string_abs(self.safe_string_2(position, 'size', 'qty')) + side = self.safe_string(position, 'side') + if side is not None: + if side == 'Buy': + side = 'short' if isHistory else 'long' + elif side == 'Sell': + side = 'long' if isHistory else 'short' + else: + side = None + notional = self.safe_string_2(position, 'positionValue', 'cumExitValue') + unrealisedPnl = self.omit_zero(self.safe_string(position, 'unrealisedPnl')) + initialMarginString = self.safe_string_2(position, 'positionIM', 'cumEntryValue') + maintenanceMarginString = self.safe_string(position, 'positionMM') + timestamp = self.safe_integer_n(position, ['createdTime', 'createdAt']) + lastUpdateTimestamp = self.parse8601(self.safe_string(position, 'updated_at')) + if lastUpdateTimestamp is None: + lastUpdateTimestamp = self.safe_integer_n(position, ['updatedTime', 'updatedAt', 'updatedTime']) + tradeMode = self.safe_integer(position, 'tradeMode', 0) + marginMode = None + if (not self.options['enableUnifiedAccount']) or (self.options['enableUnifiedAccount'] and market['inverse']): + # tradeMode would work for classic and UTA(inverse) + if not isHistory: # cannot tell marginMode for fetchPositionsHistory, and closedSize will only be defined for fetchPositionsHistory response + marginMode = 'isolated' if (tradeMode == 1) else 'cross' + collateralString = self.safe_string(position, 'positionBalance') + entryPrice = self.omit_zero(self.safe_string_n(position, ['entryPrice', 'avgPrice', 'avgEntryPrice'])) + markPrice = self.safe_string(position, 'markPrice') + liquidationPrice = self.omit_zero(self.safe_string(position, 'liqPrice')) + leverage = self.safe_string(position, 'leverage') + if liquidationPrice is not None: + if market['settle'] == 'USDC': + # (Entry price - Liq price) * Contracts + Maintenance Margin + (unrealised pnl) = Collateral + price = markPrice if self.safe_bool(self.options, 'useMarkPriceForPositionCollateral', False) else entryPrice + difference = Precise.string_abs(Precise.string_sub(price, liquidationPrice)) + collateralString = Precise.string_add(Precise.string_add(Precise.string_mul(difference, size), maintenanceMarginString), unrealisedPnl) + else: + bustPrice = self.safe_string(position, 'bustPrice') + if market['linear']: + # derived from the following formulas + # (Entry price - Bust price) * Contracts = Collateral + # (Entry price - Liq price) * Contracts = Collateral - Maintenance Margin + # Maintenance Margin = (Bust price - Liq price) x Contracts + maintenanceMarginPriceDifference = Precise.string_abs(Precise.string_sub(liquidationPrice, bustPrice)) + maintenanceMarginString = Precise.string_mul(maintenanceMarginPriceDifference, size) + # Initial Margin = Contracts x Entry Price / Leverage + if (entryPrice is not None) and (initialMarginString is None): + initialMarginString = Precise.string_div(Precise.string_mul(size, entryPrice), leverage) + else: + # Contracts * (1 / Entry price - 1 / Bust price) = Collateral + # Contracts * (1 / Entry price - 1 / Liq price) = Collateral - Maintenance Margin + # Maintenance Margin = Contracts * (1 / Liq price - 1 / Bust price) + # Maintenance Margin = Contracts * (Bust price - Liq price) / (Liq price x Bust price) + difference = Precise.string_abs(Precise.string_sub(bustPrice, liquidationPrice)) + multiply = Precise.string_mul(bustPrice, liquidationPrice) + maintenanceMarginString = Precise.string_div(Precise.string_mul(size, difference), multiply) + # Initial Margin = Leverage x Contracts / EntryPrice + if (entryPrice is not None) and (initialMarginString is None): + initialMarginString = Precise.string_div(size, Precise.string_mul(entryPrice, leverage)) + maintenanceMarginPercentage = Precise.string_div(maintenanceMarginString, notional) + marginRatio = Precise.string_div(maintenanceMarginString, collateralString, 4) + positionIdx = self.safe_string(position, 'positionIdx') + hedged = (positionIdx is not None) and (positionIdx != '0') + return self.safe_position({ + 'info': position, + 'id': None, + 'symbol': market['symbol'], + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastUpdateTimestamp': lastUpdateTimestamp, + 'initialMargin': self.parse_number(initialMarginString), + 'initialMarginPercentage': self.parse_number(Precise.string_div(initialMarginString, notional)), + 'maintenanceMargin': self.parse_number(maintenanceMarginString), + 'maintenanceMarginPercentage': self.parse_number(maintenanceMarginPercentage), + 'entryPrice': self.parse_number(entryPrice), + 'notional': self.parse_number(notional), + 'leverage': self.parse_number(leverage), + 'unrealizedPnl': self.parse_number(unrealisedPnl), + 'realizedPnl': self.safe_number(position, 'closedPnl'), + 'contracts': self.parse_number(size), # in USD for inverse swaps + 'contractSize': self.safe_number(market, 'contractSize'), + 'marginRatio': self.parse_number(marginRatio), + 'liquidationPrice': self.parse_number(liquidationPrice), + 'markPrice': self.parse_number(markPrice), + 'lastPrice': self.safe_number(position, 'avgExitPrice'), + 'collateral': self.parse_number(collateralString), + 'marginMode': marginMode, + 'side': side, + 'percentage': None, + 'stopLossPrice': self.safe_number_2(position, 'stop_loss', 'stopLoss'), + 'takeProfitPrice': self.safe_number_2(position, 'take_profit', 'takeProfit'), + 'hedged': hedged, + }) + + def fetch_leverage(self, symbol: str, params={}) -> Leverage: + """ + fetch the set leverage for a market + + https://bybit-exchange.github.io/docs/v5/position + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `leverage structure ` + """ + self.load_markets() + market = self.market(symbol) + position = self.fetch_position(symbol, params) + return self.parse_leverage(position, market) + + def parse_leverage(self, leverage: dict, market: Market = None) -> Leverage: + marketId = self.safe_string(leverage, 'symbol') + leverageValue = self.safe_integer(leverage, 'leverage') + return { + 'info': leverage, + 'symbol': self.safe_symbol(marketId, market), + 'marginMode': self.safe_string_lower(leverage, 'marginMode'), + 'longLeverage': leverageValue, + 'shortLeverage': leverageValue, + } + + def set_margin_mode(self, marginMode: str, symbol: Str = None, params={}): + """ + set margin mode(account) or trade mode(symbol) + + https://bybit-exchange.github.io/docs/v5/account/set-margin-mode + https://bybit-exchange.github.io/docs/v5/position/cross-isolate + + :param str marginMode: account mode must be either [isolated, cross, portfolio], trade mode must be either [isolated, cross] + :param str symbol: unified market symbol of the market the position is held in, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.leverage]: the rate of leverage, is required if setting trade mode(symbol) + :returns dict: response from the exchange + """ + self.load_markets() + enableUnifiedMargin, enableUnifiedAccount = self.is_unified_enabled() + isUnifiedAccount = (enableUnifiedMargin or enableUnifiedAccount) + market = None + response = None + if isUnifiedAccount: + if marginMode == 'isolated': + marginMode = 'ISOLATED_MARGIN' + elif marginMode == 'cross': + marginMode = 'REGULAR_MARGIN' + elif marginMode == 'portfolio': + marginMode = 'PORTFOLIO_MARGIN' + else: + raise NotSupported(self.id + ' setMarginMode() marginMode must be either [isolated, cross, portfolio]') + request: dict = { + 'setMarginMode': marginMode, + } + response = self.privatePostV5AccountSetMarginMode(self.extend(request, params)) + else: + if symbol is None: + raise ArgumentsRequired(self.id + ' setMarginMode() requires a symbol parameter for non unified account') + market = self.market(symbol) + isUsdcSettled = market['settle'] == 'USDC' + if isUsdcSettled: + if marginMode == 'cross': + marginMode = 'REGULAR_MARGIN' + elif marginMode == 'portfolio': + marginMode = 'PORTFOLIO_MARGIN' + else: + raise NotSupported(self.id + ' setMarginMode() for usdc market marginMode must be either [cross, portfolio]') + request: dict = { + 'setMarginMode': marginMode, + } + response = self.privatePostV5AccountSetMarginMode(self.extend(request, params)) + else: + type = None + type, params = self.get_bybit_type('setPositionMode', market, params) + tradeMode = None + if marginMode == 'cross': + tradeMode = 0 + elif marginMode == 'isolated': + tradeMode = 1 + else: + raise NotSupported(self.id + ' setMarginMode() with symbol marginMode must be either [isolated, cross]') + sellLeverage = None + buyLeverage = None + leverage = self.safe_string(params, 'leverage') + if leverage is None: + sellLeverage = self.safe_string_2(params, 'sell_leverage', 'sellLeverage') + buyLeverage = self.safe_string_2(params, 'buy_leverage', 'buyLeverage') + if sellLeverage is None and buyLeverage is None: + raise ArgumentsRequired(self.id + ' setMarginMode() requires a leverage parameter or sell_leverage and buy_leverage parameters') + if buyLeverage is None: + buyLeverage = sellLeverage + if sellLeverage is None: + sellLeverage = buyLeverage + params = self.omit(params, ['buy_leverage', 'sell_leverage', 'sellLeverage', 'buyLeverage']) + else: + sellLeverage = leverage + buyLeverage = leverage + params = self.omit(params, 'leverage') + request: dict = { + 'category': type, + 'symbol': market['id'], + 'tradeMode': tradeMode, + 'buyLeverage': buyLeverage, + 'sellLeverage': sellLeverage, + } + response = self.privatePostV5PositionSwitchIsolated(self.extend(request, params)) + return response + + def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://bybit-exchange.github.io/docs/v5/position/leverage + + :param float leverage: the rate of leverage + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.buyLeverage]: leverage for buy side + :param str [params.sellLeverage]: leverage for sell side + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setLeverage() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + # WARNING: THIS WILL INCREASE LIQUIDATION PRICE FOR OPEN ISOLATED LONG POSITIONS + # AND DECREASE LIQUIDATION PRICE FOR OPEN ISOLATED SHORT POSITIONS + # engage in leverage setting + # we reuse the code here instead of having two methods + leverageString = self.number_to_string(leverage) + request: dict = { + 'symbol': market['id'], + 'buyLeverage': leverageString, + 'sellLeverage': leverageString, + } + request['buyLeverage'] = leverageString + request['sellLeverage'] = leverageString + if market['linear']: + request['category'] = 'linear' + elif market['inverse']: + request['category'] = 'inverse' + else: + raise NotSupported(self.id + ' setLeverage() only support linear and inverse market') + response = self.privatePostV5PositionSetLeverage(self.extend(request, params)) + return response + + def set_position_mode(self, hedged: bool, symbol: Str = None, params={}): + """ + set hedged to True or False for a market + + https://bybit-exchange.github.io/docs/v5/position/position-mode + + :param bool hedged: + :param str symbol: used for unified account with inverse market + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + mode = None + if hedged: + mode = 3 + else: + mode = 0 + request: dict = { + 'mode': mode, + } + if symbol is None: + request['coin'] = 'USDT' + else: + request['symbol'] = market['id'] + if symbol is not None: + request['category'] = 'linear' if market['linear'] else 'inverse' + else: + type = None + type, params = self.get_bybit_type('setPositionMode', market, params) + request['category'] = type + params = self.omit(params, 'type') + response = self.privatePostV5PositionSwitchMode(self.extend(request, params)) + # + # v5 + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": {}, + # "retExtInfo": {}, + # "time": 1675249072814 + # } + return response + + def fetch_derivatives_open_interest_history(self, symbol: str, timeframe='1h', since: Int = None, limit: Int = None, params={}): + self.load_markets() + market = self.market(symbol) + subType = 'linear' if market['linear'] else 'inverse' + category = self.safe_string(params, 'category', subType) + intervals = self.safe_dict(self.options, 'intervals') + interval = self.safe_string(intervals, timeframe) # 5min,15min,30min,1h,4h,1d + if interval is None: + raise BadRequest(self.id + ' fetchOpenInterestHistory() cannot use the ' + timeframe + ' timeframe') + request: dict = { + 'symbol': market['id'], + 'intervalTime': interval, + 'category': category, + } + if since is not None: + request['startTime'] = since + until = self.safe_integer(params, 'until') # unified in milliseconds + params = self.omit(params, ['until']) + if until is not None: + request['endTime'] = until + if limit is not None: + request['limit'] = limit + response = self.publicGetV5MarketOpenInterest(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "symbol": "BTCUSD", + # "category": "inverse", + # "list": [ + # { + # "openInterest": "461134384.00000000", + # "timestamp": "1669571400000" + # }, + # { + # "openInterest": "461134292.00000000", + # "timestamp": "1669571100000" + # } + # ], + # "nextPageCursor": "" + # }, + # "retExtInfo": {}, + # "time": 1672053548579 + # } + # + result = self.safe_dict(response, 'result', {}) + data = self.add_pagination_cursor_to_result(response) + id = self.safe_string(result, 'symbol') + market = self.safe_market(id, market, None, 'contract') + return self.parse_open_interests_history(data, market, since, limit) + + def fetch_open_interest(self, symbol: str, params={}): + """ + Retrieves the open interest of a derivative trading pair + + https://bybit-exchange.github.io/docs/v5/market/open-interest + + :param str symbol: Unified CCXT market symbol + :param dict [params]: exchange specific parameters + :param str [params.interval]: 5m, 15m, 30m, 1h, 4h, 1d + :param str [params.category]: "linear" or "inverse" + :returns dict} an open interest structure{@link https://docs.ccxt.com/#/?id=open-interest-structure: + """ + self.load_markets() + market = self.market(symbol) + if not market['contract']: + raise BadRequest(self.id + ' fetchOpenInterest() supports contract markets only') + timeframe = self.safe_string(params, 'interval', '1h') + intervals = self.safe_dict(self.options, 'intervals') + interval = self.safe_string(intervals, timeframe) # 5min,15min,30min,1h,4h,1d + if interval is None: + raise BadRequest(self.id + ' fetchOpenInterest() cannot use the ' + timeframe + ' timeframe') + subType = 'linear' if market['linear'] else 'inverse' + category = self.safe_string(params, 'category', subType) + request: dict = { + 'symbol': market['id'], + 'intervalTime': interval, + 'category': category, + } + response = self.publicGetV5MarketOpenInterest(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "symbol": "BTCUSD", + # "category": "inverse", + # "list": [ + # { + # "openInterest": "461134384.00000000", + # "timestamp": "1669571400000" + # }, + # { + # "openInterest": "461134292.00000000", + # "timestamp": "1669571100000" + # } + # ], + # "nextPageCursor": "" + # }, + # "retExtInfo": {}, + # "time": 1672053548579 + # } + # + result = self.safe_dict(response, 'result', {}) + id = self.safe_string(result, 'symbol') + market = self.safe_market(id, market, None, 'contract') + data = self.add_pagination_cursor_to_result(response) + return self.parse_open_interest(data[0], market) + + def fetch_open_interest_history(self, symbol: str, timeframe='1h', since: Int = None, limit: Int = None, params={}): + """ + Gets the total amount of unsettled contracts. In other words, the total number of contracts held in open positions + + https://bybit-exchange.github.io/docs/v5/market/open-interest + + :param str symbol: Unified market symbol + :param str timeframe: "5m", 15m, 30m, 1h, 4h, 1d + :param int [since]: Not used by Bybit + :param int [limit]: The number of open interest structures to return. Max 200, default 50 + :param dict [params]: Exchange specific parameters + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns: An array of open interest structures + """ + if timeframe == '1m': + raise BadRequest(self.id + ' fetchOpenInterestHistory cannot use the 1m timeframe') + self.load_markets() + paginate = self.safe_bool(params, 'paginate') + if paginate: + params = self.omit(params, 'paginate') + params['timeframe'] = timeframe + return self.fetch_paginated_call_cursor('fetchOpenInterestHistory', symbol, since, limit, params, 'nextPageCursor', 'cursor', None, 200) + market = self.market(symbol) + if market['spot'] or market['option']: + raise BadRequest(self.id + ' fetchOpenInterestHistory() symbol does not support market ' + symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['limit'] = limit + return self.fetch_derivatives_open_interest_history(symbol, timeframe, since, limit, params) + + def parse_open_interest(self, interest, market: Market = None): + # + # { + # "openInterest": 64757.62400000, + # "timestamp": 1665784800000, + # } + # + timestamp = self.safe_integer(interest, 'timestamp') + openInterest = self.safe_number_2(interest, 'open_interest', 'openInterest') + # the openInterest is in the base asset for linear and quote asset for inverse + amount = openInterest if market['linear'] else None + value = openInterest if market['inverse'] else None + return self.safe_open_interest({ + 'symbol': market['symbol'], + 'openInterestAmount': amount, + 'openInterestValue': value, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'info': interest, + }, market) + + def fetch_cross_borrow_rate(self, code: str, params={}) -> CrossBorrowRate: + """ + fetch the rate of interest to borrow a currency for margin trading + + https://bybit-exchange.github.io/docs/zh-TW/v5/spot-margin-normal/interest-quota + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `borrow rate structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'coin': currency['id'], + } + response = self.privateGetV5SpotCrossMarginTradeLoanInfo(self.extend(request, params)) + # + # { + # "retCode": "0", + # "retMsg": "success", + # "result": { + # "coin": "USDT", + # "interestRate": "0.000107000000", + # "loanAbleAmount": "", + # "maxLoanAmount": "79999.999" + # }, + # "retExtInfo": null, + # "time": "1666734490778" + # } + # + timestamp = self.safe_integer(response, 'time') + data = self.safe_dict(response, 'result', {}) + data['timestamp'] = timestamp + return self.parse_borrow_rate(data, currency) + + def parse_borrow_rate(self, info, currency: Currency = None): + # + # { + # "coin": "USDT", + # "interestRate": "0.000107000000", + # "loanAbleAmount": "", + # "maxLoanAmount": "79999.999", + # "timestamp": 1666734490778 + # } + # + # fetchBorrowRateHistory + # { + # "timestamp": 1721469600000, + # "currency": "USDC", + # "hourlyBorrowRate": "0.000014621596", + # "vipLevel": "No VIP" + # } + # + timestamp = self.safe_integer(info, 'timestamp') + currencyId = self.safe_string_2(info, 'coin', 'currency') + hourlyBorrowRate = self.safe_number(info, 'hourlyBorrowRate') + period = 3600000 if (hourlyBorrowRate is not None) else 86400000 # 1h or 1d + return { + 'currency': self.safe_currency_code(currencyId, currency), + 'rate': self.safe_number(info, 'interestRate', hourlyBorrowRate), + 'period': period, # Daily + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'info': info, + } + + def fetch_borrow_interest(self, code: Str = None, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[BorrowInterest]: + """ + fetch the interest owed by the user for borrowing currency for margin trading + + https://bybit-exchange.github.io/docs/zh-TW/v5/spot-margin-normal/account-info + + :param str code: unified currency code + :param str symbol: unified market symbol when fetch interest in isolated markets + :param number [since]: the earliest time in ms to fetch borrrow interest for + :param number [limit]: the maximum number of structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `borrow interest structures ` + """ + self.load_markets() + request: dict = {} + response = self.privateGetV5SpotCrossMarginTradeAccount(self.extend(request, params)) + # + # { + # "ret_code": 0, + # "ret_msg": "", + # "ext_code": null, + # "ext_info": null, + # "result": { + # "status": "1", + # "riskRate": "0", + # "acctBalanceSum": "0.000486213817680857", + # "debtBalanceSum": "0", + # "loanAccountList": [ + # { + # "tokenId": "BTC", + # "total": "0.00048621", + # "locked": "0", + # "loan": "0", + # "interest": "0", + # "free": "0.00048621" + # }, + # ... + # ] + # } + # } + # + data = self.safe_dict(response, 'result', {}) + rows = self.safe_list(data, 'loanAccountList', []) + interest = self.parse_borrow_interests(rows, None) + return self.filter_by_currency_since_limit(interest, code, since, limit) + + def fetch_borrow_rate_history(self, code: str, since: Int = None, limit: Int = None, params={}): + """ + retrieves a history of a currencies borrow interest rate at specific time slots + + https://bybit-exchange.github.io/docs/v5/spot-margin-uta/historical-interest + + :param str code: unified currency code + :param int [since]: timestamp for the earliest borrow rate + :param int [limit]: the maximum number of `borrow rate structures ` to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch entries for + :returns dict[]: an array of `borrow rate structures ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + } + if since is None: + since = self.milliseconds() - 86400000 * 30 # last 30 days + request['startTime'] = since + endTime = self.safe_integer_2(params, 'until', 'endTime') + params = self.omit(params, ['until']) + if endTime is None: + endTime = since + 86400000 * 30 # since + 30 days + request['endTime'] = endTime + response = self.privateGetV5SpotMarginTradeInterestRateHistory(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "list": [ + # { + # "timestamp": 1721469600000, + # "currency": "USDC", + # "hourlyBorrowRate": "0.000014621596", + # "vipLevel": "No VIP" + # } + # ] + # }, + # "retExtInfo": "{}", + # "time": 1721899048991 + # } + # + data = self.safe_dict(response, 'result') + rows = self.safe_list(data, 'list', []) + return self.parse_borrow_rate_history(rows, code, since, limit) + + def parse_borrow_interest(self, info: dict, market: Market = None) -> BorrowInterest: + # + # { + # "tokenId": "BTC", + # "total": "0.00048621", + # "locked": "0", + # "loan": "0", + # "interest": "0", + # "free": "0.00048621" + # }, + # + return { + 'info': info, + 'symbol': None, + 'currency': self.safe_currency_code(self.safe_string(info, 'tokenId')), + 'interest': self.safe_number(info, 'interest'), + 'interestRate': None, + 'amountBorrowed': self.safe_number(info, 'loan'), + 'marginMode': 'cross', + 'timestamp': None, + 'datetime': None, + } + + def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://bybit-exchange.github.io/docs/v5/asset/create-inter-transfer + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account to transfer from + :param str toAccount: account to transfer to + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.transferId]: UUID, which is unique across the platform + :returns dict: a `transfer structure ` + """ + self.load_markets() + transferId = self.safe_string(params, 'transferId', self.uuid()) + accountTypes = self.safe_dict(self.options, 'accountsByType', {}) + fromId = self.safe_string(accountTypes, fromAccount, fromAccount) + toId = self.safe_string(accountTypes, toAccount, toAccount) + currency = self.currency(code) + amountToPrecision = self.currency_to_precision(code, amount) + request: dict = { + 'transferId': transferId, + 'fromAccountType': fromId, + 'toAccountType': toId, + 'coin': currency['id'], + 'amount': amountToPrecision, + } + response = self.privatePostV5AssetTransferInterTransfer(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "success", + # "result": { + # "transferId": "4244af44-f3b0-4cf6-a743-b56560e987bc" + # }, + # "retExtInfo": {}, + # "time": 1666875857205 + # } + # + timestamp = self.safe_integer(response, 'time') + transfer = self.safe_dict(response, 'result', {}) + statusRaw = self.safe_string_n(response, ['retCode', 'retMsg']) + status = self.parse_transfer_status(statusRaw) + return self.extend(self.parse_transfer(transfer, currency), { + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'amount': self.parse_number(amountToPrecision), + 'fromAccount': fromAccount, + 'toAccount': toAccount, + 'status': status, + }) + + def fetch_transfers(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[TransferEntry]: + """ + fetch a history of internal transfers made on an account + + https://bybit-exchange.github.io/docs/v5/asset/inter-transfer-list + + :param str code: unified currency code of the currency transferred + :param int [since]: the earliest time in ms to fetch transfers for + :param int [limit]: the maximum number of transfer structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch entries for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `transfer structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchTransfers', 'paginate') + if paginate: + return self.fetch_paginated_call_cursor('fetchTransfers', code, since, limit, params, 'nextPageCursor', 'cursor', None, 50) + currency = None + request: dict = {} + if code is not None: + currency = self.safe_currency(code) + request['coin'] = currency['id'] + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + request, params = self.handle_until_option('endTime', request, params) + response = self.privateGetV5AssetTransferQueryInterTransferList(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "success", + # "result": { + # "list": [ + # { + # "transferId": "selfTransfer_a1091cc7-9364-4b74-8de1-18f02c6f2d5c", + # "coin": "USDT", + # "amount": "5000", + # "fromAccountType": "SPOT", + # "toAccountType": "UNIFIED", + # "timestamp": "1667283263000", + # "status": "SUCCESS" + # } + # ], + # "nextPageCursor": "eyJtaW5JRCI6MTM1ODQ2OCwibWF4SUQiOjEzNTg0Njh9" + # }, + # "retExtInfo": {}, + # "time": 1670988271677 + # } + # + data = self.add_pagination_cursor_to_result(response) + return self.parse_transfers(data, currency, since, limit) + + def borrow_cross_margin(self, code: str, amount: float, params={}): + """ + create a loan to borrow margin + + https://bybit-exchange.github.io/docs/v5/spot-margin-normal/borrow + + :param str code: unified currency code of the currency to borrow + :param float amount: the amount to borrow + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin loan structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'coin': currency['id'], + 'qty': self.currency_to_precision(code, amount), + } + response = self.privatePostV5SpotCrossMarginTradeLoan(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "success", + # "result": { + # "transactId": "14143" + # }, + # "retExtInfo": null, + # "time": 1662617848970 + # } + # + result = self.safe_dict(response, 'result', {}) + transaction = self.parse_margin_loan(result, currency) + return self.extend(transaction, { + 'symbol': None, + 'amount': amount, + }) + + def repay_cross_margin(self, code: str, amount, params={}): + """ + repay borrowed margin and interest + + https://bybit-exchange.github.io/docs/v5/spot-margin-normal/repay + + :param str code: unified currency code of the currency to repay + :param float amount: the amount to repay + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin loan structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'coin': currency['id'], + 'qty': self.number_to_string(amount), + } + response = self.privatePostV5SpotCrossMarginTradeRepay(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "success", + # "result": { + # "repayId": "12128" + # }, + # "retExtInfo": null, + # "time": 1662618298452 + # } + # + result = self.safe_dict(response, 'result', {}) + transaction = self.parse_margin_loan(result, currency) + return self.extend(transaction, { + 'symbol': None, + 'amount': amount, + }) + + def parse_margin_loan(self, info, currency: Currency = None) -> dict: + # + # borrowCrossMargin + # + # { + # "transactId": "14143" + # } + # + # repayCrossMargin + # + # { + # "repayId": "12128" + # } + # + return { + 'id': self.safe_string_2(info, 'transactId', 'repayId'), + 'currency': self.safe_string(currency, 'code'), + 'amount': None, + 'symbol': None, + 'timestamp': None, + 'datetime': None, + 'info': info, + } + + def parse_transfer_status(self, status: Str) -> Str: + statuses: dict = { + '0': 'ok', + 'OK': 'ok', + 'SUCCESS': 'ok', + } + return self.safe_string(statuses, status, status) + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + # + # transfer + # + # { + # "transferId": "22c2bc11-ed5b-49a4-8647-c4e0f5f6f2b2" + # } + # + # fetchTransfers + # + # { + # "transferId": "e9c421c4-b010-4b16-abd6-106179f27702", + # "coin": "USDT", + # "amount": "8", + # "fromAccountType": "FUND", + # "toAccountType": "SPOT", + # "timestamp": "1666879426000", + # "status": "SUCCESS" + # } + # + currencyId = self.safe_string(transfer, 'coin') + timestamp = self.safe_integer(transfer, 'timestamp') + fromAccountId = self.safe_string(transfer, 'fromAccountType') + toAccountId = self.safe_string(transfer, 'toAccountType') + accountIds = self.safe_dict(self.options, 'accountsById', {}) + fromAccount = self.safe_string(accountIds, fromAccountId, fromAccountId) + toAccount = self.safe_string(accountIds, toAccountId, toAccountId) + return { + 'info': transfer, + 'id': self.safe_string(transfer, 'transferId'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'currency': self.safe_currency_code(currencyId, currency), + 'amount': self.safe_number(transfer, 'amount'), + 'fromAccount': fromAccount, + 'toAccount': toAccount, + 'status': self.parse_transfer_status(self.safe_string(transfer, 'status')), + } + + def fetch_derivatives_market_leverage_tiers(self, symbol: str, params={}) -> List[LeverageTier]: + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if market['linear']: + request['category'] = 'linear' + elif market['inverse']: + request['category'] = 'inverse' + response = self.publicGetV5MarketRiskLimit(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "category": "inverse", + # "list": [ + # { + # "id": 1, + # "symbol": "BTCUSD", + # "riskLimitValue": "150", + # "maintenanceMargin": "0.5", + # "initialMargin": "1", + # "isLowestRisk": 1, + # "maxLeverage": "100.00" + # }, + # .... + # ] + # }, + # "retExtInfo": {}, + # "time": 1672054488010 + # } + # + result = self.safe_dict(response, 'result') + tiers = self.safe_list(result, 'list') + return self.parse_market_leverage_tiers(tiers, market) + + def fetch_market_leverage_tiers(self, symbol: str, params={}) -> List[LeverageTier]: + """ + retrieve information on the maximum leverage, and maintenance margin for trades of varying trade sizes for a single market + + https://bybit-exchange.github.io/docs/v5/market/risk-limit + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `leverage tiers structure ` + """ + self.load_markets() + request: dict = {} + market = None + market = self.market(symbol) + if market['spot'] or market['option']: + raise BadRequest(self.id + ' fetchMarketLeverageTiers() symbol does not support market ' + symbol) + request['symbol'] = market['id'] + return self.fetch_derivatives_market_leverage_tiers(symbol, params) + + def parse_trading_fee(self, fee: dict, market: Market = None) -> TradingFeeInterface: + # + # { + # "symbol": "ETHUSDT", + # "makerFeeRate": 0.001, + # "takerFeeRate": 0.001 + # } + # + marketId = self.safe_string(fee, 'symbol') + defaultType = market['type'] if (market is not None) else 'contract' + symbol = self.safe_symbol(marketId, market, None, defaultType) + return { + 'info': fee, + 'symbol': symbol, + 'maker': self.safe_number(fee, 'makerFeeRate'), + 'taker': self.safe_number(fee, 'takerFeeRate'), + 'percentage': None, + 'tierBased': None, + } + + def fetch_trading_fee(self, symbol: str, params={}) -> TradingFeeInterface: + """ + fetch the trading fees for a market + + https://bybit-exchange.github.io/docs/v5/account/fee-rate + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `fee structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + category = None + category, params = self.get_bybit_type('fetchTradingFee', market, params) + request['category'] = category + response = self.privateGetV5AccountFeeRate(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "list": [ + # { + # "symbol": "ETHUSDT", + # "takerFeeRate": "0.0006", + # "makerFeeRate": "0.0001" + # } + # ] + # }, + # "retExtInfo": {}, + # "time": 1676360412576 + # } + # + result = self.safe_dict(response, 'result', {}) + fees = self.safe_list(result, 'list', []) + first = self.safe_dict(fees, 0, {}) + return self.parse_trading_fee(first, market) + + def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://bybit-exchange.github.io/docs/v5/account/fee-rate + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: market type, ['swap', 'option', 'spot'] + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + self.load_markets() + type = None + type, params = self.handle_option_and_params(params, 'fetchTradingFees', 'type', 'future') + if type == 'spot': + raise NotSupported(self.id + ' fetchTradingFees() is not supported for spot market') + response = self.privateGetV5AccountFeeRate(params) + # + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "list": [ + # { + # "symbol": "ETHUSDT", + # "takerFeeRate": "0.0006", + # "makerFeeRate": "0.0001" + # } + # ] + # }, + # "retExtInfo": {}, + # "time": 1676360412576 + # } + # + fees = self.safe_dict(response, 'result', {}) + fees = self.safe_list(fees, 'list', []) + result: dict = {} + for i in range(0, len(fees)): + fee = self.parse_trading_fee(fees[i]) + symbol = fee['symbol'] + result[symbol] = fee + return result + + def parse_deposit_withdraw_fee(self, fee, currency: Currency = None) -> Any: + # + # { + # "name": "BTC", + # "coin": "BTC", + # "remainAmount": "150", + # "chains": [ + # { + # "chainType": "BTC", + # "confirmation": "10000", + # "withdrawFee": "0.0005", + # "depositMin": "0.0005", + # "withdrawMin": "0.001", + # "chain": "BTC", + # "chainDeposit": "1", + # "chainWithdraw": "1", + # "minAccuracy": "8" + # } + # ] + # } + # + chains = self.safe_list(fee, 'chains', []) + chainsLength = len(chains) + result: dict = { + 'info': fee, + 'withdraw': { + 'fee': None, + 'percentage': None, + }, + 'deposit': { + 'fee': None, + 'percentage': None, + }, + 'networks': {}, + } + if chainsLength != 0: + for i in range(0, chainsLength): + chain = chains[i] + networkId = self.safe_string(chain, 'chain') + currencyCode = self.safe_string(currency, 'code') + networkCode = self.network_id_to_code(networkId, currencyCode) + result['networks'][networkCode] = { + 'deposit': {'fee': None, 'percentage': None}, + 'withdraw': {'fee': self.safe_number(chain, 'withdrawFee'), 'percentage': False}, + } + if chainsLength == 1: + result['withdraw']['fee'] = self.safe_number(chain, 'withdrawFee') + result['withdraw']['percentage'] = False + return result + + def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}): + """ + fetch deposit and withdraw fees + + https://bybit-exchange.github.io/docs/v5/asset/coin-info + + :param str[] codes: list of unified currency codes + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `fee structures ` + """ + self.check_required_credentials() + self.load_markets() + response = self.privateGetV5AssetCoinQueryInfo(params) + # + # { + # "retCode": 0, + # "retMsg": "", + # "result": { + # "rows": [ + # { + # "name": "BTC", + # "coin": "BTC", + # "remainAmount": "150", + # "chains": [ + # { + # "chainType": "BTC", + # "confirmation": "10000", + # "withdrawFee": "0.0005", + # "depositMin": "0.0005", + # "withdrawMin": "0.001", + # "chain": "BTC", + # "chainDeposit": "1", + # "chainWithdraw": "1", + # "minAccuracy": "8" + # } + # ] + # } + # ] + # }, + # "retExtInfo": {}, + # "time": 1672194582264 + # } + # + data = self.safe_dict(response, 'result', {}) + rows = self.safe_list(data, 'rows', []) + return self.parse_deposit_withdraw_fees(rows, codes, 'coin') + + def fetch_settlement_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical settlement records + + https://bybit-exchange.github.io/docs/v5/market/delivery-price + + :param str symbol: unified market symbol of the settlement history + :param int [since]: timestamp in ms + :param int [limit]: number of records + :param dict [params]: exchange specific params + :param str [params.type]: market type, ['swap', 'option', 'spot'] + :param str [params.subType]: market subType, ['linear', 'inverse'] + :returns dict[]: a list of [settlement history objects] + """ + self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + type = None + type, params = self.get_bybit_type('fetchSettlementHistory', market, params) + if type == 'spot': + raise NotSupported(self.id + ' fetchSettlementHistory() is not supported for spot market') + request['category'] = type + if limit is not None: + request['limit'] = limit + response = self.publicGetV5MarketDeliveryPrice(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "success", + # "result": { + # "category": "option", + # "nextPageCursor": "0%2C3", + # "list": [ + # { + # "symbol": "SOL-27JUN23-20-C", + # "deliveryPrice": "16.62258889", + # "deliveryTime": "1687852800000" + # }, + # ] + # }, + # "retExtInfo": {}, + # "time": 1689043527231 + # } + # + result = self.safe_dict(response, 'result', {}) + data = self.safe_list(result, 'list', []) + settlements = self.parse_settlements(data, market) + sorted = self.sort_by(settlements, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, market['symbol'], since, limit) + + def fetch_my_settlement_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical settlement records of the user + + https://bybit-exchange.github.io/docs/v5/asset/delivery + + :param str symbol: unified market symbol of the settlement history + :param int [since]: timestamp in ms + :param int [limit]: number of records + :param dict [params]: exchange specific params + :param str [params.type]: market type, ['swap', 'option', 'spot'] + :param str [params.subType]: market subType, ['linear', 'inverse'] + :returns dict[]: a list of [settlement history objects] + """ + self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + type = None + type, params = self.get_bybit_type('fetchMySettlementHistory', market, params) + if type == 'spot': + raise NotSupported(self.id + ' fetchMySettlementHistory() is not supported for spot market') + request['category'] = type + if limit is not None: + request['limit'] = limit + response = self.privateGetV5AssetDeliveryRecord(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "success", + # "result": { + # "category": "option", + # "nextPageCursor": "0%2C3", + # "list": [ + # { + # "symbol": "SOL-27JUN23-20-C", + # "deliveryPrice": "16.62258889", + # "deliveryTime": "1687852800000", + # "side": "Buy", + # "strike": "20", + # "fee": "0.00000000", + # "position": "0.01", + # "deliveryRpl": "3.5" + # }, + # ] + # }, + # "retExtInfo": {}, + # "time": 1689043527231 + # } + # + result = self.safe_dict(response, 'result', {}) + data = self.safe_list(result, 'list', []) + settlements = self.parse_settlements(data, market) + sorted = self.sort_by(settlements, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, market['symbol'], since, limit) + + def parse_settlement(self, settlement, market): + # + # fetchSettlementHistory + # + # { + # "symbol": "SOL-27JUN23-20-C", + # "deliveryPrice": "16.62258889", + # "deliveryTime": "1687852800000" + # } + # + # fetchMySettlementHistory + # + # { + # "symbol": "SOL-27JUN23-20-C", + # "deliveryPrice": "16.62258889", + # "deliveryTime": "1687852800000", + # "side": "Buy", + # "strike": "20", + # "fee": "0.00000000", + # "position": "0.01", + # "deliveryRpl": "3.5" + # } + # + timestamp = self.safe_integer(settlement, 'deliveryTime') + marketId = self.safe_string(settlement, 'symbol') + return { + 'info': settlement, + 'symbol': self.safe_symbol(marketId, market), + 'price': self.safe_number(settlement, 'deliveryPrice'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + + def parse_settlements(self, settlements, market): + # + # fetchSettlementHistory + # + # [ + # { + # "symbol": "SOL-27JUN23-20-C", + # "deliveryPrice": "16.62258889", + # "deliveryTime": "1687852800000" + # } + # ] + # + # fetchMySettlementHistory + # + # [ + # { + # "symbol": "SOL-27JUN23-20-C", + # "deliveryPrice": "16.62258889", + # "deliveryTime": "1687852800000", + # "side": "Buy", + # "strike": "20", + # "fee": "0.00000000", + # "position": "0.01", + # "deliveryRpl": "3.5" + # } + # ] + # + result = [] + for i in range(0, len(settlements)): + result.append(self.parse_settlement(settlements[i], market)) + return result + + def fetch_volatility_history(self, code: str, params={}): + """ + fetch the historical volatility of an option market based on an underlying asset + + https://bybit-exchange.github.io/docs/v5/market/iv + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.period]: the period in days to fetch the volatility for: 7,14,21,30,60,90,180,270 + :returns dict[]: a list of `volatility history objects ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'category': 'option', + 'baseCoin': currency['id'], + } + response = self.publicGetV5MarketHistoricalVolatility(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "SUCCESS", + # "category": "option", + # "result": [ + # { + # "period": 7, + # "value": "0.23854072", + # "time": "1690574400000" + # } + # ] + # } + # + volatility = self.safe_list(response, 'result', []) + return self.parse_volatility_history(volatility) + + def parse_volatility_history(self, volatility): + # + # { + # "period": 7, + # "value": "0.23854072", + # "time": "1690574400000" + # } + # + result = [] + for i in range(0, len(volatility)): + entry = volatility[i] + timestamp = self.safe_integer(entry, 'time') + result.append({ + 'info': volatility, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'volatility': self.safe_number(entry, 'value'), + }) + return result + + def fetch_greeks(self, symbol: str, params={}) -> Greeks: + """ + fetches an option contracts greeks, financial metrics used to measure the factors that affect the price of an options contract + + https://bybit-exchange.github.io/docs/api-explorer/v5/market/tickers + + :param str symbol: unified symbol of the market to fetch greeks for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `greeks structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'category': 'option', + } + response = self.publicGetV5MarketTickers(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "SUCCESS", + # "result": { + # "category": "option", + # "list": [ + # { + # "symbol": "BTC-26JAN24-39000-C", + # "bid1Price": "3205", + # "bid1Size": "7.1", + # "bid1Iv": "0.5478", + # "ask1Price": "3315", + # "ask1Size": "1.98", + # "ask1Iv": "0.5638", + # "lastPrice": "3230", + # "highPrice24h": "3255", + # "lowPrice24h": "3200", + # "markPrice": "3273.02263032", + # "indexPrice": "36790.96", + # "markIv": "0.5577", + # "underlyingPrice": "37649.67254894", + # "openInterest": "19.67", + # "turnover24h": "170140.33875912", + # "volume24h": "4.56", + # "totalVolume": "22", + # "totalTurnover": "789305", + # "delta": "0.49640971", + # "gamma": "0.00004131", + # "vega": "69.08651675", + # "theta": "-24.9443226", + # "predictedDeliveryPrice": "0", + # "change24h": "0.18532111" + # } + # ] + # }, + # "retExtInfo": {}, + # "time": 1699584008326 + # } + # + timestamp = self.safe_integer(response, 'time') + result = self.safe_dict(response, 'result', {}) + data = self.safe_list(result, 'list', []) + greeks = self.parse_greeks(data[0], market) + return self.extend(greeks, { + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + + def fetch_all_greeks(self, symbols: Strings = None, params={}) -> List[Greeks]: + """ + fetches all option contracts greeks, financial metrics used to measure the factors that affect the price of an options contract + + https://bybit-exchange.github.io/docs/api-explorer/v5/market/tickers + + :param str[] [symbols]: unified symbols of the markets to fetch greeks for, all markets are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.baseCoin]: the baseCoin of the symbol, default is BTC + :returns dict: a `greeks structure ` + """ + self.load_markets() + symbols = self.market_symbols(symbols, None, True, True, True) + baseCoin = self.safe_string(params, 'baseCoin', 'BTC') + request: dict = { + 'category': 'option', + 'baseCoin': baseCoin, + } + market = None + if symbols is not None: + symbolsLength = len(symbols) + if symbolsLength == 1: + market = self.market(symbols[0]) + request['symbol'] = market['id'] + response = self.publicGetV5MarketTickers(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "SUCCESS", + # "result": { + # "category": "option", + # "list": [ + # { + # "symbol": "BTC-26JAN24-39000-C", + # "bid1Price": "3205", + # "bid1Size": "7.1", + # "bid1Iv": "0.5478", + # "ask1Price": "3315", + # "ask1Size": "1.98", + # "ask1Iv": "0.5638", + # "lastPrice": "3230", + # "highPrice24h": "3255", + # "lowPrice24h": "3200", + # "markPrice": "3273.02263032", + # "indexPrice": "36790.96", + # "markIv": "0.5577", + # "underlyingPrice": "37649.67254894", + # "openInterest": "19.67", + # "turnover24h": "170140.33875912", + # "volume24h": "4.56", + # "totalVolume": "22", + # "totalTurnover": "789305", + # "delta": "0.49640971", + # "gamma": "0.00004131", + # "vega": "69.08651675", + # "theta": "-24.9443226", + # "predictedDeliveryPrice": "0", + # "change24h": "0.18532111" + # } + # ] + # }, + # "retExtInfo": {}, + # "time": 1699584008326 + # } + # + result = self.safe_dict(response, 'result', {}) + data = self.safe_list(result, 'list', []) + return self.parse_all_greeks(data, symbols) + + def parse_greeks(self, greeks: dict, market: Market = None) -> Greeks: + # + # { + # "symbol": "BTC-26JAN24-39000-C", + # "bid1Price": "3205", + # "bid1Size": "7.1", + # "bid1Iv": "0.5478", + # "ask1Price": "3315", + # "ask1Size": "1.98", + # "ask1Iv": "0.5638", + # "lastPrice": "3230", + # "highPrice24h": "3255", + # "lowPrice24h": "3200", + # "markPrice": "3273.02263032", + # "indexPrice": "36790.96", + # "markIv": "0.5577", + # "underlyingPrice": "37649.67254894", + # "openInterest": "19.67", + # "turnover24h": "170140.33875912", + # "volume24h": "4.56", + # "totalVolume": "22", + # "totalTurnover": "789305", + # "delta": "0.49640971", + # "gamma": "0.00004131", + # "vega": "69.08651675", + # "theta": "-24.9443226", + # "predictedDeliveryPrice": "0", + # "change24h": "0.18532111" + # } + # + marketId = self.safe_string(greeks, 'symbol') + symbol = self.safe_symbol(marketId, market) + return { + 'symbol': symbol, + 'timestamp': None, + 'datetime': None, + 'delta': self.safe_number(greeks, 'delta'), + 'gamma': self.safe_number(greeks, 'gamma'), + 'theta': self.safe_number(greeks, 'theta'), + 'vega': self.safe_number(greeks, 'vega'), + 'rho': None, + 'bidSize': self.safe_number(greeks, 'bid1Size'), + 'askSize': self.safe_number(greeks, 'ask1Size'), + 'bidImpliedVolatility': self.safe_number(greeks, 'bid1Iv'), + 'askImpliedVolatility': self.safe_number(greeks, 'ask1Iv'), + 'markImpliedVolatility': self.safe_number(greeks, 'markIv'), + 'bidPrice': self.safe_number(greeks, 'bid1Price'), + 'askPrice': self.safe_number(greeks, 'ask1Price'), + 'markPrice': self.safe_number(greeks, 'markPrice'), + 'lastPrice': self.safe_number(greeks, 'lastPrice'), + 'underlyingPrice': self.safe_number(greeks, 'underlyingPrice'), + 'info': greeks, + } + + def fetch_my_liquidations(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Liquidation]: + """ + retrieves the users liquidated positions + + https://bybit-exchange.github.io/docs/api-explorer/v5/position/execution + + :param str [symbol]: unified CCXT market symbol + :param int [since]: the earliest time in ms to fetch liquidations for + :param int [limit]: the maximum number of liquidation structures to retrieve + :param dict [params]: exchange specific parameters for the exchange API endpoint + :param str [params.type]: market type, ['swap', 'option', 'spot'] + :param str [params.subType]: market subType, ['linear', 'inverse'] + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict: an array of `liquidation structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchMyLiquidations', 'paginate') + if paginate: + return self.fetch_paginated_call_cursor('fetchMyLiquidations', symbol, since, limit, params, 'nextPageCursor', 'cursor', None, 100) + request: dict = { + 'execType': 'BustTrade', + } + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + type = None + type, params = self.get_bybit_type('fetchMyLiquidations', market, params) + request['category'] = type + if limit is not None: + request['limit'] = limit + if since is not None: + request['startTime'] = since + request, params = self.handle_until_option('endTime', request, params) + response = self.privateGetV5ExecutionList(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "nextPageCursor": "132766%3A2%2C132766%3A2", + # "category": "linear", + # "list": [ + # { + # "symbol": "ETHPERP", + # "orderType": "Market", + # "underlyingPrice": "", + # "orderLinkId": "", + # "side": "Buy", + # "indexPrice": "", + # "orderId": "8c065341-7b52-4ca9-ac2c-37e31ac55c94", + # "stopOrderType": "UNKNOWN", + # "leavesQty": "0", + # "execTime": "1672282722429", + # "isMaker": False, + # "execFee": "0.071409", + # "feeRate": "0.0006", + # "execId": "e0cbe81d-0f18-5866-9415-cf319b5dab3b", + # "tradeIv": "", + # "blockTradeId": "", + # "markPrice": "1183.54", + # "execPrice": "1190.15", + # "markIv": "", + # "orderQty": "0.1", + # "orderPrice": "1236.9", + # "execValue": "119.015", + # "execType": "Trade", + # "execQty": "0.1" + # } + # ] + # }, + # "retExtInfo": {}, + # "time": 1672283754510 + # } + # + liquidations = self.add_pagination_cursor_to_result(response) + return self.parse_liquidations(liquidations, market, since, limit) + + def parse_liquidation(self, liquidation, market: Market = None) -> Liquidation: + # + # { + # "symbol": "ETHPERP", + # "orderType": "Market", + # "underlyingPrice": "", + # "orderLinkId": "", + # "side": "Buy", + # "indexPrice": "", + # "orderId": "8c065341-7b52-4ca9-ac2c-37e31ac55c94", + # "stopOrderType": "UNKNOWN", + # "leavesQty": "0", + # "execTime": "1672282722429", + # "isMaker": False, + # "execFee": "0.071409", + # "feeRate": "0.0006", + # "execId": "e0cbe81d-0f18-5866-9415-cf319b5dab3b", + # "tradeIv": "", + # "blockTradeId": "", + # "markPrice": "1183.54", + # "execPrice": "1190.15", + # "markIv": "", + # "orderQty": "0.1", + # "orderPrice": "1236.9", + # "execValue": "119.015", + # "execType": "Trade", + # "execQty": "0.1" + # } + # + marketId = self.safe_string(liquidation, 'symbol') + timestamp = self.safe_integer(liquidation, 'execTime') + contractsString = self.safe_string(liquidation, 'execQty') + contractSizeString = self.safe_string(market, 'contractSize') + priceString = self.safe_string(liquidation, 'execPrice') + baseValueString = Precise.string_mul(contractsString, contractSizeString) + quoteValueString = Precise.string_mul(baseValueString, priceString) + return self.safe_liquidation({ + 'info': liquidation, + 'symbol': self.safe_symbol(marketId, market, None, 'contract'), + 'contracts': self.parse_number(contractsString), + 'contractSize': self.parse_number(contractSizeString), + 'price': self.parse_number(priceString), + 'baseValue': self.parse_number(baseValueString), + 'quoteValue': self.parse_number(quoteValueString), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + + def get_leverage_tiers_paginated(self, symbol: Str = None, params={}): + self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + paginate = False + paginate, params = self.handle_option_and_params(params, 'getLeverageTiersPaginated', 'paginate') + if paginate: + return self.fetch_paginated_call_cursor('getLeverageTiersPaginated', symbol, None, None, params, 'nextPageCursor', 'cursor', None, 100) + subType = None + subType, params = self.handle_sub_type_and_params('getLeverageTiersPaginated', market, params, 'linear') + request: dict = { + 'category': subType, + } + response = self.publicGetV5MarketRiskLimit(self.extend(request, params)) + result = self.add_pagination_cursor_to_result(response) + first = self.safe_dict(result, 0) + total = len(result) + lastIndex = total - 1 + last = self.safe_dict(result, lastIndex) + cursorValue = self.safe_string(first, 'nextPageCursor') + last['info'] = { + 'nextPageCursor': cursorValue, + } + result[lastIndex] = last + return result + + def fetch_leverage_tiers(self, symbols: Strings = None, params={}) -> LeverageTiers: + """ + retrieve information on the maximum leverage, for different trade sizes + + https://bybit-exchange.github.io/docs/v5/market/risk-limit + + :param str[] [symbols]: a list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.subType]: market subType, ['linear', 'inverse'], default is 'linear' + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict: a dictionary of `leverage tiers structures `, indexed by market symbols + """ + self.load_markets() + market = None + symbol = None + if symbols is not None: + market = self.market(symbols[0]) + if market['spot']: + raise NotSupported(self.id + ' fetchLeverageTiers() is not supported for spot market') + symbol = market['symbol'] + data = self.get_leverage_tiers_paginated(symbol, self.extend({'paginate': True, 'paginationCalls': 50}, params)) + symbols = self.market_symbols(symbols) + return self.parse_leverage_tiers(data, symbols, 'symbol') + + def parse_leverage_tiers(self, response, symbols: Strings = None, marketIdKey=None) -> LeverageTiers: + # + # [ + # { + # "id": 1, + # "symbol": "BTCUSD", + # "riskLimitValue": "150", + # "maintenanceMargin": "0.5", + # "initialMargin": "1", + # "isLowestRisk": 1, + # "maxLeverage": "100.00" + # } + # ] + # + tiers: dict = {} + marketIds = self.market_ids(symbols) + filteredResults = self.filter_by_array(response, marketIdKey, marketIds, False) + grouped = self.group_by(filteredResults, marketIdKey) + keys = list(grouped.keys()) + for i in range(0, len(keys)): + marketId = keys[i] + entry = grouped[marketId] + for j in range(0, len(entry)): + id = self.safe_integer(entry[j], 'id') + entry[j]['id'] = id + market = self.safe_market(marketId, None, None, 'contract') + symbol = market['symbol'] + tiers[symbol] = self.parse_market_leverage_tiers(self.sort_by(entry, 'id'), market) + return tiers + + def parse_market_leverage_tiers(self, info, market: Market = None) -> List[LeverageTier]: + # + # [ + # { + # "id": 1, + # "symbol": "BTCUSD", + # "riskLimitValue": "150", + # "maintenanceMargin": "0.5", + # "initialMargin": "1", + # "isLowestRisk": 1, + # "maxLeverage": "100.00" + # } + # ] + # + tiers = [] + for i in range(0, len(info)): + tier = info[i] + marketId = self.safe_string(info, 'symbol') + market = self.safe_market(marketId) + minNotional = self.parse_number('0') + if i != 0: + minNotional = self.safe_number(info[i - 1], 'riskLimitValue') + tiers.append({ + 'tier': self.safe_integer(tier, 'id'), + 'symbol': self.safe_symbol(marketId, market), + 'currency': market['settle'], + 'minNotional': minNotional, + 'maxNotional': self.safe_number(tier, 'riskLimitValue'), + 'maintenanceMarginRate': self.safe_number(tier, 'maintenanceMargin'), + 'maxLeverage': self.safe_number(tier, 'maxLeverage'), + 'info': tier, + }) + return tiers + + def fetch_funding_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[FundingHistory]: + """ + fetch the history of funding payments paid and received on self account + + https://bybit-exchange.github.io/docs/api-explorer/v5/position/execution + + :param str [symbol]: unified market symbol + :param int [since]: the earliest time in ms to fetch funding history for + :param int [limit]: the maximum number of funding history structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict: a `funding history structure ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchFundingHistory', 'paginate') + if paginate: + return self.fetch_paginated_call_cursor('fetchFundingHistory', symbol, since, limit, params, 'nextPageCursor', 'cursor', None, 100) + request: dict = { + 'execType': 'Funding', + } + market: Market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + type = None + type, params = self.get_bybit_type('fetchFundingHistory', market, params) + request['category'] = type + if symbol is not None: + request['symbol'] = market['id'] + if since is not None: + request['startTime'] = since + if limit is not None: + request['size'] = limit + else: + request['size'] = 100 + request, params = self.handle_until_option('endTime', request, params) + response = self.privateGetV5ExecutionList(self.extend(request, params)) + fundings = self.add_pagination_cursor_to_result(response) + return self.parse_incomes(fundings, market, since, limit) + + def parse_income(self, income, market: Market = None) -> object: + # + # { + # "symbol": "XMRUSDT", + # "orderType": "UNKNOWN", + # "underlyingPrice": "", + # "orderLinkId": "", + # "orderId": "a11e5fe2-1dbf-4bab-a9b2-af80a14efc5d", + # "stopOrderType": "UNKNOWN", + # "execTime": "1710950400000", + # "feeCurrency": "", + # "createType": "", + # "feeRate": "-0.000761", + # "tradeIv": "", + # "blockTradeId": "", + # "markPrice": "136.79", + # "execPrice": "137.11", + # "markIv": "", + # "orderQty": "0", + # "orderPrice": "0", + # "execValue": "134.3678", + # "closedSize": "0", + # "execType": "Funding", + # "seq": "28097658790", + # "side": "Sell", + # "indexPrice": "", + # "leavesQty": "0", + # "isMaker": False, + # "execFee": "-0.10232512", + # "execId": "8d1ef156-4ec6-4445-9a6c-1c0c24dbd046", + # "marketUnit": "", + # "execQty": "0.98", + # "nextPageCursor": "5774437%3A0%2C5771289%3A0" + # } + # + marketId = self.safe_string(income, 'symbol') + market = self.safe_market(marketId, market, None, 'contract') + code = 'USDT' + if market['inverse']: + code = market['quote'] + timestamp = self.safe_integer(income, 'execTime') + return { + 'info': income, + 'symbol': self.safe_symbol(marketId, market, '-', 'swap'), + 'code': code, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': self.safe_string(income, 'execId'), + 'amount': self.safe_number(income, 'execFee'), + 'rate': self.safe_number(income, 'feeRate'), + } + + def fetch_option(self, symbol: str, params={}) -> Option: + """ + fetches option data that is commonly found in an option chain + + https://bybit-exchange.github.io/docs/v5/market/tickers + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `option chain structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'category': 'option', + 'symbol': market['id'], + } + response = self.publicGetV5MarketTickers(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "SUCCESS", + # "result": { + # "category": "option", + # "list": [ + # { + # "symbol": "BTC-27DEC24-55000-P", + # "bid1Price": "0", + # "bid1Size": "0", + # "bid1Iv": "0", + # "ask1Price": "0", + # "ask1Size": "0", + # "ask1Iv": "0", + # "lastPrice": "10980", + # "highPrice24h": "0", + # "lowPrice24h": "0", + # "markPrice": "11814.66756236", + # "indexPrice": "63838.92", + # "markIv": "0.8866", + # "underlyingPrice": "71690.55303594", + # "openInterest": "0.01", + # "turnover24h": "0", + # "volume24h": "0", + # "totalVolume": "2", + # "totalTurnover": "78719", + # "delta": "-0.23284954", + # "gamma": "0.0000055", + # "vega": "191.70757975", + # "theta": "-30.43617927", + # "predictedDeliveryPrice": "0", + # "change24h": "0" + # } + # ] + # }, + # "retExtInfo": {}, + # "time": 1711162003672 + # } + # + result = self.safe_dict(response, 'result', {}) + resultList = self.safe_list(result, 'list', []) + chain = self.safe_dict(resultList, 0, {}) + return self.parse_option(chain, None, market) + + def fetch_option_chain(self, code: str, params={}) -> OptionChain: + """ + fetches data for an underlying asset that is commonly found in an option chain + + https://bybit-exchange.github.io/docs/v5/market/tickers + + :param str code: base currency to fetch an option chain for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `option chain structures ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'category': 'option', + 'baseCoin': currency['id'], + } + response = self.publicGetV5MarketTickers(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "SUCCESS", + # "result": { + # "category": "option", + # "list": [ + # { + # "symbol": "BTC-27DEC24-55000-P", + # "bid1Price": "0", + # "bid1Size": "0", + # "bid1Iv": "0", + # "ask1Price": "0", + # "ask1Size": "0", + # "ask1Iv": "0", + # "lastPrice": "10980", + # "highPrice24h": "0", + # "lowPrice24h": "0", + # "markPrice": "11814.66756236", + # "indexPrice": "63838.92", + # "markIv": "0.8866", + # "underlyingPrice": "71690.55303594", + # "openInterest": "0.01", + # "turnover24h": "0", + # "volume24h": "0", + # "totalVolume": "2", + # "totalTurnover": "78719", + # "delta": "-0.23284954", + # "gamma": "0.0000055", + # "vega": "191.70757975", + # "theta": "-30.43617927", + # "predictedDeliveryPrice": "0", + # "change24h": "0" + # }, + # ] + # }, + # "retExtInfo": {}, + # "time": 1711162003672 + # } + # + result = self.safe_dict(response, 'result', {}) + resultList = self.safe_list(result, 'list', []) + return self.parse_option_chain(resultList, None, 'symbol') + + def parse_option(self, chain: dict, currency: Currency = None, market: Market = None) -> Option: + # + # { + # "symbol": "BTC-27DEC24-55000-P", + # "bid1Price": "0", + # "bid1Size": "0", + # "bid1Iv": "0", + # "ask1Price": "0", + # "ask1Size": "0", + # "ask1Iv": "0", + # "lastPrice": "10980", + # "highPrice24h": "0", + # "lowPrice24h": "0", + # "markPrice": "11814.66756236", + # "indexPrice": "63838.92", + # "markIv": "0.8866", + # "underlyingPrice": "71690.55303594", + # "openInterest": "0.01", + # "turnover24h": "0", + # "volume24h": "0", + # "totalVolume": "2", + # "totalTurnover": "78719", + # "delta": "-0.23284954", + # "gamma": "0.0000055", + # "vega": "191.70757975", + # "theta": "-30.43617927", + # "predictedDeliveryPrice": "0", + # "change24h": "0" + # } + # + marketId = self.safe_string(chain, 'symbol') + market = self.safe_market(marketId, market) + return { + 'info': chain, + 'currency': None, + 'symbol': market['symbol'], + 'timestamp': None, + 'datetime': None, + 'impliedVolatility': self.safe_number(chain, 'markIv'), + 'openInterest': self.safe_number(chain, 'openInterest'), + 'bidPrice': self.safe_number(chain, 'bid1Price'), + 'askPrice': self.safe_number(chain, 'ask1Price'), + 'midPrice': None, + 'markPrice': self.safe_number(chain, 'markPrice'), + 'lastPrice': self.safe_number(chain, 'lastPrice'), + 'underlyingPrice': self.safe_number(chain, 'underlyingPrice'), + 'change': self.safe_number(chain, 'change24h'), + 'percentage': None, + 'baseVolume': self.safe_number(chain, 'totalVolume'), + 'quoteVolume': None, + } + + def fetch_positions_history(self, symbols: Strings = None, since: Int = None, limit: Int = None, params={}) -> List[Position]: + """ + fetches historical positions + + https://bybit-exchange.github.io/docs/v5/position/close-pnl + + :param str[] symbols: a list of unified market symbols + :param int [since]: timestamp in ms of the earliest position to fetch, params["until"] - since <= 7 days + :param int [limit]: the maximum amount of records to fetch, default=50, max=100 + :param dict params: extra parameters specific to the exchange api endpoint + :param int [params.until]: timestamp in ms of the latest position to fetch, params["until"] - since <= 7 days + :param str [params.subType]: 'linear' or 'inverse' + :returns dict[]: a list of `position structures ` + """ + self.load_markets() + market = None + subType = None + symbolsLength = 0 + if symbols is not None: + symbolsLength = len(symbols) + if symbolsLength > 0: + market = self.market(symbols[0]) + until = self.safe_integer(params, 'until') + subType, params = self.handle_sub_type_and_params('fetchPositionsHistory', market, params, 'linear') + params = self.omit(params, 'until') + request: dict = { + 'category': subType, + } + if (symbols is not None) and (symbolsLength == 1): + request['symbol'] = market['id'] + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + if until is not None: + request['endTime'] = until + response = self.privateGetV5PositionClosedPnl(self.extend(request, params)) + # + # { + # retCode: '0', + # retMsg: 'OK', + # result: { + # nextPageCursor: '071749f3-a9fa-427b-b5ca-27b2f52b81de%3A1712717265566520788%2C071749f3-a9fa-427b-b5ca-27b2f52b81de%3A1712717265566520788', + # category: 'linear', + # list: [ + # { + # symbol: 'XRPUSDT', + # orderType: 'Market', + # leverage: '10', + # updatedTime: '1712717265572', + # side: 'Sell', + # orderId: '071749f3-a9fa-427b-b5ca-27b2f52b81de', + # closedPnl: '-0.00049568', + # avgEntryPrice: '0.6045', + # qty: '3', + # cumEntryValue: '1.8135', + # createdTime: '1712717265566', + # orderPrice: '0.5744', + # closedSize: '3', + # avgExitPrice: '0.605', + # execType: 'Trade', + # fillCount: '1', + # cumExitValue: '1.815' + # } + # ] + # }, + # retExtInfo: {}, + # time: '1712717286073' + # } + # + result = self.safe_dict(response, 'result') + rawPositions = self.safe_list(result, 'list') + positions = self.parse_positions(rawPositions, symbols, params) + return self.filter_by_since_limit(positions, since, limit) + + def fetch_convert_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies that can be converted + + https://bybit-exchange.github.io/docs/v5/asset/convert/convert-coin-list + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.accountType]: eb_convert_uta, eb_convert_spot, eb_convert_funding, eb_convert_inverse, or eb_convert_contract + :returns dict: an associative dictionary of currencies + """ + self.load_markets() + accountType = None + enableUnifiedMargin, enableUnifiedAccount = self.is_unified_enabled() + isUnifiedAccount = (enableUnifiedMargin or enableUnifiedAccount) + accountTypeDefault = 'eb_convert_uta' if isUnifiedAccount else 'eb_convert_spot' + accountType, params = self.handle_option_and_params(params, 'fetchConvertCurrencies', 'accountType', accountTypeDefault) + request: dict = { + 'accountType': accountType, + } + response = self.privateGetV5AssetExchangeQueryCoinList(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "ok", + # "result": { + # "coins": [ + # { + # "coin": "MATIC", + # "fullName": "MATIC", + # "icon": "https://s1.bycsi.com/app/assets/token/0552ae79c535c3095fa18f7b377dd2e9.svg", + # "iconNight": "https://t1.bycsi.com/app/assets/token/f59301aef2d6ac2165c4c4603e672fb4.svg", + # "accuracyLength": 8, + # "coinType": "crypto", + # "balance": "0", + # "uBalance": "0", + # "timePeriod": 0, + # "singleFromMinLimit": "1.1", + # "singleFromMaxLimit": "20001", + # "singleToMinLimit": "0", + # "singleToMaxLimit": "0", + # "dailyFromMinLimit": "0", + # "dailyFromMaxLimit": "0", + # "dailyToMinLimit": "0", + # "dailyToMaxLimit": "0", + # "disableFrom": False, + # "disableTo": False + # }, + # ] + # }, + # "retExtInfo": {}, + # "time": 1727256416250 + # } + # + result: dict = {} + data = self.safe_dict(response, 'result', {}) + coins = self.safe_list(data, 'coins', []) + for i in range(0, len(coins)): + entry = coins[i] + id = self.safe_string(entry, 'coin') + disableFrom = self.safe_bool(entry, 'disableFrom') + disableTo = self.safe_bool(entry, 'disableTo') + inactive = (disableFrom or disableTo) + code = self.safe_currency_code(id) + result[code] = { + 'info': entry, + 'id': id, + 'code': code, + 'networks': None, + 'type': self.safe_string(entry, 'coinType'), + 'name': self.safe_string(entry, 'fullName'), + 'active': not inactive, + 'deposit': None, + 'withdraw': self.safe_number(entry, 'balance'), + 'fee': None, + 'precision': None, + 'limits': { + 'amount': { + 'min': self.safe_number(entry, 'singleFromMinLimit'), + 'max': self.safe_number(entry, 'singleFromMaxLimit'), + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + 'deposit': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + } + return result + + def fetch_convert_quote(self, fromCode: str, toCode: str, amount: Num = None, params={}) -> Conversion: + """ + fetch a quote for converting from one currency to another + + https://bybit-exchange.github.io/docs/v5/asset/convert/apply-quote + + :param str fromCode: the currency that you want to sell and convert from + :param str toCode: the currency that you want to buy and convert into + :param float [amount]: how much you want to trade in units of the from currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.accountType]: eb_convert_uta, eb_convert_spot, eb_convert_funding, eb_convert_inverse, or eb_convert_contract + :returns dict: a `conversion structure ` + """ + self.load_markets() + accountType = None + enableUnifiedMargin, enableUnifiedAccount = self.is_unified_enabled() + isUnifiedAccount = (enableUnifiedMargin or enableUnifiedAccount) + accountTypeDefault = 'eb_convert_uta' if isUnifiedAccount else 'eb_convert_spot' + accountType, params = self.handle_option_and_params(params, 'fetchConvertQuote', 'accountType', accountTypeDefault) + request: dict = { + 'fromCoin': fromCode, + 'toCoin': toCode, + 'requestAmount': self.number_to_string(amount), + 'requestCoin': fromCode, + 'accountType': accountType, + } + response = self.privatePostV5AssetExchangeQuoteApply(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "ok", + # "result": { + # "quoteTxId": "1010020692439481682687668224", + # "exchangeRate": "0.000015330836780000", + # "fromCoin": "USDT", + # "fromCoinType": "crypto", + # "toCoin": "BTC", + # "toCoinType": "crypto", + # "fromAmount": "10", + # "toAmount": "0.000153308367800000", + # "expiredTime": "1727257413353", + # "requestId": "" + # }, + # "retExtInfo": {}, + # "time": 1727257398375 + # } + # + data = self.safe_dict(response, 'result', {}) + fromCurrencyId = self.safe_string(data, 'fromCoin', fromCode) + fromCurrency = self.currency(fromCurrencyId) + toCurrencyId = self.safe_string(data, 'toCoin', toCode) + toCurrency = self.currency(toCurrencyId) + return self.parse_conversion(data, fromCurrency, toCurrency) + + def create_convert_trade(self, id: str, fromCode: str, toCode: str, amount: Num = None, params={}) -> Conversion: + """ + convert from one currency to another + + https://bybit-exchange.github.io/docs/v5/asset/convert/confirm-quote + + :param str id: the id of the trade that you want to make + :param str fromCode: the currency that you want to sell and convert from + :param str toCode: the currency that you want to buy and convert into + :param float amount: how much you want to trade in units of the from currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `conversion structure ` + """ + self.load_markets() + request: dict = { + 'quoteTxId': id, + } + response = self.privatePostV5AssetExchangeConvertExecute(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "ok", + # "result": { + # "exchangeStatus": "processing", + # "quoteTxId": "1010020692439483803499737088" + # }, + # "retExtInfo": {}, + # "time": 1727257904969 + # } + # + data = self.safe_dict(response, 'result', {}) + return self.parse_conversion(data) + + def fetch_convert_trade(self, id: str, code: Str = None, params={}) -> Conversion: + """ + fetch the data for a conversion trade + + https://bybit-exchange.github.io/docs/v5/asset/convert/get-convert-result + + :param str id: the id of the trade that you want to fetch + :param str [code]: the unified currency code of the conversion trade + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.accountType]: eb_convert_uta, eb_convert_spot, eb_convert_funding, eb_convert_inverse, or eb_convert_contract + :returns dict: a `conversion structure ` + """ + self.load_markets() + accountType = None + enableUnifiedMargin, enableUnifiedAccount = self.is_unified_enabled() + isUnifiedAccount = (enableUnifiedMargin or enableUnifiedAccount) + accountTypeDefault = 'eb_convert_uta' if isUnifiedAccount else 'eb_convert_spot' + accountType, params = self.handle_option_and_params(params, 'fetchConvertQuote', 'accountType', accountTypeDefault) + request: dict = { + 'quoteTxId': id, + 'accountType': accountType, + } + response = self.privateGetV5AssetExchangeConvertResultQuery(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "ok", + # "result": { + # "result": { + # "accountType": "eb_convert_uta", + # "exchangeTxId": "1010020692439483803499737088", + # "userId": "100406395", + # "fromCoin": "USDT", + # "fromCoinType": "crypto", + # "fromAmount": "10", + # "toCoin": "BTC", + # "toCoinType": "crypto", + # "toAmount": "0.00015344889", + # "exchangeStatus": "success", + # "extInfo": {}, + # "convertRate": "0.000015344889", + # "createdAt": "1727257904726" + # } + # }, + # "retExtInfo": {}, + # "time": 1727258257216 + # } + # + data = self.safe_dict(response, 'result', {}) + result = self.safe_dict(data, 'result', {}) + fromCurrencyId = self.safe_string(result, 'fromCoin') + toCurrencyId = self.safe_string(result, 'toCoin') + fromCurrency = None + toCurrency = None + if fromCurrencyId is not None: + fromCurrency = self.currency(fromCurrencyId) + if toCurrencyId is not None: + toCurrency = self.currency(toCurrencyId) + return self.parse_conversion(result, fromCurrency, toCurrency) + + def fetch_convert_trade_history(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Conversion]: + """ + fetch the users history of conversion trades + + https://bybit-exchange.github.io/docs/v5/asset/convert/get-convert-history + + :param str [code]: the unified currency code + :param int [since]: the earliest time in ms to fetch conversions for + :param int [limit]: the maximum number of conversion structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.accountType]: eb_convert_uta, eb_convert_spot, eb_convert_funding, eb_convert_inverse, or eb_convert_contract + :returns dict[]: a list of `conversion structures ` + """ + self.load_markets() + request: dict = {} + if limit is not None: + request['limit'] = limit + response = self.privateGetV5AssetExchangeQueryConvertHistory(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "ok", + # "result": { + # "list": [ + # { + # "accountType": "eb_convert_uta", + # "exchangeTxId": "1010020692439483803499737088", + # "userId": "100406395", + # "fromCoin": "USDT", + # "fromCoinType": "crypto", + # "fromAmount": "10", + # "toCoin": "BTC", + # "toCoinType": "crypto", + # "toAmount": "0.00015344889", + # "exchangeStatus": "success", + # "extInfo": {}, + # "convertRate": "0.000015344889", + # "createdAt": "1727257904726" + # } + # ] + # }, + # "retExtInfo": {}, + # "time": 1727258761874 + # } + # + data = self.safe_dict(response, 'result', {}) + dataList = self.safe_list(data, 'list', []) + return self.parse_conversions(dataList, code, 'fromCoin', 'toCoin', since, limit) + + def parse_conversion(self, conversion: dict, fromCurrency: Currency = None, toCurrency: Currency = None) -> Conversion: + # + # fetchConvertQuote + # + # { + # "quoteTxId": "1010020692439481682687668224", + # "exchangeRate": "0.000015330836780000", + # "fromCoin": "USDT", + # "fromCoinType": "crypto", + # "toCoin": "BTC", + # "toCoinType": "crypto", + # "fromAmount": "10", + # "toAmount": "0.000153308367800000", + # "expiredTime": "1727257413353", + # "requestId": "" + # } + # + # createConvertTrade + # + # { + # "exchangeStatus": "processing", + # "quoteTxId": "1010020692439483803499737088" + # } + # + # fetchConvertTrade, fetchConvertTradeHistory + # + # { + # "accountType": "eb_convert_uta", + # "exchangeTxId": "1010020692439483803499737088", + # "userId": "100406395", + # "fromCoin": "USDT", + # "fromCoinType": "crypto", + # "fromAmount": "10", + # "toCoin": "BTC", + # "toCoinType": "crypto", + # "toAmount": "0.00015344889", + # "exchangeStatus": "success", + # "extInfo": {}, + # "convertRate": "0.000015344889", + # "createdAt": "1727257904726" + # } + # + timestamp = self.safe_integer_2(conversion, 'expiredTime', 'createdAt') + fromCoin = self.safe_string(conversion, 'fromCoin') + fromCode = self.safe_currency_code(fromCoin, fromCurrency) + to = self.safe_string(conversion, 'toCoin') + toCode = self.safe_currency_code(to, toCurrency) + return { + 'info': conversion, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': self.safe_string_2(conversion, 'quoteTxId', 'exchangeTxId'), + 'fromCurrency': fromCode, + 'fromAmount': self.safe_number(conversion, 'fromAmount'), + 'toCurrency': toCode, + 'toAmount': self.safe_number(conversion, 'toAmount'), + 'price': None, + 'fee': None, + } + + def fetch_long_short_ratio_history(self, symbol: Str = None, timeframe: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LongShortRatio]: + """ + fetches the long short ratio history for a unified market symbol + + https://bybit-exchange.github.io/docs/v5/market/long-short-ratio + + :param str symbol: unified symbol of the market to fetch the long short ratio for + :param str [timeframe]: the period for the ratio, default is 24 hours + :param int [since]: the earliest time in ms to fetch ratios for + :param int [limit]: the maximum number of long short ratio structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of `long short ratio structures ` + """ + self.load_markets() + market = self.market(symbol) + type = None + type, params = self.get_bybit_type('fetchLongShortRatioHistory', market, params) + if type == 'spot' or type == 'option': + raise NotSupported(self.id + ' fetchLongShortRatioHistory() only support linear and inverse markets') + if timeframe is None: + timeframe = '1d' + request: dict = { + 'symbol': market['id'], + 'period': timeframe, + 'category': type, + } + if limit is not None: + request['limit'] = limit + response = self.publicGetV5MarketAccountRatio(self.extend(request, params)) + # + # { + # "retCode": 0, + # "retMsg": "OK", + # "result": { + # "list": [ + # { + # "symbol": "BTCUSDT", + # "buyRatio": "0.5707", + # "sellRatio": "0.4293", + # "timestamp": "1729123200000" + # }, + # ] + # }, + # "retExtInfo": {}, + # "time": 1729147842516 + # } + # + result = self.safe_dict(response, 'result', {}) + data = self.safe_list(result, 'list', []) + return self.parse_long_short_ratio_history(data, market) + + def parse_long_short_ratio(self, info: dict, market: Market = None) -> LongShortRatio: + # + # { + # "symbol": "BTCUSDT", + # "buyRatio": "0.5707", + # "sellRatio": "0.4293", + # "timestamp": "1729123200000" + # } + # + marketId = self.safe_string(info, 'symbol') + timestamp = self.safe_integer_omit_zero(info, 'timestamp') + longString = self.safe_string(info, 'buyRatio') + shortString = self.safe_string(info, 'sellRatio') + return { + 'info': info, + 'symbol': self.safe_symbol(marketId, market, None, 'contract'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'timeframe': None, + 'longShortRatio': self.parse_to_numeric(Precise.string_div(longString, shortString)), + } + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = self.implode_hostname(self.urls['api'][api]) + '/' + path + if api == 'public': + if params: + url += '?' + self.rawencode(params) + elif api == 'private': + self.check_required_credentials() + isOpenapi = url.find('openapi') >= 0 + isV3UnifiedMargin = url.find('unified/v3') >= 0 + isV3Contract = url.find('contract/v3') >= 0 + isV5UnifiedAccount = url.find('v5') >= 0 + timestamp = str(self.nonce()) + if isOpenapi: + if params: + body = self.json(params) + else: + # self fix for PHP is required otherwise it generates + # '[]' on empty arrays even when forced to use objects + body = '{}' + payload = timestamp + self.apiKey + body + signature = self.hmac(self.encode(payload), self.encode(self.secret), hashlib.sha256, 'hex') + headers = { + 'Content-Type': 'application/json', + 'X-BAPI-API-KEY': self.apiKey, + 'X-BAPI-TIMESTAMP': timestamp, + 'X-BAPI-SIGN': signature, + } + elif isV3UnifiedMargin or isV3Contract or isV5UnifiedAccount: + headers = { + 'Content-Type': 'application/json', + 'X-BAPI-API-KEY': self.apiKey, + 'X-BAPI-TIMESTAMP': timestamp, + 'X-BAPI-RECV-WINDOW': str(self.options['recvWindow']), + } + if isV3UnifiedMargin or isV3Contract: + headers['X-BAPI-SIGN-TYPE'] = '2' + query = self.extend({}, params) + queryEncoded = self.rawencode(query) + auth_base = str(timestamp) + self.apiKey + str(self.options['recvWindow']) + authFull = None + if method == 'POST': + body = self.json(query) + authFull = auth_base + body + else: + authFull = auth_base + queryEncoded + url += '?' + queryEncoded + signature = None + if self.secret.find('PRIVATE KEY') > -1: + signature = self.rsa(authFull, self.secret, 'sha256') + else: + signature = self.hmac(self.encode(authFull), self.encode(self.secret), hashlib.sha256) + headers['X-BAPI-SIGN'] = signature + else: + query = self.extend(params, { + 'api_key': self.apiKey, + 'recv_window': self.options['recvWindow'], + 'timestamp': timestamp, + }) + sortedQuery = self.keysort(query) + auth = self.rawencode(sortedQuery, True) + signature = None + if self.secret.find('PRIVATE KEY') > -1: + signature = self.rsa(auth, self.secret, 'sha256') + else: + signature = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256) + if method == 'POST': + isSpot = url.find('spot') >= 0 + extendedQuery = self.extend(query, { + 'sign': signature, + }) + if isSpot: + body = self.urlencode(extendedQuery) + headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + } + else: + body = self.json(extendedQuery) + headers = { + 'Content-Type': 'application/json', + } + else: + url += '?' + self.rawencode(sortedQuery, True) + url += '&sign=' + signature + if method == 'POST': + brokerId = self.safe_string(self.options, 'brokerId') + if brokerId is not None: + headers['Referer'] = brokerId + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if not response: + return None # fallback to default error handler + # + # { + # "ret_code": 10001, + # "ret_msg": "ReadMapCB: expect {or n, but found \u0000, error " + + # "found in #0 byte of ...||..., bigger context " + + # "...||...", + # "ext_code": '', + # "ext_info": '', + # "result": null, + # "time_now": "1583934106.590436" + # } + # + # { + # "retCode":10001, + # "retMsg":"symbol params err", + # "result":{"symbol":"","bid":"","bidIv":"","bidSize":"","ask":"","askIv":"","askSize":"","lastPrice":"","openInterest":"","indexPrice":"","markPrice":"","markPriceIv":"","change24h":"","high24h":"","low24h":"","volume24h":"","turnover24h":"","totalVolume":"","totalTurnover":"","fundingRate":"","predictedFundingRate":"","nextFundingTime":"","countdownHour":"0","predictedDeliveryPrice":"","underlyingPrice":"","delta":"","gamma":"","vega":"","theta":""} + # } + # + errorCode = self.safe_string_2(response, 'ret_code', 'retCode') + if errorCode != '0': + if errorCode == '30084': + # not an error + # https://github.com/ccxt/ccxt/issues/11268 + # https://github.com/ccxt/ccxt/pull/11624 + # POST https://api.bybit.com/v2/private/position/switch-isolated 200 OK + # {"ret_code":30084,"ret_msg":"Isolated not modified","ext_code":"","ext_info":"","result":null,"time_now":"1642005219.937988","rate_limit_status":73,"rate_limit_reset_ms":1642005219894,"rate_limit":75} + return None + feedback = None + if errorCode == '10005' and url.find('order') < 0: + feedback = self.id + ' private api uses /user/v3/private/query-api to check if you have a unified account. The API key of user id must own one of permissions: "Account Transfer", "Subaccount Transfer", "Withdrawal" ' + body + else: + feedback = self.id + ' ' + body + if body.find('Withdraw address chain or destination tag are not equal') > -1: + feedback = feedback + '; You might also need to ensure the address is whitelisted' + self.throw_broadly_matched_exception(self.exceptions['broad'], body, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + raise ExchangeError(feedback) # unknown message + return None diff --git a/ccxt/cex.py b/ccxt/cex.py new file mode 100644 index 0000000..3d1df20 --- /dev/null +++ b/ccxt/cex.py @@ -0,0 +1,1741 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.cex import ImplicitAPI +import hashlib +from ccxt.base.types import Account, Any, Balances, Currencies, Currency, DepositAddress, Int, LedgerEntry, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, TradingFeeInterface, TradingFees, Transaction, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import NullResponse +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class cex(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(cex, self).describe(), { + 'id': 'cex', + 'name': 'CEX.IO', + 'countries': ['GB', 'EU', 'CY', 'RU'], + 'rateLimit': 300, # 200 req/min + 'pro': True, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, # has, but not through api + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createOrder': True, + 'createOrderWithTakeProfitAndStopLoss': False, + 'createOrderWithTakeProfitAndStopLossWs': False, + 'createPostOnlyOrder': False, + 'createReduceOnlyOrder': False, + 'createStopOrder': True, + 'createTriggerOrder': True, + 'fetchAccounts': True, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchClosedOrder': True, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': True, + 'fetchDepositsWithdrawals': True, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLedger': True, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrder': True, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrderBook': True, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFees': True, + 'fetchVolatilityHistory': False, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'repayMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'transfer': True, + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/27766442-8ddc33b0-5ed8-11e7-8b98-f786aef0f3c9.jpg', + 'api': { + 'public': 'https://trade.cex.io/api/spot/rest-public', + 'private': 'https://trade.cex.io/api/spot/rest', + }, + 'www': 'https://cex.io', + 'doc': 'https://trade.cex.io/docs/', + 'fees': [ + 'https://cex.io/fee-schedule', + 'https://cex.io/limits-commissions', + ], + 'referral': 'https://cex.io/r/0/up105393824/0/', + }, + 'api': { + 'public': { + 'get': {}, + 'post': { + 'get_server_time': 1, + 'get_pairs_info': 1, + 'get_currencies_info': 1, + 'get_processing_info': 10, + 'get_ticker': 1, + 'get_trade_history': 1, + 'get_order_book': 1, + 'get_candles': 1, + }, + }, + 'private': { + 'get': {}, + 'post': { + 'get_my_current_fee': 5, + 'get_fee_strategy': 1, + 'get_my_volume': 5, + 'do_create_account': 1, + 'get_my_account_status_v3': 5, + 'get_my_wallet_balance': 5, + 'get_my_orders': 5, + 'do_my_new_order': 1, + 'do_cancel_my_order': 1, + 'do_cancel_all_orders': 5, + 'get_order_book': 1, + 'get_candles': 1, + 'get_trade_history': 1, + 'get_my_transaction_history': 1, + 'get_my_funding_history': 5, + 'do_my_internal_transfer': 1, + 'get_processing_info': 10, + 'get_deposit_address': 5, + 'do_deposit_funds_from_wallet': 1, + 'do_withdrawal_funds_to_wallet': 1, + }, + }, + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': False, # todo check + 'GTD': True, + }, + 'hedged': False, + 'leverage': False, + 'marketBuyRequiresPrice': False, + 'marketBuyByCost': True, # todo check + 'selfTradePrevention': False, + 'trailing': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': None, + 'fetchOrder': None, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 1000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 100000, + 'daysBackCanceled': 1, + 'untilDays': 100000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 1000, + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'exact': {}, + 'broad': { + 'You have negative balance on following accounts': InsufficientFunds, + 'Mandatory parameter side should be one of BUY,SELL': BadRequest, + 'API orders from Main account are not allowed': BadRequest, + 'check failed': BadRequest, + 'Insufficient funds': InsufficientFunds, + 'Get deposit address for main account is not allowed': PermissionDenied, + 'Market Trigger orders are not allowed': BadRequest, # for some reason, triggerPrice does not work for market orders + 'key not passed or incorrect': AuthenticationError, + }, + }, + 'timeframes': { + '1m': '1m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1h', + '2h': '2h', + '4h': '4h', + '1d': '1d', + }, + 'options': { + 'networks': { + 'BTC': 'bitcoin', + 'ERC20': 'ERC20', + 'BSC20': 'binancesmartchain', + 'DOGE': 'dogecoin', + 'ALGO': 'algorand', + 'XLM': 'stellar', + 'ATOM': 'cosmos', + 'LTC': 'litecoin', + 'XRP': 'ripple', + 'FTM': 'fantom', + 'MINA': 'mina', + 'THETA': 'theta', + 'XTZ': 'tezos', + 'TIA': 'celestia', + 'CRONOS': 'cronos', # CRC20 + 'MATIC': 'polygon', + 'TON': 'ton', + 'TRC20': 'tron', + 'SOLANA': 'solana', + 'SGB': 'songbird', + 'DYDX': 'dydx', + 'DASH': 'dash', + 'ZIL': 'zilliqa', + 'EOS': 'eos', + 'AVALANCHEC': 'avalanche', + 'ETHPOW': 'ethereumpow', + 'NEAR': 'near', + 'ARB': 'arbitrum', + 'DOT': 'polkadot', + 'OPT': 'optimism', + 'INJ': 'injective', + 'ADA': 'cardano', + 'ONT': 'ontology', + 'ICP': 'icp', + 'KAVA': 'kava', + 'KSM': 'kusama', + 'SEI': 'sei', + # 'OSM': 'osmosis', + 'NEO': 'neo', + 'NEO3': 'neo3', + # 'TERRAOLD': 'terra', # tbd + # 'TERRA': 'terra2', # tbd + # 'EVER': 'everscale', # tbd + 'XDC': 'xdc', + }, + }, + }) + + def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://trade.cex.io/docs/#rest-public-api-calls-currencies-info + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + promises = [] + promises.append(self.publicPostGetCurrenciesInfo(params)) + # + # { + # "ok": "ok", + # "data": [ + # { + # "currency": "ZAP", + # "fiat": False, + # "precision": "8", + # "walletPrecision": "6", + # "walletDeposit": True, + # "walletWithdrawal": True + # }, + # ... + # + promises.append(self.publicPostGetProcessingInfo(params)) + # + # { + # "ok": "ok", + # "data": { + # "ADA": { + # "name": "Cardano", + # "blockchains": { + # "cardano": { + # "type": "coin", + # "deposit": "enabled", + # "minDeposit": "1", + # "withdrawal": "enabled", + # "minWithdrawal": "5", + # "withdrawalFee": "1", + # "withdrawalFeePercent": "0", + # "depositConfirmations": "15" + # } + # } + # }, + # ... + # + responses = promises + dataCurrencies = self.safe_list(responses[0], 'data', []) + dataNetworks = self.safe_dict(responses[1], 'data', {}) + currenciesIndexed = self.index_by(dataCurrencies, 'currency') + data = self.deep_extend(currenciesIndexed, dataNetworks) + return self.parse_currencies(self.to_array(data)) + + def parse_currency(self, rawCurrency: dict) -> Currency: + id = self.safe_string(rawCurrency, 'currency') + code = self.safe_currency_code(id) + type = 'fiat' if self.safe_bool(rawCurrency, 'fiat') else 'crypto' + currencyPrecision = self.parse_number(self.parse_precision(self.safe_string(rawCurrency, 'precision'))) + networks: dict = {} + rawNetworks = self.safe_dict(rawCurrency, 'blockchains', {}) + keys = list(rawNetworks.keys()) + for j in range(0, len(keys)): + networkId = keys[j] + rawNetwork = rawNetworks[networkId] + networkCode = self.network_id_to_code(networkId) + deposit = self.safe_string(rawNetwork, 'deposit') == 'enabled' + withdraw = self.safe_string(rawNetwork, 'withdrawal') == 'enabled' + networks[networkCode] = { + 'id': networkId, + 'network': networkCode, + 'margin': None, + 'deposit': deposit, + 'withdraw': withdraw, + 'active': None, + 'fee': self.safe_number(rawNetwork, 'withdrawalFee'), + 'precision': currencyPrecision, + 'limits': { + 'deposit': { + 'min': self.safe_number(rawNetwork, 'minDeposit'), + 'max': None, + }, + 'withdraw': { + 'min': self.safe_number(rawNetwork, 'minWithdrawal'), + 'max': None, + }, + }, + 'info': rawNetwork, + } + return self.safe_currency_structure({ + 'id': id, + 'code': code, + 'name': None, + 'type': type, + 'active': None, + 'deposit': self.safe_bool(rawCurrency, 'walletDeposit'), + 'withdraw': self.safe_bool(rawCurrency, 'walletWithdrawal'), + 'fee': None, + 'precision': currencyPrecision, + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + }, + 'networks': networks, + 'info': rawCurrency, + }) + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for ace + + https://trade.cex.io/docs/#rest-public-api-calls-pairs-info + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = self.publicPostGetPairsInfo(params) + # + # { + # "ok": "ok", + # "data": [ + # { + # "base": "AI", + # "quote": "USD", + # "baseMin": "30", + # "baseMax": "2516000", + # "baseLotSize": "0.000001", + # "quoteMin": "10", + # "quoteMax": "1000000", + # "quoteLotSize": "0.01000000", + # "basePrecision": "6", + # "quotePrecision": "8", + # "pricePrecision": "4", + # "minPrice": "0.0377", + # "maxPrice": "19.5000" + # }, + # ... + # + data = self.safe_list(response, 'data', []) + return self.parse_markets(data) + + def parse_market(self, market: dict) -> Market: + baseId = self.safe_string(market, 'base') + base = self.safe_currency_code(baseId) + quoteId = self.safe_string(market, 'quote') + quote = self.safe_currency_code(quoteId) + id = base + '-' + quote # not actual id, but for self exchange we can use self abbreviation, because e.g. tickers have hyphen in between + symbol = base + '/' + quote + return self.safe_market_structure({ + 'id': id, + 'symbol': symbol, + 'base': base, + 'baseId': baseId, + 'quote': quote, + 'quoteId': quoteId, + 'settle': None, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'limits': { + 'amount': { + 'min': self.safe_number(market, 'baseMin'), + 'max': self.safe_number(market, 'baseMax'), + }, + 'price': { + 'min': self.safe_number(market, 'minPrice'), + 'max': self.safe_number(market, 'maxPrice'), + }, + 'cost': { + 'min': self.safe_number(market, 'quoteMin'), + 'max': self.safe_number(market, 'quoteMax'), + }, + 'leverage': { + 'min': None, + 'max': None, + }, + }, + 'precision': { + 'amount': self.safe_string(market, 'baseLotSize'), + 'price': self.parse_number(self.parse_precision(self.safe_string(market, 'pricePrecision'))), + # 'cost': self.parse_number(self.parse_precision(self.safe_string(market, 'quoteLotSize'))), # buggy, doesn't reflect their documentation + 'base': self.parse_number(self.parse_precision(self.safe_string(market, 'basePrecision'))), + 'quote': self.parse_number(self.parse_precision(self.safe_string(market, 'quotePrecision'))), + }, + 'active': None, + 'created': None, + 'info': market, + }) + + def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = self.publicPostGetServerTime(params) + # + # { + # "ok": "ok", + # "data": { + # "timestamp": "1728472063472", + # "ISODate": "2024-10-09T11:07:43.472Z" + # } + # } + # + data = self.safe_dict(response, 'data') + timestamp = self.safe_integer(data, 'timestamp') + return timestamp + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://trade.cex.io/docs/#rest-public-api-calls-ticker + + :param str symbol: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + response = self.fetch_tickers([symbol], params) + return self.safe_dict(response, symbol, {}) + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://trade.cex.io/docs/#rest-public-api-calls-ticker + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + request = {} + if symbols is not None: + request['pairs'] = self.market_ids(symbols) + response = self.publicPostGetTicker(self.extend(request, params)) + # + # { + # "ok": "ok", + # "data": { + # "AI-USD": { + # "bestBid": "0.3917", + # "bestAsk": "0.3949", + # "bestBidChange": "0.0035", + # "bestBidChangePercentage": "0.90", + # "bestAskChange": "0.0038", + # "bestAskChangePercentage": "0.97", + # "low": "0.3787", + # "high": "0.3925", + # "volume30d": "2945.722277", + # "lastTradeDateISO": "2024-10-11T06:18:42.077Z", + # "volume": "120.736000", + # "quoteVolume": "46.65654070", + # "lastTradeVolume": "67.914000", + # "volumeUSD": "46.65", + # "last": "0.3949", + # "lastTradePrice": "0.3925", + # "priceChange": "0.0038", + # "priceChangePercentage": "0.97" + # }, + # ... + # + data = self.safe_dict(response, 'data', {}) + return self.parse_tickers(data, symbols) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + marketId = self.safe_string(ticker, 'id') + symbol = self.safe_symbol(marketId, market) + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': None, + 'datetime': None, + 'high': self.safe_number(ticker, 'high'), + 'low': self.safe_number(ticker, 'low'), + 'bid': self.safe_number(ticker, 'bestBid'), + 'bidVolume': None, + 'ask': self.safe_number(ticker, 'bestAsk'), + 'askVolume': None, + 'vwap': None, + 'open': None, + 'close': self.safe_string(ticker, 'last'), # last indicative price per api docs(difference also seen here: https://github.com/ccxt/ccxt/actions/runs/14593899575/job/40935513901?pr=25767#step:11:456 ) + 'previousClose': None, + 'change': self.safe_number(ticker, 'priceChange'), + 'percentage': self.safe_number(ticker, 'priceChangePercentage'), + 'average': None, + 'baseVolume': self.safe_string(ticker, 'volume'), + 'quoteVolume': self.safe_string(ticker, 'quoteVolume'), + 'info': ticker, + }, market) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://trade.cex.io/docs/#rest-public-api-calls-trade-history + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest entry + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + if since is not None: + request['fromDateISO'] = self.iso8601(since) + until = None + until, params = self.handle_param_integer_2(params, 'until', 'till') + if until is not None: + request['toDateISO'] = self.iso8601(until) + if limit is not None: + request['pageSize'] = min(limit, 10000) # has a bug, still returns more trades + response = self.publicPostGetTradeHistory(self.extend(request, params)) + # + # { + # "ok": "ok", + # "data": { + # "pageSize": "10", + # "trades": [ + # { + # "tradeId": "1728630559823-0", + # "dateISO": "2024-10-11T07:09:19.823Z", + # "side": "SELL", + # "price": "60879.5", + # "amount": "0.00165962" + # }, + # ... followed by older trades + # + data = self.safe_dict(response, 'data', {}) + trades = self.safe_list(data, 'trades', []) + return self.parse_trades(trades, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # public fetchTrades + # + # { + # "tradeId": "1728630559823-0", + # "dateISO": "2024-10-11T07:09:19.823Z", + # "side": "SELL", + # "price": "60879.5", + # "amount": "0.00165962" + # }, + # + dateStr = self.safe_string(trade, 'dateISO') + timestamp = self.parse8601(dateStr) + market = self.safe_market(None, market) + return self.safe_trade({ + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'id': self.safe_string(trade, 'tradeId'), + 'order': None, + 'type': None, + 'takerOrMaker': None, + 'side': self.safe_string_lower(trade, 'side'), + 'price': self.safe_string(trade, 'price'), + 'amount': self.safe_string(trade, 'amount'), + 'cost': None, + 'fee': None, + }, market) + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://trade.cex.io/docs/#rest-public-api-calls-order-book + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + response = self.publicPostGetOrderBook(self.extend(request, params)) + # + # { + # "ok": "ok", + # "data": { + # "timestamp": "1728636922648", + # "currency1": "BTC", + # "currency2": "USDT", + # "bids": [ + # [ + # "60694.1", + # "13.12849761" + # ], + # [ + # "60694.0", + # "0.71829244" + # ], + # ... + # + orderBook = self.safe_dict(response, 'data', {}) + timestamp = self.safe_integer(orderBook, 'timestamp') + return self.parse_order_book(orderBook, market['symbol'], timestamp) + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://trade.cex.io/docs/#rest-public-api-calls-candles + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest entry + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + dataType = None + dataType, params = self.handle_option_and_params(params, 'fetchOHLCV', 'dataType') + if dataType is None: + raise ArgumentsRequired(self.id + ' fetchOHLCV requires a parameter "dataType" to be either "bestBid" or "bestAsk"') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + 'resolution': self.timeframes[timeframe], + 'dataType': dataType, + } + if since is not None: + request['fromISO'] = self.iso8601(since) + until = None + until, params = self.handle_param_integer_2(params, 'until', 'till') + if until is not None: + request['toISO'] = self.iso8601(until) + elif since is None: + # exchange still requires that we provide one of them + request['toISO'] = self.iso8601(self.milliseconds()) + if since is not None and until is not None and limit is not None: + raise ArgumentsRequired(self.id + ' fetchOHLCV does not support fetching candles with both a limit and since/until') + elif (since is not None or until is not None) and limit is None: + raise ArgumentsRequired(self.id + ' fetchOHLCV requires a limit parameter when fetching candles with since or until') + if limit is not None: + request['limit'] = limit + response = self.publicPostGetCandles(self.extend(request, params)) + # + # { + # "ok": "ok", + # "data": [ + # { + # "timestamp": "1728643320000", + # "open": "61061", + # "high": "61095.1", + # "low": "61048.5", + # "close": "61087.8", + # "volume": "0", + # "resolution": "1m", + # "isClosed": True, + # "timestampISO": "2024-10-11T10:42:00.000Z" + # }, + # ... + # + data = self.safe_list(response, 'data', []) + return self.parse_ohlcvs(data, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + return [ + self.safe_integer(ohlcv, 'timestamp'), + self.safe_number(ohlcv, 'open'), + self.safe_number(ohlcv, 'high'), + self.safe_number(ohlcv, 'low'), + self.safe_number(ohlcv, 'close'), + self.safe_number(ohlcv, 'volume'), + ] + + def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://trade.cex.io/docs/#rest-public-api-calls-candles + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + self.load_markets() + response = self.privatePostGetMyCurrentFee(params) + # + # { + # "ok": "ok", + # "data": { + # "tradingFee": { + # "AI-USD": { + # "percent": "0.25" + # }, + # ... + # + data = self.safe_dict(response, 'data', {}) + fees = self.safe_dict(data, 'tradingFee', {}) + return self.parse_trading_fees(fees, True) + + def parse_trading_fees(self, response, useKeyAsId=False) -> TradingFees: + result: dict = {} + keys = list(response.keys()) + for i in range(0, len(keys)): + key = keys[i] + market = None + if useKeyAsId: + market = self.safe_market(key) + parsed = self.parse_trading_fee(response[key], market) + result[parsed['symbol']] = parsed + for i in range(0, len(self.symbols)): + symbol = self.symbols[i] + if not (symbol in result): + market = self.market(symbol) + result[symbol] = self.parse_trading_fee(response, market) + return result + + def parse_trading_fee(self, fee: dict, market: Market = None) -> TradingFeeInterface: + return { + 'info': fee, + 'symbol': self.safe_string(market, 'symbol'), + 'maker': self.safe_number(fee, 'percent'), + 'taker': self.safe_number(fee, 'percent'), + 'percentage': None, + 'tierBased': None, + } + + def fetch_accounts(self, params={}) -> List[Account]: + self.load_markets() + response = self.privatePostGetMyAccountStatusV3(params) + # + # { + # "ok": "ok", + # "data": { + # "convertedCurrency": "USD", + # "balancesPerAccounts": { + # "": { + # "AI": { + # "balance": "0.000000", + # "balanceOnHold": "0.000000" + # }, + # "USDT": { + # "balance": "0.00000000", + # "balanceOnHold": "0.00000000" + # } + # } + # } + # } + # } + # + data = self.safe_dict(response, 'data', {}) + balances = self.safe_dict(data, 'balancesPerAccounts', {}) + arrays = self.to_array(balances) + return self.parse_accounts(arrays, params) + + def parse_account(self, account: dict) -> Account: + return { + 'id': None, + 'type': None, + 'code': None, + 'info': account, + } + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://trade.cex.io/docs/#rest-private-api-calls-account-status-v3 + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param dict [params.method]: 'privatePostGetMyWalletBalance' or 'privatePostGetMyAccountStatusV3' + :param dict [params.account]: in case 'privatePostGetMyAccountStatusV3' is chosen, self can specify the account name(default is empty string) + :returns dict: a `balance structure ` + """ + accountName = None + accountName, params = self.handle_param_string(params, 'account', '') # default is empty string + method = None + method, params = self.handle_param_string(params, 'method', 'privatePostGetMyWalletBalance') + accountBalance = None + if method == 'privatePostGetMyAccountStatusV3': + response = self.privatePostGetMyAccountStatusV3(params) + # + # { + # "ok": "ok", + # "data": { + # "convertedCurrency": "USD", + # "balancesPerAccounts": { + # "": { + # "AI": { + # "balance": "0.000000", + # "balanceOnHold": "0.000000" + # }, + # .... + # + data = self.safe_dict(response, 'data', {}) + balances = self.safe_dict(data, 'balancesPerAccounts', {}) + accountBalance = self.safe_dict(balances, accountName, {}) + else: + response = self.privatePostGetMyWalletBalance(params) + # + # { + # "ok": "ok", + # "data": { + # "AI": { + # "balance": "25.606429" + # }, + # "USDT": { + # "balance": "7.935449" + # }, + # ... + # + accountBalance = self.safe_dict(response, 'data', {}) + return self.parse_balance(accountBalance) + + def parse_balance(self, response) -> Balances: + result: dict = { + 'info': response, + } + keys = list(response.keys()) + for i in range(0, len(keys)): + key = keys[i] + balance = self.safe_dict(response, key, {}) + code = self.safe_currency_code(key) + account: dict = { + 'used': self.safe_string(balance, 'balanceOnHold'), + 'total': self.safe_string(balance, 'balance'), + } + result[code] = account + return self.safe_balance(result) + + def fetch_orders_by_status(self, status: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://trade.cex.io/docs/#rest-private-api-calls-orders + + :param str status: order status to fetch for + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest entry + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + request: dict = {} + isClosedOrders = (status == 'closed') + if isClosedOrders: + request['archived'] = True + market = None + if symbol is not None: + market = self.market(symbol) + request['pair'] = market['id'] + if limit is not None: + request['pageSize'] = limit + if since is not None: + request['serverCreateTimestampFrom'] = since + elif isClosedOrders: + # exchange requires a `since` parameter for closed orders, so set default to allowed 365 + request['serverCreateTimestampFrom'] = self.milliseconds() - 364 * 24 * 60 * 60 * 1000 + until = None + until, params = self.handle_param_integer_2(params, 'until', 'till') + if until is not None: + request['serverCreateTimestampTo'] = until + response = self.privatePostGetMyOrders(self.extend(request, params)) + # + # if called without `pair` + # + # { + # "ok": "ok", + # "data": [ + # { + # "orderId": "1313003", + # "clientOrderId": "037F0AFEB93A", + # "clientId": "up421412345", + # "accountId": null, + # "status": "FILLED", + # "statusIsFinal": True, + # "currency1": "AI", + # "currency2": "USDT", + # "side": "BUY", + # "orderType": "Market", + # "timeInForce": "IOC", + # "comment": null, + # "rejectCode": null, + # "rejectReason": null, + # "initialOnHoldAmountCcy1": null, + # "initialOnHoldAmountCcy2": "10.23456700", + # "executedAmountCcy1": "25.606429", + # "executedAmountCcy2": "10.20904439", + # "requestedAmountCcy1": null, + # "requestedAmountCcy2": "10.20904439", + # "originalAmountCcy2": "10.23456700", + # "feeAmount": "0.02552261", + # "feeCurrency": "USDT", + # "price": null, + # "averagePrice": "0.3986", + # "clientCreateTimestamp": "1728474625320", + # "serverCreateTimestamp": "1728474624956", + # "lastUpdateTimestamp": "1728474628015", + # "expireTime": null, + # "effectiveTime": null + # }, + # ... + # + data = self.safe_list(response, 'data', []) + return self.parse_orders(data, market, since, limit) + + def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + + https://trade.cex.io/docs/#rest-private-api-calls-orders + + fetches information on multiple canceled orders made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: timestamp in ms of the earliest order, default is None + :param int [limit]: max number of orders to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `order structures ` + """ + return self.fetch_orders_by_status('closed', symbol, since, limit, params) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + + https://trade.cex.io/docs/#rest-private-api-calls-orders + + fetches information on multiple canceled orders made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: timestamp in ms of the earliest order, default is None + :param int [limit]: max number of orders to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `order structures ` + """ + return self.fetch_orders_by_status('open', symbol, since, limit, params) + + def fetch_open_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an open order made by the user + + https://trade.cex.io/docs/#rest-private-api-calls-orders + + :param str id: order id + :param str [symbol]: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + request: dict = { + 'orderId': int(id), + } + result = self.fetch_open_orders(symbol, None, None, self.extend(request, params)) + return result[0] + + def fetch_closed_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an closed order made by the user + + https://trade.cex.io/docs/#rest-private-api-calls-orders + + :param str id: order id + :param str [symbol]: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + request: dict = { + 'orderId': int(id), + } + result = self.fetch_closed_orders(symbol, None, None, self.extend(request, params)) + return result[0] + + def parse_order_status(self, status: Str): + statuses: dict = { + 'PENDING_NEW': 'open', + 'NEW': 'open', + 'PARTIALLY_FILLED': 'open', + 'FILLED': 'closed', + 'EXPIRED': 'expired', + 'REJECTED': 'rejected', + 'PENDING_CANCEL': 'canceling', + 'CANCELLED': 'canceled', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # "orderId": "1313003", + # "clientOrderId": "037F0AFEB93A", + # "clientId": "up421412345", + # "accountId": null, + # "status": "FILLED", + # "statusIsFinal": True, + # "currency1": "AI", + # "currency2": "USDT", + # "side": "BUY", + # "orderType": "Market", + # "timeInForce": "IOC", + # "comment": null, + # "rejectCode": null, + # "rejectReason": null, + # "initialOnHoldAmountCcy1": null, + # "initialOnHoldAmountCcy2": "10.23456700", + # "executedAmountCcy1": "25.606429", + # "executedAmountCcy2": "10.20904439", + # "requestedAmountCcy1": null, + # "requestedAmountCcy2": "10.20904439", + # "originalAmountCcy2": "10.23456700", + # "feeAmount": "0.02552261", + # "feeCurrency": "USDT", + # "price": null, + # "averagePrice": "0.3986", + # "clientCreateTimestamp": "1728474625320", + # "serverCreateTimestamp": "1728474624956", + # "lastUpdateTimestamp": "1728474628015", + # "expireTime": null, + # "effectiveTime": null + # + currency1 = self.safe_string(order, 'currency1') + currency2 = self.safe_string(order, 'currency2') + marketId = None + if currency1 is not None and currency2 is not None: + marketId = currency1 + '-' + currency2 + market = self.safe_market(marketId, market) + symbol = market['symbol'] + status = self.parse_order_status(self.safe_string(order, 'status')) + fee = {} + feeAmount = self.safe_number(order, 'feeAmount') + if feeAmount is not None: + currencyId = self.safe_string(order, 'feeCurrency') + feeCode = self.safe_currency_code(currencyId) + fee['currency'] = feeCode + fee['cost'] = feeAmount + timestamp = self.safe_integer(order, 'serverCreateTimestamp') + requestedBase = self.safe_number(order, 'requestedAmountCcy1') + executedBase = self.safe_number(order, 'executedAmountCcy1') + # requestedQuote = self.safe_number(order, 'requestedAmountCcy2') + executedQuote = self.safe_number(order, 'executedAmountCcy2') + return self.safe_order({ + 'id': self.safe_string(order, 'orderId'), + 'clientOrderId': self.safe_string(order, 'clientOrderId'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastUpdateTimestamp': self.safe_integer(order, 'lastUpdateTimestamp'), + 'lastTradeTimestamp': None, + 'symbol': symbol, + 'type': self.safe_string_lower(order, 'orderType'), + 'timeInForce': self.safe_string(order, 'timeInForce'), + 'postOnly': None, + 'side': self.safe_string_lower(order, 'side'), + 'price': self.safe_number(order, 'price'), + 'triggerPrice': self.safe_number(order, 'stopPrice'), + 'amount': requestedBase, + 'cost': executedQuote, + 'average': self.safe_number(order, 'averagePrice'), + 'filled': executedBase, + 'remaining': None, + 'status': status, + 'fee': fee, + 'trades': None, + 'info': order, + }, market) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://trade.cex.io/docs/#rest-private-api-calls-new-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.accountId]: account-id to use(default is empty string) + :param float [params.triggerPrice]: the price at which a trigger order is triggered at + :returns dict: an `order structure ` + """ + accountId = None + accountId, params = self.handle_option_and_params(params, 'createOrder', 'accountId') + if accountId is None: + raise ArgumentsRequired(self.id + ' createOrder() : API trading is now allowed from main account, set params["accountId"] or .options["createOrder"]["accountId"] to the name of your sub-account') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'clientOrderId': self.uuid(), + 'currency1': market['baseId'], + 'currency2': market['quoteId'], + 'accountId': accountId, + 'orderType': self.capitalize(type.lower()), + 'side': side.upper(), + 'timestamp': self.milliseconds(), + 'amountCcy1': self.amount_to_precision(symbol, amount), + } + timeInForce = None + timeInForce, params = self.handle_option_and_params(params, 'createOrder', 'timeInForce', 'GTC') + if type == 'limit': + request['price'] = self.price_to_precision(symbol, price) + request['timeInForce'] = timeInForce + triggerPrice = None + triggerPrice, params = self.handle_param_string(params, 'triggerPrice') + if triggerPrice is not None: + request['type'] = 'Stop Limit' + request['stopPrice'] = triggerPrice + response = self.privatePostDoMyNewOrder(self.extend(request, params)) + # + # on success + # + # { + # "ok": "ok", + # "data": { + # "messageType": "executionReport", + # "clientId": "up132245425", + # "orderId": "1318485", + # "clientOrderId": "b5b6cd40-154c-4c1c-bd51-4a442f3d50b9", + # "accountId": "sub1", + # "status": "FILLED", + # "currency1": "LTC", + # "currency2": "USDT", + # "side": "BUY", + # "executedAmountCcy1": "0.23000000", + # "executedAmountCcy2": "15.09030000", + # "requestedAmountCcy1": "0.23000000", + # "requestedAmountCcy2": null, + # "orderType": "Market", + # "timeInForce": null, + # "comment": null, + # "executionType": "Trade", + # "executionId": "1726747124624_101_41116", + # "transactTime": "2024-10-15T15:08:12.794Z", + # "expireTime": null, + # "effectiveTime": null, + # "averagePrice": "65.61", + # "lastQuantity": "0.23000000", + # "lastAmountCcy1": "0.23000000", + # "lastAmountCcy2": "15.09030000", + # "lastPrice": "65.61", + # "feeAmount": "0.03772575", + # "feeCurrency": "USDT", + # "clientCreateTimestamp": "1729004892014", + # "serverCreateTimestamp": "1729004891628", + # "lastUpdateTimestamp": "1729004892786" + # } + # } + # + # on failure, there are extra fields + # + # "status": "REJECTED", + # "requestedAmountCcy1": null, + # "orderRejectReason": "{\\" code \\ ":405,\\" reason \\ ":\\" Either AmountCcy1(OrderQty)or AmountCcy2(CashOrderQty)should be specified for market order not both \\ "}", + # "rejectCode": 405, + # "rejectReason": "Either AmountCcy1(OrderQty) or AmountCcy2(CashOrderQty) should be specified for market order not both", + # + data = self.safe_dict(response, 'data') + return self.parse_order(data, market) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://trade.cex.io/docs/#rest-private-api-calls-cancel-order + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + request: dict = { + 'orderId': int(id), + 'cancelRequestId': 'c_' + str((self.milliseconds())), + 'timestamp': self.milliseconds(), + } + response = self.privatePostDoCancelMyOrder(self.extend(request, params)) + # + # {"ok":"ok","data":{}} + # + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data) + + def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders in a market + + https://trade.cex.io/docs/#rest-private-api-calls-cancel-all-orders + + :param str symbol: alpaca cancelAllOrders cannot setting symbol, it will cancel all open orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + self.load_markets() + response = self.privatePostDoCancelAllOrders(params) + # + # { + # "ok": "ok", + # "data": { + # "clientOrderIds": [ + # "3AF77B67109F" + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + ids = self.safe_list(data, 'clientOrderIds', []) + orders = [] + for i in range(0, len(ids)): + id = ids[i] + orders.append({'clientOrderId': id}) + return self.parse_orders(orders) + + def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + + https://trade.cex.io/docs/#rest-private-api-calls-transaction-history + + :param str [code]: unified currency code + :param int [since]: timestamp in ms of the earliest ledger entry + :param int [limit]: max number of ledger entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest ledger entry + :returns dict: a `ledger structure ` + """ + self.load_markets() + currency = None + request: dict = {} + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + if since is not None: + request['dateFrom'] = since + if limit is not None: + request['pageSize'] = limit + until = None + until, params = self.handle_param_integer_2(params, 'until', 'till') + if until is not None: + request['dateTo'] = until + response = self.privatePostGetMyTransactionHistory(self.extend(request, params)) + # + # { + # "ok": "ok", + # "data": [ + # { + # "transactionId": "30367722", + # "timestamp": "2024-10-14T14:08:49.987Z", + # "accountId": "", + # "type": "withdraw", + # "amount": "-12.39060600", + # "details": "Withdraw fundingId=1235039 clientId=up421412345 walletTxId=76337154166", + # "currency": "USDT" + # }, + # ... + # + data = self.safe_list(response, 'data', []) + return self.parse_ledger(data, currency, since, limit) + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + amount = self.safe_string(item, 'amount') + direction = None + if Precise.string_le(amount, '0'): + direction = 'out' + amount = Precise.string_mul('-1', amount) + else: + direction = 'in' + currencyId = self.safe_string(item, 'currency') + currency = self.safe_currency(currencyId, currency) + code = self.safe_currency_code(currencyId, currency) + timestampString = self.safe_string(item, 'timestamp') + timestamp = self.parse8601(timestampString) + type = self.safe_string(item, 'type') + return self.safe_ledger_entry({ + 'info': item, + 'id': self.safe_string(item, 'transactionId'), + 'direction': direction, + 'account': self.safe_string(item, 'accountId', ''), + 'referenceAccount': None, + 'referenceId': None, + 'type': self.parse_ledger_entry_type(type), + 'currency': code, + 'amount': self.parse_number(amount), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'before': None, + 'after': None, + 'status': None, + 'fee': None, + }, currency) + + def parse_ledger_entry_type(self, type): + ledgerType: dict = { + 'deposit': 'deposit', + 'withdraw': 'withdrawal', + 'commission': 'fee', + } + return self.safe_string(ledgerType, type, type) + + def fetch_deposits_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch history of deposits and withdrawals + + https://trade.cex.io/docs/#rest-private-api-calls-funding-history + + :param str [code]: unified currency code for the currency of the deposit/withdrawals, default is None + :param int [since]: timestamp in ms of the earliest deposit/withdrawal, default is None + :param int [limit]: max number of deposit/withdrawals to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `transaction structure ` + """ + self.load_markets() + request: dict = {} + currency = None + if code is not None: + currency = self.currency(code) + if since is not None: + request['dateFrom'] = since + if limit is not None: + request['pageSize'] = limit + until = None + until, params = self.handle_param_integer_2(params, 'until', 'till') + if until is not None: + request['dateTo'] = until + response = self.privatePostGetMyFundingHistory(self.extend(request, params)) + # + # { + # "ok": "ok", + # "data": [ + # { + # "clientId": "up421412345", + # "accountId": "", + # "currency": "USDT", + # "direction": "withdraw", + # "amount": "12.39060600", + # "commissionAmount": "0.00000000", + # "status": "approved", + # "updatedAt": "2024-10-14T14:08:50.013Z", + # "txId": "30367718", + # "details": {} + # }, + # ... + # + data = self.safe_list(response, 'data', []) + return self.parse_transactions(data, currency, since, limit) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + currencyId = self.safe_string(transaction, 'currency') + direction = self.safe_string(transaction, 'direction') + type = 'withdrawal' if (direction == 'withdraw') else 'deposit' + code = self.safe_currency_code(currencyId, currency) + updatedAt = self.safe_string(transaction, 'updatedAt') + timestamp = self.parse8601(updatedAt) + return { + 'info': transaction, + 'id': self.safe_string(transaction, 'txId'), + 'txid': None, + 'type': type, + 'currency': code, + 'network': None, + 'amount': self.safe_number(transaction, 'amount'), + 'status': self.parse_transaction_status(self.safe_string(transaction, 'status')), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'address': None, + 'addressFrom': None, + 'addressTo': None, + 'tag': None, + 'tagFrom': None, + 'tagTo': None, + 'updated': None, + 'comment': None, + 'fee': { + 'currency': code, + 'cost': self.safe_number(transaction, 'commissionAmount'), + }, + 'internal': None, + } + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'rejected': 'rejected', + 'pending': 'pending', + 'approved': 'ok', + } + return self.safe_string(statuses, status, status) + + def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://trade.cex.io/docs/#rest-private-api-calls-internal-transfer + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: 'SPOT', 'FUND', or 'CONTRACT' + :param str toAccount: 'SPOT', 'FUND', or 'CONTRACT' + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transfer structure ` + """ + transfer = None + if toAccount != '' and fromAccount != '': + transfer = self.transfer_between_sub_accounts(code, amount, fromAccount, toAccount, params) + else: + transfer = self.transfer_between_main_and_sub_account(code, amount, fromAccount, toAccount, params) + fillResponseFromRequest = self.handle_option('transfer', 'fillResponseFromRequest', True) + if fillResponseFromRequest: + transfer['fromAccount'] = fromAccount + transfer['toAccount'] = toAccount + return transfer + + def transfer_between_main_and_sub_account(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + self.load_markets() + currency = self.currency(code) + fromMain = (fromAccount == '') + targetAccount = toAccount if fromMain else fromAccount + guid = self.safe_string(params, 'guid', self.uuid()) + request: dict = { + 'currency': currency['id'], + 'amount': self.currency_to_precision(code, amount), + 'accountId': targetAccount, + 'clientTxId': guid, + } + response = None + if fromMain: + response = self.privatePostDoDepositFundsFromWallet(self.extend(request, params)) + else: + response = self.privatePostDoWithdrawalFundsToWallet(self.extend(request, params)) + # both endpoints return the same structure, the only difference is that + # the "accountId" is filled with the "subAccount" + # + # { + # "ok": "ok", + # "data": { + # "accountId": "sub1", + # "clientTxId": "27ba8284-67cf-4386-9ec7-80b3871abd45", + # "currency": "USDT", + # "status": "approved" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_transfer(data, currency) + + def transfer_between_sub_accounts(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + 'amount': self.currency_to_precision(code, amount), + 'fromAccountId': fromAccount, + 'toAccountId': toAccount, + } + response = self.privatePostDoMyInternalTransfer(self.extend(request, params)) + # + # { + # "ok": "ok", + # "data": { + # "transactionId": "30225415" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_transfer(data, currency) + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + # + # transferBetweenSubAccounts + # + # { + # "ok": "ok", + # "data": { + # "transactionId": "30225415" + # } + # } + # + # transfer between main/sub + # + # { + # "ok": "ok", + # "data": { + # "accountId": "sub1", + # "clientTxId": "27ba8284-67cf-4386-9ec7-80b3871abd45", + # "currency": "USDT", + # "status": "approved" + # } + # } + # + currencyId = self.safe_string(transfer, 'currency') + currencyCode = self.safe_currency_code(currencyId, currency) + return { + 'info': transfer, + 'id': self.safe_string_2(transfer, 'transactionId', 'clientTxId'), + 'timestamp': None, + 'datetime': None, + 'currency': currencyCode, + 'amount': None, + 'fromAccount': None, + 'toAccount': None, + 'status': self.parse_transaction_status(self.safe_string(transfer, 'status')), + } + + def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://trade.cex.io/docs/#rest-private-api-calls-deposit-address + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.accountId]: account-id(default to empty string) to refer to(at self moment, only sub-accounts allowed by exchange) + :returns dict: an `address structure ` + """ + accountId = None + accountId, params = self.handle_option_and_params(params, 'createOrder', 'accountId') + if accountId is None: + raise ArgumentsRequired(self.id + ' fetchDepositAddress() : main account is not allowed to fetch deposit address from api, set params["accountId"] or .options["createOrder"]["accountId"] to the name of your sub-account') + self.load_markets() + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + currency = self.currency(code) + request: dict = { + 'accountId': accountId, + 'currency': currency['id'], # documentation is wrong about self param + 'blockchain': self.network_code_to_id(networkCode), + } + response = self.privatePostGetDepositAddress(self.extend(request, params)) + # + # { + # "ok": "ok", + # "data": { + # "address": "TCr..................1AE", + # "accountId": "sub1", + # "currency": "USDT", + # "blockchain": "tron" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_deposit_address(data, currency) + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + address = self.safe_string(depositAddress, 'address') + currencyId = self.safe_string(depositAddress, 'currency') + currency = self.safe_currency(currencyId, currency) + self.check_address(address) + return { + 'info': depositAddress, + 'currency': currency['code'], + 'network': self.network_id_to_code(self.safe_string(depositAddress, 'blockchain')), + 'address': address, + 'tag': None, + } + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = self.urls['api'][api] + '/' + self.implode_params(path, params) + query = self.omit(params, self.extract_params(path)) + if api == 'public': + if method == 'GET': + if query: + url += '?' + self.urlencode(query) + else: + body = self.json(query) + headers = { + 'Content-Type': 'application/json', + } + else: + self.check_required_credentials() + seconds = str(self.seconds()) + body = self.json(query) + auth = path + seconds + body + signature = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256, 'base64') + headers = { + 'Content-Type': 'application/json', + 'X-AGGR-KEY': self.apiKey, + 'X-AGGR-TIMESTAMP': seconds, + 'X-AGGR-SIGNATURE': signature, + } + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + # in some cases, like from createOrder, exchange returns nested escaped JSON string: + # {"ok":"ok","data":{"messageType":"executionReport", "orderRejectReason":"{\"code\":405}"}} + # and because of `.parseJson` bug, we need extra fix + if response is None: + if body is None: + raise NullResponse(self.id + ' returned empty response') + elif body[0] == '{': + fixed = self.fix_stringified_json_members(body) + response = self.parse_json(fixed) + else: + raise NullResponse(self.id + ' returned unparsed response: ' + body) + error = self.safe_string(response, 'error') + if error is not None: + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], error, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], error, feedback) + raise ExchangeError(feedback) + # check errors in order-engine(the responses are not standard, so we parse here) + if url.find('do_my_new_order') >= 0: + data = self.safe_dict(response, 'data', {}) + rejectReason = self.safe_string(data, 'rejectReason') + if rejectReason is not None: + self.throw_broadly_matched_exception(self.exceptions['broad'], rejectReason, rejectReason) + raise ExchangeError(self.id + ' createOrder() ' + rejectReason) + return None diff --git a/ccxt/coinbase.py b/ccxt/coinbase.py new file mode 100644 index 0000000..9fa269e --- /dev/null +++ b/ccxt/coinbase.py @@ -0,0 +1,5001 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.coinbase import ImplicitAPI +import hashlib +from ccxt.base.types import Account, Any, Balances, Conversion, Currencies, Currency, DepositAddress, Int, LedgerEntry, Market, Num, Order, OrderBook, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, Trade, TradingFees, Transaction, MarketInterface +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import NotSupported +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import InvalidNonce +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class coinbase(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(coinbase, self).describe(), { + 'id': 'coinbase', + 'name': 'Coinbase Advanced', + 'countries': ['US'], + 'pro': True, + 'certified': False, + # rate-limits: + # ADVANCED API: https://docs.cloud.coinbase.com/advanced-trade/docs/rest-api-rate-limits + # - max 30 req/second for private data, 10 req/s for public data + # DATA API : https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/rate-limiting + # - max 10000 req/hour(to prevent userland mistakes we apply ~3 req/second RL per call + 'rateLimit': 34, + 'version': 'v2', + 'userAgent': self.userAgents['chrome'], + 'headers': { + 'CB-VERSION': '2018-05-30', + }, + 'has': { + 'CORS': True, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelOrder': True, + 'cancelOrders': True, + 'closeAllPositions': False, + 'closePosition': True, + 'createConvertTrade': True, + 'createDepositAddress': True, + 'createLimitBuyOrder': True, + 'createLimitSellOrder': True, + 'createMarketBuyOrder': True, + 'createMarketBuyOrderWithCost': True, + 'createMarketOrderWithCost': False, + 'createMarketSellOrder': True, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createOrderWithTakeProfitAndStopLoss': False, + 'createOrderWithTakeProfitAndStopLossWs': False, + 'createPostOnlyOrder': True, + 'createReduceOnlyOrder': False, + 'createStopLimitOrder': True, + 'createStopMarketOrder': False, + 'createStopOrder': True, + 'deposit': True, + 'editOrder': True, + 'fetchAccounts': True, + 'fetchBalance': True, + 'fetchBidsAsks': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchCanceledOrders': True, + 'fetchClosedOrders': True, + 'fetchConvertQuote': True, + 'fetchConvertTrade': True, + 'fetchConvertTradeHistory': False, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDeposit': True, + 'fetchDepositAddress': 'emulated', + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': True, + 'fetchDepositMethodId': True, + 'fetchDepositMethodIds': True, + 'fetchDeposits': True, + 'fetchDepositsWithdrawals': True, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchL2OrderBook': False, + 'fetchLedger': True, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrices': False, + 'fetchMyBuys': True, + 'fetchMyLiquidations': False, + 'fetchMySells': True, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchPosition': True, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': True, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': 'emulated', + 'fetchTradingFees': True, + 'fetchVolatilityHistory': False, + 'fetchWithdrawals': True, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'repayMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'withdraw': True, + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/40811661-b6eceae2-653a-11e8-829e-10bfadb078cf.jpg', + 'api': { + 'rest': 'https://api.coinbase.com', + }, + 'www': 'https://www.coinbase.com', + 'doc': [ + 'https://developers.coinbase.com/api/v2', + 'https://docs.cloud.coinbase.com/advanced-trade/docs/welcome', + ], + 'fees': [ + 'https://support.coinbase.com/customer/portal/articles/2109597-buy-sell-bank-transfer-fees', + 'https://www.coinbase.com/advanced-fees', + ], + 'referral': 'https://www.coinbase.com/join/58cbe25a355148797479dbd2', + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + }, + 'api': { + 'v2': { + 'public': { + 'get': { + 'currencies': 10.6, + 'currencies/crypto': 10.6, + 'time': 10.6, + 'exchange-rates': 10.6, + 'users/{user_id}': 10.6, + 'prices/{symbol}/buy': 10.6, + 'prices/{symbol}/sell': 10.6, + 'prices/{symbol}/spot': 10.6, + }, + }, + 'private': { + 'get': { + 'accounts': 10.6, + 'accounts/{account_id}': 10.6, + 'accounts/{account_id}/addresses': 10.6, + 'accounts/{account_id}/addresses/{address_id}': 10.6, + 'accounts/{account_id}/addresses/{address_id}/transactions': 10.6, + 'accounts/{account_id}/transactions': 10.6, + 'accounts/{account_id}/transactions/{transaction_id}': 10.6, + 'accounts/{account_id}/buys': 10.6, + 'accounts/{account_id}/buys/{buy_id}': 10.6, + 'accounts/{account_id}/sells': 10.6, + 'accounts/{account_id}/sells/{sell_id}': 10.6, + 'accounts/{account_id}/deposits': 10.6, + 'accounts/{account_id}/deposits/{deposit_id}': 10.6, + 'accounts/{account_id}/withdrawals': 10.6, + 'accounts/{account_id}/withdrawals/{withdrawal_id}': 10.6, + 'payment-methods': 10.6, + 'payment-methods/{payment_method_id}': 10.6, + 'user': 10.6, + 'user/auth': 10.6, + }, + 'post': { + 'accounts': 10.6, + 'accounts/{account_id}/primary': 10.6, + 'accounts/{account_id}/addresses': 10.6, + 'accounts/{account_id}/transactions': 10.6, + 'accounts/{account_id}/transactions/{transaction_id}/complete': 10.6, + 'accounts/{account_id}/transactions/{transaction_id}/resend': 10.6, + 'accounts/{account_id}/buys': 10.6, + 'accounts/{account_id}/buys/{buy_id}/commit': 10.6, + 'accounts/{account_id}/sells': 10.6, + 'accounts/{account_id}/sells/{sell_id}/commit': 10.6, + 'accounts/{account_id}/deposits': 10.6, + 'accounts/{account_id}/deposits/{deposit_id}/commit': 10.6, + 'accounts/{account_id}/withdrawals': 10.6, + 'accounts/{account_id}/withdrawals/{withdrawal_id}/commit': 10.6, + }, + 'put': { + 'accounts/{account_id}': 10.6, + 'user': 10.6, + }, + 'delete': { + 'accounts/{id}': 10.6, + 'accounts/{account_id}/transactions/{transaction_id}': 10.6, + }, + }, + }, + 'v3': { + 'public': { + 'get': { + 'brokerage/time': 3, + 'brokerage/market/product_book': 3, + 'brokerage/market/products': 3, + 'brokerage/market/products/{product_id}': 3, + 'brokerage/market/products/{product_id}/candles': 3, + 'brokerage/market/products/{product_id}/ticker': 3, + }, + }, + 'private': { + 'get': { + 'brokerage/accounts': 1, + 'brokerage/accounts/{account_uuid}': 1, + 'brokerage/orders/historical/batch': 1, + 'brokerage/orders/historical/fills': 1, + 'brokerage/orders/historical/{order_id}': 1, + 'brokerage/products': 3, + 'brokerage/products/{product_id}': 3, + 'brokerage/products/{product_id}/candles': 3, + 'brokerage/products/{product_id}/ticker': 3, + 'brokerage/best_bid_ask': 3, + 'brokerage/product_book': 3, + 'brokerage/transaction_summary': 3, + 'brokerage/portfolios': 1, + 'brokerage/portfolios/{portfolio_uuid}': 1, + 'brokerage/convert/trade/{trade_id}': 1, + 'brokerage/cfm/balance_summary': 1, + 'brokerage/cfm/positions': 1, + 'brokerage/cfm/positions/{product_id}': 1, + 'brokerage/cfm/sweeps': 1, + 'brokerage/intx/portfolio/{portfolio_uuid}': 1, + 'brokerage/intx/positions/{portfolio_uuid}': 1, + 'brokerage/intx/positions/{portfolio_uuid}/{symbol}': 1, + 'brokerage/payment_methods': 1, + 'brokerage/payment_methods/{payment_method_id}': 1, + 'brokerage/key_permissions': 1, + }, + 'post': { + 'brokerage/orders': 1, + 'brokerage/orders/batch_cancel': 1, + 'brokerage/orders/edit': 1, + 'brokerage/orders/edit_preview': 1, + 'brokerage/orders/preview': 1, + 'brokerage/portfolios': 1, + 'brokerage/portfolios/move_funds': 1, + 'brokerage/convert/quote': 1, + 'brokerage/convert/trade/{trade_id}': 1, + 'brokerage/cfm/sweeps/schedule': 1, + 'brokerage/intx/allocate': 1, + # futures + 'brokerage/orders/close_position': 1, + }, + 'put': { + 'brokerage/portfolios/{portfolio_uuid}': 1, + }, + 'delete': { + 'brokerage/portfolios/{portfolio_uuid}': 1, + 'brokerage/cfm/sweeps': 1, + }, + }, + }, + }, + 'fees': { + 'trading': { + 'taker': self.parse_number('0.012'), + 'maker': self.parse_number('0.006'), # {"pricing_tier":"Advanced 1","usd_from":"0","usd_to":"1000","taker_fee_rate":"0.012","maker_fee_rate":"0.006","aop_from":"","aop_to":""} + 'tierBased': True, + 'percentage': True, + 'tiers': { + 'taker': [ + [self.parse_number('0'), self.parse_number('0.006')], + [self.parse_number('10000'), self.parse_number('0.004')], + [self.parse_number('50000'), self.parse_number('0.0025')], + [self.parse_number('100000'), self.parse_number('0.002')], + [self.parse_number('1000000'), self.parse_number('0.0018')], + [self.parse_number('15000000'), self.parse_number('0.0016')], + [self.parse_number('75000000'), self.parse_number('0.0012')], + [self.parse_number('250000000'), self.parse_number('0.0008')], + [self.parse_number('400000000'), self.parse_number('0.0005')], + ], + 'maker': [ + [self.parse_number('0'), self.parse_number('0.004')], + [self.parse_number('10000'), self.parse_number('0.0025')], + [self.parse_number('50000'), self.parse_number('0.0015')], + [self.parse_number('100000'), self.parse_number('0.001')], + [self.parse_number('1000000'), self.parse_number('0.0008')], + [self.parse_number('15000000'), self.parse_number('0.0006')], + [self.parse_number('75000000'), self.parse_number('0.0003')], + [self.parse_number('250000000'), self.parse_number('0.0')], + [self.parse_number('400000000'), self.parse_number('0.0')], + ], + }, + }, + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'exact': { + 'two_factor_required': AuthenticationError, # 402 When sending money over 2fa limit + 'param_required': ExchangeError, # 400 Missing parameter + 'validation_error': ExchangeError, # 400 Unable to validate POST/PUT + 'invalid_request': ExchangeError, # 400 Invalid request + 'personal_details_required': AuthenticationError, # 400 User’s personal detail required to complete self request + 'identity_verification_required': AuthenticationError, # 400 Identity verification is required to complete self request + 'jumio_verification_required': AuthenticationError, # 400 Document verification is required to complete self request + 'jumio_face_match_verification_required': AuthenticationError, # 400 Document verification including face match is required to complete self request + 'unverified_email': AuthenticationError, # 400 User has not verified their email + 'authentication_error': AuthenticationError, # 401 Invalid auth(generic) + 'invalid_authentication_method': AuthenticationError, # 401 API access is blocked for deleted users. + 'invalid_token': AuthenticationError, # 401 Invalid Oauth token + 'revoked_token': AuthenticationError, # 401 Revoked Oauth token + 'expired_token': AuthenticationError, # 401 Expired Oauth token + 'invalid_scope': AuthenticationError, # 403 User hasn’t authenticated necessary scope + 'not_found': ExchangeError, # 404 Resource not found + 'rate_limit_exceeded': RateLimitExceeded, # 429 Rate limit exceeded + 'internal_server_error': ExchangeError, # 500 Internal server error + 'UNSUPPORTED_ORDER_CONFIGURATION': BadRequest, + 'INSUFFICIENT_FUND': InsufficientFunds, + 'PERMISSION_DENIED': PermissionDenied, + 'INVALID_ARGUMENT': BadRequest, + 'PREVIEW_STOP_PRICE_ABOVE_LAST_TRADE_PRICE': InvalidOrder, + 'PREVIEW_INSUFFICIENT_FUND': InsufficientFunds, + }, + 'broad': { + 'Insufficient balance in source account': InsufficientFunds, + 'request timestamp expired': InvalidNonce, # {"errors":[{"id":"authentication_error","message":"request timestamp expired"}]} + 'order with self orderID was not found': OrderNotFound, # {"error":"unknown","error_details":"order with self orderID was not found","message":"order with self orderID was not found"} + }, + }, + 'timeframes': { + '1m': 'ONE_MINUTE', + '5m': 'FIVE_MINUTE', + '15m': 'FIFTEEN_MINUTE', + '30m': 'THIRTY_MINUTE', + '1h': 'ONE_HOUR', + '2h': 'TWO_HOUR', + '6h': 'SIX_HOUR', + '1d': 'ONE_DAY', + }, + 'commonCurrencies': { + 'CGLD': 'CELO', + }, + 'options': { + 'usePrivate': False, + 'brokerId': 'ccxt', + 'stablePairs': ['BUSD-USD', 'CBETH-ETH', 'DAI-USD', 'GUSD-USD', 'GYEN-USD', 'PAX-USD', 'PAX-USDT', 'USDC-EUR', 'USDC-GBP', 'USDT-EUR', 'USDT-GBP', 'USDT-USD', 'USDT-USDC', 'WBTC-BTC'], + 'fetchCurrencies': { + 'expires': 5000, + }, + 'accounts': [ + 'wallet', + 'fiat', + # 'vault', + ], + 'v3Accounts': [ + 'ACCOUNT_TYPE_CRYPTO', + 'ACCOUNT_TYPE_FIAT', + ], + 'networks': { + 'ERC20': 'ethereum', + 'XLM': 'stellar', + }, + 'createMarketBuyOrderRequiresPrice': True, + 'advanced': True, # set to True if using any v3 endpoints from the advanced trade API + 'fetchMarkets': 'fetchMarketsV3', # 'fetchMarketsV3' or 'fetchMarketsV2' + 'timeDifference': 0, # the difference between system clock and exchange server clock + 'adjustForTimeDifference': False, # controls the adjustment logic upon instantiation + 'fetchTicker': 'fetchTickerV3', # 'fetchTickerV3' or 'fetchTickerV2' + 'fetchTickers': 'fetchTickersV3', # 'fetchTickersV3' or 'fetchTickersV2' + 'fetchAccounts': 'fetchAccountsV3', # 'fetchAccountsV3' or 'fetchAccountsV2' + 'fetchBalance': 'v2PrivateGetAccounts', # 'v2PrivateGetAccounts' or 'v3PrivateGetBrokerageAccounts' + 'fetchTime': 'v2PublicGetTime', # 'v2PublicGetTime' or 'v3PublicGetBrokerageTime' + 'user_native_currency': 'USD', # needed to get fees for v3 + }, + 'features': { + 'default': { + 'sandbox': False, + 'createOrder': { + 'marginMode': True, + 'triggerPrice': True, + 'triggerPriceType': None, + 'triggerDirection': True, + 'stopLossPrice': True, + 'takeProfitPrice': True, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': True, + }, + 'hedged': False, + 'trailing': False, + 'leverage': True, # todo implement + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': True, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 3000, + 'daysBack': None, + 'untilDays': 10000, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': None, + 'daysBack': None, + 'untilDays': 10000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': None, + 'daysBack': None, + 'daysBackCanceled': None, + 'untilDays': 10000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 300, + }, + }, + 'spot': { + 'extends': 'default', + }, + 'swap': { + 'linear': { + 'extends': 'default', + }, + 'inverse': None, + }, + 'future': { + 'linear': { + 'extends': 'default', + }, + 'inverse': None, + }, + }, + }) + + def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/api-time#http-request + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.method]: 'v2PublicGetTime' or 'v3PublicGetBrokerageTime' default is 'v2PublicGetTime' + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + defaultMethod = self.safe_string(self.options, 'fetchTime', 'v2PublicGetTime') + method = self.safe_string(params, 'method', defaultMethod) + params = self.omit(params, 'method') + response = None + if method == 'v2PublicGetTime': + response = self.v2PublicGetTime(params) + # + # { + # "data": { + # "epoch": 1589295679, + # "iso": "2020-05-12T15:01:19Z" + # } + # } + # + response = self.safe_dict(response, 'data', {}) + else: + response = self.v3PublicGetBrokerageTime(params) + # + # { + # "iso": "2024-02-27T03:37:14Z", + # "epochSeconds": "1709005034", + # "epochMillis": "1709005034333" + # } + # + return self.safe_timestamp_2(response, 'epoch', 'epochSeconds') + + def fetch_accounts(self, params={}) -> List[Account]: + """ + fetch all the accounts associated with a profile + + https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_getaccounts + https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/api-accounts#list-accounts + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict: a dictionary of `account structures ` indexed by the account type + """ + method = self.safe_string(self.options, 'fetchAccounts', 'fetchAccountsV3') + if method == 'fetchAccountsV3': + return self.fetch_accounts_v3(params) + return self.fetch_accounts_v2(params) + + def fetch_accounts_v2(self, params={}) -> List[Account]: + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchAccounts', 'paginate') + if paginate: + return self.fetch_paginated_call_cursor('fetchAccounts', None, None, None, params, 'next_starting_after', 'starting_after', None, 100) + request: dict = { + 'limit': 100, + } + response = self.v2PrivateGetAccounts(self.extend(request, params)) + # + # { + # "pagination": { + # "ending_before": null, + # "starting_after": null, + # "previous_ending_before": null, + # "next_starting_after": null, + # "limit": 244, + # "order": "desc", + # "previous_uri": null, + # "next_uri": null + # }, + # "data": [ + # { + # "id": "XLM", + # "name": "XLM Wallet", + # "primary": False, + # "type": "wallet", + # "currency": { + # "code": "XLM", + # "name": "Stellar Lumens", + # "color": "#000000", + # "sort_index": 127, + # "exponent": 7, + # "type": "crypto", + # "address_regex": "^G[A-Z2-7]{55}$", + # "asset_id": "13b83335-5ede-595b-821e-5bcdfa80560f", + # "destination_tag_name": "XLM Memo ID", + # "destination_tag_regex": "^[-~]{1,28}$" + # }, + # "balance": { + # "amount": "0.0000000", + # "currency": "XLM" + # }, + # "created_at": null, + # "updated_at": null, + # "resource": "account", + # "resource_path": "/v2/accounts/XLM", + # "allow_deposits": True, + # "allow_withdrawals": True + # }, + # ] + # } + # + data = self.safe_list(response, 'data', []) + pagination = self.safe_dict(response, 'pagination', {}) + cursor = self.safe_string(pagination, 'next_starting_after') + accounts = self.safe_list(response, 'data', []) + length = len(accounts) + lastIndex = length - 1 + last = self.safe_dict(accounts, lastIndex) + if (cursor is not None) and (cursor != ''): + last['next_starting_after'] = cursor + accounts[lastIndex] = last + return self.parse_accounts(data, params) + + def fetch_accounts_v3(self, params={}) -> List[Account]: + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchAccounts', 'paginate') + if paginate: + return self.fetch_paginated_call_cursor('fetchAccounts', None, None, None, params, 'cursor', 'cursor', None, 250) + request: dict = { + 'limit': 250, + } + response = self.v3PrivateGetBrokerageAccounts(self.extend(request, params)) + # + # { + # "accounts": [ + # { + # "uuid": "11111111-1111-1111-1111-111111111111", + # "name": "USDC Wallet", + # "currency": "USDC", + # "available_balance": { + # "value": "0.0000000000000000", + # "currency": "USDC" + # }, + # "default": True, + # "active": True, + # "created_at": "2023-01-04T06:20:06.456Z", + # "updated_at": "2023-01-04T06:20:07.181Z", + # "deleted_at": null, + # "type": "ACCOUNT_TYPE_CRYPTO", + # "ready": False, + # "hold": { + # "value": "0.0000000000000000", + # "currency": "USDC" + # } + # }, + # ... + # ], + # "has_next": False, + # "cursor": "", + # "size": 9 + # } + # + accounts = self.safe_list(response, 'accounts', []) + length = len(accounts) + lastIndex = length - 1 + last = self.safe_dict(accounts, lastIndex) + cursor = self.safe_string(response, 'cursor') + if (cursor is not None) and (cursor != ''): + last['cursor'] = cursor + accounts[lastIndex] = last + return self.parse_accounts(accounts, params) + + def fetch_portfolios(self, params={}) -> List[Account]: + """ + fetch all the portfolios + + https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_getportfolios + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `account structures ` indexed by the account type + """ + response = self.v3PrivateGetBrokeragePortfolios(params) + portfolios = self.safe_list(response, 'portfolios', []) + result = [] + for i in range(0, len(portfolios)): + portfolio = portfolios[i] + result.append({ + 'id': self.safe_string(portfolio, 'uuid'), + 'type': self.safe_string(portfolio, 'type'), + 'code': None, + 'info': portfolio, + }) + return result + + def parse_account(self, account): + # + # fetchAccountsV2 + # + # { + # "id": "XLM", + # "name": "XLM Wallet", + # "primary": False, + # "type": "wallet", + # "currency": { + # "code": "XLM", + # "name": "Stellar Lumens", + # "color": "#000000", + # "sort_index": 127, + # "exponent": 7, + # "type": "crypto", + # "address_regex": "^G[A-Z2-7]{55}$", + # "asset_id": "13b83335-5ede-595b-821e-5bcdfa80560f", + # "destination_tag_name": "XLM Memo ID", + # "destination_tag_regex": "^[-~]{1,28}$" + # }, + # "balance": { + # "amount": "0.0000000", + # "currency": "XLM" + # }, + # "created_at": null, + # "updated_at": null, + # "resource": "account", + # "resource_path": "/v2/accounts/XLM", + # "allow_deposits": True, + # "allow_withdrawals": True + # } + # + # fetchAccountsV3 + # + # { + # "uuid": "11111111-1111-1111-1111-111111111111", + # "name": "USDC Wallet", + # "currency": "USDC", + # "available_balance": { + # "value": "0.0000000000000000", + # "currency": "USDC" + # }, + # "default": True, + # "active": True, + # "created_at": "2023-01-04T06:20:06.456Z", + # "updated_at": "2023-01-04T06:20:07.181Z", + # "deleted_at": null, + # "type": "ACCOUNT_TYPE_CRYPTO", + # "ready": False, + # "hold": { + # "value": "0.0000000000000000", + # "currency": "USDC" + # } + # } + # + active = self.safe_bool(account, 'active') + currencyIdV3 = self.safe_string(account, 'currency') + currency = self.safe_dict(account, 'currency', {}) + currencyId = self.safe_string(currency, 'code', currencyIdV3) + typeV3 = self.safe_string(account, 'name') + typeV2 = self.safe_string(account, 'type') + parts = typeV3.split(' ') + return { + 'id': self.safe_string_2(account, 'id', 'uuid'), + 'type': self.safe_string_lower(parts, 1) if (active is not None) else typeV2, + 'code': self.safe_currency_code(currencyId), + 'info': account, + } + + def create_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + create a currency deposit address + + https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/api-addresses#create-address + + :param str code: unified currency code of the currency for the deposit address + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + accountId = self.safe_string(params, 'account_id') + params = self.omit(params, 'account_id') + if accountId is None: + self.load_accounts() + for i in range(0, len(self.accounts)): + account = self.accounts[i] + if account['code'] == code and account['type'] == 'wallet': + accountId = account['id'] + break + if accountId is None: + raise ExchangeError(self.id + ' createDepositAddress() could not find the account with matching currency code ' + code + ', specify an `account_id` extra param to target specific wallet') + request: dict = { + 'account_id': accountId, + } + response = self.v2PrivatePostAccountsAccountIdAddresses(self.extend(request, params)) + # + # { + # "data": { + # "id": "05b1ebbf-9438-5dd4-b297-2ddedc98d0e4", + # "address": "coinbasebase", + # "address_info": { + # "address": "coinbasebase", + # "destination_tag": "287594668" + # }, + # "name": null, + # "created_at": "2019-07-01T14:39:29Z", + # "updated_at": "2019-07-01T14:39:29Z", + # "network": "eosio", + # "uri_scheme": "eosio", + # "resource": "address", + # "resource_path": "/v2/accounts/14cfc769-e852-52f3-b831-711c104d194c/addresses/05b1ebbf-9438-5dd4-b297-2ddedc98d0e4", + # "warnings": [ + # { + # "title": "Only send EOS(EOS) to self address", + # "details": "Sending any other cryptocurrency will result in permanent loss.", + # "image_url": "https://dynamic-assets.coinbase.com/deaca3d47b10ed4a91a872e9618706eec34081127762d88f2476ac8e99ada4b48525a9565cf2206d18c04053f278f693434af4d4629ca084a9d01b7a286a7e26/asset_icons/1f8489bb280fb0a0fd643c1161312ba49655040e9aaaced5f9ad3eeaf868eadc.png" + # }, + # { + # "title": "Both an address and EOS memo are required to receive EOS", + # "details": "If you send funds without an EOS memo or with an incorrect EOS memo, your funds cannot be credited to your account.", + # "image_url": "https://www.coinbase.com/assets/receive-warning-2f3269d83547a7748fb39d6e0c1c393aee26669bfea6b9f12718094a1abff155.png" + # } + # ], + # "warning_title": "Only send EOS(EOS) to self address", + # "warning_details": "Sending any other cryptocurrency will result in permanent loss.", + # "destination_tag": "287594668", + # "deposit_uri": "eosio:coinbasebase?dt=287594668", + # "callback_url": null + # } + # } + # + data = self.safe_dict(response, 'data', {}) + tag = self.safe_string(data, 'destination_tag') + address = self.safe_string(data, 'address') + return { + 'currency': code, + 'tag': tag, + 'address': address, + 'network': None, + 'info': response, + } + + def fetch_my_sells(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + @ignore + fetch sells + + https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/api-sells#list-sells + + :param str symbol: not used by coinbase fetchMySells() + :param int [since]: timestamp in ms of the earliest sell, default is None + :param int [limit]: max number of sells to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `list of order structures ` + """ + # v2 did't have an endpoint for all historical trades + request = self.prepare_account_request(limit, params) + self.load_markets() + query = self.omit(params, ['account_id', 'accountId']) + sells = self.v2PrivateGetAccountsAccountIdSells(self.extend(request, query)) + return self.parse_trades(sells['data'], None, since, limit) + + def fetch_my_buys(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + @ignore + fetch buys + + https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/api-buys#list-buys + + :param str symbol: not used by coinbase fetchMyBuys() + :param int [since]: timestamp in ms of the earliest buy, default is None + :param int [limit]: max number of buys to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `order structures ` + """ + # v2 did't have an endpoint for all historical trades + request = self.prepare_account_request(limit, params) + self.load_markets() + query = self.omit(params, ['account_id', 'accountId']) + buys = self.v2PrivateGetAccountsAccountIdBuys(self.extend(request, query)) + return self.parse_trades(buys['data'], None, since, limit) + + def fetch_transactions_with_method(self, method, code: Str = None, since: Int = None, limit: Int = None, params={}): + request = None + request, params = self.prepare_account_request_with_currency_code(code, limit, params) + self.load_markets() + response = getattr(self, method)(self.extend(request, params)) + return self.parse_transactions(response['data'], None, since, limit) + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + Fetch all withdrawals made from an account. Won't return crypto withdrawals. Use fetchLedger for those. + + https://docs.cdp.coinbase.com/coinbase-app/docs/api-withdrawals#list-withdrawals + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.currencyType]: "fiat" or "crypto" + :returns dict[]: a list of `transaction structures ` + """ + currencyType = None + currencyType, params = self.handle_option_and_params(params, 'fetchWithdrawals', 'currencyType') + if currencyType == 'crypto': + results = self.fetch_transactions_with_method('v2PrivateGetAccountsAccountIdTransactions', code, since, limit, params) + return self.filter_by_array(results, 'type', 'withdrawal', False) + return self.fetch_transactions_with_method('v2PrivateGetAccountsAccountIdWithdrawals', code, since, limit, params) + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + Fetch all fiat deposits made to an account. Won't return crypto deposits or staking rewards. Use fetchLedger for those. + + https://docs.cdp.coinbase.com/coinbase-app/docs/api-deposits#list-deposits + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.currencyType]: "fiat" or "crypto" + :returns dict[]: a list of `transaction structures ` + """ + currencyType = None + currencyType, params = self.handle_option_and_params(params, 'fetchWithdrawals', 'currencyType') + if currencyType == 'crypto': + results = self.fetch_transactions_with_method('v2PrivateGetAccountsAccountIdTransactions', code, since, limit, params) + return self.filter_by_array(results, 'type', 'deposit', False) + return self.fetch_transactions_with_method('v2PrivateGetAccountsAccountIdDeposits', code, since, limit, params) + + def fetch_deposits_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch history of deposits and withdrawals + + https://docs.cdp.coinbase.com/coinbase-app/docs/api-transactions + + :param str [code]: unified currency code for the currency of the deposit/withdrawals, default is None + :param int [since]: timestamp in ms of the earliest deposit/withdrawal, default is None + :param int [limit]: max number of deposit/withdrawals to return, default = 50, Min: 1, Max: 100 + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `transaction structure ` + """ + self.load_markets() + results = self.fetch_transactions_with_method('v2PrivateGetAccountsAccountIdTransactions', code, since, limit, params) + return self.filter_by_array(results, 'type', ['deposit', 'withdrawal'], False) + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'created': 'pending', + 'completed': 'ok', + 'canceled': 'canceled', + } + return self.safe_string(statuses, status, status) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fiat deposit + # + # { + # "id": "f34c19f3-b730-5e3d-9f72", + # "status": "completed", + # "payment_method": { + # "id": "a022b31d-f9c7-5043-98f2", + # "resource": "payment_method", + # "resource_path": "/v2/payment-methods/a022b31d-f9c7-5043-98f2" + # }, + # "transaction": { + # "id": "04ed4113-3732-5b0c-af86-b1d2146977d0", + # "resource": "transaction", + # "resource_path": "/v2/accounts/91cd2d36-3a91-55b6-a5d4-0124cf105483/transactions/04ed4113-3732-5b0c-af86" + # }, + # "user_reference": "2VTYTH", + # "created_at": "2017-02-09T07:01:18Z", + # "updated_at": "2017-02-09T07:01:26Z", + # "resource": "deposit", + # "resource_path": "/v2/accounts/91cd2d36-3a91-55b6-a5d4-0124cf105483/deposits/f34c19f3-b730-5e3d-9f72", + # "committed": True, + # "payout_at": "2017-02-12T07:01:17Z", + # "instant": False, + # "fee": {"amount": "0.00", "currency": "EUR"}, + # "amount": {"amount": "114.02", "currency": "EUR"}, + # "subtotal": {"amount": "114.02", "currency": "EUR"}, + # "hold_until": null, + # "hold_days": 0, + # "hold_business_days": 0, + # "next_step": null + # } + # + # fiat_withdrawal + # + # { + # "id": "cfcc3b4a-eeb6-5e8c-8058", + # "status": "completed", + # "payment_method": { + # "id": "8b94cfa4-f7fd-5a12-a76a", + # "resource": "payment_method", + # "resource_path": "/v2/payment-methods/8b94cfa4-f7fd-5a12-a76a" + # }, + # "transaction": { + # "id": "fcc2550b-5104-5f83-a444", + # "resource": "transaction", + # "resource_path": "/v2/accounts/91cd2d36-3a91-55b6-a5d4-0124cf105483/transactions/fcc2550b-5104-5f83-a444" + # }, + # "user_reference": "MEUGK", + # "created_at": "2018-07-26T08:55:12Z", + # "updated_at": "2018-07-26T08:58:18Z", + # "resource": "withdrawal", + # "resource_path": "/v2/accounts/91cd2d36-3a91-55b6-a5d4-0124cf105483/withdrawals/cfcc3b4a-eeb6-5e8c-8058", + # "committed": True, + # "payout_at": "2018-07-31T08:55:12Z", + # "instant": False, + # "fee": {"amount": "0.15", "currency": "EUR"}, + # "amount": {"amount": "13130.69", "currency": "EUR"}, + # "subtotal": {"amount": "13130.84", "currency": "EUR"}, + # "idem": "e549dee5-63ed-4e79-8a96", + # "next_step": null + # } + # + # withdraw + # + # { + # "id": "a1794ecf-5693-55fa-70cf-ef731748ed82", + # "type": "send", + # "status": "pending", + # "amount": { + # "amount": "-14.008308", + # "currency": "USDC" + # }, + # "native_amount": { + # "amount": "-18.74", + # "currency": "CAD" + # }, + # "description": null, + # "created_at": "2024-01-12T01:27:31Z", + # "updated_at": "2024-01-12T01:27:31Z", + # "resource": "transaction", + # "resource_path": "/v2/accounts/a34bgfad-ed67-538b-bffc-730c98c10da0/transactions/a1794ecf-5693-55fa-70cf-ef731748ed82", + # "instant_exchange": False, + # "network": { + # "status": "pending", + # "status_description": "Pending(est. less than 10 minutes)", + # "transaction_fee": { + # "amount": "4.008308", + # "currency": "USDC" + # }, + # "transaction_amount": { + # "amount": "10.000000", + # "currency": "USDC" + # }, + # "confirmations": 0 + # }, + # "to": { + # "resource": "ethereum_address", + # "address": "0x9...", + # "currency": "USDC", + # "address_info": { + # "address": "0x9..." + # } + # }, + # "idem": "748d8591-dg9a-7831-a45b-crd61dg78762", + # "details": { + # "title": "Sent USDC", + # "subtitle": "To USDC address on Ethereum network", + # "header": "Sent 14.008308 USDC($18.74)", + # "health": "warning" + # }, + # "hide_native_amount": False + # } + # + # + # crypto deposit & withdrawal(using `/transactions` endpoint) + # { + # "amount": { + # "amount": "0.00014200",(negative for withdrawal) + # "currency": "BTC" + # }, + # "created_at": "2024-03-29T15:48:30Z", + # "id": "0031a605-241d-514d-a97b-d4b99f3225d3", + # "idem": "092a979b-017e-4403-940a-2ca57811f442", # field present only in case of withdrawal + # "native_amount": { + # "amount": "9.85",(negative for withdrawal) + # "currency": "USD" + # }, + # "network": { + # "status": "pending", # if status is `off_blockchain` then no more other fields are hasattr(self, present) object + # "hash": "5jYuvrNsvX2DZoMnzGYzVpYxJLfYu4GSK3xetG1H5LHrSovsuFCFYdFMwNRoiht3s6fBk92MM8QLLnz65xuEFTrE", + # "network_name": "solana", + # "transaction_fee": { + # "amount": "0.000100000", + # "currency": "SOL" + # } + # }, + # "resource": "transaction", + # "resource_path": "/v2/accounts/dc504b1c-248e-5b68-a3b0-b991f7fa84e6/transactions/0031a605-241d-514d-a97b-d4b99f3225d3", + # "status": "completed", + # "type": "send", + # "from": { # in some cases, field might be present for deposit + # "id": "7fd10cd7-b091-5cee-ba41-c29e49a7cccf", + # "name": "Coinbase", + # "resource": "user" + # }, + # "to": { # field only present for withdrawal + # "address": "5HA12BNthAvBwNYARYf9y5MqqCpB4qhCNFCs1Qw48ACE", + # "resource": "address" + # }, + # "description": "C3 - One Time BTC Credit . Reference Case # 123.", # in some cases, field might be present for deposit + # } + # + transactionType = self.safe_string(transaction, 'type') + amountAndCurrencyObject = None + feeObject = None + network = self.safe_dict(transaction, 'network', {}) + if transactionType == 'send': + amountAndCurrencyObject = self.safe_dict(network, 'transaction_amount') + feeObject = self.safe_dict(network, 'transaction_fee', {}) + else: + amountAndCurrencyObject = self.safe_dict(transaction, 'subtotal') + feeObject = self.safe_dict(transaction, 'fee', {}) + if amountAndCurrencyObject is None: + amountAndCurrencyObject = self.safe_dict(transaction, 'amount') + amountString = self.safe_string(amountAndCurrencyObject, 'amount') + amountStringAbs = Precise.string_abs(amountString) + status = self.parse_transaction_status(self.safe_string(transaction, 'status')) + if status is None: + committed = self.safe_bool(transaction, 'committed') + status = 'ok' if committed else 'pending' + id = self.safe_string(transaction, 'id') + currencyId = self.safe_string(amountAndCurrencyObject, 'currency') + feeCurrencyId = self.safe_string(feeObject, 'currency') + datetime = self.safe_string(transaction, 'created_at') + resource = self.safe_string(transaction, 'resource') + type = resource + if not self.in_array(type, ['deposit', 'withdrawal']): + if Precise.string_gt(amountString, '0'): + type = 'deposit' + elif Precise.string_lt(amountString, '0'): + type = 'withdrawal' + toObject = self.safe_dict(transaction, 'to') + addressTo = self.safe_string(toObject, 'address') + networkId = self.safe_string(network, 'network_name') + return { + 'info': transaction, + 'id': id, + 'txid': self.safe_string(network, 'hash', id), + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + 'network': self.network_id_to_code(networkId), + 'address': addressTo, + 'addressTo': addressTo, + 'addressFrom': None, + 'tag': None, + 'tagTo': None, + 'tagFrom': None, + 'type': type, + 'amount': self.parse_number(amountStringAbs), + 'currency': self.safe_currency_code(currencyId, currency), + 'status': status, + 'updated': self.parse8601(self.safe_string(transaction, 'updated_at')), + 'fee': { + 'cost': self.safe_number(feeObject, 'amount'), + 'currency': self.safe_currency_code(feeCurrencyId), + }, + } + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchMyBuys, fetchMySells + # + # { + # "id": "67e0eaec-07d7-54c4-a72c-2e92826897df", + # "status": "completed", + # "payment_method": { + # "id": "83562370-3e5c-51db-87da-752af5ab9559", + # "resource": "payment_method", + # "resource_path": "/v2/payment-methods/83562370-3e5c-51db-87da-752af5ab9559" + # }, + # "transaction": { + # "id": "441b9494-b3f0-5b98-b9b0-4d82c21c252a", + # "resource": "transaction", + # "resource_path": "/v2/accounts/2bbf394c-193b-5b2a-9155-3b4732659ede/transactions/441b9494-b3f0-5b98-b9b0-4d82c21c252a" + # }, + # "amount": {"amount": "1.00000000", "currency": "BTC"}, + # "total": {"amount": "10.25", "currency": "USD"}, + # "subtotal": {"amount": "10.10", "currency": "USD"}, + # "created_at": "2015-01-31T20:49:02Z", + # "updated_at": "2015-02-11T16:54:02-08:00", + # "resource": "buy", + # "resource_path": "/v2/accounts/2bbf394c-193b-5b2a-9155-3b4732659ede/buys/67e0eaec-07d7-54c4-a72c-2e92826897df", + # "committed": True, + # "instant": False, + # "fee": {"amount": "0.15", "currency": "USD"}, + # "payout_at": "2015-02-18T16:54:00-08:00" + # } + # + # fetchTrades + # + # { + # "trade_id": "10092327", + # "product_id": "BTC-USDT", + # "price": "17488.12", + # "size": "0.0000623", + # "time": "2023-01-11T00:52:37.557001Z", + # "side": "BUY", + # "bid": "", + # "ask": "" + # } + # + # fetchMyTrades + # + # { + # "entry_id": "b88b82cc89e326a2778874795102cbafd08dd979a2a7a3c69603fc4c23c2e010", + # "trade_id": "cdc39e45-bbd3-44ec-bf02-61742dfb16a1", + # "order_id": "813a53c5-3e39-47bb-863d-2faf685d22d8", + # "trade_time": "2023-01-18T01:37:38.091377090Z", + # "trade_type": "FILL", + # "price": "21220.64", + # "size": "0.0046830664333996", + # "commission": "0.0000280983986004", + # "product_id": "BTC-USDT", + # "sequence_timestamp": "2023-01-18T01:37:38.092520Z", + # "liquidity_indicator": "UNKNOWN_LIQUIDITY_INDICATOR", + # "size_in_quote": True, + # "user_id": "1111111-1111-1111-1111-111111111111", + # "side": "BUY" + # } + # + symbol = None + totalObject = self.safe_dict(trade, 'total', {}) + amountObject = self.safe_dict(trade, 'amount', {}) + subtotalObject = self.safe_dict(trade, 'subtotal', {}) + feeObject = self.safe_dict(trade, 'fee', {}) + marketId = self.safe_string(trade, 'product_id') + market = self.safe_market(marketId, market, '-') + if market is not None: + symbol = market['symbol'] + else: + baseId = self.safe_string(amountObject, 'currency') + quoteId = self.safe_string(totalObject, 'currency') + if (baseId is not None) and (quoteId is not None): + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + symbol = base + '/' + quote + sizeInQuote = self.safe_bool(trade, 'size_in_quote') + v3Price = self.safe_string(trade, 'price') + v3Cost = None + v3Amount = self.safe_string(trade, 'size') + if sizeInQuote: + # calculate base size + v3Cost = v3Amount + v3Amount = Precise.string_div(v3Amount, v3Price) + v3FeeCost = self.safe_string(trade, 'commission') + amountString = self.safe_string(amountObject, 'amount', v3Amount) + costString = self.safe_string(subtotalObject, 'amount', v3Cost) + priceString = None + cost = None + if (costString is not None) and (amountString is not None): + priceString = Precise.string_div(costString, amountString) + else: + priceString = v3Price + if (priceString is not None) and (amountString is not None): + cost = Precise.string_mul(priceString, amountString) + else: + cost = costString + feeCurrencyId = self.safe_string(feeObject, 'currency') + feeCost = self.safe_number(feeObject, 'amount', self.parse_number(v3FeeCost)) + if (feeCurrencyId is None) and (market is not None) and (feeCost is not None): + feeCurrencyId = market['quote'] + datetime = self.safe_string_n(trade, ['created_at', 'trade_time', 'time']) + side = self.safe_string_lower_2(trade, 'resource', 'side') + takerOrMaker = self.safe_string_lower(trade, 'liquidity_indicator') + return self.safe_trade({ + 'info': trade, + 'id': self.safe_string_2(trade, 'id', 'trade_id'), + 'order': self.safe_string(trade, 'order_id'), + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + 'symbol': symbol, + 'type': None, + 'side': None if (side == 'unknown_order_side') else side, + 'takerOrMaker': None if (takerOrMaker == 'unknown_liquidity_indicator') else takerOrMaker, + 'price': priceString, + 'amount': amountString, + 'cost': cost, + 'fee': { + 'cost': feeCost, + 'currency': self.safe_currency_code(feeCurrencyId), + }, + }) + + def fetch_markets(self, params={}) -> List[Market]: + """ + + https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_getpublicproducts + https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/api-currencies#get-fiat-currencies + https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/api-exchange-rates#get-exchange-rates + + retrieves data on all markets for coinbase + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.usePrivate]: use private endpoint for fetching markets + :returns dict[]: an array of objects representing market data + """ + if self.options['adjustForTimeDifference']: + self.load_time_difference() + method = self.safe_string(self.options, 'fetchMarkets', 'fetchMarketsV3') + if method == 'fetchMarketsV3': + return self.fetch_markets_v3(params) + return self.fetch_markets_v2(params) + + def fetch_markets_v2(self, params={}) -> List[Market]: + response = self.fetch_currencies_from_cache(params) + currencies = self.safe_dict(response, 'currencies', {}) + exchangeRates = self.safe_dict(response, 'exchangeRates', {}) + data = self.safe_list(currencies, 'data', []) + dataById = self.index_by(data, 'id') + rates = self.safe_dict(self.safe_dict(exchangeRates, 'data', {}), 'rates', {}) + baseIds = list(rates.keys()) + result = [] + for i in range(0, len(baseIds)): + baseId = baseIds[i] + base = self.safe_currency_code(baseId) + type = 'fiat' if (baseId in dataById) else 'crypto' + # https://github.com/ccxt/ccxt/issues/6066 + if type == 'crypto': + for j in range(0, len(data)): + quoteCurrency = data[j] + quoteId = self.safe_string(quoteCurrency, 'id') + quote = self.safe_currency_code(quoteId) + result.append(self.safe_market_structure({ + 'id': baseId + '-' + quoteId, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': None, + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': None, + 'price': None, + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': None, + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': self.safe_number(quoteCurrency, 'min_size'), + 'max': None, + }, + }, + 'info': quoteCurrency, + })) + return result + + def fetch_markets_v3(self, params={}) -> List[Market]: + usePrivate = False + usePrivate, params = self.handle_option_and_params(params, 'fetchMarkets', 'usePrivate', False) + spotUnresolvedPromises = [] + if usePrivate: + spotUnresolvedPromises.append(self.v3PrivateGetBrokerageProducts(params)) + else: + spotUnresolvedPromises.append(self.v3PublicGetBrokerageMarketProducts(params)) + # + # { + # products: [ + # { + # product_id: 'BTC-USD', + # price: '67060', + # price_percentage_change_24h: '3.30054960636883', + # volume_24h: '10967.87426597', + # volume_percentage_change_24h: '141.73048325503036', + # base_increment: '0.00000001', + # quote_increment: '0.01', + # quote_min_size: '1', + # quote_max_size: '150000000', + # base_min_size: '0.00000001', + # base_max_size: '3400', + # base_name: 'Bitcoin', + # quote_name: 'US Dollar', + # watched: False, + # is_disabled: False, + # new: False, + # status: 'online', + # cancel_only: False, + # limit_only: False, + # post_only: False, + # trading_disabled: False, + # auction_mode: False, + # product_type: 'SPOT', + # quote_currency_id: 'USD', + # base_currency_id: 'BTC', + # fcm_trading_session_details: null, + # mid_market_price: '', + # alias: '', + # alias_to: ['BTC-USDC'], + # base_display_symbol: 'BTC', + # quote_display_symbol: 'USD', + # view_only: False, + # price_increment: '0.01', + # display_name: 'BTC-USD', + # product_venue: 'CBE' + # }, + # ... + # ], + # num_products: '646' + # } + # + if self.check_required_credentials(False): + spotUnresolvedPromises.append(self.v3PrivateGetBrokerageTransactionSummary(params)) + # + # { + # total_volume: '9.995989116664404', + # total_fees: '0.07996791093331522', + # fee_tier: { + # pricing_tier: 'Advanced 1', + # usd_from: '0', + # usd_to: '1000', + # taker_fee_rate: '0.008', + # maker_fee_rate: '0.006', + # aop_from: '', + # aop_to: '' + # }, + # margin_rate: null, + # goods_and_services_tax: null, + # advanced_trade_only_volume: '9.995989116664404', + # advanced_trade_only_fees: '0.07996791093331522', + # coinbase_pro_volume: '0', + # coinbase_pro_fees: '0', + # total_balance: '', + # has_promo_fee: False + # } + # + unresolvedContractPromises = [] + try: + unresolvedContractPromises = [ + self.v3PublicGetBrokerageMarketProducts(self.extend(params, {'product_type': 'FUTURE'})), + self.v3PublicGetBrokerageMarketProducts(self.extend(params, {'product_type': 'FUTURE', 'contract_expiry_type': 'PERPETUAL'})), + ] + except Exception as e: + unresolvedContractPromises = [] # the sync version of ccxt won't have the promise.all line so the request is made here. Some users can't access perpetual products + promises = spotUnresolvedPromises + contractPromises = None + try: + contractPromises = unresolvedContractPromises # some users don't have access to contracts + except Exception as e: + contractPromises = [] + spot = self.safe_dict(promises, 0, {}) + fees = self.safe_dict(promises, 1, {}) + expiringFutures = self.safe_dict(contractPromises, 0, {}) + perpetualFutures = self.safe_dict(contractPromises, 1, {}) + expiringFees = self.safe_dict(contractPromises, 0, {}) + perpetualFees = self.safe_dict(contractPromises, 1, {}) + # + # { + # "total_volume": 0, + # "total_fees": 0, + # "fee_tier": { + # "pricing_tier": "", + # "usd_from": "0", + # "usd_to": "10000", + # "taker_fee_rate": "0.006", + # "maker_fee_rate": "0.004" + # }, + # "margin_rate": null, + # "goods_and_services_tax": null, + # "advanced_trade_only_volume": 0, + # "advanced_trade_only_fees": 0, + # "coinbase_pro_volume": 0, + # "coinbase_pro_fees": 0 + # } + # + feeTier = self.safe_dict(fees, 'fee_tier', {}) + expiringFeeTier = self.safe_dict(expiringFees, 'fee_tier', {}) # fee tier null? + perpetualFeeTier = self.safe_dict(perpetualFees, 'fee_tier', {}) # fee tier null? + data = self.safe_list(spot, 'products', []) + result = [] + for i in range(0, len(data)): + result.append(self.parse_spot_market(data[i], feeTier)) + futureData = self.safe_list(expiringFutures, 'products', []) + for i in range(0, len(futureData)): + result.append(self.parse_contract_market(futureData[i], expiringFeeTier)) + perpetualData = self.safe_list(perpetualFutures, 'products', []) + for i in range(0, len(perpetualData)): + result.append(self.parse_contract_market(perpetualData[i], perpetualFeeTier)) + newMarkets = [] + for i in range(0, len(result)): + market = result[i] + info = self.safe_value(market, 'info', {}) + realMarketIds = self.safe_list(info, 'alias_to', []) + length = len(realMarketIds) + if length > 0: + market['alias'] = realMarketIds[0] + else: + market['alias'] = None + newMarkets.append(market) + return newMarkets + + def parse_spot_market(self, market, feeTier) -> MarketInterface: + # + # { + # "product_id": "TONE-USD", + # "price": "0.01523", + # "price_percentage_change_24h": "1.94109772423025", + # "volume_24h": "19773129", + # "volume_percentage_change_24h": "437.0170530929949", + # "base_increment": "1", + # "quote_increment": "0.00001", + # "quote_min_size": "1", + # "quote_max_size": "10000000", + # "base_min_size": "26.7187147229469674", + # "base_max_size": "267187147.2294696735908216", + # "base_name": "TE-FOOD", + # "quote_name": "US Dollar", + # "watched": False, + # "is_disabled": False, + # "new": False, + # "status": "online", + # "cancel_only": False, + # "limit_only": False, + # "post_only": False, + # "trading_disabled": False, + # "auction_mode": False, + # "product_type": "SPOT", + # "quote_currency_id": "USD", + # "base_currency_id": "TONE", + # "fcm_trading_session_details": null, + # "mid_market_price": "" + # } + # + id = self.safe_string(market, 'product_id') + baseId = self.safe_string(market, 'base_currency_id') + quoteId = self.safe_string(market, 'quote_currency_id') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + marketType = self.safe_string_lower(market, 'product_type') + tradingDisabled = self.safe_bool(market, 'trading_disabled') + stablePairs = self.safe_list(self.options, 'stablePairs', []) + defaultTakerFee = self.safe_number(self.fees['trading'], 'taker') + defaultMakerFee = self.safe_number(self.fees['trading'], 'maker') + takerFee = 0.00001 if self.in_array(id, stablePairs) else self.safe_number(feeTier, 'taker_fee_rate', defaultTakerFee) + makerFee = 0.0 if self.in_array(id, stablePairs) else self.safe_number(feeTier, 'maker_fee_rate', defaultMakerFee) + return self.safe_market_structure({ + 'id': id, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': marketType, + 'spot': (marketType == 'spot'), + 'margin': None, + 'swap': False, + 'future': False, + 'option': False, + 'active': not tradingDisabled, + 'contract': False, + 'linear': None, + 'inverse': None, + 'taker': takerFee, + 'maker': makerFee, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.safe_number(market, 'base_increment'), + 'price': self.safe_number_2(market, 'price_increment', 'quote_increment'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(market, 'base_min_size'), + 'max': self.safe_number(market, 'base_max_size'), + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': self.safe_number(market, 'quote_min_size'), + 'max': self.safe_number(market, 'quote_max_size'), + }, + }, + 'created': None, + 'info': market, + }) + + def parse_contract_market(self, market, feeTier) -> MarketInterface: + # expiring + # + # { + # "product_id":"BIT-26APR24-CDE", + # "price":"71145", + # "price_percentage_change_24h":"-2.36722931247427", + # "volume_24h":"108549", + # "volume_percentage_change_24h":"155.78255337197794", + # "base_increment":"1", + # "quote_increment":"0.01", + # "quote_min_size":"0", + # "quote_max_size":"100000000", + # "base_min_size":"1", + # "base_max_size":"100000000", + # "base_name":"", + # "quote_name":"US Dollar", + # "watched":false, + # "is_disabled":false, + # "new":false, + # "status":"", + # "cancel_only":false, + # "limit_only":false, + # "post_only":false, + # "trading_disabled":false, + # "auction_mode":false, + # "product_type":"FUTURE", + # "quote_currency_id":"USD", + # "base_currency_id":"", + # "fcm_trading_session_details":{ + # "is_session_open":true, + # "open_time":"2024-04-08T22:00:00Z", + # "close_time":"2024-04-09T21:00:00Z" + # }, + # "mid_market_price":"71105", + # "alias":"", + # "alias_to":[ + # ], + # "base_display_symbol":"", + # "quote_display_symbol":"USD", + # "view_only":false, + # "price_increment":"5", + # "display_name":"BTC 26 APR 24", + # "product_venue":"FCM", + # "future_product_details":{ + # "venue":"cde", + # "contract_code":"BIT", + # "contract_expiry":"2024-04-26T15:00:00Z", + # "contract_size":"0.01", + # "contract_root_unit":"BTC", + # "group_description":"Nano Bitcoin Futures", + # "contract_expiry_timezone":"Europe/London", + # "group_short_description":"Nano BTC", + # "risk_managed_by":"MANAGED_BY_FCM", + # "contract_expiry_type":"EXPIRING", + # "contract_display_name":"BTC 26 APR 24" + # } + # } + # + # perpetual + # + # { + # "product_id":"ETH-PERP-INTX", + # "price":"3630.98", + # "price_percentage_change_24h":"0.65142426292038", + # "volume_24h":"114020.1501", + # "volume_percentage_change_24h":"63.33650787154869", + # "base_increment":"0.0001", + # "quote_increment":"0.01", + # "quote_min_size":"10", + # "quote_max_size":"50000000", + # "base_min_size":"0.0001", + # "base_max_size":"50000", + # "base_name":"", + # "quote_name":"USDC", + # "watched":false, + # "is_disabled":false, + # "new":false, + # "status":"", + # "cancel_only":false, + # "limit_only":false, + # "post_only":false, + # "trading_disabled":false, + # "auction_mode":false, + # "product_type":"FUTURE", + # "quote_currency_id":"USDC", + # "base_currency_id":"", + # "fcm_trading_session_details":null, + # "mid_market_price":"3630.975", + # "alias":"", + # "alias_to":[], + # "base_display_symbol":"", + # "quote_display_symbol":"USDC", + # "view_only":false, + # "price_increment":"0.01", + # "display_name":"ETH PERP", + # "product_venue":"INTX", + # "future_product_details":{ + # "venue":"", + # "contract_code":"ETH", + # "contract_expiry":null, + # "contract_size":"1", + # "contract_root_unit":"ETH", + # "group_description":"", + # "contract_expiry_timezone":"", + # "group_short_description":"", + # "risk_managed_by":"MANAGED_BY_VENUE", + # "contract_expiry_type":"PERPETUAL", + # "perpetual_details":{ + # "open_interest":"0", + # "funding_rate":"0.000016", + # "funding_time":"2024-04-09T09:00:00.000008Z", + # "max_leverage":"10" + # }, + # "contract_display_name":"ETH PERPETUAL" + # } + # } + # + id = self.safe_string(market, 'product_id') + futureProductDetails = self.safe_dict(market, 'future_product_details', {}) + contractExpiryType = self.safe_string(futureProductDetails, 'contract_expiry_type') + contractSize = self.safe_number(futureProductDetails, 'contract_size') + contractExpire = self.safe_string(futureProductDetails, 'contract_expiry') + expireTimestamp = self.parse8601(contractExpire) + expireDateTime = self.iso8601(expireTimestamp) + isSwap = (contractExpiryType == 'PERPETUAL') + baseId = self.safe_string(futureProductDetails, 'contract_root_unit') + quoteId = self.safe_string(market, 'quote_currency_id') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + tradingDisabled = self.safe_bool(market, 'is_disabled') + symbol = base + '/' + quote + type = None + if isSwap: + type = 'swap' + symbol = symbol + ':' + quote + else: + type = 'future' + symbol = symbol + ':' + quote + '-' + self.yymmdd(expireTimestamp) + takerFeeRate = self.safe_number(feeTier, 'taker_fee_rate') + makerFeeRate = self.safe_number(feeTier, 'maker_fee_rate') + taker = takerFeeRate if takerFeeRate else self.parse_number('0.06') + maker = makerFeeRate if makerFeeRate else self.parse_number('0.04') + return self.safe_market_structure({ + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': quote, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': quoteId, + 'type': type, + 'spot': False, + 'margin': False, + 'swap': isSwap, + 'future': not isSwap, + 'option': False, + 'active': not tradingDisabled, + 'contract': True, + 'linear': True, + 'inverse': False, + 'taker': taker, + 'maker': maker, + 'contractSize': contractSize, + 'expiry': expireTimestamp, + 'expiryDatetime': expireDateTime, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.safe_number(market, 'base_increment'), + 'price': self.safe_number_2(market, 'price_increment', 'quote_increment'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(market, 'base_min_size'), + 'max': self.safe_number(market, 'base_max_size'), + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': self.safe_number(market, 'quote_min_size'), + 'max': self.safe_number(market, 'quote_max_size'), + }, + }, + 'created': None, + 'info': market, + }) + + def fetch_currencies_from_cache(self, params={}): + options = self.safe_dict(self.options, 'fetchCurrencies', {}) + timestamp = self.safe_integer(options, 'timestamp') + expires = self.safe_integer(options, 'expires', 1000) + now = self.milliseconds() + if (timestamp is None) or ((now - timestamp) > expires): + promises = [ + self.v2PublicGetCurrencies(params), + self.v2PublicGetCurrenciesCrypto(params), + ] + promisesResult = promises + fiatResponse = self.safe_dict(promisesResult, 0, {}) + # + # [ + # "data": { + # id: 'IMP', + # name: 'Isle of Man Pound', + # min_size: '0.01' + # }, + # ... + # ] + # + cryptoResponse = self.safe_dict(promisesResult, 1, {}) + # + # { + # asset_id: '9476e3be-b731-47fa-82be-347fabc573d9', + # code: 'AERO', + # name: 'Aerodrome Finance', + # color: '#0433FF', + # sort_index: '340', + # exponent: '8', + # type: 'crypto', + # address_regex: '^(?:0x)?[0-9a-fA-F]{40}$' + # } + # + fiatData = self.safe_list(fiatResponse, 'data', []) + cryptoData = self.safe_list(cryptoResponse, 'data', []) + exchangeRates = self.v2PublicGetExchangeRates(params) + self.options['fetchCurrencies'] = self.extend(options, { + 'currencies': self.array_concat(fiatData, cryptoData), + 'exchangeRates': exchangeRates, + 'timestamp': now, + }) + return self.safe_dict(self.options, 'fetchCurrencies', {}) + + def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/api-currencies#get-fiat-currencies + https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/api-exchange-rates#get-exchange-rates + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + promises = [ + self.v2PublicGetCurrencies(params), + self.v2PublicGetCurrenciesCrypto(params), + self.v2PublicGetExchangeRates(params), + ] + promisesResult = promises + fiatResponse = self.safe_dict(promisesResult, 0, {}) + # + # [ + # "data": [ + # { + # id: 'IMP', + # name: 'Isle of Man Pound', + # min_size: '0.01' + # }, + # ... + # + cryptoResponse = self.safe_dict(promisesResult, 1, {}) + # + # [ + # "data": [ + # { + # asset_id: '9476e3be-b731-47fa-82be-347fabc573d9', + # code: 'AERO', + # name: 'Aerodrome Finance', + # color: '#0433FF', + # sort_index: '340', + # exponent: '8', + # type: 'crypto', + # address_regex: '^(?:0x)?[0-9a-fA-F]{40}$' + # }, + # ... + # + ratesResponse = self.safe_dict(promisesResult, 2, {}) + fiatData = self.safe_list(fiatResponse, 'data', []) + cryptoData = self.safe_list(cryptoResponse, 'data', []) + ratesData = self.safe_dict(ratesResponse, 'data', {}) + rates = self.safe_dict(ratesData, 'rates', {}) + ratesIds = list(rates.keys()) + currencies = self.array_concat(fiatData, cryptoData) + result: dict = {} + networks: dict = {} + networksById: dict = {} + for i in range(0, len(currencies)): + currency = currencies[i] + assetId = self.safe_string(currency, 'asset_id') + id = self.safe_string_2(currency, 'id', 'code') + code = self.safe_currency_code(id) + name = self.safe_string(currency, 'name') + self.options['networks'][code] = name.lower() + self.options['networksById'][code] = name.lower() + type = 'crypto' if (assetId is not None) else 'fiat' + result[code] = self.safe_currency_structure({ + 'info': currency, + 'id': id, + 'code': code, + 'type': type, + 'name': name, + 'active': True, + 'deposit': None, + 'withdraw': None, + 'fee': None, + 'precision': None, + 'networks': {}, # todo + 'limits': { + 'amount': { + 'min': self.safe_number(currency, 'min_size'), + 'max': None, + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + }, + }) + if assetId is not None: + lowerCaseName = name.lower() + networks[code] = lowerCaseName + networksById[lowerCaseName] = code + # we have to add other currencies here( https://discord.com/channels/1220414409550336183/1220464770239430761/1372215891940479098 ) + for i in range(0, len(ratesIds)): + currencyId = ratesIds[i] + code = self.safe_currency_code(currencyId) + if not (code in result): + result[code] = self.safe_currency_structure({ + 'info': {}, + 'id': currencyId, + 'code': code, + 'type': 'crypto', + 'networks': {}, # todo + }) + self.options['networks'] = self.extend(networks, self.options['networks']) + self.options['networksById'] = self.extend(networksById, self.options['networksById']) + return result + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_getproducts + https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/api-exchange-rates#get-exchange-rates + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.usePrivate]: use private endpoint for fetching tickers + :returns dict: a dictionary of `ticker structures ` + """ + method = self.safe_string(self.options, 'fetchTickers', 'fetchTickersV3') + if method == 'fetchTickersV3': + return self.fetch_tickers_v3(symbols, params) + return self.fetch_tickers_v2(symbols, params) + + def fetch_tickers_v2(self, symbols: Strings = None, params={}) -> Tickers: + self.load_markets() + symbols = self.market_symbols(symbols) + request: dict = { + # 'currency': 'USD', + } + response = self.v2PublicGetExchangeRates(self.extend(request, params)) + # + # { + # "data":{ + # "currency":"USD", + # "rates":{ + # "AED":"3.6731", + # "AFN":"103.163942", + # "ALL":"106.973038", + # } + # } + # } + # + data = self.safe_dict(response, 'data', {}) + rates = self.safe_dict(data, 'rates', {}) + quoteId = self.safe_string(data, 'currency') + result: dict = {} + baseIds = list(rates.keys()) + delimiter = '-' + for i in range(0, len(baseIds)): + baseId = baseIds[i] + marketId = baseId + delimiter + quoteId + market = self.safe_market(marketId, None, delimiter) + symbol = market['symbol'] + result[symbol] = self.parse_ticker(rates[baseId], market) + return self.filter_by_array_tickers(result, 'symbol', symbols) + + def fetch_tickers_v3(self, symbols: Strings = None, params={}) -> Tickers: + self.load_markets() + symbols = self.market_symbols(symbols) + request: dict = {} + if symbols is not None: + request['product_ids'] = self.market_ids(symbols) + marketType = None + marketType, params = self.handle_market_type_and_params('fetchTickers', self.get_market_from_symbols(symbols), params, 'default') + if marketType is not None and marketType != 'default': + request['product_type'] = 'FUTURE' if (marketType == 'swap') else 'SPOT' + response = None + usePrivate = False + usePrivate, params = self.handle_option_and_params(params, 'fetchTickers', 'usePrivate', False) + if usePrivate: + response = self.v3PrivateGetBrokerageProducts(self.extend(request, params)) + else: + response = self.v3PublicGetBrokerageMarketProducts(self.extend(request, params)) + # + # { + # "products": [ + # { + # "product_id": "TONE-USD", + # "price": "0.01523", + # "price_percentage_change_24h": "1.94109772423025", + # "volume_24h": "19773129", + # "volume_percentage_change_24h": "437.0170530929949", + # "base_increment": "1", + # "quote_increment": "0.00001", + # "quote_min_size": "1", + # "quote_max_size": "10000000", + # "base_min_size": "26.7187147229469674", + # "base_max_size": "267187147.2294696735908216", + # "base_name": "TE-FOOD", + # "quote_name": "US Dollar", + # "watched": False, + # "is_disabled": False, + # "new": False, + # "status": "online", + # "cancel_only": False, + # "limit_only": False, + # "post_only": False, + # "trading_disabled": False, + # "auction_mode": False, + # "product_type": "SPOT", + # "quote_currency_id": "USD", + # "base_currency_id": "TONE", + # "fcm_trading_session_details": null, + # "mid_market_price": "" + # }, + # ... + # ], + # "num_products": 549 + # } + # + data = self.safe_list(response, 'products', []) + result: dict = {} + for i in range(0, len(data)): + entry = data[i] + marketId = self.safe_string(entry, 'product_id') + market = self.safe_market(marketId, None, '-') + symbol = market['symbol'] + result[symbol] = self.parse_ticker(entry, market) + return self.filter_by_array_tickers(result, 'symbol', symbols) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_getmarkettrades + https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/api-prices#get-spot-price + https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/api-prices#get-buy-price + https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/api-prices#get-sell-price + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.usePrivate]: whether to use the private endpoint for fetching the ticker + :returns dict: a `ticker structure ` + """ + method = self.safe_string(self.options, 'fetchTicker', 'fetchTickerV3') + if method == 'fetchTickerV3': + return self.fetch_ticker_v3(symbol, params) + return self.fetch_ticker_v2(symbol, params) + + def fetch_ticker_v2(self, symbol: str, params={}): + self.load_markets() + market = self.market(symbol) + request = self.extend({ + 'symbol': market['id'], + }, params) + spot = self.v2PublicGetPricesSymbolSpot(request) + # + # {"data":{"base":"BTC","currency":"USD","amount":"48691.23"}} + # + ask = self.v2PublicGetPricesSymbolBuy(request) + # + # {"data":{"base":"BTC","currency":"USD","amount":"48691.23"}} + # + bid = self.v2PublicGetPricesSymbolSell(request) + # + # {"data":{"base":"BTC","currency":"USD","amount":"48691.23"}} + # + spotData = self.safe_dict(spot, 'data', {}) + askData = self.safe_dict(ask, 'data', {}) + bidData = self.safe_dict(bid, 'data', {}) + bidAskLast: dict = { + 'bid': self.safe_number(bidData, 'amount'), + 'ask': self.safe_number(askData, 'amount'), + 'price': self.safe_number(spotData, 'amount'), + } + return self.parse_ticker(bidAskLast, market) + + def fetch_ticker_v3(self, symbol: str, params={}): + self.load_markets() + market = self.market(symbol) + request: dict = { + 'product_id': market['id'], + 'limit': 1, + } + usePrivate = False + usePrivate, params = self.handle_option_and_params(params, 'fetchTicker', 'usePrivate', False) + response = None + if usePrivate: + response = self.v3PrivateGetBrokerageProductsProductIdTicker(self.extend(request, params)) + else: + response = self.v3PublicGetBrokerageMarketProductsProductIdTicker(self.extend(request, params)) + # + # { + # "trades": [ + # { + # "trade_id": "518078013", + # "product_id": "BTC-USD", + # "price": "28208.1", + # "size": "0.00659179", + # "time": "2023-04-04T23:05:34.492746Z", + # "side": "BUY", + # "bid": "", + # "ask": "" + # } + # ], + # "best_bid": "28208.61", + # "best_ask": "28208.62" + # } + # + data = self.safe_list(response, 'trades', []) + ticker = self.parse_ticker(data[0], market) + ticker['bid'] = self.safe_number(response, 'best_bid') + ticker['ask'] = self.safe_number(response, 'best_ask') + return ticker + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # fetchTickerV2 + # + # { + # "bid": 20713.37, + # "ask": 20924.65, + # "price": 20809.83 + # } + # + # fetchTickerV3 + # + # { + # "trade_id": "10209805", + # "product_id": "BTC-USDT", + # "price": "19381.27", + # "size": "0.1", + # "time": "2023-01-13T20:35:41.865970Z", + # "side": "BUY", + # "bid": "", + # "ask": "" + # } + # + # fetchTickersV2 + # + # "48691.23" + # + # fetchTickersV3 + # + # [ + # { + # "product_id": "ETH-USD", + # "price": "4471.59", + # "price_percentage_change_24h": "0.14243387238731", + # "volume_24h": "87329.92990204", + # "volume_percentage_change_24h": "-60.7789801794578", + # "base_increment": "0.00000001", + # "quote_increment": "0.01", + # "quote_min_size": "1", + # "quote_max_size": "150000000", + # "base_min_size": "0.00000001", + # "base_max_size": "42000", + # "base_name": "Ethereum", + # "quote_name": "US Dollar", + # "watched": False, + # "is_disabled": False, + # "new": False, + # "status": "online", + # "cancel_only": False, + # "limit_only": False, + # "post_only": False, + # "trading_disabled": False, + # "auction_mode": False, + # "product_type": "SPOT", + # "quote_currency_id": "USD", + # "base_currency_id": "ETH", + # "fcm_trading_session_details": null, + # "mid_market_price": "", + # "alias": "", + # "alias_to": ["ETH-USDC"], + # "base_display_symbol": "ETH", + # "quote_display_symbol": "USD", + # "view_only": False, + # "price_increment": "0.01", + # "display_name": "ETH-USD", + # "product_venue": "CBE", + # "approximate_quote_24h_volume": "390503641.25", + # "new_at": "2023-01-01T00:00:00Z" + # }, + # ... + # ] + # + # fetchBidsAsks + # + # { + # "product_id": "TRAC-EUR", + # "bids": [ + # { + # "price": "0.2384", + # "size": "386.1" + # } + # ], + # "asks": [ + # { + # "price": "0.2406", + # "size": "672" + # } + # ], + # "time": "2023-06-30T07:15:24.656044Z" + # } + # + bid = self.safe_number(ticker, 'bid') + ask = self.safe_number(ticker, 'ask') + bidVolume = None + askVolume = None + if ('bids' in ticker): + bids = self.safe_list(ticker, 'bids', []) + asks = self.safe_list(ticker, 'asks', []) + firstBid = self.safe_dict(bids, 0, {}) + firstAsk = self.safe_dict(asks, 0, {}) + bid = self.safe_number(firstBid, 'price') + bidVolume = self.safe_number(firstBid, 'size') + ask = self.safe_number(firstAsk, 'price') + askVolume = self.safe_number(firstAsk, 'size') + marketId = self.safe_string(ticker, 'product_id') + market = self.safe_market(marketId, market) + last = self.safe_number(ticker, 'price') + datetime = self.safe_string(ticker, 'time') + return self.safe_ticker({ + 'symbol': market['symbol'], + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + 'bid': bid, + 'ask': ask, + 'last': last, + 'high': None, + 'low': None, + 'bidVolume': bidVolume, + 'askVolume': askVolume, + 'vwap': None, + 'open': None, + 'close': last, + 'previousClose': None, + 'change': None, + 'percentage': self.safe_number(ticker, 'price_percentage_change_24h'), + 'average': None, + 'baseVolume': self.safe_number(ticker, 'volume_24h'), + 'quoteVolume': self.safe_number(ticker, 'approximate_quote_24h_volume'), + 'info': ticker, + }, market) + + def parse_custom_balance(self, response, params={}): + balances = self.safe_list_2(response, 'data', 'accounts', []) + accounts = self.safe_list(params, 'type', self.options['accounts']) + v3Accounts = self.safe_list(params, 'type', self.options['v3Accounts']) + result: dict = {'info': response} + for b in range(0, len(balances)): + balance = balances[b] + type = self.safe_string(balance, 'type') + if self.in_array(type, accounts): + value = self.safe_dict(balance, 'balance') + if value is not None: + currencyId = self.safe_string(value, 'currency') + code = self.safe_currency_code(currencyId) + total = self.safe_string(value, 'amount') + free = total + account = self.safe_dict(result, code) + if account is None: + account = self.account() + account['free'] = free + account['total'] = total + else: + account['free'] = Precise.string_add(account['free'], total) + account['total'] = Precise.string_add(account['total'], total) + result[code] = account + elif self.in_array(type, v3Accounts): + available = self.safe_dict(balance, 'available_balance') + hold = self.safe_dict(balance, 'hold') + if available is not None and hold is not None: + currencyId = self.safe_string(available, 'currency') + code = self.safe_currency_code(currencyId) + used = self.safe_string(hold, 'value') + free = self.safe_string(available, 'value') + total = Precise.string_add(used, free) + account = self.safe_dict(result, code) + if account is None: + account = self.account() + account['free'] = free + account['used'] = used + account['total'] = total + else: + account['free'] = Precise.string_add(account['free'], free) + account['used'] = Precise.string_add(account['used'], used) + account['total'] = Precise.string_add(account['total'], total) + result[code] = account + return self.safe_balance(result) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_getaccounts + https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/api-accounts#list-accounts + https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_getfcmbalancesummary + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.v3]: default False, set True to use v3 api endpoint + :param str [params.type]: "spot"(default) or "swap" or "future" + :param int [params.limit]: default 250, maximum number of accounts to return + :returns dict: a `balance structure ` + """ + self.load_markets() + request: dict = {} + response = None + isV3 = self.safe_bool(params, 'v3', False) + params = self.omit(params, ['v3']) + marketType = None + marketType, params = self.handle_market_type_and_params('fetchBalance', None, params) + method = self.safe_string(self.options, 'fetchBalance', 'v3PrivateGetBrokerageAccounts') + if marketType == 'future': + response = self.v3PrivateGetBrokerageCfmBalanceSummary(self.extend(request, params)) + elif (isV3) or (method == 'v3PrivateGetBrokerageAccounts'): + request['limit'] = 250 + response = self.v3PrivateGetBrokerageAccounts(self.extend(request, params)) + else: + request['limit'] = 250 + response = self.v2PrivateGetAccounts(self.extend(request, params)) + # + # v2PrivateGetAccounts + # { + # "pagination":{ + # "ending_before":null, + # "starting_after":null, + # "previous_ending_before":null, + # "next_starting_after":"6b17acd6-2e68-5eb0-9f45-72d67cef578a", + # "limit":100, + # "order":"desc", + # "previous_uri":null, + # "next_uri":"/v2/accounts?limit=100\u0026starting_after=6b17acd6-2e68-5eb0-9f45-72d67cef578b" + # }, + # "data":[ + # { + # "id":"94ad58bc-0f15-5309-b35a-a4c86d7bad60", + # "name":"MINA Wallet", + # "primary":false, + # "type":"wallet", + # "currency":{ + # "code":"MINA", + # "name":"Mina", + # "color":"#EA6B48", + # "sort_index":397, + # "exponent":9, + # "type":"crypto", + # "address_regex":"^(B62)[A-Za-z0-9]{52}$", + # "asset_id":"a4ffc575-942c-5e26-b70c-cb3befdd4229", + # "slug":"mina" + # }, + # "balance":{"amount":"0.000000000","currency":"MINA"}, + # "created_at":"2022-03-25T00:36:16Z", + # "updated_at":"2022-03-25T00:36:16Z", + # "resource":"account", + # "resource_path":"/v2/accounts/94ad58bc-0f15-5309-b35a-a4c86d7bad60", + # "allow_deposits":true, + # "allow_withdrawals":true + # }, + # ] + # } + # + # v3PrivateGetBrokerageAccounts + # { + # "accounts": [ + # { + # "uuid": "11111111-1111-1111-1111-111111111111", + # "name": "USDC Wallet", + # "currency": "USDC", + # "available_balance": { + # "value": "0.0000000000000000", + # "currency": "USDC" + # }, + # "default": True, + # "active": True, + # "created_at": "2023-01-04T06:20:06.456Z", + # "updated_at": "2023-01-04T06:20:07.181Z", + # "deleted_at": null, + # "type": "ACCOUNT_TYPE_CRYPTO", + # "ready": False, + # "hold": { + # "value": "0.0000000000000000", + # "currency": "USDC" + # } + # }, + # ... + # ], + # "has_next": False, + # "cursor": "", + # "size": 9 + # } + # + params['type'] = marketType + return self.parse_custom_balance(response, params) + + def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + Fetch the history of changes, i.e. actions done by the user or operations that altered the balance. Will return staking rewards, and crypto deposits or withdrawals. + + https://docs.cdp.coinbase.com/coinbase-app/docs/api-transactions#list-transactions + + :param str [code]: unified currency code, default is None + :param int [since]: timestamp in ms of the earliest ledger entry, default is None + :param int [limit]: max number of ledger entries to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict: a `ledger structure ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchLedger', 'paginate') + if paginate: + return self.fetch_paginated_call_cursor('fetchLedger', code, since, limit, params, 'next_starting_after', 'starting_after', None, 100) + currency = None + if code is not None: + currency = self.currency(code) + request = None + request, params = self.prepare_account_request_with_currency_code(code, limit, params) + # for pagination use parameter 'starting_after' + # the value for the next page can be obtained from the result of the previous call in the 'pagination' field + # eg: instance.last_http_response -> pagination.next_starting_after + response = self.v2PrivateGetAccountsAccountIdTransactions(self.extend(request, params)) + ledger = self.parse_ledger(response['data'], currency, since, limit) + length = len(ledger) + if length == 0: + return ledger + lastIndex = length - 1 + last = self.safe_dict(ledger, lastIndex) + pagination = self.safe_dict(response, 'pagination', {}) + cursor = self.safe_string(pagination, 'next_starting_after') + if (cursor is not None) and (cursor != ''): + last['info']['next_starting_after'] = cursor + ledger[lastIndex] = last + return ledger + + def parse_ledger_entry_status(self, status): + types: dict = { + 'completed': 'ok', + } + return self.safe_string(types, status, status) + + def parse_ledger_entry_type(self, type): + types: dict = { + 'buy': 'trade', + 'sell': 'trade', + 'fiat_deposit': 'transaction', + 'fiat_withdrawal': 'transaction', + 'exchange_deposit': 'transaction', # fiat withdrawal(from coinbase to coinbasepro) + 'exchange_withdrawal': 'transaction', # fiat deposit(to coinbase from coinbasepro) + 'send': 'transaction', # crypto deposit OR withdrawal + 'pro_deposit': 'transaction', # crypto withdrawal(from coinbase to coinbasepro) + 'pro_withdrawal': 'transaction', # crypto deposit(to coinbase from coinbasepro) + } + return self.safe_string(types, type, type) + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + # + # crypto deposit transaction + # + # { + # "id": "34e4816b-4c8c-5323-a01c-35a9fa26e490", + # "type": "send", + # "status": "completed", + # "amount": {amount: "28.31976528", currency: "BCH"}, + # "native_amount": {amount: "2799.65", currency: "GBP"}, + # "description": null, + # "created_at": "2019-02-28T12:35:20Z", + # "updated_at": "2019-02-28T12:43:24Z", + # "resource": "transaction", + # "resource_path": "/v2/accounts/c01d7364-edd7-5f3a-bd1d-de53d4cbb25e/transactions/34e4816b-4c8c-5323-a01c-35a9fa26e490", + # "instant_exchange": False, + # "network": { + # "status": "confirmed", + # "hash": "56222d865dae83774fccb2efbd9829cf08c75c94ce135bfe4276f3fb46d49701", + # "transaction_url": "https://bch.btc.com/56222d865dae83774fccb2efbd9829cf08c75c94ce135bfe4276f3fb46d49701" + # }, + # "from": {resource: "bitcoin_cash_network", currency: "BCH"}, + # "details": {title: 'Received Bitcoin Cash', subtitle: "From Bitcoin Cash address"} + # } + # + # crypto withdrawal transaction + # + # { + # "id": "459aad99-2c41-5698-ac71-b6b81a05196c", + # "type": "send", + # "status": "completed", + # "amount": {amount: "-0.36775642", currency: "BTC"}, + # "native_amount": {amount: "-1111.65", currency: "GBP"}, + # "description": null, + # "created_at": "2019-03-20T08:37:07Z", + # "updated_at": "2019-03-20T08:49:33Z", + # "resource": "transaction", + # "resource_path": "/v2/accounts/c6afbd34-4bd0-501e-8616-4862c193cd84/transactions/459aad99-2c41-5698-ac71-b6b81a05196c", + # "instant_exchange": False, + # "network": { + # "status": "confirmed", + # "hash": "2732bbcf35c69217c47b36dce64933d103895277fe25738ffb9284092701e05b", + # "transaction_url": "https://blockchain.info/tx/2732bbcf35c69217c47b36dce64933d103895277fe25738ffb9284092701e05b", + # "transaction_fee": {amount: "0.00000000", currency: "BTC"}, + # "transaction_amount": {amount: "0.36775642", currency: "BTC"}, + # "confirmations": 15682 + # }, + # "to": { + # "resource": "bitcoin_address", + # "address": "1AHnhqbvbYx3rnZx8uC7NbFZaTe4tafFHX", + # "currency": "BTC", + # "address_info": {address: "1AHnhqbvbYx3rnZx8uC7NbFZaTe4tafFHX"} + # }, + # "idem": "da0a2f14-a2af-4c5a-a37e-d4484caf582bsend", + # "application": { + # "id": "5756ab6e-836b-553b-8950-5e389451225d", + # "resource": "application", + # "resource_path": "/v2/applications/5756ab6e-836b-553b-8950-5e389451225d" + # }, + # "details": {title: 'Sent Bitcoin', subtitle: "To Bitcoin address"} + # } + # + # withdrawal transaction from coinbase to coinbasepro + # + # { + # "id": "5b1b9fb8-5007-5393-b923-02903b973fdc", + # "type": "pro_deposit", + # "status": "completed", + # "amount": {amount: "-0.00001111", currency: "BCH"}, + # "native_amount": {amount: "0.00", currency: "GBP"}, + # "description": null, + # "created_at": "2019-02-28T13:31:58Z", + # "updated_at": "2019-02-28T13:31:58Z", + # "resource": "transaction", + # "resource_path": "/v2/accounts/c01d7364-edd7-5f3a-bd1d-de53d4cbb25e/transactions/5b1b9fb8-5007-5393-b923-02903b973fdc", + # "instant_exchange": False, + # "application": { + # "id": "5756ab6e-836b-553b-8950-5e389451225d", + # "resource": "application", + # "resource_path": "/v2/applications/5756ab6e-836b-553b-8950-5e389451225d" + # }, + # "details": {title: 'Transferred Bitcoin Cash', subtitle: "To Coinbase Pro"} + # } + # + # withdrawal transaction from coinbase to gdax + # + # { + # "id": "badb7313-a9d3-5c07-abd0-00f8b44199b1", + # "type": "exchange_deposit", + # "status": "completed", + # "amount": {amount: "-0.43704149", currency: "BCH"}, + # "native_amount": {amount: "-51.90", currency: "GBP"}, + # "description": null, + # "created_at": "2019-03-19T10:30:40Z", + # "updated_at": "2019-03-19T10:30:40Z", + # "resource": "transaction", + # "resource_path": "/v2/accounts/c01d7364-edd7-5f3a-bd1d-de53d4cbb25e/transactions/badb7313-a9d3-5c07-abd0-00f8b44199b1", + # "instant_exchange": False, + # "details": {title: 'Transferred Bitcoin Cash', subtitle: "To GDAX"} + # } + # + # deposit transaction from gdax to coinbase + # + # { + # "id": "9c4b642c-8688-58bf-8962-13cef64097de", + # "type": "exchange_withdrawal", + # "status": "completed", + # "amount": {amount: "0.57729420", currency: "BTC"}, + # "native_amount": {amount: "4418.72", currency: "GBP"}, + # "description": null, + # "created_at": "2018-02-17T11:33:33Z", + # "updated_at": "2018-02-17T11:33:33Z", + # "resource": "transaction", + # "resource_path": "/v2/accounts/c6afbd34-4bd0-501e-8616-4862c193cd84/transactions/9c4b642c-8688-58bf-8962-13cef64097de", + # "instant_exchange": False, + # "details": {title: 'Transferred Bitcoin', subtitle: "From GDAX"} + # } + # + # deposit transaction from coinbasepro to coinbase + # + # { + # "id": "8d6dd0b9-3416-568a-889d-8f112fae9e81", + # "type": "pro_withdrawal", + # "status": "completed", + # "amount": {amount: "0.40555386", currency: "BTC"}, + # "native_amount": {amount: "1140.27", currency: "GBP"}, + # "description": null, + # "created_at": "2019-03-04T19:41:58Z", + # "updated_at": "2019-03-04T19:41:58Z", + # "resource": "transaction", + # "resource_path": "/v2/accounts/c6afbd34-4bd0-501e-8616-4862c193cd84/transactions/8d6dd0b9-3416-568a-889d-8f112fae9e81", + # "instant_exchange": False, + # "application": { + # "id": "5756ab6e-836b-553b-8950-5e389451225d", + # "resource": "application", + # "resource_path": "/v2/applications/5756ab6e-836b-553b-8950-5e389451225d" + # }, + # "details": {title: 'Transferred Bitcoin', subtitle: "From Coinbase Pro"} + # } + # + # sell trade + # + # { + # "id": "a9409207-df64-585b-97ab-a50780d2149e", + # "type": "sell", + # "status": "completed", + # "amount": {amount: "-9.09922880", currency: "BTC"}, + # "native_amount": {amount: "-7285.73", currency: "GBP"}, + # "description": null, + # "created_at": "2017-03-27T15:38:34Z", + # "updated_at": "2017-03-27T15:38:34Z", + # "resource": "transaction", + # "resource_path": "/v2/accounts/c6afbd34-4bd0-501e-8616-4862c193cd84/transactions/a9409207-df64-585b-97ab-a50780d2149e", + # "instant_exchange": False, + # "sell": { + # "id": "e3550b4d-8ae6-5de3-95fe-1fb01ba83051", + # "resource": "sell", + # "resource_path": "/v2/accounts/c6afbd34-4bd0-501e-8616-4862c193cd84/sells/e3550b4d-8ae6-5de3-95fe-1fb01ba83051" + # }, + # "details": { + # "title": "Sold Bitcoin", + # "subtitle": "Using EUR Wallet", + # "payment_method_name": "EUR Wallet" + # } + # } + # + # buy trade + # + # { + # "id": "63eeed67-9396-5912-86e9-73c4f10fe147", + # "type": "buy", + # "status": "completed", + # "amount": {amount: "2.39605772", currency: "ETH"}, + # "native_amount": {amount: "98.31", currency: "GBP"}, + # "description": null, + # "created_at": "2017-03-27T09:07:56Z", + # "updated_at": "2017-03-27T09:07:57Z", + # "resource": "transaction", + # "resource_path": "/v2/accounts/8902f85d-4a69-5d74-82fe-8e390201bda7/transactions/63eeed67-9396-5912-86e9-73c4f10fe147", + # "instant_exchange": False, + # "buy": { + # "id": "20b25b36-76c6-5353-aa57-b06a29a39d82", + # "resource": "buy", + # "resource_path": "/v2/accounts/8902f85d-4a69-5d74-82fe-8e390201bda7/buys/20b25b36-76c6-5353-aa57-b06a29a39d82" + # }, + # "details": { + # "title": "Bought Ethereum", + # "subtitle": "Using EUR Wallet", + # "payment_method_name": "EUR Wallet" + # } + # } + # + # fiat deposit transaction + # + # { + # "id": "04ed4113-3732-5b0c-af86-b1d2146977d0", + # "type": "fiat_deposit", + # "status": "completed", + # "amount": {amount: "114.02", currency: "EUR"}, + # "native_amount": {amount: "97.23", currency: "GBP"}, + # "description": null, + # "created_at": "2017-02-09T07:01:21Z", + # "updated_at": "2017-02-09T07:01:22Z", + # "resource": "transaction", + # "resource_path": "/v2/accounts/91cd2d36-3a91-55b6-a5d4-0124cf105483/transactions/04ed4113-3732-5b0c-af86-b1d2146977d0", + # "instant_exchange": False, + # "fiat_deposit": { + # "id": "f34c19f3-b730-5e3d-9f72-96520448677a", + # "resource": "fiat_deposit", + # "resource_path": "/v2/accounts/91cd2d36-3a91-55b6-a5d4-0124cf105483/deposits/f34c19f3-b730-5e3d-9f72-96520448677a" + # }, + # "details": { + # "title": "Deposited funds", + # "subtitle": "From SEPA Transfer(GB47 BARC 20..., reference CBADVI)", + # "payment_method_name": "SEPA Transfer(GB47 BARC 20..., reference CBADVI)" + # } + # } + # + # fiat withdrawal transaction + # + # { + # "id": "957d98e2-f80e-5e2f-a28e-02945aa93079", + # "type": "fiat_withdrawal", + # "status": "completed", + # "amount": {amount: "-11000.00", currency: "EUR"}, + # "native_amount": {amount: "-9698.22", currency: "GBP"}, + # "description": null, + # "created_at": "2017-12-06T13:19:19Z", + # "updated_at": "2017-12-06T13:19:19Z", + # "resource": "transaction", + # "resource_path": "/v2/accounts/91cd2d36-3a91-55b6-a5d4-0124cf105483/transactions/957d98e2-f80e-5e2f-a28e-02945aa93079", + # "instant_exchange": False, + # "fiat_withdrawal": { + # "id": "f4bf1fd9-ab3b-5de7-906d-ed3e23f7a4e7", + # "resource": "fiat_withdrawal", + # "resource_path": "/v2/accounts/91cd2d36-3a91-55b6-a5d4-0124cf105483/withdrawals/f4bf1fd9-ab3b-5de7-906d-ed3e23f7a4e7" + # }, + # "details": { + # "title": "Withdrew funds", + # "subtitle": "To HSBC BANK PLC(GB74 MIDL...)", + # "payment_method_name": "HSBC BANK PLC(GB74 MIDL...)" + # } + # } + # + amountInfo = self.safe_dict(item, 'amount', {}) + amount = self.safe_string(amountInfo, 'amount') + direction = None + if Precise.string_lt(amount, '0'): + direction = 'out' + amount = Precise.string_neg(amount) + else: + direction = 'in' + currencyId = self.safe_string(amountInfo, 'currency') + code = self.safe_currency_code(currencyId, currency) + currency = self.safe_currency(currencyId, currency) + # + # the address and txid do not belong to the unified ledger structure + # + # address = None + # if item['to']: + # address = self.safe_string(item['to'], 'address') + # } + # txid = None + # + fee = None + networkInfo = self.safe_dict(item, 'network', {}) + # txid = network['hash'] # txid does not belong to the unified ledger structure + feeInfo = self.safe_dict(networkInfo, 'transaction_fee') + if feeInfo is not None: + feeCurrencyId = self.safe_string(feeInfo, 'currency') + feeCurrencyCode = self.safe_currency_code(feeCurrencyId, currency) + feeAmount = self.safe_number(feeInfo, 'amount') + fee = { + 'cost': feeAmount, + 'currency': feeCurrencyCode, + } + timestamp = self.parse8601(self.safe_string(item, 'created_at')) + id = self.safe_string(item, 'id') + type = self.parse_ledger_entry_type(self.safe_string(item, 'type')) + status = self.parse_ledger_entry_status(self.safe_string(item, 'status')) + path = self.safe_string(item, 'resource_path') + accountId = None + if path is not None: + parts = path.split('/') + numParts = len(parts) + if numParts > 3: + accountId = parts[3] + return self.safe_ledger_entry({ + 'info': item, + 'id': id, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'direction': direction, + 'account': accountId, + 'referenceId': None, + 'referenceAccount': None, + 'type': type, + 'currency': code, + 'amount': self.parse_number(amount), + 'before': None, + 'after': None, + 'status': status, + 'fee': fee, + }, currency) + + def find_account_id(self, code, params={}): + self.load_markets() + self.load_accounts(False, params) + for i in range(0, len(self.accounts)): + account = self.accounts[i] + if account['code'] == code: + return account['id'] + return None + + def prepare_account_request(self, limit: Int = None, params={}): + accountId = self.safe_string_2(params, 'account_id', 'accountId') + if accountId is None: + raise ArgumentsRequired(self.id + ' prepareAccountRequest() method requires an account_id(or accountId) parameter') + request: dict = { + 'account_id': accountId, + } + if limit is not None: + request['limit'] = limit + return request + + def prepare_account_request_with_currency_code(self, code: Str = None, limit: Int = None, params={}): + accountId = self.safe_string_2(params, 'account_id', 'accountId') + params = self.omit(params, ['account_id', 'accountId']) + if accountId is None: + if code is None: + raise ArgumentsRequired(self.id + ' prepareAccountRequestWithCurrencyCode() method requires an account_id(or accountId) parameter OR a currency code argument') + accountId = self.find_account_id(code, params) + if accountId is None: + raise ExchangeError(self.id + ' prepareAccountRequestWithCurrencyCode() could not find account id for ' + code + '. You might try to generate the deposit address in the website for that coin first.') + request: dict = { + 'account_id': accountId, + } + if limit is not None: + request['limit'] = limit + return [request, params] + + def create_market_buy_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market buy order by providing the symbol and cost + + https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_postorder + + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' createMarketBuyOrderWithCost() supports spot orders only') + params['createMarketBuyOrderRequiresPrice'] = False + return self.create_order(symbol, 'market', 'buy', cost, None, params) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_postorder + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency, quote currency for 'market' 'buy' orders + :param float [price]: the price to fulfill the order, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.stopPrice]: price to trigger stop orders + :param float [params.triggerPrice]: price to trigger stop orders + :param float [params.stopLossPrice]: price to trigger stop-loss orders + :param float [params.takeProfitPrice]: price to trigger take-profit orders + :param bool [params.postOnly]: True or False + :param str [params.timeInForce]: 'GTC', 'IOC', 'GTD' or 'PO', 'FOK' + :param str [params.stop_direction]: 'UNKNOWN_STOP_DIRECTION', 'STOP_DIRECTION_STOP_UP', 'STOP_DIRECTION_STOP_DOWN' the direction the stopPrice is triggered from + :param str [params.end_time]: '2023-05-25T17:01:05.092Z' for 'GTD' orders + :param float [params.cost]: *spot market buy only* the quote quantity that can be used alternative for the amount + :param boolean [params.preview]: default to False, wether to use the test/preview endpoint or not + :param float [params.leverage]: default to 1, the leverage to use for the order + :param str [params.marginMode]: 'cross' or 'isolated' + :param str [params.retail_portfolio_id]: portfolio uid + :param boolean [params.is_max]: Used in conjunction with tradable_balance to indicate the user wants to use their entire tradable balance + :param str [params.tradable_balance]: amount of tradable balance + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + id = self.safe_string(self.options, 'brokerId', 'ccxt') + request: dict = { + 'client_order_id': id + '-' + self.uuid(), + 'product_id': market['id'], + 'side': side.upper(), + } + triggerPrice = self.safe_number_n(params, ['stopPrice', 'stop_price', 'triggerPrice']) + stopLossPrice = self.safe_number(params, 'stopLossPrice') + takeProfitPrice = self.safe_number(params, 'takeProfitPrice') + isStop = triggerPrice is not None + isStopLoss = stopLossPrice is not None + isTakeProfit = takeProfitPrice is not None + timeInForce = self.safe_string(params, 'timeInForce') + postOnly = True if (timeInForce == 'PO') else self.safe_bool_2(params, 'postOnly', 'post_only', False) + endTime = self.safe_string(params, 'end_time') + stopDirection = self.safe_string(params, 'stop_direction') + if type == 'limit': + if isStop: + if stopDirection is None: + stopDirection = 'STOP_DIRECTION_STOP_DOWN' if (side == 'buy') else 'STOP_DIRECTION_STOP_UP' + if (timeInForce == 'GTD') or (endTime is not None): + if endTime is None: + raise ExchangeError(self.id + ' createOrder() requires an end_time parameter for a GTD order') + request['order_configuration'] = { + 'stop_limit_stop_limit_gtd': { + 'base_size': self.amount_to_precision(symbol, amount), + 'limit_price': self.price_to_precision(symbol, price), + 'stop_price': self.price_to_precision(symbol, triggerPrice), + 'stop_direction': stopDirection, + 'end_time': endTime, + }, + } + else: + request['order_configuration'] = { + 'stop_limit_stop_limit_gtc': { + 'base_size': self.amount_to_precision(symbol, amount), + 'limit_price': self.price_to_precision(symbol, price), + 'stop_price': self.price_to_precision(symbol, triggerPrice), + 'stop_direction': stopDirection, + }, + } + elif isStopLoss or isTakeProfit: + tpslPrice = None + if isStopLoss: + if stopDirection is None: + stopDirection = 'STOP_DIRECTION_STOP_UP' if (side == 'buy') else 'STOP_DIRECTION_STOP_DOWN' + tpslPrice = self.price_to_precision(symbol, stopLossPrice) + else: + if stopDirection is None: + stopDirection = 'STOP_DIRECTION_STOP_DOWN' if (side == 'buy') else 'STOP_DIRECTION_STOP_UP' + tpslPrice = self.price_to_precision(symbol, takeProfitPrice) + request['order_configuration'] = { + 'stop_limit_stop_limit_gtc': { + 'base_size': self.amount_to_precision(symbol, amount), + 'limit_price': self.price_to_precision(symbol, price), + 'stop_price': tpslPrice, + 'stop_direction': stopDirection, + }, + } + else: + if (timeInForce == 'GTD') or (endTime is not None): + if endTime is None: + raise ExchangeError(self.id + ' createOrder() requires an end_time parameter for a GTD order') + request['order_configuration'] = { + 'limit_limit_gtd': { + 'base_size': self.amount_to_precision(symbol, amount), + 'limit_price': self.price_to_precision(symbol, price), + 'end_time': endTime, + 'post_only': postOnly, + }, + } + elif timeInForce == 'IOC': + request['order_configuration'] = { + 'sor_limit_ioc': { + 'base_size': self.amount_to_precision(symbol, amount), + 'limit_price': self.price_to_precision(symbol, price), + }, + } + elif timeInForce == 'FOK': + request['order_configuration'] = { + 'limit_limit_fok': { + 'base_size': self.amount_to_precision(symbol, amount), + 'limit_price': self.price_to_precision(symbol, price), + }, + } + else: + request['order_configuration'] = { + 'limit_limit_gtc': { + 'base_size': self.amount_to_precision(symbol, amount), + 'limit_price': self.price_to_precision(symbol, price), + 'post_only': postOnly, + }, + } + else: + if isStop or isStopLoss or isTakeProfit: + raise NotSupported(self.id + ' createOrder() only stop limit orders are supported') + if market['spot'] and (side == 'buy'): + total = None + createMarketBuyOrderRequiresPrice = True + createMarketBuyOrderRequiresPrice, params = self.handle_option_and_params(params, 'createOrder', 'createMarketBuyOrderRequiresPrice', True) + cost = self.safe_number(params, 'cost') + params = self.omit(params, 'cost') + if cost is not None: + total = self.cost_to_precision(symbol, cost) + elif createMarketBuyOrderRequiresPrice: + if price is None: + raise InvalidOrder(self.id + ' createOrder() requires a price argument for market buy orders on spot markets to calculate the total amount to spend(amount * price), alternatively set the createMarketBuyOrderRequiresPrice option or param to False and pass the cost to spend in the amount argument') + else: + amountString = self.number_to_string(amount) + priceString = self.number_to_string(price) + costRequest = Precise.string_mul(amountString, priceString) + total = self.cost_to_precision(symbol, costRequest) + else: + total = self.cost_to_precision(symbol, amount) + request['order_configuration'] = { + 'market_market_ioc': { + 'quote_size': total, + }, + } + else: + request['order_configuration'] = { + 'market_market_ioc': { + 'base_size': self.amount_to_precision(symbol, amount), + }, + } + marginMode = self.safe_string(params, 'marginMode') + if marginMode is not None: + if marginMode == 'isolated': + request['margin_type'] = 'ISOLATED' + elif marginMode == 'cross': + request['margin_type'] = 'CROSS' + params = self.omit(params, ['timeInForce', 'triggerPrice', 'stopLossPrice', 'takeProfitPrice', 'stopPrice', 'stop_price', 'stopDirection', 'stop_direction', 'clientOrderId', 'postOnly', 'post_only', 'end_time', 'marginMode']) + preview = self.safe_bool_2(params, 'preview', 'test', False) + response = None + if preview: + params = self.omit(params, ['preview', 'test']) + request = self.omit(request, 'client_order_id') + response = self.v3PrivatePostBrokerageOrdersPreview(self.extend(request, params)) + else: + response = self.v3PrivatePostBrokerageOrders(self.extend(request, params)) + # + # successful order + # + # { + # "success": True, + # "failure_reason": "UNKNOWN_FAILURE_REASON", + # "order_id": "52cfe5e2-0b29-4c19-a245-a6a773de5030", + # "success_response": { + # "order_id": "52cfe5e2-0b29-4c19-a245-a6a773de5030", + # "product_id": "LTC-BTC", + # "side": "SELL", + # "client_order_id": "4d760580-6fca-4094-a70b-ebcca8626288" + # }, + # "order_configuration": null + # } + # + # failed order + # + # { + # "success": False, + # "failure_reason": "UNKNOWN_FAILURE_REASON", + # "order_id": "", + # "error_response": { + # "error": "UNSUPPORTED_ORDER_CONFIGURATION", + # "message": "source is not enabled for trading", + # "error_details": "", + # "new_order_failure_reason": "UNSUPPORTED_ORDER_CONFIGURATION" + # }, + # "order_configuration": { + # "limit_limit_gtc": { + # "base_size": "100", + # "limit_price": "40000", + # "post_only": False + # } + # } + # } + # + success = self.safe_bool(response, 'success') + if success is not True: + errorResponse = self.safe_dict(response, 'error_response') + errorTitle = self.safe_string(errorResponse, 'error') + errorMessage = self.safe_string(errorResponse, 'message') + if errorResponse is not None: + self.throw_exactly_matched_exception(self.exceptions['exact'], errorTitle, errorMessage) + self.throw_broadly_matched_exception(self.exceptions['broad'], errorTitle, errorMessage) + raise ExchangeError(errorMessage) + data = self.safe_dict(response, 'success_response', {}) + return self.parse_order(data, market) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder + # + # { + # "order_id": "52cfe5e2-0b29-4c19-a245-a6a773de5030", + # "product_id": "LTC-BTC", + # "side": "SELL", + # "client_order_id": "4d760580-6fca-4094-a70b-ebcca8626288" + # } + # + # cancelOrder, cancelOrders + # + # { + # "success": True, + # "failure_reason": "UNKNOWN_CANCEL_FAILURE_REASON", + # "order_id": "bb8851a3-4fda-4a2c-aa06-9048db0e0f0d" + # } + # + # fetchOrder, fetchOrders, fetchOpenOrders, fetchClosedOrders, fetchCanceledOrders + # + # { + # "order_id": "9bc1eb3b-5b46-4b71-9628-ae2ed0cca75b", + # "product_id": "LTC-BTC", + # "user_id": "1111111-1111-1111-1111-111111111111", + # "order_configuration": { + # "limit_limit_gtc": { + # "base_size": "0.2", + # "limit_price": "0.006", + # "post_only": False + # }, + # "stop_limit_stop_limit_gtc": { + # "base_size": "48.54", + # "limit_price": "6.998", + # "stop_price": "7.0687", + # "stop_direction": "STOP_DIRECTION_STOP_DOWN" + # } + # }, + # "side": "SELL", + # "client_order_id": "e5fe8482-05bb-428f-ad4d-dbc8ce39239c", + # "status": "OPEN", + # "time_in_force": "GOOD_UNTIL_CANCELLED", + # "created_time": "2023-01-16T23:37:23.947030Z", + # "completion_percentage": "0", + # "filled_size": "0", + # "average_filled_price": "0", + # "fee": "", + # "number_of_fills": "0", + # "filled_value": "0", + # "pending_cancel": False, + # "size_in_quote": False, + # "total_fees": "0", + # "size_inclusive_of_fees": False, + # "total_value_after_fees": "0", + # "trigger_status": "INVALID_ORDER_TYPE", + # "order_type": "LIMIT", + # "reject_reason": "REJECT_REASON_UNSPECIFIED", + # "settled": False, + # "product_type": "SPOT", + # "reject_message": "", + # "cancel_message": "" + # } + # + marketId = self.safe_string(order, 'product_id') + symbol = self.safe_symbol(marketId, market, '-') + if symbol is not None: + market = self.safe_market(symbol, market) + orderConfiguration = self.safe_dict(order, 'order_configuration', {}) + limitGTC = self.safe_dict(orderConfiguration, 'limit_limit_gtc') + limitGTD = self.safe_dict(orderConfiguration, 'limit_limit_gtd') + limitIOC = self.safe_dict(orderConfiguration, 'sor_limit_ioc') + stopLimitGTC = self.safe_dict(orderConfiguration, 'stop_limit_stop_limit_gtc') + stopLimitGTD = self.safe_dict(orderConfiguration, 'stop_limit_stop_limit_gtd') + marketIOC = self.safe_dict(orderConfiguration, 'market_market_ioc') + isLimit = ((limitGTC is not None) or (limitGTD is not None) or (limitIOC is not None)) + isStop = ((stopLimitGTC is not None) or (stopLimitGTD is not None)) + price = None + amount = None + postOnly = None + triggerPrice = None + if isLimit: + target = None + if limitGTC is not None: + target = limitGTC + elif limitGTD is not None: + target = limitGTD + else: + target = limitIOC + price = self.safe_string(target, 'limit_price') + amount = self.safe_string(target, 'base_size') + postOnly = self.safe_bool(target, 'post_only') + elif isStop: + stopTarget = stopLimitGTC if (stopLimitGTC is not None) else stopLimitGTD + price = self.safe_string(stopTarget, 'limit_price') + amount = self.safe_string(stopTarget, 'base_size') + postOnly = self.safe_bool(stopTarget, 'post_only') + triggerPrice = self.safe_string(stopTarget, 'stop_price') + else: + amount = self.safe_string(marketIOC, 'base_size') + datetime = self.safe_string(order, 'created_time') + totalFees = self.safe_string(order, 'total_fees') + currencyFee = None + if (totalFees is not None) and (market is not None): + currencyFee = market['quote'] + return self.safe_order({ + 'info': order, + 'id': self.safe_string(order, 'order_id'), + 'clientOrderId': self.safe_string(order, 'client_order_id'), + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + 'lastTradeTimestamp': None, + 'symbol': symbol, + 'type': self.parse_order_type(self.safe_string(order, 'order_type')), + 'timeInForce': self.parse_time_in_force(self.safe_string(order, 'time_in_force')), + 'postOnly': postOnly, + 'side': self.safe_string_lower(order, 'side'), + 'price': price, + 'triggerPrice': triggerPrice, + 'amount': amount, + 'filled': self.safe_string(order, 'filled_size'), + 'remaining': None, + 'cost': None, + 'average': self.safe_string(order, 'average_filled_price'), + 'status': self.parse_order_status(self.safe_string(order, 'status')), + 'fee': { + 'cost': self.safe_string(order, 'total_fees'), + 'currency': currencyFee, + }, + 'trades': None, + }, market) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'OPEN': 'open', + 'FILLED': 'closed', + 'CANCELLED': 'canceled', + 'EXPIRED': 'canceled', + 'FAILED': 'canceled', + 'UNKNOWN_ORDER_STATUS': None, + } + return self.safe_string(statuses, status, status) + + def parse_order_type(self, type: Str): + if type == 'UNKNOWN_ORDER_TYPE': + return None + types: dict = { + 'MARKET': 'market', + 'LIMIT': 'limit', + 'STOP': 'limit', + 'STOP_LIMIT': 'limit', + } + return self.safe_string(types, type, type) + + def parse_time_in_force(self, timeInForce: Str): + timeInForces: dict = { + 'GOOD_UNTIL_CANCELLED': 'GTC', + 'GOOD_UNTIL_DATE_TIME': 'GTD', + 'IMMEDIATE_OR_CANCEL': 'IOC', + 'FILL_OR_KILL': 'FOK', + 'UNKNOWN_TIME_IN_FORCE': None, + } + return self.safe_string(timeInForces, timeInForce, timeInForce) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_cancelorders + + :param str id: order id + :param str symbol: not used by coinbase cancelOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + orders = self.cancel_orders([id], symbol, params) + return self.safe_dict(orders, 0, {}) + + def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): + """ + cancel multiple orders + + https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_cancelorders + + :param str[] ids: order ids + :param str symbol: not used by coinbase cancelOrders() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `order structures ` + """ + self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + request: dict = { + 'order_ids': ids, + } + response = self.v3PrivatePostBrokerageOrdersBatchCancel(self.extend(request, params)) + # + # { + # "results": [ + # { + # "success": True, + # "failure_reason": "UNKNOWN_CANCEL_FAILURE_REASON", + # "order_id": "bb8851a3-4fda-4a2c-aa06-9048db0e0f0d" + # } + # ] + # } + # + orders = self.safe_list(response, 'results', []) + for i in range(0, len(orders)): + success = self.safe_bool(orders[i], 'success') + if success is not True: + raise BadRequest(self.id + ' cancelOrders() has failed, check your arguments and parameters') + return self.parse_orders(orders, market) + + def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + """ + edit a trade order + + https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_editorder + + :param str id: cancel order id + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.preview]: default to False, wether to use the test/preview endpoint or not + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'order_id': id, + } + if amount is not None: + request['size'] = self.amount_to_precision(symbol, amount) + if price is not None: + request['price'] = self.price_to_precision(symbol, price) + preview = self.safe_bool_2(params, 'preview', 'test', False) + response = None + if preview: + params = self.omit(params, ['preview', 'test']) + response = self.v3PrivatePostBrokerageOrdersEditPreview(self.extend(request, params)) + else: + response = self.v3PrivatePostBrokerageOrdersEdit(self.extend(request, params)) + # + # { + # "success": True, + # "errors": { + # "edit_failure_reason": "UNKNOWN_EDIT_ORDER_FAILURE_REASON", + # "preview_failure_reason": "UNKNOWN_PREVIEW_FAILURE_REASON" + # } + # } + # + return self.parse_order(response, market) + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_gethistoricalorder + + :param str id: the order id + :param str symbol: unified market symbol that the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + request: dict = { + 'order_id': id, + } + response = self.v3PrivateGetBrokerageOrdersHistoricalOrderId(self.extend(request, params)) + # + # { + # "order": { + # "order_id": "9bc1eb3b-5b46-4b71-9628-ae2ed0cca75b", + # "product_id": "LTC-BTC", + # "user_id": "1111111-1111-1111-1111-111111111111", + # "order_configuration": { + # "limit_limit_gtc": { + # "base_size": "0.2", + # "limit_price": "0.006", + # "post_only": False + # } + # }, + # "side": "SELL", + # "client_order_id": "e5fe8482-05bb-428f-ad4d-dbc8ce39239c", + # "status": "OPEN", + # "time_in_force": "GOOD_UNTIL_CANCELLED", + # "created_time": "2023-01-16T23:37:23.947030Z", + # "completion_percentage": "0", + # "filled_size": "0", + # "average_filled_price": "0", + # "fee": "", + # "number_of_fills": "0", + # "filled_value": "0", + # "pending_cancel": False, + # "size_in_quote": False, + # "total_fees": "0", + # "size_inclusive_of_fees": False, + # "total_value_after_fees": "0", + # "trigger_status": "INVALID_ORDER_TYPE", + # "order_type": "LIMIT", + # "reject_reason": "REJECT_REASON_UNSPECIFIED", + # "settled": False, + # "product_type": "SPOT", + # "reject_message": "", + # "cancel_message": "" + # } + # } + # + order = self.safe_dict(response, 'order', {}) + return self.parse_order(order, market) + + def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = 100, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_gethistoricalorders + + :param str symbol: unified market symbol that the orders were made in + :param int [since]: the earliest time in ms to fetch orders + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch trades for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOrders', 'paginate') + if paginate: + return self.fetch_paginated_call_cursor('fetchOrders', symbol, since, limit, params, 'cursor', 'cursor', None, 1000) + market = None + if symbol is not None: + market = self.market(symbol) + request: dict = {} + if market is not None: + request['product_id'] = market['id'] + if limit is not None: + request['limit'] = limit + if since is not None: + request['start_date'] = self.iso8601(since) + until = self.safe_integer_n(params, ['until']) + if until is not None: + params = self.omit(params, ['until']) + request['end_date'] = self.iso8601(until) + response = self.v3PrivateGetBrokerageOrdersHistoricalBatch(self.extend(request, params)) + # + # { + # "orders": [ + # { + # "order_id": "813a53c5-3e39-47bb-863d-2faf685d22d8", + # "product_id": "BTC-USDT", + # "user_id": "1111111-1111-1111-1111-111111111111", + # "order_configuration": { + # "market_market_ioc": { + # "quote_size": "6.36" + # } + # }, + # "side": "BUY", + # "client_order_id": "18eb9947-db49-4874-8e7b-39b8fe5f4317", + # "status": "FILLED", + # "time_in_force": "IMMEDIATE_OR_CANCEL", + # "created_time": "2023-01-18T01:37:37.975552Z", + # "completion_percentage": "100", + # "filled_size": "0.000297920684505", + # "average_filled_price": "21220.6399999973697697", + # "fee": "", + # "number_of_fills": "2", + # "filled_value": "6.3220675944333996", + # "pending_cancel": False, + # "size_in_quote": True, + # "total_fees": "0.0379324055666004", + # "size_inclusive_of_fees": True, + # "total_value_after_fees": "6.36", + # "trigger_status": "INVALID_ORDER_TYPE", + # "order_type": "MARKET", + # "reject_reason": "REJECT_REASON_UNSPECIFIED", + # "settled": True, + # "product_type": "SPOT", + # "reject_message": "", + # "cancel_message": "Internal error" + # }, + # ], + # "sequence": "0", + # "has_next": False, + # "cursor": "" + # } + # + orders = self.safe_list(response, 'orders', []) + first = self.safe_dict(orders, 0) + cursor = self.safe_string(response, 'cursor') + if (cursor is not None) and (cursor != ''): + first['cursor'] = cursor + orders[0] = first + return self.parse_orders(orders, market, since, limit) + + def fetch_orders_by_status(self, status, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + request: dict = { + 'order_status': status, + } + if market is not None: + request['product_id'] = market['id'] + if limit is None: + limit = 100 + request['limit'] = limit + if since is not None: + request['start_date'] = self.iso8601(since) + until = self.safe_integer_n(params, ['until']) + if until is not None: + params = self.omit(params, ['until']) + request['end_date'] = self.iso8601(until) + response = self.v3PrivateGetBrokerageOrdersHistoricalBatch(self.extend(request, params)) + # + # { + # "orders": [ + # { + # "order_id": "813a53c5-3e39-47bb-863d-2faf685d22d8", + # "product_id": "BTC-USDT", + # "user_id": "1111111-1111-1111-1111-111111111111", + # "order_configuration": { + # "market_market_ioc": { + # "quote_size": "6.36" + # } + # }, + # "side": "BUY", + # "client_order_id": "18eb9947-db49-4874-8e7b-39b8fe5f4317", + # "status": "FILLED", + # "time_in_force": "IMMEDIATE_OR_CANCEL", + # "created_time": "2023-01-18T01:37:37.975552Z", + # "completion_percentage": "100", + # "filled_size": "0.000297920684505", + # "average_filled_price": "21220.6399999973697697", + # "fee": "", + # "number_of_fills": "2", + # "filled_value": "6.3220675944333996", + # "pending_cancel": False, + # "size_in_quote": True, + # "total_fees": "0.0379324055666004", + # "size_inclusive_of_fees": True, + # "total_value_after_fees": "6.36", + # "trigger_status": "INVALID_ORDER_TYPE", + # "order_type": "MARKET", + # "reject_reason": "REJECT_REASON_UNSPECIFIED", + # "settled": True, + # "product_type": "SPOT", + # "reject_message": "", + # "cancel_message": "Internal error" + # }, + # ], + # "sequence": "0", + # "has_next": False, + # "cursor": "" + # } + # + orders = self.safe_list(response, 'orders', []) + first = self.safe_dict(orders, 0) + cursor = self.safe_string(response, 'cursor') + if (cursor is not None) and (cursor != ''): + first['cursor'] = cursor + orders[0] = first + return self.parse_orders(orders, market, since, limit) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on all currently open orders + + https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_gethistoricalorders + + :param str symbol: unified market symbol of the orders + :param int [since]: timestamp in ms of the earliest order, default is None + :param int [limit]: the maximum number of open order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param int [params.until]: the latest time in ms to fetch trades for + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOpenOrders', 'paginate') + if paginate: + return self.fetch_paginated_call_cursor('fetchOpenOrders', symbol, since, limit, params, 'cursor', 'cursor', None, 100) + return self.fetch_orders_by_status('OPEN', symbol, since, limit, params) + + def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_gethistoricalorders + + :param str symbol: unified market symbol of the orders + :param int [since]: timestamp in ms of the earliest order, default is None + :param int [limit]: the maximum number of closed order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param int [params.until]: the latest time in ms to fetch trades for + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchClosedOrders', 'paginate') + if paginate: + return self.fetch_paginated_call_cursor('fetchClosedOrders', symbol, since, limit, params, 'cursor', 'cursor', None, 100) + return self.fetch_orders_by_status('FILLED', symbol, since, limit, params) + + def fetch_canceled_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches information on multiple canceled orders made by the user + + https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_gethistoricalorders + + :param str symbol: unified market symbol of the orders + :param int [since]: timestamp in ms of the earliest order, default is None + :param int [limit]: the maximum number of canceled order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `order structures ` + """ + return self.fetch_orders_by_status('CANCELLED', symbol, since, limit, params) + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_getpubliccandles + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch, not used by coinbase + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch trades for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param boolean [params.usePrivate]: default False, when True will use the private endpoint to fetch the candles + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + maxLimit = 300 + limit = maxLimit if (limit is None) else min(limit, maxLimit) + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate', False) + if paginate: + return self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params, maxLimit - 1) + market = self.market(symbol) + request: dict = { + 'product_id': market['id'], + 'granularity': self.safe_string(self.timeframes, timeframe, timeframe), + } + until = self.safe_integer_n(params, ['until', 'end']) + params = self.omit(params, ['until']) + duration = self.parse_timeframe(timeframe) + requestedDuration = limit * duration + sinceString = None + if since is not None: + sinceString = self.number_to_string(self.parse_to_int(since / 1000)) + else: + now = str(self.seconds()) + sinceString = Precise.string_sub(now, str(requestedDuration)) + request['start'] = sinceString + if until is not None: + request['end'] = self.number_to_string(self.parse_to_int(until / 1000)) + else: + # 300 candles max + request['end'] = Precise.string_add(sinceString, str(requestedDuration)) + response = None + usePrivate = False + usePrivate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'usePrivate', False) + if usePrivate: + response = self.v3PrivateGetBrokerageProductsProductIdCandles(self.extend(request, params)) + else: + response = self.v3PublicGetBrokerageMarketProductsProductIdCandles(self.extend(request, params)) + # + # { + # "candles": [ + # { + # "start": "1673391780", + # "low": "17414.36", + # "high": "17417.99", + # "open": "17417.74", + # "close": "17417.38", + # "volume": "1.87780853" + # }, + # ] + # } + # + candles = self.safe_list(response, 'candles', []) + return self.parse_ohlcvs(candles, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # [ + # { + # "start": "1673391780", + # "low": "17414.36", + # "high": "17417.99", + # "open": "17417.74", + # "close": "17417.38", + # "volume": "1.87780853" + # }, + # ] + # + return [ + self.safe_timestamp(ohlcv, 'start'), + self.safe_number(ohlcv, 'open'), + self.safe_number(ohlcv, 'high'), + self.safe_number(ohlcv, 'low'), + self.safe_number(ohlcv, 'close'), + self.safe_number(ohlcv, 'volume'), + ] + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_getpublicmarkettrades + + :param str symbol: unified market symbol of the trades + :param int [since]: not used by coinbase fetchTrades + :param int [limit]: the maximum number of trade structures to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.usePrivate]: default False, when True will use the private endpoint to fetch the trades + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'product_id': market['id'], + } + if since is not None: + request['start'] = self.number_to_string(self.parse_to_int(since / 1000)) + if limit is not None: + request['limit'] = min(limit, 1000) + until = None + until, params = self.handle_option_and_params(params, 'fetchTrades', 'until') + if until is not None: + request['end'] = self.number_to_string(self.parse_to_int(until / 1000)) + elif since is not None: + raise ArgumentsRequired(self.id + ' fetchTrades() requires a `until` parameter when you use `since` argument') + response = None + usePrivate = False + usePrivate, params = self.handle_option_and_params(params, 'fetchTrades', 'usePrivate', False) + if usePrivate: + response = self.v3PrivateGetBrokerageProductsProductIdTicker(self.extend(request, params)) + else: + response = self.v3PublicGetBrokerageMarketProductsProductIdTicker(self.extend(request, params)) + # + # { + # "trades": [ + # { + # "trade_id": "10092327", + # "product_id": "BTC-USDT", + # "price": "17488.12", + # "size": "0.0000623", + # "time": "2023-01-11T00:52:37.557001Z", + # "side": "BUY", + # "bid": "", + # "ask": "" + # }, + # ] + # } + # + trades = self.safe_list(response, 'trades', []) + return self.parse_trades(trades, market, since, limit) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_getfills + + :param str symbol: unified market symbol of the trades + :param int [since]: timestamp in ms of the earliest order, default is None + :param int [limit]: the maximum number of trade structures to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch trades for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchMyTrades', 'paginate') + if paginate: + return self.fetch_paginated_call_cursor('fetchMyTrades', symbol, since, limit, params, 'cursor', 'cursor', None, 250) + market = None + if symbol is not None: + market = self.market(symbol) + request: dict = {} + if market is not None: + request['product_id'] = market['id'] + if limit is not None: + request['limit'] = limit + if since is not None: + request['start_sequence_timestamp'] = self.iso8601(since) + until = self.safe_integer_n(params, ['until']) + if until is not None: + params = self.omit(params, ['until']) + request['end_sequence_timestamp'] = self.iso8601(until) + response = self.v3PrivateGetBrokerageOrdersHistoricalFills(self.extend(request, params)) + # + # { + # "fills": [ + # { + # "entry_id": "b88b82cc89e326a2778874795102cbafd08dd979a2a7a3c69603fc4c23c2e010", + # "trade_id": "cdc39e45-bbd3-44ec-bf02-61742dfb16a1", + # "order_id": "813a53c5-3e39-47bb-863d-2faf685d22d8", + # "trade_time": "2023-01-18T01:37:38.091377090Z", + # "trade_type": "FILL", + # "price": "21220.64", + # "size": "0.0046830664333996", + # "commission": "0.0000280983986004", + # "product_id": "BTC-USDT", + # "sequence_timestamp": "2023-01-18T01:37:38.092520Z", + # "liquidity_indicator": "UNKNOWN_LIQUIDITY_INDICATOR", + # "size_in_quote": True, + # "user_id": "1111111-1111-1111-1111-111111111111", + # "side": "BUY" + # }, + # ], + # "cursor": "" + # } + # + trades = self.safe_list(response, 'fills', []) + first = self.safe_dict(trades, 0) + cursor = self.safe_string(response, 'cursor') + if (cursor is not None) and (cursor != ''): + first['cursor'] = cursor + trades[0] = first + return self.parse_trades(trades, market, since, limit) + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_getpublicproductbook + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.usePrivate]: default False, when True will use the private endpoint to fetch the order book + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'product_id': market['id'], + } + if limit is not None: + request['limit'] = limit + response = None + usePrivate = False + usePrivate, params = self.handle_option_and_params(params, 'fetchOrderBook', 'usePrivate', False) + if usePrivate: + response = self.v3PrivateGetBrokerageProductBook(self.extend(request, params)) + else: + response = self.v3PublicGetBrokerageMarketProductBook(self.extend(request, params)) + # + # { + # "pricebook": { + # "product_id": "BTC-USDT", + # "bids": [ + # { + # "price": "30757.85", + # "size": "0.115" + # }, + # ], + # "asks": [ + # { + # "price": "30759.07", + # "size": "0.04877659" + # }, + # ], + # "time": "2023-06-30T04:02:40.533606Z" + # } + # } + # + data = self.safe_dict(response, 'pricebook', {}) + time = self.safe_string(data, 'time') + timestamp = self.parse8601(time) + return self.parse_order_book(data, symbol, timestamp, 'bids', 'asks', 'price', 'size') + + def fetch_bids_asks(self, symbols: Strings = None, params={}): + """ + fetches the bid and ask price and volume for multiple markets + + https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_getbestbidask + + :param str[] [symbols]: unified symbols of the markets to fetch the bids and asks for, all markets are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + symbols = self.market_symbols(symbols) + request: dict = {} + if symbols is not None: + request['product_ids'] = self.market_ids(symbols) + response = self.v3PrivateGetBrokerageBestBidAsk(self.extend(request, params)) + # + # { + # "pricebooks": [ + # { + # "product_id": "TRAC-EUR", + # "bids": [ + # { + # "price": "0.2384", + # "size": "386.1" + # } + # ], + # "asks": [ + # { + # "price": "0.2406", + # "size": "672" + # } + # ], + # "time": "2023-06-30T07:15:24.656044Z" + # }, + # ] + # } + # + tickers = self.safe_list(response, 'pricebooks', []) + return self.parse_tickers(tickers, symbols) + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/api-transactions#send-money + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str [tag]: an optional tag for the withdrawal + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.check_address(address) + self.load_markets() + currency = self.currency(code) + accountId = self.safe_string_2(params, 'account_id', 'accountId') + params = self.omit(params, ['account_id', 'accountId']) + if accountId is None: + if code is None: + raise ArgumentsRequired(self.id + ' withdraw() requires an account_id(or accountId) parameter OR a currency code argument') + accountId = self.find_account_id(code, params) + if accountId is None: + raise ExchangeError(self.id + ' withdraw() could not find account id for ' + code) + request: dict = { + 'account_id': accountId, + 'type': 'send', + 'to': address, + 'amount': amount, + 'currency': currency['id'], + } + if tag is not None: + request['destination_tag'] = tag + response = self.v2PrivatePostAccountsAccountIdTransactions(self.extend(request, params)) + # + # { + # "data": { + # "id": "a1794ecf-5693-55fa-70cf-ef731748ed82", + # "type": "send", + # "status": "pending", + # "amount": { + # "amount": "-14.008308", + # "currency": "USDC" + # }, + # "native_amount": { + # "amount": "-18.74", + # "currency": "CAD" + # }, + # "description": null, + # "created_at": "2024-01-12T01:27:31Z", + # "updated_at": "2024-01-12T01:27:31Z", + # "resource": "transaction", + # "resource_path": "/v2/accounts/a34bgfad-ed67-538b-bffc-730c98c10da0/transactions/a1794ecf-5693-55fa-70cf-ef731748ed82", + # "instant_exchange": False, + # "network": { + # "status": "pending", + # "status_description": "Pending(est. less than 10 minutes)", + # "transaction_fee": { + # "amount": "4.008308", + # "currency": "USDC" + # }, + # "transaction_amount": { + # "amount": "10.000000", + # "currency": "USDC" + # }, + # "confirmations": 0 + # }, + # "to": { + # "resource": "ethereum_address", + # "address": "0x9...", + # "currency": "USDC", + # "address_info": { + # "address": "0x9..." + # } + # }, + # "idem": "748d8591-dg9a-7831-a45b-crd61dg78762", + # "details": { + # "title": "Sent USDC", + # "subtitle": "To USDC address on Ethereum network", + # "header": "Sent 14.008308 USDC($18.74)", + # "health": "warning" + # }, + # "hide_native_amount": False + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_transaction(data, currency) + + def fetch_deposit_addresses_by_network(self, code: str, params={}) -> List[DepositAddress]: + """ + fetch the deposit address for a currency associated with self account + + https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_postcoinbaseaccountaddresses + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + self.load_markets() + currency = self.currency(code) + request = None + request, params = self.prepare_account_request_with_currency_code(currency['code'], None, params) + response = self.v2PrivateGetAccountsAccountIdAddresses(self.extend(request, params)) + # + # { + # pagination: { + # ending_before: null, + # starting_after: null, + # previous_ending_before: null, + # next_starting_after: null, + # limit: '25', + # order: 'desc', + # previous_uri: null, + # next_uri: null + # }, + # data: [ + # { + # id: '64ceb5f1-5fa2-5310-a4ff-9fd46271003d', + # address: '5xjPKeAXpnhA2kHyinvdVeui6RXVdEa3B2J3SCAwiKnk', + # address_info: {address: '5xjPKeAXpnhA2kHyinvdVeui6RXVdEa3B2J3SCAwiKnk'}, + # name: null, + # created_at: '2023-05-29T21:12:12Z', + # updated_at: '2023-05-29T21:12:12Z', + # network: 'solana', + # uri_scheme: 'solana', + # resource: 'address', + # resource_path: '/v2/accounts/a7b3d387-bfb8-5ce7-b8da-1f507e81cf25/addresses/64ceb5f1-5fa2-5310-a4ff-9fd46271003d', + # warnings: [ + # { + # type: 'correct_address_warning', + # title: 'This is an ERC20 USDC address.', + # details: 'Only send ERC20 USD Coin(USDC) to self address.', + # image_url: 'https://www.coinbase.com/assets/addresses/global-receive-warning-a3d91807e61c717e5a38d270965003dcc025ca8a3cea40ec3d7835b7c86087fa.png', + # options: [{text: 'I understand', style: 'primary', id: 'dismiss'}] + # } + # ], + # qr_code_image_url: 'https://static-assets.coinbase.com/p2p/l2/asset_network_combinations/v5/usdc-solana.png', + # address_label: 'USDC address(Solana)', + # default_receive: True, + # deposit_uri: 'solana:5xjPKeAXpnhA2kHyinvdVeui6RXVdEa3B2J3SCAwiKnk?spl-token=EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', + # callback_url: null, + # share_address_copy: { + # line1: '5xjPKeAXpnhA2kHyinvdVeui6RXVdEa3B2J3SCAwiKnk', + # line2: 'This address can only receive USDC-SPL from Solana network. Don’t send USDC from other networks, other SPL tokens or NFTs, or it may result in a loss of funds.' + # }, + # receive_subtitle: 'ERC-20', + # inline_warning: { + # text: 'This address can only receive USDC-SPL from Solana network. Don’t send USDC from other networks, other SPL tokens or NFTs, or it may result in a loss of funds.', + # tooltip: { + # title: 'USDC(Solana)', + # subtitle: 'This address can only receive USDC-SPL from Solana network.' + # } + # } + # }, + # ... + # ] + # } + # + data = self.safe_list(response, 'data', []) + addressStructures = self.parse_deposit_addresses(data, None, False) + return self.index_by(addressStructures, 'network') + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + # + # { + # id: '64ceb5f1-5fa2-5310-a4ff-9fd46271003d', + # address: '5xjPKeAXpnhA2kHyinvdVeui6RXVdEa3B2J3SCAwiKnk', + # address_info: { + # address: 'GCF74576I7AQ56SLMKBQAP255EGUOWCRVII3S44KEXVNJEOIFVBDMXVL', + # destination_tag: '3722061866' + # }, + # name: null, + # created_at: '2023-05-29T21:12:12Z', + # updated_at: '2023-05-29T21:12:12Z', + # network: 'solana', + # uri_scheme: 'solana', + # resource: 'address', + # resource_path: '/v2/accounts/a7b3d387-bfb8-5ce7-b8da-1f507e81cf25/addresses/64ceb5f1-5fa2-5310-a4ff-9fd46271003d', + # warnings: [ + # { + # type: 'correct_address_warning', + # title: 'This is an ERC20 USDC address.', + # details: 'Only send ERC20 USD Coin(USDC) to self address.', + # image_url: 'https://www.coinbase.com/assets/addresses/global-receive-warning-a3d91807e61c717e5a38d270965003dcc025ca8a3cea40ec3d7835b7c86087fa.png', + # options: [{text: 'I understand', style: 'primary', id: 'dismiss'}] + # } + # ], + # qr_code_image_url: 'https://static-assets.coinbase.com/p2p/l2/asset_network_combinations/v5/usdc-solana.png', + # address_label: 'USDC address(Solana)', + # default_receive: True, + # deposit_uri: 'solana:5xjPKeAXpnhA2kHyinvdVeui6RXVdEa3B2J3SCAwiKnk?spl-token=EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', + # callback_url: null, + # share_address_copy: { + # line1: '5xjPKeAXpnhA2kHyinvdVeui6RXVdEa3B2J3SCAwiKnk', + # line2: 'This address can only receive USDC-SPL from Solana network. Don’t send USDC from other networks, other SPL tokens or NFTs, or it may result in a loss of funds.' + # }, + # receive_subtitle: 'ERC-20', + # inline_warning: { + # text: 'This address can only receive USDC-SPL from Solana network. Don’t send USDC from other networks, other SPL tokens or NFTs, or it may result in a loss of funds.', + # tooltip: { + # title: 'USDC(Solana)', + # subtitle: 'This address can only receive USDC-SPL from Solana network.' + # } + # } + # } + # + address = self.safe_string(depositAddress, 'address') + self.check_address(address) + networkId = self.safe_string(depositAddress, 'network') + code = self.safe_currency_code(None, currency) + addressLabel = self.safe_string(depositAddress, 'address_label') + currencyId = None + if addressLabel is not None: + splitAddressLabel = addressLabel.split(' ') + currencyId = self.safe_string(splitAddressLabel, 0) + addressInfo = self.safe_dict(depositAddress, 'address_info') + return { + 'info': depositAddress, + 'currency': self.safe_currency_code(currencyId, currency), + 'network': self.network_id_to_code(networkId, code), + 'address': address, + 'tag': self.safe_string(addressInfo, 'destination_tag'), + } + + def deposit(self, code: str, amount: float, id: str, params={}): + """ + make a deposit + + https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/api-deposits#deposit-funds + + :param str code: unified currency code + :param float amount: the amount to deposit + :param str id: the payment method id to be used for the deposit, can be retrieved from v2PrivateGetPaymentMethods + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.accountId]: the id of the account to deposit into + :returns dict: a `transaction structure ` + """ + self.load_markets() + accountId = self.safe_string_2(params, 'account_id', 'accountId') + params = self.omit(params, ['account_id', 'accountId']) + if accountId is None: + if code is None: + raise ArgumentsRequired(self.id + ' deposit() requires an account_id(or accountId) parameter OR a currency code argument') + accountId = self.find_account_id(code, params) + if accountId is None: + raise ExchangeError(self.id + ' deposit() could not find account id for ' + code) + request: dict = { + 'account_id': accountId, + 'amount': self.number_to_string(amount), + 'currency': code.upper(), # need to use code in case depositing USD etc. + 'payment_method': id, + 'commit': True, # otheriwse the deposit does not go through + } + response = self.v2PrivatePostAccountsAccountIdDeposits(self.extend(request, params)) + # + # { + # "data": { + # "id": "67e0eaec-07d7-54c4-a72c-2e92826897df", + # "status": "created", + # "payment_method": { + # "id": "83562370-3e5c-51db-87da-752af5ab9559", + # "resource": "payment_method", + # "resource_path": "/v2/payment-methods/83562370-3e5c-51db-87da-752af5ab9559" + # }, + # "transaction": { + # "id": "441b9494-b3f0-5b98-b9b0-4d82c21c252a", + # "resource": "transaction", + # "resource_path": "/v2/accounts/2bbf394c-193b-5b2a-9155-3b4732659ede/transactions/441b9494-b3f0-5b98-b9b0-4d82c21c252a" + # }, + # "amount": { + # "amount": "10.00", + # "currency": "USD" + # }, + # "subtotal": { + # "amount": "10.00", + # "currency": "USD" + # }, + # "created_at": "2015-01-31T20:49:02Z", + # "updated_at": "2015-02-11T16:54:02-08:00", + # "resource": "deposit", + # "resource_path": "/v2/accounts/2bbf394c-193b-5b2a-9155-3b4732659ede/deposits/67e0eaec-07d7-54c4-a72c-2e92826897df", + # "committed": True, + # "fee": { + # "amount": "0.00", + # "currency": "USD" + # }, + # "payout_at": "2015-02-18T16:54:00-08:00" + # } + # } + # + # https://github.com/ccxt/ccxt/issues/25484 + data = self.safe_dict_2(response, 'data', 'transfer', {}) + return self.parse_transaction(data) + + def fetch_deposit(self, id: str, code: Str = None, params={}): + """ + fetch information on a deposit, fiat only, for crypto transactions use fetchLedger + + https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/api-deposits#show-deposit + + :param str id: deposit id + :param str [code]: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.accountId]: the id of the account that the funds were deposited into + :returns dict: a `transaction structure ` + """ + self.load_markets() + accountId = self.safe_string_2(params, 'account_id', 'accountId') + params = self.omit(params, ['account_id', 'accountId']) + if accountId is None: + if code is None: + raise ArgumentsRequired(self.id + ' fetchDeposit() requires an account_id(or accountId) parameter OR a currency code argument') + accountId = self.find_account_id(code, params) + if accountId is None: + raise ExchangeError(self.id + ' fetchDeposit() could not find account id for ' + code) + request: dict = { + 'account_id': accountId, + 'deposit_id': id, + } + response = self.v2PrivateGetAccountsAccountIdDepositsDepositId(self.extend(request, params)) + # + # { + # "data": { + # "id": "67e0eaec-07d7-54c4-a72c-2e92826897df", + # "status": "completed", + # "payment_method": { + # "id": "83562370-3e5c-51db-87da-752af5ab9559", + # "resource": "payment_method", + # "resource_path": "/v2/payment-methods/83562370-3e5c-51db-87da-752af5ab9559" + # }, + # "transaction": { + # "id": "441b9494-b3f0-5b98-b9b0-4d82c21c252a", + # "resource": "transaction", + # "resource_path": "/v2/accounts/2bbf394c-193b-5b2a-9155-3b4732659ede/transactions/441b9494-b3f0-5b98-b9b0-4d82c21c252a" + # }, + # "amount": { + # "amount": "10.00", + # "currency": "USD" + # }, + # "subtotal": { + # "amount": "10.00", + # "currency": "USD" + # }, + # "created_at": "2015-01-31T20:49:02Z", + # "updated_at": "2015-02-11T16:54:02-08:00", + # "resource": "deposit", + # "resource_path": "/v2/accounts/2bbf394c-193b-5b2a-9155-3b4732659ede/deposits/67e0eaec-07d7-54c4-a72c-2e92826897df", + # "committed": True, + # "fee": { + # "amount": "0.00", + # "currency": "USD" + # }, + # "payout_at": "2015-02-18T16:54:00-08:00" + # } + # } + # + # https://github.com/ccxt/ccxt/issues/25484 + data = self.safe_dict_2(response, 'data', 'transfer', {}) + return self.parse_transaction(data) + + def fetch_deposit_method_ids(self, params={}): + """ + fetch the deposit id for a fiat currency associated with self account + + https://docs.cdp.coinbase.com/advanced-trade/reference/retailbrokerageapi_getpaymentmethods + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an array of `deposit id structures ` + """ + self.load_markets() + response = self.v3PrivateGetBrokeragePaymentMethods(params) + # + # { + # "payment_methods": [ + # { + # "id": "21b39a5d-f7b46876fb2e", + # "type": "COINBASE_FIAT_ACCOUNT", + # "name": "CAD Wallet", + # "currency": "CAD", + # "verified": True, + # "allow_buy": False, + # "allow_sell": True, + # "allow_deposit": False, + # "allow_withdraw": False, + # "created_at": "2023-06-29T19:58:46Z", + # "updated_at": "2023-10-30T20:25:01Z" + # } + # ] + # } + # + result = self.safe_list(response, 'payment_methods', []) + return self.parse_deposit_method_ids(result) + + def fetch_deposit_method_id(self, id: str, params={}): + """ + fetch the deposit id for a fiat currency associated with self account + + https://docs.cdp.coinbase.com/advanced-trade/reference/retailbrokerageapi_getpaymentmethod + + :param str id: the deposit payment method id + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `deposit id structure ` + """ + self.load_markets() + request: dict = { + 'payment_method_id': id, + } + response = self.v3PrivateGetBrokeragePaymentMethodsPaymentMethodId(self.extend(request, params)) + # + # { + # "payment_method": { + # "id": "21b39a5d-f7b46876fb2e", + # "type": "COINBASE_FIAT_ACCOUNT", + # "name": "CAD Wallet", + # "currency": "CAD", + # "verified": True, + # "allow_buy": False, + # "allow_sell": True, + # "allow_deposit": False, + # "allow_withdraw": False, + # "created_at": "2023-06-29T19:58:46Z", + # "updated_at": "2023-10-30T20:25:01Z" + # } + # } + # + result = self.safe_dict(response, 'payment_method', {}) + return self.parse_deposit_method_id(result) + + def parse_deposit_method_ids(self, ids, params={}): + result = [] + for i in range(0, len(ids)): + id = self.extend(self.parse_deposit_method_id(ids[i]), params) + result.append(id) + return result + + def parse_deposit_method_id(self, depositId): + return { + 'info': depositId, + 'id': self.safe_string(depositId, 'id'), + 'currency': self.safe_string(depositId, 'currency'), + 'verified': self.safe_bool(depositId, 'verified'), + 'tag': self.safe_string(depositId, 'name'), + } + + def fetch_convert_quote(self, fromCode: str, toCode: str, amount: Num = None, params={}) -> Conversion: + """ + fetch a quote for converting from one currency to another + + https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_createconvertquote + + :param str fromCode: the currency that you want to sell and convert from + :param str toCode: the currency that you want to buy and convert into + :param float [amount]: how much you want to trade in units of the from currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :param dict [params.trade_incentive_metadata]: an object to fill in user incentive data + :param str [params.trade_incentive_metadata.user_incentive_id]: the id of the incentive + :param str [params.trade_incentive_metadata.code_val]: the code value of the incentive + :returns dict: a `conversion structure ` + """ + self.load_markets() + request: dict = { + 'from_account': fromCode, + 'to_account': toCode, + 'amount': self.number_to_string(amount), + } + response = self.v3PrivatePostBrokerageConvertQuote(self.extend(request, params)) + data = self.safe_dict(response, 'trade', {}) + return self.parse_conversion(data) + + def create_convert_trade(self, id: str, fromCode: str, toCode: str, amount: Num = None, params={}) -> Conversion: + """ + convert from one currency to another + + https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_commitconverttrade + + :param str id: the id of the trade that you want to make + :param str fromCode: the currency that you want to sell and convert from + :param str toCode: the currency that you want to buy and convert into + :param float [amount]: how much you want to trade in units of the from currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `conversion structure ` + """ + self.load_markets() + request: dict = { + 'trade_id': id, + 'from_account': fromCode, + 'to_account': toCode, + } + response = self.v3PrivatePostBrokerageConvertTradeTradeId(self.extend(request, params)) + data = self.safe_dict(response, 'trade', {}) + return self.parse_conversion(data) + + def fetch_convert_trade(self, id: str, code: Str = None, params={}) -> Conversion: + """ + fetch the data for a conversion trade + + https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_getconverttrade + + :param str id: the id of the trade that you want to commit + :param str code: the unified currency code that was converted from + :param dict [params]: extra parameters specific to the exchange API endpoint + :param strng params['toCode']: the unified currency code that was converted into + :returns dict: a `conversion structure ` + """ + self.load_markets() + if code is None: + raise ArgumentsRequired(self.id + ' fetchConvertTrade() requires a code argument') + toCode = self.safe_string(params, 'toCode') + if toCode is None: + raise ArgumentsRequired(self.id + ' fetchConvertTrade() requires a toCode parameter') + params = self.omit(params, 'toCode') + request: dict = { + 'trade_id': id, + 'from_account': code, + 'to_account': toCode, + } + response = self.v3PrivateGetBrokerageConvertTradeTradeId(self.extend(request, params)) + data = self.safe_dict(response, 'trade', {}) + return self.parse_conversion(data) + + def parse_conversion(self, conversion: dict, fromCurrency: Currency = None, toCurrency: Currency = None) -> Conversion: + fromCoin = self.safe_string(conversion, 'source_currency') + fromCode = self.safe_currency_code(fromCoin, fromCurrency) + to = self.safe_string(conversion, 'target_currency') + toCode = self.safe_currency_code(to, toCurrency) + fromAmountStructure = self.safe_dict(conversion, 'user_entered_amount') + feeStructure = self.safe_dict(conversion, 'total_fee') + feeAmountStructure = self.safe_dict(feeStructure, 'amount') + return { + 'info': conversion, + 'timestamp': None, + 'datetime': None, + 'id': self.safe_string(conversion, 'id'), + 'fromCurrency': fromCode, + 'fromAmount': self.safe_number(fromAmountStructure, 'value'), + 'toCurrency': toCode, + 'toAmount': None, + 'price': None, + 'fee': self.safe_number(feeAmountStructure, 'value'), + } + + def close_position(self, symbol: str, side: OrderSide = None, params={}) -> Order: + """ + *futures only* closes open positions for a market + + https://docs.cdp.coinbase.com/coinbase-app/trade/reference/retailbrokerageapi_closeposition + + :param str symbol: Unified CCXT market symbol + :param str [side]: not used by coinbase + :param dict [params]: extra parameters specific to the coinbase api endpoint + @param {str} params.clientOrderId *mandatory* the client order id of the position to close + :param float [params.size]: the size of the position to close, optional + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + clientOrderId = self.safe_string_2(params, 'client_order_id', 'clientOrderId') + params = self.omit(params, 'clientOrderId') + request: dict = { + 'product_id': market['id'], + } + if clientOrderId is None: + raise ArgumentsRequired(self.id + ' closePosition() requires a clientOrderId parameter') + request['client_order_id'] = clientOrderId + response = self.v3PrivatePostBrokerageOrdersClosePosition(self.extend(request, params)) + order = self.safe_dict(response, 'success_response', {}) + return self.parse_order(order) + + def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_getfcmpositions + https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_getintxpositions + + :param str[] [symbols]: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.portfolio]: the portfolio UUID to fetch positions for + :returns dict[]: a list of `position structure ` + """ + self.load_markets() + symbols = self.market_symbols(symbols) + market = None + if symbols is not None: + market = self.market(symbols[0]) + type = None + type, params = self.handle_market_type_and_params('fetchPositions', market, params) + response = None + if type == 'future': + response = self.v3PrivateGetBrokerageCfmPositions(params) + else: + portfolio = None + portfolio, params = self.handle_option_and_params(params, 'fetchPositions', 'portfolio') + if portfolio is None: + raise ArgumentsRequired(self.id + ' fetchPositions() requires a "portfolio" value in params(eg: dbcb91e7-2bc9-515), or set.options["portfolio"]. You can get a list of portfolios with fetchPortfolios()') + request: dict = { + 'portfolio_uuid': portfolio, + } + response = self.v3PrivateGetBrokerageIntxPositionsPortfolioUuid(self.extend(request, params)) + positions = self.safe_list(response, 'positions', []) + return self.parse_positions(positions, symbols) + + def fetch_position(self, symbol: str, params={}): + """ + fetch data on a single open contract trade position + + https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_getintxposition + https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_getfcmposition + + :param str symbol: unified market symbol of the market the position is held in, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.product_id]: *futures only* the product id of the position to fetch, required for futures markets only + :param str [params.portfolio]: *perpetual/swaps only* the portfolio UUID to fetch the position for, required for perpetual/swaps markets only + :returns dict: a `position structure ` + """ + self.load_markets() + market = self.market(symbol) + response = None + if market['future']: + productId = self.safe_string(market, 'product_id') + if productId is None: + raise ArgumentsRequired(self.id + ' fetchPosition() requires a "product_id" in params') + futureRequest: dict = { + 'product_id': productId, + } + response = self.v3PrivateGetBrokerageCfmPositionsProductId(self.extend(futureRequest, params)) + else: + portfolio = None + portfolio, params = self.handle_option_and_params(params, 'fetchPositions', 'portfolio') + if portfolio is None: + raise ArgumentsRequired(self.id + ' fetchPosition() requires a "portfolio" value in params(eg: dbcb91e7-2bc9-515), or set.options["portfolio"]. You can get a list of portfolios with fetchPortfolios()') + request: dict = { + 'symbol': market['id'], + 'portfolio_uuid': portfolio, + } + response = self.v3PrivateGetBrokerageIntxPositionsPortfolioUuidSymbol(self.extend(request, params)) + position = self.safe_dict(response, 'position', {}) + return self.parse_position(position, market) + + def parse_position(self, position: dict, market: Market = None): + # + # { + # "product_id": "1r4njf84-0-0", + # "product_uuid": "cd34c18b-3665-4ed8-9305-3db277c49fc5", + # "symbol": "ADA-PERP-INTX", + # "vwap": { + # "value": "0.6171", + # "currency": "USDC" + # }, + # "position_side": "POSITION_SIDE_LONG", + # "net_size": "20", + # "buy_order_size": "0", + # "sell_order_size": "0", + # "im_contribution": "0.1", + # "unrealized_pnl": { + # "value": "0.074", + # "currency": "USDC" + # }, + # "mark_price": { + # "value": "0.6208", + # "currency": "USDC" + # }, + # "liquidation_price": { + # "value": "0", + # "currency": "USDC" + # }, + # "leverage": "1", + # "im_notional": { + # "value": "12.342", + # "currency": "USDC" + # }, + # "mm_notional": { + # "value": "0.814572", + # "currency": "USDC" + # }, + # "position_notional": { + # "value": "12.342", + # "currency": "USDC" + # }, + # "margin_type": "MARGIN_TYPE_CROSS", + # "liquidation_buffer": "19.677828", + # "liquidation_percentage": "4689.3506", + # "portfolio_summary": { + # "portfolio_uuid": "018ebd63-1f6d-7c8e-ada9-0761c5a2235f", + # "collateral": "20.4184", + # "position_notional": "12.342", + # "open_position_notional": "12.342", + # "pending_fees": "0", + # "borrow": "0", + # "accrued_interest": "0", + # "rolling_debt": "0", + # "portfolio_initial_margin": "0.1", + # "portfolio_im_notional": { + # "value": "12.342", + # "currency": "USDC" + # }, + # "portfolio_maintenance_margin": "0.066", + # "portfolio_mm_notional": { + # "value": "0.814572", + # "currency": "USDC" + # }, + # "liquidation_percentage": "4689.3506", + # "liquidation_buffer": "19.677828", + # "margin_type": "MARGIN_TYPE_CROSS", + # "margin_flags": "PORTFOLIO_MARGIN_FLAGS_UNSPECIFIED", + # "liquidation_status": "PORTFOLIO_LIQUIDATION_STATUS_NOT_LIQUIDATING", + # "unrealized_pnl": { + # "value": "0.074", + # "currency": "USDC" + # }, + # "buying_power": { + # "value": "8.1504", + # "currency": "USDC" + # }, + # "total_balance": { + # "value": "20.4924", + # "currency": "USDC" + # }, + # "max_withdrawal": { + # "value": "8.0764", + # "currency": "USDC" + # } + # }, + # "entry_vwap": { + # "value": "0.6091", + # "currency": "USDC" + # } + # } + # + marketId = self.safe_string(position, 'symbol', '') + market = self.safe_market(marketId, market) + rawMargin = self.safe_string(position, 'margin_type') + marginMode = None + if rawMargin is not None: + marginMode = 'cross' if (rawMargin == 'MARGIN_TYPE_CROSS') else 'isolated' + notionalObject = self.safe_dict(position, 'position_notional', {}) + positionSide = self.safe_string(position, 'position_side') + side = 'long' if (positionSide == 'POSITION_SIDE_LONG') else 'short' + unrealizedPNLObject = self.safe_dict(position, 'unrealized_pnl', {}) + liquidationPriceObject = self.safe_dict(position, 'liquidation_price', {}) + liquidationPrice = self.safe_number(liquidationPriceObject, 'value') + vwapObject = self.safe_dict(position, 'vwap', {}) + summaryObject = self.safe_dict(position, 'portfolio_summary', {}) + return self.safe_position({ + 'info': position, + 'id': self.safe_string(position, 'product_id'), + 'symbol': self.safe_symbol(marketId, market), + 'notional': self.safe_number(notionalObject, 'value'), + 'marginMode': marginMode, + 'liquidationPrice': liquidationPrice, + 'entryPrice': self.safe_number(vwapObject, 'value'), + 'unrealizedPnl': self.safe_number(unrealizedPNLObject, 'value'), + 'realizedPnl': None, + 'percentage': None, + 'contracts': self.safe_number(position, 'net_size'), + 'contractSize': market['contractSize'], + 'markPrice': None, + 'lastPrice': None, + 'side': side, + 'hedged': None, + 'timestamp': None, + 'datetime': None, + 'lastUpdateTimestamp': None, + 'maintenanceMargin': None, + 'maintenanceMarginPercentage': None, + 'collateral': self.safe_number(summaryObject, 'collateral'), + 'initialMargin': None, + 'initialMarginPercentage': None, + 'leverage': self.safe_number(position, 'leverage'), + 'marginRatio': None, + 'stopLossPrice': None, + 'takeProfitPrice': None, + }) + + def fetch_trading_fees(self, params={}) -> TradingFees: + """ + + https://docs.cdp.coinbase.com/advanced-trade/reference/retailbrokerageapi_gettransactionsummary/ + + fetch the trading fees for multiple markets + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: 'spot' or 'swap' + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + self.load_markets() + type = None + type, params = self.handle_market_type_and_params('fetchTradingFees', None, params) + isSpot = (type == 'spot') + productType = 'SPOT' if isSpot else 'FUTURE' + request: dict = { + 'product_type': productType, + } + response = self.v3PrivateGetBrokerageTransactionSummary(self.extend(request, params)) + # + # { + # total_volume: '0', + # total_fees: '0', + # fee_tier: { + # pricing_tier: 'Advanced 1', + # usd_from: '0', + # usd_to: '1000', + # taker_fee_rate: '0.008', + # maker_fee_rate: '0.006', + # aop_from: '', + # aop_to: '' + # }, + # margin_rate: null, + # goods_and_services_tax: null, + # advanced_trade_only_volume: '0', + # advanced_trade_only_fees: '0', + # coinbase_pro_volume: '0', + # coinbase_pro_fees: '0', + # total_balance: '', + # has_promo_fee: False + # } + # + data = self.safe_dict(response, 'fee_tier', {}) + taker_fee = self.safe_number(data, 'taker_fee_rate') + marker_fee = self.safe_number(data, 'maker_fee_rate') + result: dict = {} + for i in range(0, len(self.symbols)): + symbol = self.symbols[i] + market = self.market(symbol) + if (isSpot and market['spot']) or (not isSpot and not market['spot']): + result[symbol] = { + 'info': response, + 'symbol': symbol, + 'maker': taker_fee, + 'taker': marker_fee, + 'percentage': True, + } + return result + + def fetch_portfolio_details(self, portfolioUuid: str, params={}) -> List[Any]: + """ + Fetch details for a specific portfolio by UUID + + https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_getportfolios + + :param str portfolioUuid: The unique identifier of the portfolio to fetch + :param Dict [params]: Extra parameters specific to the exchange API endpoint + :returns any[]: An account structure + """ + self.load_markets() + request = { + 'portfolio_uuid': portfolioUuid, + } + response = self.v3PrivateGetBrokeragePortfoliosPortfolioUuid(self.extend(request, params)) + result = self.parse_portfolio_details(response) + return result + + def parse_portfolio_details(self, portfolioData: dict): + breakdown = portfolioData['breakdown'] + portfolioInfo = self.safe_dict(breakdown, 'portfolio', {}) + portfolioName = self.safe_string(portfolioInfo, 'name', 'Unknown') + portfolioUuid = self.safe_string(portfolioInfo, 'uuid', '') + spotPositions = self.safe_list(breakdown, 'spot_positions', []) + parsedPositions = [] + for i in range(0, len(spotPositions)): + position: dict = spotPositions[i] + currencyCode = self.safe_string(position, 'asset', 'Unknown') + availableBalanceStr = self.safe_string(position, 'available_to_trade_fiat', '0') + availableBalance = self.parse_number(availableBalanceStr) + totalBalanceFiatStr = self.safe_string(position, 'total_balance_fiat', '0') + totalBalanceFiat = self.parse_number(totalBalanceFiatStr) + holdAmount = totalBalanceFiat - availableBalance + costBasisDict = self.safe_dict(position, 'cost_basis', {}) + costBasisStr = self.safe_string(costBasisDict, 'value', '0') + averageEntryPriceDict = self.safe_dict(position, 'average_entry_price', {}) + averageEntryPriceStr = self.safe_string(averageEntryPriceDict, 'value', '0') + positionData: dict = { + 'currency': currencyCode, + 'available_balance': availableBalance, + 'hold_amount': holdAmount > holdAmount if 0 else 0, + 'wallet_name': portfolioName, + 'account_id': portfolioUuid, + 'account_uuid': self.safe_string(position, 'account_uuid', ''), + 'total_balance_fiat': totalBalanceFiat, + 'total_balance_crypto': self.parse_number(self.safe_string(position, 'total_balance_crypto', '0')), + 'available_to_trade_fiat': self.parse_number(self.safe_string(position, 'available_to_trade_fiat', '0')), + 'available_to_trade_crypto': self.parse_number(self.safe_string(position, 'available_to_trade_crypto', '0')), + 'available_to_transfer_fiat': self.parse_number(self.safe_string(position, 'available_to_transfer_fiat', '0')), + 'available_to_transfer_crypto': self.parse_number(self.safe_string(position, 'available_to_trade_crypto', '0')), + 'allocation': self.parse_number(self.safe_string(position, 'allocation', '0')), + 'cost_basis': self.parse_number(costBasisStr), + 'cost_basis_currency': self.safe_string(costBasisDict, 'currency', 'USD'), + 'is_cash': self.safe_bool(position, 'is_cash', False), + 'average_entry_price': self.parse_number(averageEntryPriceStr), + 'average_entry_price_currency': self.safe_string(averageEntryPriceDict, 'currency', 'USD'), + 'asset_uuid': self.safe_string(position, 'asset_uuid', ''), + 'unrealized_pnl': self.parse_number(self.safe_string(position, 'unrealized_pnl', '0')), + 'asset_color': self.safe_string(position, 'asset_color', ''), + 'account_type': self.safe_string(position, 'account_type', ''), + } + parsedPositions.append(positionData) + return parsedPositions + + def create_auth_token(self, seconds: Int, method: Str = None, url: Str = None, useEddsa=False): + # v1 https://docs.cdp.coinbase.com/api-reference/authentication#php-2 + # v2 https://docs.cdp.coinbase.com/api-reference/v2/authentication + uri = None + if url is not None: + uri = method + ' ' + url.replace('https://', '') + quesPos = uri.find('?') + # Due to we use mb_strpos, quesPos could be False in php. In that case, the quesPos >= 0 is True + # Also it's not possible that the question mark is first character, only check > 0 here. + if quesPos > 0: + uri = uri[0:quesPos] + # self.eddsa{"sub":"d2efa49a-369c-43d7-a60e-ae26e28853c2","iss":"cdp","aud":["cdp_service"],"uris":["GET api.coinbase.com/api/v3/brokerage/transaction_summary"]} + nonce = self.random_bytes(16) + aud = 'cdp_service' if useEddsa else 'retail_rest_api_proxy' + iss = 'cdp' if useEddsa else 'coinbase-cloud' + request: dict = { + 'aud': [aud], + 'iss': iss, + 'nbf': seconds, + 'exp': seconds + 120, + 'sub': self.apiKey, + 'iat': seconds, + } + if uri is not None: + if not useEddsa: + request['uri'] = uri + else: + request['uris'] = [uri] + if useEddsa: + byteArray = self.base64_to_binary(self.secret) + seed = self.array_slice(byteArray, 0, 32) + return self.jwt(request, seed, 'sha256', False, {'kid': self.apiKey, 'nonce': nonce, 'alg': 'EdDSA'}) + else: + # self.ecdsawith p256 + return self.jwt(request, self.encode(self.secret), 'sha256', False, {'kid': self.apiKey, 'nonce': nonce, 'alg': 'ES256'}) + + def nonce(self): + return self.milliseconds() - self.options['timeDifference'] + + def sign(self, path, api=[], method='GET', params={}, headers=None, body=None): + version = api[0] + signed = api[1] == 'private' + isV3 = version == 'v3' + pathPart = 'api/v3' if (isV3) else 'v2' + fullPath = '/' + pathPart + '/' + self.implode_params(path, params) + query = self.omit(params, self.extract_params(path)) + savedPath = fullPath + if method == 'GET': + if query: + fullPath += '?' + self.urlencode_with_array_repeat(query) + url = self.urls['api']['rest'] + fullPath + if signed: + authorization = self.safe_string(self.headers, 'Authorization') + authorizationString = None + if authorization is not None: + authorizationString = authorization + elif self.token and not self.check_required_credentials(False): + authorizationString = 'Bearer ' + self.token + else: + self.check_required_credentials() + seconds = self.seconds() + payload = '' + if method != 'GET': + if query: + body = self.json(query) + payload = body + else: + if not isV3: + if query: + payload += '?' + self.urlencode(query) + # v3: 'GET' doesn't need payload in the signature. inside url is enough + # https://docs.cloud.coinbase.com/advanced-trade/docs/auth#example-request + # v2: 'GET' require payload in the signature + # https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/api-key-authentication + isCloudAPiKey = (self.apiKey.find('organizations/') >= 0) or (self.secret.startswith('-----BEGIN')) + # using the size might be fragile, so we add an option to force v2 cloud api key if needed + isV2CloudAPiKey = len(self.secret) == 88 or self.safe_bool(self.options, 'v2CloudAPiKey', False) or self.secret.endswith('=') + if isCloudAPiKey or isV2CloudAPiKey: + if isCloudAPiKey and self.apiKey.startswith('-----BEGIN'): + raise ArgumentsRequired(self.id + ' apiKey should contain the name(eg: organizations/3b910e93....) and not the public key') + # # it may not work for v2 + # uri = method + ' ' + url.replace('https://', '') + # quesPos = uri.find('?') + # # Due to we use mb_strpos, quesPos could be False in php. In that case, the quesPos >= 0 is True + # # Also it's not possible that the question mark is first character, only check > 0 here. + # if quesPos > 0: + # uri = uri[0:quesPos] + # } + # nonce = self.random_bytes(16) + # request: Dict = { + # 'aud': ['retail_rest_api_proxy'], + # 'iss': 'coinbase-cloud', + # 'nbf': seconds, + # 'exp': seconds + 120, + # 'sub': self.apiKey, + # 'uri': uri, + # 'iat': seconds, + # } + token = self.create_auth_token(seconds, method, url, isV2CloudAPiKey) + # token = self.jwt(request, self.encode(self.secret), 'sha256', False, {'kid': self.apiKey, 'nonce': nonce, 'alg': 'ES256'}) + authorizationString = 'Bearer ' + token + else: + nonce = self.nonce() + timestamp = self.parse_to_int(nonce / 1000) + timestampString = str(timestamp) + auth = timestampString + method + savedPath + payload + signature = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256) + headers = { + 'CB-ACCESS-KEY': self.apiKey, + 'CB-ACCESS-SIGN': signature, + 'CB-ACCESS-TIMESTAMP': timestampString, + 'Content-Type': 'application/json', + } + if authorizationString is not None: + headers = { + 'Authorization': authorizationString, + 'Content-Type': 'application/json', + } + if method != 'GET': + if query: + body = self.json(query) + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None # fallback to default error handler + feedback = self.id + ' ' + body + # + # {"error": "invalid_request", "error_description": "The request is missing a required parameter, includes an unsupported parameter value, or is otherwise malformed."} + # + # or + # + # { + # "errors": [ + # { + # "id": "not_found", + # "message": "Not found" + # } + # ] + # } + # or + # { + # "success": False, + # "error_response": { + # "error": "UNKNOWN_FAILURE_REASON", + # "message": "", + # "error_details": "", + # "preview_failure_reason": "PREVIEW_STOP_PRICE_ABOVE_LAST_TRADE_PRICE" + # }, + # "order_configuration": { + # "stop_limit_stop_limit_gtc": { + # "base_size": "0.0001", + # "limit_price": "2000", + # "stop_price": "2005", + # "stop_direction": "STOP_DIRECTION_STOP_DOWN", + # "reduce_only": False + # } + # } + # } + # + errorCode = self.safe_string(response, 'error') + if errorCode is not None: + errorMessage = self.safe_string_2(response, 'error_description', 'error') + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], errorMessage, feedback) + raise ExchangeError(feedback) + errorResponse = self.safe_dict(response, 'error_response') + if errorResponse is not None: + errorMessageInner = self.safe_string_2(errorResponse, 'preview_failure_reason', 'preview_failure_reason') + self.throw_exactly_matched_exception(self.exceptions['exact'], errorMessageInner, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], errorMessageInner, feedback) + raise ExchangeError(feedback) + errors = self.safe_list(response, 'errors') + if errors is not None: + if isinstance(errors, list): + numErrors = len(errors) + if numErrors > 0: + errorCode = self.safe_string(errors[0], 'id') + errorMessage = self.safe_string(errors[0], 'message') + if errorCode is not None: + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], errorMessage, feedback) + raise ExchangeError(feedback) + advancedTrade = self.options['advanced'] + if not ('data' in response) and (not advancedTrade): + raise ExchangeError(self.id + ' failed due to a malformed response ' + self.json(response)) + return None diff --git a/ccxt/coinbaseadvanced.py b/ccxt/coinbaseadvanced.py new file mode 100644 index 0000000..dae2b9f --- /dev/null +++ b/ccxt/coinbaseadvanced.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.coinbase import coinbase +from ccxt.abstract.coinbaseadvanced import ImplicitAPI +from ccxt.base.types import Any + + +class coinbaseadvanced(coinbase, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(coinbaseadvanced, self).describe(), { + 'id': 'coinbaseadvanced', + 'name': 'Coinbase Advanced', + 'alias': True, + }) diff --git a/ccxt/coinbaseexchange.py b/ccxt/coinbaseexchange.py new file mode 100644 index 0000000..2d14497 --- /dev/null +++ b/ccxt/coinbaseexchange.py @@ -0,0 +1,2048 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.coinbaseexchange import ImplicitAPI +import hashlib +from ccxt.base.types import Account, Any, Balances, Currencies, Currency, DepositAddress, Int, LedgerEntry, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, TradingFees, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidAddress +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import OnMaintenance +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class coinbaseexchange(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(coinbaseexchange, self).describe(), { + 'id': 'coinbaseexchange', + 'name': 'Coinbase Exchange', + 'countries': ['US'], + 'rateLimit': 100, + 'userAgent': self.userAgents['chrome'], + 'pro': True, + 'has': { + 'CORS': True, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createDepositAddress': True, + 'createOrder': True, + 'createOrderWithTakeProfitAndStopLoss': False, + 'createOrderWithTakeProfitAndStopLossWs': False, + 'createPostOnlyOrder': False, + 'createReduceOnlyOrder': False, + 'createStopLimitOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'fetchAccounts': True, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': False, # the exchange does not have self method, only createDepositAddress, see https://github.com/ccxt/ccxt/pull/7405 + 'fetchDeposits': True, + 'fetchDepositsWithdrawals': True, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLedger': True, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchOrderTrades': True, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': True, + 'fetchTransactions': 'emulated', + 'fetchVolatilityHistory': False, + 'fetchWithdrawals': True, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'repayMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'withdraw': True, + }, + 'timeframes': { + '1m': 60, + '5m': 300, + '15m': 900, + '1h': 3600, + '6h': 21600, + '1d': 86400, + }, + 'hostname': 'exchange.coinbase.com', + 'urls': { + 'test': { + 'public': 'https://api-public.sandbox.exchange.coinbase.com', + 'private': 'https://api-public.sandbox.exchange.coinbase.com', + }, + 'logo': 'https://github.com/ccxt/ccxt/assets/43336371/34a65553-88aa-4a38-a714-064bd228b97e', + 'api': { + 'public': 'https://api.{hostname}', + 'private': 'https://api.{hostname}', + }, + 'www': 'https://coinbase.com/', + 'doc': 'https://docs.cloud.coinbase.com/exchange/docs/', + 'fees': [ + 'https://docs.pro.coinbase.com/#fees', + 'https://support.pro.coinbase.com/customer/en/portal/articles/2945310-fees', + ], + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + 'password': True, + }, + 'api': { + 'public': { + 'get': [ + 'currencies', + 'products', + 'products/{id}', + 'products/{id}/book', + 'products/{id}/candles', + 'products/{id}/stats', + 'products/{id}/ticker', + 'products/{id}/trades', + 'time', + 'products/spark-lines', # experimental, + 'products/volume-summary', + ], + }, + 'private': { + 'get': [ + 'address-book', + 'accounts', + 'accounts/{id}', + 'accounts/{id}/holds', + 'accounts/{id}/ledger', + 'accounts/{id}/transfers', + 'coinbase-accounts', + 'fills', + 'funding', + 'fees', + 'margin/profile_information', + 'margin/buying_power', + 'margin/withdrawal_power', + 'margin/withdrawal_power_all', + 'margin/exit_plan', + 'margin/liquidation_history', + 'margin/position_refresh_amounts', + 'margin/status', + 'oracle', + 'orders', + 'orders/{id}', + 'orders/client:{client_oid}', + 'otc/orders', + 'payment-methods', + 'position', + 'profiles', + 'profiles/{id}', + 'reports/{report_id}', + 'transfers', + 'transfers/{transfer_id}', + 'users/self/exchange-limits', + 'users/self/hold-balances', + 'users/self/trailing-volume', + 'withdrawals/fee-estimate', + 'conversions/{conversion_id}', + 'conversions', + 'conversions/fees', + 'loans/lending-overview', + 'loans/lending-overview-xm', + 'loans/loan-preview', + 'loans/loan-preview-xm', + 'loans/repayment-preview', + 'loans/repayment-preview-xm', + 'loans/interest/{loan_id}', + 'loans/interest/history/{loan_id}', + 'loans/interest', + 'loans/assets', + 'loans', + ], + 'post': [ + 'conversions', + 'deposits/coinbase-account', + 'deposits/payment-method', + 'coinbase-accounts/{id}/addresses', + 'funding/repay', + 'orders', + 'position/close', + 'profiles/margin-transfer', + 'profiles/transfer', + 'reports', + 'withdrawals/coinbase', + 'withdrawals/coinbase-account', + 'withdrawals/crypto', + 'withdrawals/payment-method', + 'loans/open', + 'loans/repay-interest', + 'loans/repay-principal', + ], + 'delete': [ + 'orders', + 'orders/client:{client_oid}', + 'orders/{id}', + ], + }, + }, + 'commonCurrencies': { + 'CGLD': 'CELO', + }, + 'precisionMode': TICK_SIZE, + 'fees': { + 'trading': { + 'tierBased': True, # complicated tier system per coin + 'percentage': True, + 'maker': self.parse_number('0.004'), # highest fee of all tiers + 'taker': self.parse_number('0.006'), # highest fee of all tiers + }, + 'funding': { + 'tierBased': False, + 'percentage': False, + 'withdraw': { + 'BCH': 0, + 'BTC': 0, + 'LTC': 0, + 'ETH': 0, + 'EUR': 0.15, + 'USD': 25, + }, + 'deposit': { + 'BCH': 0, + 'BTC': 0, + 'LTC': 0, + 'ETH': 0, + 'EUR': 0.15, + 'USD': 10, + }, + }, + }, + 'features': { + 'default': { + 'sandbox': True, + 'createOrder': { + 'marginMode': True, + 'triggerPrice': True, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': True, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': False, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': False, + 'iceberg': True, # todo: implement + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 100000, + 'untilDays': 100000, + 'symbolRequired': True, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 100, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 100000, + 'untilDays': 100000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 100000, + 'daysBackCanceled': 1, + 'untilDays': 100000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 300, + }, + }, + 'spot': { + 'extends': 'default', + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'options': { + 'networks': { + 'BTC': 'bitcoin', + # LIGHTNING unsupported + 'ETH': 'ethereum', + # TRON unsupported + 'SOL': 'solana', + # BSC unsupported + 'ARBONE': 'arbitrum', + 'AVAXC': 'avacchain', + 'MATIC': 'polygon', + 'BASE': 'base', + 'SUI': 'sui', + 'OP': 'optimism', + 'NEAR': 'near', + # CRONOS unsupported + # GNO unsupported + 'APT': 'aptos', + # SCROLL unsupported + 'KAVA': 'kava', + # TAIKO unsupported + # BOB unsupported + # LINEA unsupported + 'BLAST': 'blast', + 'XLM': 'stellar', + # RSK unsupported + 'SEI': 'sei', + # TON unsupported + # MANTLE unsupported + 'ADA': 'cardano', + # HYPE unsupported + 'CORE': 'coredao', + 'ALGO': 'algorand', + # RUNE unsupported + 'OSMO': 'osmosis', + # XIN unsupported + 'CELO': 'celo', + 'HBAR': 'hedera', + # FTM unsupported + # WEMIX unsupported + 'ZKSYNC': 'zksync', + # KLAY unsupported + # HT unsupported + # FSN unsupported + # EOS unsupported, eosio? + # ACA unsupported + 'STX': 'stacks', + 'XTZ': 'tezos', + # NEO unsupported + # METIS unsupported + # TLOS unsupported + 'EGLD': 'elrond', + # ASTR unsupported + # CFX unsupported + # GLMR unsupported + # CANTO unsupported + # SCRT unsupported + 'LTC': 'litecoin', + # AURORA unsupported + # ONG unsupported + 'ATOM': 'cosmos', + # CHZ unsupported + 'FIL': 'filecoin', + 'DOT': 'polkadot', + 'DOGE': 'dogecoin', + # BRC20 unsupported + 'XRP': 'ripple', + # XMR unsupported + 'DASH': 'dash', + # akash, aleo, axelar, bitcoincash, berachain, deso, ethereumclassic, unichain, flow, flare, dfinity, story,kusama, mina, ronin, oasis, bittensor, celestia, noble, vara, vechain, zcash, horizen, zetachain + }, + }, + 'exceptions': { + 'exact': { + 'Insufficient funds': InsufficientFunds, + 'NotFound': OrderNotFound, + 'Invalid API Key': AuthenticationError, + 'invalid signature': AuthenticationError, + 'Invalid Passphrase': AuthenticationError, + 'Invalid order id': InvalidOrder, + 'Private rate limit exceeded': RateLimitExceeded, + 'Trading pair not available': PermissionDenied, + 'Product not found': InvalidOrder, + }, + 'broad': { + 'Order already done': OrderNotFound, + 'order not found': OrderNotFound, + 'price too small': InvalidOrder, + 'price too precise': InvalidOrder, + 'under maintenance': OnMaintenance, + 'size is too small': InvalidOrder, + 'Cancel only mode': OnMaintenance, # https://github.com/ccxt/ccxt/issues/7690 + }, + }, + }) + + def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getcurrencies + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = self.publicGetCurrencies(params) + # + # { + # "id": "USDT", + # "name": "Tether", + # "min_size": "0.000001", + # "status": "online", + # "message": "", + # "max_precision": "0.000001", + # "convertible_to": [], + # "details": { + # "type": "crypto", + # "symbol": null, + # "network_confirmations": 14, + # "sort_order": 0, + # "crypto_address_link": "https://etherscan.io/token/0xdac17f958d2ee523a2206206994597c13d831ec7?a={{address}}", + # "crypto_transaction_link": "https://etherscan.io/tx/0x{{txId}}", + # "push_payment_methods": [], + # "group_types": [], + # "display_name": null, + # "processing_time_seconds": null, + # "min_withdrawal_amount": 0.000001, + # "max_withdrawal_amount": 20000000 + # }, + # "default_network": "ethereum", + # "supported_networks": [ + # { + # "id": "ethereum", + # "name": "Ethereum", + # "status": "online", + # "contract_address": "0xdac17f958d2ee523a2206206994597c13d831ec7", + # "crypto_address_link": "https://etherscan.io/token/0xdac17f958d2ee523a2206206994597c13d831ec7?a={{address}}", + # "crypto_transaction_link": "https://etherscan.io/tx/0x{{txId}}", + # "min_withdrawal_amount": 0.000001, + # "max_withdrawal_amount": 20000000, + # "network_confirmations": 14, + # "processing_time_seconds": null + # } + # ], + # "display_name": "USDT" + # } + # + result: dict = {} + for i in range(0, len(response)): + currency = response[i] + id = self.safe_string(currency, 'id') + name = self.safe_string(currency, 'name') + code = self.safe_currency_code(id) + details = self.safe_dict(currency, 'details', {}) + networks: dict = {} + supportedNetworks = self.safe_list(currency, 'supported_networks', []) + for j in range(0, len(supportedNetworks)): + network = supportedNetworks[j] + networkId = self.safe_string(network, 'id') + networkCode = self.network_id_to_code(networkId) + networks[networkCode] = { + 'id': networkId, + 'name': self.safe_string(network, 'name'), + 'network': networkCode, + 'active': self.safe_string(network, 'status') == 'online', + 'withdraw': None, + 'deposit': None, + 'fee': None, + 'precision': None, + 'limits': { + 'withdraw': { + 'min': self.safe_number(network, 'min_withdrawal_amount'), + 'max': self.safe_number(network, 'max_withdrawal_amount'), + }, + }, + 'contract': self.safe_string(network, 'contract_address'), + 'info': network, + } + result[code] = self.safe_currency_structure({ + 'id': id, + 'code': code, + 'info': currency, + 'type': self.safe_string(details, 'type'), + 'name': name, + 'active': self.safe_string(currency, 'status') == 'online', + 'deposit': None, + 'withdraw': None, + 'fee': None, + 'precision': self.safe_number(currency, 'max_precision'), + 'limits': { + 'amount': { + 'min': self.safe_number(details, 'min_size'), + 'max': None, + }, + 'withdraw': { + 'min': self.safe_number(details, 'min_withdrawal_amount'), + 'max': self.safe_number(details, 'max_withdrawal_amount'), + }, + }, + 'networks': networks, + }) + return result + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for coinbaseexchange + + https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getproducts + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = self.publicGetProducts(params) + # + # [ + # { + # "id": "BTCAUCTION-USD", + # "base_currency": "BTC", + # "quote_currency": "USD", + # "base_min_size": "0.000016", + # "base_max_size": "1500", + # "quote_increment": "0.01", + # "base_increment": "0.00000001", + # "display_name": "BTCAUCTION/USD", + # "min_market_funds": "1", + # "max_market_funds": "20000000", + # "margin_enabled": False, + # "fx_stablecoin": False, + # "max_slippage_percentage": "0.02000000", + # "post_only": False, + # "limit_only": False, + # "cancel_only": True, + # "trading_disabled": False, + # "status": "online", + # "status_message": '', + # "auction_mode": False + # }, + # { + # "id": "BTC-USD", + # "base_currency": "BTC", + # "quote_currency": "USD", + # "base_min_size": "0.000016", + # "base_max_size": "1500", + # "quote_increment": "0.01", + # "base_increment": "0.00000001", + # "display_name": "BTC/USD", + # "min_market_funds": "1", + # "max_market_funds": "20000000", + # "margin_enabled": False, + # "fx_stablecoin": False, + # "max_slippage_percentage": "0.02000000", + # "post_only": False, + # "limit_only": False, + # "cancel_only": False, + # "trading_disabled": False, + # "status": "online", + # "status_message": '', + # "auction_mode": False + # } + # ] + # + result = [] + for i in range(0, len(response)): + market = response[i] + id = self.safe_string(market, 'id') + baseId, quoteId = id.split('-') + # BTCAUCTION-USD vs BTC-USD conflict workaround, see the output sample above + # baseId = self.safe_string(market, 'base_currency') + # quoteId = self.safe_string(market, 'quote_currency') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + status = self.safe_string(market, 'status') + result.append(self.extend(self.fees['trading'], { + 'id': id, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': self.safe_value(market, 'margin_enabled'), + 'swap': False, + 'future': False, + 'option': False, + 'active': (status == 'online'), + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.safe_number(market, 'base_increment'), + 'price': self.safe_number(market, 'quote_increment'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': None, + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': self.safe_number(market, 'min_market_funds'), + 'max': None, + }, + }, + 'created': None, + 'info': market, + })) + return result + + def fetch_accounts(self, params={}) -> List[Account]: + """ + fetch all the accounts associated with a profile + + https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getaccounts + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `account structures ` indexed by the account type + """ + self.load_markets() + response = self.privateGetAccounts(params) + # + # [ + # { + # "id": "4aac9c60-cbda-4396-9da4-4aa71e95fba0", + # "currency": "BTC", + # "balance": "0.0000000000000000", + # "available": "0", + # "hold": "0.0000000000000000", + # "profile_id": "b709263e-f42a-4c7d-949a-a95c83d065da" + # }, + # { + # "id": "f75fa69a-1ad1-4a80-bd61-ee7faa6135a3", + # "currency": "USDC", + # "balance": "0.0000000000000000", + # "available": "0", + # "hold": "0.0000000000000000", + # "profile_id": "b709263e-f42a-4c7d-949a-a95c83d065da" + # }, + # ] + # + return self.parse_accounts(response, params) + + def parse_account(self, account): + # + # { + # "id": "4aac9c60-cbda-4396-9da4-4aa71e95fba0", + # "currency": "BTC", + # "balance": "0.0000000000000000", + # "available": "0", + # "hold": "0.0000000000000000", + # "profile_id": "b709263e-f42a-4c7d-949a-a95c83d065da" + # } + # + currencyId = self.safe_string(account, 'currency') + return { + 'id': self.safe_string(account, 'id'), + 'type': None, + 'code': self.safe_currency_code(currencyId), + 'info': account, + } + + def parse_balance(self, response) -> Balances: + result: dict = {'info': response} + for i in range(0, len(response)): + balance = response[i] + currencyId = self.safe_string(balance, 'currency') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(balance, 'available') + account['used'] = self.safe_string(balance, 'hold') + account['total'] = self.safe_string(balance, 'balance') + result[code] = account + return self.safe_balance(result) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getaccounts + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + response = self.privateGetAccounts(params) + return self.parse_balance(response) + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + + https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getproductbook + + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + # level 1 - only the best bid and ask + # level 2 - top 50 bids and asks(aggregated) + # level 3 - full order book(non aggregated) + request: dict = { + 'id': self.market_id(symbol), + 'level': 2, # 1 best bidask, 2 aggregated, 3 full + } + response = self.publicGetProductsIdBook(self.extend(request, params)) + # + # { + # "sequence":1924393896, + # "bids":[ + # ["0.01825","24.34811287",2], + # ["0.01824","72.5463",3], + # ["0.01823","424.54298049",6], + # ], + # "asks":[ + # ["0.01826","171.10414904",4], + # ["0.01827","22.60427028",1], + # ["0.01828","397.46018784",7], + # ] + # } + # + orderbook = self.parse_order_book(response, symbol) + orderbook['nonce'] = self.safe_integer(response, 'sequence') + return orderbook + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # fetchTickers + # + # [ + # 1639472400, # timestamp + # 4.26, # low + # 4.38, # high + # 4.35, # open + # 4.27 # close + # ] + # + # fetchTicker + # + # publicGetProductsIdTicker + # + # { + # "trade_id":843439, + # "price":"0.997999", + # "size":"80.29769", + # "time":"2020-01-28T02:13:33.012523Z", + # "bid":"0.997094", + # "ask":"0.998", + # "volume":"1903188.03750000" + # } + # + # publicGetProductsIdStats + # + # { + # "open": "34.19000000", + # "high": "95.70000000", + # "low": "7.06000000", + # "volume": "2.41000000" + # } + # + timestamp = None + bid = None + ask = None + last = None + high = None + low = None + open = None + volume = None + symbol = None if (market is None) else market['symbol'] + if isinstance(ticker, list): + last = self.safe_string(ticker, 4) + timestamp = self.milliseconds() + else: + timestamp = self.parse8601(self.safe_value(ticker, 'time')) + bid = self.safe_string(ticker, 'bid') + ask = self.safe_string(ticker, 'ask') + high = self.safe_string(ticker, 'high') + low = self.safe_string(ticker, 'low') + open = self.safe_string(ticker, 'open') + last = self.safe_string_2(ticker, 'price', 'last') + volume = self.safe_string(ticker, 'volume') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': high, + 'low': low, + 'bid': bid, + 'bidVolume': None, + 'ask': ask, + 'askVolume': None, + 'vwap': None, + 'open': open, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': volume, + 'quoteVolume': None, + 'info': ticker, + }, market) + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getproduct + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + symbols = self.market_symbols(symbols) + request: dict = {} + response = self.publicGetProductsSparkLines(self.extend(request, params)) + # + # { + # YYY-USD: [ + # [ + # 1639472400, # timestamp + # 4.26, # low + # 4.38, # high + # 4.35, # open + # 4.27 # close + # ], + # [ + # 1639468800, + # 4.31, + # 4.45, + # 4.35, + # 4.35 + # ], + # ] + # } + # + result: dict = {} + marketIds = list(response.keys()) + delimiter = '-' + for i in range(0, len(marketIds)): + marketId = marketIds[i] + entry = self.safe_value(response, marketId, []) + first = self.safe_value(entry, 0, []) + market = self.safe_market(marketId, None, delimiter) + symbol = market['symbol'] + result[symbol] = self.parse_ticker(first, market) + return self.filter_by_array_tickers(result, 'symbol', symbols) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + + https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getproductticker + + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'id': market['id'], + } + # publicGetProductsIdTicker or publicGetProductsIdStats + method = self.safe_string(self.options, 'fetchTickerMethod', 'publicGetProductsIdTicker') + response = getattr(self, method)(self.extend(request, params)) + # + # publicGetProductsIdTicker + # + # { + # "trade_id":843439, + # "price":"0.997999", + # "size":"80.29769", + # "time":"2020-01-28T02:13:33.012523Z", + # "bid":"0.997094", + # "ask":"0.998", + # "volume":"1903188.03750000" + # } + # + # publicGetProductsIdStats + # + # { + # "open": "34.19000000", + # "high": "95.70000000", + # "low": "7.06000000", + # "volume": "2.41000000" + # } + # + return self.parse_ticker(response, market) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # { + # "type": "match", + # "trade_id": 82047307, + # "maker_order_id": "0f358725-2134-435e-be11-753912a326e0", + # "taker_order_id": "252b7002-87a3-425c-ac73-f5b9e23f3caf", + # "order_id": "d50ec984-77a8-460a-b958-66f114b0de9b", + # "side": "sell", + # "size": "0.00513192", + # "price": "9314.78", + # "product_id": "BTC-USD", + # "profile_id": "6244401d-c078-40d9-b305-7ad3551bc3b0", + # "sequence": 12038915443, + # "time": "2020-01-31T20:03:41.158814Z" + # "created_at": "2014-11-07T22:19:28.578544Z", + # "liquidity": "T", + # "fee": "0.00025", + # "settled": True, + # "usd_volume": "0.0924556000000000", + # "user_id": "595eb864313c2b02ddf2937d" + # } + # + timestamp = self.parse8601(self.safe_string_2(trade, 'time', 'created_at')) + marketId = self.safe_string(trade, 'product_id') + market = self.safe_market(marketId, market, '-') + feeRate = None + takerOrMaker = None + cost = None + feeCurrencyId = self.safe_string_lower(market, 'quoteId') + if feeCurrencyId is not None: + costField = feeCurrencyId + '_value' + cost = self.safe_string(trade, costField) + liquidity = self.safe_string(trade, 'liquidity') + if liquidity is not None: + takerOrMaker = 'taker' if (liquidity == 'T') else 'maker' + feeRate = self.safe_string(market, takerOrMaker) + feeCost = self.safe_string_2(trade, 'fill_fees', 'fee') + fee = { + 'cost': feeCost, + 'currency': market['quote'], + 'rate': feeRate, + } + id = self.safe_string(trade, 'trade_id') + side = 'sell' if (trade['side'] == 'buy') else 'buy' + orderId = self.safe_string(trade, 'order_id') + # Coinbase Pro returns inverted side to fetchMyTrades vs fetchTrades + makerOrderId = self.safe_string(trade, 'maker_order_id') + takerOrderId = self.safe_string(trade, 'taker_order_id') + if (orderId is not None) or ((makerOrderId is not None) and (takerOrderId is not None)): + side = 'buy' if (trade['side'] == 'buy') else 'sell' + price = self.safe_string(trade, 'price') + amount = self.safe_string(trade, 'size') + return self.safe_trade({ + 'id': id, + 'order': orderId, + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'type': None, + 'takerOrMaker': takerOrMaker, + 'side': side, + 'price': price, + 'amount': amount, + 'fee': fee, + 'cost': cost, + }, market) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + + https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getfills + + fetch all trades made by the user + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch trades for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Trade[]: a list of `trade structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMyTrades() requires a symbol argument') + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchMyTrades', 'paginate') + if paginate: + return self.fetch_paginated_call_dynamic('fetchMyTrades', symbol, since, limit, params, 100) + self.load_markets() + market = self.market(symbol) + request: dict = { + 'product_id': market['id'], + } + if limit is not None: + request['limit'] = limit + if since is not None: + request['start_date'] = self.iso8601(since) + until = self.safe_value_2(params, 'until', 'end_date') + if until is not None: + params = self.omit(params, ['until']) + request['end_date'] = self.iso8601(until) + response = self.privateGetFills(self.extend(request, params)) + return self.parse_trades(response, market, since, limit) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + + https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getproducttrades + + get the list of most recent trades for a particular symbol + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'id': market['id'], # fixes issue #2 + } + if limit is not None: + request['limit'] = limit # default 100 + response = self.publicGetProductsIdTrades(self.extend(request, params)) + # + # [ + # { + # "trade_id": "15035219", + # "side": "sell", + # "size": "0.27426731", + # "price": "25820.42000000", + # "time": "2023-09-10T13:47:41.447577Z" + # }, + # ] + # + return self.parse_trades(response, market, since, limit) + + def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getfees + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + self.load_markets() + response = self.privateGetFees(params) + # + # { + # "maker_fee_rate": "0.0050", + # "taker_fee_rate": "0.0050", + # "usd_volume": "43806.92" + # } + # + maker = self.safe_number(response, 'maker_fee_rate') + taker = self.safe_number(response, 'taker_fee_rate') + result: dict = {} + for i in range(0, len(self.symbols)): + symbol = self.symbols[i] + result[symbol] = { + 'info': response, + 'symbol': symbol, + 'maker': maker, + 'taker': taker, + 'percentage': True, + 'tierBased': True, + } + return result + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # [ + # 1591514160, + # 0.02507, + # 0.02507, + # 0.02507, + # 0.02507, + # 0.02816506 + # ] + # + return [ + self.safe_timestamp(ohlcv, 0), + self.safe_number(ohlcv, 3), + self.safe_number(ohlcv, 2), + self.safe_number(ohlcv, 1), + self.safe_number(ohlcv, 4), + self.safe_number(ohlcv, 5), + ] + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + + https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getproductcandles + + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch trades for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate', False) + if paginate: + return self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params, 300) + market = self.market(symbol) + parsedTimeframe = self.safe_integer(self.timeframes, timeframe) + request: dict = { + 'id': market['id'], + } + if parsedTimeframe is not None: + request['granularity'] = parsedTimeframe + else: + request['granularity'] = timeframe + until = self.safe_value_2(params, 'until', 'end') + params = self.omit(params, ['until']) + if since is not None: + request['start'] = self.iso8601(since) + if limit is None: + # https://docs.pro.coinbase.com/#get-historic-rates + limit = 300 # max = 300 + else: + limit = min(300, limit) + if until is None: + parsedTimeframeMilliseconds = parsedTimeframe * 1000 + if self.is_round_number(since % parsedTimeframeMilliseconds): + request['end'] = self.iso8601(self.sum((limit - 1) * parsedTimeframeMilliseconds, since)) + else: + request['end'] = self.iso8601(self.sum(limit * parsedTimeframeMilliseconds, since)) + else: + request['end'] = self.iso8601(until) + response = self.publicGetProductsIdCandles(self.extend(request, params)) + # + # [ + # [1591514160,0.02507,0.02507,0.02507,0.02507,0.02816506], + # [1591514100,0.02507,0.02507,0.02507,0.02507,1.63830323], + # [1591514040,0.02505,0.02507,0.02505,0.02507,0.19918178] + # ] + # + return self.parse_ohlcvs(response, market, timeframe, since, limit) + + def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = self.publicGetTime(params) + # + # { + # "iso":"2020-05-12T08:00:51.504Z", + # "epoch":1589270451.504 + # } + # + return self.safe_timestamp(response, 'epoch') + + def parse_order_status(self, status: Str): + statuses: dict = { + 'pending': 'open', + 'active': 'open', + 'open': 'open', + 'done': 'closed', + 'canceled': 'canceled', + 'canceling': 'open', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder + # + # { + # "id": "d0c5340b-6d6c-49d9-b567-48c4bfca13d2", + # "price": "0.10000000", + # "size": "0.01000000", + # "product_id": "BTC-USD", + # "side": "buy", + # "stp": "dc", + # "type": "limit", + # "time_in_force": "GTC", + # "post_only": False, + # "created_at": "2016-12-08T20:02:28.53864Z", + # "fill_fees": "0.0000000000000000", + # "filled_size": "0.00000000", + # "executed_value": "0.0000000000000000", + # "status": "pending", + # "settled": False + # } + # + timestamp = self.parse8601(self.safe_string(order, 'created_at')) + marketId = self.safe_string(order, 'product_id') + market = self.safe_market(marketId, market, '-') + status = self.parse_order_status(self.safe_string(order, 'status')) + doneReason = self.safe_string(order, 'done_reason') + if (status == 'closed') and (doneReason == 'canceled'): + status = 'canceled' + price = self.safe_string(order, 'price') + filled = self.safe_string(order, 'filled_size') + amount = self.safe_string(order, 'size', filled) + cost = self.safe_string(order, 'executed_value') + feeCost = self.safe_number(order, 'fill_fees') + fee = None + if feeCost is not None: + fee = { + 'cost': feeCost, + 'currency': market['quote'], + 'rate': None, + } + id = self.safe_string(order, 'id') + type = self.safe_string(order, 'type') + side = self.safe_string(order, 'side') + timeInForce = self.safe_string(order, 'time_in_force') + postOnly = self.safe_value(order, 'post_only') + triggerPrice = self.safe_number(order, 'stop_price') + clientOrderId = self.safe_string(order, 'client_oid') + return self.safe_order({ + 'id': id, + 'clientOrderId': clientOrderId, + 'info': order, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'status': status, + 'symbol': market['symbol'], + 'type': type, + 'timeInForce': timeInForce, + 'postOnly': postOnly, + 'side': side, + 'price': price, + 'triggerPrice': triggerPrice, + 'cost': cost, + 'amount': amount, + 'filled': filled, + 'remaining': None, + 'fee': fee, + 'average': None, + 'trades': None, + }, market) + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + + https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getorder + + fetches information on an order made by the user + :param str id: the order id + :param str symbol: not used by coinbaseexchange fetchOrder + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + request: dict = {} + clientOrderId = self.safe_string_2(params, 'clientOrderId', 'client_oid') + method = None + if clientOrderId is None: + method = 'privateGetOrdersId' + request['id'] = id + else: + method = 'privateGetOrdersClientClientOid' + request['client_oid'] = clientOrderId + params = self.omit(params, ['clientOrderId', 'client_oid']) + response = getattr(self, method)(self.extend(request, params)) + return self.parse_order(response) + + def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all the trades made from a single order + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + request: dict = { + 'order_id': id, + } + response = self.privateGetFills(self.extend(request, params)) + return self.parse_trades(response, market, since, limit) + + def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + + https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getorders + + fetches information on multiple orders made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch open orders for + :returns Order[]: a list of `order structures ` + """ + request: dict = { + 'status': 'all', + } + return self.fetch_open_orders(symbol, since, limit, self.extend(request, params)) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + + https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getorders + + fetch all unfilled currently open orders + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch open orders for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOpenOrders', 'paginate') + if paginate: + return self.fetch_paginated_call_dynamic('fetchOpenOrders', symbol, since, limit, params, 100) + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['product_id'] = market['id'] + if limit is not None: + request['limit'] = limit # default 100 + if since is not None: + request['start_date'] = self.iso8601(since) + until = self.safe_value_2(params, 'until', 'end_date') + if until is not None: + params = self.omit(params, ['until']) + request['end_date'] = self.iso8601(until) + response = self.privateGetOrders(self.extend(request, params)) + return self.parse_orders(response, market, since, limit) + + def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + + https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getorders + + fetches information on multiple closed orders made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch open orders for + :returns Order[]: a list of `order structures ` + """ + request: dict = { + 'status': 'done', + } + return self.fetch_open_orders(symbol, since, limit, self.extend(request, params)) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + + https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_postorders + + create a trade order + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + # common params -------------------------------------------------- + # 'client_oid': clientOrderId, + 'type': type, + 'side': side, + 'product_id': market['id'], + # 'size': self.amount_to_precision(symbol, amount), + # 'stp': 'dc', # self-trade prevention, dc = decrease and cancel, co = cancel oldest, cn = cancel newest, cb = cancel both + # 'stop': 'loss', # "loss" = stop loss below price, "entry" = take profit above price + # 'stop_price': self.price_to_precision(symbol, price), + # limit order params --------------------------------------------- + # 'price': self.price_to_precision(symbol, price), + # 'size': self.amount_to_precision(symbol, amount), + # 'time_in_force': 'GTC', # GTC, GTT, IOC, or FOK + # 'cancel_after' [optional]* min, hour, day, requires time_in_force to be GTT + # 'post_only': False, # invalid when time_in_force is IOC or FOK + # market order params -------------------------------------------- + # 'size': self.amount_to_precision(symbol, amount), + # 'funds': self.cost_to_precision(symbol, amount), + } + clientOrderId = self.safe_string_2(params, 'clientOrderId', 'client_oid') + if clientOrderId is not None: + request['client_oid'] = clientOrderId + triggerPrice = self.safe_number_n(params, ['stopPrice', 'stop_price', 'triggerPrice']) + if triggerPrice is not None: + request['stop_price'] = self.price_to_precision(symbol, triggerPrice) + timeInForce = self.safe_string_2(params, 'timeInForce', 'time_in_force') + if timeInForce is not None: + request['time_in_force'] = timeInForce + postOnly = self.safe_value_2(params, 'postOnly', 'post_only', False) + if postOnly: + request['post_only'] = True + params = self.omit(params, ['timeInForce', 'time_in_force', 'stopPrice', 'stop_price', 'clientOrderId', 'client_oid', 'postOnly', 'post_only', 'triggerPrice']) + if type == 'limit': + request['price'] = self.price_to_precision(symbol, price) + request['size'] = self.amount_to_precision(symbol, amount) + elif type == 'market': + cost = self.safe_number_2(params, 'cost', 'funds') + if cost is None: + if price is not None: + cost = amount * price + else: + params = self.omit(params, ['cost', 'funds']) + if cost is not None: + request['funds'] = self.cost_to_precision(symbol, cost) + else: + request['size'] = self.amount_to_precision(symbol, amount) + response = self.privatePostOrders(self.extend(request, params)) + # + # { + # "id": "d0c5340b-6d6c-49d9-b567-48c4bfca13d2", + # "price": "0.10000000", + # "size": "0.01000000", + # "product_id": "BTC-USD", + # "side": "buy", + # "stp": "dc", + # "type": "limit", + # "time_in_force": "GTC", + # "post_only": False, + # "created_at": "2016-12-08T20:02:28.53864Z", + # "fill_fees": "0.0000000000000000", + # "filled_size": "0.00000000", + # "executed_value": "0.0000000000000000", + # "status": "pending", + # "settled": False + # } + # + return self.parse_order(response, market) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + + https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_deleteorder + + cancels an open order + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + request: dict = { + # 'product_id': market['id'], # the request will be more performant if you include it + } + clientOrderId = self.safe_string_2(params, 'clientOrderId', 'client_oid') + method = None + if clientOrderId is None: + method = 'privateDeleteOrdersId' + request['id'] = id + else: + method = 'privateDeleteOrdersClientClientOid' + request['client_oid'] = clientOrderId + params = self.omit(params, ['clientOrderId', 'client_oid']) + market = None + if symbol is not None: + market = self.market(symbol) + request['product_id'] = market['symbol'] # the request will be more performant if you include it + response = getattr(self, method)(self.extend(request, params)) + return self.safe_order({'info': response}) + + def cancel_all_orders(self, symbol: Str = None, params={}): + """ + + https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_deleteorders + + cancel all open orders + :param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['product_id'] = market['symbol'] # the request will be more performant if you include it + response = self.privateDeleteOrders(self.extend(request, params)) + return [self.safe_order({'info': response})] + + def fetch_payment_methods(self, params={}): + return self.privateGetPaymentMethods(params) + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_postwithdrawpaymentmethod + https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_postwithdrawcoinbaseaccount + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.check_address(address) + self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + 'amount': amount, + } + method = 'privatePostWithdrawals' + if 'payment_method_id' in params: + method += 'PaymentMethod' + elif 'coinbase_account_id' in params: + method += 'CoinbaseAccount' + else: + method += 'Crypto' + request['crypto_address'] = address + if tag is not None: + request['destination_tag'] = tag + response = getattr(self, method)(self.extend(request, params)) + if not response: + raise ExchangeError(self.id + ' withdraw() error: ' + self.json(response)) + return self.parse_transaction(response, currency) + + def parse_ledger_entry_type(self, type): + types: dict = { + 'transfer': 'transfer', # Funds moved between portfolios + 'match': 'trade', # Funds moved result of a trade + 'fee': 'fee', # Fee result of a trade + 'rebate': 'rebate', # Fee rebate + 'conversion': 'trade', # Funds converted between fiat currency and a stablecoin + } + return self.safe_string(types, type, type) + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + # { + # "id": "12087495079", + # "amount": "-0.0100000000000000", + # "balance": "0.0645419900000000", + # "created_at": "2021-10-28T17:14:32.593168Z", + # "type": "transfer", + # "details": { + # "from": "2f74edf7-1440-4586-86dc-ae58c5693691", + # "profile_transfer_id": "3ef093ad-2482-40d1-8ede-2f89cff5099e", + # "to": "dda99503-4980-4b60-9549-0b770ee51336" + # } + # }, + # { + # "id": "11740725774", + # "amount": "-1.7565669701255000", + # "balance": "0.0016490047745000", + # "created_at": "2021-10-22T03:47:34.764122Z", + # "type": "fee", + # "details": { + # "order_id": "ad06abf4-95ab-432a-a1d8-059ef572e296", + # "product_id": "ETH-DAI", + # "trade_id": "1740617" + # } + # } + id = self.safe_string(item, 'id') + amountString = self.safe_string(item, 'amount') + direction = None + afterString = self.safe_string(item, 'balance') + beforeString = Precise.string_sub(afterString, amountString) + if Precise.string_lt(amountString, '0'): + direction = 'out' + amountString = Precise.string_abs(amountString) + else: + direction = 'in' + amount = self.parse_number(amountString) + after = self.parse_number(afterString) + before = self.parse_number(beforeString) + timestamp = self.parse8601(self.safe_value(item, 'created_at')) + type = self.parse_ledger_entry_type(self.safe_string(item, 'type')) + code = self.safe_currency_code(None, currency) + details = self.safe_value(item, 'details', {}) + account = None + referenceAccount = None + referenceId = None + if type == 'transfer': + account = self.safe_string(details, 'from') + referenceAccount = self.safe_string(details, 'to') + referenceId = self.safe_string(details, 'profile_transfer_id') + else: + referenceId = self.safe_string(details, 'order_id') + status = 'ok' + return self.safe_ledger_entry({ + 'info': item, + 'id': id, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'direction': direction, + 'account': account, + 'referenceAccount': referenceAccount, + 'referenceId': referenceId, + 'type': type, + 'currency': code, + 'amount': amount, + 'before': before, + 'after': after, + 'status': status, + 'fee': None, + }, currency) + + def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + + https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getaccountledger + + :param str code: unified currency code, default is None + :param int [since]: timestamp in ms of the earliest ledger entry, default is None + :param int [limit]: max number of ledger entries to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch trades for + :returns dict: a `ledger structure ` + """ + # https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getaccountledger + if code is None: + raise ArgumentsRequired(self.id + ' fetchLedger() requires a code param') + self.load_markets() + self.load_accounts() + currency = self.currency(code) + accountsByCurrencyCode = self.index_by(self.accounts, 'code') + account = self.safe_value(accountsByCurrencyCode, code) + if account is None: + raise ExchangeError(self.id + ' fetchLedger() could not find account id for ' + code) + request: dict = { + 'id': account['id'], + # 'start_date': self.iso8601(since), + # 'end_date': self.iso8601(self.milliseconds()), + # 'before': 'cursor', # sets start cursor to before date + # 'after': 'cursor', # sets end cursor to after date + # 'limit': limit, # default 100 + # 'profile_id': 'string' + } + if since is not None: + request['start_date'] = self.iso8601(since) + if limit is not None: + request['limit'] = limit # default 100 + until = self.safe_value_2(params, 'until', 'end_date') + if until is not None: + params = self.omit(params, ['until']) + request['end_date'] = self.iso8601(until) + response = self.privateGetAccountsIdLedger(self.extend(request, params)) + for i in range(0, len(response)): + response[i]['currency'] = code + return self.parse_ledger(response, currency, since, limit) + + def fetch_deposits_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch history of deposits and withdrawals + + https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_gettransfers + https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getaccounttransfers + + :param str [code]: unified currency code for the currency of the deposit/withdrawals, default is None + :param int [since]: timestamp in ms of the earliest deposit/withdrawal, default is None + :param int [limit]: max number of deposit/withdrawals to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.id]: account id, when defined, the endpoint used is '/accounts/{account_id}/transfers/' instead of '/transfers/' + :returns dict: a list of `transaction structure ` + """ + self.load_markets() + self.load_accounts() + currency = None + id = self.safe_string(params, 'id') # account id + if id is None: + if code is not None: + currency = self.currency(code) + accountsByCurrencyCode = self.index_by(self.accounts, 'code') + account = self.safe_value(accountsByCurrencyCode, code) + if account is None: + raise ExchangeError(self.id + ' fetchDepositsWithdrawals() could not find account id for ' + code) + id = account['id'] + request: dict = {} + if id is not None: + request['id'] = id + if limit is not None: + request['limit'] = limit + response = None + if id is None: + response = self.privateGetTransfers(self.extend(request, params)) + # + # [ + # { + # "id": "bee6fd7c-afb2-4e47-8298-671d09997d16", + # "type": "deposit", + # "created_at": "2022-12-21 00:48:45.477503+00", + # "completed_at": null, + # "account_id": "sal3802-36bd-46be-a7b8-alsjf383sldak", + # "user_id": "6382048209f92as392039dlks2", + # "amount": "0.01000000", + # "details": { + # "network": "litecoin", + # "crypto_address": "MKemtnCFUYKsNWaf5EMYMpwSszcXWFDtTY", + # "coinbase_account_id": "fl2b6925-f6ba-403n-jj03-40fl435n430f", + # "coinbase_transaction_id": "63a25bb13cb5cf0001d2cf17", # withdrawals only + # "crypto_transaction_hash": "752f35570736341e2a253f7041a34cf1e196fc56128c900fd03d99da899d94c1", + # "tx_service_transaction_id": "1873249104", + # "coinbase_payment_method_id": "" + # }, + # "canceled_at": null, + # "processed_at": null, + # "user_nonce": null, + # "idem": "5e3201b0-e390-5k3k-a913-c32932049242", + # "profile_id": "k3k302a8-c4dk-4f49-9d39-3203923wpk39", + # "currency": "LTC" + # } + # ] + # + for i in range(0, len(response)): + account_id = self.safe_string(response[i], 'account_id') + account = self.safe_value(self.accountsById, account_id) + codeInner = self.safe_string(account, 'code') + response[i]['currency'] = codeInner + else: + response = self.privateGetAccountsIdTransfers(self.extend(request, params)) + # + # [ + # { + # "id": "bee6fd7c-afb2-4e47-8298-671d09997d16", + # "type": "deposit", + # "created_at": "2022-12-21 00:48:45.477503+00", + # "completed_at": null, + # "amount": "0.01000000", + # "details": { + # "network": "litecoin", + # "crypto_address": "MKemtnCFUYKsNWaf5EMYMpwSszcXWFDtTY", + # "coinbase_account_id": "fl2b6925-f6ba-403n-jj03-40fl435n430f", + # "coinbase_transaction_id": "63a25bb13cb5cf0001d2cf17", # withdrawals only + # "crypto_transaction_hash": "752f35570736341e2a253f7041a34cf1e196fc56128c900fd03d99da899d94c1", + # "tx_service_transaction_id": "1873249104", + # "coinbase_payment_method_id": "" + # }, + # "canceled_at": null, + # "processed_at": null, + # "user_nonce": null, + # "idem": "5e3201b0-e390-5k3k-a913-c32932049242", + # "profile_id": "k3k302a8-c4dk-4f49-9d39-3203923wpk39", + # "currency": "LTC" + # } + # ] + # + for i in range(0, len(response)): + response[i]['currency'] = code + return self.parse_transactions(response, currency, since, limit) + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_gettransfers + https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getaccounttransfers + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + return self.fetch_deposits_withdrawals(code, since, limit, self.extend({'type': 'deposit'}, params)) + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_gettransfers + https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getaccounttransfers + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + return self.fetch_deposits_withdrawals(code, since, limit, self.extend({'type': 'withdraw'}, params)) + + def parse_transaction_status(self, transaction): + canceled = self.safe_value(transaction, 'canceled_at') + if canceled: + return 'canceled' + processed = self.safe_value(transaction, 'processed_at') + completed = self.safe_value(transaction, 'completed_at') + if completed: + return 'ok' + elif processed and not completed: + return 'failed' + else: + return 'pending' + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # privateGetTransfers + # + # [ + # { + # "id": "bee6fd7c-afb2-4e47-8298-671d09997d16", + # "type": "deposit", + # "created_at": "2022-12-21 00:48:45.477503+00", + # "completed_at": null, + # "account_id": "sal3802-36bd-46be-a7b8-alsjf383sldak", # only from privateGetTransfers + # "user_id": "6382048209f92as392039dlks2", # only from privateGetTransfers + # "amount": "0.01000000", + # "details": { + # "network": "litecoin", + # "crypto_address": "MKemtnCFUYKsNWaf5EMYMpwSszcXWFDtTY", + # "coinbase_account_id": "fl2b6925-f6ba-403n-jj03-40fl435n430f", + # "coinbase_transaction_id": "63a25bb13cb5cf0001d2cf17", # withdrawals only + # "crypto_transaction_hash": "752f35570736341e2a253f7041a34cf1e196fc56128c900fd03d99da899d94c1", + # "tx_service_transaction_id": "1873249104", + # "coinbase_payment_method_id": "" + # }, + # "canceled_at": null, + # "processed_at": null, + # "user_nonce": null, + # "idem": "5e3201b0-e390-5k3k-a913-c32932049242", + # "profile_id": "k3k302a8-c4dk-4f49-9d39-3203923wpk39", + # "currency": "LTC" + # } + # ] + # + details = self.safe_value(transaction, 'details', {}) + timestamp = self.parse8601(self.safe_string(transaction, 'created_at')) + currencyId = self.safe_string(transaction, 'currency') + code = self.safe_currency_code(currencyId, currency) + amount = self.safe_number(transaction, 'amount') + type = self.safe_string(transaction, 'type') + address = self.safe_string(details, 'crypto_address') + address = self.safe_string(transaction, 'crypto_address', address) + fee = { + 'currency': None, + 'cost': None, + 'rate': None, + } + if type == 'withdraw': + type = 'withdrawal' + address = self.safe_string(details, 'sent_to_address', address) + feeCost = self.safe_number(details, 'fee') + if feeCost is not None: + if amount is not None: + amount -= feeCost + fee['cost'] = feeCost + fee['currency'] = code + networkId = self.safe_string(details, 'network') + return { + 'info': transaction, + 'id': self.safe_string(transaction, 'id'), + 'txid': self.safe_string(details, 'crypto_transaction_hash'), + 'type': type, + 'currency': code, + 'network': self.network_id_to_code(networkId), + 'amount': amount, + 'status': self.parse_transaction_status(transaction), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'address': address, + 'addressFrom': None, + 'addressTo': self.safe_string(details, 'crypto_address'), + 'tag': self.safe_string(details, 'destination_tag'), + 'tagFrom': None, + 'tagTo': None, + 'updated': self.parse8601(self.safe_string(transaction, 'processed_at')), + 'comment': None, + 'internal': False, + 'fee': fee, + } + + def create_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + create a currency deposit address + + https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_postcoinbaseaccountaddresses + + :param str code: unified currency code of the currency for the deposit address + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + self.load_markets() + currency = self.currency(code) + accounts = self.safe_value(self.options, 'coinbaseAccounts') + if accounts is None: + accounts = self.privateGetCoinbaseAccounts() + self.options['coinbaseAccounts'] = accounts # cache it + self.options['coinbaseAccountsByCurrencyId'] = self.index_by(accounts, 'currency') + currencyId = currency['id'] + account = self.safe_value(self.options['coinbaseAccountsByCurrencyId'], currencyId) + if account is None: + # eslint-disable-next-line quotes + raise InvalidAddress(self.id + " createDepositAddress() could not find currency code " + code + " with id = " + currencyId + " in self.options['coinbaseAccountsByCurrencyId']") + request: dict = { + 'id': account['id'], + } + response = self.privatePostCoinbaseAccountsIdAddresses(self.extend(request, params)) + address = self.safe_string(response, 'address') + tag = self.safe_string(response, 'destination_tag') + return { + 'currency': code, + 'address': self.check_address(address), + 'network': None, + 'tag': tag, + 'info': response, + } + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + request = '/' + self.implode_params(path, params) + query = self.omit(params, self.extract_params(path)) + if method == 'GET': + if query: + request += '?' + self.urlencode(query) + url = self.implode_hostname(self.urls['api'][api]) + request + if api == 'private': + self.check_required_credentials() + nonce = str(self.nonce()) + payload = '' + if method != 'GET': + if query: + body = self.json(query) + payload = body + what = nonce + method + request + payload + secret = None + try: + secret = self.base64_to_binary(self.secret) + except Exception as e: + raise AuthenticationError(self.id + ' sign() invalid base64 secret') + signature = self.hmac(self.encode(what), secret, hashlib.sha256, 'base64') + headers = { + 'CB-ACCESS-KEY': self.apiKey, + 'CB-ACCESS-SIGN': signature, + 'CB-ACCESS-TIMESTAMP': nonce, + 'CB-ACCESS-PASSPHRASE': self.password, + 'Content-Type': 'application/json', + } + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if (code == 400) or (code == 404): + if body[0] == '{': + message = self.safe_string(response, 'message') + feedback = self.id + ' ' + message + self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + raise ExchangeError(feedback) # unknown message + raise ExchangeError(self.id + ' ' + body) + return None + + def request(self, path, api='public', method='GET', params={}, headers=None, body=None, config={}): + response = self.fetch2(path, api, method, params, headers, body, config) + if not isinstance(response, str): + if 'message' in response: + raise ExchangeError(self.id + ' ' + self.json(response)) + return response diff --git a/ccxt/coinbaseinternational.py b/ccxt/coinbaseinternational.py new file mode 100644 index 0000000..319e23a --- /dev/null +++ b/ccxt/coinbaseinternational.py @@ -0,0 +1,2254 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.coinbaseinternational import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currencies, Currency, DepositAddress, Int, MarginModification, Market, Order, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, Trade, Transaction, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import DuplicateOrderId +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class coinbaseinternational(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(coinbaseinternational, self).describe(), { + 'id': 'coinbaseinternational', + 'name': 'Coinbase International', + 'countries': ['US'], + 'certified': False, + 'pro': True, + 'rateLimit': 100, # 10 requests per second + 'version': 'v1', + 'userAgent': self.userAgents['chrome'], + 'headers': { + 'CB-VERSION': '2018-05-30', + }, + 'has': { + 'CORS': True, + 'spot': True, + 'margin': True, + 'swap': True, + 'future': True, + 'option': False, + 'addMargin': False, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'cancelOrders': False, + 'closeAllPositions': False, + 'closePosition': False, + 'createDepositAddress': True, + 'createLimitBuyOrder': True, + 'createLimitSellOrder': True, + 'createMarketBuyOrder': True, + 'createMarketBuyOrderWithCost': False, + 'createMarketOrderWithCost': False, + 'createMarketSellOrder': True, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createPostOnlyOrder': True, + 'createReduceOnlyOrder': False, + 'createStopLimitOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'editOrder': True, + 'fetchAccounts': True, + 'fetchBalance': True, + 'fetchBidsAsks': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchCanceledOrders': False, + 'fetchClosedOrders': False, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDeposits': True, + 'fetchFundingHistory': True, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': True, + 'fetchFundingRates': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchL2OrderBook': False, + 'fetchLedger': False, + 'fetchLeverage': False, + 'fetchLeverageTiers': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMyBuys': True, + 'fetchMySells': True, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterestHistory': False, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': False, + 'fetchOrders': False, + 'fetchPosition': True, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': True, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': False, + 'fetchTrades': False, + 'fetchTradingFee': False, + 'fetchTradingFees': False, + 'fetchTransfers': True, + 'fetchWithdrawals': True, + 'reduceMargin': False, + 'sandbox': True, + 'setLeverage': False, + 'setMargin': True, + 'setMarginMode': False, + 'setPositionMode': False, + 'withdraw': True, + }, + 'urls': { + 'logo': 'https://github.com/ccxt/ccxt/assets/43336371/866ae638-6ab5-4ebf-ab2c-cdcce9545625', + 'api': { + 'rest': 'https://api.international.coinbase.com/api', + }, + 'test': { + 'rest': 'https://api-n5e1.coinbase.com/api', + }, + 'www': 'https://international.coinbase.com', + 'doc': [ + 'https://docs.cloud.coinbase.com/intx/docs', + ], + 'fees': [ + 'https://help.coinbase.com/en/international-exchange/trading-deposits-withdrawals/international-exchange-fees', + ], + 'referral': '', + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + 'password': True, + }, + 'api': { + 'v1': { + 'public': { + 'get': [ + 'assets', + 'assets/{assets}', + 'assets/{asset}/networks', + 'instruments', + 'instruments/{instrument}', + 'instruments/{instrument}/quote', + 'instruments/{instrument}/funding', + 'instruments/{instrument}/candles', + ], + }, + 'private': { + 'get': [ + 'orders', + 'orders/{id}', + 'portfolios', + 'portfolios/{portfolio}', + 'portfolios/{portfolio}/detail', + 'portfolios/{portfolio}/summary', + 'portfolios/{portfolio}/balances', + 'portfolios/{portfolio}/balances/{asset}', + 'portfolios/{portfolio}/positions', + 'portfolios/{portfolio}/positions/{instrument}', + 'portfolios/fills', + 'portfolios/{portfolio}/fills', + 'transfers', + 'transfers/{transfer_uuid}', + ], + 'post': [ + 'orders', + 'portfolios', + 'portfolios/margin', + 'portfolios/transfer', + 'transfers/withdraw', + 'transfers/address', + 'transfers/create-counterparty-id', + 'transfers/validate-counterparty-id', + 'transfers/withdraw/counterparty', + ], + 'put': [ + 'orders/{id}', + 'portfolios/{portfolio}', + ], + 'delete': [ + 'orders', + 'orders/{id}', + ], + }, + }, + }, + 'fees': { + 'trading': { + 'taker': self.parse_number('0.004'), + 'maker': self.parse_number('0.002'), + 'tierBased': True, + 'percentage': True, + 'tiers': { + 'taker': [ + [self.parse_number('0'), self.parse_number('0.004')], + [self.parse_number('1000000'), self.parse_number('0.004')], + [self.parse_number('5000000'), self.parse_number('0.0035')], + [self.parse_number('10000000'), self.parse_number('0.0035')], + [self.parse_number('50000000'), self.parse_number('0.003')], + [self.parse_number('250000000'), self.parse_number('0.0025')], + ], + 'maker': [ + [self.parse_number('0'), self.parse_number('0.002')], + [self.parse_number('1000000'), self.parse_number('0.0016')], + [self.parse_number('5000000'), self.parse_number('0.001')], + [self.parse_number('10000000'), self.parse_number('0.0008')], + [self.parse_number('50000000'), self.parse_number('0.0005')], + [self.parse_number('250000000'), self.parse_number('0')], + ], + }, + }, + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'exact': {}, + 'broad': { + 'DUPLICATE_CLIENT_ORDER_ID': DuplicateOrderId, + 'Order rejected': InvalidOrder, + 'market orders must be IoC': InvalidOrder, + 'tif is required': InvalidOrder, + 'Invalid replace order request': InvalidOrder, + 'Unauthorized': PermissionDenied, + 'invalid result_limit': BadRequest, + 'is a required field': BadRequest, + 'Not Found': BadRequest, + 'ip not allowed': AuthenticationError, + }, + }, + 'timeframes': { + '1m': 'ONE_MINUTE', + '5m': 'FIVE_MINUTE', + '15m': 'FIFTEEN_MINUTE', + '30m': 'THIRTY_MINUTE', + '1h': 'ONE_HOUR', + '2h': 'TWO_HOUR', + '6h': 'SIX_HOUR', + '1d': 'ONE_DAY', + }, + 'options': { + 'brokerId': 'nfqkvdjp', + 'portfolio': '', # default portfolio id + 'withdraw': { + 'method': 'v1PrivatePostTransfersWithdraw', # use v1PrivatePostTransfersWithdrawCounterparty for counterparty withdrawals + }, + 'networksById': { + 'ethereum': 'ETH', + 'arbitrum': 'ARBITRUM', + 'avacchain': 'AVAX', + 'optimism': 'OPTIMISM', + 'polygon': 'MATIC', + 'solana': 'SOL', + 'bitcoin': 'BTC', + }, + }, + 'features': { + 'default': { + 'sandbox': True, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerPriceType': None, + 'triggerDirection': True, + 'stopLossPrice': False, # todo implementation + 'takeProfitPrice': False, # todo implementation + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': True, + 'GTC': True, # has 30 days max + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': False, + 'marketBuyRequiresPrice': True, + 'selfTradePrevention': True, # todo: implement + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, + 'daysBack': None, + 'untilDays': 10000, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 100, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': None, + 'fetchOHLCV': { + 'limit': 300, + }, + }, + 'spot': { + 'extends': 'default', + }, + 'swap': { + 'linear': { + 'extends': 'default', + }, + 'inverse': { + 'extends': 'default', + }, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + }) + + def handle_portfolio_and_params(self, methodName: str, params={}): + portfolio = None + portfolio, params = self.handle_option_and_params(params, methodName, 'portfolio') + if (portfolio is not None) and (portfolio != ''): + return [portfolio, params] + defaultPortfolio = self.safe_string(self.options, 'portfolio') + if (defaultPortfolio is not None) and (defaultPortfolio != ''): + return [defaultPortfolio, params] + accounts = self.fetch_accounts() + for i in range(0, len(accounts)): + account = accounts[i] + info = self.safe_dict(account, 'info', {}) + if self.safe_bool(info, 'is_default'): + portfolioId = self.safe_string(info, 'portfolio_id') + self.options['portfolio'] = portfolioId + return [portfolioId, params] + raise ArgumentsRequired(self.id + ' ' + methodName + '() requires a portfolio parameter or set the default portfolio with self.options["portfolio"]') + + def handle_network_id_and_params(self, currencyCode: str, methodName: str, params): + networkId = None + networkId, params = self.handle_option_and_params(params, methodName, 'network_arn_id') + if networkId is None: + self.load_currency_networks(currencyCode) + networks = self.currencies[currencyCode]['networks'] + network = self.safe_string_2(params, 'networkCode', 'network') + if network is None: + # find default network + if self.is_empty(networks): + raise BadRequest(self.id + ' createDepositAddress network not found for currency ' + currencyCode + ' please specify networkId in params') + defaultNetwork = self.find_default_network(networks) + networkId = defaultNetwork['id'] + else: + networkId = self.network_code_to_id(network, currencyCode) + return [networkId, params] + + def fetch_accounts(self, params={}): + """ + fetch all the accounts associated with a profile + + https://docs.cloud.coinbase.com/intx/reference/getportfolios + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `account structures ` indexed by the account type + """ + self.load_markets() + response = self.v1PrivateGetPortfolios(params) + # + # [ + # { + # "portfolio_id":"1ap32qsc-1-0", + # "portfolio_uuid":"028d7f6c-b92c-7361-8b7e-2932711e5a22", + # "name":"CCXT Portfolio 030624-17:16", + # "user_uuid":"e6cf46b6-a32f-5fa7-addb-3324d4526fbd", + # "maker_fee_rate":"0", + # "taker_fee_rate":"0.0002", + # "trading_lock":false, + # "borrow_disabled":false, + # "is_lsp":false, + # "is_default":true, + # "cross_collateral_enabled":false + # } + # ] + # + return self.parse_accounts(response, params) + + def parse_account(self, account): + # + # { + # "portfolio_id":"1ap32qsc-1-0", + # "portfolio_uuid":"028d7f6c-b92c-7361-8b7e-2932711e5a22", + # "name":"CCXT Portfolio 030624-17:16", + # "user_uuid":"e6cf46b6-a32f-5fa7-addb-3324d4526fbd", + # "maker_fee_rate":"0", + # "taker_fee_rate":"0.0002", + # "trading_lock":false, + # "borrow_disabled":false, + # "is_lsp":false, + # "is_default":true, + # "cross_collateral_enabled":false + # } + # + return { + 'id': self.safe_string_2(account, 'portfolio_id', 'portfolio_uuid'), + 'type': None, + 'code': None, + 'info': account, + } + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = 100, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://docs.cdp.coinbase.com/intx/reference/getinstrumentcandles + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch, default 100 max 10000 + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + :param int [params.until]: timestamp in ms of the latest candle to fetch + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate') + if paginate: + return self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params, 10000) + market = self.market(symbol) + request: dict = { + 'instrument': market['id'], + 'granularity': self.safe_string(self.timeframes, timeframe, timeframe), + } + if since is not None: + request['start'] = self.iso8601(since) + else: + raise ArgumentsRequired(self.id + ' fetchOHLCV() requires a since argument') + unitl = self.safe_integer(params, 'until') + if unitl is not None: + params = self.omit(params, 'until') + request['end'] = self.iso8601(unitl) + response = self.v1PublicGetInstrumentsInstrumentCandles(self.extend(request, params)) + # + # { + # "aggregations": [ + # { + # "start": "2024-04-23T00:00:00Z", + # "open": "62884.4", + # "high": "64710.6", + # "low": "62884.4", + # "close": "63508.4", + # "volume": "3253.9983" + # } + # ] + # } + # + candles = self.safe_list(response, 'aggregations', []) + return self.parse_ohlcvs(candles, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # { + # "start": "2024-04-23T00:00:00Z", + # "open": "62884.4", + # "high": "64710.6", + # "low": "62884.4", + # "close": "63508.4", + # "volume": "3253.9983" + # } + # + return [ + self.parse8601(self.safe_string_2(ohlcv, 'start', 'time')), + self.safe_number(ohlcv, 'open'), + self.safe_number(ohlcv, 'high'), + self.safe_number(ohlcv, 'low'), + self.safe_number(ohlcv, 'close'), + self.safe_number(ohlcv, 'volume'), + ] + + def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical funding rate prices + + https://docs.cloud.coinbase.com/intx/reference/getinstrumentfunding + + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: timestamp in ms of the earliest funding rate to fetch + :param int [limit]: the maximum amount of `funding rate structures ` to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `funding rate structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchFundingRateHistory() requires a symbol argument') + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchFundingRateHistory', 'paginate') + maxEntriesPerRequest = None + maxEntriesPerRequest, params = self.handle_option_and_params(params, 'fetchFundingRateHistory', 'maxEntriesPerRequest', 100) + pageKey = 'ccxtPageKey' + if paginate: + return self.fetch_paginated_call_incremental('fetchFundingRateHistory', symbol, since, limit, params, pageKey, maxEntriesPerRequest) + market = self.market(symbol) + page = self.safe_integer(params, pageKey, 1) - 1 + request: dict = { + 'instrument': market['id'], + 'result_offset': self.safe_integer_2(params, 'offset', 'result_offset', page * maxEntriesPerRequest), + } + if limit is not None: + request['result_limit'] = limit + response = self.v1PublicGetInstrumentsInstrumentFunding(self.extend(request, params)) + # + # { + # "pagination":{ + # "result_limit":"25", + # "result_offset":"0" + # }, + # "results":[ + # { + # "instrument_id":"149264167780483072", + # "funding_rate":"0.000011", + # "mark_price":"47388.1", + # "event_time":"2024-02-10T16:00:00Z" + # }, + # ... + # ] + # } + # + rawRates = self.safe_list(response, 'results', []) + return self.parse_funding_rate_histories(rawRates, market, since, limit) + + def parse_funding_rate_history(self, info, market: Market = None): + return self.parse_funding_rate(info, market) + + def parse_funding_rate(self, contract, market: Market = None): + # + # { + # "instrument_id":"149264167780483072", + # "funding_rate":"0.000011", + # "mark_price":"47388.1", + # "event_time":"2024-02-10T16:00:00Z" + # } + # + fundingDatetime = self.safe_string_2(contract, 'event_time', 'time') + return { + 'info': contract, + 'symbol': self.safe_symbol(None, market), + 'markPrice': self.safe_number(contract, 'mark_price'), + 'indexPrice': None, + 'interestRate': None, + 'estimatedSettlePrice': None, + 'timestamp': self.parse8601(fundingDatetime), + 'datetime': fundingDatetime, + 'fundingRate': self.safe_number(contract, 'funding_rate'), + 'fundingTimestamp': self.parse8601(fundingDatetime), + 'fundingDatetime': fundingDatetime, + 'nextFundingRate': None, + 'nextFundingTimestamp': None, + 'nextFundingDatetime': None, + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + } + + def fetch_funding_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch the history of funding payments paid and received on self account + + https://docs.cdp.coinbase.com/intx/reference/gettransfers + + :param str [symbol]: unified market symbol + :param int [since]: the earliest time in ms to fetch funding history for + :param int [limit]: the maximum number of funding history structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding history structure ` + """ + self.load_markets() + request: dict = { + 'type': 'FUNDING', + } + market: Market = None + if symbol is not None: + market = self.market(symbol) + portfolios = None + portfolios, params = self.handle_option_and_params(params, 'fetchFundingHistory', 'portfolios') + if portfolios is not None: + request['portfolios'] = portfolios + if since is not None: + request['time_from'] = self.iso8601(since) + if limit is not None: + request['result_limit'] = limit + else: + request['result_limit'] = 100 + response = self.v1PrivateGetTransfers(self.extend(request, params)) + fundings = self.safe_list(response, 'results', []) + return self.parse_incomes(fundings, market, since, limit) + + def parse_income(self, income, market: Market = None): + # + # { + # "amount":"0.0008", + # "asset":"USDC", + # "created_at":"2024-02-22T16:00:00Z", + # "from_portfolio":{ + # "id":"13yuk1fs-1-0", + # "name":"Eng Test Portfolio - 2", + # "uuid":"018712f2-5ff9-7de3-9010-xxxxxxxxx" + # }, + # "instrument_id":"149264164756389888", + # "instrument_symbol":"ETH-PERP", + # "position_id":"1xy4v51m-1-2", + # "status":"PROCESSED", + # "to_portfolio":{ + # "name":"CB_FUND" + # }, + # "transfer_type":"FUNDING", + # "transfer_uuid":"a6b708df-2c44-32c5-bb98-xxxxxxxxxx", + # "updated_at":"2024-02-22T16:00:00Z" + # } + # + marketId = self.safe_string(income, 'symbol') + market = self.safe_market(marketId, market, None, 'contract') + datetime = self.safe_integer(income, 'created_at') + timestamp = self.parse8601(datetime) + currencyId = self.safe_string(income, 'asset') + code = self.safe_currency_code(currencyId) + return { + 'info': income, + 'symbol': market['symbol'], + 'code': code, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': self.safe_string(income, 'transfer_uuid'), + 'amount': self.safe_number(income, 'amount'), + 'rate': None, + } + + def fetch_transfers(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[TransferEntry]: + """ + fetch a history of internal transfers made on an account + + https://docs.cdp.coinbase.com/intx/reference/gettransfers + + :param str code: unified currency code of the currency transferred + :param int [since]: the earliest time in ms to fetch transfers for + :param int [limit]: the maximum number of transfers structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transfer structures ` + """ + self.load_markets() + request: dict = { + 'type': 'INTERNAL', + } + currency = None + if code is not None: + currency = self.currency(code) + portfolios = None + portfolios, params = self.handle_option_and_params(params, 'fetchTransfers', 'portfolios') + if portfolios is not None: + request['portfolios'] = portfolios + if since is not None: + request['time_from'] = self.iso8601(since) + if limit is not None: + request['result_limit'] = limit + else: + request['result_limit'] = 100 + response = self.v1PrivateGetTransfers(self.extend(request, params)) + transfers = self.safe_list(response, 'results', []) + return self.parse_transfers(transfers, currency, since, limit) + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + # + # { + # "amount":"0.0008", + # "asset":"USDC", + # "created_at":"2024-02-22T16:00:00Z", + # "from_portfolio":{ + # "id":"13yuk1fs-1-0", + # "name":"Eng Test Portfolio - 2", + # "uuid":"018712f2-5ff9-7de3-9010-xxxxxxxxx" + # }, + # "instrument_id":"149264164756389888", + # "instrument_symbol":"ETH-PERP", + # "position_id":"1xy4v51m-1-2", + # "status":"PROCESSED", + # "to_portfolio":{ + # "name":"CB_FUND" + # }, + # "transfer_type":"FUNDING", + # "transfer_uuid":"a6b708df-2c44-32c5-bb98-xxxxxxxxxx", + # "updated_at":"2024-02-22T16:00:00Z" + # } + # + datetime = self.safe_integer(transfer, 'created_at') + timestamp = self.parse8601(datetime) + currencyId = self.safe_string(transfer, 'asset') + code = self.safe_currency_code(currencyId) + fromPorfolio = self.safe_dict(transfer, 'from_portfolio', {}) + fromId = self.safe_string(fromPorfolio, 'id') + toPorfolio = self.safe_dict(transfer, 'to_portfolio', {}) + toId = self.safe_string(toPorfolio, 'id') + return { + 'info': transfer, + 'id': self.safe_string(transfer, 'transfer_uuid'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'currency': code, + 'amount': self.safe_number(transfer, 'amount'), + 'fromAccount': fromId, + 'toAccount': toId, + 'status': self.parse_transfer_status(self.safe_string(transfer, 'status')), + } + + def parse_transfer_status(self, status: Str) -> Str: + statuses: dict = { + 'FAILED': 'failed', + 'PROCESSED': 'ok', + 'NEW': 'pending', + 'STARTED': 'pending', + } + return self.safe_string(statuses, status, status) + + def create_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + create a currency deposit address + + https://docs.cloud.coinbase.com/intx/reference/createaddress + https://docs.cloud.coinbase.com/intx/reference/createcounterpartyid + + :param str code: unified currency code of the currency for the deposit address + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.network_arn_id]: Identifies the blockchain network(e.g., networks/ethereum-mainnet/assets/313ef8a9-ae5a-5f2f-8a56-572c0e2a4d5a) if not provided will pick default + :param str [params.network]: unified network code to identify the blockchain network + :returns dict: an `address structure ` + """ + self.load_markets() + method = None + method, params = self.handle_option_and_params(params, 'createDepositAddress', 'method', 'v1PrivatePostTransfersAddress') + portfolio = None + portfolio, params = self.handle_portfolio_and_params('createDepositAddress', params) + request: dict = { + 'portfolio': portfolio, + } + if method == 'v1PrivatePostTransfersAddress': + currency = self.currency(code) + request['asset'] = currency['id'] + networkId = None + networkId, params = self.handle_network_id_and_params(code, 'createDepositAddress', params) + request['network_arn_id'] = networkId + response = getattr(self, method)(self.extend(request, params)) + # + # v1PrivatePostTransfersAddress + # { + # address: "3LkwYscRyh6tUR1XTqXSJQoJnK7ucC1F4n", + # network_arn_id: "networks/bitcoin-mainnet/assets/6ecc0dcc-10a2-500e-b315-a3b9abae19ce", + # destination_tag: "", + # } + # v1PrivatePostTransfersCreateCounterpartyId + # { + # "portfolio_uuid":"018e0a8b-6b6b-70e0-9689-1e7926c2c8bc", + # "counterparty_id":"CB2ZPUCZBE" + # } + # + tag = self.safe_string(response, 'destination_tag') + address = self.safe_string_2(response, 'address', 'counterparty_id') + return { + 'currency': code, + 'tag': tag, + 'address': address, + 'network': None, + 'info': response, + } + + def find_default_network(self, networks): + networksArray = self.to_array(networks) + for i in range(0, len(networksArray)): + info = networksArray[i]['info'] + is_default = self.safe_bool(info, 'is_default', False) + if is_default is True: + return networksArray[i] + return networksArray[0] + + def load_currency_networks(self, code, params={}): + currency = self.currency(code) + networks = self.safe_dict(currency, 'networks') + if networks is not None: + return False + request: dict = { + 'asset': currency['id'], + } + rawNetworks = self.v1PublicGetAssetsAssetNetworks(request) + # + # [ + # { + # "asset_id":"1", + # "asset_uuid":"2b92315d-eab7-5bef-84fa-089a131333f5", + # "asset_name":"USDC", + # "network_arn_id":"networks/ethereum-mainnet/assets/9bc140b4-69c3-5fc9-bd0d-b041bcf40039", + # "min_withdrawal_amt":"1", + # "max_withdrawal_amt":"100000000", + # "network_confirms":35, + # "processing_time":485, + # "is_default":true, + # "network_name":"ethereum", + # "display_name":"Ethereum" + # }, + # .... + # ] + # + currency['networks'] = self.parse_networks(rawNetworks) + return True + + def parse_networks(self, networks, params={}): + result: dict = {} + for i in range(0, len(networks)): + network = self.extend(self.parse_network(networks[i]), params) + result[network['network']] = network + return result + + def parse_network(self, network, params={}): + # + # { + # "asset_id":"1", + # "asset_uuid":"2b92315d-eab7-5bef-84fa-089a131333f5", + # "asset_name":"USDC", + # "network_arn_id":"networks/ethereum-mainnet/assets/9bc140b4-69c3-5fc9-bd0d-b041bcf40039", + # "min_withdrawal_amt":"1", + # "max_withdrawal_amt":"100000000", + # "network_confirms":35, + # "processing_time":485, + # "is_default":true, + # "network_name":"ethereum", + # "display_name":"Ethereum" + # } + # + currencyId = self.safe_string(network, 'asset_name') + currencyCode = self.safe_currency_code(currencyId) + networkId = self.safe_string(network, 'network_arn_id') + networkIdForCode = self.safe_string_n(network, ['network_name', 'display_name', 'network_arn_id'], '') + return self.safe_network({ + 'info': network, + 'id': networkId, + 'name': self.safe_string(network, 'display_name'), + 'network': self.network_id_to_code(networkIdForCode, currencyCode), + 'active': None, + 'deposit': None, + 'withdraw': None, + 'precision': None, + 'fee': None, + 'limits': { + 'withdraw': { + 'min': self.safe_number(network, 'min_withdrawal_amt'), + 'max': self.safe_number(network, 'max_withdrawal_amt'), + }, + 'deposit': { + 'min': None, + 'max': None, + }, + }, + }) + + def set_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + Either adds or reduces margin in order to set the margin to a specific value + + https://docs.cloud.coinbase.com/intx/reference/setportfoliomarginoverride + + :param str symbol: unified market symbol of the market to set margin in + :param float amount: the amount to set the margin to + :param dict [params]: parameters specific to the exchange API endpoint + :returns dict: A `margin structure ` + """ + portfolio = None + portfolio, params = self.handle_portfolio_and_params('setMargin', params) + if symbol is not None: + raise BadRequest(self.id + ' setMargin() only allows setting margin to full portfolio') + request: dict = { + 'portfolio': portfolio, + 'margin_override': amount, + } + return self.v1PrivatePostPortfoliosMargin(self.extend(request, params)) + + def fetch_deposits_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch history of deposits and withdrawals + + https://docs.cloud.coinbase.com/intx/reference/gettransfers + + :param str [code]: unified currency code for the currency of the deposit/withdrawals, default is None + :param int [since]: timestamp in ms of the earliest deposit/withdrawal, default is None + :param int [limit]: max number of deposit/withdrawals to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.portfolios]: Identifies the portfolios by UUID(e.g., 892e8c7c-e979-4cad-b61b-55a197932cf1) or portfolio ID(e.g., 5189861793641175). Can provide single or multiple portfolios to filter by or fetches transfers for all portfolios if none are provided. + :param int [params.until]: Only find transfers updated before self time. Use timestamp format + :param str [params.status]: The current status of transfer. Possible values: [PROCESSED, NEW, FAILED, STARTED] + :param str [params.type]: The type of transfer Possible values: [DEPOSIT, WITHDRAW, REBATE, STIPEND, INTERNAL, FUNDING] + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict: a list of `transaction structures ` + """ + self.load_markets() + paginate = None + paginate, params = self.handle_option_and_params(params, 'fetchDepositsWithdrawals', 'paginate') + maxEntriesPerRequest = None + maxEntriesPerRequest, params = self.handle_option_and_params(params, 'fetchDepositsWithdrawals', 'maxEntriesPerRequest', 100) + pageKey = 'ccxtPageKey' + if paginate: + return self.fetch_paginated_call_incremental('fetchDepositsWithdrawals', code, since, limit, params, pageKey, maxEntriesPerRequest) + page = self.safe_integer(params, pageKey, 1) - 1 + request: dict = { + 'result_offset': self.safe_integer_2(params, 'offset', 'result_offset', page * maxEntriesPerRequest), + } + if since is not None: + request['time_from'] = self.iso8601(since) + if limit is not None: + newLimit = min(limit, 100) + request['result_limit'] = newLimit + portfolios = None + portfolios, params = self.handle_option_and_params(params, 'fetchDepositsWithdrawals', 'portfolios') + if portfolios is not None: + request['portfolios'] = portfolios + until = None + until, params = self.handle_option_and_params(params, 'fetchDepositsWithdrawals', 'until') + if until is not None: + request['time_to'] = self.iso8601(until) + response = self.v1PrivateGetTransfers(self.extend(request, params)) + # + # { + # "pagination":{ + # "result_limit":25, + # "result_offset":0 + # }, + # "results":[ + # { + # "transfer_uuid":"8e471d77-4208-45a8-9e5b-f3bd8a2c1fc3", + # "transfer_type":"WITHDRAW", + # "amount":"1.000000", + # "asset":"USDC", + # "status":"PROCESSED", + # "network_name":"ethereum", + # "created_at":"2024-03-14T02:32:18.497795Z", + # "updated_at":"2024-03-14T02:35:38.514588Z", + # "from_portfolio":{ + # "id":"1yun54bb-1-6", + # "uuid":"018e0a8b-6b6b-70e0-9689-1e7926c2c8bc", + # "name":"fungus technology o?Portfolio" + # }, + # "to_address":"0xcdcE79F820BE9d6C5033db5c31d1AE3A8c2399bB" + # } + # ] + # } + # + rawTransactions = self.safe_list(response, 'results', []) + return self.parse_transactions(rawTransactions) + + def fetch_position(self, symbol: str, params={}): + """ + + https://docs.cloud.coinbase.com/intx/reference/getportfolioposition + + fetch data on an open position + :param str symbol: unified market symbol of the market the position is held in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `position structure ` + """ + self.load_markets() + symbol = self.symbol(symbol) + portfolio = None + portfolio, params = self.handle_portfolio_and_params('fetchPosition', params) + request: dict = { + 'portfolio': portfolio, + 'instrument': self.market_id(symbol), + } + position = self.v1PrivateGetPortfoliosPortfolioPositionsInstrument(self.extend(request, params)) + # + # { + # "symbol":"BTC-PERP", + # "instrument_id":"114jqr89-0-0", + # "instrument_uuid":"b3469e0b-222c-4f8a-9f68-1f9e44d7e5e0", + # "vwap":"52482.3", + # "net_size":"0", + # "buy_order_size":"0.001", + # "sell_order_size":"0", + # "im_contribution":"0.2", + # "unrealized_pnl":"0", + # "mark_price":"52406.8", + # "entry_vwap":"52472.9" + # } + # + return self.parse_position(position) + + def parse_position(self, position: dict, market: Market = None): + # + # { + # "symbol":"BTC-PERP", + # "instrument_id":"114jqr89-0-0", + # "instrument_uuid":"b3469e0b-222c-4f8a-9f68-1f9e44d7e5e0", + # "vwap":"52482.3", + # "net_size":"0", + # "buy_order_size":"0.001", + # "sell_order_size":"0", + # "im_contribution":"0.2", + # "unrealized_pnl":"0", + # "mark_price":"52406.8", + # "entry_vwap":"52472.9" + # } + # + marketId = self.safe_string(position, 'symbol') + quantity = self.safe_string(position, 'net_size') + market = self.safe_market(marketId, market, '-') + side = 'long' + if Precise.string_le(quantity, '0'): + side = 'short' + quantity = Precise.string_mul('-1', quantity) + return self.safe_position({ + 'info': position, + 'id': self.safe_string(position, 'id'), + 'symbol': market['symbol'], + 'entryPrice': None, + 'markPrice': self.safe_number(position, 'mark_price'), + 'notional': None, + 'collateral': None, + 'unrealizedPnl': self.safe_number(position, 'unrealized_pnl'), + 'side': side, + 'contracts': self.parse_number(quantity), + 'contractSize': self.safe_number(market, 'contractSize'), + 'timestamp': None, + 'datetime': None, + 'hedged': None, + 'maintenanceMargin': None, + 'maintenanceMarginPercentage': None, + 'initialMargin': self.safe_number(position, 'im_contribution'), + 'initialMarginPercentage': None, + 'leverage': None, + 'liquidationPrice': None, + 'marginRatio': None, + 'marginMode': None, + 'percentage': None, + }) + + def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + + https://docs.cloud.coinbase.com/intx/reference/getportfoliopositions + + fetch all open positions + :param str[] [symbols]: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + self.load_markets() + portfolio = None + portfolio, params = self.handle_portfolio_and_params('fetchPositions', params) + request: dict = { + 'portfolio': portfolio, + } + response = self.v1PrivateGetPortfoliosPortfolioPositions(self.extend(request, params)) + # + # [ + # { + # "symbol":"BTC-PERP", + # "instrument_id":"114jqr89-0-0", + # "instrument_uuid":"b3469e0b-222c-4f8a-9f68-1f9e44d7e5e0", + # "vwap":"52482.3", + # "net_size":"0", + # "buy_order_size":"0.001", + # "sell_order_size":"0", + # "im_contribution":"0.2", + # "unrealized_pnl":"0", + # "mark_price":"52406.8", + # "entry_vwap":"52472.9" + # } + # ] + # + positions = self.parse_positions(response) + if self.is_empty(symbols): + return positions + symbols = self.market_symbols(symbols) + return self.filter_by_array_positions(positions, 'symbol', symbols, False) + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://docs.cloud.coinbase.com/intx/reference/gettransfers + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.portfolios]: Identifies the portfolios by UUID(e.g., 892e8c7c-e979-4cad-b61b-55a197932cf1) or portfolio ID(e.g., 5189861793641175). Can provide single or multiple portfolios to filter by or fetches transfers for all portfolios if none are provided. + :param int [params.until]: Only find transfers updated before self time. Use timestamp format + :param str [params.status]: The current status of transfer. Possible values: [PROCESSED, NEW, FAILED, STARTED] + :param str [params.type]: The type of transfer Possible values: [DEPOSIT, WITHDRAW, REBATE, STIPEND, INTERNAL, FUNDING] + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + params['type'] = 'WITHDRAW' + return self.fetch_deposits_withdrawals(code, since, limit, params) + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.portfolios]: Identifies the portfolios by UUID(e.g., 892e8c7c-e979-4cad-b61b-55a197932cf1) or portfolio ID(e.g., 5189861793641175). Can provide single or multiple portfolios to filter by or fetches transfers for all portfolios if none are provided. + :param int [params.until]: Only find transfers updated before self time. Use timestamp format + :param str [params.status]: The current status of transfer. Possible values: [PROCESSED, NEW, FAILED, STARTED] + :param str [params.type]: The type of transfer Possible values: [DEPOSIT, WITHDRAW, REBATE, STIPEND, INTERNAL, FUNDING] + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + params['type'] = 'DEPOSIT' + return self.fetch_deposits_withdrawals(code, since, limit, params) + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'PROCESSED': 'ok', + 'NEW': 'pending', + 'STARTED': 'pending', + 'FAILED': 'canceled', + } + return self.safe_string(statuses, status, status) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # { + # "idem":"8e471d77-4208-45a8-9e5b-f3bd8a2c1fc3" + # } + # transactionType = self.safe_string(transaction, 'type') + datetime = self.safe_string(transaction, 'updated_at') + fromPorfolio = self.safe_dict(transaction, 'from_portfolio', {}) + addressFrom = self.safe_string_n(transaction, ['from_address', 'from_cb_account', self.safe_string_n(fromPorfolio, ['id', 'uuid', 'name']), 'from_counterparty_id']) + toPorfolio = self.safe_dict(transaction, 'from_portfolio', {}) + addressTo = self.safe_string_n(transaction, ['to_address', 'to_cb_account', self.safe_string_n(toPorfolio, ['id', 'uuid', 'name']), 'to_counterparty_id']) + return { + 'info': transaction, + 'id': self.safe_string(transaction, 'transfer_uuid'), + 'txid': self.safe_string(transaction, 'transaction_uuid'), + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + 'network': self.network_id_to_code(self.safe_string(transaction, 'network_name')), + 'address': None, # TODO check if withdraw or deposit and populate + 'addressTo': addressTo, + 'addressFrom': addressFrom, + 'tag': None, + 'tagTo': None, + 'tagFrom': None, + 'type': self.safe_string(transaction, 'resource'), + 'amount': self.safe_number(transaction, 'amount'), + 'currency': self.safe_currency_code(self.safe_string(transaction, 'asset'), currency), + 'status': self.parse_transaction_status(self.safe_string(transaction, 'status')), + 'updated': self.parse8601(datetime), + 'fee': { + 'cost': None, + 'currency': None, + }, + } + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # { + # "portfolio_id":"1wp37qsc-1-0", + # "portfolio_uuid":"018d7f6c-b92c-7361-8b7e-2932711e5a22", + # "portfolio_name":"CCXT Portfolio 020624-17:16", + # "fill_id":"1xbfy19y-1-184", + # "exec_id":"280841526207070392", + # "order_id":"1xbfv8yw-1-0", + # "instrument_id":"114jqr89-0-0", + # "instrument_uuid":"b3469e0b-222c-4f8a-9f68-1f9e44d7e5e0", + # "symbol":"BTC-PERP", + # "match_id":"280841526207053840", + # "fill_price":"52500", + # "fill_qty":"0.01", + # "client_id":"1x59ctku-1-1", + # "client_order_id":"ccxt3e4e2a5f-4a89-", + # "order_qty":"0.01", + # "limit_price":"52500", + # "total_filled":"0.01", + # "filled_vwap":"52500", + # "expire_time":"", + # "stop_price":"", + # "side":"BUY", + # "tif":"GTC", + # "stp_mode":"BOTH", + # "flags":"", + # "fee":"0.105", + # "fee_asset":"USDC", + # "order_status":"DONE", + # "event_time":"2024-02-15T00:43:57.631Z" + # } + # + marketId = self.safe_string(trade, 'symbol') + datetime = self.safe_string(trade, 'event_time') + return self.safe_trade({ + 'info': trade, + 'id': self.safe_string_2(trade, 'fill_id', 'exec_id'), + 'order': self.safe_string(trade, 'order_id'), + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + 'symbol': self.safe_symbol(marketId, market), + 'type': None, + 'side': self.safe_string_lower(trade, 'side'), + 'takerOrMaker': None, + 'price': self.safe_number(trade, 'fill_price'), + 'amount': self.safe_number(trade, 'fill_qty'), + 'cost': None, + 'fee': { + 'cost': self.safe_number(trade, 'fee'), + 'currency': self.safe_currency_code(self.safe_string(trade, 'fee_asset')), + }, + }) + + def fetch_markets(self, params={}) -> List[Market]: + """ + + https://docs.cloud.coinbase.com/intx/reference/getinstruments + + retrieves data on all markets for coinbaseinternational + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = self.v1PublicGetInstruments(params) + # + # [ + # { + # "instrument_id":"149264164756389888", + # "instrument_uuid":"e9360798-6a10-45d6-af05-67c30eb91e2d", + # "symbol":"ETH-PERP", + # "type":"PERP", + # "base_asset_id":"118059611793145856", + # "base_asset_uuid":"d85dce9b-5b73-5c3c-8978-522ce1d1c1b4", + # "base_asset_name":"ETH", + # "quote_asset_id":"1", + # "quote_asset_uuid":"2b92315d-eab7-5bef-84fa-089a131333f5", + # "quote_asset_name":"USDC", + # "base_increment":"0.0001", + # "quote_increment":"0.01", + # "price_band_percent":"0.02", + # "market_order_percent":"0.0075", + # "qty_24hr":"44434.8131", + # "notional_24hr":"110943454.279785", + # "avg_daily_qty":"1099171.6025", + # "avg_daily_notional":"2637240145.456987", + # "previous_day_qty":"78909.3939", + # "open_interest":"1270.749", + # "position_limit_qty":"1831.9527", + # "position_limit_adq_pct":"0.05", + # "replacement_cost":"0.23", + # "base_imf":"0.1", + # "min_notional_value":"10", + # "funding_interval":"3600000000000", + # "trading_state":"TRADING", + # "quote":{ + # "best_bid_price":"2490.8", + # "best_bid_size":"9.0515", + # "best_ask_price":"2490.81", + # "best_ask_size":"4.8486", + # "trade_price":"2490.39", + # "trade_qty":"0.9508", + # "index_price":"2490.5", + # "mark_price":"2490.8", + # "settlement_price":"2490.81", + # "limit_up":"2615.42", + # "limit_down":"2366.34", + # "predicted_funding":"0.000009", + # "timestamp":"2024-02-10T16:07:39.454Z" + # } + # }, + # ... + # ] + # + return self.parse_markets(response) + + def parse_market(self, market: dict) -> Market: + # + # { + # "instrument_id":"149264164756389888", + # "instrument_uuid":"e9360798-6a10-45d6-af05-67c30eb91e2d", + # "symbol":"ETH-PERP", + # "type":"PERP", + # "base_asset_id":"118059611793145856", + # "base_asset_uuid":"d85dce9b-5b73-5c3c-8978-522ce1d1c1b4", + # "base_asset_name":"ETH", + # "quote_asset_id":"1", + # "quote_asset_uuid":"2b92315d-eab7-5bef-84fa-089a131333f5", + # "quote_asset_name":"USDC", + # "base_increment":"0.0001", + # "quote_increment":"0.01", + # "price_band_percent":"0.02", + # "market_order_percent":"0.0075", + # "qty_24hr":"44434.8131", + # "notional_24hr":"110943454.279785", + # "avg_daily_qty":"1099171.6025", + # "avg_daily_notional":"2637240145.456987", + # "previous_day_qty":"78909.3939", + # "open_interest":"1270.749", + # "position_limit_qty":"1831.9527", + # "position_limit_adq_pct":"0.05", + # "replacement_cost":"0.23", + # "base_imf":"0.1", + # "min_notional_value":"10", + # "funding_interval":"3600000000000", + # "trading_state":"TRADING", + # "quote":{ + # "best_bid_price":"2490.8", + # "best_bid_size":"9.0515", + # "best_ask_price":"2490.81", + # "best_ask_size":"4.8486", + # "trade_price":"2490.39", + # "trade_qty":"0.9508", + # "index_price":"2490.5", + # "mark_price":"2490.8", + # "settlement_price":"2490.81", + # "limit_up":"2615.42", + # "limit_down":"2366.34", + # "predicted_funding":"0.000009", + # "timestamp":"2024-02-10T16:07:39.454Z" + # } + # } + # + marketId = self.safe_string(market, 'symbol') + baseId = self.safe_string(market, 'base_asset_name') + quoteId = self.safe_string(market, 'quote_asset_name') + typeId = self.safe_string(market, 'type') # 'SPOT', 'PERP' + isSpot = (typeId == 'SPOT') + fees = self.fees + symbol = baseId + '/' + quoteId + settleId = None + if not isSpot: + settleId = quoteId + symbol += ':' + quoteId + return { + 'id': marketId, + 'lowercaseId': marketId.lower(), + 'symbol': symbol, + 'base': baseId, + 'quote': quoteId, + 'settle': settleId if settleId else None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId if settleId else None, + 'type': 'spot' if isSpot else 'swap', + 'spot': isSpot, + 'margin': False, + 'swap': not isSpot, + 'future': False, + 'option': False, + 'active': self.safe_string(market, 'trading_state') == 'TRADING', + 'contract': not isSpot, + 'linear': None if isSpot else (settleId == quoteId), + 'inverse': None if isSpot else (settleId != quoteId), + 'taker': fees['trading']['taker'], + 'maker': fees['trading']['maker'], + 'contractSize': None if isSpot else 1, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.safe_number(market, 'base_increment'), + 'price': self.safe_number(market, 'quote_increment'), + 'cost': self.safe_number(market, 'quote_increment'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': self.safe_number(market, 'base_imf'), + }, + 'amount': { + 'min': None, + 'max': None if isSpot else self.safe_number(market, 'position_limit_qty'), + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': self.safe_number(market, 'min_notional_value'), + 'max': None, + }, + }, + 'info': market, + 'created': None, + } + + def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://docs.cloud.coinbase.com/intx/reference/getassets + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + currencies = self.v1PublicGetAssets(params) + # + # [ + # { + # "asset_id":"1", + # "asset_uuid":"2b92315d-eab7-5bef-84fa-089a131333f5", + # "asset_name":"USDC", + # "status":"ACTIVE", + # "collateral_weight":1.0, + # "supported_networks_enabled":true + # }, + # ... + # ] + # + return self.parse_currencies(currencies) + + def parse_currency(self, currency: dict) -> Currency: + # + # { + # "asset_id":"1", + # "asset_uuid":"2b92315d-eab7-5bef-84fa-089a131333f5", + # "asset_name":"USDC", + # "status":"ACTIVE", + # "collateral_weight":1.0, + # "supported_networks_enabled":true + # } + # + id = self.safe_string(currency, 'asset_name') + code = self.safe_currency_code(id) + statusId = self.safe_string(currency, 'status') + return self.safe_currency_structure({ + 'id': id, + 'name': code, + 'code': code, + 'precision': None, + 'info': currency, + 'active': (statusId == 'ACTIVE'), + 'deposit': None, + 'withdraw': None, + 'networks': None, + 'fee': None, + 'fees': None, + 'limits': self.limits, + }) + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://docs.cloud.coinbase.com/intx/reference/getinstruments + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + symbols = self.market_symbols(symbols) + instruments = self.v1PublicGetInstruments(params) + tickers: dict = {} + for i in range(0, len(instruments)): + instrument = instruments[i] + marketId = self.safe_string(instrument, 'symbol') + symbol = self.safe_symbol(marketId) + quote = self.safe_dict(instrument, 'quote', {}) + tickers[symbol] = self.parse_ticker(quote, self.safe_market(marketId)) + return self.filter_by_array(tickers, 'symbol', symbols, True) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://docs.cloud.coinbase.com/intx/reference/getinstrumentquote + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'instrument': self.market_id(symbol), + } + ticker = self.v1PublicGetInstrumentsInstrumentQuote(self.extend(request, params)) + return self.parse_ticker(ticker, market) + + def parse_ticker(self, ticker: object, market: Market = None) -> Ticker: + # + # { + # "best_bid_price":"2490.8", + # "best_bid_size":"9.0515", + # "best_ask_price":"2490.81", + # "best_ask_size":"4.8486", + # "trade_price":"2490.39", + # "trade_qty":"0.9508", + # "index_price":"2490.5", + # "mark_price":"2490.8", + # "settlement_price":"2490.81", + # "limit_up":"2615.42", + # "limit_down":"2366.34", + # "predicted_funding":"0.000009", + # "timestamp":"2024-02-10T16:07:39.454Z" + # } + # + datetime = self.safe_string(ticker, 'timestamp') + return self.safe_ticker({ + 'info': ticker, + 'symbol': self.safe_symbol(None, market), + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + 'bid': self.safe_number(ticker, 'best_bid_price'), + 'bidVolume': self.safe_number(ticker, 'best_bid_size'), + 'ask': self.safe_number(ticker, 'best_ask_price'), + 'askVolume': self.safe_number(ticker, 'best_ask_size'), + 'high': None, + 'low': None, + 'open': None, + 'close': None, + 'last': None, + 'change': None, + 'percentage': None, + 'average': None, + 'vwap': None, + 'baseVolume': None, + 'quoteVolume': None, + 'previousClose': None, + 'markPrice': self.safe_number(ticker, 'mark_price'), + 'indexPrice': self.safe_number(ticker, 'index_price'), + }) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://docs.cloud.coinbase.com/intx/reference/getportfoliobalances + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.v3]: default False, set True to use v3 api endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + portfolio = None + portfolio, params = self.handle_portfolio_and_params('fetchBalance', params) + request: dict = { + 'portfolio': portfolio, + } + balances = self.v1PrivateGetPortfoliosPortfolioBalances(self.extend(request, params)) + # + # [ + # { + # "asset_id":"0-0-1", + # "asset_name":"USDC", + # "asset_uuid":"2b92315d-eab7-5bef-84fa-089a131333f5", + # "quantity":"500000.0000000000", + # "hold":"0", + # "hold_available_for_collateral":"0", + # "transfer_hold":"0", + # "collateral_value":"500000.0", + # "max_withdraw_amount":"500000.0000000000", + # "loan":"0", + # "loan_collateral_requirement":"0.0" + # } + # ] + # + return self.parse_balance(balances) + + def parse_balance(self, response) -> Balances: + # + # { + # "asset_id":"0-0-1", + # "asset_name":"USDC", + # "asset_uuid":"2b92315d-eab7-5bef-84fa-089a131333f5", + # "quantity":"500000.0000000000", + # "hold":"0", + # "hold_available_for_collateral":"0", + # "transfer_hold":"0", + # "collateral_value":"500000.0", + # "max_withdraw_amount":"500000.0000000000", + # "loan":"0", + # "loan_collateral_requirement":"0.0" + # } + # + result: dict = { + 'info': response, + } + for i in range(0, len(response)): + rawBalance = response[i] + currencyId = self.safe_string(rawBalance, 'asset_name') + code = self.safe_currency_code(currencyId) + account = self.account() + account['total'] = self.safe_string(rawBalance, 'quantity') + account['used'] = self.safe_string(rawBalance, 'hold') + result[code] = account + return self.safe_balance(result) + + def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + Transfer an amount of asset from one portfolio to another. + + https://docs.cloud.coinbase.com/intx/reference/createportfolioassettransfer + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account to transfer from + :param str toAccount: account to transfer to + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transfer structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'asset': currency['id'], + 'ammount': amount, + 'from': fromAccount, + 'to': toAccount, + } + response = self.v1PrivatePostPortfoliosTransfer(self.extend(request, params)) + success = self.safe_bool(response, 'success') + return { + 'info': response, + 'id': None, + 'timestamp': None, + 'datetime': None, + 'currency': code, + 'amount': amount, + 'fromAccount': fromAccount, + 'toAccount': toAccount, + 'status': 'ok' if success else 'failed', + } + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: float = None, params={}): + """ + create a trade order + + https://docs.cloud.coinbase.com/intx/reference/createorder + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency, quote currency for 'market' 'buy' orders + :param float [price]: the price to fulfill the order, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.stopPrice]: alias for triggerPrice + :param float [params.triggerPrice]: price to trigger stop orders + :param float [params.stopLossPrice]: price to trigger stop-loss orders + :param bool [params.postOnly]: True or False + :param str [params.tif]: 'GTC', 'IOC', 'GTD' default is 'GTC' for limit orders and 'IOC' for market orders + :param str [params.expire_time]: The expiration time required for orders with the time in force set to GTT. Must not go beyond 30 days of the current time. Uses ISO-8601 format(e.g., 2023-03-16T23:59:53Z) + :param str [params.stp_mode]: Possible values: [NONE, AGGRESSING, BOTH] Specifies the behavior for self match handling. None disables the functionality, new cancels the newest order, and both cancels both orders. + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + typeId = type.upper() + triggerPrice = self.safe_number_n(params, ['triggerPrice', 'stopPrice', 'stop_price']) + clientOrderIdprefix = self.safe_string(self.options, 'brokerId', 'nfqkvdjp') + clientOrderId = clientOrderIdprefix + '-' + self.uuid() + clientOrderId = clientOrderId[0:17] + request: dict = { + 'client_order_id': clientOrderId, + 'side': side.upper(), + 'instrument': market['id'], + 'size': self.amount_to_precision(market['symbol'], amount), + } + if triggerPrice is not None: + if type == 'limit': + typeId = 'STOP_LIMIT' + else: + typeId = 'STOP' + request['stop_price'] = triggerPrice + request['type'] = typeId + if type == 'limit': + if price is None: + raise InvalidOrder(self.id + ' createOrder() requires a price parameter for a limit order types') + request['price'] = price + portfolio = None + portfolio, params = self.handle_portfolio_and_params('createOrder', params) + if portfolio is not None: + request['portfolio'] = portfolio + postOnly = self.safe_bool_2(params, 'postOnly', 'post_only') + tif = self.safe_string_2(params, 'tif', 'timeInForce') + # market orders must be IOC + if typeId == 'MARKET': + if tif is not None and tif != 'IOC': + raise InvalidOrder(self.id + ' createOrder() market orders must have tif set to "IOC"') + tif = 'IOC' + else: + tif = 'GTC' if (tif is None) else tif + if postOnly is not None: + request['post_only'] = postOnly + request['tif'] = tif + params = self.omit(params, ['client_order_id', 'user', 'postOnly', 'timeInForce']) + response = self.v1PrivatePostOrders(self.extend(request, params)) + # + # { + # "order_id":"1x96skvg-1-0", + # "client_order_id":"ccxt", + # "side":"BUY", + # "instrument_id":"114jqr89-0-0", + # "instrument_uuid":"b3469e0b-222c-4f8a-9f68-1f9e44d7e5e0", + # "symbol":"BTC-PERP", + # "portfolio_id":"1wp37qsc-1-0", + # "portfolio_uuid":"018d7f6c-b92c-7361-8b7e-2932711e5a22", + # "type":"LIMIT", + # "price":"10000", + # "size":"0.001", + # "tif":"GTC", + # "stp_mode":"BOTH", + # "event_type":"NEW", + # "order_status":"WORKING", + # "leaves_qty":"0.001", + # "exec_qty":"0", + # "avg_price":"0", + # "fee":"0" + # } + # + return self.parse_order(response, market) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # { + # "order_id":"1x96skvg-1-0", + # "client_order_id":"ccxt", + # "side":"BUY", + # "instrument_id":"114jqr89-0-0", + # "instrument_uuid":"b3469e0b-222c-4f8a-9f68-1f9e44d7e5e0", + # "symbol":"BTC-PERP", + # "portfolio_id":"1wp37qsc-1-0", + # "portfolio_uuid":"018d7f6c-b92c-7361-8b7e-2932711e5a22", + # "type":"LIMIT", + # "price":"10000", + # "size":"0.001", + # "tif":"GTC", + # "stp_mode":"BOTH", + # "event_type":"NEW", + # "order_status":"WORKING", + # "leaves_qty":"0.001", + # "exec_qty":"0", + # "avg_price":"0", + # "fee":"0" + # } + # + marketId = self.safe_string(order, 'symbol') + feeCost = self.safe_number(order, 'fee') + fee = None + if feeCost is not None: + fee = { + 'cost': feeCost, + } + datetime = self.safe_string_2(order, 'submit_time', 'event_time') + return self.safe_order({ + 'info': order, + 'id': self.safe_string(order, 'order_id'), + 'clientOrderId': self.safe_string(order, 'client_order_id'), + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + 'lastTradeTimestamp': None, + 'symbol': self.safe_symbol(marketId, market), + 'type': self.parse_order_type(self.safe_string(order, 'type')), + 'timeInForce': self.safe_string(order, 'tif'), + 'postOnly': None, + 'side': self.safe_string_lower(order, 'side'), + 'price': self.safe_string(order, 'price'), + 'triggerPrice': self.safe_string(order, 'stop_price'), + 'amount': self.safe_string(order, 'size'), + 'filled': self.safe_string(order, 'exec_qty'), + 'remaining': self.safe_string(order, 'leaves_qty'), + 'cost': None, + 'average': self.safe_string(order, 'avg_price'), + 'status': self.parse_order_status(self.safe_string(order, 'order_status')), + 'fee': fee, + 'trades': None, + }, market) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'NEW': 'open', + 'PARTIAL_FILLED': 'open', + 'FILLED': 'closed', + 'CANCELED': 'canceled', + 'REPLACED': 'canceled', + 'PENDING_CANCEL': 'open', + 'REJECTED': 'rejected', + 'PENDING_NEW': 'open', + 'EXPIRED': 'expired', + 'PENDING_REPLACE': 'open', + } + return self.safe_string(statuses, status, status) + + def parse_order_type(self, type: Str): + if type == 'UNKNOWN_ORDER_TYPE': + return None + types: dict = { + 'MARKET': 'market', + 'LIMIT': 'limit', + 'STOP': 'limit', + 'STOP_LIMIT': 'limit', + } + return self.safe_string(types, type, type) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://docs.cloud.coinbase.com/intx/reference/cancelorder + + :param str id: order id + :param str symbol: not used by coinbaseinternational cancelOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + portfolio = None + portfolio, params = self.handle_portfolio_and_params('cancelOrder', params) + request: dict = { + 'portfolio': portfolio, + 'id': id, + } + market = None + if symbol is not None: + market = self.market(symbol) + orders = self.v1PrivateDeleteOrdersId(self.extend(request, params)) + # + # { + # "order_id":"1x96skvg-1-0", + # "client_order_id":"ccxt", + # "side":"BUY", + # "instrument_id":"114jqr89-0-0", + # "instrument_uuid":"b3469e0b-222c-4f8a-9f68-1f9e44d7e5e0", + # "symbol":"BTC-PERP", + # "portfolio_id":"1wp37qsc-1-0", + # "portfolio_uuid":"018d7f6c-b92c-7361-8b7e-2932711e5a22", + # "type":"LIMIT", + # "price":"10000", + # "size":"0.001", + # "tif":"GTC", + # "stp_mode":"BOTH", + # "event_type":"CANCELED", + # "order_status":"DONE", + # "leaves_qty":"0.001", + # "exec_qty":"0", + # "avg_price":"0", + # "fee":"0" + # } + # + return self.parse_order(orders, market) + + def cancel_all_orders(self, symbol: str = None, params={}): + """ + cancel all open orders + :param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + self.load_markets() + portfolio = None + portfolio, params = self.handle_portfolio_and_params('cancelAllOrders', params) + request: dict = { + 'portfolio': portfolio, + } + market = None + if symbol: + market = self.market(symbol) + request['instrument'] = market['id'] + orders = self.v1PrivateDeleteOrders(self.extend(request, params)) + return self.parse_orders(orders, market) + + def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: float = None, price: float = None, params={}): + """ + edit a trade order + + https://docs.cloud.coinbase.com/intx/reference/modifyorder + + :param str id: cancel order id + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str params['clientOrderId']: client order id + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'id': id, + } + portfolio = None + portfolio, params = self.handle_portfolio_and_params('editOrder', params) + if portfolio is not None: + request['portfolio'] = portfolio + if amount is not None: + request['size'] = self.amount_to_precision(symbol, amount) + if price is not None: + request['price'] = self.price_to_precision(symbol, price) + triggerPrice = self.safe_number_n(params, ['stopPrice', 'stop_price', 'triggerPrice']) + if triggerPrice is not None: + request['stop_price'] = triggerPrice + clientOrderId = self.safe_string_2(params, 'client_order_id', 'clientOrderId') + if clientOrderId is None: + raise BadRequest(self.id + ' editOrder() requires a clientOrderId parameter') + request['client_order_id'] = clientOrderId + order = self.v1PrivatePutOrdersId(self.extend(request, params)) + return self.parse_order(order, market) + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://docs.cloud.coinbase.com/intx/reference/modifyorder + + :param str id: the order id + :param str symbol: unified market symbol that the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + portfolio = None + portfolio, params = self.handle_portfolio_and_params('fetchOrder', params) + request: dict = { + 'id': id, + 'portfolio': portfolio, + } + order = self.v1PrivateGetOrdersId(self.extend(request, params)) + # + # { + # "order_id":"1x96skvg-1-0", + # "client_order_id":"ccxt", + # "side":"BUY", + # "instrument_id":"114jqr89-0-0", + # "instrument_uuid":"b3469e0b-222c-4f8a-9f68-1f9e44d7e5e0", + # "symbol":"BTC-PERP", + # "portfolio_id":"1wp37qsc-1-0", + # "portfolio_uuid":"018d7f6c-b92c-7361-8b7e-2932711e5a22", + # "type":"LIMIT", + # "price":"10000", + # "size":"0.001", + # "tif":"GTC", + # "stp_mode":"BOTH", + # "event_type":"NEW", + # "event_time":"2024-02-14T03:25:14Z", + # "submit_time":"2024-02-14T03:25:13.999Z", + # "order_status":"WORKING", + # "leaves_qty":"0.001", + # "exec_qty":"0", + # "avg_price":"0", + # "fee":"0" + # } + # + return self.parse_order(order, market) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on all currently open orders + + https://docs.cloud.coinbase.com/intx/reference/getorders + + :param str symbol: unified market symbol of the orders + :param int [since]: timestamp in ms of the earliest order, default is None + :param int [limit]: the maximum number of open order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param int [params.offset]: offset + :param str [params.event_type]: The most recent type of event that happened to the order. Allowed values: NEW, TRADE, REPLACED + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + portfolio = None + portfolio, params = self.handle_portfolio_and_params('fetchOpenOrders', params) + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOpenOrders', 'paginate') + maxEntriesPerRequest = None + maxEntriesPerRequest, params = self.handle_option_and_params(params, 'fetchOpenOrders', 'maxEntriesPerRequest', 100) + pageKey = 'ccxtPageKey' + if paginate: + return self.fetch_paginated_call_incremental('fetchOpenOrders', symbol, since, limit, params, pageKey, maxEntriesPerRequest) + page = self.safe_integer(params, pageKey, 1) - 1 + request: dict = { + 'portfolio': portfolio, + 'result_offset': self.safe_integer_2(params, 'offset', 'result_offset', page * maxEntriesPerRequest), + } + market = None + if symbol: + market = self.market(symbol) + request['instrument'] = symbol + if limit is not None: + if limit > 100: + raise BadRequest(self.id + ' fetchOpenOrders() maximum limit is 100') + request['result_limit'] = limit + if since is not None: + request['ref_datetime'] = self.iso8601(since) + response = self.v1PrivateGetOrders(self.extend(request, params)) + # + # { + # "pagination":{ + # "result_limit":25, + # "result_offset":0 + # }, + # "results":[ + # { + # "order_id":"1y4cm6b4-1-0", + # "client_order_id":"ccxtd0dd4b5d-8e5f-", + # "side":"SELL", + # "instrument_id":"114jqr89-0-0", + # "instrument_uuid":"b3469e0b-222c-4f8a-9f68-1f9e44d7e5e0", + # "symbol":"BTC-PERP", + # "portfolio_id":"1wp37qsc-1-0", + # "portfolio_uuid":"018d7f6c-b92c-7361-8b7e-2932711e5a22", + # "type":"LIMIT", + # "price":"54000", + # "size":"0.01", + # "tif":"GTC", + # "stp_mode":"BOTH", + # "event_type":"NEW", + # "event_time":"2024-02-24T16:46:37.413Z", + # "submit_time":"2024-02-24T16:46:37.412Z", + # "order_status":"WORKING", + # "leaves_qty":"0.01", + # "exec_qty":"0", + # "avg_price":"0", + # "fee":"0" + # }, + # ... + # ] + # } + # + rawOrders = self.safe_list(response, 'results', []) + return self.parse_orders(rawOrders, market, since, limit) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://docs.cloud.coinbase.com/intx/reference/getmultiportfoliofills + + :param str symbol: unified market symbol of the trades + :param int [since]: timestamp in ms of the earliest order, default is None + :param int [limit]: the maximum number of trade structures to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch trades for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchMyTrades', 'paginate') + pageKey = 'ccxtPageKey' + maxEntriesPerRequest = None + maxEntriesPerRequest, params = self.handle_option_and_params(params, 'fetchMyTrades', 'maxEntriesPerRequest', 100) + if paginate: + return self.fetch_paginated_call_incremental('fetchMyTrades', symbol, since, limit, params, pageKey, maxEntriesPerRequest) + market = None + if symbol is not None: + market = self.market(symbol) + page = self.safe_integer(params, pageKey, 1) - 1 + request: dict = { + 'result_offset': self.safe_integer_2(params, 'offset', 'result_offset', page * maxEntriesPerRequest), + } + if limit is not None: + if limit > 100: + raise BadRequest(self.id + ' fetchMyTrades() maximum limit is 100. Consider setting paginate to True to fetch more trades.') + request['result_limit'] = limit + if since is not None: + request['time_from'] = self.iso8601(since) + until = self.safe_string_n(params, ['until']) + if until is not None: + params = self.omit(params, ['until']) + request['ref_datetime'] = self.iso8601(until) + response = self.v1PrivateGetPortfoliosFills(self.extend(request, params)) + # + # { + # "pagination":{ + # "result_limit":25, + # "result_offset":0 + # }, + # "results":[ + # { + # "portfolio_id":"1wp37qsc-1-0", + # "portfolio_uuid":"018d7f6c-b92c-7361-8b7e-2932711e5a22", + # "portfolio_name":"CCXT Portfolio 020624-17:16", + # "fill_id":"1xbfy19y-1-184", + # "exec_id":"280841526207070392", + # "order_id":"1xbfv8yw-1-0", + # "instrument_id":"114jqr89-0-0", + # "instrument_uuid":"b3469e0b-222c-4f8a-9f68-1f9e44d7e5e0", + # "symbol":"BTC-PERP", + # "match_id":"280841526207053840", + # "fill_price":"52500", + # "fill_qty":"0.01", + # "client_id":"1x59ctku-1-1", + # "client_order_id":"ccxt3e4e2a5f-4a89-", + # "order_qty":"0.01", + # "limit_price":"52500", + # "total_filled":"0.01", + # "filled_vwap":"52500", + # "expire_time":"", + # "stop_price":"", + # "side":"BUY", + # "tif":"GTC", + # "stp_mode":"BOTH", + # "flags":"", + # "fee":"0.105", + # "fee_asset":"USDC", + # "order_status":"DONE", + # "event_time":"2024-02-15T00:43:57.631Z" + # }, + # ] + # } + # + trades = self.safe_list(response, 'results', []) + return self.parse_trades(trades, market, since, limit) + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://docs.cloud.coinbase.com/intx/reference/withdraw + https://docs.cloud.coinbase.com/intx/reference/counterpartywithdraw + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str [tag]: an optional tag for the withdrawal + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.add_network_fee_to_total]: if True, deducts network fee from the portfolio, otherwise deduct fee from the withdrawal + :param str [params.network_arn_id]: Identifies the blockchain network(e.g., networks/ethereum-mainnet/assets/313ef8a9-ae5a-5f2f-8a56-572c0e2a4d5a) + :param str [params.nonce]: a unique integer representing the withdrawal request + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.check_address(address) + self.load_markets() + currency = self.currency(code) + portfolio = None + portfolio, params = self.handle_portfolio_and_params('withdraw', params) + method = None + method, params = self.handle_option_and_params(params, 'withdraw', 'method', 'v1PrivatePostTransfersWithdraw') + networkId = None + networkId, params = self.handle_network_id_and_params(code, 'withdraw', params) + request: dict = { + 'portfolio': portfolio, + 'type': 'send', + 'asset': currency['id'], + 'address': address, + 'amount': amount, + 'currency': currency['id'], + 'network_arn_id': networkId, + 'nonce': self.nonce(), + } + response = getattr(self, method)(self.extend(request, params)) + # + # { + # "idem":"8e471d77-4208-45a8-9e5b-f3bd8a2c1fc3" + # } + # + return self.parse_transaction(response, currency) + + def safe_network(self, network): + withdrawEnabled = self.safe_bool(network, 'withdraw') + depositEnabled = self.safe_bool(network, 'deposit') + limits = self.safe_dict(network, 'limits') + withdraw = self.safe_dict(limits, 'withdraw') + withdrawMax = self.safe_number(withdraw, 'max') + deposit = self.safe_dict(limits, 'deposit') + depositMax = self.safe_number(deposit, 'max') + if withdrawEnabled is None and withdrawMax is not None: + withdrawEnabled = (withdrawMax > 0) + if depositEnabled is None and depositMax is not None: + depositEnabled = (depositMax > 0) + networkId = self.safe_string(network, 'id') + isEnabled = (withdrawEnabled and depositEnabled) + return { + 'info': network['info'], + 'id': networkId, + 'name': self.safe_string(network, 'name'), + 'network': self.safe_string(network, 'network'), + 'active': self.safe_bool(network, 'active', isEnabled), + 'deposit': depositEnabled, + 'withdraw': withdrawEnabled, + 'fee': self.safe_number(network, 'fee'), + 'precision': self.safe_number(network, 'precision'), + 'limits': { + 'withdraw': { + 'min': self.safe_number(withdraw, 'min'), + 'max': withdrawMax, + }, + 'deposit': { + 'min': self.safe_number(deposit, 'min'), + 'max': depositMax, + }, + }, + } + + def sign(self, path, api=[], method='GET', params={}, headers=None, body=None): + version = api[0] + signed = api[1] == 'private' + fullPath = '/' + version + '/' + self.implode_params(path, params) + query = self.omit(params, self.extract_params(path)) + savedPath = '/api' + fullPath + if method == 'GET' or method == 'DELETE': + if query: + fullPath += '?' + self.urlencode_with_array_repeat(query) + url = self.urls['api']['rest'] + fullPath + if signed: + self.check_required_credentials() + nonce = str(self.nonce()) + payload = '' + if method != 'GET': + if query: + body = self.json(query) + payload = body + auth = nonce + method + savedPath + payload + signature = self.hmac(self.encode(auth), self.base64_to_binary(self.secret), hashlib.sha256, 'base64') + headers = { + 'CB-ACCESS-TIMESTAMP': nonce, + 'CB-ACCESS-SIGN': signature, + 'CB-ACCESS-PASSPHRASE': self.password, + 'CB-ACCESS-KEY': self.apiKey, + } + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + # + # { + # "title":"io.javalin.http.BadRequestResponse: Order rejected(DUPLICATE_CLIENT_ORDER_ID - duplicate client order id detected)", + # "status":400 + # } + # + if response is None: + return None # fallback to default error handler + feedback = self.id + ' ' + body + errMsg = self.safe_string(response, 'title') + if errMsg is not None: + self.throw_exactly_matched_exception(self.exceptions['exact'], errMsg, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], errMsg, feedback) + raise ExchangeError(feedback) + return None diff --git a/ccxt/coincatch.py b/ccxt/coincatch.py new file mode 100644 index 0000000..0cdb933 --- /dev/null +++ b/ccxt/coincatch.py @@ -0,0 +1,5178 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.coincatch import ImplicitAPI +import hashlib +import math +import json +from ccxt.base.types import Any, Balances, Bool, Currencies, Currency, DepositAddress, Int, LedgerEntry, Leverage, MarginMode, MarginModification, Market, Num, Order, OrderBook, OrderRequest, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, Trade, Transaction, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import NotSupported +from ccxt.base.errors import DDoSProtection +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import OnMaintenance +from ccxt.base.errors import InvalidNonce +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class coincatch(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(coincatch, self).describe(), { + 'id': 'coincatch', + 'name': 'CoinCatch', + 'countries': ['VG'], # British Virgin Islands + 'rateLimit': 50, # 20 times per second + 'version': 'v1', + 'certified': False, + 'pro': True, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': True, + 'future': False, + 'option': False, + 'addMargin': True, + 'cancelAllOrders': True, + 'cancelAllOrdersAfter': False, + 'cancelOrder': True, + 'cancelOrders': True, + 'cancelWithdraw': False, + 'closePosition': False, + 'createConvertTrade': False, + 'createDepositAddress': False, + 'createLimitBuyOrder': True, + 'createLimitSellOrder': True, + 'createMarketBuyOrder': True, + 'createMarketBuyOrderWithCost': True, + 'createMarketOrder': True, + 'createMarketOrderWithCost': False, + 'createMarketSellOrder': True, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createOrders': True, + 'createOrderWithTakeProfitAndStopLoss': True, + 'createPostOnlyOrder': True, + 'createReduceOnlyOrder': True, + 'createStopLimitOrder': True, + 'createStopLossOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'createTakeProfitOrder': True, + 'createTrailingAmountOrder': False, + 'createTrailingPercentOrder': False, + 'createTriggerOrder': True, + 'fetchAccounts': False, + 'fetchBalance': True, + 'fetchCanceledAndClosedOrders': True, + 'fetchCanceledOrders': False, + 'fetchClosedOrder': False, + 'fetchClosedOrders': False, + 'fetchConvertCurrencies': False, + 'fetchConvertQuote': False, + 'fetchConvertTrade': False, + 'fetchConvertTradeHistory': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': True, + 'fetchDeposits': True, + 'fetchDepositsWithdrawals': False, + 'fetchDepositWithdrawFees': True, + 'fetchFundingHistory': False, + 'fetchFundingRate': True, + 'fetchFundingRateHistory': True, + 'fetchFundingRates': False, + 'fetchIndexOHLCV': False, + 'fetchLedger': True, + 'fetchLeverage': True, + 'fetchLeverageTiers': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': True, + 'fetchMarkets': True, + 'fetchMarkOHLCV': True, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterestHistory': False, + 'fetchOpenOrder': False, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': False, + 'fetchOrderTrades': True, + 'fetchPosition': True, + 'fetchPositionHistory': False, + 'fetchPositionMode': True, + 'fetchPositions': True, + 'fetchPositionsForSymbol': True, + 'fetchPositionsHistory': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchStatus': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': False, + 'fetchTransactions': False, + 'fetchTransfers': False, + 'fetchWithdrawals': True, + 'reduceMargin': True, + 'sandbox': False, + 'setLeverage': True, + 'setMargin': False, + 'setMarginMode': True, + 'setPositionMode': True, + 'transfer': False, + 'withdraw': True, + }, + 'timeframes': { + '1m': '1m', + '3m': '3m', + '5m': '5m', + '15': '15m', + '30': '30m', + '1h': '1H', + '2h': '2H', + '4h': '4H', + '6h': '6H', + '12h': '12H', + '1d': '1D', + '3d': '3D', + '1w': '1W', + '1M': '1M', + }, + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/3d49065f-f05d-4573-88a2-1b5201ec6ff3', + 'api': { + 'public': 'https://api.coincatch.com', + 'private': 'https://api.coincatch.com', + }, + 'www': 'https://www.coincatch.com/', + 'doc': 'https://coincatch.github.io/github.io/en/', + 'fees': 'https://www.coincatch.com/en/rate/', + 'referral': { + 'url': 'https://partner.coincatch.cc/bg/92hy70391729607848548', + 'discount': 0.1, + }, + }, + 'api': { + 'public': { + 'get': { + 'api/spot/v1/public/time': 1, # done + 'api/spot/v1/public/currencies': 20 / 3, # done + 'api/spot/v1/market/ticker': 1, # done + 'api/spot/v1/market/tickers': 1, # done + 'api/spot/v1/market/fills': 2, # not used + 'api/spot/v1/market/fills-history': 2, # done + 'api/spot/v1/market/candles': 1, # done + 'api/spot/v1/market/history-candles': 1, # not used + 'api/spot/v1/market/depth': 1, # not used + 'api/spot/v1/market/merge-depth': 1, # done + 'api/mix/v1/market/contracts': 1, # done + 'api/mix/v1/market/merge-depth': 1, # done + 'api/mix/v1/market/depth': 1, # not used + 'api/mix/v1/market/ticker': 1, # done + 'api/mix/v1/market/tickers': 1, # done + 'api/mix/v1/market/fills': 1, # not used + 'api/mix/v1/market/fills-history': 1, # done + 'api/mix/v1/market/candles': 1, # done + 'pi/mix/v1/market/index': 1, + 'api/mix/v1/market/funding-time': 1, + 'api/mix/v1/market/history-fundRate': 1, # done + 'api/mix/v1/market/current-fundRate': 1, # done + 'api/mix/v1/market/open-interest': 1, + 'api/mix/v1/market/mark-price': 1, + 'api/mix/v1/market/symbol-leverage': 1, # done + 'api/mix/v1/market/queryPositionLever': 1, + }, + }, + 'private': { + 'get': { + 'api/spot/v1/wallet/deposit-address': 4, # done + 'pi/spot/v1/wallet/withdrawal-list': 1, # not used + 'api/spot/v1/wallet/withdrawal-list-v2': 1, # done but should be checked + 'api/spot/v1/wallet/deposit-list': 1, # done + 'api/spot/v1/account/getInfo': 1, + 'api/spot/v1/account/assets': 2, # done + 'api/spot/v1/account/transferRecords': 1, + 'api/mix/v1/account/account': 2, # done + 'api/mix/v1/account/accounts': 2, # done + 'api/mix/v1/position/singlePosition-v2': 2, # done + 'api/mix/v1/position/allPosition-v2': 4, # done + 'api/mix/v1/account/accountBill': 2, + 'api/mix/v1/account/accountBusinessBill': 4, + 'api/mix/v1/order/current': 1, # done + 'api/mix/v1/order/marginCoinCurrent': 1, # done + 'api/mix/v1/order/history': 2, # done + 'api/mix/v1/order/historyProductType': 4, # done + 'api/mix/v1/order/detail': 2, # done + 'api/mix/v1/order/fills': 2, # done + 'api/mix/v1/order/allFills': 2, # done + 'api/mix/v1/plan/currentPlan': 1, # done + 'api/mix/v1/plan/historyPlan': 2, # done + }, + 'post': { + 'api/spot/v1/wallet/transfer-v2': 4, # done + 'api/spot/v1/wallet/withdrawal-v2': 4, # done but should be checked + 'api/spot/v1/wallet/withdrawal-inner-v2': 1, + 'api/spot/v1/account/bills': 2, # done + 'api/spot/v1/trade/orders': 2, # done + 'api/spot/v1/trade/batch-orders': {'cost': 4, 'step': 10}, # done + 'api/spot/v1/trade/cancel-order': 1, # not used + 'api/spot/v1/trade/cancel-order-v2': 2, # done + 'api/spot/v1/trade/cancel-symbol-order': 2, # done + 'api/spot/v1/trade/cancel-batch-orders': 1, # not used + 'api/spot/v1/trade/cancel-batch-orders-v2': 1, # done + 'api/spot/v1/trade/orderInfo': 1, # done + 'api/spot/v1/trade/open-orders': 1, # done + 'api/spot/v1/trade/history': 1, # done + 'api/spot/v1/trade/fills': 1, # done + 'api/spot/v1/plan/placePlan': 1, # done + 'api/spot/v1/plan/modifyPlan': 1, # done + 'api/spot/v1/plan/cancelPlan': 1, # done + 'api/spot/v1/plan/currentPlan': 1, # done + 'api/spot/v1/plan/historyPlan': 1, # done + 'api/spot/v1/plan/batchCancelPlan': 2, # done + 'api/mix/v1/account/open-count': 1, + 'api/mix/v1/account/setLeverage': 4, # done + 'api/mix/v1/account/setMargin': 4, # done + 'api/mix/v1/account/setMarginMode': 4, # done + 'api/mix/v1/account/setPositionMode': 4, # done + 'api/mix/v1/order/placeOrder': 2, # done + 'api/mix/v1/order/batch-orders': {'cost': 4, 'step': 10}, # done + 'api/mix/v1/order/cancel-order': 2, # done + 'api/mix/v1/order/cancel-batch-orders': 2, # done + 'api/mix/v1/order/cancel-symbol-orders': 2, # done + 'api/mix/v1/order/cancel-all-orders': 2, # done + 'api/mix/v1/plan/placePlan': 2, # done + 'api/mix/v1/plan/modifyPlan': 2, + 'api/mix/v1/plan/modifyPlanPreset': 2, + 'api/mix/v1/plan/placeTPSL': 2, # done + 'api/mix/v1/plan/placeTrailStop': 2, # not used + 'api/mix/v1/plan/placePositionsTPSL': 2, # not used + 'api/mix/v1/plan/modifyTPSLPlan': 2, + 'api/mix/v1/plan/cancelPlan': 2, # done + 'api/mix/v1/plan/cancelSymbolPlan': 2, # done + 'api/mix/v1/plan/cancelAllPlan': 2, # done + }, + }, + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + 'password': True, + }, + 'fees': { + 'trading': { + 'spot': { + 'tierBased': False, + 'percentage': True, + 'feeSide': 'get', + 'maker': self.parse_number('0.001'), + 'taker': self.parse_number('0.001'), + }, + }, + }, + 'options': { + 'brokerId': '47cfy', + 'createMarketBuyOrderRequiresPrice': True, # for spot orders only + 'timeframes': { + 'spot': { + '1m': '1min', + '5m': '5min', + '15m': '15min', + '30m': '30min', + '1h': '1h', + '4h': '4h', + '6h': '6h', + '12h': '12h', + '1d': '1day', + '3d': '3day', + '1w': '1week', + '1M': '1M', + }, + 'swap': { + '1m': '1m', + '3m': '3m', + '5m': '5m', + '15': '15m', + '30': '30m', + '1h': '1H', + '2h': '2H', + '4h': '4H', + '6h': '6H', + '12h': '12H', + '1d': '1D', + '3d': '3D', + '1w': '1W', + '1M': '1M', + }, + }, + 'currencyIdsListForParseMarket': None, + 'broker': '', + 'networks': { + 'BTC': 'BITCOIN', + 'ERC20': 'ERC20', + 'TRC20': 'TRC20', + 'BEP20': 'BEP20', + 'ARB': 'ArbitrumOne', + 'OPTIMISM': 'Optimism', + 'LTC': 'LTC', + 'BCH': 'BCH', + 'ETC': 'ETC', + 'SOL': 'SOL', + 'NEO3': 'NEO3', + 'STX': 'stacks', + 'EGLD': 'Elrond', + 'NEAR': 'NEARProtocol', + 'ACA': 'AcalaToken', + 'KLAY': 'Klaytn', + 'FTM': 'Fantom', + 'TERRA': 'Terra', + 'WAVES': 'WAVES', + 'TAO': 'TAO', + 'SUI': 'SUI', + 'SEI': 'SEI', + 'RUNE': 'THORChain', + 'ZIL': 'ZIL', + 'SXP': 'Solar', + 'FET': 'FET', + 'AVAX': 'C-Chain', + 'XRP': 'XRP', + 'EOS': 'EOS', + 'DOGE': 'DOGECOIN', + 'CAP20': 'CAP20', + 'MATIC': 'Polygon', + 'CSPR': 'CSPR', + 'GLMR': 'Moonbeam', + 'MINA': 'MINA', + 'CFX': 'CFX', + 'STRAT': 'StratisEVM', + 'TIA': 'Celestia', + 'ChilizChain': 'ChilizChain', + 'APT': 'Aptos', + 'ONT': 'Ontology', + 'ICP': 'ICP', + 'ADA': 'Cardano', + 'FIL': 'FIL', + 'CELO': 'CELO', + 'DOT': 'DOT', + 'XLM': 'StellarLumens', + 'ATOM': 'ATOM', + 'CRO': 'CronosChain', + }, + 'networksById': { + 'TRC20': 'TRC20', + 'TRX(TRC20)': 'TRC20', + 'ArbitrumOne': 'ARB', # todo check + 'THORChain': 'RUNE', # todo check + 'Solar': 'SXP', # todo check + 'C-Chain': 'AVAX', # todo check + 'CAP20': 'CAP20', # todo check + 'CFXeSpace': 'CFX', # todo check + 'CFX': 'CFX', + 'StratisEVM': 'STRAT', # todo check + 'ChilizChain': 'ChilizChain', # todo check + 'StellarLumens': 'XLM', # todo check + 'CronosChain': 'CRO', # todo check + 'Optimism': 'Optimism', + }, + }, + 'features': { + 'default': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerPriceType': { + 'last': True, + 'mark': True, + 'index': False, + }, + 'triggerDirection': False, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': { + 'max': 50, + }, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 500, + 'daysBack': 100000, # todo implement + 'untilDays': 100000, # todo implement + 'symbolRequired': True, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 100, + 'trigger': True, + 'trailing': False, + 'marketType': True, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': None, # todo implement + 'fetchOHLCV': { + 'limit': 1000, + }, + }, + 'spot': { + 'extends': 'default', + }, + 'forDerivatives': { + 'extends': 'default', + 'createOrder': { + # todo check + 'attachedStopLossTakeProfit': { + 'triggerPriceType': None, + 'price': False, + }, + }, + 'fetchMyTrades': { + 'limit': 100, + }, + }, + 'swap': { + 'linear': { + 'extends': 'forDerivatives', + }, + 'inverse': { + 'extends': 'forDerivatives', + }, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'commonCurrencies': {}, + 'exceptions': { + 'exact': { + '22001': OrderNotFound, # No order to cancel + '429': DDoSProtection, # Request is too frequent + '40001': AuthenticationError, # The request header "ACCESS_KEY" cannot be empty + '40002': AuthenticationError, # The request header "ACCESS_SIGN" cannot be empty + '40003': AuthenticationError, # The request header "ACCESS_TIMESTAMP" cannot be empty + '40005': InvalidNonce, # Invalid ACCESS_TIMESTAMP + '40006': AuthenticationError, # Invalid ACCESS_KEY + '40007': BadRequest, # Invalid Content_Type,please use“application/json”format + '40008': InvalidNonce, # Request timestamp expired + '40009': AuthenticationError, # api verification failed + '40011': AuthenticationError, # The request header "ACCESS_PASSPHRASE" cannot be empty + '40012': AuthenticationError, # apikey/passphrase is incorrect + '40013': ExchangeError, # User has been frozen + '40014': PermissionDenied, # Incorrect permissions + '40015': ExchangeError, # System error + '40016': PermissionDenied, # The user must bind a mobile phone or Google authenticator + '40017': ExchangeError, # Parameter verification failed + '40018': PermissionDenied, # Illegal IP request + '40019': BadRequest, # Parameter {0} cannot be empty + '40020': BadRequest, # Parameter orderIds or clientOids error + '40034': BadRequest, # Parameter {0} does not exist + '400172': BadRequest, # symbol cannot be empty + '40912': BadRequest, # Batch processing orders can only process up to 50 + '40913': BadRequest, # orderId or clientOrderId must be passed one + '40102': BadRequest, # The contract configuration does not exist, please check the parameters + '40200': OnMaintenance, # Server upgrade, please try again later + '40305': BadRequest, # client_oid length is not greater than 40, and cannot be Martian characters + '40409': ExchangeError, # wrong format + '40704': ExchangeError, # Only check the data of the last three months + '40724': BadRequest, # Parameter is empty + '40725': ExchangeError, # spot service return an error + '40762': InsufficientFunds, # The order amount exceeds the balance + '40774': BadRequest, # The order type for unilateral position must also be the unilateral position type. + '40808': BadRequest, # Parameter verification exception {0} + '43001': OrderNotFound, # The order does not exist + '43002': InvalidOrder, # Pending order failed + '43004': OrderNotFound, # There is no order to cancel + '43005': RateLimitExceeded, # Exceeded the maximum order limit of transaction volume + '43006': BadRequest, # The order quantity is less than the minimum transaction quantity + '43007': BadRequest, # The order quantity is greater than the maximum transaction quantity + '43008': BadRequest, # The current order price cannot be less than {0} + '43009': BadRequest, # The current commission price exceeds the limit {0} + '43010': BadRequest, # The transaction amount cannot be less than {0} + '43011': BadRequest, # The current order price cannot be less than {0} + '43012': InsufficientFunds, # {"code":"43012","msg":"Insufficient balance","requestTime":1729327822139,"data":null} + '43117': InsufficientFunds, # Exceeds the maximum amount that can be transferred + '43118': BadRequest, # clientOrderId duplicate + '43122': BadRequest, # The purchase limit of self currency is {0}, and there is still {1} left + '45006': InsufficientFunds, # Insufficient position + '45110': BadRequest, # less than the minimum amount {0} {1} + # {"code":"40913","msg":"orderId or clientOrderId must be passed one","requestTime":1726160988275,"data":null} + }, + 'broad': {}, + }, + 'precisionMode': TICK_SIZE, + }) + + def calculate_rate_limiter_cost(self, api, method, path, params, config={}): + step = self.safe_integer(config, 'step') + cost = self.safe_integer(config, 'cost', 1) + orders = self.safe_list_2(params, 'orderList', 'orderDataList', []) + ordersLength = len(orders) + if (step is not None) and (ordersLength > step): + numberOfSteps = int(math.ceil(ordersLength / step)) + return cost * numberOfSteps + else: + return cost + + def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://coincatch.github.io/github.io/en/spot/#get-server-time + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = self.publicGetApiSpotV1PublicTime(params) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1725046822028, + # "data": "1725046822028" + # } + # + return self.safe_integer(response, 'data') + + def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://coincatch.github.io/github.io/en/spot/#get-coin-list + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = self.publicGetApiSpotV1PublicCurrencies(params) + data = self.safe_list(response, 'data', []) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1725102364202, + # "data": [ + # { + # "coinId": "1", + # "coinName": "BTC", + # "transfer": "true", + # "chains": [ + # { + # "chainId": "10", + # "chain": "BITCOIN", + # "needTag": "false", + # "withdrawable": "true", + # "rechargeable": "true", + # "withdrawFee": "0.0005", + # "extraWithDrawFee": "0", + # "depositConfirm": "1", + # "withdrawConfirm": "1", + # "minDepositAmount": "0.00001", + # "minWithdrawAmount": "0.001", + # "browserUrl": "https://blockchair.com/bitcoin/transaction/" + # } + # ] + # }, + # ... + # ] + # } + # + result: dict = {} + currenciesIds = [] + for i in range(0, len(data)): + currecy = data[i] + currencyId = self.safe_string(currecy, 'coinName') + currenciesIds.append(currencyId) + code = self.safe_currency_code(currencyId) + networks = self.safe_list(currecy, 'chains') + parsedNetworks: dict = {} + for j in range(0, len(networks)): + network = networks[j] + networkId = self.safe_string(network, 'chain') + networkCode = self.network_id_to_code(networkId) + parsedNetworks[networkCode] = { + 'id': networkId, + 'network': networkCode, + 'limits': { + 'deposit': { + 'min': self.safe_number(network, 'minDepositAmount'), + 'max': None, + }, + 'withdraw': { + 'min': self.safe_number(network, 'minWithdrawAmount'), + 'max': None, + }, + }, + 'active': None, + 'deposit': self.safe_string(network, 'rechargeable') == 'true', + 'withdraw': self.safe_string(network, 'withdrawable') == 'true', + 'fee': self.safe_number(network, 'withdrawFee'), + 'precision': None, + 'info': network, + } + result[code] = self.safe_currency_structure({ + 'id': currencyId, + 'numericId': self.safe_integer(currecy, 'coinId'), + 'code': code, + 'precision': None, + 'type': None, + 'name': None, + 'active': None, + 'deposit': None, + 'withdraw': None, + 'fee': None, + 'limits': { + 'deposit': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + }, + 'networks': parsedNetworks, + 'info': currecy, + }) + if self.safe_list(self.options, 'currencyIdsListForParseMarket') is None: + self.options['currencyIdsListForParseMarket'] = currenciesIds + return result + + def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}): + """ + fetch deposit and withdraw fees + + https://coincatch.github.io/github.io/en/spot/#get-coin-list + + :param str[] [codes]: list of unified currency codes + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `fee structures ` + """ + self.load_markets() + response = self.publicGetApiSpotV1PublicCurrencies(params) + data = self.safe_list(response, 'data', []) + return self.parse_deposit_withdraw_fees(data, codes, 'coinName') + + def parse_deposit_withdraw_fee(self, fee, currency: Currency = None): + # + # { + # "coinId":"1", + # "coinName":"BTC", + # "transfer":"true", + # "chains":[ + # { + # "chain":null, + # "needTag":"false", + # "withdrawable":"true", + # "rechargeAble":"true", + # "withdrawFee":"0.005", + # "depositConfirm":"1", + # "withdrawConfirm":"1", + # "minDepositAmount":"0.001", + # "minWithdrawAmount":"0.001", + # "browserUrl":"https://blockchair.com/bitcoin/testnet/transaction/" + # } + # ] + # } + # + chains = self.safe_list(fee, 'chains', []) + chainsLength = len(chains) + result: dict = { + 'info': fee, + 'withdraw': { + 'fee': None, + 'percentage': None, + }, + 'deposit': { + 'fee': None, + 'percentage': None, + }, + 'networks': {}, + } + for i in range(0, chainsLength): + chain = chains[i] + networkId = self.safe_string(chain, 'chain') + currencyCode = self.safe_string(currency, 'code') + networkCode = self.network_id_to_code(networkId, currencyCode) + result['networks'][networkCode] = { + 'deposit': {'fee': None, 'percentage': None}, + 'withdraw': {'fee': self.safe_number(chain, 'withdrawFee'), 'percentage': False}, + } + if chainsLength == 1: + result['withdraw']['fee'] = self.safe_number(chain, 'withdrawFee') + result['withdraw']['percentage'] = False + return result + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for the exchange + + https://coincatch.github.io/github.io/en/spot/#get-all-tickers + https://coincatch.github.io/github.io/en/mix/#get-all-symbols + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = self.publicGetApiSpotV1MarketTickers(params) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1725114040155, + # "data": [ + # { + # "symbol": "BTCUSDT", + # "high24h": "59461.34", + # "low24h": "57723.23", + # "close": "59056.02", + # "quoteVol": "18240112.23368", + # "baseVol": "309.05564", + # "usdtVol": "18240112.2336744", + # "ts": "1725114038951", + # "buyOne": "59055.85", + # "sellOne": "59057.45", + # "bidSz": "0.0139", + # "askSz": "0.0139", + # "openUtc0": "59126.71", + # "changeUtc": "-0.0012", + # "change": "0.01662" + # }, + # ... + # ] + # } + # + if self.safe_list(self.options, 'currencyIdsListForParseMarket') is None: + self.fetch_currencies() + spotMarkets = self.safe_list(response, 'data', []) + request: dict = {} + productType: Str = None + productType, params = self.handle_option_and_params(params, 'fetchMarkets', 'productType', productType) + swapMarkets = [] + request['productType'] = 'umcbl' + response = self.publicGetApiMixV1MarketContracts(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1725297439225, + # "data": [ + # { + # "symbol": "BTCUSDT_UMCBL", + # "makerFeeRate": "0.0002", + # "takerFeeRate": "0.0006", + # "feeRateUpRatio": "0.005", + # "openCostUpRatio": "0.01", + # "quoteCoin": "USDT", + # "baseCoin": "BTC", + # "buyLimitPriceRatio": "0.01", + # "sellLimitPriceRatio": "0.01", + # "supportMarginCoins": ["USDT"], + # "minTradeNum": "0.001", + # "priceEndStep": "1", + # "volumePlace": "3", + # "pricePlace": "1", + # "sizeMultiplier": "0.001", + # "symbolType": "perpetual", + # "symbolStatus": "normal", + # "offTime": "-1", + # "limitOpenTime": "-1", + # "maintainTime": "", + # "symbolName": "BTCUSDT", + # "minTradeUSDT": null, + # "maxPositionNum": null, + # "maxOrderNum": null + # } + # ] + # } + # + swapUMCBL = self.safe_list(response, 'data', []) + request['productType'] = 'dmcbl' + response = self.publicGetApiMixV1MarketContracts(self.extend(request, params)) + # + # { + # "code":"00000", + # "msg":"success", + # "requestTime":1725297439646, + # "data":[ + # { + # "symbol":"BTCUSD_DMCBL", + # "makerFeeRate":"0.0002", + # "takerFeeRate":"0.0006", + # "feeRateUpRatio":"0.005", + # "openCostUpRatio":"0.01", + # "quoteCoin":"USD", + # "baseCoin":"BTC", + # "buyLimitPriceRatio":"0.01", + # "sellLimitPriceRatio":"0.01", + # "supportMarginCoins":[ + # "BTC", + # "ETH" + # ], + # "minTradeNum":"0.001", + # "priceEndStep":"1", + # "volumePlace":"3", + # "pricePlace":"1", + # "sizeMultiplier":"0.001", + # "symbolType":"perpetual", + # "symbolStatus":"normal", + # "offTime":"-1", + # "limitOpenTime":"-1", + # "maintainTime":"", + # "symbolName":"BTCUSD", + # "minTradeUSDT":null, + # "maxPositionNum":null, + # "maxOrderNum":null + # } + # ] + # } + swapDMCBL = self.safe_list(response, 'data', []) + swapDMCBLExtended = [] + for i in range(0, len(swapDMCBL)): + market = swapDMCBL[i] + supportMarginCoins = self.safe_list(market, 'supportMarginCoins', []) + for j in range(0, len(supportMarginCoins)): + settle = supportMarginCoins[j] + obj = { + 'supportMarginCoins': [settle], + } + swapDMCBLExtended.append(self.extend(market, obj)) + swapMarkets = self.array_concat(swapUMCBL, swapDMCBLExtended) + markets = self.array_concat(spotMarkets, swapMarkets) + return self.parse_markets(markets) + + def parse_market(self, market: dict) -> Market: + # + # spot + # { + # "symbol": "BTCUSDT", + # "high24h": "59461.34", + # "low24h": "57723.23", + # "close": "59056.02", + # "quoteVol": "18240112.23368", + # "baseVol": "309.05564", + # "usdtVol": "18240112.2336744", + # "ts": "1725114038951", + # "buyOne": "59055.85", + # "sellOne": "59057.45", + # "bidSz": "0.0139", + # "askSz": "0.0139", + # "openUtc0": "59126.71", + # "changeUtc": "-0.0012", + # "change": "0.01662" + # }, + # + # swap + # { + # "symbol": "BTCUSDT_UMCBL", + # "makerFeeRate": "0.0002", + # "takerFeeRate": "0.0006", + # "feeRateUpRatio": "0.005", + # "openCostUpRatio": "0.01", + # "quoteCoin": "USDT", + # "baseCoin": "BTC", + # "buyLimitPriceRatio": "0.01", + # "sellLimitPriceRatio": "0.01", + # "supportMarginCoins": ["USDT"], + # "minTradeNum": "0.001", + # "priceEndStep": "1", + # "volumePlace": "3", + # "pricePlace": "1", + # "sizeMultiplier": "0.001", + # "symbolType": "perpetual", + # "symbolStatus": "normal", + # "offTime": "-1", + # "limitOpenTime": "-1", + # "maintainTime": "", + # "symbolName": "BTCUSDT", + # "minTradeUSDT": null, + # "maxPositionNum": null, + # "maxOrderNum": null + # } + # + marketId = self.safe_string(market, 'symbol') + tradingFees = self.safe_dict(self.fees, 'trading') + fees = self.safe_dict(tradingFees, 'spot') + baseId = self.safe_string(market, 'baseCoin') + quoteId = self.safe_string(market, 'quoteCoin') + settleId: Str = None + suffix = '' + settle: Str = None + type = 'spot' + isLinear: Bool = None + isInverse: Bool = None + subType: Str = None + isSpot = baseId is None # for now spot markets have no properties baseCoin and quoteCoin + if isSpot: + parsedMarketId = self.parse_spot_market_id(marketId) + baseId = self.safe_string(parsedMarketId, 'baseId') + quoteId = self.safe_string(parsedMarketId, 'quoteId') + marketId += '_SPBL' # spot markets should have current suffix + else: + type = 'swap' + fees['taker'] = self.safe_number(market, 'takerFeeRate') + fees['maker'] = self.safe_number(market, 'makerFeeRate') + supportMarginCoins = self.safe_list(market, 'supportMarginCoins', []) + settleId = self.safe_string(supportMarginCoins, 0) + settle = self.safe_currency_code(settleId) + suffix = ':' + settle + isLinear = quoteId == settleId # todo check + isInverse = baseId == settleId # todo check + if isLinear: + subType = 'linear' + elif isInverse: + subType = 'inverse' + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + symbol = base + '/' + quote + suffix + symbolStatus = self.safe_string(market, 'symbolStatus') + active = (symbolStatus == 'normal') if symbolStatus else None + volumePlace = self.safe_string(market, 'volumePlace') + amountPrecisionString = self.parse_precision(volumePlace) + pricePlace = self.safe_string(market, 'pricePlace') + priceEndStep = self.safe_string(market, 'priceEndStep') + pricePrecisionString = Precise.string_mul(self.parse_precision(pricePlace), priceEndStep) + return self.safe_market_structure({ + 'id': marketId, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'baseId': baseId, + 'quoteId': quoteId, + 'active': active, + 'type': type, + 'subType': subType, + 'spot': isSpot, + 'margin': False if isSpot else None, + 'swap': not isSpot, + 'future': False, + 'option': False, + 'contract': not isSpot, + 'settle': settle, + 'settleId': settleId, + 'contractSize': self.safe_number(market, 'sizeMultiplier'), + 'linear': isLinear, + 'inverse': isInverse, + 'taker': self.safe_number(fees, 'taker'), + 'maker': self.safe_number(fees, 'maker'), + 'percentage': self.safe_bool(fees, 'percentage'), + 'tierBased': self.safe_bool(fees, 'tierBased'), + 'feeSide': self.safe_string(fees, 'feeSide'), + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(amountPrecisionString), + 'price': self.parse_number(pricePrecisionString), + }, + 'limits': { + 'amount': { + 'min': self.safe_number(market, 'minTradeNum'), + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'leverage': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + 'info': market, + }) + + def parse_spot_market_id(self, marketId): + baseId = None + quoteId = None + currencyIds = self.safe_list(self.options, 'currencyIdsListForParseMarket', []) + for i in range(0, len(currencyIds)): + currencyId = currencyIds[i] + entryIndex = marketId.find(currencyId) + if entryIndex > -1: + restId = marketId.replace(currencyId, '') + if entryIndex == 0: + baseId = currencyId + quoteId = restId + else: + baseId = restId + quoteId = currencyId + break + result: dict = { + 'baseId': baseId, + 'quoteId': quoteId, + } + return result + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://coincatch.github.io/github.io/en/spot/#get-single-ticker + https://coincatch.github.io/github.io/en/mix/#get-single-symbol-ticker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = None + if market['spot']: + response = self.publicGetApiSpotV1MarketTicker(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1725132487751, + # "data": { + # "symbol": "ETHUSDT", + # "high24h": "2533.76", + # "low24h": "2492.72", + # "close": "2499.76", + # "quoteVol": "21457850.7442", + # "baseVol": "8517.1869", + # "usdtVol": "21457850.744163", + # "ts": "1725132487476", + # "buyOne": "2499.75", + # "sellOne": "2499.76", + # "bidSz": "0.5311", + # "askSz": "4.5806", + # "openUtc0": "2525.69", + # "changeUtc": "-0.01027", + # "change": "-0.00772" + # } + # } + # + elif market['swap']: + response = self.publicGetApiMixV1MarketTicker(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1725316687174, + # "data": { + # "symbol": "ETHUSDT_UMCBL", + # "last": "2540.6", + # "bestAsk": "2540.71", + # "bestBid": "2540.38", + # "bidSz": "12.1", + # "askSz": "20", + # "high24h": "2563.91", + # "low24h": "2398.3", + # "timestamp": "1725316687177", + # "priceChangePercent": "0.01134", + # "baseVolume": "706928.96", + # "quoteVolume": "1756401737.8766", + # "usdtVolume": "1756401737.8766", + # "openUtc": "2424.49", + # "chgUtc": "0.04789", + # "indexPrice": "2541.977142", + # "fundingRate": "0.00006", + # "holdingAmount": "144688.49", + # "deliveryStartTime": null, + # "deliveryTime": null, + # "deliveryStatus": "normal" + # } + # } + # + else: + raise NotSupported(self.id + ' ' + 'fetchTicker() is not supported for ' + market['type'] + ' type of markets') + data = self.safe_dict(response, 'data', {}) + return self.parse_ticker(data, market) + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://coincatch.github.io/github.io/en/spot/#get-all-tickers + https://coincatch.github.io/github.io/en/mix/#get-all-symbol-ticker + + :param str[] [symbols]: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: 'spot' or 'swap'(default 'spot') + :param str [params.productType]: 'umcbl' or 'dmcbl'(default 'umcbl') - USDT perpetual contract or Universal margin perpetual contract + :returns dict: a dictionary of `ticker structures ` + """ + methodName = 'fetchTickers' + self.load_markets() + symbols = self.market_symbols(symbols, None, True, True) + market = self.get_market_from_symbols(symbols) + marketType = 'spot' + marketType, params = self.handle_market_type_and_params(methodName, market, params, marketType) + response = None + if marketType == 'spot': + response = self.publicGetApiSpotV1MarketTickers(params) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1725114040155, + # "data": [ + # { + # "symbol": "BTCUSDT", + # "high24h": "59461.34", + # "low24h": "57723.23", + # "close": "59056.02", + # "quoteVol": "18240112.23368", + # "baseVol": "309.05564", + # "usdtVol": "18240112.2336744", + # "ts": "1725114038951", + # "buyOne": "59055.85", + # "sellOne": "59057.45", + # "bidSz": "0.0139", + # "askSz": "0.0139", + # "openUtc0": "59126.71", + # "changeUtc": "-0.0012", + # "change": "0.01662" + # }, + # ... + # ] + # } + # + elif marketType == 'swap': + productType = 'umcbl' + productType, params = self.handle_option_and_params(params, methodName, 'productType', productType) + request: dict = { + 'productType': productType, + } + response = self.publicGetApiMixV1MarketTickers(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1725320291340, + # "data": [ + # { + # "symbol": "BTCUSDT_UMCBL", + # "last": "59110.5", + # "bestAsk": "59113.2", + # "bestBid": "59109.5", + # "bidSz": "1.932", + # "askSz": "0.458", + # "high24h": "59393.5", + # "low24h": "57088.5", + # "timestamp": "1725320291347", + # "priceChangePercent": "0.01046", + # "baseVolume": "59667.001", + # "quoteVolume": "3472522256.9927", + # "usdtVolume": "3472522256.9927", + # "openUtc": "57263", + # "chgUtc": "0.03231", + # "indexPrice": "59151.25442", + # "fundingRate": "0.00007", + # "holdingAmount": "25995.377", + # "deliveryStartTime": null, + # "deliveryTime": null, + # "deliveryStatus": "normal"} + # }, + # ... + # ] + # } + # + else: + raise NotSupported(self.id + ' ' + methodName + '() is not supported for ' + marketType + ' type of markets') + data = self.safe_list(response, 'data', []) + return self.parse_tickers(data, symbols) + + def parse_ticker(self, ticker, market: Market = None) -> Ticker: + # + # spot + # { + # "symbol": "BTCUSDT", + # "high24h": "59461.34", + # "low24h": "57723.23", + # "close": "59056.02", + # "quoteVol": "18240112.23368", + # "baseVol": "309.05564", + # "usdtVol": "18240112.2336744", + # "ts": "1725114038951", + # "buyOne": "59055.85", + # "sellOne": "59057.45", + # "bidSz": "0.0139", + # "askSz": "0.0139", + # "openUtc0": "59126.71", + # "changeUtc": "-0.0012", + # "change": "0.01662" + # } + # + # swap + # { + # "symbol": "ETHUSDT_UMCBL", + # "last": "2540.6", + # "bestAsk": "2540.71", + # "bestBid": "2540.38", + # "bidSz": "12.1", + # "askSz": "20", + # "high24h": "2563.91", + # "low24h": "2398.3", + # "timestamp": "1725316687177", + # "priceChangePercent": "0.01134", + # "baseVolume": "706928.96", + # "quoteVolume": "1756401737.8766", + # "usdtVolume": "1756401737.8766", + # "openUtc": "2424.49", + # "chgUtc": "0.04789", + # "indexPrice": "2541.977142", + # "fundingRate": "0.00006", + # "holdingAmount": "144688.49", + # "deliveryStartTime": null, + # "deliveryTime": null, + # "deliveryStatus": "normal" + # } + # + timestamp = self.safe_integer_2(ticker, 'ts', 'timestamp') + marketId = self.safe_string(ticker, 'symbol', '') + if marketId.find('_') < 0: + marketId += '_SPBL' # spot markets from tickers endpoints have no suffix specific for market id + market = self.safe_market_custom(marketId, market) + last = self.safe_string_2(ticker, 'close', 'last') + return self.safe_ticker({ + 'symbol': market['symbol'], + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high24h'), + 'low': self.safe_string(ticker, 'low24h'), + 'bid': self.safe_string_2(ticker, 'buyOne', 'bestBid'), + 'bidVolume': self.safe_string(ticker, 'bidSz'), + 'ask': self.safe_string_2(ticker, 'sellOne', 'bestAsk'), + 'askVolume': self.safe_string(ticker, 'askSz'), + 'vwap': None, + 'open': self.safe_string_2(ticker, 'openUtc0', 'openUtc'), + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': Precise.string_mul(self.safe_string_2(ticker, 'changeUtc', 'chgUtc'), '100'), + 'average': None, + 'baseVolume': self.safe_string_2(ticker, 'baseVol', 'baseVolume'), + 'quoteVolume': self.safe_string_2(ticker, 'quoteVol', 'quoteVolume'), + 'indexPrice': self.safe_string(ticker, 'indexPrice'), + 'markPrice': None, + 'info': ticker, + }, market) + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://coincatch.github.io/github.io/en/spot/#get-merged-depth-data + https://coincatch.github.io/github.io/en/mix/#get-merged-depth-data + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return(maximum and default value is 100) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.precision]: 'scale0'(default), 'scale1', 'scale2' or 'scale3' - price accuracy, according to the selected accuracy step size to return the cumulative depth + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + methodName = 'fetchOrderBook' + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['limit'] = limit + precision: Str = None + precision, params = self.handle_option_and_params(params, methodName, 'precision') + if precision is not None: + request['precision'] = precision + response = None + if market['spot']: + response = self.publicGetApiSpotV1MarketMergeDepth(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1725137170814, + # "data": { + # "asks": [[2507.07, 0.4248]], + # "bids": [[2507.05, 0.1198]], + # "ts": "1725137170850", + # "scale": "0.01", + # "precision": "scale0", + # "isMaxPrecision": "NO" + # } + # } + # + elif market['swap']: + response = self.publicGetApiMixV1MarketMergeDepth(self.extend(request, params)) + else: + raise NotSupported(self.id + ' ' + methodName + '() is not supported for ' + market['type'] + ' type of markets') + data = self.safe_dict(response, 'data', {}) + timestamp = self.safe_integer(data, 'ts') + return self.parse_order_book(data, symbol, timestamp, 'bids', 'asks') + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + + https://coincatch.github.io/github.io/en/spot/#get-candle-data + https://coincatch.github.io/github.io/en/mix/#get-candle-data + + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch(default 100) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest candle to fetch + :param str [params.price]: "mark" for mark price candles + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + methodName = 'fetchOHLCV' + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + until: Int = None + until, params = self.handle_option_and_params(params, methodName, 'until') + marketType = market['type'] + timeframes = self.options['timeframes'][marketType] + encodedTimeframe = self.safe_string(timeframes, timeframe, timeframe) + maxLimit = 1000 + requestedLimit = limit + if (since is not None) or (until is not None): + requestedLimit = maxLimit # the exchange returns only last limit candles, so we have to fetch max limit if since or until are provided + if requestedLimit is not None: + request['limit'] = requestedLimit + response = None + if market['spot']: + request['period'] = encodedTimeframe + if since is not None: + request['after'] = since + if until is not None: + request['before'] = until + response = self.publicGetApiSpotV1MarketCandles(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1725142465742, + # "data": [ + # { + # "open": "2518.6", + # "high": "2519.19", + # "low": "2518.42", + # "close": "2518.86", + # "quoteVol": "17193.239401", + # "baseVol": "6.8259", + # "usdtVol": "17193.239401", + # "ts": "1725142200000" + # }, + # ... + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_ohlcvs(data, market, timeframe, since, limit) + elif market['swap']: + request['granularity'] = encodedTimeframe + if until is None: + until = self.milliseconds() + if since is None: + duration = self.parse_timeframe(timeframe) + since = until - (duration * maxLimit * 1000) + request['startTime'] = since # since and until are mandatory for swap + request['endTime'] = until + priceType: Str = None + priceType, params = self.handle_option_and_params(params, methodName, 'price') + if priceType == 'mark': + request['kLineType'] = 'market mark index' + response = self.publicGetApiMixV1MarketCandles(self.extend(request, params)) + # + # [ + # [ + # "1725379020000", + # "57614", + # "57636", + # "57614", + # "57633", + # "28.725", + # "1655346.493" + # ], + # ... + # ] + # + return self.parse_ohlcvs(response, market, timeframe, since, limit) + else: + raise NotSupported(self.id + ' ' + methodName + '() is not supported for ' + market['type'] + ' type of markets') + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + return [ + self.safe_integer_2(ohlcv, 'ts', 0), + self.safe_number_2(ohlcv, 'open', 1), + self.safe_number_2(ohlcv, 'high', 2), + self.safe_number_2(ohlcv, 'low', 3), + self.safe_number_2(ohlcv, 'close', 4), + self.safe_number_2(ohlcv, 'baseVol', 5), + ] + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://coincatch.github.io/github.io/en/spot/#get-recent-trades + https://coincatch.github.io/github.io/en/mix/#get-fills + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest entry to fetch + :returns Trade[]: a list of `trade structures ` + """ + methodName = 'fetchTrades' + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + until: Int = None + until, params = self.handle_option_and_params(params, methodName, 'until') + maxLimit = 1000 + requestLimit = limit + if (since is not None) or (until is not None): + requestLimit = maxLimit + if since is not None: + request['startTime'] = since + if until is not None: + request['endTime'] = until + if requestLimit is not None: + request['limit'] = requestLimit + response = None + if market['spot']: + response = self.publicGetApiSpotV1MarketFillsHistory(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1725198410976, + # "data": [ + # { + # "symbol": "ETHUSDT_SPBL", + # "tradeId": "1214135619719827457", + # "side": "buy", + # "fillPrice": "2458.62", + # "fillQuantity": "0.4756", + # "fillTime": "1725198409967" + # } + # ] + # } + # + elif market['swap']: + response = self.publicGetApiMixV1MarketFillsHistory(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1725389251975, + # "data": [ + # { + # "tradeId": "1214936067582234782", + # "price": "57998.5", + # "size": "1.918", + # "side": "Sell", + # "timestamp": "1725389251000", + # "symbol": "BTCUSDT_UMCBL" + # }, + # ... + # ] + # } + # + else: + raise NotSupported(self.id + ' ' + methodName + '() is not supported for ' + market['type'] + ' type of markets') + data = self.safe_list(response, 'data', []) + return self.parse_trades(data, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades spot + # { + # "symbol": "ETHUSDT_SPBL", + # "tradeId": "1214135619719827457", + # "side": "Buy", + # "fillPrice": "2458.62", + # "fillQuantity": "0.4756", + # "fillTime": "1725198409967" + # } + # + # fetchTrades swap + # { + # "tradeId": "1214936067582234782", + # "price": "57998.5", + # "size": "1.918", + # "side": "Sell", + # "timestamp": "1725389251000", + # "symbol": "BTCUSDT_UMCBL" + # } + # + # fetchMyTrades spot + # { + # "accountId": "1002820815393", + # "symbol": "ETHUSDT_SPBL", + # "orderId": "1217143186968068096", + # "fillId": "1217143193356505089", + # "orderType": "market", + # "side": "buy", + # "fillPrice": "2340.55", + # "fillQuantity": "0.0042", + # "fillTotalAmount": "9.83031", + # "feeCcy": "ETH", + # "fees": "-0.0000042", + # "takerMakerFlag": "taker", + # "cTime": "1725915471400" + # } + # + # fetchMyTrades swap + # { + # "tradeId": "1225467075440189441", + # "symbol": "ETHUSD_DMCBL", + # "orderId": "1225467075288719360", + # "price": "2362.03", + # "sizeQty": "0.1", + # "fee": "-0.00005996", + # "side": "burst_close_long", + # "fillAmount": "236.203", + # "profit": "-0.0083359", + # "enterPointSource": "SYS", + # "tradeSide": "burst_close_long", + # "holdMode": "double_hold", + # "takerMakerFlag": "taker", + # "cTime": "1727900039539" + # } + # + marketId = self.safe_string(trade, 'symbol') + market = self.safe_market_custom(marketId, market) + timestamp = self.safe_integer_n(trade, ['fillTime', 'timestamp', 'cTime']) + fees = self.safe_string_2(trade, 'fees', 'fee') + feeCost: Str = None + if fees is not None: + feeCost = Precise.string_abs(fees) + feeCurrency = self.safe_string(trade, 'feeCcy') + if (feeCurrency is None) and (market['settle'] is not None): + feeCurrency = market['settle'] + side = self.safe_string_lower_2(trade, 'tradeSide', 'side') + return self.safe_trade({ + 'id': self.safe_string_2(trade, 'tradeId', 'fillId'), + 'order': self.safe_string(trade, 'orderId'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'type': self.safe_string(trade, 'orderType'), + 'side': self.parse_order_side(side), + 'takerOrMaker': self.safe_string(trade, 'takerMakerFlag'), + 'price': self.safe_string_2(trade, 'fillPrice', 'price'), + 'amount': self.safe_string_n(trade, ['fillQuantity', 'size', 'sizeQty']), + 'cost': self.safe_string_2(trade, 'fillTotalAmount', 'fillAmount'), + 'fee': { + 'cost': feeCost, + 'currency': feeCurrency, + }, + 'info': trade, + }, market) + + def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate + + https://coincatch.github.io/github.io/en/mix/#get-current-funding-rate + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + self.load_markets() + market = self.market(symbol) + marketId = market['id'] + parts = marketId.split('_') + request: dict = { + 'symbol': marketId, + 'productType': self.safe_string(parts, 1), + } + response = self.publicGetApiMixV1MarketCurrentFundRate(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1725402130395, + # "data": { + # "symbol": "BTCUSDT_UMCBL", + # "fundingRate": "0.000043" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_funding_rate(data, market) + + def parse_funding_rate(self, contract, market: Market = None): + marketId = self.safe_string(contract, 'symbol') + market = self.safe_market_custom(marketId, market) + fundingRate = self.safe_number(contract, 'fundingRate') + return { + 'info': contract, + 'symbol': market['symbol'], + 'markPrice': None, + 'indexPrice': None, + 'interestRate': None, + 'estimatedSettlePrice': None, + 'timestamp': None, + 'datetime': None, + 'fundingRate': fundingRate, + 'fundingTimestamp': None, + 'fundingDatetime': None, + 'nextFundingRate': None, + 'nextFundingTimestamp': None, + 'nextFundingDatetime': None, + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + } + + def handle_option_params_and_request(self, params: object, methodName: str, optionName: str, request: object, requestProperty: str, defaultValue=None): + option, paramsOmited = self.handle_option_and_params(params, methodName, optionName, defaultValue) + if option is not None: + request[requestProperty] = option + return [request, paramsOmited] + + def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical funding rate prices + + https://coincatch.github.io/github.io/en/mix/#get-history-funding-rate + + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: timestamp in ms of the earliest funding rate to fetch + :param int [limit]: the maximum amount of entries to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.pageNo]: the page number to fetch + :param bool [params.nextPage]: whether to query the next page(default False) + :returns dict[]: a list of `funding rate structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchFundingRateHistory() requires a symbol argument') + self.load_markets() + maxEntriesPerRequest = 100 + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + requestedLimit = limit + if since is not None: + requestedLimit = maxEntriesPerRequest + if requestedLimit is not None: + request['pageSize'] = requestedLimit + response = self.publicGetApiMixV1MarketHistoryFundRate(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1725455810888, + # "data": [ + # { + # "symbol": "BTCUSD", + # "fundingRate": "0.000635", + # "settleTime": "1724889600000" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + rates = [] + for i in range(0, len(data)): + entry = data[i] + timestamp = self.safe_integer(entry, 'settleTime') + rates.append({ + 'info': entry, + 'symbol': self.safe_symbol(self.safe_string(entry, 'symbol'), market, None, 'swap'), + 'fundingRate': self.safe_number(entry, 'fundingRate'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + sorted = self.sort_by(rates, 'timestamp') + return self.filter_by_since_limit(sorted, since, limit) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://coincatch.github.io/github.io/en/spot/#get-account-assets + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: 'spot' or 'swap' - the type of the market to fetch balance for(default 'spot') + :param str [params.productType]: *swap only* 'umcbl' or 'dmcbl'(default 'umcbl') + :returns dict: a `balance structure ` + """ + self.load_markets() + methodName = 'fetchBalance' + marketType = None + marketType, params = self.handle_market_type_and_params(methodName, None, params) + response = None + if marketType == 'spot': + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1725202685986, + # "data": [ + # { + # "coinId": 2, + # "coinName": "USDT", + # "available": "99.20000000", + # "frozen": "0.00000000", + # "lock": "0.00000000", + # "uTime": "1724938746000" + # } + # ] + # } + # + response = self.privateGetApiSpotV1AccountAssets(params) + elif marketType == 'swap': + productType = 'umcbl' + productType, params = self.handle_option_and_params(params, methodName, 'productType', productType) + request: dict = { + 'productType': productType, + } + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1726666298135, + # "data": [ + # { + # "marginCoin": "USDT", + # "locked": "0", + # "available": "60", + # "crossMaxAvailable": "60", + # "fixedMaxAvailable": "60", + # "maxTransferOut": "60", + # "equity": "60", + # "usdtEquity": "60", + # "btcEquity": "0.001002360626", + # "crossRiskRate": "0", + # "unrealizedPL": "0", + # "bonus": "0", + # "crossedUnrealizedPL": null, + # "isolatedUnrealizedPL": null + # } + # ] + # } + # + response = self.privateGetApiMixV1AccountAccounts(self.extend(request, params)) + else: + raise NotSupported(self.id + ' ' + methodName + '() is not supported for ' + marketType + ' type of markets') + data = self.safe_list(response, 'data', []) + return self.parse_balance(data) + + def parse_balance(self, balances) -> Balances: + # + # spot + # [ + # { + # "coinId": 2, + # "coinName": "USDT", + # "available": "99.20000000", + # "frozen": "0.00000000", + # "lock": "0.00000000", + # "uTime": "1724938746000" + # } + # ] + # + # swap + # [ + # { + # "marginCoin": "USDT", + # "locked": "0", + # "available": "60", + # "crossMaxAvailable": "60", + # "fixedMaxAvailable": "60", + # "maxTransferOut": "60", + # "equity": "60", + # "usdtEquity": "60", + # "btcEquity": "0.001002360626", + # "crossRiskRate": "0", + # "unrealizedPL": "0", + # "bonus": "0", + # "crossedUnrealizedPL": null, + # "isolatedUnrealizedPL": null + # } + # ] + # + result: dict = { + 'info': balances, + } + for i in range(0, len(balances)): + balanceEntry = self.safe_dict(balances, i, {}) + currencyId = self.safe_string_2(balanceEntry, 'coinName', 'marginCoin') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(balanceEntry, 'available') + locked = self.safe_string_2(balanceEntry, 'lock', 'locked') + frozen = self.safe_string(balanceEntry, 'frozen', '0') + account['used'] = Precise.string_add(locked, frozen) + account['total'] = self.safe_string(balanceEntry, 'equity') + result[code] = account + return self.safe_balance(result) + + def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://coincatch.github.io/github.io/en/spot/#transfer + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: 'spot' or 'swap' or 'mix_usdt' or 'mix_usd' - account to transfer from + :param str toAccount: 'spot' or 'swap' or 'mix_usdt' or 'mix_usd' - account to transfer to + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.clientOrderId]: a unique id for the transfer + :returns dict: a `transfer structure ` + """ + self.load_markets() + currency = self.currency(code) + if fromAccount == 'swap': + if code == 'USDT': + fromAccount = 'mix_usdt' + else: + fromAccount = 'mix_usd' + if toAccount == 'swap': + if code == 'USDT': + toAccount = 'mix_usdt' + else: + toAccount = 'mix_usd' + request: dict = { + 'coin': currency['id'], + 'amount': self.currency_to_precision(code, amount), + 'fromType': fromAccount, + 'toType': toAccount, + } + clientOrderId: Str = None + clientOrderId, params = self.handle_option_and_params(params, 'transfer', 'clientOrderId') + if clientOrderId is not None: + request['clientOid'] = clientOrderId + response = self.privatePostApiSpotV1WalletTransferV2(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1726664727436, + # "data": { + # "transferId": "1220285801129066496", + # "clientOrderId": null + # } + # } + # + return self.parse_transfer(response, currency) + + def parse_transfer(self, transfer, currency: Currency = None): + msg = self.safe_string(transfer, 'msg') + status: Str = None + if msg == 'success': + status = 'ok' + data = self.safe_dict(transfer, 'data', {}) + return { + 'id': self.safe_string(data, 'transferId'), + 'timestamp': None, + 'datetime': None, + 'currency': self.safe_currency_code(None, currency), + 'amount': None, + 'fromAccount': None, + 'toAccount': None, + 'status': status, + 'info': transfer, + } + + def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://coincatch.github.io/github.io/en/spot/#get-coin-address + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.network]: network for fetch deposit address + :returns dict: an `address structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'coin': currency['id'], + } + networkCode: Str = None + networkCode, params = self.handle_network_code_and_params(params) + if networkCode is None: + networkCode = self.default_network_code(code) + if networkCode is None: + raise ArgumentsRequired(self.id + ' fetchDepositAddress() requires a network parameter or a default network code') + request['chain'] = self.network_code_to_id(networkCode, code) + response = self.privateGetApiSpotV1WalletDepositAddress(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1725210515143, + # "data": { + # "coin": "USDT", + # "address": "TKTUt7qiTaMgnTwZXjE3ZBkPB6LKhLPJyZ", + # "chain": "TRC20", + # "tag": null, + # "url": "https://tronscan.org/#/transaction/" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + depositAddress = self.parse_deposit_address(data, currency) + return depositAddress + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + # + # { + # "coin": "USDT", + # "address": "TKTUt7qiTaMgnTwZXjE3ZBkPB6LKhLPJyZ", + # "chain": "TRC20", + # "tag": null, + # "url": "https://tronscan.org/#/transaction/" + # } + # + address = self.safe_string(depositAddress, 'address') + self.check_address(address) + networkId = self.safe_string(depositAddress, 'chain') + network = self.safe_string(self.options['networksById'], networkId, networkId) + tag = self.safe_string(depositAddress, 'tag') + return { + 'currency': currency['code'], + 'address': address, + 'tag': tag, + 'network': network, + 'info': depositAddress, + } + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://coincatch.github.io/github.io/en/spot/#get-deposit-list + + :param str code: unified currency code of the currency transferred + :param int [since]: the earliest time in ms to fetch transfers for(default 24 hours ago) + :param int [limit]: the maximum number of transfer structures to retrieve(not used by exchange) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch transfers for(default time now) + :param int [params.pageNo]: pageNo default 1 + :param int [params.pageSize]: pageSize(default 20, max 100) + :returns dict[]: a list of `transfer structures ` + """ + methodName = 'fetchDeposits' + self.load_markets() + request: dict = {} + currency: Currency = None + if code is not None: + currency = self.currency(code) + request['coin'] = currency['id'] + if since is not None: + request['startTime'] = since + until: Int = None + until, params = self.handle_option_and_params(params, methodName, 'until') + if until is not None: + request['endTime'] = until + response = self.privateGetApiSpotV1WalletDepositList(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1725205525239, + # "data": [ + # { + # "id": "1213046466852196352", + # "txId": "824246b030cd84d56400661303547f43a1d9fef66cf968628dd5112f362053ff", + # "coin": "USDT", + # "type": "deposit", + # "amount": "99.20000000", + # "status": "success", + # "toAddress": "TKTUt7qiTaMgnTwZXjE3ZBkPB6LKhLPJyZ", + # "fee": null, + # "chain": "TRX(TRC20)", + # "confirm": null, + # "clientOid": null, + # "tag": null, + # "fromAddress": null, + # "dest": "on_chain", + # "cTime": "1724938735688", + # "uTime": "1724938746015" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_transactions(data, currency, since, limit) + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://coincatch.github.io/github.io/en/spot/#get-withdraw-list-v2 + + :param str code: unified currency code of the currency transferred + :param int [since]: the earliest time in ms to fetch transfers for(default 24 hours ago) + :param int [limit]: the maximum number of transfer structures to retrieve(default 50, max 200) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch transfers for(default time now) + :param str [params.clientOid]: clientOid + :param str [params.orderId]: The response orderId + :param str [params.idLessThan]: Requests the content on the page before self ID(older data), the value input should be the orderId of the corresponding interface. + :returns dict[]: a list of `transaction structures ` + """ + methodName = 'fetchWithdrawals' + self.load_markets() + request: dict = {} + currency: Currency = None + if code is not None: + currency = self.currency(code) + request['coin'] = currency['id'] + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + until: Int = None + until, params = self.handle_option_and_params(params, methodName, 'until') + if until is not None: + request['endTime'] = until + response = self.privateGetApiSpotV1WalletWithdrawalListV2(self.extend(request, params)) + # todo add after withdrawal + # + data = self.safe_list(response, 'data', []) + return self.parse_transactions(data, currency, since, limit) + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://coincatch.github.io/github.io/en/spot/#withdraw + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str [tag]: + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str params['network']: network for withdraw(mandatory) + :param str [params.remark]: remark + :param str [params.clientOid]: custom id + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.load_markets() + currency = self.currency(code) + request: dict = { + 'coin': currency['id'], + 'address': address, + 'amount': amount, + } + if tag is not None: + request['tag'] = tag + networkCode: Str = None + networkCode, params = self.handle_network_code_and_params(params) + if networkCode is not None: + request['chain'] = self.network_code_to_id(networkCode) + response = self.privatePostApiSpotV1WalletWithdrawalV2(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "data": { + # "orderId":888291686266343424", + # "clientOrderId":"123" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_transaction(data, currency) + + def parse_transaction(self, transaction, currency: Currency = None) -> Transaction: + # + # fetchDeposits + # + # { + # "id": "1213046466852196352", + # "txId": "824246b030cd84d56400661303547f43a1d9fef66cf968628dd5112f362053ff", + # "coin": "USDT", + # "type": "deposit", + # "amount": "99.20000000", + # "status": "success", + # "toAddress": "TKTUt7qiTaMgnTwZXjE3ZBkPB6LKhLPJyZ", + # "fee": null, + # "chain": "TRX(TRC20)", + # "confirm": null, + # "clientOid": null, + # "tag": null, + # "fromAddress": null, + # "dest": "on_chain", + # "cTime": "1724938735688", + # "uTime": "1724938746015" + # } + # + # withdraw + # + # { + # "code": "00000", + # "msg": "success", + # "data": { + # "orderId":888291686266343424", + # "clientOrderId":"123" + # } + # } + # + status = self.safe_string(transaction, 'status') + if status == 'success': + status = 'ok' + txid = self.safe_string(transaction, 'txId') + coin = self.safe_string(transaction, 'coin') + code = self.safe_currency_code(coin, currency) + timestamp = self.safe_integer(transaction, 'cTime') + amount = self.safe_number(transaction, 'amount') + networkId = self.safe_string(transaction, 'chain') + network = self.safe_string(self.options['networksById'], networkId, networkId) + addressTo = self.safe_string(transaction, 'toAddress') + addressFrom = self.safe_string(transaction, 'fromAddress') + tag = self.safe_string(transaction, 'tag') + type = self.safe_string(transaction, 'type') + feeCost = self.safe_number(transaction, 'fee') + fee = None + if feeCost is not None: + fee = { + 'cost': feeCost, + 'currency': code, + } + return { + 'info': transaction, + 'id': self.safe_string_2(transaction, 'id', 'orderId'), + 'txid': txid, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': network, + 'address': None, + 'addressTo': addressTo, + 'addressFrom': addressFrom, + 'tag': tag, + 'tagTo': None, + 'tagFrom': None, + 'type': type, + 'amount': amount, + 'currency': code, + 'status': status, + 'updated': None, + 'internal': None, + 'comment': None, + 'fee': fee, + } + + def create_market_buy_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market buy order by providing the symbol and cost + + https://coincatch.github.io/github.io/en/spot/#place-order + + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + methodName = 'createMarketBuyOrderWithCost' + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' ' + methodName + '() supports spot orders only') + params['methodName'] = methodName + params['createMarketBuyOrderRequiresPrice'] = False + return self.create_order(symbol, 'market', 'buy', cost, None, params) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}) -> Order: + """ + create a trade order + + https://coincatch.github.io/github.io/en/spot/#place-order + https://coincatch.github.io/github.io/en/spot/#place-plan-order + https://coincatch.github.io/github.io/en/mix/#place-order + https://coincatch.github.io/github.io/en/mix/#place-plan-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' or 'LIMIT_MAKER' for spot, 'market' or 'limit' or 'STOP' for swap + :param str side: 'buy' or 'sell' + :param float amount: how much of you want to trade in units of the base currency + :param float [price]: the price that the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.hedged]: *swap markets only* must be set to True if position mode is hedged(default False) + :param float [params.cost]: *spot market buy only* the quote quantity that can be used alternative for the amount + :param float [params.triggerPrice]: the price that the order is to be triggered + :param bool [params.postOnly]: if True, the order will only be posted to the order book and not executed immediately + :param str [params.timeInForce]: 'GTC', 'IOC', 'FOK' or 'PO' + :param str [params.clientOrderId]: a unique id for the order - is mandatory for swap + :returns dict: an `order structure ` + """ + self.load_markets() + params['methodName'] = self.safe_string(params, 'methodName', 'createOrder') + market = self.market(symbol) + if market['spot']: + return self.create_spot_order(symbol, type, side, amount, price, params) + elif market['swap']: + return self.create_swap_order(symbol, type, side, amount, price, params) + else: + raise NotSupported(self.id + ' createOrder() is not supported for ' + market['type'] + ' type of markets') + + def create_spot_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}) -> Order: + """ + create a trade order on spot market + + https://coincatch.github.io/github.io/en/spot/#place-order + https://coincatch.github.io/github.io/en/spot/#place-plan-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of you want to trade in units of the base currency + :param float [price]: the price that the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.cost]: *market buy only* the quote quantity that can be used alternative for the amount + :param float [params.triggerPrice]: the price that the order is to be triggered at + :param bool [params.postOnly]: if True, the order will only be posted to the order book and not executed immediately + :param str [params.timeInForce]: 'GTC', 'IOC', 'FOK' or 'PO' + :param str [params.clientOrderId]: a unique id for the order(max length 40) + :returns dict: an `order structure ` + """ + self.load_markets() + params['methodName'] = self.safe_string(params, 'methodName', 'createSpotOrder') + request: dict = self.create_spot_order_request(symbol, type, side, amount, price, params) + isPlanOrer = self.safe_string(request, 'triggerPrice') is not None + response = None + if isPlanOrer: + response = self.privatePostApiSpotV1PlanPlacePlan(request) + else: + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1725915469815, + # "data": { + # "orderId": "1217143186968068096", + # "clientOrderId": "8fa3eb89-2377-4519-a199-35d5db9ed262" + # } + # } + # + response = self.privatePostApiSpotV1TradeOrders(request) + data = self.safe_dict(response, 'data', {}) + market = self.market(symbol) + return self.parse_order(data, market) + + def create_spot_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}) -> dict: + """ + @ignore + helper function to build request + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of you want to trade in units of the base currency + :param float [price]: the price that the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: the price that the order is to be triggered at + :param float [params.cost]: *market buy only* the quote quantity that can be used alternative for the amount + :param bool [params.postOnly]: if True, the order will only be posted to the order book and not executed immediately + :param str [params.timeInForce]: 'GTC', 'IOC', 'FOK' or 'PO'(default 'GTC') + :param str [params.clientOrderId]: a unique id for the order(max length 40) + :returns dict: request to be sent to the exchange + """ + methodName = 'createSpotOrderRequest' + # spot market info has no presicion so we do not use it + methodName, params = self.handle_param_string(params, 'methodName', methodName) + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'side': side, + 'orderType': type, + } + isMarketOrder = (type == 'market') + timeInForceAndParams = self.handle_time_in_force_and_post_only(methodName, params, isMarketOrder) + params = timeInForceAndParams['params'] + timeInForce = timeInForceAndParams['timeInForce'] + cost: Str = None + cost, params = self.handle_param_string(params, 'cost') + triggerPrice: Str = None + triggerPrice, params = self.handle_param_string(params, 'triggerPrice') + isMarketBuy = isMarketOrder and (side == 'buy') + if (not isMarketBuy) and (cost is not None): + raise NotSupported(self.id + ' ' + methodName + ' supports cost parameter for market buy orders only') + if isMarketBuy: + costAndParams = self.handle_requires_price_and_cost(methodName, params, price, amount, cost) + cost = costAndParams['cost'] + params = costAndParams['params'] + if triggerPrice is None: + if type == 'limit': + request['price'] = price # spot markets have no precision + request['quantity'] = cost if isMarketBuy else self.number_to_string(amount) # spot markets have no precision + request['force'] = timeInForce if timeInForce else 'normal' # the exchange requres force but accepts any value + else: + request['triggerPrice'] = triggerPrice # spot markets have no precision + if timeInForce is not None: + request['timeInForceValue'] = timeInForce + clientOrderId: Str = None + clientOrderId, params = self.handle_param_string(params, 'clientOrderId') + if clientOrderId is not None: + request['clientOid'] = clientOrderId + if type == 'limit': + request['executePrice'] = price # spot markets have no precision + triggerType: Str = None + if isMarketOrder: + triggerType = 'market_price' + else: + triggerType = 'fill_price' + request['triggerType'] = triggerType + # tood check placeType + request['size'] = cost if isMarketOrder else self.number_to_string(amount) # spot markets have no precision + return self.extend(request, params) + + def handle_requires_price_and_cost(self, methodName: str, params: dict = {}, price: Num = None, amount: Num = None, cost: Str = None, side: str = 'buy'): + optionName = 'createMarket' + self.capitalize(side) + 'OrderRequiresPrice' + requiresPrice = True + requiresPrice, params = self.handle_option_and_params(params, methodName, optionName, True) + amountString: Str = None + if amount is not None: + amountString = self.number_to_string(amount) + priceString: Str = None + if price is not None: + priceString = self.number_to_string(price) + if requiresPrice: + if (price is None) and (cost is None): + raise InvalidOrder(self.id + ' ' + methodName + '() requires the price argument for market ' + side + ' orders to calculate the total cost to spend(amount * price), alternatively set the ' + optionName + ' option or param to False and pass the cost to spend in the amount argument') + elif cost is None: + cost = Precise.string_mul(amountString, priceString) + else: + cost = cost if cost else amountString + result: dict = { + 'cost': cost, + 'params': params, + } + return result + + def handle_time_in_force_and_post_only(self, methodName: str, params: dict = {}, isMarketOrder: Bool = False): + timeInForce: Str = None + timeInForce, params = self.handle_option_and_params(params, methodName, 'timeInForce') + postOnly = False + postOnly, params = self.handle_post_only(isMarketOrder, timeInForce == 'post_only', params) + if postOnly: + timeInForce = 'PO' + timeInForce = self.encode_time_in_force(timeInForce) + result: dict = { + 'timeInForce': timeInForce, + 'params': params, + } + return result + + def create_swap_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}) -> Order: + """ + create a trade order on swap market + + https://coincatch.github.io/github.io/en/mix/#place-order + https://coincatch.github.io/github.io/en/mix/#place-plan-order + https://coincatch.github.io/github.io/en/mix/#place-stop-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of you want to trade in units of the base currency + :param float [price]: the price that the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.hedged]: must be set to True if position mode is hedged(default False) + :param bool [params.postOnly]: *non-trigger orders only* if True, the order will only be posted to the order book and not executed immediately + :param bool [params.reduceOnly]: True or False whether the order is reduce only + :param str [params.timeInForce]: *non-trigger orders only* 'GTC', 'FOK', 'IOC' or 'PO' + :param str [params.clientOrderId]: a unique id for the order + :param float [params.triggerPrice]: the price that the order is to be triggered at + :param float [params.stopLossPrice]: The price at which a stop loss order is triggered at + :param float [params.takeProfitPrice]: The price at which a take profit order is triggered at + :param dict [params.takeProfit]: *takeProfit object in params* containing the triggerPrice at which the attached take profit order will be triggered(perpetual swap markets only) + :param float [params.takeProfit.triggerPrice]: take profit trigger price + :param dict [params.stopLoss]: *stopLoss object in params* containing the triggerPrice at which the attached stop loss order will be triggered(perpetual swap markets only) + :param float [params.stopLoss.triggerPrice]: stop loss trigger price + :returns dict: an `order structure ` + """ + params['methodName'] = self.safe_string(params, 'methodName', 'createSwapOrder') + self.load_markets() + market = self.market(symbol) + request = self.create_swap_order_request(symbol, type, side, amount, price, params) + endpointType = self.safe_string(request, 'endpointType') + request = self.omit(request, 'endpointType') + response = None + if endpointType == 'trigger': + response = self.privatePostApiMixV1PlanPlacePlan(request) + elif endpointType == 'tpsl': + response = self.privatePostApiMixV1PlanPlaceTPSL(request) + else: # standard + response = self.privatePostApiMixV1OrderPlaceOrder(request) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1727977301979, + # "data": + # { + # "clientOid": "1225791137701519360", + # "orderId": "1225791137697325056" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data, market) + + def create_swap_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}) -> dict: + """ + @ignore + helper function to build request + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of you want to trade in units of the base currency + :param float [price]: the price that the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.hedged]: must be set to True if position mode is hedged(default False) + :param bool [params.postOnly]: *non-trigger orders only* if True, the order will only be posted to the order book and not executed immediately + :param bool [params.reduceOnly]: True or False whether the order is reduce only + :param str [params.timeInForce]: *non-trigger orders only* 'GTC', 'FOK', 'IOC' or 'PO' + :param str [params.clientOrderId]: a unique id for the order + :param float [params.triggerPrice]: the price that the order is to be triggered at + :param float [params.stopLossPrice]: The price at which a stop loss order is triggered at + :param float [params.takeProfitPrice]: The price at which a take profit order is triggered at + :param dict [params.takeProfit]: *takeProfit object in params* containing the triggerPrice at which the attached take profit order will be triggered(perpetual swap markets only) + :param float [params.takeProfit.triggerPrice]: take profit trigger price + :param dict [params.stopLoss]: *stopLoss object in params* containing the triggerPrice at which the attached stop loss order will be triggered(perpetual swap markets only) + :param float [params.stopLoss.triggerPrice]: stop loss trigger price + :returns dict: request to be sent to the exchange + """ + methodName = 'createSwapOrderRequest' + methodName, params = self.handle_param_string(params, 'methodName', methodName) + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'marginCoin': market['settleId'], + 'size': self.amount_to_precision(symbol, amount), + } + request, params = self.handle_option_params_and_request(params, methodName, 'clientOrderId', request, 'clientOid') + isMarketOrder = (type == 'market') + params = self.handle_trigger_stop_loss_and_take_profit(symbol, side, type, price, methodName, params) + endpointType = self.safe_string(params, 'endpointType') + if (endpointType is None) or (endpointType == 'standard'): + timeInForceAndParams = self.handle_time_in_force_and_post_only(methodName, params, isMarketOrder) # only for non-trigger orders + params = timeInForceAndParams['params'] + timeInForce = timeInForceAndParams['timeInForce'] + if timeInForce is not None: + request['timeInForceValue'] = timeInForce + if price is not None: + request['price'] = self.price_to_precision(symbol, price) + if (endpointType != 'tpsl'): + request['orderType'] = type + sideIsExchangeSpecific = False + hedged = False + if (side == 'buy_single') or (side == 'sell_single') or (side == 'open_long') or (side == 'open_short') or (side == 'close_long') or (side == 'close_short'): + sideIsExchangeSpecific = True + if (side != 'buy_single') and (side != 'sell_single'): + hedged = True + if not sideIsExchangeSpecific: + hedged, params = self.handle_option_and_params(params, methodName, 'hedged', hedged) + # hedged and non-hedged orders have different side values and reduceOnly handling + reduceOnly = self.safe_bool(params, 'reduceOnly') + if hedged: + if (reduceOnly is not None) and reduceOnly: + if side == 'buy': + side = 'close_short' + elif side == 'sell': + side = 'close_long' + else: + if side == 'buy': + side = 'open_long' + elif side == 'sell': + side = 'open_short' + else: + side = side.lower() + '_single' + if hedged: + params = self.omit(params, 'reduceOnly') + request['side'] = side + return self.extend(request, params) + + def handle_trigger_stop_loss_and_take_profit(self, symbol, side, type, price, methodName='createOrder', params={}): + request: dict = {} + endpointType = 'standard' # standard, trigger, tpsl, trailing - to define the endpoint to use + stopLossPrice = self.safe_string(params, 'stopLossPrice') + takeProfitPrice = self.safe_string(params, 'takeProfitPrice') + requestTriggerPrice: Str = None + takeProfitParams = self.safe_dict(params, 'takeProfit') + stopLossParams = self.safe_dict(params, 'stopLoss') + triggerPrice = self.safe_string_2(params, 'triggerPrice', 'stopPrice') + isTrigger = (triggerPrice is not None) + trailingPercent = self.safe_string(params, 'trailingPercent') + trailingTriggerPrice = self.safe_string(params, 'trailingTriggerPrice') + hasTPPrice = (takeProfitPrice is not None) + hasSLPrice = (stopLossPrice is not None) + hasTPParams = (takeProfitParams is not None) + if hasTPParams and not hasTPPrice: + takeProfitPrice = self.safe_string(takeProfitParams, 'triggerPrice') + hasTPPrice = (takeProfitPrice is not None) + hasSLParams = (stopLossParams is not None) + if hasSLParams and not hasSLPrice: + stopLossPrice = self.safe_string(stopLossParams, 'triggerPrice') + hasSLPrice = (stopLossPrice is not None) + hasBothTPAndSL = hasTPPrice and hasSLPrice + isTrailingPercentOrder = (trailingPercent is not None) + isMarketOrder = (type == 'market') + # handle with triggerPrice stopLossPrice and takeProfitPrice + if hasBothTPAndSL or isTrigger or (methodName == 'createOrderWithTakeProfitAndStopLoss'): + if isTrigger: + if isMarketOrder: + request['triggerType'] = 'market_price' + else: + request['triggerType'] = 'fill_price' + request['executePrice'] = self.price_to_precision(symbol, price) + request['triggerPrice'] = self.price_to_precision(symbol, triggerPrice) + endpointType = 'trigger' # if order also has triggerPrice we use endpoint for trigger orders + if methodName == 'createOrders': + endpointType = None # we do not provide endpointType for createOrders + if hasTPPrice: + request['presetTakeProfitPrice'] = takeProfitPrice + if hasSLPrice: + request['presetStopLossPrice'] = stopLossPrice + elif hasTPPrice or hasSLPrice or isTrailingPercentOrder: + if not isMarketOrder: + raise NotSupported(self.id + ' ' + methodName + '() supports does not support ' + type + ' type of stop loss and take profit orders(only market type is supported for stop loss and take profit orders). To create a market order with stop loss or take profit attached use createOrderWithTakeProfitAndStopLoss()') + endpointType = 'tpsl' # if order has only one of the two we use endpoint for tpsl orders + holdSide = 'long' + if side == 'buy': + holdSide = 'short' + request['holdSide'] = holdSide + if isTrailingPercentOrder: + if trailingTriggerPrice is None: + raise ArgumentsRequired(self.id + ' ' + methodName + '() requires the trailingTriggerPrice parameter for trailing stop orders') + requestTriggerPrice = trailingTriggerPrice + request['rangeRate'] = trailingPercent + request['planType'] = 'moving_plan' + elif hasTPPrice: # take profit + requestTriggerPrice = takeProfitPrice + request['planType'] = 'profit_plan' + else: # stop loss + requestTriggerPrice = stopLossPrice + request['planType'] = 'loss_plan' + request['triggerPrice'] = self.price_to_precision(symbol, requestTriggerPrice) + if endpointType is not None: + request['endpointType'] = endpointType + params = self.omit(params, ['stopLoss', 'takeProfit', 'stopLossPrice', 'takeProfitPrice', 'triggerPrice', 'stopPrice', 'trailingPercent', 'trailingTriggerPrice']) + return self.extend(request, params) + + def create_order_with_take_profit_and_stop_loss(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, takeProfit: Num = None, stopLoss: Num = None, params={}) -> Order: + """ + *swap markets only* create an order with a stop loss or take profit attached(type 3) + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency or the number of contracts + :param float [price]: the price to fulfill the order, in units of the quote currency, ignored in market orders + :param float [takeProfit]: the take profit price, in units of the quote currency + :param float [stopLoss]: the stop loss price, in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + methodName = 'createOrderWithTakeProfitAndStopLoss' + self.load_markets() + market = self.market(symbol) + if not market['swap']: + raise NotSupported(self.id + ' ' + methodName + '() is supported for swap markets only') + params['methodName'] = methodName + return super(coincatch, self).create_order_with_take_profit_and_stop_loss(symbol, type, side, amount, price, takeProfit, stopLoss, params) + + def encode_time_in_force(self, timeInForce: Str) -> Str: + timeInForceMap = { + 'GTC': 'normal', + 'IOC': 'iok', + 'FOK': 'fok', + 'PO': 'post_only', + } + return self.safe_string(timeInForceMap, timeInForce, timeInForce) + + def create_orders(self, orders: List[OrderRequest], params={}): + """ + create a list of trade orders(all orders should be of the same symbol) + + https://coincatch.github.io/github.io/en/spot/#batch-order + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params(max 50 entries) + :param dict [params]: extra parameters specific to the api endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + # same symbol for all orders + methodName = 'createOrders' + params['methodName'] = methodName + ordersRequests = [] + symbols = [] + for i in range(0, len(orders)): + rawOrder = orders[i] + symbol = self.safe_string(rawOrder, 'symbol') + symbols.append(symbol) + type = self.safe_string(rawOrder, 'type') + side = self.safe_string(rawOrder, 'side') + amount = self.safe_number(rawOrder, 'amount') + price = self.safe_number(rawOrder, 'price') + orderParams = self.safe_dict(rawOrder, 'params', {}) + orderRequest = self.create_order_request(symbol, type, side, amount, price, orderParams) + triggerPrice = self.safe_string(orderParams, 'triggerPrice') + if triggerPrice is not None: + raise NotSupported(self.id + ' ' + methodName + '() does not support trigger orders') + clientOrderId = self.safe_string(orderRequest, 'clientOrderId') + if clientOrderId is None: + orderRequest['clientOrderId'] = self.uuid() # both spot and swap endpoints require clientOrderId + ordersRequests.append(orderRequest) + symbols = self.unique(symbols) + symbolsLength = len(symbols) + if symbolsLength != 1: + raise BadRequest(self.id + ' createOrders() requires all orders to be of the same symbol') + ordersSymbol = self.safe_string(symbols, 0) + market = self.market(ordersSymbol) + request: dict = { + 'symbol': market['id'], + } + marketType = market['type'] + response = None + responseOrders = None + propertyName: Str = None + if marketType == 'spot': + request['orderList'] = ordersRequests + response = self.privatePostApiSpotV1TradeBatchOrders(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1726160718706, + # "data": { + # "resultList": [ + # { + # "orderId": "1218171835238367232", + # "clientOrderId": "28759338-ca10-42dd-8ac3-5183785ef60b" + # } + # ], + # "failure": [ + # { + # "orderId": "", + # "clientOrderId": "ee2e67c9-47fc-4311-9cc1-737ec408d509", + # "errorMsg": "The order price of eth_usdt cannot be less than 5.00% of the current price", + # "errorCode": "43008" + # }, + # { + # "orderId": "", + # "clientOrderId": "1af2defa-0c2d-4bb5-acb7-6feb6a86787a", + # "errorMsg": "less than the minimum amount 1 USDT", + # "errorCode": "45110" + # } + # ] + # } + # } + # + propertyName = 'resultList' + elif market['swap']: + request['marginCoin'] = market['settleId'] + request['orderDataList'] = ordersRequests + response = self.privatePostApiMixV1OrderBatchOrders(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1729100084017, + # "data": { + # "orderInfo": [ + # { + # "orderId": "1230500426827522049", + # "clientOid": "1230500426898825216" + # } + # ], + # "failure": [ + # { + # "orderId": "", + # "clientOid": null, + # "errorMsg": "The order price exceeds the maximum price limit: 2,642.53", + # "errorCode": "22047" + # } + # ], + # "result": True + # } + # } + # + propertyName = 'orderInfo' + else: + raise NotSupported(self.id + ' ' + methodName + '() is not supported for ' + marketType + ' type of markets') + data = self.safe_dict(response, 'data', {}) + responseOrders = self.safe_list(data, propertyName, []) + return self.parse_orders(responseOrders) + + def create_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}) -> dict: + methodName = self.safe_string(params, 'methodName', 'createOrderRequest') + params['methodName'] = methodName + market = self.market(symbol) + if market['spot']: + return self.create_spot_order_request(symbol, type, side, amount, price, params) + elif market['swap']: + return self.create_swap_order_request(symbol, type, side, amount, price, params) + else: + raise NotSupported(self.id + ' ' + methodName + '() is not supported for ' + market['type'] + ' type of markets') + + def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + """ + edit a trade trigger, stop-looss or take-profit order + + https://coincatch.github.io/github.io/en/spot/#modify-plan-order + + :param str id: order id + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + methodName = 'editOrder' + # only trigger, stop-looss or take-profit orders can be edited + params['methodName'] = methodName + self.load_markets() + market = self.market(symbol) + if market['spot']: + return self.edit_spot_order(id, symbol, type, side, amount, price, params) + else: + # todo return self.editSwapOrder(id, symbol, type, side, amount, price, params) + raise NotSupported(self.id + ' ' + methodName + '() is not supported for ' + market['type'] + ' type of markets') + + def edit_spot_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + @ignore + edit a trade order + + https://coincatch.github.io/github.io/en/spot/#modify-plan-order + + :param str id: order id + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.clientOrderId]: a unique id for the order that can be used alternative for the id + :param str params['triggerPrice']: *mandatory* the price that the order is to be triggered at + :param float [params.cost]: *market buy only* the quote quantity that can be used alternative for the amount + :returns dict: an `order structure ` + """ + self.load_markets() + methodName = 'editSpotOrder' + methodName, params = self.handle_param_string(params, 'methodName', methodName) + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' editSpotOrder() does not support ' + market['type'] + ' orders') + request: dict = { + 'orderType': type, + } + clientOrderId = self.safe_string(params, 'clientOrderId') + if clientOrderId is not None: + request['clientOid'] = clientOrderId + elif id is None: + raise BadRequest(self.id + ' ' + methodName + '() requires id or clientOrderId') + else: + request['orderId'] = id + cost: Str = None + cost, params = self.handle_param_string(params, 'cost') + isMarketBuy = (type == 'market') and (side == 'buy') + if (not isMarketBuy) and (cost is not None): + raise NotSupported(self.id + ' ' + methodName + '() supports cost parameter for market buy orders only') + if amount is not None: + if isMarketBuy: + costAndParams = self.handle_requires_price_and_cost(methodName, params, price, amount, cost) + cost = costAndParams['cost'] + params = costAndParams['params'] + else: + request['size'] = self.number_to_string(amount) # spot markets have no precision + if cost is not None: + request['size'] = cost # spot markets have no precision + if (type == 'limit') and (price is not None): + request['price'] = price # spot markets have no precision + response = self.privatePostApiSpotV1PlanModifyPlan(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1668136575920, + # "data": { + # "orderId": "974792060738441216", + # "clientOrderId": "974792554995224576" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data, market) + + def fetch_order(self, id: str, symbol: Str = None, params={}) -> Order: + """ + fetches information on an order made by the user(non-trigger orders only) + + https://coincatch.github.io/github.io/en/spot/#get-order-details + https://coincatch.github.io/github.io/en/mix/#get-order-details + + :param str id: the order id + :param str symbol: unified symbol of the market the order was made in(is mandatory for swap) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: 'spot' or 'swap' - the type of the market to fetch entry for(default 'spot') + :param str [params.clientOrderId]: a unique id for the order that can be used alternative for the id + :returns dict: An `order structure ` + """ + methodName = 'fetchOrder' + # for non-trigger orders only + self.load_markets() + request: dict = {} + clientOrderId = self.safe_string(params, 'clientOrderId') + if clientOrderId is None: + request['orderId'] = id + market: Market = None + if symbol is not None: + market = self.market(symbol) + marketType = 'spot' + marketType, params = self.handle_market_type_and_params(methodName, market, params, marketType) + response = None + order: dict = None + if marketType == 'spot': + # user could query cancelled/filled order details within 24 hours, After 24 hours should use fetchOrders + response = self.privatePostApiSpotV1TradeOrderInfo(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1725918004434, + # "data": [ + # { + # "accountId": "1002820815393", + # "symbol": "ETHUSDT_SPBL", + # "orderId": "1217143186968068096", + # "clientOrderId": "8fa3eb89-2377-4519-a199-35d5db9ed262", + # "price": "0", + # "quantity": "10.0000000000000000", + # "orderType": "market", + # "side": "buy", + # "status": "full_fill", + # "fillPrice": "2340.5500000000000000", + # "fillQuantity": "0.0042000000000000", + # "fillTotalAmount": "9.8303100000000000", + # "enterPointSource": "API", + # "feeDetail": "{ + # \"ETH\": { + # \"deduction\": False, + # \"feeCoinCode\": \"ETH\", + # \"totalDeductionFee\": 0, + # \"totalFee\": -0.0000042000000000}, + # \"newFees\": { + # \"c\": 0, + # \"d\": 0, + # \"deduction\": False, + # \"r\": -0.0000042, + # \"t\": -0.0000042, + # \"totalDeductionFee\": 0 + # } + # }", + # "orderSource": "market", + # "cTime": "1725915469877" + # } + # ] + # } + # + data = self.safe_list(response, 'data') + if data is None: + response = json.loads(response) # the response from closed orders is not a standard JSON + data = self.safe_list(response, 'data', []) + order = self.safe_dict(data, 0, {}) + elif marketType == 'swap': + if market is None: + raise ArgumentsRequired(self.id + ' ' + methodName + '() requires a symbol argument for ' + marketType + ' type of markets') + request['symbol'] = market['id'] + if clientOrderId is not None: + params = self.omit(params, 'clientOrderId') + request['clientOid'] = clientOrderId + response = self.privateGetApiMixV1OrderDetail(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1727981421364, + # "data": { + # "symbol": "ETHUSDT_UMCBL", + # "size": 0.01, + # "orderId": "1225791137697325056", + # "clientOid": "1225791137701519360", + # "filledQty": 0.01, + # "fee": -0.01398864, + # "price": null, + # "priceAvg": 2331.44, + # "state": "filled", + # "side": "close_long", + # "timeInForce": "normal", + # "totalProfits": -2.23680000, + # "posSide": "long", + # "marginCoin": "USDT", + # "filledAmount": 23.3144, + # "orderType": "market", + # "leverage": "5", + # "marginMode": "crossed", + # "reduceOnly": True, + # "enterPointSource": "API", + # "tradeSide": "close_long", + # "holdMode": "double_hold", + # "orderSource": "market", + # "cTime": "1727977302003", + # "uTime": "1727977303604" + # } + # } + # + order = self.safe_dict(response, 'data', {}) + else: + raise NotSupported(self.id + ' ' + methodName + '() is not supported for ' + marketType + ' type of markets') + return self.parse_order(order, market) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://coincatch.github.io/github.io/en/spot/#get-order-list + https://coincatch.github.io/github.io/en/spot/#get-current-plan-orders + https://coincatch.github.io/github.io/en/mix/#get-open-order + https://coincatch.github.io/github.io/en/mix/#get-all-open-order + https://coincatch.github.io/github.io/en/mix/#get-plan-order-tpsl-list + + :param str [symbol]: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: True if fetching trigger orders(default False) + :param str [params.type]: 'spot' or 'swap' - the type of the market to fetch entries for(default 'spot') + :param str [params.productType]: *swap only* 'umcbl' or 'dmcbl' - the product type of the market to fetch entries for(default 'umcbl') + :param str [params.marginCoin]: *swap only* the margin coin of the market to fetch entries for + :param str [params.isPlan]: *swap trigger only* 'plan' or 'profit_loss'('plan'(default) for trigger(plan) orders, 'profit_loss' for stop-loss and take-profit orders) + :returns Order[]: a list of `order structures ` + """ + methodName = 'fetchOpenOrders' + self.load_markets() + market: Market = None + if symbol is not None: + market = self.market(symbol) + marketType = 'spot' + marketType, params = self.handle_market_type_and_params(methodName, market, params, marketType) + params['methodName'] = methodName + if marketType == 'spot': + return self.fetch_open_spot_orders(symbol, since, limit, params) + elif marketType == 'swap': + return self.fetch_open_swap_orders(symbol, since, limit, params) + else: + raise NotSupported(self.id + ' ' + methodName + '() is not supported for ' + marketType + ' type of markets') + + def fetch_open_spot_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + @ignore + fetch all unfilled currently open orders for spot markets + + https://coincatch.github.io/github.io/en/spot/#get-order-list + https://coincatch.github.io/github.io/en/spot/#get-current-plan-orders + + :param str [symbol]: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: True if fetching trigger orders(default False) + :param str [params.lastEndId]: *for trigger orders only* the last order id to fetch entries after + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + methodName = 'fetchOpenSpotOrders' + methodName, params = self.handle_param_string(params, 'methodName', methodName) + request: dict = {} + market: Market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + isTrigger = False + isTrigger, params = self.handle_option_and_params_2(params, methodName, 'trigger', 'stop', isTrigger) + result = None + if isTrigger: + if symbol is None: + raise ArgumentsRequired(self.id + ' ' + methodName + '() requires a symbol argument for trigger orders') + if limit is not None: + request['pageSize'] = limit + response = self.privatePostApiSpotV1PlanCurrentPlan(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1728664710749, + # "data": { + # "nextFlag": False, + # "endId": 1228661660806787072, + # "orderList": [ + # { + # "orderId": "1228669617606991872", + # "clientOid": "1228669617573437440", + # "symbol": "ETHUSDT_SPBL", + # "size": "50", + # "executePrice": "0", + # "triggerPrice": "4000", + # "status": "not_trigger", + # "orderType": "market", + # "side": "sell", + # "triggerType": "fill_price", + # "enterPointSource": "API", + # "placeType": null, + # "cTime": "1728663585092", + # "uTime": null + # }, + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + result = self.safe_list(data, 'orderList', []) + else: + response = self.privatePostApiSpotV1TradeOpenOrders(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1725965783430, + # "data": [ + # { + # "accountId": "1002820815393", + # "symbol": "ETHUSDT_SPBL", + # "orderId": "1217347655911653376", + # "clientOrderId": "c57c07d1-bd00-4167-95e2-9b22a55fbc28", + # "price": "2000.0000000000000000", + # "quantity": "0.0010000000000000", + # "orderType": "limit", + # "side": "buy", + # "status": "new", + # "fillPrice": "0", + # "fillQuantity": "0.0000000000000000", + # "fillTotalAmount": "0.0000000000000000", + # "enterPointSource": "API", + # "feeDetail": "", + # "orderSource": "normal", + # "cTime": "1725964219072" + # }, + # ... + # ] + # } + # + result = self.safe_list(response, 'data', []) + return self.parse_orders(result, market, since, limit) + + def fetch_open_swap_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + @ignore + fetch all unfilled currently open orders for swap markets + + https://coincatch.github.io/github.io/en/mix/#get-open-order + https://coincatch.github.io/github.io/en/mix/#get-all-open-order + https://coincatch.github.io/github.io/en/mix/#get-plan-order-tpsl-list + + :param str [symbol]: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: True if fetching trigger orders(default False) + :param str [params.isPlan]: 'plan' or 'profit_loss'('plan'(default) for trigger(plan) orders, 'profit_loss' for stop-loss and take-profit orders) + :param str [params.productType]: 'umcbl' or 'dmcbl' - the product type of the market to fetch entries for(default 'umcbl') + :param str [params.marginCoin]: the margin coin of the market to fetch entries for + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + methodName = 'fetchOpenSwapOrders' + methodName, params = self.handle_param_string(params, 'methodName', methodName) + isTrigger = False + isTrigger, params = self.handle_option_and_params_2(params, methodName, 'trigger', 'stop', isTrigger) + plan: Str = None + plan, params = self.handle_option_and_params(params, methodName, 'isPlan', plan) + productType = self.handle_option(methodName, 'productType') + market: Market = None + response = None + if symbol is not None: + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if (isTrigger) or (plan is not None): # the same endpoint is used for trigger and stop-loss/take-profit orders + if productType is not None: + request['productType'] = productType + if plan is not None: + request['isPlan'] = plan # current param is used to define the type of the orders to fetch(trigger or stop-loss/take-profit) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1729168682690, + # "data": [ + # { + # "orderId": "1230779428914049025", + # "clientOid": "1230779428914049024", + # "symbol": "ETHUSDT_UMCBL", + # "marginCoin": "USDT", + # "size": "0.01", + # "executePrice": "1000", + # "triggerPrice": "1200", + # "status": "not_trigger", + # "orderType": "limit", + # "planType": "normal_plan", + # "side": "buy_single", + # "triggerType": "fill_price", + # "presetTakeProfitPrice": "4000", + # "presetTakeLossPrice": "900", + # "rangeRate": "", + # "enterPointSource": "API", + # "tradeSide": "buy_single", + # "holdMode": "single_hold", + # "reduceOnly": False, + # "cTime": "1729166603306", + # "uTime": null + # } + # ] + # } + # + response = self.privateGetApiMixV1PlanCurrentPlan(self.extend(request, params)) + else: + response = self.privateGetApiMixV1OrderCurrent(self.extend(request, params)) + elif isTrigger: + raise ArgumentsRequired(self.id + ' ' + methodName + '() requires a symbol argument for swap trigger orders') + else: + if productType is None: + productType = 'umcbl' + request: dict = { + 'productType': productType, # is mandatory for current endpoint(all open non-trigger orders) + } + marginCoin: Str = None + marginCoin = self.handle_option(methodName, 'marginCoin', marginCoin) + if marginCoin is not None: + request['marginCoin'] = marginCoin + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1728127869097, + # "data": [ + # { + # "symbol": "ETHUSDT_UMCBL", + # "size": 0.02, + # "orderId": "1226422495431974913", + # "clientOid": "1226422495457140736", + # "filledQty": 0.00, + # "fee": 0E-8, + # "price": 500.00, + # "state": "new", + # "side": "buy_single", + # "timeInForce": "normal", + # "totalProfits": 0E-8, + # "posSide": "long", + # "marginCoin": "USDT", + # "filledAmount": 0.0000, + # "orderType": "limit", + # "leverage": "5", + # "marginMode": "crossed", + # "reduceOnly": False, + # "enterPointSource": "API", + # "tradeSide": "buy_single", + # "holdMode": "single_hold", + # "orderSource": "normal", + # "cTime": "1728127829422", + # "uTime": "1728127830980" + # } + # ] + # } + # + response = self.privateGetApiMixV1OrderMarginCoinCurrent(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + return self.parse_orders(data, market, since, limit) + + def fetch_canceled_and_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple canceled and closed orders made by the user + + https://coincatch.github.io/github.io/en/spot/#get-order-list + https://coincatch.github.io/github.io/en/spot/#get-history-plan-orders + https://coincatch.github.io/github.io/en/mix/#get-history-orders + https://coincatch.github.io/github.io/en/mix/#get-producttype-history-orders + https://coincatch.github.io/github.io/en/mix/#get-history-plan-orders-tpsl + + :param str symbol: *is mandatory* unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch orders for + :param boolean [params.trigger]: True if fetching trigger orders(default False) + :param str [params.isPlan]: *swap only* 'plan' or 'profit_loss'('plan'(default) for trigger(plan) orders, 'profit_loss' for stop-loss and take-profit orders) + :param str [params.type]: 'spot' or 'swap' - the type of the market to fetch entries for(default 'spot') + :param str [params.productType]: *swap only* 'umcbl' or 'dmcbl' - the product type of the market to fetch entries for(default 'umcbl') + :returns Order[]: a list of `order structures ` + """ + methodName = 'fetchCanceledAndClosedOrders' + self.load_markets() + market: Market = None + if symbol is not None: + market = self.market(symbol) + marketType = 'spot' + marketType, params = self.handle_market_type_and_params(methodName, market, params, marketType) + params['methodName'] = methodName + if marketType == 'spot': + return self.fetch_canceled_and_closed_spot_orders(symbol, since, limit, params) + elif marketType == 'swap': + return self.fetch_canceled_and_closed_swap_orders(symbol, since, limit, params) + else: + raise NotSupported(self.id + ' ' + methodName + '() is not supported for ' + marketType + ' type of markets') + + def fetch_canceled_and_closed_spot_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + @ignore + fetches information on multiple canceled and closed orders made by the user on spot markets + + https://coincatch.github.io/github.io/en/spot/#get-order-history + https://coincatch.github.io/github.io/en/spot/#get-history-plan-orders + + :param str symbol: *is mandatory* unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: *for trigger orders only* the latest time in ms to fetch orders for + :param boolean [params.trigger]: True if fetching trigger orders(default False) + :param str [params.lastEndId]: *for trigger orders only* the last order id to fetch entries after + :returns Order[]: a list of `order structures ` + """ + methodName = 'fetchCanceledAndClosedSpotOrders' + methodName, params = self.handle_param_string(params, 'methodName', methodName) + if symbol is None: + raise ArgumentsRequired(self.id + ' ' + methodName + '() requires a symbol argument for spot markets') + maxLimit = 500 + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + requestLimit = limit + isTrigger = False + isTrigger, params = self.handle_option_and_params_2(params, methodName, 'trigger', 'stop', isTrigger) + result = None + if isTrigger: + until: Int = None + until, params = self.handle_option_and_params(params, methodName, 'until', until) + # now = self.milliseconds() + requestSince = since + interval = 90 * 24 * 60 * 60 * 1000 # startTime and endTime interval cannot be greater than 90 days + now = self.milliseconds() + # both since and until are required for trigger orders + if (until is None) and (requestSince is None): + requestSince = now - interval + until = now + elif until is not None: + requestSince = until - interval + else: # if since is defined + until = since + interval + request['startTime'] = requestSince + request['endTime'] = until + if requestLimit is None: + requestLimit = maxLimit + request['pageSize'] = requestLimit + response = self.privatePostApiSpotV1PlanHistoryPlan(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1728668998002, + # "data": { + # "nextFlag": False, + # "endId": 1228669617606991872, + # "orderList": [ + # { + # "orderId": "1228669617606991872", + # "clientOid": "1228669617573437440", + # "symbol": "ETHUSDT_SPBL", + # "size": "50", + # "executePrice": "0", + # "triggerPrice": "4000", + # "status": "cancel", + # "orderType": "market", + # "side": "sell", + # "triggerType": "fill_price", + # "enterPointSource": "API", + # "placeType": null, + # "cTime": "1728663585092", + # "uTime": "1728666719223" + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + result = self.safe_list(data, 'orderList', []) + else: + if since is not None: + request['after'] = since + requestLimit = maxLimit + if requestLimit is not None: + request['limit'] = requestLimit + response = self.privatePostApiSpotV1TradeHistory(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1725963777690, + # "data": [ + # { + # "accountId": "1002820815393", + # "symbol": "ETHUSDT_SPBL", + # "orderId": "1217143186968068096", + # "clientOrderId": "8fa3eb89-2377-4519-a199-35d5db9ed262", + # "price": "0", + # "quantity": "10.0000000000000000", + # "orderType": "market", + # "side": "buy", + # "status": "full_fill", + # "fillPrice": "2340.5500000000000000", + # "fillQuantity": "0.0042000000000000", + # "fillTotalAmount": "9.8303100000000000", + # "enterPointSource": "API", + # "feeDetail": "{ + # \"ETH\": { + # \"deduction\": False, + # \"feeCoinCode\": \"ETH\", + # \"totalDeductionFee\": 0, + # \"totalFee\": -0.0000042000000000 + # }, + # \"newFees\": { + # \"c\": 0, + # \"d\": 0, + # \"deduction\": False, + # \"r\": -0.0000042, + # \"t\": -0.0000042, + # \"totalDeductionFee\": 0 + # } + # }", + # "orderSource": "market", + # "cTime": "1725915469877" + # }, + # ... + # ] + # } + # + parsedResponse = json.loads(response) # the response is not a standard JSON + result = self.safe_list(parsedResponse, 'data', []) + return self.parse_orders(result, market, since, limit) + + def fetch_canceled_and_closed_swap_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + @ignore + fetches information on multiple canceled and closed orders made by the user on swap markets + + https://coincatch.github.io/github.io/en/mix/#get-history-orders + https://coincatch.github.io/github.io/en/mix/#get-producttype-history-orders + https://coincatch.github.io/github.io/en/mix/#get-history-plan-orders-tpsl + + :param str [symbol]: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch orders for + :param boolean [params.trigger]: True if fetching trigger orders(default False) + :param str [params.isPlan]: *swap only* 'plan' or 'profit_loss'('plan'(default) for trigger(plan) orders, 'profit_loss' for stop-loss and take-profit orders) + :param str [params.productType]: *swap only* 'umcbl' or 'dmcbl' - the product type of the market to fetch entries for(default 'umcbl') + :returns Order[]: a list of `order structures ` + """ + methodName = 'fetchCanceledAndClosedSwapOrders' + methodName, params = self.handle_param_string(params, 'methodName', methodName) + requestSince = since + until: Int = None + until, params = self.handle_option_and_params(params, methodName, 'until', until) + now = self.milliseconds() + # since and until are mandatory + # they should be within 90 days interval + interval = 90 * 24 * 60 * 60 * 1000 + if (until is None) and (requestSince is None): + requestSince = now - interval + until = now + elif until is not None: + requestSince = until - interval + else: # if since is defined + until = since + interval + request: dict = { + 'startTime': requestSince, + 'endTime': until, + } + if limit is not None: + request['pageSize'] = limit + market: Market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + productType = self.handle_option(methodName, 'productType') + isTrigger = False + isTrigger, params = self.handle_option_and_params_2(params, methodName, 'trigger', 'stop', isTrigger) + plan: Str = None + plan, params = self.handle_option_and_params(params, methodName, 'isPlan', plan) + response = None + result = None + if (isTrigger) or (plan is not None): + if plan is not None: + request['isPlan'] = plan + if productType is not None: + request['productType'] = productType + response = self.privateGetApiMixV1PlanHistoryPlan(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1729174716526, + # "data": [ + # { + # "orderId": "1230763430987104257", + # "clientOid": "1230763431003881472", + # "executeOrderId": "", + # "symbol": "ETHUSDT_UMCBL", + # "marginCoin": "USDT", + # "size": "0.03", + # "executePrice": "0", + # "triggerPrice": "2000", + # "status": "cancel", + # "orderType": "market", + # "planType": "loss_plan", + # "side": "sell_single", + # "triggerType": "fill_price", + # "presetTakeProfitPrice": "0", + # "presetTakeLossPrice": "0", + # "rangeRate": null, + # "enterPointSource": "SYS", + # "tradeSide": "sell_single", + # "holdMode": "single_hold", + # "reduceOnly": True, + # "executeTime": "1729173770776", + # "executeSize": "0", + # "cTime": "1729162789103", + # "uTime": "1729173770776" + # } + # ] + # } + # + result = self.safe_list(response, 'data', []) + else: + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + response = self.privateGetApiMixV1OrderHistory(self.extend(request, params)) + else: + if productType is None: + productType = 'umcbl' # is mandatory for current endpoint + request['productType'] = productType + response = self.privateGetApiMixV1OrderHistoryProductType(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1728129807637, + # "data": { + # "nextFlag": False, + # "endId": "1221413696648339457", + # "orderList": [ + # { + # "symbol": "ETHUSD_DMCBL", + # "size": 0.1, + # "orderId": "1225467075288719360", + # "clientOid": "1225467075288719361", + # "filledQty": 0.1, + # "fee": -0.00005996, + # "price": null, + # "priceAvg": 2362.03, + # "state": "filled", + # "side": "burst_close_long", + # "timeInForce": "normal", + # "totalProfits": -0.00833590, + # "posSide": "long", + # "marginCoin": "ETH", + # "filledAmount": 236.20300000, + # "orderType": "market", + # "leverage": "12", + # "marginMode": "fixed", + # "reduceOnly": True, + # "enterPointSource": "SYS", + # "tradeSide": "burst_close_long", + # "holdMode": "double_hold", + # "orderSource": "market", + # "cTime": "1727900039503", + # "uTime": "1727900039576" + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + result = self.safe_list(data, 'orderList', []) + return self.parse_orders(result, market) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://coincatch.github.io/github.io/en/spot/#cancel-order-v2 + https://coincatch.github.io/github.io/en/spot/#cancel-plan-order + https://coincatch.github.io/github.io/en/mix/#cancel-order + https://coincatch.github.io/github.io/en/mix/#cancel-plan-order-tpsl + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.clientOrderId]: a unique id for the order that can be used alternative for the id + :param bool [params.trigger]: True for canceling a trigger order(default False) + :param bool [params.stop]: *swap only* an alternative for trigger param + :param str [params.planType]: *swap trigger only* the type of the plan order to cancel: 'profit_plan' - profit order, 'loss_plan' - loss order, 'normal_plan' - plan order, 'pos_profit' - position profit, 'pos_loss' - position loss, 'moving_plan' - Trailing TP/SL, 'track_plan' - Trailing Stop + :returns dict: An `order structure ` + """ + methodName = 'cancelOrder' + if symbol is None: + raise ArgumentsRequired(self.id + ' ' + methodName + '() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = {} + clientOrderId: Str = None + clientOrderId, params = self.handle_param_string(params, 'clientOrderId') + if (id is None) and (clientOrderId is None): + raise ArgumentsRequired(self.id + ' ' + methodName + '() requires an id argument or clientOrderId parameter') + if clientOrderId is not None: + request['clientOid'] = clientOrderId + else: + request['orderId'] = id + marketType = market['type'] + trigger = False + trigger, params = self.handle_option_and_params_2(params, methodName, 'trigger', 'stop', trigger) + response = None + if not trigger or (marketType != 'spot'): + request['symbol'] = market['id'] + if marketType == 'spot': + if trigger: + response = self.privatePostApiSpotV1PlanCancelPlan(self.extend(request, params)) + else: + response = self.privatePostApiSpotV1TradeCancelOrderV2(self.extend(request, params)) + elif marketType == 'swap': + planType: Str = None + planType, params = self.handle_option_and_params(params, methodName, 'planType', planType) + request['marginCoin'] = market['settleId'] + if (trigger) or (planType is not None): + if planType is None: + raise ArgumentsRequired(self.id + ' ' + methodName + '() requires a planType parameter for swap trigger orders("profit_plan" - profit order, "loss_plan" - loss order, "normal_plan" - plan order, "pos_profit" - position profit, "pos_loss" - position loss, "moving_plan" - Trailing TP/SL, "track_plan" - Trailing Stop)') + request['planType'] = planType + response = self.privatePostApiMixV1PlanCancelPlan(self.extend(request, params)) + else: + response = self.privatePostApiMixV1OrderCancelOrder(self.extend(request, params)) + else: + raise NotSupported(self.id + ' ' + methodName + '() is not supported for ' + marketType + ' type of markets') + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data, market) + + def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancels all open orders + + https://coincatch.github.io/github.io/en/spot/#cancel-all-orders + https://coincatch.github.io/github.io/en/spot/#batch-cancel-plan-orders + https://coincatch.github.io/github.io/en/mix/#batch-cancel-order + https://coincatch.github.io/github.io/en/mix/#cancel-order-by-symbol + https://coincatch.github.io/github.io/en/mix/#cancel-plan-order-tpsl-by-symbol + https://coincatch.github.io/github.io/en/mix/#cancel-all-trigger-order-tpsl + + :param str [symbol]: unified symbol of the market the orders were made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: 'spot' or 'swap' - the type of the market to cancel orders for(default 'spot') + :param bool [params.trigger]: True for canceling a trigger orders(default False) + :param str [params.productType]: *swap only(if symbol is not provided* 'umcbl' or 'dmcbl' - the product type of the market to cancel orders for(default 'umcbl') + :param str [params.marginCoin]: *mandatory for swap non-trigger dmcb(if symbol is not provided)* the margin coin of the market to cancel orders for + :param str [params.planType]: *swap trigger only* the type of the plan order to cancel: 'profit_plan' - profit order, 'loss_plan' - loss order, 'normal_plan' - plan order, 'pos_profit' - position profit, 'pos_loss' - position loss, 'moving_plan' - Trailing TP/SL, 'track_plan' - Trailing Stop + :returns dict: response from the exchange + """ + methodName = 'cancelAllOrders' + self.load_markets() + market: Market = None + if symbol is not None: + market = self.market(symbol) + request: dict = {} + marketType = 'spot' + marketType, params = self.handle_market_type_and_params(methodName, market, params, marketType) + trigger = False + trigger, params = self.handle_option_and_params_2(params, methodName, 'trigger', 'stop', trigger) + response = None + if marketType == 'spot': + if trigger: + if symbol is not None: + request['symbols'] = [market['id']] + response = self.privatePostApiSpotV1PlanBatchCancelPlan(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1728670464735, + # "data": [ + # { + # "orderId": "1228661660806787072", + # "clientOid": "1228661660752261120", + # "result": True + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_orders(data, market) + else: + if symbol is None: + raise ArgumentsRequired(self.id + ' ' + methodName + '() requires a symbol argument for spot non-trigger orders') + request['symbol'] = market['id'] + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1725989560461, + # "data": "ETHUSDT_SPBL" + # } + # + response = self.privatePostApiSpotV1TradeCancelSymbolOrder(self.extend(request, params)) + elif marketType == 'swap': + productType = 'umcbl' + if symbol is not None: + request['symbol'] = market['id'] + else: + productType = self.handle_option(methodName, 'productType', productType) + request['productType'] = productType # we need either symbol or productType + planType: Str = None + planType, params = self.handle_option_and_params(params, methodName, 'planType', planType) + if (trigger) or (planType is not None): # if trigger or stop-loss/take-profit orders + if planType is None: + raise ArgumentsRequired(self.id + ' ' + methodName + '() requires a planType parameter for swap trigger orders("profit_plan" - profit order, "loss_plan" - loss order, "normal_plan" - plan order, "pos_profit" - position profit, "pos_loss" - position loss, "moving_plan" - Trailing TP/SL, "track_plan" - Trailing Stop)') + request['planType'] = planType + if symbol is not None: + response = self.privatePostApiMixV1PlanCancelSymbolPlan(self.extend(request, params)) + else: + response = self.privatePostApiMixV1PlanCancelAllPlan(self.extend(request, params)) + elif symbol is not None: # if non-trigger orders and symbol is provided + request['marginCoin'] = market['settleId'] + response = self.privatePostApiMixV1OrderCancelSymbolOrders(self.extend(request, params)) + else: # if non-trigger orders and symbol is not provided + marginCoin: Str = None + if productType == 'umcbl': + marginCoin = 'USDT' + else: + marginCoin, params = self.handle_option_and_params(params, methodName, 'marginCoin', marginCoin) + if marginCoin is None: + raise ArgumentsRequired(self.id + ' ' + methodName + '() requires a marginCoin parameter for dmcbl product type') + request['marginCoin'] = marginCoin + response = self.privatePostApiMixV1OrderCancelAllOrders(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1729104940774, + # "data": { + # "result": True, + # "order_ids": ["1230500426827522049"], + # "client_order_ids": ["1230500426898825216"], + # "fail_infos": [] + # } + # } + # + result = self.get_result_from_batch_canceling_swap_orders(response) + return self.parse_orders(result, market) + else: + raise NotSupported(self.id + ' ' + methodName + '() is not supported for ' + marketType + ' type of markets') + order = self.safe_order(response) + order['info'] = response + return [order] + + def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): + """ + cancel multiple non-trigger orders + + https://coincatch.github.io/github.io/en/spot/#cancel-order-in-batch-v2-single-instruments + + :param str[] ids: order ids + :param str symbol: *is mandatory* unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str[] [params.clientOrderIds]: client order ids + :returns dict: an list of `order structures ` + """ + methodName = 'cancelOrders' + # only non-trigger and not tp/sl orders can be canceled via cancelOrders + if symbol is None: + raise ArgumentsRequired(self.id + ' ' + methodName + '() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request = { + 'symbol': market['id'], + } + marketType = market['type'] + clientOrderIds = self.safe_list(params, 'clientOrderIds') + if clientOrderIds is not None: + request['clientOids'] = clientOrderIds + params = self.omit(params, 'clientOrderIds') + elif ids is None: + raise ArgumentsRequired(self.id + ' ' + methodName + '() requires either ids argument or clientOrderIds parameter') + else: + request['orderIds'] = ids + response = None + result = None + if marketType == 'spot': + response = self.privatePostApiSpotV1TradeCancelBatchOrdersV2(self.extend(request)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1726491486352, + # "data": { + # "resultList": [ + # { + # "orderId": "1219555778395160576", + # "clientOrderId": "e229d70a-bb16-4633-a45c-d7f4d3b5d2cf" + # } + # ], + # "failure": [ + # { + # "orderId": "123124124", + # "clientOrderId": null, + # "errorMsg": "The order does not exist", + # "errorCode": "43001" + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + result = self.safe_list(data, 'resultList', []) + elif marketType == 'swap': + request['marginCoin'] = market['settleId'] + response = self.privatePostApiMixV1OrderCancelBatchOrders(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1729101962321, + # "data": { + # "result": True, + # "symbol": "ETHUSDT_UMCBL", + # "order_ids": ["1226441551501418496", "1230506854262857729"], + # "client_order_ids": [], + # "fail_infos": [] + # } + # } + # + result = self.get_result_from_batch_canceling_swap_orders(response) + else: + raise NotSupported(self.id + ' ' + methodName + '() is not supported for ' + marketType + ' type of markets') + return self.parse_orders(result, market) + + def get_result_from_batch_canceling_swap_orders(self, response): + data = self.safe_dict(response, 'data', {}) + result = [] + orderIds = self.safe_value(data, 'order_ids', []) + for i in range(0, len(orderIds)): + orderId = orderIds[i] + resultItem = { + 'orderId': orderId, + } + result.append(resultItem) + return result + + def parse_order(self, order, market=None) -> Order: + # + # createOrder spot + # { + # "orderId": "1217143186968068096", + # "clientOrderId": "8fa3eb89-2377-4519-a199-35d5db9ed262" + # } + # + # createOrder swap + # { + # "clientOid": "1225791137701519360", + # "orderId": "1225791137697325056" + # } + # + # privatePostApiSpotV1TradeOrderInfo, privatePostApiSpotV1TradeHistory + # { + # "accountId": "1002820815393", + # "symbol": "ETHUSDT_SPBL", + # "orderId": "1217143186968068096", + # "clientOrderId": "8fa3eb89-2377-4519-a199-35d5db9ed262", + # "price": "0", + # "quantity": "10.0000000000000000", + # "orderType": "market", + # "side": "buy", + # "status": "full_fill", + # "fillPrice": "2340.5500000000000000", + # "fillQuantity": "0.0042000000000000", + # "fillTotalAmount": "9.8303100000000000", + # "enterPointSource": "API", + # "feeDetail": "{ + # \"ETH\": { + # \"deduction\": False, + # \"feeCoinCode\": \"ETH\", + # \"totalDeductionFee\": 0, + # \"totalFee\": -0.0000042000000000}, + # \"newFees\": { + # \"c\": 0, + # \"d\": 0, + # \"deduction\": False, + # \"r\": -0.0000042, + # \"t\": -0.0000042, + # \"totalDeductionFee\": 0 + # } + # }", + # "orderSource": "market", + # "cTime": "1725915469877" + # } + # + # privatePostApiMixV1OrderDetail, privateGetApiMixV1OrderMarginCoinCurrent + # { + # "symbol": "ETHUSDT_UMCBL", + # "size": 0.01, + # "orderId": "1225791137697325056", + # "clientOid": "1225791137701519360", + # "filledQty": 0.01, + # "fee": -0.01398864, + # "price": null, + # "priceAvg": 2331.44, + # "state": "filled", + # "side": "close_long", + # "timeInForce": "normal", + # "totalProfits": -2.23680000, + # "posSide": "long", + # "marginCoin": "USDT", + # "filledAmount": 23.3144, + # "orderType": "market", + # "leverage": "5", + # "marginMode": "crossed", + # "reduceOnly": True, + # "enterPointSource": "API", + # "tradeSide": "close_long", + # "holdMode": "double_hold", + # "orderSource": "market", + # "cTime": "1727977302003", + # "uTime": "1727977303604" + # } + # + # privatePostApiSpotV1TradeOpenOrders + # { + # "accountId": "1002820815393", + # "symbol": "ETHUSDT_SPBL", + # "orderId": "1217347655911653376", + # "clientOrderId": "c57c07d1-bd00-4167-95e2-9b22a55fbc28", + # "price": "2000.0000000000000000", + # "quantity": "0.0010000000000000", + # "orderType": "limit", + # "side": "buy", + # "status": "new", + # "fillPrice": "0", + # "fillQuantity": "0.0000000000000000", + # "fillTotalAmount": "0.0000000000000000", + # "enterPointSource": "API", + # "feeDetail": "", + # "orderSource": "normal", + # "cTime": "1725964219072" + # } + # + # privatePostApiSpotV1PlanCurrentPlan, privatePostApiSpotV1PlanHistoryPlan + # { + # "orderId": "1228669617606991872", + # "clientOid": "1228669617573437440", + # "symbol": "ETHUSDT_SPBL", + # "size": "50", + # "executePrice": "0", + # "triggerPrice": "4000", + # "status": "not_trigger", + # "orderType": "market", + # "side": "sell", + # "triggerType": "fill_price", + # "enterPointSource": "API", + # "placeType": null, + # "cTime": "1728663585092", + # "uTime": null + # } + # + # privateGetApiMixV1PlanCurrentPlan + # { + # "orderId": "1230779428914049025", + # "clientOid": "1230779428914049024", + # "symbol": "ETHUSDT_UMCBL", + # "marginCoin": "USDT", + # "size": "0.01", + # "executePrice": "1000", + # "triggerPrice": "1200", + # "status": "not_trigger", + # "orderType": "limit", + # "planType": "normal_plan", + # "side": "buy_single", + # "triggerType": "fill_price", + # "presetTakeProfitPrice": "4000", + # "presetTakeLossPrice": "900", + # "rangeRate": "", + # "enterPointSource": "API", + # "tradeSide": "buy_single", + # "holdMode": "single_hold", + # "reduceOnly": False, + # "cTime": "1729166603306", + # "uTime": null + # } + # + marketId = self.safe_string(order, 'symbol') + marginCoin = self.safe_string(order, 'marginCoin') + market = self.safe_market_custom(marketId, market, marginCoin) + timestamp = self.safe_integer(order, 'cTime') + price = self.omit_zero(self.safe_string_2(order, 'price', 'executePrice')) # price is zero for market orders + priceAvg = self.omit_zero(self.safe_string(order, 'priceAvg')) + if price is None: + price = priceAvg + type = self.safe_string(order, 'orderType') + side = self.parse_order_side(self.safe_string_lower(order, 'side')) + amount = self.safe_string_2(order, 'quantity', 'size') + isTrigger = self.safe_string(order, 'triggerType') is not None + isMarketBuy = (type == 'market') and (side == 'buy') + if (market['spot']) and (isMarketBuy) and (not isTrigger): + amount = None # cost instead of amount is returned for market buy spot non-trigger orders + status = self.safe_string_2(order, 'status', 'state') + feeDetailString = self.safe_string(order, 'feeDetail') + fees = None + feeCurrency: Str = None + feeCost: Str = None + if feeDetailString is not None: + fees = self.parse_fee_detail_string(feeDetailString) + else: + feeCurrency = self.safe_currency_code(marginCoin) if marginCoin else None + feeCost = Precise.string_abs(self.safe_string(order, 'fee')) + timeInForce = self.parse_order_time_in_force(self.safe_string_lower(order, 'timeInForce')) + postOnly: Bool = None + if timeInForce is not None: + postOnly = timeInForce == 'PO' + triggerPrice = self.omit_zero(self.safe_string(order, 'triggerPrice')) + takeProfitPrice = self.omit_zero(self.safe_string(order, 'presetTakeProfitPrice')) + stopLossPrice = self.omit_zero(self.safe_string_2(order, 'presetTakeProfitPrice', 'presetTakeLossPrice')) + planType = self.safe_string(order, 'planType') + if planType == 'loss_plan': + stopLossPrice = triggerPrice + elif planType == 'profit_plan': + takeProfitPrice = triggerPrice + return self.safe_order({ + 'id': self.safe_string(order, 'orderId'), + 'clientOrderId': self.safe_string_2(order, 'clientOrderId', 'clientOid'), + 'datetime': self.iso8601(timestamp), + 'timestamp': timestamp, + 'lastTradeTimestamp': None, + 'lastUpdateTimestamp': self.safe_integer(order, 'uTime'), + 'status': self.parse_order_status(status), + 'symbol': market['symbol'], + 'type': type, + 'timeInForce': timeInForce, + 'side': side, + 'price': price, + 'average': priceAvg if priceAvg else self.safe_string(order, 'fillPrice'), + 'amount': amount, + 'filled': self.safe_string_2(order, 'fillQuantity', 'filledQty'), + 'remaining': None, + 'triggerPrice': triggerPrice, + 'takeProfitPrice': takeProfitPrice, + 'stopLossPrice': stopLossPrice, + 'cost': self.safe_string_2(order, 'fillTotalAmount', 'filledAmount'), + 'trades': None, + 'fee': { + 'currency': feeCurrency, + 'cost': feeCost, + }, + 'fees': fees, + 'reduceOnly': self.safe_bool(order, 'reduceOnly'), + 'postOnly': postOnly, + 'info': order, + }, market) + + def parse_order_status(self, status: Str) -> Str: + satuses = { + 'not_trigger': 'open', + 'init': 'open', + 'new': 'open', + 'partially_filled': 'open', + 'full_fill': 'closed', + 'filled': 'closed', + 'cancel': 'canceled', + 'canceled': 'canceled', + 'cancelled': 'canceled', + } + return self.safe_string(satuses, status, status) + + def parse_order_side(self, side: Str) -> Str: + sides = { + 'buy': 'buy', + 'sell': 'sell', + 'open_long': 'buy', + 'open_short': 'sell', + 'close_long': 'sell', + 'close_short': 'buy', + 'reduce_close_long': 'sell', + 'reduce_close_short': 'buy', + 'offset_close_long': 'sell', + 'offset_close_short': 'buy', + 'burst_close_long': 'sell', + 'burst_close_short': 'buy', + 'delivery_close_long': 'sell', + 'delivery_close_short': 'buy', + 'buy_single': 'buy', + 'sell_single': 'sell', + } + return self.safe_string(sides, side, side) + + def parse_order_time_in_force(self, timeInForce: Str) -> Str: + timeInForces = { + 'normal': 'GTC', + 'post_only': 'PO', + 'iok': 'IOC', + 'fok': 'FOK', + } + return self.safe_string(timeInForces, timeInForce, timeInForce) + + def parse_fee_detail_string(self, feeDetailString: Str): + result = [] + feeDetail = self.parse_json(feeDetailString) + if feeDetail: + keys = list(feeDetail.keys()) + for i in range(0, len(keys)): + currencyId = self.safe_string(keys, i) + if currencyId in self.currencies_by_id: + currency = self.safe_currency_code(currencyId) + feeEntry = self.safe_dict(feeDetail, currencyId, {}) + amount = Precise.string_abs(self.safe_string(feeEntry, 'totalFee')) + result.append({ + 'currency': currency, + 'amount': amount, + }) + return result + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + fetch all trades made by the user + + https://coincatch.github.io/github.io/en/spot/#get-transaction-details + https://coincatch.github.io/github.io/en/mix/#get-order-fill-detail + https://coincatch.github.io/github.io/en/mix/#get-producttype-order-fill-detail + + :param str symbol: *is mandatory* unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: *swap markets only* the latest time in ms to fetch trades for, only supports the last 30 days timeframe + :param str [params.lastEndId]: *swap markets only* query the data after self tradeId + :returns Trade[]: a list of `trade structures ` + """ + methodName = 'fetchMyTrades' + methodName, params = self.handle_param_string(params, 'methodName', methodName) + self.load_markets() + market: Market = None + marketType = 'spot' + request: dict = {} + if symbol is not None: + market = self.market(symbol) + marketType = market['type'] + request['symbol'] = market['id'] + else: + marketType, params = self.handle_market_type_and_params(methodName, market, params, marketType) + if marketType == 'spot': + raise ArgumentsRequired(self.id + ' ' + methodName + '() requires a symbol argument for spot markets') + response = None + requestLimit = limit + if marketType == 'spot': + maxSpotLimit = 500 + if since is not None: + requestLimit = maxSpotLimit + if requestLimit is not None: + request['limit'] = requestLimit + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1725968747299, + # "data": [ + # { + # "accountId": "1002820815393", + # "symbol": "ETHUSDT_SPBL", + # "orderId": "1217143186968068096", + # "fillId": "1217143193356505089", + # "orderType": "market", + # "side": "buy", + # "fillPrice": "2340.55", + # "fillQuantity": "0.0042", + # "fillTotalAmount": "9.83031", + # "feeCcy": "ETH", + # "fees": "-0.0000042", + # "takerMakerFlag": "taker", + # "cTime": "1725915471400" + # }, + # ... + # ] + # } + # + response = self.privatePostApiSpotV1TradeFills(self.extend(request, params)) + elif marketType == 'swap': + if since is not None: + params['startTime'] = since + else: + params['startTime'] = 0 # mandatory + until: Int = None + until, params = self.handle_option_and_params(params, methodName, 'until') + if until is not None: + request['endTime'] = until + else: + request['endTime'] = self.milliseconds() # mandatory + if symbol is not None: + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1728306590704, + # "data": [ + # { + # "tradeId": "1221355735285014530", + # "symbol": "ETHUSDT_UMCBL", + # "orderId": "1221355728716259329", + # "price": "2555.12", + # "sizeQty": "0.01", + # "fee": "-0.01533072", + # "side": "open_long", + # "fillAmount": "25.5512", + # "profit": "0", + # "enterPointSource": "API", + # "tradeSide": "open_long", + # "holdMode": "double_hold", + # "takerMakerFlag": "taker", + # "cTime": "1726919819661" + # } + # ] + # } + # + response = self.privateGetApiMixV1OrderFills(self.extend(request, params)) + else: + productType = 'umcbl' + productType = self.handle_option(methodName, 'productType', productType) + request['productType'] = productType + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1728306372044, + # "data": [ + # { + # "tradeId": "1225467075440189441", + # "symbol": "ETHUSD_DMCBL", + # "orderId": "1225467075288719360", + # "price": "2362.03", + # "sizeQty": "0.1", + # "fee": "-0.00005996", + # "side": "burst_close_long", + # "fillAmount": "236.203", + # "profit": "-0.0083359", + # "enterPointSource": "SYS", + # "tradeSide": "burst_close_long", + # "holdMode": "double_hold", + # "takerMakerFlag": "taker", + # "cTime": "1727900039539" + # }, + # ... + # ] + # } + # + response = self.privateGetApiMixV1OrderAllFills(self.extend(request, params)) + else: + raise NotSupported(self.id + ' ' + methodName + '() is not supported for ' + marketType + ' type of markets') + data = self.safe_list(response, 'data', []) + return self.parse_trades(data, market, since, limit) + + def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + fetch all the trades made from a single order + + https://coincatch.github.io/github.io/en/spot/#get-transaction-details + + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + methodName = 'fetchOrderTrades' + if symbol is None: + raise ArgumentsRequired(self.id + ' ' + methodName + '() requires a symbol argument') + request: dict = { + 'orderId': id, + 'methodName': methodName, + } + return self.fetch_my_trades(symbol, since, limit, self.extend(request, params)) + + def fetch_margin_mode(self, symbol: str, params={}) -> MarginMode: + """ + fetches the margin mode of the trading pair + + https://coincatch.github.io/github.io/en/mix/#get-single-account + + :param str symbol: unified symbol of the market to fetch the margin mode for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin mode structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'marginCoin': market['settleId'], + } + response = self.privateGetApiMixV1AccountAccount(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1726669633799, + # "data": { + # "marginCoin": "ETH", + # "locked": "0", + # "available": "0.01", + # "crossMaxAvailable": "0.01", + # "fixedMaxAvailable": "0.01", + # "maxTransferOut": "0.01", + # "equity": "0.01", + # "usdtEquity": "22.97657025", + # "btcEquity": "0.000386195288", + # "crossRiskRate": "0", + # "crossMarginLeverage": 100, + # "fixedLongLeverage": 100, + # "fixedShortLeverage": 100, + # "marginMode": "crossed", + # "holdMode": "double_hold", + # "unrealizedPL": "0", + # "bonus": "0", + # "crossedUnrealizedPL": "0", + # "isolatedUnrealizedPL": "" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_margin_mode(data, market) + + def parse_margin_mode(self, marginMode: dict, market=None) -> MarginMode: + marginType = self.safe_string_lower(marginMode, 'marginMode') + return { + 'info': marginMode, + 'symbol': self.safe_symbol(None, market), + 'marginMode': self.parse_margin_mode_type(marginType), + } + + def parse_margin_mode_type(self, type: str) -> str: + types: dict = { + 'crossed': 'cross', + 'fixed': 'isolated', + } + return self.safe_string(types, type, type) + + def set_margin_mode(self, marginMode: str, symbol: Str = None, params={}): + """ + set margin mode to 'cross' or 'isolated' + + https://coincatch.github.io/github.io/en/mix/#change-margin-mode + + :param str marginMode: 'cross' or 'isolated' + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setMarginMode() requires a symbol argument') + marginMode = marginMode.lower() + self.load_markets() + market = self.market(symbol) + if market['type'] != 'swap': + raise NotSupported(self.id + ' setMarginMode() is not supported for ' + market['type'] + ' type of markets') + request: dict = { + 'symbol': market['id'], + 'marginCoin': market['settleId'], + 'marginMode': self.encode_margin_mode_type(marginMode), + } + response = self.privatePostApiMixV1AccountSetMarginMode(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1726670096099, + # "data": { + # "symbol": "ETHUSD_DMCBL", + # "marginCoin": "ETH", + # "longLeverage": 10, + # "shortLeverage": 10, + # "crossMarginLeverage": null, + # "marginMode": "fixed" + # } + # } + # + return response + + def encode_margin_mode_type(self, type: str) -> str: + types: dict = { + 'cross': 'crossed', + 'isolated': 'fixed', + } + return self.safe_string(types, type, type) + + def fetch_position_mode(self, symbol: Str = None, params={}): + """ + fetchs the position mode, hedged or one way + + https://coincatch.github.io/github.io/en/mix/#get-single-account + + :param str symbol: unified symbol of the market to fetch entry for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an object detailing whether the market is in hedged or one-way mode + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchPositionMode() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + if market['type'] != 'swap': + raise NotSupported(self.id + ' fetchPositionMode() is not supported for ' + market['type'] + ' type of markets') + request: dict = { + 'symbol': market['id'], + 'marginCoin': market['settleId'], + } + response = self.privateGetApiMixV1AccountAccount(self.extend(request, params)) # same endpoint + data = self.safe_dict(response, 'data', {}) + holdMode = self.safe_string(data, 'holdMode') + return { + 'info': response, + 'hedged': holdMode == 'double_hold', + } + + def set_position_mode(self, hedged: bool, symbol: Str = None, params={}): + """ + set hedged to True or False for a market + + https://bingx-api.github.io/docs/#/en-us/swapV2/trade-api.html#Set%20Position%20Mode + + :param bool hedged: set to True to use dualSidePosition + :param str symbol: unified symbol of the market to fetch entry for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.productType]: 'umcbl' or 'dmcbl'(default 'umcbl' if symbol is not provided) + :returns dict: response from the exchange + """ + methodName = 'setPositionMode' + defaultProductType = 'umcbl' + self.load_markets() + productType = self.safe_string(params, 'productType') + if productType is None: + if symbol is not None: + market = self.market(symbol) + if market['type'] != 'swap': + raise NotSupported(self.id + ' setPositionMode() is not supported for ' + market['type'] + ' type of markets') + marketId = market['id'] + parts = marketId.split('_') + productType = self.safe_string_lower(parts, 1, productType) + else: + productType = self.handle_option(methodName, 'productType', defaultProductType) + request: dict = { + 'productType': productType, + 'holdMode': 'double_hold' if hedged else 'single_hold', + } + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1726677135005, + # "data": { + # "marginCoin": "ETH", + # "dualSidePosition": False + # } + # } + # + return self.privatePostApiMixV1AccountSetPositionMode(self.extend(request, params)) + + def fetch_leverage(self, symbol: str, params={}) -> Leverage: + """ + fetch the set leverage for a market + + https://coincatch.github.io/github.io/en/mix/#get-single-account + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `leverage structure ` + """ + self.load_markets() + market = self.market(symbol) + if market['type'] != 'swap': + raise NotSupported(self.id + ' fetchLeverage() is not supported for ' + market['type'] + ' type of markets') + request: dict = { + 'symbol': market['id'], + 'marginCoin': market['settleId'], + } + response = self.privateGetApiMixV1AccountAccount(self.extend(request, params)) # same endpoint + data = self.safe_dict(response, 'data', {}) + return self.parse_leverage(data, market) + + def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://hashkeyglobal-apidoc.readme.io/reference/change-futures-leverage-trade + + :param float leverage: the rate of leverage + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.side]: *for isolated margin mode with hedged position mode only* 'long' or 'short' + :returns dict: response from the exchange + """ + methodName = 'setLeverage' + if symbol is None: + raise ArgumentsRequired(self.id + ' ' + methodName + '() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + if market['type'] != 'swap': + raise NotSupported(self.id + ' ' + methodName + '() is not supported for ' + market['type'] + ' type of markets') + request: dict = { + 'symbol': market['id'], + 'marginCoin': market['settleId'], + 'leverage': leverage, + } + side: Str = None + side, params = self.handle_option_and_params(params, methodName, 'side') + if side is not None: + request['holdSide'] = side + response = self.privatePostApiMixV1AccountSetLeverage(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1726680486657, + # "data": { + # "symbol": "ETHUSD_DMCBL", + # "marginCoin": "ETH", + # "longLeverage": 2, + # "shortLeverage": 2, + # "crossMarginLeverage": 2, + # "marginMode": "crossed" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_leverage(data, market) + + def parse_leverage(self, leverage: dict, market: Market = None) -> Leverage: + # + # fetchLeverage + # { + # "marginCoin": "ETH", + # "locked": "0", + # "available": "0.01", + # "crossMaxAvailable": "0.01", + # "fixedMaxAvailable": "0.01", + # "maxTransferOut": "0.01", + # "equity": "0.01", + # "usdtEquity": "22.97657025", + # "btcEquity": "0.000386195288", + # "crossRiskRate": "0", + # "crossMarginLeverage": 100, + # "fixedLongLeverage": 100, + # "fixedShortLeverage": 100, + # "marginMode": "crossed", + # "holdMode": "double_hold", + # "unrealizedPL": "0", + # "bonus": "0", + # "crossedUnrealizedPL": "0", + # "isolatedUnrealizedPL": "" + # } + # + # setLeverage + # { + # "symbol": "ETHUSD_DMCBL", + # "marginCoin": "ETH", + # "longLeverage": 2, + # "shortLeverage": 2, + # "crossMarginLeverage": 2, + # "marginMode": "crossed" + # } + # + marketId = self.safe_string(leverage, 'symbol') + market = self.safe_market_custom(marketId, market) + marginMode = self.parse_margin_mode_type(self.safe_string_lower(leverage, 'marginMode')) + longLeverage = self.safe_integer_2(leverage, 'fixedLongLeverage', 'longLeverage') + shortLeverage = self.safe_integer_2(leverage, 'fixedShortLeverage', 'shortLeverage') + crossMarginLeverage = self.safe_integer(leverage, 'crossMarginLeverage') + if marginMode == 'cross': + longLeverage = crossMarginLeverage + shortLeverage = crossMarginLeverage + return { + 'info': leverage, + 'symbol': market['symbol'], + 'marginMode': marginMode, + 'longLeverage': longLeverage, + 'shortLeverage': shortLeverage, + } + + def modify_margin_helper(self, symbol: str, amount, type, params={}) -> MarginModification: + methodName = 'modifyMarginHelper' + methodName, params = self.handle_param_string(params, 'methodName', methodName) + self.load_markets() + market = self.market(symbol) + if market['type'] != 'swap': + raise NotSupported(self.id + ' ' + methodName + '() is not supported for ' + market['type'] + ' type of markets') + amount = self.amount_to_precision(symbol, amount) + request: dict = { + 'symbol': market['id'], + 'marginCoin': market['settleId'], + 'amount': amount, # positive value for adding margin, negative for reducing + } + side: Str = None + side, params = self.handle_option_and_params(params, methodName, 'side') + if side is not None: + request['holdSide'] = side + response = self.privatePostApiMixV1AccountSetMargin(self.extend(request, params)) + # todo check response + # always returns error + # addMargin - "code":"45006","msg":"Insufficient position","requestTime":1729162281543,"data":null + # reduceMargin - "code":"40800","msg":"Insufficient amount of margin","requestTime":1729162362718,"data":null + if type == 'reduce': + amount = Precise.string_abs(amount) + return self.extend(self.parse_margin_modification(response, market), { + 'amount': self.parse_number(amount), + 'type': type, + }) + + def parse_margin_modification(self, data: dict, market: Market = None) -> MarginModification: + # + # + msg = self.safe_string(data, 'msg') + status = 'ok' if (msg == 'success') else 'failed' + return { + 'info': data, + 'symbol': market['symbol'], + 'type': None, + 'marginMode': None, + 'amount': None, + 'total': None, + 'code': market['quote'], + 'status': status, + 'timestamp': None, + 'datetime': None, + } + + def reduce_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + remove margin from a position + + https://coincatch.github.io/github.io/en/mix/#change-margin + + :param str symbol: unified market symbol + :param float amount: the amount of margin to remove + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.side]: *for isolated margin mode with hedged position mode only* 'long' or 'short' + :returns dict: a `margin structure ` + """ + params['methodName'] = 'reduceMargin' + return self.modify_margin_helper(symbol, -amount, 'reduce', params) + + def add_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + add margin + + https://coincatch.github.io/github.io/en/mix/#change-margin + + :param str symbol: unified market symbol + :param float amount: amount of margin to add + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.side]: *for isolated margin mode with hedged position mode only* 'long' or 'short' + :returns dict: a `margin structure ` + """ + params['methodName'] = 'addMargin' + return self.modify_margin_helper(symbol, amount, 'add', params) + + def fetch_position(self, symbol: str, params={}) -> Position: + """ + fetch data on a single open contract trade position + + https://coincatch.github.io/github.io/en/mix/#get-symbol-position + + :param str symbol: unified market symbol of the market the position is held in, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + @param {str} [params.side] 'long' or 'short' *for non-hedged position mode only* (default 'long') + :returns dict: a `position structure ` + """ + methodName = 'fetchPosition' + side = 'long' + side, params = self.handle_option_and_params(params, methodName, 'side') + positions = self.fetch_positions_for_symbol(symbol, params) + arrayLength = len(positions) + if arrayLength > 1: + for i in range(0, len(positions)): + position = positions[i] + if position['side'] == side: + return position + return self.safe_dict(positions, 0, {}) + + def fetch_positions_for_symbol(self, symbol: str, params={}) -> List[Position]: + """ + fetch open positions for a single market + + https://coincatch.github.io/github.io/en/mix/#get-symbol-position + + fetch all open positions for specific symbol + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'marginCoin': market['settleId'], + } + response = self.privateGetApiMixV1PositionSinglePositionV2(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1726926959041, + # "data": [ + # { + # "marginCoin": "USDT", + # "symbol": "ETHUSDT_UMCBL", + # "holdSide": "long", + # "openDelegateCount": "0", + # "margin": "2.55512", + # "available": "0.01", + # "locked": "0", + # "total": "0.01", + # "leverage": 10, + # "achievedProfits": "0", + # "averageOpenPrice": "2555.12", + # "marginMode": "crossed", + # "holdMode": "double_hold", + # "unrealizedPL": "0.1371", + # "liquidationPrice": "-3433.328491", + # "keepMarginRate": "0.0033", + # "marketPrice": "2568.83", + # "marginRatio": "0.001666357648", + # "autoMargin": "off", + # "cTime": "1726919819686" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_positions(data, [symbol]) + + def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://coincatch.github.io/github.io/en/mix/#get-all-position + + :param str[] [symbols]: list of unified market symbols(all symbols must belong to the same product type) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.productType]: 'umcbl' or 'dmcbl'(default 'umcbl' if symbols are not provided) + :param str [params.marginCoin]: the settle currency of the positions, needs to match the productType + :returns dict[]: a list of `position structure ` + """ + methodName = 'fetchPositions' + self.load_markets() + productType = 'umcbl' + if symbols is not None: + marketIds = self.market_ids(symbols) + productTypes = [] + for i in range(0, len(marketIds)): + marketId = marketIds[i] + parts = marketId.split('_') + marketProductType = self.safe_string(parts, 1) + productTypes.append(marketProductType) + productTypes = self.unique(productTypes) + arrayLength = len(productTypes) + if arrayLength > 1: + raise BadSymbol(self.id + ' ' + methodName + '() requires all symbols to belong to the same product type(umcbl or dmcbl)') + else: + productType = productTypes[0] + else: + productType, params = self.handle_option_and_params(params, methodName, 'productType', productType) + request: dict = { + 'productType': productType, + } + if productType == 'dmcbl': + marginCoin: Str = None + marginCoin, params = self.handle_option_and_params(params, methodName, 'marginCoin') + if marginCoin is not None: + currency = self.currency(marginCoin) + request['marginCoin'] = currency['id'] + response = self.privateGetApiMixV1PositionAllPositionV2(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1726933132054, + # "data": [ + # { + # "marginCoin": "USDT", + # "symbol": "ETHUSDT_UMCBL", + # "holdSide": "long", + # "openDelegateCount": "0", + # "margin": "2.55512", + # "available": "0.01", + # "locked": "0", + # "total": "0.01", + # "leverage": 10, + # "achievedProfits": "0", + # "averageOpenPrice": "2555.12", + # "marginMode": "crossed", + # "holdMode": "double_hold", + # "unrealizedPL": "0.0093", + # "liquidationPrice": "-3433.378333", + # "keepMarginRate": "0.0033", + # "marketPrice": "2556.05", + # "marginRatio": "0.001661599511", + # "autoMargin": "off", + # "cTime": "1726919819686", + # "uTime": "1726919819686" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_positions(data, symbols) + + def parse_position(self, position: dict, market: Market = None): + # + # { + # "marginCoin": "USDT", + # "symbol": "ETHUSDT_UMCBL", + # "holdSide": "long", + # "openDelegateCount": "0", + # "margin": "2.55512", + # "available": "0.01", + # "locked": "0", + # "total": "0.01", + # "leverage": 10, + # "achievedProfits": "0", + # "averageOpenPrice": "2555.12", + # "marginMode": "crossed", + # "holdMode": "double_hold", + # "unrealizedPL": "0.0093", + # "liquidationPrice": "-3433.378333", + # "keepMarginRate": "0.0033", + # "marketPrice": "2556.05", + # "marginRatio": "0.001661599511", + # "autoMargin": "off", + # "cTime": "1726919819686", + # "uTime": "1726919819686" + # } + # + marketId = self.safe_string(position, 'symbol') + settleId = self.safe_string(position, 'marginCoin') + market = self.safe_market_custom(marketId, market, settleId) + timestamp = self.safe_integer(position, 'cTime') + marginMode = self.safe_string(position, 'marginMode') + isHedged: Bool = None + holdMode = self.safe_string(position, 'holdMode') + if holdMode == 'double_hold': + isHedged = True + elif holdMode == 'single_hold': + isHedged = False + margin = self.safe_number(position, 'margin') + keepMarginRate = self.safe_string(position, 'keepMarginRate') + return self.safe_position({ + 'symbol': market['symbol'], + 'id': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'contracts': self.safe_number(position, 'total'), # todo check + 'contractSize': None, + 'side': self.safe_string_lower(position, 'holdSide'), + 'notional': margin, # todo check + 'leverage': self.safe_integer(position, 'leverage'), + 'unrealizedPnl': self.safe_number(position, 'unrealizedPL'), + 'realizedPnl': self.safe_number(position, 'achievedProfits'), + 'collateral': None, # todo check + 'entryPrice': self.safe_number(position, 'averageOpenPrice'), + 'markPrice': self.safe_number(position, 'marketPrice'), + 'liquidationPrice': self.safe_number(position, 'liquidationPrice'), + 'marginMode': self.parse_margin_mode_type(marginMode), + 'hedged': isHedged, + 'maintenanceMargin': None, # todo check + 'maintenanceMarginPercentage': self.parse_number(Precise.string_mul(keepMarginRate, '100')), # todo check + 'initialMargin': margin, # todo check + 'initialMarginPercentage': None, + 'marginRatio': self.safe_number(position, 'marginRatio'), + 'lastUpdateTimestamp': self.safe_integer(position, 'uTime'), + 'lastPrice': None, + 'stopLossPrice': None, + 'takeProfitPrice': None, + 'percentage': None, + 'info': position, + }) + + def safe_market_custom(self, marketId: Str, market: Market = None, settleId: Str = None) -> Market: + try: + market = self.safe_market(marketId, market) + except Exception as e: + # dmcbl markets have the same id and market type but different settleId + # so we need to resolve the market by settleId + marketsWithCurrentId = self.safe_list(self.markets_by_id, marketId, []) + if settleId is None: + market = marketsWithCurrentId[0] # if settleId is not provided, return the first market with the current id + else: + for i in range(0, len(marketsWithCurrentId)): + marketWithCurrentId = marketsWithCurrentId[i] + if marketWithCurrentId['settleId'] == settleId: + market = marketWithCurrentId + break + return market + + def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch the history of changes, actions done by the user or operations that altered balance of the user + + https://coincatch.github.io/github.io/en/spot/#get-bills + https://coincatch.github.io/github.io/en/mix/#get-business-account-bill + + :param str [code]: unified currency code + :param int [since]: timestamp in ms of the earliest ledger entry, default is None + :param int [limit]: max number of ledger entrys to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: *swap only* the latest time in ms to fetch entries for + :param str [params.type]: 'spot' or 'swap'(default 'spot') + :param str [params.after]: *spot only* billId, return the data less than self billId + :param str [params.before]: *spot only* billId, return the data greater than or equals to self billId + :param str [params.groupType]: *spot only* + :param str [params.bizType]: *spot only* + :param str [params.productType]: *swap only* 'umcbl' or 'dmcbl'(default 'umcbl' or 'dmcbl' if code is provided and code is not equal to 'USDT') + :param str [params.business]: *swap only* + :param str [params.lastEndId]: *swap only* + :param bool [params.next]: *swap only* + :returns dict: a `ledger structure ` + """ + methodName = 'fetchLedger' + self.load_markets() + request: dict = {} + marketType = 'spot' + marketType, params = self.handle_market_type_and_params(methodName, None, params, marketType) + result = None + currency = None + if code is not None: + currency = self.currency(code) + if marketType == 'spot': + if currency is not None: + numericId = self.safe_string(currency, 'numericId') + request['coinId'] = numericId + if limit is not None: + request['limit'] = limit + response = self.privatePostApiSpotV1AccountBills(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1727964749515, + # "data": [ + # { + # "billId": "1220289012519190529", + # "coinId": 2, + # "coinName": "USDT", + # "groupType": "transfer", + # "bizType": "Transfer out", + # "quantity": "-40.00000000", + # "balance": "4.43878673", + # "fees": "0.00000000", + # "cTime": "1726665493092" + # }, + # ... + # ] + # } + # + result = self.safe_list(response, 'data', []) + elif marketType == 'swap': + if since is not None: + request['startTime'] = since + else: + request['startTime'] = 0 # is mandatory + until: Int = None + until, params = self.handle_option_and_params(params, methodName, 'until') + if until is not None: + request['endTime'] = until + else: + request['endTime'] = self.milliseconds() # is mandatory + if limit is not None: + request['pageSize'] = limit + productType = 'umcbl' + if code is None: + productType = self.handle_option(methodName, 'productType', productType) + elif code == 'USDT': + productType = 'umcbl' + else: + productType = 'dmcbl' + productType, params = self.handle_param_string(params, 'productType', productType) + request['productType'] = productType + response = self.privateGetApiMixV1AccountAccountBusinessBill(self.extend(request, params)) + # + # { + # "code": "00000", + # "msg": "success", + # "requestTime": 1727971607663, + # "data": { + # "result": [ + # { + # "id": "1225766556446064640", + # "symbol": null, + # "marginCoin": "ETH", + # "amount": "-0.0016", + # "fee": "0", + # "feeByCoupon": "", + # "feeCoin": "ETH", + # "business": "trans_to_exchange", + # "cTime": "1727971441425" + # }, + # { + # "id": "1225467081664061441", + # "symbol": "ETHUSD_DMCBL", + # "marginCoin": "ETH", + # "amount": "-0.00052885", + # "fee": "0", + # "feeByCoupon": "", + # "feeCoin": "ETH", + # "business": "risk_captital_user_transfer", + # "cTime": "1727900041024" + # }, + # { + # "id": "1225467075440189441", + # "symbol": "ETHUSD_DMCBL", + # "marginCoin": "ETH", + # "amount": "-0.0083359", + # "fee": "-0.00005996", + # "feeByCoupon": "", + # "feeCoin": "ETH", + # "business": "burst_long_loss_query", + # "cTime": "1727900039576" + # }, + # { + # "id": "1221416895715303426", + # "symbol": "ETHUSD_DMCBL", + # "marginCoin": "ETH", + # "amount": "0.00004756", + # "fee": "0", + # "feeByCoupon": "", + # "feeCoin": "ETH", + # "business": "contract_settle_fee", + # "cTime": "1726934401444" + # }, + # { + # "id": "1221413703233871873", + # "symbol": "ETHUSD_DMCBL", + # "marginCoin": "ETH", + # "amount": "0", + # "fee": "-0.00005996", + # "feeByCoupon": "", + # "feeCoin": "ETH", + # "business": "open_long", + # "cTime": "1726933640336" + # }, + # { + # "id": "1220288640761122816", + # "symbol": null, + # "marginCoin": "ETH", + # "amount": "0.01", + # "fee": "0", + # "feeByCoupon": "", + # "feeCoin": "ETH", + # "business": "trans_from_exchange", + # "cTime": "1726665404563" + # } + # ], + # "lastEndId": "1220288641021337600", + # "nextFlag": False, + # "preFlag": False + # } + # } + # + data = self.safe_dict(response, 'data', {}) + result = self.safe_list(data, 'result', []) + else: + raise NotSupported(self.id + ' ' + methodName + '() does not support market type ' + marketType) + return self.parse_ledger(result, currency, since, limit) + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + # + # spot + # { + # "billId": "1220289012519190529", + # "coinId": 2, + # "coinName": "USDT", + # "groupType": "transfer", + # "bizType": "Transfer out", + # "quantity": "-40.00000000", + # "balance": "4.43878673", + # "fees": "0.00000000", + # "cTime": "1726665493092" + # } + # + # swap + # { + # "id": "1220288640761122816", + # "symbol": null, + # "marginCoin": "ETH", + # "amount": "0.01", + # "fee": "0", + # "feeByCoupon": "", + # "feeCoin": "ETH", + # "business": "trans_from_exchange", + # "cTime": "1726665404563" + # } + # + timestamp = self.safe_integer(item, 'cTime') + settleId = self.safe_string_2(item, 'coinName', 'marginCoin') + market: Market = None + marketId = self.safe_string(item, 'symbol') + market = self.safe_market_custom(marketId, market, settleId) + amountString = self.safe_string_2(item, 'quantity', 'amount') + direction = 'in' + if Precise.string_lt(amountString, '0'): + direction = 'out' + amountString = Precise.string_mul(amountString, '-1') + fee = { + 'cost': Precise.string_abs(self.safe_string_2(item, 'fee', 'fees')), + 'currency': self.safe_string(item, 'feeCoin'), + } + return self.safe_ledger_entry({ + 'id': self.safe_string_2(item, 'billId', 'id'), + 'info': item, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'account': None, + 'direction': direction, + 'referenceId': None, + 'referenceAccount': None, + 'type': self.parse_ledger_entry_type(self.safe_string_lower_2(item, 'bizType', 'business')), + 'currency': self.safe_currency_code(settleId, currency), + 'symbol': market['symbol'], + 'amount': amountString, + 'before': None, + 'after': self.safe_string(item, 'balance'), + 'status': 'ok', + 'fee': fee, + }, currency) + + def parse_ledger_entry_type(self, type: str) -> str: + types = { + 'deposit': 'deposit', + 'withdraw': 'withdrawal', + 'buy': 'trade', + 'sell': 'trade', + 'deduction of handling fee': 'fee', # todo check + 'transfer-in': 'transfer', + 'transfer in': 'transfer', + 'transfer out': 'transfer', + 'rebate rewards': 'rebate', # todo check + 'airdrop rewards': 'rebate', # todo check + 'usdt contract rewards': 'rebate', # todo check + 'mix contract rewards': 'rebate', # todo check + 'system lock': 'system lock', + 'user lock': 'user lock', + 'open_long': 'trade', + 'open_short': 'trade', + 'close_long': 'trade', + 'close_short': 'trade', + 'trans_from_exchange': 'transfer', + 'trans_to_exchange': 'transfer', + 'contract_settle_fee': 'fee', # todo check sometimes it is positive, sometimes negative + 'burst_long_loss_query': 'trade', # todo check + 'burst_short_loss_query': 'trade', # todo check + } + return self.safe_string(types, type, type) + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if not response: + return None # fallback to default error handler + message = self.safe_string(response, 'msg') + feedback = self.id + ' ' + body + messageCode = self.safe_string(response, 'code') + success = (message == 'success') or (message is None) + if url.find('batch') >= 0: # createOrders, cancelOrders + data = self.safe_dict(response, 'data', {}) + failure = self.safe_list_2(data, 'failure', 'fail_infos', []) + if not self.is_empty(failure): + success = False + firstEntry = self.safe_dict(failure, 0, {}) + messageCode = self.safe_string(firstEntry, 'errorCode') + message = self.safe_string(firstEntry, 'errorMsg') + if not success: + self.throw_exactly_matched_exception(self.exceptions['exact'], messageCode, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + raise ExchangeError(feedback) # unknown message + return None + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + endpoint = '/' + path + if method == 'GET': + query = self.urlencode(params) + if len(query) != 0: + endpoint += '?' + query + if api == 'private': + self.check_required_credentials() + timestamp = self.number_to_string(self.milliseconds()) + suffix = '' + if method != 'GET': + body = self.json(params) + suffix = body + payload = timestamp + method + endpoint + suffix + signature = self.hmac(self.encode(payload), self.encode(self.secret), hashlib.sha256, 'base64') + headers = { + 'ACCESS-KEY': self.apiKey, + 'ACCESS-SIGN': signature, + 'ACCESS-TIMESTAMP': timestamp, + 'ACCESS-PASSPHRASE': self.password, + 'Content-Type': 'application/json', + 'X-CHANNEL-API-CODE': self.safe_string(self.options, 'brokerId', '47cfy'), + } + url = self.urls['api'][api] + endpoint + return {'url': url, 'method': method, 'body': body, 'headers': headers} diff --git a/ccxt/coincheck.py b/ccxt/coincheck.py new file mode 100644 index 0000000..ea5b5a0 --- /dev/null +++ b/ccxt/coincheck.py @@ -0,0 +1,931 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.coincheck import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currency, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Ticker, Trade, TradingFees, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadSymbol +from ccxt.base.decimal_to_precision import TICK_SIZE + + +class coincheck(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(coincheck, self).describe(), { + 'id': 'coincheck', + 'name': 'coincheck', + 'countries': ['JP', 'ID'], + 'rateLimit': 1500, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelOrder': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createOrder': True, + 'createOrderWithTakeProfitAndStopLoss': False, + 'createOrderWithTakeProfitAndStopLossWs': False, + 'createPostOnlyOrder': False, + 'createReduceOnlyOrder': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchDeposits': True, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkOHLCV': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrderBook': True, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': True, + 'fetchVolatilityHistory': False, + 'fetchWithdrawals': True, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'repayMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'ws': True, + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/51840849/87182088-1d6d6380-c2ec-11ea-9c64-8ab9f9b289f5.jpg', + 'api': { + 'rest': 'https://coincheck.com/api', + }, + 'www': 'https://coincheck.com', + 'doc': 'https://coincheck.com/documents/exchange/api', + 'fees': [ + 'https://coincheck.com/exchange/fee', + 'https://coincheck.com/info/fee', + ], + }, + 'api': { + 'public': { + 'get': [ + 'exchange/orders/rate', + 'order_books', + 'rate/{pair}', + 'ticker', + 'trades', + ], + }, + 'private': { + 'get': [ + 'accounts', + 'accounts/balance', + 'accounts/leverage_balance', + 'bank_accounts', + 'deposit_money', + 'exchange/orders/opens', + 'exchange/orders/transactions', + 'exchange/orders/transactions_pagination', + 'exchange/leverage/positions', + 'lending/borrows/matches', + 'send_money', + 'withdraws', + ], + 'post': [ + 'bank_accounts', + 'deposit_money/{id}/fast', + 'exchange/orders', + 'exchange/transfers/to_leverage', + 'exchange/transfers/from_leverage', + 'lending/borrows', + 'lending/borrows/{id}/repay', + 'send_money', + 'withdraws', + ], + 'delete': [ + 'bank_accounts/{id}', + 'exchange/orders/{id}', + 'withdraws/{id}', + ], + }, + }, + 'markets': { + 'BTC/JPY': self.safe_market_structure({'id': 'btc_jpy', 'symbol': 'BTC/JPY', 'base': 'BTC', 'quote': 'JPY', 'baseId': 'btc', 'quoteId': 'jpy', 'type': 'spot', 'spot': True}), # the only real pair + # 'ETH/JPY': {'id': 'eth_jpy', 'symbol': 'ETH/JPY', 'base': 'ETH', 'quote': 'JPY', 'baseId': 'eth', 'quoteId': 'jpy'}, + 'ETC/JPY': self.safe_market_structure({'id': 'etc_jpy', 'symbol': 'ETC/JPY', 'base': 'ETC', 'quote': 'JPY', 'baseId': 'etc', 'quoteId': 'jpy', 'type': 'spot', 'spot': True}), + # 'DAO/JPY': {'id': 'dao_jpy', 'symbol': 'DAO/JPY', 'base': 'DAO', 'quote': 'JPY', 'baseId': 'dao', 'quoteId': 'jpy'}, + # 'LSK/JPY': {'id': 'lsk_jpy', 'symbol': 'LSK/JPY', 'base': 'LSK', 'quote': 'JPY', 'baseId': 'lsk', 'quoteId': 'jpy'}, + 'FCT/JPY': self.safe_market_structure({'id': 'fct_jpy', 'symbol': 'FCT/JPY', 'base': 'FCT', 'quote': 'JPY', 'baseId': 'fct', 'quoteId': 'jpy', 'type': 'spot', 'spot': True}), + 'MONA/JPY': self.safe_market_structure({'id': 'mona_jpy', 'symbol': 'MONA/JPY', 'base': 'MONA', 'quote': 'JPY', 'baseId': 'mona', 'quoteId': 'jpy', 'type': 'spot', 'spot': True}), + # 'XMR/JPY': {'id': 'xmr_jpy', 'symbol': 'XMR/JPY', 'base': 'XMR', 'quote': 'JPY', 'baseId': 'xmr', 'quoteId': 'jpy'}, + # 'REP/JPY': {'id': 'rep_jpy', 'symbol': 'REP/JPY', 'base': 'REP', 'quote': 'JPY', 'baseId': 'rep', 'quoteId': 'jpy'}, + # 'XRP/JPY': {'id': 'xrp_jpy', 'symbol': 'XRP/JPY', 'base': 'XRP', 'quote': 'JPY', 'baseId': 'xrp', 'quoteId': 'jpy'}, + # 'ZEC/JPY': {'id': 'zec_jpy', 'symbol': 'ZEC/JPY', 'base': 'ZEC', 'quote': 'JPY', 'baseId': 'zec', 'quoteId': 'jpy'}, + # 'XEM/JPY': {'id': 'xem_jpy', 'symbol': 'XEM/JPY', 'base': 'XEM', 'quote': 'JPY', 'baseId': 'xem', 'quoteId': 'jpy'}, + # 'LTC/JPY': {'id': 'ltc_jpy', 'symbol': 'LTC/JPY', 'base': 'LTC', 'quote': 'JPY', 'baseId': 'ltc', 'quoteId': 'jpy'}, + # 'DASH/JPY': {'id': 'dash_jpy', 'symbol': 'DASH/JPY', 'base': 'DASH', 'quote': 'JPY', 'baseId': 'dash', 'quoteId': 'jpy'}, + # 'ETH/BTC': {'id': 'eth_btc', 'symbol': 'ETH/BTC', 'base': 'ETH', 'quote': 'BTC', 'baseId': 'eth', 'quoteId': 'btc'}, + 'ETC/BTC': self.safe_market_structure({'id': 'etc_btc', 'symbol': 'ETC/BTC', 'base': 'ETC', 'quote': 'BTC', 'baseId': 'etc', 'quoteId': 'btc', 'type': 'spot', 'spot': True}), + # 'LSK/BTC': {'id': 'lsk_btc', 'symbol': 'LSK/BTC', 'base': 'LSK', 'quote': 'BTC', 'baseId': 'lsk', 'quoteId': 'btc'}, + # 'FCT/BTC': {'id': 'fct_btc', 'symbol': 'FCT/BTC', 'base': 'FCT', 'quote': 'BTC', 'baseId': 'fct', 'quoteId': 'btc'}, + # 'XMR/BTC': {'id': 'xmr_btc', 'symbol': 'XMR/BTC', 'base': 'XMR', 'quote': 'BTC', 'baseId': 'xmr', 'quoteId': 'btc'}, + # 'REP/BTC': {'id': 'rep_btc', 'symbol': 'REP/BTC', 'base': 'REP', 'quote': 'BTC', 'baseId': 'rep', 'quoteId': 'btc'}, + # 'XRP/BTC': {'id': 'xrp_btc', 'symbol': 'XRP/BTC', 'base': 'XRP', 'quote': 'BTC', 'baseId': 'xrp', 'quoteId': 'btc'}, + # 'ZEC/BTC': {'id': 'zec_btc', 'symbol': 'ZEC/BTC', 'base': 'ZEC', 'quote': 'BTC', 'baseId': 'zec', 'quoteId': 'btc'}, + # 'XEM/BTC': {'id': 'xem_btc', 'symbol': 'XEM/BTC', 'base': 'XEM', 'quote': 'BTC', 'baseId': 'xem', 'quoteId': 'btc'}, + # 'LTC/BTC': {'id': 'ltc_btc', 'symbol': 'LTC/BTC', 'base': 'LTC', 'quote': 'BTC', 'baseId': 'ltc', 'quoteId': 'btc'}, + # 'DASH/BTC': {'id': 'dash_btc', 'symbol': 'DASH/BTC', 'base': 'DASH', 'quote': 'BTC', 'baseId': 'dash', 'quoteId': 'btc'}, + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': False, # todo + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': False, + 'FOK': False, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': False, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': None, + 'daysBack': None, + 'untilDays': None, + 'symbolRequired': True, + }, + 'fetchOrder': None, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': None, + 'fetchOHLCV': None, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'fees': { + 'trading': { + 'tierBased': False, + 'percentage': True, + 'maker': self.parse_number('0'), + 'taker': self.parse_number('0'), + }, + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'exact': { + 'disabled API Key': AuthenticationError, # {"success":false,"error":"disabled API Key"}' + 'invalid authentication': AuthenticationError, # {"success":false,"error":"invalid authentication"} + }, + 'broad': {}, + }, + }) + + def parse_balance(self, response) -> Balances: + result: dict = {'info': response} + codes = list(self.currencies.keys()) + for i in range(0, len(codes)): + code = codes[i] + currency = self.currency(code) + currencyId = currency['id'] + if currencyId in response: + account = self.account() + reserved = currencyId + '_reserved' + account['free'] = self.safe_string(response, currencyId) + account['used'] = self.safe_string(response, reserved) + result[code] = account + return self.safe_balance(result) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://coincheck.com/documents/exchange/api#order-transactions-pagination + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + response = self.privateGetAccountsBalance(params) + return self.parse_balance(response) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://coincheck.com/documents/exchange/api#order-opens + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + # Only BTC/JPY is meaningful + market = None + if symbol is not None: + market = self.market(symbol) + response = self.privateGetExchangeOrdersOpens(params) + rawOrders = self.safe_value(response, 'orders', []) + parsedOrders = self.parse_orders(rawOrders, market, since, limit) + result = [] + for i in range(0, len(parsedOrders)): + result.append(self.extend(parsedOrders[i], {'status': 'open'})) + return result + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # fetchOpenOrders + # + # { id: 202835, + # "order_type": "buy", + # "rate": 26890, + # "pair": "btc_jpy", + # "pending_amount": "0.5527", + # "pending_market_buy_amount": null, + # "stop_loss_rate": null, + # "created_at": "2015-01-10T05:55:38.000Z"} + # + # todo: add formats for fetchOrder, fetchClosedOrders here + # + id = self.safe_string(order, 'id') + side = self.safe_string(order, 'order_type') + timestamp = self.parse8601(self.safe_string(order, 'created_at')) + amount = self.safe_string(order, 'pending_amount') + remaining = self.safe_string(order, 'pending_amount') + price = self.safe_string(order, 'rate') + status = None + marketId = self.safe_string(order, 'pair') + symbol = self.safe_symbol(marketId, market, '_') + return self.safe_order({ + 'id': id, + 'clientOrderId': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'amount': amount, + 'remaining': remaining, + 'filled': None, + 'side': side, + 'type': None, + 'timeInForce': None, + 'postOnly': None, + 'status': status, + 'symbol': symbol, + 'price': price, + 'triggerPrice': None, + 'cost': None, + 'fee': None, + 'info': order, + 'average': None, + 'trades': None, + }, market) + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://coincheck.com/documents/exchange/api#order-book + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + response = self.publicGetOrderBooks(self.extend(request, params)) + return self.parse_order_book(response, market['symbol']) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "last":4192632.0, + # "bid":4192496.0, + # "ask":4193749.0, + # "high":4332000.0, + # "low":4101047.0, + # "volume":2313.43191762, + # "timestamp":1643374115 + # } + # + symbol = self.safe_symbol(None, market) + timestamp = self.safe_timestamp(ticker, 'timestamp') + last = self.safe_string(ticker, 'last') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': self.safe_string(ticker, 'bid'), + 'bidVolume': None, + 'ask': self.safe_string(ticker, 'ask'), + 'askVolume': None, + 'vwap': None, + 'open': None, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': self.safe_string(ticker, 'volume'), + 'quoteVolume': None, + 'info': ticker, + }, market) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://coincheck.com/documents/exchange/api#ticker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + if symbol != 'BTC/JPY': + raise BadSymbol(self.id + ' fetchTicker() supports BTC/JPY only') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + ticker = self.publicGetTicker(self.extend(request, params)) + # + # { + # "last":4192632.0, + # "bid":4192496.0, + # "ask":4193749.0, + # "high":4332000.0, + # "low":4101047.0, + # "volume":2313.43191762, + # "timestamp":1643374115 + # } + # + return self.parse_ticker(ticker, market) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(public) + # + # { + # "id": "206849494", + # "amount": "0.01", + # "rate": "5598346.0", + # "pair": "btc_jpy", + # "order_type": "sell", + # "created_at": "2021-12-08T14:10:33.000Z" + # } + # + # fetchMyTrades(private) - example from docs + # + # { + # "id": 38, + # "order_id": 49, + # "created_at": "2015-11-18T07:02:21.000Z", + # "funds": { + # "btc": "0.1", + # "jpy": "-4096.135" + # }, + # "pair": "btc_jpy", + # "rate": "40900.0", + # "fee_currency": "JPY", + # "fee": "6.135", + # "liquidity": "T", + # "side": "buy" + # } + # + timestamp = self.parse8601(self.safe_string(trade, 'created_at')) + id = self.safe_string(trade, 'id') + priceString = self.safe_string(trade, 'rate') + marketId = self.safe_string(trade, 'pair') + market = self.safe_market(marketId, market, '_') + baseId = market['baseId'] + quoteId = market['quoteId'] + symbol = market['symbol'] + takerOrMaker = None + amountString = None + costString = None + side = None + fee = None + orderId = None + if 'liquidity' in trade: + if self.safe_string(trade, 'liquidity') == 'T': + takerOrMaker = 'taker' + elif self.safe_string(trade, 'liquidity') == 'M': + takerOrMaker = 'maker' + funds = self.safe_value(trade, 'funds', {}) + amountString = self.safe_string(funds, baseId) + costString = self.safe_string(funds, quoteId) + fee = { + 'currency': self.safe_string(trade, 'fee_currency'), + 'cost': self.safe_string(trade, 'fee'), + } + side = self.safe_string(trade, 'side') + orderId = self.safe_string(trade, 'order_id') + else: + amountString = self.safe_string(trade, 'amount') + side = self.safe_string(trade, 'order_type') + return self.safe_trade({ + 'id': id, + 'info': trade, + 'datetime': self.iso8601(timestamp), + 'timestamp': timestamp, + 'symbol': symbol, + 'type': None, + 'side': side, + 'order': orderId, + 'takerOrMaker': takerOrMaker, + 'price': priceString, + 'amount': amountString, + 'cost': costString, + 'fee': fee, + }, market) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://coincheck.com/documents/exchange/api#order-transactions-pagination + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = {} + if limit is not None: + request['limit'] = limit + response = self.privateGetExchangeOrdersTransactionsPagination(self.extend(request, params)) + # + # { + # "success": True, + # "data": [ + # { + # "id": 38, + # "order_id": 49, + # "created_at": "2015-11-18T07:02:21.000Z", + # "funds": { + # "btc": "0.1", + # "jpy": "-4096.135" + # }, + # "pair": "btc_jpy", + # "rate": "40900.0", + # "fee_currency": "JPY", + # "fee": "6.135", + # "liquidity": "T", + # "side": "buy" + # }, + # ] + # } + # + transactions = self.safe_list(response, 'data', []) + return self.parse_trades(transactions, market, since, limit) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://coincheck.com/documents/exchange/api#public-trades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + if limit is not None: + request['limit'] = limit + response = self.publicGetTrades(self.extend(request, params)) + # + # { + # "id": "206849494", + # "amount": "0.01", + # "rate": "5598346.0", + # "pair": "btc_jpy", + # "order_type": "sell", + # "created_at": "2021-12-08T14:10:33.000Z" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_trades(data, market, since, limit) + + def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://coincheck.com/documents/exchange/api#account-info + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + self.load_markets() + response = self.privateGetAccounts(params) + # + # { + # "success": True, + # "id": "7487995", + # "email": "some@email.com", + # "identity_status": "identity_pending", + # "bitcoin_address": null, + # "lending_leverage": "4", + # "taker_fee": "0.0", + # "maker_fee": "0.0", + # "exchange_fees": { + # "btc_jpy": {taker_fee: '0.0', maker_fee: "0.0"}, + # "etc_jpy": {taker_fee: '0.0', maker_fee: "0.0"}, + # "fct_jpy": {taker_fee: '0.0', maker_fee: "0.0"}, + # "mona_jpy": {taker_fee: '0.0', maker_fee: "0.0"}, + # "plt_jpy": {taker_fee: '0.0', maker_fee: "0.0"} + # } + # } + # + fees = self.safe_value(response, 'exchange_fees', {}) + result: dict = {} + for i in range(0, len(self.symbols)): + symbol = self.symbols[i] + market = self.market(symbol) + fee = self.safe_value(fees, market['id'], {}) + result[symbol] = { + 'info': fee, + 'symbol': symbol, + 'maker': self.safe_number(fee, 'maker_fee'), + 'taker': self.safe_number(fee, 'taker_fee'), + 'percentage': True, + 'tierBased': False, + } + return result + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://coincheck.com/documents/exchange/api#order-new + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + if type == 'market': + request['order_type'] = type + '_' + side + if side == 'sell': + request['amount'] = amount + else: + cost = self.safe_number(params, 'cost') + params = self.omit(params, 'cost') + if cost is not None: + raise ArgumentsRequired(self.id + ' createOrder() : you should use "cost" parameter instead of "amount" argument to create market buy orders') + request['market_buy_amount'] = cost + else: + request['order_type'] = side + request['rate'] = price + request['amount'] = amount + response = self.privatePostExchangeOrders(self.extend(request, params)) + id = self.safe_string(response, 'id') + return self.safe_order({ + 'id': id, + 'info': response, + }, market) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://coincheck.com/documents/exchange/api#order-cancel + + :param str id: order id + :param str symbol: not used by coincheck cancelOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + request: dict = { + 'id': id, + } + response = self.privateDeleteExchangeOrdersId(self.extend(request, params)) + # + # { + # "success": True, + # "id": 12345 + # } + # + return self.parse_order(response) + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://coincheck.com/documents/exchange/api#account-deposits + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + currency = None + request: dict = {} + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + if limit is not None: + request['limit'] = limit + response = self.privateGetDepositMoney(self.extend(request, params)) + # { + # "success": True, + # "deposits": [ + # { + # "id": 2, + # "amount": "0.05", + # "currency": "BTC", + # "address": "13PhzoK8me3u5nHzzFD85qT9RqEWR9M4Ty", + # "status": "confirmed", + # "confirmed_at": "2015-06-13T08:29:18.000Z", + # "created_at": "2015-06-13T08:22:18.000Z" + # }, + # { + # "id": 1, + # "amount": "0.01", + # "currency": "BTC", + # "address": "13PhzoK8me3u5nHzzFD85qT9RqEWR9M4Ty", + # "status": "received", + # "confirmed_at": "2015-06-13T08:21:18.000Z", + # "created_at": "2015-06-13T08:21:18.000Z" + # } + # ] + # } + data = self.safe_list(response, 'deposits', []) + return self.parse_transactions(data, currency, since, limit, {'type': 'deposit'}) + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://coincheck.com/documents/exchange/api#withdraws + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + currency = None + if code is not None: + currency = self.currency(code) + request: dict = {} + if limit is not None: + request['limit'] = limit + response = self.privateGetWithdraws(self.extend(request, params)) + # { + # "success": True, + # "pagination": { + # "limit": 25, + # "order": "desc", + # "starting_after": null, + # "ending_before": null + # }, + # "data": [ + # { + # "id": 398, + # "status": "finished", + # "amount": "242742.0", + # "currency": "JPY", + # "created_at": "2014-12-04T15:00:00.000Z", + # "bank_account_id": 243, + # "fee": "400.0", + # "is_fast": True + # } + # ] + # } + data = self.safe_list(response, 'data', []) + return self.parse_transactions(data, currency, since, limit, {'type': 'withdrawal'}) + + def parse_transaction_status(self, status: Str): + statuses: dict = { + # withdrawals + 'pending': 'pending', + 'processing': 'pending', + 'finished': 'ok', + 'canceled': 'canceled', + # deposits + 'confirmed': 'pending', + 'received': 'ok', + } + return self.safe_string(statuses, status, status) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fetchDeposits + # + # { + # "id": 2, + # "amount": "0.05", + # "currency": "BTC", + # "address": "13PhzoK8me3u5nHzzFD85qT9RqEWR9M4Ty", + # "status": "confirmed", + # "confirmed_at": "2015-06-13T08:29:18.000Z", + # "created_at": "2015-06-13T08:22:18.000Z" + # } + # + # fetchWithdrawals + # + # { + # "id": 398, + # "status": "finished", + # "amount": "242742.0", + # "currency": "JPY", + # "created_at": "2014-12-04T15:00:00.000Z", + # "bank_account_id": 243, + # "fee": "400.0", + # "is_fast": True + # } + # + id = self.safe_string(transaction, 'id') + timestamp = self.parse8601(self.safe_string(transaction, 'created_at')) + address = self.safe_string(transaction, 'address') + amount = self.safe_number(transaction, 'amount') + currencyId = self.safe_string(transaction, 'currency') + code = self.safe_currency_code(currencyId, currency) + status = self.parse_transaction_status(self.safe_string(transaction, 'status')) + updated = self.parse8601(self.safe_string(transaction, 'confirmed_at')) + fee = None + feeCost = self.safe_number(transaction, 'fee') + if feeCost is not None: + fee = { + 'cost': feeCost, + 'currency': code, + } + return { + 'info': transaction, + 'id': id, + 'txid': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': None, + 'address': address, + 'addressTo': address, + 'addressFrom': None, + 'tag': None, + 'tagTo': None, + 'tagFrom': None, + 'type': None, + 'amount': amount, + 'currency': code, + 'status': status, + 'updated': updated, + 'comment': None, + 'internal': None, + 'fee': fee, + } + + def nonce(self): + return self.milliseconds() + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = self.urls['api']['rest'] + '/' + self.implode_params(path, params) + query = self.omit(params, self.extract_params(path)) + if api == 'public': + if query: + url += '?' + self.urlencode(query) + else: + self.check_required_credentials() + nonce = str(self.nonce()) + queryString = '' + if method == 'GET': + if query: + url += '?' + self.urlencode(self.keysort(query)) + else: + if query: + body = self.urlencode(self.keysort(query)) + queryString = body + auth = nonce + url + queryString + headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + 'ACCESS-KEY': self.apiKey, + 'ACCESS-NONCE': nonce, + 'ACCESS-SIGNATURE': self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256), + } + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None + # + # {"success":false,"error":"disabled API Key"}' + # {"success":false,"error":"invalid authentication"} + # + success = self.safe_bool(response, 'success', True) + if not success: + error = self.safe_string(response, 'error') + feedback = self.id + ' ' + self.json(response) + self.throw_exactly_matched_exception(self.exceptions['exact'], error, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], body, feedback) + raise ExchangeError(self.id + ' ' + self.json(response)) + return None diff --git a/ccxt/coinex.py b/ccxt/coinex.py new file mode 100644 index 0000000..5eda048 --- /dev/null +++ b/ccxt/coinex.py @@ -0,0 +1,5899 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.coinex import ImplicitAPI +from ccxt.base.types import Any, Balances, BorrowInterest, Currencies, Currency, DepositAddress, Int, IsolatedBorrowRate, Leverage, LeverageTier, LeverageTiers, MarginModification, Market, Num, Order, OrderRequest, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, FundingRates, Trade, TradingFeeInterface, TradingFees, Transaction, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import AccountSuspended +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import NotSupported +from ccxt.base.errors import OperationFailed +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.errors import RequestTimeout +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class coinex(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(coinex, self).describe(), { + 'id': 'coinex', + 'name': 'CoinEx', + 'version': 'v2', + 'countries': ['CN'], + # IP ratelimit is 400 requests per second + # rateLimit = 1000ms / 400 = 2.5 + # 200 per 2 seconds => 100 per second => weight = 4 + # 120 per 2 seconds => 60 per second => weight = 6.667 + # 80 per 2 seconds => 40 per second => weight = 10 + # 60 per 2 seconds => 30 per second => weight = 13.334 + # 40 per 2 seconds => 20 per second => weight = 20 + # 20 per 2 seconds => 10 per second => weight = 40 + # v1 is per 2 seconds and v2 is per 1 second + 'rateLimit': 2.5, + 'pro': True, + 'certified': True, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': True, + 'swap': True, + 'future': False, + 'option': False, + 'addMargin': True, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': True, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'cancelOrders': True, + 'closeAllPositions': False, + 'closePosition': True, + 'createDepositAddress': True, + 'createMarketBuyOrderWithCost': True, + 'createMarketOrderWithCost': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createOrders': True, + 'createReduceOnlyOrder': True, + 'createStopLossOrder': True, + 'createStopOrder': True, + 'createTakeProfitOrder': True, + 'createTriggerOrder': True, + 'editOrder': True, + 'fetchBalance': True, + 'fetchBorrowInterest': True, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchDepositWithdrawFee': True, + 'fetchDepositWithdrawFees': True, + 'fetchFundingHistory': True, + 'fetchFundingInterval': True, + 'fetchFundingIntervals': False, + 'fetchFundingRate': True, + 'fetchFundingRateHistory': True, + 'fetchFundingRates': True, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': True, + 'fetchIsolatedBorrowRates': False, + 'fetchLeverage': True, + 'fetchLeverages': False, + 'fetchLeverageTiers': True, + 'fetchMarginAdjustmentHistory': True, + 'fetchMarketLeverageTiers': 'emulated', + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchPosition': True, + 'fetchPositionHistory': True, + 'fetchPositions': True, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': True, + 'fetchTradingFees': True, + 'fetchTransfer': False, + 'fetchTransfers': True, + 'fetchWithdrawal': False, + 'fetchWithdrawals': True, + 'reduceMargin': True, + 'repayCrossMargin': False, + 'repayIsolatedMargin': True, + 'setLeverage': True, + 'setMarginMode': True, + 'setPositionMode': False, + 'transfer': True, + 'withdraw': True, + }, + 'timeframes': { + '1m': '1min', + '3m': '3min', + '5m': '5min', + '15m': '15min', + '30m': '30min', + '1h': '1hour', + '2h': '2hour', + '4h': '4hour', + '6h': '6hour', + '12h': '12hour', + '1d': '1day', + '3d': '3day', + '1w': '1week', + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/51840849/87182089-1e05fa00-c2ec-11ea-8da9-cc73b45abbbc.jpg', + 'api': { + 'public': 'https://api.coinex.com', + 'private': 'https://api.coinex.com', + 'perpetualPublic': 'https://api.coinex.com/perpetual', + 'perpetualPrivate': 'https://api.coinex.com/perpetual', + }, + 'www': 'https://www.coinex.com', + 'doc': 'https://docs.coinex.com/api/v2', + 'fees': 'https://www.coinex.com/fees', + 'referral': 'https://www.coinex.com/register?refer_code=yw5fz', + }, + 'api': { + 'v1': { + 'public': { + 'get': { + 'amm/market': 1, + 'common/currency/rate': 1, + 'common/asset/config': 1, + 'common/maintain/info': 1, + 'common/temp-maintain/info': 1, + 'margin/market': 1, + 'market/info': 1, + 'market/list': 1, + 'market/ticker': 1, + 'market/ticker/all': 1, + 'market/depth': 1, + 'market/deals': 1, + 'market/kline': 1, + 'market/detail': 1, + }, + }, + 'private': { + 'get': { + 'account/amm/balance': 40, + 'account/investment/balance': 40, + 'account/balance/history': 40, + 'account/market/fee': 40, + 'balance/coin/deposit': 40, + 'balance/coin/withdraw': 40, + 'balance/info': 40, + 'balance/deposit/address/{coin_type}': 40, + 'contract/transfer/history': 40, + 'credit/info': 40, + 'credit/balance': 40, + 'investment/transfer/history': 40, + 'margin/account': 1, + 'margin/config': 1, + 'margin/loan/history': 40, + 'margin/transfer/history': 40, + 'order/deals': 40, + 'order/finished': 40, + 'order/pending': 8, + 'order/status': 8, + 'order/status/batch': 8, + 'order/user/deals': 40, + 'order/stop/finished': 40, + 'order/stop/pending': 8, + 'order/user/trade/fee': 1, + 'order/market/trade/info': 1, + 'sub_account/balance': 1, + 'sub_account/transfer/history': 40, + 'sub_account/auth/api': 40, + 'sub_account/auth/api/{user_auth_id}': 40, + }, + 'post': { + 'balance/coin/withdraw': 40, + 'contract/balance/transfer': 40, + 'margin/flat': 40, + 'margin/loan': 40, + 'margin/transfer': 40, + 'order/limit/batch': 40, + 'order/ioc': 13.334, + 'order/limit': 13.334, + 'order/market': 13.334, + 'order/modify': 13.334, + 'order/stop/limit': 13.334, + 'order/stop/market': 13.334, + 'order/stop/modify': 13.334, + 'sub_account/transfer': 40, + 'sub_account/register': 1, + 'sub_account/unfrozen': 40, + 'sub_account/frozen': 40, + 'sub_account/auth/api': 40, + }, + 'put': { + 'balance/deposit/address/{coin_type}': 40, + 'sub_account/unfrozen': 40, + 'sub_account/frozen': 40, + 'sub_account/auth/api/{user_auth_id}': 40, + 'v1/account/settings': 40, + }, + 'delete': { + 'balance/coin/withdraw': 40, + 'order/pending/batch': 40, + 'order/pending': 13.334, + 'order/stop/pending': 40, + 'order/stop/pending/{id}': 13.334, + 'order/pending/by_client_id': 40, + 'order/stop/pending/by_client_id': 40, + 'sub_account/auth/api/{user_auth_id}': 40, + 'sub_account/authorize/{id}': 40, + }, + }, + 'perpetualPublic': { + 'get': { + 'ping': 1, + 'time': 1, + 'market/list': 1, + 'market/limit_config': 1, + 'market/ticker': 1, + 'market/ticker/all': 1, + 'market/depth': 1, + 'market/deals': 1, + 'market/funding_history': 1, + 'market/kline': 1, + }, + }, + 'perpetualPrivate': { + 'get': { + 'market/user_deals': 1, + 'asset/query': 40, + 'order/pending': 8, + 'order/finished': 40, + 'order/stop_finished': 40, + 'order/stop_pending': 8, + 'order/status': 8, + 'order/stop_status': 8, + 'position/finished': 40, + 'position/pending': 40, + 'position/funding': 40, + 'position/adl_history': 40, + 'market/preference': 40, + 'position/margin_history': 40, + 'position/settle_history': 40, + }, + 'post': { + 'market/adjust_leverage': 1, + 'market/position_expect': 1, + 'order/put_limit': 20, + 'order/put_market': 20, + 'order/put_stop_limit': 20, + 'order/put_stop_market': 20, + 'order/modify': 20, + 'order/modify_stop': 20, + 'order/cancel': 20, + 'order/cancel_all': 40, + 'order/cancel_batch': 40, + 'order/cancel_stop': 20, + 'order/cancel_stop_all': 40, + 'order/close_limit': 20, + 'order/close_market': 20, + 'position/adjust_margin': 20, + 'position/stop_loss': 20, + 'position/take_profit': 20, + 'position/market_close': 20, + 'order/cancel/by_client_id': 20, + 'order/cancel_stop/by_client_id': 20, + 'market/preference': 20, + }, + }, + }, + 'v2': { + 'public': { + 'get': { + 'maintain/info': 1, + 'ping': 1, + 'time': 1, + 'spot/market': 1, + 'spot/ticker': 1, + 'spot/depth': 1, + 'spot/deals': 1, + 'spot/kline': 1, + 'spot/index': 1, + 'futures/market': 1, + 'futures/ticker': 1, + 'futures/depth': 1, + 'futures/deals': 1, + 'futures/kline': 1, + 'futures/index': 1, + 'futures/funding-rate': 1, + 'futures/funding-rate-history': 1, + 'futures/position-level': 1, + 'futures/liquidation-history': 1, + 'futures/basis-history': 1, + 'assets/deposit-withdraw-config': 1, + 'assets/all-deposit-withdraw-config': 1, + }, + }, + 'private': { + 'get': { + 'account/subs': 1, + 'account/subs/api-detail': 40, + 'account/subs/info': 1, + 'account/subs/api': 40, + 'account/subs/transfer-history': 40, + 'account/subs/spot-balance': 1, + 'account/trade-fee-rate': 40, + 'assets/spot/balance': 40, + 'assets/futures/balance': 40, + 'assets/margin/balance': 1, + 'assets/financial/balance': 40, + 'assets/amm/liquidity': 40, + 'assets/credit/info': 40, + 'assets/margin/borrow-history': 40, + 'assets/margin/interest-limit': 1, + 'assets/deposit-address': 40, + 'assets/deposit-history': 40, + 'assets/withdraw': 40, + 'assets/transfer-history': 40, + 'spot/order-status': 8, + 'spot/batch-order-status': 8, + 'spot/pending-order': 8, + 'spot/finished-order': 40, + 'spot/pending-stop-order': 8, + 'spot/finished-stop-order': 40, + 'spot/user-deals': 40, + 'spot/order-deals': 40, + 'futures/order-status': 8, + 'futures/batch-order-status': 1, + 'futures/pending-order': 8, + 'futures/finished-order': 40, + 'futures/pending-stop-order': 8, + 'futures/finished-stop-order': 40, + 'futures/user-deals': 1, + 'futures/order-deals': 1, + 'futures/pending-position': 40, + 'futures/finished-position': 1, + 'futures/position-margin-history': 1, + 'futures/position-funding-history': 40, + 'futures/position-adl-history': 1, + 'futures/position-settle-history': 1, + }, + 'post': { + 'account/subs': 40, + 'account/subs/frozen': 40, + 'account/subs/unfrozen': 40, + 'account/subs/api': 40, + 'account/subs/edit-api': 40, + 'account/subs/delete-api': 40, + 'account/subs/transfer': 40, + 'account/settings': 40, + 'assets/margin/borrow': 40, + 'assets/margin/repay': 40, + 'assets/renewal-deposit-address': 40, + 'assets/withdraw': 40, + 'assets/cancel-withdraw': 40, + 'assets/transfer': 40, + 'assets/amm/add-liquidity': 1, + 'assets/amm/remove-liquidity': 1, + 'spot/order': 13.334, + 'spot/stop-order': 13.334, + 'spot/batch-order': 40, + 'spot/batch-stop-order': 1, + 'spot/modify-order': 13.334, + 'spot/modify-stop-order': 13.334, + 'spot/cancel-all-order': 1, + 'spot/cancel-order': 6.667, + 'spot/cancel-stop-order': 6.667, + 'spot/cancel-batch-order': 10, + 'spot/cancel-batch-stop-order': 10, + 'spot/cancel-order-by-client-id': 1, + 'spot/cancel-stop-order-by-client-id': 1, + 'futures/order': 20, + 'futures/stop-order': 20, + 'futures/batch-order': 1, + 'futures/batch-stop-order': 1, + 'futures/modify-order': 20, + 'futures/modify-stop-order': 20, + 'futures/cancel-all-order': 1, + 'futures/cancel-order': 10, + 'futures/cancel-stop-order': 10, + 'futures/cancel-batch-order': 20, + 'futures/cancel-batch-stop-order': 20, + 'futures/cancel-order-by-client-id': 1, + 'futures/cancel-stop-order-by-client-id': 1, + 'futures/close-position': 20, + 'futures/adjust-position-margin': 20, + 'futures/adjust-position-leverage': 20, + 'futures/set-position-stop-loss': 20, + 'futures/set-position-take-profit': 20, + }, + }, + }, + }, + 'fees': { + 'trading': { + 'maker': 0.001, + 'taker': 0.001, + }, + 'funding': { + 'withdraw': { + 'BCH': 0.0, + 'BTC': 0.001, + 'LTC': 0.001, + 'ETH': 0.001, + 'ZEC': 0.0001, + 'DASH': 0.0001, + }, + }, + }, + 'limits': { + 'amount': { + 'min': 0.001, + 'max': None, + }, + }, + 'options': { + 'brokerId': 'x-167673045', + 'createMarketBuyOrderRequiresPrice': True, + 'defaultType': 'spot', # spot, swap, margin + 'defaultSubType': 'linear', # linear, inverse + 'fetchDepositAddress': { + 'fillResponseFromRequest': True, + }, + 'accountsByType': { + 'spot': 'SPOT', + 'margin': 'MARGIN', + 'swap': 'FUTURES', + }, + 'accountsById': { + 'SPOT': 'spot', + 'MARGIN': 'margin', + 'FUTURES': 'swap', + }, + 'networks': { + 'BTC': 'BTC', + 'BEP20': 'BSC', + 'TRC20': 'TRC20', + 'ERC20': 'ERC20', + 'BRC20': 'BRC20', + 'SOL': 'SOL', + 'TON': 'TON', + 'BSV': 'BSV', + 'AVAXC': 'AVA_C', + 'AVAXX': 'AVA', + 'SUI': 'SUI', + 'ACA': 'ACA', + 'CHZ': 'CHILIZ', + 'ADA': 'ADA', + 'ARB': 'ARBITRUM', + 'ARBNOVA': 'ARBITRUM_NOVA', + 'OP': 'OPTIMISM', + 'APT': 'APTOS', + 'ATOM': 'ATOM', + 'FTM': 'FTM', + 'BCH': 'BCH', + 'ASTR': 'ASTR', + 'LTC': 'LTC', + 'MATIC': 'MATIC', + 'CRONOS': 'CRONOS', + 'DASH': 'DASH', + 'DOT': 'DOT', + 'ETC': 'ETC', + 'ETHW': 'ETHPOW', + 'FIL': 'FIL', + 'ZIL': 'ZIL', + 'DOGE': 'DOGE', + 'TIA': 'CELESTIA', + 'SEI': 'SEI', + 'XRP': 'XRP', + 'XMR': 'XMR', + # CSC, AE, BASE, AIPG, AKASH, POLKADOTASSETHUB ?, ALEO, STX, ALGO, ALPH, BLAST, AR, ARCH, ARDR, ARK, ARRR, MANTA, NTRN, LUNA, AURORA, AVAIL, ASC20, AVA, AYA, AZERO, BAN, BAND, BB, RUNES, BEAM, BELLSCOIN, BITCI, NEAR, AGORIC, BLOCX, BNC, BOBA, BRISE, KRC20, CANTO, CAPS, CCD, CELO, CFX, CHI, CKB, CLORE, CLV, CORE, CSPR, CTXC, DAG, DCR, DERO, DESO, DEFI, DGB, DNX, DOCK, DOGECHAIN, DYDX, DYMENSION, EGLD, ELA, ELF, ENJIN, EOSIO, ERG, ETN_SC, EVMOS, EWC, SGB, FACT, FB, FET, FIO, FIRO, NEO3, FLOW, FLARE, FLUX, LINEA, FREN, FSN, FB_BRC20, GLMR, GRIN, GRS, HACASH, HBAR, HERB, HIVE, MAPO, HMND, HNS, ZKSYNC, HTR, HUAHUA, MERLIN, ICP, ICX, INJ, IOST, IOTA, IOTX, IRIS, IRON, ONE, JOYSTREAM, KAI, KAR, KAS, KAVA, KCN, KDA, KLAY, KLY, KMD, KSM, KUB, KUJIRA, LAT, LBC, LUNC, LUKSO, MARS, METIS, MINA, MANTLE, MOB, MODE, MONA, MOVR, MTL, NEOX, NEXA, NIBI, NIMIQ, NMC, ONOMY, NRG, WAVES, NULS, OAS, OCTA, OLT, ONT, OORT, ORAI, OSMO, P3D, COMPOSABLE, PIVX, RON, POKT, POLYMESH, PRE_MARKET, PYI, QKC, QTUM, QUBIC, RSK, ROSE, ROUTE, RTM, THORCHAIN, RVN, RADIANT, SAGA, SALVIUM, SATOX, SC, SCP, _NULL, SCRT, SDN, RGBPP, SELF, SMH, SPACE, STARGAZE, STC, STEEM, STRATISEVM, STRD, STARKNET, SXP, SYS, TAIKO, TAO, TARA, TENET, THETA, TT, VENOM, VECHAIN, TOMO, VITE, VLX, VSYS, VTC, WAN, WAXP, WEMIX, XCH, XDC, XEC, XELIS, NEM, XHV, XLM, XNA, NANO, XPLA, XPR, XPRT, XRD, XTZ, XVG, XYM, ZANO, ZEC, ZEN, ZEPH, ZETA + }, + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': True, + 'triggerPrice': True, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': True, + 'selfTradePrevention': True, # todo: implement + 'iceberg': True, # todo implement + }, + 'createOrders': { + 'max': 5, + }, + 'fetchMyTrades': { + 'marginMode': True, + 'limit': 1000, + 'daysBack': None, + 'untilDays': 100000, + 'symbolRequired': True, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOpenOrders': { + 'marginMode': True, + 'limit': 1000, + 'trigger': True, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': { + 'marginMode': True, + 'limit': 1000, + 'daysBack': None, + 'daysBackCanceled': None, + 'untilDays': None, + 'trigger': True, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 1000, + }, + }, + 'forDerivatives': { + 'extends': 'spot', + 'createOrder': { + 'marginMode': True, + 'stopLossPrice': True, + 'takeProfitPrice': True, + }, + 'fetchOpenOrders': { + 'marginMode': False, + }, + 'fetchClosedOrders': { + 'marginMode': False, + }, + }, + 'swap': { + 'linear': { + 'extends': 'forDerivatives', + }, + 'inverse': { + 'extends': 'forDerivatives', + }, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'commonCurrencies': { + 'ACM': 'Actinium', + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'exact': { + # https://github.com/coinexcom/coinex_exchange_api/wiki/013error_code + '23': PermissionDenied, # IP Prohibited + '24': AuthenticationError, + '25': AuthenticationError, + '34': AuthenticationError, # Access id is expires + '35': ExchangeNotAvailable, # Service unavailable + '36': RequestTimeout, # Service timeout + '213': RateLimitExceeded, # Too many requests + '107': InsufficientFunds, + '158': PermissionDenied, # {"code":158,"data":{},"message":"API permission is not allowed"} + '600': OrderNotFound, + '601': InvalidOrder, + '602': InvalidOrder, + '606': InvalidOrder, + '3008': RequestTimeout, # Service busy, please try again later. + '3109': InsufficientFunds, # {"code":3109,"data":{},"message":"balance not enough"} + '3127': InvalidOrder, # The order quantity is below the minimum requirement. Please adjust the order quantity. + '3600': OrderNotFound, # {"code":3600,"data":{},"message":"Order not found"} + '3606': InvalidOrder, # The price difference between the order price and the latest price is too large. Please adjust the order amount accordingly. + '3610': ExchangeError, # Order cancellation prohibited during the Call Auction period. + '3612': InvalidOrder, # The est. ask price is lower than the current bottom ask price. Please reduce the amount. + '3613': InvalidOrder, # The est. bid price is higher than the current top bid price. Please reduce the amount. + '3614': InvalidOrder, # The deviation between your est. filled price and the index price. Please reduce the amount. + '3615': InvalidOrder, # The deviation between your order price and the index price is too high. Please adjust your order price and try again. + '3616': InvalidOrder, # The order price exceeds the current top bid price. Please adjust the order price and try again. + '3617': InvalidOrder, # The order price exceeds the current bottom ask price. Please adjust the order price and try again. + '3618': InvalidOrder, # The deviation between your order price and the index price is too high. Please adjust your order price and try again. + '3619': InvalidOrder, # The deviation between your order price and the trigger price is too high. Please adjust your order price and try again. + '3620': InvalidOrder, # Market order submission is temporarily unavailable due to insufficient depth in the current market + '3621': InvalidOrder, # This order can't be completely executed and has been canceled. + '3622': InvalidOrder, # This order can't be set Only and has been canceled. + '3627': InvalidOrder, # The current market depth is low, please reduce your order amount and try again. + '3628': InvalidOrder, # The current market depth is low, please reduce your order amount and try again. + '3629': InvalidOrder, # The current market depth is low, please reduce your order amount and try again. + '3632': InvalidOrder, # The order price exceeds the current top bid price. Please adjust the order price and try again. + '3633': InvalidOrder, # The order price exceeds the current bottom ask price. Please adjust the order price and try again. + '3634': InvalidOrder, # The deviation between your est. filled price and the index price is too high. Please reduce the amount and try again. + '3635': InvalidOrder, # The deviation between your est. filled price and the index price is too high. Please reduce the amount and try again. + '4001': ExchangeNotAvailable, # Service unavailable, please try again later. + '4002': RequestTimeout, # Service request timed out, please try again later. + '4003': ExchangeError, # Internal error, please contact customer service for help. + '4004': BadRequest, # Parameter error, please check whether the request parameters are abnormal. + '4005': AuthenticationError, # Abnormal access_id, please check whether the value passed by X-COINEX-KEY is normal. + '4006': AuthenticationError, # Signature verification failed, please check the signature according to the documentation instructions. + '4007': PermissionDenied, # IP address prohibited, please check whether the whitelist or export IP is normal. + '4008': AuthenticationError, # Abnormal X-COIN-SIGN value, please check. + '4009': ExchangeError, # Abnormal request method, please check. + '4010': ExchangeError, # Expired request, please try again later. + '4011': PermissionDenied, # User prohibited from accessing, please contact customer service for help. + '4017': ExchangeError, # Signature expired, please try again later. + '4115': AccountSuspended, # User prohibited from trading, please contact customer service for help. + '4117': BadSymbol, # Trading hasattr(self, prohibited) market, please try again later. + '4123': RateLimitExceeded, # Rate limit triggered. Please adjust your strategy and reduce the request rate. + '4130': ExchangeError, # Futures trading prohibited, please try again later. + '4158': ExchangeError, # Trading prohibited, please try again later. + '4213': RateLimitExceeded, # The request is too frequent, please try again later. + '4512': PermissionDenied, # Insufficient sub-account permissions, please check. + }, + 'broad': { + 'ip not allow visit': PermissionDenied, + 'service too busy': ExchangeNotAvailable, + 'Service is not available during funding fee settlement': OperationFailed, + }, + }, + }) + + def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://docs.coinex.com/api/v2/assets/deposit-withdrawal/http/list-all-deposit-withdrawal-config + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = self.v2PublicGetAssetsAllDepositWithdrawConfig(params) + # + # { + # "code": 0, + # "data": [ + # { + # "asset": { + # "ccy": "CET", + # "deposit_enabled": True, + # "withdraw_enabled": True, + # "inter_transfer_enabled": True, + # "is_st": False + # }, + # "chains": [ + # { + # "chain": "CSC", + # "min_deposit_amount": "0.8", + # "min_withdraw_amount": "8", + # "deposit_enabled": True, + # "withdraw_enabled": True, + # "deposit_delay_minutes": 0, + # "safe_confirmations": 10, + # "irreversible_confirmations": 20, + # "deflation_rate": "0", + # "withdrawal_fee": "0.026", + # "withdrawal_precision": 8, + # "memo": "", + # "is_memo_required_for_deposit": False, + # "explorer_asset_url": "" + # }, + # ] + # } + # ], + # "message": "OK" + # } + # + data = self.safe_list(response, 'data', []) + result: dict = {} + for i in range(0, len(data)): + coin = data[i] + asset = self.safe_dict(coin, 'asset', {}) + chains = self.safe_list(coin, 'chains', []) + currencyId = self.safe_string(asset, 'ccy') + if currencyId is None: + continue # coinex returns empty structures for some reason + code = self.safe_currency_code(currencyId) + canDeposit = self.safe_bool(asset, 'deposit_enabled') + canWithdraw = self.safe_bool(asset, 'withdraw_enabled') + firstChain = self.safe_dict(chains, 0, {}) + firstPrecisionString = self.parse_precision(self.safe_string(firstChain, 'withdrawal_precision')) + networks: dict = {} + for j in range(0, len(chains)): + chain = chains[j] + networkId = self.safe_string(chain, 'chain') + networkCode = self.network_id_to_code(networkId, code) + if networkId is None: + continue + precisionString = self.parse_precision(self.safe_string(chain, 'withdrawal_precision')) + feeString = self.safe_string(chain, 'withdrawal_fee') + minNetworkDepositString = self.safe_string(chain, 'min_deposit_amount') + minNetworkWithdrawString = self.safe_string(chain, 'min_withdraw_amount') + canDepositChain = self.safe_bool(chain, 'deposit_enabled') + canWithdrawChain = self.safe_bool(chain, 'withdraw_enabled') + network: dict = { + 'id': networkId, + 'network': networkCode, + 'name': None, + 'active': canDepositChain and canWithdrawChain, + 'deposit': canDepositChain, + 'withdraw': canWithdrawChain, + 'fee': self.parse_number(feeString), + 'precision': self.parse_number(precisionString), + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'deposit': { + 'min': self.parse_number(minNetworkDepositString), + 'max': None, + }, + 'withdraw': { + 'min': self.parse_number(minNetworkWithdrawString), + 'max': None, + }, + }, + 'info': chain, + } + networks[networkCode] = network + result[code] = self.safe_currency_structure({ + 'id': currencyId, + 'code': code, + 'name': None, + 'active': canDeposit and canWithdraw, + 'deposit': canDeposit, + 'withdraw': canWithdraw, + 'fee': None, + 'precision': self.parse_number(firstPrecisionString), + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'deposit': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + }, + 'networks': networks, + 'type': 'crypto', + 'info': coin, + }) + return result + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for coinex + + https://docs.coinex.com/api/v2/spot/market/http/list-market + https://docs.coinex.com/api/v2/futures/market/http/list-market + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + promisesUnresolved = [ + self.fetch_spot_markets(params), + self.fetch_contract_markets(params), + ] + promises = promisesUnresolved + spotMarkets = promises[0] + swapMarkets = promises[1] + return self.array_concat(spotMarkets, swapMarkets) + + def fetch_spot_markets(self, params) -> List[Market]: + response = self.v2PublicGetSpotMarket(params) + # + # { + # "code": 0, + # "data": [ + # { + # "market": "BTCUSDT", + # "taker_fee_rate": "0.002", + # "maker_fee_rate": "0.002", + # "min_amount": "0.0005", + # "base_ccy": "BTC", + # "quote_ccy": "USDT", + # "base_ccy_precision": 8, + # "quote_ccy_precision": 2, + # "is_amm_available": True, + # "is_margin_available": True, + # "is_pre_trading_available": True, + # "is_api_trading_available": True + # } + # ], + # "message": "OK" + # } + # + markets = self.safe_list(response, 'data', []) + result = [] + for i in range(0, len(markets)): + market = markets[i] + id = self.safe_string(market, 'market') + baseId = self.safe_string(market, 'base_ccy') + quoteId = self.safe_string(market, 'quote_ccy') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + symbol = base + '/' + quote + result.append({ + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': self.safe_bool(market, 'is_margin_available'), + 'swap': False, + 'future': False, + 'option': False, + 'active': self.safe_bool(market, 'is_api_trading_available'), + 'contract': False, + 'linear': None, + 'inverse': None, + 'taker': self.safe_number(market, 'taker_fee_rate'), + 'maker': self.safe_number(market, 'maker_fee_rate'), + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(self.parse_precision(self.safe_string(market, 'base_ccy_precision'))), + 'price': self.parse_number(self.parse_precision(self.safe_string(market, 'quote_ccy_precision'))), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(market, 'min_amount'), + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + 'info': market, + }) + return result + + def fetch_contract_markets(self, params): + response = self.v2PublicGetFuturesMarket(params) + # + # { + # "code": 0, + # "data": [ + # { + # "base_ccy": "BTC", + # "base_ccy_precision": 8, + # "contract_type": "inverse", + # "leverage": ["1","2","3","5","8","10","15","20","30","50","100"], + # "maker_fee_rate": "0", + # "market": "BTCUSD", + # "min_amount": "10", + # "open_interest_volume": "2566879", + # "quote_ccy": "USD", + # "quote_ccy_precision": 2, + # "taker_fee_rate": "0" + # }, + # ], + # "message": "OK" + # } + # + markets = self.safe_list(response, 'data', []) + result = [] + for i in range(0, len(markets)): + entry = markets[i] + fees = self.fees + leverages = self.safe_list(entry, 'leverage', []) + subType = self.safe_string(entry, 'contract_type') + linear = (subType == 'linear') + inverse = (subType == 'inverse') + id = self.safe_string(entry, 'market') + baseId = self.safe_string(entry, 'base_ccy') + quoteId = self.safe_string(entry, 'quote_ccy') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + settleId = 'USDT' if (subType == 'linear') else baseId + settle = self.safe_currency_code(settleId) + symbol = base + '/' + quote + ':' + settle + leveragesLength = len(leverages) + result.append({ + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': 'swap', + 'spot': False, + 'margin': False, + 'swap': True, + 'future': False, + 'option': False, + 'active': None, + 'contract': True, + 'linear': linear, + 'inverse': inverse, + 'taker': fees['trading']['taker'], + 'maker': fees['trading']['maker'], + 'contractSize': self.parse_number('1'), + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(self.parse_precision(self.safe_string(entry, 'base_ccy_precision'))), + 'price': self.parse_number(self.parse_precision(self.safe_string(entry, 'quote_ccy_precision'))), + }, + 'limits': { + 'leverage': { + 'min': self.safe_number(leverages, 0), + 'max': self.safe_number(leverages, leveragesLength - 1), + }, + 'amount': { + 'min': self.safe_number(entry, 'min_amount'), + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + 'info': entry, + }) + return result + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # Spot fetchTicker, fetchTickers + # + # { + # "close": "62393.47", + # "high": "64106.41", + # "last": "62393.47", + # "low": "59650.01", + # "market": "BTCUSDT", + # "open": "61616.15", + # "period": 86400, + # "value": "28711273.4065667262", + # "volume": "461.76557205", + # "volume_buy": "11.41506354", + # "volume_sell": "7.3240169" + # } + # + # Swap fetchTicker, fetchTickers + # + # { + # "close": "62480.08", + # "high": "64100", + # "index_price": "62443.05", + # "last": "62480.08", + # "low": "59600", + # "mark_price": "62443.05", + # "market": "BTCUSDT", + # "open": "61679.98", + # "period": 86400, + # "value": "180226025.69791713065326633165", + # "volume": "2900.2218", + # "volume_buy": "7.3847", + # "volume_sell": "6.1249" + # } + # + marketType = 'swap' if ('mark_price' in ticker) else 'spot' + marketId = self.safe_string(ticker, 'market') + symbol = self.safe_symbol(marketId, market, None, marketType) + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': None, + 'datetime': None, + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': None, + 'bidVolume': self.safe_string(ticker, 'volume_buy'), + 'ask': None, + 'askVolume': self.safe_string(ticker, 'volume_sell'), + 'vwap': None, + 'open': self.safe_string(ticker, 'open'), + 'close': self.safe_string(ticker, 'close'), + 'last': self.safe_string(ticker, 'last'), + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': self.safe_string(ticker, 'volume'), + 'quoteVolume': None, + 'markPrice': self.safe_string(ticker, 'mark_price'), + 'indexPrice': self.safe_string(ticker, 'index_price'), + 'info': ticker, + }, market) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://docs.coinex.com/api/v2/spot/market/http/list-market-ticker + https://docs.coinex.com/api/v2/futures/market/http/list-market-ticker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + response = None + if market['swap']: + response = self.v2PublicGetFuturesTicker(self.extend(request, params)) + else: + response = self.v2PublicGetSpotTicker(self.extend(request, params)) + # + # Spot + # + # { + # "code": 0, + # "data": [ + # { + # "close": "62393.47", + # "high": "64106.41", + # "last": "62393.47", + # "low": "59650.01", + # "market": "BTCUSDT", + # "open": "61616.15", + # "period": 86400, + # "value": "28711273.4065667262", + # "volume": "461.76557205", + # "volume_buy": "11.41506354", + # "volume_sell": "7.3240169" + # } + # ], + # "message": "OK" + # } + # + # Swap + # + # { + # "code": 0, + # "data": [ + # { + # "close": "62480.08", + # "high": "64100", + # "index_price": "62443.05", + # "last": "62480.08", + # "low": "59600", + # "mark_price": "62443.05", + # "market": "BTCUSDT", + # "open": "61679.98", + # "period": 86400, + # "value": "180226025.69791713065326633165", + # "volume": "2900.2218", + # "volume_buy": "7.3847", + # "volume_sell": "6.1249" + # } + # ], + # "message": "OK" + # } + # + data = self.safe_list(response, 'data', []) + result = self.safe_dict(data, 0, {}) + return self.parse_ticker(result, market) + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://docs.coinex.com/api/v2/spot/market/http/list-market-ticker + https://docs.coinex.com/api/v2/futures/market/http/list-market-ticker + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + symbols = self.market_symbols(symbols) + market = None + if symbols is not None: + symbol = self.safe_value(symbols, 0) + market = self.market(symbol) + marketType, query = self.handle_market_type_and_params('fetchTickers', market, params) + response = None + if marketType == 'swap': + response = self.v2PublicGetFuturesTicker(query) + else: + response = self.v2PublicGetSpotTicker(query) + # + # Spot + # + # { + # "code": 0, + # "data": [ + # { + # "close": "62393.47", + # "high": "64106.41", + # "last": "62393.47", + # "low": "59650.01", + # "market": "BTCUSDT", + # "open": "61616.15", + # "period": 86400, + # "value": "28711273.4065667262", + # "volume": "461.76557205", + # "volume_buy": "11.41506354", + # "volume_sell": "7.3240169" + # } + # ], + # "message": "OK" + # } + # + # Swap + # + # { + # "code": 0, + # "data": [ + # { + # "close": "62480.08", + # "high": "64100", + # "index_price": "62443.05", + # "last": "62480.08", + # "low": "59600", + # "mark_price": "62443.05", + # "market": "BTCUSDT", + # "open": "61679.98", + # "period": 86400, + # "value": "180226025.69791713065326633165", + # "volume": "2900.2218", + # "volume_buy": "7.3847", + # "volume_sell": "6.1249" + # } + # ], + # "message": "OK" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_tickers(data, symbols) + + def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://docs.coinex.com/api/v2/common/http/time + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = self.v2PublicGetTime(params) + # + # { + # "code": 0, + # "data": { + # "timestamp": 1711699867777 + # }, + # "message": "OK" + # } + # + data = self.safe_dict(response, 'data', {}) + return self.safe_integer(data, 'timestamp') + + def fetch_order_book(self, symbol: str, limit: Int = 20, params={}): + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://docs.coinex.com/api/v2/spot/market/http/list-market-depth + https://docs.coinex.com/api/v2/futures/market/http/list-market-depth + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + if limit is None: + limit = 20 # default + request: dict = { + 'market': market['id'], + 'limit': limit, + 'interval': '0', + } + response = None + if market['swap']: + response = self.v2PublicGetFuturesDepth(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "depth": { + # "asks": [ + # ["70851.94", "0.2119"], + # ["70851.95", "0.0004"], + # ["70851.96", "0.0004"] + # ], + # "bids": [ + # ["70851.93", "1.0314"], + # ["70850.93", "0.0021"], + # ["70850.42", "0.0306"] + # ], + # "checksum": 2956436260, + # "last": "70851.94", + # "updated_at": 1712824003252 + # }, + # "is_full": True, + # "market": "BTCUSDT" + # }, + # "message": "OK" + # } + # + else: + response = self.v2PublicGetSpotDepth(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "depth": { + # "asks": [ + # ["70875.31", "0.28670282"], + # ["70875.32", "0.31008114"], + # ["70875.42", "0.05876653"] + # ], + # "bids": [ + # ["70855.3", "0.00632222"], + # ["70855.29", "0.36216834"], + # ["70855.17", "0.10166802"] + # ], + # "checksum": 2313816665, + # "last": "70857.19", + # "updated_at": 1712823790987 + # }, + # "is_full": True, + # "market": "BTCUSDT" + # }, + # "message": "OK" + # } + # + data = self.safe_dict(response, 'data', {}) + depth = self.safe_dict(data, 'depth', {}) + timestamp = self.safe_integer(depth, 'updated_at') + return self.parse_order_book(depth, symbol, timestamp) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # Spot and Swap fetchTrades(public) + # + # { + # "amount": "0.00049432", + # "created_at": 1713849825667, + # "deal_id": 4137517302, + # "price": "66251", + # "side": "buy" + # } + # + # Spot and Margin fetchMyTrades(private) + # + # { + # "amount": "0.00010087", + # "created_at": 1714618087585, + # "deal_id": 4161200602, + # "margin_market": "", + # "market": "BTCUSDT", + # "order_id": 117654919342, + # "price": "57464.04", + # "side": "sell" + # } + # + # Swap fetchMyTrades(private) + # + # { + # "deal_id": 1180222387, + # "created_at": 1714119054558, + # "market": "BTCUSDT", + # "side": "buy", + # "order_id": 136915589622, + # "price": "64376", + # "amount": "0.0001", + # "role": "taker", + # "fee": "0.0299", + # "fee_ccy": "USDT" + # } + # + timestamp = self.safe_integer(trade, 'created_at') + defaultType = self.safe_string(self.options, 'defaultType') + if market is not None: + defaultType = market['type'] + marketId = self.safe_string(trade, 'market') + market = self.safe_market(marketId, market, None, defaultType) + feeCostString = self.safe_string(trade, 'fee') + fee = None + if feeCostString is not None: + feeCurrencyId = self.safe_string(trade, 'fee_ccy') + feeCurrencyCode = self.safe_currency_code(feeCurrencyId) + fee = { + 'cost': feeCostString, + 'currency': feeCurrencyCode, + } + return self.safe_trade({ + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'id': self.safe_string(trade, 'deal_id'), + 'order': self.safe_string(trade, 'order_id'), + 'type': None, + 'side': self.safe_string(trade, 'side'), + 'takerOrMaker': self.safe_string(trade, 'role'), + 'price': self.safe_string(trade, 'price'), + 'amount': self.safe_string(trade, 'amount'), + 'cost': self.safe_string(trade, 'deal_money'), + 'fee': fee, + }, market) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of the most recent trades for a particular symbol + + https://docs.coinex.com/api/v2/spot/market/http/list-market-deals + https://docs.coinex.com/api/v2/futures/market/http/list-market-deals + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + # 'last_id': 0, + } + if limit is not None: + request['limit'] = limit + response = None + if market['swap']: + response = self.v2PublicGetFuturesDeals(self.extend(request, params)) + else: + response = self.v2PublicGetSpotDeals(self.extend(request, params)) + # + # Spot and Swap + # + # { + # "code": 0, + # "data": [ + # { + # "amount": "0.00049432", + # "created_at": 1713849825667, + # "deal_id": 4137517302, + # "price": "66251", + # "side": "buy" + # }, + # ], + # "message": "OK" + # } + # + return self.parse_trades(response['data'], market, since, limit) + + def fetch_trading_fee(self, symbol: str, params={}) -> TradingFeeInterface: + """ + fetch the trading fees for a market + + https://docs.coinex.com/api/v2/spot/market/http/list-market + https://docs.coinex.com/api/v2/futures/market/http/list-market + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `fee structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + response = None + if market['spot']: + response = self.v2PublicGetSpotMarket(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "base_ccy": "BTC", + # "base_ccy_precision": 8, + # "is_amm_available": False, + # "is_margin_available": True, + # "maker_fee_rate": "0.002", + # "market": "BTCUSDT", + # "min_amount": "0.0001", + # "quote_ccy": "USDT", + # "quote_ccy_precision": 2, + # "taker_fee_rate": "0.002" + # } + # ], + # "message": "OK" + # } + # + else: + response = self.v2PublicGetFuturesMarket(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "base_ccy": "BTC", + # "base_ccy_precision": 8, + # "contract_type": "linear", + # "leverage": ["1","2","3","5","8","10","15","20","30","50","100"], + # "maker_fee_rate": "0", + # "market": "BTCUSDT", + # "min_amount": "0.0001", + # "open_interest_volume": "185.7498", + # "quote_ccy": "USDT", + # "quote_ccy_precision": 2, + # "taker_fee_rate": "0" + # } + # ], + # "message": "OK" + # } + # + data = self.safe_list(response, 'data', []) + result = self.safe_dict(data, 0, {}) + return self.parse_trading_fee(result, market) + + def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://docs.coinex.com/api/v2/spot/market/http/list-market + https://docs.coinex.com/api/v2/futures/market/http/list-market + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + self.load_markets() + type = None + type, params = self.handle_market_type_and_params('fetchTradingFees', None, params) + response = None + if type == 'swap': + response = self.v2PublicGetFuturesMarket(params) + # + # { + # "code": 0, + # "data": [ + # { + # "base_ccy": "BTC", + # "base_ccy_precision": 8, + # "contract_type": "linear", + # "leverage": ["1","2","3","5","8","10","15","20","30","50","100"], + # "maker_fee_rate": "0", + # "market": "BTCUSDT", + # "min_amount": "0.0001", + # "open_interest_volume": "185.7498", + # "quote_ccy": "USDT", + # "quote_ccy_precision": 2, + # "taker_fee_rate": "0" + # } + # ], + # "message": "OK" + # } + # + else: + response = self.v2PublicGetSpotMarket(params) + # + # { + # "code": 0, + # "data": [ + # { + # "base_ccy": "BTC", + # "base_ccy_precision": 8, + # "is_amm_available": False, + # "is_margin_available": True, + # "maker_fee_rate": "0.002", + # "market": "BTCUSDT", + # "min_amount": "0.0001", + # "quote_ccy": "USDT", + # "quote_ccy_precision": 2, + # "taker_fee_rate": "0.002" + # }, + # ], + # "message": "OK" + # } + # + data = self.safe_list(response, 'data', []) + result: dict = {} + for i in range(0, len(data)): + entry = data[i] + marketId = self.safe_string(entry, 'market') + market = self.safe_market(marketId, None, None, type) + symbol = market['symbol'] + result[symbol] = self.parse_trading_fee(entry, market) + return result + + def parse_trading_fee(self, fee: dict, market: Market = None) -> TradingFeeInterface: + marketId = self.safe_value(fee, 'market') + symbol = self.safe_symbol(marketId, market) + return { + 'info': fee, + 'symbol': symbol, + 'maker': self.safe_number(fee, 'maker_fee_rate'), + 'taker': self.safe_number(fee, 'taker_fee_rate'), + 'percentage': True, + 'tierBased': True, + } + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # { + # "close": "66999.95", + # "created_at": 1713934620000, + # "high": "66999.95", + # "low": "66988.53", + # "market": "BTCUSDT", + # "open": "66988.53", + # "value": "0.1572393", # base volume + # "volume": "10533.2501364336" # quote volume + # } + # + return [ + self.safe_integer(ohlcv, 'created_at'), + self.safe_number(ohlcv, 'open'), + self.safe_number(ohlcv, 'high'), + self.safe_number(ohlcv, 'low'), + self.safe_number(ohlcv, 'close'), + self.safe_number(ohlcv, 'value'), + ] + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://docs.coinex.com/api/v2/spot/market/http/list-market-kline + https://docs.coinex.com/api/v2/futures/market/http/list-market-kline + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + 'period': self.safe_string(self.timeframes, timeframe, timeframe), + } + if limit is not None: + request['limit'] = limit + response = None + if market['swap']: + response = self.v2PublicGetFuturesKline(self.extend(request, params)) + else: + response = self.v2PublicGetSpotKline(self.extend(request, params)) + # + # Spot and Swap + # + # { + # "code": 0, + # "data": [ + # { + # "close": "66999.95", + # "created_at": 1713934620000, + # "high": "66999.95", + # "low": "66988.53", + # "market": "BTCUSDT", + # "open": "66988.53", + # "value": "0.1572393", + # "volume": "10533.2501364336" + # }, + # ], + # "message": "OK" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_ohlcvs(data, market, timeframe, since, limit) + + def fetch_margin_balance(self, params={}): + self.load_markets() + response = self.v2PrivateGetAssetsMarginBalance(params) + # + # { + # "data": [ + # { + # "margin_account": "BTCUSDT", + # "base_ccy": "BTC", + # "quote_ccy": "USDT", + # "available": { + # "base_ccy": "0.00000026", + # "quote_ccy": "0" + # }, + # "frozen": { + # "base_ccy": "0", + # "quote_ccy": "0" + # }, + # "repaid": { + # "base_ccy": "0", + # "quote_ccy": "0" + # }, + # "interest": { + # "base_ccy": "0", + # "quote_ccy": "0" + # }, + # "rik_rate": "", + # "liq_price": "" + # }, + # ], + # "code": 0, + # "message": "OK" + # } + # + result: dict = {'info': response} + balances = self.safe_list(response, 'data', []) + for i in range(0, len(balances)): + entry = balances[i] + free = self.safe_dict(entry, 'available', {}) + used = self.safe_dict(entry, 'frozen', {}) + loan = self.safe_dict(entry, 'repaid', {}) + interest = self.safe_dict(entry, 'interest', {}) + baseAccount = self.account() + baseCurrencyId = self.safe_string(entry, 'base_ccy') + baseCurrencyCode = self.safe_currency_code(baseCurrencyId) + baseAccount['free'] = self.safe_string(free, 'base_ccy') + baseAccount['used'] = self.safe_string(used, 'base_ccy') + baseDebt = self.safe_string(loan, 'base_ccy') + baseInterest = self.safe_string(interest, 'base_ccy') + baseAccount['debt'] = Precise.string_add(baseDebt, baseInterest) + result[baseCurrencyCode] = baseAccount + return self.safe_balance(result) + + def fetch_spot_balance(self, params={}): + self.load_markets() + response = self.v2PrivateGetAssetsSpotBalance(params) + # + # { + # "code": 0, + # "data": [ + # { + # "available": "0.00000046", + # "ccy": "USDT", + # "frozen": "0" + # } + # ], + # "message": "OK" + # } + # + result: dict = {'info': response} + balances = self.safe_list(response, 'data', []) + for i in range(0, len(balances)): + entry = balances[i] + currencyId = self.safe_string(entry, 'ccy') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(entry, 'available') + account['used'] = self.safe_string(entry, 'frozen') + result[code] = account + return self.safe_balance(result) + + def fetch_swap_balance(self, params={}): + self.load_markets() + response = self.v2PrivateGetAssetsFuturesBalance(params) + # + # { + # "code": 0, + # "data": [ + # { + # "available": "0.00000046", + # "ccy": "USDT", + # "frozen": "0", + # "margin": "0", + # "transferrable": "0.00000046", + # "unrealized_pnl": "0" + # } + # ], + # "message": "OK" + # } + # + result: dict = {'info': response} + balances = self.safe_list(response, 'data', []) + for i in range(0, len(balances)): + entry = balances[i] + currencyId = self.safe_string(entry, 'ccy') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(entry, 'available') + account['used'] = self.safe_string(entry, 'frozen') + result[code] = account + return self.safe_balance(result) + + def fetch_financial_balance(self, params={}): + self.load_markets() + response = self.v2PrivateGetAssetsFinancialBalance(params) + # + # { + # "code": 0, + # "data": [ + # { + # "available": "0.00000046", + # "ccy": "USDT", + # "frozen": "0" + # } + # ], + # "message": "OK" + # } + # + result: dict = {'info': response} + balances = self.safe_list(response, 'data', []) + for i in range(0, len(balances)): + entry = balances[i] + currencyId = self.safe_string(entry, 'ccy') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(entry, 'available') + account['used'] = self.safe_string(entry, 'frozen') + result[code] = account + return self.safe_balance(result) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://docs.coinex.com/api/v2/assets/balance/http/get-spot-balance # spot + https://docs.coinex.com/api/v2/assets/balance/http/get-futures-balance # swap + https://docs.coinex.com/api/v2/assets/balance/http/get-marigin-balance # margin + https://docs.coinex.com/api/v2/assets/balance/http/get-financial-balance # financial + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: 'margin', 'swap', 'financial', or 'spot' + :returns dict: a `balance structure ` + """ + marketType = None + marketType, params = self.handle_market_type_and_params('fetchBalance', None, params) + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchBalance', params) + isMargin = (marginMode is not None) or (marketType == 'margin') + if marketType == 'swap': + return self.fetch_swap_balance(params) + elif marketType == 'financial': + return self.fetch_financial_balance(params) + elif isMargin: + return self.fetch_margin_balance(params) + else: + return self.fetch_spot_balance(params) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'rejected': 'rejected', + 'open': 'open', + 'not_deal': 'open', + 'part_deal': 'open', + 'done': 'closed', + 'cancel': 'canceled', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # Spot and Margin createOrder, createOrders, editOrder, cancelOrders, cancelOrder, fetchOpenOrders + # + # { + # "amount": "0.0001", + # "base_fee": "0", + # "ccy": "BTC", + # "client_id": "x-167673045-a0a3c6461459a801", + # "created_at": 1714114386250, + # "discount_fee": "0", + # "filled_amount": "0", + # "filled_value": "0", + # "last_fill_amount": "0", + # "last_fill_price": "0", + # "maker_fee_rate": "0.002", + # "market": "BTCUSDT", + # "market_type": "SPOT", + # "order_id": 117178743547, + # "price": "61000", + # "quote_fee": "0", + # "side": "buy", + # "taker_fee_rate": "0.002", + # "type": "limit", + # "unfilled_amount": "0.0001", + # "updated_at": 1714114386250 + # } + # + # Spot and Margin fetchClosedOrders + # + # { + # "order_id": 117180532345, + # "market": "BTCUSDT", + # "market_type": "SPOT", + # "side": "sell", + # "type": "market", + # "ccy": "BTC", + # "amount": "0.00015484", + # "price": "0", + # "client_id": "", + # "created_at": 1714116494219, + # "updated_at": 0, + # "base_fee": "0", + # "quote_fee": "0.0199931699632", + # "discount_fee": "0", + # "maker_fee_rate": "0", + # "taker_fee_rate": "0.002", + # "unfilled_amount": "0", + # "filled_amount": "0.00015484", + # "filled_value": "9.9965849816" + # } + # + # Spot, Margin and Swap trigger createOrder, createOrders, editOrder + # + # { + # "stop_id": 117180138153 + # } + # + # Swap createOrder, createOrders, editOrder, cancelOrders, cancelOrder, fetchOpenOrders, fetchClosedOrders, closePosition + # + # { + # "amount": "0.0001", + # "client_id": "x-167673045-1471b81d747080a0", + # "created_at": 1714116769986, + # "fee": "0", + # "fee_ccy": "USDT", + # "filled_amount": "0", + # "filled_value": "0", + # "last_filled_amount": "0", + # "last_filled_price": "0", + # "maker_fee_rate": "0.0003", + # "market": "BTCUSDT", + # "market_type": "FUTURES", + # "order_id": 136913377780, + # "price": "61000.42", + # "realized_pnl": "0", + # "side": "buy", + # "taker_fee_rate": "0.0005", + # "type": "limit", + # "unfilled_amount": "0.0001", + # "updated_at": 1714116769986 + # } + # + # Swap stopLossPrice and takeProfitPrice createOrder + # + # { + # "adl_level": 1, + # "ath_margin_size": "2.14586666", + # "ath_position_amount": "0.0001", + # "avg_entry_price": "64376", + # "bkr_price": "0", + # "close_avbl": "0.0001", + # "cml_position_value": "6.4376", + # "created_at": 1714119054558, + # "leverage": "3", + # "liq_price": "0", + # "maintenance_margin_rate": "0.005", + # "maintenance_margin_value": "0.03218632", + # "margin_avbl": "2.14586666", + # "margin_mode": "cross", + # "market": "BTCUSDT", + # "market_type": "FUTURES", + # "max_position_value": "6.4376", + # "open_interest": "0.0001", + # "position_id": 303884204, + # "position_margin_rate": "3.10624785634397912265", + # "realized_pnl": "-0.0032188", + # "settle_price": "64376", + # "settle_value": "6.4376", + # "side": "long", + # "stop_loss_price": "62000", + # "stop_loss_type": "latest_price", + # "take_profit_price": "0", + # "take_profit_type": "", + # "unrealized_pnl": "0", + # "updated_at": 1714119054559 + # } + # + # Swap fetchOrder + # + # { + # "amount": "0.0001", + # "client_id": "x-167673045-da5f31dcd478a829", + # "created_at": 1714460987164, + # "fee": "0", + # "fee_ccy": "USDT", + # "filled_amount": "0", + # "filled_value": "0", + # "last_filled_amount": "0", + # "last_filled_price": "0", + # "maker_fee_rate": "0.0003", + # "market": "BTCUSDT", + # "market_type": "FUTURES", + # "order_id": 137319868771, + # "price": "61000", + # "realized_pnl": "0", + # "side": "buy", + # "status": "open", + # "taker_fee_rate": "0.0005", + # "type": "limit", + # "unfilled_amount": "0.0001", + # "updated_at": 1714460987164 + # } + # + # Spot and Margin fetchOrder + # + # { + # "amount": "0.0001", + # "base_fee": "0", + # "ccy": "BTC", + # "client_id": "x-167673045-da918d6724e3af81", + # "created_at": 1714461638958, + # "discount_fee": "0", + # "filled_amount": "0", + # "filled_value": "0", + # "last_fill_amount": "0", + # "last_fill_price": "0", + # "maker_fee_rate": "0.002", + # "market": "BTCUSDT", + # "market_type": "SPOT", + # "order_id": 117492012985, + # "price": "61000", + # "quote_fee": "0", + # "side": "buy", + # "status": "open", + # "taker_fee_rate": "0.002", + # "type": "limit", + # "unfilled_amount": "0.0001", + # "updated_at": 1714461638958 + # } + # + # Swap trigger fetchOpenOrders, fetchClosedOrders - Spot and Swap trigger cancelOrders, cancelOrder + # + # { + # "amount": "0.0001", + # "client_id": "x-167673045-a7d7714c6478acf6", + # "created_at": 1714187923820, + # "market": "BTCUSDT", + # "market_type": "FUTURES", + # "price": "61000", + # "side": "buy", + # "stop_id": 136984426097, + # "trigger_direction": "higher", + # "trigger_price": "62000", + # "trigger_price_type": "latest_price", + # "type": "limit", + # "updated_at": 1714187974363 + # } + # + # Spot and Margin trigger fetchOpenOrders, fetchClosedOrders + # + # { + # "stop_id": 117586439530, + # "market": "BTCUSDT", + # "market_type": "SPOT", + # "ccy": "BTC", + # "side": "buy", + # "type": "limit", + # "amount": "0.0001", + # "price": "51000", + # "trigger_price": "52000", + # "trigger_direction": "higher", + # "trigger_price_type": "mark_price", + # "client_id": "x-167673045-df61777094c69312", + # "created_at": 1714551237335, + # "updated_at": 1714551237335 + # } + # + rawStatus = self.safe_string(order, 'status') + timestamp = self.safe_integer(order, 'created_at') + updatedTimestamp = self.safe_integer(order, 'updated_at') + if updatedTimestamp == 0: + updatedTimestamp = timestamp + marketId = self.safe_string(order, 'market') + defaultType = self.safe_string(self.options, 'defaultType') + orderType = self.safe_string_lower(order, 'market_type', defaultType) + if orderType == 'futures': + orderType = 'swap' + marketType = 'swap' if (orderType == 'swap') else 'spot' + market = self.safe_market(marketId, market, None, marketType) + feeCurrencyId = self.safe_string(order, 'fee_ccy') + feeCurrency = self.safe_currency_code(feeCurrencyId) + if feeCurrency is None: + feeCurrency = market['quote'] + side = self.safe_string(order, 'side') + if side == 'long': + side = 'buy' + elif side == 'short': + side = 'sell' + clientOrderId = self.safe_string(order, 'client_id') + if clientOrderId == '': + clientOrderId = None + return self.safe_order({ + 'id': self.safe_string_n(order, ['position_id', 'order_id', 'stop_id']), + 'clientOrderId': clientOrderId, + 'datetime': self.iso8601(timestamp), + 'timestamp': timestamp, + 'lastTradeTimestamp': updatedTimestamp, + 'status': self.parse_order_status(rawStatus), + 'symbol': market['symbol'], + 'type': self.safe_string(order, 'type'), + 'timeInForce': None, + 'postOnly': None, + 'reduceOnly': None, + 'side': side, + 'price': self.safe_string(order, 'price'), + 'triggerPrice': self.safe_string(order, 'trigger_price'), + 'takeProfitPrice': self.safe_number(order, 'take_profit_price'), + 'stopLossPrice': self.safe_number(order, 'stop_loss_price'), + 'cost': self.safe_string(order, 'filled_value'), + 'average': self.safe_string(order, 'avg_entry_price'), + 'amount': self.safe_string(order, 'amount'), + 'filled': self.safe_string(order, 'filled_amount'), + 'remaining': self.safe_string(order, 'unfilled_amount'), + 'trades': None, + 'fee': { + 'currency': feeCurrency, + 'cost': self.safe_string_2(order, 'quote_fee', 'fee'), + }, + 'info': order, + }, market) + + def create_market_buy_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market buy order by providing the symbol and cost + + https://viabtc.github.io/coinex_api_en_doc/spot/#docsspot003_trade003_market_order + https://docs.coinex.com/api/v2/spot/order/http/put-order + + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' createMarketBuyOrderWithCost() supports spot orders only') + params['createMarketBuyOrderRequiresPrice'] = False + return self.create_order(symbol, 'market', 'buy', cost, None, params) + + def create_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + market = self.market(symbol) + swap = market['swap'] + clientOrderId = self.safe_string_2(params, 'client_id', 'clientOrderId') + triggerPrice = self.safe_string_2(params, 'stopPrice', 'triggerPrice') + stopLossPrice = self.safe_string(params, 'stopLossPrice') + takeProfitPrice = self.safe_string(params, 'takeProfitPrice') + option = self.safe_string(params, 'option') + isMarketOrder = type == 'market' + postOnly = self.is_post_only(isMarketOrder, option == 'maker_only', params) + timeInForceRaw = self.safe_string_upper(params, 'timeInForce') + reduceOnly = self.safe_bool(params, 'reduceOnly') + if reduceOnly: + if not market['swap']: + raise InvalidOrder(self.id + ' createOrder() does not support reduceOnly for ' + market['type'] + ' orders, reduceOnly orders are supported for swap markets only') + request: dict = { + 'market': market['id'], + } + if clientOrderId is None: + defaultId = 'x-167673045' + brokerId = self.safe_string(self.options, 'brokerId', defaultId) + request['client_id'] = brokerId + '-' + self.uuid16() + else: + request['client_id'] = clientOrderId + if (stopLossPrice is None) and (takeProfitPrice is None): + if not reduceOnly: + request['side'] = side + requestType = type + if postOnly: + requestType = 'maker_only' + elif timeInForceRaw is not None: + if timeInForceRaw == 'IOC': + requestType = 'ioc' + elif timeInForceRaw == 'FOK': + requestType = 'fok' + if not isMarketOrder: + request['price'] = self.price_to_precision(symbol, price) + request['type'] = requestType + if swap: + request['market_type'] = 'FUTURES' + if stopLossPrice or takeProfitPrice: + if stopLossPrice: + request['stop_loss_price'] = self.price_to_precision(symbol, stopLossPrice) + request['stop_loss_type'] = self.safe_string(params, 'stop_type', 'latest_price') + elif takeProfitPrice: + request['take_profit_price'] = self.price_to_precision(symbol, takeProfitPrice) + request['take_profit_type'] = self.safe_string(params, 'stop_type', 'latest_price') + else: + request['amount'] = self.amount_to_precision(symbol, amount) + if triggerPrice is not None: + request['trigger_price'] = self.price_to_precision(symbol, triggerPrice) + request['trigger_price_type'] = self.safe_string(params, 'stop_type', 'latest_price') + else: + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('createOrder', params) + if marginMode is not None: + request['market_type'] = 'MARGIN' + else: + request['market_type'] = 'SPOT' + if (type == 'market') and (side == 'buy'): + createMarketBuyOrderRequiresPrice = True + createMarketBuyOrderRequiresPrice, params = self.handle_option_and_params(params, 'createOrder', 'createMarketBuyOrderRequiresPrice', True) + cost = self.safe_number(params, 'cost') + params = self.omit(params, 'cost') + if createMarketBuyOrderRequiresPrice: + if (price is None) and (cost is None): + raise InvalidOrder(self.id + ' createOrder() requires the price argument for market buy orders to calculate the total cost to spend(amount * price), alternatively set the createMarketBuyOrderRequiresPrice option or param to False and pass the cost to spend in the amount argument') + else: + amountString = self.number_to_string(amount) + priceString = self.number_to_string(price) + quoteAmount = self.parse_to_numeric(Precise.string_mul(amountString, priceString)) + costRequest = cost if (cost is not None) else quoteAmount + request['amount'] = self.cost_to_precision(symbol, costRequest) + else: + request['amount'] = self.cost_to_precision(symbol, amount) + else: + request['amount'] = self.amount_to_precision(symbol, amount) + if triggerPrice is not None: + request['trigger_price'] = self.price_to_precision(symbol, triggerPrice) + params = self.omit(params, ['reduceOnly', 'timeInForce', 'postOnly', 'stopPrice', 'triggerPrice', 'stopLossPrice', 'takeProfitPrice']) + return self.extend(request, params) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://docs.coinex.com/api/v2/spot/order/http/put-order + https://docs.coinex.com/api/v2/spot/order/http/put-stop-order + https://docs.coinex.com/api/v2/futures/order/http/put-order + https://docs.coinex.com/api/v2/futures/order/http/put-stop-order + https://docs.coinex.com/api/v2/futures/position/http/close-position + https://docs.coinex.com/api/v2/futures/position/http/set-position-stop-loss + https://docs.coinex.com/api/v2/futures/position/http/set-position-take-profit + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: price to trigger stop orders + :param float [params.stopLossPrice]: price to trigger stop loss orders + :param float [params.takeProfitPrice]: price to trigger take profit orders + :param str [params.timeInForce]: 'GTC', 'IOC', 'FOK', 'PO' + :param boolean [params.postOnly]: set to True if you wish to make a post only order + :param boolean [params.reduceOnly]: *contract only* indicates if self order is to reduce the size of a position + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + reduceOnly = self.safe_bool(params, 'reduceOnly') + triggerPrice = self.safe_string_2(params, 'stopPrice', 'triggerPrice') + stopLossTriggerPrice = self.safe_string(params, 'stopLossPrice') + takeProfitTriggerPrice = self.safe_string(params, 'takeProfitPrice') + isTriggerOrder = triggerPrice is not None + isStopLossTriggerOrder = stopLossTriggerPrice is not None + isTakeProfitTriggerOrder = takeProfitTriggerPrice is not None + isStopLossOrTakeProfitTrigger = isStopLossTriggerOrder or isTakeProfitTriggerOrder + request = self.create_order_request(symbol, type, side, amount, price, params) + response = None + if market['spot']: + if isTriggerOrder: + response = self.v2PrivatePostSpotStopOrder(request) + # + # { + # "code": 0, + # "data": { + # "stop_id": 117180138153 + # }, + # "message": "OK" + # } + # + else: + response = self.v2PrivatePostSpotOrder(request) + # + # { + # "code": 0, + # "data": { + # "amount": "0.0001", + # "base_fee": "0", + # "ccy": "BTC", + # "client_id": "x-167673045-a0a3c6461459a801", + # "created_at": 1714114386250, + # "discount_fee": "0", + # "filled_amount": "0", + # "filled_value": "0", + # "last_fill_amount": "0", + # "last_fill_price": "0", + # "maker_fee_rate": "0.002", + # "market": "BTCUSDT", + # "market_type": "SPOT", + # "order_id": 117178743547, + # "price": "61000", + # "quote_fee": "0", + # "side": "buy", + # "taker_fee_rate": "0.002", + # "type": "limit", + # "unfilled_amount": "0.0001", + # "updated_at": 1714114386250 + # }, + # "message": "OK" + # } + # + else: + if isTriggerOrder: + response = self.v2PrivatePostFuturesStopOrder(request) + # + # { + # "code": 0, + # "data": { + # "stop_id": 136915460994 + # }, + # "message": "OK" + # } + # + elif isStopLossOrTakeProfitTrigger: + if isStopLossTriggerOrder: + response = self.v2PrivatePostFuturesSetPositionStopLoss(request) + # + # { + # "code": 0, + # "data": { + # "adl_level": 1, + # "ath_margin_size": "2.14586666", + # "ath_position_amount": "0.0001", + # "avg_entry_price": "64376", + # "bkr_price": "0", + # "close_avbl": "0.0001", + # "cml_position_value": "6.4376", + # "created_at": 1714119054558, + # "leverage": "3", + # "liq_price": "0", + # "maintenance_margin_rate": "0.005", + # "maintenance_margin_value": "0.03218632", + # "margin_avbl": "2.14586666", + # "margin_mode": "cross", + # "market": "BTCUSDT", + # "market_type": "FUTURES", + # "max_position_value": "6.4376", + # "open_interest": "0.0001", + # "position_id": 303884204, + # "position_margin_rate": "3.10624785634397912265", + # "realized_pnl": "-0.0032188", + # "settle_price": "64376", + # "settle_value": "6.4376", + # "side": "long", + # "stop_loss_price": "62000", + # "stop_loss_type": "latest_price", + # "take_profit_price": "0", + # "take_profit_type": "", + # "unrealized_pnl": "0", + # "updated_at": 1714119054559 + # }, + # "message": "OK" + # } + # + elif isTakeProfitTriggerOrder: + response = self.v2PrivatePostFuturesSetPositionTakeProfit(request) + # + # { + # "code": 0, + # "data": { + # "adl_level": 1, + # "ath_margin_size": "2.14586666", + # "ath_position_amount": "0.0001", + # "avg_entry_price": "64376", + # "bkr_price": "0", + # "close_avbl": "0.0001", + # "cml_position_value": "6.4376", + # "created_at": 1714119054558, + # "leverage": "3", + # "liq_price": "0", + # "maintenance_margin_rate": "0.005", + # "maintenance_margin_value": "0.03218632", + # "margin_avbl": "2.14586666", + # "margin_mode": "cross", + # "market": "BTCUSDT", + # "market_type": "FUTURES", + # "max_position_value": "6.4376", + # "open_interest": "0.0001", + # "position_id": 303884204, + # "position_margin_rate": "3.10624785634397912265", + # "realized_pnl": "-0.0032188", + # "settle_price": "64376", + # "settle_value": "6.4376", + # "side": "long", + # "stop_loss_price": "62000", + # "stop_loss_type": "latest_price", + # "take_profit_price": "70000", + # "take_profit_type": "latest_price", + # "unrealized_pnl": "0", + # "updated_at": 1714119054559 + # }, + # "message": "OK" + # } + # + else: + if reduceOnly: + response = self.v2PrivatePostFuturesClosePosition(request) + # + # { + # "code": 0, + # "data": { + # "amount": "0.0001", + # "client_id": "x-167673045-4f264600c432ac06", + # "created_at": 1714119323764, + # "fee": "0.003221", + # "fee_ccy": "USDT", + # "filled_amount": "0.0001", + # "filled_value": "6.442017", + # "last_filled_amount": "0.0001", + # "last_filled_price": "64420.17", + # "maker_fee_rate": "0", + # "market": "BTCUSDT", + # "market_type": "FUTURES", + # "order_id": 136915813578, + # "price": "0", + # "realized_pnl": "0.004417", + # "side": "sell", + # "taker_fee_rate": "0.0005", + # "type": "market", + # "unfilled_amount": "0", + # "updated_at": 1714119323764 + # }, + # "message": "OK" + # } + # + else: + response = self.v2PrivatePostFuturesOrder(request) + # + # { + # "code": 0, + # "data": { + # "amount": "0.0001", + # "client_id": "x-167673045-1471b81d747080a0", + # "created_at": 1714116769986, + # "fee": "0", + # "fee_ccy": "USDT", + # "filled_amount": "0", + # "filled_value": "0", + # "last_filled_amount": "0", + # "last_filled_price": "0", + # "maker_fee_rate": "0.0003", + # "market": "BTCUSDT", + # "market_type": "FUTURES", + # "order_id": 136913377780, + # "price": "61000.42", + # "realized_pnl": "0", + # "side": "buy", + # "taker_fee_rate": "0.0005", + # "type": "limit", + # "unfilled_amount": "0.0001", + # "updated_at": 1714116769986 + # }, + # "message": "OK" + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data, market) + + def create_orders(self, orders: List[OrderRequest], params={}) -> List[Order]: + """ + create a list of trade orders(all orders should be of the same symbol) + + https://docs.coinex.com/api/v2/spot/order/http/put-multi-order + https://docs.coinex.com/api/v2/spot/order/http/put-multi-stop-order + https://docs.coinex.com/api/v2/futures/order/http/put-multi-order + https://docs.coinex.com/api/v2/futures/order/http/put-multi-stop-order + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the api endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + ordersRequests = [] + symbol = None + reduceOnly = False + isTriggerOrder = False + isStopLossOrTakeProfitTrigger = False + for i in range(0, len(orders)): + rawOrder = orders[i] + marketId = self.safe_string(rawOrder, 'symbol') + if symbol is None: + symbol = marketId + else: + if symbol != marketId: + raise BadRequest(self.id + ' createOrders() requires all orders to have the same symbol') + type = self.safe_string(rawOrder, 'type') + side = self.safe_string(rawOrder, 'side') + amount = self.safe_value(rawOrder, 'amount') + price = self.safe_value(rawOrder, 'price') + orderParams = self.safe_value(rawOrder, 'params', {}) + if type != 'limit': + raise NotSupported(self.id + ' createOrders() does not support ' + type + ' orders, only limit orders are accepted') + reduceOnly = self.safe_value(orderParams, 'reduceOnly') + triggerPrice = self.safe_number_2(orderParams, 'stopPrice', 'triggerPrice') + stopLossTriggerPrice = self.safe_number(orderParams, 'stopLossPrice') + takeProfitTriggerPrice = self.safe_number(orderParams, 'takeProfitPrice') + isTriggerOrder = triggerPrice is not None + isStopLossTriggerOrder = stopLossTriggerPrice is not None + isTakeProfitTriggerOrder = takeProfitTriggerPrice is not None + isStopLossOrTakeProfitTrigger = isStopLossTriggerOrder or isTakeProfitTriggerOrder + orderRequest = self.create_order_request(marketId, type, side, amount, price, orderParams) + ordersRequests.append(orderRequest) + market = self.market(symbol) + request: dict = { + 'market': market['id'], + 'orders': ordersRequests, + } + response = None + if market['spot']: + if isTriggerOrder: + response = self.v2PrivatePostSpotBatchStopOrder(request) + # + # { + # "code": 0, + # "data": [ + # { + # "code": 0, + # "data": { + # "stop_id": 117186257510 + # }, + # "message": "OK" + # }, + # ], + # "message": "OK" + # } + # + else: + response = self.v2PrivatePostSpotBatchOrder(request) + # + # { + # "code": 0, + # "data": [ + # { + # "amount": "0.0001", + # "base_fee": "0", + # "ccy": "BTC", + # "client_id": "x-167673045-f3651372049dab0d", + # "created_at": 1714121403450, + # "discount_fee": "0", + # "filled_amount": "0", + # "filled_value": "0", + # "last_fill_amount": "0", + # "last_fill_price": "0", + # "maker_fee_rate": "0.002", + # "market": "BTCUSDT", + # "market_type": "SPOT", + # "order_id": 117185362233, + # "price": "61000", + # "quote_fee": "0", + # "side": "buy", + # "taker_fee_rate": "0.002", + # "type": "limit", + # "unfilled_amount": "0.0001", + # "updated_at": 1714121403450 + # }, + # { + # "code": 3109, + # "data": null, + # "message": "balance not enough" + # } + # ], + # "message": "OK" + # } + # + else: + if isTriggerOrder: + response = self.v2PrivatePostFuturesBatchStopOrder(request) + # + # { + # "code": 0, + # "data": [ + # { + # "code": 0, + # "data": { + # "stop_id": 136919625994 + # }, + # "message": "OK" + # }, + # ], + # "message": "OK" + # } + # + elif isStopLossOrTakeProfitTrigger: + raise NotSupported(self.id + ' createOrders() does not support stopLossPrice or takeProfitPrice orders') + else: + if reduceOnly: + raise NotSupported(self.id + ' createOrders() does not support reduceOnly orders') + else: + response = self.v2PrivatePostFuturesBatchOrder(request) + # + # { + # "code": 0, + # "data": [ + # { + # "code": 0, + # "data": { + # "amount": "0.0001", + # "client_id": "x-167673045-2cb7436f3462a654", + # "created_at": 1714122832493, + # "fee": "0", + # "fee_ccy": "USDT", + # "filled_amount": "0", + # "filled_value": "0", + # "last_filled_amount": "0", + # "last_filled_price": "0", + # "maker_fee_rate": "0.0003", + # "market": "BTCUSDT", + # "market_type": "FUTURES", + # "order_id": 136918835063, + # "price": "61000", + # "realized_pnl": "0", + # "side": "buy", + # "taker_fee_rate": "0.0005", + # "type": "limit", + # "unfilled_amount": "0.0001", + # "updated_at": 1714122832493 + # }, + # "message": "OK" + # }, + # ], + # "message": "OK" + # } + # + data = self.safe_list(response, 'data', []) + results = [] + for i in range(0, len(data)): + entry = data[i] + status = None + code = self.safe_integer(entry, 'code') + if code is not None: + if code != 0: + status = 'rejected' + else: + status = 'open' + innerData = self.safe_dict(entry, 'data', {}) + order = None + if market['spot'] and not isTriggerOrder: + entry['status'] = status + order = self.parse_order(entry, market) + else: + innerData['status'] = status + order = self.parse_order(innerData, market) + results.append(order) + return results + + def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): + """ + cancel multiple orders + + https://docs.coinex.com/api/v2/spot/order/http/cancel-batch-order + https://docs.coinex.com/api/v2/spot/order/http/cancel-batch-stop-order + https://docs.coinex.com/api/v2/futures/order/http/cancel-batch-order + https://docs.coinex.com/api/v2/futures/order/http/cancel-batch-stop-order + + :param str[] ids: order ids + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: set to True for canceling stop orders + :returns dict: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrders() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + trigger = self.safe_bool_2(params, 'stop', 'trigger') + params = self.omit(params, ['stop', 'trigger']) + response = None + requestIds = [] + for i in range(0, len(ids)): + requestIds.append(int(ids[i])) + if trigger: + request['stop_ids'] = requestIds + else: + request['order_ids'] = requestIds + if market['spot']: + if trigger: + response = self.v2PrivatePostSpotCancelBatchStopOrder(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "code": 0, + # "data": { + # "amount": "0.0001", + # "ccy": "BTC", + # "client_id": "x-167673045-8e33d6f4a4bcb022", + # "created_at": 1714188827291, + # "market": "BTCUSDT", + # "market_type": "SPOT", + # "price": "61000", + # "side": "buy", + # "stop_id": 117248845854, + # "trigger_direction": "higher", + # "trigger_price": "62000", + # "trigger_price_type": "mark_price", + # "type": "limit", + # "updated_at": 1714188827291 + # }, + # "message": "OK" + # }, + # ], + # "message": "OK" + # } + # + else: + response = self.v2PrivatePostSpotCancelBatchOrder(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "code": 0, + # "data": { + # "amount": "0.0001", + # "base_fee": "0", + # "ccy": "BTC", + # "client_id": "x-167673045-c1cc78e5b42d8c4e", + # "created_at": 1714188449497, + # "discount_fee": "0", + # "filled_amount": "0", + # "filled_value": "0", + # "last_fill_amount": "0", + # "last_fill_price": "0", + # "maker_fee_rate": "0.002", + # "market": "BTCUSDT", + # "market_type": "SPOT", + # "order_id": 117248494358, + # "price": "60000", + # "quote_fee": "0", + # "side": "buy", + # "taker_fee_rate": "0.002", + # "type": "limit", + # "unfilled_amount": "0.0001", + # "updated_at": 1714188449497 + # }, + # "message": "" + # }, + # ], + # "message": "OK" + # } + # + else: + request['market_type'] = 'FUTURES' + if trigger: + response = self.v2PrivatePostFuturesCancelBatchStopOrder(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "code": 0, + # "data": { + # "amount": "0.0001", + # "client_id": "x-167673045-a7d7714c6478acf6", + # "created_at": 1714187923820, + # "market": "BTCUSDT", + # "market_type": "FUTURES", + # "price": "61000", + # "side": "buy", + # "stop_id": 136984426097, + # "trigger_direction": "higher", + # "trigger_price": "62000", + # "trigger_price_type": "latest_price", + # "type": "limit", + # "updated_at": 1714187974363 + # }, + # "message": "" + # }, + # ], + # "message": "OK" + # } + # + else: + response = self.v2PrivatePostFuturesCancelBatchOrder(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "code": 0, + # "data": { + # "amount": "0.0001", + # "client_id": "x-167673045-9f80fde284339a72", + # "created_at": 1714187491784, + # "fee": "0", + # "fee_ccy": "USDT", + # "filled_amount": "0", + # "filled_value": "0", + # "last_filled_amount": "0", + # "last_filled_price": "0", + # "maker_fee_rate": "0.0003", + # "market": "BTCUSDT", + # "market_type": "FUTURES", + # "order_id": 136983851788, + # "price": "61000", + # "realized_pnl": "0", + # "side": "buy", + # "taker_fee_rate": "0.0005", + # "type": "limit", + # "unfilled_amount": "0.0001", + # "updated_at": 1714187567079 + # }, + # "message": "" + # }, + # ], + # "message": "OK" + # } + # + data = self.safe_list(response, 'data', []) + results = [] + for i in range(0, len(data)): + entry = data[i] + item = self.safe_dict(entry, 'data', {}) + order = self.parse_order(item, market) + results.append(order) + return results + + def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + """ + edit a trade order + + https://docs.coinex.com/api/v2/spot/order/http/edit-order + https://docs.coinex.com/api/v2/spot/order/http/edit-stop-order + https://docs.coinex.com/api/v2/futures/order/http/edit-order + https://docs.coinex.com/api/v2/futures/order/http/edit-stop-order + + :param str id: order id + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of the currency you want to trade in units of the base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: the price to trigger stop orders + :returns dict: an `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' editOrder() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + if amount is not None: + request['amount'] = self.amount_to_precision(symbol, amount) + if price is not None: + request['price'] = self.price_to_precision(symbol, price) + response = None + triggerPrice = self.safe_string_n(params, ['stopPrice', 'triggerPrice', 'trigger_price']) + params = self.omit(params, ['stopPrice', 'triggerPrice']) + isTriggerOrder = triggerPrice is not None + if isTriggerOrder: + request['trigger_price'] = self.price_to_precision(symbol, triggerPrice) + request['stop_id'] = self.parse_to_numeric(id) + else: + request['order_id'] = self.parse_to_numeric(id) + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('editOrder', params) + if market['spot']: + if marginMode is not None: + request['market_type'] = 'MARGIN' + else: + request['market_type'] = 'SPOT' + if isTriggerOrder: + response = self.v2PrivatePostSpotModifyStopOrder(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "stop_id": 117337235167 + # }, + # "message": "OK" + # } + # + else: + response = self.v2PrivatePostSpotModifyOrder(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "amount": "0.0001", + # "base_fee": "0", + # "ccy": "BTC", + # "client_id": "x-167673045-87eb2bebf42882d8", + # "created_at": 1714290302047, + # "discount_fee": "0", + # "filled_amount": "0", + # "filled_value": "0", + # "last_fill_amount": "0", + # "last_fill_price": "0", + # "maker_fee_rate": "0.002", + # "market": "BTCUSDT", + # "market_type": "SPOT", + # "order_id": 117336922195, + # "price": "61000", + # "quote_fee": "0", + # "side": "buy", + # "status": "open", + # "taker_fee_rate": "0.002", + # "type": "limit", + # "unfilled_amount": "0.0001", + # "updated_at": 1714290191141 + # }, + # "message": "OK" + # } + # + else: + request['market_type'] = 'FUTURES' + if isTriggerOrder: + response = self.v2PrivatePostFuturesModifyStopOrder(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "stop_id": 137091875605 + # }, + # "message": "OK" + # } + # + else: + response = self.v2PrivatePostFuturesModifyOrder(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "amount": "0.0001", + # "client_id": "x-167673045-3f2d09191462b207", + # "created_at": 1714290927630, + # "fee": "0", + # "fee_ccy": "USDT", + # "filled_amount": "0", + # "filled_value": "0", + # "last_filled_amount": "0", + # "last_filled_price": "0", + # "maker_fee_rate": "0.0003", + # "market": "BTCUSDT", + # "market_type": "FUTURES", + # "order_id": 137091566717, + # "price": "61000", + # "realized_pnl": "0", + # "side": "buy", + # "taker_fee_rate": "0.0005", + # "type": "limit", + # "unfilled_amount": "0.0001", + # "updated_at": 1714290927630 + # }, + # "message": "OK" + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data, market) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://docs.coinex.com/api/v2/spot/order/http/cancel-order + https://docs.coinex.com/api/v2/spot/order/http/cancel-stop-order + https://docs.coinex.com/api/v2/spot/order/http/cancel-order-by-client-id + https://docs.coinex.com/api/v2/spot/order/http/cancel-stop-order-by-client-id + https://docs.coinex.com/api/v2/futures/order/http/cancel-order + https://docs.coinex.com/api/v2/futures/order/http/cancel-stop-order + https://docs.coinex.com/api/v2/futures/order/http/cancel-order-by-client-id + https://docs.coinex.com/api/v2/futures/order/http/cancel-stop-order-by-client-id + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.clientOrderId]: client order id, defaults to id if not passed + :param boolean [params.trigger]: set to True for canceling a trigger order + :returns dict: an `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + isTriggerOrder = self.safe_bool_2(params, 'stop', 'trigger') + swap = market['swap'] + request: dict = { + 'market': market['id'], + } + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('cancelOrder', params) + if swap: + request['market_type'] = 'FUTURES' + else: + if marginMode is not None: + request['market_type'] = 'MARGIN' + else: + request['market_type'] = 'SPOT' + clientOrderId = self.safe_string_2(params, 'client_id', 'clientOrderId') + params = self.omit(params, ['stop', 'trigger', 'clientOrderId']) + response = None + if clientOrderId is not None: + request['client_id'] = clientOrderId + if isTriggerOrder: + if swap: + response = self.v2PrivatePostFuturesCancelStopOrderByClientId(self.extend(request, params)) + # { + # "code": 0, + # "data": [ + # { + # "code": 0, + # "data": { + # "amount": "0.0001", + # "client_id": "client01", + # "created_at": 1714368624473, + # "market": "BTCUSDT", + # "market_type": "FUTURES", + # "price": "61000", + # "side": "buy", + # "stop_id": 137175823891, + # "trigger_direction": "higher", + # "trigger_price": "61500", + # "trigger_price_type": "latest_price", + # "type": "limit", + # "updated_at": 1714368661444 + # }, + # "message": "" + # } + # ], + # "message": "OK" + # } + else: + response = self.v2PrivatePostSpotCancelStopOrderByClientId(self.extend(request, params)) + # { + # "code" :0, + # "data": [ + # { + # "code": 0, + # "data": { + # "amount": "0.0001", + # "ccy": "BTC", + # "client_id": "client01", + # "created_at": 1714366950279, + # "market": "BTCUSDT", + # "market_type": "SPOT", + # "price": "61000", + # "side": "buy", + # "stop_id": 117402512706, + # "trigger_direction": "higher", + # "trigger_price": "61500", + # "trigger_price_type": "mark_price", + # "type": "limit", + # "updated_at": 1714366950279 + # }, + # "message": "OK" + # } + # ], + # "message": "OK" + # } + else: + if swap: + response = self.v2PrivatePostFuturesCancelOrderByClientId(self.extend(request, params)) + # { + # "code": 0, + # "data": [ + # { + # "code": 0, + # "data": { + # "amount": "0.0001", + # "client_id": "x-167673045-bf60e24bb437a3df", + # "created_at": 1714368416437, + # "fee": "0", + # "fee_ccy": "USDT", + # "filled_amount": "0", + # "filled_value": "0", + # "last_filled_amount": "0", + # "last_filled_price": "0", + # "maker_fee_rate": "0.0003", + # "market": "BTCUSDT", + # "market_type": "FUTURES", + # "order_id": 137175616437, + # "price": "61000", + # "realized_pnl": "0", + # "side": "buy", + # "taker_fee_rate": "0.0005", + # "type": "limit", + # "unfilled_amount": "0.0001", + # "updated_at": 1714368507174 + # }, + # "message": "" + # } + # ], + # "message": "OK" + # } + else: + response = self.v2PrivatePostSpotCancelOrderByClientId(self.extend(request, params)) + # { + # "code": 0, + # "data": [ + # { + # "code": 0, + # "data": { + # "amount": "0.0001", + # "base_fee": "0", + # "ccy": "BTC", + # "client_id": "x-167673045-d49eaca5f412afc8", + # "created_at": 1714366502807, + # "discount_fee": "0", + # "filled_amount": "0", + # "filled_value": "0", + # "last_fill_amount": "0", + # "last_fill_price": "0", + # "maker_fee_rate": "0.002", + # "market": "BTCUSDT", + # "market_type": "SPOT", + # "order_id": 117402157490, + # "price": "61000", + # "quote_fee": "0", + # "side": "buy", + # "taker_fee_rate": "0.002", + # "type": "limit", + # "unfilled_amount": "0.0001", + # "updated_at": 1714366502807 + # }, + # "message": "OK" + # } + # ], + # "message": "OK" + # } + else: + if isTriggerOrder: + request['stop_id'] = self.parse_to_numeric(id) + if swap: + response = self.v2PrivatePostFuturesCancelStopOrder(self.extend(request, params)) + # { + # "code": 0, + # "data": { + # "amount": "0.0001", + # "ccy": "BTC", + # "client_id": "x-167673045-f21ecfd7542abf1f", + # "created_at": 1714366177334, + # "market": "BTCUSDT", + # "market_type": "SPOT", + # "price": "61000", + # "side": "buy", + # "stop_id": 117401897954, + # "trigger_direction": "higher", + # "trigger_price": "61500", + # "trigger_price_type": "mark_price", + # "type": "limit", + # "updated_at": 1714366177334 + # }, + # "message": "OK" + # } + else: + response = self.v2PrivatePostSpotCancelStopOrder(self.extend(request, params)) + # { + # "code": 0, + # "data": { + # "amount": "0.0001", + # "ccy": "BTC", + # "client_id": "x-167673045-f21ecfd7542abf1f", + # "created_at": 1714366177334, + # "market": "BTCUSDT", + # "market_type": "SPOT", + # "price": "61000", + # "side": "buy", + # "stop_id": 117401897954, + # "trigger_direction": "higher", + # "trigger_price": "61500", + # "trigger_price_type": "mark_price", + # "type": "limit", + # "updated_at": 1714366177334 + # }, + # "message": "OK" + # } + else: + request['order_id'] = self.parse_to_numeric(id) + if swap: + response = self.v2PrivatePostFuturesCancelOrder(self.extend(request, params)) + # { + # "code": 0, + # "data": { + # "amount": "0.0001", + # "client_id": "x-167673045-7f14381c74a98a85", + # "created_at": 1714367342024, + # "fee": "0", + # "fee_ccy": "USDT", + # "filled_amount": "0", + # "filled_value": "0", + # "last_filled_amount": "0", + # "last_filled_price": "0", + # "maker_fee_rate": "0.0003", + # "market": "BTCUSDT", + # "market_type": "FUTURES", + # "order_id": 137174472136, + # "price": "61000", + # "realized_pnl": "0", + # "side": "buy", + # "taker_fee_rate": "0.0005", + # "type": "limit", + # "unfilled_amount": "0.0001", + # "updated_at": 1714367515978 + # }, + # "message": "OK" + # } + else: + response = self.v2PrivatePostSpotCancelOrder(self.extend(request, params)) + # { + # "code": 0, + # "data": { + # "amount": "0.0001", + # "base_fee": "0", + # "ccy": "BTC", + # "client_id": "x-167673045-86fbe37b54a2aea3", + # "created_at": 1714365277437, + # "discount_fee": "0", + # "filled_amount": "0", + # "filled_value": "0", + # "last_fill_amount": "0", + # "last_fill_price": "0", + # "maker_fee_rate": "0.002", + # "market": "BTCUSDT", + # "market_type": "SPOT", + # "order_id": 117401168172, + # "price": "61000", + # "quote_fee": "0", + # "side": "buy", + # "taker_fee_rate": "0.002", + # "type": "limit", + # "unfilled_amount": "0.0001", + # "updated_at": 1714365277437 + # }, + # "message": "OK" + # } + data = None + if clientOrderId is not None: + rows = self.safe_list(response, 'data', []) + data = self.safe_dict(rows[0], 'data', {}) + else: + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data, market) + + def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders in a market + + https://docs.coinex.com/api/v2/spot/order/http/cancel-all-order + https://docs.coinex.com/api/v2/futures/order/http/cancel-all-order + + :param str symbol: unified market symbol of the market to cancel orders in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' for canceling spot margin orders + :returns dict[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelAllOrders() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + response = None + if market['swap']: + request['market_type'] = 'FUTURES' + response = self.v2PrivatePostFuturesCancelAllOrder(self.extend(request, params)) + # + # {"code":0,"data":{},"message":"OK"} + # + else: + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('cancelAllOrders', params) + if marginMode is not None: + request['market_type'] = 'MARGIN' + else: + request['market_type'] = 'SPOT' + response = self.v2PrivatePostSpotCancelAllOrder(self.extend(request, params)) + # + # {"code":0,"data":{},"message":"OK"} + # + return [ + self.safe_order({ + 'info': response, + }), + ] + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://docs.coinex.com/api/v2/spot/order/http/get-order-status + https://docs.coinex.com/api/v2/futures/order/http/get-order-status + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrder() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + 'order_id': self.parse_to_numeric(id), + } + response = None + if market['swap']: + response = self.v2PrivateGetFuturesOrderStatus(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "amount": "0.0001", + # "client_id": "x-167673045-da5f31dcd478a829", + # "created_at": 1714460987164, + # "fee": "0", + # "fee_ccy": "USDT", + # "filled_amount": "0", + # "filled_value": "0", + # "last_filled_amount": "0", + # "last_filled_price": "0", + # "maker_fee_rate": "0.0003", + # "market": "BTCUSDT", + # "market_type": "FUTURES", + # "order_id": 137319868771, + # "price": "61000", + # "realized_pnl": "0", + # "side": "buy", + # "status": "open", + # "taker_fee_rate": "0.0005", + # "type": "limit", + # "unfilled_amount": "0.0001", + # "updated_at": 1714460987164 + # }, + # "message": "OK" + # } + # + else: + response = self.v2PrivateGetSpotOrderStatus(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "amount": "0.0001", + # "base_fee": "0", + # "ccy": "BTC", + # "client_id": "x-167673045-da918d6724e3af81", + # "created_at": 1714461638958, + # "discount_fee": "0", + # "filled_amount": "0", + # "filled_value": "0", + # "last_fill_amount": "0", + # "last_fill_price": "0", + # "maker_fee_rate": "0.002", + # "market": "BTCUSDT", + # "market_type": "SPOT", + # "order_id": 117492012985, + # "price": "61000", + # "quote_fee": "0", + # "side": "buy", + # "status": "open", + # "taker_fee_rate": "0.002", + # "type": "limit", + # "unfilled_amount": "0.0001", + # "updated_at": 1714461638958 + # }, + # "message": "OK" + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data, market) + + def fetch_orders_by_status(self, status, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch a list of orders + + https://docs.coinex.com/api/v2/spot/order/http/list-finished-order + https://docs.coinex.com/api/v2/spot/order/http/list-finished-stop-order + https://docs.coinex.com/api/v2/futures/order/http/list-finished-order + https://docs.coinex.com/api/v2/futures/order/http/list-finished-stop-order + + :param str status: order status to fetch for + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: set to True for fetching trigger orders + :param str [params.marginMode]: 'cross' or 'isolated' for fetching spot margin orders + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['market'] = market['id'] + if limit is not None: + request['limit'] = limit + trigger = self.safe_bool_2(params, 'stop', 'trigger') + params = self.omit(params, ['stop', 'trigger']) + marketType = None + marketType, params = self.handle_market_type_and_params('fetchOrdersByStatus', market, params) + response = None + isClosed = (status == 'finished') or (status == 'closed') + isOpen = (status == 'pending') or (status == 'open') + if marketType == 'swap': + request['market_type'] = 'FUTURES' + if isClosed: + if trigger: + response = self.v2PrivateGetFuturesFinishedStopOrder(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "stop_id": 52431158859, + # "market": "BTCUSDT", + # "market_type": "FUTURES", + # "side": "sell", + # "type": "market", + # "amount": "0.0005", + # "price": "20599.64", + # "client_id": "", + # "created_at": 1667547909856, + # "updated_at": 1667547909856, + # "trigger_price": "20599.64", + # "trigger_price_type": "latest_price", + # "trigger_direction": "" + # }, + # ], + # "message": "OK", + # "pagination": { + # "has_next": False + # } + # } + # + else: + response = self.v2PrivateGetFuturesFinishedOrder(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "order_id": 136915813578, + # "market": "BTCUSDT", + # "market_type": "FUTURES", + # "side": "sell", + # "type": "market", + # "amount": "0.0001", + # "price": "0", + # "client_id": "x-167673045-4f264600c432ac06", + # "created_at": 1714119323764, + # "updated_at": 1714119323764, + # "unfilled_amount": "0", + # "filled_amount": "0.0001", + # "filled_value": "6.442017", + # "fee": "0.003221", + # "fee_ccy": "USDT", + # "maker_fee_rate": "0", + # "taker_fee_rate": "0.0005" + # }, + # ], + # "message": "OK", + # "pagination": { + # "has_next": False + # } + # } + # + elif isOpen: + if trigger: + response = self.v2PrivateGetFuturesPendingStopOrder(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "stop_id": 137481469849, + # "market": "BTCUSDT", + # "market_type": "FUTURES", + # "side": "buy", + # "type": "limit", + # "amount": "0.0001", + # "price": "51000", + # "client_id": "x-167673045-2b932341949fa2a1", + # "created_at": 1714552257876, + # "updated_at": 1714552257876, + # "trigger_price": "52000", + # "trigger_price_type": "latest_price", + # "trigger_direction": "higher" + # } + # ], + # "message": "OK", + # "pagination": { + # "total": 1, + # "has_next": False + # } + # } + # + else: + response = self.v2PrivateGetFuturesPendingOrder(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "order_id": 137480580906, + # "market": "BTCUSDT", + # "market_type": "FUTURES", + # "side": "buy", + # "type": "limit", + # "amount": "0.0001", + # "price": "51000", + # "client_id": "", + # "created_at": 1714551877569, + # "updated_at": 1714551877569, + # "unfilled_amount": "0.0001", + # "filled_amount": "0", + # "filled_value": "0", + # "fee": "0", + # "fee_ccy": "USDT", + # "maker_fee_rate": "0.0003", + # "taker_fee_rate": "0.0005", + # "last_filled_amount": "0", + # "last_filled_price": "0", + # "realized_pnl": "0" + # } + # ], + # "message": "OK", + # "pagination": { + # "total": 1, + # "has_next": False + # } + # } + # + else: + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchOrdersByStatus', params) + if marginMode is not None: + request['market_type'] = 'MARGIN' + else: + request['market_type'] = 'SPOT' + if isClosed: + if trigger: + response = self.v2PrivateGetSpotFinishedStopOrder(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "stop_id": 117654881420, + # "market": "BTCUSDT", + # "market_type": "SPOT", + # "ccy": "USDT", + # "side": "buy", + # "type": "market", + # "amount": "5.83325524", + # "price": "0", + # "trigger_price": "57418", + # "trigger_direction": "lower", + # "trigger_price_type": "mark_price", + # "client_id": "", + # "created_at": 1714618050597, + # "updated_at": 0 + # } + # ], + # "message": "OK", + # "pagination": { + # "has_next": False + # } + # } + # + else: + response = self.v2PrivateGetSpotFinishedOrder(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "order_id": 117180532345, + # "market": "BTCUSDT", + # "market_type": "SPOT", + # "side": "sell", + # "type": "market", + # "ccy": "BTC", + # "amount": "0.00015484", + # "price": "0", + # "client_id": "", + # "created_at": 1714116494219, + # "updated_at": 0, + # "base_fee": "0", + # "quote_fee": "0.0199931699632", + # "discount_fee": "0", + # "maker_fee_rate": "0", + # "taker_fee_rate": "0.002", + # "unfilled_amount": "0", + # "filled_amount": "0.00015484", + # "filled_value": "9.9965849816" + # }, + # ], + # "message": "OK", + # "pagination": { + # "has_next": False + # } + # } + # + elif status == 'pending': + if trigger: + response = self.v2PrivateGetSpotPendingStopOrder(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "stop_id": 117586439530, + # "market": "BTCUSDT", + # "market_type": "SPOT", + # "ccy": "BTC", + # "side": "buy", + # "type": "limit", + # "amount": "0.0001", + # "price": "51000", + # "trigger_price": "52000", + # "trigger_direction": "higher", + # "trigger_price_type": "mark_price", + # "client_id": "x-167673045-df61777094c69312", + # "created_at": 1714551237335, + # "updated_at": 1714551237335 + # } + # ], + # "message": "OK", + # "pagination": { + # "total": 1, + # "has_next": False + # } + # } + # + else: + response = self.v2PrivateGetSpotPendingOrder(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "order_id": 117585921297, + # "market": "BTCUSDT", + # "market_type": "SPOT", + # "side": "buy", + # "type": "limit", + # "ccy": "BTC", + # "amount": "0.00011793", + # "price": "52000", + # "client_id": "", + # "created_at": 1714550707486, + # "updated_at": 1714550707486, + # "base_fee": "0", + # "quote_fee": "0", + # "discount_fee": "0", + # "maker_fee_rate": "0.002", + # "taker_fee_rate": "0.002", + # "last_fill_amount": "0", + # "last_fill_price": "0", + # "unfilled_amount": "0.00011793", + # "filled_amount": "0", + # "filled_value": "0" + # } + # ], + # "message": "OK", + # "pagination": { + # "total": 1, + # "has_next": False + # } + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_orders(data, market, since, limit) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://docs.coinex.com/api/v2/spot/order/http/list-pending-order + https://docs.coinex.com/api/v2/spot/order/http/list-pending-stop-order + https://docs.coinex.com/api/v2/futures/order/http/list-pending-order + https://docs.coinex.com/api/v2/futures/order/http/list-pending-stop-order + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: set to True for fetching trigger orders + :param str [params.marginMode]: 'cross' or 'isolated' for fetching spot margin orders + :returns Order[]: a list of `order structures ` + """ + openOrders = self.fetch_orders_by_status('pending', symbol, since, limit, params) + for i in range(0, len(openOrders)): + openOrders[i]['status'] = 'open' + return openOrders + + def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://docs.coinex.com/api/v2/spot/order/http/list-finished-order + https://docs.coinex.com/api/v2/spot/order/http/list-finished-stop-order + https://docs.coinex.com/api/v2/futures/order/http/list-finished-order + https://docs.coinex.com/api/v2/futures/order/http/list-finished-stop-order + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: set to True for fetching trigger orders + :param str [params.marginMode]: 'cross' or 'isolated' for fetching spot margin orders + :returns Order[]: a list of `order structures ` + """ + return self.fetch_orders_by_status('finished', symbol, since, limit, params) + + def create_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + create a currency deposit address + + https://docs.coinex.com/api/v2/assets/deposit-withdrawal/http/update-deposit-address + + :param str code: unified currency code of the currency for the deposit address + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.network]: the blockchain network to create a deposit address on + :returns dict: an `address structure ` + """ + self.load_markets() + currency = self.currency(code) + network = self.safe_string_2(params, 'chain', 'network') + if network is None: + raise ArgumentsRequired(self.id + ' createDepositAddress() requires a network parameter') + params = self.omit(params, 'network') + request: dict = { + 'ccy': currency['id'], + 'chain': self.network_code_to_id(network, currency['code']), + } + response = self.v2PrivatePostAssetsRenewalDepositAddress(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "address": "0x321bd6479355142334f45653ad5d8b76105a1234", + # "memo": "" + # }, + # "message": "OK" + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_deposit_address(data, currency) + + def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://docs.coinex.com/api/v2/assets/deposit-withdrawal/http/get-deposit-address + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.network]: the blockchain network to create a deposit address on + :returns dict: an `address structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'ccy': currency['id'], + } + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + if networkCode is None: + raise ArgumentsRequired(self.id + ' fetchDepositAddress() requires a "network" parameter') + request['chain'] = self.network_code_to_id(networkCode) # required for on-chain, not required for inter-user transfer + response = self.v2PrivateGetAssetsDepositAddress(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "address": "0x321bd6479355142334f45653ad5d8b76105a1234", + # "memo": "" + # }, + # "message": "OK" + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_deposit_address(data, currency) + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + # + # { + # "address": "1P1JqozxioQwaqPwgMAQdNDYNyaVSqgARq", + # "memo": "" + # } + # + coinAddress = self.safe_string(depositAddress, 'address') + parts = coinAddress.split(':') + address = None + tag = None + partsLength = len(parts) + if partsLength > 1 and parts[0] != 'cfx': + address = parts[0] + tag = parts[1] + else: + address = coinAddress + return { + 'info': depositAddress, + 'currency': self.safe_currency_code(None, currency), + 'network': None, + 'address': address, + 'tag': self.safe_string(depositAddress, 'memo', tag), + } + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://docs.coinex.com/api/v2/spot/deal/http/list-user-deals + https://docs.coinex.com/api/v2/futures/deal/http/list-user-deals + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trade structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest trades + :param str [params.side]: the side of the trades, either 'buy' or 'sell', required for swap + :returns Trade[]: a list of `trade structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMyTrades() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + if limit is not None: + request['limit'] = limit + if since is not None: + request['start_time'] = since + request, params = self.handle_until_option('end_time', request, params) + response = None + if market['swap']: + request['market_type'] = 'FUTURES' + response = self.v2PrivateGetFuturesUserDeals(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "deal_id": 1180222387, + # "created_at": 1714119054558, + # "market": "BTCUSDT", + # "side": "buy", + # "order_id": 136915589622, + # "price": "64376", + # "amount": "0.0001" + # } + # ], + # "message": "OK", + # "pagination": { + # "has_next": True + # } + # } + # + else: + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchMyTrades', params) + if marginMode is not None: + request['market_type'] = 'MARGIN' + else: + request['market_type'] = 'SPOT' + response = self.v2PrivateGetSpotUserDeals(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "amount": "0.00010087", + # "created_at": 1714618087585, + # "deal_id": 4161200602, + # "margin_market": "", + # "market": "BTCUSDT", + # "order_id": 117654919342, + # "price": "57464.04", + # "side": "sell" + # } + # ], + # "message": "OK", + # "pagination": { + # "has_next": True + # } + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_trades(data, market, since, limit) + + def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://docs.coinex.com/api/v2/futures/position/http/list-pending-position + https://docs.coinex.com/api/v2/futures/position/http/list-finished-position + + :param str[] [symbols]: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.method]: the method to use 'v2PrivateGetFuturesPendingPosition' or 'v2PrivateGetFuturesFinishedPosition' default is 'v2PrivateGetFuturesPendingPosition' + :returns dict[]: a list of `position structure ` + """ + self.load_markets() + defaultMethod = None + defaultMethod, params = self.handle_option_and_params(params, 'fetchPositions', 'method', 'v2PrivateGetFuturesPendingPosition') + symbols = self.market_symbols(symbols) + request: dict = { + 'market_type': 'FUTURES', + } + market = None + if symbols is not None: + symbol = None + if isinstance(symbols, list): + symbolsLength = len(symbols) + if symbolsLength > 1: + raise BadRequest(self.id + ' fetchPositions() symbols argument cannot contain more than 1 symbol') + symbol = symbols[0] + else: + symbol = symbols + market = self.market(symbol) + request['market'] = market['id'] + response = None + if defaultMethod == 'v2PrivateGetFuturesPendingPosition': + response = self.v2PrivateGetFuturesPendingPosition(self.extend(request, params)) + else: + response = self.v2PrivateGetFuturesFinishedPosition(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "position_id": 305891033, + # "market": "BTCUSDT", + # "market_type": "FUTURES", + # "side": "long", + # "margin_mode": "cross", + # "open_interest": "0.0001", + # "close_avbl": "0.0001", + # "ath_position_amount": "0.0001", + # "unrealized_pnl": "0", + # "realized_pnl": "-0.00311684", + # "avg_entry_price": "62336.8", + # "cml_position_value": "6.23368", + # "max_position_value": "6.23368", + # "created_at": 1715152208041, + # "updated_at": 1715152208041, + # "take_profit_price": "0", + # "stop_loss_price": "0", + # "take_profit_type": "", + # "stop_loss_type": "", + # "settle_price": "62336.8", + # "settle_value": "6.23368", + # "leverage": "3", + # "margin_avbl": "2.07789333", + # "ath_margin_size": "2.07789333", + # "position_margin_rate": "2.40545879023305655728", + # "maintenance_margin_rate": "0.005", + # "maintenance_margin_value": "0.03118094", + # "liq_price": "0", + # "bkr_price": "0", + # "adl_level": 1 + # } + # ], + # "message": "OK", + # "pagination": { + # "has_next": False + # } + # } + # + position = self.safe_list(response, 'data', []) + result = [] + for i in range(0, len(position)): + result.append(self.parse_position(position[i], market)) + return self.filter_by_array_positions(result, 'symbol', symbols, False) + + def fetch_position(self, symbol: str, params={}): + """ + fetch data on a single open contract trade position + + https://docs.coinex.com/api/v2/futures/position/http/list-pending-position + + :param str symbol: unified market symbol of the market the position is held in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `position structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'market_type': 'FUTURES', + 'market': market['id'], + } + response = self.v2PrivateGetFuturesPendingPosition(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "position_id": 305891033, + # "market": "BTCUSDT", + # "market_type": "FUTURES", + # "side": "long", + # "margin_mode": "cross", + # "open_interest": "0.0001", + # "close_avbl": "0.0001", + # "ath_position_amount": "0.0001", + # "unrealized_pnl": "0", + # "realized_pnl": "-0.00311684", + # "avg_entry_price": "62336.8", + # "cml_position_value": "6.23368", + # "max_position_value": "6.23368", + # "created_at": 1715152208041, + # "updated_at": 1715152208041, + # "take_profit_price": "0", + # "stop_loss_price": "0", + # "take_profit_type": "", + # "stop_loss_type": "", + # "settle_price": "62336.8", + # "settle_value": "6.23368", + # "leverage": "3", + # "margin_avbl": "2.07789333", + # "ath_margin_size": "2.07789333", + # "position_margin_rate": "2.40545879023305655728", + # "maintenance_margin_rate": "0.005", + # "maintenance_margin_value": "0.03118094", + # "liq_price": "0", + # "bkr_price": "0", + # "adl_level": 1 + # } + # ], + # "message": "OK", + # "pagination": { + # "has_next": False + # } + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_position(data[0], market) + + def parse_position(self, position: dict, market: Market = None): + # + # { + # "position_id": 305891033, + # "market": "BTCUSDT", + # "market_type": "FUTURES", + # "side": "long", + # "margin_mode": "cross", + # "open_interest": "0.0001", + # "close_avbl": "0.0001", + # "ath_position_amount": "0.0001", + # "unrealized_pnl": "0", + # "realized_pnl": "-0.00311684", + # "avg_entry_price": "62336.8", + # "cml_position_value": "6.23368", + # "max_position_value": "6.23368", + # "created_at": 1715152208041, + # "updated_at": 1715152208041, + # "take_profit_price": "0", + # "stop_loss_price": "0", + # "take_profit_type": "", + # "stop_loss_type": "", + # "settle_price": "62336.8", + # "settle_value": "6.23368", + # "leverage": "3", + # "margin_avbl": "2.07789333", + # "ath_margin_size": "2.07789333", + # "position_margin_rate": "2.40545879023305655728", + # "maintenance_margin_rate": "0.005", + # "maintenance_margin_value": "0.03118094", + # "liq_price": "0", + # "bkr_price": "0", + # "adl_level": 1 + # } + # + marketId = self.safe_string(position, 'market') + market = self.safe_market(marketId, market, None, 'swap') + timestamp = self.safe_integer(position, 'created_at') + return self.safe_position({ + 'info': position, + 'id': self.safe_integer(position, 'position_id'), + 'symbol': market['symbol'], + 'notional': self.safe_number(position, 'settle_value'), + 'marginMode': self.safe_string(position, 'margin_mode'), + 'liquidationPrice': self.safe_number(position, 'liq_price'), + 'entryPrice': self.safe_number(position, 'avg_entry_price'), + 'unrealizedPnl': self.safe_number(position, 'unrealized_pnl'), + 'realizedPnl': self.safe_number(position, 'realized_pnl'), + 'percentage': None, + 'contracts': self.safe_number(position, 'close_avbl'), + 'contractSize': self.safe_number(market, 'contractSize'), + 'markPrice': None, + 'lastPrice': None, + 'side': self.safe_string(position, 'side'), + 'hedged': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastUpdateTimestamp': self.safe_integer(position, 'updated_at'), + 'maintenanceMargin': self.safe_number(position, 'maintenance_margin_value'), + 'maintenanceMarginPercentage': self.safe_number(position, 'maintenance_margin_rate'), + 'collateral': self.safe_number(position, 'margin_avbl'), + 'initialMargin': None, + 'initialMarginPercentage': None, + 'leverage': self.safe_number(position, 'leverage'), + 'marginRatio': self.safe_number(position, 'position_margin_rate'), + 'stopLossPrice': self.omit_zero(self.safe_string(position, 'stop_loss_price')), + 'takeProfitPrice': self.omit_zero(self.safe_string(position, 'take_profit_price')), + }) + + def set_margin_mode(self, marginMode: str, symbol: Str = None, params={}): + """ + set margin mode to 'cross' or 'isolated' + + https://docs.coinex.com/api/v2/futures/position/http/adjust-position-leverage + + :param str marginMode: 'cross' or 'isolated' + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int params['leverage']: the rate of leverage + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setMarginMode() requires a symbol argument') + marginMode = marginMode.lower() + if marginMode != 'isolated' and marginMode != 'cross': + raise BadRequest(self.id + ' setMarginMode() marginMode argument should be isolated or cross') + self.load_markets() + market = self.market(symbol) + if market['type'] != 'swap': + raise BadSymbol(self.id + ' setMarginMode() supports swap contracts only') + leverage = self.safe_integer(params, 'leverage') + maxLeverage = self.safe_integer(market['limits']['leverage'], 'max', 100) + if leverage is None: + raise ArgumentsRequired(self.id + ' setMarginMode() requires a leverage parameter') + if (leverage < 1) or (leverage > maxLeverage): + raise BadRequest(self.id + ' setMarginMode() leverage should be between 1 and ' + str(maxLeverage) + ' for ' + symbol) + request: dict = { + 'market': market['id'], + 'market_type': 'FUTURES', + 'margin_mode': marginMode, + 'leverage': leverage, + } + return self.v2PrivatePostFuturesAdjustPositionLeverage(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "leverage": 1, + # "margin_mode": "isolated" + # }, + # "message": "OK" + # } + # + + def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + + https://docs.coinex.com/api/v2/futures/position/http/adjust-position-leverage + + set the level of leverage for a market + :param float leverage: the rate of leverage + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated'(default is 'cross') + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setLeverage() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + if not market['swap']: + raise BadSymbol(self.id + ' setLeverage() supports swap contracts only') + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('setLeverage', params, 'cross') + minLeverage = self.safe_integer(market['limits']['leverage'], 'min', 1) + maxLeverage = self.safe_integer(market['limits']['leverage'], 'max', 100) + if (leverage < minLeverage) or (leverage > maxLeverage): + raise BadRequest(self.id + ' setLeverage() leverage should be between ' + str(minLeverage) + ' and ' + str(maxLeverage) + ' for ' + symbol) + request: dict = { + 'market': market['id'], + 'market_type': 'FUTURES', + 'margin_mode': marginMode, + 'leverage': leverage, + } + return self.v2PrivatePostFuturesAdjustPositionLeverage(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "leverage": 1, + # "margin_mode": "isolated" + # }, + # "message": "OK" + # } + # + + def fetch_leverage_tiers(self, symbols: Strings = None, params={}) -> LeverageTiers: + """ + retrieve information on the maximum leverage, and maintenance margin for trades of varying trade sizes + + https://docs.coinex.com/api/v2/futures/market/http/list-market-position-level + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `leverage tiers structures `, indexed by market symbols + """ + self.load_markets() + request: dict = {} + if symbols is not None: + marketIds = self.market_ids(symbols) + request['market'] = ','.join(marketIds) + response = self.v2PublicGetFuturesPositionLevel(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "level": [ + # { + # "amount": "20001", + # "leverage": "20", + # "maintenance_margin_rate": "0.02", + # "min_initial_margin_rate": "0.05" + # }, + # { + # "amount": "50001", + # "leverage": "10", + # "maintenance_margin_rate": "0.04", + # "min_initial_margin_rate": "0.1" + # }, + # ], + # "market": "MINAUSDT" + # }, + # ], + # "message": "OK" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_leverage_tiers(data, symbols, 'market') + + def parse_market_leverage_tiers(self, info, market: Market = None) -> List[LeverageTier]: + tiers = [] + brackets = self.safe_list(info, 'level', []) + minNotional = 0 + for i in range(0, len(brackets)): + tier = brackets[i] + marketId = self.safe_string(info, 'market') + market = self.safe_market(marketId, market, None, 'swap') + maxNotional = self.safe_number(tier, 'amount') + tiers.append({ + 'tier': self.sum(i, 1), + 'symbol': self.safe_symbol(marketId, market, None, 'swap'), + 'currency': market['base'] if market['linear'] else market['quote'], + 'minNotional': minNotional, + 'maxNotional': maxNotional, + 'maintenanceMarginRate': self.safe_number(tier, 'maintenance_margin_rate'), + 'maxLeverage': self.safe_integer(tier, 'leverage'), + 'info': tier, + }) + minNotional = maxNotional + return tiers + + def modify_margin_helper(self, symbol: str, amount, addOrReduce, params={}): + self.load_markets() + market = self.market(symbol) + rawAmount = self.amount_to_precision(symbol, amount) + requestAmount = rawAmount + if addOrReduce == 'reduce': + requestAmount = Precise.string_neg(rawAmount) + request: dict = { + 'market': market['id'], + 'market_type': 'FUTURES', + 'amount': requestAmount, + } + response = self.v2PrivatePostFuturesAdjustPositionMargin(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "adl_level": 1, + # "ath_margin_size": "2.034928", + # "ath_position_amount": "0.0001", + # "avg_entry_price": "61047.84", + # "bkr_price": "30698.5600000000000004142", + # "close_avbl": "0.0001", + # "cml_position_value": "6.104784", + # "created_at": 1715488472908, + # "leverage": "3", + # "liq_price": "30852.82412060301507579316", + # "maintenance_margin_rate": "0.005", + # "maintenance_margin_value": "0.03051465", + # "margin_avbl": "3.034928", + # "margin_mode": "isolated", + # "market": "BTCUSDT", + # "market_type": "FUTURES", + # "max_position_value": "6.104784", + # "open_interest": "0.0001", + # "position_id": 306458800, + # "position_margin_rate": "0.49713929272518077625", + # "realized_pnl": "-0.003052392", + # "settle_price": "61047.84", + # "settle_value": "6.104784", + # "side": "long", + # "stop_loss_price": "0", + # "stop_loss_type": "", + # "take_profit_price": "0", + # "take_profit_type": "", + # "unrealized_pnl": "0", + # "updated_at": 1715488805563 + # }, + # "message": "OK" + # } + # + data = self.safe_dict(response, 'data') + status = self.safe_string_lower(response, 'message') + type = 'reduce' if (addOrReduce == 'reduce') else 'add' + return self.extend(self.parse_margin_modification(data, market), { + 'type': type, + 'amount': self.parse_number(amount), + 'status': status, + }) + + def parse_margin_modification(self, data: dict, market: Market = None) -> MarginModification: + # + # addMargin/reduceMargin + # + # { + # "adl_level": 1, + # "ath_margin_size": "2.034928", + # "ath_position_amount": "0.0001", + # "avg_entry_price": "61047.84", + # "bkr_price": "30698.5600000000000004142", + # "close_avbl": "0.0001", + # "cml_position_value": "6.104784", + # "created_at": 1715488472908, + # "leverage": "3", + # "liq_price": "30852.82412060301507579316", + # "maintenance_margin_rate": "0.005", + # "maintenance_margin_value": "0.03051465", + # "margin_avbl": "3.034928", + # "margin_mode": "isolated", + # "market": "BTCUSDT", + # "market_type": "FUTURES", + # "max_position_value": "6.104784", + # "open_interest": "0.0001", + # "position_id": 306458800, + # "position_margin_rate": "0.49713929272518077625", + # "realized_pnl": "-0.003052392", + # "settle_price": "61047.84", + # "settle_value": "6.104784", + # "side": "long", + # "stop_loss_price": "0", + # "stop_loss_type": "", + # "take_profit_price": "0", + # "take_profit_type": "", + # "unrealized_pnl": "0", + # "updated_at": 1715488805563 + # } + # + # fetchMarginAdjustmentHistory + # + # { + # "bkr_pirce": "24698.56000000000000005224", + # "created_at": 1715489978697, + # "leverage": "3", + # "liq_price": "24822.67336683417085432386", + # "margin_avbl": "3.634928", + # "margin_change": "-1.5", + # "margin_mode": "isolated", + # "market": "BTCUSDT", + # "market_type": "FUTURES", + # "open_interest": "0.0001", + # "position_id": 306458800, + # "settle_price": "61047.84" + # } + # + marketId = self.safe_string(data, 'market') + timestamp = self.safe_integer_2(data, 'updated_at', 'created_at') + change = self.safe_string(data, 'margin_change') + return { + 'info': data, + 'symbol': self.safe_symbol(marketId, market, None, 'swap'), + 'type': None, + 'marginMode': 'isolated', + 'amount': self.parse_number(Precise.string_abs(change)), + 'total': self.safe_number(data, 'margin_avbl'), + 'code': market['quote'], + 'status': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + + def add_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + add margin + + https://docs.coinex.com/api/v2/futures/position/http/adjust-position-margin + + :param str symbol: unified market symbol + :param float amount: amount of margin to add + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin structure ` + """ + return self.modify_margin_helper(symbol, amount, 'add', params) + + def reduce_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + remove margin from a position + + https://docs.coinex.com/api/v2/futures/position/http/adjust-position-margin + + :param str symbol: unified market symbol + :param float amount: the amount of margin to remove + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin structure ` + """ + return self.modify_margin_helper(symbol, amount, 'reduce', params) + + def fetch_funding_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch the history of funding fee payments paid and received on self account + + https://docs.coinex.com/api/v2/futures/position/http/list-position-funding-history + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch funding history for + :param int [limit]: the maximum number of funding history structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding history structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchFundingHistory() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + 'market_type': 'FUTURES', + } + request, params = self.handle_until_option('end_time', request, params) + if since is not None: + request['start_time'] = since + if limit is not None: + request['limit'] = limit + response = self.v2PrivateGetFuturesPositionFundingHistory(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "ccy": "USDT", + # "created_at": 1715673620183, + # "funding_rate": "0", + # "funding_value": "0", + # "market": "BTCUSDT", + # "market_type": "FUTURES", + # "position_id": 306458800, + # "side": "long" + # }, + # ], + # "message": "OK", + # "pagination": { + # "has_next": True + # } + # } + # + data = self.safe_list(response, 'data', []) + result = [] + for i in range(0, len(data)): + entry = data[i] + timestamp = self.safe_integer(entry, 'created_at') + currencyId = self.safe_string(entry, 'ccy') + code = self.safe_currency_code(currencyId) + result.append({ + 'info': entry, + 'symbol': symbol, + 'code': code, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': self.safe_number(entry, 'position_id'), + 'amount': self.safe_number(entry, 'funding_value'), + }) + return result + + def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate + + https://docs.coinex.com/api/v2/futures/market/http/list-market-funding-rate + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + self.load_markets() + market = self.market(symbol) + if not market['swap']: + raise BadSymbol(self.id + ' fetchFundingRate() supports swap contracts only') + request: dict = { + 'market': market['id'], + } + response = self.v2PublicGetFuturesFundingRate(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "latest_funding_rate": "0", + # "latest_funding_time": 1715731200000, + # "mark_price": "61602.22", + # "market": "BTCUSDT", + # "max_funding_rate": "0.00375", + # "min_funding_rate": "-0.00375", + # "next_funding_rate": "0.00021074", + # "next_funding_time": 1715760000000 + # } + # ], + # "message": "OK" + # } + # + data = self.safe_list(response, 'data', []) + first = self.safe_dict(data, 0, {}) + return self.parse_funding_rate(first, market) + + def fetch_funding_interval(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate interval + + https://docs.coinex.com/api/v2/futures/market/http/list-market-funding-rate + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + return self.fetch_funding_rate(symbol, params) + + def parse_funding_rate(self, contract, market: Market = None) -> FundingRate: + # + # fetchFundingRate, fetchFundingRates, fetchFundingInterval + # + # { + # "latest_funding_rate": "0", + # "latest_funding_time": 1715731200000, + # "mark_price": "61602.22", + # "market": "BTCUSDT", + # "max_funding_rate": "0.00375", + # "min_funding_rate": "-0.00375", + # "next_funding_rate": "0.00021074", + # "next_funding_time": 1715760000000 + # } + # + currentFundingTimestamp = self.safe_integer(contract, 'latest_funding_time') + futureFundingTimestamp = self.safe_integer(contract, 'next_funding_time') + fundingTimeString = self.safe_string(contract, 'latest_funding_time') + nextFundingTimeString = self.safe_string(contract, 'next_funding_time') + millisecondsInterval = Precise.string_sub(nextFundingTimeString, fundingTimeString) + marketId = self.safe_string(contract, 'market') + return { + 'info': contract, + 'symbol': self.safe_symbol(marketId, market, None, 'swap'), + 'markPrice': self.safe_number(contract, 'mark_price'), + 'indexPrice': None, + 'interestRate': None, + 'estimatedSettlePrice': None, + 'timestamp': None, + 'datetime': None, + 'fundingRate': self.safe_number(contract, 'latest_funding_rate'), + 'fundingTimestamp': currentFundingTimestamp, + 'fundingDatetime': self.iso8601(currentFundingTimestamp), + 'nextFundingRate': self.safe_number(contract, 'next_funding_rate'), + 'nextFundingTimestamp': futureFundingTimestamp, + 'nextFundingDatetime': self.iso8601(futureFundingTimestamp), + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': self.parse_funding_interval(millisecondsInterval), + } + + def parse_funding_interval(self, interval): + intervals: dict = { + '3600000': '1h', + '14400000': '4h', + '28800000': '8h', + '57600000': '16h', + '86400000': '24h', + } + return self.safe_string(intervals, interval, interval) + + def fetch_funding_rates(self, symbols: Strings = None, params={}) -> FundingRates: + """ + fetch the current funding rates for multiple markets + + https://docs.coinex.com/api/v2/futures/market/http/list-market-funding-rate + + :param str[] symbols: unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of `funding rate structures ` + """ + self.load_markets() + symbols = self.market_symbols(symbols) + request: dict = {} + market = None + if symbols is not None: + symbol = self.safe_value(symbols, 0) + market = self.market(symbol) + if not market['swap']: + raise BadSymbol(self.id + ' fetchFundingRates() supports swap contracts only') + marketIds = self.market_ids(symbols) + request['market'] = ','.join(marketIds) + response = self.v2PublicGetFuturesFundingRate(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "latest_funding_rate": "0", + # "latest_funding_time": 1715731200000, + # "mark_price": "61602.22", + # "market": "BTCUSDT", + # "max_funding_rate": "0.00375", + # "min_funding_rate": "-0.00375", + # "next_funding_rate": "0.00021074", + # "next_funding_time": 1715760000000 + # } + # ], + # "message": "OK" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_funding_rates(data, symbols) + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://docs.coinex.com/api/v2/assets/deposit-withdrawal/http/withdrawal + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str [tag]: memo + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.network]: unified network code + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.check_address(address) + self.load_markets() + currency = self.currency(code) + request: dict = { + 'ccy': currency['id'], + 'to_address': address, # must be authorized, inter-user transfer by a registered mobile phone number or an email address is supported + 'amount': self.currency_to_precision(code, amount), # the actual amount without fees, https://www.coinex.com/fees + } + if tag is not None: + request['memo'] = tag + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + if networkCode is not None: + request['chain'] = self.network_code_to_id(networkCode) # required for on-chain, not required for inter-user transfer + response = self.v2PrivatePostAssetsWithdraw(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "withdraw_id": 31193755, + # "created_at": 1716874165038, + # "withdraw_method": "ON_CHAIN", + # "ccy": "USDT", + # "amount": "17.3", + # "actual_amount": "15", + # "chain": "TRC20", + # "tx_fee": "2.3", + # "fee_asset": "USDT", + # "fee_amount": "2.3", + # "to_address": "TY5vq3MT6b5cQVAHWHtpGyPg1ERcQgi3UN", + # "memo": "", + # "tx_id": "", + # "confirmations": 0, + # "explorer_address_url": "https://tronscan.org/#/address/TY5vq3MT6b5cQVAHWHtpGyPg1ERcQgi3UN", + # "explorer_tx_url": "https://tronscan.org/#/transaction/", + # "remark": "", + # "status": "audit_required" + # }, + # "message": "OK" + # } + # + transaction = self.safe_dict(response, 'data', {}) + return self.parse_transaction(transaction, currency) + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'audit': 'pending', + 'pass': 'pending', + 'audit_required': 'pending', + 'processing': 'pending', + 'confirming': 'pending', + 'not_pass': 'failed', + 'cancel': 'canceled', + 'finish': 'ok', + 'finished': 'ok', + 'fail': 'failed', + } + return self.safe_string(statuses, status, status) + + def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical funding rate prices + + https://docs.coinex.com/api/v2/futures/market/http/list-market-funding-rate-history + + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: timestamp in ms of the earliest funding rate to fetch + :param int [limit]: the maximum amount of `funding rate structures ` to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param int [params.until]: timestamp in ms of the latest funding rate + :returns dict[]: a list of `funding rate structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchFundingRateHistory() requires a symbol argument') + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchFundingRateHistory', 'paginate') + if paginate: + return self.fetch_paginated_call_deterministic('fetchFundingRateHistory', symbol, since, limit, '8h', params, 1000) + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + if since is not None: + request['start_time'] = since + if limit is not None: + request['limit'] = limit + request, params = self.handle_until_option('end_time', request, params) + response = self.v2PublicGetFuturesFundingRateHistory(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "actual_funding_rate": "0", + # "funding_time": 1715731221761, + # "market": "BTCUSDT", + # "theoretical_funding_rate": "0" + # }, + # ], + # "message": "OK", + # "pagination": { + # "has_next": True + # } + # } + # + data = self.safe_list(response, 'data', []) + rates = [] + for i in range(0, len(data)): + entry = data[i] + marketId = self.safe_string(entry, 'market') + symbolInner = self.safe_symbol(marketId, market, None, 'swap') + timestamp = self.safe_integer(entry, 'funding_time') + rates.append({ + 'info': entry, + 'symbol': symbolInner, + 'fundingRate': self.safe_number(entry, 'actual_funding_rate'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + sorted = self.sort_by(rates, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, market['symbol'], since, limit) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fetchDeposits + # + # { + # "deposit_id": 5173806, + # "created_at": 1714021652557, + # "tx_id": "d9f47d2550397c635cb89a8963118f8fe78ef048bc8b6f0caaeaa7dc6", + # "tx_id_display": "", + # "ccy": "USDT", + # "chain": "TRC20", + # "deposit_method": "ON_CHAIN", + # "amount": "30", + # "actual_amount": "", + # "to_address": "TYewD2pVWDUwfNr9A", + # "confirmations": 20, + # "status": "FINISHED", + # "tx_explorer_url": "https://tronscan.org/#/transaction", + # "to_addr_explorer_url": "https://tronscan.org/#/address", + # "remark": "" + # } + # + # fetchWithdrawals and withdraw + # + # { + # "withdraw_id": 259364, + # "created_at": 1701323541548, + # "withdraw_method": "ON_CHAIN", + # "ccy": "USDT", + # "amount": "23.845744", + # "actual_amount": "22.445744", + # "chain": "TRC20", + # "tx_fee": "1.4", + # "fee_asset": "USDT", + # "fee_amount": "1.4", + # "to_address": "T8t5i2454dhdhnnnGdi49vMbihvY", + # "memo": "", + # "tx_id": "1237623941964de9954ed2e36640228d78765c1026", + # "confirmations": 18, + # "explorer_address_url": "https://tronscan.org/#/address", + # "explorer_tx_url": "https://tronscan.org/#/transaction", + # "remark": "", + # "status": "finished" + # } + # + address = self.safe_string(transaction, 'to_address') + tag = self.safe_string(transaction, 'memo') + if tag is not None: + if len(tag) < 1: + tag = None + remark = self.safe_string(transaction, 'remark') + if remark is not None: + if len(remark) < 1: + remark = None + txid = self.safe_string(transaction, 'tx_id') + if txid is not None: + if len(txid) < 1: + txid = None + currencyId = self.safe_string(transaction, 'ccy') + code = self.safe_currency_code(currencyId, currency) + timestamp = self.safe_integer(transaction, 'created_at') + type = 'withdrawal' if ('withdraw_id' in transaction) else 'deposit' + networkId = self.safe_string(transaction, 'chain') + feeCost = self.safe_string(transaction, 'tx_fee') + transferMethod = self.safe_string_lower_2(transaction, 'withdraw_method', 'deposit_method') + internal = transferMethod == 'local' + amount = self.safe_number(transaction, 'actual_amount') + if amount is None: + amount = self.safe_number(transaction, 'amount') + if type == 'deposit': + feeCost = '0' + feeCurrencyId = self.safe_string(transaction, 'fee_asset') + fee = { + 'cost': self.parse_number(feeCost), + 'currency': self.safe_currency_code(feeCurrencyId), + } + return { + 'info': transaction, + 'id': self.safe_string_2(transaction, 'withdraw_id', 'deposit_id'), + 'txid': txid, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': self.network_id_to_code(networkId), + 'address': address, + 'addressTo': address, + 'addressFrom': None, + 'tag': tag, + 'tagTo': None, + 'tagFrom': None, + 'type': type, + 'amount': amount, + 'currency': code, + 'status': self.parse_transaction_status(self.safe_string(transaction, 'status')), + 'updated': None, + 'fee': fee, + 'comment': remark, + 'internal': internal, + } + + def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://docs.coinex.com/api/v2/assets/transfer/http/transfer + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account to transfer from + :param str toAccount: account to transfer to + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.symbol]: unified ccxt symbol, required when either the fromAccount or toAccount is margin + :returns dict: a `transfer structure ` + """ + self.load_markets() + currency = self.currency(code) + amountToPrecision = self.currency_to_precision(code, amount) + accountsByType = self.safe_dict(self.options, 'accountsByType', {}) + fromId = self.safe_string(accountsByType, fromAccount, fromAccount) + toId = self.safe_string(accountsByType, toAccount, toAccount) + request: dict = { + 'ccy': currency['id'], + 'amount': amountToPrecision, + 'from_account_type': fromId, + 'to_account_type': toId, + } + if (fromAccount == 'margin') or (toAccount == 'margin'): + symbol = self.safe_string(params, 'symbol') + if symbol is None: + raise ArgumentsRequired(self.id + ' transfer() the symbol parameter must be defined for a margin account') + params = self.omit(params, 'symbol') + request['market'] = self.market_id(symbol) + if (fromAccount != 'spot') and (toAccount != 'spot'): + raise BadRequest(self.id + ' transfer() can only be between spot and swap, or spot and margin, either the fromAccount or toAccount must be spot') + response = self.v2PrivatePostAssetsTransfer(self.extend(request, params)) + # + # { + # "code": 0, + # "data": {}, + # "message": "OK" + # } + # + return self.extend(self.parse_transfer(response, currency), { + 'amount': self.parse_number(amountToPrecision), + 'fromAccount': fromAccount, + 'toAccount': toAccount, + }) + + def parse_transfer_status(self, status): + statuses: dict = { + '0': 'ok', + 'SUCCESS': 'ok', + 'OK': 'ok', + 'finished': 'ok', + 'FINISHED': 'ok', + } + return self.safe_string(statuses, status, status) + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + timestamp = self.safe_integer(transfer, 'created_at') + currencyId = self.safe_string(transfer, 'ccy') + fromId = self.safe_string(transfer, 'from_account_type') + toId = self.safe_string(transfer, 'to_account_type') + accountsById = self.safe_value(self.options, 'accountsById', {}) + return { + 'id': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'currency': self.safe_currency_code(currencyId, currency), + 'amount': self.safe_number(transfer, 'amount'), + 'fromAccount': self.safe_string(accountsById, fromId, fromId), + 'toAccount': self.safe_string(accountsById, toId, toId), + 'status': self.parse_transfer_status(self.safe_string_2(transfer, 'code', 'status')), + } + + def fetch_transfers(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[TransferEntry]: + """ + fetch a history of internal transfers made on an account + + https://docs.coinex.com/api/v2/assets/transfer/http/list-transfer-history + + :param str code: unified currency code of the currency transferred + :param int [since]: the earliest time in ms to fetch transfers for + :param int [limit]: the maximum number of transfer structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' for fetching transfers to and from your margin account + :returns dict[]: a list of `transfer structures ` + """ + self.load_markets() + if code is None: + raise ArgumentsRequired(self.id + ' fetchTransfers() requires a code argument') + currency = self.currency(code) + request: dict = { + 'ccy': currency['id'], + } + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchTransfers', params) + if marginMode is not None: + request['transfer_type'] = 'MARGIN' + else: + request['transfer_type'] = 'FUTURES' + if since is not None: + request['start_time'] = since + if limit is not None: + request['limit'] = limit + request, params = self.handle_until_option('end_time', request, params) + response = self.v2PrivateGetAssetsTransferHistory(self.extend(request, params)) + # + # { + # "data": [ + # { + # "created_at": 1715848480646, + # "from_account_type": "SPOT", + # "to_account_type": "FUTURES", + # "ccy": "USDT", + # "amount": "10", + # "status": "finished" + # }, + # ], + # "pagination": { + # "total": 8, + # "has_next": False + # }, + # "code": 0, + # "message": "OK" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_transfers(data, currency, since, limit) + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://docs.coinex.com/api/v2/assets/deposit-withdrawal/http/list-withdrawal-history + + :param str [code]: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawal structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + request: dict = {} + currency = None + if code is not None: + currency = self.currency(code) + request['ccy'] = currency['id'] + if limit is not None: + request['limit'] = limit + response = self.v2PrivateGetAssetsWithdraw(self.extend(request, params)) + # + # { + # "data": [ + # { + # "withdraw_id": 259364, + # "created_at": 1701323541548, + # "withdraw_method": "ON_CHAIN", + # "ccy": "USDT", + # "amount": "23.845744", + # "actual_amount": "22.445744", + # "chain": "TRC20", + # "tx_fee": "1.4", + # "fee_asset": "USDT", + # "fee_amount": "1.4", + # "to_address": "T8t5i2454dhdhnnnGdi49vMbihvY", + # "memo": "", + # "tx_id": "1237623941964de9954ed2e36640228d78765c1026", + # "confirmations": 18, + # "explorer_address_url": "https://tronscan.org/#/address", + # "explorer_tx_url": "https://tronscan.org/#/transaction", + # "remark": "", + # "status": "finished" + # }, + # ], + # "pagination": { + # "total": 9, + # "has_next": True + # }, + # "code": 0, + # "message": "OK" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_transactions(data, currency, since, limit) + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://docs.coinex.com/api/v2/assets/deposit-withdrawal/http/list-deposit-history + + :param str [code]: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposit structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + request: dict = {} + currency = None + if code is not None: + currency = self.currency(code) + request['ccy'] = currency['id'] + if limit is not None: + request['limit'] = limit + response = self.v2PrivateGetAssetsDepositHistory(self.extend(request, params)) + # + # { + # "data": [ + # { + # "deposit_id": 5173806, + # "created_at": 1714021652557, + # "tx_id": "d9f47d2550397c635cb89a8963118f8fe78ef048bc8b6f0caaeaa7dc6", + # "tx_id_display": "", + # "ccy": "USDT", + # "chain": "TRC20", + # "deposit_method": "ON_CHAIN", + # "amount": "30", + # "actual_amount": "", + # "to_address": "TYewD2pVWDUwfNr9A", + # "confirmations": 20, + # "status": "FINISHED", + # "tx_explorer_url": "https://tronscan.org/#/transaction", + # "to_addr_explorer_url": "https://tronscan.org/#/address", + # "remark": "" + # }, + # ], + # "paginatation": { + # "total": 8, + # "has_next": True + # }, + # "code": 0, + # "message": "OK" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_transactions(data, currency, since, limit) + + def parse_isolated_borrow_rate(self, info: dict, market: Market = None) -> IsolatedBorrowRate: + # + # { + # "market": "BTCUSDT", + # "ccy": "USDT", + # "leverage": 10, + # "min_amount": "60", + # "max_amount": "500000", + # "daily_interest_rate": "0.001" + # } + # + marketId = self.safe_string(info, 'market') + market = self.safe_market(marketId, market, None, 'spot') + currency = self.safe_string(info, 'ccy') + rate = self.safe_number(info, 'daily_interest_rate') + baseRate = None + quoteRate = None + if currency == market['baseId']: + baseRate = rate + elif currency == market['quoteId']: + quoteRate = rate + return { + 'symbol': market['symbol'], + 'base': market['base'], + 'baseRate': baseRate, + 'quote': market['quote'], + 'quoteRate': quoteRate, + 'period': 86400000, + 'timestamp': None, + 'datetime': None, + 'info': info, + } + + def fetch_isolated_borrow_rate(self, symbol: str, params={}) -> IsolatedBorrowRate: + """ + fetch the rate of interest to borrow a currency for margin trading + + https://docs.coinex.com/api/v2/assets/loan-flat/http/list-margin-interest-limit + + :param str symbol: unified symbol of the market to fetch the borrow rate for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str params['code']: unified currency code + :returns dict: an `isolated borrow rate structure ` + """ + self.load_markets() + code = self.safe_string(params, 'code') + if code is None: + raise ArgumentsRequired(self.id + ' fetchIsolatedBorrowRate() requires a code parameter') + params = self.omit(params, 'code') + currency = self.currency(code) + market = self.market(symbol) + request: dict = { + 'market': market['id'], + 'ccy': currency['id'], + } + response = self.v2PrivateGetAssetsMarginInterestLimit(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "market": "BTCUSDT", + # "ccy": "USDT", + # "leverage": 10, + # "min_amount": "60", + # "max_amount": "500000", + # "daily_interest_rate": "0.001" + # }, + # "message": "OK" + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_isolated_borrow_rate(data, market) + + def fetch_borrow_interest(self, code: Str = None, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[BorrowInterest]: + """ + fetch the interest owed by the user for borrowing currency for margin trading + + https://docs.coinex.com/api/v2/assets/loan-flat/http/list-margin-borrow-history + + :param str [code]: unified currency code + :param str [symbol]: unified market symbol when fetch interest in isolated markets + :param int [since]: the earliest time in ms to fetch borrrow interest for + :param int [limit]: the maximum number of structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `borrow interest structures ` + """ + self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['market'] = market['id'] + if limit is not None: + request['limit'] = limit + response = self.v2PrivateGetAssetsMarginBorrowHistory(self.extend(request, params)) + # + # { + # "data": [ + # { + # "borrow_id": 2642934, + # "created_at": 1654761016000, + # "market": "BTCUSDT", + # "ccy": "USDT", + # "daily_interest_rate": "0.001", + # "expired_at": 1655625016000, + # "borrow_amount": "100", + # "to_repaied_amount": "0", + # "is_auto_renew": False, + # "status": "finish" + # }, + # ], + # "pagination": { + # "total": 4, + # "has_next": True + # }, + # "code": 0, + # "message": "OK" + # } + # + rows = self.safe_value(response, 'data', []) + interest = self.parse_borrow_interests(rows, market) + return self.filter_by_currency_since_limit(interest, code, since, limit) + + def parse_borrow_interest(self, info: dict, market: Market = None) -> BorrowInterest: + # + # { + # "borrow_id": 2642934, + # "created_at": 1654761016000, + # "market": "BTCUSDT", + # "ccy": "USDT", + # "daily_interest_rate": "0.001", + # "expired_at": 1655625016000, + # "borrow_amount": "100", + # "to_repaied_amount": "0", + # "is_auto_renew": False, + # "status": "finish" + # } + # + marketId = self.safe_string(info, 'market') + market = self.safe_market(marketId, market, None, 'spot') + timestamp = self.safe_integer(info, 'expired_at') + return { + 'info': info, + 'symbol': market['symbol'], + 'currency': self.safe_currency_code(self.safe_string(info, 'ccy')), + 'interest': self.safe_number(info, 'to_repaied_amount'), + 'interestRate': self.safe_number(info, 'daily_interest_rate'), + 'amountBorrowed': self.safe_number(info, 'borrow_amount'), + 'marginMode': 'isolated', + 'timestamp': timestamp, # expiry time + 'datetime': self.iso8601(timestamp), + } + + def borrow_isolated_margin(self, symbol: str, code: str, amount: float, params={}): + """ + create a loan to borrow margin + + https://docs.coinex.com/api/v2/assets/loan-flat/http/margin-borrow + + :param str symbol: unified market symbol, required for coinex + :param str code: unified currency code of the currency to borrow + :param float amount: the amount to borrow + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.isAutoRenew]: whether to renew the margin loan automatically or not, default is False + :returns dict: a `margin loan structure ` + """ + self.load_markets() + market = self.market(symbol) + currency = self.currency(code) + isAutoRenew = self.safe_bool_2(params, 'isAutoRenew', 'is_auto_renew', False) + params = self.omit(params, 'isAutoRenew') + request: dict = { + 'market': market['id'], + 'ccy': currency['id'], + 'borrow_amount': self.currency_to_precision(code, amount), + 'is_auto_renew': isAutoRenew, + } + response = self.v2PrivatePostAssetsMarginBorrow(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "borrow_id": 13784021, + # "market": "BTCUSDT", + # "ccy": "USDT", + # "daily_interest_rate": "0.001", + # "expired_at": 1717299948340, + # "borrow_amount": "60", + # "to_repaied_amount": "60.0025", + # "status": "loan" + # }, + # "message": "OK" + # } + # + data = self.safe_dict(response, 'data', {}) + transaction = self.parse_margin_loan(data, currency) + return self.extend(transaction, { + 'amount': amount, + 'symbol': symbol, + }) + + def repay_isolated_margin(self, symbol: str, code: str, amount, params={}): + """ + repay borrowed margin and interest + + https://docs.coinex.com/api/v2/assets/loan-flat/http/margin-repay + + :param str symbol: unified market symbol, required for coinex + :param str code: unified currency code of the currency to repay + :param float amount: the amount to repay + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.borrow_id]: extra parameter that is not required + :returns dict: a `margin loan structure ` + """ + self.load_markets() + market = self.market(symbol) + currency = self.currency(code) + request: dict = { + 'market': market['id'], + 'ccy': currency['id'], + 'amount': self.currency_to_precision(code, amount), + } + response = self.v2PrivatePostAssetsMarginRepay(self.extend(request, params)) + # + # { + # "code": 0, + # "data": {}, + # "message": "OK" + # } + # + data = self.safe_dict(response, 'data', {}) + transaction = self.parse_margin_loan(data, currency) + return self.extend(transaction, { + 'amount': amount, + 'symbol': symbol, + }) + + def parse_margin_loan(self, info, currency: Currency = None): + # + # { + # "borrow_id": 13784021, + # "market": "BTCUSDT", + # "ccy": "USDT", + # "daily_interest_rate": "0.001", + # "expired_at": 1717299948340, + # "borrow_amount": "60", + # "to_repaied_amount": "60.0025", + # "status": "loan" + # } + # + currencyId = self.safe_string(info, 'ccy') + marketId = self.safe_string(info, 'market') + timestamp = self.safe_integer(info, 'expired_at') + return { + 'id': self.safe_integer(info, 'borrow_id'), + 'currency': self.safe_currency_code(currencyId, currency), + 'amount': self.safe_string(info, 'borrow_amount'), + 'symbol': self.safe_symbol(marketId, None, None, 'spot'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'info': info, + } + + def fetch_deposit_withdraw_fee(self, code: str, params={}): + """ + fetch the fee for deposits and withdrawals + + https://docs.coinex.com/api/v2/assets/deposit-withdrawal/http/get-deposit-withdrawal-config + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `fee structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'ccy': currency['id'], + } + response = self.v2PublicGetAssetsDepositWithdrawConfig(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "asset": { + # "ccy": "USDT", + # "deposit_enabled": True, + # "withdraw_enabled": True, + # "inter_transfer_enabled": True, + # "is_st": False + # }, + # "chains": [ + # { + # "chain": "TRC20", + # "min_deposit_amount": "2.4", + # "min_withdraw_amount": "2.4", + # "deposit_enabled": True, + # "withdraw_enabled": True, + # "deposit_delay_minutes": 0, + # "safe_confirmations": 10, + # "irreversible_confirmations": 20, + # "deflation_rate": "0", + # "withdrawal_fee": "2.4", + # "withdrawal_precision": 6, + # "memo": "", + # "is_memo_required_for_deposit": False, + # "explorer_asset_url": "https://tronscan.org/#/token20/TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t" + # }, + # ] + # }, + # "message": "OK" + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_deposit_withdraw_fee(data, currency) + + def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}): + """ + fetch the fees for deposits and withdrawals + + https://docs.coinex.com/api/v2/assets/deposit-withdrawal/http/list-all-deposit-withdrawal-config + + @param codes + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` + """ + self.load_markets() + response = self.v2PublicGetAssetsAllDepositWithdrawConfig(params) + # + # { + # "code": 0, + # "data": [ + # { + # "asset": { + # "ccy": "CET", + # "deposit_enabled": True, + # "withdraw_enabled": True, + # "inter_transfer_enabled": True, + # "is_st": False + # }, + # "chains": [ + # { + # "chain": "CSC", + # "min_deposit_amount": "0.8", + # "min_withdraw_amount": "8", + # "deposit_enabled": True, + # "withdraw_enabled": True, + # "deposit_delay_minutes": 0, + # "safe_confirmations": 10, + # "irreversible_confirmations": 20, + # "deflation_rate": "0", + # "withdrawal_fee": "0.026", + # "withdrawal_precision": 8, + # "memo": "", + # "is_memo_required_for_deposit": False, + # "explorer_asset_url": "" + # }, + # ] + # } + # ], + # "message": "OK" + # } + # + data = self.safe_list(response, 'data', []) + result: dict = {} + for i in range(0, len(data)): + item = data[i] + asset = self.safe_dict(item, 'asset', {}) + currencyId = self.safe_string(asset, 'ccy') + if currencyId is None: + continue + code = self.safe_currency_code(currencyId) + if codes is None or self.in_array(code, codes): + result[code] = self.parse_deposit_withdraw_fee(item) + return result + + def parse_deposit_withdraw_fee(self, fee, currency: Currency = None): + # + # { + # "asset": { + # "ccy": "USDT", + # "deposit_enabled": True, + # "withdraw_enabled": True, + # "inter_transfer_enabled": True, + # "is_st": False + # }, + # "chains": [ + # { + # "chain": "TRC20", + # "min_deposit_amount": "2.4", + # "min_withdraw_amount": "2.4", + # "deposit_enabled": True, + # "withdraw_enabled": True, + # "deposit_delay_minutes": 0, + # "safe_confirmations": 10, + # "irreversible_confirmations": 20, + # "deflation_rate": "0", + # "withdrawal_fee": "2.4", + # "withdrawal_precision": 6, + # "memo": "", + # "is_memo_required_for_deposit": False, + # "explorer_asset_url": "https://tronscan.org/#/token20/TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t" + # }, + # ] + # } + # + result: dict = { + 'info': fee, + 'withdraw': { + 'fee': None, + 'percentage': None, + }, + 'deposit': { + 'fee': None, + 'percentage': None, + }, + 'networks': {}, + } + chains = self.safe_list(fee, 'chains', []) + asset = self.safe_dict(fee, 'asset', {}) + for i in range(0, len(chains)): + entry = chains[i] + isWithdrawEnabled = self.safe_bool(entry, 'withdraw_enabled') + if isWithdrawEnabled: + result['withdraw']['fee'] = self.safe_number(entry, 'withdrawal_fee') + result['withdraw']['percentage'] = False + networkId = self.safe_string(entry, 'chain') + if networkId: + networkCode = self.network_id_to_code(networkId, self.safe_string(asset, 'ccy')) + result['networks'][networkCode] = { + 'withdraw': { + 'fee': self.safe_number(entry, 'withdrawal_fee'), + 'percentage': False, + }, + 'deposit': { + 'fee': None, + 'percentage': None, + }, + } + return result + + def fetch_leverage(self, symbol: str, params={}) -> Leverage: + """ + fetch the set leverage for a market + + https://docs.coinex.com/api/v2/assets/loan-flat/http/list-margin-interest-limit + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str params['code']: unified currency code + :returns dict: a `leverage structure ` + """ + self.load_markets() + code = self.safe_string(params, 'code') + if code is None: + raise ArgumentsRequired(self.id + ' fetchLeverage() requires a code parameter') + params = self.omit(params, 'code') + currency = self.currency(code) + market = self.market(symbol) + request: dict = { + 'market': market['id'], + 'ccy': currency['id'], + } + response = self.v2PrivateGetAssetsMarginInterestLimit(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "market": "BTCUSDT", + # "ccy": "USDT", + # "leverage": 10, + # "min_amount": "50", + # "max_amount": "500000", + # "daily_interest_rate": "0.001" + # }, + # "message": "OK" + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_leverage(data, market) + + def parse_leverage(self, leverage: dict, market: Market = None) -> Leverage: + # + # { + # "market": "BTCUSDT", + # "ccy": "USDT", + # "leverage": 10, + # "min_amount": "50", + # "max_amount": "500000", + # "daily_interest_rate": "0.001" + # } + # + marketId = self.safe_string(leverage, 'market') + leverageValue = self.safe_integer(leverage, 'leverage') + return { + 'info': leverage, + 'symbol': self.safe_symbol(marketId, market, None, 'spot'), + 'marginMode': 'isolated', + 'longLeverage': leverageValue, + 'shortLeverage': leverageValue, + } + + def fetch_position_history(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Position]: + """ + fetches historical positions + + https://docs.coinex.com/api/v2/futures/position/http/list-finished-position + + :param str symbol: unified contract symbol + :param int [since]: the earliest time in ms to fetch positions for + :param int [limit]: the maximum amount of records to fetch, default is 10 + :param dict [params]: extra parameters specific to the exchange api endpoint + :param int [params.until]: the latest time in ms to fetch positions for + :returns dict[]: a list of `position structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'market_type': 'FUTURES', + 'market': market['id'], + } + if limit is not None: + request['limit'] = limit + if since is not None: + request['start_time'] = since + request, params = self.handle_until_option('end_time', request, params) + response = self.v2PrivateGetFuturesFinishedPosition(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "position_id": 305891033, + # "market": "BTCUSDT", + # "market_type": "FUTURES", + # "side": "long", + # "margin_mode": "cross", + # "open_interest": "0.0001", + # "close_avbl": "0.0001", + # "ath_position_amount": "0.0001", + # "unrealized_pnl": "0", + # "realized_pnl": "-0.00311684", + # "avg_entry_price": "62336.8", + # "cml_position_value": "6.23368", + # "max_position_value": "6.23368", + # "created_at": 1715152208041, + # "updated_at": 1715152208041, + # "take_profit_price": "0", + # "stop_loss_price": "0", + # "take_profit_type": "", + # "stop_loss_type": "", + # "settle_price": "62336.8", + # "settle_value": "6.23368", + # "leverage": "3", + # "margin_avbl": "2.07789333", + # "ath_margin_size": "2.07789333", + # "position_margin_rate": "2.40545879023305655728", + # "maintenance_margin_rate": "0.005", + # "maintenance_margin_value": "0.03118094", + # "liq_price": "0", + # "bkr_price": "0", + # "adl_level": 1 + # } + # ], + # "message": "OK", + # "pagination": { + # "has_next": False + # } + # } + # + records = self.safe_list(response, 'data', []) + positions = self.parse_positions(records) + return self.filter_by_symbol_since_limit(positions, symbol, since, limit) + + def close_position(self, symbol: str, side: OrderSide = None, params={}) -> Order: + """ + closes an open position for a market + + https://docs.coinex.com/api/v2/futures/position/http/close-position + + :param str symbol: unified CCXT market symbol + :param str [side]: buy or sell, not used by coinex + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str params['type']: required by coinex, one of: limit, market, maker_only, ioc or fok, default is *market* + :param str [params.price]: the price to fulfill the order, ignored in market orders + :param str [params.amount]: the amount to trade in units of the base currency + :param str [params.clientOrderId]: the client id of the order + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + type = self.safe_string(params, 'type', 'market') + request: dict = { + 'market': market['id'], + 'market_type': 'FUTURES', + 'type': type, + } + clientOrderId = self.safe_string_2(params, 'client_id', 'clientOrderId') + if clientOrderId is not None: + request['client_id'] = clientOrderId + params = self.omit(params, 'clientOrderId') + response = self.v2PrivatePostFuturesClosePosition(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "amount": "0.0001", + # "client_id": "", + # "created_at": 1729666043969, + # "fee": "0.00335858", + # "fee_ccy": "USDT", + # "filled_amount": "0.0001", + # "filled_value": "6.717179", + # "last_filled_amount": "0.0001", + # "last_filled_price": "67171.79", + # "maker_fee_rate": "0", + # "market": "BTCUSDT", + # "market_type": "FUTURES", + # "order_id": 155477479761, + # "price": "0", + # "realized_pnl": "-0.001823", + # "side": "sell", + # "taker_fee_rate": "0.0005", + # "type": "market", + # "unfilled_amount": "0", + # "updated_at": 1729666043969 + # }, + # "message": "OK" + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data, market) + + def handle_margin_mode_and_params(self, methodName, params={}, defaultValue=None): + """ + @ignore + marginMode specified by params["marginMode"], self.options["marginMode"], self.options["defaultMarginMode"], params["margin"] = True or self.options["defaultType"] = 'margin' + :param dict params: extra parameters specific to the exchange api endpoint + :returns Array: the marginMode in lowercase + """ + defaultType = self.safe_string(self.options, 'defaultType') + isMargin = self.safe_bool(params, 'margin', False) + marginMode = None + marginMode, params = super(coinex, self).handle_margin_mode_and_params(methodName, params, defaultValue) + if marginMode is None: + if (defaultType == 'margin') or (isMargin is True): + marginMode = 'isolated' + return [marginMode, params] + + def nonce(self): + return self.milliseconds() + + def sign(self, path, api=[], method='GET', params={}, headers=None, body=None): + path = self.implode_params(path, params) + version = api[0] + requestUrl = api[1] + url = self.urls['api'][requestUrl] + '/' + version + '/' + path + query = self.omit(params, self.extract_params(path)) + nonce = str(self.nonce()) + if method == 'POST': + parts = path.split('/') + firstPart = self.safe_string(parts, 0, '') + numParts = len(parts) + lastPart = self.safe_string(parts, numParts - 1, '') + lastWords = lastPart.split('_') + numWords = len(lastWords) + lastWord = self.safe_string(lastWords, numWords - 1, '') + if (firstPart == 'order') and (lastWord == 'limit' or lastWord == 'market'): + # inject in implicit API calls + # POST /order/limit - Place limit orders + # POST /order/market - Place market orders + # POST /order/stop/limit - Place stop limit orders + # POST /order/stop/market - Place stop market orders + # POST /perpetual/v1/order/put_limit - Place limit orders + # POST /perpetual/v1/order/put_market - Place market orders + # POST /perpetual/v1/order/put_stop_limit - Place stop limit orders + # POST /perpetual/v1/order/put_stop_market - Place stop market orders + clientOrderId = self.safe_string(params, 'client_id') + if clientOrderId is None: + defaultId = 'x-167673045' + brokerId = self.safe_value(self.options, 'brokerId', defaultId) + query['client_id'] = brokerId + '_' + self.uuid16() + if requestUrl == 'perpetualPrivate': + self.check_required_credentials() + query = self.extend({ + 'access_id': self.apiKey, + 'timestamp': nonce, + }, query) + query = self.keysort(query) + urlencoded = self.rawencode(query) + signature = self.hash(self.encode(urlencoded + '&secret_key=' + self.secret), 'sha256') + headers = { + 'Authorization': signature.lower(), + 'AccessId': self.apiKey, + } + if (method == 'GET') or (method == 'PUT'): + url += '?' + urlencoded + else: + headers['Content-Type'] = 'application/x-www-form-urlencoded' + body = urlencoded + elif requestUrl == 'public' or requestUrl == 'perpetualPublic': + if query: + url += '?' + self.urlencode(query) + else: + if version == 'v1': + self.check_required_credentials() + query = self.extend({ + 'access_id': self.apiKey, + 'tonce': nonce, + }, query) + query = self.keysort(query) + urlencoded = self.rawencode(query) + signature = self.hash(self.encode(urlencoded + '&secret_key=' + self.secret), 'md5') + headers = { + 'Authorization': signature.upper(), + 'Content-Type': 'application/json', + } + if (method == 'GET') or (method == 'DELETE') or (method == 'PUT'): + url += '?' + urlencoded + else: + body = self.json(query) + elif version == 'v2': + self.check_required_credentials() + query = self.keysort(query) + urlencoded = self.rawencode(query) + preparedString = method + '/' + version + '/' + path + if method == 'POST': + body = self.json(query) + preparedString += body + elif urlencoded: + preparedString += '?' + urlencoded + preparedString += nonce + self.secret + signature = self.hash(self.encode(preparedString), 'sha256') + headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'X-COINEX-KEY': self.apiKey, + 'X-COINEX-SIGN': signature, + 'X-COINEX-TIMESTAMP': nonce, + } + if method != 'POST': + if urlencoded: + url += '?' + urlencoded + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None + code = self.safe_string(response, 'code') + data = self.safe_value(response, 'data') + message = self.safe_string(response, 'message') + if (code != '0') or ((message != 'Success') and (message != 'Succeeded') and (message.lower() != 'ok') and not data): + feedback = self.id + ' ' + message + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], code, feedback) + raise ExchangeError(feedback) + return None + + def fetch_margin_adjustment_history(self, symbol: Str = None, type: Str = None, since: Num = None, limit: Num = None, params={}) -> List[MarginModification]: + """ + fetches the history of margin added or reduced from contract isolated positions + + https://docs.coinex.com/api/v2/futures/position/http/list-position-margin-history + + :param str symbol: unified market symbol + :param str [type]: not used by coinex fetchMarginAdjustmentHistory + :param int [since]: timestamp in ms of the earliest change to fetch + :param int [limit]: the maximum amount of changes to fetch, default is 10 + :param dict params: extra parameters specific to the exchange api endpoint + :param int [params.until]: timestamp in ms of the latest change to fetch + :param int [params.positionId]: the id of the position that you want to retrieve margin adjustment history for + :returns dict[]: a list of `margin structures ` + """ + self.load_markets() + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMarginAdjustmentHistory() requires a symbol argument') + positionId = self.safe_integer_2(params, 'positionId', 'position_id') + params = self.omit(params, 'positionId') + if positionId is None: + raise ArgumentsRequired(self.id + ' fetchMarginAdjustmentHistory() requires a positionId parameter') + market = self.market(symbol) + request: dict = { + 'market': market['id'], + 'market_type': 'FUTURES', + 'position_id': positionId, + } + request, params = self.handle_until_option('end_time', request, params) + if since is not None: + request['start_time'] = since + if limit is not None: + request['limit'] = limit + response = self.v2PrivateGetFuturesPositionMarginHistory(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "bkr_pirce": "24698.56000000000000005224", + # "created_at": 1715489978697, + # "leverage": "3", + # "liq_price": "24822.67336683417085432386", + # "margin_avbl": "3.634928", + # "margin_change": "-1.5", + # "margin_mode": "isolated", + # "market": "BTCUSDT", + # "market_type": "FUTURES", + # "open_interest": "0.0001", + # "position_id": 306458800, + # "settle_price": "61047.84" + # }, + # ], + # "message": "OK", + # "pagination": { + # "has_next": True + # } + # } + # + data = self.safe_list(response, 'data', []) + modifications = self.parse_margin_modifications(data, None, 'market', 'swap') + return self.filter_by_symbol_since_limit(modifications, symbol, since, limit) diff --git a/ccxt/coinmate.py b/ccxt/coinmate.py new file mode 100644 index 0000000..f53da61 --- /dev/null +++ b/ccxt/coinmate.py @@ -0,0 +1,1193 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.coinmate import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currency, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, TradingFeeInterface, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class coinmate(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(coinmate, self).describe(), { + 'id': 'coinmate', + 'name': 'CoinMate', + 'countries': ['GB', 'CZ', 'EU'], # UK, Czech Republic + 'rateLimit': 600, + 'has': { + 'CORS': True, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelOrder': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createOrder': True, + 'createOrderWithTakeProfitAndStopLoss': False, + 'createOrderWithTakeProfitAndStopLossWs': False, + 'createPostOnlyOrder': False, + 'createReduceOnlyOrder': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchDepositsWithdrawals': True, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTrades': True, + 'fetchTradingFee': True, + 'fetchTradingFees': False, + 'fetchTransactions': 'emulated', + 'fetchVolatilityHistory': False, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'repayMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'transfer': False, + 'withdraw': True, + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/51840849/87460806-1c9f3f00-c616-11ea-8c46-a77018a8f3f4.jpg', + 'api': { + 'rest': 'https://coinmate.io/api', + }, + 'www': 'https://coinmate.io', + 'fees': 'https://coinmate.io/fees', + 'doc': [ + 'https://coinmate.docs.apiary.io', + 'https://coinmate.io/developers', + ], + 'referral': 'https://coinmate.io?referral=YTFkM1RsOWFObVpmY1ZjMGREQmpTRnBsWjJJNVp3PT0', + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + 'uid': True, + }, + 'api': { + 'public': { + 'get': [ + 'orderBook', + 'ticker', + 'tickerAll', + 'products', + 'transactions', + 'tradingPairs', + ], + }, + 'private': { + 'post': [ + 'balances', + 'bitcoinCashWithdrawal', + 'bitcoinCashDepositAddresses', + 'bitcoinDepositAddresses', + 'bitcoinWithdrawal', + 'bitcoinWithdrawalFees', + 'buyInstant', + 'buyLimit', + 'cancelOrder', + 'cancelOrderWithInfo', + 'createVoucher', + 'dashDepositAddresses', + 'dashWithdrawal', + 'ethereumWithdrawal', + 'ethereumDepositAddresses', + 'litecoinWithdrawal', + 'litecoinDepositAddresses', + 'openOrders', + 'order', + 'orderHistory', + 'orderById', + 'pusherAuth', + 'redeemVoucher', + 'replaceByBuyLimit', + 'replaceByBuyInstant', + 'replaceBySellLimit', + 'replaceBySellInstant', + 'rippleDepositAddresses', + 'rippleWithdrawal', + 'sellInstant', + 'sellLimit', + 'transactionHistory', + 'traderFees', + 'tradeHistory', + 'transfer', + 'transferHistory', + 'unconfirmedBitcoinDeposits', + 'unconfirmedBitcoinCashDeposits', + 'unconfirmedDashDeposits', + 'unconfirmedEthereumDeposits', + 'unconfirmedLitecoinDeposits', + 'unconfirmedRippleDeposits', + 'cancelAllOpenOrders', + 'withdrawVirtualCurrency', + 'virtualCurrencyDepositAddresses', + 'unconfirmedVirtualCurrencyDeposits', + 'adaWithdrawal', + 'adaDepositAddresses', + 'unconfirmedAdaDeposits', + 'solWithdrawal', + 'solDepositAddresses', + 'unconfirmedSolDeposits', + ], + }, + }, + 'fees': { + 'trading': { + 'tierBased': True, + 'percentage': True, + 'taker': self.parse_number('0.006'), + 'maker': self.parse_number('0.004'), + 'tiers': { + 'taker': [ + [self.parse_number('0'), self.parse_number('0.006')], + [self.parse_number('10000'), self.parse_number('0.003')], + [self.parse_number('100000'), self.parse_number('0.0023')], + [self.parse_number('250000'), self.parse_number('0.0021')], + [self.parse_number('500000'), self.parse_number('0.0018')], + [self.parse_number('1000000'), self.parse_number('0.0015')], + [self.parse_number('3000000'), self.parse_number('0.0012')], + [self.parse_number('15000000'), self.parse_number('0.001')], + ], + 'maker': [ + [self.parse_number('0'), self.parse_number('0.004')], + [self.parse_number('10000'), self.parse_number('0.002')], + [self.parse_number('100000'), self.parse_number('0.0012')], + [self.parse_number('250000'), self.parse_number('0.0009')], + [self.parse_number('500000'), self.parse_number('0.0005')], + [self.parse_number('1000000'), self.parse_number('0.0003')], + [self.parse_number('3000000'), self.parse_number('0.0002')], + [self.parse_number('15000000'), self.parse_number('-0.0004')], + ], + }, + }, + }, + 'options': { + 'withdraw': { + 'fillResponsefromRequest': True, + 'methods': { + 'BTC': 'privatePostBitcoinWithdrawal', + 'LTC': 'privatePostLitecoinWithdrawal', + 'BCH': 'privatePostBitcoinCashWithdrawal', + 'ETH': 'privatePostEthereumWithdrawal', + 'XRP': 'privatePostRippleWithdrawal', + 'DASH': 'privatePostDashWithdrawal', + 'DAI': 'privatePostDaiWithdrawal', + 'ADA': 'privatePostAdaWithdrawal', + 'SOL': 'privatePostSolWithdrawal', + }, + }, + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, # todo implement + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': False, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': True, # todo implement + 'leverage': False, + 'marketBuyByCost': False, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': False, + 'iceberg': True, # todo + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 100000, + 'untilDays': 100000, # todo implement + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': None, + 'untilDays': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchClosedOrders': None, + 'fetchOHLCV': None, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'exceptions': { + 'exact': { + 'No order with given ID': OrderNotFound, + }, + 'broad': { + 'Not enough account balance available': InsufficientFunds, + 'Incorrect order ID': InvalidOrder, + 'Minimum Order Size ': InvalidOrder, + 'max allowed precision': InvalidOrder, # {"error":true,"errorMessage":"USDT_EUR - max allowed precision is 4 decimal places","data":null} + 'TOO MANY REQUESTS': RateLimitExceeded, + 'Access denied.': AuthenticationError, # {"error":true,"errorMessage":"Access denied.","data":null} + }, + }, + 'precisionMode': TICK_SIZE, + }) + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for coinmate + + https://coinmate.docs.apiary.io/#reference/trading-pairs/get-trading-pairs/get + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = self.publicGetTradingPairs(params) + # + # { + # "error":false, + # "errorMessage":null, + # "data": [ + # { + # "name":"BTC_EUR", + # "firstCurrency":"BTC", + # "secondCurrency":"EUR", + # "priceDecimals":2, + # "lotDecimals":8, + # "minAmount":0.0002, + # "tradesWebSocketChannelId":"trades-BTC_EUR", + # "orderBookWebSocketChannelId":"order_book-BTC_EUR", + # "tradeStatisticsWebSocketChannelId":"statistics-BTC_EUR" + # }, + # ] + # } + # + data = self.safe_value(response, 'data', []) + result = [] + for i in range(0, len(data)): + market = data[i] + id = self.safe_string(market, 'name') + baseId = self.safe_string(market, 'firstCurrency') + quoteId = self.safe_string(market, 'secondCurrency') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + symbol = base + '/' + quote + result.append({ + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': None, + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(self.parse_precision(self.safe_string(market, 'lotDecimals'))), + 'price': self.parse_number(self.parse_precision(self.safe_string(market, 'priceDecimals'))), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(market, 'minAmount'), + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + 'info': market, + }) + return result + + def parse_balance(self, response) -> Balances: + balances = self.safe_value(response, 'data', {}) + result: dict = {'info': response} + currencyIds = list(balances.keys()) + for i in range(0, len(currencyIds)): + currencyId = currencyIds[i] + code = self.safe_currency_code(currencyId) + balance = self.safe_value(balances, currencyId) + account = self.account() + account['free'] = self.safe_string(balance, 'available') + account['used'] = self.safe_string(balance, 'reserved') + account['total'] = self.safe_string(balance, 'balance') + result[code] = account + return self.safe_balance(result) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://coinmate.docs.apiary.io/#reference/balance/get-balances/post + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + response = self.privatePostBalances(params) + return self.parse_balance(response) + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://coinmate.docs.apiary.io/#reference/order-book/get-order-book/get + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'currencyPair': market['id'], + 'groupByPriceLimit': 'False', + } + response = self.publicGetOrderBook(self.extend(request, params)) + orderbook = response['data'] + timestamp = self.safe_timestamp(orderbook, 'timestamp') + return self.parse_order_book(orderbook, market['symbol'], timestamp, 'bids', 'asks', 'price', 'amount') + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://coinmate.docs.apiary.io/#reference/ticker/get-ticker/get + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'currencyPair': market['id'], + } + response = self.publicGetTicker(self.extend(request, params)) + # + # { + # "error": False, + # "errorMessage": null, + # "data": { + # "last": 0.55105, + # "high": 0.56439, + # "low": 0.54358, + # "amount": 37038.993381, + # "bid": 0.54595, + # "ask": 0.55324, + # "change": 3.03659243, + # "open": 0.53481, + # "timestamp": 1708074779 + # } + # } + # + data = self.safe_dict(response, 'data') + return self.parse_ticker(data, market) + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://coinmate.docs.apiary.io/#reference/ticker/get-ticker-all/get + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + symbols = self.market_symbols(symbols) + response = self.publicGetTickerAll(params) + # + # { + # "error": False, + # "errorMessage": null, + # "data": { + # "LTC_BTC": { + # "last": "0.001337", + # "high": "0.001348", + # "low": "0.001332", + # "amount": "34.75472959", + # "bid": "0.001348", + # "ask": "0.001356", + # "change": "-0.74239050", + # "open": "0.001347", + # "timestamp": "1708074485" + # } + # } + # } + # + data = self.safe_value(response, 'data', {}) + keys = list(data.keys()) + result: dict = {} + for i in range(0, len(keys)): + market = self.market(keys[i]) + ticker = self.parse_ticker(self.safe_value(data, keys[i]), market) + result[market['symbol']] = ticker + return self.filter_by_array_tickers(result, 'symbol', symbols) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "last": "0.001337", + # "high": "0.001348", + # "low": "0.001332", + # "amount": "34.75472959", + # "bid": "0.001348", + # "ask": "0.001356", + # "change": "-0.74239050", + # "open": "0.001347", + # "timestamp": "1708074485" + # } + # + timestamp = self.safe_timestamp(ticker, 'timestamp') + last = self.safe_number(ticker, 'last') + return self.safe_ticker({ + 'symbol': market['symbol'], + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_number(ticker, 'high'), + 'low': self.safe_number(ticker, 'low'), + 'bid': self.safe_number(ticker, 'bid'), + 'bidVolume': None, + 'ask': self.safe_number(ticker, 'ask'), + 'vwap': None, + 'askVolume': None, + 'open': None, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': self.safe_number(ticker, 'amount'), + 'quoteVolume': None, + 'info': ticker, + }, market) + + def fetch_deposits_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch history of deposits and withdrawals + + https://coinmate.docs.apiary.io/#reference/transfers/get-transfer-history/post + + :param str [code]: unified currency code for the currency of the deposit/withdrawals, default is None + :param int [since]: timestamp in ms of the earliest deposit/withdrawal, default is None + :param int [limit]: max number of deposit/withdrawals to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `transaction structure ` + """ + self.load_markets() + request: dict = { + 'limit': 1000, + } + if limit is not None: + request['limit'] = limit + if since is not None: + request['timestampFrom'] = since + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + response = self.privatePostTransferHistory(self.extend(request, params)) + items = response['data'] + return self.parse_transactions(items, None, since, limit) + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'COMPLETED': 'ok', + 'WAITING': 'pending', + 'SENT': 'pending', + 'CREATED': 'pending', + 'OK': 'ok', + 'NEW': 'pending', + 'CANCELED': 'canceled', + } + return self.safe_string(statuses, status, status) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # deposits + # + # { + # "transactionId": 1862815, + # "timestamp": 1516803982388, + # "amountCurrency": "LTC", + # "amount": 1, + # "fee": 0, + # "walletType": "LTC", + # "transferType": "DEPOSIT", + # "transferStatus": "COMPLETED", + # "txid": + # "ccb9255dfa874e6c28f1a64179769164025329d65e5201849c2400abd6bce245", + # "destination": "LQrtSKA6LnhcwRrEuiborQJnjFF56xqsFn", + # "destinationTag": null + # } + # + # withdrawals + # + # { + # "transactionId": 2140966, + # "timestamp": 1519314282976, + # "amountCurrency": "EUR", + # "amount": 8421.7228, + # "fee": 16.8772, + # "walletType": "BANK_WIRE", + # "transferType": "WITHDRAWAL", + # "transferStatus": "COMPLETED", + # "txid": null, + # "destination": null, + # "destinationTag": null + # } + # + # withdraw + # + # { + # "id": 2132583, + # } + # + timestamp = self.safe_integer(transaction, 'timestamp') + currencyId = self.safe_string(transaction, 'amountCurrency') + code = self.safe_currency_code(currencyId, currency) + return { + 'info': transaction, + 'id': self.safe_string_2(transaction, 'transactionId', 'id'), + 'txid': self.safe_string(transaction, 'txid'), + 'type': self.safe_string_lower(transaction, 'transferType'), + 'currency': code, + 'network': self.safe_string(transaction, 'walletType'), + 'amount': self.safe_number(transaction, 'amount'), + 'status': self.parse_transaction_status(self.safe_string(transaction, 'transferStatus')), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'address': self.safe_string(transaction, 'destination'), + 'addressFrom': None, + 'addressTo': None, + 'tag': self.safe_string(transaction, 'destinationTag'), + 'tagFrom': None, + 'tagTo': None, + 'updated': None, + 'comment': None, + 'internal': None, + 'fee': { + 'cost': self.safe_number(transaction, 'fee'), + 'currency': code, + 'rate': None, + }, + } + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://coinmate.docs.apiary.io/#reference/bitcoin-withdrawal-and-deposit/withdraw-bitcoins/post + https://coinmate.docs.apiary.io/#reference/litecoin-withdrawal-and-deposit/withdraw-litecoins/post + https://coinmate.docs.apiary.io/#reference/ethereum-withdrawal-and-deposit/withdraw-ethereum/post + https://coinmate.docs.apiary.io/#reference/ripple-withdrawal-and-deposit/withdraw-ripple/post + https://coinmate.docs.apiary.io/#reference/cardano-withdrawal-and-deposit/withdraw-cardano/post + https://coinmate.docs.apiary.io/#reference/solana-withdrawal-and-deposit/withdraw-solana/post + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.check_address(address) + self.load_markets() + currency = self.currency(code) + withdrawOptions = self.safe_value(self.options, 'withdraw', {}) + methods = self.safe_value(withdrawOptions, 'methods', {}) + method = self.safe_string(methods, code) + if method is None: + allowedCurrencies = list(methods.keys()) + raise ExchangeError(self.id + ' withdraw() only allows withdrawing the following currencies: ' + ', '.join(allowedCurrencies)) + request: dict = { + 'amount': self.currency_to_precision(code, amount), + 'address': address, + } + if tag is not None: + request['destinationTag'] = tag + response = getattr(self, method)(self.extend(request, params)) + # + # { + # "error": False, + # "errorMessage": null, + # "data": { + # "id": "9e0a37fc-4ab4-4b9d-b9e7-c9c8f7c4c8e0" + # } + # } + # + data = self.safe_value(response, 'data') + transaction = self.parse_transaction(data, currency) + fillResponseFromRequest = self.safe_bool(withdrawOptions, 'fillResponseFromRequest', True) + if fillResponseFromRequest: + transaction['amount'] = amount + transaction['currency'] = code + transaction['address'] = address + transaction['tag'] = tag + transaction['type'] = 'withdrawal' + transaction['status'] = 'pending' + return transaction + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://coinmate.docs.apiary.io/#reference/trade-history/get-trade-history/post + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + if limit is None: + limit = 1000 + request: dict = { + 'limit': limit, + } + if symbol is not None: + market = self.market(symbol) + request['currencyPair'] = market['id'] + if since is not None: + request['timestampFrom'] = since + response = self.privatePostTradeHistory(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + return self.parse_trades(data, None, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchMyTrades(private) + # + # { + # "transactionId": 2671819, + # "createdTimestamp": 1529649127605, + # "currencyPair": "LTC_BTC", + # "type": "BUY", + # "orderType": "LIMIT", + # "orderId": 101810227, + # "amount": 0.01, + # "price": 0.01406, + # "fee": 0, + # "feeType": "MAKER" + # } + # + # fetchTrades(public) + # + # { + # "timestamp":1561598833416, + # "transactionId":"4156303", + # "price":10950.41, + # "amount":0.004, + # "currencyPair":"BTC_EUR", + # "tradeType":"BUY" + # } + # + marketId = self.safe_string(trade, 'currencyPair') + market = self.safe_market(marketId, market, '_') + priceString = self.safe_string(trade, 'price') + amountString = self.safe_string(trade, 'amount') + side = self.safe_string_lower_2(trade, 'type', 'tradeType') + type = self.safe_string_lower(trade, 'orderType') + orderId = self.safe_string(trade, 'orderId') + id = self.safe_string(trade, 'transactionId') + timestamp = self.safe_integer_2(trade, 'timestamp', 'createdTimestamp') + fee = None + feeCostString = self.safe_string(trade, 'fee') + if feeCostString is not None: + fee = { + 'cost': feeCostString, + 'currency': market['quote'], + } + takerOrMaker = self.safe_string(trade, 'feeType') + takerOrMaker = 'maker' if (takerOrMaker == 'MAKER') else 'taker' + return self.safe_trade({ + 'id': id, + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'type': type, + 'side': side, + 'order': orderId, + 'takerOrMaker': takerOrMaker, + 'price': priceString, + 'amount': amountString, + 'cost': None, + 'fee': fee, + }, market) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://coinmate.docs.apiary.io/#reference/transactions/transactions/get + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'currencyPair': market['id'], + 'minutesIntoHistory': 10, + } + response = self.publicGetTransactions(self.extend(request, params)) + # + # { + # "error":false, + # "errorMessage":null, + # "data":[ + # { + # "timestamp":1561598833416, + # "transactionId":"4156303", + # "price":10950.41, + # "amount":0.004, + # "currencyPair":"BTC_EUR", + # "tradeType":"BUY" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_trades(data, market, since, limit) + + def fetch_trading_fee(self, symbol: str, params={}) -> TradingFeeInterface: + """ + fetch the trading fees for a market + + https://coinmate.docs.apiary.io/#reference/trader-fees/get-trading-fees/post + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `fee structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'currencyPair': market['id'], + } + response = self.privatePostTraderFees(self.extend(request, params)) + # + # { + # "error": False, + # "errorMessage": null, + # "data": {maker: '0.3', taker: "0.35", timestamp: "1646253217815"} + # } + # + data = self.safe_value(response, 'data', {}) + makerString = self.safe_string(data, 'maker') + takerString = self.safe_string(data, 'taker') + maker = self.parse_number(Precise.string_div(makerString, '100')) + taker = self.parse_number(Precise.string_div(takerString, '100')) + return { + 'info': data, + 'symbol': market['symbol'], + 'maker': maker, + 'taker': taker, + 'percentage': True, + 'tierBased': True, + } + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://coinmate.docs.apiary.io/#reference/order/get-open-orders/post + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + response = self.privatePostOpenOrders(self.extend({}, params)) + extension: dict = {'status': 'open'} + return self.parse_orders(response['data'], None, since, limit, extension) + + def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://coinmate.docs.apiary.io/#reference/order/order-history/post + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrders() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'currencyPair': market['id'], + } + # offset param that appears in other parts of the API doesn't appear to be supported here + if limit is not None: + request['limit'] = limit + response = self.privatePostOrderHistory(self.extend(request, params)) + return self.parse_orders(response['data'], market, since, limit) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'FILLED': 'closed', + 'CANCELLED': 'canceled', + 'PARTIALLY_FILLED': 'open', + 'OPEN': 'open', + } + return self.safe_string(statuses, status, status) + + def parse_order_type(self, type: Str): + types: dict = { + 'LIMIT': 'limit', + 'MARKET': 'market', + } + return self.safe_string(types, type, type) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # limit sell + # + # { + # "id": 781246605, + # "timestamp": 1584480015133, + # "trailingUpdatedTimestamp": null, + # "type": "SELL", + # "currencyPair": "ETH_BTC", + # "price": 0.0345, + # "amount": 0.01, + # "stopPrice": null, + # "originalStopPrice": null, + # "marketPriceAtLastUpdate": null, + # "marketPriceAtOrderCreation": null, + # "orderTradeType": "LIMIT", + # "hidden": False, + # "trailing": False, + # "clientOrderId": null + # } + # + # limit buy + # + # { + # "id": 67527001, + # "timestamp": 1517931722613, + # "trailingUpdatedTimestamp": null, + # "type": "BUY", + # "price": 5897.24, + # "remainingAmount": 0.002367, + # "originalAmount": 0.1, + # "stopPrice": null, + # "originalStopPrice": null, + # "marketPriceAtLastUpdate": null, + # "marketPriceAtOrderCreation": null, + # "status": "CANCELLED", + # "orderTradeType": "LIMIT", + # "hidden": False, + # "avgPrice": null, + # "trailing": False, + # } + # + # cancelOrder + # + # { + # "success": True, + # "remainingAmount": 0.1 + # } + # + id = self.safe_string(order, 'id') + timestamp = self.safe_integer(order, 'timestamp') + side = self.safe_string_lower(order, 'type') + priceString = self.safe_string(order, 'price') + amountString = self.safe_string(order, 'originalAmount') + remainingString = self.safe_string_2(order, 'remainingAmount', 'amount') + status = self.parse_order_status(self.safe_string(order, 'status')) + type = self.parse_order_type(self.safe_string(order, 'orderTradeType')) + averageString = self.safe_string(order, 'avgPrice') + marketId = self.safe_string(order, 'currencyPair') + symbol = self.safe_symbol(marketId, market, '_') + clientOrderId = self.safe_string(order, 'clientOrderId') + return self.safe_order({ + 'id': id, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'symbol': symbol, + 'type': type, + 'timeInForce': None, + 'postOnly': None, + 'side': side, + 'price': priceString, + 'triggerPrice': self.safe_number(order, 'stopPrice'), + 'amount': amountString, + 'cost': None, + 'average': averageString, + 'filled': None, + 'remaining': remainingString, + 'status': status, + 'trades': None, + 'info': order, + 'fee': None, + }, market) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://coinmate.docs.apiary.io/#reference/order/buy-limit-order/post + https://coinmate.docs.apiary.io/#reference/order/sell-limit-order/post + https://coinmate.docs.apiary.io/#reference/order/buy-instant-order/post + https://coinmate.docs.apiary.io/#reference/order/sell-instant-order/post + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + method = 'privatePost' + self.capitalize(side) + market = self.market(symbol) + request: dict = { + 'currencyPair': market['id'], + } + if type == 'market': + if side == 'buy': + request['total'] = self.amount_to_precision(symbol, amount) # amount in fiat + else: + request['amount'] = self.amount_to_precision(symbol, amount) # amount in fiat + method += 'Instant' + else: + request['amount'] = self.amount_to_precision(symbol, amount) # amount in crypto + request['price'] = self.price_to_precision(symbol, price) + method += self.capitalize(type) + response = getattr(self, method)(self.extend(request, params)) + id = self.safe_string(response, 'data') + return self.safe_order({ + 'info': response, + 'id': id, + }, market) + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://coinmate.docs.apiary.io/#reference/order/get-order-by-orderid/post + https://coinmate.docs.apiary.io/#reference/order/get-order-by-clientorderid/post + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + request: dict = { + 'orderId': id, + } + market = None + if symbol: + market = self.market(symbol) + response = self.privatePostOrderById(self.extend(request, params)) + data = self.safe_dict(response, 'data') + return self.parse_order(data, market) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://coinmate.docs.apiary.io/#reference/order/cancel-order/post + + :param str id: order id + :param str symbol: not used by coinmate cancelOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + # {"error":false,"errorMessage":null,"data":{"success":true,"remainingAmount":0.01}} + request: dict = {'orderId': id} + response = self.privatePostCancelOrderWithInfo(self.extend(request, params)) + # + # { + # "error": False, + # "errorMessage": null, + # "data": { + # "success": True, + # "remainingAmount": 0.1 + # } + # } + # + data = self.safe_dict(response, 'data') + return self.parse_order(data) + + def nonce(self): + return self.milliseconds() + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = self.urls['api']['rest'] + '/' + path + if api == 'public': + if params: + url += '?' + self.urlencode(params) + else: + self.check_required_credentials() + nonce = str(self.nonce()) + auth = nonce + self.uid + self.apiKey + signature = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256) + body = self.urlencode(self.extend({ + 'clientId': self.uid, + 'nonce': nonce, + 'publicKey': self.apiKey, + 'signature': signature.upper(), + }, params)) + headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + } + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None # fallback to default error handler + # + # {"error":true,"errorMessage":"Api internal error","data":null} + # {"error":true,"errorMessage":"Access denied.","data":null} + # + errorMessage = self.safe_string(response, 'errorMessage') + if errorMessage is not None: + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], errorMessage, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], errorMessage, feedback) + raise ExchangeError(feedback) # unknown message + return None diff --git a/ccxt/coinmetro.py b/ccxt/coinmetro.py new file mode 100644 index 0000000..cdcc92d --- /dev/null +++ b/ccxt/coinmetro.py @@ -0,0 +1,1945 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.coinmetro import ImplicitAPI +from ccxt.base.types import Any, Balances, Currencies, Currency, IndexType, Int, LedgerEntry, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class coinmetro(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(coinmetro, self).describe(), { + 'id': 'coinmetro', + 'name': 'Coinmetro', + 'countries': ['EE'], # Republic of Estonia + 'version': 'v1', + 'rateLimit': 200, # 1 request per 200 ms, 20 per minute, 300 per hour, 1k per day + 'certified': False, + 'pro': False, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': True, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': True, + 'borrowIsolatedMargin': False, + 'cancelAllOrders': False, + 'cancelOrder': True, + 'cancelOrders': False, + 'closeAllPositions': False, + 'closePosition': True, + 'createDepositAddress': False, + 'createOrder': True, + 'createPostOnlyOrder': False, + 'createReduceOnlyOrder': False, + 'createStopLimitOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'deposit': False, + 'editOrder': False, + 'fetchAccounts': False, + 'fetchBalance': True, + 'fetchBidsAsks': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchCanceledAndClosedOrders': True, + 'fetchCanceledOrders': False, + 'fetchClosedOrder': False, + 'fetchClosedOrders': False, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDeposit': False, + 'fetchDepositAddress': False, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': False, + 'fetchDepositsWithdrawals': False, + 'fetchDepositWithdrawFee': False, + 'fetchDepositWithdrawFees': False, + 'fetchFundingHistory': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchL3OrderBook': False, + 'fetchLedger': True, + 'fetchLeverage': False, + 'fetchLeverageTiers': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterestHistory': False, + 'fetchOpenOrder': False, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrderBooks': False, + 'fetchOrders': False, + 'fetchOrderTrades': False, + 'fetchPosition': False, + 'fetchPositions': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchStatus': False, + 'fetchTicker': False, + 'fetchTickers': True, + 'fetchTime': False, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': False, + 'fetchTradingLimits': False, + 'fetchTransactionFee': False, + 'fetchTransactionFees': False, + 'fetchTransactions': False, + 'fetchTransfers': False, + 'fetchWithdrawal': False, + 'fetchWithdrawals': False, + 'fetchWithdrawalWhitelist': False, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'sandbox': True, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'signIn': False, + 'transfer': False, + 'withdraw': False, + 'ws': False, + }, + 'timeframes': { + '1m': '60000', + '5m': '300000', + '30m': '1800000', + '4h': '14400000', + '1d': '86400000', + }, + 'urls': { + 'logo': 'https://github.com/ccxt/ccxt/assets/43336371/e86f87ec-6ba3-4410-962b-f7988c5db539', + 'api': { + 'public': 'https://api.coinmetro.com', + 'private': 'https://api.coinmetro.com', + }, + 'test': { + 'public': 'https://api.coinmetro.com/open', + 'private': 'https://api.coinmetro.com/open', + }, + 'www': 'https://coinmetro.com/', + 'doc': [ + 'https://documenter.getpostman.com/view/3653795/SVfWN6KS', + ], + 'fees': 'https://help.coinmetro.com/hc/en-gb/articles/6844007317789-What-are-the-fees-on-Coinmetro-', + 'referral': 'https://go.coinmetro.com/?ref=crypto24', + }, + 'api': { + 'public': { + 'get': { + 'demo/temp': 1, + 'exchange/candles/{pair}/{timeframe}/{from}/{to}': 3, + 'exchange/prices': 1, + 'exchange/ticks/{pair}/{from}': 3, + 'assets': 1, + 'markets': 1, + 'exchange/book/{pair}': 3, + 'exchange/bookUpdates/{pair}/{from}': 1, # not unified + }, + }, + 'private': { + 'get': { + 'users/balances': 1, + 'users/wallets': 1, + 'users/wallets/history/{since}': 1.67, + 'exchange/orders/status/{orderID}': 1, + 'exchange/orders/active': 1, + 'exchange/orders/history/{since}': 1.67, + 'exchange/fills/{since}': 1.67, + 'exchange/margin': 1, # not unified + }, + 'post': { + 'jwt': 1, # not unified + 'jwtDevice': 1, # not unified + 'devices': 1, # not unified + 'jwt-read-only': 1, # not unified + 'exchange/orders/create': 1, + 'exchange/orders/modify/{orderID}': 1, # not unified + 'exchange/swap': 1, # not unified + 'exchange/swap/confirm/{swapId}': 1, # not unified + 'exchange/orders/close/{orderID}': 1, + 'exchange/orders/hedge': 1, # not unified + }, + 'put': { + 'jwt': 1, # not unified + 'exchange/orders/cancel/{orderID}': 1, + 'users/margin/collateral': 1, + 'users/margin/primary/{currency}': 1, # not unified + }, + }, + }, + 'requiredCredentials': { + 'apiKey': False, + 'secret': False, + 'uid': True, + 'token': True, + }, + 'fees': { + 'trading': { + 'feeSide': 'get', + 'tierBased': False, + 'percentage': True, + 'taker': self.parse_number('0.001'), + 'maker': self.parse_number('0'), + }, + }, + 'precisionMode': TICK_SIZE, + # exchange-specific options + 'options': { + 'currenciesByIdForParseMarket': None, + 'currencyIdsListForParseMarket': ['QRDO'], + }, + 'features': { + 'spot': { + 'sandbox': True, + 'createOrder': { + 'marginMode': True, # todo implement + 'triggerPrice': True, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': { + 'triggerPriceType': None, + 'price': False, + }, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': False, + 'GTD': True, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': False, + 'iceberg': True, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': None, + 'daysBack': 100000, + 'untilDays': None, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': None, + 'daysBack': 100000, + 'untilDays': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchClosedOrders': None, + 'fetchOHLCV': { + 'limit': 1000, + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'exceptions': { + # https://trade-docs.coinmetro.co/?javascript--nodejs#message-codes + 'exact': { + 'Both buyingCurrency and sellingCurrency are required': InvalidOrder, # 422 - "Both buyingCurrency and sellingCurrency are required" + 'One and only one of buyingQty and sellingQty is required': InvalidOrder, # 422 - "One and only one of buyingQty and sellingQty is required" + 'Invalid buyingCurrency': InvalidOrder, # 422 - "Invalid buyingCurrency" + 'Invalid \'from\'': BadRequest, # 422 Unprocessable Entity {"message":"Invalid 'from'"} + 'Invalid sellingCurrency': InvalidOrder, # 422 - "Invalid sellingCurrency" + 'Invalid buyingQty': InvalidOrder, # 422 - "Invalid buyingQty" + 'Invalid sellingQty': InvalidOrder, # 422 - "Invalid sellingQty" + 'Insufficient balance': InsufficientFunds, # 422 - "Insufficient balance" + 'Expiration date is in the past or too near in the future': InvalidOrder, # 422 Unprocessable Entity {"message":"Expiration date is in the past or too near in the future"} + 'Forbidden': PermissionDenied, # 403 Forbidden {"message":"Forbidden"} + 'Order Not Found': OrderNotFound, # 404 Not Found {"message":"Order Not Found"} + 'since must be a millisecond timestamp': BadRequest, # 422 Unprocessable Entity {"message":"since must be a millisecond timestamp"} + 'This pair is disabled on margin': BadSymbol, # 422 Unprocessable Entity {"message":"This pair is disabled on margin"} + }, + 'broad': { + 'accessing from a new IP': PermissionDenied, # 403 Forbidden {"message":"You're accessing from a new IP. Please check your email."} + 'available to allocate': InsufficientFunds, # 403 Forbidden {"message":"Insufficient EUR available to allocate"} + 'At least': BadRequest, # 422 Unprocessable Entity {"message":"At least 5 EUR per operation"} + 'collateral is not allowed': BadRequest, # 422 Unprocessable Entity {"message":"DOGE collateral is not allowed"} + 'Insufficient liquidity': InvalidOrder, # 503 Service Unavailable {"message":"Insufficient liquidity to fill the FOK order completely."} + 'Insufficient order size': InvalidOrder, # 422 Unprocessable Entity {"message":"Insufficient order size - min 0.002 ETH"} + 'Invalid quantity': InvalidOrder, # 422 Unprocessable Entity {"message":"Invalid quantity!"} + 'Invalid Stop Loss': InvalidOrder, # 422 Unprocessable Entity {"message":"Invalid Stop Loss!"} + 'Invalid stop price!': InvalidOrder, # 422 Unprocessable Entity {"message":"Invalid stop price!"} + 'Not enough balance': InsufficientFunds, # 422 Unprocessable Entity {"message":"Not enough balance!"} + 'Not enough margin': InsufficientFunds, # Unprocessable Entity {"message":"Not enough margin!"} + 'orderType missing': BadRequest, # 422 Unprocessable Entity {"message":"orderType missing!"} + 'Server Timeout': ExchangeError, # 503 Service Unavailable {"message":"Server Timeout!"} + 'Time in force has to be IOC or FOK for market orders': InvalidOrder, # 422 Unprocessable Entity {"message":"Time in force has to be IOC or FOK for market orders!"} + 'Too many attempts': RateLimitExceeded, # 429 Too Many Requests {"message":"Too many attempts. Try again in 3 seconds"} + }, + }, + }) + + def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://documenter.getpostman.com/view/3653795/SVfWN6KS#d5876d43-a3fe-4479-8c58-24d0f044edfb + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = self.publicGetAssets(params) + # + # [ + # { + # "symbol": "BTC", + # "name": "Bitcoin", + # "color": "#FFA500", + # "type": "coin", + # "canDeposit": True, + # "canWithdraw": True, + # "canTrade": True, + # "notabeneDecimals": 8, + # "canMarket": True, + # "maxSwap": 10000, + # "digits": 6, + # "multiplier": 1000000, + # "bookDigits": 8, + # "bookMultiplier": 100000000, + # "sentimentData": { + # "sentiment": 51.59555555555555, + # "interest": 1.127511216044664 + # }, + # "minQty": 0.0001 + # }, + # { + # "symbol": "EUR", + # "name": "Euro", + # "color": "#1246FF", + # "type": "fiat", + # "canDeposit": True, + # "canWithdraw": True, + # "canTrade": True, + # "canMarket": True, + # "maxSwap": 10000, + # "digits": 2, + # "multiplier": 100, + # "bookDigits": 3, + # "bookMultiplier": 1000, + # "minQty": 5 + # } + # ... + # ] + # + result: dict = {} + for i in range(0, len(response)): + currency = response[i] + id = self.safe_string(currency, 'symbol') + code = self.safe_currency_code(id) + typeRaw = self.safe_string(currency, 'type') + type = None + if typeRaw == 'coin' or typeRaw == 'token' or typeRaw == 'erc20': + type = 'crypto' + elif typeRaw == 'fiat': + type = 'fiat' + precisionDigits = self.safe_string_2(currency, 'digits', 'notabeneDecimals') + if code == 'RENDER': + # RENDER is an exception(with broken info) + precisionDigits = '4' + result[code] = self.safe_currency_structure({ + 'id': id, + 'code': code, + 'name': code, + 'type': type, + 'info': currency, + 'active': self.safe_bool(currency, 'canTrade'), + 'deposit': self.safe_bool(currency, 'canDeposit'), + 'withdraw': self.safe_bool(currency, 'canWithdraw'), + 'fee': None, + 'precision': self.parse_number(self.parse_precision(precisionDigits)), + 'limits': { + 'amount': { + 'min': self.safe_number(currency, 'minQty'), + 'max': None, + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + }, + 'networks': {}, + }) + if self.safe_value(self.options, 'currenciesByIdForParseMarket') is None: + currenciesById = self.index_by(result, 'id') + self.options['currenciesByIdForParseMarket'] = currenciesById + currentCurrencyIdsList = self.safe_list(self.options, 'currencyIdsListForParseMarket', []) + currencyIdsList = list(currenciesById.keys()) + for i in range(0, len(currencyIdsList)): + currentCurrencyIdsList.append(currencyIdsList[i]) + self.options['currencyIdsListForParseMarket'] = currentCurrencyIdsList + return result + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for coinmetro + + https://documenter.getpostman.com/view/3653795/SVfWN6KS#9fd18008-338e-4863-b07d-722878a46832 + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + promises = [] + promises.append(self.publicGetMarkets(params)) + if self.safe_value(self.options, 'currenciesByIdForParseMarket') is None: + promises.append(self.fetch_currencies()) + responses = promises + response = responses[0] + # + # [ + # { + # "pair": "YFIEUR", + # "precision": 5, + # "margin": False + # }, + # { + # "pair": "BTCEUR", + # "precision": 2, + # "margin": True + # }, + # ... + # ] + # + result = [] + for i in range(0, len(response)): + market = self.parse_market(response[i]) + # there are several broken(unavailable info) markets + if market['base'] is None or market['quote'] is None: + continue + result.append(market) + return result + + def parse_market(self, market: dict) -> Market: + id = self.safe_string(market, 'pair') + parsedMarketId = self.parse_market_id(id) + baseId = self.safe_string(parsedMarketId, 'baseId') + quoteId = self.safe_string(parsedMarketId, 'quoteId') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + basePrecisionAndLimits = self.parse_market_precision_and_limits(baseId) + quotePrecisionAndLimits = self.parse_market_precision_and_limits(quoteId) + margin = self.safe_bool(market, 'margin', False) + tradingFees = self.safe_value(self.fees, 'trading', {}) + return self.safe_market_structure({ + 'id': id, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': margin, + 'swap': False, + 'future': False, + 'option': False, + 'active': True, + 'contract': False, + 'linear': None, + 'inverse': None, + 'taker': self.safe_number(tradingFees, 'taker'), + 'maker': self.safe_number(tradingFees, 'maker'), + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': basePrecisionAndLimits['precision'], + 'price': self.parse_number(self.parse_precision(self.safe_string(market, 'precision'))), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': basePrecisionAndLimits['minLimit'], + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': quotePrecisionAndLimits['minLimit'], + 'max': None, + }, + }, + 'created': None, + 'info': market, + }) + + def parse_market_id(self, marketId): + baseId = None + quoteId = None + currencyIds = self.safe_value(self.options, 'currencyIdsListForParseMarket', []) + # Bubble sort by length(longest first) + currencyIdsLength = len(currencyIds) + for i in range(0, currencyIdsLength): + for j in range(0, currencyIdsLength - i - 1): + a = currencyIds[j] + b = currencyIds[j + 1] + if len(a) < len(b): + currencyIds[j] = b + currencyIds[j + 1] = a + for i in range(0, len(currencyIds)): + currencyId = currencyIds[i] + entryIndex = marketId.find(currencyId) + if entryIndex == 0: + restId = marketId.replace(currencyId, '') + if self.in_array(restId, currencyIds): + if entryIndex == 0: + baseId = currencyId + quoteId = restId + else: + baseId = restId + quoteId = currencyId + break + if baseId is None or quoteId is None: + # https://github.com/ccxt/ccxt/issues/26820 + if marketId.endswith('USDT'): + baseId = marketId.replace('USDT', '') + quoteId = 'USDT' + if marketId.endswith('USD'): + baseId = marketId.replace('USD', '') + quoteId = 'USD' + result: dict = { + 'baseId': baseId, + 'quoteId': quoteId, + } + return result + + def parse_market_precision_and_limits(self, currencyId): + currencies = self.safe_value(self.options, 'currenciesByIdForParseMarket', {}) + currency = self.safe_value(currencies, currencyId, {}) + limits = self.safe_value(currency, 'limits', {}) + amountLimits = self.safe_value(limits, 'amount', {}) + minLimit = self.safe_number(amountLimits, 'min') + result: dict = { + 'precision': self.safe_number(currency, 'precision'), + 'minLimit': minLimit, + } + return result + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://documenter.getpostman.com/view/3653795/SVfWN6KS#13cfb5bc-7bfb-4847-85e1-e0f35dfb3573 + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch entries for + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + 'timeframe': self.safe_string(self.timeframes, timeframe, timeframe), + } + until = None + if since is not None: + request['from'] = since + if limit is not None: + duration = self.parse_timeframe(timeframe) * 1000 + until = self.sum(since, duration * (limit)) + else: + request['from'] = ':from' # self endpoint doesn't accept empty from and to params(setting them into the value described in the documentation) + until = self.safe_integer(params, 'until', until) + if until is not None: + params = self.omit(params, ['until']) + request['to'] = until + else: + request['to'] = ':to' + response = self.publicGetExchangeCandlesPairTimeframeFromTo(self.extend(request, params)) + # + # { + # "candleHistory": [ + # { + # "pair": "ETHUSDT", + # "timeframe": 86400000, + # "timestamp": 1697673600000, + # "c": 1567.4409353098604, + # "h": 1566.7514068472303, + # "l": 1549.4563666936847, + # "o": 1563.4490341395904, + # "v": 0 + # }, + # { + # "pair": "ETHUSDT", + # "timeframe": 86400000, + # "timestamp": 1697760000000, + # "c": 1603.7831363339324, + # "h": 1625.0356823666407, + # "l": 1565.4629390011505, + # "o": 1566.8387619426028, + # "v": 0 + # }, + # ... + # ] + # } + # + candleHistory = self.safe_list(response, 'candleHistory', []) + return self.parse_ohlcvs(candleHistory, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + return [ + self.safe_integer(ohlcv, 'timestamp'), + self.safe_number(ohlcv, 'o'), + self.safe_number(ohlcv, 'h'), + self.safe_number(ohlcv, 'l'), + self.safe_number(ohlcv, 'c'), + self.safe_number(ohlcv, 'v'), + ] + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://documenter.getpostman.com/view/3653795/SVfWN6KS#6ee5d698-06da-4570-8c84-914185e05065 + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch(default 200, max 500) + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + if since is not None: + request['from'] = since + else: + # self endpoint accepts empty from param + request['from'] = '' + response = self.publicGetExchangeTicksPairFrom(self.extend(request, params)) + # + # { + # "tickHistory": [ + # { + # "pair": "ETHUSDT", + # "price": 2077.5623, + # "qty": 0.002888, + # "timestamp": 1700684689420, + # "seqNum": 10644554718 + # }, + # { + # "pair": "ETHUSDT", + # "price": 2078.3848, + # "qty": 0.003368, + # "timestamp": 1700684738410, + # "seqNum": 10644559561 + # }, + # { + # "pair": "ETHUSDT", + # "price": 2077.1513, + # "qty": 0.00337, + # "timestamp": 1700684816853, + # "seqNum": 10644567113 + # }, + # ... + # ] + # } + # + tickHistory = self.safe_list(response, 'tickHistory', []) + return self.parse_trades(tickHistory, market, since, limit) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://documenter.getpostman.com/view/3653795/SVfWN6KS#4d48ae69-8ee2-44d1-a268-71f84e557b7b + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve(default 500, max 1000) + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + request: dict = {} + if since is not None: + request['since'] = since + else: + # the exchange requires a value for the since param + request['since'] = 0 + response = self.privateGetExchangeFillsSince(self.extend(request, params)) + # + # [ + # { + # "pair": "ETHUSDC", + # "seqNumber": 10873722343, + # "timestamp": 1702570610747, + # "qty": 0.002, + # "price": 2282, + # "side": "buy", + # "orderID": "65671262d93d9525ac009e36170257061073952c6423a8c5b4d6c" + # }, + # ... + # ] + # + return self.parse_trades(response, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades + # { + # "pair": "ETHUSDT", + # "price": 2077.1513, + # "qty": 0.00337, + # "timestamp": 1700684816853, + # "seqNum": 10644567113 + # }, + # + # fetchMyTrades + # { + # "pair": "ETHUSDC", + # "seqNumber": 10873722343, + # "timestamp": 1702570610747, + # "qty": 0.002, + # "price": 2282, + # "side": "buy", + # "orderID": "65671262d93d9525ac009e36170257061073952c6423a8c5b4d6c" + # } + # + # fetchOrders + # { + # "_id": "657b31d360a9542449381bdc", + # "seqNumber": 10873722343, + # "timestamp": 1702570610747, + # "qty": 0.002, + # "price": 2282, + # "side": "buy" + # } + # + # { + # "pair":"ETHUSDC", + # "seqNumber":"10873722343", + # "timestamp":"1702570610747", + # "qty":"0.002", + # "price":"2282", + # "side":"buy", + # "orderID":"65671262d93d9525ac009e36170257061073952c6423a8c5b4d6c", + # "userID":"65671262d93d9525ac009e36" + # } + # + marketId = self.safe_string_2(trade, 'symbol', 'pair') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + id = self.safe_string_n(trade, ['_id', 'seqNum', 'seqNumber']) + timestamp = self.safe_integer(trade, 'timestamp') + priceString = self.safe_string(trade, 'price') + amountString = self.safe_string(trade, 'qty') + order = self.safe_string(trade, 'orderID') + side = self.safe_string(trade, 'side') + return self.safe_trade({ + 'id': id, + 'order': order, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'type': None, + 'side': side, + 'takerOrMaker': None, + 'price': priceString, + 'amount': amountString, + 'cost': None, + 'fee': None, + 'info': trade, + }, market) + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://documenter.getpostman.com/view/3653795/SVfWN6KS#26ad80d7-8c46-41b5-9208-386f439a8b87 + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return(default 100, max 200) + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + response = self.publicGetExchangeBookPair(self.extend(request, params)) + # + # { + # "book": { + # "pair": "ETHUSDT", + # "seqNumber": 10800409239, + # "ask": { + # "2354.2861": 3.75, + # "2354.3138": 19, + # "2354.7538": 80, + # "2355.5430": 260, + # "2356.4611": 950, + # "2361.7150": 1500, + # "206194.0000": 0.01 + # }, + # "bid": { + # "2352.6339": 3.75, + # "2352.6002": 19, + # "2352.2402": 80, + # "2351.4582": 260, + # "2349.3111": 950, + # "2343.8601": 1500, + # "1.0000": 5 + # }, + # "checksum": 2108177337 + # } + # } + # + book = self.safe_value(response, 'book', {}) + rawBids = self.safe_value(book, 'bid', {}) + rawAsks = self.safe_value(book, 'ask', {}) + rawOrderbook: dict = { + 'bids': rawBids, + 'asks': rawAsks, + } + orderbook = self.parse_order_book(rawOrderbook, symbol) + orderbook['nonce'] = self.safe_integer(book, 'seqNumber') + return orderbook + + def parse_bids_asks(self, bidasks, priceKey: IndexType = 0, amountKey: IndexType = 1, countOrIdKey: IndexType = 2): + prices = list(bidasks.keys()) + result = [] + for i in range(0, len(prices)): + priceString = self.safe_string(prices, i) + price = self.safe_number(prices, i) + volume = self.safe_number(bidasks, priceString) + result.append([price, volume]) + return result + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://documenter.getpostman.com/view/3653795/SVfWN6KS#6ecd1cd1-f162-45a3-8b3b-de690332a485 + + :param str[] [symbols]: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + response = self.publicGetExchangePrices(params) + # + # { + # "latestPrices": [ + # { + # "pair": "PERPEUR", + # "timestamp": 1702549840393, + # "price": 0.7899997816001223, + # "qty": 1e-12, + # "ask": 0.8, + # "bid": 0.7799995632002446 + # }, + # { + # "pair": "PERPUSD", + # "timestamp": 1702549841973, + # "price": 0.8615317721366659, + # "qty": 1e-12, + # "ask": 0.8742333599999257, + # "bid": 0.8490376365388491 + # }, + # ... + # ], + # "24hInfo": [ + # { + # "delta": 0.25396444229149906, + # "h": 0.78999978160012, + # "l": 0.630001740844, + # "v": 54.910000002833996, + # "pair": "PERPEUR", + # "sentimentData": { + # "sentiment": 36.71333333333333, + # "interest": 0.47430830039525695 + # } + # }, + # { + # "delta": 0.26915154078134096, + # "h": 0.86220315458898, + # "l": 0.67866757035154, + # "v": 2.835000000000001e-9, + # "pair": "PERPUSD", + # "sentimentData": { + # "sentiment": 36.71333333333333, + # "interest": 0.47430830039525695 + # } + # }, + # ... + # ] + # } + # + latestPrices = self.safe_value(response, 'latestPrices', []) + twentyFourHInfos = self.safe_value(response, '24hInfo', []) + tickersObject: dict = {} + # merging info from two lists into one + for i in range(0, len(latestPrices)): + latestPrice = latestPrices[i] + marketId = self.safe_string(latestPrice, 'pair') + if marketId is not None: + tickersObject[marketId] = latestPrice + for i in range(0, len(twentyFourHInfos)): + twentyFourHInfo = twentyFourHInfos[i] + marketId = self.safe_string(twentyFourHInfo, 'pair') + if marketId is not None: + latestPrice = self.safe_value(tickersObject, marketId, {}) + tickersObject[marketId] = self.extend(twentyFourHInfo, latestPrice) + tickers = list(tickersObject.values()) + return self.parse_tickers(tickers, symbols) + + def fetch_bids_asks(self, symbols: Strings = None, params={}): + """ + fetches the bid and ask price and volume for multiple markets + + https://documenter.getpostman.com/view/3653795/SVfWN6KS#6ecd1cd1-f162-45a3-8b3b-de690332a485 + + :param str[] [symbols]: unified symbols of the markets to fetch the bids and asks for, all markets are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + response = self.publicGetExchangePrices(params) + latestPrices = self.safe_list(response, 'latestPrices', []) + return self.parse_tickers(latestPrices, symbols) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "pair": "PERPUSD", + # "timestamp": 1702549841973, + # "price": 0.8615317721366659, + # "qty": 1e-12, + # "ask": 0.8742333599999257, + # "bid": 0.8490376365388491 + # "delta": 0.26915154078134096, + # "h": 0.86220315458898, + # "l": 0.67866757035154, + # "v": 2.835000000000001e-9, + # "sentimentData": { + # "sentiment": 36.71333333333333, + # "interest": 0.47430830039525695 + # } + # } + # + marketId = self.safe_string(ticker, 'pair') + market = self.safe_market(marketId, market) + timestamp = self.safe_integer(ticker, 'timestamp') + bid = self.safe_string(ticker, 'bid') + ask = self.safe_string(ticker, 'ask') + high = self.safe_string(ticker, 'h') + low = self.safe_string(ticker, 'l') + last = self.safe_string(ticker, 'price') + baseVolume = self.safe_string(ticker, 'v') + delta = self.safe_string(ticker, 'delta') + percentage = Precise.string_mul(delta, '100') + return self.safe_ticker({ + 'symbol': market['symbol'], + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'open': None, + 'high': high, + 'low': low, + 'close': None, + 'last': last, + 'bid': bid, + 'bidVolume': None, + 'ask': ask, + 'askVolume': None, + 'vwap': None, + 'previousClose': None, + 'change': None, + 'percentage': percentage, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': None, + 'info': ticker, + }, market) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://documenter.getpostman.com/view/3653795/SVfWN6KS#741a1dcc-7307-40d0-acca-28d003d1506a + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + response = self.privateGetUsersWallets(params) + list = self.safe_list(response, 'list', []) + return self.parse_balance(list) + + def parse_balance(self, balances) -> Balances: + # + # [ + # { + # "xcmLocks": [], + # "xcmLockAmounts": [], + # "refList": [], + # "balanceHistory": [], + # "_id": "5fecd3c998e75c2e4d63f7c3", + # "currency": "BTC", + # "label": "BTC", + # "userId": "5fecd3c97fbfed1521db23bd", + # "__v": 0, + # "balance": 0.5, + # "createdAt": "2020-12-30T19:23:53.646Z", + # "disabled": False, + # "updatedAt": "2020-12-30T19:23:53.653Z", + # "reserved": 0, + # "id": "5fecd3c998e75c2e4d63f7c3" + # }, + # ... + # ] + # + result: dict = { + 'info': balances, + } + for i in range(0, len(balances)): + balanceEntry = self.safe_dict(balances, i, {}) + currencyId = self.safe_string(balanceEntry, 'currency') + code = self.safe_currency_code(currencyId) + account = self.account() + account['total'] = self.safe_string(balanceEntry, 'balance') + account['used'] = self.safe_string(balanceEntry, 'reserved') + result[code] = account + return self.safe_balance(result) + + def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + + https://documenter.getpostman.com/view/3653795/SVfWN6KS#4e7831f7-a0e7-4c3e-9336-1d0e5dcb15cf + + :param str [code]: unified currency code, default is None + :param int [since]: timestamp in ms of the earliest ledger entry, default is None + :param int [limit]: max number of ledger entries to return(default 200, max 500) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch entries for + :returns dict: a `ledger structure ` + """ + self.load_markets() + request: dict = {} + if since is not None: + request['since'] = since + else: + # self endpoint accepts empty since param + request['since'] = '' + currency = None + if code is not None: + currency = self.currency(code) + response = self.privateGetUsersWalletsHistorySince(self.extend(request, params)) + # + # { + # "list": [ + # { + # "currency": "USDC", + # "label": "USDC", + # "userId": "65671262d93d9525ac009e36", + # "balance": 0, + # "disabled": False, + # "balanceHistory": [ + # { + # "description": "Deposit - 657973a9b6eadf0f33d70100", + # "JSONdata": { + # "fees": 0, + # "notes": "Via Crypto", + # "txHash": "0x2e4875185b0f312d8e24b2d26d46bf9877db798b608ad2ff97b2b8bc7d8134e5", + # "last4Digits": null, + # "IBAN": null, + # "alternativeChain": "polygon", + # "referenceId": "657973a9b6eadf0f33d70100", + # "status": "completed", + # "tracked": True + # }, + # "amount": 99, + # "timestamp": "2023-12-13T09:04:51.270Z", + # "amountEUR": 91.79310117335974 + # }, + # { + # "description": "Order 65671262d93d9525ac009e36170257061073952c6423a8c5b4d6c SeqNum 10873722342", + # "JSONdata": { + # "price": "2282.00 ETH/USDC", + # "fees": 0, + # "notes": "Order 3a8c5b4d6c" + # }, + # "amount": -4.564, + # "timestamp": "2023-12-14T16:16:50.760Z", + # "amountEUR": -4.150043849187587 + # }, + # ... + # ] + # }, + # { + # "currency": "ETH", + # "label": "ETH", + # "userId": "65671262d93d9525ac009e36", + # "balance": 0, + # "disabled": False, + # "balanceHistory": [ + # { + # "description": "Order 65671262d93d9525ac009e36170257061073952c6423a8c5b4d6c SeqNum 10873722342", + # "JSONdata": { + # "price": "2282.00 ETH/USDC", + # "fees": 0.000002, + # "notes": "Order 3a8c5b4d6c" + # }, + # "amount": 0.001998, + # "timestamp": "2023-12-14T16:16:50.761Z", + # "amountEUR": 4.144849415806856 + # }, + # ... + # ] + # }, + # { + # "currency": "DOGE", + # "label": "DOGE", + # "userId": "65671262d93d9525ac009e36", + # "balance": 0, + # "disabled": False, + # "balanceHistory": [ + # { + # "description": "Order 65671262d93d9525ac009e361702905785319b5d9016dc20736034d13ca6a - Swap", + # "JSONdata": { + # "swap": True, + # "subtype": "swap", + # "fees": 0, + # "price": "0.0905469 DOGE/USDC", + # "notes": "Swap 034d13ca6a" + # }, + # "amount": 70, + # "timestamp": "2023-12-18T13:23:05.836Z", + # "amountEUR": 5.643627624549227 + # } + # ] + # }, + # ... + # ] + # } + # + ledgerByCurrencies = self.safe_value(response, 'list', []) + ledger = [] + for i in range(0, len(ledgerByCurrencies)): + currencyLedger = ledgerByCurrencies[i] + currencyId = self.safe_string(currencyLedger, 'currency') + balanceHistory = self.safe_value(currencyLedger, 'balanceHistory', []) + for j in range(0, len(balanceHistory)): + rawLedgerEntry = balanceHistory[j] + rawLedgerEntry['currencyId'] = currencyId + ledger.append(rawLedgerEntry) + return self.parse_ledger(ledger, currency, since, limit) + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + datetime = self.safe_string(item, 'timestamp') + currencyId = self.safe_string(item, 'currencyId') + item = self.omit(item, 'currencyId') + currency = self.safe_currency(currencyId, currency) + description = self.safe_string(item, 'description', '') + type, referenceId = self.parse_ledger_entry_description(description) + JSONdata = self.safe_value(item, 'JSONdata', {}) + feeCost = self.safe_string(JSONdata, 'fees') + fee = { + 'cost': feeCost, + 'currency': None, + } + amount = self.safe_string(item, 'amount') + direction = None + if amount is not None: + if Precise.string_lt(amount, '0'): + direction = 'out' + amount = Precise.string_abs(amount) + elif Precise.string_gt(amount, '0'): + direction = 'in' + return self.safe_ledger_entry({ + 'info': item, + 'id': None, + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + 'direction': direction, + 'account': None, + 'referenceId': referenceId, + 'referenceAccount': None, + 'type': type, + 'currency': currency, + 'amount': amount, + 'before': None, + 'after': None, + 'status': None, + 'fee': fee, + }, currency) + + def parse_ledger_entry_description(self, description): + descriptionArray = [] + if description is not None: + descriptionArray = description.split(' ') + type = None + referenceId = None + length = len(descriptionArray) + if length > 1: + type = self.parse_ledger_entry_type(descriptionArray[0]) + if descriptionArray[1] != '-': + referenceId = descriptionArray[1] + else: + referenceId = self.safe_string(descriptionArray, 2) + return [type, referenceId] + + def parse_ledger_entry_type(self, type): + types: dict = { + 'Deposit': 'transaction', + 'Withdraw': 'transaction', + 'Order': 'trade', + } + return self.safe_string(types, type, type) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://documenter.getpostman.com/view/3653795/SVfWN6KS#a4895a1d-3f50-40ae-8231-6962ef06c771 + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.cost]: the quote quantity that can be used alternative for the amount in market orders + :param str [params.timeInForce]: "GTC", "IOC", "FOK", "GTD" + :param number [params.expirationTime]: timestamp in millisecond, for GTD orders only + :param float [params.triggerPrice]: the price at which a trigger order is triggered at + :param float [params.stopLossPrice]: *margin only* The price at which a stop loss order is triggered at + :param float [params.takeProfitPrice]: *margin only* The price at which a take profit order is triggered at + :param bool [params.margin]: True for creating a margin order + :param str [params.fillStyle]: fill style of the limit order: "sell" fulfills selling quantity "buy" fulfills buying quantity "base" fulfills base currency quantity "quote" fulfills quote currency quantity + :param str [params.clientOrderId]: client's comment + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + } + request['orderType'] = type + formattedAmount = None + if amount is not None: + formattedAmount = self.amount_to_precision(symbol, amount) + cost = self.safe_value(params, 'cost') + params = self.omit(params, 'cost') + if type == 'limit': + if (price is None) and (cost is None): + raise ArgumentsRequired(self.id + ' createOrder() requires a price or params.cost argument for a ' + type + ' order') + elif (price is not None) and (amount is not None): + costString = Precise.string_mul(self.number_to_string(price), self.number_to_string(formattedAmount)) + cost = self.parse_to_numeric(costString) + precisedCost = None + if cost is not None: + precisedCost = self.cost_to_precision(symbol, cost) + if side == 'sell': + request = self.handle_create_order_side(market['baseId'], market['quoteId'], formattedAmount, precisedCost, request) + elif side == 'buy': + request = self.handle_create_order_side(market['quoteId'], market['baseId'], precisedCost, formattedAmount, request) + timeInForce = self.safe_value(params, 'timeInForce') + if timeInForce is not None: + params = self.omit(params, 'timeInForce') + request['timeInForce'] = self.encode_order_time_in_force(timeInForce) + triggerPrice = self.safe_string_2(params, 'triggerPrice', 'stopPrice') + if triggerPrice is not None: + params = self.omit(params, ['triggerPrice']) + request['stopPrice'] = self.price_to_precision(symbol, triggerPrice) + userData = self.safe_value(params, 'userData', {}) + comment = self.safe_string_2(params, 'clientOrderId', 'comment') + if comment is not None: + params = self.omit(params, ['clientOrderId']) + userData['comment'] = comment + stopLossPrice = self.safe_string(params, 'stopLossPrice') + if stopLossPrice is not None: + params = self.omit(params, 'stopLossPrice') + userData['stopLoss'] = self.price_to_precision(symbol, stopLossPrice) + takeProfitPrice = self.safe_string(params, 'takeProfitPrice') + if takeProfitPrice is not None: + params = self.omit(params, 'takeProfitPrice') + userData['takeProfit'] = self.price_to_precision(symbol, takeProfitPrice) + if not self.is_empty(userData): + request['userData'] = userData + response = self.privatePostExchangeOrdersCreate(self.extend(request, params)) + # + # { + # "userID": "65671262d93d9525ac009e36", + # "orderID": "65671262d93d9525ac009e36170257448481749b7ee2893bafec2", + # "orderType": "market", + # "buyingCurrency": "ETH", + # "sellingCurrency": "USDC", + # "buyingQty": 0.002, + # "timeInForce": 4, + # "boughtQty": 0.002, + # "soldQty": 4.587, + # "creationTime": 1702574484829, + # "seqNumber": 10874285330, + # "firstFillTime": 1702574484831, + # "lastFillTime": 1702574484831, + # "fills": [ + # { + # "seqNumber": 10874285329, + # "timestamp": 1702574484831, + # "qty": 0.002, + # "price": 2293.5, + # "side": "buy" + # } + # ], + # "completionTime": 1702574484831, + # "takerQty": 0.002 + # } + # + return self.parse_order(response, market) + + def handle_create_order_side(self, sellingCurrency, buyingCurrency, sellingQty, buyingQty, request={}): + request['sellingCurrency'] = sellingCurrency + request['buyingCurrency'] = buyingCurrency + if sellingQty is not None: + request['sellingQty'] = sellingQty + if buyingQty is not None: + request['buyingQty'] = buyingQty + return request + + def encode_order_time_in_force(self, timeInForce): + timeInForceTypes: dict = { + 'GTC': 1, + 'IOC': 2, + 'GTD': 3, + 'FOK': 4, + } + return self.safe_value(timeInForceTypes, timeInForce, timeInForce) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://documenter.getpostman.com/view/3653795/SVfWN6KS#eaea86da-16ca-4c56-9f00-5b1cb2ad89f8 + https://documenter.getpostman.com/view/3653795/SVfWN6KS#47f913fb-8cab-49f4-bc78-d980e6ced316 + + :param str id: order id + :param str symbol: not used by coinmetro cancelOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.margin]: True for cancelling a margin order + :returns dict: An `order structure ` + """ + self.load_markets() + request: dict = { + 'orderID': id, + } + marginMode = None + params, params = self.handle_margin_mode_and_params('cancelOrder', params) + isMargin = self.safe_bool(params, 'margin', False) + params = self.omit(params, 'margin') + response = None + if isMargin or (marginMode is not None): + response = self.privatePostExchangeOrdersCloseOrderID(self.extend(request, params)) + else: + response = self.privatePutExchangeOrdersCancelOrderID(self.extend(request, params)) + # + # { + # "userID": "65671262d93d9525ac009e36", + # "orderID": "65671262d93d9525ac009e3617026635256739c996fe17d7cd5d4", + # "orderType": "limit", + # "buyingCurrency": "ETH", + # "sellingCurrency": "USDC", + # "fillStyle": "sell", + # "orderPlatform": "trade-v3", + # "timeInForce": 1, + # "buyingQty": 0.005655, + # "sellingQty": 11.31, + # "boughtQty": 0, + # "soldQty": 0, + # "creationTime": 1702663525713, + # "seqNumber": 10915220048, + # "completionTime": 1702928369053 + # } + # + return self.parse_order(response) + + def close_position(self, symbol: str, side: OrderSide = None, params={}): + """ + closes an open position + + https://documenter.getpostman.com/view/3653795/SVfWN6KS#47f913fb-8cab-49f4-bc78-d980e6ced316 + + :param str symbol: not used by coinmetro closePosition() + :param str [side]: not used by coinmetro closePosition() + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.orderID]: order id + :param number [params.fraction]: fraction of order to close, between 0 and 1(defaults to 1) + :returns dict: An `order structure ` + """ + self.load_markets() + orderId = self.safe_string(params, 'orderId') + if orderId is None: + raise ArgumentsRequired(self.id + ' closePosition() requires a orderId parameter') + request: dict = { + 'orderID': orderId, + } + response = self.privatePostExchangeOrdersCloseOrderID(self.extend(request, params)) + # + # { + # "userID": "65671262d93d9525ac009e36", + # "orderID": "65671262d93d9525ac009e3617030152811996e5b352556d3d7d8_CL", + # "orderType": "market", + # "buyingCurrency": "ETH", + # "sellingCurrency": "EUR", + # "margin": True, + # "buyingQty": 0.03, + # "timeInForce": 4, + # "boughtQty": 0.03, + # "soldQty": 59.375, + # "creationTime": 1703015488482, + # "seqNumber": 10925321179, + # "firstFillTime": 1703015488483, + # "lastFillTime": 1703015488483, + # "fills": [ + # { + # "seqNumber": 10925321178, + # "timestamp": 1703015488483, + # "qty": 0.03, + # "price": 1979.1666666666667, + # "side": "buy" + # } + # ], + # "completionTime": 1703015488483, + # "takerQty": 0.03 + # } + # + return self.parse_order(response) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://documenter.getpostman.com/view/3653795/SVfWN6KS#518afd7a-4338-439c-a651-d4fdaa964138 + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + response = self.privateGetExchangeOrdersActive(params) + orders = self.parse_orders(response, market, since, limit) + for i in range(0, len(orders)): + order = orders[i] + order['status'] = 'open' + return orders + + def fetch_canceled_and_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple canceled and closed orders made by the user + + https://documenter.getpostman.com/view/3653795/SVfWN6KS#4d48ae69-8ee2-44d1-a268-71f84e557b7b + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + request: dict = {} + if since is not None: + request['since'] = since + response = self.privateGetExchangeOrdersHistorySince(self.extend(request, params)) + return self.parse_orders(response, market, since, limit) + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://documenter.getpostman.com/view/3653795/SVfWN6KS#95bbed87-db1c-47a7-a03e-aa247e91d5a6 + + :param int|str id: order id + :param str symbol: not used by coinmetro fetchOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + request: dict = { + 'orderID': id, + } + response = self.privateGetExchangeOrdersStatusOrderID(self.extend(request, params)) + # + # { + # "_id": "657b4e6d60a954244939ac6f", + # "userID": "65671262d93d9525ac009e36", + # "orderID": "65671262d93d9525ac009e361702576531985b78465468b9cc544", + # "orderType": "market", + # "buyingCurrency": "ETH", + # "sellingCurrency": "USDC", + # "buyingQty": 0.004, + # "timeInForce": 4, + # "boughtQty": 0.004, + # "soldQty": 9.236, + # "creationTime": 1702576531995, + # "seqNumber": 10874644062, + # "firstFillTime": 1702576531995, + # "lastFillTime": 1702576531995, + # "fills": [ + # { + # "_id": "657b4e6d60a954244939ac70", + # "seqNumber": 10874644061, + # "timestamp": 1702576531995, + # "qty": 0.004, + # "price": 2309, + # "side": "buy" + # } + # ], + # "completionTime": 1702576531995, + # "takerQty": 0.004, + # "fees": 0.000004, + # "isAncillary": False, + # "margin": False, + # "trade": False, + # "canceled": False + # } + # + return self.parse_order(response) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder market + # { + # "userID": "65671262d93d9525ac009e36", + # "orderID": "65671262d93d9525ac009e36170257448481749b7ee2893bafec2", + # "orderType": "market", + # "buyingCurrency": "ETH", + # "sellingCurrency": "USDC", + # "buyingQty": 0.002, + # "timeInForce": 4, + # "boughtQty": 0.002, + # "soldQty": 4.587, + # "creationTime": 1702574484829, + # "seqNumber": 10874285330, + # "firstFillTime": 1702574484831, + # "lastFillTime": 1702574484831, + # "fills": [ + # { + # "seqNumber": 10874285329, + # "timestamp": 1702574484831, + # "qty": 0.002, + # "price": 2293.5, + # "side": "buy" + # } + # ], + # "completionTime": 1702574484831, + # "takerQty": 0.002 + # } + # + # createOrder limit + # { + # "userID": "65671262d93d9525ac009e36", + # "orderID": "65671262d93d9525ac009e3617026635256739c996fe17d7cd5d4", + # "orderType": "limit", + # "buyingCurrency": "ETH", + # "sellingCurrency": "USDC", + # "fillStyle": "sell", + # "orderPlatform": "trade-v3", + # "timeInForce": 1, + # "buyingQty": 0.005655, + # "sellingQty": 11.31, + # "boughtQty": 0, + # "soldQty": 0, + # "creationTime": 1702663525713, + # "seqNumber": 10885528683, + # "fees": 0, + # "fills": [], + # "isAncillary": False, + # "margin": False, + # "trade": False + # } + # + # fetchOrders market + # { + # "userID": "65671262d93d9525ac009e36", + # "orderID": "65671262d93d9525ac009e36170257061073952c6423a8c5b4d6c", + # "orderType": "market", + # "buyingCurrency": "ETH", + # "sellingCurrency": "USDC", + # "buyingQty": 0.002, + # "timeInForce": 4, + # "boughtQty": 0.002, + # "soldQty": 4.564, + # "creationTime": 1702570610746, + # "seqNumber": 10873722344, + # "firstFillTime": 1702570610747, + # "lastFillTime": 1702570610747, + # "fills": [ + # { + # "_id": "657b31d360a9542449381bdc", + # "seqNumber": 10873722343, + # "timestamp": 1702570610747, + # "qty": 0.002, + # "price": 2282, + # "side": "buy" + # } + # ], + # "completionTime": 1702570610747, + # "takerQty": 0.002, + # "fees": 0.000002, + # "isAncillary": False, + # "margin": False, + # "trade": False, + # "canceled": False, + # "__v": 0 + # } + # + # fetchOrders margin + # { + # "userData": { + # "takeProfit": 1700, + # "stopLoss": 2100 + # }, + # "_id": "658201d060a95424499394a2", + # "seqNumber": 10925300213, + # "orderType": "limit", + # "buyingCurrency": "EUR", + # "sellingCurrency": "ETH", + # "userID": "65671262d93d9525ac009e36", + # "closedQty": 0.03, + # "sellingQty": 0.03, + # "buyingQty": 58.8, + # "creationTime": 1703015281205, + # "margin": True, + # "timeInForce": 1, + # "boughtQty": 59.31, + # "orderID": "65671262d93d9525ac009e3617030152811996e5b352556d3d7d8", + # "lastFillTime": 1703015281206, + # "soldQty": 0.03, + # "closedTime": 1703015488488, + # "closedVal": 59.375, + # "trade": True, + # "takerQty": 59.31, + # "firstFillTime": 1703015281206, + # "completionTime": 1703015281206, + # "fills": [ + # { + # "_id": "658201d060a95424499394a3", + # "seqNumber": 10925300212, + # "side": "sell", + # "price": 1977, + # "qty": 0.03, + # "timestamp": 1703015281206 + # }, + # { + # "_id": "658201d060a95424499394a4", + # "seqNumber": 10925321178, + # "timestamp": 1703015488483, + # "qty": 0.03, + # "price": 1979.1666666666667, + # "side": "buy" + # } + # ], + # "fees": 0.11875000200000001, + # "settledQtys": { + # "ETH": -0.000092842104710025 + # }, + # "isAncillary": False, + # "canceled": False + # } + # + # fetchOrder + # { + # "_id": "657b4e6d60a954244939ac6f", + # "userID": "65671262d93d9525ac009e36", + # "orderID": "65671262d93d9525ac009e361702576531985b78465468b9cc544", + # "orderType": "market", + # "buyingCurrency": "ETH", + # "sellingCurrency": "USDC", + # "buyingQty": 0.004, + # "timeInForce": 4, + # "boughtQty": 0.004, + # "soldQty": 9.236, + # "creationTime": 1702576531995, + # "seqNumber": 10874644062, + # "firstFillTime": 1702576531995, + # "lastFillTime": 1702576531995, + # "fills": [ + # { + # "_id": "657b4e6d60a954244939ac70", + # "seqNumber": 10874644061, + # "timestamp": 1702576531995, + # "qty": 0.004, + # "price": 2309, + # "side": "buy" + # } + # ], + # "completionTime": 1702576531995, + # "takerQty": 0.004, + # "fees": 0.000004, + # "isAncillary": False, + # "margin": False, + # "trade": False, + # "canceled": False + # } + # + timestamp = self.safe_integer(order, 'creationTime') + isCanceled = self.safe_value(order, 'canceled') + status = None + if isCanceled is True: + if timestamp is None: + timestamp = self.safe_integer(order, 'completionTime') # market orders with bad price gain IOC - we mark them as 'rejected'? + status = 'rejected' # these orders don't have the 'creationTime` param and have 'canceled': True + else: + status = 'canceled' + else: + status = self.safe_string(order, 'status') + order = self.omit(order, 'status') # we mark orders from fetchOpenOrders with param 'status': 'open' + type = self.safe_string(order, 'orderType') + buyingQty = self.safe_string(order, 'buyingQty') + sellingQty = self.safe_string(order, 'sellingQty') + boughtQty = self.safe_string(order, 'boughtQty') + soldQty = self.safe_string(order, 'soldQty') + if type == 'market': + if (buyingQty is None) and (boughtQty is not None) and (boughtQty != '0'): + buyingQty = boughtQty + if (sellingQty is None) and (soldQty is not None) and (soldQty != '0'): + sellingQty = soldQty + buyingCurrencyId = self.safe_string(order, 'buyingCurrency', '') + sellingCurrencyId = self.safe_string(order, 'sellingCurrency', '') + byuingIdPlusSellingId = buyingCurrencyId + sellingCurrencyId + sellingIdPlusBuyingId = sellingCurrencyId + buyingCurrencyId + side = None + marketId = None + baseAmount = buyingQty + quoteAmount = buyingQty + filled = None + cost = None + feeInBaseOrQuote = None + marketsById = self.index_by(self.markets, 'id') + if self.safe_value(marketsById, byuingIdPlusSellingId) is not None: + side = 'buy' + marketId = byuingIdPlusSellingId + quoteAmount = sellingQty + filled = boughtQty + cost = soldQty + feeInBaseOrQuote = 'base' + elif self.safe_value(marketsById, sellingIdPlusBuyingId) is not None: + side = 'sell' + marketId = sellingIdPlusBuyingId + baseAmount = sellingQty + filled = soldQty + cost = boughtQty + feeInBaseOrQuote = 'quote' + price = None + if (baseAmount is not None) and (quoteAmount is not None): + price = Precise.string_div(quoteAmount, baseAmount) + market = self.safe_market(marketId, market) + fee = None + feeCost = self.safe_string(order, 'fees') + if (feeCost is not None) and (feeInBaseOrQuote is not None): + fee = { + 'currency': market[feeInBaseOrQuote], + 'cost': feeCost, + 'rate': None, + } + trades = self.safe_value(order, 'fills', []) + userData = self.safe_value(order, 'userData', {}) + clientOrderId = self.safe_string(userData, 'comment') + takeProfitPrice = self.safe_string(userData, 'takeProfit') + stopLossPrice = self.safe_string(userData, 'stopLoss') + return self.safe_order({ + 'id': self.safe_string(order, 'orderID'), + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': self.safe_integer(order, 'lastFillTime'), + 'status': status, + 'symbol': market['symbol'], + 'type': type, + 'timeInForce': self.parse_order_time_in_force(self.safe_integer(order, 'timeInForce')), + 'side': side, + 'price': price, + 'triggerPrice': self.safe_string(order, 'stopPrice'), + 'takeProfitPrice': takeProfitPrice, + 'stopLossPrice': stopLossPrice, + 'average': None, + 'amount': baseAmount, + 'cost': cost, + 'filled': filled, + 'remaining': None, + 'fee': fee, + 'fees': None, + 'trades': trades, + 'info': order, + }, market) + + def parse_order_time_in_force(self, timeInForce): + timeInForceTypes = [ + None, + 'GTC', + 'IOC', + 'GTD', + 'FOK', + ] + return self.safe_value(timeInForceTypes, timeInForce, timeInForce) + + def borrow_cross_margin(self, code: str, amount: float, params={}): + """ + create a loan to borrow margin + + https://documenter.getpostman.com/view/3653795/SVfWN6KS#5b90b3b9-e5db-4d07-ac9d-d680a06fd110 + + :param str code: unified currency code of the currency to borrow + :param float amount: the amount to borrow + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin loan structure ` + """ + self.load_markets() + currency = self.currency(code) + currencyId = currency['id'] + request: dict = {} + request[currencyId] = self.currency_to_precision(code, amount) + response = self.privatePutUsersMarginCollateral(self.extend(request, params)) + # + # {"message": "OK"} + # + result = self.safe_value(response, 'result', {}) + transaction = self.parse_margin_loan(result, currency) + return self.extend(transaction, { + 'amount': amount, + }) + + def parse_margin_loan(self, info, currency: Currency = None): + currencyId = self.safe_string(info, 'coin') + return { + 'id': None, + 'currency': self.safe_currency_code(currencyId, currency), + 'amount': None, + 'symbol': None, + 'timestamp': None, + 'datetime': None, + 'info': info, + } + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + request = self.omit(params, self.extract_params(path)) + endpoint = '/' + self.implode_params(path, params) + url = self.urls['api'][api] + endpoint + query = self.urlencode(request) + if headers is None: + headers = {} + headers['CCXT'] = 'true' + if api == 'private': + if (self.uid is None) and (self.apiKey is not None): + self.uid = self.apiKey + if (self.token is None) and (self.secret is not None): + self.token = self.secret + if url == 'https://api.coinmetro.com/jwt': # handle with headers for login endpoint + headers['X-Device-Id'] = 'bypass' + if self.twofa is not None: + headers['X-OTP'] = self.twofa + elif url == 'https://api.coinmetro.com/jwtDevice': # handle with headers for long lived token login endpoint + headers['X-Device-Id'] = self.uid + if self.twofa is not None: + headers['X-OTP'] = self.twofa + else: + headers['Authorization'] = 'Bearer ' + self.token + if not url.startswith('https://api.coinmetro.com/open'): # if not sandbox endpoint + self.check_required_credentials() + headers['X-Device-Id'] = self.uid + if (method == 'POST') or (method == 'PUT'): + headers['Content-Type'] = 'application/x-www-form-urlencoded' + body = self.urlencode(request) + elif len(query) != 0: + url += '?' + query + while(url.endswith('/')): + url = url[0:len(url) - 1] + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None + if (code != 200) and (code != 201) and (code != 202): + feedback = self.id + ' ' + body + message = self.safe_string(response, 'message') + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) + raise ExchangeError(feedback) + return None diff --git a/ccxt/coinone.py b/ccxt/coinone.py new file mode 100644 index 0000000..3c1849e --- /dev/null +++ b/ccxt/coinone.py @@ -0,0 +1,1234 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.coinone import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currencies, DepositAddress, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import OnMaintenance +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class coinone(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(coinone, self).describe(), { + 'id': 'coinone', + 'name': 'CoinOne', + 'countries': ['KR'], # Korea + 'rateLimit': 50, + 'version': 'v2', + 'pro': False, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelOrder': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createMarketOrder': False, + 'createOrder': True, + 'createOrderWithTakeProfitAndStopLoss': False, + 'createOrderWithTakeProfitAndStopLossWs': False, + 'createPostOnlyOrder': False, + 'createReduceOnlyOrder': False, + 'createStopLimitOrder': False, + 'createStopMarketOrder': False, + 'createStopOrder': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchClosedOrders': False, # the endpoint that should return closed orders actually returns trades, https://github.com/ccxt/ccxt/pull/7067 + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': False, + 'fetchDepositAddresses': True, + 'fetchDepositAddressesByNetwork': False, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTrades': True, + 'fetchVolatilityHistory': False, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'repayMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'ws': True, + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/38003300-adc12fba-323f-11e8-8525-725f53c4a659.jpg', + 'api': { + 'rest': 'https://api.coinone.co.kr', + 'v2Public': 'https://api.coinone.co.kr/public/v2', + 'v2Private': 'https://api.coinone.co.kr/v2', + 'v2_1Private': 'https://api.coinone.co.kr/v2.1', + }, + 'www': 'https://coinone.co.kr', + 'doc': 'https://doc.coinone.co.kr', + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + }, + 'api': { + 'public': { + 'get': [ + 'orderbook', + 'ticker', + 'ticker_utc', + 'trades', + ], + }, + 'v2Public': { + 'get': [ + 'range_units', + 'markets/{quote_currency}', + 'markets/{quote_currency}/{target_currency}', + 'orderbook/{quote_currency}/{target_currency}', + 'trades/{quote_currency}/{target_currency}', + 'ticker_new/{quote_currency}', + 'ticker_new/{quote_currency}/{target_currency}', + 'ticker_utc_new/{quote_currency}', + 'ticker_utc_new/{quote_currency}/{target_currency}', + 'currencies', + 'currencies/{currency}', + 'chart/{quote_currency}/{target_currency}', + ], + }, + 'private': { + 'post': [ + 'account/deposit_address', + 'account/btc_deposit_address', + 'account/balance', + 'account/daily_balance', + 'account/user_info', + 'account/virtual_account', + 'order/cancel_all', + 'order/cancel', + 'order/limit_buy', + 'order/limit_sell', + 'order/complete_orders', + 'order/limit_orders', + 'order/order_info', + 'transaction/auth_number', + 'transaction/history', + 'transaction/krw/history', + 'transaction/btc', + 'transaction/coin', + ], + }, + 'v2Private': { + 'post': [ + 'account/balance', + 'account/deposit_address', + 'account/user_info', + 'account/virtual_account', + 'order/cancel', + 'order/limit_buy', + 'order/limit_sell', + 'order/limit_orders', + 'order/complete_orders', + 'order/query_order', + 'transaction/auth_number', + 'transaction/btc', + 'transaction/history', + 'transaction/krw/history', + ], + }, + 'v2_1Private': { + 'post': [ + 'account/balance/all', + 'account/balance', + 'account/trade_fee', + 'account/trade_fee/{quote_currency}/{target_currency}', + 'order/limit', + 'order/cancel', + 'order/cancel/all', + 'order/open_orders', + 'order/open_orders/all', + 'order/complete_orders', + 'order/complete_orders/all', + 'order/info', + 'transaction/krw/history', + 'transaction/coin/history', + 'transaction/coin/withdrawal/limit', + ], + }, + }, + 'fees': { + 'trading': { + 'tierBased': False, + 'percentage': True, + 'taker': 0.002, + 'maker': 0.002, + }, + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': False, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': False, + 'FOK': False, + 'PO': False, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': False, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, # todo implement + 'daysBack': 100000, # todo implement + 'untilDays': 100000, # todo implement + 'symbolRequired': True, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOrders': None, + 'fetchClosedOrders': None, # todo implement + 'fetchOHLCV': None, # todo implement + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + '104': OrderNotFound, + '107': BadRequest, + '108': BadSymbol, + '405': OnMaintenance, + }, + 'commonCurrencies': { + 'SOC': 'Soda Coin', + }, + }) + + def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://docs.coinone.co.kr/reference/currencies + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = self.v2PublicGetCurrencies(params) + # + # { + # "result": "success", + # "error_code": "0", + # "server_time": 1701054555578, + # "currencies": [ + # { + # "name": "Polygon", + # "symbol": "MATIC", + # "deposit_status": "normal", + # "withdraw_status": "normal", + # "deposit_confirm_count": 150, + # "max_precision": 8, + # "deposit_fee": "0.0", + # "withdrawal_min_amount": "1.0", + # "withdrawal_fee": "3.0" + # } + # ] + # } + # + result: dict = {} + currencies = self.safe_list(response, 'currencies', []) + for i in range(0, len(currencies)): + entry = currencies[i] + id = self.safe_string(entry, 'symbol') + code = self.safe_currency_code(id) + isWithdrawEnabled = self.safe_string(entry, 'withdraw_status', '') == 'normal' + isDepositEnabled = self.safe_string(entry, 'deposit_status', '') == 'normal' + type = 'crypto' if (code != 'KRW') else 'fiat' + result[code] = self.safe_currency_structure({ + 'id': id, + 'code': code, + 'info': entry, + 'name': self.safe_string(entry, 'name'), + 'active': None, + 'deposit': isDepositEnabled, + 'withdraw': isWithdrawEnabled, + 'fee': self.safe_number(entry, 'withdrawal_fee'), + 'precision': self.parse_number(self.parse_precision(self.safe_string(entry, 'max_precision'))), + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': self.safe_number(entry, 'withdrawal_min_amount'), + 'max': None, + }, + }, + 'networks': {}, + 'type': type, + }) + return result + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for coinone + + https://docs.coinone.co.kr/v1.0/reference/tickers + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + request: dict = { + 'quote_currency': 'KRW', + } + response = self.v2PublicGetTickerNewQuoteCurrency(request) + # + # { + # "result": "success", + # "error_code": "0", + # "server_time": 1701067923060, + # "tickers": [ + # { + # "quote_currency": "krw", + # "target_currency": "stg", + # "timestamp": 1701067920001, + # "high": "667.5", + # "low": "667.5", + # "first": "667.5", + # "last": "667.5", + # "quote_volume": "0.0", + # "target_volume": "0.0", + # "best_asks": [ + # { + # "price": "777.0", + # "qty": "73.9098" + # } + # ], + # "best_bids": [ + # { + # "price": "690.8", + # "qty": "40.7768" + # } + # ], + # "id": "1701067920001001" + # } + # ] + # } + # + tickers = self.safe_list(response, 'tickers', []) + result = [] + for i in range(0, len(tickers)): + entry = self.safe_value(tickers, i) + id = self.safe_string(entry, 'id') + baseId = self.safe_string_upper(entry, 'target_currency') + quoteId = self.safe_string_upper(entry, 'quote_currency') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + result.append({ + 'id': id, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': None, + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number('1e-4'), + 'price': self.parse_number('1e-4'), + 'cost': self.parse_number('1e-8'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': None, + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + 'info': entry, + }) + return result + + def parse_balance(self, response) -> Balances: + result: dict = {'info': response} + balances = self.omit(response, [ + 'errorCode', + 'result', + 'normalWallets', + ]) + currencyIds = list(balances.keys()) + for i in range(0, len(currencyIds)): + currencyId = currencyIds[i] + balance = balances[currencyId] + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(balance, 'avail') + account['total'] = self.safe_string(balance, 'balance') + result[code] = account + return self.safe_balance(result) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://docs.coinone.co.kr/v1.0/reference/v21 + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + response = self.v2PrivatePostAccountBalance(params) + return self.parse_balance(response) + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://docs.coinone.co.kr/v1.0/reference/orderbook + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'quote_currency': market['quote'], + 'target_currency': market['base'], + } + if limit is not None: + request['size'] = limit # only support 5, 10, 15, 16 + response = self.v2PublicGetOrderbookQuoteCurrencyTargetCurrency(self.extend(request, params)) + # + # { + # "result": "success", + # "error_code": "0", + # "timestamp": 1701071108673, + # "id": "1701071108673001", + # "quote_currency": "KRW", + # "target_currency": "BTC", + # "order_book_unit": "0.0", + # "bids": [ + # { + # "price": "50048000", + # "qty": "0.01080229" + # } + # ], + # "asks": [ + # { + # "price": "50058000", + # "qty": "0.00272592" + # } + # ] + # } + # + timestamp = self.safe_integer(response, 'timestamp') + return self.parse_order_book(response, market['symbol'], timestamp, 'bids', 'asks', 'price', 'qty') + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://docs.coinone.co.kr/v1.0/reference/tickers + https://docs.coinone.co.kr/v1.0/reference/ticker + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + symbols = self.market_symbols(symbols) + request: dict = { + 'quote_currency': 'KRW', + } + market = None + response = None + if symbols is not None: + first = self.safe_string(symbols, 0) + market = self.market(first) + request['quote_currency'] = market['quote'] + request['target_currency'] = market['base'] + response = self.v2PublicGetTickerNewQuoteCurrencyTargetCurrency(self.extend(request, params)) + else: + response = self.v2PublicGetTickerNewQuoteCurrency(self.extend(request, params)) + # + # { + # "result": "success", + # "error_code": "0", + # "server_time": 1701073358487, + # "tickers": [ + # { + # "quote_currency": "krw", + # "target_currency": "btc", + # "timestamp": 1701073357818, + # "high": "50543000.0", + # "low": "49945000.0", + # "first": "50487000.0", + # "last": "50062000.0", + # "quote_volume": "11349804285.3859", + # "target_volume": "226.07268994", + # "best_asks": [ + # { + # "price": "50081000.0", + # "qty": "0.18471358" + # } + # ], + # "best_bids": [ + # { + # "price": "50062000.0", + # "qty": "0.04213455" + # } + # ], + # "id": "1701073357818001" + # } + # ] + # } + # + data = self.safe_list(response, 'tickers', []) + return self.parse_tickers(data, symbols) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://docs.coinone.co.kr/v1.0/reference/ticker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'quote_currency': market['quote'], + 'target_currency': market['base'], + } + response = self.v2PublicGetTickerNewQuoteCurrencyTargetCurrency(self.extend(request, params)) + # + # { + # "result": "success", + # "error_code": "0", + # "server_time": 1701073358487, + # "tickers": [ + # { + # "quote_currency": "krw", + # "target_currency": "btc", + # "timestamp": 1701073357818, + # "high": "50543000.0", + # "low": "49945000.0", + # "first": "50487000.0", + # "last": "50062000.0", + # "quote_volume": "11349804285.3859", + # "target_volume": "226.07268994", + # "best_asks": [ + # { + # "price": "50081000.0", + # "qty": "0.18471358" + # } + # ], + # "best_bids": [ + # { + # "price": "50062000.0", + # "qty": "0.04213455" + # } + # ], + # "id": "1701073357818001" + # } + # ] + # } + # + data = self.safe_list(response, 'tickers', []) + ticker = self.safe_dict(data, 0, {}) + return self.parse_ticker(ticker, market) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "quote_currency": "krw", + # "target_currency": "btc", + # "timestamp": 1701073357818, + # "high": "50543000.0", + # "low": "49945000.0", + # "first": "50487000.0", + # "last": "50062000.0", + # "quote_volume": "11349804285.3859", + # "target_volume": "226.07268994", + # "best_asks": [ + # { + # "price": "50081000.0", + # "qty": "0.18471358" + # } + # ], + # "best_bids": [ + # { + # "price": "50062000.0", + # "qty": "0.04213455" + # } + # ], + # "id": "1701073357818001" + # } + # + timestamp = self.safe_integer(ticker, 'timestamp') + last = self.safe_string(ticker, 'last') + asks = self.safe_list(ticker, 'best_asks', []) + bids = self.safe_list(ticker, 'best_bids', []) + baseId = self.safe_string(ticker, 'target_currency') + quoteId = self.safe_string(ticker, 'quote_currency') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + return self.safe_ticker({ + 'symbol': base + '/' + quote, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': self.safe_string(bids, 'price'), + 'bidVolume': self.safe_string(bids, 'qty'), + 'ask': self.safe_string(asks, 'price'), + 'askVolume': self.safe_string(asks, 'qty'), + 'vwap': None, + 'open': self.safe_string(ticker, 'first'), + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': self.safe_string(ticker, 'target_volume'), + 'quoteVolume': self.safe_string(ticker, 'quote_volume'), + 'info': ticker, + }, market) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(public) + # + # { + # "id": "1701075265708001", + # "timestamp": 1701075265708, + # "price": "50020000", + # "qty": "0.00155177", + # "is_seller_maker": False + # } + # + # fetchMyTrades(private) + # + # { + # "timestamp": "1416561032", + # "price": "419000.0", + # "type": "bid", + # "qty": "0.001", + # "feeRate": "-0.0015", + # "fee": "-0.0000015", + # "orderId": "E84A1AC2-8088-4FA0-B093-A3BCDB9B3C85" + # } + # + timestamp = self.safe_integer(trade, 'timestamp') + market = self.safe_market(None, market) + isSellerMaker = self.safe_bool(trade, 'is_seller_maker') + side = None + if isSellerMaker is not None: + side = 'sell' if isSellerMaker else 'buy' + priceString = self.safe_string(trade, 'price') + amountString = self.safe_string(trade, 'qty') + orderId = self.safe_string(trade, 'orderId') + feeCostString = self.safe_string(trade, 'fee') + fee = None + if feeCostString is not None: + feeCostString = Precise.string_abs(feeCostString) + feeRateString = self.safe_string(trade, 'feeRate') + feeRateString = Precise.string_abs(feeRateString) + feeCurrencyCode = market['quote'] if (side == 'sell') else market['base'] + fee = { + 'cost': feeCostString, + 'currency': feeCurrencyCode, + 'rate': feeRateString, + } + return self.safe_trade({ + 'id': self.safe_string(trade, 'id'), + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'order': orderId, + 'symbol': market['symbol'], + 'type': None, + 'side': side, + 'takerOrMaker': None, + 'price': priceString, + 'amount': amountString, + 'cost': None, + 'fee': fee, + }, market) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://docs.coinone.co.kr/v1.0/reference/recent-completed-orders + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'quote_currency': market['quote'], + 'target_currency': market['base'], + } + if limit is not None: + request['size'] = min(limit, 200) + response = self.v2PublicGetTradesQuoteCurrencyTargetCurrency(self.extend(request, params)) + # + # { + # "result": "success", + # "error_code": "0", + # "server_time": 1701075315771, + # "quote_currency": "KRW", + # "target_currency": "BTC", + # "transactions": [ + # { + # "id": "1701075265708001", + # "timestamp": 1701075265708, + # "price": "50020000", + # "qty": "0.00155177", + # "is_seller_maker": False + # } + # ] + # } + # + data = self.safe_list(response, 'transactions', []) + return self.parse_trades(data, market, since, limit) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://doc.coinone.co.kr/#tag/Order-V2/operation/v2_order_limit_buy + https://doc.coinone.co.kr/#tag/Order-V2/operation/v2_order_limit_sell + + :param str symbol: unified symbol of the market to create an order in + :param str type: must be 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + if type != 'limit': + raise ExchangeError(self.id + ' createOrder() allows limit orders only') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'price': price, + 'currency': market['id'], + 'qty': amount, + } + method = 'privatePostOrder' + self.capitalize(type) + self.capitalize(side) + response = getattr(self, method)(self.extend(request, params)) + # + # { + # "result": "success", + # "errorCode": "0", + # "orderId": "8a82c561-40b4-4cb3-9bc0-9ac9ffc1d63b" + # } + # + return self.parse_order(response, market) + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrder() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'order_id': id, + 'currency': market['id'], + } + response = self.v2PrivatePostOrderQueryOrder(self.extend(request, params)) + # + # { + # "result": "success", + # "errorCode": "0", + # "orderId": "0e3019f2-1e4d-11e9-9ec7-00e04c3600d7", + # "baseCurrency": "KRW", + # "targetCurrency": "BTC", + # "price": "10011000.0", + # "originalQty": "3.0", + # "executedQty": "0.62", + # "canceledQty": "1.125", + # "remainQty": "1.255", + # "status": "partially_filled", + # "side": "bid", + # "orderedAt": 1499340941, + # "updatedAt": 1499341142, + # "feeRate": "0.002", + # "fee": "0.00124", + # "averageExecutedPrice": "10011000.0" + # } + # + return self.parse_order(response, market) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'live': 'open', + 'partially_filled': 'open', + 'partially_canceled': 'open', + 'filled': 'closed', + 'canceled': 'canceled', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder + # + # { + # "result": "success", + # "errorCode": "0", + # "orderId": "8a82c561-40b4-4cb3-9bc0-9ac9ffc1d63b" + # } + # + # fetchOrder + # + # { + # "result": "success", + # "errorCode": "0", + # "orderId": "0e3019f2-1e4d-11e9-9ec7-00e04c3600d7", + # "baseCurrency": "KRW", + # "targetCurrency": "BTC", + # "price": "10011000.0", + # "originalQty": "3.0", + # "executedQty": "0.62", + # "canceledQty": "1.125", + # "remainQty": "1.255", + # "status": "partially_filled", + # "side": "bid", + # "orderedAt": 1499340941, + # "updatedAt": 1499341142, + # "feeRate": "0.002", + # "fee": "0.00124", + # "averageExecutedPrice": "10011000.0" + # } + # + # fetchOpenOrders + # + # { + # "index": "0", + # "orderId": "68665943-1eb5-4e4b-9d76-845fc54f5489", + # "timestamp": "1449037367", + # "price": "444000.0", + # "qty": "0.3456", + # "type": "ask", + # "feeRate": "-0.0015" + # } + # + id = self.safe_string(order, 'orderId') + baseId = self.safe_string(order, 'baseCurrency') + quoteId = self.safe_string(order, 'targetCurrency') + base = None + quote = None + if baseId is not None: + base = self.safe_currency_code(baseId) + if quoteId is not None: + quote = self.safe_currency_code(quoteId) + symbol = None + if (base is not None) and (quote is not None): + symbol = base + '/' + quote + market = self.safe_market(symbol, market, '/') + timestamp = self.safe_timestamp_2(order, 'timestamp', 'updatedAt') + side = self.safe_string_2(order, 'type', 'side') + if side == 'ask': + side = 'sell' + elif side == 'bid': + side = 'buy' + remainingString = self.safe_string(order, 'remainQty') + amountString = self.safe_string_2(order, 'originalQty', 'qty') + status = self.safe_string(order, 'status') + # https://github.com/ccxt/ccxt/pull/7067 + if status == 'live': + if (remainingString is not None) and (amountString is not None): + isLessThan = Precise.string_lt(remainingString, amountString) + if isLessThan: + status = 'canceled' + status = self.parse_order_status(status) + fee = None + feeCostString = self.safe_string(order, 'fee') + if feeCostString is not None: + feeCurrencyCode = quote if (side == 'sell') else base + fee = { + 'cost': feeCostString, + 'rate': self.safe_string(order, 'feeRate'), + 'currency': feeCurrencyCode, + } + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'symbol': symbol, + 'type': 'limit', + 'timeInForce': None, + 'postOnly': None, + 'side': side, + 'price': self.safe_string(order, 'price'), + 'triggerPrice': None, + 'cost': None, + 'average': self.safe_string(order, 'averageExecutedPrice'), + 'amount': amountString, + 'filled': self.safe_string(order, 'executedQty'), + 'remaining': remainingString, + 'status': status, + 'fee': fee, + 'trades': None, + }, market) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + # The returned amount might not be same ordered amount. If an order is partially filled, the returned amount means the remaining amount. + # For the same reason, the returned amount and remaining are always same, and the returned filled and cost are always zero. + if symbol is None: + raise ExchangeError(self.id + ' fetchOpenOrders() allows fetching closed orders with a specific symbol') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'currency': market['id'], + } + response = self.privatePostOrderLimitOrders(self.extend(request, params)) + # + # { + # "result": "success", + # "errorCode": "0", + # "limitOrders": [ + # { + # "index": "0", + # "orderId": "68665943-1eb5-4e4b-9d76-845fc54f5489", + # "timestamp": "1449037367", + # "price": "444000.0", + # "qty": "0.3456", + # "type": "ask", + # "feeRate": "-0.0015" + # } + # ] + # } + # + limitOrders = self.safe_list(response, 'limitOrders', []) + return self.parse_orders(limitOrders, market, since, limit) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMyTrades() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'currency': market['id'], + } + response = self.v2PrivatePostOrderCompleteOrders(self.extend(request, params)) + # + # despite the name of the endpoint it returns trades which may have a duplicate orderId + # https://github.com/ccxt/ccxt/pull/7067 + # + # { + # "result": "success", + # "errorCode": "0", + # "completeOrders": [ + # { + # "timestamp": "1416561032", + # "price": "419000.0", + # "type": "bid", + # "qty": "0.001", + # "feeRate": "-0.0015", + # "fee": "-0.0000015", + # "orderId": "E84A1AC2-8088-4FA0-B093-A3BCDB9B3C85" + # } + # ] + # } + # + completeOrders = self.safe_list(response, 'completeOrders', []) + return self.parse_trades(completeOrders, market, since, limit) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + if symbol is None: + # eslint-disable-next-line quotes + raise ArgumentsRequired(self.id + " cancelOrder() requires a symbol argument. To cancel the order, pass a symbol argument and {'price': 12345, 'qty': 1.2345, 'is_ask': 0} in the params argument of cancelOrder.") + price = self.safe_number(params, 'price') + qty = self.safe_number(params, 'qty') + isAsk = self.safe_integer(params, 'is_ask') + if (price is None) or (qty is None) or (isAsk is None): + # eslint-disable-next-line quotes + raise ArgumentsRequired(self.id + " cancelOrder() requires {'price': 12345, 'qty': 1.2345, 'is_ask': 0} in the params argument.") + self.load_markets() + request: dict = { + 'order_id': id, + 'price': price, + 'qty': qty, + 'is_ask': isAsk, + 'currency': self.market_id(symbol), + } + response = self.v2PrivatePostOrderCancel(self.extend(request, params)) + # + # { + # "result": "success", + # "errorCode": "0" + # } + # + return self.safe_order(response) + + def fetch_deposit_addresses(self, codes: Strings = None, params={}) -> List[DepositAddress]: + """ + fetch deposit addresses for multiple currencies and chain types + :param str[]|None codes: list of unified currency codes, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `address structures ` + """ + self.load_markets() + response = self.v2PrivatePostAccountDepositAddress(params) + # + # { + # "result": "success", + # "errorCode": "0", + # "walletAddress": { + # "matic": null, + # "btc": "mnobqu4i6qMCJWDpf5UimRmr8JCvZ8FLcN", + # "xrp": null, + # "xrp_tag": "-1", + # "kava": null, + # "kava_memo": null, + # } + # } + # + walletAddress = self.safe_dict(response, 'walletAddress', {}) + keys = list(walletAddress.keys()) + result: dict = {} + for i in range(0, len(keys)): + key = keys[i] + value = walletAddress[key] + if (not value) or (value == '-1'): + continue + parts = key.split('_') + currencyId = self.safe_value(parts, 0) + secondPart = self.safe_value(parts, 1) + code = self.safe_currency_code(currencyId) + depositAddress = self.safe_value(result, code) + if depositAddress is None: + depositAddress = { + 'info': value, + 'currency': code, + 'network': None, + 'address': None, + 'tag': None, + } + address = self.safe_string(depositAddress, 'address', value) + self.check_address(address) + depositAddress['address'] = address + depositAddress['info'] = address + if (secondPart == 'tag' or secondPart == 'memo'): + depositAddress['tag'] = value + depositAddress['info'] = [address, value] + result[code] = depositAddress + return result + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + request = self.implode_params(path, params) + query = self.omit(params, self.extract_params(path)) + url = self.urls['api']['rest'] + '/' + if api == 'v2Public': + url = self.urls['api']['v2Public'] + '/' + api = 'public' + elif api == 'v2Private': + url = self.urls['api']['v2Private'] + '/' + elif api == 'v2_1Private': + url = self.urls['api']['v2_1Private'] + '/' + if api == 'public': + url += request + if query: + url += '?' + self.urlencode(query) + else: + self.check_required_credentials() + url += request + nonce = str(self.nonce()) + json = self.json(self.extend({ + 'access_token': self.apiKey, + 'nonce': nonce, + }, params)) + payload = self.string_to_base64(json) + body = payload + secret = self.secret.upper() + signature = self.hmac(self.encode(payload), self.encode(secret), hashlib.sha512) + headers = { + 'Content-Type': 'application/json', + 'X-COINONE-PAYLOAD': payload, + 'X-COINONE-SIGNATURE': signature, + } + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None # fallback to default error handler + # + # {"result":"error","error_code":"107","error_msg":"Parameter value is wrong"} + # {"result":"error","error_code":"108","error_msg":"Unknown CryptoCurrency"} + # + errorCode = self.safe_string(response, 'error_code') + if errorCode is not None and errorCode != '0': + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions, errorCode, feedback) + raise ExchangeError(feedback) # unknown message + return None diff --git a/ccxt/coinsph.py b/ccxt/coinsph.py new file mode 100644 index 0000000..90f27a6 --- /dev/null +++ b/ccxt/coinsph.py @@ -0,0 +1,2128 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.coinsph import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currencies, Currency, DepositAddress, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, TradingFeeInterface, TradingFees, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidAddress +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import OrderImmediatelyFillable +from ccxt.base.errors import DuplicateOrderId +from ccxt.base.errors import NotSupported +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.errors import BadResponse +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class coinsph(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(coinsph, self).describe(), { + 'id': 'coinsph', + 'name': 'Coins.ph', + 'countries': ['PH'], # Philippines + 'version': 'v1', + 'rateLimit': 50, # 1200 per minute + 'certified': False, + 'pro': False, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'cancelOrders': False, + 'closeAllPositions': False, + 'closePosition': False, + 'createDepositAddress': False, + 'createMarketBuyOrderWithCost': True, + 'createMarketOrderWithCost': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createOrderWithTakeProfitAndStopLoss': False, + 'createOrderWithTakeProfitAndStopLossWs': False, + 'createPostOnlyOrder': False, + 'createReduceOnlyOrder': False, + 'createStopLimitOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'deposit': True, + 'editOrder': False, + 'fetchAccounts': False, + 'fetchBalance': True, + 'fetchBidsAsks': False, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchCanceledOrders': False, + 'fetchClosedOrder': False, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDeposit': None, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchDepositWithdrawFee': False, + 'fetchDepositWithdrawFees': False, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchL3OrderBook': False, + 'fetchLedger': False, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrder': None, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrderBooks': False, + 'fetchOrders': False, + 'fetchOrderTrades': True, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchStatus': True, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': True, + 'fetchTradingFees': True, + 'fetchTradingLimits': False, + 'fetchTransactionFee': False, + 'fetchTransactionFees': False, + 'fetchTransactions': False, + 'fetchTransfers': False, + 'fetchVolatilityHistory': False, + 'fetchWithdrawal': None, + 'fetchWithdrawals': True, + 'fetchWithdrawalWhitelist': False, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'repayMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'signIn': False, + 'transfer': False, + 'withdraw': True, + 'ws': False, + }, + 'timeframes': { + '1m': '1m', + '3m': '3m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1h', + '2h': '2h', + '4h': '4h', + '6h': '6h', + '8h': '8h', + '12h': '12h', + '1d': '1d', + '3d': '3d', + '1w': '1w', + '1M': '1M', + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/225719995-48ab2026-4ddb-496c-9da7-0d7566617c9b.jpg', + 'api': { + 'public': 'https://api.pro.coins.ph', + 'private': 'https://api.pro.coins.ph', + }, + 'www': 'https://coins.ph/', + 'doc': [ + 'https://coins-docs.github.io/rest-api', + ], + 'fees': 'https://support.coins.ph/hc/en-us/sections/4407198694681-Limits-Fees', + }, + 'api': { + 'public': { + 'get': { + 'openapi/v1/ping': 1, + 'openapi/v1/time': 1, + # cost 1 if 'symbol' param defined(one market symbol) or if 'symbols' param is a list of 1-20 market symbols + # cost 20 if 'symbols' param is a list of 21-100 market symbols + # cost 40 if 'symbols' param is a list of 101 or more market symbols or if both 'symbol' and 'symbols' params are omited + 'openapi/quote/v1/ticker/24hr': {'cost': 1, 'noSymbolAndNoSymbols': 40, 'byNumberOfSymbols': [[101, 40], [21, 20], [0, 1]]}, + # cost 1 if 'symbol' param defined(one market symbol) + # cost 2 if 'symbols' param is a list of 1 or more market symbols or if both 'symbol' and 'symbols' params are omited + 'openapi/quote/v1/ticker/price': {'cost': 1, 'noSymbol': 2}, + # cost 1 if 'symbol' param defined(one market symbol) + # cost 2 if 'symbols' param is a list of 1 or more market symbols or if both 'symbol' and 'symbols' params are omited + 'openapi/quote/v1/ticker/bookTicker': {'cost': 1, 'noSymbol': 2}, + 'openapi/v1/exchangeInfo': 10, + # cost 1 if limit <= 100; 5 if limit > 100. + 'openapi/quote/v1/depth': {'cost': 1, 'byLimit': [[101, 5], [0, 1]]}, + 'openapi/quote/v1/klines': 1, # default limit 500; max 1000. + 'openapi/quote/v1/trades': 1, # default limit 500; max 1000. if limit <=0 or > 1000 then return 1000 + 'openapi/v1/pairs': 1, + 'openapi/quote/v1/avgPrice': 1, + }, + }, + 'private': { + 'get': { + 'openapi/wallet/v1/config/getall': 10, + 'openapi/wallet/v1/deposit/address': 10, + 'openapi/wallet/v1/deposit/history': 1, + 'openapi/wallet/v1/withdraw/history': 1, + 'openapi/v1/account': 10, + # cost 3 for a single symbol; 40 when the symbol parameter is omitted + 'openapi/v1/openOrders': {'cost': 3, 'noSymbol': 40}, + 'openapi/v1/asset/tradeFee': 1, + 'openapi/v1/order': 2, + # cost 10 with symbol, 40 when the symbol parameter is omitted + 'openapi/v1/historyOrders': {'cost': 10, 'noSymbol': 40}, + 'openapi/v1/myTrades': 10, + 'openapi/v1/capital/deposit/history': 1, + 'openapi/v1/capital/withdraw/history': 1, + 'openapi/v3/payment-request/get-payment-request': 1, + 'merchant-api/v1/get-invoices': 1, + 'openapi/account/v3/crypto-accounts': 1, + 'openapi/transfer/v3/transfers/{id}': 1, + }, + 'post': { + 'openapi/wallet/v1/withdraw/apply': 600, + 'openapi/v1/order/test': 1, + 'openapi/v1/order': 1, + 'openapi/v1/capital/withdraw/apply': 1, + 'openapi/v1/capital/deposit/apply': 1, + 'openapi/v3/payment-request/payment-requests': 1, + 'openapi/v3/payment-request/delete-payment-request': 1, + 'openapi/v3/payment-request/payment-request-reminder': 1, + 'openapi/v1/userDataStream': 1, + 'merchant-api/v1/invoices': 1, + 'merchant-api/v1/invoices-cancel': 1, + 'openapi/convert/v1/get-supported-trading-pairs': 1, + 'openapi/convert/v1/get-quote': 1, + 'openapi/convert/v1/accpet-quote': 1, + 'openapi/fiat/v1/support-channel': 1, + 'openapi/fiat/v1/cash-out': 1, + 'openapi/fiat/v1/history': 1, + 'openapi/migration/v4/sellorder': 1, + 'openapi/migration/v4/validate-field': 1, + 'openapi/transfer/v3/transfers': 1, + }, + 'delete': { + 'openapi/v1/order': 1, + 'openapi/v1/openOrders': 1, + 'openapi/v1/userDataStream': 1, + }, + }, + }, + 'fees': { + # todo: zero fees for USDT, ETH and BTC markets till 2023-04-02 + 'trading': { + 'feeSide': 'get', + 'tierBased': True, + 'percentage': True, + 'maker': self.parse_number('0.0025'), + 'taker': self.parse_number('0.003'), + 'tiers': { + 'taker': [ + [self.parse_number('0'), self.parse_number('0.003')], + [self.parse_number('500000'), self.parse_number('0.0027')], + [self.parse_number('1000000'), self.parse_number('0.0024')], + [self.parse_number('2500000'), self.parse_number('0.002')], + [self.parse_number('5000000'), self.parse_number('0.0018')], + [self.parse_number('10000000'), self.parse_number('0.0015')], + [self.parse_number('100000000'), self.parse_number('0.0012')], + [self.parse_number('500000000'), self.parse_number('0.0009')], + [self.parse_number('1000000000'), self.parse_number('0.0007')], + [self.parse_number('2500000000'), self.parse_number('0.0005')], + ], + 'maker': [ + [self.parse_number('0'), self.parse_number('0.0025')], + [self.parse_number('500000'), self.parse_number('0.0022')], + [self.parse_number('1000000'), self.parse_number('0.0018')], + [self.parse_number('2500000'), self.parse_number('0.0015')], + [self.parse_number('5000000'), self.parse_number('0.0012')], + [self.parse_number('10000000'), self.parse_number('0.001')], + [self.parse_number('100000000'), self.parse_number('0.0008')], + [self.parse_number('500000000'), self.parse_number('0.0007')], + [self.parse_number('1000000000'), self.parse_number('0.0006')], + [self.parse_number('2500000000'), self.parse_number('0.0005')], + ], + }, + }, + }, + 'precisionMode': TICK_SIZE, + # exchange-specific options + 'options': { + 'createMarketBuyOrderRequiresPrice': True, # True or False + 'withdraw': { + 'warning': False, + }, + 'deposit': { + 'warning': False, + }, + 'createOrder': { + 'timeInForce': 'GTC', # FOK, IOC + 'newOrderRespType': { + 'market': 'FULL', # FULL, RESULT. ACK + 'limit': 'FULL', # we change it from 'ACK' by default to 'FULL' + }, + }, + 'fetchTicker': { + 'method': 'publicGetOpenapiQuoteV1Ticker24hr', # publicGetOpenapiQuoteV1TickerPrice, publicGetOpenapiQuoteV1TickerBookTicker + }, + 'fetchTickers': { + 'method': 'publicGetOpenapiQuoteV1Ticker24hr', # publicGetOpenapiQuoteV1TickerPrice, publicGetOpenapiQuoteV1TickerBookTicker + }, + 'networks': { + # all networks: 'ETH', 'TRX', 'BSC', 'ARBITRUM', 'RON', 'BTC', 'XRP' + # you can call api privateGetOpenapiWalletV1ConfigGetall to check which network is supported for the currency + 'TRC20': 'TRX', + 'ERC20': 'ETH', + 'BEP20': 'BSC', + 'ARB': 'ARBITRUM', + }, + }, + 'features': { + 'spot': { + 'sandbox': False, + 'fetchCurrencies': { + 'private': True, + }, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': False, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': True, # todo implement + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 100000, + 'untilDays': 100000, # todo implement + 'symbolRequired': True, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 100000, + 'daysBackCanceled': 1, + 'untilDays': 100000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOHLCV': { + 'limit': 1000, + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + # https://coins-docs.github.io/errors/ + 'exceptions': { + 'exact': { + '-1000': BadRequest, # An unknown error occured while processing the request. + '-1001': BadRequest, # {"code":-1001,"msg":"Internal error."} + '-1002': AuthenticationError, # You are not authorized to execute self request. Request need API Key included in . We suggest that API Key be included in any request. + '-1003': RateLimitExceeded, # Too many requests; please use the websocket for live updates. Too many requests; current limit is %s requests per minute. Please use the websocket for live updates to avoid polling the API. Way too many requests; IP banned until %s. Please use the websocket for live updates to avoid bans. + '-1004': InvalidOrder, # {"code":-1004,"msg":"Missing required parameter \u0027symbol\u0027"} + '-1006': BadResponse, # An unexpected response was received from the message bus. Execution status unknown. OPEN API server find some exception in execute request .Please report to Customer service. + '-1007': BadResponse, # Timeout waiting for response from backend server. Send status unknown; execution status unknown. + '-1014': InvalidOrder, # Unsupported order combination. + '-1015': RateLimitExceeded, # Reach the rate limit .Please slow down your request speed. Too many new orders. Too many new orders; current limit is %s orders per %s. + '-1016': NotSupported, # This service is no longer available. + '-1020': NotSupported, # This operation is not supported. + '-1021': BadRequest, # {"code":-1021,"msg":"Timestamp for self request is outside of the recvWindow."} + '-1022': BadRequest, # {"code":-1022,"msg":"Signature for self request is not valid."} + '-1023': AuthenticationError, # Please set IP whitelist before using API. + '-1024': BadRequest, # {"code":-1024,"msg":"recvWindow is not valid."} + '-1025': BadRequest, # {"code":-1025,"msg":"recvWindow cannot be greater than 60000"} + '-1030': ExchangeError, # Business error. + '-1100': BadRequest, # Illegal characters found in a parameter. Illegal characters found in parameter ‘%s’; legal range is ‘%s’. + '-1101': BadRequest, # Too many parameters sent for self endpoint. Too many parameters; expected ‘%s’ and received ‘%s’. Duplicate values for a parameter detected. + '-1102': BadRequest, # A mandatory parameter was not sent, was empty/null, or malformed. Mandatory parameter ‘%s’ was not sent, was empty/null, or malformed. Param ‘%s’ or ‘%s’ must be sent, but both were empty/null! + '-1103': BadRequest, # An unknown parameter was sent. In BHEx Open Api , each request requires at least one parameter. {Timestamp}. + '-1104': BadRequest, # Not all sent parameters were read. Not all sent parameters were read; read ‘%s’ parameter(s) but was sent ‘%s’. + '-1105': BadRequest, # {"code":-1105,"msg":"Parameter \u0027orderId and origClientOrderId\u0027 is empty."} + '-1106': BadRequest, # A parameter was sent when not required. Parameter ‘%s’ sent when not required. + '-1111': BadRequest, # Precision is over the maximum defined for self asset. + '-1112': BadResponse, # No orders on book for symbol. + '-1114': BadRequest, # TimeInForce parameter sent when not required. + '-1115': InvalidOrder, # {"code":-1115,"msg":"Invalid timeInForce."} + '-1116': InvalidOrder, # {"code":-1116,"msg":"Invalid orderType."} + '-1117': InvalidOrder, # {"code":-1117,"msg":"Invalid side."} + '-1118': InvalidOrder, # New client order ID was empty. + '-1119': InvalidOrder, # Original client order ID was empty. + '-1120': BadRequest, # Invalid interval. + '-1121': BadSymbol, # Invalid symbol. + '-1122': InvalidOrder, # Invalid newOrderRespType. + '-1125': BadRequest, # This listenKey does not exist. + '-1127': BadRequest, # Lookup interval is too big. More than %s hours between startTime and endTime. + '-1128': BadRequest, # Combination of optional parameters invalid. + '-1130': BadRequest, # Invalid data sent for a parameter. Data sent for paramter ‘%s’ is not valid. + '-1131': InsufficientFunds, # {"code":-1131,"msg":"Balance insufficient "} + '-1132': InvalidOrder, # Order price too high. + '-1133': InvalidOrder, # Order price lower than the minimum,please check general broker info. + '-1134': InvalidOrder, # Order price decimal too long,please check general broker info. + '-1135': InvalidOrder, # Order quantity too large. + '-1136': InvalidOrder, # Order quantity lower than the minimum. + '-1137': InvalidOrder, # Order quantity decimal too long. + '-1138': InvalidOrder, # Order price exceeds permissible range. + '-1139': InvalidOrder, # Order has been filled. + '-1140': InvalidOrder, # {"code":-1140,"msg":"Transaction amount lower than the minimum."} + '-1141': DuplicateOrderId, # {"code":-1141,"msg":"Duplicate clientOrderId"} + '-1142': InvalidOrder, # {"code":-1142,"msg":"Order has been canceled"} + '-1143': OrderNotFound, # Cannot be found on order book + '-1144': InvalidOrder, # Order has been locked + '-1145': InvalidOrder, # This order type does not support cancellation + '-1146': InvalidOrder, # Order creation timeout + '-1147': InvalidOrder, # Order cancellation timeout + '-1148': InvalidOrder, # Market order amount decimal too long + '-1149': InvalidOrder, # Create order failed + '-1150': InvalidOrder, # Cancel order failed + '-1151': BadSymbol, # The trading pair is not open yet + '-1152': NotSupported, # Coming soon + '-1153': AuthenticationError, # User not exist + '-1154': BadRequest, # Invalid price type + '-1155': BadRequest, # Invalid position side + '-1156': InvalidOrder, # Order quantity invalid + '-1157': BadSymbol, # The trading pair is not available for api trading + '-1158': InvalidOrder, # create limit maker order failed + '-1159': InvalidOrder, # {"code":-1159,"msg":"STOP_LOSS/TAKE_PROFIT order is not allowed to trade immediately"} + '-1160': BadRequest, # Modify futures margin error + '-1161': BadRequest, # Reduce margin forbidden + '-2010': InvalidOrder, # {"code":-2010,"msg":"New order rejected."} + '-2013': OrderNotFound, # {"code":-2013,"msg":"Order does not exist."} + '-2011': BadRequest, # CANCEL_REJECTED + '-2014': BadRequest, # API-key format invalid. + '-2015': AuthenticationError, # {"code":-2015,"msg":"Invalid API-key, IP, or permissions for action."} + '-2016': BadResponse, # No trading window could be found for the symbol. Try ticker/24hrs instead + '-3126': InvalidOrder, # {"code":-3126,"msg":"Order price lower than 72005.93415"} + '-3127': InvalidOrder, # {"code":-3127,"msg":"Order price higher than 1523.192"} + '-4001': BadRequest, # {"code":-4001,"msg":"start time must less than end time"} + '-100011': BadSymbol, # {"code":-100011,"msg":"Not supported symbols"} + '-100012': BadSymbol, # {"code":-100012,"msg":"Parameter symbol [str] missing!"} + '-30008': InsufficientFunds, # {"code":-30008,"msg":"withdraw balance insufficient"} + '-30036': InsufficientFunds, # {"code":-30036,"msg":"Available balance not enough!"} + '403': ExchangeNotAvailable, + }, + 'broad': { + 'Unknown order sent': OrderNotFound, # The order(by either orderId, clOrdId, origClOrdId) could not be found + 'Duplicate order sent': DuplicateOrderId, # The clOrdId is already in use + 'Market is closed': BadSymbol, # The symbol is not trading + 'Account has insufficient balance for requested action': InsufficientFunds, # Not enough funds to complete the action + 'Market orders are not supported for self symbol': BadSymbol, # MARKET is not enabled on the symbol + 'Iceberg orders are not supported for self symbol': BadSymbol, # icebergQty is not enabled on the symbol + 'Stop loss orders are not supported for self symbol': BadSymbol, # STOP_LOSS is not enabled on the symbol + 'Stop loss limit orders are not supported for self symbol': BadSymbol, # STOP_LOSS_LIMIT is not enabled on the symbol + 'Take profit orders are not supported for self symbol': BadSymbol, # TAKE_PROFIT is not enabled on the symbol + 'Take profit limit orders are not supported for self symbol': BadSymbol, # TAKE_PROFIT_LIMIT is not enabled on the symbol + 'Price* QTY is zero or less': BadRequest, # price* quantity is too low + 'IcebergQty exceeds QTY': BadRequest, # icebergQty must be less than the order quantity + 'This action disabled is on self account': PermissionDenied, # Contact customer support; some actions have been disabled on the account. + 'Unsupported order combination': InvalidOrder, # The orderType, timeInForce, stopPrice, and or icebergQty combination isn’t allowed. + 'Order would trigger immediately': InvalidOrder, # The order’s stop price is not valid when compared to the last traded price. + 'Cancel order is invalid. Check origClOrdId and orderId': InvalidOrder, # No origClOrdId or orderId was sent in. + 'Order would immediately match and take': OrderImmediatelyFillable, # LIMIT_MAKER order type would immediately match and trade, and not be a pure maker order. + 'PRICE_FILTER': InvalidOrder, # price is too high, too low, and or not following the tick size rule for the symbol. + 'LOT_SIZE': InvalidOrder, # quantity is too high, too low, and or not following the step size rule for the symbol. + 'MIN_NOTIONAL': InvalidOrder, # price* quantity is too low to be a valid order for the symbol. + 'MAX_NUM_ORDERS': InvalidOrder, # Account has too many open orders on the symbol. + 'MAX_ALGO_ORDERS': InvalidOrder, # Account has too many open stop loss and or take profit orders on the symbol. + 'BROKER_MAX_NUM_ORDERS': InvalidOrder, # Account has too many open orders on the broker. + 'BROKER_MAX_ALGO_ORDERS': InvalidOrder, # Account has too many open stop loss and or take profit orders on the broker. + 'ICEBERG_PARTS': BadRequest, # Iceberg order would break into too many parts; icebergQty is too small. + }, + }, + }) + + def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://docs.coins.ph/rest-api/#all-coins-information-user_data + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + if not self.check_required_credentials(False): + return {} + response = self.privateGetOpenapiWalletV1ConfigGetall(params) + # + # [ + # { + # "coin": "PHP", + # "name": "PHP", + # "depositAllEnable": False, + # "withdrawAllEnable": False, + # "free": "0", + # "locked": "0", + # "transferPrecision": "2", + # "transferMinQuantity": "0", + # "networkList": [], + # "legalMoney": True + # }, + # { + # "coin": "USDT", + # "name": "USDT", + # "depositAllEnable": True, + # "withdrawAllEnable": True, + # "free": "0", + # "locked": "0", + # "transferPrecision": "8", + # "transferMinQuantity": "0", + # "networkList": [ + # { + # "addressRegex": "^0x[0-9a-fA-F]{40}$", + # "memoRegex": " ", + # "network": "ETH", + # "name": "Ethereum(ERC20)", + # "depositEnable": True, + # "minConfirm": "12", + # "unLockConfirm": "-1", + # "withdrawDesc": "", + # "withdrawEnable": True, + # "withdrawFee": "6", + # "withdrawIntegerMultiple": "0.000001", + # "withdrawMax": "500000", + # "withdrawMin": "10", + # "sameAddress": False + # }, + # { + # "addressRegex": "^T[0-9a-zA-Z]{33}$", + # "memoRegex": "", + # "network": "TRX", + # "name": "TRON", + # "depositEnable": True, + # "minConfirm": "19", + # "unLockConfirm": "-1", + # "withdrawDesc": "", + # "withdrawEnable": True, + # "withdrawFee": "3", + # "withdrawIntegerMultiple": "0.000001", + # "withdrawMax": "1000000", + # "withdrawMin": "20", + # "sameAddress": False + # } + # ], + # "legalMoney": False + # } + # ] + # + result: dict = {} + for i in range(0, len(response)): + entry = response[i] + id = self.safe_string(entry, 'coin') + code = self.safe_currency_code(id) + isFiat = self.safe_bool(entry, 'isLegalMoney') + networkList = self.safe_list(entry, 'networkList', []) + networks: dict = {} + for j in range(0, len(networkList)): + networkItem = networkList[j] + network = self.safe_string(networkItem, 'network') + networkCode = self.network_id_to_code(network) + networks[networkCode] = { + 'info': networkItem, + 'id': network, + 'network': networkCode, + 'active': None, + 'deposit': self.safe_bool(networkItem, 'depositEnable'), + 'withdraw': self.safe_bool(networkItem, 'withdrawEnable'), + 'fee': self.safe_number(networkItem, 'withdrawFee'), + 'precision': self.safe_number(networkItem, 'withdrawIntegerMultiple'), + 'limits': { + 'withdraw': { + 'min': self.safe_number(networkItem, 'withdrawMin'), + 'max': self.safe_number(networkItem, 'withdrawMax'), + }, + 'deposit': { + 'min': None, + 'max': None, + }, + }, + } + result[code] = self.safe_currency_structure({ + 'id': id, + 'name': self.safe_string(entry, 'name'), + 'code': code, + 'type': 'fiat' if isFiat else 'crypto', + 'precision': self.parse_number(self.parse_precision(self.safe_string(entry, 'transferPrecision'))), + 'info': entry, + 'active': None, + 'deposit': self.safe_bool(entry, 'depositAllEnable'), + 'withdraw': self.safe_bool(entry, 'withdrawAllEnable'), + 'networks': networks, + 'fee': None, + 'fees': None, + 'limits': {}, + }) + return result + + def calculate_rate_limiter_cost(self, api, method, path, params, config={}): + if ('noSymbol' in config) and not ('symbol' in params): + return config['noSymbol'] + elif ('noSymbolAndNoSymbols' in config) and not ('symbol' in params) and not ('symbols' in params): + return config['noSymbolAndNoSymbols'] + elif ('byNumberOfSymbols' in config) and ('symbols' in params): + symbols = params['symbols'] + symbolsAmount = len(symbols) + byNumberOfSymbols = config['byNumberOfSymbols'] + for i in range(0, len(byNumberOfSymbols)): + entry = byNumberOfSymbols[i] + if symbolsAmount >= entry[0]: + return entry[1] + elif ('byLimit' in config) and ('limit' in params): + limit = params['limit'] + byLimit = config['byLimit'] + for i in range(0, len(byLimit)): + entry = byLimit[i] + if limit >= entry[0]: + return entry[1] + return self.safe_value(config, 'cost', 1) + + def fetch_status(self, params={}): + """ + the latest known information on the availability of the exchange API + + https://coins-docs.github.io/rest-api/#test-connectivity + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `status structure ` + """ + response = self.publicGetOpenapiV1Ping(params) + return { + 'status': 'ok', # if there's no Errors, status = 'ok' + 'updated': None, + 'eta': None, + 'url': None, + 'info': response, + } + + def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://coins-docs.github.io/rest-api/#check-server-time + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = self.publicGetOpenapiV1Time(params) + # + # {"serverTime":1677705408268} + # + return self.safe_integer(response, 'serverTime') + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for coinsph + + https://coins-docs.github.io/rest-api/#exchange-information + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = self.publicGetOpenapiV1ExchangeInfo(params) + # + # { + # "timezone": "UTC", + # "serverTime": "1677449496897", + # "exchangeFilters": [], + # "symbols": [ + # { + # "symbol": "XRPPHP", + # "status": "TRADING", + # "baseAsset": "XRP", + # "baseAssetPrecision": "2", + # "quoteAsset": "PHP", + # "quoteAssetPrecision": "4", + # "orderTypes": [ + # "LIMIT", + # "MARKET", + # "LIMIT_MAKER", + # "STOP_LOSS_LIMIT", + # "STOP_LOSS", + # "TAKE_PROFIT_LIMIT", + # "TAKE_PROFIT" + # ], + # "filters": [ + # { + # "minPrice": "0.01", + # "maxPrice": "99999999.00000000", + # "tickSize": "0.01", + # "filterType": "PRICE_FILTER" + # }, + # { + # "minQty": "0.01", + # "maxQty": "99999999999.00000000", + # "stepSize": "0.01", + # "filterType": "LOT_SIZE" + # }, + # {minNotional: "50", filterType: "NOTIONAL"}, + # {minNotional: "50", filterType: "MIN_NOTIONAL"}, + # { + # "priceUp": "99999999", + # "priceDown": "0.01", + # "filterType": "STATIC_PRICE_RANGE" + # }, + # { + # "multiplierUp": "1.1", + # "multiplierDown": "0.9", + # "filterType": "PERCENT_PRICE_INDEX" + # }, + # { + # "multiplierUp": "1.1", + # "multiplierDown": "0.9", + # "filterType": "PERCENT_PRICE_ORDER_SIZE" + # }, + # {maxNumOrders: "200", filterType: "MAX_NUM_ORDERS"}, + # {maxNumAlgoOrders: "5", filterType: "MAX_NUM_ALGO_ORDERS"} + # ] + # }, + # ] + # } + # + markets = self.safe_list(response, 'symbols', []) + result = [] + for i in range(0, len(markets)): + market = markets[i] + id = self.safe_string(market, 'symbol') + baseId = self.safe_string(market, 'baseAsset') + quoteId = self.safe_string(market, 'quoteAsset') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + limits = self.index_by(self.safe_list(market, 'filters', []), 'filterType') + amountLimits = self.safe_value(limits, 'LOT_SIZE', {}) + priceLimits = self.safe_value(limits, 'PRICE_FILTER', {}) + costLimits = self.safe_value(limits, 'NOTIONAL', {}) + result.append({ + 'id': id, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': self.safe_string_lower(market, 'status') == 'trading', + 'contract': False, + 'linear': None, + 'inverse': None, + 'taker': None, + 'maker': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(self.safe_string(amountLimits, 'stepSize')), + 'price': self.parse_number(self.safe_string(priceLimits, 'tickSize')), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.parse_number(self.safe_string(amountLimits, 'minQty')), + 'max': self.parse_number(self.safe_string(amountLimits, 'maxQty')), + }, + 'price': { + 'min': self.parse_number(self.safe_string(priceLimits, 'minPrice')), + 'max': self.parse_number(self.safe_string(priceLimits, 'maxPrice')), + }, + 'cost': { + 'min': self.parse_number(self.safe_string(costLimits, 'minNotional')), + 'max': None, + }, + }, + 'created': None, + 'info': market, + }) + self.set_markets(result) + return result + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://coins-docs.github.io/rest-api/#24hr-ticker-price-change-statistics + https://coins-docs.github.io/rest-api/#symbol-price-ticker + https://coins-docs.github.io/rest-api/#symbol-order-book-ticker + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + request: dict = {} + if symbols is not None: + ids = [] + for i in range(0, len(symbols)): + market = self.market(symbols[i]) + id = market['id'] + ids.append(id) + request['symbols'] = ids + defaultMethod = 'publicGetOpenapiQuoteV1Ticker24hr' + options = self.safe_dict(self.options, 'fetchTickers', {}) + method = self.safe_string(options, 'method', defaultMethod) + tickers = None + if method == 'publicGetOpenapiQuoteV1TickerPrice': + tickers = self.publicGetOpenapiQuoteV1TickerPrice(self.extend(request, params)) + elif method == 'publicGetOpenapiQuoteV1TickerBookTicker': + tickers = self.publicGetOpenapiQuoteV1TickerBookTicker(self.extend(request, params)) + else: + tickers = self.publicGetOpenapiQuoteV1Ticker24hr(self.extend(request, params)) + return self.parse_tickers(tickers, symbols, params) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://coins-docs.github.io/rest-api/#24hr-ticker-price-change-statistics + https://coins-docs.github.io/rest-api/#symbol-price-ticker + https://coins-docs.github.io/rest-api/#symbol-order-book-ticker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + defaultMethod = 'publicGetOpenapiQuoteV1Ticker24hr' + options = self.safe_dict(self.options, 'fetchTicker', {}) + method = self.safe_string(options, 'method', defaultMethod) + ticker = None + if method == 'publicGetOpenapiQuoteV1TickerPrice': + ticker = self.publicGetOpenapiQuoteV1TickerPrice(self.extend(request, params)) + elif method == 'publicGetOpenapiQuoteV1TickerBookTicker': + ticker = self.publicGetOpenapiQuoteV1TickerBookTicker(self.extend(request, params)) + else: + ticker = self.publicGetOpenapiQuoteV1Ticker24hr(self.extend(request, params)) + return self.parse_ticker(ticker, market) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # publicGetOpenapiQuoteV1Ticker24hr + # { + # "symbol": "ETHUSDT", + # "priceChange": "41.440000000000000000", + # "priceChangePercent": "0.0259", + # "weightedAvgPrice": "1631.169825783972125436", + # "prevClosePrice": "1601.520000000000000000", + # "lastPrice": "1642.96", + # "lastQty": "0.000001000000000000", + # "bidPrice": "1638.790000000000000000", + # "bidQty": "0.280075000000000000", + # "askPrice": "1647.340000000000000000", + # "askQty": "0.165183000000000000", + # "openPrice": "1601.52", + # "highPrice": "1648.28", + # "lowPrice": "1601.52", + # "volume": "0.000287", + # "quoteVolume": "0.46814574", + # "openTime": "1677417000000", + # "closeTime": "1677503415200", + # "firstId": "1364680572697591809", + # "lastId": "1365389809203560449", + # "count": "100" + # } + # + # publicGetOpenapiQuoteV1TickerPrice + # {"symbol": "ETHUSDT", "price": "1599.68"} + # + # publicGetOpenapiQuoteV1TickerBookTicker + # { + # "symbol": "ETHUSDT", + # "bidPrice": "1596.57", + # "bidQty": "0.246405", + # "askPrice": "1605.12", + # "askQty": "0.242681" + # } + # + marketId = self.safe_string(ticker, 'symbol') + market = self.safe_market(marketId, market) + timestamp = self.safe_integer(ticker, 'closeTime') + bid = self.safe_string(ticker, 'bidPrice') + ask = self.safe_string(ticker, 'askPrice') + bidVolume = self.safe_string(ticker, 'bidQty') + askVolume = self.safe_string(ticker, 'askQty') + baseVolume = self.safe_string(ticker, 'volume') + quoteVolume = self.safe_string(ticker, 'quoteVolume') + open = self.safe_string(ticker, 'openPrice') + high = self.safe_string(ticker, 'highPrice') + low = self.safe_string(ticker, 'lowPrice') + prevClose = self.safe_string(ticker, 'prevClosePrice') + vwap = self.safe_string(ticker, 'weightedAvgPrice') + changeValue = self.safe_string(ticker, 'priceChange') + changePcnt = self.safe_string(ticker, 'priceChangePercent') + changePcnt = Precise.string_mul(changePcnt, '100') + return self.safe_ticker({ + 'symbol': market['symbol'], + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'open': open, + 'high': high, + 'low': low, + 'close': self.safe_string_2(ticker, 'lastPrice', 'price'), + 'bid': bid, + 'bidVolume': bidVolume, + 'ask': ask, + 'askVolume': askVolume, + 'vwap': vwap, + 'previousClose': prevClose, + 'change': changeValue, + 'percentage': changePcnt, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'info': ticker, + }, market) + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://coins-docs.github.io/rest-api/#order-book + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return(default 100, max 200) + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['limit'] = limit + response = self.publicGetOpenapiQuoteV1Depth(self.extend(request, params)) + # + # { + # "lastUpdateId": "1667022157000699400", + # "bids": [ + # ['1651.810000000000000000', '0.214556000000000000'], + # ['1651.730000000000000000', '0.257343000000000000'], + # ], + # "asks": [ + # ['1660.510000000000000000', '0.299092000000000000'], + # ['1660.600000000000000000', '0.253667000000000000'], + # ] + # } + # + orderbook = self.parse_order_book(response, symbol) + orderbook['nonce'] = self.safe_integer(response, 'lastUpdateId') + return orderbook + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://coins-docs.github.io/rest-api/#klinecandlestick-data + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch(default 500, max 1000) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest candle to fetch + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + market = self.market(symbol) + interval = self.safe_string(self.timeframes, timeframe) + until = self.safe_integer(params, 'until') + request: dict = { + 'symbol': market['id'], + 'interval': interval, + } + if limit is None: + limit = 1000 + if since is not None: + request['startTime'] = since + # since work properly only when it is "younger" than last "limit" candle + if until is not None: + request['endTime'] = until + else: + duration = self.parse_timeframe(timeframe) * 1000 + endTimeByLimit = self.sum(since, duration * (limit - 1)) + now = self.milliseconds() + request['endTime'] = min(endTimeByLimit, now) + elif until is not None: + request['endTime'] = until + # since work properly only when it is "younger" than last "limit" candle + duration = self.parse_timeframe(timeframe) * 1000 + request['startTime'] = until - (duration * (limit - 1)) + request['limit'] = limit + params = self.omit(params, 'until') + response = self.publicGetOpenapiQuoteV1Klines(self.extend(request, params)) + # + # [ + # [ + # 1499040000000, # Open time + # "0.01634790", # Open + # "0.80000000", # High + # "0.01575800", # Low + # "0.01577100", # Close + # "148976.11427815", # Volume + # 1499644799999, # Close time + # "2434.19055334", # Quote asset volume + # 308, # Number of trades + # "1756.87402397", # Taker buy base asset volume + # "28.46694368" # Taker buy quote asset volume + # ] + # ] + # + return self.parse_ohlcvs(response, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + return [ + self.safe_integer(ohlcv, 0), + self.safe_number(ohlcv, 1), + self.safe_number(ohlcv, 2), + self.safe_number(ohlcv, 3), + self.safe_number(ohlcv, 4), + self.safe_number(ohlcv, 5), + ] + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://coins-docs.github.io/rest-api/#recent-trades-list + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch(default 500, max 1000) + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if since is not None: + # since work properly only when it is "younger" than last 'limit' trade + request['limit'] = 1000 + else: + if limit is not None: + request['limit'] = limit + response = self.publicGetOpenapiQuoteV1Trades(self.extend(request, params)) + # + # [ + # { + # "price": "89685.8", + # "id": "1365561108437680129", + # "qty": "0.000004", + # "quoteQty": "0.000004000000000000", + # "time": "1677523569575", + # "isBuyerMaker": False, + # "isBestMatch": True + # }, + # ] + # + return self.parse_trades(response, market, since, limit) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://coins-docs.github.io/rest-api/#account-trade-list-user_data + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve(default 500, max 1000) + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMyTrades() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if since is not None: + request['startTime'] = since + # since work properly only when it is "younger" than last 'limit' trade + request['limit'] = 1000 + elif limit is not None: + request['limit'] = limit + response = self.privateGetOpenapiV1MyTrades(self.extend(request, params)) + return self.parse_trades(response, market, since, limit) + + def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all the trades made from a single order + + https://coins-docs.github.io/rest-api/#account-trade-list-user_data + + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrderTrades() requires a symbol argument') + request: dict = { + 'orderId': id, + } + return self.fetch_my_trades(symbol, since, limit, self.extend(request, params)) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades + # { + # "price": "89685.8", + # "id": "1365561108437680129", + # "qty": "0.000004", + # "quoteQty": "0.000004000000000000", # warning: report to exchange - self is not quote quantity, self is base quantity + # "time": "1677523569575", + # "isBuyerMaker": False, + # "isBestMatch": True + # }, + # + # fetchMyTrades + # { + # "symbol": "ETHUSDT", + # "id": 1375426310524125185, + # "orderId": 1375426310415879614, + # "price": "1580.91", + # "qty": "0.01", + # "quoteQty": "15.8091", + # "commission": "0", + # "commissionAsset": "USDT", + # "time": 1678699593307, + # "isBuyer": False, + # "isMaker":false, + # "isBestMatch":false + # } + # + # createOrder + # { + # "price": "1579.51", + # "qty": "0.001899", + # "commission": "0", + # "commissionAsset": "ETH", + # "tradeId":1375445992035598337 + # } + # + marketId = self.safe_string(trade, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + id = self.safe_string_2(trade, 'id', 'tradeId') + orderId = self.safe_string(trade, 'orderId') + timestamp = self.safe_integer(trade, 'time') + priceString = self.safe_string(trade, 'price') + amountString = self.safe_string(trade, 'qty') + type = None + fee = None + feeCost = self.safe_string(trade, 'commission') + if feeCost is not None: + feeCurrencyId = self.safe_string(trade, 'commissionAsset') + fee = { + 'cost': feeCost, + 'currency': self.safe_currency_code(feeCurrencyId), + } + isBuyer = self.safe_bool_2(trade, 'isBuyer', 'isBuyerMaker', None) + side = None + if isBuyer is not None: + side = 'buy' if (isBuyer is True) else 'sell' + isMaker = self.safe_string_2(trade, 'isMaker', None) + takerOrMaker = None + if isMaker is not None: + takerOrMaker = 'maker' if (isMaker == 'true') else 'taker' + costString = None + if orderId is not None: + costString = self.safe_string(trade, 'quoteQty') + return self.safe_trade({ + 'id': id, + 'order': orderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'type': type, + 'side': side, + 'takerOrMaker': takerOrMaker, + 'price': priceString, + 'amount': amountString, + 'cost': costString, + 'fee': fee, + 'info': trade, + }, market) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://coins-docs.github.io/rest-api/#accept-the-quote + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + response = self.privateGetOpenapiV1Account(params) + # + # { + # "accountType": "SPOT", + # "balances": [ + # { + # "asset": "BTC", + # "free": "4723846.89208129", + # "locked": "0.00000000" + # }, + # { + # "asset": "LTC", + # "free": "4763368.68006011", + # "locked": "0.00000000" + # } + # ], + # "canDeposit": True, + # "canTrade": True, + # "canWithdraw": True, + # "updateTime": "1677430932528" + # } + # + return self.parse_balance(response) + + def parse_balance(self, response) -> Balances: + balances = self.safe_list(response, 'balances', []) + result: dict = { + 'info': response, + 'timestamp': None, + 'datetime': None, + } + for i in range(0, len(balances)): + balance = balances[i] + currencyId = self.safe_string(balance, 'asset') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(balance, 'free') + account['used'] = self.safe_string(balance, 'locked') + result[code] = account + return self.safe_balance(result) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://coins-docs.github.io/rest-api/#new-order--trade + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market', 'limit', 'stop_loss', 'take_profit', 'stop_loss_limit', 'take_profit_limit' or 'limit_maker' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.cost]: the quote quantity that can be used alternative for the amount for market buy orders + :param bool [params.test]: set to True to test an order, no order will be created but the request will be validated + :returns dict: an `order structure ` + """ + # todo: add test order low priority + self.load_markets() + market = self.market(symbol) + testOrder = self.safe_bool(params, 'test', False) + params = self.omit(params, 'test') + orderType = self.safe_string(params, 'type', type) + orderType = self.encode_order_type(orderType) + params = self.omit(params, 'type') + orderSide = self.encode_order_side(side) + request: dict = { + 'symbol': market['id'], + 'type': orderType, + 'side': orderSide, + } + options = self.safe_value(self.options, 'createOrder', {}) + newOrderRespType = self.safe_value(options, 'newOrderRespType', {}) + # if limit order + if orderType == 'LIMIT' or orderType == 'STOP_LOSS_LIMIT' or orderType == 'TAKE_PROFIT_LIMIT' or orderType == 'LIMIT_MAKER': + if price is None: + raise ArgumentsRequired(self.id + ' createOrder() requires a price argument for a ' + type + ' order') + newOrderRespType = self.safe_string(newOrderRespType, 'limit', 'FULL') + request['price'] = self.price_to_precision(symbol, price) + request['quantity'] = self.amount_to_precision(symbol, amount) + if orderType != 'LIMIT_MAKER': + request['timeInForce'] = self.safe_string(options, 'timeInForce', 'GTC') + # if market order + elif orderType == 'MARKET' or orderType == 'STOP_LOSS' or orderType == 'TAKE_PROFIT': + newOrderRespType = self.safe_string(newOrderRespType, 'market', 'FULL') + if orderSide == 'SELL': + request['quantity'] = self.amount_to_precision(symbol, amount) + elif orderSide == 'BUY': + quoteAmount = None + createMarketBuyOrderRequiresPrice = True + createMarketBuyOrderRequiresPrice, params = self.handle_option_and_params(params, 'createOrder', 'createMarketBuyOrderRequiresPrice', True) + cost = self.safe_number_2(params, 'cost', 'quoteOrderQty') + params = self.omit(params, 'cost') + if cost is not None: + quoteAmount = self.cost_to_precision(symbol, cost) + elif createMarketBuyOrderRequiresPrice: + if price is None: + raise InvalidOrder(self.id + ' createOrder() requires the price argument for market buy orders to calculate the total cost to spend(amount * price), alternatively set the createMarketBuyOrderRequiresPrice option or param to False and pass the cost to spend in the amount argument') + else: + amountString = self.number_to_string(amount) + priceString = self.number_to_string(price) + costRequest = Precise.string_mul(amountString, priceString) + quoteAmount = self.cost_to_precision(symbol, costRequest) + else: + quoteAmount = self.cost_to_precision(symbol, amount) + request['quoteOrderQty'] = quoteAmount + if orderType == 'STOP_LOSS' or orderType == 'STOP_LOSS_LIMIT' or orderType == 'TAKE_PROFIT' or orderType == 'TAKE_PROFIT_LIMIT': + triggerPrice = self.safe_string_2(params, 'triggerPrice', 'stopPrice') + if triggerPrice is None: + raise InvalidOrder(self.id + ' createOrder() requires a triggerPrice or stopPrice param for stop_loss, take_profit, stop_loss_limit, and take_profit_limit orders') + request['stopPrice'] = self.price_to_precision(symbol, triggerPrice) + request['newOrderRespType'] = newOrderRespType + params = self.omit(params, 'price', 'stopPrice', 'triggerPrice', 'quantity', 'quoteOrderQty') + response = None + if testOrder: + response = self.privatePostOpenapiV1OrderTest(self.extend(request, params)) + else: + response = self.privatePostOpenapiV1Order(self.extend(request, params)) + # + # { + # "symbol": "ETHUSDT", + # "orderId": "1375407140139731486", + # "clientOrderId": "1375407140139733169", + # "transactTime": "1678697308023", + # "price": "1600", + # "origQty": "0.02", + # "executedQty": "0.02", + # "cummulativeQuoteQty": "31.9284", + # "status": "FILLED", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "side": "BUY", + # "stopPrice": "0", + # "origQuoteOrderQty": "0", + # "fills": [ + # { + # "price": "1596.42", + # "qty": "0.02", + # "commission": "0", + # "commissionAsset": "ETH", + # "tradeId": "1375407140281532417" + # } + # ] + # }, + # + return self.parse_order(response, market) + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://coins-docs.github.io/rest-api/#query-order-user_data + + :param int|str id: order id + :param str symbol: not used by coinsph fetchOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + request: dict = {} + clientOrderId = self.safe_value_2(params, 'origClientOrderId', 'clientOrderId') + if clientOrderId is not None: + request['origClientOrderId'] = clientOrderId + else: + request['orderId'] = id + params = self.omit(params, ['clientOrderId', 'origClientOrderId']) + response = self.privateGetOpenapiV1Order(self.extend(request, params)) + return self.parse_order(response) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://coins-docs.github.io/rest-api/#current-open-orders-user_data + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + market = None + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + response = self.privateGetOpenapiV1OpenOrders(self.extend(request, params)) + return self.parse_orders(response, market, since, limit) + + def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://coins-docs.github.io/rest-api/#history-orders-user_data + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve(default 500, max 1000) + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchClosedOrders() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if since is not None: + request['startTime'] = since + # since work properly only when it is "younger" than last 'limit' order + request['limit'] = 1000 + elif limit is not None: + request['limit'] = limit + response = self.privateGetOpenapiV1HistoryOrders(self.extend(request, params)) + return self.parse_orders(response, market, since, limit) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://coins-docs.github.io/rest-api/#cancel-order-trade + + :param str id: order id + :param str symbol: not used by coinsph cancelOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + request: dict = {} + clientOrderId = self.safe_value_2(params, 'origClientOrderId', 'clientOrderId') + if clientOrderId is not None: + request['origClientOrderId'] = clientOrderId + else: + request['orderId'] = id + params = self.omit(params, ['clientOrderId', 'origClientOrderId']) + response = self.privateDeleteOpenapiV1Order(self.extend(request, params)) + return self.parse_order(response) + + def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel open orders of market + + https://coins-docs.github.io/rest-api/#cancel-all-open-orders-on-a-symbol-trade + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelAllOrders() requires a symbol argument') + self.load_markets() + market = None + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + response = self.privateDeleteOpenapiV1OpenOrders(self.extend(request, params)) + return self.parse_orders(response, market) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder POST /openapi/v1/order + # { + # "symbol": "ETHUSDT", + # "orderId": 1375445991893797391, + # "clientOrderId": "1375445991893799115", + # "transactTime": 1678701939513, + # "price": "0", + # "origQty": "0", + # "executedQty": "0.001899", + # "cummulativeQuoteQty": "2.99948949", + # "status": "FILLED", + # "timeInForce": "GTC", + # "type": "MARKET", + # "side": "BUY", + # "stopPrice": "0", + # "origQuoteOrderQty": "3", + # "fills": [ + # { + # "price": "1579.51", + # "qty": "0.001899", + # "commission": "0", + # "commissionAsset": "ETH", + # "tradeId":1375445992035598337 + # } + # ] + # } + # + # fetchOrder GET /openapi/v1/order + # fetchOpenOrders GET /openapi/v1/openOrders + # fetchClosedOrders GET /openapi/v1/historyOrders + # cancelAllOrders DELETE /openapi/v1/openOrders + # { + # "symbol": "DOGEPHP", + # "orderId":1375465375097982423, + # "clientOrderId": "1375465375098001241", + # "price": "0", + # "origQty": "0", + # "executedQty": "13", + # "cummulativeQuoteQty": "49.621", + # "status": "FILLED", + # "timeInForce": "GTC", + # "type": "MARKET", + # "side": "BUY", + # "stopPrice": "0", + # "time":1678704250171, + # "updateTime":1678704250256, + # "isWorking":false, + # "origQuoteOrderQty": "50" + # } + # + # cancelOrder DELETE /openapi/v1/order + # { + # "symbol": "ETHPHP", + # "orderId":1375609441915774332, + # "clientOrderId": "1375609441915899557", + # "price": "96000", + # "origQty": "0.001", + # "executedQty": "0", + # "cummulativeQuoteQty": "0", + # "status": "CANCELED", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "side": "SELL", + # "stopPrice": "0", + # "origQuoteOrderQty": "0" + # } + # + id = self.safe_string(order, 'orderId') + marketId = self.safe_string(order, 'symbol') + market = self.safe_market(marketId, market) + timestamp = self.safe_integer_2(order, 'time', 'transactTime') + trades = self.safe_value(order, 'fills', None) + triggerPrice = self.safe_string(order, 'stopPrice') + if Precise.string_eq(triggerPrice, '0'): + triggerPrice = None + return self.safe_order({ + 'id': id, + 'clientOrderId': self.safe_string(order, 'clientOrderId'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'status': self.parse_order_status(self.safe_string(order, 'status')), + 'symbol': market['symbol'], + 'type': self.parse_order_type(self.safe_string(order, 'type')), + 'timeInForce': self.parse_order_time_in_force(self.safe_string(order, 'timeInForce')), + 'side': self.parse_order_side(self.safe_string(order, 'side')), + 'price': self.safe_string(order, 'price'), + 'triggerPrice': triggerPrice, + 'average': None, + 'amount': self.safe_string(order, 'origQty'), + 'cost': self.safe_string(order, 'cummulativeQuoteQty'), + 'filled': self.safe_string(order, 'executedQty'), + 'remaining': None, + 'fee': None, + 'fees': None, + 'trades': trades, + 'info': order, + }, market) + + def parse_order_side(self, status): + statuses: dict = { + 'BUY': 'buy', + 'SELL': 'sell', + } + return self.safe_string(statuses, status, status) + + def encode_order_side(self, status): + statuses: dict = { + 'buy': 'BUY', + 'sell': 'SELL', + } + return self.safe_string(statuses, status, status) + + def parse_order_type(self, status): + statuses: dict = { + 'MARKET': 'market', + 'LIMIT': 'limit', + 'LIMIT_MAKER': 'limit', + 'STOP_LOSS': 'market', + 'STOP_LOSS_LIMIT': 'limit', + 'TAKE_PROFIT': 'market', + 'TAKE_PROFIT_LIMIT': 'limit', + } + return self.safe_string(statuses, status, status) + + def encode_order_type(self, status): + statuses: dict = { + 'market': 'MARKET', + 'limit': 'LIMIT', + 'limit_maker': 'LIMIT_MAKER', + 'stop_loss': 'STOP_LOSS', + 'stop_loss_limit': 'STOP_LOSS_LIMIT', + 'take_profit': 'TAKE_PROFIT', + 'take_profit_limit': 'TAKE_PROFIT_LIMIT', + } + return self.safe_string(statuses, status, status) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'NEW': 'open', + 'FILLED': 'closed', + 'CANCELED': 'canceled', + 'PARTIALLY_FILLED': 'open', + 'PARTIALLY_CANCELED': 'canceled', + 'REJECTED': 'rejected', + } + return self.safe_string(statuses, status, status) + + def parse_order_time_in_force(self, status): + statuses: dict = { + 'GTC': 'GTC', + 'FOK': 'FOK', + 'IOC': 'IOC', + } + return self.safe_string(statuses, status, status) + + def fetch_trading_fee(self, symbol: str, params={}) -> TradingFeeInterface: + """ + fetch the trading fees for a market + + https://coins-docs.github.io/rest-api/#trade-fee-user_data + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `fee structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = self.privateGetOpenapiV1AssetTradeFee(self.extend(request, params)) + # + # [ + # { + # "symbol": "ETHUSDT", + # "makerCommission": "0.0025", + # "takerCommission": "0.003" + # } + # ] + # + tradingFee = self.safe_dict(response, 0, {}) + return self.parse_trading_fee(tradingFee, market) + + def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://coins-docs.github.io/rest-api/#trade-fee-user_data + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + self.load_markets() + response = self.privateGetOpenapiV1AssetTradeFee(params) + # + # [ + # { + # "symbol": "ETHPHP", + # "makerCommission": "0.0025", + # "takerCommission": "0.003" + # }, + # { + # "symbol": "UNIPHP", + # "makerCommission": "0.0025", + # "takerCommission": "0.003" + # }, + # ] + # + result: dict = {} + for i in range(0, len(response)): + fee = self.parse_trading_fee(response[i]) + symbol = fee['symbol'] + result[symbol] = fee + return result + + def parse_trading_fee(self, fee: dict, market: Market = None) -> TradingFeeInterface: + # + # { + # "symbol": "ETHUSDT", + # "makerCommission": "0.0025", + # "takerCommission": "0.003" + # } + # + marketId = self.safe_string(fee, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + return { + 'info': fee, + 'symbol': symbol, + 'maker': self.safe_number(fee, 'makerCommission'), + 'taker': self.safe_number(fee, 'takerCommission'), + 'percentage': None, + 'tierBased': None, + } + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal to coins_ph account + + https://coins-docs.github.io/rest-api/#withdrawuser_data + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: not used by coinsph withdraw() + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + options = self.safe_value(self.options, 'withdraw') + warning = self.safe_bool(options, 'warning', True) + if warning: + raise InvalidAddress(self.id + " withdraw() makes a withdrawals only to coins_ph account, add .options['withdraw']['warning'] = False to make a withdrawal to your coins_ph account") + networkCode = self.safe_string(params, 'network') + networkId = self.network_code_to_id(networkCode, code) + if networkId is None: + raise BadRequest(self.id + ' withdraw() require network parameter') + self.load_markets() + currency = self.currency(code) + request: dict = { + 'coin': currency['id'], + 'amount': self.number_to_string(amount), + 'network': networkId, + 'address': address, + } + if tag is not None: + request['withdrawOrderId'] = tag + params = self.omit(params, 'network') + response = self.privatePostOpenapiWalletV1WithdrawApply(self.extend(request, params)) + return self.parse_transaction(response, currency) + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://coins-docs.github.io/rest-api/#deposit-history-user_data + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + # todo: returns an empty array - find out why + self.load_markets() + currency = None + request: dict = {} + if code is not None: + currency = self.currency(code) + request['coin'] = currency['id'] + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + response = self.privateGetOpenapiWalletV1DepositHistory(self.extend(request, params)) + # + # [ + # { + # "id": "d_769800519366885376", + # "amount": "0.001", + # "coin": "BNB", + # "network": "BNB", + # "status": 0, + # "address": "bnb136ns6lfw4zs5hg4n85vdthaad7hq5m4gtkgf23", + # "addressTag": "101764890", + # "txId": "98A3EA560C6B3336D348B6C83F0F95ECE4F1F5919E94BD006E5BF3BF264FACFC", + # "insertTime": 1661493146000, + # "confirmNo": 10, + # }, + # { + # "id": "d_769754833590042625", + # "amount":"0.5", + # "coin":"IOTA", + # "network":"IOTA", + # "status":1, + # "address":"SIZ9VLMHWATXKV99LH99CIGFJFUMLEHGWVZVNNZXRJJVWBPHYWPPBOSDORZ9EQSHCZAMPVAPGFYQAUUV9DROOXJLNW", + # "addressTag":"", + # "txId":"ESBFVQUTPIWQNJSPXFNHNYHSQNTGKRVKPRABQWTAXCDWOAKDKYWPTVG9BGXNVNKTLEJGESAVXIKIZ9999", + # "insertTime":1599620082000, + # "confirmNo": 20, + # } + # ] + # + return self.parse_transactions(response, currency, since, limit) + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://coins-docs.github.io/rest-api/#withdraw-history-user_data + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + # todo: returns an empty array - find out why + self.load_markets() + currency = None + request: dict = {} + if code is not None: + currency = self.currency(code) + request['coin'] = currency['id'] + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + response = self.privateGetOpenapiWalletV1WithdrawHistory(self.extend(request, params)) + # + # [ + # { + # "id": "459890698271244288", + # "amount": "0.01", + # "transactionFee": "0", + # "coin": "ETH", + # "status": 1, + # "address": "0x386AE30AE2dA293987B5d51ddD03AEb70b21001F", + # "addressTag": "", + # "txId": "0x4ae2fed36a90aada978fc31c38488e8b60d7435cfe0b4daed842456b4771fcf7", + # "applyTime": 1673601139000, + # "network": "ETH", + # "withdrawOrderId": "thomas123", + # "info": "", + # "confirmNo": 100 + # }, + # { + # "id": "451899190746456064", + # "amount": "0.00063", + # "transactionFee": "0.00037", + # "coin": "ETH", + # "status": 1, + # "address": "0x386AE30AE2dA293987B5d51ddD03AEb70b21001F", + # "addressTag": "", + # "txId": "0x62690ca4f9d6a8868c258e2ce613805af614d9354dda7b39779c57b2e4da0260", + # "applyTime": 1671695815000, + # "network": "ETH", + # "withdrawOrderId": "", + # "info": "", + # "confirmNo": 100 + # } + # ] + # + return self.parse_transactions(response, currency, since, limit) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fetchDeposits + # { + # "coin": "PHP", + # "address": "Internal Transfer", + # "addressTag": "Internal Transfer", + # "amount": "0.02", + # "id": "31312321312312312312322", + # "network": "Internal", + # "transferType": "0", + # "status": 3, + # "confirmTimes": "", + # "unlockConfirm": "", + # "txId": "Internal Transfer", + # "insertTime": 1657623798000, + # "depositOrderId": "the deposit id which created by client" + # } + # + # fetchWithdrawals + # { + # "coin": "BTC", + # "address": "Internal Transfer", + # "amount": "0.1", + # "id": "1201515362324421632", + # "withdrawOrderId": null, + # "network": "Internal", + # "transferType": "0", + # "status": 0, + # "transactionFee": "0", + # "confirmNo": 0, + # "info": "{}", + # "txId": "Internal Transfer", + # "applyTime": 1657967792000 + # } + # + # todo: self is in progress + id = self.safe_string(transaction, 'id') + address = self.safe_string(transaction, 'address') + tag = self.safe_string(transaction, 'addressTag') + if tag is not None: + if len(tag) < 1: + tag = None + txid = self.safe_string(transaction, 'txId') + currencyId = self.safe_string(transaction, 'coin') + code = self.safe_currency_code(currencyId, currency) + timestamp = None + timestamp = self.safe_integer_2(transaction, 'insertTime', 'applyTime') + updated = None + type = None + withdrawOrderId = self.safe_string(transaction, 'withdrawOrderId') + depositOrderId = self.safe_string(transaction, 'depositOrderId') + if withdrawOrderId is not None: + type = 'withdrawal' + elif depositOrderId is not None: + type = 'deposit' + status = self.parse_transaction_status(self.safe_string(transaction, 'status')) + amount = self.safe_number(transaction, 'amount') + feeCost = self.safe_number(transaction, 'transactionFee') + fee = None + if feeCost is not None: + fee = {'currency': code, 'cost': feeCost} + network = self.safe_string(transaction, 'network') + internal = network == 'Internal' + return { + 'info': transaction, + 'id': id, + 'txid': txid, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': network, + 'address': address, + 'addressTo': address, + 'addressFrom': None, + 'tag': tag, + 'tagTo': tag, + 'tagFrom': None, + 'type': type, + 'amount': amount, + 'currency': code, + 'status': status, + 'updated': updated, + 'internal': internal, + 'comment': None, + 'fee': fee, + } + + def parse_transaction_status(self, status: Str): + statuses: dict = { + '0': 'pending', + '1': 'ok', + '2': 'failed', + '3': 'pending', + } + return self.safe_string(statuses, status, status) + + def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://coins-docs.github.io/rest-api/#deposit-address-user_data + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.network]: network for fetch deposit address + :returns dict: an `address structure ` + """ + networkCode = self.safe_string(params, 'network') + networkId = self.network_code_to_id(networkCode, code) + if networkId is None: + raise BadRequest(self.id + ' fetchDepositAddress() require network parameter') + self.load_markets() + currency = self.currency(code) + request: dict = { + 'coin': currency['id'], + 'network': networkId, + } + params = self.omit(params, 'network') + response = self.privateGetOpenapiWalletV1DepositAddress(self.extend(request, params)) + # + # { + # "coin": "ETH", + # "address": "0xfe98628173830bf79c59f04585ce41f7de168784", + # "addressTag": "" + # } + # + return self.parse_deposit_address(response, currency) + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + # + # { + # "coin": "ETH", + # "address": "0xfe98628173830bf79c59f04585ce41f7de168784", + # "addressTag": "" + # } + # + currencyId = self.safe_string(depositAddress, 'coin') + parsedCurrency = self.safe_currency_code(currencyId, currency) + return { + 'info': depositAddress, + 'currency': parsedCurrency, + 'network': None, + 'address': self.safe_string(depositAddress, 'address'), + 'tag': self.safe_string(depositAddress, 'addressTag'), + } + + def url_encode_query(self, query={}): + encodedArrayParams = '' + keys = list(query.keys()) + for i in range(0, len(keys)): + key = keys[i] + if isinstance(query[key], list): + if i != 0: + encodedArrayParams += '&' + innerArray = query[key] + query = self.omit(query, key) + encodedArrayParam = self.parse_array_param(innerArray, key) + encodedArrayParams += encodedArrayParam + encodedQuery = self.urlencode(query) + if len(encodedQuery) != 0: + return encodedQuery + '&' + encodedArrayParams + else: + return encodedArrayParams + + def parse_array_param(self, array, key): + stringifiedArray = self.json(array) + stringifiedArray = stringifiedArray.replace('[', '%5B') + stringifiedArray = stringifiedArray.replace(']', '%5D') + urlEncodedParam = key + '=' + stringifiedArray + return urlEncodedParam + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = self.urls['api'][api] + query = self.omit(params, self.extract_params(path)) + endpoint = self.implode_params(path, params) + url = url + '/' + endpoint + if api == 'private': + self.check_required_credentials() + query['timestamp'] = self.milliseconds() + recvWindow = self.safe_integer(query, 'recvWindow') + if recvWindow is None: + defaultRecvWindow = self.safe_integer(self.options, 'recvWindow') + if defaultRecvWindow is not None: + query['recvWindow'] = defaultRecvWindow + query = self.url_encode_query(query) + signature = self.hmac(self.encode(query), self.encode(self.secret), hashlib.sha256) + url = url + '?' + query + '&signature=' + signature + headers = { + 'X-COINS-APIKEY': self.apiKey, + } + else: + query = self.url_encode_query(query) + if len(query) != 0: + url += '?' + query + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None + responseCode = self.safe_string(response, 'code', None) + if (responseCode is not None) and (responseCode != '200') and (responseCode != '0'): + feedback = self.id + ' ' + body + self.throw_broadly_matched_exception(self.exceptions['broad'], body, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], responseCode, feedback) + raise ExchangeError(feedback) + return None diff --git a/ccxt/coinspot.py b/ccxt/coinspot.py new file mode 100644 index 0000000..f7034ad --- /dev/null +++ b/ccxt/coinspot.py @@ -0,0 +1,633 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.coinspot import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Int, Market, Num, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class coinspot(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(coinspot, self).describe(), { + 'id': 'coinspot', + 'name': 'CoinSpot', + 'countries': ['AU'], # Australia + 'rateLimit': 1000, + 'pro': False, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelOrder': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createMarketOrder': False, + 'createOrder': True, + 'createOrderWithTakeProfitAndStopLoss': False, + 'createOrderWithTakeProfitAndStopLossWs': False, + 'createPostOnlyOrder': False, + 'createReduceOnlyOrder': False, + 'createStopLimitOrder': False, + 'createStopMarketOrder': False, + 'createStopOrder': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkOHLCV': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrderBook': True, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': False, + 'fetchVolatilityHistory': False, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'repayMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'ws': False, + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/28208429-3cacdf9a-6896-11e7-854e-4c79a772a30f.jpg', + 'api': { + 'public': 'https://www.coinspot.com.au/pubapi', + 'private': 'https://www.coinspot.com.au/api', + }, + 'www': 'https://www.coinspot.com.au', + 'doc': 'https://www.coinspot.com.au/api', + 'referral': 'https://www.coinspot.com.au/register?code=PJURCU', + }, + 'api': { + 'public': { + 'get': [ + 'latest', + ], + }, + 'private': { + 'post': [ + 'orders', + 'orders/history', + 'my/coin/deposit', + 'my/coin/send', + 'quote/buy', + 'quote/sell', + 'my/balances', + 'my/orders', + 'my/buy', + 'my/sell', + 'my/buy/cancel', + 'my/sell/cancel', + 'ro/my/balances', + 'ro/my/balances/{cointype}', + 'ro/my/deposits', + 'ro/my/withdrawals', + 'ro/my/transactions', + 'ro/my/transactions/{cointype}', + 'ro/my/transactions/open', + 'ro/my/transactions/{cointype}/open', + 'ro/my/sendreceive', + 'ro/my/affiliatepayments', + 'ro/my/referralpayments', + ], + }, + }, + 'markets': { + 'ADA/AUD': self.safe_market_structure({'id': 'ada', 'symbol': 'ADA/AUD', 'base': 'ADA', 'quote': 'AUD', 'baseId': 'ada', 'quoteId': 'aud', 'type': 'spot', 'spot': True}), + 'BTC/AUD': self.safe_market_structure({'id': 'btc', 'symbol': 'BTC/AUD', 'base': 'BTC', 'quote': 'AUD', 'baseId': 'btc', 'quoteId': 'aud', 'type': 'spot', 'spot': True}), + 'ETH/AUD': self.safe_market_structure({'id': 'eth', 'symbol': 'ETH/AUD', 'base': 'ETH', 'quote': 'AUD', 'baseId': 'eth', 'quoteId': 'aud', 'type': 'spot', 'spot': True}), + 'XRP/AUD': self.safe_market_structure({'id': 'xrp', 'symbol': 'XRP/AUD', 'base': 'XRP', 'quote': 'AUD', 'baseId': 'xrp', 'quoteId': 'aud', 'type': 'spot', 'spot': True}), + 'LTC/AUD': self.safe_market_structure({'id': 'ltc', 'symbol': 'LTC/AUD', 'base': 'LTC', 'quote': 'AUD', 'baseId': 'ltc', 'quoteId': 'aud', 'type': 'spot', 'spot': True}), + 'DOGE/AUD': self.safe_market_structure({'id': 'doge', 'symbol': 'DOGE/AUD', 'base': 'DOGE', 'quote': 'AUD', 'baseId': 'doge', 'quoteId': 'aud', 'type': 'spot', 'spot': True}), + 'RFOX/AUD': self.safe_market_structure({'id': 'rfox', 'symbol': 'RFOX/AUD', 'base': 'RFOX', 'quote': 'AUD', 'baseId': 'rfox', 'quoteId': 'aud', 'type': 'spot', 'spot': True}), + 'POWR/AUD': self.safe_market_structure({'id': 'powr', 'symbol': 'POWR/AUD', 'base': 'POWR', 'quote': 'AUD', 'baseId': 'powr', 'quoteId': 'aud', 'type': 'spot', 'spot': True}), + 'NEO/AUD': self.safe_market_structure({'id': 'neo', 'symbol': 'NEO/AUD', 'base': 'NEO', 'quote': 'AUD', 'baseId': 'neo', 'quoteId': 'aud', 'type': 'spot', 'spot': True}), + 'TRX/AUD': self.safe_market_structure({'id': 'trx', 'symbol': 'TRX/AUD', 'base': 'TRX', 'quote': 'AUD', 'baseId': 'trx', 'quoteId': 'aud', 'type': 'spot', 'spot': True}), + 'EOS/AUD': self.safe_market_structure({'id': 'eos', 'symbol': 'EOS/AUD', 'base': 'EOS', 'quote': 'AUD', 'baseId': 'eos', 'quoteId': 'aud', 'type': 'spot', 'spot': True}), + 'XLM/AUD': self.safe_market_structure({'id': 'xlm', 'symbol': 'XLM/AUD', 'base': 'XLM', 'quote': 'AUD', 'baseId': 'xlm', 'quoteId': 'aud', 'type': 'spot', 'spot': True}), + 'RHOC/AUD': self.safe_market_structure({'id': 'rhoc', 'symbol': 'RHOC/AUD', 'base': 'RHOC', 'quote': 'AUD', 'baseId': 'rhoc', 'quoteId': 'aud', 'type': 'spot', 'spot': True}), + 'GAS/AUD': self.safe_market_structure({'id': 'gas', 'symbol': 'GAS/AUD', 'base': 'GAS', 'quote': 'AUD', 'baseId': 'gas', 'quoteId': 'aud', 'type': 'spot', 'spot': True}), + }, + 'commonCurrencies': { + 'DRK': 'DASH', + }, + 'options': { + 'fetchBalance': 'private_post_my_balances', + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': False, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': False, + 'FOK': False, + 'PO': False, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': False, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': None, + 'daysBack': 100000, + 'untilDays': 100000, # todo implement + 'symbolRequired': False, + }, + 'fetchOrder': None, + 'fetchOpenOrders': None, # todo implement + 'fetchOrders': None, + 'fetchClosedOrders': None, # todo implement + 'fetchOHLCV': None, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'precisionMode': TICK_SIZE, + }) + + def parse_balance(self, response) -> Balances: + result: dict = {'info': response} + balances = self.safe_value_2(response, 'balance', 'balances') + if isinstance(balances, list): + for i in range(0, len(balances)): + currencies = balances[i] + currencyIds = list(currencies.keys()) + for j in range(0, len(currencyIds)): + currencyId = currencyIds[j] + balance = currencies[currencyId] + code = self.safe_currency_code(currencyId) + account = self.account() + account['total'] = self.safe_string(balance, 'balance') + result[code] = account + else: + currencyIds = list(balances.keys()) + for i in range(0, len(currencyIds)): + currencyId = currencyIds[i] + code = self.safe_currency_code(currencyId) + account = self.account() + account['total'] = self.safe_string(balances, currencyId) + result[code] = account + return self.safe_balance(result) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://www.coinspot.com.au/api#listmybalance + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + method = self.safe_string(self.options, 'fetchBalance', 'private_post_my_balances') + response = getattr(self, method)(params) + # + # read-write api keys + # + # ... + # + # read-only api keys + # + # { + # "status":"ok", + # "balances":[ + # { + # "LTC":{"balance":0.1,"audbalance":16.59,"rate":165.95} + # } + # ] + # } + # + return self.parse_balance(response) + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://www.coinspot.com.au/api#listopenorders + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'cointype': market['id'], + } + orderbook = self.privatePostOrders(self.extend(request, params)) + return self.parse_order_book(orderbook, market['symbol'], None, 'buyorders', 'sellorders', 'rate', 'amount') + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "btc":{ + # "bid":"51970", + # "ask":"53000", + # "last":"52806.47" + # } + # } + # + symbol = self.safe_symbol(None, market) + last = self.safe_string(ticker, 'last') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': None, + 'datetime': None, + 'high': None, + 'low': None, + 'bid': self.safe_string(ticker, 'bid'), + 'bidVolume': None, + 'ask': self.safe_string(ticker, 'ask'), + 'askVolume': None, + 'vwap': None, + 'open': None, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': None, + 'quoteVolume': None, + 'info': ticker, + }, market) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://www.coinspot.com.au/api#latestprices + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + response = self.publicGetLatest(params) + id = market['id'] + id = id.lower() + prices = self.safe_dict(response, 'prices', {}) + # + # { + # "status":"ok", + # "prices":{ + # "btc":{ + # "bid":"52732.47000022", + # "ask":"53268.0699976", + # "last":"53284.03" + # } + # } + # } + # + ticker = self.safe_dict(prices, id) + return self.parse_ticker(ticker, market) + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://www.coinspot.com.au/api#latestprices + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + response = self.publicGetLatest(params) + # + # { + # "status": "ok", + # "prices": { + # "btc": { + # "bid": "25050", + # "ask": "25370", + # "last": "25234" + # }, + # "ltc": { + # "bid": "79.39192993", + # "ask": "87.98", + # "last": "87.95" + # } + # } + # } + # + result: dict = {} + prices = self.safe_dict(response, 'prices', {}) + ids = list(prices.keys()) + for i in range(0, len(ids)): + id = ids[i] + market = self.safe_market(id) + if market['spot']: + symbol = market['symbol'] + ticker = prices[id] + result[symbol] = self.parse_ticker(ticker, market) + return self.filter_by_array_tickers(result, 'symbol', symbols) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://www.coinspot.com.au/api#orderhistory + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'cointype': market['id'], + } + response = self.privatePostOrdersHistory(self.extend(request, params)) + # + # { + # "status":"ok", + # "orders":[ + # {"amount":0.00102091,"rate":21549.09999991,"total":21.99969168,"coin":"BTC","solddate":1604890646143,"market":"BTC/AUD"}, + # ], + # } + # + trades = self.safe_list(response, 'orders', []) + return self.parse_trades(trades, market, since, limit) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://www.coinspot.com.au/api#rotransaction + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + if since is not None: + request['startdate'] = self.yyyymmdd(since) + response = self.privatePostRoMyTransactions(self.extend(request, params)) + # { + # "status": "ok", + # "buyorders": [ + # { + # "otc": False, + # "market": "ALGO/AUD", + # "amount": 386.95197925, + # "created": "2022-10-20T09:56:44.502Z", + # "audfeeExGst": 1.80018002, + # "audGst": 0.180018, + # "audtotal": 200 + # }, + # ], + # "sellorders": [ + # { + # "otc": False, + # "market": "SOLO/ALGO", + # "amount": 154.52345614, + # "total": 115.78858204658796, + # "created": "2022-04-16T09:36:43.698Z", + # "audfeeExGst": 1.08995731, + # "audGst": 0.10899573, + # "audtotal": 118.7 + # }, + # ] + # } + buyTrades = self.safe_list(response, 'buyorders', []) + for i in range(0, len(buyTrades)): + buyTrades[i]['side'] = 'buy' + sellTrades = self.safe_list(response, 'sellorders', []) + for i in range(0, len(sellTrades)): + sellTrades[i]['side'] = 'sell' + trades = self.array_concat(buyTrades, sellTrades) + return self.parse_trades(trades, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # public fetchTrades + # + # { + # "amount":0.00102091, + # "rate":21549.09999991, + # "total":21.99969168, + # "coin":"BTC", + # "solddate":1604890646143, + # "market":"BTC/AUD" + # } + # + # private fetchMyTrades + # { + # "otc": False, + # "market": "ALGO/AUD", + # "amount": 386.95197925, + # "created": "2022-10-20T09:56:44.502Z", + # "audfeeExGst": 1.80018002, + # "audGst": 0.180018, + # "audtotal": 200, + # "total": 200, + # "side": "buy", + # "price": 0.5168600000125209 + # } + timestamp = None + priceString = None + fee = None + audTotal = self.safe_string(trade, 'audtotal') + costString = self.safe_string(trade, 'total', audTotal) + side = self.safe_string(trade, 'side') + amountString = self.safe_string(trade, 'amount') + marketId = self.safe_string(trade, 'market') + symbol = self.safe_symbol(marketId, market, '/') + solddate = self.safe_integer(trade, 'solddate') + if solddate is not None: + priceString = self.safe_string(trade, 'rate') + timestamp = solddate + else: + priceString = Precise.string_div(costString, amountString) + createdString = self.safe_string(trade, 'created') + timestamp = self.parse8601(createdString) + audfeeExGst = self.safe_string(trade, 'audfeeExGst') + audGst = self.safe_string(trade, 'audGst') + # The transaction fee which consumers pay is inclusive of GST by default + feeCost = Precise.string_add(audfeeExGst, audGst) + feeCurrencyId = 'AUD' + fee = { + 'cost': self.parse_number(feeCost), + 'currency': self.safe_currency_code(feeCurrencyId), + } + return self.safe_trade({ + 'info': trade, + 'id': None, + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'order': None, + 'type': None, + 'side': side, + 'takerOrMaker': None, + 'price': self.parse_number(priceString), + 'amount': self.parse_number(amountString), + 'cost': self.parse_number(costString), + 'fee': fee, + }, market) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://www.coinspot.com.au/api#placebuyorder + + :param str symbol: unified symbol of the market to create an order in + :param str type: must be 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + method = 'privatePostMy' + self.capitalize(side) + if type == 'market': + raise ExchangeError(self.id + ' createOrder() allows limit orders only') + market = self.market(symbol) + request: dict = { + 'cointype': market['id'], + 'amount': amount, + 'rate': price, + } + response = getattr(self, method)(self.extend(request, params)) + return self.parse_order(response) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://www.coinspot.com.au/api#cancelbuyorder + https://www.coinspot.com.au/api#cancelsellorder + + :param str id: order id + :param str symbol: not used by coinspot cancelOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + side = self.safe_string(params, 'side') + if side != 'buy' and side != 'sell': + raise ArgumentsRequired(self.id + ' cancelOrder() requires a side parameter, "buy" or "sell"') + params = self.omit(params, 'side') + request: dict = { + 'id': id, + } + response = None + if side == 'buy': + response = self.privatePostMyBuyCancel(self.extend(request, params)) + else: + response = self.privatePostMySellCancel(self.extend(request, params)) + # + # status - ok, error + # + return self.safe_order({ + 'info': response, + }) + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = self.urls['api'][api] + '/' + path + if api == 'private': + self.check_required_credentials() + nonce = self.nonce() + body = self.json(self.extend({'nonce': nonce}, params)) + headers = { + 'Content-Type': 'application/json', + 'key': self.apiKey, + 'sign': self.hmac(self.encode(body), self.encode(self.secret), hashlib.sha512), + } + return {'url': url, 'method': method, 'body': body, 'headers': headers} diff --git a/ccxt/cryptocom.py b/ccxt/cryptocom.py new file mode 100644 index 0000000..064e71a --- /dev/null +++ b/ccxt/cryptocom.py @@ -0,0 +1,3379 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.cryptocom import ImplicitAPI +import hashlib +import math +from ccxt.base.types import Account, Any, Balances, Currencies, Currency, DepositAddress, Int, LedgerEntry, Market, Num, Order, OrderBook, OrderRequest, CancellationRequest, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, Trade, TradingFeeInterface, TradingFees, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import AccountNotEnabled +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import NotSupported +from ccxt.base.errors import DDoSProtection +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import OnMaintenance +from ccxt.base.errors import InvalidNonce +from ccxt.base.errors import RequestTimeout +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class cryptocom(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(cryptocom, self).describe(), { + 'id': 'cryptocom', + 'name': 'Crypto.com', + 'countries': ['MT'], + 'version': 'v2', + 'rateLimit': 10, # 100 requests per second + 'certified': True, + 'pro': True, + 'has': { + 'CORS': False, + 'spot': True, + 'margin': True, + 'swap': True, + 'future': True, + 'option': True, + 'addMargin': False, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'cancelOrders': True, + 'cancelOrdersForSymbols': True, + 'closeAllPositions': False, + 'closePosition': True, + 'createMarketBuyOrderWithCost': False, + 'createMarketOrderWithCost': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createOrders': True, + 'createStopOrder': True, + 'createTriggerOrder': True, + 'editOrder': True, + 'fetchAccounts': True, + 'fetchBalance': True, + 'fetchBidsAsks': False, + 'fetchBorrowInterest': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchClosedOrders': 'emulated', + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': True, + 'fetchDeposits': True, + 'fetchDepositsWithdrawals': False, + 'fetchDepositWithdrawFee': 'emulated', + 'fetchDepositWithdrawFees': True, + 'fetchFundingHistory': False, + 'fetchFundingRate': True, + 'fetchFundingRateHistory': True, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchLedger': True, + 'fetchLeverage': False, + 'fetchLeverageTiers': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchPosition': True, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': True, + 'fetchPositionsHistory': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': True, + 'fetchStatus': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': False, + 'fetchTrades': True, + 'fetchTradingFee': True, + 'fetchTradingFees': True, + 'fetchTransactionFees': False, + 'fetchTransactions': False, + 'fetchTransfers': False, + 'fetchUnderlyingAssets': False, + 'fetchVolatilityHistory': False, + 'fetchWithdrawals': True, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'sandbox': True, + 'setLeverage': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'transfer': False, + 'withdraw': True, + }, + 'timeframes': { + '1m': '1m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1h', + '4h': '4h', + '6h': '6h', + '12h': '12h', + '1d': '1D', + '1w': '7D', + '2w': '14D', + '1M': '1M', + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/147792121-38ed5e36-c229-48d6-b49a-48d05fc19ed4.jpeg', + 'test': { + 'v1': 'https://uat-api.3ona.co/exchange/v1', + 'v2': 'https://uat-api.3ona.co/v2', + 'derivatives': 'https://uat-api.3ona.co/v2', + }, + 'api': { + 'base': 'https://api.crypto.com', + 'v1': 'https://api.crypto.com/exchange/v1', + 'v2': 'https://api.crypto.com/v2', + 'derivatives': 'https://deriv-api.crypto.com/v1', + }, + 'www': 'https://crypto.com/', + 'referral': { + 'url': 'https://crypto.com/exch/kdacthrnxt', + 'discount': 0.75, + }, + 'doc': [ + 'https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html', + 'https://exchange-docs.crypto.com/spot/index.html', + 'https://exchange-docs.crypto.com/derivatives/index.html', + ], + 'fees': 'https://crypto.com/exchange/document/fees-limits', + }, + 'api': { + 'base': { + 'public': { + 'get': { + 'v1/public/get-announcements': 1, # no description of rate limit + }, + }, + }, + 'v1': { + 'public': { + 'get': { + 'public/auth': 10 / 3, + 'public/get-instruments': 10 / 3, + 'public/get-book': 1, + 'public/get-candlestick': 1, + 'public/get-trades': 1, + 'public/get-tickers': 1, + 'public/get-valuations': 1, + 'public/get-expired-settlement-price': 10 / 3, + 'public/get-insurance': 1, + 'public/get-risk-parameters': 1, + }, + 'post': { + 'public/staking/get-conversion-rate': 2, + }, + }, + 'private': { + 'post': { + 'private/set-cancel-on-disconnect': 10 / 3, + 'private/get-cancel-on-disconnect': 10 / 3, + 'private/user-balance': 10 / 3, + 'private/user-balance-history': 10 / 3, + 'private/get-positions': 10 / 3, + 'private/create-order': 2 / 3, + 'private/amend-order': 4 / 3, # no description of rate limit + 'private/create-order-list': 10 / 3, + 'private/cancel-order': 2 / 3, + 'private/cancel-order-list': 10 / 3, + 'private/cancel-all-orders': 2 / 3, + 'private/close-position': 10 / 3, + 'private/get-order-history': 100, + 'private/get-open-orders': 10 / 3, + 'private/get-order-detail': 1 / 3, + 'private/get-trades': 100, + 'private/change-account-leverage': 10 / 3, + 'private/get-transactions': 10 / 3, + 'private/create-subaccount-transfer': 10 / 3, + 'private/get-subaccount-balances': 10 / 3, + 'private/get-order-list': 10 / 3, + 'private/create-withdrawal': 10 / 3, + 'private/get-currency-networks': 10 / 3, + 'private/get-deposit-address': 10 / 3, + 'private/get-accounts': 10 / 3, + 'private/get-withdrawal-history': 10 / 3, + 'private/get-deposit-history': 10 / 3, + 'private/get-fee-rate': 2, + 'private/get-instrument-fee-rate': 2, + 'private/staking/stake': 2, + 'private/staking/unstake': 2, + 'private/staking/get-staking-position': 2, + 'private/staking/get-staking-instruments': 2, + 'private/staking/get-open-stake': 2, + 'private/staking/get-stake-history': 2, + 'private/staking/get-reward-history': 2, + 'private/staking/convert': 2, + 'private/staking/get-open-convert': 2, + 'private/staking/get-convert-history': 2, + }, + }, + }, + 'v2': { + 'public': { + 'get': { + 'public/auth': 1, + 'public/get-instruments': 1, + 'public/get-book': 1, + 'public/get-candlestick': 1, + 'public/get-ticker': 1, + 'public/get-trades': 1, + 'public/margin/get-transfer-currencies': 1, + 'public/margin/get-load-currenices': 1, + 'public/respond-heartbeat': 1, + }, + }, + 'private': { + 'post': { + 'private/set-cancel-on-disconnect': 10 / 3, + 'private/get-cancel-on-disconnect': 10 / 3, + 'private/create-withdrawal': 10 / 3, + 'private/get-withdrawal-history': 10 / 3, + 'private/get-currency-networks': 10 / 3, + 'private/get-deposit-history': 10 / 3, + 'private/get-deposit-address': 10 / 3, + 'private/export/create-export-request': 10 / 3, + 'private/export/get-export-requests': 10 / 3, + 'private/export/download-export-output': 10 / 3, + 'private/get-account-summary': 10 / 3, + 'private/create-order': 2 / 3, + 'private/cancel-order': 2 / 3, + 'private/cancel-all-orders': 2 / 3, + 'private/create-order-list': 10 / 3, + 'private/get-order-history': 10 / 3, + 'private/get-open-orders': 10 / 3, + 'private/get-order-detail': 1 / 3, + 'private/get-trades': 100, + 'private/get-accounts': 10 / 3, + 'private/get-subaccount-balances': 10 / 3, + 'private/create-subaccount-transfer': 10 / 3, + 'private/otc/get-otc-user': 10 / 3, + 'private/otc/get-instruments': 10 / 3, + 'private/otc/request-quote': 100, + 'private/otc/accept-quote': 100, + 'private/otc/get-quote-history': 10 / 3, + 'private/otc/get-trade-history': 10 / 3, + 'private/otc/create-order': 10 / 3, + }, + }, + }, + 'derivatives': { + 'public': { + 'get': { + 'public/auth': 10 / 3, + 'public/get-instruments': 10 / 3, + 'public/get-book': 1, + 'public/get-candlestick': 1, + 'public/get-trades': 1, + 'public/get-tickers': 1, + 'public/get-valuations': 1, + 'public/get-expired-settlement-price': 10 / 3, + 'public/get-insurance': 1, + }, + }, + 'private': { + 'post': { + 'private/set-cancel-on-disconnect': 10 / 3, + 'private/get-cancel-on-disconnect': 10 / 3, + 'private/user-balance': 10 / 3, + 'private/user-balance-history': 10 / 3, + 'private/get-positions': 10 / 3, + 'private/create-order': 2 / 3, + 'private/create-order-list': 10 / 3, + 'private/cancel-order': 2 / 3, + 'private/cancel-order-list': 10 / 3, + 'private/cancel-all-orders': 2 / 3, + 'private/close-position': 10 / 3, + 'private/convert-collateral': 10 / 3, + 'private/get-order-history': 100, + 'private/get-open-orders': 10 / 3, + 'private/get-order-detail': 1 / 3, + 'private/get-trades': 100, + 'private/change-account-leverage': 10 / 3, + 'private/get-transactions': 10 / 3, + 'private/create-subaccount-transfer': 10 / 3, + 'private/get-subaccount-balances': 10 / 3, + 'private/get-order-list': 10 / 3, + }, + }, + }, + }, + 'fees': { + 'trading': { + 'maker': self.parse_number('0.0025'), + 'taker': self.parse_number('0.005'), + 'tiers': { + 'maker': [ + [self.parse_number('0'), self.parse_number('0.0025')], + [self.parse_number('10000'), self.parse_number('0.002')], + [self.parse_number('50000'), self.parse_number('0.0015')], + [self.parse_number('250000'), self.parse_number('0.001')], + [self.parse_number('500000'), self.parse_number('0.0008')], + [self.parse_number('2500000'), self.parse_number('0.00065')], + [self.parse_number('10000000'), self.parse_number('0')], + [self.parse_number('25000000'), self.parse_number('0')], + [self.parse_number('100000000'), self.parse_number('0')], + [self.parse_number('250000000'), self.parse_number('0')], + [self.parse_number('500000000'), self.parse_number('0')], + ], + 'taker': [ + [self.parse_number('0'), self.parse_number('0.005')], + [self.parse_number('10000'), self.parse_number('0.004')], + [self.parse_number('50000'), self.parse_number('0.0025')], + [self.parse_number('250000'), self.parse_number('0.002')], + [self.parse_number('500000'), self.parse_number('0.0018')], + [self.parse_number('2500000'), self.parse_number('0.001')], + [self.parse_number('10000000'), self.parse_number('0.0005')], + [self.parse_number('25000000'), self.parse_number('0.0004')], + [self.parse_number('100000000'), self.parse_number('0.00035')], + [self.parse_number('250000000'), self.parse_number('0.00031')], + [self.parse_number('500000000'), self.parse_number('0.00025')], + ], + }, + }, + }, + 'options': { + 'defaultType': 'spot', + 'accountsById': { + 'funding': 'SPOT', + 'spot': 'SPOT', + 'margin': 'MARGIN', + 'derivatives': 'DERIVATIVES', + 'swap': 'DERIVATIVES', + 'future': 'DERIVATIVES', + }, + 'networks': { + 'BEP20': 'BSC', + 'ERC20': 'ETH', + 'TRC20': 'TRON', + }, + 'broker': 'CCXT', + }, + 'features': { + 'default': { + 'sandbox': True, + 'createOrder': { + 'marginMode': True, + 'triggerPrice': True, + # todo: implementation fix + 'triggerPriceType': { + 'last': True, + 'mark': True, + 'index': True, + }, + 'triggerDirection': False, + 'stopLossPrice': True, + 'takeProfitPrice': True, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'selfTradePrevention': True, # todo: implement + 'trailing': False, + 'iceberg': False, + 'leverage': False, + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': True, + }, + 'createOrders': { + 'max': 10, + }, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, + 'daysBack': None, + 'untilDays': 1, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': True, + 'limit': 100, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': None, + 'untilDays': 1, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': None, + 'daysBackCanceled': None, + 'untilDays': 1, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 300, + }, + }, + 'spot': { + 'extends': 'default', + 'fetchCurrencies': { + 'private': True, + }, + }, + 'swap': { + 'linear': { + 'extends': 'default', + }, + 'inverse': { + 'extends': 'default', + }, + }, + 'future': { + 'linear': { + 'extends': 'default', + }, + 'inverse': { + 'extends': 'default', + }, + }, + }, + # https://exchange-docs.crypto.com/spot/index.html#response-and-reason-codes + 'commonCurrencies': { + 'USD_STABLE_COIN': 'USDC', + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'exact': { + '219': InvalidOrder, + '306': InsufficientFunds, # {"id" : 1753xxx, "method" : "private/amend-order", "code" : 306, "message" : "INSUFFICIENT_AVAILABLE_BALANCE", "result" : {"client_oid" : "1753xxx", "order_id" : "6530xxx"}} + '314': InvalidOrder, # {"id" : 1700xxx, "method" : "private/create-order", "code" : 314, "message" : "EXCEEDS_MAX_ORDER_SIZE", "result" : {"client_oid" : "1700xxx", "order_id" : "6530xxx"}} + '325': InvalidOrder, # {"id" : 1741xxx, "method" : "private/create-order", "code" : 325, "message" : "EXCEED_DAILY_VOL_LIMIT", "result" : {"client_oid" : "1741xxx", "order_id" : "6530xxx"}} + '415': InvalidOrder, # {"id" : 1741xxx, "method" : "private/create-order", "code" : 415, "message" : "BELOW_MIN_ORDER_SIZE", "result" : {"client_oid" : "1741xxx", "order_id" : "6530xxx"}} + '10001': ExchangeError, + '10002': PermissionDenied, + '10003': PermissionDenied, + '10004': BadRequest, + '10005': PermissionDenied, + '10006': DDoSProtection, + '10007': InvalidNonce, + '10008': BadRequest, + '10009': BadRequest, + '20001': BadRequest, + '20002': InsufficientFunds, + '20005': AccountNotEnabled, # {"id":"123xxx","method":"private/margin/xxx","code":"20005","message":"ACCOUNT_NOT_FOUND"} + '30003': BadSymbol, + '30004': BadRequest, + '30005': BadRequest, + '30006': InvalidOrder, + '30007': InvalidOrder, + '30008': InvalidOrder, + '30009': InvalidOrder, + '30010': BadRequest, + '30013': InvalidOrder, + '30014': InvalidOrder, + '30016': InvalidOrder, + '30017': InvalidOrder, + '30023': InvalidOrder, + '30024': InvalidOrder, + '30025': InvalidOrder, + '40001': BadRequest, + '40002': BadRequest, + '40003': BadRequest, + '40004': BadRequest, + '40005': BadRequest, + '40006': BadRequest, + '40007': BadRequest, + '40101': AuthenticationError, + '40102': InvalidNonce, # Nonce value differs by more than 60 seconds from server + '40103': AuthenticationError, # IP address not whitelisted + '40104': AuthenticationError, # Disallowed based on user tier + '40107': BadRequest, # Session subscription limit has been exceeded + '40401': OrderNotFound, + '40801': RequestTimeout, + '42901': RateLimitExceeded, + '43005': InvalidOrder, # Rejected POST_ONLY create-order request(normally happened when exec_inst contains POST_ONLY but time_in_force is NOT GOOD_TILL_CANCEL) + '43003': InvalidOrder, # FOK order has not been filled and cancelled + '43004': InvalidOrder, # IOC order has not been filled and cancelled + '43012': BadRequest, # Canceled due to Self Trade Prevention + '50001': ExchangeError, + '9010001': OnMaintenance, # {"code":9010001,"message":"SYSTEM_MAINTENANCE","details":"Crypto.com Exchange is currently under maintenance. Please refer to https://status.crypto.com for more details."} + }, + 'broad': {}, + }, + }) + + def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#private-get-currency-networks + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + # self endpoint requires authentication + if not self.check_required_credentials(False): + return {} + skipFetchCurrencies = False + skipFetchCurrencies, params = self.handle_option_and_params(params, 'fetchCurrencies', 'skipFetchCurrencies', False) + if skipFetchCurrencies: + # sub-accounts can't access self endpoint + return {} + response = {} + try: + response = self.v1PrivatePostPrivateGetCurrencyNetworks(params) + except Exception as e: + if isinstance(e, ExchangeError): + # sub-accounts can't access self endpoint + # {"code":"10001","msg":"SYS_ERROR"} + return {} + raise e + # do nothing + # sub-accounts can't access self endpoint + # + # { + # "id": "1747502328559", + # "method": "private/get-currency-networks", + # "code": "0", + # "result": { + # "update_time": "1747502281000", + # "currency_map": { + # "USDT": { + # "full_name": "Tether USD", + # "default_network": "ETH", + # "network_list": [ + # { + # "network_id": "ETH", + # "withdrawal_fee": "10.00000000", + # "withdraw_enabled": True, + # "min_withdrawal_amount": "20.0", + # "deposit_enabled": True, + # "confirmation_required": "32" + # }, + # { + # "network_id": "CRONOS", + # "withdrawal_fee": "0.18000000", + # "withdraw_enabled": True, + # "min_withdrawal_amount": "0.35", + # "deposit_enabled": True, + # "confirmation_required": "15" + # }, + # { + # "network_id": "SOL", + # "withdrawal_fee": "5.31000000", + # "withdraw_enabled": True, + # "min_withdrawal_amount": "10.62", + # "deposit_enabled": True, + # "confirmation_required": "1" + # } + # ] + # } + # } + # } + # } + # + resultData = self.safe_dict(response, 'result', {}) + currencyMap = self.safe_dict(resultData, 'currency_map', {}) + keys = list(currencyMap.keys()) + result: dict = {} + for i in range(0, len(keys)): + key = keys[i] + currency = currencyMap[key] + id = key + code = self.safe_currency_code(id) + networks: dict = {} + chains = self.safe_list(currency, 'network_list', []) + for j in range(0, len(chains)): + chain = chains[j] + networkId = self.safe_string(chain, 'network_id') + network = self.network_id_to_code(networkId) + networks[network] = { + 'info': chain, + 'id': networkId, + 'network': network, + 'active': None, + 'deposit': self.safe_bool(chain, 'deposit_enabled', False), + 'withdraw': self.safe_bool(chain, 'withdraw_enabled', False), + 'fee': self.safe_number(chain, 'withdrawal_fee'), + 'precision': None, + 'limits': { + 'withdraw': { + 'min': self.safe_number(chain, 'min_withdrawal_amount'), + 'max': None, + }, + }, + } + result[code] = self.safe_currency_structure({ + 'info': currency, + 'id': id, + 'code': code, + 'name': self.safe_string(currency, 'full_name'), + 'active': None, + 'deposit': None, + 'withdraw': None, + 'fee': None, + 'precision': None, + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + }, + 'type': 'crypto', # only crypto now + 'networks': networks, + }) + return result + + def fetch_markets(self, params={}) -> List[Market]: + """ + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#public-get-instruments + + retrieves data on all markets for cryptocom + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = self.v1PublicGetPublicGetInstruments(params) + # + # { + # "id": 1, + # "method": "public/get-instruments", + # "code": 0, + # "result": { + # "data": [ + # { + # "symbol": "BTC_USDT", + # "inst_type": "CCY_PAIR", + # "display_name": "BTC/USDT", + # "base_ccy": "BTC", + # "quote_ccy": "USDT", + # "quote_decimals": 2, + # "quantity_decimals": 5, + # "price_tick_size": "0.01", + # "qty_tick_size": "0.00001", + # "max_leverage": "50", + # "tradable": True, + # "expiry_timestamp_ms": 0, + # "beta_product": False, + # "margin_buy_enabled": False, + # "margin_sell_enabled": True + # }, + # { + # "symbol": "RUNEUSD-PERP", + # "inst_type": "PERPETUAL_SWAP", + # "display_name": "RUNEUSD Perpetual", + # "base_ccy": "RUNE", + # "quote_ccy": "USD", + # "quote_decimals": 3, + # "quantity_decimals": 1, + # "price_tick_size": "0.001", + # "qty_tick_size": "0.1", + # "max_leverage": "50", + # "tradable": True, + # "expiry_timestamp_ms": 0, + # "beta_product": False, + # "underlying_symbol": "RUNEUSD-INDEX", + # "contract_size": "1", + # "margin_buy_enabled": False, + # "margin_sell_enabled": False + # }, + # { + # "symbol": "ETHUSD-230825", + # "inst_type": "FUTURE", + # "display_name": "ETHUSD Futures 20230825", + # "base_ccy": "ETH", + # "quote_ccy": "USD", + # "quote_decimals": 2, + # "quantity_decimals": 4, + # "price_tick_size": "0.01", + # "qty_tick_size": "0.0001", + # "max_leverage": "100", + # "tradable": True, + # "expiry_timestamp_ms": 1692950400000, + # "beta_product": False, + # "underlying_symbol": "ETHUSD-INDEX", + # "contract_size": "1", + # "margin_buy_enabled": False, + # "margin_sell_enabled": False + # }, + # { + # "symbol": "BTCUSD-230630-CW30000", + # "inst_type": "WARRANT", + # "display_name": "BTCUSD-230630-CW30000", + # "base_ccy": "BTC", + # "quote_ccy": "USD", + # "quote_decimals": 3, + # "quantity_decimals": 0, + # "price_tick_size": "0.001", + # "qty_tick_size": "10", + # "max_leverage": "50", + # "tradable": True, + # "expiry_timestamp_ms": 1688112000000, + # "beta_product": False, + # "underlying_symbol": "BTCUSD-INDEX", + # "put_call": "CALL", + # "strike": "30000", + # "contract_size": "0.0001", + # "margin_buy_enabled": False, + # "margin_sell_enabled": False + # }, + # ] + # } + # } + # + resultResponse = self.safe_dict(response, 'result', {}) + data = self.safe_list(resultResponse, 'data', []) + result = [] + for i in range(0, len(data)): + market = data[i] + inst_type = self.safe_string(market, 'inst_type') + spot = inst_type == 'CCY_PAIR' + swap = inst_type == 'PERPETUAL_SWAP' + future = inst_type == 'FUTURE' + option = inst_type == 'WARRANT' + baseId = self.safe_string(market, 'base_ccy') + quoteId = self.safe_string(market, 'quote_ccy') + settleId = None if spot else quoteId + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + settle = None if spot else self.safe_currency_code(settleId) + optionType = self.safe_string_lower(market, 'put_call') + strike = self.safe_string(market, 'strike') + marginBuyEnabled = self.safe_bool(market, 'margin_buy_enabled') + marginSellEnabled = self.safe_bool(market, 'margin_sell_enabled') + expiryString = self.omit_zero(self.safe_string(market, 'expiry_timestamp_ms')) + expiry = int(expiryString) if (expiryString is not None) else None + symbol = base + '/' + quote + type = None + contract = None + if inst_type == 'CCY_PAIR': + type = 'spot' + contract = False + elif inst_type == 'PERPETUAL_SWAP': + type = 'swap' + symbol = symbol + ':' + quote + contract = True + elif inst_type == 'FUTURE': + type = 'future' + symbol = symbol + ':' + quote + '-' + self.yymmdd(expiry) + contract = True + elif inst_type == 'WARRANT': + type = 'option' + symbolOptionType = 'C' if (optionType == 'call') else 'P' + symbol = symbol + ':' + quote + '-' + self.yymmdd(expiry) + '-' + strike + '-' + symbolOptionType + contract = True + result.append({ + 'id': self.safe_string(market, 'symbol'), + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': type, + 'spot': spot, + 'margin': ((marginBuyEnabled) or (marginSellEnabled)), + 'swap': swap, + 'future': future, + 'option': option, + 'active': self.safe_bool(market, 'tradable'), + 'contract': contract, + 'linear': True if (contract) else None, + 'inverse': False if (contract) else None, + 'contractSize': self.safe_number(market, 'contract_size'), + 'expiry': expiry, + 'expiryDatetime': self.iso8601(expiry), + 'strike': self.parse_number(strike), + 'optionType': optionType, + 'precision': { + 'price': self.parse_number(self.safe_string(market, 'price_tick_size')), + 'amount': self.parse_number(self.safe_string(market, 'qty_tick_size')), + }, + 'limits': { + 'leverage': { + 'min': self.parse_number('1'), + 'max': self.safe_number(market, 'max_leverage'), + }, + 'amount': { + 'min': None, + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + 'info': market, + }) + return result + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#public-get-tickers + https://exchange-docs.crypto.com/derivatives/index.html#public-get-tickers + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + market = None + request: dict = {} + if symbols is not None: + symbol = None + if isinstance(symbols, list): + symbolsLength = len(symbols) + if symbolsLength > 1: + raise BadRequest(self.id + ' fetchTickers() symbols argument cannot contain more than 1 symbol') + symbol = symbols[0] + else: + symbol = symbols + market = self.market(symbol) + request['instrument_name'] = market['id'] + response = self.v1PublicGetPublicGetTickers(self.extend(request, params)) + # + # { + # "id": -1, + # "method": "public/get-tickers", + # "code": 0, + # "result": { + # "data": [ + # { + # "i": "AVAXUSD-PERP", + # "h": "13.209", + # "l": "12.148", + # "a": "13.209", + # "v": "1109.8", + # "vv": "14017.33", + # "c": "0.0732", + # "b": "13.210", + # "k": "13.230", + # "oi": "10888.9", + # "t": 1687402657575 + # }, + # ] + # } + # } + # + result = self.safe_dict(response, 'result', {}) + data = self.safe_list(result, 'data', []) + return self.parse_tickers(data, symbols) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#public-get-tickers + + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + symbol = self.symbol(symbol) + tickers = self.fetch_tickers([symbol], params) + return self.safe_value(tickers, symbol) + + def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#private-get-order-history + + :param str symbol: unified market symbol of the market the orders were made in + :param int [since]: the earliest time in ms to fetch orders for, max date range is one day + :param int [limit]: the maximum number of order structures to retrieve, default 100 max 100 + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms for the ending date filter, default is the current time + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOrders', 'paginate') + if paginate: + return self.fetch_paginated_call_dynamic('fetchOrders', symbol, since, limit, params) + market = None + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['instrument_name'] = market['id'] + if since is not None: + request['start_time'] = since + if limit is not None: + request['limit'] = limit + until = self.safe_integer(params, 'until') + params = self.omit(params, ['until']) + if until is not None: + request['end_time'] = until + response = self.v1PrivatePostPrivateGetOrderHistory(self.extend(request, params)) + # + # { + # "id": 1686881486183, + # "method": "private/get-order-history", + # "code": 0, + # "result": { + # "data": [ + # { + # "account_id": "ce075bef-1234-4321-bd6g-ff9007252e63", + # "order_id": "6142909895014042762", + # "client_oid": "4e918597-1234-4321-8201-a7577e1e1d91", + # "order_type": "MARKET", + # "time_in_force": "GOOD_TILL_CANCEL", + # "side": "SELL", + # "exec_inst": [], + # "quantity": "0.00024", + # "order_value": "5.7054672", + # "maker_fee_rate": "0", + # "taker_fee_rate": "0", + # "avg_price": "25023.97", + # "trigger_price": "0", + # "ref_price": "0", + # "ref_price_type": "NULL_VAL", + # "cumulative_quantity": "0.00024", + # "cumulative_value": "6.0057528", + # "cumulative_fee": "0.001501438200", + # "status": "FILLED", + # "update_user_id": "ce075bef-1234-4321-bd6g-ff9007252e63", + # "order_date": "2023-06-15", + # "instrument_name": "BTC_USD", + # "fee_instrument_name": "USD", + # "create_time": 1686805465891, + # "create_time_ns": "1686805465891812578", + # "update_time": 1686805465891 + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'result', {}) + orders = self.safe_list(data, 'data', []) + return self.parse_orders(orders, market, since, limit) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get a list of the most recent trades for a particular symbol + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#public-get-trades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch, maximum date range is one day + :param int [limit]: the maximum number of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms for the ending date filter, default is the current time + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchTrades', 'paginate') + if paginate: + return self.fetch_paginated_call_dynamic('fetchTrades', symbol, since, limit, params) + market = self.market(symbol) + request: dict = { + 'instrument_name': market['id'], + } + if since is not None: + request['start_ts'] = since + if limit is not None: + request['count'] = limit + until = self.safe_integer(params, 'until') + params = self.omit(params, ['until']) + if until is not None: + request['end_ts'] = until + response = self.v1PublicGetPublicGetTrades(self.extend(request, params)) + # + # { + # "id": -1, + # "method": "public/get-trades", + # "code": 0, + # "result": { + # "data": [ + # { + # "s": "sell", + # "p": "26386.00", + # "q": "0.00453", + # "t": 1686944282062, + # "tn" : 1704476468851524373, + # "d": "4611686018455979970", + # "i": "BTC_USD" + # }, + # ] + # } + # } + # + result = self.safe_dict(response, 'result', {}) + trades = self.safe_list(result, 'data', []) + return self.parse_trades(trades, market, since, limit) + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#public-get-candlestick + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms for the ending date filter, default is the current time + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate', False) + if paginate: + return self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params, 300) + market = self.market(symbol) + request: dict = { + 'instrument_name': market['id'], + 'timeframe': self.safe_string(self.timeframes, timeframe, timeframe), + } + if limit is not None: + if limit > 300: + limit = 300 + request['count'] = limit + now = self.microseconds() + duration = self.parse_timeframe(timeframe) + until = self.safe_integer(params, 'until', now) + params = self.omit(params, ['until']) + if since is not None: + request['start_ts'] = since - duration * 1000 + if limit is not None: + request['end_ts'] = self.sum(since, duration * limit * 1000) + else: + request['end_ts'] = until + else: + request['end_ts'] = until + response = self.v1PublicGetPublicGetCandlestick(self.extend(request, params)) + # + # { + # "id": -1, + # "method": "public/get-candlestick", + # "code": 0, + # "result": { + # "interval": "1m", + # "data": [ + # { + # "o": "26949.89", + # "h": "26957.64", + # "l": "26948.24", + # "c": "26950.00", + # "v": "0.0670", + # "t": 1687237080000 + # }, + # ], + # "instrument_name": "BTC_USD" + # } + # } + # + result = self.safe_dict(response, 'result', {}) + data = self.safe_list(result, 'data', []) + return self.parse_ohlcvs(data, market, timeframe, since, limit) + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#public-get-book + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the number of order book entries to return, max 50 + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'instrument_name': market['id'], + } + if limit: + request['depth'] = min(limit, 50) # max 50 + response = self.v1PublicGetPublicGetBook(self.extend(request, params)) + # + # { + # "id": -1, + # "method": "public/get-book", + # "code": 0, + # "result": { + # "depth": 3, + # "data": [ + # { + # "bids": [["30025.00", "0.00004", "1"], ["30020.15", "0.02498", "1"], ["30020.00", "0.00004", "1"]], + # "asks": [["30025.01", "0.04090", "1"], ["30025.70", "0.01000", "1"], ["30026.94", "0.02681", "1"]], + # "t": 1687491287380 + # } + # ], + # "instrument_name": "BTC_USD" + # } + # } + # + result = self.safe_dict(response, 'result', {}) + data = self.safe_list(result, 'data', []) + orderBook = self.safe_value(data, 0) + timestamp = self.safe_integer(orderBook, 't') + return self.parse_order_book(orderBook, symbol, timestamp) + + def parse_balance(self, response) -> Balances: + responseResult = self.safe_dict(response, 'result', {}) + data = self.safe_list(responseResult, 'data', []) + positionBalances = self.safe_value(data[0], 'position_balances', []) + result: dict = {'info': response} + for i in range(0, len(positionBalances)): + balance = positionBalances[i] + currencyId = self.safe_string(balance, 'instrument_name') + code = self.safe_currency_code(currencyId) + account = self.account() + account['total'] = self.safe_string(balance, 'quantity') + account['used'] = self.safe_string(balance, 'reserved_qty') + result[code] = account + return self.safe_balance(result) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#private-user-balance + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + response = self.v1PrivatePostPrivateUserBalance(params) + # + # { + # "id": 1687300499018, + # "method": "private/user-balance", + # "code": 0, + # "result": { + # "data": [ + # { + # "total_available_balance": "5.84684368", + # "total_margin_balance": "5.84684368", + # "total_initial_margin": "0", + # "total_maintenance_margin": "0", + # "total_position_cost": "0", + # "total_cash_balance": "6.44412101", + # "total_collateral_value": "5.846843685", + # "total_session_unrealized_pnl": "0", + # "instrument_name": "USD", + # "total_session_realized_pnl": "0", + # "position_balances": [ + # { + # "quantity": "0.0002119875", + # "reserved_qty": "0", + # "collateral_weight": "0.9", + # "collateral_amount": "5.37549592", + # "market_value": "5.97277325", + # "max_withdrawal_balance": "0.00021198", + # "instrument_name": "BTC", + # "hourly_interest_rate": "0" + # }, + # ], + # "total_effective_leverage": "0", + # "position_limit": "3000000", + # "used_position_limit": "0", + # "total_borrow": "0", + # "margin_score": "0", + # "is_liquidating": False, + # "has_risk": False, + # "terminatable": True + # } + # ] + # } + # } + # + return self.parse_balance(response) + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#private-get-order-detail + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + request: dict = { + 'order_id': id, + } + response = self.v1PrivatePostPrivateGetOrderDetail(self.extend(request, params)) + # + # { + # "id": 1686872583882, + # "method": "private/get-order-detail", + # "code": 0, + # "result": { + # "account_id": "ae075bef-1234-4321-bd6g-bb9007252a63", + # "order_id": "6142909895025252686", + # "client_oid": "CCXT_c2d2152cc32d40a3ae7fbf", + # "order_type": "LIMIT", + # "time_in_force": "GOOD_TILL_CANCEL", + # "side": "BUY", + # "exec_inst": [], + # "quantity": "0.00020", + # "limit_price": "20000.00", + # "order_value": "4", + # "avg_price": "0", + # "trigger_price": "0", + # "ref_price": "0", + # "cumulative_quantity": "0", + # "cumulative_value": "0", + # "cumulative_fee": "0", + # "status": "ACTIVE", + # "update_user_id": "ae075bef-1234-4321-bd6g-bb9007252a63", + # "order_date": "2023-06-15", + # "instrument_name": "BTC_USD", + # "fee_instrument_name": "BTC", + # "create_time": 1686870220684, + # "create_time_ns": "1686870220684239675", + # "update_time": 1686870220684 + # } + # } + # + order = self.safe_dict(response, 'result', {}) + return self.parse_order(order, market) + + def create_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + market = self.market(symbol) + uppercaseType = type.upper() + request: dict = { + 'instrument_name': market['id'], + 'side': side.upper(), + 'quantity': self.amount_to_precision(symbol, amount), + } + if (uppercaseType == 'LIMIT') or (uppercaseType == 'STOP_LIMIT') or (uppercaseType == 'TAKE_PROFIT_LIMIT'): + request['price'] = self.price_to_precision(symbol, price) + broker = self.safe_string(self.options, 'broker', 'CCXT') + request['broker_id'] = broker + marketType = None + marginMode = None + marketType, params = self.handle_market_type_and_params('createOrder', market, params) + marginMode, params = self.custom_handle_margin_mode_and_params('createOrder', params) + if (marketType == 'margin') or (marginMode is not None): + request['spot_margin'] = 'MARGIN' + elif marketType == 'spot': + request['spot_margin'] = 'SPOT' + timeInForce = self.safe_string_upper_2(params, 'timeInForce', 'time_in_force') + if timeInForce is not None: + if timeInForce == 'GTC': + request['time_in_force'] = 'GOOD_TILL_CANCEL' + elif timeInForce == 'IOC': + request['time_in_force'] = 'IMMEDIATE_OR_CANCEL' + elif timeInForce == 'FOK': + request['time_in_force'] = 'FILL_OR_KILL' + else: + request['time_in_force'] = timeInForce + postOnly = self.safe_bool(params, 'postOnly', False) + if (postOnly) or (timeInForce == 'PO'): + request['exec_inst'] = ['POST_ONLY'] + request['time_in_force'] = 'GOOD_TILL_CANCEL' + triggerPrice = self.safe_string_n(params, ['stopPrice', 'triggerPrice', 'ref_price']) + stopLossPrice = self.safe_number(params, 'stopLossPrice') + takeProfitPrice = self.safe_number(params, 'takeProfitPrice') + isTrigger = (triggerPrice is not None) + isStopLossTrigger = (stopLossPrice is not None) + isTakeProfitTrigger = (takeProfitPrice is not None) + if isTrigger: + request['ref_price'] = self.price_to_precision(symbol, triggerPrice) + priceString = self.number_to_string(price) + if (uppercaseType == 'LIMIT') or (uppercaseType == 'STOP_LIMIT') or (uppercaseType == 'TAKE_PROFIT_LIMIT'): + if side == 'buy': + if Precise.string_lt(priceString, triggerPrice): + request['type'] = 'TAKE_PROFIT_LIMIT' + else: + request['type'] = 'STOP_LIMIT' + else: + if Precise.string_lt(priceString, triggerPrice): + request['type'] = 'STOP_LIMIT' + else: + request['type'] = 'TAKE_PROFIT_LIMIT' + else: + if side == 'buy': + if Precise.string_lt(priceString, triggerPrice): + request['type'] = 'TAKE_PROFIT' + else: + request['type'] = 'STOP_LOSS' + else: + if Precise.string_lt(priceString, triggerPrice): + request['type'] = 'STOP_LOSS' + else: + request['type'] = 'TAKE_PROFIT' + elif isStopLossTrigger: + if (uppercaseType == 'LIMIT') or (uppercaseType == 'STOP_LIMIT'): + request['type'] = 'STOP_LIMIT' + else: + request['type'] = 'STOP_LOSS' + request['ref_price'] = self.price_to_precision(symbol, stopLossPrice) + elif isTakeProfitTrigger: + if (uppercaseType == 'LIMIT') or (uppercaseType == 'TAKE_PROFIT_LIMIT'): + request['type'] = 'TAKE_PROFIT_LIMIT' + else: + request['type'] = 'TAKE_PROFIT' + request['ref_price'] = self.price_to_precision(symbol, takeProfitPrice) + else: + request['type'] = uppercaseType + params = self.omit(params, ['postOnly', 'clientOrderId', 'timeInForce', 'stopPrice', 'triggerPrice', 'stopLossPrice', 'takeProfitPrice']) + return self.extend(request, params) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#private-create-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market', 'limit', 'stop_loss', 'stop_limit', 'take_profit', 'take_profit_limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.timeInForce]: 'GTC', 'IOC', 'FOK' or 'PO' + :param str [params.ref_price_type]: 'MARK_PRICE', 'INDEX_PRICE', 'LAST_PRICE' which trigger price type to use, default is MARK_PRICE + :param float [params.triggerPrice]: price to trigger a trigger order + :param float [params.stopLossPrice]: price to trigger a stop-loss trigger order + :param float [params.takeProfitPrice]: price to trigger a take-profit trigger order + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + request = self.create_order_request(symbol, type, side, amount, price, params) + response = self.v1PrivatePostPrivateCreateOrder(request) + # + # { + # "id": 1686804664362, + # "method": "private/create-order", + # "code" : 0, + # "result": { + # "order_id": "6540219377766741832", + # "client_oid": "CCXT_d6ef7c3db6c1495aa8b757" + # } + # } + # + result = self.safe_dict(response, 'result', {}) + return self.parse_order(result, market) + + def create_orders(self, orders: List[OrderRequest], params={}): + """ + create a list of trade orders + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#private-create-order-list-list + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#private-create-order-list-oco + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + ordersRequests = [] + for i in range(0, len(orders)): + rawOrder = orders[i] + marketId = self.safe_string(rawOrder, 'symbol') + type = self.safe_string(rawOrder, 'type') + side = self.safe_string(rawOrder, 'side') + amount = self.safe_value(rawOrder, 'amount') + price = self.safe_value(rawOrder, 'price') + orderParams = self.safe_dict(rawOrder, 'params', {}) + orderRequest = self.create_advanced_order_request(marketId, type, side, amount, price, orderParams) + ordersRequests.append(orderRequest) + contigency = self.safe_string(params, 'contingency_type', 'LIST') + request: dict = { + 'contingency_type': contigency, # or OCO + 'order_list': ordersRequests, + } + response = self.v1PrivatePostPrivateCreateOrderList(self.extend(request, params)) + # + # { + # "id": 12, + # "method": "private/create-order-list", + # "code": 10001, + # "result": { + # "result_list": [ + # { + # "index": 0, + # "code": 0, + # "order_id": "2015106383706015873", + # "client_oid": "my_order_0001" + # }, + # { + # "index": 1, + # "code": 20007, + # "message": "INVALID_REQUEST", + # "client_oid": "my_order_0002" + # } + # ] + # } + # } + # + # { + # "id" : 1698068111133, + # "method" : "private/create-order-list", + # "code" : 0, + # "result" : [{ + # "code" : 0, + # "index" : 0, + # "client_oid" : "1698068111133_0", + # "order_id" : "6142909896519488206" + # }, { + # "code" : 306, + # "index" : 1, + # "client_oid" : "1698068111133_1", + # "message" : "INSUFFICIENT_AVAILABLE_BALANCE", + # "order_id" : "6142909896519488207" + # }] + # } + # + result = self.safe_value(response, 'result', []) + listId = self.safe_string(result, 'list_id') + if listId is not None: + ocoOrders = [{'order_id': listId}] + return self.parse_orders(ocoOrders) + return self.parse_orders(result) + + def create_advanced_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + # differs slightly from createOrderRequest + # since the advanced order endpoint requires a different set of parameters + # namely here we don't support ref_price or spot_margin + # and market-buy orders need to send notional instead of quantity + market = self.market(symbol) + uppercaseType = type.upper() + request: dict = { + 'instrument_name': market['id'], + 'side': side.upper(), + } + if (uppercaseType == 'LIMIT') or (uppercaseType == 'STOP_LIMIT') or (uppercaseType == 'TAKE_PROFIT_LIMIT'): + request['price'] = self.price_to_precision(symbol, price) + broker = self.safe_string(self.options, 'broker', 'CCXT') + request['broker_id'] = broker + timeInForce = self.safe_string_upper_2(params, 'timeInForce', 'time_in_force') + if timeInForce is not None: + if timeInForce == 'GTC': + request['time_in_force'] = 'GOOD_TILL_CANCEL' + elif timeInForce == 'IOC': + request['time_in_force'] = 'IMMEDIATE_OR_CANCEL' + elif timeInForce == 'FOK': + request['time_in_force'] = 'FILL_OR_KILL' + else: + request['time_in_force'] = timeInForce + postOnly = self.safe_bool(params, 'postOnly', False) + if (postOnly) or (timeInForce == 'PO'): + request['exec_inst'] = ['POST_ONLY'] + request['time_in_force'] = 'GOOD_TILL_CANCEL' + triggerPrice = self.safe_string_n(params, ['stopPrice', 'triggerPrice', 'ref_price']) + stopLossPrice = self.safe_number(params, 'stopLossPrice') + takeProfitPrice = self.safe_number(params, 'takeProfitPrice') + isTrigger = (triggerPrice is not None) + isStopLossTrigger = (stopLossPrice is not None) + isTakeProfitTrigger = (takeProfitPrice is not None) + if isTrigger: + priceString = self.number_to_string(price) + if (uppercaseType == 'LIMIT') or (uppercaseType == 'STOP_LIMIT') or (uppercaseType == 'TAKE_PROFIT_LIMIT'): + if side == 'buy': + if Precise.string_lt(priceString, triggerPrice): + request['type'] = 'TAKE_PROFIT_LIMIT' + else: + request['type'] = 'STOP_LIMIT' + else: + if Precise.string_lt(priceString, triggerPrice): + request['type'] = 'STOP_LIMIT' + else: + request['type'] = 'TAKE_PROFIT_LIMIT' + else: + if side == 'buy': + if Precise.string_lt(priceString, triggerPrice): + request['type'] = 'TAKE_PROFIT' + else: + request['type'] = 'STOP_LOSS' + else: + if Precise.string_lt(priceString, triggerPrice): + request['type'] = 'STOP_LOSS' + else: + request['type'] = 'TAKE_PROFIT' + elif isStopLossTrigger: + if (uppercaseType == 'LIMIT') or (uppercaseType == 'STOP_LIMIT'): + request['type'] = 'STOP_LIMIT' + else: + request['type'] = 'STOP_LOSS' + elif isTakeProfitTrigger: + if (uppercaseType == 'LIMIT') or (uppercaseType == 'TAKE_PROFIT_LIMIT'): + request['type'] = 'TAKE_PROFIT_LIMIT' + else: + request['type'] = 'TAKE_PROFIT' + else: + request['type'] = uppercaseType + if (side == 'buy') and ((uppercaseType == 'MARKET') or (uppercaseType == 'STOP_LOSS') or (uppercaseType == 'TAKE_PROFIT')): + # use createmarketBuy logic here + quoteAmount = None + createMarketBuyOrderRequiresPrice = True + createMarketBuyOrderRequiresPrice, params = self.handle_option_and_params(params, 'createOrder', 'createMarketBuyOrderRequiresPrice', True) + cost = self.safe_number_2(params, 'cost', 'notional') + params = self.omit(params, 'cost') + if cost is not None: + quoteAmount = self.cost_to_precision(symbol, cost) + elif createMarketBuyOrderRequiresPrice: + if price is None: + raise InvalidOrder(self.id + ' createOrder() requires the price argument for market buy orders to calculate the total cost to spend(amount * price), alternatively set the createMarketBuyOrderRequiresPrice option or param to False and pass the cost to spend(quote quantity) in the amount argument') + else: + amountString = self.number_to_string(amount) + priceString = self.number_to_string(price) + costRequest = Precise.string_mul(amountString, priceString) + quoteAmount = self.cost_to_precision(symbol, costRequest) + else: + quoteAmount = self.cost_to_precision(symbol, amount) + request['notional'] = quoteAmount + else: + request['quantity'] = self.amount_to_precision(symbol, amount) + params = self.omit(params, ['postOnly', 'clientOrderId', 'timeInForce', 'stopPrice', 'triggerPrice', 'stopLossPrice', 'takeProfitPrice']) + return self.extend(request, params) + + def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + """ + edit a trade order + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#private-amend-order + + :param str id: order id + :param str symbol: unified market symbol of the order to edit + :param str [type]: not used by cryptocom editOrder + :param str [side]: not used by cryptocom editOrder + :param float amount:(mandatory) how much of the currency you want to trade in units of the base currency + :param float price:(mandatory) the price for the order, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.clientOrderId]: the original client order id of the order to edit, required if id is not provided + :returns dict: an `order structure ` + """ + self.load_markets() + request = self.edit_order_request(id, symbol, amount, price, params) + response = self.v1PrivatePostPrivateAmendOrder(request) + result = self.safe_dict(response, 'result', {}) + return self.parse_order(result) + + def edit_order_request(self, id: str, symbol: str, amount: float, price: Num = None, params={}): + request: dict = {} + if id is not None: + request['order_id'] = id + else: + originalClientOrderId = self.safe_string_2(params, 'orig_client_oid', 'clientOrderId') + if originalClientOrderId is None: + raise ArgumentsRequired(self.id + ' editOrder() requires an id argument or orig_client_oid parameter') + else: + request['orig_client_oid'] = originalClientOrderId + params = self.omit(params, ['orig_client_oid', 'clientOrderId']) + if (amount is None) or (price is None): + raise ArgumentsRequired(self.id + ' editOrder() requires both amount and price arguments. If you do not want to change the amount or price, you should pass the original values') + request['new_quantity'] = self.amount_to_precision(symbol, amount) + request['new_price'] = self.price_to_precision(symbol, price) + return self.extend(request, params) + + def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#private-cancel-all-orders + + :param str symbol: unified market symbol of the orders to cancel + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict} Returns exchange raw message{@link https://docs.ccxt.com/#/?id=order-structure: + """ + self.load_markets() + market = None + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['instrument_name'] = market['id'] + response = self.v1PrivatePostPrivateCancelAllOrders(self.extend(request, params)) + return [self.safe_order({'info': response})] + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#private-cancel-order + + :param str id: the order id of the order to cancel + :param str [symbol]: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + request: dict = { + 'order_id': id, + } + response = self.v1PrivatePostPrivateCancelOrder(self.extend(request, params)) + # + # { + # "id": 1686882846638, + # "method": "private/cancel-order", + # "code": 0, + # "message": "NO_ERROR", + # "result": { + # "client_oid": "CCXT_c2d2152cc32d40a3ae7fbf", + # "order_id": "6142909895025252686" + # } + # } + # + result = self.safe_dict(response, 'result', {}) + return self.parse_order(result, market) + + def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): + """ + cancel multiple orders + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#private-cancel-order-list-list + + :param str[] ids: order ids + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrders() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + orderRequests = [] + for i in range(0, len(ids)): + id = ids[i] + order: dict = { + 'instrument_name': market['id'], + 'order_id': str(id), + } + orderRequests.append(order) + request: dict = { + 'contingency_type': 'LIST', + 'order_list': orderRequests, + } + response = self.v1PrivatePostPrivateCancelOrderList(self.extend(request, params)) + result = self.safe_list(response, 'result', []) + return self.parse_orders(result, market, None, None, params) + + def cancel_orders_for_symbols(self, orders: List[CancellationRequest], params={}): + """ + cancel multiple orders for multiple symbols + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#private-cancel-order-list-list + + :param CancellationRequest[] orders: each order should contain the parameters required by cancelOrder namely id and symbol, example [{"id": "a", "symbol": "BTC/USDT"}, {"id": "b", "symbol": "ETH/USDT"}] + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an list of `order structures ` + """ + self.load_markets() + orderRequests = [] + for i in range(0, len(orders)): + order = orders[i] + id = self.safe_string(order, 'id') + symbol = self.safe_string(order, 'symbol') + market = self.market(symbol) + orderItem: dict = { + 'instrument_name': market['id'], + 'order_id': str(id), + } + orderRequests.append(orderItem) + request: dict = { + 'contingency_type': 'LIST', + 'order_list': orderRequests, + } + response = self.v1PrivatePostPrivateCancelOrderList(self.extend(request, params)) + result = self.safe_list(response, 'result', []) + return self.parse_orders(result, None, None, None, params) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#private-get-open-orders + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + market = None + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['instrument_name'] = market['id'] + response = self.v1PrivatePostPrivateGetOpenOrders(self.extend(request, params)) + # + # { + # "id": 1686806134961, + # "method": "private/get-open-orders", + # "code": 0, + # "result": { + # "data": [ + # { + # "account_id": "ce075bef-1234-4321-bd6g-ff9007252e63", + # "order_id": "6530219477767564494", + # "client_oid": "CCXT_7ce730f0388441df9bc218", + # "order_type": "LIMIT", + # "time_in_force": "GOOD_TILL_CANCEL", + # "side": "BUY", + # "exec_inst": [], + # "quantity": "0.00020", + # "limit_price": "20000.00", + # "order_value": "4", + # "avg_price": "0", + # "trigger_price": "0", + # "ref_price": "0", + # "cumulative_quantity": "0", + # "cumulative_value": "0", + # "cumulative_fee": "0", + # "status": "ACTIVE", + # "update_user_id": "ce075bef-1234-4321-bd6g-gg9007252e63", + # "order_date": "2023-06-15", + # "instrument_name": "BTC_USD", + # "fee_instrument_name": "BTC", + # "create_time": 1686806053992, + # "create_time_ns": "1686806053992921880", + # "update_time": 1686806053993 + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'result', {}) + orders = self.safe_list(data, 'data', []) + return self.parse_orders(orders, market, since, limit) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#private-get-trades + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for, maximum date range is one day + :param int [limit]: the maximum number of trade structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms for the ending date filter, default is the current time + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchMyTrades', 'paginate') + if paginate: + return self.fetch_paginated_call_dynamic('fetchMyTrades', symbol, since, limit, params, 100) + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['instrument_name'] = market['id'] + if since is not None: + request['start_time'] = since + if limit is not None: + request['limit'] = limit + until = self.safe_integer(params, 'until') + params = self.omit(params, ['until']) + if until is not None: + request['end_time'] = until + response = self.v1PrivatePostPrivateGetTrades(self.extend(request, params)) + # + # { + # "id": 1686942003520, + # "method": "private/get-trades", + # "code": 0, + # "result": { + # "data": [ + # { + # "account_id": "ds075abc-1234-4321-bd6g-ff9007252r63", + # "event_date": "2023-06-16", + # "journal_type": "TRADING", + # "side": "BUY", + # "instrument_name": "BTC_USD", + # "fees": "-0.0000000525", + # "trade_id": "6142909898247428343", + # "trade_match_id": "4611686018455978480", + # "create_time": 1686941992887, + # "traded_price": "26347.16", + # "traded_quantity": "0.00021", + # "fee_instrument_name": "BTC", + # "client_oid": "d1c70a60-810e-4c92-b2a0-72b931cb31e0", + # "taker_side": "TAKER", + # "order_id": "6142909895036331486", + # "create_time_ns": "1686941992887207066" + # } + # ] + # } + # } + # + result = self.safe_dict(response, 'result', {}) + trades = self.safe_list(result, 'data', []) + return self.parse_trades(trades, market, since, limit) + + def parse_address(self, addressString): + address = None + tag = None + rawTag = None + if addressString.find('?') > 0: + address, rawTag = addressString.split('?') + splitted = rawTag.split('=') + tag = splitted[1] + else: + address = addressString + return [address, tag] + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#private-create-withdrawal + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.load_markets() + currency = self.safe_currency(code) # for instance, USDC is not inferred from markets but it's still available + request: dict = { + 'currency': currency['id'], + 'amount': amount, + 'address': address, + } + if tag is not None: + request['address_tag'] = tag + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + networkId = self.network_code_to_id(networkCode) + if networkId is not None: + request['network_id'] = networkId + response = self.v1PrivatePostPrivateCreateWithdrawal(self.extend(request, params)) + # + # { + # "id":-1, + # "method":"private/create-withdrawal", + # "code":0, + # "result": { + # "id": 2220, + # "amount": 1, + # "fee": 0.0004, + # "symbol": "BTC", + # "address": "2NBqqD5GRJ8wHy1PYyCXTe9ke5226FhavBf", + # "client_wid": "my_withdrawal_002", + # "create_time":1607063412000 + # } + # } + # + result = self.safe_dict(response, 'result') + return self.parse_transaction(result, currency) + + def fetch_deposit_addresses_by_network(self, code: str, params={}) -> List[DepositAddress]: + """ + fetch a dictionary of addresses for a currency, indexed by network + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#private-get-deposit-address + + :param str code: unified currency code of the currency for the deposit address + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `address structures ` indexed by the network + """ + self.load_markets() + currency = self.safe_currency(code) + request: dict = { + 'currency': currency['id'], + } + response = self.v1PrivatePostPrivateGetDepositAddress(self.extend(request, params)) + # + # { + # "id": 1234555011221, + # "method": "private/get-deposit-address", + # "code": 0, + # "result": { + # "deposit_address_list": [ + # { + # "currency": "BTC", + # "create_time": 1686730755000, + # "id": "3737377", + # "address": "3N9afggxTSmJ3H4jaMQuWyEiLBzZdAbK6d", + # "status":"1", + # "network": "BTC" + # }, + # ] + # } + # } + # + data = self.safe_dict(response, 'result', {}) + addresses = self.safe_list(data, 'deposit_address_list', []) + addressesLength = len(addresses) + if addressesLength == 0: + raise ExchangeError(self.id + ' fetchDepositAddressesByNetwork() generating address...') + result: dict = {} + for i in range(0, addressesLength): + value = self.safe_dict(addresses, i) + addressString = self.safe_string(value, 'address') + currencyId = self.safe_string(value, 'currency') + responseCode = self.safe_currency_code(currencyId) + address, tag = self.parse_address(addressString) + self.check_address(address) + networkId = self.safe_string(value, 'network') + network = self.network_id_to_code(networkId, responseCode) + result[network] = { + 'info': value, + 'currency': responseCode, + 'network': network, + 'address': address, + 'tag': tag, + } + return result + + def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#private-get-deposit-address + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + network = self.safe_string_upper(params, 'network') + params = self.omit(params, ['network']) + depositAddresses = self.fetch_deposit_addresses_by_network(code, params) + if network in depositAddresses: + return depositAddresses[network] + else: + keys = list(depositAddresses.keys()) + return depositAddresses[keys[0]] + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#private-get-deposit-history + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms for the ending date filter, default is the current time + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + currency = None + request: dict = {} + if code is not None: + currency = self.safe_currency(code) + request['currency'] = currency['id'] + if since is not None: + # 90 days date range + request['start_ts'] = since + if limit is not None: + request['page_size'] = limit + until = self.safe_integer(params, 'until') + params = self.omit(params, ['until']) + if until is not None: + request['end_ts'] = until + response = self.v1PrivatePostPrivateGetDepositHistory(self.extend(request, params)) + # + # { + # "id": 1688701375714, + # "method": "private/get-deposit-history", + # "code": 0, + # "result": { + # "deposit_list": [ + # { + # "currency": "BTC", + # "fee": 0, + # "create_time": 1688023659000, + # "id": "6201135", + # "update_time": 1688178509000, + # "amount": 0.00114571, + # "address": "1234fggxTSmJ3H4jaMQuWyEiLBzZdAbK6d", + # "status": "1", + # "txid": "f0ae4202b76eb999c301eccdde44dc639bee42d1fdd5974105286ca3393f6065/2" + # }, + # ] + # } + # } + # + data = self.safe_dict(response, 'result', {}) + depositList = self.safe_list(data, 'deposit_list', []) + return self.parse_transactions(depositList, currency, since, limit) + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#private-get-withdrawal-history + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms for the ending date filter, default is the current time + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + currency = None + request: dict = {} + if code is not None: + currency = self.safe_currency(code) + request['currency'] = currency['id'] + if since is not None: + # 90 days date range + request['start_ts'] = since + if limit is not None: + request['page_size'] = limit + until = self.safe_integer(params, 'until') + params = self.omit(params, ['until']) + if until is not None: + request['end_ts'] = until + response = self.v1PrivatePostPrivateGetWithdrawalHistory(self.extend(request, params)) + # + # { + # "id": 1688613879534, + # "method": "private/get-withdrawal-history", + # "code": 0, + # "result": { + # "withdrawal_list": [ + # { + # "currency": "BTC", + # "client_wid": "", + # "fee": 0.0005, + # "create_time": 1688613850000, + # "id": "5275977", + # "update_time": 1688613850000, + # "amount": 0.0005, + # "address": "1234NMEWbiF8ZkwUMxmfzMxi2A1MQ44bMn", + # "status": "1", + # "txid": "", + # "network_id": "BTC" + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'result', {}) + withdrawalList = self.safe_list(data, 'withdrawal_list', []) + return self.parse_transactions(withdrawalList, currency, since, limit) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # fetchTicker + # + # { + # "i": "BTC_USD", + # "h": "30821.45", + # "l": "28685.11", + # "a": "30446.00", + # "v": "1767.8734", + # "vv": "52436726.42", + # "c": "0.0583", + # "b": "30442.00", + # "k": "30447.66", + # "t": 1687403045415 + # } + # + # fetchTickers + # + # { + # "i": "AVAXUSD-PERP", + # "h": "13.209", + # "l": "12.148", + # "a": "13.209", + # "v": "1109.8", + # "vv": "14017.33", + # "c": "0.0732", + # "b": "13.210", + # "k": "13.230", + # "oi": "10888.9", + # "t": 1687402657575 + # } + # + timestamp = self.safe_integer(ticker, 't') + marketId = self.safe_string(ticker, 'i') + market = self.safe_market(marketId, market, '_') + quote = self.safe_string(market, 'quote') + last = self.safe_string(ticker, 'a') + return self.safe_ticker({ + 'symbol': market['symbol'], + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_number(ticker, 'h'), + 'low': self.safe_number(ticker, 'l'), + 'bid': self.safe_number(ticker, 'b'), + 'bidVolume': None, + 'ask': self.safe_number(ticker, 'k'), + 'askVolume': None, + 'vwap': None, + 'open': None, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': self.safe_string(ticker, 'c'), + 'average': None, + 'baseVolume': self.safe_string(ticker, 'v'), + 'quoteVolume': self.safe_string(ticker, 'vv') if (quote == 'USD') else None, + 'info': ticker, + }, market) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades + # + # { + # "s": "sell", + # "p": "26386.00", + # "q": "0.00453", + # "tn": 1686944282062, + # "tn": 1704476468851524373, + # "d": "4611686018455979970", + # "i": "BTC_USD" + # } + # + # fetchMyTrades + # + # { + # "account_id": "ds075abc-1234-4321-bd6g-ff9007252r63", + # "event_date": "2023-06-16", + # "journal_type": "TRADING", + # "side": "BUY", + # "instrument_name": "BTC_USD", + # "fees": "-0.0000000525", + # "trade_id": "6142909898247428343", + # "trade_match_id": "4611686018455978480", + # "create_time": 1686941992887, + # "traded_price": "26347.16", + # "traded_quantity": "0.00021", + # "fee_instrument_name": "BTC", + # "client_oid": "d1c70a60-1234-4c92-b2a0-72b931cb31e0", + # "taker_side": "TAKER", + # "order_id": "6142909895036331486", + # "create_time_ns": "1686941992887207066" + # } + # + timestamp = self.safe_integer_2(trade, 't', 'create_time') + marketId = self.safe_string_2(trade, 'i', 'instrument_name') + market = self.safe_market(marketId, market, '_') + feeCurrency = self.safe_string(trade, 'fee_instrument_name') + feeCostString = self.safe_string(trade, 'fees') + return self.safe_trade({ + 'info': trade, + 'id': self.safe_string_2(trade, 'd', 'trade_id'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'order': self.safe_string(trade, 'order_id'), + 'side': self.safe_string_lower_2(trade, 's', 'side'), + 'takerOrMaker': self.safe_string_lower(trade, 'taker_side'), + 'price': self.safe_number_2(trade, 'p', 'traded_price'), + 'amount': self.safe_number_2(trade, 'q', 'traded_quantity'), + 'cost': None, + 'type': None, + 'fee': { + 'currency': self.safe_currency_code(feeCurrency), + 'cost': self.parse_number(Precise.string_neg(feeCostString)), + }, + }, market) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # { + # "o": "26949.89", + # "h": "26957.64", + # "l": "26948.24", + # "c": "26950.00", + # "v": "0.0670", + # "t": 1687237080000 + # } + # + return [ + self.safe_integer(ohlcv, 't'), + self.safe_number(ohlcv, 'o'), + self.safe_number(ohlcv, 'h'), + self.safe_number(ohlcv, 'l'), + self.safe_number(ohlcv, 'c'), + self.safe_number(ohlcv, 'v'), + ] + + def parse_order_status(self, status: Str): + statuses: dict = { + 'ACTIVE': 'open', + 'CANCELED': 'canceled', + 'FILLED': 'closed', + 'REJECTED': 'rejected', + 'EXPIRED': 'expired', + } + return self.safe_string(statuses, status, status) + + def parse_time_in_force(self, timeInForce: Str): + timeInForces: dict = { + 'GOOD_TILL_CANCEL': 'GTC', + 'IMMEDIATE_OR_CANCEL': 'IOC', + 'FILL_OR_KILL': 'FOK', + } + return self.safe_string(timeInForces, timeInForce, timeInForce) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder, cancelOrder + # + # { + # "order_id": "6540219377766741832", + # "client_oid": "CCXT_d6ef7c3db6c1495aa8b757" + # } + # + # fetchOpenOrders, fetchOrder, fetchOrders + # + # { + # "account_id": "ce075bef-1234-4321-bd6g-ff9007252e63", + # "order_id": "6530219477767564494", + # "client_oid": "CCXT_7ce730f0388441df9bc218", + # "order_type": "LIMIT", + # "time_in_force": "GOOD_TILL_CANCEL", + # "side": "BUY", + # "exec_inst": [], + # "quantity": "0.00020", + # "limit_price": "20000.00", + # "order_value": "4", + # "avg_price": "0", + # "trigger_price": "0", + # "ref_price": "0", + # "cumulative_quantity": "0", + # "cumulative_value": "0", + # "cumulative_fee": "0", + # "status": "ACTIVE", + # "update_user_id": "ce075bef-1234-4321-bd6g-gg9007252e63", + # "order_date": "2023-06-15", + # "instrument_name": "BTC_USD", + # "fee_instrument_name": "BTC", + # "create_time": 1686806053992, + # "create_time_ns": "1686806053992921880", + # "update_time": 1686806053993 + # } + # + # createOrders + # { + # "code" : 306, + # "index" : 1, + # "client_oid" : "1698068111133_1", + # "message" : "INSUFFICIENT_AVAILABLE_BALANCE", + # "order_id" : "6142909896519488207" + # } + # + code = self.safe_integer(order, 'code') + if (code is not None) and (code != 0): + return self.safe_order({ + 'id': self.safe_string(order, 'order_id'), + 'clientOrderId': self.safe_string(order, 'client_oid'), + 'info': order, + 'status': 'rejected', + }) + created = self.safe_integer(order, 'create_time') + marketId = self.safe_string(order, 'instrument_name') + symbol = self.safe_symbol(marketId, market) + execInst = self.safe_value(order, 'exec_inst') + postOnly = None + if execInst is not None: + postOnly = False + for i in range(0, len(execInst)): + inst = execInst[i] + if inst == 'POST_ONLY': + postOnly = True + break + feeCurrency = self.safe_string(order, 'fee_instrument_name') + return self.safe_order({ + 'info': order, + 'id': self.safe_string(order, 'order_id'), + 'clientOrderId': self.safe_string(order, 'client_oid'), + 'timestamp': created, + 'datetime': self.iso8601(created), + 'lastTradeTimestamp': self.safe_integer(order, 'update_time'), + 'status': self.parse_order_status(self.safe_string(order, 'status')), + 'symbol': symbol, + 'type': self.safe_string_lower(order, 'order_type'), + 'timeInForce': self.parse_time_in_force(self.safe_string(order, 'time_in_force')), + 'postOnly': postOnly, + 'side': self.safe_string_lower(order, 'side'), + 'price': self.safe_number(order, 'limit_price'), + 'amount': self.safe_number(order, 'quantity'), + 'filled': self.safe_number(order, 'cumulative_quantity'), + 'remaining': None, + 'average': self.safe_number(order, 'avg_price'), + 'cost': self.safe_number(order, 'cumulative_value'), + 'fee': { + 'currency': self.safe_currency_code(feeCurrency), + 'cost': self.safe_number(order, 'cumulative_fee'), + }, + 'trades': [], + }, market) + + def parse_deposit_status(self, status): + statuses: dict = { + '0': 'pending', + '1': 'ok', + '2': 'failed', + '3': 'pending', + } + return self.safe_string(statuses, status, status) + + def parse_withdrawal_status(self, status): + statuses: dict = { + '0': 'pending', + '1': 'pending', + '2': 'failed', + '3': 'pending', + '4': 'failed', + '5': 'ok', + '6': 'canceled', + } + return self.safe_string(statuses, status, status) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fetchDeposits + # + # { + # "currency": "BTC", + # "fee": 0, + # "create_time": 1688023659000, + # "id": "6201135", + # "update_time": 1688178509000, + # "amount": 0.00114571, + # "address": "1234fggxTSmJ3H4jaMQuWyEiLBzZdAbK6d", + # "status": "1", + # "txid": "f0ae4202b76eb999c301eccdde44dc639bee42d1fdd5974105286ca3393f6065/2" + # } + # + # fetchWithdrawals + # + # { + # "currency": "BTC", + # "client_wid": "", + # "fee": 0.0005, + # "create_time": 1688613850000, + # "id": "5775977", + # "update_time": 1688613850000, + # "amount": 0.0005, + # "address": "1234NMEWbiF8ZkwUMxmfzMxi2A1MQ44bMn", + # "status": "1", + # "txid": "", + # "network_id": "BTC" + # } + # + # withdraw + # + # { + # "id": 2220, + # "amount": 1, + # "fee": 0.0004, + # "symbol": "BTC", + # "address": "2NBqqD5GRJ8wHy1PYyCXTe9ke5226FhavBf", + # "client_wid": "my_withdrawal_002", + # "create_time":1607063412000 + # } + # + type = None + rawStatus = self.safe_string(transaction, 'status') + status = None + if 'client_wid' in transaction: + type = 'withdrawal' + status = self.parse_withdrawal_status(rawStatus) + else: + type = 'deposit' + status = self.parse_deposit_status(rawStatus) + addressString = self.safe_string(transaction, 'address') + address, tag = self.parse_address(addressString) + currencyId = self.safe_string(transaction, 'currency') + code = self.safe_currency_code(currencyId, currency) + timestamp = self.safe_integer(transaction, 'create_time') + feeCost = self.safe_number(transaction, 'fee') + fee = None + if feeCost is not None: + fee = {'currency': code, 'cost': feeCost} + return { + 'info': transaction, + 'id': self.safe_string(transaction, 'id'), + 'txid': self.safe_string(transaction, 'txid'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': None, + 'address': address, + 'addressTo': address, + 'addressFrom': None, + 'tag': tag, + 'tagTo': tag, + 'tagFrom': None, + 'type': type, + 'amount': self.safe_number(transaction, 'amount'), + 'currency': code, + 'status': status, + 'updated': self.safe_integer(transaction, 'update_time'), + 'internal': None, + 'comment': self.safe_string(transaction, 'client_wid'), + 'fee': fee, + } + + def custom_handle_margin_mode_and_params(self, methodName, params={}): + """ + @ignore + marginMode specified by params["marginMode"], self.options["marginMode"], self.options["defaultMarginMode"], params["margin"] = True or self.options["defaultType"] = 'margin' + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Array: the marginMode in lowercase + """ + defaultType = self.safe_string(self.options, 'defaultType') + isMargin = self.safe_bool(params, 'margin', False) + params = self.omit(params, 'margin') + marginMode = None + marginMode, params = self.handle_margin_mode_and_params(methodName, params) + if marginMode is not None: + if marginMode != 'cross': + raise NotSupported(self.id + ' only cross margin is supported') + else: + if (defaultType == 'margin') or (isMargin is True): + marginMode = 'cross' + return [marginMode, params] + + def parse_deposit_withdraw_fee(self, fee, currency: Currency = None): + # + # { + # "full_name": "Alchemix", + # "default_network": "ETH", + # "network_list": [ + # { + # "network_id": "ETH", + # "withdrawal_fee": "0.25000000", + # "withdraw_enabled": True, + # "min_withdrawal_amount": "0.5", + # "deposit_enabled": True, + # "confirmation_required": "0" + # } + # ] + # } + # + networkList = self.safe_list(fee, 'network_list', []) + networkListLength = len(networkList) + result: dict = { + 'info': fee, + 'withdraw': { + 'fee': None, + 'percentage': None, + }, + 'deposit': { + 'fee': None, + 'percentage': None, + }, + 'networks': {}, + } + if networkList is not None: + for i in range(0, networkListLength): + networkInfo = networkList[i] + networkId = self.safe_string(networkInfo, 'network_id') + currencyCode = self.safe_string(currency, 'code') + networkCode = self.network_id_to_code(networkId, currencyCode) + result['networks'][networkCode] = { + 'deposit': {'fee': None, 'percentage': None}, + 'withdraw': {'fee': self.safe_number(networkInfo, 'withdrawal_fee'), 'percentage': False}, + } + if networkListLength == 1: + result['withdraw']['fee'] = self.safe_number(networkInfo, 'withdrawal_fee') + result['withdraw']['percentage'] = False + return result + + def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}): + """ + fetch deposit and withdraw fees + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#private-get-currency-networks + + :param str[]|None codes: list of unified currency codes + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `fee structures ` + """ + self.load_markets() + response = self.v1PrivatePostPrivateGetCurrencyNetworks(params) + data = self.safe_value(response, 'result') + currencyMap = self.safe_list(data, 'currency_map') + return self.parse_deposit_withdraw_fees(currencyMap, codes, 'full_name') + + def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#private-get-transactions + + :param str [code]: unified currency code + :param int [since]: timestamp in ms of the earliest ledger entry + :param int [limit]: max number of ledger entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms for the ending date filter, default is the current time + :returns dict: a `ledger structure ` + """ + self.load_markets() + request: dict = {} + currency = None + if code is not None: + currency = self.safe_currency(code) + if since is not None: + request['start_time'] = since + if limit is not None: + request['limit'] = limit + until = self.safe_integer(params, 'until') + params = self.omit(params, ['until']) + if until is not None: + request['end_time'] = until + response = self.v1PrivatePostPrivateGetTransactions(self.extend(request, params)) + # + # { + # "id": 1686813195698, + # "method": "private/get-transactions", + # "code": 0, + # "result": { + # "data": [ + # { + # "account_id": "ce075cef-1234-4321-bd6e-gf9007351e64", + # "event_date": "2023-06-15", + # "journal_type": "TRADING", + # "journal_id": "6530219460124075091", + # "transaction_qty": "6.0091224", + # "transaction_cost": "6.0091224", + # "realized_pnl": "0", + # "order_id": "6530219477766741833", + # "trade_id": "6530219495775954765", + # "trade_match_id": "4611686018455865176", + # "event_timestamp_ms": 1686804665013, + # "event_timestamp_ns": "1686804665013642422", + # "client_oid": "CCXT_d6ea7c5db6c1495aa8b758", + # "taker_side": "", + # "side": "BUY", + # "instrument_name": "USD" + # }, + # ] + # } + # } + # + result = self.safe_dict(response, 'result', {}) + ledger = self.safe_list(result, 'data', []) + return self.parse_ledger(ledger, currency, since, limit) + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + # + # { + # "account_id": "ce075cef-1234-4321-bd6e-gf9007351e64", + # "event_date": "2023-06-15", + # "journal_type": "TRADING", + # "journal_id": "6530219460124075091", + # "transaction_qty": "6.0091224", + # "transaction_cost": "6.0091224", + # "realized_pnl": "0", + # "order_id": "6530219477766741833", + # "trade_id": "6530219495775954765", + # "trade_match_id": "4611686018455865176", + # "event_timestamp_ms": 1686804665013, + # "event_timestamp_ns": "1686804665013642422", + # "client_oid": "CCXT_d6ea7c5db6c1495aa8b758", + # "taker_side": "", + # "side": "BUY", + # "instrument_name": "USD" + # } + # + timestamp = self.safe_integer(item, 'event_timestamp_ms') + currencyId = self.safe_string(item, 'instrument_name') + code = self.safe_currency_code(currencyId, currency) + currency = self.safe_currency(currencyId, currency) + amount = self.safe_string(item, 'transaction_qty') + direction = None + if Precise.string_lt(amount, '0'): + direction = 'out' + amount = Precise.string_abs(amount) + else: + direction = 'in' + return self.safe_ledger_entry({ + 'info': item, + 'id': self.safe_string(item, 'order_id'), + 'direction': direction, + 'account': self.safe_string(item, 'account_id'), + 'referenceId': self.safe_string(item, 'trade_id'), + 'referenceAccount': self.safe_string(item, 'trade_match_id'), + 'type': self.parse_ledger_entry_type(self.safe_string(item, 'journal_type')), + 'currency': code, + 'amount': self.parse_number(amount), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'before': None, + 'after': None, + 'status': None, + 'fee': { + 'currency': None, + 'cost': None, + }, + }, currency) + + def parse_ledger_entry_type(self, type): + ledgerType: dict = { + 'TRADING': 'trade', + 'TRADE_FEE': 'fee', + 'WITHDRAW_FEE': 'fee', + 'WITHDRAW': 'withdrawal', + 'DEPOSIT': 'deposit', + 'ROLLBACK_WITHDRAW': 'rollback', + 'ROLLBACK_DEPOSIT': 'rollback', + 'FUNDING': 'fee', + 'REALIZED_PNL': 'trade', + 'INSURANCE_FUND': 'insurance', + 'SOCIALIZED_LOSS': 'trade', + 'LIQUIDATION_FEE': 'fee', + 'SESSION_RESET': 'reset', + 'ADJUSTMENT': 'adjustment', + 'SESSION_SETTLE': 'settlement', + 'UNCOVERED_LOSS': 'trade', + 'ADMIN_ADJUSTMENT': 'adjustment', + 'DELIST': 'delist', + 'SETTLEMENT_FEE': 'fee', + 'AUTO_CONVERSION': 'conversion', + 'MANUAL_CONVERSION': 'conversion', + } + return self.safe_string(ledgerType, type, type) + + def fetch_accounts(self, params={}) -> List[Account]: + """ + fetch all the accounts associated with a profile + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#private-get-accounts + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `account structures ` indexed by the account type + """ + self.load_markets() + response = self.v1PrivatePostPrivateGetAccounts(params) + # + # { + # "id": 1234567894321, + # "method": "private/get-accounts", + # "code": 0, + # "result": { + # "master_account": { + # "uuid": "a1234abc-1234-4321-q5r7-b1ab0a0b12b", + # "user_uuid": "a1234abc-1234-4321-q5r7-b1ab0a0b12b", + # "enabled": True, + # "tradable": True, + # "name": "YOUR_NAME", + # "country_code": "CAN", + # "phone_country_code": "CAN", + # "incorp_country_code": "", + # "margin_access": "DEFAULT", + # "derivatives_access": "DEFAULT", + # "create_time": 1656445188000, + # "update_time": 1660794567262, + # "two_fa_enabled": True, + # "kyc_level": "ADVANCED", + # "suspended": False, + # "terminated": False, + # "spot_enabled": False, + # "margin_enabled": False, + # "derivatives_enabled": False + # }, + # "sub_account_list": [] + # } + # } + # + result = self.safe_dict(response, 'result', {}) + masterAccount = self.safe_dict(result, 'master_account', {}) + accounts = self.safe_list(result, 'sub_account_list', []) + accounts.append(masterAccount) + return self.parse_accounts(accounts, params) + + def parse_account(self, account): + # + # { + # "uuid": "a1234abc-1234-4321-q5r7-b1ab0a0b12b", + # "user_uuid": "a1234abc-1234-4321-q5r7-b1ab0a0b12b", + # "master_account_uuid": "a1234abc-1234-4321-q5r7-b1ab0a0b12b", + # "label": "FORMER_MASTER_MARGIN", + # "enabled": True, + # "tradable": True, + # "name": "YOUR_NAME", + # "country_code": "YOUR_COUNTRY_CODE", + # "incorp_country_code": "", + # "margin_access": "DEFAULT", + # "derivatives_access": "DEFAULT", + # "create_time": 1656481992000, + # "update_time": 1667272884594, + # "two_fa_enabled": False, + # "kyc_level": "ADVANCED", + # "suspended": False, + # "terminated": False, + # "spot_enabled": False, + # "margin_enabled": False, + # "derivatives_enabled": False, + # "system_label": "FORMER_MASTER_MARGIN" + # } + # + return { + 'id': self.safe_string(account, 'uuid'), + 'type': self.safe_string(account, 'label'), + 'code': None, + 'info': account, + } + + def fetch_settlement_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical settlement records + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#public-get-expired-settlement-price + + :param str symbol: unified market symbol of the settlement history + :param int [since]: timestamp in ms + :param int [limit]: number of records + :param dict [params]: exchange specific params + :param int [params.type]: 'future', 'option' + :returns dict[]: a list of `settlement history objects ` + """ + self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + type = None + type, params = self.handle_market_type_and_params('fetchSettlementHistory', market, params) + self.check_required_argument('fetchSettlementHistory', type, 'type', ['future', 'option', 'WARRANT', 'FUTURE']) + if type == 'option': + type = 'WARRANT' + request: dict = { + 'instrument_type': type.upper(), + } + response = self.v1PublicGetPublicGetExpiredSettlementPrice(self.extend(request, params)) + # + # { + # "id": -1, + # "method": "public/get-expired-settlement-price", + # "code": 0, + # "result": { + # "data": [ + # { + # "i": "BTCUSD-230526", + # "x": 1685088000000, + # "v": "26464.1", + # "t": 1685087999500 + # } + # ] + # } + # } + # + result = self.safe_dict(response, 'result', {}) + data = self.safe_list(result, 'data', []) + settlements = self.parse_settlements(data, market) + sorted = self.sort_by(settlements, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, symbol, since, limit) + + def parse_settlement(self, settlement, market): + # + # { + # "i": "BTCUSD-230526", + # "x": 1685088000000, + # "v": "26464.1", + # "t": 1685087999500 + # } + # + timestamp = self.safe_integer(settlement, 'x') + marketId = self.safe_string(settlement, 'i') + return { + 'info': settlement, + 'symbol': self.safe_symbol(marketId, market), + 'price': self.safe_number(settlement, 'v'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + + def parse_settlements(self, settlements, market): + # + # [ + # { + # "i": "BTCUSD-230526", + # "x": 1685088000000, + # "v": "26464.1", + # "t": 1685087999500 + # } + # ] + # + result = [] + for i in range(0, len(settlements)): + result.append(self.parse_settlement(settlements[i], market)) + return result + + def fetch_funding_rate(self, symbol: str, params={}): + """ + fetches historical funding rates + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#public-get-valuations + + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `funding rate structures ` + """ + self.load_markets() + market = self.market(symbol) + if not market['swap']: + raise BadSymbol(self.id + ' fetchFundingRate() supports swap contracts only') + request: dict = { + 'instrument_name': market['id'], + 'valuation_type': 'estimated_funding_rate', + 'count': 1, + } + response = self.v1PublicGetPublicGetValuations(self.extend(request, params)) + # + # { + # "id": -1, + # "method": "public/get-valuations", + # "code": 0, + # "result": { + # "data": [ + # { + # "v": "-0.000001884", + # "t": 1687892400000 + # }, + # ], + # "instrument_name": "BTCUSD-PERP" + # } + # } + # + result = self.safe_dict(response, 'result', {}) + data = self.safe_list(result, 'data', []) + entry = self.safe_dict(data, 0, {}) + return self.parse_funding_rate(entry, market) + + def parse_funding_rate(self, contract, market: Market = None) -> FundingRate: + # + # { + # "v": "-0.000001884", + # "t": 1687892400000 + # }, + # + timestamp = self.safe_integer(contract, 't') + fundingTimestamp = None + if timestamp is not None: + fundingTimestamp = int(math.ceil(timestamp / 3600000)) * 3600000 # end of the next hour + return { + 'info': contract, + 'symbol': self.safe_symbol(None, market), + 'markPrice': None, + 'indexPrice': None, + 'interestRate': None, + 'estimatedSettlePrice': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'fundingRate': self.safe_number(contract, 'v'), + 'fundingTimestamp': fundingTimestamp, + 'fundingDatetime': self.iso8601(fundingTimestamp), + 'nextFundingRate': None, + 'nextFundingTimestamp': None, + 'nextFundingDatetime': None, + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': '1h', + } + + def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical funding rates + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#public-get-valuations + + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: timestamp in ms of the earliest funding rate to fetch + :param int [limit]: the maximum amount of [funding rate structures] to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms for the ending date filter, default is the current time + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `funding rate structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchFundingRateHistory() requires a symbol argument') + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchFundingRateHistory', 'paginate') + if paginate: + return self.fetch_paginated_call_deterministic('fetchFundingRateHistory', symbol, since, limit, '8h', params) + market = self.market(symbol) + if not market['swap']: + raise BadSymbol(self.id + ' fetchFundingRateHistory() supports swap contracts only') + request: dict = { + 'instrument_name': market['id'], + 'valuation_type': 'funding_hist', + } + if since is not None: + request['start_ts'] = since + if limit is not None: + request['count'] = limit + until = self.safe_integer(params, 'until') + params = self.omit(params, ['until']) + if until is not None: + request['end_ts'] = until + response = self.v1PublicGetPublicGetValuations(self.extend(request, params)) + # + # { + # "id": -1, + # "method": "public/get-valuations", + # "code": 0, + # "result": { + # "data": [ + # { + # "v": "-0.000001884", + # "t": 1687892400000 + # }, + # ], + # "instrument_name": "BTCUSD-PERP" + # } + # } + # + result = self.safe_dict(response, 'result', {}) + data = self.safe_list(result, 'data', []) + marketId = self.safe_string(result, 'instrument_name') + rates = [] + for i in range(0, len(data)): + entry = data[i] + timestamp = self.safe_integer(entry, 't') + rates.append({ + 'info': entry, + 'symbol': self.safe_symbol(marketId, market), + 'fundingRate': self.safe_number(entry, 'v'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + sorted = self.sort_by(rates, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, market['symbol'], since, limit) + + def fetch_position(self, symbol: str, params={}): + """ + fetch data on a single open contract trade position + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#private-get-positions + + :param str symbol: unified market symbol of the market the position is held in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `position structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'instrument_name': market['id'], + } + response = self.v1PrivatePostPrivateGetPositions(self.extend(request, params)) + # + # { + # "id": 1688015952050, + # "method": "private/get-positions", + # "code": 0, + # "result": { + # "data": [ + # { + # "account_id": "ce075bef-b600-4277-bd6e-ff9007251e63", + # "quantity": "0.0001", + # "cost": "3.02392", + # "open_pos_cost": "3.02392", + # "open_position_pnl": "-0.0010281328", + # "session_pnl": "-0.0010281328", + # "update_timestamp_ms": 1688015919091, + # "instrument_name": "BTCUSD-PERP", + # "type": "PERPETUAL_SWAP" + # } + # ] + # } + # } + # + result = self.safe_dict(response, 'result', {}) + data = self.safe_list(result, 'data', []) + return self.parse_position(self.safe_dict(data, 0), market) + + def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#private-get-positions + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + self.load_markets() + symbols = self.market_symbols(symbols) + request: dict = {} + market = None + if symbols is not None: + symbol = None + if isinstance(symbols, list): + symbolsLength = len(symbols) + if symbolsLength > 1: + raise BadRequest(self.id + ' fetchPositions() symbols argument cannot contain more than 1 symbol') + symbol = symbols[0] + else: + symbol = symbols + market = self.market(symbol) + request['instrument_name'] = market['id'] + response = self.v1PrivatePostPrivateGetPositions(self.extend(request, params)) + # + # { + # "id": 1688015952050, + # "method": "private/get-positions", + # "code": 0, + # "result": { + # "data": [ + # { + # "account_id": "ce075bef-b600-4277-bd6e-ff9007251e63", + # "quantity": "0.0001", + # "cost": "3.02392", + # "open_pos_cost": "3.02392", + # "open_position_pnl": "-0.0010281328", + # "session_pnl": "-0.0010281328", + # "update_timestamp_ms": 1688015919091, + # "instrument_name": "BTCUSD-PERP", + # "type": "PERPETUAL_SWAP" + # } + # ] + # } + # } + # + responseResult = self.safe_dict(response, 'result', {}) + positions = self.safe_list(responseResult, 'data', []) + result = [] + for i in range(0, len(positions)): + entry = positions[i] + marketId = self.safe_string(entry, 'instrument_name') + marketInner = self.safe_market(marketId, None, None, 'contract') + result.append(self.parse_position(entry, marketInner)) + return self.filter_by_array_positions(result, 'symbol', None, False) + + def parse_position(self, position: dict, market: Market = None): + # + # { + # "account_id": "ce075bef-b600-4277-bd6e-ff9007251e63", + # "quantity": "0.0001", + # "cost": "3.02392", + # "open_pos_cost": "3.02392", + # "open_position_pnl": "-0.0010281328", + # "session_pnl": "-0.0010281328", + # "update_timestamp_ms": 1688015919091, + # "instrument_name": "BTCUSD-PERP", + # "type": "PERPETUAL_SWAP" + # } + # + marketId = self.safe_string(position, 'instrument_name') + market = self.safe_market(marketId, market, None, 'contract') + symbol = self.safe_symbol(marketId, market, None, 'contract') + timestamp = self.safe_integer(position, 'update_timestamp_ms') + amount = self.safe_string(position, 'quantity') + return self.safe_position({ + 'info': position, + 'id': None, + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'hedged': None, + 'side': 'buy' if Precise.string_gt(amount, '0') else 'sell', + 'contracts': Precise.string_abs(amount), + 'contractSize': market['contractSize'], + 'entryPrice': None, + 'markPrice': None, + 'notional': None, + 'leverage': None, + 'collateral': self.safe_number(position, 'open_pos_cost'), + 'initialMargin': self.safe_number(position, 'cost'), + 'maintenanceMargin': None, + 'initialMarginPercentage': None, + 'maintenanceMarginPercentage': None, + 'unrealizedPnl': self.safe_number(position, 'open_position_pnl'), + 'liquidationPrice': None, + 'marginMode': None, + 'percentage': None, + 'marginRatio': None, + 'stopLossPrice': None, + 'takeProfitPrice': None, + }) + + def nonce(self): + return self.milliseconds() + + def params_to_string(self, object, level): + maxLevel = 3 + if level >= maxLevel: + return str(object) + if isinstance(object, str): + return object + returnString = '' + paramsKeys = None + if isinstance(object, list): + paramsKeys = object + else: + sorted = self.keysort(object) + paramsKeys = list(sorted.keys()) + for i in range(0, len(paramsKeys)): + key = paramsKeys[i] + returnString += key + value = object[key] + if value == 'None': + returnString += 'None' + elif isinstance(value, list): + for j in range(0, len(value)): + returnString += self.params_to_string(value[j], level + 1) + else: + returnString += str(value) + return returnString + + def close_position(self, symbol: str, side: OrderSide = None, params={}) -> Order: + """ + closes open positions for a market + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#private-close-position + + :param str symbol: Unified CCXT market symbol + :param str [side]: not used by cryptocom.closePositions + :param dict [params]: extra parameters specific to the okx api endpoint + + EXCHANGE SPECIFIC PARAMETERS + :param str [params.type]: LIMIT or MARKET + :param number [params.price]: for limit orders only + :returns dict[]: `A list of position structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'instrument_name': market['id'], + 'type': 'MARKET', + } + type = self.safe_string_upper(params, 'type') + price = self.safe_string(params, 'price') + if type is not None: + request['type'] = type + if price is not None: + request['price'] = self.price_to_precision(market['symbol'], price) + response = self.v1PrivatePostPrivateClosePosition(self.extend(request, params)) + # + # { + # "id" : 1700830813298, + # "method" : "private/close-position", + # "code" : 0, + # "result" : { + # "client_oid" : "179a909d-5614-655b-0d0e-9e85c9a25c85", + # "order_id" : "6142909897021751347" + # } + # } + # + result = self.safe_dict(response, 'result') + return self.parse_order(result, market) + + def fetch_trading_fee(self, symbol: str, params={}) -> TradingFeeInterface: + """ + fetch the trading fees for a market + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#private-get-instrument-fee-rate + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `fee structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'instrument_name': market['id'], + } + response = self.v1PrivatePostPrivateGetInstrumentFeeRate(self.extend(request, params)) + # + # { + # "id": 1, + # "code": 0, + # "method": "private/staking/unstake", + # "result": { + # "staking_id": "1", + # "instrument_name": "SOL.staked", + # "status": "NEW", + # "quantity": "1", + # "underlying_inst_name": "SOL", + # "reason": "NO_ERROR" + # } + # } + # + data = self.safe_dict(response, 'result', {}) + return self.parse_trading_fee(data, market) + + def fetch_trading_fees(self, params={}) -> TradingFees: + """ + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#private-get-fee-rate + + fetch the trading fees for multiple markets + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + self.load_markets() + response = self.v1PrivatePostPrivateGetFeeRate(params) + # + # { + # "id": 1, + # "method": "/private/get-fee-rate", + # "code": 0, + # "result": { + # "spot_tier": "3", + # "deriv_tier": "3", + # "effective_spot_maker_rate_bps": "6.5", + # "effective_spot_taker_rate_bps": "6.9", + # "effective_deriv_maker_rate_bps": "1.1", + # "effective_deriv_taker_rate_bps": "3" + # } + # } + # + result = self.safe_dict(response, 'result', {}) + return self.parse_trading_fees(result) + + def parse_trading_fees(self, response): + # + # { + # "spot_tier": "3", + # "deriv_tier": "3", + # "effective_spot_maker_rate_bps": "6.5", + # "effective_spot_taker_rate_bps": "6.9", + # "effective_deriv_maker_rate_bps": "1.1", + # "effective_deriv_taker_rate_bps": "3" + # } + # + result: dict = {} + result['info'] = response + for i in range(0, len(self.symbols)): + symbol = self.symbols[i] + market = self.market(symbol) + isSwap = market['swap'] + takerFeeKey = 'effective_deriv_taker_rate_bps' if isSwap else 'effective_spot_taker_rate_bps' + makerFeeKey = 'effective_deriv_maker_rate_bps' if isSwap else 'effective_spot_maker_rate_bps' + tradingFee = { + 'info': response, + 'symbol': symbol, + 'maker': self.parse_number(Precise.string_div(self.safe_string(response, makerFeeKey), '10000')), + 'taker': self.parse_number(Precise.string_div(self.safe_string(response, takerFeeKey), '10000')), + 'percentage': None, + 'tierBased': None, + } + result[symbol] = tradingFee + return result + + def parse_trading_fee(self, fee: dict, market: Market = None) -> TradingFeeInterface: + # + # { + # "instrument_name": "BTC_USD", + # "effective_maker_rate_bps": "6.5", + # "effective_taker_rate_bps": "6.9" + # } + # + marketId = self.safe_string(fee, 'instrument_name') + symbol = self.safe_symbol(marketId, market) + return { + 'info': fee, + 'symbol': symbol, + 'maker': self.parse_number(Precise.string_div(self.safe_string(fee, 'effective_maker_rate_bps'), '10000')), + 'taker': self.parse_number(Precise.string_div(self.safe_string(fee, 'effective_taker_rate_bps'), '10000')), + 'percentage': None, + 'tierBased': None, + } + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + type = self.safe_string(api, 0) + access = self.safe_string(api, 1) + url = self.urls['api'][type] + '/' + path + query = self.omit(params, self.extract_params(path)) + if access == 'public': + if query: + url += '?' + self.urlencode(query) + else: + self.check_required_credentials() + nonce = str(self.nonce()) + requestParams = self.extend({}, params) + paramsKeys = list(requestParams.keys()) + strSortKey = self.params_to_string(requestParams, 0) + payload = path + nonce + self.apiKey + strSortKey + nonce + signature = self.hmac(self.encode(payload), self.encode(self.secret), hashlib.sha256) + paramsKeysLength = len(paramsKeys) + body = self.json({ + 'id': nonce, + 'method': path, + 'params': params, + 'api_key': self.apiKey, + 'sig': signature, + 'nonce': nonce, + }) + # fix issue https://github.com/ccxt/ccxt/issues/11179 + # php always encodes dictionaries + # if an array is empty, php will put it in square brackets + # python and js will put it in curly brackets + # the code below checks and replaces those brackets in empty requests + if paramsKeysLength == 0: + paramsString = '{}' + arrayString = '[]' + body = body.replace(arrayString, paramsString) + headers = { + 'Content-Type': 'application/json', + } + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + errorCode = self.safe_string(response, 'code') + if errorCode != '0': + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + raise ExchangeError(self.id + ' ' + body) + return None diff --git a/ccxt/cryptomus.py b/ccxt/cryptomus.py new file mode 100644 index 0000000..c152107 --- /dev/null +++ b/ccxt/cryptomus.py @@ -0,0 +1,1137 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.cryptomus import ImplicitAPI +from ccxt.base.types import Any, Balances, Currencies, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, TradingFees +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class cryptomus(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(cryptomus, self).describe(), { + 'id': 'cryptomus', + 'name': 'Cryptomus', + 'countries': ['CA'], + 'rateLimit': 100, # todo check + 'version': 'v2', + 'certified': False, + 'pro': False, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelAllOrders': False, + 'cancelAllOrdersAfter': False, + 'cancelOrder': True, + 'cancelOrders': False, + 'cancelWithdraw': False, + 'closeAllPositions': False, + 'closePosition': False, + 'createConvertTrade': False, + 'createDepositAddress': False, + 'createMarketBuyOrderWithCost': False, + 'createMarketOrder': False, + 'createMarketOrderWithCost': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createOrderWithTakeProfitAndStopLoss': False, + 'createOrderWithTakeProfitAndStopLossWs': False, + 'createPostOnlyOrder': False, + 'createReduceOnlyOrder': False, + 'createStopLimitOrder': False, + 'createStopLossOrder': False, + 'createStopMarketOrder': False, + 'createStopOrder': False, + 'createTakeProfitOrder': False, + 'createTrailingAmountOrder': False, + 'createTrailingPercentOrder': False, + 'createTriggerOrder': False, + 'fetchAccounts': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchCanceledAndClosedOrders': True, + 'fetchCanceledOrders': False, + 'fetchClosedOrder': False, + 'fetchClosedOrders': False, + 'fetchConvertCurrencies': False, + 'fetchConvertQuote': False, + 'fetchConvertTrade': False, + 'fetchConvertTradeHistory': False, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': False, + 'fetchDeposits': False, + 'fetchDepositsWithdrawals': False, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLedger': False, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': False, + 'fetchOHLCV': False, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrder': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': False, + 'fetchOrderTrades': False, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchStatus': False, + 'fetchTicker': False, + 'fetchTickers': True, + 'fetchTime': False, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': True, + 'fetchTransactions': False, + 'fetchTransfers': False, + 'fetchVolatilityHistory': False, + 'fetchWithdrawals': False, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'repayMargin': False, + 'sandbox': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'transfer': False, + 'withdraw': False, + }, + 'timeframes': {}, + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/8e0b1c48-7c01-4177-9224-f1b01d89d7e7', + 'api': { + 'public': 'https://api.cryptomus.com', + 'private': 'https://api.cryptomus.com', + }, + 'www': 'https://cryptomus.com', + 'doc': 'https://doc.cryptomus.com/personal', + 'fees': 'https://cryptomus.com/tariffs', # todo check + 'referral': 'https://app.cryptomus.com/signup/?ref=JRP4yj', # todo + }, + 'api': { + 'public': { + 'get': { + 'v2/user-api/exchange/markets': 1, # done + 'v2/user-api/exchange/market/price': 1, # not used + 'v1/exchange/market/assets': 1, # done + 'v1/exchange/market/order-book/{currencyPair}': 1, # done + 'v1/exchange/market/tickers': 1, # done + 'v1/exchange/market/trades/{currencyPair}': 1, # done + }, + }, + 'private': { + 'get': { + 'v2/user-api/exchange/orders': 1, # done + 'v2/user-api/exchange/orders/history': 1, # done + 'v2/user-api/exchange/account/balance': 1, # done + 'v2/user-api/exchange/account/tariffs': 1, # done + 'v2/user-api/payment/services': 1, + 'v2/user-api/payout/services': 1, + 'v2/user-api/transaction/list': 1, + }, + 'post': { + 'v2/user-api/exchange/orders': 1, # done + 'v2/user-api/exchange/orders/market': 1, # done + }, + 'delete': { + 'v2/user-api/exchange/orders/{orderId}': 1, # done + }, + }, + }, + 'fees': { + 'trading': { + 'percentage': True, + 'feeSide': 'get', + 'maker': self.parse_number('0.02'), + 'taker': self.parse_number('0.02'), + }, + }, + 'options': { + 'createMarketBuyOrderRequiresPrice': True, + 'networks': { + 'BEP20': 'bsc', + 'DASH': 'dash', + 'POLYGON': 'polygon', + 'ARB': 'arbitrum', + 'SOL': 'sol', + 'TON': 'ton', + 'ERC20': 'eth', + 'TRC20': 'tron', + 'LTC': 'ltc', + 'XMR': 'xmr', + 'BCH': 'bch', + 'DOGE': 'doge', + 'AVAX': 'avalanche', + 'BTC': 'btc', + 'RUB': 'rub', + }, + 'networksById': { + 'bsc': 'BEP20', + 'dash': 'DASH', + 'polygon': 'POLYGON', + 'arbitrum': 'ARB', + 'sol': 'SOL', + 'ton': 'TON', + 'eth': 'ERC20', + 'tron': 'TRC20', + 'ltc': 'LTC', + 'xmr': 'XMR', + 'bch': 'BCH', + 'doge': 'DOGE', + 'avalanche': 'AVAX', + 'btc': 'BTC', + 'rub': 'RUB', + }, + 'fetchOrderBook': { + 'level': 0, # 0, 1, 2, 4 or 5 + }, + }, + 'commonCurrencies': {}, + 'exceptions': { + 'exact': { + '500': ExchangeError, + '6': InsufficientFunds, # {"code":6,"message":"Insufficient funds."} + 'Insufficient funds.': InsufficientFunds, + 'Minimum amount 15 USDT': InvalidOrder, + # {"code":500,"message":"Server error."} + # {"message":"Minimum amount 15 USDT","state":1} + # {"message":"Insufficient funds. USDT wallet balance is 35.21617400.","state":1} + }, + 'broad': {}, + }, + 'precisionMode': TICK_SIZE, + 'requiredCredentials': { + 'apiKey': False, + 'uid': True, + }, + 'features': {}, + }) + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for the exchange + + https://doc.cryptomus.com/personal/market-cap/tickers + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = self.publicGetV2UserApiExchangeMarkets(params) + # + # { + # "result": [ + # { + # "id": "01JHN5EFT64YC4HR9KCGM5M65D", + # "symbol": "POL_USDT", + # "baseCurrency": "POL", + # "quoteCurrency": "USDT", + # "baseMinSize": "1.00000000", + # "quoteMinSize": "5.00000000", + # "baseMaxSize": "50000.00000000", + # "quoteMaxSize": "10000000000.00000000", + # "basePrec": "1", + # "quotePrec": "4" + # }, + # ... + # ] + # } + # + result = self.safe_list(response, 'result', []) + return self.parse_markets(result) + + def parse_market(self, market: dict) -> Market: + # + # { + # "id": "01JHN5EFT64YC4HR9KCGM5M65D", + # "symbol": "POL_USDT", + # "baseCurrency": "POL", + # "quoteCurrency": "USDT", + # "baseMinSize": "1.00000000", + # "quoteMinSize": "5.00000000", + # "baseMaxSize": "50000.00000000", + # "quoteMaxSize": "10000000000.00000000", + # "basePrec": "1", + # "quotePrec": "4" + # } + # + marketId = self.safe_string(market, 'symbol') + parts = marketId.split('_') + baseId = parts[0] + quoteId = parts[1] + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + fees = self.safe_dict(self.fees, 'trading') + return self.safe_market_structure({ + 'id': marketId, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'baseId': baseId, + 'quoteId': quoteId, + 'active': True, + 'type': 'spot', + 'subType': None, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'contract': False, + 'settle': None, + 'settleId': None, + 'contractSize': None, + 'linear': None, + 'inverse': None, + 'taker': self.safe_number(fees, 'taker'), + 'maker': self.safe_number(fees, 'maker'), + 'percentage': self.safe_bool(fees, 'percentage'), + 'tierBased': None, + 'feeSide': self.safe_string(fees, 'feeSide'), + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(self.parse_precision(self.safe_string(market, 'quotePrec'))), + 'price': self.parse_number(self.parse_precision(self.safe_string(market, 'basePrec'))), + }, + 'limits': { + 'amount': { + 'min': self.safe_number(market, 'quoteMinSize'), + 'max': self.safe_number(market, 'quoteMaxSize'), + }, + 'price': { + 'min': self.safe_number(market, 'baseMinSize'), + 'max': self.safe_number(market, 'baseMaxSize'), + }, + 'leverage': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + 'info': market, + }) + + def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://doc.cryptomus.com/personal/market-cap/assets + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = self.publicGetV1ExchangeMarketAssets(params) + # + # { + # 'state': '0', + # 'result': [ + # { + # 'currency_code': 'USDC', + # 'network_code': 'bsc', + # 'can_withdraw': True, + # 'can_deposit': True, + # 'min_withdraw': '1.00000000', + # 'max_withdraw': '10000000.00000000', + # 'max_deposit': '10000000.00000000', + # 'min_deposit': '1.00000000' + # }, + # ... + # ] + # } + # + coins = self.safe_list(response, 'result') + groupedById = self.group_by(coins, 'currency_code') + keys = list(groupedById.keys()) + result: dict = {} + for i in range(0, len(keys)): + id = keys[i] + code = self.safe_currency_code(id) + networks = {} + networkEntries = groupedById[id] + for j in range(0, len(networkEntries)): + networkEntry = networkEntries[j] + networkId = self.safe_string(networkEntry, 'network_code') + networkCode = self.network_id_to_code(networkId) + networks[networkCode] = { + 'id': networkId, + 'network': networkCode, + 'limits': { + 'withdraw': { + 'min': self.safe_number(networkEntry, 'min_withdraw'), + 'max': self.safe_number(networkEntry, 'max_withdraw'), + }, + 'deposit': { + 'min': self.safe_number(networkEntry, 'min_deposit'), + 'max': self.safe_number(networkEntry, 'max_deposit'), + }, + }, + 'active': None, + 'deposit': self.safe_bool(networkEntry, 'can_withdraw'), + 'withdraw': self.safe_bool(networkEntry, 'can_deposit'), + 'fee': None, + 'precision': None, + 'info': networkEntry, + } + result[code] = self.safe_currency_structure({ + 'id': id, + 'code': code, + 'networks': networks, + 'info': networkEntries, + }) + return result + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://doc.cryptomus.com/personal/market-cap/tickers + + :param str[] [symbols]: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + symbols = self.market_symbols(symbols) + response = self.publicGetV1ExchangeMarketTickers(params) + # + # { + # "data": [ + # { + # "currency_pair": "MATIC_USDT", + # "last_price": "0.342", + # "base_volume": "1676.84092771", + # "quote_volume": "573.48033609043" + # }, + # ... + # } + # + data = self.safe_list(response, 'data') + return self.parse_tickers(data, symbols) + + def parse_ticker(self, ticker, market: Market = None) -> Ticker: + # + # { + # "currency_pair": "XMR_USDT", + # "last_price": "158.04829772", + # "base_volume": "0.35185785", + # "quote_volume": "55.523761128544" + # } + # + marketId = self.safe_string(ticker, 'currency_pair') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + last = self.safe_string(ticker, 'last_price') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': None, + 'datetime': None, + 'high': None, + 'low': None, + 'bid': None, + 'bidVolume': None, + 'ask': None, + 'askVolume': None, + 'vwap': None, + 'open': None, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': self.safe_string(ticker, 'base_volume'), + 'quoteVolume': self.safe_string(ticker, 'quote_volume'), + 'info': ticker, + }, market) + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://doc.cryptomus.com/personal/market-cap/orderbook + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.level]: 0 or 1 or 2 or 3 or 4 or 5 - the level of volume + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'currencyPair': market['id'], + } + level = 0 + level, params = self.handle_option_and_params(params, 'fetchOrderBook', 'level', level) + request['level'] = level + response = self.publicGetV1ExchangeMarketOrderBookCurrencyPair(self.extend(request, params)) + # + # { + # "data": { + # "timestamp": "1730138702", + # "bids": [ + # { + # "price": "2250.00", + # "quantity": "1.00000" + # } + # ], + # "asks": [ + # { + # "price": "2428.69", + # "quantity": "0.16470" + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + timestamp = self.safe_timestamp(data, 'timestamp') + return self.parse_order_book(data, symbol, timestamp, 'bids', 'asks', 'price', 'quantity') + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://doc.cryptomus.com/personal/market-cap/trades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch(maximum value is 100) + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'currencyPair': market['id'], + } + response = self.publicGetV1ExchangeMarketTradesCurrencyPair(self.extend(request, params)) + # + # { + # "data": [ + # { + # "trade_id": "01J829C3RAXHXHR09HABGQ1YAT", + # "price": "2315.6320500000000000", + # "base_volume": "21.9839623057260000", + # "quote_volume": "0.0094937200000000", + # "timestamp": 1726653796, + # "type": "sell" + # } + # ] + # } + # + data = self.safe_list(response, 'data') + return self.parse_trades(data, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # { + # "trade_id": "01J017Q6B3JGHZRP9D2NZHVKFX", + # "price": "59498.63487492", + # "base_volume": "94.00784310", + # "quote_volume": "0.00158000", + # "timestamp": 1718028573, + # "type": "sell" + # } + # + timestamp = self.safe_timestamp(trade, 'timestamp') + return self.safe_trade({ + 'id': self.safe_string(trade, 'trade_id'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'side': self.safe_string(trade, 'type'), + 'price': self.safe_string(trade, 'price'), + 'amount': self.safe_string(trade, 'quote_volume'), # quote_volume is amount + 'cost': self.safe_string(trade, 'base_volume'), # base_volume is cost + 'takerOrMaker': None, + 'type': None, + 'order': None, + 'fee': { + 'currency': None, + 'cost': None, + }, + 'info': trade, + }, market) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://doc.cryptomus.com/personal/converts/balance + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + request: dict = {} + response = self.privateGetV2UserApiExchangeAccountBalance(self.extend(request, params)) + # + # { + # "result": [ + # { + # "ticker": "AVAX", + # "available": "0.00000000", + # "held": "0.00000000" + # } + # ] + # } + # + result = self.safe_list(response, 'result', []) + return self.parse_balance(result) + + def parse_balance(self, balance) -> Balances: + # + # { + # "ticker": "AVAX", + # "available": "0.00000000", + # "held": "0.00000000" + # } + # + result: dict = { + 'info': balance, + } + for i in range(0, len(balance)): + balanceEntry = balance[i] + currencyId = self.safe_string(balanceEntry, 'ticker') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(balanceEntry, 'available') + account['used'] = self.safe_string(balanceEntry, 'held') + result[code] = account + return self.safe_balance(result) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}) -> Order: + """ + create a trade order + + https://doc.cryptomus.com/personal/exchange/market-order-creation + https://doc.cryptomus.com/personal/exchange/limit-order-creation + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' or for spot + :param str side: 'buy' or 'sell' + :param float amount: how much of you want to trade in units of the base currency + :param float [price]: the price that the order is to be fulfilled, in units of the quote currency, ignored in market orders(only for limit orders) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.cost]: *market buy only* the quote quantity that can be used alternative for the amount + :param str [params.clientOrderId]: a unique identifier for the order(optional) + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + 'direction': side, + 'tag': 'ccxt', + } + clientOrderId = self.safe_string(params, 'clientOrderId') + if clientOrderId is not None: + params = self.omit(params, 'clientOrderId') + request['client_order_id'] = clientOrderId + sideBuy = side == 'buy' + amountToString = self.number_to_string(amount) + priceToString = self.number_to_string(price) + cost = None + cost, params = self.handle_param_string(params, 'cost') + response = None + if type == 'market': + if sideBuy: + createMarketBuyOrderRequiresPrice = True + createMarketBuyOrderRequiresPrice, params = self.handle_option_and_params(params, 'createOrder', 'createMarketBuyOrderRequiresPrice', True) + if createMarketBuyOrderRequiresPrice: + if (price is None) and (cost is None): + raise InvalidOrder(self.id + ' createOrder() requires the price argument for market buy orders to calculate the total cost to spend(amount * price), alternatively set the createMarketBuyOrderRequiresPrice option of param to False and pass the cost to spend in the amount argument') + elif cost is None: + cost = Precise.string_mul(amountToString, priceToString) + else: + cost = cost if cost else amountToString + request['value'] = cost + else: + request['quantity'] = amountToString + response = self.privatePostV2UserApiExchangeOrdersMarket(self.extend(request, params)) + elif type == 'limit': + if price is None: + raise ArgumentsRequired(self.id + ' createOrder() requires a price parameter for a ' + type + ' order') + request['quantity'] = amountToString + request['price'] = price + response = self.privatePostV2UserApiExchangeOrders(self.extend(request, params)) + else: + raise ArgumentsRequired(self.id + ' createOrder() requires a type parameter(limit or market)') + # + # { + # "order_id": "01JEXAFCCC5ZVJPZAAHHDKQBNG" + # } + # + return self.parse_order(response, market) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open limit order + + https://doc.cryptomus.com/personal/exchange/limit-order-cancellation + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in(not used in cryptomus) + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + request: dict = {} + request['orderId'] = id + response = self.privateDeleteV2UserApiExchangeOrdersOrderId(self.extend(request, params)) + # + # { + # "success": True + # } + # + return self.safe_order({'info': response}) + + def fetch_canceled_and_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://doc.cryptomus.com/personal/exchange/history-of-completed-orders + + :param str symbol: unified market symbol of the market orders were made in(not used in cryptomus) + :param int [since]: the earliest time in ms to fetch orders for(not used in cryptomus) + :param int [limit]: the maximum number of order structures to retrieve(not used in cryptomus) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.direction]: order direction 'buy' or 'sell' + :param str [params.order_id]: order id + :param str [params.client_order_id]: client order id + :param str [params.limit]: A special parameter that sets the maximum number of records the request will return + :param str [params.offset]: A special parameter that sets the number of records from the beginning of the list + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['market'] = market['id'] + if limit is not None: + request['limit'] = limit + response = self.privateGetV2UserApiExchangeOrdersHistory(self.extend(request, params)) + # + # { + # "result": [ + # { + # "id": "01JEXAPY04JDFBVFC2D23BCKMK", + # "type": "market", + # "direction": "sell", + # "symbol": "TRX_USDT", + # "quantity": "67.5400000000000000", + # "filledQuantity": "67.5400000000000000", + # "filledValue": "20.0053480000000000", + # "state": "completed", + # "internalState": "filled", + # "createdAt": "2024-12-12 11:40:19", + # "finishedAt": "2024-12-12 11:40:21", + # "deal": { + # "id": "01JEXAPZ9C9TWENPFZJASZ1YD2", + # "state": "completed", + # "createdAt": "2024-12-12 11:40:21", + # "completedAt": "2024-12-12 11:40:21", + # "averageFilledPrice": "0.2962000000000000", + # "transactions": [ + # { + # "id": "01JEXAPZ9C9TWENPFZJASZ1YD3", + # "tradeRole": "taker", + # "filledPrice": "0.2962000000000000", + # "filledQuantity": "67.5400000000000000", + # "filledValue": "20.0053480000000000", + # "fee": "0.0000000000000000", + # "feeCurrency": "USDT", + # "committedAt": "2024-12-12 11:40:21" + # } + # ] + # } + # }, + # ... + # ] + # } + # + result = self.safe_list(response, 'result', []) + orders = [] + for i in range(0, len(result)): + order = result[i] + orders.append(self.parse_order(order, market)) + return orders + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://doc.cryptomus.com/personal/exchange/list-of-active-orders + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for(not used in cryptomus) + :param int [limit]: the maximum number of open orders structures to retrieve(not used in cryptomus) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.direction]: order direction 'buy' or 'sell' + :param str [params.order_id]: order id + :param str [params.client_order_id]: client order id + :param str [params.limit]: A special parameter that sets the maximum number of records the request will return + :param str [params.offset]: A special parameter that sets the number of records from the beginning of the list + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + request: dict = { + } + if market is not None: + request['market'] = market['id'] + response = self.privateGetV2UserApiExchangeOrders(self.extend(request, params)) + # + # { + # "result": [ + # { + # "id": "01JFFG72CBRDP68K179KC9DSTG", + # "direction": "sell", + # "symbol": "BTC_USDT", + # "price": "102.0130000000000000", + # "quantity": "0.0005000000000000", + # "value": "0.0510065000000000", + # "filledQuantity": "0.0000000000000000", + # "filledValue": "0.0000000000000000", + # "createdAt": "2024-12-19 09:02:51", + # "clientOrderId": "987654321", + # "stopLossPrice": "101.12" + # }, + # ... + # ] + # } + result = self.safe_list(response, 'result', []) + return self.parse_orders(result, market, None, None) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder + # { + # "order_id": "01JEXAFCCC5ZVJPZAAHHDKQBNG" + # } + # + # fetchOrders + # { + # "id": "01JEXAPY04JDFBVFC2D23BCKMK", + # "type": "market", + # "direction": "sell", + # "symbol": "TRX_USDT", + # "quantity": "67.5400000000000000", + # "filledQuantity": "67.5400000000000000", + # "filledValue": "20.0053480000000000", + # "state": "completed", + # "internalState": "filled", + # "createdAt": "2024-12-12 11:40:19", + # "finishedAt": "2024-12-12 11:40:21", + # "deal": { + # "id": "01JEXAPZ9C9TWENPFZJASZ1YD2", + # "state": "completed", + # "createdAt": "2024-12-12 11:40:21", + # "completedAt": "2024-12-12 11:40:21", + # "averageFilledPrice": "0.2962000000000000", + # "transactions": [ + # { + # "id": "01JEXAPZ9C9TWENPFZJASZ1YD3", + # "tradeRole": "taker", + # "filledPrice": "0.2962000000000000", + # "filledQuantity": "67.5400000000000000", + # "filledValue": "20.0053480000000000", + # "fee": "0.0000000000000000", + # "feeCurrency": "USDT", + # "committedAt": "2024-12-12 11:40:21" + # } + # ] + # } + # }, + # ... + # + # fetchOpenOrders + # { + # "id": "01JFFG72CBRDP68K179KC9DSTG", + # "direction": "sell", + # "symbol": "BTC_USDT", + # "price": "102.0130000000000000", + # "quantity": "0.0005000000000000", + # "value": "0.0510065000000000", + # "filledQuantity": "0.0000000000000000", + # "filledValue": "0.0000000000000000", + # "createdAt": "2024-12-19 09:02:51", + # "clientOrderId": "987654321", + # "stopLossPrice": "101.12" + # } + # + id = self.safe_string_2(order, 'order_id', 'id') + marketId = self.safe_string(order, 'symbol') + market = self.safe_market(marketId, market) + dateTime = self.safe_string(order, 'createdAt') + timestamp = self.parse8601(dateTime) + deal = self.safe_dict(order, 'deal', {}) + averageFilledPrice = self.safe_number(deal, 'averageFilledPrice') + type = self.safe_string(order, 'type') + side = self.safe_string(order, 'direction') + price = self.safe_number(order, 'price') + transaction = self.safe_list(deal, 'transactions', []) + fee = None + firstTx = self.safe_dict(transaction, 0) + feeCurrency = self.safe_string(firstTx, 'feeCurrency') + if feeCurrency is not None: + fee = { + 'currency': self.safe_currency_code(feeCurrency), + 'cost': self.safe_number(firstTx, 'fee'), + } + if price is None: + price = self.safe_number(firstTx, 'filledPrice') + amount = self.safe_number(order, 'quantity') + cost = self.safe_number(order, 'value') + status = self.parse_order_status(self.safe_string(order, 'state')) + clientOrderId = self.safe_string(order, 'clientOrderId') + return self.safe_order({ + 'id': id, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'symbol': market['symbol'], + 'type': type, + 'timeInForce': None, + 'postOnly': None, + 'side': side, + 'price': price, + 'stopPrice': self.safe_string(order, 'stopLossPrice'), + 'triggerPrice': self.safe_string(order, 'stopLossPrice'), + 'amount': amount, + 'cost': cost, + 'average': averageFilledPrice, + 'filled': self.safe_string(order, 'filledQuantity'), + 'remaining': None, + 'status': status, + 'fee': fee, + 'trades': None, + 'info': order, + }, market) + + def parse_order_status(self, status: Str = None) -> Str: + statuses = { + 'active': 'open', + 'completed': 'closed', + 'partially_completed': 'open', + 'cancelled': 'canceled', + 'expired': 'expired', + 'failed': 'failed', + } + return self.safe_string(statuses, status, status) + + def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://trade-docs.coinlist.co/?javascript--nodejs#list-fees + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + response = self.privateGetV2UserApiExchangeAccountTariffs(params) + # + # { + # result: { + # equivalent_currency_code: 'USD', + # current_tariff_step: { + # step: '0', + # from_turnover: '0.00000000', + # maker_percent: '0.08', + # taker_percent: '0.1' + # }, + # tariff_steps: [ + # { + # step: '0', + # from_turnover: '0.00000000', + # maker_percent: '0.08', + # taker_percent: '0.1' + # }, + # { + # step: '1', + # from_turnover: '100001.00000000', + # maker_percent: '0.06', + # taker_percent: '0.095' + # }, + # { + # step: '2', + # from_turnover: '250001.00000000', + # maker_percent: '0.055', + # taker_percent: '0.085' + # }, + # { + # step: '3', + # from_turnover: '500001.00000000', + # maker_percent: '0.05', + # taker_percent: '0.075' + # }, + # { + # step: '4', + # from_turnover: '2500001.00000000', + # maker_percent: '0.04', + # taker_percent: '0.07' + # } + # ], + # daily_turnover: '0.00000000', + # monthly_turnover: '77.52062617', + # circulation_funds: '25.48900443' + # } + # } + # + data = self.safe_dict(response, 'result', {}) + currentFeeTier = self.safe_dict(data, 'current_tariff_step', {}) + makerFee = self.safe_string(currentFeeTier, 'maker_percent') + takerFee = self.safe_string(currentFeeTier, 'taker_percent') + makerFee = Precise.string_div(makerFee, '100') + takerFee = Precise.string_div(takerFee, '100') + feeTiers = self.safe_list(data, 'tariff_steps', []) + result: dict = {} + tiers = self.parse_fee_tiers(feeTiers) + for i in range(0, len(self.symbols)): + symbol = self.symbols[i] + result[symbol] = { + 'info': response, + 'symbol': symbol, + 'maker': self.parse_number(makerFee), + 'taker': self.parse_number(takerFee), + 'percentage': True, + 'tierBased': True, + 'tiers': tiers, + } + return result + + def parse_fee_tiers(self, feeTiers, market: Market = None): + takerFees = [] + makerFees = [] + for i in range(0, len(feeTiers)): + tier = feeTiers[i] + turnover = self.safe_number(tier, 'from_turnover') + taker = self.safe_string(tier, 'taker_percent') + maker = self.safe_string(tier, 'maker_percent') + maker = Precise.string_div(maker, '100') + taker = Precise.string_div(taker, '100') + makerFees.append([turnover, self.parse_number(maker)]) + takerFees.append([turnover, self.parse_number(taker)]) + return { + 'maker': makerFees, + 'taker': takerFees, + } + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + endpoint = self.implode_params(path, params) + params = self.omit(params, self.extract_params(path)) + url = self.urls['api'][api] + '/' + endpoint + if api == 'private': + self.check_required_credentials() + jsonParams = '' + headers = { + 'userId': self.uid, + } + if method != 'GET': + body = self.json(params) + jsonParams = body + headers['Content-Type'] = 'application/json' + else: + query = self.urlencode(params) + if len(query) != 0: + url += '?' + query + jsonParamsBase64 = self.string_to_base64(jsonParams) + stringToSign = jsonParamsBase64 + self.secret + signature = self.hash(self.encode(stringToSign), 'md5') + headers['sign'] = signature + else: + query = self.urlencode(params) + if len(query) != 0: + url += '?' + query + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None + if 'code' in response: + code = self.safe_string(response, 'code') + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], code, feedback) + raise ExchangeError(feedback) + elif 'message' in response: + # + # {"message":"Minimum amount 15 USDT","state":1} + # + message = self.safe_string(response, 'message') + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + raise ExchangeError(feedback) # unknown message + return None diff --git a/ccxt/deepcoin.py b/ccxt/deepcoin.py new file mode 100644 index 0000000..e1cf096 --- /dev/null +++ b/ccxt/deepcoin.py @@ -0,0 +1,2858 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.deepcoin import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currency, DepositAddress, Int, LedgerEntry, Market, Num, Order, OrderBook, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, FundingRates, Trade, Transaction, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import NotSupported +from ccxt.base.errors import NullResponse +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class deepcoin(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(deepcoin, self).describe(), { + 'id': 'deepcoin', + 'name': 'DeepCoin', + 'countries': ['SG'], # Singapore + 'rateLimit': 200, # 5 times per second + 'version': 'v1', + 'certified': False, + 'pro': True, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': True, + 'swap': True, + 'future': False, + 'option': False, + 'addMargin': False, + 'cancelAllOrders': True, + 'cancelAllOrdersAfter': False, + 'cancelOrder': True, + 'cancelOrders': True, + 'cancelWithdraw': False, + 'closePosition': True, + 'createConvertTrade': False, + 'createDepositAddress': False, + 'createLimitBuyOrder': True, + 'createLimitOrder': True, + 'createLimitSellOrder': True, + 'createMarketBuyOrder': True, + 'createMarketBuyOrderWithCost': True, + 'createMarketOrder': True, + 'createMarketOrderWithCost': True, + 'createMarketSellOrder': True, + 'createMarketSellOrderWithCost': True, + 'createOrder': True, + 'createOrders': False, + 'createOrderWithTakeProfitAndStopLoss': True, + 'createPostOnlyOrder': True, + 'createReduceOnlyOrder': True, + 'createStopLossOrder': False, + 'createTakeProfitOrder': False, + 'createTrailingAmountOrder': False, + 'createTrailingPercentOrder': False, + 'createTriggerOrder': True, + 'editOrder': True, + 'fetchAccounts': False, + 'fetchBalance': True, + 'fetchCanceledAndClosedOrders': True, + 'fetchCanceledOrders': True, + 'fetchClosedOrder': True, + 'fetchClosedOrders': True, + 'fetchConvertCurrencies': False, + 'fetchConvertQuote': False, + 'fetchConvertTrade': False, + 'fetchConvertTradeHistory': False, + 'fetchCurrencies': False, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': True, + 'fetchDeposits': True, + 'fetchDepositsWithdrawals': False, + 'fetchDepositWithdrawFees': False, + 'fetchFundingHistory': False, + 'fetchFundingRate': True, + 'fetchFundingRateHistory': True, + 'fetchFundingRates': True, + 'fetchIndexOHLCV': True, + 'fetchLedger': True, + 'fetchLeverage': False, + 'fetchLeverageTiers': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': True, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenOrder': True, + 'fetchOpenOrders': True, + 'fetchOrder': False, + 'fetchOrderBook': True, + 'fetchOrders': False, + 'fetchOrderTrades': True, + 'fetchPosition': True, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': True, + 'fetchPositionsForSymbol': True, + 'fetchPositionsHistory': True, + 'fetchPremiumIndexOHLCV': False, + 'fetchStatus': False, + 'fetchTicker': False, + 'fetchTickers': True, + 'fetchTime': False, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': False, + 'fetchTransactions': False, + 'fetchTransfers': False, + 'fetchWithdrawals': True, + 'reduceMargin': False, + 'sandbox': False, + 'setLeverage': True, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'transfer': True, + 'withdraw': False, + }, + 'timeframes': { + '1m': '1m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1H', + '4h': '4H', + '12h': '12H', + '1d': '1D', + '1w': '1W', + '1M': '1M', + '1y': '1Y', + }, + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/671bd35c-770e-4935-9070-f8fb114f79c4', + 'api': { + 'public': 'https://api.deepcoin.com', + 'private': 'https://api.deepcoin.com', + }, + 'www': 'https://www.deepcoin.com/', + 'doc': 'https://www.deepcoin.com/docs', + 'referral': { + 'url': 'https://s.deepcoin.com/UzkyODgy', + 'discount': 0.1, + }, + }, + 'api': { + 'public': { + 'get': { + 'deepcoin/market/books': 1, + 'deepcoin/market/candles': 1, + 'deepcoin/market/instruments': 1, + 'deepcoin/market/tickers': 1, + 'deepcoin/market/index-candles': 1, + 'deepcoin/market/trades': 1, + 'deepcoin/market/mark-price-candles': 1, + 'deepcoin/market/step-margin': 5, + }, + }, + 'private': { + 'get': { + 'deepcoin/account/balances': 5, + 'deepcoin/account/bills': 5, + 'deepcoin/account/positions': 5, + 'deepcoin/trade/fills': 5, + 'deepcoin/trade/orderByID': 5, + 'deepcoin/trade/finishOrderByID': 5, + 'deepcoin/trade/orders-history': 5, + 'deepcoin/trade/v2/orders-pending': 5, + 'deepcoin/trade/funding-rate': 5, + 'deepcoin/trade/fund-rate/current-funding-rate': 5, + 'deepcoin/trade/fund-rate/history': 5, + 'deepcoin/trade/trigger-orders-pending': 5, + 'deepcoin/trade/trigger-orders-history': 5, + 'deepcoin/copytrading/support-contracts': 5, + 'deepcoin/copytrading/leader-position': 5, + 'deepcoin/copytrading/estimate-profit': 5, + 'deepcoin/copytrading/history-profit': 5, + 'deepcoin/copytrading/follower-rank': 5, + 'deepcoin/internal-transfer/support': 5, + 'deepcoin/internal-transfer/history-order': 5, + 'deepcoin/rebate/config': 5, + 'deepcoin/agents/users': 5, + 'deepcoin/agents/users/rebate-list': 5, + 'deepcoin/agents/users/rebates': 5, + 'deepcoin/asset/deposit-list': 5, + 'deepcoin/asset/withdraw-list': 5, + 'deepcoin/asset/recharge-chain-list': 5, + 'deepcoin/listenkey/acquire': 5, + 'deepcoin/listenkey/extend': 5, + }, + 'post': { + 'deepcoin/account/set-leverage': 5, + 'deepcoin/trade/order': 5, + 'deepcoin/trade/replace-order': 5, + 'deepcoin/trade/cancel-order': 5, + 'deepcoin/trade/batch-cancel-order': 5, + 'deepcoin/trade/cancel-trigger-order': 1 / 6, + 'deepcoin/trade/swap/cancel-all': 5, + 'deepcoin/trade/trigger-order': 5, + 'deepcoin/trade/batch-close-position': 5, + 'deepcoin/trade/replace-order-sltp': 5, + 'deepcoin/trade/close-position-by-ids': 5, + 'deepcoin/copytrading/leader-settings': 5, + 'deepcoin/copytrading/set-contracts': 5, + 'deepcoin/internal-transfer': 5, + 'deepcoin/rebate/config': 5, + 'deepcoin/asset/transfer': 5, + }, + }, + }, + 'fees': { + 'trading': { + 'taker': self.parse_number('0.0015'), + 'maker': self.parse_number('0.0010'), + }, + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': True, + 'triggerPrice': True, + 'triggerPriceType': { + 'last': True, + 'mark': False, + 'index': False, + }, + 'triggerDirection': False, + 'stopLossPrice': True, + 'takeProfitPrice': True, + 'attachedStopLossTakeProfit': { + 'triggerPriceType': { + 'last': False, + 'mark': False, + 'index': False, + }, + 'price': True, + }, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': True, + 'trailing': False, + 'marketBuyRequiresPrice': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 60, + 'untilDays': None, + 'symbolRequired': True, + }, + 'fetchOrder': None, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 100, + 'trigger': True, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOrders': None, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': None, + 'daysBackCanceled': None, + 'untilDays': None, + 'trigger': True, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOHLCV': { + 'limit': 300, + }, + }, + 'swap': { + 'linear': { + 'extends': 'spot', + }, + 'inverse': { + 'extends': 'spot', + }, + }, + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + 'password': True, + }, + 'precisionMode': TICK_SIZE, + 'options': { + 'recvWindow': 5000, + 'defaultNetworks': { + 'ETH': 'ERC20', + 'USDT': 'TRC20', + 'USDC': 'ERC20', + }, + 'networks': { + 'ERC20': 'ERC20', + 'TRC20': 'TRC20', + 'ARB': 'ARBITRUM', + 'BSC': 'BSC(BEP20)', + 'SOL': 'SOL', + 'BTC': 'Bitcoin', + 'ADA': 'Cardano', + }, + 'networksById': { + }, + 'fetchMarkets': { + 'types': ['spot', 'swap'], # spot, swap, + }, + 'timeInForce': { + 'GTC': 'GTC', # Good Till Cancel + 'IOC': 'IOC', # Immediate Or Cancel + 'PO': 'PO', # Post Only + }, + 'exchangeType': { + 'spot': 'SPOT', + 'swap': 'SWAP', + 'SPOT': 'SPOT', + 'SWAP': 'SWAP', + }, + 'accountsByType': { + 'spot': 1, + 'fund': 2, + 'rebate': 3, + 'inverse': 5, + 'linear': 7, + 'demo': 10, + }, + }, + 'commonCurrencies': {}, + 'exceptions': { + 'exact': { + '24': OrderNotFound, # {"code":"0","msg":"","data":{"ordId":"","clOrdId":"","sCode":"24","sMsg":"OrderNotFound:1"}} + '31': InsufficientFunds, # {"code":"0","msg":"","data":{"ordId":"","clOrdId":"","tag":"","sCode":"31","sMsg":"NotEnoughPositionToClose:Position=0"}} + '36': InsufficientFunds, # {"code":"0","msg":"","data":{"ordId":"","clOrdId":"","tag":"","sCode":"36","sMsg":"InsufficientMoney:-0.000004"}} + '44': BadRequest, # {"code":"0","msg":"","data":{"ordId":"","clOrdId":"","tag":"","sCode":"44","sMsg":"VolumeNotOnTick"}} + '49': InvalidOrder, # {"code":"0","msg":"","data":{"ordId":"","clOrdId":"","tag":"","sCode":"49","sMsg":"PriceOutOfUpperLimit:Price\u003eUpperLimitPrice[0.28422]"}} + '194': InvalidOrder, # {"code":"0","msg":"","data":{"ordId":"","clOrdId":"","tag":"","sCode":"194","sMsg":"LessThanMinVolume"}} + '195': InvalidOrder, # {"code":"0","msg":"","data":{"ordId":"","clOrdId":"","tag":"","sCode":"195","sMsg":"PositionLessThanMinVolume"}} + '199': BadRequest, # {"code":"0","msg":"","data":{"instId":"","lever":"","mgnMode":"","mrgPosition":"","sCode":"199","sMsg":"LeverageTooHigh:Amount[10000.0]\u003eLeverage[75.1880]"}} + '100010': InsufficientFunds, # {"code":"0","msg":"","data":{"retCode":100010,"retMsg":"Balance is insufficient, please deposit first.","retData":{}}} + 'unsupportedAction': BadRequest, + 'localIDNotExist': BadRequest, + }, + 'broad': { + 'no available': NotSupported, # orderbook does not exist: ETHUSD_0.1, no available orderbook data + 'field is required': ArgumentsRequired, # {"code":"51","msg":"The productGroup field is required","data":null} + 'not in acceptable range': BadRequest, # {"code":"51","msg":"The instType value `spot` is not in acceptable range: SPOT,SWAP","data":null} + 'subscription cluster does not "exist"': BadRequest, + 'must be equal or lesser than': BadRequest, # {"code":"51","msg":"The Size value `100` must be equal or lesser than 50","data":null} + }, + }, + }) + + def handle_market_type_and_params(self, methodName: str, market: Market = None, params={}, defaultValue=None) -> Any: + instType = self.safe_string(params, 'instType') + params = self.omit(params, 'instType') + type = self.safe_string(params, 'type') + if (type is None) and (instType is not None): + params = self.extend(params, {'type': instType}) + return super(deepcoin, self).handle_market_type_and_params(methodName, market, params, defaultValue) + + def convert_to_instrument_type(self, type): + exchangeTypes = self.safe_dict(self.options, 'exchangeType', {}) + return self.safe_string(exchangeTypes, type, type) + + def fetch_markets(self, params={}) -> List[Market]: + """ + + https://www.deepcoin.com/docs/DeepCoinMarket/getBaseInfo + + retrieves data on all markets for okcoin + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + types = ['spot', 'swap'] + fetchMarketsOption = self.safe_dict(self.options, 'fetchMarkets') + if fetchMarketsOption is not None: + types = self.safe_list(fetchMarketsOption, 'types', types) + else: + types = self.safe_list(self.options, 'fetchMarkets', types) # backward-support + promises = [] + result = [] + for i in range(0, len(types)): + promises.append(self.fetch_markets_by_type(types[i], params)) + promises = promises + for i in range(0, len(promises)): + result = self.array_concat(result, promises[i]) + return result + + def fetch_markets_by_type(self, type, params={}): + request: dict = { + 'instType': self.convert_to_instrument_type(type), + } + response = self.publicGetDeepcoinMarketInstruments(self.extend(request, params)) + # + # spot + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "instType": "SPOT", + # "instId": "A-USDT", + # "uly": "", + # "baseCcy": "A", + # "quoteCcy": "USDT", + # "ctVal": "1", + # "ctValCcy": "", + # "listTime": "0", + # "lever": "1", + # "tickSz": "0.0001", + # "lotSz": "0.001", + # "minSz": "0.5", + # "ctType": "", + # "alias": "", + # "state": "live", + # "maxLmtSz": "7692307", + # "maxMktSz": "7692307" + # } + # ] + # } + # + dataResponse = self.safe_list(response, 'data', []) + return self.parse_markets(dataResponse) + + def parse_market(self, market: dict) -> Market: + # + # spot markets + # + # { + # "instType": "SPOT", + # "instId": "A-USDT", + # "uly": "", + # "baseCcy": "A", + # "quoteCcy": "USDT", + # "ctVal": "1", + # "ctValCcy": "", + # "listTime": "0", + # "lever": "1", + # "tickSz": "0.0001", + # "lotSz": "0.001", + # "minSz": "0.5", + # "ctType": "", + # "alias": "", + # "state": "live", + # "maxLmtSz": "7692307", + # "maxMktSz": "7692307" + # } + # + # swap markets + # + # { + # "instType": "SWAP", + # "instId": "ZORA-USDT-SWAP", + # "uly": "", + # "baseCcy": "ZORA", + # "quoteCcy": "USDT", + # "ctVal": "1", + # "ctValCcy": "", + # "listTime": "0", + # "lever": "20", + # "tickSz": "0.00001", + # "lotSz": "1", + # "minSz": "1685", + # "ctType": "", + # "alias": "", + # "state": "live", + # "maxLmtSz": "10000000", + # "maxMktSz": "10000000" + # } + # + id = self.safe_string(market, 'instId') + type = self.safe_string_lower(market, 'instType') + spot = (type == 'spot') + swap = (type == 'swap') + baseId = self.safe_string(market, 'baseCcy') + quoteId = self.safe_string(market, 'quoteCcy', '') + settleId = None + settle = None + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + symbol = base + '/' + quote + isLinear = None + if swap: + isLinear = (quoteId != 'USD') + settleId = quoteId if isLinear else baseId + settle = self.safe_currency_code(settleId) + symbol = symbol + ':' + settle + fees = self.safe_dict_2(self.fees, type, 'trading', {}) + maxLeverage = self.safe_string(market, 'lever', '1') + maxLeverage = Precise.string_max(maxLeverage, '1') + maxMarketSize = self.safe_string(market, 'maxMktSz') + maxLimitSize = self.safe_string(market, 'maxLmtSz') + maxAmount = self.parse_number(Precise.string_max(maxMarketSize, maxLimitSize)) + state = self.safe_string(market, 'state') + return self.extend(fees, { + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': type, + 'spot': spot, + 'margin': spot and (Precise.string_gt(maxLeverage, '1')), + 'swap': swap, + 'future': False, + 'option': False, + 'active': state == 'live', + 'contract': swap, + 'linear': isLinear, + 'inverse': (not isLinear) if swap else None, + 'contractSize': self.safe_number(market, 'ctVal') if swap else None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'created': None, + 'precision': { + 'amount': self.safe_number(market, 'lotSz'), + 'price': self.safe_number(market, 'tickSz'), + }, + 'limits': { + 'leverage': { + 'min': self.parse_number('1'), + 'max': self.parse_number(maxLeverage), + }, + 'amount': { + 'min': self.safe_number(market, 'minSz'), + 'max': maxAmount, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'info': market, + }) + + def set_markets(self, markets, currencies=None): + markets = super(deepcoin, self).set_markets(markets, currencies) + symbols = list(markets.keys()) + for i in range(0, len(symbols)): + symbol = symbols[i] + market = markets[symbol] + if market['swap']: + additionalId = market['baseId'] + market['quoteId'] + self.markets_by_id[additionalId] = [market] # some endpoints return swap market id+quote + return self.markets + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://www.deepcoin.com/docs/DeepCoinMarket/marketBooks + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + if limit is None: + limit = 400 + request: dict = { + 'instId': market['id'], + 'sz': limit, + } + response = self.publicGetDeepcoinMarketBooks(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "", + # "data": { + # "bids": [ + # ["3732.21", "99.6"], + # ["3732.2", "54.7"] + # ], + # "asks": [ + # ["3732.22", "85.1"], + # ["3732.23", "49.4"] + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_order_book(data, symbol, None, 'bids', 'asks', 0, 1) + + def fetch_ohlcv(self, symbol: str, timeframe='1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://www.deepcoin.com/docs/DeepCoinMarket/getKlineData + https://www.deepcoin.com/docs/DeepCoinMarket/getIndexKlineData + https://www.deepcoin.com/docs/DeepCoinMarket/getMarkKlineData + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest candle to fetch + :param str [params.price]: "mark" or "index" for mark price and index price candles + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + maxLimit = 300 + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate', False) + if paginate: + params = self.extend(params, {'calculateUntil': True}) + return self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params, maxLimit) + market = self.market(symbol) + price = self.safe_string(params, 'price') + params = self.omit(params, 'price') + bar = self.safe_string(self.timeframes, timeframe, timeframe) + request: dict = { + 'instId': market['id'], + 'bar': bar, + } + if limit is not None: + request['limit'] = limit + until = self.safe_integer(params, 'until') + if until is not None: + request['after'] = until + params = self.omit(params, 'until') + calculateUntil = self.safe_bool(params, 'calculateUntil', False) + if calculateUntil: + params = self.omit(params, 'calculateUntil') + if since is not None: + # the exchange do not have a since param for self endpoint + # we canlculate until(after) for correct pagination + duration = self.parse_timeframe(timeframe) + numberOfCandles = maxLimit if (limit is None) else limit + endTime = since + (duration * numberOfCandles) * 1000 + if until is not None: + endTime = min(endTime, until) + now = self.milliseconds() + request['after'] = min(endTime, now) + response = None + if price == 'mark': + response = self.publicGetDeepcoinMarketMarkPriceCandles(self.extend(request, params)) + elif price == 'index': + response = self.publicGetDeepcoinMarketIndexCandles(self.extend(request, params)) + else: + response = self.publicGetDeepcoinMarketCandles(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "", + # "data":[ + # [ + # "1760221800000", + # "3739.08", + # "3741.95", + # "3737.75", + # "3740.1", + # "2849", + # "1065583.744" + # ], + # [ + # "1760221740000", + # "3742.36", + # "3743.01", + # "3736.83", + # "3739.08", + # "2723", + # "1018290.723" + # ] + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_ohlcvs(data, market, timeframe, since, limit) + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://www.deepcoin.com/docs/DeepCoinMarket/getMarketTickers + + :param str[] [symbols]: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + symbols = self.market_symbols(symbols) + market = self.get_market_from_symbols(symbols) + marketType = None + marketType, params = self.handle_market_type_and_params('fetchTickers', market, params) + request: dict = { + 'instType': self.convert_to_instrument_type(marketType), + } + response = self.publicGetDeepcoinMarketTickers(self.extend(request, params)) + tickers = self.safe_list(response, 'data', []) + return self.parse_tickers(tickers, symbols) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "instType": "SWAP", + # "instId": "BTC-USD-SWAP", + # "last": "114113.3", + # "lastSz": "", + # "askPx": "114113.5", + # "askSz": "56280", + # "bidPx": "114113.2", + # "bidSz": "63220", + # "open24h": "113214.7", + # "high24h": "116039.2", + # "low24h": "113214.7", + # "volCcy24h": "73.31475724", + # "vol24h": "8406739", + # "sodUtc0": "", + # "sodUtc8": "", + # "ts": "1760367816000" + # } + # + timestamp = self.safe_integer(ticker, 'ts') + marketId = self.safe_string(ticker, 'instId') + market = self.safe_market(marketId, market, '-') + symbol = market['symbol'] + last = self.safe_string(ticker, 'last') + open = self.safe_string(ticker, 'open24h') + quoteVolume = self.safe_string(ticker, 'volCcy24h') + baseVolume = self.safe_string(ticker, 'vol24h') + if market['swap'] and market['inverse']: + temp = baseVolume + baseVolume = quoteVolume + quoteVolume = temp + high = self.safe_string(ticker, 'high24h') + low = self.safe_string(ticker, 'low24h') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': high, + 'low': low, + 'bid': self.safe_string(ticker, 'bidPx'), + 'bidVolume': self.safe_string(ticker, 'bidSz'), + 'ask': self.safe_string(ticker, 'askPx'), + 'askVolume': self.safe_string(ticker, 'askSz'), + 'vwap': None, + 'open': open, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'markPrice': None, + 'indexPrice': None, + 'info': ticker, + }, market) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://www.deepcoin.com/docs/DeepCoinMarket/getTrades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch(default 100, max 500) + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + } + if limit is not None: + request['limit'] = limit # default 100, max 500 + productGroup = self.get_product_group_from_market(market) + request['productGroup'] = productGroup + response = self.publicGetDeepcoinMarketTrades(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + return self.parse_trades(data, market, since, limit) + + def get_product_group_from_market(self, market: Market) -> str: + productGroup = 'Spot' + if market['swap']: + if market['linear']: + productGroup = 'SwapU' + else: + productGroup = 'Swap' + return productGroup + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # public fetchTrades + # + # { + # "instId": "ETH-USDT", + # "tradeId": "1001056388761321", + # "px": "4095.66", + # "sz": "0.01311251", + # "side": "sell", + # "ts": "1760367870000" + # } + # + # private fetchMyTrades + # { + # "instType": "SPOT", + # "instId": "ETH-USDT", + # "tradeId": "1001056429613610", + # "ordId": "1001435238208686", + # "clOrdId": "", + # "billId": "10010564296136101", + # "tag": "", + # "fillPx": "3791.15", + # "fillSz": "0.004", + # "side": "sell", + # "posSide": "", + # "execType": "", + # "feeCcy": "USDT", + # "fee": "0.0151646", + # "ts": "1760704540000" + # } + # + marketId = self.safe_string(trade, 'instId') + market = self.safe_market(marketId, market) + timestamp = self.safe_integer(trade, 'ts') + side = self.safe_string(trade, 'side') + execType = self.safe_string(trade, 'execType') + fee = None + feeCost = self.safe_string(trade, 'fee') + if feeCost is not None: + feeCurrencyId = self.safe_string(trade, 'feeCcy') + feeCurrencyCode = self.safe_currency_code(feeCurrencyId) + fee = { + 'cost': feeCost, + 'currency': feeCurrencyCode, + } + return self.safe_trade({ + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'id': self.safe_string(trade, 'tradeId'), + 'order': self.safe_string(trade, 'ordId'), + 'type': None, + 'takerOrMaker': self.parse_taker_or_maker(execType), + 'side': side, + 'price': self.safe_string_2(trade, 'fillPx', 'px'), + 'amount': self.safe_string_2(trade, 'fillSz', 'sz'), + 'cost': None, + 'fee': fee, + }, market) + + def parse_taker_or_maker(self, execType: Str): + types = { + 'T': 'taker', + 'M': 'maker', + } + return self.safe_string(types, execType, execType) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://www.deepcoin.com/docs/DeepCoinAccount/getAccountBalance + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: "spot" or "swap", the market type for the balance + :returns dict: a `balance structure ` + """ + self.load_markets() + marketType = 'spot' + marketType, params = self.handle_market_type_and_params('fetchBalance', None, params, marketType) + request: dict = { + 'instType': self.convert_to_instrument_type(marketType), + } + response = self.privateGetDeepcoinAccountBalances(self.extend(request, params)) + return self.parse_balance(response) + + def parse_balance(self, response) -> Balances: + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "ccy": "USDT", + # "bal": "74", + # "frozenBal": "0", + # "availBal": "74" + # } + # ] + # } + # + result: dict = { + 'info': response, + 'timestamp': None, + 'datetime': None, + } + balances = self.safe_list(response, 'data', []) + for i in range(0, len(balances)): + balance = balances[i] + symbol = self.safe_string(balance, 'ccy') + code = self.safe_currency_code(symbol) + account = self.account() + account['total'] = self.safe_string(balance, 'bal') + account['used'] = self.safe_string(balance, 'frozenBal') + account['free'] = self.safe_string(balance, 'availBal') + result[code] = account + return self.safe_balance(result) + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://www.deepcoin.com/docs/assets/deposit + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch entries for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchDeposits', 'paginate', False) + if paginate: + return self.fetch_paginated_call_cursor('fetchDeposits', code, since, limit, params, 'code', None, 1, 50) + request: dict = {} + currency = None + if code is not None: + currency = self.currency(code) + request['coin'] = currency['id'] + if since is not None: + request['startTime'] = since + if limit is not None: + request['size'] = limit + until = self.safe_integer(params, 'until') + if until is not None: + request['endTime'] = until + params = self.omit(params, 'until') + response = self.privateGetDeepcoinAssetDepositList(self.extend(request, params)) + data = self.safe_dict(response, 'data', {}) + items = self.safe_list(data, 'data', []) + transactionParams: dict = { + 'type': 'deposit', + } + return self.parse_transactions(items, currency, since, limit, transactionParams) + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://www.deepcoin.com/docs/assets/withdraw + + :param str code: unified currency code of the currency transferred + :param int [since]: the earliest time in ms to fetch transfers for(default 24 hours ago) + :param int [limit]: the maximum number of transfer structures to retrieve(default 50, max 200) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch transfers for(default time now) + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchDeposits', 'paginate', False) + if paginate: + return self.fetch_paginated_call_cursor('fetchDeposits', code, since, limit, params, 'code', None, 1, 50) + request: dict = {} + currency = None + if code is not None: + currency = self.currency(code) + request['coin'] = currency['id'] + if since is not None: + request['startTime'] = since + if limit is not None: + request['size'] = limit + until = self.safe_integer(params, 'until') + if until is not None: + request['endTime'] = until + params = self.omit(params, 'until') + response = self.privateGetDeepcoinAssetWithdrawList(self.extend(request, params)) + data = self.safe_dict(response, 'data', {}) + items = self.safe_list(data, 'data', []) + transactionParams: dict = { + 'type': 'withdrawal', + } + return self.parse_transactions(items, currency, since, limit, transactionParams) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fetchDeposits + # { + # "createTime": 1760368656, + # "txHash": "03fe3244d89e794586222413c61779380da9e9fe5baaa253c38d01a4199a3499", + # "chainName": "TRC20", + # "amount": "149", + # "coin": "USDT", + # "status": "succeed" + # } + # + txid = self.safe_string(transaction, 'txHash') + currencyId = self.safe_string(transaction, 'coin') + code = self.safe_currency_code(currencyId, currency) + amount = self.safe_number(transaction, 'amount') + timestamp = self.safe_timestamp(transaction, 'createTime') + networkId = self.safe_string(transaction, 'chainName') + network = self.network_id_to_code(networkId) + status = self.parse_transaction_status(self.safe_string(transaction, 'status')) + return { + 'info': transaction, + 'id': None, + 'currency': code, + 'amount': amount, + 'network': network, + 'addressFrom': None, + 'addressTo': None, + 'address': self.safe_string(transaction, 'address'), + 'tagFrom': None, + 'tagTo': None, + 'tag': None, + 'status': status, + 'type': None, + 'updated': None, + 'txid': txid, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'internal': None, + 'comment': None, + 'fee': { + 'currency': None, + 'cost': None, + }, + } + + def parse_transaction_status(self, status: Str) -> Str: + statuses: dict = { + 'confirming': 'pending', + 'succeed': 'ok', + } + return self.safe_string(statuses, status, status) + + def fetch_deposit_addresses(self, codes: Strings = None, params={}) -> List[DepositAddress]: + """ + fetch deposit addresses for multiple currencies and chain types + + https://www.deepcoin.com/docs/assets/chainlist + + :param str[]|None codes: list of unified currency codes, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `address structures ` + """ + self.load_markets() + if codes is None: + raise ArgumentsRequired(self.id + ' fetchDepositAddresses requires a list with one currency code') + length = len(codes) + if length != 1: + raise NotSupported(self.id + ' fetchDepositAddresses requires a list with one currency code') + code = codes[0] + currency = self.currency(code) + request: dict = { + 'currency_id': currency['id'], + 'lang': 'en', + } + response = self.privateGetDeepcoinAssetRechargeChainList(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "", + # "data": { + # "list": [ + # { + # "chain": "TRC20", + # "state": 1, + # "remind": "Only support deposits and withdrawals via TRC20 network. If you send it via other address by mistake, it will not be credited and will result in the permanent loss of your deposit.", + # "inNotice": "", + # "actLogo": "", + # "address": "TNJYDW9Bk87VwfA6s7FtxURLEMHesQbYgF", + # "hasMemo": False, + # "memo": "", + # "estimatedTime": 1, + # "fastConfig": { + # "fastLimitNum": 0, + # "fastBlock": 10, + # "realBlock": 1 + # } + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + list = self.safe_list(data, 'list', []) + additionalParams: dict = { + 'currency': code, + } + return self.parse_deposit_addresses(list, codes, False, additionalParams) + + def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://www.deepcoin.com/docs/assets/chainlist + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.network]: unified network code for deposit chain + :returns dict: an `address structure ` + """ + self.load_markets() + network = self.safe_string(params, 'network') + defaultNetworks = self.safe_dict(self.options, 'defaultNetworks', {}) + defaultNetwork = self.safe_string(defaultNetworks, code) + network = network if network else defaultNetwork + if network is not None: + params = self.omit(params, 'network') + addressess = self.fetch_deposit_addresses([code], params) + length = len(addressess) + address = self.safe_dict(addressess, 0, {}) + if (network is not None) and (length > 1): + for i in range(0, length): + entry = addressess[i] + if entry['network'] == network: + address = entry + return address + + def parse_deposit_address(self, response, currency: Currency = None) -> DepositAddress: + # + # { + # "chain": "TRC20", + # "state": 1, + # "remind": "Only support deposits and withdrawals via TRC20 network. If you send it via other address by mistake, it will not be credited and will result in the permanent loss of your deposit.", + # "inNotice": "", + # "actLogo": "", + # "address": "TNJYDW9Bk87VwfA6s7FtxURLEMHesQbYgF", + # "hasMemo": False, + # "memo": "", + # "estimatedTime": 1, + # "fastConfig": { + # "fastLimitNum": 0, + # "fastBlock": 10, + # "realBlock": 1 + # } + # } + # + chain = self.safe_string(response, 'chain') + address = self.safe_string(response, 'address') + self.check_address(address) + return { + 'info': response, + 'currency': None, + 'network': self.network_id_to_code(chain), + 'address': address, + 'tag': self.safe_string(response, 'memo'), + } + + def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + + https://www.deepcoin.com/docs/DeepCoinAccount/getAccountBills + + :param str [code]: unified currency code + :param int [since]: timestamp in ms of the earliest ledger entry + :param int [limit]: max number of ledger entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest ledger entry + :param str [params.type]: 'spot' or 'swap', the market type for the ledger(default 'spot') + :returns dict[]: a list of `ledger structures ` + """ + self.load_markets() + marketType = 'spot' + marketType, params = self.handle_market_type_and_params('fetchLedger', None, params, marketType) + request: dict = { + 'instType': self.convert_to_instrument_type(marketType), + } + currency = None + if code is not None: + currency = self.currency(code) + request['ccy'] = currency['id'] + if since is not None: + request['after'] = since + if limit is not None: + request['limit'] = limit + until = self.safe_integer(params, 'until') + if until is not None: + request['before'] = until + params = self.omit(params, 'until') + response = self.privateGetDeepcoinAccountBills(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "billId": "1001044652247714", + # "ccy": "USDT", + # "clientId": "", + # "balChg": "-0.03543537", + # "bal": "72.41881427", + # "type": "5", + # "ts": "1761047448000" + # }, + # { + # "billId": "1001044652258368", + # "ccy": "DOGE", + # "clientId": "", + # "balChg": "76", + # "bal": "76", + # "type": "2", + # "ts": "1761051006000" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_ledger(data, currency, since, limit) + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + # + # { + # "billId": "1001044652247714", + # "ccy": "USDT", + # "clientId": "", + # "balChg": "-0.03543537", + # "bal": "72.41881427", + # "type": "5", + # "ts": "1761047448000" + # } + # + timestamp = self.safe_integer(item, 'ts') + change = self.safe_string(item, 'balChg') + amount = Precise.string_abs(change) + direction = 'out' if Precise.string_lt(change, '0') else 'in' + currencyId = self.safe_string(item, 'ccy') + currency = self.safe_currency(currencyId, currency) + type = self.safe_string(item, 'type') + return self.safe_ledger_entry({ + 'info': item, + 'id': self.safe_string(item, 'billId'), + 'direction': direction, + 'account': None, + 'referenceAccount': None, + 'referenceId': None, + 'type': self.parse_ledger_entry_type(type), + 'currency': currency['code'], + 'amount': amount, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'before': None, + 'after': self.safe_string(item, 'bal'), + 'status': None, + 'fee': None, + }, currency) + + def parse_ledger_entry_type(self, type): + ledgerType: dict = { + '1': 'trade', + '2': 'trade', + '3': 'transfer', + '4': 'transfer', + '5': 'fee', + } + return self.safe_string(ledgerType, type, type) + + def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://www.deepcoin.com/docs/assets/transfer + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account to transfer from('spot', 'inverse', 'linear', 'fund', 'rebate' or 'demo') + :param str toAccount: account to transfer to('spot', 'inverse', 'linear', 'fund', 'rebate' or 'demo') + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.userId]: user id + :returns dict: a `transfer structure ` + """ + userId = None + userId, params = self.handle_option_and_params(params, 'transfer', 'userId') + userId = userId if userId else self.safe_string(params, 'uid') + if userId is None: + raise ArgumentsRequired(self.id + ' transfer() requires a userId parameter') + self.load_markets() + currency = self.currency(code) + accountsByType = self.safe_dict(self.options, 'accountsByType', {}) + fromId = self.safe_string(accountsByType, fromAccount, fromAccount) + toId = self.safe_string(accountsByType, toAccount, toAccount) + request: dict = { + 'currency_id': currency['id'], + 'amount': self.currency_to_precision(code, amount), + 'from_id': fromId, + 'to_id': toId, + 'uid': userId, + } + response = self.privatePostDeepcoinAssetTransfer(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "", + # "data": { + # "retCode": 0, + # "retMsg": "", + # "retData": {} + # } + # } + # + data = self.safe_dict(response, 'data', {}) + transfer = self.parse_transfer(data, currency) + transferOptions = self.safe_dict(self.options, 'transfer', {}) + fillResponseFromRequest = self.safe_bool(transferOptions, 'fillResponseFromRequest', True) + if fillResponseFromRequest: + transfer['fromAccount'] = fromAccount + transfer['toAccount'] = toAccount + transfer['amount'] = amount + return transfer + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + # + # { + # "retCode": 0, + # "retMsg": "", + # "retData": {} + # } + # + status = self.safe_string(transfer, 'retCode') + currencyCode = self.safe_currency_code(None, currency) + return { + 'info': transfer, + 'id': None, + 'timestamp': None, + 'datetime': None, + 'currency': currencyCode, + 'amount': None, + 'fromAccount': None, + 'toAccount': None, + 'status': self.parse_transfer_status(status), + } + + def parse_transfer_status(self, status: Str) -> Str: + if status == '0': + return 'ok' + return 'failed' + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://www.deepcoin.com/docs/DeepCoinTrade/order + https://www.deepcoin.com/docs/DeepCoinTrade/triggerOrder + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.clientOrderId]: a unique id for the order + :param str [params.timeInForce]: *non trigger orders only* 'GTC'(Good Till Cancel), 'IOC'(Immediate Or Cancel) or 'PO'(Post Only) + :param bool [params.postOnly]: *non trigger orders only* True to place a post only order + :param bool [params.reduceOnly]: *non trigger orders only* a mark to reduce the position size for margin, swap and future orders + :param float [params.triggerPrice]: the price a trigger order is triggered at + :param float [params.stopLoss.triggerPrice]: the price that a stop loss order is triggered at + :param float [params.takeProfit.triggerPrice]: the price that a take profit order is triggered at + :param str [params.positionSide]: if position mode is one-way: set to 'net', if position mode is hedge-mode: set to 'long' or 'short' + :param bool [params.hedged]: *swap only* True for hedged mode, False for one way mode + :param str [params.marginMode]: *swap only*'cross' or 'isolated', the default is 'cash' for spot and 'cross' for swap + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + triggerPrice = self.safe_string(params, 'triggerPrice') + request = self.create_order_request(symbol, type, side, amount, price, params) + response = None + if triggerPrice is not None: + # trigger orders + response = self.privatePostDeepcoinTradeTriggerOrder(request) + else: + # regular orders + # + # { + # "code": "0", + # "msg": "", + # "data": { + # "ordId": "1001434570213727", + # "clOrdId": "", + # "tag": "", + # "sCode": "0", + # "sMsg": "" + # } + # } + # + response = self.privatePostDeepcoinTradeOrder(request) + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data, market) + + def create_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + @ignore + helper function to build request + """ + market = self.market(symbol) + triggerPrice = self.safe_string(params, 'triggerPrice') + # isTriggerOrder = (triggerPrice is not None) or self.safe_string_2(params, 'stopLossPrice', 'takeProfitPrice') is not None + isTriggerOrder = (triggerPrice is not None) + cost = self.safe_string(params, 'cost') + if cost is not None: + if not market['spot'] or (triggerPrice is not None): + raise BadRequest(self.id + ' createOrder() accepts a cost parameter for spot non-trigger market orders only') + if isTriggerOrder: + return self.create_trigger_order_request(symbol, type, side, amount, price, params) + else: + return self.create_regular_order_request(symbol, type, side, amount, price, params) + + def create_regular_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + @ignore + helper function to build request + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.cost]: *spot only* the cost of the order in units of the quote currency, for market orders only + :param str [params.clientOrderId]: a unique id for the order + :param str [params.timeInForce]: 'GTC'(Good Till Cancel), 'IOC'(Immediate Or Cancel) or 'PO'(Post Only) + :param bool [params.postOnly]: True to place a post only order + :param bool [params.reduceOnly]: a mark to reduce the position size for margin and swap orders + :param float [params.stopLossPrice]: the price that a stop loss order is triggered at + :param float [params.takeProfitPrice]: the price that a take profit order is triggered at + :param str [params.marginMode]: *swap only* 'cross' or 'isolated', the default is 'cash' for spot and 'cross' for swap + :param str [params.mrgPosition]: *swap only* 'merge' or 'split', the default is 'merge' + """ + market = self.market(symbol) + orderType = type + orderType, params = self.handle_type_post_only_and_time_in_force(type, params) + request: dict = { + 'instId': market['id'], + # 'tdMode': 'cash', # 'cash' for spot, 'cross' or 'isolated' for swap + # 'ccy': currency['id'], # only applicable to cross MARGIN orders in single-currency margin + # 'clOrdId': clientOrderId, + 'side': side, + 'ordType': orderType, + # 'sz': amount or cost + # 'px': price, # limit orders only + # 'reduceOnly': False, # a mark to reduce the position size for margin and swap orders + # 'tgtCcy': 'base_ccy', # spot only 'base_ccy' or 'quote_ccy', the default is 'base_ccy' for spot orders + # 'tpTriggerPx': takeProfitPrice, # take profit trigger price + # 'slTriggerPx': stopLossPrice, # stop loss trigger price + # 'posSide': 'long', # swap only 'long' or 'short' + # 'mrgPosition': 'merge', # swap only 'merge' or 'split' + # 'closePosId': 'id', # swap only position ID to close, required in split mode + } + clientOrderId = self.safe_string(params, 'clientOrderId') + if clientOrderId is not None: + request['clOrdId'] = clientOrderId + params = self.omit(params, 'clientOrderId') + stopLoss = self.safe_dict(params, 'stopLoss', {}) + stopLossPrice = self.safe_string(stopLoss, 'triggerPrice') + if stopLossPrice is not None: + params = self.omit(params, ['stopLoss']) + request['slTriggerPx'] = self.price_to_precision(symbol, stopLossPrice) + takeProfit = self.safe_dict(params, 'takeProfit', {}) + takeProfitPrice = self.safe_string(takeProfit, 'triggerPrice') + if takeProfitPrice is not None: + params = self.omit(params, ['takeProfit']) + request['tpTriggerPx'] = self.price_to_precision(symbol, takeProfitPrice) + isMarketOrder = (type == 'market') + if price is not None: + if isMarketOrder: + raise BadRequest(self.id + ' createOrder() does not require a price argument for market orders') + request['px'] = self.price_to_precision(symbol, price) + elif not isMarketOrder: + raise BadRequest(self.id + ' createOrder() requires a price argument for limit orders') + if market['spot']: + cost = self.safe_string(params, 'cost') + if cost is not None: + if not isMarketOrder: + raise BadRequest(self.id + ' createOrder() accepts a cost parameter for spot market orders only') + params = self.omit(params, 'cost') + request['sz'] = self.cost_to_precision(symbol, cost) + request['tgtCcy'] = 'quote_ccy' + else: + request['sz'] = self.amount_to_precision(symbol, amount) + request['tgtCcy'] = 'base_ccy' + request['side'] = side + request['tdMode'] = 'cash' + else: + request['sz'] = self.amount_to_precision(symbol, amount) + marginMode = 'cross' + marginMode, params = self.handle_margin_mode_and_params('createOrder', params, marginMode) + request['tdMode'] = marginMode + mrgPosition = 'merge' + mrgPosition, params = self.handle_option_and_params(params, 'createOrder', 'mrgPosition', mrgPosition) + request['mrgPosition'] = mrgPosition + posSide: Str = None + reduceOnly = self.safe_bool(params, 'reduceOnly', False) + if reduceOnly: + if side == 'buy': + posSide = 'short' + elif side == 'sell': + posSide = 'long' + else: + if side == 'buy': + posSide = 'long' + elif side == 'sell': + posSide = 'short' + request['posSide'] = posSide + return self.extend(request, params) + + def create_trigger_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + @ignore + helper function to build request + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.reduceOnly]: a mark to reduce the position size for margin orders + :param str [params.marginMode]: *swap only* 'cross' or 'isolated', the default is 'cash' for spot and 'cross' for swap + """ + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + 'productGroup': self.capitalize(market['type']), + 'sz': self.amount_to_precision(symbol, amount), + 'side': side, + # 'posSide': 'long', # 'long' or 'short' - required when product type is SWAP + # 'price': price, + # 'isCrossMargin': 1, # 1 for cross margin, 0 for isolated margin + 'orderType': type, + # 'triggerPrice': triggerPrice, + # 'mrgPosition': 'merge', # 'merge' or 'split', the default is 'merge' - required when product type is SWAP + # 'tdMode': 'cash', # 'cash' for spot, 'cross' or 'isolated' for swap + } + triggerPrice = self.safe_string(params, 'triggerPrice') + # takeProfitPrice = self.safe_string(params, 'takeProfitPrice') + # stopLossPrice = self.safe_string(params, 'stopLossPrice') + # isTpOrSlOrder = (takeProfitPrice is not None) or (stopLossPrice is not None) + # if isTpOrSlOrder: + # if takeProfitPrice is not None: + # request['triggerPrice'] = self.price_to_precision(symbol, takeProfitPrice) + # else: + # request['triggerPrice'] = self.price_to_precision(symbol, stopLossPrice) + # } + # else: + request['triggerPrice'] = self.price_to_precision(symbol, triggerPrice) + # } + if price is not None: + request['price'] = self.price_to_precision(symbol, price) + elif type == 'limit': + raise ArgumentsRequired(self.id + ' createOrder() requires a price argument for limit trigger orders') + marginMode = 'cross' + marginMode, params = self.handle_margin_mode_and_params('createOrder', params, marginMode) + isCrossMargin = 1 + if marginMode == 'isolated': + isCrossMargin = 0 + reduceOnly = self.safe_bool(params, 'reduceOnly', False) + params = self.omit(params, 'reduceOnly') + request['isCrossMargin'] = isCrossMargin + request['tdMode'] = marginMode + if market['swap']: + if reduceOnly: + if side == 'buy': + request['posSide'] = 'short' + elif side == 'sell': + request['posSide'] = 'long' + else: + if side == 'buy': + request['posSide'] = 'long' + elif side == 'sell': + request['posSide'] = 'short' + mrgPosition = 'merge' + mrgPosition, params = self.handle_option_and_params(params, 'createOrder', 'mrgPosition', mrgPosition) + request['mrgPosition'] = mrgPosition + return self.extend(request, params) + + def handle_type_post_only_and_time_in_force(self, type: OrderType, params): + postOnly = False + postOnly, params = self.handle_post_only(type == 'market', type == 'post_only', params) + if postOnly: + type = 'post_only' + timeInForce = self.handle_time_in_force(params) + params = self.omit(params, 'timeInForce') + if (timeInForce is not None) and (timeInForce == 'IOC'): + type = 'ioc' + return [type, params] + + def create_market_order_with_cost(self, symbol: str, side: OrderSide, cost: float, params={}): + """ + create a market order by providing the symbol, side and cost + :param str symbol: unified symbol of the market to create an order in + :param str side: 'buy' or 'sell' + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + params = self.extend(params, {'cost': cost}) + return self.create_order(symbol, 'market', side, 0, None, params) + + def create_market_buy_order_with_cost(self, symbol: str, cost: float, params={}) -> Order: + """ + create a market buy order by providing the symbol and cost + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + params = self.extend(params, {'cost': cost}) + return self.create_order(symbol, 'market', 'buy', 0, None, params) + + def create_market_sell_order_with_cost(self, symbol: str, cost: float, params={}) -> Order: + """ + create a market sell order by providing the symbol and cost + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + params = self.extend(params, {'cost': cost}) + return self.create_order(symbol, 'market', 'sell', 0, None, params) + + def fetch_closed_order(self, id: str, symbol: Str = None, params={}) -> Order: + """ + fetches information on a closed order made by the user + + https://www.deepcoin.com/docs/DeepCoinTrade/finishOrderByID + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchClosedOrder() requires a symbol argument') + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + 'ordId': id, + } + response = self.privateGetDeepcoinTradeFinishOrderByID(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "instType": "SPOT", + # "instId": "ETH-USDT", + # "tgtCcy": "", + # "ccy": "", + # "ordId": "1001434573319675", + # "clOrdId": "", + # "tag": "", + # "px": "4056.620000000000", + # "sz": "0.004000", + # "pnl": "0.000000", + # "ordType": "market", + # "side": "buy", + # "posSide": "", + # "tdMode": "cash", + # "accFillSz": "0.004000", + # "fillPx": "", + # "tradeId": "", + # "fillSz": "0.004000", + # "fillTime": "1760619119000", + # "avgPx": "", + # "state": "filled", + # "lever": "1.000000", + # "tpTriggerPx": "", + # "tpTriggerPxType": "", + # "tpOrdPx": "", + # "slTriggerPx": "", + # "slTriggerPxType": "", + # "slOrdPx": "", + # "feeCcy": "USDT", + # "fee": "0.000004", + # "rebateCcy": "", + # "source": "", + # "rebate": "", + # "category": "normal", + # "uTime": "1760619119000", + # "cTime": "1760619119000" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + entry = self.safe_dict(data, 0, {}) + return self.parse_order(entry, market) + + def fetch_open_order(self, id: str, symbol: Str = None, params={}) -> Order: + """ + fetch an open order by it's id + + https://www.deepcoin.com/docs/DeepCoinTrade/orderByID + + :param str id: order id + :param str symbol: unified market symbol, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchClosedOrder() requires a symbol argument') + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + 'ordId': id, + } + response = self.privateGetDeepcoinTradeOrderByID(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + length = len(data) + if length == 0: + return None + entry = self.safe_dict(data, 0, {}) + return self.parse_order(entry, market) + + def fetch_canceled_and_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + + https://www.deepcoin.com/docs/DeepCoinTrade/ordersHistory + https://www.deepcoin.com/docs/DeepCoinTrade/triggerOrdersHistory + + fetches information on multiple canceled and closed orders made by the user + :param str [symbol]: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.trigger]: whether to fetch trigger/algo orders(default False) + :param str [params.type]: *non trigger orders only* 'spot' or 'swap', the market type for the orders + :param str [params.state]: *non trigger orders only* 'canceled' or 'filled', the order state to filter by + :param str [params.OrderType]: *trigger orders only* 'limit' or 'market' + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchCanceledAndClosedOrders', 'paginate') + if paginate: + return self.fetch_paginated_call_dynamic('fetchCanceledAndClosedOrders', symbol, since, limit, params) + trigger = self.safe_bool(params, 'trigger', False) + methodName = 'fetchCanceledAndClosedOrders' + methodName, params = self.handle_param_string(params, 'methodName', methodName) + market: Market = None + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['instId'] = market['id'] + marketType = 'spot' + marketType, params = self.handle_market_type_and_params(methodName, market, params, marketType) + request['instType'] = self.convert_to_instrument_type(marketType) + if limit is not None: + request['limit'] = limit # default 100 + response = None + if trigger: + if methodName != 'fetchCanceledAndClosedOrders': + raise BadRequest(self.id + ' ' + methodName + '() does not support trigger orders') + if market is None: + raise ArgumentsRequired(self.id + ' fetchCanceledAndClosedOrders() requires a symbol argument for trigger orders') + params = self.omit(params, 'trigger') + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "instType": "SWAP", + # "instId": "DOGE-USDT-SWAP", + # "ordId": "1001110510915416", + # "px": "0", + # "sz": "76", + # "triggerPx": "0", + # "triggerPxType": "last", + # "ordType": "TPSL", + # "side": "sell", + # "posSide": "long", + # "tdMode": "cross", + # "lever": "2", + # "triggerTime": "0", + # "uTime": "1761059366000", + # "cTime": "1761059218", + # "errorCode": "0", + # "errorMsg": "" + # } + # ] + # } + # + response = self.privateGetDeepcoinTradeTriggerOrdersHistory(self.extend(request, params)) + else: + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "instType": "SPOT", + # "instId": "ETH-USDT", + # "tgtCcy": "", + # "ccy": "", + # "ordId": "1001434573319675", + # "clOrdId": "", + # "tag": "", + # "px": "4056.620000000000", + # "sz": "0.004000", + # "pnl": "0.000000", + # "ordType": "market", + # "side": "buy", + # "posSide": "", + # "tdMode": "cash", + # "accFillSz": "0.004000", + # "fillPx": "", + # "tradeId": "", + # "fillSz": "0.004000", + # "fillTime": "1760619119000", + # "avgPx": "", + # "state": "filled", + # "lever": "1.000000", + # "tpTriggerPx": "", + # "tpTriggerPxType": "", + # "tpOrdPx": "", + # "slTriggerPx": "", + # "slTriggerPxType": "", + # "slOrdPx": "", + # "feeCcy": "USDT", + # "fee": "0.000004", + # "rebateCcy": "", + # "source": "", + # "rebate": "", + # "category": "normal", + # "uTime": "1760619119000", + # "cTime": "1760619119000" + # } + # ] + # } + # + response = self.privateGetDeepcoinTradeOrdersHistory(self.extend(request, params)) + # todo handle with since, until and pagination + data = self.safe_list(response, 'data', []) + return self.parse_orders(data, market, since, limit) + + def fetch_canceled_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple canceled orders made by the user + + https://www.deepcoin.com/docs/DeepCoinTrade/ordersHistory + + :param str symbol: unified market symbol of the market the orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: 'spot' or 'swap', the market type for the orders + :returns dict[]: a list of `order structures ` + """ + methodName = 'fetchCanceledOrders' + params = self.extend(params, {'methodName': methodName}) + params = self.extend(params, {'state': 'canceled'}) + return self.fetch_canceled_and_closed_orders(symbol, since, limit, params) + + def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://www.deepcoin.com/docs/DeepCoinTrade/ordersHistory + + :param str symbol: unified market symbol of the market the orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: 'spot' or 'swap', the market type for the orders + :returns dict[]: a list of `order structures ` + """ + methodName = 'fetchClosedOrders' + params = self.extend(params, {'methodName': methodName}) + params = self.extend(params, {'state': 'filled'}) + return self.fetch_canceled_and_closed_orders(symbol, since, limit, params) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://www.deepcoin.com/docs/DeepCoinTrade/ordersPendingV2 + https://www.deepcoin.com/docs/DeepCoinTrade/triggerOrdersPending + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.trigger]: whether to fetch trigger/algo orders(default False) + :param int [params.index]: *non trigger orders only* pagination index, default is 1 + :param str [params.orderType]: *trigger orders only* 'limit' or 'market' + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOpenOrders() requires a symbol argument') + market = self.market(symbol) + index = self.safe_integer(params, 'index', 1) # todo add pagination handling + request: dict = { + 'instId': market['id'], + } + if limit is not None: + request['limit'] = limit + trigger = self.safe_bool(params, 'trigger', False) + response = None + if trigger: + params = self.omit(params, 'trigger') + request['instType'] = self.convert_to_instrument_type(market['type']) + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "instType": "SPOT", + # "instId": "DOGE-USDT", + # "ordId": "1001442305797142", + # "triggerPx": "0.01", + # "ordPx": "0.01", + # "sz": "20", + # "ordType": "", + # "side": "buy", + # "posSide": "", + # "tdMode": "cash", + # "triggerOrderType": "Conditional", + # "triggerPxType": "last", + # "lever": "", + # "slPrice": "", + # "slTriggerPrice": "", + # "tpPrice": "", + # "tpTriggerPrice": "", + # "closeSLTriggerPrice": "", + # "closeTPTriggerPrice": "", + # "cTime": "1761814167000", + # "uTime": "1761814167000" + # } + # ] + # } + # + response = self.privateGetDeepcoinTradeTriggerOrdersPending(self.extend(request, params)) + else: + request['index'] = index + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "instType": "SPOT", + # "instId": "ETH-USDT", + # "tgtCcy": "", + # "ccy": "", + # "ordId": "1001435158096314", + # "clOrdId": "", + # "tag": "", + # "px": "1000.000000000000", + # "sz": "0.004000", + # "pnl": "0.000000", + # "ordType": "limit", + # "side": "buy", + # "posSide": "", + # "tdMode": "cash", + # "accFillSz": "0.000000", + # "fillPx": "", + # "tradeId": "", + # "fillSz": "0.000000", + # "fillTime": "1760695267000", + # "avgPx": "", + # "state": "live", + # "lever": "1", + # "tpTriggerPx": "", + # "tpTriggerPxType": "", + # "tpOrdPx": "", + # "slTriggerPx": "", + # "slTriggerPxType": "", + # "slOrdPx": "", + # "feeCcy": "USDT", + # "fee": "0.000000", + # "rebateCcy": "", + # "source": "", + # "rebate": "", + # "category": "normal", + # "uTime": "1760695267000", + # "cTime": "1760695267000" + # } + # ] + # } + # + response = self.privateGetDeepcoinTradeV2OrdersPending(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + return self.parse_orders(data, market, since, limit, {'status': 'open'}) + + def cancel_order(self, id: str, symbol: Str = None, params={}) -> Order: + """ + cancels an open order + + https://www.deepcoin.com/docs/DeepCoinTrade/cancelOrder + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.trigger]: whether the order is a trigger/algo order(default False) + :returns dict: An `order structure ` + """ + self.load_markets() + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + 'ordId': id, + } + response = None + trigger = self.safe_bool(params, 'trigger', False) + if trigger: + params = self.omit(params, 'trigger') + response = self.privatePostDeepcoinTradeCancelTriggerOrder(self.extend(request, params)) + else: + response = self.privatePostDeepcoinTradeCancelOrder(self.extend(request, params)) + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data, market) + + def cancel_all_orders(self, symbol: Str = None, params={}) -> List[Order]: + """ + cancel all open orders in a market + + https://www.deepcoin.com/docs/DeepCoinTrade/cancelAllOrder + + :param str symbol: unified market symbol of the market to cancel orders in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: *swap only* 'cross' or 'isolated', the default is 'cash' for spot and 'cross' for swap + :param bool [params.merged]: *swap only* True for merged positions, False for split positions(default True) + :returns dict[]: a list of `order structures ` + """ + self.load_markets() + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelAllOrders() requires a symbol argument') + market = self.market(symbol) + if market['spot']: + raise NotSupported(self.id + ' cancelAllOrders() is not supported for spot markets') + productGroup = self.get_product_group_from_market(market) + marginMode = self.safe_string(params, 'marginMode') + encodedMarginMode = 1 + if marginMode is not None: + params = self.omit(params, 'marginMode') + if marginMode == 'isolated': + encodedMarginMode = 0 + merged = True + merged, params = self.handle_option_and_params(params, 'cancelAllOrders', 'merged', merged) + request: dict = { + 'InstrumentID': market['id'], + 'ProductGroup': productGroup, + 'IsCrossMargin': encodedMarginMode, + 'IsMergeMode': 1 if merged else 0, + } + response = self.privatePostDeepcoinTradeSwapCancelAll(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + return self.parse_orders(data, market) + + def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + """ + edit a trade order + + https://www.deepcoin.com/docs/DeepCoinTrade/replaceOrder + https://www.deepcoin.com/docs/DeepCoinTrade/replaceTPSL + + :param str id: cancel order id + :param str [symbol]: unified symbol of the market to create an order in(not used in deepcoin editOrder) + :param str [type]: 'market' or 'limit'(not used in deepcoin editOrder) + :param str [side]: 'buy' or 'sell'(not used in deepcoin editOrder) + :param float [amount]: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.stopLossPrice]: the price that a stop loss order is triggered at + :param float [params.takeProfitPrice]: the price that a take profit order is triggered at + :returns dict: an `order structure ` + """ + self.load_markets() + request: dict = { + 'OrderSysID': id, + } + market: Market = None + if symbol is not None: + market = self.market(symbol) + if market['spot']: + raise NotSupported(self.id + ' editOrder() is not supported for spot markets') + symbol = market['symbol'] + stopLossPrice = self.safe_number(params, 'stopLossPrice') + takeProfitPrice = self.safe_number(params, 'takeProfitPrice') + isTPSL = (stopLossPrice is not None) or (takeProfitPrice is not None) + response = None + if isTPSL: + if (price is not None) or (amount is not None): + raise BadRequest(self.id + ' editOrder() with stopLossPrice or takeProfitPrice cannot have price or amount. Either use stopLossPrice/takeProfitPrice or price/amount to edit order.') + if stopLossPrice is not None: + request['slTriggerPx'] = self.price_to_precision(symbol, stopLossPrice) if symbol else self.number_to_string(stopLossPrice) + if takeProfitPrice is not None: + request['tpTriggerPx'] = self.price_to_precision(symbol, takeProfitPrice) if symbol else self.number_to_string(takeProfitPrice) + params = self.omit(params, ['stopLossPrice', 'takeProfitPrice']) + response = self.privatePostDeepcoinTradeReplaceOrderSltp(self.extend(request, params)) + else: + if price is not None: + if symbol is not None: + request['price'] = self.price_to_precision(symbol, price) + else: + request['price'] = self.number_to_string(price) + if amount is not None: + if symbol is not None: + request['volume'] = self.amount_to_precision(symbol, amount) + else: + request['volume'] = self.number_to_string(amount) + response = self.privatePostDeepcoinTradeReplaceOrder(self.extend(request, params)) + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data) + + def cancel_orders(self, ids: List[str], symbol: Str = None, params={}) -> List[Order]: + """ + cancel multiple orders + :param str[] ids: order ids + :param str [symbol]: unified market symbol, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an list of `order structures ` + """ + self.load_markets() + market: Market = None + if symbol is not None: + market = self.market(symbol) + if market['spot']: + raise NotSupported(self.id + ' cancelOrders() is not supported for spot markets') + request: dict = { + 'OrderSysIDs': ids, + } + response = self.privatePostDeepcoinTradeBatchCancelOrder(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + return self.parse_orders(data, market) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # regular order + # { + # "instType": "SPOT", + # "instId": "ETH-USDT", + # "tgtCcy": "", + # "ccy": "", + # "ordId": "1001434573319675", + # "clOrdId": "", + # "tag": "", + # "px": "4056.620000000000", + # "sz": "0.004000", + # "pnl": "0.000000", + # "ordType": "market", + # "side": "buy", + # "posSide": "", + # "tdMode": "cash", + # "accFillSz": "0.004000", + # "fillPx": "", + # "tradeId": "", + # "fillSz": "0.004000", + # "fillTime": "1760619119000", + # "avgPx": "", + # "state": "filled", + # "lever": "1.000000", + # "tpTriggerPx": "", + # "tpTriggerPxType": "", + # "tpOrdPx": "", + # "slTriggerPx": "", + # "slTriggerPxType": "", + # "slOrdPx": "", + # "feeCcy": "USDT", + # "fee": "0.000004", + # "rebateCcy": "", + # "source": "", + # "rebate": "", + # "category": "normal", + # "uTime": "1760619119000", + # "cTime": "1760619119000" + # } + # + # trigger order + # { + # "instType": "SPOT", + # "instId": "DOGE-USDT", + # "ordId": "1001442305797142", + # "triggerPx": "0.01", + # "ordPx": "0.01", + # "sz": "20", + # "ordType": "", + # "side": "buy", + # "posSide": "", + # "tdMode": "cash", + # "triggerOrderType": "Conditional", + # "triggerPxType": "last", + # "lever": "", + # "slPrice": "", + # "slTriggerPrice": "", + # "tpPrice": "", + # "tpTriggerPrice": "", + # "closeSLTriggerPrice": "", + # "closeTPTriggerPrice": "", + # "cTime": "1761814167000", + # "uTime": "1761814167000" + # } + # + marketId = self.safe_string(order, 'instId') + market = self.safe_market(marketId, market) + timestamp = self.safe_integer(order, 'cTime') + timestampString = self.safe_string(order, 'cTime', '') + if len(timestampString) < 13: + timestamp = self.safe_timestamp(order, 'cTime') + state = self.safe_string(order, 'state') + orderType = self.safe_string(order, 'ordType') + average = self.safe_string(order, 'avgPx') + if average == '': + average = None + feeCurrencyId = self.safe_string(order, 'feeCcy') + fee = None + if feeCurrencyId is not None: + feeCost = self.safe_string(order, 'fee') + fee = { + 'cost': self.parse_number(feeCost), + 'currency': self.safe_currency_code(feeCurrencyId), + } + return self.safe_order({ + 'id': self.safe_string(order, 'ordId'), + 'clientOrderId': self.safe_string(order, 'clOrdId'), + 'datetime': self.iso8601(timestamp), + 'timestamp': timestamp, + 'lastTradeTimestamp': None, + 'lastUpdateTimestamp': self.safe_integer(order, 'uTime'), + 'status': self.parse_order_status(state), + 'symbol': market['symbol'], + 'type': self.parse_order_type(orderType), + 'timeInForce': self.parse_order_time_in_force(orderType), + 'side': self.safe_string(order, 'side'), + 'price': self.safe_string_2(order, 'px', 'ordPx'), + 'average': average, + 'amount': self.safe_string(order, 'sz'), + 'filled': self.safe_string(order, 'accFillSz'), + 'remaining': None, + 'triggerPrice': self.omit_zero(self.safe_string(order, 'triggerPx')), + 'takeProfitPrice': self.safe_string_2(order, 'tpTriggerPx', 'tpTriggerPrice'), + 'stopLossPrice': self.safe_string_2(order, 'slTriggerPx', 'slTriggerPrice'), + 'cost': None, + 'trades': None, + 'fee': fee, + 'reduceOnly': None, + 'postOnly': (orderType == 'post_only') if orderType else None, + 'info': order, + }, market) + + def parse_order_status(self, status: Str) -> Str: + statuses = { + 'live': 'open', + 'filled': 'closed', + 'canceled': 'canceled', + 'partially_filled': 'open', + } + return self.safe_string(statuses, status, status) + + def parse_order_type(self, type: Str) -> Str: + types = { + 'limit': 'limit', + 'market': 'market', + 'post_only': 'limit', + 'ioc': 'market', + 'TPSL': 'market', + } + return self.safe_string(types, type, type) + + def parse_order_time_in_force(self, type: Str) -> Str: + timeInForces = { + 'post_only': 'PO', + 'ioc': 'IOC', + 'limit': 'GTC', + 'market': 'GTC', + } + return self.safe_string(timeInForces, type, type) + + def fetch_positions_for_symbol(self, symbol: str, params={}) -> List[Position]: + """ + fetch open positions for a single market + + https://www.deepcoin.com/docs/DeepCoinAccount/accountPositions + + fetch all open positions for specific symbol + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + self.load_markets() + market = self.market(symbol) + instrumentType = self.convert_to_instrument_type(market['type']) + request: dict = { + 'instType': instrumentType, + 'instId': market['id'], + } + response = self.privateGetDeepcoinAccountPositions(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + return self.parse_positions(data, [market['symbol']]) + + def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://www.deepcoin.com/docs/DeepCoinAccount/accountPositions + + :param str[] [symbols]: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + self.load_markets() + symbols = self.market_symbols(symbols, None, True, True) + marketType = 'swap' + market: Market = None + if symbols is not None: + firstSymbol = self.safe_string(symbols, 0) + market = self.market(firstSymbol) + marketType, params = self.handle_market_type_and_params('fetchPositions', market, params, marketType) + instrumentType = self.convert_to_instrument_type(marketType) + request: dict = { + 'instType': instrumentType, + } + response = self.privateGetDeepcoinAccountPositions(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "instType": "SWAP", + # "mgnMode": "cross", + # "instId": "DOGE-USDT-SWAP", + # "posId": "1001110099878275", + # "posSide": "long", + # "pos": "20", + # "avgPx": "0.18408", + # "lever": "75", + # "liqPx": "0.00001", + # "useMargin": "0.049088", + # "mrgPosition": "merge", + # "ccy": "USDT", + # "uTime": "1760709419000", + # "cTime": "1760709419000" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_positions(data, symbols) + + def parse_position(self, position: dict, market: Market = None) -> Position: + # + # { + # "instType": "SWAP", + # "mgnMode": "cross", + # "instId": "DOGE-USDT-SWAP", + # "posId": "1001110099878275", + # "posSide": "long", + # "pos": "20", + # "avgPx": "0.18408", + # "lever": "75", + # "liqPx": "0.00001", + # "useMargin": "0.049088", + # "mrgPosition": "merge", + # "ccy": "USDT", + # "uTime": "1760709419000", + # "cTime": "1760709419000" + # } + # + marketId = self.safe_string(position, 'instId') + market = self.safe_market(marketId, market) + timestamp = self.safe_integer(position, 'cTime') + return self.safe_position({ + 'symbol': market['symbol'], + 'id': self.safe_string(position, 'posId'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'contracts': self.safe_string(position, 'pos'), + 'contractSize': None, + 'side': self.safe_string(position, 'posSide'), + 'notional': None, + 'leverage': self.omit_zero(self.safe_string(position, 'lever')), + 'unrealizedPnl': None, + 'realizedPnl': None, + 'collateral': None, + 'entryPrice': self.safe_string(position, 'avgPx'), + 'markPrice': None, + 'liquidationPrice': self.safe_string(position, 'liqPx'), + 'marginMode': self.safe_string(position, 'mgnMode'), + 'hedged': True, + 'maintenanceMargin': self.safe_string(position, 'useMargin'), + 'maintenanceMarginPercentage': None, + 'initialMargin': None, + 'initialMarginPercentage': None, + 'marginRatio': None, + 'lastUpdateTimestamp': self.safe_integer(position, 'uTime'), + 'lastPrice': None, + 'stopLossPrice': None, + 'takeProfitPrice': None, + 'percentage': None, + 'info': position, + }) + + def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://www.deepcoin.com/docs/DeepCoinAccount/accountSetLeverage + + :param float leverage: the rate of leverage + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated'(default is cross) + :param str [params.mrgPosition]: 'merge' or 'split', default is merge + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setLeverage() requires a symbol argument') + # WARNING: THIS WILL INCREASE LIQUIDATION PRICE FOR OPEN ISOLATED LONG POSITIONS + # AND DECREASE LIQUIDATION PRICE FOR OPEN ISOLATED SHORT POSITIONS + if leverage < 1: + raise BadRequest(self.id + ' setLeverage() leverage should be minimum 1') + self.load_markets() + market = self.market(symbol) + marginMode = 'cross' + marginMode, params = self.handle_margin_mode_and_params('setLeverage', params, marginMode) + if (marginMode != 'cross') and (marginMode != 'isolated'): + raise BadRequest(self.id + ' setLeverage() requires a marginMode parameter that must be either cross or isolated') + mrgPosition = 'merge' + mrgPosition, params = self.handle_option_and_params(params, 'setLeverage', 'mrgPosition', mrgPosition) + if mrgPosition != 'merge' and mrgPosition != 'split': + raise BadRequest(self.id + ' setLeverage() mrgPosition parameter must be either merge or split') + request: dict = { + 'lever': leverage, + 'mgnMode': marginMode, + 'instId': market['id'], + 'mrgPosition': mrgPosition, + } + response = self.privatePostDeepcoinAccountSetLeverage(self.extend(request, params)) + # + # { + # code: '0', + # msg: '', + # data: { + # instId: 'ETH-USDT-SWAP', + # lever: '2', + # mgnMode: 'cross', + # mrgPosition: 'merge', + # sCode: '0', + # sMsg: '' + # } + # } + # + return response + + def fetch_funding_rates(self, symbols: Strings = None, params={}) -> FundingRates: + """ + fetch the funding rate for multiple markets + + https://www.deepcoin.com/docs/DeepCoinTrade/currentFundRate + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.subType]: "linear" or "inverse" + :returns dict[]: a list of `funding rate structures `, indexed by market symbols + """ + self.load_markets() + symbols = self.market_symbols(symbols, 'swap', True, True, True) + subType = 'linear' + firstMarket: Market = None + if symbols is not None: + firstSymbol = self.safe_string(symbols, 0) + firstMarket = self.market(firstSymbol) + subType, params = self.handle_sub_type_and_params('fetchFundingRates', firstMarket, params, subType) + instType = 'SwapU' + if subType == 'inverse': + instType = 'Swap' + elif subType != 'linear': + raise BadRequest(self.id + ' fetchFundingRates() subType parameter must be either linear or inverse') + request: dict = { + 'instType': instType, + } + response = self.privateGetDeepcoinTradeFundRateCurrentFundingRate(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "", + # "data": { + # "current_fund_rates": [ + # { + # "instrumentId": "SPKUSDT", + # "fundingRate": 0.00005 + # }, + # { + # "instrumentId": "LAUNCHCOINUSDT", + # "fundingRate": 0.00005 + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + rates = self.safe_list(data, 'current_fund_rates', []) + return self.parse_funding_rates(rates, symbols) + + def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate + + https://www.deepcoin.com/docs/DeepCoinTrade/currentFundRate + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + self.load_markets() + market = self.market(symbol) + if not market['swap']: + raise ExchangeError(self.id + ' fetchFundingRate() is only valid for swap markets') + request: dict = { + 'instId': market['id'], + 'instType': self.get_product_group_from_market(market), + } + response = self.privateGetDeepcoinTradeFundRateCurrentFundingRate(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "", + # "data": { + # "current_fund_rates": [ + # { + # "instrumentId": "ETHUSDT", + # "fundingRate": 0.0000402356250176 + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + rates = self.safe_list(data, 'current_fund_rates', []) + entry = self.safe_dict(rates, 0, {}) + return self.parse_funding_rate(entry, market) + + def parse_funding_rate(self, contract, market: Market = None) -> FundingRate: + # + # { + # "instrumentId": "ETHUSDT", + # "fundingRate": 0.0000402356250176 + # } + # + marketId = self.safe_string_2(contract, 'instrumentId', 'instrumentID') + symbol = self.safe_symbol(marketId, market) + return { + 'info': contract, + 'symbol': symbol, + 'markPrice': None, + 'indexPrice': None, + 'interestRate': None, + 'estimatedSettlePrice': None, + 'timestamp': None, + 'datetime': None, + 'fundingRate': self.safe_number(contract, 'fundingRate'), + 'fundingTimestamp': None, + 'fundingDatetime': None, + 'nextFundingRate': None, + 'nextFundingTimestamp': None, + 'nextFundingDatetime': None, + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': None, + } + + def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical funding rate prices + + https://www.deepcoin.com/docs/DeepCoinTrade/fundingRateHistory + + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: timestamp in ms of the earliest funding rate to fetch + :param int [limit]: the maximum amount of `funding rate structures ` to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.page]: pagination page number + :returns dict[]: a list of `funding rate structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchFundingRateHistory() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + } + if limit is not None: + request['size'] = limit # default 20, max 100 + response = self.privateGetDeepcoinTradeFundRateHistory(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "", + # "data": { + # "rows": [ + # { + # "instrumentID": "ETHUSD", + # "rate": "0.00046493", + # "CreateTime": 1760860800, + # "ratePeriodSec": 0 + # }, + # { + # "instrumentID": "ETHUSD", + # "rate": "0.00047949", + # "CreateTime": 1760832000, + # "ratePeriodSec": 0 + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + rows = self.safe_list(data, 'rows', []) + return self.parse_funding_rate_histories(rows, market, since, limit) + + def parse_funding_rate_history(self, info, market: Market = None): + # + # { + # "instrumentID": "ETHUSD", + # "rate": "0.00047949", + # "CreateTime": 1760832000, + # "ratePeriodSec": 0 + # } + # + timestamp = self.safe_timestamp(info, 'CreateTime') + instrumentID = self.safe_string_2(info, 'instrumentID', 'instrumentId') + market = self.safe_market(instrumentID, market, None, 'swap') + return { + 'info': info, + 'symbol': market['symbol'], + 'fundingRate': self.safe_number(info, 'rate'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://www.deepcoin.com/docs/DeepCoinTrade/tradeFills + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest trade to fetch + :param str [params.type]: 'spot' or 'swap', the market type for the trades(default is 'spot') + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchMyTrades', 'paginate') + if paginate: + return self.fetch_paginated_call_dynamic('fetchMyTrades', symbol, since, limit, params) + market: Market = None + if symbol is not None: + market = self.market(symbol) + marketType = 'spot' + marketType, params = self.handle_market_type_and_params('fetchMyTrades', market, params, marketType) + request: dict = { + 'instType': self.convert_to_instrument_type(marketType), + } + if market is not None: + request['instId'] = market['id'] + if since is not None: + request['begin'] = since + if limit is not None: + request['limit'] = limit # default 100, max 100 + until = self.safe_integer(params, 'until') + if until is not None: + params = self.omit(params, 'until') + request['end'] = until + response = self.privateGetDeepcoinTradeFills(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "instType": "SPOT", + # "instId": "ETH-USDT", + # "tradeId": "1001056429613610", + # "ordId": "1001435238208686", + # "clOrdId": "", + # "billId": "10010564296136101", + # "tag": "", + # "fillPx": "3791.15", + # "fillSz": "0.004", + # "side": "sell", + # "posSide": "", + # "execType": "", + # "feeCcy": "USDT", + # "fee": "0.0151646", + # "ts": "1760704540000" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_trades(data, market, since, limit) + + def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all the trades made from a single order + + https://www.deepcoin.com/docs/DeepCoinTrade/tradeFills + + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: 'spot' or 'swap', the market type for the trades + :returns dict[]: a list of `trade structures ` + """ + self.load_markets() + marketType = self.safe_string(params, 'type') + if symbol is None and marketType is None: + raise ArgumentsRequired(self.id + ' fetchOrderTrades requires a symbol argument or a market type in the params') + params = self.extend({'ordId': id}, params) + return self.fetch_my_trades(symbol, since, limit, params) + + def close_position(self, symbol: str, side: OrderSide = None, params={}) -> Order: + """ + closes open positions for a market + + https://www.deepcoin.com/docs/DeepCoinTrade/batchClosePosition + https://www.deepcoin.com/docs/DeepCoinTrade/closePositionByIds + + :param str symbol: Unified CCXT market symbol + :param str [side]: not used by deepcoin + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str|None [params.positionId]: the id of the position you would like to close + :param str[]|None [params.positionIds]: list of position ids to close(for batch closing) + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + productGroup = self.get_product_group_from_market(market) + positionId = self.safe_string(params, 'positionId') + positionIds = self.safe_list(params, 'positionIds') + request: dict = { + 'instId': market['id'], + 'productGroup': productGroup, + } + response = None + if positionId is None and positionIds is None: + response = self.privatePostDeepcoinTradeBatchClosePosition(self.extend(request, params)) + else: + if positionId is not None: + params = self.omit(params, 'positionId') + request['positionIds'] = [positionId] + response = self.privatePostDeepcoinTradeClosePositionByIds(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + return self.parse_order(data, market) + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + requestPath = path + if method == 'GET': + query = self.urlencode(params) + if len(query): + requestPath += '?' + query + url = self.urls['api'][api] + '/' + requestPath + if api == 'private': + self.check_required_credentials() + timestamp = self.milliseconds() + dateTime = self.iso8601(timestamp) + payload = dateTime + method + '/' + requestPath + headers = { + 'DC-ACCESS-KEY': self.apiKey, + 'DC-ACCESS-TIMESTAMP': dateTime, + 'DC-ACCESS-PASSPHRASE': self.password, + 'appid': '200103', + } + if method != 'GET': + body = self.json(params) + headers['Content-Type'] = 'application/json' + payload += body + signature = self.hmac(self.encode(payload), self.encode(self.secret), hashlib.sha256, 'base64') + headers['DC-ACCESS-SIGN'] = signature + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + data = self.safe_dict(response, 'data', {}) + msg = self.safe_string(response, 'msg') + messageCode = self.safe_string(response, 'code') + sCode = self.safe_string(data, 'sCode') + sMsg = self.safe_string(data, 'sMsg') + errorCode = self.safe_string(data, 'errorCode') + if (msg is not None) and (msg == '') and (sMsg is not None): + msg = sMsg + errorList = self.safe_list(data, 'errorList') + if errorList is not None: + for i in range(0, len(errorList)): + entry = self.safe_dict(errorList, i, {}) + errorCode = self.safe_string(entry, 'errorCode') + feedback = self.id + ' ' + body + if (sCode is None) and (errorCode is not None): + sCode = errorCode + retCode = self.safe_string(data, 'retCode') + if (sCode is None) and (retCode is not None): + sCode = retCode + if (code != 200) or (messageCode != '0') or (sCode is not None and sCode != '0'): + self.throw_exactly_matched_exception(self.exceptions['exact'], messageCode, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], sCode, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], msg, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], msg, feedback) + raise ExchangeError(feedback) + else: + list = self.safe_list(data, 'list', []) + if ('list' in data) and (list is None): + raise NullResponse(feedback) + return None diff --git a/ccxt/defx.py b/ccxt/defx.py new file mode 100644 index 0000000..f0b3e57 --- /dev/null +++ b/ccxt/defx.py @@ -0,0 +1,2071 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.defx import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currency, Int, LedgerEntry, Leverage, Market, Num, Order, OrderBook, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, Trade, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import NotSupported +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class defx(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(defx, self).describe(), { + 'id': 'defx', + 'name': 'Defx X', + # 'countries': [''], + 'rateLimit': 100, + 'version': 'v1', + 'certified': False, + 'pro': False, + 'hostname': 'defx.com', + 'dex': True, + 'has': { + 'CORS': None, + 'spot': False, + 'margin': False, + 'swap': True, + 'future': False, + 'option': False, + 'addMargin': True, + 'cancelAllOrders': True, + 'cancelAllOrdersAfter': False, + 'cancelOrder': True, + 'cancelWithdraw': False, + 'closeAllPositions': True, + 'closePosition': True, + 'createConvertTrade': False, + 'createDepositAddress': False, + 'createMarketBuyOrderWithCost': False, + 'createMarketOrder': False, + 'createMarketOrderWithCost': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createOrderWithTakeProfitAndStopLoss': True, + 'createReduceOnlyOrder': True, + 'createStopLimitOrder': False, + 'createStopLossOrder': False, + 'createStopMarketOrder': False, + 'createStopOrder': False, + 'createTakeProfitOrder': True, + 'createTrailingAmountOrder': False, + 'createTrailingPercentOrder': False, + 'createTriggerOrder': True, + 'fetchAccounts': False, + 'fetchBalance': True, + 'fetchCanceledOrders': True, + 'fetchClosedOrder': False, + 'fetchClosedOrders': True, + 'fetchConvertCurrencies': False, + 'fetchConvertQuote': False, + 'fetchConvertTrade': False, + 'fetchConvertTradeHistory': False, + 'fetchCurrencies': False, + 'fetchDepositAddress': False, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': False, + 'fetchDepositsWithdrawals': False, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': True, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchIndexOHLCV': False, + 'fetchLedger': True, + 'fetchLeverage': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrice': False, + 'fetchMarkPrices': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterestHistory': False, + 'fetchOpenOrder': False, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchOrderTrades': False, + 'fetchPosition': True, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': True, + 'fetchPositionsHistory': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchStatus': True, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': False, + 'fetchTransactions': False, + 'fetchTransfers': False, + 'fetchWithdrawals': False, + 'reduceMargin': False, + 'sandbox': True, + 'setLeverage': True, + 'setMargin': False, + 'setPositionMode': False, + 'transfer': False, + 'withdraw': True, + }, + 'timeframes': { + '1m': '1m', + '3m': '3m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1h', + '2h': '2h', + '4h': '4h', + '12h': '12h', + '1d': '1d', + '1w': '1w', + '1M': '1M', + }, + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/4e92bace-d7a9-45ea-92be-122168dc87e4', + 'api': { + 'public': 'https://api.{hostname}', + 'private': 'https://api.{hostname}', + }, + 'test': { + 'public': 'https://api.testnet.{hostname}', + 'private': 'https://api.testnet.{hostname}', + }, + 'www': 'https://defx.com/home', + 'doc': [ + 'https://docs.defx.com/docs', + 'https://api-docs.defx.com/', + ], + 'fees': [ + '', + ], + 'referral': { + 'url': 'https://app.defx.com/join/6I2CZ7', + }, + }, + 'api': { + 'v1': { + 'public': { + 'get': { + 'healthcheck/ping': 1, + 'symbols/{symbol}/ohlc': 1, + 'symbols/{symbol}/trades': 1, + 'symbols/{symbol}/prices': 1, + 'symbols/{symbol}/ticker/24hr': 1, + 'symbols/{symbol}/depth/{level}/{slab}': 1, + 'ticker/24HrAgg': 1, + 'c/markets': 1, + 'c/markets/metadata': 1, + 'analytics/market/stats/newUsers': 1, + 'analytics/market/stats/tvl': 1, + 'analytics/market/stats/volumeByInstrument': 1, + 'analytics/market/stats/liquidation': 1, + 'analytics/market/stats/totalVolume': 1, + 'analytics/market/stats/openInterest': 1, + 'analytics/market/stats/totalTrades': 1, + 'analytics/market/stats/basis': 1, + 'analytics/market/stats/insuranceFund': 1, + 'analytics/market/stats/longAndShortRatio': 1, + 'analytics/market/stats/fundingRate': 1, + 'analytics/market/overview': 1, + 'explorer/search': 1, + 'explorer/transactions': 1, + 'explorer/blocks': 1, + }, + }, + 'private': { + 'get': { + 'api/order/{orderId}': 1, + 'api/orders': 1, + 'api/orders/oco/{parentOrderId}': 1, + 'api/trades': 1, + 'api/position/active': 1, + 'api/users/metadata/leverage': 1, + 'api/users/metadata/feeMultiplier': 1, + 'api/users/metadata/slippage': 1, + 'api/users/referral': 1, + 'api/users/apikeys': 1, + 'connection-signature-message/evm': 1, + 'api/users/profile/wallets': 1, + 'api/notifications': 1, + 'api/wallet/balance': 1, + 'api/wallet/transactions': 1, + 'api/analytics/user/overview': 1, + 'api/analytics/user/pnl': 1, + 'api/analytics/points/overview': 1, + 'api/analytics/points/history': 1, + }, + 'post': { + 'api/order': 1, + 'api/position/oco': 1, + 'api/users/socket/listenKeys': 1, + 'api/users/metadata/leverage': 1, + 'api/users/metadata/feeMultiplier': 1, + 'api/users/metadata/slippage': 1, + 'api/users/referral/recordReferralSignup': 1, + 'api/users/apikeys': 1, + 'api/users/profile/wallets': 1, + 'api/transfers/withdrawal': 1, + 'api/transfers/bridge/withdrawal': 1, + }, + 'put': { + 'api/position/updatePositionMargin': 1, + 'api/users/socket/listenKeys/{listenKey}': 1, + 'api/users/apikeys/{accessKey}/status': 1, + 'api/users/referral': 1, + }, + 'patch': { + 'api/users/apikeys/{accessKey}': 1, + }, + 'delete': { + 'api/orders/allOpen': 1, + 'api/order/{orderId}': 1, + 'api/position/{positionId}': 1, + 'api/position/all': 1, + 'api/users/socket/listenKeys/{listenKey}': 1, + 'api/users/apikeys/{accessKey}': 1, + }, + }, + }, + }, + 'fees': { + 'trading': { + 'tierBased': True, + 'percentage': True, + 'maker': self.parse_number('0.0002'), + 'taker': self.parse_number('0.0005'), + }, + }, + 'options': { + 'sandboxMode': False, + }, + 'features': { + 'spot': None, + 'forDerivatives': { + 'sandbox': True, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + # todo implement + 'triggerPriceType': { + 'last': True, + 'mark': True, + 'index': False, + }, + 'triggerDirection': False, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'selfTradePrevention': False, + 'trailing': False, + 'iceberg': False, + 'leverage': False, + 'marketBuyByCost': False, + 'marketBuyRequiresPrice': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': None, + 'untilDays': None, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': True, + 'limit': 100, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': 500, + 'daysBack': 100000, + 'untilDays': 100000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 500, + 'daysBack': 100000, + 'daysBackCanceled': 1, + 'untilDays': 100000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 1000, + }, + }, + 'swap': { + 'linear': { + 'extends': 'forDerivatives', + }, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'commonCurrencies': {}, + 'exceptions': { + 'exact': { + '404': BadRequest, # {"errorCode":404,"errorMessage":"Not Found"} + 'missing_auth_signature': AuthenticationError, # {"msg":"Missing auth signature","code":"missing_auth_signature"} + 'order_rejected': InvalidOrder, # {"success":false,"err":{"msg":"Order has already been rejected","code":"order_rejected"}} + 'invalid_order_id': InvalidOrder, # {"success":false,"err":{"msg":"Invalid order id","code":"invalid_order_id"}} + 'filter_lotsize_maxqty': InvalidOrder, # {"errorCode":"filter_lotsize_maxqty","errorMessage":"LOT_SIZE filter failed, quantity more than maxQty","errorData":{"maxQty":"5000.00"}} + 'filter_notional_min': InvalidOrder, # {"errorCode":"filter_notional_min","errorMessage":"NOTIONAL filter failed, Notional value of quote asset less than minNotional","errorData":{"minNotional":"100.00000000"}} + 'failed_index_price_up_multiplier_filter': InvalidOrder, # {"errorCode":"failed_index_price_up_multiplier_filter","errorMessage":"failed_index_price_up_multiplier_filter","errorData":{"maxPrice":"307.81241042"}} + 'no_open_orders': InvalidOrder, # {"errorMessage":"No open orders found","errorCode":"no_open_orders"} + 'active_position_not_found': InvalidOrder, # {"errorCode":"active_position_not_found","errorMessage":"Active position not found"} + 'position_inactive': InvalidOrder, # {"errorCode":"position_inactive","errorMessage":"Position is already inactive"} + 'invalid_position_id': InvalidOrder, # {"errorCode":"invalid_position_id","errorMessage":"Position id is invalid"} + 'Internal server error': ExchangeError, # {"msg":"Internal server error","code":"internal_server_error"} + }, + 'broad': { + 'Bad Request': BadRequest, # {"errorMessage":"Bad Request","data":[{"param":"symbol","message":"\"symbol\" must be one of [ETH_USDC, BTC_USDC, BNB_USDC, SOL_USDC, DOGE_USDC, TON_USDC, AVAX_USDC, WIF_USDC, KPEPE_USDC, KSHIB_USDC, KBONK_USDC, MOODENG_USDC, POPCAT_USDC, MOTHER_USDC]"}]} + }, + }, + 'precisionMode': TICK_SIZE, + }) + + def fetch_status(self, params={}): + """ + the latest known information on the availability of the exchange API + + https://api-docs.defx.com/#4b03bb3b-a0fa-4dfb-b96c-237bde0ce9e6 + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `status structure ` + """ + response = self.v1PublicGetHealthcheckPing(params) + # + # { + # "success": True, + # "t": 1709705048323, + # "v": "0.0.7", + # "msg": "A programmer’s wife tells him, “While you’re at the grocery store, buy some eggs.” He never comes back." + # } + # + status = None + success = self.safe_bool(response, 'success') + if success: + status = 'ok' + else: + status = 'error' + return { + 'status': status, + 'updated': None, + 'eta': None, + 'url': None, + 'info': response, + } + + def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://api-docs.defx.com/#4b03bb3b-a0fa-4dfb-b96c-237bde0ce9e6 + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = self.v1PublicGetHealthcheckPing(params) + # + # { + # "success": True, + # "t": 1709705048323, + # "v": "0.0.7", + # "msg": "A programmer’s wife tells him, “While you’re at the grocery store, buy some eggs.” He never comes back." + # } + # + return self.safe_integer(response, 't') + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for defx + + https://api-docs.defx.com/#73cce0c8-f842-4891-9145-01bb6d61324d + https://api-docs.defx.com/#24fd4e5b-840e-451e-99e0-7fea47c7f371 + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + request = { + 'type': 'perps', + } + promises = [ + self.v1PublicGetCMarkets(self.extend(request, params)), + self.v1PublicGetCMarketsMetadata(self.extend(request, params)), + ] + responses = promises + # + # { + # "data": [ + # { + # "market": "DOGE_USDC", + # "candleWindows": [ + # "1m", + # "3m", + # "5m", + # "15m", + # "30m", + # "1h", + # "2h", + # "4h", + # "12h", + # "1d", + # "1w", + # "1M" + # ], + # "depthSlabs": [ + # "0.00001", + # "0.00005", + # "0.0001", + # "0.001", + # "0.01" + # ], + # "filters": [ + # { + # "filterType": "LOT_SIZE", + # "minQty": "1.00000", + # "maxQty": "1500000.00000", + # "stepSize": "1.00000" + # }, + # { + # "filterType": "MARKET_LOT_SIZE", + # "minQty": "1.00000", + # "maxQty": "750000.00000", + # "stepSize": "1.00000" + # }, + # { + # "filterType": "PRICE_FILTER", + # "minPrice": "0.00244000", + # "maxPrice": "30.00000000", + # "tickSize": "0.00001" + # }, + # { + # "filterType": "NOTIONAL", + # "minNotional": "100.00000000" + # }, + # { + # "filterType": "PERCENT_PRICE_BY_SIDE", + # "bidMultiplierUp": "1.5", + # "bidMultiplierDown": "0.5", + # "askMultiplierUp": "1.5", + # "askMultiplierDown": "0.5" + # }, + # { + # "filterType": "INDEX_PRICE_FILTER", + # "multiplierUp": "1.3", + # "multiplierDown": "0.7" + # } + # ], + # "cappedLeverage": "25", + # "maintenanceMarginTiers": [ + # { + # "tier": "1", + # "minMaintenanceMargin": "0", + # "maxMaintenanceMargin": "2500", + # "leverage": "25" + # }, + # { + # "tier": "2", + # "minMaintenanceMargin": "2500", + # "maxMaintenanceMargin": "12500", + # "leverage": "20" + # }, + # { + # "tier": "3", + # "minMaintenanceMargin": "12500", + # "maxMaintenanceMargin": "25000", + # "leverage": "15" + # }, + # { + # "tier": "4", + # "minMaintenanceMargin": "25000", + # "maxMaintenanceMargin": "50000", + # "leverage": "10" + # }, + # { + # "tier": "5", + # "minMaintenanceMargin": "50000", + # "maxMaintenanceMargin": "75000", + # "leverage": "8" + # }, + # { + # "tier": "6", + # "minMaintenanceMargin": "75000", + # "maxMaintenanceMargin": "125000", + # "leverage": "7" + # }, + # { + # "tier": "7", + # "minMaintenanceMargin": "125000", + # "maxMaintenanceMargin": "187500", + # "leverage": "5" + # }, + # { + # "tier": "8", + # "minMaintenanceMargin": "187500", + # "maxMaintenanceMargin": "250000", + # "leverage": "3" + # }, + # { + # "tier": "9", + # "minMaintenanceMargin": "250000", + # "maxMaintenanceMargin": "375000", + # "leverage": "2" + # }, + # { + # "tier": "10", + # "minMaintenanceMargin": "375000", + # "maxMaintenanceMargin": "500000", + # "leverage": "1" + # } + # ], + # "fees": { + # "maker": "0.08", + # "taker": "0.1" + # } + # }, + # ] + # } + # + activeMarkets = self.safe_list(responses[0], 'data') + activeMarketsByType = self.index_by(activeMarkets, 'market') + marketMetadatas = self.safe_list(responses[1], 'data') + for i in range(0, len(marketMetadatas)): + marketId = marketMetadatas[i]['market'] + status = None + if marketId in activeMarketsByType: + status = activeMarketsByType[marketId]['status'] + marketMetadatas[i]['status'] = status + return self.parse_markets(marketMetadatas) + + def parse_market(self, market: dict) -> Market: + marketId = self.safe_string(market, 'market') + parts = marketId.split('_') + baseId = self.safe_string(parts, 0) + quoteId = self.safe_string(parts, 1) + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + symbol = base + '/' + quote + ':' + quote + filters = self.safe_list(market, 'filters', []) + fees = self.safe_dict(market, 'fees', {}) + filtersByType = self.index_by(filters, 'filterType') + priceFilter = self.safe_dict(filtersByType, 'PRICE_FILTER', {}) + lotFilter = self.safe_dict(filtersByType, 'LOT_SIZE', {}) + marketLotFilter = self.safe_dict(filtersByType, 'MARKET_LOT_SIZE', {}) + notionalFilter = self.safe_dict(filtersByType, 'NOTIONAL', {}) + return { + 'id': marketId, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': quote, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': quoteId, + 'type': 'swap', + 'spot': False, + 'margin': False, + 'swap': True, + 'future': False, + 'option': False, + 'active': self.safe_string(market, 'status', '') == 'active', + 'contract': True, + 'linear': True, + 'inverse': False, + 'taker': self.safe_number(fees, 'taker'), + 'maker': self.safe_number(fees, 'maker'), + 'contractSize': self.parse_number('1'), + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.safe_number(lotFilter, 'stepSize'), + 'price': self.safe_number(priceFilter, 'tickSize'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': self.safe_number(market, 'cappedLeverage'), + }, + 'amount': { + 'min': self.safe_number(lotFilter, 'minQty'), + 'max': self.safe_number(lotFilter, 'maxQty'), + }, + 'price': { + 'min': self.safe_number(priceFilter, 'minPrice'), + 'max': self.safe_number(priceFilter, 'maxPrice'), + }, + 'cost': { + 'min': self.safe_number(notionalFilter, 'minNotional'), + 'max': None, + }, + 'market': { + 'min': self.safe_number(marketLotFilter, 'minQty'), + 'max': self.safe_number(marketLotFilter, 'maxQty'), + }, + }, + 'created': None, + 'info': market, + } + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://api-docs.defx.com/#fe6f81d0-2f3a-4eee-976f-c8fc8f4c5d56 + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = self.v1PublicGetSymbolsSymbolTicker24hr(self.extend(request, params)) + # + # { + # "symbol": "BTC_USDC", + # "priceChange": "0", + # "priceChangePercent": "0", + # "weightedAvgPrice": "0", + # "lastPrice": "2.00", + # "lastQty": "10.000", + # "bestBidPrice": "1646.00", + # "bestBidQty": "10.000", + # "bestAskPrice": "1646.00", + # "bestAskQty": "10.000", + # "openPrice": "0.00", + # "highPrice": "0.00", + # "lowPrice": "0.00", + # "volume": "0.000", + # "quoteVolume": "0.00", + # "openTime": 1700142658697, + # "closeTime": 1700142658697, + # "openInterestBase": "1.000", + # "openInterestQuote": "0.43112300" + # } + # + return self.parse_ticker(response, market) + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://api-docs.defx.com/#8c61cfbd-40d9-410e-b014-f5b36eba51d1 + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + market = None + if symbols is not None: + symbols = self.market_symbols(symbols) + firstSymbol = self.safe_string(symbols, 0) + if firstSymbol is not None: + market = self.market(firstSymbol) + type = None + type, params = self.handle_market_type_and_params('fetchTickers', market, params) + if type == 'spot': + raise NotSupported(self.id + ' fetchTickers() is not supported for ' + type + ' markets') + response = self.v1PublicGetTicker24HrAgg(params) + # + # { + # "ETH_USDC": { + # "priceChange": "0", + # "priceChangePercent": "0", + # "openPrice": "1646.15", + # "highPrice": "1646.15", + # "lowPrice": "1646.15", + # "lastPrice": "1646.15", + # "quoteVolume": "13.17", + # "volume": "0.008", + # "markPrice": "1645.15" + # } + # } + # + return self.parse_tickers(response, symbols) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # fetchTicker + # + # { + # "symbol": "BTC_USDC", + # "priceChange": "0", + # "priceChangePercent": "0", + # "weightedAvgPrice": "0", + # "lastPrice": "2.00", + # "lastQty": "10.000", + # "bestBidPrice": "1646.00", + # "bestBidQty": "10.000", + # "bestAskPrice": "1646.00", + # "bestAskQty": "10.000", + # "openPrice": "0.00", + # "highPrice": "0.00", + # "lowPrice": "0.00", + # "volume": "0.000", + # "quoteVolume": "0.00", + # "openTime": 1700142658697, + # "closeTime": 1700142658697, + # "openInterestBase": "1.000", + # "openInterestQuote": "0.43112300" + # } + # + # fetchTickers + # + # "ETH_USDC": { + # "priceChange": "0", + # "priceChangePercent": "0", + # "openPrice": "1646.15", + # "highPrice": "1646.15", + # "lowPrice": "1646.15", + # "lastPrice": "1646.15", + # "quoteVolume": "13.17", + # "volume": "0.008", + # "markPrice": "1645.15" + # } + # + # fetchMarkPrice + # + # { + # "markPrice": "100.00", + # "indexPrice": "100.00", + # "ltp": "101.34", + # "movingFundingRate": "0.08", + # "payoutFundingRate": "-0.03", + # "nextFundingPayout": 1711555532146 + # } + # + marketId = self.safe_string(ticker, 'symbol') + if marketId is not None: + market = self.market(marketId) + symbol = market['symbol'] + open = self.safe_string(ticker, 'openPrice') + high = self.safe_string(ticker, 'highPrice') + low = self.safe_string(ticker, 'lowPrice') + close = self.safe_string(ticker, 'lastPrice') + quoteVolume = self.safe_string(ticker, 'quoteVolume') + baseVolume = self.safe_string(ticker, 'volume') + percentage = self.safe_string(ticker, 'priceChangePercent') + change = self.safe_string(ticker, 'priceChange') + ts = self.safe_integer(ticker, 'closeTime') + if ts == 0: + ts = None + datetime = self.iso8601(ts) + bid = self.safe_string(ticker, 'bestBidPrice') + bidVolume = self.safe_string(ticker, 'bestBidQty') + ask = self.safe_string(ticker, 'bestAskPrice') + askVolume = self.safe_string(ticker, 'bestAskQty') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': ts, + 'datetime': datetime, + 'high': high, + 'low': low, + 'bid': bid, + 'bidVolume': bidVolume, + 'ask': ask, + 'askVolume': askVolume, + 'vwap': None, + 'open': open, + 'close': close, + 'last': None, + 'previousClose': None, + 'change': change, + 'percentage': percentage, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'markPrice': self.safe_string(ticker, 'markPrice'), + 'indexPrice': self.safe_string(ticker, 'indexPrice'), + 'info': ticker, + }, market) + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + + https://api-docs.defx.com/#54b71951-1472-4670-b5af-4c2dc41e73d0 + + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: max=1000, max=100 when since is defined and is less than(now - (999 * (timeframe in ms))) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch orders for + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + market = self.market(symbol) + maxLimit = 1000 + if limit is None: + limit = maxLimit + limit = min(maxLimit, limit) + request: dict = { + 'symbol': market['id'], + 'interval': self.safe_string(self.timeframes, timeframe, timeframe), + 'limit': limit, + } + until = self.safe_integer_2(params, 'until', 'till') + params = self.omit(params, ['until', 'till']) + request['endTime'] = self.milliseconds() if (until is None) else until + if since is None: + request['startTime'] = 0 + else: + request['startTime'] = since + if until is None: + timeframeInSeconds = self.parse_timeframe(timeframe) + timeframeInMilliseconds = timeframeInSeconds * 1000 + totalTimeframeInMilliseconds = limit * timeframeInMilliseconds + request['endTime'] = self.sum(since, totalTimeframeInMilliseconds) + response = self.v1PublicGetSymbolsSymbolOhlc(self.extend(request, params)) + # + # [ + # { + # "symbol": "BTC_USDC", + # "open": "0.00", + # "high": "0.00", + # "low": "0.00", + # "close": "0.00", + # "volume": "0.000", + # "quoteAssetVolume": "0.00", + # "takerBuyAssetVolume": "0.000", + # "takerBuyQuoteAssetVolume": "0.00", + # "numberOfTrades": 0, + # "start": 1702453663894, + # "end": 1702453663894, + # "isClosed": True + # } + # ] + # + return self.parse_ohlcvs(response, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # example response in fetchOHLCV + return [ + self.safe_integer(ohlcv, 'start'), + self.safe_number(ohlcv, 'open'), + self.safe_number(ohlcv, 'high'), + self.safe_number(ohlcv, 'low'), + self.safe_number(ohlcv, 'close'), + self.safe_number(ohlcv, 'volume'), + ] + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://api-docs.defx.com/#5865452f-ea32-4f13-bfbc-03af5f5574fd + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + maxLimit = 50 + if limit is None: + limit = maxLimit + limit = min(maxLimit, limit) + request: dict = { + 'symbol': market['id'], + 'limit': limit, + } + response = self.v1PublicGetSymbolsSymbolTrades(self.extend(request, params)) + # + # [ + # { + # "buyerMaker": "false", + # "price": "2.0000", + # "qty": "10.0000", + # "symbol": "BTC_USDC", + # "timestamp": "1702453663894" + # } + # ] + # + return self.parse_trades(response, market, since, limit) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + fetch all trades made by the user + + https://api-docs.defx.com/#06b5b33c-2fc6-48de-896c-fc316f5871a7 + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['symbols'] = market['id'] + if limit is not None: + maxLimit = 100 + limit = min(maxLimit, limit) + request['pageSize'] = limit + response = self.v1PrivateGetApiTrades(self.extend(request, params)) + # + # { + # "data": [ + # { + # "id": "0192f665-c05b-7ba0-a080-8b6c99083489", + # "orderId": "757730811259651728", + # "time": "2024-11-04T08:58:36.474Z", + # "symbol": "SOL_USDC", + # "side": "SELL", + # "price": "160.43600000", + # "qty": "1.00", + # "fee": "0.08823980", + # "role": "TAKER", + # "pnl": "0.00000000" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_trades(data, None, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades + # { + # "buyerMaker": "false", + # "price": "2.0000", + # "qty": "10.0000", + # "symbol": "BTC_USDC", + # "timestamp": "1702453663894" + # } + # + # fetchMyTrades + # { + # "id": "0192f665-c05b-7ba0-a080-8b6c99083489", + # "orderId": "757730811259651728", + # "time": "2024-11-04T08:58:36.474Z", + # "symbol": "SOL_USDC", + # "side": "SELL", + # "price": "160.43600000", + # "qty": "1.00", + # "fee": "0.08823980", + # "role": "TAKER", + # "pnl": "0.00000000" + # } + # + time = self.safe_string(trade, 'time') + timestamp = self.safe_integer(trade, 'timestamp', self.parse8601(time)) + marketId = self.safe_string(trade, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + price = self.safe_string(trade, 'price') + amount = self.safe_string(trade, 'qty') + id = self.safe_string(trade, 'id') + oid = self.safe_string(trade, 'orderId') + takerOrMaker = self.safe_string_lower(trade, 'role') + buyerMaker = self.safe_bool(trade, 'buyerMaker') + side = self.safe_string_lower(trade, 'side') + if buyerMaker is not None: + if buyerMaker: + side = 'sell' + else: + side = 'buy' + return self.safe_trade({ + 'id': id, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'side': side, + 'price': price, + 'amount': amount, + 'cost': None, + 'order': oid, + 'takerOrMaker': takerOrMaker, + 'type': None, + 'fee': { + 'cost': self.safe_string(trade, 'fee'), + 'currency': 'USDC', + }, + 'info': trade, + }, market) + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://api-docs.defx.com/#6c1a2971-8325-4e7d-9962-e0bfcaacf9c4 + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.slab]: slab from market.info.depthSlabs + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + if limit is None: + limit = 10 # limit must be one of [5, 10, 20] + marketInfo = self.safe_dict(market, 'info', {}) + slab = self.safe_list(marketInfo, 'depthSlabs', []) + request: dict = { + 'symbol': market['id'], + 'level': limit, + 'slab': self.safe_string(slab, 0), + } + response = self.v1PublicGetSymbolsSymbolDepthLevelSlab(self.extend(request, params)) + # + # { + # "symbol": "ETH_USDC", + # "level": "5", + # "slab": "1", + # "lastTradeTimestamp": "1708313446812", + # "timestamp": "1708313446812", + # "bids": [ + # { + # "price": "1646.16", + # "qty": "0.001" + # } + # ], + # "asks": [ + # { + # "price": "1646.16", + # "qty": "0.001" + # } + # ] + # } + # + timestamp = self.safe_integer(response, 'timestamp') + return self.parse_order_book(response, symbol, timestamp, 'bids', 'asks', 'price', 'qty') + + def fetch_mark_price(self, symbol: str, params={}) -> Ticker: + """ + fetches mark price for the market + + https://api-docs.defx.com/#12168192-4e7b-4458-a001-e8b80961f0b7 + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.subType]: "linear" or "inverse" + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + market = self.market(symbol) + request = { + 'symbol': market['id'], + } + response = self.v1PublicGetSymbolsSymbolPrices(self.extend(request, params)) + # + # { + # "markPrice": "100.00", + # "indexPrice": "100.00", + # "ltp": "101.34", + # "movingFundingRate": "0.08", + # "payoutFundingRate": "-0.03", + # "nextFundingPayout": 1711555532146 + # } + # + return self.parse_ticker(response, market) + + def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate + + https://api-docs.defx.com/#12168192-4e7b-4458-a001-e8b80961f0b7 + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + self.load_markets() + market = self.market(symbol) + request = { + 'symbol': market['id'], + } + response = self.v1PublicGetSymbolsSymbolPrices(self.extend(request, params)) + # + # { + # "markPrice": "100.00", + # "indexPrice": "100.00", + # "ltp": "101.34", + # "movingFundingRate": "0.08", + # "payoutFundingRate": "-0.03", + # "nextFundingPayout": 1711555532146 + # } + # + return self.parse_funding_rate(response, market) + + def parse_funding_rate(self, contract, market: Market = None) -> FundingRate: + # + # { + # "markPrice": "100.00", + # "indexPrice": "100.00", + # "ltp": "101.34", + # "movingFundingRate": "0.08", + # "payoutFundingRate": "-0.03", + # "nextFundingPayout": 1711555532146 + # } + # + markPrice = self.safe_number(contract, 'markPrice') + indexPrice = self.safe_number(contract, 'indexPrice') + fundingRateRaw = self.safe_string(contract, 'payoutFundingRate') + fundingRate = Precise.string_div(fundingRateRaw, '100') + fundingTime = self.safe_integer(contract, 'nextFundingPayout') + return { + 'info': contract, + 'symbol': market['symbol'], + 'markPrice': markPrice, + 'indexPrice': indexPrice, + 'interestRate': None, + 'estimatedSettlePrice': None, + 'timestamp': None, + 'datetime': None, + 'fundingRate': self.parse_number(fundingRate), + 'fundingTimestamp': fundingTime, + 'fundingDatetime': self.iso8601(fundingTime), + 'nextFundingRate': None, + 'nextFundingTimestamp': None, + 'nextFundingDatetime': None, + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': None, + } + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://api-docs.defx.com/#26414338-14f7-40a1-b246-f8ea8571493f + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + response = self.v1PrivateGetApiWalletBalance(params) + # + # { + # "assets": [ + # { + # "asset": "USDC", + # "balance": "0.000" + # } + # ] + # } + # + data = self.safe_list(response, 'assets') + return self.parse_balance(data) + + def parse_balance(self, balances) -> Balances: + result: dict = { + 'info': balances, + } + for i in range(0, len(balances)): + balance = balances[i] + code = self.safe_currency_code(self.safe_string(balance, 'asset')) + account = self.account() + account['total'] = self.safe_string(balance, 'balance') + result[code] = account + return self.safe_balance(result) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://api-docs.defx.com/#ba222d88-8856-4d3c-87a9-7cec07bb2622 + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: The price a trigger order is triggered at + :param str [params.reduceOnly]: for swap and future reduceOnly is a string 'true' or 'false' that cant be sent with close position set to True or in hedge mode. For spot margin and option reduceOnly is a boolean. + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + reduceOnly = self.safe_bool_2(params, 'reduceOnly', 'reduce_only') + params = self.omit(params, ['reduceOnly', 'reduce_only']) + orderType = type.upper() + orderSide = side.upper() + request: dict = { + 'symbol': market['id'], + 'side': orderSide, + 'type': orderType, + } + takeProfitPrice = self.safe_string(params, 'takeProfitPrice') + triggerPrice = self.safe_string_2(params, 'stopPrice', 'triggerPrice') + isMarket = orderType == 'MARKET' + isLimit = orderType == 'LIMIT' + timeInForce = self.safe_string_upper(params, 'timeInForce') + if timeInForce is not None: + # GTC, IOC, FOK, AON + request['timeInForce'] = timeInForce + else: + if isLimit: + request['timeInForce'] = 'GTC' + if reduceOnly: + request['reduceOnly'] = reduceOnly + clientOrderId = self.safe_string(params, 'clientOrderId') + if clientOrderId is not None: + request['newClientOrderId'] = clientOrderId + if triggerPrice is not None or takeProfitPrice is not None: + request['workingType'] = 'MARK_PRICE' + if takeProfitPrice is not None: + request['stopPrice'] = self.price_to_precision(symbol, takeProfitPrice) + if isMarket: + request['type'] = 'TAKE_PROFIT_MARKET' + else: + request['type'] = 'TAKE_PROFIT_LIMIT' + else: + request['stopPrice'] = self.price_to_precision(symbol, triggerPrice) + if isMarket: + request['type'] = 'STOP_MARKET' + else: + request['type'] = 'STOP_LIMIT' + if isLimit and price is not None: + request['price'] = self.price_to_precision(symbol, price) + request['quantity'] = self.amount_to_precision(symbol, amount) + params = self.omit(params, ['clOrdID', 'clientOrderId', 'client_order_id', 'postOnly', 'timeInForce', 'stopPrice', 'triggerPrice', 'takeProfitPrice']) + response = self.v1PrivatePostApiOrder(self.extend(request, params)) + # + # { + # "success": True, + # "data": { + # "orderId": "", + # "clientOrderId": "", + # "cumulativeQty": "", + # "cumulativeQuote": "", + # "executedQty": "", + # "avgPrice": "", + # "origQty": "", + # "price": "", + # "reduceOnly": True, + # "side": "", + # "status": "", + # "symbol": "", + # "timeInForce": "", + # "type": "", + # "workingType": "" + # } + # } + # + data = self.safe_dict(response, 'data') + return self.parse_order(data, market) + + def parse_order_status(self, status: Str): + if status is not None: + statuses: dict = { + 'NEW': 'open', + 'OPEN': 'open', + 'CANCELLED': 'canceled', + 'REJECTED': 'rejected', + 'FILLED': 'closed', + } + return self.safe_string(statuses, status, status) + return status + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # { + # "orderId": "746472647227344528", + # "createdAt": "2024-10-25T16:49:31.077Z", + # "updatedAt": "2024-10-25T16:49:31.378Z", + # "clientOrderId": "0192c495-49c3-71ee-b3d3-7442a2090807", + # "reduceOnly": False, + # "side": "SELL", + # "status": "FILLED", + # "symbol": "SOL_USDC", + # "timeInForce": "GTC", + # "type": "MARKET", + # "origQty": "0.80", + # "executedQty": "0.80", + # "cumulativeQuote": "137.87440000", + # "avgPrice": "172.34300000", + # "totalPnL": "0.00000000", + # "totalFee": "0.07583092", + # "workingType": null, + # "postOnly": False, + # "linkedOrderParentType": null, + # "isTriggered": False, + # "slippagePercentage": "5" + # } + # + orderId = self.safe_string(order, 'orderId') + clientOrderId = self.safe_string(order, 'clientOrderId') + marketId = self.safe_string(order, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + price = self.safe_string(order, 'price') + amount = self.safe_string(order, 'origQty') + orderType = self.safe_string_lower(order, 'type') + status = self.safe_string(order, 'status') + side = self.safe_string_lower(order, 'side') + filled = self.omit_zero(self.safe_string(order, 'executedQty')) + average = self.omit_zero(self.safe_string(order, 'avgPrice')) + timeInForce = self.safe_string_lower(order, 'timeInForce') + takeProfitPrice: Str = None + triggerPrice: Str = None + if orderType is not None: + if orderType.find('take_profit') >= 0: + takeProfitPrice = self.safe_string(order, 'stopPrice') + else: + triggerPrice = self.safe_string(order, 'stopPrice') + timestamp = self.parse8601(self.safe_string(order, 'createdAt')) + lastTradeTimestamp = self.parse8601(self.safe_string(order, 'updatedAt')) + return self.safe_order({ + 'id': orderId, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': lastTradeTimestamp, + 'lastUpdateTimestamp': lastTradeTimestamp, + 'status': self.parse_order_status(status), + 'symbol': symbol, + 'type': orderType, + 'timeInForce': timeInForce, + 'postOnly': self.safe_bool(order, 'postOnly'), + 'reduceOnly': self.safe_bool(order, 'reduceOnly'), + 'side': side, + 'price': price, + 'triggerPrice': triggerPrice, + 'takeProfitPrice': takeProfitPrice, + 'stopLossPrice': None, + 'average': average, + 'amount': amount, + 'filled': filled, + 'remaining': None, + 'cost': None, + 'trades': None, + 'fee': { + 'cost': self.safe_string(order, 'totalFee'), + 'currency': 'USDC', + }, + 'info': order, + }, market) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + + https://api-docs.defx.com/#09186f23-f8d1-4993-acf4-9974d8a6ddb0 + + cancels an open order + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + request: dict = { + 'orderId': id, + 'idType': 'orderId', + } + clientOrderId = self.safe_string_n(params, ['clOrdID', 'clientOrderId', 'client_order_id']) + isByClientOrder = clientOrderId is not None + if isByClientOrder: + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + market = self.market(symbol) + request['orderId'] = clientOrderId + request['idType'] = 'clientOrderId' + request['symbol'] = market['id'] + params = self.omit(params, ['clOrdID', 'clientOrderId', 'client_order_id']) + response = self.v1PrivateDeleteApiOrderOrderId(self.extend(request, params)) + # + # { + # "success": True + # } + # + extendParams: dict = {'symbol': symbol} + if isByClientOrder: + extendParams['clientOrderId'] = clientOrderId + else: + extendParams['id'] = id + return self.extend(self.parse_order(response), extendParams) + + def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders + + https://api-docs.defx.com/#db5531da-3692-4a53-841f-6ad6495f823a + + :param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbols': [market['id']], + } + response = self.v1PrivateDeleteApiOrdersAllOpen(self.extend(request, params)) + # + # { + # "data": { + # "msg": "The operation of cancel all open order is done." + # } + # } + # + return [self.safe_order({'info': response})] + + def fetch_position(self, symbol: str, params={}): + """ + fetch data on a single open contract trade position + + https://api-docs.defx.com/#d89dbb86-9aba-4f59-ac5d-a97ff25ea80e + + :param str symbol: unified market symbol of the market the position is held in, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `position structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchPosition() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = self.v1PrivateGetApiPositionActive(self.extend(request, params)) + # + # { + # "data": [ + # { + # "positionId": "0192c495-4a68-70ee-9081-9d368bd16dfc", + # "symbol": "SOL_USDC", + # "positionSide": "SHORT", + # "entryPrice": "172.34300000", + # "quantity": "0.80", + # "marginAmount": "20.11561173", + # "marginAsset": "USDC", + # "pnl": "0.00000000" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + first = self.safe_dict(data, 0, {}) + return self.parse_position(first, market) + + def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://api-docs.defx.com/#d89dbb86-9aba-4f59-ac5d-a97ff25ea80e + + :param str[] [symbols]: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + self.load_markets() + response = self.v1PrivateGetApiPositionActive(params) + # + # { + # "data": [ + # { + # "positionId": "0192c495-4a68-70ee-9081-9d368bd16dfc", + # "symbol": "SOL_USDC", + # "positionSide": "SHORT", + # "entryPrice": "172.34300000", + # "quantity": "0.80", + # "marginAmount": "20.11561173", + # "marginAsset": "USDC", + # "pnl": "0.00000000" + # } + # ] + # } + # + positions = self.safe_list(response, 'data', []) + return self.parse_positions(positions, symbols) + + def parse_position(self, position: dict, market: Market = None): + # + # { + # "positionId": "0192c495-4a68-70ee-9081-9d368bd16dfc", + # "symbol": "SOL_USDC", + # "positionSide": "SHORT", + # "entryPrice": "172.34300000", + # "quantity": "0.80", + # "marginAmount": "20.11561173", + # "marginAsset": "USDC", + # "pnl": "0.00000000" + # } + # + marketId = self.safe_string(position, 'symbol') + market = self.safe_market(marketId, market) + size = Precise.string_abs(self.safe_string(position, 'quantity')) + side = self.safe_string_lower(position, 'positionSide') + unrealisedPnl = self.omit_zero(self.safe_string(position, 'pnl')) + entryPrice = self.omit_zero(self.safe_string(position, 'entryPrice')) + initialMargin = self.safe_string(position, 'marginAmount') + return self.safe_position({ + 'info': position, + 'id': self.safe_string(position, 'positionId'), + 'symbol': market['symbol'], + 'timestamp': None, + 'datetime': None, + 'lastUpdateTimestamp': None, + 'initialMargin': self.parse_number(initialMargin), + 'initialMarginPercentage': None, + 'maintenanceMargin': None, + 'maintenanceMarginPercentage': None, + 'entryPrice': self.parse_number(entryPrice), + 'notional': None, + 'leverage': None, + 'unrealizedPnl': self.parse_number(unrealisedPnl), + 'realizedPnl': None, + 'contracts': self.parse_number(size), + 'contractSize': self.safe_number(market, 'contractSize'), + 'marginRatio': None, + 'liquidationPrice': None, + 'markPrice': None, + 'lastPrice': None, + 'collateral': None, + 'marginMode': None, + 'side': side, + 'percentage': None, + 'stopLossPrice': None, + 'takeProfitPrice': None, + 'hedged': None, + }) + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://api-docs.defx.com/#44f82dd5-26b3-4e1f-b4aa-88ceddd65237 + + :param str id: the order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + request: dict = { + 'orderId': id, + 'idType': 'orderId', + } + clientOrderId = self.safe_string_n(params, ['clOrdID', 'clientOrderId', 'client_order_id']) + params = self.omit(params, ['clOrdID', 'clientOrderId', 'client_order_id']) + if clientOrderId is not None: + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrder() requires a symbol argument') + market = self.market(symbol) + request['orderId'] = clientOrderId + request['idType'] = 'clientOrderId' + request['symbol'] = market['id'] + response = self.v1PrivateGetApiOrderOrderId(self.extend(request, params)) + # + # { + # "success": True, + # "data": { + # "orderId": "555068654076559792", + # "createdAt": "2024-05-08T05:45:42.148Z", + # "updatedAt": "2024-05-08T05:45:42.166Z", + # "clientOrderId": "dummyClientOrderId", + # "reduceOnly": False, + # "side": "SELL", + # "status": "REJECTED", + # "symbol": "BTC_USDC", + # "timeInForce": "GTC", + # "type": "TAKE_PROFIT_MARKET", + # "origQty": "1.000", + # "executedQty": "0.000", + # "cumulativeQuote": "0.00", + # "avgPrice": "0.00", + # "stopPrice": "65000.00", + # "totalPnL": "0.00", + # "workingType": "MARK_PRICE", + # "postOnly": False + # } + # } + # + data = self.safe_dict(response, 'data') + return self.parse_order(data) + + def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://api-docs.defx.com/#ab200038-8acb-4170-b05e-4fcb4cc13751 + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch orders for + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['symbols'] = market['id'] + until = self.safe_integer(params, 'until') + if until is not None: + params = self.omit(params, 'until') + request['end'] = self.iso8601(until) + if since is not None: + request['start'] = self.iso8601(since) + if limit is not None: + maxLimit = 100 + limit = min(maxLimit, limit) + request['pageSize'] = limit + response = self.v1PrivateGetApiOrders(self.extend(request, params)) + # + # { + # "data": [ + # { + # "orderId": "746472647227344528", + # "createdAt": "2024-10-25T16:49:31.077Z", + # "updatedAt": "2024-10-25T16:49:31.378Z", + # "clientOrderId": "0192c495-49c3-71ee-b3d3-7442a2090807", + # "reduceOnly": False, + # "side": "SELL", + # "status": "FILLED", + # "symbol": "SOL_USDC", + # "timeInForce": "GTC", + # "type": "MARKET", + # "origQty": "0.80", + # "executedQty": "0.80", + # "cumulativeQuote": "137.87440000", + # "avgPrice": "172.34300000", + # "totalPnL": "0.00000000", + # "totalFee": "0.07583092", + # "workingType": null, + # "postOnly": False, + # "linkedOrderParentType": null, + # "isTriggered": False, + # "slippagePercentage": 5 + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_orders(data, None, since, limit) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://api-docs.defx.com/#ab200038-8acb-4170-b05e-4fcb4cc13751 + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch orders for + :returns Order[]: a list of `order structures ` + """ + req = { + 'statuses': 'OPEN', + } + return self.fetch_orders(symbol, since, limit, self.extend(req, params)) + + def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://api-docs.defx.com/#ab200038-8acb-4170-b05e-4fcb4cc13751 + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch orders for + :returns Order[]: a list of `order structures ` + """ + req = { + 'statuses': 'FILLED', + } + return self.fetch_orders(symbol, since, limit, self.extend(req, params)) + + def fetch_canceled_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple canceled orders made by the user + + https://api-docs.defx.com/#ab200038-8acb-4170-b05e-4fcb4cc13751 + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch orders for + :returns Order[]: a list of `order structures ` + """ + req = { + 'statuses': 'CANCELED', + } + return self.fetch_orders(symbol, since, limit, self.extend(req, params)) + + def close_position(self, symbol: str, side: OrderSide = None, params={}) -> Order: + """ + closes an open position for a market + + https://api-docs.defx.com/#b2c08074-c4d9-4e50-b637-0d6c498fa29e + + :param str symbol: unified CCXT market symbol + :param str [side]: one-way mode: 'buy' or 'sell', hedge-mode: 'long' or 'short' + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.positionId]: the position id you want to close + :param str [params.type]: 'MARKET' or 'LIMIT' + :param str [params.quantity]: how much of currency you want to trade in units of base currency + :param str [params.price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :returns dict: An `order structure ` + """ + self.load_markets() + positionId = self.safe_string(params, 'positionId') + if positionId is None: + raise ArgumentsRequired(self.id + ' closePosition() requires a positionId') + type = self.safe_string_upper(params, 'type') + if type is None: + raise ArgumentsRequired(self.id + ' closePosition() requires a type') + quantity = self.safe_string(params, 'quantity') + if quantity is None: + raise ArgumentsRequired(self.id + ' closePosition() requires a quantity') + request: dict = { + 'positionId': positionId, + 'type': type, + 'quantity': quantity, + } + if type != 'MARKET': + price = self.safe_string(params, 'price') + if price is None: + raise ArgumentsRequired(self.id + ' closePosition() requires a price') + request['price'] = price + params = self.omit(params, ['positionId', 'type', 'quantity', 'price']) + response = self.v1PrivateDeleteApiPositionPositionId(self.extend(request, params)) + # + # {} + # + return response + + def close_all_positions(self, params={}) -> List[Position]: + """ + closes all open positions for a market type + + https://api-docs.defx.com/#d6f63b43-100e-47a9-998c-8b6c0c72d204 + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: A list of `position structures ` + """ + self.load_markets() + response = self.v1PrivateDeleteApiPositionAll(params) + # + # { + # "data": [ + # { + # "positionId": "d6ca1a27-28ad-47ae-b244-0bda5ac37b2b", + # "success": True + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_positions(data, None, params) + + def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + + https://api-docs.defx.com/#38cc8974-794f-48c0-b959-db045a0ee565 + + :param str [code]: unified currency code + :param int [since]: timestamp in ms of the earliest ledger entry + :param int [limit]: max number of ledger entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest ledger entry + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict: a `ledger structure ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchLedger', 'paginate') + if paginate: + return self.fetch_paginated_call_dynamic('fetchLedger', code, since, limit, params) + request: dict = {} + if since is not None: + request['start'] = since + else: + request['start'] = 0 + until = self.safe_integer(params, 'until') + if until is not None: + params = self.omit(params, 'until') + request['end'] = until + else: + request['end'] = self.milliseconds() + response = self.v1PrivateGetApiWalletTransactions(self.extend(request, params)) + data = self.safe_list(response, 'transactions', []) + return self.parse_ledger(data, None, since, limit) + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + # + # { + # "id": "01JCSZS6H5VQND3GF5P98SJ29C", + # "timestamp": 1731744012054, + # "type": "FundingFee", + # "amount": "0.02189287", + # "asset": "USDC", + # "operation": "CREDIT" + # } + # + amount = self.safe_string(item, 'amount') + currencyId = self.safe_string(item, 'asset') + code = self.safe_currency_code(currencyId, currency) + currency = self.safe_currency(currencyId, currency) + timestamp = self.safe_integer(item, 'timestamp') + type = self.safe_string(item, 'type') + return self.safe_ledger_entry({ + 'info': item, + 'id': self.safe_string(item, 'id'), + 'direction': None, + 'account': None, + 'referenceAccount': None, + 'referenceId': None, + 'type': self.parse_ledger_entry_type(type), + 'currency': code, + 'amount': self.parse_number(amount), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'before': None, + 'after': None, + 'status': None, + 'fee': None, + }, currency) + + def parse_ledger_entry_type(self, type): + ledgerType: dict = { + 'FundingFee': 'fee', + 'FeeRebate': 'fee', + 'FeeKickback': 'fee', + 'RealizedPnl': 'trade', + 'LiquidationClearance': 'trade', + 'Transfer': 'transfer', + 'ReferralPayout': 'referral', + 'Commission': 'commission', + } + return self.safe_string(ledgerType, type, type) + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://api-docs.defx.com/#2600f503-63ed-4672-b8f6-69ea5f03203b + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'amount': self.currency_to_precision(code, amount), + 'asset': currency['id'], + # 'network': 'ARB_SEPOLIA', + # 'chainId': '421614', + } + response = self.v1PrivatePostApiTransfersBridgeWithdrawal(self.extend(request, params)) + # + # { + # "transactionId": "0x301e5851e5aefa733abfbc8b30817ca3b61601e0ddf1df8c59656fb888b0bc9c" + # } + # + return self.parse_transaction(response, currency) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # withdraw + # + # { + # "transactionId": "0x301e5851e5aefa733abfbc8b30817ca3b61601e0ddf1df8c59656fb888b0bc9c" + # } + # + txid = self.safe_string(transaction, 'transactionId') + return { + 'info': transaction, + 'id': None, + 'txid': txid, + 'timestamp': None, + 'datetime': None, + 'network': None, + 'address': None, + 'addressTo': None, + 'addressFrom': None, + 'tag': None, + 'tagTo': None, + 'tagFrom': None, + 'type': None, + 'amount': None, + 'currency': self.safe_currency_code(None, currency), + 'status': None, + 'updated': None, + 'internal': None, + 'comment': None, + 'fee': None, + } + + def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://api-docs.defx.com/#4cb4ecc4-6c61-4194-8353-be67faaf7ca7 + + :param float leverage: the rate of leverage + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setLeverage() requires a symbol argument') + self.load_markets() + request: dict = { + 'leverage': self.number_to_string(leverage), + } + market = self.market(symbol) + request['symbol'] = market['id'] + response = self.v1PrivatePostApiUsersMetadataLeverage(self.extend(request, params)) + # + # { + # "success": True, + # "data": { + # "leverage": "11", + # "symbol": "BTC_USDC" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_leverage(data, market) + + def parse_leverage(self, leverage: dict, market: Market = None) -> Leverage: + # + # "data": { + # "leverage": "11", + # "symbol": "BTC_USDC" + # } + # + marketId = self.safe_string(leverage, 'symbol') + leverageValue = self.safe_integer(leverage, 'leverage') + return { + 'info': leverage, + 'symbol': self.safe_symbol(marketId, market), + 'marginMode': None, + 'longLeverage': leverageValue, + 'shortLeverage': leverageValue, + } + + def nonce(self): + return self.milliseconds() + + def sign(self, path, section='public', method='GET', params={}, headers=None, body=None): + version = section[0] + access = section[1] + pathWithParams = self.implode_params(path, params) + url = self.implode_hostname(self.urls['api'][access]) + url += '/' + version + '/' + params = self.omit(params, self.extract_params(path)) + params = self.keysort(params) + if access == 'public': + url += 'open/' + pathWithParams + if params: + url += '?' + self.rawencode(params) + else: + self.check_required_credentials() + headers = {'X-DEFX-SOURCE': 'ccxt'} + url += 'auth/' + pathWithParams + nonce = str(self.milliseconds()) + payload = nonce + if method == 'GET' or path == 'api/order/{orderId}': + payload += self.rawencode(params) + if params: + url += '?' + self.rawencode(params) + else: + if params is not None: + body = self.json(params) + payload += body + headers['Content-Type'] = 'application/json' + signature = self.hmac(self.encode(payload), self.encode(self.secret), hashlib.sha256) + headers['X-DEFX-APIKEY'] = self.apiKey + headers['X-DEFX-TIMESTAMP'] = nonce + headers['X-DEFX-SIGNATURE'] = signature + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if not response: + return None # fallback to default error handler + # {"errorCode":404,"errorMessage":"Not Found"} + # {"msg":"Missing auth signature","code":"missing_auth_signature"} + # {"success":false,"err":{"msg":"Invalid order id","code":"invalid_order_id"}} + success = self.safe_bool(response, 'success') + err = self.safe_dict(response, 'err', response) + errorCode = self.safe_string_2(err, 'errorCode', 'code') + if not success: + feedback = self.id + ' ' + self.json(response) + self.throw_broadly_matched_exception(self.exceptions['broad'], body, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + return None + + def default_network_code_for_currency(self, code): + currencyItem = self.currency(code) + networks = currencyItem['networks'] + networkKeys = list(networks.keys()) + for i in range(0, len(networkKeys)): + network = networkKeys[i] + if network == 'ETH': + return network + # if it was not returned according to above options, then return the first network of currency + return self.safe_value(networkKeys, 0) + + def set_sandbox_mode(self, enable: bool): + super(defx, self).set_sandbox_mode(enable) + self.options['sandboxMode'] = enable diff --git a/ccxt/delta.py b/ccxt/delta.py new file mode 100644 index 0000000..49dd94c --- /dev/null +++ b/ccxt/delta.py @@ -0,0 +1,3665 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.delta import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currencies, Currency, DepositAddress, Greeks, Int, LedgerEntry, Leverage, MarginMode, MarginModification, Market, Num, Option, Order, OrderBook, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, FundingRates, Trade, MarketInterface +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class delta(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(delta, self).describe(), { + 'id': 'delta', + 'name': 'Delta Exchange', + 'countries': ['VC'], # Saint Vincent and the Grenadines + 'rateLimit': 300, + 'version': 'v2', + # new metainfo interface + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': True, + 'future': False, + 'option': True, + 'addMargin': True, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'closeAllPositions': True, + 'closePosition': False, + 'createOrder': True, + 'createReduceOnlyOrder': True, + 'editOrder': True, + 'fetchBalance': True, + 'fetchClosedOrders': True, + 'fetchCurrencies': True, + 'fetchDeposit': None, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': None, + 'fetchFundingHistory': False, + 'fetchFundingRate': True, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': True, + 'fetchGreeks': True, + 'fetchIndexOHLCV': True, + 'fetchLedger': True, + 'fetchLeverage': True, + 'fetchLeverageTiers': False, # An infinite number of tiers, see examples/js/delta-maintenance-margin-rate-max-leverage.js + 'fetchMarginMode': True, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': True, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': True, + 'fetchOpenOrders': True, + 'fetchOption': True, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchPosition': True, + 'fetchPositionMode': False, + 'fetchPositions': True, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': True, + 'fetchStatus': True, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTransfer': None, + 'fetchTransfers': None, + 'fetchUnderlyingAssets': False, + 'fetchVolatilityHistory': False, + 'fetchWithdrawal': None, + 'fetchWithdrawals': None, + 'reduceMargin': True, + 'setLeverage': True, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'transfer': False, + 'withdraw': False, + }, + 'timeframes': { + '1m': '1m', + '3m': '3m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1h', + '2h': '2h', + '4h': '4h', + '6h': '6h', + '1d': '1d', + '7d': '7d', + '1w': '1w', + '2w': '2w', + '1M': '30d', + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/99450025-3be60a00-2931-11eb-9302-f4fd8d8589aa.jpg', + 'test': { + 'public': 'https://testnet-api.delta.exchange', + 'private': 'https://testnet-api.delta.exchange', + }, + 'api': { + 'public': 'https://api.delta.exchange', + 'private': 'https://api.delta.exchange', + }, + 'www': 'https://www.delta.exchange', + 'doc': [ + 'https://docs.delta.exchange', + ], + 'fees': 'https://www.delta.exchange/fees', + 'referral': 'https://www.delta.exchange/app/signup/?code=IULYNB', + }, + 'api': { + 'public': { + 'get': [ + 'assets', + 'indices', + 'products', + 'products/{symbol}', + 'tickers', + 'tickers/{symbol}', + 'l2orderbook/{symbol}', + 'trades/{symbol}', + 'stats', + 'history/candles', + 'history/sparklines', + 'settings', + ], + }, + 'private': { + 'get': [ + 'orders', + 'orders/{order_id}', + 'orders/client_order_id/{client_oid}', + 'products/{product_id}/orders/leverage', + 'positions/margined', + 'positions', + 'orders/history', + 'fills', + 'fills/history/download/csv', + 'wallet/balances', + 'wallet/transactions', + 'wallet/transactions/download', + 'wallets/sub_accounts_transfer_history', + 'users/trading_preferences', + 'sub_accounts', + 'profile', + 'heartbeat', + 'deposits/address', + ], + 'post': [ + 'orders', + 'orders/bracket', + 'orders/batch', + 'products/{product_id}/orders/leverage', + 'positions/change_margin', + 'positions/close_all', + 'wallets/sub_account_balance_transfer', + 'heartbeat/create', + 'heartbeat', + 'orders/cancel_after', + 'orders/leverage', + ], + 'put': [ + 'orders', + 'orders/bracket', + 'orders/batch', + 'positions/auto_topup', + 'users/update_mmp', + 'users/reset_mmp', + ], + 'delete': [ + 'orders', + 'orders/all', + 'orders/batch', + ], + }, + }, + 'fees': { + 'trading': { + 'tierBased': True, + 'percentage': True, + 'taker': self.parse_number('0.0015'), + 'maker': self.parse_number('0.0010'), + 'tiers': { + 'taker': [ + [self.parse_number('0'), self.parse_number('0.0015')], + [self.parse_number('100'), self.parse_number('0.0013')], + [self.parse_number('250'), self.parse_number('0.0013')], + [self.parse_number('1000'), self.parse_number('0.001')], + [self.parse_number('5000'), self.parse_number('0.0009')], + [self.parse_number('10000'), self.parse_number('0.00075')], + [self.parse_number('20000'), self.parse_number('0.00065')], + ], + 'maker': [ + [self.parse_number('0'), self.parse_number('0.001')], + [self.parse_number('100'), self.parse_number('0.001')], + [self.parse_number('250'), self.parse_number('0.0009')], + [self.parse_number('1000'), self.parse_number('0.00075')], + [self.parse_number('5000'), self.parse_number('0.0006')], + [self.parse_number('10000'), self.parse_number('0.0005')], + [self.parse_number('20000'), self.parse_number('0.0005')], + ], + }, + }, + }, + 'userAgent': self.userAgents['chrome39'], # needed for C# + 'options': { + 'networks': { + 'TRC20': 'TRC20(TRON)', + 'BEP20': 'BEP20(BSC)', + }, + }, + 'features': { + 'default': { + 'sandbox': True, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, # todo implement + # todo implement + 'triggerPriceType': { + 'last': True, + 'mark': True, + 'index': True, + }, + 'triggerDirection': False, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': { + 'triggerPriceType': None, + 'price': True, + }, + # todo implementation + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'selfTradePrevention': False, + 'trailing': False, # todo: implement + 'iceberg': False, + 'leverage': False, + 'marketBuyByCost': False, + 'marketBuyRequiresPrice': False, + }, + 'createOrders': None, # todo: implement + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, # todo: revise + 'daysBack': 100000, + 'untilDays': 100000, + 'symbolRequired': False, + }, + 'fetchOrder': None, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 100, # todo: revise + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 500, + 'daysBack': 100000, + 'daysBackCanceled': 1, + 'untilDays': 100000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 2000, # todo: recheck + }, + }, + 'spot': { + 'extends': 'default', + }, + 'swap': { + 'linear': { + 'extends': 'default', + }, + 'inverse': { + 'extends': 'default', + }, + }, + 'future': { + 'linear': { + 'extends': 'default', + }, + 'inverse': { + 'extends': 'default', + }, + }, + }, + 'precisionMode': TICK_SIZE, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + }, + 'exceptions': { + 'exact': { + # Margin required to place order with selected leverage and quantity is insufficient. + 'insufficient_margin': InsufficientFunds, # {"error":{"code":"insufficient_margin","context":{"available_balance":"0.000000000000000000","required_additional_balance":"1.618626000000000000000000000"}},"success":false} + 'order_size_exceed_available': InvalidOrder, # The order book doesn't have sufficient liquidity, hence the order couldnt be filled, for example, ioc orders + 'risk_limits_breached': BadRequest, # orders couldn't be placed will breach allowed risk limits. + 'invalid_contract': BadSymbol, # The contract/product is either doesn't exist or has already expired. + 'immediate_liquidation': InvalidOrder, # Order will cause immediate liquidation. + 'out_of_bankruptcy': InvalidOrder, # Order prices are out of position bankruptcy limits. + 'self_matching_disrupted_post_only': InvalidOrder, # Self matching is not allowed during auction. + 'immediate_execution_post_only': InvalidOrder, # orders couldn't be placed includes post only orders which will be immediately executed + 'bad_schema': BadRequest, # {"error":{"code":"bad_schema","context":{"schema_errors":[{"code":"validation_error","message":"id is required","param":""}]}},"success":false} + 'invalid_api_key': AuthenticationError, # {"success":false,"error":{"code":"invalid_api_key"}} + 'invalid_signature': AuthenticationError, # {"success":false,"error":{"code":"invalid_signature"}} + 'open_order_not_found': OrderNotFound, # {"error":{"code":"open_order_not_found"},"success":false} + 'unavailable': ExchangeNotAvailable, # {"error":{"code":"unavailable"},"success":false} + }, + 'broad': { + }, + }, + }) + + def create_expired_option_market(self, symbol: str): + # support expired option contracts + quote = 'USDT' + optionParts = symbol.split('-') + symbolBase = symbol.split('/') + base = None + expiry = None + optionType = None + if symbol.find('/') > -1: + base = self.safe_string(symbolBase, 0) + expiry = self.safe_string(optionParts, 1) + optionType = self.safe_string(optionParts, 3) + else: + base = self.safe_string(optionParts, 1) + expiry = self.safe_string(optionParts, 3) + optionType = self.safe_string(optionParts, 0) + if expiry is not None: + expiry = expiry[4:] + expiry[2:4] + expiry[0:2] + settle = quote + strike = self.safe_string(optionParts, 2) + datetime = self.convert_expire_date(expiry) + timestamp = self.parse8601(datetime) + return { + 'id': optionType + '-' + base + '-' + strike + '-' + expiry, + 'symbol': base + '/' + quote + ':' + settle + '-' + expiry + '-' + strike + '-' + optionType, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': base, + 'quoteId': quote, + 'settleId': settle, + 'active': False, + 'type': 'option', + 'linear': None, + 'inverse': None, + 'spot': False, + 'swap': False, + 'future': False, + 'option': True, + 'margin': False, + 'contract': True, + 'contractSize': self.parse_number('1'), + 'expiry': timestamp, + 'expiryDatetime': datetime, + 'optionType': 'call' if (optionType == 'C') else 'put', + 'strike': self.parse_number(strike), + 'precision': { + 'amount': None, + 'price': None, + }, + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'info': None, + } + + def safe_market(self, marketId: Str = None, market: Market = None, delimiter: Str = None, marketType: Str = None) -> MarketInterface: + isOption = (marketId is not None) and ((marketId.endswith('-C')) or (marketId.endswith('-P')) or (marketId.startswith('C-')) or (marketId.startswith('P-'))) + if isOption and not (marketId in self.markets_by_id): + # handle expired option contracts + return self.create_expired_option_market(marketId) + return super(delta, self).safe_market(marketId, market, delimiter, marketType) + + def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = self.publicGetSettings(params) + # full response sample under `fetchStatus` + result = self.safe_dict(response, 'result', {}) + return self.safe_integer_product(result, 'server_time', 0.001) + + def fetch_status(self, params={}): + """ + the latest known information on the availability of the exchange API + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `status structure ` + """ + response = self.publicGetSettings(params) + # + # { + # "result": { + # "deto_liquidity_mining_daily_reward": "40775", + # "deto_msp": "1.0", + # "deto_staking_daily_reward": "23764.08", + # "enabled_wallets": [ + # "BTC", + # ... + # ], + # "portfolio_margin_params": { + # "enabled_portfolios": { + # ".DEAVAXUSDT": { + # "asset_id": 5, + # "futures_contingency_margin_percent": "1", + # "interest_rate": "0", + # "maintenance_margin_multiplier": "0.8", + # "max_price_shock": "20", + # "max_short_notional_limit": "2000", + # "options_contingency_margin_percent": "1", + # "options_discount_range": "10", + # "options_liq_band_range_percentage": "25", + # "settling_asset": "USDT", + # "sort_priority": 5, + # "underlying_asset": "AVAX", + # "volatility_down_shock": "30", + # "volatility_up_shock": "45" + # }, + # ... + # }, + # "portfolio_enabled_contracts": [ + # "futures", + # "perpetual_futures", + # "call_options", + # "put_options" + # ] + # }, + # "server_time": 1650640673500273, + # "trade_farming_daily_reward": "100000", + # "circulating_supply": "140000000", + # "circulating_supply_update_time": "1636752800", + # "deto_referral_mining_daily_reward": "0", + # "deto_total_reward_pool": "100000000", + # "deto_trade_mining_daily_reward": "0", + # "kyc_deposit_limit": "20", + # "kyc_withdrawal_limit": "10000", + # "maintenance_start_time": "1650387600000000", + # "msp_deto_commission_percent": "25", + # "under_maintenance": "false" + # }, + # "success": True + # } + # + result = self.safe_dict(response, 'result', {}) + underMaintenance = self.safe_string(result, 'under_maintenance') + status = 'maintenance' if (underMaintenance == 'true') else 'ok' + updated = self.safe_integer_product(result, 'server_time', 0.001, self.milliseconds()) + return { + 'status': status, + 'updated': updated, + 'eta': None, + 'url': None, + 'info': response, + } + + def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://docs.delta.exchange/#get-list-of-all-assets + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = self.publicGetAssets(params) + # + # { + # "result": [ + # { + # "base_withdrawal_fee": "0.005000000000000000", + # "id": "1", + # "interest_credit": False, + # "interest_slabs": null, + # "kyc_deposit_limit": "0.000000000000000000", + # "kyc_withdrawal_limit": "0.000000000000000000", + # "min_withdrawal_amount": "0.010000000000000000", + # "minimum_precision": "4", + # "name": "Ethereum", + # "networks": [ + # { + # "allowed_deposit_groups": null, + # "base_withdrawal_fee": "0.0025", + # "deposit_status": "enabled", + # "memo_required": False, + # "min_deposit_amount": "0.000050000000000000", + # "min_withdrawal_amount": "0.010000000000000000", + # "minimum_deposit_confirmations": "12", + # "network": "ERC20", + # "variable_withdrawal_fee": "0", + # "withdrawal_status": "enabled" + # }, + # { + # "allowed_deposit_groups": null, + # "base_withdrawal_fee": "0.0001", + # "deposit_status": "enabled", + # "memo_required": False, + # "min_deposit_amount": "0.000050000000000000", + # "min_withdrawal_amount": "0.000300000000000000", + # "minimum_deposit_confirmations": "15", + # "network": "BEP20(BSC)", + # "variable_withdrawal_fee": "0", + # "withdrawal_status": "enabled" + # } + # ], + # "precision": "18", + # "sort_priority": "3", + # "symbol": "ETH", + # "variable_withdrawal_fee": "0.000000000000000000" + # }, + # ], + # "success":true + # } + # + currencies = self.safe_list(response, 'result', []) + result: dict = {} + for i in range(0, len(currencies)): + currency = currencies[i] + id = self.safe_string(currency, 'symbol') + numericId = self.safe_integer(currency, 'id') + code = self.safe_currency_code(id) + chains = self.safe_list(currency, 'networks', []) + networks = {} + for j in range(0, len(chains)): + chain = chains[j] + networkId = self.safe_string(chain, 'network') + networkCode = self.network_id_to_code(networkId) + networks[networkCode] = { + 'id': networkId, + 'network': networkCode, + 'name': self.safe_string(chain, 'name'), + 'info': chain, + 'active': self.safe_string(chain, 'status') == 'enabled', + 'deposit': self.safe_string(chain, 'deposit_status') == 'enabled', + 'withdraw': self.safe_string(chain, 'withdrawal_status') == 'enabled', + 'fee': self.safe_number(chain, 'base_withdrawal_fee'), + 'limits': { + 'deposit': { + 'min': self.safe_number(chain, 'min_deposit_amount'), + 'max': None, + }, + 'withdraw': { + 'min': self.safe_number(chain, 'min_withdrawal_amount'), + 'max': None, + }, + }, + } + result[code] = self.safe_currency_structure({ + 'id': id, + 'numericId': numericId, + 'code': code, + 'name': self.safe_string(currency, 'name'), + 'info': currency, # the original payload + 'active': None, + 'deposit': self.safe_string(currency, 'deposit_status') == 'enabled', + 'withdraw': self.safe_string(currency, 'withdrawal_status') == 'enabled', + 'fee': self.safe_number(currency, 'base_withdrawal_fee'), + 'precision': self.parse_number(self.parse_precision(self.safe_string(currency, 'precision'))), + 'limits': { + 'amount': {'min': None, 'max': None}, + 'withdraw': { + 'min': self.safe_number(currency, 'min_withdrawal_amount'), + 'max': None, + }, + }, + 'networks': networks, + 'type': 'crypto', + }) + return result + + def load_markets(self, reload=False, params={}): + markets = super(delta, self).load_markets(reload, params) + currenciesByNumericId = self.safe_dict(self.options, 'currenciesByNumericId') + if (currenciesByNumericId is None) or reload: + self.options['currenciesByNumericId'] = self.index_by_stringified_numeric_id(self.currencies) + marketsByNumericId = self.safe_dict(self.options, 'marketsByNumericId') + if (marketsByNumericId is None) or reload: + self.options['marketsByNumericId'] = self.index_by_stringified_numeric_id(self.markets) + return markets + + def index_by_stringified_numeric_id(self, input): + result: dict = {} + if input is None: + return None + keys = list(input.keys()) + for i in range(0, len(keys)): + key = keys[i] + item = input[key] + numericIdString = self.safe_string(item, 'numericId') + if numericIdString is None: + continue + result[numericIdString] = item + return result + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for delta + + https://docs.delta.exchange/#get-list-of-products + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = self.publicGetProducts(params) + # + # { + # "meta":{"after":null, "before":null, "limit":100, "total_count":81}, + # "result":[ + # # the below response represents item from perpetual market + # { + # "annualized_funding":"5.475000000000000000", + # "is_quanto":false, + # "ui_config":{ + # "default_trading_view_candle":"15", + # "leverage_slider_values":[1,3,5,10,25,50], + # "price_clubbing_values":[0.001,0.005,0.05,0.1,0.5,1,5], + # "show_bracket_orders":false, + # "sort_priority":29, + # "tags":[] + # }, + # "basis_factor_max_limit":"0.15", + # "symbol":"P-LINK-D-151120", + # "id":1584, + # "default_leverage":"5.000000000000000000", + # "maker_commission_rate":"0.0005", + # "contract_unit_currency":"LINK", + # "strike_price":"12.507948", + # "settling_asset":{ + # # asset structure + # }, + # "auction_start_time":null, + # "auction_finish_time":null, + # "settlement_time":"2020-11-15T12:00:00Z", + # "launch_time":"2020-11-14T11:55:05Z", + # "spot_index":{ + # # index structure + # }, + # "trading_status":"operational", + # "tick_size":"0.001", + # "position_size_limit":100000, + # "notional_type":"vanilla", # vanilla, inverse + # "price_band":"0.4", + # "barrier_price":null, + # "description":"Daily LINK PUT options quoted in USDT and settled in USDT", + # "insurance_fund_margin_contribution":"1", + # "quoting_asset":{ + # # asset structure + # }, + # "liquidation_penalty_factor":"0.2", + # "product_specs":{"max_volatility":3,"min_volatility":0.3,"spot_price_band":"0.40"}, + # "initial_margin_scaling_factor":"0.0001", + # "underlying_asset":{ + # # asset structure + # }, + # "state":"live", + # "contract_value":"1", + # "initial_margin":"2", + # "impact_size":5000, + # "settlement_price":null, + # "contract_type":"put_options", # put_options, call_options, move_options, perpetual_futures, interest_rate_swaps, futures, spreads + # "taker_commission_rate":"0.0005", + # "maintenance_margin":"1", + # "short_description":"LINK Daily PUT Options", + # "maintenance_margin_scaling_factor":"0.00005", + # "funding_method":"mark_price", + # "max_leverage_notional":"20000" + # }, + # # the below response represents item from spot market + # { + # "position_size_limit": 10000000, + # "settlement_price": null, + # "funding_method": "mark_price", + # "settling_asset": null, + # "impact_size": 10, + # "id": 32258, + # "auction_finish_time": null, + # "description": "Solana tether spot market", + # "trading_status": "operational", + # "tick_size": "0.01", + # "liquidation_penalty_factor": "1", + # "spot_index": { + # "config": {"quoting_asset": "USDT", "service_id": 8, "underlying_asset": "SOL"}, + # "constituent_exchanges": [ + # {"exchange": "binance", "health_interval": 60, "health_priority": 1, "weight": 1}, + # {"exchange": "huobi", "health_interval": 60, "health_priority": 2, "weight": 1} + # ], + # "constituent_indices": null, + # "description": "Solana index from binance and huobi", + # "health_interval": 300, + # "id": 105, + # "impact_size": "40.000000000000000000", + # "index_type": "spot_pair", + # "is_composite": False, + # "price_method": "ltp", + # "quoting_asset_id": 5, + # "symbol": ".DESOLUSDT", + # "tick_size": "0.000100000000000000", + # "underlying_asset_id": 66 + # }, + # "contract_type": "spot", + # "launch_time": "2022-02-03T10:18:11Z", + # "symbol": "SOL_USDT", + # "disruption_reason": null, + # "settlement_time": null, + # "insurance_fund_margin_contribution": "1", + # "is_quanto": False, + # "maintenance_margin": "5", + # "taker_commission_rate": "0.0005", + # "auction_start_time": null, + # "max_leverage_notional": "10000000", + # "state": "live", + # "annualized_funding": "0", + # "notional_type": "vanilla", + # "price_band": "100", + # "product_specs": {"kyc_required": False, "max_order_size": 2000, "min_order_size": 0.01, "quoting_precision": 4, "underlying_precision": 2}, + # "default_leverage": "1.000000000000000000", + # "initial_margin": "10", + # "maintenance_margin_scaling_factor": "1", + # "ui_config": { + # "default_trading_view_candle": "1d", + # "leverage_slider_values": [], + # "price_clubbing_values": [0.01, 0.05, 0.1, 0.5, 1, 2.5, 5], + # "show_bracket_orders": False, + # "sort_priority": 2, + # "tags": [] + # }, + # "basis_factor_max_limit": "10000", + # "contract_unit_currency": "SOL", + # "strike_price": null, + # "quoting_asset": { + # "base_withdrawal_fee": "10.000000000000000000", + # "deposit_status": "enabled", + # "id": 5, + # "interest_credit": False, + # "interest_slabs": null, + # "kyc_deposit_limit": "100000.000000000000000000", + # "kyc_withdrawal_limit": "10000.000000000000000000", + # "min_withdrawal_amount": "30.000000000000000000", + # "minimum_precision": 2, + # "name": "Tether", + # "networks": [ + # {"base_withdrawal_fee": "25", "deposit_status": "enabled", "memo_required": False, "network": "ERC20", "variable_withdrawal_fee": "0", "withdrawal_status": "enabled"}, + # {"base_withdrawal_fee": "1", "deposit_status": "enabled", "memo_required": False, "network": "BEP20(BSC)", "variable_withdrawal_fee": "0", "withdrawal_status": "enabled"}, + # {"base_withdrawal_fee": "1", "deposit_status": "disabled", "memo_required": False, "network": "TRC20(TRON)", "variable_withdrawal_fee": "0", "withdrawal_status": "disabled"} + # ], + # "precision": 8, + # "sort_priority": 1, + # "symbol": "USDT", + # "variable_withdrawal_fee": "0.000000000000000000", + # "withdrawal_status": "enabled" + # }, + # "maker_commission_rate": "0.0005", + # "initial_margin_scaling_factor": "2", + # "underlying_asset": { + # "base_withdrawal_fee": "0.000000000000000000", + # "deposit_status": "enabled", + # "id": 66, + # "interest_credit": False, + # "interest_slabs": null, + # "kyc_deposit_limit": "0.000000000000000000", + # "kyc_withdrawal_limit": "0.000000000000000000", + # "min_withdrawal_amount": "0.020000000000000000", + # "minimum_precision": 4, + # "name": "Solana", + # "networks": [ + # {"base_withdrawal_fee": "0.01", "deposit_status": "enabled", "memo_required": False, "network": "SOLANA", "variable_withdrawal_fee": "0", "withdrawal_status": "enabled"}, + # {"base_withdrawal_fee": "0.01", "deposit_status": "enabled", "memo_required": False, "network": "BEP20(BSC)", "variable_withdrawal_fee": "0", "withdrawal_status": "enabled"} + # ], + # "precision": 8, + # "sort_priority": 7, + # "symbol": "SOL", + # "variable_withdrawal_fee": "0.000000000000000000", + # "withdrawal_status": "enabled" + # }, + # "barrier_price": null, + # "contract_value": "1", + # "short_description": "SOL-USDT spot market" + # }, + # ], + # "success":true + # } + # + markets = self.safe_list(response, 'result', []) + result = [] + for i in range(0, len(markets)): + market = markets[i] + type = self.safe_string(market, 'contract_type') + if type == 'options_combos': + continue + # settlingAsset = self.safe_value(market, 'settling_asset', {}) + quotingAsset = self.safe_dict(market, 'quoting_asset', {}) + underlyingAsset = self.safe_dict(market, 'underlying_asset', {}) + settlingAsset = self.safe_dict(market, 'settling_asset') + productSpecs = self.safe_dict(market, 'product_specs', {}) + baseId = self.safe_string(underlyingAsset, 'symbol') + quoteId = self.safe_string(quotingAsset, 'symbol') + settleId = self.safe_string(settlingAsset, 'symbol') + id = self.safe_string(market, 'symbol') + numericId = self.safe_integer(market, 'id') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + settle = self.safe_currency_code(settleId) + callOptions = (type == 'call_options') + putOptions = (type == 'put_options') + moveOptions = (type == 'move_options') + spot = (type == 'spot') + swap = (type == 'perpetual_futures') + future = (type == 'futures') + option = (callOptions or putOptions or moveOptions) + strike = self.safe_string(market, 'strike_price') + expiryDatetime = self.safe_string(market, 'settlement_time') + expiry = self.parse8601(expiryDatetime) + contractSize = self.safe_number(market, 'contract_value') + amountPrecision = None + if spot: + amountPrecision = self.parse_number(self.parse_precision(self.safe_string(productSpecs, 'underlying_precision'))) # seems inverse of 'impact_size' + else: + # other markets(swap, futures, move, spread, irs) seem to use the step of '1' contract + amountPrecision = self.parse_number('1') + linear = (settle == quote) + optionType = None + symbol = base + '/' + quote + if swap or future or option: + symbol = symbol + ':' + settle + if future or option: + symbol = symbol + '-' + self.yymmdd(expiry) + if option: + type = 'option' + letter = 'C' + optionType = 'call' + if putOptions: + letter = 'P' + optionType = 'put' + elif moveOptions: + letter = 'M' + optionType = 'move' + symbol = symbol + '-' + strike + '-' + letter + else: + type = 'future' + else: + type = 'swap' + state = self.safe_string(market, 'state') + result.append({ + 'id': id, + 'numericId': numericId, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': type, + 'spot': spot, + 'margin': None if spot else False, + 'swap': swap, + 'future': future, + 'option': option, + 'active': (state == 'live'), + 'contract': not spot, + 'linear': None if spot else linear, + 'inverse': None if spot else not linear, + 'taker': self.safe_number(market, 'taker_commission_rate'), + 'maker': self.safe_number(market, 'maker_commission_rate'), + 'contractSize': None if spot else contractSize, + 'expiry': expiry, + 'expiryDatetime': self.iso8601(expiry), # do not use raw expiry string + 'strike': self.parse_number(strike), + 'optionType': optionType, + 'precision': { + 'amount': amountPrecision, + 'price': self.safe_number(market, 'tick_size'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.parse_number('1'), + 'max': self.safe_number(market, 'position_size_limit'), + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': self.safe_number(market, 'min_size'), + 'max': None, + }, + }, + 'created': self.parse8601(self.safe_string(market, 'launch_time')), + 'info': market, + }) + return result + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # spot: fetchTicker, fetchTickers + # + # { + # "close": 30634.0, + # "contract_type": "spot", + # "greeks": null, + # "high": 30780.0, + # "low": 30340.5, + # "mark_price": "48000", + # "oi": "0.0000", + # "oi_change_usd_6h": "0.0000", + # "oi_contracts": "0", + # "oi_value": "0.0000", + # "oi_value_symbol": "BTC", + # "oi_value_usd": "0.0000", + # "open": 30464.0, + # "price_band": null, + # "product_id": 8320, + # "quotes": {}, + # "size": 2.6816639999999996, + # "spot_price": "30637.91465121", + # "symbol": "BTC_USDT", + # "timestamp": 1689139767621299, + # "turnover": 2.6816639999999996, + # "turnover_symbol": "BTC", + # "turnover_usd": 81896.45613400004, + # "volume": 2.6816639999999996 + # } + # + # swap: fetchTicker, fetchTickers + # + # { + # "close": 30600.5, + # "contract_type": "perpetual_futures", + # "funding_rate": "0.00602961", + # "greeks": null, + # "high": 30803.0, + # "low": 30265.5, + # "mark_basis": "-0.45601594", + # "mark_price": "30600.10481568", + # "oi": "469.9190", + # "oi_change_usd_6h": "2226314.9900", + # "oi_contracts": "469919", + # "oi_value": "469.9190", + # "oi_value_symbol": "BTC", + # "oi_value_usd": "14385640.6802", + # "open": 30458.5, + # "price_band": { + # "lower_limit": "29067.08312627", + # "upper_limit": "32126.77608693" + # }, + # "product_id": 139, + # "quotes": { + # "ask_iv": null, + # "ask_size": "965", + # "best_ask": "30600.5", + # "best_bid": "30599.5", + # "bid_iv": null, + # "bid_size": "196", + # "impact_mid_price": null, + # "mark_iv": "-0.44931641" + # }, + # "size": 1226303, + # "spot_price": "30612.85362773", + # "symbol": "BTCUSDT", + # "timestamp": 1689136597460456, + # "turnover": 37392218.45999999, + # "turnover_symbol": "USDT", + # "turnover_usd": 37392218.45999999, + # "volume": 1226.3029999999485 + # } + # + # option: fetchTicker, fetchTickers + # + # { + # "contract_type": "call_options", + # "greeks": { + # "delta": "0.60873994", + # "gamma": "0.00014854", + # "rho": "7.71808010", + # "spot": "30598.49040622", + # "theta": "-30.44743017", + # "vega": "24.83508248" + # }, + # "mark_price": "1347.74819696", + # "mark_vol": "0.39966303", + # "oi": "2.7810", + # "oi_change_usd_6h": "0.0000", + # "oi_contracts": "2781", + # "oi_value": "2.7810", + # "oi_value_symbol": "BTC", + # "oi_value_usd": "85127.4337", + # "price_band": { + # "lower_limit": "91.27423497", + # "upper_limit": "7846.19454697" + # }, + # "product_id": 107150, + # "quotes": { + # "ask_iv": "0.41023239", + # "ask_size": "2397", + # "best_ask": "1374", + # "best_bid": "1322", + # "bid_iv": "0.38929375", + # "bid_size": "3995", + # "impact_mid_price": null, + # "mark_iv": "0.39965618" + # }, + # "spot_price": "30598.43379314", + # "strike_price": "30000", + # "symbol": "C-BTC-30000-280723", + # "timestamp": 1689136932893181, + # "turnover_symbol": "USDT" + # } + # + timestamp = self.safe_integer_product(ticker, 'timestamp', 0.001) + marketId = self.safe_string(ticker, 'symbol') + symbol = self.safe_symbol(marketId, market) + last = self.safe_string(ticker, 'close') + quotes = self.safe_dict(ticker, 'quotes', {}) + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_number(ticker, 'high'), + 'low': self.safe_number(ticker, 'low'), + 'bid': self.safe_number(quotes, 'best_bid'), + 'bidVolume': self.safe_number(quotes, 'bid_size'), + 'ask': self.safe_number(quotes, 'best_ask'), + 'askVolume': self.safe_number(quotes, 'ask_size'), + 'vwap': None, + 'open': self.safe_string(ticker, 'open'), + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': self.safe_number(ticker, 'volume'), + 'quoteVolume': self.safe_number(ticker, 'turnover'), + 'markPrice': self.safe_number(ticker, 'mark_price'), + 'indexPrice': self.safe_number(ticker, 'spot_price'), + 'info': ticker, + }, market) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://docs.delta.exchange/#get-ticker-for-a-product-by-symbol + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = self.publicGetTickersSymbol(self.extend(request, params)) + # + # spot + # + # { + # "result": { + # "close": 30634.0, + # "contract_type": "spot", + # "greeks": null, + # "high": 30780.0, + # "low": 30340.5, + # "mark_price": "48000", + # "oi": "0.0000", + # "oi_change_usd_6h": "0.0000", + # "oi_contracts": "0", + # "oi_value": "0.0000", + # "oi_value_symbol": "BTC", + # "oi_value_usd": "0.0000", + # "open": 30464.0, + # "price_band": null, + # "product_id": 8320, + # "quotes": {}, + # "size": 2.6816639999999996, + # "spot_price": "30637.91465121", + # "symbol": "BTC_USDT", + # "timestamp": 1689139767621299, + # "turnover": 2.6816639999999996, + # "turnover_symbol": "BTC", + # "turnover_usd": 81896.45613400004, + # "volume": 2.6816639999999996 + # }, + # "success": True + # } + # + # swap + # + # { + # "result": { + # "close": 30600.5, + # "contract_type": "perpetual_futures", + # "funding_rate": "0.00602961", + # "greeks": null, + # "high": 30803.0, + # "low": 30265.5, + # "mark_basis": "-0.45601594", + # "mark_price": "30600.10481568", + # "oi": "469.9190", + # "oi_change_usd_6h": "2226314.9900", + # "oi_contracts": "469919", + # "oi_value": "469.9190", + # "oi_value_symbol": "BTC", + # "oi_value_usd": "14385640.6802", + # "open": 30458.5, + # "price_band": { + # "lower_limit": "29067.08312627", + # "upper_limit": "32126.77608693" + # }, + # "product_id": 139, + # "quotes": { + # "ask_iv": null, + # "ask_size": "965", + # "best_ask": "30600.5", + # "best_bid": "30599.5", + # "bid_iv": null, + # "bid_size": "196", + # "impact_mid_price": null, + # "mark_iv": "-0.44931641" + # }, + # "size": 1226303, + # "spot_price": "30612.85362773", + # "symbol": "BTCUSDT", + # "timestamp": 1689136597460456, + # "turnover": 37392218.45999999, + # "turnover_symbol": "USDT", + # "turnover_usd": 37392218.45999999, + # "volume": 1226.3029999999485 + # }, + # "success": True + # } + # + # option + # + # { + # "result": { + # "contract_type": "call_options", + # "greeks": { + # "delta": "0.60873994", + # "gamma": "0.00014854", + # "rho": "7.71808010", + # "spot": "30598.49040622", + # "theta": "-30.44743017", + # "vega": "24.83508248" + # }, + # "mark_price": "1347.74819696", + # "mark_vol": "0.39966303", + # "oi": "2.7810", + # "oi_change_usd_6h": "0.0000", + # "oi_contracts": "2781", + # "oi_value": "2.7810", + # "oi_value_symbol": "BTC", + # "oi_value_usd": "85127.4337", + # "price_band": { + # "lower_limit": "91.27423497", + # "upper_limit": "7846.19454697" + # }, + # "product_id": 107150, + # "quotes": { + # "ask_iv": "0.41023239", + # "ask_size": "2397", + # "best_ask": "1374", + # "best_bid": "1322", + # "bid_iv": "0.38929375", + # "bid_size": "3995", + # "impact_mid_price": null, + # "mark_iv": "0.39965618" + # }, + # "spot_price": "30598.43379314", + # "strike_price": "30000", + # "symbol": "C-BTC-30000-280723", + # "timestamp": 1689136932893181, + # "turnover_symbol": "USDT" + # }, + # "success": True + # } + # + result = self.safe_dict(response, 'result', {}) + return self.parse_ticker(result, market) + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://docs.delta.exchange/#get-tickers-for-products + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + symbols = self.market_symbols(symbols) + response = self.publicGetTickers(params) + # + # spot + # + # { + # "result": [ + # { + # "close": 30634.0, + # "contract_type": "spot", + # "greeks": null, + # "high": 30780.0, + # "low": 30340.5, + # "mark_price": "48000", + # "oi": "0.0000", + # "oi_change_usd_6h": "0.0000", + # "oi_contracts": "0", + # "oi_value": "0.0000", + # "oi_value_symbol": "BTC", + # "oi_value_usd": "0.0000", + # "open": 30464.0, + # "price_band": null, + # "product_id": 8320, + # "quotes": {}, + # "size": 2.6816639999999996, + # "spot_price": "30637.91465121", + # "symbol": "BTC_USDT", + # "timestamp": 1689139767621299, + # "turnover": 2.6816639999999996, + # "turnover_symbol": "BTC", + # "turnover_usd": 81896.45613400004, + # "volume": 2.6816639999999996 + # }, + # ], + # "success":true + # } + # + # swap + # + # { + # "result": [ + # { + # "close": 30600.5, + # "contract_type": "perpetual_futures", + # "funding_rate": "0.00602961", + # "greeks": null, + # "high": 30803.0, + # "low": 30265.5, + # "mark_basis": "-0.45601594", + # "mark_price": "30600.10481568", + # "oi": "469.9190", + # "oi_change_usd_6h": "2226314.9900", + # "oi_contracts": "469919", + # "oi_value": "469.9190", + # "oi_value_symbol": "BTC", + # "oi_value_usd": "14385640.6802", + # "open": 30458.5, + # "price_band": { + # "lower_limit": "29067.08312627", + # "upper_limit": "32126.77608693" + # }, + # "product_id": 139, + # "quotes": { + # "ask_iv": null, + # "ask_size": "965", + # "best_ask": "30600.5", + # "best_bid": "30599.5", + # "bid_iv": null, + # "bid_size": "196", + # "impact_mid_price": null, + # "mark_iv": "-0.44931641" + # }, + # "size": 1226303, + # "spot_price": "30612.85362773", + # "symbol": "BTCUSDT", + # "timestamp": 1689136597460456, + # "turnover": 37392218.45999999, + # "turnover_symbol": "USDT", + # "turnover_usd": 37392218.45999999, + # "volume": 1226.3029999999485 + # }, + # ], + # "success":true + # } + # + # option + # + # { + # "result": [ + # { + # "contract_type": "call_options", + # "greeks": { + # "delta": "0.60873994", + # "gamma": "0.00014854", + # "rho": "7.71808010", + # "spot": "30598.49040622", + # "theta": "-30.44743017", + # "vega": "24.83508248" + # }, + # "mark_price": "1347.74819696", + # "mark_vol": "0.39966303", + # "oi": "2.7810", + # "oi_change_usd_6h": "0.0000", + # "oi_contracts": "2781", + # "oi_value": "2.7810", + # "oi_value_symbol": "BTC", + # "oi_value_usd": "85127.4337", + # "price_band": { + # "lower_limit": "91.27423497", + # "upper_limit": "7846.19454697" + # }, + # "product_id": 107150, + # "quotes": { + # "ask_iv": "0.41023239", + # "ask_size": "2397", + # "best_ask": "1374", + # "best_bid": "1322", + # "bid_iv": "0.38929375", + # "bid_size": "3995", + # "impact_mid_price": null, + # "mark_iv": "0.39965618" + # }, + # "spot_price": "30598.43379314", + # "strike_price": "30000", + # "symbol": "C-BTC-30000-280723", + # "timestamp": 1689136932893181, + # "turnover_symbol": "USDT" + # }, + # ], + # "success":true + # } + # + tickers = self.safe_list(response, 'result', []) + result: dict = {} + for i in range(0, len(tickers)): + ticker = self.parse_ticker(tickers[i]) + symbol = ticker['symbol'] + result[symbol] = ticker + return self.filter_by_array_tickers(result, 'symbol', symbols) + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://docs.delta.exchange/#get-l2-orderbook + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['depth'] = limit + response = self.publicGetL2orderbookSymbol(self.extend(request, params)) + # + # { + # "result":{ + # "buy":[ + # {"price":"15814.0","size":912}, + # {"price":"15813.5","size":1279}, + # {"price":"15813.0","size":1634}, + # ], + # "sell":[ + # {"price":"15814.5","size":625}, + # {"price":"15815.0","size":982}, + # {"price":"15815.5","size":1328}, + # ], + # "symbol":"BTCUSDT" + # }, + # "success":true + # } + # + result = self.safe_dict(response, 'result', {}) + return self.parse_order_book(result, market['symbol'], None, 'buy', 'sell', 'price', 'size') + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # public fetchTrades + # + # { + # "buyer_role":"maker", + # "price":"15896.5", + # "seller_role":"taker", + # "size":241, + # "symbol":"BTCUSDT", + # "timestamp":1605376684714595 + # } + # + # private fetchMyTrades + # + # { + # "commission":"0.008335000000000000", + # "created_at":"2020-11-16T19:07:19Z", + # "fill_type":"normal", + # "id":"e7ff05c233a74245b72381f8dd91d1ce", + # "meta_data":{ + # "effective_commission_rate":"0.0005", + # "order_price":"16249", + # "order_size":1, + # "order_type":"market_order", + # "order_unfilled_size":0, + # "trading_fee_credits_used":"0" + # }, + # "order_id":"152999629", + # "price":"16669", + # "product":{ + # "contract_type":"perpetual_futures", + # "contract_unit_currency":"BTC", + # "contract_value":"0.001", + # "id":139, + # "notional_type":"vanilla", + # "quoting_asset":{"minimum_precision":2,"precision":6,"symbol":"USDT"}, + # "settling_asset":{"minimum_precision":2,"precision":6,"symbol":"USDT"}, + # "symbol":"BTCUSDT", + # "tick_size":"0.5", + # "underlying_asset":{"minimum_precision":4,"precision":8,"symbol":"BTC"} + # }, + # "product_id":139, + # "role":"taker", + # "side":"sell", + # "size":1 + # } + # + id = self.safe_string(trade, 'id') + orderId = self.safe_string(trade, 'order_id') + timestamp = self.parse8601(self.safe_string(trade, 'created_at')) + timestamp = self.safe_integer_product(trade, 'timestamp', 0.001, timestamp) + priceString = self.safe_string(trade, 'price') + amountString = self.safe_string(trade, 'size') + product = self.safe_dict(trade, 'product', {}) + marketId = self.safe_string(product, 'symbol') + symbol = self.safe_symbol(marketId, market) + sellerRole = self.safe_string(trade, 'seller_role') + side = self.safe_string(trade, 'side') + if side is None: + if sellerRole == 'taker': + side = 'sell' + elif sellerRole == 'maker': + side = 'buy' + takerOrMaker = self.safe_string(trade, 'role') + metaData = self.safe_dict(trade, 'meta_data', {}) + type = self.safe_string(metaData, 'order_type') + if type is not None: + type = type.replace('_order', '') + feeCostString = self.safe_string(trade, 'commission') + fee = None + if feeCostString is not None: + settlingAsset = self.safe_dict(product, 'settling_asset', {}) + feeCurrencyId = self.safe_string(settlingAsset, 'symbol') + feeCurrencyCode = self.safe_currency_code(feeCurrencyId) + fee = { + 'cost': feeCostString, + 'currency': feeCurrencyCode, + } + return self.safe_trade({ + 'id': id, + 'order': orderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'type': type, + 'side': side, + 'price': priceString, + 'amount': amountString, + 'cost': None, + 'takerOrMaker': takerOrMaker, + 'fee': fee, + 'info': trade, + }, market) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://docs.delta.exchange/#get-public-trades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = self.publicGetTradesSymbol(self.extend(request, params)) + # + # { + # "result":[ + # { + # "buyer_role":"maker", + # "price":"15896.5", + # "seller_role":"taker", + # "size":241, + # "symbol":"BTCUSDT", + # "timestamp":1605376684714595 + # } + # ], + # "success":true + # } + # + result = self.safe_list(response, 'result', []) + return self.parse_trades(result, market, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # { + # "time":1605393120, + # "open":15989, + # "high":15989, + # "low":15987.5, + # "close":15987.5, + # "volume":565 + # } + # + return [ + self.safe_timestamp(ohlcv, 'time'), + self.safe_number(ohlcv, 'open'), + self.safe_number(ohlcv, 'high'), + self.safe_number(ohlcv, 'low'), + self.safe_number(ohlcv, 'close'), + self.safe_number(ohlcv, 'volume'), + ] + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://docs.delta.exchange/#delta-exchange-api-v2-historical-ohlc-candles-sparklines + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.until]: timestamp in ms of the latest candle to fetch + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'resolution': self.safe_string(self.timeframes, timeframe, timeframe), + } + duration = self.parse_timeframe(timeframe) + limit = limit if limit else 2000 # max 2000 + until = self.safe_integer_product(params, 'until', 0.001) + untilIsDefined = (until is not None) + if untilIsDefined: + until = self.parse_to_int(until) + if since is None: + end = until if untilIsDefined else self.seconds() + request['end'] = end + request['start'] = end - limit * duration + else: + start = self.parse_to_int(since / 1000) + request['start'] = start + request['end'] = until if untilIsDefined else self.sum(start, limit * duration) + price = self.safe_string(params, 'price') + if price == 'mark': + request['symbol'] = 'MARK:' + market['id'] + elif price == 'index': + request['symbol'] = market['info']['spot_index']['symbol'] + else: + request['symbol'] = market['id'] + params = self.omit(params, ['price', 'until']) + response = self.publicGetHistoryCandles(self.extend(request, params)) + # + # { + # "success":true, + # "result":[ + # {"time":1605393120,"open":15989,"high":15989,"low":15987.5,"close":15987.5,"volume":565}, + # {"time":1605393180,"open":15966,"high":15966,"low":15959,"close":15959,"volume":24}, + # {"time":1605393300,"open":15973,"high":15973,"low":15973,"close":15973,"volume":1288}, + # ] + # } + # + result = self.safe_list(response, 'result', []) + return self.parse_ohlcvs(result, market, timeframe, since, limit) + + def parse_balance(self, response) -> Balances: + balances = self.safe_list(response, 'result', []) + result: dict = {'info': response} + currenciesByNumericId = self.safe_dict(self.options, 'currenciesByNumericId', {}) + for i in range(0, len(balances)): + balance = balances[i] + currencyId = self.safe_string(balance, 'asset_id') + currency = self.safe_dict(currenciesByNumericId, currencyId) + code = currencyId if (currency is None) else currency['code'] + account = self.account() + account['total'] = self.safe_string(balance, 'balance') + account['free'] = self.safe_string(balance, 'available_balance') + result[code] = account + return self.safe_balance(result) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://docs.delta.exchange/#get-wallet-balances + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + response = self.privateGetWalletBalances(params) + # + # { + # "result":[ + # { + # "asset_id":1, + # "available_balance":"0", + # "balance":"0", + # "commission":"0", + # "id":154883, + # "interest_credit":"0", + # "order_margin":"0", + # "pending_referral_bonus":"0", + # "pending_trading_fee_credit":"0", + # "position_margin":"0", + # "trading_fee_credit":"0", + # "user_id":22142 + # }, + # ], + # "success":true + # } + # + return self.parse_balance(response) + + def fetch_position(self, symbol: str, params={}): + """ + fetch data on a single open contract trade position + + https://docs.delta.exchange/#get-position + + :param str symbol: unified market symbol of the market the position is held in, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `position structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'product_id': market['numericId'], + } + response = self.privateGetPositions(self.extend(request, params)) + # + # { + # "result":{ + # "entry_price":null, + # "size":0, + # "timestamp":1605454074268079 + # }, + # "success":true + # } + # + result = self.safe_dict(response, 'result', {}) + return self.parse_position(result, market) + + def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://docs.delta.exchange/#get-margined-positions + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + self.load_markets() + response = self.privateGetPositionsMargined(params) + # + # { + # "success": True, + # "result": [ + # { + # "user_id": 0, + # "size": 0, + # "entry_price": "string", + # "margin": "string", + # "liquidation_price": "string", + # "bankruptcy_price": "string", + # "adl_level": 0, + # "product_id": 0, + # "product_symbol": "string", + # "commission": "string", + # "realized_pnl": "string", + # "realized_funding": "string" + # } + # ] + # } + # + result = self.safe_list(response, 'result', []) + return self.parse_positions(result, symbols) + + def parse_position(self, position: dict, market: Market = None): + # + # fetchPosition + # + # { + # "entry_price":null, + # "size":0, + # "timestamp":1605454074268079 + # } + # + # + # fetchPositions + # + # { + # "user_id": 0, + # "size": 0, + # "entry_price": "string", + # "margin": "string", + # "liquidation_price": "string", + # "bankruptcy_price": "string", + # "adl_level": 0, + # "product_id": 0, + # "product_symbol": "string", + # "commission": "string", + # "realized_pnl": "string", + # "realized_funding": "string" + # } + # + marketId = self.safe_string(position, 'product_symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + timestamp = self.safe_integer_product(position, 'timestamp', 0.001) + sizeString = self.safe_string(position, 'size') + side = None + if sizeString is not None: + if Precise.string_gt(sizeString, '0'): + side = 'buy' + elif Precise.string_lt(sizeString, '0'): + side = 'sell' + return self.safe_position({ + 'info': position, + 'id': None, + 'symbol': symbol, + 'notional': None, + 'marginMode': None, + 'liquidationPrice': self.safe_number(position, 'liquidation_price'), + 'entryPrice': self.safe_number(position, 'entry_price'), + 'unrealizedPnl': None, # todo - realized_pnl ? + 'percentage': None, + 'contracts': self.parse_number(sizeString), + 'contractSize': self.safe_number(market, 'contractSize'), + 'markPrice': None, + 'side': side, + 'hedged': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'maintenanceMargin': None, + 'maintenanceMarginPercentage': None, + 'collateral': None, + 'initialMargin': None, + 'initialMarginPercentage': None, + 'leverage': None, + 'marginRatio': None, + 'stopLossPrice': None, + 'takeProfitPrice': None, + }) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'open': 'open', + 'pending': 'open', + 'closed': 'closed', + 'cancelled': 'canceled', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder, cancelOrder, editOrder, fetchOpenOrders, fetchClosedOrders + # + # { + # "average_fill_price":null, + # "bracket_order":null, + # "bracket_stop_loss_limit_price":null, + # "bracket_stop_loss_price":null, + # "bracket_take_profit_limit_price":null, + # "bracket_take_profit_price":null, + # "bracket_trail_amount":null, + # "cancellation_reason":null, + # "client_order_id":null, + # "close_on_trigger":"false", + # "commission":"0", + # "created_at":"2020-11-16T02:38:26Z", + # "id":152870626, + # "limit_price":"10000", + # "meta_data":{"source":"api"}, + # "order_type":"limit_order", + # "paid_commission":"0", + # "product_id":139, + # "reduce_only":false, + # "side":"buy", + # "size":0, + # "state":"open", + # "stop_order_type":null, + # "stop_price":null, + # "stop_trigger_method":"mark_price", + # "time_in_force":"gtc", + # "trail_amount":null, + # "unfilled_size":0, + # "user_id":22142 + # } + # + # fetchOrder + # + # { + # "id": 123, + # "user_id": 453671, + # "size": 10, + # "unfilled_size": 2, + # "side": "buy", + # "order_type": "limit_order", + # "limit_price": "59000", + # "stop_order_type": "stop_loss_order", + # "stop_price": "55000", + # "paid_commission": "0.5432", + # "commission": "0.5432", + # "reduce_only": False, + # "client_order_id": "my_signal_34521712", + # "state": "open", + # "created_at": "1725865012000000", + # "product_id": 27, + # "product_symbol": "BTCUSD" + # } + # + id = self.safe_string(order, 'id') + clientOrderId = self.safe_string(order, 'client_order_id') + createdAt = self.safe_string(order, 'created_at') + timestamp = None + if createdAt is not None: + if createdAt.find('-') >= 0: + timestamp = self.parse8601(createdAt) + else: + timestamp = self.safe_integer_product(order, 'created_at', 0.001) + marketId = self.safe_string(order, 'product_id') + marketsByNumericId = self.safe_dict(self.options, 'marketsByNumericId', {}) + market = self.safe_value(marketsByNumericId, marketId, market) + symbol = marketId if (market is None) else market['symbol'] + status = self.parse_order_status(self.safe_string(order, 'state')) + side = self.safe_string(order, 'side') + type = self.safe_string(order, 'order_type') + if type is not None: + type = type.replace('_order', '') + price = self.safe_string(order, 'limit_price') + amount = self.safe_string(order, 'size') + remaining = self.safe_string(order, 'unfilled_size') + average = self.safe_string(order, 'average_fill_price') + fee = None + feeCostString = self.safe_string(order, 'paid_commission') + if feeCostString is not None: + feeCurrencyCode = None + if market is not None: + settlingAsset = self.safe_dict(market['info'], 'settling_asset', {}) + feeCurrencyId = self.safe_string(settlingAsset, 'symbol') + feeCurrencyCode = self.safe_currency_code(feeCurrencyId) + fee = { + 'cost': feeCostString, + 'currency': feeCurrencyCode, + } + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'symbol': symbol, + 'type': type, + 'side': side, + 'price': price, + 'amount': amount, + 'cost': None, + 'average': average, + 'filled': None, + 'remaining': remaining, + 'status': status, + 'fee': fee, + 'trades': None, + }, market) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://docs.delta.exchange/#place-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.reduceOnly]: *contract only* indicates if self order is to reduce the size of a position + :returns dict: an `order structure ` + """ + self.load_markets() + orderType = type + '_order' + market = self.market(symbol) + request: dict = { + 'product_id': market['numericId'], + # 'limit_price': self.price_to_precision(market['symbol'], price), + 'size': self.amount_to_precision(market['symbol'], amount), + 'side': side, + 'order_type': orderType, + # 'client_order_id': 'string', + # 'time_in_force': 'gtc', # gtc, ioc, fok + # 'post_only': 'false', # 'true', + # 'reduce_only': 'false', # 'true', + } + if type == 'limit': + request['limit_price'] = self.price_to_precision(market['symbol'], price) + clientOrderId = self.safe_string_2(params, 'clientOrderId', 'client_order_id') + params = self.omit(params, ['clientOrderId', 'client_order_id']) + if clientOrderId is not None: + request['client_order_id'] = clientOrderId + reduceOnly = self.safe_bool(params, 'reduceOnly') + if reduceOnly: + request['reduce_only'] = reduceOnly + params = self.omit(params, 'reduceOnly') + response = self.privatePostOrders(self.extend(request, params)) + # + # { + # "result":{ + # "average_fill_price":null, + # "bracket_order":null, + # "bracket_stop_loss_limit_price":null, + # "bracket_stop_loss_price":null, + # "bracket_take_profit_limit_price":null, + # "bracket_take_profit_price":null, + # "bracket_trail_amount":null, + # "cancellation_reason":null, + # "client_order_id":null, + # "close_on_trigger":"false", + # "commission":"0", + # "created_at":"2020-11-16T02:38:26Z", + # "id":152870626, + # "limit_price":"10000", + # "meta_data":{"source":"api"}, + # "order_type":"limit_order", + # "paid_commission":"0", + # "product_id":139, + # "reduce_only":false, + # "side":"buy", + # "size":0, + # "state":"open", + # "stop_order_type":null, + # "stop_price":null, + # "stop_trigger_method":"mark_price", + # "time_in_force":"gtc", + # "trail_amount":null, + # "unfilled_size":0, + # "user_id":22142 + # }, + # "success":true + # } + # + result = self.safe_dict(response, 'result', {}) + return self.parse_order(result, market) + + def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + """ + edit a trade order + + https://docs.delta.exchange/#edit-order + + :param str id: order id + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of the currency you want to trade in units of the base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'id': int(id), + 'product_id': market['numericId'], + # "limit_price": self.price_to_precision(symbol, price), + # "size": self.amount_to_precision(symbol, amount), + } + if amount is not None: + request['size'] = int(self.amount_to_precision(symbol, amount)) + if price is not None: + request['limit_price'] = self.price_to_precision(symbol, price) + response = self.privatePutOrders(self.extend(request, params)) + # + # { + # "success": True, + # "result": { + # "id": "ashb1212", + # "product_id": 27, + # "limit_price": "9200", + # "side": "buy", + # "size": 100, + # "unfilled_size": 50, + # "user_id": 1, + # "order_type": "limit_order", + # "state": "open", + # "created_at": "..." + # } + # } + # + result = self.safe_dict(response, 'result') + return self.parse_order(result, market) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://docs.delta.exchange/#cancel-order + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'id': int(id), + 'product_id': market['numericId'], + } + response = self.privateDeleteOrders(self.extend(request, params)) + # + # { + # "result":{ + # "average_fill_price":null, + # "bracket_order":null, + # "bracket_stop_loss_limit_price":null, + # "bracket_stop_loss_price":null, + # "bracket_take_profit_limit_price":null, + # "bracket_take_profit_price":null, + # "bracket_trail_amount":null, + # "cancellation_reason":"cancelled_by_user", + # "client_order_id":null, + # "close_on_trigger":"false", + # "commission":"0", + # "created_at":"2020-11-16T02:38:26Z", + # "id":152870626, + # "limit_price":"10000", + # "meta_data":{"source":"api"}, + # "order_type":"limit_order", + # "paid_commission":"0", + # "product_id":139, + # "reduce_only":false, + # "side":"buy", + # "size":0, + # "state":"cancelled", + # "stop_order_type":null, + # "stop_price":null, + # "stop_trigger_method":"mark_price", + # "time_in_force":"gtc", + # "trail_amount":null, + # "unfilled_size":0, + # "user_id":22142 + # }, + # "success":true + # } + # + result = self.safe_dict(response, 'result') + return self.parse_order(result, market) + + def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders in a market + + https://docs.delta.exchange/#cancel-all-open-orders + + :param str symbol: unified market symbol of the market to cancel orders in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelAllOrders() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'product_id': market['numericId'], + # 'cancel_limit_orders': 'true', + # 'cancel_stop_orders': 'true', + } + response = self.privateDeleteOrdersAll(self.extend(request, params)) + # + # { + # "result":{}, + # "success":true + # } + # + return [ + self.safe_order({ + 'info': response, + }), + ] + + def fetch_order(self, id: str, symbol: Str = None, params={}) -> Order: + """ + fetches information on an order made by the user + + https://docs.delta.exchange/#get-order-by-id + https://docs.delta.exchange/#get-order-by-client-oid + + :param str id: the order id + :param str [symbol]: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.clientOrderId]: client order id of the order + :returns dict: an `order structure ` + """ + self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + clientOrderId = self.safe_string_n(params, ['clientOrderId', 'client_oid', 'clientOid']) + params = self.omit(params, ['clientOrderId', 'client_oid', 'clientOid']) + request: dict = {} + response = None + if clientOrderId is not None: + request['client_oid'] = clientOrderId + response = self.privateGetOrdersClientOrderIdClientOid(self.extend(request, params)) + else: + request['order_id'] = id + response = self.privateGetOrdersOrderId(self.extend(request, params)) + # + # { + # "success": True, + # "result": { + # "id": 123, + # "user_id": 453671, + # "size": 10, + # "unfilled_size": 2, + # "side": "buy", + # "order_type": "limit_order", + # "limit_price": "59000", + # "stop_order_type": "stop_loss_order", + # "stop_price": "55000", + # "paid_commission": "0.5432", + # "commission": "0.5432", + # "reduce_only": False, + # "client_order_id": "my_signal_34521712", + # "state": "open", + # "created_at": "1725865012000000", + # "product_id": 27, + # "product_symbol": "BTCUSD" + # } + # } + # + result = self.safe_dict(response, 'result', {}) + return self.parse_order(result, market) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://docs.delta.exchange/#get-active-orders + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + return self.fetch_orders_with_method('privateGetOrders', symbol, since, limit, params) + + def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://docs.delta.exchange/#get-order-history-cancelled-and-closed + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + return self.fetch_orders_with_method('privateGetOrdersHistory', symbol, since, limit, params) + + def fetch_orders_with_method(self, method, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + self.load_markets() + request: dict = { + # 'product_ids': market['id'], # comma-separated + # 'contract_types': types, # comma-separated, futures, perpetual_futures, call_options, put_options, interest_rate_swaps, move_options, spreads + # 'order_types': types, # comma-separated, market, limit, stop_market, stop_limit, all_stop + # 'start_time': since * 1000, + # 'end_time': self.microseconds(), + # 'after', # after cursor for pagination + # 'before', # before cursor for pagination + # 'page_size': limit, # number of records per page + } + market = None + if symbol is not None: + market = self.market(symbol) + request['product_ids'] = market['numericId'] # accepts a comma-separated list of ids + if since is not None: + request['start_time'] = str(since) + '000' + if limit is not None: + request['page_size'] = limit + response = None + if method == 'privateGetOrders': + response = self.privateGetOrders(self.extend(request, params)) + elif method == 'privateGetOrdersHistory': + response = self.privateGetOrdersHistory(self.extend(request, params)) + # + # { + # "success": True, + # "result": [ + # { + # "id": "ashb1212", + # "product_id": 27, + # "limit_price": "9200", + # "side": "buy", + # "size": 100, + # "unfilled_size": 50, + # "user_id": 1, + # "order_type": "limit_order", + # "state": "open", + # "created_at": "..." + # } + # ], + # "meta": { + # "after": "string", + # "before": "string" + # } + # } + # + result = self.safe_list(response, 'result', []) + return self.parse_orders(result, market, since, limit) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://docs.delta.exchange/#get-user-fills-by-filters + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + request: dict = { + # 'product_ids': market['id'], # comma-separated + # 'contract_types': types, # comma-separated, futures, perpetual_futures, call_options, put_options, interest_rate_swaps, move_options, spreads + # 'start_time': since * 1000, + # 'end_time': self.microseconds(), + # 'after', # after cursor for pagination + # 'before', # before cursor for pagination + # 'page_size': limit, # number of records per page + } + market = None + if symbol is not None: + market = self.market(symbol) + request['product_ids'] = market['numericId'] # accepts a comma-separated list of ids + if since is not None: + request['start_time'] = str(since) + '000' + if limit is not None: + request['page_size'] = limit + response = self.privateGetFills(self.extend(request, params)) + # + # { + # "meta":{ + # "after":null, + # "before":null, + # "limit":10, + # "total_count":2 + # }, + # "result":[ + # { + # "commission":"0.008335000000000000", + # "created_at":"2020-11-16T19:07:19Z", + # "fill_type":"normal", + # "id":"e7ff05c233a74245b72381f8dd91d1ce", + # "meta_data":{ + # "effective_commission_rate":"0.0005", + # "order_price":"16249", + # "order_size":1, + # "order_type":"market_order", + # "order_unfilled_size":0, + # "trading_fee_credits_used":"0" + # }, + # "order_id":"152999629", + # "price":"16669", + # "product":{ + # "contract_type":"perpetual_futures", + # "contract_unit_currency":"BTC", + # "contract_value":"0.001", + # "id":139, + # "notional_type":"vanilla", + # "quoting_asset":{"minimum_precision":2,"precision":6,"symbol":"USDT"}, + # "settling_asset":{"minimum_precision":2,"precision":6,"symbol":"USDT"}, + # "symbol":"BTCUSDT", + # "tick_size":"0.5", + # "underlying_asset":{"minimum_precision":4,"precision":8,"symbol":"BTC"} + # }, + # "product_id":139, + # "role":"taker", + # "side":"sell", + # "size":1 + # } + # ], + # "success":true + # } + # + result = self.safe_list(response, 'result', []) + return self.parse_trades(result, market, since, limit) + + def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + + https://docs.delta.exchange/#get-wallet-transactions + + :param str [code]: unified currency code, default is None + :param int [since]: timestamp in ms of the earliest ledger entry, default is None + :param int [limit]: max number of ledger entries to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ledger structure ` + """ + self.load_markets() + request: dict = { + # 'asset_id': currency['numericId'], + # 'end_time': self.seconds(), + # 'after': 'string', # after cursor for pagination + # 'before': 'string', # before cursor for pagination + # 'page_size': limit, + } + currency = None + if code is not None: + currency = self.currency(code) + request['asset_id'] = currency['numericId'] + if limit is not None: + request['page_size'] = limit + response = self.privateGetWalletTransactions(self.extend(request, params)) + # + # { + # "meta":{"after":null,"before":null,"limit":10,"total_count":1}, + # "result":[ + # { + # "amount":"29.889184", + # "asset_id":5, + # "balance":"29.889184", + # "created_at":"2020-11-15T21:25:01Z", + # "meta_data":{ + # "deposit_id":3884, + # "transaction_id":"0x41a60174849828530abb5008e98fc63c9b598288743ec4ba9620bcce900a3b8d" + # }, + # "transaction_type":"deposit", + # "user_id":22142, + # "uuid":"70bb5679da3c4637884e2dc63efaa846" + # } + # ], + # "success":true + # } + # + result = self.safe_list(response, 'result', []) + return self.parse_ledger(result, currency, since, limit) + + def parse_ledger_entry_type(self, type): + types: dict = { + 'pnl': 'pnl', + 'deposit': 'transaction', + 'withdrawal': 'transaction', + 'commission': 'fee', + 'conversion': 'trade', + # 'perpetual_futures_funding': 'perpetual_futures_funding', + # 'withdrawal_cancellation': 'withdrawal_cancellation', + 'referral_bonus': 'referral', + 'commission_rebate': 'rebate', + # 'promo_credit': 'promo_credit', + } + return self.safe_string(types, type, type) + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + # + # { + # "amount":"29.889184", + # "asset_id":5, + # "balance":"29.889184", + # "created_at":"2020-11-15T21:25:01Z", + # "meta_data":{ + # "deposit_id":3884, + # "transaction_id":"0x41a60174849828530abb5008e98fc63c9b598288743ec4ba9620bcce900a3b8d" + # }, + # "transaction_type":"deposit", + # "user_id":22142, + # "uuid":"70bb5679da3c4637884e2dc63efaa846" + # } + # + id = self.safe_string(item, 'uuid') + direction = None + account = None + metaData = self.safe_dict(item, 'meta_data', {}) + referenceId = self.safe_string(metaData, 'transaction_id') + referenceAccount = None + type = self.safe_string(item, 'transaction_type') + if (type == 'deposit') or (type == 'commission_rebate') or (type == 'referral_bonus') or (type == 'pnl') or (type == 'withdrawal_cancellation') or (type == 'promo_credit'): + direction = 'in' + elif (type == 'withdrawal') or (type == 'commission') or (type == 'conversion') or (type == 'perpetual_futures_funding'): + direction = 'out' + type = self.parse_ledger_entry_type(type) + currencyId = self.safe_string(item, 'asset_id') + currenciesByNumericId = self.safe_dict(self.options, 'currenciesByNumericId') + currency = self.safe_value(currenciesByNumericId, currencyId, currency) + code = None if (currency is None) else currency['code'] + amount = self.safe_string(item, 'amount') + timestamp = self.parse8601(self.safe_string(item, 'created_at')) + after = self.safe_string(item, 'balance') + before = Precise.string_max('0', Precise.string_sub(after, amount)) + status = 'ok' + return self.safe_ledger_entry({ + 'info': item, + 'id': id, + 'direction': direction, + 'account': account, + 'referenceId': referenceId, + 'referenceAccount': referenceAccount, + 'type': type, + 'currency': code, + 'amount': self.parse_number(amount), + 'before': self.parse_number(before), + 'after': self.parse_number(after), + 'status': status, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'fee': None, + }, currency) + + def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.network]: unified network code + :returns dict: an `address structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'asset_symbol': currency['id'], + } + networkCode = self.safe_string_upper(params, 'network') + if networkCode is not None: + request['network'] = self.network_code_to_id(networkCode, code) + params = self.omit(params, 'network') + response = self.privateGetDepositsAddress(self.extend(request, params)) + # + # { + # "success": True, + # "result": { + # "id": 1915615, + # "user_id": 27854758, + # "address": "TXYB4GdKsXKEWbeSNPsmGZu4ZVCkhVh1Zz", + # "memo": "", + # "status": "active", + # "updated_at": "2023-01-12T06:03:46.000Z", + # "created_at": "2023-01-12T06:03:46.000Z", + # "asset_symbol": "USDT", + # "network": "TRC20(TRON)", + # "custodian": "fireblocks" + # } + # } + # + result = self.safe_dict(response, 'result', {}) + return self.parse_deposit_address(result, currency) + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + # + # { + # "id": 1915615, + # "user_id": 27854758, + # "address": "TXYB4GdKsXKEWbeSNPsmGZu4ZVCkhVh1Zz", + # "memo": "", + # "status": "active", + # "updated_at": "2023-01-12T06:03:46.000Z", + # "created_at": "2023-01-12T06:03:46.000Z", + # "asset_symbol": "USDT", + # "network": "TRC20(TRON)", + # "custodian": "fireblocks" + # } + # + address = self.safe_string(depositAddress, 'address') + marketId = self.safe_string(depositAddress, 'asset_symbol') + networkId = self.safe_string(depositAddress, 'network') + self.check_address(address) + return { + 'info': depositAddress, + 'currency': self.safe_currency_code(marketId, currency), + 'network': self.network_id_to_code(networkId), + 'address': address, + 'tag': self.safe_string(depositAddress, 'memo'), + } + + def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate + + https://docs.delta.exchange/#get-ticker-for-a-product-by-symbol + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + self.load_markets() + market = self.market(symbol) + if not market['swap']: + raise BadSymbol(self.id + ' fetchFundingRate() supports swap contracts only') + request: dict = { + 'symbol': market['id'], + } + response = self.publicGetTickersSymbol(self.extend(request, params)) + # + # { + # "result": { + # "close": 30600.5, + # "contract_type": "perpetual_futures", + # "funding_rate": "0.00602961", + # "greeks": null, + # "high": 30803.0, + # "low": 30265.5, + # "mark_basis": "-0.45601594", + # "mark_price": "30600.10481568", + # "oi": "469.9190", + # "oi_change_usd_6h": "2226314.9900", + # "oi_contracts": "469919", + # "oi_value": "469.9190", + # "oi_value_symbol": "BTC", + # "oi_value_usd": "14385640.6802", + # "open": 30458.5, + # "price_band": { + # "lower_limit": "29067.08312627", + # "upper_limit": "32126.77608693" + # }, + # "product_id": 139, + # "quotes": { + # "ask_iv": null, + # "ask_size": "965", + # "best_ask": "30600.5", + # "best_bid": "30599.5", + # "bid_iv": null, + # "bid_size": "196", + # "impact_mid_price": null, + # "mark_iv": "-0.44931641" + # }, + # "size": 1226303, + # "spot_price": "30612.85362773", + # "symbol": "BTCUSDT", + # "timestamp": 1689136597460456, + # "turnover": 37392218.45999999, + # "turnover_symbol": "USDT", + # "turnover_usd": 37392218.45999999, + # "volume": 1226.3029999999485 + # }, + # "success": True + # } + # + result = self.safe_dict(response, 'result', {}) + return self.parse_funding_rate(result, market) + + def fetch_funding_rates(self, symbols: Strings = None, params={}) -> FundingRates: + """ + fetch the funding rate for multiple markets + + https://docs.delta.exchange/#get-tickers-for-products + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `funding rate structures `, indexed by market symbols + """ + self.load_markets() + symbols = self.market_symbols(symbols) + request: dict = { + 'contract_types': 'perpetual_futures', + } + response = self.publicGetTickers(self.extend(request, params)) + # + # { + # "result": [ + # { + # "close": 30600.5, + # "contract_type": "perpetual_futures", + # "funding_rate": "0.00602961", + # "greeks": null, + # "high": 30803.0, + # "low": 30265.5, + # "mark_basis": "-0.45601594", + # "mark_price": "30600.10481568", + # "oi": "469.9190", + # "oi_change_usd_6h": "2226314.9900", + # "oi_contracts": "469919", + # "oi_value": "469.9190", + # "oi_value_symbol": "BTC", + # "oi_value_usd": "14385640.6802", + # "open": 30458.5, + # "price_band": { + # "lower_limit": "29067.08312627", + # "upper_limit": "32126.77608693" + # }, + # "product_id": 139, + # "quotes": { + # "ask_iv": null, + # "ask_size": "965", + # "best_ask": "30600.5", + # "best_bid": "30599.5", + # "bid_iv": null, + # "bid_size": "196", + # "impact_mid_price": null, + # "mark_iv": "-0.44931641" + # }, + # "size": 1226303, + # "spot_price": "30612.85362773", + # "symbol": "BTCUSDT", + # "timestamp": 1689136597460456, + # "turnover": 37392218.45999999, + # "turnover_symbol": "USDT", + # "turnover_usd": 37392218.45999999, + # "volume": 1226.3029999999485 + # }, + # ], + # "success":true + # } + # + rates = self.safe_list(response, 'result', []) + return self.parse_funding_rates(rates, symbols) + + def parse_funding_rate(self, contract, market: Market = None) -> FundingRate: + # + # { + # "close": 30600.5, + # "contract_type": "perpetual_futures", + # "funding_rate": "0.00602961", + # "greeks": null, + # "high": 30803.0, + # "low": 30265.5, + # "mark_basis": "-0.45601594", + # "mark_price": "30600.10481568", + # "oi": "469.9190", + # "oi_change_usd_6h": "2226314.9900", + # "oi_contracts": "469919", + # "oi_value": "469.9190", + # "oi_value_symbol": "BTC", + # "oi_value_usd": "14385640.6802", + # "open": 30458.5, + # "price_band": { + # "lower_limit": "29067.08312627", + # "upper_limit": "32126.77608693" + # }, + # "product_id": 139, + # "quotes": { + # "ask_iv": null, + # "ask_size": "965", + # "best_ask": "30600.5", + # "best_bid": "30599.5", + # "bid_iv": null, + # "bid_size": "196", + # "impact_mid_price": null, + # "mark_iv": "-0.44931641" + # }, + # "size": 1226303, + # "spot_price": "30612.85362773", + # "symbol": "BTCUSDT", + # "timestamp": 1689136597460456, + # "turnover": 37392218.45999999, + # "turnover_symbol": "USDT", + # "turnover_usd": 37392218.45999999, + # "volume": 1226.3029999999485 + # } + # + timestamp = self.safe_integer_product(contract, 'timestamp', 0.001) + marketId = self.safe_string(contract, 'symbol') + fundingRateString = self.safe_string(contract, 'funding_rate') + fundingRate = Precise.string_div(fundingRateString, '100') + return { + 'info': contract, + 'symbol': self.safe_symbol(marketId, market), + 'markPrice': self.safe_number(contract, 'mark_price'), + 'indexPrice': self.safe_number(contract, 'spot_price'), + 'interestRate': None, + 'estimatedSettlePrice': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'fundingRate': self.parse_number(fundingRate), + 'fundingTimestamp': None, + 'fundingDatetime': None, + 'nextFundingRate': None, + 'nextFundingTimestamp': None, + 'nextFundingDatetime': None, + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': None, + } + + def add_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + add margin + + https://docs.delta.exchange/#add-remove-position-margin + + :param str symbol: unified market symbol + :param float amount: amount of margin to add + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin structure ` + """ + return self.modify_margin_helper(symbol, amount, 'add', params) + + def reduce_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + remove margin from a position + + https://docs.delta.exchange/#add-remove-position-margin + + :param str symbol: unified market symbol + :param float amount: the amount of margin to remove + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin structure ` + """ + return self.modify_margin_helper(symbol, amount, 'reduce', params) + + def modify_margin_helper(self, symbol: str, amount, type, params={}) -> MarginModification: + self.load_markets() + market = self.market(symbol) + amount = str(amount) + if type == 'reduce': + amount = Precise.string_mul(amount, '-1') + request: dict = { + 'product_id': market['numericId'], + 'delta_margin': amount, + } + response = self.privatePostPositionsChangeMargin(self.extend(request, params)) + # + # { + # "result": { + # "auto_topup": False, + # "bankruptcy_price": "24934.12", + # "commission": "0.01197072", + # "created_at": "2023-07-20T03:49:09.159401Z", + # "entry_price": "29926.8", + # "liquidation_price": "25083.754", + # "margin": "4.99268", + # "margin_mode": "isolated", + # "product_id": 84, + # "product_symbol": "BTCUSDT", + # "realized_cashflow": "0", + # "realized_funding": "0", + # "realized_pnl": "0", + # "size": 1, + # "updated_at": "2023-07-20T03:49:09.159401Z", + # "user_id": 30084879 + # }, + # "success": True + # } + # + result = self.safe_dict(response, 'result', {}) + return self.parse_margin_modification(result, market) + + def parse_margin_modification(self, data: dict, market: Market = None) -> MarginModification: + # + # { + # "auto_topup": False, + # "bankruptcy_price": "24934.12", + # "commission": "0.01197072", + # "created_at": "2023-07-20T03:49:09.159401Z", + # "entry_price": "29926.8", + # "liquidation_price": "25083.754", + # "margin": "4.99268", + # "margin_mode": "isolated", + # "product_id": 84, + # "product_symbol": "BTCUSDT", + # "realized_cashflow": "0", + # "realized_funding": "0", + # "realized_pnl": "0", + # "size": 1, + # "updated_at": "2023-07-20T03:49:09.159401Z", + # "user_id": 30084879 + # } + # + marketId = self.safe_string(data, 'product_symbol') + market = self.safe_market(marketId, market) + return { + 'info': data, + 'symbol': market['symbol'], + 'type': None, + 'marginMode': 'isolated', + 'amount': None, + 'total': self.safe_number(data, 'margin'), + 'code': None, + 'status': None, + 'timestamp': None, + 'datetime': None, + } + + def fetch_open_interest(self, symbol: str, params={}): + """ + retrieves the open interest of a derivative market + + https://docs.delta.exchange/#get-ticker-for-a-product-by-symbol + + :param str symbol: unified market symbol + :param dict [params]: exchange specific parameters + :returns dict} an open interest structure{@link https://docs.ccxt.com/#/?id=open-interest-structure: + """ + self.load_markets() + market = self.market(symbol) + if not market['contract']: + raise BadRequest(self.id + ' fetchOpenInterest() supports contract markets only') + request: dict = { + 'symbol': market['id'], + } + response = self.publicGetTickersSymbol(self.extend(request, params)) + # + # { + # "result": { + # "close": 894.0, + # "contract_type": "call_options", + # "greeks": { + # "delta": "0.67324861", + # "gamma": "0.00022178", + # "rho": "4.34638266", + # "spot": "30178.53195697", + # "theta": "-35.64972577", + # "vega": "16.34381277" + # }, + # "high": 946.0, + # "low": 893.0, + # "mark_price": "1037.07582681", + # "mark_vol": "0.35899491", + # "oi": "0.0910", + # "oi_change_usd_6h": "-90.5500", + # "oi_contracts": "91", + # "oi_value": "0.0910", + # "oi_value_symbol": "BTC", + # "oi_value_usd": "2746.3549", + # "open": 946.0, + # "price_band": { + # "lower_limit": "133.37794509", + # "upper_limit": "5663.66930164" + # }, + # "product_id": 116171, + # "quotes": { + # "ask_iv": "0.36932389", + # "ask_size": "1321", + # "best_ask": "1054", + # "best_bid": "1020", + # "bid_iv": "0.34851914", + # "bid_size": "2202", + # "impact_mid_price": null, + # "mark_iv": "0.35896335" + # }, + # "size": 152, + # "spot_price": "30178.53195697", + # "strike_price": "29500", + # "symbol": "C-BTC-29500-280723", + # "timestamp": 1689834695286094, + # "turnover": 4546.601744940001, + # "turnover_symbol": "USDT", + # "turnover_usd": 4546.601744940001, + # "volume": 0.15200000000000002 + # }, + # "success": True + # } + # + result = self.safe_dict(response, 'result', {}) + return self.parse_open_interest(result, market) + + def parse_open_interest(self, interest, market: Market = None): + # + # { + # "close": 894.0, + # "contract_type": "call_options", + # "greeks": { + # "delta": "0.67324861", + # "gamma": "0.00022178", + # "rho": "4.34638266", + # "spot": "30178.53195697", + # "theta": "-35.64972577", + # "vega": "16.34381277" + # }, + # "high": 946.0, + # "low": 893.0, + # "mark_price": "1037.07582681", + # "mark_vol": "0.35899491", + # "oi": "0.0910", + # "oi_change_usd_6h": "-90.5500", + # "oi_contracts": "91", + # "oi_value": "0.0910", + # "oi_value_symbol": "BTC", + # "oi_value_usd": "2746.3549", + # "open": 946.0, + # "price_band": { + # "lower_limit": "133.37794509", + # "upper_limit": "5663.66930164" + # }, + # "product_id": 116171, + # "quotes": { + # "ask_iv": "0.36932389", + # "ask_size": "1321", + # "best_ask": "1054", + # "best_bid": "1020", + # "bid_iv": "0.34851914", + # "bid_size": "2202", + # "impact_mid_price": null, + # "mark_iv": "0.35896335" + # }, + # "size": 152, + # "spot_price": "30178.53195697", + # "strike_price": "29500", + # "symbol": "C-BTC-29500-280723", + # "timestamp": 1689834695286094, + # "turnover": 4546.601744940001, + # "turnover_symbol": "USDT", + # "turnover_usd": 4546.601744940001, + # "volume": 0.15200000000000002 + # } + # + timestamp = self.safe_integer_product(interest, 'timestamp', 0.001) + marketId = self.safe_string(interest, 'symbol') + return self.safe_open_interest({ + 'symbol': self.safe_symbol(marketId, market), + 'baseVolume': self.safe_number(interest, 'oi_value'), + 'quoteVolume': self.safe_number(interest, 'oi_value_usd'), + 'openInterestAmount': self.safe_number(interest, 'oi_contracts'), + 'openInterestValue': self.safe_number(interest, 'oi'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'info': interest, + }, market) + + def fetch_leverage(self, symbol: str, params={}) -> Leverage: + """ + fetch the set leverage for a market + + https://docs.delta.exchange/#get-order-leverage + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `leverage structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'product_id': market['numericId'], + } + response = self.privateGetProductsProductIdOrdersLeverage(self.extend(request, params)) + # + # { + # "result": { + # "index_symbol": null, + # "leverage": "10", + # "margin_mode": "isolated", + # "order_margin": "0", + # "product_id": 84, + # "user_id": 30084879 + # }, + # "success": True + # } + # + result = self.safe_dict(response, 'result', {}) + return self.parse_leverage(result, market) + + def parse_leverage(self, leverage: dict, market: Market = None) -> Leverage: + marketId = self.safe_string(leverage, 'index_symbol') + leverageValue = self.safe_integer(leverage, 'leverage') + return { + 'info': leverage, + 'symbol': self.safe_symbol(marketId, market), + 'marginMode': self.safe_string_lower(leverage, 'margin_mode'), + 'longLeverage': leverageValue, + 'shortLeverage': leverageValue, + } + + def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://docs.delta.exchange/#change-order-leverage + + :param float leverage: the rate of leverage + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setLeverage() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'product_id': market['numericId'], + 'leverage': leverage, + } + # + # { + # "result": { + # "leverage": "20", + # "margin_mode": "isolated", + # "order_margin": "0", + # "product_id": 84 + # }, + # "success": True + # } + # + return self.privatePostProductsProductIdOrdersLeverage(self.extend(request, params)) + + def fetch_settlement_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical settlement records + + https://docs.delta.exchange/#get-product-settlement-prices + + :param str symbol: unified market symbol of the settlement history + :param int [since]: timestamp in ms + :param int [limit]: number of records + :param dict [params]: exchange specific params + :returns dict[]: a list of `settlement history objects ` + """ + self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + request: dict = { + 'states': 'expired', + } + if limit is not None: + request['page_size'] = limit + response = self.publicGetProducts(self.extend(request, params)) + # + # { + # "result": [ + # { + # "contract_value": "0.001", + # "basis_factor_max_limit": "10.95", + # "maker_commission_rate": "0.0003", + # "launch_time": "2023-07-19T04:30:03Z", + # "trading_status": "operational", + # "product_specs": { + # "backup_vol_expiry_time": 31536000, + # "max_deviation_from_external_vol": 0.75, + # "max_lower_deviation_from_external_vol": 0.75, + # "max_upper_deviation_from_external_vol": 0.5, + # "max_volatility": 3, + # "min_volatility": 0.1, + # "premium_commission_rate": 0.1, + # "settlement_index_price": "29993.536675710806", + # "vol_calculation_method": "orderbook", + # "vol_expiry_time": 31536000 + # }, + # "description": "BTC call option expiring on 19-7-2023", + # "settlement_price": "0", + # "disruption_reason": null, + # "settling_asset": {}, + # "initial_margin": "1", + # "tick_size": "0.1", + # "maintenance_margin": "0.5", + # "id": 117542, + # "notional_type": "vanilla", + # "ui_config": {}, + # "contract_unit_currency": "BTC", + # "symbol": "C-BTC-30900-190723", + # "insurance_fund_margin_contribution": "1", + # "price_band": "2", + # "annualized_funding": "10.95", + # "impact_size": 200, + # "contract_type": "call_options", + # "position_size_limit": 255633, + # "max_leverage_notional": "200000", + # "initial_margin_scaling_factor": "0.000002", + # "strike_price": "30900", + # "is_quanto": False, + # "settlement_time": "2023-07-19T12:00:00Z", + # "liquidation_penalty_factor": "0.5", + # "funding_method": "mark_price", + # "taker_commission_rate": "0.0003", + # "default_leverage": "100.000000000000000000", + # "state": "expired", + # "auction_start_time": null, + # "short_description": "BTC Call", + # "quoting_asset": {}, + # "maintenance_margin_scaling_factor":"0.000002" + # } + # ], + # "success": True + # } + # + result = self.safe_list(response, 'result', []) + settlements = self.parse_settlements(result, market) + sorted = self.sort_by(settlements, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, market['symbol'], since, limit) + + def parse_settlement(self, settlement, market): + # + # { + # "contract_value": "0.001", + # "basis_factor_max_limit": "10.95", + # "maker_commission_rate": "0.0003", + # "launch_time": "2023-07-19T04:30:03Z", + # "trading_status": "operational", + # "product_specs": { + # "backup_vol_expiry_time": 31536000, + # "max_deviation_from_external_vol": 0.75, + # "max_lower_deviation_from_external_vol": 0.75, + # "max_upper_deviation_from_external_vol": 0.5, + # "max_volatility": 3, + # "min_volatility": 0.1, + # "premium_commission_rate": 0.1, + # "settlement_index_price": "29993.536675710806", + # "vol_calculation_method": "orderbook", + # "vol_expiry_time": 31536000 + # }, + # "description": "BTC call option expiring on 19-7-2023", + # "settlement_price": "0", + # "disruption_reason": null, + # "settling_asset": {}, + # "initial_margin": "1", + # "tick_size": "0.1", + # "maintenance_margin": "0.5", + # "id": 117542, + # "notional_type": "vanilla", + # "ui_config": {}, + # "contract_unit_currency": "BTC", + # "symbol": "C-BTC-30900-190723", + # "insurance_fund_margin_contribution": "1", + # "price_band": "2", + # "annualized_funding": "10.95", + # "impact_size": 200, + # "contract_type": "call_options", + # "position_size_limit": 255633, + # "max_leverage_notional": "200000", + # "initial_margin_scaling_factor": "0.000002", + # "strike_price": "30900", + # "is_quanto": False, + # "settlement_time": "2023-07-19T12:00:00Z", + # "liquidation_penalty_factor": "0.5", + # "funding_method": "mark_price", + # "taker_commission_rate": "0.0003", + # "default_leverage": "100.000000000000000000", + # "state": "expired", + # "auction_start_time": null, + # "short_description": "BTC Call", + # "quoting_asset": {}, + # "maintenance_margin_scaling_factor":"0.000002" + # } + # + datetime = self.safe_string(settlement, 'settlement_time') + marketId = self.safe_string(settlement, 'symbol') + return { + 'info': settlement, + 'symbol': self.safe_symbol(marketId, market), + 'price': self.safe_number(settlement, 'settlement_price'), + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + } + + def parse_settlements(self, settlements, market): + result = [] + for i in range(0, len(settlements)): + result.append(self.parse_settlement(settlements[i], market)) + return result + + def fetch_greeks(self, symbol: str, params={}) -> Greeks: + """ + fetches an option contracts greeks, financial metrics used to measure the factors that affect the price of an options contract + + https://docs.delta.exchange/#get-ticker-for-a-product-by-symbol + + :param str symbol: unified symbol of the market to fetch greeks for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `greeks structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = self.publicGetTickersSymbol(self.extend(request, params)) + # + # { + # "result": { + # "close": 6793.0, + # "contract_type": "call_options", + # "greeks": { + # "delta": "0.94739174", + # "gamma": "0.00002206", + # "rho": "11.00890725", + # "spot": "36839.58124652", + # "theta": "-18.18365310", + # "vega": "7.85209698" + # }, + # "high": 7556.0, + # "low": 6793.0, + # "mark_price": "6955.70698909", + # "mark_vol": "0.66916863", + # "oi": "1.8980", + # "oi_change_usd_6h": "110.4600", + # "oi_contracts": "1898", + # "oi_value": "1.8980", + # "oi_value_symbol": "BTC", + # "oi_value_usd": "69940.7319", + # "open": 7.2e3, + # "price_band": { + # "lower_limit": "5533.89814767", + # "upper_limit": "11691.37688371" + # }, + # "product_id": 129508, + # "quotes": { + # "ask_iv": "0.90180438", + # "ask_size": "1898", + # "best_ask": "7210", + # "best_bid": "6913", + # "bid_iv": "0.60881706", + # "bid_size": "3163", + # "impact_mid_price": null, + # "mark_iv": "0.66973549" + # }, + # "size": 5, + # "spot_price": "36839.58153868", + # "strike_price": "30000", + # "symbol": "C-BTC-30000-241123", + # "timestamp": 1699584998504530, + # "turnover": 184.41206804, + # "turnover_symbol": "USDT", + # "turnover_usd": 184.41206804, + # "volume": 0.005 + # }, + # "success": True + # } + # + result = self.safe_dict(response, 'result', {}) + return self.parse_greeks(result, market) + + def parse_greeks(self, greeks: dict, market: Market = None) -> Greeks: + # + # { + # "close": 6793.0, + # "contract_type": "call_options", + # "greeks": { + # "delta": "0.94739174", + # "gamma": "0.00002206", + # "rho": "11.00890725", + # "spot": "36839.58124652", + # "theta": "-18.18365310", + # "vega": "7.85209698" + # }, + # "high": 7556.0, + # "low": 6793.0, + # "mark_price": "6955.70698909", + # "mark_vol": "0.66916863", + # "oi": "1.8980", + # "oi_change_usd_6h": "110.4600", + # "oi_contracts": "1898", + # "oi_value": "1.8980", + # "oi_value_symbol": "BTC", + # "oi_value_usd": "69940.7319", + # "open": 7.2e3, + # "price_band": { + # "lower_limit": "5533.89814767", + # "upper_limit": "11691.37688371" + # }, + # "product_id": 129508, + # "quotes": { + # "ask_iv": "0.90180438", + # "ask_size": "1898", + # "best_ask": "7210", + # "best_bid": "6913", + # "bid_iv": "0.60881706", + # "bid_size": "3163", + # "impact_mid_price": null, + # "mark_iv": "0.66973549" + # }, + # "size": 5, + # "spot_price": "36839.58153868", + # "strike_price": "30000", + # "symbol": "C-BTC-30000-241123", + # "timestamp": 1699584998504530, + # "turnover": 184.41206804, + # "turnover_symbol": "USDT", + # "turnover_usd": 184.41206804, + # "volume": 0.005 + # } + # + timestamp = self.safe_integer_product(greeks, 'timestamp', 0.001) + marketId = self.safe_string(greeks, 'symbol') + symbol = self.safe_symbol(marketId, market) + stats = self.safe_dict(greeks, 'greeks', {}) + quotes = self.safe_dict(greeks, 'quotes', {}) + return { + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'delta': self.safe_number(stats, 'delta'), + 'gamma': self.safe_number(stats, 'gamma'), + 'theta': self.safe_number(stats, 'theta'), + 'vega': self.safe_number(stats, 'vega'), + 'rho': self.safe_number(stats, 'rho'), + 'bidSize': self.safe_number(quotes, 'bid_size'), + 'askSize': self.safe_number(quotes, 'ask_size'), + 'bidImpliedVolatility': self.safe_number(quotes, 'bid_iv'), + 'askImpliedVolatility': self.safe_number(quotes, 'ask_iv'), + 'markImpliedVolatility': self.safe_number(quotes, 'mark_iv'), + 'bidPrice': self.safe_number(quotes, 'best_bid'), + 'askPrice': self.safe_number(quotes, 'best_ask'), + 'markPrice': self.safe_number(greeks, 'mark_price'), + 'lastPrice': None, + 'underlyingPrice': self.safe_number(greeks, 'spot_price'), + 'info': greeks, + } + + def close_all_positions(self, params={}) -> List[Position]: + """ + closes all open positions for a market type + + https://docs.delta.exchange/#close-all-positions + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.user_id]: the users id + :returns dict[]: A list of `position structures ` + """ + self.load_markets() + request: dict = { + 'close_all_portfolio': True, + 'close_all_isolated': True, + # 'user_id': 12345, + } + response = self.privatePostPositionsCloseAll(self.extend(request, params)) + # + # {"result":{},"success":true} + # + position = self.parse_position(self.safe_dict(response, 'result', {})) + return [position] + + def fetch_margin_mode(self, symbol: str, params={}) -> MarginMode: + """ + fetches the margin mode of a trading pair + + https://docs.delta.exchange/#get-user + + :param str symbol: unified symbol of the market to fetch the margin mode for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin mode structure ` + """ + self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + response = self.privateGetProfile(params) + # + # { + # "result": { + # "is_password_set": True, + # "kyc_expiry_date": null, + # "phishing_code": "12345", + # "preferences": { + # "favorites": [] + # }, + # "is_kyc_provisioned": False, + # "country": "Canada", + # "margin_mode": "isolated", + # "mfa_updated_at": "2023-07-19T01:04:43Z", + # "last_name": "", + # "oauth_apple_active": False, + # "pf_index_symbol": null, + # "proof_of_identity_status": "approved", + # "dob": null, + # "email": "abc_123@gmail.com", + # "force_change_password": False, + # "nick_name": "still-breeze-123", + # "oauth_google_active": False, + # "phone_verification_status": "verified", + # "id": 12345678, + # "last_seen": null, + # "is_withdrawal_enabled": True, + # "force_change_mfa": False, + # "enable_bots": False, + # "kyc_verified_on": null, + # "created_at": "2023-07-19T01:02:32Z", + # "withdrawal_blocked_till": null, + # "proof_of_address_status": "approved", + # "is_password_change_blocked": False, + # "is_mfa_enabled": True, + # "is_kyc_done": True, + # "oauth": null, + # "account_name": "Main", + # "sub_account_permissions": null, + # "phone_number": null, + # "tracking_info": { + # "ga_cid": "1234.4321", + # "is_kyc_gtm_tracked": True, + # "sub_account_config": { + # "cross": 2, + # "isolated": 2, + # "portfolio": 2 + # } + # }, + # "first_name": "", + # "phone_verified_on": null, + # "seen_intro": False, + # "password_updated_at": null, + # "is_login_enabled": True, + # "registration_date": "2023-07-19T01:02:32Z", + # "permissions": {}, + # "max_sub_accounts_limit": 2, + # "country_calling_code": null, + # "is_sub_account": False, + # "is_kyc_refresh_required": False + # }, + # "success": True + # } + # + result = self.safe_dict(response, 'result', {}) + return self.parse_margin_mode(result, market) + + def parse_margin_mode(self, marginMode: dict, market=None) -> MarginMode: + symbol = None + if market is not None: + symbol = market['symbol'] + return { + 'info': marginMode, + 'symbol': symbol, + 'marginMode': self.safe_string(marginMode, 'margin_mode'), + } + + def fetch_option(self, symbol: str, params={}) -> Option: + """ + fetches option data that is commonly found in an option chain + + https://docs.delta.exchange/#get-ticker-for-a-product-by-symbol + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `option chain structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = self.publicGetTickersSymbol(self.extend(request, params)) + # + # { + # "result": { + # "close": 6793.0, + # "contract_type": "call_options", + # "greeks": { + # "delta": "0.94739174", + # "gamma": "0.00002206", + # "rho": "11.00890725", + # "spot": "36839.58124652", + # "theta": "-18.18365310", + # "vega": "7.85209698" + # }, + # "high": 7556.0, + # "low": 6793.0, + # "mark_price": "6955.70698909", + # "mark_vol": "0.66916863", + # "oi": "1.8980", + # "oi_change_usd_6h": "110.4600", + # "oi_contracts": "1898", + # "oi_value": "1.8980", + # "oi_value_symbol": "BTC", + # "oi_value_usd": "69940.7319", + # "open": 7.2e3, + # "price_band": { + # "lower_limit": "5533.89814767", + # "upper_limit": "11691.37688371" + # }, + # "product_id": 129508, + # "quotes": { + # "ask_iv": "0.90180438", + # "ask_size": "1898", + # "best_ask": "7210", + # "best_bid": "6913", + # "bid_iv": "0.60881706", + # "bid_size": "3163", + # "impact_mid_price": null, + # "mark_iv": "0.66973549" + # }, + # "size": 5, + # "spot_price": "36839.58153868", + # "strike_price": "30000", + # "symbol": "C-BTC-30000-241123", + # "timestamp": 1699584998504530, + # "turnover": 184.41206804, + # "turnover_symbol": "USDT", + # "turnover_usd": 184.41206804, + # "volume": 0.005 + # }, + # "success": True + # } + # + result = self.safe_dict(response, 'result', {}) + return self.parse_option(result, None, market) + + def parse_option(self, chain: dict, currency: Currency = None, market: Market = None) -> Option: + # + # { + # "close": 6793.0, + # "contract_type": "call_options", + # "greeks": { + # "delta": "0.94739174", + # "gamma": "0.00002206", + # "rho": "11.00890725", + # "spot": "36839.58124652", + # "theta": "-18.18365310", + # "vega": "7.85209698" + # }, + # "high": 7556.0, + # "low": 6793.0, + # "mark_price": "6955.70698909", + # "mark_vol": "0.66916863", + # "oi": "1.8980", + # "oi_change_usd_6h": "110.4600", + # "oi_contracts": "1898", + # "oi_value": "1.8980", + # "oi_value_symbol": "BTC", + # "oi_value_usd": "69940.7319", + # "open": 7.2e3, + # "price_band": { + # "lower_limit": "5533.89814767", + # "upper_limit": "11691.37688371" + # }, + # "product_id": 129508, + # "quotes": { + # "ask_iv": "0.90180438", + # "ask_size": "1898", + # "best_ask": "7210", + # "best_bid": "6913", + # "bid_iv": "0.60881706", + # "bid_size": "3163", + # "impact_mid_price": null, + # "mark_iv": "0.66973549" + # }, + # "size": 5, + # "spot_price": "36839.58153868", + # "strike_price": "30000", + # "symbol": "C-BTC-30000-241123", + # "timestamp": 1699584998504530, + # "turnover": 184.41206804, + # "turnover_symbol": "USDT", + # "turnover_usd": 184.41206804, + # "volume": 0.005 + # } + # + marketId = self.safe_string(chain, 'symbol') + market = self.safe_market(marketId, market) + quotes = self.safe_dict(chain, 'quotes', {}) + timestamp = self.safe_integer_product(chain, 'timestamp', 0.001) + return { + 'info': chain, + 'currency': None, + 'symbol': market['symbol'], + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'impliedVolatility': self.safe_number(quotes, 'mark_iv'), + 'openInterest': self.safe_number(chain, 'oi'), + 'bidPrice': self.safe_number(quotes, 'best_bid'), + 'askPrice': self.safe_number(quotes, 'best_ask'), + 'midPrice': self.safe_number(quotes, 'impact_mid_price'), + 'markPrice': self.safe_number(chain, 'mark_price'), + 'lastPrice': None, + 'underlyingPrice': self.safe_number(chain, 'spot_price'), + 'change': None, + 'percentage': None, + 'baseVolume': self.safe_number(chain, 'volume'), + 'quoteVolume': None, + } + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + requestPath = '/' + self.version + '/' + self.implode_params(path, params) + url = self.urls['api'][api] + requestPath + query = self.omit(params, self.extract_params(path)) + if api == 'public': + if query: + url += '?' + self.urlencode(query) + elif api == 'private': + self.check_required_credentials() + timestamp = str(self.seconds()) + headers = { + 'api-key': self.apiKey, + 'timestamp': timestamp, + } + auth = method + timestamp + requestPath + if method == 'GET': + if query: + queryString = '?' + self.urlencode(query) + auth += queryString + url += queryString + else: + body = self.json(query) + auth += body + headers['Content-Type'] = 'application/json' + signature = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256) + headers['signature'] = signature + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None + # + # {"error":{"code":"insufficient_margin","context":{"available_balance":"0.000000000000000000","required_additional_balance":"1.618626000000000000000000000"}},"success":false} + # + error = self.safe_dict(response, 'error', {}) + errorCode = self.safe_string(error, 'code') + if errorCode is not None: + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], errorCode, feedback) + raise ExchangeError(feedback) # unknown message + return None diff --git a/ccxt/deribit.py b/ccxt/deribit.py new file mode 100644 index 0000000..ba6932a --- /dev/null +++ b/ccxt/deribit.py @@ -0,0 +1,3666 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.deribit import ImplicitAPI +import hashlib +from ccxt.base.types import Account, Any, Balances, Currencies, Currency, DepositAddress, Greeks, Int, Market, Num, Option, OptionChain, Order, OrderBook, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, Trade, TradingFees, Transaction, MarketInterface, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidAddress +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import NotSupported +from ccxt.base.errors import DDoSProtection +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.errors import OnMaintenance +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class deribit(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(deribit, self).describe(), { + 'id': 'deribit', + 'name': 'Deribit', + 'countries': ['NL'], # Netherlands + 'version': 'v2', + 'userAgent': None, + # 20 requests per second for non-matching-engine endpoints, 1000ms / 20 = 50ms between requests + # 5 requests per second for matching-engine endpoints, cost = (1000ms / rateLimit) / 5 = 4 + 'rateLimit': 50, + 'pro': True, + 'has': { + 'CORS': True, + 'spot': False, + 'margin': False, + 'swap': True, + 'future': True, + 'option': True, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'cancelOrders': False, + 'createDepositAddress': True, + 'createOrder': True, + 'createReduceOnlyOrder': True, + 'createStopLimitOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'createTrailingAmountOrder': True, + 'editOrder': True, + 'fetchAccounts': True, + 'fetchBalance': True, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDeposit': False, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchDepositWithdrawFees': True, + 'fetchFundingRate': True, + 'fetchFundingRateHistory': True, + 'fetchGreeks': True, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': True, + 'fetchMarginMode': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMyLiquidations': True, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenOrders': True, + 'fetchOption': True, + 'fetchOptionChain': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': False, + 'fetchOrderTrades': True, + 'fetchPosition': True, + 'fetchPositionMode': False, + 'fetchPositions': True, + 'fetchPremiumIndexOHLCV': False, + 'fetchStatus': True, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': True, + 'fetchTransactions': False, + 'fetchTransfer': False, + 'fetchTransfers': True, + 'fetchUnderlyingAssets': False, + 'fetchVolatilityHistory': True, + 'fetchWithdrawal': False, + 'fetchWithdrawals': True, + 'sandbox': True, + 'transfer': True, + 'withdraw': True, + }, + 'timeframes': { + '1m': '1', + '3m': '3', + '5m': '5', + '10m': '10', + '15m': '15', + '30m': '30', + '1h': '60', + '2h': '120', + '3h': '180', + '6h': '360', + '12h': '720', + '1d': '1D', + }, + 'urls': { + 'test': { + 'rest': 'https://test.deribit.com', + }, + 'logo': 'https://user-images.githubusercontent.com/1294454/41933112-9e2dd65a-798b-11e8-8440-5bab2959fcb8.jpg', + 'api': { + 'rest': 'https://www.deribit.com', + }, + 'www': 'https://www.deribit.com', + 'doc': [ + 'https://docs.deribit.com/v2', + 'https://github.com/deribit', + ], + 'fees': 'https://www.deribit.com/pages/information/fees', + 'referral': { + 'url': 'https://www.deribit.com/reg-1189.4038', + 'discount': 0.1, + }, + }, + 'api': { + 'public': { + 'get': { + # Authentication + 'auth': 1, + 'exchange_token': 1, + 'fork_token': 1, + # Session management + 'set_heartbeat': 1, + 'disable_heartbeat': 1, + # Supporting + 'get_time': 1, + 'hello': 1, + 'status': 1, + 'test': 1, + # Subscription management + 'subscribe': 1, + 'unsubscribe': 1, + 'unsubscribe_all': 1, + # Account management + 'get_announcements': 1, + # Market data + 'get_book_summary_by_currency': 1, + 'get_book_summary_by_instrument': 1, + 'get_contract_size': 1, + 'get_currencies': 1, + 'get_delivery_prices': 1, + 'get_funding_chart_data': 1, + 'get_funding_rate_history': 1, + 'get_funding_rate_value': 1, + 'get_historical_volatility': 1, + 'get_index': 1, + 'get_index_price': 1, + 'get_index_price_names': 1, + 'get_instrument': 1, + 'get_instruments': 1, + 'get_last_settlements_by_currency': 1, + 'get_last_settlements_by_instrument': 1, + 'get_last_trades_by_currency': 1, + 'get_last_trades_by_currency_and_time': 1, + 'get_last_trades_by_instrument': 1, + 'get_last_trades_by_instrument_and_time': 1, + 'get_mark_price_history': 1, + 'get_order_book': 1, + 'get_trade_volumes': 1, + 'get_tradingview_chart_data': 1, + 'get_volatility_index_data': 1, + 'ticker': 1, + }, + }, + 'private': { + 'get': { + # Authentication + 'logout': 1, + # Session management + 'enable_cancel_on_disconnect': 1, + 'disable_cancel_on_disconnect': 1, + 'get_cancel_on_disconnect': 1, + # Subscription management + 'subscribe': 1, + 'unsubscribe': 1, + 'unsubscribe_all': 1, + # Account management + 'change_api_key_name': 1, + 'change_scope_in_api_key': 1, + 'change_subaccount_name': 1, + 'create_api_key': 1, + 'create_subaccount': 1, + 'disable_api_key': 1, + 'disable_tfa_for_subaccount': 1, + 'enable_affiliate_program': 1, + 'enable_api_key': 1, + 'get_access_log': 1, + 'get_account_summary': 1, + 'get_account_summaries': 1, + 'get_affiliate_program_info': 1, + 'get_email_language': 1, + 'get_new_announcements': 1, + 'get_portfolio_margins': 1, + 'get_position': 1, + 'get_positions': 1, + 'get_subaccounts': 1, + 'get_subaccounts_details': 1, + 'get_transaction_log': 1, + 'list_api_keys': 1, + 'remove_api_key': 1, + 'remove_subaccount': 1, + 'reset_api_key': 1, + 'set_announcement_as_read': 1, + 'set_api_key_as_default': 1, + 'set_email_for_subaccount': 1, + 'set_email_language': 1, + 'set_password_for_subaccount': 1, + 'toggle_notifications_from_subaccount': 1, + 'toggle_subaccount_login': 1, + # Block Trade + 'execute_block_trade': 4, + 'get_block_trade': 1, + 'get_last_block_trades_by_currency': 1, + 'invalidate_block_trade_signature': 1, + 'verify_block_trade': 4, + # Trading + 'buy': 4, + 'sell': 4, + 'edit': 4, + 'edit_by_label': 4, + 'cancel': 4, + 'cancel_all': 4, + 'cancel_all_by_currency': 4, + 'cancel_all_by_instrument': 4, + 'cancel_by_label': 4, + 'close_position': 4, + 'get_margins': 1, + 'get_mmp_config': 1, + 'get_open_orders_by_currency': 1, + 'get_open_orders_by_instrument': 1, + 'get_order_history_by_currency': 1, + 'get_order_history_by_instrument': 1, + 'get_order_margin_by_ids': 1, + 'get_order_state': 1, + 'get_stop_order_history': 1, # deprecated + 'get_trigger_order_history': 1, + 'get_user_trades_by_currency': 1, + 'get_user_trades_by_currency_and_time': 1, + 'get_user_trades_by_instrument': 1, + 'get_user_trades_by_instrument_and_time': 1, + 'get_user_trades_by_order': 1, + 'reset_mmp': 1, + 'set_mmp_config': 1, + 'get_settlement_history_by_instrument': 1, + 'get_settlement_history_by_currency': 1, + # Wallet + 'cancel_transfer_by_id': 1, + 'cancel_withdrawal': 1, + 'create_deposit_address': 1, + 'get_current_deposit_address': 1, + 'get_deposits': 1, + 'get_transfers': 1, + 'get_withdrawals': 1, + 'submit_transfer_to_subaccount': 1, + 'submit_transfer_to_user': 1, + 'withdraw': 1, + }, + }, + }, + 'features': { + 'default': { + 'sandbox': True, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, # todo + # todo implement + 'triggerPriceType': { + 'last': True, + 'mark': True, + 'index': True, + }, + 'triggerDirection': False, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': True, + }, + 'hedged': False, + 'selfTradePrevention': False, + 'trailing': True, # todo + 'leverage': False, + 'marketBuyByCost': True, # todo + 'marketBuyRequiresPrice': False, + 'iceberg': True, # todo + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, # todo: revise + 'daysBack': 100000, + 'untilDays': 100000, + 'symbolRequired': True, # todo + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, # todo + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, # todo + }, + 'fetchOrders': None, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 100000, + 'daysBackCanceled': 1, + 'untilDays': 100000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, # todo + }, + 'fetchOHLCV': { + 'limit': 1000, # todo: recheck + }, + }, + 'spot': { + 'extends': 'default', + }, + 'swap': { + 'linear': { + 'extends': 'default', + }, + 'inverse': { + 'extends': 'default', + }, + }, + 'future': { + 'linear': { + 'extends': 'default', + }, + 'inverse': { + 'extends': 'default', + }, + }, + }, + 'exceptions': { + # 0 or absent Success, No error. + '9999': PermissionDenied, # 'api_not_enabled' User didn't enable API for the Account. + '10000': AuthenticationError, # 'authorization_required' Authorization issue, invalid or absent signature etc. + '10001': ExchangeError, # 'error' Some general failure, no public information available. + '10002': InvalidOrder, # 'qty_too_low' Order quantity is too low. + '10003': InvalidOrder, # 'order_overlap' Rejection, order overlap is found and self-trading is not enabled. + '10004': OrderNotFound, # 'order_not_found' Attempt to operate with order that can't be found by specified id. + '10005': InvalidOrder, # 'price_too_low ' Price is too low, defines current limit for the operation. + '10006': InvalidOrder, # 'price_too_low4idx ' Price is too low for current index, defines current bottom limit for the operation. + '10007': InvalidOrder, # 'price_too_high ' Price is too high, defines current up limit for the operation. + '10008': InvalidOrder, # 'price_too_high4idx ' Price is too high for current index, defines current up limit for the operation. + '10009': InsufficientFunds, # 'not_enough_funds' Account has not enough funds for the operation. + '10010': OrderNotFound, # 'already_closed' Attempt of doing something with closed order. + '10011': InvalidOrder, # 'price_not_allowed' This price is not allowed for some reason. + '10012': InvalidOrder, # 'book_closed' Operation for instrument which order book had been closed. + '10013': PermissionDenied, # 'pme_max_total_open_orders ' Total limit of open orders has been exceeded, it is applicable for PME users. + '10014': PermissionDenied, # 'pme_max_future_open_orders ' Limit of count of futures' open orders has been exceeded, it is applicable for PME users. + '10015': PermissionDenied, # 'pme_max_option_open_orders ' Limit of count of options' open orders has been exceeded, it is applicable for PME users. + '10016': PermissionDenied, # 'pme_max_future_open_orders_size ' Limit of size for futures has been exceeded, it is applicable for PME users. + '10017': PermissionDenied, # 'pme_max_option_open_orders_size ' Limit of size for options has been exceeded, it is applicable for PME users. + '10018': PermissionDenied, # 'non_pme_max_future_position_size ' Limit of size for futures has been exceeded, it is applicable for non-PME users. + '10019': PermissionDenied, # 'locked_by_admin' Trading is temporary locked by admin. + '10020': ExchangeError, # 'invalid_or_unsupported_instrument' Instrument name is not valid. + '10021': InvalidOrder, # 'invalid_amount' Amount is not valid. + '10022': InvalidOrder, # 'invalid_quantity' quantity was not recognized valid number(for API v1). + '10023': InvalidOrder, # 'invalid_price' price was not recognized valid number. + '10024': InvalidOrder, # 'invalid_max_show' max_show parameter was not recognized valid number. + '10025': InvalidOrder, # 'invalid_order_id' Order id is missing or its format was not recognized. + '10026': InvalidOrder, # 'price_precision_exceeded' Extra precision of the price is not supported. + '10027': InvalidOrder, # 'non_integer_contract_amount' Futures contract amount was not recognized. + '10028': DDoSProtection, # 'too_many_requests' Allowed request rate has been exceeded. + '10029': OrderNotFound, # 'not_owner_of_order' Attempt to operate with not own order. + '10030': ExchangeError, # 'must_be_websocket_request' REST request where Websocket is expected. + '10031': ExchangeError, # 'invalid_args_for_instrument' Some of arguments are not recognized. + '10032': InvalidOrder, # 'whole_cost_too_low' Total cost is too low. + '10033': NotSupported, # 'not_implemented' Method is not implemented yet. + '10034': InvalidOrder, # 'stop_price_too_high' Stop price is too high. + '10035': InvalidOrder, # 'stop_price_too_low' Stop price is too low. + '10036': InvalidOrder, # 'invalid_max_show_amount' Max Show Amount is not valid. + '10040': ExchangeNotAvailable, # 'retry' Request can't be processed right now and should be retried. + '10041': OnMaintenance, # 'settlement_in_progress' Settlement is in progress. Every day at settlement time for several seconds, the system calculates user profits and updates balances. That time trading is paused for several seconds till the calculation is completed. + '10043': InvalidOrder, # 'price_wrong_tick' Price has to be rounded to a certain tick size. + '10044': InvalidOrder, # 'stop_price_wrong_tick' Stop Price has to be rounded to a certain tick size. + '10045': InvalidOrder, # 'can_not_cancel_liquidation_order' Liquidation order can't be canceled. + '10046': InvalidOrder, # 'can_not_edit_liquidation_order' Liquidation order can't be edited. + '10047': DDoSProtection, # 'matching_engine_queue_full' Reached limit of pending Matching Engine requests for user. + '10048': ExchangeError, # 'not_on_self_server' The requested operation is not available on self server. + '11008': InvalidOrder, # 'already_filled' This request is not allowed in regards to the filled order. + '11029': BadRequest, # 'invalid_arguments' Some invalid input has been detected. + '11030': ExchangeError, # 'other_reject ' Some rejects which are not considered often, more info may be specified in . + '11031': ExchangeError, # 'other_error ' Some errors which are not considered often, more info may be specified in . + '11035': DDoSProtection, # 'no_more_stops ' Allowed amount of stop orders has been exceeded. + '11036': InvalidOrder, # 'invalid_stoppx_for_index_or_last' Invalid StopPx(too high or too low) current index or market. + '11037': BadRequest, # 'outdated_instrument_for_IV_order' Instrument already not available for trading. + '11038': InvalidOrder, # 'no_adv_for_futures' Advanced orders are not available for futures. + '11039': InvalidOrder, # 'no_adv_postonly' Advanced post-only orders are not supported yet. + '11041': InvalidOrder, # 'not_adv_order' Advanced order properties can't be set if the order is not advanced. + '11042': PermissionDenied, # 'permission_denied' Permission for the operation has been denied. + '11043': BadRequest, # 'bad_argument' Bad argument has been passed. + '11044': InvalidOrder, # 'not_open_order' Attempt to do open order operations with the not open order. + '11045': BadRequest, # 'invalid_event' Event name has not been recognized. + '11046': BadRequest, # 'outdated_instrument' At several minutes to instrument expiration, corresponding advanced implied volatility orders are not allowed. + '11047': BadRequest, # 'unsupported_arg_combination' The specified combination of arguments is not supported. + '11048': ExchangeError, # 'wrong_max_show_for_option' Wrong Max Show for options. + '11049': BadRequest, # 'bad_arguments' Several bad arguments have been passed. + '11050': BadRequest, # 'bad_request' Request has not been parsed properly. + '11051': OnMaintenance, # 'system_maintenance' System is under maintenance. + '11052': ExchangeError, # 'subscribe_error_unsubscribed' Subscription error. However, subscription may fail without self error, please check list of subscribed channels returned, channels can be not subscribed due to wrong input or lack of permissions. + '11053': ExchangeError, # 'transfer_not_found' Specified transfer is not found. + '11090': InvalidAddress, # 'invalid_addr' Invalid address. + '11091': InvalidAddress, # 'invalid_transfer_address' Invalid addres for the transfer. + '11092': InvalidAddress, # 'address_already_exist' The address already exists. + '11093': DDoSProtection, # 'max_addr_count_exceeded' Limit of allowed addresses has been reached. + '11094': ExchangeError, # 'internal_server_error' Some unhandled error on server. Please report to admin. The details of the request will help to locate the problem. + '11095': ExchangeError, # 'disabled_deposit_address_creation' Deposit address creation has been disabled by admin. + '11096': ExchangeError, # 'address_belongs_to_user' Withdrawal instead of transfer. + '12000': AuthenticationError, # 'bad_tfa' Wrong TFA code + '12001': DDoSProtection, # 'too_many_subaccounts' Limit of subbacounts is reached. + '12002': ExchangeError, # 'wrong_subaccount_name' The input is not allowed of subaccount. + '12998': AuthenticationError, # 'tfa_over_limit' The number of failed TFA attempts is limited. + '12003': AuthenticationError, # 'login_over_limit' The number of failed login attempts is limited. + '12004': AuthenticationError, # 'registration_over_limit' The number of registration requests is limited. + '12005': AuthenticationError, # 'country_is_banned' The country is banned(possibly via IP check). + '12100': ExchangeError, # 'transfer_not_allowed' Transfer is not allowed. Possible wrong direction or other mistake. + '12999': AuthenticationError, # 'tfa_used' TFA code is correct but it is already used. Please, use next code. + '13000': AuthenticationError, # 'invalid_login' Login name is invalid(not allowed or it contains wrong characters). + '13001': AuthenticationError, # 'account_not_activated' Account must be activated. + '13002': PermissionDenied, # 'account_blocked' Account is blocked by admin. + '13003': AuthenticationError, # 'tfa_required' This action requires TFA authentication. + '13004': AuthenticationError, # 'invalid_credentials' Invalid credentials has been used. + '13005': AuthenticationError, # 'pwd_match_error' Password confirmation error. + '13006': AuthenticationError, # 'security_error' Invalid Security Code. + '13007': AuthenticationError, # 'user_not_found' User's security code has been changed or wrong. + '13008': ExchangeError, # 'request_failed' Request failed because of invalid input or internal failure. + '13009': AuthenticationError, # 'unauthorized' Wrong or expired authorization token or bad signature. For example, please check scope of the token, 'connection' scope can't be reused for other connections. + '13010': BadRequest, # 'value_required' Invalid input, missing value. + '13011': BadRequest, # 'value_too_short' Input is too short. + '13012': PermissionDenied, # 'unavailable_in_subaccount' Subaccount restrictions. + '13013': BadRequest, # 'invalid_phone_number' Unsupported or invalid phone number. + '13014': BadRequest, # 'cannot_send_sms' SMS sending failed -- phone number is wrong. + '13015': BadRequest, # 'invalid_sms_code' Invalid SMS code. + '13016': BadRequest, # 'invalid_input' Invalid input. + '13017': ExchangeError, # 'subscription_failed' Subscription hailed, invalid subscription parameters. + '13018': ExchangeError, # 'invalid_content_type' Invalid content type of the request. + '13019': ExchangeError, # 'orderbook_closed' Closed, expired order book. + '13020': ExchangeError, # 'not_found' Instrument is not found, invalid instrument name. + '13021': PermissionDenied, # 'forbidden' Not enough permissions to execute the request, forbidden. + '13025': ExchangeError, # 'method_switched_off_by_admin' API method temporarily switched off by administrator. + '-32602': BadRequest, # 'Invalid params' see JSON-RPC spec. + '-32601': BadRequest, # 'Method not found' see JSON-RPC spec. + '-32700': BadRequest, # 'Parse error' see JSON-RPC spec. + '-32000': BadRequest, # 'Missing params' see JSON-RPC spec. + '11054': InvalidOrder, # 'post_only_reject' post order would be filled immediately + }, + 'precisionMode': TICK_SIZE, + 'options': { + 'code': 'BTC', + 'fetchBalance': { + 'code': 'BTC', + }, + 'transfer': { + 'method': 'privateGetSubmitTransferToSubaccount', # or 'privateGetSubmitTransferToUser' + }, + }, + }) + + def create_expired_option_market(self, symbol: str): + # support expired option contracts + quote = 'USD' + settle = None + optionParts = symbol.split('-') + symbolBase = symbol.split('/') + base = None + expiry = None + if symbol.find('/') > -1: + base = self.safe_string(symbolBase, 0) + expiry = self.safe_string(optionParts, 1) + if symbol.find('USDC') > -1: + base = base + '_USDC' + else: + base = self.safe_string(optionParts, 0) + expiry = self.convert_market_id_expire_date(self.safe_string(optionParts, 1)) + if symbol.find('USDC') > -1: + quote = 'USDC' + settle = 'USDC' + else: + settle = base + splitBase = base + if base.find('_') > -1: + splitSymbol = base.split('_') + splitBase = self.safe_string(splitSymbol, 0) + strike = self.safe_string(optionParts, 2) + optionType = self.safe_string(optionParts, 3) + datetime = self.convert_expire_date(expiry) + timestamp = self.parse8601(datetime) + return { + 'id': base + '-' + self.convert_expire_date_to_market_id_date(expiry) + '-' + strike + '-' + optionType, + 'symbol': splitBase + '/' + quote + ':' + settle + '-' + expiry + '-' + strike + '-' + optionType, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': base, + 'quoteId': quote, + 'settleId': settle, + 'active': False, + 'type': 'option', + 'linear': None, + 'inverse': None, + 'spot': False, + 'swap': False, + 'future': False, + 'option': True, + 'margin': False, + 'contract': True, + 'contractSize': None, + 'expiry': timestamp, + 'expiryDatetime': datetime, + 'optionType': 'call' if (optionType == 'C') else 'put', + 'strike': self.parse_number(strike), + 'precision': { + 'amount': None, + 'price': None, + }, + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'info': None, + } + + def safe_market(self, marketId: Str = None, market: Market = None, delimiter: Str = None, marketType: Str = None) -> MarketInterface: + isOption = (marketId is not None) and ((marketId.endswith('-C')) or (marketId.endswith('-P'))) + if isOption and not (marketId in self.markets_by_id): + # handle expired option contracts + return self.create_expired_option_market(marketId) + return super(deribit, self).safe_market(marketId, market, delimiter, marketType) + + def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://docs.deribit.com/#public-get_time + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = self.publicGetGetTime(params) + # + # { + # "jsonrpc": "2.0", + # "result": 1583922446019, + # "usIn": 1583922446019955, + # "usOut": 1583922446019956, + # "usDiff": 1, + # "testnet": False + # } + # + return self.safe_integer(response, 'result') + + def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://docs.deribit.com/#public-get_currencies + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = self.publicGetGetCurrencies(params) + # + # { + # "jsonrpc": "2.0", + # "result": [ + # { + # "currency": "XRP", + # "network_fee": "1.5e-5", + # "min_withdrawal_fee": "0.0001", + # "apr": "0.0", + # "withdrawal_fee": "0.0001", + # "network_currency": "XRP", + # "coin_type": "XRP", + # "withdrawal_priorities": [], + # "min_confirmations": "1", + # "currency_long": "XRP", + # "in_cross_collateral_pool": False + # }, + # ], + # "usIn": "1760110326693923", + # "usOut": "1760110326944891", + # "usDiff": "250968", + # "testnet": False + # } + # + data = self.safe_list(response, 'result', []) + result: dict = {} + for i in range(0, len(data)): + currency = data[i] + currencyId = self.safe_string(currency, 'currency') + code = self.safe_currency_code(currencyId) + result[code] = self.safe_currency_structure({ + 'info': currency, + 'code': code, + 'id': currencyId, + 'name': self.safe_string(currency, 'currency_long'), + 'active': None, + 'deposit': None, + 'withdraw': None, + 'type': 'crypto', + 'fee': self.safe_number(currency, 'withdrawal_fee'), + 'precision': None, + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + 'deposit': { + 'min': None, + 'max': None, + }, + }, + 'networks': None, + }) + return result + + def code_from_options(self, methodName, params={}): + defaultCode = self.safe_value(self.options, 'code', 'BTC') + options = self.safe_value(self.options, methodName, {}) + code = self.safe_value(options, 'code', defaultCode) + return self.safe_value(params, 'code', code) + + def fetch_status(self, params={}): + """ + the latest known information on the availability of the exchange API + + https://docs.deribit.com/#public-status + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `status structure ` + """ + response = self.publicGetStatus(params) + # + # { + # "jsonrpc": "2.0", + # "result": { + # "locked": "false" # True, partial, False + # }, + # "usIn": 1650641690226788, + # "usOut": 1650641690226836, + # "usDiff": 48, + # "testnet": False + # } + # + result = self.safe_value(response, 'result') + locked = self.safe_string(result, 'locked') + updateTime = self.safe_integer_product(response, 'usIn', 0.001, self.milliseconds()) + return { + 'status': 'ok' if (locked == 'false') else 'maintenance', + 'updated': updateTime, + 'eta': None, + 'url': None, + 'info': response, + } + + def fetch_accounts(self, params={}) -> List[Account]: + """ + fetch all the accounts associated with a profile + + https://docs.deribit.com/#private-get_subaccounts + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `account structures ` indexed by the account type + """ + self.load_markets() + response = self.privateGetGetSubaccounts(params) + # + # { + # "jsonrpc": "2.0", + # "result": [{ + # "username": "someusername", + # "type": "main", + # "system_name": "someusername", + # "security_keys_enabled": False, + # "security_keys_assignments": [], + # "receive_notifications": False, + # "login_enabled": True, + # "is_password": True, + # "id": "238216", + # "email": "pablo@abcdef.com" + # }, + # { + # "username": "someusername_1", + # "type": "subaccount", + # "system_name": "someusername_1", + # "security_keys_enabled": False, + # "security_keys_assignments": [], + # "receive_notifications": False, + # "login_enabled": False, + # "is_password": False, + # "id": "245499", + # "email": "pablo@abcdef.com" + # } + # ], + # "usIn": "1652736468292006", + # "usOut": "1652736468292377", + # "usDiff": "371", + # "testnet": False + # } + # + result = self.safe_value(response, 'result', []) + return self.parse_accounts(result) + + def parse_account(self, account): + # + # { + # "username": "someusername_1", + # "type": "subaccount", + # "system_name": "someusername_1", + # "security_keys_enabled": False, + # "security_keys_assignments": [], + # "receive_notifications": False, + # "login_enabled": False, + # "is_password": False, + # "id": "245499", + # "email": "pablo@abcdef.com" + # } + # + return { + 'info': account, + 'id': self.safe_string(account, 'id'), + 'type': self.safe_string(account, 'type'), + 'code': None, + } + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for deribit + + https://docs.deribit.com/#public-get_currencies + https://docs.deribit.com/#public-get_instruments + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + instrumentsResponses = [] + result = [] + parsedMarkets: dict = {} + fetchAllMarkets = None + fetchAllMarkets, params = self.handle_option_and_params(params, 'fetchMarkets', 'fetchAllMarkets', True) + if fetchAllMarkets: + instrumentsResponse = self.publicGetGetInstruments(params) + instrumentsResponses.append(instrumentsResponse) + else: + currenciesResponse = self.publicGetGetCurrencies(params) + # + # { + # "jsonrpc": "2.0", + # "result": [ + # { + # "withdrawal_priorities": [ + # {value: 0.15, name: "very_low"}, + # {value: 1.5, name: "very_high"}, + # ], + # "withdrawal_fee": 0.0005, + # "min_withdrawal_fee": 0.0005, + # "min_confirmations": 1, + # "fee_precision": 4, + # "currency_long": "Bitcoin", + # "currency": "BTC", + # "coin_type": "BITCOIN" + # } + # ], + # "usIn": 1583761588590479, + # "usOut": 1583761588590544, + # "usDiff": 65, + # "testnet": False + # } + # + currenciesResult = self.safe_value(currenciesResponse, 'result', []) + for i in range(0, len(currenciesResult)): + currencyId = self.safe_string(currenciesResult[i], 'currency') + request: dict = { + 'currency': currencyId, + } + instrumentsResponse = self.publicGetGetInstruments(self.extend(request, params)) + # + # { + # "jsonrpc":"2.0", + # "result":[ + # { + # "tick_size":0.0005, + # "taker_commission":0.0003, + # "strike":52000.0, + # "settlement_period":"month", + # "settlement_currency":"BTC", + # "quote_currency":"BTC", + # "option_type":"put", # put, call + # "min_trade_amount":0.1, + # "maker_commission":0.0003, + # "kind":"option", + # "is_active":true, + # "instrument_name":"BTC-24JUN22-52000-P", + # "expiration_timestamp":1656057600000, + # "creation_timestamp":1648199543000, + # "counter_currency":"USD", + # "contract_size":1.0, + # "block_trade_commission":0.0003, + # "base_currency":"BTC" + # }, + # { + # "tick_size":0.5, + # "taker_commission":0.0005, + # "settlement_period":"month", # month, week + # "settlement_currency":"BTC", + # "quote_currency":"USD", + # "min_trade_amount":10.0, + # "max_liquidation_commission":0.0075, + # "max_leverage":50, + # "maker_commission":0.0, + # "kind":"future", + # "is_active":true, + # "instrument_name":"BTC-27MAY22", + # "future_type":"reversed", + # "expiration_timestamp":1653638400000, + # "creation_timestamp":1648195209000, + # "counter_currency":"USD", + # "contract_size":10.0, + # "block_trade_commission":0.0001, + # "base_currency":"BTC" + # }, + # { + # "tick_size":0.5, + # "taker_commission":0.0005, + # "settlement_period":"perpetual", + # "settlement_currency":"BTC", + # "quote_currency":"USD", + # "min_trade_amount":10.0, + # "max_liquidation_commission":0.0075, + # "max_leverage":50, + # "maker_commission":0.0, + # "kind":"future", + # "is_active":true, + # "instrument_name":"BTC-PERPETUAL", + # "future_type":"reversed", + # "expiration_timestamp":32503708800000, + # "creation_timestamp":1534242287000, + # "counter_currency":"USD", + # "contract_size":10.0, + # "block_trade_commission":0.0001, + # "base_currency":"BTC" + # }, + # ], + # "usIn":1648691472831791, + # "usOut":1648691472831896, + # "usDiff":105, + # "testnet":false + # } + # + instrumentsResponses.append(instrumentsResponse) + for i in range(0, len(instrumentsResponses)): + instrumentsResult = self.safe_value(instrumentsResponses[i], 'result', []) + for k in range(0, len(instrumentsResult)): + market = instrumentsResult[k] + kind = self.safe_string(market, 'kind') + isSpot = (kind == 'spot') + id = self.safe_string(market, 'instrument_name') + baseId = self.safe_string(market, 'base_currency') + quoteId = self.safe_string(market, 'counter_currency') + settleId = self.safe_string(market, 'settlement_currency') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + settle = self.safe_currency_code(settleId) + settlementPeriod = self.safe_value(market, 'settlement_period') + swap = (settlementPeriod == 'perpetual') + future = not swap and (kind.find('future') >= 0) + option = (kind.find('option') >= 0) + isComboMarket = kind.find('combo') >= 0 + expiry = self.safe_integer(market, 'expiration_timestamp') + strike = None + optionType = None + symbol = id + type = 'swap' + if future: + type = 'future' + elif option: + type = 'option' + elif isSpot: + type = 'spot' + inverse = None + linear = None + if isSpot: + symbol = base + '/' + quote + elif not isComboMarket: + symbol = base + '/' + quote + ':' + settle + if option or future: + symbol = symbol + '-' + self.yymmdd(expiry, '') + if option: + strike = self.safe_number(market, 'strike') + optionType = self.safe_string(market, 'option_type') + letter = 'C' if (optionType == 'call') else 'P' + symbol = symbol + '-' + self.number_to_string(strike) + '-' + letter + inverse = (quote != settle) + linear = (settle == quote) + parsedMarketValue = self.safe_value(parsedMarkets, symbol) + if parsedMarketValue: + continue + parsedMarkets[symbol] = True + minTradeAmount = self.safe_number(market, 'min_trade_amount') + tickSize = self.safe_number(market, 'tick_size') + result.append({ + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': type, + 'spot': isSpot, + 'margin': False, + 'swap': swap, + 'future': future, + 'option': option, + 'active': self.safe_value(market, 'is_active'), + 'contract': not isSpot, + 'linear': linear, + 'inverse': inverse, + 'taker': self.safe_number(market, 'taker_commission'), + 'maker': self.safe_number(market, 'maker_commission'), + 'contractSize': self.safe_number(market, 'contract_size'), + 'expiry': expiry, + 'expiryDatetime': self.iso8601(expiry), + 'strike': strike, + 'optionType': optionType, + 'precision': { + 'amount': minTradeAmount, + 'price': tickSize, + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': minTradeAmount, + 'max': None, + }, + 'price': { + 'min': tickSize, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': self.safe_integer(market, 'creation_timestamp'), + 'info': market, + }) + return result + + def parse_balance(self, balance) -> Balances: + result: dict = { + 'info': balance, + } + summaries = [] + if 'summaries' in balance: + summaries = self.safe_list(balance, 'summaries') + else: + summaries = [balance] + for i in range(0, len(summaries)): + data = summaries[i] + currencyId = self.safe_string(data, 'currency') + currencyCode = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(data, 'available_funds') + account['used'] = self.safe_string(data, 'maintenance_margin') + account['total'] = self.safe_string(data, 'equity') + result[currencyCode] = account + return self.safe_balance(result) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://docs.deribit.com/#private-get_account_summary + https://docs.deribit.com/#private-get_account_summaries + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.code]: unified currency code of the currency for the balance, if defined 'privateGetGetAccountSummary' will be used, otherwise 'privateGetGetAccountSummaries' will be used + :returns dict: a `balance structure ` + """ + self.load_markets() + code = self.safe_string(params, 'code') + params = self.omit(params, 'code') + request: dict = { + } + if code is not None: + request['currency'] = self.currency_id(code) + response = None + if code is None: + response = self.privateGetGetAccountSummaries(params) + else: + response = self.privateGetGetAccountSummary(self.extend(request, params)) + # + # { + # "jsonrpc": "2.0", + # "result": { + # "total_pl": 0, + # "session_upl": 0, + # "session_rpl": 0, + # "session_funding": 0, + # "portfolio_margining_enabled": False, + # "options_vega": 0, + # "options_theta": 0, + # "options_session_upl": 0, + # "options_session_rpl": 0, + # "options_pl": 0, + # "options_gamma": 0, + # "options_delta": 0, + # "margin_balance": 0.00062359, + # "maintenance_margin": 0, + # "limits": { + # "non_matching_engine_burst": 300, + # "non_matching_engine": 200, + # "matching_engine_burst": 20, + # "matching_engine": 2 + # }, + # "initial_margin": 0, + # "futures_session_upl": 0, + # "futures_session_rpl": 0, + # "futures_pl": 0, + # "equity": 0.00062359, + # "deposit_address": "13tUtNsJSZa1F5GeCmwBywVrymHpZispzw", + # "delta_total": 0, + # "currency": "BTC", + # "balance": 0.00062359, + # "available_withdrawal_funds": 0.00062359, + # "available_funds": 0.00062359 + # }, + # "usIn": 1583775838115975, + # "usOut": 1583775838116520, + # "usDiff": 545, + # "testnet": False + # } + # + result = self.safe_dict(response, 'result', {}) + return self.parse_balance(result) + + def create_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + create a currency deposit address + + https://docs.deribit.com/#private-create_deposit_address + + :param str code: unified currency code of the currency for the deposit address + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + } + response = self.privateGetCreateDepositAddress(self.extend(request, params)) + # + # { + # "jsonrpc": "2.0", + # "id": 7538, + # "result": { + # "address": "2N8udZGBc1hLRCFsU9kGwMPpmYUwMFTuCwB", + # "creation_timestamp": 1550575165170, + # "currency": "BTC", + # "type": "deposit" + # } + # } + # + result = self.safe_value(response, 'result', {}) + address = self.safe_string(result, 'address') + self.check_address(address) + return { + 'currency': code, + 'address': address, + 'tag': None, + 'network': None, + 'info': response, + } + + def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://docs.deribit.com/#private-get_current_deposit_address + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + } + response = self.privateGetGetCurrentDepositAddress(self.extend(request, params)) + # + # { + # "jsonrpc": "2.0", + # "result": { + # "type": "deposit", + # "status": "ready", + # "requires_confirmation": True, + # "currency": "BTC", + # "creation_timestamp": 1514694684651, + # "address": "13tUtNsJSZa1F5GeCmwBywVrymHpZispzw" + # }, + # "usIn": 1583785137274288, + # "usOut": 1583785137274454, + # "usDiff": 166, + # "testnet": False + # } + # + result = self.safe_value(response, 'result', {}) + address = self.safe_string(result, 'address') + self.check_address(address) + return { + 'info': response, + 'currency': code, + 'network': None, + 'address': address, + 'tag': None, + } + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # fetchTicker /public/ticker + # + # { + # "timestamp": 1583778859480, + # "stats": {volume: 60627.57263769, low: 7631.5, high: 8311.5}, + # "state": "open", + # "settlement_price": 7903.21, + # "open_interest": 111543850, + # "min_price": 7634, + # "max_price": 7866.51, + # "mark_price": 7750.02, + # "last_price": 7750.5, + # "instrument_name": "BTC-PERPETUAL", + # "index_price": 7748.01, + # "funding_8h": 0.0000026, + # "current_funding": 0, + # "best_bid_price": 7750, + # "best_bid_amount": 19470, + # "best_ask_price": 7750.5, + # "best_ask_amount": 343280 + # } + # + # fetchTicker /public/get_book_summary_by_instrument + # fetchTickers /public/get_book_summary_by_currency + # + # { + # "volume": 124.1, + # "underlying_price": 7856.445926872601, + # "underlying_index": "SYN.BTC-10MAR20", + # "quote_currency": "USD", + # "open_interest": 121.8, + # "mid_price": 0.01975, + # "mark_price": 0.01984559, + # "low": 0.0095, + # "last": 0.0205, + # "interest_rate": 0, + # "instrument_name": "BTC-10MAR20-7750-C", + # "high": 0.0295, + # "estimated_delivery_price": 7856.29, + # "creation_timestamp": 1583783678366, + # "bid_price": 0.0185, + # "base_currency": "BTC", + # "ask_price": 0.021 + # }, + # + timestamp = self.safe_integer_2(ticker, 'timestamp', 'creation_timestamp') + marketId = self.safe_string(ticker, 'instrument_name') + symbol = self.safe_symbol(marketId, market) + last = self.safe_string_2(ticker, 'last_price', 'last') + stats = self.safe_value(ticker, 'stats', ticker) + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string_2(stats, 'high', 'max_price'), + 'low': self.safe_string_2(stats, 'low', 'min_price'), + 'bid': self.safe_string_2(ticker, 'best_bid_price', 'bid_price'), + 'bidVolume': self.safe_string(ticker, 'best_bid_amount'), + 'ask': self.safe_string_2(ticker, 'best_ask_price', 'ask_price'), + 'askVolume': self.safe_string(ticker, 'best_ask_amount'), + 'vwap': None, + 'open': None, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': None, + 'quoteVolume': self.safe_string(stats, 'volume'), + 'markPrice': self.safe_string(ticker, 'mark_price'), + 'indexPrice': self.safe_string(ticker, 'index_price'), + 'info': ticker, + }, market) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://docs.deribit.com/#public-ticker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'instrument_name': market['id'], + } + response = self.publicGetTicker(self.extend(request, params)) + # + # { + # "jsonrpc": "2.0", + # "result": { + # "timestamp": 1583778859480, + # "stats": {volume: 60627.57263769, low: 7631.5, high: 8311.5}, + # "state": "open", + # "settlement_price": 7903.21, + # "open_interest": 111543850, + # "min_price": 7634, + # "max_price": 7866.51, + # "mark_price": 7750.02, + # "last_price": 7750.5, + # "instrument_name": "BTC-PERPETUAL", + # "index_price": 7748.01, + # "funding_8h": 0.0000026, + # "current_funding": 0, + # "best_bid_price": 7750, + # "best_bid_amount": 19470, + # "best_ask_price": 7750.5, + # "best_ask_amount": 343280 + # }, + # "usIn": 1583778859483941, + # "usOut": 1583778859484075, + # "usDiff": 134, + # "testnet": False + # } + # + result = self.safe_dict(response, 'result') + return self.parse_ticker(result, market) + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://docs.deribit.com/#public-get_book_summary_by_currency + + :param str[] [symbols]: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.code]: *required* the currency code to fetch the tickers for, eg. 'BTC', 'ETH' + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + symbols = self.market_symbols(symbols) + code = self.safe_string_2(params, 'code', 'currency') + type = None + params = self.omit(params, ['code']) + if symbols is not None: + for i in range(0, len(symbols)): + market = self.market(symbols[i]) + if code is not None and code != market['base']: + raise BadRequest(self.id + ' fetchTickers the base currency must be the same for all symbols, self endpoint only supports one base currency at a time. Read more about it here: https://docs.deribit.com/#public-get_book_summary_by_currency') + if code is None: + code = market['base'] + type = market['type'] + if code is None: + raise ArgumentsRequired(self.id + ' fetchTickers requires a currency/code(eg: BTC/ETH/USDT) parameter to fetch tickers for') + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + } + if type is not None: + requestType = None + if type == 'spot': + requestType = 'spot' + elif type == 'future' or (type == 'contract'): + requestType = 'future' + elif type == 'option': + requestType = 'option' + if requestType is not None: + request['kind'] = requestType + response = self.publicGetGetBookSummaryByCurrency(self.extend(request, params)) + # + # { + # "jsonrpc": "2.0", + # "result": [ + # { + # "volume": 124.1, + # "underlying_price": 7856.445926872601, + # "underlying_index": "SYN.BTC-10MAR20", + # "quote_currency": "USD", + # "open_interest": 121.8, + # "mid_price": 0.01975, + # "mark_price": 0.01984559, + # "low": 0.0095, + # "last": 0.0205, + # "interest_rate": 0, + # "instrument_name": "BTC-10MAR20-7750-C", + # "high": 0.0295, + # "estimated_delivery_price": 7856.29, + # "creation_timestamp": 1583783678366, + # "bid_price": 0.0185, + # "base_currency": "BTC", + # "ask_price": 0.021 + # }, + # ], + # "usIn": 1583783678361966, + # "usOut": 1583783678372069, + # "usDiff": 10103, + # "testnet": False + # } + # + result = self.safe_list(response, 'result', []) + tickers: dict = {} + for i in range(0, len(result)): + ticker = self.parse_ticker(result[i]) + symbol = ticker['symbol'] + tickers[symbol] = ticker + return self.filter_by_array_tickers(tickers, 'symbol', symbols) + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://docs.deribit.com/#public-get_tradingview_chart_data + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: whether to paginate the results, set to False by default + :param int [params.until]: the latest time in ms to fetch ohlcv for + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate') + if paginate: + return self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params, 5000) + market = self.market(symbol) + request: dict = { + 'instrument_name': market['id'], + 'resolution': self.safe_string(self.timeframes, timeframe, timeframe), + } + duration = self.parse_timeframe(timeframe) + now = self.milliseconds() + if since is None: + if limit is None: + limit = 1000 # at max, it provides 5000 bars, but we set generous default here + request['start_timestamp'] = now - (limit - 1) * duration * 1000 + request['end_timestamp'] = now + else: + since = max(since - 1, 0) + request['start_timestamp'] = since + if limit is None: + request['end_timestamp'] = now + else: + request['end_timestamp'] = self.sum(since, limit * duration * 1000) + until = self.safe_integer(params, 'until') + if until is not None: + params = self.omit(params, 'until') + request['end_timestamp'] = until + response = self.publicGetGetTradingviewChartData(self.extend(request, params)) + # + # { + # "jsonrpc": "2.0", + # "result": { + # "volume": [3.6680847969999992, 22.682721123, 3.011587939, 0], + # "ticks": [1583916960000, 1583917020000, 1583917080000, 1583917140000], + # "status": "ok", + # "open": [7834, 7839, 7833.5, 7833], + # "low": [7834, 7833.5, 7832.5, 7833], + # "high": [7839.5, 7839, 7833.5, 7833], + # "cost": [28740, 177740, 23590, 0], + # "close": [7839.5, 7833.5, 7833, 7833] + # }, + # "usIn": 1583917166709801, + # "usOut": 1583917166710175, + # "usDiff": 374, + # "testnet": False + # } + # + result = self.safe_value(response, 'result', {}) + ohlcvs = self.convert_trading_view_to_ohlcv(result, 'ticks', 'open', 'high', 'low', 'close', 'volume', True) + return self.parse_ohlcvs(ohlcvs, market, timeframe, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(public) + # + # { + # "trade_seq":132564271, + # "trade_id":"195402220", + # "timestamp":1639684927932, + # "tick_direction":0, + # "price":47946.5, + # "mark_price":47944.13, + # "instrument_name":"BTC-PERPETUAL", + # "index_price":47925.45, + # "direction":"buy", + # "amount":580.0 + # } + # + # + # fetchMyTrades, fetchOrderTrades(private) + # + # { + # "trade_seq": 3, + # "trade_id": "ETH-34066", + # "timestamp": 1550219814585, + # "tick_direction": 1, + # "state": "open", + # "self_trade": False, + # "reduce_only": False, + # "price": 0.04, + # "post_only": False, + # "order_type": "limit", + # "order_id": "ETH-334607", + # "matching_id": null, + # "liquidity": "M", + # "iv": 56.83, + # "instrument_name": "ETH-22FEB19-120-C", + # "index_price": 121.37, + # "fee_currency": "ETH", + # "fee": 0.0011, + # "direction": "buy", + # "amount": 11 + # } + # + id = self.safe_string(trade, 'trade_id') + marketId = self.safe_string(trade, 'instrument_name') + symbol = self.safe_symbol(marketId, market) + timestamp = self.safe_integer(trade, 'timestamp') + side = self.safe_string(trade, 'direction') + priceString = self.safe_string(trade, 'price') + market = self.safe_market(marketId, market) + # Amount for inverse perpetual and futures is in USD which in ccxt is the cost + # For options amount and linear is in corresponding cryptocurrency contracts, e.g., BTC or ETH + amount = self.safe_string(trade, 'amount') + cost = Precise.string_mul(amount, priceString) + if market['inverse']: + cost = Precise.string_div(amount, priceString) + liquidity = self.safe_string(trade, 'liquidity') + takerOrMaker = None + if liquidity is not None: + # M = maker, T = taker, MT = both + takerOrMaker = 'maker' if (liquidity == 'M') else 'taker' + feeCostString = self.safe_string(trade, 'fee') + fee = None + if feeCostString is not None: + feeCurrencyId = self.safe_string(trade, 'fee_currency') + feeCurrencyCode = self.safe_currency_code(feeCurrencyId) + fee = { + 'cost': feeCostString, + 'currency': feeCurrencyCode, + } + return self.safe_trade({ + 'id': id, + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'order': self.safe_string(trade, 'order_id'), + 'type': self.safe_string(trade, 'order_type'), + 'side': side, + 'takerOrMaker': takerOrMaker, + 'price': priceString, + 'amount': amount, + 'cost': cost, + 'fee': fee, + }, market) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + + https://docs.deribit.com/#public-get_last_trades_by_instrument + https://docs.deribit.com/#public-get_last_trades_by_instrument_and_time + + get the list of most recent trades for a particular symbol. + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch trades for + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'instrument_name': market['id'], + 'include_old': True, + } + if since is not None: + request['start_timestamp'] = since + if limit is not None: + request['count'] = min(limit, 1000) # default 10 + until = self.safe_integer_2(params, 'until', 'end_timestamp') + if until is not None: + params = self.omit(params, ['until']) + request['end_timestamp'] = until + response = None + if (since is None) and not ('end_timestamp' in request): + response = self.publicGetGetLastTradesByInstrument(self.extend(request, params)) + else: + response = self.publicGetGetLastTradesByInstrumentAndTime(self.extend(request, params)) + # + # { + # "jsonrpc":"2.0", + # "result": { + # "trades": [ + # { + # "trade_seq":132564271, + # "trade_id":"195402220", + # "timestamp":1639684927932, + # "tick_direction":0, + # "price":47946.5, + # "mark_price":47944.13, + # "instrument_name":"BTC-PERPETUAL", + # "index_price":47925.45, + # "direction":"buy","amount":580.0 + # } + # ], + # "has_more":true + # }, + # "usIn":1639684931934671, + # "usOut":1639684931935337, + # "usDiff":666, + # "testnet":false + # } + # + result = self.safe_value(response, 'result', {}) + trades = self.safe_list(result, 'trades', []) + return self.parse_trades(trades, market, since, limit) + + def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://docs.deribit.com/#private-get_account_summary + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + self.load_markets() + code = self.code_from_options('fetchTradingFees', params) + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + 'extended': True, + } + response = self.privateGetGetAccountSummary(self.extend(request, params)) + # + # { + # "jsonrpc": "2.0", + # "result": { + # "total_pl": 0, + # "session_upl": 0, + # "session_rpl": 0, + # "session_funding": 0, + # "portfolio_margining_enabled": False, + # "options_vega": 0, + # "options_theta": 0, + # "options_session_upl": 0, + # "options_session_rpl": 0, + # "options_pl": 0, + # "options_gamma": 0, + # "options_delta": 0, + # "margin_balance": 0.00062359, + # "maintenance_margin": 0, + # "limits": { + # "non_matching_engine_burst": 300, + # "non_matching_engine": 200, + # "matching_engine_burst": 20, + # "matching_engine": 2 + # }, + # "initial_margin": 0, + # "futures_session_upl": 0, + # "futures_session_rpl": 0, + # "futures_pl": 0, + # "equity": 0.00062359, + # "deposit_address": "13tUtNsJSZa1F5GeCmwBywVrymHpZispzw", + # "delta_total": 0, + # "currency": "BTC", + # "balance": 0.00062359, + # "available_withdrawal_funds": 0.00062359, + # "available_funds": 0.00062359, + # "fees": [ + # "currency": '', + # "instrument_type": "perpetual", + # "fee_type": "relative", + # "maker_fee": 0, + # "taker_fee": 0, + # ], + # }, + # "usIn": 1583775838115975, + # "usOut": 1583775838116520, + # "usDiff": 545, + # "testnet": False + # } + # + result = self.safe_value(response, 'result', {}) + fees = self.safe_value(result, 'fees', []) + perpetualFee: dict = {} + futureFee: dict = {} + optionFee: dict = {} + for i in range(0, len(fees)): + fee = fees[i] + instrumentType = self.safe_string(fee, 'instrument_type') + if instrumentType == 'future': + futureFee = { + 'info': fee, + 'maker': self.safe_number(fee, 'maker_fee'), + 'taker': self.safe_number(fee, 'taker_fee'), + } + elif instrumentType == 'perpetual': + perpetualFee = { + 'info': fee, + 'maker': self.safe_number(fee, 'maker_fee'), + 'taker': self.safe_number(fee, 'taker_fee'), + } + elif instrumentType == 'option': + optionFee = { + 'info': fee, + 'maker': self.safe_number(fee, 'maker_fee'), + 'taker': self.safe_number(fee, 'taker_fee'), + } + parsedFees: dict = {} + for i in range(0, len(self.symbols)): + symbol = self.symbols[i] + market = self.market(symbol) + fee: dict = { + 'info': market, + 'symbol': symbol, + 'percentage': True, + 'tierBased': True, + 'maker': market['maker'], + 'taker': market['taker'], + } + if market['swap']: + fee = self.extend(fee, perpetualFee) + elif market['future']: + fee = self.extend(fee, futureFee) + elif market['option']: + fee = self.extend(fee, optionFee) + parsedFees[symbol] = fee + return parsedFees + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://docs.deribit.com/#public-get_order_book + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'instrument_name': market['id'], + } + if limit is not None: + request['depth'] = limit + response = self.publicGetGetOrderBook(self.extend(request, params)) + # + # { + # "jsonrpc": "2.0", + # "result": { + # "timestamp": 1583781354740, + # "stats": {volume: 61249.66735634, low: 7631.5, high: 8311.5}, + # "state": "open", + # "settlement_price": 7903.21, + # "open_interest": 111536690, + # "min_price": 7695.13, + # "max_price": 7929.49, + # "mark_price": 7813.06, + # "last_price": 7814.5, + # "instrument_name": "BTC-PERPETUAL", + # "index_price": 7810.12, + # "funding_8h": 0.0000031, + # "current_funding": 0, + # "change_id": 17538025952, + # "bids": [ + # [7814, 351820], + # [7813.5, 207490], + # [7813, 32160], + # ], + # "best_bid_price": 7814, + # "best_bid_amount": 351820, + # "best_ask_price": 7814.5, + # "best_ask_amount": 11880, + # "asks": [ + # [7814.5, 11880], + # [7815, 18100], + # [7815.5, 2640], + # ], + # }, + # "usIn": 1583781354745804, + # "usOut": 1583781354745932, + # "usDiff": 128, + # "testnet": False + # } + # + result = self.safe_value(response, 'result', {}) + timestamp = self.safe_integer(result, 'timestamp') + nonce = self.safe_integer(result, 'change_id') + orderbook = self.parse_order_book(result, market['symbol'], timestamp) + orderbook['nonce'] = nonce + return orderbook + + def parse_order_status(self, status: Str): + statuses: dict = { + 'open': 'open', + 'cancelled': 'canceled', + 'filled': 'closed', + 'rejected': 'rejected', + 'untriggered': 'open', + } + return self.safe_string(statuses, status, status) + + def parse_time_in_force(self, timeInForce: Str): + timeInForces: dict = { + 'good_til_cancelled': 'GTC', + 'fill_or_kill': 'FOK', + 'immediate_or_cancel': 'IOC', + } + return self.safe_string(timeInForces, timeInForce, timeInForce) + + def parse_order_type(self, orderType): + orderTypes: dict = { + 'stop_limit': 'limit', + 'take_limit': 'limit', + 'stop_market': 'market', + 'take_market': 'market', + } + return self.safe_string(orderTypes, orderType, orderType) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder + # + # { + # "time_in_force": "good_til_cancelled", + # "reduce_only": False, + # "profit_loss": 0, + # "price": "market_price", + # "post_only": False, + # "order_type": "market", + # "order_state": "filled", + # "order_id": "ETH-349249", + # "max_show": 40, + # "last_update_timestamp": 1550657341322, + # "label": "market0000234", + # "is_liquidation": False, + # "instrument_name": "ETH-PERPETUAL", + # "filled_amount": 40, + # "direction": "buy", + # "creation_timestamp": 1550657341322, + # "commission": 0.000139, + # "average_price": 143.81, + # "api": True, + # "amount": 40, + # "trades": [], # injected by createOrder + # } + # + marketId = self.safe_string(order, 'instrument_name') + market = self.safe_market(marketId, market) + timestamp = self.safe_integer(order, 'creation_timestamp') + lastUpdate = self.safe_integer(order, 'last_update_timestamp') + id = self.safe_string(order, 'order_id') + priceString = self.safe_string(order, 'price') + if priceString == 'market_price': + priceString = None + averageString = self.safe_string(order, 'average_price') + # Inverse contracts amount is in USD which in ccxt is the cost + # For options and Linear contracts amount is in corresponding cryptocurrency, e.g., BTC or ETH + filledString = self.safe_string(order, 'filled_amount') + amount = self.safe_string(order, 'amount') + cost = Precise.string_mul(filledString, averageString) + if self.safe_bool(market, 'inverse'): + if averageString != '0': + cost = Precise.string_div(amount, averageString) + lastTradeTimestamp = None + if filledString is not None: + isFilledPositive = Precise.string_gt(filledString, '0') + if isFilledPositive: + lastTradeTimestamp = lastUpdate + status = self.parse_order_status(self.safe_string(order, 'order_state')) + side = self.safe_string_lower(order, 'direction') + feeCostString = self.safe_string(order, 'commission') + fee = None + if feeCostString is not None: + feeCostString = Precise.string_abs(feeCostString) + fee = { + 'cost': feeCostString, + 'currency': market['base'], + } + rawType = self.safe_string(order, 'order_type') + type = self.parse_order_type(rawType) + # injected in createOrder + trades = self.safe_value(order, 'trades') + timeInForce = self.parse_time_in_force(self.safe_string(order, 'time_in_force')) + postOnly = self.safe_value(order, 'post_only') + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': lastTradeTimestamp, + 'symbol': market['symbol'], + 'type': type, + 'timeInForce': timeInForce, + 'postOnly': postOnly, + 'side': side, + 'price': priceString, + 'triggerPrice': self.safe_value(order, 'stop_price'), + 'amount': amount, + 'cost': cost, + 'average': averageString, + 'filled': filledString, + 'remaining': None, + 'status': status, + 'fee': fee, + 'trades': trades, + }, market) + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://docs.deribit.com/#private-get_order_state + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + request: dict = { + 'order_id': id, + } + market = None + if symbol is not None: + market = self.market(symbol) + response = self.privateGetGetOrderState(self.extend(request, params)) + # + # { + # "jsonrpc": "2.0", + # "id": 4316, + # "result": { + # "time_in_force": "good_til_cancelled", + # "reduce_only": False, + # "profit_loss": 0.051134, + # "price": 118.94, + # "post_only": False, + # "order_type": "limit", + # "order_state": "filled", + # "order_id": "ETH-331562", + # "max_show": 37, + # "last_update_timestamp": 1550219810944, + # "label": "", + # "is_liquidation": False, + # "instrument_name": "ETH-PERPETUAL", + # "filled_amount": 37, + # "direction": "sell", + # "creation_timestamp": 1550219749176, + # "commission": 0.000031, + # "average_price": 118.94, + # "api": False, + # "amount": 37 + # } + # } + # + result = self.safe_dict(response, 'result') + return self.parse_order(result, market) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://docs.deribit.com/#private-buy + https://docs.deribit.com/#private-sell + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency. For perpetual and inverse futures the amount is in USD units. For options it is in the underlying assets base currency. + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.trigger]: the trigger type 'index_price', 'mark_price', or 'last_price', default is 'last_price' + :param float [params.trailingAmount]: the quote amount to trail away from the current market price + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'instrument_name': market['id'], + 'amount': self.amount_to_precision(symbol, amount), + 'type': type, # limit, stop_limit, market, stop_market, default is limit + # 'label': 'string', # user-defined label for the order(maximum 64 characters) + # 'price': self.price_to_precision(symbol, 123.45), # only for limit and stop_limit orders + # 'time_in_force' : 'good_til_cancelled', # fill_or_kill, immediate_or_cancel + # 'max_show': 123.45, # max amount within an order to be shown to other customers, 0 for invisible order + # 'post_only': False, # if the new price would cause the order to be filled immediately(as taker), the price will be changed to be just below the spread. + # 'reject_post_only': False, # if True the order is put to order book unmodified or request is rejected + # 'reduce_only': False, # if True, the order is intended to only reduce a current position + # 'stop_price': False, # stop price, required for stop_limit orders + # 'trigger': 'index_price', # mark_price, last_price, required for stop_limit orders + # 'advanced': 'usd', # 'implv', advanced option order type, options only + } + trigger = self.safe_string(params, 'trigger', 'last_price') + timeInForce = self.safe_string_upper(params, 'timeInForce') + reduceOnly = self.safe_value_2(params, 'reduceOnly', 'reduce_only') + # only stop loss sell orders are allowed when price crossed from above + stopLossPrice = self.safe_value(params, 'stopLossPrice') + # only take profit buy orders are allowed when price crossed from below + takeProfitPrice = self.safe_value(params, 'takeProfitPrice') + trailingAmount = self.safe_string_2(params, 'trailingAmount', 'trigger_offset') + isTrailingAmountOrder = trailingAmount is not None + isStopLimit = type == 'stop_limit' + isStopMarket = type == 'stop_market' + isTakeLimit = type == 'take_limit' + isTakeMarket = type == 'take_market' + isStopLossOrder = isStopLimit or isStopMarket or (stopLossPrice is not None) + isTakeProfitOrder = isTakeLimit or isTakeMarket or (takeProfitPrice is not None) + if isStopLossOrder and isTakeProfitOrder: + raise InvalidOrder(self.id + ' createOrder() only allows one of stopLossPrice or takeProfitPrice to be specified') + isStopOrder = isStopLossOrder or isTakeProfitOrder + isLimitOrder = (type == 'limit') or isStopLimit or isTakeLimit + isMarketOrder = (type == 'market') or isStopMarket or isTakeMarket + exchangeSpecificPostOnly = self.safe_value(params, 'post_only') + postOnly = self.is_post_only(isMarketOrder, exchangeSpecificPostOnly, params) + if isLimitOrder: + request['type'] = 'limit' + request['price'] = self.price_to_precision(symbol, price) + else: + request['type'] = 'market' + if isTrailingAmountOrder: + request['trigger'] = trigger + request['type'] = 'trailing_stop' + request['trigger_offset'] = self.parse_to_numeric(trailingAmount) + elif isStopOrder: + triggerPrice = stopLossPrice if (stopLossPrice is not None) else takeProfitPrice + request['trigger_price'] = self.price_to_precision(symbol, triggerPrice) + request['trigger'] = trigger + if isStopLossOrder: + if isMarketOrder: + # stop_market(sell only) + request['type'] = 'stop_market' + else: + # stop_limit(sell only) + request['type'] = 'stop_limit' + else: + if isMarketOrder: + # take_market(buy only) + request['type'] = 'take_market' + else: + # take_limit(buy only) + request['type'] = 'take_limit' + if reduceOnly: + request['reduce_only'] = True + if postOnly: + request['post_only'] = True + request['reject_post_only'] = True + if timeInForce is not None: + if timeInForce == 'GTC': + request['time_in_force'] = 'good_til_cancelled' + if timeInForce == 'IOC': + request['time_in_force'] = 'immediate_or_cancel' + if timeInForce == 'FOK': + request['time_in_force'] = 'fill_or_kill' + params = self.omit(params, ['timeInForce', 'stopLossPrice', 'takeProfitPrice', 'postOnly', 'reduceOnly', 'trailingAmount']) + response = None + if self.capitalize(side) == 'Buy': + response = self.privateGetBuy(self.extend(request, params)) + else: + response = self.privateGetSell(self.extend(request, params)) + # + # { + # "jsonrpc": "2.0", + # "id": 5275, + # "result": { + # "trades": [ + # { + # "trade_seq": 14151, + # "trade_id": "ETH-37435", + # "timestamp": 1550657341322, + # "tick_direction": 2, + # "state": "closed", + # "self_trade": False, + # "price": 143.81, + # "order_type": "market", + # "order_id": "ETH-349249", + # "matching_id": null, + # "liquidity": "T", + # "label": "market0000234", + # "instrument_name": "ETH-PERPETUAL", + # "index_price": 143.73, + # "fee_currency": "ETH", + # "fee": 0.000139, + # "direction": "buy", + # "amount": 40 + # } + # ], + # "order": { + # "time_in_force": "good_til_cancelled", + # "reduce_only": False, + # "profit_loss": 0, + # "price": "market_price", + # "post_only": False, + # "order_type": "market", + # "order_state": "filled", + # "order_id": "ETH-349249", + # "max_show": 40, + # "last_update_timestamp": 1550657341322, + # "label": "market0000234", + # "is_liquidation": False, + # "instrument_name": "ETH-PERPETUAL", + # "filled_amount": 40, + # "direction": "buy", + # "creation_timestamp": 1550657341322, + # "commission": 0.000139, + # "average_price": 143.81, + # "api": True, + # "amount": 40 + # } + # } + # } + # + result = self.safe_value(response, 'result', {}) + order = self.safe_value(result, 'order') + trades = self.safe_value(result, 'trades', []) + order['trades'] = trades + return self.parse_order(order, market) + + def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + """ + edit a trade order + + https://docs.deribit.com/#private-edit + + :param str id: edit order id + :param str [symbol]: unified symbol of the market to edit an order in + :param str [type]: 'market' or 'limit' + :param str [side]: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency. For perpetual and inverse futures the amount is in USD units. For options it is in the underlying assets base currency. + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.trailingAmount]: the quote amount to trail away from the current market price + :returns dict: an `order structure ` + """ + if amount is None: + raise ArgumentsRequired(self.id + ' editOrder() requires an amount argument') + self.load_markets() + request: dict = { + 'order_id': id, + 'amount': self.amount_to_precision(symbol, amount), + # 'post_only': False, # if the new price would cause the order to be filled immediately(as taker), the price will be changed to be just below the spread. + # 'reject_post_only': False, # if True the order is put to order book unmodified or request is rejected + # 'reduce_only': False, # if True, the order is intended to only reduce a current position + # 'stop_price': False, # stop price, required for stop_limit orders + # 'advanced': 'usd', # 'implv', advanced option order type, options only + } + if price is not None: + request['price'] = self.price_to_precision(symbol, price) + trailingAmount = self.safe_string_2(params, 'trailingAmount', 'trigger_offset') + isTrailingAmountOrder = trailingAmount is not None + if isTrailingAmountOrder: + request['trigger_offset'] = self.parse_to_numeric(trailingAmount) + params = self.omit(params, 'trigger_offset') + response = self.privateGetEdit(self.extend(request, params)) + result = self.safe_value(response, 'result', {}) + order = self.safe_value(result, 'order') + trades = self.safe_value(result, 'trades', []) + order['trades'] = trades + return self.parse_order(order) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://docs.deribit.com/#private-cancel + + :param str id: order id + :param str symbol: not used by deribit cancelOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + request: dict = { + 'order_id': id, + } + response = self.privateGetCancel(self.extend(request, params)) + result = self.safe_dict(response, 'result', {}) + return self.parse_order(result) + + def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders + + https://docs.deribit.com/#private-cancel_all + https://docs.deribit.com/#private-cancel_all_by_instrument + + :param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + self.load_markets() + request: dict = {} + response = None + if symbol is None: + response = self.privateGetCancelAll(self.extend(request, params)) + else: + market = self.market(symbol) + request['instrument_name'] = market['id'] + response = self.privateGetCancelAllByInstrument(self.extend(request, params)) + # + # { + # jsonrpc: '2.0', + # result: '1', + # usIn: '1720508354127369', + # usOut: '1720508354133603', + # usDiff: '6234', + # testnet: True + # } + # + return [ + self.safe_order({ + 'info': response, + }), + ] + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://docs.deribit.com/#private-get_open_orders_by_currency + https://docs.deribit.com/#private-get_open_orders_by_instrument + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + request: dict = {} + market = None + response = None + if symbol is None: + code = self.code_from_options('fetchOpenOrders', params) + currency = self.currency(code) + request['currency'] = currency['id'] + response = self.privateGetGetOpenOrdersByCurrency(self.extend(request, params)) + else: + market = self.market(symbol) + request['instrument_name'] = market['id'] + response = self.privateGetGetOpenOrdersByInstrument(self.extend(request, params)) + result = self.safe_list(response, 'result', []) + return self.parse_orders(result, market, since, limit) + + def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://docs.deribit.com/#private-get_order_history_by_currency + https://docs.deribit.com/#private-get_order_history_by_instrument + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + request: dict = {} + market = None + response = None + if limit is not None: + request['count'] = limit + else: + request['count'] = 1000 # max value + if symbol is None: + code = self.code_from_options('fetchClosedOrders', params) + currency = self.currency(code) + request['currency'] = currency['id'] + response = self.privateGetGetOrderHistoryByCurrency(self.extend(request, params)) + else: + market = self.market(symbol) + request['instrument_name'] = market['id'] + response = self.privateGetGetOrderHistoryByInstrument(self.extend(request, params)) + result = self.safe_list(response, 'result', []) + return self.parse_orders(result, market, since, limit) + + def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all the trades made from a single order + + https://docs.deribit.com/#private-get_user_trades_by_order + + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + self.load_markets() + request: dict = { + 'order_id': id, + } + response = self.privateGetGetUserTradesByOrder(self.extend(request, params)) + # + # { + # "jsonrpc": "2.0", + # "id": 9367, + # "result": { + # "trades": [ + # { + # "trade_seq": 3, + # "trade_id": "ETH-34066", + # "timestamp": 1550219814585, + # "tick_direction": 1, + # "state": "open", + # "self_trade": False, + # "reduce_only": False, + # "price": 0.04, + # "post_only": False, + # "order_type": "limit", + # "order_id": "ETH-334607", + # "matching_id": null, + # "liquidity": "M", + # "iv": 56.83, + # "instrument_name": "ETH-22FEB19-120-C", + # "index_price": 121.37, + # "fee_currency": "ETH", + # "fee": 0.0011, + # "direction": "buy", + # "amount": 11 + # }, + # ], + # "has_more": True + # } + # } + # + result = self.safe_list(response, 'result', []) + return self.parse_trades(result, None, since, limit) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://docs.deribit.com/#private-get_user_trades_by_currency + https://docs.deribit.com/#private-get_user_trades_by_currency_and_time + https://docs.deribit.com/#private-get_user_trades_by_instrument + https://docs.deribit.com/#private-get_user_trades_by_instrument_and_time + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + request: dict = { + 'include_old': True, + } + market = None + if limit is not None: + request['count'] = limit # default 10 + response = None + if symbol is None: + code = self.code_from_options('fetchMyTrades', params) + currency = self.currency(code) + request['currency'] = currency['id'] + if since is None: + response = self.privateGetGetUserTradesByCurrency(self.extend(request, params)) + else: + request['start_timestamp'] = since + response = self.privateGetGetUserTradesByCurrencyAndTime(self.extend(request, params)) + else: + market = self.market(symbol) + request['instrument_name'] = market['id'] + if since is None: + response = self.privateGetGetUserTradesByInstrument(self.extend(request, params)) + else: + request['start_timestamp'] = since + response = self.privateGetGetUserTradesByInstrumentAndTime(self.extend(request, params)) + # + # { + # "jsonrpc": "2.0", + # "id": 9367, + # "result": { + # "trades": [ + # { + # "trade_seq": 3, + # "trade_id": "ETH-34066", + # "timestamp": 1550219814585, + # "tick_direction": 1, + # "state": "open", + # "self_trade": False, + # "reduce_only": False, + # "price": 0.04, + # "post_only": False, + # "order_type": "limit", + # "order_id": "ETH-334607", + # "matching_id": null, + # "liquidity": "M", + # "iv": 56.83, + # "instrument_name": "ETH-22FEB19-120-C", + # "index_price": 121.37, + # "fee_currency": "ETH", + # "fee": 0.0011, + # "direction": "buy", + # "amount": 11 + # }, + # ], + # "has_more": True + # } + # } + # + result = self.safe_value(response, 'result', {}) + trades = self.safe_list(result, 'trades', []) + return self.parse_trades(trades, market, since, limit) + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://docs.deribit.com/#private-get_deposits + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + if code is None: + raise ArgumentsRequired(self.id + ' fetchDeposits() requires a currency code argument') + self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + } + if limit is not None: + request['count'] = limit + response = self.privateGetGetDeposits(self.extend(request, params)) + # + # { + # "jsonrpc": "2.0", + # "id": 5611, + # "result": { + # "count": 1, + # "data": [ + # { + # "address": "2N35qDKDY22zmJq9eSyiAerMD4enJ1xx6ax", + # "amount": 5, + # "currency": "BTC", + # "received_timestamp": 1549295017670, + # "state": "completed", + # "transaction_id": "230669110fdaf0a0dbcdc079b6b8b43d5af29cc73683835b9bc6b3406c065fda", + # "updated_timestamp": 1549295130159 + # } + # ] + # } + # } + # + result = self.safe_value(response, 'result', {}) + data = self.safe_list(result, 'data', []) + return self.parse_transactions(data, currency, since, limit, params) + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://docs.deribit.com/#private-get_withdrawals + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + if code is None: + raise ArgumentsRequired(self.id + ' fetchWithdrawals() requires a currency code argument') + self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + } + if limit is not None: + request['count'] = limit + response = self.privateGetGetWithdrawals(self.extend(request, params)) + # + # { + # "jsonrpc": "2.0", + # "id": 2745, + # "result": { + # "count": 1, + # "data": [ + # { + # "address": "2NBqqD5GRJ8wHy1PYyCXTe9ke5226FhavBz", + # "amount": 0.5, + # "confirmed_timestamp": null, + # "created_timestamp": 1550571443070, + # "currency": "BTC", + # "fee": 0.0001, + # "id": 1, + # "priority": 0.15, + # "state": "unconfirmed", + # "transaction_id": null, + # "updated_timestamp": 1550571443070 + # } + # ] + # } + # } + # + result = self.safe_value(response, 'result', {}) + data = self.safe_list(result, 'data', []) + return self.parse_transactions(data, currency, since, limit, params) + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'completed': 'ok', + 'unconfirmed': 'pending', + } + return self.safe_string(statuses, status, status) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fetchWithdrawals + # + # { + # "address": "2NBqqD5GRJ8wHy1PYyCXTe9ke5226FhavBz", + # "amount": 0.5, + # "confirmed_timestamp": null, + # "created_timestamp": 1550571443070, + # "currency": "BTC", + # "fee": 0.0001, + # "id": 1, + # "priority": 0.15, + # "state": "unconfirmed", + # "transaction_id": null, + # "updated_timestamp": 1550571443070 + # } + # + # fetchDeposits + # + # { + # "address": "2N35qDKDY22zmJq9eSyiAerMD4enJ1xx6ax", + # "amount": 5, + # "currency": "BTC", + # "received_timestamp": 1549295017670, + # "state": "completed", + # "transaction_id": "230669110fdaf0a0dbcdc079b6b8b43d5af29cc73683835b9bc6b3406c065fda", + # "updated_timestamp": 1549295130159 + # } + # + currencyId = self.safe_string(transaction, 'currency') + code = self.safe_currency_code(currencyId, currency) + timestamp = self.safe_integer_2(transaction, 'created_timestamp', 'received_timestamp') + updated = self.safe_integer(transaction, 'updated_timestamp') + status = self.parse_transaction_status(self.safe_string(transaction, 'state')) + address = self.safe_string(transaction, 'address') + feeCost = self.safe_number(transaction, 'fee') + type = 'deposit' + fee = None + if feeCost is not None: + type = 'withdrawal' + fee = { + 'cost': feeCost, + 'currency': code, + } + return { + 'info': transaction, + 'id': self.safe_string(transaction, 'id'), + 'txid': self.safe_string(transaction, 'transaction_id'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'address': address, + 'addressTo': address, + 'addressFrom': None, + 'tag': None, + 'tagTo': None, + 'tagFrom': None, + 'type': type, + 'amount': self.safe_number(transaction, 'amount'), + 'currency': code, + 'status': status, + 'updated': updated, + 'network': None, + 'internal': None, + 'comment': None, + 'fee': fee, + } + + def parse_position(self, position: dict, market: Market = None): + # + # { + # "jsonrpc": "2.0", + # "id": 404, + # "result": { + # "average_price": 0, + # "delta": 0, + # "direction": "buy", + # "estimated_liquidation_price": 0, + # "floating_profit_loss": 0, + # "index_price": 3555.86, + # "initial_margin": 0, + # "instrument_name": "BTC-PERPETUAL", + # "leverage": 100, + # "kind": "future", + # "maintenance_margin": 0, + # "mark_price": 3556.62, + # "open_orders_margin": 0.000165889, + # "realized_profit_loss": 0, + # "settlement_price": 3555.44, + # "size": 0, + # "size_currency": 0, + # "total_profit_loss": 0 + # } + # } + # + contract = self.safe_string(position, 'instrument_name') + market = self.safe_market(contract, market) + side = self.safe_string(position, 'direction') + side = 'long' if (side == 'buy') else 'short' + unrealizedPnl = self.safe_string(position, 'floating_profit_loss') + initialMarginString = self.safe_string(position, 'initial_margin') + notionalString = self.safe_string(position, 'size_currency') + notionalStringAbs = Precise.string_abs(notionalString) + maintenanceMarginString = self.safe_string(position, 'maintenance_margin') + return self.safe_position({ + 'info': position, + 'id': None, + 'symbol': self.safe_string(market, 'symbol'), + 'timestamp': None, + 'datetime': None, + 'lastUpdateTimestamp': None, + 'initialMargin': self.parse_number(initialMarginString), + 'initialMarginPercentage': self.parse_number(Precise.string_mul(Precise.string_div(initialMarginString, notionalStringAbs), '100')), + 'maintenanceMargin': self.parse_number(maintenanceMarginString), + 'maintenanceMarginPercentage': self.parse_number(Precise.string_mul(Precise.string_div(maintenanceMarginString, notionalStringAbs), '100')), + 'entryPrice': self.safe_number(position, 'average_price'), + 'notional': self.parse_number(notionalStringAbs), + 'leverage': self.safe_integer(position, 'leverage'), + 'unrealizedPnl': self.parse_number(unrealizedPnl), + 'realizedPnl': self.safe_number(position, 'realized_profit_loss'), + 'contracts': self.safe_number(position, 'size'), + 'contractSize': self.safe_number(position, 'contractSize'), + 'marginRatio': None, + 'liquidationPrice': self.safe_number(position, 'estimated_liquidation_price'), + 'markPrice': self.safe_number(position, 'mark_price'), + 'lastPrice': None, + 'collateral': None, + 'marginMode': None, + 'side': side, + 'percentage': None, + 'hedged': None, + 'stopLossPrice': None, + 'takeProfitPrice': None, + }) + + def fetch_position(self, symbol: str, params={}): + """ + fetch data on a single open contract trade position + + https://docs.deribit.com/#private-get_position + + :param str symbol: unified market symbol of the market the position is held in, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `position structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'instrument_name': market['id'], + } + response = self.privateGetGetPosition(self.extend(request, params)) + # + # { + # "jsonrpc": "2.0", + # "id": 404, + # "result": { + # "average_price": 0, + # "delta": 0, + # "direction": "buy", + # "estimated_liquidation_price": 0, + # "floating_profit_loss": 0, + # "index_price": 3555.86, + # "initial_margin": 0, + # "instrument_name": "BTC-PERPETUAL", + # "leverage": 100, + # "kind": "future", + # "maintenance_margin": 0, + # "mark_price": 3556.62, + # "open_orders_margin": 0.000165889, + # "realized_profit_loss": 0, + # "settlement_price": 3555.44, + # "size": 0, + # "size_currency": 0, + # "total_profit_loss": 0 + # } + # } + # + result = self.safe_dict(response, 'result') + return self.parse_position(result) + + def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://docs.deribit.com/#private-get_positions + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.currency]: currency code filter for positions + :param str [params.kind]: market type filter for positions 'future', 'option', 'spot', 'future_combo' or 'option_combo' + :param int [params.subaccount_id]: the user id for the subaccount + :returns dict[]: a list of `position structure ` + """ + self.load_markets() + code = self.safe_string(params, 'currency') + request: dict = {} + if code is not None: + params = self.omit(params, 'currency') + currency = self.currency(code) + request['currency'] = currency['id'] + response = self.privateGetGetPositions(self.extend(request, params)) + # + # { + # "jsonrpc": "2.0", + # "id": 2236, + # "result": [ + # { + # "average_price": 7440.18, + # "delta": 0.006687487, + # "direction": "buy", + # "estimated_liquidation_price": 1.74, + # "floating_profit_loss": 0, + # "index_price": 7466.79, + # "initial_margin": 0.000197283, + # "instrument_name": "BTC-PERPETUAL", + # "kind": "future", + # "leverage": 34, + # "maintenance_margin": 0.000143783, + # "mark_price": 7476.65, + # "open_orders_margin": 0.000197288, + # "realized_funding": -1e-8, + # "realized_profit_loss": -9e-9, + # "settlement_price": 7476.65, + # "size": 50, + # "size_currency": 0.006687487, + # "total_profit_loss": 0.000032781 + # }, + # ] + # } + # + result = self.safe_list(response, 'result') + return self.parse_positions(result, symbols) + + def fetch_volatility_history(self, code: str, params={}): + """ + fetch the historical volatility of an option market based on an underlying asset + + https://docs.deribit.com/#public-get_historical_volatility + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `volatility history objects ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + } + response = self.publicGetGetHistoricalVolatility(self.extend(request, params)) + # + # { + # "jsonrpc": "2.0", + # "result": [ + # [1640142000000,63.828320460740585], + # [1640142000000,63.828320460740585], + # [1640145600000,64.03821964123213] + # ], + # "usIn": 1641515379467734, + # "usOut": 1641515379468095, + # "usDiff": 361, + # "testnet": False + # } + # + return self.parse_volatility_history(response) + + def parse_volatility_history(self, volatility): + # + # { + # "jsonrpc": "2.0", + # "result": [ + # [1640142000000,63.828320460740585], + # [1640142000000,63.828320460740585], + # [1640145600000,64.03821964123213] + # ], + # "usIn": 1641515379467734, + # "usOut": 1641515379468095, + # "usDiff": 361, + # "testnet": False + # } + # + volatilityResult = self.safe_value(volatility, 'result', []) + result = [] + for i in range(0, len(volatilityResult)): + timestamp = self.safe_integer(volatilityResult[i], 0) + volatilityObj = self.safe_number(volatilityResult[i], 1) + result.append({ + 'info': volatilityObj, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'volatility': volatilityObj, + }) + return result + + def fetch_transfers(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[TransferEntry]: + """ + fetch a history of internal transfers made on an account + + https://docs.deribit.com/#private-get_transfers + + :param str code: unified currency code of the currency transferred + :param int [since]: the earliest time in ms to fetch transfers for + :param int [limit]: the maximum number of transfers structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transfer structures ` + """ + if code is None: + raise ArgumentsRequired(self.id + ' fetchTransfers() requires a currency code argument') + self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + } + if limit is not None: + request['count'] = limit + response = self.privateGetGetTransfers(self.extend(request, params)) + # + # { + # "jsonrpc": "2.0", + # "id": 7606, + # "result": { + # "count": 2, + # "data": [ + # { + # "amount": 0.2, + # "created_timestamp": 1550579457727, + # "currency": "BTC", + # "direction": "payment", + # "id": 2, + # "other_side": "2MzyQc5Tkik61kJbEpJV5D5H9VfWHZK9Sgy", + # "state": "prepared", + # "type": "user", + # "updated_timestamp": 1550579457727 + # }, + # { + # "amount": 0.3, + # "created_timestamp": 1550579255800, + # "currency": "BTC", + # "direction": "payment", + # "id": 1, + # "other_side": "new_user_1_1", + # "state": "confirmed", + # "type": "subaccount", + # "updated_timestamp": 1550579255800 + # } + # ] + # } + # } + # + result = self.safe_value(response, 'result', {}) + transfers = self.safe_list(result, 'data', []) + return self.parse_transfers(transfers, currency, since, limit, params) + + def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://docs.deribit.com/#private-submit_transfer_to_user + https://docs.deribit.com/#private-submit_transfer_to_subaccount + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account to transfer from + :param str toAccount: account to transfer to + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transfer structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'amount': amount, + 'currency': currency['id'], + 'destination': toAccount, + } + method = self.safe_string(params, 'method') + params = self.omit(params, 'method') + if method is None: + transferOptions = self.safe_value(self.options, 'transfer', {}) + method = self.safe_string(transferOptions, 'method', 'privateGetSubmitTransferToSubaccount') + response = None + if method == 'privateGetSubmitTransferToUser': + response = self.privateGetSubmitTransferToUser(self.extend(request, params)) + else: + response = self.privateGetSubmitTransferToSubaccount(self.extend(request, params)) + # + # { + # "jsonrpc": "2.0", + # "id": 9421, + # "result": { + # "updated_timestamp": 1550232862350, + # "type": "user", + # "state": "prepared", + # "other_side": "0x4aa0753d798d668056920094d65321a8e8913e26", + # "id": 3, + # "direction": "payment", + # "currency": "ETH", + # "created_timestamp": 1550232862350, + # "amount": 13.456 + # } + # } + # + result = self.safe_dict(response, 'result', {}) + return self.parse_transfer(result, currency) + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + # + # { + # "updated_timestamp": 1550232862350, + # "type": "user", + # "state": "prepared", + # "other_side": "0x4aa0753d798d668056920094d65321a8e8913e26", + # "id": 3, + # "direction": "payment", + # "currency": "ETH", + # "created_timestamp": 1550232862350, + # "amount": 13.456 + # } + # + timestamp = self.safe_timestamp(transfer, 'created_timestamp') + status = self.safe_string(transfer, 'state') + account = self.safe_string(transfer, 'other_side') + direction = self.safe_string(transfer, 'direction') + currencyId = self.safe_string(transfer, 'currency') + return { + 'info': transfer, + 'id': self.safe_string(transfer, 'id'), + 'status': self.parse_transfer_status(status), + 'amount': self.safe_number(transfer, 'amount'), + 'currency': self.safe_currency_code(currencyId, currency), + 'fromAccount': direction != account if 'payment' else None, + 'toAccount': direction == account if 'payment' else None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + + def parse_transfer_status(self, status: Str) -> Str: + statuses: dict = { + 'prepared': 'pending', + 'confirmed': 'ok', + 'cancelled': 'cancelled', + 'waiting_for_admin': 'pending', + } + return self.safe_string(statuses, status, status) + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://docs.deribit.com/#private-withdraw + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.check_address(address) + self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + 'address': address, # must be in the address book + 'amount': amount, + # 'priority': 'high', # low, mid, high, very_high, extreme_high, insane + # 'tfa': '123456', # if enabled + } + if self.twofa is not None: + request['tfa'] = self.totp(self.twofa) + response = self.privateGetWithdraw(self.extend(request, params)) + return self.parse_transaction(response, currency) + + def parse_deposit_withdraw_fee(self, fee, currency: Currency = None): + # + # { + # "withdrawal_priorities": [], + # "withdrawal_fee": 0.01457324, + # "min_withdrawal_fee": 0.000001, + # "min_confirmations": 1, + # "fee_precision": 8, + # "currency_long": "Solana", + # "currency": "SOL", + # "coin_type": "SOL" + # } + # + return { + 'info': fee, + 'withdraw': { + 'fee': self.safe_number(fee, 'withdrawal_fee'), + 'percentage': False, + }, + 'deposit': { + 'fee': None, + 'percentage': None, + }, + 'networks': {}, + } + + def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}): + """ + fetch deposit and withdraw fees + + https://docs.deribit.com/#public-get_currencies + + :param str[]|None codes: list of unified currency codes + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `fee structures ` + """ + self.load_markets() + response = self.publicGetGetCurrencies(params) + # + # { + # "jsonrpc": "2.0", + # "result": [ + # { + # "withdrawal_priorities": [], + # "withdrawal_fee": 0.01457324, + # "min_withdrawal_fee": 0.000001, + # "min_confirmations": 1, + # "fee_precision": 8, + # "currency_long": "Solana", + # "currency": "SOL", + # "coin_type": "SOL" + # }, + # ... + # ], + # "usIn": 1688652701456124, + # "usOut": 1688652701456390, + # "usDiff": 266, + # "testnet": True + # } + # + data = self.safe_list(response, 'result', []) + return self.parse_deposit_withdraw_fees(data, codes, 'currency') + + def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate + + https://docs.deribit.com/#public-get_funding_rate_value + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.start_timestamp]: fetch funding rate starting from self timestamp + :param int [params.end_timestamp]: fetch funding rate ending at self timestamp + :returns dict: a `funding rate structure ` + """ + self.load_markets() + market = self.market(symbol) + time = self.milliseconds() + request: dict = { + 'instrument_name': market['id'], + 'start_timestamp': time - (8 * 60 * 60 * 1000), # 8h ago, + 'end_timestamp': time, + } + response = self.publicGetGetFundingRateValue(self.extend(request, params)) + # + # { + # "jsonrpc":"2.0", + # "result":"0", + # "usIn":"1691161645596519", + # "usOut":"1691161645597149", + # "usDiff":"630", + # "testnet":false + # } + # + return self.parse_funding_rate(response, market) + + def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch the current funding rate + + https://docs.deribit.com/#public-get_funding_rate_history + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch funding rate history for + :param int [limit]: the maximum number of entries to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: fetch funding rate ending at self timestamp + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict: a `funding rate structure ` + """ + self.load_markets() + market = self.market(symbol) + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchFundingRateHistory', 'paginate') + maxEntriesPerRequest = 744 # seems exchange returns max 744 items per request + eachItemDuration = '1h' + if paginate: + # fix for: https://github.com/ccxt/ccxt/issues/25040 + return self.fetch_paginated_call_deterministic('fetchFundingRateHistory', symbol, since, limit, eachItemDuration, self.extend(params, {'isDeribitPaginationCall': True}), maxEntriesPerRequest) + duration = self.parse_timeframe(eachItemDuration) * 1000 + time = self.milliseconds() + month = 30 * 24 * 60 * 60 * 1000 + if since is None: + since = time - month + else: + time = since + month + request: dict = { + 'instrument_name': market['id'], + 'start_timestamp': since - 1, + } + until = self.safe_integer_2(params, 'until', 'end_timestamp') + if until is not None: + params = self.omit(params, ['until']) + request['end_timestamp'] = until + else: + request['end_timestamp'] = time + if 'isDeribitPaginationCall' in params: + params = self.omit(params, 'isDeribitPaginationCall') + maxUntil = self.sum(since, limit * duration) + request['end_timestamp'] = min(request['end_timestamp'], maxUntil) + response = self.publicGetGetFundingRateHistory(self.extend(request, params)) + # + # { + # "jsonrpc": "2.0", + # "id": 7617, + # "result": [ + # { + # "timestamp": 1569891600000, + # "index_price": 8222.87, + # "prev_index_price": 8305.72, + # "interest_8h": -0.00009234260068476106, + # "interest_1h": -4.739622041017375e-7 + # } + # ] + # } + # + rates = [] + result = self.safe_value(response, 'result', []) + for i in range(0, len(result)): + fr = result[i] + rate = self.parse_funding_rate(fr, market) + rates.append(rate) + return self.filter_by_symbol_since_limit(rates, symbol, since, limit) + + def parse_funding_rate(self, contract, market: Market = None) -> FundingRate: + # + # { + # "jsonrpc":"2.0", + # "result":"0", + # "usIn":"1691161645596519", + # "usOut":"1691161645597149", + # "usDiff":"630", + # "testnet":false + # } + # history + # { + # "timestamp": 1569891600000, + # "index_price": 8222.87, + # "prev_index_price": 8305.72, + # "interest_8h": -0.00009234260068476106, + # "interest_1h": -4.739622041017375e-7 + # } + # + timestamp = self.safe_integer(contract, 'timestamp') + datetime = self.iso8601(timestamp) + result = self.safe_number_2(contract, 'result', 'interest_8h') + return { + 'info': contract, + 'symbol': self.safe_symbol(None, market), + 'markPrice': None, + 'indexPrice': self.safe_number(contract, 'index_price'), + 'interestRate': None, + 'estimatedSettlePrice': None, + 'timestamp': timestamp, + 'datetime': datetime, + 'fundingRate': result, + 'fundingTimestamp': None, + 'fundingDatetime': None, + 'nextFundingRate': None, + 'nextFundingTimestamp': None, + 'nextFundingDatetime': None, + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': '8h', + } + + def fetch_liquidations(self, symbol: str, since: Int = None, limit: Int = None, params={}): + """ + retrieves the public liquidations of a trading pair + + https://docs.deribit.com/#public-get_last_settlements_by_currency + + :param str symbol: unified CCXT market symbol + :param int [since]: the earliest time in ms to fetch liquidations for + :param int [limit]: the maximum number of liquidation structures to retrieve + :param dict [params]: exchange specific parameters for the deribit api endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict: an array of `liquidation structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchLiquidations', 'paginate') + if paginate: + return self.fetch_paginated_call_cursor('fetchLiquidations', symbol, since, limit, params, 'continuation', 'continuation', None) + market = self.market(symbol) + if market['spot']: + raise NotSupported(self.id + ' fetchLiquidations() does not support ' + market['type'] + ' markets') + request: dict = { + 'instrument_name': market['id'], + 'type': 'bankruptcy', + } + if since is not None: + request['search_start_timestamp'] = since + if limit is not None: + request['count'] = limit + response = self.publicGetGetLastSettlementsByInstrument(self.extend(request, params)) + # + # { + # "jsonrpc": "2.0", + # "result": { + # "settlements": [ + # { + # "type": "bankruptcy", + # "timestamp": 1696579200041, + # "funded": 10000.0, + # "session_bankrupcy": 10000.0 + # "session_profit_loss": 112951.68715857354, + # "session_tax": 0.15, + # "session_tax_rate": 0.0015, + # "socialized": 0.001, + # }, + # ], + # "continuation": "5dHzoGyD8Hs8KURoUhfgXgHpJTA5oyapoudSmNeAfEftqRbjNE6jNNUpo2oCu1khnZL9ao" + # }, + # "usIn": 1696652052254890, + # "usOut": 1696652052255733, + # "usDiff": 843, + # "testnet": False + # } + # + result = self.safe_value(response, 'result', {}) + cursor = self.safe_string(result, 'continuation') + settlements = self.safe_value(result, 'settlements', []) + settlementsWithCursor = self.add_pagination_cursor_to_result(cursor, settlements) + return self.parse_liquidations(settlementsWithCursor, market, since, limit) + + def add_pagination_cursor_to_result(self, cursor, data): + if cursor is not None: + dataLength = len(data) + if dataLength > 0: + first = data[0] + last = data[dataLength - 1] + first['continuation'] = cursor + last['continuation'] = cursor + data[0] = first + data[dataLength - 1] = last + return data + + def fetch_my_liquidations(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + retrieves the users liquidated positions + + https://docs.deribit.com/#private-get_settlement_history_by_instrument + + :param str symbol: unified CCXT market symbol + :param int [since]: the earliest time in ms to fetch liquidations for + :param int [limit]: the maximum number of liquidation structures to retrieve + :param dict [params]: exchange specific parameters for the deribit api endpoint + :returns dict: an array of `liquidation structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMyLiquidations() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + if market['spot']: + raise NotSupported(self.id + ' fetchMyLiquidations() does not support ' + market['type'] + ' markets') + request: dict = { + 'instrument_name': market['id'], + 'type': 'bankruptcy', + } + if since is not None: + request['search_start_timestamp'] = since + if limit is not None: + request['count'] = limit + response = self.privateGetGetSettlementHistoryByInstrument(self.extend(request, params)) + # + # { + # "jsonrpc": "2.0", + # "result": { + # "settlements": [ + # { + # "type": "bankruptcy", + # "timestamp": 1696579200041, + # "funded": 10000.0, + # "session_bankrupcy": 10000.0 + # "session_profit_loss": 112951.68715857354, + # "session_tax": 0.15, + # "session_tax_rate": 0.0015, + # "socialized": 0.001, + # }, + # ], + # "continuation": "5dHzoGyD8Hs8KURoUhfgXgHpJTA5oyapoudSmNeAfEftqRbjNE6jNNUpo2oCu1khnZL9ao" + # }, + # "usIn": 1696652052254890, + # "usOut": 1696652052255733, + # "usDiff": 843, + # "testnet": False + # } + # + result = self.safe_value(response, 'result', {}) + settlements = self.safe_list(result, 'settlements', []) + return self.parse_liquidations(settlements, market, since, limit) + + def parse_liquidation(self, liquidation, market: Market = None): + # + # { + # "type": "bankruptcy", + # "timestamp": 1696579200041, + # "funded": 1, + # "session_bankrupcy": 0.001, + # "session_profit_loss": 0.001, + # "session_tax": 0.0015, + # "session_tax_rate": 0.0015, + # "socialized": 0.001, + # } + # + timestamp = self.safe_integer(liquidation, 'timestamp') + return self.safe_liquidation({ + 'info': liquidation, + 'symbol': self.safe_symbol(None, market), + 'contracts': None, + 'contractSize': self.safe_number(market, 'contractSize'), + 'price': None, + 'baseValue': self.safe_number(liquidation, 'session_bankrupcy'), + 'quoteValue': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + + def fetch_greeks(self, symbol: str, params={}) -> Greeks: + """ + fetches an option contracts greeks, financial metrics used to measure the factors that affect the price of an options contract + + https://docs.deribit.com/#public-ticker + + :param str symbol: unified symbol of the market to fetch greeks for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `greeks structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'instrument_name': market['id'], + } + response = self.publicGetTicker(self.extend(request, params)) + # + # { + # "jsonrpc": "2.0", + # "result": { + # "estimated_delivery_price": 36552.72, + # "best_bid_amount": 0.2, + # "best_ask_amount": 9.1, + # "interest_rate": 0.0, + # "best_bid_price": 0.214, + # "best_ask_price": 0.219, + # "open_interest": 368.8, + # "settlement_price": 0.22103022, + # "last_price": 0.215, + # "bid_iv": 60.51, + # "ask_iv": 61.88, + # "mark_iv": 61.27, + # "underlying_index": "BTC-27SEP24", + # "underlying_price": 38992.71, + # "min_price": 0.1515, + # "max_price": 0.326, + # "mark_price": 0.2168, + # "instrument_name": "BTC-27SEP24-40000-C", + # "index_price": 36552.72, + # "greeks": { + # "rho": 130.63998, + # "theta": -13.48784, + # "vega": 141.90146, + # "gamma": 0.00002, + # "delta": 0.59621 + # }, + # "stats": { + # "volume_usd": 100453.9, + # "volume": 12.0, + # "price_change": -2.2727, + # "low": 0.2065, + # "high": 0.238 + # }, + # "state": "open", + # "timestamp": 1699578548021 + # }, + # "usIn": 1699578548308414, + # "usOut": 1699578548308606, + # "usDiff": 192, + # "testnet": False + # } + # + result = self.safe_value(response, 'result', {}) + return self.parse_greeks(result, market) + + def parse_greeks(self, greeks: dict, market: Market = None) -> Greeks: + # + # { + # "estimated_delivery_price": 36552.72, + # "best_bid_amount": 0.2, + # "best_ask_amount": 9.1, + # "interest_rate": 0.0, + # "best_bid_price": 0.214, + # "best_ask_price": 0.219, + # "open_interest": 368.8, + # "settlement_price": 0.22103022, + # "last_price": 0.215, + # "bid_iv": 60.51, + # "ask_iv": 61.88, + # "mark_iv": 61.27, + # "underlying_index": "BTC-27SEP24", + # "underlying_price": 38992.71, + # "min_price": 0.1515, + # "max_price": 0.326, + # "mark_price": 0.2168, + # "instrument_name": "BTC-27SEP24-40000-C", + # "index_price": 36552.72, + # "greeks": { + # "rho": 130.63998, + # "theta": -13.48784, + # "vega": 141.90146, + # "gamma": 0.00002, + # "delta": 0.59621 + # }, + # "stats": { + # "volume_usd": 100453.9, + # "volume": 12.0, + # "price_change": -2.2727, + # "low": 0.2065, + # "high": 0.238 + # }, + # "state": "open", + # "timestamp": 1699578548021 + # } + # + timestamp = self.safe_integer(greeks, 'timestamp') + marketId = self.safe_string(greeks, 'instrument_name') + symbol = self.safe_symbol(marketId, market) + stats = self.safe_value(greeks, 'greeks', {}) + return { + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'delta': self.safe_number(stats, 'delta'), + 'gamma': self.safe_number(stats, 'gamma'), + 'theta': self.safe_number(stats, 'theta'), + 'vega': self.safe_number(stats, 'vega'), + 'rho': self.safe_number(stats, 'rho'), + 'bidSize': self.safe_number(greeks, 'best_bid_amount'), + 'askSize': self.safe_number(greeks, 'best_ask_amount'), + 'bidImpliedVolatility': self.safe_number(greeks, 'bid_iv'), + 'askImpliedVolatility': self.safe_number(greeks, 'ask_iv'), + 'markImpliedVolatility': self.safe_number(greeks, 'mark_iv'), + 'bidPrice': self.safe_number(greeks, 'best_bid_price'), + 'askPrice': self.safe_number(greeks, 'best_ask_price'), + 'markPrice': self.safe_number(greeks, 'mark_price'), + 'lastPrice': self.safe_number(greeks, 'last_price'), + 'underlyingPrice': self.safe_number(greeks, 'underlying_price'), + 'info': greeks, + } + + def fetch_option(self, symbol: str, params={}) -> Option: + """ + fetches option data that is commonly found in an option chain + + https://docs.deribit.com/#public-get_book_summary_by_instrument + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `option chain structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'instrument_name': market['id'], + } + response = self.publicGetGetBookSummaryByInstrument(self.extend(request, params)) + # + # { + # "jsonrpc": "2.0", + # "result": [ + # { + # "mid_price": 0.04025, + # "volume_usd": 11045.12, + # "quote_currency": "BTC", + # "estimated_delivery_price": 65444.72, + # "creation_timestamp": 1711100949273, + # "base_currency": "BTC", + # "underlying_index": "BTC-27DEC24", + # "underlying_price": 73742.14, + # "volume": 4.0, + # "interest_rate": 0.0, + # "price_change": -6.9767, + # "open_interest": 274.2, + # "ask_price": 0.042, + # "bid_price": 0.0385, + # "instrument_name": "BTC-27DEC24-240000-C", + # "mark_price": 0.04007735, + # "last": 0.04, + # "low": 0.04, + # "high": 0.043 + # } + # ], + # "usIn": 1711100949273223, + # "usOut": 1711100949273580, + # "usDiff": 357, + # "testnet": False + # } + # + result = self.safe_list(response, 'result', []) + chain = self.safe_dict(result, 0, {}) + return self.parse_option(chain, None, market) + + def fetch_option_chain(self, code: str, params={}) -> OptionChain: + """ + fetches data for an underlying asset that is commonly found in an option chain + + https://docs.deribit.com/#public-get_book_summary_by_currency + + :param str code: base currency to fetch an option chain for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `option chain structures ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + 'kind': 'option', + } + response = self.publicGetGetBookSummaryByCurrency(self.extend(request, params)) + # + # { + # "jsonrpc": "2.0", + # "result": [ + # { + # "mid_price": 0.4075, + # "volume_usd": 2836.83, + # "quote_currency": "BTC", + # "estimated_delivery_price": 65479.26, + # "creation_timestamp": 1711101594477, + # "base_currency": "BTC", + # "underlying_index": "BTC-28JUN24", + # "underlying_price": 68827.27, + # "volume": 0.1, + # "interest_rate": 0.0, + # "price_change": 0.0, + # "open_interest": 364.1, + # "ask_price": 0.411, + # "bid_price": 0.404, + # "instrument_name": "BTC-28JUN24-42000-C", + # "mark_price": 0.40752052, + # "last": 0.423, + # "low": 0.423, + # "high": 0.423 + # } + # ], + # "usIn": 1711101594456388, + # "usOut": 1711101594484065, + # "usDiff": 27677, + # "testnet": False + # } + # + result = self.safe_list(response, 'result', []) + return self.parse_option_chain(result, 'base_currency', 'instrument_name') + + def parse_option(self, chain: dict, currency: Currency = None, market: Market = None) -> Option: + # + # { + # "mid_price": 0.04025, + # "volume_usd": 11045.12, + # "quote_currency": "BTC", + # "estimated_delivery_price": 65444.72, + # "creation_timestamp": 1711100949273, + # "base_currency": "BTC", + # "underlying_index": "BTC-27DEC24", + # "underlying_price": 73742.14, + # "volume": 4.0, + # "interest_rate": 0.0, + # "price_change": -6.9767, + # "open_interest": 274.2, + # "ask_price": 0.042, + # "bid_price": 0.0385, + # "instrument_name": "BTC-27DEC24-240000-C", + # "mark_price": 0.04007735, + # "last": 0.04, + # "low": 0.04, + # "high": 0.043 + # } + # + marketId = self.safe_string(chain, 'instrument_name') + market = self.safe_market(marketId, market) + currencyId = self.safe_string(chain, 'base_currency') + code = self.safe_currency_code(currencyId, currency) + timestamp = self.safe_integer(chain, 'timestamp') + return { + 'info': chain, + 'currency': code, + 'symbol': market['symbol'], + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'impliedVolatility': None, + 'openInterest': self.safe_number(chain, 'open_interest'), + 'bidPrice': self.safe_number(chain, 'bid_price'), + 'askPrice': self.safe_number(chain, 'ask_price'), + 'midPrice': self.safe_number(chain, 'mid_price'), + 'markPrice': self.safe_number(chain, 'mark_price'), + 'lastPrice': self.safe_number(chain, 'last'), + 'underlyingPrice': self.safe_number(chain, 'underlying_price'), + 'change': None, + 'percentage': self.safe_number(chain, 'price_change'), + 'baseVolume': self.safe_number(chain, 'volume'), + 'quoteVolume': self.safe_number(chain, 'volume_usd'), + } + + def nonce(self): + return self.milliseconds() + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + request = '/' + 'api/' + self.version + '/' + api + '/' + path + if api == 'public': + if params: + request += '?' + self.urlencode(params) + if api == 'private': + self.check_required_credentials() + nonce = str(self.nonce()) + timestamp = str(self.milliseconds()) + requestBody = '' + if params: + request += '?' + self.urlencode(params) + requestData = method + "\n" + request + "\n" + requestBody + "\n" # eslint-disable-line quotes + auth = timestamp + "\n" + nonce + "\n" + requestData # eslint-disable-line quotes + signature = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256) + headers = { + 'Authorization': 'deri-hmac-sha256 id=' + self.apiKey + ',ts=' + timestamp + ',sig=' + signature + ',' + 'nonce=' + nonce, + } + url = self.urls['api']['rest'] + request + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if not response: + return None # fallback to default error handler + # + # { + # "jsonrpc": "2.0", + # "error": { + # "message": "Invalid params", + # "data": {reason: "invalid currency", param: "currency"}, + # "code": -32602 + # }, + # "testnet": False, + # "usIn": 1583763842150374, + # "usOut": 1583763842150410, + # "usDiff": 36 + # } + # + error = self.safe_value(response, 'error') + if error is not None: + errorCode = self.safe_string(error, 'code') + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions, errorCode, feedback) + raise ExchangeError(feedback) # unknown message + return None diff --git a/ccxt/derive.py b/ccxt/derive.py new file mode 100644 index 0000000..813987b --- /dev/null +++ b/ccxt/derive.py @@ -0,0 +1,2571 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.derive import ImplicitAPI +from ccxt.base.types import Any, Balances, Bool, Currencies, Currency, Int, Market, MarketType, Num, Order, OrderSide, OrderType, Position, Str, Strings, Ticker, FundingRate, Trade, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class derive(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(derive, self).describe(), { + 'id': 'derive', + 'name': 'derive', + 'countries': [], + 'version': 'v1', + 'rateLimit': 50, + 'certified': False, + 'pro': True, + 'dex': True, + 'has': { + 'CORS': None, + 'spot': False, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'cancelAllOrders': True, + 'cancelAllOrdersAfter': False, + 'cancelOrder': True, + 'cancelOrders': False, + 'cancelOrdersForSymbols': False, + 'closeAllPositions': False, + 'closePosition': False, + 'createMarketBuyOrderWithCost': False, + 'createMarketOrderWithCost': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createOrders': False, + 'createReduceOnlyOrder': False, + 'createStopOrder': False, + 'createTriggerOrder': False, + 'editOrder': True, + 'fetchAccounts': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchCanceledAndClosedOrders': False, + 'fetchCanceledOrders': True, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': False, + 'fetchDepositAddresses': False, + 'fetchDeposits': True, + 'fetchDepositWithdrawFee': 'emulated', + 'fetchDepositWithdrawFees': False, + 'fetchFundingHistory': True, + 'fetchFundingRate': True, + 'fetchFundingRateHistory': True, + 'fetchFundingRates': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchLedger': True, + 'fetchLeverage': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchMarginMode': None, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMyLiquidations': False, + 'fetchMyTrades': True, + 'fetchOHLCV': False, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOrder': False, + 'fetchOrderBook': False, + 'fetchOrders': True, + 'fetchOrderTrades': True, + 'fetchPosition': False, + 'fetchPositionMode': False, + 'fetchPositions': True, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchTicker': True, + 'fetchTickers': False, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': False, + 'fetchTransfer': False, + 'fetchTransfers': False, + 'fetchWithdrawal': False, + 'fetchWithdrawals': True, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'sandbox': True, + 'setLeverage': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'transfer': False, + 'withdraw': False, + }, + 'timeframes': { + '1m': '1m', + '3m': '3m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1h', + '2h': '2h', + '4h': '4h', + '8h': '8h', + '12h': '12h', + '1d': '1d', + '3d': '3d', + '1w': '1w', + '1M': '1M', + }, + 'hostname': 'derive.xyz', + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/f835b95f-033a-43dd-b6bb-24e698fc498c', + 'api': { + 'public': 'https://api.lyra.finance/public', + 'private': 'https://api.lyra.finance/private', + }, + 'test': { + 'public': 'https://api-demo.lyra.finance/public', + 'private': 'https://api-demo.lyra.finance/private', + }, + 'www': 'https://www.derive.xyz/', + 'doc': 'https://docs.derive.xyz/docs/', + 'fees': 'https://docs.derive.xyz/reference/fees-1/', + 'referral': 'https://www.derive.xyz/invite/3VB0B', + }, + 'api': { + 'public': { + 'get': [ + 'get_all_currencies', + ], + 'post': [ + 'build_register_session_key_tx', + 'register_session_key', + 'deregister_session_key', + 'login', + 'statistics', + 'get_all_currencies', + 'get_currency', + 'get_instrument', + 'get_all_instruments', + 'get_instruments', + 'get_ticker', + 'get_latest_signed_feeds', + 'get_option_settlement_prices', + 'get_spot_feed_history', + 'get_spot_feed_history_candles', + 'get_funding_rate_history', + 'get_trade_history', + 'get_option_settlement_history', + 'get_liquidation_history', + 'get_interest_rate_history', + 'get_transaction', + 'get_margin', + 'margin_watch', + 'validate_invite_code', + 'get_points', + 'get_all_points', + 'get_points_leaderboard', + 'get_descendant_tree', + 'get_tree_roots', + 'get_swell_percent_points', + 'get_vault_assets', + 'get_etherfi_effective_balances', + 'get_kelp_effective_balances', + 'get_bridge_balances', + 'get_ethena_participants', + 'get_vault_share', + 'get_vault_statistics', + 'get_vault_balances', + 'estimate_integrator_points', + 'create_subaccount_debug', + 'deposit_debug', + 'withdraw_debug', + 'send_quote_debug', + 'execute_quote_debug', + 'get_invite_code', + 'register_invite', + 'get_time', + 'get_live_incidents', + 'get_maker_programs', + 'get_maker_program_scores', + ], + }, + 'private': { + 'post': [ + 'get_account', + 'create_subaccount', + 'get_subaccount', + 'get_subaccounts', + 'get_all_portfolios', + 'change_subaccount_label', + 'get_notificationsv', + 'update_notifications', + 'deposit', + 'withdraw', + 'transfer_erc20', + 'transfer_position', + 'transfer_positions', + 'order', + 'replace', + 'order_debug', + 'get_order', + 'get_orders', + 'get_open_orders', + 'cancel', + 'cancel_by_label', + 'cancel_by_nonce', + 'cancel_by_instrument', + 'cancel_all', + 'cancel_trigger_order', + 'get_order_history', + 'get_trade_history', + 'get_deposit_history', + 'get_withdrawal_history', + 'send_rfq', + 'cancel_rfq', + 'cancel_batch_rfqs', + 'get_rfqs', + 'poll_rfqs', + 'send_quote', + 'cancel_quote', + 'cancel_batch_quotes', + 'get_quotes', + 'poll_quotes', + 'execute_quote', + 'rfq_get_best_quote', + 'get_margin', + 'get_collaterals', + 'get_positions', + 'get_option_settlement_history', + 'get_subaccount_value_history', + 'expired_and_cancelled_history', + 'get_funding_history', + 'get_interest_history', + 'get_erc20_transfer_history', + 'get_liquidation_history', + 'liquidate', + 'get_liquidator_history', + 'session_keys', + 'edit_session_key', + 'register_scoped_session_key', + 'get_mmp_config', + 'set_mmp_config', + 'reset_mmp', + 'set_cancel_on_disconnect', + 'get_invite_code', + 'register_invite', + ], + }, + }, + 'fees': { + }, + 'requiredCredentials': { + 'apiKey': False, + 'secret': False, + 'walletAddress': True, + 'privateKey': True, + }, + 'exceptions': { + 'exact': { + '-32000': RateLimitExceeded, # Rate limit exceeded + '-32100': RateLimitExceeded, # Number of concurrent websocket clients limit exceeded + '-32700': BadRequest, # Parse error + '-32600': BadRequest, # Invalid Request + '-32601': BadRequest, # Method not found + '-32602': InvalidOrder, # {"id":"55e66a3d-6a4e-4a36-a23d-5cf8a91ef478","error":{"code":"","message":"Invalid params"}} + '-32603': InvalidOrder, # {"code":"-32603","message":"Internal error","data":"SubAccount matching query does not exist."} + '9000': InvalidOrder, # Order confirmation timeout + '10000': BadRequest, # Manager not found + '10001': BadRequest, # Asset is not an ERC20 token + '10002': BadRequest, # Sender and recipient wallet do not match + '10003': BadRequest, # Sender and recipient subaccount IDs are the same + '10004': InvalidOrder, # Multiple currencies not supported + '10005': BadRequest, # Maximum number of subaccounts per wallet reached + '10006': BadRequest, # Maximum number of session keys per wallet reached + '10007': BadRequest, # Maximum number of assets per subaccount reached + '10008': BadRequest, # Maximum number of expiries per subaccount reached + '10009': BadRequest, # Recipient subaccount ID of the transfer cannot be 0 + '10010': InvalidOrder, # PMRM only supports USDC asset collateral. Cannot trade spot markets. + '10011': InsufficientFunds, # ERC20 allowance is insufficient + '10012': InsufficientFunds, # ERC20 balance is less than transfer amount + '10013': ExchangeError, # There is a pending deposit for self asset + '10014': ExchangeError, # There is a pending withdrawal for self asset + '11000': InsufficientFunds, # Insufficient funds + '11002': InvalidOrder, # Order rejected from queue + '11003': InvalidOrder, # Already cancelled + '11004': InvalidOrder, # Already filled + '11005': InvalidOrder, # Already expired + '11006': OrderNotFound, # {"code":"11006","message":"Does not exist","data":"Open order with id: 804018f3-b092-40a3-a933-b29574fa1ff8 does not exist."} + '11007': InvalidOrder, # Self-crossing disallowed + '11008': InvalidOrder, # Post-only reject + '11009': InvalidOrder, # Zero liquidity for market or IOC/FOK order + '11010': InvalidOrder, # Post-only invalid order type + '11011': InvalidOrder, # {"code":11011,"message":"Invalid signature expiry","data":"Order must expire in 300 sec or more"} + '11012': InvalidOrder, # {"code":"11012","message":"Invalid amount","data":"Amount must be a multiple of 0.01"} + '11013': InvalidOrder, # {"code":"11013","message":"Invalid limit price","data":{"limit":"10000","bandwidth":"92530"}} + '11014': InvalidOrder, # Fill-or-kill not filled + '11015': InvalidOrder, # MMP frozen + '11016': InvalidOrder, # Already consumed + '11017': InvalidOrder, # Non unique nonce + '11018': InvalidOrder, # Invalid nonce date + '11019': InvalidOrder, # Open orders limit exceeded + '11020': InsufficientFunds, # Negative ERC20 balance + '11021': InvalidOrder, # Instrument is not live + '11022': InvalidOrder, # Reject timestamp exceeded + '11023': InvalidOrder, # {"code":"11023","message":"Max fee order param is too low","data":"signed max_fee must be >= 194.420835871999983091712000000000000000"} + '11024': InvalidOrder, # {"code":11024,"message":"Reduce only not supported with self time in force"} + '11025': InvalidOrder, # Reduce only reject + '11026': BadRequest, # Transfer reject + '11027': InvalidOrder, # Subaccount undergoing liquidation + '11028': InvalidOrder, # Replaced order filled amount does not match expected state. + '11050': InvalidOrder, # Trigger order was cancelled between the time worker sent order and engine processed order + '11051': InvalidOrder, # {"code":"11051","message":"Trigger price must be higher than the current price for stop orders and vice versa for take orders","data":"Trigger price 9000.0 must be < or > current price 102671.2 depending on trigger type and direction."} + '11052': InvalidOrder, # Trigger order limit exceeded(separate limit from regular orders) + '11053': InvalidOrder, # Index and last-trade trigger price types not supported yet + '11054': InvalidOrder, # {"code":"11054","message":"Trigger orders cannot replace or be replaced"} + '11055': InvalidOrder, # Market order limit_price is unfillable at the given trigger price + '11100': InvalidOrder, # Leg instruments are not unique + '11101': InvalidOrder, # RFQ not found + '11102': InvalidOrder, # Quote not found + '11103': InvalidOrder, # Quote leg does not match RFQ leg + '11104': InvalidOrder, # Requested quote or RFQ is not open + '11105': InvalidOrder, # Requested quote ID references a different RFQ ID + '11106': InvalidOrder, # Invalid RFQ counterparty + '11107': InvalidOrder, # Quote maker total cost too high + '11200': InvalidOrder, # Auction not ongoing + '11201': InvalidOrder, # Open orders not allowed + '11202': InvalidOrder, # Price limit exceeded + '11203': InvalidOrder, # Last trade ID mismatch + '12000': InvalidOrder, # Asset not found + '12001': InvalidOrder, # Instrument not found + '12002': BadRequest, # Currency not found + '12003': BadRequest, # USDC does not have asset caps per manager + '13000': BadRequest, # Invalid channels + '14000': BadRequest, # {"code": 14000, "message": "Account not found"} + '14001': InvalidOrder, # {"code": 14001, "message": "Subaccount not found"} + '14002': BadRequest, # Subaccount was withdrawn + '14008': BadRequest, # Cannot reduce expiry using registerSessionKey RPC route + '14009': BadRequest, # Session key expiry must be > utc_now + 10 min + '14010': BadRequest, # Session key already registered for self account + '14011': BadRequest, # Session key already registered with another account + '14012': BadRequest, # Address must be checksummed + '14013': BadRequest, # str is not a valid ethereum address + '14014': InvalidOrder, # {"code":"14014","message":"Signature invalid for message or transaction","data":"Signature does not match data"} + '14015': BadRequest, # Transaction count for given wallet does not match provided nonce + '14016': BadRequest, # The provided signed raw transaction contains function name that does not match the expected function name + '14017': BadRequest, # The provided signed raw transaction contains contract address that does not match the expected contract address + '14018': BadRequest, # The provided signed raw transaction contains function params that do not match any expected function params + '14019': BadRequest, # The provided signed raw transaction contains function param values that do not match the expected values + '14020': BadRequest, # The X-LyraWallet header does not match the requested subaccount_id or wallet + '14021': BadRequest, # The X-LyraWallet header not provided + '14022': AuthenticationError, # Subscription to a private channel failed + '14023': InvalidOrder, # {"code":"14023","message":"Signer in on-chain related request is not wallet owner or registered session key","data":"Session key does not belong to wallet"} + '14024': BadRequest, # Chain ID must match the current roll up chain id + '14025': BadRequest, # The private request is missing a wallet or subaccount_id param + '14026': BadRequest, # Session key not found + '14027': AuthenticationError, # Unauthorized maker + '14028': BadRequest, # Cross currency RFQ not supported + '14029': AuthenticationError, # Session key IP not whitelisted + '14030': BadRequest, # Session key expired + '14031': AuthenticationError, # Unauthorized key scope + '14032': BadRequest, # Scope should not be changed + '16000': AuthenticationError, # You are in a restricted region that violates our terms of service. + '16001': AuthenticationError, # Account is disabled due to compliance violations, please contact support to enable it. + '16100': AuthenticationError, # Sentinel authorization is invalid + '17000': BadRequest, # This accoount does not have a shareable invite code + '17001': BadRequest, # Invalid invite code + '17002': BadRequest, # Invite code already registered for self account + '17003': BadRequest, # Invite code has no remaining uses + '17004': BadRequest, # Requirement for successful invite registration not met + '17005': BadRequest, # Account must register with a valid invite code to be elligible for points + '17006': BadRequest, # Point program does not exist + '17007': BadRequest, # Invalid leaderboard page number + '18000': BadRequest, # Invalid block number + '18001': BadRequest, # Failed to estimate block number. Please try again later. + '18002': BadRequest, # The provided smart contract owner does not match the wallet in LightAccountFactory.getAddress() + '18003': BadRequest, # Vault ERC20 asset does not exist + '18004': BadRequest, # Vault ERC20 pool does not exist + '18005': BadRequest, # Must add asset to pool before getting balances + '18006': BadRequest, # Invalid Swell season. Swell seasons are in the form 'swell_season_X'. + '18007': BadRequest, # Vault not found + '19000': BadRequest, # Maker program not found + }, + 'broad': { + }, + }, + 'precisionMode': TICK_SIZE, + 'commonCurrencies': { + }, + 'options': { + 'deriveWalletAddress': '', # a derive wallet address "0x"-prefixed hexstring + 'id': '0x0ad42b8e602c2d3d475ae52d678cf63d84ab2749', + }, + }) + + def set_sandbox_mode(self, enable: bool): + super(derive, self).set_sandbox_mode(enable) + self.options['sandboxMode'] = enable + + def fetch_time(self, params={}): + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://docs.derive.xyz/reference/post_public-get-time + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = self.publicPostGetTime(params) + # + # { + # "result": 1735846536758, + # "id": "f1c03d21-f886-4c5a-9a9d-33dd06f180f0" + # } + # + return self.safe_integer(response, 'result') + + def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://docs.derive.xyz/reference/post_public-get-all-currencies + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + result: dict = {} + tokenResponse = self.publicGetGetAllCurrencies(params) + # + # { + # "result": [ + # { + # "currency": "SEI", + # "instrument_types": [ + # "perp" + # ], + # "protocol_asset_addresses": { + # "perp": "0x7225889B75fd34C68eA3098dAE04D50553C09840", + # "option": null, + # "spot": null, + # "underlying_erc20": null + # }, + # "managers": [ + # { + # "address": "0x28c9ddF9A3B29c2E6a561c1BC520954e5A33de5D", + # "margin_type": "SM", + # "currency": null + # } + # ], + # "srm_im_discount": "0", + # "srm_mm_discount": "0", + # "pm2_collateral_discounts": [], + # "borrow_apy": "0", + # "supply_apy": "0", + # "total_borrow": "0", + # "total_supply": "0", + # "asset_cap_and_supply_per_manager": { + # "perp": { + # "SM": [ + # { + # "current_open_interest": "0", + # "interest_cap": "2000000", + # "manager_currency": null + # } + # ] + # }, + # "option": {}, + # "erc20": {} + # }, + # "market_type": "SRM_PERP_ONLY", + # "spot_price": "0.2193542905042081", + # "spot_price_24h": "0.238381655533635830" + # }, + # "id": "7e07fe1d-0ab4-4d2b-9e22-b65ce9e232dc" + # } + # + currencies = self.safe_list(tokenResponse, 'result', []) + for i in range(0, len(currencies)): + currency = currencies[i] + currencyId = self.safe_string(currency, 'currency') + code = self.safe_currency_code(currencyId) + result[code] = self.safe_currency_structure({ + 'id': currencyId, + 'name': None, + 'code': code, + 'precision': None, + 'active': None, + 'fee': None, + 'networks': None, + 'deposit': None, + 'withdraw': None, + 'limits': { + 'deposit': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + }, + 'info': currency, + }) + return result + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for bybit + + https://docs.derive.xyz/reference/post_public-get-all-instruments + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + spotMarketsPromise = self.fetch_spot_markets(params) + swapMarketsPromise = self.fetch_swap_markets(params) + optionMarketsPromise = self.fetch_option_markets(params) + spotMarkets, swapMarkets, optionMarkets = [spotMarketsPromise, swapMarketsPromise, optionMarketsPromise] + # + # { + # "result": { + # "instruments": [ + # { + # "instrument_type": "perp", + # "instrument_name": "BTC-PERP", + # "scheduled_activation": 1701840228, + # "scheduled_deactivation": 9223372036854776000, + # "is_active": True, + # "tick_size": "0.1", + # "minimum_amount": "0.01", + # "maximum_amount": "10000", + # "amount_step": "0.001", + # "mark_price_fee_rate_cap": "0", + # "maker_fee_rate": "0.00005", + # "taker_fee_rate": "0.0003", + # "base_fee": "0.1", + # "base_currency": "BTC", + # "quote_currency": "USD", + # "option_details": null, + # "perp_details": { + # "index": "BTC-USD", + # "max_rate_per_hour": "0.004", + # "min_rate_per_hour": "-0.004", + # "static_interest_rate": "0.0000125", + # "aggregate_funding": "10538.574363381759146829", + # "funding_rate": "0.0000125" + # }, + # "erc20_details": null, + # "base_asset_address": "0xDBa83C0C654DB1cd914FA2710bA743e925B53086", + # "base_asset_sub_id": "0", + # "pro_rata_fraction": "0", + # "fifo_min_allocation": "0", + # "pro_rata_amount_step": "0.1" + # } + # ], + # "pagination": { + # "num_pages": 1, + # "count": 1 + # } + # }, + # "id": "a06bc0b2-8e78-4536-a21f-f785f225b5a5" + # } + # + result = self.array_concat(spotMarkets, swapMarkets) + result = self.array_concat(result, optionMarkets) + return result + + def fetch_spot_markets(self, params={}) -> List[Market]: + request: dict = { + 'expired': False, + 'instrument_type': 'erc20', + } + response = self.publicPostGetAllInstruments(self.extend(request, params)) + result = self.safe_dict(response, 'result', {}) + data = self.safe_list(result, 'instruments', []) + return self.parse_markets(data) + + def fetch_swap_markets(self, params={}) -> List[Market]: + request: dict = { + 'expired': False, + 'instrument_type': 'perp', + } + response = self.publicPostGetAllInstruments(self.extend(request, params)) + result = self.safe_dict(response, 'result', {}) + data = self.safe_list(result, 'instruments', []) + return self.parse_markets(data) + + def fetch_option_markets(self, params={}) -> List[Market]: + request: dict = { + 'expired': False, + 'instrument_type': 'option', + } + response = self.publicPostGetAllInstruments(self.extend(request, params)) + result = self.safe_dict(response, 'result', {}) + data = self.safe_list(result, 'instruments', []) + return self.parse_markets(data) + + def parse_market(self, market: dict) -> Market: + type = self.safe_string(market, 'instrument_type') + marketType: MarketType + spot = False + margin = True + swap = False + option = False + linear: Bool = None + inverse: Bool = None + baseId = self.safe_string(market, 'base_currency') + quoteId = self.safe_string(market, 'quote_currency') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + marketId = self.safe_string(market, 'instrument_name') + symbol = base + '/' + quote + settleId: Str = None + settle: Str = None + expiry: Num = None + strike: Num = None + optionType: Str = None + optionLetter: Str = None + if type == 'erc20': + spot = True + marketType = 'spot' + elif type == 'perp': + margin = False + settleId = 'USDC' + settle = self.safe_currency_code(settleId) + symbol = base + '/' + quote + ':' + settle + swap = True + linear = True + inverse = False + marketType = 'swap' + elif type == 'option': + settleId = 'USDC' + settle = self.safe_currency_code(settleId) + margin = False + option = True + marketType = 'option' + optionDetails = self.safe_dict(market, 'option_details') + expiry = self.safe_timestamp(optionDetails, 'expiry') + strike = self.safe_integer(optionDetails, 'strike') + optionLetter = self.safe_string(optionDetails, 'option_type') + symbol = base + '/' + quote + ':' + settle + '-' + self.yymmdd(expiry) + '-' + self.number_to_string(strike) + '-' + optionLetter + if optionLetter == 'P': + optionType = 'put' + else: + optionType = 'call' + linear = True + inverse = False + return self.safe_market_structure({ + 'id': marketId, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': marketType, + 'spot': spot, + 'margin': margin, + 'swap': swap, + 'future': False, + 'option': option, + 'active': self.safe_bool(market, 'is_active'), + 'contract': (swap or option), + 'linear': linear, + 'inverse': inverse, + 'contractSize': None if (spot) else 1, + 'expiry': expiry, + 'expiryDatetime': self.iso8601(expiry), + 'taker': self.safe_number(market, 'taker_fee_rate'), + 'maker': self.safe_number(market, 'maker_fee_rate'), + 'strike': strike, + 'optionType': optionType, + 'precision': { + 'amount': self.safe_number(market, 'amount_step'), + 'price': self.safe_number(market, 'tick_size'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(market, 'minimum_amount'), + 'max': self.safe_number(market, 'maximum_amount'), + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + 'info': market, + }) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://docs.derive.xyz/reference/post_public-get-ticker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'instrument_name': market['id'], + } + response = self.publicPostGetTicker(self.extend(request, params)) + # + # spot + # + # { + # "result": { + # "instrument_type": "perp", + # "instrument_name": "BTC-PERP", + # "scheduled_activation": 1701840228, + # "scheduled_deactivation": 9223372036854776000, + # "is_active": True, + # "tick_size": "0.1", + # "minimum_amount": "0.01", + # "maximum_amount": "10000", + # "amount_step": "0.001", + # "mark_price_fee_rate_cap": "0", + # "maker_fee_rate": "0.00005", + # "taker_fee_rate": "0.0003", + # "base_fee": "0.1", + # "base_currency": "BTC", + # "quote_currency": "USD", + # "option_details": null, + # "perp_details": { + # "index": "BTC-USD", + # "max_rate_per_hour": "0.004", + # "min_rate_per_hour": "-0.004", + # "static_interest_rate": "0.0000125", + # "aggregate_funding": "10512.580833189805742522", + # "funding_rate": "-0.000022223906766867" + # }, + # "erc20_details": null, + # "base_asset_address": "0xDBa83C0C654DB1cd914FA2710bA743e925B53086", + # "base_asset_sub_id": "0", + # "pro_rata_fraction": "0", + # "fifo_min_allocation": "0", + # "pro_rata_amount_step": "0.1", + # "best_ask_amount": "0.012", + # "best_ask_price": "99567.9", + # "best_bid_amount": "0.129", + # "best_bid_price": "99554.5", + # "five_percent_bid_depth": "11.208", + # "five_percent_ask_depth": "11.42", + # "option_pricing": null, + # "index_price": "99577.2", + # "mark_price": "99543.642926357933902181684970855712890625", + # "stats": { + # "contract_volume": "464.712", + # "num_trades": "10681", + # "open_interest": "72.804739389481989861", + # "high": "99519.1", + # "low": "97254.1", + # "percent_change": "0.0128", + # "usd_change": "1258.1" + # }, + # "timestamp": 1736140984000, + # "min_price": "97591.2", + # "max_price": "101535.1" + # }, + # "id": "bbd7c271-c2be-48f7-b93a-26cf6d4cb79f" + # } + # + data = self.safe_dict(response, 'result', {}) + return self.parse_ticker(data, market) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "instrument_type": "perp", + # "instrument_name": "BTC-PERP", + # "scheduled_activation": 1701840228, + # "scheduled_deactivation": 9223372036854776000, + # "is_active": True, + # "tick_size": "0.1", + # "minimum_amount": "0.01", + # "maximum_amount": "10000", + # "amount_step": "0.001", + # "mark_price_fee_rate_cap": "0", + # "maker_fee_rate": "0.00005", + # "taker_fee_rate": "0.0003", + # "base_fee": "0.1", + # "base_currency": "BTC", + # "quote_currency": "USD", + # "option_details": null, + # "perp_details": { + # "index": "BTC-USD", + # "max_rate_per_hour": "0.004", + # "min_rate_per_hour": "-0.004", + # "static_interest_rate": "0.0000125", + # "aggregate_funding": "10512.580833189805742522", + # "funding_rate": "-0.000022223906766867" + # }, + # "erc20_details": null, + # "base_asset_address": "0xDBa83C0C654DB1cd914FA2710bA743e925B53086", + # "base_asset_sub_id": "0", + # "pro_rata_fraction": "0", + # "fifo_min_allocation": "0", + # "pro_rata_amount_step": "0.1", + # "best_ask_amount": "0.012", + # "best_ask_price": "99567.9", + # "best_bid_amount": "0.129", + # "best_bid_price": "99554.5", + # "five_percent_bid_depth": "11.208", + # "five_percent_ask_depth": "11.42", + # "option_pricing": null, + # "index_price": "99577.2", + # "mark_price": "99543.642926357933902181684970855712890625", + # "stats": { + # "contract_volume": "464.712", + # "num_trades": "10681", + # "open_interest": "72.804739389481989861", + # "high": "99519.1", + # "low": "97254.1", + # "percent_change": "0.0128", + # "usd_change": "1258.1" + # }, + # "timestamp": 1736140984000, + # "min_price": "97591.2", + # "max_price": "101535.1" + # } + # + marketId = self.safe_string(ticker, 'instrument_name') + timestamp = self.safe_integer_omit_zero(ticker, 'timestamp') + symbol = self.safe_symbol(marketId, market) + stats = self.safe_dict(ticker, 'stats') + change = self.safe_string(stats, 'percent_change') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(stats, 'high'), + 'low': self.safe_string(stats, 'low'), + 'bid': self.safe_string(ticker, 'best_bid_price'), + 'bidVolume': self.safe_string(ticker, 'best_bid_amount'), + 'ask': self.safe_string(ticker, 'best_ask_price'), + 'askVolume': self.safe_string(ticker, 'best_ask_amount'), + 'vwap': None, + 'open': None, + 'close': None, + 'last': None, + 'previousClose': None, + 'change': change, + 'percentage': Precise.string_mul(change, '100'), + 'average': None, + 'baseVolume': None, + 'quoteVolume': None, + 'indexPrice': self.safe_string(ticker, 'index_price'), + 'markPrice': self.safe_string(ticker, 'mark_price'), + 'info': ticker, + }, market) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://docs.derive.xyz/reference/post_public-get-trade-history + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch trades for + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['instrument_name'] = market['id'] + if limit is not None: + if limit > 1000: + limit = 1000 + request['page_size'] = limit # default 100, max 1000 + if since is not None: + request['from_timestamp'] = since + until = self.safe_integer(params, 'until') + params = self.omit(params, ['until']) + if until is not None: + request['to_timestamp'] = until + response = self.publicPostGetTradeHistory(self.extend(request, params)) + # + # { + # "result": { + # "trades": [ + # { + # "trade_id": "9dbc88b0-f0c4-4439-9cc1-4e6409d4eafb", + # "instrument_name": "BTC-PERP", + # "timestamp": 1736153910930, + # "trade_price": "98995.3", + # "trade_amount": "0.033", + # "mark_price": "98990.875914388161618263", + # "index_price": "99038.050611100001501184", + # "direction": "sell", + # "quote_id": null, + # "wallet": "0x88B6BB87fbFac92a34F8155aaA35c87B5b166fA9", + # "subaccount_id": 8250, + # "tx_status": "settled", + # "tx_hash": "0x020bd735b312f867f17f8cc254946d87cfe9f2c8ff3605035d8129082eb73723", + # "trade_fee": "0.980476701049890015", + # "liquidity_role": "taker", + # "realized_pnl": "-2.92952402688793509", + # "realized_pnl_excl_fees": "-1.949047325838045075" + # } + # ], + # "pagination": { + # "num_pages": 598196, + # "count": 598196 + # } + # }, + # "id": "b8539544-6975-4497-8163-5e51a38e4aa7" + # } + # + result = self.safe_dict(response, 'result', {}) + data = self.safe_list(result, 'trades', []) + return self.parse_trades(data, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # { + # "subaccount_id": 130837, + # "order_id": "30c48194-8d48-43ac-ad00-0d5ba29eddc9", + # "instrument_name": "BTC-PERP", + # "direction": "sell", + # "label": "test1234", + # "quote_id": null, + # "trade_id": "f8a30740-488c-4c2d-905d-e17057bafde1", + # "timestamp": 1738065303708, + # "mark_price": "102740.137375457314192317", + # "index_price": "102741.553409299981533184", + # "trade_price": "102700.6", + # "trade_amount": "0.01", + # "liquidity_role": "taker", + # "realized_pnl": "0", + # "realized_pnl_excl_fees": "0", + # "is_transfer": False, + # "tx_status": "settled", + # "trade_fee": "1.127415534092999815", + # "tx_hash": "0xc55df1f07330faf86579bd8a6385391fbe9e73089301149d8550e9d29c9ead74", + # "transaction_id": "e18b9426-3fa5-41bb-99d3-8b54fb4d51bb" + # } + # + marketId = self.safe_string(trade, 'instrument_name') + symbol = self.safe_symbol(marketId, market) + timestamp = self.safe_integer(trade, 'timestamp') + fee = { + 'currency': 'USDC', + 'cost': self.safe_string(trade, 'trade_fee'), + } + return self.safe_trade({ + 'info': trade, + 'id': self.safe_string(trade, 'trade_id'), + 'order': self.safe_string(trade, 'order_id'), + 'symbol': symbol, + 'side': self.safe_string_lower(trade, 'direction'), + 'type': None, + 'takerOrMaker': self.safe_string(trade, 'liquidity_role'), + 'price': self.safe_string(trade, 'trade_price'), + 'amount': self.safe_string(trade, 'trade_amount'), + 'cost': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'fee': fee, + }, market) + + def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical funding rate prices + + https://docs.derive.xyz/reference/post_public-get-funding-rate-history + + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: timestamp in ms of the earliest funding rate to fetch + :param int [limit]: the maximum amount of funding rate structures to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `funding rate structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'instrument_name': market['id'], + } + if since is not None: + request['start_timestamp'] = since + until = self.safe_integer(params, 'until') + params = self.omit(params, ['until']) + if until is not None: + request['to_timestamp'] = until + response = self.publicPostGetFundingRateHistory(self.extend(request, params)) + # + # { + # "result": { + # "funding_rate_history": [ + # { + # "timestamp": 1736215200000, + # "funding_rate": "-0.000020014" + # } + # ] + # }, + # "id": "3200ab8d-0080-42f0-8517-c13e3d9201d8" + # } + # + result = self.safe_dict(response, 'result', {}) + data = self.safe_list(result, 'funding_rate_history', []) + rates = [] + for i in range(0, len(data)): + entry = data[i] + timestamp = self.safe_integer(entry, 'timestamp') + rates.append({ + 'info': entry, + 'symbol': market['symbol'], + 'fundingRate': self.safe_number(entry, 'funding_rate'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + sorted = self.sort_by(rates, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, market['symbol'], since, limit) + + def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate + + https://docs.derive.xyz/reference/post_public-get-funding-rate-history + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + response = self.fetch_funding_rate_history(symbol, None, 1, params) + # + # [ + # { + # "info": { + # "timestamp": 1736157600000, + # "funding_rate": "-0.000008872" + # }, + # "symbol": "BTC/USD:USDC", + # "fundingRate": -0.000008872, + # "timestamp": 1736157600000, + # "datetime": "2025-01-06T10:00:00.000Z" + # } + # ] + # + data = self.safe_dict(response, 0) + return self.parse_funding_rate(data) + + def parse_funding_rate(self, contract, market: Market = None) -> FundingRate: + symbol = self.safe_string(contract, 'symbol') + fundingTimestamp = self.safe_integer(contract, 'timestamp') + return { + 'info': contract, + 'symbol': symbol, + 'markPrice': None, + 'indexPrice': None, + 'interestRate': None, + 'estimatedSettlePrice': None, + 'timestamp': None, + 'datetime': None, + 'fundingRate': self.safe_number(contract, 'fundingRate'), + 'fundingTimestamp': fundingTimestamp, + 'fundingDatetime': self.iso8601(fundingTimestamp), + 'nextFundingRate': None, + 'nextFundingTimestamp': None, + 'nextFundingDatetime': None, + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': None, + } + + def hash_order_message(self, order): + accountHash = self.hash(self.eth_abi_encode([ + 'bytes32', 'uint256', 'uint256', 'address', 'bytes32', 'uint256', 'address', 'address', + ], order), 'keccak', 'binary') + sandboxMode = self.safe_bool(self.options, 'sandboxMode', False) + DOMAIN_SEPARATOR = '9bcf4dc06df5d8bf23af818d5716491b995020f377d3b7b64c29ed14e3dd1105' if (sandboxMode) else 'd96e5f90797da7ec8dc4e276260c7f3f87fedf68775fbe1ef116e996fc60441b' + binaryDomainSeparator = self.base16_to_binary(DOMAIN_SEPARATOR) + prefix = self.base16_to_binary('1901') + return self.hash(self.binary_concat(prefix, binaryDomainSeparator, accountHash), 'keccak', 'hex') + + def sign_order(self, order, privateKey): + hashOrder = self.hash_order_message(order) + return self.sign_hash(hashOrder[-64:], privateKey[-64:]) + + def hash_message(self, message): + binaryMessage = self.encode(message) + binaryMessageLength = self.binary_length(binaryMessage) + x19 = self.base16_to_binary('19') + newline = self.base16_to_binary('0a') + prefix = self.binary_concat(x19, self.encode('Ethereum Signed Message:'), newline, self.encode(self.number_to_string(binaryMessageLength))) + return '0x' + self.hash(self.binary_concat(prefix, binaryMessage), 'keccak', 'hex') + + def sign_hash(self, hash, privateKey): + self.check_required_credentials() + signature = self.ecdsa(hash[-64:], privateKey[-64:], 'secp256k1', None) + r = signature['r'] + s = signature['s'] + v = self.int_to_base16(self.sum(27, signature['v'])) + return '0x' + r.rjust(64, '0') + s.rjust(64, '0') + v + + def sign_message(self, message, privateKey): + return self.sign_hash(self.hash_message(message), privateKey[-64:]) + + def parse_units(self, num: str, dec='1000000000000000000'): + return Precise.string_mul(num, dec) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://docs.derive.xyz/reference/post_private-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.subaccount_id]: *required* the subaccount id + :param float [params.triggerPrice]: The price a trigger order is triggered at + :param dict [params.takeProfit]: *takeProfit object in params* containing the triggerPrice at which the attached take profit order will be triggered(perpetual swap markets only) + :param float [params.takeProfit.triggerPrice]: take profit trigger price + :param dict [params.stopLoss]: *stopLoss object in params* containing the triggerPrice at which the attached stop loss order will be triggered(perpetual swap markets only) + :param float [params.stopLoss.triggerPrice]: stop loss trigger price + :param float [params.max_fee]: *required* the maximum fee you are willing to pay for the order + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + if price is None: + raise ArgumentsRequired(self.id + ' createOrder() requires a price argument') + subaccountId = None + subaccountId, params = self.handle_derive_subaccount_id('createOrder', params) + test = self.safe_bool(params, 'test', False) + reduceOnly = self.safe_bool_2(params, 'reduceOnly', 'reduce_only') + timeInForce = self.safe_string_lower_2(params, 'timeInForce', 'time_in_force') + postOnly = self.safe_bool(params, 'postOnly') + orderType = type.lower() + orderSide = side.lower() + nonce = self.milliseconds() + # Order signature expiry must be between 2592000 and 7776000 sec from now + signatureExpiry = self.safe_integer(params, 'signature_expiry_sec', self.seconds() + 7776000) + ACTION_TYPEHASH = self.base16_to_binary('4d7a9f27c403ff9c0f19bce61d76d82f9aa29f8d6d4b0c5474607d9770d1af17') + sandboxMode = self.safe_bool(self.options, 'sandboxMode', False) + TRADE_MODULE_ADDRESS = '0x87F2863866D85E3192a35A73b388BD625D83f2be' if (sandboxMode) else '0xB8D20c2B7a1Ad2EE33Bc50eF10876eD3035b5e7b' + priceString = self.number_to_string(price) + maxFee = None + maxFee, params = self.handle_option_and_params(params, 'createOrder', 'max_fee') + if maxFee is None: + raise ArgumentsRequired(self.id + ' createOrder() requires a max_fee argument in params') + maxFeeString = self.number_to_string(maxFee) + amountString = self.number_to_string(amount) + tradeModuleDataHash = self.hash(self.eth_abi_encode([ + 'address', 'uint', 'int', 'int', 'uint', 'uint', 'bool', + ], [ + market['info']['base_asset_address'], + self.parse_to_numeric(market['info']['base_asset_sub_id']), + self.convert_to_big_int(self.parse_units(priceString)), + self.convert_to_big_int(self.parse_units(self.amount_to_precision(symbol, amountString))), + self.convert_to_big_int(self.parse_units(maxFeeString)), + subaccountId, + orderSide == 'buy', + ]), 'keccak', 'binary') + deriveWalletAddress = None + deriveWalletAddress, params = self.handle_derive_wallet_address('createOrder', params) + signature = self.sign_order([ + ACTION_TYPEHASH, + subaccountId, + nonce, + TRADE_MODULE_ADDRESS, + tradeModuleDataHash, + signatureExpiry, + deriveWalletAddress, + self.walletAddress, + ], self.privateKey) + request: dict = { + 'instrument_name': market['id'], + 'direction': orderSide, + 'order_type': orderType, + 'nonce': nonce, + 'amount': amountString, + 'limit_price': priceString, + 'max_fee': maxFeeString, + 'subaccount_id': subaccountId, + 'signature_expiry_sec': signatureExpiry, + 'referral_code': self.safe_string(self.options, 'id', '0x0ad42b8e602c2d3d475ae52d678cf63d84ab2749'), + 'signer': self.walletAddress, + } + if reduceOnly is not None: + request['reduce_only'] = reduceOnly + if reduceOnly and postOnly: + raise InvalidOrder(self.id + ' cannot use reduce only with post only time in force') + if postOnly is not None: + request['time_in_force'] = 'post_only' + elif timeInForce is not None: + request['time_in_force'] = timeInForce + stopLoss = self.safe_value(params, 'stopLoss') + takeProfit = self.safe_value(params, 'takeProfit') + triggerPriceType = self.safe_string(params, 'trigger_price_type', 'mark') + if stopLoss is not None: + stopLossPrice = self.safe_string(stopLoss, 'triggerPrice', stopLoss) + request['trigger_price'] = stopLossPrice + request['trigger_type'] = 'stoploss' + request['trigger_price_type'] = triggerPriceType + elif takeProfit is not None: + takeProfitPrice = self.safe_string(takeProfit, 'triggerPrice', takeProfit) + request['trigger_price'] = takeProfitPrice + request['trigger_type'] = 'takeprofit' + request['trigger_price_type'] = triggerPriceType + clientOrderId = self.safe_string(params, 'clientOrderId') + if clientOrderId is not None: + request['label'] = clientOrderId + request['signature'] = signature + params = self.omit(params, ['reduceOnly', 'reduce_only', 'timeInForce', 'time_in_force', 'postOnly', 'test', 'clientOrderId', 'stopPrice', 'triggerPrice', 'trigger_price', 'stopLoss', 'takeProfit', 'trigger_price_type']) + response = None + if test: + response = self.privatePostOrderDebug(self.extend(request, params)) + else: + response = self.privatePostOrder(self.extend(request, params)) + # + # { + # "result": { + # "raw_data": { + # "subaccount_id": 130837, + # "nonce": 1736923517552, + # "module": "0x87F2863866D85E3192a35A73b388BD625D83f2be", + # "expiry": 86400, + # "owner": "0x108b9aF9279a525b8A8AeAbE7AC2bA925Bc50075", + # "signer": "0x108b9aF9279a525b8A8AeAbE7AC2bA925Bc50075", + # "signature": "0xaa4f42b2f3da33c668fa703ea872d4c3a6b55aca66025b5119e3bebb6679fe2e2794638db51dcace21fc39a498047835994f07eb59f311bb956ce057e66793d1c", + # "data": { + # "asset": "0xAFB6Bb95cd70D5367e2C39e9dbEb422B9815339D", + # "sub_id": 0, + # "limit_price": "10000", + # "desired_amount": "0.001", + # "worst_fee": "0", + # "recipient_id": 130837, + # "is_bid": True, + # "trade_id": "" + # } + # }, + # "encoded_data": "0x000000000000000000000000afb6bb95cd70d5367e2c39e9dbeb422b9815339d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000021e19e0c9bab240000000000000000000000000000000000000000000000000000000038d7ea4c680000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001ff150000000000000000000000000000000000000000000000000000000000000001", + # "encoded_data_hashed": "0xe88fb416bc54dba2d288988f1a82fee40fd792ed555b3471b5f6b4b810d279b4", + # "action_hash": "0x273a0befb3751fa991edc7ed73582456c3b50ae964d458c8f472e932fb6a0069", + # "typed_data_hash": "0x123e2d2f3d5b2473b4e260f51c6459d6bf904e5db8f042a3ea63be8d55329ce9" + # }, + # "id": "f851c8c4-dddf-4b77-93cf-aeddd0966f29" + # } + # { + # "result": { + # "order": { + # "subaccount_id": 130837, + # "order_id": "96349ebb-7d46-43ae-81c7-7ab390444293", + # "instrument_name": "BTC-PERP", + # "direction": "buy", + # "label": "", + # "quote_id": null, + # "creation_timestamp": 1737467576257, + # "last_update_timestamp": 1737467576257, + # "limit_price": "10000", + # "amount": "0.01", + # "filled_amount": "0", + # "average_price": "0", + # "order_fee": "0", + # "order_type": "limit", + # "time_in_force": "gtc", + # "order_status": "open", + # "max_fee": "210", + # "signature_expiry_sec": 1737468175989, + # "nonce": 1737467575989, + # "signer": "0x30CB7B06AdD6749BbE146A6827502B8f2a79269A", + # "signature": "0xd1ca49df1fa06bd805bb59b132ff6c0de29bf973a3e01705abe0a01cc956e4945ed9eb99ab68f3df4c037908113cac5a5bfc3a954a0b7103cdab285962fa6a51c", + # "cancel_reason": "", + # "mmp": False, + # "is_transfer": False, + # "replaced_order_id": null, + # "trigger_type": null, + # "trigger_price_type": null, + # "trigger_price": null, + # "trigger_reject_message": null + # }, + # "trades": [] + # }, + # "id": "397087fa-0125-42af-bfc3-f66166f9fb55" + # } + # + result = self.safe_dict(response, 'result') + rawOrder = self.safe_dict(result, 'raw_data') + if rawOrder is None: + rawOrder = self.safe_dict(result, 'order') + order = self.parse_order(rawOrder, market) + order['type'] = type + return order + + def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + """ + edit a trade order + + https://docs.derive.xyz/reference/post_private-replace + + :param str id: order id + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.subaccount_id]: *required* the subaccount id + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + subaccountId = None + subaccountId, params = self.handle_derive_subaccount_id('editOrder', params) + reduceOnly = self.safe_bool_2(params, 'reduceOnly', 'reduce_only') + timeInForce = self.safe_string_lower_2(params, 'timeInForce', 'time_in_force') + postOnly = self.safe_bool(params, 'postOnly') + orderType = type.lower() + orderSide = side.lower() + nonce = self.milliseconds() + signatureExpiry = self.safe_number(params, 'signature_expiry_sec', self.seconds() + 7776000) + # TODO: subaccount id / trade module address + ACTION_TYPEHASH = self.base16_to_binary('4d7a9f27c403ff9c0f19bce61d76d82f9aa29f8d6d4b0c5474607d9770d1af17') + sandboxMode = self.safe_bool(self.options, 'sandboxMode', False) + TRADE_MODULE_ADDRESS = '0x87F2863866D85E3192a35A73b388BD625D83f2be' if (sandboxMode) else '0xB8D20c2B7a1Ad2EE33Bc50eF10876eD3035b5e7b' + priceString = self.number_to_string(price) + maxFeeString = self.safe_string(params, 'max_fee', '0') + amountString = self.number_to_string(amount) + tradeModuleDataHash = self.hash(self.eth_abi_encode([ + 'address', 'uint', 'int', 'int', 'uint', 'uint', 'bool', + ], [ + market['info']['base_asset_address'], + self.parse_to_numeric(market['info']['base_asset_sub_id']), + self.convert_to_big_int(self.parse_units(priceString)), + self.convert_to_big_int(self.parse_units(self.amount_to_precision(symbol, amountString))), + self.convert_to_big_int(self.parse_units(maxFeeString)), + subaccountId, + orderSide == 'buy', + ]), 'keccak', 'binary') + deriveWalletAddress = None + deriveWalletAddress, params = self.handle_derive_wallet_address('editOrder', params) + signature = self.sign_order([ + ACTION_TYPEHASH, + subaccountId, + nonce, + TRADE_MODULE_ADDRESS, + tradeModuleDataHash, + signatureExpiry, + deriveWalletAddress, + self.walletAddress, + ], self.privateKey) + request: dict = { + 'instrument_name': market['id'], + 'order_id_to_cancel': id, + 'direction': orderSide, + 'order_type': orderType, + 'nonce': nonce, + 'amount': amountString, + 'limit_price': priceString, + 'max_fee': maxFeeString, + 'subaccount_id': subaccountId, + 'signature_expiry_sec': signatureExpiry, + 'signer': self.walletAddress, + } + if reduceOnly is not None: + request['reduce_only'] = reduceOnly + if reduceOnly and postOnly: + raise InvalidOrder(self.id + ' cannot use reduce only with post only time in force') + if postOnly is not None: + request['time_in_force'] = 'post_only' + elif timeInForce is not None: + request['time_in_force'] = timeInForce + clientOrderId = self.safe_string(params, 'clientOrderId') + if clientOrderId is not None: + request['label'] = clientOrderId + request['signature'] = signature + params = self.omit(params, ['reduceOnly', 'reduce_only', 'timeInForce', 'time_in_force', 'postOnly', 'clientOrderId']) + response = self.privatePostReplace(self.extend(request, params)) + # + # { + # "result": + # { + # "cancelled_order": + # { + # "subaccount_id": 130837, + # "order_id": "c2337704-f1af-437d-91c8-dddb9d6bac59", + # "instrument_name": "BTC-PERP", + # "direction": "buy", + # "label": "test1234", + # "quote_id": null, + # "creation_timestamp": 1737539743959, + # "last_update_timestamp": 1737539764234, + # "limit_price": "10000", + # "amount": "0.01", + # "filled_amount": "0", + # "average_price": "0", + # "order_fee": "0", + # "order_type": "limit", + # "time_in_force": "post_only", + # "order_status": "cancelled", + # "max_fee": "211", + # "signature_expiry_sec": 1737540343631, + # "nonce": 1737539743631, + # "signer": "0x30CB7B06AdD6749BbE146A6827502B8f2a79269A", + # "signature": "0xdb669e18f407a3efa816b79c0dd3bac1c651d4dbf3caad4db67678ce9b81c76378d787a08143a30707eb0827ce4626640767c9f174358df1b90611bd6d1391711b", + # "cancel_reason": "user_request", + # "mmp": False, + # "is_transfer": False, + # "replaced_order_id": null, + # "trigger_type": null, + # "trigger_price_type": null, + # "trigger_price": null, + # "trigger_reject_message": null, + # }, + # "order": + # { + # "subaccount_id": 130837, + # "order_id": "97af0902-813f-4892-a54b-797e5689db05", + # "instrument_name": "BTC-PERP", + # "direction": "buy", + # "label": "test1234", + # "quote_id": null, + # "creation_timestamp": 1737539764154, + # "last_update_timestamp": 1737539764154, + # "limit_price": "10000", + # "amount": "0.01", + # "filled_amount": "0", + # "average_price": "0", + # "order_fee": "0", + # "order_type": "limit", + # "time_in_force": "post_only", + # "order_status": "open", + # "max_fee": "211", + # "signature_expiry_sec": 1737540363890, + # "nonce": 1737539763890, + # "signer": "0x30CB7B06AdD6749BbE146A6827502B8f2a79269A", + # "signature": "0xef2c459ab4797cbbd7d97b47678ff172542af009bac912bf53e7879cf92eb1aa6b1a6cf40bf0928684f5394942fb424cc2db71eac0eaf7226a72480034332f291c", + # "cancel_reason": "", + # "mmp": False, + # "is_transfer": False, + # "replaced_order_id": "c2337704-f1af-437d-91c8-dddb9d6bac59", + # "trigger_type": null, + # "trigger_price_type": null, + # "trigger_price": null, + # "trigger_reject_message": null, + # }, + # "trades": [], + # "create_order_error": null, + # }, + # "id": "fb19e991-15f6-4c80-a20c-917e762a1a38", + # } + # + result = self.safe_dict(response, 'result') + rawOrder = self.safe_dict(result, 'order') + order = self.parse_order(rawOrder, market) + return order + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + + https://docs.derive.xyz/reference/post_private-cancel + + cancels an open order + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: whether the order is a trigger/algo order + :param str [params.subaccount_id]: *required* the subaccount id + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + self.load_markets() + market: Market = self.market(symbol) + isTrigger = self.safe_bool_2(params, 'trigger', 'stop', False) + subaccountId = None + subaccountId, params = self.handle_derive_subaccount_id('cancelOrder', params) + params = self.omit(params, ['trigger', 'stop']) + request: dict = { + 'instrument_name': market['id'], + 'subaccount_id': subaccountId, + } + clientOrderIdUnified = self.safe_string(params, 'clientOrderId') + clientOrderIdExchangeSpecific = self.safe_string(params, 'label', clientOrderIdUnified) + isByClientOrder = clientOrderIdExchangeSpecific is not None + response = None + if isByClientOrder: + request['label'] = clientOrderIdExchangeSpecific + params = self.omit(params, ['clientOrderId', 'label']) + response = self.privatePostCancelByLabel(self.extend(request, params)) + else: + request['order_id'] = id + if isTrigger: + response = self.privatePostCancelTriggerOrder(self.extend(request, params)) + else: + response = self.privatePostCancel(self.extend(request, params)) + # + # { + # "result": { + # "subaccount_id": 130837, + # "order_id": "de4f30b6-0dcb-4df6-9222-c1a27f1ad80d", + # "instrument_name": "BTC-PERP", + # "direction": "buy", + # "label": "test1234", + # "quote_id": null, + # "creation_timestamp": 1737540100989, + # "last_update_timestamp": 1737540574696, + # "limit_price": "10000", + # "amount": "0.01", + # "filled_amount": "0", + # "average_price": "0", + # "order_fee": "0", + # "order_type": "limit", + # "time_in_force": "post_only", + # "order_status": "cancelled", + # "max_fee": "211", + # "signature_expiry_sec": 1737540700726, + # "nonce": 1737540100726, + # "signer": "0x30CB7B06AdD6749BbE146A6827502B8f2a79269A", + # "signature": "0x9cd1a6e32a0699929e4e090c08c548366b1353701ec56e02d5cdf37fc89bd19b7b29e00e57e8383bb6336d73019027a7e2a4364f40859e7a949115024c7f199a1b", + # "cancel_reason": "user_request", + # "mmp": False, + # "is_transfer": False, + # "replaced_order_id": "4ccc89ba-3c3d-4047-8900-0aa5fb4ef706", + # "trigger_type": null, + # "trigger_price_type": null, + # "trigger_price": null, + # "trigger_reject_message": null + # }, + # "id": "cef61e2a-cb13-4779-8e6b-535361981fad" + # } + # + # { + # "result": { + # "cancelled_orders": 1 + # }, + # "id": "674e075e-1e8a-4a47-99ff-75efbdd2370f" + # } + # + extendParams: dict = {'symbol': symbol} + order = self.safe_dict(response, 'result') + if isByClientOrder: + extendParams['client_order_id'] = clientOrderIdExchangeSpecific + return self.extend(self.parse_order(order, market), extendParams) + + def cancel_all_orders(self, symbol: Str = None, params={}): + """ + + https://docs.derive.xyz/reference/post_private-cancel-by-instrument + https://docs.derive.xyz/reference/post_private-cancel-all + + cancel all open orders in a market + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.subaccount_id]: *required* the subaccount id + :returns dict: an list of `order structures ` + """ + self.load_markets() + market: Market = None + if symbol is not None: + market = self.market(symbol) + subaccountId = None + subaccountId, params = self.handle_derive_subaccount_id('cancelAllOrders', params) + request: dict = { + 'subaccount_id': subaccountId, + } + response = None + if market is not None: + request['instrument_name'] = market['id'] + response = self.privatePostCancelByInstrument(self.extend(request, params)) + else: + response = self.privatePostCancelAll(self.extend(request, params)) + # + # { + # "result": { + # "cancelled_orders": 0 + # }, + # "id": "9d633799-2098-4559-b547-605bb6f4d8f4" + # } + # + # { + # "id": "45548646-c74f-4ca2-9de4-551e6de49afa", + # "result": "ok" + # } + # + return [self.safe_order({'info': response})] + + def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://docs.derive.xyz/reference/post_private-get-orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: set to True if you want to fetch orders with pagination + :param boolean [params.trigger]: whether the order is a trigger/algo order + :param str [params.subaccount_id]: *required* the subaccount id + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOrders', 'paginate') + if paginate: + return self.fetch_paginated_call_incremental('fetchOrders', symbol, since, limit, params, 'page', 500) + isTrigger = self.safe_bool_2(params, 'trigger', 'stop', False) + params = self.omit(params, ['trigger', 'stop']) + subaccountId = None + subaccountId, params = self.handle_derive_subaccount_id('fetchOrders', params) + request: dict = { + 'subaccount_id': subaccountId, + } + market: Market = None + if symbol is not None: + market = self.market(symbol) + request['instrument_name'] = market['id'] + if limit is not None: + request['page_size'] = limit + else: + request['page_size'] = 500 + if isTrigger: + request['status'] = 'untriggered' + response = self.privatePostGetOrders(self.extend(request, params)) + # + # { + # "result": { + # "subaccount_id": 130837, + # "orders": [ + # { + # "subaccount_id": 130837, + # "order_id": "63a80cb8-387b-472b-a838-71cd9513c365", + # "instrument_name": "BTC-PERP", + # "direction": "buy", + # "label": "test1234", + # "quote_id": null, + # "creation_timestamp": 1737551053207, + # "last_update_timestamp": 1737551053207, + # "limit_price": "10000", + # "amount": "0.01", + # "filled_amount": "0", + # "average_price": "0", + # "order_fee": "0", + # "order_type": "limit", + # "time_in_force": "post_only", + # "order_status": "open", + # "max_fee": "211", + # "signature_expiry_sec": 1737551652765, + # "nonce": 1737551052765, + # "signer": "0x30CB7B06AdD6749BbE146A6827502B8f2a79269A", + # "signature": "0x35535ccb1bcad509ecc435c79e966174db6403fc9aeee1e237d08a941014c57b59279dfe4be39e081f9921a53eaad59cb2a151d9f52f2d05fc47e6280254952e1c", + # "cancel_reason": "", + # "mmp": False, + # "is_transfer": False, + # "replaced_order_id": null, + # "trigger_type": null, + # "trigger_price_type": null, + # "trigger_price": null, + # "trigger_reject_message": null + # } + # ], + # "pagination": { + # "num_pages": 1, + # "count": 1 + # } + # }, + # "id": "e5a88d4f-7ac7-40cd-aec9-e0e8152b8b92" + # } + # + data = self.safe_value(response, 'result') + page = self.safe_integer(params, 'page') + if page is not None: + pagination = self.safe_dict(data, 'pagination') + currentPage = self.safe_integer(pagination, 'num_pages') + if page > currentPage: + return [] + orders = self.safe_list(data, 'orders') + return self.parse_orders(orders, market, since, limit) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://docs.derive.xyz/reference/post_private-get-orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: set to True if you want to fetch orders with pagination + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + extendedParams = self.extend(params, {'status': 'open'}) + return self.fetch_orders(symbol, since, limit, extendedParams) + + def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://docs.derive.xyz/reference/post_private-get-orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: set to True if you want to fetch orders with pagination + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + extendedParams = self.extend(params, {'status': 'filled'}) + return self.fetch_orders(symbol, since, limit, extendedParams) + + def fetch_canceled_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches information on multiple canceled orders made by the user + + https://docs.derive.xyz/reference/post_private-get-orders + + :param str symbol: unified market symbol of the market the orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `order structures ` + """ + self.load_markets() + extendedParams = self.extend(params, {'status': 'cancelled'}) + return self.fetch_orders(symbol, since, limit, extendedParams) + + def parse_time_in_force(self, timeInForce: Str): + timeInForces: dict = { + 'ioc': 'IOC', + 'fok': 'FOK', + 'gtc': 'GTC', + 'post_only': 'PO', + } + return self.safe_string(timeInForces, timeInForce, None) + + def parse_order_status(self, status: Str): + if status is not None: + statuses: dict = { + 'open': 'open', + 'untriggered': 'open', + 'filled': 'closed', + 'cancelled': 'canceled', + 'expired': 'rejected', + } + return self.safe_string(statuses, status, status) + return status + + def parse_order(self, rawOrder: dict, market: Market = None) -> Order: + # + # { + # "subaccount_id": 130837, + # "nonce": 1736923517552, + # "module": "0x87F2863866D85E3192a35A73b388BD625D83f2be", + # "expiry": 86400, + # "owner": "0x108b9aF9279a525b8A8AeAbE7AC2bA925Bc50075", + # "signer": "0x108b9aF9279a525b8A8AeAbE7AC2bA925Bc50075", + # "signature": "0xaa4f42b2f3da33c668fa703ea872d4c3a6b55aca66025b5119e3bebb6679fe2e2794638db51dcace21fc39a498047835994f07eb59f311bb956ce057e66793d1c", + # "data": { + # "asset": "0xAFB6Bb95cd70D5367e2C39e9dbEb422B9815339D", + # "sub_id": 0, + # "limit_price": "10000", + # "desired_amount": "0.001", + # "worst_fee": "0", + # "recipient_id": 130837, + # "is_bid": True, + # "trade_id": "" + # } + # } + # { + # "subaccount_id": 130837, + # "order_id": "96349ebb-7d46-43ae-81c7-7ab390444293", + # "instrument_name": "BTC-PERP", + # "direction": "buy", + # "label": "", + # "quote_id": null, + # "creation_timestamp": 1737467576257, + # "last_update_timestamp": 1737467576257, + # "limit_price": "10000", + # "amount": "0.01", + # "filled_amount": "0", + # "average_price": "0", + # "order_fee": "0", + # "order_type": "limit", + # "time_in_force": "gtc", + # "order_status": "open", + # "max_fee": "210", + # "signature_expiry_sec": 1737468175989, + # "nonce": 1737467575989, + # "signer": "0x30CB7B06AdD6749BbE146A6827502B8f2a79269A", + # "signature": "0xd1ca49df1fa06bd805bb59b132ff6c0de29bf973a3e01705abe0a01cc956e4945ed9eb99ab68f3df4c037908113cac5a5bfc3a954a0b7103cdab285962fa6a51c", + # "cancel_reason": "", + # "mmp": False, + # "is_transfer": False, + # "replaced_order_id": null, + # "trigger_type": null, + # "trigger_price_type": null, + # "trigger_price": null, + # "trigger_reject_message": null + # } + order = self.safe_dict(rawOrder, 'data') + if order is None: + order = rawOrder + timestamp = self.safe_integer_2(rawOrder, 'creation_timestamp', 'nonce') + orderId = self.safe_string(order, 'order_id') + marketId = self.safe_string(order, 'instrument_name') + if marketId is not None: + market = self.safe_market(marketId, market) + symbol = market['symbol'] + price = self.safe_string(order, 'limit_price') + average = self.safe_string(order, 'average_price') + amount = self.safe_string(order, 'desired_amount') + filled = self.safe_string(order, 'filled_amount') + fee = self.safe_string(order, 'order_fee') + orderType = self.safe_string_lower(order, 'order_type') + isBid = self.safe_bool(order, 'is_bid') + side = self.safe_string(order, 'direction') + if side is None: + if isBid: + side = 'buy' + else: + side = 'sell' + triggerType = self.safe_string(order, 'trigger_type') + stopLossPrice = None + takeProfitPrice = None + triggerPrice = None + if triggerType is not None: + triggerPrice = self.safe_string(order, 'trigger_price') + if triggerType == 'stoploss': + stopLossPrice = triggerPrice + else: + takeProfitPrice = triggerPrice + lastUpdateTimestamp = self.safe_integer(rawOrder, 'last_update_timestamp') + status = self.safe_string(order, 'order_status') + timeInForce = self.safe_string(order, 'time_in_force') + return self.safe_order({ + 'id': orderId, + 'clientOrderId': self.safe_string(order, 'label'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'lastUpdateTimestamp': lastUpdateTimestamp, + 'status': self.parse_order_status(status), + 'symbol': symbol, + 'type': orderType, + 'timeInForce': self.parse_time_in_force(timeInForce), + 'postOnly': None, # handled in safeOrder + 'reduceOnly': self.safe_bool(order, 'reduce_only'), + 'side': side, + 'price': price, + 'triggerPrice': triggerPrice, + 'takeProfitPrice': takeProfitPrice, + 'stopLossPrice': stopLossPrice, + 'average': average, + 'amount': amount, + 'filled': filled, + 'remaining': None, + 'cost': None, + 'trades': None, + 'fee': { + 'cost': fee, + 'currency': 'USDC', + }, + 'info': order, + }, market) + + def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all the trades made from a single order + + https://docs.derive.xyz/reference/post_private-get-trade-history + + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.subaccount_id]: *required* the subaccount id + :returns dict[]: a list of `trade structures ` + """ + self.load_markets() + subaccountId = None + subaccountId, params = self.handle_derive_subaccount_id('fetchOrderTrades', params) + request: dict = { + 'order_id': id, + 'subaccount_id': subaccountId, + } + market: Market = None + if symbol is not None: + market = self.market(symbol) + request['instrument_name'] = market['id'] + if limit is not None: + request['page_size'] = limit + if since is not None: + request['from_timestamp'] = since + response = self.privatePostGetTradeHistory(self.extend(request, params)) + # + # { + # "result": { + # "subaccount_id": 130837, + # "trades": [ + # { + # "subaccount_id": 130837, + # "order_id": "30c48194-8d48-43ac-ad00-0d5ba29eddc9", + # "instrument_name": "BTC-PERP", + # "direction": "sell", + # "label": "test1234", + # "quote_id": null, + # "trade_id": "f8a30740-488c-4c2d-905d-e17057bafde1", + # "timestamp": 1738065303708, + # "mark_price": "102740.137375457314192317", + # "index_price": "102741.553409299981533184", + # "trade_price": "102700.6", + # "trade_amount": "0.01", + # "liquidity_role": "taker", + # "realized_pnl": "0", + # "realized_pnl_excl_fees": "0", + # "is_transfer": False, + # "tx_status": "settled", + # "trade_fee": "1.127415534092999815", + # "tx_hash": "0xc55df1f07330faf86579bd8a6385391fbe9e73089301149d8550e9d29c9ead74", + # "transaction_id": "e18b9426-3fa5-41bb-99d3-8b54fb4d51bb" + # } + # ], + # "pagination": { + # "num_pages": 1, + # "count": 1 + # } + # }, + # "id": "a16f798c-a121-44e2-b77e-c38a063f8a99" + # } + # + result = self.safe_dict(response, 'result', {}) + trades = self.safe_list(result, 'trades', []) + return self.parse_trades(trades, market, since, limit, params) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://docs.derive.xyz/reference/post_private-get-trade-history + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: set to True if you want to fetch trades with pagination + :param str [params.subaccount_id]: *required* the subaccount id + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchMyTrades', 'paginate') + if paginate: + return self.fetch_paginated_call_incremental('fetchMyTrades', symbol, since, limit, params, 'page', 500) + subaccountId = None + subaccountId, params = self.handle_derive_subaccount_id('fetchMyTrades', params) + request: dict = { + 'subaccount_id': subaccountId, + } + market: Market = None + if symbol is not None: + market = self.market(symbol) + request['instrument_name'] = market['id'] + if limit is not None: + request['page_size'] = limit + if since is not None: + request['from_timestamp'] = since + response = self.privatePostGetTradeHistory(self.extend(request, params)) + # + # { + # "result": { + # "subaccount_id": 130837, + # "trades": [ + # { + # "subaccount_id": 130837, + # "order_id": "30c48194-8d48-43ac-ad00-0d5ba29eddc9", + # "instrument_name": "BTC-PERP", + # "direction": "sell", + # "label": "test1234", + # "quote_id": null, + # "trade_id": "f8a30740-488c-4c2d-905d-e17057bafde1", + # "timestamp": 1738065303708, + # "mark_price": "102740.137375457314192317", + # "index_price": "102741.553409299981533184", + # "trade_price": "102700.6", + # "trade_amount": "0.01", + # "liquidity_role": "taker", + # "realized_pnl": "0", + # "realized_pnl_excl_fees": "0", + # "is_transfer": False, + # "tx_status": "settled", + # "trade_fee": "1.127415534092999815", + # "tx_hash": "0xc55df1f07330faf86579bd8a6385391fbe9e73089301149d8550e9d29c9ead74", + # "transaction_id": "e18b9426-3fa5-41bb-99d3-8b54fb4d51bb" + # } + # ], + # "pagination": { + # "num_pages": 1, + # "count": 1 + # } + # }, + # "id": "a16f798c-a121-44e2-b77e-c38a063f8a99" + # } + # + result = self.safe_dict(response, 'result', {}) + page = self.safe_integer(params, 'page') + if page is not None: + pagination = self.safe_dict(result, 'pagination') + currentPage = self.safe_integer(pagination, 'num_pages') + if page > currentPage: + return [] + trades = self.safe_list(result, 'trades', []) + return self.parse_trades(trades, market, since, limit, params) + + def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://docs.derive.xyz/reference/post_private-get-positions + + :param str[] [symbols]: not used by kraken fetchPositions() + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.subaccount_id]: *required* the subaccount id + :returns dict[]: a list of `position structure ` + """ + self.load_markets() + subaccountId = None + subaccountId, params = self.handle_derive_subaccount_id('fetchPositions', params) + request: dict = { + 'subaccount_id': subaccountId, + } + params = self.omit(params, ['subaccount_id']) + response = self.privatePostGetPositions(self.extend(request, params)) + # + # { + # "result": { + # "subaccount_id": 130837, + # "positions": [ + # { + # "instrument_type": "perp", + # "instrument_name": "BTC-PERP", + # "amount": "-0.02", + # "average_price": "102632.9105389869500088", + # "realized_pnl": "0", + # "unrealized_pnl": "-2.6455959784245548835819950103759765625", + # "total_fees": "2.255789220260999824", + # "average_price_excl_fees": "102745.7", + # "realized_pnl_excl_fees": "0", + # "unrealized_pnl_excl_fees": "-0.3898067581635550595819950103759765625", + # "net_settlements": "-4.032902047219498639", + # "cumulative_funding": "-0.004677736347850093", + # "pending_funding": "0", + # "mark_price": "102765.190337908177752979099750518798828125", + # "index_price": "102767.657193800017641472", + # "delta": "1", + # "gamma": "0", + # "vega": "0", + # "theta": "0", + # "mark_value": "1.38730606879471451975405216217041015625", + # "maintenance_margin": "-101.37788426911356509663164615631103515625", + # "initial_margin": "-132.2074413704858670826070010662078857421875", + # "open_orders_margin": "264.116085900726830004714429378509521484375", + # "leverage": "8.6954476205089299495699106539379941746377322586618", + # "liquidation_price": "109125.705451984322280623018741607666015625", + # "creation_timestamp": 1738065303840 + # } + # ] + # }, + # "id": "167350f1-d9fc-41d4-9797-1c78f83fda8e" + # } + # + result = self.safe_dict(response, 'result', {}) + positions = self.safe_list(result, 'positions', []) + return self.parse_positions(positions, symbols) + + def parse_position(self, position: dict, market: Market = None): + # + # { + # "instrument_type": "perp", + # "instrument_name": "BTC-PERP", + # "amount": "-0.02", + # "average_price": "102632.9105389869500088", + # "realized_pnl": "0", + # "unrealized_pnl": "-2.6455959784245548835819950103759765625", + # "total_fees": "2.255789220260999824", + # "average_price_excl_fees": "102745.7", + # "realized_pnl_excl_fees": "0", + # "unrealized_pnl_excl_fees": "-0.3898067581635550595819950103759765625", + # "net_settlements": "-4.032902047219498639", + # "cumulative_funding": "-0.004677736347850093", + # "pending_funding": "0", + # "mark_price": "102765.190337908177752979099750518798828125", + # "index_price": "102767.657193800017641472", + # "delta": "1", + # "gamma": "0", + # "vega": "0", + # "theta": "0", + # "mark_value": "1.38730606879471451975405216217041015625", + # "maintenance_margin": "-101.37788426911356509663164615631103515625", + # "initial_margin": "-132.2074413704858670826070010662078857421875", + # "open_orders_margin": "264.116085900726830004714429378509521484375", + # "leverage": "8.6954476205089299495699106539379941746377322586618", + # "liquidation_price": "109125.705451984322280623018741607666015625", + # "creation_timestamp": 1738065303840 + # } + # + contract = self.safe_string(position, 'instrument_name') + market = self.safe_market(contract, market) + size = self.safe_string(position, 'amount') + side: Str = None + if Precise.string_gt(size, '0'): + side = 'long' + else: + side = 'short' + contractSize = self.safe_string(market, 'contractSize') + markPrice = self.safe_string(position, 'mark_price') + timestamp = self.safe_integer(position, 'creation_timestamp') + unrealisedPnl = self.safe_string(position, 'unrealized_pnl') + size = Precise.string_abs(size) + notional = Precise.string_mul(size, markPrice) + return self.safe_position({ + 'info': position, + 'id': None, + 'symbol': self.safe_string(market, 'symbol'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastUpdateTimestamp': None, + 'initialMargin': self.safe_string(position, 'initial_margin'), + 'initialMarginPercentage': None, + 'maintenanceMargin': self.safe_string(position, 'maintenance_margin'), + 'maintenanceMarginPercentage': None, + 'entryPrice': None, + 'notional': self.parse_number(notional), + 'leverage': self.safe_number(position, 'leverage'), + 'unrealizedPnl': self.parse_number(unrealisedPnl), + 'contracts': self.parse_number(size), + 'contractSize': self.parse_number(contractSize), + 'marginRatio': None, + 'liquidationPrice': self.safe_number(position, 'liquidation_price'), + 'markPrice': self.parse_number(markPrice), + 'lastPrice': None, + 'collateral': None, + 'marginMode': None, + 'side': side, + 'percentage': None, + 'hedged': None, + 'stopLossPrice': None, + 'takeProfitPrice': None, + }) + + def fetch_funding_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch the history of funding payments paid and received on self account + + https://docs.derive.xyz/reference/post_private-get-funding-history + + :param str [symbol]: unified market symbol + :param int [since]: the earliest time in ms to fetch funding history for + :param int [limit]: the maximum number of funding history structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict: a `funding history structure ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchFundingHistory', 'paginate') + if paginate: + return self.fetch_paginated_call_incremental('fetchFundingHistory', symbol, since, limit, params, 'page', 500) + subaccountId = None + subaccountId, params = self.handle_derive_subaccount_id('fetchFundingHistory', params) + request: dict = { + 'subaccount_id': subaccountId, + } + market: Market = None + if symbol is not None: + market = self.market(symbol) + request['instrument_name'] = market['id'] + if since is not None: + request['start_timestamp'] = since + if limit is not None: + request['page_size'] = limit + response = self.privatePostGetFundingHistory(self.extend(request, params)) + # + # { + # "result": { + # "events": [ + # { + # "instrument_name": "BTC-PERP", + # "timestamp": 1738066618272, + # "funding": "-0.004677736347850093", + # "pnl": "-0.944081615774632967" + # }, + # { + # "instrument_name": "BTC-PERP", + # "timestamp": 1738066617964, + # "funding": "0", + # "pnl": "-0.437556413479249408" + # }, + # { + # "instrument_name": "BTC-PERP", + # "timestamp": 1738065307565, + # "funding": "0", + # "pnl": "-0.39547479770461644" + # } + # ], + # "pagination": { + # "num_pages": 1, + # "count": 3 + # } + # }, + # "id": "524b817f-2108-467f-8795-511066f4acec" + # } + # + result = self.safe_dict(response, 'result', {}) + page = self.safe_integer(params, 'page') + if page is not None: + pagination = self.safe_dict(result, 'pagination') + currentPage = self.safe_integer(pagination, 'num_pages') + if page > currentPage: + return [] + events = self.safe_list(result, 'events', []) + return self.parse_incomes(events, market, since, limit) + + def parse_income(self, income, market: Market = None): + # + # { + # "instrument_name": "BTC-PERP", + # "timestamp": 1738065307565, + # "funding": "0", + # "pnl": "-0.39547479770461644" + # } + # + marketId = self.safe_string(income, 'instrument_name') + symbol = self.safe_symbol(marketId, market) + rate = self.safe_string(income, 'funding') + code = self.safe_currency_code('USDC') + timestamp = self.safe_integer(income, 'timestamp') + return { + 'info': income, + 'symbol': symbol, + 'code': code, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': None, + 'amount': None, + 'rate': rate, + } + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://docs.derive.xyz/reference/post_private-get-all-portfolios + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + deriveWalletAddress = None + deriveWalletAddress, params = self.handle_derive_wallet_address('fetchBalance', params) + request = { + 'wallet': deriveWalletAddress, + } + response = self.privatePostGetAllPortfolios(self.extend(request, params)) + # + # { + # "result": [{ + # "subaccount_id": 130837, + # "label": "", + # "currency": "all", + # "margin_type": "SM", + # "is_under_liquidation": False, + # "positions_value": "0", + # "collaterals_value": "318.0760325000001103035174310207366943359375", + # "subaccount_value": "318.0760325000001103035174310207366943359375", + # "positions_maintenance_margin": "0", + # "positions_initial_margin": "0", + # "collaterals_maintenance_margin": "238.557024375000082727638073265552520751953125", + # "collaterals_initial_margin": "190.845619500000083235136116854846477508544921875", + # "maintenance_margin": "238.557024375000082727638073265552520751953125", + # "initial_margin": "190.845619500000083235136116854846477508544921875", + # "open_orders_margin": "0", + # "projected_margin_change": "0", + # "open_orders": [], + # "positions": [], + # "collaterals": [ + # { + # "asset_type": "erc20", + # "asset_name": "ETH", + # "currency": "ETH", + # "amount": "0.1", + # "mark_price": "3180.760325000000438272", + # "mark_value": "318.0760325000001103035174310207366943359375", + # "cumulative_interest": "0", + # "pending_interest": "0", + # "initial_margin": "190.845619500000083235136116854846477508544921875", + # "maintenance_margin": "238.557024375000082727638073265552520751953125", + # "realized_pnl": "0", + # "average_price": "3184.891931", + # "unrealized_pnl": "-0.413161", + # "total_fees": "0", + # "average_price_excl_fees": "3184.891931", + # "realized_pnl_excl_fees": "0", + # "unrealized_pnl_excl_fees": "-0.413161", + # "open_orders_margin": "0", + # "creation_timestamp": 1736860533493 + # } + # ] + # }], + # "id": "27b9a64e-3379-4ce6-a126-9fb941c4a970" + # } + # + result = self.safe_list(response, 'result') + return self.parse_balance(result) + + def parse_balance(self, response) -> Balances: + result: dict = { + 'info': response, + } + for i in range(0, len(response)): + subaccount = response[i] + collaterals = self.safe_list(subaccount, 'collaterals', []) + for j in range(0, len(collaterals)): + balance = collaterals[j] + code = self.safe_currency_code(self.safe_string(balance, 'currency')) + account = self.safe_dict(result, code) + if account is None: + account = self.account() + account['total'] = self.safe_string(balance, 'amount') + else: + amount = self.safe_string(balance, 'amount') + account['total'] = Precise.string_add(account['total'], amount) + result[code] = account + return self.safe_balance(result) + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://docs.derive.xyz/reference/post_private-get-deposit-history + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.subaccount_id]: *required* the subaccount id + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + subaccountId = None + subaccountId, params = self.handle_derive_subaccount_id('fetchDeposits', params) + request: dict = { + 'subaccount_id': subaccountId, + } + if since is not None: + request['start_timestamp'] = since + response = self.privatePostGetDepositHistory(self.extend(request, params)) + # + # { + # "result": { + # "events": [ + # { + # "timestamp": 1736860533599, + # "transaction_id": "f2069395-ec00-49f5-925a-87202a5d240f", + # "asset": "ETH", + # "amount": "0.1", + # "tx_status": "settled", + # "tx_hash": "0xeda21a315c59302a19c42049b4cef05a10b685302b6cc3edbaf49102d91166d4", + # "error_log": {} + # } + # ] + # }, + # "id": "ceebc730-22ab-40cd-9941-33ceb2a74389" + # } + # + currency = self.safe_currency(code) + result = self.safe_dict(response, 'result', {}) + events = self.safe_list(result, 'events') + return self.parse_transactions(events, currency, since, limit, params) + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://docs.derive.xyz/reference/post_private-get-withdrawal-history + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.subaccount_id]: *required* the subaccount id + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + subaccountId = None + subaccountId, params = self.handle_derive_subaccount_id('fetchWithdrawals', params) + request: dict = { + 'subaccount_id': subaccountId, + } + if since is not None: + request['start_timestamp'] = since + response = self.privatePostGetWithdrawalHistory(self.extend(request, params)) + # + # { + # "result": { + # "events": [ + # { + # "timestamp": 1736860533599, + # "transaction_id": "f2069395-ec00-49f5-925a-87202a5d240f", + # "asset": "ETH", + # "amount": "0.1", + # "tx_status": "settled", + # "tx_hash": "0xeda21a315c59302a19c42049b4cef05a10b685302b6cc3edbaf49102d91166d4", + # "error_log": {} + # } + # ] + # }, + # "id": "ceebc730-22ab-40cd-9941-33ceb2a74389" + # } + # + currency = self.safe_currency(code) + result = self.safe_dict(response, 'result', {}) + events = self.safe_list(result, 'events') + return self.parse_transactions(events, currency, since, limit, params) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # { + # "timestamp": 1736860533599, + # "transaction_id": "f2069395-ec00-49f5-925a-87202a5d240f", + # "asset": "ETH", + # "amount": "0.1", + # "tx_status": "settled", + # "tx_hash": "0xeda21a315c59302a19c42049b4cef05a10b685302b6cc3edbaf49102d91166d4", + # "error_log": {} + # } + # + code = self.safe_string(transaction, 'asset') + timestamp = self.safe_integer(transaction, 'timestamp') + txId = self.safe_string(transaction, 'tx_hash') + if txId == '0x0': + txId = None + return { + 'info': transaction, + 'id': None, + 'txid': txId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'address': None, + 'addressFrom': None, + 'addressTo': None, + 'tag': None, + 'tagFrom': None, + 'tagTo': None, + 'type': None, + 'amount': self.safe_number(transaction, 'amount'), + 'currency': code, + 'status': self.parse_transaction_status(self.safe_string(transaction, 'tx_status')), + 'updated': None, + 'comment': None, + 'internal': None, + 'fee': None, + 'network': None, + } + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'settled': 'ok', + 'reverted': 'failed', + } + return self.safe_string(statuses, status, status) + + def handle_derive_subaccount_id(self, methodName: str, params: dict): + derivesubAccountId = None + derivesubAccountId, params = self.handle_option_and_params(params, methodName, 'subaccount_id') + if (derivesubAccountId is not None) and (derivesubAccountId != ''): + self.options['subaccount_id'] = derivesubAccountId # saving in options + return [derivesubAccountId, params] + optionsWallet = self.safe_string(self.options, 'subaccount_id') + if optionsWallet is not None: + return [optionsWallet, params] + raise ArgumentsRequired(self.id + ' ' + methodName + '() requires a subaccount_id parameter inside \'params\' or exchange.options[\'subaccount_id\']=ID.') + + def handle_derive_wallet_address(self, methodName: str, params: dict): + deriveWalletAddress = None + deriveWalletAddress, params = self.handle_option_and_params(params, methodName, 'deriveWalletAddress') + if (deriveWalletAddress is not None) and (deriveWalletAddress != ''): + self.options['deriveWalletAddress'] = deriveWalletAddress # saving in options + return [deriveWalletAddress, params] + optionsWallet = self.safe_string(self.options, 'deriveWalletAddress') + if optionsWallet is not None: + return [optionsWallet, params] + raise ArgumentsRequired(self.id + ' ' + methodName + '() requires a deriveWalletAddress parameter inside \'params\' or exchange.options[\'deriveWalletAddress\'] = ADDRESS, the address can find in HOME => Developers tab.') + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if not response: + return None # fallback to default error handler + error = self.safe_dict(response, 'error') + if error is not None: + errorCode = self.safe_string(error, 'code') + feedback = self.id + ' ' + self.json(response) + self.throw_broadly_matched_exception(self.exceptions['broad'], body, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + raise ExchangeError(feedback) + return None + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = self.urls['api'][api] + '/' + path + if method == 'POST': + headers = { + 'Content-Type': 'application/json', + } + if api == 'private': + now = str(self.milliseconds()) + signature = self.sign_message(now, self.privateKey) + headers['X-LyraWallet'] = self.safe_string(self.options, 'deriveWalletAddress') + headers['X-LyraTimestamp'] = now + headers['X-LyraSignature'] = signature + body = self.json(params) + return {'url': url, 'method': method, 'body': body, 'headers': headers} diff --git a/ccxt/digifinex.py b/ccxt/digifinex.py new file mode 100644 index 0000000..5aebdcf --- /dev/null +++ b/ccxt/digifinex.py @@ -0,0 +1,4175 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.digifinex import ImplicitAPI +import hashlib +import json +from ccxt.base.types import Any, Balances, BorrowInterest, CrossBorrowRate, CrossBorrowRates, Currencies, Currency, DepositAddress, Int, LedgerEntry, LeverageTier, LeverageTiers, MarginModification, Market, Num, Order, OrderBook, OrderRequest, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, Trade, TradingFeeInterface, Transaction, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import AccountSuspended +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidAddress +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import NotSupported +from ccxt.base.errors import NetworkError +from ccxt.base.errors import DDoSProtection +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import InvalidNonce +from ccxt.base.errors import BadResponse +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class digifinex(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(digifinex, self).describe(), { + 'id': 'digifinex', + 'name': 'DigiFinex', + 'countries': ['SG'], + 'version': 'v3', + 'rateLimit': 900, # 300 for posts + 'has': { + 'CORS': None, + 'spot': True, + 'margin': True, + 'swap': True, + 'future': False, + 'option': False, + 'addMargin': True, + 'cancelOrder': True, + 'cancelOrders': True, + 'createMarketBuyOrderWithCost': True, + 'createMarketOrderWithCost': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createOrders': True, + 'createPostOnlyOrder': True, + 'createReduceOnlyOrder': True, + 'createStopLimitOrder': False, + 'createStopMarketOrder': False, + 'createStopOrder': False, + 'fetchBalance': True, + 'fetchBorrowInterest': True, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchCrossBorrowRate': True, + 'fetchCrossBorrowRates': True, + 'fetchCurrencies': True, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchDepositWithdrawFee': 'emulated', + 'fetchDepositWithdrawFees': True, + 'fetchFundingHistory': True, + 'fetchFundingInterval': True, + 'fetchFundingIntervals': False, + 'fetchFundingRate': True, + 'fetchFundingRateHistory': True, + 'fetchFundingRates': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchLedger': True, + 'fetchLeverage': False, + 'fetchLeverageTiers': True, + 'fetchMarginMode': False, + 'fetchMarketLeverageTiers': True, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchPosition': True, + 'fetchPositionMode': False, + 'fetchPositions': True, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchStatus': True, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': True, + 'fetchTradingFees': False, + 'fetchTransfers': True, + 'fetchWithdrawals': True, + 'reduceMargin': True, + 'setLeverage': True, + 'setMargin': False, + 'setMarginMode': True, + 'setPositionMode': False, + 'transfer': True, + 'withdraw': True, + }, + 'timeframes': { + '1m': '1', + '5m': '5', + '15m': '15', + '30m': '30', + '1h': '60', + '4h': '240', + '12h': '720', + '1d': '1D', + '1w': '1W', + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/51840849/87443315-01283a00-c5fe-11ea-8628-c2a0feaf07ac.jpg', + 'api': { + 'rest': 'https://openapi.digifinex.com', + }, + 'www': 'https://www.digifinex.com', + 'doc': [ + 'https://docs.digifinex.com', + ], + 'fees': 'https://digifinex.zendesk.com/hc/en-us/articles/360000328422-Fee-Structure-on-DigiFinex', + 'referral': 'https://www.digifinex.com/en-ww/from/DhOzBg?channelCode=ljaUPp', + }, + 'api': { + 'public': { + 'spot': { + 'get': [ + '{market}/symbols', + 'kline', + 'margin/currencies', + 'margin/symbols', + 'markets', + 'order_book', + 'ping', + 'spot/symbols', + 'time', + 'trades', + 'trades/symbols', + 'ticker', + 'currencies', + ], + }, + 'swap': { + 'get': [ + 'public/api_weight', + 'public/candles', + 'public/candles_history', + 'public/depth', + 'public/funding_rate', + 'public/funding_rate_history', + 'public/instrument', + 'public/instruments', + 'public/ticker', + 'public/tickers', + 'public/time', + 'public/trades', + ], + }, + }, + 'private': { + 'spot': { + 'get': [ + '{market}/financelog', + '{market}/mytrades', + '{market}/order', + '{market}/order/detail', + '{market}/order/current', + '{market}/order/history', + 'margin/assets', + 'margin/financelog', + 'margin/mytrades', + 'margin/order', + 'margin/order/current', + 'margin/order/history', + 'margin/positions', + 'otc/financelog', + 'spot/assets', + 'spot/financelog', + 'spot/mytrades', + 'spot/order', + 'spot/order/current', + 'spot/order/history', + 'deposit/address', + 'deposit/history', + 'withdraw/history', + ], + 'post': [ + '{market}/order/cancel', + '{market}/order/new', + '{market}/order/batch_new', + 'margin/order/cancel', + 'margin/order/new', + 'margin/position/close', + 'spot/order/cancel', + 'spot/order/new', + 'transfer', + 'withdraw/new', + 'withdraw/cancel', + ], + }, + 'swap': { + 'get': [ + 'account/balance', + 'account/positions', + 'account/finance_record', + 'account/trading_fee_rate', + 'account/transfer_record', + 'account/funding_fee', + 'trade/history_orders', + 'trade/history_trades', + 'trade/open_orders', + 'trade/order_info', + ], + 'post': [ + 'account/transfer', + 'account/leverage', + 'account/position_mode', + 'account/position_margin', + 'trade/batch_cancel_order', + 'trade/batch_order', + 'trade/cancel_order', + 'trade/order_place', + 'follow/sponsor_order', + 'follow/close_order', + 'follow/cancel_order', + 'follow/user_center_current', + 'follow/user_center_history', + 'follow/expert_current_open_order', + 'follow/add_algo', + 'follow/cancel_algo', + 'follow/account_available', + 'follow/plan_task', + 'follow/instrument_list', + ], + }, + }, + }, + 'features': { + 'default': { + 'sandbox': False, + 'createOrder': { + 'marginMode': True, + 'triggerPrice': False, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': False, + 'FOK': False, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'selfTradePrevention': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': False, + 'marketBuyRequiresPrice': False, + 'iceberg': False, + }, + 'createOrders': { + 'max': 10, + }, + 'fetchMyTrades': { + 'marginMode': True, + 'limit': 500, + 'daysBack': 100000, # todo + 'untilDays': 30, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': True, + 'trigger': False, + 'trailing': False, + 'marketType': True, + 'symbolRequired': True, + }, + 'fetchOpenOrders': { + 'marginMode': True, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': True, + 'limit': 100, + 'daysBack': 100000, # todo + 'untilDays': 30, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchClosedOrders': None, + 'fetchOHLCV': { + 'limit': 500, + }, + }, + 'spot': { + 'extends': 'default', + }, + 'forDerivatives': { + 'extends': 'default', + 'createOrders': { + 'max': 20, + 'marginMode': False, + }, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 100000, # todo + 'untilDays': 100000, # todo + }, + 'fetchOrder': { + 'marginMode': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 100, + }, + 'fetchOrders': { + 'marginMode': False, + 'daysBack': 100000, # todo + }, + 'fetchOHLCV': { + 'limit': 100, + }, + }, + 'swap': { + 'linear': { + 'extends': 'forDerivatives', + }, + 'inverse': { + 'extends': 'forDerivatives', + }, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'fees': { + 'trading': { + 'tierBased': True, + 'percentage': True, + 'maker': self.parse_number('0.002'), + 'taker': self.parse_number('0.002'), + }, + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'exact': { + '10001': [BadRequest, "Wrong request method, please check it's a GET ot POST request"], + '10002': [AuthenticationError, 'Invalid ApiKey'], + '10003': [AuthenticationError, "Sign doesn't match"], + '10004': [BadRequest, 'Illegal request parameters'], + '10005': [DDoSProtection, 'Request frequency exceeds the limit'], + '10006': [PermissionDenied, 'Unauthorized to execute self request'], + '10007': [PermissionDenied, 'IP address Unauthorized'], + '10008': [InvalidNonce, 'Timestamp for self request is invalid, timestamp must within 1 minute'], + '10009': [NetworkError, 'Unexist endpoint, please check endpoint URL'], + '10011': [AccountSuspended, 'ApiKey expired. Please go to client side to re-create an ApiKey'], + '20001': [PermissionDenied, 'Trade is not open for self trading pair'], + '20002': [PermissionDenied, 'Trade of self trading pair is suspended'], + '20003': [InvalidOrder, 'Invalid price or amount'], + '20007': [InvalidOrder, 'Price precision error'], + '20008': [InvalidOrder, 'Amount precision error'], + '20009': [InvalidOrder, 'Amount is less than the minimum requirement'], + '20010': [InvalidOrder, 'Cash Amount is less than the minimum requirement'], + '20011': [InsufficientFunds, 'Insufficient balance'], + '20012': [BadRequest, 'Invalid trade type, valid value: buy/sell)'], + '20013': [InvalidOrder, 'No order info found'], + '20014': [BadRequest, 'Invalid date, Valid format: 2018-07-25)'], + '20015': [BadRequest, 'Date exceeds the limit'], + '20018': [PermissionDenied, 'Your trading rights have been banned by the system'], + '20019': [BadSymbol, 'Wrong trading pair symbol. Correct format:"usdt_btc". Quote asset is in the front'], + '20020': [DDoSProtection, "You have violated the API operation trading rules and temporarily forbid trading. At present, we have certain restrictions on the user's transaction rate and withdrawal rate."], + '50000': [ExchangeError, 'Exception error'], + '20021': [BadRequest, 'Invalid currency'], + '20022': [BadRequest, 'The ending timestamp must be larger than the starting timestamp'], + '20023': [BadRequest, 'Invalid transfer type'], + '20024': [BadRequest, 'Invalid amount'], + '20025': [BadRequest, 'This currency is not transferable at the moment'], + '20026': [InsufficientFunds, 'Transfer amount exceed your balance'], + '20027': [PermissionDenied, 'Abnormal account status'], + '20028': [PermissionDenied, 'Blacklist for transfer'], + '20029': [PermissionDenied, 'Transfer amount exceed your daily limit'], + '20030': [BadRequest, 'You have no position on self trading pair'], + '20032': [PermissionDenied, 'Withdrawal limited'], + '20033': [BadRequest, 'Wrong Withdrawal ID'], + '20034': [PermissionDenied, 'Withdrawal service of self crypto has been closed'], + '20035': [PermissionDenied, 'Withdrawal limit'], + '20036': [ExchangeError, 'Withdrawal cancellation failed'], + '20037': [InvalidAddress, 'The withdrawal address, Tag or chain type is not included in the withdrawal management list'], + '20038': [InvalidAddress, 'The withdrawal address is not on the white list'], + '20039': [ExchangeError, "Can't be canceled in current status"], + '20040': [RateLimitExceeded, 'Withdraw too frequently; limitation: 3 times a minute, 100 times a day'], + '20041': [PermissionDenied, 'Beyond the daily withdrawal limit'], + '20042': [BadSymbol, 'Current trading pair does not support API trading'], + '400002': [BadRequest, 'Invalid Parameter'], + }, + 'broad': { + }, + }, + 'options': { + 'defaultType': 'spot', + 'types': ['spot', 'margin', 'otc'], + 'createMarketBuyOrderRequiresPrice': True, + 'accountsByType': { + 'spot': '1', + 'margin': '2', + 'OTC': '3', + }, + 'networks': { + 'ARBITRUM': 'Arbitrum', + 'AVALANCEC': 'AVAX-CCHAIN', + 'AVALANCEX': 'AVAX-XCHAIN', + 'BEP20': 'BEP20', + 'BSC': 'BEP20', + 'CARDANO': 'Cardano', + 'CELO': 'Celo', + 'CHILIZ': 'Chiliz', + 'COSMOS': 'COSMOS', + 'CRC20': 'Crypto.com', + 'CRONOS': 'Crypto.com', + 'DOGECOIN': 'DogeChain', + 'ERC20': 'ERC20', + 'ETH': 'ERC20', + 'ETHW': 'ETHW', + 'IOTA': 'MIOTA', + 'KLAYTN': 'KLAY', + 'MATIC': 'Polygon', + 'METIS': 'MetisDAO', + 'MOONBEAM': 'GLMR', + 'MOONRIVER': 'Moonriver', + 'OPTIMISM': 'OPETH', + 'POLYGON': 'Polygon', + 'RIPPLE': 'XRP', + 'SOLANA': 'SOL', # SOL & SPL + 'STELLAR': 'Stella', # XLM + 'TERRACLASSIC': 'TerraClassic', + 'TERRA': 'Terra', + 'TON': 'Ton', + 'TRC20': 'TRC20', + 'TRON': 'TRC20', + 'TRX': 'TRC20', + 'VECHAIN': 'Vechain', # VET + }, + 'networksById': { + 'TRC20': 'TRC20', + 'TRX': 'TRC20', + 'BEP20': 'BEP20', + 'BSC': 'BEP20', + 'ERC20': 'ERC20', + 'ETH': 'ERC20', + 'Polygon': 'POLYGON', + 'Crypto.com': 'CRONOS', + }, + }, + 'commonCurrencies': { + 'BHT': 'Black House Test', + 'EPS': 'Epanus', + 'FREE': 'FreeRossDAO', + 'MBN': 'Mobilian Coin', + 'TEL': 'TEL666', + }, + }) + + def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = self.publicSpotGetCurrencies(params) + # + # { + # "data":[ + # { + # "deposit_status":1, + # "min_deposit_amount":10, + # "withdraw_fee_rate":0, + # "min_withdraw_amount":10, + # "min_withdraw_fee":5, + # "currency":"USDT", + # "withdraw_fee_currency":"USDT", + # "withdraw_status":0, + # "chain":"OMNI" + # }, + # { + # "deposit_status":1, + # "min_deposit_amount":10, + # "withdraw_fee_rate":0, + # "min_withdraw_amount":10, + # "min_withdraw_fee":3, + # "currency":"USDT", + # "withdraw_fee_currency":"USDT", + # "withdraw_status":1, + # "chain":"ERC20" + # }, + # { + # "deposit_status":0, + # "min_deposit_amount":0, + # "withdraw_fee_rate":0, + # "min_withdraw_amount":0, + # "min_withdraw_fee":0, + # "currency":"DGF13", + # "withdraw_fee_currency":"DGF13", + # "withdraw_status":0, + # "chain":"" + # }, + # ], + # "code":200 + # } + # + data = self.safe_list(response, 'data', []) + groupedById = self.group_by(data, 'currency') + keys = list(groupedById.keys()) + result: dict = {} + for i in range(0, len(keys)): + id = keys[i] + networkEntries = groupedById[id] + code = self.safe_currency_code(id) + networks = {} + for j in range(0, len(networkEntries)): + networkEntry = networkEntries[j] + networkId = self.safe_string_2(networkEntry, 'chain', 'currency') + networkCode = self.network_id_to_code(networkId) + networks[networkCode] = { + 'id': networkId, + 'network': networkCode, + 'active': None, + 'deposit': self.safe_integer(networkEntry, 'deposit_status') == 1, + 'withdraw': self.safe_integer(networkEntry, 'withdraw_status') == 1, + 'fee': self.safe_number(networkEntry, 'min_withdraw_fee'), + 'precision': None, + 'limits': { + 'withdraw': { + 'min': self.safe_number(networkEntry, 'min_withdraw_amount'), + 'max': None, + }, + 'deposit': { + 'min': self.safe_number(networkEntry, 'min_deposit_amount'), + 'max': None, + }, + }, + 'info': networkEntry, + } + result[code] = self.safe_currency_structure({ + 'id': id, + 'code': code, + 'info': networkEntries, + 'networks': networks, + }) + return result + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for digifinex + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + options = self.safe_value(self.options, 'fetchMarkets', {}) + method = self.safe_string(options, 'method', 'fetch_markets_v2') + if method == 'fetch_markets_v2': + return self.fetch_markets_v2(params) + return self.fetch_markets_v1(params) + + def fetch_markets_v2(self, params={}): + defaultType = self.safe_string(self.options, 'defaultType') + marginMode, query = self.handle_margin_mode_and_params('fetchMarketsV2', params) + promisesRaw = [] + if marginMode is not None: + promisesRaw.append(self.publicSpotGetMarginSymbols(query)) + else: + promisesRaw.append(self.publicSpotGetTradesSymbols(query)) + promisesRaw.append(self.publicSwapGetPublicInstruments(params)) + promises = promisesRaw + spotMarkets = promises[0] + swapMarkets = promises[1] + # + # spot and margin + # + # { + # "symbol_list":[ + # { + # "order_types":["LIMIT","MARKET"], + # "quote_asset":"USDT", + # "minimum_value":2, + # "amount_precision":4, + # "status":"TRADING", + # "minimum_amount":0.0001, + # "symbol":"BTC_USDT", + # "is_allow":1, + # "zone":"MAIN", + # "base_asset":"BTC", + # "price_precision":2 + # } + # ], + # "code":0 + # } + # + # swap + # + # { + # "code": 0, + # "data": [ + # { + # "instrument_id": "BTCUSDTPERP", + # "type": "REAL", + # "contract_type": "PERPETUAL", + # "base_currency": "BTC", + # "quote_currency": "USDT", + # "clear_currency": "USDT", + # "contract_value": "0.001", + # "contract_value_currency": "BTC", + # "is_inverse": False, + # "is_trading": True, + # "status": "ONLINE", + # "price_precision": 4, + # "tick_size": "0.0001", + # "min_order_amount": 1, + # "open_max_limits": [ + # { + # "leverage": "50", + # "max_limit": "1000000" + # } + # ] + # }, + # ] + # } + # + spotData = self.safe_value(spotMarkets, 'symbol_list', []) + swapData = self.safe_value(swapMarkets, 'data', []) + response = self.array_concat(spotData, swapData) + result = [] + for i in range(0, len(response)): + market = response[i] + id = self.safe_string_2(market, 'symbol', 'instrument_id') + baseId = self.safe_string_2(market, 'base_asset', 'base_currency') + quoteId = self.safe_string_2(market, 'quote_asset', 'quote_currency') + settleId = self.safe_string(market, 'clear_currency') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + settle = self.safe_currency_code(settleId) + # + # The status is documented in the exchange API docs: + # TRADING, HALT(delisted), BREAK(trading paused) + # https://docs.digifinex.vip/en-ww/v3/#/public/spot/symbols + # However, all spot markets actually have status == 'HALT' + # despite that they appear to be active on the exchange website. + # Apparently, we can't trust self status. + # status = self.safe_string(market, 'status') + # active = (status == 'TRADING') + # + isAllowed = self.safe_integer(market, 'is_allow', 1) + type = 'margin' if (defaultType == 'margin') else 'spot' + spot = settle is None + swap = not spot + margin = True if (marginMode is not None) else None + symbol = base + '/' + quote + isInverse = None + isLinear = None + if swap: + type = 'swap' + symbol = base + '/' + quote + ':' + settle + isInverse = self.safe_value(market, 'is_inverse') + isLinear = True if (not isInverse) else False + isTrading = self.safe_value(market, 'isTrading') + if isTrading: + isAllowed = 1 + result.append({ + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': type, + 'spot': spot, + 'margin': margin, + 'swap': swap, + 'future': False, + 'option': False, + 'active': True if isAllowed else False, + 'contract': swap, + 'linear': isLinear, + 'inverse': isInverse, + 'contractSize': self.safe_number(market, 'contract_value'), + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(self.parse_precision(self.safe_string(market, 'amount_precision'))), + 'price': self.parse_number(self.parse_precision(self.safe_string(market, 'price_precision'))), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number_2(market, 'minimum_amount', 'min_order_amount'), + 'max': None, + }, + 'price': { + 'min': self.safe_number(market, 'tick_size'), + 'max': None, + }, + 'cost': { + 'min': self.safe_number(market, 'minimum_value'), + 'max': None, + }, + }, + 'created': None, + 'info': market, + }) + return result + + def fetch_markets_v1(self, params={}): + response = self.publicSpotGetMarkets(params) + # + # { + # "data": [ + # { + # "volume_precision":4, + # "price_precision":2, + # "market":"btc_usdt", + # "min_amount":2, + # "min_volume":0.0001 + # }, + # ], + # "date":1564507456, + # "code":0 + # } + # + markets = self.safe_value(response, 'data', []) + result = [] + for i in range(0, len(markets)): + market = markets[i] + id = self.safe_string(market, 'market') + baseId, quoteId = id.split('_') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + result.append({ + 'id': id, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': None, + 'swap': False, + 'future': False, + 'option': False, + 'active': None, + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'price': self.parse_number(self.parse_precision(self.safe_string(market, 'price_precision'))), + 'amount': self.parse_number(self.parse_precision(self.safe_string(market, 'volume_precision'))), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(market, 'min_volume'), + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': self.safe_number(market, 'min_amount'), + 'max': None, + }, + }, + 'info': market, + }) + return result + + def parse_balance(self, response) -> Balances: + # + # spot and margin + # + # { + # "currency": "BTC", + # "free": 4723846.89208129, + # "total": 0 + # } + # + # swap + # + # { + # "equity": "0", + # "currency": "BTC", + # "margin": "0", + # "frozen_margin": "0", + # "frozen_money": "0", + # "margin_ratio": "0", + # "realized_pnl": "0", + # "avail_balance": "0", + # "unrealized_pnl": "0", + # "time_stamp": 1661487402396 + # } + # + result: dict = {'info': response} + for i in range(0, len(response)): + balance = response[i] + currencyId = self.safe_string(balance, 'currency') + code = self.safe_currency_code(currencyId) + account = self.account() + free = self.safe_string_2(balance, 'free', 'avail_balance') + total = self.safe_string_2(balance, 'total', 'equity') + account['free'] = free + account['used'] = Precise.string_sub(total, free) + account['total'] = total + result[code] = account + return self.safe_balance(result) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://docs.digifinex.com/en-ww/spot/v3/rest.html#spot-account-assets + https://docs.digifinex.com/en-ww/spot/v3/rest.html#margin-assets + https://docs.digifinex.com/en-ww/swap/v2/rest.html#accountbalance + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + marketType = None + marketType, params = self.handle_market_type_and_params('fetchBalance', None, params) + marginMode, query = self.handle_margin_mode_and_params('fetchBalance', params) + response = None + if marginMode is not None or marketType == 'margin': + marketType = 'margin' + response = self.privateSpotGetMarginAssets(query) + elif marketType == 'spot': + response = self.privateSpotGetSpotAssets(query) + elif marketType == 'swap': + response = self.privateSwapGetAccountBalance(query) + else: + raise NotSupported(self.id + ' fetchBalance() not support self market type') + # + # spot and margin + # + # { + # "code": 0, + # "list": [ + # { + # "currency": "BTC", + # "free": 4723846.89208129, + # "total": 0 + # }, + # ... + # ] + # } + # + # swap + # + # { + # "code": 0, + # "data": [ + # { + # "equity": "0", + # "currency": "BTC", + # "margin": "0", + # "frozen_margin": "0", + # "frozen_money": "0", + # "margin_ratio": "0", + # "realized_pnl": "0", + # "avail_balance": "0", + # "unrealized_pnl": "0", + # "time_stamp": 1661487402396 + # }, + # ... + # ] + # } + # + balanceRequest = 'data' if (marketType == 'swap') else 'list' + balances = self.safe_value(response, balanceRequest, []) + return self.parse_balance(balances) + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://docs.digifinex.com/en-ww/spot/v3/rest.html#get-orderbook + https://docs.digifinex.com/en-ww/swap/v2/rest.html#orderbook + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + marketType, query = self.handle_market_type_and_params('fetchOrderBook', market, params) + request: dict = {} + if limit is not None: + request['limit'] = limit + response = None + if marketType == 'swap': + request['instrument_id'] = market['id'] + response = self.publicSwapGetPublicDepth(self.extend(request, query)) + else: + request['symbol'] = market['id'] + response = self.publicSpotGetOrderBook(self.extend(request, query)) + # + # spot + # + # { + # "bids": [ + # [9605.77,0.0016], + # [9605.46,0.0003], + # [9602.04,0.0127], + # ], + # "asks": [ + # [9627.22,0.025803], + # [9627.12,0.168543], + # [9626.52,0.0011529], + # ], + # "date":1564509499, + # "code":0 + # } + # + # swap + # + # { + # "code": 0, + # "data": { + # "instrument_id": "BTCUSDTPERP", + # "timestamp": 1667975290425, + # "asks": [ + # ["18384.7",3492], + # ["18402.7",5000], + # ["18406.7",5000], + # ], + # "bids": [ + # ["18366.2",4395], + # ["18364.3",3070], + # ["18359.4",5000], + # ] + # } + # } + # + timestamp = None + orderBook = None + if marketType == 'swap': + orderBook = self.safe_value(response, 'data', {}) + timestamp = self.safe_integer(orderBook, 'timestamp') + else: + orderBook = response + timestamp = self.safe_timestamp(response, 'date') + return self.parse_order_book(orderBook, market['symbol'], timestamp) + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://docs.digifinex.com/en-ww/spot/v3/rest.html#ticker-price + https://docs.digifinex.com/en-ww/swap/v2/rest.html#tickers + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + symbols = self.market_symbols(symbols) + first = self.safe_string(symbols, 0) + market = None + if first is not None: + market = self.market(first) + type = None + type, params = self.handle_market_type_and_params('fetchTickers', market, params) + request: dict = {} + response = None + if type == 'swap': + response = self.publicSwapGetPublicTickers(self.extend(request, params)) + else: + response = self.publicSpotGetTicker(self.extend(request, params)) + # + # spot + # + # { + # "ticker": [{ + # "vol": 40717.4461, + # "change": -1.91, + # "base_vol": 392447999.65374, + # "sell": 9592.23, + # "last": 9592.22, + # "symbol": "btc_usdt", + # "low": 9476.24, + # "buy": 9592.03, + # "high": 9793.87 + # }], + # "date": 1589874294, + # "code": 0 + # } + # + # swap + # + # { + # "code": 0, + # "data": [ + # { + # "instrument_id": "SUSHIUSDTPERP", + # "index_price": "1.1297", + # "mark_price": "1.1289", + # "max_buy_price": "1.1856", + # "min_sell_price": "1.0726", + # "best_bid": "1.1278", + # "best_bid_size": "500", + # "best_ask": "1.1302", + # "best_ask_size": "471", + # "high_24h": "1.2064", + # "open_24h": "1.1938", + # "low_24h": "1.1239", + # "last": "1.1302", + # "last_qty": "29", + # "volume_24h": "4946163", + # "price_change_percent": "-0.053275255486681085", + # "open_interest": "-", + # "timestamp": 1663222782100 + # }, + # ... + # ] + # } + # + result: dict = {} + tickers = self.safe_value_2(response, 'ticker', 'data', []) + date = self.safe_integer(response, 'date') + for i in range(0, len(tickers)): + rawTicker = self.extend({ + 'date': date, + }, tickers[i]) + ticker = self.parse_ticker(rawTicker) + symbol = ticker['symbol'] + result[symbol] = ticker + return self.filter_by_array_tickers(result, 'symbol', symbols) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://docs.digifinex.com/en-ww/spot/v3/rest.html#ticker-price + https://docs.digifinex.com/en-ww/swap/v2/rest.html#ticker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = {} + response = None + if market['swap']: + request['instrument_id'] = market['id'] + response = self.publicSwapGetPublicTicker(self.extend(request, params)) + else: + request['symbol'] = market['id'] + response = self.publicSpotGetTicker(self.extend(request, params)) + # + # spot + # + # { + # "ticker": [{ + # "vol": 40717.4461, + # "change": -1.91, + # "base_vol": 392447999.65374, + # "sell": 9592.23, + # "last": 9592.22, + # "symbol": "btc_usdt", + # "low": 9476.24, + # "buy": 9592.03, + # "high": 9793.87 + # }], + # "date": 1589874294, + # "code": 0 + # } + # + # swap + # + # { + # "code": 0, + # "data": { + # "instrument_id": "BTCUSDTPERP", + # "index_price": "20141.9967", + # "mark_price": "20139.3404", + # "max_buy_price": "21146.4838", + # "min_sell_price": "19132.2725", + # "best_bid": "20140.0998", + # "best_bid_size": "3116", + # "best_ask": "20140.0999", + # "best_ask_size": "9004", + # "high_24h": "20410.6496", + # "open_24h": "20308.6998", + # "low_24h": "19600", + # "last": "20140.0999", + # "last_qty": "2", + # "volume_24h": "49382816", + # "price_change_percent": "-0.008301855936636448", + # "open_interest": "-", + # "timestamp": 1663221614998 + # } + # } + # + date = self.safe_integer(response, 'date') + tickers = self.safe_value(response, 'ticker', []) + data = self.safe_value(response, 'data', {}) + firstTicker = self.safe_value(tickers, 0, {}) + result = None + if market['swap']: + result = data + else: + result = self.extend({'date': date}, firstTicker) + return self.parse_ticker(result, market) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # spot: fetchTicker, fetchTickers + # + # { + # "last":0.021957, + # "symbol": "btc_usdt", + # "base_vol":2249.3521732227, + # "change":-0.6, + # "vol":102443.5111, + # "sell":0.021978, + # "low":0.021791, + # "buy":0.021946, + # "high":0.022266, + # "date"1564518452, # injected from fetchTicker/fetchTickers + # } + # + # swap: fetchTicker, fetchTickers + # + # { + # "instrument_id": "BTCUSDTPERP", + # "index_price": "20141.9967", + # "mark_price": "20139.3404", + # "max_buy_price": "21146.4838", + # "min_sell_price": "19132.2725", + # "best_bid": "20140.0998", + # "best_bid_size": "3116", + # "best_ask": "20140.0999", + # "best_ask_size": "9004", + # "high_24h": "20410.6496", + # "open_24h": "20308.6998", + # "low_24h": "19600", + # "last": "20140.0999", + # "last_qty": "2", + # "volume_24h": "49382816", + # "price_change_percent": "-0.008301855936636448", + # "open_interest": "-", + # "timestamp": 1663221614998 + # } + # + indexPrice = self.safe_number(ticker, 'index_price') + marketType = 'contract' if (indexPrice is not None) else 'spot' + marketId = self.safe_string_upper_2(ticker, 'symbol', 'instrument_id') + symbol = self.safe_symbol(marketId, market, None, marketType) + market = self.safe_market(marketId, market, None, marketType) + timestamp = self.safe_timestamp(ticker, 'date') + if market['swap']: + timestamp = self.safe_integer(ticker, 'timestamp') + last = self.safe_string(ticker, 'last') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string_2(ticker, 'high', 'high_24h'), + 'low': self.safe_string_2(ticker, 'low', 'low_24h'), + 'bid': self.safe_string_2(ticker, 'buy', 'best_bid'), + 'bidVolume': self.safe_string(ticker, 'best_bid_size'), + 'ask': self.safe_string_2(ticker, 'sell', 'best_ask'), + 'askVolume': self.safe_string(ticker, 'best_ask_size'), + 'vwap': None, + 'open': self.safe_string(ticker, 'open_24h'), + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': self.safe_string_2(ticker, 'change', 'price_change_percent'), + 'average': None, + 'baseVolume': self.safe_string_2(ticker, 'vol', 'volume_24h'), + 'quoteVolume': self.safe_string(ticker, 'base_vol'), + 'markPrice': self.safe_string(ticker, 'mark_price'), + 'indexPrice': indexPrice, + 'info': ticker, + }, market) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # spot: fetchTrades + # + # { + # "date":1564520003, + # "id":1596149203, + # "amount":0.7073, + # "type":"buy", + # "price":0.02193, + # } + # + # swap: fetchTrades + # + # { + # "instrument_id": "BTCUSDTPERP", + # "trade_id": "1595190773677035521", + # "direction": "4", + # "volume": "4", + # "price": "16188.3", + # "trade_time": 1669158092314 + # } + # + # spot: fetchMyTrades + # + # { + # "symbol": "BTC_USDT", + # "order_id": "6707cbdcda0edfaa7f4ab509e4cbf966", + # "id": 28457, + # "price": 0.1, + # "amount": 0, + # "fee": 0.096, + # "fee_currency": "USDT", + # "timestamp": 1499865549, + # "side": "buy", # or "side": "sell_market" + # "is_maker": True + # } + # + # swap: fetchMyTrades + # + # { + # "trade_id": "1590136768424841218", + # "instrument_id": "BTCUSDTPERP", + # "order_id": "1590136768156405760", + # "type": 1, + # "order_type": 8, + # "price": "18514.5", + # "size": "1", + # "fee": "0.00925725", + # "close_profit": "0", + # "leverage": "20", + # "trade_type": 0, + # "match_role": 1, + # "trade_time": 1667953123562 + # } + # + id = self.safe_string_2(trade, 'id', 'trade_id') + orderId = self.safe_string(trade, 'order_id') + priceString = self.safe_string(trade, 'price') + amountString = self.safe_string_n(trade, ['amount', 'volume', 'size']) + marketId = self.safe_string_upper_2(trade, 'symbol', 'instrument_id') + symbol = self.safe_symbol(marketId, market) + if market is None: + market = self.safe_market(marketId) + timestamp = self.safe_timestamp_2(trade, 'date', 'timestamp') + side = self.safe_string_2(trade, 'type', 'side') + type = None + takerOrMaker = None + if market['type'] == 'swap': + timestamp = self.safe_integer(trade, 'trade_time') + orderType = self.safe_string(trade, 'order_type') + tradeRole = self.safe_string(trade, 'match_role') + direction = self.safe_string(trade, 'direction') + if orderType is not None: + type = 'limit' if (orderType == '0') else None + if tradeRole == '1': + takerOrMaker = 'taker' + elif tradeRole == '2': + takerOrMaker = 'maker' + else: + takerOrMaker = None + if (side == '1') or (direction == '1'): + # side = 'open long' + side = 'buy' + elif (side == '2') or (direction == '2'): + # side = 'open short' + side = 'sell' + elif (side == '3') or (direction == '3'): + # side = 'close long' + side = 'sell' + elif (side == '4') or (direction == '4'): + # side = 'close short' + side = 'buy' + else: + parts = side.split('_') + side = self.safe_string(parts, 0) + type = self.safe_string(parts, 1) + if type is None: + type = 'limit' + isMaker = self.safe_value(trade, 'is_maker') + takerOrMaker = 'maker' if isMaker else 'taker' + fee = None + feeCostString = self.safe_string(trade, 'fee') + if feeCostString is not None: + feeCurrencyId = self.safe_string(trade, 'fee_currency') + feeCurrencyCode = None + if feeCurrencyId is not None: + feeCurrencyCode = self.safe_currency_code(feeCurrencyId) + fee = { + 'cost': feeCostString, + 'currency': feeCurrencyCode, + } + return self.safe_trade({ + 'id': id, + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'type': type, + 'order': orderId, + 'side': side, + 'price': priceString, + 'amount': amountString, + 'cost': None, + 'takerOrMaker': takerOrMaker, + 'fee': fee, + }, market) + + def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = self.publicSpotGetTime(params) + # + # { + # "server_time": 1589873762, + # "code": 0 + # } + # + return self.safe_timestamp(response, 'server_time') + + def fetch_status(self, params={}): + """ + the latest known information on the availability of the exchange API + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `status structure ` + """ + response = self.publicSpotGetPing(params) + # + # { + # "msg": "pong", + # "code": 0 + # } + # + code = self.safe_integer(response, 'code') + status = 'ok' if (code == 0) else 'maintenance' + return { + 'status': status, + 'updated': None, + 'eta': None, + 'url': None, + 'info': response, + } + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://docs.digifinex.com/en-ww/spot/v3/rest.html#get-recent-trades + https://docs.digifinex.com/en-ww/swap/v2/rest.html#recenttrades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = {} + if limit is not None: + request['limit'] = min(limit, 100) if market['swap'] else limit + response = None + if market['swap']: + request['instrument_id'] = market['id'] + response = self.publicSwapGetPublicTrades(self.extend(request, params)) + else: + request['symbol'] = market['id'] + response = self.publicSpotGetTrades(self.extend(request, params)) + # + # spot + # + # { + # "data":[ + # { + # "date":1564520003, + # "id":1596149203, + # "amount":0.7073, + # "type":"buy", + # "price":0.02193, + # }, + # { + # "date":1564520002, + # "id":1596149165, + # "amount":0.3232, + # "type":"sell", + # "price":0.021927, + # }, + # ], + # "code": 0, + # "date": 1564520003, + # } + # + # swap + # + # { + # "code": 0, + # "data": [ + # { + # "instrument_id": "BTCUSDTPERP", + # "trade_id": "1595190773677035521", + # "direction": "4", + # "volume": "4", + # "price": "16188.3", + # "trade_time": 1669158092314 + # }, + # ... + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_trades(data, market, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # [ + # 1556712900, + # 2205.899, + # 0.029967, + # 0.02997, + # 0.029871, + # 0.029927 + # ] + # + if market['swap']: + return [ + self.safe_integer(ohlcv, 0), + self.safe_number(ohlcv, 1), # open + self.safe_number(ohlcv, 2), # high + self.safe_number(ohlcv, 3), # low + self.safe_number(ohlcv, 4), # close + self.safe_number(ohlcv, 5), # volume + ] + else: + return [ + self.safe_timestamp(ohlcv, 0), + self.safe_number(ohlcv, 5), # open + self.safe_number(ohlcv, 3), # high + self.safe_number(ohlcv, 4), # low + self.safe_number(ohlcv, 2), # close + self.safe_number(ohlcv, 1), # volume + ] + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://docs.digifinex.com/en-ww/spot/v3/rest.html#get-candles-data + https://docs.digifinex.com/en-ww/swap/v2/rest.html#recentcandle + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest candle to fetch + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + market = self.market(symbol) + request: dict = {} + response = None + if market['swap']: + request['instrument_id'] = market['id'] + request['granularity'] = timeframe + if limit is not None: + request['limit'] = min(limit, 100) + response = self.publicSwapGetPublicCandles(self.extend(request, params)) + else: + until = self.safe_integer(params, 'until') + request['symbol'] = market['id'] + request['period'] = self.safe_string(self.timeframes, timeframe, timeframe) + startTime = since + duration = self.parse_timeframe(timeframe) + if startTime is None: + if (limit is not None) or (until is not None): + endTime = until if (until is not None) else self.milliseconds() + startLimit = limit if (limit is not None) else 200 + startTime = endTime - (startLimit * duration * 1000) + if startTime is not None: + startTime = self.parse_to_int(startTime / 1000) + request['start_time'] = startTime + if (limit is not None) or (until is not None): + if until is not None: + endByUntil = self.parse_to_int(until / 1000) + if limit is not None: + endByLimit = self.sum(startTime, limit * duration) + request['end_time'] = min(endByLimit, endByUntil) + else: + request['end_time'] = endByUntil + else: + request['end_time'] = self.sum(startTime, limit * duration) + params = self.omit(params, 'until') + response = self.publicSpotGetKline(self.extend(request, params)) + # + # spot + # + # { + # "code":0, + # "data":[ + # [1556712900,2205.899,0.029967,0.02997,0.029871,0.029927], + # [1556713800,1912.9174,0.029992,0.030014,0.029955,0.02996], + # [1556714700,1556.4795,0.029974,0.030019,0.029969,0.02999], + # ] + # } + # + # swap + # + # { + # "code": 0, + # "data": { + # "instrument_id": "BTCUSDTPERP", + # "granularity": "1m", + # "candles": [ + # [1588089660000,"6900","6900","6900","6900","0","0"], + # [1588089720000,"6900","6900","6900","6900","0","0"], + # [1588089780000,"6900","6900","6900","6900","0","0"], + # ] + # } + # } + # + candles = None + if market['swap']: + data = self.safe_value(response, 'data', {}) + candles = self.safe_value(data, 'candles', []) + else: + candles = self.safe_value(response, 'data', []) + return self.parse_ohlcvs(candles, market, timeframe, since, limit) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://docs.digifinex.com/en-ww/spot/v3/rest.html#create-new-order + https://docs.digifinex.com/en-ww/swap/v2/rest.html#orderplace + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency, spot market orders use the quote currency, swap requires the number of contracts + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.timeInForce]: "GTC", "IOC", "FOK", or "PO" + :param bool [params.postOnly]: True or False + :param bool [params.reduceOnly]: True or False + :param str [params.marginMode]: 'cross' or 'isolated', for spot margin trading + :param float [params.cost]: *spot market buy only* the quote quantity that can be used alternative for the amount + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + marginResult = self.handle_margin_mode_and_params('createOrder', params) + marginMode = marginResult[0] + request = self.create_order_request(symbol, type, side, amount, price, params) + response = None + if market['swap']: + response = self.privateSwapPostTradeOrderPlace(request) + else: + if marginMode is not None: + response = self.privateSpotPostMarginOrderNew(request) + else: + response = self.privateSpotPostSpotOrderNew(request) + # + # spot and margin + # + # { + # "code": 0, + # "order_id": "198361cecdc65f9c8c9bb2fa68faec40" + # } + # + # swap + # + # { + # "code": 0, + # "data": "1590873693003714560" + # } + # + order = self.parse_order(response, market) + order['symbol'] = market['symbol'] + order['type'] = type + order['side'] = side + order['amount'] = amount + order['price'] = price + return order + + def create_orders(self, orders: List[OrderRequest], params={}): + """ + create a list of trade orders(all orders should be of the same symbol) + + https://docs.digifinex.com/en-ww/spot/v3/rest.html#create-multiple-order + https://docs.digifinex.com/en-ww/swap/v2/rest.html#batchorder + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + ordersRequests = [] + symbol = None + marginMode = None + for i in range(0, len(orders)): + rawOrder = orders[i] + marketId = self.safe_string(rawOrder, 'symbol') + if symbol is None: + symbol = marketId + else: + if symbol != marketId: + raise BadRequest(self.id + ' createOrders() requires all orders to have the same symbol') + type = self.safe_string(rawOrder, 'type') + side = self.safe_string(rawOrder, 'side') + amount = self.safe_value(rawOrder, 'amount') + price = self.safe_value(rawOrder, 'price') + orderParams = self.safe_value(rawOrder, 'params', {}) + marginResult = self.handle_margin_mode_and_params('createOrders', orderParams) + currentMarginMode = marginResult[0] + if currentMarginMode is not None: + if marginMode is None: + marginMode = currentMarginMode + else: + if marginMode != currentMarginMode: + raise BadRequest(self.id + ' createOrders() requires all orders to have the same margin mode(isolated or cross)') + orderRequest = self.create_order_request(marketId, type, side, amount, price, orderParams) + ordersRequests.append(orderRequest) + market = self.market(symbol) + request: dict = {} + response = None + if market['swap']: + response = self.privateSwapPostTradeBatchOrder(ordersRequests) + else: + request['market'] = 'margin' if (marginMode is not None) else 'spot' + request['symbol'] = market['id'] + request['list'] = self.json(ordersRequests) + response = self.privateSpotPostMarketOrderBatchNew(request) + # + # spot + # + # { + # "code": 0, + # "order_ids": [ + # "064290fbe2d26e7b28d7e6c0a5cf70a5", + # "24c8f9b73d81e4d9d8d7e3280281c258" + # ] + # } + # + # swap + # + # { + # "code": 0, + # "data": [ + # "1720297963537829888", + # "1720297963537829889" + # ] + # } + # + data = [] + if market['swap']: + data = self.safe_value(response, 'data', []) + else: + data = self.safe_value(response, 'order_ids', []) + result = [] + for i in range(0, len(orders)): + rawOrder = orders[i] + individualOrder: dict = {} + individualOrder['order_id'] = data[i] + individualOrder['instrument_id'] = market['id'] + individualOrder['amount'] = self.safe_number(rawOrder, 'amount') + individualOrder['price'] = self.safe_number(rawOrder, 'price') + result.append(individualOrder) + return self.parse_orders(result, market) + + def create_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + @ignore + helper function to build request + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency, spot market orders use the quote currency, swap requires the number of contracts + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: request to be sent to the exchange + """ + market = self.market(symbol) + marketType = None + marginMode = None + marketType, params = self.handle_market_type_and_params('createOrderRequest', market, params) + marginMode, params = self.handle_margin_mode_and_params('createOrderRequest', params) + if marginMode is not None: + marketType = 'margin' + request: dict = {} + swap = (marketType == 'swap') + isMarketOrder = (type == 'market') + isLimitOrder = (type == 'limit') + marketIdRequest = 'instrument_id' if swap else 'symbol' + request[marketIdRequest] = market['id'] + postOnly = self.is_post_only(isMarketOrder, False, params) + postOnlyParsed = None + if swap: + reduceOnly = self.safe_bool(params, 'reduceOnly', False) + timeInForce = self.safe_string(params, 'timeInForce') + orderType = None + if side == 'buy': + requestType = 4 if (reduceOnly) else 1 + request['type'] = requestType + else: + requestType = 3 if (reduceOnly) else 2 + request['type'] = requestType + if isLimitOrder: + orderType = 0 + if timeInForce == 'FOK': + orderType = 15 if isMarketOrder else 9 + elif timeInForce == 'IOC': + orderType = 13 if isMarketOrder else 4 + elif (timeInForce == 'GTC') or (isMarketOrder): + orderType = 14 + elif timeInForce == 'PO': + postOnly = True + if price is not None: + request['price'] = self.price_to_precision(symbol, price) + request['order_type'] = orderType + request['size'] = amount # swap orders require the amount to be the number of contracts + params = self.omit(params, ['reduceOnly', 'timeInForce']) + else: + postOnlyParsed = 1 if (postOnly is True) else 2 + request['market'] = marketType + suffix = '' + if type == 'market': + suffix = '_market' + else: + request['price'] = self.price_to_precision(symbol, price) + request['type'] = side + suffix + # limit orders require the amount in the base currency, market orders require the amount in the quote currency + quantity = None + createMarketBuyOrderRequiresPrice = True + createMarketBuyOrderRequiresPrice, params = self.handle_option_and_params(params, 'createOrderRequest', 'createMarketBuyOrderRequiresPrice', True) + if isMarketOrder and (side == 'buy'): + cost = self.safe_number(params, 'cost') + params = self.omit(params, 'cost') + if cost is not None: + quantity = self.cost_to_precision(symbol, cost) + elif createMarketBuyOrderRequiresPrice: + if price is None: + raise InvalidOrder(self.id + ' createOrder() requires a price argument for market buy orders on spot markets to calculate the total amount to spend(amount * price), alternatively set the createMarketBuyOrderRequiresPrice option or param to False and pass the cost to spend in the amount argument') + else: + amountString = self.number_to_string(amount) + priceString = self.number_to_string(price) + costRequest = self.parse_number(Precise.string_mul(amountString, priceString)) + quantity = self.cost_to_precision(symbol, costRequest) + else: + quantity = self.cost_to_precision(symbol, amount) + else: + quantity = self.amount_to_precision(symbol, amount) + request['amount'] = quantity + if postOnly: + if postOnlyParsed: + request['post_only'] = postOnlyParsed + else: + request['post_only'] = postOnly + params = self.omit(params, ['postOnly']) + return self.extend(request, params) + + def create_market_buy_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market buy order by providing the symbol and cost + + https://docs.digifinex.com/en-ww/spot/v3/rest.html#create-new-order + + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' createMarketBuyOrderWithCost() supports spot orders only') + params['createMarketBuyOrderRequiresPrice'] = False + return self.create_order(symbol, 'market', 'buy', cost, None, params) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://docs.digifinex.com/en-ww/spot/v3/rest.html#cancel-order + https://docs.digifinex.com/en-ww/swap/v2/rest.html#cancelorder + + :param str id: order id + :param str symbol: not used by digifinex cancelOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + id = str(id) + marketType = None + marketType, params = self.handle_market_type_and_params('cancelOrder', market, params) + request: dict = { + 'order_id': id, + } + if marketType == 'swap': + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + request['instrument_id'] = market['id'] + else: + request['market'] = marketType + marginMode, query = self.handle_margin_mode_and_params('cancelOrder', params) + response = None + if marginMode is not None or marketType == 'margin': + marketType = 'margin' + response = self.privateSpotPostMarginOrderCancel(self.extend(request, query)) + elif marketType == 'spot': + response = self.privateSpotPostSpotOrderCancel(self.extend(request, query)) + elif marketType == 'swap': + response = self.privateSwapPostTradeCancelOrder(self.extend(request, query)) + else: + raise NotSupported(self.id + ' cancelOrder() not support self market type') + # + # spot and margin + # + # { + # "code": 0, + # "success": [ + # "198361cecdc65f9c8c9bb2fa68faec40", + # "3fb0d98e51c18954f10d439a9cf57de0" + # ], + # "error": [ + # "78a7104e3c65cc0c5a212a53e76d0205" + # ] + # } + # + # swap + # + # { + # "code": 0, + # "data": "1590923061186531328" + # } + # + if (marketType == 'spot') or (marketType == 'margin'): + canceledOrders = self.safe_value(response, 'success', []) + numCanceledOrders = len(canceledOrders) + if numCanceledOrders != 1: + raise OrderNotFound(self.id + ' cancelOrder() ' + id + ' not found') + orders = self.parse_cancel_orders(response) + return self.safe_dict(orders, 0) + else: + return self.safe_order({ + 'info': response, + 'orderId': self.safe_string(response, 'data'), + }) + + def parse_cancel_orders(self, response): + success = self.safe_list(response, 'success') + error = self.safe_list(response, 'error') + result = [] + for i in range(0, len(success)): + order = success[i] + result.append(self.safe_order({ + 'info': order, + 'id': order, + 'status': 'canceled', + })) + for i in range(0, len(error)): + order = error[i] + result.append(self.safe_order({ + 'info': order, + 'id': self.safe_string_2(order, 'order-id', 'order_id'), + 'status': 'failed', + 'clientOrderId': self.safe_string(order, 'client-order-id'), + })) + return result + + def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): + """ + cancel multiple orders + :param str[] ids: order ids + :param str symbol: not used by digifinex cancelOrders() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an list of `order structures ` + """ + self.load_markets() + defaultType = self.safe_string(self.options, 'defaultType', 'spot') + orderType = self.safe_string(params, 'type', defaultType) + params = self.omit(params, 'type') + request: dict = { + 'market': orderType, + 'order_id': ','.join(ids), + } + response = self.privateSpotPostSpotOrderCancel(self.extend(request, params)) + # + # { + # "code": 0, + # "success": [ + # "198361cecdc65f9c8c9bb2fa68faec40", + # "3fb0d98e51c18954f10d439a9cf57de0" + # ], + # "error": [ + # "78a7104e3c65cc0c5a212a53e76d0205" + # ] + # } + # + return self.parse_cancel_orders(response) + + def parse_order_status(self, status: Str): + statuses: dict = { + '0': 'open', + '1': 'open', # partially filled + '2': 'closed', + '3': 'canceled', + '4': 'canceled', # partially filled and canceled + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # spot: createOrder + # + # { + # "code": 0, + # "order_id": "198361cecdc65f9c8c9bb2fa68faec40" + # } + # + # swap: createOrder + # + # { + # "code": 0, + # "data": "1590873693003714560" + # } + # + # spot and swap: createOrders + # + # { + # "order_id": "d64d92a5e0a120f792f385485bc3d95b", + # "instrument_id": "BTC_USDT", + # "amount": 0.0001, + # "price": 27000 + # } + # + # spot: fetchOrder, fetchOpenOrders, fetchOrders + # + # { + # "symbol": "BTC_USDT", + # "order_id": "dd3164b333a4afa9d5730bb87f6db8b3", + # "created_date": 1562303547, + # "finished_date": 0, + # "price": 0.1, + # "amount": 1, + # "cash_amount": 1, + # "executed_amount": 0, + # "avg_price": 0, + # "status": 1, + # "type": "buy", + # "kind": "margin" + # } + # + # swap: fetchOrder, fetchOpenOrders, fetchOrders + # + # { + # "order_id": "1590898207657824256", + # "instrument_id": "BTCUSDTPERP", + # "margin_mode": "crossed", + # "contract_val": "0.001", + # "type": 1, + # "order_type": 0, + # "price": "14000", + # "size": "6", + # "filled_qty": "0", + # "price_avg": "0", + # "fee": "0", + # "state": 0, + # "leverage": "20", + # "turnover": "0", + # "has_stop": 0, + # "insert_time": 1668134664828, + # "time_stamp": 1668134664828 + # } + # + timestamp = None + lastTradeTimestamp = None + timeInForce = None + type = None + side = self.safe_string(order, 'type') + marketId = self.safe_string_2(order, 'symbol', 'instrument_id') + symbol = self.safe_symbol(marketId, market) + market = self.market(symbol) + if market['type'] == 'swap': + orderType = self.safe_integer(order, 'order_type') + if orderType is not None: + if (orderType == 9) or (orderType == 10) or (orderType == 11) or (orderType == 12) or (orderType == 15): + timeInForce = 'FOK' + elif (orderType == 1) or (orderType == 2) or (orderType == 3) or (orderType == 4) or (orderType == 13): + timeInForce = 'IOC' + elif (orderType == 6) or (orderType == 7) or (orderType == 8) or (orderType == 14): + timeInForce = 'GTC' + if (orderType == 0) or (orderType == 1) or (orderType == 4) or (orderType == 5) or (orderType == 9) or (orderType == 10): + type = 'limit' + else: + type = 'market' + if side == '1': + side = 'open long' + elif side == '2': + side = 'open short' + elif side == '3': + side = 'close long' + elif side == '4': + side = 'close short' + timestamp = self.safe_integer(order, 'insert_time') + lastTradeTimestamp = self.safe_integer(order, 'time_stamp') + else: + timestamp = self.safe_timestamp(order, 'created_date') + lastTradeTimestamp = self.safe_timestamp(order, 'finished_date') + if side is not None: + parts = side.split('_') + numParts = len(parts) + if numParts > 1: + side = parts[0] + type = parts[1] + else: + type = 'limit' + return self.safe_order({ + 'info': order, + 'id': self.safe_string_2(order, 'order_id', 'data'), + 'clientOrderId': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': lastTradeTimestamp, + 'symbol': symbol, + 'type': type, + 'timeInForce': timeInForce, + 'postOnly': None, + 'side': side, + 'price': self.safe_number(order, 'price'), + 'triggerPrice': None, + 'amount': self.safe_number_2(order, 'amount', 'size'), + 'filled': self.safe_number_2(order, 'executed_amount', 'filled_qty'), + 'remaining': None, + 'cost': None, + 'average': self.safe_number_2(order, 'avg_price', 'price_avg'), + 'status': self.parse_order_status(self.safe_string_2(order, 'status', 'state')), + 'fee': { + 'cost': self.safe_number(order, 'fee'), + }, + 'trades': None, + }, market) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://docs.digifinex.com/en-ww/spot/v3/rest.html#current-active-orders + https://docs.digifinex.com/en-ww/swap/v2/rest.html#openorder + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + marketType = None + marketType, params = self.handle_market_type_and_params('fetchOpenOrders', market, params) + marginMode, query = self.handle_margin_mode_and_params('fetchOpenOrders', params) + request: dict = {} + swap = (marketType == 'swap') + if swap: + if since is not None: + request['start_timestamp'] = since + if limit is not None: + request['limit'] = limit + else: + request['market'] = marketType + if market is not None: + marketIdRequest = 'instrument_id' if swap else 'symbol' + request[marketIdRequest] = market['id'] + response = None + if marginMode is not None or marketType == 'margin': + marketType = 'margin' + response = self.privateSpotGetMarginOrderCurrent(self.extend(request, query)) + elif marketType == 'spot': + response = self.privateSpotGetSpotOrderCurrent(self.extend(request, query)) + elif marketType == 'swap': + response = self.privateSwapGetTradeOpenOrders(self.extend(request, query)) + else: + raise NotSupported(self.id + ' fetchOpenOrders() not support self market type') + # + # spot and margin + # + # { + # "code": 0, + # "data": [ + # { + # "symbol": "BTC_USDT", + # "order_id": "dd3164b333a4afa9d5730bb87f6db8b3", + # "created_date": 1562303547, + # "finished_date": 0, + # "price": 0.1, + # "amount": 1, + # "cash_amount": 1, + # "executed_amount": 0, + # "avg_price": 0, + # "status": 1, + # "type": "buy", + # "kind": "margin" + # } + # ] + # } + # + # swap + # + # { + # "code": 0, + # "data": [ + # { + # "order_id": "1590898207657824256", + # "instrument_id": "BTCUSDTPERP", + # "margin_mode": "crossed", + # "contract_val": "0.001", + # "type": 1, + # "order_type": 0, + # "price": "14000", + # "size": "6", + # "filled_qty": "0", + # "price_avg": "0", + # "fee": "0", + # "state": 0, + # "leverage": "20", + # "turnover": "0", + # "has_stop": 0, + # "insert_time": 1668134664828, + # "time_stamp": 1668134664828 + # }, + # ... + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_orders(data, market, since, limit) + + def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://docs.digifinex.com/en-ww/spot/v3/rest.html#get-all-orders-including-history-orders + https://docs.digifinex.com/en-ww/swap/v2/rest.html#historyorder + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + marketType = None + marketType, params = self.handle_market_type_and_params('fetchOrders', market, params) + marginMode, query = self.handle_margin_mode_and_params('fetchOrders', params) + request: dict = {} + if marketType == 'swap': + if since is not None: + request['start_timestamp'] = since + else: + request['market'] = marketType + if since is not None: + request['start_time'] = self.parse_to_int(since / 1000) # default 3 days from now, max 30 days + if market is not None: + marketIdRequest = 'instrument_id' if (marketType == 'swap') else 'symbol' + request[marketIdRequest] = market['id'] + if limit is not None: + request['limit'] = limit + response = None + if marginMode is not None or marketType == 'margin': + marketType = 'margin' + response = self.privateSpotGetMarginOrderHistory(self.extend(request, query)) + elif marketType == 'spot': + response = self.privateSpotGetSpotOrderHistory(self.extend(request, query)) + elif marketType == 'swap': + response = self.privateSwapGetTradeHistoryOrders(self.extend(request, query)) + else: + raise NotSupported(self.id + ' fetchOrders() not support self market type') + # + # spot and margin + # + # { + # "code": 0, + # "data": [ + # { + # "symbol": "BTC_USDT", + # "order_id": "dd3164b333a4afa9d5730bb87f6db8b3", + # "created_date": 1562303547, + # "finished_date": 0, + # "price": 0.1, + # "amount": 1, + # "cash_amount": 1, + # "executed_amount": 0, + # "avg_price": 0, + # "status": 1, + # "type": "buy", + # "kind": "margin" + # } + # ] + # } + # + # swap + # + # { + # "code": 0, + # "data": [ + # { + # "order_id": "1590136768156405760", + # "instrument_id": "BTCUSDTPERP", + # "margin_mode": "crossed", + # "contract_val": "0.001", + # "type": 1, + # "order_type": 8, + # "price": "18660.2", + # "size": "1", + # "filled_qty": "1", + # "price_avg": "18514.5", + # "fee": "0.00925725", + # "state": 2, + # "leverage": "20", + # "turnover": "18.5145", + # "has_stop": 0, + # "insert_time": 1667953123526, + # "time_stamp": 1667953123596 + # }, + # ... + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_orders(data, market, since, limit) + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://docs.digifinex.com/en-ww/spot/v3/rest.html#get-order-status + https://docs.digifinex.com/en-ww/swap/v2/rest.html#orderinfo + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + marketType = None + marketType, params = self.handle_market_type_and_params('fetchOrder', market, params) + marginMode, query = self.handle_margin_mode_and_params('fetchOrder', params) + request: dict = { + 'order_id': id, + } + if marketType == 'swap': + if market is not None: + request['instrument_id'] = market['id'] + else: + request['market'] = marketType + response = None + if (marginMode is not None) or (marketType == 'margin'): + marketType = 'margin' + response = self.privateSpotGetMarginOrder(self.extend(request, query)) + elif marketType == 'spot': + response = self.privateSpotGetSpotOrder(self.extend(request, query)) + elif marketType == 'swap': + response = self.privateSwapGetTradeOrderInfo(self.extend(request, query)) + else: + raise NotSupported(self.id + ' fetchOrder() not support self market type') + # + # spot and margin + # + # { + # "code": 0, + # "data": [ + # { + # "symbol": "BTC_USDT", + # "order_id": "dd3164b333a4afa9d5730bb87f6db8b3", + # "created_date": 1562303547, + # "finished_date": 0, + # "price": 0.1, + # "amount": 1, + # "cash_amount": 1, + # "executed_amount": 0, + # "avg_price": 0, + # "status": 1, + # "type": "buy", + # "kind": "margin" + # } + # ] + # } + # + # swap + # + # { + # "code": 0, + # "data": { + # "order_id": "1590923061186531328", + # "instrument_id": "ETHUSDTPERP", + # "margin_mode": "crossed", + # "contract_val": "0.01", + # "type": 1, + # "order_type": 0, + # "price": "900", + # "size": "6", + # "filled_qty": "0", + # "price_avg": "0", + # "fee": "0", + # "state": 0, + # "leverage": "20", + # "turnover": "0", + # "has_stop": 0, + # "insert_time": 1668140590372, + # "time_stamp": 1668140590372 + # } + # } + # + data = self.safe_value(response, 'data') + order = data if (marketType == 'swap') else self.safe_value(data, 0) + if order is None: + raise OrderNotFound(self.id + ' fetchOrder() order ' + str(id) + ' not found') + return self.parse_order(order, market) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://docs.digifinex.com/en-ww/spot/v3/rest.html#customer-39-s-trades + https://docs.digifinex.com/en-ww/swap/v2/rest.html#historytrade + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = None + request: dict = {} + if symbol is not None: + market = self.market(symbol) + marketType = None + marketType, params = self.handle_market_type_and_params('fetchMyTrades', market, params) + marginMode, query = self.handle_margin_mode_and_params('fetchMyTrades', params) + if marketType == 'swap': + if since is not None: + request['start_timestamp'] = since + else: + request['market'] = marketType + if since is not None: + request['start_time'] = self.parse_to_int(since / 1000) # default 3 days from now, max 30 days + marketIdRequest = 'instrument_id' if (marketType == 'swap') else 'symbol' + if symbol is not None: + request[marketIdRequest] = market['id'] + if limit is not None: + request['limit'] = limit + response = None + if marginMode is not None or marketType == 'margin': + marketType = 'margin' + response = self.privateSpotGetMarginMytrades(self.extend(request, query)) + elif marketType == 'spot': + response = self.privateSpotGetSpotMytrades(self.extend(request, query)) + elif marketType == 'swap': + response = self.privateSwapGetTradeHistoryTrades(self.extend(request, query)) + else: + raise NotSupported(self.id + ' fetchMyTrades() not support self market type') + # + # spot and margin + # + # { + # "list":[ + # { + # "timestamp":1639506068, + # "is_maker":false, + # "id":"8975951332", + # "amount":31.83, + # "side":"sell_market", + # "symbol":"DOGE_USDT", + # "fee_currency":"USDT", + # "fee":0.01163774826 + # ,"order_id":"32b169792f4a7a19e5907dc29fc123d4", + # "price":0.182811 + # } + # ], + # "code": 0 + # } + # + # swap + # + # { + # "code": 0, + # "data": [ + # { + # "trade_id": "1590136768424841218", + # "instrument_id": "BTCUSDTPERP", + # "order_id": "1590136768156405760", + # "type": 1, + # "order_type": 8, + # "price": "18514.5", + # "size": "1", + # "fee": "0.00925725", + # "close_profit": "0", + # "leverage": "20", + # "trade_type": 0, + # "match_role": 1, + # "trade_time": 1667953123562 + # }, + # ... + # ] + # } + # + responseRequest = 'data' if (marketType == 'swap') else 'list' + data = self.safe_list(response, responseRequest, []) + return self.parse_trades(data, market, since, limit) + + def parse_ledger_entry_type(self, type): + types: dict = {} + return self.safe_string(types, type, type) + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + # + # spot and margin + # + # { + # "currency_mark": "BTC", + # "type": 100234, + # "num": -10, + # "balance": 0.1, + # "time": 1546272000 + # } + # + # swap + # + # { + # "currency": "USDT", + # "finance_type": 17, + # "change": "-3.01", + # "timestamp": 1650809432000 + # } + # + type = self.parse_ledger_entry_type(self.safe_string_2(item, 'type', 'finance_type')) + currencyId = self.safe_string_2(item, 'currency_mark', 'currency') + code = self.safe_currency_code(currencyId, currency) + currency = self.safe_currency(currencyId, currency) + amount = self.safe_number_2(item, 'num', 'change') + after = self.safe_number(item, 'balance') + timestamp = self.safe_timestamp(item, 'time') + if timestamp is None: + timestamp = self.safe_integer(item, 'timestamp') + return self.safe_ledger_entry({ + 'info': item, + 'id': None, + 'direction': None, + 'account': None, + 'referenceId': None, + 'referenceAccount': None, + 'type': type, + 'currency': code, + 'amount': amount, + 'before': None, + 'after': after, + 'status': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'fee': None, + }, currency) + + def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + + https://docs.digifinex.com/en-ww/spot/v3/rest.html#spot-margin-otc-financial-logs + https://docs.digifinex.com/en-ww/swap/v2/rest.html#bills + + :param str [code]: unified currency code, default is None + :param int [since]: timestamp in ms of the earliest ledger entry, default is None + :param int [limit]: max number of ledger entries to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ledger structure ` + """ + self.load_markets() + request: dict = {} + marketType = None + marketType, params = self.handle_market_type_and_params('fetchLedger', None, params) + marginMode, query = self.handle_margin_mode_and_params('fetchLedger', params) + if marketType == 'swap': + if since is not None: + request['start_timestamp'] = since + else: + request['market'] = marketType + if since is not None: + request['start_time'] = self.parse_to_int(since / 1000) # default 3 days from now, max 30 days + currencyIdRequest = 'currency' if (marketType == 'swap') else 'currency_mark' + currency = None + if code is not None: + currency = self.currency(code) + request[currencyIdRequest] = currency['id'] + if limit is not None: + request['limit'] = limit + response = None + if marginMode is not None or marketType == 'margin': + marketType = 'margin' + response = self.privateSpotGetMarginFinancelog(self.extend(request, query)) + elif marketType == 'spot': + response = self.privateSpotGetSpotFinancelog(self.extend(request, query)) + elif marketType == 'swap': + response = self.privateSwapGetAccountFinanceRecord(self.extend(request, query)) + else: + raise NotSupported(self.id + ' fetchLedger() not support self market type') + # + # spot and margin + # + # { + # "code": 0, + # "data": { + # "total": 521, + # "finance": [ + # { + # "currency_mark": "BTC", + # "type": 100234, + # "num": 28457, + # "balance": 0.1, + # "time": 1546272000 + # } + # ] + # } + # } + # + # swap + # + # { + # "code": 0, + # "data": [ + # { + # "currency": "USDT", + # "finance_type": 17, + # "change": "3.01", + # "timestamp": 1650809432000 + # }, + # ] + # } + # + ledger = None + if marketType == 'swap': + ledger = self.safe_value(response, 'data', []) + else: + data = self.safe_value(response, 'data', {}) + ledger = self.safe_value(data, 'finance', []) + return self.parse_ledger(ledger, currency, since, limit) + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + # + # { + # "addressTag":"", + # "address":"0xf1104d9f8624f89775a3e9d480fc0e75a8ef4373", + # "currency":"USDT", + # "chain":"ERC20" + # } + # + address = self.safe_string(depositAddress, 'address') + tag = self.safe_string(depositAddress, 'addressTag') + currencyId = self.safe_string_upper(depositAddress, 'currency') + code = self.safe_currency_code(currencyId) + return { + 'info': depositAddress, + 'currency': code, + 'network': None, + 'address': address, + 'tag': tag, + } + + def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + } + response = self.privateSpotGetDepositAddress(self.extend(request, params)) + # + # { + # "data":[ + # { + # "addressTag":"", + # "address":"0xf1104d9f8624f89775a3e9d480fc0e75a8ef4373", + # "currency":"USDT", + # "chain":"ERC20" + # } + # ], + # "code":200 + # } + # + data = self.safe_value(response, 'data', []) + addresses = self.parse_deposit_addresses(data, [currency['code']]) + address = self.safe_value(addresses, code) + if address is None: + raise InvalidAddress(self.id + ' fetchDepositAddress() did not return an address for ' + code + ' - create the deposit address in the user settings on the exchange website first.') + return address + + def fetch_transactions_by_type(self, type, code: Str = None, since: Int = None, limit: Int = None, params={}): + self.load_markets() + currency = None + request: dict = { + # 'currency': currency['id'], + # 'from': 'fromId', # When direct is' prev ', from is 1, returning from old to new ascending, when direct is' next ', from is the ID of the most recent record, returned from the old descending order + # 'size': 100, # default 100, max 500 + # 'direct': 'prev', # "prev" ascending, "next" descending + } + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + if limit is not None: + request['size'] = min(500, limit) + response = None + if type == 'deposit': + response = self.privateSpotGetDepositHistory(self.extend(request, params)) + else: + response = self.privateSpotGetWithdrawHistory(self.extend(request, params)) + # + # { + # "code": 200, + # "data": [ + # { + # "id": 1171, + # "currency": "xrp", + # "hash": "ed03094b84eafbe4bc16e7ef766ee959885ee5bcb265872baaa9c64e1cf86c2b", + # "chain": "", + # "amount": 7.457467, + # "address": "rae93V8d2mdoUQHwBDBdM4NHCMehRJAsbm", + # "memo": "100040", + # "fee": 0, + # "state": "safe", + # "created_date": "2020-04-20 11:23:00", + # "finished_date": "2020-04-20 13:23:00" + # }, + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_transactions(data, currency, since, limit, {'type': type}) + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + return self.fetch_transactions_by_type('deposit', code, since, limit, params) + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + return self.fetch_transactions_by_type('withdrawal', code, since, limit, params) + + def parse_transaction_status(self, status: Str): + # deposit state includes: 1(in deposit), 2(to be confirmed), 3(successfully deposited), 4(stopped) + # withdrawal state includes: 1(application in progress), 2(to be confirmed), 3(completed), 4(rejected) + statuses: dict = { + '1': 'pending', # in Progress + '2': 'pending', # to be confirmed + '3': 'ok', # Completed + '4': 'failed', # Rejected + } + return self.safe_string(statuses, status, status) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # withdraw + # + # { + # "code": 200, + # "withdraw_id": 700 + # } + # + # fetchDeposits, fetchWithdrawals + # + # { + # "id": 1171, + # "currency": "xrp", + # "hash": "ed03094b84eafbe4bc16e7ef766ee959885ee5bcb265872baaa9c64e1cf86c2b", + # "chain": "", + # "amount": 7.457467, + # "address": "rae93V8d2mdoUQHwBDBdM4NHCMehRJAsbm", + # "memo": "100040", + # "fee": 0, + # "state": "safe", + # "created_date": "2020-04-20 11:23:00", + # "finished_date": "2020-04-20 13:23:00" + # } + # + id = self.safe_string_2(transaction, 'id', 'withdraw_id') + address = self.safe_string(transaction, 'address') + tag = self.safe_string(transaction, 'memo') + txid = self.safe_string(transaction, 'hash') + currencyId = self.safe_string_upper(transaction, 'currency') + code = self.safe_currency_code(currencyId, currency) + timestamp = self.parse8601(self.safe_string(transaction, 'created_date')) + updated = self.parse8601(self.safe_string(transaction, 'finished_date')) + status = self.parse_transaction_status(self.safe_string(transaction, 'state')) + amount = self.safe_number(transaction, 'amount') + feeCost = self.safe_number(transaction, 'fee') + fee = None + if feeCost is not None: + fee = {'currency': code, 'cost': feeCost} + network = self.safe_string(transaction, 'chain') + return { + 'info': transaction, + 'id': id, + 'txid': txid, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': network, + 'address': address, + 'addressTo': address, + 'addressFrom': None, + 'tag': tag, + 'tagTo': tag, + 'tagFrom': None, + 'type': None, + 'amount': amount, + 'currency': code, + 'status': status, + 'updated': updated, + 'internal': None, + 'comment': None, + 'fee': fee, + } + + def parse_transfer_status(self, status: Str) -> Str: + statuses: dict = { + '0': 'ok', + } + return self.safe_string(statuses, status, status) + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + # + # transfer between spot, margin and OTC + # + # { + # "code": 0 + # } + # + # transfer between spot and swap + # + # { + # "code": 0, + # "data": { + # "type": 2, + # "currency": "USDT", + # "transfer_amount": "5" + # } + # } + # + # fetchTransfers + # + # { + # "transfer_id": 130524, + # "type": 1, + # "currency": "USDT", + # "amount": "24", + # "timestamp": 1666505659000 + # } + # + fromAccount = None + toAccount = None + data = self.safe_dict(transfer, 'data', transfer) + type = self.safe_integer(data, 'type') + if type == 1: + fromAccount = 'spot' + toAccount = 'swap' + elif type == 2: + fromAccount = 'swap' + toAccount = 'spot' + timestamp = self.safe_integer(transfer, 'timestamp') + return { + 'info': transfer, + 'id': self.safe_string(transfer, 'transfer_id'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'currency': self.safe_currency_code(self.safe_string(data, 'currency'), currency), + 'amount': self.safe_number_2(data, 'amount', 'transfer_amount'), + 'fromAccount': fromAccount, + 'toAccount': toAccount, + 'status': self.parse_transfer_status(self.safe_string(transfer, 'code')), + } + + def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://docs.digifinex.com/en-ww/spot/v3/rest.html#transfer-assets-among-accounts + https://docs.digifinex.com/en-ww/swap/v2/rest.html#accounttransfer + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: 'spot', 'swap', 'margin', 'OTC' - account to transfer from + :param str toAccount: 'spot', 'swap', 'margin', 'OTC' - account to transfer to + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transfer structure ` + """ + self.load_markets() + currency = self.currency(code) + currencyId = currency['id'] + accountsByType = self.safe_value(self.options, 'accountsByType', {}) + fromId = self.safe_string(accountsByType, fromAccount, fromAccount) + toId = self.safe_string(accountsByType, toAccount, toAccount) + request = {} + fromSwap = (fromAccount == 'swap') + toSwap = (toAccount == 'swap') + response = None + amountString = self.currency_to_precision(code, amount) + if fromSwap or toSwap: + if (fromId != '1') and (toId != '1'): + raise ExchangeError(self.id + ' transfer() supports transferring between spot and swap, spot and margin, spot and OTC only') + request['type'] = 1 if toSwap else 2 # 1 = spot to swap, 2 = swap to spot + request['currency'] = currencyId + request['transfer_amount'] = amountString + # + # { + # "code": 0, + # "data": { + # "type": 2, + # "currency": "USDT", + # "transfer_amount": "5" + # } + # } + # + response = self.privateSwapPostAccountTransfer(self.extend(request, params)) + else: + request['currency_mark'] = currencyId + request['num'] = amountString + request['from'] = fromId # 1 = SPOT, 2 = MARGIN, 3 = OTC + request['to'] = toId # 1 = SPOT, 2 = MARGIN, 3 = OTC + # + # { + # "code": 0 + # } + # + response = self.privateSpotPostTransfer(self.extend(request, params)) + return self.parse_transfer(response, currency) + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.check_address(address) + self.load_markets() + currency = self.currency(code) + request: dict = { + # 'chain': 'ERC20', 'OMNI', 'TRC20', # required for USDT + 'address': address, + 'amount': self.currency_to_precision(code, amount), + 'currency': currency['id'], + } + if tag is not None: + request['memo'] = tag + response = self.privateSpotPostWithdrawNew(self.extend(request, params)) + # + # { + # "code": 200, + # "withdraw_id": 700 + # } + # + return self.parse_transaction(response, currency) + + def fetch_borrow_interest(self, code: Str = None, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[BorrowInterest]: + self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + response = self.privateSpotGetMarginPositions(self.extend(request, params)) + # + # { + # "margin": "45.71246418952618", + # "code": 0, + # "margin_rate": "7.141978570340037", + # "positions": [ + # { + # "amount": 0.0006103, + # "side": "go_long", + # "entry_price": 31428.72, + # "liquidation_rate": 0.3, + # "liquidation_price": 10225.335481159, + # "unrealized_roe": -0.0076885829266987, + # "symbol": "BTC_USDT", + # "unrealized_pnl": -0.049158102631999, + # "leverage_ratio": 3 + # } + # ], + # "unrealized_pnl": "-0.049158102631998504" + # } + # + rows = self.safe_value(response, 'positions') + interest = self.parse_borrow_interests(rows, market) + return self.filter_by_currency_since_limit(interest, code, since, limit) + + def parse_borrow_interest(self, info: dict, market: Market = None) -> BorrowInterest: + # + # { + # "amount": 0.0006103, + # "side": "go_long", + # "entry_price": 31428.72, + # "liquidation_rate": 0.3, + # "liquidation_price": 10225.335481159, + # "unrealized_roe": -0.0076885829266987, + # "symbol": "BTC_USDT", + # "unrealized_pnl": -0.049158102631999, + # "leverage_ratio": 3 + # } + # + marketId = self.safe_string(info, 'symbol') + amountString = self.safe_string(info, 'amount') + leverageString = self.safe_string(info, 'leverage_ratio') + amountInvested = Precise.string_div(amountString, leverageString) + amountBorrowed = Precise.string_sub(amountString, amountInvested) + currency = None if (market is None) else market['base'] + symbol = self.safe_symbol(marketId, market) + return { + 'info': info, + 'symbol': symbol, + 'currency': currency, + 'interest': None, + 'interestRate': 0.001, # all interest rates on digifinex are 0.1% + 'amountBorrowed': self.parse_number(amountBorrowed), + 'marginMode': None, + 'timestamp': None, + 'datetime': None, + } + + def fetch_cross_borrow_rate(self, code: str, params={}) -> CrossBorrowRate: + """ + fetch the rate of interest to borrow a currency for margin trading + + https://docs.digifinex.com/en-ww/spot/v3/rest.html#margin-assets + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `borrow rate structure ` + """ + self.load_markets() + request: dict = {} + response = self.privateSpotGetMarginAssets(self.extend(request, params)) + # + # { + # "list": [ + # { + # "valuation_rate": 1, + # "total": 1.92012186174, + # "free": 1.92012186174, + # "currency": "USDT" + # }, + # ], + # "total": 45.133305540922, + # "code": 0, + # "unrealized_pnl": 0, + # "free": 45.133305540922, + # "equity": 45.133305540922 + # } + # + data = self.safe_value(response, 'list', []) + result = [] + for i in range(0, len(data)): + entry = data[i] + if self.safe_string(entry, 'currency') == code: + result = entry + currency = self.currency(code) + return self.parse_borrow_rate(result, currency) + + def fetch_cross_borrow_rates(self, params={}) -> CrossBorrowRates: + """ + fetch the borrow interest rates of all currencies + + https://docs.digifinex.com/en-ww/spot/v3/rest.html#margin-assets + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `borrow rate structures ` + """ + self.load_markets() + response = self.privateSpotGetMarginAssets(params) + # + # { + # "list": [ + # { + # "valuation_rate": 1, + # "total": 1.92012186174, + # "free": 1.92012186174, + # "currency": "USDT" + # }, + # ], + # "total": 45.133305540922, + # "code": 0, + # "unrealized_pnl": 0, + # "free": 45.133305540922, + # "equity": 45.133305540922 + # } + # + result = self.safe_value(response, 'list', []) + return self.parse_borrow_rates(result, 'currency') + + def parse_borrow_rate(self, info, currency: Currency = None): + # + # { + # "valuation_rate": 1, + # "total": 1.92012186174, + # "free": 1.92012186174, + # "currency": "USDT" + # } + # + timestamp = self.milliseconds() + currencyId = self.safe_string(info, 'currency') + return { + 'currency': self.safe_currency_code(currencyId, currency), + 'rate': 0.001, # all interest rates on digifinex are 0.1% + 'period': 86400000, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'info': info, + } + + def parse_borrow_rates(self, info, codeKey): + # + # { + # "valuation_rate": 1, + # "total": 1.92012186174, + # "free": 1.92012186174, + # "currency": "USDT" + # }, + # + result: dict = {} + for i in range(0, len(info)): + item = info[i] + currency = self.safe_string(item, codeKey) + code = self.safe_currency_code(currency) + borrowRate = self.parse_borrow_rate(item) + result[code] = borrowRate + return result + + def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate + + https://docs.digifinex.com/en-ww/swap/v2/rest.html#currentfundingrate + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + self.load_markets() + market = self.market(symbol) + if not market['swap']: + raise BadSymbol(self.id + ' fetchFundingRate() supports swap contracts only') + request: dict = { + 'instrument_id': market['id'], + } + response = self.publicSwapGetPublicFundingRate(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "instrument_id": "BTCUSDTPERP", + # "funding_rate": "-0.00012", + # "funding_time": 1662710400000, + # "next_funding_rate": "0.0001049907085171607", + # "next_funding_time": 1662739200000 + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_funding_rate(data, market) + + def fetch_funding_interval(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate interval + + https://docs.digifinex.com/en-ww/swap/v2/rest.html#currentfundingrate + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + return self.fetch_funding_rate(symbol, params) + + def parse_funding_rate(self, contract, market: Market = None) -> FundingRate: + # + # { + # "instrument_id": "BTCUSDTPERP", + # "funding_rate": "-0.00012", + # "funding_time": 1662710400000, + # "next_funding_rate": "0.0001049907085171607", + # "next_funding_time": 1662739200000 + # } + # + marketId = self.safe_string(contract, 'instrument_id') + timestamp = self.safe_integer(contract, 'funding_time') + nextTimestamp = self.safe_integer(contract, 'next_funding_time') + fundingTimeString = self.safe_string(contract, 'funding_time') + nextFundingTimeString = self.safe_string(contract, 'next_funding_time') + millisecondsInterval = Precise.string_sub(nextFundingTimeString, fundingTimeString) + return { + 'info': contract, + 'symbol': self.safe_symbol(marketId, market), + 'markPrice': None, + 'indexPrice': None, + 'interestRate': None, + 'estimatedSettlePrice': None, + 'timestamp': None, + 'datetime': None, + 'fundingRate': self.safe_number(contract, 'funding_rate'), + 'fundingTimestamp': timestamp, + 'fundingDatetime': self.iso8601(timestamp), + 'nextFundingRate': self.safe_number(contract, 'next_funding_rate'), + 'nextFundingTimestamp': nextTimestamp, + 'nextFundingDatetime': self.iso8601(nextTimestamp), + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': self.parse_funding_interval(millisecondsInterval), + } + + def parse_funding_interval(self, interval): + intervals: dict = { + '3600000': '1h', + '14400000': '4h', + '28800000': '8h', + '57600000': '16h', + '86400000': '24h', + } + return self.safe_string(intervals, interval, interval) + + def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical funding rate prices + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: timestamp in ms of the earliest funding rate to fetch + :param int [limit]: the maximum amount of `funding rate structures ` to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `funding rate structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchFundingRateHistory() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + if not market['swap']: + raise BadSymbol(self.id + ' fetchFundingRateHistory() supports swap contracts only') + request: dict = { + 'instrument_id': market['id'], + } + if since is not None: + request['start_timestamp'] = since + if limit is not None: + request['limit'] = limit + response = self.publicSwapGetPublicFundingRateHistory(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "instrument_id": "BTCUSDTPERP", + # "funding_rates": [ + # { + # "rate": "-0.00375", + # "time": 1607673600000 + # }, + # ... + # ] + # } + # } + # + data = self.safe_value(response, 'data', {}) + result = self.safe_value(data, 'funding_rates', []) + rates = [] + for i in range(0, len(result)): + entry = result[i] + marketId = self.safe_string(data, 'instrument_id') + symbolInner = self.safe_symbol(marketId) + timestamp = self.safe_integer(entry, 'time') + rates.append({ + 'info': entry, + 'symbol': symbolInner, + 'fundingRate': self.safe_number(entry, 'rate'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + sorted = self.sort_by(rates, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, symbol, since, limit) + + def fetch_trading_fee(self, symbol: str, params={}) -> TradingFeeInterface: + """ + fetch the trading fees for a market + + https://docs.digifinex.com/en-ww/swap/v2/rest.html#tradingfee + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `fee structure ` + """ + self.load_markets() + market = self.market(symbol) + if not market['swap']: + raise BadRequest(self.id + ' fetchTradingFee() supports swap markets only') + request: dict = { + 'instrument_id': market['id'], + } + response = self.privateSwapGetAccountTradingFeeRate(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "instrument_id": "BTCUSDTPERP", + # "taker_fee_rate": "0.0005", + # "maker_fee_rate": "0.0003" + # } + # } + # + data = self.safe_value(response, 'data', {}) + return self.parse_trading_fee(data, market) + + def parse_trading_fee(self, fee: dict, market: Market = None) -> TradingFeeInterface: + # + # { + # "instrument_id": "BTCUSDTPERP", + # "taker_fee_rate": "0.0005", + # "maker_fee_rate": "0.0003" + # } + # + marketId = self.safe_string(fee, 'instrument_id') + symbol = self.safe_symbol(marketId, market) + return { + 'info': fee, + 'symbol': symbol, + 'maker': self.safe_number(fee, 'maker_fee_rate'), + 'taker': self.safe_number(fee, 'taker_fee_rate'), + 'percentage': None, + 'tierBased': None, + } + + def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://docs.digifinex.com/en-ww/spot/v3/rest.html#margin-positions + https://docs.digifinex.com/en-ww/swap/v2/rest.html#positions + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structures ` + """ + self.load_markets() + symbols = self.market_symbols(symbols) + request: dict = {} + market = None + marketType = None + if symbols is not None: + symbol = None + if isinstance(symbols, list): + symbolsLength = len(symbols) + if symbolsLength > 1: + raise BadRequest(self.id + ' fetchPositions() symbols argument cannot contain more than 1 symbol') + symbol = symbols[0] + else: + symbol = symbols + market = self.market(symbol) + marketType, params = self.handle_market_type_and_params('fetchPositions', market, params) + marginMode, query = self.handle_margin_mode_and_params('fetchPositions', params) + if marginMode is not None: + marketType = 'margin' + if market is not None: + marketIdRequest = 'instrument_id' if (marketType == 'swap') else 'symbol' + request[marketIdRequest] = market['id'] + response = None + if marketType == 'spot' or marketType == 'margin': + response = self.privateSpotGetMarginPositions(self.extend(request, query)) + elif marketType == 'swap': + response = self.privateSwapGetAccountPositions(self.extend(request, query)) + else: + raise NotSupported(self.id + ' fetchPositions() not support self market type') + # + # swap + # + # { + # "code": 0, + # "data": [ + # { + # "instrument_id": "BTCUSDTPERP", + # "margin_mode": "crossed", + # "avail_position": "1", + # "avg_cost": "18369.3", + # "last": "18404.7", + # "leverage": "20", + # "liquidation_price": "451.12820512820264", + # "maint_margin_ratio": "0.005", + # "margin": "0.918465", + # "position": "1", + # "realized_pnl": "0", + # "unrealized_pnl": "0.03410000000000224", + # "unrealized_pnl_rate": "0.03712716325608732", + # "side": "long", + # "open_outstanding": "0", + # "risk_score": "0.495049504950495", + # "margin_ratio": "0.4029464788983229", + # "timestamp": 1667960497145 + # }, + # ... + # ] + # } + # + # margin + # + # { + # "margin": "77.71534772983289", + # "code": 0, + # "margin_rate": "10.284503769497306", + # "positions": [ + # { + # "amount": 0.0010605, + # "side": "go_long", + # "entry_price": 18321.39, + # "liquidation_rate": 0.3, + # "liquidation_price": -52754.371758471, + # "unrealized_roe": -0.002784390267332, + # "symbol": "BTC_USDT", + # "unrealized_pnl": -0.010820048189999, + # "leverage_ratio": 5 + # }, + # ... + # ], + # "unrealized_pnl": "-0.10681600018999979" + # } + # + positionRequest = 'data' if (marketType == 'swap') else 'positions' + positions = self.safe_value(response, positionRequest, []) + result = [] + for i in range(0, len(positions)): + result.append(self.parse_position(positions[i], market)) + return self.filter_by_array_positions(result, 'symbol', symbols, False) + + def fetch_position(self, symbol: str, params={}): + """ + + https://docs.digifinex.com/en-ww/spot/v3/rest.html#margin-positions + https://docs.digifinex.com/en-ww/swap/v2/rest.html#positions + + fetch data on a single open contract trade position + :param str symbol: unified market symbol of the market the position is held in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `position structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = {} + marketType = None + marketType, params = self.handle_market_type_and_params('fetchPosition', market, params) + marginMode, query = self.handle_margin_mode_and_params('fetchPosition', params) + if marginMode is not None: + marketType = 'margin' + marketIdRequest = 'instrument_id' if (marketType == 'swap') else 'symbol' + request[marketIdRequest] = market['id'] + response = None + if marketType == 'spot' or marketType == 'margin': + response = self.privateSpotGetMarginPositions(self.extend(request, query)) + elif marketType == 'swap': + response = self.privateSwapGetAccountPositions(self.extend(request, query)) + else: + raise NotSupported(self.id + ' fetchPosition() not support self market type') + # + # swap + # + # { + # "code": 0, + # "data": [ + # { + # "instrument_id": "BTCUSDTPERP", + # "margin_mode": "crossed", + # "avail_position": "1", + # "avg_cost": "18369.3", + # "last": "18388.9", + # "leverage": "20", + # "liquidation_price": "383.38712921065553", + # "maint_margin_ratio": "0.005", + # "margin": "0.918465", + # "position": "1", + # "realized_pnl": "0", + # "unrealized_pnl": "0.021100000000004115", + # "unrealized_pnl_rate": "0.02297311274790451", + # "side": "long", + # "open_outstanding": "0", + # "risk_score": "0.4901960784313725", + # "margin_ratio": "0.40486964045976204", + # "timestamp": 1667960241758 + # } + # ] + # } + # + # margin + # + # { + # "margin": "77.71534772983289", + # "code": 0, + # "margin_rate": "10.284503769497306", + # "positions": [ + # { + # "amount": 0.0010605, + # "side": "go_long", + # "entry_price": 18321.39, + # "liquidation_rate": 0.3, + # "liquidation_price": -52754.371758471, + # "unrealized_roe": -0.002784390267332, + # "symbol": "BTC_USDT", + # "unrealized_pnl": -0.010820048189999, + # "leverage_ratio": 5 + # } + # ], + # "unrealized_pnl": "-0.10681600018999979" + # } + # + dataRequest = 'data' if (marketType == 'swap') else 'positions' + data = self.safe_value(response, dataRequest, []) + position = self.parse_position(data[0], market) + if marketType == 'swap': + return position + else: + position['collateral'] = self.safe_number(response, 'margin') + position['marginRatio'] = self.safe_number(response, 'margin_rate') + return position + + def parse_position(self, position: dict, market: Market = None): + # + # swap + # + # { + # "instrument_id": "BTCUSDTPERP", + # "margin_mode": "crossed", + # "avail_position": "1", + # "avg_cost": "18369.3", + # "last": "18388.9", + # "leverage": "20", + # "liquidation_price": "383.38712921065553", + # "maint_margin_ratio": "0.005", + # "margin": "0.918465", + # "position": "1", + # "realized_pnl": "0", + # "unrealized_pnl": "0.021100000000004115", + # "unrealized_pnl_rate": "0.02297311274790451", + # "side": "long", + # "open_outstanding": "0", + # "risk_score": "0.4901960784313725", + # "margin_ratio": "0.40486964045976204", + # "timestamp": 1667960241758 + # } + # + # margin + # + # { + # "amount": 0.0010605, + # "side": "go_long", + # "entry_price": 18321.39, + # "liquidation_rate": 0.3, + # "liquidation_price": -52754.371758471, + # "unrealized_roe": -0.002784390267332, + # "symbol": "BTC_USDT", + # "unrealized_pnl": -0.010820048189999, + # "leverage_ratio": 5 + # } + # + marketId = self.safe_string_2(position, 'instrument_id', 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + marginMode = self.safe_string(position, 'margin_mode') + if marginMode is not None: + marginMode = 'cross' if (marginMode == 'crossed') else 'isolated' + else: + marginMode = 'crossed' + timestamp = self.safe_integer(position, 'timestamp') + side = self.safe_string(position, 'side') + if side == 'go_long': + side = 'long' + elif side == 'go_short': + side = 'short' + return self.safe_position({ + 'info': position, + 'id': None, + 'symbol': symbol, + 'notional': self.safe_number(position, 'amount'), + 'marginMode': marginMode, + 'liquidationPrice': self.safe_number(position, 'liquidation_price'), + 'entryPrice': self.safe_number_2(position, 'avg_cost', 'entry_price'), + 'unrealizedPnl': self.safe_number(position, 'unrealized_pnl'), + 'contracts': self.safe_number(position, 'avail_position'), + 'contractSize': self.safe_number(market, 'contractSize'), + 'markPrice': self.safe_number(position, 'last'), + 'side': side, + 'hedged': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'maintenanceMargin': self.safe_number(position, 'margin'), + 'maintenanceMarginPercentage': self.safe_number(position, 'maint_margin_ratio'), + 'collateral': None, + 'initialMargin': None, + 'initialMarginPercentage': None, + 'leverage': self.safe_number_2(position, 'leverage', 'leverage_ratio'), + 'marginRatio': self.safe_number(position, 'margin_ratio'), + 'percentage': None, + 'stopLossPrice': None, + 'takeProfitPrice': None, + }) + + def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://docs.digifinex.com/en-ww/swap/v2/rest.html#setleverage + + :param float leverage: the rate of leverage + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: either 'cross' or 'isolated', default is cross + :param str [params.side]: either 'long' or 'short', required for isolated markets only + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setLeverage() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + if market['type'] != 'swap': + raise BadSymbol(self.id + ' setLeverage() supports swap contracts only') + if (leverage < 1) or (leverage > 100): + raise BadRequest(self.id + ' leverage should be between 1 and 100') + request: dict = { + 'instrument_id': market['id'], + 'leverage': leverage, + } + defaultMarginMode = self.safe_string_2(self.options, 'marginMode', 'defaultMarginMode') + marginMode = self.safe_string_lower_2(params, 'marginMode', 'defaultMarginMode', defaultMarginMode) + if marginMode is not None: + marginMode = 'crossed' if (marginMode == 'cross') else 'isolated' + request['margin_mode'] = marginMode + params = self.omit(params, ['marginMode', 'defaultMarginMode']) + if marginMode == 'isolated': + side = self.safe_string(params, 'side') + if side is not None: + request['side'] = side + params = self.omit(params, 'side') + else: + self.check_required_argument('setLeverage', side, 'side', ['long', 'short']) + return self.privateSwapPostAccountLeverage(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "instrument_id": "BTCUSDTPERP", + # "leverage": 30, + # "margin_mode": "crossed", + # "side": "both" + # } + # } + # + + def fetch_transfers(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[TransferEntry]: + """ + fetch the transfer history, only transfers between spot and swap accounts are supported + + https://docs.digifinex.com/en-ww/swap/v2/rest.html#transferrecord + + :param str code: unified currency code of the currency transferred + :param int [since]: the earliest time in ms to fetch transfers for + :param int [limit]: the maximum number of transfers to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transfer structures ` + """ + self.load_markets() + currency = None + request: dict = {} + if code is not None: + currency = self.safe_currency_code(code) + request['currency'] = currency['id'] + if since is not None: + request['start_timestamp'] = since + if limit is not None: + request['limit'] = limit # default 20 max 100 + response = self.privateSwapGetAccountTransferRecord(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "transfer_id": 130524, + # "type": 1, + # "currency": "USDT", + # "amount": "24", + # "timestamp": 1666505659000 + # }, + # ... + # ] + # } + # + transfers = self.safe_list(response, 'data', []) + return self.parse_transfers(transfers, currency, since, limit) + + def fetch_leverage_tiers(self, symbols: Strings = None, params={}) -> LeverageTiers: + """ + + https://docs.digifinex.com/en-ww/swap/v2/rest.html#instruments + + retrieve information on the maximum leverage, for different trade sizes + :param str[]|None symbols: a list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `leverage tiers structures `, indexed by market symbols + """ + self.load_markets() + response = self.publicSwapGetPublicInstruments(params) + # + # { + # "code": 0, + # "data": [ + # { + # "instrument_id": "BTCUSDTPERP", + # "type": "REAL", + # "contract_type": "PERPETUAL", + # "base_currency": "BTC", + # "quote_currency": "USDT", + # "clear_currency": "USDT", + # "contract_value": "0.001", + # "contract_value_currency": "BTC", + # "is_inverse": False, + # "is_trading": True, + # "status": "ONLINE", + # "price_precision": 1, + # "tick_size": "0.1", + # "min_order_amount": 1, + # "open_max_limits": [ + # { + # "leverage": "50", + # "max_limit": "1000000" + # }, + # ] + # }, + # ] + # } + # + data = self.safe_value(response, 'data', []) + symbols = self.market_symbols(symbols) + return self.parse_leverage_tiers(data, symbols, 'instrument_id') + + def fetch_market_leverage_tiers(self, symbol: str, params={}) -> List[LeverageTier]: + """ + retrieve information on the maximum leverage, for different trade sizes for a single market + + https://docs.digifinex.com/en-ww/swap/v2/rest.html#instrument + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `leverage tiers structure ` + """ + self.load_markets() + market = self.market(symbol) + if not market['swap']: + raise BadRequest(self.id + ' fetchMarketLeverageTiers() supports swap markets only') + request: dict = { + 'instrument_id': market['id'], + } + response = self.publicSwapGetPublicInstrument(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "instrument_id": "BTCUSDTPERP", + # "type": "REAL", + # "contract_type": "PERPETUAL", + # "base_currency": "BTC", + # "quote_currency": "USDT", + # "clear_currency": "USDT", + # "contract_value": "0.001", + # "contract_value_currency": "BTC", + # "is_inverse": False, + # "is_trading": True, + # "status": "ONLINE", + # "price_precision": 1, + # "tick_size": "0.1", + # "min_order_amount": 1, + # "open_max_limits": [ + # { + # "leverage": "50", + # "max_limit": "1000000" + # } + # ] + # } + # } + # + data = self.safe_value(response, 'data', {}) + return self.parse_market_leverage_tiers(data, market) + + def parse_market_leverage_tiers(self, info, market: Market = None) -> List[LeverageTier]: + # + # { + # "instrument_id": "BTCUSDTPERP", + # "type": "REAL", + # "contract_type": "PERPETUAL", + # "base_currency": "BTC", + # "quote_currency": "USDT", + # "clear_currency": "USDT", + # "contract_value": "0.001", + # "contract_value_currency": "BTC", + # "is_inverse": False, + # "is_trading": True, + # "status": "ONLINE", + # "price_precision": 1, + # "tick_size": "0.1", + # "min_order_amount": 1, + # "open_max_limits": [ + # { + # "leverage": "50", + # "max_limit": "1000000" + # } + # ] + # } + # + tiers = [] + brackets = self.safe_value(info, 'open_max_limits', {}) + for i in range(0, len(brackets)): + tier = brackets[i] + marketId = self.safe_string(info, 'instrument_id') + market = self.safe_market(marketId, market) + tiers.append({ + 'tier': self.sum(i, 1), + 'symbol': self.safe_symbol(marketId, market, None, 'swap'), + 'currency': market['settle'], + 'minNotional': None, + 'maxNotional': self.safe_number(tier, 'max_limit'), + 'maintenanceMarginRate': None, + 'maxLeverage': self.safe_number(tier, 'leverage'), + 'info': tier, + }) + return tiers + + def handle_margin_mode_and_params(self, methodName, params={}, defaultValue=None): + """ + @ignore + marginMode specified by params["marginMode"], self.options["marginMode"], self.options["defaultMarginMode"], params["margin"] = True or self.options["defaultType"] = 'margin' + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Array: the marginMode in lowercase + """ + defaultType = self.safe_string(self.options, 'defaultType') + isMargin = self.safe_bool(params, 'margin', False) + marginMode = None + marginMode, params = super(digifinex, self).handle_margin_mode_and_params(methodName, params, defaultValue) + if marginMode is not None: + if marginMode != 'cross': + raise NotSupported(self.id + ' only cross margin is supported') + else: + if (defaultType == 'margin') or (isMargin is True): + marginMode = 'cross' + return [marginMode, params] + + def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}): + """ + fetch deposit and withdraw fees + + https://docs.digifinex.com/en-ww/spot/v3/rest.html#get-currency-deposit-and-withdrawal-information + + :param str[]|None codes: not used by fetchDepositWithdrawFees() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `fee structures ` + """ + self.load_markets() + response = self.publicSpotGetCurrencies(params) + # + # { + # "data": [ + # { + # "deposit_status": 0, + # "min_withdraw_fee": 5, + # "withdraw_fee_currency": "USDT", + # "chain": "OMNI", + # "withdraw_fee_rate": 0, + # "min_withdraw_amount": 10, + # "currency": "USDT", + # "withdraw_status": 0, + # "min_deposit_amount": 10 + # }, + # { + # "deposit_status": 1, + # "min_withdraw_fee": 5, + # "withdraw_fee_currency": "USDT", + # "chain": "ERC20", + # "withdraw_fee_rate": 0, + # "min_withdraw_amount": 10, + # "currency": "USDT", + # "withdraw_status": 1, + # "min_deposit_amount": 10 + # }, + # ], + # "code": 200, + # } + # + data = self.safe_list(response, 'data') + return self.parse_deposit_withdraw_fees(data, codes) + + def parse_deposit_withdraw_fees(self, response, codes=None, currencyIdKey=None): + # + # [ + # { + # "deposit_status": 0, + # "min_withdraw_fee": 5, + # "withdraw_fee_currency": "USDT", + # "chain": "OMNI", + # "withdraw_fee_rate": 0, + # "min_withdraw_amount": 10, + # "currency": "USDT", + # "withdraw_status": 0, + # "min_deposit_amount": 10 + # }, + # { + # "deposit_status": 1, + # "min_withdraw_fee": 5, + # "withdraw_fee_currency": "USDT", + # "chain": "ERC20", + # "withdraw_fee_rate": 0, + # "min_withdraw_amount": 10, + # "currency": "USDT", + # "withdraw_status": 1, + # "min_deposit_amount": 10 + # }, + # ] + # + depositWithdrawFees: dict = {} + codes = self.market_codes(codes) + for i in range(0, len(response)): + entry = response[i] + currencyId = self.safe_string(entry, 'currency') + code = self.safe_currency_code(currencyId) + if (codes is None) or (self.in_array(code, codes)): + depositWithdrawFee = self.safe_value(depositWithdrawFees, code) + if depositWithdrawFee is None: + depositWithdrawFees[code] = self.deposit_withdraw_fee({}) + depositWithdrawFees[code]['info'] = [] + depositWithdrawInfo = depositWithdrawFees[code]['info'] + depositWithdrawInfo.append(entry) + networkId = self.safe_string(entry, 'chain') + withdrawFee = self.safe_value(entry, 'min_withdraw_fee') + withdrawResult: dict = { + 'fee': withdrawFee, + 'percentage': False if (withdrawFee is not None) else None, + } + depositResult: dict = { + 'fee': None, + 'percentage': None, + } + if networkId is not None: + networkCode = self.network_id_to_code(networkId) + depositWithdrawFees[code]['networks'][networkCode] = { + 'withdraw': withdrawResult, + 'deposit': depositResult, + } + else: + depositWithdrawFees[code]['withdraw'] = withdrawResult + depositWithdrawFees[code]['deposit'] = depositResult + depositWithdrawCodes = list(depositWithdrawFees.keys()) + for i in range(0, len(depositWithdrawCodes)): + code = depositWithdrawCodes[i] + currency = self.currency(code) + depositWithdrawFees[code] = self.assign_default_deposit_withdraw_fees(depositWithdrawFees[code], currency) + return depositWithdrawFees + + def add_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + add margin to a position + + https://docs.digifinex.com/en-ww/swap/v2/rest.html#positionmargin + + :param str symbol: unified market symbol + :param float amount: amount of margin to add + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str params['side']: the position side: 'long' or 'short' + :returns dict: a `margin structure ` + """ + side = self.safe_string(params, 'side') + self.check_required_argument('addMargin', side, 'side', ['long', 'short']) + return self.modify_margin_helper(symbol, amount, 1, params) + + def reduce_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + remove margin from a position + + https://docs.digifinex.com/en-ww/swap/v2/rest.html#positionmargin + + :param str symbol: unified market symbol + :param float amount: the amount of margin to remove + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str params['side']: the position side: 'long' or 'short' + :returns dict: a `margin structure ` + """ + side = self.safe_string(params, 'side') + self.check_required_argument('reduceMargin', side, 'side', ['long', 'short']) + return self.modify_margin_helper(symbol, amount, 2, params) + + def modify_margin_helper(self, symbol: str, amount, type, params={}) -> MarginModification: + self.load_markets() + side = self.safe_string(params, 'side') + market = self.market(symbol) + request: dict = { + 'instrument_id': market['id'], + 'amount': self.number_to_string(amount), + 'type': type, + 'side': side, + } + response = self.privateSwapPostAccountPositionMargin(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "instrument_id": "BTCUSDTPERP", + # "side": "long", + # "type": 1, + # "amount": "3.6834" + # } + # } + # + code = self.safe_integer(response, 'code') + status = 'ok' if (code == 0) else 'failed' + data = self.safe_value(response, 'data', {}) + return self.extend(self.parse_margin_modification(data, market), { + 'status': status, + }) + + def parse_margin_modification(self, data: dict, market: Market = None) -> MarginModification: + # + # { + # "instrument_id": "BTCUSDTPERP", + # "side": "long", + # "type": 1, + # "amount": "3.6834" + # } + # + marketId = self.safe_string(data, 'instrument_id') + rawType = self.safe_integer(data, 'type') + return { + 'info': data, + 'symbol': self.safe_symbol(marketId, market, None, 'swap'), + 'type': 'add' if (rawType == 1) else 'reduce', + 'marginMode': 'isolated', + 'amount': self.safe_number(data, 'amount'), + 'total': None, + 'code': market['settle'], + 'status': None, + 'timestamp': None, + 'datetime': None, + } + + def fetch_funding_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch the history of funding payments paid and received on self account + + https://docs.digifinex.com/en-ww/swap/v2/rest.html#funding-fee + + :param str [symbol]: unified market symbol + :param int [since]: the earliest time in ms to fetch funding history for + :param int [limit]: the maximum number of funding history structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest funding payment + :returns dict: a `funding history structure ` + """ + self.load_markets() + request: dict = {} + request, params = self.handle_until_option('end_timestamp', request, params) + market = None + if symbol is not None: + market = self.market(symbol) + request['instrument_id'] = market['id'] + if limit is not None: + request['limit'] = limit + if since is not None: + request['start_timestamp'] = since + response = self.privateSwapGetAccountFundingFee(self.extend(request, params)) + # + # { + # "code": 0, + # "data": [ + # { + # "instrument_id": "BTCUSDTPERP", + # "currency": "USDT", + # "amount": "-0.000342814", + # "timestamp": 1698768009440 + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_incomes(data, market, since, limit) + + def parse_income(self, income, market: Market = None): + # + # { + # "instrument_id": "BTCUSDTPERP", + # "currency": "USDT", + # "amount": "-0.000342814", + # "timestamp": 1698768009440 + # } + # + marketId = self.safe_string(income, 'instrument_id') + currencyId = self.safe_string(income, 'currency') + timestamp = self.safe_integer(income, 'timestamp') + return { + 'info': income, + 'symbol': self.safe_symbol(marketId, market, None, 'swap'), + 'code': self.safe_currency_code(currencyId), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': None, + 'amount': self.safe_number(income, 'amount'), + } + + def set_margin_mode(self, marginMode: str, symbol: Str = None, params={}): + """ + set margin mode to 'cross' or 'isolated' + + https://docs.digifinex.com/en-ww/swap/v2/rest.html#positionmode + + :param str marginMode: 'cross' or 'isolated' + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setMarginMode() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + marginMode = marginMode.lower() + if marginMode == 'cross': + marginMode = 'crossed' + request: dict = { + 'instrument_id': market['id'], + 'margin_mode': marginMode, + } + return self.privateSwapPostAccountPositionMode(self.extend(request, params)) + + def sign(self, path, api=[], method='GET', params={}, headers=None, body=None): + signed = api[0] == 'private' + endpoint = api[1] + pathPart = '/v3' if (endpoint == 'spot') else '/swap/v2' + request = '/' + self.implode_params(path, params) + payload = pathPart + request + url = self.urls['api']['rest'] + payload + query = self.omit(params, self.extract_params(path)) + urlencoded = None + if signed and (pathPart == '/swap/v2') and (method == 'POST'): + urlencoded = json.dumps(params) + else: + urlencoded = self.urlencode(self.keysort(query)) + if signed: + auth = None + nonce = None + if pathPart == '/swap/v2': + nonce = str(self.milliseconds()) + auth = nonce + method + payload + if method == 'GET': + if urlencoded: + auth += '?' + urlencoded + elif method == 'POST': + auth += urlencoded + else: + nonce = str(self.nonce()) + auth = urlencoded + signature = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256) + if method == 'GET': + if urlencoded: + url += '?' + urlencoded + elif method == 'POST': + headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + } + if urlencoded: + body = urlencoded + headers = { + 'ACCESS-KEY': self.apiKey, + 'ACCESS-SIGN': signature, + 'ACCESS-TIMESTAMP': nonce, + } + else: + if urlencoded: + url += '?' + urlencoded + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, statusCode: int, statusText: str, url: str, method: str, responseHeaders: dict, responseBody, response, requestHeaders, requestBody): + if not response: + return None # fall back to default error handler + code = self.safe_string(response, 'code') + if (code == '0') or (code == '200'): + return None # no error + feedback = self.id + ' ' + responseBody + if code is None: + raise BadResponse(feedback) + unknownError = [ExchangeError, feedback] + ExceptionClass, message = self.safe_value(self.exceptions['exact'], code, unknownError) + raise ExceptionClass(message) diff --git a/ccxt/exmo.py b/ccxt/exmo.py new file mode 100644 index 0000000..8a603f2 --- /dev/null +++ b/ccxt/exmo.py @@ -0,0 +1,2674 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.exmo import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currencies, Currency, DepositAddress, Int, MarginModification, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, OrderBooks, Trade, TradingFees, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import OnMaintenance +from ccxt.base.errors import InvalidNonce +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class exmo(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(exmo, self).describe(), { + 'id': 'exmo', + 'name': 'EXMO', + 'countries': ['LT'], # Lithuania + 'rateLimit': 100, # 10 requests per 1 second + 'version': 'v1.1', + 'has': { + 'CORS': None, + 'spot': True, + 'margin': True, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': True, + 'cancelOrder': True, + 'cancelOrders': False, + 'createDepositAddress': False, + 'createMarketBuyOrder': True, + 'createMarketBuyOrderWithCost': True, + 'createMarketOrderWithCost': True, + 'createOrder': True, + 'createStopLimitOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'editOrder': True, # margin only + 'fetchAccounts': False, + 'fetchBalance': True, + 'fetchCanceledOrders': True, + 'fetchCurrencies': True, + 'fetchDeposit': True, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchDepositsWithdrawals': True, + 'fetchDepositWithdrawFee': 'emulated', + 'fetchDepositWithdrawFees': True, + 'fetchFundingHistory': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchIndexOHLCV': False, + 'fetchMarginMode': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterestHistory': False, + 'fetchOpenOrders': True, + 'fetchOrder': 'emulated', + 'fetchOrderBook': True, + 'fetchOrderBooks': True, + 'fetchOrderTrades': True, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': True, + 'fetchTransactionFees': True, + 'fetchTransactions': 'emulated', + 'fetchTransfer': False, + 'fetchTransfers': False, + 'fetchWithdrawal': True, + 'fetchWithdrawals': True, + 'reduceMargin': True, + 'setMargin': False, + 'transfer': False, + 'withdraw': True, + }, + 'timeframes': { + '1m': '1', + '5m': '5', + '15m': '15', + '30m': '30', + '45m': '45', + '1h': '60', + '2h': '120', + '3h': '180', + '4h': '240', + '1d': 'D', + '1w': 'W', + '1M': 'M', + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/27766491-1b0ea956-5eda-11e7-9225-40d67b481b8d.jpg', + 'api': { + 'public': 'https://api.exmo.com', + 'private': 'https://api.exmo.com', + 'web': 'https://exmo.me', + }, + 'www': 'https://exmo.me', + 'referral': 'https://exmo.me/?ref=131685', + 'doc': [ + 'https://exmo.me/en/api_doc?ref=131685', + ], + 'fees': 'https://exmo.com/en/docs/fees', + }, + 'api': { + 'web': { + 'get': [ + 'ctrl/feesAndLimits', + 'en/docs/fees', + ], + }, + 'public': { + 'get': [ + 'currency', + 'currency/list/extended', + 'order_book', + 'pair_settings', + 'ticker', + 'trades', + 'candles_history', + 'required_amount', + 'payments/providers/crypto/list', + ], + }, + 'private': { + 'post': [ + 'user_info', + 'order_create', + 'order_cancel', + 'stop_market_order_create', + 'stop_market_order_cancel', + 'user_open_orders', + 'user_trades', + 'user_cancelled_orders', + 'order_trades', + 'deposit_address', + 'withdraw_crypt', + 'withdraw_get_txid', + 'excode_create', + 'excode_load', + 'code_check', + 'wallet_history', + 'wallet_operations', + 'margin/user/order/create', + 'margin/user/order/update', + 'margin/user/order/cancel', + 'margin/user/position/close', + 'margin/user/position/margin_add', + 'margin/user/position/margin_remove', + 'margin/currency/list', + 'margin/pair/list', + 'margin/settings', + 'margin/funding/list', + 'margin/user/info', + 'margin/user/order/list', + 'margin/user/order/history', + 'margin/user/order/trades', + 'margin/user/order/max_quantity', + 'margin/user/position/list', + 'margin/user/position/margin_remove_info', + 'margin/user/position/margin_add_info', + 'margin/user/wallet/list', + 'margin/user/wallet/history', + 'margin/user/trade/list', + 'margin/trades', + 'margin/liquidation/feed', + ], + }, + }, + 'fees': { + 'trading': { + 'feeSide': 'get', + 'tierBased': True, + 'percentage': True, + 'maker': self.parse_number('0.004'), + 'taker': self.parse_number('0.004'), + }, + 'transaction': { + 'tierBased': False, + 'percentage': False, # fixed transaction fees for crypto, see fetchDepositWithdrawFees below + }, + }, + 'options': { + 'networks': { + 'ETH': 'ERC20', + 'TRX': 'TRC20', + }, + 'fetchTradingFees': { + 'method': 'fetchPrivateTradingFees', # or 'fetchPublicTradingFees' + }, + 'margin': { + 'fillResponseFromRequest': True, + }, + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': True, # todo revise + 'triggerPrice': True, # todo: endpoint lacks other features + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': True, + }, + 'hedged': False, + 'selfTradePrevention': False, + 'trailing': False, + 'leverage': True, + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': True, + 'limit': 100, + 'daysBack': None, + 'untilDays': None, + 'symbolRequired': True, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': None, + 'fetchOHLCV': { + 'limit': 1000, # todo, not in request + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'commonCurrencies': { + 'GMT': 'GMT Token', + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'exact': { + '140333': InvalidOrder, # {"error":{"code":140333,"msg":"The number of characters after the point in the price exceeds the maximum number '8\u003e6'"}} + '140434': BadRequest, + '40005': AuthenticationError, # Authorization error, incorrect signature + '40009': InvalidNonce, # + '40015': ExchangeError, # API function do not exist + '40016': OnMaintenance, # {"result":false,"error":"Error 40016: Maintenance work in progress"} + '40017': AuthenticationError, # Wrong API Key + '40032': PermissionDenied, # {"result":false,"error":"Error 40032: Access is denied for self API key"} + '40033': PermissionDenied, # {"result":false,"error":"Error 40033: Access is denied, self resources are temporarily blocked to user"} + '40034': RateLimitExceeded, # {"result":false,"error":"Error 40034: Access is denied, rate limit is exceeded"} + '50052': InsufficientFunds, + '50054': InsufficientFunds, + '50304': OrderNotFound, # "Order was not found '123456789'"(fetching order trades for an order that does not have trades yet) + '50173': OrderNotFound, # "Order with id X was not found."(cancelling non-existent, closed and cancelled order) + '50277': InvalidOrder, + '50319': InvalidOrder, # Price by order is less than permissible minimum for self pair + '50321': InvalidOrder, # Price by order is more than permissible maximum for self pair + '50381': InvalidOrder, # {"result":false,"error":"Error 50381: More than 2 decimal places are not permitted for pair BTC_USD"} + }, + 'broad': { + 'range period is too long': BadRequest, + 'invalid syntax': BadRequest, + 'API rate limit exceeded': RateLimitExceeded, # {"result":false,"error":"API rate limit exceeded for x.x.x.x. Retry after 60 sec.","history":[],"begin":1579392000,"end":1579478400} + }, + }, + }) + + def modify_margin_helper(self, symbol: str, amount, type, params={}): + self.load_markets() + market = self.market(symbol) + request: dict = { + 'position_id': market['id'], + 'quantity': amount, + } + response = None + if type == 'add': + response = self.privatePostMarginUserPositionMarginAdd(self.extend(request, params)) + elif type == 'reduce': + response = self.privatePostMarginUserPositionMarginRemove(self.extend(request, params)) + # + # {} + # + margin = self.parse_margin_modification(response, market) + options = self.safe_value(self.options, 'margin', {}) + fillResponseFromRequest = self.safe_bool(options, 'fillResponseFromRequest', True) + if fillResponseFromRequest: + margin['type'] = type + margin['amount'] = amount + return margin + + def parse_margin_modification(self, data: dict, market: Market = None) -> MarginModification: + # + # {} + # + return { + 'info': data, + 'symbol': self.safe_symbol(None, market), + 'type': None, + 'marginMode': 'isolated', + 'amount': None, + 'total': None, + 'code': self.safe_value(market, 'quote'), + 'status': 'ok', + 'timestamp': None, + 'datetime': None, + } + + def reduce_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + remove margin from a position + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#eebf9f25-0289-4946-9482-89872c738449 + + :param str symbol: unified market symbol + :param float amount: the amount of margin to remove + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin structure ` + """ + return self.modify_margin_helper(symbol, amount, 'reduce', params) + + def add_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + add margin + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#143ef808-79ca-4e49-9e79-a60ea4d8c0e3 + + :param str symbol: unified market symbol + :param float amount: amount of margin to add + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin structure ` + """ + return self.modify_margin_helper(symbol, amount, 'add', params) + + def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#90927062-256c-4b03-900f-2b99131f9a54 + https://documenter.getpostman.com/view/10287440/SzYXWKPi#7de7e75c-5833-45a8-b937-c2276d235aaa + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + options = self.safe_value(self.options, 'fetchTradingFees', {}) + defaultMethod = self.safe_string(options, 'method', 'fetchPrivateTradingFees') + method = self.safe_string(params, 'method', defaultMethod) + params = self.omit(params, 'method') + if method == 'fetchPrivateTradingFees': + return self.fetch_private_trading_fees(params) + else: + return self.fetch_public_trading_fees(params) + + def fetch_private_trading_fees(self, params={}): + self.load_markets() + response = self.privatePostMarginPairList(params) + # + # { + # "pairs": [{ + # "name": "EXM_USD", + # "buy_price": "0.02728391", + # "sell_price": "0.0276", + # "last_trade_price": "0.0276", + # "ticker_updated": "1646956050056696046", + # "is_fair_price": True, + # "max_price_precision": "8", + # "min_order_quantity": "1", + # "max_order_quantity": "50000", + # "min_order_price": "0.00000001", + # "max_order_price": "1000", + # "max_position_quantity": "50000", + # "trade_taker_fee": "0.05", + # "trade_maker_fee": "0", + # "liquidation_fee": "0.5", + # "max_leverage": "3", + # "default_leverage": "3", + # "liquidation_level": "5", + # "margin_call_level": "7.5", + # "position": "1", + # "updated": "1638976144797807397" + # } + # ... + # ] + # } + # + pairs = self.safe_value(response, 'pairs', []) + result: dict = {} + for i in range(0, len(pairs)): + pair = pairs[i] + marketId = self.safe_string(pair, 'name') + symbol = self.safe_symbol(marketId, None, '_') + makerString = self.safe_string(pair, 'trade_maker_fee') + takerString = self.safe_string(pair, 'trade_taker_fee') + maker = self.parse_number(Precise.string_div(makerString, '100')) + taker = self.parse_number(Precise.string_div(takerString, '100')) + result[symbol] = { + 'info': pair, + 'symbol': symbol, + 'maker': maker, + 'taker': taker, + 'percentage': True, + 'tierBased': True, + } + return result + + def fetch_public_trading_fees(self, params={}): + self.load_markets() + response = self.publicGetPairSettings(params) + # + # { + # "BTC_USD": { + # "min_quantity": "0.00002", + # "max_quantity": "1000", + # "min_price": "1", + # "max_price": "150000", + # "max_amount": "500000", + # "min_amount": "1", + # "price_precision": "2", + # "commission_taker_percent": "0.3", + # "commission_maker_percent": "0.3" + # }, + # } + # + result: dict = {} + for i in range(0, len(self.symbols)): + symbol = self.symbols[i] + market = self.market(symbol) + fee = self.safe_value(response, market['id'], {}) + makerString = self.safe_string(fee, 'commission_maker_percent') + takerString = self.safe_string(fee, 'commission_taker_percent') + maker = self.parse_number(Precise.string_div(makerString, '100')) + taker = self.parse_number(Precise.string_div(takerString, '100')) + result[symbol] = { + 'info': fee, + 'symbol': symbol, + 'maker': maker, + 'taker': taker, + 'percentage': True, + 'tierBased': True, + } + return result + + def parse_fixed_float_value(self, input): + if (input is None) or (input == '-'): + return None + if input == '': + return 0 + isPercentage = (input.find('%') >= 0) + parts = input.split(' ') + value = parts[0].replace('%', '') + result = float(value) + if (result > 0) and isPercentage: + raise ExchangeError(self.id + ' parseFixedFloatValue() detected an unsupported non-zero percentage-based fee ' + input) + return result + + def fetch_transaction_fees(self, codes: Strings = None, params={}): + """ + @deprecated + please use fetchDepositWithdrawFees instead + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#4190035d-24b1-453d-833b-37e0a52f88e2 + + :param str[]|None codes: list of unified currency codes + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `transaction fees structures ` + """ + self.load_markets() + cryptoList = self.publicGetPaymentsProvidersCryptoList(params) + # + # { + # "BTC":[ + # {"type":"deposit", "name":"BTC", "currency_name":"BTC", "min":"0.001", "max":"0", "enabled":true,"comment":"Minimum deposit amount is 0.001 BTC. We do not support BSC and BEP20 network, please consider self when sending funds", "commission_desc":"0%", "currency_confirmations":1}, + # {"type":"withdraw", "name":"BTC", "currency_name":"BTC", "min":"0.001", "max":"350", "enabled":true,"comment":"Do not withdraw directly to the Crowdfunding or ICO address account will not be credited with tokens from such sales.", "commission_desc":"0.0005 BTC", "currency_confirmations":6} + # ], + # "ETH":[ + # {"type":"withdraw", "name":"ETH", "currency_name":"ETH", "min":"0.01", "max":"500", "enabled":true,"comment":"Do not withdraw directly to the Crowdfunding or ICO address account will not be credited with tokens from such sales.", "commission_desc":"0.004 ETH", "currency_confirmations":4}, + # {"type":"deposit", "name":"ETH", "currency_name":"ETH", "min":"0.01", "max":"0", "enabled":true,"comment":"Minimum deposit amount is 0.01 ETH. We do not support BSC and BEP20 network, please consider self when sending funds", "commission_desc":"0%", "currency_confirmations":1} + # ], + # "USDT":[ + # {"type":"deposit", "name":"USDT(OMNI)", "currency_name":"USDT", "min":"10", "max":"0", "enabled":false,"comment":"Minimum deposit amount is 10 USDT", "commission_desc":"0%", "currency_confirmations":2}, + # {"type":"withdraw", "name":"USDT(OMNI)", "currency_name":"USDT", "min":"10", "max":"100000", "enabled":false,"comment":"Do not withdraw directly to the Crowdfunding or ICO address account will not be credited with tokens from such sales.", "commission_desc":"5 USDT", "currency_confirmations":6}, + # {"type":"deposit", "name":"USDT(ERC20)", "currency_name":"USDT", "min":"10", "max":"0", "enabled":true,"comment":"Minimum deposit amount is 10 USDT", "commission_desc":"0%", "currency_confirmations":2}, + # { + # "type":"withdraw", + # "name":"USDT(ERC20)", + # "currency_name":"USDT", + # "min":"55", + # "max":"200000", + # "enabled":true, + # "comment":"Caution! Do not withdraw directly to a crowdfund or ICO address, account will not be credited with tokens from such sales. Recommendation: Due to the high load of ERC20 network, using TRC20 address for withdrawal is recommended.", + # "commission_desc":"10 USDT", + # "currency_confirmations":6 + # }, + # {"type":"deposit", "name":"USDT(TRC20)", "currency_name":"USDT", "min":"10", "max":"100000", "enabled":true,"comment":"Minimum deposit amount is 10 USDT. Only TRON main network supported", "commission_desc":"0%", "currency_confirmations":2}, + # {"type":"withdraw", "name":"USDT(TRC20)", "currency_name":"USDT", "min":"10", "max":"150000", "enabled":true,"comment":"Caution! Do not withdraw directly to a crowdfund or ICO address, account will not be credited with tokens from such sales. Only TRON main network supported.", "commission_desc":"1 USDT", "currency_confirmations":6} + # ], + # "XLM":[ + # {"type":"deposit", "name":"XLM", "currency_name":"XLM", "min":"1", "max":"1000000", "enabled":true,"comment":"Attention! A deposit without memo(invoice) will not be credited. Minimum deposit amount is 1 XLM. We do not support BSC and BEP20 network, please consider self when sending funds", "commission_desc":"0%", "currency_confirmations":1}, + # {"type":"withdraw", "name":"XLM", "currency_name":"XLM", "min":"21", "max":"1000000", "enabled":true,"comment":"Caution! Do not withdraw directly to a crowdfund or ICO address, account will not be credited with tokens from such sales.", "commission_desc":"0.01 XLM", "currency_confirmations":1} + # ], + # } + # + result: dict = {} + cryptoListKeys = list(cryptoList.keys()) + for i in range(0, len(cryptoListKeys)): + code = cryptoListKeys[i] + if codes is not None and not self.in_array(code, codes): + continue + result[code] = { + 'deposit': None, + 'withdraw': None, + } + currency = self.currency(code) + currencyId = self.safe_string(currency, 'id') + providers = self.safe_value(cryptoList, currencyId, []) + for j in range(0, len(providers)): + provider = providers[j] + typeInner = self.safe_string(provider, 'type') + commissionDesc = self.safe_string(provider, 'commission_desc') + fee = self.parse_fixed_float_value(commissionDesc) + result[code][typeInner] = fee + result[code]['info'] = providers + # cache them for later use + self.options['transactionFees'] = result + return result + + def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}): + """ + fetch deposit and withdraw fees + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#4190035d-24b1-453d-833b-37e0a52f88e2 + + :param str[]|None codes: list of unified currency codes + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `transaction fees structures ` + """ + self.load_markets() + response = self.publicGetPaymentsProvidersCryptoList(params) + # + # { + # "USDT": [ + # { + # "type": "deposit", # or "withdraw" + # "name": "USDT(ERC20)", + # "currency_name": "USDT", + # "min": "10", + # "max": "0", + # "enabled": True, + # "comment": "Minimum deposit amount is 10 USDT", + # "commission_desc": "0%", + # "currency_confirmations": 2 + # }, + # ... + # ], + # ... + # } + # + result = self.parse_deposit_withdraw_fees(response, codes) + # cache them for later use + self.options['transactionFees'] = result + return result + + def parse_deposit_withdraw_fee(self, fee, currency: Currency = None): + # + # [ + # { + # "type": "deposit", # or "withdraw" + # "name": "BTC", + # "currency_name": "BTC", + # "min": "0.001", + # "max": "0", + # "enabled": True, + # "comment": "Minimum deposit amount is 0.001 BTC. We do not support BSC and BEP20 network, please consider self when sending funds", + # "commission_desc": "0%", + # "currency_confirmations": 1 + # }, + # ... + # ] + # + result = self.deposit_withdraw_fee(fee) + for i in range(0, len(fee)): + provider = fee[i] + type = self.safe_string(provider, 'type') + networkId = self.safe_string(provider, 'name') + networkCode = self.network_id_to_code(networkId, self.safe_string(currency, 'code')) + commissionDesc = self.safe_string(provider, 'commission_desc') + splitCommissionDesc = [] + percentage = None + if commissionDesc is not None: + splitCommissionDesc = commissionDesc.split('%') + splitCommissionDescLength = len(splitCommissionDesc) + percentage = splitCommissionDescLength >= 2 + network = self.safe_value(result['networks'], networkCode) + if network is None: + result['networks'][networkCode] = { + 'withdraw': { + 'fee': None, + 'percentage': None, + }, + 'deposit': { + 'fee': None, + 'percentage': None, + }, + } + result['networks'][networkCode][type] = { + 'fee': self.parse_fixed_float_value(self.safe_string(splitCommissionDesc, 0)), + 'percentage': percentage, + } + return self.assign_default_deposit_withdraw_fees(result) + + def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#7cdf0ca8-9ff6-4cf3-aa33-bcec83155c49 + https://documenter.getpostman.com/view/10287440/SzYXWKPi#4190035d-24b1-453d-833b-37e0a52f88e2 + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + promises = [] + # + promises.append(self.publicGetCurrencyListExtended(params)) + # + # [ + # {"name":"VLX","description":"Velas"}, + # {"name":"RUB","description":"Russian Ruble"}, + # {"name":"BTC","description":"Bitcoin"}, + # {"name":"USD","description":"US Dollar"} + # ] + # + promises.append(self.publicGetPaymentsProvidersCryptoList(params)) + # + # { + # "BTC":[ + # {"type":"deposit", "name":"BTC", "currency_name":"BTC", "min":"0.001", "max":"0", "enabled":true,"comment":"Minimum deposit amount is 0.001 BTC. We do not support BSC and BEP20 network, please consider self when sending funds", "commission_desc":"0%", "currency_confirmations":1}, + # {"type":"withdraw", "name":"BTC", "currency_name":"BTC", "min":"0.001", "max":"350", "enabled":true,"comment":"Do not withdraw directly to the Crowdfunding or ICO address account will not be credited with tokens from such sales.", "commission_desc":"0.0005 BTC", "currency_confirmations":6} + # ], + # "ETH":[ + # {"type":"withdraw", "name":"ETH", "currency_name":"ETH", "min":"0.01", "max":"500", "enabled":true,"comment":"Do not withdraw directly to the Crowdfunding or ICO address account will not be credited with tokens from such sales.", "commission_desc":"0.004 ETH", "currency_confirmations":4}, + # {"type":"deposit", "name":"ETH", "currency_name":"ETH", "min":"0.01", "max":"0", "enabled":true,"comment":"Minimum deposit amount is 0.01 ETH. We do not support BSC and BEP20 network, please consider self when sending funds", "commission_desc":"0%", "currency_confirmations":1} + # ], + # "USDT":[ + # {"type":"deposit", "name":"USDT(OMNI)", "currency_name":"USDT", "min":"10", "max":"0", "enabled":false,"comment":"Minimum deposit amount is 10 USDT", "commission_desc":"0%", "currency_confirmations":2}, + # {"type":"withdraw", "name":"USDT(OMNI)", "currency_name":"USDT", "min":"10", "max":"100000", "enabled":false,"comment":"Do not withdraw directly to the Crowdfunding or ICO address account will not be credited with tokens from such sales.", "commission_desc":"5 USDT", "currency_confirmations":6}, + # {"type":"deposit", "name":"USDT(ERC20)", "currency_name":"USDT", "min":"10", "max":"0", "enabled":true,"comment":"Minimum deposit amount is 10 USDT", "commission_desc":"0%", "currency_confirmations":2}, + # {"type":"withdraw", "name":"USDT(ERC20)", "currency_name":"USDT", "min":"55", "max":"200000", "enabled":true, "comment":"Caution! Do not withdraw directly to a crowdfund or ICO address, account will not be credited with tokens from such sales. Recommendation: Due to the high load of ERC20 network, using TRC20 address for withdrawal is recommended.", "commission_desc":"10 USDT", "currency_confirmations":6}, + # {"type":"deposit", "name":"USDT(TRC20)", "currency_name":"USDT", "min":"10", "max":"100000", "enabled":true,"comment":"Minimum deposit amount is 10 USDT. Only TRON main network supported", "commission_desc":"0%", "currency_confirmations":2}, + # {"type":"withdraw", "name":"USDT(TRC20)", "currency_name":"USDT", "min":"10", "max":"150000", "enabled":true,"comment":"Caution! Do not withdraw directly to a crowdfund or ICO address, account will not be credited with tokens from such sales. Only TRON main network supported.", "commission_desc":"1 USDT", "currency_confirmations":6} + # ], + # "XLM":[ + # {"type":"deposit", "name":"XLM", "currency_name":"XLM", "min":"1", "max":"1000000", "enabled":true,"comment":"Attention! A deposit without memo(invoice) will not be credited. Minimum deposit amount is 1 XLM. We do not support BSC and BEP20 network, please consider self when sending funds", "commission_desc":"0%", "currency_confirmations":1}, + # {"type":"withdraw", "name":"XLM", "currency_name":"XLM", "min":"21", "max":"1000000", "enabled":true,"comment":"Caution! Do not withdraw directly to a crowdfund or ICO address, account will not be credited with tokens from such sales.", "commission_desc":"0.01 XLM", "currency_confirmations":1} + # ], + # } + # + responses = promises + currencyList = responses[0] + cryptoList = responses[1] + result: dict = {} + for i in range(0, len(currencyList)): + currency = currencyList[i] + currencyId = self.safe_string(currency, 'name') + code = self.safe_currency_code(currencyId) + type = 'crypto' + networks = {} + providers = self.safe_list(cryptoList, currencyId) + if providers is None: + type = 'fiat' + else: + for j in range(0, len(providers)): + provider = providers[j] + name = self.safe_string(provider, 'name') + # get network-id by removing extra things + networkId = name.replace(currencyId + ' ', '') + networkId = networkId.replace('(', '') + replaceChar = ')' # transpiler trick + networkId = networkId.replace(replaceChar, '') + networkCode = self.network_id_to_code(networkId) + if not (networkCode in networks): + networks[networkCode] = { + 'id': networkId, + 'network': networkCode, + 'active': None, + 'deposit': None, + 'withdraw': None, + 'fee': None, + 'limits': { + 'withdraw': { + 'min': None, + 'max': None, + }, + 'deposit': { + 'min': None, + 'max': None, + }, + }, + 'info': [], # set, because of multiple network sub-entries + } + typeInner = self.safe_string(provider, 'type') + minValue = self.safe_string(provider, 'min') + maxValue = self.safe_string(provider, 'max') + activeProvider = self.safe_bool(provider, 'enabled') + networkEntry = networks[networkCode] + if typeInner == 'deposit': + networkEntry['deposit'] = activeProvider + networkEntry['limits']['deposit']['min'] = minValue + networkEntry['limits']['deposit']['max'] = maxValue + elif typeInner == 'withdraw': + networkEntry['withdraw'] = activeProvider + networkEntry['limits']['withdraw']['min'] = minValue + networkEntry['limits']['withdraw']['max'] = maxValue + info = self.safe_list(networkEntry, 'info') + info.append(provider) + networkEntry['info'] = info + networks[networkCode] = networkEntry + result[code] = self.safe_currency_structure({ + 'id': currencyId, + 'code': code, + 'name': self.safe_string(currency, 'description'), + 'type': type, + 'active': None, + 'deposit': None, + 'withdraw': None, + 'fee': None, + 'precision': self.parse_number('1e-8'), + 'limits': { + 'withdraw': { + 'min': None, + 'max': None, + }, + 'deposit': { + 'min': None, + 'max': None, + }, + }, + 'info': { + 'currency': currency, + 'providers': providers, + }, + 'networks': networks, + }) + return result + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for exmo + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#7de7e75c-5833-45a8-b937-c2276d235aaa + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + promises = [] + promises.append(self.publicGetPairSettings(params)) + # + # { + # "BTC_USD":{ + # "min_quantity":"0.0001", + # "max_quantity":"1000", + # "min_price":"1", + # "max_price":"30000", + # "max_amount":"500000", + # "min_amount":"1", + # "price_precision":8, + # "commission_taker_percent":"0.4", + # "commission_maker_percent":"0.4" + # }, + # } + # + marginPairsDict: dict = {} + fetchMargin = self.check_required_credentials(False) + if fetchMargin: + promises.append(self.privatePostMarginPairList(params)) + # + # { + # "pairs": [ + # { + # "buy_price": "55978.85", + # "default_leverage": "3", + # "is_fair_price": True, + # "last_trade_price": "55999.23", + # "liquidation_fee": "2", + # "liquidation_level": "10", + # "margin_call_level": "15", + # "max_leverage": "3", + # "max_order_price": "150000", + # "max_order_quantity": "1", + # "max_position_quantity": "1", + # "max_price_precision": 2, + # "min_order_price": "1", + # "min_order_quantity": "0.00002", + # "name": "BTC_USD", + # "position": 1, + # "sell_price": "55985.51", + # "ticker_updated": "1619019818936107989", + # "trade_maker_fee": "0", + # "trade_taker_fee": "0.05", + # "updated": "1619008608955599013" + # } + # ] + # } + # + responses = promises + spotResponse = responses[0] + if fetchMargin: + marginPairs = responses[1] + pairs = self.safe_list(marginPairs, 'pairs') + marginPairsDict = self.index_by(pairs, 'name') + keys = list(spotResponse.keys()) + result = [] + for i in range(0, len(keys)): + id = keys[i] + market = spotResponse[id] + marginMarket = self.safe_dict(marginPairsDict, id) + symbol = id.replace('_', '/') + baseId, quoteId = symbol.split('/') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + takerString = self.safe_string(market, 'commission_taker_percent') + makerString = self.safe_string(market, 'commission_maker_percent') + maxQuantity = self.safe_string(market, 'max_quantity') + marginMaxQuantity = self.safe_string(marginMarket, 'max_order_quantity') + result.append({ + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': marginMarket is not None, + 'swap': False, + 'future': False, + 'option': False, + 'active': None, + 'contract': False, + 'linear': None, + 'inverse': None, + 'taker': self.parse_number(Precise.string_div(takerString, '100')), + 'maker': self.parse_number(Precise.string_div(makerString, '100')), + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number('1e-8'), + 'price': self.parse_number(self.parse_precision(self.safe_string(market, 'price_precision'))), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': self.safe_number(market, 'leverage'), + }, + 'amount': { + 'min': self.safe_number(market, 'min_quantity'), + 'max': self.parse_number(Precise.string_max(maxQuantity, marginMaxQuantity)), + }, + 'price': { + 'min': self.safe_number(market, 'min_price'), + 'max': self.safe_number(market, 'max_price'), + }, + 'cost': { + 'min': self.safe_number(market, 'min_amount'), + 'max': self.safe_number(market, 'max_amount'), + }, + }, + 'created': None, + 'info': market, + }) + return result + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#65eeb949-74e5-4631-9184-c38387fe53e8 + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest candle to fetch + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + market = self.market(symbol) + until = self.safe_integer_product(params, 'until', 0.001) + untilIsDefined = (until is not None) + request: dict = { + 'symbol': market['id'], + 'resolution': self.safe_string(self.timeframes, timeframe, timeframe), + } + maxLimit = 3000 + duration = self.parse_timeframe(timeframe) + now = self.parse_to_int(self.milliseconds() / 1000) + if since is None: + to = min(until, now) if untilIsDefined else now + if limit is None: + limit = 1000 # cap default at generous amount + else: + limit = min(limit, maxLimit) + request['from'] = to - (limit * duration) - 1 + request['to'] = to + else: + request['from'] = self.parse_to_int(since / 1000) + if untilIsDefined: + request['to'] = min(until, now) + else: + if limit is None: + limit = maxLimit + else: + limit = min(limit, maxLimit) + to = self.sum(since, limit * duration) + request['to'] = min(to, now) + params = self.omit(params, 'until') + response = self.publicGetCandlesHistory(self.extend(request, params)) + # + # { + # "candles":[ + # {"t":1584057600000,"o":0.02235144,"c":0.02400233,"h":0.025171,"l":0.02221,"v":5988.34031761}, + # {"t":1584144000000,"o":0.0240373,"c":0.02367413,"h":0.024399,"l":0.0235,"v":2027.82522329}, + # {"t":1584230400000,"o":0.02363458,"c":0.02319242,"h":0.0237948,"l":0.02223196,"v":1707.96944997}, + # ] + # } + # + candles = self.safe_list(response, 'candles', []) + return self.parse_ohlcvs(candles, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # { + # "t":1584057600000, + # "o":0.02235144, + # "c":0.02400233, + # "h":0.025171, + # "l":0.02221, + # "v":5988.34031761 + # } + # + return [ + self.safe_integer(ohlcv, 't'), + self.safe_number(ohlcv, 'o'), + self.safe_number(ohlcv, 'h'), + self.safe_number(ohlcv, 'l'), + self.safe_number(ohlcv, 'c'), + self.safe_number(ohlcv, 'v'), + ] + + def parse_balance(self, response) -> Balances: + result: dict = {'info': response} + wallets = self.safe_value(response, 'wallets') + if wallets is not None: + currencyIds = list(wallets.keys()) + for i in range(0, len(currencyIds)): + currencyId = currencyIds[i] + item = wallets[currencyId] + currency = self.safe_currency_code(currencyId) + account = self.account() + account['used'] = self.safe_string(item, 'used') + account['free'] = self.safe_string(item, 'free') + account['total'] = self.safe_string(item, 'balance') + result[currency] = account + else: + free = self.safe_value(response, 'balances', {}) + used = self.safe_value(response, 'reserved', {}) + currencyIds = list(free.keys()) + for i in range(0, len(currencyIds)): + currencyId = currencyIds[i] + code = self.safe_currency_code(currencyId) + account = self.account() + if currencyId in free: + account['free'] = self.safe_string(free, currencyId) + if currencyId in used: + account['used'] = self.safe_string(used, currencyId) + result[code] = account + return self.safe_balance(result) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#59c5160f-27a1-4d9a-8cfb-7979c7ffaac6 + https://documenter.getpostman.com/view/10287440/SzYXWKPi#c8388df7-1f9f-4d41-81c4-5a387d171dc6 + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: *isolated* fetches the isolated margin balance + :returns dict: a `balance structure ` + """ + self.load_markets() + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchBalance', params) + if marginMode == 'cross': + raise BadRequest(self.id + ' does not support cross margin') + response = None + if marginMode == 'isolated': + response = self.privatePostMarginUserWalletList(params) + # + # { + # "wallets": { + # "USD": { + # "balance": "1000", + # "free": "600", + # "used": "400" + # } + # } + # } + # + else: + response = self.privatePostUserInfo(params) + # + # { + # "uid":131685, + # "server_date":1628999600, + # "balances":{ + # "EXM":"0", + # "USD":"0", + # "EUR":"0", + # "GBP":"0", + # }, + # } + # + return self.parse_balance(response) + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#c60c51a8-e683-4f45-a000-820723d37871 + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + if limit is not None: + request['limit'] = limit + response = self.publicGetOrderBook(self.extend(request, params)) + result = self.safe_dict(response, market['id']) + return self.parse_order_book(result, market['symbol'], None, 'bid', 'ask') + + def fetch_order_books(self, symbols: Strings = None, limit: Int = None, params={}) -> OrderBooks: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data for multiple markets + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#c60c51a8-e683-4f45-a000-820723d37871 + + :param str[]|None symbols: list of unified market symbols, all symbols fetched if None, default is None + :param int [limit]: max number of entries per orderbook to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `order book structures ` indexed by market symbol + """ + self.load_markets() + ids = None + if symbols is None: + ids = ','.join(self.ids) + # max URL length is 2083 symbols, including http schema, hostname, tld, etc... + if len(ids) > 2048: + numIds = len(self.ids) + raise ExchangeError(self.id + ' fetchOrderBooks() has ' + str(numIds) + ' symbols exceeding max URL length, you are required to specify a list of symbols in the first argument to fetchOrderBooks') + else: + ids = self.market_ids(symbols) + ids = ','.join(ids) + request: dict = { + 'pair': ids, + } + if limit is not None: + request['limit'] = limit + response = self.publicGetOrderBook(self.extend(request, params)) + result: dict = {} + marketIds = list(response.keys()) + for i in range(0, len(marketIds)): + marketId = marketIds[i] + symbol = self.safe_symbol(marketId) + result[symbol] = self.parse_order_book(response[marketId], symbol, None, 'bid', 'ask') + return result + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "buy_price":"0.00002996", + # "sell_price":"0.00003002", + # "last_trade":"0.00002992", + # "high":"0.00003028", + # "low":"0.00002935", + # "avg":"0.00002963", + # "vol":"1196546.3163222", + # "vol_curr":"35.80066578", + # "updated":1642291733 + # } + # + timestamp = self.safe_timestamp(ticker, 'updated') + market = self.safe_market(None, market) + last = self.safe_string(ticker, 'last_trade') + return self.safe_ticker({ + 'symbol': market['symbol'], + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': self.safe_string(ticker, 'buy_price'), + 'bidVolume': None, + 'ask': self.safe_string(ticker, 'sell_price'), + 'askVolume': None, + 'vwap': None, + 'open': None, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': self.safe_string(ticker, 'avg'), + 'baseVolume': self.safe_string(ticker, 'vol'), + 'quoteVolume': self.safe_string(ticker, 'vol_curr'), + 'info': ticker, + }, market) + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#4c8e6459-3503-4361-b012-c34bb9f7e385 + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + symbols = self.market_symbols(symbols) + response = self.publicGetTicker(params) + # + # { + # "ADA_BTC":{ + # "buy_price":"0.00002996", + # "sell_price":"0.00003002", + # "last_trade":"0.00002992", + # "high":"0.00003028", + # "low":"0.00002935", + # "avg":"0.00002963", + # "vol":"1196546.3163222", + # "vol_curr":"35.80066578", + # "updated":1642291733 + # } + # } + # + result: dict = {} + marketIds = list(response.keys()) + for i in range(0, len(marketIds)): + marketId = marketIds[i] + market = self.safe_market(marketId, None, '_') + symbol = market['symbol'] + ticker = self.safe_value(response, marketId) + result[symbol] = self.parse_ticker(ticker, market) + return self.filter_by_array_tickers(result, 'symbol', symbols) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#4c8e6459-3503-4361-b012-c34bb9f7e385 + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + response = self.publicGetTicker(params) + market = self.market(symbol) + return self.parse_ticker(response[market['id']], market) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(public) + # + # { + # "trade_id":165087520, + # "date":1587470005, + # "type":"buy", + # "quantity":"1.004", + # "price":"0.02491461", + # "amount":"0.02501426" + # }, + # + # fetchMyTrades, fetchOrderTrades + # + # { + # "trade_id": 3, + # "date": 1435488248, + # "type": "buy", + # "pair": "BTC_USD", + # "order_id": 12345, + # "quantity": 1, + # "price": 100, + # "amount": 100, + # "exec_type": "taker", + # "commission_amount": "0.02", + # "commission_currency": "BTC", + # "commission_percent": "0.2" + # } + # + # fetchMyTrades(margin) + # + # { + # "trade_id": "692861757015952517", + # "trade_dt": "1693951853197811824", + # "trade_type": "buy", + # "pair": "ADA_USDT", + # "quantity": "1.96607879", + # "price": "0.2568", + # "amount": "0.50488903" + # } + # + timestamp = self.safe_timestamp(trade, 'date') + id = self.safe_string(trade, 'trade_id') + orderId = self.safe_string(trade, 'order_id') + priceString = self.safe_string(trade, 'price') + amountString = self.safe_string(trade, 'quantity') + costString = self.safe_string(trade, 'amount') + side = self.safe_string_2(trade, 'type', 'trade_type') + type = None + marketId = self.safe_string(trade, 'pair') + market = self.safe_market(marketId, market, '_') + symbol = market['symbol'] + isMaker = self.safe_value(trade, 'is_maker') + takerOrMakerDefault = None + if isMaker is not None: + takerOrMakerDefault = 'maker' if isMaker else 'taker' + takerOrMaker = self.safe_string(trade, 'exec_type', takerOrMakerDefault) + fee = None + feeCostString = self.safe_string(trade, 'commission_amount') + if feeCostString is not None: + feeCurrencyId = self.safe_string(trade, 'commission_currency') + feeCurrencyCode = self.safe_currency_code(feeCurrencyId) + feeRateString = self.safe_string(trade, 'commission_percent') + if feeRateString is not None: + feeRateString = Precise.string_div(feeRateString, '1000', 18) + fee = { + 'cost': feeCostString, + 'currency': feeCurrencyCode, + 'rate': feeRateString, + } + return self.safe_trade({ + 'id': id, + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'order': orderId, + 'type': type, + 'side': side, + 'takerOrMaker': takerOrMaker, + 'price': priceString, + 'amount': amountString, + 'cost': costString, + 'fee': fee, + }, market) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#5a5a9c0d-cf17-47f6-9d62-6d4404ebd5ac + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + response = self.publicGetTrades(self.extend(request, params)) + # + # { + # "ETH_BTC":[ + # { + # "trade_id":165087520, + # "date":1587470005, + # "type":"buy", + # "quantity":"1.004", + # "price":"0.02491461", + # "amount":"0.02501426" + # }, + # { + # "trade_id":165087369, + # "date":1587469938, + # "type":"buy", + # "quantity":"0.94", + # "price":"0.02492348", + # "amount":"0.02342807" + # } + # ] + # } + # + data = self.safe_list(response, market['id'], []) + return self.parse_trades(data, market, since, limit) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#b8d8d9af-4f46-46a1-939b-ad261d79f452 # spot + https://documenter.getpostman.com/view/10287440/SzYXWKPi#f4b1aaf8-399f-403b-ab5e-4926d967a106 # margin + + :param str symbol: a symbol is required but it can be a single string, or a non-empty array + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: *required for margin orders* the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + + EXCHANGE SPECIFIC PARAMETERS + :param int [params.offset]: last deal offset, default = 0 + :returns Trade[]: a list of `trade structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMyTrades() requires a symbol argument') + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchMyTrades', params) + if marginMode == 'cross': + raise BadRequest(self.id + ' only isolated margin is supported') + self.load_markets() + market = self.market(symbol) + pair = market['id'] + isSpot = marginMode != 'isolated' + if limit is None: + limit = 100 + request: dict = {} + if isSpot: + request['pair'] = pair + else: + request['pair_name'] = pair + if limit is not None: + request['limit'] = limit + offset = self.safe_integer(params, 'offset', 0) + request['offset'] = offset + response = None + if isSpot: + response = self.privatePostUserTrades(self.extend(request, params)) + # + # { + # "BTC_USD": [ + # { + # "trade_id": 20056872, + # "client_id": 100500, + # "date": 1435488248, + # "type": "buy", + # "pair": "BTC_USD", + # "quantity": "1", + # "price": "100", + # "amount": "100", + # "order_id": 7, + # "parent_order_id": 117684023830293, + # "exec_type": "taker", + # "commission_amount": "0.02", + # "commission_currency": "BTC", + # "commission_percent": "0.2" + # } + # ], + # ... + # } + # + else: + responseFromExchange = self.privatePostMarginTrades(self.extend(request, params)) + # + # { + # "trades": { + # "ADA_USDT": [ + # { + # "trade_id": "692861757015952517", + # "trade_dt": "1693951853197811824", + # "trade_type": "buy", + # "pair": "ADA_USDT", + # "quantity": "1.96607879", + # "price": "0.2568", + # "amount": "0.50488903" + # }, + # ] + # ... + # } + # } + # + response = self.safe_value(responseFromExchange, 'trades') + result = [] + marketIdsInner = list(response.keys()) + for i in range(0, len(marketIdsInner)): + marketId = marketIdsInner[i] + resultMarket = self.safe_market(marketId, None, '_') + items = response[marketId] + trades = self.parse_trades(items, resultMarket, since, limit) + result = self.array_concat(result, trades) + return self.filter_by_since_limit(result, since, limit) + + def create_market_order_with_cost(self, symbol: str, side: OrderSide, cost: float, params={}): + """ + create a market order by providing the symbol, side and cost + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#80daa469-ec59-4d0a-b229-6a311d8dd1cd + + :param str symbol: unified symbol of the market to create an order in + :param str side: 'buy' or 'sell' + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + params = self.extend(params, {'cost': cost}) + return self.create_order(symbol, 'market', side, cost, None, params) + + def create_market_buy_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market buy order by providing the symbol and cost + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#80daa469-ec59-4d0a-b229-6a311d8dd1cd + + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + params = self.extend(params, {'cost': cost}) + return self.create_order(symbol, 'market', 'buy', cost, None, params) + + def create_market_sell_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market sell order by providing the symbol and cost + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#80daa469-ec59-4d0a-b229-6a311d8dd1cd + + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + params = self.extend(params, {'cost': cost}) + return self.create_order(symbol, 'market', 'sell', cost, None, params) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#80daa469-ec59-4d0a-b229-6a311d8dd1cd + https://documenter.getpostman.com/view/10287440/SzYXWKPi#de6f4321-eeac-468c-87f7-c4ad7062e265 # stop market + https://documenter.getpostman.com/view/10287440/SzYXWKPi#3561b86c-9ff1-436e-8e68-ac926b7eb523 # margin + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: the price at which a trigger order is triggered at + :param str [params.timeInForce]: *spot only* 'fok', 'ioc' or 'post_only' + :param boolean [params.postOnly]: *spot only* True for post only orders + :param float [params.cost]: *spot only* *market orders only* the cost of the order in the quote currency for market orders + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + isMarket = (type == 'market') and (price is None) + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('createOrder', params) + if marginMode == 'cross': + raise BadRequest(self.id + ' only supports isolated margin') + isSpot = (marginMode != 'isolated') + triggerPrice = self.safe_string_n(params, ['triggerPrice', 'stopPrice', 'stop_price']) + cost = self.safe_string(params, 'cost') + request: dict = { + 'pair': market['id'], + # 'leverage': 2, + # 'quantity': self.amount_to_precision(market['symbol'], amount), + # spot - buy, sell, market_buy, market_sell, market_buy_total, market_sell_total + # margin - limit_buy, limit_sell, market_buy, market_sell, stop_buy, stop_sell, stop_limit_buy, stop_limit_sell, trailing_stop_buy, trailing_stop_sell + # 'stop_price': self.price_to_precision(symbol, stopPrice), + # 'distance': 0, # distance for trailing stop orders + # 'expire': 0, # expiration timestamp in UTC timezone for the order, unless expire is 0 + # 'client_id': 123, # optional, must be a positive integer + # 'comment': '', # up to 50 latin symbols, whitespaces, underscores + } + if cost is None: + request['quantity'] = self.amount_to_precision(market['symbol'], amount) + else: + request['quantity'] = self.cost_to_precision(market['symbol'], cost) + clientOrderId = self.safe_value_2(params, 'client_id', 'clientOrderId') + if clientOrderId is not None: + clientOrderId = self.safe_integer_2(params, 'client_id', 'clientOrderId') + if clientOrderId is None: + raise BadRequest(self.id + ' createOrder() client order id must be an integer / numeric literal') + else: + request['client_id'] = clientOrderId + leverage = self.safe_number(params, 'leverage') + if not isSpot and (leverage is None): + raise ArgumentsRequired(self.id + ' createOrder requires an extra param params["leverage"] for margin orders') + params = self.omit(params, ['stopPrice', 'stop_price', 'triggerPrice', 'timeInForce', 'client_id', 'clientOrderId', 'cost']) + if price is not None: + request['price'] = self.price_to_precision(market['symbol'], price) + response = None + if isSpot: + if triggerPrice is not None: + if type == 'limit': + raise BadRequest(self.id + ' createOrder() cannot create stop limit orders for spot, only stop market') + else: + request['type'] = side + request['trigger_price'] = self.price_to_precision(symbol, triggerPrice) + response = self.privatePostStopMarketOrderCreate(self.extend(request, params)) + else: + execType = self.safe_string(params, 'exec_type') + isPostOnly = None + isPostOnly, params = self.handle_post_only(type == 'market', execType == 'post_only', params) + timeInForce = self.safe_string(params, 'timeInForce') + request['price'] = 0 if isMarket else self.price_to_precision(market['symbol'], price) + if type == 'limit': + request['type'] = side + elif type == 'market': + marketSuffix = '_total' if (cost is not None) else '' + request['type'] = 'market_' + side + marketSuffix + if isPostOnly: + request['exec_type'] = 'post_only' + elif timeInForce is not None: + request['exec_type'] = timeInForce + response = self.privatePostOrderCreate(self.extend(request, params)) + else: + if triggerPrice is not None: + request['stop_price'] = self.price_to_precision(symbol, triggerPrice) + if type == 'limit': + request['type'] = 'stop_limit_' + side + elif type == 'market': + request['type'] = 'stop_' + side + else: + request['type'] = type + else: + if type == 'limit' or type == 'market': + request['type'] = type + '_' + side + else: + request['type'] = type + response = self.privatePostMarginUserOrderCreate(self.extend(request, params)) + return self.parse_order(response, market) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#1f710d4b-75bc-4b65-ad68-006f863a3f26 + https://documenter.getpostman.com/view/10287440/SzYXWKPi#a4d0aae8-28f7-41ac-94fd-c4030130453d # stop market + https://documenter.getpostman.com/view/10287440/SzYXWKPi#705dfec5-2b35-4667-862b-faf54eca6209 # margin + + :param str id: order id + :param str symbol: not used by exmo cancelOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: True to cancel a trigger order + :param str [params.marginMode]: set to 'cross' or 'isolated' to cancel a margin order + :returns dict: An `order structure ` + """ + self.load_markets() + request: dict = {} + trigger = self.safe_value_2(params, 'trigger', 'stop') + params = self.omit(params, ['trigger', 'stop']) + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('cancelOrder', params) + if marginMode == 'cross': + raise BadRequest(self.id + ' only supports isolated margin') + response = None + if (marginMode == 'isolated'): + request['order_id'] = id + response = self.privatePostMarginUserOrderCancel(self.extend(request, params)) + # + # {} + # + else: + if trigger: + request['parent_order_id'] = id + response = self.privatePostStopMarketOrderCancel(self.extend(request, params)) + # + # {} + # + else: + request['order_id'] = id + response = self.privatePostOrderCancel(self.extend(request, params)) + # + # { + # "error": '', + # "result": True + # } + # + return self.parse_order(response) + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + *spot only* fetches information on an order made by the user + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#cf27781e-28e5-4b39-a52d-3110f5d22459 # spot + + :param str id: order id + :param str symbol: not used by exmo fetchOrder + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + request: dict = { + 'order_id': str(id), + } + response = self.privatePostOrderTrades(self.extend(request, params)) + # + # { + # "type": "buy", + # "in_currency": "BTC", + # "in_amount": "1", + # "out_currency": "USD", + # "out_amount": "100", + # "trades": [ + # { + # "trade_id": 3, + # "date": 1435488248, + # "type": "buy", + # "pair": "BTC_USD", + # "order_id": 12345, + # "quantity": 1, + # "price": 100, + # "amount": 100 + # } + # ] + # } + # + order = self.parse_order(response) + order['id'] = str(id) + return order + + def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all the trades made from a single order + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#cf27781e-28e5-4b39-a52d-3110f5d22459 # spot + https://documenter.getpostman.com/view/10287440/SzYXWKPi#00810661-9119-46c5-aec5-55abe9cb42c7 # margin + + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: set to "isolated" to fetch trades for a margin order + :returns dict[]: a list of `trade structures ` + """ + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchOrderTrades', params) + if marginMode == 'cross': + raise BadRequest(self.id + ' only supports isolated margin') + market = None + if symbol is not None: + market = self.market(symbol) + request: dict = { + 'order_id': str(id), + } + response = None + if marginMode == 'isolated': + response = self.privatePostMarginUserOrderTrades(self.extend(request, params)) + # + # { + # "trades": [ + # { + # "is_maker": False, + # "order_id": "123", + # "pair": "BTC_USD", + # "price": "54122.25", + # "quantity": "0.00069994", + # "trade_dt": "1619069561718824428", + # "trade_id": "692842802860135010", + # "type": "sell" + # } + # ] + # } + # + else: + response = self.privatePostOrderTrades(self.extend(request, params)) + # + # { + # "type": "buy", + # "in_currency": "BTC", + # "in_amount": "1", + # "out_currency": "USD", + # "out_amount": "100", + # "trades": [ + # { + # "trade_id": 3, + # "date": 1435488248, + # "type": "buy", + # "pair": "BTC_USD", + # "order_id": 12345, + # "quantity": 1, + # "price": 100, + # "amount": 100, + # "exec_type": "taker", + # "commission_amount": "0.02", + # "commission_currency": "BTC", + # "commission_percent": "0.2" + # } + # ] + # } + # + trades = self.safe_list(response, 'trades') + return self.parse_trades(trades, market, since, limit) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#0e135370-daa4-4689-8acd-b6876dee9ba1 # spot open orders + https://documenter.getpostman.com/view/10287440/SzYXWKPi#a7cfd4f0-476e-4675-b33f-22a46902f245 # margin + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: set to "isolated" for margin orders + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchOpenOrders', params) + isMargin = ((marginMode == 'cross') or (marginMode == 'isolated')) + response = None + orders = [] + if isMargin: + response = self.privatePostMarginUserOrderList(params) + # + # { + # "orders": [ + # { + # "client_id": "0", + # "comment": "", + # "created": "1619068707985325495", + # "distance": "0", + # "expire": 0, + # "funding_currency": "BTC", + # "funding_quantity": "0.01", + # "funding_rate": "0.02", + # "leverage": "2", + # "order_id": "123", + # "pair": "BTC_USD", + # "previous_type": "limit_sell", + # "price": "58000", + # "quantity": "0.01", + # "src": 0, + # "stop_price": "0", + # "trigger_price": "58000", + # "type": "limit_sell", + # "updated": 1619068707989411800 + # } + # ] + # } + # + params = self.extend(params, { + 'status': 'open', + }) + responseOrders = self.safe_value(response, 'orders') + orders = self.parse_orders(responseOrders, market, since, limit, params) + else: + response = self.privatePostUserOpenOrders(params) + # + # { + # "USDT_USD": [ + # { + # "parent_order_id": "507061384740151010", + # "client_id": "100500", + # "created": "1589547391", + # "type": "stop_market_buy", + # "pair": "USDT_USD", + # "quantity": "1", + # "trigger_price": "5", + # "amount": "5" + # } + # ], + # ... + # } + # + marketIds = list(response.keys()) + for i in range(0, len(marketIds)): + marketId = marketIds[i] + marketInner = self.safe_market(marketId) + params = self.extend(params, { + 'status': 'open', + }) + parsedOrders = self.parse_orders(response[marketId], marketInner, since, limit, params) + orders = self.array_concat(orders, parsedOrders) + return orders + + def parse_status(self, status): + if status is None: + return None + statuses: dict = { + 'cancel_started': 'canceled', + } + if status.find('cancel') >= 0: + status = 'canceled' + return self.safe_string(statuses, status, status) + + def parse_side(self, orderType): + side: dict = { + 'limit_buy': 'buy', + 'limit_sell': 'sell', + 'market_buy': 'buy', + 'market_sell': 'sell', + 'stop_buy': 'buy', + 'stop_sell': 'sell', + 'stop_limit_buy': 'buy', + 'stop_limit_sell': 'sell', + 'trailing_stop_buy': 'buy', + 'trailing_stop_sell': 'sell', + 'stop_market_sell': 'sell', + 'stop_market_buy': 'buy', + 'buy': 'buy', + 'sell': 'sell', + } + return self.safe_string(side, orderType, orderType) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # fetchOrders, fetchOpenOrders, fetchClosedOrders, fetchCanceledOrders + # + # { + # "order_id": "14", + # "created": "1435517311", + # "type": "buy", + # "pair": "BTC_USD", + # "price": "100", + # "quantity": "1", + # "amount": "100" + # } + # + # fetchOrder + # + # { + # "type": "buy", + # "in_currency": "BTC", + # "in_amount": "1", + # "out_currency": "USD", + # "out_amount": "100", + # "trades": [ + # { + # "trade_id": 3, + # "date": 1435488248, + # "type": "buy", + # "pair": "BTC_USD", + # "order_id": 12345, + # "quantity": 1, + # "price": 100, + # "amount": 100 + # } + # ] + # } + # + # Margin fetchOpenOrders + # + # { + # "client_id": "0", + # "comment": "", + # "created": "1619068707985325495", + # "distance": "0", + # "expire": 0, + # "funding_currency": "BTC", + # "funding_quantity": "0.01", + # "funding_rate": "0.02", + # "leverage": "2", + # "order_id": "123", + # "pair": "BTC_USD", + # "previous_type": "limit_sell", + # "price": "58000", + # "quantity": "0.01", + # "src": 0, + # "stop_price": "0", + # "trigger_price": "58000", + # "type": "limit_sell", + # "updated": 1619068707989411800 + # } + # + # Margin fetchClosedOrders + # + # { + # "distance": "0", + # "event_id": "692842802860022508", + # "event_time": "1619069531190173720", + # "event_type": "OrderCancelStarted", + # "order_id": "123", + # "order_status": "cancel_started", + # "order_type": "limit_sell", + # "pair": "BTC_USD", + # "price": "54115", + # "quantity": "0.001", + # "stop_price": "0", + # "trade_id": "0", + # "trade_price": "0", + # "trade_quantity": "0", + # "trade_type": "" + # }, + # + id = self.safe_string_2(order, 'order_id', 'parent_order_id') + eventTime = self.safe_integer_product_2(order, 'event_time', 'created', 0.000001) + timestamp = self.safe_timestamp(order, 'created', eventTime) + orderType = self.safe_string_2(order, 'type', 'order_type') + side = self.parse_side(orderType) + marketId = None + if 'pair' in order: + marketId = order['pair'] + elif ('in_currency' in order) and ('out_currency' in order): + if side == 'buy': + marketId = order['in_currency'] + '_' + order['out_currency'] + else: + marketId = order['out_currency'] + '_' + order['in_currency'] + market = self.safe_market(marketId, market) + symbol = market['symbol'] + amount = self.safe_string(order, 'quantity') + if amount is None: + amountField = 'in_amount' if (side == 'buy') else 'out_amount' + amount = self.safe_string(order, amountField) + price = self.safe_string(order, 'price') + cost = self.safe_string(order, 'amount') + transactions = self.safe_value(order, 'trades', []) + clientOrderId = self.safe_integer(order, 'client_id') + triggerPrice = self.safe_string(order, 'stop_price') + if triggerPrice == '0': + triggerPrice = None + type = None + if (orderType != 'buy') and (orderType != 'sell'): + type = orderType + return self.safe_order({ + 'id': id, + 'clientOrderId': clientOrderId, + 'datetime': self.iso8601(timestamp), + 'timestamp': timestamp, + 'lastTradeTimestamp': self.safe_integer_product(order, 'updated', 0.000001), + 'status': self.parse_status(self.safe_string(order, 'order_status')), + 'symbol': symbol, + 'type': type, + 'timeInForce': None, + 'postOnly': None, + 'side': side, + 'price': price, + 'triggerPrice': triggerPrice, + 'cost': cost, + 'amount': amount, + 'filled': None, + 'remaining': None, + 'average': None, + 'trades': transactions, + 'fee': None, + 'info': order, + }, market) + + def fetch_canceled_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches information on multiple canceled orders made by the user + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#1d2524dd-ae6d-403a-a067-77b50d13fbe5 # margin + https://documenter.getpostman.com/view/10287440/SzYXWKPi#a51be1d0-af5f-44e4-99d7-f7b04c6067d0 # spot canceled orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: timestamp in ms of the earliest order, default is None + :param int [limit]: max number of orders to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: set to "isolated" for margin orders + :returns dict: a list of `order structures ` + """ + self.load_markets() + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchOrders', params) + if marginMode == 'cross': + raise BadRequest(self.id + ' only supports isolated margin') + if limit is None: + limit = 100 + isSpot = (marginMode != 'isolated') + if symbol is not None: + marketInner = self.market(symbol) + symbol = marketInner['symbol'] + request: dict = { + 'limit': limit, + } + request['offset'] = limit if (since is not None) else 0 + request['limit'] = limit + market = None + if symbol is not None: + market = self.market(symbol) + response = None + if isSpot: + response = self.privatePostUserCancelledOrders(self.extend(request, params)) + # + # [ + # { + # "order_id": "27056153840", + # "client_id": "0", + # "created": "1653428646", + # "type": "buy", + # "pair": "BTC_USDT", + # "quantity": "0.1", + # "price": "10", + # "amount": "1" + # } + # ] + # + params = self.extend(params, { + 'status': 'canceled', + }) + return self.parse_orders(response, market, since, limit, params) + else: + responseSwap = self.privatePostMarginUserOrderHistory(self.extend(request, params)) + # + # { + # "items": [ + # { + # "event_id": "692862104574106858", + # "event_time": "1694116400173489405", + # "event_type": "OrderCancelStarted", + # "order_id": "692862104561289319", + # "order_type": "stop_limit_sell", + # "order_status": "cancel_started", + # "trade_id": "0", + # "trade_type":"", + # "trade_quantity": "0", + # "trade_price": "0", + # "pair": "ADA_USDT", + # "quantity": "12", + # "price": "0.23", + # "stop_price": "0.22", + # "distance": "0" + # } + # ... + # ] + # } + # + items = self.safe_value(responseSwap, 'items') + orders = self.parse_orders(items, market, since, limit, params) + result = [] + for i in range(0, len(orders)): + order = orders[i] + if order['status'] == 'canceled': + result.append(order) + return result + + def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + """ + *margin only* edit a trade order + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#f27ee040-c75f-4b59-b608-d05bd45b7899 # margin + + :param str id: order id + :param str symbol: unified CCXT market symbol + :param str type: not used by exmo editOrder + :param str side: not used by exmo editOrder + :param float [amount]: how much of the currency you want to trade in units of the base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: stop price for stop-market and stop-limit orders + :param str params['marginMode']: must be set to isolated + + EXCHANGE SPECIFIC PARAMETERS + :param int [params.distance]: distance for trailing stop orders + :param int [params.expire]: expiration timestamp in UTC timezone for the order. order will not be expired if expire is 0 + :param str [params.comment]: optional comment for order. up to 50 latin symbols, whitespaces, underscores + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('editOrder', params) + if marginMode != 'isolated': + raise BadRequest(self.id + ' editOrder() can only be used for isolated margin orders') + triggerPrice = self.safe_number_n(params, ['triggerPrice', 'stopPrice', 'stop_price']) + params = self.omit(params, ['triggerPrice', 'stopPrice']) + request: dict = { + 'order_id': id, # id of the open order + } + if amount is not None: + request['quantity'] = amount + if price is not None: + request['price'] = self.price_to_precision(market['symbol'], price) + if triggerPrice is not None: + request['stop_price'] = self.price_to_precision(market['symbol'], triggerPrice) + response = self.privatePostMarginUserOrderUpdate(self.extend(request, params)) + return self.parse_order(response) + + def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#c8f9ced9-7ab6-4383-a6a4-bc54469ba60e + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + self.load_markets() + response = self.privatePostDepositAddress(params) + # + # { + # "TRX":"TBnwrf4ZdoYXE3C8L2KMs7YPSL3fg6q6V9", + # "USDTTRC20":"TBnwrf4ZdoYXE3C8L2KMs7YPSL3fg6q6V9" + # } + # + depositAddress = self.safe_string(response, code) + address = None + tag = None + if depositAddress: + addressAndTag = depositAddress.split(',') + address = addressAndTag[0] + numParts = len(addressAndTag) + if numParts > 1: + tag = addressAndTag[1] + self.check_address(address) + return { + 'info': response, + 'currency': code, + 'network': None, + 'address': address, + 'tag': tag, + } + + def get_market_from_trades(self, trades): + tradesBySymbol = self.index_by(trades, 'pair') + symbols = list(tradesBySymbol.keys()) + numSymbols = len(symbols) + if numSymbols == 1: + return self.markets[symbols[0]] + return None + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#3ab9c34d-ad58-4f87-9c57-2e2ea88a8325 + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.load_markets() + currency = self.currency(code) + request: dict = { + 'amount': amount, + 'currency': currency['id'], + 'address': address, + } + if tag is not None: + request['invoice'] = tag + networks = self.safe_value(self.options, 'networks', {}) + network = self.safe_string_upper(params, 'network') # self line allows the user to specify either ERC20 or ETH + network = self.safe_string(networks, network, network) # handle ERC20>ETH alias + if network is not None: + request['transport'] = network + params = self.omit(params, 'network') + response = self.privatePostWithdrawCrypt(self.extend(request, params)) + return self.parse_transaction(response, currency) + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'transferred': 'ok', + 'paid': 'ok', + 'pending': 'pending', + 'processing': 'pending', + 'verifying': 'pending', + } + return self.safe_string(statuses, status, status) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fetchDepositsWithdrawals + # + # { + # "dt": 1461841192, + # "type": "deposit", + # "curr": "RUB", + # "status": "processing", + # "provider": "Qiwi(LA) [12345]", + # "amount": "1", + # "account": "", + # "txid": "ec46f784ad976fd7f7539089d1a129fe46...", + # } + # + # fetchWithdrawals + # + # { + # "operation_id": 47412538520634344, + # "created": 1573760013, + # "updated": 1573760013, + # "type": "withdraw", + # "currency": "DOGE", + # "status": "Paid", + # "amount": "300", + # "provider": "DOGE", + # "commission": "0", + # "account": "DOGE: DBVy8pF1f8yxaCVEHqHeR7kkcHecLQ8nRS", + # "order_id": 69670170, + # "provider_type": "crypto", + # "crypto_address": "DBVy8pF1f8yxaCVEHqHeR7kkcHecLQ8nRS", + # "card_number": "", + # "wallet_address": "", + # "email": "", + # "phone": "", + # "extra": { + # "txid": "f2b66259ae1580f371d38dd27e31a23fff8c04122b65ee3ab5a3f612d579c792", + # "confirmations": null, + # "excode": "", + # "invoice": "" + # }, + # "error": "" + # } + # + # withdraw + # + # { + # "result": True, + # "error": "", + # "task_id": 11775077 + # } + # + timestamp = self.safe_timestamp_2(transaction, 'dt', 'created') + amountString = self.safe_string(transaction, 'amount') + if amountString is not None: + amountString = Precise.string_abs(amountString) + txid = self.safe_string(transaction, 'txid') + if txid is None: + extra = self.safe_value(transaction, 'extra', {}) + extraTxid = self.safe_string(extra, 'txid') + if extraTxid != '': + txid = extraTxid + type = self.safe_string(transaction, 'type') + currencyId = self.safe_string_2(transaction, 'curr', 'currency') + code = self.safe_currency_code(currencyId, currency) + address = None + comment = None + account = self.safe_string(transaction, 'account') + if type == 'deposit': + comment = account + elif type == 'withdrawal': + address = account + if address is not None: + parts = address.split(':') + numParts = len(parts) + if numParts == 2: + address = self.safe_string(parts, 1) + address = address.replace(' ', '') + fee = { + 'currency': None, + 'cost': None, + 'rate': None, + } + # fixed funding fees only(for now) + if not self.fees['transaction']['percentage']: + key = 'withdraw' if (type == 'withdrawal') else 'deposit' + feeCost = self.safe_string(transaction, 'commission') + if feeCost is None: + transactionFees = self.safe_value(self.options, 'transactionFees', {}) + codeFees = self.safe_value(transactionFees, code, {}) + feeCost = self.safe_string(codeFees, key) + # users don't pay for cashbacks, no fees for that + provider = self.safe_string(transaction, 'provider') + if provider == 'cashback': + feeCost = '0' + if feeCost is not None: + # withdrawal amount includes the fee + if type == 'withdrawal': + amountString = Precise.string_sub(amountString, feeCost) + fee['cost'] = self.parse_number(feeCost) + fee['currency'] = code + return { + 'info': transaction, + 'id': self.safe_string_2(transaction, 'order_id', 'task_id'), + 'txid': txid, + 'type': type, + 'currency': code, + 'network': self.safe_string(transaction, 'provider'), + 'amount': self.parse_number(amountString), + 'status': self.parse_transaction_status(self.safe_string_lower(transaction, 'status')), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'address': address, + 'addressFrom': None, + 'addressTo': address, + 'tag': None, + 'tagFrom': None, + 'tagTo': None, + 'updated': self.safe_timestamp(transaction, 'updated'), + 'comment': comment, + 'internal': None, + 'fee': fee, + } + + def fetch_deposits_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch history of deposits and withdrawals + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#31e69a33-4849-4e6a-b4b4-6d574238f6a7 + + :param str [code]: unified currency code for the currency of the deposit/withdrawals, default is None + :param int [since]: timestamp in ms of the earliest deposit/withdrawal, default is None + :param int [limit]: max number of deposit/withdrawals to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `transaction structure ` + """ + self.load_markets() + request: dict = {} + if since is not None: + request['date'] = self.parse_to_int(since / 1000) + currency = None + if code is not None: + currency = self.currency(code) + response = self.privatePostWalletHistory(self.extend(request, params)) + # + # { + # "result": True, + # "error": "", + # "begin": "1493942400", + # "end": "1494028800", + # "history": [ + # { + # "dt": 1461841192, + # "type": "deposit", + # "curr": "RUB", + # "status": "processing", + # "provider": "Qiwi(LA) [12345]", + # "amount": "1", + # "account": "", + # "txid": "ec46f784ad976fd7f7539089d1a129fe46...", + # }, + # { + # "dt": 1463414785, + # "type": "withdrawal", + # "curr": "USD", + # "status": "paid", + # "provider": "EXCODE", + # "amount": "-1", + # "account": "EX-CODE_19371_USDda...", + # "txid": "", + # }, + # ], + # } + # + return self.parse_transactions(response['history'], currency, since, limit) + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#97f1becd-7aad-4e0e-babe-7bbe09e33706 + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + currency = None + request: dict = { + 'type': 'withdraw', + } + if limit is not None: + request['limit'] = limit # default: 100, maximum: 100 + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + response = self.privatePostWalletOperations(self.extend(request, params)) + # + # { + # "items": [ + # { + # "operation_id": 47412538520634344, + # "created": 1573760013, + # "updated": 1573760013, + # "type": "withdraw", + # "currency": "DOGE", + # "status": "Paid", + # "amount": "300", + # "provider": "DOGE", + # "commission": "0", + # "account": "DOGE: DBVy8pF1f8yxaCVEHqHeR7kkcHecLQ8nRS", + # "order_id": 69670170, + # "extra": { + # "txid": "f2b66259ae1580f371d38dd27e31a23fff8c04122b65ee3ab5a3f612d579c792", + # "excode": "", + # "invoice": "" + # }, + # "error": "" + # }, + # ], + # "count": 23 + # } + # + items = self.safe_list(response, 'items', []) + return self.parse_transactions(items, currency, since, limit) + + def fetch_withdrawal(self, id: str, code: Str = None, params={}): + """ + fetch data on a currency withdrawal via the withdrawal id + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#97f1becd-7aad-4e0e-babe-7bbe09e33706 + + :param str id: withdrawal id + :param str code: unified currency code of the currency withdrawn, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + self.load_markets() + currency = None + request: dict = { + 'order_id': id, + 'type': 'withdraw', + } + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + response = self.privatePostWalletOperations(self.extend(request, params)) + # + # { + # "items": [ + # { + # "operation_id": 47412538520634344, + # "created": 1573760013, + # "updated": 1573760013, + # "type": "deposit", + # "currency": "DOGE", + # "status": "Paid", + # "amount": "300", + # "provider": "DOGE", + # "commission": "0", + # "account": "DOGE: DBVy8pF1f8yxaCVEHqHeR7kkcHecLQ8nRS", + # "order_id": 69670170, + # "extra": { + # "txid": "f2b66259ae1580f371d38dd27e31a23fff8c04122b65ee3ab5a3f612d579c792", + # "excode": "", + # "invoice": "" + # }, + # "error": "" + # }, + # ], + # "count": 23 + # } + # + items = self.safe_value(response, 'items', []) + first = self.safe_dict(items, 0, {}) + return self.parse_transaction(first, currency) + + def fetch_deposit(self, id: str, code: Str = None, params={}): + """ + fetch information on a deposit + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#97f1becd-7aad-4e0e-babe-7bbe09e33706 + + :param str id: deposit id + :param str code: unified currency code, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + self.load_markets() + currency = None + request: dict = { + 'order_id': id, + 'type': 'deposit', + } + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + response = self.privatePostWalletOperations(self.extend(request, params)) + # + # { + # "items": [ + # { + # "operation_id": 47412538520634344, + # "created": 1573760013, + # "updated": 1573760013, + # "type": "deposit", + # "currency": "DOGE", + # "status": "Paid", + # "amount": "300", + # "provider": "DOGE", + # "commission": "0", + # "account": "DOGE: DBVy8pF1f8yxaCVEHqHeR7kkcHecLQ8nRS", + # "order_id": 69670170, + # "extra": { + # "txid": "f2b66259ae1580f371d38dd27e31a23fff8c04122b65ee3ab5a3f612d579c792", + # "excode": "", + # "invoice": "" + # }, + # "error": "" + # }, + # ], + # "count": 23 + # } + # + items = self.safe_value(response, 'items', []) + first = self.safe_dict(items, 0, {}) + return self.parse_transaction(first, currency) + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#97f1becd-7aad-4e0e-babe-7bbe09e33706 + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + currency = None + request: dict = { + 'type': 'deposit', + } + if limit is not None: + request['limit'] = limit # default: 100, maximum: 100 + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + response = self.privatePostWalletOperations(self.extend(request, params)) + # + # { + # "items": [ + # { + # "operation_id": 47412538520634344, + # "created": 1573760013, + # "updated": 1573760013, + # "type": "deposit", + # "currency": "DOGE", + # "status": "Paid", + # "amount": "300", + # "provider": "DOGE", + # "commission": "0", + # "account": "DOGE: DBVy8pF1f8yxaCVEHqHeR7kkcHecLQ8nRS", + # "order_id": 69670170, + # "extra": { + # "txid": "f2b66259ae1580f371d38dd27e31a23fff8c04122b65ee3ab5a3f612d579c792", + # "excode": "", + # "invoice": "" + # }, + # "error": "" + # }, + # ], + # "count": 23 + # } + # + items = self.safe_list(response, 'items', []) + return self.parse_transactions(items, currency, since, limit) + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = self.urls['api'][api] + '/' + if api != 'web': + url += self.version + '/' + url += path + if (api == 'public') or (api == 'web'): + if params: + url += '?' + self.urlencode(params) + elif api == 'private': + self.check_required_credentials() + nonce = self.nonce() + body = self.urlencode(self.extend({'nonce': nonce}, params)) + headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Key': self.apiKey, + 'Sign': self.hmac(self.encode(body), self.encode(self.secret), hashlib.sha512), + } + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def nonce(self): + return self.milliseconds() + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None # fallback to default error handler + if ('error' in response) and not ('result' in response): + # error: { + # "code": "140434", + # "msg": "Your margin balance is not sufficient to place the order for '5 TON'. Please top up your margin wallet by "2.5 USDT"." + # } + # + errorCode = self.safe_value(response, 'error', {}) + messageError = self.safe_string(errorCode, 'msg') + code = self.safe_string(errorCode, 'code') + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], code, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], messageError, feedback) + raise ExchangeError(feedback) + if ('result' in response) or ('errmsg' in response): + # + # {"result":false,"error":"Error 50052: Insufficient funds"} + # {"s":"error","errmsg":"strconv.ParseInt: parsing \"\": invalid syntax"} + # + success = self.safe_bool(response, 'result', False) + if isinstance(success, str): + if (success == 'true') or (success == '1'): + success = True + else: + success = False + if not success: + code = None + message = self.safe_string_2(response, 'error', 'errmsg') + errorParts = message.split(':') + numParts = len(errorParts) + if numParts > 1: + errorSubParts = errorParts[0].split(' ') + numSubParts = len(errorSubParts) + code = errorSubParts[1] if (numSubParts > 1) else errorSubParts[0] + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], code, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + raise ExchangeError(feedback) + return None diff --git a/ccxt/fmfwio.py b/ccxt/fmfwio.py new file mode 100644 index 0000000..99b4a9e --- /dev/null +++ b/ccxt/fmfwio.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.hitbtc import hitbtc +from ccxt.abstract.fmfwio import ImplicitAPI +from ccxt.base.types import Any + + +class fmfwio(hitbtc, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(fmfwio, self).describe(), { + 'id': 'fmfwio', + 'name': 'FMFW.io', + 'countries': ['KN'], + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/159177712-b685b40c-5269-4cea-ac83-f7894c49525d.jpg', + 'api': { + 'public': 'https://api.fmfw.io/api/3', + 'private': 'https://api.fmfw.io/api/3', + }, + 'www': 'https://fmfw.io', + 'doc': 'https://api.fmfw.io/', + 'fees': 'https://fmfw.io/fees-and-limits', + 'referral': 'https://fmfw.io/referral/da948b21d6c92d69', + }, + 'fees': { + 'trading': { + 'maker': self.parse_number('0.005'), + 'taker': self.parse_number('0.005'), + }, + }, + }) diff --git a/ccxt/foxbit.py b/ccxt/foxbit.py new file mode 100644 index 0000000..aac0bf4 --- /dev/null +++ b/ccxt/foxbit.py @@ -0,0 +1,1935 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.foxbit import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currencies, Currency, DepositAddress, Int, Market, Num, Order, OrderBook, OrderRequest, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, TradingFeeInterface, TradingFees, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import AccountSuspended +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.errors import OnMaintenance +from ccxt.base.decimal_to_precision import DECIMAL_PLACES +from ccxt.base.precise import Precise + + +class foxbit(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(foxbit, self).describe(), { + 'id': 'foxbit', + 'name': 'Foxbit', + 'countries': ['pt-BR'], + # 300 requests per 10 seconds = 30 requests per second + # rateLimit = 1000 ms / 30 requests ~= 33.334 + 'rateLimit': 33.334, + 'version': '1', + 'comment': 'Foxbit Exchange', + 'certified': False, + 'pro': False, + 'has': { + 'CORS': True, + 'spot': True, + 'margin': None, + 'swap': None, + 'future': None, + 'option': None, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'createLimitBuyOrder': True, + 'createLimitSellOrder': True, + 'createMarketBuyOrder': True, + 'createMarketSellOrder': True, + 'createOrder': True, + 'fecthOrderBook': True, + 'fetchBalance': True, + 'fetchCanceledOrders': True, + 'fetchClosedOrders': True, + 'fetchCurrencies': True, + 'fetchDepositAddress': True, + 'fetchDeposits': True, + 'fetchL2OrderBook': True, + 'fetchLedger': True, + 'fetchMarkets': True, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrders': True, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTrades': True, + 'fetchTradingFee': True, + 'fetchTradingFees': True, + 'fetchTransactions': True, + 'fetchWithdrawals': True, + 'loadMarkets': True, + 'sandbox': False, + 'withdraw': True, + 'ws': False, + }, + 'timeframes': { + '1m': '1m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1h', + '2h': '2h', + '4h': '4h', + '6h': '6h', + '12h': '12h', + '1d': '1d', + '1w': '1w', + '2w': '2w', + '1M': '1M', + }, + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/1f8faca2-ae2f-4222-b33e-5671e7d873dd', + 'api': { + 'public': 'https://api.foxbit.com.br', + 'private': 'https://api.foxbit.com.br', + 'status': 'https://metadata-v2.foxbit.com.br/api', + }, + 'www': 'https://app.foxbit.com.br', + 'doc': [ + 'https://docs.foxbit.com.br', + ], + }, + 'precisionMode': DECIMAL_PLACES, + 'exceptions': { + 'exact': { + # https://docs.foxbit.com.br/rest/v3/#tag/API-Codes/Errors + '400': BadRequest, # Bad request. An unknown error occurred while processing request parameters. + '429': RateLimitExceeded, # Too many requests. Request limit exceeded. Try again later. + '404': BadRequest, # Resource not found. A resource was not found while processing the request. + '500': ExchangeError, # Internal server error. An unknown error occurred while processing the request. + '2001': AuthenticationError, # Authentication error. Error authenticating request. + '2002': AuthenticationError, # Invalid signature. The signature for self request is not valid. + '2003': AuthenticationError, # Invalid access key. Access key missing, invalid or not found. + '2004': BadRequest, # Invalid timestamp. Invalid or missing timestamp. + '2005': PermissionDenied, # IP not allowed. The IP address {IP_ADDR} isn't on the trusted list for self API key. + '3001': PermissionDenied, # Permission denied. Permission denied for self request. + '3002': PermissionDenied, # KYC required. A greater level of KYC verification is required to proceed with self request. + '3003': AccountSuspended, # Member disabled. This member is disabled. Please get in touch with our support for more information. + '4001': BadRequest, # Validation error. A validation error occurred. + '4002': InsufficientFunds, # Insufficient funds. Insufficient funds to proceed with self request. + '4003': InvalidOrder, # Quantity below the minimum allowed. Quantity below the minimum allowed to proceed with self request. + '4004': BadSymbol, # Invalid symbol. The market or asset symbol is invalid or was not found. + '4005': BadRequest, # Invalid idempotent. Characters allowed are "a-z", "0-9", "_" or "-", and 36 at max. We recommend UUID v4 in lowercase. + '4007': ExchangeError, # Locked error. There was an error in your allocated balance, please contact us. + '4008': InvalidOrder, # Cannot submit order. The order cannot be created. + '4009': PermissionDenied, # Invalid level. The sub-member does not have the required level to create the transaction. + '4011': RateLimitExceeded, # Too many open orders. You have reached the limit of open orders per market/side. + '4012': ExchangeError, # Too many simultaneous account operations. We are currently unable to process your balance change due to simultaneous operations on your account. Please retry shortly. + '5001': ExchangeNotAvailable, # Service unavailable. The requested resource is currently unavailable. Try again later. + '5002': OnMaintenance, # Service under maintenance. The requested resource is currently under maintenance. Try again later. + '5003': OnMaintenance, # Market under maintenance. The market is under maintenance. Try again later. + '5004': InvalidOrder, # Market is not deep enough. The market is not deep enough to complete your request. + '5005': InvalidOrder, # Price out of range from market. The order price is out of range from market to complete your request. + '5006': InvalidOrder, # Significant price deviation detected, exceeding acceptable limits. The order price is exceeding acceptable limits from market to complete your request. + }, + 'broad': { + # todo: add details messages that can be usefull here, like when market is not found + }, + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + }, + 'api': { + 'v3': { + 'public': { + 'get': { + 'currencies': 5, # 6 requests per second + 'markets': 5, # 6 requests per second + 'markets/ticker/24hr': 60, # 1 request per 2 seconds + 'markets/{market}/orderbook': 6, # 10 requests per 2 seconds + 'markets/{market}/candlesticks': 12, # 5 requests per 2 seconds + 'markets/{market}/trades/history': 12, # 5 requests per 2 seconds + 'markets/{market}/ticker/24hr': 15, # 4 requests per 2 seconds + }, + }, + 'private': { + 'get': { + 'accounts': 2, # 15 requests per second + 'accounts/{symbol}/transactions': 60, # 1 requests per 2 seconds + 'orders': 2, # 30 requests per 2 seconds + 'orders/by-order-id/{id}': 2, # 30 requests per 2 seconds + 'trades': 6, # 5 orders per second + 'deposits/address': 10, # 3 requests per second + 'deposits': 10, # 3 requests per second + 'withdrawals': 10, # 3 requests per second + 'me/fees/trading': 60, # 1 requests per 2 seconds + }, + 'post': { + 'orders': 2, # 30 requests per 2 seconds + 'orders/batch': 7.5, # 8 requests per 2 seconds + 'orders/cancel-replace': 3, # 20 requests per 2 seconds + 'withdrawals': 10, # 3 requests per second + }, + 'put': { + 'orders/cancel': 2, # 30 requests per 2 seconds + }, + }, + }, + 'status': { + 'public': { + 'get': { + 'status': 30, # 1 request per second + }, + }, + }, + }, + 'fees': { + 'trading': { + 'feeSide': 'get', + 'tierBased': False, + 'percentage': True, + 'taker': self.parse_number('0.005'), + 'maker': self.parse_number('0.0025'), + }, + }, + 'options': { + 'sandboxMode': False, + 'networksById': { + 'algorand': 'ALGO', + 'arbitrum': 'ARBITRUM', + 'avalanchecchain': 'AVAX', + 'bitcoin': 'BTC', + 'bitcoincash': 'BCH', + 'bsc': 'BEP20', + 'cardano': 'ADA', + 'cosmos': 'ATOM', + 'dogecoin': 'DOGE', + 'erc20': 'ETH', + 'hedera': 'HBAR', + 'litecoin': 'LTC', + 'near': 'NEAR', + 'optimism': 'OPTIMISM', + 'polkadot': 'DOT', + 'polygon': 'MATIC', + 'ripple': 'XRP', + 'solana': 'SOL', + 'stacks': 'STX', + 'stellar': 'XLM', + 'tezos': 'XTZ', + 'trc20': 'TRC20', + }, + 'networks': { + 'ALGO': 'algorand', + 'ARBITRUM': 'arbitrum', + 'AVAX': 'avalanchecchain', + 'BTC': 'bitcoin', + 'BCH': 'bitcoincash', + 'BEP20': 'bsc', + 'ADA': 'cardano', + 'ATOM': 'cosmos', + 'DOGE': 'dogecoin', + 'ETH': 'erc20', + 'HBAR': 'hedera', + 'LTC': 'litecoin', + 'NEAR': 'near', + 'OPTIMISM': 'optimism', + 'DOT': 'polkadot', + 'MATIC': 'polygon', + 'XRP': 'ripple', + 'SOL': 'solana', + 'STX': 'stacks', + 'XLM': 'stellar', + 'XTZ': 'tezos', + 'TRC20': 'trc20', + }, + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerPriceType': { + 'last': True, # foxbit default trigger price type is last, no params will change it + 'mark': False, + 'index': False, + }, + 'triggerDirection': False, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'GTC': True, + 'FOK': True, + 'IOC': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'leverage': False, + 'marketBuyByCost': False, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': { + 'expire_maker': True, # foxbit prevents self trading by default, no params can change self + 'expire_taker': True, # foxbit prevents self trading by default, no params can change self + 'expire_both': True, # foxbit prevents self trading by default, no params can change self + 'none': True, # foxbit prevents self trading by default, no params can change self + }, + 'trailing': False, + 'icebergAmount': False, + }, + 'createOrders': { + 'max': 5, + }, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 90, + 'untilDays': 10000, # high value just to keep clear that there is no range limit, just the limit of the page size + 'symbolRequired': True, + }, + 'fetchOrder': { + 'marginMode': False, + 'limit': 1, + 'daysBack': 90, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 90, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': True, + 'limit': 100, + 'daysBack': 90, + 'untilDays': 10000, # high value just to keep clear that there is no range limit, just the limit of the page size + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchClosedOrders': { + 'marginMode': True, + 'limit': 100, + 'daysBack': 90, + 'daysBackCanceled': 90, + 'untilDays': 10000, # high value just to keep clear that there is no range limit, just the limit of the page size + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 500, + }, + }, + }, + }) + + def fetch_currencies(self, params={}) -> Currencies: + response = self.v3PublicGetCurrencies(params) + # { + # "data": [ + # { + # "symbol": "btc", + # "name": "Bitcoin", + # "type": "CRYPTO", + # "precision": 8, + # "deposit_info": { + # "min_to_confirm": "1", + # "min_amount": "0.0001" + # }, + # "withdraw_info": { + # "enabled": True, + # "min_amount": "0.0001", + # "fee": "0.0001" + # }, + # "category": { + # "code": "cripto", + # "name": "Cripto" + # }, + # "networks": [ + # { + # "name": "Bitcoin", + # "code": "btc", + # "deposit_info": { + # status: "ENABLED", + # }, + # "withdraw_info": { + # "status": "ENABLED", + # "fee": "0.0001", + # }, + # "has_destination_tag": False + # } + # ] + # } + # ] + # } + data = self.safe_list(response, 'data', []) + result: dict = {} + for i in range(0, len(data)): + currency = data[i] + precision = self.safe_integer(currency, 'precision') + currencyId = self.safe_string(currency, 'symbol') + name = self.safe_string(currency, 'name') + code = self.safe_currency_code(currencyId) + depositInfo = self.safe_dict(currency, 'deposit_info') + withdrawInfo = self.safe_dict(currency, 'withdraw_info') + networks = self.safe_list(currency, 'networks', []) + type = self.safe_string_lower(currency, 'type') + parsedNetworks: dict = {} + for j in range(0, len(networks)): + network = networks[j] + networkId = self.safe_string(network, 'code') + networkCode = self.network_id_to_code(networkId, code) + networkWithdrawInfo = self.safe_dict(network, 'withdraw_info') + networkDepositInfo = self.safe_dict(network, 'deposit_info') + isWithdrawEnabled = self.safe_string(networkWithdrawInfo, 'status') == 'ENABLED' + isDepositEnabled = self.safe_string(networkDepositInfo, 'status') == 'ENABLED' + parsedNetworks[networkCode] = { + 'info': currency, + 'id': networkId, + 'network': networkCode, + 'name': self.safe_string(network, 'name'), + 'deposit': isDepositEnabled, + 'withdraw': isWithdrawEnabled, + 'active': True, + 'precision': precision, + 'fee': self.safe_number(networkWithdrawInfo, 'fee'), + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'deposit': { + 'min': self.safe_number(depositInfo, 'min_amount'), + 'max': None, + }, + 'withdraw': { + 'min': self.safe_number(withdrawInfo, 'min_amount'), + 'max': None, + }, + }, + } + if self.safe_dict(result, code) is None: + result[code] = self.safe_currency_structure({ + 'id': currencyId, + 'code': code, + 'info': currency, + 'name': name, + 'active': True, + 'type': type, + 'deposit': self.safe_bool(depositInfo, 'enabled', False), + 'withdraw': self.safe_bool(withdrawInfo, 'enabled', False), + 'fee': self.safe_number(withdrawInfo, 'fee'), + 'precision': precision, + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'deposit': { + 'min': self.safe_number(depositInfo, 'min_amount'), + 'max': None, + }, + 'withdraw': { + 'min': self.safe_number(withdrawInfo, 'min_amount'), + 'max': None, + }, + }, + 'networks': parsedNetworks, + }) + return result + + def fetch_markets(self, params={}) -> List[Market]: + """ + Retrieves data on all markets for foxbit. + + https://docs.foxbit.com.br/rest/v3/#tag/Market-Data/operation/MarketsController_index + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = self.v3PublicGetMarkets(params) + # { + # "data": [ + # { + # "symbol": "btcbrl", + # "quantity_min": "0.00000236", + # "quantity_increment": "0.00000001", + # "quantity_precision": 8, + # "price_min": "0.0001", + # "price_increment": "0.0001", + # "price_precision": 4, + # "default_fees": { + # "maker": "0.001", + # "taker": "0.001" + # }, + # "base": { + # "symbol": "btc", + # "name": "Bitcoin", + # "type": "CRYPTO", + # "precision": 8, + # "category": { + # "code": "cripto", + # "name": "Cripto" + # }, + # "deposit_info": { + # "min_to_confirm": "1", + # "min_amount": "0.0001", + # "enabled": True + # }, + # "withdraw_info": { + # "enabled": True, + # "min_amount": "0.0001", + # "fee": "0.0001" + # }, + # "networks": [ + # { + # "name": "Bitcoin", + # "code": "bitcoin", + # "deposit_info": { + # "status": "ENABLED" + # }, + # "withdraw_info": { + # "status": "ENABLED", + # "fee": "0.0001" + # }, + # "has_destination_tag": False + # } + # ], + # "default_network_code": "bitcoin" + # }, + # "quote": { + # "symbol": "btc", + # "name": "Bitcoin", + # "type": "CRYPTO", + # "precision": 8, + # "category": { + # "code": "cripto", + # "name": "Cripto" + # }, + # "deposit_info": { + # "min_to_confirm": "1", + # "min_amount": "0.0001", + # "enabled": True + # }, + # "withdraw_info": { + # "enabled": True, + # "min_amount": "0.0001", + # "fee": "0.0001" + # }, + # "networks": [ + # { + # "name": "Bitcoin", + # "code": "bitcoin", + # "deposit_info": { + # "status": "ENABLED" + # }, + # "withdraw_info": { + # "status": "ENABLED", + # "fee": "0.0001" + # }, + # "has_destination_tag": False + # } + # ], + # "default_network_code": "bitcoin" + # }, + # "order_type": [ + # "LIMIT", + # "MARKET", + # "INSTANT", + # "STOP_LIMIT", + # "STOP_MARKET" + # ] + # } + # ] + # } + markets = self.safe_list(response, 'data', []) + return self.parse_markets(markets) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + Get last 24 hours ticker information, in real-time, for given market. + + https://docs.foxbit.com.br/rest/v3/#tag/Market-Data/operation/MarketsController_ticker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + response = self.v3PublicGetMarketsMarketTicker24hr(self.extend(request, params)) + # { + # "data": [ + # { + # "market_symbol": "btcbrl", + # "last_trade": { + # "price": "358504.69340000", + # "volume": "0.00027893", + # "date": "2024-01-01T00:00:00.000Z" + # }, + # "rolling_24h": { + # "price_change": "3211.87290000", + # "price_change_percent": "0.90400726", + # "volume": "20.03206866", + # "trades_count": "4376", + # "open": "355292.82050000", + # "high": "362999.99990000", + # "low": "355002.88880000" + # }, + # "best": { + # "ask": { + # "price": "358504.69340000", + # "volume": "0.00027893" + # }, + # "bid": { + # "price": "358504.69340000", + # "volume": "0.00027893" + # } + # } + # } + # ] + # } + data = self.safe_list(response, 'data', []) + result = self.safe_dict(data, 0, {}) + return self.parse_ticker(result, market) + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + Retrieve the ticker data of all markets. + + https://docs.foxbit.com.br/rest/v3/#tag/Market-Data/operation/MarketsController_tickers + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + symbols = self.market_symbols(symbols) + response = self.v3PublicGetMarketsTicker24hr(params) + # { + # "data": [ + # { + # "market_symbol": "btcbrl", + # "last_trade": { + # "price": "358504.69340000", + # "volume": "0.00027893", + # "date": "2024-01-01T00:00:00.000Z" + # }, + # "rolling_24h": { + # "price_change": "3211.87290000", + # "price_change_percent": "0.90400726", + # "volume": "20.03206866", + # "trades_count": "4376", + # "open": "355292.82050000", + # "high": "362999.99990000", + # "low": "355002.88880000" + # }, + # } + # ] + # } + data = self.safe_list(response, 'data', []) + return self.parse_tickers(data, symbols) + + def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://docs.foxbit.com.br/rest/v3/#tag/Member-Info/operation/MembersController_listTradingFees + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + self.load_markets() + response = self.v3PrivateGetMeFeesTrading(params) + # [ + # { + # "market_symbol": "btcbrl", + # "maker": "0.0025", + # "taker": "0.005" + # } + # ] + data = self.safe_list(response, 'data', []) + result = {} + for i in range(0, len(data)): + entry = data[i] + marketId = self.safe_string(entry, 'market_symbol') + market = self.safe_market(marketId) + symbol = market['symbol'] + result[symbol] = self.parse_trading_fee(entry, market) + return result + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + Exports a copy of the order book of a specific market. + + https://docs.foxbit.com.br/rest/v3/#tag/Market-Data/operation/MarketsController_findOrderbook + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return, the maximum is 100 + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + defaultLimit = 20 + request: dict = { + 'market': market['id'], + 'depth': defaultLimit if (limit is None) else limit, + } + response = self.v3PublicGetMarketsMarketOrderbook(self.extend(request, params)) + # { + # "sequence_id": 1234567890, + # "timestamp": 1713187921336, + # "bids": [ + # [ + # "3.00000000", + # "300.00000000" + # ], + # [ + # "1.70000000", + # "310.00000000" + # ] + # ], + # "asks": [ + # [ + # "3.00000000", + # "300.00000000" + # ], + # [ + # "2.00000000", + # "321.00000000" + # ] + # ] + # } + timestamp = self.safe_integer(response, 'timestamp') + return self.parse_order_book(response, symbol, timestamp) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + Retrieve the trades of a specific market. + + https://docs.foxbit.com.br/rest/v3/#tag/Market-Data/operation/MarketsController_publicTrades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + if limit is not None: + request['page_size'] = limit + if limit > 200: + request['page_size'] = 200 + # [ + # { + # "id": 1, + # "price": "329248.74700000", + # "volume": "0.00100000", + # "taker_side": "BUY", + # "created_at": "2024-01-01T00:00:00Z" + # } + # ] + response = self.v3PublicGetMarketsMarketTradesHistory(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + return self.parse_trades(data, market, since, limit) + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + Fetch historical candlestick data containing the open, high, low, and close price, and the volume of a market. + + https://docs.foxbit.com.br/rest/v3/#tag/Market-Data/operation/MarketsController_findCandlesticks + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + market = self.market(symbol) + interval = self.safe_string(self.timeframes, timeframe, timeframe) + request: dict = { + 'market': market['id'], + 'interval': interval, + } + if since is not None: + request['start_time'] = self.iso8601(since) + if limit is not None: + request['limit'] = limit + if limit > 500: + request['limit'] = 500 + response = self.v3PublicGetMarketsMarketCandlesticks(self.extend(request, params)) + # [ + # [ + # "1692918000000", # timestamp + # "127772.05150000", # open + # "128467.99980000", # high + # "127750.01000000", # low + # "128353.99990000", # close + # "1692918060000", # close timestamp + # "0.17080431", # base volume + # "21866.35948786", # quote volume + # 66, # number of trades + # "0.12073605", # taker buy base volume + # "15466.34096391" # taker buy quote volume + # ] + # ] + return self.parse_ohlcvs(response, market, interval, since, limit) + + def fetch_balance(self, params={}) -> Balances: + """ + Query for balance and get the amount of funds available for trading or funds locked in orders. + + https://docs.foxbit.com.br/rest/v3/#tag/Account/operation/AccountsController_all + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + response = self.v3PrivateGetAccounts(params) + # { + # "data": [ + # { + # "currency_symbol": "btc", + # "balance": "10000.0", + # "balance_available": "9000.0", + # "balance_locked": "1000.0" + # } + # ] + # } + accounts = self.safe_list(response, 'data', []) + result: dict = { + 'info': response, + } + for i in range(0, len(accounts)): + account = accounts[i] + currencyId = self.safe_string(account, 'currency_symbol') + currencyCode = self.safe_currency_code(currencyId) + total = self.safe_string(account, 'balance') + used = self.safe_string(account, 'balance_locked') + free = self.safe_string(account, 'balance_available') + balanceObj = { + 'free': free, + 'used': used, + 'total': total, + } + result[currencyCode] = balanceObj + return self.safe_balance(result) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + Fetch all unfilled currently open orders. + + https://docs.foxbit.com.br/rest/v3/#tag/Trading/operation/OrdersController_listOrders + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + return self.fetch_orders_by_status('ACTIVE', symbol, since, limit, params) + + def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + Fetch all currently closed orders. + + https://docs.foxbit.com.br/rest/v3/#tag/Trading/operation/OrdersController_listOrders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + return self.fetch_orders_by_status('FILLED', symbol, since, limit, params) + + def fetch_canceled_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + return self.fetch_orders_by_status('CANCELED', symbol, since, limit, params) + + def fetch_orders_by_status(self, status: Str, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + self.load_markets() + market = None + request: dict = { + 'state': status, + } + if symbol is not None: + market = self.market(symbol) + request['market_symbol'] = market['id'] + if since is not None: + request['start_time'] = self.iso8601(since) + if limit is not None: + request['page_size'] = limit + if limit > 100: + request['page_size'] = 100 + response = self.v3PrivateGetOrders(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + return self.parse_orders(data) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}) -> Order: + """ + Create an order with the specified characteristics + + https://docs.foxbit.com.br/rest/v3/#tag/Trading/operation/OrdersController_create + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market', 'limit', 'stop_market', 'stop_limit', 'instant' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency + :param float [price]: the price at which the order is to be fullfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.timeInForce]: "GTC", "FOK", "IOC", "PO" + :param float [params.triggerPrice]: The time in force for the order. One of GTC, FOK, IOC, PO. See .features or foxbit's doc to see more details. + :param bool [params.postOnly]: True or False whether the order is post-only + :param str [params.clientOrderId]: a unique identifier for the order + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + type = type.upper() + if type != 'LIMIT' and type != 'MARKET' and type != 'STOP_MARKET' and type != 'STOP_LIMIT' and type != 'INSTANT': + raise InvalidOrder('Invalid order type: ' + type + '. Must be one of: limit, market, stop_market, stop_limit, instant.') + timeInForce = self.safe_string_upper(params, 'timeInForce') + postOnly = self.safe_bool(params, 'postOnly', False) + triggerPrice = self.safe_number(params, 'triggerPrice') + request: dict = { + 'market_symbol': market['id'], + 'side': side.upper(), + 'type': type, + } + if type == 'STOP_MARKET' or type == 'STOP_LIMIT': + if triggerPrice is None: + raise InvalidOrder('Invalid order type: ' + type + '. Must have triggerPrice.') + if timeInForce is not None: + if timeInForce == 'PO': + request['post_only'] = True + else: + request['time_in_force'] = timeInForce + if postOnly: + request['post_only'] = True + if triggerPrice is not None: + request['stop_price'] = self.price_to_precision(symbol, triggerPrice) + if type == 'INSTANT': + request['amount'] = self.price_to_precision(symbol, amount) + else: + request['quantity'] = self.amount_to_precision(symbol, amount) + if type == 'LIMIT' or type == 'STOP_LIMIT': + request['price'] = self.price_to_precision(symbol, price) + clientOrderId = self.safe_string(params, 'clientOrderId') + if clientOrderId is not None: + request['client_order_id'] = clientOrderId + params = self.omit(params, ['timeInForce', 'postOnly', 'triggerPrice', 'clientOrderId']) + response = self.v3PrivatePostOrders(self.extend(request, params)) + # { + # "id": 1234567890, + # "sn": "OKMAKSDHRVVREK", + # "client_order_id": "451637946501" + # } + return self.parse_order(response, market) + + def create_orders(self, orders: List[OrderRequest], params={}): + """ + create a list of trade orders + + https://docs.foxbit.com.br/rest/v3/#tag/Trading/operation/createBatch + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + ordersRequests = [] + for i in range(0, len(orders)): + order = self.safe_dict(orders, i) + symbol = self.safe_string(order, 'symbol') + market = self.market(symbol) + type = self.safe_string_upper(order, 'type') + orderParams = self.safe_dict(order, 'params', {}) + if type != 'LIMIT' and type != 'MARKET' and type != 'STOP_MARKET' and type != 'STOP_LIMIT' and type != 'INSTANT': + raise InvalidOrder('Invalid order type: ' + type + '. Must be one of: limit, market, stop_market, stop_limit, instant.') + timeInForce = self.safe_string_upper(orderParams, 'timeInForce') + postOnly = self.safe_bool(orderParams, 'postOnly', False) + triggerPrice = self.safe_number(orderParams, 'triggerPrice') + request: dict = { + 'market_symbol': market['id'], + 'side': self.safe_string_upper(order, 'side'), + 'type': type, + } + if type == 'STOP_MARKET' or type == 'STOP_LIMIT': + if triggerPrice is None: + raise InvalidOrder('Invalid order type: ' + type + '. Must have triggerPrice.') + if timeInForce is not None: + if timeInForce == 'PO': + request['post_only'] = True + else: + request['time_in_force'] = timeInForce + del orderParams['timeInForce'] + if postOnly: + request['post_only'] = True + del orderParams['postOnly'] + if triggerPrice is not None: + request['stop_price'] = self.price_to_precision(symbol, triggerPrice) + del orderParams['triggerPrice'] + if type == 'INSTANT': + request['amount'] = self.price_to_precision(symbol, self.safe_string(order, 'amount')) + else: + request['quantity'] = self.amount_to_precision(symbol, self.safe_string(order, 'amount')) + if type == 'LIMIT' or type == 'STOP_LIMIT': + request['price'] = self.price_to_precision(symbol, self.safe_string(order, 'price')) + ordersRequests.append(self.extend(request, orderParams)) + createOrdersRequest = {'data': ordersRequests} + response = self.v3PrivatePostOrdersBatch(self.extend(createOrdersRequest, params)) + # { + # "data": [ + # { + # "side": "BUY", + # "type": "LIMIT", + # "market_symbol": "btcbrl", + # "client_order_id": "451637946501", + # "remark": "A remarkable note for the order.", + # "quantity": "0.42", + # "price": "250000.0", + # "post_only": True, + # "time_in_force": "GTC" + # } + # ] + # } + data = self.safe_list(response, 'data', []) + return self.parse_orders(data) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + Cancel open orders. + + https://docs.foxbit.com.br/rest/v3/#tag/Trading/operation/OrdersController_cancel + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + request: dict = { + 'id': self.parse_number(id), + 'type': 'ID', + } + response = self.v3PrivatePutOrdersCancel(self.extend(request, params)) + # { + # "data": [ + # { + # "sn": "OKMAKSDHRVVREK", + # "id": 123456789 + # } + # ] + # } + data = self.safe_list(response, 'data', []) + result = self.safe_dict(data, 0, {}) + return self.parse_order(result) + + def cancel_all_orders(self, symbol: Str = None, params={}): + """ + Cancel all open orders or all open orders for a specific market. + + https://docs.foxbit.com.br/rest/v3/#tag/Trading/operation/OrdersController_cancel + + :param str symbol: unified market symbol of the market to cancel orders in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + self.load_markets() + request: dict = { + 'type': 'ALL', + } + if symbol is not None: + market = self.market(symbol) + request['type'] = 'MARKET' + request['market_symbol'] = market['id'] + response = self.v3PrivatePutOrdersCancel(self.extend(request, params)) + # { + # "data": [ + # { + # "sn": "OKMAKSDHRVVREK", + # "id": 123456789 + # } + # ] + # } + return [self.safe_order({ + 'info': response, + })] + + def fetch_order(self, id: str, symbol: Str = None, params={}) -> Order: + """ + Get an order by ID. + + https://docs.foxbit.com.br/rest/v3/#tag/Trading/operation/OrdersController_findByOrderId + + @param id + :param str symbol: it is not used in the foxbit API + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + request: dict = { + 'id': id, + } + response = self.v3PrivateGetOrdersByOrderIdId(self.extend(request, params)) + # { + # "id": "1234567890", + # "sn": "OKMAKSDHRVVREK", + # "client_order_id": "451637946501", + # "market_symbol": "btcbrl", + # "side": "BUY", + # "type": "LIMIT", + # "state": "ACTIVE", + # "price": "290000.0", + # "price_avg": "295333.3333", + # "quantity": "0.42", + # "quantity_executed": "0.41", + # "instant_amount": "290.0", + # "instant_amount_executed": "290.0", + # "created_at": "2021-02-15T22:06:32.999Z", + # "trades_count": "2", + # "remark": "A remarkable note for the order.", + # "funds_received": "290.0" + # } + return self.parse_order(response, None) + + def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://docs.foxbit.com.br/rest/v3/#tag/Trading/operation/OrdersController_listOrders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.state]: Enum: ACTIVE, CANCELED, FILLED, PARTIALLY_CANCELED, PARTIALLY_FILLED + :param str [params.side]: Enum: BUY, SELL + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + market = None + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['market_symbol'] = market['id'] + if since is not None: + request['start_time'] = self.iso8601(since) + if limit is not None: + request['page_size'] = limit + if limit > 100: + request['page_size'] = 100 + response = self.v3PrivateGetOrders(self.extend(request, params)) + # { + # "data": [ + # { + # "id": "1234567890", + # "sn": "OKMAKSDHRVVREK", + # "client_order_id": "451637946501", + # "market_symbol": "btcbrl", + # "side": "BUY", + # "type": "LIMIT", + # "state": "ACTIVE", + # "price": "290000.0", + # "price_avg": "295333.3333", + # "quantity": "0.42", + # "quantity_executed": "0.41", + # "instant_amount": "290.0", + # "instant_amount_executed": "290.0", + # "created_at": "2021-02-15T22:06:32.999Z", + # "trades_count": "2", + # "remark": "A remarkable note for the order.", + # "funds_received": "290.0" + # } + # ] + # } + list = self.safe_list(response, 'data', []) + return self.parse_orders(list, market, since, limit) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + Trade history queries will only have data available for the last 3 months, in descending order(most recents trades first). + + https://docs.foxbit.com.br/rest/v3/#tag/Trading/operation/TradesController_all + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trade structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMyTrades() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request = { + 'market_symbol': market['id'], + } + if since is not None: + request['start_time'] = self.iso8601(since) + if limit is not None: + request['page_size'] = limit + if limit > 100: + request['page_size'] = 100 + response = self.v3PrivateGetTrades(self.extend(request, params)) + # { + # "data": [ + # "id": 1234567890, + # "sn": "TC5JZVW2LLJ3IW", + # "order_id": 1234567890, + # "market_symbol": "btcbrl", + # "side": "BUY", + # "price": "290000.0", + # "quantity": "1.0", + # "fee": "0.01", + # "fee_currency_symbol": "btc", + # "created_at": "2021-02-15T22:06:32.999Z" + # ] + # } + data = self.safe_list(response, 'data', []) + return self.parse_trades(data, market, since, limit) + + def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + Fetch the deposit address for a currency associated with self account. + + https://docs.foxbit.com.br/rest/v3/#tag/Deposit/operation/DepositsController_depositAddress + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.networkCode]: the blockchain network to create a deposit address on + :returns dict: an `address structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency_symbol': currency['id'], + } + networkCode, paramsOmited = self.handle_network_code_and_params(params) + if networkCode is not None: + request['network_code'] = self.network_code_to_id(networkCode, code) + response = self.v3PrivateGetDepositsAddress(self.extend(request, paramsOmited)) + # { + # "currency_symbol": "btc", + # "address": "2N9sS8LgrY19rvcCWDmE1ou1tTVmqk4KQAB", + # "message": "Address was retrieved successfully", + # "destination_tag": "string", + # "network": { + # "name": "Bitcoin Network", + # "code": "btc" + # } + # } + return self.parse_deposit_address(response, currency) + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + Fetch all deposits made to an account. + + https://docs.foxbit.com.br/rest/v3/#tag/Deposit/operation/DepositsController_listOrders + + :param str [code]: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposit structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + request: dict = {} + currency = None + if code is not None: + currency = self.currency(code) + if limit is not None: + request['page_size'] = limit + if limit > 100: + request['page_size'] = 100 + if since is not None: + request['start_time'] = self.iso8601(since) + response = self.v3PrivateGetDeposits(self.extend(request, params)) + # { + # "data": [ + # { + # "sn": "OKMAKSDHRVVREK", + # "state": "ACCEPTED", + # "currency_symbol": "btc", + # "amount": "1.0", + # "fee": "0.1", + # "created_at": "2022-02-18T22:06:32.999Z", + # "details_crypto": { + # "transaction_id": "e20f035387020c5d5ea18ad53244f09f3", + # "receiving_address": "2N2rTrnKEFcyJjEJqvVjgWZ3bKvKT7Aij61" + # } + # } + # ] + # } + data = self.safe_list(response, 'data', []) + return self.parse_transactions(data, currency, since, limit) + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + Fetch all withdrawals made from an account. + + https://docs.foxbit.com.br/rest/v3/#tag/Withdrawal/operation/WithdrawalsController_listWithdrawals + + :param str [code]: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawal structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + request: dict = {} + currency = None + if code is not None: + currency = self.currency(code) + if limit is not None: + request['page_size'] = limit + if limit > 100: + request['page_size'] = 100 + if since is not None: + request['start_time'] = self.iso8601(since) + response = self.v3PrivateGetWithdrawals(self.extend(request, params)) + # { + # "data": [ + # { + # "sn": "OKMAKSDHRVVREK", + # "state": "ACCEPTED", + # "rejection_reason": "monthly_limit_exceeded", + # "currency_symbol": "btc", + # "amount": "1.0", + # "fee": "0.1", + # "created_at": "2022-02-18T22:06:32.999Z", + # "details_crypto": { + # "transaction_id": "e20f035387020c5d5ea18ad53244f09f3", + # "destination_address": "2N2rTrnKEFcyJjEJqvVjgWZ3bKvKT7Aij61" + # }, + # "details_fiat": { + # "bank": { + # "code": "1", + # "branch": { + # "number": "1234567890", + # "digit": "1" + # }, + # "account": { + # "number": "1234567890", + # "digit": "1", + # "type": "CHECK" + # } + # } + # } + # } + # ] + # } + data = self.safe_list(response, 'data', []) + return self.parse_transactions(data, currency, since, limit) + + def fetch_transactions(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + Fetch all transactions(deposits and withdrawals) made from an account. + + https://docs.foxbit.com.br/rest/v3/#tag/Withdrawal/operation/WithdrawalsController_listWithdrawals + https://docs.foxbit.com.br/rest/v3/#tag/Deposit/operation/DepositsController_listOrders + + :param str [code]: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawal structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + withdrawals = self.fetch_withdrawals(code, since, limit, params) + deposits = self.fetch_deposits(code, since, limit, params) + allTransactions = self.array_concat(withdrawals, deposits) + result = self.sort_by(allTransactions, 'timestamp') + return result + + def fetch_status(self, params={}): + """ + The latest known information on the availability of the exchange API. + + https://status.foxbit.com/ + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `status structure ` + """ + response = self.statusPublicGetStatus(params) + # { + # "data": { + # "id": 1, + # "attributes": { + # "status": "NORMAL", + # "createdAt": "2023-05-17T18:37:05.934Z", + # "updatedAt": "2024-04-17T02:33:50.945Z", + # "publishedAt": "2023-05-17T18:37:07.653Z", + # "locale": "pt-BR" + # } + # }, + # "meta": { + # } + # } + data = self.safe_dict(response, 'data', {}) + attributes = self.safe_dict(data, 'attributes', {}) + statusRaw = self.safe_string(attributes, 'status') + statusMap = { + 'NORMAL': 'ok', + 'UNDER_MAINTENANCE': 'maintenance', + } + return { + 'status': self.safe_string(statusMap, statusRaw, statusRaw), + 'updated': self.safe_string(attributes, 'updatedAt'), + 'eta': None, + 'url': None, + 'info': response, + } + + def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}) -> Order: + """ + Simultaneously cancel an existing order and create a new one. + + https://docs.foxbit.com.br/rest/v3/#tag/Trading/operation/OrdersController_cancelReplace + + :param str id: order id + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of the currency you want to trade in units of the base currency + :param float [price]: the price at which the order is to be fullfilled, in units of the quote currency, ignored in market orders, used on stop market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' editOrder() requires a symbol argument') + type = type.upper() + if type != 'LIMIT' and type != 'MARKET' and type != 'STOP_MARKET' and type != 'INSTANT': + raise InvalidOrder('Invalid order type: ' + type + '. Must be one of: LIMIT, MARKET, STOP_MARKET, INSTANT.') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'mode': 'ALLOW_FAILURE', + 'cancel': { + 'type': 'ID', + 'id': self.parse_number(id), + }, + 'create': { + 'type': type, + 'side': side.upper(), + 'market_symbol': market['id'], + }, + } + if type == 'LIMIT' or type == 'MARKET': + request['create']['quantity'] = self.amount_to_precision(symbol, amount) + if type == 'LIMIT': + request['create']['price'] = self.price_to_precision(symbol, price) + if type == 'STOP_MARKET': + request['create']['stop_price'] = self.price_to_precision(symbol, price) + request['create']['quantity'] = self.amount_to_precision(symbol, amount) + if type == 'INSTANT': + request['create']['amount'] = self.price_to_precision(symbol, amount) + response = self.v3PrivatePostOrdersCancelReplace(self.extend(request, params)) + # { + # "cancel": { + # "id": 123456789 + # }, + # "create": { + # "id": 1234567890, + # "client_order_id": "451637946501" + # } + # } + return self.parse_order(response['create'], market) + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + Make a withdrawal. + + https://docs.foxbit.com.br/rest/v3/#tag/Withdrawal/operation/WithdrawalsController_createWithdrawal + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency_symbol': currency['id'], + 'amount': self.number_to_string(amount), + 'destination_address': address, + } + if tag is not None: + request['destination_tag'] = tag + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + if networkCode is not None: + request['network_code'] = self.network_code_to_id(networkCode) + response = self.v3PrivatePostWithdrawals(self.extend(request, params)) + # { + # "amount": "1", + # "currency_symbol": "xrp", + # "network_code": "ripple", + # "destination_address": "0x1234567890123456789012345678", + # "destination_tag": "123456" + # } + return self.parse_transaction(response) + + def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch the history of changes, actions done by the user or operations that altered balance of the user + + https://docs.foxbit.com.br/rest/v3/#tag/Account/operation/AccountsController_getTransactions + + :param str code: unified currency code, default is None + :param int [since]: timestamp in ms of the earliest ledger entry, default is None + :param int [limit]: max number of ledger entrys to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ledger structure ` + """ + self.load_markets() + request: dict = {} + if code is None: + raise ArgumentsRequired(self.id + ' fetchLedger() requires a code argument') + if limit is not None: + request['page_size'] = limit + if limit > 100: + request['page_size'] = 100 + if since is not None: + request['start_time'] = self.iso8601(since) + currency = self.currency(code) + request['symbol'] = currency['id'] + response = self.v3PrivateGetAccountsSymbolTransactions(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + return self.parse_ledger(data, currency, since, limit) + + def parse_market(self, market: dict) -> Market: + id = self.safe_string(market, 'symbol') + baseAssets = self.safe_dict(market, 'base') + baseId = self.safe_string(baseAssets, 'symbol') + quoteAssets = self.safe_dict(market, 'quote') + quoteId = self.safe_string(quoteAssets, 'symbol') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + symbol = base + '/' + quote + fees = self.safe_dict(market, 'default_fees') + return self.safe_market_structure({ + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'baseId': baseId, + 'quoteId': quoteId, + 'active': True, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'future': False, + 'swap': False, + 'option': False, + 'contract': False, + 'settle': None, + 'settleId': None, + 'contractSize': None, + 'linear': None, + 'inverse': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'taker': self.safe_number(fees, 'taker'), + 'maker': self.safe_number(fees, 'maker'), + 'percentage': True, + 'tierBased': False, + 'feeSide': 'get', + 'precision': { + 'price': self.safe_integer(quoteAssets, 'precision'), + 'amount': self.safe_integer(baseAssets, 'precision'), + 'cost': self.safe_integer(quoteAssets, 'precision'), + }, + 'limits': { + 'amount': { + 'min': self.safe_number(market, 'quantity_min'), + 'max': None, + }, + 'price': { + 'min': self.safe_number(market, 'price_min'), + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + 'leverage': { + 'min': None, + 'max': None, + }, + }, + 'info': market, + }) + + def parse_trading_fee(self, entry: dict, market: Market = None) -> TradingFeeInterface: + return { + 'info': entry, + 'symbol': market['symbol'], + 'maker': self.safe_number(entry, 'maker'), + 'taker': self.safe_number(entry, 'taker'), + 'percentage': True, + 'tierBased': True, + } + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + marketId = self.safe_string(ticker, 'market_symbol') + symbol = self.safe_symbol(marketId, market, None, 'spot') + rolling_24h = ticker['rolling_24h'] + best = self.safe_dict(ticker, 'best') + bestAsk = self.safe_dict(best, 'ask') + bestBid = self.safe_dict(best, 'bid') + lastTrade = ticker['last_trade'] + lastPrice = self.safe_string(lastTrade, 'price') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': self.parse_date(self.safe_string(lastTrade, 'date')), + 'datetime': self.iso8601(self.parse_date(self.safe_string(lastTrade, 'date'))), + 'high': self.safe_number(rolling_24h, 'high'), + 'low': self.safe_number(rolling_24h, 'low'), + 'bid': self.safe_number(bestBid, 'price'), + 'bidVolume': self.safe_number(bestBid, 'volume'), + 'ask': self.safe_number(bestAsk, 'price'), + 'askVolume': self.safe_number(bestAsk, 'volume'), + 'vwap': None, + 'open': self.safe_number(rolling_24h, 'open'), + 'close': lastPrice, + 'last': lastPrice, + 'previousClose': None, + 'change': self.safe_string(rolling_24h, 'price_change'), + 'percentage': self.safe_string(rolling_24h, 'price_change_percent'), + 'average': None, + 'baseVolume': self.safe_string(rolling_24h, 'volume'), + 'quoteVolume': self.safe_string(rolling_24h, 'quote_volume'), + 'info': ticker, + }, market) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + return [ + self.safe_integer(ohlcv, 0), + self.safe_number(ohlcv, 1), + self.safe_number(ohlcv, 2), + self.safe_number(ohlcv, 3), + self.safe_number(ohlcv, 4), + self.safe_number(ohlcv, 6), + ] + + def parse_trade(self, trade, market=None) -> Trade: + timestamp = self.parse_date(self.safe_string(trade, 'created_at')) + price = self.safe_string(trade, 'price') + amount = self.safe_string(trade, 'volume', self.safe_string(trade, 'quantity')) + privateSideField = self.safe_string_lower(trade, 'side') + side = self.safe_string_lower(trade, 'taker_side', privateSideField) + cost = Precise.string_mul(price, amount) + fee = { + 'currency': self.safe_symbol(self.safe_string(trade, 'fee_currency_symbol')), + 'cost': self.safe_number(trade, 'fee'), + 'rate': None, + } + return self.safe_trade({ + 'id': self.safe_string(trade, 'id'), + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'order': None, + 'type': None, + 'side': side, + 'takerOrMaker': None, + 'price': price, + 'amount': amount, + 'cost': cost, + 'fee': fee, + }, market) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'PARTIALLY_CANCELED': 'open', + 'ACTIVE': 'open', + 'PARTIALLY_FILLED': 'open', + 'FILLED': 'closed', + 'PENDING_CANCEL': 'canceled', + 'CANCELED': 'canceled', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order, market=None) -> Order: + symbol = self.safe_string(order, 'market_symbol') + if market is None and symbol is not None: + market = self.market(symbol) + if market is not None: + symbol = market['symbol'] + timestamp = self.parse_date(self.safe_string(order, 'created_at')) + price = self.safe_string(order, 'price') + filled = self.safe_string(order, 'quantity_executed') + remaining = self.safe_string(order, 'quantity') + # TODO: validate logic of amount here, should self be calculated? + amount = None + if remaining is not None and filled is not None: + amount = Precise.string_add(remaining, filled) + cost = self.safe_string(order, 'funds_received') + if not cost: + priceAverage = self.safe_string(order, 'price_avg') + priceToCalculate = self.safe_string(order, 'price', priceAverage) + cost = Precise.string_mul(priceToCalculate, amount) + side = self.safe_string_lower(order, 'side') + feeCurrency = self.safe_string_upper(market, 'quoteId') + if side == 'buy': + feeCurrency = self.safe_string_upper(market, 'baseId') + return self.safe_order({ + 'id': self.safe_string(order, 'id'), + 'info': order, + 'clientOrderId': self.safe_string(order, 'client_order_id'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'status': self.parse_order_status(self.safe_string(order, 'state')), + 'symbol': self.safe_string(market, 'symbol'), + 'type': self.safe_string(order, 'type'), + 'timeInForce': self.safe_string(order, 'time_in_force'), + 'postOnly': self.safe_bool(order, 'post_only'), + 'reduceOnly': None, + 'side': side, + 'price': self.parse_number(price), + 'triggerPrice': self.safe_number(order, 'stop_price'), + 'takeProfitPrice': None, + 'stopLossPrice': None, + 'cost': self.parse_number(cost), + 'average': self.safe_number(order, 'price_avg'), + 'amount': self.parse_number(amount), + 'filled': self.parse_number(filled), + 'remaining': self.parse_number(remaining), + 'trades': None, + 'fee': { + 'currency': feeCurrency, + 'cost': self.safe_number(order, 'fee_paid'), + }, + }) + + def parse_deposit_address(self, depositAddress, currency: Currency = None): + network = self.safe_dict(depositAddress, 'network') + networkId = self.safe_string(network, 'code') + currencyCode = self.safe_currency_code(None, currency) + unifiedNetwork = self.network_id_to_code(networkId, currencyCode) + return { + 'address': self.safe_string(depositAddress, 'address'), + 'tag': self.safe_string(depositAddress, 'tag'), + 'currency': currencyCode, + 'network': unifiedNetwork, + 'info': depositAddress, + } + + def parse_transaction_status(self, status: Str): + statuses: dict = { + # BOTH + 'SUBMITTING': 'pending', + 'SUBMITTED': 'pending', + 'REJECTED': 'failed', + # DEPOSIT-SPECIFIC + 'CANCELLED': 'canceled', + 'ACCEPTED': 'ok', + 'WARNING': 'pending', + 'UNBLOCKED': 'pending', + 'BLOCKED': 'pending', + # WITHDRAWAL-SPECIFIC + 'PROCESSING': 'pending', + 'CANCELED': 'canceled', + 'FAILED': 'failed', + 'DONE': 'ok', + } + return self.safe_string(statuses, status, status) + + def parse_transaction(self, transaction, currency: Currency = None, since: Int = None, limit: Int = None) -> Transaction: + cryptoDetails = self.safe_dict(transaction, 'details_crypto') + address = self.safe_string_2(cryptoDetails, 'receiving_address', 'destination_address') + sn = self.safe_string(transaction, 'sn') + type = 'withdrawal' + if sn is not None and sn[0] == 'D': + type = 'deposit' + fee = self.safe_string(transaction, 'fee', '0') + amount = self.safe_string(transaction, 'amount') + currencySymbol = self.safe_string(transaction, 'currency_symbol') + actualAmount = amount + currencyCode = self.safe_currency_code(currencySymbol) + status = self.parse_transaction_status(self.safe_string(transaction, 'state')) + created_at = self.safe_string(transaction, 'created_at') + timestamp = self.parse_date(created_at) + datetime = self.iso8601(timestamp) + if fee is not None and amount is not None: + # actualAmount = amount - fee + actualAmount = Precise.string_sub(amount, fee) + feeRate = Precise.string_div(fee, actualAmount) + feeObj = { + 'cost': self.parse_number(fee), + 'currency': currencyCode, + 'rate': self.parse_number(feeRate), + } + return { + 'info': transaction, + 'id': self.safe_string(transaction, 'sn'), + 'txid': self.safe_string(cryptoDetails, 'transaction_id'), + 'timestamp': timestamp, + 'datetime': datetime, + 'network': self.safe_string(transaction, 'network_code'), + 'address': address, + 'addressTo': address, + 'addressFrom': None, + 'tag': self.safe_string(transaction, 'destination_tag'), + 'tagTo': self.safe_string(transaction, 'destination_tag'), + 'tagFrom': None, + 'type': type, + 'amount': self.parse_number(amount), + 'currency': currencyCode, + 'status': status, + 'updated': None, + 'fee': feeObj, + 'comment': None, + 'internal': None, + } + + def parse_ledger_entry_type(self, type): + types: dict = { + 'DEPOSITING': 'transaction', + 'WITHDRAWING': 'transaction', + 'TRADING': 'trade', + 'INTERNAL_TRANSFERING': 'transfer', + 'OTHERS': 'transaction', + } + return self.safe_string(types, type, type) + + def parse_ledger_entry(self, item: dict, currency: Currency = None): + # { + # "uuid": "f8e9f2d6-3c1e-4f2d-8f8e-9f2d6c1e4f2d", + # "amount": "0.0001", + # "balance": "0.0002", + # "created_at": "2021-07-01T12:00:00Z", + # "currency_symbol": "btc", + # "fee": "0.0001", + # "locked": "0.0001", + # "locked_amount": "0.0001", + # "reason_type": "DEPOSITING" + # } + id = self.safe_string(item, 'uuid') + createdAt = self.safe_string(item, 'created_at') + timestamp = self.parse8601(createdAt) + reasonType = self.safe_string(item, 'reason_type') + type = self.parse_ledger_entry_type(reasonType) + exchangeSymbol = self.safe_string(item, 'currency_symbol') + currencySymbol = self.safe_currency_code(exchangeSymbol) + direction = 'in' + amount = self.safe_number(item, 'amount') + realAmount = amount + balance = self.safe_number(item, 'balance') + fee = { + 'cost': self.safe_number(item, 'fee'), + 'currency': currencySymbol, + } + if amount < 0: + direction = 'out' + realAmount = amount * -1 + return { + 'id': id, + 'info': item, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'direction': direction, + 'account': None, + 'referenceId': None, + 'referenceAccount': None, + 'type': type, + 'currency': currencySymbol, + 'amount': realAmount, + 'before': balance - amount, + 'after': balance, + 'status': 'ok', + 'fee': fee, + } + + def sign(self, path, api=[], method='GET', params={}, headers=None, body=None): + version = api[0] + urlPath = api[1] + fullPath = '/rest/' + version + '/' + self.implode_params(path, params) + if version == 'status': + fullPath = '/status' + urlPath = 'status' + url = self.urls['api'][urlPath] + fullPath + params = self.omit(params, self.extract_params(path)) + timestamp = self.milliseconds() + query = '' + signatureQuery = '' + if method == 'GET': + paramKeys = list(params.keys()) + paramKeysLength = len(paramKeys) + if paramKeysLength > 0: + query = self.urlencode(params) + url += '?' + query + for i in range(0, len(paramKeys)): + key = paramKeys[i] + value = self.safe_string(params, key) + if value is not None: + signatureQuery += key + '=' + value + if i < paramKeysLength - 1: + signatureQuery += '&' + if method == 'POST' or method == 'PUT': + body = self.json(params) + bodyToSignature = '' + if body is not None: + bodyToSignature = body + headers = { + 'Content-Type': 'application/json', + } + if urlPath == 'private': + self.check_required_credentials() + preHash = self.number_to_string(timestamp) + method + fullPath + signatureQuery + bodyToSignature + signature = self.hmac(self.encode(preHash), self.encode(self.secret), hashlib.sha256, 'hex') + headers['X-FB-ACCESS-KEY'] = self.apiKey + headers['X-FB-ACCESS-TIMESTAMP'] = self.number_to_string(timestamp) + headers['X-FB-ACCESS-SIGNATURE'] = signature + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None + error = self.safe_dict(response, 'error') + code = self.safe_string(error, 'code') + details = self.safe_list(error, 'details') + message = self.safe_string(error, 'message') + detailsString = '' + if details: + for i in range(0, len(details)): + detailsString = detailsString + details[i] + ' ' + if error is not None: + feedback = self.id + ' ' + message + ' details: ' + detailsString + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], detailsString, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], code, feedback) + raise ExchangeError(feedback) + return None diff --git a/ccxt/gate.py b/ccxt/gate.py new file mode 100644 index 0000000..fe94442 --- /dev/null +++ b/ccxt/gate.py @@ -0,0 +1,7868 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.gate import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, BorrowInterest, Bool, Currencies, Currency, DepositAddress, FundingHistory, Greeks, Int, LedgerEntry, Leverage, Leverages, LeverageTier, LeverageTiers, MarginModification, Market, Num, Option, OptionChain, Order, OrderBook, OrderRequest, CancellationRequest, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, FundingRates, Trade, TradingFeeInterface, TradingFees, Transaction, MarketInterface, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import AccountNotEnabled +from ccxt.base.errors import AccountSuspended +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import OrderImmediatelyFillable +from ccxt.base.errors import NotSupported +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class gate(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(gate, self).describe(), { + 'id': 'gate', + 'name': 'Gate', + 'countries': ['KR'], + 'rateLimit': 50, # 200 requests per 10 second or 50ms + 'version': 'v4', + 'certified': True, + 'pro': True, + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/64f988c5-07b6-4652-b5c1-679a6bf67c85', + 'doc': 'https://www.gate.com/docs/developers/apiv4/en/', + 'www': 'https://gate.com', + 'api': { + 'public': { + 'wallet': 'https://api.gateio.ws/api/v4', + 'futures': 'https://api.gateio.ws/api/v4', + 'margin': 'https://api.gateio.ws/api/v4', + 'delivery': 'https://api.gateio.ws/api/v4', + 'spot': 'https://api.gateio.ws/api/v4', + 'options': 'https://api.gateio.ws/api/v4', + 'sub_accounts': 'https://api.gateio.ws/api/v4', + 'earn': 'https://api.gateio.ws/api/v4', + }, + 'private': { + 'withdrawals': 'https://api.gateio.ws/api/v4', + 'wallet': 'https://api.gateio.ws/api/v4', + 'futures': 'https://api.gateio.ws/api/v4', + 'margin': 'https://api.gateio.ws/api/v4', + 'delivery': 'https://api.gateio.ws/api/v4', + 'spot': 'https://api.gateio.ws/api/v4', + 'options': 'https://api.gateio.ws/api/v4', + 'subAccounts': 'https://api.gateio.ws/api/v4', + 'unified': 'https://api.gateio.ws/api/v4', + 'rebate': 'https://api.gateio.ws/api/v4', + 'earn': 'https://api.gateio.ws/api/v4', + 'account': 'https://api.gateio.ws/api/v4', + 'loan': 'https://api.gateio.ws/api/v4', + }, + }, + 'test': { + 'public': { + 'futures': 'https://api-testnet.gateapi.io/api/v4', + 'delivery': 'https://api-testnet.gateapi.io/api/v4', + 'options': 'https://api-testnet.gateapi.io/api/v4', + 'spot': 'https://api-testnet.gateapi.io/api/v4', + 'wallet': 'https://api-testnet.gateapi.io/api/v4', + 'margin': 'https://api-testnet.gateapi.io/api/v4', + 'sub_accounts': 'https://api-testnet.gateapi.io/api/v4', + 'account': 'https://api-testnet.gateapi.io/api/v4', + }, + 'private': { + 'futures': 'https://api-testnet.gateapi.io/api/v4', + 'delivery': 'https://api-testnet.gateapi.io/api/v4', + 'options': 'https://api-testnet.gateapi.io/api/v4', + 'spot': 'https://api-testnet.gateapi.io/api/v4', + 'wallet': 'https://api-testnet.gateapi.io/api/v4', + 'margin': 'https://api-testnet.gateapi.io/api/v4', + 'sub_accounts': 'https://api-testnet.gateapi.io/api/v4', + 'account': 'https://api-testnet.gateapi.io/api/v4', + }, + }, + 'referral': { + 'url': 'https://www.gate.com/share/CCXTGATE', + 'discount': 0.2, + }, + }, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': True, + 'swap': True, + 'future': True, + 'option': True, + 'addMargin': True, + 'borrowCrossMargin': True, + 'borrowIsolatedMargin': True, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'cancelOrders': True, + 'cancelOrdersForSymbols': True, + 'createMarketBuyOrderWithCost': True, + 'createMarketOrder': True, + 'createMarketOrderWithCost': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createOrders': True, + 'createPostOnlyOrder': True, + 'createReduceOnlyOrder': True, + 'createStopLimitOrder': True, + 'createStopLossOrder': True, + 'createStopMarketOrder': False, + 'createStopOrder': True, + 'createTakeProfitOrder': True, + 'createTriggerOrder': True, + 'editOrder': True, + 'fetchBalance': True, + 'fetchBorrowInterest': True, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': True, + 'fetchDeposits': True, + 'fetchDepositWithdrawFee': 'emulated', + 'fetchDepositWithdrawFees': True, + 'fetchFundingHistory': True, + 'fetchFundingRate': True, + 'fetchFundingRateHistory': True, + 'fetchFundingRates': True, + 'fetchGreeks': True, + 'fetchIndexOHLCV': True, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchLedger': True, + 'fetchLeverage': True, + 'fetchLeverages': True, + 'fetchLeverageTiers': True, + 'fetchLiquidations': True, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarketLeverageTiers': True, + 'fetchMarkets': True, + 'fetchMarkOHLCV': True, + 'fetchMyLiquidations': True, + 'fetchMySettlementHistory': True, + 'fetchMyTrades': True, + 'fetchNetworkDepositAddress': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': True, + 'fetchOpenOrders': True, + 'fetchOption': True, + 'fetchOptionChain': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchPosition': True, + 'fetchPositionHistory': 'emulated', + 'fetchPositionMode': False, + 'fetchPositions': True, + 'fetchPositionsHistory': True, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': True, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': True, + 'fetchTradingFees': True, + 'fetchTransactionFees': True, + 'fetchUnderlyingAssets': True, + 'fetchVolatilityHistory': False, + 'fetchWithdrawals': True, + 'reduceMargin': True, + 'repayCrossMargin': True, + 'repayIsolatedMargin': True, + 'sandbox': True, + 'setLeverage': True, + 'setMarginMode': False, + 'setPositionMode': True, + 'signIn': False, + 'transfer': True, + 'withdraw': True, + }, + 'api': { + 'public': { + # All public endpoints 200r/10s per endpoint + 'wallet': { + 'get': { + 'currency_chains': 1, + }, + }, + 'unified': { + 'get': { + 'currencies': 1, + 'history_loan_rate': 1, + }, + }, + 'spot': { + 'get': { + 'currencies': 1, + 'currencies/{currency}': 1, + 'currency_pairs': 1, + 'currency_pairs/{currency_pair}': 1, + 'tickers': 1, + 'order_book': 1, + 'trades': 1, + 'candlesticks': 1, + 'time': 1, + 'insurance_history': 1, + }, + }, + 'margin': { + 'get': { + 'uni/currency_pairs': 1, + 'uni/currency_pairs/{currency_pair}': 1, + 'loan_margin_tiers': 1, + 'currency_pairs': 1, # deprecated + 'currency_pairs/{currency_pair}': 1, # deprecated + 'funding_book': 1, # deprecated + 'cross/currencies': 1, # deprecated + 'cross/currencies/{currency}': 1, # deprecated + }, + }, + 'flash_swap': { + 'get': { + 'currency_pairs': 1, + 'currencies': 1, # deprecated + }, + }, + 'futures': { + 'get': { + '{settle}/contracts': 1, + '{settle}/contracts/{contract}': 1, + '{settle}/order_book': 1, + '{settle}/trades': 1, + '{settle}/candlesticks': 1, + '{settle}/premium_index': 1, + '{settle}/tickers': 1, + '{settle}/funding_rate': 1, + '{settle}/insurance': 1, + '{settle}/contract_stats': 1, + '{settle}/index_constituents/{index}': 1, + '{settle}/liq_orders': 1, + '{settle}/risk_limit_tiers': 1, + }, + }, + 'delivery': { + 'get': { + '{settle}/contracts': 1, + '{settle}/contracts/{contract}': 1, + '{settle}/order_book': 1, + '{settle}/trades': 1, + '{settle}/candlesticks': 1, + '{settle}/tickers': 1, + '{settle}/insurance': 1, + '{settle}/risk_limit_tiers': 1, + }, + }, + 'options': { + 'get': { + 'underlyings': 1, + 'expirations': 1, + 'contracts': 1, + 'contracts/{contract}': 1, + 'settlements': 1, + 'settlements/{contract}': 1, + 'order_book': 1, + 'tickers': 1, + 'underlying/tickers/{underlying}': 1, + 'candlesticks': 1, + 'underlying/candlesticks': 1, + 'trades': 1, + }, + }, + 'earn': { + 'get': { + 'uni/currencies': 1, + 'uni/currencies/{currency}': 1, + 'dual/investment_plan': 1, + 'structured/products': 1, + }, + }, + 'loan': { + 'get': { + 'collateral/currencies': 1, + 'multi_collateral/currencies': 1, + 'multi_collateral/ltv': 1, + 'multi_collateral/fixed_rate': 1, + 'multi_collateral/current_rate': 1, + }, + }, + }, + 'private': { + # private endpoints default is 150r/10s per endpoint + 'withdrawals': { + 'post': { + 'withdrawals': 20, # 1r/s cost = 20 / 1 = 20 + 'push': 1, + }, + 'delete': { + 'withdrawals/{withdrawal_id}': 1, + }, + }, + 'wallet': { + 'get': { + 'deposit_address': 1, + 'withdrawals': 1, + 'deposits': 1, + 'sub_account_transfers': 1, + 'order_status': 1, + 'withdraw_status': 1, + 'sub_account_balances': 2.5, + 'sub_account_margin_balances': 2.5, + 'sub_account_futures_balances': 2.5, + 'sub_account_cross_margin_balances': 2.5, + 'saved_address': 1, + 'fee': 1, + 'total_balance': 2.5, + 'small_balance': 1, + 'small_balance_history': 1, + 'push': 1, + }, + 'post': { + 'transfers': 2.5, # 8r/s cost = 20 / 8 = 2.5 + 'sub_account_transfers': 2.5, + 'sub_account_to_sub_account': 2.5, + 'small_balance': 1, + }, + }, + 'subAccounts': { + 'get': { + 'sub_accounts': 2.5, + 'sub_accounts/{user_id}': 2.5, + 'sub_accounts/{user_id}/keys': 2.5, + 'sub_accounts/{user_id}/keys/{key}': 2.5, + }, + 'post': { + 'sub_accounts': 2.5, + 'sub_accounts/{user_id}/keys': 2.5, + 'sub_accounts/{user_id}/lock': 2.5, + 'sub_accounts/{user_id}/unlock': 2.5, + }, + 'put': { + 'sub_accounts/{user_id}/keys/{key}': 2.5, + }, + 'delete': { + 'sub_accounts/{user_id}/keys/{key}': 2.5, + }, + }, + 'unified': { + 'get': { + 'accounts': 20 / 15, + 'borrowable': 20 / 15, + 'transferable': 20 / 15, + 'transferables': 20 / 15, + 'batch_borrowable': 20 / 15, + 'loans': 20 / 15, + 'loan_records': 20 / 15, + 'interest_records': 20 / 15, + 'risk_units': 20 / 15, + 'unified_mode': 20 / 15, + 'estimate_rate': 20 / 15, + 'currency_discount_tiers': 20 / 15, + 'loan_margin_tiers': 20 / 15, + 'leverage/user_currency_config': 20 / 15, + 'leverage/user_currency_setting': 20 / 15, + 'account_mode': 20 / 15, # deprecated + }, + 'post': { + 'loans': 200 / 15, # 15r/10s cost = 20 / 1.5 = 13.33 + 'portfolio_calculator': 20 / 15, + 'leverage/user_currency_setting': 20 / 15, + 'collateral_currencies': 20 / 15, + 'account_mode': 20 / 15, # deprecated + }, + 'put': { + 'unified_mode': 20 / 15, + }, + }, + 'spot': { + # default is 200r/10s + 'get': { + 'fee': 1, + 'batch_fee': 1, + 'accounts': 1, + 'account_book': 1, + 'open_orders': 1, + 'orders': 1, + 'orders/{order_id}': 1, + 'my_trades': 1, + 'price_orders': 1, + 'price_orders/{order_id}': 1, + }, + 'post': { + 'batch_orders': 0.4, + 'cross_liquidate_orders': 1, + 'orders': 0.4, + 'cancel_batch_orders': 20 / 75, + 'countdown_cancel_all': 20 / 75, + 'amend_batch_orders': 0.4, + 'price_orders': 0.4, + }, + 'delete': { + 'orders': 20 / 75, + 'orders/{order_id}': 20 / 75, + 'price_orders': 20 / 75, + 'price_orders/{order_id}': 20 / 75, + }, + 'patch': { + 'orders/{order_id}': 0.4, + }, + }, + 'margin': { + 'get': { + 'accounts': 20 / 15, + 'account_book': 20 / 15, + 'funding_accounts': 20 / 15, + 'auto_repay': 20 / 15, + 'transferable': 20 / 15, + 'uni/estimate_rate': 20 / 15, + 'uni/loans': 20 / 15, + 'uni/loan_records': 20 / 15, + 'uni/interest_records': 20 / 15, + 'uni/borrowable': 20 / 15, + 'user/loan_margin_tiers': 20 / 15, + 'user/account': 20 / 15, + 'loans': 20 / 15, # deprecated + 'loans/{loan_id}': 20 / 15, # deprecated + 'loans/{loan_id}/repayment': 20 / 15, # deprecated + 'loan_records': 20 / 15, # deprecated + 'loan_records/{loan_record_id}': 20 / 15, # deprecated + 'borrowable': 20 / 15, # deprecated + 'cross/accounts': 20 / 15, # deprecated + 'cross/account_book': 20 / 15, # deprecated + 'cross/loans': 20 / 15, # deprecated + 'cross/loans/{loan_id}': 20 / 15, # deprecated + 'cross/repayments': 20 / 15, # deprecated + 'cross/interest_records': 20 / 15, # deprecated + 'cross/transferable': 20 / 15, # deprecated + 'cross/estimate_rate': 20 / 15, # deprecated + 'cross/borrowable': 20 / 15, # deprecated + }, + 'post': { + 'auto_repay': 20 / 15, + 'uni/loans': 20 / 15, + 'leverage/user_market_setting': 20 / 15, + 'loans': 20 / 15, # deprecated + 'merged_loans': 20 / 15, # deprecated + 'loans/{loan_id}/repayment': 20 / 15, # deprecated + 'cross/loans': 20 / 15, # deprecated + 'cross/repayments': 20 / 15, # deprecated + }, + 'patch': { + 'loans/{loan_id}': 20 / 15, # deprecated + 'loan_records/{loan_record_id}': 20 / 15, # deprecated + }, + 'delete': { + 'loans/{loan_id}': 20 / 15, # deprecated + }, + }, + 'flash_swap': { + 'get': { + 'orders': 1, + 'orders/{order_id}': 1, + }, + 'post': { + 'orders': 1, + 'orders/preview': 1, + }, + }, + 'futures': { + 'get': { + '{settle}/accounts': 1, + '{settle}/account_book': 1, + '{settle}/positions': 1, + '{settle}/positions/{contract}': 1, + '{settle}/dual_comp/positions/{contract}': 1, + '{settle}/orders': 1, + '{settle}/orders_timerange': 1, + '{settle}/orders/{order_id}': 1, + '{settle}/my_trades': 1, + '{settle}/my_trades_timerange': 1, + '{settle}/position_close': 1, + '{settle}/liquidates': 1, + '{settle}/auto_deleverages': 1, + '{settle}/fee': 1, + '{settle}/risk_limit_table': 1, + '{settle}/price_orders': 1, + '{settle}/price_orders/{order_id}': 1, + }, + 'post': { + '{settle}/positions/{contract}/margin': 1, + '{settle}/positions/{contract}/leverage': 1, + '{settle}/positions/{contract}/risk_limit': 1, + '{settle}/positions/cross_mode': 1, + '{settle}/dual_comp/positions/cross_mode': 1, + '{settle}/dual_mode': 1, + '{settle}/dual_comp/positions/{contract}/margin': 1, + '{settle}/dual_comp/positions/{contract}/leverage': 1, + '{settle}/dual_comp/positions/{contract}/risk_limit': 1, + '{settle}/orders': 0.4, + '{settle}/batch_orders': 0.4, + '{settle}/countdown_cancel_all': 0.4, + '{settle}/batch_cancel_orders': 0.4, + '{settle}/batch_amend_orders': 0.4, + '{settle}/bbo_orders': 0.4, + '{settle}/price_orders': 0.4, + }, + 'put': { + '{settle}/orders/{order_id}': 1, + }, + 'delete': { + '{settle}/orders': 20 / 75, + '{settle}/orders/{order_id}': 20 / 75, + '{settle}/price_orders': 20 / 75, + '{settle}/price_orders/{order_id}': 20 / 75, + }, + }, + 'delivery': { + 'get': { + '{settle}/accounts': 20 / 15, + '{settle}/account_book': 20 / 15, + '{settle}/positions': 20 / 15, + '{settle}/positions/{contract}': 20 / 15, + '{settle}/orders': 20 / 15, + '{settle}/orders/{order_id}': 20 / 15, + '{settle}/my_trades': 20 / 15, + '{settle}/position_close': 20 / 15, + '{settle}/liquidates': 20 / 15, + '{settle}/settlements': 20 / 15, + '{settle}/price_orders': 20 / 15, + '{settle}/price_orders/{order_id}': 20 / 15, + }, + 'post': { + '{settle}/positions/{contract}/margin': 20 / 15, + '{settle}/positions/{contract}/leverage': 20 / 15, + '{settle}/positions/{contract}/risk_limit': 20 / 15, + '{settle}/orders': 20 / 15, + '{settle}/price_orders': 20 / 15, + }, + 'delete': { + '{settle}/orders': 20 / 15, + '{settle}/orders/{order_id}': 20 / 15, + '{settle}/price_orders': 20 / 15, + '{settle}/price_orders/{order_id}': 20 / 15, + }, + }, + 'options': { + 'get': { + 'my_settlements': 20 / 15, + 'accounts': 20 / 15, + 'account_book': 20 / 15, + 'positions': 20 / 15, + 'positions/{contract}': 20 / 15, + 'position_close': 20 / 15, + 'orders': 20 / 15, + 'orders/{order_id}': 20 / 15, + 'my_trades': 20 / 15, + 'mmp': 20 / 15, + }, + 'post': { + 'orders': 20 / 15, + 'countdown_cancel_all': 20 / 15, + 'mmp': 20 / 15, + 'mmp/reset': 20 / 15, + }, + 'delete': { + 'orders': 20 / 15, + 'orders/{order_id}': 20 / 15, + }, + }, + 'earn': { + 'get': { + 'uni/lends': 20 / 15, + 'uni/lend_records': 20 / 15, + 'uni/interests/{currency}': 20 / 15, + 'uni/interest_records': 20 / 15, + 'uni/interest_status/{currency}': 20 / 15, + 'uni/chart': 20 / 15, + 'uni/rate': 20 / 15, + 'staking/eth2/rate_records': 20 / 15, + 'dual/orders': 20 / 15, + 'structured/orders': 20 / 15, + 'staking/coins': 20 / 15, + 'staking/order_list': 20 / 15, + 'staking/award_list': 20 / 15, + 'staking/assets': 20 / 15, + 'uni/currencies': 20 / 15, # deprecated + 'uni/currencies/{currency}': 20 / 15, # deprecated + }, + 'post': { + 'uni/lends': 20 / 15, + 'staking/eth2/swap': 20 / 15, + 'dual/orders': 20 / 15, + 'structured/orders': 20 / 15, + 'staking/swap': 20 / 15, + }, + 'put': { + 'uni/interest_reinvest': 20 / 15, # deprecated + }, + 'patch': { + 'uni/lends': 20 / 15, + }, + }, + 'loan': { + 'get': { + 'collateral/orders': 20 / 15, + 'collateral/orders/{order_id}': 20 / 15, + 'collateral/repay_records': 20 / 15, + 'collateral/collaterals': 20 / 15, + 'collateral/total_amount': 20 / 15, + 'collateral/ltv': 20 / 15, + 'multi_collateral/orders': 20 / 15, + 'multi_collateral/orders/{order_id}': 20 / 15, + 'multi_collateral/repay': 20 / 15, + 'multi_collateral/mortgage': 20 / 15, + 'multi_collateral/currency_quota': 20 / 15, + 'collateral/currencies': 20 / 15, # deprecated + 'multi_collateral/currencies': 20 / 15, # deprecated + 'multi_collateral/ltv': 20 / 15, # deprecated + 'multi_collateral/fixed_rate': 20 / 15, # deprecated + 'multi_collateral/current_rate': 20 / 15, # deprecated + }, + 'post': { + 'collateral/orders': 20 / 15, + 'collateral/repay': 20 / 15, + 'collateral/collaterals': 20 / 15, + 'multi_collateral/orders': 20 / 15, + 'multi_collateral/repay': 20 / 15, + 'multi_collateral/mortgage': 20 / 15, + }, + }, + 'account': { + 'get': { + 'detail': 20 / 15, + 'main_keys': 20 / 15, + 'rate_limit': 20 / 15, + 'stp_groups': 20 / 15, + 'stp_groups/{stp_id}/users': 20 / 15, + 'stp_groups/debit_fee': 20 / 15, + 'debit_fee': 20 / 15, + }, + 'post': { + 'stp_groups': 20 / 15, + 'stp_groups/{stp_id}/users': 20 / 15, + 'debit_fee': 20 / 15, + }, + 'delete': { + 'stp_groups/{stp_id}/users': 20 / 15, + }, + }, + 'rebate': { + 'get': { + 'agency/transaction_history': 20 / 15, + 'agency/commission_history': 20 / 15, + 'partner/transaction_history': 20 / 15, + 'partner/commission_history': 20 / 15, + 'partner/sub_list': 20 / 15, + 'broker/commission_history': 20 / 15, + 'broker/transaction_history': 20 / 15, + 'user/info': 20 / 15, + 'user/sub_relation': 20 / 15, + }, + }, + }, + }, + 'timeframes': { + '10s': '10s', + '1m': '1m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1h', + '2h': '2h', + '4h': '4h', + '8h': '8h', + '1d': '1d', + '7d': '7d', + '1w': '7d', + }, + # copied from gatev2 + 'commonCurrencies': { + 'ORT': 'XREATORS', + 'ASS': 'ASSF', + '88MPH': 'MPH', + 'AXIS': 'AXISDEFI', + 'BIFI': 'BITCOINFILE', + 'BOX': 'DEFIBOX', + 'BYN': 'BEYONDFI', + 'EGG': 'GOOSEFINANCE', + 'GTC': 'GAMECOM', # conflict with Gitcoin and Gastrocoin + 'GTC_HT': 'GAMECOM_HT', + 'GTC_BSC': 'GAMECOM_BSC', + 'HIT': 'HITCHAIN', + 'MM': 'MILLION', # conflict with MilliMeter + 'MPH': 'MORPHER', # conflict with 88MPH + 'POINT': 'GATEPOINT', + 'RAI': 'RAIREFLEXINDEX', # conflict with RAI Finance + 'RED': 'RedLang', + 'SBTC': 'SUPERBITCOIN', + 'TNC': 'TRINITYNETWORKCREDIT', + 'VAI': 'VAIOT', + 'TRAC': 'TRACO', # conflict with OriginTrail(TRAC) + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + }, + 'headers': { + 'X-Gate-Channel-Id': 'ccxt', + }, + 'options': { + 'timeDifference': 0, # the difference between system clock and exchange clock + 'adjustForTimeDifference': False, # controls the adjustment logic upon instantiation + 'sandboxMode': False, + 'unifiedAccount': None, + 'createOrder': { + 'expiration': 86400, # for conditional orders + }, + 'createMarketBuyOrderRequiresPrice': True, + 'networks': { + 'BTC': 'BTC', + 'BRC20': 'BTCBRC', # for eg: ORDI, RATS, ... + 'ETH': 'ETH', + 'ERC20': 'ETH', + 'TRX': 'TRX', + 'TRC20': 'TRX', + 'HECO': 'HT', + 'HRC20': 'HT', + 'BSC': 'BSC', + 'BEP20': 'BSC', + 'SOL': 'SOL', + 'MATIC': 'MATIC', + 'OPTIMISM': 'OPETH', + 'ADA': 'ADA', # CARDANO + 'AVAXC': 'AVAX_C', + 'NEAR': 'NEAR', + 'ARBONE': 'ARBEVM', + 'BASE': 'BASEEVM', + 'SUI': 'SUI', + 'CRONOS': 'CRO', + 'CRO': 'CRO', + 'APT': 'APT', + 'SCROLL': 'SCROLLETH', + 'TAIKO': 'TAIKOETH', + 'HYPE': 'HYPE', + 'ALGO': 'ALGO', + # KAVA: ['KAVA', 'KAVAEVM'] + # SEI: ['SEI', 'SEIEVM'] + 'LINEA': 'LINEAETH', + 'BLAST': 'BLASTETH', + 'XLM': 'XLM', + 'RSK': 'RBTC', + 'TON': 'TON', + 'MNT': 'MNT', + # 'RUNE': 'BTCRUNES', probably, cant verify atm + 'CELO': 'CELO', + 'HBAR': 'HBAR', + # 'FTM': SONIC REBRAND, todo + 'ZKSERA': 'ZKSERA', + 'KLAY': 'KLAY', + 'EOS': 'EOS', + 'ACA': 'ACA', + # TLOS: ['TLOS', 'TLOSEVM'] + # ASTR: ['ASTR', 'ASTREVM'] + # CFX: ['CFX', 'CFXEVM'] + 'XTZ': 'XTZ', + 'EGLD': 'EGLD', + 'GLMR': 'GLMR', + 'AURORA': 'AURORAEVM', + # others + 'KON': 'KONET', + 'GATECHAIN': 'GTEVM', + 'KUSAMA': 'KSMSM', + 'OKC': 'OKT', + 'POLKADOT': 'DOTSM', # todo: DOT for main DOT + 'LUNA': 'LUNC', + }, + 'networksById': { + 'OPETH': 'OP', + 'ETH': 'ERC20', # for GOlang + 'ERC20': 'ERC20', + 'TRX': 'TRC20', + 'TRC20': 'TRC20', + 'HT': 'HRC20', + 'HECO': 'HRC20', + 'BSC': 'BEP20', + 'BEP20': 'BEP20', + 'POLYGON': 'MATIC', + 'POL': 'MATIC', + }, + 'timeInForce': { + 'GTC': 'gtc', + 'IOC': 'ioc', + 'PO': 'poc', + 'POC': 'poc', + 'FOK': 'fok', + }, + 'accountsByType': { + 'funding': 'spot', + 'spot': 'spot', + 'margin': 'margin', + 'cross_margin': 'cross_margin', + 'cross': 'cross_margin', + 'isolated': 'margin', + 'swap': 'futures', + 'future': 'delivery', + 'futures': 'futures', + 'delivery': 'delivery', + 'option': 'options', + 'options': 'options', + }, + 'fetchMarkets': { + 'types': ['spot', 'swap', 'future', 'option'], + }, + 'swap': { + 'fetchMarkets': { + 'settlementCurrencies': ['usdt', 'btc'], + }, + }, + 'future': { + 'fetchMarkets': { + 'settlementCurrencies': ['usdt'], + }, + }, + }, + 'features': { + 'default': { + 'sandbox': True, + 'createOrder': { + 'marginMode': True, + 'triggerPrice': True, + 'triggerDirection': True, # todo: implementation edit needed + 'triggerPriceType': None, + 'stopLossPrice': True, + 'takeProfitPrice': True, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'iceberg': True, # todo implement + 'selfTradePrevention': True, # todo implement + 'leverage': False, + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': True, + }, + 'createOrders': { + 'max': 40, # NOTE! max 10 per symbol + }, + 'fetchMyTrades': { + 'marginMode': True, + 'limit': 1000, + 'daysBack': None, + 'untilDays': 30, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': True, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOpenOrders': { + 'marginMode': True, + 'trigger': True, + 'trailing': False, + 'limit': 100, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': { + 'marginMode': True, + 'trigger': True, + 'trailing': False, + 'limit': 100, + 'untilDays': 30, + 'daysBack': None, + 'daysBackCanceled': None, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 1000, + }, + }, + 'spot': { + 'extends': 'default', + }, + 'forDerivatives': { + 'extends': 'spot', + 'createOrder': { + 'marginMode': False, + 'triggerPriceType': { + 'last': True, + 'mark': True, + 'index': True, + }, + }, + 'createOrders': { + 'max': 10, + }, + 'fetchMyTrades': { + 'marginMode': False, + 'untilDays': None, + }, + 'fetchOpenOrders': { + 'marginMode': False, + }, + 'fetchClosedOrders': { + 'marginMode': False, + 'untilDays': None, + 'limit': 1000, + }, + 'fetchOHLCV': { + 'limit': 1999, + }, + }, + 'swap': { + 'linear': { + 'extends': 'forDerivatives', + }, + 'inverse': { + 'extends': 'forDerivatives', + }, + }, + 'future': { + 'linear': { + 'extends': 'forDerivatives', + }, + 'inverse': { + 'extends': 'forDerivatives', + }, + }, + }, + 'precisionMode': TICK_SIZE, + 'fees': { + 'trading': { + 'tierBased': True, + 'feeSide': 'get', + 'percentage': True, + 'maker': self.parse_number('0.002'), + 'taker': self.parse_number('0.002'), + 'tiers': { + # volume is in BTC + 'maker': [ + [self.parse_number('0'), self.parse_number('0.002')], + [self.parse_number('1.5'), self.parse_number('0.00185')], + [self.parse_number('3'), self.parse_number('0.00175')], + [self.parse_number('6'), self.parse_number('0.00165')], + [self.parse_number('12.5'), self.parse_number('0.00155')], + [self.parse_number('25'), self.parse_number('0.00145')], + [self.parse_number('75'), self.parse_number('0.00135')], + [self.parse_number('200'), self.parse_number('0.00125')], + [self.parse_number('500'), self.parse_number('0.00115')], + [self.parse_number('1250'), self.parse_number('0.00105')], + [self.parse_number('2500'), self.parse_number('0.00095')], + [self.parse_number('3000'), self.parse_number('0.00085')], + [self.parse_number('6000'), self.parse_number('0.00075')], + [self.parse_number('11000'), self.parse_number('0.00065')], + [self.parse_number('20000'), self.parse_number('0.00055')], + [self.parse_number('40000'), self.parse_number('0.00055')], + [self.parse_number('75000'), self.parse_number('0.00055')], + ], + 'taker': [ + [self.parse_number('0'), self.parse_number('0.002')], + [self.parse_number('1.5'), self.parse_number('0.00195')], + [self.parse_number('3'), self.parse_number('0.00185')], + [self.parse_number('6'), self.parse_number('0.00175')], + [self.parse_number('12.5'), self.parse_number('0.00165')], + [self.parse_number('25'), self.parse_number('0.00155')], + [self.parse_number('75'), self.parse_number('0.00145')], + [self.parse_number('200'), self.parse_number('0.00135')], + [self.parse_number('500'), self.parse_number('0.00125')], + [self.parse_number('1250'), self.parse_number('0.00115')], + [self.parse_number('2500'), self.parse_number('0.00105')], + [self.parse_number('3000'), self.parse_number('0.00095')], + [self.parse_number('6000'), self.parse_number('0.00085')], + [self.parse_number('11000'), self.parse_number('0.00075')], + [self.parse_number('20000'), self.parse_number('0.00065')], + [self.parse_number('40000'), self.parse_number('0.00065')], + [self.parse_number('75000'), self.parse_number('0.00065')], + ], + }, + }, + 'swap': { + 'tierBased': True, + 'feeSide': 'base', + 'percentage': True, + 'maker': self.parse_number('0.0'), + 'taker': self.parse_number('0.0005'), + 'tiers': { + 'maker': [ + [self.parse_number('0'), self.parse_number('0.0000')], + [self.parse_number('1.5'), self.parse_number('-0.00005')], + [self.parse_number('3'), self.parse_number('-0.00005')], + [self.parse_number('6'), self.parse_number('-0.00005')], + [self.parse_number('12.5'), self.parse_number('-0.00005')], + [self.parse_number('25'), self.parse_number('-0.00005')], + [self.parse_number('75'), self.parse_number('-0.00005')], + [self.parse_number('200'), self.parse_number('-0.00005')], + [self.parse_number('500'), self.parse_number('-0.00005')], + [self.parse_number('1250'), self.parse_number('-0.00005')], + [self.parse_number('2500'), self.parse_number('-0.00005')], + [self.parse_number('3000'), self.parse_number('-0.00008')], + [self.parse_number('6000'), self.parse_number('-0.01000')], + [self.parse_number('11000'), self.parse_number('-0.01002')], + [self.parse_number('20000'), self.parse_number('-0.01005')], + [self.parse_number('40000'), self.parse_number('-0.02000')], + [self.parse_number('75000'), self.parse_number('-0.02005')], + ], + 'taker': [ + [self.parse_number('0'), self.parse_number('0.00050')], + [self.parse_number('1.5'), self.parse_number('0.00048')], + [self.parse_number('3'), self.parse_number('0.00046')], + [self.parse_number('6'), self.parse_number('0.00044')], + [self.parse_number('12.5'), self.parse_number('0.00042')], + [self.parse_number('25'), self.parse_number('0.00040')], + [self.parse_number('75'), self.parse_number('0.00038')], + [self.parse_number('200'), self.parse_number('0.00036')], + [self.parse_number('500'), self.parse_number('0.00034')], + [self.parse_number('1250'), self.parse_number('0.00032')], + [self.parse_number('2500'), self.parse_number('0.00030')], + [self.parse_number('3000'), self.parse_number('0.00030')], + [self.parse_number('6000'), self.parse_number('0.00030')], + [self.parse_number('11000'), self.parse_number('0.00030')], + [self.parse_number('20000'), self.parse_number('0.00030')], + [self.parse_number('40000'), self.parse_number('0.00030')], + [self.parse_number('75000'), self.parse_number('0.00030')], + ], + }, + }, + }, + # https://www.gate.com/docs/developers/apiv4/en/#label-list + 'exceptions': { + 'exact': { + 'INVALID_PARAM_VALUE': BadRequest, + 'INVALID_PROTOCOL': BadRequest, + 'INVALID_ARGUMENT': BadRequest, + 'INVALID_REQUEST_BODY': BadRequest, + 'MISSING_REQUIRED_PARAM': ArgumentsRequired, + 'BAD_REQUEST': BadRequest, + 'INVALID_CONTENT_TYPE': BadRequest, + 'NOT_ACCEPTABLE': BadRequest, + 'METHOD_NOT_ALLOWED': BadRequest, + 'NOT_FOUND': ExchangeError, + 'AUTHENTICATION_FAILED': AuthenticationError, + 'INVALID_CREDENTIALS': AuthenticationError, + 'INVALID_KEY': AuthenticationError, + 'IP_FORBIDDEN': AuthenticationError, + 'READ_ONLY': PermissionDenied, + 'INVALID_SIGNATURE': AuthenticationError, + 'MISSING_REQUIRED_HEADER': AuthenticationError, + 'REQUEST_EXPIRED': AuthenticationError, + 'ACCOUNT_LOCKED': AccountSuspended, + 'FORBIDDEN': PermissionDenied, + 'SUB_ACCOUNT_NOT_FOUND': ExchangeError, + 'SUB_ACCOUNT_LOCKED': AccountSuspended, + 'MARGIN_BALANCE_EXCEPTION': ExchangeError, + 'MARGIN_TRANSFER_FAILED': ExchangeError, + 'TOO_MUCH_FUTURES_AVAILABLE': ExchangeError, + 'FUTURES_BALANCE_NOT_ENOUGH': InsufficientFunds, + 'ACCOUNT_EXCEPTION': ExchangeError, + 'SUB_ACCOUNT_TRANSFER_FAILED': ExchangeError, + 'ADDRESS_NOT_USED': ExchangeError, + 'TOO_FAST': RateLimitExceeded, + 'WITHDRAWAL_OVER_LIMIT': ExchangeError, + 'API_WITHDRAW_DISABLED': ExchangeNotAvailable, + 'INVALID_WITHDRAW_ID': ExchangeError, + 'INVALID_WITHDRAW_CANCEL_STATUS': ExchangeError, + 'INVALID_PRECISION': InvalidOrder, + 'INVALID_CURRENCY': BadSymbol, + 'INVALID_CURRENCY_PAIR': BadSymbol, + 'POC_FILL_IMMEDIATELY': OrderImmediatelyFillable, # {"label":"POC_FILL_IMMEDIATELY","message":"Order would match and take immediately so its cancelled"} + 'ORDER_NOT_FOUND': OrderNotFound, + 'CLIENT_ID_NOT_FOUND': OrderNotFound, + 'ORDER_CLOSED': InvalidOrder, + 'ORDER_CANCELLED': InvalidOrder, + 'QUANTITY_NOT_ENOUGH': InvalidOrder, + 'BALANCE_NOT_ENOUGH': InsufficientFunds, + 'MARGIN_NOT_SUPPORTED': InvalidOrder, + 'MARGIN_BALANCE_NOT_ENOUGH': InsufficientFunds, + 'AMOUNT_TOO_LITTLE': InvalidOrder, + 'AMOUNT_TOO_MUCH': InvalidOrder, + 'REPEATED_CREATION': InvalidOrder, + 'LOAN_NOT_FOUND': OrderNotFound, + 'LOAN_RECORD_NOT_FOUND': OrderNotFound, + 'NO_MATCHED_LOAN': ExchangeError, + 'NOT_MERGEABLE': ExchangeError, + 'NO_CHANGE': ExchangeError, + 'REPAY_TOO_MUCH': ExchangeError, + 'TOO_MANY_CURRENCY_PAIRS': InvalidOrder, + 'TOO_MANY_ORDERS': InvalidOrder, + 'TOO_MANY_REQUESTS': RateLimitExceeded, + 'MIXED_ACCOUNT_TYPE': InvalidOrder, + 'AUTO_BORROW_TOO_MUCH': ExchangeError, + 'TRADE_RESTRICTED': InsufficientFunds, + 'USER_NOT_FOUND': AccountNotEnabled, + 'CONTRACT_NO_COUNTER': ExchangeError, + 'CONTRACT_NOT_FOUND': BadSymbol, + 'RISK_LIMIT_EXCEEDED': ExchangeError, + 'INSUFFICIENT_AVAILABLE': InsufficientFunds, + 'LIQUIDATE_IMMEDIATELY': InvalidOrder, + 'LEVERAGE_TOO_HIGH': InvalidOrder, + 'LEVERAGE_TOO_LOW': InvalidOrder, + 'ORDER_NOT_OWNED': ExchangeError, + 'ORDER_FINISHED': ExchangeError, + 'POSITION_CROSS_MARGIN': ExchangeError, + 'POSITION_IN_LIQUIDATION': ExchangeError, + 'POSITION_IN_CLOSE': ExchangeError, + 'POSITION_EMPTY': InvalidOrder, + 'REMOVE_TOO_MUCH': ExchangeError, + 'RISK_LIMIT_NOT_MULTIPLE': ExchangeError, + 'RISK_LIMIT_TOO_HIGH': ExchangeError, + 'RISK_LIMIT_TOO_lOW': ExchangeError, + 'PRICE_TOO_DEVIATED': InvalidOrder, + 'SIZE_TOO_LARGE': InvalidOrder, + 'SIZE_TOO_SMALL': InvalidOrder, + 'PRICE_OVER_LIQUIDATION': InvalidOrder, + 'PRICE_OVER_BANKRUPT': InvalidOrder, + 'ORDER_POC_IMMEDIATE': OrderImmediatelyFillable, # {"label":"ORDER_POC_IMMEDIATE","detail":"order price 1700 while counter price 1793.55"} + 'INCREASE_POSITION': InvalidOrder, + 'CONTRACT_IN_DELISTING': ExchangeError, + 'INTERNAL': ExchangeNotAvailable, + 'SERVER_ERROR': ExchangeNotAvailable, + 'TOO_BUSY': ExchangeNotAvailable, + 'CROSS_ACCOUNT_NOT_FOUND': ExchangeError, + 'RISK_LIMIT_TOO_LOW': BadRequest, # {"label":"RISK_LIMIT_TOO_LOW","detail":"limit 1000000"} + 'AUTO_TRIGGER_PRICE_LESS_LAST': InvalidOrder, # {"label":"AUTO_TRIGGER_PRICE_LESS_LAST","message":"invalid argument: Trigger.Price must < last_price"} + 'AUTO_TRIGGER_PRICE_GREATE_LAST': InvalidOrder, # {"label":"AUTO_TRIGGER_PRICE_GREATE_LAST","message":"invalid argument: Trigger.Price must > last_price"} + 'POSITION_HOLDING': BadRequest, + 'USER_LOAN_EXCEEDED': BadRequest, # {"label":"USER_LOAN_EXCEEDED","message":"Max loan amount per user would be exceeded"} + }, + 'broad': {}, + }, + }) + + def set_sandbox_mode(self, enable: bool): + super(gate, self).set_sandbox_mode(enable) + self.options['sandboxMode'] = enable + + def load_unified_status(self, params={}): + """ + :param dict [params]: extra parameters specific to the exchange API endpoint + returns unifiedAccount so the user can check if the unified account is enabled + + https://www.gate.com/docs/developers/apiv4/#get-account-detail + + :returns boolean: True or False if the enabled unified account is enabled or not and sets the unifiedAccount option if it is None + """ + unifiedAccount = self.safe_bool(self.options, 'unifiedAccount') + if unifiedAccount is None: + try: + # + # { + # "user_id": 10406147, + # "ip_whitelist": [], + # "currency_pairs": [], + # "key": { + # "mode": 1 + # }, + # "tier": 0, + # "tier_expire_time": "0001-01-01T00:00:00Z", + # "copy_trading_role": 0 + # } + # + response = self.privateAccountGetDetail(params) + result = self.safe_dict(response, 'key', {}) + self.options['unifiedAccount'] = self.safe_integer(result, 'mode') == 2 + except Exception as e: + # if the request fails, the unifiedAccount is disabled + self.options['unifiedAccount'] = False + return self.options['unifiedAccount'] + + def upgrade_unified_trade_account(self, params={}): + return self.privateUnifiedPutUnifiedMode(params) + + def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://www.gate.com/docs/developers/apiv4/en/#get-server-current-time + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = self.publicSpotGetTime(params) + # + # { + # "server_time": 1731447921098 + # } + # + return self.safe_integer(response, 'server_time') + + def create_expired_option_market(self, symbol: str): + # support expired option contracts + quote = 'USDT' + settle = quote + optionParts = symbol.split('-') + symbolBase = symbol.split('/') + marketIdBase = symbol.split('_') + base = None + expiry = self.safe_string(optionParts, 1) + if symbol.find('/') > -1: + base = self.safe_string(symbolBase, 0) + else: + base = self.safe_string(marketIdBase, 0) + expiry = expiry[2:8] # convert 20230728 to 230728 + strike = self.safe_string(optionParts, 2) + optionType = self.safe_string(optionParts, 3) + datetime = self.convert_expire_date(expiry) + timestamp = self.parse8601(datetime) + return { + 'id': base + '_' + quote + '-' + '20' + expiry + '-' + strike + '-' + optionType, + 'symbol': base + '/' + quote + ':' + settle + '-' + expiry + '-' + strike + '-' + optionType, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': base, + 'quoteId': quote, + 'settleId': settle, + 'active': False, + 'type': 'option', + 'linear': None, + 'inverse': None, + 'spot': False, + 'swap': False, + 'future': False, + 'option': True, + 'margin': False, + 'contract': True, + 'contractSize': self.parse_number('1'), + 'expiry': timestamp, + 'expiryDatetime': datetime, + 'optionType': 'call' if (optionType == 'C') else 'put', + 'strike': self.parse_number(strike), + 'precision': { + 'amount': self.parse_number('1'), + 'price': None, + }, + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'info': None, + } + + def safe_market(self, marketId: Str = None, market: Market = None, delimiter: Str = None, marketType: Str = None) -> MarketInterface: + isOption = (marketId is not None) and ((marketId.find('-C') > -1) or (marketId.find('-P') > -1)) + if isOption and not (marketId in self.markets_by_id): + # handle expired option contracts + return self.create_expired_option_market(marketId) + return super(gate, self).safe_market(marketId, market, delimiter, marketType) + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for gate + + https://www.gate.com/docs/developers/apiv4/en/#list-all-currency-pairs-supported # spot + https://www.gate.com/docs/developers/apiv4/en/#list-all-supported-currency-pairs-supported-in-margin-trading # margin + https://www.gate.com/docs/developers/apiv4/en/#list-all-futures-contracts # swap + https://www.gate.com/docs/developers/apiv4/en/#list-all-futures-contracts-2 # future + https://www.gate.com/docs/developers/apiv4/en/#list-all-the-contracts-with-specified-underlying-and-expiration-time # option + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + if self.options['adjustForTimeDifference']: + self.load_time_difference() + if self.check_required_credentials(False): + self.load_unified_status() + rawPromises = [] + fetchMarketsOptions = self.safe_dict(self.options, 'fetchMarkets') + types = self.safe_list(fetchMarketsOptions, 'types', ['spot', 'swap', 'future', 'option']) + for i in range(0, len(types)): + marketType = types[i] + if marketType == 'spot': + # if not sandboxMode: + # gate doesn't have a sandbox for spot markets + rawPromises.append(self.fetch_spot_markets(params)) + # } + elif marketType == 'swap': + rawPromises.append(self.fetch_swap_markets(params)) + elif marketType == 'future': + rawPromises.append(self.fetch_future_markets(params)) + elif marketType == 'option': + rawPromises.append(self.fetch_option_markets(params)) + results = rawPromises + return self.arrays_concat(results) + + def fetch_spot_markets(self, params={}): + marginPromise = self.publicMarginGetCurrencyPairs(params) + spotMarketsPromise = self.publicSpotGetCurrencyPairs(params) + marginResponse, spotMarketsResponse = [marginPromise, spotMarketsPromise] + marginMarkets = self.index_by(marginResponse, 'id') + # + # Spot + # + # [ + # { + # "id": "QTUM_ETH", + # "base": "QTUM", + # "base_name": "Quantum", + # "quote": "ETH", + # "quote_name": "Ethereum", + # "fee": "0.2", + # "min_base_amount": "0.01", + # "min_quote_amount": "0.001", + # "max_quote_amount": "50000", + # "amount_precision": 3, + # "precision": 6, + # "trade_status": "tradable", + # "sell_start": 1607313600, + # "buy_start": 1700492400, + # "type": "normal", + # "trade_url": "https://www.gate.com/trade/QTUM_ETH", + # } + # + # Margin + # + # [ + # { + # "id": "ETH_USDT", + # "base": "ETH", + # "quote": "USDT", + # "leverage": 3, + # "min_base_amount": "0.01", + # "min_quote_amount": "100", + # "max_quote_amount": "1000000" + # } + # ] + # + result = [] + for i in range(0, len(spotMarketsResponse)): + spotMarket = spotMarketsResponse[i] + id = self.safe_string(spotMarket, 'id') + marginMarket = self.safe_value(marginMarkets, id) + market = self.deep_extend(marginMarket, spotMarket) + baseId, quoteId = id.split('_') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + takerPercent = self.safe_string(market, 'fee') + makerPercent = self.safe_string(market, 'maker_fee_rate', takerPercent) + amountPrecision = self.parse_number(self.parse_precision(self.safe_string(market, 'amount_precision'))) + tradeStatus = self.safe_string(market, 'trade_status') + leverage = self.safe_number(market, 'leverage') + margin = leverage is not None + buyStart = self.safe_integer_product(spotMarket, 'buy_start', 1000) # buy_start is the trading start time, while sell_start is offline orders start time + createdTs = buyStart if (buyStart != 0) else None + result.append({ + 'id': id, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': margin, + 'swap': False, + 'future': False, + 'option': False, + 'active': (tradeStatus == 'tradable'), + 'contract': False, + 'linear': None, + 'inverse': None, + # Fee is in %, so divide by 100 + 'taker': self.parse_number(Precise.string_div(takerPercent, '100')), + 'maker': self.parse_number(Precise.string_div(makerPercent, '100')), + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': amountPrecision, + 'price': self.parse_number(self.parse_precision(self.safe_string(market, 'precision'))), + }, + 'limits': { + 'leverage': { + 'min': self.parse_number('1'), + 'max': self.safe_number(market, 'leverage', 1), + }, + 'amount': { + 'min': self.safe_number(spotMarket, 'min_base_amount', amountPrecision), + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': self.safe_number(market, 'min_quote_amount'), + 'max': self.safe_number(market, 'max_quote_amount') if margin else None, + }, + }, + 'created': createdTs, + 'info': market, + }) + return result + + def fetch_swap_markets(self, params={}): + result = [] + swapSettlementCurrencies = self.get_settlement_currencies('swap', 'fetchMarkets') + if self.options['sandboxMode']: + swapSettlementCurrencies = ['usdt'] # gate sandbox only has usdt-margined swaps + for c in range(0, len(swapSettlementCurrencies)): + settleId = swapSettlementCurrencies[c] + request: dict = { + 'settle': settleId, + } + response = self.publicFuturesGetSettleContracts(self.extend(request, params)) + for i in range(0, len(response)): + parsedMarket = self.parse_contract_market(response[i], settleId) + result.append(parsedMarket) + return result + + def fetch_future_markets(self, params={}): + if self.options['sandboxMode']: + return [] # right now sandbox does not have inverse swaps + result = [] + futureSettlementCurrencies = self.get_settlement_currencies('future', 'fetchMarkets') + for c in range(0, len(futureSettlementCurrencies)): + settleId = futureSettlementCurrencies[c] + request: dict = { + 'settle': settleId, + } + response = self.publicDeliveryGetSettleContracts(self.extend(request, params)) + for i in range(0, len(response)): + parsedMarket = self.parse_contract_market(response[i], settleId) + result.append(parsedMarket) + return result + + def parse_contract_market(self, market, settleId): + # + # Perpetual swap + # + # { + # "name": "BTC_USDT", + # "type": "direct", + # "quanto_multiplier": "0.0001", + # "ref_discount_rate": "0", + # "order_price_deviate": "0.5", + # "maintenance_rate": "0.005", + # "mark_type": "index", + # "last_price": "38026", + # "mark_price": "37985.6", + # "index_price": "37954.92", + # "funding_rate_indicative": "0.000219", + # "mark_price_round": "0.01", + # "funding_offset": 0, + # "in_delisting": False, + # "risk_limit_base": "1000000", + # "interest_rate": "0.0003", + # "order_price_round": "0.1", + # "order_size_min": 1, + # "ref_rebate_rate": "0.2", + # "funding_interval": 28800, + # "risk_limit_step": "1000000", + # "leverage_min": "1", + # "leverage_max": "100", + # "risk_limit_max": "8000000", + # "maker_fee_rate": "-0.00025", # not actual value for regular users + # "taker_fee_rate": "0.00075", # not actual value for regular users + # "funding_rate": "0.002053", + # "order_size_max": 1000000, + # "funding_next_apply": 1610035200, + # "short_users": 977, + # "config_change_time": 1609899548, + # "create_time": 1609800048, + # "trade_size": 28530850594, + # "position_size": 5223816, + # "long_users": 455, + # "funding_impact_value": "60000", + # "orders_limit": 50, + # "trade_id": 10851092, + # "orderbook_id": 2129638396 + # } + # + # Delivery Futures + # + # { + # "name": "BTC_USDT_20200814", + # "underlying": "BTC_USDT", + # "cycle": "WEEKLY", + # "type": "direct", + # "quanto_multiplier": "0.0001", + # "mark_type": "index", + # "last_price": "9017", + # "mark_price": "9019", + # "index_price": "9005.3", + # "basis_rate": "0.185095", + # "basis_value": "13.7", + # "basis_impact_value": "100000", + # "settle_price": "0", + # "settle_price_interval": 60, + # "settle_price_duration": 1800, + # "settle_fee_rate": "0.0015", + # "expire_time": 1593763200, + # "order_price_round": "0.1", + # "mark_price_round": "0.1", + # "leverage_min": "1", + # "leverage_max": "100", + # "maintenance_rate": "1000000", + # "risk_limit_base": "140.726652109199", + # "risk_limit_step": "1000000", + # "risk_limit_max": "8000000", + # "maker_fee_rate": "-0.00025", # not actual value for regular users + # "taker_fee_rate": "0.00075", # not actual value for regular users + # "ref_discount_rate": "0", + # "ref_rebate_rate": "0.2", + # "order_price_deviate": "0.5", + # "order_size_min": 1, + # "order_size_max": 1000000, + # "orders_limit": 50, + # "orderbook_id": 63, + # "trade_id": 26, + # "trade_size": 435, + # "position_size": 130, + # "config_change_time": 1593158867, + # "in_delisting": False + # } + # + id = self.safe_string(market, 'name') + parts = id.split('_') + baseId = self.safe_string(parts, 0) + quoteId = self.safe_string(parts, 1) + date = self.safe_string(parts, 2) + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + settle = self.safe_currency_code(settleId) + expiry = self.safe_timestamp(market, 'expire_time') + symbol = '' + marketType = 'swap' + if date is not None: + symbol = base + '/' + quote + ':' + settle + '-' + self.yymmdd(expiry, '') + marketType = 'future' + else: + symbol = base + '/' + quote + ':' + settle + priceDeviate = self.safe_string(market, 'order_price_deviate') + markPrice = self.safe_string(market, 'mark_price') + minMultiplier = Precise.string_sub('1', priceDeviate) + maxMultiplier = Precise.string_add('1', priceDeviate) + minPrice = Precise.string_mul(minMultiplier, markPrice) + maxPrice = Precise.string_mul(maxMultiplier, markPrice) + isLinear = quote == settle + contractSize = self.safe_string(market, 'quanto_multiplier') + # exception only for one market: https://api.gateio.ws/api/v4/futures/btc/contracts + if contractSize == '0': + contractSize = '1' # 1 USD in WEB: https://i.imgur.com/MBBUI04.png + return { + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': marketType, + 'spot': False, + 'margin': False, + 'swap': marketType == 'swap', + 'future': marketType == 'future', + 'option': marketType == 'option', + 'active': True, + 'contract': True, + 'linear': isLinear, + 'inverse': not isLinear, + 'taker': None, + 'maker': None, + 'contractSize': self.parse_number(contractSize), + 'expiry': expiry, + 'expiryDatetime': self.iso8601(expiry), + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number('1'), # all contracts have self step size + 'price': self.safe_number(market, 'order_price_round'), + }, + 'limits': { + 'leverage': { + 'min': self.safe_number(market, 'leverage_min'), + 'max': self.safe_number(market, 'leverage_max'), + }, + 'amount': { + 'min': self.safe_number(market, 'order_size_min'), + 'max': self.safe_number(market, 'order_size_max'), + }, + 'price': { + 'min': self.parse_number(minPrice), + 'max': self.parse_number(maxPrice), + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': self.safe_integer_product(market, 'create_time', 1000), + 'info': market, + } + + def fetch_option_markets(self, params={}): + result = [] + underlyings = self.fetch_option_underlyings() + for i in range(0, len(underlyings)): + underlying = underlyings[i] + query = self.extend({}, params) + query['underlying'] = underlying + response = self.publicOptionsGetContracts(query) + # + # [ + # { + # "orders_limit": "50", + # "order_size_max": "100000", + # "mark_price_round": "0.1", + # "order_size_min": "1", + # "position_limit": "1000000", + # "orderbook_id": "575967", + # "order_price_deviate": "0.9", + # "is_call": True, # True means Call False means Put + # "last_price": "93.9", + # "bid1_size": "0", + # "bid1_price": "0", + # "taker_fee_rate": "0.0004", + # "underlying": "BTC_USDT", + # "create_time": "1646381188", + # "price_limit_fee_rate": "0.1", + # "maker_fee_rate": "0.0004", + # "trade_id": "727", + # "order_price_round": "0.1", + # "settle_fee_rate": "0.0001", + # "trade_size": "1982", + # "ref_rebate_rate": "0", + # "name": "BTC_USDT-20220311-44000-C", + # "underlying_price": "39194.26", + # "strike_price": "44000", + # "multiplier": "0.0001", + # "ask1_price": "0", + # "ref_discount_rate": "0", + # "expiration_time": "1646985600", + # "mark_price": "12.15", + # "position_size": "4", + # "ask1_size": "0", + # "tag": "WEEK" + # } + # ] + # + for j in range(0, len(response)): + market = response[j] + id = self.safe_string(market, 'name') + parts = underlying.split('_') + baseId = self.safe_string(parts, 0) + quoteId = self.safe_string(parts, 1) + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + symbol = base + '/' + quote + expiry = self.safe_timestamp(market, 'expiration_time') + strike = self.safe_string(market, 'strike_price') + isCall = self.safe_value(market, 'is_call') + optionLetter = 'C' if isCall else 'P' + optionType = 'call' if isCall else 'put' + symbol = symbol + ':' + quote + '-' + self.yymmdd(expiry) + '-' + strike + '-' + optionLetter + priceDeviate = self.safe_string(market, 'order_price_deviate') + markPrice = self.safe_string(market, 'mark_price') + minMultiplier = Precise.string_sub('1', priceDeviate) + maxMultiplier = Precise.string_add('1', priceDeviate) + minPrice = Precise.string_mul(minMultiplier, markPrice) + maxPrice = Precise.string_mul(maxMultiplier, markPrice) + result.append({ + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': quote, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': quoteId, + 'type': 'option', + 'spot': False, + 'margin': False, + 'swap': False, + 'future': False, + 'option': True, + 'active': True, + 'contract': True, + 'linear': True, + 'inverse': False, + 'taker': None, + 'maker': None, + 'contractSize': self.parse_number('1'), + 'expiry': expiry, + 'expiryDatetime': self.iso8601(expiry), + 'strike': self.parse_number(strike), + 'optionType': optionType, + 'precision': { + 'amount': self.parse_number('1'), # all options have self step size + 'price': self.safe_number(market, 'order_price_round'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(market, 'order_size_min'), + 'max': self.safe_number(market, 'order_size_max'), + }, + 'price': { + 'min': self.parse_number(minPrice), + 'max': self.parse_number(maxPrice), + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': self.safe_timestamp(market, 'create_time'), + 'info': market, + }) + return result + + def fetch_option_underlyings(self): + underlyingsResponse = self.publicOptionsGetUnderlyings() + # + # [ + # { + # "index_time": "1646915796", + # "name": "BTC_USDT", + # "index_price": "39142.73" + # } + # ] + # + underlyings = [] + for i in range(0, len(underlyingsResponse)): + underlying = underlyingsResponse[i] + name = self.safe_string(underlying, 'name') + if name is not None: + underlyings.append(name) + return underlyings + + def prepare_request(self, market=None, type=None, params={}): + """ + @ignore + Fills request params contract, settle, currency_pair, market and account where applicable + :param dict market: CCXT market, required when type is None + :param str type: 'spot', 'swap', or 'future', required when market is None + :param dict [params]: request parameters + :returns: the api request object, and the new params object with non-needed parameters removed + """ + # * Do not call for multi spot order methods like cancelAllOrders and fetchOpenOrders. Use multiOrderSpotPrepareRequest instead + request: dict = {} + if market is not None: + if market['contract']: + request['contract'] = market['id'] + if not market['option']: + request['settle'] = market['settleId'] + else: + request['currency_pair'] = market['id'] + else: + swap = type == 'swap' + future = type == 'future' + if swap or future: + defaultSettle = 'usdt' if swap else 'btc' + settle = self.safe_string_lower(params, 'settle', defaultSettle) + params = self.omit(params, 'settle') + request['settle'] = settle + return [request, params] + + def spot_order_prepare_request(self, market=None, trigger=False, params={}): + """ + @ignore + Fills request params currency_pair, market and account where applicable for spot order methods like fetchOpenOrders, cancelAllOrders + :param dict market: CCXT market + :param bool trigger: True if for a trigger order + :param dict [params]: request parameters + :returns: the api request object, and the new params object with non-needed parameters removed + """ + marginMode, query = self.get_margin_mode(trigger, params) + request: dict = {} + if not trigger: + if market is None: + raise ArgumentsRequired(self.id + ' spotOrderPrepareRequest() requires a market argument for non-trigger orders') + request['account'] = marginMode + request['currency_pair'] = market['id'] # Should always be set for non-trigger + return [request, query] + + def multi_order_spot_prepare_request(self, market=None, trigger=False, params={}): + """ + @ignore + Fills request params currency_pair, market and account where applicable for spot order methods like fetchOpenOrders, cancelAllOrders + :param dict market: CCXT market + :param bool trigger: True if for a trigger order + :param dict [params]: request parameters + :returns: the api request object, and the new params object with non-needed parameters removed + """ + marginMode, query = self.get_margin_mode(trigger, params) + request: dict = { + 'account': marginMode, + } + if market is not None: + if trigger: + # gate spot and margin trigger orders use the term market instead of currency_pair, and normal instead of spot. Neither parameter is used when fetching/cancelling a single order. They are used for creating a single trigger order, but createOrder does not call self method + request['market'] = market['id'] + else: + request['currency_pair'] = market['id'] + return [request, query] + + def get_margin_mode(self, trigger, params): + """ + @ignore + Gets the margin type for self api call + :param bool trigger: True if for a trigger order + :param dict [params]: Request params + :returns: The marginMode and the updated request params with marginMode removed, marginMode value is the value that can be read by the "account" property specified in gates api docs + """ + defaultMarginMode = self.safe_string_lower_2(self.options, 'defaultMarginMode', 'marginMode', 'spot') # 'margin' is isolated margin on gate's api + marginMode = self.safe_string_lower_2(params, 'marginMode', 'account', defaultMarginMode) + params = self.omit(params, ['marginMode', 'account']) + if marginMode == 'cross': + marginMode = 'cross_margin' + elif marginMode == 'isolated': + marginMode = 'margin' + elif marginMode == '': + marginMode = 'spot' + if trigger: + if marginMode == 'spot': + # gate spot trigger orders use the term normal instead of spot + marginMode = 'normal' + if marginMode == 'cross_margin': + raise BadRequest(self.id + ' getMarginMode() does not support trigger orders for cross margin') + isUnifiedAccount = False + isUnifiedAccount, params = self.handle_option_and_params(params, 'getMarginMode', 'unifiedAccount') + if isUnifiedAccount: + marginMode = 'unified' + return [marginMode, params] + + def get_settlement_currencies(self, type, method): + options = self.safe_value(self.options, type, {}) # ['BTC', 'USDT'] unified codes + fetchMarketsContractOptions = self.safe_value(options, method, {}) + defaultSettle = ['usdt'] if (type == 'swap') else ['btc'] + return self.safe_value(fetchMarketsContractOptions, 'settlementCurrencies', defaultSettle) + + def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://www.gate.com/docs/developers/apiv4/en/#list-all-currencies-details + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + # sandbox/testnet only supports future markets + apiBackup = self.safe_value(self.urls, 'apiBackup') + if apiBackup is not None: + return {} + response = self.publicSpotGetCurrencies(params) + # + # [ + # { + # "currency": "USDT", + # "name": "Tether", + # "delisted": False, + # "withdraw_disabled": False, + # "withdraw_delayed": False, + # "deposit_disabled": False, + # "trade_disabled": False, + # "fixed_rate": "", + # "chain": "ETH", + # "chains": [ + # { + # "name": "ETH", + # "addr": "0xdAC17F958D2ee523a2206206994597C13D831ec7", + # "withdraw_disabled": False, + # "withdraw_delayed": False, + # "deposit_disabled": False + # }, + # { + # "name": "ARBEVM", + # "addr": "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", + # "withdraw_disabled": False, + # "withdraw_delayed": False, + # "deposit_disabled": False + # }, + # { + # "name": "BSC", + # "addr": "0x55d398326f99059fF775485246999027B3197955", + # "withdraw_disabled": False, + # "withdraw_delayed": False, + # "deposit_disabled": False + # }, + # ] + # }, + # ] + # + indexedCurrencies = self.index_by(response, 'currency') + result: dict = {} + for i in range(0, len(response)): + entry = response[i] + currencyId = self.safe_string(entry, 'currency') + code = self.safe_currency_code(currencyId) + # check leveraged tokens(e.g. BTC3S, ETH5L) + type = 'leveraged' if self.is_leveraged_currency(currencyId, True, indexedCurrencies) else 'crypto' + chains = self.safe_list(entry, 'chains', []) + networks = {} + for j in range(0, len(chains)): + chain = chains[j] + networkId = self.safe_string(chain, 'name') + networkCode = self.network_id_to_code(networkId) + networks[networkCode] = { + 'info': chain, + 'id': networkId, + 'network': networkCode, + 'active': None, + 'deposit': not self.safe_bool(chain, 'deposit_disabled'), + 'withdraw': not self.safe_bool(chain, 'withdraw_disabled'), + 'fee': None, + 'precision': self.parse_number('0.0001'), # temporary safe default, because no value provided from API, + 'limits': { + 'deposit': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + }, + } + result[code] = self.safe_currency_structure({ + 'id': currencyId, + 'code': code, + 'name': self.safe_string(entry, 'name'), + 'type': type, + 'active': not self.safe_bool(entry, 'delisted'), + 'deposit': not self.safe_bool(entry, 'deposit_disabled'), + 'withdraw': not self.safe_bool(entry, 'withdraw_disabled'), + 'fee': None, + 'networks': networks, + 'precision': self.parse_number('0.0001'), + 'info': entry, + }) + return result + + def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate + + https://www.gate.com/docs/developers/apiv4/en/#get-a-single-contract + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + self.load_markets() + market = self.market(symbol) + if not market['swap']: + raise BadSymbol(self.id + ' fetchFundingRate() supports swap contracts only') + request, query = self.prepare_request(market, None, params) + response = self.publicFuturesGetSettleContractsContract(self.extend(request, query)) + # + # [ + # { + # "name": "BTC_USDT", + # "type": "direct", + # "quanto_multiplier": "0.0001", + # "ref_discount_rate": "0", + # "order_price_deviate": "0.5", + # "maintenance_rate": "0.005", + # "mark_type": "index", + # "last_price": "38026", + # "mark_price": "37985.6", + # "index_price": "37954.92", + # "funding_rate_indicative": "0.000219", + # "mark_price_round": "0.01", + # "funding_offset": 0, + # "in_delisting": False, + # "risk_limit_base": "1000000", + # "interest_rate": "0.0003", + # "order_price_round": "0.1", + # "order_size_min": 1, + # "ref_rebate_rate": "0.2", + # "funding_interval": 28800, + # "risk_limit_step": "1000000", + # "leverage_min": "1", + # "leverage_max": "100", + # "risk_limit_max": "8000000", + # "maker_fee_rate": "-0.00025", + # "taker_fee_rate": "0.00075", + # "funding_rate": "0.002053", + # "order_size_max": 1000000, + # "funding_next_apply": 1610035200, + # "short_users": 977, + # "config_change_time": 1609899548, + # "trade_size": 28530850594, + # "position_size": 5223816, + # "long_users": 455, + # "funding_impact_value": "60000", + # "orders_limit": 50, + # "trade_id": 10851092, + # "orderbook_id": 2129638396 + # } + # ] + # + return self.parse_funding_rate(response) + + def fetch_funding_rates(self, symbols: Strings = None, params={}) -> FundingRates: + """ + fetch the funding rate for multiple markets + + https://www.gate.com/docs/developers/apiv4/en/#list-all-futures-contracts + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `funding rate structures `, indexed by market symbols + """ + self.load_markets() + symbols = self.market_symbols(symbols) + market = None + if symbols is not None: + firstSymbol = self.safe_string(symbols, 0) + market = self.market(firstSymbol) + request, query = self.prepare_request(market, 'swap', params) + response = self.publicFuturesGetSettleContracts(self.extend(request, query)) + # + # [ + # { + # "name": "BTC_USDT", + # "type": "direct", + # "quanto_multiplier": "0.0001", + # "ref_discount_rate": "0", + # "order_price_deviate": "0.5", + # "maintenance_rate": "0.005", + # "mark_type": "index", + # "last_price": "38026", + # "mark_price": "37985.6", + # "index_price": "37954.92", + # "funding_rate_indicative": "0.000219", + # "mark_price_round": "0.01", + # "funding_offset": 0, + # "in_delisting": False, + # "risk_limit_base": "1000000", + # "interest_rate": "0.0003", + # "order_price_round": "0.1", + # "order_size_min": 1, + # "ref_rebate_rate": "0.2", + # "funding_interval": 28800, + # "risk_limit_step": "1000000", + # "leverage_min": "1", + # "leverage_max": "100", + # "risk_limit_max": "8000000", + # "maker_fee_rate": "-0.00025", + # "taker_fee_rate": "0.00075", + # "funding_rate": "0.002053", + # "order_size_max": 1000000, + # "funding_next_apply": 1610035200, + # "short_users": 977, + # "config_change_time": 1609899548, + # "trade_size": 28530850594, + # "position_size": 5223816, + # "long_users": 455, + # "funding_impact_value": "60000", + # "orders_limit": 50, + # "trade_id": 10851092, + # "orderbook_id": 2129638396 + # } + # ] + # + return self.parse_funding_rates(response, symbols) + + def parse_funding_rate(self, contract, market: Market = None) -> FundingRate: + # + # { + # "name": "BTC_USDT", + # "type": "direct", + # "quanto_multiplier": "0.0001", + # "ref_discount_rate": "0", + # "order_price_deviate": "0.5", + # "maintenance_rate": "0.005", + # "mark_type": "index", + # "last_price": "38026", + # "mark_price": "37985.6", + # "index_price": "37954.92", + # "funding_rate_indicative": "0.000219", + # "mark_price_round": "0.01", + # "funding_offset": 0, + # "in_delisting": False, + # "risk_limit_base": "1000000", + # "interest_rate": "0.0003", + # "order_price_round": "0.1", + # "order_size_min": 1, + # "ref_rebate_rate": "0.2", + # "funding_interval": 28800, + # "risk_limit_step": "1000000", + # "leverage_min": "1", + # "leverage_max": "100", + # "risk_limit_max": "8000000", + # "maker_fee_rate": "-0.00025", + # "taker_fee_rate": "0.00075", + # "funding_rate": "0.002053", + # "order_size_max": 1000000, + # "funding_next_apply": 1610035200, + # "short_users": 977, + # "config_change_time": 1609899548, + # "trade_size": 28530850594, + # "position_size": 5223816, + # "long_users": 455, + # "funding_impact_value": "60000", + # "orders_limit": 50, + # "trade_id": 10851092, + # "orderbook_id": 2129638396 + # } + # + marketId = self.safe_string(contract, 'name') + symbol = self.safe_symbol(marketId, market, '_', 'swap') + markPrice = self.safe_number(contract, 'mark_price') + indexPrice = self.safe_number(contract, 'index_price') + interestRate = self.safe_number(contract, 'interest_rate') + fundingRate = self.safe_number(contract, 'funding_rate') + fundingTime = self.safe_timestamp(contract, 'funding_next_apply') + fundingRateIndicative = self.safe_number(contract, 'funding_rate_indicative') + fundingInterval = Precise.string_mul('1000', self.safe_string(contract, 'funding_interval')) + return { + 'info': contract, + 'symbol': symbol, + 'markPrice': markPrice, + 'indexPrice': indexPrice, + 'interestRate': interestRate, + 'estimatedSettlePrice': None, + 'timestamp': None, + 'datetime': None, + 'fundingRate': fundingRate, + 'fundingTimestamp': fundingTime, + 'fundingDatetime': self.iso8601(fundingTime), + 'nextFundingRate': fundingRateIndicative, + 'nextFundingTimestamp': None, + 'nextFundingDatetime': None, + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': self.parse_funding_interval(fundingInterval), + } + + def parse_funding_interval(self, interval): + intervals: dict = { + '3600000': '1h', + '14400000': '4h', + '28800000': '8h', + '57600000': '16h', + '86400000': '24h', + } + return self.safe_string(intervals, interval, interval) + + def fetch_network_deposit_address(self, code: str, params={}): + self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], # todo: currencies have network-junctions + } + response = self.privateWalletGetDepositAddress(self.extend(request, params)) + addresses = self.safe_value(response, 'multichain_addresses') + currencyId = self.safe_string(response, 'currency') + code = self.safe_currency_code(currencyId) + result: dict = {} + for i in range(0, len(addresses)): + entry = addresses[i] + # + # { + # "chain": "ETH", + # "address": "0x359a697945E79C7e17b634675BD73B33324E9408", + # "payment_id": "", + # "payment_name": "", + # "obtain_failed": "0" + # } + # + obtainFailed = self.safe_integer(entry, 'obtain_failed') + if obtainFailed: + continue + network = self.safe_string(entry, 'chain') + address = self.safe_string(entry, 'address') + tag = self.safe_string(entry, 'payment_id') + result[network] = { + 'info': entry, + 'code': code, # kept here for backward-compatibility, but will be removed soon + 'currency': code, + 'address': address, + 'tag': tag, + } + return result + + def fetch_deposit_addresses_by_network(self, code: str, params={}) -> List[DepositAddress]: + """ + fetch a dictionary of addresses for a currency, indexed by network + :param str code: unified currency code of the currency for the deposit address + :param dict [params]: extra parameters specific to the api endpoint + :returns dict: a dictionary of `address structures ` indexed by the network + """ + self.load_markets() + currency = self.currency(code) + request = { + 'currency': currency['id'], + } + response = self.privateWalletGetDepositAddress(self.extend(request, params)) + chains = self.safe_value(response, 'multichain_addresses', []) + currencyId = self.safe_string(response, 'currency') + currency = self.safe_currency(currencyId, currency) + parsed = self.parse_deposit_addresses(chains, None, False) + return self.index_by(parsed, 'network') + + def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://www.gate.com/docs/developers/apiv4/en/#generate-currency-deposit-address + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.network]: unified network code(not used directly by gate.com but used by ccxt to filter the response) + :returns dict: an `address structure ` + """ + self.load_markets() + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + chainsIndexedById = self.fetch_deposit_addresses_by_network(code, params) + selectedNetworkIdOrCode = self.select_network_code_from_unified_networks(code, networkCode, chainsIndexedById) + return chainsIndexedById[selectedNetworkIdOrCode] + + def parse_deposit_address(self, depositAddress, currency=None): + # + # { + # chain: "BTC", + # address: "1Nxu.......Ys", + # payment_id: "", + # payment_name: "", + # obtain_failed: "0", + # } + # + address = self.safe_string(depositAddress, 'address') + self.check_address(address) + return { + 'info': depositAddress, + 'currency': self.safe_string(currency, 'code'), + 'address': address, + 'tag': self.safe_string(depositAddress, 'payment_id'), + 'network': self.network_id_to_code(self.safe_string(depositAddress, 'chain')), + } + + def fetch_trading_fee(self, symbol: str, params={}) -> TradingFeeInterface: + """ + fetch the trading fees for a market + + https://www.gate.com/docs/developers/apiv4/en/#retrieve-personal-trading-fee + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `fee structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'currency_pair': market['id'], + } + response = self.privateWalletGetFee(self.extend(request, params)) + # + # { + # "user_id": 1486602, + # "taker_fee": "0.002", + # "maker_fee": "0.002", + # "gt_discount": True, + # "gt_taker_fee": "0.0015", + # "gt_maker_fee": "0.0015", + # "loan_fee": "0.18", + # "point_type": "0", + # "futures_taker_fee": "0.0005", + # "futures_maker_fee": "0" + # } + # + return self.parse_trading_fee(response, market) + + def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://www.gate.com/docs/developers/apiv4/en/#retrieve-personal-trading-fee + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + self.load_markets() + response = self.privateWalletGetFee(params) + # + # { + # "user_id": 1486602, + # "taker_fee": "0.002", + # "maker_fee": "0.002", + # "gt_discount": True, + # "gt_taker_fee": "0.0015", + # "gt_maker_fee": "0.0015", + # "loan_fee": "0.18", + # "point_type": "0", + # "futures_taker_fee": "0.0005", + # "futures_maker_fee": "0" + # } + # + return self.parse_trading_fees(response) + + def parse_trading_fees(self, response): + result: dict = {} + for i in range(0, len(self.symbols)): + symbol = self.symbols[i] + market = self.market(symbol) + result[symbol] = self.parse_trading_fee(response, market) + return result + + def parse_trading_fee(self, info, market: Market = None): + # + # { + # "user_id": 1486602, + # "taker_fee": "0.002", + # "maker_fee": "0.002", + # "gt_discount": True, + # "gt_taker_fee": "0.0015", + # "gt_maker_fee": "0.0015", + # "loan_fee": "0.18", + # "point_type": "0", + # "futures_taker_fee": "0.0005", + # "futures_maker_fee": "0" + # } + # + gtDiscount = self.safe_value(info, 'gt_discount') + taker = 'gt_taker_fee' if gtDiscount else 'taker_fee' + maker = 'gt_maker_fee' if gtDiscount else 'maker_fee' + contract = self.safe_value(market, 'contract') + takerKey = 'futures_taker_fee' if contract else taker + makerKey = 'futures_maker_fee' if contract else maker + return { + 'info': info, + 'symbol': self.safe_string(market, 'symbol'), + 'maker': self.safe_number(info, makerKey), + 'taker': self.safe_number(info, takerKey), + 'percentage': None, + 'tierBased': None, + } + + def fetch_transaction_fees(self, codes: Strings = None, params={}): + """ + @deprecated + please use fetchDepositWithdrawFees instead + + https://www.gate.com/docs/developers/apiv4/en/#retrieve-withdrawal-status + + :param str[]|None codes: list of unified currency codes + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `fee structures ` + """ + self.load_markets() + response = self.privateWalletGetWithdrawStatus(params) + # + # { + # "currency": "MTN", + # "name": "Medicalchain", + # "name_cn": "Medicalchain", + # "deposit": "0", + # "withdraw_percent": "0%", + # "withdraw_fix": "900", + # "withdraw_day_limit": "500000", + # "withdraw_day_limit_remain": "500000", + # "withdraw_amount_mini": "900.1", + # "withdraw_eachtime_limit": "90000000000", + # "withdraw_fix_on_chains": { + # "ETH": "900" + # } + # } + # + result: dict = {} + withdrawFees = {} + for i in range(0, len(response)): + withdrawFees = {} + entry = response[i] + currencyId = self.safe_string(entry, 'currency') + code = self.safe_currency_code(currencyId) + if (codes is not None) and not self.in_array(code, codes): + continue + withdrawFixOnChains = self.safe_value(entry, 'withdraw_fix_on_chains') + if withdrawFixOnChains is None: + withdrawFees = self.safe_number(entry, 'withdraw_fix') + else: + networkIds = list(withdrawFixOnChains.keys()) + for j in range(0, len(networkIds)): + networkId = networkIds[j] + networkCode = self.network_id_to_code(networkId) + withdrawFees[networkCode] = self.parse_number(withdrawFixOnChains[networkId]) + result[code] = { + 'withdraw': withdrawFees, + 'deposit': None, + 'info': entry, + } + return result + + def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}): + """ + fetch deposit and withdraw fees + + https://www.gate.com/docs/developers/apiv4/en/#retrieve-withdrawal-status + + :param str[]|None codes: list of unified currency codes + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `fee structures ` + """ + self.load_markets() + response = self.privateWalletGetWithdrawStatus(params) + # + # [ + # { + # "currency": "MTN", + # "name": "Medicalchain", + # "name_cn": "Medicalchain", + # "deposit": "0", + # "withdraw_percent": "0%", + # "withdraw_fix": "900", + # "withdraw_day_limit": "500000", + # "withdraw_day_limit_remain": "500000", + # "withdraw_amount_mini": "900.1", + # "withdraw_eachtime_limit": "90000000000", + # "withdraw_fix_on_chains": { + # "ETH": "900" + # } + # } + # ] + # + return self.parse_deposit_withdraw_fees(response, codes, 'currency') + + def parse_deposit_withdraw_fee(self, fee, currency: Currency = None): + # + # { + # "currency": "MTN", + # "name": "Medicalchain", + # "name_cn": "Medicalchain", + # "deposit": "0", + # "withdraw_percent": "0%", + # "withdraw_fix": "900", + # "withdraw_day_limit": "500000", + # "withdraw_day_limit_remain": "500000", + # "withdraw_amount_mini": "900.1", + # "withdraw_eachtime_limit": "90000000000", + # "withdraw_fix_on_chains": { + # "ETH": "900" + # } + # } + # + withdrawFixOnChains = self.safe_value(fee, 'withdraw_fix_on_chains') + result: dict = { + 'info': fee, + 'withdraw': { + 'fee': self.safe_number(fee, 'withdraw_fix'), + 'percentage': False, + }, + 'deposit': { + 'fee': self.safe_number(fee, 'deposit'), + 'percentage': False, + }, + 'networks': {}, + } + if withdrawFixOnChains is not None: + chainKeys = list(withdrawFixOnChains.keys()) + for i in range(0, len(chainKeys)): + chainKey = chainKeys[i] + networkCode = self.network_id_to_code(chainKey, self.safe_string(fee, 'currency')) + result['networks'][networkCode] = { + 'withdraw': { + 'fee': self.parse_number(withdrawFixOnChains[chainKey]), + 'percentage': False, + }, + 'deposit': { + 'fee': None, + 'percentage': None, + }, + } + return result + + def fetch_funding_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch the history of funding payments paid and received on self account + + https://www.gate.com/docs/developers/apiv4/en/#query-account-book-2 + https://www.gate.com/docs/developers/apiv4/en/#query-account-book-3 + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch funding history for + :param int [limit]: the maximum number of funding history structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding history structure ` + """ + self.load_markets() + # defaultType = 'future' + market = None + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + type, query = self.handle_market_type_and_params('fetchFundingHistory', market, params) + request, requestParams = self.prepare_request(market, type, query) + request['type'] = 'fund' # 'dnw' 'pnl' 'fee' 'refr' 'fund' 'point_dnw' 'point_fee' 'point_refr' + if since is not None: + # from should be integer + request['from'] = self.parse_to_int(since / 1000) + if limit is not None: + request['limit'] = limit + response = None + if type == 'swap': + response = self.privateFuturesGetSettleAccountBook(self.extend(request, requestParams)) + elif type == 'future': + response = self.privateDeliveryGetSettleAccountBook(self.extend(request, requestParams)) + else: + raise NotSupported(self.id + ' fetchFundingHistory() only support swap & future market type') + # + # [ + # { + # "time": 1646899200, + # "change": "-0.027722", + # "balance": "11.653120591841", + # "text": "XRP_USDT", + # "type": "fund" + # }, + # ... + # ] + # + return self.parse_funding_histories(response, symbol, since, limit) + + def parse_funding_histories(self, response, symbol, since, limit) -> List[FundingHistory]: + result = [] + for i in range(0, len(response)): + entry = response[i] + funding = self.parse_funding_history(entry) + result.append(funding) + sorted = self.sort_by(result, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, symbol, since, limit) + + def parse_funding_history(self, info, market: Market = None): + # + # { + # "time": 1646899200, + # "change": "-0.027722", + # "balance": "11.653120591841", + # "text": "XRP_USDT", + # "type": "fund" + # } + # + timestamp = self.safe_timestamp(info, 'time') + marketId = self.safe_string(info, 'text') + market = self.safe_market(marketId, market, '_', 'swap') + return { + 'info': info, + 'symbol': self.safe_string(market, 'symbol'), + 'code': self.safe_string(market, 'settle'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': None, + 'amount': self.safe_number(info, 'change'), + } + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://www.gate.com/docs/developers/apiv4/en/#retrieve-order-book + https://www.gate.com/docs/developers/apiv4/en/#futures-order-book + https://www.gate.com/docs/developers/apiv4/en/#futures-order-book-2 + https://www.gate.com/docs/developers/apiv4/en/#options-order-book + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + # + # request: Dict = { + # 'currency_pair': market['id'], + # 'interval': '0', # depth, 0 means no aggregation is applied, default to 0 + # 'limit': limit, # maximum number of order depth data in asks or bids + # 'with_id': True, # return order book ID + # } + # + request, query = self.prepare_request(market, market['type'], params) + if limit is not None: + if market['spot']: + limit = min(limit, 1000) + else: + limit = min(limit, 300) + request['limit'] = limit + request['with_id'] = True + response = None + if market['spot'] or market['margin']: + response = self.publicSpotGetOrderBook(self.extend(request, query)) + elif market['swap']: + response = self.publicFuturesGetSettleOrderBook(self.extend(request, query)) + elif market['future']: + response = self.publicDeliveryGetSettleOrderBook(self.extend(request, query)) + elif market['option']: + response = self.publicOptionsGetOrderBook(self.extend(request, query)) + else: + raise NotSupported(self.id + ' fetchOrderBook() not support self market type') + # + # spot + # + # { + # "id": 6358770031 + # "current": 1634345973275, + # "update": 1634345973271, + # "asks": [ + # ["2.2241","12449.827"], + # ["2.2242","200"], + # ["2.2244","826.931"], + # ["2.2248","3876.107"], + # ["2.225","2377.252"], + # ["2.22509","439.484"], + # ["2.2251","1489.313"], + # ["2.2253","714.582"], + # ["2.2254","1349.784"], + # ["2.2256","234.701"]], + # "bids": [ + # ["2.2236","32.465"], + # ["2.2232","243.983"], + # ["2.2231","32.207"], + # ["2.223","449.827"], + # ["2.2228","7.918"], + # ["2.2227","12703.482"], + # ["2.2226","143.033"], + # ["2.2225","143.027"], + # ["2.2224","1369.352"], + # ["2.2223","756.063"] + # ] + # } + # + # swap, future and option + # + # { + # "id": 6358770031 + # "current": 1634350208.745, + # "asks": [ + # {"s": 24909, "p": "61264.8"}, + # {"s": 81, "p": "61266.6"}, + # {"s": 2000, "p": "61267.6"}, + # {"s": 490, "p": "61270.2"}, + # {"s": 12, "p": "61270.4"}, + # {"s": 11782, "p": "61273.2"}, + # {"s": 14666, "p": "61273.3"}, + # {"s": 22541, "p": "61273.4"}, + # {"s": 33, "p": "61273.6"}, + # {"s": 11980, "p": "61274.5"} + # ], + # "bids": [ + # {"s": 41844, "p": "61264.7"}, + # {"s": 13783, "p": "61263.3"}, + # {"s": 1143, "p": "61259.8"}, + # {"s": 81, "p": "61258.7"}, + # {"s": 2471, "p": "61257.8"}, + # {"s": 2471, "p": "61257.7"}, + # {"s": 2471, "p": "61256.5"}, + # {"s": 3, "p": "61254.2"}, + # {"s": 114, "p": "61252.4"}, + # {"s": 14372, "p": "61248.6"} + # ], + # "update": 1634350208.724 + # } + # + timestamp = self.safe_integer(response, 'current') + if not market['spot']: + timestamp = timestamp * 1000 + priceKey = 0 if market['spot'] else 'p' + amountKey = 1 if market['spot'] else 's' + nonce = self.safe_integer(response, 'id') + result = self.parse_order_book(response, symbol, timestamp, 'bids', 'asks', priceKey, amountKey) + result['nonce'] = nonce + return result + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://www.gate.com/docs/developers/apiv4/en/#get-details-of-a-specifc-order + https://www.gate.com/docs/developers/apiv4/en/#list-futures-tickers + https://www.gate.com/docs/developers/apiv4/en/#list-futures-tickers-2 + https://www.gate.com/docs/developers/apiv4/en/#list-tickers-of-options-contracts + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request, query = self.prepare_request(market, None, params) + response = None + if market['spot'] or market['margin']: + response = self.publicSpotGetTickers(self.extend(request, query)) + elif market['swap']: + response = self.publicFuturesGetSettleTickers(self.extend(request, query)) + elif market['future']: + response = self.publicDeliveryGetSettleTickers(self.extend(request, query)) + elif market['option']: + marketId = market['id'] + optionParts = marketId.split('-') + request['underlying'] = self.safe_string(optionParts, 0) + response = self.publicOptionsGetTickers(self.extend(request, query)) + else: + raise NotSupported(self.id + ' fetchTicker() not support self market type') + ticker = None + if market['option']: + for i in range(0, len(response)): + entry = response[i] + if entry['name'] == market['id']: + ticker = entry + break + else: + ticker = self.safe_value(response, 0) + return self.parse_ticker(ticker, market) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # SPOT + # + # { + # "currency_pair": "KFC_USDT", + # "last": "7.255", + # "lowest_ask": "7.298", + # "highest_bid": "7.218", + # "change_percentage": "-1.18", + # "base_volume": "1219.053687865", + # "quote_volume": "8807.40299875455", + # "high_24h": "7.262", + # "low_24h": "7.095" + # } + # + # LINEAR/DELIVERY + # + # { + # "contract": "BTC_USDT", + # "last": "6432", + # "low_24h": "6278", + # "high_24h": "6790", + # "change_percentage": "4.43", + # "total_size": "32323904", + # "volume_24h": "184040233284", + # "volume_24h_btc": "28613220", + # "volume_24h_usd": "184040233284", + # "volume_24h_base": "28613220", + # "volume_24h_quote": "184040233284", + # "volume_24h_settle": "28613220", + # "mark_price": "6534", + # "funding_rate": "0.0001", + # "funding_rate_indicative": "0.0001", + # "index_price": "6531" + # } + # + # bookTicker + # { + # "t": 1671363004228, + # "u": 9793320464, + # "s": "BTC_USDT", + # "b": "16716.8", # best bid price + # "B": "0.0134", # best bid size + # "a": "16716.9", # best ask price + # "A": "0.0353" # best ask size + # } + # + # option + # + # { + # "vega": "0.00002", + # "leverage": "12.277188268663", + # "ask_iv": "0", + # "delta": "-0.99999", + # "last_price": "0", + # "theta": "-0.00661", + # "bid1_price": "1096", + # "mark_iv": "0.7799", + # "name": "BTC_USDT-20230608-28500-P", + # "bid_iv": "0", + # "ask1_price": "2935", + # "mark_price": "2147.3", + # "position_size": 0, + # "bid1_size": 12, + # "ask1_size": -14, + # "gamma": "0" + # } + # + marketId = self.safe_string_n(ticker, ['currency_pair', 'contract', 'name']) + marketType = 'contract' if ('mark_price' in ticker) else 'spot' + symbol = self.safe_symbol(marketId, market, '_', marketType) + last = self.safe_string_2(ticker, 'last', 'last_price') + ask = self.safe_string_n(ticker, ['lowest_ask', 'a', 'ask1_price']) + bid = self.safe_string_n(ticker, ['highest_bid', 'b', 'bid1_price']) + high = self.safe_string(ticker, 'high_24h') + low = self.safe_string(ticker, 'low_24h') + bidVolume = self.safe_string_2(ticker, 'B', 'bid1_size') + askVolume = self.safe_string_2(ticker, 'A', 'ask1_size') + timestamp = self.safe_integer(ticker, 't') + baseVolume = self.safe_string_2(ticker, 'base_volume', 'volume_24h_base') + if baseVolume == 'nan': + baseVolume = '0' + quoteVolume = self.safe_string_2(ticker, 'quote_volume', 'volume_24h_quote') + if quoteVolume == 'nan': + quoteVolume = '0' + percentage = self.safe_string(ticker, 'change_percentage') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': high, + 'low': low, + 'bid': bid, + 'bidVolume': bidVolume, + 'ask': ask, + 'askVolume': askVolume, + 'vwap': None, + 'open': None, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': percentage, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'markPrice': self.safe_string(ticker, 'mark_price'), + 'indexPrice': self.safe_string(ticker, 'index_price'), + 'info': ticker, + }, market) + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://www.gate.com/docs/developers/apiv4/en/#get-details-of-a-specifc-order + https://www.gate.com/docs/developers/apiv4/en/#list-futures-tickers + https://www.gate.com/docs/developers/apiv4/en/#list-futures-tickers-2 + https://www.gate.com/docs/developers/apiv4/en/#list-tickers-of-options-contracts + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + symbols = self.market_symbols(symbols) + first = self.safe_string(symbols, 0) + market = None + if first is not None: + market = self.market(first) + type, query = self.handle_market_type_and_params('fetchTickers', market, params) + request, requestParams = self.prepare_request(None, type, query) + response = None + request['timezone'] = 'utc0' # default to utc + if type == 'spot' or type == 'margin': + response = self.publicSpotGetTickers(self.extend(request, requestParams)) + elif type == 'swap': + response = self.publicFuturesGetSettleTickers(self.extend(request, requestParams)) + elif type == 'future': + response = self.publicDeliveryGetSettleTickers(self.extend(request, requestParams)) + elif type == 'option': + self.check_required_argument('fetchTickers', symbols, 'symbols') + marketId = market['id'] + optionParts = marketId.split('-') + request['underlying'] = self.safe_string(optionParts, 0) + response = self.publicOptionsGetTickers(self.extend(request, requestParams)) + else: + raise NotSupported(self.id + ' fetchTickers() not support self market type, provide symbols or set params["defaultType"] to one from spot/margin/swap/future/option') + return self.parse_tickers(response, symbols) + + def parse_balance_helper(self, entry): + account = self.account() + account['used'] = self.safe_string_2(entry, 'freeze', 'locked') + account['free'] = self.safe_string(entry, 'available') + account['total'] = self.safe_string(entry, 'total') + if 'borrowed' in entry: + account['debt'] = self.safe_string(entry, 'borrowed') + return account + + def fetch_balance(self, params={}) -> Balances: + """ + + https://www.gate.com/docs/developers/apiv4/en/#margin-account-list + https://www.gate.com/docs/developers/apiv4/en/#get-unified-account-information + https://www.gate.com/docs/developers/apiv4/en/#list-spot-trading-accounts + https://www.gate.com/docs/developers/apiv4/en/#get-futures-account + https://www.gate.com/docs/developers/apiv4/en/#get-futures-account-2 + https://www.gate.com/docs/developers/apiv4/en/#query-account-information + + :param dict [params]: exchange specific parameters + :param str [params.type]: spot, margin, swap or future, if not provided self.options['defaultType'] is used + :param str [params.settle]: 'btc' or 'usdt' - settle currency for perpetual swap and future - default="usdt" for swap and "btc" for future + :param str [params.marginMode]: 'cross' or 'isolated' - marginMode for margin trading if not provided self.options['defaultMarginMode'] is used + :param str [params.symbol]: margin only - unified ccxt symbol + :param boolean [params.unifiedAccount]: default False, set to True for fetching the unified account balance + :returns dict: a `balance structure ` + """ + self.load_markets() + self.load_unified_status() + symbol = self.safe_string(params, 'symbol') + params = self.omit(params, 'symbol') + isUnifiedAccount = False + isUnifiedAccount, params = self.handle_option_and_params(params, 'fetchBalance', 'unifiedAccount') + type, query = self.handle_market_type_and_params('fetchBalance', None, params) + request, requestParams = self.prepare_request(None, type, query) + marginMode, requestQuery = self.get_margin_mode(False, requestParams) + if symbol is not None: + market = self.market(symbol) + request['currency_pair'] = market['id'] + response = None + if isUnifiedAccount: + response = self.privateUnifiedGetAccounts(self.extend(request, params)) + elif type == 'spot': + if marginMode == 'spot': + response = self.privateSpotGetAccounts(self.extend(request, requestQuery)) + elif marginMode == 'margin': + response = self.privateMarginGetAccounts(self.extend(request, requestQuery)) + elif marginMode == 'cross_margin': + response = self.privateMarginGetCrossAccounts(self.extend(request, requestQuery)) + else: + raise NotSupported(self.id + ' fetchBalance() not support self marginMode') + elif type == 'funding': + response = self.privateMarginGetFundingAccounts(self.extend(request, requestQuery)) + elif type == 'swap': + response = self.privateFuturesGetSettleAccounts(self.extend(request, requestQuery)) + elif type == 'future': + response = self.privateDeliveryGetSettleAccounts(self.extend(request, requestQuery)) + elif type == 'option': + response = self.privateOptionsGetAccounts(self.extend(request, requestQuery)) + else: + raise NotSupported(self.id + ' fetchBalance() not support self market type') + contract = ((type == 'swap') or (type == 'future') or (type == 'option')) + if contract: + response = [response] + # + # Spot / margin funding + # + # [ + # { + # "currency": "DBC", + # "available": "0", + # "locked": "0" + # "lent": "0", # margin funding only + # "total_lent": "0" # margin funding only + # }, + # ... + # ] + # + # Margin + # + # [ + # { + # "currency_pair": "DOGE_USDT", + # "locked": False, + # "risk": "9999.99", + # "base": { + # "currency": "DOGE", + # "available": "0", + # "locked": "0", + # "borrowed": "0", + # "interest": "0" + # }, + # "quote": { + # "currency": "USDT", + # "available": "0.73402", + # "locked": "0", + # "borrowed": "0", + # "interest": "0" + # } + # }, + # ... + # ] + # + # Cross margin + # + # { + # "user_id": 10406147, + # "locked": False, + # "balances": { + # "USDT": { + # "available": "1", + # "freeze": "0", + # "borrowed": "0", + # "interest": "0" + # } + # }, + # "total": "1", + # "borrowed": "0", + # "interest": "0", + # "risk": "9999.99" + # } + # + # Perpetual Swap + # + # { + # "order_margin": "0", + # "point": "0", + # "bonus": "0", + # "history": { + # "dnw": "2.1321", + # "pnl": "11.5351", + # "refr": "0", + # "point_fee": "0", + # "fund": "-0.32340576684", + # "bonus_dnw": "0", + # "point_refr": "0", + # "bonus_offset": "0", + # "fee": "-0.20132775", + # "point_dnw": "0", + # }, + # "unrealised_pnl": "13.315100000006", + # "total": "12.51345151332", + # "available": "0", + # "in_dual_mode": False, + # "currency": "USDT", + # "position_margin": "12.51345151332", + # "user": "6333333", + # } + # + # Delivery Future + # + # { + # "order_margin": "0", + # "point": "0", + # "history": { + # "dnw": "1", + # "pnl": "0", + # "refr": "0", + # "point_fee": "0", + # "point_dnw": "0", + # "settle": "0", + # "settle_fee": "0", + # "point_refr": "0", + # "fee": "0", + # }, + # "unrealised_pnl": "0", + # "total": "1", + # "available": "1", + # "currency": "USDT", + # "position_margin": "0", + # "user": "6333333", + # } + # + # option + # + # { + # "order_margin": "0", + # "bid_order_margin": "0", + # "init_margin": "0", + # "history": { + # "dnw": "32", + # "set": "0", + # "point_fee": "0", + # "point_dnw": "0", + # "prem": "0", + # "point_refr": "0", + # "insur": "0", + # "fee": "0", + # "refr": "0" + # }, + # "total": "32", + # "available": "32", + # "liq_triggered": False, + # "maint_margin": "0", + # "ask_order_margin": "0", + # "point": "0", + # "position_notional_limit": "2000000", + # "unrealised_pnl": "0", + # "equity": "32", + # "user": 5691076, + # "currency": "USDT", + # "short_enabled": False, + # "orders_limit": 10 + # } + # + # unified + # + # { + # "user_id": 10001, + # "locked": False, + # "balances": { + # "ETH": { + # "available": "0", + # "freeze": "0", + # "borrowed": "0.075393666654", + # "negative_liab": "0", + # "futures_pos_liab": "0", + # "equity": "1016.1", + # "total_freeze": "0", + # "total_liab": "0" + # }, + # "POINT": { + # "available": "9999999999.017023138734", + # "freeze": "0", + # "borrowed": "0", + # "negative_liab": "0", + # "futures_pos_liab": "0", + # "equity": "12016.1", + # "total_freeze": "0", + # "total_liab": "0" + # }, + # "USDT": { + # "available": "0.00000062023", + # "freeze": "0", + # "borrowed": "0", + # "negative_liab": "0", + # "futures_pos_liab": "0", + # "equity": "16.1", + # "total_freeze": "0", + # "total_liab": "0" + # } + # }, + # "total": "230.94621713", + # "borrowed": "161.66395521", + # "total_initial_margin": "1025.0524665088", + # "total_margin_balance": "3382495.944473949183", + # "total_maintenance_margin": "205.01049330176", + # "total_initial_margin_rate": "3299.827135672679", + # "total_maintenance_margin_rate": "16499.135678363399", + # "total_available_margin": "3381470.892007440383", + # "unified_account_total": "3381470.892007440383", + # "unified_account_total_liab": "0", + # "unified_account_total_equity": "100016.1", + # "leverage": "2" + # } + # + result: dict = { + 'info': response, + } + isolated = marginMode == 'margin' and type == 'spot' + data = response + if 'balances' in data: # True for cross_margin and unified + flatBalances = [] + balances = self.safe_value(data, 'balances', []) + # inject currency and create an artificial balance object + # so it can follow the existent flow + keys = list(balances.keys()) + for i in range(0, len(keys)): + currencyId = keys[i] + content = balances[currencyId] + content['currency'] = currencyId + flatBalances.append(content) + data = flatBalances + for i in range(0, len(data)): + entry = data[i] + if isolated: + marketId = self.safe_string(entry, 'currency_pair') + symbolInner = self.safe_symbol(marketId, None, '_', 'margin') + base = self.safe_value(entry, 'base', {}) + quote = self.safe_value(entry, 'quote', {}) + baseCode = self.safe_currency_code(self.safe_string(base, 'currency')) + quoteCode = self.safe_currency_code(self.safe_string(quote, 'currency')) + subResult: dict = {} + subResult[baseCode] = self.parse_balance_helper(base) + subResult[quoteCode] = self.parse_balance_helper(quote) + result[symbolInner] = self.safe_balance(subResult) + else: + code = self.safe_currency_code(self.safe_string(entry, 'currency')) + result[code] = self.parse_balance_helper(entry) + returnResult = result if isolated else self.safe_balance(result) + return returnResult + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://www.gate.com/docs/developers/apiv4/en/#market-candlesticks # spot + https://www.gate.com/docs/developers/apiv4/en/#get-futures-candlesticks # swap + https://www.gate.com/docs/developers/apiv4/en/#market-candlesticks # future + https://www.gate.com/docs/developers/apiv4/en/#get-options-candlesticks # option + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch, limit is conflicted with since and params["until"], If either since and params["until"] is specified, request will be rejected + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.price]: "mark" or "index" for mark price and index price candles + :param int [params.until]: timestamp in ms of the latest candle to fetch + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns int[][]: A list of candles ordered, open, high, low, close, volume(units in quote currency) + """ + self.load_markets() + market = self.market(symbol) + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate') + if paginate: + return self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params, 1000) + if market['option']: + return self.fetch_option_ohlcv(symbol, timeframe, since, limit, params) + price = self.safe_string(params, 'price') + request: dict = {} + request, params = self.prepare_request(market, None, params) + request['interval'] = self.safe_string(self.timeframes, timeframe, timeframe) + maxLimit = 1999 if market['contract'] else 1000 + limit = maxLimit if (limit is None) else min(limit, maxLimit) + until = self.safe_integer(params, 'until') + if until is not None: + until = self.parse_to_int(until / 1000) + params = self.omit(params, 'until') + if since is not None: + duration = self.parse_timeframe(timeframe) + request['from'] = self.parse_to_int(since / 1000) + distance = (limit - 1) * duration + toTimestamp = self.sum(request['from'], distance) + currentTimestamp = self.seconds() + to = min(toTimestamp, currentTimestamp) + if until is not None: + request['to'] = min(to, until) + else: + request['to'] = to + else: + if until is not None: + request['to'] = until + request['limit'] = limit + response = None + if market['contract']: + isMark = (price == 'mark') + isIndex = (price == 'index') + if isMark or isIndex: + request['contract'] = price + '_' + market['id'] + params = self.omit(params, 'price') + if market['future']: + response = self.publicDeliveryGetSettleCandlesticks(self.extend(request, params)) + elif market['swap']: + response = self.publicFuturesGetSettleCandlesticks(self.extend(request, params)) + else: + response = self.publicSpotGetCandlesticks(self.extend(request, params)) + return self.parse_ohlcvs(response, market, timeframe, since, limit) + + def fetch_option_ohlcv(self, symbol: str, timeframe='1m', since: Int = None, limit: Int = None, params={}): + # separated option logic because the from, to and limit parameters weren't functioning + self.load_markets() + market = self.market(symbol) + request: dict = {} + request, params = self.prepare_request(market, None, params) + request['interval'] = self.safe_string(self.timeframes, timeframe, timeframe) + response = self.publicOptionsGetCandlesticks(self.extend(request, params)) + return self.parse_ohlcvs(response, market, timeframe, since, limit) + + def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical funding rate prices + + https://www.gate.com/docs/developers/apiv4/en/#funding-rate-history + + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: timestamp in ms of the earliest funding rate to fetch + :param int [limit]: the maximum amount of `funding rate structures ` to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest funding rate to fetch + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `funding rate structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchFundingRateHistory() requires a symbol argument') + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchFundingRateHistory', 'paginate') + if paginate: + return self.fetch_paginated_call_deterministic('fetchFundingRateHistory', symbol, since, limit, '8h', params) + market = self.market(symbol) + if not market['swap']: + raise BadSymbol(self.id + ' fetchFundingRateHistory() supports swap contracts only') + request: dict = {} + request, params = self.prepare_request(market, None, params) + if limit is not None: + request['limit'] = limit + if since is not None: + request['from'] = self.parse_to_int(since / 1000) + until = self.safe_integer(params, 'until') + if until is not None: + params = self.omit(params, 'until') + request['to'] = self.parse_to_int(until / 1000) + response = self.publicFuturesGetSettleFundingRate(self.extend(request, params)) + # + # { + # "r": "0.00063521", + # "t": "1621267200000", + # } + # + rates = [] + for i in range(0, len(response)): + entry = response[i] + timestamp = self.safe_timestamp(entry, 't') + rates.append({ + 'info': entry, + 'symbol': symbol, + 'fundingRate': self.safe_number(entry, 'r'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + sorted = self.sort_by(rates, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, market['symbol'], since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # Spot market candles + # + # [ + # "1660957920", # timestamp + # "6227.070147198573", # quote volume + # "0.0000133485", # close + # "0.0000133615", # high + # "0.0000133347", # low + # "0.0000133468", # open + # "466641934.99" # base volume + # ] + # + # + # Swap, Future, Option, Mark and Index price candles + # + # { + # "t":1632873600, # Unix timestamp in seconds + # "o": "41025", # Open price + # "h": "41882.17", # Highest price + # "c": "41776.92", # Close price + # "l": "40783.94" # Lowest price + # } + # + if isinstance(ohlcv, list): + return [ + self.safe_timestamp(ohlcv, 0), # unix timestamp in seconds + self.safe_number(ohlcv, 5), # open price + self.safe_number(ohlcv, 3), # highest price + self.safe_number(ohlcv, 4), # lowest price + self.safe_number(ohlcv, 2), # close price + self.safe_number(ohlcv, 6), # trading volume + ] + else: + # Swap, Future, Option, Mark and Index price candles + return [ + self.safe_timestamp(ohlcv, 't'), # unix timestamp in seconds + self.safe_number(ohlcv, 'o'), # open price + self.safe_number(ohlcv, 'h'), # highest price + self.safe_number(ohlcv, 'l'), # lowest price + self.safe_number(ohlcv, 'c'), # close price + self.safe_number(ohlcv, 'v'), # trading volume, None for mark or index price + ] + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://www.gate.com/docs/developers/apiv4/en/#retrieve-market-trades + https://www.gate.com/docs/developers/apiv4/en/#futures-trading-history + https://www.gate.com/docs/developers/apiv4/en/#futures-trading-history-2 + https://www.gate.com/docs/developers/apiv4/en/#options-trade-history + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest trade to fetch + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchTrades', 'paginate') + if paginate: + return self.fetch_paginated_call_dynamic('fetchTrades', symbol, since, limit, params) + market = self.market(symbol) + # + # spot + # + # request: Dict = { + # 'currency_pair': market['id'], + # 'limit': limit, # maximum number of records to be returned in a single list + # 'last_id': 'id', # specify list staring point using the id of last record in previous list-query results + # 'reverse': False, # True to retrieve records where id is smaller than the specified last_id, False to retrieve records where id is larger than the specified last_id + # } + # + # swap, future + # + # request: Dict = { + # 'settle': market['settleId'], + # 'contract': market['id'], + # 'limit': limit, # maximum number of records to be returned in a single list + # 'last_id': 'id', # specify list staring point using the id of last record in previous list-query results + # 'from': since / 1000), # starting time in seconds, if not specified, to and limit will be used to limit response items + # 'to': self.seconds(), # end time in seconds, default to current time + # } + # + request, query = self.prepare_request(market, None, params) + until = self.safe_integer_2(params, 'to', 'until') + if until is not None: + params = self.omit(params, ['until']) + request['to'] = self.parse_to_int(until / 1000) + if limit is not None: + request['limit'] = min(limit, 1000) # default 100, max 1000 + if since is not None and (market['contract']): + request['from'] = self.parse_to_int(since / 1000) + response = None + if market['type'] == 'spot' or market['type'] == 'margin': + response = self.publicSpotGetTrades(self.extend(request, query)) + elif market['swap']: + response = self.publicFuturesGetSettleTrades(self.extend(request, query)) + elif market['future']: + response = self.publicDeliveryGetSettleTrades(self.extend(request, query)) + elif market['type'] == 'option': + response = self.publicOptionsGetTrades(self.extend(request, query)) + else: + raise NotSupported(self.id + ' fetchTrades() not support self market type.') + # + # spot + # + # [ + # { + # "id": "1852958144", + # "create_time": "1634673259", + # "create_time_ms": "1634673259378.105000", + # "currency_pair": "ADA_USDT", + # "side": "sell", + # "amount": "307.078", + # "price": "2.104", + # } + # ] + # + # perpetual swap + # + # [ + # { + # "size": "2", + # "id": "2522911", + # "create_time_ms": "1634673380.182", + # "create_time": "1634673380.182", + # "contract": "ADA_USDT", + # "price": "2.10486", + # } + # ] + # + # option + # + # [ + # { + # "size": -5, + # "id": 25, + # "create_time": 1682378573, + # "contract": "ETH_USDT-20230526-2000-P", + # "price": "209.1" + # } + # ] + # + return self.parse_trades(response, market, since, limit) + + def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all the trades made from a single order + + https://www.gate.com/docs/developers/apiv4/en/#list-personal-trading-history + https://www.gate.com/docs/developers/apiv4/en/#list-personal-trading-history-2 + https://www.gate.com/docs/developers/apiv4/en/#list-personal-trading-history-3 + https://www.gate.com/docs/developers/apiv4/en/#list-personal-trading-history-4 + + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrderTrades() requires a symbol argument') + self.load_markets() + # + # [ + # { + # "id":"3711449544", + # "create_time":"1655486040", + # "create_time_ms":"1655486040177.599900", + # "currency_pair":"SHIB_USDT", + # "side":"buy", + # "role":"taker", + # "amount":"1360039", + # "price":"0.0000081084", + # "order_id":"169717399644", + # "fee":"2720.078", + # "fee_currency":"SHIB", + # "point_fee":"0", + # "gt_fee":"0" + # } + # ] + # + response = self.fetch_my_trades(symbol, since, limit, {'order_id': id}) + return response + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + Fetch personal trading history + + https://www.gate.com/docs/developers/apiv4/en/#list-personal-trading-history + https://www.gate.com/docs/developers/apiv4/en/#list-personal-trading-history-2 + https://www.gate.com/docs/developers/apiv4/en/#list-personal-trading-history-3 + https://www.gate.com/docs/developers/apiv4/en/#list-personal-trading-history-4 + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' - marginMode for margin trading if not provided self.options['defaultMarginMode'] is used + :param str [params.type]: 'spot', 'swap', or 'future', if not provided self.options['defaultMarginMode'] is used + :param int [params.until]: The latest timestamp, in ms, that fetched trades were made + :param int [params.page]: *spot only* Page number + :param str [params.order_id]: *spot only* Filter trades with specified order ID. symbol is also required if self field is present + :param str [params.order]: *contract only* Futures order ID, return related data only if specified + :param int [params.offset]: *contract only* list offset, starting from 0 + :param str [params.last_id]: *contract only* specify list staring point using the id of last record in previous list-query results + :param int [params.count_total]: *contract only* whether to return total number matched, default to 0(no return) + :param bool [params.unifiedAccount]: set to True for fetching trades in a unified account + :param bool [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + self.load_unified_status() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchMyTrades', 'paginate') + if paginate: + return self.fetch_paginated_call_dynamic('fetchMyTrades', symbol, since, limit, params) + type = None + marginMode = None + request: dict = {} + market = self.market(symbol) if (symbol is not None) else None + until = self.safe_integer(params, 'until') + params = self.omit(params, ['until']) + type, params = self.handle_market_type_and_params('fetchMyTrades', market, params) + contract = (type == 'swap') or (type == 'future') or (type == 'option') + if contract: + request, params = self.prepare_request(market, type, params) + if type == 'option': + params = self.omit(params, 'order_id') + else: + if market is not None: + request['currency_pair'] = market['id'] # Should always be set for non-trigger + marginMode, params = self.get_margin_mode(False, params) + request['account'] = marginMode + if limit is not None: + request['limit'] = limit # default 100, max 1000 + if since is not None: + request['from'] = self.parse_to_int(since / 1000) + if until is not None: + request['to'] = self.parse_to_int(until / 1000) + response = None + if type == 'spot' or type == 'margin': + response = self.privateSpotGetMyTrades(self.extend(request, params)) + elif type == 'swap': + response = self.privateFuturesGetSettleMyTradesTimerange(self.extend(request, params)) + elif type == 'future': + response = self.privateDeliveryGetSettleMyTrades(self.extend(request, params)) + elif type == 'option': + response = self.privateOptionsGetMyTrades(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchMyTrades() not support self market type.') + # + # spot + # + # [ + # { + # "id": "2876130500", + # "create_time": "1645464610", + # "create_time_ms": "1645464610777.399200", + # "currency_pair": "DOGE_USDT", + # "side": "sell", + # "role": "taker", + # "amount": "10.97", + # "price": "0.137384", + # "order_id": "125924049993", + # "fee": "0.00301420496", + # "fee_currency": "USDT", + # "point_fee": "0", + # "gt_fee": "0" + # } + # ] + # + # perpetual swap + # + # [ + # { + # "size": -5, + # "order_id": "130264979823", + # "id": 26884791, + # "role": "taker", + # "create_time": 1645465199.5472, + # "contract": "DOGE_USDT", + # "price": "0.136888" + # } + # ] + # + # future + # + # [ + # { + # "id": 121234231, + # "create_time": 1514764800.123, + # "contract": "BTC_USDT", + # "order_id": "21893289839", + # "size": 100, + # "price": "100.123", + # "role": "taker" + # } + # ] + # + # option + # + # [ + # { + # "underlying_price": "26817.84", + # "size": -1, + # "contract": "BTC_USDT-20230602-26500-C", + # "id": 16, + # "role": "taker", + # "create_time": 1685594770, + # "order_id": 2611026125, + # "price": "333" + # } + # ] + # + return self.parse_trades(response, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # public + # + # spot: + # { + # "id": "1334253759", + # "create_time": "1626342738", + # "create_time_ms": "1626342738331.497000", + # "currency_pair": "BTC_USDT", + # "side": "sell", + # "amount": "0.0022", + # "price": "32452.16" + # } + # + # swap: + # + # { + # "id": "442288327", + # "contract": "BTC_USDT", + # "create_time": "1739814676.707", + # "create_time_ms": "1739814676.707", + # "size": "-105", + # "price": "95594.8" + # } + # + # + # public ws + # + # { + # "id": 221994511, + # "time": 1580311438.618647, + # "price": "9309", + # "amount": "0.0019", + # "type": "sell" + # } + # + # spot rest + # + # { + # "id": "2876130500", + # "create_time": "1645464610", + # "create_time_ms": "1645464610777.399200", + # "currency_pair": "DOGE_USDT", + # "side": "sell", + # "role": "taker", + # "amount": "10.97", + # "price": "0.137384", + # "order_id": "125924049993", + # "fee": "0.00301420496", + # "fee_currency": "USDT", + # "point_fee": "1.1", + # "gt_fee":"2.2" + # } + # + # perpetual swap rest + # + # { + # "size": -5, + # "order_id": "130264979823", + # "id": 26884791, + # "role": "taker", + # "create_time": 1645465199.5472, + # "contract": "DOGE_USDT", + # "price": "0.136888" + # } + # + # future rest + # + # { + # "id": 121234231, + # "create_time": 1514764800.123, + # "contract": "BTC_USDT", + # "order_id": "21893289839", + # "size": 100, + # "price": "100.123", + # "role": "taker" + # } + # + # fetchTrades: option + # + # { + # "size": -5, + # "id": 25, + # "create_time": 1682378573, + # "contract": "ETH_USDT-20230526-2000-P", + # "price": "209.1" + # } + # + # fetchMyTrades: option + # + # { + # "underlying_price": "26817.84", + # "size": -1, + # "contract": "BTC_USDT-20230602-26500-C", + # "id": 16, + # "role": "taker", + # "create_time": 1685594770, + # "order_id": 2611026125, + # "price": "333" + # } + # + id = self.safe_string_2(trade, 'id', 'trade_id') + timestamp: Int = None + msString = self.safe_string(trade, 'create_time_ms') + if msString is not None: + msString = Precise.string_mul(msString, '1000') + msString = msString[0:13] + timestamp = self.parse_to_int(msString) + else: + timestamp = self.safe_timestamp_2(trade, 'time', 'create_time') + marketId = self.safe_string_2(trade, 'currency_pair', 'contract') + marketType = 'contract' if ('contract' in trade) else 'spot' + market = self.safe_market(marketId, market, '_', marketType) + amountString = self.safe_string_2(trade, 'amount', 'size') + priceString = self.safe_string(trade, 'price') + contractSide = 'sell' if Precise.string_lt(amountString, '0') else 'buy' + amountString = Precise.string_abs(amountString) + side = self.safe_string_2(trade, 'side', 'type', contractSide) + orderId = self.safe_string(trade, 'order_id') + feeAmount = self.safe_string(trade, 'fee') + gtFee = self.omit_zero(self.safe_string(trade, 'gt_fee')) + pointFee = self.omit_zero(self.safe_string(trade, 'point_fee')) + fees = [] + if feeAmount is not None: + feeCurrencyId = self.safe_string(trade, 'fee_currency') + feeCurrencyCode = self.safe_currency_code(feeCurrencyId) + if feeCurrencyCode is None: + feeCurrencyCode = self.safe_string(market, 'settle') + fees.append({ + 'cost': feeAmount, + 'currency': feeCurrencyCode, + }) + if gtFee is not None: + fees.append({ + 'cost': gtFee, + 'currency': 'GT', + }) + if pointFee is not None: + fees.append({ + 'cost': pointFee, + 'currency': 'GatePoint', + }) + takerOrMaker = self.safe_string(trade, 'role') + return self.safe_trade({ + 'info': trade, + 'id': id, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'order': orderId, + 'type': None, + 'side': side, + 'takerOrMaker': takerOrMaker, + 'price': priceString, + 'amount': amountString, + 'cost': None, + 'fee': None, + 'fees': fees, + }, market) + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://www.gate.com/docs/developers/apiv4/en/#retrieve-deposit-records + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: end time in ms + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchDeposits', 'paginate') + if paginate: + return self.fetch_paginated_call_dynamic('fetchDeposits', code, since, limit, params) + request: dict = {} + currency = None + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] # todo: currencies have network-junctions + if limit is not None: + request['limit'] = limit + if since is not None: + start = self.parse_to_int(since / 1000) + request['from'] = start + request['to'] = self.sum(start, 30 * 24 * 60 * 60) + request, params = self.handle_until_option('to', request, params, 0.001) + response = self.privateWalletGetDeposits(self.extend(request, params)) + return self.parse_transactions(response, currency) + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://www.gate.com/docs/developers/apiv4/en/#retrieve-withdrawal-records + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: end time in ms + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchWithdrawals', 'paginate') + if paginate: + return self.fetch_paginated_call_dynamic('fetchWithdrawals', code, since, limit, params) + request: dict = {} + currency = None + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] # todo: currencies have network-junctions + if limit is not None: + request['limit'] = limit + if since is not None: + start = self.parse_to_int(since / 1000) + request['from'] = start + request['to'] = self.sum(start, 30 * 24 * 60 * 60) + request, params = self.handle_until_option('to', request, params, 0.001) + response = self.privateWalletGetWithdrawals(self.extend(request, params)) + return self.parse_transactions(response, currency) + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://www.gate.com/docs/developers/apiv4/en/#withdraw + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.check_address(address) + self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], # todo: currencies have network-junctions + 'address': address, + 'amount': self.currency_to_precision(code, amount), + } + if tag is not None: + request['memo'] = tag + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + if networkCode is not None: + request['chain'] = self.network_code_to_id(networkCode) + response = self.privateWithdrawalsPostWithdrawals(self.extend(request, params)) + # + # { + # "id": "w13389675", + # "currency": "USDT", + # "amount": "50", + # "address": "TUu2rLFrmzUodiWfYki7QCNtv1akL682p1", + # "memo": null + # } + # + return self.parse_transaction(response, currency) + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'PEND': 'pending', + 'REQUEST': 'pending', + 'DMOVE': 'pending', + 'MANUAL': 'pending', + 'VERIFY': 'pending', + 'PROCES': 'pending', + 'EXTPEND': 'pending', + 'SPLITPEND': 'pending', + 'CANCEL': 'canceled', + 'FAIL': 'failed', + 'INVALID': 'failed', + 'DONE': 'ok', + 'BCODE': 'ok', # GateCode withdrawal + } + return self.safe_string(statuses, status, status) + + def parse_transaction_type(self, type): + types: dict = { + 'd': 'deposit', + 'w': 'withdrawal', + } + return self.safe_string(types, type, type) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fetchDeposits + # + # { + # "id": "d33361395", + # "currency": "USDT_TRX", + # "address": "TErdnxenuLtXfnMafLbfappYdHtnXQ5U4z", + # "amount": "100", + # "txid": "ae9374de34e558562fe18cbb1bf9ab4d9eb8aa7669d65541c9fa2a532c1474a0", + # "timestamp": "1626345819", + # "status": "DONE", + # "memo": "" + # } + # + # withdraw + # + # { + # "id":"w64413318", + # "currency":"usdt", + # "amount":"10150", + # "address":"0x0ab891497116f7f5532a4c2f4f7b1784488628e1", + # "memo":null, + # "status":"REQUEST", + # "chain":"eth", + # "withdraw_order_id":"", + # "fee_amount":"4.15000000" + # } + # + # fetchWithdrawals + # + # { + # "id": "210496", + # "timestamp": "1542000000", + # "withdraw_order_id": "order_123456", + # "currency": "USDT", + # "address": "1HkxtBAMrA3tP5ENnYY2CZortjZvFDH5Cs", + # "txid": "128988928203223323290", + # "block_number": "41575382", + # "amount": "222.61", + # "fee": "0.01", + # "memo": "", + # "status": "DONE", + # "chain": "TRX" + # } + # + # { + # "id": "w13389675", + # "currency": "USDT", + # "amount": "50", + # "address": "TUu2rLFrmzUodiWfYki7QCNtv1akL682p1", + # "memo": null + # } + # + # { + # "currency":"usdt", + # "address":"0x01c0A9b7b4CdE774AF0f3E47CB4f1c2CCdBa0806", + # "amount":"1880", + # "chain":"eth" + # } + # + id = self.safe_string(transaction, 'id') + type = None + amountString = self.safe_string(transaction, 'amount') + if id is not None: + if id[0] == 'b': + # GateCode handling + type = 'deposit' if Precise.string_gt(amountString, '0') else 'withdrawal' + amountString = Precise.string_abs(amountString) + else: + type = self.parse_transaction_type(id[0]) + feeCostString = self.safe_string_2(transaction, 'fee', 'fee_amount') + if type == 'withdrawal': + amountString = Precise.string_sub(amountString, feeCostString) + networkId = self.safe_string_upper(transaction, 'chain') + currencyId = self.safe_string(transaction, 'currency') + code = self.safe_currency_code(currencyId) + txid = self.safe_string(transaction, 'txid') + rawStatus = self.safe_string(transaction, 'status') + status = self.parse_transaction_status(rawStatus) + address = self.safe_string(transaction, 'address') + tag = self.safe_string(transaction, 'memo') + timestamp = self.safe_timestamp(transaction, 'timestamp') + return { + 'info': transaction, + 'id': id, + 'txid': txid, + 'currency': code, + 'amount': self.parse_number(amountString), + 'network': self.network_id_to_code(networkId), + 'address': address, + 'addressTo': None, + 'addressFrom': None, + 'tag': tag, + 'tagTo': None, + 'tagFrom': None, + 'status': status, + 'type': type, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'updated': None, + 'internal': None, + 'comment': None, + 'fee': { + 'currency': code, + 'cost': self.parse_number(feeCostString), + }, + } + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + Create an order on the exchange + + https://www.gate.com/docs/developers/apiv4/en/#create-an-order + https://www.gate.com/docs/developers/apiv4/en/#create-a-price-triggered-order + https://www.gate.com/docs/developers/apiv4/en/#create-a-futures-order + https://www.gate.com/docs/developers/apiv4/en/#create-a-price-triggered-order-2 + https://www.gate.com/docs/developers/apiv4/en/#create-a-futures-order-2 + https://www.gate.com/docs/developers/apiv4/en/#create-a-price-triggered-order-3 + https://www.gate.com/docs/developers/apiv4/en/#create-an-options-order + + :param str symbol: Unified CCXT market symbol + :param str type: 'limit' or 'market' *"market" is contract only* + :param str side: 'buy' or 'sell' + :param float amount: the amount of currency to trade + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: The price at which a trigger order is triggered at + :param str [params.timeInForce]: "GTC", "IOC", or "PO" + :param float [params.stopLossPrice]: The price at which a stop loss order is triggered at + :param float [params.takeProfitPrice]: The price at which a take profit order is triggered at + :param str [params.marginMode]: 'cross' or 'isolated' - marginMode for margin trading if not provided self.options['defaultMarginMode'] is used + :param int [params.iceberg]: Amount to display for the iceberg order, Null or 0 for normal orders, Set to -1 to hide the order completely + :param str [params.text]: User defined information + :param str [params.account]: *spot and margin only* "spot", "margin" or "cross_margin" + :param bool [params.auto_borrow]: *margin only* Used in margin or cross margin trading to allow automatic loan of insufficient amount if balance is not enough + :param str [params.settle]: *contract only* Unified Currency Code for settle currency + :param bool [params.reduceOnly]: *contract only* Indicates if self order is to reduce the size of a position + :param bool [params.close]: *contract only* Set to close the position, with size set to 0 + :param bool [params.auto_size]: *contract only* Set side to close dual-mode position, close_long closes the long side, while close_short the short one, size also needs to be set to 0 + :param int [params.price_type]: *contract only* 0 latest deal price, 1 mark price, 2 index price + :param float [params.cost]: *spot market buy only* the quote quantity that can be used alternative for the amount + :param bool [params.unifiedAccount]: set to True for creating an order in the unified account + :returns dict|None: `An order structure ` + """ + self.load_markets() + self.load_unified_status() + market = self.market(symbol) + trigger = self.safe_value(params, 'trigger') + triggerPrice = self.safe_value_2(params, 'triggerPrice', 'stopPrice') + stopLossPrice = self.safe_value(params, 'stopLossPrice', triggerPrice) + takeProfitPrice = self.safe_value(params, 'takeProfitPrice') + isStopLossOrder = stopLossPrice is not None + isTakeProfitOrder = takeProfitPrice is not None + isTpsl = isStopLossOrder or isTakeProfitOrder + nonTriggerOrder = not isTpsl and (trigger is None) + orderRequest = self.create_order_request(symbol, type, side, amount, price, params) + response = None + if market['spot'] or market['margin']: + if nonTriggerOrder: + response = self.privateSpotPostOrders(orderRequest) + else: + response = self.privateSpotPostPriceOrders(orderRequest) + elif market['swap']: + if nonTriggerOrder: + response = self.privateFuturesPostSettleOrders(orderRequest) + else: + response = self.privateFuturesPostSettlePriceOrders(orderRequest) + elif market['future']: + if nonTriggerOrder: + response = self.privateDeliveryPostSettleOrders(orderRequest) + else: + response = self.privateDeliveryPostSettlePriceOrders(orderRequest) + else: + response = self.privateOptionsPostOrders(orderRequest) + # response = getattr(self, method)(self.deep_extend(request, params)) + # + # spot + # + # { + # "id": "95282841887", + # "text": "apiv4", + # "create_time": "1637383156", + # "update_time": "1637383156", + # "create_time_ms": 1637383156017, + # "update_time_ms": 1637383156017, + # "status": "open", + # "currency_pair": "ETH_USDT", + # "type": "limit", + # "account": "spot", + # "side": "buy", + # "amount": "0.01", + # "price": "3500", + # "time_in_force": "gtc", + # "iceberg": "0", + # "left": "0.01", + # "fill_price": "0", + # "filled_total": "0", + # "fee": "0", + # "fee_currency": "ETH", + # "point_fee": "0", + # "gt_fee": "0", + # "gt_discount": False, + # "rebated_fee": "0", + # "rebated_fee_currency": "USDT" + # } + # + # spot conditional + # + # {"id": 5891843} + # + # futures, perpetual swaps and options + # + # { + # "id": 95938572327, + # "contract": "ETH_USDT", + # "mkfr": "0", + # "tkfr": "0.0005", + # "tif": "gtc", + # "is_reduce_only": False, + # "create_time": 1637384600.08, + # "price": "3000", + # "size": 1, + # "refr": "0", + # "left": 1, + # "text": "api", + # "fill_price": "0", + # "user": 2436035, + # "status": "open", + # "is_liq": False, + # "refu": 0, + # "is_close": False, + # "iceberg": 0 + # } + # + # futures and perpetual swaps conditionals + # + # {"id": 7615567} + # + return self.parse_order(response, market) + + def create_orders_request(self, orders: List[OrderRequest], params={}): + ordersRequests = [] + orderSymbols = [] + ordersLength = len(orders) + if ordersLength == 0: + raise BadRequest(self.id + ' createOrders() requires at least one order') + if ordersLength > 10: + raise BadRequest(self.id + ' createOrders() accepts a maximum of 10 orders at a time') + for i in range(0, len(orders)): + rawOrder = orders[i] + marketId = self.safe_string(rawOrder, 'symbol') + orderSymbols.append(marketId) + type = self.safe_string(rawOrder, 'type') + side = self.safe_string(rawOrder, 'side') + amount = self.safe_value(rawOrder, 'amount') + price = self.safe_value(rawOrder, 'price') + orderParams = self.safe_value(rawOrder, 'params', {}) + extendedParams = self.extend(orderParams, params) # the request does not accept extra params since it's a list, so we're extending each order with the common params + triggerValue = self.safe_value_n(orderParams, ['triggerPrice', 'stopPrice', 'takeProfitPrice', 'stopLossPrice']) + if triggerValue is not None: + raise NotSupported(self.id + ' createOrders() does not support advanced order properties(stopPrice, takeProfitPrice, stopLossPrice)') + extendedParams['textIsRequired'] = True # the exchange requires a text parameter for each order here + orderRequest = self.create_order_request(marketId, type, side, amount, price, extendedParams) + ordersRequests.append(orderRequest) + symbols = self.market_symbols(orderSymbols, None, False, True, True) + market = self.market(symbols[0]) + if market['future'] or market['option']: + raise NotSupported(self.id + ' createOrders() does not support futures or options markets') + return ordersRequests + + def create_orders(self, orders: List[OrderRequest], params={}): + """ + create a list of trade orders + + https://www.gate.com/docs/developers/apiv4/en/#get-a-single-order-2 + https://www.gate.com/docs/developers/apiv4/en/#create-a-batch-of-orders + https://www.gate.com/docs/developers/apiv4/en/#create-a-batch-of-futures-orders + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + self.load_unified_status() + ordersRequests = self.create_orders_request(orders, params) + firstOrder = orders[0] + market = self.market(firstOrder['symbol']) + response = None + if market['spot']: + response = self.privateSpotPostBatchOrders(ordersRequests) + elif market['swap']: + response = self.privateFuturesPostSettleBatchOrders(ordersRequests) + return self.parse_orders(response) + + def create_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + market = self.market(symbol) + contract = market['contract'] + trigger = self.safe_value(params, 'trigger') + triggerPrice = self.safe_value_2(params, 'triggerPrice', 'stopPrice') + stopLossPrice = self.safe_value(params, 'stopLossPrice', triggerPrice) + takeProfitPrice = self.safe_value(params, 'takeProfitPrice') + isStopLossOrder = stopLossPrice is not None + isTakeProfitOrder = takeProfitPrice is not None + isTpsl = isStopLossOrder or isTakeProfitOrder + if isStopLossOrder and isTakeProfitOrder: + raise ExchangeError(self.id + ' createOrder() stopLossPrice and takeProfitPrice cannot both be defined') + reduceOnly = self.safe_value(params, 'reduceOnly') + exchangeSpecificTimeInForce = self.safe_string_lower_n(params, ['timeInForce', 'tif', 'time_in_force']) + postOnly = None + postOnly, params = self.handle_post_only(type == 'market', exchangeSpecificTimeInForce == 'poc', params) + timeInForce = self.handle_time_in_force(params) + if postOnly: + timeInForce = 'poc' + # we only omit the unified params here + # self is because the other params will get extended into the request + params = self.omit(params, ['stopPrice', 'triggerPrice', 'stopLossPrice', 'takeProfitPrice', 'reduceOnly', 'timeInForce', 'postOnly']) + isLimitOrder = (type == 'limit') + isMarketOrder = (type == 'market') + if isLimitOrder and price is None: + raise ArgumentsRequired(self.id + ' createOrder() requires a price argument for ' + type + ' orders') + if isMarketOrder: + if (timeInForce == 'poc') or (timeInForce == 'gtc'): + raise ExchangeError(self.id + ' createOrder() timeInForce for market order can only be "FOK" or "IOC"') + else: + if timeInForce is None: + defaultTif = self.safe_string(self.options, 'defaultTimeInForce', 'IOC') + exchangeSpecificTif = self.safe_string(self.options['timeInForce'], defaultTif, 'ioc') + timeInForce = exchangeSpecificTif + if contract: + price = 0 + if contract: + isClose = self.safe_value(params, 'close') + if isClose: + amount = 0 + else: + amountToPrecision = self.amount_to_precision(symbol, amount) + signedAmount = Precise.string_neg(amountToPrecision) if (side == 'sell') else amountToPrecision + amount = int(signedAmount) + request = None + nonTriggerOrder = not isTpsl and (trigger is None) + if nonTriggerOrder: + if contract: + # contract order + request = { + 'contract': market['id'], # filled in prepareRequest above + 'size': amount, # int64, positive = bid, negative = ask + # 'iceberg': 0, # int64, display size for iceberg order, 0 for non-iceberg, note that you will have to pay the taker fee for the hidden size + # 'close': False, # True to close the position, with size set to 0 + # 'reduce_only': False, # St to be reduce-only order + # 'tif': 'gtc', # gtc, ioc, poc PendingOrCancelled == postOnly order + # 'text': clientOrderId, # 't-abcdef1234567890', + # 'auto_size': '', # close_long, close_short, note size also needs to be set to 0 + } + if not market['option']: + request['settle'] = market['settleId'] # filled in prepareRequest above + if isMarketOrder: + request['price'] = '0' # set to 0 for market orders + else: + request['price'] = '0' if (price == 0) else self.price_to_precision(symbol, price) + if reduceOnly is not None: + request['reduce_only'] = reduceOnly + if timeInForce is not None: + request['tif'] = timeInForce + else: + marginMode = None + marginMode, params = self.get_margin_mode(False, params) + # spot order + request = { + # 'text': clientOrderId, # 't-abcdef1234567890', + 'currency_pair': market['id'], # filled in prepareRequest above + 'type': type, + 'account': marginMode, # spot, margin, cross_margin, unified + 'side': side, + # 'time_in_force': 'gtc', # gtc, ioc, poc PendingOrCancelled == postOnly order + # 'iceberg': 0, # amount to display for the iceberg order, null or 0 for normal orders, set to -1 to hide the order completely + # 'auto_borrow': False, # used in margin or cross margin trading to allow automatic loan of insufficient amount if balance is not enough + # 'auto_repay': False, # automatic repayment for automatic borrow loan generated by cross margin order, diabled by default + } + if isMarketOrder and (side == 'buy'): + quoteAmount = None + createMarketBuyOrderRequiresPrice = True + createMarketBuyOrderRequiresPrice, params = self.handle_option_and_params(params, 'createOrder', 'createMarketBuyOrderRequiresPrice', True) + cost = self.safe_number(params, 'cost') + params = self.omit(params, 'cost') + if cost is not None: + quoteAmount = self.cost_to_precision(symbol, cost) + elif createMarketBuyOrderRequiresPrice: + if price is None: + raise InvalidOrder(self.id + ' createOrder() requires the price argument for market buy orders to calculate the total cost to spend(amount * price), alternatively set the createMarketBuyOrderRequiresPrice option or param to False and pass the cost to spend(quote quantity) in the amount argument') + else: + amountString = self.number_to_string(amount) + priceString = self.number_to_string(price) + costRequest = Precise.string_mul(amountString, priceString) + quoteAmount = self.cost_to_precision(symbol, costRequest) + else: + quoteAmount = self.cost_to_precision(symbol, amount) + request['amount'] = quoteAmount + else: + request['amount'] = self.amount_to_precision(symbol, amount) + if isLimitOrder: + request['price'] = self.price_to_precision(symbol, price) + if timeInForce is not None: + request['time_in_force'] = timeInForce + clientOrderId = self.safe_string_2(params, 'text', 'clientOrderId') + textIsRequired = self.safe_bool(params, 'textIsRequired', False) + if clientOrderId is not None: + # user-defined, must follow the rules if not empty + # prefixed with t- + # no longer than 28 bytes without t- prefix + # can only include 0-9, A-Z, a-z, underscores(_), hyphens(-) or dots(.) + if len(clientOrderId) > 28: + raise BadRequest(self.id + ' createOrder() clientOrderId or text param must be up to 28 characters') + params = self.omit(params, ['text', 'clientOrderId', 'textIsRequired']) + if clientOrderId[0] != 't': + clientOrderId = 't-' + clientOrderId + request['text'] = clientOrderId + else: + if textIsRequired: + # batchOrders requires text in the request + request['text'] = 't-' + self.uuid16() + else: + if market['option']: + raise NotSupported(self.id + ' createOrder() conditional option orders are not supported') + if contract: + # contract conditional order + request = { + 'initial': { + 'contract': market['id'], + 'size': amount, # positive = buy, negative = sell, set to 0 to close the position + # 'price': '0' if (price == 0) else self.price_to_precision(symbol, price), # set to 0 to use market price + # 'close': False, # set to True if trying to close the position + # 'tif': 'gtc', # gtc, ioc, if using market price, only ioc is supported + # 'text': clientOrderId, # web, api, app + # 'reduce_only': False, + }, + 'settle': market['settleId'], + } + if type == 'market': + request['initial']['price'] = '0' + else: + request['initial']['price'] = '0' if (price == 0) else self.price_to_precision(symbol, price) + if trigger is None: + rule = None + triggerOrderPrice = None + if isStopLossOrder: + # we trigger orders be aliases for stopLoss orders because + # gateio doesn't accept conventional trigger orders for spot markets + rule = 1 if (side == 'buy') else 2 + triggerOrderPrice = self.price_to_precision(symbol, stopLossPrice) + elif isTakeProfitOrder: + rule = 2 if (side == 'buy') else 1 + triggerOrderPrice = self.price_to_precision(symbol, takeProfitPrice) + priceType = self.safe_integer(params, 'price_type', 0) + if priceType < 0 or priceType > 2: + raise BadRequest(self.id + ' createOrder() price_type should be 0 latest deal price, 1 mark price, 2 index price') + params = self.omit(params, ['price_type']) + request['trigger'] = { + # 'strategy_type': 0, # 0 = by price, 1 = by price gap, only 0 is supported currently + 'price_type': priceType, # 0 latest deal price, 1 mark price, 2 index price + 'price': self.price_to_precision(symbol, triggerOrderPrice), # price or gap + 'rule': rule, # 1 means price_type >= price, 2 means price_type <= price + # 'expiration': expiration, how many seconds to wait for the condition to be triggered before cancelling the order + } + if reduceOnly is not None: + request['initial']['reduce_only'] = reduceOnly + if timeInForce is not None: + request['initial']['tif'] = timeInForce + else: + # spot conditional order + options = self.safe_value(self.options, 'createOrder', {}) + marginMode = None + marginMode, params = self.get_margin_mode(True, params) + if timeInForce is None: + timeInForce = 'gtc' + request = { + 'put': { + 'type': type, + 'side': side, + 'price': self.price_to_precision(symbol, price), + 'amount': self.amount_to_precision(symbol, amount), + 'account': marginMode, + 'time_in_force': timeInForce, # gtc, ioc(ioc is for taker only, so shouldnt't be in conditional order) + }, + 'market': market['id'], + } + if trigger is None: + defaultExpiration = self.safe_integer(options, 'expiration') + expiration = self.safe_integer(params, 'expiration', defaultExpiration) + rule = None + triggerOrderPrice = None + if isStopLossOrder: + # we trigger orders be aliases for stopLoss orders because + # gateio doesn't accept conventional trigger orders for spot markets + rule = '>=' if (side == 'buy') else '<=' + triggerOrderPrice = self.price_to_precision(symbol, stopLossPrice) + elif isTakeProfitOrder: + rule = '<=' if (side == 'buy') else '>=' + triggerOrderPrice = self.price_to_precision(symbol, takeProfitPrice) + request['trigger'] = { + 'price': self.price_to_precision(symbol, triggerOrderPrice), + 'rule': rule, # >= triggered when market price larger than or equal to price field, <= triggered when market price less than or equal to price field + 'expiration': expiration, # required, how long(in seconds) to wait for the condition to be triggered before cancelling the order + } + return self.extend(request, params) + + def create_market_buy_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market buy order by providing the symbol and cost + + https://www.gate.com/docs/developers/apiv4/en/#create-an-order + + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.unifiedAccount]: set to True for creating a unified account order + :returns dict: an `order structure ` + """ + self.load_markets() + self.load_unified_status() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' createMarketBuyOrderWithCost() supports spot orders only') + params['createMarketBuyOrderRequiresPrice'] = False + return self.create_order(symbol, 'market', 'buy', cost, None, params) + + def edit_order_request(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + market = self.market(symbol) + marketType = None + marketType, params = self.handle_market_type_and_params('editOrder', market, params) + account = self.convert_type_to_account(marketType) + isUnifiedAccount = False + isUnifiedAccount, params = self.handle_option_and_params(params, 'editOrder', 'unifiedAccount') + if isUnifiedAccount: + account = 'unified' + isLimitOrder = (type == 'limit') + if account == 'spot': + if not isLimitOrder: + # exchange doesn't have market orders for spot + raise InvalidOrder(self.id + ' editOrder() does not support ' + type + ' orders for ' + marketType + ' markets') + request: dict = { + 'order_id': str(id), + 'currency_pair': market['id'], + 'account': account, + } + if amount is not None: + if market['spot']: + request['amount'] = self.amount_to_precision(symbol, amount) + else: + if side == 'sell': + request['size'] = self.parse_to_numeric(Precise.string_neg(self.amount_to_precision(symbol, amount))) + else: + request['size'] = self.parse_to_numeric(self.amount_to_precision(symbol, amount)) + if price is not None: + request['price'] = self.price_to_precision(symbol, price) + if not market['spot']: + request['settle'] = market['settleId'] + return self.extend(request, params) + + def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + """ + edit a trade order, gate currently only supports the modification of the price or amount fields + + https://www.gate.com/docs/developers/apiv4/en/#amend-an-order + https://www.gate.com/docs/developers/apiv4/en/#amend-an-order-2 + + :param str id: order id + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of the currency you want to trade in units of the base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.unifiedAccount]: set to True for editing an order in a unified account + :returns dict: an `order structure ` + """ + self.load_markets() + self.load_unified_status() + market = self.market(symbol) + extendedRequest = self.edit_order_request(id, symbol, type, side, amount, price, params) + response = None + if market['spot']: + response = self.privateSpotPatchOrdersOrderId(extendedRequest) + else: + response = self.privateFuturesPutSettleOrdersOrderId(extendedRequest) + # + # { + # "id": "243233276443", + # "text": "apiv4", + # "create_time": "1670908873", + # "update_time": "1670914102", + # "create_time_ms": 1670908873077, + # "update_time_ms": 1670914102241, + # "status": "open", + # "currency_pair": "ADA_USDT", + # "type": "limit", + # "account": "spot", + # "side": "sell", + # "amount": "10", + # "price": "0.6", + # "time_in_force": "gtc", + # "iceberg": "0", + # "left": "10", + # "fill_price": "0", + # "filled_total": "0", + # "fee": "0", + # "fee_currency": "USDT", + # "point_fee": "0", + # "gt_fee": "0", + # "gt_maker_fee": "0", + # "gt_taker_fee": "0", + # "gt_discount": False, + # "rebated_fee": "0", + # "rebated_fee_currency": "ADA" + # } + # + return self.parse_order(response, market) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'open': 'open', + '_new': 'open', + 'filled': 'closed', + 'cancelled': 'canceled', + 'liquidated': 'closed', + 'ioc': 'canceled', + 'failed': 'canceled', + 'expired': 'canceled', + 'finished': 'closed', + 'finish': 'closed', + 'succeeded': 'closed', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # SPOT + # createOrder/cancelOrder/fetchOrder/editOrder + # + # { + # "id": "62364648575", + # "text": "apiv4", + # "create_time": "1626354834", + # "update_time": "1626354834", + # "create_time_ms": "1626354833544", + # "update_time_ms": "1626354833544", + # "status": "open", + # "currency_pair": "BTC_USDT", + # "type": "limit", + # "account": "spot", + # "side": "buy", + # "amount": "0.0001", + # "price": "30000", + # "time_in_force": "gtc", + # "iceberg": "0", + # "left": "0.0001", + # "fill_price": "0", + # "filled_total": "0", + # "fee": "0", + # "fee_currency": "BTC", + # "point_fee": "0", + # "gt_fee": "0", + # "gt_discount": True, + # "rebated_fee": "0", + # "rebated_fee_currency": "USDT" + # } + # + # SPOT TRIGGER ORDERS + # createOrder + # + # { + # "id": 12604556 + # } + # + # fetchOrder/cancelOrder + # + # { + # "market": "ADA_USDT", + # "user": 6392049, + # "trigger": { + # "price": "1.08", # stopPrice + # "rule": "\u003e=", + # "expiration": 86400 + # }, + # "put": { + # "type": "limit", + # "side": "buy", + # "price": "1.08", # order price + # "amount": "1.00000000000000000000", + # "account": "normal", + # "time_in_force": "gtc" + # }, + # "id": 71639298, + # "ctime": 1643945985, + # "status": "open" + # } + # + # FUTURE, SWAP AND OPTION + # createOrder/cancelOrder/fetchOrder + # + # { + # "id": 123028481731, + # "contract": "ADA_USDT", + # "mkfr": "-0.00005", + # "tkfr": "0.00048", + # "tif": "ioc", + # "is_reduce_only": False, + # "create_time": 1643950262.68, + # "finish_time": 1643950262.68, + # "price": "0", + # "size": 1, + # "refr": "0", + # "left":0, + # "text": "api", + # "fill_price": "1.05273", + # "user":6329238, + # "finish_as": "filled", + # "status": "finished", + # "is_liq": False, + # "refu":0, + # "is_close": False, + # "iceberg": 0 + # } + # + # TRIGGER ORDERS(FUTURE AND SWAP) + # createOrder + # + # { + # "id": 12604556 + # } + # + # fetchOrder/cancelOrder + # + # { + # "user": 6320300, + # "trigger": { + # "strategy_type": 0, + # "price_type": 0, + # "price": "1.03", # stopPrice + # "rule": 2, + # "expiration": 0 + # }, + # "initial": { + # "contract": "ADA_USDT", + # "size": -1, + # "price": "1.02", + # "tif": "gtc", + # "text": "", + # "iceberg": 0, + # "is_close": False, + # "is_reduce_only": False, + # "auto_size": "" + # }, + # "id": 126393906, + # "trade_id": 0, + # "status": "open", + # "reason": "", + # "create_time": 1643953482, + # "finish_time": 1643953482, + # "is_stop_order": False, + # "stop_trigger": { + # "rule": 0, + # "trigger_price": "", + # "order_price": "" + # }, + # "me_order_id": 0, + # "order_type": "" + # } + # + # { + # "text": "t-d18baf9ac44d82e2", + # "succeeded": False, + # "label": "BALANCE_NOT_ENOUGH", + # "message": "Not enough balance" + # } + # + # {"user_id":10406147,"id":"id","succeeded":false,"message":"INVALID_PROTOCOL","label":"INVALID_PROTOCOL"} + # + succeeded = self.safe_bool(order, 'succeeded', True) + if not succeeded: + # cancelOrders response + return self.safe_order({ + 'clientOrderId': self.safe_string(order, 'text'), + 'info': order, + 'status': 'rejected', + 'id': self.safe_string(order, 'id'), + }) + put = self.safe_value_2(order, 'put', 'initial', {}) + trigger = self.safe_value(order, 'trigger', {}) + contract = self.safe_string(put, 'contract') + type = self.safe_string(put, 'type') + timeInForce = self.safe_string_upper_2(put, 'time_in_force', 'tif') + amount = self.safe_string_2(put, 'amount', 'size') + side = self.safe_string(put, 'side') + price = self.safe_string(put, 'price') + contract = self.safe_string(order, 'contract', contract) + type = self.safe_string(order, 'type', type) + timeInForce = self.safe_string_upper_2(order, 'time_in_force', 'tif', timeInForce) + if timeInForce == 'POC': + timeInForce = 'PO' + postOnly = (timeInForce == 'PO') + amount = self.safe_string_2(order, 'amount', 'size', amount) + side = self.safe_string(order, 'side', side) + price = self.safe_string(order, 'price', price) + remainingString = self.safe_string(order, 'left') + cost = self.safe_string(order, 'filled_total') + triggerPrice = self.safe_number(trigger, 'price') + average = self.safe_number_2(order, 'avg_deal_price', 'fill_price') + if triggerPrice: + remainingString = amount + cost = '0' + if contract: + isMarketOrder = Precise.string_equals(price, '0') and (timeInForce == 'IOC') + type = 'market' if isMarketOrder else 'limit' + side = 'buy' if Precise.string_gt(amount, '0') else 'sell' + rawStatus = self.safe_string_n(order, ['finish_as', 'status', 'open']) + timestamp = self.safe_integer(order, 'create_time_ms') + if timestamp is None: + timestamp = self.safe_timestamp_2(order, 'create_time', 'ctime') + lastTradeTimestamp = self.safe_integer(order, 'update_time_ms') + if lastTradeTimestamp is None: + lastTradeTimestamp = self.safe_timestamp_2(order, 'update_time', 'finish_time') + marketType = 'contract' + if ('currency_pair' in order) or ('market' in order): + marketType = 'spot' + exchangeSymbol = self.safe_string_2(order, 'currency_pair', 'market', contract) + symbol = self.safe_symbol(exchangeSymbol, market, '_', marketType) + # Everything below self(above return) is related to fees + fees = [] + gtFee = self.safe_string(order, 'gt_fee') + if gtFee is not None: + fees.append({ + 'currency': 'GT', + 'cost': gtFee, + }) + fee = self.safe_string(order, 'fee') + if fee is not None: + fees.append({ + 'currency': self.safe_currency_code(self.safe_string(order, 'fee_currency')), + 'cost': fee, + }) + rebate = self.safe_string(order, 'rebated_fee') + if rebate is not None: + fees.append({ + 'currency': self.safe_currency_code(self.safe_string(order, 'rebated_fee_currency')), + 'cost': Precise.string_neg(rebate), + }) + numFeeCurrencies = len(fees) + multipleFeeCurrencies = numFeeCurrencies > 1 + status = self.parse_order_status(rawStatus) + remaining = Precise.string_abs(remainingString) + # handle spot market buy + account = self.safe_string(order, 'account') # using self instead of market type because of the conflicting ids + if account == 'spot': + averageString = self.safe_string(order, 'avg_deal_price') + average = self.parse_number(averageString) + if (type == 'market') and (side == 'buy'): + remaining = Precise.string_div(remainingString, averageString) + price = None # arrives + cost = amount + amount = Precise.string_div(amount, averageString) + return self.safe_order({ + 'id': self.safe_string(order, 'id'), + 'clientOrderId': self.safe_string(order, 'text'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': lastTradeTimestamp, + 'status': status, + 'symbol': symbol, + 'type': type, + 'timeInForce': timeInForce, + 'postOnly': postOnly, + 'reduceOnly': self.safe_value(order, 'is_reduce_only'), + 'side': side, + 'price': price, + 'triggerPrice': triggerPrice, + 'average': average, + 'amount': Precise.string_abs(amount), + 'cost': Precise.string_abs(cost), + 'filled': None, + 'remaining': remaining, + 'fee': None if multipleFeeCurrencies else self.safe_value(fees, 0), + 'fees': fees if multipleFeeCurrencies else [], + 'trades': None, + 'info': order, + }, market) + + def fetch_order_request(self, id: str, symbol: Str = None, params={}): + market = None if (symbol is None) else self.market(symbol) + trigger = self.safe_bool_n(params, ['trigger', 'is_stop_order', 'stop'], False) + params = self.omit(params, ['is_stop_order', 'stop', 'trigger']) + clientOrderId = self.safe_string_2(params, 'text', 'clientOrderId') + orderId = id + if clientOrderId is not None: + params = self.omit(params, ['text', 'clientOrderId']) + if clientOrderId[0] != 't': + clientOrderId = 't-' + clientOrderId + orderId = clientOrderId + type, query = self.handle_market_type_and_params('fetchOrder', market, params) + contract = (type == 'swap') or (type == 'future') or (type == 'option') + request, requestParams = self.prepare_request(market, type, query) if contract else self.spot_order_prepare_request(market, trigger, query) + request['order_id'] = str(orderId) + return [request, requestParams] + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + Retrieves information on an order + + https://www.gate.com/docs/developers/apiv4/en/#get-a-single-order + https://www.gate.com/docs/developers/apiv4/en/#get-a-single-order-2 + https://www.gate.com/docs/developers/apiv4/en/#get-a-single-order-3 + https://www.gate.com/docs/developers/apiv4/en/#get-a-single-order-4 + + :param str id: Order id + :param str symbol: Unified market symbol, *required for spot and margin* + :param dict [params]: Parameters specified by the exchange api + :param bool [params.trigger]: True if the order being fetched is a trigger order + :param str [params.marginMode]: 'cross' or 'isolated' - marginMode for margin trading if not provided self.options['defaultMarginMode'] is used + :param str [params.type]: 'spot', 'swap', or 'future', if not provided self.options['defaultMarginMode'] is used + :param str [params.settle]: 'btc' or 'usdt' - settle currency for perpetual swap and future - market settle currency is used if symbol is not None, default="usdt" for swap and "btc" for future + :param bool [params.unifiedAccount]: set to True for fetching a unified account order + :returns: An `order structure ` + """ + self.load_markets() + self.load_unified_status() + market = None if (symbol is None) else self.market(symbol) + result = self.handle_market_type_and_params('fetchOrder', market, params) + type = self.safe_string(result, 0) + trigger = self.safe_bool_n(params, ['trigger', 'is_stop_order', 'stop'], False) + request, requestParams = self.fetch_order_request(id, symbol, params) + response = None + if type == 'spot' or type == 'margin': + if trigger: + response = self.privateSpotGetPriceOrdersOrderId(self.extend(request, requestParams)) + else: + response = self.privateSpotGetOrdersOrderId(self.extend(request, requestParams)) + elif type == 'swap': + if trigger: + response = self.privateFuturesGetSettlePriceOrdersOrderId(self.extend(request, requestParams)) + else: + response = self.privateFuturesGetSettleOrdersOrderId(self.extend(request, requestParams)) + elif type == 'future': + if trigger: + response = self.privateDeliveryGetSettlePriceOrdersOrderId(self.extend(request, requestParams)) + else: + response = self.privateDeliveryGetSettleOrdersOrderId(self.extend(request, requestParams)) + elif type == 'option': + response = self.privateOptionsGetOrdersOrderId(self.extend(request, requestParams)) + else: + raise NotSupported(self.id + ' fetchOrder() not support self market type') + return self.parse_order(response, market) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://www.gate.com/docs/developers/apiv4/en/#list-all-open-orders + https://www.gate.com/docs/developers/apiv4/en/#retrieve-running-auto-order-list + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.trigger]: True for fetching trigger orders + :param str [params.type]: spot, margin, swap or future, if not provided self.options['defaultType'] is used + :param str [params.marginMode]: 'cross' or 'isolated' - marginMode for type='margin', if not provided self.options['defaultMarginMode'] is used + :param bool [params.unifiedAccount]: set to True for fetching unified account orders + :returns Order[]: a list of `order structures ` + """ + return self.fetch_orders_by_status('open', symbol, since, limit, params) + + def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://www.gate.com/docs/developers/apiv4/en/#list-orders + https://www.gate.com/docs/developers/apiv4/en/#retrieve-running-auto-order-list + https://www.gate.com/docs/developers/apiv4/en/#list-futures-orders + https://www.gate.com/docs/developers/apiv4/en/#list-all-auto-orders + https://www.gate.com/docs/developers/apiv4/en/#list-futures-orders-2 + https://www.gate.com/docs/developers/apiv4/en/#list-all-auto-orders-2 + https://www.gate.com/docs/developers/apiv4/en/#list-options-orders + https://www.gate.com/docs/developers/apiv4/en/#list-futures-orders-by-time-range + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.trigger]: True for fetching trigger orders + :param str [params.type]: spot, swap or future, if not provided self.options['defaultType'] is used + :param str [params.marginMode]: 'cross' or 'isolated' - marginMode for margin trading if not provided self.options['defaultMarginMode'] is used + :param boolean [params.historical]: *swap only* True for using historical endpoint + :param bool [params.unifiedAccount]: set to True for fetching unified account orders + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + self.load_unified_status() + until = self.safe_integer(params, 'until') + market = None + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + res = self.handle_market_type_and_params('fetchClosedOrders', market, params) + type = self.safe_string(res, 0) + useHistorical = False + useHistorical, params = self.handle_option_and_params(params, 'fetchClosedOrders', 'historical', False) + if not useHistorical and ((since is None and until is None) or (type != 'swap')): + return self.fetch_orders_by_status('finished', symbol, since, limit, params) + params = self.omit(params, 'type') + request = {} + request, params = self.prepare_request(market, type, params) + if since is not None: + request['from'] = self.parse_to_int(since / 1000) + if until is not None: + params = self.omit(params, 'until') + request['to'] = self.parse_to_int(until / 1000) + if limit is not None: + request['limit'] = limit + response = self.privateFuturesGetSettleOrdersTimerange(self.extend(request, params)) + return self.parse_orders(response, market, since, limit) + + def prepare_orders_by_status_request(self, status, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + market = None + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + trigger: Bool = None + trigger, params = self.handle_param_bool_2(params, 'trigger', 'stop') + type: Str = None + type, params = self.handle_market_type_and_params('fetchOrdersByStatus', market, params) + spot = (type == 'spot') or (type == 'margin') + request: dict = {} + request, params = self.multi_order_spot_prepare_request(market, trigger, params) if spot else self.prepare_request(market, type, params) + if spot and trigger: + request = self.omit(request, 'account') + if status == 'closed': + status = 'finished' + request['status'] = status + if limit is not None: + request['limit'] = limit + if spot: + if since is not None: + request['from'] = self.parse_to_int(since / 1000) + until = self.safe_integer(params, 'until') + if until is not None: + params = self.omit(params, 'until') + request['to'] = self.parse_to_int(until / 1000) + lastId, finalParams = self.handle_param_string_2(params, 'lastId', 'last_id') + if lastId is not None: + request['last_id'] = lastId + return [request, finalParams] + + def fetch_orders_by_status(self, status, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + self.load_markets() + self.load_unified_status() + market = None + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + # don't omit here, omits done in prepareOrdersByStatusRequest + trigger: Bool = self.safe_bool_2(params, 'trigger', 'stop') + res = self.handle_market_type_and_params('fetchOrdersByStatus', market, params) + type = self.safe_string(res, 0) + request, requestParams = self.prepare_orders_by_status_request(status, symbol, since, limit, params) + spot = (type == 'spot') or (type == 'margin') + openStatus = (status == 'open') + openSpotOrders = spot and openStatus and not trigger + response = None + if spot: + if not trigger: + if openStatus: + response = self.privateSpotGetOpenOrders(self.extend(request, requestParams)) + else: + response = self.privateSpotGetOrders(self.extend(request, requestParams)) + else: + response = self.privateSpotGetPriceOrders(self.extend(request, requestParams)) + elif type == 'swap': + if trigger: + response = self.privateFuturesGetSettlePriceOrders(self.extend(request, requestParams)) + else: + response = self.privateFuturesGetSettleOrders(self.extend(request, requestParams)) + elif type == 'future': + if trigger: + response = self.privateDeliveryGetSettlePriceOrders(self.extend(request, requestParams)) + else: + response = self.privateDeliveryGetSettleOrders(self.extend(request, requestParams)) + elif type == 'option': + response = self.privateOptionsGetOrders(self.extend(request, requestParams)) + else: + raise NotSupported(self.id + ' fetchOrders() not support self market type') + # + # spot open orders + # + # [ + # { + # "currency_pair": "ADA_USDT", + # "total": 2, + # "orders": [ + # { + # "id": "155498539874", + # "text": "apiv4", + # "create_time": "1652406843", + # "update_time": "1652406843", + # "create_time_ms": 1652406843295, + # "update_time_ms": 1652406843295, + # "status": "open", + # "currency_pair": "ADA_USDT", + # "type": "limit", + # "account": "spot", + # "side": "buy", + # "amount": "3", + # "price": "0.35", + # "time_in_force": "gtc", + # "iceberg": "0", + # "left": "3", + # "fill_price": "0", + # "filled_total": "0", + # "fee": "0", + # "fee_currency": "ADA", + # "point_fee": "0", + # "gt_fee": "0", + # "gt_discount": False, + # "rebated_fee": "0", + # "rebated_fee_currency": "USDT" + # }, + # ... + # ] + # }, + # ... + # ] + # + # spot + # + # [ + # { + # "id": "8834234273", + # "text": "3", + # "create_time": "1635406193", + # "update_time": "1635406193", + # "create_time_ms": 1635406193361, + # "update_time_ms": 1635406193361, + # "status": "closed", + # "currency_pair": "BTC_USDT", + # "type": "limit", + # "account": "spot", # margin for margin orders + # "side": "sell", + # "amount": "0.0002", + # "price": "58904.01", + # "time_in_force": "gtc", + # "iceberg": "0", + # "left": "0.0000", + # "fill_price": "11.790516", + # "filled_total": "11.790516", + # "fee": "0.023581032", + # "fee_currency": "USDT", + # "point_fee": "0", + # "gt_fee": "0", + # "gt_discount": False, + # "rebated_fee_currency": "BTC" + # } + # ] + # + # spot trigger + # + # [ + # { + # "market": "ADA_USDT", + # "user": 10406147, + # "trigger": { + # "price": "0.65", + # "rule": "\u003c=", + # "expiration": 86400 + # }, + # "put": { + # "type": "limit", + # "side": "sell", + # "price": "0.65", + # "amount": "2.00000000000000000000", + # "account": "normal", # margin for margin orders + # "time_in_force": "gtc" + # }, + # "id": 8449909, + # "ctime": 1652188982, + # "status": "open" + # } + # ] + # + # swap + # + # [ + # { + # "status": "finished", + # "size": -1, + # "left": 0, + # "id": 82750739203, + # "is_liq": False, + # "is_close": False, + # "contract": "BTC_USDT", + # "text": "web", + # "fill_price": "60721.3", + # "finish_as": "filled", + # "iceberg": 0, + # "tif": "ioc", + # "is_reduce_only": True, + # "create_time": 1635403475.412, + # "finish_time": 1635403475.4127, + # "price": "0" + # } + # ] + # + # option + # + # [ + # { + # "id": 2593450699, + # "contract": "BTC_USDT-20230601-27500-C", + # "mkfr": "0.0003", + # "tkfr": "0.0003", + # "tif": "gtc", + # "is_reduce_only": False, + # "create_time": 1685503873, + # "price": "200", + # "size": 1, + # "refr": "0", + # "left": 1, + # "text": "api", + # "fill_price": "0", + # "user": 5691076, + # "status": "open", + # "is_liq": False, + # "refu": 0, + # "is_close": False, + # "iceberg": 0 + # } + # ] + # + result = response + if openSpotOrders: + result = [] + for i in range(0, len(response)): + ordersInner = self.safe_value(response[i], 'orders') + result = self.array_concat(result, ordersInner) + orders = self.parse_orders(result, market, since, limit) + return self.filter_by_symbol_since_limit(orders, symbol, since, limit) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + Cancels an open order + + https://www.gate.com/docs/developers/apiv4/en/#cancel-a-single-order + https://www.gate.com/docs/developers/apiv4/en/#cancel-a-single-order-2 + https://www.gate.com/docs/developers/apiv4/en/#cancel-a-single-order-3 + https://www.gate.com/docs/developers/apiv4/en/#cancel-a-single-order-4 + + :param str id: Order id + :param str symbol: Unified market symbol + :param dict [params]: Parameters specified by the exchange api + :param bool [params.trigger]: True if the order to be cancelled is a trigger order + :param bool [params.unifiedAccount]: set to True for canceling unified account orders + :returns: An `order structure ` + """ + self.load_markets() + self.load_unified_status() + market = None if (symbol is None) else self.market(symbol) + trigger = self.safe_bool_n(params, ['is_stop_order', 'stop', 'trigger'], False) + params = self.omit(params, ['is_stop_order', 'stop', 'trigger']) + type, query = self.handle_market_type_and_params('cancelOrder', market, params) + request, requestParams = self.spot_order_prepare_request(market, trigger, query) if (type == 'spot' or type == 'margin') else self.prepare_request(market, type, query) + request['order_id'] = id + response = None + if type == 'spot' or type == 'margin': + if trigger: + response = self.privateSpotDeletePriceOrdersOrderId(self.extend(request, requestParams)) + else: + response = self.privateSpotDeleteOrdersOrderId(self.extend(request, requestParams)) + elif type == 'swap': + if trigger: + response = self.privateFuturesDeleteSettlePriceOrdersOrderId(self.extend(request, requestParams)) + else: + response = self.privateFuturesDeleteSettleOrdersOrderId(self.extend(request, requestParams)) + elif type == 'future': + if trigger: + response = self.privateDeliveryDeleteSettlePriceOrdersOrderId(self.extend(request, requestParams)) + else: + response = self.privateDeliveryDeleteSettleOrdersOrderId(self.extend(request, requestParams)) + elif type == 'option': + response = self.privateOptionsDeleteOrdersOrderId(self.extend(request, requestParams)) + else: + raise NotSupported(self.id + ' cancelOrder() not support self market type') + # + # spot + # + # { + # "id": "95282841887", + # "text": "apiv4", + # "create_time": "1637383156", + # "update_time": "1637383235", + # "create_time_ms": 1637383156017, + # "update_time_ms": 1637383235085, + # "status": "cancelled", + # "currency_pair": "ETH_USDT", + # "type": "limit", + # "account": "spot", + # "side": "buy", + # "amount": "0.01", + # "price": "3500", + # "time_in_force": "gtc", + # "iceberg": "0", + # "left": "0.01", + # "fill_price": "0", + # "filled_total": "0", + # "fee": "0", + # "fee_currency": "ETH", + # "point_fee": "0", + # "gt_fee": "0", + # "gt_discount": False, + # "rebated_fee": "0", + # "rebated_fee_currency": "USDT" + # } + # + # spot conditional + # + # { + # "market": "ETH_USDT", + # "user": 2436035, + # "trigger": { + # "price": "3500", + # "rule": "\u003c=", + # "expiration": 86400 + # }, + # "put": { + # "type": "limit", + # "side": "buy", + # "price": "3500", + # "amount": "0.01000000000000000000", + # "account": "normal", + # "time_in_force": "gtc" + # }, + # "id": 5891843, + # "ctime": 1637382379, + # "ftime": 1637382673, + # "status": "canceled" + # } + # + # swap, future and option + # + # { + # "id": "82241928192", + # "contract": "BTC_USDT", + # "mkfr": "0", + # "tkfr": "0.0005", + # "tif": "gtc", + # "is_reduce_only": False, + # "create_time": "1635196145.06", + # "finish_time": "1635196233.396", + # "price": "61000", + # "size": "4", + # "refr": "0", + # "left": "4", + # "text": "web", + # "fill_price": "0", + # "user": "6693577", + # "finish_as": "cancelled", + # "status": "finished", + # "is_liq": False, + # "refu": "0", + # "is_close": False, + # "iceberg": "0", + # } + # + return self.parse_order(response, market) + + def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): + """ + cancel multiple orders + + https://www.gate.com/docs/developers/apiv4/en/#cancel-a-batch-of-orders-with-an-id-list + https://www.gate.com/docs/developers/apiv4/en/#cancel-a-batch-of-orders-with-an-id-list-2 + + :param str[] ids: order ids + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.unifiedAccount]: set to True for canceling unified account orders + :returns dict: an list of `order structures ` + """ + self.load_markets() + self.load_unified_status() + market = None + if symbol is not None: + market = self.market(symbol) + type = None + defaultSettle = 'usdt' if (market is None) else market['settle'] + settle = self.safe_string_lower(params, 'settle', defaultSettle) + type, params = self.handle_market_type_and_params('cancelOrders', market, params) + isSpot = (type == 'spot') + if isSpot and (symbol is None): + raise ArgumentsRequired(self.id + ' cancelOrders requires a symbol argument for spot markets') + if isSpot: + ordersRequests = [] + for i in range(0, len(ids)): + id = ids[i] + orderItem: dict = { + 'id': id, + 'symbol': symbol, + } + ordersRequests.append(orderItem) + return self.cancel_orders_for_symbols(ordersRequests, params) + request = { + 'settle': settle, + } + finalList = [request] # hacky but needs to be done here + for i in range(0, len(ids)): + finalList.append(ids[i]) + response = self.privateFuturesPostSettleBatchCancelOrders(finalList) + return self.parse_orders(response) + + def cancel_orders_for_symbols(self, orders: List[CancellationRequest], params={}): + """ + cancel multiple orders for multiple symbols + + https://www.gate.com/docs/developers/apiv4/en/#cancel-a-batch-of-orders-with-an-id-list + + :param CancellationRequest[] orders: list of order ids with symbol, example [{"id": "a", "symbol": "BTC/USDT"}, {"id": "b", "symbol": "ETH/USDT"}] + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str[] [params.clientOrderIds]: client order ids + :param bool [params.unifiedAccount]: set to True for canceling unified account orders + :returns dict: an list of `order structures ` + """ + self.load_markets() + self.load_unified_status() + ordersRequests = [] + for i in range(0, len(orders)): + order = orders[i] + symbol = self.safe_string(order, 'symbol') + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' cancelOrdersForSymbols() supports only spot markets') + id = self.safe_string(order, 'id') + orderItem: dict = { + 'id': id, + 'currency_pair': market['id'], + } + ordersRequests.append(orderItem) + response = self.privateSpotPostCancelBatchOrders(ordersRequests) + # + # [ + # { + # "currency_pair": "BTC_USDT", + # "id": "123456" + # } + # ] + # + return self.parse_orders(response) + + def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders + + https://www.gate.com/docs/developers/apiv4/en/#cancel-all-open-orders-in-specified-currency-pair + https://www.gate.com/docs/developers/apiv4/en/#cancel-all-open-orders-matched + https://www.gate.com/docs/developers/apiv4/en/#cancel-all-open-orders-matched-2 + https://www.gate.com/docs/developers/apiv4/en/#cancel-all-open-orders-matched-3 + + :param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.unifiedAccount]: set to True for canceling unified account orders + :returns dict[]: a list of `order structures ` + """ + self.load_markets() + self.load_unified_status() + market = None if (symbol is None) else self.market(symbol) + trigger = self.safe_bool_2(params, 'stop', 'trigger') + params = self.omit(params, ['stop', 'trigger']) + type, query = self.handle_market_type_and_params('cancelAllOrders', market, params) + request, requestParams = self.multi_order_spot_prepare_request(market, trigger, query) if (type == 'spot') else self.prepare_request(market, type, query) + response = None + if type == 'spot' or type == 'margin': + if trigger: + response = self.privateSpotDeletePriceOrders(self.extend(request, requestParams)) + else: + response = self.privateSpotDeleteOrders(self.extend(request, requestParams)) + elif type == 'swap': + if trigger: + response = self.privateFuturesDeleteSettlePriceOrders(self.extend(request, requestParams)) + else: + response = self.privateFuturesDeleteSettleOrders(self.extend(request, requestParams)) + elif type == 'future': + if trigger: + response = self.privateDeliveryDeleteSettlePriceOrders(self.extend(request, requestParams)) + else: + response = self.privateDeliveryDeleteSettleOrders(self.extend(request, requestParams)) + elif type == 'option': + response = self.privateOptionsDeleteOrders(self.extend(request, requestParams)) + else: + raise NotSupported(self.id + ' cancelAllOrders() not support self market type') + # + # [ + # { + # "id": 139797004085, + # "contract": "ADA_USDT", + # "mkfr": "0", + # "tkfr": "0.0005", + # "tif": "gtc", + # "is_reduce_only": False, + # "create_time": 1647911169.343, + # "finish_time": 1647911226.849, + # "price": "0.8", + # "size": 1, + # "refr": "0.3", + # "left": 1, + # "text": "api", + # "fill_price": "0", + # "user": 6693577, + # "finish_as": "cancelled", + # "status": "finished", + # "is_liq": False, + # "refu": 2436035, + # "is_close": False, + # "iceberg": 0 + # } + # ... + # ] + # + return self.parse_orders(response, market) + + def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://www.gate.com/docs/developers/apiv4/en/#transfer-between-trading-accounts + + :param str code: unified currency code for currency being transferred + :param float amount: the amount of currency to transfer + :param str fromAccount: the account to transfer currency from + :param str toAccount: the account to transfer currency to + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.symbol]: Unified market symbol *required for type == margin* + :returns: A `transfer structure ` + """ + self.load_markets() + currency = self.currency(code) + fromId = self.convert_type_to_account(fromAccount) + toId = self.convert_type_to_account(toAccount) + truncated = self.currency_to_precision(code, amount) + request: dict = { + 'currency': currency['id'], # todo: currencies have network-junctions + 'amount': truncated, + } + if not (fromId in self.options['accountsByType']): + request['from'] = 'margin' + request['currency_pair'] = fromId + else: + request['from'] = fromId + if not (toId in self.options['accountsByType']): + request['to'] = 'margin' + request['currency_pair'] = toId + else: + request['to'] = toId + if fromId == 'margin' or toId == 'margin': + symbol = self.safe_string_2(params, 'symbol', 'currency_pair') + if symbol is None: + raise ArgumentsRequired(self.id + ' transfer requires params["symbol"] for isolated margin transfers') + market = self.market(symbol) + request['currency_pair'] = market['id'] + params = self.omit(params, 'symbol') + if (toId == 'futures') or (toId == 'delivery') or (fromId == 'futures') or (fromId == 'delivery'): + request['settle'] = currency['id'] # todo: currencies have network-junctions + response = self.privateWalletPostTransfers(self.extend(request, params)) + # + # according to the docs(however actual response seems to be an empty string '') + # + # { + # "currency": "BTC", + # "from": "spot", + # "to": "margin", + # "amount": "1", + # "currency_pair": "BTC_USDT" + # } + # + return self.parse_transfer(response, currency) + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + # + # { + # "currency": "BTC", + # "from": "spot", + # "to": "margin", + # "amount": "1", + # "currency_pair": "BTC_USDT" + # } + # + return { + 'id': self.safe_string(transfer, 'tx_id'), + 'timestamp': None, + 'datetime': None, + 'currency': self.safe_currency_code(None, currency), + 'amount': None, + 'fromAccount': None, + 'toAccount': None, + 'status': None, + 'info': transfer, + } + + def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://www.gate.com/docs/developers/apiv4/en/#update-position-leverage + https://www.gate.com/docs/developers/apiv4/en/#update-position-leverage-2 + + :param float leverage: the rate of leverage + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setLeverage() requires a symbol argument') + # WARNING: THIS WILL INCREASE LIQUIDATION PRICE FOR OPEN ISOLATED LONG POSITIONS + # AND DECREASE LIQUIDATION PRICE FOR OPEN ISOLATED SHORT POSITIONS + if (leverage < 0) or (leverage > 100): + raise BadRequest(self.id + ' setLeverage() leverage should be between 1 and 100') + self.load_markets() + market = self.market(symbol) + request, query = self.prepare_request(market, None, params) + defaultMarginMode = self.safe_string_2(self.options, 'marginMode', 'defaultMarginMode') + crossLeverageLimit = self.safe_string(query, 'cross_leverage_limit') + marginMode = self.safe_string(query, 'marginMode', defaultMarginMode) + stringifiedMargin = self.number_to_string(leverage) + if crossLeverageLimit is not None: + marginMode = 'cross' + stringifiedMargin = crossLeverageLimit + if marginMode == 'cross' or marginMode == 'cross_margin': + request['cross_leverage_limit'] = stringifiedMargin + request['leverage'] = '0' + else: + request['leverage'] = stringifiedMargin + response = None + if market['swap']: + response = self.privateFuturesPostSettlePositionsContractLeverage(self.extend(request, query)) + elif market['future']: + response = self.privateDeliveryPostSettlePositionsContractLeverage(self.extend(request, query)) + else: + raise NotSupported(self.id + ' setLeverage() not support self market type') + # + # { + # "value": "0", + # "leverage": "5", + # "mode": "single", + # "realised_point": "0", + # "contract": "BTC_USDT", + # "entry_price": "0", + # "mark_price": "62035.86", + # "history_point": "0", + # "realised_pnl": "0", + # "close_order": null, + # "size": 0, + # "cross_leverage_limit": "0", + # "pending_orders": 0, + # "adl_ranking": 6, + # "maintenance_rate": "0.005", + # "unrealised_pnl": "0", + # "user": 2436035, + # "leverage_max": "100", + # "history_pnl": "0", + # "risk_limit": "1000000", + # "margin": "0", + # "last_close_pnl": "0", + # "liq_price": "0" + # } + # + return response + + def parse_position(self, position: dict, market: Market = None): + # + # swap and future + # + # { + # "value": "4.60516", + # "leverage": "0", + # "mode": "single", + # "realised_point": "0", + # "contract": "BTC_USDT", + # "entry_price": "46030.3", + # "mark_price": "46051.6", + # "history_point": "0", + # "realised_pnl": "-0.002301515", + # "close_order": null, + # "size": 1, + # "cross_leverage_limit": "0", + # "pending_orders": 0, + # "adl_ranking": 5, + # "maintenance_rate": "0.004", + # "unrealised_pnl": "0.00213", + # "user": 5691076, + # "leverage_max": "125", + # "history_pnl": "0", + # "risk_limit": "1000000", + # "margin": "8.997698485", + # "last_close_pnl": "0", + # "liq_price": "0", + # "update_time": 1705034246, + # "update_id": 1, + # "initial_margin": "0", + # "maintenance_margin": "0", + # "open_time": 1705034246, + # "trade_max_size": "0" + # } + # + # option + # + # { + # "close_order": null, + # "size": 1, + # "vega": "5.29756", + # "theta": "-98.98917", + # "gamma": "0.00056", + # "delta": "0.68691", + # "contract": "BTC_USDT-20230602-26500-C", + # "entry_price": "529", + # "unrealised_pnl": "-1.0131", + # "user": 5691076, + # "mark_price": "427.69", + # "underlying_price": "26810.2", + # "underlying": "BTC_USDT", + # "realised_pnl": "-0.08042877", + # "mark_iv": "0.4224", + # "pending_orders": 0 + # } + # + # fetchPositionsHistory(swap and future) + # + # { + # "contract": "SLERF_USDT", # Futures contract + # "text": "web", # Text of close order + # "long_price": "0.766306", # When 'side' is 'long,' it indicates the opening average price; when 'side' is 'short,' it indicates the closing average price. + # "pnl": "-23.41702352", # PNL + # "pnl_pnl": "-22.7187", # Position P/L + # "pnl_fee": "-0.06527125", # Transaction Fees + # "pnl_fund": "-0.63305227", # Funding Fees + # "accum_size": "100", + # "time": 1711279263, # Position close time + # "short_price": "0.539119", # When 'side' is 'long,' it indicates the opening average price; when 'side' is 'short,' it indicates the closing average price + # "side": "long", # Position side, long or short + # "max_size": "100", # Max Trade Size + # "first_open_time": 1711037985 # First Open Time + # } + # + contract = self.safe_string(position, 'contract') + market = self.safe_market(contract, market, '_', 'contract') + size = self.safe_string_2(position, 'size', 'accum_size') + side = self.safe_string(position, 'side') + if side is None: + if Precise.string_gt(size, '0'): + side = 'long' + elif Precise.string_lt(size, '0'): + side = 'short' + maintenanceRate = self.safe_string(position, 'maintenance_rate') + notional = self.safe_string(position, 'value') + leverage = self.safe_string(position, 'leverage') + marginMode = None + if leverage is not None: + if leverage == '0': + marginMode = 'cross' + else: + marginMode = 'isolated' + # Initial Position Margin = ( Position Value / Leverage ) + Close Position Fee + # *The default leverage under the full position is the highest leverage in the market. + # *Trading fee is charged Fee Rate(0.075%). + feePaid = self.safe_string(position, 'pnl_fee') + initialMarginString = None + if feePaid is None: + takerFee = '0.00075' + feePaid = Precise.string_mul(takerFee, notional) + initialMarginString = Precise.string_add(Precise.string_div(notional, leverage), feePaid) + timestamp = self.safe_timestamp_2(position, 'open_time', 'first_open_time') + if timestamp == 0: + timestamp = None + return self.safe_position({ + 'info': position, + 'id': None, + 'symbol': self.safe_string(market, 'symbol'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastUpdateTimestamp': self.safe_timestamp_2(position, 'update_time', 'time'), + 'initialMargin': self.parse_number(initialMarginString), + 'initialMarginPercentage': self.parse_number(Precise.string_div(initialMarginString, notional)), + 'maintenanceMargin': self.parse_number(Precise.string_mul(maintenanceRate, notional)), + 'maintenanceMarginPercentage': self.parse_number(maintenanceRate), + 'entryPrice': self.safe_number(position, 'entry_price'), + 'notional': self.parse_number(notional), + 'leverage': self.safe_number(position, 'leverage'), + 'unrealizedPnl': self.safe_number(position, 'unrealised_pnl'), + 'realizedPnl': self.safe_number_2(position, 'realised_pnl', 'pnl'), + 'contracts': self.parse_number(Precise.string_abs(size)), + 'contractSize': self.safe_number(market, 'contractSize'), + 'marginRatio': None, + 'liquidationPrice': self.safe_number(position, 'liq_price'), + 'markPrice': self.safe_number(position, 'mark_price'), + 'lastPrice': None, + 'collateral': self.safe_number(position, 'margin'), + 'marginMode': marginMode, + 'side': side, + 'percentage': None, + 'stopLossPrice': None, + 'takeProfitPrice': None, + }) + + def fetch_position(self, symbol: str, params={}): + """ + fetch data on an open contract position + + https://www.gate.com/docs/developers/apiv4/en/#get-single-position + https://www.gate.com/docs/developers/apiv4/en/#get-single-position-2 + https://www.gate.com/docs/developers/apiv4/en/#get-specified-contract-position + + :param str symbol: unified market symbol of the market the position is held in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `position structure ` + """ + self.load_markets() + market = self.market(symbol) + if not market['contract']: + raise BadRequest(self.id + ' fetchPosition() supports contract markets only') + request: dict = {} + request, params = self.prepare_request(market, market['type'], params) + extendedRequest = self.extend(request, params) + response = None + if market['swap']: + response = self.privateFuturesGetSettlePositionsContract(extendedRequest) + elif market['future']: + response = self.privateDeliveryGetSettlePositionsContract(extendedRequest) + elif market['type'] == 'option': + response = self.privateOptionsGetPositionsContract(extendedRequest) + # + # swap and future + # + # { + # "value": "4.60516", + # "leverage": "0", + # "mode": "single", + # "realised_point": "0", + # "contract": "BTC_USDT", + # "entry_price": "46030.3", + # "mark_price": "46051.6", + # "history_point": "0", + # "realised_pnl": "-0.002301515", + # "close_order": null, + # "size": 1, + # "cross_leverage_limit": "0", + # "pending_orders": 0, + # "adl_ranking": 5, + # "maintenance_rate": "0.004", + # "unrealised_pnl": "0.00213", + # "user": 5691076, + # "leverage_max": "125", + # "history_pnl": "0", + # "risk_limit": "1000000", + # "margin": "8.997698485", + # "last_close_pnl": "0", + # "liq_price": "0", + # "update_time": 1705034246, + # "update_id": 1, + # "initial_margin": "0", + # "maintenance_margin": "0", + # "open_time": 1705034246, + # "trade_max_size": "0" + # } + # + # option + # + # { + # "close_order": null, + # "size": 1, + # "vega": "5.29756", + # "theta": "-98.98917", + # "gamma": "0.00056", + # "delta": "0.68691", + # "contract": "BTC_USDT-20230602-26500-C", + # "entry_price": "529", + # "unrealised_pnl": "-1.0131", + # "user": 5691076, + # "mark_price": "427.69", + # "underlying_price": "26810.2", + # "underlying": "BTC_USDT", + # "realised_pnl": "-0.08042877", + # "mark_iv": "0.4224", + # "pending_orders": 0 + # } + # + return self.parse_position(response, market) + + def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://www.gate.com/docs/developers/apiv4/en/#list-all-positions-of-a-user + https://www.gate.com/docs/developers/apiv4/en/#list-all-positions-of-a-user-2 + https://www.gate.com/docs/developers/apiv4/en/#list-user-s-positions-of-specified-underlying + + :param str[]|None symbols: Not used by gate, but parsed internally by CCXT + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.settle]: 'btc' or 'usdt' - settle currency for perpetual swap and future - default="usdt" for swap and "btc" for future + :param str [params.type]: swap, future or option, if not provided self.options['defaultType'] is used + :returns dict[]: a list of `position structure ` + """ + self.load_markets() + market = None + symbols = self.market_symbols(symbols, None, True, True, True) + if symbols is not None: + symbolsLength = len(symbols) + if symbolsLength > 0: + market = self.market(symbols[0]) + type = None + request: dict = {} + type, params = self.handle_market_type_and_params('fetchPositions', market, params) + if (type is None) or (type == 'spot'): + type = 'swap' # default to swap + if type == 'option': + if symbols is not None: + marketId = market['id'] + optionParts = marketId.split('-') + request['underlying'] = self.safe_string(optionParts, 0) + else: + request, params = self.prepare_request(None, type, params) + response = None + if type == 'swap': + response = self.privateFuturesGetSettlePositions(self.extend(request, params)) + elif type == 'future': + response = self.privateDeliveryGetSettlePositions(self.extend(request, params)) + elif type == 'option': + response = self.privateOptionsGetPositions(self.extend(request, params)) + # + # swap and future + # + # [ + # { + # "value": "4.602828", + # "leverage": "0", + # "mode": "single", + # "realised_point": "0", + # "contract": "BTC_USDT", + # "entry_price": "46030.3", + # "mark_price": "46028.28", + # "history_point": "0", + # "realised_pnl": "-0.002301515", + # "close_order": null, + # "size": 1, + # "cross_leverage_limit": "0", + # "pending_orders": 0, + # "adl_ranking": 5, + # "maintenance_rate": "0.004", + # "unrealised_pnl": "-0.000202", + # "user": 5691076, + # "leverage_max": "125", + # "history_pnl": "0", + # "risk_limit": "1000000", + # "margin": "8.997698485", + # "last_close_pnl": "0", + # "liq_price": "0", + # "update_time": 1705034246, + # "update_id": 1, + # "initial_margin": "0", + # "maintenance_margin": "0", + # "open_time": 1705034246, + # "trade_max_size": "0" + # } + # ] + # + # option + # + # [ + # { + # "close_order": null, + # "size": 0, + # "vega": "0.01907", + # "theta": "-3.04888", + # "gamma": "0.00001", + # "delta": "0.0011", + # "contract": "BTC_USDT-20230601-27500-C", + # "entry_price": "0", + # "unrealised_pnl": "0", + # "user": 5691076, + # "mark_price": "0.07", + # "underlying_price": "26817.27", + # "underlying": "BTC_USDT", + # "realised_pnl": "0", + # "mark_iv": "0.4339", + # "pending_orders": 0 + # } + # ] + # + return self.parse_positions(response, symbols) + + def fetch_leverage_tiers(self, symbols: Strings = None, params={}) -> LeverageTiers: + """ + retrieve information on the maximum leverage, and maintenance margin for trades of varying trade sizes + + https://www.gate.com/docs/developers/apiv4/en/#list-all-futures-contracts + https://www.gate.com/docs/developers/apiv4/en/#list-all-futures-contracts-2 + + :param str[] [symbols]: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `leverage tiers structures `, indexed by market symbols + """ + self.load_markets() + type, query = self.handle_market_type_and_params('fetchLeverageTiers', None, params) + request, requestParams = self.prepare_request(None, type, query) + if type != 'future' and type != 'swap': + raise BadRequest(self.id + ' fetchLeverageTiers only supports swap and future') + response = None + if type == 'swap': + response = self.publicFuturesGetSettleContracts(self.extend(request, requestParams)) + elif type == 'future': + response = self.publicDeliveryGetSettleContracts(self.extend(request, requestParams)) + else: + raise NotSupported(self.id + ' fetchLeverageTiers() not support self market type') + # + # Perpetual swap + # + # [ + # { + # "name": "BTC_USDT", + # "type": "direct", + # "quanto_multiplier": "0.0001", + # "ref_discount_rate": "0", + # "order_price_deviate": "0.5", + # "maintenance_rate": "0.005", + # "mark_type": "index", + # "last_price": "38026", + # "mark_price": "37985.6", + # "index_price": "37954.92", + # "funding_rate_indicative": "0.000219", + # "mark_price_round": "0.01", + # "funding_offset": 0, + # "in_delisting": False, + # "risk_limit_base": "1000000", + # "interest_rate": "0.0003", + # "order_price_round": "0.1", + # "order_size_min": 1, + # "ref_rebate_rate": "0.2", + # "funding_interval": 28800, + # "risk_limit_step": "1000000", + # "leverage_min": "1", + # "leverage_max": "100", + # "risk_limit_max": "8000000", + # "maker_fee_rate": "-0.00025", + # "taker_fee_rate": "0.00075", + # "funding_rate": "0.002053", + # "order_size_max": 1000000, + # "funding_next_apply": 1610035200, + # "short_users": 977, + # "config_change_time": 1609899548, + # "trade_size": 28530850594, + # "position_size": 5223816, + # "long_users": 455, + # "funding_impact_value": "60000", + # "orders_limit": 50, + # "trade_id": 10851092, + # "orderbook_id": 2129638396 + # } + # ] + # + # Delivery Futures + # + # [ + # { + # "name": "BTC_USDT_20200814", + # "underlying": "BTC_USDT", + # "cycle": "WEEKLY", + # "type": "direct", + # "quanto_multiplier": "0.0001", + # "mark_type": "index", + # "last_price": "9017", + # "mark_price": "9019", + # "index_price": "9005.3", + # "basis_rate": "0.185095", + # "basis_value": "13.7", + # "basis_impact_value": "100000", + # "settle_price": "0", + # "settle_price_interval": 60, + # "settle_price_duration": 1800, + # "settle_fee_rate": "0.0015", + # "expire_time": 1593763200, + # "order_price_round": "0.1", + # "mark_price_round": "0.1", + # "leverage_min": "1", + # "leverage_max": "100", + # "maintenance_rate": "1000000", + # "risk_limit_base": "140.726652109199", + # "risk_limit_step": "1000000", + # "risk_limit_max": "8000000", + # "maker_fee_rate": "-0.00025", + # "taker_fee_rate": "0.00075", + # "ref_discount_rate": "0", + # "ref_rebate_rate": "0.2", + # "order_price_deviate": "0.5", + # "order_size_min": 1, + # "order_size_max": 1000000, + # "orders_limit": 50, + # "orderbook_id": 63, + # "trade_id": 26, + # "trade_size": 435, + # "position_size": 130, + # "config_change_time": 1593158867, + # "in_delisting": False + # } + # ] + # + return self.parse_leverage_tiers(response, symbols, 'name') + + def fetch_market_leverage_tiers(self, symbol: str, params={}) -> List[LeverageTier]: + """ + retrieve information on the maximum leverage, and maintenance margin for trades of varying trade sizes for a single market + + https://www.gate.com/docs/developers/apiv4/en/#list-risk-limit-tiers + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `leverage tiers structure ` + """ + self.load_markets() + market = self.market(symbol) + type, query = self.handle_market_type_and_params('fetchMarketLeverageTiers', market, params) + request, requestParams = self.prepare_request(market, type, query) + if type != 'future' and type != 'swap': + raise BadRequest(self.id + ' fetchMarketLeverageTiers only supports swap and future') + response = self.publicFuturesGetSettleRiskLimitTiers(self.extend(request, requestParams)) + # + # [ + # { + # "maintenance_rate": "0.004", + # "tier": 1, + # "initial_rate": "0.008", + # "leverage_max": "125", + # "risk_limit": "1000000" + # } + # ] + # + return self.parse_market_leverage_tiers(response, market) + + def parse_emulated_leverage_tiers(self, info, market=None) -> List[LeverageTier]: + marketId = self.safe_string(info, 'name') + maintenanceMarginUnit = self.safe_string(info, 'maintenance_rate') # '0.005', + leverageMax = self.safe_string(info, 'leverage_max') # '100', + riskLimitStep = self.safe_string(info, 'risk_limit_step') # '1000000', + riskLimitMax = self.safe_string(info, 'risk_limit_max') # '16000000', + initialMarginUnit = Precise.string_div('1', leverageMax) + maintenanceMarginRate = maintenanceMarginUnit + initialMarginRatio = initialMarginUnit + floor = '0' + tiers = [] + while(Precise.string_lt(floor, riskLimitMax)): + cap = Precise.string_add(floor, riskLimitStep) + tiers.append({ + 'tier': self.parse_number(Precise.string_div(cap, riskLimitStep)), + 'symbol': self.safe_symbol(marketId, market, None, 'contract'), + 'currency': self.safe_string(market, 'settle'), + 'minNotional': self.parse_number(floor), + 'maxNotional': self.parse_number(cap), + 'maintenanceMarginRate': self.parse_number(maintenanceMarginRate), + 'maxLeverage': self.parse_number(Precise.string_div('1', initialMarginRatio)), + 'info': info, + }) + maintenanceMarginRate = Precise.string_add(maintenanceMarginRate, maintenanceMarginUnit) + initialMarginRatio = Precise.string_add(initialMarginRatio, initialMarginUnit) + floor = cap + return tiers + + def parse_market_leverage_tiers(self, info, market: Market = None) -> List[LeverageTier]: + # + # [ + # { + # "maintenance_rate": "0.004", + # "tier": 1, + # "initial_rate": "0.008", + # "leverage_max": "125", + # "risk_limit": "1000000" + # } + # ] + # + if not isinstance(info, list): + return self.parse_emulated_leverage_tiers(info, market) + minNotional = 0 + tiers = [] + for i in range(0, len(info)): + item = info[i] + maxNotional = self.safe_number(item, 'risk_limit') + tiers.append({ + 'tier': self.sum(i, 1), + 'symbol': market['symbol'], + 'currency': market['base'], + 'minNotional': minNotional, + 'maxNotional': maxNotional, + 'maintenanceMarginRate': self.safe_number(item, 'maintenance_rate'), + 'maxLeverage': self.safe_number(item, 'leverage_max'), + 'info': item, + }) + minNotional = maxNotional + return tiers + + def repay_isolated_margin(self, symbol: str, code: str, amount, params={}): + """ + repay borrowed margin and interest + + https://www.gate.com/docs/apiv4/en/#repay-a-loan + + :param str symbol: unified market symbol + :param str code: unified currency code of the currency to repay + :param float amount: the amount to repay + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.mode]: 'all' or 'partial' payment mode, extra parameter required for isolated margin + :param str [params.id]: '34267567' loan id, extra parameter required for isolated margin + :returns dict: a `margin loan structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'].upper(), # todo: currencies have network-junctions + 'amount': self.currency_to_precision(code, amount), + } + market = self.market(symbol) + request['currency_pair'] = market['id'] + request['type'] = 'repay' + response = self.privateMarginPostUniLoans(self.extend(request, params)) + # + # empty response + # + return self.parse_margin_loan(response, currency) + + def repay_cross_margin(self, code: str, amount, params={}): + """ + repay cross margin borrowed margin and interest + + https://www.gate.com/docs/developers/apiv4/en/#cross-margin-repayments + https://www.gate.com/docs/developers/apiv4/en/#borrow-or-repay + + :param str code: unified currency code of the currency to repay + :param float amount: the amount to repay + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.mode]: 'all' or 'partial' payment mode, extra parameter required for isolated margin + :param str [params.id]: '34267567' loan id, extra parameter required for isolated margin + :param boolean [params.unifiedAccount]: set to True for repaying in the unified account + :returns dict: a `margin loan structure ` + """ + self.load_markets() + self.load_unified_status() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'].upper(), # todo: currencies have network-junctions + 'amount': self.currency_to_precision(code, amount), + } + isUnifiedAccount = False + isUnifiedAccount, params = self.handle_option_and_params(params, 'repayCrossMargin', 'unifiedAccount') + response = None + if isUnifiedAccount: + request['type'] = 'repay' + response = self.privateUnifiedPostLoans(self.extend(request, params)) + else: + response = self.privateMarginPostCrossRepayments(self.extend(request, params)) + response = self.safe_dict(response, 0) + # + # [ + # { + # "id": "17", + # "create_time": 1620381696159, + # "update_time": 1620381696159, + # "currency": "EOS", + # "amount": "110.553635", + # "text": "web", + # "status": 2, + # "repaid": "110.506649705159", + # "repaid_interest": "0.046985294841", + # "unpaid_interest": "0.0000074393366667" + # } + # ] + # + return self.parse_margin_loan(response, currency) + + def borrow_isolated_margin(self, symbol: str, code: str, amount: float, params={}): + """ + create a loan to borrow margin + + https://www.gate.com/docs/developers/apiv4/en/#marginuni + + :param str symbol: unified market symbol, required for isolated margin + :param str code: unified currency code of the currency to borrow + :param float amount: the amount to borrow + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.rate]: '0.0002' or '0.002' extra parameter required for isolated margin + :returns dict: a `margin loan structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'].upper(), # todo: currencies have network-junctions + 'amount': self.currency_to_precision(code, amount), + } + response = None + market = self.market(symbol) + request['currency_pair'] = market['id'] + request['type'] = 'borrow' + response = self.privateMarginPostUniLoans(self.extend(request, params)) + # + # { + # "id": "34267567", + # "create_time": "1656394778", + # "expire_time": "1657258778", + # "status": "loaned", + # "side": "borrow", + # "currency": "USDT", + # "rate": "0.0002", + # "amount": "100", + # "days": 10, + # "auto_renew": False, + # "currency_pair": "LTC_USDT", + # "left": "0", + # "repaid": "0", + # "paid_interest": "0", + # "unpaid_interest": "0.003333333333" + # } + # + return self.parse_margin_loan(response, currency) + + def borrow_cross_margin(self, code: str, amount: float, params={}): + """ + create a loan to borrow margin + + https://www.gate.com/docs/apiv4/en/#create-a-cross-margin-borrow-loan + https://www.gate.com/docs/developers/apiv4/en/#borrow-or-repay + + :param str code: unified currency code of the currency to borrow + :param float amount: the amount to borrow + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.rate]: '0.0002' or '0.002' extra parameter required for isolated margin + :param boolean [params.unifiedAccount]: set to True for borrowing in the unified account + :returns dict: a `margin loan structure ` + """ + self.load_markets() + self.load_unified_status() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'].upper(), # todo: currencies have network-junctions + 'amount': self.currency_to_precision(code, amount), + } + isUnifiedAccount = False + isUnifiedAccount, params = self.handle_option_and_params(params, 'borrowCrossMargin', 'unifiedAccount') + response = None + if isUnifiedAccount: + request['type'] = 'borrow' + response = self.privateUnifiedPostLoans(self.extend(request, params)) + else: + response = self.privateMarginPostCrossLoans(self.extend(request, params)) + # + # { + # "id": "17", + # "create_time": 1620381696159, + # "update_time": 1620381696159, + # "currency": "EOS", + # "amount": "110.553635", + # "text": "web", + # "status": 2, + # "repaid": "110.506649705159", + # "repaid_interest": "0.046985294841", + # "unpaid_interest": "0.0000074393366667" + # } + # + return self.parse_margin_loan(response, currency) + + def parse_margin_loan(self, info, currency: Currency = None): + # + # Cross + # + # { + # "id": "17", + # "create_time": 1620381696159, + # "update_time": 1620381696159, + # "currency": "EOS", + # "amount": "110.553635", + # "text": "web", + # "status": 2, + # "repaid": "110.506649705159", + # "repaid_interest": "0.046985294841", + # "unpaid_interest": "0.0000074393366667" + # } + # + # Isolated + # + # { + # "id": "34267567", + # "create_time": "1656394778", + # "expire_time": "1657258778", + # "status": "loaned", + # "side": "borrow", + # "currency": "USDT", + # "rate": "0.0002", + # "amount": "100", + # "days": 10, + # "auto_renew": False, + # "currency_pair": "LTC_USDT", + # "left": "0", + # "repaid": "0", + # "paid_interest": "0", + # "unpaid_interest": "0.003333333333" + # } + # + marginMode = self.safe_string_2(self.options, 'defaultMarginMode', 'marginMode', 'cross') + timestamp = self.safe_integer(info, 'create_time') + if marginMode == 'isolated': + timestamp = self.safe_timestamp(info, 'create_time') + currencyId = self.safe_string(info, 'currency') + marketId = self.safe_string(info, 'currency_pair') + return { + 'id': self.safe_integer(info, 'id'), + 'currency': self.safe_currency_code(currencyId, currency), + 'amount': self.safe_number(info, 'amount'), + 'symbol': self.safe_symbol(marketId, None, '_', 'margin'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'info': info, + } + + def fetch_borrow_interest(self, code: Str = None, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[BorrowInterest]: + """ + fetch the interest owed by the user for borrowing currency for margin trading + + https://www.gate.com/docs/developers/apiv4/en/#list-interest-records + https://www.gate.com/docs/developers/apiv4/en/#interest-records-for-the-cross-margin-account + https://www.gate.com/docs/developers/apiv4/en/#list-interest-records-2 + + :param str [code]: unified currency code + :param str [symbol]: unified market symbol when fetching interest in isolated markets + :param int [since]: the earliest time in ms to fetch borrow interest for + :param int [limit]: the maximum number of structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.unifiedAccount]: set to True for fetching borrow interest in the unified account + :returns dict[]: a list of `borrow interest structures ` + """ + self.load_markets() + self.load_unified_status() + isUnifiedAccount = False + isUnifiedAccount, params = self.handle_option_and_params(params, 'fetchBorrowInterest', 'unifiedAccount') + request: dict = {} + request, params = self.handle_until_option('to', request, params) + currency = None + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + market = None + if symbol is not None: + market = self.market(symbol) + if since is not None: + request['from'] = since + if limit is not None: + request['limit'] = limit + response = None + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchBorrowInterest', params, 'cross') + if isUnifiedAccount: + response = self.privateUnifiedGetInterestRecords(self.extend(request, params)) + elif marginMode == 'isolated': + if market is not None: + request['currency_pair'] = market['id'] + response = self.privateMarginGetUniInterestRecords(self.extend(request, params)) + elif marginMode == 'cross': + response = self.privateMarginGetCrossInterestRecords(self.extend(request, params)) + interest = self.parse_borrow_interests(response, market) + return self.filter_by_currency_since_limit(interest, code, since, limit) + + def parse_borrow_interest(self, info: dict, market: Market = None) -> BorrowInterest: + marketId = self.safe_string(info, 'currency_pair') + market = self.safe_market(marketId, market) + marginMode = 'isolated' if (marketId is not None) else 'cross' + timestamp = self.safe_integer(info, 'create_time') + return { + 'info': info, + 'symbol': self.safe_string(market, 'symbol'), + 'currency': self.safe_currency_code(self.safe_string(info, 'currency')), + 'interest': self.safe_number(info, 'interest'), + 'interestRate': self.safe_number(info, 'actual_rate'), + 'amountBorrowed': None, + 'marginMode': marginMode, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + + def nonce(self): + return self.milliseconds() - self.options['timeDifference'] + + def sign(self, path, api=[], method='GET', params={}, headers=None, body=None): + authentication = api[0] # public, private + type = api[1] # spot, margin, future, delivery + query = self.omit(params, self.extract_params(path)) + containsSettle = path.find('settle') > -1 + if containsSettle and path.endswith('batch_cancel_orders'): # weird check to prevent $settle in php and converting {settle} to array(settle) + # special case where we need to extract the settle from the path + # but the body is an array of strings + settle = self.safe_dict(params, 0) + path = self.implode_params(path, settle) + # remove the first element from params + newParams = [] + anyParams = params + for i in range(1, len(anyParams)): + newParams.append(params[i]) + params = newParams + query = newParams + elif isinstance(params, list): + # endpoints like createOrders use an array instead of an object + # so we infer the settle from one of the elements + # they have to be all the same so relying on the first one is fine + first = self.safe_value(params, 0, {}) + path = self.implode_params(path, first) + else: + path = self.implode_params(path, params) + endPart = '' if (path == '') else ('/' + path) + entirePath = '/' + type + endPart + if (type == 'subAccounts') or (type == 'withdrawals'): + entirePath = endPart + url = self.urls['api'][authentication][type] + if url is None: + raise NotSupported(self.id + ' does not have a testnet for the ' + type + ' market type.') + url += entirePath + if authentication == 'public': + if query: + url += '?' + self.urlencode(query) + else: + self.check_required_credentials() + queryString = '' + requiresURLEncoding = False + if ((type == 'futures') or (type == 'delivery')) and method == 'POST': + pathParts = path.split('/') + secondPart = self.safe_string(pathParts, 1, '') + requiresURLEncoding = (secondPart.find('dual') >= 0) or (secondPart.find('positions') >= 0) + if (method == 'GET') or (method == 'DELETE') or requiresURLEncoding or (method == 'PATCH'): + if query: + queryString = self.urlencode(query) + # https://github.com/ccxt/ccxt/issues/25570 + if queryString.find('currencies=') >= 0 and queryString.find('%2C') >= 0: + queryString = queryString.replace('%2C', ',') + url += '?' + queryString + if method == 'PATCH': + body = self.json(query) + else: + urlQueryParams = self.safe_value(query, 'query', {}) + if urlQueryParams: + queryString = self.urlencode(urlQueryParams) + url += '?' + queryString + query = self.omit(query, 'query') + body = self.json(query) + bodyPayload = '' if (body is None) else body + bodySignature = self.hash(self.encode(bodyPayload), 'sha512') + nonce = self.nonce() + timestamp = self.parse_to_int(nonce / 1000) + timestampString = str(timestamp) + signaturePath = '/api/' + self.version + entirePath + payloadArray = [method.upper(), signaturePath, queryString, bodySignature, timestampString] + # eslint-disable-next-line quotes + payload = "\n".join(payloadArray) + signature = self.hmac(self.encode(payload), self.encode(self.secret), hashlib.sha512) + headers = { + 'KEY': self.apiKey, + 'Timestamp': timestampString, + 'SIGN': signature, + 'Content-Type': 'application/json', + } + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def modify_margin_helper(self, symbol: str, amount, params={}): + self.load_markets() + market = self.market(symbol) + request, query = self.prepare_request(market, None, params) + request['change'] = self.number_to_string(amount) + response = None + if market['swap']: + response = self.privateFuturesPostSettlePositionsContractMargin(self.extend(request, query)) + elif market['future']: + response = self.privateDeliveryPostSettlePositionsContractMargin(self.extend(request, query)) + else: + raise NotSupported(self.id + ' modifyMarginHelper() not support self market type') + return self.parse_margin_modification(response, market) + + def parse_margin_modification(self, data: dict, market: Market = None) -> MarginModification: + # + # { + # "value": "11.9257", + # "leverage": "5", + # "mode": "single", + # "realised_point": "0", + # "contract": "ETH_USDT", + # "entry_price": "1203.45", + # "mark_price": "1192.57", + # "history_point": "0", + # "realised_pnl": "-0.00577656", + # "close_order": null, + # "size": "1", + # "cross_leverage_limit": "0", + # "pending_orders": "0", + # "adl_ranking": "5", + # "maintenance_rate": "0.005", + # "unrealised_pnl": "-0.1088", + # "user": "1486602", + # "leverage_max": "100", + # "history_pnl": "0", + # "risk_limit": "1000000", + # "margin": "5.415925875", + # "last_close_pnl": "0", + # "liq_price": "665.69" + # } + # + contract = self.safe_string(data, 'contract') + market = self.safe_market(contract, market, '_', 'contract') + total = self.safe_number(data, 'margin') + return { + 'info': data, + 'symbol': market['symbol'], + 'type': None, + 'marginMode': 'isolated', + 'amount': None, + 'total': total, + 'code': self.safe_value(market, 'quote'), + 'status': 'ok', + 'timestamp': None, + 'datetime': None, + } + + def reduce_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + remove margin from a position + + https://www.gate.com/docs/developers/apiv4/en/#update-position-margin + https://www.gate.com/docs/developers/apiv4/en/#update-position-margin-2 + + :param str symbol: unified market symbol + :param float amount: the amount of margin to remove + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin structure ` + """ + return self.modify_margin_helper(symbol, -amount, params) + + def add_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + add margin + + https://www.gate.com/docs/developers/apiv4/en/#update-position-margin + https://www.gate.com/docs/developers/apiv4/en/#update-position-margin-2 + + :param str symbol: unified market symbol + :param float amount: amount of margin to add + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin structure ` + """ + return self.modify_margin_helper(symbol, amount, params) + + def fetch_open_interest_history(self, symbol: str, timeframe='5m', since: Int = None, limit: Int = None, params={}): + """ + Retrieves the open interest of a currency + + https://www.gate.com/docs/developers/apiv4/en/#futures-stats + + :param str symbol: Unified CCXT market symbol + :param str timeframe: "5m", "15m", "30m", "1h", "4h", "1d" + :param int [since]: the time(ms) of the earliest record to retrieve unix timestamp + :param int [limit]: default 30 + :param dict [params]: exchange specific parameters + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict} an open interest structure{@link https://docs.ccxt.com/#/?id=open-interest-structure: + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOpenInterestHistory', 'paginate', False) + if paginate: + return self.fetch_paginated_call_deterministic('fetchOpenInterestHistory', symbol, since, limit, timeframe, params, 100) + market = self.market(symbol) + if not market['swap']: + raise BadRequest(self.id + ' fetchOpenInterest() supports swap markets only') + request: dict = { + 'contract': market['id'], + 'settle': market['settleId'], + 'interval': self.safe_string(self.timeframes, timeframe, timeframe), + } + if limit is not None: + request['limit'] = limit + if since is not None: + request['from'] = since + response = self.publicFuturesGetSettleContractStats(self.extend(request, params)) + # + # [ + # { + # "long_liq_size": "0", + # "short_liq_size": "0", + # "short_liq_usd": "0", + # "lsr_account": "3.2808988764045", + # "mark_price": "0.34619", + # "top_lsr_size": "0", + # "time": "1674057000", + # "short_liq_amount": "0", + # "long_liq_amount": "0", + # "open_interest_usd": "9872386.7775", + # "top_lsr_account": "0", + # "open_interest": "2851725", + # "long_liq_usd": "0", + # "lsr_taker": "9.3765153315902" + # }, + # ... + # ] + # + return self.parse_open_interests_history(response, market, since, limit) + + def parse_open_interest(self, interest, market: Market = None): + # + # { + # "long_liq_size": "0", + # "short_liq_size": "0", + # "short_liq_usd": "0", + # "lsr_account": "3.2808988764045", + # "mark_price": "0.34619", + # "top_lsr_size": "0", + # "time": "1674057000", + # "short_liq_amount": "0", + # "long_liq_amount": "0", + # "open_interest_usd": "9872386.7775", + # "top_lsr_account": "0", + # "open_interest": "2851725", + # "long_liq_usd": "0", + # "lsr_taker": "9.3765153315902" + # } + # + timestamp = self.safe_timestamp(interest, 'time') + return { + 'symbol': self.safe_string(market, 'symbol'), + 'openInterestAmount': self.safe_number(interest, 'open_interest'), + 'openInterestValue': self.safe_number(interest, 'open_interest_usd'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'info': interest, + } + + def fetch_settlement_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical settlement records + + https://www.gate.com/docs/developers/apiv4/en/#list-settlement-history-2 + + :param str symbol: unified market symbol of the settlement history, required on gate + :param int [since]: timestamp in ms + :param int [limit]: number of records + :param dict [params]: exchange specific params + :returns dict[]: a list of `settlement history objects ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchSettlementHistory() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + type = None + type, params = self.handle_market_type_and_params('fetchSettlementHistory', market, params) + if type != 'option': + raise NotSupported(self.id + ' fetchSettlementHistory() supports option markets only') + marketId = market['id'] + optionParts = marketId.split('-') + request: dict = { + 'underlying': self.safe_string(optionParts, 0), + } + if since is not None: + request['from'] = since + if limit is not None: + request['limit'] = limit + response = self.publicOptionsGetSettlements(self.extend(request, params)) + # + # [ + # { + # "time": 1685952000, + # "profit": "18.266806892718", + # "settle_price": "26826.68068927182", + # "fee": "0.040240021034", + # "contract": "BTC_USDT-20230605-25000-C", + # "strike_price": "25000" + # } + # ] + # + settlements = self.parse_settlements(response, market) + sorted = self.sort_by(settlements, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, symbol, since, limit) + + def fetch_my_settlement_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical settlement records of the user + + https://www.gate.com/docs/developers/apiv4/en/#list-my-options-settlements + + :param str symbol: unified market symbol of the settlement history + :param int [since]: timestamp in ms + :param int [limit]: number of records + :param dict [params]: exchange specific params + :returns dict[]: a list of [settlement history objects] + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMySettlementHistory() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + type = None + type, params = self.handle_market_type_and_params('fetchMySettlementHistory', market, params) + if type != 'option': + raise NotSupported(self.id + ' fetchMySettlementHistory() supports option markets only') + marketId = market['id'] + optionParts = marketId.split('-') + request: dict = { + 'underlying': self.safe_string(optionParts, 0), + 'contract': marketId, + } + if since is not None: + request['from'] = since + if limit is not None: + request['limit'] = limit + response = self.privateOptionsGetMySettlements(self.extend(request, params)) + # + # [ + # { + # "size": -1, + # "settle_profit": "0", + # "contract": "BTC_USDT-20220624-26000-C", + # "strike_price": "26000", + # "time": 1656057600, + # "settle_price": "20917.461281337048", + # "underlying": "BTC_USDT", + # "realised_pnl": "-0.00116042", + # "fee": "0" + # } + # ] + # + result = self.safe_value(response, 'result', {}) + data = self.safe_value(result, 'list', []) + settlements = self.parse_settlements(data, market) + sorted = self.sort_by(settlements, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, market['symbol'], since, limit) + + def parse_settlement(self, settlement, market): + # + # fetchSettlementHistory + # + # { + # "time": 1685952000, + # "profit": "18.266806892718", + # "settle_price": "26826.68068927182", + # "fee": "0.040240021034", + # "contract": "BTC_USDT-20230605-25000-C", + # "strike_price": "25000" + # } + # + # fetchMySettlementHistory + # + # { + # "size": -1, + # "settle_profit": "0", + # "contract": "BTC_USDT-20220624-26000-C", + # "strike_price": "26000", + # "time": 1656057600, + # "settle_price": "20917.461281337048", + # "underlying": "BTC_USDT", + # "realised_pnl": "-0.00116042", + # "fee": "0" + # } + # + timestamp = self.safe_timestamp(settlement, 'time') + marketId = self.safe_string(settlement, 'contract') + return { + 'info': settlement, + 'symbol': self.safe_symbol(marketId, market), + 'price': self.safe_number(settlement, 'settle_price'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + + def parse_settlements(self, settlements, market): + # + # fetchSettlementHistory + # + # [ + # { + # "time": 1685952000, + # "profit": "18.266806892718", + # "settle_price": "26826.68068927182", + # "fee": "0.040240021034", + # "contract": "BTC_USDT-20230605-25000-C", + # "strike_price": "25000" + # } + # ] + # + # fetchMySettlementHistory + # + # [ + # { + # "size": -1, + # "settle_profit": "0", + # "contract": "BTC_USDT-20220624-26000-C", + # "strike_price": "26000", + # "time": 1656057600, + # "settle_price": "20917.461281337048", + # "underlying": "BTC_USDT", + # "realised_pnl": "-0.00116042", + # "fee": "0" + # } + # ] + # + result = [] + for i in range(0, len(settlements)): + result.append(self.parse_settlement(settlements[i], market)) + return result + + def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + + https://www.gate.com/docs/developers/apiv4/en/#query-account-book + https://www.gate.com/docs/developers/apiv4/en/#list-margin-account-balance-change-history + https://www.gate.com/docs/developers/apiv4/en/#query-account-book-2 + https://www.gate.com/docs/developers/apiv4/en/#query-account-book-3 + https://www.gate.com/docs/developers/apiv4/en/#list-account-changing-history + + :param str [code]: unified currency code + :param int [since]: timestamp in ms of the earliest ledger entry + :param int [limit]: max number of ledger entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: end time in ms + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict: a `ledger structure ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchLedger', 'paginate') + if paginate: + return self.fetch_paginated_call_dynamic('fetchLedger', code, since, limit, params) + type = None + currency = None + response = None + request: dict = {} + type, params = self.handle_market_type_and_params('fetchLedger', None, params) + if (type == 'spot') or (type == 'margin'): + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] # todo: currencies have network-junctions + if (type == 'swap') or (type == 'future'): + defaultSettle = 'usdt' if (type == 'swap') else 'btc' + settle = self.safe_string_lower(params, 'settle', defaultSettle) + params = self.omit(params, 'settle') + request['settle'] = settle + if since is not None: + request['from'] = since + if limit is not None: + request['limit'] = limit + request, params = self.handle_until_option('to', request, params) + if type == 'spot': + response = self.privateSpotGetAccountBook(self.extend(request, params)) + elif type == 'margin': + response = self.privateMarginGetAccountBook(self.extend(request, params)) + elif type == 'swap': + response = self.privateFuturesGetSettleAccountBook(self.extend(request, params)) + elif type == 'future': + response = self.privateDeliveryGetSettleAccountBook(self.extend(request, params)) + elif type == 'option': + response = self.privateOptionsGetAccountBook(self.extend(request, params)) + # + # spot + # + # [ + # { + # "id": "123456", + # "time": 1547633726123, + # "currency": "BTC", + # "change": "1.03", + # "balance": "4.59316525194", + # "type": "margin_in" + # } + # ] + # + # margin + # + # [ + # { + # "id": "123456", + # "time": "1547633726", + # "time_ms": 1547633726123, + # "currency": "BTC", + # "currency_pair": "BTC_USDT", + # "change": "1.03", + # "balance": "4.59316525194" + # } + # ] + # + # swap and future + # + # [ + # { + # "time": 1682294400.123456, + # "change": "0.000010152188", + # "balance": "4.59316525194", + # "text": "ETH_USD:6086261", + # "type": "fee" + # } + # ] + # + # option + # + # [ + # { + # "time": 1685594770, + # "change": "3.33", + # "balance": "29.87911771", + # "text": "BTC_USDT-20230602-26500-C:2611026125", + # "type": "prem" + # } + # ] + # + return self.parse_ledger(response, currency, since, limit) + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + # + # spot + # + # { + # "id": "123456", + # "time": 1547633726123, + # "currency": "BTC", + # "change": "1.03", + # "balance": "4.59316525194", + # "type": "margin_in" + # } + # + # margin + # + # { + # "id": "123456", + # "time": "1547633726", + # "time_ms": 1547633726123, + # "currency": "BTC", + # "currency_pair": "BTC_USDT", + # "change": "1.03", + # "balance": "4.59316525194" + # } + # + # swap and future + # + # { + # "time": 1682294400.123456, + # "change": "0.000010152188", + # "balance": "4.59316525194", + # "text": "ETH_USD:6086261", + # "type": "fee" + # } + # + # option + # + # { + # "time": 1685594770, + # "change": "3.33", + # "balance": "29.87911771", + # "text": "BTC_USDT-20230602-26500-C:2611026125", + # "type": "prem" + # } + # + direction = None + amount = self.safe_string(item, 'change') + if Precise.string_lt(amount, '0'): + direction = 'out' + amount = Precise.string_abs(amount) + else: + direction = 'in' + currencyId = self.safe_string(item, 'currency') + currency = self.safe_currency(currencyId, currency) + type = self.safe_string(item, 'type') + rawTimestamp = self.safe_string(item, 'time') + timestamp = None + if len(rawTimestamp) > 10: + timestamp = int(rawTimestamp) + else: + timestamp = int(rawTimestamp) * 1000 + balanceString = self.safe_string(item, 'balance') + changeString = self.safe_string(item, 'change') + before = self.parse_number(Precise.string_sub(balanceString, changeString)) + return self.safe_ledger_entry({ + 'info': item, + 'id': self.safe_string(item, 'id'), + 'direction': direction, + 'account': None, + 'referenceAccount': None, + 'referenceId': None, + 'type': self.parse_ledger_entry_type(type), + 'currency': self.safe_currency_code(currencyId, currency), + 'amount': self.parse_number(amount), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'before': before, + 'after': self.safe_number(item, 'balance'), + 'status': None, + 'fee': None, + }, currency) + + def parse_ledger_entry_type(self, type): + ledgerType: dict = { + 'deposit': 'deposit', + 'withdraw': 'withdrawal', + 'sub_account_transfer': 'transfer', + 'margin_in': 'transfer', + 'margin_out': 'transfer', + 'margin_funding_in': 'transfer', + 'margin_funding_out': 'transfer', + 'cross_margin_in': 'transfer', + 'cross_margin_out': 'transfer', + 'copy_trading_in': 'transfer', + 'copy_trading_out': 'transfer', + 'quant_in': 'transfer', + 'quant_out': 'transfer', + 'futures_in': 'transfer', + 'futures_out': 'transfer', + 'delivery_in': 'transfer', + 'delivery_out': 'transfer', + 'new_order': 'trade', + 'order_fill': 'trade', + 'referral_fee': 'rebate', + 'order_fee': 'fee', + 'interest': 'interest', + 'lend': 'loan', + 'redeem': 'loan', + 'profit': 'interest', + 'flash_swap_buy': 'trade', + 'flash_swap_sell': 'trade', + 'unknown': 'unknown', + 'set': 'settlement', + 'prem': 'trade', + 'point_refr': 'rebate', + 'point_fee': 'fee', + 'point_dnw': 'deposit/withdraw', + 'fund': 'fee', + 'refr': 'rebate', + 'fee': 'fee', + 'pnl': 'trade', + 'dnw': 'deposit/withdraw', + } + return self.safe_string(ledgerType, type, type) + + def set_position_mode(self, hedged: bool, symbol: Str = None, params={}): + """ + set dual/hedged mode to True or False for a swap market, make sure all positions are closed and no orders are open before setting dual mode + + https://www.gate.com/docs/developers/apiv4/en/#enable-or-disable-dual-mode + + :param bool hedged: set to True to enable dual mode + :param str|None symbol: if passed, dual mode is set for all markets with the same settle currency + :param dict params: extra parameters specific to the exchange API endpoint + :param str params['settle']: settle currency + :returns dict: response from the exchange + """ + market = self.market(symbol) if (symbol is not None) else None + request, query = self.prepare_request(market, 'swap', params) + request['dual_mode'] = hedged + return self.privateFuturesPostSettleDualMode(self.extend(request, query)) + + def fetch_underlying_assets(self, params={}): + """ + fetches the market ids of underlying assets for a specific contract market type + + https://www.gate.com/docs/developers/apiv4/en/#list-all-underlyings + + :param dict [params]: exchange specific params + :param str [params.type]: the contract market type, 'option', 'swap' or 'future', the default is 'option' + :returns dict[]: a list of `underlying assets ` + """ + self.load_markets() + marketType = None + marketType, params = self.handle_market_type_and_params('fetchUnderlyingAssets', None, params) + if (marketType is None) or (marketType == 'spot'): + marketType = 'option' + if marketType != 'option': + raise NotSupported(self.id + ' fetchUnderlyingAssets() supports option markets only') + response = self.publicOptionsGetUnderlyings(params) + # + # [ + # { + # "index_time": "1646915796", + # "name": "BTC_USDT", + # "index_price": "39142.73" + # } + # ] + # + underlyings = [] + for i in range(0, len(response)): + underlying = response[i] + name = self.safe_string(underlying, 'name') + if name is not None: + underlyings.append(name) + return underlyings + + def fetch_liquidations(self, symbol: str, since: Int = None, limit: Int = None, params={}): + """ + retrieves the public liquidations of a trading pair + + https://www.gate.com/docs/developers/apiv4/en/#retrieve-liquidation-history + + :param str symbol: unified CCXT market symbol + :param int [since]: the earliest time in ms to fetch liquidations for + :param int [limit]: the maximum number of liquidation structures to retrieve + :param dict [params]: exchange specific parameters for the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest liquidation + :returns dict: an array of `liquidation structures ` + """ + self.load_markets() + market = self.market(symbol) + if not market['swap']: + raise NotSupported(self.id + ' fetchLiquidations() supports swap markets only') + request: dict = { + 'settle': market['settleId'], + 'contract': market['id'], + } + if since is not None: + request['from'] = since + if limit is not None: + request['limit'] = limit + request, params = self.handle_until_option('to', request, params) + response = self.publicFuturesGetSettleLiqOrders(self.extend(request, params)) + # + # [ + # { + # "contract": "BTC_USDT", + # "left": 0, + # "size": -165, + # "fill_price": "28070", + # "order_price": "28225", + # "time": 1696736132 + # }, + # ] + # + return self.parse_liquidations(response, market, since, limit) + + def fetch_my_liquidations(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + retrieves the users liquidated positions + + https://www.gate.com/docs/developers/apiv4/en/#list-liquidation-history + https://www.gate.com/docs/developers/apiv4/en/#list-liquidation-history-2 + https://www.gate.com/docs/developers/apiv4/en/#list-user-s-liquidation-history-of-specified-underlying + + :param str symbol: unified CCXT market symbol + :param int [since]: the earliest time in ms to fetch liquidations for + :param int [limit]: the maximum number of liquidation structures to retrieve + :param dict [params]: exchange specific parameters for the exchange API endpoint + :returns dict: an array of `liquidation structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMyLiquidations() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'contract': market['id'], + } + response = None + if (market['swap']) or (market['future']): + if limit is not None: + request['limit'] = limit + request['settle'] = market['settleId'] + elif market['option']: + marketId = market['id'] + optionParts = marketId.split('-') + request['underlying'] = self.safe_string(optionParts, 0) + if market['swap']: + response = self.privateFuturesGetSettleLiquidates(self.extend(request, params)) + elif market['future']: + response = self.privateDeliveryGetSettleLiquidates(self.extend(request, params)) + elif market['option']: + response = self.privateOptionsGetPositionClose(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchMyLiquidations() does not support ' + market['type'] + ' orders') + # + # swap and future + # + # [ + # { + # "time": 1548654951, + # "contract": "BTC_USDT", + # "size": 600, + # "leverage": "25", + # "margin": "0.006705256878", + # "entry_price": "3536.123", + # "liq_price": "3421.54", + # "mark_price": "3420.27", + # "order_id": 317393847, + # "order_price": "3405", + # "fill_price": "3424", + # "left": 0 + # } + # ] + # + # option + # + # [ + # { + # "time": 1631764800, + # "pnl": "-42914.291", + # "settle_size": "-10001", + # "side": "short", + # "contract": "BTC_USDT-20210916-5000-C", + # "text": "settled" + # } + # ] + # + return self.parse_liquidations(response, market, since, limit) + + def parse_liquidation(self, liquidation, market: Market = None): + # + # fetchLiquidations + # + # { + # "contract": "BTC_USDT", + # "left": 0, + # "size": -165, + # "fill_price": "28070", + # "order_price": "28225", + # "time": 1696736132 + # } + # + # swap and future: fetchMyLiquidations + # + # { + # "time": 1548654951, + # "contract": "BTC_USDT", + # "size": 600, + # "leverage": "25", + # "margin": "0.006705256878", + # "entry_price": "3536.123", + # "liq_price": "3421.54", + # "mark_price": "3420.27", + # "order_id": 317393847, + # "order_price": "3405", + # "fill_price": "3424", + # "left": 0 + # } + # + # option: fetchMyLiquidations + # + # { + # "time": 1631764800, + # "pnl": "-42914.291", + # "settle_size": "-10001", + # "side": "short", + # "contract": "BTC_USDT-20210916-5000-C", + # "text": "settled" + # } + # + marketId = self.safe_string(liquidation, 'contract') + timestamp = self.safe_timestamp(liquidation, 'time') + size = self.safe_string_2(liquidation, 'size', 'settle_size') + left = self.safe_string(liquidation, 'left', '0') + contractsString = Precise.string_abs(Precise.string_sub(size, left)) + contractSizeString = self.safe_string(market, 'contractSize') + priceString = self.safe_string_2(liquidation, 'liq_price', 'fill_price') + baseValueString = Precise.string_mul(contractsString, contractSizeString) + quoteValueString = self.safe_string(liquidation, 'pnl') + if quoteValueString is None: + quoteValueString = Precise.string_mul(baseValueString, priceString) + # --- derive side --- + # 1) options payload has explicit 'side': 'long' | 'short' + optPos = self.safe_string_lower(liquidation, 'side') + side: Str = None + if optPos == 'long': + side = 'buy' + elif optPos == 'short': + side = 'sell' + else: + if size is not None: # 2) futures/perpetual(and fallback for options): infer from size + if Precise.string_gt(size, '0'): + side = 'buy' + elif Precise.string_lt(size, '0'): + side = 'sell' + return self.safe_liquidation({ + 'info': liquidation, + 'symbol': self.safe_symbol(marketId, market), + 'contracts': self.parse_number(contractsString), + 'contractSize': self.parse_number(contractSizeString), + 'price': self.parse_number(priceString), + 'side': side, + 'baseValue': self.parse_number(baseValueString), + 'quoteValue': self.parse_number(Precise.string_abs(quoteValueString)), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + + def fetch_greeks(self, symbol: str, params={}) -> Greeks: + """ + fetches an option contracts greeks, financial metrics used to measure the factors that affect the price of an options contract + + https://www.gate.com/docs/developers/apiv4/en/#list-tickers-of-options-contracts + + :param str symbol: unified symbol of the market to fetch greeks for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `greeks structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'underlying': market['info']['underlying'], + } + response = self.publicOptionsGetTickers(self.extend(request, params)) + # + # [ + # { + # "vega": "1.78992", + # "leverage": "6.2096777055417", + # "ask_iv": "0.6245", + # "delta": "-0.69397", + # "last_price": "0", + # "theta": "-2.5723", + # "bid1_price": "222.9", + # "mark_iv": "0.5909", + # "name": "ETH_USDT-20231201-2300-P", + # "bid_iv": "0.5065", + # "ask1_price": "243.6", + # "mark_price": "236.57", + # "position_size": 0, + # "bid1_size": 368, + # "ask1_size": -335, + # "gamma": "0.00116" + # }, + # ] + # + marketId = market['id'] + for i in range(0, len(response)): + entry = response[i] + entryMarketId = self.safe_string(entry, 'name') + if entryMarketId == marketId: + return self.parse_greeks(entry, market) + return None + + def parse_greeks(self, greeks: dict, market: Market = None) -> Greeks: + # + # { + # "vega": "1.78992", + # "leverage": "6.2096777055417", + # "ask_iv": "0.6245", + # "delta": "-0.69397", + # "last_price": "0", + # "theta": "-2.5723", + # "bid1_price": "222.9", + # "mark_iv": "0.5909", + # "name": "ETH_USDT-20231201-2300-P", + # "bid_iv": "0.5065", + # "ask1_price": "243.6", + # "mark_price": "236.57", + # "position_size": 0, + # "bid1_size": 368, + # "ask1_size": -335, + # "gamma": "0.00116" + # } + # + marketId = self.safe_string(greeks, 'name') + symbol = self.safe_symbol(marketId, market) + return { + 'symbol': symbol, + 'timestamp': None, + 'datetime': None, + 'delta': self.safe_number(greeks, 'delta'), + 'gamma': self.safe_number(greeks, 'gamma'), + 'theta': self.safe_number(greeks, 'theta'), + 'vega': self.safe_number(greeks, 'vega'), + 'rho': None, + 'bidSize': self.safe_number(greeks, 'bid1_size'), + 'askSize': self.safe_number(greeks, 'ask1_size'), + 'bidImpliedVolatility': self.safe_number(greeks, 'bid_iv'), + 'askImpliedVolatility': self.safe_number(greeks, 'ask_iv'), + 'markImpliedVolatility': self.safe_number(greeks, 'mark_iv'), + 'bidPrice': self.safe_number(greeks, 'bid1_price'), + 'askPrice': self.safe_number(greeks, 'ask1_price'), + 'markPrice': self.safe_number(greeks, 'mark_price'), + 'lastPrice': self.safe_number(greeks, 'last_price'), + 'underlyingPrice': self.parse_number(market['info']['underlying_price']), + 'info': greeks, + } + + def close_position(self, symbol: str, side: OrderSide = None, params={}) -> Order: + """ + closes open positions for a market + + https://www.gate.com/docs/developers/apiv4/en/#create-a-futures-order + https://www.gate.com/docs/developers/apiv4/en/#create-a-futures-order-2 + https://www.gate.com/docs/developers/apiv4/en/#create-an-options-order + + :param str symbol: Unified CCXT market symbol + :param str side: 'buy' or 'sell' + :param dict [params]: extra parameters specific to the okx api endpoint + :returns dict[]: `A list of position structures ` + """ + request: dict = { + 'close': True, + } + params = self.extend(request, params) + if side is None: + side = '' # side is not used but needs to be present, otherwise crashes in php + return self.create_order(symbol, 'market', side, 0, None, params) + + def fetch_leverage(self, symbol: str, params={}) -> Leverage: + """ + fetch the set leverage for a market + + https://www.gate.com/docs/developers/apiv4/en/#get-unified-account-information + https://www.gate.com/docs/developers/apiv4/en/#get-detail-of-lending-market + https://www.gate.com/docs/developers/apiv4/en/#query-one-single-margin-currency-pair-deprecated + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.unified]: default False, set to True for fetching the unified accounts leverage + :returns dict: a `leverage structure ` + """ + self.load_markets() + market = None + if symbol is not None: + # unified account does not require a symbol + market = self.market(symbol) + request: dict = {} + response = None + isUnified = self.safe_bool(params, 'unified') + params = self.omit(params, 'unified') + if market['spot']: + request['currency_pair'] = market['id'] + if isUnified: + response = self.publicMarginGetUniCurrencyPairsCurrencyPair(self.extend(request, params)) + # + # { + # "currency_pair": "BTC_USDT", + # "base_min_borrow_amount": "0.0001", + # "quote_min_borrow_amount": "1", + # "leverage": "10" + # } + # + else: + response = self.publicMarginGetCurrencyPairsCurrencyPair(self.extend(request, params)) + # + # { + # "id": "BTC_USDT", + # "base": "BTC", + # "quote": "USDT", + # "leverage": 10, + # "min_base_amount": "0.0001", + # "min_quote_amount": "1", + # "max_quote_amount": "40000000", + # "status": 1 + # } + # + elif isUnified: + response = self.privateUnifiedGetAccounts(self.extend(request, params)) + # + # { + # "user_id": 10001, + # "locked": False, + # "balances": { + # "ETH": { + # "available": "0", + # "freeze": "0", + # "borrowed": "0.075393666654", + # "negative_liab": "0", + # "futures_pos_liab": "0", + # "equity": "1016.1", + # "total_freeze": "0", + # "total_liab": "0" + # }, + # "POINT": { + # "available": "9999999999.017023138734", + # "freeze": "0", + # "borrowed": "0", + # "negative_liab": "0", + # "futures_pos_liab": "0", + # "equity": "12016.1", + # "total_freeze": "0", + # "total_liab": "0" + # }, + # "USDT": { + # "available": "0.00000062023", + # "freeze": "0", + # "borrowed": "0", + # "negative_liab": "0", + # "futures_pos_liab": "0", + # "equity": "16.1", + # "total_freeze": "0", + # "total_liab": "0" + # } + # }, + # "total": "230.94621713", + # "borrowed": "161.66395521", + # "total_initial_margin": "1025.0524665088", + # "total_margin_balance": "3382495.944473949183", + # "total_maintenance_margin": "205.01049330176", + # "total_initial_margin_rate": "3299.827135672679", + # "total_maintenance_margin_rate": "16499.135678363399", + # "total_available_margin": "3381470.892007440383", + # "unified_account_total": "3381470.892007440383", + # "unified_account_total_liab": "0", + # "unified_account_total_equity": "100016.1", + # "leverage": "2" + # } + # + else: + raise NotSupported(self.id + ' fetchLeverage() does not support ' + market['type'] + ' markets') + return self.parse_leverage(response, market) + + def fetch_leverages(self, symbols: Strings = None, params={}) -> Leverages: + """ + fetch the set leverage for all leverage markets, only spot margin is supported on gate + + https://www.gate.com/docs/developers/apiv4/en/#list-lending-markets + https://www.gate.com/docs/developers/apiv4/en/#list-all-supported-currency-pairs-supported-in-margin-trading-deprecated + + :param str[] symbols: a list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.unified]: default False, set to True for fetching unified account leverages + :returns dict: a list of `leverage structures ` + """ + self.load_markets() + symbols = self.market_symbols(symbols) + response = None + isUnified = self.safe_bool(params, 'unified') + params = self.omit(params, 'unified') + marketIdRequest = 'id' + if isUnified: + marketIdRequest = 'currency_pair' + response = self.publicMarginGetUniCurrencyPairs(params) + # + # [ + # { + # "currency_pair": "1INCH_USDT", + # "base_min_borrow_amount": "8", + # "quote_min_borrow_amount": "1", + # "leverage": "3" + # }, + # ] + # + else: + response = self.publicMarginGetCurrencyPairs(params) + # + # [ + # { + # "id": "1CAT_USDT", + # "base": "1CAT", + # "quote": "USDT", + # "leverage": 3, + # "min_base_amount": "71", + # "min_quote_amount": "1", + # "max_quote_amount": "10000", + # "status": 1 + # }, + # ] + # + return self.parse_leverages(response, symbols, marketIdRequest, 'spot') + + def parse_leverage(self, leverage: dict, market: Market = None) -> Leverage: + marketId = self.safe_string_2(leverage, 'currency_pair', 'id') + leverageValue = self.safe_integer(leverage, 'leverage') + return { + 'info': leverage, + 'symbol': self.safe_symbol(marketId, market, '_', 'spot'), + 'marginMode': None, + 'longLeverage': leverageValue, + 'shortLeverage': leverageValue, + } + + def fetch_option(self, symbol: str, params={}) -> Option: + """ + fetches option data that is commonly found in an option chain + + https://www.gate.com/docs/developers/apiv4/en/#query-specified-contract-detail + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `option chain structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'contract': market['id'], + } + response = self.publicOptionsGetContractsContract(self.extend(request, params)) + # + # { + # "is_active": True, + # "mark_price_round": "0.01", + # "settle_fee_rate": "0.00015", + # "bid1_size": 30, + # "taker_fee_rate": "0.0003", + # "price_limit_fee_rate": "0.1", + # "order_price_round": "0.1", + # "tag": "month", + # "ref_rebate_rate": "0", + # "name": "ETH_USDT-20240628-4500-C", + # "strike_price": "4500", + # "ask1_price": "280.5", + # "ref_discount_rate": "0", + # "order_price_deviate": "0.2", + # "ask1_size": -19, + # "mark_price_down": "155.45", + # "orderbook_id": 11724695, + # "is_call": True, + # "last_price": "188.7", + # "mark_price": "274.26", + # "underlying": "ETH_USDT", + # "create_time": 1688024882, + # "settle_limit_fee_rate": "0.1", + # "orders_limit": 10, + # "mark_price_up": "403.83", + # "position_size": 80, + # "order_size_max": 10000, + # "position_limit": 100000, + # "multiplier": "0.01", + # "order_size_min": 1, + # "trade_size": 229, + # "underlying_price": "3326.6", + # "maker_fee_rate": "0.0003", + # "expiration_time": 1719561600, + # "trade_id": 15, + # "bid1_price": "269.3" + # } + # + return self.parse_option(response, None, market) + + def fetch_option_chain(self, code: str, params={}) -> OptionChain: + """ + fetches data for an underlying asset that is commonly found in an option chain + + https://www.gate.com/docs/developers/apiv4/en/#list-all-the-contracts-with-specified-underlying-and-expiration-time + + :param str code: base currency to fetch an option chain for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.underlying]: the underlying asset, can be obtained from fetchUnderlyingAssets() + :param int [params.expiration]: unix timestamp of the expiration time + :returns dict: a list of `option chain structures ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'underlying': currency['code'] + '_USDT', # todo: currency['id'].upper() & network junctions + } + response = self.publicOptionsGetContracts(self.extend(request, params)) + # + # [ + # { + # "is_active": True, + # "mark_price_round": "0.1", + # "settle_fee_rate": "0.00015", + # "bid1_size": 434, + # "taker_fee_rate": "0.0003", + # "price_limit_fee_rate": "0.1", + # "order_price_round": "1", + # "tag": "day", + # "ref_rebate_rate": "0", + # "name": "BTC_USDT-20240324-63500-P", + # "strike_price": "63500", + # "ask1_price": "387", + # "ref_discount_rate": "0", + # "order_price_deviate": "0.15", + # "ask1_size": -454, + # "mark_price_down": "124.3", + # "orderbook_id": 29600, + # "is_call": False, + # "last_price": "0", + # "mark_price": "366.6", + # "underlying": "BTC_USDT", + # "create_time": 1711118829, + # "settle_limit_fee_rate": "0.1", + # "orders_limit": 10, + # "mark_price_up": "630", + # "position_size": 0, + # "order_size_max": 10000, + # "position_limit": 10000, + # "multiplier": "0.01", + # "order_size_min": 1, + # "trade_size": 0, + # "underlying_price": "64084.65", + # "maker_fee_rate": "0.0003", + # "expiration_time": 1711267200, + # "trade_id": 0, + # "bid1_price": "307" + # }, + # ] + # + return self.parse_option_chain(response, None, 'name') + + def parse_option(self, chain: dict, currency: Currency = None, market: Market = None) -> Option: + # + # { + # "is_active": True, + # "mark_price_round": "0.1", + # "settle_fee_rate": "0.00015", + # "bid1_size": 434, + # "taker_fee_rate": "0.0003", + # "price_limit_fee_rate": "0.1", + # "order_price_round": "1", + # "tag": "day", + # "ref_rebate_rate": "0", + # "name": "BTC_USDT-20240324-63500-P", + # "strike_price": "63500", + # "ask1_price": "387", + # "ref_discount_rate": "0", + # "order_price_deviate": "0.15", + # "ask1_size": -454, + # "mark_price_down": "124.3", + # "orderbook_id": 29600, + # "is_call": False, + # "last_price": "0", + # "mark_price": "366.6", + # "underlying": "BTC_USDT", + # "create_time": 1711118829, + # "settle_limit_fee_rate": "0.1", + # "orders_limit": 10, + # "mark_price_up": "630", + # "position_size": 0, + # "order_size_max": 10000, + # "position_limit": 10000, + # "multiplier": "0.01", + # "order_size_min": 1, + # "trade_size": 0, + # "underlying_price": "64084.65", + # "maker_fee_rate": "0.0003", + # "expiration_time": 1711267200, + # "trade_id": 0, + # "bid1_price": "307" + # } + # + marketId = self.safe_string(chain, 'name') + market = self.safe_market(marketId, market) + timestamp = self.safe_timestamp(chain, 'create_time') + return { + 'info': chain, + 'currency': None, + 'symbol': market['symbol'], + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'impliedVolatility': None, + 'openInterest': None, + 'bidPrice': self.safe_number(chain, 'bid1_price'), + 'askPrice': self.safe_number(chain, 'ask1_price'), + 'midPrice': None, + 'markPrice': self.safe_number(chain, 'mark_price'), + 'lastPrice': self.safe_number(chain, 'last_price'), + 'underlyingPrice': self.safe_number(chain, 'underlying_price'), + 'change': None, + 'percentage': None, + 'baseVolume': None, + 'quoteVolume': None, + } + + def fetch_positions_history(self, symbols: Strings = None, since: Int = None, limit: Int = None, params={}) -> List[Position]: + """ + fetches historical positions + + https://www.gate.com/docs/developers/apiv4/#list-position-close-history + https://www.gate.com/docs/developers/apiv4/#list-position-close-history-2 + + :param str[] symbols: unified conract symbols, must all have the same settle currency and the same market type + :param int [since]: the earliest time in ms to fetch positions for + :param int [limit]: the maximum amount of records to fetch, default=1000 + :param dict params: extra parameters specific to the exchange api endpoint + :param int [params.until]: the latest time in ms to fetch positions for + + EXCHANGE SPECIFIC PARAMETERS + :param int [params.offset]: list offset, starting from 0 + :param str [params.side]: long or short + :param str [params.pnl]: query profit or loss + :returns dict[]: a list of `position structures ` + """ + self.load_markets() + market = None + if symbols is not None: + symbolsLength = len(symbols) + if symbolsLength == 1: + market = self.market(symbols[0]) + marketType = None + marketType, params = self.handle_market_type_and_params('fetchPositionsHistory', market, params, 'swap') + until = self.safe_integer(params, 'until') + params = self.omit(params, 'until') + request: dict = {} + request, params = self.prepare_request(market, marketType, params) + if limit is not None: + request['limit'] = limit + if since is not None: + request['from'] = self.parse_to_int(since / 1000) + if until is not None: + request['to'] = self.parse_to_int(until / 1000) + response = None + if marketType == 'swap': + response = self.privateFuturesGetSettlePositionClose(self.extend(request, params)) + elif marketType == 'future': + response = self.privateDeliveryGetSettlePositionClose(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchPositionsHistory() does not support markets of type ' + marketType) + # + # [ + # { + # "contract": "SLERF_USDT", + # "text": "web", + # "long_price": "0.766306", + # "pnl": "-23.41702352", + # "pnl_pnl": "-22.7187", + # "pnl_fee": "-0.06527125", + # "pnl_fund": "-0.63305227", + # "accum_size": "100", + # "time": 1711279263, + # "short_price": "0.539119", + # "side": "long", + # "max_size": "100", + # "first_open_time": 1711037985 + # }, + # ... + # ] + # + return self.parse_positions(response, symbols, params) + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None + # + # {"label": "ORDER_NOT_FOUND", "message": "Order not found"} + # {"label": "INVALID_PARAM_VALUE", "message": "invalid argument: status"} + # {"label": "INVALID_PARAM_VALUE", "message": "invalid argument: Trigger.rule"} + # {"label": "INVALID_PARAM_VALUE", "message": "invalid argument: trigger.expiration invalid range"} + # {"label": "INVALID_ARGUMENT", "detail": "invalid size"} + # {"user_id":10406147,"id":"id","succeeded":false,"message":"INVALID_PROTOCOL","label":"INVALID_PROTOCOL"} + # + label = self.safe_string(response, 'label') + if label is not None: + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], label, feedback) + raise ExchangeError(feedback) + return None diff --git a/ccxt/gateio.py b/ccxt/gateio.py new file mode 100644 index 0000000..f52d6b7 --- /dev/null +++ b/ccxt/gateio.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.gate import gate +from ccxt.abstract.gateio import ImplicitAPI +from ccxt.base.types import Any + + +class gateio(gate, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(gateio, self).describe(), { + 'id': 'gateio', + 'alias': True, + }) diff --git a/ccxt/gemini.py b/ccxt/gemini.py new file mode 100644 index 0000000..479c086 --- /dev/null +++ b/ccxt/gemini.py @@ -0,0 +1,1933 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.gemini import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currencies, Currency, DepositAddress, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, TradingFees, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import NotSupported +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.errors import OnMaintenance +from ccxt.base.errors import InvalidNonce +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class gemini(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(gemini, self).describe(), { + 'id': 'gemini', + 'name': 'Gemini', + 'countries': ['US'], + # 600 requests a minute = 10 requests per second => 1000ms / 10 = 100ms between requests(private endpoints) + # 120 requests a minute = 2 requests per second =>( 1000ms / rateLimit ) / 2 = 5(public endpoints) + 'rateLimit': 100, + 'version': 'v1', + 'pro': True, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': True, + 'future': False, + 'option': False, + 'addMargin': False, + 'cancelOrder': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createDepositAddress': True, + 'createMarketOrder': False, + 'createOrder': True, + 'createReduceOnlyOrder': False, + 'fetchBalance': True, + 'fetchBidsAsks': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchClosedOrders': False, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': True, + 'fetchDepositsWithdrawals': True, + 'fetchFundingHistory': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchLeverage': False, + 'fetchLeverageTiers': False, + 'fetchMarginMode': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterestHistory': False, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': False, + 'fetchPosition': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': True, + 'fetchTransactions': 'emulated', + 'postOnly': True, + 'reduceMargin': False, + 'sandbox': True, + 'setLeverage': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'withdraw': True, + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/27816857-ce7be644-6096-11e7-82d6-3c257263229c.jpg', + 'api': { + 'public': 'https://api.gemini.com', + 'private': 'https://api.gemini.com', + 'web': 'https://docs.gemini.com', + 'webExchange': 'https://exchange.gemini.com', + }, + 'www': 'https://gemini.com/', + 'doc': [ + 'https://docs.gemini.com/rest-api', + 'https://docs.sandbox.gemini.com', + ], + 'test': { + 'public': 'https://api.sandbox.gemini.com', + 'private': 'https://api.sandbox.gemini.com', + # use the True doc instead of the sandbox doc + # since they differ in parsing + # https://github.com/ccxt/ccxt/issues/7874 + # https://github.com/ccxt/ccxt/issues/7894 + 'web': 'https://docs.gemini.com', + 'webExchange': 'https://exchange.gemini.com', + }, + 'fees': [ + 'https://gemini.com/api-fee-schedule', + 'https://gemini.com/trading-fees', + 'https://gemini.com/transfer-fees', + ], + }, + 'api': { + 'webExchange': { + 'get': [ + '', + ], + }, + 'web': { + 'get': [ + 'rest-api', + ], + }, + 'public': { + 'get': { + 'v1/symbols': 5, + 'v1/symbols/details/{symbol}': 5, + 'v1/staking/rates': 5, + 'v1/pubticker/{symbol}': 5, + 'v2/ticker/{symbol}': 5, + 'v2/candles/{symbol}/{timeframe}': 5, + 'v1/trades/{symbol}': 5, + 'v1/auction/{symbol}': 5, + 'v1/auction/{symbol}/history': 5, + 'v1/pricefeed': 5, + 'v1/book/{symbol}': 5, + 'v1/earn/rates': 5, + }, + }, + 'private': { + 'post': { + 'v1/staking/unstake': 1, + 'v1/staking/stake': 1, + 'v1/staking/rewards': 1, + 'v1/staking/history': 1, + 'v1/order/new': 1, + 'v1/order/cancel': 1, + 'v1/wrap/{symbol}': 1, + 'v1/order/cancel/session': 1, + 'v1/order/cancel/all': 1, + 'v1/order/status': 1, + 'v1/orders': 1, + 'v1/mytrades': 1, + 'v1/notionalvolume': 1, + 'v1/tradevolume': 1, + 'v1/clearing/new': 1, + 'v1/clearing/status': 1, + 'v1/clearing/cancel': 1, + 'v1/clearing/confirm': 1, + 'v1/balances': 1, + 'v1/balances/staking': 1, + 'v1/notionalbalances/{currency}': 1, + 'v1/transfers': 1, + 'v1/addresses/{network}': 1, + 'v1/deposit/{network}/newAddress': 1, + 'v1/deposit/{currency}/newAddress': 1, + 'v1/withdraw/{currency}': 1, + 'v1/account/transfer/{currency}': 1, + 'v1/payments/addbank': 1, + 'v1/payments/methods': 1, + 'v1/payments/sen/withdraw': 1, + 'v1/balances/earn': 1, + 'v1/earn/interest': 1, + 'v1/earn/history': 1, + 'v1/approvedAddresses/{network}/request': 1, + 'v1/approvedAddresses/account/{network}': 1, + 'v1/approvedAddresses/{network}/remove': 1, + 'v1/account': 1, + 'v1/account/create': 1, + 'v1/account/list': 1, + 'v1/heartbeat': 1, + 'v1/roles': 1, + }, + }, + }, + 'precisionMode': TICK_SIZE, + 'fees': { + 'trading': { + 'taker': 0.004, + 'maker': 0.002, + }, + }, + 'httpExceptions': { + '400': BadRequest, # Auction not open or paused, ineligible timing, market not open, or the request was malformed, in the case of a private API request, missing or malformed Gemini private API authentication headers + '403': PermissionDenied, # The API key is missing the role necessary to access self private API endpoint + '404': OrderNotFound, # Unknown API entry point or Order not found + '406': InsufficientFunds, # Insufficient Funds + '429': RateLimitExceeded, # Rate Limiting was applied + '500': ExchangeError, # The server encountered an error + '502': ExchangeNotAvailable, # Technical issues are preventing the request from being satisfied + '503': OnMaintenance, # The exchange is down for maintenance + }, + 'timeframes': { + '1m': '1m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1hr', + '6h': '6hr', + '1d': '1day', + }, + 'exceptions': { + 'exact': { + 'AuctionNotOpen': BadRequest, # Failed to place an auction-only order because there is no current auction open for self symbol + 'ClientOrderIdTooLong': BadRequest, # The Client Order ID must be under 100 characters + 'ClientOrderIdMustBeString': BadRequest, # The Client Order ID must be a string + 'ConflictingOptions': BadRequest, # New orders using a combination of order execution options are not supported + 'EndpointMismatch': BadRequest, # The request was submitted to an endpoint different than the one in the payload + 'EndpointNotFound': BadRequest, # No endpoint was specified + 'IneligibleTiming': BadRequest, # Failed to place an auction order for the current auction on self symbol because the timing is not eligible, new orders may only be placed before the auction begins. + 'InsufficientFunds': InsufficientFunds, # The order was rejected because of insufficient funds + 'InvalidJson': BadRequest, # The JSON provided is invalid + 'InvalidNonce': InvalidNonce, # The nonce was not greater than the previously used nonce, or was not present + 'InvalidApiKey': AuthenticationError, # Invalid API key + 'InvalidOrderType': InvalidOrder, # An unknown order type was provided + 'InvalidPrice': InvalidOrder, # For new orders, the price was invalid + 'InvalidQuantity': InvalidOrder, # A negative or otherwise invalid quantity was specified + 'InvalidSide': InvalidOrder, # For new orders, and invalid side was specified + 'InvalidSignature': AuthenticationError, # The signature did not match the expected signature + 'InvalidSymbol': BadRequest, # An invalid symbol was specified + 'InvalidTimestampInPayload': BadRequest, # The JSON payload contained a timestamp parameter with an unsupported value. + 'Maintenance': OnMaintenance, # The system is down for maintenance + 'MarketNotOpen': InvalidOrder, # The order was rejected because the market is not accepting new orders + 'MissingApikeyHeader': AuthenticationError, # The X-GEMINI-APIKEY header was missing + 'MissingOrderField': InvalidOrder, # A required order_id field was not specified + 'MissingRole': AuthenticationError, # The API key used to access self endpoint does not have the required role assigned to it + 'MissingPayloadHeader': AuthenticationError, # The X-GEMINI-PAYLOAD header was missing + 'MissingSignatureHeader': AuthenticationError, # The X-GEMINI-SIGNATURE header was missing + 'NoSSL': AuthenticationError, # You must use HTTPS to access the API + 'OptionsMustBeArray': BadRequest, # The options parameter must be an array. + 'OrderNotFound': OrderNotFound, # The order specified was not found + 'RateLimit': RateLimitExceeded, # Requests were made too frequently. See Rate Limits below. + 'System': ExchangeError, # We are experiencing technical issues + 'UnsupportedOption': BadRequest, # This order execution option is not supported. + }, + 'broad': { + 'The Gemini Exchange is currently undergoing maintenance.': OnMaintenance, # The Gemini Exchange is currently undergoing maintenance. Please check https://status.gemini.com/ for more information. + 'We are investigating technical issues with the Gemini Exchange.': ExchangeNotAvailable, # We are investigating technical issues with the Gemini Exchange. Please check https://status.gemini.com/ for more information. + 'Internal Server Error': ExchangeNotAvailable, + }, + }, + 'options': { + 'fetchMarketsMethod': 'fetch_markets_from_api', # fetch_markets_from_api, fetch_markets_from_web + 'fetchMarketFromWebRetries': 10, + 'fetchMarketsFromAPI': { + 'fetchDetailsForAllSymbols': False, + 'quoteCurrencies': ['USDT', 'GUSD', 'USD', 'DAI', 'EUR', 'GBP', 'SGD', 'BTC', 'ETH', 'LTC', 'BCH', 'SOL', 'USDC'], + }, + 'fetchMarkets': { + 'webApiEnable': True, # fetches from WEB + 'webApiRetries': 10, + }, + 'fetchUsdtMarkets': ['btcusdt', 'ethusdt'], # self is only used if markets-fetch is set from "web"; keep self list updated(not available trough web api) + 'fetchCurrencies': { + 'webApiEnable': True, # fetches from WEB + 'webApiRetries': 5, + 'webApiMuteFailure': True, + }, + 'fetchTickerMethod': 'fetchTickerV1', # fetchTickerV1, fetchTickerV2, fetchTickerV1AndV2 + 'networks': { + 'BTC': 'bitcoin', + 'ERC20': 'ethereum', + 'BCH': 'bitcoincash', + 'LTC': 'litecoin', + 'ZEC': 'zcash', + 'FIL': 'filecoin', + 'DOGE': 'dogecoin', + 'XTZ': 'tezos', + 'AVAXX': 'avalanche', + 'SOL': 'solana', + 'ATOM': 'cosmos', + 'DOT': 'polkadot', + }, + 'nonce': 'milliseconds', # if getting a Network 400 error change to seconds, + 'conflictingMarkets': { + 'paxgusd': { + 'base': 'PAXG', + 'quote': 'USD', + }, + }, + 'brokenPairs': ['efilusd', 'maticrlusd', 'maticusdc', 'eurusdc', 'maticgusd', 'maticusd', 'efilfil', 'eurusd'], + }, + 'features': { + 'default': { + 'sandbox': True, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 500, + 'daysBack': None, + 'untilDays': None, + 'symbolRequired': True, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': None, # todo: implement + 'fetchOHLCV': { + 'limit': None, + }, + }, + 'spot': { + 'extends': 'default', + }, + 'swap': { + 'linear': { + 'extends': 'default', + }, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + }) + + def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + :param dict [params]: extra parameters specific to the endpoint + :returns dict: an associative dictionary of currencies + """ + return self.fetch_currencies_from_web(params) + + def fetch_currencies_from_web(self, params={}): + """ + @ignore + fetches all available currencies on an exchange + :param dict [params]: extra parameters specific to the endpoint + :returns dict: an associative dictionary of currencies + """ + data = self.fetch_web_endpoint('fetchCurrencies', 'webExchangeGet', True, '="currencyData">', '') + if data is None: + return {} + # + # { + # "tradingPairs": [['BTCUSD', 2, 8, '0.00001', 10, True], ...], + # "currencies": [ + # ["ORCA", "Orca", 204, 6, 0, 6, 8, False, null, "solana"], #, precisions seem to be the 5th index + # ["ATOM", "Cosmos", 44, 6, 0, 6, 8, False, null, "cosmos"], + # ["ETH", "Ether", 2, 6, 0, 18, 8, False, null, "ethereum"], + # ["GBP", "Pound Sterling", 22, 2, 2, 2, 2, True, "£", null], + # ... + # ], + # "networks": [ + # ["solana", "SOL", "Solana"], + # ["zcash", "ZEC", "Zcash"], + # ["tezos", "XTZ", "Tezos"], + # ["cosmos", "ATOM", "Cosmos"], + # ["ethereum", "ETH", "Ethereum"], + # ... + # ] + # } + # + result: dict = {} + self.options['tradingPairs'] = self.safe_list(data, 'tradingPairs') + currenciesArray = self.safe_value(data, 'currencies', []) + for i in range(0, len(currenciesArray)): + currency = currenciesArray[i] + id = self.safe_string(currency, 0) + code = self.safe_currency_code(id) + type = 'fiat' if self.safe_string(currency, 7) else 'crypto' + precision = self.parse_number(self.parse_precision(self.safe_string(currency, 5))) + networks: dict = {} + networkId = self.safe_string(currency, 9) + networkCode = None + if networkId is not None: + networkCode = self.network_id_to_code(networkId) + networks[networkCode] = { + 'info': currency, + 'id': networkId, + 'network': networkCode, + 'active': None, + 'deposit': None, + 'withdraw': None, + 'fee': None, + 'precision': precision, + 'limits': { + 'deposit': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + }, + } + result[code] = self.safe_currency_structure({ + 'info': currency, + 'id': id, + 'code': code, + 'name': self.safe_string(currency, 1), + 'active': None, + 'deposit': None, + 'withdraw': None, + 'fee': None, + 'type': type, + 'precision': precision, + 'limits': { + 'deposit': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + }, + 'networks': networks, + }) + return result + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for gemini + + https://docs.gemini.com/rest-api/#symbols + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + method = self.safe_value(self.options, 'fetchMarketsMethod', 'fetch_markets_from_api') + if method == 'fetch_markets_from_web': + promises = [] + promises.append(self.fetch_markets_from_web(params)) # get usd markets + promises.append(self.fetch_usdt_markets(params)) # get usdt markets + promisesResult = promises + return self.array_concat(promisesResult[0], promisesResult[1]) + return self.fetch_markets_from_api(params) + + def fetch_markets_from_web(self, params={}): + data = self.fetch_web_endpoint('fetchMarkets', 'webGetRestApi', False, '

Symbols and minimums

') + error = self.id + ' fetchMarketsFromWeb() the API doc HTML markup has changed, breaking the parser of order limits and precision info for markets.' + tables = data.split('tbody>') + numTables = len(tables) + if numTables < 2: + raise NotSupported(error) + rows = tables[1].split("\n\n") # eslint-disable-line quotes + numRows = len(rows) + if numRows < 2: + raise NotSupported(error) + result = [] + # skip the first element(empty string) + for i in range(1, numRows): + row = rows[i] + cells = row.split("\n") # eslint-disable-line quotes + numCells = len(cells) + if numCells < 5: + raise NotSupported(error) + # [ + # 'btcusd', # currency + # '0.00001 BTC(1e-5)', # min order size + # '0.00000001 BTC(1e-8)', # tick size + # '0.01 USD', # quote currency price increment + # '' + # ] + marketId = cells[0].replace('', '') + marketId = marketId.replace('*', '') + # base = self.safe_currency_code(baseId) + minAmountString = cells[1].replace('', '') + minAmountParts = minAmountString.split(' ') + minAmount = self.safe_number(minAmountParts, 0) + amountPrecisionString = cells[2].replace('', '') + amountPrecisionParts = amountPrecisionString.split(' ') + idLength = len(marketId) - 0 + startingIndex = idLength - 3 + pricePrecisionString = cells[3].replace('', '') + pricePrecisionParts = pricePrecisionString.split(' ') + quoteId = self.safe_string_lower(pricePrecisionParts, 1, marketId[startingIndex:idLength]) + baseId = self.safe_string_lower(amountPrecisionParts, 1, marketId.replace(quoteId, '')) + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + result.append({ + 'id': marketId, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': None, + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.safe_number(amountPrecisionParts, 0), + 'price': self.safe_number(pricePrecisionParts, 0), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': minAmount, + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + 'info': row, + }) + return result + + def parse_market_active(self, status): + statuses: dict = { + 'open': True, + 'closed': False, + 'cancel_only': True, + 'post_only': True, + 'limit_only': True, + } + if status is None: + return True # below + return self.safe_bool(statuses, status, True) + + def fetch_usdt_markets(self, params={}): + # these markets can't be scrapped and fetchMarketsFrom api does an extra call + # to load market ids which we don't need here + if 'test' in self.urls: + return [] # sandbox does not have usdt markets + fetchUsdtMarkets = self.safe_value(self.options, 'fetchUsdtMarkets', []) + result = [] + for i in range(0, len(fetchUsdtMarkets)): + marketId = fetchUsdtMarkets[i] + request: dict = { + 'symbol': marketId, + } + # don't use Promise.all here, for some reason the exchange can't handle it and crashes + rawResponse = self.publicGetV1SymbolsDetailsSymbol(self.extend(request, params)) + result.append(self.parse_market(rawResponse)) + return result + + def fetch_markets_from_api(self, params={}): + marketIdsRaw = self.publicGetV1Symbols(params) + # + # [ + # "btcusd", + # "linkusd", + # ... + # ] + # + result = [] + options = self.safe_dict(self.options, 'fetchMarketsFromAPI', {}) + brokenPairs = self.safe_list(self.options, 'brokenPairs', []) + marketIds = [] + for i in range(0, len(marketIdsRaw)): + if not self.in_array(marketIdsRaw[i], brokenPairs): + marketIds.append(marketIdsRaw[i]) + if self.safe_bool(options, 'fetchDetailsForAllSymbols', False): + promises = [] + for i in range(0, len(marketIds)): + marketId = marketIds[i] + request: dict = { + 'symbol': marketId, + } + promises.append(self.publicGetV1SymbolsDetailsSymbol(self.extend(request, params))) + # + # { + # "symbol": "BTCUSD", + # "base_currency": "BTC", + # "quote_currency": "USD", + # "tick_size": 1E-8, + # "quote_increment": 0.01, + # "min_order_size": "0.00001", + # "status": "open", + # "wrap_enabled": False + # } + # + responses = promises + for i in range(0, len(responses)): + result.append(self.parse_market(responses[i])) + else: + # use trading-pairs info, if it was fetched + tradingPairs = self.safe_list(self.options, 'tradingPairs') + if tradingPairs is not None: + indexedTradingPairs = self.index_by(tradingPairs, 0) + for i in range(0, len(marketIds)): + marketId = marketIds[i] + pairInfo = self.safe_list(indexedTradingPairs, marketId.upper()) + if pairInfo is not None and not self.in_array(marketId, brokenPairs): + result.append(self.parse_market(pairInfo)) + else: + for i in range(0, len(marketIds)): + if not self.in_array(marketIds[i], brokenPairs): + result.append(self.parse_market(marketIds[i])) + return result + + def parse_market(self, response) -> Market: + # + # response might be: + # + # btcusd + # + # or + # + # [ + # 'BTCUSD', # symbol + # 2, # tick precision(priceTickDecimalPlaces) + # 8, # amount precision(quantityTickDecimalPlaces) + # '0.00001', # quantityMinimum + # 10, # quantityRoundDecimalPlaces + # True # minimumsAreInclusive + # ], + # + # or + # + # { + # "symbol": "BTCUSD", # perpetuals have 'PERP' suffix, i.e. DOGEUSDPERP + # "base_currency": "BTC", + # "quote_currency": "USD", + # "tick_size": 1E-8, + # "quote_increment": 0.01, + # "min_order_size": "0.00001", + # "status": "open", + # "wrap_enabled": False + # "product_type": "swap", # only in perps + # "contract_type": "linear", # only in perps + # "contract_price_currency": "GUSD" + # } + # + marketId = None + baseId = None + quoteId = None + settleId = None + tickSize = None + amountPrecision = None + minSize = None + status = None + swap = False + contractSize = None + linear = None + inverse = None + isString = (isinstance(response, str)) + isArray = (isinstance(response, list)) + if not isString and not isArray: + marketId = self.safe_string_lower(response, 'symbol') + amountPrecision = self.safe_number(response, 'tick_size') # right, exchange has an imperfect naming and self turns out to be an amount-precision + tickSize = self.safe_number(response, 'quote_increment') # self is tick-size actually + minSize = self.safe_number(response, 'min_order_size') + status = self.parse_market_active(self.safe_string(response, 'status')) + baseId = self.safe_string(response, 'base_currency') + quoteId = self.safe_string(response, 'quote_currency') + settleId = self.safe_string(response, 'contract_price_currency') + else: + # if no detailed API was called, then parse either string or array + if isString: + marketId = response + else: + marketId = self.safe_string_lower(response, 0) + tickSize = self.parse_number(self.parse_precision(self.safe_string(response, 1))) # priceTickDecimalPlaces + amountPrecision = self.parse_number(self.parse_precision(self.safe_string(response, 2))) # quantityTickDecimalPlaces + minSize = self.safe_number(response, 3) # quantityMinimum + marketIdUpper = marketId.upper() + isPerp = (marketIdUpper.find('PERP') >= 0) + marketIdWithoutPerp = marketIdUpper.replace('PERP', '') + conflictingMarkets = self.safe_dict(self.options, 'conflictingMarkets', {}) + lowerCaseId = marketIdWithoutPerp.lower() + if lowerCaseId in conflictingMarkets: + conflictingMarket = conflictingMarkets[lowerCaseId] + baseId = conflictingMarket['base'] + quoteId = conflictingMarket['quote'] + if isPerp: + settleId = conflictingMarket['quote'] + else: + quoteCurrencies = self.handle_option('fetchMarketsFromAPI', 'quoteCurrencies', []) + for i in range(0, len(quoteCurrencies)): + quoteCurrency = quoteCurrencies[i] + if marketIdWithoutPerp.endswith(quoteCurrency): + quoteLength = self.parse_to_int(-1 * len(quoteCurrency)) + baseId = marketIdWithoutPerp[0:quoteLength] + quoteId = quoteCurrency + if isPerp: + settleId = quoteCurrency # always same + break + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + settle = self.safe_currency_code(settleId) + symbol = base + '/' + quote + if settleId is not None: + symbol = symbol + ':' + settle + swap = True + contractSize = tickSize # always same + linear = True # always linear + inverse = False + type = 'swap' if swap else 'spot' + return { + 'id': marketId, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': type, + 'spot': not swap, + 'margin': False, + 'swap': swap, + 'future': False, + 'option': False, + 'active': status, + 'contract': swap, + 'linear': linear, + 'inverse': inverse, + 'contractSize': contractSize, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'price': tickSize, + 'amount': amountPrecision, + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': minSize, + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + 'info': response, + } + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://docs.gemini.com/rest-api/#current-order-book + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['limit_bids'] = limit + request['limit_asks'] = limit + response = self.publicGetV1BookSymbol(self.extend(request, params)) + return self.parse_order_book(response, market['symbol'], None, 'bids', 'asks', 'price', 'amount') + + def fetch_ticker_v1(self, symbol: str, params={}): + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = self.publicGetV1PubtickerSymbol(self.extend(request, params)) + # + # { + # "bid":"9117.95", + # "ask":"9117.96", + # "volume":{ + # "BTC":"1615.46861748", + # "USD":"14727307.57545006088", + # "timestamp":1594982700000 + # }, + # "last":"9115.23" + # } + # + return self.parse_ticker(response, market) + + def fetch_ticker_v2(self, symbol: str, params={}): + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = self.publicGetV2TickerSymbol(self.extend(request, params)) + # + # { + # "symbol":"BTCUSD", + # "open":"9080.58", + # "high":"9184.53", + # "low":"9063.56", + # "close":"9116.08", + # # Hourly prices descending for past 24 hours + # "changes":["9117.33","9105.69","9106.23","9120.35","9098.57","9114.53","9113.55","9128.01","9113.63","9133.49","9133.49","9137.75","9126.73","9103.91","9119.33","9123.04","9124.44","9117.57","9114.22","9102.33","9076.67","9074.72","9074.97","9092.05"], + # "bid":"9115.86", + # "ask":"9115.87" + # } + # + return self.parse_ticker(response, market) + + def fetch_ticker_v1_and_v2(self, symbol: str, params={}): + tickerPromiseA = self.fetch_ticker_v1(symbol, params) + tickerPromiseB = self.fetch_ticker_v2(symbol, params) + tickerA, tickerB = [tickerPromiseA, tickerPromiseB] + return self.deep_extend(tickerA, { + 'open': tickerB['open'], + 'high': tickerB['high'], + 'low': tickerB['low'], + 'change': tickerB['change'], + 'percentage': tickerB['percentage'], + 'average': tickerB['average'], + 'info': tickerB['info'], + }) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://docs.gemini.com/rest-api/#ticker + https://docs.gemini.com/rest-api/#ticker-v2 + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param dict [params.fetchTickerMethod]: 'fetchTickerV2', 'fetchTickerV1' or 'fetchTickerV1AndV2' - 'fetchTickerV1' for original ccxt.gemini.fetchTicker - 'fetchTickerV1AndV2' for 2 api calls to get the result of both fetchTicker methods - default = 'fetchTickerV1' + :returns dict: a `ticker structure ` + """ + method = self.safe_value(self.options, 'fetchTickerMethod', 'fetchTickerV1') + if method == 'fetchTickerV1': + return self.fetch_ticker_v1(symbol, params) + if method == 'fetchTickerV2': + return self.fetch_ticker_v2(symbol, params) + return self.fetch_ticker_v1_and_v2(symbol, params) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # fetchTickers + # + # { + # "pair": "BATUSD", + # "price": "0.20687", + # "percentChange24h": "0.0146" + # } + # + # fetchTickerV1 + # + # { + # "bid":"9117.95", + # "ask":"9117.96", + # "volume":{ + # "BTC":"1615.46861748", + # "USD":"14727307.57545006088", + # "timestamp":1594982700000 + # }, + # "last":"9115.23" + # } + # + # fetchTickerV2 + # + # { + # "symbol":"BTCUSD", + # "open":"9080.58", + # "high":"9184.53", + # "low":"9063.56", + # "close":"9116.08", + # # Hourly prices descending for past 24 hours + # "changes":["9117.33","9105.69","9106.23","9120.35","9098.57","9114.53","9113.55","9128.01","9113.63","9133.49","9133.49","9137.75","9126.73","9103.91","9119.33","9123.04","9124.44","9117.57","9114.22","9102.33","9076.67","9074.72","9074.97","9092.05"], + # "bid":"9115.86", + # "ask":"9115.87" + # } + # + volume = self.safe_value(ticker, 'volume', {}) + timestamp = self.safe_integer(volume, 'timestamp') + symbol = None + marketId = self.safe_string_lower(ticker, 'pair') + market = self.safe_market(marketId, market) + baseId = None + quoteId = None + base = None + quote = None + if (marketId is not None) and (market is None): + idLength = len(marketId) - 0 + if idLength == 7: + baseId = marketId[0:4] + quoteId = marketId[4:7] + else: + baseId = marketId[0:3] + quoteId = marketId[3:6] + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + symbol = base + '/' + quote + if (symbol is None) and (market is not None): + symbol = market['symbol'] + baseId = self.safe_string_upper(market, 'baseId') + quoteId = self.safe_string_upper(market, 'quoteId') + price = self.safe_string(ticker, 'price') + last = self.safe_string_2(ticker, 'last', 'close', price) + percentage = self.safe_string(ticker, 'percentChange24h') + open = self.safe_string(ticker, 'open') + baseVolume = self.safe_string(volume, baseId) + quoteVolume = self.safe_string(volume, quoteId) + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': self.safe_string(ticker, 'bid'), + 'bidVolume': None, + 'ask': self.safe_string(ticker, 'ask'), + 'askVolume': None, + 'vwap': None, + 'open': open, + 'close': last, + 'last': last, + 'previousClose': None, # previous day close + 'change': None, + 'percentage': percentage, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'info': ticker, + }, market) + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://docs.gemini.com/rest-api/#price-feed + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + response = self.publicGetV1Pricefeed(params) + # + # [ + # { + # "pair": "BATUSD", + # "price": "0.20687", + # "percentChange24h": "0.0146" + # }, + # { + # "pair": "LINKETH", + # "price": "0.018", + # "percentChange24h": "0.0000" + # }, + # ] + # + result = self.parse_tickers(response, symbols) + brokenPairs = self.safe_list(self.options, 'brokenPairs', []) + return self.remove_keys_from_dict(result, brokenPairs) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # public fetchTrades + # + # { + # "timestamp":1601617445, + # "timestampms":1601617445144, + # "tid":14122489752, + # "price":"0.46476", + # "amount":"28.407209", + # "exchange":"gemini", + # "type":"buy" + # } + # + # private fetchTrades + # + # { + # "price":"3900.00", + # "amount":"0.00996", + # "timestamp":1638891173, + # "timestampms":1638891173518, + # "type":"Sell", + # "aggressor":false, + # "fee_currency":"EUR", + # "fee_amount":"0.00", + # "tid":73621746145, + # "order_id":"73621746059", + # "exchange":"gemini", + # "is_auction_fill":false, + # "is_clearing_fill":false, + # "symbol":"ETHEUR", + # "client_order_id":"1638891171610" + # } + # + timestamp = self.safe_integer(trade, 'timestampms') + id = self.safe_string(trade, 'tid') + orderId = self.safe_string(trade, 'order_id') + feeCurrencyId = self.safe_string(trade, 'fee_currency') + feeCurrencyCode = self.safe_currency_code(feeCurrencyId) + fee = { + 'cost': self.safe_string(trade, 'fee_amount'), + 'currency': feeCurrencyCode, + } + priceString = self.safe_string(trade, 'price') + amountString = self.safe_string(trade, 'amount') + side = self.safe_string_lower(trade, 'type') + symbol = self.safe_symbol(None, market) + return self.safe_trade({ + 'id': id, + 'order': orderId, + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'type': None, + 'side': side, + 'takerOrMaker': None, + 'price': priceString, + 'cost': None, + 'amount': amountString, + 'fee': fee, + }, market) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://docs.gemini.com/rest-api/#trade-history + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['limit_trades'] = min(limit, 500) + if since is not None: + request['timestamp'] = since + response = self.publicGetV1TradesSymbol(self.extend(request, params)) + # + # [ + # { + # "timestamp":1601617445, + # "timestampms":1601617445144, + # "tid":14122489752, + # "price":"0.46476", + # "amount":"28.407209", + # "exchange":"gemini", + # "type":"buy" + # }, + # ] + # + return self.parse_trades(response, market, since, limit) + + def parse_balance(self, response) -> Balances: + result: dict = {'info': response} + for i in range(0, len(response)): + balance = response[i] + currencyId = self.safe_string(balance, 'currency') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(balance, 'available') + account['total'] = self.safe_string(balance, 'amount') + result[code] = account + return self.safe_balance(result) + + def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://docs.gemini.com/rest-api/#get-notional-volume + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + self.load_markets() + response = self.privatePostV1Notionalvolume(params) + # + # { + # "web_maker_fee_bps": 25, + # "web_taker_fee_bps": 35, + # "web_auction_fee_bps": 25, + # "api_maker_fee_bps": 10, + # "api_taker_fee_bps": 35, + # "api_auction_fee_bps": 20, + # "fix_maker_fee_bps": 10, + # "fix_taker_fee_bps": 35, + # "fix_auction_fee_bps": 20, + # "block_maker_fee_bps": 0, + # "block_taker_fee_bps": 50, + # "notional_30d_volume": 150.00, + # "last_updated_ms": 1551371446000, + # "date": "2019-02-28", + # "notional_1d_volume": [ + # { + # "date": "2019-02-22", + # "notional_volume": 75.00 + # }, + # { + # "date": "2019-02-14", + # "notional_volume": 75.00 + # } + # ] + # } + # + makerBps = self.safe_string(response, 'api_maker_fee_bps') + takerBps = self.safe_string(response, 'api_taker_fee_bps') + makerString = Precise.string_div(makerBps, '10000') + takerString = Precise.string_div(takerBps, '10000') + maker = self.parse_number(makerString) + taker = self.parse_number(takerString) + result: dict = {} + for i in range(0, len(self.symbols)): + symbol = self.symbols[i] + result[symbol] = { + 'info': response, + 'symbol': symbol, + 'maker': maker, + 'taker': taker, + 'percentage': True, + 'tierBased': True, + } + return result + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://docs.gemini.com/rest-api/#get-available-balances + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + response = self.privatePostV1Balances(params) + return self.parse_balance(response) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder(private) + # + # { + # "order_id":"106027397702", + # "id":"106027397702", + # "symbol":"etheur", + # "exchange":"gemini", + # "avg_execution_price":"2877.48", + # "side":"sell", + # "type":"exchange limit", + # "timestamp":"1650398122", + # "timestampms":1650398122308, + # "is_live":false, + # "is_cancelled":false, + # "is_hidden":false, + # "was_forced":false, + # "executed_amount":"0.014434", + # "client_order_id":"1650398121695", + # "options":[], + # "price":"2800.00", + # "original_amount":"0.014434", + # "remaining_amount":"0" + # } + # + # fetchOrder(private) + # + # { + # "order_id":"106028543717", + # "id":"106028543717", + # "symbol":"etheur", + # "exchange":"gemini", + # "avg_execution_price":"0.00", + # "side":"buy", + # "type":"exchange limit", + # "timestamp":"1650398446", + # "timestampms":1650398446375, + # "is_live":true, + # "is_cancelled":false, + # "is_hidden":false, + # "was_forced":false, + # "executed_amount":"0", + # "client_order_id":"1650398445709", + # "options":[], + # "price":"2000.00", + # "original_amount":"0.01", + # "remaining_amount":"0.01" + # } + # + # fetchOpenOrders(private) + # + # { + # "order_id":"106028543717", + # "id":"106028543717", + # "symbol":"etheur", + # "exchange":"gemini", + # "avg_execution_price":"0.00", + # "side":"buy", + # "type":"exchange limit", + # "timestamp":"1650398446", + # "timestampms":1650398446375, + # "is_live":true, + # "is_cancelled":false, + # "is_hidden":false, + # "was_forced":false, + # "executed_amount":"0", + # "client_order_id":"1650398445709", + # "options":[], + # "price":"2000.00", + # "original_amount":"0.01", + # "remaining_amount":"0.01" + # } + # + # cancelOrder(private) + # + # { + # "order_id":"106028543717", + # "id":"106028543717", + # "symbol":"etheur", + # "exchange":"gemini", + # "avg_execution_price":"0.00", + # "side":"buy", + # "type":"exchange limit", + # "timestamp":"1650398446", + # "timestampms":1650398446375, + # "is_live":false, + # "is_cancelled":true, + # "is_hidden":false, + # "was_forced":false, + # "executed_amount":"0", + # "client_order_id":"1650398445709", + # "reason":"Requested", + # "options":[], + # "price":"2000.00", + # "original_amount":"0.01", + # "remaining_amount":"0.01" + # } + # + timestamp = self.safe_integer(order, 'timestampms') + amount = self.safe_string(order, 'original_amount') + remaining = self.safe_string(order, 'remaining_amount') + filled = self.safe_string(order, 'executed_amount') + status = 'closed' + if order['is_live']: + status = 'open' + if order['is_cancelled']: + status = 'canceled' + price = self.safe_string(order, 'price') + average = self.safe_string(order, 'avg_execution_price') + type = self.safe_string(order, 'type') + if type == 'exchange limit': + type = 'limit' + elif type == 'market buy' or type == 'market sell': + type = 'market' + else: + type = order['type'] + fee = None + marketId = self.safe_string(order, 'symbol') + symbol = self.safe_symbol(marketId, market) + id = self.safe_string(order, 'order_id') + side = self.safe_string_lower(order, 'side') + clientOrderId = self.safe_string(order, 'client_order_id') + optionsArray = self.safe_value(order, 'options', []) + option = self.safe_string(optionsArray, 0) + timeInForce = 'GTC' + postOnly = False + if option is not None: + if option == 'immediate-or-cancel': + timeInForce = 'IOC' + elif option == 'fill-or-kill': + timeInForce = 'FOK' + elif option == 'maker-or-cancel': + timeInForce = 'PO' + postOnly = True + return self.safe_order({ + 'id': id, + 'clientOrderId': clientOrderId, + 'info': order, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'status': status, + 'symbol': symbol, + 'type': type, + 'timeInForce': timeInForce, # default set to GTC + 'postOnly': postOnly, + 'side': side, + 'price': price, + 'triggerPrice': None, + 'average': average, + 'cost': None, + 'amount': amount, + 'filled': filled, + 'remaining': remaining, + 'fee': fee, + 'trades': None, + }, market) + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://docs.gemini.com/rest-api/#order-status + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + request: dict = { + 'order_id': id, + } + response = self.privatePostV1OrderStatus(self.extend(request, params)) + # + # { + # "order_id":"106028543717", + # "id":"106028543717", + # "symbol":"etheur", + # "exchange":"gemini", + # "avg_execution_price":"0.00", + # "side":"buy", + # "type":"exchange limit", + # "timestamp":"1650398446", + # "timestampms":1650398446375, + # "is_live":true, + # "is_cancelled":false, + # "is_hidden":false, + # "was_forced":false, + # "executed_amount":"0", + # "client_order_id":"1650398445701", + # "options":[], + # "price":"2000.00", + # "original_amount":"0.01", + # "remaining_amount":"0.01" + # } + # + return self.parse_order(response) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://docs.gemini.com/rest-api/#get-active-orders + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + response = self.privatePostV1Orders(params) + # + # [ + # { + # "order_id":"106028543717", + # "id":"106028543717", + # "symbol":"etheur", + # "exchange":"gemini", + # "avg_execution_price":"0.00", + # "side":"buy", + # "type":"exchange limit", + # "timestamp":"1650398446", + # "timestampms":1650398446375, + # "is_live":true, + # "is_cancelled":false, + # "is_hidden":false, + # "was_forced":false, + # "executed_amount":"0", + # "client_order_id":"1650398445709", + # "options":[], + # "price":"2000.00", + # "original_amount":"0.01", + # "remaining_amount":"0.01" + # } + # ] + # + market = None + if symbol is not None: + market = self.market(symbol) # throws on non-existent symbol + return self.parse_orders(response, market, since, limit) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://docs.gemini.com/rest-api/#new-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: must be 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + if type != 'limit': + raise ExchangeError(self.id + ' createOrder() allows limit orders only') + clientOrderId = self.safe_string_2(params, 'clientOrderId', 'client_order_id') + params = self.omit(params, ['clientOrderId', 'client_order_id']) + if clientOrderId is None: + clientOrderId = str(self.milliseconds()) + market = self.market(symbol) + amountString = self.amount_to_precision(symbol, amount) + priceString = self.price_to_precision(symbol, price) + request: dict = { + 'client_order_id': clientOrderId, + 'symbol': market['id'], + 'amount': amountString, + 'price': priceString, + 'side': side, + 'type': 'exchange limit', # gemini allows limit orders only + # 'options': [], one of: maker-or-cancel, immediate-or-cancel, fill-or-kill, auction-only, indication-of-interest + } + type = self.safe_string(params, 'type', type) + params = self.omit(params, 'type') + triggerPrice = self.safe_string_n(params, ['triggerPrice', 'stop_price', 'stopPrice']) + params = self.omit(params, ['triggerPrice', 'stop_price', 'stopPrice', 'type']) + if type == 'stopLimit': + raise ArgumentsRequired(self.id + ' createOrder() requires a triggerPrice parameter or a stop_price parameter for ' + type + ' orders') + if triggerPrice is not None: + request['stop_price'] = self.price_to_precision(symbol, triggerPrice) + request['type'] = 'exchange stop limit' + else: + # No options can be applied to stop-limit orders at self time. + timeInForce = self.safe_string(params, 'timeInForce') + params = self.omit(params, 'timeInForce') + if timeInForce is not None: + if (timeInForce == 'IOC') or (timeInForce == 'immediate-or-cancel'): + request['options'] = ['immediate-or-cancel'] + elif (timeInForce == 'FOK') or (timeInForce == 'fill-or-kill'): + request['options'] = ['fill-or-kill'] + elif timeInForce == 'PO': + request['options'] = ['maker-or-cancel'] + postOnly = self.safe_bool(params, 'postOnly', False) + params = self.omit(params, 'postOnly') + if postOnly: + request['options'] = ['maker-or-cancel'] + # allowing override for auction-only and indication-of-interest order options + options = self.safe_string(params, 'options') + if options is not None: + request['options'] = [options] + response = self.privatePostV1OrderNew(self.extend(request, params)) + # + # { + # "order_id":"106027397702", + # "id":"106027397702", + # "symbol":"etheur", + # "exchange":"gemini", + # "avg_execution_price":"2877.48", + # "side":"sell", + # "type":"exchange limit", + # "timestamp":"1650398122", + # "timestampms":1650398122308, + # "is_live":false, + # "is_cancelled":false, + # "is_hidden":false, + # "was_forced":false, + # "executed_amount":"0.014434", + # "client_order_id":"1650398121695", + # "options":[], + # "price":"2800.00", + # "original_amount":"0.014434", + # "remaining_amount":"0" + # } + # + return self.parse_order(response) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://docs.gemini.com/rest-api/#cancel-order + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + request: dict = { + 'order_id': id, + } + response = self.privatePostV1OrderCancel(self.extend(request, params)) + # + # { + # "order_id":"106028543717", + # "id":"106028543717", + # "symbol":"etheur", + # "exchange":"gemini", + # "avg_execution_price":"0.00", + # "side":"buy", + # "type":"exchange limit", + # "timestamp":"1650398446", + # "timestampms":1650398446375, + # "is_live":false, + # "is_cancelled":true, + # "is_hidden":false, + # "was_forced":false, + # "executed_amount":"0", + # "client_order_id":"1650398445709", + # "reason":"Requested", + # "options":[], + # "price":"2000.00", + # "original_amount":"0.01", + # "remaining_amount":"0.01" + # } + # + return self.parse_order(response) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://docs.gemini.com/rest-api/#get-past-trades + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMyTrades() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['limit_trades'] = limit + if since is not None: + request['timestamp'] = self.parse_to_int(since / 1000) + response = self.privatePostV1Mytrades(self.extend(request, params)) + return self.parse_trades(response, market, since, limit) + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://docs.gemini.com/rest-api/#withdraw-crypto-funds + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.check_address(address) + self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + 'amount': amount, + 'address': address, + } + response = self.privatePostV1WithdrawCurrency(self.extend(request, params)) + # + # for BTC + # { + # "address":"mi98Z9brJ3TgaKsmvXatuRahbFRUFKRUdR", + # "amount":"1", + # "withdrawalId":"02176a83-a6b1-4202-9b85-1c1c92dd25c4", + # "message":"You have requested a transfer of 1 BTC to mi98Z9brJ3TgaKsmvXatuRahbFRUFKRUdR. This withdrawal will be sent to the blockchain within the next 60 seconds." + # } + # + # for ETH + # { + # "address":"0xA63123350Acc8F5ee1b1fBd1A6717135e82dBd28", + # "amount":"2.34567", + # "txHash":"0x28267179f92926d85c5516bqc063b2631935573d8915258e95d9572eedcc8cc" + # } + # + # for error(other variations of error messages are also expected) + # { + # "result":"error", + # "reason":"CryptoAddressWhitelistsNotEnabled", + # "message":"Cryptocurrency withdrawal address whitelists are not enabled for account 24. Please contact support@gemini.com for information on setting up a withdrawal address whitelist." + # } + # + result = self.safe_string(response, 'result') + if result == 'error': + raise ExchangeError(self.id + ' withdraw() failed: ' + self.json(response)) + return self.parse_transaction(response, currency) + + def nonce(self): + nonceMethod = self.safe_string(self.options, 'nonce', 'milliseconds') + if nonceMethod == 'milliseconds': + return self.milliseconds() + return self.seconds() + + def fetch_deposits_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch history of deposits and withdrawals + + https://docs.gemini.com/rest-api/#transfers + + :param str [code]: unified currency code for the currency of the deposit/withdrawals, default is None + :param int [since]: timestamp in ms of the earliest deposit/withdrawal, default is None + :param int [limit]: max number of deposit/withdrawals to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `transaction structure ` + """ + self.load_markets() + request: dict = {} + if limit is not None: + request['limit_transfers'] = limit + if since is not None: + request['timestamp'] = since + response = self.privatePostV1Transfers(self.extend(request, params)) + return self.parse_transactions(response) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # withdraw + # + # for BTC + # { + # "address":"mi98Z9brJ3TgaKsmvXatuRahbFRUFKRUdR", + # "amount":"1", + # "withdrawalId":"02176a83-a6b1-4202-9b85-1c1c92dd25c4", + # "message":"You have requested a transfer of 1 BTC to mi98Z9brJ3TgaKsmvXatuRahbFRUFKRUdR. This withdrawal will be sent to the blockchain within the next 60 seconds." + # } + # + # for ETH + # { + # "address":"0xA63123350Acc8F5ee1b1fBd1A6717135e82dBd28", + # "amount":"2.34567", + # "txHash":"0x28267179f92926d85c5516bqc063b2631935573d8915258e95d9572eedcc8cc" + # } + # + timestamp = self.safe_integer(transaction, 'timestampms') + currencyId = self.safe_string(transaction, 'currency') + code = self.safe_currency_code(currencyId, currency) + address = self.safe_string(transaction, 'destination') + type = self.safe_string_lower(transaction, 'type') + # if status field is available, then it's complete + statusRaw = self.safe_string(transaction, 'status') + fee = None + feeAmount = self.safe_number(transaction, 'feeAmount') + if feeAmount is not None: + fee = { + 'cost': feeAmount, + 'currency': code, + } + return { + 'info': transaction, + 'id': self.safe_string_2(transaction, 'eid', 'withdrawalId'), + 'txid': self.safe_string(transaction, 'txHash'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': None, + 'address': address, + 'addressTo': None, + 'addressFrom': None, + 'tag': None, # or is it defined? + 'tagTo': None, + 'tagFrom': None, + 'type': type, # direction of the transaction,('deposit' | 'withdraw') + 'amount': self.safe_number(transaction, 'amount'), + 'currency': code, + 'status': self.parse_transaction_status(statusRaw), + 'updated': None, + 'internal': None, + 'comment': self.safe_string(transaction, 'message'), + 'fee': fee, + } + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'Advanced': 'ok', + 'Complete': 'ok', + } + return self.safe_string(statuses, status, status) + + def parse_deposit_address(self, depositAddress, currency: Currency = None): + # + # { + # "address": "0xed6494Fe7c1E56d1bd6136e89268C51E32d9708B", + # "timestamp": "1636813923098", + # "addressVersion": "eV1" } + # } + # + address = self.safe_string(depositAddress, 'address') + code = self.safe_currency_code(None, currency) + return { + 'currency': code, + 'network': None, + 'address': address, + 'tag': None, + 'info': depositAddress, + } + + def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + + https://docs.gemini.com/rest-api/#get-deposit-addresses + + fetch the deposit address for a currency associated with self account + :param str code: unified currency code + :param dict [params]: extra parameters specific to the endpoint + :param str [params.network]: *required* The chain of currency + :returns dict: an `address structure ` + """ + self.load_markets() + groupedByNetwork = self.fetch_deposit_addresses_by_network(code, params) + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + networkGroup = self.index_by(self.safe_value(groupedByNetwork, networkCode), 'currency') + return self.safe_value(networkGroup, code) + + def fetch_deposit_addresses_by_network(self, code: str, params={}) -> List[DepositAddress]: + """ + fetch a dictionary of addresses for a currency, indexed by network + + https://docs.gemini.com/rest-api/#get-deposit-addresses + + :param str code: unified currency code of the currency for the deposit address + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.network]: *required* The chain of currency + :returns dict: a dictionary of `address structures ` indexed by the network + """ + self.load_markets() + currency = self.currency(code) + code = currency['code'] + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + if networkCode is None: + raise ArgumentsRequired(self.id + ' fetchDepositAddresses() requires a network parameter') + networkId = self.network_code_to_id(networkCode) + request: dict = { + 'network': networkId, + } + response = self.privatePostV1AddressesNetwork(self.extend(request, params)) + results = self.parse_deposit_addresses(response, [code], False, {'network': networkCode, 'currency': code}) + return self.group_by(results, 'network') + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = '/' + self.implode_params(path, params) + query = self.omit(params, self.extract_params(path)) + if api == 'private': + self.check_required_credentials() + apiKey = self.apiKey + if apiKey.find('account') < 0: + raise AuthenticationError(self.id + ' sign() requires an account-key, master-keys are not-supported') + nonce = str(self.nonce()) + request = self.extend({ + 'request': url, + 'nonce': nonce, + }, query) + payload = self.json(request) + payload = self.string_to_base64(payload) + signature = self.hmac(self.encode(payload), self.encode(self.secret), hashlib.sha384) + headers = { + 'Content-Type': 'text/plain', + 'X-GEMINI-APIKEY': self.apiKey, + 'X-GEMINI-PAYLOAD': payload, + 'X-GEMINI-SIGNATURE': signature, + } + else: + if query: + url += '?' + self.urlencode(query) + url = self.urls['api'][api] + url + if (method == 'POST') or (method == 'DELETE'): + body = self.json(query) + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + if isinstance(body, str): + feedback = self.id + ' ' + body + self.throw_broadly_matched_exception(self.exceptions['broad'], body, feedback) + return None # fallback to default error handler + # + # { + # "result": "error", + # "reason": "BadNonce", + # "message": "Out-of-sequence nonce <1234> precedes previously used nonce <2345>" + # } + # + result = self.safe_string(response, 'result') + if result == 'error': + reasonInner = self.safe_string(response, 'reason') + message = self.safe_string(response, 'message') + feedback = self.id + ' ' + message + self.throw_exactly_matched_exception(self.exceptions['exact'], reasonInner, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + raise ExchangeError(feedback) # unknown message + return None + + def create_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + create a currency deposit address + + https://docs.gemini.com/rest-api/#new-deposit-address + + :param str code: unified currency code of the currency for the deposit address + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + } + response = self.privatePostV1DepositCurrencyNewAddress(self.extend(request, params)) + address = self.safe_string(response, 'address') + self.check_address(address) + return { + 'currency': code, + 'address': address, + 'tag': None, + 'network': None, + 'info': response, + } + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://docs.gemini.com/rest-api/#candles + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + market = self.market(symbol) + timeframeId = self.safe_string(self.timeframes, timeframe, timeframe) + request: dict = { + 'timeframe': timeframeId, + 'symbol': market['id'], + } + response = self.publicGetV2CandlesSymbolTimeframe(self.extend(request, params)) + # + # [ + # [1591515000000,0.02509,0.02509,0.02509,0.02509,0], + # [1591514700000,0.02503,0.02509,0.02503,0.02509,44.6405], + # [1591514400000,0.02503,0.02503,0.02503,0.02503,0], + # ] + # + return self.parse_ohlcvs(response, market, timeframe, since, limit) diff --git a/ccxt/hashkey.py b/ccxt/hashkey.py new file mode 100644 index 0000000..3cebb74 --- /dev/null +++ b/ccxt/hashkey.py @@ -0,0 +1,4193 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.hashkey import ImplicitAPI +import hashlib +from ccxt.base.types import Account, Any, Balances, Bool, Currencies, Currency, DepositAddress, Int, LastPrice, LastPrices, LedgerEntry, Leverage, LeverageTier, LeverageTiers, Market, Num, Order, OrderBook, OrderRequest, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, FundingRates, Trade, TradingFeeInterface, TradingFees, Transaction, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import AccountNotEnabled +from ccxt.base.errors import AccountSuspended +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import OperationRejected +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidAddress +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import OrderImmediatelyFillable +from ccxt.base.errors import OrderNotFillable +from ccxt.base.errors import DuplicateOrderId +from ccxt.base.errors import ContractUnavailable +from ccxt.base.errors import NotSupported +from ccxt.base.errors import OperationFailed +from ccxt.base.errors import DDoSProtection +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.errors import InvalidNonce +from ccxt.base.errors import RequestTimeout +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class hashkey(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(hashkey, self).describe(), { + 'id': 'hashkey', + 'name': 'HashKey Global', + 'countries': ['BM'], # Bermuda + 'rateLimit': 100, + 'version': 'v1', + 'certified': True, + 'pro': True, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelAllOrders': True, + 'cancelAllOrdersAfter': False, + 'cancelOrder': True, + 'cancelOrders': True, + 'cancelWithdraw': False, + 'closeAllPositions': False, + 'closePosition': False, + 'createConvertTrade': False, + 'createDepositAddress': False, + 'createMarketBuyOrderWithCost': True, + 'createMarketOrder': True, + 'createMarketOrderWithCost': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createOrderWithTakeProfitAndStopLoss': False, + 'createReduceOnlyOrder': True, + 'createStopLimitOrder': True, + 'createStopLossOrder': False, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'createTakeProfitOrder': False, + 'createTrailingAmountOrder': False, + 'createTrailingPercentOrder': False, + 'createTriggerOrder': True, + 'fetchAccounts': True, + 'fetchAllGreeks': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchCanceledAndClosedOrders': True, + 'fetchCanceledOrders': True, + 'fetchClosedOrder': True, + 'fetchClosedOrders': False, + 'fetchConvertCurrencies': False, + 'fetchConvertQuote': False, + 'fetchConvertTrade': False, + 'fetchConvertTradeHistory': False, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchDepositsWithdrawals': False, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': True, + 'fetchFundingRateHistory': True, + 'fetchFundingRates': True, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLedger': True, + 'fetchLeverage': True, + 'fetchLeverages': False, + 'fetchLeverageTiers': True, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': 'emulated', + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrice': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrder': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': False, + 'fetchOrderTrades': False, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': True, + 'fetchPositionsForSymbol': True, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchStatus': True, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': True, # emulated for spot markets + 'fetchTradingFees': True, # for spot markets only + 'fetchTransactions': False, + 'fetchTransfers': False, + 'fetchUnderlyingAssets': False, + 'fetchVolatilityHistory': False, + 'fetchWithdrawals': True, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'sandbox': False, + 'setLeverage': True, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'transfer': True, + 'withdraw': True, + }, + 'timeframes': { + '1m': '1m', + '3m': '3m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1h', + '2h': '2h', + '4h': '4h', + '6h': '6h', + '8h': '8h', + '12h': '12h', + '1d': '1d', + '1w': '1w', + '1M': '1M', + }, + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/6dd6127b-cc19-4a13-9b29-a98d81f80e98', + 'api': { + 'public': 'https://api-glb.hashkey.com', + 'private': 'https://api-glb.hashkey.com', + }, + 'test': { + 'public': 'https://api-glb.sim.hashkeydev.com', + 'private': 'https://api-glb.sim.hashkeydev.com', + }, + 'www': 'https://global.hashkey.com/', + 'doc': 'https://hashkeyglobal-apidoc.readme.io/', + 'fees': 'https://support.global.hashkey.com/hc/en-us/articles/13199900083612-HashKey-Global-Fee-Structure', + 'referral': 'https://global.hashkey.com/en-US/register/invite?invite_code=82FQUN', + }, + 'api': { + 'public': { + 'get': { + 'api/v1/exchangeInfo': 5, + 'quote/v1/depth': 1, + 'quote/v1/trades': 1, + 'quote/v1/klines': 1, + 'quote/v1/ticker/24hr': 1, + 'quote/v1/ticker/price': 1, + 'quote/v1/ticker/bookTicker': 1, # not unified + 'quote/v1/depth/merged': 1, + 'quote/v1/markPrice': 1, + 'quote/v1/index': 1, + 'api/v1/futures/fundingRate': 1, + 'api/v1/futures/historyFundingRate': 1, + 'api/v1/ping': 1, + 'api/v1/time': 1, + }, + }, + 'private': { + 'get': { + 'api/v1/spot/order': 1, + 'api/v1/spot/openOrders': 1, + 'api/v1/spot/tradeOrders': 5, + 'api/v1/futures/leverage': 1, + 'api/v1/futures/order': 1, + 'api/v1/futures/openOrders': 1, + 'api/v1/futures/userTrades': 1, + 'api/v1/futures/positions': 1, + 'api/v1/futures/historyOrders': 1, + 'api/v1/futures/balance': 1, + 'api/v1/futures/liquidationAssignStatus': 1, + 'api/v1/futures/riskLimit': 1, + 'api/v1/futures/commissionRate': 1, + 'api/v1/futures/getBestOrder': 1, + 'api/v1/account/vipInfo': 1, + 'api/v1/account': 1, + 'api/v1/account/trades': 5, + 'api/v1/account/type': 5, + 'api/v1/account/checkApiKey': 1, + 'api/v1/account/balanceFlow': 5, + 'api/v1/spot/subAccount/openOrders': 1, + 'api/v1/spot/subAccount/tradeOrders': 1, + 'api/v1/subAccount/trades': 1, + 'api/v1/futures/subAccount/openOrders': 1, + 'api/v1/futures/subAccount/historyOrders': 1, + 'api/v1/futures/subAccount/userTrades': 1, + 'api/v1/account/deposit/address': 1, + 'api/v1/account/depositOrders': 1, + 'api/v1/account/withdrawOrders': 1, + }, + 'post': { + 'api/v1/userDataStream': 1, + 'api/v1/spot/orderTest': 1, + 'api/v1/spot/order': 1, + 'api/v1.1/spot/order': 1, + 'api/v1/spot/batchOrders': 5, + 'api/v1/futures/leverage': 1, + 'api/v1/futures/order': 1, + 'api/v1/futures/position/trading-stop': 3, + 'api/v1/futures/batchOrders': 5, + 'api/v1/account/assetTransfer': 1, + 'api/v1/account/authAddress': 1, + 'api/v1/account/withdraw': 1, + }, + 'put': { + 'api/v1/userDataStream': 1, + }, + 'delete': { + 'api/v1/spot/order': 1, + 'api/v1/spot/openOrders': 5, + 'api/v1/spot/cancelOrderByIds': 5, + 'api/v1/futures/order': 1, + 'api/v1/futures/batchOrders': 1, + 'api/v1/futures/cancelOrderByIds': 1, + 'api/v1/userDataStream': 1, + }, + }, + }, + 'fees': { + 'trading': { + 'spot': { + 'tierBased': True, + 'percentage': True, + 'feeSide': 'get', + 'maker': self.parse_number('0.0012'), + 'taker': self.parse_number('0.0012'), + 'tiers': { + 'maker': [ + [self.parse_number('0'), self.parse_number('0.0012')], + [self.parse_number('1000000'), self.parse_number('0.00080')], + [self.parse_number('5000000'), self.parse_number('0.00070')], + [self.parse_number('10000000'), self.parse_number('0.00060')], + [self.parse_number('50000000'), self.parse_number('0.00040')], + [self.parse_number('200000000'), self.parse_number('0.00030')], + [self.parse_number('400000000'), self.parse_number('0.00010')], + [self.parse_number('800000000'), self.parse_number('0.00')], + ], + 'taker': [ + [self.parse_number('0'), self.parse_number('0.0012')], + [self.parse_number('1000000'), self.parse_number('0.00090')], + [self.parse_number('5000000'), self.parse_number('0.00085')], + [self.parse_number('10000000'), self.parse_number('0.00075')], + [self.parse_number('50000000'), self.parse_number('0.00065')], + [self.parse_number('200000000'), self.parse_number('0.00045')], + [self.parse_number('400000000'), self.parse_number('0.00040')], + [self.parse_number('800000000'), self.parse_number('0.00035')], + ], + }, + }, + 'swap': { + 'tierBased': True, + 'percentage': True, + 'feeSide': 'get', + 'maker': self.parse_number('0.00025'), + 'taker': self.parse_number('0.00060'), + 'tiers': { + 'maker': [ + [self.parse_number('0'), self.parse_number('0.00025')], + [self.parse_number('1000000'), self.parse_number('0.00016')], + [self.parse_number('5000000'), self.parse_number('0.00014')], + [self.parse_number('10000000'), self.parse_number('0.00012')], + [self.parse_number('50000000'), self.parse_number('0.000080')], + [self.parse_number('200000000'), self.parse_number('0.000060')], + [self.parse_number('400000000'), self.parse_number('0.000020')], + [self.parse_number('800000000'), self.parse_number('0.00')], + ], + 'taker': [ + [self.parse_number('0'), self.parse_number('0.00060')], + [self.parse_number('1000000'), self.parse_number('0.00050')], + [self.parse_number('5000000'), self.parse_number('0.00045')], + [self.parse_number('10000000'), self.parse_number('0.00040')], + [self.parse_number('50000000'), self.parse_number('0.00035')], + [self.parse_number('200000000'), self.parse_number('0.00030')], + [self.parse_number('400000000'), self.parse_number('0.00025')], + [self.parse_number('800000000'), self.parse_number('0.00020')], + ], + }, + }, + }, + }, + 'options': { + 'broker': '10000700011', + 'recvWindow': None, + 'sandboxMode': False, + 'networks': { + 'BTC': 'BTC', + 'ERC20': 'ETH', + 'AVAX': 'AvalancheC', + 'SOL': 'Solana', + 'MATIC': 'Polygon', + 'ATOM': 'Cosmos', + 'DOT': 'Polkadot', + 'LTC': 'LTC', + 'OPTIMISM': 'Optimism', + 'ARB': 'Arbitrum', + 'DOGE': 'Dogecoin', + 'TRC20': 'Tron', + 'ZKSYNC': 'zkSync', + 'TON': 'TON', + 'KLAYTN': 'Klaytn', + 'MERLINCHAIN': 'Merlin Chain', + }, + 'networksById': { + 'BTC': 'BTC', + 'Bitcoin': 'BTC', + 'ETH': 'ERC20', + 'ERC20': 'ERC20', + 'AvalancheC': 'AVAX', + 'AVAX C-Chain': 'AVAX', + 'Solana': 'SOL', + 'Cosmos': 'ATOM', + 'Arbitrum': 'ARB', + 'Polygon': 'MATIC', + 'Optimism': 'OPTIMISM', + 'Polkadot': 'DOT', + 'LTC': 'LTC', + 'Litecoin': 'LTC', + 'Dogecoin': 'DOGE', + 'Merlin Chain': 'MERLINCHAIN', + 'zkSync': 'ZKSYNC', + 'TRC20': 'TRC20', + 'Tron': 'TRC20', + 'TON': 'TON', + 'BSC(BEP20)': 'BSC', + 'Klaytn': 'KLAYTN', + }, + 'defaultNetwork': 'ERC20', + }, + 'features': { + 'default': { + 'sandbox': True, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': False, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': True, # todo fix + 'selfTradePrevention': True, # todo implement + 'iceberg': False, + }, + 'createOrders': { + 'max': 20, + }, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 30, + 'untilDays': 30, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 1000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': None, # todo + 'fetchOHLCV': { + 'limit': 1000, + }, + }, + 'spot': { + 'extends': 'default', + }, + 'forDerivatives': { + 'extends': 'default', + 'createOrder': { + 'triggerPrice': True, + 'selfTradePrevention': True, + }, + 'fetchOpenOrders': { + 'trigger': True, + 'limit': 500, + }, + }, + 'swap': { + 'linear': { + 'extends': 'forDerivatives', + }, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'commonCurrencies': {}, + 'exceptions': { + 'exact': { + '0001': BadRequest, # Required field '%s' missing or invalid. + '0002': AuthenticationError, # Incorrect signature + '0003': RateLimitExceeded, # Rate limit exceeded + '0102': AuthenticationError, # Invalid APIKey + '0103': AuthenticationError, # APIKey expired + '0104': PermissionDenied, # The accountId defined is not permissible + '0201': ExchangeError, # Instrument not found + '0202': PermissionDenied, # Invalid IP + '0206': BadRequest, # Unsupported order type + '0207': BadRequest, # Invalid price + '0209': BadRequest, # Invalid price precision + '0210': BadRequest, # Price outside of allowed range + '0211': OrderNotFound, # Order not found + '0401': InsufficientFunds, # Insufficient asset + '0402': BadRequest, # Invalid asset + '-1000': ExchangeError, # An unknown error occurred while processing the request + '-1001': ExchangeError, # Internal error + '-100010': BadSymbol, # Invalid Symbols! + '-100012': BadSymbol, # Parameter symbol [str] missing! + '-1002': AuthenticationError, # Unauthorized operation + '-1004': BadRequest, # Bad request + '-1005': PermissionDenied, # No permission + '-1006': ExchangeError, # Execution status unknown + '-1007': RequestTimeout, # Timeout waiting for response from server + '-1014': InvalidOrder, # Unsupported order combination + '-1015': InvalidOrder, # Too many new orders + '-1020': OperationRejected, # Unsupported operation + '-1021': InvalidNonce, # Timestamp for self request is outside of the recvWindow + '-1024': BadRequest, # Duplicate request + '-1101': ExchangeNotAvailable, # Feature has been offline + '-1115': InvalidOrder, # Invalid timeInForce + '-1117': InvalidOrder, # Invalid order side + '-1123': InvalidOrder, # Invalid client order id + '-1124': InvalidOrder, # Invalid price + '-1126': InvalidOrder, # Invalid quantity + '-1129': BadRequest, # Invalid parameters, quantity and amount are not allowed to be sent at the same time. + '-1130': BadRequest, # Illegal parameter '%s' + '-1132': BadRequest, # Order price greater than the maximum + '-1133': BadRequest, # Order price lower than the minimum + '-1135': BadRequest, # Order quantity greater than the maximum + '-1136': BadRequest, # Order quantity lower than the minimum + '-1138': InvalidOrder, # Order has been partially cancelled + '-1137': InvalidOrder, # Order quantity precision too large + '-1139': OrderImmediatelyFillable, # Order has been filled + '-1140': InvalidOrder, # Order amount lower than the minimum + '-1141': DuplicateOrderId, # Duplicate order + '-1142': OrderNotFillable, # Order has been cancelled + '-1143': OrderNotFound, # Order not found on order book + '-1144': OperationRejected, # Order has been locked + '-1145': NotSupported, # Cancellation on self order type not supported + '-1146': RequestTimeout, # Order creation timeout + '-1147': RequestTimeout, # Order cancellation timeout + '-1148': InvalidOrder, # Order amount precision too large + '-1149': OperationRejected, # Order creation failed + '-1150': OperationFailed, # Order cancellation failed + '-1151': OperationRejected, # The trading pair is not open yet + '-1152': AccountNotEnabled, # User does not exist + '-1153': InvalidOrder, # Invalid price type + '-1154': InvalidOrder, # Invalid position side + '-1155': OperationRejected, # The trading pair is not available for api trading + '-1156': OperationFailed, # Limit maker order creation failed + '-1157': OperationFailed, # Modify futures margin failed + '-1158': OperationFailed, # Reduce margin is forbidden + '-1159': AccountNotEnabled, # Finance account already exists + '-1160': AccountNotEnabled, # Account does not exist + '-1161': OperationFailed, # Balance transfer failed + '-1162': ContractUnavailable, # Unsupport contract address + '-1163': InvalidAddress, # Illegal withdrawal address + '-1164': OperationFailed, # Withdraw failed + '-1165': ArgumentsRequired, # Withdrawal amount cannot be null + '-1166': OperationRejected, # Withdrawal amount exceeds the daily limit + '-1167': BadRequest, # Withdrawal amount less than the minimum + '-1168': BadRequest, # Illegal withdrawal amount + '-1169': PermissionDenied, # Withdraw not allowed + '-1170': PermissionDenied, # Deposit not allowed + '-1171': PermissionDenied, # Withdrawal address not in whitelist + '-1172': BadRequest, # Invalid from account id + '-1173': BadRequest, # Invalid to account i + '-1174': PermissionDenied, # Transfer not allowed between the same account + '-1175': BadRequest, # Invalid fiat deposit status + '-1176': BadRequest, # Invalid fiat withdrawal status + '-1177': InvalidOrder, # Invalid fiat order type + '-1178': AccountNotEnabled, # Brokerage account does not exist + '-1179': AccountSuspended, # Address owner is not True + '-1181': ExchangeError, # System error + '-1193': OperationRejected, # Order creation count exceeds the limit + '-1194': OperationRejected, # Market order creation forbidden + '-1195': BadRequest, # Market order long position cannot exceed %s above the market price + '-1196': BadRequest, # Market order short position cannot be below %s of the market price + '-1200': BadRequest, # Order buy quantity too small + '-1201': BadRequest, # Order buy quantity too large + '-1202': BadRequest, # Order sell quantity too small + '-1203': BadRequest, # Order sell quantity too large + '-1204': BadRequest, # From account must be a main account + '-1205': AccountNotEnabled, # Account not authorized + '-1206': BadRequest, # Order amount greater than the maximum + '-1207': BadRequest, # The status of deposit is invalid + '-1208': BadRequest, # The orderType of fiat is invalid + '-1209': BadRequest, # The status of withdraw is invalid + '-2001': ExchangeNotAvailable, # Platform is yet to open trading + '-2002': OperationFailed, # The number of open orders exceeds the limit 300 + '-2003': OperationFailed, # Position size cannot meet target leverage + '-2004': OperationFailed, # Adjust leverage fail + '-2005': RequestTimeout, # Adjust leverage timeout + '-2010': OperationRejected, # New order rejected + '-2011': OperationRejected, # Order cancellation rejected + '-2016': OperationRejected, # API key creation exceeds the limit + '-2017': OperationRejected, # Open orders exceeds the limit of the trading pair + '-2018': OperationRejected, # Trade user creation exceeds the limit + '-2019': PermissionDenied, # Trader and omnibus user not allowed to login app + '-2020': PermissionDenied, # Not allowed to trade self trading pair + '-2021': PermissionDenied, # Not allowed to trade self trading pair + '-2022': OperationRejected, # Order batch size exceeds the limit + '-2023': AuthenticationError, # Need to pass KYC verification + '-2024': AccountNotEnabled, # Fiat account does not exist + '-2025': AccountNotEnabled, # Custody account not exist + '-2026': BadRequest, # Invalid type + '-2027': OperationRejected, # Exceed maximum time range of 30 days + '-2028': OperationRejected, # The search is limited to data within the last one month + '-2029': OperationRejected, # The search is limited to data within the last three months + '-2030': InsufficientFunds, # Insufficient margin + '-2031': NotSupported, # Leverage reduction is not supported in Isolated Margin Mode with open positions + '-2032': OperationRejected, # After the transaction, your %s position will account for %s of the total position, which poses concentration risk. Do you want to continue with the transaction? + '-2033': OperationFailed, # Order creation failed. Please verify if the order parameters comply with the trading rules + '-2034': InsufficientFunds, # Trade account holding limit is zero + '-2035': OperationRejected, # The sub account has been frozen and cannot transfer + '-2036': NotSupported, # We do not support queries for records exceeding 30 days + '-2037': ExchangeError, # Position and order data error + '-2038': InsufficientFunds, # Insufficient margin + '-2039': NotSupported, # Leverage reduction is not supported in Isolated Margin Mode with open positions + '-2040': ExchangeNotAvailable, # There is a request being processed. Please try again later + '-2041': BadRequest, # Token does not exist + '-2042': OperationRejected, # You have passed the trade limit, please pay attention to the risks + '-2043': OperationRejected, # Maximum allowed leverage reached, please lower your leverage + '-2044': BadRequest, # This order price is unreasonable to exceed(or be lower than) the liquidation price + '-2045': BadRequest, # Price too low, please order again! + '-2046': BadRequest, # Price too high, please order again! + '-2048': BadRequest, # Exceed the maximum number of conditional orders of %s + '-2049': BadRequest, # Create stop order buy price too big + '-2050': BadRequest, # Create stop order sell price too small + '-2051': OperationRejected, # Create order rejected + '-2052': OperationRejected, # Create stop profit-loss plan order reject + '-2053': OperationRejected, # Position not enough + '-2054': BadRequest, # Invalid long stop profit price + '-2055': BadRequest, # Invalid long stop loss price + '-2056': BadRequest, # Invalid short stop profit price + '-2057': BadRequest, # Invalid short stop loss price + '-3117': PermissionDenied, # Invalid permission + '-3143': PermissionDenied, # According to KYC and risk assessment, your trading account has exceeded the limit. + '-3144': PermissionDenied, # Currently, your trading account has exceeded its limit and is temporarily unable to perform transfers + '-3145': DDoSProtection, # Please DO NOT submit request too frequently + '-4001': BadRequest, # Invalid asset + '-4002': BadRequest, # Withdrawal amount less than Minimum Withdrawal Amount + '-4003': InsufficientFunds, # Insufficient Balance + '-4004': BadRequest, # Invalid bank account number + '-4005': BadRequest, # Assets are not listed + '-4006': AccountNotEnabled, # KYC is not certified + '-4007': NotSupported, # Withdrawal channels are not supported + '-4008': AccountNotEnabled, # This currency does not support self customer type + '-4009': PermissionDenied, # No withdrawal permission + '-4010': PermissionDenied, # Withdrawals on the same day exceed the maximum limit for a single day + '-4011': ExchangeError, # System error + '-4012': ExchangeError, # Parameter error + '-4013': OperationFailed, # Withdraw repeatly + }, + 'broad': {}, + }, + 'precisionMode': TICK_SIZE, + }) + + def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://hashkeyglobal-apidoc.readme.io/reference/check-server-time + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = self.publicGetApiV1Time(params) + # + # { + # "serverTime": 1721661553214 + # } + # + return self.safe_integer(response, 'serverTime') + + def fetch_status(self, params={}): + """ + the latest known information on the availability of the exchange API + + https://hashkeyglobal-apidoc.readme.io/reference/test-connectivity + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `status structure ` + """ + response = self.publicGetApiV1Ping(params) + # + # {} + # + return { + 'status': 'ok', + 'updated': None, + 'eta': None, + 'url': None, + 'info': response, + } + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for the exchange + + https://hashkeyglobal-apidoc.readme.io/reference/exchangeinfo + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.symbol]: the id of the market to fetch + :returns dict[]: an array of objects representing market data + """ + request: dict = {} + response = self.publicGetApiV1ExchangeInfo(self.extend(request, params)) + # + # { + # "timezone": "UTC", + # "serverTime": "1721661653952", + # "brokerFilters": [], + # "symbols": [ + # { + # "symbol": "BTCUSDT", + # "symbolName": "BTCUSDT", + # "status": "TRADING", + # "baseAsset": "BTC", + # "baseAssetName": "BTC", + # "baseAssetPrecision": "0.00001", + # "quoteAsset": "USDT", + # "quoteAssetName": "USDT", + # "quotePrecision": "0.0000001", + # "retailAllowed": True, + # "piAllowed": True, + # "corporateAllowed": True, + # "omnibusAllowed": True, + # "icebergAllowed": False, + # "isAggregate": False, + # "allowMargin": False, + # "filters": [ + # { + # "minPrice": "0.01", + # "maxPrice": "100000.00000000", + # "tickSize": "0.01", + # "filterType": "PRICE_FILTER" + # }, + # { + # "minQty": "0.00001", + # "maxQty": "8", + # "stepSize": "0.00001", + # "marketOrderMinQty": "0.00001", + # "marketOrderMaxQty": "4", + # "filterType": "LOT_SIZE" + # }, + # { + # "minNotional": "1", + # "filterType": "MIN_NOTIONAL" + # }, + # { + # "minAmount": "1", + # "maxAmount": "400000", + # "minBuyPrice": "0", + # "marketOrderMinAmount": "1", + # "marketOrderMaxAmount": "200000", + # "filterType": "TRADE_AMOUNT" + # }, + # { + # "maxSellPrice": "0", + # "buyPriceUpRate": "0.1", + # "sellPriceDownRate": "0.1", + # "filterType": "LIMIT_TRADING" + # }, + # { + # "buyPriceUpRate": "0.1", + # "sellPriceDownRate": "0.1", + # "filterType": "MARKET_TRADING" + # }, + # { + # "noAllowMarketStartTime": "1710485700000", + # "noAllowMarketEndTime": "1710486000000", + # "limitOrderStartTime": "0", + # "limitOrderEndTime": "0", + # "limitMinPrice": "0", + # "limitMaxPrice": "0", + # "filterType": "OPEN_QUOTE" + # } + # ] + # } + # ], + # "options": [], + # "contracts": [ + # { + # "filters": [ + # { + # "minPrice": "0.1", + # "maxPrice": "100000.00000000", + # "tickSize": "0.1", + # "filterType": "PRICE_FILTER" + # }, + # { + # "minQty": "0.001", + # "maxQty": "10", + # "stepSize": "0.001", + # "marketOrderMinQty": "0", + # "marketOrderMaxQty": "0", + # "filterType": "LOT_SIZE" + # }, + # { + # "minNotional": "0", + # "filterType": "MIN_NOTIONAL" + # }, + # { + # "maxSellPrice": "999999", + # "buyPriceUpRate": "0.05", + # "sellPriceDownRate": "0.05", + # "maxEntrustNum": 200, + # "maxConditionNum": 200, + # "filterType": "LIMIT_TRADING" + # }, + # { + # "buyPriceUpRate": "0.05", + # "sellPriceDownRate": "0.05", + # "filterType": "MARKET_TRADING" + # }, + # { + # "noAllowMarketStartTime": "0", + # "noAllowMarketEndTime": "0", + # "limitOrderStartTime": "0", + # "limitOrderEndTime": "0", + # "limitMinPrice": "0", + # "limitMaxPrice": "0", + # "filterType": "OPEN_QUOTE" + # } + # ], + # "exchangeId": "301", + # "symbol": "BTCUSDT-PERPETUAL", + # "symbolName": "BTCUSDT-PERPETUAL", + # "status": "TRADING", + # "baseAsset": "BTCUSDT-PERPETUAL", + # "baseAssetPrecision": "0.001", + # "quoteAsset": "USDT", + # "quoteAssetPrecision": "0.1", + # "icebergAllowed": False, + # "inverse": False, + # "index": "USDT", + # "marginToken": "USDT", + # "marginPrecision": "0.0001", + # "contractMultiplier": "0.001", + # "underlying": "BTC", + # "riskLimits": [ + # { + # "riskLimitId": "200000722", + # "quantity": "1000.00", + # "initialMargin": "0.10", + # "maintMargin": "0.005", + # "isWhite": False + # }, + # { + # "riskLimitId": "200000723", + # "quantity": "2000.00", + # "initialMargin": "0.10", + # "maintMargin": "0.01", + # "isWhite": False + # } + # ] + # } + # ], + # "coins": [ + # { + # "orgId": "9001", + # "coinId": "BTC", + # "coinName": "BTC", + # "coinFullName": "Bitcoin", + # "allowWithdraw": True, + # "allowDeposit": True, + # "tokenType": "CHAIN_TOKEN", + # "chainTypes": [ + # { + # "chainType": "Bitcoin", + # "withdrawFee": "0", + # "minWithdrawQuantity": "0.002", + # "maxWithdrawQuantity": "0", + # "minDepositQuantity": "0.0005", + # "allowDeposit": True, + # "allowWithdraw": True + # } + # ] + # } + # ] + # } + # + spotMarkets = self.safe_list(response, 'symbols', []) + swapMarkets = self.safe_list(response, 'contracts', []) + markets = self.array_concat(spotMarkets, swapMarkets) + if self.is_empty(markets): + markets = [response] # if user provides params.symbol the exchange returns a single object insted of list of objects + return self.parse_markets(markets) + + def parse_market(self, market: dict) -> Market: + # spot + # { + # "symbol": "BTCUSDT", + # "symbolName": "BTCUSDT", + # "status": "TRADING", + # "baseAsset": "BTC", + # "baseAssetName": "BTC", + # "baseAssetPrecision": "0.00001", + # "quoteAsset": "USDT", + # "quoteAssetName": "USDT", + # "quotePrecision": "0.0000001", + # "retailAllowed": True, + # "piAllowed": True, + # "corporateAllowed": True, + # "omnibusAllowed": True, + # "icebergAllowed": False, + # "isAggregate": False, + # "allowMargin": False, + # "filters": [ + # { + # "minPrice": "0.01", + # "maxPrice": "100000.00000000", + # "tickSize": "0.01", + # "filterType": "PRICE_FILTER" + # }, + # { + # "minQty": "0.00001", + # "maxQty": "8", + # "stepSize": "0.00001", + # "marketOrderMinQty": "0.00001", + # "marketOrderMaxQty": "4", + # "filterType": "LOT_SIZE" + # }, + # { + # "minNotional": "1", + # "filterType": "MIN_NOTIONAL" + # }, + # { + # "minAmount": "1", + # "maxAmount": "400000", + # "minBuyPrice": "0", + # "marketOrderMinAmount": "1", + # "marketOrderMaxAmount": "200000", + # "filterType": "TRADE_AMOUNT" + # }, + # { + # "maxSellPrice": "0", + # "buyPriceUpRate": "0.1", + # "sellPriceDownRate": "0.1", + # "filterType": "LIMIT_TRADING" + # }, + # { + # "buyPriceUpRate": "0.1", + # "sellPriceDownRate": "0.1", + # "filterType": "MARKET_TRADING" + # }, + # { + # "noAllowMarketStartTime": "1710485700000", + # "noAllowMarketEndTime": "1710486000000", + # "limitOrderStartTime": "0", + # "limitOrderEndTime": "0", + # "limitMinPrice": "0", + # "limitMaxPrice": "0", + # "filterType": "OPEN_QUOTE" + # } + # ] + # } + # + # swap + # { + # "filters": [ + # { + # "minPrice": "0.1", + # "maxPrice": "100000.00000000", + # "tickSize": "0.1", + # "filterType": "PRICE_FILTER" + # }, + # { + # "minQty": "0.001", + # "maxQty": "10", + # "stepSize": "0.001", + # "marketOrderMinQty": "0", + # "marketOrderMaxQty": "0", + # "filterType": "LOT_SIZE" + # }, + # { + # "minNotional": "0", + # "filterType": "MIN_NOTIONAL" + # }, + # { + # "maxSellPrice": "999999", + # "buyPriceUpRate": "0.05", + # "sellPriceDownRate": "0.05", + # "maxEntrustNum": 200, + # "maxConditionNum": 200, + # "filterType": "LIMIT_TRADING" + # }, + # { + # "buyPriceUpRate": "0.05", + # "sellPriceDownRate": "0.05", + # "filterType": "MARKET_TRADING" + # }, + # { + # "noAllowMarketStartTime": "0", + # "noAllowMarketEndTime": "0", + # "limitOrderStartTime": "0", + # "limitOrderEndTime": "0", + # "limitMinPrice": "0", + # "limitMaxPrice": "0", + # "filterType": "OPEN_QUOTE" + # } + # ], + # "exchangeId": "301", + # "symbol": "BTCUSDT-PERPETUAL", + # "symbolName": "BTCUSDT-PERPETUAL", + # "status": "TRADING", + # "baseAsset": "BTCUSDT-PERPETUAL", + # "baseAssetPrecision": "0.001", + # "quoteAsset": "USDT", + # "quoteAssetPrecision": "0.1", + # "icebergAllowed": False, + # "inverse": False, + # "index": "USDT", + # "marginToken": "USDT", + # "marginPrecision": "0.0001", + # "contractMultiplier": "0.001", + # "underlying": "BTC", + # "riskLimits": [ + # { + # "riskLimitId": "200000722", + # "quantity": "1000.00", + # "initialMargin": "0.10", + # "maintMargin": "0.005", + # "isWhite": False + # }, + # { + # "riskLimitId": "200000723", + # "quantity": "2000.00", + # "initialMargin": "0.10", + # "maintMargin": "0.01", + # "isWhite": False + # } + # ] + # } + # + marketId = self.safe_string(market, 'symbol') + quoteId = self.safe_string(market, 'quoteAsset') + quote = self.safe_currency_code(quoteId) + settleId = self.safe_string(market, 'marginToken') + settle = self.safe_currency_code(settleId) + baseId = self.safe_string(market, 'baseAsset') + marketType = 'spot' + isSpot = True + isSwap = False + suffix = '' + parts = marketId.split('-') + secondPart = self.safe_string(parts, 1) + if secondPart == 'PERPETUAL': + marketType = 'swap' + isSpot = False + isSwap = True + baseId = self.safe_string(market, 'underlying') + suffix += ':' + settleId + base = self.safe_currency_code(baseId) + symbol = base + '/' + quote + suffix + status = self.safe_string(market, 'status') + active = status == 'TRADING' + isLinear: Bool = None + subType = None + isInverse = self.safe_bool(market, 'inverse') + if isInverse is not None: + if isInverse: + isLinear = False + subType = 'inverse' + else: + isLinear = True + subType = 'linear' + filtersList = self.safe_list(market, 'filters', []) + filters = self.index_by(filtersList, 'filterType') + priceFilter = self.safe_dict(filters, 'PRICE_FILTER', {}) + amountFilter = self.safe_dict(filters, 'LOT_SIZE', {}) + costFilter = self.safe_dict(filters, 'MIN_NOTIONAL', {}) + minCostString = self.omit_zero(self.safe_string(costFilter, 'min_notional')) + contractSizeString = self.safe_string(market, 'contractMultiplier') + amountPrecisionString = self.safe_string(amountFilter, 'stepSize') + amountMinLimitString = self.safe_string(amountFilter, 'minQty') + amountMaxLimitString = self.safe_string(amountFilter, 'maxQty') + minLeverage: Int = None + maxLeverage: Int = None + if isSwap: + amountPrecisionString = Precise.string_div(amountPrecisionString, contractSizeString) + amountMinLimitString = Precise.string_div(amountMinLimitString, contractSizeString) + amountMaxLimitString = Precise.string_div(amountMaxLimitString, contractSizeString) + riskLimits = self.safe_list(market, 'riskLimits') + if riskLimits is not None: + first = self.safe_dict(riskLimits, 0) + arrayLength = len(riskLimits) + last = self.safe_dict(riskLimits, arrayLength - 1) + minInitialMargin = self.safe_string(first, 'initialMargin') + maxInitialMargin = self.safe_string(last, 'initialMargin') + if Precise.string_gt(minInitialMargin, maxInitialMargin): + minInitialMargin, maxInitialMargin = [maxInitialMargin, minInitialMargin] + minLeverage = self.parse_to_int(Precise.string_div('1', maxInitialMargin)) + maxLeverage = self.parse_to_int(Precise.string_div('1', minInitialMargin)) + tradingFees = self.safe_dict(self.fees, 'trading') + fees = self.safe_dict(tradingFees, 'spot') if isSpot else self.safe_dict(tradingFees, 'swap') + return self.safe_market_structure({ + 'id': marketId, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'baseId': baseId, + 'quoteId': quoteId, + 'active': active, + 'type': marketType, + 'subType': subType, + 'spot': isSpot, + 'margin': self.safe_bool(market, 'allowMargin'), + 'swap': isSwap, + 'future': False, + 'option': False, + 'contract': isSwap, + 'settle': settle, + 'settleId': settleId, + 'contractSize': self.parse_number(contractSizeString), + 'linear': isLinear, + 'inverse': isInverse, + 'taker': self.safe_number(fees, 'taker'), + 'maker': self.safe_number(fees, 'maker'), + 'percentage': self.safe_bool(fees, 'percentage'), + 'tierBased': self.safe_bool(fees, 'tierBased'), + 'feeSide': self.safe_string(fees, 'feeSide'), + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(amountPrecisionString), + 'price': self.safe_number(priceFilter, 'tickSize'), + }, + 'limits': { + 'amount': { + 'min': self.parse_number(amountMinLimitString), + 'max': self.parse_number(amountMaxLimitString), + }, + 'price': { + 'min': self.safe_number(priceFilter, 'minPrice'), + 'max': self.safe_number(priceFilter, 'maxPrice'), + }, + 'leverage': { + 'min': minLeverage, + 'max': maxLeverage, + }, + 'cost': { + 'min': self.parse_number(minCostString), + 'max': None, + }, + }, + 'created': None, + 'info': market, + }) + + def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://hashkeyglobal-apidoc.readme.io/reference/exchangeinfo + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = self.publicGetApiV1ExchangeInfo(params) + coins = self.safe_list(response, 'coins') + # + # { + # ... + # "coins": [ + # { + # "orgId": "9001", + # "coinId": "BTC", + # "coinName": "BTC", + # "coinFullName": "Bitcoin", + # "allowWithdraw": True, + # "allowDeposit": True, + # "tokenType": "CHAIN_TOKEN", + # "chainTypes": [ + # { + # "chainType": "Bitcoin", + # "withdrawFee": "0", + # "minWithdrawQuantity": "0.002", + # "maxWithdrawQuantity": "0", + # "minDepositQuantity": "0.0005", + # "allowDeposit": True, + # "allowWithdraw": True + # } + # ] + # } + # ] + # } + # + result: dict = {} + for i in range(0, len(coins)): + currecy = coins[i] + currencyId = self.safe_string(currecy, 'coinId') + code = self.safe_currency_code(currencyId) + networks = self.safe_list(currecy, 'chainTypes') + parsedNetworks: dict = {} + for j in range(0, len(networks)): + network = networks[j] + networkId = self.safe_string(network, 'chainType') + networkCode = self.network_code_to_id(networkId) + parsedNetworks[networkCode] = { + 'id': networkId, + 'network': networkCode, + 'limits': { + 'withdraw': { + 'min': self.safe_number(network, 'minWithdrawQuantity'), + 'max': self.parse_number(self.omit_zero(self.safe_string(network, 'maxWithdrawQuantity'))), + }, + 'deposit': { + 'min': self.safe_number(network, 'minDepositQuantity'), + 'max': None, + }, + }, + 'active': None, + 'deposit': self.safe_bool(network, 'allowDeposit'), + 'withdraw': self.safe_bool(network, 'allowWithdraw'), + 'fee': self.safe_number(network, 'withdrawFee'), + 'precision': None, + 'info': network, + } + rawType = self.safe_string(currecy, 'tokenType') + type = 'fiat' if (rawType == 'REAL_MONEY') else 'crypto' + result[code] = self.safe_currency_structure({ + 'id': currencyId, + 'code': code, + 'precision': None, + 'type': type, + 'name': self.safe_string(currecy, 'coinFullName'), + 'active': None, + 'deposit': self.safe_bool(currecy, 'allowDeposit'), + 'withdraw': self.safe_bool(currecy, 'allowWithdraw'), + 'fee': None, + 'limits': { + 'deposit': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + }, + 'networks': parsedNetworks, + 'info': currecy, + }) + return result + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://hashkeyglobal-apidoc.readme.io/reference/get-order-book + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return(maximum value is 200) + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['limit'] = limit + response = self.publicGetQuoteV1Depth(self.extend(request, params)) + # + # { + # "t": 1721681436393, + # "b": [ + # ["67902.49", "0.00112"], + # ["67901.08", "0.01014"] + # ... + # ], + # "a": [ + # ["67905.99", "0.87134"], + # ["67906", "0.57361"] + # ... + # ] + # } + # + timestamp = self.safe_integer(response, 't') + return self.parse_order_book(response, symbol, timestamp, 'b', 'a') + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://hashkeyglobal-apidoc.readme.io/reference/get-recent-trade-list + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch(maximum value is 100) + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['limit'] = limit + response = self.publicGetQuoteV1Trades(self.extend(request, params)) + # + # [ + # { + # "t": 1721682745779, + # "p": "67835.99", + # "q": "0.00017", + # "ibm": True + # }, + # ... + # ] + # + return self.parse_trades(response, market, since, limit) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://hashkeyglobal-apidoc.readme.io/reference/get-account-trade-list + https://hashkeyglobal-apidoc.readme.io/reference/query-futures-trades + https://hashkeyglobal-apidoc.readme.io/reference/get-sub-account-user + + :param str symbol: *is mandatory for swap markets* unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum amount of trades to fetch(default 200, max 500) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: 'spot' or 'swap' - the type of the market to fetch trades for(default 'spot') + :param int [params.until]: the latest time in ms to fetch trades for, only supports the last 30 days timeframe + :param str [params.fromId]: srarting trade id + :param str [params.toId]: ending trade id + :param str [params.clientOrderId]: *spot markets only* filter trades by orderId + :param str [params.accountId]: account id to fetch the orders from + :returns Trade[]: a list of `trade structures ` + """ + methodName = 'fetchMyTrades' + self.load_markets() + request: dict = {} + market: Market = None + if symbol is not None: + market = self.market(symbol) + marketType = 'spot' + marketType, params = self.handle_market_type_and_params(methodName, market, params) + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + until: Int = None + until, params = self.handle_option_and_params(params, methodName, 'until') + if until is not None: + request['endTime'] = until + accountId: Str = None + accountId, params = self.handle_option_and_params(params, methodName, 'accountId') + response = None + if marketType == 'spot': + if market is not None: + request['symbol'] = market['id'] + if accountId is not None: + request['accountId'] = accountId + response = self.privateGetApiV1AccountTrades(self.extend(request, params)) + # + # [ + # { + # "id": "1739352552862964736", + # "clientOrderId": "1722082982086472", + # "ticketId": "1739352552795029504", + # "symbol": "ETHUSDT", + # "symbolName": "ETHUSDT", + # "orderId": "1739352552762301440", + # "matchOrderId": "0", + # "price": "3289.96", + # "qty": "0.001", + # "commission": "0.0000012", + # "commissionAsset": "ETH", + # "time": "1722082982097", + # "isBuyer": True, + # "isMaker": False, + # "fee": { + # "feeCoinId": "ETH", + # "feeCoinName": "ETH", + # "fee": "0.0000012" + # }, + # "feeCoinId": "ETH", + # "feeAmount": "0.0000012", + # "makerRebate": "0" + # }, + # ... + # ] + # + elif marketType == 'swap': + if symbol is None: + raise ArgumentsRequired(self.id + ' ' + methodName + '() requires a symbol argument for swap markets') + request['symbol'] = market['id'] + if accountId is not None: + request['subAccountId'] = accountId + response = self.privateGetApiV1FuturesSubAccountUserTrades(self.extend(request, params)) + else: + response = self.privateGetApiV1FuturesUserTrades(self.extend(request, params)) + # + # [ + # { + # "time": "1722429951648", + # "tradeId": "1742263144691139328", + # "orderId": "1742263144028363776", + # "symbol": "ETHUSDT-PERPETUAL", + # "price": "3327.54", + # "quantity": "4", + # "commissionAsset": "USDT", + # "commission": "0.00798609", + # "makerRebate": "0", + # "type": "LIMIT", + # "side": "BUY_OPEN", + # "realizedPnl": "0", + # "isMarker": False + # } + # ] + # + else: + raise NotSupported(self.id + ' ' + methodName + '() is not supported for ' + marketType + ' type of markets') + return self.parse_trades(response, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades + # + # { + # "t": 1721682745779, + # "p": "67835.99", + # "q": "0.00017", + # "ibm": True + # } + # + # fetchMyTrades spot + # + # { + # "id": "1739352552862964736", + # "clientOrderId": "1722082982086472", + # "ticketId": "1739352552795029504", + # "symbol": "ETHUSDT", + # "symbolName": "ETHUSDT", + # "orderId": "1739352552762301440", + # "matchOrderId": "0", + # "price": "3289.96", + # "qty": "0.001", + # "commission": "0.0000012", + # "commissionAsset": "ETH", + # "time": "1722082982097", + # "isBuyer": True, + # "isMaker": False, + # "fee": { + # "feeCoinId": "ETH", + # "feeCoinName": "ETH", + # "fee": "0.0000012" + # }, + # "feeCoinId": "ETH", + # "feeAmount": "0.0000012", + # "makerRebate": "0" + # } + # + # fetchMyTrades swap + # { + # "time": "1722429951648", + # "tradeId": "1742263144691139328", + # "orderId": "1742263144028363776", + # "symbol": "ETHUSDT-PERPETUAL", + # "price": "3327.54", + # "quantity": "4", + # "commissionAsset": "USDT", + # "commission": "0.00798609", + # "makerRebate": "0", + # "type": "LIMIT", + # "side": "BUY_OPEN", + # "realizedPnl": "0", + # "isMarker": False + # } + timestamp = self.safe_integer_2(trade, 't', 'time') + marketId = self.safe_string(trade, 'symbol') + market = self.safe_market(marketId, market) + side = self.safe_string_lower(trade, 'side') # swap trades have side param + if side is not None: + side = self.safe_string(side.split('_'), 0) + isBuyer = self.safe_bool(trade, 'isBuyer') + if isBuyer is not None: + side = 'buy' if isBuyer else 'sell' + takerOrMaker = None + isMaker = self.safe_bool_n(trade, ['isMaker', 'isMarker']) + if isMaker is not None: + takerOrMaker = 'maker' if isMaker else 'taker' + isBuyerMaker = self.safe_bool(trade, 'ibm') + # if public trade + if isBuyerMaker is not None: + takerOrMaker = 'taker' + side = 'sell' if isBuyerMaker else 'buy' + feeCost = self.safe_string(trade, 'commission') + feeCurrncyId = self.safe_string(trade, 'commissionAsset') + feeInfo = self.safe_dict(trade, 'fee') + fee = None + if feeInfo is not None: + feeCost = self.safe_string(feeInfo, 'fee') + feeCurrncyId = self.safe_string(feeInfo, 'feeCoinId') + if feeCost is not None: + fee = { + 'cost': self.parse_number(feeCost), + 'currency': self.safe_currency_code(feeCurrncyId), + } + return self.safe_trade({ + 'id': self.safe_string_2(trade, 'id', 'tradeId'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'side': side, + 'price': self.safe_string_2(trade, 'p', 'price'), + 'amount': self.safe_string_n(trade, ['q', 'qty', 'quantity']), + 'cost': None, + 'takerOrMaker': takerOrMaker, + 'type': None, + 'order': self.safe_string(trade, 'orderId'), + 'fee': fee, + 'info': trade, + }, market) + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + + https://hashkeyglobal-apidoc.readme.io/reference/get-kline + + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest candle to fetch + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + methodName = 'fetchOHLCV' + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, methodName, 'paginate') + if paginate: + return self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params, 1000) + market = self.market(symbol) + timeframe = self.safe_string(self.timeframes, timeframe, timeframe) + request: dict = { + 'symbol': market['id'], + 'interval': timeframe, + } + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + until: Int = None + until, params = self.handle_option_and_params(params, methodName, 'until') + if until is not None: + request['endTime'] = until + response = self.publicGetQuoteV1Klines(self.extend(request, params)) + # + # [ + # [ + # 1721684280000, + # "67832.49", + # "67862.5", + # "67832.49", + # "67861.44", + # "0.01122",0, + # "761.2763533",68, + # "0.00561", + # "380.640643" + # ], + # ... + # ] + # + return self.parse_ohlcvs(response, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # [ + # 1721684280000, + # "67832.49", + # "67862.5", + # "67832.49", + # "67861.44", + # "0.01122",0, + # "761.2763533",68, + # "0.00561", + # "380.640643" + # ] + # + return [ + self.safe_integer(ohlcv, 0), + self.safe_number(ohlcv, 1), + self.safe_number(ohlcv, 2), + self.safe_number(ohlcv, 3), + self.safe_number(ohlcv, 4), + self.safe_number(ohlcv, 5), + ] + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://hashkeyglobal-apidoc.readme.io/reference/get-24hr-ticker-price-change + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = self.publicGetQuoteV1Ticker24hr(self.extend(request, params)) + # + # [ + # { + # "t": 1721685896846, + # "s": "BTCUSDT-PERPETUAL", + # "c": "67756.7", + # "h": "68479.9", + # "l": "66594.3", + # "o": "68279.7", + # "b": "67756.6", + # "a": "67756.7", + # "v": "1604722", + # "qv": "108827258.7761" + # } + # ] + # + ticker = self.safe_dict(response, 0, {}) + return self.parse_ticker(ticker, market) + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://hashkeyglobal-apidoc.readme.io/reference/get-24hr-ticker-price-change + + :param str[] [symbols]: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + symbols = self.market_symbols(symbols) + response = self.publicGetQuoteV1Ticker24hr(params) + return self.parse_tickers(response, symbols) + + def parse_ticker(self, ticker, market: Market = None) -> Ticker: + # + # { + # "t": 1721685896846, + # "s": "BTCUSDT-PERPETUAL", + # "c": "67756.7", + # "h": "68479.9", + # "l": "66594.3", + # "o": "68279.7", + # "b": "67756.6", + # "a": "67756.7", + # "v": "1604722", + # "qv": "108827258.7761" + # } + # + timestamp = self.safe_integer(ticker, 't') + marketId = self.safe_string(ticker, 's') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + last = self.safe_string(ticker, 'c') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'h'), + 'low': self.safe_string(ticker, 'l'), + 'bid': self.safe_string(ticker, 'b'), + 'bidVolume': None, + 'ask': self.safe_string(ticker, 'a'), + 'askVolume': None, + 'vwap': None, + 'open': self.safe_string(ticker, 'o'), + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': self.safe_string(ticker, 'v'), + 'quoteVolume': self.safe_string(ticker, 'qv'), + 'info': ticker, + }, market) + + def fetch_last_prices(self, symbols: Strings = None, params={}) -> LastPrices: + """ + fetches the last price for multiple markets + + https://hashkeyglobal-apidoc.readme.io/reference/get-symbol-price-ticker + + :param str[] [symbols]: unified symbols of the markets to fetch the last prices + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.symbol]: the id of the market to fetch last price for + :returns dict: a dictionary of lastprices structures + """ + self.load_markets() + symbols = self.market_symbols(symbols) + request: dict = {} + response = self.publicGetQuoteV1TickerPrice(self.extend(request, params)) + # + # [ + # { + # "s": "BTCUSDT-PERPETUAL", + # "p": "64871" + # }, + # ... + # ] + # + return self.parse_last_prices(response, symbols) + + def parse_last_price(self, entry, market: Market = None) -> LastPrice: + marketId = self.safe_string(entry, 's') + market = self.safe_market(marketId, market) + return { + 'symbol': market['symbol'], + 'timestamp': None, + 'datetime': None, + 'price': self.safe_number(entry, 'p'), + 'side': None, + 'info': entry, + } + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://hashkeyglobal-apidoc.readme.io/reference/get-account-information + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.accountId]: account ID, for Master Key only + :param str [params.type]: 'spot' or 'swap' - the type of the market to fetch balance for(default 'spot') + :returns dict: a `balance structure ` + """ + self.load_markets() + request: dict = {} + methodName = 'fetchBalance' + marketType = 'spot' + marketType, params = self.handle_market_type_and_params(methodName, None, params, marketType) + if marketType == 'swap': + response = self.privateGetApiV1FuturesBalance(params) + # + # [ + # { + # "balance": "30.63364672", + # "availableBalance": "28.85635534", + # "positionMargin": "4.3421", + # "orderMargin": "0", + # "asset": "USDT", + # "crossUnRealizedPnl": "2.5649" + # } + # ] + # + balance = self.safe_dict(response, 0, {}) + return self.parse_swap_balance(balance) + elif marketType == 'spot': + response = self.privateGetApiV1Account(self.extend(request, params)) + # + # { + # "balances": [ + # { + # "asset":"USDT", + # "assetId":"USDT", + # "assetName":"USDT", + # "total":"40", + # "free":"40", + # "locked":"0" + # }, + # ... + # ], + # "userId": "1732885739572845312" + # } + # + return self.parse_balance(response) + else: + raise NotSupported(self.id + ' ' + methodName + '() is not supported for ' + marketType + ' type of markets') + + def parse_balance(self, balance) -> Balances: + # + # { + # "balances": [ + # { + # "asset":"USDT", + # "assetId":"USDT", + # "assetName":"USDT", + # "total":"40", + # "free":"40", + # "locked":"0" + # }, + # ... + # ], + # "userId": "1732885739572845312" + # } + # + result: dict = { + 'info': balance, + } + balances = self.safe_list(balance, 'balances', []) + for i in range(0, len(balances)): + balanceEntry = balances[i] + currencyId = self.safe_string(balanceEntry, 'asset') + code = self.safe_currency_code(currencyId) + account = self.account() + account['total'] = self.safe_string(balanceEntry, 'total') + account['free'] = self.safe_string(balanceEntry, 'free') + account['used'] = self.safe_string(balanceEntry, 'locked') + result[code] = account + return self.safe_balance(result) + + def parse_swap_balance(self, balance) -> Balances: + # + # { + # "balance": "30.63364672", + # "availableBalance": "28.85635534", + # "positionMargin": "4.3421", + # "orderMargin": "0", + # "asset": "USDT", + # "crossUnRealizedPnl": "2.5649" + # } + # + currencyId = self.safe_string(balance, 'asset') + code = self.safe_currency_code(currencyId) + account = self.account() + account['total'] = self.safe_string(balance, 'balance') + positionMargin = self.safe_string(balance, 'positionMargin') + orderMargin = self.safe_string(balance, 'orderMargin') + account['used'] = Precise.string_add(positionMargin, orderMargin) + result: dict = { + 'info': balance, + } + result[code] = account + return self.safe_balance(result) + + def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://hashkeyglobal-apidoc.readme.io/reference/get-deposit-address + + :param str code: unified currency code(default is 'USDT') + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.network]: network for fetch deposit address(default is 'ETH') + :returns dict: an `address structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'coin': currency['id'], + } + networkCode: Str = None + networkCode, params = self.handle_network_code_and_params(params) + if networkCode is None: + networkCode = self.default_network_code(code) + request['chainType'] = self.network_code_to_id(networkCode, code) + response = self.privateGetApiV1AccountDepositAddress(self.extend(request, params)) + # + # { + # "canDeposit": True, + # "address": "0x61AAd7F763e2C7fF1CC996918740F67f9dC8BF4e", + # "addressExt": "", + # "minQuantity": "1", + # "needAddressTag": False, + # "requiredConfirmTimes": 64, + # "canWithdrawConfirmTimes": 64, + # "coinType": "ERC20_TOKEN" + # } + # + depositAddress = self.parse_deposit_address(response, currency) + depositAddress['network'] = networkCode + return depositAddress + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + # + # { + # "canDeposit": True, + # "address": "0x61AAd7F763e2C7fF1CC996918740F67f9dC8BF4e", + # "addressExt": "", + # "minQuantity": "1", + # "needAddressTag": False, + # "requiredConfirmTimes": 64, + # "canWithdrawConfirmTimes": 64, + # "coinType": "ERC20_TOKEN" + # } + # + address = self.safe_string(depositAddress, 'address') + self.check_address(address) + tag = self.safe_string(depositAddress, 'addressExt') + if tag == '': + tag = None + return { + 'info': depositAddress, + 'currency': currency['code'], + 'network': None, + 'address': address, + 'tag': tag, + } + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://hashkeyglobal-apidoc.readme.io/reference/get-deposit-history + + :param str code: unified currency code of the currency transferred + :param int [since]: the earliest time in ms to fetch transfers for(default 24 hours ago) + :param int [limit]: the maximum number of transfer structures to retrieve(default 50, max 200) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch transfers for(default time now) + :param int [params.fromId]: starting ID(To be released) + :returns dict[]: a list of `transfer structures ` + """ + methodName = 'fetchDeposits' + self.load_markets() + request: dict = {} + currency: Currency = None + if code is not None: + currency = self.currency(code) + request['coin'] = currency['id'] + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + until: Int = None + until, params = self.handle_option_and_params(params, methodName, 'until') + if until is not None: + request['endTime'] = until + response = self.privateGetApiV1AccountDepositOrders(self.extend(request, params)) + # + # [ + # { + # "time": "1721641082163", + # "coin": "TRXUSDT", + # "coinName": "TRXUSDT", + # "address": "TBA6CypYJizwA9XdC7Ubgc5F1bxrQ7SqPt", + # "quantity": "86.00000000000000000000", + # "status": 4, + # "statusCode": "4", + # "txId": "0970c14da4d7412295fa7b21c03a08da319e746a0d59ef14462a74183d118da4" + # } + # ] + # + return self.parse_transactions(response, currency, since, limit, {'type': 'deposit'}) + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://hashkeyglobal-apidoc.readme.io/reference/withdrawal-records + + :param str code: unified currency code of the currency transferred + :param int [since]: the earliest time in ms to fetch transfers for(default 24 hours ago) + :param int [limit]: the maximum number of transfer structures to retrieve(default 50, max 200) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch transfers for(default time now) + :returns dict[]: a list of `transaction structures ` + """ + methodName = 'fetchWithdrawals' + self.load_markets() + request: dict = {} + currency: Currency = None + if code is not None: + currency = self.currency(code) + request['coin'] = currency['id'] + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + until: Int = None + until, params = self.handle_option_and_params(params, methodName, 'until') + if until is not None: + request['endTime'] = until + response = self.privateGetApiV1AccountWithdrawOrders(self.extend(request, params)) + # + # [ + # { + # "time": "1723545505366", + # "id": "W611267400947572736", + # "coin": "USDT", + # "coinId": "USDT", + # "coinName": "USDT", + # "address": "TQbkBMnWnJNGTAUpFS4kvv4NRLzUAnGAes", + # "quantity": "2.00000000", + # "arriveQuantity": "2.00000000", + # "txId": "f83f94e7d2e81fbec98c66c25d6615872cc2d426145629b6cf22e5e0a0753715", + # "addressUrl": "TQbkBMnWnJNGTAUpFS4kvv4NRLzUAnGAes", + # "feeCoinId": "USDT", + # "feeCoinName": "USDT", + # "fee": "1.00000000", + # "remark": "", + # "platform": "" + # } + # ] + # + return self.parse_transactions(response, currency, since, limit, {'type': 'withdrawal'}) + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://hashkeyglobal-apidoc.readme.io/reference/withdraw + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.network]: network for withdraw + :param str [params.clientOrderId]: client order id + :param str [params.platform]: the platform to withdraw to(hashkey, HashKey HK) + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.load_markets() + currency = self.currency(code) + request: dict = { + 'coin': currency['id'], + 'address': address, + 'quantity': amount, + } + if tag is not None: + request['addressExt'] = tag + networkCode: Str = None + networkCode, params = self.handle_network_code_and_params(params) + if networkCode is not None: + request['chainType'] = self.network_code_to_id(networkCode) + response = self.privatePostApiV1AccountWithdraw(self.extend(request, params)) + # + # { + # "success": True, + # "id": "0", + # "orderId": "W611267400947572736", + # "accountId": "1732885739589466115" + # } + # + return self.parse_transaction(response, currency) + + def parse_transaction(self, transaction, currency: Currency = None) -> Transaction: + # + # fetchDeposits + # { + # "time": "1721641082163", + # "coin": "TRXUSDT", # todo how to parse it? + # "coinName": "TRXUSDT", + # "address": "TBA6CypYJizwA9XdC7Ubgc5F1bxrQ7SqPt", + # "quantity": "86.00000000000000000000", + # "status": 4, + # "statusCode": "4", + # "txId": "0970c14da4d7412295fa7b21c03a08da319e746a0d59ef14462a74183d118da4" + # } + # + # fetchWithdrawals + # { + # "time": "1723545505366", + # "id": "W611267400947572736", + # "coin": "USDT", + # "coinId": "USDT", + # "coinName": "USDT", + # "address": "TQbkBMnWnJNGTAUpFS4kvv4NRLzUAnGAes", + # "quantity": "2.00000000", + # "arriveQuantity": "2.00000000", + # "txId": "f83f94e7d2e81fbec98c66c25d6615872cc2d426145629b6cf22e5e0a0753715", + # "addressUrl": "TQbkBMnWnJNGTAUpFS4kvv4NRLzUAnGAes", + # "feeCoinId": "USDT", + # "feeCoinName": "USDT", + # "fee": "1.00000000", + # "remark": "", + # "platform": "" + # } + # + # withdraw + # { + # "success": True, + # "id": "0", + # "orderId": "W611267400947572736", + # "accountId": "1732885739589466115" + # } + # + id = self.safe_string_2(transaction, 'id', 'orderId') + address = self.safe_string(transaction, 'address') + status = self.safe_string(transaction, 'status') # for fetchDeposits + if status is None: + success = self.safe_bool(transaction, 'success', False) # for withdraw + if success: + status = 'ok' + else: + addressUrl = self.safe_string(transaction, 'addressUrl') # for fetchWithdrawals + if addressUrl is not None: + status = 'ok' + txid = self.safe_string(transaction, 'txId') + coin = self.safe_string(transaction, 'coin') + code = self.safe_currency_code(coin, currency) + timestamp = self.safe_integer(transaction, 'time') + amount = self.safe_number(transaction, 'quantity') + feeCost = self.safe_number(transaction, 'fee') + fee = None + if feeCost is not None: + fee = { + 'cost': feeCost, + 'currency': code, + } + return { + 'info': transaction, + 'id': id, + 'txid': txid, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': None, + 'address': address, + 'addressTo': None, + 'addressFrom': None, + 'tag': None, + 'tagTo': None, + 'tagFrom': None, + 'type': None, + 'amount': amount, + 'currency': code, + 'status': self.parse_transaction_status(status), + 'updated': None, + 'internal': None, + 'comment': None, + 'fee': fee, + } + + def parse_transaction_status(self, status): + statuses: dict = { + '1': 'pending', + '2': 'pending', + '3': 'failed', + '4': 'ok', + '5': 'pending', + '6': 'ok', + '7': 'failed', + '8': 'cancelled', + '9': 'failed', + '10': 'failed', + 'successful': 'ok', + 'success': 'ok', + } + return self.safe_string(statuses, status, status) + + def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://hashkeyglobal-apidoc.readme.io/reference/new-account-transfer + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account id to transfer from + :param str toAccount: account id to transfer to + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.clientOrderId]: a unique id for the transfer + :param str [params.remark]: a note for the transfer + :returns dict: a `transfer structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'coin': currency['id'], + 'quantity': self.currency_to_precision(code, amount), + 'fromAccountId': fromAccount, + 'toAccountId': toAccount, + } + response = self.privatePostApiV1AccountAssetTransfer(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1722260230773, + # "clientOrderId": "", + # "orderId": "1740839420695806720" + # } + # + return self.parse_transfer(response, currency) + + def parse_transfer(self, transfer, currency: Currency = None): + timestamp = self.safe_integer(transfer, 'timestamp') + currencyId = self.safe_string(currency, 'id') + status: Str = None + success = self.safe_bool(transfer, 'success', False) + if success: + status = 'ok' + return { + 'id': self.safe_string(transfer, 'orderId'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'currency': self.safe_currency_code(currencyId, currency), + 'amount': None, + 'fromAccount': None, + 'toAccount': None, + 'status': status, + 'info': transfer, + } + + def fetch_accounts(self, params={}) -> List[Account]: + """ + fetch all the accounts associated with a profile + + https://hashkeyglobal-apidoc.readme.io/reference/query-sub-account + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `account structures ` indexed by the account type + """ + self.load_markets() + response = self.privateGetApiV1AccountType(params) + # + # [ + # { + # "accountId": "1732885739589466112", + # "accountLabel": "Main Trading Account", + # "accountType": 1, + # "accountIndex": 0 + # }, + # ... + # ] + # + return self.parse_accounts(response, params) + + def parse_account(self, account): + accountLabel = self.safe_string(account, 'accountLabel') + label = '' + if accountLabel == 'Main Trading Account' or accountLabel == 'Main Future Account': + label = 'main' + elif accountLabel == 'Sub Main Trading Account' or accountLabel == 'Sub Main Future Account': + label = 'sub' + accountType = self.parse_account_type(self.safe_string(account, 'accountType')) + type = label + ' ' + accountType + return { + 'id': self.safe_string(account, 'accountId'), + 'type': type, + 'code': None, + 'info': account, + } + + def parse_account_type(self, type): + types: dict = { + '1': 'spot account', + '3': 'swap account', + '5': 'custody account', + '6': 'fiat account', + } + return self.safe_string(types, type, type) + + def encode_account_type(self, type): + types = { + 'spot': '1', + 'swap': '3', + 'custody': '5', + } + return self.safe_integer(types, type, type) + + def encode_flow_type(self, type): + types = { + 'trade': '1', + 'fee': '3', + 'transfer': '51', + 'deposit': '900', + 'withdraw': '904', + } + return self.safe_integer(types, type, type) + + def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + + https://hashkeyglobal-apidoc.readme.io/reference/get-account-transaction-list + + :param str [code]: unified currency code, default is None(not used) + :param int [since]: timestamp in ms of the earliest ledger entry, default is None + :param int [limit]: max number of ledger entries to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch entries for + :param int [params.flowType]: trade, fee, transfer, deposit, withdrawal + :param int [params.accountType]: spot, swap, custody + :returns dict: a `ledger structure ` + """ + methodName = 'fetchLedger' + if since is None: + raise ArgumentsRequired(self.id + ' ' + methodName + '() requires a since argument') + until: Int = None + until, params = self.handle_option_and_params(params, methodName, 'until') + if until is None: + raise ArgumentsRequired(self.id + ' ' + methodName + '() requires an until argument') + self.load_markets() + currency = self.currency(code) + request = {} + request['startTime'] = since + if limit is not None: + request['limit'] = limit + request['endTime'] = until + flowType = None + flowType, params = self.handle_option_and_params(params, methodName, 'flowType') + if flowType is not None: + request['flowType'] = self.encode_flow_type(flowType) + accountType = None + accountType, params = self.handle_option_and_params(params, methodName, 'accountType') + if accountType is not None: + request['accountType'] = self.encode_account_type(accountType) + response = self.privateGetApiV1AccountBalanceFlow(self.extend(request, params)) + # + # [ + # { + # "id": "1740844413612065537", + # "accountId": "1732885739589466112", + # "coin": "USDT", + # "coinId": "USDT", + # "coinName": "USDT", + # "flowTypeValue": 51, + # "flowType": "USER_ACCOUNT_TRANSFER", + # "flowName": "", + # "change": "-1", + # "total": "8.015680088", + # "created": "1722260825765" + # }, + # ... + # ] + # + return self.parse_ledger(response, currency, since, limit) + + def parse_ledger_entry_type(self, type): + types: dict = { + '1': 'trade', # transfer + '2': 'fee', # trade + '51': 'transfer', + '900': 'deposit', + '904': 'withdraw', + } + return self.safe_string(types, type, type) + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + # + # { + # "id": "1740844413612065537", + # "accountId": "1732885739589466112", + # "coin": "USDT", + # "coinId": "USDT", + # "coinName": "USDT", + # "flowTypeValue": 51, + # "flowType": "USER_ACCOUNT_TRANSFER", + # "flowName": "", + # "change": "-1", + # "total": "8.015680088", + # "created": "1722260825765" + # } + # + id = self.safe_string(item, 'id') + account = self.safe_string(item, 'accountId') + timestamp = self.safe_integer(item, 'created') + type = self.parse_ledger_entry_type(self.safe_string(item, 'flowTypeValue')) + currencyId = self.safe_string(item, 'coin') + code = self.safe_currency_code(currencyId, currency) + currency = self.safe_currency(currencyId, currency) + amountString = self.safe_string(item, 'change') + amount = self.parse_number(amountString) + direction = 'in' + if amountString.find('-') >= 0: + direction = 'out' + afterString = self.safe_string(item, 'total') + after = self.parse_number(afterString) + status = 'ok' + return self.safe_ledger_entry({ + 'info': item, + 'id': id, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'account': account, + 'direction': direction, + 'referenceId': None, + 'referenceAccount': None, + 'type': type, + 'currency': code, + 'symbol': None, + 'amount': amount, + 'before': None, + 'after': after, + 'status': status, + 'fee': None, + }, currency) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}) -> Order: + """ + create a trade order + + https://hashkeyglobal-apidoc.readme.io/reference/test-new-order + https://hashkeyglobal-apidoc.readme.io/reference/create-order + https://hashkeyglobal-apidoc.readme.io/reference/create-new-futures-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' or 'LIMIT_MAKER' for spot, 'market' or 'limit' or 'STOP' for swap + :param str side: 'buy' or 'sell' + :param float amount: how much of you want to trade in units of the base currency + :param float [price]: the price that the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.cost]: *spot market buy only* the quote quantity that can be used alternative for the amount + :param boolean [params.test]: *spot markets only* whether to use the test endpoint or not, default is False + :param bool [params.postOnly]: if True, the order will only be posted to the order book and not executed immediately + :param str [params.timeInForce]: "GTC" or "IOC" or "PO" for spot, 'GTC' or 'FOK' or 'IOC' or 'LIMIT_MAKER' or 'PO' for swap + :param str [params.clientOrderId]: a unique id for the order - is mandatory for swap + :param float [params.triggerPrice]: *swap markets only* The price at which a trigger order is triggered at + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + if market['spot']: + return self.create_spot_order(symbol, type, side, amount, price, params) + elif market['swap']: + return self.create_swap_order(symbol, type, side, amount, price, params) + else: + raise NotSupported(self.id + ' createOrder() is not supported for ' + market['type'] + ' type of markets') + + def create_market_buy_order_with_cost(self, symbol: str, cost: float, params={}) -> Order: + """ + create a market buy order by providing the symbol and cost + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' createMarketBuyOrderWithCost() is supported for spot markets only') + req = { + 'cost': cost, + } + return self.create_order(symbol, 'market', 'buy', cost, None, self.extend(req, params)) + + def create_spot_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}) -> Order: + """ + create a trade order on spot market + + https://hashkeyglobal-apidoc.readme.io/reference/test-new-order + https://hashkeyglobal-apidoc.readme.io/reference/create-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' or 'LIMIT_MAKER' + :param str side: 'buy' or 'sell' + :param float amount: how much of you want to trade in units of the base currency + :param float [price]: the price that the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.cost]: *market buy only* the quote quantity that can be used alternative for the amount + :param bool [params.test]: whether to use the test endpoint or not, default is False + :param bool [params.postOnly]: if True, the order will only be posted to the order book and not executed immediately + :param str [params.timeInForce]: 'GTC', 'IOC', or 'PO' + :param str [params.clientOrderId]: a unique id for the order + :returns dict: an `order structure ` + """ + triggerPrice = self.safe_string_2(params, 'stopPrice', 'triggerPrice') + if triggerPrice is not None: + raise NotSupported(self.id + ' trigger orders are not supported for spot markets') + self.load_markets() + market = self.market(symbol) + isMarketBuy = (type == 'market') and (side == 'buy') + cost = self.safe_string(params, 'cost') + if (not isMarketBuy) and (cost is not None): + raise NotSupported(self.id + ' createOrder() supports cost parameter for spot market buy orders only') + request: dict = self.create_spot_order_request(symbol, type, side, amount, price, params) + response: dict = {} + test = self.safe_bool(params, 'test') + if test: + params = self.omit(params, 'test') + response = self.privatePostApiV1SpotOrderTest(request) + elif isMarketBuy and (cost is None): + response = self.privatePostApiV11SpotOrder(request) # the endpoint for market buy orders by amount + # + # { + # "accountId": "1732885739589466112", + # "symbol": "ETHUSDT", + # "symbolName": "ETHUSDT", + # "clientOrderId": "1722005792096557", + # "orderId": "1738705036219839744", + # "transactTime": "1722005792106", + # "price": "0", + # "origQty": "0.006", + # "executedQty": "0.0059", + # "status": "FILLED", + # "timeInForce": "IOC", + # "type": "MARKET", + # "side": "BUY", + # "reqAmount": "0", + # "concentration": "" + # } + # + else: + response = self.privatePostApiV1SpotOrder(request) # the endpoint for market buy orders by cost and other orders + # + # market buy + # { + # "accountId": "1732885739589466112", + # "symbol": "ETHUSDT", + # "symbolName": "ETHUSDT", + # "clientOrderId": "1722004623170558", + # "orderId": "1738695230608169984", + # "transactTime": "1722004623186", + # "price": "0", + # "origQty": "0", + # "executedQty": "0.0061", + # "status": "FILLED", + # "timeInForce": "IOC", + # "type": "MARKET", + # "side": "BUY", + # "reqAmount": "20", + # "concentration": "" + # } + # + # market sell + # { + # "accountId": "1732885739589466112", + # "symbol": "ETHUSDT", + # "symbolName": "ETHUSDT", + # "clientOrderId": "1722005654516362", + # "orderId": "1738703882140316928", + # "transactTime": "1722005654529", + # "price": "0", + # "origQty": "0.006", + # "executedQty": "0.006", + # "status": "FILLED", + # "timeInForce": "IOC", + # "type": "MARKET", + # "side": "SELL", + # "reqAmount": "0", + # "concentration": "" + # } + # + # limit + # { + # "accountId": "1732885739589466112", + # "symbol": "ETHUSDT", + # "symbolName": "ETHUSDT", + # "clientOrderId": "1722006209978370", + # "orderId": "1738708541676585728", + # "transactTime": "1722006209989", + # "price": "5000", + # "origQty": "0.005", + # "executedQty": "0", + # "status": "NEW", + # "timeInForce": "GTC", + # "type": "LIMIT_MAKER", + # "side": "SELL", + # "reqAmount": "0", + # "concentration": "" + # } + # + return self.parse_order(response, market) + + def create_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}) -> dict: + market = self.market(symbol) + if market['spot']: + return self.create_spot_order_request(symbol, type, side, amount, price, params) + elif market['swap']: + return self.create_swap_order_request(symbol, type, side, amount, price, params) + else: + raise NotSupported(self.id + ' ' + 'createOrderRequest() is not supported for ' + market['type'] + ' type of markets') + + def create_spot_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}) -> dict: + """ + @ignore + helper function to build request + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' or 'LIMIT_MAKER' + :param str side: 'buy' or 'sell' + :param float amount: how much of you want to trade in units of the base currency + :param float [price]: the price that the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.cost]: *market buy only* the quote quantity that can be used alternative for the amount + :param bool [params.postOnly]: if True, the order will only be posted to the order book and not executed immediately + :param str [params.timeInForce]: "GTC", "IOC", or "PO" + :param str [params.clientOrderId]: a unique id for the order + :returns dict: request to be sent to the exchange + """ + market = self.market(symbol) + type = type.upper() + request: dict = { + 'symbol': market['id'], + 'side': side.upper(), + 'type': type, + } + if amount is not None: + request['quantity'] = self.amount_to_precision(symbol, amount) + cost: Str = None + cost, params = self.handle_param_string(params, 'cost') + if cost is not None: + request['quantity'] = self.cost_to_precision(symbol, cost) + if price is not None: + request['price'] = self.price_to_precision(symbol, price) + isMarketOrder = type == 'MARKET' + postOnly = False + postOnly, params = self.handle_post_only(isMarketOrder, type == 'LIMIT_MAKER', params) + if postOnly and (type == 'LIMIT'): + request['type'] = 'LIMIT_MAKER' + clientOrderId: Str = None + clientOrderId, params = self.handle_param_string(params, 'clientOrderId') + if clientOrderId is not None: + params['newClientOrderId'] = clientOrderId + return self.extend(request, params) + + def create_swap_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}) -> dict: + """ + @ignore + helper function to build request + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' or 'STOP' + :param str side: 'buy' or 'sell' + :param float amount: how much of you want to trade in units of the base currency + :param float [price]: the price that the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.postOnly]: if True, the order will only be posted to the order book and not executed immediately + :param bool [params.reduceOnly]: True or False whether the order is reduce only + :param float [params.triggerPrice]: The price at which a trigger order is triggered at + :param str [params.timeInForce]: 'GTC', 'FOK', 'IOC', 'LIMIT_MAKER' or 'PO' + :param str [params.clientOrderId]: a unique id for the order + :returns dict: request to be sent to the exchange + """ + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'type': 'LIMIT', + 'quantity': self.amount_to_precision(symbol, amount), + } + isMarketOrder = type == 'market' + if isMarketOrder: + request['priceType'] = 'MARKET' + if price is not None: + request['price'] = self.price_to_precision(symbol, price) + request['priceType'] = 'INPUT' + reduceOnly = False + reduceOnly, params = self.handle_param_bool(params, 'reduceOnly', reduceOnly) + suffix = '_OPEN' + if reduceOnly: + suffix = '_CLOSE' + request['side'] = side.upper() + suffix + timeInForce: Str = None + timeInForce, params = self.handle_param_string(params, 'timeInForce') + postOnly = False + postOnly, params = self.handle_post_only(isMarketOrder, timeInForce == 'LIMIT_MAKER', params) + if postOnly: + timeInForce = 'LIMIT_MAKER' + if timeInForce is not None: + request['timeInForce'] = timeInForce + clientOrderId = self.safe_string(params, 'clientOrderId') + if clientOrderId is None: + request['clientOrderId'] = self.uuid() + triggerPrice = self.safe_string(params, 'triggerPrice') + if triggerPrice is not None: + request['stopPrice'] = self.price_to_precision(symbol, triggerPrice) + request['type'] = 'STOP' + params = self.omit(params, 'triggerPrice') + return self.extend(request, params) + + def create_swap_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}) -> Order: + """ + create a trade order on swap market + + https://hashkeyglobal-apidoc.readme.io/reference/create-new-futures-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' or 'STOP' + :param str side: 'buy' or 'sell' + :param float amount: how much of you want to trade in units of the base currency + :param float [price]: the price that the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.postOnly]: if True, the order will only be posted to the order book and not executed immediately + :param bool [params.reduceOnly]: True or False whether the order is reduce only + :param float [params.triggerPrice]: The price at which a trigger order is triggered at + :param str [params.timeInForce]: 'GTC', 'FOK', 'IOC', 'LIMIT_MAKER' or 'PO' + :param str [params.clientOrderId]: a unique id for the order + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + request = self.create_swap_order_request(symbol, type, side, amount, price, params) + response = self.privatePostApiV1FuturesOrder(self.extend(request, params)) + # + # { + # "time": "1722429951611", + # "updateTime": "1722429951648", + # "orderId": "1742263144028363776", + # "clientOrderId": "1722429950315", + # "symbol": "ETHUSDT-PERPETUAL", + # "price": "3460.62", + # "leverage": "5", + # "origQty": "10", + # "executedQty": "10", + # "avgPrice": "0", + # "marginLocked": "6.9212", + # "type": "LIMIT", + # "side": "BUY_OPEN", + # "timeInForce": "IOC", + # "status": "FILLED", + # "priceType": "MARKET", + # "contractMultiplier": "0.00100000" + # } + # + return self.parse_order(response, market) + + def create_orders(self, orders: List[OrderRequest], params={}): + """ + create a list of trade orders(all orders should be of the same symbol) + + https://hashkeyglobal-apidoc.readme.io/reference/create-multiple-orders + https://hashkeyglobal-apidoc.readme.io/reference/batch-create-new-futures-order + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the api endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + ordersRequests = [] + for i in range(0, len(orders)): + rawOrder = orders[i] + symbol = self.safe_string(rawOrder, 'symbol') + type = self.safe_string(rawOrder, 'type') + side = self.safe_string(rawOrder, 'side') + amount = self.safe_number(rawOrder, 'amount') + price = self.safe_number(rawOrder, 'price') + orderParams = self.safe_dict(rawOrder, 'params', {}) + orderRequest = self.create_order_request(symbol, type, side, amount, price, orderParams) + clientOrderId = self.safe_string(orderRequest, 'clientOrderId') + if clientOrderId is None: + orderRequest['clientOrderId'] = self.uuid() # both spot and swap endpoints require clientOrderId + ordersRequests.append(orderRequest) + firstOrder = ordersRequests[0] + firstSymbol = self.safe_string(firstOrder, 'symbol') + market = self.market(firstSymbol) + request: dict = { + 'orders': ordersRequests, + } + response = None + if market['spot']: + response = self.privatePostApiV1SpotBatchOrders(self.extend(request, params)) + # + # { + # "code": 0, + # "result": [ + # { + # "code": "0000", + # "order": { + # "accountId": "1732885739589466112", + # "symbol": "ETHUSDT", + # "symbolName": "ETHUSDT", + # "clientOrderId": "1722701490163000", + # "orderId": "1744540984757258752", + # "transactTime": "1722701491385", + # "price": "1500", + # "origQty": "0.001", + # "executedQty": "0", + # "status": "NEW", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "side": "BUY", + # "reqAmount": "0" + # } + # } + # ], + # "concentration": "" + # } + # + elif market['swap']: + response = self.privatePostApiV1FuturesBatchOrders(self.extend(request, params)) + # + # { + # "code": "0000", + # "result": [ + # { + # "code": "0000", + # "order": { + # "time": "1722704251911", + # "updateTime": "1722704251918", + # "orderId": "1744564141727808768", + # "clientOrderId": "1722704250648000", + # "symbol": "ETHUSDT-PERPETUAL", + # "price": "1500", + # "leverage": "4", + # "origQty": "1", + # "executedQty": "0", + # "avgPrice": "0", + # "marginLocked": "0.375", + # "type": "LIMIT", + # "side": "BUY_OPEN", + # "timeInForce": "GTC", + # "status": "NEW", + # "priceType": "INPUT", + # "isLiquidationOrder": False, + # "indexPrice": "0", + # "liquidationType": "" + # } + # }, + # { + # "code": "0207", + # "msg": "Create limit order sell price too low" + # } + # ] + # } + # + else: + raise NotSupported(self.id + ' ' + 'createOrderRequest() is not supported for ' + market['type'] + ' type of markets') + result = self.safe_list(response, 'result', []) + responseOrders = [] + for i in range(0, len(result)): + responseEntry = self.safe_dict(result, i, {}) + responseOrder = self.safe_dict(responseEntry, 'order', {}) + responseOrders.append(responseOrder) + return self.parse_orders(responseOrders) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://hashkeyglobal-apidoc.readme.io/reference/cancel-order + https://hashkeyglobal-apidoc.readme.io/reference/cancel-futures-order + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: 'spot' or 'swap' - the type of the market to fetch entry for(default 'spot') + :param str [params.clientOrderId]: a unique id for the order that can be used alternative for the id + :param bool [params.trigger]: *swap markets only* True for canceling a trigger order(default False) + :param bool [params.stop]: *swap markets only* an alternative for trigger param + :returns dict: An `order structure ` + """ + methodName = 'cancelOrder' + self.check_type_param(methodName, params) + self.load_markets() + request: dict = {} + clientOrderId = self.safe_string(params, 'clientOrderId') + if clientOrderId is None: + request['orderId'] = id + market: Market = None + if symbol is not None: + market = self.market(symbol) + marketType = 'spot' + marketType, params = self.handle_market_type_and_params(methodName, market, params, marketType) + response = None + if marketType == 'spot': + response = self.privateDeleteApiV1SpotOrder(self.extend(request, params)) + # + # { + # "accountId": "1732885739589466112", + # "symbol": "ETHUSDT", + # "clientOrderId": "1722006209978370", + # "orderId": "1738708541676585728", + # "transactTime": "1722006209989", + # "price": "5000", + # "origQty": "0.005", + # "executedQty": "0", + # "status": "NEW", + # "timeInForce": "GTC", + # "type": "LIMIT_MAKER", + # "side": "SELL" + # } + # + elif marketType == 'swap': + isTrigger = False + isTrigger, params = self.handle_trigger_option_and_params(params, methodName, isTrigger) + if isTrigger: + request['type'] = 'STOP' + else: + request['type'] = 'LIMIT' + if market is not None: + request['symbol'] = market['id'] + response = self.privateDeleteApiV1FuturesOrder(self.extend(request, params)) + # + # { + # "time": "1722432302919", + # "updateTime": "1722432302925", + # "orderId": "1742282868229463040", + # "clientOrderId": "1722432301670", + # "symbol": "ETHUSDT-PERPETUAL", + # "price": "4000", + # "leverage": "5", + # "origQty": "10", + # "executedQty": "0", + # "avgPrice": "0", + # "marginLocked": "0", + # "type": "LIMIT_MAKER", + # "side": "SELL_CLOSE", + # "timeInForce": "GTC", + # "status": "NEW", + # "priceType": "INPUT", + # "isLiquidationOrder": False, + # "indexPrice": "0", + # "liquidationType": "" + # } + # + else: + raise NotSupported(self.id + ' ' + methodName + '() is not supported for ' + marketType + ' type of markets') + return self.parse_order(response) + + def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders + + https://hashkeyglobal-apidoc.readme.io/reference/cancel-all-open-orders + https://hashkeyglobal-apidoc.readme.io/reference/batch-cancel-futures-order + + :param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.side]: 'buy' or 'sell' + :returns dict: response from exchange + """ + # Does not cancel trigger orders. For canceling trigger order use cancelOrder() or cancelOrders() + methodName = 'cancelAllOrders' + if symbol is None: + raise ArgumentsRequired(self.id + ' ' + methodName + '() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + side = self.safe_string(params, 'side') + if side is not None: + request['side'] = side + response = None + if market['spot']: + response = self.privateDeleteApiV1SpotOpenOrders(self.extend(request, params)) + # + # {"success": True} + # + elif market['swap']: + response = self.privateDeleteApiV1FuturesBatchOrders(self.extend(request, params)) + # + # {"message": "success", "timestamp": "1723127222198", "code": "0000"} + # + else: + raise NotSupported(self.id + ' ' + methodName + '() is not supported for ' + market['type'] + ' type of markets') + order = self.safe_order(response) + order['info'] = response + return [order] + + def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): + """ + cancel multiple orders + + https://hashkeyglobal-apidoc.readme.io/reference/cancel-multiple-orders + https://hashkeyglobal-apidoc.readme.io/reference/batch-cancel-futures-order-by-order-id + + :param str[] ids: order ids + :param str [symbol]: unified market symbol(not used by hashkey) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: 'spot' or 'swap' - the type of the market to fetch entry for(default 'spot') + :returns dict: an list of `order structures ` + """ + methodName = 'cancelOrders' + self.load_markets() + request = {} + orderIds = ','.join(ids) + request['ids'] = orderIds + market: Market = None + if symbol is not None: + market = self.market(symbol) + marketType = 'spot' + marketType, params = self.handle_market_type_and_params(methodName, market, params, marketType) + response = None + if marketType == 'spot': + response = self.privateDeleteApiV1SpotCancelOrderByIds(self.extend(request)) + # + # { + # "code": "0000", + # "result": [] + # } + # + elif marketType == 'swap': + response = self.privateDeleteApiV1FuturesCancelOrderByIds(self.extend(request)) + else: + raise NotSupported(self.id + ' ' + methodName + '() is not supported for ' + marketType + ' type of markets') + order = self.safe_order(response) + order['info'] = response + return [order] + + def fetch_order(self, id: str, symbol: Str = None, params={}) -> Order: + """ + fetches information on an order made by the user + + https://hashkeyglobal-apidoc.readme.io/reference/query-order + https://hashkeyglobal-apidoc.readme.io/reference/get-futures-order + + :param str id: the order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: 'spot' or 'swap' - the type of the market to fetch entry for(default 'spot') + :param str [params.clientOrderId]: a unique id for the order that can be used alternative for the id + :param str [params.accountId]: *spot markets only* account id to fetch the order from + :param bool [params.trigger]: *swap markets only* True for fetching a trigger order(default False) + :param bool [params.stop]: *swap markets only* an alternative for trigger param + :returns dict: An `order structure ` + """ + methodName = 'fetchOrder' + self.check_type_param(methodName, params) + self.load_markets() + request: dict = {} + clientOrderId: Str = None + clientOrderId, params = self.handle_param_string(params, 'clientOrderId') + if clientOrderId is None: + request['orderId'] = id + market: Market = None + if symbol is not None: + market = self.market(symbol) + marketType = 'spot' + marketType, params = self.handle_market_type_and_params(methodName, market, params, marketType) + response = None + if marketType == 'spot': + if clientOrderId is not None: + request['origClientOrderId'] = clientOrderId + response = self.privateGetApiV1SpotOrder(self.extend(request, params)) + # + # { + # "accountId": "1732885739589466112", + # "exchangeId": "301", + # "symbol": "ETHUSDT", + # "symbolName": "ETHUSDT", + # "clientOrderId": "1722004623170558", + # "orderId": "1738695230608169984", + # "price": "0", + # "origQty": "0", + # "executedQty": "0.0061", + # "cummulativeQuoteQty": "19.736489", + # "cumulativeQuoteQty": "19.736489", + # "avgPrice": "3235.49", + # "status": "FILLED", + # "timeInForce": "IOC", + # "type": "MARKET", + # "side": "BUY", + # "stopPrice": "0.0", + # "icebergQty": "0.0", + # "time": "1722004623186", + # "updateTime": "1722004623406", + # "isWorking": True, + # "reqAmount": "20", + # "feeCoin": "", + # "feeAmount": "0", + # "sumFeeAmount": "0" + # } + # + elif marketType == 'swap': + isTrigger = False + isTrigger, params = self.handle_trigger_option_and_params(params, methodName, isTrigger) + if isTrigger: + request['type'] = 'STOP' + response = self.privateGetApiV1FuturesOrder(self.extend(request, params)) + # + # { + # "time": "1722429951611", + # "updateTime": "1722429951700", + # "orderId": "1742263144028363776", + # "clientOrderId": "1722429950315", + # "symbol": "ETHUSDT-PERPETUAL", + # "price": "3460.62", + # "leverage": "5", + # "origQty": "10", + # "executedQty": "10", + # "avgPrice": "3327.52", + # "marginLocked": "0", + # "type": "LIMIT", + # "side": "BUY_OPEN", + # "timeInForce": "IOC", + # "status": "FILLED", + # "priceType": "MARKET", + # "isLiquidationOrder": False, + # "indexPrice": "0", + # "liquidationType": "" + # } + # + else: + raise NotSupported(self.id + ' ' + methodName + '() is not supported for ' + marketType + ' type of markets') + return self.parse_order(response) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://hashkeyglobal-apidoc.readme.io/reference/get-current-open-orders + https://hashkeyglobal-apidoc.readme.io/reference/get-sub-account-open-orders + https://hashkeyglobal-apidoc.readme.io/reference/sub + https://hashkeyglobal-apidoc.readme.io/reference/query-open-futures-orders + + :param str [symbol]: unified market symbol of the market orders were made in - is mandatory for swap markets + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve - default 500, maximum 1000 + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: 'spot' or 'swap' - the type of the market to fetch entries for(default 'spot') + :param str [params.orderId]: *spot markets only* the id of the order to fetch + :param str [params.side]: *spot markets only* 'buy' or 'sell' - the side of the orders to fetch + :param str [params.fromOrderId]: *swap markets only* the id of the order to start from + :param bool [params.trigger]: *swap markets only* True for fetching trigger orders(default False) + :param bool [params.stop]: *swap markets only* an alternative for trigger param + :param str [params.accountId]: account id to fetch the orders from + :returns Order[]: a list of `order structures ` + """ + methodName = 'fetchOpenOrders' + self.check_type_param(methodName, params) + self.load_markets() + market: Market = None + if symbol is not None: + market = self.market(symbol) + marketType = 'spot' + marketType, params = self.handle_market_type_and_params(methodName, market, params, marketType) + params = self.extend({'methodName': methodName}, params) + if marketType == 'spot': + return self.fetch_open_spot_orders(symbol, since, limit, params) + elif marketType == 'swap': + return self.fetch_open_swap_orders(symbol, since, limit, params) + else: + raise NotSupported(self.id + ' ' + methodName + '() is not supported for ' + marketType + ' type of markets') + + def fetch_open_spot_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + @ignore + fetch all unfilled currently open orders for spot markets + + https://hashkeyglobal-apidoc.readme.io/reference/get-current-open-orders + https://hashkeyglobal-apidoc.readme.io/reference/sub + + :param str [symbol]: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve - default 500, maximum 1000 + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.orderId]: the id of the order to fetch + :param str [params.side]: 'buy' or 'sell' - the side of the orders to fetch + :param str [params.accountId]: account id to fetch the orders from + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + methodName = 'fetchOpenSpotOrders' + methodName, params = self.handle_param_string(params, 'methodName', methodName) + market: Market = None + request: dict = {} + response = None + accountId: Str = None + accountId, params = self.handle_option_and_params(params, methodName, 'accountId') + if accountId is not None: + request['subAccountId'] = accountId + response = self.privateGetApiV1SpotSubAccountOpenOrders(self.extend(request, params)) + else: + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if limit is not None: + request['limit'] = limit + response = self.privateGetApiV1SpotOpenOrders(self.extend(request, params)) + # + # [ + # { + # "accountId": "1732885739589466112", + # "exchangeId": "301", + # "symbol": "ETHUSDT", + # "symbolName": "ETHUSDT", + # "clientOrderId": "1", + # "orderId": "1739491435386897152", + # "price": "2000", + # "origQty": "0.001", + # "executedQty": "0", + # "cummulativeQuoteQty": "0", + # "cumulativeQuoteQty": "0", + # "avgPrice": "0", + # "status": "NEW", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "side": "BUY", + # "stopPrice": "0.0", + # "icebergQty": "0.0", + # "time": "1722099538193", + # "updateTime": "1722099538197", + # "isWorking": True, + # "reqAmount": "0" + # } + # ] + # + return self.parse_orders(response, market, since, limit) + + def fetch_open_swap_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + @ignore + fetch all unfilled currently open orders for swap markets + + https://hashkeyglobal-apidoc.readme.io/reference/query-open-futures-orders + https://hashkeyglobal-apidoc.readme.io/reference/get-sub-account-open-orders + + :param str symbol: *is mandatory* unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve - maximum 500 + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.fromOrderId]: the id of the order to start from + :param bool [params.trigger]: True for fetching trigger orders(default False) + :param bool [params.stop]: an alternative for trigger param + :param str [params.accountId]: account id to fetch the orders from + :returns Order[]: a list of `order structures ` + """ + methodName = 'fetchOpenSwapOrders' + methodName, params = self.handle_param_string(params, 'methodName', methodName) + if symbol is None: + raise ArgumentsRequired(self.id + ' ' + methodName + '() requires a symbol argument for swap market orders') + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + isTrigger = False + isTrigger, params = self.handle_trigger_option_and_params(params, methodName, isTrigger) + if isTrigger: + request['type'] = 'STOP' + else: + request['type'] = 'LIMIT' + if limit is not None: + request['limit'] = limit + response = None + accountId: Str = None + accountId, params = self.handle_option_and_params(params, methodName, 'accountId') + if accountId is not None: + request['subAccountId'] = accountId + response = self.privateGetApiV1FuturesSubAccountOpenOrders(self.extend(request, params)) + else: + response = self.privateGetApiV1FuturesOpenOrders(self.extend(request, params)) + # 'LIMIT' + # [ + # { + # "time": "1722432302919", + # "updateTime": "1722432302925", + # "orderId": "1742282868229463040", + # "clientOrderId": "1722432301670", + # "symbol": "ETHUSDT-PERPETUAL", + # "price": "4000", + # "leverage": "5", + # "origQty": "10", + # "executedQty": "0", + # "avgPrice": "0", + # "marginLocked": "0", + # "type": "LIMIT_MAKER", + # "side": "SELL_CLOSE", + # "timeInForce": "GTC", + # "status": "NEW", + # "priceType": "INPUT", + # "isLiquidationOrder": False, + # "indexPrice": "0", + # "liquidationType": "" + # } + # ] + # + # 'STOP' + # [ + # { + # "time": "1722433095688", + # "updateTime": "1722433095688", + # "orderId": "1742289518466225664", + # "accountId": "1735619524953226496", + # "clientOrderId": "1722433094438", + # "symbol": "ETHUSDT-PERPETUAL", + # "price": "3700", + # "leverage": "0", + # "origQty": "10", + # "type": "STOP", + # "side": "SELL_CLOSE", + # "status": "ORDER_NEW", + # "stopPrice": "3600" + # } + # ] + return self.parse_orders(response, market, since, limit) + + def fetch_canceled_and_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple canceled and closed orders made by the user + + https://hashkeyglobal-apidoc.readme.io/reference/get-all-orders + https://hashkeyglobal-apidoc.readme.io/reference/query-futures-history-orders + https://hashkeyglobal-apidoc.readme.io/reference/get-sub-account-history-orders + + :param str symbol: *is mandatory for swap markets* unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve - default 500, maximum 1000 + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch entries for - only supports the last 90 days timeframe + :param str [params.type]: 'spot' or 'swap' - the type of the market to fetch entries for(default 'spot') + :param str [params.orderId]: *spot markets only* the id of the order to fetch + :param str [params.side]: *spot markets only* 'buy' or 'sell' - the side of the orders to fetch + :param str [params.fromOrderId]: *swap markets only* the id of the order to start from + :param bool [params.trigger]: *swap markets only* the id of the order to start from True for fetching trigger orders(default False) + :param bool [params.stop]: *swap markets only* the id of the order to start from an alternative for trigger param + :param str [params.accountId]: account id to fetch the orders from + :returns Order[]: a list of `order structures ` + """ + methodName = 'fetchCanceledAndClosedOrders' + self.check_type_param(methodName, params) + self.load_markets() + request: dict = {} + if limit is not None: + request['limit'] = limit + if since is not None: + request['startTime'] = since + until: Int = None + until, params = self.handle_option_and_params(params, methodName, 'until') + if until is not None: + request['endTime'] = until + accountId: Str = None + accountId, params = self.handle_option_and_params(params, methodName, 'accountId') + market: Market = None + if symbol is not None: + market = self.market(symbol) + marketType = 'spot' + marketType, params = self.handle_market_type_and_params(methodName, market, params, marketType) + response = None + if marketType == 'spot': + if market is not None: + request['symbol'] = market['id'] + if accountId is not None: + request['accountId'] = accountId + response = self.privateGetApiV1SpotTradeOrders(self.extend(request, params)) + # + # [ + # { + # "accountId": "1732885739589466112", + # "exchangeId": "301", + # "symbol": "ETHUSDT", + # "symbolName": "ETHUSDT", + # "clientOrderId": "1722082982086472", + # "orderId": "1739352552762301440", + # "price": "0", + # "origQty": "0.001", + # "executedQty": "0.001", + # "cummulativeQuoteQty": "3.28996", + # "cumulativeQuoteQty": "3.28996", + # "avgPrice": "3289.96", + # "status": "FILLED", + # "timeInForce": "IOC", + # "type": "MARKET", + # "side": "BUY", + # "stopPrice": "0.0", + # "icebergQty": "0.0", + # "time": "1722082982093", + # "updateTime": "1722082982097", + # "isWorking": True, + # "reqAmount": "0" + # }, + # ... + # ] + # + elif marketType == 'swap': + if symbol is None: + raise ArgumentsRequired(self.id + ' ' + methodName + '() requires a symbol argument for swap markets') + request['symbol'] = market['id'] + isTrigger = False + isTrigger, params = self.handle_trigger_option_and_params(params, methodName, isTrigger) + if isTrigger: + request['type'] = 'STOP' + else: + request['type'] = 'LIMIT' + if accountId is not None: + request['subAccountId'] = accountId + response = self.privateGetApiV1FuturesSubAccountHistoryOrders(self.extend(request, params)) + else: + response = self.privateGetApiV1FuturesHistoryOrders(self.extend(request, params)) + # + # [ + # { + # "time": "1722429951611", + # "updateTime": "1722429951700", + # "orderId": "1742263144028363776", + # "clientOrderId": "1722429950315", + # "symbol": "ETHUSDT-PERPETUAL", + # "price": "3460.62", + # "leverage": "5", + # "origQty": "10", + # "executedQty": "10", + # "avgPrice": "3327.52", + # "marginLocked": "0", + # "type": "LIMIT", + # "side": "BUY_OPEN", + # "timeInForce": "IOC", + # "status": "FILLED", + # "priceType": "MARKET", + # "isLiquidationOrder": False, + # "indexPrice": "0", + # "liquidationType": "" + # } + # ] + # + else: + raise NotSupported(self.id + ' ' + methodName + '() is not supported for ' + marketType + ' type of markets') + return self.parse_orders(response, market, since, limit) + + def check_type_param(self, methodName, params): + # some hashkey endpoints have a type param for swap markets that defines the type of an order + # type param is reserved in ccxt for defining the type of the market + # current method warns user if he provides the exchange specific value in type parameter + paramsType = self.safe_string(params, 'type') + if (paramsType is not None) and (paramsType != 'spot') and (paramsType != 'swap'): + raise BadRequest(self.id + ' ' + methodName + '() type parameter can not be "' + paramsType + '". It should define the type of the market("spot" or "swap"). To define the type of an order use the trigger parameter(True for trigger orders)') + + def handle_trigger_option_and_params(self, params: object, methodName: str, defaultValue=None): + isTrigger = defaultValue + isTrigger, params = self.handle_option_and_params_2(params, methodName, 'stop', 'trigger', isTrigger) + return [isTrigger, params] + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder spot + # { + # "accountId": "1732885739589466112", + # "symbol": "ETHUSDT", + # "symbolName": "ETHUSDT", + # "clientOrderId": "1722004623170558", + # "orderId": "1738695230608169984", + # "transactTime": "1722004623186", + # "price": "0", + # "origQty": "0", + # "executedQty": "0.0061", + # "status": "FILLED", + # "timeInForce": "IOC", + # "type": "MARKET", + # "side": "BUY", + # "reqAmount": "20", + # "concentration": "" + # } + # + # fetchOrder spot + # { + # "accountId": "1732885739589466112", + # "exchangeId": "301", + # "symbol": "ETHUSDT", + # "symbolName": "ETHUSDT", + # "clientOrderId": "1722004623170558", + # "orderId": "1738695230608169984", + # "price": "0", + # "origQty": "0", + # "executedQty": "0.0061", + # "cummulativeQuoteQty": "19.736489", + # "cumulativeQuoteQty": "19.736489", + # "avgPrice": "3235.49", + # "status": "FILLED", + # "timeInForce": "IOC", + # "type": "MARKET", + # "side": "BUY", + # "stopPrice": "0.0", + # "icebergQty": "0.0", + # "time": "1722004623186", + # "updateTime": "1722004623406", + # "isWorking": True, + # "reqAmount": "20", + # "feeCoin": "", + # "feeAmount": "0", + # "sumFeeAmount": "0" + # } + # + # cancelOrder + # { + # "accountId": "1732885739589466112", + # "symbol": "ETHUSDT", + # "clientOrderId": "1722006209978370", + # "orderId": "1738708541676585728", + # "transactTime": "1722006209989", + # "price": "5000", + # "origQty": "0.005", + # "executedQty": "0", + # "status": "NEW", + # "timeInForce": "GTC", + # "type": "LIMIT_MAKER", + # "side": "SELL" + # } + # + # createOrder swap + # { + # "time": "1722429951611", + # "updateTime": "1722429951648", + # "orderId": "1742263144028363776", + # "clientOrderId": "1722429950315", + # "symbol": "ETHUSDT-PERPETUAL", + # "price": "3460.62", + # "leverage": "5", + # "origQty": "10", + # "executedQty": "10", + # "avgPrice": "0", + # "marginLocked": "6.9212", + # "type": "LIMIT", + # "side": "BUY_OPEN", + # "timeInForce": "IOC", + # "status": "FILLED", + # "priceType": "MARKET", + # "contractMultiplier": "0.00100000" + # } + # + # fetchOrder swap + # { + # "time": "1722429951611", + # "updateTime": "1722429951700", + # "orderId": "1742263144028363776", + # "clientOrderId": "1722429950315", + # "symbol": "ETHUSDT-PERPETUAL", + # "price": "3460.62", + # "leverage": "5", + # "origQty": "10", + # "executedQty": "10", + # "avgPrice": "3327.52", + # "marginLocked": "0", + # "type": "LIMIT", + # "side": "BUY_OPEN", + # "timeInForce": "IOC", + # "status": "FILLED", + # "priceType": "MARKET", + # "isLiquidationOrder": False, + # "indexPrice": "0", + # "liquidationType": "" + # } + # + marketId = self.safe_string(order, 'symbol') + market = self.safe_market(marketId, market) + timestamp = self.safe_integer_2(order, 'transactTime', 'time') + status = self.safe_string(order, 'status') + type = self.safe_string(order, 'type') + priceType = self.safe_string(order, 'priceType') + if priceType == 'MARKET': + type = 'market' + price = self.omit_zero(self.safe_string(order, 'price')) + if type == 'STOP': + if price is None: + type = 'market' + else: + type = 'limit' + timeInForce = self.safe_string(order, 'timeInForce') + postOnly: Bool = None + type, timeInForce, postOnly = self.parse_order_type_time_in_force_and_post_only(type, timeInForce) + average = self.omit_zero(self.safe_string(order, 'avgPrice')) + if price is None: + price = average + side = self.safe_string_lower(order, 'side') + reduceOnly: Bool = None + side, reduceOnly = self.parse_order_side_and_reduce_only(side) + feeCurrncyId = self.safe_string(order, 'feeCoin') + if feeCurrncyId == '': + feeCurrncyId = None + return self.safe_order({ + 'id': self.safe_string(order, 'orderId'), + 'clientOrderId': self.safe_string(order, 'clientOrderId'), + 'datetime': self.iso8601(timestamp), + 'timestamp': timestamp, + 'lastTradeTimestamp': None, + 'lastUpdateTimestamp': self.safe_integer(order, 'updateTime'), + 'status': self.parse_order_status(status), + 'symbol': market['symbol'], + 'type': type, + 'timeInForce': timeInForce, + 'side': side, + 'price': price, + 'average': average, + 'amount': self.omit_zero(self.safe_string(order, 'origQty')), + 'filled': self.safe_string(order, 'executedQty'), + 'remaining': None, + 'triggerPrice': self.omit_zero(self.safe_string(order, 'stopPrice')), + 'takeProfitPrice': None, + 'stopLossPrice': None, + 'cost': self.omit_zero(self.safe_string_2(order, 'cumulativeQuoteQty', 'cummulativeQuoteQty')), + 'trades': None, + 'fee': { + 'currency': self.safe_currency_code(feeCurrncyId), + 'amount': self.omit_zero(self.safe_string(order, 'feeAmount')), + }, + 'reduceOnly': reduceOnly, + 'postOnly': postOnly, + 'info': order, + }, market) + + def parse_order_side_and_reduce_only(self, unparsed): + parts = unparsed.split('_') + side = parts[0] + reduceOnly: Bool = None + secondPart = self.safe_string(parts, 1) + if secondPart is not None: + if secondPart == 'open': + reduceOnly = False + elif (secondPart == 'close'): + reduceOnly = True + return [side, reduceOnly] + + def parse_order_status(self, status): + statuses = { + 'NEW': 'open', + 'PARTIALLY_FILLED': 'open', + 'PARTIALLY_CANCELED': 'canceled', + 'FILLED': 'closed', + 'CANCELED': 'canceled', + 'ORDER_CANCELED': 'canceled', + 'PENDING_CANCEL': 'canceled', + 'REJECTED': 'rejected', + 'ORDER_NEW': 'open', + } + return self.safe_string(statuses, status, status) + + def parse_order_type_time_in_force_and_post_only(self, type, timeInForce): + postOnly: Bool = None + if type == 'LIMIT_MAKER': + postOnly = True + elif (timeInForce == 'LIMIT_MAKER') or (timeInForce == 'MAKER'): + postOnly = True + timeInForce = 'PO' + type = self.parse_order_type(type) + return [type, timeInForce, postOnly] + + def parse_order_type(self, type): + types = { + 'MARKET': 'market', + 'LIMIT': 'limit', + 'LIMIT_MAKER': 'limit', + 'MARKET_OF_BASE': 'market', + } + return self.safe_string(types, type, type) + + def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate + + https://hashkeyglobal-apidoc.readme.io/reference/get-futures-funding-rate + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'timestamp': self.milliseconds(), + } + response = self.publicGetApiV1FuturesFundingRate(self.extend(request, params)) + # + # [ + # {"symbol": "ETHUSDT-PERPETUAL", "rate": "0.0001", "nextSettleTime": "1722297600000"} + # ] + # + rate = self.safe_dict(response, 0, {}) + return self.parse_funding_rate(rate, market) + + def fetch_funding_rates(self, symbols: Strings = None, params={}) -> FundingRates: + """ + fetch the funding rate for multiple markets + + https://hashkeyglobal-apidoc.readme.io/reference/get-futures-funding-rate + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `funding rate structures `, indexed by market symbols + """ + self.load_markets() + symbols = self.market_symbols(symbols) + request: dict = { + 'timestamp': self.milliseconds(), + } + response = self.publicGetApiV1FuturesFundingRate(self.extend(request, params)) + # + # [ + # {"symbol": "BTCUSDT-PERPETUAL", "rate": "0.0001", "nextSettleTime": "1722297600000"}, + # {"symbol": "ETHUSDT-PERPETUAL", "rate": "0.0001", "nextSettleTime": "1722297600000"} + # ] + # + return self.parse_funding_rates(response, symbols) + + def parse_funding_rate(self, contract, market: Market = None) -> FundingRate: + # + # { + # "symbol": "ETHUSDT-PERPETUAL", + # "rate": "0.0001", + # "nextSettleTime": "1722297600000" + # } + # + marketId = self.safe_string(contract, 'symbol') + market = self.safe_market(marketId, market, None, 'swap') + fundingRate = self.safe_number(contract, 'rate') + fundingTimestamp = self.safe_integer(contract, 'nextSettleTime') + return { + 'info': contract, + 'symbol': market['symbol'], + 'markPrice': None, + 'indexPrice': None, + 'interestRate': None, + 'estimatedSettlePrice': None, + 'timestamp': None, + 'datetime': None, + 'fundingRate': fundingRate, + 'fundingTimestamp': None, + 'fundingDatetime': None, + 'nextFundingRate': None, + 'nextFundingTimestamp': fundingTimestamp, + 'nextFundingDatetime': self.iso8601(fundingTimestamp), + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': None, + } + + def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical funding rate prices + + https://hashkeyglobal-apidoc.readme.io/reference/get-futures-history-funding-rate + + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: timestamp in ms of the earliest funding rate to fetch + :param int [limit]: the maximum amount of `funding rate structures ` to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.fromId]: the id of the entry to start from + :param int [params.endId]: the id of the entry to end with + :returns dict[]: a list of `funding rate structures ` + """ + self.load_markets() + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchFundingRateHistory() requires a symbol argument') + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['limit'] = limit + response = self.publicGetApiV1FuturesHistoryFundingRate(self.extend(request, params)) + # + # [ + # { + # "id": "10698", + # "symbol": "ETHUSDT-PERPETUAL", + # "settleTime": "1722268800000", + # "settleRate": "0.0001" + # }, + # ... + # ] + # + rates = [] + for i in range(0, len(response)): + entry = response[i] + timestamp = self.safe_integer(entry, 'settleTime') + rates.append({ + 'info': entry, + 'symbol': self.safe_symbol(self.safe_string(entry, 'symbol'), market, None, 'swap'), + 'fundingRate': self.safe_number(entry, 'settleRate'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + sorted = self.sort_by(rates, 'timestamp') + return self.filter_by_since_limit(sorted, since, limit) + + def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch open positions for a market + + https://hashkeyglobal-apidoc.readme.io/reference/get-futures-positions + + fetch all open positions + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.side]: 'LONG' or 'SHORT' - the direction of the position(if not provided, positions for both sides will be returned) + :returns dict[]: a list of `position structure ` + """ + methodName = 'fetchPositions' + if (symbols is None): + raise ArgumentsRequired(self.id + ' ' + methodName + '() requires a symbol argument with one single market symbol') + else: + symbolsLength = len(symbols) + if symbolsLength != 1: + raise NotSupported(self.id + ' ' + methodName + '() is supported for a symbol argument with one single market symbol only') + self.load_markets() + return self.fetch_positions_for_symbol(symbols[0], self.extend({'methodName': 'fetchPositions'}, params)) + + def fetch_positions_for_symbol(self, symbol: str, params={}) -> List[Position]: + """ + fetch open positions for a single market + + https://hashkeyglobal-apidoc.readme.io/reference/get-futures-positions + + fetch all open positions for specific symbol + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.side]: 'LONG' or 'SHORT' - the direction of the position(if not provided, positions for both sides will be returned) + :returns dict[]: a list of `position structure ` + """ + self.load_markets() + market = self.market(symbol) + methodName = 'fetchPosition' + methodName, params = self.handle_param_string(params, 'methodName', methodName) + if not market['swap']: + raise NotSupported(self.id + ' ' + methodName + '() supports swap markets only') + request: dict = { + 'symbol': market['id'], + } + response = self.privateGetApiV1FuturesPositions(self.extend(request, params)) + # + # [ + # { + # "symbol": "ETHUSDT-PERPETUAL", + # "side": "LONG", + # "avgPrice": "3327.52", + # "position": "10", + # "available": "0", + # "leverage": "5", + # "lastPrice": "3324.44", + # "positionValue": "33.2752", + # "liquidationPrice": "-953.83", + # "margin": "6.9012", + # "marginRate": "", + # "unrealizedPnL": "-0.0288", + # "profitRate": "-0.0041", + # "realizedPnL": "-0.0199", + # "minMargin": "0.2173" + # } + # ] + # + return self.parse_positions(response, [symbol]) + + def parse_position(self, position: dict, market: Market = None): + marketId = self.safe_string(position, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + return self.safe_position({ + 'symbol': symbol, + 'id': None, + 'timestamp': None, + 'datetime': None, + 'contracts': self.safe_number(position, 'position'), + 'contractSize': None, + 'side': self.safe_string_lower(position, 'side'), + 'notional': self.safe_number(position, 'positionValue'), + 'leverage': self.safe_integer(position, 'leverage'), + 'unrealizedPnl': self.safe_number(position, 'unrealizedPnL'), + 'realizedPnl': self.safe_number(position, 'realizedPnL'), + 'collateral': None, + 'entryPrice': self.safe_number(position, 'avgPrice'), + 'markPrice': None, + 'liquidationPrice': self.safe_number(position, 'liquidationPrice'), + 'marginMode': 'cross', + 'hedged': True, + 'maintenanceMargin': self.safe_number(position, 'minMargin'), + 'maintenanceMarginPercentage': None, + 'initialMargin': self.safe_number(position, 'margin'), + 'initialMarginPercentage': None, + 'marginRatio': None, + 'lastUpdateTimestamp': None, + 'lastPrice': self.safe_number(position, 'lastPrice'), + 'stopLossPrice': None, + 'takeProfitPrice': None, + 'percentage': None, + 'info': position, + }) + + def fetch_leverage(self, symbol: str, params={}) -> Leverage: + """ + fetch the set leverage for a market + + https://hashkeyglobal-apidoc.readme.io/reference/query-futures-leverage-trade + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `leverage structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = self.privateGetApiV1FuturesLeverage(self.extend(request, params)) + # + # [ + # { + # "symbolId": "ETHUSDT-PERPETUAL", + # "leverage": "5", + # "marginType": "CROSS" + # } + # ] + # + leverage = self.safe_dict(response, 0, {}) + return self.parse_leverage(leverage, market) + + def parse_leverage(self, leverage: dict, market: Market = None) -> Leverage: + marginMode = self.safe_string_lower(leverage, 'marginType') + leverageValue = self.safe_number(leverage, 'leverage') + return { + 'info': leverage, + 'symbol': market['symbol'], + 'marginMode': marginMode, + 'longLeverage': leverageValue, + 'shortLeverage': leverageValue, + } + + def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://hashkeyglobal-apidoc.readme.io/reference/change-futures-leverage-trade + + :param float leverage: the rate of leverage + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setLeverage() requires a symbol argument') + self.load_markets() + request: dict = { + 'leverage': leverage, + } + market = self.market(symbol) + request['symbol'] = market['id'] + response = self.privatePostApiV1FuturesLeverage(self.extend(request, params)) + # + # { + # "code": "0000", + # "symbolId": "ETHUSDT-PERPETUAL", + # "leverage": "3" + # } + # + return self.parse_leverage(response, market) + + def fetch_leverage_tiers(self, symbols: Strings = None, params={}) -> LeverageTiers: + """ + retrieve information on the maximum leverage, and maintenance margin for trades of varying trade sizes + + https://hashkeyglobal-apidoc.readme.io/reference/exchangeinfo + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `leverage tiers structures `, indexed by market symbols + """ + self.load_markets() + response = self.publicGetApiV1ExchangeInfo(params) + # response is the same fetchMarkets() + data = self.safe_list(response, 'contracts', []) + symbols = self.market_symbols(symbols) + return self.parse_leverage_tiers(data, symbols, 'symbol') + + def parse_market_leverage_tiers(self, info, market: Market = None) -> List[LeverageTier]: + # + # { + # "filters": [ + # { + # "minPrice": "0.1", + # "maxPrice": "100000.00000000", + # "tickSize": "0.1", + # "filterType": "PRICE_FILTER" + # }, + # { + # "minQty": "0.001", + # "maxQty": "10", + # "stepSize": "0.001", + # "marketOrderMinQty": "0", + # "marketOrderMaxQty": "0", + # "filterType": "LOT_SIZE" + # }, + # { + # "minNotional": "0", + # "filterType": "MIN_NOTIONAL" + # }, + # { + # "maxSellPrice": "999999", + # "buyPriceUpRate": "0.05", + # "sellPriceDownRate": "0.05", + # "maxEntrustNum": 200, + # "maxConditionNum": 200, + # "filterType": "LIMIT_TRADING" + # }, + # { + # "buyPriceUpRate": "0.05", + # "sellPriceDownRate": "0.05", + # "filterType": "MARKET_TRADING" + # }, + # { + # "noAllowMarketStartTime": "0", + # "noAllowMarketEndTime": "0", + # "limitOrderStartTime": "0", + # "limitOrderEndTime": "0", + # "limitMinPrice": "0", + # "limitMaxPrice": "0", + # "filterType": "OPEN_QUOTE" + # } + # ], + # "exchangeId": "301", + # "symbol": "BTCUSDT-PERPETUAL", + # "symbolName": "BTCUSDT-PERPETUAL", + # "status": "TRADING", + # "baseAsset": "BTCUSDT-PERPETUAL", + # "baseAssetPrecision": "0.001", + # "quoteAsset": "USDT", + # "quoteAssetPrecision": "0.1", + # "icebergAllowed": False, + # "inverse": False, + # "index": "USDT", + # "marginToken": "USDT", + # "marginPrecision": "0.0001", + # "contractMultiplier": "0.001", + # "underlying": "BTC", + # "riskLimits": [ + # { + # "riskLimitId": "200000722", + # "quantity": "1000.00", + # "initialMargin": "0.10", + # "maintMargin": "0.005", + # "isWhite": False + # }, + # { + # "riskLimitId": "200000723", + # "quantity": "2000.00", + # "initialMargin": "0.10", + # "maintMargin": "0.01", + # "isWhite": False + # } + # ] + # } + # + riskLimits = self.safe_list(info, 'riskLimits', []) + marketId = self.safe_string(info, 'symbol') + market = self.safe_market(marketId, market) + tiers = [] + for i in range(0, len(riskLimits)): + tier = riskLimits[i] + initialMarginRate = self.safe_string(tier, 'initialMargin') + tiers.append({ + 'tier': self.sum(i, 1), + 'symbol': self.safe_symbol(marketId, market), + 'currency': market['settle'], + 'minNotional': None, + 'maxNotional': self.safe_number(tier, 'quantity'), + 'maintenanceMarginRate': self.safe_number(tier, 'maintMargin'), + 'maxLeverage': self.parse_number(Precise.string_div('1', initialMarginRate)), + 'info': tier, + }) + return tiers + + def fetch_trading_fee(self, symbol: str, params={}) -> TradingFeeInterface: + """ + fetch the trading fees for a market + + https://developers.binance.com/docs/wallet/asset/trade-fee # spot + https://hashkeyglobal-apidoc.readme.io/reference/get-futures-commission-rate-request-weight # swap + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `fee structure ` + """ + self.load_markets() + market = self.market(symbol) + methodName = 'fetchTradingFee' + response = None + if market['spot']: + response = self.fetch_trading_fees(params) + return self.safe_dict(response, symbol) + elif market['swap']: + response = self.privateGetApiV1FuturesCommissionRate(self.extend({'symbol': market['id']}, params)) + return self.parse_trading_fee(response, market) + # + # { + # "openMakerFee": "0.00025", + # "openTakerFee": "0.0006", + # "closeMakerFee": "0.00025", + # "closeTakerFee": "0.0006" + # } + # + else: + raise NotSupported(self.id + ' ' + methodName + '() is not supported for ' + market['type'] + ' type of markets') + + def fetch_trading_fees(self, params={}) -> TradingFees: + """ + *for spot markets only* fetch the trading fees for multiple markets + + https://developers.binance.com/docs/wallet/asset/trade-fee + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + self.load_markets() + response = self.privateGetApiV1AccountVipInfo(params) + # + # { + # "code": 0, + # "vipLevel": "0", + # "tradeVol30Day": "67", + # "totalAssetBal": "0", + # "data": [ + # { + # "symbol": "UXLINKUSDT", + # "productType": "Token-Token", + # "buyMakerFeeCurrency": "UXLINK", + # "buyTakerFeeCurrency": "UXLINK", + # "sellMakerFeeCurrency": "USDT", + # "sellTakerFeeCurrency": "USDT", + # "actualMakerRate": "0.0012", + # "actualTakerRate": "0.0012" + # }, + # ... + # ], + # "updateTimestamp": "1722320137809" + # } + # + data = self.safe_list(response, 'data', []) + result: dict = {} + for i in range(0, len(data)): + fee = self.safe_dict(data, i, {}) + parsedFee = self.parse_trading_fee(fee) + result[parsedFee['symbol']] = parsedFee + return result + + def parse_trading_fee(self, fee: dict, market: Market = None) -> TradingFeeInterface: + # + # spot + # { + # "symbol": "UXLINKUSDT", + # "productType": "Token-Token", + # "buyMakerFeeCurrency": "UXLINK", + # "buyTakerFeeCurrency": "UXLINK", + # "sellMakerFeeCurrency": "USDT", + # "sellTakerFeeCurrency": "USDT", + # "actualMakerRate": "0.0012", + # "actualTakerRate": "0.0012" + # } + # + # swap + # { + # "openMakerFee": "0.00025", + # "openTakerFee": "0.0006", + # "closeMakerFee": "0.00025", + # "closeTakerFee": "0.0006" + # } + # + marketId = self.safe_string(fee, 'symbol') + market = self.safe_market(marketId, market) + return { + 'info': fee, + 'symbol': market['symbol'], + 'maker': self.safe_number_2(fee, 'openMakerFee', 'actualMakerRate'), + 'taker': self.safe_number_2(fee, 'openTakerFee', 'actualTakerRate'), + 'percentage': True, + 'tierBased': True, + } + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = self.urls['api'][api] + '/' + path + query: Str = None + if api == 'private': + self.check_required_credentials() + timestamp = self.milliseconds() + additionalParams = { + 'timestamp': timestamp, + } + recvWindow = self.safe_integer(self.options, 'recvWindow') + if recvWindow is not None: + additionalParams['recvWindow'] = recvWindow + headers = { + 'X-HK-APIKEY': self.apiKey, + 'Content-Type': 'application/x-www-form-urlencoded', + } + signature: Str = None + if (method == 'POST') and ((path == 'api/v1/spot/batchOrders') or (path == 'api/v1/futures/batchOrders')): + headers['Content-Type'] = 'application/json' + body = self.json(self.safe_list(params, 'orders')) + signature = self.hmac(self.encode(self.custom_urlencode(additionalParams)), self.encode(self.secret), hashlib.sha256) + query = self.custom_urlencode(self.extend(additionalParams, {'signature': signature})) + url += '?' + query + else: + totalParams = self.extend(additionalParams, params) + signature = self.hmac(self.encode(self.custom_urlencode(totalParams)), self.encode(self.secret), hashlib.sha256) + totalParams['signature'] = signature + query = self.custom_urlencode(totalParams) + if method == 'GET': + url += '?' + query + else: + body = query + headers['INPUT-SOURCE'] = self.safe_string(self.options, 'broker', '10000700011') + headers['broker_sign'] = signature + else: + query = self.urlencode(params) + if len(query) != 0: + url += '?' + query + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def custom_urlencode(self, params: dict = {}) -> Str: + result = self.urlencode(params) + result = result.replace('%2C', ',') + return result + + def handle_errors(self, code, reason, url, method, headers, body, response, requestHeaders, requestBody): + if response is None: + return None + errorInArray = False + responseCodeString = self.safe_string(response, 'code', None) + responseCodeInteger = self.safe_integer(response, 'code', None) # some codes in response are returned as '0000' others + if responseCodeInteger == 0: + result = self.safe_list(response, 'result', []) # for batch methods + for i in range(0, len(result)): + entry = self.safe_dict(result, i) + entryCodeInteger = self.safe_integer(entry, 'code') + if entryCodeInteger != 0: + errorInArray = True + responseCodeString = self.safe_string(entry, 'code') + if (code != 200) or errorInArray: + feedback = self.id + ' ' + body + self.throw_broadly_matched_exception(self.exceptions['broad'], responseCodeString, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], responseCodeString, feedback) + raise ExchangeError(feedback) + return None diff --git a/ccxt/hibachi.py b/ccxt/hibachi.py new file mode 100644 index 0000000..799129a --- /dev/null +++ b/ccxt/hibachi.py @@ -0,0 +1,2072 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.hibachi import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currencies, Currency, DepositAddress, Int, LedgerEntry, Market, Num, Order, OrderBook, OrderRequest, OrderSide, OrderType, Position, Str, Strings, Ticker, FundingRate, Trade, TradingFees, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import BadRequest +from ccxt.base.errors import OrderNotFound +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class hibachi(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(hibachi, self).describe(), { + 'id': 'hibachi', + 'name': 'Hibachi', + 'countries': ['US'], + 'rateLimit': 100, + 'userAgent': self.userAgents['chrome'], + 'certified': False, + 'pro': False, + 'dex': True, + 'has': { + 'CORS': None, + 'spot': False, + 'margin': False, + 'swap': True, + 'future': False, + 'option': False, + 'addMargin': False, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'cancelOrders': True, + 'cancelWithdraw': False, + 'closeAllPositions': False, + 'closePosition': False, + 'createConvertTrade': False, + 'createDepositAddress': False, + 'createMarketBuyOrderWithCost': False, + 'createMarketOrder': False, + 'createMarketOrderWithCost': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createOrders': True, + 'createOrderWithTakeProfitAndStopLoss': False, + 'createReduceOnlyOrder': False, + 'createStopLimitOrder': False, + 'createStopLossOrder': False, + 'createStopMarketOrder': False, + 'createStopOrder': False, + 'createTakeProfitOrder': False, + 'createTrailingAmountOrder': False, + 'createTrailingPercentOrder': False, + 'createTriggerOrder': False, + 'editOrder': True, + 'editOrders': True, + 'fetchAccounts': False, + 'fetchBalance': True, + 'fetchCanceledOrders': False, + 'fetchClosedOrder': False, + 'fetchClosedOrders': False, + 'fetchConvertCurrencies': False, + 'fetchConvertQuote': False, + 'fetchCurrencies': False, + 'fetchDepositAddress': True, + 'fetchDeposits': True, + 'fetchDepositsWithdrawals': False, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': True, + 'fetchFundingRateHistory': True, + 'fetchFundingRates': False, + 'fetchIndexOHLCV': False, + 'fetchLedger': True, + 'fetchLeverage': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarkets': True, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': True, + 'fetchOpenInterestHistory': False, + 'fetchOpenOrder': False, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': False, + 'fetchOrderTrades': False, + 'fetchPosition': False, + 'fetchPositionMode': False, + 'fetchPositions': True, + 'fetchPremiumIndexOHLCV': False, + 'fetchStatus': False, + 'fetchTicker': True, + 'fetchTickers': False, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': True, + 'fetchTradingLimits': False, + 'fetchTransactions': 'emulated', + 'fetchTransfers': False, + 'fetchWithdrawals': True, + 'reduceMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setPositionMode': False, + 'transfer': False, + 'withdraw': True, + }, + 'timeframes': { + '1m': '1min', + '5m': '5min', + '15m': '15min', + '1h': '1h', + '4h': '4h', + '1d': '1d', + '1w': '1w', + }, + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/7301bbb1-4f27-4167-8a55-75f74b14e973', + 'api': { + 'public': 'https://data-api.hibachi.xyz', + 'private': 'https://api.hibachi.xyz', + }, + 'www': 'https://www.hibachi.xyz/', + 'referral': { + 'url': 'hibachi.xyz/r/ZBL2YFWIHU', + }, + }, + 'api': { + 'public': { + 'get': { + 'market/exchange-info': 1, + 'market/data/trades': 1, + 'market/data/prices': 1, + 'market/data/stats': 1, + 'market/data/klines': 1, + 'market/data/orderbook': 1, + 'market/data/open-interest': 1, + 'market/data/funding-rates': 1, + 'exchange/utc-timestamp': 1, + }, + }, + 'private': { + 'get': { + 'capital/deposit-info': 1, + 'capital/history': 1, + 'trade/account/trading_history': 1, + 'trade/account/info': 1, + 'trade/order': 1, + 'trade/account/trades': 1, + 'trade/orders': 1, + }, + 'put': { + 'trade/order': 1, + }, + 'delete': { + 'trade/order': 1, + 'trade/orders': 1, + }, + 'post': { + 'trade/order': 1, + 'trade/orders': 1, + 'capital/withdraw': 1, + }, + }, + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': False, + 'accountId': True, + 'privateKey': True, + }, + 'fees': { + 'trading': { + 'tierBased': False, + 'percentage': True, + 'maker': self.parse_number('0.00015'), + 'taker': self.parse_number('0.00045'), + }, + }, + 'currencies': self.hardcoded_currencies(), + 'options': { + }, + 'features': { + 'default': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerPriceType': None, + 'triggerDirection': None, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': False, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': False, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': None, + 'daysBack': None, + 'untilDays': None, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': None, + 'fetchOHLCV': { + 'limit': None, + }, + }, + 'swap': { + 'linear': { + 'extends': 'default', + }, + 'inverse': None, + }, + 'future': { + 'linear': { + 'extends': 'default', + }, + 'inverse': None, + }, + }, + 'commonCurrencies': {}, + 'exceptions': { + 'exact': { + '2': BadRequest, # {"errorCode":2,"message":"Invalid signature: Failed to verify signature"} + '3': OrderNotFound, # {"errorCode":3,"message":"Not found: order ID 33","status":"failed"} + '4': BadRequest, # {"errorCode":4,"message":"Missing accountId","status":"failed"} + }, + 'broad': { + }, + }, + 'precisionMode': TICK_SIZE, + }) + + def get_account_id(self): + self.check_required_credentials() + id = self.parse_to_int(self.accountId) + return id + + def parse_market(self, market: dict) -> Market: + marketId = self.safe_string(market, 'symbol') + numericId = self.safe_number(market, 'id') + marketType = 'swap' + baseId = self.safe_string(market, 'underlyingSymbol') + quoteId = self.safe_string(market, 'settlementSymbol') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + settleId: Str = self.safe_string(market, 'settlementSymbol') + settle: Str = self.safe_currency_code(settleId) + symbol = base + '/' + quote + ':' + settle + created = self.safe_integer_product(market, 'marketCreationTimestamp', 1000) + return { + 'id': marketId, + 'numericId': numericId, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': marketType, + 'spot': False, + 'margin': False, + 'swap': True, + 'future': False, + 'option': False, + 'active': self.safe_string(market, 'status') == 'LIVE', + 'contract': True, + 'linear': True, + 'inverse': False, + 'contractSize': self.parse_number('1'), + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(self.parse_precision(self.safe_string(market, 'underlyingDecimals'))), + 'price': self.parse_number(self.safe_list(market, 'orderbookGranularities')[0]) / 10000.0, + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': None, + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': self.safe_number(market, 'minNotional'), + 'max': None, + }, + }, + 'created': created, + 'info': market, + } + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for hibachi + + https://api-doc.hibachi.xyz/#183981da-8df5-40a0-a155-da15015dd536 + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = self.publicGetMarketExchangeInfo(params) + # { + # "displayName": "ETH/USDT Perps", + # "id": 1, + # "maintenanceFactorForPositions": "0.030000", + # "marketCloseTimestamp": null, + # "marketOpenTimestamp": null, + # "minNotional": "1", + # "minOrderSize": "0.000000001", + # "orderbookGranularities": [ + # "0.01", + # "0.1", + # "1", + # "10" + # ], + # "riskFactorForOrders": "0.066667", + # "riskFactorForPositions": "0.030000", + # "settlementDecimals": 6, + # "settlementSymbol": "USDT", + # "status": "LIVE", + # "stepSize": "0.000000001", + # "symbol": "ETH/USDT-P", + # "tickSize": "0.000001", + # "underlyingDecimals": 9, + # "underlyingSymbol": "ETH" + # }, + rows = self.safe_list(response, 'futureContracts') + return self.parse_markets(rows) + + def hardcoded_currencies(self) -> Currencies: + # Hibachi only supports USDT on Arbitrum at self time + # We don't have an API endpoint to expose self information yet + result: dict = {} + networks: dict = {} + networkId = 'ARBITRUM' + networks[networkId] = { + 'id': networkId, + 'network': networkId, + 'limits': { + 'withdraw': { + 'min': None, + 'max': None, + }, + 'deposit': { + 'min': None, + 'max': None, + }, + }, + 'active': None, + 'deposit': None, + 'withdraw': None, + 'info': {}, + } + code = self.safe_currency_code('USDT') + result[code] = self.safe_currency_structure({ + 'id': 'USDT', + 'name': 'USDT', + 'type': 'fiat', + 'code': code, + 'precision': self.parse_number('0.000001'), + 'active': True, + 'fee': None, + 'networks': networks, + 'deposit': True, + 'withdraw': True, + 'limits': { + 'deposit': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + }, + 'info': {}, + }) + return result + + def parse_balance(self, response) -> Balances: + result: dict = { + 'info': response, + } + # Hibachi only supports USDT on Arbitrum at self time + code = self.safe_currency_code('USDT') + account = self.account() + account['total'] = self.safe_string(response, 'balance') + account['free'] = self.safe_string(response, 'maximalWithdraw') + result[code] = account + return self.safe_balance(result) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://api-doc.hibachi.xyz/#69aafedb-8274-4e21-bbaf-91dace8b8f31 + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + request: dict = { + 'accountId': self.get_account_id(), + } + response = self.privateGetTradeAccountInfo(self.extend(request, params)) + # + # { + # assets: [{quantity: '3.000000', symbol: 'USDT'}], + # balance: '3.000000', + # maximalWithdraw: '3.000000', + # numFreeTransfersRemaining: '100', + # positions: [], + # totalOrderNotional: '0.000000', + # totalPositionNotional: '0.000000', + # totalUnrealizedFundingPnl: '0.000000', + # totalUnrealizedPnl: '0.000000', + # totalUnrealizedTradingPnl: '0.000000', + # tradeMakerFeeRate: '0.00000000', + # tradeTakerFeeRate: '0.00020000' + # } + # + return self.parse_balance(response) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + prices = self.safe_dict(ticker, 'prices') + stats = self.safe_dict(ticker, 'stats') + bid = self.safe_number(prices, 'bidPrice') + ask = self.safe_number(prices, 'askPrice') + last = self.safe_number(prices, 'tradePrice') + high = self.safe_number(stats, 'high24h') + low = self.safe_number(stats, 'low24h') + volume = self.safe_number(stats, 'volume24h') + return self.safe_ticker({ + 'symbol': self.safe_symbol(None, market), + 'timestamp': None, + 'datetime': None, + 'bid': bid, + 'ask': ask, + 'last': last, + 'high': high, + 'low': low, + 'bidVolume': None, + 'askVolume': None, + 'vwap': None, + 'open': None, + 'close': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': None, + 'quoteVolume': volume, + 'info': ticker, + }, market) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # public fetchTrades: + # { + # "price": "3512.431902", + # "quantity": "1.414780098", + # "takerSide": "Buy", + # "timestamp": 1712692147 + # } + # + # private fetchMyTrades: + # { + # "askAccountId": 221, + # "askOrderId": 589168494921909200, + # "bidAccountId": 132, + # "bidOrderId": 589168494829895700, + # "fee": "0.000477", + # "id": 199511136, + # "orderType": "MARKET", + # "price": "119257.90000", + # "quantity": "0.0000200000", + # "realizedPnl": "-0.000352", + # "side": "Sell", + # "symbol": "BTC/USDT-P", + # "timestamp": 1752543391 + # } + marketId = self.safe_string(trade, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + id = self.safe_string(trade, 'id') + price = self.safe_string(trade, 'price') + amount = self.safe_string(trade, 'quantity') + timestamp = self.safe_integer_product(trade, 'timestamp', 1000) + cost = Precise.string_mul(price, amount) + side = None + fee = None + orderType = None + orderId = None + takerOrMaker = None + if id is None: + # public trades + side = self.safe_string_lower(trade, 'takerSide') + takerOrMaker = 'taker' + else: + # private trades + side = self.safe_string_lower(trade, 'side') + fee = {'cost': self.safe_string(trade, 'fee'), 'currency': 'USDT'} + orderType = self.safe_string_lower(trade, 'orderType') + if side == 'buy': + orderId = self.safe_string(trade, 'bidOrderId') + else: + orderId = self.safe_string(trade, 'askOrderId') + return self.safe_trade({ + 'id': id, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'side': side, + 'price': price, + 'amount': amount, + 'cost': cost, + 'order': orderId, + 'takerOrMaker': takerOrMaker, + 'type': orderType, + 'fee': fee, + 'info': trade, + }, market) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://api-doc.hibachi.xyz/#86a53bc1-d3bb-4b93-8a11-7034d4698caa + + :param str symbol: unified market symbol + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch(maximum value is 100) + :param dict [params]: extra parameters specific to the hibachi api endpoint + :returns dict[]: a list of recent [trade structures] + """ + self.load_markets() + market = self.market(symbol) + request = { + 'symbol': market['id'], + } + response = self.publicGetMarketDataTrades(self.extend(request, params)) + # + # { + # "trades": [ + # { + # "price": "111091.38352", + # "quantity": "0.0090090093", + # "takerSide": "Buy", + # "timestamp": 1752095479 + # }, + # ] + # } + # + trades = self.safe_list(response, 'trades', []) + return self.parse_trades(trades, market) + + def fetch_ticker(self, symbol: Str, params={}) -> Ticker: + """ + + https://api-doc.hibachi.xyz/#4abb30c4-e5c7-4b0f-9ade-790111dbfa47 + + fetches a price ticker and the related information for the past 24h + :param str symbol: unified symbol of the market + :param dict [params]: extra parameters specific to the hibachi api endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + rawPromises = [ + self.publicGetMarketDataPrices(self.extend(request, params)), + self.publicGetMarketDataStats(self.extend(request, params)), + ] + promises = rawPromises + pricesResponse = promises[0] + # { + # "askPrice": "3514.650296", + # "bidPrice": "3513.596112", + # "fundingRateEstimation": { + # "estimatedFundingRate": "0.000001", + # "nextFundingTimestamp": 1712707200 + # }, + # "markPrice": "3514.288858", + # "spotPrice": "3514.715000", + # "symbol": "ETH/USDT-P", + # "tradePrice": "2372.746570" + # } + statsResponse = promises[1] + # { + # "high24h": "3819.507827", + # "low24h": "3754.474162", + # "symbol": "ETH/USDT-P", + # "volume24h": "23554.858590416" + # } + ticker = { + 'prices': pricesResponse, + 'stats': statsResponse, + } + return self.parse_ticker(ticker, market) + + def parse_order_status(self, status: str) -> str: + statuses: dict = { + 'PENDING': 'open', + 'CHILD_PENDING': 'open', + 'SCHEDULED_TWAP': 'open', + 'PLACED': 'open', + 'PARTIALLY_FILLED': 'open', + 'FILLED': 'closed', + 'CANCELLED': 'canceled', + 'REJECTED': 'rejected', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + marketId = self.safe_string(order, 'symbol') + market = self.safe_market(marketId, market) + status = self.safe_string(order, 'status') + type = self.safe_string_lower(order, 'orderType') + price = self.safe_string(order, 'price') + rawSide = self.safe_string(order, 'side') + side = None + if rawSide == 'BID': + side = 'buy' + elif rawSide == 'ASK': + side = 'sell' + amount = self.safe_string(order, 'totalQuantity') + remaining = self.safe_string(order, 'availableQuantity') + totalQuantity = self.safe_string(order, 'totalQuantity') + availableQuantity = self.safe_string(order, 'availableQuantity') + filled = None + if totalQuantity is not None and availableQuantity is not None: + filled = Precise.string_sub(totalQuantity, availableQuantity) + timeInForce = 'GTC' + orderFlags = self.safe_value(order, 'orderFlags') + postOnly = False + reduceOnly = False + if orderFlags == 'POST_ONLY': + timeInForce = 'PO' + postOnly = True + elif orderFlags == 'IOC': + timeInForce = 'IOC' + elif orderFlags == 'REDUCE_ONLY': + reduceOnly = True + return self.safe_order({ + 'info': order, + 'id': self.safe_string(order, 'orderId'), + 'clientOrderId': None, + 'datetime': None, + 'timestamp': None, + 'lastTradeTimestamp': None, + 'lastUpdateTimestamp': None, + 'status': self.parse_order_status(status), + 'symbol': market['symbol'], + 'type': type, + 'timeInForce': timeInForce, + 'side': side, + 'price': price, + 'average': None, + 'amount': amount, + 'filled': filled, + 'remaining': remaining, + 'cost': None, + 'trades': None, + 'fee': None, + 'reduceOnly': reduceOnly, + 'postOnly': postOnly, + 'triggerPrice': self.safe_number(order, 'triggerPrice'), + }, market) + + def fetch_order(self, id: str, symbol: Str = None, params={}) -> Order: + """ + fetches information on an order made by the user + + https://api-doc.hibachi.xyz/#096a8854-b918-4de8-8731-b2a28d26b96d + + :param str id: the order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + request: dict = { + 'orderId': id, + 'accountId': self.get_account_id(), + } + response = self.privateGetTradeOrder(self.extend(request, params)) + return self.parse_order(response, market) + + def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fee + @param params extra parameters + :returns dict: a map of market symbols to `fee structures ` + """ + self.load_markets() + request: dict = { + 'accountId': self.get_account_id(), + } + response = self.privateGetTradeAccountInfo(self.extend(request, params)) + # { + # "tradeMakerFeeRate": "0.00000000", + # "tradeTakerFeeRate": "0.00020000" + # }, + makerFeeRate = self.safe_number(response, 'tradeMakerFeeRate') + takerFeeRate = self.safe_number(response, 'tradeTakerFeeRate') + result: dict = {} + for i in range(0, len(self.symbols)): + symbol = self.symbols[i] + result[symbol] = { + 'info': response, + 'symbol': symbol, + 'maker': makerFeeRate, + 'taker': takerFeeRate, + 'percentage': True, + } + return result + + def order_message(self, market, nonce: float, feeRate: float, type: OrderType, side: OrderSide, amount: float, price: Num = None): + sideInternal = 0 + if side == 'sell': + sideInternal = 0 + elif side == 'buy': + sideInternal = 1 + # Converting them to internal representation: + # - Quantity: Internal = External * (10^underlyingDecimals) + # - Price: Internal = External * (2^32) * (10^(settlementDecimals-underlyingDecimals)) + # - FeeRate: Internal = External * (10^8) + amountStr = self.amount_to_precision(self.safe_string(market, 'symbol'), amount) + feeRateStr = self.number_to_string(feeRate) + info = self.safe_dict(market, 'info') + underlying = '1e' + self.safe_string(info, 'underlyingDecimals') + settlement = '1e' + self.safe_string(info, 'settlementDecimals') + one = '1' + feeRateFactor = '100000000' # 10^8 + priceFactor = '4294967296' # 2^32 + quantityInternal = Precise.string_div(Precise.string_mul(amountStr, underlying), one, 0) + feeRateInternal = Precise.string_div(Precise.string_mul(feeRateStr, feeRateFactor), one, 0) + # Encoding + nonce16 = self.int_to_base16(nonce) + noncePadded = nonce16.rjust(16, '0') + encodedNonce = self.base16_to_binary(noncePadded) + numericId = self.int_to_base16(self.safe_integer(market, 'numericId')) + numericIdPadded = numericId.rjust(8, '0') + encodedMarketId = self.base16_to_binary(numericIdPadded) + quantity16 = self.int_to_base16(self.parse_to_int(quantityInternal)) + quantityPadded = quantity16.rjust(16, '0') + encodedQuantity = self.base16_to_binary(quantityPadded) + sideInternal16 = self.int_to_base16(sideInternal) + sidePadded = sideInternal16.rjust(8, '0') + encodedSide = self.base16_to_binary(sidePadded) + feeRateInternal16 = self.int_to_base16(self.parse_to_int(feeRateInternal)) + feeRatePadded = feeRateInternal16.rjust(16, '0') + encodedFeeRate = self.base16_to_binary(feeRatePadded) + encodedPrice = self.binary_concat() + if type == 'limit': + priceStr = self.price_to_precision(self.safe_string(market, 'symbol'), price) + priceInternal = Precise.string_div(Precise.string_div(Precise.string_mul(Precise.string_mul(priceStr, priceFactor), settlement), underlying), one, 0) + price16 = self.int_to_base16(self.parse_to_int(priceInternal)) + pricePadded = price16.rjust(16, '0') + encodedPrice = self.base16_to_binary(pricePadded) + message = self.binary_concat(encodedNonce, encodedMarketId, encodedQuantity, encodedSide, encodedPrice, encodedFeeRate) + return message + + def create_order_request(self, nonce: float, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + market = self.market(symbol) + feeRate = max(self.safe_number(market, 'taker', self.safe_number(self.options, 'defaultTakerFee', 0.00045)), self.safe_number(market, 'maker', self.safe_number(self.options, 'defaultMakerFee', 0.00015))) + sideInternal = '' + if side == 'sell': + sideInternal = 'ASK' + elif side == 'buy': + sideInternal = 'BID' + priceInternal = '' + if price: + priceInternal = self.price_to_precision(symbol, price) + message = self.order_message(market, nonce, feeRate, type, side, amount, price) + signature = self.sign_message(message, self.privateKey) + request = { + 'symbol': self.safe_string(market, 'id'), + 'nonce': nonce, + 'side': sideInternal, + 'orderType': type.upper(), + 'quantity': self.amount_to_precision(symbol, amount), + 'price': priceInternal, + 'signature': signature, + 'maxFeesPercent': self.number_to_string(feeRate), + } + postOnly = self.is_post_only(type.upper() == 'MARKET', None, params) + reduceOnly = self.safe_bool_2(params, 'reduceOnly', 'reduce_only') + timeInForce = self.safe_string_lower(params, 'timeInForce') + triggerPrice = self.safe_string_2(params, 'triggerPrice', 'stopPrice') + if postOnly: + request['orderFlags'] = 'POST_ONLY' + elif timeInForce == 'ioc': + request['orderFlags'] = 'IOC' + elif reduceOnly: + request['orderFlags'] = 'REDUCE_ONLY' + if triggerPrice is not None: + request['triggerPrice'] = triggerPrice + params = self.omit(params, ['reduceOnly', 'reduce_only', 'postOnly', 'timeInForce', 'stopPrice', 'triggerPrice']) + return self.extend(request, params) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://api-doc.hibachi.xyz/#00f6d5ad-5275-41cb-a1a8-19ed5d142124 + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + nonce = self.nonce() + request = self.create_order_request(nonce, symbol, type, side, amount, price, params) + request['accountId'] = self.get_account_id() + response = self.privatePostTradeOrder(request) + # + # { + # "orderId": "578721673790138368" + # } + # + return self.safe_order({ + 'id': self.safe_string(response, 'orderId'), + 'status': 'pending', + }) + + def create_orders(self, orders: List[OrderRequest], params={}) -> List[Order]: + """ + *contract only* create a list of trade orders + + https://api-doc.hibachi.xyz/#c2840b9b-f02c-44ed-937d-dc2819f135b4 + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + nonce = self.nonce() + requestOrders = [] + for i in range(0, len(orders)): + rawOrder = orders[i] + symbol = self.safe_string(rawOrder, 'symbol') + type = self.safe_string(rawOrder, 'type') + side = self.safe_string(rawOrder, 'side') + amount = self.safe_value(rawOrder, 'amount') + price = self.safe_value(rawOrder, 'price') + orderParams = self.safe_dict(rawOrder, 'params', {}) + orderRequest = self.create_order_request(nonce + i, symbol, type, side, amount, price, orderParams) + orderRequest['action'] = 'place' + requestOrders.append(orderRequest) + request: dict = { + 'accountId': self.get_account_id(), + 'orders': requestOrders, + } + response = self.privatePostTradeOrders(self.extend(request, params)) + # + # {"orders": [{nonce: '1754349993908', orderId: '589642085255349248'}]} + # + ret = [] + responseOrders = self.safe_list(response, 'orders') + for i in range(0, len(responseOrders)): + responseOrder = responseOrders[i] + ret.append(self.safe_order({ + 'info': responseOrder, + 'id': self.safe_string(responseOrder, 'orderId'), + 'status': 'pending', + })) + return ret + + def edit_order_request(self, nonce: float, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + market = self.market(symbol) + feeRate = max(self.safe_number(market, 'taker'), self.safe_number(market, 'maker')) + message = self.order_message(market, nonce, feeRate, type, side, amount, price) + signature = self.sign_message(message, self.privateKey) + request = { + 'orderId': id, + 'nonce': nonce, + 'updatedQuantity': self.amount_to_precision(symbol, amount), + 'updatedPrice': self.price_to_precision(symbol, price), + 'maxFeesPercent': self.number_to_string(feeRate), + 'signature': signature, + } + return self.extend(request, params) + + def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + """ + edit a limit order that is not matched + + https://api-doc.hibachi.xyz/#94d2cdaf-1c71-440f-a981-da1112824810 + + :param str id: order id + :param str symbol: unified symbol of the market to create an order in + :param str type: must be 'limit' + :param str side: 'buy' or 'sell', should stay the same with original side + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + nonce = self.nonce() + request = self.edit_order_request(nonce, id, symbol, type, side, amount, price, params) + request['accountId'] = self.get_account_id() + self.privatePutTradeOrder(request) + # At self time the response body is empty. A 200 response means the update request is accepted and sent to process + # + # {} + # + return self.safe_order({ + 'id': id, + 'status': 'pending', + }) + + def edit_orders(self, orders: List[OrderRequest], params={}) -> List[Order]: + """ + edit a list of trade orders + + https://api-doc.hibachi.xyz/#c2840b9b-f02c-44ed-937d-dc2819f135b4 + + :param Array orders: list of orders to edit, each object should contain the parameters required by editOrder, namely id, symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + nonce = self.nonce() + requestOrders = [] + for i in range(0, len(orders)): + rawOrder = orders[i] + id = self.safe_string(rawOrder, 'id') + symbol = self.safe_string(rawOrder, 'symbol') + type = self.safe_string(rawOrder, 'type') + side = self.safe_string(rawOrder, 'side') + amount = self.safe_value(rawOrder, 'amount') + price = self.safe_value(rawOrder, 'price') + orderParams = self.safe_dict(rawOrder, 'params', {}) + orderRequest = self.edit_order_request(nonce + i, id, symbol, type, side, amount, price, orderParams) + orderRequest['action'] = 'modify' + requestOrders.append(orderRequest) + request: dict = { + 'accountId': self.get_account_id(), + 'orders': requestOrders, + } + response = self.privatePostTradeOrders(self.extend(request, params)) + # + # {"orders": [{"orderId": "589636801329628160"}]} + # + ret = [] + responseOrders = self.safe_list(response, 'orders') + for i in range(0, len(responseOrders)): + responseOrder = responseOrders[i] + ret.append(self.safe_order({ + 'info': responseOrder, + 'id': self.safe_string(responseOrder, 'orderId'), + 'status': 'pending', + })) + return ret + + def cancel_order_request(self, id: str): + bigid = self.convert_to_big_int(id) + idbase16 = self.int_to_base16(bigid) + idPadded = idbase16.rjust(16, '0') + message = self.base16_to_binary(idPadded) + signature = self.sign_message(message, self.privateKey) + return { + 'orderId': id, + 'signature': signature, + } + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + + https://api-doc.hibachi.xyz/#e99c4f48-e610-4b7c-b7f6-1b4bb7af0271 + + cancels an open order + :param str id: order id + :param str symbol: is unused + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + request: dict = self.cancel_order_request(id) + request['accountId'] = self.get_account_id() + response = self.privateDeleteTradeOrder(self.extend(request, params)) + # At self time the response body is empty. A 200 response means the cancel request is accepted and sent to cancel + # + # {} + # + return self.safe_order({ + 'info': response, + 'id': id, + 'status': 'canceled', + }) + + def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): + """ + cancel multiple orders + + https://api-doc.hibachi.xyz/#c2840b9b-f02c-44ed-937d-dc2819f135b4 + + :param str[] ids: order ids + :param str [symbol]: unified market symbol, unused + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an list of `order structures ` + """ + orders = [] + for i in range(0, len(ids)): + orderRequest = self.cancel_order_request(ids[i]) + orderRequest['action'] = 'cancel' + orders.append(orderRequest) + request: dict = { + 'accountId': self.get_account_id(), + 'orders': orders, + } + response = self.privatePostTradeOrders(self.extend(request, params)) + # + # {"orders": [{"orderId": "589636801329628160"}]} + # + ret = [] + responseOrders = self.safe_list(response, 'orders') + for i in range(0, len(responseOrders)): + responseOrder = responseOrders[i] + ret.append(self.safe_order({ + 'info': responseOrder, + 'id': self.safe_string(responseOrder, 'orderId'), + 'status': 'canceled', + })) + return ret + + def cancel_all_orders(self, symbol: Str = None, params={}): + """ + + https://api-doc.hibachi.xyz/#8ed24695-016e-49b2-a72d-7511ca921fee + + cancel all open orders in a market + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an list of `order structures ` + """ + self.load_markets() + nonce = self.nonce() + nonce16 = self.int_to_base16(nonce) + noncePadded = nonce16.rjust(16, '0') + message = self.base16_to_binary(noncePadded) + signature = self.sign_message(message, self.privateKey) + request: dict = { + 'accountId': self.get_account_id(), + 'nonce': nonce, + 'signature': signature, + } + if symbol is not None: + market = self.market(symbol) + request['contractId'] = self.safe_integer(market, 'numericId') + response = self.privateDeleteTradeOrders(self.extend(request, params)) + # At self time the response body is empty. A 200 response means the cancel request is accepted and sent to process + # + # {} + # + return [ + self.safe_order({ + 'info': response, + }), + ] + + def encode_withdraw_message(self, amount: float, maxFees: float, address: str): + # Converting them to internal representation: + # - Quantity: Internal = External * (10^6) + # - maxFees: Internal = External * (10^6) + # We only have USDT currency time + USDTAssetId = 1 + USDTFactor = '1000000' + amountStr = self.number_to_string(amount) + maxFeesStr = self.number_to_string(maxFees) + one = '1' + quantityInternal = Precise.string_div(Precise.string_mul(amountStr, USDTFactor), one, 0) + maxFeesInternal = Precise.string_div(Precise.string_mul(maxFeesStr, USDTFactor), one, 0) + # Encoding + usdtAsset16 = self.int_to_base16(USDTAssetId) + usdtAssetPadded = usdtAsset16.rjust(8, '0') + encodedAssetId = self.base16_to_binary(usdtAssetPadded) + quantity16 = self.int_to_base16(self.parse_to_int(quantityInternal)) + quantityPadded = quantity16.rjust(16, '0') + encodedQuantity = self.base16_to_binary(quantityPadded) + maxFees16 = self.int_to_base16(self.parse_to_int(maxFeesInternal)) + maxFeesPadded = maxFees16.rjust(16, '0') + encodedMaxFees = self.base16_to_binary(maxFeesPadded) + encodedAddress = self.base16_to_binary(address) + message = self.binary_concat(encodedAssetId, encodedQuantity, encodedMaxFees, encodedAddress) + return message + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://api-doc.hibachi.xyz/#6421625d-3e45-45fa-be9b-d2a0e780c090 + + :param str code: unified currency code, only support USDT + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + withdrawAddress = address[-40:] + # Get the withdraw fees + exchangeInfo = self.publicGetMarketExchangeInfo(params) + # { + # "feeConfig": { + # "depositFees": "0.004518", + # "tradeMakerFeeRate": "0.00000000", + # "tradeTakerFeeRate": "0.00020000", + # "transferFeeRate": "0.00010000", + # "withdrawalFees": "0.012050" + # }, + # } + feeConfig = self.safe_dict(exchangeInfo, 'feeConfig') + maxFees = self.safe_number(feeConfig, 'withdrawalFees') + # Generate the signature + message = self.encode_withdraw_message(amount, maxFees, withdrawAddress) + signature = self.sign_message(message, self.privateKey) + request = { + 'accountId': self.get_account_id(), + 'coin': 'USDT', + 'network': 'ARBITRUM', + 'withdrawAddress': withdrawAddress, + 'selfWithdrawal': False, + 'quantity': self.number_to_string(amount), + 'maxFees': self.number_to_string(maxFees), + 'signature': signature, + } + self.privatePostCapitalWithdraw(self.extend(request, params)) + # At self time the response body is empty. A 200 response means the withdraw request is accepted and sent to process + # + # {} + # + return { + 'info': None, + 'id': None, + 'txid': None, + 'timestamp': self.milliseconds(), + 'datetime': None, + 'address': None, + 'addressFrom': None, + 'addressTo': withdrawAddress, + 'tag': None, + 'tagFrom': None, + 'tagTo': None, + 'type': 'withdrawal', + 'amount': amount, + 'currency': code, + 'status': 'pending', + 'fee': {'currency': 'USDT', 'cost': maxFees}, + 'network': 'ARBITRUM', + 'updated': None, + 'comment': None, + 'internal': None, + } + + def nonce(self): + return self.milliseconds() + + def sign_message(self, message, privateKey): + if len(privateKey) == 44: + # For Exchange Managed account, the key length is 44 and we use HMAC to sign the message + return self.hmac(message, self.encode(privateKey), hashlib.sha256, 'hex') + else: + # For Trustless account, the key length is 66 including '0x' and we use ECDSA to sign the message + hash = self.hash(message, 'sha256', 'hex') + signature = self.ecdsa(hash[-64:], privateKey[-64:], 'secp256k1', None) + r = signature['r'] + s = signature['s'] + v = self.int_to_base16(signature['v']) + return r.rjust(64, '0') + s.rjust(64, '0') + v.rjust(2, '0') + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches the state of the open orders on the orderbook + + https://api-doc.hibachi.xyz/#4abb30c4-e5c7-4b0f-9ade-790111dbfa47 + + :param str symbol: unified symbol of the market + :param int [limit]: currently unused + :param dict [params]: extra parameters to be passed -- see documentation link above + :returns dict: A dictionary containg `orderbook information ` + """ + self.load_markets() + market: Market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = self.publicGetMarketDataOrderbook(self.extend(request, params)) + formattedResponse = {} + formattedResponse['ask'] = self.safe_list(self.safe_dict(response, 'ask'), 'levels') + formattedResponse['bid'] = self.safe_list(self.safe_dict(response, 'bid'), 'levels') + # { + # "ask": { + # "endPrice": "3512.63", + # "levels": [ + # { + # "price": "3511.93", + # "quantity": "0.284772482" + # }, + # { + # "price": "3512.28", + # "quantity": "0.569544964" + # }, + # { + # "price": "3512.63", + # "quantity": "0.854317446" + # } + # ], + # "startPrice": "3511.93" + # }, + # "bid": { + # "endPrice": "3510.87", + # "levels": [ + # { + # "price": "3515.39", + # "quantity": "2.345153070" + # }, + # { + # "price": "3511.22", + # "quantity": "0.284772482" + # }, + # { + # "price": "3510.87", + # "quantity": "0.569544964" + # } + # ], + # "startPrice": "3515.39" + # } + # } + return self.parse_order_book(formattedResponse, symbol, self.milliseconds(), 'bid', 'ask', 'price', 'quantity') + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + + https://api-doc.hibachi.xyz/#0adbf143-189f-40e0-afdc-88af4cba3c79 + + fetch all trades made by the user + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market: Market = None + if symbol is not None: + market = self.market(symbol) + request = {'accountId': self.get_account_id()} + response = self.privateGetTradeAccountTrades(self.extend(request, params)) + # + # { + # "trades": [ + # { + # "askAccountId": 221, + # "askOrderId": 589168494921909200, + # "bidAccountId": 132, + # "bidOrderId": 589168494829895700, + # "fee": "0.000477", + # "id": 199511136, + # "orderType": "MARKET", + # "price": "119257.90000", + # "quantity": "0.0000200000", + # "realizedPnl": "-0.000352", + # "side": "Sell", + # "symbol": "BTC/USDT-P", + # "timestamp": 1752543391 + # } + # ] + # } + # + trades = self.safe_list(response, 'trades') + return self.parse_trades(trades, market, since, limit, params) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # [ + # { + # "close": "3704.751036", + # "high": "3716.530378", + # "interval": "1h", + # "low": "3699.627883", + # "open": "3716.406894", + # "timestamp": 1712628000, + # "volumeNotional": "1637355.846362" + # } + # ] + # + return [ + self.safe_integer_product(ohlcv, 'timestamp', 1000), + self.safe_number(ohlcv, 'open'), + self.safe_number(ohlcv, 'high'), + self.safe_number(ohlcv, 'low'), + self.safe_number(ohlcv, 'close'), + self.safe_number(ohlcv, 'volumeNotional'), + ] + + def fetch_open_orders(self, symbol: str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches all current open orders + + https://api-doc.hibachi.xyz/#3243f8a0-086c-44c5-ab8a-71bbb7bab403 + + :param str [symbol]: unified market symbol to filter by + :param int [since]: milisecond timestamp of the earliest order + :param int [limit]: the maximum number of open orders to return + :param dict [params]: extra parameters + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + request = { + 'accountId': self.get_account_id(), + } + response = self.privateGetTradeOrders(self.extend(request, params)) + # [ + # { + # "accountId": 12452, + # "availableQuantity": "0.0000230769", + # "contractId": 2, + # "creationTime": 1752684501, + # "orderId": "589205486123876352", + # "orderType": "LIMIT", + # "price": "130000.00000", + # "side": "ASK", + # "status": "PLACED", + # "symbol": "BTC/USDT-P", + # "totalQuantity": "0.0000230769" + # }, + # { + # "accountId": 12452, + # "availableQuantity": "1.234000000", + # "contractId": 1, + # "creationTime": 1752240682, + # "orderId": "589089141754429441", + # "orderType": "LIMIT", + # "price": "1.234000", + # "side": "BID", + # "status": "PLACED", + # "symbol": "ETH/USDT-P", + # "totalQuantity": "1.234000000" + # } + # ] + return self.parse_orders(response, market, since, limit) + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + + https://api-doc.hibachi.xyz/#4f0eacec-c61e-4d51-afb3-23c51c2c6bac + + fetches historical candlestick data containing the close, high, low, open prices, interval and the volumeNotional + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest candle to fetch + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + market = self.market(symbol) + timeframe = self.safe_string(self.timeframes, timeframe, timeframe) + request: dict = { + 'symbol': market['id'], + 'interval': timeframe, + } + if since is not None: + request['fromMs'] = since + until: Int = None + until, params = self.handle_option_and_params(params, 'fetchOHLCV', 'until') + if until is not None: + request['toMs'] = until + response = self.publicGetMarketDataKlines(self.extend(request, params)) + # + # [ + # { + # "close": "3704.751036", + # "high": "3716.530378", + # "interval": "1h", + # "low": "3699.627883", + # "open": "3716.406894", + # "timestamp": 1712628000, + # "volumeNotional": "1637355.846362" + # } + # ] + # + klines = self.safe_list(response, 'klines', []) + return self.parse_ohlcvs(klines, market, timeframe, since, limit) + + def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://api-doc.hibachi.xyz/#69aafedb-8274-4e21-bbaf-91dace8b8f31 + + :param str[] [symbols]: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + self.load_markets() + symbols = self.market_symbols(symbols) + request: dict = { + 'accountId': self.get_account_id(), + } + response = self.privateGetTradeAccountInfo(self.extend(request, params)) + # + # { + # "assets": [ + # { + # "quantity": "14.130626", + # "symbol": "USDT" + # } + # ], + # "balance": "14.186087", + # "maximalWithdraw": "4.152340", + # "numFreeTransfersRemaining": 96, + # "positions": [ + # { + # "direction": "Short", + # "entryNotional": "10.302213", + # "notionalValue": "10.225008", + # "quantity": "0.004310550", + # "symbol": "ETH/USDT-P", + # "unrealizedFundingPnl": "0.000000", + # "unrealizedTradingPnl": "0.077204" + # }, + # { + # "direction": "Short", + # "entryNotional": "2.000016", + # "notionalValue": "1.999390", + # "quantity": "0.0000328410", + # "symbol": "BTC/USDT-P", + # "unrealizedFundingPnl": "0.000000", + # "unrealizedTradingPnl": "0.000625" + # }, + # { + # "direction": "Short", + # "entryNotional": "2.000015", + # "notionalValue": "2.022384", + # "quantity": "0.01470600", + # "symbol": "SOL/USDT-P", + # "unrealizedFundingPnl": "0.000000", + # "unrealizedTradingPnl": "-0.022369" + # } + # ], + # } + # + data = self.safe_list(response, 'positions', []) + return self.parse_positions(data, symbols) + + def parse_position(self, position: dict, market: Market = None): + # + # { + # "direction": "Short", + # "entryNotional": "10.302213", + # "notionalValue": "10.225008", + # "quantity": "0.004310550", + # "symbol": "ETH/USDT-P", + # "unrealizedFundingPnl": "0.000000", + # "unrealizedTradingPnl": "0.077204" + # } + # + marketId = self.safe_string(position, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + side = self.safe_string_lower(position, 'direction') + quantity = self.safe_string(position, 'quantity') + unrealizedFunding = self.safe_string(position, 'unrealizedFundingPnl', '0') + unrealizedTrading = self.safe_string(position, 'unrealizedTradingPnl', '0') + unrealizedPnl = Precise.string_add(unrealizedFunding, unrealizedTrading) + return self.safe_position({ + 'info': position, + 'id': None, + 'symbol': symbol, + 'entryPrice': self.safe_string(position, 'average_entry_price'), + 'markPrice': None, + 'notional': self.safe_string(position, 'notionalValue'), + 'collateral': None, + 'unrealizedPnl': unrealizedPnl, + 'side': side, + 'contracts': self.parse_number(quantity), + 'contractSize': None, + 'timestamp': None, + 'datetime': None, + 'hedged': None, + 'maintenanceMargin': None, + 'maintenanceMarginPercentage': None, + 'initialMargin': None, + 'initialMarginPercentage': None, + 'leverage': None, + 'liquidationPrice': None, + 'marginRatio': None, + 'marginMode': None, + 'percentage': None, + }) + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + endpoint = '/' + self.implode_params(path, params) + url = self.urls['api'][api] + endpoint + headers = {'Hibachi-Client': 'HibachiCCXT/unversioned'} + if method == 'GET': + request = self.omit(params, self.extract_params(path)) + query = self.urlencode(request) + if len(query) != 0: + url += '?' + query + if method == 'POST' or method == 'PUT' or method == 'DELETE': + headers['Content-Type'] = 'application/json' + body = self.json(params) + if api == 'private': + self.check_required_credentials() + headers['Authorization'] = self.apiKey + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None # fallback to default error handler + if 'status' in response: + # + # {"errorCode":4,"message":"Invalid input: Invalid quantity: 0","status":"failed"} + # + status = self.safe_string(response, 'status') + if status == 'failed': + code = self.safe_string(response, 'errorCode') + feedback = self.id + ' ' + body + self.throw_broadly_matched_exception(self.exceptions['broad'], body, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], code, feedback) + message = self.safe_string(response, 'message') + self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) + raise ExchangeError(feedback) + return None + + def parse_transaction_type(self, type): + types: dict = { + 'deposit': 'transaction', + 'withdrawal': 'transaction', + 'transfer-in': 'transfer', + 'transfer-out': 'transfer', + } + return self.safe_string(types, type, type) + + def parse_transaction_status(self, status): + statuses: dict = { + 'pending': 'pending', + 'claimable': 'pending', + 'completed': 'ok', + 'failed': 'canceled', + } + return self.safe_string(statuses, status, status) + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + transactionType = self.safe_string(item, 'transactionType') + timestamp = None + type = None + direction = None + amount = None + fee = None + referenceId = None + referenceAccount = None + status = None + if transactionType is None: + # response from TradeAccountTradingHistory + timestamp = self.safe_integer_product(item, 'timestamp', 1000) + type = 'trade' + amountStr = self.safe_string(item, 'realizedPnl') + if Precise.string_lt(amountStr, '0'): + direction = 'out' + amountStr = Precise.string_neg(amountStr) + else: + direction = 'in' + amount = self.parse_number(amountStr) + fee = {'currency': 'USDT', 'cost': self.safe_number(item, 'fee')} + status = 'ok' + else: + # response from CapitalHistory + timestamp = self.safe_integer_product(item, 'timestampSec', 1000) + amount = self.safe_number(item, 'quantity') + direction = 'in' if (transactionType == 'deposit' or transactionType == 'transfer-in') else 'out' + type = self.parse_transaction_type(transactionType) + status = self.parse_transaction_status(self.safe_string(item, 'status')) + if transactionType == 'transfer-in': + referenceAccount = self.safe_string(item, 'srcAccountId') + elif transactionType == 'transfer-out': + referenceAccount = self.safe_string(item, 'receivingAccountId') + referenceId = self.safe_string(item, 'transactionHash') + return self.safe_ledger_entry({ + 'id': self.safe_string(item, 'id'), + 'currency': self.currency('USDT'), + 'account': self.number_to_string(self.accountId), + 'referenceAccount': referenceAccount, + 'referenceId': referenceId, + 'status': status, + 'amount': amount, + 'before': None, + 'after': None, + 'fee': fee, + 'direction': direction, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'type': type, + 'info': item, + }, currency) + + def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + + https://api-doc.hibachi.xyz/#35125e3f-d154-4bfd-8276-a48bb1c62020 + + :param str [code]: unified currency code, default is None + :param int [since]: timestamp in ms of the earliest ledger entry, default is None + :param int [limit]: max number of ledger entries to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ledger structure ` + """ + self.load_markets() + currency = self.currency('USDT') + request = {'accountId': self.get_account_id()} + rawPromises = [ + self.privateGetCapitalHistory(self.extend(request, params)), + self.privateGetTradeAccountTradingHistory(self.extend(request, params)), + ] + promises = rawPromises + responseCapitalHistory = promises[0] + # + # { + # "transactions": [ + # { + # "assetId": 1, + # "blockNumber": 358396669, + # "chain": "Arbitrum", + # "etaTsSec": null, + # "id": 358396669, + # "quantity": "0.999500", + # "status": "pending", + # "timestampSec": 1752692872, + # "token": "USDT", + # "transactionHash": "0x408e48881e0ba77d8638e3fe57bc06bdec513ddaa8b672e0aefa7e22e2f18b5e", + # "transactionType": "deposit" + # }, + # { + # "assetId": 1, + # "etaTsSec": null, + # "id": 13116, + # "instantWithdrawalChain": null, + # "instantWithdrawalToken": null, + # "isInstantWithdrawal": False, + # "quantity": "0.040000", + # "status": "completed", + # "timestampSec": 1752542708, + # "transactionHash": "0xe89cf90b2408d1a273dc9427654145def102d9449e5e2cfc10690ccffc3d7e28", + # "transactionType": "withdrawal", + # "withdrawalAddress": "0x23625d5fc6a6e32638d908eb4c3a3415e5121f76" + # }, + # { + # "assetId": 1, + # "id": 167, + # "quantity": "10.000000", + # "srcAccountId": 175, + # "srcAddress": "0xc2f77ce029438a3fdfe68ddee25991a9fb985a86", + # "status": "completed", + # "timestampSec": 1732224729, + # "transactionType": "transfer-in" + # }, + # { + # "assetId": 1, + # "id": 170, + # "quantity": "10.000000", + # "receivingAccountId": 175, + # "receivingAddress": "0xc2f77ce029438a3fdfe68ddee25991a9fb985a86", + # "status": "completed", + # "timestampSec": 1732225631, + # "transactionType": "transfer-out" + # }, + # ] + # } + # + rowsCapitalHistory = self.safe_list(responseCapitalHistory, 'transactions') + responseTradingHistory = promises[1] + # + # { + # "tradingHistory": [ + # { + # "eventType": "MARKET", + # "fee": "0.000008", + # "priceOrFundingRate": "119687.82481", + # "quantity": "0.0000003727", + # "realizedPnl": "0.004634", + # "side": "Sell", + # "symbol": "BTC/USDT-P", + # "timestamp": 1752522571 + # }, + # { + # "eventType": "FundingEvent", + # "fee": "0", + # "priceOrFundingRate": "0.000203", + # "quantity": "0.0000003727", + # "realizedPnl": "-0.000009067899008751979", + # "side": "Long", + # "symbol": "BTC/USDT-P", + # "timestamp": 1752508800 + # }, + # ] + # } + # + rowsTradingHistory = self.safe_list(responseTradingHistory, 'tradingHistory') + rows = self.array_concat(rowsCapitalHistory, rowsTradingHistory) + return self.parse_ledger(rows, currency, since, limit, params) + + def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch deposit address for given currency and chain. currently, we have a single EVM address across multiple EVM chains. Note: This method is currently only supported for trustless accounts + :param str code: unified currency code + :param dict [params]: extra parameters for API + :param str [params.publicKey]: your public key, you can get it from UI after creating API key + :returns dict: an `address structure ` + """ + request = { + 'publicKey': self.safe_string(params, 'publicKey'), + 'accountId': self.get_account_id(), + } + response = self.privateGetCapitalDepositInfo(self.extend(request, params)) + # { + # "depositAddressEvm": "0x0b95d90b9345dadf1460bd38b9f4bb0d2f4ed788" + # } + return { + 'info': response, + 'currency': 'USDT', + 'network': 'ARBITRUM', + 'address': self.safe_string(response, 'depositAddressEvm'), + 'tag': None, + } + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + timestamp = self.safe_integer_product(transaction, 'timestampSec', 1000) + address = self.safe_string(transaction, 'withdrawalAddress') + transactionType = self.safe_string(transaction, 'transactionType') + if transactionType != 'deposit' and transactionType != 'withdrawal': + transactionType = self.parse_transaction_type(transactionType) + return { + 'info': transaction, + 'id': self.safe_string(transaction, 'id'), + 'txid': self.safe_string(transaction, 'transactionHash'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': 'ARBITRUM', # Currently the exchange only exists on Arbitrum, + 'address': address, + 'addressTo': address, + 'addressFrom': None, + 'tag': None, + 'tagTo': None, + 'tagFrom': None, + 'type': transactionType, + 'amount': self.safe_number(transaction, 'quantity'), + 'currency': 'USDT', + 'status': self.parse_transaction_status(self.safe_string(transaction, 'status')), + 'updated': None, + 'internal': None, + 'comment': None, + 'fee': None, + } + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch deposits made to account + + https://api-doc.hibachi.xyz/#35125e3f-d154-4bfd-8276-a48bb1c62020 + + :param str [code]: unified currency code + :param int [since]: filter by earliest timestamp(ms) + :param int [limit]: maximum number of deposits to be returned + :param dict [params]: extra parameters to be passed to API + :returns dict[]: a list of `transaction structures ` + """ + currency = self.safe_currency(code) + request = { + 'accountId': self.get_account_id(), + } + response = self.privateGetCapitalHistory(self.extend(request, params)) + # { + # "transactions": [ + # { + # "assetId": 1, + # "blockNumber": 0, + # "chain": null, + # "etaTsSec": 1752758789, + # "id": 42688, + # "quantity": "6.130000", + # "status": "completed", + # "timestampSec": 1752758788, + # "token": null, + # "transactionHash": "0x8dcd7bd1155b5624fb5e38a1365888f712ec633a57434340e05080c70b0e3bba", + # "transactionType": "deposit" + # }, + # { + # "assetId": 1, + # "etaTsSec": null, + # "id": 12993, + # "instantWithdrawalChain": null, + # "instantWithdrawalToken": null, + # "isInstantWithdrawal": False, + # "quantity": "0.111930", + # "status": "completed", + # "timestampSec": 1752387891, + # "transactionHash": "0x32ab5fe5b90f6d753bab83523ebc8465eb9daef54580e13cb9ff031d400c5620", + # "transactionType": "withdrawal", + # "withdrawalAddress": "0x43f15ef2ef2ab5e61e987ee3d652a5872aea8a6c" + # }, + # ] + # } + transactions = self.safe_list(response, 'transactions') + deposits = [] + for i in range(0, len(transactions)): + transaction = transactions[i] + if self.safe_string(transaction, 'transactionType') == 'deposit': + deposits.append(transaction) + return self.parse_transactions(deposits, currency, since, limit, params) + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch withdrawals made from account + + https://api-doc.hibachi.xyz/#35125e3f-d154-4bfd-8276-a48bb1c62020 + + :param str [code]: unified currency code + :param int [since]: filter by earliest timestamp(ms) + :param int [limit]: maximum number of deposits to be returned + :param dict [params]: extra parameters to be passed to API + :returns dict[]: a list of `transaction structures ` + """ + currency = self.safe_currency(code) + request = { + 'accountId': self.get_account_id(), + } + response = self.privateGetCapitalHistory(self.extend(request, params)) + # { + # "transactions": [ + # { + # "assetId": 1, + # "blockNumber": 0, + # "chain": null, + # "etaTsSec": 1752758789, + # "id": 42688, + # "quantity": "6.130000", + # "status": "completed", + # "timestampSec": 1752758788, + # "token": null, + # "transactionHash": "0x8dcd7bd1155b5624fb5e38a1365888f712ec633a57434340e05080c70b0e3bba", + # "transactionType": "deposit" + # }, + # { + # "assetId": 1, + # "etaTsSec": null, + # "id": 12993, + # "instantWithdrawalChain": null, + # "instantWithdrawalToken": null, + # "isInstantWithdrawal": False, + # "quantity": "0.111930", + # "status": "completed", + # "timestampSec": 1752387891, + # "transactionHash": "0x32ab5fe5b90f6d753bab83523ebc8465eb9daef54580e13cb9ff031d400c5620", + # "transactionType": "withdrawal", + # "withdrawalAddress": "0x43f15ef2ef2ab5e61e987ee3d652a5872aea8a6c" + # }, + # ] + # } + transactions = self.safe_list(response, 'transactions') + withdrawals = [] + for i in range(0, len(transactions)): + transaction = transactions[i] + if self.safe_string(transaction, 'transactionType') == 'withdrawal': + withdrawals.append(transaction) + return self.parse_transactions(withdrawals, currency, since, limit, params) + + def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + http://api-doc.hibachi.xyz/#b5c6a3bc-243d-4d35-b6d4-a74c92495434 + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = self.publicGetExchangeUtcTimestamp(params) + # + # {"timestampMs":1754077574040} + # + return self.safe_integer(response, 'timestampMs') + + def fetch_open_interest(self, symbol: str, params={}): + """ + retrieves the open interest of a contract trading pair + + https://api-doc.hibachi.xyz/#bc34e8ae-e094-4802-8d56-3efe3a7bad49 + + :param str symbol: unified CCXT market symbol + :param dict [params]: exchange specific parameters + :returns dict} an open interest structure{@link https://docs.ccxt.com/#/?id=open-interest-structure: + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = self.publicGetMarketDataOpenInterest(self.extend(request, params)) + # + # {"totalQuantity" : "2.3299770166"} + # + timestamp = self.milliseconds() + return self.safe_open_interest({ + 'symbol': symbol, + 'openInterestAmount': self.safe_string(response, 'totalQuantity'), + 'openInterestValue': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'info': response, + }, market) + + def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate + + https://api-doc.hibachi.xyz/#bca696ca-b9b2-4072-8864-5d6b8c09807e + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = self.publicGetMarketDataPrices(self.extend(request, params)) + # + # { + # "askPrice": "3514.650296", + # "bidPrice": "3513.596112", + # "fundingRateEstimation": { + # "estimatedFundingRate": "0.000001", + # "nextFundingTimestamp": 1712707200 + # }, + # "markPrice": "3514.288858", + # "spotPrice": "3514.715000", + # "symbol": "ETH/USDT-P", + # "tradePrice": "2372.746570" + # } + # + funding = self.safe_dict(response, 'fundingRateEstimation', {}) + timestamp = self.milliseconds() + nextFundingTimestamp = self.safe_integer_product(funding, 'nextFundingTimestamp', 1000) + return { + 'info': funding, + 'symbol': market['symbol'], + 'markPrice': None, + 'indexPrice': None, + 'interestRate': self.parse_number('0'), + 'estimatedSettlePrice': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'fundingRate': self.safe_number(funding, 'estimatedFundingRate'), + 'fundingTimestamp': nextFundingTimestamp, + 'fundingDatetime': self.iso8601(nextFundingTimestamp), + 'nextFundingRate': None, + 'nextFundingTimestamp': None, + 'nextFundingDatetime': None, + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': '8h', + } + + def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical funding rate prices + + https://api-doc.hibachi.xyz/#4abb30c4-e5c7-4b0f-9ade-790111dbfa47 + + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: timestamp in ms of the earliest funding rate to fetch + :param int [limit]: the maximum amount of `funding rate structures ` to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `funding rate structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = self.publicGetMarketDataFundingRates(self.extend(request, params)) + # + # { + # "data": [ + # { + # "contractId": 2, + # "fundingTimestamp": 1753488000, + # "fundingRate": "0.000137", + # "indexPrice": "117623.65010" + # } + # ] + # } + # + data = self.safe_list(response, 'data') + rates = [] + for i in range(0, len(data)): + entry = data[i] + timestamp = self.safe_integer_product(entry, 'fundingTimestamp', 1000) + rates.append({ + 'info': entry, + 'symbol': symbol, + 'fundingRate': self.safe_number(entry, 'fundingRate'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + sorted = self.sort_by(rates, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, symbol, since, limit) diff --git a/ccxt/hitbtc.py b/ccxt/hitbtc.py new file mode 100644 index 0000000..99d08b1 --- /dev/null +++ b/ccxt/hitbtc.py @@ -0,0 +1,3651 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.hitbtc import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currencies, Currency, DepositAddress, Int, Leverage, MarginMode, MarginModes, MarginModification, Market, Num, Order, OrderBook, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, FundingRates, OrderBooks, Trade, TradingFeeInterface, TradingFees, Transaction, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import AccountSuspended +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import NotSupported +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.errors import OnMaintenance +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class hitbtc(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(hitbtc, self).describe(), { + 'id': 'hitbtc', + 'name': 'HitBTC', + 'countries': ['HK'], + # 300 requests per second => 1000ms / 300 = 3.333(Trading: placing, replacing, deleting) + # 30 requests per second =>( 1000ms / rateLimit ) / 30 = cost = 10(Market Data and other Public Requests) + # 20 requests per second =>( 1000ms / rateLimit ) / 20 = cost = 15(All Other) + 'rateLimit': 3.333, # TODO: optimize https://api.hitbtc.com/#rate-limiting + 'version': '3', + 'has': { + 'CORS': False, + 'spot': True, + 'margin': True, + 'swap': True, + 'future': False, + 'option': False, + 'addMargin': True, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'closePosition': False, + 'createDepositAddress': True, + 'createOrder': True, + 'createPostOnlyOrder': True, + 'createReduceOnlyOrder': True, + 'createStopLimitOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'editOrder': True, + 'fetchAccounts': False, + 'fetchBalance': True, + 'fetchBorrowRateHistories': None, + 'fetchBorrowRateHistory': None, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchDepositsWithdrawals': True, + 'fetchDepositWithdrawFee': 'emulated', + 'fetchDepositWithdrawFees': True, + 'fetchFundingHistory': None, + 'fetchFundingRate': True, + 'fetchFundingRateHistory': True, + 'fetchFundingRates': True, + 'fetchGreeks': False, + 'fetchIndexOHLCV': True, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchLeverage': True, + 'fetchLeverageTiers': None, + 'fetchLiquidations': False, + 'fetchMarginMode': 'emulated', + 'fetchMarginModes': True, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': True, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': True, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': True, + 'fetchOpenOrder': True, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrderBooks': True, + 'fetchOrders': False, + 'fetchOrderTrades': True, + 'fetchPosition': True, + 'fetchPositions': True, + 'fetchPremiumIndexOHLCV': True, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTrades': True, + 'fetchTradingFee': True, + 'fetchTradingFees': True, + 'fetchTransactions': 'emulated', + 'fetchVolatilityHistory': False, + 'fetchWithdrawals': True, + 'reduceMargin': True, + 'sandbox': True, + 'setLeverage': True, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'transfer': True, + 'withdraw': True, + }, + 'precisionMode': TICK_SIZE, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/27766555-8eaec20e-5edc-11e7-9c5b-6dc69fc42f5e.jpg', + 'test': { + 'public': 'https://api.demo.hitbtc.com/api/3', + 'private': 'https://api.demo.hitbtc.com/api/3', + }, + 'api': { + 'public': 'https://api.hitbtc.com/api/3', + 'private': 'https://api.hitbtc.com/api/3', + }, + 'www': 'https://hitbtc.com', + 'referral': 'https://hitbtc.com/?ref_id=5a5d39a65d466', + 'doc': [ + 'https://api.hitbtc.com', + 'https://github.com/hitbtc-com/hitbtc-api/blob/master/APIv2.md', + ], + 'fees': [ + 'https://hitbtc.com/fees-and-limits', + 'https://support.hitbtc.com/hc/en-us/articles/115005148605-Fees-and-limits', + ], + }, + 'api': { + 'public': { + 'get': { + 'public/currency': 10, + 'public/currency/{currency}': 10, + 'public/symbol': 10, + 'public/symbol/{symbol}': 10, + 'public/ticker': 10, + 'public/ticker/{symbol}': 10, + 'public/price/rate': 10, + 'public/price/history': 10, + 'public/price/ticker': 10, + 'public/price/ticker/{symbol}': 10, + 'public/trades': 10, + 'public/trades/{symbol}': 10, + 'public/orderbook': 10, + 'public/orderbook/{symbol}': 10, + 'public/candles': 10, + 'public/candles/{symbol}': 10, + 'public/converted/candles': 10, + 'public/converted/candles/{symbol}': 10, + 'public/futures/info': 10, + 'public/futures/info/{symbol}': 10, + 'public/futures/history/funding': 10, + 'public/futures/history/funding/{symbol}': 10, + 'public/futures/candles/index_price': 10, + 'public/futures/candles/index_price/{symbol}': 10, + 'public/futures/candles/mark_price': 10, + 'public/futures/candles/mark_price/{symbol}': 10, + 'public/futures/candles/premium_index': 10, + 'public/futures/candles/premium_index/{symbol}': 10, + 'public/futures/candles/open_interest': 10, + 'public/futures/candles/open_interest/{symbol}': 10, + }, + }, + 'private': { + 'get': { + 'spot/balance': 15, + 'spot/balance/{currency}': 15, + 'spot/order': 1, + 'spot/order/{client_order_id}': 1, + 'spot/fee': 15, + 'spot/fee/{symbol}': 15, + 'spot/history/order': 15, + 'spot/history/trade': 15, + 'margin/account': 1, + 'margin/account/isolated/{symbol}': 1, + 'margin/account/cross/{currency}': 1, + 'margin/order': 1, + 'margin/order/{client_order_id}': 1, + 'margin/config': 15, + 'margin/history/order': 15, + 'margin/history/trade': 15, + 'margin/history/positions': 15, + 'margin/history/clearing': 15, + 'futures/balance': 15, + 'futures/balance/{currency}': 15, + 'futures/account': 1, + 'futures/account/isolated/{symbol}': 1, + 'futures/order': 1, + 'futures/order/{client_order_id}': 1, + 'futures/config': 15, + 'futures/fee': 15, + 'futures/fee/{symbol}': 15, + 'futures/history/order': 15, + 'futures/history/trade': 15, + 'futures/history/positions': 15, + 'futures/history/clearing': 15, + 'wallet/balance': 30, + 'wallet/balance/{currency}': 30, + 'wallet/crypto/address': 30, + 'wallet/crypto/address/recent-deposit': 30, + 'wallet/crypto/address/recent-withdraw': 30, + 'wallet/crypto/address/check-mine': 30, + 'wallet/transactions': 30, + 'wallet/transactions/{tx_id}': 30, + 'wallet/crypto/fee/estimate': 30, + 'wallet/airdrops': 30, + 'wallet/amount-locks': 30, + 'sub-account': 15, + 'sub-account/acl': 15, + 'sub-account/balance/{subAccID}': 15, + 'sub-account/crypto/address/{subAccID}/{currency}': 15, + }, + 'post': { + 'spot/order': 1, + 'spot/order/list': 1, + 'margin/order': 1, + 'margin/order/list': 1, + 'futures/order': 1, + 'futures/order/list': 1, + 'wallet/crypto/address': 30, + 'wallet/crypto/withdraw': 30, + 'wallet/convert': 30, + 'wallet/transfer': 30, + 'wallet/internal/withdraw': 30, + 'wallet/crypto/check-offchain-available': 30, + 'wallet/crypto/fees/estimate': 30, + 'wallet/airdrops/{id}/claim': 30, + 'sub-account/freeze': 15, + 'sub-account/activate': 15, + 'sub-account/transfer': 15, + 'sub-account/acl': 15, + }, + 'patch': { + 'spot/order/{client_order_id}': 1, + 'margin/order/{client_order_id}': 1, + 'futures/order/{client_order_id}': 1, + }, + 'delete': { + 'spot/order': 1, + 'spot/order/{client_order_id}': 1, + 'margin/position': 1, + 'margin/position/isolated/{symbol}': 1, + 'margin/order': 1, + 'margin/order/{client_order_id}': 1, + 'futures/position': 1, + 'futures/position/{margin_mode}/{symbol}': 1, + 'futures/order': 1, + 'futures/order/{client_order_id}': 1, + 'wallet/crypto/withdraw/{id}': 30, + }, + 'put': { + 'margin/account/isolated/{symbol}': 1, + 'futures/account/isolated/{symbol}': 1, + 'wallet/crypto/withdraw/{id}': 30, + }, + }, + }, + 'fees': { + 'trading': { + 'tierBased': True, + 'percentage': True, + 'taker': self.parse_number('0.0009'), + 'maker': self.parse_number('0.0009'), + 'tiers': { + 'maker': [ + [self.parse_number('0'), self.parse_number('0.0009')], + [self.parse_number('10'), self.parse_number('0.0007')], + [self.parse_number('100'), self.parse_number('0.0006')], + [self.parse_number('500'), self.parse_number('0.0005')], + [self.parse_number('1000'), self.parse_number('0.0003')], + [self.parse_number('5000'), self.parse_number('0.0002')], + [self.parse_number('10000'), self.parse_number('0.0001')], + [self.parse_number('20000'), self.parse_number('0')], + [self.parse_number('50000'), self.parse_number('-0.0001')], + [self.parse_number('100000'), self.parse_number('-0.0001')], + ], + 'taker': [ + [self.parse_number('0'), self.parse_number('0.0009')], + [self.parse_number('10'), self.parse_number('0.0008')], + [self.parse_number('100'), self.parse_number('0.0007')], + [self.parse_number('500'), self.parse_number('0.0007')], + [self.parse_number('1000'), self.parse_number('0.0006')], + [self.parse_number('5000'), self.parse_number('0.0006')], + [self.parse_number('10000'), self.parse_number('0.0005')], + [self.parse_number('20000'), self.parse_number('0.0004')], + [self.parse_number('50000'), self.parse_number('0.0003')], + [self.parse_number('100000'), self.parse_number('0.0002')], + ], + }, + }, + }, + 'features': { + 'default': { + 'sandbox': True, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': True, + }, + 'hedged': False, + 'selfTradePrevention': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': False, + 'marketBuyRequiresPrice': False, + 'iceberg': True, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': True, + 'limit': 1000, + 'daysBack': 100000, + 'untilDays': 100000, + 'symbolRequired': False, + 'marketType': True, + }, + 'fetchOrder': { + 'marginMode': True, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + 'marketType': True, + }, + 'fetchOpenOrders': { + 'marginMode': True, + 'limit': 1000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + 'marketType': True, + }, + 'fetchOrders': None, + 'fetchClosedOrders': { + 'marginMode': True, + 'limit': 1000, + 'daysBack': 100000, # todo + 'daysBackCanceled': 1, # todo + 'untilDays': 100000, # todo + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + 'marketType': True, + }, + 'fetchOHLCV': { + 'limit': 1000, + }, + }, + 'spot': { + 'extends': 'default', + }, + 'forDerivatives': { + 'extends': 'default', + 'createOrder': { + 'marginMode': True, + }, + 'fetchOrder': { + 'marginMode': False, + }, + 'fetchMyTrades': { + 'marginMode': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + }, + 'fetchClosedOrders': { + 'marginMode': False, + }, + }, + 'swap': { + 'linear': { + 'extends': 'forDerivatives', + }, + 'inverse': { + 'extends': 'forDerivatives', + }, + }, + 'future': { + 'linear': { + 'extends': 'forDerivatives', + }, + 'inverse': { + 'extends': 'forDerivatives', + }, + }, + }, + 'timeframes': { + '1m': 'M1', + '3m': 'M3', + '5m': 'M5', + '15m': 'M15', + '30m': 'M30', # default + '1h': 'H1', + '4h': 'H4', + '1d': 'D1', + '1w': 'D7', + '1M': '1M', + }, + 'exceptions': { + 'exact': { + '429': RateLimitExceeded, + '500': ExchangeError, + '503': ExchangeNotAvailable, + '504': ExchangeNotAvailable, + '600': PermissionDenied, + '800': ExchangeError, + '1002': AuthenticationError, + '1003': PermissionDenied, + '1004': AuthenticationError, + '1005': AuthenticationError, + '2001': BadSymbol, + '2002': BadRequest, + '2003': BadRequest, + '2010': BadRequest, + '2011': BadRequest, + '2012': BadRequest, + '2020': BadRequest, + '2022': BadRequest, + '2024': InvalidOrder, # Invalid margin mode. + '10001': BadRequest, + '10021': AccountSuspended, + '10022': BadRequest, + '20001': InsufficientFunds, + '20002': OrderNotFound, + '20003': ExchangeError, + '20004': ExchangeError, + '20005': ExchangeError, + '20006': ExchangeError, + '20007': ExchangeError, + '20008': InvalidOrder, + '20009': InvalidOrder, + '20010': OnMaintenance, + '20011': ExchangeError, + '20012': ExchangeError, + '20014': ExchangeError, + '20016': ExchangeError, + '20018': ExchangeError, # Withdrawals are unavailable due to the current configuration. Any of: - internal withdrawals are disabled; - in-chain withdrawals are disabled. + '20031': ExchangeError, + '20032': ExchangeError, + '20033': ExchangeError, + '20034': ExchangeError, + '20040': ExchangeError, + '20041': ExchangeError, + '20042': ExchangeError, + '20043': ExchangeError, + '20044': PermissionDenied, + '20045': InvalidOrder, + '20047': InvalidOrder, # Order placing exceeds the central counterparty balance limit. + '20048': InvalidOrder, # Provided Time-In-Force instruction is invalid or the combination of the instruction and the order type is not allowed. + '20049': InvalidOrder, # Provided order type is invalid. + '20080': ExchangeError, + '21001': ExchangeError, + '21003': AccountSuspended, + '21004': AccountSuspended, + '22004': ExchangeError, # User is not found. + '22008': ExchangeError, # Gateway timeout exceeded. + }, + 'broad': {}, + }, + 'options': { + 'defaultNetwork': 'ERC20', + 'defaultNetworks': { + 'ETH': 'ETH', + 'USDT': 'TRC20', + }, + 'networks': { + # mainnet network ids are in lowercase for BTC & ETH + 'BTC': 'btc', + 'OMNI': 'BTC', + 'ETH': 'eth', + 'ERC20': 'ETH', + 'ETC': 'ETC', + 'BEP20': 'BSC', + 'TRC20': 'TRX', + # '': 'UGT', + 'NEAR': 'NEAR', + # '': 'LWF', + 'DGB': 'DGB', + # '': 'YOYOW', + 'AE': 'AE', + # 'BCHABC': 'BCHABC', + # '': 'BCI', + # 'BYTECOIN': 'bcn', + 'AR': 'AR', + # '': 'HPC', + 'ADA': 'ADA', + # 'BELDEX': 'BDX', + # 'ARDOR': 'ARDR', + # 'NEBLIO': 'NEBL', + # '': 'DIM', + 'CHZ': 'CHZ', + # '': 'BET', + # '': '8BT', + 'ABBC': 'ABBC', + # '': 'ABTC', + # 'ACHAIN': 'ACT', + # '': 'ADK', + # '': 'AEON', + 'ALGO': 'ALGO', + # 'AMBROSUS': 'AMB', + # '': 'APL', + 'APT': 'APT', + # '': 'ARK', + # 'PIRATECHAIN': 'ARRR', + # '': 'ASP', + # '': 'ATB', + 'ATOM': 'ATOM', + 'AVAXC': 'AVAC', + 'AVAXX': 'AVAX', + # '': 'AYA', + # '': 'B2G', + # '': 'B2X', + # '': 'BANANO', + # '': 'BCCF', + 'BSV': 'BCHSV', + 'BEP2': 'BNB', + # 'BOSON': 'BOS', + # '': 'BRL', # brazilian real + # '': 'BST', + # 'BITCOINADDITION': 'BTCADD', + # '': 'BTCP', + # 'SUPERBTC': 'SBTC', + # 'BITCOINVAULT': 'BTCV', + # 'BITCOINGOLD': 'BTG', + # 'BITCOINDIAMOND': 'BCD', + # 'BITCONNECT': 'BCC', + # '': 'BTM', + # 'BITSHARES_OLD': 'BTS', + # '': 'BTX', + # '': 'BWI', + 'CELO': 'CELO', + # '': 'CENNZ', + # '': 'CHX', + 'CKB': 'CKB', + # 'CALLISTO': 'CLO', + # '': 'CLR', + # '': 'CNX', + # '': 'CRS', + # '': 'CSOV', + 'CTXC': 'CTXC', + # '': 'CURE', + # 'CONSTELLATION': 'DAG', + # '': 'DAPS', + 'DASH': 'DASH', + # '': 'DBIX', + 'DCR': 'DCR', + # '': 'DCT', + # '': 'DDR', + # '': 'DNA', + 'DOGE': 'doge', + # 'POLKADOT': 'DOT', + # '': 'NEWDOT', POLKADOT NEW + # '': 'dsh', + # '': 'ECA', + # '': 'ECOIN', + # '': 'EEX', + 'EGLD': 'EGLD', + # '': 'ELE', + # 'ELECTRONEUM': 'Electroneum', + # '': 'ELM', + # '': 'EMC', + 'EOS': 'EOS', + # 'AERGO': 'ERG', + 'ETHW': 'ETHW', + # 'ETHERLITE': 'ETL', + # '': 'ETP', # metaverse etp + # '': 'EUNO', + 'EVER': 'EVER', + # '': 'EXP', + # '': 'fcn', + 'FET': 'FET', + 'FIL': 'FIL', + # '': 'FIRO', + 'FLOW': 'FLOW', + # '': 'G999', + # '': 'GAME', + # '': 'GASP', + # '': 'GBX', + # '': 'GHOST', + # '': 'GLEEC', + 'GLMR': 'GLMR', + # '': 'GMD', + # '': 'GRAPH', + 'GRIN': 'GRIN', + 'HBAR': 'HBAR', + # '': 'HDG', + 'HIVE': 'HIVE', + # 'HARBOR': 'HRB', + # '': 'HSR', + # '': 'HTML', + 'HYDRA': 'HYDRA', + 'ICP': 'ICP', + 'ICX': 'ICX', + # '': 'IML', + 'IOST': 'IOST', + 'IOTA': 'IOTA', + 'IOTX': 'IOTX', + # '': 'IQ', + 'KAVA': 'KAVA', + 'KLAY': 'KIM', + 'KOMODO': 'KMD', + # '': 'KRM', + 'KSM': 'KSM', + # '': 'LAVA', + # 'LITECOINCASH': 'LCC', + 'LSK': 'LSK', + # '': 'LOC', + 'LTC': 'ltc', + # '': 'LTNM', + # 'TERRACLASSIC': 'LUNA', + # 'TERRA': 'LUNANEW', + # '': 'MAN', + # '': 'MESH', + 'MINA': 'MINA', + # '': 'MNX', + # 'MOBILECOIN': 'MOB', + 'MOVR': 'MOVR', + # '': 'MPK', + # '': 'MRV', + 'NANO': 'NANO', + # '': 'NAV', + 'NEO': 'NEO', + # 'NIMIQ': 'NIM', + # '': 'NJBC', + # '': 'NKN', + # '': 'NLC2', + # '': 'NOF', + # 'ENERGI': 'NRG', + # '': 'nxt', + # '': 'ODN', + 'ONE': 'ONE', + # 'ONTOLOGYGAS': 'ONG', + 'ONT': 'ONT', + 'OPTIMISM': 'OP', + # '': 'PAD', + # '': 'PART', + # '': 'PBKX', + # '': 'PLC', + 'PLCU': 'PLCU', + # '': 'PLI', + # '': 'POA', + 'MATIC': 'POLYGON', + # '': 'PPC', + # '': 'PQT', + # '': 'PROC', + # 'PASTEL': 'PSL', + # '': 'qcn', + 'QTUM': 'QTUM', + # '': 'RCOIN', + 'REI': 'REI', + # '': 'RIF', + # '': 'ROOTS', + 'OASIS': 'ROSE', + # '': 'RPX', + # '': 'RUB', + 'RVN': 'RVN', + # '': 'SBD', + 'SC': 'SC', + 'SCRT': 'SCRT', + # '': 'SLX', + # 'SMARTMESH': 'SMART', + # '': 'SMT', + # '': 'SNM', + 'SOL': 'SOL', + # '': 'SRX', + # '': 'STAK', + 'STEEM': 'STEEM', + # 'STRATIS': 'STRAT', + # '': 'TCN', + # '': 'TENT', + 'THETA': 'Theta', + # '': 'TIV', + # '': 'TNC', + # 'TON': 'TONCOIN', + 'TRUE': 'TRUE', + # '': 'TRY', # turkish lira + # '': 'UNO', + # '': 'USNOTA', + # '': 'VEO', + 'VET': 'VET', + # '': 'VITAE', + # 'VELAS': 'VLX', + 'VSYS': 'VSYS', + # '': 'VTC', + 'WAVES': 'WAVES', + 'WAX': 'WAX', + # '': 'WEALTH', + # 'WALTONCHAIN': 'WTC', + # '': 'WTT', + 'XCH': 'XCH', + # '': 'XDC', # xinfin? + # '': 'xdn', + # '': 'XDNCO', + # '': 'XDNICCO', + 'XEC': 'XEC', + 'NEM': 'XEM', + # 'HAVEN': 'XHV', + # '': 'XLC', + 'XLM': 'XLM', + # '': 'XMO', + 'XMR': 'xmr', + # 'MONEROCLASSIC': 'XMC', + # '': 'XNS', + # '': 'XPRM', + # '': 'XRC', + 'XRD': 'XRD', + 'XRP': 'XRP', + 'XTZ': 'XTZ', + 'XVG': 'XVG', + 'XYM': 'XYM', + 'ZEC': 'ZEC', + 'ZEN': 'ZEN', + 'ZIL': 'ZIL', + # '': 'ZYN', + }, + 'accountsByType': { + 'spot': 'spot', + 'funding': 'wallet', + 'swap': 'derivatives', + 'future': 'derivatives', + }, + 'withdraw': { + 'includeFee': False, + }, + }, + 'commonCurrencies': { + 'AUTO': 'Cube', + 'BCC': 'BCC', # initial symbol for Bitcoin Cash, now inactive + 'BDP': 'BidiPass', + 'BET': 'DAO.Casino', + 'BIT': 'BitRewards', + 'BOX': 'BOX Token', + 'CPT': 'Cryptaur', # conflict with CPT = Contents Protocol https://github.com/ccxt/ccxt/issues/4920 and https://github.com/ccxt/ccxt/issues/6081 + 'GET': 'Themis', + 'GMT': 'GMT Token', + 'HSR': 'HC', + 'IQ': 'IQ.Cash', + 'LNC': 'LinkerCoin', + 'PLA': 'PlayChip', + 'PNT': 'Penta', + 'SBTC': 'Super Bitcoin', + 'STEPN': 'GMT', + 'STX': 'STOX', + 'TV': 'Tokenville', + 'XMT': 'MTL', + 'XPNT': 'PNT', + }, + }) + + def nonce(self): + return self.milliseconds() + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for hitbtc + + https://api.hitbtc.com/#symbols + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = self.publicGetPublicSymbol(params) + # + # { + # "AAVEUSDT_PERP":{ + # "type":"futures", + # "expiry":null, + # "underlying":"AAVE", + # "base_currency":null, + # "quote_currency":"USDT", + # "quantity_increment":"0.01", + # "tick_size":"0.001", + # "take_rate":"0.0005", + # "make_rate":"0.0002", + # "fee_currency":"USDT", + # "margin_trading":true, + # "max_initial_leverage":"50.00" + # }, + # "MANAUSDT":{ + # "type":"spot", + # "base_currency":"MANA", + # "quote_currency":"USDT", + # "quantity_increment":"1", + # "tick_size":"0.0000001", + # "take_rate":"0.0025", + # "make_rate":"0.001", + # "fee_currency":"USDT", + # "margin_trading":true, + # "max_initial_leverage":"5.00" + # }, + # } + # + result = [] + ids = list(response.keys()) + for i in range(0, len(ids)): + id = ids[i] + if id.endswith('_BQX'): + continue # seems like an invalid symbol and if we try to access it individually we get: {"timestamp":"2023-09-02T14:38:20.351Z","error":{"description":"Try get /public/symbol, to get list of all available symbols.","code":2001,"message":"No such symbol: EOSUSD_BQX"},"path":"/api/3/public/symbol/EOSUSD_BQX","requestId":"e1e9fce6-16374591"} + market = self.safe_value(response, id) + marketType = self.safe_string(market, 'type') + expiry = self.safe_integer(market, 'expiry') + contract = (marketType == 'futures') + spot = (marketType == 'spot') + marginTrading = self.safe_bool(market, 'margin_trading', False) + margin = spot and marginTrading + future = (expiry is not None) + swap = (contract and not future) + option = False + baseId = self.safe_string_2(market, 'base_currency', 'underlying') + quoteId = self.safe_string(market, 'quote_currency') + feeCurrencyId = self.safe_string(market, 'fee_currency') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + feeCurrency = self.safe_currency_code(feeCurrencyId) + settleId = None + settle = None + symbol = base + '/' + quote + type = 'spot' + contractSize = None + linear = None + inverse = None + if contract: + contractSize = self.parse_number('1') + settleId = feeCurrencyId + settle = self.safe_currency_code(settleId) + linear = ((quote is not None) and (quote == settle)) + inverse = not linear + symbol = symbol + ':' + settle + if future: + symbol = symbol + '-' + expiry + type = 'future' + else: + type = 'swap' + lotString = self.safe_string(market, 'quantity_increment') + stepString = self.safe_string(market, 'tick_size') + lot = self.parse_number(lotString) + step = self.parse_number(stepString) + result.append({ + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': type, + 'spot': spot, + 'margin': margin, + 'swap': swap, + 'future': future, + 'option': option, + 'active': True, + 'contract': contract, + 'linear': linear, + 'inverse': inverse, + 'taker': self.safe_number(market, 'take_rate'), + 'maker': self.safe_number(market, 'make_rate'), + 'contractSize': contractSize, + 'expiry': expiry, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'feeCurrency': feeCurrency, + 'precision': { + 'amount': lot, + 'price': step, + }, + 'limits': { + 'leverage': { + 'min': self.parse_number('1'), + 'max': self.safe_number(market, 'max_initial_leverage', 1), + }, + 'amount': { + 'min': lot, + 'max': None, + }, + 'price': { + 'min': step, + 'max': None, + }, + 'cost': { + 'min': self.parse_number(Precise.string_mul(lotString, stepString)), + 'max': None, + }, + }, + 'created': None, + 'info': market, + }) + return result + + def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://api.hitbtc.com/#currencies + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = self.publicGetPublicCurrency(params) + # + # { + # "DFC": { + # "full_name": "DeFiScale", + # "crypto": True, + # "payin_enabled": False, + # "payout_enabled": True, + # "transfer_enabled": False, + # "transfer_to_wallet_enabled": True, + # "transfer_to_exchange_enabled": False, + # "sign": "D", + # "crypto_payment_id_name": "", + # "crypto_explorer": "https://etherscan.io/tx/{tx}", + # "precision_transfer": "0.00000001", + # "delisted": False, + # "networks": [ + # { + # "code": "ETH", + # "network_name": "Ethereum", + # "network": "ETH", + # "protocol": "ERC-20", + # "default": True, + # "is_ens_available": True, + # "payin_enabled": True, + # "payout_enabled": True, + # "precision_payout": "0.000000000000000001", + # "payout_fee": "277000.0000000000", + # "payout_is_payment_id": False, + # "payin_payment_id": False, + # "payin_confirmations": "2", + # "contract_address": "0x1b2a76da77d03b7fc21189d9838f55bd849014af", + # "crypto_payment_id_name": "", + # "crypto_explorer": "https://etherscan.io/tx/{tx}", + # "is_multichain": True, + # "asset_id": { + # "contract_address": "0x1b2a76da77d03b7fc21189d9838f55bd849014af" + # } + # } + # ] + # }, + # } + # + result: dict = {} + currencies = list(response.keys()) + for i in range(0, len(currencies)): + currencyId = currencies[i] + code = self.safe_currency_code(currencyId) + entry = response[currencyId] + rawNetworks = self.safe_list(entry, 'networks', []) + networks: dict = {} + for j in range(0, len(rawNetworks)): + rawNetwork = rawNetworks[j] + networkId = self.safe_string_2(rawNetwork, 'protocol', 'network') + networkCode = self.network_id_to_code(networkId) + networkCode = networkCode.upper() if (networkCode is not None) else code # is white label, ensure we safeguard from possible bugs + networks[networkCode] = { + 'info': rawNetwork, + 'id': networkId, + 'network': networkCode, + 'active': None, + 'fee': self.safe_number(rawNetwork, 'payout_fee'), + 'deposit': self.safe_bool(rawNetwork, 'payin_enabled'), + 'withdraw': self.safe_bool(rawNetwork, 'payout_enabled'), + 'precision': self.safe_number(rawNetwork, 'precision_payout'), + 'limits': { + 'withdraw': { + 'min': None, + 'max': None, + }, + }, + } + result[code] = self.safe_currency_structure({ + 'info': entry, + 'code': code, + 'id': currencyId, + 'precision': self.safe_number(entry, 'precision_transfer'), + 'name': self.safe_string(entry, 'full_name'), + 'active': not self.safe_bool(entry, 'delisted'), + 'deposit': self.safe_bool(entry, 'payin_enabled'), + 'withdraw': self.safe_bool(entry, 'payout_enabled'), + 'networks': networks, + 'fee': None, + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + }, + 'type': None, # 'crypto' field emits incorrect values + }) + return result + + def create_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + create a currency deposit address + + https://api.hitbtc.com/#generate-deposit-crypto-address + + :param str code: unified currency code of the currency for the deposit address + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + } + network = self.safe_string_upper(params, 'network') + if (network is not None) and (code == 'USDT'): + networks = self.safe_value(self.options, 'networks') + parsedNetwork = self.safe_string(networks, network) + if parsedNetwork is not None: + request['currency'] = parsedNetwork + params = self.omit(params, 'network') + response = self.privatePostWalletCryptoAddress(self.extend(request, params)) + # + # {"currency":"ETH","address":"0xd0d9aea60c41988c3e68417e2616065617b7afd3"} + # + currencyId = self.safe_string(response, 'currency') + return { + 'currency': self.safe_currency_code(currencyId), + 'address': self.safe_string(response, 'address'), + 'tag': self.safe_string(response, 'payment_id'), + 'network': None, + 'info': response, + } + + def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://api.hitbtc.com/#get-deposit-crypto-address + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + } + network = self.safe_string_upper(params, 'network') + if (network is not None) and (code == 'USDT'): + networks = self.safe_value(self.options, 'networks') + parsedNetwork = self.safe_string(networks, network) + if parsedNetwork is not None: + request['currency'] = parsedNetwork + params = self.omit(params, 'network') + response = self.privateGetWalletCryptoAddress(self.extend(request, params)) + # + # [{"currency":"ETH","address":"0xd0d9aea60c41988c3e68417e2616065617b7afd3"}] + # + firstAddress = self.safe_value(response, 0) + address = self.safe_string(firstAddress, 'address') + currencyId = self.safe_string(firstAddress, 'currency') + tag = self.safe_string(firstAddress, 'payment_id') + parsedCode = self.safe_currency_code(currencyId) + return { + 'info': response, + 'currency': parsedCode, + 'network': None, + 'address': address, + 'tag': tag, + } + + def parse_balance(self, response) -> Balances: + result: dict = {'info': response} + for i in range(0, len(response)): + entry = response[i] + currencyId = self.safe_string(entry, 'currency') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(entry, 'available') + account['used'] = self.safe_string(entry, 'reserved') + result[code] = account + return self.safe_balance(result) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://api.hitbtc.com/#wallet-balance + https://api.hitbtc.com/#get-spot-trading-balance + https://api.hitbtc.com/#get-trading-balance + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + type = self.safe_string_lower(params, 'type', 'spot') + params = self.omit(params, ['type']) + accountsByType = self.safe_value(self.options, 'accountsByType', {}) + account = self.safe_string(accountsByType, type, type) + response = None + if account == 'wallet': + response = self.privateGetWalletBalance(params) + elif account == 'spot': + response = self.privateGetSpotBalance(params) + elif account == 'derivatives': + response = self.privateGetFuturesBalance(params) + else: + keys = list(accountsByType.keys()) + raise BadRequest(self.id + ' fetchBalance() type parameter must be one of ' + ', '.join(keys)) + # + # [ + # { + # "currency": "PAXG", + # "available": "0", + # "reserved": "0", + # "reserved_margin": "0", + # }, + # ... + # ] + # + return self.parse_balance(response) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://api.hitbtc.com/#tickers + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = self.publicGetPublicTickerSymbol(self.extend(request, params)) + # + # { + # "ask": "0.020572", + # "bid": "0.020566", + # "last": "0.020574", + # "low": "0.020388", + # "high": "0.021084", + # "open": "0.020913", + # "volume": "138444.3666", + # "volume_quote": "2853.6874972480", + # "timestamp": "2021-06-02T17:52:36.731Z" + # } + # + return self.parse_ticker(response, market) + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://api.hitbtc.com/#tickers + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + symbols = self.market_symbols(symbols) + request: dict = {} + if symbols is not None: + marketIds = self.market_ids(symbols) + delimited = ','.join(marketIds) + request['symbols'] = delimited + response = self.publicGetPublicTicker(self.extend(request, params)) + # + # { + # "BTCUSDT": { + # "ask": "63049.06", + # "bid": "63046.41", + # "last": "63048.36", + # "low": "62010.00", + # "high": "66657.99", + # "open": "64839.75", + # "volume": "15272.13278", + # "volume_quote": "976312127.6277998", + # "timestamp": "2021-10-22T04:25:47.573Z" + # } + # } + # + result: dict = {} + keys = list(response.keys()) + for i in range(0, len(keys)): + marketId = keys[i] + market = self.safe_market(marketId) + symbol = market['symbol'] + entry = response[marketId] + result[symbol] = self.parse_ticker(entry, market) + return self.filter_by_array_tickers(result, 'symbol', symbols) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "ask": "62756.01", + # "bid": "62754.09", + # "last": "62755.87", + # "low": "62010.00", + # "high": "66657.99", + # "open": "65089.27", + # "volume": "16719.50366", + # "volume_quote": "1063422878.8156828", + # "timestamp": "2021-10-22T07:29:14.585Z" + # } + # + timestamp = self.parse8601(ticker['timestamp']) + symbol = self.safe_symbol(None, market) + baseVolume = self.safe_string(ticker, 'volume') + quoteVolume = self.safe_string(ticker, 'volume_quote') + open = self.safe_string(ticker, 'open') + last = self.safe_string(ticker, 'last') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': self.safe_string(ticker, 'bid'), + 'bidVolume': None, + 'ask': self.safe_string(ticker, 'ask'), + 'askVolume': None, + 'vwap': None, + 'open': open, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'info': ticker, + }, market) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://api.hitbtc.com/#trades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = None + request: dict = {} + if limit is not None: + request['limit'] = min(limit, 1000) + if since is not None: + request['from'] = since + response = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + response = self.publicGetPublicTradesSymbol(self.extend(request, params)) + else: + response = self.publicGetPublicTrades(self.extend(request, params)) + if symbol is not None: + return self.parse_trades(response, market) + trades = [] + marketIds = list(response.keys()) + for i in range(0, len(marketIds)): + marketId = marketIds[i] + marketInner = self.market(marketId) + rawTrades = response[marketId] + parsed = self.parse_trades(rawTrades, marketInner) + trades = self.array_concat(trades, parsed) + return trades + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://api.hitbtc.com/#spot-trades-history + https://api.hitbtc.com/#futures-trades-history + https://api.hitbtc.com/#margin-trades-history + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' only 'isolated' is supported + :param bool [params.margin]: True for fetching margin trades + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = None + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if limit is not None: + request['limit'] = limit + if since is not None: + request['from'] = since + marketType = None + marginMode = None + response = None + marketType, params = self.handle_market_type_and_params('fetchMyTrades', market, params) + marginMode, params = self.handle_margin_mode_and_params('fetchMyTrades', params) + params = self.omit(params, ['marginMode', 'margin']) + if marginMode is not None: + response = self.privateGetMarginHistoryTrade(self.extend(request, params)) + else: + if marketType == 'spot': + response = self.privateGetSpotHistoryTrade(self.extend(request, params)) + elif marketType == 'swap': + response = self.privateGetFuturesHistoryTrade(self.extend(request, params)) + elif marketType == 'margin': + response = self.privateGetMarginHistoryTrade(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchMyTrades() not support self market type') + return self.parse_trades(response, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # createOrder(market) + # + # { + # "id": "1569252895", + # "position_id": "0", + # "quantity": "10", + # "price": "0.03919424", + # "fee": "0.000979856000", + # "timestamp": "2022-01-25T19:38:36.153Z", + # "taker": True + # } + # + # fetchTrades + # + # { + # "id": 974786185, + # "price": "0.032462", + # "qty": "0.3673", + # "side": "buy", + # "timestamp": "2020-10-16T12:57:39.846Z" + # } + # + # fetchMyTrades spot + # + # { + # "id": 277210397, + # "clientOrderId": "6e102f3e7f3f4e04aeeb1cdc95592f1a", + # "orderId": 28102855393, + # "symbol": "ETHBTC", + # "side": "sell", + # "quantity": "0.002", + # "price": "0.073365", + # "fee": "0.000000147", + # "timestamp": "2018-04-28T18:39:55.345Z", + # "taker": True + # } + # + # fetchMyTrades swap and margin + # + # { + # "id": 4718564, + # "order_id": 58730811958, + # "client_order_id": "475c47d97f867f09726186eb22b4c3d4", + # "symbol": "BTCUSDT_PERP", + # "side": "sell", + # "quantity": "0.0001", + # "price": "41118.51", + # "fee": "0.002055925500", + # "timestamp": "2022-03-17T05:23:17.795Z", + # "taker": True, + # "position_id": 2350122, + # "pnl": "0.002255000000", + # "liquidation": False + # } + # + timestamp = self.parse8601(trade['timestamp']) + marketId = self.safe_string(trade, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + fee = None + feeCostString = self.safe_string(trade, 'fee') + taker = self.safe_value(trade, 'taker') + takerOrMaker: str + if taker is not None: + takerOrMaker = 'taker' if taker else 'maker' + else: + takerOrMaker = 'taker' # the only case when `taker` field is missing, is public fetchTrades and it must be taker + if feeCostString is not None: + info = self.safe_value(market, 'info', {}) + feeCurrency = self.safe_string(info, 'fee_currency') + feeCurrencyCode = self.safe_currency_code(feeCurrency) + fee = { + 'cost': feeCostString, + 'currency': feeCurrencyCode, + } + # we use clientOrderId order id with self exchange intentionally + # because most of their endpoints will require clientOrderId + # explained here: https://github.com/ccxt/ccxt/issues/5674 + orderId = self.safe_string_2(trade, 'clientOrderId', 'client_order_id') + priceString = self.safe_string(trade, 'price') + amountString = self.safe_string_2(trade, 'quantity', 'qty') + side = self.safe_string(trade, 'side') + id = self.safe_string(trade, 'id') + return self.safe_trade({ + 'info': trade, + 'id': id, + 'order': orderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'type': None, + 'side': side, + 'takerOrMaker': takerOrMaker, + 'price': priceString, + 'amount': amountString, + 'cost': None, + 'fee': fee, + }, market) + + def fetch_transactions_helper(self, types, code, since, limit, params): + self.load_markets() + request: dict = { + 'types': types, + } + currency = None + if code is not None: + currency = self.currency(code) + request['currencies'] = currency['id'] + if since is not None: + request['from'] = self.iso8601(since) + if limit is not None: + request['limit'] = limit + response = self.privateGetWalletTransactions(self.extend(request, params)) + # + # [ + # { + # "id": "101609495", + # "created_at": "2018-03-06T22:05:06.507Z", + # "updated_at": "2018-03-06T22:11:45.03Z", + # "status": "SUCCESS", + # "type": "DEPOSIT", + # "subtype": "BLOCKCHAIN", + # "native": { + # "tx_id": "e20b0965-4024-44d0-b63f-7fb8996a6706", + # "index": "881652766", + # "currency": "ETH", + # "amount": "0.01418088", + # "hash": "d95dbbff3f9234114f1211ab0ba2a94f03f394866fd5749d74a1edab80e6c5d3", + # "address": "0xd9259302c32c0a0295d86a39185c9e14f6ba0a0d", + # "confirmations": "20", + # "senders": [ + # "0x243bec9256c9a3469da22103891465b47583d9f1" + # ] + # } + # } + # ] + # + return self.parse_transactions(response, currency, since, limit, params) + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'CREATED': 'pending', + 'PENDING': 'pending', + 'FAILED': 'failed', + 'ROLLED_BACK': 'failed', + 'SUCCESS': 'ok', + } + return self.safe_string(statuses, status, status) + + def parse_transaction_type(self, type): + types: dict = { + 'DEPOSIT': 'deposit', + 'WITHDRAW': 'withdrawal', + } + return self.safe_string(types, type, type) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # transaction + # + # { + # "id": "101609495", + # "created_at": "2018-03-06T22:05:06.507Z", + # "updated_at": "2018-03-06T22:11:45.03Z", + # "status": "SUCCESS", + # "type": "DEPOSIT", # DEPOSIT, WITHDRAW, .. + # "subtype": "BLOCKCHAIN", + # "native": { + # "tx_id": "e20b0965-4024-44d0-b63f-7fb8996a6706", + # "index": "881652766", + # "currency": "ETH", + # "amount": "0.01418088", + # "hash": "d95dbbff3f9234114f1211ab0ba2a94f03f394866fd5749d74a1edab80e6c5d3", + # "address": "0xd9259302c32c0a0295d86a39185c9e14f6ba0a0d", + # "confirmations": "20", + # "senders": [ + # "0x243bec9256c9a3469da22103891465b47583d9f1" + # ], + # "fee": "1.22" # only for WITHDRAW + # } + # }, + # "operation_id": "084cfcd5-06b9-4826-882e-fdb75ec3625d", # only for WITHDRAW + # "commit_risk": {} + # withdraw + # + # { + # "id":"084cfcd5-06b9-4826-882e-fdb75ec3625d" + # } + # + id = self.safe_string_2(transaction, 'operation_id', 'id') + timestamp = self.parse8601(self.safe_string(transaction, 'created_at')) + updated = self.parse8601(self.safe_string(transaction, 'updated_at')) + type = self.parse_transaction_type(self.safe_string(transaction, 'type')) + status = self.parse_transaction_status(self.safe_string(transaction, 'status')) + native = self.safe_value(transaction, 'native', {}) + currencyId = self.safe_string(native, 'currency') + code = self.safe_currency_code(currencyId) + txhash = self.safe_string(native, 'hash') + address = self.safe_string(native, 'address') + addressTo = address + tag = self.safe_string(native, 'payment_id') + tagTo = tag + sender = self.safe_value(native, 'senders') + addressFrom = self.safe_string(sender, 0) + amount = self.safe_number(native, 'amount') + subType = self.safe_string(transaction, 'subtype') + internal = subType == 'OFFCHAIN' + # https://api.hitbtc.com/#check-if-offchain-is-available + fee = { + 'currency': None, + 'cost': None, + 'rate': None, + } + feeCost = self.safe_number(native, 'fee') + if feeCost is not None: + fee['currency'] = code + fee['cost'] = feeCost + return { + 'info': transaction, + 'id': id, + 'txid': txhash, + 'type': type, + 'currency': code, + 'network': None, + 'amount': amount, + 'status': status, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'address': address, + 'addressFrom': addressFrom, + 'addressTo': addressTo, + 'tag': tag, + 'tagFrom': None, + 'tagTo': tagTo, + 'updated': updated, + 'comment': None, + 'internal': internal, + 'fee': fee, + } + + def fetch_deposits_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch history of deposits and withdrawals + + https://api.hitbtc.com/#get-transactions-history + + :param str [code]: unified currency code for the currency of the deposit/withdrawals, default is None + :param int [since]: timestamp in ms of the earliest deposit/withdrawal, default is None + :param int [limit]: max number of deposit/withdrawals to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `transaction structure ` + """ + return self.fetch_transactions_helper('DEPOSIT,WITHDRAW', code, since, limit, params) + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://api.hitbtc.com/#get-transactions-history + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + return self.fetch_transactions_helper('DEPOSIT', code, since, limit, params) + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://api.hitbtc.com/#get-transactions-history + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + return self.fetch_transactions_helper('WITHDRAW', code, since, limit, params) + + def fetch_order_books(self, symbols: Strings = None, limit: Int = None, params={}) -> OrderBooks: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data for multiple markets + + https://api.hitbtc.com/#order-books + + :param str[] [symbols]: list of unified market symbols, all symbols fetched if None, default is None + :param int [limit]: max number of entries per orderbook to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `order book structures ` indexed by market symbol + """ + self.load_markets() + request: dict = {} + if symbols is not None: + marketIdsInner = self.market_ids(symbols) + request['symbols'] = ','.join(marketIdsInner) + if limit is not None: + request['depth'] = limit + response = self.publicGetPublicOrderbook(self.extend(request, params)) + result: dict = {} + marketIds = list(response.keys()) + for i in range(0, len(marketIds)): + marketId = marketIds[i] + orderbook = response[marketId] + symbol = self.safe_symbol(marketId) + timestamp = self.parse8601(self.safe_string(orderbook, 'timestamp')) + result[symbol] = self.parse_order_book(response[marketId], symbol, timestamp, 'bid', 'ask') + return result + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://api.hitbtc.com/#order-books + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['depth'] = limit + response = self.publicGetPublicOrderbookSymbol(self.extend(request, params)) + timestamp = self.parse8601(self.safe_string(response, 'timestamp')) + return self.parse_order_book(response, symbol, timestamp, 'bid', 'ask') + + def parse_trading_fee(self, fee: dict, market: Market = None) -> TradingFeeInterface: + # + # { + # "symbol":"ARVUSDT", # returned from fetchTradingFees only + # "take_rate":"0.0009", + # "make_rate":"0.0009" + # } + # + taker = self.safe_number(fee, 'take_rate') + maker = self.safe_number(fee, 'make_rate') + marketId = self.safe_string(fee, 'symbol') + symbol = self.safe_symbol(marketId, market) + return { + 'info': fee, + 'symbol': symbol, + 'taker': taker, + 'maker': maker, + 'percentage': None, + 'tierBased': None, + } + + def fetch_trading_fee(self, symbol: str, params={}) -> TradingFeeInterface: + """ + fetch the trading fees for a market + + https://api.hitbtc.com/#get-trading-commission + https://api.hitbtc.com/#get-trading-commission-2 + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `fee structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = None + if market['type'] == 'spot': + response = self.privateGetSpotFeeSymbol(self.extend(request, params)) + elif market['type'] == 'swap': + response = self.privateGetFuturesFeeSymbol(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchTradingFee() not support self market type') + # + # { + # "take_rate":"0.0009", + # "make_rate":"0.0009" + # } + # + return self.parse_trading_fee(response, market) + + def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://api.hitbtc.com/#get-all-trading-commissions + https://api.hitbtc.com/#get-all-trading-commissions-2 + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + self.load_markets() + marketType, query = self.handle_market_type_and_params('fetchTradingFees', None, params) + response = None + if marketType == 'spot': + response = self.privateGetSpotFee(query) + elif marketType == 'swap': + response = self.privateGetFuturesFee(query) + else: + raise NotSupported(self.id + ' fetchTradingFees() not support self market type') + # + # [ + # { + # "symbol":"ARVUSDT", + # "take_rate":"0.0009", + # "make_rate":"0.0009" + # } + # ] + # + result: dict = {} + for i in range(0, len(response)): + fee = self.parse_trading_fee(response[i]) + symbol = fee['symbol'] + result[symbol] = fee + return result + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://api.hitbtc.com/#candles + https://api.hitbtc.com/#futures-index-price-candles + https://api.hitbtc.com/#futures-mark-price-candles + https://api.hitbtc.com/#futures-premium-index-candles + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest funding rate + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate') + if paginate: + return self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params, 1000) + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'period': self.safe_string(self.timeframes, timeframe, timeframe), + } + if since is not None: + request['from'] = self.iso8601(since) + request, params = self.handle_until_option('until', request, params) + if limit is not None: + request['limit'] = min(limit, 1000) + price = self.safe_string(params, 'price') + params = self.omit(params, 'price') + response = None + if price == 'mark': + response = self.publicGetPublicFuturesCandlesMarkPriceSymbol(self.extend(request, params)) + elif price == 'index': + response = self.publicGetPublicFuturesCandlesIndexPriceSymbol(self.extend(request, params)) + elif price == 'premiumIndex': + response = self.publicGetPublicFuturesCandlesPremiumIndexSymbol(self.extend(request, params)) + else: + response = self.publicGetPublicCandlesSymbol(self.extend(request, params)) + # + # Spot and Swap + # + # [ + # { + # "timestamp": "2021-10-25T07:38:00.000Z", + # "open": "4173.391", + # "close": "4170.923", + # "min": "4170.923", + # "max": "4173.986", + # "volume": "0.1879", + # "volume_quote": "784.2517846" + # } + # ] + # + # Mark, Index and Premium Index + # + # [ + # { + # "timestamp": "2022-04-01T01:28:00.000Z", + # "open": "45146.39", + # "close": "45219.43", + # "min": "45146.39", + # "max": "45219.43" + # }, + # ] + # + return self.parse_ohlcvs(response, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # Spot and Swap + # + # { + # "timestamp":"2015-08-20T19:01:00.000Z", + # "open":"0.006", + # "close":"0.006", + # "min":"0.006", + # "max":"0.006", + # "volume":"0.003", + # "volume_quote":"0.000018" + # } + # + # Mark, Index and Premium Index + # + # { + # "timestamp": "2022-04-01T01:28:00.000Z", + # "open": "45146.39", + # "close": "45219.43", + # "min": "45146.39", + # "max": "45219.43" + # }, + # + return [ + self.parse8601(self.safe_string(ohlcv, 'timestamp')), + self.safe_number(ohlcv, 'open'), + self.safe_number(ohlcv, 'max'), + self.safe_number(ohlcv, 'min'), + self.safe_number(ohlcv, 'close'), + self.safe_number(ohlcv, 'volume'), + ] + + def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://api.hitbtc.com/#spot-orders-history + https://api.hitbtc.com/#futures-orders-history + https://api.hitbtc.com/#margin-orders-history + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' only 'isolated' is supported + :param bool [params.margin]: True for fetching margin orders + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + market = None + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if since is not None: + request['from'] = self.iso8601(since) + if limit is not None: + request['limit'] = limit + marketType = None + marginMode = None + marketType, params = self.handle_market_type_and_params('fetchClosedOrders', market, params) + marginMode, params = self.handle_margin_mode_and_params('fetchClosedOrders', params) + params = self.omit(params, ['marginMode', 'margin']) + response = None + if marginMode is not None: + response = self.privateGetMarginHistoryOrder(self.extend(request, params)) + else: + if marketType == 'spot': + response = self.privateGetSpotHistoryOrder(self.extend(request, params)) + elif marketType == 'swap': + response = self.privateGetFuturesHistoryOrder(self.extend(request, params)) + elif marketType == 'margin': + response = self.privateGetMarginHistoryOrder(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchClosedOrders() not support self market type') + parsed = self.parse_orders(response, market, since, limit) + return self.filter_by_array(parsed, 'status', ['closed', 'canceled'], False) + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://api.hitbtc.com/#spot-orders-history + https://api.hitbtc.com/#futures-orders-history + https://api.hitbtc.com/#margin-orders-history + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' only 'isolated' is supported + :param bool [params.margin]: True for fetching a margin order + :returns dict: An `order structure ` + """ + self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + request: dict = { + 'client_order_id': id, + } + marketType = None + marginMode = None + marketType, params = self.handle_market_type_and_params('fetchOrder', market, params) + marginMode, params = self.handle_margin_mode_and_params('fetchOrder', params) + params = self.omit(params, ['marginMode', 'margin']) + response = None + if marginMode is not None: + response = self.privateGetMarginHistoryOrder(self.extend(request, params)) + else: + if marketType == 'spot': + response = self.privateGetSpotHistoryOrder(self.extend(request, params)) + elif marketType == 'swap': + response = self.privateGetFuturesHistoryOrder(self.extend(request, params)) + elif marketType == 'margin': + response = self.privateGetMarginHistoryOrder(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchOrder() not support self market type') + # + # [ + # { + # "id": "685965182082", + # "client_order_id": "B3CBm9uGg9oYQlw96bBSEt38-6gbgBO0", + # "symbol": "BTCUSDT", + # "side": "buy", + # "status": "new", + # "type": "limit", + # "time_in_force": "GTC", + # "quantity": "0.00010", + # "quantity_cumulative": "0", + # "price": "50000.00", + # "price_average": "0", + # "created_at": "2021-10-26T11:40:09.287Z", + # "updated_at": "2021-10-26T11:40:09.287Z" + # } + # ] + # + order = self.safe_dict(response, 0) + return self.parse_order(order, market) + + def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all the trades made from a single order + + https://api.hitbtc.com/#spot-trades-history + https://api.hitbtc.com/#futures-trades-history + https://api.hitbtc.com/#margin-trades-history + + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' only 'isolated' is supported + :param bool [params.margin]: True for fetching margin trades + :returns dict[]: a list of `trade structures ` + """ + self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + request: dict = { + 'order_id': id, # exchange assigned order id to the client order id + } + marketType = None + marginMode = None + marketType, params = self.handle_market_type_and_params('fetchOrderTrades', market, params) + marginMode, params = self.handle_margin_mode_and_params('fetchOrderTrades', params) + params = self.omit(params, ['marginMode', 'margin']) + response = None + if marginMode is not None: + response = self.privateGetMarginHistoryTrade(self.extend(request, params)) + else: + if marketType == 'spot': + response = self.privateGetSpotHistoryTrade(self.extend(request, params)) + elif marketType == 'swap': + response = self.privateGetFuturesHistoryTrade(self.extend(request, params)) + elif marketType == 'margin': + response = self.privateGetMarginHistoryTrade(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchOrderTrades() not support self market type') + # + # Spot + # + # [ + # { + # "id": 1393448977, + # "order_id": 653496804534, + # "client_order_id": "065f6f0ff9d54547848454182263d7b4", + # "symbol": "DICEETH", + # "side": "buy", + # "quantity": "1.4", + # "price": "0.00261455", + # "fee": "0.000003294333", + # "timestamp": "2021-09-19T05:35:56.601Z", + # "taker": True + # } + # ] + # + # Swap and Margin + # + # [ + # { + # "id": 4718551, + # "order_id": 58730748700, + # "client_order_id": "dcbcd8549e3445ee922665946002ef67", + # "symbol": "BTCUSDT_PERP", + # "side": "buy", + # "quantity": "0.0001", + # "price": "41095.96", + # "fee": "0.002054798000", + # "timestamp": "2022-03-17T05:23:02.217Z", + # "taker": True, + # "position_id": 2350122, + # "pnl": "0", + # "liquidation": False + # } + # ] + # + return self.parse_trades(response, market, since, limit) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://api.hitbtc.com/#get-all-active-spot-orders + https://api.hitbtc.com/#get-active-futures-orders + https://api.hitbtc.com/#get-active-margin-orders + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' only 'isolated' is supported + :param bool [params.margin]: True for fetching open margin orders + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + market = None + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + marketType = None + marginMode = None + marketType, params = self.handle_market_type_and_params('fetchOpenOrders', market, params) + marginMode, params = self.handle_margin_mode_and_params('fetchOpenOrders', params) + params = self.omit(params, ['marginMode', 'margin']) + response = None + if marginMode is not None: + response = self.privateGetMarginOrder(self.extend(request, params)) + else: + if marketType == 'spot': + response = self.privateGetSpotOrder(self.extend(request, params)) + elif marketType == 'swap': + response = self.privateGetFuturesOrder(self.extend(request, params)) + elif marketType == 'margin': + response = self.privateGetMarginOrder(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchOpenOrders() not support self market type') + # + # [ + # { + # "id": "488953123149", + # "client_order_id": "103ad305301e4c3590045b13de15b36e", + # "symbol": "BTCUSDT", + # "side": "buy", + # "status": "new", + # "type": "limit", + # "time_in_force": "GTC", + # "quantity": "0.00001", + # "quantity_cumulative": "0", + # "price": "0.01", + # "post_only": False, + # "created_at": "2021-04-13T13:06:16.567Z", + # "updated_at": "2021-04-13T13:06:16.567Z" + # } + # ] + # + return self.parse_orders(response, market, since, limit) + + def fetch_open_order(self, id: str, symbol: Str = None, params={}): + """ + fetch an open order by it's id + + https://api.hitbtc.com/#get-active-spot-order + https://api.hitbtc.com/#get-active-futures-order + https://api.hitbtc.com/#get-active-margin-order + + :param str id: order id + :param str symbol: unified market symbol, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' only 'isolated' is supported + :param bool [params.margin]: True for fetching an open margin order + :returns dict: an `order structure ` + """ + self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + request: dict = { + 'client_order_id': id, + } + marketType = None + marginMode = None + marketType, params = self.handle_market_type_and_params('fetchOpenOrder', market, params) + marginMode, params = self.handle_margin_mode_and_params('fetchOpenOrder', params) + params = self.omit(params, ['marginMode', 'margin']) + response = None + if marginMode is not None: + response = self.privateGetMarginOrderClientOrderId(self.extend(request, params)) + else: + if marketType == 'spot': + response = self.privateGetSpotOrderClientOrderId(self.extend(request, params)) + elif marketType == 'swap': + response = self.privateGetFuturesOrderClientOrderId(self.extend(request, params)) + elif marketType == 'margin': + response = self.privateGetMarginOrderClientOrderId(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchOpenOrder() not support self market type') + return self.parse_order(response, market) + + def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders + + https://api.hitbtc.com/#cancel-all-spot-orders + https://api.hitbtc.com/#cancel-futures-orders + https://api.hitbtc.com/#cancel-all-margin-orders + + :param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' only 'isolated' is supported + :param bool [params.margin]: True for canceling margin orders + :returns dict[]: a list of `order structures ` + """ + self.load_markets() + market = None + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + marketType = None + marginMode = None + marketType, params = self.handle_market_type_and_params('cancelAllOrders', market, params) + marginMode, params = self.handle_margin_mode_and_params('cancelAllOrders', params) + params = self.omit(params, ['marginMode', 'margin']) + response = None + if marginMode is not None: + response = self.privateDeleteMarginOrder(self.extend(request, params)) + else: + if marketType == 'spot': + response = self.privateDeleteSpotOrder(self.extend(request, params)) + elif marketType == 'swap': + response = self.privateDeleteFuturesOrder(self.extend(request, params)) + elif marketType == 'margin': + response = self.privateDeleteMarginOrder(self.extend(request, params)) + else: + raise NotSupported(self.id + ' cancelAllOrders() not support self market type') + return self.parse_orders(response, market) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://api.hitbtc.com/#cancel-spot-order + https://api.hitbtc.com/#cancel-futures-order + https://api.hitbtc.com/#cancel-margin-order + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' only 'isolated' is supported + :param bool [params.margin]: True for canceling a margin order + :returns dict: An `order structure ` + """ + self.load_markets() + market = None + request: dict = { + 'client_order_id': id, + } + if symbol is not None: + market = self.market(symbol) + marketType = None + marginMode = None + marketType, params = self.handle_market_type_and_params('cancelOrder', market, params) + marginMode, params = self.handle_margin_mode_and_params('cancelOrder', params) + params = self.omit(params, ['marginMode', 'margin']) + response = None + if marginMode is not None: + response = self.privateDeleteMarginOrderClientOrderId(self.extend(request, params)) + else: + if marketType == 'spot': + response = self.privateDeleteSpotOrderClientOrderId(self.extend(request, params)) + elif marketType == 'swap': + response = self.privateDeleteFuturesOrderClientOrderId(self.extend(request, params)) + elif marketType == 'margin': + response = self.privateDeleteMarginOrderClientOrderId(self.extend(request, params)) + else: + raise NotSupported(self.id + ' cancelOrder() not support self market type') + return self.parse_order(response, market) + + def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + self.load_markets() + market = None + request: dict = { + 'client_order_id': id, + 'quantity': self.amount_to_precision(symbol, amount), + } + if (type == 'limit') or (type == 'stopLimit'): + if price is None: + raise ExchangeError(self.id + ' editOrder() limit order requires price') + request['price'] = self.price_to_precision(symbol, price) + if symbol is not None: + market = self.market(symbol) + marketType = None + marginMode = None + marketType, params = self.handle_market_type_and_params('editOrder', market, params) + marginMode, params = self.handle_margin_mode_and_params('editOrder', params) + params = self.omit(params, ['marginMode', 'margin']) + response = None + if marginMode is not None: + response = self.privatePatchMarginOrderClientOrderId(self.extend(request, params)) + else: + if marketType == 'spot': + response = self.privatePatchSpotOrderClientOrderId(self.extend(request, params)) + elif marketType == 'swap': + response = self.privatePatchFuturesOrderClientOrderId(self.extend(request, params)) + elif marketType == 'margin': + response = self.privatePatchMarginOrderClientOrderId(self.extend(request, params)) + else: + raise NotSupported(self.id + ' editOrder() not support self market type') + return self.parse_order(response, market) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://api.hitbtc.com/#create-new-spot-order + https://api.hitbtc.com/#create-margin-order + https://api.hitbtc.com/#create-futures-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' only 'isolated' is supported for spot-margin, swap supports both, default is 'cross' + :param bool [params.margin]: True for creating a margin order + :param float [params.triggerPrice]: The price at which a trigger order is triggered at + :param bool [params.postOnly]: if True, the order will only be posted to the order book and not executed immediately + :param str [params.timeInForce]: "GTC", "IOC", "FOK", "Day", "GTD" + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + request = None + marketType = None + marketType, params = self.handle_market_type_and_params('createOrder', market, params) + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('createOrder', params) + request, params = self.create_order_request(market, marketType, type, side, amount, price, marginMode, params) + response = None + if marketType == 'swap': + response = self.privatePostFuturesOrder(self.extend(request, params)) + elif (marketType == 'margin') or (marginMode is not None): + response = self.privatePostMarginOrder(self.extend(request, params)) + else: + response = self.privatePostSpotOrder(self.extend(request, params)) + return self.parse_order(response, market) + + def create_order_request(self, market: object, marketType: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, marginMode: Str = None, params={}): + isLimit = (type == 'limit') + reduceOnly = self.safe_value(params, 'reduceOnly') + timeInForce = self.safe_string(params, 'timeInForce') + triggerPrice = self.safe_number_n(params, ['triggerPrice', 'stopPrice', 'stop_price']) + isPostOnly = self.is_post_only(type == 'market', None, params) + request: dict = { + 'type': type, + 'side': side, + 'quantity': self.amount_to_precision(market['symbol'], amount), + 'symbol': market['id'], + # 'client_order_id': 'r42gdPjNMZN-H_xs8RKl2wljg_dfgdg4', # Optional + # 'time_in_force': 'GTC', # Optional GTC, IOC, FOK, Day, GTD + # 'price': self.price_to_precision(symbol, price), # Required if type is limit, stopLimit, or takeProfitLimit + # 'stop_price': self.safe_number(params, 'stop_price'), # Required if type is stopLimit, stopMarket, takeProfitLimit, takeProfitMarket + # 'expire_time': '2021-06-15T17:01:05.092Z', # Required if timeInForce is GTD + # 'strict_validate': False, + # 'post_only': False, # Optional + # 'reduce_only': False, # Optional + # 'display_quantity': '0', # Optional + # 'take_rate': 0.001, # Optional + # 'make_rate': 0.001, # Optional + } + if reduceOnly is not None: + if (market['type'] != 'swap') and (market['type'] != 'margin'): + raise InvalidOrder(self.id + ' createOrder() does not support reduce_only for ' + market['type'] + ' orders, reduce_only orders are supported for swap and margin markets only') + if reduceOnly is True: + request['reduce_only'] = reduceOnly + if isPostOnly: + request['post_only'] = True + if timeInForce is not None: + request['time_in_force'] = timeInForce + if isLimit or (type == 'stopLimit') or (type == 'takeProfitLimit'): + if price is None: + raise ExchangeError(self.id + ' createOrder() requires a price argument for limit orders') + request['price'] = self.price_to_precision(market['symbol'], price) + if (timeInForce == 'GTD'): + expireTime = self.safe_string(params, 'expire_time') + if expireTime is None: + raise ExchangeError(self.id + ' createOrder() requires an expire_time parameter for a GTD order') + if triggerPrice is not None: + request['stop_price'] = self.price_to_precision(market['symbol'], triggerPrice) + if isLimit: + request['type'] = 'stopLimit' + elif type == 'market': + request['type'] = 'stopMarket' + elif (type == 'stopLimit') or (type == 'stopMarket') or (type == 'takeProfitLimit') or (type == 'takeProfitMarket'): + raise ExchangeError(self.id + ' createOrder() requires a triggerPrice parameter for stop-loss and take-profit orders') + params = self.omit(params, ['triggerPrice', 'timeInForce', 'stopPrice', 'stop_price', 'reduceOnly', 'postOnly']) + if marketType == 'swap': + # set default margin mode to cross + if marginMode is None: + marginMode = 'cross' + request['margin_mode'] = marginMode + return [request, params] + + def parse_order_status(self, status: Str): + statuses: dict = { + 'new': 'open', + 'suspended': 'open', + 'partiallyFilled': 'open', + 'filled': 'closed', + 'canceled': 'canceled', + 'expired': 'failed', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # limit + # { + # "id": 488953123149, + # "client_order_id": "103ad305301e4c3590045b13de15b36e", + # "symbol": "BTCUSDT", + # "side": "buy", + # "status": "new", + # "type": "limit", + # "time_in_force": "GTC", + # "quantity": "0.00001", + # "quantity_cumulative": "0", + # "price": "0.01", + # "price_average": "0.01", + # "post_only": False, + # "created_at": "2021-04-13T13:06:16.567Z", + # "updated_at": "2021-04-13T13:06:16.567Z" + # } + # + # market + # { + # "id": "685877626834", + # "client_order_id": "Yshl7G-EjaREyXQYaGbsmdtVbW-nzQwu", + # "symbol": "BTCUSDT", + # "side": "buy", + # "status": "filled", + # "type": "market", + # "time_in_force": "GTC", + # "quantity": "0.00010", + # "quantity_cumulative": "0.00010", + # "post_only": False, + # "created_at": "2021-10-26T08:55:55.1Z", + # "updated_at": "2021-10-26T08:55:55.1Z", + # "trades": [ + # { + # "id": "1437229630", + # "position_id": "0", + # "quantity": "0.00010", + # "price": "62884.78", + # "fee": "0.005659630200", + # "timestamp": "2021-10-26T08:55:55.1Z", + # "taker": True + # } + # ] + # } + # + # swap and margin + # + # { + # "id": 58418961892, + # "client_order_id": "r42gdPjNMZN-H_xs8RKl2wljg_dfgdg4", + # "symbol": "BTCUSDT_PERP", + # "side": "buy", + # "status": "new", + # "type": "limit", + # "time_in_force": "GTC", + # "quantity": "0.0005", + # "quantity_cumulative": "0", + # "price": "30000.00", + # "post_only": False, + # "reduce_only": False, + # "created_at": "2022-03-16T08:16:53.039Z", + # "updated_at": "2022-03-16T08:16:53.039Z" + # } + # + id = self.safe_string(order, 'client_order_id') + # we use clientOrderId order id with self exchange intentionally + # because most of their endpoints will require clientOrderId + # explained here: https://github.com/ccxt/ccxt/issues/5674 + side = self.safe_string(order, 'side') + type = self.safe_string(order, 'type') + amount = self.safe_string(order, 'quantity') + price = self.safe_string(order, 'price') + average = self.safe_string(order, 'price_average') + created = self.safe_string(order, 'created_at') + timestamp = self.parse8601(created) + updated = self.safe_string(order, 'updated_at') + lastTradeTimestamp = None + if updated != created: + lastTradeTimestamp = self.parse8601(updated) + filled = self.safe_string(order, 'quantity_cumulative') + status = self.parse_order_status(self.safe_string(order, 'status')) + marketId = self.safe_string(order, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + postOnly = self.safe_value(order, 'post_only') + timeInForce = self.safe_string(order, 'time_in_force') + rawTrades = self.safe_value(order, 'trades') + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': id, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': lastTradeTimestamp, + 'lastUpdateTimestamp': lastTradeTimestamp, + 'symbol': symbol, + 'price': price, + 'amount': amount, + 'type': type, + 'side': side, + 'timeInForce': timeInForce, + 'postOnly': postOnly, + 'reduceOnly': self.safe_value(order, 'reduce_only'), + 'filled': filled, + 'remaining': None, + 'cost': None, + 'status': status, + 'average': average, + 'trades': rawTrades, + 'fee': None, + 'triggerPrice': self.safe_string(order, 'stop_price'), + 'takeProfitPrice': None, + 'stopLossPrice': None, + }, market) + + def fetch_margin_modes(self, symbols: List[Str] = None, params={}) -> MarginModes: + """ + fetches margin mode of the user + + https://api.hitbtc.com/#get-margin-position-parameters + https://api.hitbtc.com/#get-futures-position-parameters + + :param str[] symbols: unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `margin mode structures ` + """ + self.load_markets() + market = None + if symbols is not None: + symbols = self.market_symbols(symbols) + market = self.market(symbols[0]) + marketType = None + marketType, params = self.handle_market_type_and_params('fetchMarginMode', market, params) + response = None + if marketType == 'margin': + response = self.privateGetMarginConfig(params) + # + # { + # "config": [{ + # "symbol": "BTCUSD", + # "margin_call_leverage_mul": "1.50", + # "liquidation_leverage_mul": "2.00", + # "max_initial_leverage": "10.00", + # "margin_mode": "Isolated", + # "force_close_fee": "0.05", + # "enabled": True, + # "active": True, + # "limit_base": "50000.00", + # "limit_power": "2.2", + # "unlimited_threshold": "10.0" + # }] + # } + # + elif marketType == 'swap': + response = self.privateGetFuturesConfig(params) + # + # { + # "config": [{ + # "symbol": "BTCUSD_PERP", + # "margin_call_leverage_mul": "1.20", + # "liquidation_leverage_mul": "2.00", + # "max_initial_leverage": "100.00", + # "margin_mode": "Isolated", + # "force_close_fee": "0.001", + # "enabled": True, + # "active": False, + # "limit_base": "5000000.000000000000", + # "limit_power": "1.25", + # "unlimited_threshold": "2.00" + # }] + # } + # + else: + raise BadSymbol(self.id + ' fetchMarginModes() supports swap contracts and margin only') + config = self.safe_list(response, 'config', []) + return self.parse_margin_modes(config, symbols, 'symbol') + + def parse_margin_mode(self, marginMode: dict, market=None) -> MarginMode: + marketId = self.safe_string(marginMode, 'symbol') + return { + 'info': marginMode, + 'symbol': self.safe_symbol(marketId, market), + 'marginMode': self.safe_string_lower(marginMode, 'margin_mode'), + } + + def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://api.hitbtc.com/#transfer-between-wallet-and-exchange + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account to transfer from + :param str toAccount: account to transfer to + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transfer structure ` + """ + # account can be "spot", "wallet", or "derivatives" + self.load_markets() + currency = self.currency(code) + requestAmount = self.currency_to_precision(code, amount) + accountsByType = self.safe_value(self.options, 'accountsByType', {}) + fromAccount = fromAccount.lower() + toAccount = toAccount.lower() + fromId = self.safe_string(accountsByType, fromAccount, fromAccount) + toId = self.safe_string(accountsByType, toAccount, toAccount) + if fromId == toId: + raise BadRequest(self.id + ' transfer() fromAccount and toAccount arguments cannot be the same account') + request: dict = { + 'currency': currency['id'], + 'amount': requestAmount, + 'source': fromId, + 'destination': toId, + } + response = self.privatePostWalletTransfer(self.extend(request, params)) + # + # [ + # "2db6ebab-fb26-4537-9ef8-1a689472d236" + # ] + # + return self.parse_transfer(response, currency) + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + # + # transfer + # + # [ + # "2db6ebab-fb26-4537-9ef8-1a689472d236" + # ] + # + return { + 'id': self.safe_string(transfer, 0), + 'timestamp': None, + 'datetime': None, + 'currency': self.safe_currency_code(None, currency), + 'amount': None, + 'fromAccount': None, + 'toAccount': None, + 'status': None, + 'info': transfer, + } + + def convert_currency_network(self, code: str, amount, fromNetwork, toNetwork, params): + self.load_markets() + if code != 'USDT': + raise ExchangeError(self.id + ' convertCurrencyNetwork() only supports USDT currently') + networks = self.safe_value(self.options, 'networks', {}) + fromNetwork = fromNetwork.upper() + toNetwork = toNetwork.upper() + fromNetwork = self.safe_string(networks, fromNetwork) # handle ETH>ERC20 alias + toNetwork = self.safe_string(networks, toNetwork) # handle ETH>ERC20 alias + if fromNetwork == toNetwork: + raise BadRequest(self.id + ' convertCurrencyNetwork() fromNetwork cannot be the same') + if (fromNetwork is None) or (toNetwork is None): + keys = list(networks.keys()) + raise ArgumentsRequired(self.id + ' convertCurrencyNetwork() requires a fromNetwork parameter and a toNetwork parameter, supported networks are ' + ', '.join(keys)) + request: dict = { + 'from_currency': fromNetwork, + 'to_currency': toNetwork, + 'amount': self.currency_to_precision(code, amount), + } + response = self.privatePostWalletConvert(self.extend(request, params)) + # {"result":["587a1868-e62d-4d8e-b27c-dbdb2ee96149","e168df74-c041-41f2-b76c-e43e4fed5bc7"]} + return { + 'info': response, + } + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://api.hitbtc.com/#withdraw-crypto + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.load_markets() + self.check_address(address) + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + 'amount': amount, + 'address': address, + } + if tag is not None: + request['payment_id'] = tag + networks = self.safe_value(self.options, 'networks', {}) + network = self.safe_string_upper(params, 'network') + if (network is not None) and (code == 'USDT'): + parsedNetwork = self.safe_string(networks, network) + if parsedNetwork is not None: + request['network_code'] = parsedNetwork + params = self.omit(params, 'network') + withdrawOptions = self.safe_value(self.options, 'withdraw', {}) + includeFee = self.safe_bool(withdrawOptions, 'includeFee', False) + if includeFee: + request['include_fee'] = True + response = self.privatePostWalletCryptoWithdraw(self.extend(request, params)) + # + # { + # "id":"084cfcd5-06b9-4826-882e-fdb75ec3625d" + # } + # + return self.parse_transaction(response, currency) + + def fetch_funding_rates(self, symbols: Strings = None, params={}) -> FundingRates: + """ + fetches funding rates for multiple markets + + https://api.hitbtc.com/#futures-info + + :param str[] symbols: unified symbols of the markets to fetch the funding rates for, all market funding rates are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `funding rate structures ` + """ + self.load_markets() + market = None + request: dict = {} + if symbols is not None: + symbols = self.market_symbols(symbols) + market = self.market(symbols[0]) + queryMarketIds = self.market_ids(symbols) + request['symbols'] = ','.join(queryMarketIds) + type = None + type, params = self.handle_market_type_and_params('fetchFundingRates', market, params) + if type != 'swap': + raise NotSupported(self.id + ' fetchFundingRates() does not support ' + type + ' markets') + response = self.publicGetPublicFuturesInfo(self.extend(request, params)) + # + # { + # "BTCUSDT_PERP": { + # "contract_type": "perpetual", + # "mark_price": "30897.68", + # "index_price": "30895.29", + # "funding_rate": "0.0001", + # "open_interest": "93.7128", + # "next_funding_time": "2021-07-21T16:00:00.000Z", + # "indicative_funding_rate": "0.0001", + # "premium_index": "0.000047541807127312", + # "avg_premium_index": "0.000087063368020112", + # "interest_rate": "0.0001", + # "timestamp": "2021-07-21T09:48:37.235Z" + # } + # } + # + marketIds = list(response.keys()) + fundingRates: dict = {} + for i in range(0, len(marketIds)): + marketId = self.safe_string(marketIds, i) + rawFundingRate = self.safe_value(response, marketId) + marketInner = self.market(marketId) + symbol = marketInner['symbol'] + fundingRate = self.parse_funding_rate(rawFundingRate, marketInner) + fundingRates[symbol] = fundingRate + return self.filter_by_array(fundingRates, 'symbol', symbols) + + def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + + https://api.hitbtc.com/#funding-history + + fetches historical funding rate prices + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: timestamp in ms of the earliest funding rate to fetch + :param int [limit]: the maximum amount of `funding rate structures ` to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest funding rate + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `funding rate structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchFundingRateHistory', 'paginate') + if paginate: + return self.fetch_paginated_call_deterministic('fetchFundingRateHistory', symbol, since, limit, '8h', params, 1000) + market = None + request: dict = { + # all arguments are optional + # 'symbols': Comma separated list of symbol codes, + # 'sort': 'DESC' or 'ASC' + # 'from': 'Datetime or Number', + # 'until': 'Datetime or Number', + # 'limit': 100, + # 'offset': 0, + } + request, params = self.handle_until_option('until', request, params) + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + request['symbols'] = market['id'] + if since is not None: + request['from'] = since + if limit is not None: + request['limit'] = limit + response = self.publicGetPublicFuturesHistoryFunding(self.extend(request, params)) + # + # { + # "BTCUSDT_PERP": [ + # { + # "timestamp": "2021-07-29T16:00:00.271Z", + # "funding_rate": "0.0001", + # "avg_premium_index": "0.000061858585213222", + # "next_funding_time": "2021-07-30T00:00:00.000Z", + # "interest_rate": "0.0001" + # }, + # ... + # ], + # ... + # } + # + contracts = list(response.keys()) + rates = [] + for i in range(0, len(contracts)): + marketId = contracts[i] + marketInner = self.safe_market(marketId) + fundingRateData = response[marketId] + for j in range(0, len(fundingRateData)): + entry = fundingRateData[j] + symbolInner = self.safe_symbol(marketInner['symbol']) + fundingRate = self.safe_number(entry, 'funding_rate') + datetime = self.safe_string(entry, 'timestamp') + rates.append({ + 'info': entry, + 'symbol': symbolInner, + 'fundingRate': fundingRate, + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + }) + sorted = self.sort_by(rates, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, symbol, since, limit) + + def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://api.hitbtc.com/#get-futures-margin-accounts + https://api.hitbtc.com/#get-all-margin-accounts + + :param str[]|None symbols: not used by hitbtc fetchPositions() + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' only 'isolated' is supported, defaults to spot-margin endpoint if self is set + :param bool [params.margin]: True for fetching spot-margin positions + :returns dict[]: a list of `position structure ` + """ + self.load_markets() + request: dict = {} + marketType = None + marginMode = None + marketType, params = self.handle_market_type_and_params('fetchPositions', None, params) + if marketType == 'spot': + marketType = 'swap' + marginMode, params = self.handle_margin_mode_and_params('fetchPositions', params) + params = self.omit(params, ['marginMode', 'margin']) + response = None + if marginMode is not None: + response = self.privateGetMarginAccount(self.extend(request, params)) + else: + if marketType == 'swap': + response = self.privateGetFuturesAccount(self.extend(request, params)) + elif marketType == 'margin': + response = self.privateGetMarginAccount(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchPositions() not support self market type') + # + # [ + # { + # "symbol": "ETHUSDT_PERP", + # "type": "isolated", + # "leverage": "10.00", + # "created_at": "2022-03-19T07:54:35.24Z", + # "updated_at": "2022-03-19T07:54:58.922Z", + # currencies": [ + # { + # "code": "USDT", + # "margin_balance": "7.478100643043", + # "reserved_orders": "0", + # "reserved_positions": "0.303530761300" + # } + # ], + # "positions": [ + # { + # "id": 2470568, + # "symbol": "ETHUSDT_PERP", + # "quantity": "0.001", + # "price_entry": "2927.509", + # "price_margin_call": "0", + # "price_liquidation": "0", + # "pnl": "0", + # "created_at": "2022-03-19T07:54:35.24Z", + # "updated_at": "2022-03-19T07:54:58.922Z" + # } + # ] + # }, + # ] + # + result = [] + for i in range(0, len(response)): + result.append(self.parse_position(response[i])) + return result + + def fetch_position(self, symbol: str, params={}): + """ + fetch data on a single open contract trade position + + https://api.hitbtc.com/#get-futures-margin-account + https://api.hitbtc.com/#get-isolated-margin-account + + :param str symbol: unified market symbol of the market the position is held in, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' only 'isolated' is supported, defaults to spot-margin endpoint if self is set + :param bool [params.margin]: True for fetching a spot-margin position + :returns dict: a `position structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + marketType = None + marginMode = None + marketType, params = self.handle_market_type_and_params('fetchPosition', None, params) + marginMode, params = self.handle_margin_mode_and_params('fetchPosition', params) + params = self.omit(params, ['marginMode', 'margin']) + response = None + if marginMode is not None: + response = self.privateGetMarginAccountIsolatedSymbol(self.extend(request, params)) + else: + if marketType == 'swap': + response = self.privateGetFuturesAccountIsolatedSymbol(self.extend(request, params)) + elif marketType == 'margin': + response = self.privateGetMarginAccountIsolatedSymbol(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchPosition() not support self market type') + # + # [ + # { + # "symbol": "ETHUSDT_PERP", + # "type": "isolated", + # "leverage": "10.00", + # "created_at": "2022-03-19T07:54:35.24Z", + # "updated_at": "2022-03-19T07:54:58.922Z", + # currencies": [ + # { + # "code": "USDT", + # "margin_balance": "7.478100643043", + # "reserved_orders": "0", + # "reserved_positions": "0.303530761300" + # } + # ], + # "positions": [ + # { + # "id": 2470568, + # "symbol": "ETHUSDT_PERP", + # "quantity": "0.001", + # "price_entry": "2927.509", + # "price_margin_call": "0", + # "price_liquidation": "0", + # "pnl": "0", + # "created_at": "2022-03-19T07:54:35.24Z", + # "updated_at": "2022-03-19T07:54:58.922Z" + # } + # ] + # }, + # ] + # + return self.parse_position(response, market) + + def parse_position(self, position: dict, market: Market = None): + # + # [ + # { + # "symbol": "ETHUSDT_PERP", + # "type": "isolated", + # "leverage": "10.00", + # "created_at": "2022-03-19T07:54:35.24Z", + # "updated_at": "2022-03-19T07:54:58.922Z", + # currencies": [ + # { + # "code": "USDT", + # "margin_balance": "7.478100643043", + # "reserved_orders": "0", + # "reserved_positions": "0.303530761300" + # } + # ], + # "positions": [ + # { + # "id": 2470568, + # "symbol": "ETHUSDT_PERP", + # "quantity": "0.001", + # "price_entry": "2927.509", + # "price_margin_call": "0", + # "price_liquidation": "0", + # "pnl": "0", + # "created_at": "2022-03-19T07:54:35.24Z", + # "updated_at": "2022-03-19T07:54:58.922Z" + # } + # ] + # }, + # ] + # + marginMode = self.safe_string(position, 'type') + leverage = self.safe_number(position, 'leverage') + datetime = self.safe_string(position, 'updated_at') + positions = self.safe_value(position, 'positions', []) + liquidationPrice = None + entryPrice = None + contracts = None + for i in range(0, len(positions)): + entry = positions[i] + liquidationPrice = self.safe_number(entry, 'price_liquidation') + entryPrice = self.safe_number(entry, 'price_entry') + contracts = self.safe_number(entry, 'quantity') + currencies = self.safe_value(position, 'currencies', []) + collateral = None + for i in range(0, len(currencies)): + entry = currencies[i] + collateral = self.safe_number(entry, 'margin_balance') + marketId = self.safe_string(position, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + return self.safe_position({ + 'info': position, + 'id': None, + 'symbol': symbol, + 'notional': None, + 'marginMode': marginMode, + 'marginType': marginMode, + 'liquidationPrice': liquidationPrice, + 'entryPrice': entryPrice, + 'unrealizedPnl': None, + 'percentage': None, + 'contracts': contracts, + 'contractSize': None, + 'markPrice': None, + 'lastPrice': None, + 'side': None, + 'hedged': None, + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + 'lastUpdateTimestamp': None, + 'maintenanceMargin': None, + 'maintenanceMarginPercentage': None, + 'collateral': collateral, + 'initialMargin': None, + 'initialMarginPercentage': None, + 'leverage': leverage, + 'marginRatio': None, + 'stopLossPrice': None, + 'takeProfitPrice': None, + }) + + def parse_open_interest(self, interest, market: Market = None): + # + # { + # "contract_type": "perpetual", + # "mark_price": "42307.43", + # "index_price": "42303.27", + # "funding_rate": "0.0001", + # "open_interest": "30.9826", + # "next_funding_time": "2022-03-22T16:00:00.000Z", + # "indicative_funding_rate": "0.0001", + # "premium_index": "0", + # "avg_premium_index": "0.000029587712038098", + # "interest_rate": "0.0001", + # "timestamp": "2022-03-22T08:08:26.687Z" + # } + # + datetime = self.safe_string(interest, 'timestamp') + value = self.safe_number(interest, 'open_interest') + return self.safe_open_interest({ + 'symbol': self.safe_symbol(None, market), + 'openInterestAmount': None, + 'openInterestValue': value, + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + 'info': interest, + }, market) + + def fetch_open_interests(self, symbols: Strings = None, params={}): + """ + Retrieves the open interest for a list of symbols + + https://api.hitbtc.com/#futures-info + + :param str[] [symbols]: a list of unified CCXT market symbols + :param dict [params]: exchange specific parameters + :returns dict[]: a list of `open interest structures ` + """ + self.load_markets() + request: dict = {} + symbols = self.market_symbols(symbols) + marketIds = None + if symbols is not None: + marketIds = self.market_ids(symbols) + request['symbols'] = ','.join(marketIds) + response = self.publicGetPublicFuturesInfo(self.extend(request, params)) + # + # { + # "BTCUSDT_PERP": { + # "contract_type": "perpetual", + # "mark_price": "97291.83", + # "index_price": "97298.61", + # "funding_rate": "-0.000183473092423284", + # "open_interest": "94.1503", + # "next_funding_time": "2024-12-20T08:00:00.000Z", + # "indicative_funding_rate": "-0.00027495203277752", + # "premium_index": "-0.000789474900583786", + # "avg_premium_index": "-0.000683473092423284", + # "interest_rate": "0.0001", + # "timestamp": "2024-12-20T04:57:33.693Z" + # } + # } + # + results = [] + markets = list(response.keys()) + for i in range(0, len(markets)): + marketId = markets[i] + marketInner = self.safe_market(marketId) + results.append(self.parse_open_interest(response[marketId], marketInner)) + return self.filter_by_array(results, 'symbol', symbols) + + def fetch_open_interest(self, symbol: str, params={}): + """ + Retrieves the open interest of a derivative trading pair + + https://api.hitbtc.com/#futures-info + + :param str symbol: Unified CCXT market symbol + :param dict [params]: exchange specific parameters + :returns dict} an open interest structure{@link https://docs.ccxt.com/#/?id=interest-history-structure: + """ + self.load_markets() + market = self.market(symbol) + if not market['swap']: + raise BadSymbol(self.id + ' fetchOpenInterest() supports swap contracts only') + request: dict = { + 'symbol': market['id'], + } + response = self.publicGetPublicFuturesInfoSymbol(self.extend(request, params)) + # + # { + # "contract_type": "perpetual", + # "mark_price": "42307.43", + # "index_price": "42303.27", + # "funding_rate": "0.0001", + # "open_interest": "30.9826", + # "next_funding_time": "2022-03-22T16:00:00.000Z", + # "indicative_funding_rate": "0.0001", + # "premium_index": "0", + # "avg_premium_index": "0.000029587712038098", + # "interest_rate": "0.0001", + # "timestamp": "2022-03-22T08:08:26.687Z" + # } + # + return self.parse_open_interest(response, market) + + def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate + + https://api.hitbtc.com/#futures-info + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + self.load_markets() + market = self.market(symbol) + if not market['swap']: + raise BadSymbol(self.id + ' fetchFundingRate() supports swap contracts only') + request: dict = { + 'symbol': market['id'], + } + response = self.publicGetPublicFuturesInfoSymbol(self.extend(request, params)) + # + # { + # "contract_type": "perpetual", + # "mark_price": "42307.43", + # "index_price": "42303.27", + # "funding_rate": "0.0001", + # "open_interest": "30.9826", + # "next_funding_time": "2022-03-22T16:00:00.000Z", + # "indicative_funding_rate": "0.0001", + # "premium_index": "0", + # "avg_premium_index": "0.000029587712038098", + # "interest_rate": "0.0001", + # "timestamp": "2022-03-22T08:08:26.687Z" + # } + # + return self.parse_funding_rate(response, market) + + def parse_funding_rate(self, contract, market: Market = None) -> FundingRate: + # + # { + # "contract_type": "perpetual", + # "mark_price": "42307.43", + # "index_price": "42303.27", + # "funding_rate": "0.0001", + # "open_interest": "30.9826", + # "next_funding_time": "2022-03-22T16:00:00.000Z", + # "indicative_funding_rate": "0.0001", + # "premium_index": "0", + # "avg_premium_index": "0.000029587712038098", + # "interest_rate": "0.0001", + # "timestamp": "2022-03-22T08:08:26.687Z" + # } + # + fundingDateTime = self.safe_string(contract, 'next_funding_time') + datetime = self.safe_string(contract, 'timestamp') + return { + 'info': contract, + 'symbol': self.safe_symbol(None, market), + 'markPrice': self.safe_number(contract, 'mark_price'), + 'indexPrice': self.safe_number(contract, 'index_price'), + 'interestRate': self.safe_number(contract, 'interest_rate'), + 'estimatedSettlePrice': None, + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + 'fundingRate': self.safe_number(contract, 'funding_rate'), + 'fundingTimestamp': self.parse8601(fundingDateTime), + 'fundingDatetime': fundingDateTime, + 'nextFundingRate': self.safe_number(contract, 'indicative_funding_rate'), + 'nextFundingTimestamp': None, + 'nextFundingDatetime': None, + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': None, + } + + def modify_margin_helper(self, symbol: str, amount, type, params={}) -> MarginModification: + self.load_markets() + market = self.market(symbol) + leverage = self.safe_string(params, 'leverage') + if market['swap']: + if leverage is None: + raise ArgumentsRequired(self.id + ' modifyMarginHelper() requires a leverage parameter for swap markets') + stringAmount = self.number_to_string(amount) + if stringAmount != '0': + amount = self.amount_to_precision(symbol, stringAmount) + else: + amount = '0' + request: dict = { + 'symbol': market['id'], # swap and margin + 'margin_balance': amount, # swap and margin + # "leverage": "10", # swap only required + # "strict_validate": False, # swap and margin + } + if leverage is not None: + request['leverage'] = leverage + marketType = None + marginMode = None + marketType, params = self.handle_market_type_and_params('modifyMarginHelper', market, params) + marginMode, params = self.handle_margin_mode_and_params('modifyMarginHelper', params) + response = None + if marketType == 'swap': + response = self.privatePutFuturesAccountIsolatedSymbol(self.extend(request, params)) + elif (marketType == 'margin') or (marketType == 'spot') or (marginMode == 'isolated'): + response = self.privatePutMarginAccountIsolatedSymbol(self.extend(request, params)) + else: + raise NotSupported(self.id + ' modifyMarginHelper() not support self market type') + # + # { + # "symbol": "BTCUSDT_PERP", + # "type": "isolated", + # "leverage": "8.00", + # "created_at": "2022-03-30T23:34:27.161Z", + # "updated_at": "2022-03-30T23:34:27.161Z", + # "currencies": [ + # { + # "code": "USDT", + # "margin_balance": "7.000000000000", + # "reserved_orders": "0", + # "reserved_positions": "0" + # } + # ], + # "positions": null + # } + # + return self.extend(self.parse_margin_modification(response, market), { + 'amount': self.parse_number(amount), + 'type': type, + }) + + def parse_margin_modification(self, data: dict, market: Market = None) -> MarginModification: + # + # addMargin/reduceMargin + # + # { + # "symbol": "BTCUSDT_PERP", + # "type": "isolated", + # "leverage": "8.00", + # "created_at": "2022-03-30T23:34:27.161Z", + # "updated_at": "2022-03-30T23:34:27.161Z", + # "currencies": [ + # { + # "code": "USDT", + # "margin_balance": "7.000000000000", + # "reserved_orders": "0", + # "reserved_positions": "0" + # } + # ], + # "positions": null + # } + # + currencies = self.safe_value(data, 'currencies', []) + currencyInfo = self.safe_value(currencies, 0) + datetime = self.safe_string(data, 'updated_at') + return { + 'info': data, + 'symbol': market['symbol'], + 'type': None, + 'marginMode': 'isolated', + 'amount': None, + 'total': None, + 'code': self.safe_string(currencyInfo, 'code'), + 'status': None, + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + } + + def reduce_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + remove margin from a position + + https://api.hitbtc.com/#create-update-margin-account-2 + https://api.hitbtc.com/#create-update-margin-account + + :param str symbol: unified market symbol + :param float amount: the amount of margin to remove + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' only 'isolated' is supported, defaults to the spot-margin endpoint if self is set + :param bool [params.margin]: True for reducing spot-margin + :returns dict: a `margin structure ` + """ + if self.number_to_string(amount) != '0': + raise BadRequest(self.id + ' reduceMargin() on hitbtc requires the amount to be 0 and that will remove the entire margin amount') + return self.modify_margin_helper(symbol, amount, 'reduce', params) + + def add_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + add margin + + https://api.hitbtc.com/#create-update-margin-account-2 + https://api.hitbtc.com/#create-update-margin-account + + :param str symbol: unified market symbol + :param float amount: amount of margin to add + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' only 'isolated' is supported, defaults to the spot-margin endpoint if self is set + :param bool [params.margin]: True for adding spot-margin + :returns dict: a `margin structure ` + """ + return self.modify_margin_helper(symbol, amount, 'add', params) + + def fetch_leverage(self, symbol: str, params={}) -> Leverage: + """ + fetch the set leverage for a market + + https://api.hitbtc.com/#get-futures-margin-account + https://api.hitbtc.com/#get-isolated-margin-account + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' only 'isolated' is supported, defaults to the spot-margin endpoint if self is set + :param bool [params.margin]: True for fetching spot-margin leverage + :returns dict: a `leverage structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchLeverage', params) + params = self.omit(params, ['marginMode', 'margin']) + response = None + if marginMode is not None: + response = self.privateGetMarginAccountIsolatedSymbol(self.extend(request, params)) + else: + if market['type'] == 'spot': + response = self.privateGetMarginAccountIsolatedSymbol(self.extend(request, params)) + elif market['type'] == 'swap': + response = self.privateGetFuturesAccountIsolatedSymbol(self.extend(request, params)) + elif market['type'] == 'margin': + response = self.privateGetMarginAccountIsolatedSymbol(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchLeverage() not support self market type') + # + # { + # "symbol": "BTCUSDT", + # "type": "isolated", + # "leverage": "12.00", + # "created_at": "2022-03-29T22:31:29.067Z", + # "updated_at": "2022-03-30T00:00:00.125Z", + # "currencies": [ + # { + # "code": "USDT", + # "margin_balance": "20.824360374174", + # "reserved_orders": "0", + # "reserved_positions": "0.973330435000" + # } + # ], + # "positions": [ + # { + # "id": 631301, + # "symbol": "BTCUSDT", + # "quantity": "0.00022", + # "price_entry": "47425.57", + # "price_margin_call": "", + # "price_liquidation": "0", + # "pnl": "0", + # "created_at": "2022-03-29T22:31:29.067Z", + # "updated_at": "2022-03-30T00:00:00.125Z" + # } + # ] + # } + # + return self.parse_leverage(response, market) + + def parse_leverage(self, leverage: dict, market: Market = None) -> Leverage: + marketId = self.safe_string(leverage, 'symbol') + leverageValue = self.safe_integer(leverage, 'leverage') + return { + 'info': leverage, + 'symbol': self.safe_symbol(marketId, market), + 'marginMode': self.safe_string_lower(leverage, 'type'), + 'longLeverage': leverageValue, + 'shortLeverage': leverageValue, + } + + def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://api.hitbtc.com/#create-update-margin-account-2 + + :param float leverage: the rate of leverage + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setLeverage() requires a symbol argument') + self.load_markets() + if params['margin_balance'] is None: + raise ArgumentsRequired(self.id + ' setLeverage() requires a margin_balance parameter that will transfer margin to the specified trading pair') + market = self.market(symbol) + amount = self.safe_number(params, 'margin_balance') + maxLeverage = self.safe_integer(market['limits']['leverage'], 'max', 50) + if market['type'] != 'swap': + raise BadSymbol(self.id + ' setLeverage() supports swap contracts only') + if (leverage < 1) or (leverage > maxLeverage): + raise BadRequest(self.id + ' setLeverage() leverage should be between 1 and ' + str(maxLeverage) + ' for ' + symbol) + request: dict = { + 'symbol': market['id'], + 'leverage': str(leverage), + 'margin_balance': self.amount_to_precision(symbol, amount), + # 'strict_validate': False, + } + return self.privatePutFuturesAccountIsolatedSymbol(self.extend(request, params)) + + def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}): + """ + fetch deposit and withdraw fees + + https://api.hitbtc.com/#currencies + + :param str[]|None codes: list of unified currency codes + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `fees structures ` + """ + self.load_markets() + response = self.publicGetPublicCurrency(params) + # + # { + # "WEALTH": { + # "full_name": "ConnectWealth", + # "payin_enabled": False, + # "payout_enabled": False, + # "transfer_enabled": True, + # "precision_transfer": "0.001", + # "networks": [ + # { + # "network": "ETH", + # "protocol": "ERC20", + # "default": True, + # "payin_enabled": False, + # "payout_enabled": False, + # "precision_payout": "0.001", + # "payout_fee": "0.016800000000", + # "payout_is_payment_id": False, + # "payin_payment_id": False, + # "payin_confirmations": "2" + # } + # ] + # } + # } + # + return self.parse_deposit_withdraw_fees(response, codes) + + def parse_deposit_withdraw_fee(self, fee, currency: Currency = None): + # + # { + # "full_name": "ConnectWealth", + # "payin_enabled": False, + # "payout_enabled": False, + # "transfer_enabled": True, + # "precision_transfer": "0.001", + # "networks": [ + # { + # "network": "ETH", + # "protocol": "ERC20", + # "default": True, + # "payin_enabled": False, + # "payout_enabled": False, + # "precision_payout": "0.001", + # "payout_fee": "0.016800000000", + # "payout_is_payment_id": False, + # "payin_payment_id": False, + # "payin_confirmations": "2" + # } + # ] + # } + # + networks = self.safe_value(fee, 'networks', []) + result = self.deposit_withdraw_fee(fee) + for j in range(0, len(networks)): + networkEntry = networks[j] + networkId = self.safe_string(networkEntry, 'network') + networkCode = self.network_id_to_code(networkId) + networkCode = networkCode.upper() if (networkCode is not None) else None + withdrawFee = self.safe_number(networkEntry, 'payout_fee') + isDefault = self.safe_value(networkEntry, 'default') + withdrawResult: dict = { + 'fee': withdrawFee, + 'percentage': False if (withdrawFee is not None) else None, + } + if isDefault is True: + result['withdraw'] = withdrawResult + result['networks'][networkCode] = { + 'withdraw': withdrawResult, + 'deposit': { + 'fee': None, + 'percentage': None, + }, + } + return result + + def close_position(self, symbol: str, side: OrderSide = None, params={}) -> Order: + """ + closes open positions for a market + + https://api.hitbtc.com/#close-all-futures-margin-positions + + :param str symbol: unified ccxt market symbol + :param str side: 'buy' or 'sell' + :param dict [params]: extra parameters specific to the okx api endpoint + :param str [params.symbol]: *required* unified market symbol + :param str [params.marginMode]: 'cross' or 'isolated', default is 'cross' + :returns dict: An `order structure ` + """ + self.load_markets() + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('closePosition', params, 'cross') + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'margin_mode': marginMode, + } + response = self.privateDeleteFuturesPositionMarginModeSymbol(self.extend(request, params)) + # + # { + # "id":"202471640", + # "symbol":"TRXUSDT_PERP", + # "margin_mode":"Cross", + # "leverage":"1.00", + # "quantity":"0", + # "price_entry":"0", + # "price_margin_call":"0", + # "price_liquidation":"0", + # "pnl":"0.001234100000", + # "created_at":"2023-10-29T14:46:13.235Z", + # "updated_at":"2023-12-19T09:34:40.014Z" + # } + # + return self.parse_order(response, market) + + def handle_margin_mode_and_params(self, methodName, params={}, defaultValue=None): + """ + @ignore + marginMode specified by params["marginMode"], self.options["marginMode"], self.options["defaultMarginMode"], params["margin"] = True or self.options["defaultType"] = 'margin' + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Array: the marginMode in lowercase + """ + defaultType = self.safe_string(self.options, 'defaultType') + isMargin = self.safe_bool(params, 'margin', False) + marginMode = None + marginMode, params = super(hitbtc, self).handle_margin_mode_and_params(methodName, params, defaultValue) + if marginMode is None: + if (defaultType == 'margin') or (isMargin is True): + marginMode = 'isolated' + return [marginMode, params] + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + # + # { + # "error": { + # "code": 20001, + # "message": "Insufficient funds", + # "description": "Check that the funds are sufficient, given commissions" + # } + # } + # + # { + # "error": { + # "code": "600", + # "message": "Action not allowed" + # } + # } + # + error = self.safe_value(response, 'error') + errorCode = self.safe_string(error, 'code') + if errorCode is not None: + feedback = self.id + ' ' + body + message = self.safe_string_2(error, 'message', 'description') + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + raise ExchangeError(feedback) + return None + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + query = self.omit(params, self.extract_params(path)) + implodedPath = self.implode_params(path, params) + url = self.urls['api'][api] + '/' + implodedPath + getRequest = None + keys = list(query.keys()) + queryLength = len(keys) + headers = { + 'Content-Type': 'application/json', + } + if method == 'GET': + if queryLength: + getRequest = '?' + self.urlencode(query) + url = url + getRequest + else: + body = self.json(params) + if api == 'private': + self.check_required_credentials() + timestamp = str(self.nonce()) + payload = [method, '/api/3/' + implodedPath] + if method == 'GET': + if getRequest is not None: + payload.append(getRequest) + else: + payload.append(body) + payload.append(timestamp) + payloadString = ''.join(payload) + signature = self.hmac(self.encode(payloadString), self.encode(self.secret), hashlib.sha256, 'hex') + secondPayload = self.apiKey + ':' + signature + ':' + timestamp + encoded = self.string_to_base64(secondPayload) + headers['Authorization'] = 'HS256 ' + encoded + return {'url': url, 'method': method, 'body': body, 'headers': headers} diff --git a/ccxt/hollaex.py b/ccxt/hollaex.py new file mode 100644 index 0000000..973b24f --- /dev/null +++ b/ccxt/hollaex.py @@ -0,0 +1,2001 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.hollaex import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currencies, Currency, DepositAddress, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, OrderBooks, Trade, TradingFees, Transaction +from typing import List +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import OrderImmediatelyFillable +from ccxt.base.errors import NetworkError +from ccxt.base.errors import InvalidNonce +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class hollaex(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(hollaex, self).describe(), { + 'id': 'hollaex', + 'name': 'HollaEx', + 'countries': ['KR'], + # 4 requests per second => 1000ms / 4 = 250 ms between requests + 'rateLimit': 250, + 'version': 'v2', + 'pro': True, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': None, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'createLimitBuyOrder': True, + 'createLimitSellOrder': True, + 'createMarketBuyOrder': True, + 'createMarketSellOrder': True, + 'createOrder': True, + 'createPostOnlyOrder': True, + 'createReduceOnlyOrder': False, + 'createStopLimitOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'fetchBalance': True, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': 'emulated', + 'fetchDepositAddresses': True, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchFundingHistory': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchLeverage': False, + 'fetchMarginMode': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterestHistory': False, + 'fetchOpenOrder': True, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrderBooks': True, + 'fetchOrders': True, + 'fetchPosition': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': True, + 'fetchTransactions': False, + 'fetchTransfer': False, + 'fetchTransfers': False, + 'fetchWithdrawal': True, + 'fetchWithdrawals': True, + 'reduceMargin': False, + 'sandbox': True, + 'setLeverage': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'transfer': False, + 'withdraw': True, + }, + 'timeframes': { + '1m': '1m', + '5m': '5m', + '15m': '15m', + '1h': '1h', + '4h': '4h', + '1d': '1d', + '1w': '1w', + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/75841031-ca375180-5ddd-11ea-8417-b975674c23cb.jpg', + 'test': { + 'rest': 'https://api.sandbox.hollaex.com', + }, + 'api': { + 'rest': 'https://api.hollaex.com', + }, + 'www': 'https://hollaex.com', + 'doc': 'https://apidocs.hollaex.com', + 'referral': 'https://pro.hollaex.com/signup?affiliation_code=QSWA6G', + }, + 'precisionMode': TICK_SIZE, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + }, + 'api': { + 'public': { + 'get': { + 'health': 1, + 'constants': 1, + 'kit': 1, + 'tiers': 1, + 'ticker': 1, + 'tickers': 1, + 'orderbook': 1, + 'orderbooks': 1, + 'trades': 1, + 'chart': 1, + 'charts': 1, + 'minicharts': 1, + 'oracle/prices': 1, + 'quick-trade': 1, + # TradingView + 'udf/config': 1, + 'udf/history': 1, + 'udf/symbols': 1, + }, + }, + 'private': { + 'get': { + 'user': 1, + 'user/balance': 1, + 'user/deposits': 1, + 'user/withdrawals': 1, + 'user/withdrawal/fee': 1, + 'user/trades': 1, + 'orders': 1, + 'order': 1, + }, + 'post': { + 'user/withdrawal': 1, + 'order': 1, + }, + 'delete': { + 'order/all': 1, + 'order': 1, + }, + }, + }, + 'features': { + 'spot': { + 'sandbox': True, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': False, + 'FOK': False, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'selfTradePrevention': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': False, + 'marketBuyRequiresPrice': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 100000, + 'untilDays': 100000, # todo implement + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 100, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 100000, # todo + 'untilDays': 100000, # todo + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 100000, # todo + 'daysBackCanceled': 1, # todo + 'untilDays': 100000, # todo + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 1000, # todo: no limit in request + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'fees': { + 'trading': { + 'tierBased': True, + 'percentage': True, + 'taker': 0.001, + 'maker': 0.001, + }, + }, + 'exceptions': { + 'broad': { + 'API request is expired': InvalidNonce, + 'Invalid token': AuthenticationError, + 'Order not found': OrderNotFound, + 'Insufficient balance': InsufficientFunds, + 'Error 1001 - Order rejected. Order could not be submitted order was set to a post only order.': OrderImmediatelyFillable, + }, + 'exact': { + '400': BadRequest, + '403': AuthenticationError, + '404': BadRequest, + '405': BadRequest, + '410': BadRequest, + '429': BadRequest, + '500': NetworkError, + '503': NetworkError, + }, + }, + 'options': { + # how many seconds before the authenticated request expires + 'api-expires': self.parse_to_int(self.timeout / 1000), + 'networks': { + 'BTC': 'btc', + 'ETH': 'eth', + 'ERC20': 'eth', + 'TRX': 'trx', + 'TRC20': 'trx', + 'XRP': 'xrp', + 'XLM': 'xlm', + 'BNB': 'bnb', + 'MATIC': 'matic', + }, + 'networksById': { + 'eth': 'ERC20', + 'ETH': 'ERC20', + 'ERC20': 'ERC20', + 'trx': 'TRC20', + 'TRX': 'TRC20', + 'TRC20': 'TRC20', + }, + }, + }) + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for hollaex + + https://apidocs.hollaex.com/#constants + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = self.publicGetConstants(params) + # + # { + # "coins": { + # "xmr": { + # "id": 7, + # "fullname": "Monero", + # "symbol": "xmr", + # "active": True, + # "allow_deposit": True, + # "allow_withdrawal": True, + # "withdrawal_fee": 0.02, + # "min": 0.001, + # "max": 100000, + # "increment_unit": 0.001, + # "deposit_limits": {'1': 0, '2': 0, '3': 0, '4': 0, "5": 0, "6": 0}, + # "withdrawal_limits": {'1': 10, '2': 15, '3': 100, '4': 100, '5': 200, '6': 300, '7': 350, '8': 400, "9": 500, "10": -1}, + # "created_at": "2019-12-09T07:14:02.720Z", + # "updated_at": "2020-01-16T12:12:53.162Z" + # }, + # # ... + # }, + # "pairs": { + # "btc-usdt": { + # "id": 2, + # "name": "btc-usdt", + # "pair_base": "btc", + # "pair_2": "usdt", + # "taker_fees": {'1': 0.3, '2': 0.25, '3': 0.2, '4': 0.18, '5': 0.1, '6': 0.09, '7': 0.08, '8': 0.06, "9": 0.04, "10": 0}, + # "maker_fees": {'1': 0.1, '2': 0.08, '3': 0.05, '4': 0.03, '5': 0, '6': 0, '7': 0, '8': 0, "9": 0, "10": 0}, + # "min_size": 0.0001, + # "max_size": 1000, + # "min_price": 100, + # "max_price": 100000, + # "increment_size": 0.0001, + # "increment_price": 0.05, + # "active": True, + # "created_at": "2019-12-09T07:15:54.537Z", + # "updated_at": "2019-12-09T07:15:54.537Z" + # }, + # }, + # "config": {tiers: 10}, + # "status": True + # } + # + pairs = self.safe_value(response, 'pairs', {}) + keys = list(pairs.keys()) + result = [] + for i in range(0, len(keys)): + key = keys[i] + market = pairs[key] + baseId = self.safe_string(market, 'pair_base') + quoteId = self.safe_string(market, 'pair_2') + base = self.common_currency_code(baseId.upper()) + quote = self.common_currency_code(quoteId.upper()) + result.append({ + 'id': self.safe_string(market, 'name'), + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': self.safe_value(market, 'active'), + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.safe_number(market, 'increment_size'), + 'price': self.safe_number(market, 'increment_price'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(market, 'min_size'), + 'max': self.safe_number(market, 'max_size'), + }, + 'price': { + 'min': self.safe_number(market, 'min_price'), + 'max': self.safe_number(market, 'max_price'), + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': self.parse8601(self.safe_string(market, 'created_at')), + 'info': market, + }) + return result + + def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://apidocs.hollaex.com/#constants + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = self.publicGetConstants(params) + # + # { + # "coins": { + # "usdt": { + # "id": "6", + # "fullname": "USD Tether", + # "symbol": "usdt", + # "active": True, + # "verified": True, + # "allow_deposit": True, + # "allow_withdrawal": True, + # "withdrawal_fee": "20", + # "min": "1", + # "max": "10000000", + # "increment_unit": "0.0001", + # "logo": "https://hollaex-resources.s3.ap-southeast-1.amazonaws.com/icons/usdt.svg", + # "code": "usdt", + # "is_public": True, + # "meta": { + # "color": "#27a17a", + # "website": "https://tether.to", + # "explorer": "https://blockchair.com/tether", + # "decimal_points": "6" + # }, + # "estimated_price": "1", + # "description": "

Tether(USDT) is a stablecoin pegged 1:1 to the US dollar. It is a digital currency that aims to maintain its value while allowing for fast and secure transfer of funds. It was the first stablecoin, and is the most widely used due stablecoin due to its stability and low volatility compared to other cryptocurrencies. It was launched in 2014 by Tether Limited.

", + # "type": "blockchain", + # "network": "eth,trx,bnb,matic", + # "standard": "", + # "issuer": "HollaEx", + # "withdrawal_fees": { + # "bnb": { + # "value": "0.8", + # "active": True, + # "symbol": "usdt" + # }, + # "eth": { + # "value": "1.5", + # "active": True, + # "symbol": "usdt" + # }, + # "trx": { + # "value": "4", + # "active": True, + # "symbol": "usdt" + # }, + # "matic": { + # "value": "0.3", + # "active": True, + # "symbol": "usdt" + # } + # }, + # "display_name": null, + # "deposit_fees": null, + # "is_risky": False, + # "market_cap": "144568098696.29", + # "category": "stable", + # "created_at": "2019-08-09T10:45:43.367Z", + # "updated_at": "2025-03-25T17:12:37.970Z", + # "created_by": "168", + # "owner_id": "1" + # }, + # }, + # "network":"https://api.hollaex.network" + # } + # + coins = self.safe_dict(response, 'coins', {}) + keys = list(coins.keys()) + result: dict = {} + for i in range(0, len(keys)): + key = keys[i] + currency = coins[key] + id = self.safe_string(currency, 'symbol') + code = self.safe_currency_code(id) + withdrawalLimits = self.safe_list(currency, 'withdrawal_limits', []) + rawType = self.safe_string(currency, 'type') + type = 'crypto' if (rawType == 'blockchain') else 'other' + rawNetworks = self.safe_dict(currency, 'withdrawal_fees', {}) + networks = {} + networkIds = list(rawNetworks.keys()) + for j in range(0, len(networkIds)): + networkId = networkIds[j] + networkEntry = self.safe_dict(rawNetworks, networkId) + networkCode = self.network_id_to_code(networkId) + networks[networkCode] = { + 'id': networkId, + 'network': networkCode, + 'active': self.safe_bool(networkEntry, 'active'), + 'deposit': None, + 'withdraw': None, + 'fee': self.safe_number(networkEntry, 'value'), + 'precision': None, + 'limits': { + 'withdraw': { + 'min': None, + 'max': None, + }, + }, + 'info': networkEntry, + } + result[code] = self.safe_currency_structure({ + 'id': id, + 'numericId': self.safe_integer(currency, 'id'), + 'code': code, + 'info': currency, + 'name': self.safe_string(currency, 'fullname'), + 'active': self.safe_bool(currency, 'active'), + 'deposit': self.safe_bool(currency, 'allow_deposit'), + 'withdraw': self.safe_bool(currency, 'allow_withdrawal'), + 'fee': self.safe_number(currency, 'withdrawal_fee'), + 'precision': self.safe_number(currency, 'increment_unit'), + 'limits': { + 'amount': { + 'min': self.safe_number(currency, 'min'), + 'max': self.safe_number(currency, 'max'), + }, + 'withdraw': { + 'min': None, + 'max': self.safe_value(withdrawalLimits, 0), + }, + }, + 'networks': networks, + 'type': type, + }) + return result + + def fetch_order_books(self, symbols: Strings = None, limit: Int = None, params={}) -> OrderBooks: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data for multiple markets + + https://apidocs.hollaex.com/#orderbooks + + :param str[]|None symbols: not used by hollaex fetchOrderBooks() + :param int [limit]: not used by hollaex fetchOrderBooks() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `order book structures ` indexed by market symbol + """ + self.load_markets() + response = self.publicGetOrderbooks(params) + result: dict = {} + marketIds = list(response.keys()) + for i in range(0, len(marketIds)): + marketId = marketIds[i] + orderbook = response[marketId] + symbol = self.safe_symbol(marketId, None, '-') + timestamp = self.parse8601(self.safe_string(orderbook, 'timestamp')) + result[symbol] = self.parse_order_book(response[marketId], symbol, timestamp) + return result + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://apidocs.hollaex.com/#orderbook + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = self.publicGetOrderbook(self.extend(request, params)) + # + # { + # "btc-usdt": { + # "bids": [ + # [8836.4, 1.022], + # [8800, 0.0668], + # [8797.75, 0.2398], + # ], + # "asks": [ + # [8839.35, 1.5334], + # [8852.6, 0.0579], + # [8860.45, 0.1815], + # ], + # "timestamp": "2020-03-03T02:27:25.147Z" + # }, + # "eth-usdt": {}, + # # ... + # } + # + orderbook = self.safe_value(response, market['id']) + timestamp = self.parse8601(self.safe_string(orderbook, 'timestamp')) + return self.parse_order_book(orderbook, market['symbol'], timestamp) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://apidocs.hollaex.com/#ticker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = self.publicGetTicker(self.extend(request, params)) + # + # { + # "open": 8615.55, + # "close": 8841.05, + # "high": 8921.1, + # "low": 8607, + # "last": 8841.05, + # "volume": 20.2802, + # "timestamp": "2020-03-03T03:11:18.964Z" + # } + # + return self.parse_ticker(response, market) + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://apidocs.hollaex.com/#tickers + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + symbols = self.market_symbols(symbols) + response = self.publicGetTickers(params) + # + # { + # "bch-usdt": { + # "time": "2020-03-02T04:29:45.011Z", + # "open": 341.65, + # "close":337.9, + # "high":341.65, + # "low":337.3, + # "last":337.9, + # "volume":0.054, + # "symbol":"bch-usdt" + # }, + # # ... + # } + # + return self.parse_tickers(response, symbols) + + def parse_tickers(self, tickers, symbols: Strings = None, params={}) -> Tickers: + result: dict = {} + keys = list(tickers.keys()) + for i in range(0, len(keys)): + key = keys[i] + ticker = tickers[key] + marketId = self.safe_string(ticker, 'symbol', key) + market = self.safe_market(marketId, None, '-') + symbol = market['symbol'] + result[symbol] = self.extend(self.parse_ticker(ticker, market), params) + return self.filter_by_array_tickers(result, 'symbol', symbols) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # fetchTicker + # + # { + # "open": 8615.55, + # "close": 8841.05, + # "high": 8921.1, + # "low": 8607, + # "last": 8841.05, + # "volume": 20.2802, + # "timestamp": "2020-03-03T03:11:18.964Z", + # } + # + # fetchTickers + # + # { + # "time": "2020-03-02T04:29:45.011Z", + # "open": 341.65, + # "close": 337.9, + # "high": 341.65, + # "low": 337.3, + # "last": 337.9, + # "volume": 0.054, + # "symbol": "bch-usdt" + # } + # + marketId = self.safe_string(ticker, 'symbol') + market = self.safe_market(marketId, market, '-') + symbol = market['symbol'] + timestamp = self.parse8601(self.safe_string_2(ticker, 'time', 'timestamp')) + close = self.safe_string(ticker, 'close') + return self.safe_ticker({ + 'symbol': symbol, + 'info': ticker, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': None, + 'bidVolume': None, + 'ask': None, + 'askVolume': None, + 'vwap': None, + 'open': self.safe_string(ticker, 'open'), + 'close': close, + 'last': self.safe_string(ticker, 'last', close), + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': self.safe_string(ticker, 'volume'), + 'quoteVolume': None, + }, market) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://apidocs.hollaex.com/#trades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = self.publicGetTrades(self.extend(request, params)) + # + # { + # "btc-usdt": [ + # { + # "size": 0.5, + # "price": 8830, + # "side": "buy", + # "timestamp": "2020-03-03T04:44:33.034Z" + # }, + # # ... + # ] + # } + # + trades = self.safe_list(response, market['id'], []) + return self.parse_trades(trades, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(public) + # + # { + # "size": 0.5, + # "price": 8830, + # "side": "buy", + # "timestamp": "2020-03-03T04:44:33.034Z" + # } + # + # fetchMyTrades(private) + # { + # "side":"sell", + # "symbol":"doge-usdt", + # "size":70, + # "price":0.147411, + # "timestamp":"2022-01-26T17:53:34.650Z", + # "order_id":"cba78ecb-4187-4da2-9d2f-c259aa693b5a", + # "fee":0.01031877, + # "fee_coin":"usdt" + # } + # + marketId = self.safe_string(trade, 'symbol') + market = self.safe_market(marketId, market, '-') + symbol = market['symbol'] + datetime = self.safe_string(trade, 'timestamp') + timestamp = self.parse8601(datetime) + side = self.safe_string(trade, 'side') + orderId = self.safe_string(trade, 'order_id') + priceString = self.safe_string(trade, 'price') + amountString = self.safe_string(trade, 'size') + feeCostString = self.safe_string(trade, 'fee') + feeCoin = self.safe_string(trade, 'fee_coin') + fee = None + if feeCostString is not None: + fee = { + 'cost': feeCostString, + 'currency': self.safe_currency_code(feeCoin), + } + return self.safe_trade({ + 'info': trade, + 'id': None, + 'timestamp': timestamp, + 'datetime': datetime, + 'symbol': symbol, + 'order': orderId, + 'type': None, + 'side': side, + 'takerOrMaker': None, + 'price': priceString, + 'amount': amountString, + 'cost': None, + 'fee': fee, + }, market) + + def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://apidocs.hollaex.com/#tiers + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + self.load_markets() + response = self.publicGetTiers(params) + # + # { + # "1": { + # "id": "1", + # "name": "Silver", + # "icon": '', + # "description": "Your crypto journey starts here! Make your first deposit to start trading, and verify your account to level up!", + # "deposit_limit": "0", + # "withdrawal_limit": "1000", + # "fees": { + # "maker": { + # 'eth-btc': "0.1", + # 'ada-usdt': "0.1", + # ... + # }, + # "taker": { + # 'eth-btc': "0.1", + # 'ada-usdt': "0.1", + # ... + # } + # }, + # "note": "
    \n
  • Login and verify email
  • \n
\n", + # "created_at": "2021-03-22T03:51:39.129Z", + # "updated_at": "2021-11-01T02:51:56.214Z" + # }, + # ... + # } + # + firstTier = self.safe_value(response, '1', {}) + fees = self.safe_value(firstTier, 'fees', {}) + makerFees = self.safe_value(fees, 'maker', {}) + takerFees = self.safe_value(fees, 'taker', {}) + result: dict = {} + for i in range(0, len(self.symbols)): + symbol = self.symbols[i] + market = self.market(symbol) + makerString = self.safe_string(makerFees, market['id']) + takerString = self.safe_string(takerFees, market['id']) + result[symbol] = { + 'info': fees, + 'symbol': symbol, + 'maker': self.parse_number(Precise.string_div(makerString, '100')), + 'taker': self.parse_number(Precise.string_div(takerString, '100')), + 'percentage': True, + 'tierBased': True, + } + return result + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + hollaex has large gaps between candles, so it's recommended to specify since + + https://apidocs.hollaex.com/#chart + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch(max 500) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest candle to fetch + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'resolution': self.safe_string(self.timeframes, timeframe, timeframe), + } + paginate = False + maxLimit = 500 + paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate', paginate) + if paginate: + return self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params, maxLimit) + until = self.safe_integer(params, 'until') + timeDelta = self.parse_timeframe(timeframe) * maxLimit * 1000 + start = since + now = self.milliseconds() + if until is None and start is None: + until = now + start = until - timeDelta + elif until is None: + until = now # the exchange has not a lot of trades, so if we count until by limit and limit is small, it may return empty result + elif start is None: + start = until - timeDelta + request['from'] = self.parse_to_int(start / 1000) # convert to seconds + request['to'] = self.parse_to_int(until / 1000) # convert to seconds + params = self.omit(params, 'until') + response = self.publicGetChart(self.extend(request, params)) + # + # [ + # { + # "time":"2020-03-02T20:00:00.000Z", + # "close":8872.1, + # "high":8872.1, + # "low":8858.6, + # "open":8858.6, + # "symbol":"btc-usdt", + # "volume":1.2922 + # }, + # ] + # + return self.parse_ohlcvs(response, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # { + # "time":"2020-03-02T20:00:00.000Z", + # "close":8872.1, + # "high":8872.1, + # "low":8858.6, + # "open":8858.6, + # "symbol":"btc-usdt", + # "volume":1.2922 + # } + # + return [ + self.parse8601(self.safe_string(ohlcv, 'time')), + self.safe_number(ohlcv, 'open'), + self.safe_number(ohlcv, 'high'), + self.safe_number(ohlcv, 'low'), + self.safe_number(ohlcv, 'close'), + self.safe_number(ohlcv, 'volume'), + ] + + def parse_balance(self, response) -> Balances: + timestamp = self.parse8601(self.safe_string(response, 'updated_at')) + result: dict = { + 'info': response, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + currencyIds = list(self.currencies_by_id.keys()) + for i in range(0, len(currencyIds)): + currencyId = currencyIds[i] + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(response, currencyId + '_available') + account['total'] = self.safe_string(response, currencyId + '_balance') + result[code] = account + return self.safe_balance(result) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://apidocs.hollaex.com/#get-balance + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + response = self.privateGetUserBalance(params) + # + # { + # "updated_at": "2020-03-02T22:27:38.428Z", + # "btc_balance": 0, + # "btc_pending": 0, + # "btc_available": 0, + # "eth_balance": 0, + # "eth_pending": 0, + # "eth_available": 0, + # # ... + # } + # + return self.parse_balance(response) + + def fetch_open_order(self, id: str, symbol: Str = None, params={}): + """ + fetch an open order by it's id + + https://apidocs.hollaex.com/#get-order + + :param str id: order id + :param str symbol: not used by hollaex fetchOpenOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + request: dict = { + 'order_id': id, + } + response = self.privateGetOrder(self.extend(request, params)) + # + # { + # "id": "string", + # "side": "sell", + # "symbol": "xht-usdt", + # "size": 0.1, + # "filled": 0, + # "stop": null, + # "fee": 0, + # "fee_coin": "usdt", + # "type": "limit", + # "price": 1.09, + # "status": "new", + # "created_by": 116, + # "created_at": "2021-02-17T02:32:38.910Z", + # "updated_at": "2021-02-17T02:32:38.910Z", + # "User": { + # "id": 116, + # "email": "fight@club.com", + # "username": "narrator", + # "exchange_id": 176 + # } + # } + # + return self.parse_order(response) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://apidocs.hollaex.com/#get-all-orders + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + request: dict = { + 'open': True, + } + return self.fetch_orders(symbol, since, limit, self.extend(request, params)) + + def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://apidocs.hollaex.com/#get-all-orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + request: dict = { + 'open': False, + } + return self.fetch_orders(symbol, since, limit, self.extend(request, params)) + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://apidocs.hollaex.com/#get-order + + :param str id: + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + request: dict = { + 'order_id': id, + } + response = self.privateGetOrder(self.extend(request, params)) + # { + # "id": "string", + # "side": "sell", + # "symbol": "xht-usdt", + # "size": 0.1, + # "filled": 0, + # "stop": null, + # "fee": 0, + # "fee_coin": "usdt", + # "type": "limit", + # "price": 1.09, + # "status": "new", + # "created_by": 116, + # "created_at": "2021-02-17T02:32:38.910Z", + # "updated_at": "2021-02-17T02:32:38.910Z", + # "User": { + # "id": 116, + # "email": "fight@club.com", + # "username": "narrator", + # "exchange_id": 176 + # } + # } + order = response + if order is None: + raise OrderNotFound(self.id + ' fetchOrder() could not find order id ' + id) + return self.parse_order(order) + + def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://apidocs.hollaex.com/#get-all-orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + market = None + request: dict = { + # 'symbol': market['id'], + # 'side': 'buy', # 'sell' + # 'status': 'new', # 'filled', 'pfilled', 'canceled' + # 'open': True, + # 'limit': limit, # default 50, max 100 + # 'page': 1, + # 'order_by': 'created_at', # id, ... + # 'order': 'asc', # 'desc' + # 'start_date': self.iso8601(since), + # 'end_date': self.iso8601(self.milliseconds()), + } + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if since is not None: + request['start_date'] = self.iso8601(since) + if limit is not None: + request['limit'] = limit # default 50, max 100 + response = self.privateGetOrders(self.extend(request, params)) + # + # { + # "count": 1, + # "data": [ + # { + # "id": "string", + # "side": "sell", + # "symbol": "xht-usdt", + # "size": 0.1, + # "filled": 0, + # "stop": null, + # "fee": 0, + # "fee_coin": "usdt", + # "type": "limit", + # "price": 1.09, + # "status": "new", + # "created_by": 116, + # "created_at": "2021-02-17T02:32:38.910Z", + # "updated_at": "2021-02-17T02:32:38.910Z", + # "User": { + # "id": 116, + # "email": "fight@club.com", + # "username": "narrator", + # "exchange_id": 176 + # } + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_orders(data, market, since, limit) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'new': 'open', + 'pfilled': 'open', + 'filled': 'closed', + 'canceled': 'canceled', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder, fetchOpenOrder, fetchOpenOrders + # + # { + # "id":"10644b7e-3c90-4ba9-bc3b-188f3a4e9cfd", + # "created_by":140093, + # "exchange_id":22, + # "side":"buy", + # "symbol":"doge-usdt", + # "type":"limit", + # "price":0.05, + # "size":10, + # "stop":null, + # "filled":0, + # "status":"canceled", + # "fee":0, + # "fee_coin":"doge", + # "meta": { # optional field only returned for postOnly orders + # "post_only":true + # }, + # "fee_structure": { + # "maker":0.1, + # "taker":0.1 + # }, + # "created_at":"2022-05-31T08:14:14.747Z", + # "updated_at":"2022-05-31T08:14:23.727Z" + # } + # + marketId = self.safe_string(order, 'symbol') + symbol = self.safe_symbol(marketId, market, '-') + id = self.safe_string(order, 'id') + timestamp = self.parse8601(self.safe_string(order, 'created_at')) + type = self.safe_string(order, 'type') + side = self.safe_string(order, 'side') + price = self.safe_string(order, 'price') + amount = self.safe_string(order, 'size') + filled = self.safe_string(order, 'filled') + status = self.parse_order_status(self.safe_string(order, 'status')) + meta = self.safe_value(order, 'meta', {}) + postOnly = self.safe_bool(meta, 'post_only', False) + return self.safe_order({ + 'id': id, + 'clientOrderId': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'status': status, + 'symbol': symbol, + 'type': type, + 'timeInForce': None, + 'postOnly': postOnly, + 'side': side, + 'price': price, + 'triggerPrice': self.safe_string(order, 'stop'), + 'amount': amount, + 'filled': filled, + 'remaining': None, + 'cost': None, + 'trades': None, + 'fee': None, + 'info': order, + 'average': None, + }, market) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://apidocs.hollaex.com/#create-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: the price at which a trigger order is triggered at + :param bool [params.postOnly]: if True, the order will only be posted to the order book and not executed immediately + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'side': side, + 'size': self.amount_to_precision(symbol, amount), + 'type': type, + # 'stop': float(self.price_to_precision(symbol, stopPrice)), + # 'meta': {}, # other options such + } + triggerPrice = self.safe_number_n(params, ['triggerPrice', 'stopPrice', 'stop']) + meta = self.safe_value(params, 'meta', {}) + exchangeSpecificParam = self.safe_bool(meta, 'post_only', False) + isMarketOrder = type == 'market' + postOnly = self.is_post_only(isMarketOrder, exchangeSpecificParam, params) + if not isMarketOrder: + request['price'] = self.price_to_precision(symbol, price) + if triggerPrice is not None: + request['stop'] = self.price_to_precision(symbol, triggerPrice) + if postOnly: + request['meta'] = {'post_only': True} + params = self.omit(params, ['postOnly', 'timeInForce', 'stopPrice', 'triggerPrice', 'stop']) + response = self.privatePostOrder(self.extend(request, params)) + # + # { + # "fee": 0, + # "meta": {}, + # "symbol": "xht-usdt", + # "side": "sell", + # "size": 0.1, + # "type": "limit", + # "price": 1, + # "fee_structure": { + # "maker": 0.2, + # "taker": 0.2 + # }, + # "fee_coin": "usdt", + # "id": "string", + # "created_by": 116, + # "filled": 0, + # "status": "new", + # "updated_at": "2021-02-17T03:03:19.231Z", + # "created_at": "2021-02-17T03:03:19.231Z", + # "stop": null + # } + # + return self.parse_order(response, market) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://apidocs.hollaex.com/#cancel-order + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + request: dict = { + 'order_id': id, + } + response = self.privateDeleteOrder(self.extend(request, params)) + # + # { + # "title": "string", + # "symbol": "xht-usdt", + # "side": "sell", + # "size": 1, + # "type": "limit", + # "price": 0.1, + # "id": "string", + # "created_by": 34, + # "filled": 0 + # } + # + return self.parse_order(response) + + def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders in a market + + https://apidocs.hollaex.com/#cancel-all-orders + + :param str symbol: unified market symbol of the market to cancel orders in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelAllOrders() requires a symbol argument') + self.load_markets() + request: dict = {} + market = None + market = self.market(symbol) + request['symbol'] = market['id'] + response = self.privateDeleteOrderAll(self.extend(request, params)) + # + # [ + # { + # "title": "string", + # "symbol": "xht-usdt", + # "side": "sell", + # "size": 1, + # "type": "limit", + # "price": 0.1, + # "id": "string", + # "created_by": 34, + # "filled": 0 + # } + # ] + # + return self.parse_orders(response, market) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://apidocs.hollaex.com/#get-trades + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + request: dict = { + # 'symbol': market['id'], + # 'limit': 50, # default 50, max 100 + # 'page': 1, # page of data to retrieve + # 'order_by': 'timestamp', # field to order data + # 'order': 'asc', # asc or desc + # 'start_date': 123, # starting date of queried data + # 'end_date': 321, # ending date of queried data + } + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if limit is not None: + request['limit'] = limit # default 50, max 100 + if since is not None: + request['start_date'] = self.iso8601(since) + response = self.privateGetUserTrades(self.extend(request, params)) + # + # { + # "count": 1, + # "data": [ + # { + # "side": "buy", + # "symbol": "eth-usdt", + # "size": 0.086, + # "price": 226.19, + # "timestamp": "2020-03-03T08:03:55.459Z", + # "fee": 0.1 + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_trades(data, market, since, limit) + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + # + # { + # "currency":"usdt", + # "address":"TECLD9XBH31XpyykdHU3uEAeUK7E6Lrmik", + # "network":"trx", + # "standard":null, + # "is_valid":true, + # "created_at":"2021-05-12T02:43:05.446Z" + # } + # + address = self.safe_string(depositAddress, 'address') + tag = None + if address is not None: + parts = address.split(':') + address = self.safe_string(parts, 0) + tag = self.safe_string(parts, 1) + self.check_address(address) + currencyId = self.safe_string(depositAddress, 'currency') + currency = self.safe_currency(currencyId, currency) + network = self.safe_string(depositAddress, 'network') + return { + 'info': depositAddress, + 'currency': currency['code'], + 'network': network, + 'address': address, + 'tag': tag, + } + + def fetch_deposit_addresses(self, codes: Strings = None, params={}) -> List[DepositAddress]: + """ + fetch deposit addresses for multiple currencies and chain types + + https://apidocs.hollaex.com/#get-user + + :param str[]|None codes: list of unified currency codes, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `address structures ` + """ + self.load_markets() + network = self.safe_string(params, 'network') + params = self.omit(params, 'network') + response = self.privateGetUser(params) + # + # { + # "id":620, + # "email":"igor.kroitor@gmail.com", + # "full_name":"", + # "gender":false, + # "nationality":"", + # "dob":null, + # "phone_number":"", + # "address":{"city":"","address":"","country":"","postal_code":""}, + # "id_data":{"note":"","type":"","number":"","status":0,"issued_date":"","expiration_date":""}, + # "bank_account":[], + # "crypto_wallet":{}, + # "verification_level":1, + # "email_verified":true, + # "otp_enabled":true, + # "activated":true, + # "username":"igor.kroitor", + # "affiliation_code":"QSWA6G", + # "settings":{ + # "chat":{"set_username":false}, + # "risk":{"popup_warning":false,"order_portfolio_percentage":20}, + # "audio":{"public_trade":false,"order_completed":true,"order_partially_completed":true}, + # "language":"en", + # "interface":{"theme":"white","order_book_levels":10}, + # "notification":{"popup_order_completed":true,"popup_order_confirmation":true,"popup_order_partially_filled":true} + # }, + # "affiliation_rate":0, + # "network_id":10620, + # "discount":0, + # "created_at":"2021-03-24T02:37:57.379Z", + # "updated_at":"2021-03-24T02:37:57.379Z", + # "balance":{ + # "btc_balance":0, + # "btc_available":0, + # "eth_balance":0.000914, + # "eth_available":0.000914, + # "updated_at":"2020-03-04T04:03:27.174Z + # "}, + # "wallet":[ + # {"currency":"usdt","address":"TECLD9XBH31XpyykdHU3uEAeUK7E6Lrmik","network":"trx","standard":null,"is_valid":true,"created_at":"2021-05-12T02:43:05.446Z"}, + # {"currency":"xrp","address":"rGcSzmuRx8qngPRnrvpCKkP9V4njeCPGCv:286741597","network":"xrp","standard":null,"is_valid":true,"created_at":"2021-05-12T02:49:01.273Z"} + # ] + # } + # + wallet = self.safe_value(response, 'wallet', []) + addresses = wallet if (network is None) else self.filter_by(wallet, 'network', network) + return self.parse_deposit_addresses(addresses, codes) + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://apidocs.hollaex.com/#get-deposits + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + request: dict = { + # 'currency': currency['id'], + # 'limit': 50, # default 50, max 100 + # 'page': 1, # page of data to retrieve + # 'order_by': 'timestamp', # field to order data + # 'order': 'asc', # asc or desc + # 'start_date': 123, # starting date of queried data + # 'end_date': 321, # ending date of queried data + } + currency = None + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + if limit is not None: + request['limit'] = limit # default 50, max 100 + if since is not None: + request['start_date'] = self.iso8601(since) + response = self.privateGetUserDeposits(self.extend(request, params)) + # + # { + # "count": 1, + # "data": [ + # { + # "id": 539, + # "amount": 20, + # "fee": 0, + # "address": "0x5c0cc98270d7089408fcbcc8e2131287f5be2306", + # "transaction_id": "0xd4006327a5ec2c41adbdcf566eaaba6597c3d45906abe78ea1a4a022647c2e28", + # "status": True, + # "dismissed": False, + # "rejected": False, + # "description": "", + # "type": "deposit", + # "currency": "usdt", + # "created_at": "2020-03-03T07:56:36.198Z", + # "updated_at": "2020-03-03T08:00:05.674Z", + # "user_id": 620 + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_transactions(data, currency, since, limit) + + def fetch_withdrawal(self, id: str, code: Str = None, params={}): + """ + fetch data on a currency withdrawal via the withdrawal id + + https://apidocs.hollaex.com/#get-withdrawals + + :param str id: withdrawal id + :param str code: unified currency code of the currency withdrawn, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + self.load_markets() + request: dict = { + 'transaction_id': id, + } + currency = None + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + response = self.privateGetUserWithdrawals(self.extend(request, params)) + # + # { + # "count": 1, + # "data": [ + # { + # "id": 539, + # "amount": 20, + # "fee": 0, + # "address": "0x5c0cc98270d7089408fcbcc8e2131287f5be2306", + # "transaction_id": "0xd4006327a5ec2c41adbdcf566eaaba6597c3d45906abe78ea1a4a022647c2e28", + # "status": True, + # "dismissed": False, + # "rejected": False, + # "description": "", + # "type": "withdrawal", + # "currency": "usdt", + # "created_at": "2020-03-03T07:56:36.198Z", + # "updated_at": "2020-03-03T08:00:05.674Z", + # "user_id": 620 + # } + # ] + # } + # + data = self.safe_value(response, 'data', []) + transaction = self.safe_dict(data, 0, {}) + return self.parse_transaction(transaction, currency) + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://apidocs.hollaex.com/#get-withdrawals + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + request: dict = { + # 'currency': currency['id'], + # 'limit': 50, # default 50, max 100 + # 'page': 1, # page of data to retrieve + # 'order_by': 'timestamp', # field to order data + # 'order': 'asc', # asc or desc + # 'start_date': 123, # starting date of queried data + # 'end_date': 321, # ending date of queried data + } + currency = None + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + if limit is not None: + request['limit'] = limit # default 50, max 100 + if since is not None: + request['start_date'] = self.iso8601(since) + response = self.privateGetUserWithdrawals(self.extend(request, params)) + # + # { + # "count": 1, + # "data": [ + # { + # "id": 539, + # "amount": 20, + # "fee": 0, + # "address": "0x5c0cc98270d7089408fcbcc8e2131287f5be2306", + # "transaction_id": "0xd4006327a5ec2c41adbdcf566eaaba6597c3d45906abe78ea1a4a022647c2e28", + # "status": True, + # "dismissed": False, + # "rejected": False, + # "description": "", + # "type": "withdrawal", + # "currency": "usdt", + # "created_at": "2020-03-03T07:56:36.198Z", + # "updated_at": "2020-03-03T08:00:05.674Z", + # "user_id": 620 + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_transactions(data, currency, since, limit) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fetchWithdrawals, fetchDeposits + # + # { + # "id": 539, + # "amount": 20, + # "fee": 0, + # "address": "0x5c0cc98270d7089408fcbcc8e2131287f5be2306", + # "transaction_id": "0xd4006327a5ec2c41adbdcf566eaaba6597c3d45906abe78ea1a4a022647c2e28", + # "status": True, + # "dismissed": False, + # "rejected": False, + # "description": "", + # "type": "withdrawal", + # "currency": "usdt", + # "created_at": "2020-03-03T07:56:36.198Z", + # "updated_at": "2020-03-03T08:00:05.674Z", + # "user_id": 620 + # } + # + # withdraw + # + # { + # "message": "Withdrawal request is in the queue and will be processed.", + # "transaction_id": "1d1683c3-576a-4d53-8ff5-27c93fd9758a", + # "amount": 1, + # "currency": "xht", + # "fee": 0, + # "fee_coin": "xht" + # } + # + id = self.safe_string(transaction, 'id') + txid = self.safe_string(transaction, 'transaction_id') + timestamp = self.parse8601(self.safe_string(transaction, 'created_at')) + updated = self.parse8601(self.safe_string(transaction, 'updated_at')) + type = self.safe_string(transaction, 'type') + amount = self.safe_number(transaction, 'amount') + address = self.safe_string(transaction, 'address') + addressTo = None + addressFrom = None + tag = None + tagTo = None + tagFrom = None + if address is not None: + parts = address.split(':') + address = self.safe_string(parts, 0) + tag = self.safe_string(parts, 1) + addressTo = address + tagTo = tag + currencyId = self.safe_string(transaction, 'currency') + currency = self.safe_currency(currencyId, currency) + status = self.safe_value(transaction, 'status') + dismissed = self.safe_value(transaction, 'dismissed') + rejected = self.safe_value(transaction, 'rejected') + if status: + status = 'ok' + elif dismissed: + status = 'canceled' + elif rejected: + status = 'failed' + else: + status = 'pending' + feeCurrencyId = self.safe_string(transaction, 'fee_coin') + feeCurrencyCode = self.safe_currency_code(feeCurrencyId, currency) + feeCost = self.safe_number(transaction, 'fee') + fee = None + if feeCost is not None: + fee = { + 'currency': feeCurrencyCode, + 'cost': feeCost, + } + return { + 'info': transaction, + 'id': id, + 'txid': txid, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': None, + 'addressFrom': addressFrom, + 'address': address, + 'addressTo': addressTo, + 'tagFrom': tagFrom, + 'tag': tag, + 'tagTo': tagTo, + 'type': type, + 'amount': amount, + 'currency': currency['code'], + 'status': status, + 'updated': updated, + 'comment': self.safe_string(transaction, 'message'), + 'internal': None, + 'fee': fee, + } + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://apidocs.hollaex.com/#withdrawal + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.check_address(address) + self.load_markets() + currency = self.currency(code) + if tag is not None: + address += ':' + tag + network = self.safe_string(params, 'network') + if network is None: + raise ArgumentsRequired(self.id + ' withdraw() requires a network parameter') + params = self.omit(params, 'network') + request: dict = { + 'currency': currency['id'], + 'amount': amount, + 'address': address, + 'network': self.network_code_to_id(network, code), + } + response = self.privatePostUserWithdrawal(self.extend(request, params)) + # + # { + # "message": "Withdrawal request is in the queue and will be processed.", + # "transaction_id": "1d1683c3-576a-4d53-8ff5-27c93fd9758a", + # "amount": 1, + # "currency": "xht", + # "fee": 0, + # "fee_coin": "xht" + # } + # + return self.parse_transaction(response, currency) + + def parse_deposit_withdraw_fee(self, fee, currency: Currency = None): + # + # "bch":{ + # "id":4, + # "fullname":"Bitcoin Cash", + # "symbol":"bch", + # "active":true, + # "verified":true, + # "allow_deposit":true, + # "allow_withdrawal":true, + # "withdrawal_fee":0.0001, + # "min":0.001, + # "max":100000, + # "increment_unit":0.001, + # "logo":"https://bitholla.s3.ap-northeast-2.amazonaws.com/icon/BCH-hollaex-asset-01.svg", + # "code":"bch", + # "is_public":true, + # "meta":{}, + # "estimated_price":null, + # "description":null, + # "type":"blockchain", + # "network":null, + # "standard":null, + # "issuer":"HollaEx", + # "withdrawal_fees":null, + # "created_at":"2019-08-09T10:45:43.367Z", + # "updated_at":"2021-12-13T03:08:32.372Z", + # "created_by":1, + # "owner_id":1 + # } + # + result: dict = { + 'info': fee, + 'withdraw': { + 'fee': None, + 'percentage': None, + }, + 'deposit': { + 'fee': None, + 'percentage': None, + }, + 'networks': {}, + } + allowWithdrawal = self.safe_value(fee, 'allow_withdrawal') + if allowWithdrawal: + result['withdraw'] = {'fee': self.safe_number(fee, 'withdrawal_fee'), 'percentage': False} + withdrawalFees = self.safe_value(fee, 'withdrawal_fees') + if withdrawalFees is not None: + keys = list(withdrawalFees.keys()) + keysLength = len(keys) + for i in range(0, keysLength): + key = keys[i] + value = withdrawalFees[key] + currencyId = self.safe_string(value, 'symbol') + currencyCode = self.safe_currency_code(currencyId) + networkCode = self.network_id_to_code(key, currencyCode) + networkCodeUpper = networkCode.upper() # default to the upper case network code + withdrawalFee = self.safe_number(value, 'value') + result['networks'][networkCodeUpper] = { + 'deposit': None, + 'withdraw': withdrawalFee, + } + return result + + def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}): + """ + fetch deposit and withdraw fees + + https://apidocs.hollaex.com/#constants + + :param str[]|None codes: list of unified currency codes + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `fee structures ` + """ + response = self.publicGetConstants(params) + # + # { + # "coins":{ + # "bch":{ + # "id":4, + # "fullname":"Bitcoin Cash", + # "symbol":"bch", + # "active":true, + # "verified":true, + # "allow_deposit":true, + # "allow_withdrawal":true, + # "withdrawal_fee":0.0001, + # "min":0.001, + # "max":100000, + # "increment_unit":0.001, + # "logo":"https://bitholla.s3.ap-northeast-2.amazonaws.com/icon/BCH-hollaex-asset-01.svg", + # "code":"bch", + # "is_public":true, + # "meta":{}, + # "estimated_price":null, + # "description":null, + # "type":"blockchain", + # "network":null, + # "standard":null, + # "issuer":"HollaEx", + # "withdrawal_fees":null, + # "created_at":"2019-08-09T10:45:43.367Z", + # "updated_at":"2021-12-13T03:08:32.372Z", + # "created_by":1, + # "owner_id":1 + # }, + # }, + # "network":"https://api.hollaex.network" + # } + # + coins = self.safe_dict(response, 'coins', {}) + return self.parse_deposit_withdraw_fees(coins, codes, 'symbol') + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + query = self.omit(params, self.extract_params(path)) + path = '/' + self.version + '/' + self.implode_params(path, params) + if (method == 'GET') or (method == 'DELETE'): + if query: + path += '?' + self.urlencode(query) + url = self.urls['api']['rest'] + path + if api == 'private': + self.check_required_credentials() + defaultExpires = self.safe_integer_2(self.options, 'api-expires', 'expires', self.parse_to_int(self.timeout / 1000)) + expires = self.sum(self.seconds(), defaultExpires) + expiresString = str(expires) + auth = method + path + expiresString + headers = { + 'api-key': self.apiKey, + 'api-expires': expiresString, + } + if method == 'POST': + headers['Content-type'] = 'application/json' + if query: + body = self.json(query) + auth += body + signature = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256) + headers['api-signature'] = signature + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + # {"message": "Invalid token"} + if response is None: + return None + if (code >= 400) and (code <= 503): + # + # {"message": "Invalid token"} + # + # different errors return the same code eg + # + # {"message":"Error 1001 - Order rejected. Order could not be submitted order was set to a post only order."} + # + # {"message":"Error 1001 - POST ONLY order can not be of type market"} + # + feedback = self.id + ' ' + body + message = self.safe_string(response, 'message') + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + status = str(code) + self.throw_exactly_matched_exception(self.exceptions['exact'], status, feedback) + return None diff --git a/ccxt/htx.py b/ccxt/htx.py new file mode 100644 index 0000000..bf4690f --- /dev/null +++ b/ccxt/htx.py @@ -0,0 +1,8982 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.htx import ImplicitAPI +import hashlib +from ccxt.base.types import Account, Any, Balances, BorrowInterest, Currencies, Currency, DepositAddress, Int, IsolatedBorrowRate, IsolatedBorrowRates, LedgerEntry, LeverageTier, LeverageTiers, Market, Num, Order, OrderBook, OrderRequest, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, FundingRates, Trade, TradingFeeInterface, Transaction, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import AccountNotEnabled +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import NotSupported +from ccxt.base.errors import OperationFailed +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.errors import OnMaintenance +from ccxt.base.errors import RequestTimeout +from ccxt.base.decimal_to_precision import TRUNCATE +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class htx(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(htx, self).describe(), { + 'id': 'htx', + 'name': 'HTX', + 'countries': ['CN'], + 'rateLimit': 100, + 'userAgent': self.userAgents['chrome100'], + 'certified': True, + 'version': 'v1', + 'hostname': 'api.huobi.pro', # api.testnet.huobi.pro + 'pro': True, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': True, + 'swap': True, + 'future': True, + 'option': None, + 'addMargin': None, + 'borrowCrossMargin': True, + 'borrowIsolatedMargin': True, + 'cancelAllOrders': True, + 'cancelAllOrdersAfter': True, + 'cancelOrder': True, + 'cancelOrders': True, + 'closeAllPositions': False, + 'closePosition': True, + 'createDepositAddress': None, + 'createMarketBuyOrderWithCost': True, + 'createMarketOrderWithCost': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createOrders': True, + 'createReduceOnlyOrder': False, + 'createStopLimitOrder': True, + 'createStopLossOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'createTakeProfitOrder': True, + 'createTrailingPercentOrder': True, + 'createTriggerOrder': True, + 'fetchAccounts': True, + 'fetchBalance': True, + 'fetchBidsAsks': None, + 'fetchBorrowInterest': True, + 'fetchBorrowRateHistories': None, + 'fetchBorrowRateHistory': None, + 'fetchCanceledOrders': None, + 'fetchClosedOrder': None, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDeposit': None, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': None, + 'fetchDepositAddressesByNetwork': True, + 'fetchDeposits': True, + 'fetchDepositWithdrawFee': 'emulated', + 'fetchDepositWithdrawFees': True, + 'fetchFundingHistory': True, + 'fetchFundingRate': True, + 'fetchFundingRateHistory': True, + 'fetchFundingRates': True, + 'fetchIndexOHLCV': True, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': True, + 'fetchL3OrderBook': None, + 'fetchLastPrices': True, + 'fetchLedger': True, + 'fetchLedgerEntry': None, + 'fetchLeverage': False, + 'fetchLeverageTiers': True, + 'fetchLiquidations': True, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarketLeverageTiers': 'emulated', + 'fetchMarkets': True, + 'fetchMarkOHLCV': True, + 'fetchMyLiquidations': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': True, + 'fetchOpenInterestHistory': True, + 'fetchOpenInterests': True, + 'fetchOpenOrder': None, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrderBooks': None, + 'fetchOrders': True, + 'fetchOrderTrades': True, + 'fetchPosition': True, + 'fetchPositionHistory': 'emulated', + 'fetchPositions': True, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': True, + 'fetchSettlementHistory': True, + 'fetchStatus': True, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': True, + 'fetchTradingFees': False, + 'fetchTradingLimits': True, + 'fetchTransactionFee': None, + 'fetchTransactionFees': None, + 'fetchTransactions': None, + 'fetchTransfers': None, + 'fetchWithdrawAddresses': True, + 'fetchWithdrawal': None, + 'fetchWithdrawals': True, + 'fetchWithdrawalWhitelist': None, + 'reduceMargin': None, + 'repayCrossMargin': True, + 'repayIsolatedMargin': True, + 'setLeverage': True, + 'setMarginMode': False, + 'setPositionMode': True, + 'signIn': None, + 'transfer': True, + 'withdraw': True, + }, + 'timeframes': { + '1m': '1min', + '5m': '5min', + '15m': '15min', + '30m': '30min', + '1h': '60min', + '4h': '4hour', + '1d': '1day', + '1w': '1week', + '1M': '1mon', + '1y': '1year', + }, + 'urls': { + # 'test': { + # 'market': 'https://api.testnet.huobi.pro', + # 'public': 'https://api.testnet.huobi.pro', + # 'private': 'https://api.testnet.huobi.pro', + # }, + 'logo': 'https://user-images.githubusercontent.com/1294454/76137448-22748a80-604e-11ea-8069-6e389271911d.jpg', + 'hostnames': { + 'contract': 'api.hbdm.com', + 'spot': 'api.huobi.pro', + 'status': { + 'spot': 'status.huobigroup.com', + 'future': { + 'inverse': 'status-dm.huobigroup.com', + 'linear': 'status-linear-swap.huobigroup.com', # USDT-Margined Contracts + }, + 'swap': { + 'inverse': 'status-swap.huobigroup.com', + 'linear': 'status-linear-swap.huobigroup.com', # USDT-Margined Contracts + }, + }, + # recommended for AWS + # 'contract': 'api.hbdm.vn', + # 'spot': 'api-aws.huobi.pro', + }, + 'api': { + 'status': 'https://{hostname}', + 'contract': 'https://{hostname}', + 'spot': 'https://{hostname}', + 'public': 'https://{hostname}', + 'private': 'https://{hostname}', + 'v2Public': 'https://{hostname}', + 'v2Private': 'https://{hostname}', + }, + 'www': 'https://www.huobi.com', + 'referral': { + 'url': 'https://www.htx.com.vc/invite/en-us/1h?invite_code=6rmm2223', + 'discount': 0.15, + }, + 'doc': [ + 'https://huobiapi.github.io/docs/spot/v1/en/', + 'https://huobiapi.github.io/docs/dm/v1/en/', + 'https://huobiapi.github.io/docs/coin_margined_swap/v1/en/', + 'https://huobiapi.github.io/docs/usdt_swap/v1/en/', + 'https://www.huobi.com/en-us/opend/newApiPages/', + ], + 'fees': 'https://www.huobi.com/about/fee/', + }, + 'api': { + # ------------------------------------------------------------ + # old api definitions + 'v2Public': { + 'get': { + 'reference/currencies': 1, # 币链参考信息 + 'market-status': 1, # 获取当前市场状态 + }, + }, + 'v2Private': { + 'get': { + 'account/ledger': 1, + 'account/withdraw/quota': 1, + 'account/withdraw/address': 1, # 提币地址查询(限母用户可用) + 'account/deposit/address': 1, + 'account/repayment': 5, # 还币交易记录查询 + 'reference/transact-fee-rate': 1, + 'account/asset-valuation': 0.2, # 获取账户资产估值 + 'point/account': 5, # 点卡余额查询 + 'sub-user/user-list': 1, # 获取子用户列表 + 'sub-user/user-state': 1, # 获取特定子用户的用户状态 + 'sub-user/account-list': 1, # 获取特定子用户的账户列表 + 'sub-user/deposit-address': 1, # 子用户充币地址查询 + 'sub-user/query-deposit': 1, # 子用户充币记录查询 + 'user/api-key': 1, # 母子用户API key信息查询 + 'user/uid': 1, # 母子用户获取用户UID + 'algo-orders/opening': 1, # 查询未触发OPEN策略委托 + 'algo-orders/history': 1, # 查询策略委托历史 + 'algo-orders/specific': 1, # 查询特定策略委托 + 'c2c/offers': 1, # 查询借入借出订单 + 'c2c/offer': 1, # 查询特定借入借出订单及其交易记录 + 'c2c/transactions': 1, # 查询借入借出交易记录 + 'c2c/repayment': 1, # 查询还币交易记录 + 'c2c/account': 1, # 查询账户余额 + 'etp/reference': 1, # 基础参考信息 + 'etp/transactions': 5, # 获取杠杆ETP申赎记录 + 'etp/transaction': 5, # 获取特定杠杆ETP申赎记录 + 'etp/rebalance': 1, # 获取杠杆ETP调仓记录 + 'etp/limit': 1, # 获取ETP持仓限额 + }, + 'post': { + 'account/transfer': 1, + 'account/repayment': 5, # 归还借币(全仓逐仓通用) + 'point/transfer': 5, # 点卡划转 + 'sub-user/management': 1, # 冻结/解冻子用户 + 'sub-user/creation': 1, # 子用户创建 + 'sub-user/tradable-market': 1, # 设置子用户交易权限 + 'sub-user/transferability': 1, # 设置子用户资产转出权限 + 'sub-user/api-key-generation': 1, # 子用户API key创建 + 'sub-user/api-key-modification': 1, # 修改子用户API key + 'sub-user/api-key-deletion': 1, # 删除子用户API key + 'sub-user/deduct-mode': 1, # 设置子用户手续费抵扣模式 + 'algo-orders': 1, # 策略委托下单 + 'algo-orders/cancel-all-after': 1, # 自动撤销订单 + 'algo-orders/cancellation': 1, # 策略委托(触发前)撤单 + 'c2c/offer': 1, # 借入借出下单 + 'c2c/cancellation': 1, # 借入借出撤单 + 'c2c/cancel-all': 1, # 撤销所有借入借出订单 + 'c2c/repayment': 1, # 还币 + 'c2c/transfer': 1, # 资产划转 + 'etp/creation': 5, # 杠杆ETP换入 + 'etp/redemption': 5, # 杠杆ETP换出 + 'etp/{transactId}/cancel': 10, # 杠杆ETP单个撤单 + 'etp/batch-cancel': 50, # 杠杆ETP批量撤单 + }, + }, + 'public': { + 'get': { + 'common/symbols': 1, # 查询系统支持的所有交易对 + 'common/currencys': 1, # 查询系统支持的所有币种 + 'common/timestamp': 1, # 查询系统当前时间 + 'common/exchange': 1, # order limits + 'settings/currencys': 1, # ?language=en-US + }, + }, + 'private': { + 'get': { + 'account/accounts': 0.2, # 查询当前用户的所有账户(即account-id) + 'account/accounts/{id}/balance': 0.2, # 查询指定账户的余额 + 'account/accounts/{sub-uid}': 1, + 'account/history': 4, + 'cross-margin/loan-info': 1, + 'margin/loan-info': 1, # 查询借币币息率及额度 + 'fee/fee-rate/get': 1, + 'order/openOrders': 0.4, + 'order/orders': 0.4, + 'order/orders/{id}': 0.4, # 查询某个订单详情 + 'order/orders/{id}/matchresults': 0.4, # 查询某个订单的成交明细 + 'order/orders/getClientOrder': 0.4, + 'order/history': 1, # 查询当前委托、历史委托 + 'order/matchresults': 1, # 查询当前成交、历史成交 + # 'dw/withdraw-virtual/addresses', # 查询虚拟币提现地址(Deprecated) + 'query/deposit-withdraw': 1, + # 'margin/loan-info', # duplicate + 'margin/loan-orders': 0.2, # 借贷订单 + 'margin/accounts/balance': 0.2, # 借贷账户详情 + 'cross-margin/loan-orders': 1, # 查询借币订单 + 'cross-margin/accounts/balance': 1, # 借币账户详情 + 'points/actions': 1, + 'points/orders': 1, + 'subuser/aggregate-balance': 10, + 'stable-coin/exchange_rate': 1, + 'stable-coin/quote': 1, + }, + 'post': { + 'account/transfer': 1, # 资产划转(该节点为母用户和子用户进行资产划转的通用接口。) + 'futures/transfer': 1, + 'order/batch-orders': 0.4, + 'order/orders/place': 0.2, # 创建并执行一个新订单(一步下单, 推荐使用) + 'order/orders/submitCancelClientOrder': 0.2, + 'order/orders/batchCancelOpenOrders': 0.4, + # 'order/orders', # 创建一个新的订单请求 (仅创建订单,不执行下单) + # 'order/orders/{id}/place', # 执行一个订单 (仅执行已创建的订单) + 'order/orders/{id}/submitcancel': 0.2, # 申请撤销一个订单请求 + 'order/orders/batchcancel': 0.4, # 批量撤销订单 + # 'dw/balance/transfer', # 资产划转 + 'dw/withdraw/api/create': 1, # 申请提现虚拟币 + # 'dw/withdraw-virtual/create', # 申请提现虚拟币 + # 'dw/withdraw-virtual/{id}/place', # 确认申请虚拟币提现(Deprecated) + 'dw/withdraw-virtual/{id}/cancel': 1, # 申请取消提现虚拟币 + 'dw/transfer-in/margin': 10, # 现货账户划入至借贷账户 + 'dw/transfer-out/margin': 10, # 借贷账户划出至现货账户 + 'margin/orders': 10, # 申请借贷 + 'margin/orders/{id}/repay': 10, # 归还借贷 + 'cross-margin/transfer-in': 1, # 资产划转 + 'cross-margin/transfer-out': 1, # 资产划转 + 'cross-margin/orders': 1, # 申请借币 + 'cross-margin/orders/{id}/repay': 1, # 归还借币 + 'stable-coin/exchange': 1, + 'subuser/transfer': 10, + }, + }, + # ------------------------------------------------------------ + # new api definitions + # 'https://status.huobigroup.com/api/v2/summary.json': 1, + # 'https://status-dm.huobigroup.com/api/v2/summary.json': 1, + # 'https://status-swap.huobigroup.com/api/v2/summary.json': 1, + # 'https://status-linear-swap.huobigroup.com/api/v2/summary.json': 1, + 'status': { + 'public': { + 'spot': { + 'get': { + 'api/v2/summary.json': 1, + }, + }, + 'future': { + 'inverse': { + 'get': { + 'api/v2/summary.json': 1, + }, + }, + 'linear': { + 'get': { + 'api/v2/summary.json': 1, + }, + }, + }, + 'swap': { + 'inverse': { + 'get': { + 'api/v2/summary.json': 1, + }, + }, + 'linear': { + 'get': { + 'api/v2/summary.json': 1, + }, + }, + }, + }, + }, + 'spot': { + 'public': { + 'get': { + 'v2/market-status': 1, + 'v1/common/symbols': 1, + 'v1/common/currencys': 1, + 'v2/settings/common/currencies': 1, + 'v2/reference/currencies': 1, + 'v1/common/timestamp': 1, + 'v1/common/exchange': 1, # order limits + 'v1/settings/common/chains': 1, + 'v1/settings/common/currencys': 1, + 'v1/settings/common/symbols': 1, + 'v2/settings/common/symbols': 1, + 'v1/settings/common/market-symbols': 1, + # Market Data + 'market/history/candles': 1, + 'market/history/kline': 1, + 'market/detail/merged': 1, + 'market/tickers': 1, + 'market/detail': 1, + 'market/depth': 1, + 'market/trade': 1, + 'market/history/trade': 1, + 'market/etp': 1, # Get real-time equity of leveraged ETP + # ETP + 'v2/etp/reference': 1, + 'v2/etp/rebalance': 1, + }, + }, + 'private': { + 'get': { + # Account + 'v1/account/accounts': 0.2, + 'v1/account/accounts/{account-id}/balance': 0.2, + 'v2/account/valuation': 1, + 'v2/account/asset-valuation': 0.2, + 'v1/account/history': 4, + 'v2/account/ledger': 1, + 'v2/point/account': 5, + # Wallet(Deposit and Withdraw) + 'v2/account/deposit/address': 1, + 'v2/account/withdraw/quota': 1, + 'v2/account/withdraw/address': 1, + 'v2/reference/currencies': 1, + 'v1/query/deposit-withdraw': 1, + 'v1/query/withdraw/client-order-id': 1, + # Sub user management + 'v2/user/api-key': 1, + 'v2/user/uid': 1, + 'v2/sub-user/user-list': 1, + 'v2/sub-user/user-state': 1, + 'v2/sub-user/account-list': 1, + 'v2/sub-user/deposit-address': 1, + 'v2/sub-user/query-deposit': 1, + 'v1/subuser/aggregate-balance': 10, + 'v1/account/accounts/{sub-uid}': 1, + # Trading + 'v1/order/openOrders': 0.4, + 'v1/order/orders/{order-id}': 0.4, + 'v1/order/orders/getClientOrder': 0.4, + 'v1/order/orders/{order-id}/matchresult': 0.4, + 'v1/order/orders/{order-id}/matchresults': 0.4, + 'v1/order/orders': 0.4, + 'v1/order/history': 1, + 'v1/order/matchresults': 1, + 'v2/reference/transact-fee-rate': 1, + # Conditional Order + 'v2/algo-orders/opening': 1, + 'v2/algo-orders/history': 1, + 'v2/algo-orders/specific': 1, + # Margin Loan(Cross/Isolated) + 'v1/margin/loan-info': 1, + 'v1/margin/loan-orders': 0.2, + 'v1/margin/accounts/balance': 0.2, + 'v1/cross-margin/loan-info': 1, + 'v1/cross-margin/loan-orders': 1, + 'v1/cross-margin/accounts/balance': 1, + 'v2/account/repayment': 5, + # Stable Coin Exchange + 'v1/stable-coin/quote': 1, + 'v1/stable_coin/exchange_rate': 1, + # ETP + 'v2/etp/transactions': 5, + 'v2/etp/transaction': 5, + 'v2/etp/limit': 1, + }, + 'post': { + # Account + 'v1/account/transfer': 1, + 'v1/futures/transfer': 1, # future transfers + 'v2/point/transfer': 5, + 'v2/account/transfer': 1, # swap transfers + # Wallet(Deposit and Withdraw) + 'v1/dw/withdraw/api/create': 1, + 'v1/dw/withdraw-virtual/{withdraw-id}/cancel': 1, + # Sub user management + 'v2/sub-user/deduct-mode': 1, + 'v2/sub-user/creation': 1, + 'v2/sub-user/management': 1, + 'v2/sub-user/tradable-market': 1, + 'v2/sub-user/transferability': 1, + 'v2/sub-user/api-key-generation': 1, + 'v2/sub-user/api-key-modification': 1, + 'v2/sub-user/api-key-deletion': 1, + 'v1/subuser/transfer': 10, + 'v1/trust/user/active/credit': 10, + # Trading + 'v1/order/orders/place': 0.2, + 'v1/order/batch-orders': 0.4, + 'v1/order/auto/place': 0.2, + 'v1/order/orders/{order-id}/submitcancel': 0.2, + 'v1/order/orders/submitCancelClientOrder': 0.2, + 'v1/order/orders/batchCancelOpenOrders': 0.4, + 'v1/order/orders/batchcancel': 0.4, + 'v2/algo-orders/cancel-all-after': 1, + # Conditional Order + 'v2/algo-orders': 1, + 'v2/algo-orders/cancellation': 1, + # Margin Loan(Cross/Isolated) + 'v2/account/repayment': 5, + 'v1/dw/transfer-in/margin': 10, + 'v1/dw/transfer-out/margin': 10, + 'v1/margin/orders': 10, + 'v1/margin/orders/{order-id}/repay': 10, + 'v1/cross-margin/transfer-in': 1, + 'v1/cross-margin/transfer-out': 1, + 'v1/cross-margin/orders': 1, + 'v1/cross-margin/orders/{order-id}/repay': 1, + # Stable Coin Exchange + 'v1/stable-coin/exchange': 1, + # ETP + 'v2/etp/creation': 5, + 'v2/etp/redemption': 5, + 'v2/etp/{transactId}/cancel': 10, + 'v2/etp/batch-cancel': 50, + }, + }, + }, + 'contract': { + 'public': { + 'get': { + 'api/v1/timestamp': 1, + 'heartbeat/': 1, # backslash is not a typo + # Future Market Data interface + 'api/v1/contract_contract_info': 1, + 'api/v1/contract_index': 1, + 'api/v1/contract_query_elements': 1, + 'api/v1/contract_price_limit': 1, + 'api/v1/contract_open_interest': 1, + 'api/v1/contract_delivery_price': 1, + 'market/depth': 1, + 'market/bbo': 1, + 'market/history/kline': 1, + 'index/market/history/mark_price_kline': 1, + 'market/detail/merged': 1, + 'market/detail/batch_merged': 1, + 'v2/market/detail/batch_merged': 1, + 'market/trade': 1, + 'market/history/trade': 1, + 'api/v1/contract_risk_info': 1, + 'api/v1/contract_insurance_fund': 1, + 'api/v1/contract_adjustfactor': 1, + 'api/v1/contract_his_open_interest': 1, + 'api/v1/contract_ladder_margin': 1, + 'api/v1/contract_api_state': 1, + 'api/v1/contract_elite_account_ratio': 1, + 'api/v1/contract_elite_position_ratio': 1, + 'api/v1/contract_liquidation_orders': 1, + 'api/v1/contract_settlement_records': 1, + 'index/market/history/index': 1, + 'index/market/history/basis': 1, + 'api/v1/contract_estimated_settlement_price': 1, + 'api/v3/contract_liquidation_orders': 1, + # Swap Market Data interface + 'swap-api/v1/swap_contract_info': 1, + 'swap-api/v1/swap_index': 1, + 'swap-api/v1/swap_query_elements': 1, + 'swap-api/v1/swap_price_limit': 1, + 'swap-api/v1/swap_open_interest': 1, + 'swap-ex/market/depth': 1, + 'swap-ex/market/bbo': 1, + 'swap-ex/market/history/kline': 1, + 'index/market/history/swap_mark_price_kline': 1, + 'swap-ex/market/detail/merged': 1, + 'v2/swap-ex/market/detail/batch_merged': 1, + 'index/market/history/swap_premium_index_kline': 1, + 'swap-ex/market/detail/batch_merged': 1, + 'swap-ex/market/trade': 1, + 'swap-ex/market/history/trade': 1, + 'swap-api/v1/swap_risk_info': 1, + 'swap-api/v1/swap_insurance_fund': 1, + 'swap-api/v1/swap_adjustfactor': 1, + 'swap-api/v1/swap_his_open_interest': 1, + 'swap-api/v1/swap_ladder_margin': 1, + 'swap-api/v1/swap_api_state': 1, + 'swap-api/v1/swap_elite_account_ratio': 1, + 'swap-api/v1/swap_elite_position_ratio': 1, + 'swap-api/v1/swap_estimated_settlement_price': 1, + 'swap-api/v1/swap_liquidation_orders': 1, + 'swap-api/v1/swap_settlement_records': 1, + 'swap-api/v1/swap_funding_rate': 1, + 'swap-api/v1/swap_batch_funding_rate': 1, + 'swap-api/v1/swap_historical_funding_rate': 1, + 'swap-api/v3/swap_liquidation_orders': 1, + 'index/market/history/swap_estimated_rate_kline': 1, + 'index/market/history/swap_basis': 1, + # Swap Market Data interface + 'linear-swap-api/v1/swap_contract_info': 1, + 'linear-swap-api/v1/swap_index': 1, + 'linear-swap-api/v1/swap_query_elements': 1, + 'linear-swap-api/v1/swap_price_limit': 1, + 'linear-swap-api/v1/swap_open_interest': 1, + 'linear-swap-ex/market/depth': 1, + 'linear-swap-ex/market/bbo': 1, + 'linear-swap-ex/market/history/kline': 1, + 'index/market/history/linear_swap_mark_price_kline': 1, + 'linear-swap-ex/market/detail/merged': 1, + 'linear-swap-ex/market/detail/batch_merged': 1, + 'v2/linear-swap-ex/market/detail/batch_merged': 1, + 'linear-swap-ex/market/trade': 1, + 'linear-swap-ex/market/history/trade': 1, + 'linear-swap-api/v1/swap_risk_info': 1, + 'swap-api/v1/linear-swap-api/v1/swap_insurance_fund': 1, + 'linear-swap-api/v1/swap_adjustfactor': 1, + 'linear-swap-api/v1/swap_cross_adjustfactor': 1, + 'linear-swap-api/v1/swap_his_open_interest': 1, + 'linear-swap-api/v1/swap_ladder_margin': 1, + 'linear-swap-api/v1/swap_cross_ladder_margin': 1, + 'linear-swap-api/v1/swap_api_state': 1, + 'linear-swap-api/v1/swap_cross_transfer_state': 1, + 'linear-swap-api/v1/swap_cross_trade_state': 1, + 'linear-swap-api/v1/swap_elite_account_ratio': 1, + 'linear-swap-api/v1/swap_elite_position_ratio': 1, + 'linear-swap-api/v1/swap_liquidation_orders': 1, + 'linear-swap-api/v1/swap_settlement_records': 1, + 'linear-swap-api/v1/swap_funding_rate': 1, + 'linear-swap-api/v1/swap_batch_funding_rate': 1, + 'linear-swap-api/v1/swap_historical_funding_rate': 1, + 'linear-swap-api/v3/swap_liquidation_orders': 1, + 'index/market/history/linear_swap_premium_index_kline': 1, + 'index/market/history/linear_swap_estimated_rate_kline': 1, + 'index/market/history/linear_swap_basis': 1, + 'linear-swap-api/v1/swap_estimated_settlement_price': 1, + }, + }, + 'private': { + 'get': { + # Future Account Interface + 'api/v1/contract_sub_auth_list': 1, + 'api/v1/contract_api_trading_status': 1, + # Swap Account Interface + 'swap-api/v1/swap_sub_auth_list': 1, + 'swap-api/v1/swap_api_trading_status': 1, + # Swap Account Interface + 'linear-swap-api/v1/swap_sub_auth_list': 1, + 'linear-swap-api/v1/swap_api_trading_status': 1, + 'linear-swap-api/v1/swap_cross_position_side': 1, + 'linear-swap-api/v1/swap_position_side': 1, + 'linear-swap-api/v3/unified_account_info': 1, + 'linear-swap-api/v3/fix_position_margin_change_record': 1, + 'linear-swap-api/v3/swap_unified_account_type': 1, + 'linear-swap-api/v3/linear_swap_overview_account_info': 1, + }, + 'post': { + # Future Account Interface + 'api/v1/contract_balance_valuation': 1, + 'api/v1/contract_account_info': 1, + 'api/v1/contract_position_info': 1, + 'api/v1/contract_sub_auth': 1, + 'api/v1/contract_sub_account_list': 1, + 'api/v1/contract_sub_account_info_list': 1, + 'api/v1/contract_sub_account_info': 1, + 'api/v1/contract_sub_position_info': 1, + 'api/v1/contract_financial_record': 1, + 'api/v1/contract_financial_record_exact': 1, + 'api/v1/contract_user_settlement_records': 1, + 'api/v1/contract_order_limit': 1, + 'api/v1/contract_fee': 1, + 'api/v1/contract_transfer_limit': 1, + 'api/v1/contract_position_limit': 1, + 'api/v1/contract_account_position_info': 1, + 'api/v1/contract_master_sub_transfer': 1, + 'api/v1/contract_master_sub_transfer_record': 1, + 'api/v1/contract_available_level_rate': 1, + 'api/v3/contract_financial_record': 1, + 'api/v3/contract_financial_record_exact': 1, + # Future Trade Interface + 'api/v1/contract-cancel-after': 1, + 'api/v1/contract_order': 1, + 'api/v1/contract_batchorder': 1, + 'api/v1/contract_cancel': 1, + 'api/v1/contract_cancelall': 1, + 'api/v1/contract_switch_lever_rate': 30, + 'api/v1/lightning_close_position': 1, + 'api/v1/contract_order_info': 1, + 'api/v1/contract_order_detail': 1, + 'api/v1/contract_openorders': 1, + 'api/v1/contract_hisorders': 1, + 'api/v1/contract_hisorders_exact': 1, + 'api/v1/contract_matchresults': 1, + 'api/v1/contract_matchresults_exact': 1, + 'api/v3/contract_hisorders': 1, + 'api/v3/contract_hisorders_exact': 1, + 'api/v3/contract_matchresults': 1, + 'api/v3/contract_matchresults_exact': 1, + # Contract Strategy Order Interface + 'api/v1/contract_trigger_order': 1, + 'api/v1/contract_trigger_cancel': 1, + 'api/v1/contract_trigger_cancelall': 1, + 'api/v1/contract_trigger_openorders': 1, + 'api/v1/contract_trigger_hisorders': 1, + 'api/v1/contract_tpsl_order': 1, + 'api/v1/contract_tpsl_cancel': 1, + 'api/v1/contract_tpsl_cancelall': 1, + 'api/v1/contract_tpsl_openorders': 1, + 'api/v1/contract_tpsl_hisorders': 1, + 'api/v1/contract_relation_tpsl_order': 1, + 'api/v1/contract_track_order': 1, + 'api/v1/contract_track_cancel': 1, + 'api/v1/contract_track_cancelall': 1, + 'api/v1/contract_track_openorders': 1, + 'api/v1/contract_track_hisorders': 1, + # Swap Account Interface + 'swap-api/v1/swap_balance_valuation': 1, + 'swap-api/v1/swap_account_info': 1, + 'swap-api/v1/swap_position_info': 1, + 'swap-api/v1/swap_account_position_info': 1, + 'swap-api/v1/swap_sub_auth': 1, + 'swap-api/v1/swap_sub_account_list': 1, + 'swap-api/v1/swap_sub_account_info_list': 1, + 'swap-api/v1/swap_sub_account_info': 1, + 'swap-api/v1/swap_sub_position_info': 1, + 'swap-api/v1/swap_financial_record': 1, + 'swap-api/v1/swap_financial_record_exact': 1, + 'swap-api/v1/swap_user_settlement_records': 1, + 'swap-api/v1/swap_available_level_rate': 1, + 'swap-api/v1/swap_order_limit': 1, + 'swap-api/v1/swap_fee': 1, + 'swap-api/v1/swap_transfer_limit': 1, + 'swap-api/v1/swap_position_limit': 1, + 'swap-api/v1/swap_master_sub_transfer': 1, + 'swap-api/v1/swap_master_sub_transfer_record': 1, + 'swap-api/v3/swap_financial_record': 1, + 'swap-api/v3/swap_financial_record_exact': 1, + # Swap Trade Interface + 'swap-api/v1/swap-cancel-after': 1, + 'swap-api/v1/swap_order': 1, + 'swap-api/v1/swap_batchorder': 1, + 'swap-api/v1/swap_cancel': 1, + 'swap-api/v1/swap_cancelall': 1, + 'swap-api/v1/swap_lightning_close_position': 1, + 'swap-api/v1/swap_switch_lever_rate': 30, + 'swap-api/v1/swap_order_info': 1, + 'swap-api/v1/swap_order_detail': 1, + 'swap-api/v1/swap_openorders': 1, + 'swap-api/v1/swap_hisorders': 1, + 'swap-api/v1/swap_hisorders_exact': 1, + 'swap-api/v1/swap_matchresults': 1, + 'swap-api/v1/swap_matchresults_exact': 1, + 'swap-api/v3/swap_matchresults': 1, + 'swap-api/v3/swap_matchresults_exact': 1, + 'swap-api/v3/swap_hisorders': 1, + 'swap-api/v3/swap_hisorders_exact': 1, + # Swap Strategy Order Interface + 'swap-api/v1/swap_trigger_order': 1, + 'swap-api/v1/swap_trigger_cancel': 1, + 'swap-api/v1/swap_trigger_cancelall': 1, + 'swap-api/v1/swap_trigger_openorders': 1, + 'swap-api/v1/swap_trigger_hisorders': 1, + 'swap-api/v1/swap_tpsl_order': 1, + 'swap-api/v1/swap_tpsl_cancel': 1, + 'swap-api/v1/swap_tpsl_cancelall': 1, + 'swap-api/v1/swap_tpsl_openorders': 1, + 'swap-api/v1/swap_tpsl_hisorders': 1, + 'swap-api/v1/swap_relation_tpsl_order': 1, + 'swap-api/v1/swap_track_order': 1, + 'swap-api/v1/swap_track_cancel': 1, + 'swap-api/v1/swap_track_cancelall': 1, + 'swap-api/v1/swap_track_openorders': 1, + 'swap-api/v1/swap_track_hisorders': 1, + # Swap Account Interface + 'linear-swap-api/v1/swap_lever_position_limit': 1, + 'linear-swap-api/v1/swap_cross_lever_position_limit': 1, + 'linear-swap-api/v1/swap_balance_valuation': 1, + 'linear-swap-api/v1/swap_account_info': 1, + 'linear-swap-api/v1/swap_cross_account_info': 1, + 'linear-swap-api/v1/swap_position_info': 1, + 'linear-swap-api/v1/swap_cross_position_info': 1, + 'linear-swap-api/v1/swap_account_position_info': 1, + 'linear-swap-api/v1/swap_cross_account_position_info': 1, + 'linear-swap-api/v1/swap_sub_auth': 1, + 'linear-swap-api/v1/swap_sub_account_list': 1, + 'linear-swap-api/v1/swap_cross_sub_account_list': 1, + 'linear-swap-api/v1/swap_sub_account_info_list': 1, + 'linear-swap-api/v1/swap_cross_sub_account_info_list': 1, + 'linear-swap-api/v1/swap_sub_account_info': 1, + 'linear-swap-api/v1/swap_cross_sub_account_info': 1, + 'linear-swap-api/v1/swap_sub_position_info': 1, + 'linear-swap-api/v1/swap_cross_sub_position_info': 1, + 'linear-swap-api/v1/swap_financial_record': 1, + 'linear-swap-api/v1/swap_financial_record_exact': 1, + 'linear-swap-api/v1/swap_user_settlement_records': 1, + 'linear-swap-api/v1/swap_cross_user_settlement_records': 1, + 'linear-swap-api/v1/swap_available_level_rate': 1, + 'linear-swap-api/v1/swap_cross_available_level_rate': 1, + 'linear-swap-api/v1/swap_order_limit': 1, + 'linear-swap-api/v1/swap_fee': 1, + 'linear-swap-api/v1/swap_transfer_limit': 1, + 'linear-swap-api/v1/swap_cross_transfer_limit': 1, + 'linear-swap-api/v1/swap_position_limit': 1, + 'linear-swap-api/v1/swap_cross_position_limit': 1, + 'linear-swap-api/v1/swap_master_sub_transfer': 1, + 'linear-swap-api/v1/swap_master_sub_transfer_record': 1, + 'linear-swap-api/v1/swap_transfer_inner': 1, + 'linear-swap-api/v3/swap_financial_record': 1, + 'linear-swap-api/v3/swap_financial_record_exact': 1, + # Swap Trade Interface + 'linear-swap-api/v1/swap_order': 1, + 'linear-swap-api/v1/swap_cross_order': 1, + 'linear-swap-api/v1/swap_batchorder': 1, + 'linear-swap-api/v1/swap_cross_batchorder': 1, + 'linear-swap-api/v1/swap_cancel': 1, + 'linear-swap-api/v1/swap_cross_cancel': 1, + 'linear-swap-api/v1/swap_cancelall': 1, + 'linear-swap-api/v1/swap_cross_cancelall': 1, + 'linear-swap-api/v1/swap_switch_lever_rate': 30, + 'linear-swap-api/v1/swap_cross_switch_lever_rate': 30, + 'linear-swap-api/v1/swap_lightning_close_position': 1, + 'linear-swap-api/v1/swap_cross_lightning_close_position': 1, + 'linear-swap-api/v1/swap_order_info': 1, + 'linear-swap-api/v1/swap_cross_order_info': 1, + 'linear-swap-api/v1/swap_order_detail': 1, + 'linear-swap-api/v1/swap_cross_order_detail': 1, + 'linear-swap-api/v1/swap_openorders': 1, + 'linear-swap-api/v1/swap_cross_openorders': 1, + 'linear-swap-api/v1/swap_hisorders': 1, + 'linear-swap-api/v1/swap_cross_hisorders': 1, + 'linear-swap-api/v1/swap_hisorders_exact': 1, + 'linear-swap-api/v1/swap_cross_hisorders_exact': 1, + 'linear-swap-api/v1/swap_matchresults': 1, + 'linear-swap-api/v1/swap_cross_matchresults': 1, + 'linear-swap-api/v1/swap_matchresults_exact': 1, + 'linear-swap-api/v1/swap_cross_matchresults_exact': 1, + 'linear-swap-api/v1/linear-cancel-after': 1, + 'linear-swap-api/v1/swap_switch_position_mode': 1, + 'linear-swap-api/v1/swap_cross_switch_position_mode': 1, + 'linear-swap-api/v3/swap_matchresults': 1, + 'linear-swap-api/v3/swap_cross_matchresults': 1, + 'linear-swap-api/v3/swap_matchresults_exact': 1, + 'linear-swap-api/v3/swap_cross_matchresults_exact': 1, + 'linear-swap-api/v3/swap_hisorders': 1, + 'linear-swap-api/v3/swap_cross_hisorders': 1, + 'linear-swap-api/v3/swap_hisorders_exact': 1, + 'linear-swap-api/v3/swap_cross_hisorders_exact': 1, + 'linear-swap-api/v3/fix_position_margin_change': 1, + 'linear-swap-api/v3/swap_switch_account_type': 1, + 'linear-swap-api/v3/linear_swap_fee_switch': 1, + # Swap Strategy Order Interface + 'linear-swap-api/v1/swap_trigger_order': 1, + 'linear-swap-api/v1/swap_cross_trigger_order': 1, + 'linear-swap-api/v1/swap_trigger_cancel': 1, + 'linear-swap-api/v1/swap_cross_trigger_cancel': 1, + 'linear-swap-api/v1/swap_trigger_cancelall': 1, + 'linear-swap-api/v1/swap_cross_trigger_cancelall': 1, + 'linear-swap-api/v1/swap_trigger_openorders': 1, + 'linear-swap-api/v1/swap_cross_trigger_openorders': 1, + 'linear-swap-api/v1/swap_trigger_hisorders': 1, + 'linear-swap-api/v1/swap_cross_trigger_hisorders': 1, + 'linear-swap-api/v1/swap_tpsl_order': 1, + 'linear-swap-api/v1/swap_cross_tpsl_order': 1, + 'linear-swap-api/v1/swap_tpsl_cancel': 1, + 'linear-swap-api/v1/swap_cross_tpsl_cancel': 1, + 'linear-swap-api/v1/swap_tpsl_cancelall': 1, + 'linear-swap-api/v1/swap_cross_tpsl_cancelall': 1, + 'linear-swap-api/v1/swap_tpsl_openorders': 1, + 'linear-swap-api/v1/swap_cross_tpsl_openorders': 1, + 'linear-swap-api/v1/swap_tpsl_hisorders': 1, + 'linear-swap-api/v1/swap_cross_tpsl_hisorders': 1, + 'linear-swap-api/v1/swap_relation_tpsl_order': 1, + 'linear-swap-api/v1/swap_cross_relation_tpsl_order': 1, + 'linear-swap-api/v1/swap_track_order': 1, + 'linear-swap-api/v1/swap_cross_track_order': 1, + 'linear-swap-api/v1/swap_track_cancel': 1, + 'linear-swap-api/v1/swap_cross_track_cancel': 1, + 'linear-swap-api/v1/swap_track_cancelall': 1, + 'linear-swap-api/v1/swap_cross_track_cancelall': 1, + 'linear-swap-api/v1/swap_track_openorders': 1, + 'linear-swap-api/v1/swap_cross_track_openorders': 1, + 'linear-swap-api/v1/swap_track_hisorders': 1, + 'linear-swap-api/v1/swap_cross_track_hisorders': 1, + }, + }, + }, + }, + 'fees': { + 'trading': { + 'feeSide': 'get', + 'tierBased': False, + 'percentage': True, + 'maker': self.parse_number('0.002'), + 'taker': self.parse_number('0.002'), + }, + }, + 'exceptions': { + 'broad': { + 'contract is restricted of closing positions on API. Please contact customer service': OnMaintenance, + 'maintain': OnMaintenance, + 'API key has no permission': PermissionDenied, # {"status":"error","err-code":"api-signature-not-valid","err-msg":"Signature not valid: API key has no permission [API Key没有权限]","data":null} + }, + 'exact': { + # err-code + '403': AuthenticationError, # {"status":"error","err_code":403,"err_msg":"Incorrect Access key [Access key错误]","ts":1652774224344} + '1010': AccountNotEnabled, # {"status":"error","err_code":1010,"err_msg":"Account doesnt exist.","ts":1648137970490} + '1003': AuthenticationError, # {code: '1003', message: 'invalid signature'} + '1013': BadSymbol, # {"status":"error","err_code":1013,"err_msg":"This contract symbol doesnt exist.","ts":1640550459583} + '1017': OrderNotFound, # {"status":"error","err_code":1017,"err_msg":"Order doesnt exist.","ts":1640550859242} + '1034': InvalidOrder, # {"status":"error","err_code":1034,"err_msg":"Incorrect field of order price type.","ts":1643802870182} + '1036': InvalidOrder, # {"status":"error","err_code":1036,"err_msg":"Incorrect field of open long form.","ts":1643802518986} + '1039': InvalidOrder, # {"status":"error","err_code":1039,"err_msg":"Buy price must be lower than 39270.9USDT. Sell price must exceed 37731USDT.","ts":1643802374403} + '1041': InvalidOrder, # {"status":"error","err_code":1041,"err_msg":"The order amount exceeds the limit(170000Cont), please modify and order again.","ts":1643802784940} + '1047': InsufficientFunds, # {"status":"error","err_code":1047,"err_msg":"Insufficient margin available.","ts":1643802672652} + '1048': InsufficientFunds, # {"status":"error","err_code":1048,"err_msg":"Insufficient close amount available.","ts":1652772408864} + '1061': OrderNotFound, # {"status":"ok","data":{"errors":[{"order_id":"1349442392365359104","err_code":1061,"err_msg":"The order does not exist."}],"successes":""},"ts":1741773744526} + '1051': InvalidOrder, # {"status":"error","err_code":1051,"err_msg":"No orders to cancel.","ts":1652552125876} + '1066': BadSymbol, # {"status":"error","err_code":1066,"err_msg":"The symbol field cannot be empty. Please re-enter.","ts":1640550819147} + '1067': InvalidOrder, # {"status":"error","err_code":1067,"err_msg":"The client_order_id field is invalid. Please re-enter.","ts":1643802119413} + '1094': InvalidOrder, # {"status":"error","err_code":1094,"err_msg":"The leverage cannot be empty, please switch the leverage or contact customer service","ts":1640496946243} + '1220': AccountNotEnabled, # {"status":"error","err_code":1220,"err_msg":"You don’t have access permission have not opened contracts trading.","ts":1645096660718} + '1303': BadRequest, # {"code":1303,"data":null,"message":"Each transfer-out cannot be less than 5USDT.","success":false,"print-log":true} + '1461': InvalidOrder, # {"status":"error","err_code":1461,"err_msg":"Current positions have triggered position limits(5000USDT). Please modify.","ts":1652554651234} + '4007': BadRequest, # {"code":"4007","msg":"Unified account special interface, non - one account is not available","data":null,"ts":"1698413427651"}' + 'bad-request': BadRequest, + 'validation-format-error': BadRequest, # {"status":"error","err-code":"validation-format-error","err-msg":"Format Error: order-id.","data":null} + 'validation-constraints-required': BadRequest, # {"status":"error","err-code":"validation-constraints-required","err-msg":"Field is missing: client-order-id.","data":null} + 'base-date-limit-error': BadRequest, # {"status":"error","err-code":"base-date-limit-error","err-msg":"date less than system limit","data":null} + 'api-not-support-temp-addr': PermissionDenied, # {"status":"error","err-code":"api-not-support-temp-addr","err-msg":"API withdrawal does not support temporary addresses","data":null} + 'timeout': RequestTimeout, # {"ts":1571653730865,"status":"error","err-code":"timeout","err-msg":"Request Timeout"} + 'gateway-internal-error': ExchangeNotAvailable, # {"status":"error","err-code":"gateway-internal-error","err-msg":"Failed to load data. Try again later.","data":null} + 'account-frozen-balance-insufficient-error': InsufficientFunds, # {"status":"error","err-code":"account-frozen-balance-insufficient-error","err-msg":"trade account balance is not enough, left: `0.0027`","data":null} + 'invalid-amount': InvalidOrder, # eg "Paramemter `amount` is invalid." + 'order-limitorder-amount-min-error': InvalidOrder, # limit order amount error, min: `0.001` + 'order-limitorder-amount-max-error': InvalidOrder, # market order amount error, max: `1000000` + 'order-marketorder-amount-min-error': InvalidOrder, # market order amount error, min: `0.01` + 'order-limitorder-price-min-error': InvalidOrder, # limit order price error + 'order-limitorder-price-max-error': InvalidOrder, # limit order price error + 'order-stop-order-hit-trigger': InvalidOrder, # {"status":"error","err-code":"order-stop-order-hit-trigger","err-msg":"Orders that are triggered immediately are not supported.","data":null} + 'order-value-min-error': InvalidOrder, # {"status":"error","err-code":"order-value-min-error","err-msg":"Order total cannot be lower than: 1 USDT","data":null} + 'order-invalid-price': InvalidOrder, # {"status":"error","err-code":"order-invalid-price","err-msg":"invalid price","data":null} + 'order-holding-limit-failed': InvalidOrder, # {"status":"error","err-code":"order-holding-limit-failed","err-msg":"Order failed, exceeded the holding limit of self currency","data":null} + 'order-orderprice-precision-error': InvalidOrder, # {"status":"error","err-code":"order-orderprice-precision-error","err-msg":"order price precision error, scale: `4`","data":null} + 'order-etp-nav-price-max-error': InvalidOrder, # {"status":"error","err-code":"order-etp-nav-price-max-error","err-msg":"Order price cannot be higher than 5% of NAV","data":null} + 'order-orderstate-error': OrderNotFound, # canceling an already canceled order + 'order-queryorder-invalid': OrderNotFound, # querying a non-existent order + 'order-update-error': ExchangeNotAvailable, # undocumented error + 'api-signature-check-failed': AuthenticationError, + 'api-signature-not-valid': AuthenticationError, # {"status":"error","err-code":"api-signature-not-valid","err-msg":"Signature not valid: Incorrect Access key [Access key错误]","data":null} + 'base-record-invalid': OrderNotFound, # https://github.com/ccxt/ccxt/issues/5750 + 'base-symbol-trade-disabled': BadSymbol, # {"status":"error","err-code":"base-symbol-trade-disabled","err-msg":"Trading is disabled for self symbol","data":null} + 'base-symbol-error': BadSymbol, # {"status":"error","err-code":"base-symbol-error","err-msg":"The symbol is invalid","data":null} + 'system-maintenance': OnMaintenance, # {"status": "error", "err-code": "system-maintenance", "err-msg": "System is in maintenance!", "data": null} + 'base-request-exceed-frequency-limit': RateLimitExceeded, # {"status":"error","err-code":"base-request-exceed-frequency-limit","err-msg":"Frequency of requests has exceeded the limit, please try again later","data":null} + # err-msg + 'invalid symbol': BadSymbol, # {"ts":1568813334794,"status":"error","err-code":"invalid-parameter","err-msg":"invalid symbol"} + 'symbol trade not open now': BadSymbol, # {"ts":1576210479343,"status":"error","err-code":"invalid-parameter","err-msg":"symbol trade not open now"} + 'require-symbol': BadSymbol, # {"status":"error","err-code":"require-symbol","err-msg":"Parameter `symbol` is required.","data":null}, + 'invalid-address': BadRequest, # {"status":"error","err-code":"invalid-address","err-msg":"Invalid address.","data":null}, + 'base-currency-chain-error': BadRequest, # {"status":"error","err-code":"base-currency-chain-error","err-msg":"The current currency chain does not exist","data":null}, + 'dw-insufficient-balance': InsufficientFunds, # {"status":"error","err-code":"dw-insufficient-balance","err-msg":"Insufficient balance. You can only transfer `12.3456` at most.","data":null} + 'base-withdraw-fee-error': BadRequest, # {"status":"error","err-code":"base-withdraw-fee-error","err-msg":"withdrawal fee is not within limits","data":null} + 'dw-withdraw-min-limit': BadRequest, # {"status":"error","err-code":"dw-withdraw-min-limit","err-msg":"The withdrawal amount is less than the minimum limit.","data":null} + 'request limit': RateLimitExceeded, # {"ts":1687004814731,"status":"error","err-code":"invalid-parameter","err-msg":"request limit"} + }, + }, + 'precisionMode': TICK_SIZE, + 'options': { + 'include_OS_certificates': False, # temporarily leave self, remove in future + 'fetchMarkets': { + 'types': { + 'spot': True, + 'linear': True, + 'inverse': True, + }, + }, + 'timeDifference': 0, # the difference between system clock and exchange clock + 'adjustForTimeDifference': False, # controls the adjustment logic upon instantiation + 'fetchOHLCV': { + 'useHistoricalEndpointForSpot': True, + }, + 'withdraw': { + 'includeFee': False, + }, + 'defaultType': 'spot', # spot, future, swap + 'defaultSubType': 'linear', # inverse, linear + 'defaultNetwork': 'ERC20', + 'defaultNetworks': { + 'ETH': 'ERC20', + 'BTC': 'BTC', + 'USDT': 'TRC20', + }, + 'networks': { + # by displaynames + 'TRC20': 'TRX', # TRON for mainnet + 'BTC': 'BTC', + 'ERC20': 'ETH', # ETH for mainnet + 'SOL': 'SOLANA', + 'HRC20': 'HECO', + 'BEP20': 'BSC', + 'XMR': 'XMR', + 'LTC': 'LTC', + 'XRP': 'XRP', + 'XLM': 'XLM', + 'CRONOS': 'CRO', + 'CRO': 'CRO', + 'GLMR': 'GLMR', + 'POLYGON': 'MATIC', + 'MATIC': 'MATIC', + 'BTT': 'BTT', + 'CUBE': 'CUBE', + 'IOST': 'IOST', + 'NEO': 'NEO', + 'KLAY': 'KLAY', + 'EOS': 'EOS', + 'THETA': 'THETA', + 'NAS': 'NAS', + 'NULS': 'NULS', + 'QTUM': 'QTUM', + 'FTM': 'FTM', + 'CELO': 'CELO', + 'DOGE': 'DOGE', + 'DOGECHAIN': 'DOGECHAIN', + 'NEAR': 'NEAR', + 'STEP': 'STEP', + 'BITCI': 'BITCI', + 'CARDANO': 'ADA', + 'ADA': 'ADA', + 'ETC': 'ETC', + 'LUK': 'LUK', + 'MINEPLEX': 'MINEPLEX', + 'DASH': 'DASH', + 'ZEC': 'ZEC', + 'IOTA': 'IOTA', + 'NEON3': 'NEON3', + 'XEM': 'XEM', + 'HC': 'HC', + 'LSK': 'LSK', + 'DCR': 'DCR', + 'BTG': 'BTG', + 'STEEM': 'STEEM', + 'BTS': 'BTS', + 'ICX': 'ICX', + 'WAVES': 'WAVES', + 'CMT': 'CMT', + 'BTM': 'BTM', + 'VET': 'VET', + 'XZC': 'XZC', + 'ACT': 'ACT', + 'SMT': 'SMT', + 'BCD': 'BCD', + 'WAX': 'WAX1', + 'WICC': 'WICC', + 'ELF': 'ELF', + 'ZIL': 'ZIL', + 'ELA': 'ELA', + 'BCX': 'BCX', + 'SBTC': 'SBTC', + 'BIFI': 'BIFI', + 'CTXC': 'CTXC', + 'WAN': 'WAN', + 'POLYX': 'POLYX', + 'PAI': 'PAI', + 'WTC': 'WTC', + 'DGB': 'DGB', + 'XVG': 'XVG', + 'AAC': 'AAC', + 'AE': 'AE', + 'SEELE': 'SEELE', + 'BCV': 'BCV', + 'GRS': 'GRS', + 'ARDR': 'ARDR', + 'NANO': 'NANO', + 'ZEN': 'ZEN', + 'RBTC': 'RBTC', + 'BSV': 'BSV', + 'GAS': 'GAS', + 'XTZ': 'XTZ', + 'LAMB': 'LAMB', + 'CVNT1': 'CVNT1', + 'DOCK': 'DOCK', + 'SC': 'SC', + 'KMD': 'KMD', + 'ETN': 'ETN', + 'TOP': 'TOP', + 'IRIS': 'IRIS', + 'UGAS': 'UGAS', + 'TT': 'TT', + 'NEWTON': 'NEWTON', + 'VSYS': 'VSYS', + 'FSN': 'FSN', + 'BHD': 'BHD', + 'ONE': 'ONE', + 'EM': 'EM', + 'CKB': 'CKB', + 'EOSS': 'EOSS', + 'HIVE': 'HIVE', + 'RVN': 'RVN', + 'DOT': 'DOT', + 'KSM': 'KSM', + 'BAND': 'BAND', + 'OEP4': 'OEP4', + 'NBS': 'NBS', + 'FIS': 'FIS', + 'AR': 'AR', + 'HBAR': 'HBAR', + 'FIL': 'FIL', + 'MASS': 'MASS', + 'KAVA': 'KAVA', + 'XYM': 'XYM', + 'ENJ': 'ENJ', + 'CRUST': 'CRUST', + 'ICP': 'ICP', + 'CSPR': 'CSPR', + 'FLOW': 'FLOW', + 'IOTX': 'IOTX', + 'LAT': 'LAT', + 'APT': 'APT', + 'XCH': 'XCH', + 'MINA': 'MINA', + 'XEC': 'ECASH', + 'XPRT': 'XPRT', + 'CCA': 'ACA', + 'AOTI': 'COTI', + 'AKT': 'AKT', + 'ARS': 'ARS', + 'ASTR': 'ASTR', + 'AZERO': 'AZERO', + 'BLD': 'BLD', + 'BRISE': 'BRISE', + 'CORE': 'CORE', + 'DESO': 'DESO', + 'DFI': 'DFI', + 'EGLD': 'EGLD', + 'ERG': 'ERG', + 'ETHF': 'ETHFAIR', + 'ETHW': 'ETHW', + 'EVMOS': 'EVMOS', + 'FIO': 'FIO', + 'FLR': 'FLR', + 'FINSCHIA': 'FINSCHIA', + 'KMA': 'KMA', + 'KYVE': 'KYVE', + 'MEV': 'MEV', + 'MOVR': 'MOVR', + 'NODL': 'NODL', + 'OAS': 'OAS', + 'OSMO': 'OSMO', + 'PAYCOIN': 'PAYCOIN', + 'POKT': 'POKT', + 'PYG': 'PYG', + 'REI': 'REI', + 'SCRT': 'SCRT', + 'SDN': 'SDN', + 'SEI': 'SEI', + 'SGB': 'SGB', + 'SUI': 'SUI', + 'SXP': 'SOLAR', + 'SYS': 'SYS', + 'TENET': 'TENET', + 'TON': 'TON', + 'UNQ': 'UNQ', + 'UYU': 'UYU', + 'WEMIX': 'WEMIX', + 'XDC': 'XDC', + 'XPLA': 'XPLA', + # todo: below + # 'LUNC': 'LUNC', + # 'TERRA': 'TERRA', # tbd + # 'LUNA': 'LUNA', tbd + # 'FCT2': 'FCT2', + # FIL-0X ? + # 'COSMOS': 'ATOM1', + # 'ATOM': 'ATOM1', + # 'CRO': 'CRO', + # 'OP': ['OPTIMISM', 'OPTIMISMETH'] + # 'ARB': ['ARB', 'ARBITRUMETH'] + # 'CHZ': ['CHZ', 'CZH'], + # todo: AVAXCCHAIN CCHAIN AVAX + # 'ALGO': ['ALGO', 'ALGOUSDT'] + # 'ONT': ['ONT', 'ONTOLOGY'], + # 'BCC': 'BCC', BCH's somewhat chain + # 'DBC1': 'DBC1', + }, + # https://github.com/ccxt/ccxt/issues/5376 + 'fetchOrdersByStatesMethod': 'spot_private_get_v1_order_orders', # 'spot_private_get_v1_order_history' # https://github.com/ccxt/ccxt/pull/5392 + 'createMarketBuyOrderRequiresPrice': True, + 'language': 'en-US', + 'broker': { + 'id': 'AA03022abc', + }, + 'accountsByType': { + 'spot': 'pro', + 'funding': 'pro', + 'future': 'futures', + }, + 'accountsById': { + 'spot': 'spot', + 'margin': 'margin', + 'otc': 'otc', + 'point': 'point', + 'super-margin': 'super-margin', + 'investment': 'investment', + 'borrow': 'borrow', + 'grid-trading': 'grid-trading', + 'deposit-earning': 'deposit-earning', + 'otc-options': 'otc-options', + }, + 'typesByAccount': { + 'pro': 'spot', + 'futures': 'future', + }, + 'spot': { + 'stopOrderTypes': { + 'stop-limit': True, + 'buy-stop-limit': True, + 'sell-stop-limit': True, + 'stop-limit-fok': True, + 'buy-stop-limit-fok': True, + 'sell-stop-limit-fok': True, + }, + 'limitOrderTypes': { + 'limit': True, + 'buy-limit': True, + 'sell-limit': True, + 'ioc': True, + 'buy-ioc': True, + 'sell-ioc': True, + 'limit-maker': True, + 'buy-limit-maker': True, + 'sell-limit-maker': True, + 'stop-limit': True, + 'buy-stop-limit': True, + 'sell-stop-limit': True, + 'limit-fok': True, + 'buy-limit-fok': True, + 'sell-limit-fok': True, + 'stop-limit-fok': True, + 'buy-stop-limit-fok': True, + 'sell-stop-limit-fok': True, + }, + }, + }, + 'commonCurrencies': { + # https://github.com/ccxt/ccxt/issues/6081 + # https://github.com/ccxt/ccxt/issues/3365 + # https://github.com/ccxt/ccxt/issues/2873 + 'NGL': 'GFNGL', + 'GET': 'THEMIS', # conflict with GET(Guaranteed Entrance Token, GET Protocol) + 'GTC': 'GAMECOM', # conflict with Gitcoin and Gastrocoin + 'HIT': 'HITCHAIN', + # https://github.com/ccxt/ccxt/issues/7399 + # https://coinmarketcap.com/currencies/pnetwork/ + # https://coinmarketcap.com/currencies/penta/markets/ + # https://en.cryptonomist.ch/blog/eidoo/the-edo-to-pnt-upgrade-what-you-need-to-know-updated/ + 'PNT': 'PENTA', + 'SBTC': 'SUPERBITCOIN', + 'SOUL': 'SOULSAVER', + 'BIFI': 'BITCOINFILE', # conflict with Beefy.Finance https://github.com/ccxt/ccxt/issues/8706 + 'FUD': 'FTX Users Debt', + }, + 'features': { + 'spot': { + 'sandbox': True, + 'createOrder': { + 'marginMode': True, + 'triggerPrice': True, + 'triggerDirection': True, + 'triggerPriceType': None, + 'stopLossPrice': False, # todo: add support by triggerprice + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'iceberg': False, + 'selfTradePrevention': True, # todo implement + 'leverage': True, # todo implement + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': True, + }, + 'createOrders': { + 'max': 10, + }, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 500, + 'daysBack': 120, + 'untilDays': 2, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'trigger': True, + 'trailing': False, + 'limit': 500, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': False, + 'trigger': True, + 'trailing': False, + 'limit': 500, + 'untilDays': 2, + 'daysBack': 180, + 'symbolRequired': False, + }, + 'fetchClosedOrders': { + 'marginMode': False, + 'trigger': True, + 'trailing': False, + 'untilDays': 2, + 'limit': 500, + 'daysBack': 180, + 'daysBackCanceled': 1 / 12, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 1000, # 2000 for non-historical + }, + }, + 'forDerivatives': { + 'extends': 'spot', + 'createOrder': { + 'stopLossPrice': True, + 'takeProfitPrice': True, + 'trailing': True, + 'hedged': True, + # 'leverage': True, # todo + }, + 'createOrders': { + 'max': 25, + }, + 'fetchOrder': { + 'marginMode': True, + }, + 'fetchOpenOrders': { + 'marginMode': True, + 'trigger': False, + 'trailing': False, + 'limit': 50, + }, + 'fetchOrders': { + 'marginMode': True, + 'trigger': False, + 'trailing': False, + 'limit': 50, + 'daysBack': 90, + }, + 'fetchClosedOrders': { + 'marginMode': True, + 'trigger': False, + 'trailing': False, + 'untilDays': 2, + 'limit': 50, + 'daysBack': 90, + 'daysBackCanceled': 1 / 12, + }, + 'fetchOHLCV': { + 'limit': 2000, + }, + }, + 'swap': { + 'linear': { + 'extends': 'forDerivatives', + }, + 'inverse': { + 'extends': 'forDerivatives', + }, + }, + 'future': { + 'linear': { + 'extends': 'forDerivatives', + }, + 'inverse': { + 'extends': 'forDerivatives', + }, + }, + }, + }) + + def fetch_status(self, params={}): + """ + the latest known information on the availability of the exchange API + + https://huobiapi.github.io/docs/spot/v1/en/#get-system-status + https://huobiapi.github.io/docs/dm/v1/en/#get-system-status + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#get-system-status + https://huobiapi.github.io/docs/usdt_swap/v1/en/#get-system-status + https://huobiapi.github.io/docs/usdt_swap/v1/en/#query-whether-the-system-is-available # contractPublicGetHeartbeat + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `status structure ` + """ + self.load_markets() + marketType = None + marketType, params = self.handle_market_type_and_params('fetchStatus', None, params) + enabledForContracts = self.handle_option('fetchStatus', 'enableForContracts', False) # temp fix for: https://status-linear-swap.huobigroup.com/api/v2/summary.json + response = None + if marketType != 'spot' and enabledForContracts: + subType = self.safe_string(params, 'subType', self.options['defaultSubType']) + if marketType == 'swap': + if subType == 'linear': + response = self.statusPublicSwapLinearGetApiV2SummaryJson() + elif subType == 'inverse': + response = self.statusPublicSwapInverseGetApiV2SummaryJson() + elif marketType == 'future': + if subType == 'linear': + response = self.statusPublicFutureLinearGetApiV2SummaryJson() + elif subType == 'inverse': + response = self.statusPublicFutureInverseGetApiV2SummaryJson() + elif marketType == 'contract': + response = self.contractPublicGetHeartbeat() + elif marketType == 'spot': + response = self.statusPublicSpotGetApiV2SummaryJson() + # + # statusPublicSpotGetApiV2SummaryJson, statusPublicSwapInverseGetApiV2SummaryJson, statusPublicFutureLinearGetApiV2SummaryJson, statusPublicFutureInverseGetApiV2SummaryJson + # + # { + # "page": { + # "id":"mn7l2lw8pz4p", + # "name":"Huobi Futures-USDT-margined Swaps", + # "url":"https://status-linear-swap.huobigroup.com", + # "time_zone":"Asia/Singapore", + # "updated_at":"2022-04-29T12:47:21.319+08:00"}, + # "components": [ + # { + # "id":"lrv093qk3yp5", + # "name":"market data", + # "status":"operational", + # "created_at":"2020-10-29T14:08:59.427+08:00", + # "updated_at":"2020-10-29T14:08:59.427+08:00", + # "position":1,"description":null, + # "showcase":false, + # "start_date":null, + # "group_id":null, + # "page_id":"mn7l2lw8pz4p", + # "group":true, + # "only_show_if_degraded":false, + # "components": [ + # "82k5jxg7ltxd" # list of related components + # ] + # }, + # ], + # "incidents": [ # empty array if there are no issues + # { + # "id": "rclfxz2g21ly", # incident id + # "name": "Market data is delayed", # incident name + # "status": "investigating", # incident status + # "created_at": "2020-02-11T03:15:01.913Z", # incident create time + # "updated_at": "2020-02-11T03:15:02.003Z", # incident update time + # "monitoring_at": null, + # "resolved_at": null, + # "impact": "minor", # incident impact + # "shortlink": "http://stspg.io/pkvbwp8jppf9", + # "started_at": "2020-02-11T03:15:01.906Z", + # "page_id": "p0qjfl24znv5", + # "incident_updates": [ + # { + # "id": "dwfsk5ttyvtb", + # "status": "investigating", + # "body": "Market data is delayed", + # "incident_id": "rclfxz2g21ly", + # "created_at": "2020-02-11T03:15:02.000Z", + # "updated_at": "2020-02-11T03:15:02.000Z", + # "display_at": "2020-02-11T03:15:02.000Z", + # "affected_components": [ + # { + # "code": "nctwm9tghxh6", + # "name": "Market data", + # "old_status": "operational", + # "new_status": "degraded_performance" + # } + # ], + # "deliver_notifications": True, + # "custom_tweet": null, + # "tweet_id": null + # } + # ], + # "components": [ + # { + # "id": "nctwm9tghxh6", + # "name": "Market data", + # "status": "degraded_performance", + # "created_at": "2020-01-13T09:34:48.284Z", + # "updated_at": "2020-02-11T03:15:01.951Z", + # "position": 8, + # "description": null, + # "showcase": False, + # "group_id": null, + # "page_id": "p0qjfl24znv5", + # "group": False, + # "only_show_if_degraded": False + # } + # ] + # }, ... + # ], + # "scheduled_maintenances":[ # empty array if there are no scheduled maintenances + # { + # "id": "k7g299zl765l", # incident id + # "name": "Schedule maintenance", # incident name + # "status": "scheduled", # incident status + # "created_at": "2020-02-11T03:16:31.481Z", # incident create time + # "updated_at": "2020-02-11T03:16:31.530Z", # incident update time + # "monitoring_at": null, + # "resolved_at": null, + # "impact": "maintenance", # incident impact + # "shortlink": "http://stspg.io/md4t4ym7nytd", + # "started_at": "2020-02-11T03:16:31.474Z", + # "page_id": "p0qjfl24znv5", + # "incident_updates": [ + # { + # "id": "8whgr3rlbld8", + # "status": "scheduled", + # "body": "We will be undergoing scheduled maintenance during self time.", + # "incident_id": "k7g299zl765l", + # "created_at": "2020-02-11T03:16:31.527Z", + # "updated_at": "2020-02-11T03:16:31.527Z", + # "display_at": "2020-02-11T03:16:31.527Z", + # "affected_components": [ + # { + # "code": "h028tnzw1n5l", + # "name": "Deposit And Withdraw - Deposit", + # "old_status": "operational", + # "new_status": "operational" + # } + # ], + # "deliver_notifications": True, + # "custom_tweet": null, + # "tweet_id": null + # } + # ], + # "components": [ + # { + # "id": "h028tnzw1n5l", + # "name": "Deposit", + # "status": "operational", + # "created_at": "2019-12-05T02:07:12.372Z", + # "updated_at": "2020-02-10T12:34:52.970Z", + # "position": 1, + # "description": null, + # "showcase": False, + # "group_id": "gtd0nyr3pf0k", + # "page_id": "p0qjfl24znv5", + # "group": False, + # "only_show_if_degraded": False + # } + # ], + # "scheduled_for": "2020-02-15T00:00:00.000Z", # scheduled maintenance start time + # "scheduled_until": "2020-02-15T01:00:00.000Z" # scheduled maintenance end time + # } + # ], + # "status": { + # "indicator":"none", # none, minor, major, critical, maintenance + # "description":"all systems operational" # All Systems Operational, Minor Service Outage, Partial System Outage, Partially Degraded Service, Service Under Maintenance + # } + # } + # + # + # contractPublicGetHeartbeat + # + # { + # "status": "ok", # 'ok', 'error' + # "data": { + # "heartbeat": 1, # future 1: available, 0: maintenance with service suspended + # "estimated_recovery_time": null, # estimated recovery time in milliseconds + # "swap_heartbeat": 1, + # "swap_estimated_recovery_time": null, + # "option_heartbeat": 1, + # "option_estimated_recovery_time": null, + # "linear_swap_heartbeat": 1, + # "linear_swap_estimated_recovery_time": null + # }, + # "ts": 1557714418033 + # } + # + status = None + updated = None + url = None + if marketType == 'contract': + statusRaw = self.safe_string(response, 'status') + if statusRaw is None: + status = None + else: + status = 'ok' if (statusRaw == 'ok') else 'maintenance' # 'ok', 'error' + updated = self.safe_string(response, 'ts') + else: + statusData = self.safe_value(response, 'status', {}) + statusRaw = self.safe_string(statusData, 'indicator') + status = 'ok' if (statusRaw == 'none') else 'maintenance' # none, minor, major, critical, maintenance + pageData = self.safe_value(response, 'page', {}) + datetime = self.safe_string(pageData, 'updated_at') + updated = self.parse8601(datetime) + url = self.safe_string(pageData, 'url') + return { + 'status': status, + 'updated': updated, + 'eta': None, + 'url': url, + 'info': response, + } + + def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://huobiapi.github.io/docs/spot/v1/en/#get-current-timestamp + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#get-current-system-timestamp + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + options = self.safe_value(self.options, 'fetchTime', {}) + defaultType = self.safe_string(self.options, 'defaultType', 'spot') + type = self.safe_string(options, 'type', defaultType) + type = self.safe_string(params, 'type', type) + response = None + if (type == 'future') or (type == 'swap'): + response = self.contractPublicGetApiV1Timestamp(params) + else: + response = self.spotPublicGetV1CommonTimestamp(params) + # + # spot + # + # {"status":"ok","data":1637504261099} + # + # future, swap + # + # {"status":"ok","ts":1637504164707} + # + return self.safe_integer_2(response, 'data', 'ts') + + def parse_trading_fee(self, fee: dict, market: Market = None) -> TradingFeeInterface: + # + # { + # "symbol":"btcusdt", + # "actualMakerRate":"0.002", + # "actualTakerRate":"0.002", + # "takerFeeRate":"0.002", + # "makerFeeRate":"0.002" + # } + # + marketId = self.safe_string(fee, 'symbol') + return { + 'info': fee, + 'symbol': self.safe_symbol(marketId, market), + 'maker': self.safe_number(fee, 'actualMakerRate'), + 'taker': self.safe_number(fee, 'actualTakerRate'), + 'percentage': None, + 'tierBased': None, + } + + def fetch_trading_fee(self, symbol: str, params={}) -> TradingFeeInterface: + """ + fetch the trading fees for a market + + https://huobiapi.github.io/docs/spot/v1/en/#get-current-fee-rate-applied-to-the-user + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `fee structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbols': market['id'], # trading symbols comma-separated + } + response = self.spotPrivateGetV2ReferenceTransactFeeRate(self.extend(request, params)) + # + # { + # "code":200, + # "data":[ + # { + # "symbol":"btcusdt", + # "actualMakerRate":"0.002", + # "actualTakerRate":"0.002", + # "takerFeeRate":"0.002", + # "makerFeeRate":"0.002" + # } + # ], + # "success":true + # } + # + data = self.safe_value(response, 'data', []) + first = self.safe_value(data, 0, {}) + return self.parse_trading_fee(first, market) + + def fetch_trading_limits(self, symbols: Strings = None, params={}): + # self method should not be called directly, use loadTradingLimits() instead + # by default it will try load withdrawal fees of all currencies(with separate requests) + # however if you define symbols = ['ETH/BTC', 'LTC/BTC'] in args it will only load those + self.load_markets() + if symbols is None: + symbols = self.symbols + result: dict = {} + for i in range(0, len(symbols)): + symbol = symbols[i] + result[symbol] = self.fetch_trading_limits_by_id(self.market_id(symbol), params) + return result + + def fetch_trading_limits_by_id(self, id: str, params={}): + """ + @ignore + + https://huobiapi.github.io/docs/spot/v1/en/#get-current-fee-rate-applied-to-the-user + + :param str id: market id + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: the limits object of a market structure + """ + request: dict = { + 'symbol': id, + } + response = self.spotPublicGetV1CommonExchange(self.extend(request, params)) + # + # {status: "ok", + # "data": { symbol: "aidocbtc", + # "buy-limit-must-less-than": 1.1, + # "sell-limit-must-greater-than": 0.9, + # "limit-order-must-greater-than": 1, + # "limit-order-must-less-than": 5000000, + # "market-buy-order-must-greater-than": 0.0001, + # "market-buy-order-must-less-than": 100, + # "market-sell-order-must-greater-than": 1, + # "market-sell-order-must-less-than": 500000, + # "circuit-break-when-greater-than": 10000, + # "circuit-break-when-less-than": 10, + # "market-sell-order-rate-must-less-than": 0.1, + # "market-buy-order-rate-must-less-than": 0.1 }} + # + return self.parse_trading_limits(self.safe_value(response, 'data', {})) + + def parse_trading_limits(self, limits, symbol: Str = None, params={}): + # + # { "symbol": "aidocbtc", + # "buy-limit-must-less-than": 1.1, + # "sell-limit-must-greater-than": 0.9, + # "limit-order-must-greater-than": 1, + # "limit-order-must-less-than": 5000000, + # "market-buy-order-must-greater-than": 0.0001, + # "market-buy-order-must-less-than": 100, + # "market-sell-order-must-greater-than": 1, + # "market-sell-order-must-less-than": 500000, + # "circuit-break-when-greater-than": 10000, + # "circuit-break-when-less-than": 10, + # "market-sell-order-rate-must-less-than": 0.1, + # "market-buy-order-rate-must-less-than": 0.1 } + # + return { + 'info': limits, + 'limits': { + 'amount': { + 'min': self.safe_number(limits, 'limit-order-must-greater-than'), + 'max': self.safe_number(limits, 'limit-order-must-less-than'), + }, + }, + } + + def cost_to_precision(self, symbol, cost): + return self.decimal_to_precision(cost, TRUNCATE, self.markets[symbol]['precision']['cost'], self.precisionMode) + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for huobi + + https://huobiapi.github.io/docs/spot/v1/en/#get-all-supported-trading-symbol-v1-deprecated + https://huobiapi.github.io/docs/dm/v1/en/#get-contract-info + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#query-swap-info + https://huobiapi.github.io/docs/usdt_swap/v1/en/#general-query-swap-info + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + if self.options['adjustForTimeDifference']: + self.load_time_difference() + types = None + types, params = self.handle_option_and_params(params, 'fetchMarkets', 'types', {}) + allMarkets = [] + promises = [] + keys = list(types.keys()) + for i in range(0, len(keys)): + key = keys[i] + if self.safe_bool(types, key): + if key == 'spot': + promises.append(self.fetch_markets_by_type_and_sub_type('spot', None, params)) + elif key == 'linear': + promises.append(self.fetch_markets_by_type_and_sub_type(None, 'linear', params)) + elif key == 'inverse': + promises.append(self.fetch_markets_by_type_and_sub_type('swap', 'inverse', params)) + promises.append(self.fetch_markets_by_type_and_sub_type('future', 'inverse', params)) + promises = promises + for i in range(0, len(promises)): + allMarkets = self.array_concat(allMarkets, promises[i]) + return allMarkets + + def fetch_markets_by_type_and_sub_type(self, type: Str, subType: Str, params={}): + """ + @ignore + retrieves data on all markets of a certain type and/or subtype + + https://huobiapi.github.io/docs/spot/v1/en/#get-all-supported-trading-symbol-v1-deprecated + https://huobiapi.github.io/docs/dm/v1/en/#get-contract-info + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#query-swap-info + https://huobiapi.github.io/docs/usdt_swap/v1/en/#general-query-swap-info + + :param str [type]: 'spot', 'swap' or 'future' + :param str [subType]: 'linear' or 'inverse' + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + isSpot = (type == 'spot') + request: dict = {} + response = None + if not isSpot: + if subType == 'linear': + request['business_type'] = 'all' # override default to fetch all linear markets + response = self.contractPublicGetLinearSwapApiV1SwapContractInfo(self.extend(request, params)) + elif subType == 'inverse': + if type == 'future': + response = self.contractPublicGetApiV1ContractContractInfo(self.extend(request, params)) + elif type == 'swap': + response = self.contractPublicGetSwapApiV1SwapContractInfo(self.extend(request, params)) + else: + response = self.spotPublicGetV1CommonSymbols(self.extend(request, params)) + # + # spot + # + # { + # "status":"ok", + # "data":[ + # { + # "base-currency":"xrp3s", + # "quote-currency":"usdt", + # "price-precision":4, + # "amount-precision":4, + # "symbol-partition":"innovation", + # "symbol":"xrp3susdt", + # "state":"online", + # "value-precision":8, + # "min-order-amt":0.01, + # "max-order-amt":1616.4353, + # "min-order-value":5, + # "limit-order-min-order-amt":0.01, + # "limit-order-max-order-amt":1616.4353, + # "limit-order-max-buy-amt":1616.4353, + # "limit-order-max-sell-amt":1616.4353, + # "sell-market-min-order-amt":0.01, + # "sell-market-max-order-amt":1616.4353, + # "buy-market-max-order-value":2500, + # "max-order-value":2500, + # "underlying":"xrpusdt", + # "mgmt-fee-rate":0.035000000000000000, + # "charge-time":"23:55:00", + # "rebal-time":"00:00:00", + # "rebal-threshold":-5, + # "init-nav":10.000000000000000000, + # "api-trading":"enabled", + # "tags":"etp,nav,holdinglimit" + # }, + # ] + # } + # + # inverse(swap & future) + # + # { + # "status":"ok", + # "data":[ + # { + # "symbol":"BTC", + # "contract_code":"BTC211126", #/ BTC-USD in swap + # "contract_type":"self_week", # only in future + # "contract_size":100, + # "price_tick":0.1, + # "delivery_date":"20211126", # only in future + # "delivery_time":"1637913600000", # empty in swap + # "create_date":"20211112", + # "contract_status":1, + # "settlement_time":"1637481600000" # only in future + # "settlement_date":"16xxxxxxxxxxx" # only in swap + # }, + # ... + # ], + # "ts":1637474595140 + # } + # + # linear(swap & future) + # + # { + # "status":"ok", + # "data":[ + # { + # "symbol":"BTC", + # "contract_code":"BTC-USDT-211231", # or "BTC-USDT" in swap + # "contract_size":0.001, + # "price_tick":0.1, + # "delivery_date":"20211231", # empty in swap + # "delivery_time":"1640937600000", # empty in swap + # "create_date":"20211228", + # "contract_status":1, + # "settlement_date":"1640764800000", + # "support_margin_mode":"cross", # "all" or "cross" + # "business_type":"futures", # "swap" or "futures" + # "pair":"BTC-USDT", + # "contract_type":"self_week", # "swap", "self_week", "next_week", "quarter" + # "trade_partition":"USDT", + # } + # ], + # "ts":1640736207263 + # } + # + markets = self.safe_list(response, 'data', []) + numMarkets = len(markets) + if numMarkets < 1: + raise OperationFailed(self.id + ' fetchMarkets() returned an empty response: ' + self.json(response)) + result = [] + for i in range(0, len(markets)): + market = markets[i] + baseId = None + quoteId = None + settleId = None + id = None + lowercaseId = None + contract = ('contract_code' in market) + spot = not contract + swap = False + future = False + linear = None + inverse = None + # check if parsed market is contract + if contract: + id = self.safe_string(market, 'contract_code') + lowercaseId = id.lower() + delivery_date = self.safe_string(market, 'delivery_date') + business_type = self.safe_string(market, 'business_type') + future = delivery_date is not None + swap = not future + linear = business_type is not None + inverse = not linear + if swap: + type = 'swap' + parts = id.split('-') + baseId = self.safe_string_lower(market, 'symbol') + quoteId = self.safe_string_lower(parts, 1) + settleId = baseId if inverse else quoteId + elif future: + type = 'future' + baseId = self.safe_string_lower(market, 'symbol') + if inverse: + quoteId = 'USD' + settleId = baseId + else: + pair = self.safe_string(market, 'pair') + parts = pair.split('-') + quoteId = self.safe_string_lower(parts, 1) + settleId = quoteId + else: + type = 'spot' + baseId = self.safe_string(market, 'base-currency') + quoteId = self.safe_string(market, 'quote-currency') + id = baseId + quoteId + lowercaseId = id.lower() + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + settle = self.safe_currency_code(settleId) + symbol = base + '/' + quote + expiry = None + if contract: + if inverse: + symbol += ':' + base + elif linear: + symbol += ':' + quote + if future: + expiry = self.safe_integer(market, 'delivery_time') + symbol += '-' + self.yymmdd(expiry) + contractSize = self.safe_number(market, 'contract_size') + minCost = self.safe_number(market, 'min-order-value') + maxAmount = self.safe_number(market, 'max-order-amt') + minAmount = self.safe_number(market, 'min-order-amt') + if contract: + if linear: + minAmount = contractSize + elif inverse: + minCost = contractSize + pricePrecision = None + amountPrecision = None + costPrecision = None + maker = None + taker = None + active = None + if spot: + pricePrecision = self.parse_number(self.parse_precision(self.safe_string(market, 'price-precision'))) + amountPrecision = self.parse_number(self.parse_precision(self.safe_string(market, 'amount-precision'))) + costPrecision = self.parse_number(self.parse_precision(self.safe_string(market, 'value-precision'))) + maker = self.parse_number('0.002') + taker = self.parse_number('0.002') + state = self.safe_string(market, 'state') + active = (state == 'online') + else: + pricePrecision = self.safe_number(market, 'price_tick') + amountPrecision = self.parse_number('1') # other markets have step size of 1 contract + maker = self.parse_number('0.0002') + taker = self.parse_number('0.0005') + contractStatus = self.safe_integer(market, 'contract_status') + active = (contractStatus == 1) + leverageRatio = self.safe_string(market, 'leverage-ratio', '1') + superLeverageRatio = self.safe_string(market, 'super-margin-leverage-ratio', '1') + hasLeverage = Precise.string_gt(leverageRatio, '1') or Precise.string_gt(superLeverageRatio, '1') + # 0 Delisting + # 1 Listing + # 2 Pending Listing + # 3 Suspension + # 4 Suspending of Listing + # 5 In Settlement + # 6 Delivering + # 7 Settlement Completed + # 8 Delivered + # 9 Suspending of Trade + created = None + createdDate = self.safe_string(market, 'create_date') # i.e 20230101 + if createdDate is not None: + createdArray = self.string_to_chars_array(createdDate) + createdDate = createdArray[0] + createdArray[1] + createdArray[2] + createdArray[3] + '-' + createdArray[4] + createdArray[5] + '-' + createdArray[6] + createdArray[7] + ' 00:00:00' + created = self.parse8601(createdDate) + result.append({ + 'id': id, + 'lowercaseId': lowercaseId, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': type, + 'spot': spot, + 'margin': (spot and hasLeverage), + 'swap': swap, + 'future': future, + 'option': False, + 'active': active, + 'contract': contract, + 'linear': linear, + 'inverse': inverse, + 'taker': taker, + 'maker': maker, + 'contractSize': contractSize, + 'expiry': expiry, + 'expiryDatetime': self.iso8601(expiry), + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': amountPrecision, + 'price': pricePrecision, + 'cost': costPrecision, + }, + 'limits': { + 'leverage': { + 'min': self.parse_number('1'), + 'max': self.parse_number(leverageRatio), + 'superMax': self.parse_number(superLeverageRatio), + }, + 'amount': { + 'min': minAmount, + 'max': maxAmount, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': minCost, + 'max': None, + }, + }, + 'created': created, + 'info': market, + }) + return result + + def try_get_symbol_from_future_markets(self, symbolOrMarketId: str): + if symbolOrMarketId in self.markets: + return symbolOrMarketId + # only on "future" market type(inverse & linear), market-id differs between "fetchMarkets" and "fetchTicker" + # so we have to create a mapping + # - market-id from fetchMarkts: `BTC-USDT-240419`(linear future) or `BTC240412`(inverse future) + # - market-id from fetchTciker[s]: `BTC-USDT-CW` (linear future) or `BTC_CW` (inverse future) + if not ('futureMarketIdsForSymbols' in self.options): + self.options['futureMarketIdsForSymbols'] = {} + futureMarketIdsForSymbols = self.safe_dict(self.options, 'futureMarketIdsForSymbols', {}) + if symbolOrMarketId in futureMarketIdsForSymbols: + return futureMarketIdsForSymbols[symbolOrMarketId] + futureMarkets = self.filter_by(self.markets, 'future', True) + futuresCharsMaps: dict = { + 'this_week': 'CW', + 'next_week': 'NW', + 'quarter': 'CQ', + 'next_quarter': 'NQ', + } + for i in range(0, len(futureMarkets)): + market = futureMarkets[i] + info = self.safe_value(market, 'info', {}) + contractType = self.safe_string(info, 'contract_type') + contractSuffix = futuresCharsMaps[contractType] + # see comment on formats a bit above + constructedId = market['base'] + '-' + market['quote'] + '-' + contractSuffix if market['linear'] else market['base'] + '_' + contractSuffix + if constructedId == symbolOrMarketId: + symbol = market['symbol'] + self.options['futureMarketIdsForSymbols'][symbolOrMarketId] = symbol + return symbol + # if not found, just save it to avoid unnecessary future iterations + self.options['futureMarketIdsForSymbols'][symbolOrMarketId] = symbolOrMarketId + return symbolOrMarketId + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # fetchTicker + # + # { + # "amount": 26228.672978342216, + # "open": 9078.95, + # "close": 9146.86, + # "high": 9155.41, + # "id": 209988544334, + # "count": 265846, + # "low": 8988.0, + # "version": 209988544334, + # "ask": [9146.87, 0.156134], + # "vol": 2.3822168242201668E8, + # "bid": [9146.86, 0.080758], + # } + # + # fetchTickers + # + # { + # "symbol": "bhdht", + # "open": 2.3938, + # "high": 2.4151, + # "low": 2.3323, + # "close": 2.3909, + # "amount": 628.992, + # "vol": 1493.71841095, + # "count": 2088, + # "bid": 2.3643, + # "bidSize": 0.7136, + # "ask": 2.4061, + # "askSize": 0.4156 + # } + # + # watchTikcer - bbo + # { + # "seqId": 161499562790, + # "ask": 16829.51, + # "askSize": 0.707776, + # "bid": 16829.5, + # "bidSize": 1.685945, + # "quoteTime": 1671941599612, + # "symbol": "btcusdt" + # } + # + marketId = self.safe_string_2(ticker, 'symbol', 'contract_code') + symbol = self.safe_symbol(marketId, market) + symbol = self.try_get_symbol_from_future_markets(symbol) + timestamp = self.safe_integer_2(ticker, 'ts', 'quoteTime') + bid = None + bidVolume = None + ask = None + askVolume = None + if 'bid' in ticker: + if ticker['bid'] is not None and isinstance(ticker['bid'], list): + bid = self.safe_string(ticker['bid'], 0) + bidVolume = self.safe_string(ticker['bid'], 1) + else: + bid = self.safe_string(ticker, 'bid') + bidVolume = self.safe_string(ticker, 'bidSize') + if 'ask' in ticker: + if ticker['ask'] is not None and isinstance(ticker['ask'], list): + ask = self.safe_string(ticker['ask'], 0) + askVolume = self.safe_string(ticker['ask'], 1) + else: + ask = self.safe_string(ticker, 'ask') + askVolume = self.safe_string(ticker, 'askSize') + open = self.safe_string(ticker, 'open') + close = self.safe_string(ticker, 'close') + baseVolume = self.safe_string(ticker, 'amount') + quoteVolume = self.safe_string(ticker, 'vol') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': bid, + 'bidVolume': bidVolume, + 'ask': ask, + 'askVolume': askVolume, + 'vwap': None, + 'open': open, + 'close': close, + 'last': close, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'info': ticker, + }, market) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://huobiapi.github.io/docs/spot/v1/en/#get-latest-aggregated-ticker + https://huobiapi.github.io/docs/dm/v1/en/#get-market-data-overview + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#get-market-data-overview + https://huobiapi.github.io/docs/usdt_swap/v1/en/#general-get-market-data-overview + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = {} + response = None + if market['linear']: + request['contract_code'] = market['id'] + response = self.contractPublicGetLinearSwapExMarketDetailMerged(self.extend(request, params)) + elif market['inverse']: + if market['future']: + request['symbol'] = market['id'] + response = self.contractPublicGetMarketDetailMerged(self.extend(request, params)) + elif market['swap']: + request['contract_code'] = market['id'] + response = self.contractPublicGetSwapExMarketDetailMerged(self.extend(request, params)) + else: + request['symbol'] = market['id'] + response = self.spotPublicGetMarketDetailMerged(self.extend(request, params)) + # + # spot + # + # { + # "status": "ok", + # "ch": "market.btcusdt.detail.merged", + # "ts": 1583494336669, + # "tick": { + # "amount": 26228.672978342216, + # "open": 9078.95, + # "close": 9146.86, + # "high": 9155.41, + # "id": 209988544334, + # "count": 265846, + # "low": 8988.0, + # "version": 209988544334, + # "ask": [9146.87, 0.156134], + # "vol": 2.3822168242201668E8, + # "bid": [9146.86, 0.080758], + # } + # } + # + # future, swap + # + # { + # "ch":"market.BTC211126.detail.merged", + # "status":"ok", + # "tick":{ + # "amount":"669.3385682049668320322569544150680718474", + # "ask":[59117.44,48], + # "bid":[59082,48], + # "close":"59087.97", + # "count":5947, + # "high":"59892.62", + # "id":1637502670, + # "low":"57402.87", + # "open":"57638", + # "ts":1637502670059, + # "vol":"394598" + # }, + # "ts":1637502670059 + # } + # + tick = self.safe_value(response, 'tick', {}) + ticker = self.parse_ticker(tick, market) + timestamp = self.safe_integer(response, 'ts') + ticker['timestamp'] = timestamp + ticker['datetime'] = self.iso8601(timestamp) + return ticker + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://huobiapi.github.io/docs/spot/v1/en/#get-latest-tickers-for-all-pairs + https://huobiapi.github.io/docs/usdt_swap/v1/en/#general-get-a-batch-of-market-data-overview + https://huobiapi.github.io/docs/dm/v1/en/#get-a-batch-of-market-data-overview + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#get-a-batch-of-market-data-overview-v2 + + :param str[] [symbols]: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + symbols = self.market_symbols(symbols) + first = self.safe_string(symbols, 0) + market = None + if first is not None: + market = self.market(first) + isSubTypeRequested = ('subType' in params) or ('business_type' in params) + type = None + subType = None + type, params = self.handle_market_type_and_params('fetchTickers', market, params) + subType, params = self.handle_sub_type_and_params('fetchTickers', market, params) + request: dict = {} + isSpot = (type == 'spot') + future = (type == 'future') + swap = (type == 'swap') + linear = (subType == 'linear') + inverse = (subType == 'inverse') + response = None + if not isSpot or isSubTypeRequested: + if linear: + # independently of type, supports calling all linear symbols i.e. fetchTickers(None, {subType:'linear'}) + if future: + request['business_type'] = 'futures' + elif swap: + request['business_type'] = 'swap' + else: + request['business_type'] = 'all' + response = self.contractPublicGetLinearSwapExMarketDetailBatchMerged(self.extend(request, params)) + elif inverse: + if future: + response = self.contractPublicGetMarketDetailBatchMerged(self.extend(request, params)) + elif swap: + response = self.contractPublicGetSwapExMarketDetailBatchMerged(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchTickers() you have to set params["type"] to either "swap" or "future" for inverse contracts') + else: + raise NotSupported(self.id + ' fetchTickers() you have to set params["subType"] to either "linear" or "inverse" for contracts') + else: + response = self.spotPublicGetMarketTickers(self.extend(request, params)) + # + # spot + # + # { + # "data":[ + # { + # "symbol":"hbcbtc", + # "open":5.313E-5, + # "high":5.34E-5, + # "low":5.112E-5, + # "close":5.175E-5, + # "amount":1183.87, + # "vol":0.0618599229, + # "count":205, + # "bid":5.126E-5, + # "bidSize":5.25, + # "ask":5.214E-5, + # "askSize":150.0 + # }, + # ], + # "status":"ok", + # "ts":1639547261293 + # } + # + # linear swap, linear future, inverse swap, inverse future + # + # { + # "status":"ok", + # "ticks":[ + # { + # "id":1637504679, + # "ts":1637504679372, + # "ask":[0.10644,100], + # "bid":[0.10624,26], + # "symbol":"TRX_CW", + # "open":"0.10233", + # "close":"0.10644", + # "low":"0.1017", + # "high":"0.10725", + # "amount":"2340267.415144052378486261756692535687481566", + # "count":882, + # "vol":"24706", + # "trade_turnover":"840726.5048", # only in linear futures + # "business_type":"futures", # only in linear futures + # "contract_code":"BTC-USDT-CW", # only in linear futures, instead of 'symbol' + # } + # ], + # "ts":1637504679376 + # } + # + rawTickers = self.safe_list_2(response, 'data', 'ticks', []) + tickers = self.parse_tickers(rawTickers, symbols, params) + return self.filter_by_array_tickers(tickers, 'symbol', symbols) + + def fetch_last_prices(self, symbols: Strings = None, params={}): + """ + fetches the last price for multiple markets + + https://www.htx.com/en-us/opend/newApiPages/?id=8cb81024-77b5-11ed-9966-0242ac110003 linear swap & linear future + https://www.htx.com/en-us/opend/newApiPages/?id=28c2e8fc-77ae-11ed-9966-0242ac110003 inverse future + https://www.htx.com/en-us/opend/newApiPages/?id=5d517ef5-77b6-11ed-9966-0242ac110003 inverse swap + + :param str[] [symbols]: unified symbols of the markets to fetch the last prices + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of lastprices structures + """ + self.load_markets() + symbols = self.market_symbols(symbols) + market = self.get_market_from_symbols(symbols) + type = None + subType = None + subType, params = self.handle_sub_type_and_params('fetchLastPrices', market, params) + type, params = self.handle_market_type_and_params('fetchLastPrices', market, params) + response = None + if ((type == 'swap') or (type == 'future')) and (subType == 'linear'): + response = self.contractPublicGetLinearSwapExMarketTrade(params) + # + # { + # "ch": "market.*.trade.detail", + # "status": "ok", + # "tick": { + # "data": [ + # { + # "amount": "4", + # "quantity": "40", + # "trade_turnover": "22.176", + # "ts": 1703697705028, + # "id": 1000003558478170000, + # "price": "0.5544", + # "direction": "buy", + # "contract_code": "MANA-USDT", + # "business_type": "swap", + # "trade_partition": "USDT" + # }, + # ], + # "id": 1703697740147, + # "ts": 1703697740147 + # }, + # "ts": 1703697740147 + # } + # + elif (type == 'swap') and (subType == 'inverse'): + response = self.contractPublicGetSwapExMarketTrade(params) + # + # { + # "ch": "market.*.trade.detail", + # "status": "ok", + # "tick": { + # "data": [ + # { + # "amount": "6", + # "quantity": "94.5000945000945000945000945000945000945", + # "ts": 1703698704594, + # "id": 1000001187811060000, + # "price": "0.63492", + # "direction": "buy", + # "contract_code": "XRP-USD" + # }, + # ], + # "id": 1703698706589, + # "ts": 1703698706589 + # }, + # "ts": 1703698706589 + # } + # + elif (type == 'future') and (subType == 'inverse'): + response = self.contractPublicGetMarketTrade(params) + # + # { + # "ch": "market.*.trade.detail", + # "status": "ok", + # "tick": { + # "data": [ + # { + # "amount": "20", + # "quantity": "44.4444444444444444444444444444444444444", + # "ts": 1686134498885, + # "id": 2323000000174820000, + # "price": "4.5", + # "direction": "sell", + # "symbol": "DORA_CW" + # }, + # ], + # "id": 1703698855142, + # "ts": 1703698855142 + # }, + # "ts": 1703698855142 + # } + # + else: + raise NotSupported(self.id + ' fetchLastPrices() does not support ' + type + ' markets yet') + tick = self.safe_value(response, 'tick', {}) + data = self.safe_list(tick, 'data', []) + return self.parse_last_prices(data, symbols) + + def parse_last_price(self, entry, market: Market = None): + # example responses are documented in fetchLastPrices + marketId = self.safe_string_2(entry, 'symbol', 'contract_code') + market = self.safe_market(marketId, market) + price = self.safe_number(entry, 'price') + direction = self.safe_string(entry, 'direction') # "buy" or "sell" + # group timestamp should not be assigned to the individual trades' times + return { + 'symbol': market['symbol'], + 'timestamp': None, + 'datetime': None, + 'price': price, + 'side': direction, + 'info': entry, + } + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://huobiapi.github.io/docs/spot/v1/en/#get-market-depth + https://huobiapi.github.io/docs/dm/v1/en/#get-market-depth + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#get-market-depth + https://huobiapi.github.io/docs/usdt_swap/v1/en/#general-get-market-depth + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + # + # from the API docs + # + # to get depth data within step 150, use step0, step1, step2, step3, step4, step5, step14, step15(merged depth data 0-5,14-15, when step is 0,depth data will not be merged + # to get depth data within step 20, use step6, step7, step8, step9, step10, step11, step12, step13(merged depth data 7-13), when step is 6, depth data will not be merged + # + 'type': 'step0', + # 'symbol': market['id'], # spot, future + # 'contract_code': market['id'], # swap + } + response = None + if market['linear']: + request['contract_code'] = market['id'] + response = self.contractPublicGetLinearSwapExMarketDepth(self.extend(request, params)) + elif market['inverse']: + if market['future']: + request['symbol'] = market['id'] + response = self.contractPublicGetMarketDepth(self.extend(request, params)) + elif market['swap']: + request['contract_code'] = market['id'] + response = self.contractPublicGetSwapExMarketDepth(self.extend(request, params)) + else: + if limit is not None: + # Valid depths are 5, 10, 20 or empty https://huobiapi.github.io/docs/spot/v1/en/#get-market-depth + if (limit != 5) and (limit != 10) and (limit != 20) and (limit != 150): + raise BadRequest(self.id + ' fetchOrderBook() limit argument must be None, 5, 10, 20, or 150, default is 150') + # only set the depth if it is not 150 + # 150 is the implicit default on the exchange side for step0 and no orderbook aggregation + # it is not accepted by the exchange if you set it explicitly + if limit != 150: + request['depth'] = limit + request['symbol'] = market['id'] + response = self.spotPublicGetMarketDepth(self.extend(request, params)) + # + # spot, future, swap + # + # { + # "status": "ok", + # "ch": "market.btcusdt.depth.step0", + # "ts": 1583474832790, + # "tick": { + # "bids": [ + # [9100.290000000000000000, 0.200000000000000000], + # [9099.820000000000000000, 0.200000000000000000], + # [9099.610000000000000000, 0.205000000000000000], + # ], + # "asks": [ + # [9100.640000000000000000, 0.005904000000000000], + # [9101.010000000000000000, 0.287311000000000000], + # [9101.030000000000000000, 0.012121000000000000], + # ], + # "ch":"market.BTC-USD.depth.step0", + # "ts":1583474832008, + # "id":1637554816, + # "mrid":121654491624, + # "version":104999698781 + # } + # } + # + if 'tick' in response: + if not response['tick']: + raise BadSymbol(self.id + ' fetchOrderBook() returned empty response: ' + self.json(response)) + tick = self.safe_value(response, 'tick') + timestamp = self.safe_integer(tick, 'ts', self.safe_integer(response, 'ts')) + result = self.parse_order_book(tick, symbol, timestamp) + result['nonce'] = self.safe_integer(tick, 'version') + return result + raise ExchangeError(self.id + ' fetchOrderBook() returned unrecognized response: ' + self.json(response)) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # spot fetchTrades(public) + # + # { + # "amount": 0.010411000000000000, + # "trade-id": 102090736910, + # "ts": 1583497692182, + # "id": 10500517034273194594947, + # "price": 9096.050000000000000000, + # "direction": "sell" + # } + # + # spot fetchMyTrades(private) + # + # { + # "symbol": "swftcbtc", + # "fee-currency": "swftc", + # "filled-fees": "0", + # "source": "spot-api", + # "id": 83789509854000, + # "type": "buy-limit", + # "order-id": 83711103204909, + # 'filled-points': "0.005826843283532154", + # "fee-deduct-currency": "ht", + # 'filled-amount': "45941.53", + # "price": "0.0000001401", + # "created-at": 1597933260729, + # "match-id": 100087455560, + # "role": "maker", + # "trade-id": 100050305348 + # } + # + # linear swap isolated margin fetchOrder details + # + # { + # "trade_id": 131560927, + # "trade_price": 13059.800000000000000000, + # "trade_volume": 1.000000000000000000, + # "trade_turnover": 13.059800000000000000, + # "trade_fee": -0.005223920000000000, + # "created_at": 1603703614715, + # "role": "taker", + # "fee_asset": "USDT", + # "profit": 0, + # "real_profit": 0, + # "id": "131560927-770334322963152896-1" + # } + # + # inverse swap cross margin fetchMyTrades + # + # { + # "contract_type":"swap", + # "pair":"O3-USDT", + # "business_type":"swap", + # "query_id":652123190, + # "match_id":28306009409, + # "order_id":941137865226903553, + # "symbol":"O3", + # "contract_code":"O3-USDT", + # "direction":"sell", + # "offset":"open", + # "trade_volume":100.000000000000000000, + # "trade_price":0.398500000000000000, + # "trade_turnover":39.850000000000000000, + # "trade_fee":-0.007970000000000000, + # "offset_profitloss":0E-18, + # "create_date":1644426352999, + # "role":"Maker", + # "order_source":"api", + # "order_id_str":"941137865226903553", + # "id":"28306009409-941137865226903553-1", + # "fee_asset":"USDT", + # "margin_mode":"cross", + # "margin_account":"USDT", + # "real_profit":0E-18, + # "trade_partition":"USDT" + # } + # + marketId = self.safe_string_2(trade, 'contract_code', 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + timestamp = self.safe_integer_2(trade, 'ts', 'created-at') + timestamp = self.safe_integer_2(trade, 'created_at', 'create_date', timestamp) + order = self.safe_string_2(trade, 'order-id', 'order_id') + side = self.safe_string(trade, 'direction') + type = self.safe_string(trade, 'type') + if type is not None: + typeParts = type.split('-') + side = typeParts[0] + type = typeParts[1] + takerOrMaker = self.safe_string_lower(trade, 'role') + priceString = self.safe_string_2(trade, 'price', 'trade_price') + amountString = self.safe_string_2(trade, 'filled-amount', 'amount') + amountString = self.safe_string(trade, 'trade_volume', amountString) + costString = self.safe_string(trade, 'trade_turnover') + fee = None + feeCost = self.safe_string(trade, 'filled-fees') + if feeCost is None: + feeCost = Precise.string_neg(self.safe_string(trade, 'trade_fee')) + feeCurrencyId = self.safe_string_2(trade, 'fee-currency', 'fee_asset') + feeCurrency = self.safe_currency_code(feeCurrencyId) + filledPoints = self.safe_string(trade, 'filled-points') + if filledPoints is not None: + if (feeCost is None) or Precise.string_equals(feeCost, '0'): + feeDeductCurrency = self.safe_string(trade, 'fee-deduct-currency') + if feeDeductCurrency is not None: + feeCost = filledPoints + feeCurrency = self.safe_currency_code(feeDeductCurrency) + if feeCost is not None: + fee = { + 'cost': feeCost, + 'currency': feeCurrency, + } + # htx's multi-market trade-id is a bit complex to parse accordingly. + # - for `id` which contains hyphen, it would be the unique id, eg. xxxxxx-1, xxxxxx-2(self happens mostly for contract markets) + # - otherwise the least priority is given to the `id` key + id: Str = None + safeId = self.safe_string(trade, 'id') + if safeId is not None and safeId.find('-') >= 0: + id = safeId + else: + id = self.safe_string_n(trade, ['trade_id', 'trade-id', 'id']) + return self.safe_trade({ + 'id': id, + 'info': trade, + 'order': order, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'type': type, + 'side': side, + 'takerOrMaker': takerOrMaker, + 'price': priceString, + 'amount': amountString, + 'cost': costString, + 'fee': fee, + }, market) + + def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all the trades made from a single order + + https://huobiapi.github.io/docs/spot/v1/en/#get-the-match-result-of-an-order + + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + market = None + if symbol is not None: + market = self.market(symbol) + marketType = None + marketType, params = self.handle_market_type_and_params('fetchOrderTrades', market, params) + if marketType != 'spot': + raise NotSupported(self.id + ' fetchOrderTrades() is only supported for spot markets') + return self.fetch_spot_order_trades(id, symbol, since, limit, params) + + def fetch_spot_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + @ignore + fetch all the trades made from a single order + + https://huobiapi.github.io/docs/spot/v1/en/#get-the-match-result-of-an-order + + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + self.load_markets() + request: dict = { + 'order-id': id, + } + response = self.spotPrivateGetV1OrderOrdersOrderIdMatchresults(self.extend(request, params)) + return self.parse_trades(response['data'], None, since, limit) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + + https://huobiapi.github.io/docs/usdt_swap/v1/en/#isolated-get-history-match-results-via-multiple-fields-new + https://huobiapi.github.io/docs/usdt_swap/v1/en/#cross-get-history-match-results-via-multiple-fields-new + https://huobiapi.github.io/docs/spot/v1/en/#search-match-results + + fetch all trades made by the user + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch trades for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchMyTrades', 'paginate') + if paginate: + return self.fetch_paginated_call_dynamic('fetchMyTrades', symbol, since, limit, params) + market = None + if symbol is not None: + market = self.market(symbol) + marketType = None + marketType, params = self.handle_market_type_and_params('fetchMyTrades', market, params) + request: dict = { + # spot ----------------------------------------------------------- + # 'symbol': market['id'], + # 'types': 'buy-market,sell-market,buy-limit,sell-limit,buy-ioc,sell-ioc,buy-limit-maker,sell-limit-maker,buy-stop-limit,sell-stop-limit', + # 'start-time': since, # max 48 hours within 120 days + # 'end-time': self.milliseconds(), # max 48 hours within 120 days + # 'from': 'id', # tring False N/A Search internal id to begin with if search next page, then self should be the last id(not trade-id) of last page; if search previous page, then self should be the first id(not trade-id) of last page + # 'direct': 'next', # next, prev + # 'size': limit, # default 100, max 500 The number of orders to return [1-500] + # contracts ------------------------------------------------------ + # 'symbol': market['settleId'], # required + # 'trade_type': 0, # required, 0 all, 1 open long, 2 open short, 3 close short, 4 close long, 5 liquidate long positions, 6 liquidate short positions + # 'contract_code': market['id'], + # 'start_time': since, # max 48 hours within 120 days + # 'end_time': self.milliseconds(), # max 48 hours within 120 days + # 'from_id': 'id', # tring False N/A Search internal id to begin with if search next page, then self should be the last id(not trade-id) of last page; if search previous page, then self should be the first id(not trade-id) of last page + # 'direct': 'prev', # next, prev + # 'size': limit, # default 20, max 50 + } + response = None + if marketType == 'spot': + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if limit is not None: + request['size'] = limit # default 100, max 500 + if since is not None: + request['start-time'] = since # a date within 120 days from today + # request['end-time'] = self.sum(since, 172800000) # 48 hours window + request, params = self.handle_until_option('end-time', request, params) + response = self.spotPrivateGetV1OrderMatchresults(self.extend(request, params)) + else: + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMyTrades() requires a symbol argument') + request['contract'] = market['id'] + request['trade_type'] = 0 # 0 all, 1 open long, 2 open short, 3 close short, 4 close long, 5 liquidate long positions, 6 liquidate short positions + if since is not None: + request['start_time'] = since # a date within 120 days from today + # request['end_time'] = self.sum(request['start_time'], 172800000) # 48 hours window + request, params = self.handle_until_option('end_time', request, params) + if limit is not None: + request['page_size'] = limit # default 100, max 500 + if market['linear']: + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchMyTrades', params) + marginMode = 'cross' if (marginMode is None) else marginMode + if marginMode == 'isolated': + response = self.contractPrivatePostLinearSwapApiV3SwapMatchresultsExact(self.extend(request, params)) + elif marginMode == 'cross': + response = self.contractPrivatePostLinearSwapApiV3SwapCrossMatchresultsExact(self.extend(request, params)) + elif market['inverse']: + if marketType == 'future': + request['symbol'] = market['settleId'] + response = self.contractPrivatePostApiV3ContractMatchresultsExact(self.extend(request, params)) + elif marketType == 'swap': + response = self.contractPrivatePostSwapApiV3SwapMatchresultsExact(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchMyTrades() does not support ' + marketType + ' markets') + # + # spot + # + # { + # "status": "ok", + # "data": [ + # { + # "symbol": "polyusdt", + # "fee-currency": "poly", + # "source": "spot-web", + # "price": "0.338", + # "created-at": 1629443051839, + # "role": "taker", + # "order-id": 345487249132375, + # "match-id": 5014, + # "trade-id": 1085, + # "filled-amount": "147.928994082840236", + # "filled-fees": "0", + # "filled-points": "0.1", + # "fee-deduct-currency": "hbpoint", + # "fee-deduct-state": "done", + # "id": 313288753120940, + # "type": "buy-market" + # } + # ] + # } + # + # contracts + # + # { + # "status": "ok", + # "data": { + # "trades": [ + # { + # "query_id": 2424420723, + # "match_id": 113891764710, + # "order_id": 773135295142658048, + # "symbol": "ADA", + # "contract_type": "quarter", # swap + # "business_type": "futures", # swap + # "contract_code": "ADA201225", + # "direction": "buy", + # "offset": "open", + # "trade_volume": 1, + # "trade_price": 0.092, + # "trade_turnover": 10, + # "trade_fee": -0.021739130434782608, + # "offset_profitloss": 0, + # "create_date": 1604371703183, + # "role": "Maker", + # "order_source": "web", + # "order_id_str": "773135295142658048", + # "fee_asset": "ADA", + # "margin_mode": "isolated", # cross + # "margin_account": "BTC-USDT", + # "real_profit": 0, + # "id": "113891764710-773135295142658048-1", + # "trade_partition":"USDT", + # } + # ], + # "remain_size": 15, + # "next_id": 2424413094 + # }, + # "ts": 1604372202243 + # } + # + trades = self.safe_value(response, 'data') + if not isinstance(trades, list): + trades = self.safe_value(trades, 'trades') + return self.parse_trades(trades, market, since, limit) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = 1000, params={}) -> List[Trade]: + """ + + https://huobiapi.github.io/docs/spot/v1/en/#get-the-most-recent-trades + https://huobiapi.github.io/docs/dm/v1/en/#query-a-batch-of-trade-records-of-a-contract + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#query-a-batch-of-trade-records-of-a-contract + https://huobiapi.github.io/docs/usdt_swap/v1/en/#general-query-a-batch-of-trade-records-of-a-contract + + get the list of most recent trades for a particular symbol + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + # 'symbol': market['id'], # spot, future + # 'contract_code': market['id'], # swap + } + if limit is not None: + request['size'] = min(limit, 2000) # max 2000 + response = None + if market['future']: + if market['inverse']: + request['symbol'] = market['id'] + response = self.contractPublicGetMarketHistoryTrade(self.extend(request, params)) + elif market['linear']: + request['contract_code'] = market['id'] + response = self.contractPublicGetLinearSwapExMarketHistoryTrade(self.extend(request, params)) + elif market['swap']: + request['contract_code'] = market['id'] + if market['inverse']: + response = self.contractPublicGetSwapExMarketHistoryTrade(self.extend(request, params)) + elif market['linear']: + response = self.contractPublicGetLinearSwapExMarketHistoryTrade(self.extend(request, params)) + else: + request['symbol'] = market['id'] + response = self.spotPublicGetMarketHistoryTrade(self.extend(request, params)) + # + # { + # "status": "ok", + # "ch": "market.btcusdt.trade.detail", + # "ts": 1583497692365, + # "data": [ + # { + # "id": 105005170342, + # "ts": 1583497692182, + # "data": [ + # { + # "amount": 0.010411000000000000, + # "trade-id": 102090736910, + # "ts": 1583497692182, + # "id": 10500517034273194594947, + # "price": 9096.050000000000000000, + # "direction": "sell" + # } + # ] + # }, + # # ... + # ] + # } + # + data = self.safe_value(response, 'data', []) + result = [] + for i in range(0, len(data)): + trades = self.safe_value(data[i], 'data', []) + for j in range(0, len(trades)): + trade = self.parse_trade(trades[j], market) + result.append(trade) + result = self.sort_by(result, 'timestamp') + return self.filter_by_symbol_since_limit(result, market['symbol'], since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # { + # "amount":1.2082, + # "open":0.025096, + # "close":0.025095, + # "high":0.025096, + # "id":1591515300, + # "count":6, + # "low":0.025095, + # "vol":0.0303205097 + # } + # + return [ + self.safe_timestamp(ohlcv, 'id'), + self.safe_number(ohlcv, 'open'), + self.safe_number(ohlcv, 'high'), + self.safe_number(ohlcv, 'low'), + self.safe_number(ohlcv, 'close'), + self.safe_number(ohlcv, 'amount'), + ] + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://huobiapi.github.io/docs/spot/v1/en/#get-klines-candles + https://huobiapi.github.io/docs/dm/v1/en/#get-kline-data + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#get-kline-data + https://huobiapi.github.io/docs/usdt_swap/v1/en/#general-get-kline-data + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param str [params.useHistoricalEndpointForSpot]: True/false - whether use the historical candles endpoint for spot markets or default klines endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate') + if paginate: + return self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params, 1000) + market = self.market(symbol) + request: dict = { + 'period': self.safe_string(self.timeframes, timeframe, timeframe), + # 'symbol': market['id'], # spot, future + # 'contract_code': market['id'], # swap + # 'size': 1000, # max 1000 for spot, 2000 for contracts + # 'from': int((since / str(1000))), spot only + # 'to': self.seconds(), spot only + } + priceType = self.safe_string_n(params, ['priceType', 'price']) + params = self.omit(params, ['priceType', 'price']) + until = None + until, params = self.handle_param_integer(params, 'until') + untilSeconds = self.parse_to_int(until / 1000) if (until is not None) else None + if market['contract']: + if limit is not None: + request['size'] = min(limit, 2000) # when using limit: from & to are ignored + # https://huobiapi.github.io/docs/usdt_swap/v1/en/#general-get-kline-data + else: + limit = 2000 # only used for from/to calculation + if priceType is None: + duration = self.parse_timeframe(timeframe) + calcualtedEnd = None + if since is None: + now = self.seconds() + request['from'] = now - duration * (limit - 1) + calcualtedEnd = now + else: + start = self.parse_to_int(since / 1000) + request['from'] = start + calcualtedEnd = self.sum(start, duration * (limit - 1)) + request['to'] = untilSeconds if (untilSeconds is not None) else calcualtedEnd + response = None + if market['future']: + if market['inverse']: + request['symbol'] = market['id'] + if priceType == 'mark': + response = self.contractPublicGetIndexMarketHistoryMarkPriceKline(self.extend(request, params)) + elif priceType == 'index': + response = self.contractPublicGetIndexMarketHistoryIndex(self.extend(request, params)) + elif priceType == 'premiumIndex': + raise BadRequest(self.id + ' ' + market['type'] + ' has no api endpoint for ' + priceType + ' kline data') + else: + response = self.contractPublicGetMarketHistoryKline(self.extend(request, params)) + elif market['linear']: + request['contract_code'] = market['id'] + if priceType == 'mark': + response = self.contractPublicGetIndexMarketHistoryLinearSwapMarkPriceKline(self.extend(request, params)) + elif priceType == 'index': + raise BadRequest(self.id + ' ' + market['type'] + ' has no api endpoint for ' + priceType + ' kline data') + elif priceType == 'premiumIndex': + response = self.contractPublicGetIndexMarketHistoryLinearSwapPremiumIndexKline(self.extend(request, params)) + else: + response = self.contractPublicGetLinearSwapExMarketHistoryKline(self.extend(request, params)) + elif market['swap']: + request['contract_code'] = market['id'] + if market['inverse']: + if priceType == 'mark': + response = self.contractPublicGetIndexMarketHistorySwapMarkPriceKline(self.extend(request, params)) + elif priceType == 'index': + raise BadRequest(self.id + ' ' + market['type'] + ' has no api endpoint for ' + priceType + ' kline data') + elif priceType == 'premiumIndex': + response = self.contractPublicGetIndexMarketHistorySwapPremiumIndexKline(self.extend(request, params)) + else: + response = self.contractPublicGetSwapExMarketHistoryKline(self.extend(request, params)) + elif market['linear']: + if priceType == 'mark': + response = self.contractPublicGetIndexMarketHistoryLinearSwapMarkPriceKline(self.extend(request, params)) + elif priceType == 'index': + raise BadRequest(self.id + ' ' + market['type'] + ' has no api endpoint for ' + priceType + ' kline data') + elif priceType == 'premiumIndex': + response = self.contractPublicGetIndexMarketHistoryLinearSwapPremiumIndexKline(self.extend(request, params)) + else: + response = self.contractPublicGetLinearSwapExMarketHistoryKline(self.extend(request, params)) + else: + request['symbol'] = market['id'] + useHistorical = None + useHistorical, params = self.handle_option_and_params(params, 'fetchOHLCV', 'useHistoricalEndpointForSpot', True) + if not useHistorical: + if limit is not None: + request['size'] = min(limit, 2000) # max 2000 + response = self.spotPublicGetMarketHistoryKline(self.extend(request, params)) + else: + # "from & to" only available for the self endpoint + if since is not None: + request['from'] = self.parse_to_int(since / 1000) + if untilSeconds is not None: + request['to'] = untilSeconds + if limit is not None: + request['size'] = min(1000, limit) # max 1000, otherwise default returns 150 + response = self.spotPublicGetMarketHistoryCandles(self.extend(request, params)) + # + # { + # "status":"ok", + # "ch":"market.ethbtc.kline.1min", + # "ts":1591515374371, + # "data":[ + # {"amount":0.0,"open":0.025095,"close":0.025095,"high":0.025095,"id":1591515360,"count":0,"low":0.025095,"vol":0.0}, + # {"amount":1.2082,"open":0.025096,"close":0.025095,"high":0.025096,"id":1591515300,"count":6,"low":0.025095,"vol":0.0303205097}, + # {"amount":0.0648,"open":0.025096,"close":0.025096,"high":0.025096,"id":1591515240,"count":2,"low":0.025096,"vol":0.0016262208}, + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_ohlcvs(data, market, timeframe, since, limit) + + def fetch_accounts(self, params={}) -> List[Account]: + """ + fetch all the accounts associated with a profile + + https://huobiapi.github.io/docs/spot/v1/en/#get-all-accounts-of-the-current-user + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `account structures ` indexed by the account type + """ + self.load_markets() + response = self.spotPrivateGetV1AccountAccounts(params) + # + # { + # "status":"ok", + # "data":[ + # {"id":5202591,"type":"point","subtype":"","state":"working"}, + # {"id":1528640,"type":"spot","subtype":"","state":"working"}, + # ] + # } + # + data = self.safe_value(response, 'data') + return self.parse_accounts(data) + + def parse_account(self, account): + # + # { + # "id": 5202591, + # "type": "point", # spot, margin, otc, point, super-margin, investment, borrow, grid-trading, deposit-earning, otc-options + # "subtype": "", # The corresponding trading symbol(currency pair) the isolated margin is based on, e.g. btcusdt + # "state": "working" # working, lock + # } + # + typeId = self.safe_string(account, 'type') + accountsById = self.safe_value(self.options, 'accountsById', {}) + type = self.safe_value(accountsById, typeId, typeId) + return { + 'info': account, + 'id': self.safe_string(account, 'id'), + 'type': type, + 'code': None, + } + + def fetch_account_id_by_type(self, type: str, marginMode: Str = None, symbol: Str = None, params={}): + """ + fetch all the accounts by a type and marginModeassociated with a profile + + https://huobiapi.github.io/docs/spot/v1/en/#get-all-accounts-of-the-current-user + + :param str type: 'spot', 'swap' or 'future + :param str [marginMode]: 'cross' or 'isolated' + :param str [symbol]: unified ccxt market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `account structures ` indexed by the account type + """ + accounts = self.load_accounts() + accountId = self.safe_value_2(params, 'accountId', 'account-id') + if accountId is not None: + return accountId + if type == 'spot': + if marginMode == 'cross': + type = 'super-margin' + elif marginMode == 'isolated': + type = 'margin' + marketId = None + if symbol is not None: + marketId = self.market_id(symbol) + for i in range(0, len(accounts)): + account = accounts[i] + info = self.safe_value(account, 'info') + subtype = self.safe_string(info, 'subtype', None) + typeFromAccount = self.safe_string(account, 'type') + if type == 'margin': + if subtype == marketId: + return self.safe_string(account, 'id') + elif type == typeFromAccount: + return self.safe_string(account, 'id') + defaultAccount = self.safe_value(accounts, 0, {}) + return self.safe_string(defaultAccount, 'id') + + def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://huobiapi.github.io/docs/spot/v1/en/#apiv2-currency-amp-chains + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = self.spotPublicGetV2ReferenceCurrencies(params) + # + # { + # "code": 200, + # "data": [ + # { + # "currency": "sxp", + # "assetType": "1", + # "chains": [ + # { + # "chain": "sxp", + # "displayName": "ERC20", + # "baseChain": "ETH", + # "baseChainProtocol": "ERC20", + # "isDynamic": True, + # "numOfConfirmations": "12", + # "numOfFastConfirmations": "12", + # "depositStatus": "allowed", + # "minDepositAmt": "0.23", + # "withdrawStatus": "allowed", + # "minWithdrawAmt": "0.23", + # "withdrawPrecision": "8", + # "maxWithdrawAmt": "227000.000000000000000000", + # "withdrawQuotaPerDay": "227000.000000000000000000", + # "withdrawQuotaPerYear": null, + # "withdrawQuotaTotal": null, + # "withdrawFeeType": "fixed", + # "transactFeeWithdraw": "11.1654", + # "addrWithTag": False, + # "addrDepositTag": False + # } + # ], + # "instStatus": "normal" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + result: dict = {} + self.options['networkChainIdsByNames'] = {} + self.options['networkNamesByChainIds'] = {} + for i in range(0, len(data)): + entry = data[i] + currencyId = self.safe_string(entry, 'currency') + code = self.safe_currency_code(currencyId) + assetType = self.safe_string(entry, 'assetType') + type = assetType == 'crypto' if '1' else 'fiat' + self.options['networkChainIdsByNames'][code] = {} + chains = self.safe_list(entry, 'chains', []) + networks: dict = {} + for j in range(0, len(chains)): + chainEntry = chains[j] + uniqueChainId = self.safe_string(chainEntry, 'chain') # i.e. usdterc20, trc20usdt ... + title = self.safe_string_2(chainEntry, 'baseChain', 'displayName') # baseChain and baseChainProtocol are together existent or inexistent in entries, but baseChain is preferred. when they are both inexistent, then we use generic displayName + self.options['networkChainIdsByNames'][code][title] = uniqueChainId + self.options['networkNamesByChainIds'][uniqueChainId] = title + networkCode = self.network_id_to_code(uniqueChainId) + networks[networkCode] = { + 'info': chainEntry, + 'id': uniqueChainId, + 'network': networkCode, + 'limits': { + 'deposit': { + 'min': self.safe_number(chainEntry, 'minDepositAmt'), + 'max': None, + }, + 'withdraw': { + 'min': self.safe_number(chainEntry, 'minWithdrawAmt'), + 'max': self.safe_number(chainEntry, 'maxWithdrawAmt'), + }, + }, + 'active': None, + 'deposit': self.safe_string(chainEntry, 'depositStatus') == 'allowed', + 'withdraw': self.safe_string(chainEntry, 'withdrawStatus') == 'allowed', + 'fee': self.safe_number(chainEntry, 'transactFeeWithdraw'), + 'precision': self.parse_number(self.parse_precision(self.safe_string(chainEntry, 'withdrawPrecision'))), + } + result[code] = self.safe_currency_structure({ + 'info': entry, + 'code': code, + 'id': currencyId, + 'active': self.safe_string(entry, 'instStatus') == 'normal', + 'deposit': None, + 'withdraw': None, + 'fee': None, + 'name': None, + 'type': type, + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + 'deposit': { + 'min': None, + 'max': None, + }, + }, + 'precision': None, + 'networks': networks, + }) + return result + + def network_id_to_code(self, networkId: Str = None, currencyCode: Str = None): + # here network-id is provided pair of currency & chain(i.e. trc20usdt) + keys = list(self.options['networkNamesByChainIds'].keys()) + keysLength = len(keys) + if keysLength == 0: + raise ExchangeError(self.id + ' networkIdToCode() - markets need to be loaded at first') + networkTitle = self.safe_value(self.options['networkNamesByChainIds'], networkId, networkId) + return super(htx, self).network_id_to_code(networkTitle) + + def network_code_to_id(self, networkCode: str, currencyCode: Str = None): + if currencyCode is None: + raise ArgumentsRequired(self.id + ' networkCodeToId() requires a currencyCode argument') + keys = list(self.options['networkChainIdsByNames'].keys()) + keysLength = len(keys) + if keysLength == 0: + raise ExchangeError(self.id + ' networkCodeToId() - markets need to be loaded at first') + uniqueNetworkIds = self.safe_value(self.options['networkChainIdsByNames'], currencyCode, {}) + if networkCode in uniqueNetworkIds: + return uniqueNetworkIds[networkCode] + else: + networkTitle = super(htx, self).network_code_to_id(networkCode) + return self.safe_value(uniqueNetworkIds, networkTitle, networkTitle) + + def fetch_balance(self, params={}) -> Balances: + """ + + https://huobiapi.github.io/docs/spot/v1/en/#get-account-balance-of-a-specific-account + https://www.htx.com/en-us/opend/newApiPages/?id=7ec4b429-7773-11ed-9966-0242ac110003 + https://www.htx.com/en-us/opend/newApiPages/?id=10000074-77b7-11ed-9966-0242ac110003 + https://huobiapi.github.io/docs/dm/v1/en/#query-asset-valuation + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#query-user-s-account-information + https://huobiapi.github.io/docs/usdt_swap/v1/en/#isolated-query-user-s-account-information + https://huobiapi.github.io/docs/usdt_swap/v1/en/#cross-query-user-39-s-account-information + + query for balance and get the amount of funds available for trading or funds locked in orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.unified]: provide self parameter if you have a recent account with unified cross+isolated margin account + :returns dict: a `balance structure ` + """ + self.load_markets() + type = None + type, params = self.handle_market_type_and_params('fetchBalance', None, params) + options = self.safe_value(self.options, 'fetchBalance', {}) + isUnifiedAccount = self.safe_value_2(params, 'isUnifiedAccount', 'unified', False) + params = self.omit(params, ['isUnifiedAccount', 'unified']) + request: dict = {} + spot = (type == 'spot') + future = (type == 'future') + defaultSubType = self.safe_string_2(self.options, 'defaultSubType', 'subType', 'linear') + subType = self.safe_string_2(options, 'defaultSubType', 'subType', defaultSubType) + subType = self.safe_string_2(params, 'defaultSubType', 'subType', subType) + inverse = (subType == 'inverse') + linear = (subType == 'linear') + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchBalance', params) + params = self.omit(params, ['defaultSubType', 'subType']) + isolated = (marginMode == 'isolated') + cross = (marginMode == 'cross') + margin = (type == 'margin') or (spot and (cross or isolated)) + response = None + if spot or margin: + if margin: + if isolated: + response = self.spotPrivateGetV1MarginAccountsBalance(self.extend(request, params)) + else: + response = self.spotPrivateGetV1CrossMarginAccountsBalance(self.extend(request, params)) + else: + self.load_accounts() + accountId = self.fetch_account_id_by_type(type, None, None, params) + request['account-id'] = accountId + response = self.spotPrivateGetV1AccountAccountsAccountIdBalance(self.extend(request, params)) + elif isUnifiedAccount: + response = self.contractPrivateGetLinearSwapApiV3UnifiedAccountInfo(self.extend(request, params)) + elif linear: + if isolated: + response = self.contractPrivatePostLinearSwapApiV1SwapAccountInfo(self.extend(request, params)) + else: + response = self.contractPrivatePostLinearSwapApiV1SwapCrossAccountInfo(self.extend(request, params)) + elif inverse: + if future: + response = self.contractPrivatePostApiV1ContractAccountInfo(self.extend(request, params)) + else: + response = self.contractPrivatePostSwapApiV1SwapAccountInfo(self.extend(request, params)) + # + # spot + # + # { + # "status": "ok", + # "data": { + # "id": 1528640, + # "type": "spot", + # "state": "working", + # "list": [ + # {"currency": "lun", "type": "trade", "balance": "0", "seq-num": "0"}, + # {"currency": "lun", "type": "frozen", "balance": "0", "seq-num": "0"}, + # {"currency": "ht", "type": "frozen", "balance": "0", "seq-num": "145"}, + # ] + # }, + # "ts":1637644827566 + # } + # + # cross margin + # + # { + # "status": "ok", + # "data": { + # "id": 51015302, + # "type": "cross-margin", + # "state": "working", + # "risk-rate": "2", + # "acct-balance-sum": "100", + # "debt-balance-sum": "0", + # "list": [ + # {"currency": "usdt", "type": "trade", "balance": "100"}, + # {"currency": "usdt", "type": "frozen", "balance": "0"}, + # {"currency": "usdt", "type": "loan-available", "balance": "200"}, + # {"currency": "usdt", "type": "transfer-out-available", "balance": "-1"}, + # {"currency": "ht", "type": "loan-available", "balance": "36.60724091"}, + # {"currency": "ht", "type": "transfer-out-available", "balance": "-1"}, + # {"currency": "btc", "type": "trade", "balance": "1168.533000000000000000"}, + # {"currency": "btc", "type": "frozen", "balance": "0.000000000000000000"}, + # {"currency": "btc", "type": "loan", "balance": "-2.433000000000000000"}, + # {"currency": "btc", "type": "interest", "balance": "-0.000533000000000000"}, + # {"currency": "btc", "type": "transfer-out-available", "balance": "1163.872174670000000000"}, + # {"currency": "btc", "type": "loan-available", "balance": "8161.876538350676000000"} + # ] + # }, + # "code": 200 + # } + # + # isolated margin + # + # { + # "data": [ + # { + # "id": 18264, + # "type": "margin", + # "state": "working", + # "symbol": "btcusdt", + # "fl-price": "0", + # "fl-type": "safe", + # "risk-rate": "475.952571086994250554", + # "list": [ + # {"currency": "btc","type": "trade","balance": "1168.533000000000000000"}, + # {"currency": "btc","type": "frozen","balance": "0.000000000000000000"}, + # {"currency": "btc","type": "loan","balance": "-2.433000000000000000"}, + # {"currency": "btc","type": "interest","balance": "-0.000533000000000000"}, + # {"currency": "btc","type": "transfer-out-available", "balance": "1163.872174670000000000"}, + # {"currency": "btc","type": "loan-available", "balance": "8161.876538350676000000"} + # ] + # } + # ] + # } + # + # future, swap isolated + # + # { + # "status": "ok", + # "data": [ + # { + # "symbol": "BTC", + # "margin_balance": 0, + # "margin_position": 0E-18, + # "margin_frozen": 0, + # "margin_available": 0E-18, + # "profit_real": 0, + # "profit_unreal": 0, + # "risk_rate": null, + # "withdraw_available": 0, + # "liquidation_price": null, + # "lever_rate": 5, + # "adjust_factor": 0.025000000000000000, + # "margin_static": 0, + # "is_debit": 0, # future only + # "contract_code": "BTC-USD", # swap only + # "margin_asset": "USDT", # linear only + # "margin_mode": "isolated", # linear only + # "margin_account": "BTC-USDT" # linear only + # "transfer_profit_ratio": null # inverse only + # }, + # ], + # "ts": 1637644827566 + # } + # + # linear cross futures and linear cross swap + # + # { + # "status": "ok", + # "data": [ + # { + # "futures_contract_detail": [ + # { + # "symbol": "ETH", + # "contract_code": "ETH-USDT-220325", + # "margin_position": 0, + # "margin_frozen": 0, + # "margin_available": 200.000000000000000000, + # "profit_unreal": 0E-18, + # "liquidation_price": null, + # "lever_rate": 5, + # "adjust_factor": 0.060000000000000000, + # "contract_type": "quarter", + # "pair": "ETH-USDT", + # "business_type": "futures" + # }, + # ], + # "margin_mode": "cross", + # "margin_account": "USDT", + # "margin_asset": "USDT", + # "margin_balance": 49.874186030200000000, + # "money_in": 50, + # "money_out": 0, + # "margin_static": 49.872786030200000000, + # "margin_position": 6.180000000000000000, + # "margin_frozen": 6.000000000000000000, + # "profit_unreal": 0.001400000000000000, + # "withdraw_available": 37.6927860302, + # "risk_rate": 271.984050521072796934, + # "new_risk_rate": 0.001858676950514399, + # "contract_detail": [ + # { + # "symbol": "MANA", + # "contract_code": "MANA-USDT", + # "margin_position": 0, + # "margin_frozen": 0, + # "margin_available": 200.000000000000000000, + # "profit_unreal": 0E-18, + # "liquidation_price": null, + # "lever_rate": 5, + # "adjust_factor": 0.100000000000000000, + # "contract_type": "swap", + # "pair": "MANA-USDT", + # "business_type": "swap" + # }, + # ] + # } + # ], + # "ts": 1640915104870 + # } + # + # TODO add balance parsing for linear swap + # + result: dict = {'info': response} + data = self.safe_value(response, 'data') + if spot or margin: + if isolated: + for i in range(0, len(data)): + entry = data[i] + symbol = self.safe_symbol(self.safe_string(entry, 'symbol')) + balances = self.safe_value(entry, 'list') + subResult: dict = {} + for j in range(0, len(balances)): + balance = balances[j] + currencyId = self.safe_string(balance, 'currency') + code = self.safe_currency_code(currencyId) + subResult[code] = self.parse_margin_balance_helper(balance, code, subResult) + result[symbol] = self.safe_balance(subResult) + else: + balances = self.safe_value(data, 'list', []) + for i in range(0, len(balances)): + balance = balances[i] + currencyId = self.safe_string(balance, 'currency') + code = self.safe_currency_code(currencyId) + result[code] = self.parse_margin_balance_helper(balance, code, result) + result = self.safe_balance(result) + elif isUnifiedAccount: + for i in range(0, len(data)): + entry = data[i] + marginAsset = self.safe_string(entry, 'margin_asset') + currencyCode = self.safe_currency_code(marginAsset) + if isolated: + isolated_swap = self.safe_value(entry, 'isolated_swap', {}) + for j in range(0, len(isolated_swap)): + balance = isolated_swap[j] + marketId = self.safe_string(balance, 'contract_code') + subBalance: dict = { + 'code': currencyCode, + 'free': self.safe_number(balance, 'margin_available'), + } + symbol = self.safe_symbol(marketId) + result[symbol] = subBalance + result = self.safe_balance(result) + else: + account = self.account() + account['free'] = self.safe_string(entry, 'margin_static') + account['used'] = self.safe_string(entry, 'margin_frozen') + result[currencyCode] = account + result = self.safe_balance(result) + elif linear: + first = self.safe_value(data, 0, {}) + if isolated: + for i in range(0, len(data)): + balance = data[i] + marketId = self.safe_string_2(balance, 'contract_code', 'margin_account') + market = self.safe_market(marketId) + currencyId = self.safe_string(balance, 'margin_asset') + currency = self.safe_currency(currencyId) + code = self.safe_string(market, 'settle', currency['code']) + # the exchange outputs positions for delisted markets + # https://www.huobi.com/support/en-us/detail/74882968522337 + # we skip it if the market was delisted + if code is not None: + account = self.account() + account['free'] = self.safe_string(balance, 'margin_balance') + account['used'] = self.safe_string(balance, 'margin_frozen') + accountsByCode: dict = {} + accountsByCode[code] = account + symbol = market['symbol'] + result[symbol] = self.safe_balance(accountsByCode) + else: + account = self.account() + account['free'] = self.safe_string(first, 'withdraw_available') + account['total'] = self.safe_string(first, 'margin_balance') + currencyId = self.safe_string_2(first, 'margin_asset', 'symbol') + code = self.safe_currency_code(currencyId) + result[code] = account + result = self.safe_balance(result) + elif inverse: + for i in range(0, len(data)): + balance = data[i] + currencyId = self.safe_string(balance, 'symbol') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(balance, 'margin_available') + account['used'] = self.safe_string(balance, 'margin_frozen') + result[code] = account + result = self.safe_balance(result) + return result + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://huobiapi.github.io/docs/spot/v1/en/#get-the-order-detail-of-an-order-based-on-client-order-id + https://huobiapi.github.io/docs/spot/v1/en/#get-the-order-detail-of-an-order + https://huobiapi.github.io/docs/usdt_swap/v1/en/#isolated-get-information-of-an-order + https://huobiapi.github.io/docs/usdt_swap/v1/en/#cross-get-information-of-order + https://huobiapi.github.io/docs/dm/v1/en/#get-information-of-an-order + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#get-information-of-an-order + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + marketType = None + marketType, params = self.handle_market_type_and_params('fetchOrder', market, params) + request: dict = { + # spot ----------------------------------------------------------- + # 'order-id': 'id', + # 'symbol': market['id'], + # 'client-order-id': clientOrderId, + # 'clientOrderId': clientOrderId, + # contracts ------------------------------------------------------ + # 'order_id': id, + # 'client_order_id': clientOrderId, + # 'contract_code': market['id'], + # 'pair': 'BTC-USDT', + # 'contract_type': 'this_week', # swap, self_week, next_week, quarter, next_ quarter + } + response = None + if marketType == 'spot': + clientOrderId = self.safe_string(params, 'clientOrderId') + if clientOrderId is not None: + # will be filled below in self.extend() + # they expect clientOrderId instead of client-order-id + # request['clientOrderId'] = clientOrderId + response = self.spotPrivateGetV1OrderOrdersGetClientOrder(self.extend(request, params)) + else: + request['order-id'] = id + response = self.spotPrivateGetV1OrderOrdersOrderId(self.extend(request, params)) + else: + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrder() requires a symbol argument') + clientOrderId = self.safe_string_2(params, 'client_order_id', 'clientOrderId') + if clientOrderId is None: + request['order_id'] = id + else: + request['client_order_id'] = clientOrderId + params = self.omit(params, ['client_order_id', 'clientOrderId']) + request['contract_code'] = market['id'] + if market['linear']: + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchOrder', params) + marginMode = 'cross' if (marginMode is None) else marginMode + if marginMode == 'isolated': + response = self.contractPrivatePostLinearSwapApiV1SwapOrderInfo(self.extend(request, params)) + elif marginMode == 'cross': + response = self.contractPrivatePostLinearSwapApiV1SwapCrossOrderInfo(self.extend(request, params)) + elif market['inverse']: + if marketType == 'future': + request['symbol'] = market['settleId'] + response = self.contractPrivatePostApiV1ContractOrderInfo(self.extend(request, params)) + elif marketType == 'swap': + response = self.contractPrivatePostSwapApiV1SwapOrderInfo(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchOrder() does not support ' + marketType + ' markets') + # + # spot + # + # { + # "status":"ok", + # "data":{ + # "id":438398393065481, + # "symbol":"ethusdt", + # "account-id":1528640, + # "client-order-id":"AA03022abc2163433e-006b-480e-9ad1-d4781478c5e7", + # "amount":"0.100000000000000000", + # "price":"3000.000000000000000000", + # "created-at":1640549994642, + # "type":"buy-limit", + # "field-amount":"0.0", + # "field-cash-amount":"0.0", + # "field-fees":"0.0", + # "finished-at":0, + # "source":"spot-api", + # "state":"submitted", + # "canceled-at":0 + # } + # } + # + # linear swap cross margin + # + # { + # "status":"ok", + # "data":[ + # { + # "business_type":"swap", + # "contract_type":"swap", + # "pair":"BTC-USDT", + # "symbol":"BTC", + # "contract_code":"BTC-USDT", + # "volume":1, + # "price":3000, + # "order_price_type":"limit", + # "order_type":1, + # "direction":"buy", + # "offset":"open", + # "lever_rate":1, + # "order_id":924912513206878210, + # "client_order_id":null, + # "created_at":1640557927189, + # "trade_volume":0, + # "trade_turnover":0, + # "fee":0, + # "trade_avg_price":null, + # "margin_frozen":3.000000000000000000, + # "profit":0, + # "status":3, + # "order_source":"api", + # "order_id_str":"924912513206878210", + # "fee_asset":"USDT", + # "liquidation_type":"0", + # "canceled_at":0, + # "margin_asset":"USDT", + # "margin_account":"USDT", + # "margin_mode":"cross", + # "is_tpsl":0, + # "real_profit":0 + # } + # ], + # "ts":1640557982556 + # } + # + # linear swap isolated margin detail + # + # { + # "status": "ok", + # "data": { + # "symbol": "BTC", + # "contract_code": "BTC-USDT", + # "instrument_price": 0, + # "final_interest": 0, + # "adjust_value": 0, + # "lever_rate": 10, + # "direction": "sell", + # "offset": "open", + # "volume": 1.000000000000000000, + # "price": 13059.800000000000000000, + # "created_at": 1603703614712, + # "canceled_at": 0, + # "order_source": "api", + # "order_price_type": "opponent", + # "margin_frozen": 0, + # "profit": 0, + # "trades": [ + # { + # "trade_id": 131560927, + # "trade_price": 13059.800000000000000000, + # "trade_volume": 1.000000000000000000, + # "trade_turnover": 13.059800000000000000, + # "trade_fee": -0.005223920000000000, + # "created_at": 1603703614715, + # "role": "taker", + # "fee_asset": "USDT", + # "profit": 0, + # "real_profit": 0, + # "id": "131560927-770334322963152896-1" + # } + # ], + # "total_page": 1, + # "current_page": 1, + # "total_size": 1, + # "liquidation_type": "0", + # "fee_asset": "USDT", + # "fee": -0.005223920000000000, + # "order_id": 770334322963152896, + # "order_id_str": "770334322963152896", + # "client_order_id": 57012021045, + # "order_type": "1", + # "status": 6, + # "trade_avg_price": 13059.800000000000000000, + # "trade_turnover": 13.059800000000000000, + # "trade_volume": 1.000000000000000000, + # "margin_asset": "USDT", + # "margin_mode": "isolated", + # "margin_account": "BTC-USDT", + # "real_profit": 0, + # "is_tpsl": 0 + # }, + # "ts": 1603703678477 + # } + order = self.safe_value(response, 'data') + if isinstance(order, list): + order = self.safe_value(order, 0) + return self.parse_order(order) + + def parse_margin_balance_helper(self, balance, code, result): + account = None + if code in result: + account = result[code] + else: + account = self.account() + if balance['type'] == 'trade': + account['free'] = self.safe_string(balance, 'balance') + if balance['type'] == 'frozen': + account['used'] = self.safe_string(balance, 'balance') + return account + + def fetch_spot_orders_by_states(self, states, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + method = self.safe_string(self.options, 'fetchOrdersByStatesMethod', 'spot_private_get_v1_order_orders') # spot_private_get_v1_order_history + if method == 'spot_private_get_v1_order_orders': + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrders() requires a symbol argument') + self.load_markets() + market = None + request: dict = { + # spot_private_get_v1_order_orders GET /v1/order/orders ---------- + # 'symbol': market['id'], # required + # 'types': 'buy-market,sell-market,buy-limit,sell-limit,buy-ioc,sell-ioc,buy-stop-limit,sell-stop-limit,buy-limit-fok,sell-limit-fok,buy-stop-limit-fok,sell-stop-limit-fok', + # 'start-time': since, # max window of 48h within a range of 180 days, within past 2 hours for cancelled orders + # 'end-time': self.milliseconds(), + 'states': states, # filled, partial-canceled, canceled + # 'from': order['id'], + # 'direct': 'next', # next, prev, used with from + # 'size': 100, # max 100 + # spot_private_get_v1_order_history GET /v1/order/history -------- + # 'symbol': market['id'], # optional + # 'start-time': since, # max window of 48h within a range of 180 days, within past 2 hours for cancelled orders + # 'end-time': self.milliseconds(), + # 'direct': 'next', # next, prev, used with from + # 'size': 100, # max 100 + } + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if since is not None: + request['start-time'] = since # a window of 48 hours within 180 days + request['end-time'] = self.sum(since, 48 * 60 * 60 * 1000) + request, params = self.handle_until_option('end-time', request, params) + if limit is not None: + request['size'] = limit + response = None + if method == 'spot_private_get_v1_order_orders': + response = self.spotPrivateGetV1OrderOrders(self.extend(request, params)) + else: + response = self.spotPrivateGetV1OrderHistory(self.extend(request, params)) + # + # spot_private_get_v1_order_orders GET /v1/order/orders + # + # { + # "status": "ok", + # "data": [ + # { + # "id": 13997833014, + # "symbol": "ethbtc", + # "account-id": 3398321, + # "client-order-id": "23456", + # "amount": "0.045000000000000000", + # "price": "0.034014000000000000", + # "created-at": 1545836976871, + # "type": "sell-limit", + # "field-amount": "0.045000000000000000", + # "field-cash-amount": "0.001530630000000000", + # "field-fees": "0.000003061260000000", + # "finished-at": 1545837948214, + # "source": "spot-api", + # "state": "filled", + # "canceled-at": 0 + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_orders(data, market, since, limit) + + def fetch_spot_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + return self.fetch_spot_orders_by_states('pre-submitted,submitted,partial-filled,filled,partial-canceled,canceled', symbol, since, limit, params) + + def fetch_closed_spot_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + return self.fetch_spot_orders_by_states('filled,partial-canceled,canceled', symbol, since, limit, params) + + def fetch_contract_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchContractOrders() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + # POST /api/v1/contract_hisorders inverse futures ---------------- + # 'symbol': market['settleId'], # BTC, ETH, ... + # 'order_type': '1', # 1 limit,3 opponent,4 lightning, 5 trigger order, 6 pst_only, 7 optimal_5, 8 optimal_10, 9 optimal_20, 10 fok, 11 ioc + # POST /swap-api/v3/swap_hisorders inverse swap ------------------ + # POST /linear-swap-api/v3/swap_hisorders linear isolated -------- + # POST /linear-swap-api/v3/swap_cross_hisorders linear cross ----- + 'trade_type': 0, # 0:All; 1: Open long; 2: Open short; 3: Close short; 4: Close long; 5: Liquidate long positions; 6: Liquidate short positions, 17:buy(one-way mode), 18:sell(one-way mode) + 'status': '0', # support multiple query seperated by ',',such as '3,4,5', 0: all. 3. Have sumbmitted the orders; 4. Orders partially matched; 5. Orders cancelled with partially matched; 6. Orders fully matched; 7. Orders cancelled + } + response = None + trigger = self.safe_bool_2(params, 'stop', 'trigger') + stopLossTakeProfit = self.safe_value(params, 'stopLossTakeProfit') + trailing = self.safe_bool(params, 'trailing', False) + params = self.omit(params, ['stop', 'stopLossTakeProfit', 'trailing', 'trigger']) + if trigger or stopLossTakeProfit or trailing: + if limit is not None: + request['page_size'] = limit + request['contract_code'] = market['id'] + request['create_date'] = 90 + else: + if since is not None: + request['start_time'] = since # max 90 days back + # request['end_time'] = since + 172800000 # 48 hours window + request['contract'] = market['id'] + request['type'] = 1 # 1:All Orders,2:Order in Finished Status + request, params = self.handle_until_option('end_time', request, params) + if market['linear']: + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchContractOrders', params) + marginMode = 'cross' if (marginMode is None) else marginMode + if marginMode == 'isolated': + if trigger: + response = self.contractPrivatePostLinearSwapApiV1SwapTriggerHisorders(self.extend(request, params)) + elif stopLossTakeProfit: + response = self.contractPrivatePostLinearSwapApiV1SwapTpslHisorders(self.extend(request, params)) + elif trailing: + response = self.contractPrivatePostLinearSwapApiV1SwapTrackHisorders(self.extend(request, params)) + else: + response = self.contractPrivatePostLinearSwapApiV3SwapHisorders(self.extend(request, params)) + elif marginMode == 'cross': + if trigger: + response = self.contractPrivatePostLinearSwapApiV1SwapCrossTriggerHisorders(self.extend(request, params)) + elif stopLossTakeProfit: + response = self.contractPrivatePostLinearSwapApiV1SwapCrossTpslHisorders(self.extend(request, params)) + elif trailing: + response = self.contractPrivatePostLinearSwapApiV1SwapCrossTrackHisorders(self.extend(request, params)) + else: + response = self.contractPrivatePostLinearSwapApiV3SwapCrossHisorders(self.extend(request, params)) + elif market['inverse']: + if market['swap']: + if trigger: + response = self.contractPrivatePostSwapApiV1SwapTriggerHisorders(self.extend(request, params)) + elif stopLossTakeProfit: + response = self.contractPrivatePostSwapApiV1SwapTpslHisorders(self.extend(request, params)) + elif trailing: + response = self.contractPrivatePostSwapApiV1SwapTrackHisorders(self.extend(request, params)) + else: + response = self.contractPrivatePostSwapApiV3SwapHisorders(self.extend(request, params)) + elif market['future']: + request['symbol'] = market['settleId'] + if trigger: + response = self.contractPrivatePostApiV1ContractTriggerHisorders(self.extend(request, params)) + elif stopLossTakeProfit: + response = self.contractPrivatePostApiV1ContractTpslHisorders(self.extend(request, params)) + elif trailing: + response = self.contractPrivatePostApiV1ContractTrackHisorders(self.extend(request, params)) + else: + response = self.contractPrivatePostApiV3ContractHisorders(self.extend(request, params)) + # + # future and swap + # + # { + # "code": 200, + # "msg": "ok", + # "data": [ + # { + # "direction": "buy", + # "offset": "open", + # "volume": 1.000000000000000000, + # "price": 25000.000000000000000000, + # "profit": 0E-18, + # "pair": "BTC-USDT", + # "query_id": 47403349100, + # "order_id": 1103683465337593856, + # "contract_code": "BTC-USDT-230505", + # "symbol": "BTC", + # "lever_rate": 5, + # "create_date": 1683180243577, + # "order_source": "web", + # "canceled_source": "web", + # "order_price_type": 1, + # "order_type": 1, + # "margin_frozen": 0E-18, + # "trade_volume": 0E-18, + # "trade_turnover": 0E-18, + # "fee": 0E-18, + # "trade_avg_price": 0, + # "status": 7, + # "order_id_str": "1103683465337593856", + # "fee_asset": "USDT", + # "fee_amount": 0, + # "fee_quote_amount": 0, + # "liquidation_type": "0", + # "margin_asset": "USDT", + # "margin_mode": "cross", + # "margin_account": "USDT", + # "update_time": 1683180352034, + # "is_tpsl": 0, + # "real_profit": 0, + # "trade_partition": "USDT", + # "reduce_only": 0, + # "contract_type": "self_week", + # "business_type": "futures" + # } + # ], + # "ts": 1683239909141 + # } + # + # trigger + # + # { + # "status": "ok", + # "data": { + # "orders": [ + # { + # "contract_type": "swap", + # "business_type": "swap", + # "pair": "BTC-USDT", + # "symbol": "BTC", + # "contract_code": "BTC-USDT", + # "trigger_type": "le", + # "volume": 1.000000000000000000, + # "order_type": 1, + # "direction": "buy", + # "offset": "open", + # "lever_rate": 1, + # "order_id": 1103670703588327424, + # "order_id_str": "1103670703588327424", + # "relation_order_id": "-1", + # "order_price_type": "limit", + # "status": 6, + # "order_source": "web", + # "trigger_price": 25000.000000000000000000, + # "triggered_price": null, + # "order_price": 24000.000000000000000000, + # "created_at": 1683177200945, + # "triggered_at": null, + # "order_insert_at": 0, + # "canceled_at": 1683179075234, + # "fail_code": null, + # "fail_reason": null, + # "margin_mode": "cross", + # "margin_account": "USDT", + # "update_time": 1683179075958, + # "trade_partition": "USDT", + # "reduce_only": 0 + # }, + # ], + # "total_page": 1, + # "current_page": 1, + # "total_size": 2 + # }, + # "ts": 1683239702792 + # } + # + # stop-loss and take-profit + # + # { + # "status": "ok", + # "data": { + # "orders": [ + # { + # "contract_type": "swap", + # "business_type": "swap", + # "pair": "BTC-USDT", + # "symbol": "BTC", + # "contract_code": "BTC-USDT", + # "margin_mode": "cross", + # "margin_account": "USDT", + # "volume": 1.000000000000000000, + # "order_type": 1, + # "tpsl_order_type": "sl", + # "direction": "sell", + # "order_id": 1103680386844839936, + # "order_id_str": "1103680386844839936", + # "order_source": "web", + # "trigger_type": "le", + # "trigger_price": 25000.000000000000000000, + # "created_at": 1683179509613, + # "order_price_type": "market", + # "status": 11, + # "source_order_id": null, + # "relation_tpsl_order_id": "-1", + # "canceled_at": 0, + # "fail_code": null, + # "fail_reason": null, + # "triggered_price": null, + # "relation_order_id": "-1", + # "update_time": 1683179968231, + # "order_price": 0E-18, + # "trade_partition": "USDT" + # }, + # ], + # "total_page": 1, + # "current_page": 1, + # "total_size": 2 + # }, + # "ts": 1683229230233 + # } + # + orders = self.safe_value(response, 'data') + if not isinstance(orders, list): + orders = self.safe_value(orders, 'orders', []) + return self.parse_orders(orders, market, since, limit) + + def fetch_closed_contract_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + request: dict = { + 'status': '5,6,7', # comma separated, 0 all, 3 submitted orders, 4 partially matched, 5 partially cancelled, 6 fully matched and closed, 7 canceled + } + return self.fetch_contract_orders(symbol, since, limit, self.extend(request, params)) + + def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + + https://huobiapi.github.io/docs/spot/v1/en/#search-past-orders + https://huobiapi.github.io/docs/spot/v1/en/#search-historical-orders-within-48-hours + https://huobiapi.github.io/docs/usdt_swap/v1/en/#isolated-get-history-orders-new + https://huobiapi.github.io/docs/usdt_swap/v1/en/#cross-get-history-orders-new + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#get-history-orders-new + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#query-history-orders-via-multiple-fields-new + + fetches information on multiple orders made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.trigger]: *contract only* if the orders are trigger trigger orders or not + :param bool [params.stopLossTakeProfit]: *contract only* if the orders are stop-loss or take-profit orders + :param int [params.until]: the latest time in ms to fetch entries for + :param boolean [params.trailing]: *contract only* set to True if you want to fetch trailing stop orders + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + marketType = None + marketType, params = self.handle_market_type_and_params('fetchOrders', market, params) + contract = (marketType == 'swap') or (marketType == 'future') + if contract and (symbol is None): + raise ArgumentsRequired(self.id + ' fetchOrders() requires a symbol argument for ' + marketType + ' orders') + if contract: + return self.fetch_contract_orders(symbol, since, limit, params) + else: + return self.fetch_spot_orders(symbol, since, limit, params) + + def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + + https://huobiapi.github.io/docs/spot/v1/en/#search-past-orders + https://huobiapi.github.io/docs/spot/v1/en/#search-historical-orders-within-48-hours + https://huobiapi.github.io/docs/usdt_swap/v1/en/#isolated-get-history-orders-new + https://huobiapi.github.io/docs/usdt_swap/v1/en/#cross-get-history-orders-new + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#get-history-orders-new + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#query-history-orders-via-multiple-fields-new + + fetches information on multiple closed orders made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch entries for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchClosedOrders', 'paginate') + if paginate: + return self.fetch_paginated_call_dynamic('fetchClosedOrders', symbol, since, limit, params, 100) + market = None + if symbol is not None: + market = self.market(symbol) + marketType = None + marketType, params = self.handle_market_type_and_params('fetchClosedOrders', market, params) + if marketType == 'spot': + return self.fetch_closed_spot_orders(symbol, since, limit, params) + else: + return self.fetch_closed_contract_orders(symbol, since, limit, params) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + + https://huobiapi.github.io/docs/spot/v1/en/#get-all-open-orders + https://huobiapi.github.io/docs/usdt_swap/v1/en/#isolated-current-unfilled-order-acquisition + https://huobiapi.github.io/docs/usdt_swap/v1/en/#cross-current-unfilled-order-acquisition + + fetch all unfilled currently open orders + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.trigger]: *contract only* if the orders are trigger trigger orders or not + :param bool [params.stopLossTakeProfit]: *contract only* if the orders are stop-loss or take-profit orders + :param boolean [params.trailing]: *contract only* set to True if you want to fetch trailing stop orders + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + request: dict = {} + marketType = None + marketType, params = self.handle_market_type_and_params('fetchOpenOrders', market, params) + subType = None + subType, params = self.handle_sub_type_and_params('fetchOpenOrders', market, params, 'linear') + response = None + if marketType == 'spot': + if symbol is not None: + request['symbol'] = market['id'] + # todo replace with fetchAccountIdByType + accountId = self.safe_string(params, 'account-id') + if accountId is None: + # pick the first account + self.load_accounts() + for i in range(0, len(self.accounts)): + account = self.accounts[i] + if self.safe_string(account, 'type') == 'spot': + accountId = self.safe_string(account, 'id') + if accountId is not None: + break + request['account-id'] = accountId + if limit is not None: + request['size'] = limit + params = self.omit(params, 'account-id') + response = self.spotPrivateGetV1OrderOpenOrders(self.extend(request, params)) + else: + if symbol is not None: + # raise ArgumentsRequired(self.id + ' fetchOpenOrders() requires a symbol argument') + request['contract_code'] = market['id'] + if limit is not None: + request['page_size'] = limit + trigger = self.safe_bool_2(params, 'stop', 'trigger') + stopLossTakeProfit = self.safe_value(params, 'stopLossTakeProfit') + trailing = self.safe_bool(params, 'trailing', False) + params = self.omit(params, ['stop', 'stopLossTakeProfit', 'trailing', 'trigger']) + if subType == 'linear': + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchOpenOrders', params) + marginMode = 'cross' if (marginMode is None) else marginMode + if marginMode == 'isolated': + if trigger: + response = self.contractPrivatePostLinearSwapApiV1SwapTriggerOpenorders(self.extend(request, params)) + elif stopLossTakeProfit: + response = self.contractPrivatePostLinearSwapApiV1SwapTpslOpenorders(self.extend(request, params)) + elif trailing: + response = self.contractPrivatePostLinearSwapApiV1SwapTrackOpenorders(self.extend(request, params)) + else: + response = self.contractPrivatePostLinearSwapApiV1SwapOpenorders(self.extend(request, params)) + elif marginMode == 'cross': + if trigger: + response = self.contractPrivatePostLinearSwapApiV1SwapCrossTriggerOpenorders(self.extend(request, params)) + elif stopLossTakeProfit: + response = self.contractPrivatePostLinearSwapApiV1SwapCrossTpslOpenorders(self.extend(request, params)) + elif trailing: + response = self.contractPrivatePostLinearSwapApiV1SwapCrossTrackOpenorders(self.extend(request, params)) + else: + response = self.contractPrivatePostLinearSwapApiV1SwapCrossOpenorders(self.extend(request, params)) + elif subType == 'inverse': + if marketType == 'swap': + if trigger: + response = self.contractPrivatePostSwapApiV1SwapTriggerOpenorders(self.extend(request, params)) + elif stopLossTakeProfit: + response = self.contractPrivatePostSwapApiV1SwapTpslOpenorders(self.extend(request, params)) + elif trailing: + response = self.contractPrivatePostSwapApiV1SwapTrackOpenorders(self.extend(request, params)) + else: + response = self.contractPrivatePostSwapApiV1SwapOpenorders(self.extend(request, params)) + elif marketType == 'future': + request['symbol'] = self.safe_string(market, 'settleId', 'usdt') + if trigger: + response = self.contractPrivatePostApiV1ContractTriggerOpenorders(self.extend(request, params)) + elif stopLossTakeProfit: + response = self.contractPrivatePostApiV1ContractTpslOpenorders(self.extend(request, params)) + elif trailing: + response = self.contractPrivatePostApiV1ContractTrackOpenorders(self.extend(request, params)) + else: + response = self.contractPrivatePostApiV1ContractOpenorders(self.extend(request, params)) + # + # spot + # + # { + # "status":"ok", + # "data":[ + # { + # "symbol":"ethusdt", + # "source":"api", + # "amount":"0.010000000000000000", + # "account-id":1528640, + # "created-at":1561597491963, + # "price":"400.000000000000000000", + # "filled-amount":"0.0", + # "filled-cash-amount":"0.0", + # "filled-fees":"0.0", + # "id":38477101630, + # "state":"submitted", + # "type":"sell-limit" + # } + # ] + # } + # + # futures + # + # { + # "status": "ok", + # "data": { + # "orders": [ + # { + # "symbol": "ADA", + # "contract_code": "ADA201225", + # "contract_type": "quarter", + # "volume": 1, + # "price": 0.0925, + # "order_price_type": "post_only", + # "order_type": 1, + # "direction": "buy", + # "offset": "close", + # "lever_rate": 20, + # "order_id": 773131315209248768, + # "client_order_id": null, + # "created_at": 1604370469629, + # "trade_volume": 0, + # "trade_turnover": 0, + # "fee": 0, + # "trade_avg_price": null, + # "margin_frozen": 0, + # "profit": 0, + # "status": 3, + # "order_source": "web", + # "order_id_str": "773131315209248768", + # "fee_asset": "ADA", + # "liquidation_type": null, + # "canceled_at": null, + # "is_tpsl": 0, + # "update_time": 1606975980467, + # "real_profit": 0 + # } + # ], + # "total_page": 1, + # "current_page": 1, + # "total_size": 1 + # }, + # "ts": 1604370488518 + # } + # + # trigger + # + # { + # "status": "ok", + # "data": { + # "orders": [ + # { + # "contract_type": "swap", + # "business_type": "swap", + # "pair": "BTC-USDT", + # "symbol": "BTC", + # "contract_code": "BTC-USDT", + # "trigger_type": "le", + # "volume": 1.000000000000000000, + # "order_type": 1, + # "direction": "buy", + # "offset": "open", + # "lever_rate": 1, + # "order_id": 1103670703588327424, + # "order_id_str": "1103670703588327424", + # "order_source": "web", + # "trigger_price": 25000.000000000000000000, + # "order_price": 24000.000000000000000000, + # "created_at": 1683177200945, + # "order_price_type": "limit", + # "status": 2, + # "margin_mode": "cross", + # "margin_account": "USDT", + # "trade_partition": "USDT", + # "reduce_only": 0 + # } + # ], + # "total_page": 1, + # "current_page": 1, + # "total_size": 1 + # }, + # "ts": 1683177805320 + # } + # + # stop-loss and take-profit + # + # { + # "status": "ok", + # "data": { + # "orders": [ + # { + # "contract_type": "swap", + # "business_type": "swap", + # "pair": "BTC-USDT", + # "symbol": "BTC", + # "contract_code": "BTC-USDT", + # "margin_mode": "cross", + # "margin_account": "USDT", + # "volume": 1.000000000000000000, + # "order_type": 1, + # "direction": "sell", + # "order_id": 1103680386844839936, + # "order_id_str": "1103680386844839936", + # "order_source": "web", + # "trigger_type": "le", + # "trigger_price": 25000.000000000000000000, + # "order_price": 0E-18, + # "created_at": 1683179509613, + # "order_price_type": "market", + # "status": 2, + # "tpsl_order_type": "sl", + # "source_order_id": null, + # "relation_tpsl_order_id": "-1", + # "trade_partition": "USDT" + # } + # ], + # "total_page": 1, + # "current_page": 1, + # "total_size": 1 + # }, + # "ts": 1683179527011 + # } + # + # trailing + # + # { + # "status": "ok", + # "data": { + # "orders": [ + # { + # "contract_type": "swap", + # "business_type": "swap", + # "pair": "BTC-USDT", + # "symbol": "BTC", + # "contract_code": "BTC-USDT", + # "volume": 1.000000000000000000, + # "order_type": 1, + # "direction": "sell", + # "offset": "close", + # "lever_rate": 1, + # "order_id": 1192021437253877761, + # "order_id_str": "1192021437253877761", + # "order_source": "api", + # "created_at": 1704241657328, + # "order_price_type": "formula_price", + # "status": 2, + # "callback_rate": 0.050000000000000000, + # "active_price": 50000.000000000000000000, + # "is_active": 0, + # "margin_mode": "cross", + # "margin_account": "USDT", + # "trade_partition": "USDT", + # "reduce_only": 1 + # }, + # ], + # "total_page": 1, + # "current_page": 1, + # "total_size": 2 + # }, + # "ts": 1704242440106 + # } + # + orders = self.safe_value(response, 'data') + if not isinstance(orders, list): + orders = self.safe_value(orders, 'orders', []) + return self.parse_orders(orders, market, since, limit) + + def parse_order_status(self, status: Str): + statuses: dict = { + # spot + 'partial-filled': 'open', + 'partial-canceled': 'canceled', + 'filled': 'closed', + 'canceled': 'canceled', + 'submitted': 'open', + 'created': 'open', # For stop orders + # contract + '1': 'open', + '2': 'open', + '3': 'open', + '4': 'open', + '5': 'canceled', # partially matched + '6': 'closed', + '7': 'canceled', + '11': 'canceling', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # spot + # + # { + # "id": 13997833014, + # "symbol": "ethbtc", + # "account-id": 3398321, + # "amount": "0.045000000000000000", + # "price": "0.034014000000000000", + # "created-at": 1545836976871, + # "type": "sell-limit", + # "field-amount": "0.045000000000000000", # they have fixed it for filled-amount + # "field-cash-amount": "0.001530630000000000", # they have fixed it for filled-cash-amount + # "field-fees": "0.000003061260000000", # they have fixed it for filled-fees + # "finished-at": 1545837948214, + # "source": "spot-api", + # "state": "filled", + # "canceled-at": 0 + # } + # + # { + # "id": 20395337822, + # "symbol": "ethbtc", + # "account-id": 5685075, + # "amount": "0.001000000000000000", + # "price": "0.0", + # "created-at": 1545831584023, + # "type": "buy-market", + # "field-amount": "0.029100000000000000", # they have fixed it for filled-amount + # "field-cash-amount": "0.000999788700000000", # they have fixed it for filled-cash-amount + # "field-fees": "0.000058200000000000", # they have fixed it for filled-fees + # "finished-at": 1545831584181, + # "source": "spot-api", + # "state": "filled", + # "canceled-at": 0 + # } + # + # linear swap cross margin createOrder + # + # { + # "order_id":924660854912552960, + # "order_id_str":"924660854912552960" + # } + # + # contracts fetchOrder + # + # { + # "business_type":"swap", + # "contract_type":"swap", + # "pair":"BTC-USDT", + # "symbol":"BTC", + # "contract_code":"BTC-USDT", + # "volume":1, + # "price":3000, + # "order_price_type":"limit", + # "order_type":1, + # "direction":"buy", + # "offset":"open", + # "lever_rate":1, + # "order_id":924912513206878210, + # "client_order_id":null, + # "created_at":1640557927189, + # "trade_volume":0, + # "trade_turnover":0, + # "fee":0, + # "trade_avg_price":null, + # "margin_frozen":3.000000000000000000, + # "profit":0, + # "status":3, + # "order_source":"api", + # "order_id_str":"924912513206878210", + # "fee_asset":"USDT", + # "liquidation_type":"0", + # "canceled_at":0, + # "margin_asset":"USDT", + # "margin_account":"USDT", + # "margin_mode":"cross", + # "is_tpsl":0, + # "real_profit":0 + # } + # + # contracts fetchOrder detailed + # + # { + # "symbol": "BTC", + # "contract_code": "BTC-USDT", + # "instrument_price": 0, + # "final_interest": 0, + # "adjust_value": 0, + # "lever_rate": 10, + # "direction": "sell", + # "offset": "open", + # "volume": 1.000000000000000000, + # "price": 13059.800000000000000000, + # "created_at": 1603703614712, + # "canceled_at": 0, + # "order_source": "api", + # "order_price_type": "opponent", + # "margin_frozen": 0, + # "profit": 0, + # "trades": [ + # { + # "trade_id": 131560927, + # "trade_price": 13059.800000000000000000, + # "trade_volume": 1.000000000000000000, + # "trade_turnover": 13.059800000000000000, + # "trade_fee": -0.005223920000000000, + # "created_at": 1603703614715, + # "role": "taker", + # "fee_asset": "USDT", + # "profit": 0, + # "real_profit": 0, + # "id": "131560927-770334322963152896-1" + # } + # ], + # "total_page": 1, + # "current_page": 1, + # "total_size": 1, + # "liquidation_type": "0", + # "fee_asset": "USDT", + # "fee": -0.005223920000000000, + # "order_id": 770334322963152896, + # "order_id_str": "770334322963152896", + # "client_order_id": 57012021045, + # "order_type": "1", + # "status": 6, + # "trade_avg_price": 13059.800000000000000000, + # "trade_turnover": 13.059800000000000000, + # "trade_volume": 1.000000000000000000, + # "margin_asset": "USDT", + # "margin_mode": "isolated", + # "margin_account": "BTC-USDT", + # "real_profit": 0, + # "is_tpsl": 0 + # } + # + # future and swap: fetchOrders + # + # { + # "order_id": 773131315209248768, + # "contract_code": "ADA201225", + # "symbol": "ADA", + # "lever_rate": 20, + # "direction": "buy", + # "offset": "close", + # "volume": 1, + # "price": 0.0925, + # "create_date": 1604370469629, + # "update_time": 1603704221118, + # "order_source": "web", + # "order_price_type": 6, + # "order_type": 1, + # "margin_frozen": 0, + # "profit": 0, + # "contract_type": "quarter", + # "trade_volume": 0, + # "trade_turnover": 0, + # "fee": 0, + # "trade_avg_price": 0, + # "status": 3, + # "order_id_str": "773131315209248768", + # "fee_asset": "ADA", + # "liquidation_type": "0", + # "is_tpsl": 0, + # "real_profit": 0 + # "margin_asset": "USDT", + # "margin_mode": "cross", + # "margin_account": "USDT", + # "trade_partition": "USDT", # only in isolated & cross of linear + # "reduce_only": "1", # only in isolated & cross of linear + # "contract_type": "quarter", # only in cross-margin(inverse & linear) + # "pair": "BTC-USDT", # only in cross-margin(inverse & linear) + # "business_type": "futures" # only in cross-margin(inverse & linear) + # } + # + # trigger: fetchOpenOrders + # + # { + # "contract_type": "swap", + # "business_type": "swap", + # "pair": "BTC-USDT", + # "symbol": "BTC", + # "contract_code": "BTC-USDT", + # "trigger_type": "le", + # "volume": 1.000000000000000000, + # "order_type": 1, + # "direction": "buy", + # "offset": "open", + # "lever_rate": 1, + # "order_id": 1103670703588327424, + # "order_id_str": "1103670703588327424", + # "order_source": "web", + # "trigger_price": 25000.000000000000000000, + # "order_price": 24000.000000000000000000, + # "created_at": 1683177200945, + # "order_price_type": "limit", + # "status": 2, + # "margin_mode": "cross", + # "margin_account": "USDT", + # "trade_partition": "USDT", + # "reduce_only": 0 + # } + # + # stop-loss and take-profit: fetchOpenOrders + # + # { + # "contract_type": "swap", + # "business_type": "swap", + # "pair": "BTC-USDT", + # "symbol": "BTC", + # "contract_code": "BTC-USDT", + # "margin_mode": "cross", + # "margin_account": "USDT", + # "volume": 1.000000000000000000, + # "order_type": 1, + # "direction": "sell", + # "order_id": 1103680386844839936, + # "order_id_str": "1103680386844839936", + # "order_source": "web", + # "trigger_type": "le", + # "trigger_price": 25000.000000000000000000, + # "order_price": 0E-18, + # "created_at": 1683179509613, + # "order_price_type": "market", + # "status": 2, + # "tpsl_order_type": "sl", + # "source_order_id": null, + # "relation_tpsl_order_id": "-1", + # "trade_partition": "USDT" + # } + # + # trailing: fetchOpenOrders + # + # { + # "contract_type": "swap", + # "business_type": "swap", + # "pair": "BTC-USDT", + # "symbol": "BTC", + # "contract_code": "BTC-USDT", + # "volume": 1.000000000000000000, + # "order_type": 1, + # "direction": "sell", + # "offset": "close", + # "lever_rate": 1, + # "order_id": 1192021437253877761, + # "order_id_str": "1192021437253877761", + # "order_source": "api", + # "created_at": 1704241657328, + # "order_price_type": "formula_price", + # "status": 2, + # "callback_rate": 0.050000000000000000, + # "active_price": 50000.000000000000000000, + # "is_active": 0, + # "margin_mode": "cross", + # "margin_account": "USDT", + # "trade_partition": "USDT", + # "reduce_only": 1 + # } + # + # trigger: fetchOrders + # + # { + # "contract_type": "swap", + # "business_type": "swap", + # "pair": "BTC-USDT", + # "symbol": "BTC", + # "contract_code": "BTC-USDT", + # "trigger_type": "le", + # "volume": 1.000000000000000000, + # "order_type": 1, + # "direction": "buy", + # "offset": "open", + # "lever_rate": 1, + # "order_id": 1103670703588327424, + # "order_id_str": "1103670703588327424", + # "relation_order_id": "-1", + # "order_price_type": "limit", + # "status": 6, + # "order_source": "web", + # "trigger_price": 25000.000000000000000000, + # "triggered_price": null, + # "order_price": 24000.000000000000000000, + # "created_at": 1683177200945, + # "triggered_at": null, + # "order_insert_at": 0, + # "canceled_at": 1683179075234, + # "fail_code": null, + # "fail_reason": null, + # "margin_mode": "cross", + # "margin_account": "USDT", + # "update_time": 1683179075958, + # "trade_partition": "USDT", + # "reduce_only": 0 + # } + # + # stop-loss and take-profit: fetchOrders + # + # { + # "contract_type": "swap", + # "business_type": "swap", + # "pair": "BTC-USDT", + # "symbol": "BTC", + # "contract_code": "BTC-USDT", + # "margin_mode": "cross", + # "margin_account": "USDT", + # "volume": 1.000000000000000000, + # "order_type": 1, + # "tpsl_order_type": "sl", + # "direction": "sell", + # "order_id": 1103680386844839936, + # "order_id_str": "1103680386844839936", + # "order_source": "web", + # "trigger_type": "le", + # "trigger_price": 25000.000000000000000000, + # "created_at": 1683179509613, + # "order_price_type": "market", + # "status": 11, + # "source_order_id": null, + # "relation_tpsl_order_id": "-1", + # "canceled_at": 0, + # "fail_code": null, + # "fail_reason": null, + # "triggered_price": null, + # "relation_order_id": "-1", + # "update_time": 1683179968231, + # "order_price": 0E-18, + # "trade_partition": "USDT" + # } + # + # spot: createOrders + # + # [ + # { + # "order-id": 936847569789079, + # "client-order-id": "AA03022abc3a55e82c-0087-4fc2-beac-112fdebb1ee9" + # }, + # { + # "client-order-id": "AA03022abcdb3baefb-3cfa-4891-8009-082b3d46ca82", + # "err-code": "account-frozen-balance-insufficient-error", + # "err-msg": "trade account balance is not enough, left: `89`" + # } + # ] + # + # swap and future: createOrders + # + # [ + # { + # "index": 2, + # "err_code": 1047, + # "err_msg": "Insufficient margin available." + # }, + # { + # "order_id": 1172923090632953857, + # "index": 1, + # "order_id_str": "1172923090632953857" + # } + # ] + # + rejectedCreateOrders = self.safe_string_2(order, 'err_code', 'err-code') + status = self.parse_order_status(self.safe_string_2(order, 'state', 'status')) + if rejectedCreateOrders is not None: + status = 'rejected' + id = self.safe_string_n(order, ['id', 'order_id_str', 'order-id']) + side = self.safe_string(order, 'direction') + type = self.safe_string(order, 'order_price_type') + if 'type' in order: + orderType = order['type'].split('-') + side = orderType[0] + type = orderType[1] + marketId = self.safe_string_2(order, 'contract_code', 'symbol') + market = self.safe_market(marketId, market) + timestamp = self.safe_integer_n(order, ['created_at', 'created-at', 'create_date']) + clientOrderId = self.safe_string_2(order, 'client_order_id', 'client-or' + 'der-id') # transpiler regex trick for php issue + cost = None + amount = None + if (type is not None) and (type.find('market') >= 0): + cost = self.safe_string(order, 'field-cash-amount') + else: + amount = self.safe_string_2(order, 'volume', 'amount') + cost = self.safe_string_n(order, ['filled-cash-amount', 'field-cash-amount', 'trade_turnover']) # same typo here + filled = self.safe_string_n(order, ['filled-amount', 'field-amount', 'trade_volume']) # typo in their API, filled amount + price = self.safe_string_2(order, 'price', 'order_price') + feeCost = self.safe_string_2(order, 'filled-fees', 'field-fees') # typo in their API, filled feeSide + feeCost = self.safe_string(order, 'fee', feeCost) + fee = None + if feeCost is not None: + feeCurrency = None + feeCurrencyId = self.safe_string(order, 'fee_asset') + if feeCurrencyId is not None: + feeCurrency = self.safe_currency_code(feeCurrencyId) + else: + feeCurrency = market['quote'] if (side == 'sell') else market['base'] + fee = { + 'cost': feeCost, + 'currency': feeCurrency, + } + average = self.safe_string(order, 'trade_avg_price') + trades = self.safe_value(order, 'trades') + reduceOnlyInteger = self.safe_integer(order, 'reduce_only') + reduceOnly = None + if reduceOnlyInteger is not None: + reduceOnly = False if (reduceOnlyInteger == 0) else True + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'symbol': market['symbol'], + 'type': type, + 'timeInForce': None, + 'postOnly': None, + 'side': side, + 'price': price, + 'triggerPrice': self.safe_string_2(order, 'stop-price', 'trigger_price'), + 'average': average, + 'cost': cost, + 'amount': amount, + 'filled': filled, + 'remaining': None, + 'status': status, + 'reduceOnly': reduceOnly, + 'fee': fee, + 'trades': trades, + }, market) + + def create_market_buy_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market buy order by providing the symbol and cost + + https://www.htx.com/en-us/opend/newApiPages/?id=7ec4ee16-7773-11ed-9966-0242ac110003 + + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' createMarketBuyOrderWithCost() supports spot orders only') + params['createMarketBuyOrderRequiresPrice'] = False + return self.create_order(symbol, 'market', 'buy', cost, None, params) + + def create_trailing_percent_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, trailingPercent: Num = None, trailingTriggerPrice: Num = None, params={}) -> Order: + """ + create a trailing order by providing the symbol, type, side, amount, price and trailingPercent + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency, or number of contracts + :param float [price]: the price for the order to be filled at, in units of the quote currency, ignored in market orders + :param float trailingPercent: the percent to trail away from the current market price + :param float trailingTriggerPrice: the price to activate a trailing order, default uses the price argument + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + if trailingPercent is None: + raise ArgumentsRequired(self.id + ' createTrailingPercentOrder() requires a trailingPercent argument') + if trailingTriggerPrice is None: + raise ArgumentsRequired(self.id + ' createTrailingPercentOrder() requires a trailingTriggerPrice argument') + params['trailingPercent'] = trailingPercent + params['trailingTriggerPrice'] = trailingTriggerPrice + return self.create_order(symbol, type, side, amount, price, params) + + def create_spot_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + @ignore + helper function to build request + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.timeInForce]: supports 'IOC' and 'FOK' + :param float [params.cost]: the quote quantity that can be used alternative for the amount for market buy orders + :returns dict: request to be sent to the exchange + """ + self.load_markets() + self.load_accounts() + market = self.market(symbol) + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('createOrder', params) + accountId = self.fetch_account_id_by_type(market['type'], marginMode, symbol) + request: dict = { + # spot ----------------------------------------------------------- + 'account-id': accountId, + 'symbol': market['id'], + # 'type': side + '-' + type, # buy-market, sell-market, buy-limit, sell-limit, buy-ioc, sell-ioc, buy-limit-maker, sell-limit-maker, buy-stop-limit, sell-stop-limit, buy-limit-fok, sell-limit-fok, buy-stop-limit-fok, sell-stop-limit-fok + # 'amount': self.amount_to_precision(symbol, amount), # for buy market orders it's the order cost + # 'price': self.price_to_precision(symbol, price), + # 'source': 'spot-api', # optional, spot-api, margin-api = isolated margin, super-margin-api = cross margin, c2c-margin-api + # 'client-order-id': clientOrderId, # optional, max 64 chars, must be unique within 8 hours + # 'stop-price': self.price_to_precision(symbol, stopPrice), # trigger price for stop limit orders + # 'operator': 'gte', # gte, lte, trigger price condition + } + orderType = type.replace('buy-', '') + orderType = orderType.replace('sell-', '') + options = self.safe_value(self.options, market['type'], {}) + triggerPrice = self.safe_string_n(params, ['triggerPrice', 'stopPrice', 'stop-price']) + if triggerPrice is None: + stopOrderTypes = self.safe_value(options, 'stopOrderTypes', {}) + if orderType in stopOrderTypes: + raise ArgumentsRequired(self.id + ' createOrder() requires a triggerPrice for a trigger order') + else: + defaultOperator = 'lte' if (side == 'sell') else 'gte' + stopOperator = self.safe_string(params, 'operator', defaultOperator) + request['stop-price'] = self.price_to_precision(symbol, triggerPrice) + request['operator'] = stopOperator + if (orderType == 'limit') or (orderType == 'limit-fok'): + orderType = 'stop-' + orderType + elif (orderType != 'stop-limit') and (orderType != 'stop-limit-fok'): + raise NotSupported(self.id + ' createOrder() does not support ' + type + ' orders') + postOnly = None + postOnly, params = self.handle_post_only(orderType == 'market', orderType == 'limit-maker', params) + if postOnly: + orderType = 'limit-maker' + timeInForce = self.safe_string(params, 'timeInForce', 'GTC') + if timeInForce == 'FOK': + orderType = orderType + '-fok' + elif timeInForce == 'IOC': + orderType = 'ioc' + request['type'] = side + '-' + orderType + clientOrderId = self.safe_string_2(params, 'clientOrderId', 'client-order-id') # must be 64 chars max and unique within 24 hours + if clientOrderId is None: + broker = self.safe_value(self.options, 'broker', {}) + brokerId = self.safe_string(broker, 'id') + request['client-order-id'] = brokerId + self.uuid() + else: + request['client-order-id'] = clientOrderId + if marginMode == 'cross': + request['source'] = 'super-margin-api' + elif marginMode == 'isolated': + request['source'] = 'margin-api' + elif marginMode == 'c2c': + request['source'] = 'c2c-margin-api' + if (orderType == 'market') and (side == 'buy'): + quoteAmount = None + createMarketBuyOrderRequiresPrice = True + createMarketBuyOrderRequiresPrice, params = self.handle_option_and_params(params, 'createOrder', 'createMarketBuyOrderRequiresPrice', True) + cost = self.safe_number(params, 'cost') + params = self.omit(params, 'cost') + if cost is not None: + quoteAmount = self.amount_to_precision(symbol, cost) + elif createMarketBuyOrderRequiresPrice: + if price is None: + raise InvalidOrder(self.id + ' createOrder() requires the price argument for market buy orders to calculate the total cost to spend(amount * price), alternatively set the createMarketBuyOrderRequiresPrice option or param to False and pass the cost to spend in the amount argument') + else: + # despite that cost = amount * price is in quote currency and should have quote precision + # the exchange API requires the cost supplied in 'amount' to be of base precision + # more about it here: + # https://github.com/ccxt/ccxt/pull/4395 + # https://github.com/ccxt/ccxt/issues/7611 + # we use amountToPrecision here because the exchange requires cost in base precision + amountString = self.number_to_string(amount) + priceString = self.number_to_string(price) + quoteAmount = self.amount_to_precision(symbol, Precise.string_mul(amountString, priceString)) + else: + quoteAmount = self.amount_to_precision(symbol, amount) + request['amount'] = quoteAmount + else: + request['amount'] = self.amount_to_precision(symbol, amount) + limitOrderTypes = self.safe_value(options, 'limitOrderTypes', {}) + if orderType in limitOrderTypes: + request['price'] = self.price_to_precision(symbol, price) + params = self.omit(params, ['triggerPrice', 'stopPrice', 'stop-price', 'clientOrderId', 'client-order-id', 'operator', 'timeInForce']) + return self.extend(request, params) + + def create_contract_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + @ignore + helper function to build request + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.timeInForce]: supports 'IOC' and 'FOK' + :param float [params.trailingPercent]: *contract only* the percent to trail away from the current market price + :param float [params.trailingTriggerPrice]: *contract only* the price to trigger a trailing order, default uses the price argument + :returns dict: request to be sent to the exchange + """ + market = self.market(symbol) + request: dict = { + 'contract_code': market['id'], + 'volume': self.amount_to_precision(symbol, amount), + 'direction': side, + } + postOnly = None + postOnly, params = self.handle_post_only(type == 'market', type == 'post_only', params) + if postOnly: + type = 'post_only' + timeInForce = self.safe_string(params, 'timeInForce', 'GTC') + if timeInForce == 'FOK': + type = 'fok' + elif timeInForce == 'IOC': + type = 'ioc' + triggerPrice = self.safe_number_n(params, ['triggerPrice', 'stopPrice', 'trigger_price']) + stopLossTriggerPrice = self.safe_number_2(params, 'stopLossPrice', 'sl_trigger_price') + takeProfitTriggerPrice = self.safe_number_2(params, 'takeProfitPrice', 'tp_trigger_price') + trailingPercent = self.safe_string_2(params, 'trailingPercent', 'callback_rate') + trailingTriggerPrice = self.safe_number(params, 'trailingTriggerPrice', price) + isTrailingPercentOrder = trailingPercent is not None + isTrigger = triggerPrice is not None + isStopLossTriggerOrder = stopLossTriggerPrice is not None + isTakeProfitTriggerOrder = takeProfitTriggerPrice is not None + if isTrigger: + triggerType = self.safe_string_2(params, 'triggerType', 'trigger_type', 'le') + request['trigger_type'] = triggerType + request['trigger_price'] = self.price_to_precision(symbol, triggerPrice) + if price is not None: + request['order_price'] = self.price_to_precision(symbol, price) + elif isStopLossTriggerOrder or isTakeProfitTriggerOrder: + if isStopLossTriggerOrder: + request['sl_order_price_type'] = type + request['sl_trigger_price'] = self.price_to_precision(symbol, stopLossTriggerPrice) + if price is not None: + request['sl_order_price'] = self.price_to_precision(symbol, price) + else: + request['tp_order_price_type'] = type + request['tp_trigger_price'] = self.price_to_precision(symbol, takeProfitTriggerPrice) + if price is not None: + request['tp_order_price'] = self.price_to_precision(symbol, price) + elif isTrailingPercentOrder: + trailingPercentString = Precise.string_div(trailingPercent, '100') + request['callback_rate'] = self.parse_to_numeric(trailingPercentString) + request['active_price'] = trailingTriggerPrice + request['order_price_type'] = self.safe_string(params, 'order_price_type', 'formula_price') + else: + clientOrderId = self.safe_integer_2(params, 'client_order_id', 'clientOrderId') + if clientOrderId is not None: + request['client_order_id'] = clientOrderId + params = self.omit(params, ['clientOrderId']) + if type == 'limit' or type == 'ioc' or type == 'fok' or type == 'post_only': + request['price'] = self.price_to_precision(symbol, price) + reduceOnly = self.safe_bool_2(params, 'reduceOnly', 'reduce_only', False) + if not isStopLossTriggerOrder and not isTakeProfitTriggerOrder: + if reduceOnly: + request['reduce_only'] = 1 + request['lever_rate'] = self.safe_integer_n(params, ['leverRate', 'lever_rate', 'leverage'], 1) + if not isTrailingPercentOrder: + request['order_price_type'] = type + hedged = self.safe_bool(params, 'hedged', False) + if hedged: + if reduceOnly: + request['offset'] = 'close' + else: + request['offset'] = 'open' + broker = self.safe_value(self.options, 'broker', {}) + brokerId = self.safe_string(broker, 'id') + request['channel_code'] = brokerId + params = self.omit(params, ['reduceOnly', 'triggerPrice', 'stopPrice', 'stopLossPrice', 'takeProfitPrice', 'triggerType', 'leverRate', 'timeInForce', 'leverage', 'trailingPercent', 'trailingTriggerPrice', 'hedged']) + return self.extend(request, params) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://huobiapi.github.io/docs/spot/v1/en/#place-a-new-order # spot, margin + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#place-an-order # coin-m swap + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#place-trigger-order # coin-m swap trigger + https://huobiapi.github.io/docs/usdt_swap/v1/en/#cross-place-an-order # usdt-m swap cross + https://huobiapi.github.io/docs/usdt_swap/v1/en/#cross-place-trigger-order # usdt-m swap cross trigger + https://huobiapi.github.io/docs/usdt_swap/v1/en/#isolated-place-an-order # usdt-m swap isolated + https://huobiapi.github.io/docs/usdt_swap/v1/en/#isolated-place-trigger-order # usdt-m swap isolated trigger + https://huobiapi.github.io/docs/usdt_swap/v1/en/#isolated-set-a-take-profit-and-stop-loss-order-for-an-existing-position + https://huobiapi.github.io/docs/usdt_swap/v1/en/#cross-set-a-take-profit-and-stop-loss-order-for-an-existing-position + https://huobiapi.github.io/docs/dm/v1/en/#place-an-order # coin-m futures + https://huobiapi.github.io/docs/dm/v1/en/#place-trigger-order # coin-m futures contract trigger + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: the price a trigger order is triggered at + :param str [params.triggerType]: *contract trigger orders only* ge: greater than or equal to, le: less than or equal to + :param float [params.stopLossPrice]: *contract only* the price a stop-loss order is triggered at + :param float [params.takeProfitPrice]: *contract only* the price a take-profit order is triggered at + :param str [params.operator]: *spot and margin only* gte or lte, trigger price condition + :param str [params.offset]: *contract only* 'both'(linear only), 'open', or 'close', required in hedge mode and for inverse markets + :param bool [params.postOnly]: *contract only* True or False + :param int [params.leverRate]: *contract only* required for all contract orders except tpsl, leverage greater than 20x requires prior approval of high-leverage agreement + :param str [params.timeInForce]: supports 'IOC' and 'FOK' + :param float [params.cost]: *spot market buy only* the quote quantity that can be used alternative for the amount + :param float [params.trailingPercent]: *contract only* the percent to trail away from the current market price + :param float [params.trailingTriggerPrice]: *contract only* the price to trigger a trailing order, default uses the price argument + :param bool [params.hedged]: *contract only* True for hedged mode, False for one way mode, default is False + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + triggerPrice = self.safe_number_n(params, ['triggerPrice', 'stopPrice', 'trigger_price']) + stopLossTriggerPrice = self.safe_number_2(params, 'stopLossPrice', 'sl_trigger_price') + takeProfitTriggerPrice = self.safe_number_2(params, 'takeProfitPrice', 'tp_trigger_price') + trailingPercent = self.safe_number(params, 'trailingPercent') + isTrailingPercentOrder = trailingPercent is not None + isTrigger = triggerPrice is not None + isStopLossTriggerOrder = stopLossTriggerPrice is not None + isTakeProfitTriggerOrder = takeProfitTriggerPrice is not None + response = None + if market['spot']: + if isTrailingPercentOrder: + raise NotSupported(self.id + ' createOrder() does not support trailing orders for spot markets') + spotRequest = self.create_spot_order_request(symbol, type, side, amount, price, params) + response = self.spotPrivatePostV1OrderOrdersPlace(spotRequest) + else: + contractRequest = self.create_contract_order_request(symbol, type, side, amount, price, params) + if market['linear']: + marginMode = None + marginMode, contractRequest = self.handle_margin_mode_and_params('createOrder', contractRequest) + marginMode = 'cross' if (marginMode is None) else marginMode + if marginMode == 'isolated': + if isTrigger: + response = self.contractPrivatePostLinearSwapApiV1SwapTriggerOrder(contractRequest) + elif isStopLossTriggerOrder or isTakeProfitTriggerOrder: + response = self.contractPrivatePostLinearSwapApiV1SwapTpslOrder(contractRequest) + elif isTrailingPercentOrder: + response = self.contractPrivatePostLinearSwapApiV1SwapTrackOrder(contractRequest) + else: + response = self.contractPrivatePostLinearSwapApiV1SwapOrder(contractRequest) + elif marginMode == 'cross': + if isTrigger: + response = self.contractPrivatePostLinearSwapApiV1SwapCrossTriggerOrder(contractRequest) + elif isStopLossTriggerOrder or isTakeProfitTriggerOrder: + response = self.contractPrivatePostLinearSwapApiV1SwapCrossTpslOrder(contractRequest) + elif isTrailingPercentOrder: + response = self.contractPrivatePostLinearSwapApiV1SwapCrossTrackOrder(contractRequest) + else: + response = self.contractPrivatePostLinearSwapApiV1SwapCrossOrder(contractRequest) + elif market['inverse']: + offset = self.safe_string(params, 'offset') + if offset is None: + raise ArgumentsRequired(self.id + ' createOrder() requires an extra parameter params["offset"] to be set to "open" or "close" when placing orders in inverse markets') + if market['swap']: + if isTrigger: + response = self.contractPrivatePostSwapApiV1SwapTriggerOrder(contractRequest) + elif isStopLossTriggerOrder or isTakeProfitTriggerOrder: + response = self.contractPrivatePostSwapApiV1SwapTpslOrder(contractRequest) + elif isTrailingPercentOrder: + response = self.contractPrivatePostSwapApiV1SwapTrackOrder(contractRequest) + else: + response = self.contractPrivatePostSwapApiV1SwapOrder(contractRequest) + elif market['future']: + if isTrigger: + response = self.contractPrivatePostApiV1ContractTriggerOrder(contractRequest) + elif isStopLossTriggerOrder or isTakeProfitTriggerOrder: + response = self.contractPrivatePostApiV1ContractTpslOrder(contractRequest) + elif isTrailingPercentOrder: + response = self.contractPrivatePostApiV1ContractTrackOrder(contractRequest) + else: + response = self.contractPrivatePostApiV1ContractOrder(contractRequest) + # + # spot + # + # {"status":"ok","data":"438398393065481"} + # + # swap and future + # + # { + # "status": "ok", + # "data": { + # "order_id": 924660854912552960, + # "order_id_str": "924660854912552960" + # }, + # "ts": 1640497927185 + # } + # + # stop-loss and take-profit + # + # { + # "status": "ok", + # "data": { + # "tp_order": { + # "order_id": 1101494204040163328, + # "order_id_str": "1101494204040163328" + # }, + # "sl_order": null + # }, + # "ts": :1682658283024 + # } + # + data = None + result = None + if market['spot']: + return self.safe_order({ + 'info': response, + 'id': self.safe_string(response, 'data'), + 'timestamp': None, + 'datetime': None, + 'lastTradeTimestamp': None, + 'status': None, + 'symbol': None, + 'type': type, + 'side': side, + 'price': price, + 'amount': amount, + 'filled': None, + 'remaining': None, + 'cost': None, + 'trades': None, + 'fee': None, + 'clientOrderId': None, + 'average': None, + }, market) + elif isStopLossTriggerOrder: + data = self.safe_value(response, 'data', {}) + result = self.safe_value(data, 'sl_order', {}) + elif isTakeProfitTriggerOrder: + data = self.safe_value(response, 'data', {}) + result = self.safe_value(data, 'tp_order', {}) + else: + result = self.safe_value(response, 'data', {}) + return self.parse_order(result, market) + + def create_orders(self, orders: List[OrderRequest], params={}): + """ + create a list of trade orders + + https://huobiapi.github.io/docs/spot/v1/en/#place-a-batch-of-orders + https://huobiapi.github.io/docs/dm/v1/en/#place-a-batch-of-orders + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#place-a-batch-of-orders + https://huobiapi.github.io/docs/usdt_swap/v1/en/#isolated-place-a-batch-of-orders + https://huobiapi.github.io/docs/usdt_swap/v1/en/#cross-place-a-batch-of-orders + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + ordersRequests = [] + symbol = None + market = None + marginMode = None + for i in range(0, len(orders)): + rawOrder = orders[i] + marketId = self.safe_string(rawOrder, 'symbol') + if symbol is None: + symbol = marketId + else: + if symbol != marketId: + raise BadRequest(self.id + ' createOrders() requires all orders to have the same symbol') + type = self.safe_string(rawOrder, 'type') + side = self.safe_string(rawOrder, 'side') + amount = self.safe_value(rawOrder, 'amount') + price = self.safe_value(rawOrder, 'price') + orderParams = self.safe_value(rawOrder, 'params', {}) + marginResult = self.handle_margin_mode_and_params('createOrders', orderParams) + currentMarginMode = marginResult[0] + if currentMarginMode is not None: + if marginMode is None: + marginMode = currentMarginMode + else: + if marginMode != currentMarginMode: + raise BadRequest(self.id + ' createOrders() requires all orders to have the same margin mode(isolated or cross)') + market = self.market(symbol) + orderRequest = None + if market['spot']: + orderRequest = self.create_spot_order_request(marketId, type, side, amount, price, orderParams) + else: + orderRequest = self.create_contract_order_request(marketId, type, side, amount, price, orderParams) + orderRequest = self.omit(orderRequest, 'marginMode') + ordersRequests.append(orderRequest) + request: dict = {} + response = None + if market['spot']: + response = self.privatePostOrderBatchOrders(ordersRequests) + else: + request['orders_data'] = ordersRequests + if market['linear']: + marginMode = 'cross' if (marginMode is None) else marginMode + if marginMode == 'isolated': + response = self.contractPrivatePostLinearSwapApiV1SwapBatchorder(request) + elif marginMode == 'cross': + response = self.contractPrivatePostLinearSwapApiV1SwapCrossBatchorder(request) + elif market['inverse']: + if market['swap']: + response = self.contractPrivatePostSwapApiV1SwapBatchorder(request) + elif market['future']: + response = self.contractPrivatePostApiV1ContractBatchorder(request) + # + # spot + # + # { + # "status": "ok", + # "data": [ + # { + # "order-id": 936847569789079, + # "client-order-id": "AA03022abc3a55e82c-0087-4fc2-beac-112fdebb1ee9" + # }, + # { + # "client-order-id": "AA03022abcdb3baefb-3cfa-4891-8009-082b3d46ca82", + # "err-code": "account-frozen-balance-insufficient-error", + # "err-msg": "trade account balance is not enough, left: `89`" + # } + # ] + # } + # + # swap and future + # + # { + # "status": "ok", + # "data": { + # "errors": [ + # { + # "index": 2, + # "err_code": 1047, + # "err_msg": "Insufficient margin available." + # } + # ], + # "success": [ + # { + # "order_id": 1172923090632953857, + # "index": 1, + # "order_id_str": "1172923090632953857" + # } + # ] + # }, + # "ts": 1699688256671 + # } + # + result = None + if market['spot']: + result = self.safe_value(response, 'data', []) + else: + data = self.safe_value(response, 'data', {}) + success = self.safe_value(data, 'success', []) + errors = self.safe_value(data, 'errors', []) + result = self.array_concat(success, errors) + return self.parse_orders(result, market) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: *contract only* if the order is a trigger trigger order or not + :param boolean [params.stopLossTakeProfit]: *contract only* if the order is a stop-loss or take-profit order + :param boolean [params.trailing]: *contract only* set to True if you want to cancel a trailing order + :returns dict: An `order structure ` + """ + self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + marketType = None + marketType, params = self.handle_market_type_and_params('cancelOrder', market, params) + request: dict = { + # spot ----------------------------------------------------------- + # 'order-id': 'id', + # 'symbol': market['id'], + # 'client-order-id': clientOrderId, + # contracts ------------------------------------------------------ + # 'order_id': id, + # 'client_order_id': clientOrderId, + # 'contract_code': market['id'], + # 'pair': 'BTC-USDT', + # 'contract_type': 'this_week', # swap, self_week, next_week, quarter, next_ quarter + } + response = None + if marketType == 'spot': + clientOrderId = self.safe_string_2(params, 'client-order-id', 'clientOrderId') + if clientOrderId is None: + request['order-id'] = id + response = self.spotPrivatePostV1OrderOrdersOrderIdSubmitcancel(self.extend(request, params)) + else: + request['client-order-id'] = clientOrderId + params = self.omit(params, ['client-order-id', 'clientOrderId']) + response = self.spotPrivatePostV1OrderOrdersSubmitCancelClientOrder(self.extend(request, params)) + else: + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + clientOrderId = self.safe_string_2(params, 'client_order_id', 'clientOrderId') + if clientOrderId is None: + request['order_id'] = id + else: + request['client_order_id'] = clientOrderId + params = self.omit(params, ['client_order_id', 'clientOrderId']) + if market['future']: + request['symbol'] = market['settleId'] + else: + request['contract_code'] = market['id'] + trigger = self.safe_bool_2(params, 'stop', 'trigger') + stopLossTakeProfit = self.safe_value(params, 'stopLossTakeProfit') + trailing = self.safe_bool(params, 'trailing', False) + params = self.omit(params, ['stop', 'stopLossTakeProfit', 'trailing', 'trigger']) + if market['linear']: + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('cancelOrder', params) + marginMode = 'cross' if (marginMode is None) else marginMode + if marginMode == 'isolated': + if trigger: + response = self.contractPrivatePostLinearSwapApiV1SwapTriggerCancel(self.extend(request, params)) + elif stopLossTakeProfit: + response = self.contractPrivatePostLinearSwapApiV1SwapTpslCancel(self.extend(request, params)) + elif trailing: + response = self.contractPrivatePostLinearSwapApiV1SwapTrackCancel(self.extend(request, params)) + else: + response = self.contractPrivatePostLinearSwapApiV1SwapCancel(self.extend(request, params)) + elif marginMode == 'cross': + if trigger: + response = self.contractPrivatePostLinearSwapApiV1SwapCrossTriggerCancel(self.extend(request, params)) + elif stopLossTakeProfit: + response = self.contractPrivatePostLinearSwapApiV1SwapCrossTpslCancel(self.extend(request, params)) + elif trailing: + response = self.contractPrivatePostLinearSwapApiV1SwapCrossTrackCancel(self.extend(request, params)) + else: + response = self.contractPrivatePostLinearSwapApiV1SwapCrossCancel(self.extend(request, params)) + elif market['inverse']: + if market['swap']: + if trigger: + response = self.contractPrivatePostSwapApiV1SwapTriggerCancel(self.extend(request, params)) + elif stopLossTakeProfit: + response = self.contractPrivatePostSwapApiV1SwapTpslCancel(self.extend(request, params)) + elif trailing: + response = self.contractPrivatePostSwapApiV1SwapTrackCancel(self.extend(request, params)) + else: + response = self.contractPrivatePostSwapApiV1SwapCancel(self.extend(request, params)) + elif market['future']: + if trigger: + response = self.contractPrivatePostApiV1ContractTriggerCancel(self.extend(request, params)) + elif stopLossTakeProfit: + response = self.contractPrivatePostApiV1ContractTpslCancel(self.extend(request, params)) + elif trailing: + response = self.contractPrivatePostApiV1ContractTrackCancel(self.extend(request, params)) + else: + response = self.contractPrivatePostApiV1ContractCancel(self.extend(request, params)) + else: + raise NotSupported(self.id + ' cancelOrder() does not support ' + marketType + ' markets') + # + # spot + # + # { + # "status": "ok", + # "data": "10138899000", + # } + # + # future and swap + # + # { + # "status": "ok", + # "data": { + # "errors": [], + # "successes": "924660854912552960" + # }, + # "ts": 1640504486089 + # } + # + return self.extend(self.parse_order(response, market), { + 'id': id, + 'status': 'canceled', + }) + + def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): + """ + cancel multiple orders + :param str[] ids: order ids + :param str symbol: unified market symbol, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.trigger]: *contract only* if the orders are trigger trigger orders or not + :param bool [params.stopLossTakeProfit]: *contract only* if the orders are stop-loss or take-profit orders + :returns dict: an list of `order structures ` + """ + self.load_markets() + market: Market = None + if symbol is not None: + market = self.market(symbol) + marketType = None + marketType, params = self.handle_market_type_and_params('cancelOrders', market, params) + request: dict = { + # spot ----------------------------------------------------------- + # 'order-ids': ','.join(ids), # max 50 + # 'client-order-ids': ','.join(ids), # max 50 + # contracts ------------------------------------------------------ + # 'order_id': id, # comma separated, max 10 + # 'client_order_id': clientOrderId, # comma separated, max 10 + # 'contract_code': market['id'], + # 'symbol': market['settleId'], + } + response = None + if marketType == 'spot': + clientOrderIds = self.safe_value_2(params, 'client-order-id', 'clientOrderId') + clientOrderIds = self.safe_value_2(params, 'client-order-ids', 'clientOrderIds', clientOrderIds) + if clientOrderIds is None: + if isinstance(clientOrderIds, str): + request['order-ids'] = [ids] + else: + request['order-ids'] = ids + else: + if isinstance(clientOrderIds, str): + request['client-order-ids'] = [clientOrderIds] + else: + request['client-order-ids'] = clientOrderIds + params = self.omit(params, ['client-order-id', 'client-order-ids', 'clientOrderId', 'clientOrderIds']) + response = self.spotPrivatePostV1OrderOrdersBatchcancel(self.extend(request, params)) + else: + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrders() requires a symbol argument') + clientOrderIds = self.safe_string_2(params, 'client_order_id', 'clientOrderId') + clientOrderIds = self.safe_string_2(params, 'client_order_ids', 'clientOrderIds', clientOrderIds) + if clientOrderIds is None: + request['order_id'] = ','.join(ids) + else: + request['client_order_id'] = clientOrderIds + params = self.omit(params, ['client_order_id', 'client_order_ids', 'clientOrderId', 'clientOrderIds']) + if market['future']: + request['symbol'] = market['settleId'] + else: + request['contract_code'] = market['id'] + trigger = self.safe_bool_2(params, 'stop', 'trigger') + stopLossTakeProfit = self.safe_value(params, 'stopLossTakeProfit') + params = self.omit(params, ['stop', 'stopLossTakeProfit', 'trigger']) + if market['linear']: + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('cancelOrders', params) + marginMode = 'cross' if (marginMode is None) else marginMode + if marginMode == 'isolated': + if trigger: + response = self.contractPrivatePostLinearSwapApiV1SwapTriggerCancel(self.extend(request, params)) + elif stopLossTakeProfit: + response = self.contractPrivatePostLinearSwapApiV1SwapTpslCancel(self.extend(request, params)) + else: + response = self.contractPrivatePostLinearSwapApiV1SwapCancel(self.extend(request, params)) + elif marginMode == 'cross': + if trigger: + response = self.contractPrivatePostLinearSwapApiV1SwapCrossTriggerCancel(self.extend(request, params)) + elif stopLossTakeProfit: + response = self.contractPrivatePostLinearSwapApiV1SwapCrossTpslCancel(self.extend(request, params)) + else: + response = self.contractPrivatePostLinearSwapApiV1SwapCrossCancel(self.extend(request, params)) + elif market['inverse']: + if market['swap']: + if trigger: + response = self.contractPrivatePostSwapApiV1SwapTriggerCancel(self.extend(request, params)) + elif stopLossTakeProfit: + response = self.contractPrivatePostSwapApiV1SwapTpslCancel(self.extend(request, params)) + else: + response = self.contractPrivatePostSwapApiV1SwapCancel(self.extend(request, params)) + elif market['future']: + if trigger: + response = self.contractPrivatePostApiV1ContractTriggerCancel(self.extend(request, params)) + elif stopLossTakeProfit: + response = self.contractPrivatePostApiV1ContractTpslCancel(self.extend(request, params)) + else: + response = self.contractPrivatePostApiV1ContractCancel(self.extend(request, params)) + else: + raise NotSupported(self.id + ' cancelOrders() does not support ' + marketType + ' markets') + # + # spot + # + # { + # "status": "ok", + # "data": { + # "success": [ + # "5983466" + # ], + # "failed": [ + # { + # "err-msg": "Incorrect order state", + # "order-state": 7, + # "order-id": "", + # "err-code": "order-orderstate-error", + # "client-order-id": "first" + # }, + # { + # "err-msg": "Incorrect order state", + # "order-state": 7, + # "order-id": "", + # "err-code": "order-orderstate-error", + # "client-order-id": "second" + # }, + # { + # "err-msg": "The record is not found.", + # "order-id": "", + # "err-code": "base-not-found", + # "client-order-id": "third" + # } + # ] + # } + # } + # + # future and swap + # + # { + # "status": "ok", + # "data": { + # "errors": [ + # { + # "order_id": "769206471845261312", + # "err_code": 1061, + # "err_msg": "This order doesnt exist." + # } + # ], + # "successes": "773120304138219520" + # }, + # "ts": 1604367997451 + # } + # + data = self.safe_dict(response, 'data') + return self.parse_cancel_orders(data) + + def parse_cancel_orders(self, orders): + # + # { + # "success": [ + # "5983466" + # ], + # "failed": [ + # { + # "err-msg": "Incorrect order state", + # "order-state": 7, + # "order-id": "", + # "err-code": "order-orderstate-error", + # "client-order-id": "first" + # }, + # ... + # ] + # } + # + # { + # "errors": [ + # { + # "order_id": "769206471845261312", + # "err_code": 1061, + # "err_msg": "This order doesnt exist." + # } + # ], + # "successes": "1258075374411399168,1258075393254871040" + # } + # + successes = self.safe_string(orders, 'successes') + success = None + if successes is not None: + success = successes.split(',') + else: + success = self.safe_list(orders, 'success', []) + failed = self.safe_list_2(orders, 'errors', 'failed', []) + result = [] + for i in range(0, len(success)): + order = success[i] + result.append(self.safe_order({ + 'info': order, + 'id': order, + 'status': 'canceled', + })) + for i in range(0, len(failed)): + order = failed[i] + result.append(self.safe_order({ + 'info': order, + 'id': self.safe_string_2(order, 'order-id', 'order_id'), + 'status': 'failed', + 'clientOrderId': self.safe_string(order, 'client-order-id'), + })) + return result + + def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders + :param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: *contract only* if the orders are trigger trigger orders or not + :param boolean [params.stopLossTakeProfit]: *contract only* if the orders are stop-loss or take-profit orders + :param boolean [params.trailing]: *contract only* set to True if you want to cancel all trailing orders + :returns dict[]: a list of `order structures ` + """ + self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + marketType = None + marketType, params = self.handle_market_type_and_params('cancelAllOrders', market, params) + request: dict = { + # spot ----------------------------------------------------------- + # 'account-id': account['id'], + # 'symbol': market['id'], # a list of comma-separated symbols, all symbols by default + # 'types' 'string', buy-market, sell-market, buy-limit, sell-limit, buy-ioc, sell-ioc, buy-stop-limit, sell-stop-limit, buy-limit-fok, sell-limit-fok, buy-stop-limit-fok, sell-stop-limit-fok + # 'side': 'buy', # or 'sell' + # 'size': 100, # the number of orders to cancel 1-100 + # contract ------------------------------------------------------- + # 'symbol': market['settleId'], # required + # 'contract_code': market['id'], + # 'contract_type': 'this_week', # swap, self_week, next_week, quarter, next_ quarter + # 'direction': 'buy': # buy, sell + # 'offset': 'open', # open, close + } + response = None + if marketType == 'spot': + if symbol is not None: + request['symbol'] = market['id'] + response = self.spotPrivatePostV1OrderOrdersBatchCancelOpenOrders(self.extend(request, params)) + # + # { + # "code": 200, + # "data": { + # "success-count": 2, + # "failed-count": 0, + # "next-id": 5454600 + # } + # } + # + data = self.safe_dict(response, 'data') + return [ + self.safe_order({ + 'info': data, + }), + ] + else: + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelAllOrders() requires a symbol argument') + if market['future']: + request['symbol'] = market['settleId'] + request['contract_code'] = market['id'] + trigger = self.safe_bool_2(params, 'stop', 'trigger') + stopLossTakeProfit = self.safe_value(params, 'stopLossTakeProfit') + trailing = self.safe_bool(params, 'trailing', False) + params = self.omit(params, ['stop', 'stopLossTakeProfit', 'trailing', 'trigger']) + if market['linear']: + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('cancelAllOrders', params) + marginMode = 'cross' if (marginMode is None) else marginMode + if marginMode == 'isolated': + if trigger: + response = self.contractPrivatePostLinearSwapApiV1SwapTriggerCancelall(self.extend(request, params)) + elif stopLossTakeProfit: + response = self.contractPrivatePostLinearSwapApiV1SwapTpslCancelall(self.extend(request, params)) + elif trailing: + response = self.contractPrivatePostLinearSwapApiV1SwapTrackCancelall(self.extend(request, params)) + else: + response = self.contractPrivatePostLinearSwapApiV1SwapCancelall(self.extend(request, params)) + elif marginMode == 'cross': + if trigger: + response = self.contractPrivatePostLinearSwapApiV1SwapCrossTriggerCancelall(self.extend(request, params)) + elif stopLossTakeProfit: + response = self.contractPrivatePostLinearSwapApiV1SwapCrossTpslCancelall(self.extend(request, params)) + elif trailing: + response = self.contractPrivatePostLinearSwapApiV1SwapCrossTrackCancelall(self.extend(request, params)) + else: + response = self.contractPrivatePostLinearSwapApiV1SwapCrossCancelall(self.extend(request, params)) + elif market['inverse']: + if market['swap']: + if trigger: + response = self.contractPrivatePostSwapApiV1SwapTriggerCancelall(self.extend(request, params)) + elif stopLossTakeProfit: + response = self.contractPrivatePostSwapApiV1SwapTpslCancelall(self.extend(request, params)) + elif trailing: + response = self.contractPrivatePostSwapApiV1SwapTrackCancelall(self.extend(request, params)) + else: + response = self.contractPrivatePostSwapApiV1SwapCancelall(self.extend(request, params)) + elif market['future']: + if trigger: + response = self.contractPrivatePostApiV1ContractTriggerCancelall(self.extend(request, params)) + elif stopLossTakeProfit: + response = self.contractPrivatePostApiV1ContractTpslCancelall(self.extend(request, params)) + elif trailing: + response = self.contractPrivatePostApiV1ContractTrackCancelall(self.extend(request, params)) + else: + response = self.contractPrivatePostApiV1ContractCancelall(self.extend(request, params)) + else: + raise NotSupported(self.id + ' cancelAllOrders() does not support ' + marketType + ' markets') + # + # { + # "status": "ok", + # "data": { + # "errors": [], + # "successes": "1104754904426696704" + # }, + # "ts": "1683435723755" + # } + # + data = self.safe_dict(response, 'data') + return self.parse_cancel_orders(data) + + def cancel_all_orders_after(self, timeout: Int, params={}): + """ + dead man's switch, cancel all orders after the given timeout + + https://huobiapi.github.io/docs/spot/v1/en/#dead-man-s-switch + + :param number timeout: time in milliseconds, 0 represents cancel the timer + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: the api result + """ + self.load_markets() + request: dict = { + 'timeout': self.parse_to_int(timeout / 1000) if (timeout > 0) else 0, + } + response = self.v2PrivatePostAlgoOrdersCancelAllAfter(self.extend(request, params)) + # + # { + # "code": 200, + # "message": "success", + # "data": { + # "currentTime": 1630491627230, + # "triggerTime": 1630491637230 + # } + # } + # + return response + + def parse_deposit_address(self, depositAddress, currency: Currency = None): + # + # { + # "currency": "usdt", + # "address": "0xf7292eb9ba7bc50358e27f0e025a4d225a64127b", + # "addressTag": "", + # "chain": "usdterc20", # trc20usdt, hrc20usdt, usdt, algousdt + # } + # + address = self.safe_string(depositAddress, 'address') + tag = self.safe_string(depositAddress, 'addressTag') + currencyId = self.safe_string(depositAddress, 'currency') + currency = self.safe_currency(currencyId, currency) + code = self.safe_currency_code(currencyId, currency) + note = self.safe_string(depositAddress, 'note') + networkId = self.safe_string(depositAddress, 'chain') + self.check_address(address) + return { + 'currency': code, + 'address': address, + 'tag': tag, + 'network': self.network_id_to_code(networkId), + 'note': note, + 'info': depositAddress, + } + + def fetch_deposit_addresses_by_network(self, code: str, params={}) -> List[DepositAddress]: + """ + + https://www.htx.com/en-us/opend/newApiPages/?id=7ec50029-7773-11ed-9966-0242ac110003 + + fetch a dictionary of addresses for a currency, indexed by network + :param str code: unified currency code of the currency for the deposit address + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `address structures ` indexed by the network + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + } + response = self.spotPrivateGetV2AccountDepositAddress(self.extend(request, params)) + # + # { + # "code": 200, + # "data": [ + # { + # "currency": "eth", + # "address": "0xf7292eb9ba7bc50358e27f0e025a4d225a64127b", + # "addressTag": "", + # "chain": "eth" + # } + # ] + # } + # + data = self.safe_value(response, 'data', []) + parsed = self.parse_deposit_addresses(data, [currency['code']], False) + return self.index_by(parsed, 'network') + + def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://www.htx.com/en-us/opend/newApiPages/?id=7ec50029-7773-11ed-9966-0242ac110003 + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + self.load_markets() + currency = self.currency(code) + networkCode, paramsOmited = self.handle_network_code_and_params(params) + indexedAddresses = self.fetch_deposit_addresses_by_network(code, paramsOmited) + selectedNetworkCode = self.select_network_code_from_unified_networks(currency['code'], networkCode, indexedAddresses) + return indexedAddresses[selectedNetworkCode] + + def fetch_withdraw_addresses(self, code: str, note=None, networkCode=None, params={}): + self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + } + response = self.spotPrivateGetV2AccountWithdrawAddress(self.extend(request, params)) + # + # { + # "code": 200, + # "data": [ + # { + # "currency": "eth", + # "chain": "eth" + # "note": "Binance - TRC20", + # "addressTag": "", + # "address": "0xf7292eb9ba7bc50358e27f0e025a4d225a64127b", + # } + # ] + # } + # + data = self.safe_value(response, 'data', []) + allAddresses = self.parse_deposit_addresses(data, [currency['code']], False) # cjg: to do remove self weird object or array ambiguity + addresses = [] + for i in range(0, len(allAddresses)): + address = allAddresses[i] + noteMatch = (note is None) or (address['note'] == note) + networkMatch = (networkCode is None) or (address['network'] == networkCode) + if noteMatch and networkMatch: + addresses.append(address) + return addresses + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + + https://www.htx.com/en-us/opend/newApiPages/?id=7ec4f050-7773-11ed-9966-0242ac110003 + + fetch all deposits made to an account + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + if limit is None or limit > 100: + limit = 100 + self.load_markets() + currency = None + if code is not None: + currency = self.currency(code) + request: dict = { + 'type': 'deposit', + 'direct': 'next', + 'from': 0, # From 'id' ... if you want to get results after a particular transaction id, pass the id in params.from + } + if currency is not None: + request['currency'] = currency['id'] + if limit is not None: + request['size'] = limit # max 100 + response = self.spotPrivateGetV1QueryDepositWithdraw(self.extend(request, params)) + # + # { + # "status": "ok", + # "data": [ + # { + # "id": "75115912", + # "type": "deposit", + # "sub-type": "NORMAL", + # "request-id": "trc20usdt-a2e229a44ef2a948c874366230bb56aa73631cc0a03d177bd8b4c9d38262d7ff-200", + # "currency": "usdt", + # "chain": "trc20usdt", + # "tx-hash": "a2e229a44ef2a948c874366230bb56aa73631cc0a03d177bd8b4c9d38262d7ff", + # "amount": "12.000000000000000000", + # "from-addr-tag": "", + # "address-id": "0", + # "address": "TRFTd1FxepQE6CnpwzUEMEbFaLm5bJK67s", + # "address-tag": "", + # "fee": "0", + # "state": "safe", + # "wallet-confirm": "2", + # "created-at": "1621843808662", + # "updated-at": "1621843857137" + # }, + # ] + # } + # + return self.parse_transactions(response['data'], currency, since, limit) + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://huobiapi.github.io/docs/spot/v1/en/#search-for-existed-withdraws-and-deposits + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + if limit is None or limit > 100: + limit = 100 + self.load_markets() + currency = None + if code is not None: + currency = self.currency(code) + request: dict = { + 'type': 'withdraw', + 'direct': 'next', + 'from': 0, # From 'id' ... if you want to get results after a particular transaction id, pass the id in params.from + } + if currency is not None: + request['currency'] = currency['id'] + if limit is not None: + request['size'] = limit # max 100 + response = self.spotPrivateGetV1QueryDepositWithdraw(self.extend(request, params)) + # + # { + # "status": "ok", + # "data": [ + # { + # "id": "61335312", + # "type": "withdraw", + # "sub-type": "NORMAL", + # "currency": "usdt", + # "chain": "trc20usdt", + # "tx-hash": "30a3111f2fead74fae45c6218ca3150fc33cab2aa59cfe41526b96aae79ce4ec", + # "amount": "12.000000000000000000", + # "from-addr-tag": "", + # "address-id": "27321591", + # "address": "TRf5JacJQRsF4Nm2zu11W6maDGeiEWQu9e", + # "address-tag": "", + # "fee": "1.000000000000000000", + # "state": "confirmed", + # "created-at": "1621852316553", + # "updated-at": "1621852467041" + # }, + # ] + # } + # + return self.parse_transactions(response['data'], currency, since, limit) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fetchDeposits + # + # { + # "id": "75115912", + # "type": "deposit", + # "sub-type": "NORMAL", + # "request-id": "trc20usdt-a2e229a44ef2a948c874366230bb56aa73631cc0a03d177bd8b4c9d38262d7ff-200", + # "currency": "usdt", + # "chain": "trc20usdt", + # "tx-hash": "a2e229a44ef2a948c874366230bb56aa73631cc0a03d177bd8b4c9d38262d7ff", + # "amount": "2849.000000000000000000", + # "from-addr-tag": "", + # "address-id": "0", + # "address": "TRFTd1FxepQE6CnpwzUEMEbFaLm5bJK67s", + # "address-tag": "", + # "fee": "0", + # "state": "safe", + # "wallet-confirm": "2", + # "created-at": "1621843808662", + # "updated-at": "1621843857137" + # }, + # + # fetchWithdrawals + # + # { + # "id": "61335312", + # "type": "withdraw", + # "sub-type": "NORMAL", + # "currency": "usdt", + # "chain": "trc20usdt", + # "tx-hash": "30a3111f2fead74fae45c6218ca3150fc33cab2aa59cfe41526b96aae79ce4ec", + # "amount": "12.000000000000000000", + # "from-addr-tag": "", + # "address-id": "27321591", + # "address": "TRf5JacJQRsF4Nm2zu11W6maDGeiEWQu9e", + # "address-tag": "", + # "fee": "1.000000000000000000", + # "state": "confirmed", + # "created-at": "1621852316553", + # "updated-at": "1621852467041" + # } + # + # withdraw + # + # { + # "status": "ok", + # "data": "99562054" + # } + # + timestamp = self.safe_integer(transaction, 'created-at') + code = self.safe_currency_code(self.safe_string(transaction, 'currency')) + type = self.safe_string(transaction, 'type') + if type == 'withdraw': + type = 'withdrawal' + feeCost = self.safe_string(transaction, 'fee') + if feeCost is not None: + feeCost = Precise.string_abs(feeCost) + networkId = self.safe_string(transaction, 'chain') + txHash = self.safe_string(transaction, 'tx-hash') + if networkId == 'ETH' and txHash.find('0x') < 0: + txHash = '0x' + txHash + subType = self.safe_string(transaction, 'sub-type') + internal = subType == 'FAST' + return { + 'info': transaction, + 'id': self.safe_string_2(transaction, 'id', 'data'), + 'txid': txHash, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': self.network_id_to_code(networkId), + 'address': self.safe_string(transaction, 'address'), + 'addressTo': None, + 'addressFrom': None, + 'tag': self.safe_string(transaction, 'address-tag'), + 'tagTo': None, + 'tagFrom': None, + 'type': type, + 'amount': self.safe_number(transaction, 'amount'), + 'currency': code, + 'status': self.parse_transaction_status(self.safe_string(transaction, 'state')), + 'updated': self.safe_integer(transaction, 'updated-at'), + 'comment': None, + 'internal': internal, + 'fee': { + 'currency': code, + 'cost': self.parse_number(feeCost), + 'rate': None, + }, + } + + def parse_transaction_status(self, status: Str): + statuses: dict = { + # deposit statuses + 'unknown': 'failed', + 'confirming': 'pending', + 'confirmed': 'ok', + 'safe': 'ok', + 'orphan': 'failed', + # withdrawal statuses + 'submitted': 'pending', + 'canceled': 'canceled', + 'reexamine': 'pending', + 'reject': 'failed', + 'pass': 'pending', + 'wallet-reject': 'failed', + # 'confirmed': 'ok', # present in deposit statuses + 'confirm-error': 'failed', + 'repealed': 'failed', + 'wallet-transfer': 'pending', + 'pre-transfer': 'pending', + 'verifying': 'pending', + } + return self.safe_string(statuses, status, status) + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + + https://www.htx.com/en-us/opend/newApiPages/?id=7ec4cc41-7773-11ed-9966-0242ac110003 + + make a withdrawal + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.load_markets() + self.check_address(address) + currency = self.currency(code) + request: dict = { + 'address': address, # only supports existing addresses in your withdraw address list + 'currency': currency['id'].lower(), + } + if tag is not None: + request['addr-tag'] = tag # only for XRP? + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + if networkCode is not None: + request['chain'] = self.network_code_to_id(networkCode, code) + amount = float(self.currency_to_precision(code, amount, networkCode)) + withdrawOptions = self.safe_value(self.options, 'withdraw', {}) + if self.safe_bool(withdrawOptions, 'includeFee', False): + fee = self.safe_number(params, 'fee') + if fee is None: + currencies = self.fetch_currencies() + self.currencies = self.map_to_safe_map(self.deep_extend(self.currencies, currencies)) + targetNetwork = self.safe_value(currency['networks'], networkCode, {}) + fee = self.safe_number(targetNetwork, 'fee') + if fee is None: + raise ArgumentsRequired(self.id + ' withdraw() function can not find withdraw fee for chosen network. You need to re-load markets with "exchange.loadMarkets(True)", or provide the "fee" parameter') + # fee needs to be deducted from whole amount + feeString = self.currency_to_precision(code, fee, networkCode) + params = self.omit(params, 'fee') + amountString = self.number_to_string(amount) + amountSubtractedString = Precise.string_sub(amountString, feeString) + amountSubtracted = float(amountSubtractedString) + request['fee'] = float(feeString) + amount = float(self.currency_to_precision(code, amountSubtracted, networkCode)) + request['amount'] = amount + response = self.spotPrivatePostV1DwWithdrawApiCreate(self.extend(request, params)) + # + # { + # "status": "ok", + # "data": "99562054" + # } + # + return self.parse_transaction(response, currency) + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + # + # transfer + # + # { + # "data": 12345, + # "status": "ok" + # } + # + id = self.safe_string(transfer, 'data') + code = self.safe_currency_code(None, currency) + return { + 'info': transfer, + 'id': id, + 'timestamp': None, + 'datetime': None, + 'currency': code, + 'amount': None, + 'fromAccount': None, + 'toAccount': None, + 'status': None, + } + + def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://huobiapi.github.io/docs/dm/v1/en/#transfer-margin-between-spot-account-and-future-account + https://huobiapi.github.io/docs/spot/v1/en/#transfer-fund-between-spot-account-and-future-contract-account + https://huobiapi.github.io/docs/usdt_swap/v1/en/#general-transfer-margin-between-spot-account-and-usdt-margined-contracts-account + https://huobiapi.github.io/docs/spot/v1/en/#transfer-asset-from-spot-trading-account-to-cross-margin-account-cross + https://huobiapi.github.io/docs/spot/v1/en/#transfer-asset-from-spot-trading-account-to-isolated-margin-account-isolated + https://huobiapi.github.io/docs/spot/v1/en/#transfer-asset-from-cross-margin-account-to-spot-trading-account-cross + https://huobiapi.github.io/docs/spot/v1/en/#transfer-asset-from-isolated-margin-account-to-spot-trading-account-isolated + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account to transfer from 'spot', 'future', 'swap' + :param str toAccount: account to transfer to 'spot', 'future', 'swap' + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.symbol]: used for isolated margin transfer + :param str [params.subType]: 'linear' or 'inverse', only used when transfering to/from swap accounts + :returns dict: a `transfer structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + 'amount': float(self.currency_to_precision(code, amount)), + } + subType = None + subType, params = self.handle_sub_type_and_params('transfer', None, params) + fromAccountId = self.convert_type_to_account(fromAccount) + toAccountId = self.convert_type_to_account(toAccount) + toCross = toAccountId == 'cross' + fromCross = fromAccountId == 'cross' + toIsolated = self.in_array(toAccountId, self.ids) + fromIsolated = self.in_array(fromAccountId, self.ids) + fromSpot = fromAccountId == 'pro' + toSpot = toAccountId == 'pro' + if fromSpot and toSpot: + raise BadRequest(self.id + ' transfer() cannot make a transfer between ' + fromAccount + ' and ' + toAccount) + fromOrToFuturesAccount = (fromAccountId == 'futures') or (toAccountId == 'futures') + response = None + if fromOrToFuturesAccount: + type = fromAccountId + '-to-' + toAccountId + type = self.safe_string(params, 'type', type) + request['type'] = type + response = self.spotPrivatePostV1FuturesTransfer(self.extend(request, params)) + elif fromSpot and toCross: + response = self.privatePostCrossMarginTransferIn(self.extend(request, params)) + elif fromCross and toSpot: + response = self.privatePostCrossMarginTransferOut(self.extend(request, params)) + elif fromSpot and toIsolated: + request['symbol'] = toAccountId + response = self.privatePostDwTransferInMargin(self.extend(request, params)) + elif fromIsolated and toSpot: + request['symbol'] = fromAccountId + response = self.privatePostDwTransferOutMargin(self.extend(request, params)) + else: + if subType == 'linear': + if (fromAccountId == 'swap') or (fromAccount == 'linear-swap'): + fromAccountId = 'linear-swap' + else: + toAccountId = 'linear-swap' + # check if cross-margin or isolated + symbol = self.safe_string(params, 'symbol') + params = self.omit(params, 'symbol') + if symbol is not None: + symbol = self.market_id(symbol) + request['margin-account'] = symbol + else: + request['margin-account'] = 'USDT' # cross-margin + request['from'] = 'spot' if fromSpot else fromAccountId + request['to'] = 'spot' if toSpot else toAccountId + response = self.v2PrivatePostAccountTransfer(self.extend(request, params)) + # + # { + # "code": "200", + # "data": "660150061", + # "message": "Succeed", + # "success": True, + # "print-log": True + # } + # + return self.parse_transfer(response, currency) + + def fetch_isolated_borrow_rates(self, params={}) -> IsolatedBorrowRates: + """ + fetch the borrow interest rates of all currencies + + https://huobiapi.github.io/docs/spot/v1/en/#get-loan-interest-rate-and-quota-isolated + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `isolated borrow rate structures ` + """ + self.load_markets() + response = self.spotPrivateGetV1MarginLoanInfo(params) + # + # { + # "status": "ok", + # "data": [ + # { + # "symbol": "1inchusdt", + # "currencies": [ + # { + # "currency": "1inch", + # "interest-rate": "0.00098", + # "min-loan-amt": "90.000000000000000000", + # "max-loan-amt": "1000.000000000000000000", + # "loanable-amt": "0.0", + # "actual-rate": "0.00098" + # }, + # { + # "currency": "usdt", + # "interest-rate": "0.00098", + # "min-loan-amt": "100.000000000000000000", + # "max-loan-amt": "1000.000000000000000000", + # "loanable-amt": "0.0", + # "actual-rate": "0.00098" + # } + # ] + # }, + # ... + # ] + # } + # + data = self.safe_value(response, 'data', []) + return self.parse_isolated_borrow_rates(data) + + def parse_isolated_borrow_rate(self, info: dict, market: Market = None) -> IsolatedBorrowRate: + # + # { + # "symbol": "1inchusdt", + # "currencies": [ + # { + # "currency": "1inch", + # "interest-rate": "0.00098", + # "min-loan-amt": "90.000000000000000000", + # "max-loan-amt": "1000.000000000000000000", + # "loanable-amt": "0.0", + # "actual-rate": "0.00098" + # }, + # { + # "currency": "usdt", + # "interest-rate": "0.00098", + # "min-loan-amt": "100.000000000000000000", + # "max-loan-amt": "1000.000000000000000000", + # "loanable-amt": "0.0", + # "actual-rate": "0.00098" + # } + # ] + # }, + # + marketId = self.safe_string(info, 'symbol') + symbol = self.safe_symbol(marketId, market) + currencies = self.safe_value(info, 'currencies', []) + baseData = self.safe_value(currencies, 0) + quoteData = self.safe_value(currencies, 1) + baseId = self.safe_string(baseData, 'currency') + quoteId = self.safe_string(quoteData, 'currency') + return { + 'symbol': symbol, + 'base': self.safe_currency_code(baseId), + 'baseRate': self.safe_number(baseData, 'actual-rate'), + 'quote': self.safe_currency_code(quoteId), + 'quoteRate': self.safe_number(quoteData, 'actual-rate'), + 'period': 86400000, + 'timestamp': None, + 'datetime': None, + 'info': info, + } + + def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + + https://huobiapi.github.io/docs/usdt_swap/v1/en/#general-query-historical-funding-rate + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#query-historical-funding-rate + + fetches historical funding rate prices + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: not used by huobi, but filtered internally by ccxt + :param int [limit]: not used by huobi, but filtered internally by ccxt + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `funding rate structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchFundingRateHistory() requires a symbol argument') + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchFundingRateHistory', 'paginate') + if paginate: + return self.fetch_paginated_call_cursor('fetchFundingRateHistory', symbol, since, limit, params, 'current_page', 'page_index', 1, 50) + self.load_markets() + market = self.market(symbol) + request: dict = { + 'contract_code': market['id'], + } + if limit is not None: + request['page_size'] = limit + else: + request['page_size'] = 50 # max + response = None + if market['inverse']: + response = self.contractPublicGetSwapApiV1SwapHistoricalFundingRate(self.extend(request, params)) + elif market['linear']: + response = self.contractPublicGetLinearSwapApiV1SwapHistoricalFundingRate(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchFundingRateHistory() supports inverse and linear swaps only') + # + # { + # "status": "ok", + # "data": { + # "total_page": 62, + # "current_page": 1, + # "total_size": 1237, + # "data": [ + # { + # "avg_premium_index": "-0.000208064395065541", + # "funding_rate": "0.000100000000000000", + # "realized_rate": "0.000100000000000000", + # "funding_time": "1638921600000", + # "contract_code": "BTC-USDT", + # "symbol": "BTC", + # "fee_asset": "USDT" + # }, + # ] + # }, + # "ts": 1638939294277 + # } + # + data = self.safe_value(response, 'data') + cursor = self.safe_value(data, 'current_page') + result = self.safe_value(data, 'data', []) + rates = [] + for i in range(0, len(result)): + entry = result[i] + entry['current_page'] = cursor + marketId = self.safe_string(entry, 'contract_code') + symbolInner = self.safe_symbol(marketId) + timestamp = self.safe_integer(entry, 'funding_time') + rates.append({ + 'info': entry, + 'symbol': symbolInner, + 'fundingRate': self.safe_number(entry, 'funding_rate'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + sorted = self.sort_by(rates, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, market['symbol'], since, limit) + + def parse_funding_rate(self, contract, market: Market = None) -> FundingRate: + # + # { + # "status": "ok", + # "data": { + # "estimated_rate": "0.000100000000000000", + # "funding_rate": "0.000100000000000000", + # "contract_code": "BCH-USD", + # "symbol": "BCH", + # "fee_asset": "BCH", + # "funding_time": "1639094400000", + # "next_funding_time": "1639123200000" + # }, + # "ts": 1639085854775 + # } + # + nextFundingRate = self.safe_number(contract, 'estimated_rate') + fundingTimestamp = self.safe_integer(contract, 'funding_time') + nextFundingTimestamp = self.safe_integer(contract, 'next_funding_time') + fundingTimeString = self.safe_string(contract, 'funding_time') + nextFundingTimeString = self.safe_string(contract, 'next_funding_time') + millisecondsInterval = Precise.string_sub(nextFundingTimeString, fundingTimeString) + marketId = self.safe_string(contract, 'contract_code') + symbol = self.safe_symbol(marketId, market) + return { + 'info': contract, + 'symbol': symbol, + 'markPrice': None, + 'indexPrice': None, + 'interestRate': None, + 'estimatedSettlePrice': None, + 'timestamp': None, + 'datetime': None, + 'fundingRate': self.safe_number(contract, 'funding_rate'), + 'fundingTimestamp': fundingTimestamp, + 'fundingDatetime': self.iso8601(fundingTimestamp), + 'nextFundingRate': nextFundingRate, + 'nextFundingTimestamp': nextFundingTimestamp, + 'nextFundingDatetime': self.iso8601(nextFundingTimestamp), + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': self.parse_funding_interval(millisecondsInterval), + } + + def parse_funding_interval(self, interval): + intervals: dict = { + '3600000': '1h', + '14400000': '4h', + '28800000': '8h', + '57600000': '16h', + '86400000': '24h', + } + return self.safe_string(intervals, interval, interval) + + def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate + + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#query-funding-rate + https://huobiapi.github.io/docs/usdt_swap/v1/en/#general-query-funding-rate + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'contract_code': market['id'], + } + response = None + if market['inverse']: + response = self.contractPublicGetSwapApiV1SwapFundingRate(self.extend(request, params)) + elif market['linear']: + response = self.contractPublicGetLinearSwapApiV1SwapFundingRate(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchFundingRate() supports inverse and linear swaps only') + # + # { + # "status": "ok", + # "data": { + # "estimated_rate": "0.000100000000000000", + # "funding_rate": "0.000100000000000000", + # "contract_code": "BTC-USDT", + # "symbol": "BTC", + # "fee_asset": "USDT", + # "funding_time": "1603699200000", + # "next_funding_time": "1603728000000" + # }, + # "ts": 1603696494714 + # } + # + result = self.safe_value(response, 'data', {}) + return self.parse_funding_rate(result, market) + + def fetch_funding_rates(self, symbols: Strings = None, params={}) -> FundingRates: + """ + fetch the funding rate for multiple markets + + https://huobiapi.github.io/docs/usdt_swap/v1/en/#general-query-a-batch-of-funding-rate + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#query-a-batch-of-funding-rate + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `funding rate structures `, indexed by market symbols + """ + self.load_markets() + symbols = self.market_symbols(symbols) + defaultSubType = self.safe_string(self.options, 'defaultSubType', 'linear') + subType = None + subType, params = self.handle_option_and_params(params, 'fetchFundingRates', 'subType', defaultSubType) + if symbols is not None: + firstSymbol = self.safe_string(symbols, 0) + market = self.market(firstSymbol) + isLinear = market['linear'] + subType = 'linear' if isLinear else 'inverse' + request: dict = { + # 'contract_code': market['id'], + } + response = None + if subType == 'linear': + response = self.contractPublicGetLinearSwapApiV1SwapBatchFundingRate(self.extend(request, params)) + elif subType == 'inverse': + response = self.contractPublicGetSwapApiV1SwapBatchFundingRate(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchFundingRates() not support self market type') + # + # { + # "status": "ok", + # "data": [ + # { + # "estimated_rate": "0.000100000000000000", + # "funding_rate": "0.000100000000000000", + # "contract_code": "MANA-USDT", + # "symbol": "MANA", + # "fee_asset": "USDT", + # "funding_time": "1643356800000", + # "next_funding_time": "1643385600000", + # "trade_partition":"USDT" + # }, + # ], + # "ts": 1643346173103 + # } + # + data = self.safe_value(response, 'data', []) + return self.parse_funding_rates(data, symbols) + + def fetch_borrow_interest(self, code: Str = None, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[BorrowInterest]: + """ + fetch the interest owed by the user for borrowing currency for margin trading + + https://huobiapi.github.io/docs/spot/v1/en/#search-past-margin-orders-cross + https://huobiapi.github.io/docs/spot/v1/en/#search-past-margin-orders-isolated + + :param str code: unified currency code + :param str symbol: unified market symbol when fetch interest in isolated markets + :param int [since]: the earliest time in ms to fetch borrrow interest for + :param int [limit]: the maximum number of structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `borrow interest structures ` + """ + self.load_markets() + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchBorrowInterest', params) + marginMode = 'cross' if (marginMode is None) else marginMode + request: dict = {} + if since is not None: + request['start-date'] = self.yyyymmdd(since) + if limit is not None: + request['size'] = limit + market = None + response = None + if marginMode == 'isolated': + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + response = self.privateGetMarginLoanOrders(self.extend(request, params)) + else: # Cross + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + response = self.privateGetCrossMarginLoanOrders(self.extend(request, params)) + # + # { + # "status":"ok", + # "data":[ + # { + # "loan-balance":"0.100000000000000000", + # "interest-balance":"0.000200000000000000", + # "loan-amount":"0.100000000000000000", + # "accrued-at":1511169724531, + # "interest-amount":"0.000200000000000000", + # "filled-points":"0.2", + # "filled-ht":"0.2", + # "currency":"btc", + # "id":394, + # "state":"accrual", + # "account-id":17747, + # "user-id":119913, + # "created-at":1511169724531 + # } + # ] + # } + # + data = self.safe_value(response, 'data') + interest = self.parse_borrow_interests(data, market) + return self.filter_by_currency_since_limit(interest, code, since, limit) + + def parse_borrow_interest(self, info: dict, market: Market = None) -> BorrowInterest: + # isolated + # { + # "interest-rate":"0.000040830000000000", + # "user-id":35930539, + # "account-id":48916071, + # "updated-at":1649320794195, + # "deduct-rate":"1", + # "day-interest-rate":"0.000980000000000000", + # "hour-interest-rate":"0.000040830000000000", + # "loan-balance":"100.790000000000000000", + # "interest-balance":"0.004115260000000000", + # "loan-amount":"100.790000000000000000", + # "paid-coin":"0.000000000000000000", + # "accrued-at":1649320794148, + # "created-at":1649320794148, + # "interest-amount":"0.004115260000000000", + # "deduct-amount":"0", + # "deduct-currency":"", + # "paid-point":"0.000000000000000000", + # "currency":"usdt", + # "symbol":"ltcusdt", + # "id":20242721, + # } + # + # cross + # { + # "id":3416576, + # "user-id":35930539, + # "account-id":48956839, + # "currency":"usdt", + # "loan-amount":"102", + # "loan-balance":"102", + # "interest-amount":"0.00416466", + # "interest-balance":"0.00416466", + # "created-at":1649322735333, + # "accrued-at":1649322735382, + # "state":"accrual", + # "filled-points":"0", + # "filled-ht":"0" + # } + # + marketId = self.safe_string(info, 'symbol') + marginMode = 'cross' if (marketId is None) else 'isolated' + market = self.safe_market(marketId) + symbol = self.safe_string(market, 'symbol') + timestamp = self.safe_integer(info, 'accrued-at') + return { + 'info': info, + 'symbol': symbol, + 'currency': self.safe_currency_code(self.safe_string(info, 'currency')), + 'interest': self.safe_number(info, 'interest-amount'), + 'interestRate': self.safe_number(info, 'interest-rate'), + 'amountBorrowed': self.safe_number(info, 'loan-amount'), + 'marginMode': marginMode, + 'timestamp': timestamp, # Interest accrued time + 'datetime': self.iso8601(timestamp), + } + + def nonce(self): + return self.milliseconds() - self.options['timeDifference'] + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = '/' + query = self.omit(params, self.extract_params(path)) + if isinstance(api, str): + # signing implementation for the old endpoints + if (api == 'public') or (api == 'private'): + url += self.version + elif (api == 'v2Public') or (api == 'v2Private'): + url += 'v2' + url += '/' + self.implode_params(path, params) + if api == 'private' or api == 'v2Private': + self.check_required_credentials() + timestamp = self.ymdhms(self.nonce(), 'T') + request: dict = { + 'SignatureMethod': 'HmacSHA256', + 'SignatureVersion': '2', + 'AccessKeyId': self.apiKey, + 'Timestamp': timestamp, + } + if method != 'POST': + request = self.extend(request, query) + sortedRequest = self.keysort(request) + auth = self.urlencode(sortedRequest, True) # True is a go only requirment + # unfortunately, PHP demands double quotes for the escaped newline symbol + payload = "\n".join([method, self.hostname, url, auth]) # eslint-disable-line quotes + signature = self.hmac(self.encode(payload), self.encode(self.secret), hashlib.sha256, 'base64') + auth += '&' + self.urlencode({'Signature': signature}) + url += '?' + auth + if method == 'POST': + body = self.json(query) + headers = { + 'Content-Type': 'application/json', + } + else: + headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + } + else: + if query: + url += '?' + self.urlencode(query) + url = self.implode_params(self.urls['api'][api], { + 'hostname': self.hostname, + }) + url + else: + # signing implementation for the new endpoints + # type, access = api + type = self.safe_string(api, 0) + access = self.safe_string(api, 1) + levelOneNestedPath = self.safe_string(api, 2) + levelTwoNestedPath = self.safe_string(api, 3) + hostname = None + hostnames = self.safe_value(self.urls['hostnames'], type) + if not isinstance(hostnames, str): + hostnames = self.safe_value(hostnames, levelOneNestedPath) + if (not isinstance(hostnames, str)) and (levelTwoNestedPath is not None): + hostnames = self.safe_value(hostnames, levelTwoNestedPath) + hostname = hostnames + url += self.implode_params(path, params) + if access == 'public': + if query: + url += '?' + self.urlencode(query) + elif access == 'private': + self.check_required_credentials() + if method == 'POST': + options = self.safe_value(self.options, 'broker', {}) + id = self.safe_string(options, 'id', 'AA03022abc') + if path.find('cancel') == -1 and path.endswith('order'): + # swap order placement + channelCode = self.safe_string(params, 'channel_code') + if channelCode is None: + params['channel_code'] = id + elif path.endswith('orders/place'): + # spot order placement + clientOrderId = self.safe_string(params, 'client-order-id') + if clientOrderId is None: + params['client-order-id'] = id + self.uuid() + timestamp = self.ymdhms(self.nonce(), 'T') + request: dict = { + 'SignatureMethod': 'HmacSHA256', + 'SignatureVersion': '2', + 'AccessKeyId': self.apiKey, + 'Timestamp': timestamp, + } + # sorting needs such flow exactly, before urlencoding(more at: https://github.com/ccxt/ccxt/issues/24930 ) + request = self.keysort(request) + if method != 'POST': + sortedQuery = self.keysort(query) + request = self.extend(request, sortedQuery) + auth = self.urlencode(request, True).replace('%2c', '%2C') # in c# it manually needs to be uppercased + # unfortunately, PHP demands double quotes for the escaped newline symbol + payload = "\n".join([method, hostname, url, auth]) # eslint-disable-line quotes + signature = self.hmac(self.encode(payload), self.encode(self.secret), hashlib.sha256, 'base64') + auth += '&' + self.urlencode({'Signature': signature}) + url += '?' + auth + if method == 'POST': + body = self.json(query) + if len(body) == 2: + body = '{}' + headers = { + 'Content-Type': 'application/json', + } + else: + headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + } + url = self.implode_params(self.urls['api'][type], { + 'hostname': hostname, + }) + url + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None # fallback to default error handler + if 'status' in response: + # + # {"status":"error","err-code":"order-limitorder-amount-min-error","err-msg":"limit order amount error, min: `0.001`","data":null} + # {"status":"ok","data":{"errors":[{"order_id":"1349442392365359104","err_code":1061,"err_msg":"The order does not exist."}],"successes":""},"ts":1741773744526} + # + status = self.safe_string(response, 'status') + if status == 'error': + code = self.safe_string_2(response, 'err-code', 'err_code') + feedback = self.id + ' ' + body + self.throw_broadly_matched_exception(self.exceptions['broad'], body, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], code, feedback) + message = self.safe_string_2(response, 'err-msg', 'err_msg') + self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) + raise ExchangeError(feedback) + if 'code' in response: + # {code: '1003', message: 'invalid signature'} + feedback = self.id + ' ' + body + code = self.safe_string(response, 'code') + self.throw_exactly_matched_exception(self.exceptions['exact'], code, feedback) + data = self.safe_dict(response, 'data') + errorsList = self.safe_list(data, 'errors') + if errorsList is not None: + first = self.safe_dict(errorsList, 0) + errcode = self.safe_string(first, 'err_code') + errmessage = self.safe_string(first, 'err_msg') + feedBack = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], errcode, feedBack) + self.throw_exactly_matched_exception(self.exceptions['exact'], errmessage, feedBack) + return None + + def fetch_funding_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch the history of funding payments paid and received on self account + + https://huobiapi.github.io/docs/usdt_swap/v1/en/#general-query-account-financial-records-via-multiple-fields-new # linear swaps + https://huobiapi.github.io/docs/dm/v1/en/#query-financial-records-via-multiple-fields-new # coin-m futures + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#query-financial-records-via-multiple-fields-new # coin-m swaps + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch funding history for + :param int [limit]: the maximum number of funding history structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding history structure ` + """ + self.load_markets() + market = self.market(symbol) + marketType, query = self.handle_market_type_and_params('fetchFundingHistory', market, params) + request: dict = { + 'type': '30,31', + } + if since is not None: + request['start_date'] = since + response = None + if marketType == 'swap': + request['contract'] = market['id'] + if market['linear']: + # + # { + # "status": "ok", + # "data": { + # "financial_record": [ + # { + # "id": "1320088022", + # "type": "30", + # "amount": "0.004732510000000000", + # "ts": "1641168019321", + # "contract_code": "BTC-USDT", + # "asset": "USDT", + # "margin_account": "BTC-USDT", + # "face_margin_account": '' + # }, + # ], + # "remain_size": "0", + # "next_id": null + # }, + # "ts": "1641189898425" + # } + # + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchFundingHistory', params) + marginMode = 'cross' if (marginMode is None) else marginMode + if marginMode == 'isolated': + request['mar_acct'] = market['id'] + else: + request['mar_acct'] = market['quoteId'] + response = self.contractPrivatePostLinearSwapApiV3SwapFinancialRecordExact(self.extend(request, query)) + else: + # + # { + # "code": 200, + # "msg": "", + # "data": [ + # { + # "query_id": 138798248, + # "id": 117840, + # "type": 5, + # "amount": -0.024464850000000000, + # "ts": 1638758435635, + # "contract_code": "BTC-USDT-211210", + # "asset": "USDT", + # "margin_account": "USDT", + # "face_margin_account": "" + # } + # ], + # "ts": 1604312615051 + # } + # + response = self.contractPrivatePostSwapApiV3SwapFinancialRecordExact(self.extend(request, query)) + else: + request['symbol'] = market['id'] + response = self.contractPrivatePostApiV3ContractFinancialRecordExact(self.extend(request, query)) + data = self.safe_list(response, 'data', []) + return self.parse_incomes(data, market, since, limit) + + def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://huobiapi.github.io/docs/usdt_swap/v1/en/#isolated-switch-leverage + https://huobiapi.github.io/docs/usdt_swap/v1/en/#cross-switch-leverage + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#switch-leverage + https://huobiapi.github.io/docs/dm/v1/en/#switch-leverage # Coin-m futures + + :param float leverage: the rate of leverage + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setLeverage() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + marketType, query = self.handle_market_type_and_params('setLeverage', market, params) + request: dict = { + 'lever_rate': leverage, + } + if marketType == 'future' and market['inverse']: + request['symbol'] = market['settleId'] + else: + request['contract_code'] = market['id'] + response = None + if market['linear']: + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('setLeverage', params) + marginMode = 'cross' if (marginMode is None) else marginMode + if marginMode == 'isolated': + response = self.contractPrivatePostLinearSwapApiV1SwapSwitchLeverRate(self.extend(request, query)) + elif marginMode == 'cross': + response = self.contractPrivatePostLinearSwapApiV1SwapCrossSwitchLeverRate(self.extend(request, query)) + else: + raise NotSupported(self.id + ' setLeverage() not support self market type') + # + # { + # "status": "ok", + # "data": { + # "contract_code": "BTC-USDT", + # "lever_rate": "100", + # "margin_mode": "isolated" + # }, + # "ts": "1641184710649" + # } + # + else: + if marketType == 'future': + response = self.contractPrivatePostApiV1ContractSwitchLeverRate(self.extend(request, query)) + elif marketType == 'swap': + response = self.contractPrivatePostSwapApiV1SwapSwitchLeverRate(self.extend(request, query)) + else: + raise NotSupported(self.id + ' setLeverage() not support self market type') + # + # future + # { + # "status": "ok", + # "data": {symbol: "BTC", lever_rate: 5}, + # "ts": 1641184578678 + # } + # + # swap + # + # { + # "status": "ok", + # "data": {contract_code: "BTC-USD", lever_rate: "5"}, + # "ts": "1641184652979" + # } + # + return response + + def parse_income(self, income, market: Market = None): + # + # { + # "id": "1667161118", + # "symbol": "BTC", + # "type": "31", + # "amount": "-2.11306593188E-7", + # "ts": "1641139308983", + # "contract_code": "BTC-USD" + # } + # + marketId = self.safe_string(income, 'contract_code') + symbol = self.safe_symbol(marketId, market) + amount = self.safe_number(income, 'amount') + timestamp = self.safe_integer(income, 'ts') + id = self.safe_string(income, 'id') + currencyId = self.safe_string_2(income, 'symbol', 'asset') + code = self.safe_currency_code(currencyId) + return { + 'info': income, + 'symbol': symbol, + 'code': code, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': id, + 'amount': amount, + } + + def parse_position(self, position: dict, market: Market = None): + # + # { + # "symbol": "BTC", + # "contract_code": "BTC-USDT", + # "volume": "1.000000000000000000", + # "available": "1.000000000000000000", + # "frozen": "0E-18", + # "cost_open": "47162.000000000000000000", + # "cost_hold": "47151.300000000000000000", + # "profit_unreal": "0.007300000000000000", + # "profit_rate": "-0.000144183876850008", + # "lever_rate": "2", + # "position_margin": "23.579300000000000000", + # "direction": "buy", + # "profit": "-0.003400000000000000", + # "last_price": "47158.6", + # "margin_asset": "USDT", + # "margin_mode": "isolated", + # "margin_account": "BTC-USDT", + # "margin_balance": "24.973020070000000000", + # "margin_position": "23.579300000000000000", + # "margin_frozen": "0", + # "margin_available": "1.393720070000000000", + # "profit_real": "0E-18", + # "risk_rate": "1.044107779705080303", + # "withdraw_available": "1.386420070000000000000000000000000000", + # "liquidation_price": "22353.229148614609571788", + # "adjust_factor": "0.015000000000000000", + # "margin_static": "24.965720070000000000" + # } + # + market = self.safe_market(self.safe_string(position, 'contract_code')) + symbol = market['symbol'] + contracts = self.safe_string(position, 'volume') + contractSize = self.safe_value(market, 'contractSize') + contractSizeString = self.number_to_string(contractSize) + entryPrice = self.safe_number(position, 'cost_open') + initialMargin = self.safe_string(position, 'position_margin') + rawSide = self.safe_string(position, 'direction') + side = 'long' if (rawSide == 'buy') else 'short' + unrealizedProfit = self.safe_number(position, 'profit_unreal') + marginMode = self.safe_string(position, 'margin_mode') + leverage = self.safe_string(position, 'lever_rate') + percentage = Precise.string_mul(self.safe_string(position, 'profit_rate'), '100') + lastPrice = self.safe_string(position, 'last_price') + faceValue = Precise.string_mul(contracts, contractSizeString) + notional = None + if market['linear']: + notional = Precise.string_mul(faceValue, lastPrice) + else: + notional = Precise.string_div(faceValue, lastPrice) + marginMode = 'cross' + intialMarginPercentage = Precise.string_div(initialMargin, notional) + collateral = self.safe_string(position, 'margin_balance') + liquidationPrice = self.safe_number(position, 'liquidation_price') + adjustmentFactor = self.safe_string(position, 'adjust_factor') + maintenanceMarginPercentage = Precise.string_div(adjustmentFactor, leverage) + maintenanceMargin = Precise.string_mul(maintenanceMarginPercentage, notional) + marginRatio = Precise.string_div(maintenanceMargin, collateral) + return self.safe_position({ + 'info': position, + 'id': None, + 'symbol': symbol, + 'contracts': self.parse_number(contracts), + 'contractSize': contractSize, + 'entryPrice': entryPrice, + 'collateral': self.parse_number(collateral), + 'side': side, + 'unrealizedPnl': unrealizedProfit, + 'leverage': self.parse_number(leverage), + 'percentage': self.parse_number(percentage), + 'marginMode': marginMode, + 'notional': self.parse_number(notional), + 'markPrice': None, + 'lastPrice': None, + 'liquidationPrice': liquidationPrice, + 'initialMargin': self.parse_number(initialMargin), + 'initialMarginPercentage': self.parse_number(intialMarginPercentage), + 'maintenanceMargin': self.parse_number(maintenanceMargin), + 'maintenanceMarginPercentage': self.parse_number(maintenanceMarginPercentage), + 'marginRatio': self.parse_number(marginRatio), + 'timestamp': None, + 'datetime': None, + 'hedged': None, + 'lastUpdateTimestamp': None, + 'stopLossPrice': None, + 'takeProfitPrice': None, + }) + + def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://huobiapi.github.io/docs/usdt_swap/v1/en/#cross-query-user-39-s-position-information + https://huobiapi.github.io/docs/usdt_swap/v1/en/#isolated-query-user-s-position-information + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#query-user-s-position-information + https://huobiapi.github.io/docs/dm/v1/en/#query-user-s-position-information + + :param str[] [symbols]: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.subType]: 'linear' or 'inverse' + :param str [params.type]: *inverse only* 'future', or 'swap' + :param str [params.marginMode]: *linear only* 'cross' or 'isolated' + :returns dict[]: a list of `position structure ` + """ + self.load_markets() + symbols = self.market_symbols(symbols) + market = None + if symbols is not None: + symbolsLength = len(symbols) + if symbolsLength > 0: + first = self.safe_string(symbols, 0) + market = self.market(first) + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchPositions', params, 'cross') + subType = None + subType, params = self.handle_sub_type_and_params('fetchPositions', market, params, 'linear') + marketType = None + marketType, params = self.handle_market_type_and_params('fetchPositions', market, params) + if marketType == 'spot': + marketType = 'future' + response = None + if subType == 'linear': + if marginMode == 'isolated': + response = self.contractPrivatePostLinearSwapApiV1SwapPositionInfo(params) + elif marginMode == 'cross': + response = self.contractPrivatePostLinearSwapApiV1SwapCrossPositionInfo(params) + else: + raise NotSupported(self.id + ' fetchPositions() not support self market type') + # + # { + # "status": "ok", + # "data": [ + # { + # "symbol": "BTC", + # "contract_code": "BTC-USDT", + # "volume": "1.000000000000000000", + # "available": "1.000000000000000000", + # "frozen": "0E-18", + # "cost_open": "47162.000000000000000000", + # "cost_hold": "47162.000000000000000000", + # "profit_unreal": "0.047300000000000000", + # "profit_rate": "0.002005852169119206", + # "lever_rate": "2", + # "position_margin": "23.604650000000000000", + # "direction": "buy", + # "profit": "0.047300000000000000", + # "last_price": "47209.3", + # "margin_asset": "USDT", + # "margin_mode": "isolated", + # "margin_account": "BTC-USDT" + # } + # ], + # "ts": "1641108676768" + # } + # + else: + if marketType == 'future': + response = self.contractPrivatePostApiV1ContractPositionInfo(params) + elif marketType == 'swap': + response = self.contractPrivatePostSwapApiV1SwapPositionInfo(params) + else: + raise NotSupported(self.id + ' fetchPositions() not support self market type') + # + # future + # { + # "status": "ok", + # "data": [ + # { + # "symbol": "BTC", + # "contract_code": "BTC220624", + # "contract_type": "next_quarter", + # "volume": "1.000000000000000000", + # "available": "1.000000000000000000", + # "frozen": "0E-18", + # "cost_open": "49018.880000000009853343", + # "cost_hold": "49018.880000000009853343", + # "profit_unreal": "-8.62360608500000000000000000000000000000000000000E-7", + # "profit_rate": "-0.000845439023678622", + # "lever_rate": "2", + # "position_margin": "0.001019583964880634", + # "direction": "sell", + # "profit": "-8.62360608500000000000000000000000000000000000000E-7", + # "last_price": "49039.61" + # } + # ], + # "ts": "1641109895199" + # } + # + # swap + # { + # "status": "ok", + # "data": [ + # { + # "symbol": "BTC", + # "contract_code": "BTC-USD", + # "volume": "1.000000000000000000", + # "available": "1.000000000000000000", + # "frozen": "0E-18", + # "cost_open": "47150.000000000012353300", + # "cost_hold": "47150.000000000012353300", + # "profit_unreal": "0E-54", + # "profit_rate": "-7.86E-16", + # "lever_rate": "3", + # "position_margin": "0.000706963591375044", + # "direction": "buy", + # "profit": "0E-54", + # "last_price": "47150" + # } + # ], + # "ts": "1641109636572" + # } + # + data = self.safe_value(response, 'data', []) + timestamp = self.safe_integer(response, 'ts') + result = [] + for i in range(0, len(data)): + position = data[i] + parsed = self.parse_position(position) + result.append(self.extend(parsed, { + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + })) + return self.filter_by_array_positions(result, 'symbol', symbols, False) + + def fetch_position(self, symbol: str, params={}): + """ + fetch data on a single open contract trade position + + https://huobiapi.github.io/docs/usdt_swap/v1/en/#cross-query-assets-and-positions + https://huobiapi.github.io/docs/usdt_swap/v1/en/#isolated-query-assets-and-positions + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#query-assets-and-positions + https://huobiapi.github.io/docs/dm/v1/en/#query-assets-and-positions + + :param str symbol: unified market symbol of the market the position is held in, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `position structure ` + """ + self.load_markets() + market = self.market(symbol) + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchPosition', params) + marginMode = 'cross' if (marginMode is None) else marginMode + marketType, query = self.handle_market_type_and_params('fetchPosition', market, params) + request: dict = {} + if market['future'] and market['inverse']: + request['symbol'] = market['settleId'] + else: + if marginMode == 'cross': + request['margin_account'] = 'USDT' # only allowed value + request['contract_code'] = market['id'] + response = None + if market['linear']: + if marginMode == 'isolated': + response = self.contractPrivatePostLinearSwapApiV1SwapAccountPositionInfo(self.extend(request, query)) + elif marginMode == 'cross': + response = self.contractPrivatePostLinearSwapApiV1SwapCrossAccountPositionInfo(self.extend(request, query)) + else: + raise NotSupported(self.id + ' fetchPosition() not support self market type') + # + # isolated + # + # { + # "status": "ok", + # "data": [ + # { + # "positions": [], + # "symbol": "BTC", + # "margin_balance": 1.949728350000000000, + # "margin_position": 0, + # "margin_frozen": 0E-18, + # "margin_available": 1.949728350000000000, + # "profit_real": -0.050271650000000000, + # "profit_unreal": 0, + # "risk_rate": null, + # "withdraw_available": 1.949728350000000000, + # "liquidation_price": null, + # "lever_rate": 20, + # "adjust_factor": 0.150000000000000000, + # "margin_static": 1.949728350000000000, + # "contract_code": "BTC-USDT", + # "margin_asset": "USDT", + # "margin_mode": "isolated", + # "margin_account": "BTC-USDT", + # "trade_partition": "USDT", + # "position_mode": "dual_side" + # }, + # ... opposite side position can be present here too(if hedge) + # ], + # "ts": 1653605008286 + # } + # + # cross + # + # { + # "status": "ok", + # "data": { + # "positions": [ + # { + # "symbol": "BTC", + # "contract_code": "BTC-USDT", + # "volume": "1.000000000000000000", + # "available": "1.000000000000000000", + # "frozen": "0E-18", + # "cost_open": "29530.000000000000000000", + # "cost_hold": "29530.000000000000000000", + # "profit_unreal": "-0.010000000000000000", + # "profit_rate": "-0.016931933626820200", + # "lever_rate": "50", + # "position_margin": "0.590400000000000000", + # "direction": "buy", + # "profit": "-0.010000000000000000", + # "last_price": "29520", + # "margin_asset": "USDT", + # "margin_mode": "cross", + # "margin_account": "USDT", + # "contract_type": "swap", + # "pair": "BTC-USDT", + # "business_type": "swap", + # "trade_partition": "USDT", + # "position_mode": "dual_side" + # }, + # ... opposite side position can be present here too(if hedge) + # ], + # "futures_contract_detail": [ + # { + # "symbol": "BTC", + # "contract_code": "BTC-USDT-220624", + # "margin_position": "0", + # "margin_frozen": "0E-18", + # "margin_available": "1.497799766913531118", + # "profit_unreal": "0", + # "liquidation_price": null, + # "lever_rate": "30", + # "adjust_factor": "0.250000000000000000", + # "contract_type": "quarter", + # "pair": "BTC-USDT", + # "business_type": "futures", + # "trade_partition": "USDT" + # }, + # ... other items listed with different expiration(contract_code) + # ], + # "margin_mode": "cross", + # "margin_account": "USDT", + # "margin_asset": "USDT", + # "margin_balance": "2.088199766913531118", + # "margin_static": "2.098199766913531118", + # "margin_position": "0.590400000000000000", + # "margin_frozen": "0E-18", + # "profit_real": "-0.016972710000000000", + # "profit_unreal": "-0.010000000000000000", + # "withdraw_available": "1.497799766913531118", + # "risk_rate": "9.105496355562965147", + # "contract_detail": [ + # { + # "symbol": "BTC", + # "contract_code": "BTC-USDT", + # "margin_position": "0.590400000000000000", + # "margin_frozen": "0E-18", + # "margin_available": "1.497799766913531118", + # "profit_unreal": "-0.010000000000000000", + # "liquidation_price": "27625.176468365024050352", + # "lever_rate": "50", + # "adjust_factor": "0.350000000000000000", + # "contract_type": "swap", + # "pair": "BTC-USDT", + # "business_type": "swap", + # "trade_partition": "USDT" + # }, + # ... all symbols listed + # ], + # "position_mode": "dual_side" + # }, + # "ts": "1653604697466" + # } + # + else: + if marketType == 'future': + response = self.contractPrivatePostApiV1ContractAccountPositionInfo(self.extend(request, query)) + elif marketType == 'swap': + response = self.contractPrivatePostSwapApiV1SwapAccountPositionInfo(self.extend(request, query)) + else: + raise NotSupported(self.id + ' setLeverage() not support self market type') + # + # future, swap + # + # { + # "status": "ok", + # "data": [ + # { + # "symbol": "XRP", + # "contract_code": "XRP-USD", # only present in swap + # "margin_balance": 12.186361450698276582, + # "margin_position": 5.036261079774375503, + # "margin_frozen": 0E-18, + # "margin_available": 7.150100370923901079, + # "profit_real": -0.012672343876723438, + # "profit_unreal": 0.163382354575000020, + # "risk_rate": 2.344723929650649798, + # "withdraw_available": 6.986718016348901059, + # "liquidation_price": 0.271625200493799547, + # "lever_rate": 5, + # "adjust_factor": 0.075000000000000000, + # "margin_static": 12.022979096123276562, + # "positions": [ + # { + # "symbol": "XRP", + # "contract_code": "XRP-USD", + # # "contract_type": "self_week", # only present in future + # "volume": 1.0, + # "available": 1.0, + # "frozen": 0E-18, + # "cost_open": 0.394560000000000000, + # "cost_hold": 0.394560000000000000, + # "profit_unreal": 0.163382354575000020, + # "profit_rate": 0.032232070910556005, + # "lever_rate": 5, + # "position_margin": 5.036261079774375503, + # "direction": "buy", + # "profit": 0.163382354575000020, + # "last_price": 0.39712 + # }, + # ... opposite side position can be present here too(if hedge) + # ] + # } + # ], + # "ts": 1653600470199 + # } + # + # cross usdt swap + # + # { + # "status":"ok", + # "data":{ + # "positions":[], + # "futures_contract_detail":[] + # "margin_mode":"cross", + # "margin_account":"USDT", + # "margin_asset":"USDT", + # "margin_balance":"1.000000000000000000", + # "margin_static":"1.000000000000000000", + # "margin_position":"0", + # "margin_frozen":"1.000000000000000000", + # "profit_real":"0E-18", + # "profit_unreal":"0", + # "withdraw_available":"0", + # "risk_rate":"15.666666666666666666", + # "contract_detail":[] + # }, + # "ts":"1645521118946" + # } + # + data = self.safe_value(response, 'data') + account = None + if marginMode == 'cross': + account = data + else: + account = self.safe_value(data, 0) + omitted = self.omit(account, ['positions']) + positions = self.safe_value(account, 'positions') + position = None + if market['future'] and market['inverse']: + for i in range(0, len(positions)): + entry = positions[i] + if entry['contract_code'] == market['id']: + position = entry + break + else: + position = self.safe_value(positions, 0) + timestamp = self.safe_integer(response, 'ts') + parsed = self.parse_position(self.extend(position, omitted)) + parsed['timestamp'] = timestamp + parsed['datetime'] = self.iso8601(timestamp) + return parsed + + def parse_ledger_entry_type(self, type): + types: dict = { + 'trade': 'trade', + 'etf': 'trade', + 'transact-fee': 'fee', + 'fee-deduction': 'fee', + 'transfer': 'transfer', + 'credit': 'credit', + 'liquidation': 'trade', + 'interest': 'credit', + 'deposit': 'deposit', + 'withdraw': 'withdrawal', + 'withdraw-fee': 'fee', + 'exchange': 'exchange', + 'other-types': 'transfer', + 'rebate': 'rebate', + } + return self.safe_string(types, type, type) + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + # + # { + # "accountId": 10000001, + # "currency": "usdt", + # "transactAmt": 10.000000000000000000, + # "transactType": "transfer", + # "transferType": "margin-transfer-out", + # "transactId": 0, + # "transactTime": 1629882331066, + # "transferer": 28483123, + # "transferee": 13496526 + # } + # + currencyId = self.safe_string(item, 'currency') + code = self.safe_currency_code(currencyId, currency) + currency = self.safe_currency(currencyId, currency) + id = self.safe_string(item, 'transactId') + transferType = self.safe_string(item, 'transferType') + timestamp = self.safe_integer(item, 'transactTime') + account = self.safe_string(item, 'accountId') + return self.safe_ledger_entry({ + 'info': item, + 'id': id, + 'direction': self.safe_string(item, 'direction'), + 'account': account, + 'referenceId': id, + 'referenceAccount': account, + 'type': self.parse_ledger_entry_type(transferType), + 'currency': code, + 'amount': self.safe_number(item, 'transactAmt'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'before': None, + 'after': None, + 'status': None, + 'fee': None, + }, currency) + + def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + + https://huobiapi.github.io/docs/spot/v1/en/#get-account-history + + :param str [code]: unified currency code, default is None + :param int [since]: timestamp in ms of the earliest ledger entry, default is None + :param int [limit]: max number of ledger entries to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch entries for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict: a `ledger structure ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchLedger', 'paginate') + if paginate: + return self.fetch_paginated_call_dynamic('fetchLedger', code, since, limit, params, 500) + accountId = self.fetch_account_id_by_type('spot', None, None, params) + request: dict = { + 'accountId': accountId, + # 'currency': code, + # 'transactTypes': 'all', # default all + # 'startTime': 1546272000000, + # 'endTime': 1546272000000, + # 'sort': asc, # asc, desc + # 'limit': 100, # range 1-500 + # 'fromId': 323 # first record hasattr(self, ID) query for pagination + } + currency = None + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit # max 500 + request, params = self.handle_until_option('endTime', request, params) + response = self.spotPrivateGetV2AccountLedger(self.extend(request, params)) + # + # { + # "code": 200, + # "message": "success", + # "data": [ + # { + # "accountId": 10000001, + # "currency": "usdt", + # "transactAmt": 10.000000000000000000, + # "transactType": "transfer", + # "transferType": "margin-transfer-out", + # "transactId": 0, + # "transactTime": 1629882331066, + # "transferer": 28483123, + # "transferee": 13496526 + # }, + # { + # "accountId": 10000001, + # "currency": "usdt", + # "transactAmt": -10.000000000000000000, + # "transactType": "transfer", + # "transferType": "margin-transfer-in", + # "transactId": 0, + # "transactTime": 1629882096562, + # "transferer": 13496526, + # "transferee": 28483123 + # } + # ], + # "nextId": 1624316679, + # "ok": True + # } + # + data = self.safe_value(response, 'data', []) + return self.parse_ledger(data, currency, since, limit) + + def fetch_leverage_tiers(self, symbols: Strings = None, params={}) -> LeverageTiers: + """ + retrieve information on the maximum leverage, and maintenance margin for trades of varying trade sizes + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `leverage tiers structures `, indexed by market symbols + """ + self.load_markets() + response = self.contractPublicGetLinearSwapApiV1SwapAdjustfactor(params) + # + # { + # "status": "ok", + # "data": [ + # { + # "symbol": "MANA", + # "contract_code": "MANA-USDT", + # "margin_mode": "isolated", + # "trade_partition": "USDT", + # "list": [ + # { + # "lever_rate": 75, + # "ladders": [ + # { + # "ladder": 0, + # "min_size": 0, + # "max_size": 999, + # "adjust_factor": 0.7 + # }, + # ... + # ] + # } + # ... + # ] + # }, + # ... + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_leverage_tiers(data, symbols, 'contract_code') + + def parse_market_leverage_tiers(self, info, market: Market = None) -> List[LeverageTier]: + currencyId = self.safe_string(info, 'trade_partition') + marketId = self.safe_string(info, 'contract_code') + tiers = [] + brackets = self.safe_list(info, 'list', []) + for i in range(0, len(brackets)): + item = brackets[i] + leverage = self.safe_string(item, 'lever_rate') + ladders = self.safe_list(item, 'ladders', []) + for k in range(0, len(ladders)): + bracket = ladders[k] + adjustFactor = self.safe_string(bracket, 'adjust_factor') + tiers.append({ + 'tier': self.safe_integer(bracket, 'ladder'), + 'symbol': self.safe_symbol(marketId, market, None, 'swap'), + 'currency': self.safe_currency_code(currencyId), + 'minNotional': self.safe_number(bracket, 'min_size'), + 'maxNotional': self.safe_number(bracket, 'max_size'), + 'maintenanceMarginRate': self.parse_number(Precise.string_div(adjustFactor, leverage)), + 'maxLeverage': self.parse_number(leverage), + 'info': bracket, + }) + return tiers + + def fetch_open_interest_history(self, symbol: str, timeframe='1h', since: Int = None, limit: Int = None, params={}): + """ + Retrieves the open interest history of a currency + + https://huobiapi.github.io/docs/dm/v1/en/#query-information-on-open-interest + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#query-information-on-open-interest + https://huobiapi.github.io/docs/usdt_swap/v1/en/#general-query-information-on-open-interest + + :param str symbol: Unified CCXT market symbol + :param str timeframe: '1h', '4h', '12h', or '1d' + :param int [since]: Not used by huobi api, but response parsed by CCXT + :param int [limit]: Default:48,Data Range [1,200] + :param dict [params]: Exchange specific parameters + :param int [params.amount_type]: *required* Open interest unit. 1-cont,2-cryptocurrency + :param int [params.pair]: eg BTC-USDT *Only for USDT-M* + :returns dict: an array of `open interest structures ` + """ + if timeframe != '1h' and timeframe != '4h' and timeframe != '12h' and timeframe != '1d': + raise BadRequest(self.id + ' fetchOpenInterestHistory cannot only use the 1h, 4h, 12h and 1d timeframe') + self.load_markets() + timeframes: dict = { + '1h': '60min', + '4h': '4hour', + '12h': '12hour', + '1d': '1day', + } + market = self.market(symbol) + amountType = self.safe_integer_2(params, 'amount_type', 'amountType', 2) + request: dict = { + 'period': timeframes[timeframe], + 'amount_type': amountType, + } + if limit is not None: + request['size'] = limit + response = None + if market['future']: + request['contract_type'] = self.safe_string(market['info'], 'contract_type') + request['symbol'] = market['baseId'] # currency code on coin-m futures + # coin-m futures + response = self.contractPublicGetApiV1ContractHisOpenInterest(self.extend(request, params)) + elif market['linear']: + request['contract_type'] = 'swap' + request['contract_code'] = market['id'] + request['contract_code'] = market['id'] + # USDT-M + response = self.contractPublicGetLinearSwapApiV1SwapHisOpenInterest(self.extend(request, params)) + else: + request['contract_code'] = market['id'] + # coin-m swaps + response = self.contractPublicGetSwapApiV1SwapHisOpenInterest(self.extend(request, params)) + # + # contractPublicGetlinearSwapApiV1SwapHisOpenInterest + # { + # "status": "ok", + # "data": { + # "symbol": "BTC", + # "tick": [ + # { + # "volume": "4385.4350000000000000", + # "amount_type": "2", + # "ts": "1648220400000", + # "value": "194059884.1850000000000000" + # }, + # ... + # ], + # "contract_code": "BTC-USDT", + # "business_type": "swap", + # "pair": "BTC-USDT", + # "contract_type": "swap", + # "trade_partition": "USDT" + # }, + # "ts": "1648223733007" + # } + # + # contractPublicGetSwapApiV1SwapHisOpenInterest + # { + # "status": "ok", + # "data": { + # "symbol": "CRV", + # "tick": [ + # { + # "volume": 19174.0000000000000000, + # "amount_type": 1, + # "ts": 1648224000000 + # }, + # ... + # ], + # "contract_code": "CRV-USD" + # }, + # "ts": 1648226554260 + # } + # + # contractPublicGetApiV1ContractHisOpenInterest + # { + # "status": "ok", + # "data": { + # "symbol": "BTC", + # "contract_type": "self_week", + # "tick": [ + # { + # "volume": "48419.0000000000000000", + # "amount_type": 1, + # "ts": 1648224000000 + # }, + # ... + # ] + # }, + # "ts": 1648227062944 + # } + # + data = self.safe_value(response, 'data') + tick = self.safe_list(data, 'tick') + return self.parse_open_interests_history(tick, market, since, limit) + + def fetch_open_interests(self, symbols: Strings = None, params={}): + """ + Retrieves the open interest for a list of symbols + + https://huobiapi.github.io/docs/dm/v1/en/#get-contract-open-interest-information + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#get-swap-open-interest-information + https://huobiapi.github.io/docs/usdt_swap/v1/en/#general-get-swap-open-interest-information + + :param str[] [symbols]: a list of unified CCXT market symbols + :param dict [params]: exchange specific parameters + :returns dict[]: a list of `open interest structures ` + """ + self.load_markets() + symbols = self.market_symbols(symbols) + market = None + if symbols is not None: + symbolsLength = len(symbols) + if symbolsLength > 0: + first = self.safe_string(symbols, 0) + market = self.market(first) + request: dict = {} + subType = None + subType, params = self.handle_sub_type_and_params('fetchPositions', market, params, 'linear') + marketType = None + marketType, params = self.handle_market_type_and_params('fetchPositions', market, params) + response = None + if marketType == 'future': + response = self.contractPublicGetApiV1ContractOpenInterest(self.extend(request, params)) + # + # { + # "status": "ok", + # "data": [ + # { + # "volume": 118850.000000000000000000, + # "amount": 635.502025211544374189, + # "symbol": "BTC", + # "contract_type": "self_week", + # "contract_code": "BTC220930", + # "trade_amount": 1470.9400749347598691119206024033947897351, + # "trade_volume": 286286, + # "trade_turnover": 28628600.000000000000000000 + # } + # ], + # "ts": 1664337928805 + # } + # + elif subType == 'inverse': + response = self.contractPublicGetSwapApiV1SwapOpenInterest(self.extend(request, params)) + # + # { + # "status": "ok", + # "data": [ + # { + # "volume": 518018.000000000000000000, + # "amount": 2769.675777407074725180, + # "symbol": "BTC", + # "contract_code": "BTC-USD", + # "trade_amount": 9544.4032080046491323463688602729806842458, + # "trade_volume": 1848448, + # "trade_turnover": 184844800.000000000000000000 + # } + # ], + # "ts": 1664337226028 + # } + # + else: + request['contract_type'] = 'swap' + response = self.contractPublicGetLinearSwapApiV1SwapOpenInterest(self.extend(request, params)) + # + # { + # "status": "ok", + # "data": [ + # { + # "volume": 7192610.000000000000000000, + # "amount": 7192.610000000000000000, + # "symbol": "BTC", + # "value": 134654290.332000000000000000, + # "contract_code": "BTC-USDT", + # "trade_amount": 70692.804, + # "trade_volume": 70692804, + # "trade_turnover": 1379302592.9518, + # "business_type": "swap", + # "pair": "BTC-USDT", + # "contract_type": "swap", + # "trade_partition": "USDT" + # } + # ], + # "ts": 1664336503144 + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_open_interests(data, symbols) + + def fetch_open_interest(self, symbol: str, params={}): + """ + Retrieves the open interest of a currency + + https://huobiapi.github.io/docs/dm/v1/en/#get-contract-open-interest-information + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#get-swap-open-interest-information + https://huobiapi.github.io/docs/usdt_swap/v1/en/#general-get-swap-open-interest-information + + :param str symbol: Unified CCXT market symbol + :param dict [params]: exchange specific parameters + :returns dict} an open interest structure{@link https://docs.ccxt.com/#/?id=open-interest-structure: + """ + self.load_markets() + market = self.market(symbol) + if not market['contract']: + raise BadRequest(self.id + ' fetchOpenInterest() supports contract markets only') + if market['option']: + raise NotSupported(self.id + ' fetchOpenInterest() does not currently support option markets') + request: dict = { + 'contract_code': market['id'], + } + response = None + if market['future']: + request['contract_type'] = self.safe_string(market['info'], 'contract_type') + request['symbol'] = market['baseId'] + # COIN-M futures + response = self.contractPublicGetApiV1ContractOpenInterest(self.extend(request, params)) + elif market['linear']: + request['contract_type'] = 'swap' + # USDT-M + response = self.contractPublicGetLinearSwapApiV1SwapOpenInterest(self.extend(request, params)) + else: + # COIN-M swaps + response = self.contractPublicGetSwapApiV1SwapOpenInterest(self.extend(request, params)) + # + # USDT-M contractPublicGetLinearSwapApiV1SwapOpenInterest + # + # { + # "status": "ok", + # "data": [ + # { + # "volume": 7192610.000000000000000000, + # "amount": 7192.610000000000000000, + # "symbol": "BTC", + # "value": 134654290.332000000000000000, + # "contract_code": "BTC-USDT", + # "trade_amount": 70692.804, + # "trade_volume": 70692804, + # "trade_turnover": 1379302592.9518, + # "business_type": "swap", + # "pair": "BTC-USDT", + # "contract_type": "swap", + # "trade_partition": "USDT" + # } + # ], + # "ts": 1664336503144 + # } + # + # COIN-M Swap contractPublicGetSwapApiV1SwapOpenInterest + # + # { + # "status": "ok", + # "data": [ + # { + # "volume": 518018.000000000000000000, + # "amount": 2769.675777407074725180, + # "symbol": "BTC", + # "contract_code": "BTC-USD", + # "trade_amount": 9544.4032080046491323463688602729806842458, + # "trade_volume": 1848448, + # "trade_turnover": 184844800.000000000000000000 + # } + # ], + # "ts": 1664337226028 + # } + # + # COIN-M Futures contractPublicGetApiV1ContractOpenInterest + # + # { + # "status": "ok", + # "data": [ + # { + # "volume": 118850.000000000000000000, + # "amount": 635.502025211544374189, + # "symbol": "BTC", + # "contract_type": "self_week", + # "contract_code": "BTC220930", + # "trade_amount": 1470.9400749347598691119206024033947897351, + # "trade_volume": 286286, + # "trade_turnover": 28628600.000000000000000000 + # } + # ], + # "ts": 1664337928805 + # } + # + data = self.safe_value(response, 'data', []) + openInterest = self.parse_open_interest(data[0], market) + timestamp = self.safe_integer(response, 'ts') + openInterest['timestamp'] = timestamp + openInterest['datetime'] = self.iso8601(timestamp) + return openInterest + + def parse_open_interest(self, interest, market: Market = None): + # + # fetchOpenInterestHistory + # + # { + # "volume": "4385.4350000000000000", + # "amount_type": "2", + # "ts": "1648220400000", + # "value": "194059884.1850000000000000" + # } + # + # fetchOpenInterest: USDT-M + # + # { + # "volume": 7192610.000000000000000000, + # "amount": 7192.610000000000000000, + # "symbol": "BTC", + # "value": 134654290.332000000000000000, + # "contract_code": "BTC-USDT", + # "trade_amount": 70692.804, + # "trade_volume": 70692804, + # "trade_turnover": 1379302592.9518, + # "business_type": "swap", + # "pair": "BTC-USDT", + # "contract_type": "swap", + # "trade_partition": "USDT" + # } + # + # fetchOpenInterest: COIN-M Swap + # + # { + # "volume": 518018.000000000000000000, + # "amount": 2769.675777407074725180, + # "symbol": "BTC", + # "contract_code": "BTC-USD", + # "trade_amount": 9544.4032080046491323463688602729806842458, + # "trade_volume": 1848448, + # "trade_turnover": 184844800.000000000000000000 + # } + # + # fetchOpenInterest: COIN-M Futures + # + # { + # "volume": 118850.000000000000000000, + # "amount": 635.502025211544374189, + # "symbol": "BTC", + # "contract_type": "self_week", + # "contract_code": "BTC220930", + # "trade_amount": 1470.9400749347598691119206024033947897351, + # "trade_volume": 286286, + # "trade_turnover": 28628600.000000000000000000 + # } + # + timestamp = self.safe_integer(interest, 'ts') + amount = self.safe_number(interest, 'volume') + value = self.safe_number(interest, 'value') + marketId = self.safe_string(interest, 'contract_code') + return self.safe_open_interest({ + 'symbol': self.safe_symbol(marketId, market), + 'baseVolume': amount, # deprecated + 'quoteVolume': value, # deprecated + 'openInterestAmount': amount, + 'openInterestValue': value, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'info': interest, + }, market) + + def borrow_isolated_margin(self, symbol: str, code: str, amount: float, params={}): + """ + create a loan to borrow margin + + https://huobiapi.github.io/docs/spot/v1/en/#request-a-margin-loan-isolated + https://huobiapi.github.io/docs/spot/v1/en/#request-a-margin-loan-cross + + :param str symbol: unified market symbol, required for isolated margin + :param str code: unified currency code of the currency to borrow + :param float amount: the amount to borrow + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin loan structure ` + """ + self.load_markets() + currency = self.currency(code) + market = self.market(symbol) + request: dict = { + 'currency': currency['id'], + 'amount': self.currency_to_precision(code, amount), + 'symbol': market['id'], + } + response = self.privatePostMarginOrders(self.extend(request, params)) + # + # Isolated + # + # { + # "data": 1000 + # } + # + transaction = self.parse_margin_loan(response, currency) + return self.extend(transaction, { + 'amount': amount, + 'symbol': symbol, + }) + + def borrow_cross_margin(self, code: str, amount: float, params={}): + """ + create a loan to borrow margin + + https://huobiapi.github.io/docs/spot/v1/en/#request-a-margin-loan-isolated + https://huobiapi.github.io/docs/spot/v1/en/#request-a-margin-loan-cross + + :param str code: unified currency code of the currency to borrow + :param float amount: the amount to borrow + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin loan structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + 'amount': self.currency_to_precision(code, amount), + } + response = self.privatePostCrossMarginOrders(self.extend(request, params)) + # + # Cross + # + # { + # "status": "ok", + # "data": null + # } + # + transaction = self.parse_margin_loan(response, currency) + return self.extend(transaction, { + 'amount': amount, + }) + + def repay_isolated_margin(self, symbol: str, code: str, amount, params={}): + """ + repay borrowed margin and interest + + https://huobiapi.github.io/docs/spot/v1/en/#repay-margin-loan-cross-isolated + + :param str symbol: unified market symbol + :param str code: unified currency code of the currency to repay + :param float amount: the amount to repay + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin loan structure ` + """ + self.load_markets() + currency = self.currency(code) + accountId = self.fetch_account_id_by_type('spot', 'isolated', symbol, params) + request: dict = { + 'currency': currency['id'], + 'amount': self.currency_to_precision(code, amount), + 'accountId': accountId, + } + response = self.v2PrivatePostAccountRepayment(self.extend(request, params)) + # + # { + # "code":200, + # "data": [ + # { + # "repayId":1174424, + # "repayTime":1600747722018 + # } + # ] + # } + # + data = self.safe_value(response, 'Data', []) + loan = self.safe_value(data, 0) + transaction = self.parse_margin_loan(loan, currency) + return self.extend(transaction, { + 'amount': amount, + 'symbol': symbol, + }) + + def repay_cross_margin(self, code: str, amount, params={}): + """ + repay borrowed margin and interest + + https://huobiapi.github.io/docs/spot/v1/en/#repay-margin-loan-cross-isolated + + :param str code: unified currency code of the currency to repay + :param float amount: the amount to repay + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin loan structure ` + """ + self.load_markets() + currency = self.currency(code) + accountId = self.fetch_account_id_by_type('spot', 'cross', None, params) + request: dict = { + 'currency': currency['id'], + 'amount': self.currency_to_precision(code, amount), + 'accountId': accountId, + } + response = self.v2PrivatePostAccountRepayment(self.extend(request, params)) + # + # { + # "code":200, + # "data": [ + # { + # "repayId":1174424, + # "repayTime":1600747722018 + # } + # ] + # } + # + data = self.safe_value(response, 'Data', []) + loan = self.safe_value(data, 0) + transaction = self.parse_margin_loan(loan, currency) + return self.extend(transaction, { + 'amount': amount, + }) + + def parse_margin_loan(self, info, currency: Currency = None): + # + # borrowMargin cross + # + # { + # "status": "ok", + # "data": null + # } + # + # borrowMargin isolated + # + # { + # "data": 1000 + # } + # + # repayMargin + # + # { + # "repayId":1174424, + # "repayTime":1600747722018 + # } + # + timestamp = self.safe_integer(info, 'repayTime') + return { + 'id': self.safe_string_2(info, 'repayId', 'data'), + 'currency': self.safe_currency_code(None, currency), + 'amount': None, + 'symbol': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'info': info, + } + + def fetch_settlement_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + Fetches historical settlement records + + https://huobiapi.github.io/docs/dm/v1/en/#query-historical-settlement-records-of-the-platform-interface + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#query-historical-settlement-records-of-the-platform-interface + https://huobiapi.github.io/docs/usdt_swap/v1/en/#general-query-historical-settlement-records-of-the-platform-interface + + :param str symbol: unified symbol of the market to fetch the settlement history for + :param int [since]: timestamp in ms, value range = current time - 90 days,default = current time - 90 days + :param int [limit]: page items, default 20, shall not exceed 50 + :param dict [params]: exchange specific params + :param int [params.until]: timestamp in ms, value range = start_time -> current time,default = current time + :param int [params.page_index]: page index, default page 1 if not filled + :param int [params.code]: unified currency code, can be used when symbol is None + :returns dict[]: a list of `settlement history objects ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchSettlementHistory() requires a symbol argument') + until = self.safe_integer(params, 'until') + params = self.omit(params, ['until']) + market = self.market(symbol) + request: dict = {} + if market['future']: + request['symbol'] = market['baseId'] + else: + request['contract_code'] = market['id'] + if since is not None: + request['start_at'] = since + if limit is not None: + request['page_size'] = limit + if until is not None: + request['end_at'] = until + response = None + if market['swap']: + if market['linear']: + response = self.contractPublicGetLinearSwapApiV1SwapSettlementRecords(self.extend(request, params)) + else: + response = self.contractPublicGetSwapApiV1SwapSettlementRecords(self.extend(request, params)) + else: + response = self.contractPublicGetApiV1ContractSettlementRecords(self.extend(request, params)) + # + # linear swap, coin-m swap + # + # { + # "status": "ok", + # "data": { + # "total_page": 14, + # "current_page": 1, + # "total_size": 270, + # "settlement_record": [ + # { + # "symbol": "ADA", + # "contract_code": "ADA-USDT", + # "settlement_time": 1652313600000, + # "clawback_ratio": 0E-18, + # "settlement_price": 0.512303000000000000, + # "settlement_type": "settlement", + # "business_type": "swap", + # "pair": "ADA-USDT", + # "trade_partition": "USDT" + # }, + # ... + # ], + # "ts": 1652338693256 + # } + # + # coin-m future + # + # { + # "status": "ok", + # "data": { + # "total_page": 5, + # "current_page": 1, + # "total_size": 90, + # "settlement_record": [ + # { + # "symbol": "FIL", + # "settlement_time": 1652342400000, + # "clawback_ratio": 0E-18, + # "list": [ + # { + # "contract_code": "FIL220513", + # "settlement_price": 7.016000000000000000, + # "settlement_type": "settlement" + # }, + # ... + # ] + # }, + # ] + # } + # } + # + data = self.safe_value(response, 'data') + settlementRecord = self.safe_value(data, 'settlement_record') + settlements = self.parse_settlements(settlementRecord, market) + return self.sort_by(settlements, 'timestamp') + + def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}): + """ + fetch deposit and withdraw fees + + https://huobiapi.github.io/docs/spot/v1/en/#get-all-supported-currencies-v2 + + :param str[]|None codes: list of unified currency codes + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `fees structures ` + """ + self.load_markets() + response = self.spotPublicGetV2ReferenceCurrencies(params) + # + # { + # "code": 200, + # "data": [ + # { + # "currency": "sxp", + # "assetType": "1", + # "chains": [ + # { + # "chain": "sxp", + # "displayName": "ERC20", + # "baseChain": "ETH", + # "baseChainProtocol": "ERC20", + # "isDynamic": True, + # "numOfConfirmations": "12", + # "numOfFastConfirmations": "12", + # "depositStatus": "allowed", + # "minDepositAmt": "0.23", + # "withdrawStatus": "allowed", + # "minWithdrawAmt": "0.23", + # "withdrawPrecision": "8", + # "maxWithdrawAmt": "227000.000000000000000000", + # "withdrawQuotaPerDay": "227000.000000000000000000", + # "withdrawQuotaPerYear": null, + # "withdrawQuotaTotal": null, + # "withdrawFeeType": "fixed", + # "transactFeeWithdraw": "11.1653", + # "addrWithTag": False, + # "addrDepositTag": False + # } + # ], + # "instStatus": "normal" + # } + # ] + # } + # + data = self.safe_list(response, 'data') + return self.parse_deposit_withdraw_fees(data, codes, 'currency') + + def parse_deposit_withdraw_fee(self, fee, currency: Currency = None): + # + # { + # "currency": "sxp", + # "assetType": "1", + # "chains": [ + # { + # "chain": "sxp", + # "displayName": "ERC20", + # "baseChain": "ETH", + # "baseChainProtocol": "ERC20", + # "isDynamic": True, + # "numOfConfirmations": "12", + # "numOfFastConfirmations": "12", + # "depositStatus": "allowed", + # "minDepositAmt": "0.23", + # "withdrawStatus": "allowed", + # "minWithdrawAmt": "0.23", + # "withdrawPrecision": "8", + # "maxWithdrawAmt": "227000.000000000000000000", + # "withdrawQuotaPerDay": "227000.000000000000000000", + # "withdrawQuotaPerYear": null, + # "withdrawQuotaTotal": null, + # "withdrawFeeType": "fixed", + # "transactFeeWithdraw": "11.1653", + # "addrWithTag": False, + # "addrDepositTag": False + # } + # ], + # "instStatus": "normal" + # } + # + chains = self.safe_value(fee, 'chains', []) + result = self.deposit_withdraw_fee(fee) + for j in range(0, len(chains)): + chainEntry = chains[j] + networkId = self.safe_string(chainEntry, 'chain') + withdrawFeeType = self.safe_string(chainEntry, 'withdrawFeeType') + networkCode = self.network_id_to_code(networkId) + withdrawFee = None + withdrawResult = None + if withdrawFeeType == 'fixed': + withdrawFee = self.safe_number(chainEntry, 'transactFeeWithdraw') + withdrawResult = { + 'fee': withdrawFee, + 'percentage': False, + } + else: + withdrawFee = self.safe_number(chainEntry, 'transactFeeRateWithdraw') + withdrawResult = { + 'fee': withdrawFee, + 'percentage': True, + } + result['networks'][networkCode] = { + 'withdraw': withdrawResult, + 'deposit': { + 'fee': None, + 'percentage': None, + }, + } + result = self.assign_default_deposit_withdraw_fees(result, currency) + return result + + def parse_settlements(self, settlements, market): + # + # linear swap, coin-m swap, fetchSettlementHistory + # + # [ + # { + # "symbol": "ADA", + # "contract_code": "ADA-USDT", + # "settlement_time": 1652313600000, + # "clawback_ratio": 0E-18, + # "settlement_price": 0.512303000000000000, + # "settlement_type": "settlement", + # "business_type": "swap", + # "pair": "ADA-USDT", + # "trade_partition": "USDT" + # }, + # ... + # ] + # + # coin-m future, fetchSettlementHistory + # + # [ + # { + # "symbol": "FIL", + # "settlement_time": 1652342400000, + # "clawback_ratio": 0E-18, + # "list": [ + # { + # "contract_code": "FIL220513", + # "settlement_price": 7.016000000000000000, + # "settlement_type": "settlement" + # }, + # ... + # ] + # }, + # ] + # + result = [] + for i in range(0, len(settlements)): + settlement = settlements[i] + list = self.safe_value(settlement, 'list') + if list is not None: + timestamp = self.safe_integer(settlement, 'settlement_time') + timestampDetails: dict = { + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + for j in range(0, len(list)): + item = list[j] + parsedSettlement = self.parse_settlement(item, market) + result.append(self.extend(parsedSettlement, timestampDetails)) + else: + result.append(self.parse_settlement(settlements[i], market)) + return result + + def parse_settlement(self, settlement, market): + # + # linear swap, coin-m swap, fetchSettlementHistory + # + # { + # "symbol": "ADA", + # "contract_code": "ADA-USDT", + # "settlement_time": 1652313600000, + # "clawback_ratio": 0E-18, + # "settlement_price": 0.512303000000000000, + # "settlement_type": "settlement", + # "business_type": "swap", + # "pair": "ADA-USDT", + # "trade_partition": "USDT" + # } + # + # coin-m future, fetchSettlementHistory + # + # { + # "contract_code": "FIL220513", + # "settlement_price": 7.016000000000000000, + # "settlement_type": "settlement" + # } + # + timestamp = self.safe_integer(settlement, 'settlement_time') + marketId = self.safe_string(settlement, 'contract_code') + return { + 'info': settlement, + 'symbol': self.safe_symbol(marketId, market), + 'price': self.safe_number(settlement, 'settlement_price'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + + def fetch_liquidations(self, symbol: str, since: Int = None, limit: Int = None, params={}): + """ + retrieves the public liquidations of a trading pair + + https://huobiapi.github.io/docs/usdt_swap/v1/en/#general-query-liquidation-orders-new + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#query-liquidation-orders-new + https://huobiapi.github.io/docs/dm/v1/en/#query-liquidation-order-information-new + + :param str symbol: unified CCXT market symbol + :param int [since]: the earliest time in ms to fetch liquidations for + :param int [limit]: the maximum number of liquidation structures to retrieve + :param dict [params]: exchange specific parameters for the huobi api endpoint + :param int [params.until]: timestamp in ms of the latest liquidation + :param int [params.tradeType]: default 0, linear swap 0: all liquidated orders, 5: liquidated longs; 6: liquidated shorts, inverse swap and future 0: filled liquidated orders, 5: liquidated close orders, 6: liquidated open orders + :returns dict: an array of `liquidation structures ` + """ + self.load_markets() + market = self.market(symbol) + tradeType = self.safe_integer(params, 'trade_type', 0) + request: dict = { + 'trade_type': tradeType, + } + if since is not None: + request['start_time'] = since + request, params = self.handle_until_option('end_time', request, params) + response = None + if market['swap']: + request['contract'] = market['id'] + if market['linear']: + response = self.contractPublicGetLinearSwapApiV3SwapLiquidationOrders(self.extend(request, params)) + else: + response = self.contractPublicGetSwapApiV3SwapLiquidationOrders(self.extend(request, params)) + elif market['future']: + request['symbol'] = market['id'] + response = self.contractPublicGetApiV3ContractLiquidationOrders(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchLiquidations() does not support ' + market['type'] + ' orders') + # + # { + # "code": 200, + # "msg": "", + # "data": [ + # { + # "query_id": 452057, + # "contract_code": "BTC-USDT-211210", + # "symbol": "USDT", + # "direction": "sell", + # "offset": "close", + # "volume": 479.000000000000000000, + # "price": 51441.700000000000000000, + # "created_at": 1638593647864, + # "amount": 0.479000000000000000, + # "trade_turnover": 24640.574300000000000000, + # "business_type": "futures", + # "pair": "BTC-USDT" + # } + # ], + # "ts": 1604312615051 + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_liquidations(data, market, since, limit) + + def parse_liquidation(self, liquidation, market: Market = None): + # + # { + # "query_id": 452057, + # "contract_code": "BTC-USDT-211210", + # "symbol": "USDT", + # "direction": "sell", + # "offset": "close", + # "volume": 479.000000000000000000, + # "price": 51441.700000000000000000, + # "created_at": 1638593647864, + # "amount": 0.479000000000000000, + # "trade_turnover": 24640.574300000000000000, + # "business_type": "futures", + # "pair": "BTC-USDT" + # } + # + marketId = self.safe_string(liquidation, 'contract_code') + timestamp = self.safe_integer(liquidation, 'created_at') + return self.safe_liquidation({ + 'info': liquidation, + 'symbol': self.safe_symbol(marketId, market), + 'contracts': self.safe_number(liquidation, 'volume'), + 'contractSize': self.safe_number(market, 'contractSize'), + 'price': self.safe_number(liquidation, 'price'), + 'side': self.safe_string_lower(liquidation, 'direction'), + 'baseValue': self.safe_number(liquidation, 'amount'), + 'quoteValue': self.safe_number(liquidation, 'trade_turnover'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + + def close_position(self, symbol: str, side: OrderSide = None, params={}) -> Order: + """ + closes open positions for a contract market, requires 'amount' in params, unlike other exchanges + + https://huobiapi.github.io/docs/usdt_swap/v1/en/#isolated-place-lightning-close-order # USDT-M(isolated) + https://huobiapi.github.io/docs/usdt_swap/v1/en/#cross-place-lightning-close-position # USDT-M(cross) + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#place-lightning-close-order # Coin-M swap + https://huobiapi.github.io/docs/dm/v1/en/#place-flash-close-order # Coin-M futures + + :param str symbol: unified CCXT market symbol + :param str side: 'buy' or 'sell', the side of the closing order, opposite side side + :param dict [params]: extra parameters specific to the okx api endpoint + :param str [params.clientOrderId]: client needs to provide unique API and have to maintain the API themselves afterwards. [1, 9223372036854775807] + :param dict [params.marginMode]: 'cross' or 'isolated', required for linear markets + + EXCHANGE SPECIFIC PARAMETERS + :param number [params.amount]: order quantity + :param str [params.order_price_type]: 'lightning' by default, 'lightning_fok': lightning fok type, 'lightning_ioc': lightning ioc type 'market' by default, 'market': market order type, 'lightning_fok': lightning + :returns dict: `an order structure ` + """ + self.load_markets() + market = self.market(symbol) + clientOrderId = self.safe_string(params, 'clientOrderId') + if not market['contract']: + raise BadRequest(self.id + ' closePosition() symbol supports contract markets only') + self.check_required_argument('closePosition', side, 'side') + request: dict = { + 'contract_code': market['id'], + 'direction': side, + } + if clientOrderId is not None: + request['client_order_id'] = clientOrderId + if market['inverse']: + amount = self.safe_string_2(params, 'volume', 'amount') + if amount is None: + raise ArgumentsRequired(self.id + ' closePosition() requires an extra argument params["amount"] for inverse markets') + request['volume'] = self.amount_to_precision(symbol, amount) + params = self.omit(params, ['clientOrderId', 'volume', 'amount']) + response = None + if market['inverse']: # Coin-M + if market['swap']: + response = self.contractPrivatePostSwapApiV1SwapLightningClosePosition(self.extend(request, params)) + else: # future + response = self.contractPrivatePostApiV1LightningClosePosition(self.extend(request, params)) + else: # USDT-M + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('closePosition', params, 'cross') + if marginMode == 'cross': + response = self.contractPrivatePostLinearSwapApiV1SwapCrossLightningClosePosition(self.extend(request, params)) + else: # isolated + response = self.contractPrivatePostLinearSwapApiV1SwapLightningClosePosition(self.extend(request, params)) + return self.parse_order(response, market) + + def set_position_mode(self, hedged: bool, symbol: Str = None, params={}): + """ + set hedged to True or False + + https://huobiapi.github.io/docs/usdt_swap/v1/en/#isolated-switch-position-mode + https://huobiapi.github.io/docs/usdt_swap/v1/en/#cross-switch-position-mode + + :param bool hedged: set to True to for hedged mode, must be set separately for each market in isolated margin mode, only valid for linear markets + :param str [symbol]: unified market symbol, required for isolated margin mode + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: "cross"(default) or "isolated" + :returns dict: response from the exchange + """ + self.load_markets() + posMode = 'dual_side' if hedged else 'single_side' + market = None + if symbol is not None: + market = self.market(symbol) + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('setPositionMode', params, 'cross') + request: dict = { + 'position_mode': posMode, + } + response = None + if (market is not None) and (market['inverse']): + raise BadRequest(self.id + ' setPositionMode can only be used for linear markets') + if marginMode == 'isolated': + if symbol is None: + raise ArgumentsRequired(self.id + ' setPositionMode requires a symbol argument for isolated margin mode') + request['margin_account'] = market['id'] + response = self.contractPrivatePostLinearSwapApiV1SwapSwitchPositionMode(self.extend(request, params)) + # + # { + # "status": "ok", + # "data": [ + # { + # "margin_account": "BTC-USDT", + # "position_mode": "single_side" + # } + # ], + # "ts": 1566899973811 + # } + # + else: + request['margin_account'] = 'USDT' + response = self.contractPrivatePostLinearSwapApiV1SwapCrossSwitchPositionMode(self.extend(request, params)) + # + # { + # "status": "ok", + # "data": [ + # { + # "margin_account": "USDT", + # "position_mode": "single_side" + # } + # ], + # "ts": 1566899973811 + # } + # + return response diff --git a/ccxt/huobi.py b/ccxt/huobi.py new file mode 100644 index 0000000..e9c7ae1 --- /dev/null +++ b/ccxt/huobi.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.htx import htx +from ccxt.abstract.huobi import ImplicitAPI +from ccxt.base.types import Any + + +class huobi(htx, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(huobi, self).describe(), { + 'id': 'huobi', + 'alias': True, + }) diff --git a/ccxt/hyperliquid.py b/ccxt/hyperliquid.py new file mode 100644 index 0000000..45ba3e2 --- /dev/null +++ b/ccxt/hyperliquid.py @@ -0,0 +1,3884 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.hyperliquid import ImplicitAPI +import math +from ccxt.base.types import Any, Balances, Currencies, Currency, Int, LedgerEntry, MarginModification, Market, Num, Order, OrderBook, OrderRequest, CancellationRequest, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, FundingRates, Trade, TradingFeeInterface, Transaction, MarketInterface, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import NotSupported +from ccxt.base.decimal_to_precision import ROUND +from ccxt.base.decimal_to_precision import DECIMAL_PLACES +from ccxt.base.decimal_to_precision import SIGNIFICANT_DIGITS +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class hyperliquid(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(hyperliquid, self).describe(), { + 'id': 'hyperliquid', + 'name': 'Hyperliquid', + 'countries': [], + 'version': 'v1', + 'rateLimit': 50, # 1200 requests per minute, 20 request per second + 'certified': True, + 'pro': True, + 'dex': True, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': True, + 'future': True, + 'option': False, + 'addMargin': True, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'cancelAllOrders': False, + 'cancelAllOrdersAfter': True, + 'cancelOrder': True, + 'cancelOrders': True, + 'cancelOrdersForSymbols': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createMarketBuyOrderWithCost': False, + 'createMarketOrderWithCost': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createOrders': True, + 'createOrderWithTakeProfitAndStopLoss': True, + 'createReduceOnlyOrder': True, + 'createStopOrder': True, + 'createTriggerOrder': True, + 'editOrder': True, + 'editOrders': True, + 'fetchAccounts': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchCanceledAndClosedOrders': True, + 'fetchCanceledOrders': True, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': False, + 'fetchDepositAddresses': False, + 'fetchDeposits': True, + 'fetchDepositWithdrawFee': 'emulated', + 'fetchDepositWithdrawFees': False, + 'fetchFundingHistory': True, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': True, + 'fetchFundingRates': True, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchLedger': True, + 'fetchLeverage': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchMarginMode': None, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMyLiquidations': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': True, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': True, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchOrderTrades': False, + 'fetchPosition': True, + 'fetchPositionMode': False, + 'fetchPositions': True, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchTicker': 'emulated', + 'fetchTickers': True, + 'fetchTime': False, + 'fetchTrades': True, + 'fetchTradingFee': True, + 'fetchTradingFees': False, + 'fetchTransfer': False, + 'fetchTransfers': False, + 'fetchWithdrawal': False, + 'fetchWithdrawals': True, + 'reduceMargin': True, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'sandbox': True, + 'setLeverage': True, + 'setMarginMode': True, + 'setPositionMode': False, + 'transfer': True, + 'withdraw': True, + }, + 'timeframes': { + '1m': '1m', + '3m': '3m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1h', + '2h': '2h', + '4h': '4h', + '8h': '8h', + '12h': '12h', + '1d': '1d', + '3d': '3d', + '1w': '1w', + '1M': '1M', + }, + 'hostname': 'hyperliquid.xyz', + 'urls': { + 'logo': 'https://github.com/ccxt/ccxt/assets/43336371/b371bc6c-4a8c-489f-87f4-20a913dd8d4b', + 'api': { + 'public': 'https://api.{hostname}', + 'private': 'https://api.{hostname}', + }, + 'test': { + 'public': 'https://api.hyperliquid-testnet.xyz', + 'private': 'https://api.hyperliquid-testnet.xyz', + }, + 'www': 'https://hyperliquid.xyz', + 'doc': 'https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api', + 'fees': 'https://hyperliquid.gitbook.io/hyperliquid-docs/trading/fees', + 'referral': 'https://app.hyperliquid.xyz/', + }, + 'api': { + 'public': { + 'post': { + 'info': { + 'cost': 20, + 'byType': { + 'l2Book': 2, + 'allMids': 2, + 'clearinghouseState': 2, + 'orderStatus': 2, + 'spotClearinghouseState': 2, + 'exchangeStatus': 2, + 'candleSnapshot': 4, + }, + }, + }, + }, + 'private': { + 'post': { + 'exchange': 1, + }, + }, + }, + 'fees': { + 'swap': { + 'taker': self.parse_number('0.00045'), + 'maker': self.parse_number('0.00015'), + }, + 'spot': { + 'taker': self.parse_number('0.0007'), + 'maker': self.parse_number('0.0004'), + }, + }, + 'requiredCredentials': { + 'apiKey': False, + 'secret': False, + 'walletAddress': True, + 'privateKey': True, + }, + 'exceptions': { + 'exact': { + }, + 'broad': { + 'Price must be divisible by tick size.': InvalidOrder, + 'Order must have minimum value of $10': InvalidOrder, + 'Insufficient margin to place order.': InsufficientFunds, + 'Reduce only order would increase position.': InvalidOrder, + 'Post only order would have immediately matched,': InvalidOrder, + 'Order could not immediately match against any resting orders.': InvalidOrder, + 'Invalid TP/SL price.': InvalidOrder, + 'No liquidity available for market order.': InvalidOrder, + 'Order was never placed, already canceled, or filled.': OrderNotFound, + 'User or API Wallet ': InvalidOrder, + 'Order has invalid size': InvalidOrder, + 'Order price cannot be more than 80% away from the reference price': InvalidOrder, + 'Order has zero size.': InvalidOrder, + 'Insufficient spot balance asset': InsufficientFunds, + 'Insufficient balance for withdrawal': InsufficientFunds, + 'Insufficient balance for token transfer': InsufficientFunds, + }, + }, + 'precisionMode': TICK_SIZE, + 'commonCurrencies': { + }, + 'options': { + 'defaultType': 'swap', + 'sandboxMode': False, + 'defaultSlippage': 0.05, + 'zeroAddress': '0x0000000000000000000000000000000000000000', + 'spotCurrencyMapping': { + 'UDZ': '2Z', + 'UBONK': 'BONK', + 'UBTC': 'BTC', + 'UETH': 'ETH', + 'UFART': 'FARTCOIN', + 'HPENGU': 'PENGU', + 'UPUMP': 'PUMP', + 'USOL': 'SOL', + 'UUUSPX': 'SPX', + 'USDT0': 'USDT', + 'XAUT0': 'XAUT', + 'UXPL': 'XPL', + }, + }, + 'features': { + 'default': { + 'sandbox': True, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': False, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': { + 'triggerPriceType': { + 'last': False, + 'mark': False, + 'index': False, + }, + 'triggerPrice': True, + 'type': True, + 'price': True, + }, + 'timeInForce': { + 'IOC': True, + 'FOK': False, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': False, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': { + 'max': 1000, + }, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 2000, + 'daysBack': None, + 'untilDays': None, + 'symbolRequired': True, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 2000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': 2000, + 'daysBack': None, + 'untilDays': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 2000, + 'daysBack': None, + 'daysBackCanceled': None, + 'untilDays': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOHLCV': { + 'limit': 5000, + }, + }, + 'spot': { + 'extends': 'default', + }, + 'forPerps': { + 'extends': 'default', + 'createOrder': { + 'stopLossPrice': True, + 'takeProfitPrice': True, + 'attachedStopLossTakeProfit': None, # todo, in two orders + }, + }, + 'swap': { + 'linear': { + 'extends': 'forPerps', + }, + 'inverse': { + 'extends': 'forPerps', + }, + }, + 'future': { + 'linear': { + 'extends': 'forPerps', + }, + 'inverse': { + 'extends': 'forPerps', + }, + }, + }, + }) + + def set_sandbox_mode(self, enabled): + super(hyperliquid, self).set_sandbox_mode(enabled) + self.options['sandboxMode'] = enabled + + def market(self, symbol: str) -> MarketInterface: + if self.markets is None: + raise ExchangeError(self.id + ' markets not loaded') + if symbol in self.markets: + market = self.markets[symbol] + if market['spot']: + baseName = self.safe_string(market, 'baseName') + spotCurrencyMapping = self.safe_dict(self.options, 'spotCurrencyMapping', {}) + if baseName in spotCurrencyMapping: + unifiedBaseName = self.safe_string(spotCurrencyMapping, baseName) + quote = self.safe_string(market, 'quote') + newSymbol = self.safe_currency_code(unifiedBaseName) + '/' + quote + if newSymbol in self.markets: + return self.markets[newSymbol] + res = super(hyperliquid, self).market(symbol) + return res + + def safe_market(self, marketId: Str = None, market: Market = None, delimiter: Str = None, marketType: Str = None) -> MarketInterface: + if marketId is not None: + if (self.markets_by_id is not None) and (marketId in self.markets_by_id): + markets = self.markets_by_id[marketId] + numMarkets = len(markets) + if numMarkets == 1: + return markets[0] + else: + if numMarkets > 2: + raise ExchangeError(self.id + ' safeMarket() found more than two markets with the same market id ' + marketId) + firstMarket = markets[0] + secondMarket = markets[1] + if self.safe_string(firstMarket, 'type') != self.safe_string(secondMarket, 'type'): + raise ExchangeError(self.id + ' safeMarket() found two different market types with the same market id ' + marketId) + baseCurrency = self.safe_string(firstMarket, 'base') + spotCurrencyMapping = self.safe_dict(self.options, 'spotCurrencyMapping', {}) + if baseCurrency in spotCurrencyMapping: + return secondMarket + return firstMarket + return super(hyperliquid, self).safe_market(marketId, market, delimiter, marketType) + + def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/perpetuals#retrieve-perpetuals-metadata + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + if self.check_required_credentials(False): + self.initialize_client() + request: dict = { + 'type': 'meta', + } + response = self.publicPostInfo(self.extend(request, params)) + # + # [ + # { + # "universe": [ + # { + # "maxLeverage": 50, + # "name": "SOL", + # "onlyIsolated": False, + # "szDecimals": 2 + # } + # ] + # } + # ] + # + meta = self.safe_list(response, 'universe', []) + result: dict = {} + for i in range(0, len(meta)): + data = self.safe_dict(meta, i, {}) + id = i + name = self.safe_string(data, 'name') + code = self.safe_currency_code(name) + result[code] = self.safe_currency_structure({ + 'id': id, + 'name': name, + 'code': code, + 'precision': None, + 'info': data, + 'active': None, + 'deposit': None, + 'withdraw': None, + 'networks': None, + 'fee': None, + 'type': 'crypto', + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + }, + }) + return result + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for hyperliquid + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/perpetuals#retrieve-perpetuals-asset-contexts-includes-mark-price-current-funding-open-interest-etc + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/spot#retrieve-spot-asset-contexts + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + rawPromises = [ + self.fetch_swap_markets(params), + self.fetch_spot_markets(params), + ] + promises = rawPromises + swapMarkets = promises[0] + spotMarkets = promises[1] + return self.array_concat(swapMarkets, spotMarkets) + + def fetch_swap_markets(self, params={}) -> List[Market]: + """ + retrieves data on all swap markets for hyperliquid + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/perpetuals#retrieve-perpetuals-asset-contexts-includes-mark-price-current-funding-open-interest-etc + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + request: dict = { + 'type': 'metaAndAssetCtxs', + } + response = self.publicPostInfo(self.extend(request, params)) + # + # [ + # { + # "universe": [ + # { + # "maxLeverage": 50, + # "name": "SOL", + # "onlyIsolated": False, + # "szDecimals": 2 + # } + # ] + # }, + # [ + # { + # "dayNtlVlm": "9450588.2273", + # "funding": "0.0000198", + # "impactPxs": [ + # "108.04", + # "108.06" + # ], + # "markPx": "108.04", + # "midPx": "108.05", + # "openInterest": "10764.48", + # "oraclePx": "107.99", + # "premium": "0.00055561", + # "prevDayPx": "111.81" + # } + # ] + # ] + # + # + meta = self.safe_dict(response, 0, {}) + universe = self.safe_list(meta, 'universe', []) + assetCtxs = self.safe_list(response, 1, []) + result = [] + for i in range(0, len(universe)): + data = self.extend( + self.safe_dict(universe, i, {}), + self.safe_dict(assetCtxs, i, {}) + ) + data['baseId'] = i + result.append(data) + return self.parse_markets(result) + + def calculate_price_precision(self, price: float, amountPrecision: float, maxDecimals: float): + """ + Helper function to calculate the Hyperliquid DECIMAL_PLACES price precision + :param float price: the price to use in the calculation + :param int amountPrecision: the amountPrecision to use in the calculation + :param int maxDecimals: the maxDecimals to use in the calculation + :returns int: The calculated price precision + """ + pricePrecision = 0 + priceStr = self.number_to_string(price) + if priceStr is None: + return 0 + priceSplitted = priceStr.split('.') + if Precise.string_eq(priceStr, '0'): + # Significant digits is always hasattr(self, 5) case + significantDigits = 5 + # Integer digits is always hasattr(self, 0) case(0 doesn't count) + integerDigits = 0 + # Calculate the price precision + pricePrecision = min(maxDecimals - amountPrecision, significantDigits - integerDigits) + elif Precise.string_gt(priceStr, '0') and Precise.string_lt(priceStr, '1'): + # Significant digits, always hasattr(self, 5) case + significantDigits = 5 + # Get the part after the decimal separator + decimalPart = self.safe_string(priceSplitted, 1, '') + # Count the number of leading zeros in the decimal part + leadingZeros = 0 + while((leadingZeros <= len(decimalPart)) and (decimalPart[leadingZeros] == '0')): + leadingZeros = leadingZeros + 1 + # Calculate price precision based on leading zeros and significant digits + pricePrecision = leadingZeros + significantDigits + # Calculate the price precision based on maxDecimals - szDecimals and the calculated price precision from the previous step + pricePrecision = min(maxDecimals - amountPrecision, pricePrecision) + else: + # Count the numbers before the decimal separator + integerPart = self.safe_string(priceSplitted, 0, '') + # Get significant digits, take the max() of 5 and the integer digits count + significantDigits = max(5, len(integerPart)) + # Calculate price precision based on maxDecimals - szDecimals and significantDigits - len(integerPart) + pricePrecision = min(maxDecimals - amountPrecision, significantDigits - len(integerPart)) + return self.parse_to_int(pricePrecision) + + def fetch_spot_markets(self, params={}) -> List[Market]: + """ + retrieves data on all spot markets for hyperliquid + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/spot#retrieve-spot-asset-contexts + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + request: dict = { + 'type': 'spotMetaAndAssetCtxs', + } + response = self.publicPostInfo(self.extend(request, params)) + # + # [ + # { + # "tokens": [ + # { + # "name": "USDC", + # "szDecimals": 8, + # "weiDecimals" 8, + # "index": 0, + # "tokenId": "0x6d1e7cde53ba9467b783cb7c530ce054", + # "isCanonical": True, + # "evmContract":null, + # "fullName":null + # }, + # { + # "name": "PURR", + # "szDecimals": 0, + # "weiDecimals": 5, + # "index": 1, + # "tokenId": "0xc1fb593aeffbeb02f85e0308e9956a90", + # "isCanonical": True, + # "evmContract":null, + # "fullName":null + # } + # ], + # "universe": [ + # { + # "name": "PURR/USDC", + # "tokens": [1, 0], + # "index": 0, + # "isCanonical": True + # } + # ] + # }, + # [ + # { + # "dayNtlVlm":"8906.0", + # "markPx":"0.14", + # "midPx":"0.209265", + # "prevDayPx":"0.20432" + # } + # ] + # ] + # + first = self.safe_dict(response, 0, {}) + second = self.safe_list(response, 1, []) + meta = self.safe_list(first, 'universe', []) + tokens = self.safe_list(first, 'tokens', []) + markets = [] + for i in range(0, len(meta)): + market = self.safe_dict(meta, i, {}) + index = self.safe_integer(market, 'index') + extraData = self.safe_dict(second, index, {}) + marketName = self.safe_string(market, 'name') + # if marketName.find('/') < 0: + # # there are some weird spot markets in testnet, eg @2 + # continue + # } + # marketParts = marketName.split('/') + # baseName = self.safe_string(marketParts, 0) + # quoteId = self.safe_string(marketParts, 1) + fees = self.safe_dict(self.fees, 'spot', {}) + taker = self.safe_number(fees, 'taker') + maker = self.safe_number(fees, 'maker') + tokensPos = self.safe_list(market, 'tokens', []) + baseTokenPos = self.safe_integer(tokensPos, 0) + quoteTokenPos = self.safe_integer(tokensPos, 1) + baseTokenInfo = self.safe_dict(tokens, baseTokenPos, {}) + quoteTokenInfo = self.safe_dict(tokens, quoteTokenPos, {}) + baseName = self.safe_string(baseTokenInfo, 'name') + quoteId = self.safe_string(quoteTokenInfo, 'name') + # do spot currency mapping + spotCurrencyMapping = self.safe_dict(self.options, 'spotCurrencyMapping', {}) + mappedBaseName = self.safe_string(spotCurrencyMapping, baseName, baseName) + mappedQuoteId = self.safe_string(spotCurrencyMapping, quoteId, quoteId) + mappedBase = self.safe_currency_code(mappedBaseName) + mappedQuote = self.safe_currency_code(mappedQuoteId) + mappedSymbol = mappedBase + '/' + mappedQuote + innerBaseTokenInfo = self.safe_dict(baseTokenInfo, 'spec', baseTokenInfo) + # innerQuoteTokenInfo = self.safe_dict(quoteTokenInfo, 'spec', quoteTokenInfo) + amountPrecisionStr = self.safe_string(innerBaseTokenInfo, 'szDecimals') + amountPrecision = int(amountPrecisionStr) + price = self.safe_number(extraData, 'midPx') + pricePrecision = 0 + if price is not None: + pricePrecision = self.calculate_price_precision(price, amountPrecision, 8) + pricePrecisionStr = self.number_to_string(pricePrecision) + # quotePrecision = self.parse_number(self.parse_precision(self.safe_string(innerQuoteTokenInfo, 'szDecimals'))) + baseId = self.number_to_string(index + 10000) + entry = { + 'id': marketName, + 'symbol': mappedSymbol, + 'base': mappedBase, + 'quote': mappedQuote, + 'settle': None, + 'baseId': baseId, + 'baseName': baseName, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'subType': None, + 'margin': None, + 'swap': False, + 'future': False, + 'option': False, + 'active': True, + 'contract': False, + 'linear': None, + 'inverse': None, + 'taker': taker, + 'maker': maker, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(self.parse_precision(amountPrecisionStr)), + 'price': self.parse_number(self.parse_precision(pricePrecisionStr)), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': None, + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': self.parse_number('10'), + 'max': None, + }, + }, + 'created': None, + 'info': self.extend(extraData, market), + } + markets.append(self.safe_market_structure(entry)) + # backward support + base = self.safe_currency_code(baseName) + quote = self.safe_currency_code(quoteId) + newEntry = self.extend({}, entry) + symbol = base + '/' + quote + if symbol != mappedSymbol: + newEntry['symbol'] = symbol + newEntry['base'] = base + newEntry['quote'] = quote + newEntry['baseName'] = baseName + markets.append(self.safe_market_structure(newEntry)) + return markets + + def parse_market(self, market: dict) -> Market: + # + # { + # "maxLeverage": "50", + # "name": "ETH", + # "onlyIsolated": False, + # "szDecimals": "4", + # "dayNtlVlm": "1709813.11535", + # "funding": "0.00004807", + # "impactPxs": [ + # "2369.3", + # "2369.6" + # ], + # "markPx": "2369.6", + # "midPx": "2369.45", + # "openInterest": "1815.4712", + # "oraclePx": "2367.3", + # "premium": "0.00090821", + # "prevDayPx": "2381.5" + # } + # + quoteId = 'USDC' + baseName = self.safe_string(market, 'name') + base = self.safe_currency_code(baseName) + quote = self.safe_currency_code(quoteId) + baseId = self.safe_string(market, 'baseId') + settleId = 'USDC' + settle = self.safe_currency_code(settleId) + symbol = base + '/' + quote + contract = True + swap = True + if contract: + if swap: + symbol = symbol + ':' + settle + fees = self.safe_dict(self.fees, 'swap', {}) + taker = self.safe_number(fees, 'taker') + maker = self.safe_number(fees, 'maker') + amountPrecisionStr = self.safe_string(market, 'szDecimals') + amountPrecision = int(amountPrecisionStr) + price = self.safe_number(market, 'markPx', 0) + pricePrecision = 0 + if price is not None: + pricePrecision = self.calculate_price_precision(price, amountPrecision, 6) + pricePrecisionStr = self.number_to_string(pricePrecision) + isDelisted = self.safe_bool(market, 'isDelisted') + active = True + if isDelisted is not None: + active = not isDelisted + return self.safe_market_structure({ + 'id': baseId, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'baseName': baseName, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': 'swap', + 'spot': False, + 'margin': None, + 'swap': swap, + 'future': False, + 'option': False, + 'active': active, + 'contract': contract, + 'linear': True, + 'inverse': False, + 'taker': taker, + 'maker': maker, + 'contractSize': self.parse_number('1'), + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(self.parse_precision(amountPrecisionStr)), + 'price': self.parse_number(self.parse_precision(pricePrecisionStr)), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': self.safe_integer(market, 'maxLeverage'), + }, + 'amount': { + 'min': None, + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': self.parse_number('10'), + 'max': None, + }, + }, + 'created': None, + 'info': market, + }) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/spot#retrieve-a-users-token-balances + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/perpetuals#retrieve-users-perpetuals-account-summary + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.user]: user address, will default to self.walletAddress if not provided + :param str [params.type]: wallet type, ['spot', 'swap'], defaults to swap + :param str [params.marginMode]: 'cross' or 'isolated', for margin trading, uses self.options.defaultMarginMode if not passed, defaults to None/None/None + :param str [params.subAccountAddress]: sub account user address + :returns dict: a `balance structure ` + """ + userAddress = None + userAddress, params = self.handle_public_address('fetchBalance', params) + type = None + type, params = self.handle_market_type_and_params('fetchBalance', None, params) + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchBalance', params) + isSpot = (type == 'spot') + request: dict = { + 'type': 'spotClearinghouseState' if (isSpot) else 'clearinghouseState', + 'user': userAddress, + } + response = self.publicPostInfo(self.extend(request, params)) + # + # { + # "assetPositions": [], + # "crossMaintenanceMarginUsed": "0.0", + # "crossMarginSummary": { + # "accountValue": "100.0", + # "totalMarginUsed": "0.0", + # "totalNtlPos": "0.0", + # "totalRawUsd": "100.0" + # }, + # "marginSummary": { + # "accountValue": "100.0", + # "totalMarginUsed": "0.0", + # "totalNtlPos": "0.0", + # "totalRawUsd": "100.0" + # }, + # "time": "1704261007014", + # "withdrawable": "100.0" + # } + # spot + # + # { + # "balances":[ + # { + # "coin":"USDC", + # "hold":"0.0", + # "total":"1481.844" + # }, + # { + # "coin":"PURR", + # "hold":"0.0", + # "total":"999.65004" + # } + # } + # + balances = self.safe_list(response, 'balances') + if balances is not None: + spotBalances: dict = {'info': response} + for i in range(0, len(balances)): + balance = balances[i] + code = self.safe_currency_code(self.safe_string(balance, 'coin')) + account = self.account() + total = self.safe_string(balance, 'total') + used = self.safe_string(balance, 'hold') + account['total'] = total + account['used'] = used + spotBalances[code] = account + return self.safe_balance(spotBalances) + data = self.safe_dict(response, 'marginSummary', {}) + usdcBalance = { + 'total': self.safe_number(data, 'accountValue'), + } + if (marginMode is not None) and (marginMode == 'isolated'): + usdcBalance['free'] = self.safe_number(response, 'withdrawable') + else: + usdcBalance['used'] = self.safe_number(data, 'totalMarginUsed') + result: dict = { + 'info': response, + 'USDC': usdcBalance, + } + timestamp = self.safe_integer(response, 'time') + result['timestamp'] = timestamp + result['datetime'] = self.iso8601(timestamp) + return self.safe_balance(result) + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint#l2-book-snapshot + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'type': 'l2Book', + 'coin': market['baseName'] if market['swap'] else market['id'], + } + response = self.publicPostInfo(self.extend(request, params)) + # + # { + # "coin": "ETH", + # "levels": [ + # [ + # { + # "n": "2", + # "px": "2216.2", + # "sz": "74.0637" + # } + # ], + # [ + # { + # "n": "2", + # "px": "2216.5", + # "sz": "70.5893" + # } + # ] + # ], + # "time": "1704290104840" + # } + # + data = self.safe_list(response, 'levels', []) + result: dict = { + 'bids': self.safe_list(data, 0, []), + 'asks': self.safe_list(data, 1, []), + } + timestamp = self.safe_integer(response, 'time') + return self.parse_order_book(result, market['symbol'], timestamp, 'bids', 'asks', 'px', 'sz') + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/perpetuals#retrieve-perpetuals-asset-contexts-includes-mark-price-current-funding-open-interest-etc + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/spot#retrieve-spot-asset-contexts + + :param str[] [symbols]: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: 'spot' or 'swap', by default fetches both + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + symbols = self.market_symbols(symbols) + # at self stage, to get tickers data, we use fetchMarkets endpoints + response = [] + type = self.safe_string(params, 'type') + params = self.omit(params, 'type') + if type == 'spot': + response = self.fetch_spot_markets(params) + elif type == 'swap': + response = self.fetch_swap_markets(params) + else: + response = self.fetch_markets(params) + # same response "fetchMarkets" + result: dict = {} + for i in range(0, len(response)): + market = response[i] + info = market['info'] + ticker = self.parse_ticker(info, market) + symbol = self.safe_string(ticker, 'symbol') + result[symbol] = ticker + return self.filter_by_array_tickers(result, 'symbol', symbols) + + def fetch_funding_rates(self, symbols: Strings = None, params={}) -> FundingRates: + """ + retrieves data on all swap markets for hyperliquid + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/perpetuals#retrieve-perpetuals-asset-contexts-includes-mark-price-current-funding-open-interest-etc + + :param str[] [symbols]: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + request: dict = { + 'type': 'metaAndAssetCtxs', + } + response = self.publicPostInfo(self.extend(request, params)) + # + # [ + # { + # "universe": [ + # { + # "maxLeverage": 50, + # "name": "SOL", + # "onlyIsolated": False, + # "szDecimals": 2 + # } + # ] + # }, + # [ + # { + # "dayNtlVlm": "9450588.2273", + # "funding": "0.0000198", + # "impactPxs": [ + # "108.04", + # "108.06" + # ], + # "markPx": "108.04", + # "midPx": "108.05", + # "openInterest": "10764.48", + # "oraclePx": "107.99", + # "premium": "0.00055561", + # "prevDayPx": "111.81" + # } + # ] + # ] + # + # + meta = self.safe_dict(response, 0, {}) + universe = self.safe_list(meta, 'universe', []) + assetCtxs = self.safe_list(response, 1, []) + result = [] + for i in range(0, len(universe)): + data = self.extend( + self.safe_dict(universe, i, {}), + self.safe_dict(assetCtxs, i, {}) + ) + result.append(data) + return self.parse_funding_rates(result, symbols) + + def parse_funding_rate(self, info, market: Market = None) -> FundingRate: + # + # { + # "maxLeverage": "50", + # "name": "ETH", + # "onlyIsolated": False, + # "szDecimals": "4", + # "dayNtlVlm": "1709813.11535", + # "funding": "0.00004807", + # "impactPxs": [ + # "2369.3", + # "2369.6" + # ], + # "markPx": "2369.6", + # "midPx": "2369.45", + # "openInterest": "1815.4712", + # "oraclePx": "2367.3", + # "premium": "0.00090821", + # "prevDayPx": "2381.5" + # } + # + base = self.safe_string(info, 'name') + marketId = self.coin_to_market_id(base) + symbol = self.safe_symbol(marketId, market) + funding = self.safe_number(info, 'funding') + markPx = self.safe_number(info, 'markPx') + oraclePx = self.safe_number(info, 'oraclePx') + fundingTimestamp = (int(math.floor(self.milliseconds()) / 60 / 60 / 1000) + 1) * 60 * 60 * 1000 + return { + 'info': info, + 'symbol': symbol, + 'markPrice': markPx, + 'indexPrice': oraclePx, + 'interestRate': None, + 'estimatedSettlePrice': None, + 'timestamp': None, + 'datetime': None, + 'fundingRate': funding, + 'fundingTimestamp': fundingTimestamp, + 'fundingDatetime': self.iso8601(fundingTimestamp), + 'nextFundingRate': None, + 'nextFundingTimestamp': None, + 'nextFundingDatetime': None, + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': '1h', + } + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "prevDayPx": "3400.5", + # "dayNtlVlm": "511297257.47936022", + # "markPx": "3464.7", + # "midPx": "3465.05", + # "oraclePx": "3460.1", # only in swap + # "openInterest": "64638.1108", # only in swap + # "premium": "0.00141614", # only in swap + # "funding": "0.00008727", # only in swap + # "impactPxs": ["3465.0", "3465.1"], # only in swap + # "coin": "PURR", # only in spot + # "circulatingSupply": "998949190.03400207", # only in spot + # }, + # + bidAsk = self.safe_list(ticker, 'impactPxs') + return self.safe_ticker({ + 'symbol': market['symbol'], + 'timestamp': None, + 'datetime': None, + 'previousClose': self.safe_number(ticker, 'prevDayPx'), + 'close': self.safe_number(ticker, 'midPx'), + 'bid': self.safe_number(bidAsk, 0), + 'ask': self.safe_number(bidAsk, 1), + 'quoteVolume': self.safe_number(ticker, 'dayNtlVlm'), + 'info': ticker, + }, market) + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint#candle-snapshot + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents, support '1m', '15m', '1h', '1d' + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest candle to fetch + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + market = self.market(symbol) + until = self.safe_integer(params, 'until', self.milliseconds()) + useTail = since is None + originalSince = since + if since is None: + if limit is not None: + # optimization if limit is provided + timeframeInMilliseconds = self.parse_timeframe(timeframe) * 1000 + since = self.sum(until, timeframeInMilliseconds * limit * -1) + if since < 0: + since = 0 + useTail = False + else: + since = 0 + params = self.omit(params, ['until']) + request: dict = { + 'type': 'candleSnapshot', + 'req': { + 'coin': market['baseName'] if market['swap'] else market['id'], + 'interval': self.safe_string(self.timeframes, timeframe, timeframe), + 'startTime': since, + 'endTime': until, + }, + } + response = self.publicPostInfo(self.extend(request, params)) + # + # [ + # { + # "T": 1704287699999, + # "c": "2226.4", + # "h": "2247.9", + # "i": "15m", + # "l": "2224.6", + # "n": 46, + # "o": "2247.9", + # "s": "ETH", + # "t": 1704286800000, + # "v": "591.6427" + # } + # ] + # + return self.parse_ohlcvs(response, market, timeframe, originalSince, limit, useTail) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # { + # "T": 1704287699999, + # "c": "2226.4", + # "h": "2247.9", + # "i": "15m", + # "l": "2224.6", + # "n": 46, + # "o": "2247.9", + # "s": "ETH", + # "t": 1704286800000, + # "v": "591.6427" + # } + # + return [ + self.safe_integer(ohlcv, 't'), + self.safe_number(ohlcv, 'o'), + self.safe_number(ohlcv, 'h'), + self.safe_number(ohlcv, 'l'), + self.safe_number(ohlcv, 'c'), + self.safe_number(ohlcv, 'v'), + ] + + def fetch_trades(self, symbol: Str, since: Int = None, limit: Int = None, params={}): + """ + get the list of most recent trades for a particular symbol + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint#retrieve-a-users-fills + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint#retrieve-a-users-fills-by-time + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest trade + :param str [params.address]: wallet address that made trades + :param str [params.user]: wallet address that made trades + :param str [params.subAccountAddress]: sub account user address + :returns Trade[]: a list of `trade structures ` + """ + userAddress = None + userAddress, params = self.handle_public_address('fetchTrades', params) + self.load_markets() + market = self.safe_market(symbol) + request: dict = { + 'user': userAddress, + } + if since is not None: + request['type'] = 'userFillsByTime' + request['startTime'] = since + else: + request['type'] = 'userFills' + until = self.safe_integer(params, 'until') + params = self.omit(params, 'until') + if until is not None: + request['endTime'] = until + response = self.publicPostInfo(self.extend(request, params)) + # + # [ + # { + # "closedPnl": "0.19343", + # "coin": "ETH", + # "crossed": True, + # "dir": "Close Long", + # "fee": "0.050062", + # "hash": "0x09d77c96791e98b5775a04092584ab010d009445119c71e4005c0d634ea322bc", + # "liquidationMarkPx": null, + # "oid": 3929354691, + # "px": "2381.1", + # "side": "A", + # "startPosition": "0.0841", + # "sz": "0.0841", + # "tid": 128423918764978, + # "time": 1704262888911 + # } + # ] + # + return self.parse_trades(response, market, since, limit) + + def amount_to_precision(self, symbol, amount): + market = self.market(symbol) + return self.decimal_to_precision(amount, ROUND, market['precision']['amount'], self.precisionMode, self.paddingMode) + + def price_to_precision(self, symbol: str, price) -> str: + market = self.market(symbol) + priceStr = self.number_to_string(price) + integerPart = priceStr.split('.')[0] + significantDigits = max(5, len(integerPart)) + result = self.decimal_to_precision(price, ROUND, significantDigits, SIGNIFICANT_DIGITS, self.paddingMode) + maxDecimals = 8 if market['spot'] else 6 + subtractedValue = maxDecimals - self.precision_from_string(self.safe_string(market['precision'], 'amount')) + return self.decimal_to_precision(result, ROUND, subtractedValue, DECIMAL_PLACES, self.paddingMode) + + def hash_message(self, message): + return '0x' + self.hash(message, 'keccak', 'hex') + + def sign_hash(self, hash, privateKey): + signature = self.ecdsa(hash[-64:], privateKey[-64:], 'secp256k1', None) + return { + 'r': '0x' + signature['r'], + 's': '0x' + signature['s'], + 'v': self.sum(27, signature['v']), + } + + def sign_message(self, message, privateKey): + return self.sign_hash(self.hash_message(message), privateKey[-64:]) + + def construct_phantom_agent(self, hash, isTestnet=True): + source = 'b' if (isTestnet) else 'a' + return { + 'source': source, + 'connectionId': hash, + } + + def action_hash(self, action, vaultAddress, nonce): + dataBinary = self.packb(action) + dataHex = self.binary_to_base16(dataBinary) + data = dataHex + data += '00000' + self.int_to_base16(nonce) + if vaultAddress is None: + data += '00' + else: + data += '01' + data += vaultAddress + return self.hash(self.base16_to_binary(data), 'keccak', 'binary') + + def sign_l1_action(self, action, nonce, vaultAdress=None) -> object: + hash = self.action_hash(action, vaultAdress, nonce) + isTestnet = self.safe_bool(self.options, 'sandboxMode', False) + phantomAgent = self.construct_phantom_agent(hash, isTestnet) + # data: Dict = { + # 'domain': { + # 'chainId': 1337, + # 'name': 'Exchange', + # 'verifyingContract': '0x0000000000000000000000000000000000000000', + # 'version': '1', + # }, + # 'types': { + # 'Agent': [ + # {'name': 'source', 'type': 'string'}, + # {'name': 'connectionId', 'type': 'bytes32'}, + # ], + # 'EIP712Domain': [ + # {'name': 'name', 'type': 'string'}, + # {'name': 'version', 'type': 'string'}, + # {'name': 'chainId', 'type': 'uint256'}, + # {'name': 'verifyingContract', 'type': 'address'}, + # ], + # }, + # 'primaryType': 'Agent', + # 'message': phantomAgent, + # } + zeroAddress = self.safe_string(self.options, 'zeroAddress') + chainId = 1337 # check self out + domain: dict = { + 'chainId': chainId, + 'name': 'Exchange', + 'verifyingContract': zeroAddress, + 'version': '1', + } + messageTypes: dict = { + 'Agent': [ + {'name': 'source', 'type': 'string'}, + {'name': 'connectionId', 'type': 'bytes32'}, + ], + } + msg = self.eth_encode_structured_data(domain, messageTypes, phantomAgent) + signature = self.sign_message(msg, self.privateKey) + return signature + + def sign_user_signed_action(self, messageTypes, message): + zeroAddress = self.safe_string(self.options, 'zeroAddress') + chainId = 421614 # check self out + domain: dict = { + 'chainId': chainId, + 'name': 'HyperliquidSignTransaction', + 'verifyingContract': zeroAddress, + 'version': '1', + } + msg = self.eth_encode_structured_data(domain, messageTypes, message) + signature = self.sign_message(msg, self.privateKey) + return signature + + def build_usd_send_sig(self, message): + messageTypes: dict = { + 'HyperliquidTransaction:UsdSend': [ + {'name': 'hyperliquidChain', 'type': 'string'}, + {'name': 'destination', 'type': 'string'}, + {'name': 'amount', 'type': 'string'}, + {'name': 'time', 'type': 'uint64'}, + ], + } + return self.sign_user_signed_action(messageTypes, message) + + def build_usd_class_send_sig(self, message): + messageTypes: dict = { + 'HyperliquidTransaction:UsdClassTransfer': [ + {'name': 'hyperliquidChain', 'type': 'string'}, + {'name': 'amount', 'type': 'string'}, + {'name': 'toPerp', 'type': 'bool'}, + {'name': 'nonce', 'type': 'uint64'}, + ], + } + return self.sign_user_signed_action(messageTypes, message) + + def build_withdraw_sig(self, message): + messageTypes: dict = { + 'HyperliquidTransaction:Withdraw': [ + {'name': 'hyperliquidChain', 'type': 'string'}, + {'name': 'destination', 'type': 'string'}, + {'name': 'amount', 'type': 'string'}, + {'name': 'time', 'type': 'uint64'}, + ], + } + return self.sign_user_signed_action(messageTypes, message) + + def build_approve_builder_fee_sig(self, message): + messageTypes: dict = { + 'HyperliquidTransaction:ApproveBuilderFee': [ + {'name': 'hyperliquidChain', 'type': 'string'}, + {'name': 'maxFeeRate', 'type': 'string'}, + {'name': 'builder', 'type': 'address'}, + {'name': 'nonce', 'type': 'uint64'}, + ], + } + return self.sign_user_signed_action(messageTypes, message) + + def set_ref(self): + if self.safe_bool(self.options, 'refSet', False): + return True + self.options['refSet'] = True + action = { + 'type': 'setReferrer', + 'code': self.safe_string(self.options, 'ref', 'CCXT1'), + } + nonce = self.milliseconds() + signature = self.sign_l1_action(action, nonce) + request: dict = { + 'action': action, + 'nonce': nonce, + 'signature': signature, + } + response = None + try: + response = self.privatePostExchange(request) + return response + except Exception as e: + response = None # ignore self + return response + + def approve_builder_fee(self, builder: str, maxFeeRate: str): + nonce = self.milliseconds() + isSandboxMode = self.safe_bool(self.options, 'sandboxMode', False) + payload: dict = { + 'hyperliquidChain': 'Testnet' if isSandboxMode else 'Mainnet', + 'maxFeeRate': maxFeeRate, + 'builder': builder, + 'nonce': nonce, + } + sig = self.build_approve_builder_fee_sig(payload) + action = { + 'hyperliquidChain': payload['hyperliquidChain'], + 'signatureChainId': '0x66eee', + 'maxFeeRate': payload['maxFeeRate'], + 'builder': payload['builder'], + 'nonce': nonce, + 'type': 'approveBuilderFee', + } + request: dict = { + 'action': action, + 'nonce': nonce, + 'signature': sig, + 'vaultAddress': None, + } + # + # { + # "status": "ok", + # "response": { + # "type": "default" + # } + # } + # + return self.privatePostExchange(request) + + def initialize_client(self): + try: + [self.handle_builder_fee_approval(), self.set_ref()] + except Exception as e: + return False + return True + + def handle_builder_fee_approval(self): + buildFee = self.safe_bool(self.options, 'builderFee', True) + if not buildFee: + return False # skip if builder fee is not enabled + approvedBuilderFee = self.safe_bool(self.options, 'approvedBuilderFee', False) + if approvedBuilderFee: + return True # skip if builder fee is already approved + try: + builder = self.safe_string(self.options, 'builder', '0x6530512A6c89C7cfCEbC3BA7fcD9aDa5f30827a6') + maxFeeRate = self.safe_string(self.options, 'feeRate', '0.01%') + self.approve_builder_fee(builder, maxFeeRate) + self.options['approvedBuilderFee'] = True + except Exception as e: + self.options['builderFee'] = False # disable builder fee if an error occurs + return True + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#place-an-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.timeInForce]: 'Gtc', 'Ioc', 'Alo' + :param bool [params.postOnly]: True or False whether the order is post-only + :param bool [params.reduceOnly]: True or False whether the order is reduce-only + :param float [params.triggerPrice]: The price at which a trigger order is triggered at + :param str [params.clientOrderId]: client order id,(optional 128 bit hex string e.g. 0x1234567890abcdef1234567890abcdef) + :param str [params.slippage]: the slippage for market order + :param str [params.vaultAddress]: the vault address for order + :param str [params.subAccountAddress]: sub account user address + :returns dict: an `order structure ` + """ + self.load_markets() + order, globalParams = self.parse_create_edit_order_args(None, symbol, type, side, amount, price, params) + orders = self.create_orders([order], globalParams) + return orders[0] + + def create_orders(self, orders: List[OrderRequest], params={}): + """ + create a list of trade orders + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#place-an-order + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + self.initialize_client() + request = self.create_orders_request(orders, params) + response = self.privatePostExchange(request) + # + # { + # "status": "ok", + # "response": { + # "type": "order", + # "data": { + # "statuses": [ + # { + # "resting": { + # "oid": 5063830287 + # } + # } + # ] + # } + # } + # } + # + responseObj = self.safe_dict(response, 'response', {}) + data = self.safe_dict(responseObj, 'data', {}) + statuses = self.safe_list(data, 'statuses', []) + ordersToBeParsed = [] + for i in range(0, len(statuses)): + order = statuses[i] + if order == 'waitingForTrigger': + ordersToBeParsed.append({'status': order}) # tp/sl orders can return a string like "waitingForTrigger", + else: + ordersToBeParsed.append(order) + return self.parse_orders(ordersToBeParsed, None) + + def create_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: str, price: Str = None, params={}): + market = self.market(symbol) + type = type.upper() + side = side.upper() + isMarket = (type == 'MARKET') + isBuy = (side == 'BUY') + clientOrderId = self.safe_string_2(params, 'clientOrderId', 'client_id') + slippage = self.safe_string(params, 'slippage') + defaultTimeInForce = 'ioc' if (isMarket) else 'gtc' + postOnly = self.safe_bool(params, 'postOnly', False) + if postOnly: + defaultTimeInForce = 'alo' + timeInForce = self.safe_string_lower(params, 'timeInForce', defaultTimeInForce) + timeInForce = self.capitalize(timeInForce) + triggerPrice = self.safe_string_2(params, 'triggerPrice', 'stopPrice') + stopLossPrice = self.safe_string(params, 'stopLossPrice', triggerPrice) + takeProfitPrice = self.safe_string(params, 'takeProfitPrice') + isTrigger = (stopLossPrice or takeProfitPrice) + px = None + if isMarket: + if price is None: + raise ArgumentsRequired(self.id + ' market orders require price to calculate the max slippage price. Default slippage can be set in options(default is 5%).') + px = Precise.string_mul(price, Precise.string_add('1', slippage)) if (isBuy) else Precise.string_mul(price, Precise.string_sub('1', slippage)) + px = self.price_to_precision(symbol, px) # round after adding slippage + else: + px = self.price_to_precision(symbol, price) + sz = self.amount_to_precision(symbol, amount) + reduceOnly = self.safe_bool(params, 'reduceOnly', False) + orderType: dict = {} + if isTrigger: + isTp = False + if takeProfitPrice is not None: + triggerPrice = self.price_to_precision(symbol, takeProfitPrice) + isTp = True + else: + triggerPrice = self.price_to_precision(symbol, stopLossPrice) + orderType['trigger'] = { + 'isMarket': isMarket, + 'triggerPx': triggerPrice, + 'tpsl': 'tp' if (isTp) else 'sl', + } + else: + orderType['limit'] = { + 'tif': timeInForce, + } + params = self.omit(params, ['clientOrderId', 'slippage', 'triggerPrice', 'stopPrice', 'stopLossPrice', 'takeProfitPrice', 'timeInForce', 'client_id', 'reduceOnly', 'postOnly']) + orderObj: dict = { + 'a': self.parse_to_int(market['baseId']), + 'b': isBuy, + 'p': px, + 's': sz, + 'r': reduceOnly, + 't': orderType, + # 'c': clientOrderId, + } + if clientOrderId is not None: + orderObj['c'] = clientOrderId + return orderObj + + def create_orders_request(self, orders, params={}) -> dict: + """ + create a list of trade orders + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#place-an-order + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :returns dict: an `order structure ` + """ + self.check_required_credentials() + defaultSlippage = self.safe_string(self.options, 'defaultSlippage') + defaultSlippage = self.safe_string(params, 'slippage', defaultSlippage) + hasClientOrderId = False + for i in range(0, len(orders)): + rawOrder = orders[i] + orderParams = self.safe_dict(rawOrder, 'params', {}) + clientOrderId = self.safe_string_2(orderParams, 'clientOrderId', 'client_id') + if clientOrderId is not None: + hasClientOrderId = True + if hasClientOrderId: + for i in range(0, len(orders)): + rawOrder = orders[i] + orderParams = self.safe_dict(rawOrder, 'params', {}) + clientOrderId = self.safe_string_2(orderParams, 'clientOrderId', 'client_id') + if clientOrderId is None: + raise ArgumentsRequired(self.id + ' createOrders() all orders must have clientOrderId if at least one has a clientOrderId') + params = self.omit(params, ['slippage', 'clientOrderId', 'client_id', 'slippage', 'triggerPrice', 'stopPrice', 'stopLossPrice', 'takeProfitPrice', 'timeInForce']) + nonce = self.milliseconds() + orderReq = [] + grouping = 'na' + for i in range(0, len(orders)): + rawOrder = orders[i] + marketId = self.safe_string(rawOrder, 'symbol') + market = self.market(marketId) + symbol = market['symbol'] + type = self.safe_string_upper(rawOrder, 'type') + side = self.safe_string_upper(rawOrder, 'side') + amount = self.safe_string(rawOrder, 'amount') + price = self.safe_string(rawOrder, 'price') + orderParams = self.safe_dict(rawOrder, 'params', {}) + slippage = self.safe_string(orderParams, 'slippage', defaultSlippage) + orderParams['slippage'] = slippage + stopLoss = self.safe_value(orderParams, 'stopLoss') + takeProfit = self.safe_value(orderParams, 'takeProfit') + isTrigger = (stopLoss or takeProfit) + orderParams = self.omit(orderParams, ['stopLoss', 'takeProfit']) + mainOrderObj: dict = self.create_order_request(symbol, type, side, amount, price, orderParams) + orderReq.append(mainOrderObj) + if isTrigger: + # grouping opposed orders for sl/tp + stopLossOrderTriggerPrice = self.safe_string_n(stopLoss, ['triggerPrice', 'stopPrice']) + stopLossOrderType = self.safe_string(stopLoss, 'type', 'limit') + stopLossOrderLimitPrice = self.safe_string_n(stopLoss, ['price', 'stopLossPrice'], stopLossOrderTriggerPrice) + takeProfitOrderTriggerPrice = self.safe_string_n(takeProfit, ['triggerPrice', 'stopPrice']) + takeProfitOrderType = self.safe_string(takeProfit, 'type', 'limit') + takeProfitOrderLimitPrice = self.safe_string_n(takeProfit, ['price', 'takeProfitPrice'], takeProfitOrderTriggerPrice) + grouping = 'normalTpsl' + orderParams = self.omit(orderParams, ['stopLoss', 'takeProfit']) + triggerOrderSide = '' + if side == 'BUY': + triggerOrderSide = 'sell' + else: + triggerOrderSide = 'buy' + if takeProfit is not None: + orderObj: dict = self.create_order_request(symbol, takeProfitOrderType, triggerOrderSide, amount, takeProfitOrderLimitPrice, self.extend(orderParams, { + 'takeProfitPrice': takeProfitOrderTriggerPrice, + 'reduceOnly': True, + })) + orderReq.append(orderObj) + if stopLoss is not None: + orderObj: dict = self.create_order_request(symbol, stopLossOrderType, triggerOrderSide, amount, stopLossOrderLimitPrice, self.extend(orderParams, { + 'stopLossPrice': stopLossOrderTriggerPrice, + 'reduceOnly': True, + })) + orderReq.append(orderObj) + vaultAddress = None + vaultAddress, params = self.handle_option_and_params(params, 'createOrder', 'vaultAddress') + vaultAddress = self.format_vault_address(vaultAddress) + orderAction: dict = { + 'type': 'order', + 'orders': orderReq, + 'grouping': grouping, + } + if self.safe_bool(self.options, 'approvedBuilderFee', False): + wallet = self.safe_string_lower(self.options, 'builder', '0x6530512A6c89C7cfCEbC3BA7fcD9aDa5f30827a6') + orderAction['builder'] = {'b': wallet, 'f': self.safe_integer(self.options, 'feeInt', 10)} + signature = self.sign_l1_action(orderAction, nonce, vaultAddress) + request: dict = { + 'action': orderAction, + 'nonce': nonce, + 'signature': signature, + # 'vaultAddress': vaultAddress, + } + if vaultAddress is not None: + params = self.omit(params, 'vaultAddress') + request['vaultAddress'] = vaultAddress + return request + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#cancel-order-s + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#cancel-order-s-by-cloid + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.clientOrderId]: client order id,(optional 128 bit hex string e.g. 0x1234567890abcdef1234567890abcdef) + :param str [params.vaultAddress]: the vault address for order + :param str [params.subAccountAddress]: sub account user address + :returns dict: An `order structure ` + """ + orders = self.cancel_orders([id], symbol, params) + return self.safe_dict(orders, 0) + + def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): + """ + cancel multiple orders + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#cancel-order-s + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#cancel-order-s-by-cloid + + :param str[] ids: order ids + :param str [symbol]: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param string|str[] [params.clientOrderId]: client order ids,(optional 128 bit hex string e.g. 0x1234567890abcdef1234567890abcdef) + :param str [params.vaultAddress]: the vault address + :param str [params.subAccountAddress]: sub account user address + :returns dict: an list of `order structures ` + """ + self.check_required_credentials() + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrders() requires a symbol argument') + self.load_markets() + self.initialize_client() + request = self.cancel_orders_request(ids, symbol, params) + response = self.privatePostExchange(request) + # + # { + # "status":"ok", + # "response":{ + # "type":"cancel", + # "data":{ + # "statuses":[ + # "success" + # ] + # } + # } + # } + # + innerResponse = self.safe_dict(response, 'response') + data = self.safe_dict(innerResponse, 'data') + statuses = self.safe_list(data, 'statuses') + orders = [] + for i in range(0, len(statuses)): + status = statuses[i] + orders.append(self.safe_order({ + 'info': status, + 'status': status, + })) + return orders + + def cancel_orders_request(self, ids: List[str], symbol: Str = None, params={}) -> dict: + """ + build the request payload for cancelling multiple orders + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#cancel-order-s + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#cancel-order-s-by-cloid + :param str[] ids: order ids + :param str symbol: unified market symbol + :param dict [params]: + :returns dict: the raw request object to be sent to the exchange + """ + market = self.market(symbol) + clientOrderId = self.safe_value_2(params, 'clientOrderId', 'client_id') + params = self.omit(params, ['clientOrderId', 'client_id']) + nonce = self.milliseconds() + request: dict = { + 'nonce': nonce, + # 'vaultAddress': vaultAddress, + } + cancelReq = [] + cancelAction: dict = { + 'type': '', + 'cancels': [], + } + baseId = self.parse_to_numeric(market['baseId']) + if clientOrderId is not None: + if not isinstance(clientOrderId, list): + clientOrderId = [clientOrderId] + cancelAction['type'] = 'cancelByCloid' + for i in range(0, len(clientOrderId)): + cancelReq.append({ + 'asset': baseId, + 'cloid': clientOrderId[i], + }) + else: + cancelAction['type'] = 'cancel' + for i in range(0, len(ids)): + cancelReq.append({ + 'a': baseId, + 'o': self.parse_to_numeric(ids[i]), + }) + cancelAction['cancels'] = cancelReq + vaultAddress = None + vaultAddress, params = self.handle_option_and_params_2(params, 'cancelOrders', 'vaultAddress', 'subAccountAddress') + vaultAddress = self.format_vault_address(vaultAddress) + signature = self.sign_l1_action(cancelAction, nonce, vaultAddress) + request['action'] = cancelAction + request['signature'] = signature + if vaultAddress is not None: + params = self.omit(params, 'vaultAddress') + request['vaultAddress'] = vaultAddress + return request + + def cancel_orders_for_symbols(self, orders: List[CancellationRequest], params={}): + """ + cancel multiple orders for multiple symbols + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#cancel-order-s + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#cancel-order-s-by-cloid + + :param CancellationRequest[] orders: each order should contain the parameters required by cancelOrder namely id and symbol, example [{"id": "a", "symbol": "BTC/USDT"}, {"id": "b", "symbol": "ETH/USDT"}] + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.vaultAddress]: the vault address + :param str [params.subAccountAddress]: sub account user address + :returns dict: an list of `order structures ` + """ + self.check_required_credentials() + self.load_markets() + self.initialize_client() + nonce = self.milliseconds() + request: dict = { + 'nonce': nonce, + # 'vaultAddress': vaultAddress, + } + cancelReq = [] + cancelAction: dict = { + 'type': '', + 'cancels': [], + } + cancelByCloid = False + for i in range(0, len(orders)): + order = orders[i] + clientOrderId = self.safe_string(order, 'clientOrderId') + if clientOrderId is not None: + cancelByCloid = True + id = self.safe_string(order, 'id') + symbol = self.safe_string(order, 'symbol') + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrdersForSymbols() requires a symbol argument in each order') + if id is not None and cancelByCloid: + raise BadRequest(self.id + ' cancelOrdersForSymbols() all orders must have either id or clientOrderId') + assetKey = 'asset' if cancelByCloid else 'a' + idKey = 'cloid' if cancelByCloid else 'o' + market = self.market(symbol) + cancelObj: dict = {} + cancelObj[assetKey] = self.parse_to_numeric(market['baseId']) + cancelObj[idKey] = clientOrderId if cancelByCloid else self.parse_to_numeric(id) + cancelReq.append(cancelObj) + cancelAction['type'] = 'cancelByCloid' if cancelByCloid else 'cancel' + cancelAction['cancels'] = cancelReq + vaultAddress = None + vaultAddress, params = self.handle_option_and_params_2(params, 'cancelOrdersForSymbols', 'vaultAddress', 'subAccountAddress') + vaultAddress = self.format_vault_address(vaultAddress) + signature = self.sign_l1_action(cancelAction, nonce, vaultAddress) + request['action'] = cancelAction + request['signature'] = signature + if vaultAddress is not None: + params = self.omit(params, 'vaultAddress') + request['vaultAddress'] = vaultAddress + response = self.privatePostExchange(request) + # + # { + # "status":"ok", + # "response":{ + # "type":"cancel", + # "data":{ + # "statuses":[ + # "success" + # ] + # } + # } + # } + # + return [self.safe_order({'info': response})] + + def cancel_all_orders_after(self, timeout: Int, params={}): + """ + dead man's switch, cancel all orders after the given timeout + :param number timeout: time in milliseconds, 0 represents cancel the timer + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.vaultAddress]: the vault address + :param str [params.subAccountAddress]: sub account user address + :returns dict: the api result + """ + self.check_required_credentials() + self.load_markets() + self.initialize_client() + params = self.omit(params, ['clientOrderId', 'client_id']) + nonce = self.milliseconds() + request: dict = { + 'nonce': nonce, + # 'vaultAddress': vaultAddress, + } + cancelAction: dict = { + 'type': 'scheduleCancel', + 'time': nonce + timeout, + } + vaultAddress = None + vaultAddress, params = self.handle_option_and_params_2(params, 'cancelAllOrdersAfter', 'vaultAddress', 'subAccountAddress') + vaultAddress = self.format_vault_address(vaultAddress) + signature = self.sign_l1_action(cancelAction, nonce, vaultAddress) + request['action'] = cancelAction + request['signature'] = signature + if vaultAddress is not None: + params = self.omit(params, 'vaultAddress') + request['vaultAddress'] = vaultAddress + response = self.privatePostExchange(request) + # + # { + # "status":"err", + # "response":"Cannot set scheduled cancel time until enough volume traded. Required: $1000000. Traded: $373.47205." + # } + # + return response + + def edit_orders_request(self, orders, params={}): + self.check_required_credentials() + hasClientOrderId = False + for i in range(0, len(orders)): + rawOrder = orders[i] + orderParams = self.safe_dict(rawOrder, 'params', {}) + clientOrderId = self.safe_string_2(orderParams, 'clientOrderId', 'client_id') + if clientOrderId is not None: + hasClientOrderId = True + if hasClientOrderId: + for i in range(0, len(orders)): + rawOrder = orders[i] + orderParams = self.safe_dict(rawOrder, 'params', {}) + clientOrderId = self.safe_string_2(orderParams, 'clientOrderId', 'client_id') + if clientOrderId is None: + raise ArgumentsRequired(self.id + ' editOrders() all orders must have clientOrderId if at least one has a clientOrderId') + params = self.omit(params, ['slippage', 'clientOrderId', 'client_id', 'slippage', 'triggerPrice', 'stopPrice', 'stopLossPrice', 'takeProfitPrice', 'timeInForce']) + modifies = [] + for i in range(0, len(orders)): + rawOrder = orders[i] + id = self.safe_string(rawOrder, 'id') + marketId = self.safe_string(rawOrder, 'symbol') + market = self.market(marketId) + symbol = market['symbol'] + type = self.safe_string_upper(rawOrder, 'type') + isMarket = (type == 'MARKET') + side = self.safe_string_upper(rawOrder, 'side') + isBuy = (side == 'BUY') + amount = self.safe_string(rawOrder, 'amount') + price = self.safe_string(rawOrder, 'price') + orderParams = self.safe_dict(rawOrder, 'params', {}) + defaultSlippage = self.safe_string(self.options, 'defaultSlippage') + slippage = self.safe_string(orderParams, 'slippage', defaultSlippage) + defaultTimeInForce = 'ioc' if (isMarket) else 'gtc' + postOnly = self.safe_bool(orderParams, 'postOnly', False) + if postOnly: + defaultTimeInForce = 'alo' + timeInForce = self.safe_string_lower(orderParams, 'timeInForce', defaultTimeInForce) + timeInForce = self.capitalize(timeInForce) + clientOrderId = self.safe_string_2(orderParams, 'clientOrderId', 'client_id') + triggerPrice = self.safe_string_2(orderParams, 'triggerPrice', 'stopPrice') + stopLossPrice = self.safe_string(orderParams, 'stopLossPrice', triggerPrice) + takeProfitPrice = self.safe_string(orderParams, 'takeProfitPrice') + isTrigger = (stopLossPrice or takeProfitPrice) + reduceOnly = self.safe_bool(orderParams, 'reduceOnly', False) + orderParams = self.omit(orderParams, ['slippage', 'timeInForce', 'triggerPrice', 'stopLossPrice', 'takeProfitPrice', 'clientOrderId', 'client_id', 'postOnly', 'reduceOnly']) + px = self.number_to_string(price) + if isMarket: + px = Precise.string_mul(px, Precise.string_add('1', slippage)) if (isBuy) else Precise.string_mul(px, Precise.string_sub('1', slippage)) + px = self.price_to_precision(symbol, px) + else: + px = self.price_to_precision(symbol, px) + sz = self.amount_to_precision(symbol, amount) + orderType: dict = {} + if isTrigger: + isTp = False + if takeProfitPrice is not None: + triggerPrice = self.price_to_precision(symbol, takeProfitPrice) + isTp = True + else: + triggerPrice = self.price_to_precision(symbol, stopLossPrice) + orderType['trigger'] = { + 'isMarket': isMarket, + 'triggerPx': triggerPrice, + 'tpsl': 'tp' if (isTp) else 'sl', + } + else: + orderType['limit'] = { + 'tif': timeInForce, + } + if triggerPrice is None: + triggerPrice = '0' + orderReq: dict = { + 'a': self.parse_to_int(market['baseId']), + 'b': isBuy, + 'p': px, + 's': sz, + 'r': reduceOnly, + 't': orderType, + # 'c': clientOrderId, + } + if clientOrderId is not None: + orderReq['c'] = clientOrderId + modifyReq: dict = { + 'oid': self.parse_to_int(id), + 'order': orderReq, + } + modifies.append(modifyReq) + nonce = self.milliseconds() + modifyAction: dict = { + 'type': 'batchModify', + 'modifies': modifies, + } + vaultAddress = None + vaultAddress, params = self.handle_option_and_params(params, 'editOrder', 'vaultAddress') + vaultAddress = self.format_vault_address(vaultAddress) + signature = self.sign_l1_action(modifyAction, nonce, vaultAddress) + request: dict = { + 'action': modifyAction, + 'nonce': nonce, + 'signature': signature, + # 'vaultAddress': vaultAddress, + } + if vaultAddress is not None: + request['vaultAddress'] = vaultAddress + return request + + def edit_order(self, id: str, symbol: str, type: str, side: str, amount: Num = None, price: Num = None, params={}): + """ + edit a trade order + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#modify-multiple-orders + + :param str id: cancel order id + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.timeInForce]: 'Gtc', 'Ioc', 'Alo' + :param bool [params.postOnly]: True or False whether the order is post-only + :param bool [params.reduceOnly]: True or False whether the order is reduce-only + :param float [params.triggerPrice]: The price at which a trigger order is triggered at + :param str [params.clientOrderId]: client order id,(optional 128 bit hex string e.g. 0x1234567890abcdef1234567890abcdef) + :param str [params.vaultAddress]: the vault address for order + :param str [params.subAccountAddress]: sub account user address + :returns dict: an `order structure ` + """ + self.load_markets() + if id is None: + raise ArgumentsRequired(self.id + ' editOrder() requires an id argument') + order, globalParams = self.parse_create_edit_order_args(id, symbol, type, side, amount, price, params) + orders = self.edit_orders([order], globalParams) + return orders[0] + + def edit_orders(self, orders: List[OrderRequest], params={}): + """ + edit a list of trade orders + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#modify-multiple-orders + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + self.initialize_client() + request = self.edit_orders_request(orders, params) + response = self.privatePostExchange(request) + # + # { + # "status": "ok", + # "response": { + # "type": "order", + # "data": { + # "statuses": [ + # { + # "resting": { + # "oid": 5063830287 + # } + # } + # ] + # } + # } + # } + # when the order is filled immediately + # { + # "status":"ok", + # "response":{ + # "type":"order", + # "data":{ + # "statuses":[ + # { + # "filled":{ + # "totalSz":"0.1", + # "avgPx":"100.84", + # "oid":6195281425 + # } + # } + # ] + # } + # } + # } + # + responseObject = self.safe_dict(response, 'response', {}) + dataObject = self.safe_dict(responseObject, 'data', {}) + statuses = self.safe_list(dataObject, 'statuses', []) + return self.parse_orders(statuses) + + def create_vault(self, name: str, description: str, initialUsd: int, params={}): + """ + creates a value + :param str name: The name of the vault + :param str description: The description of the vault + :param number initialUsd: The initialUsd of the vault + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: the api result + """ + self.check_required_credentials() + self.load_markets() + nonce = self.milliseconds() + request: dict = { + 'nonce': nonce, + } + usd = self.parse_to_int(Precise.string_mul(self.number_to_string(initialUsd), '1000000')) + action: dict = { + 'type': 'createVault', + 'name': name, + 'description': description, + 'initialUsd': usd, + 'nonce': nonce, + } + signature = self.sign_l1_action(action, nonce) + request['action'] = action + request['signature'] = signature + response = self.privatePostExchange(self.extend(request, params)) + # + # { + # "status": "ok", + # "response": { + # "type": "createVault", + # "data": "0x04fddcbc9ce80219301bd16f18491bedf2a8c2b8" + # } + # } + # + return response + + def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical funding rate prices + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/perpetuals#retrieve-historical-funding-rates + + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: timestamp in ms of the earliest funding rate to fetch + :param int [limit]: the maximum amount of `funding rate structures ` to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest funding rate + :returns dict[]: a list of `funding rate structures ` + """ + self.load_markets() + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchFundingRateHistory() requires a symbol argument') + market = self.market(symbol) + request: dict = { + 'type': 'fundingHistory', + 'coin': market['baseName'], + } + if since is not None: + request['startTime'] = since + else: + maxLimit = 500 if (limit is None) else limit + request['startTime'] = self.milliseconds() - maxLimit * 60 * 60 * 1000 + until = self.safe_integer(params, 'until') + params = self.omit(params, 'until') + if until is not None: + request['endTime'] = until + response = self.publicPostInfo(self.extend(request, params)) + # + # [ + # { + # "coin": "ETH", + # "fundingRate": "0.0000125", + # "premium": "0.00057962", + # "time": 1704290400031 + # } + # ] + # + result = [] + for i in range(0, len(response)): + entry = response[i] + timestamp = self.safe_integer(entry, 'time') + result.append({ + 'info': entry, + 'symbol': self.safe_symbol(None, market), + 'fundingRate': self.safe_number(entry, 'fundingRate'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + sorted = self.sort_by(result, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, symbol, since, limit) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint#retrieve-a-users-open-orders + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.user]: user address, will default to self.walletAddress if not provided + :param str [params.method]: 'openOrders' or 'frontendOpenOrders' default is 'frontendOpenOrders' + :param str [params.subAccountAddress]: sub account user address + :returns Order[]: a list of `order structures ` + """ + userAddress = None + userAddress, params = self.handle_public_address('fetchOpenOrders', params) + method = None + method, params = self.handle_option_and_params(params, 'fetchOpenOrders', 'method', 'frontendOpenOrders') + self.load_markets() + market = self.safe_market(symbol) + request: dict = { + 'type': method, + 'user': userAddress, + } + response = self.publicPostInfo(self.extend(request, params)) + # + # [ + # { + # "coin": "ETH", + # "limitPx": "2000.0", + # "oid": 3991946565, + # "origSz": "0.1", + # "side": "B", + # "sz": "0.1", + # "timestamp": 1704346468838 + # } + # ] + # + orderWithStatus = [] + for i in range(0, len(response)): + order = response[i] + extendOrder = {} + if self.safe_string(order, 'status') is None: + extendOrder['ccxtStatus'] = 'open' + orderWithStatus.append(self.extend(order, extendOrder)) + return self.parse_orders(orderWithStatus, market, since, limit) + + def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently closed orders + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.user]: user address, will default to self.walletAddress if not provided + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + orders = self.fetch_orders(symbol, None, None, params) # don't filter here because we don't want to catch open orders + closedOrders = self.filter_by_array(orders, 'status', ['closed'], False) + return self.filter_by_symbol_since_limit(closedOrders, symbol, since, limit) + + def fetch_canceled_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all canceled orders + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.user]: user address, will default to self.walletAddress if not provided + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + orders = self.fetch_orders(symbol, None, None, params) # don't filter here because we don't want to catch open orders + closedOrders = self.filter_by_array(orders, 'status', ['canceled'], False) + return self.filter_by_symbol_since_limit(closedOrders, symbol, since, limit) + + def fetch_canceled_and_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all closed and canceled orders + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.user]: user address, will default to self.walletAddress if not provided + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + orders = self.fetch_orders(symbol, None, None, params) # don't filter here because we don't want to catch open orders + closedOrders = self.filter_by_array(orders, 'status', ['canceled', 'closed', 'rejected'], False) + return self.filter_by_symbol_since_limit(closedOrders, symbol, since, limit) + + def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all orders + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.user]: user address, will default to self.walletAddress if not provided + :param str [params.subAccountAddress]: sub account user address + :returns Order[]: a list of `order structures ` + """ + userAddress = None + userAddress, params = self.handle_public_address('fetchOrders', params) + self.load_markets() + market = self.safe_market(symbol) + request: dict = { + 'type': 'historicalOrders', + 'user': userAddress, + } + response = self.publicPostInfo(self.extend(request, params)) + # + # [ + # { + # "coin": "ETH", + # "limitPx": "2000.0", + # "oid": 3991946565, + # "origSz": "0.1", + # "side": "B", + # "sz": "0.1", + # "timestamp": 1704346468838 + # } + # ] + # + return self.parse_orders(response, market, since, limit) + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint#query-order-status-by-oid-or-cloid + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.clientOrderId]: client order id,(optional 128 bit hex string e.g. 0x1234567890abcdef1234567890abcdef) + :param str [params.user]: user address, will default to self.walletAddress if not provided + :param str [params.subAccountAddress]: sub account user address + :returns dict: An `order structure ` + """ + userAddress = None + userAddress, params = self.handle_public_address('fetchOrder', params) + self.load_markets() + market = self.safe_market(symbol) + clientOrderId = self.safe_string(params, 'clientOrderId') + request: dict = { + 'type': 'orderStatus', + # 'oid': id if isClientOrderId else self.parse_to_numeric(id), + 'user': userAddress, + } + if clientOrderId is not None: + params = self.omit(params, 'clientOrderId') + request['oid'] = clientOrderId + else: + isClientOrderId = len(id) >= 34 + request['oid'] = id if isClientOrderId else self.parse_to_numeric(id) + response = self.publicPostInfo(self.extend(request, params)) + # + # { + # "order": { + # "order": { + # "children": [], + # "cloid": null, + # "coin": "ETH", + # "isPositionTpsl": False, + # "isTrigger": False, + # "limitPx": "2000.0", + # "oid": "3991946565", + # "orderType": "Limit", + # "origSz": "0.1", + # "reduceOnly": False, + # "side": "B", + # "sz": "0.1", + # "tif": "Gtc", + # "timestamp": "1704346468838", + # "triggerCondition": "N/A", + # "triggerPx": "0.0" + # }, + # "status": "open", + # "statusTimestamp": "1704346468838" + # }, + # "status": "order" + # } + # + data = self.safe_dict(response, 'order') + return self.parse_order(data, market) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrdersWs error + # + # {error: 'Insufficient margin to place order. asset=159'} + # + # fetchOpenOrders + # + # { + # "coin": "ETH", + # "limitPx": "2000.0", + # "oid": 3991946565, + # "origSz": "0.1", + # "side": "B", + # "sz": "0.1", + # "timestamp": 1704346468838 + # } + # fetchClosedorders + # { + # "cloid": null, + # "closedPnl": "0.0", + # "coin": "SOL", + # "crossed": True, + # "dir": "Open Long", + # "fee": "0.003879", + # "hash": "0x4a2647998682b7f07bc5040ab531e1011400f9a51bfa0346a0b41ebe510e8875", + # "liquidationMarkPx": null, + # "oid": "6463280784", + # "px": "110.83", + # "side": "B", + # "startPosition": "1.64", + # "sz": "0.1", + # "tid": "232174667018988", + # "time": "1709142268394" + # } + # + # fetchOrder + # + # { + # "order": { + # "children": [], + # "cloid": null, + # "coin": "ETH", + # "isPositionTpsl": False, + # "isTrigger": False, + # "limitPx": "2000.0", + # "oid": "3991946565", + # "orderType": "Limit", + # "origSz": "0.1", + # "reduceOnly": False, + # "side": "B", + # "sz": "0.1", + # "tif": "Gtc", + # "timestamp": "1704346468838", + # "triggerCondition": "N/A", + # "triggerPx": "0.0" + # }, + # "status": "open", + # "statusTimestamp": "1704346468838" + # } + # + # createOrder + # + # { + # "resting": { + # "oid": 5063830287 + # } + # } + # + # { + # "filled":{ + # "totalSz":"0.1", + # "avgPx":"100.84", + # "oid":6195281425 + # } + # } + # frontendOrder + # { + # "children": [], + # "cloid": null, + # "coin": "BLUR", + # "isPositionTpsl": False, + # "isTrigger": True, + # "limitPx": "0.5", + # "oid": 8670487141, + # "orderType": "Stop Limit", + # "origSz": "20.0", + # "reduceOnly": False, + # "side": "B", + # "sz": "20.0", + # "tif": null, + # "timestamp": 1715523663687, + # "triggerCondition": "Price above 0.6", + # "triggerPx": "0.6" + # } + # + error = self.safe_string(order, 'error') + if error is not None: + return self.safe_order({ + 'info': order, + 'status': 'rejected', + }) + entry = self.safe_dict_n(order, ['order', 'resting', 'filled']) + if entry is None: + entry = order + coin = self.safe_string(entry, 'coin') + marketId = None + if coin is not None: + marketId = self.coin_to_market_id(coin) + if self.safe_string(entry, 'id') is None: + market = self.safe_market(marketId, None) + else: + market = self.safe_market(marketId, market) + symbol = market['symbol'] + timestamp = self.safe_integer(entry, 'timestamp') + status = self.safe_string_2(order, 'status', 'ccxtStatus') + order = self.omit(order, ['ccxtStatus']) + side = self.safe_string(entry, 'side') + if side is not None: + side = 'sell' if (side == 'A') else 'buy' + totalAmount = self.safe_string_2(entry, 'origSz', 'totalSz') + remaining = self.safe_string(entry, 'sz') + tif = self.safe_string_upper(entry, 'tif') + postOnly = None + if tif is not None: + postOnly = (tif == 'ALO') + return self.safe_order({ + 'info': order, + 'id': self.safe_string(entry, 'oid'), + 'clientOrderId': self.safe_string(entry, 'cloid'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'lastUpdateTimestamp': self.safe_integer(order, 'statusTimestamp'), + 'symbol': symbol, + 'type': self.parse_order_type(self.safe_string_lower(entry, 'orderType')), + 'timeInForce': tif, + 'postOnly': postOnly, + 'reduceOnly': self.safe_bool(entry, 'reduceOnly'), + 'side': side, + 'price': self.safe_string(entry, 'limitPx'), + 'triggerPrice': self.safe_number(entry, 'triggerPx') if self.safe_bool(entry, 'isTrigger') else None, + 'amount': totalAmount, + 'cost': None, + 'average': self.safe_string(entry, 'avgPx'), + 'filled': Precise.string_sub(totalAmount, remaining), + 'remaining': remaining, + 'status': self.parse_order_status(status), + 'fee': None, + 'trades': None, + }, market) + + def parse_order_status(self, status: Str): + if status is None: + return None + statuses: dict = { + 'triggered': 'open', + 'filled': 'closed', + 'open': 'open', + 'canceled': 'canceled', + 'rejected': 'rejected', + 'marginCanceled': 'canceled', + } + if status.endswith('Rejected'): + return 'rejected' + if status.endswith('Canceled'): + return 'canceled' + return self.safe_string(statuses, status, status) + + def parse_order_type(self, status): + statuses: dict = { + 'stop limit': 'limit', + 'stop market': 'market', + } + return self.safe_string(statuses, status, status) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint#retrieve-a-users-fills + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint#retrieve-a-users-fills-by-time + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest trade + :param str [params.subAccountAddress]: sub account user address + :returns Trade[]: a list of `trade structures ` + """ + userAddress = None + userAddress, params = self.handle_public_address('fetchMyTrades', params) + self.load_markets() + market = self.safe_market(symbol) + request: dict = { + 'user': userAddress, + } + if since is not None: + request['type'] = 'userFillsByTime' + request['startTime'] = since + else: + request['type'] = 'userFills' + until = self.safe_integer(params, 'until') + params = self.omit(params, 'until') + if until is not None: + request['endTime'] = until + response = self.publicPostInfo(self.extend(request, params)) + # + # [ + # { + # "closedPnl": "0.19343", + # "coin": "ETH", + # "crossed": True, + # "dir": "Close Long", + # "fee": "0.050062", + # "feeToken": "USDC", + # "hash": "0x09d77c96791e98b5775a04092584ab010d009445119c71e4005c0d634ea322bc", + # "liquidationMarkPx": null, + # "oid": 3929354691, + # "px": "2381.1", + # "side": "A", + # "startPosition": "0.0841", + # "sz": "0.0841", + # "tid": 128423918764978, + # "time": 1704262888911 + # } + # ] + # + return self.parse_trades(response, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # { + # "closedPnl": "0.19343", + # "coin": "ETH", + # "crossed": True, + # "dir": "Close Long", + # "fee": "0.050062", + # "hash": "0x09d77c96791e98b5775a04092584ab010d009445119c71e4005c0d634ea322bc", + # "liquidationMarkPx": null, + # "oid": 3929354691, + # "px": "2381.1", + # "side": "A", + # "startPosition": "0.0841", + # "sz": "0.0841", + # "tid": 128423918764978, + # "time": 1704262888911 + # } + # + timestamp = self.safe_integer(trade, 'time') + price = self.safe_string(trade, 'px') + amount = self.safe_string(trade, 'sz') + coin = self.safe_string(trade, 'coin') + marketId = self.coin_to_market_id(coin) + market = self.safe_market(marketId, None) + symbol = market['symbol'] + id = self.safe_string(trade, 'tid') + side = self.safe_string(trade, 'side') + if side is not None: + side = 'sell' if (side == 'A') else 'buy' + fee = self.safe_string(trade, 'fee') + takerOrMaker = None + crossed = self.safe_bool(trade, 'crossed') + if crossed is not None: + takerOrMaker = 'taker' if crossed else 'maker' + return self.safe_trade({ + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'id': id, + 'order': self.safe_string(trade, 'oid'), + 'type': None, + 'side': side, + 'takerOrMaker': takerOrMaker, + 'price': price, + 'amount': amount, + 'cost': None, + 'fee': { + 'cost': fee, + 'currency': self.safe_string(trade, 'feeToken'), + 'rate': None, + }, + }, market) + + def fetch_position(self, symbol: str, params={}): + """ + fetch data on an open position + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/perpetuals#retrieve-users-perpetuals-account-summary + + :param str symbol: unified market symbol of the market the position is held in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.user]: user address, will default to self.walletAddress if not provided + :returns dict: a `position structure ` + """ + positions = self.fetch_positions([symbol], params) + return self.safe_dict(positions, 0, {}) + + def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/perpetuals#retrieve-users-perpetuals-account-summary + + :param str[] [symbols]: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.user]: user address, will default to self.walletAddress if not provided + :param str [params.subAccountAddress]: sub account user address + :returns dict[]: a list of `position structure ` + """ + self.load_markets() + userAddress = None + userAddress, params = self.handle_public_address('fetchPositions', params) + symbols = self.market_symbols(symbols) + request: dict = { + 'type': 'clearinghouseState', + 'user': userAddress, + } + response = self.publicPostInfo(self.extend(request, params)) + # + # { + # "assetPositions": [ + # { + # "position": { + # "coin": "ETH", + # "cumFunding": { + # "allTime": "0.0", + # "sinceChange": "0.0", + # "sinceOpen": "0.0" + # }, + # "entryPx": "2213.9", + # "leverage": { + # "rawUsd": "-475.23904", + # "type": "isolated", + # "value": "20" + # }, + # "liquidationPx": "2125.00856238", + # "marginUsed": "24.88097", + # "maxLeverage": "50", + # "positionValue": "500.12001", + # "returnOnEquity": "0.0", + # "szi": "0.2259", + # "unrealizedPnl": "0.0" + # }, + # "type": "oneWay" + # } + # ], + # "crossMaintenanceMarginUsed": "0.0", + # "crossMarginSummary": { + # "accountValue": "100.0", + # "totalMarginUsed": "0.0", + # "totalNtlPos": "0.0", + # "totalRawUsd": "100.0" + # }, + # "marginSummary": { + # "accountValue": "100.0", + # "totalMarginUsed": "0.0", + # "totalNtlPos": "0.0", + # "totalRawUsd": "100.0" + # }, + # "time": "1704261007014", + # "withdrawable": "100.0" + # } + # + data = self.safe_list(response, 'assetPositions', []) + result = [] + for i in range(0, len(data)): + result.append(self.parse_position(data[i], None)) + return self.filter_by_array_positions(result, 'symbol', symbols, False) + + def parse_position(self, position: dict, market: Market = None): + # + # { + # "position": { + # "coin": "ETH", + # "cumFunding": { + # "allTime": "0.0", + # "sinceChange": "0.0", + # "sinceOpen": "0.0" + # }, + # "entryPx": "2213.9", + # "leverage": { + # "rawUsd": "-475.23904", + # "type": "isolated", + # "value": "20" + # }, + # "liquidationPx": "2125.00856238", + # "marginUsed": "24.88097", + # "maxLeverage": "50", + # "positionValue": "500.12001", + # "returnOnEquity": "0.0", + # "szi": "0.2259", + # "unrealizedPnl": "0.0" + # }, + # "type": "oneWay" + # } + # + entry = self.safe_dict(position, 'position', {}) + coin = self.safe_string(entry, 'coin') + marketId = self.coin_to_market_id(coin) + market = self.safe_market(marketId, None) + symbol = market['symbol'] + leverage = self.safe_dict(entry, 'leverage', {}) + marginMode = self.safe_string(leverage, 'type') + isIsolated = (marginMode == 'isolated') + rawSize = self.safe_string(entry, 'szi') + size = rawSize + side = None + if size is not None: + side = 'long' if Precise.string_gt(rawSize, '0') else 'short' + size = Precise.string_abs(size) + rawUnrealizedPnl = self.safe_string(entry, 'unrealizedPnl') + absRawUnrealizedPnl = Precise.string_abs(rawUnrealizedPnl) + marginUsed = self.safe_string(entry, 'marginUsed') + initialMargin = None + if isIsolated: + initialMargin = Precise.string_sub(marginUsed, rawUnrealizedPnl) + else: + initialMargin = marginUsed + percentage = Precise.string_mul(Precise.string_div(absRawUnrealizedPnl, marginUsed), '100') + return self.safe_position({ + 'info': position, + 'id': None, + 'symbol': symbol, + 'timestamp': None, + 'datetime': None, + 'isolated': isIsolated, + 'hedged': None, + 'side': side, + 'contracts': self.parse_number(size), + 'contractSize': None, + 'entryPrice': self.safe_number(entry, 'entryPx'), + 'markPrice': None, + 'notional': self.safe_number(entry, 'positionValue'), + 'leverage': self.safe_number(leverage, 'value'), + 'collateral': self.parse_number(marginUsed), + 'initialMargin': self.parse_number(initialMargin), + 'maintenanceMargin': None, + 'initialMarginPercentage': None, + 'maintenanceMarginPercentage': None, + 'unrealizedPnl': self.parse_number(rawUnrealizedPnl), + 'liquidationPrice': self.safe_number(entry, 'liquidationPx'), + 'marginMode': marginMode, + 'percentage': self.parse_number(percentage), + }) + + def set_margin_mode(self, marginMode: str, symbol: Str = None, params={}): + """ + set margin mode(symbol) + :param str marginMode: margin mode must be either [isolated, cross] + :param str symbol: unified market symbol of the market the position is held in, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.leverage]: the rate of leverage, is required if setting trade mode(symbol) + :param str [params.vaultAddress]: the vault address + :param str [params.subAccountAddress]: sub account user address + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setMarginMode() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + leverage = self.safe_integer(params, 'leverage') + if leverage is None: + raise ArgumentsRequired(self.id + ' setMarginMode() requires a leverage parameter') + asset = self.parse_to_int(market['baseId']) + isCross = (marginMode == 'cross') + nonce = self.milliseconds() + params = self.omit(params, ['leverage']) + updateAction: dict = { + 'type': 'updateLeverage', + 'asset': asset, + 'isCross': isCross, + 'leverage': leverage, + } + vaultAddress = None + vaultAddress, params = self.handle_option_and_params_2(params, 'setMarginMode', 'vaultAddress', 'subAccountAddress') + if vaultAddress is not None: + if vaultAddress.startswith('0x'): + vaultAddress = vaultAddress.replace('0x', '') + signature = self.sign_l1_action(updateAction, nonce, vaultAddress) + request: dict = { + 'action': updateAction, + 'nonce': nonce, + 'signature': signature, + # 'vaultAddress': vaultAddress, + } + if vaultAddress is not None: + request['vaultAddress'] = vaultAddress + response = self.privatePostExchange(request) + # + # { + # 'response': { + # 'type': 'default' + # }, + # 'status': 'ok' + # } + # + return response + + def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + :param float leverage: the rate of leverage + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: margin mode must be either [isolated, cross], default is cross + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setLeverage() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + marginMode = self.safe_string(params, 'marginMode', 'cross') + isCross = (marginMode == 'cross') + asset = self.parse_to_int(market['baseId']) + nonce = self.milliseconds() + params = self.omit(params, 'marginMode') + updateAction: dict = { + 'type': 'updateLeverage', + 'asset': asset, + 'isCross': isCross, + 'leverage': leverage, + } + vaultAddress = None + vaultAddress, params = self.handle_option_and_params_2(params, 'setLeverage', 'vaultAddress', 'subAccountAddress') + vaultAddress = self.format_vault_address(vaultAddress) + signature = self.sign_l1_action(updateAction, nonce, vaultAddress) + request: dict = { + 'action': updateAction, + 'nonce': nonce, + 'signature': signature, + # 'vaultAddress': vaultAddress, + } + if vaultAddress is not None: + params = self.omit(params, 'vaultAddress') + request['vaultAddress'] = vaultAddress + response = self.privatePostExchange(request) + # + # { + # 'response': { + # 'type': 'default' + # }, + # 'status': 'ok' + # } + # + return response + + def add_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + add margin + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#update-isolated-margin + + :param str symbol: unified market symbol + :param float amount: amount of margin to add + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.vaultAddress]: the vault address + :param str [params.subAccountAddress]: sub account user address + :returns dict: a `margin structure ` + """ + return self.modify_margin_helper(symbol, amount, 'add', params) + + def reduce_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#update-isolated-margin + + remove margin from a position + :param str symbol: unified market symbol + :param float amount: the amount of margin to remove + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.vaultAddress]: the vault address + :param str [params.subAccountAddress]: sub account user address + :returns dict: a `margin structure ` + """ + return self.modify_margin_helper(symbol, amount, 'reduce', params) + + def modify_margin_helper(self, symbol: str, amount, type, params={}) -> MarginModification: + self.load_markets() + market = self.market(symbol) + asset = self.parse_to_int(market['baseId']) + sz = self.parse_to_int(Precise.string_mul(self.amount_to_precision(symbol, amount), '1000000')) + if type == 'reduce': + sz = -sz + nonce = self.milliseconds() + updateAction: dict = { + 'type': 'updateIsolatedMargin', + 'asset': asset, + 'isBuy': True, + 'ntli': sz, + } + vaultAddress = None + vaultAddress, params = self.handle_option_and_params_2(params, 'modifyMargin', 'vaultAddress', 'subAccountAddress') + vaultAddress = self.format_vault_address(vaultAddress) + signature = self.sign_l1_action(updateAction, nonce, vaultAddress) + request: dict = { + 'action': updateAction, + 'nonce': nonce, + 'signature': signature, + # 'vaultAddress': vaultAddress, + } + if vaultAddress is not None: + request['vaultAddress'] = vaultAddress + response = self.privatePostExchange(request) + # + # { + # 'response': { + # 'type': 'default' + # }, + # 'status': 'ok' + # } + # + return self.extend(self.parse_margin_modification(response, market), { + 'code': self.safe_string(response, 'status'), + }) + + def parse_margin_modification(self, data: dict, market: Market = None) -> MarginModification: + # + # { + # 'type': 'default' + # } + # + return { + 'info': data, + 'symbol': self.safe_symbol(None, market), + 'type': None, + 'marginMode': 'isolated', + 'amount': None, + 'total': None, + 'code': self.safe_string(market, 'settle'), + 'status': None, + 'timestamp': None, + 'datetime': None, + } + + def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#l1-usdc-transfer + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account to transfer from *spot, swap* + :param str toAccount: account to transfer to *swap, spot or address* + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.vaultAddress]: the vault address for order + :returns dict: a `transfer structure ` + """ + self.check_required_credentials() + self.load_markets() + isSandboxMode = self.safe_bool(self.options, 'sandboxMode') + nonce = self.milliseconds() + if self.in_array(fromAccount, ['spot', 'swap', 'perp']): + # handle swap <> spot account transfer + if not self.in_array(toAccount, ['spot', 'swap', 'perp']): + raise NotSupported(self.id + ' transfer() only support spot <> swap transfer') + strAmount = self.number_to_string(amount) + vaultAddress = self.safe_string_2(params, 'vaultAddress', 'subAccountAddress') + if vaultAddress is not None: + vaultAddress = self.format_vault_address(vaultAddress) + strAmount = strAmount + ' subaccount:' + vaultAddress + toPerp = (toAccount == 'perp') or (toAccount == 'swap') + transferPayload: dict = { + 'hyperliquidChain': 'Testnet' if isSandboxMode else 'Mainnet', + 'amount': strAmount, + 'toPerp': toPerp, + 'nonce': nonce, + } + transferSig = self.build_usd_class_send_sig(transferPayload) + transferRequest: dict = { + 'action': { + 'hyperliquidChain': transferPayload['hyperliquidChain'], + 'signatureChainId': '0x66eee', + 'type': 'usdClassTransfer', + 'amount': strAmount, + 'toPerp': toPerp, + 'nonce': nonce, + }, + 'nonce': nonce, + 'signature': transferSig, + } + transferResponse = self.privatePostExchange(transferRequest) + return transferResponse + # transfer between main account and subaccount + isDeposit = False + subAccountAddress = None + if fromAccount == 'main': + subAccountAddress = toAccount + isDeposit = True + elif toAccount == 'main': + subAccountAddress = fromAccount + else: + raise NotSupported(self.id + ' transfer() only support main <> subaccount transfer') + self.check_address(subAccountAddress) + if code is None or code.upper() == 'USDC': + # Transfer USDC with subAccountTransfer + usd = self.parse_to_int(Precise.string_mul(self.number_to_string(amount), '1000000')) + action = { + 'type': 'subAccountTransfer', + 'subAccountUser': subAccountAddress, + 'isDeposit': isDeposit, + 'usd': usd, + } + sig = self.sign_l1_action(action, nonce) + request: dict = { + 'action': action, + 'nonce': nonce, + 'signature': sig, + } + response = self.privatePostExchange(request) + # + # {'response': {'type': 'default'}, 'status': 'ok'} + # + return self.parse_transfer(response) + else: + # Transfer non-USDC with subAccountSpotTransfer + symbol = self.symbol(code) + action = { + 'type': 'subAccountSpotTransfer', + 'subAccountUser': subAccountAddress, + 'isDeposit': isDeposit, + 'token': symbol, + 'amount': self.number_to_string(amount), + } + sig = self.sign_l1_action(action, nonce) + request: dict = { + 'action': action, + 'nonce': nonce, + 'signature': sig, + } + response = self.privatePostExchange(request) + return self.parse_transfer(response) + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + # + # {'response': {'type': 'default'}, 'status': 'ok'} + # + return { + 'info': transfer, + 'id': None, + 'timestamp': None, + 'datetime': None, + 'currency': None, + 'amount': None, + 'fromAccount': None, + 'toAccount': None, + 'status': 'ok', + } + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal(only support USDC) + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#initiate-a-withdrawal-request + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#deposit-or-withdraw-from-a-vault + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.vaultAddress]: vault address withdraw from + :returns dict: a `transaction structure ` + """ + self.check_required_credentials() + self.load_markets() + self.check_address(address) + if code is not None: + code = code.upper() + if code != 'USDC': + raise NotSupported(self.id + ' withdraw() only support USDC') + vaultAddress = None + vaultAddress, params = self.handle_option_and_params(params, 'withdraw', 'vaultAddress') + vaultAddress = self.format_vault_address(vaultAddress) + params = self.omit(params, 'vaultAddress') + nonce = self.milliseconds() + action: dict = {} + sig = None + if vaultAddress is not None: + action = { + 'type': 'vaultTransfer', + 'vaultAddress': '0x' + vaultAddress, + 'isDeposit': False, + 'usd': amount, + } + sig = self.sign_l1_action(action, nonce) + else: + isSandboxMode = self.safe_bool(self.options, 'sandboxMode', False) + payload: dict = { + 'hyperliquidChain': 'Testnet' if isSandboxMode else 'Mainnet', + 'destination': address, + 'amount': str(amount), + 'time': nonce, + } + sig = self.build_withdraw_sig(payload) + action = { + 'hyperliquidChain': payload['hyperliquidChain'], + 'signatureChainId': '0x66eee', # check self out + 'destination': address, + 'amount': str(amount), + 'time': nonce, + 'type': 'withdraw3', + } + request: dict = { + 'action': action, + 'nonce': nonce, + 'signature': sig, + } + response = self.privatePostExchange(request) + return self.parse_transaction(response) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # {status: 'ok', response: {type: 'default'}} + # + # fetchDeposits / fetchWithdrawals + # { + # "time":1724762307531, + # "hash":"0x620a234a7e0eb7930575040f59482a01050058b0802163b4767bfd9033e77781", + # "delta":{ + # "type":"accountClassTransfer", + # "usdc":"50.0", + # "toPerp":false + # } + # } + # + timestamp = self.safe_integer(transaction, 'time') + delta = self.safe_dict(transaction, 'delta', {}) + fee = None + feeCost = self.safe_integer(delta, 'fee') + if feeCost is not None: + fee = { + 'currency': 'USDC', + 'cost': feeCost, + } + internal = None + type = self.safe_string(delta, 'type') + if type is not None: + internal = (type == 'internalTransfer') + return { + 'info': transaction, + 'id': None, + 'txid': self.safe_string(transaction, 'hash'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': None, + 'address': None, + 'addressTo': self.safe_string(delta, 'destination'), + 'addressFrom': self.safe_string(delta, 'user'), + 'tag': None, + 'tagTo': None, + 'tagFrom': None, + 'type': None, + 'amount': self.safe_number(delta, 'usdc'), + 'currency': None, + 'status': self.safe_string(transaction, 'status'), + 'updated': None, + 'comment': None, + 'internal': internal, + 'fee': fee, + } + + def fetch_trading_fee(self, symbol: str, params={}) -> TradingFeeInterface: + """ + fetch the trading fees for a market + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.user]: user address, will default to self.walletAddress if not provided + :param str [params.subAccountAddress]: sub account user address + :returns dict: a `fee structure ` + """ + self.load_markets() + userAddress = None + userAddress, params = self.handle_public_address('fetchTradingFee', params) + market = self.market(symbol) + request: dict = { + 'type': 'userFees', + 'user': userAddress, + } + response = self.publicPostInfo(self.extend(request, params)) + # + # { + # "dailyUserVlm": [ + # { + # "date": "2024-07-08", + # "userCross": "0.0", + # "userAdd": "0.0", + # "exchange": "90597185.23639999" + # } + # ], + # "feeSchedule": { + # "cross": "0.00035", + # "add": "0.0001", + # "tiers": { + # "vip": [ + # { + # "ntlCutoff": "5000000.0", + # "cross": "0.0003", + # "add": "0.00005" + # } + # ], + # "mm": [ + # { + # "makerFractionCutoff": "0.005", + # "add": "-0.00001" + # } + # ] + # }, + # "referralDiscount": "0.04" + # }, + # "userCrossRate": "0.00035", + # "userAddRate": "0.0001", + # "activeReferralDiscount": "0.0" + # } + # + data: dict = { + 'userCrossRate': self.safe_string(response, 'userCrossRate'), + 'userAddRate': self.safe_string(response, 'userAddRate'), + } + return self.parse_trading_fee(data, market) + + def parse_trading_fee(self, fee: dict, market: Market = None) -> TradingFeeInterface: + # + # { + # "dailyUserVlm": [ + # { + # "date": "2024-07-08", + # "userCross": "0.0", + # "userAdd": "0.0", + # "exchange": "90597185.23639999" + # } + # ], + # "feeSchedule": { + # "cross": "0.00035", + # "add": "0.0001", + # "tiers": { + # "vip": [ + # { + # "ntlCutoff": "5000000.0", + # "cross": "0.0003", + # "add": "0.00005" + # } + # ], + # "mm": [ + # { + # "makerFractionCutoff": "0.005", + # "add": "-0.00001" + # } + # ] + # }, + # "referralDiscount": "0.04" + # }, + # "userCrossRate": "0.00035", + # "userAddRate": "0.0001", + # "activeReferralDiscount": "0.0" + # } + # + symbol = self.safe_symbol(None, market) + return { + 'info': fee, + 'symbol': symbol, + 'maker': self.safe_number(fee, 'userAddRate'), + 'taker': self.safe_number(fee, 'userCrossRate'), + 'percentage': None, + 'tierBased': None, + } + + def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + :param str [code]: unified currency code + :param int [since]: timestamp in ms of the earliest ledger entry + :param int [limit]: max number of ledger entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest ledger entry + :param str [params.subAccountAddress]: sub account user address + :returns dict: a `ledger structure ` + """ + self.load_markets() + userAddress = None + userAddress, params = self.handle_public_address('fetchLedger', params) + request: dict = { + 'type': 'userNonFundingLedgerUpdates', + 'user': userAddress, + } + if since is not None: + request['startTime'] = since + until = self.safe_integer(params, 'until') + if until is not None: + request['endTime'] = until + params = self.omit(params, ['until']) + response = self.publicPostInfo(self.extend(request, params)) + # + # [ + # { + # "time":1724762307531, + # "hash":"0x620a234a7e0eb7930575040f59482a01050058b0802163b4767bfd9033e77781", + # "delta":{ + # "type":"accountClassTransfer", + # "usdc":"50.0", + # "toPerp":false + # } + # } + # ] + # + return self.parse_ledger(response, None, since, limit) + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + # + # { + # "time":1724762307531, + # "hash":"0x620a234a7e0eb7930575040f59482a01050058b0802163b4767bfd9033e77781", + # "delta":{ + # "type":"accountClassTransfer", + # "usdc":"50.0", + # "toPerp":false + # } + # } + # + timestamp = self.safe_integer(item, 'time') + delta = self.safe_dict(item, 'delta', {}) + fee = None + feeCost = self.safe_integer(delta, 'fee') + if feeCost is not None: + fee = { + 'currency': 'USDC', + 'cost': feeCost, + } + type = self.safe_string(delta, 'type') + amount = self.safe_string(delta, 'usdc') + return self.safe_ledger_entry({ + 'info': item, + 'id': self.safe_string(item, 'hash'), + 'direction': None, + 'account': None, + 'referenceAccount': self.safe_string(delta, 'user'), + 'referenceId': self.safe_string(item, 'hash'), + 'type': self.parse_ledger_entry_type(type), + 'currency': None, + 'amount': self.parse_number(amount), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'before': None, + 'after': None, + 'status': None, + 'fee': fee, + }, currency) + + def parse_ledger_entry_type(self, type): + ledgerType: dict = { + 'internalTransfer': 'transfer', + 'accountClassTransfer': 'transfer', + } + return self.safe_string(ledgerType, type, type) + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all deposits made to an account + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch withdrawals for + :param str [params.subAccountAddress]: sub account user address + :param str [params.vaultAddress]: vault address + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + userAddress = None + userAddress, params = self.handle_public_address('fetchDepositsWithdrawals', params) + request: dict = { + 'type': 'userNonFundingLedgerUpdates', + 'user': userAddress, + } + if since is not None: + request['startTime'] = since + until = self.safe_integer(params, 'until') + if until is not None: + if since is None: + raise ArgumentsRequired(self.id + ' fetchDeposits requires since while until is set') + request['endTime'] = until + params = self.omit(params, ['until']) + response = self.publicPostInfo(self.extend(request, params)) + # + # [ + # { + # "time":1724762307531, + # "hash":"0x620a234a7e0eb7930575040f59482a01050058b0802163b4767bfd9033e77781", + # "delta":{ + # "type":"accountClassTransfer", + # "usdc":"50.0", + # "toPerp":false + # } + # } + # ] + # + records = self.extract_type_from_delta(response) + vaultAddress = None + vaultAddress, params = self.handle_option_and_params(params, 'fetchDepositsWithdrawals', 'vaultAddress') + vaultAddress = self.format_vault_address(vaultAddress) + deposits = [] + if vaultAddress is not None: + for i in range(0, len(records)): + record = records[i] + if record['type'] == 'vaultDeposit': + delta = self.safe_dict(record, 'delta') + if delta['vault'] == '0x' + vaultAddress: + deposits.append(record) + else: + deposits = self.filter_by_array(records, 'type', ['deposit'], False) + return self.parse_transactions(deposits, None, since, limit) + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch withdrawals for + :param str [params.subAccountAddress]: sub account user address + :param str [params.vaultAddress]: vault address + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + userAddress = None + userAddress, params = self.handle_public_address('fetchDepositsWithdrawals', params) + request: dict = { + 'type': 'userNonFundingLedgerUpdates', + 'user': userAddress, + } + if since is not None: + request['startTime'] = since + until = self.safe_integer(params, 'until') + if until is not None: + request['endTime'] = until + params = self.omit(params, ['until']) + response = self.publicPostInfo(self.extend(request, params)) + # + # [ + # { + # "time":1724762307531, + # "hash":"0x620a234a7e0eb7930575040f59482a01050058b0802163b4767bfd9033e77781", + # "delta":{ + # "type":"accountClassTransfer", + # "usdc":"50.0", + # "toPerp":false + # } + # } + # ] + # + records = self.extract_type_from_delta(response) + vaultAddress = None + vaultAddress, params = self.handle_option_and_params(params, 'fetchDepositsWithdrawals', 'vaultAddress') + vaultAddress = self.format_vault_address(vaultAddress) + withdrawals = [] + if vaultAddress is not None: + for i in range(0, len(records)): + record = records[i] + if record['type'] == 'vaultWithdraw': + delta = self.safe_dict(record, 'delta') + if delta['vault'] == '0x' + vaultAddress: + withdrawals.append(record) + else: + withdrawals = self.filter_by_array(records, 'type', ['withdraw'], False) + return self.parse_transactions(withdrawals, None, since, limit) + + def fetch_open_interests(self, symbols: Strings = None, params={}): + """ + Retrieves the open interest for a list of symbols + :param str[] [symbols]: Unified CCXT market symbol + :param dict [params]: exchange specific parameters + :returns dict} an open interest structure{@link https://docs.ccxt.com/#/?id=open-interest-structure: + """ + self.load_markets() + symbols = self.market_symbols(symbols) + swapMarkets = self.fetch_swap_markets() + return self.parse_open_interests(swapMarkets, symbols) + + def fetch_open_interest(self, symbol: str, params={}): + """ + retrieves the open interest of a contract trading pair + :param str symbol: unified CCXT market symbol + :param dict [params]: exchange specific parameters + :returns dict: an `open interest structure ` + """ + symbol = self.symbol(symbol) + self.load_markets() + ois = self.fetch_open_interests([symbol], params) + return ois[symbol] + + def parse_open_interest(self, interest, market: Market = None): + # + # { + # szDecimals: '2', + # name: 'HYPE', + # maxLeverage: '3', + # funding: '0.00014735', + # openInterest: '14677900.74', + # prevDayPx: '26.145', + # dayNtlVlm: '299643445.12560016', + # premium: '0.00081613', + # oraclePx: '27.569', + # markPx: '27.63', + # midPx: '27.599', + # impactPxs: ['27.5915', '27.6319'], + # dayBaseVlm: '10790652.83', + # baseId: 159 + # } + # + interest = self.safe_dict(interest, 'info', {}) + coin = self.safe_string(interest, 'name') + marketId = None + if coin is not None: + marketId = self.coin_to_market_id(coin) + return self.safe_open_interest({ + 'symbol': self.safe_symbol(marketId), + 'openInterestAmount': self.safe_number(interest, 'openInterest'), + 'openInterestValue': None, + 'timestamp': None, + 'datetime': None, + 'info': interest, + }, market) + + def fetch_funding_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch the history of funding payments paid and received on self account + :param str [symbol]: unified market symbol + :param int [since]: the earliest time in ms to fetch funding history for + :param int [limit]: the maximum number of funding history structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.subAccountAddress]: sub account user address + :returns dict: a `funding history structure ` + """ + self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + userAddress = None + userAddress, params = self.handle_public_address('fetchFundingHistory', params) + request: dict = { + 'user': userAddress, + 'type': 'userFunding', + } + if since is not None: + request['startTime'] = since + until = self.safe_integer(params, 'until') + params = self.omit(params, 'until') + if until is not None: + request['endTime'] = until + response = self.publicPostInfo(self.extend(request, params)) + # + # [ + # { + # "time": 1734026400057, + # "hash": "0x0000000000000000000000000000000000000000000000000000000000000000", + # "delta": { + # "type": "funding", + # "coin": "SOL", + # "usdc": "75.635093", + # "szi": "-7375.9", + # "fundingRate": "0.00004381", + # "nSamples": null + # } + # } + # ] + # + return self.parse_incomes(response, market, since, limit) + + def parse_income(self, income, market: Market = None): + # + # { + # "time": 1734026400057, + # "hash": "0x0000000000000000000000000000000000000000000000000000000000000000", + # "delta": { + # "type": "funding", + # "coin": "SOL", + # "usdc": "75.635093", + # "szi": "-7375.9", + # "fundingRate": "0.00004381", + # "nSamples": null + # } + # } + # + id = self.safe_string(income, 'hash') + timestamp = self.safe_integer(income, 'time') + delta = self.safe_dict(income, 'delta') + baseId = self.safe_string(delta, 'coin') + marketSymbol = baseId + '/USDC:USDC' + market = self.safe_market(marketSymbol) + symbol = market['symbol'] + amount = self.safe_string(delta, 'usdc') + code = self.safe_currency_code('USDC') + rate = self.safe_number(delta, 'fundingRate') + return { + 'info': income, + 'symbol': symbol, + 'code': code, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': id, + 'amount': self.parse_number(amount), + 'rate': rate, + } + + def reserve_request_weight(self, weight: Num, params={}) -> dict: + """ + Instead of trading to increase the address based rate limits, self action allows reserving additional actions for 0.0005 USDC per request. The cost is paid from the Perps balance. + :param number weight: the weight to reserve, 1 weight = 1 action, 0.0005 USDC per action + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a response object + """ + nonce = self.milliseconds() + request: dict = { + 'nonce': nonce, + } + action: dict = { + 'type': 'reserveRequestWeight', + 'weight': weight, + } + signature = self.sign_l1_action(action, nonce) + request['action'] = action + request['signature'] = signature + response = self.privatePostExchange(self.extend(request, params)) + return response + + def extract_type_from_delta(self, data=[]): + records = [] + for i in range(0, len(data)): + record = data[i] + record['type'] = record['delta']['type'] + records.append(record) + return records + + def format_vault_address(self, address: Str = None): + if address is None: + return None + if address.startswith('0x'): + return address.replace('0x', '') + return address + + def handle_public_address(self, methodName: str, params: dict): + userAux = None + userAux, params = self.handle_option_and_params_2(params, methodName, 'user', 'subAccountAddress') + user = userAux + user, params = self.handle_option_and_params(params, methodName, 'address', userAux) + if (user is not None) and (user != ''): + return [user, params] + if (self.walletAddress is not None) and (self.walletAddress != ''): + return [self.walletAddress, params] + raise ArgumentsRequired(self.id + ' ' + methodName + '() requires a user parameter inside \'params\' or the wallet address set') + + def coin_to_market_id(self, coin: Str): + if coin.find('/') > -1 or coin.find('@') > -1: + return coin # spot + return self.safe_currency_code(coin) + '/USDC:USDC' + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if not response: + return None # fallback to default error handler + # {"status":"err","response":"User or API Wallet 0xb8a6f8b26223de27c31938d56e470a5b832703a5 does not exist."} + # + # { + # status: 'ok', + # response: {type: 'order', data: {statuses: [{error: 'Insufficient margin to place order. asset=4'}]}} + # } + # {"status":"ok","response":{"type":"order","data":{"statuses":[{"error":"Insufficient margin to place order. asset=84"}]}}} + # + # {"status":"unknownOid"} + # + status = self.safe_string(response, 'status', '') + error = self.safe_string(response, 'error') + message = None + if status == 'err': + message = self.safe_string(response, 'response') + elif status == 'unknownOid': + raise OrderNotFound(self.id + ' ' + body) # {"status":"unknownOid"} + elif error is not None: + message = error + else: + responsePayload = self.safe_dict(response, 'response', {}) + data = self.safe_dict(responsePayload, 'data', {}) + statuses = self.safe_list(data, 'statuses', []) + for i in range(0, len(statuses)): + message = self.safe_string(statuses[i], 'error') + if message is not None: + break + feedback = self.id + ' ' + body + nonEmptyMessage = ((message is not None) and (message != '')) + if nonEmptyMessage: + self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + if nonEmptyMessage: + raise ExchangeError(feedback) # unknown message + return None + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = self.implode_hostname(self.urls['api'][api]) + '/' + path + if method == 'POST': + headers = { + 'Content-Type': 'application/json', + } + body = self.json(params) + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def calculate_rate_limiter_cost(self, api, method, path, params, config={}): + if ('byType' in config) and ('type' in params): + type = params['type'] + byType = config['byType'] + if type in byType: + return byType[type] + return self.safe_value(config, 'cost', 1) + + def parse_create_edit_order_args(self, id: Str, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + market = self.market(symbol) + vaultAddress = None + vaultAddress, params = self.handle_option_and_params_2(params, 'createOrder', 'vaultAddress', 'subAccountAddress') + vaultAddress = self.format_vault_address(vaultAddress) + symbol = market['symbol'] + order = { + 'symbol': symbol, + 'type': type, + 'side': side, + 'amount': amount, + 'price': price, + 'params': params, + } + globalParams = {} + if vaultAddress is not None: + globalParams['vaultAddress'] = vaultAddress + if id is not None: + order['id'] = id + return [order, globalParams] diff --git a/ccxt/independentreserve.py b/ccxt/independentreserve.py new file mode 100644 index 0000000..306db46 --- /dev/null +++ b/ccxt/independentreserve.py @@ -0,0 +1,1057 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.independentreserve import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currency, DepositAddress, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Ticker, Trade, TradingFees, Transaction +from typing import List +from ccxt.base.errors import BadRequest +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class independentreserve(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(independentreserve, self).describe(), { + 'id': 'independentreserve', + 'name': 'Independent Reserve', + 'countries': ['AU', 'NZ'], # Australia, New Zealand + 'rateLimit': 1000, + 'pro': True, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelOrder': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createOrder': True, + 'createReduceOnlyOrder': False, + 'createStopLimitOrder': False, + 'createStopMarketOrder': False, + 'createStopOrder': False, + 'fetchAllGreeks': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': False, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrice': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchPosition': False, + 'fetchPositionForSymbolWs': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsForSymbolWs': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': True, + 'fetchUnderlyingAssets': False, + 'fetchVolatilityHistory': False, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'withdraw': True, + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/51840849/87182090-1e9e9080-c2ec-11ea-8e49-563db9a38f37.jpg', + 'api': { + 'public': 'https://api.independentreserve.com/Public', + 'private': 'https://api.independentreserve.com/Private', + }, + 'www': 'https://www.independentreserve.com', + 'doc': 'https://www.independentreserve.com/API', + }, + 'api': { + 'public': { + 'get': [ + 'GetValidPrimaryCurrencyCodes', + 'GetValidSecondaryCurrencyCodes', + 'GetValidLimitOrderTypes', + 'GetValidMarketOrderTypes', + 'GetValidOrderTypes', + 'GetValidTransactionTypes', + 'GetMarketSummary', + 'GetOrderBook', + 'GetAllOrders', + 'GetTradeHistorySummary', + 'GetRecentTrades', + 'GetFxRates', + 'GetOrderMinimumVolumes', + 'GetCryptoWithdrawalFees', # deprecated - replaced by GetCryptoWithdrawalFees2(docs removed) + 'GetCryptoWithdrawalFees2', + 'GetNetworks', + 'GetPrimaryCurrencyConfig2', + ], + }, + 'private': { + 'post': [ + 'GetOpenOrders', + 'GetClosedOrders', + 'GetClosedFilledOrders', + 'GetOrderDetails', + 'GetAccounts', + 'GetTransactions', + 'GetFiatBankAccounts', + 'GetDigitalCurrencyDepositAddress', # deprecated - replaced by GetDigitalCurrencyDepositAddress2(docs removed) + 'GetDigitalCurrencyDepositAddress2', + 'GetDigitalCurrencyDepositAddresses', # deprecated - replaced by GetDigitalCurrencyDepositAddresses2(docs removed) + 'GetDigitalCurrencyDepositAddresses2', + 'GetTrades', + 'GetBrokerageFees', + 'GetDigitalCurrencyWithdrawal', + 'PlaceLimitOrder', + 'PlaceMarketOrder', + 'CancelOrder', + 'SynchDigitalCurrencyDepositAddressWithBlockchain', + 'RequestFiatWithdrawal', + 'WithdrawFiatCurrency', + 'WithdrawDigitalCurrency', # deprecated - replaced by WithdrawCrypto(docs removed) + 'WithdrawCrypto', + ], + }, + }, + 'fees': { + 'trading': { + 'taker': self.parse_number('0.005'), + 'maker': self.parse_number('0.005'), + 'percentage': True, + 'tierBased': False, + }, + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': False, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': False, + 'FOK': False, + 'PO': False, + 'GTD': False, + }, + 'hedged': False, + 'selfTradePrevention': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': False, + 'marketBuyRequiresPrice': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, # todo + 'daysBack': None, + 'untilDays': None, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 100, # todo + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 100, # todo + 'daysBack': None, + 'daysBackCanceled': None, + 'untilDays': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': None, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'commonCurrencies': { + 'PLA': 'PlayChip', + }, + 'precisionMode': TICK_SIZE, + 'options': { + 'defaultNetworks': { + 'USDT': 'Ethereum', + 'USDC': 'Ethereum', + 'BTC': 'Bitcoin', + 'BCH': 'BitcoinCash', + 'ETH': 'Ethereum', + 'LTC': 'Litecoin', + 'XRP': 'XrpLedger', + 'ZRX': 'Ethereum', + 'EOS': 'EosIo', + 'XLM': 'Stellar', + 'BAT': 'Ethereum', + 'ETC': 'EthereumClassic', + 'LINK': 'Ethereum', + 'MKR': 'Ethereum', + 'DAI': 'Ethereum', + 'COMP': 'Ethereum', + 'SNX': 'Ethereum', + 'YFI': 'Ethereum', + 'AAVE': 'Ethereum', + 'GRT': 'Ethereum', + 'DOT': 'Polkadot', + 'UNI': 'Ethereum', + 'ADA': 'Cardano', + 'MATIC': 'Ethereum', + 'DOGE': 'Dogecoin', + 'SOL': 'Solana', + 'MANA': 'Ethereum', + 'SAND': 'Ethereum', + 'SHIB': 'Ethereum', + 'TRX': 'Tron', + 'RENDER': 'Solana', + 'WIF': 'Solana', + 'RLUSD': 'Ethereum', + 'PEPE': 'Ethereum', + }, + 'networks': { + 'BTC': 'Bitcoin', + 'ETH': 'Ethereum', + 'BCH': 'BitcoinCash', + 'LTC': 'Litecoin', + 'XRP': 'XrpLedger', + 'EOS': 'EosIo', + 'XLM': 'Stellar', + 'ETC': 'EthereumClassic', + 'BSV': 'BitcoinSV', + 'DOGE': 'Dogecoin', + 'DOT': 'Polkadot', + 'ADA': 'Cardano', + 'SOL': 'Solana', + 'TRX': 'Tron', + }, + }, + }) + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for independentreserve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + baseCurrenciesPromise = self.publicGetGetValidPrimaryCurrencyCodes(params) + # ['Xbt', 'Eth', 'Usdt', ...] + quoteCurrenciesPromise = self.publicGetGetValidSecondaryCurrencyCodes(params) + # ['Aud', 'Usd', 'Nzd', 'Sgd'] + limitsPromise = self.publicGetGetOrderMinimumVolumes(params) + baseCurrencies, quoteCurrencies, limits = [baseCurrenciesPromise, quoteCurrenciesPromise, limitsPromise] + # + # { + # "Xbt": 0.0001, + # "Eth": 0.001, + # "Ltc": 0.01, + # "Xrp": 1.0, + # } + # + result = [] + for i in range(0, len(baseCurrencies)): + baseId = baseCurrencies[i] + base = self.safe_currency_code(baseId) + minAmount = self.safe_number(limits, baseId) + for j in range(0, len(quoteCurrencies)): + quoteId = quoteCurrencies[j] + quote = self.safe_currency_code(quoteId) + id = baseId + '/' + quoteId + result.append({ + 'id': id, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': None, + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': None, + 'price': None, + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': minAmount, + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + 'info': id, + }) + return result + + def parse_balance(self, response) -> Balances: + result: dict = {'info': response} + for i in range(0, len(response)): + balance = response[i] + currencyId = self.safe_string(balance, 'CurrencyCode') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(balance, 'AvailableBalance') + account['total'] = self.safe_string(balance, 'TotalBalance') + result[code] = account + return self.safe_balance(result) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + response = self.privatePostGetAccounts(params) + return self.parse_balance(response) + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'primaryCurrencyCode': market['baseId'], + 'secondaryCurrencyCode': market['quoteId'], + } + response = self.publicGetGetOrderBook(self.extend(request, params)) + timestamp = self.parse8601(self.safe_string(response, 'CreatedTimestampUtc')) + return self.parse_order_book(response, market['symbol'], timestamp, 'BuyOrders', 'SellOrders', 'Price', 'Volume') + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # { + # "DayHighestPrice":43489.49, + # "DayLowestPrice":41998.32, + # "DayAvgPrice":42743.9, + # "DayVolumeXbt":44.54515625000, + # "DayVolumeXbtInSecondaryCurrrency":0.12209818, + # "CurrentLowestOfferPrice":43619.64, + # "CurrentHighestBidPrice":43153.58, + # "LastPrice":43378.43, + # "PrimaryCurrencyCode":"Xbt", + # "SecondaryCurrencyCode":"Usd", + # "CreatedTimestampUtc":"2022-01-14T22:52:29.5029223Z" + # } + timestamp = self.parse8601(self.safe_string(ticker, 'CreatedTimestampUtc')) + baseId = self.safe_string(ticker, 'PrimaryCurrencyCode') + quoteId = self.safe_string(ticker, 'SecondaryCurrencyCode') + defaultMarketId = None + if (baseId is not None) and (quoteId is not None): + defaultMarketId = baseId + '/' + quoteId + market = self.safe_market(defaultMarketId, market, '/') + symbol = market['symbol'] + last = self.safe_string(ticker, 'LastPrice') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'DayHighestPrice'), + 'low': self.safe_string(ticker, 'DayLowestPrice'), + 'bid': self.safe_string(ticker, 'CurrentHighestBidPrice'), + 'bidVolume': None, + 'ask': self.safe_string(ticker, 'CurrentLowestOfferPrice'), + 'askVolume': None, + 'vwap': None, + 'open': None, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': self.safe_string(ticker, 'DayAvgPrice'), + 'baseVolume': self.safe_string(ticker, 'DayVolumeXbtInSecondaryCurrrency'), + 'quoteVolume': None, + 'info': ticker, + }, market) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'primaryCurrencyCode': market['baseId'], + 'secondaryCurrencyCode': market['quoteId'], + } + response = self.publicGetGetMarketSummary(self.extend(request, params)) + # { + # "DayHighestPrice":43489.49, + # "DayLowestPrice":41998.32, + # "DayAvgPrice":42743.9, + # "DayVolumeXbt":44.54515625000, + # "DayVolumeXbtInSecondaryCurrrency":0.12209818, + # "CurrentLowestOfferPrice":43619.64, + # "CurrentHighestBidPrice":43153.58, + # "LastPrice":43378.43, + # "PrimaryCurrencyCode":"Xbt", + # "SecondaryCurrencyCode":"Usd", + # "CreatedTimestampUtc":"2022-01-14T22:52:29.5029223Z" + # } + return self.parse_ticker(response, market) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # fetchOrder + # + # { + # "OrderGuid": "c7347e4c-b865-4c94-8f74-d934d4b0b177", + # "CreatedTimestampUtc": "2014-09-23T12:39:34.3817763Z", + # "Type": "MarketBid", + # "VolumeOrdered": 5.0, + # "VolumeFilled": 5.0, + # "Price": null, + # "AvgPrice": 100.0, + # "ReservedAmount": 0.0, + # "Status": "Filled", + # "PrimaryCurrencyCode": "Xbt", + # "SecondaryCurrencyCode": "Usd" + # } + # + # fetchOpenOrders & fetchClosedOrders + # + # { + # "OrderGuid": "b8f7ad89-e4e4-4dfe-9ea3-514d38b5edb3", + # "CreatedTimestampUtc": "2020-09-08T03:04:18.616367Z", + # "OrderType": "LimitOffer", + # "Volume": 0.0005, + # "Outstanding": 0.0005, + # "Price": 113885.83, + # "AvgPrice": 113885.83, + # "Value": 56.94, + # "Status": "Open", + # "PrimaryCurrencyCode": "Xbt", + # "SecondaryCurrencyCode": "Usd", + # "FeePercent": 0.005, + # } + # + # cancelOrder + # + # { + # "AvgPrice": 455.48, + # "CreatedTimestampUtc": "2022-08-05T06:42:11.3032208Z", + # "OrderGuid": "719c495c-a39e-4884-93ac-280b37245037", + # "Price": 485.76, + # "PrimaryCurrencyCode": "Xbt", + # "ReservedAmount": 0.358, + # "SecondaryCurrencyCode": "Usd", + # "Status": "Cancelled", + # "Type": "LimitOffer", + # "VolumeFilled": 0, + # "VolumeOrdered": 0.358 + # } + symbol = None + baseId = self.safe_string(order, 'PrimaryCurrencyCode') + quoteId = self.safe_string(order, 'SecondaryCurrencyCode') + base = None + quote = None + if (baseId is not None) and (quoteId is not None): + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + symbol = base + '/' + quote + elif market is not None: + symbol = market['symbol'] + base = market['base'] + quote = market['quote'] + orderType = self.safe_string_2(order, 'Type', 'OrderType') + side = None + if orderType is not None: + if orderType.find('Bid') >= 0: + side = 'buy' + elif orderType.find('Offer') >= 0: + side = 'sell' + if orderType.find('Market') >= 0: + orderType = 'market' + elif orderType.find('Limit') >= 0: + orderType = 'limit' + timestamp = self.parse8601(self.safe_string(order, 'CreatedTimestampUtc')) + filled = self.safe_string(order, 'VolumeFilled') + feeRate = self.safe_string(order, 'FeePercent') + feeCost = None + if feeRate is not None and filled is not None: + feeCost = Precise.string_mul(feeRate, filled) + return self.safe_order({ + 'info': order, + 'id': self.safe_string(order, 'OrderGuid'), + 'clientOrderId': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'symbol': symbol, + 'type': orderType, + 'timeInForce': None, + 'postOnly': None, + 'side': side, + 'price': self.safe_string(order, 'Price'), + 'triggerPrice': None, + 'cost': self.safe_string(order, 'Value'), + 'average': self.safe_string(order, 'AvgPrice'), + 'amount': self.safe_string_2(order, 'VolumeOrdered', 'Volume'), + 'filled': filled, + 'remaining': self.safe_string(order, 'Outstanding'), + 'status': self.parse_order_status(self.safe_string(order, 'Status')), + 'fee': { + 'rate': feeRate, + 'cost': feeCost, + 'currency': base, + }, + 'trades': None, + }, market) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'Open': 'open', + 'PartiallyFilled': 'open', + 'Filled': 'closed', + 'PartiallyFilledAndCancelled': 'canceled', + 'Cancelled': 'canceled', + 'PartiallyFilledAndExpired': 'canceled', + 'Expired': 'canceled', + } + return self.safe_string(statuses, status, status) + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + response = self.privatePostGetOrderDetails(self.extend({ + 'orderGuid': id, + }, params)) + market = None + if symbol is not None: + market = self.market(symbol) + return self.parse_order(response, market) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + request = self.ordered({}) + market = None + if symbol is not None: + market = self.market(symbol) + request['primaryCurrencyCode'] = market['baseId'] + request['secondaryCurrencyCode'] = market['quoteId'] + if limit is None: + limit = 50 + request['pageIndex'] = 1 + request['pageSize'] = limit + response = self.privatePostGetOpenOrders(self.extend(request, params)) + data = self.safe_list(response, 'Data', []) + return self.parse_orders(data, market, since, limit) + + def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + request = self.ordered({}) + market = None + if symbol is not None: + market = self.market(symbol) + request['primaryCurrencyCode'] = market['baseId'] + request['secondaryCurrencyCode'] = market['quoteId'] + if limit is None: + limit = 50 + request['pageIndex'] = 1 + request['pageSize'] = limit + response = self.privatePostGetClosedOrders(self.extend(request, params)) + data = self.safe_list(response, 'Data', []) + return self.parse_orders(data, market, since, limit) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = 50, params={}): + """ + fetch all trades made by the user + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + pageIndex = self.safe_integer(params, 'pageIndex', 1) + if limit is None: + limit = 50 + request = self.ordered({ + 'pageIndex': pageIndex, + 'pageSize': limit, + }) + response = self.privatePostGetTrades(self.extend(request, params)) + market = None + if symbol is not None: + market = self.market(symbol) + return self.parse_trades(response['Data'], market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + timestamp = self.parse8601(trade['TradeTimestampUtc']) + id = self.safe_string(trade, 'TradeGuid') + orderId = self.safe_string(trade, 'OrderGuid') + priceString = self.safe_string_2(trade, 'Price', 'SecondaryCurrencyTradePrice') + amountString = self.safe_string_2(trade, 'VolumeTraded', 'PrimaryCurrencyAmount') + price = self.parse_number(priceString) + amount = self.parse_number(amountString) + cost = self.parse_number(Precise.string_mul(priceString, amountString)) + baseId = self.safe_string(trade, 'PrimaryCurrencyCode') + quoteId = self.safe_string(trade, 'SecondaryCurrencyCode') + marketId = None + if (baseId is not None) and (quoteId is not None): + marketId = baseId + '/' + quoteId + symbol = self.safe_symbol(marketId, market, '/') + side = self.safe_string(trade, 'OrderType') + if side is not None: + if side.find('Bid') >= 0: + side = 'buy' + elif side.find('Offer') >= 0: + side = 'sell' + return self.safe_trade({ + 'id': id, + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'order': orderId, + 'type': None, + 'side': side, + 'takerOrMaker': None, + 'price': price, + 'amount': amount, + 'cost': cost, + 'fee': None, + }, market) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'primaryCurrencyCode': market['baseId'], + 'secondaryCurrencyCode': market['quoteId'], + 'numberOfRecentTradesToRetrieve': 50, # max = 50 + } + response = self.publicGetGetRecentTrades(self.extend(request, params)) + return self.parse_trades(response['Trades'], market, since, limit) + + def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + self.load_markets() + response = self.privatePostGetBrokerageFees(params) + # + # [ + # { + # "CurrencyCode": "Xbt", + # "Fee": 0.005 + # } + # ... + # ] + # + fees: dict = {} + for i in range(0, len(response)): + fee = response[i] + currencyId = self.safe_string(fee, 'CurrencyCode') + code = self.safe_currency_code(currencyId) + tradingFee = self.safe_number(fee, 'Fee') + fees[code] = { + 'info': fee, + 'fee': tradingFee, + } + result: dict = {} + for i in range(0, len(self.symbols)): + symbol = self.symbols[i] + market = self.market(symbol) + fee = self.safe_value(fees, market['base'], {}) + result[symbol] = { + 'info': self.safe_value(fee, 'info'), + 'symbol': symbol, + 'maker': self.safe_number(fee, 'fee'), + 'taker': self.safe_number(fee, 'fee'), + 'percentage': True, + 'tierBased': True, + } + return result + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + orderType = self.capitalize(type) + orderType += 'Offer' if (side == 'sell') else 'Bid' + request = self.ordered({ + 'primaryCurrencyCode': market['baseId'], + 'secondaryCurrencyCode': market['quoteId'], + 'orderType': orderType, + }) + response = None + request['volume'] = amount + if type == 'limit': + request['price'] = price + response = self.privatePostPlaceLimitOrder(self.extend(request, params)) + else: + response = self.privatePostPlaceMarketOrder(self.extend(request, params)) + return self.safe_order({ + 'info': response, + 'id': response['OrderGuid'], + }, market) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://www.independentreserve.com/features/api#CancelOrder + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + request: dict = { + 'orderGuid': id, + } + response = self.privatePostCancelOrder(self.extend(request, params)) + # + # { + # "AvgPrice": 455.48, + # "CreatedTimestampUtc": "2022-08-05T06:42:11.3032208Z", + # "OrderGuid": "719c495c-a39e-4884-93ac-280b37245037", + # "Price": 485.76, + # "PrimaryCurrencyCode": "Xbt", + # "ReservedAmount": 0.358, + # "SecondaryCurrencyCode": "Usd", + # "Status": "Cancelled", + # "Type": "LimitOffer", + # "VolumeFilled": 0, + # "VolumeOrdered": 0.358 + # } + # + return self.parse_order(response) + + def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://www.independentreserve.com/features/api#GetDigitalCurrencyDepositAddress + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'primaryCurrencyCode': currency['id'], + } + response = self.privatePostGetDigitalCurrencyDepositAddress(self.extend(request, params)) + # + # { + # Tag: '3307446684', + # DepositAddress: 'GCCQH4HACMRAD56EZZZ4TOIDQQRVNADMJ35QOFWF4B2VQGODMA2WVQ22', + # LastCheckedTimestampUtc: '2024-02-20T11:13:35.6912985Z', + # NextUpdateTimestampUtc: '2024-02-20T11:14:56.5112394Z' + # } + # + return self.parse_deposit_address(response) + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + # + # { + # Tag: '3307446684', + # DepositAddress: 'GCCQH4HACMRAD56EZZZ4TOIDQQRVNADMJ35QOFWF4B2VQGODMA2WVQ22', + # LastCheckedTimestampUtc: '2024-02-20T11:13:35.6912985Z', + # NextUpdateTimestampUtc: '2024-02-20T11:14:56.5112394Z' + # } + # + address = self.safe_string(depositAddress, 'DepositAddress') + self.check_address(address) + return { + 'info': depositAddress, + 'currency': self.safe_string(currency, 'code'), + 'network': None, + 'address': address, + 'tag': self.safe_string(depositAddress, 'Tag'), + } + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://www.independentreserve.com/features/api#WithdrawDigitalCurrency + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + + EXCHANGE SPECIFIC PARAMETERS + :param dict [params.comment]: withdrawal comment, should not exceed 500 characters + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.load_markets() + currency = self.currency(code) + request: dict = { + 'primaryCurrencyCode': currency['id'], + 'withdrawalAddress': address, + 'amount': self.currency_to_precision(code, amount), + } + if tag is not None: + request['destinationTag'] = tag + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + if networkCode is not None: + raise BadRequest(self.id + ' withdraw() does not accept params["networkCode"]') + response = self.privatePostWithdrawDigitalCurrency(self.extend(request, params)) + # + # { + # "TransactionGuid": "dc932e19-562b-4c50-821e-a73fd048b93b", + # "PrimaryCurrencyCode": "Bch", + # "CreatedTimestampUtc": "2020-04-01T05:26:30.5093622+00:00", + # "Amount": { + # "Total": 0.1231, + # "Fee": 0.0001 + # }, + # "Destination": { + # "Address": "bc1qhpqxkjpvgkckw530yfmxyr53c94q8f4273a7ez", + # "Tag": null + # }, + # "Status": "Pending", + # "Transaction": null + # } + # + return self.parse_transaction(response, currency) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # { + # "TransactionGuid": "dc932e19-562b-4c50-821e-a73fd048b93b", + # "PrimaryCurrencyCode": "Bch", + # "CreatedTimestampUtc": "2020-04-01T05:26:30.5093622+00:00", + # "Amount": { + # "Total": 0.1231, + # "Fee": 0.0001 + # }, + # "Destination": { + # "Address": "bc1qhpqxkjpvgkckw530yfmxyr53c94q8f4273a7ez", + # "Tag": null + # }, + # "Status": "Pending", + # "Transaction": null + # } + # + amount = self.safe_dict(transaction, 'Amount') + destination = self.safe_dict(transaction, 'Destination') + currencyId = self.safe_string(transaction, 'PrimaryCurrencyCode') + datetime = self.safe_string(transaction, 'CreatedTimestampUtc') + address = self.safe_string(destination, 'Address') + tag = self.safe_string(destination, 'Tag') + code = self.safe_currency_code(currencyId, currency) + return { + 'info': transaction, + 'id': self.safe_string(transaction, 'TransactionGuid'), + 'txid': None, + 'type': 'withdraw', + 'currency': code, + 'network': None, + 'amount': self.safe_number(amount, 'Total'), + 'status': self.safe_string(transaction, 'Status'), + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + 'address': address, + 'addressFrom': None, + 'addressTo': address, + 'tag': tag, + 'tagFrom': None, + 'tagTo': tag, + 'updated': None, + 'comment': None, + 'fee': { + 'currency': code, + 'cost': self.safe_number(amount, 'Fee'), + 'rate': None, + }, + 'internal': False, + } + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = self.urls['api'][api] + '/' + path + if api == 'public': + if params: + url += '?' + self.urlencode(params) + else: + self.check_required_credentials() + nonce = self.nonce() + auth = [ + url, + 'apiKey=' + self.apiKey, + 'nonce=' + str(nonce), + ] + keys = list(params.keys()) + for i in range(0, len(keys)): + key = keys[i] + value = str(params[key]) + auth.append(key + '=' + value) + message = ','.join(auth) + signature = self.hmac(self.encode(message), self.encode(self.secret), hashlib.sha256) + query = self.ordered({}) + query['apiKey'] = self.apiKey + query['nonce'] = nonce + query['signature'] = signature.upper() + for i in range(0, len(keys)): + key = keys[i] + query[key] = params[key] + body = self.json(query) + headers = {'Content-Type': 'application/json'} + return {'url': url, 'method': method, 'body': body, 'headers': headers} diff --git a/ccxt/indodax.py b/ccxt/indodax.py new file mode 100644 index 0000000..c4bcb49 --- /dev/null +++ b/ccxt/indodax.py @@ -0,0 +1,1413 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.indodax import ImplicitAPI +import hashlib +import math +from ccxt.base.types import Any, Balances, Currency, DepositAddress, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class indodax(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(indodax, self).describe(), { + 'id': 'indodax', + 'name': 'INDODAX', + 'countries': ['ID'], # Indonesia + # 10 requests per second for making trades => 1000ms / 10 = 100ms + # 180 requests per minute(public endpoints) = 2 requests per second => cost = (1000ms / rateLimit) / 2 = 5 + 'rateLimit': 50, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelAllOrders': False, + 'cancelOrder': True, + 'cancelOrders': False, + 'closeAllPositions': False, + 'closePosition': False, + 'createDepositAddress': False, + 'createOrder': True, + 'createReduceOnlyOrder': False, + 'createStopLimitOrder': False, + 'createStopMarketOrder': False, + 'createStopOrder': False, + 'fetchAllGreeks': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': False, + 'fetchDeposit': False, + 'fetchDepositAddress': 'emulated', + 'fetchDepositAddresses': True, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': False, + 'fetchDepositsWithdrawals': True, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrice': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': False, + 'fetchPosition': False, + 'fetchPositionForSymbolWs': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsForSymbolWs': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': False, + 'fetchTransactionFee': True, + 'fetchTransactionFees': False, + 'fetchTransactions': 'emulated', + 'fetchTransfer': False, + 'fetchTransfers': False, + 'fetchUnderlyingAssets': False, + 'fetchVolatilityHistory': False, + 'fetchWithdrawal': False, + 'fetchWithdrawals': False, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'transfer': False, + 'withdraw': True, + }, + 'version': '2.0', # 9 April 2018 + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/51840849/87070508-9358c880-c221-11ea-8dc5-5391afbbb422.jpg', + 'api': { + 'public': 'https://indodax.com', + 'private': 'https://indodax.com/tapi', + }, + 'www': 'https://www.indodax.com', + 'doc': 'https://github.com/btcid/indodax-official-api-docs', + 'referral': 'https://indodax.com/ref/testbitcoincoid/1', + }, + 'api': { + 'public': { + 'get': { + 'api/server_time': 5, + 'api/pairs': 5, + 'api/price_increments': 5, + 'api/summaries': 5, + 'api/ticker/{pair}': 5, + 'api/ticker_all': 5, + 'api/trades/{pair}': 5, + 'api/depth/{pair}': 5, + 'tradingview/history_v2': 5, + }, + }, + 'private': { + 'post': { + 'getInfo': 4, + 'transHistory': 4, + 'trade': 1, + 'tradeHistory': 4, # TODO add fetchMyTrades + 'openOrders': 4, + 'orderHistory': 4, + 'getOrder': 4, + 'cancelOrder': 4, + 'withdrawFee': 4, + 'withdrawCoin': 4, + 'listDownline': 4, + 'checkDownline': 4, + 'createVoucher': 4, # partner only + }, + }, + }, + 'fees': { + 'trading': { + 'tierBased': False, + 'percentage': True, + 'maker': 0, + 'taker': 0.003, + }, + }, + 'exceptions': { + 'exact': { + 'invalid_pair': BadSymbol, # {"error":"invalid_pair","error_description":"Invalid Pair"} + 'Insufficient balance.': InsufficientFunds, + 'invalid order.': OrderNotFound, + 'Invalid credentials. API not found or session has expired.': AuthenticationError, + 'Invalid credentials. Bad sign.': AuthenticationError, + }, + 'broad': { + 'Minimum price': InvalidOrder, + 'Minimum order': InvalidOrder, + }, + }, + 'timeframes': { + '1m': '1', + '15m': '15', + '30m': '30', + '1h': '60', + '4h': '240', + '1d': '1D', + '3d': '3D', + '1w': '1W', + }, + # exchange-specific options + 'options': { + 'recvWindow': 5 * 1000, # default 5 sec + 'timeDifference': 0, # the difference between system clock and exchange clock + 'adjustForTimeDifference': False, # controls the adjustment logic upon instantiation + 'networks': { + 'XLM': 'Stellar Token', + 'BSC': 'bep20', + 'TRC20': 'trc20', + 'MATIC': 'polygon', + # 'BEP2': 'bep2', + # 'ARB': 'arb', + # 'ERC20': 'erc20', + # 'KIP7': 'kip7', + # 'MAINNET': 'mainnet', # TODO: does mainnet just mean the default? + # 'OEP4': 'oep4', + # 'OP': 'op', + # 'SPL': 'spl', + # 'TRC10': 'trc10', + # 'ZRC2': 'zrc2' + # 'ETH': 'eth' + # 'BASE': 'base' + }, + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': False, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, # todo implementation + 'FOK': False, + 'PO': False, + 'GTD': False, + }, + 'hedged': False, + 'selfTradePrevention': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': False, + 'marketBuyRequiresPrice': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': None, # todo implement + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 100000, # todo + 'daysBackCanceled': 1, + 'untilDays': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOHLCV': { + 'limit': 2000, # todo: not in request + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'commonCurrencies': { + 'STR': 'XLM', + 'BCHABC': 'BCH', + 'BCHSV': 'BSV', + 'DRK': 'DASH', + 'NEM': 'XEM', + }, + 'precisionMode': TICK_SIZE, + }) + + def nonce(self): + return self.milliseconds() - self.options['timeDifference'] + + def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://github.com/btcid/indodax-official-api-docs/blob/master/Public-RestAPI.md#server-time + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = self.publicGetApiServerTime(params) + # + # { + # "timezone": "UTC", + # "server_time": 1571205969552 + # } + # + return self.safe_integer(response, 'server_time') + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for indodax + + https://github.com/btcid/indodax-official-api-docs/blob/master/Public-RestAPI.md#pairs + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = self.publicGetApiPairs(params) + # + # [ + # { + # "id": "btcidr", + # "symbol": "BTCIDR", + # "base_currency": "idr", + # "traded_currency": "btc", + # "traded_currency_unit": "BTC", + # "description": "BTC/IDR", + # "ticker_id": "btc_idr", + # "volume_precision": 0, + # "price_precision": 1000, + # "price_round": 8, + # "pricescale": 1000, + # "trade_min_base_currency": 10000, + # "trade_min_traded_currency": 0.00007457, + # "has_memo": False, + # "memo_name": False, + # "has_payment_id": False, + # "trade_fee_percent": 0.3, + # "url_logo": "https://indodax.com/v2/logo/svg/color/btc.svg", + # "url_logo_png": "https://indodax.com/v2/logo/png/color/btc.png", + # "is_maintenance": 0 + # } + # ] + # + result = [] + for i in range(0, len(response)): + market = response[i] + id = self.safe_string(market, 'id') + baseId = self.safe_string(market, 'traded_currency') + quoteId = self.safe_string(market, 'base_currency') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + isMaintenance = self.safe_integer(market, 'is_maintenance') + result.append({ + 'id': id, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': False if isMaintenance else True, + 'contract': False, + 'linear': None, + 'inverse': None, + 'taker': self.safe_number(market, 'trade_fee_percent'), + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'percentage': True, + 'precision': { + 'amount': self.parse_number('1e-8'), + 'price': self.parse_number(self.parse_precision(self.safe_string(market, 'price_round'))), + 'cost': self.parse_number(self.parse_precision(self.safe_string(market, 'volume_precision'))), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(market, 'trade_min_traded_currency'), + 'max': None, + }, + 'price': { + 'min': self.safe_number(market, 'trade_min_base_currency'), + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + 'info': market, + }) + return result + + def parse_balance(self, response) -> Balances: + balances = self.safe_value(response, 'return', {}) + free = self.safe_value(balances, 'balance', {}) + used = self.safe_value(balances, 'balance_hold', {}) + timestamp = self.safe_timestamp(balances, 'server_time') + result: dict = { + 'info': response, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + currencyIds = list(free.keys()) + for i in range(0, len(currencyIds)): + currencyId = currencyIds[i] + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(free, currencyId) + account['used'] = self.safe_string(used, currencyId) + result[code] = account + return self.safe_balance(result) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://github.com/btcid/indodax-official-api-docs/blob/master/Private-RestAPI.md#get-info-endpoint + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + response = self.privatePostGetInfo(params) + # + # { + # "success":1, + # "return":{ + # "server_time":1619562628, + # "balance":{ + # "idr":167, + # "btc":"0.00000000", + # "1inch":"0.00000000", + # }, + # "balance_hold":{ + # "idr":0, + # "btc":"0.00000000", + # "1inch":"0.00000000", + # }, + # "address":{ + # "btc":"1KMntgzvU7iTSgMBWc11nVuJjAyfW3qJyk", + # "1inch":"0x1106c8bb3172625e1f411c221be49161dac19355", + # "xrp":"rwWr7KUZ3ZFwzgaDGjKBysADByzxvohQ3C", + # "zrx":"0x1106c8bb3172625e1f411c221be49161dac19355" + # }, + # "user_id":"276011", + # "name":"", + # "email":"testbitcoincoid@mailforspam.com", + # "profile_picture":null, + # "verification_status":"unverified", + # "gauth_enable":true + # } + # } + # + return self.parse_balance(response) + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://github.com/btcid/indodax-official-api-docs/blob/master/Public-RestAPI.md#depth + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + orderbook = self.publicGetApiDepthPair(self.extend(request, params)) + return self.parse_order_book(orderbook, market['symbol'], None, 'buy', 'sell') + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "high":"0.01951", + # "low":"0.01877", + # "vol_eth":"39.38839319", + # "vol_btc":"0.75320886", + # "last":"0.01896", + # "buy":"0.01896", + # "sell":"0.019", + # "server_time":1565248908 + # } + # + symbol = self.safe_symbol(None, market) + timestamp = self.safe_timestamp(ticker, 'server_time') + baseVolume = 'vol_' + market['baseId'].lower() + quoteVolume = 'vol_' + market['quoteId'].lower() + last = self.safe_string(ticker, 'last') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': self.safe_string(ticker, 'buy'), + 'bidVolume': None, + 'ask': self.safe_string(ticker, 'sell'), + 'askVolume': None, + 'vwap': None, + 'open': None, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': self.safe_string(ticker, baseVolume), + 'quoteVolume': self.safe_string(ticker, quoteVolume), + 'info': ticker, + }, market) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://github.com/btcid/indodax-official-api-docs/blob/master/Public-RestAPI.md#ticker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + response = self.publicGetApiTickerPair(self.extend(request, params)) + # + # { + # "ticker": { + # "high":"0.01951", + # "low":"0.01877", + # "vol_eth":"39.38839319", + # "vol_btc":"0.75320886", + # "last":"0.01896", + # "buy":"0.01896", + # "sell":"0.019", + # "server_time":1565248908 + # } + # } + # + ticker = self.safe_dict(response, 'ticker', {}) + return self.parse_ticker(ticker, market) + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://github.com/btcid/indodax-official-api-docs/blob/master/Public-RestAPI.md#ticker-all + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + # + # { + # "tickers": { + # "btc_idr": { + # "high": "120009000", + # "low": "116735000", + # "vol_btc": "218.13777777", + # "vol_idr": "25800033297", + # "last": "117088000", + # "buy": "117002000", + # "sell": "117078000", + # "server_time": 1571207881 + # } + # } + # } + # + response = self.publicGetApiTickerAll(params) + tickers = self.safe_dict(response, 'tickers', {}) + keys = list(tickers.keys()) + parsedTickers = {} + for i in range(0, len(keys)): + key = keys[i] + rawTicker = tickers[key] + marketId = key.replace('_', '') + market = self.safe_market(marketId) + parsed = self.parse_ticker(rawTicker, market) + parsedTickers[marketId] = parsed + return self.filter_by_array(parsedTickers, 'symbol', symbols) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + timestamp = self.safe_timestamp(trade, 'date') + return self.safe_trade({ + 'id': self.safe_string(trade, 'tid'), + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': self.safe_symbol(None, market), + 'type': None, + 'side': self.safe_string(trade, 'type'), + 'order': None, + 'takerOrMaker': None, + 'price': self.safe_string(trade, 'price'), + 'amount': self.safe_string(trade, 'amount'), + 'cost': None, + 'fee': None, + }, market) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://github.com/btcid/indodax-official-api-docs/blob/master/Public-RestAPI.md#trades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + response = self.publicGetApiTradesPair(self.extend(request, params)) + return self.parse_trades(response, market, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # { + # "Time": 1708416900, + # "Open": 51707.52, + # "High": 51707.52, + # "Low": 51707.52, + # "Close": 51707.52, + # "Volume": "0" + # } + # + return [ + self.safe_timestamp(ohlcv, 'Time'), + self.safe_number(ohlcv, 'Open'), + self.safe_number(ohlcv, 'High'), + self.safe_number(ohlcv, 'Low'), + self.safe_number(ohlcv, 'Close'), + self.safe_number(ohlcv, 'Volume'), + ] + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest candle to fetch + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + market = self.market(symbol) + selectedTimeframe = self.safe_string(self.timeframes, timeframe, timeframe) + now = self.seconds() + until = self.safe_integer(params, 'until', now) + params = self.omit(params, ['until']) + request: dict = { + 'to': until, + 'tf': selectedTimeframe, + 'symbol': market['id'], + } + if limit is None: + limit = 1000 + if since is not None: + request['from'] = int(math.floor(since / 1000)) + else: + duration = self.parse_timeframe(timeframe) + request['from'] = now - limit * duration - 1 + response = self.publicGetTradingviewHistoryV2(self.extend(request, params)) + # + # [ + # { + # "Time": 1708416900, + # "Open": 51707.52, + # "High": 51707.52, + # "Low": 51707.52, + # "Close": 51707.52, + # "Volume": "0" + # } + # ] + # + return self.parse_ohlcvs(response, market, timeframe, since, limit) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'open': 'open', + 'filled': 'closed', + 'cancelled': 'canceled', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # { + # "order_id": "12345", + # "submit_time": "1392228122", + # "price": "8000000", + # "type": "sell", + # "order_ltc": "100000000", + # "remain_ltc": "100000000" + # } + # + # market closed orders - note that the price is very high + # and does not reflect actual price the order executed at + # + # { + # "order_id": "49326856", + # "type": "sell", + # "price": "1000000000", + # "submit_time": "1618314671", + # "finish_time": "1618314671", + # "status": "filled", + # "order_xrp": "30.45000000", + # "remain_xrp": "0.00000000" + # } + # + # cancelOrder + # + # { + # "order_id": 666883, + # "client_order_id": "clientx-sj82ks82j", + # "type": "sell", + # "pair": "btc_idr", + # "balance": { + # "idr": "33605800", + # "btc": "0.00000000", + # ... + # "frozen_idr": "0", + # "frozen_btc": "0.00000000", + # ... + # } + # } + # + side = None + if 'type' in order: + side = order['type'] + status = self.parse_order_status(self.safe_string(order, 'status', 'open')) + symbol = None + cost = None + price = self.safe_string(order, 'price') + amount = None + remaining = None + marketId = self.safe_string(order, 'pair') + market = self.safe_market(marketId, market) + if market is not None: + symbol = market['symbol'] + quoteId = market['quoteId'] + baseId = market['baseId'] + if (market['quoteId'] == 'idr') and ('order_rp' in order): + quoteId = 'rp' + if (market['baseId'] == 'idr') and ('remain_rp' in order): + baseId = 'rp' + cost = self.safe_string(order, 'order_' + quoteId) + if not cost: + amount = self.safe_string(order, 'order_' + baseId) + remaining = self.safe_string(order, 'remain_' + baseId) + timestamp = self.safe_integer(order, 'submit_time') + fee = None + id = self.safe_string(order, 'order_id') + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': self.safe_string(order, 'client_order_id'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'symbol': symbol, + 'type': 'limit', + 'timeInForce': None, + 'postOnly': None, + 'side': side, + 'price': price, + 'triggerPrice': None, + 'cost': cost, + 'average': None, + 'amount': amount, + 'filled': None, + 'remaining': remaining, + 'status': status, + 'fee': fee, + 'trades': None, + }) + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://github.com/btcid/indodax-official-api-docs/blob/master/Private-RestAPI.md#get-order-endpoints + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrder() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + 'order_id': id, + } + response = self.privatePostGetOrder(self.extend(request, params)) + orders = response['return'] + order = self.parse_order(self.extend({'id': id}, orders['order']), market) + order['info'] = response + return order + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://github.com/btcid/indodax-official-api-docs/blob/master/Private-RestAPI.md#open-orders-endpoints + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + market = None + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['pair'] = market['id'] + response = self.privatePostOpenOrders(self.extend(request, params)) + rawOrders = response['return']['orders'] + # {success: 1, return: {orders: null}} if no orders + if not rawOrders: + return [] + # {success: 1, return: {orders: [... objects]}} for orders fetched by symbol + if symbol is not None: + return self.parse_orders(rawOrders, market, since, limit) + # {success: 1, return: {orders: {marketid: [... objects]}}} if all orders are fetched + marketIds = list(rawOrders.keys()) + exchangeOrders = [] + for i in range(0, len(marketIds)): + marketId = marketIds[i] + marketOrders = rawOrders[marketId] + market = self.safe_market(marketId) + parsedOrders = self.parse_orders(marketOrders, market, since, limit) + exchangeOrders = self.array_concat(exchangeOrders, parsedOrders) + return exchangeOrders + + def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://github.com/btcid/indodax-official-api-docs/blob/master/Private-RestAPI.md#order-history + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchClosedOrders() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + response = self.privatePostOrderHistory(self.extend(request, params)) + orders = self.parse_orders(response['return']['orders'], market) + orders = self.filter_by(orders, 'status', 'closed') + return self.filter_by_symbol_since_limit(orders, symbol, since, limit) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://github.com/btcid/indodax-official-api-docs/blob/master/Private-RestAPI.md#trade-endpoints + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + 'type': side, + 'price': price, + } + priceIsRequired = False + quantityIsRequired = False + if type == 'market': + if side == 'buy': + quoteAmount = None + cost = self.safe_number(params, 'cost') + params = self.omit(params, 'cost') + if cost is not None: + quoteAmount = self.cost_to_precision(symbol, cost) + else: + if price is None: + raise InvalidOrder(self.id + ' createOrder() requires the price argument for market buy orders to calculate the total cost to spend(amount * price).') + amountString = self.number_to_string(amount) + priceString = self.number_to_string(price) + costRequest = Precise.string_mul(amountString, priceString) + quoteAmount = self.cost_to_precision(symbol, costRequest) + request[market['quoteId']] = quoteAmount + else: + quantityIsRequired = True + elif type == 'limit': + priceIsRequired = True + quantityIsRequired = True + if side == 'buy': + request[market['quoteId']] = self.parse_to_numeric(Precise.string_mul(self.number_to_string(amount), self.number_to_string(price))) + if priceIsRequired: + if price is None: + raise InvalidOrder(self.id + ' createOrder() requires a price argument for a ' + type + ' order') + request['price'] = price + if quantityIsRequired: + request[market['baseId']] = self.amount_to_precision(symbol, amount) + result = self.privatePostTrade(self.extend(request, params)) + data = self.safe_value(result, 'return', {}) + id = self.safe_string(data, 'order_id') + return self.safe_order({ + 'info': result, + 'id': id, + }, market) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://github.com/btcid/indodax-official-api-docs/blob/master/Private-RestAPI.md#cancel-order-endpoints + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + side = self.safe_value(params, 'side') + if side is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires an extra "side" param') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'order_id': id, + 'pair': market['id'], + 'type': side, + } + response = self.privatePostCancelOrder(self.extend(request, params)) + # + # { + # "success": 1, + # "return": { + # "order_id": 666883, + # "client_order_id": "clientx-sj82ks82j", + # "type": "sell", + # "pair": "btc_idr", + # "balance": { + # "idr": "33605800", + # "btc": "0.00000000", + # ... + # "frozen_idr": "0", + # "frozen_btc": "0.00000000", + # ... + # } + # } + # } + # + data = self.safe_dict(response, 'return') + return self.parse_order(data) + + def fetch_transaction_fee(self, code: str, params={}): + """ + fetch the fee for a transaction + + https://github.com/btcid/indodax-official-api-docs/blob/master/Private-RestAPI.md#withdraw-fee-endpoints + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `fee structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + } + response = self.privatePostWithdrawFee(self.extend(request, params)) + # + # { + # "success": 1, + # "return": { + # "server_time": 1607923272, + # "withdraw_fee": 0.005, + # "currency": "eth" + # } + # } + # + data = self.safe_value(response, 'return', {}) + currencyId = self.safe_string(data, 'currency') + return { + 'info': response, + 'rate': self.safe_number(data, 'withdraw_fee'), + 'currency': self.safe_currency_code(currencyId, currency), + } + + def fetch_deposits_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch history of deposits and withdrawals + + https://github.com/btcid/indodax-official-api-docs/blob/master/Private-RestAPI.md#transaction-history-endpoints + + :param str [code]: unified currency code for the currency of the deposit/withdrawals, default is None + :param int [since]: timestamp in ms of the earliest deposit/withdrawal, default is None + :param int [limit]: max number of deposit/withdrawals to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `transaction structure ` + """ + self.load_markets() + request: dict = {} + if since is not None: + startTime = self.iso8601(since)[0:10] + request['start'] = startTime + request['end'] = self.iso8601(self.milliseconds())[0:10] + response = self.privatePostTransHistory(self.extend(request, params)) + # + # { + # "success": 1, + # "return": { + # "withdraw": { + # "idr": [ + # { + # "status": "success", + # "type": "coupon", + # "rp": "115205", + # "fee": "500", + # "amount": "114705", + # "submit_time": "1539844166", + # "success_time": "1539844189", + # "withdraw_id": "1783717", + # "tx": "BTC-IDR-RDTVVO2P-ETD0EVAW-VTNZGMIR-HTNTUAPI-84ULM9OI", + # "sender": "boris", + # "used_by": "viginia88" + # }, + # ... + # ], + # "btc": [], + # "abyss": [], + # ... + # }, + # "deposit": { + # "idr": [ + # { + # "status": "success", + # "type": "duitku", + # "rp": "393000", + # "fee": "5895", + # "amount": "387105", + # "submit_time": "1576555012", + # "success_time": "1576555012", + # "deposit_id": "3395438", + # "tx": "Duitku OVO Settlement" + # }, + # ... + # ], + # "btc": [ + # { + # "status": "success", + # "btc": "0.00118769", + # "amount": "0.00118769", + # "success_time": "1539529208", + # "deposit_id": "3602369", + # "tx": "c816aeb35a5b42f389970325a32aff69bb6b2126784dcda8f23b9dd9570d6573" + # }, + # ... + # ], + # "abyss": [], + # ... + # } + # } + # } + # + data = self.safe_value(response, 'return', {}) + withdraw = self.safe_value(data, 'withdraw', {}) + deposit = self.safe_value(data, 'deposit', {}) + transactions = [] + currency = None + if code is None: + keys = list(withdraw.keys()) + for i in range(0, len(keys)): + key = keys[i] + transactions = self.array_concat(transactions, withdraw[key]) + keys = list(deposit.keys()) + for i in range(0, len(keys)): + key = keys[i] + transactions = self.array_concat(transactions, deposit[key]) + else: + currency = self.currency(code) + withdraws = self.safe_value(withdraw, currency['id'], []) + deposits = self.safe_value(deposit, currency['id'], []) + transactions = self.array_concat(withdraws, deposits) + return self.parse_transactions(transactions, currency, since, limit) + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://github.com/btcid/indodax-official-api-docs/blob/master/Private-RestAPI.md#withdraw-coin-endpoints + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.check_address(address) + self.load_markets() + currency = self.currency(code) + # Custom string you need to provide to identify each withdrawal. + # Will be passed to callback URL(assigned via website to the API key) + # so your system can identify the request and confirm it. + # Alphanumeric, max length 255. + requestId = self.milliseconds() + # Alternatively: + # requestId = self.uuid() + request: dict = { + 'currency': currency['id'], + 'withdraw_amount': amount, + 'withdraw_address': address, + 'request_id': str(requestId), + } + if tag: + request['withdraw_memo'] = tag + response = self.privatePostWithdrawCoin(self.extend(request, params)) + # + # { + # "success": 1, + # "status": "approved", + # "withdraw_currency": "xrp", + # "withdraw_address": "rwWr7KUZ3ZFwzgaDGjKBysADByzxvohQ3C", + # "withdraw_amount": "10000.00000000", + # "fee": "2.00000000", + # "amount_after_fee": "9998.00000000", + # "submit_time": "1509469200", + # "withdraw_id": "xrp-12345", + # "txid": "", + # "withdraw_memo": "123123" + # } + # + return self.parse_transaction(response, currency) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # withdraw + # + # { + # "success": 1, + # "status": "approved", + # "withdraw_currency": "xrp", + # "withdraw_address": "rwWr7KUZ3ZFwzgaDGjKBysADByzxvohQ3C", + # "withdraw_amount": "10000.00000000", + # "fee": "2.00000000", + # "amount_after_fee": "9998.00000000", + # "submit_time": "1509469200", + # "withdraw_id": "xrp-12345", + # "txid": "", + # "withdraw_memo": "123123" + # } + # + # transHistory + # + # { + # "status": "success", + # "type": "coupon", + # "rp": "115205", + # "fee": "500", + # "amount": "114705", + # "submit_time": "1539844166", + # "success_time": "1539844189", + # "withdraw_id": "1783717", + # "tx": "BTC-IDR-RDTVVO2P-ETD0EVAW-VTNZGMIR-HTNTUAPI-84ULM9OI", + # "sender": "boris", + # "used_by": "viginia88" + # } + # + # { + # "status": "success", + # "btc": "0.00118769", + # "amount": "0.00118769", + # "success_time": "1539529208", + # "deposit_id": "3602369", + # "tx": "c816aeb35a5b42f389970325a32aff69bb6b2126784dcda8f23b9dd9570d6573" + # }, + status = self.safe_string(transaction, 'status') + timestamp = self.safe_timestamp_2(transaction, 'success_time', 'submit_time') + depositId = self.safe_string(transaction, 'deposit_id') + feeCost = self.safe_number(transaction, 'fee') + fee = None + if feeCost is not None: + fee = { + 'currency': self.safe_currency_code(None, currency), + 'cost': feeCost, + 'rate': None, + } + return { + 'id': self.safe_string_2(transaction, 'withdraw_id', 'deposit_id'), + 'txid': self.safe_string_2(transaction, 'txid', 'tx'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': None, + 'addressFrom': None, + 'address': self.safe_string(transaction, 'withdraw_address'), + 'addressTo': None, + 'amount': self.safe_number_n(transaction, ['amount', 'withdraw_amount', 'deposit_amount']), + 'type': 'withdraw' if (depositId is None) else 'deposit', + 'currency': self.safe_currency_code(None, currency), + 'status': self.parse_transaction_status(status), + 'updated': None, + 'tagFrom': None, + 'tag': None, + 'tagTo': None, + 'comment': self.safe_string(transaction, 'withdraw_memo'), + 'internal': None, + 'fee': fee, + 'info': transaction, + } + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'success': 'ok', + } + return self.safe_string(statuses, status, status) + + def fetch_deposit_addresses(self, codes: Strings = None, params={}) -> List[DepositAddress]: + """ + fetch deposit addresses for multiple currencies and chain types + + https://github.com/btcid/indodax-official-api-docs/blob/master/Private-RestAPI.md#general-information-on-endpoints + + :param str[] [codes]: list of unified currency codes, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `address structures ` + """ + self.load_markets() + response = self.privatePostGetInfo(params) + # + # { + # success: '1', + # return: { + # server_time: '1708031570', + # balance: { + # idr: '29952', + # ... + # }, + # balance_hold: { + # idr: '0', + # ... + # }, + # address: { + # btc: '1KMntgzvU7iTSgMBWc11nVuJjAyfW3qJyk', + # ... + # }, + # memo_is_required: { + # btc: {mainnet: False}, + # ... + # }, + # network: { + # btc: 'mainnet', + # ... + # }, + # user_id: '276011', + # name: '', + # email: 'testbitcoincoid@mailforspam.com', + # profile_picture: null, + # verification_status: 'unverified', + # gauth_enable: True, + # withdraw_status: '0' + # } + # } + # + data = self.safe_dict(response, 'return') + addresses = self.safe_dict(data, 'address', {}) + networks = self.safe_dict(data, 'network', {}) + addressKeys = list(addresses.keys()) + result: dict = { + 'info': data, + } + for i in range(0, len(addressKeys)): + marketId = addressKeys[i] + code = self.safe_currency_code(marketId) + address = self.safe_string(addresses, marketId) + if (address is not None) and ((codes is None) or (self.in_array(code, codes))): + self.check_address(address) + network = None + if marketId in networks: + networkId = self.safe_string(networks, marketId) + if networkId.find(',') >= 0: + network = [] + networkIds = networkId.split(',') + for j in range(0, len(networkIds)): + network.append(self.network_id_to_code(networkIds[j]).upper()) + else: + network = self.network_id_to_code(networkId).upper() + result[code] = { + 'info': {}, + 'currency': code, + 'network': network, + 'address': address, + 'tag': None, + } + return result + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = self.urls['api'][api] + if api == 'public': + query = self.omit(params, self.extract_params(path)) + requestPath = '/' + self.implode_params(path, params) + url = url + requestPath + if query: + url += '?' + self.urlencode_with_array_repeat(query) + else: + self.check_required_credentials() + body = self.urlencode(self.extend({ + 'method': path, + 'timestamp': self.nonce(), + 'recvWindow': self.options['recvWindow'], + }, params)) + headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Key': self.apiKey, + 'Sign': self.hmac(self.encode(body), self.encode(self.secret), hashlib.sha512), + } + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None + # {success: 0, error: "invalid order."} + # or + # [{data, ...}, {...}, ...] + # {"success":"1","status":"approved","withdraw_currency":"strm","withdraw_address":"0x2b9A8cd5535D99b419aEfFBF1ae8D90a7eBdb24E","withdraw_amount":"2165.05767839","fee":"21.11000000","amount_after_fee":"2143.94767839","submit_time":"1730759489","withdraw_id":"strm-3423","txid":""} + if isinstance(response, list): + return None # public endpoints may return []-arrays + error = self.safe_value(response, 'error', '') + if not ('success' in response) and error == '': + return None # no 'success' property on public responses + status = self.safe_string(response, 'success') + if status == 'approved': + return None + if self.safe_integer(response, 'success', 0) == 1: + # {success: 1, return: {orders: []}} + if not ('return' in response): + raise ExchangeError(self.id + ': malformed response: ' + self.json(response)) + else: + return None + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], error, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], error, feedback) + raise ExchangeError(feedback) # unknown message diff --git a/ccxt/kraken.py b/ccxt/kraken.py new file mode 100644 index 0000000..3b286f7 --- /dev/null +++ b/ccxt/kraken.py @@ -0,0 +1,3443 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.kraken import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currencies, Currency, DepositAddress, IndexType, Int, LedgerEntry, Market, Num, Order, OrderBook, OrderRequest, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, Trade, TradingFeeInterface, Transaction, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import AccountSuspended +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidAddress +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import NotSupported +from ccxt.base.errors import DDoSProtection +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.errors import OnMaintenance +from ccxt.base.errors import InvalidNonce +from ccxt.base.errors import CancelPending +from ccxt.base.decimal_to_precision import TRUNCATE +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class kraken(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(kraken, self).describe(), { + 'id': 'kraken', + 'name': 'Kraken', + 'countries': ['US'], + 'version': '0', + # rate-limits: https://support.kraken.com/hc/en-us/articles/206548367-What-are-the-API-rate-limits-#1 + # for public: 1 req/s + # for private: every second 0.33 weight added to your allowed capacity(some private endpoints need 1 weight, some need 2) + 'rateLimit': 1000, + 'certified': False, + 'pro': True, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': True, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'cancelAllOrders': True, + 'cancelAllOrdersAfter': True, + 'cancelOrder': True, + 'cancelOrders': True, + 'createDepositAddress': True, + 'createMarketBuyOrderWithCost': True, + 'createMarketOrderWithCost': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createOrders': True, + 'createStopLimitOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'createTrailingAmountOrder': True, + 'createTrailingPercentOrder': True, + 'editOrder': True, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchFundingHistory': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchLedger': True, + 'fetchLedgerEntry': True, + 'fetchLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterestHistory': False, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrderTrades': 'emulated', + 'fetchPositions': True, + 'fetchPremiumIndexOHLCV': False, + 'fetchStatus': True, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': True, + 'fetchTradingFees': False, + 'fetchWithdrawals': True, + 'setLeverage': False, + 'setMarginMode': False, # Kraken only supports cross margin + 'transfer': True, + 'withdraw': True, + }, + 'timeframes': { + '1m': 1, + '5m': 5, + '15m': 15, + '30m': 30, + '1h': 60, + '4h': 240, + '1d': 1440, + '1w': 10080, + '2w': 21600, + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/51840849/76173629-fc67fb00-61b1-11ea-84fe-f2de582f58a3.jpg', + 'api': { + 'public': 'https://api.kraken.com', + 'private': 'https://api.kraken.com', + 'zendesk': 'https://kraken.zendesk.com/api/v2/help_center/en-us/articles', # use the public zendesk api to receive article bodies and bypass new anti-spam protections + }, + 'www': 'https://www.kraken.com', + 'doc': 'https://docs.kraken.com/rest/', + 'fees': 'https://www.kraken.com/en-us/features/fee-schedule', + }, + 'fees': { + 'trading': { + 'tierBased': True, + 'percentage': True, + 'taker': self.parse_number('0.0026'), + 'maker': self.parse_number('0.0016'), + 'tiers': { + 'taker': [ + [self.parse_number('0'), self.parse_number('0.0026')], + [self.parse_number('50000'), self.parse_number('0.0024')], + [self.parse_number('100000'), self.parse_number('0.0022')], + [self.parse_number('250000'), self.parse_number('0.0020')], + [self.parse_number('500000'), self.parse_number('0.0018')], + [self.parse_number('1000000'), self.parse_number('0.0016')], + [self.parse_number('2500000'), self.parse_number('0.0014')], + [self.parse_number('5000000'), self.parse_number('0.0012')], + [self.parse_number('10000000'), self.parse_number('0.0001')], + ], + 'maker': [ + [self.parse_number('0'), self.parse_number('0.0016')], + [self.parse_number('50000'), self.parse_number('0.0014')], + [self.parse_number('100000'), self.parse_number('0.0012')], + [self.parse_number('250000'), self.parse_number('0.0010')], + [self.parse_number('500000'), self.parse_number('0.0008')], + [self.parse_number('1000000'), self.parse_number('0.0006')], + [self.parse_number('2500000'), self.parse_number('0.0004')], + [self.parse_number('5000000'), self.parse_number('0.0002')], + [self.parse_number('10000000'), self.parse_number('0.0')], + ], + }, + }, + }, + 'handleContentTypeApplicationZip': True, + 'api': { + 'zendesk': { + 'get': [ + # we should really refrain from putting fixed fee numbers and stop hardcoding + # we will be using their web APIs to scrape all numbers from these articles + '360000292886', # -What-are-the-deposit-fees- + '201893608', # -What-are-the-withdrawal-fees- + ], + }, + 'public': { + 'get': { + # rate-limits explained in comment in the top of self file + 'Assets': 1, + 'AssetPairs': 1, + 'Depth': 1.2, + 'OHLC': 1.2, # 1.2 because 1 triggers too many requests immediately + 'Spread': 1, + 'SystemStatus': 1, + 'Ticker': 1, + 'Time': 1, + 'Trades': 1.2, + }, + }, + 'private': { + 'post': { + 'AddOrder': 0, + 'AddOrderBatch': 0, + 'AddExport': 3, + 'AmendOrder': 0, + 'Balance': 3, + 'CancelAll': 3, + 'CancelAllOrdersAfter': 3, + 'CancelOrder': 0, + 'CancelOrderBatch': 0, + 'ClosedOrders': 3, + 'DepositAddresses': 3, + 'DepositMethods': 3, + 'DepositStatus': 3, + 'EditOrder': 0, + 'ExportStatus': 3, + 'GetWebSocketsToken': 3, + 'Ledgers': 6, + 'OpenOrders': 3, + 'OpenPositions': 3, + 'QueryLedgers': 3, + 'QueryOrders': 3, + 'QueryTrades': 3, + 'RetrieveExport': 3, + 'RemoveExport': 3, + 'BalanceEx': 3, + 'TradeBalance': 3, + 'TradesHistory': 6, + 'TradeVolume': 3, + 'Withdraw': 3, + 'WithdrawCancel': 3, + 'WithdrawInfo': 3, + 'WithdrawMethods': 3, + 'WithdrawAddresses': 3, + 'WithdrawStatus': 3, + 'WalletTransfer': 3, + # sub accounts + 'CreateSubaccount': 3, + 'AccountTransfer': 3, + # earn + 'Earn/Allocate': 3, + 'Earn/Deallocate': 3, + 'Earn/AllocateStatus': 3, + 'Earn/DeallocateStatus': 3, + 'Earn/Strategies': 3, + 'Earn/Allocations': 3, + }, + }, + }, + 'commonCurrencies': { + # about X & Z prefixes and .S & .M suffixes, see comment under fetchCurrencies + 'LUNA': 'LUNC', + 'LUNA2': 'LUNA', + 'REPV2': 'REP', + 'REP': 'REPV1', + 'UST': 'USTC', + 'XBT': 'BTC', + 'XDG': 'DOGE', + 'FEE': 'KFEE', + 'XETC': 'ETC', + 'XETH': 'ETH', + 'XLTC': 'LTC', + 'XMLN': 'MLN', + 'XREP': 'REP', + 'XXBT': 'BTC', + 'XXDG': 'DOGE', + 'XXLM': 'XLM', + 'XXMR': 'XMR', + 'XXRP': 'XRP', + 'XZEC': 'ZEC', + 'ZAUD': 'AUD', + 'ZCAD': 'CAD', + 'ZEUR': 'EUR', + 'ZGBP': 'GBP', + 'ZJPY': 'JPY', + 'ZUSD': 'USD', + }, + 'options': { + 'timeDifference': 0, # the difference between system clock and Binance clock + 'adjustForTimeDifference': False, # controls the adjustment logic upon instantiation + 'marketsByAltname': {}, + 'delistedMarketsById': {}, + # cannot withdraw/deposit these + 'inactiveCurrencies': ['CAD', 'USD', 'JPY', 'GBP'], + 'networks': { + 'ETH': 'ERC20', + 'TRX': 'TRC20', + }, + 'depositMethods': { + '1INCH': '1inch' + ' ' + '(1INCH)', + 'AAVE': 'Aave', + 'ADA': 'ADA', + 'ALGO': 'Algorand', + 'ANKR': 'ANKR' + ' ' + '(ANKR)', + 'ANT': 'Aragon' + ' ' + '(ANT)', + 'ATOM': 'Cosmos', + 'AXS': 'Axie Infinity Shards' + ' ' + '(AXS)', + 'BADGER': 'Bager DAO' + ' ' + '(BADGER)', + 'BAL': 'Balancer' + ' ' + '(BAL)', + 'BAND': 'Band Protocol' + ' ' + '(BAND)', + 'BAT': 'BAT', + 'BCH': 'Bitcoin Cash', + 'BNC': 'Bifrost' + ' ' + '(BNC)', + 'BNT': 'Bancor' + ' ' + '(BNT)', + 'BTC': 'Bitcoin', + 'CHZ': 'Chiliz' + ' ' + '(CHZ)', + 'COMP': 'Compound' + ' ' + '(COMP)', + 'CQT': '\tCovalent Query Token' + ' ' + '(CQT)', + 'CRV': 'Curve DAO Token' + ' ' + '(CRV)', + 'CTSI': 'Cartesi' + ' ' + '(CTSI)', + 'DAI': 'Dai', + 'DASH': 'Dash', + 'DOGE': 'Dogecoin', + 'DOT': 'Polkadot', + 'DYDX': 'dYdX' + ' ' + '(DYDX)', + 'ENJ': 'Enjin Coin' + ' ' + '(ENJ)', + 'EOS': 'EOS', + 'ETC': 'Ether Classic' + ' ' + '(Hex)', + 'ETH': 'Ether' + ' ' + '(Hex)', + 'EWT': 'Energy Web Token', + 'FEE': 'Kraken Fee Credit', + 'FIL': 'Filecoin', + 'FLOW': 'Flow', + 'GHST': 'Aavegotchi' + ' ' + '(GHST)', + 'GNO': 'GNO', + 'GRT': 'GRT', + 'ICX': 'Icon', + 'INJ': 'Injective Protocol' + ' ' + '(INJ)', + 'KAR': 'Karura' + ' ' + '(KAR)', + 'KAVA': 'Kava', + 'KEEP': 'Keep Token' + ' ' + '(KEEP)', + 'KNC': 'Kyber Network' + ' ' + '(KNC)', + 'KSM': 'Kusama', + 'LINK': 'Link', + 'LPT': 'Livepeer Token' + ' ' + '(LPT)', + 'LRC': 'Loopring' + ' ' + '(LRC)', + 'LSK': 'Lisk', + 'LTC': 'Litecoin', + 'MANA': 'MANA', + 'MATIC': 'Polygon' + ' ' + '(MATIC)', + 'MINA': 'Mina', # inspected from webui + 'MIR': 'Mirror Protocol' + ' ' + '(MIR)', + 'MKR': 'Maker' + ' ' + '(MKR)', + 'MLN': 'MLN', + 'MOVR': 'Moonriver' + ' ' + '(MOVR)', + 'NANO': 'NANO', + 'OCEAN': 'OCEAN', + 'OGN': 'Origin Protocol' + ' ' + '(OGN)', + 'OMG': 'OMG', + 'OXT': 'Orchid' + ' ' + '(OXT)', + 'OXY': 'Oxygen' + ' ' + '(OXY)', + 'PAXG': 'PAX' + ' ' + '(Gold)', + 'PERP': 'Perpetual Protocol' + ' ' + '(PERP)', + 'PHA': 'Phala' + ' ' + '(PHA)', + 'QTUM': 'QTUM', + 'RARI': 'Rarible' + ' ' + '(RARI)', + 'RAY': 'Raydium' + ' ' + '(RAY)', + 'REN': 'Ren Protocol' + ' ' + '(REN)', + 'REP': 'REPv2', + 'REPV1': 'REP', + 'SAND': 'The Sandbox' + ' ' + '(SAND)', + 'SC': 'Siacoin', + 'SDN': 'Shiden' + ' ' + '(SDN)', + 'SOL': 'Solana', # their deposit method api doesn't work for SOL - was guessed + 'SNX': 'Synthetix Network' + ' ' + '(SNX)', + 'SRM': 'Serum', # inspected from webui + 'STORJ': 'Storj' + ' ' + '(STORJ)', + 'SUSHI': 'Sushiswap' + ' ' + '(SUSHI)', + 'TBTC': 'tBTC', + 'TRX': 'Tron', + 'UNI': 'UNI', + 'USDC': 'USDC', + 'USDT': 'Tether USD' + ' ' + '(ERC20)', + 'USDT-TRC20': 'Tether USD' + ' ' + '(TRC20)', + 'WAVES': 'Waves', + 'WBTC': 'Wrapped Bitcoin' + ' ' + '(WBTC)', + 'XLM': 'Stellar XLM', + 'XMR': 'Monero', + 'XRP': 'Ripple XRP', + 'XTZ': 'XTZ', + 'YFI': 'YFI', + 'ZEC': 'Zcash' + ' ' + '(Transparent)', + 'ZRX': '0x' + ' ' + '(ZRX)', + }, + 'withdrawMethods': { # keeping it here because deposit and withdraw return different networks codes + 'Lightning': 'Lightning', + 'Bitcoin': 'BTC', + 'Ripple': 'XRP', + 'Litecoin': 'LTC', + 'Dogecoin': 'DOGE', + 'Stellar': 'XLM', + 'Ethereum': 'ERC20', + 'Arbitrum One': 'Arbitrum', + 'Polygon': 'MATIC', + 'Arbitrum Nova': 'Arbitrum', + 'Optimism': 'Optimism', + 'zkSync Era': 'zkSync', + 'Ethereum Classic': 'ETC', + 'Zcash': 'ZEC', + 'Monero': 'XMR', + 'Tron': 'TRC20', + 'Solana': 'SOL', + 'EOS': 'EOS', + 'Bitcoin Cash': 'BCH', + 'Cardano': 'ADA', + 'Qtum': 'QTUM', + 'Tezos': 'XTZ', + 'Cosmos': 'ATOM', + 'Nano': 'NANO', + 'Siacoin': 'SC', + 'Lisk': 'LSK', + 'Waves': 'WAVES', + 'ICON': 'ICX', + 'Algorand': 'ALGO', + 'Polygon - USDC.e': 'MATIC', + 'Arbitrum One - USDC.e': 'Arbitrum', + 'Polkadot': 'DOT', + 'Kava': 'KAVA', + 'Filecoin': 'FIL', + 'Kusama': 'KSM', + 'Flow': 'FLOW', + 'Energy Web': 'EW', + 'Mina': 'MINA', + 'Centrifuge': 'CFG', + 'Karura': 'KAR', + 'Moonriver': 'MOVR', + 'Shiden': 'SDN', + 'Khala': 'PHA', + 'Bifrost Kusama': 'BNC', + 'Songbird': 'SGB', + 'Terra classic': 'LUNC', + 'KILT': 'KILT', + 'Basilisk': 'BSX', + 'Flare': 'FLR', + 'Avalanche C-Chain': 'AVAX', + 'Kintsugi': 'KINT', + 'Altair': 'AIR', + 'Moonbeam': 'GLMR', + 'Acala': 'ACA', + 'Astar': 'ASTR', + 'Akash': 'AKT', + 'Robonomics': 'XRT', + 'Fantom': 'FTM', + 'Elrond': 'EGLD', + 'THORchain': 'RUNE', + 'Secret': 'SCRT', + 'Near': 'NEAR', + 'Internet Computer Protocol': 'ICP', + 'Picasso': 'PICA', + 'Crust Shadow': 'CSM', + 'Integritee': 'TEER', + 'Parallel Finance': 'PARA', + 'HydraDX': 'HDX', + 'Interlay': 'INTR', + 'Fetch.ai': 'FET', + 'NYM': 'NYM', + 'Terra 2.0': 'LUNA2', + 'Juno': 'JUNO', + 'Nodle': 'NODL', + 'Stacks': 'STX', + 'Ethereum PoW': 'ETHW', + 'Aptos': 'APT', + 'Sui': 'SUI', + 'Genshiro': 'GENS', + 'Aventus': 'AVT', + 'Sei': 'SEI', + 'OriginTrail': 'OTP', + 'Celestia': 'TIA', + }, + 'marketHelperProps': ['marketsByAltname', 'delistedMarketsById'], # used by setMarketsFromExchange + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': False, # todo + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': True, + 'takeProfitPrice': True, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': True, + 'leverage': False, + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': True, # todo implement + 'iceberg': True, # todo implement + }, + 'createOrders': { + 'min': 2, + 'max': 15, + 'sameSymbolOnly': True, + }, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': None, + 'daysBack': None, + 'untilDays': None, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': None, + 'daysBack': None, + 'daysBackCanceled': None, + 'untilDays': 100000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 720, + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'exact': { + 'EQuery:Invalid asset pair': BadSymbol, # {"error":["EQuery:Invalid asset pair"]} + 'EAPI:Invalid key': AuthenticationError, + 'EFunding:Unknown withdraw key': InvalidAddress, # {"error":["EFunding:Unknown withdraw key"]} + 'EFunding:Invalid amount': InsufficientFunds, + 'EService:Unavailable': ExchangeNotAvailable, + 'EDatabase:Internal error': ExchangeNotAvailable, + 'EService:Busy': ExchangeNotAvailable, + 'EQuery:Unknown asset': BadSymbol, # {"error":["EQuery:Unknown asset"]} + 'EAPI:Rate limit exceeded': DDoSProtection, + 'EOrder:Rate limit exceeded': DDoSProtection, + 'EGeneral:Internal error': ExchangeNotAvailable, + 'EGeneral:Temporary lockout': DDoSProtection, + 'EGeneral:Permission denied': PermissionDenied, + 'EGeneral:Invalid arguments:price': InvalidOrder, + 'EOrder:Unknown order': InvalidOrder, + 'EOrder:Invalid price:Invalid price argument': InvalidOrder, + 'EOrder:Order minimum not met': InvalidOrder, + 'EOrder:Insufficient funds': InsufficientFunds, + 'EGeneral:Invalid arguments': BadRequest, + 'ESession:Invalid session': AuthenticationError, + 'EAPI:Invalid nonce': InvalidNonce, + 'EFunding:No funding method': BadRequest, # {"error":"EFunding:No funding method"} + 'EFunding:Unknown asset': BadSymbol, # {"error":["EFunding:Unknown asset"]} + 'EService:Market in post_only mode': OnMaintenance, # {"error":["EService:Market in post_only mode"]} + 'EGeneral:Too many requests': DDoSProtection, # {"error":["EGeneral:Too many requests"]} + 'ETrade:User Locked': AccountSuspended, # {"error":["ETrade:User Locked"]} + }, + 'broad': { + ':Invalid order': InvalidOrder, + ':Invalid arguments:volume': InvalidOrder, + ':Invalid arguments:viqc': InvalidOrder, + ':Invalid nonce': InvalidNonce, + ':IInsufficient funds': InsufficientFunds, + ':Cancel pending': CancelPending, + ':Rate limit exceeded': RateLimitExceeded, + }, + }, + }) + + def fee_to_precision(self, symbol, fee): + return self.decimal_to_precision(fee, TRUNCATE, self.markets[symbol]['precision']['amount'], self.precisionMode) + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for kraken + + https://docs.kraken.com/rest/#tag/Spot-Market-Data/operation/getTradableAssetPairs + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + promises = [] + promises.append(self.publicGetAssetPairs(params)) + if self.options['adjustForTimeDifference']: + promises.append(self.load_time_difference()) + responses = promises + assetsResponse = responses[0] + # + # { + # "error": [], + # "result": { + # "ADAETH": { + # "altname": "ADAETH", + # "wsname": "ADA\/ETH", + # "aclass_base": "currency", + # "base": "ADA", + # "aclass_quote": "currency", + # "quote": "XETH", + # "lot": "unit", + # "pair_decimals": 7, + # "lot_decimals": 8, + # "lot_multiplier": 1, + # "leverage_buy": [], + # "leverage_sell": [], + # "fees": [ + # [0, 0.26], + # [50000, 0.24], + # [100000, 0.22], + # [250000, 0.2], + # [500000, 0.18], + # [1000000, 0.16], + # [2500000, 0.14], + # [5000000, 0.12], + # [10000000, 0.1] + # ], + # "fees_maker": [ + # [0, 0.16], + # [50000, 0.14], + # [100000, 0.12], + # [250000, 0.1], + # [500000, 0.08], + # [1000000, 0.06], + # [2500000, 0.04], + # [5000000, 0.02], + # [10000000, 0] + # ], + # "fee_volume_currency": "ZUSD", + # "margin_call": 80, + # "margin_stop": 40, + # "ordermin": "1" + # }, + # } + # } + # + markets = self.safe_dict(assetsResponse, 'result', {}) + cachedCurrencies = self.safe_dict(self.options, 'cachedCurrencies', {}) + keys = list(markets.keys()) + result = [] + for i in range(0, len(keys)): + id = keys[i] + market = markets[id] + baseIdRaw = self.safe_string(market, 'base') + quoteIdRaw = self.safe_string(market, 'quote') + baseId = self.safe_currency_code(baseIdRaw) + quoteId = self.safe_currency_code(quoteIdRaw) + base = baseId + quote = quoteId + makerFees = self.safe_list(market, 'fees_maker', []) + firstMakerFee = self.safe_list(makerFees, 0, []) + firstMakerFeeRate = self.safe_string(firstMakerFee, 1) + maker = None + if firstMakerFeeRate is not None: + maker = self.parse_number(Precise.string_div(firstMakerFeeRate, '100')) + takerFees = self.safe_list(market, 'fees', []) + firstTakerFee = self.safe_list(takerFees, 0, []) + firstTakerFeeRate = self.safe_string(firstTakerFee, 1) + taker = None + if firstTakerFeeRate is not None: + taker = self.parse_number(Precise.string_div(firstTakerFeeRate, '100')) + leverageBuy = self.safe_list(market, 'leverage_buy', []) + leverageBuyLength = len(leverageBuy) + precisionPrice = self.parse_number(self.parse_precision(self.safe_string(market, 'pair_decimals'))) + precisionAmount = self.parse_number(self.parse_precision(self.safe_string(market, 'lot_decimals'))) + spot = True + # fix https://github.com/freqtrade/freqtrade/issues/11765#issuecomment-2894224103 + if spot and (base in cachedCurrencies): + currency = cachedCurrencies[base] + currencyPrecision = self.safe_number(currency, 'precision') + # if currency precision is greater(e.g. 0.01) than market precision(e.g. 0.001) + if currencyPrecision > precisionAmount: + precisionAmount = currencyPrecision + status = self.safe_string(market, 'status') + isActive = status == 'online' + result.append({ + 'id': id, + 'wsId': self.safe_string(market, 'wsname'), + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'altname': market['altname'], + 'type': 'spot', + 'spot': spot, + 'margin': (leverageBuyLength > 0), + 'swap': False, + 'future': False, + 'option': False, + 'active': isActive, + 'contract': False, + 'linear': None, + 'inverse': None, + 'taker': taker, + 'maker': maker, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': precisionAmount, + 'price': precisionPrice, + }, + 'limits': { + 'leverage': { + 'min': self.parse_number('1'), + 'max': self.safe_number(leverageBuy, leverageBuyLength - 1, 1), + }, + 'amount': { + 'min': self.safe_number(market, 'ordermin'), + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': self.safe_number(market, 'costmin'), + 'max': None, + }, + }, + 'created': None, + 'info': market, + }) + self.options['marketsByAltname'] = self.index_by(result, 'altname') + return result + + def fetch_status(self, params={}): + """ + the latest known information on the availability of the exchange API + + https://docs.kraken.com/api/docs/rest-api/get-system-status/ + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `status structure ` + """ + response = self.publicGetSystemStatus(params) + # + # { + # error: [], + # result: {status: 'online', timestamp: '2024-07-22T16:34:44Z'} + # } + # + result = self.safe_dict(response, 'result') + statusRaw = self.safe_string(result, 'status') + return { + 'status': 'ok' if (statusRaw == 'online') else 'maintenance', + 'updated': None, + 'eta': None, + 'url': None, + 'info': response, + } + + def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://docs.kraken.com/rest/#tag/Spot-Market-Data/operation/getAssetInfo + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = self.publicGetAssets(params) + # + # { + # "error": [], + # "result": { + # "ATOM": { + # "aclass": "currency", + # "altname": "ATOM", + # "collateral_value": "0.7", + # "decimals": 8, + # "display_decimals": 6, + # "margin_rate": 0.02, + # "status": "enabled", + # }, + # "ATOM.S": { + # "aclass": "currency", + # "altname": "ATOM.S", + # "decimals": 8, + # "display_decimals": 6, + # "status": "enabled", + # }, + # "XXBT": { + # "aclass": "currency", + # "altname": "XBT", + # "decimals": 10, + # "display_decimals": 5, + # "margin_rate": 0.01, + # "status": "enabled", + # }, + # "XETH": { + # "aclass": "currency", + # "altname": "ETH", + # "decimals": 10, + # "display_decimals": 5 + # "margin_rate": 0.02, + # "status": "enabled", + # }, + # "XBT.M": { + # "aclass": "currency", + # "altname": "XBT.M", + # "decimals": 10, + # "display_decimals": 5 + # "status": "enabled", + # }, + # "ETH.M": { + # "aclass": "currency", + # "altname": "ETH.M", + # "decimals": 10, + # "display_decimals": 5 + # "status": "enabled", + # }, + # ... + # }, + # } + # + currencies = self.safe_value(response, 'result', {}) + ids = list(currencies.keys()) + result: dict = {} + for i in range(0, len(ids)): + id = ids[i] + currency = currencies[id] + # todo: will need to rethink the fees + # see: https://support.kraken.com/hc/en-us/articles/201893608-What-are-the-withdrawal-fees- + # to add support for multiple withdrawal/deposit methods and + # differentiated fees for each particular method + # + # Notes about abbreviations: + # Z and X prefixes: https://support.kraken.com/hc/en-us/articles/360001206766-Bitcoin-currency-code-XBT-vs-BTC + # S and M suffixes: https://support.kraken.com/hc/en-us/articles/360039879471-What-is-Asset-S-and-Asset-M- + # + code = self.safe_currency_code(id) + # the below can not be reliable done in `safeCurrencyCode`, so we have to do it here + if id.find('.') < 0: + altName = self.safe_string(currency, 'altname') + # handle cases like below: + # + # id | altname + # --------------- + # XXBT | XBT + # ZUSD | USD + if id != altName and (id.startswith('X') or id.startswith('Z')): + code = self.safe_currency_code(altName) + # also, add map in commonCurrencies: + self.commonCurrencies[id] = code + else: + code = self.safe_currency_code(id) + isFiat = code.find('.HOLD') >= 0 + result[code] = self.safe_currency_structure({ + 'id': id, + 'code': code, + 'info': currency, + 'name': self.safe_string(currency, 'altname'), + 'active': self.safe_string(currency, 'status') == 'enabled', + 'type': 'fiat' if isFiat else 'crypto', + 'deposit': None, + 'withdraw': None, + 'fee': None, + 'precision': self.parse_number(self.parse_precision(self.safe_string(currency, 'decimals'))), + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + }, + 'networks': {}, + }) + return result + + def safe_currency_code(self, currencyId: Str, currency: Currency = None) -> Str: + if currencyId is None: + return currencyId + if currencyId.find('.') > 0: + # if ID contains .M, .S or .F, then it can't contain X or Z prefix. in such case, ID equals to ALTNAME + parts = currencyId.split('.') + firstPart = self.safe_string(parts, 0) + secondPart = self.safe_string(parts, 1) + return super(kraken, self).safe_currency_code(firstPart, currency) + '.' + secondPart + return super(kraken, self).safe_currency_code(currencyId, currency) + + def fetch_trading_fee(self, symbol: str, params={}) -> TradingFeeInterface: + """ + fetch the trading fees for a market + + https://docs.kraken.com/rest/#tag/Account-Data/operation/getTradeVolume + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `fee structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + 'fee-info': True, + } + response = self.privatePostTradeVolume(self.extend(request, params)) + # + # { + # "error": [], + # "result": { + # "currency": 'ZUSD', + # "volume": '0.0000', + # "fees": { + # "XXBTZUSD": { + # "fee": '0.2600', + # "minfee": '0.1000', + # "maxfee": '0.2600', + # "nextfee": '0.2400', + # "tiervolume": '0.0000', + # "nextvolume": '50000.0000' + # } + # }, + # "fees_maker": { + # "XXBTZUSD": { + # "fee": '0.1600', + # "minfee": '0.0000', + # "maxfee": '0.1600', + # "nextfee": '0.1400', + # "tiervolume": '0.0000', + # "nextvolume": '50000.0000' + # } + # } + # } + # } + # + result = self.safe_value(response, 'result', {}) + return self.parse_trading_fee(result, market) + + def parse_trading_fee(self, response, market): + makerFees = self.safe_value(response, 'fees_maker', {}) + takerFees = self.safe_value(response, 'fees', {}) + symbolMakerFee = self.safe_value(makerFees, market['id'], {}) + symbolTakerFee = self.safe_value(takerFees, market['id'], {}) + return { + 'info': response, + 'symbol': market['symbol'], + 'maker': self.parse_number(Precise.string_div(self.safe_string(symbolMakerFee, 'fee'), '100')), + 'taker': self.parse_number(Precise.string_div(self.safe_string(symbolTakerFee, 'fee'), '100')), + 'percentage': True, + 'tierBased': True, + } + + def parse_bid_ask(self, bidask, priceKey: IndexType = 0, amountKey: IndexType = 1, countOrIdKey: IndexType = 2): + price = self.safe_number(bidask, priceKey) + amount = self.safe_number(bidask, amountKey) + timestamp = self.safe_integer(bidask, 2) + return [price, amount, timestamp] + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://docs.kraken.com/rest/#tag/Spot-Market-Data/operation/getOrderBook + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + if limit is not None: + request['count'] = limit # 100 + response = self.publicGetDepth(self.extend(request, params)) + # + # { + # "error":[], + # "result":{ + # "XETHXXBT":{ + # "asks":[ + # ["0.023480","4.000",1586321307], + # ["0.023490","50.095",1586321306], + # ["0.023500","28.535",1586321302], + # ], + # "bids":[ + # ["0.023470","59.580",1586321307], + # ["0.023460","20.000",1586321301], + # ["0.023440","67.832",1586321306], + # ] + # } + # } + # } + # + result = self.safe_value(response, 'result', {}) + orderbook = self.safe_value(result, market['id']) + # sometimes kraken returns wsname instead of market id + # https://github.com/ccxt/ccxt/issues/8662 + marketInfo = self.safe_value(market, 'info', {}) + wsName = self.safe_value(marketInfo, 'wsname') + if wsName is not None: + orderbook = self.safe_value(result, wsName, orderbook) + return self.parse_order_book(orderbook, symbol) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "a":["2432.77000","1","1.000"], + # "b":["2431.37000","2","2.000"], + # "c":["2430.58000","0.04408910"], + # "v":["4147.94474901","8896.96086304"], + # "p":["2456.22239","2568.63032"], + # "t":[3907,10056], + # "l":["2302.18000","2302.18000"], + # "h":["2621.14000","2860.01000"], + # "o":"2571.56000" + # } + # + symbol = self.safe_symbol(None, market) + v = self.safe_value(ticker, 'v', []) + baseVolume = self.safe_string(v, 1) + p = self.safe_value(ticker, 'p', []) + vwap = self.safe_string(p, 1) + quoteVolume = Precise.string_mul(baseVolume, vwap) + c = self.safe_value(ticker, 'c', []) + last = self.safe_string(c, 0) + high = self.safe_value(ticker, 'h', []) + low = self.safe_value(ticker, 'l', []) + bid = self.safe_value(ticker, 'b', []) + ask = self.safe_value(ticker, 'a', []) + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': None, + 'datetime': None, + 'high': self.safe_string(high, 1), + 'low': self.safe_string(low, 1), + 'bid': self.safe_string(bid, 0), + 'bidVolume': self.safe_string(bid, 2), + 'ask': self.safe_string(ask, 0), + 'askVolume': self.safe_string(ask, 2), + 'vwap': vwap, + 'open': self.safe_string(ticker, 'o'), + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'info': ticker, + }, market) + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://docs.kraken.com/rest/#tag/Spot-Market-Data/operation/getTickerInformation + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + request: dict = {} + if symbols is not None: + symbols = self.market_symbols(symbols) + marketIds = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + market = self.markets[symbol] + if market['active']: + marketIds.append(market['id']) + request['pair'] = ','.join(marketIds) + response = self.publicGetTicker(self.extend(request, params)) + tickers = response['result'] + ids = list(tickers.keys()) + result: dict = {} + for i in range(0, len(ids)): + id = ids[i] + market = self.safe_market(id) + symbol = market['symbol'] + ticker = tickers[id] + result[symbol] = self.parse_ticker(ticker, market) + return self.filter_by_array_tickers(result, 'symbol', symbols) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://docs.kraken.com/rest/#tag/Spot-Market-Data/operation/getTickerInformation + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + response = self.publicGetTicker(self.extend(request, params)) + ticker = response['result'][market['id']] + return self.parse_ticker(ticker, market) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # [ + # 1591475640, + # "0.02500", + # "0.02500", + # "0.02500", + # "0.02500", + # "0.02500", + # "9.12201000", + # 5 + # ] + # + return [ + self.safe_timestamp(ohlcv, 0), + self.safe_number(ohlcv, 1), + self.safe_number(ohlcv, 2), + self.safe_number(ohlcv, 3), + self.safe_number(ohlcv, 4), + self.safe_number(ohlcv, 6), + ] + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://docs.kraken.com/api/docs/rest-api/get-ohlc-data + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate') + if paginate: + return self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params, 720) + market = self.market(symbol) + parsedTimeframe = self.safe_integer(self.timeframes, timeframe) + request: dict = { + 'pair': market['id'], + } + if parsedTimeframe is not None: + request['interval'] = parsedTimeframe + else: + request['interval'] = timeframe + if since is not None: + scaledSince = self.parse_to_int(since / 1000) + timeFrameInSeconds = parsedTimeframe * 60 + request['since'] = self.number_to_string(scaledSince - timeFrameInSeconds) # expected to be in seconds + response = self.publicGetOHLC(self.extend(request, params)) + # + # { + # "error":[], + # "result":{ + # "XETHXXBT":[ + # [1591475580,"0.02499","0.02499","0.02499","0.02499","0.00000","0.00000000",0], + # [1591475640,"0.02500","0.02500","0.02500","0.02500","0.02500","9.12201000",5], + # [1591475700,"0.02499","0.02499","0.02499","0.02499","0.02499","1.28681415",2], + # [1591475760,"0.02499","0.02499","0.02499","0.02499","0.02499","0.08800000",1], + # ], + # "last":1591517580 + # } + # } + result = self.safe_value(response, 'result', {}) + ohlcvs = self.safe_list(result, market['id'], []) + return self.parse_ohlcvs(ohlcvs, market, timeframe, since, limit) + + def parse_ledger_entry_type(self, type): + types: dict = { + 'trade': 'trade', + 'withdrawal': 'transaction', + 'deposit': 'transaction', + 'transfer': 'transfer', + 'margin': 'margin', + } + return self.safe_string(types, type, type) + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + # + # { + # 'LTFK7F-N2CUX-PNY4SX': { + # "refid": "TSJTGT-DT7WN-GPPQMJ", + # "time": 1520102320.555, + # "type": "trade", + # "aclass": "currency", + # "asset": "XETH", + # "amount": "0.1087194600", + # "fee": "0.0000000000", + # "balance": "0.2855851000" + # }, + # ... + # } + # + id = self.safe_string(item, 'id') + direction = None + account = None + referenceId = self.safe_string(item, 'refid') + referenceAccount = None + type = self.parse_ledger_entry_type(self.safe_string(item, 'type')) + currencyId = self.safe_string(item, 'asset') + code = self.safe_currency_code(currencyId, currency) + currency = self.safe_currency(currencyId, currency) + amount = self.safe_string(item, 'amount') + if Precise.string_lt(amount, '0'): + direction = 'out' + amount = Precise.string_abs(amount) + else: + direction = 'in' + timestamp = self.safe_integer_product(item, 'time', 1000) + return self.safe_ledger_entry({ + 'info': item, + 'id': id, + 'direction': direction, + 'account': account, + 'referenceId': referenceId, + 'referenceAccount': referenceAccount, + 'type': type, + 'currency': code, + 'amount': self.parse_number(amount), + 'before': None, + 'after': self.safe_number(item, 'balance'), + 'status': 'ok', + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'fee': { + 'cost': self.safe_number(item, 'fee'), + 'currency': code, + }, + }, currency) + + def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + + https://docs.kraken.com/rest/#tag/Account-Data/operation/getLedgers + + :param str [code]: unified currency code, default is None + :param int [since]: timestamp in ms of the earliest ledger entry, default is None + :param int [limit]: max number of ledger entries to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest ledger entry + :param int [params.end]: timestamp in seconds of the latest ledger entry + :returns dict: a `ledger structure ` + """ + # https://www.kraken.com/features/api#get-ledgers-info + self.load_markets() + request: dict = {} + currency = None + if code is not None: + currency = self.currency(code) + request['asset'] = currency['id'] + if since is not None: + request['start'] = self.parse_to_int(since / 1000) + until = self.safe_string_n(params, ['until', 'till']) + if until is not None: + params = self.omit(params, ['until', 'till']) + untilDivided = Precise.string_div(until, '1000') + request['end'] = self.parse_to_int(Precise.string_add(untilDivided, '1')) + response = self.privatePostLedgers(self.extend(request, params)) + # { error: [], + # "result": {ledger: {'LPUAIB-TS774-UKHP7X': { refid: "A2B4HBV-L4MDIE-JU4N3N", + # "time": 1520103488.314, + # "type": "withdrawal", + # "aclass": "currency", + # "asset": "XETH", + # "amount": "-0.2805800000", + # "fee": "0.0050000000", + # "balance": "0.0000051000" }, + result = self.safe_value(response, 'result', {}) + ledger = self.safe_value(result, 'ledger', {}) + keys = list(ledger.keys()) + items = [] + for i in range(0, len(keys)): + key = keys[i] + value = ledger[key] + value['id'] = key + items.append(value) + return self.parse_ledger(items, currency, since, limit) + + def fetch_ledger_entries_by_ids(self, ids, code: Str = None, params={}): + # https://www.kraken.com/features/api#query-ledgers + self.load_markets() + ids = ','.join(ids) + request = self.extend({ + 'id': ids, + }, params) + response = self.privatePostQueryLedgers(request) + # { error: [], + # "result": {'LPUAIB-TS774-UKHP7X': { refid: "A2B4HBV-L4MDIE-JU4N3N", + # "time": 1520103488.314, + # "type": "withdrawal", + # "aclass": "currency", + # "asset": "XETH", + # "amount": "-0.2805800000", + # "fee": "0.0050000000", + # "balance": "0.0000051000" }}} + result = response['result'] + keys = list(result.keys()) + items = [] + for i in range(0, len(keys)): + key = keys[i] + value = result[key] + value['id'] = key + items.append(value) + return self.parse_ledger(items) + + def fetch_ledger_entry(self, id: str, code: Str = None, params={}) -> LedgerEntry: + items = self.fetch_ledger_entries_by_ids([id], code, params) + return items[0] + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(public) + # + # [ + # "0.032310", # price + # "4.28169434", # amount + # 1541390792.763, # timestamp + # "s", # sell or buy + # "l", # limit or market + # "" + # ] + # + # fetchOrderTrades(private) + # + # { + # "id": 'TIMIRG-WUNNE-RRJ6GT', # injected from outside + # "ordertxid": 'OQRPN2-LRHFY-HIFA7D', + # "postxid": 'TKH2SE-M7IF5-CFI7LT', + # "pair": 'USDCUSDT', + # "time": 1586340086.457, + # "type": 'sell', + # "ordertype": 'market', + # "price": '0.99860000', + # "cost": '22.16892001', + # "fee": '0.04433784', + # "vol": '22.20000000', + # "margin": '0.00000000', + # "misc": '' + # } + # + # fetchMyTrades + # + # { + # "ordertxid": "OSJVN7-A2AE-63WZV", + # "postxid": "TBP7O6-PNXI-CONU", + # "pair": "XXBTZUSD", + # "time": 1710429248.3052235, + # "type": "sell", + # "ordertype": "liquidation market", + # "price": "72026.50000", + # "cost": "7.20265", + # "fee": "0.01873", + # "vol": "0.00010000", + # "margin": "1.44053", + # "leverage": "5", + # "misc": "closing", + # "trade_id": 68230622, + # "maker": False + # } + # + # watchTrades + # + # { + # "symbol": "BTC/USD", + # "side": "buy", + # "price": 109601.2, + # "qty": 0.04561994, + # "ord_type": "market", + # "trade_id": 83449369, + # "timestamp": "2025-05-27T11:24:03.847761Z" + # } + # + timestamp = None + datetime = None + side = None + type = None + price = None + amount = None + id = None + orderId = None + fee = None + symbol = None + if isinstance(trade, list): + timestamp = self.safe_timestamp(trade, 2) + side = 'sell' if (trade[3] == 's') else 'buy' + type = 'limit' if (trade[4] == 'l') else 'market' + price = self.safe_string(trade, 0) + amount = self.safe_string(trade, 1) + tradeLength = len(trade) + if tradeLength > 6: + id = self.safe_string(trade, 6) # artificially added #1794 + elif isinstance(trade, str): + id = trade + elif 'ordertxid' in trade: + marketId = self.safe_string(trade, 'pair') + foundMarket = self.find_market_by_altname_or_id(marketId) + if foundMarket is not None: + market = foundMarket + elif marketId is not None: + # delisted market ids go here + market = self.get_delisted_market_by_id(marketId) + orderId = self.safe_string(trade, 'ordertxid') + id = self.safe_string_2(trade, 'id', 'postxid') + timestamp = self.safe_timestamp(trade, 'time') + side = self.safe_string(trade, 'type') + type = self.safe_string(trade, 'ordertype') + price = self.safe_string(trade, 'price') + amount = self.safe_string(trade, 'vol') + if 'fee' in trade: + currency = None + if market is not None: + currency = market['quote'] + fee = { + 'cost': self.safe_string(trade, 'fee'), + 'currency': currency, + } + else: + symbol = self.safe_string(trade, 'symbol') + datetime = self.safe_string(trade, 'timestamp') + id = self.safe_string(trade, 'trade_id') + side = self.safe_string(trade, 'side') + type = self.safe_string(trade, 'ord_type') + price = self.safe_string(trade, 'price') + amount = self.safe_string(trade, 'qty') + if market is not None: + symbol = market['symbol'] + cost = self.safe_string(trade, 'cost') + maker = self.safe_bool(trade, 'maker') + takerOrMaker = None + if maker is not None: + takerOrMaker = 'maker' if maker else 'taker' + if datetime is None: + datetime = self.iso8601(timestamp) + else: + timestamp = self.parse8601(datetime) + return self.safe_trade({ + 'id': id, + 'order': orderId, + 'info': trade, + 'timestamp': timestamp, + 'datetime': datetime, + 'symbol': symbol, + 'type': type, + 'side': side, + 'takerOrMaker': takerOrMaker, + 'price': price, + 'amount': amount, + 'cost': cost, + 'fee': fee, + }, market) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://docs.kraken.com/rest/#tag/Spot-Market-Data/operation/getRecentTrades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + id = market['id'] + request: dict = { + 'pair': id, + } + # https://support.kraken.com/hc/en-us/articles/218198197-How-to-pull-all-trade-data-using-the-Kraken-REST-API + # https://github.com/ccxt/ccxt/issues/5677 + if since is not None: + request['since'] = self.number_to_string(self.parse_to_int(since / 1000)) # expected to be in seconds + if limit is not None: + request['count'] = limit + response = self.publicGetTrades(self.extend(request, params)) + # + # { + # "error": [], + # "result": { + # "XETHXXBT": [ + # ["0.032310","4.28169434",1541390792.763,"s","l",""] + # ], + # "last": "1541439421200678657" + # } + # } + # + result = response['result'] + trades = result[id] + # trades is a sorted array: last(most recent trade) goes last + length = len(trades) + if length <= 0: + return [] + lastTrade = trades[length - 1] + lastTradeId = self.safe_string(result, 'last') + lastTrade.append(lastTradeId) + trades[length - 1] = lastTrade + return self.parse_trades(trades, market, since, limit) + + def parse_balance(self, response) -> Balances: + balances = self.safe_value(response, 'result', {}) + result: dict = { + 'info': response, + 'timestamp': None, + 'datetime': None, + } + currencyIds = list(balances.keys()) + for i in range(0, len(currencyIds)): + currencyId = currencyIds[i] + code = self.safe_currency_code(currencyId) + balance = self.safe_value(balances, currencyId, {}) + account = self.account() + account['used'] = self.safe_string(balance, 'hold_trade') + account['total'] = self.safe_string(balance, 'balance') + result[code] = account + return self.safe_balance(result) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://docs.kraken.com/rest/#tag/Account-Data/operation/getExtendedBalance + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + response = self.privatePostBalanceEx(params) + # + # { + # "error": [], + # "result": { + # "ZUSD": { + # "balance": 25435.21, + # "hold_trade": 8249.76 + # }, + # "XXBT": { + # "balance": 1.2435, + # "hold_trade": 0.8423 + # } + # } + # } + # + return self.parse_balance(response) + + def create_market_order_with_cost(self, symbol: str, side: OrderSide, cost: float, params={}): + """ + create a market order by providing the symbol, side and cost + + https://docs.kraken.com/rest/#tag/Spot-Trading/operation/addOrder + + :param str symbol: unified symbol of the market to create an order in(only USD markets are supported) + :param str side: 'buy' or 'sell' + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + # only buy orders are supported by the endpoint + req = { + 'cost': cost, + } + return self.create_order(symbol, 'market', side, cost, None, self.extend(req, params)) + + def create_market_buy_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market buy order by providing the symbol, side and cost + + https://docs.kraken.com/rest/#tag/Spot-Trading/operation/addOrder + + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + return self.create_market_order_with_cost(symbol, 'buy', cost, params) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://docs.kraken.com/api/docs/rest-api/add-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.postOnly]: if True, the order will only be posted to the order book and not executed immediately + :param bool [params.reduceOnly]: *margin only* indicates if self order is to reduce the size of a position + :param float [params.stopLossPrice]: *margin only* the price that a stop loss order is triggered at + :param float [params.takeProfitPrice]: *margin only* the price that a take profit order is triggered at + :param str [params.trailingAmount]: *margin only* the quote amount to trail away from the current market price + :param str [params.trailingPercent]: *margin only* the percent to trail away from the current market price + :param str [params.trailingLimitAmount]: *margin only* the quote amount away from the trailingAmount + :param str [params.trailingLimitPercent]: *margin only* the percent away from the trailingAmount + :param str [params.offset]: *margin only* '+' or '-' whether you want the trailingLimitAmount value to be positive or negative, default is negative '-' + :param str [params.trigger]: *margin only* the activation price type, 'last' or 'index', default is 'last' + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + 'type': side, + 'ordertype': type, + 'volume': self.amount_to_precision(symbol, amount), + } + orderRequest = self.order_request('createOrder', symbol, type, request, amount, price, params) + flags = self.safe_string(orderRequest[0], 'oflags', '') + isUsingCost = flags.find('viqc') > -1 + response = self.privatePostAddOrder(self.extend(orderRequest[0], orderRequest[1])) + # + # { + # "error": [], + # "result": { + # "descr": {order: 'buy 0.02100000 ETHUSDT @ limit 330.00'}, # see more examples in "parseOrder" + # "txid": ['OEKVV2-IH52O-TPL6GZ'] + # } + # } + # + result = self.safe_dict(response, 'result') + result['usingCost'] = isUsingCost + # it's impossible to know if the order was created using cost or base currency + # becuase kraken only returns something like self: {order: 'buy 10.00000000 LTCUSD @ market'} + # self usingCost flag is used to help the parsing but omited from the order + return self.parse_order(result) + + def create_orders(self, orders: List[OrderRequest], params={}): + """ + create a list of trade orders + + https://docs.kraken.com/api/docs/rest-api/add-order-batch/ + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + ordersRequests = [] + orderSymbols = [] + symbol = None + market = None + for i in range(0, len(orders)): + rawOrder = orders[i] + marketId = self.safe_string(rawOrder, 'symbol') + if symbol is None: + symbol = marketId + else: + if symbol != marketId: + raise BadRequest(self.id + ' createOrders() requires all orders to have the same symbol') + market = self.market(marketId) + orderSymbols.append(marketId) + type = self.safe_string(rawOrder, 'type') + side = self.safe_string(rawOrder, 'side') + amount = self.safe_value(rawOrder, 'amount') + price = self.safe_value(rawOrder, 'price') + orderParams = self.safe_dict(rawOrder, 'params', {}) + req: dict = { + 'type': side, + 'ordertype': type, + 'volume': self.amount_to_precision(market['symbol'], amount), + } + orderRequest = self.order_request('createOrders', marketId, type, req, amount, price, orderParams) + ordersRequests.append(orderRequest[0]) + orderSymbols = self.market_symbols(orderSymbols, None, False, True, True) + response = None + request: dict = { + 'orders': ordersRequests, + 'pair': market['id'], + } + request = self.extend(request, params) + response = self.privatePostAddOrderBatch(request) + # + # { + # "error":[ + # ], + # "result":{ + # "orders":[ + # { + # "txid":"OEPPJX-34RMM-OROGZE", + # "descr":{ + # "order":"sell 6.000000 ADAUSDC @ limit 0.400000" + # } + # }, + # { + # "txid":"OLQY7O-OYBXW-W23PGL", + # "descr":{ + # "order":"sell 6.000000 ADAUSDC @ limit 0.400000" + # } + # } + # ] + # } + # + result = self.safe_dict(response, 'result', {}) + return self.parse_orders(self.safe_list(result, 'orders')) + + def find_market_by_altname_or_id(self, id): + marketsByAltname = self.safe_value(self.options, 'marketsByAltname', {}) + if id in marketsByAltname: + return marketsByAltname[id] + else: + return self.safe_market(id) + + def get_delisted_market_by_id(self, id): + if id is None: + return id + market = self.safe_value(self.options['delistedMarketsById'], id) + if market is not None: + return market + baseIdStart = 0 + baseIdEnd = 3 + quoteIdStart = 3 + quoteIdEnd = 6 + if len(id) == 8: + baseIdEnd = 4 + quoteIdStart = 4 + quoteIdEnd = 8 + elif len(id) == 7: + baseIdEnd = 4 + quoteIdStart = 4 + quoteIdEnd = 7 + baseId = id[baseIdStart:baseIdEnd] + quoteId = id[quoteIdStart:quoteIdEnd] + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + symbol = base + '/' + quote + market = { + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'baseId': baseId, + 'quoteId': quoteId, + } + self.options['delistedMarketsById'][id] = market + return market + + def parse_order_status(self, status: Str): + statuses: dict = { + 'pending': 'open', # order pending book entry + 'open': 'open', + 'pending_new': 'open', + 'new': 'open', + 'partially_filled': 'open', + 'filled': 'closed', + 'closed': 'closed', + 'canceled': 'canceled', + 'expired': 'expired', + } + return self.safe_string(statuses, status, status) + + def parse_order_type(self, status): + statuses: dict = { + # we dont add "space" delimited orders here(eg. stop loss) because they need separate parsing + 'take-profit': 'market', + 'stop-loss': 'market', + 'stop-loss-limit': 'limit', + 'take-profit-limit': 'limit', + 'trailing-stop-limit': 'limit', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder + # + # { + # "descr": { + # "order": "buy 0.02100000 ETHUSDT @ limit 330.00" # limit orders + # "buy 0.12345678 ETHUSDT @ market" # market order + # "sell 0.28002676 ETHUSDT @ stop loss 0.0123 -> limit 0.0.1222" # stop order + # "sell 0.00100000 ETHUSDT @ stop loss 2677.00 -> limit 2577.00 with 5:1 leverage" + # "buy 0.10000000 LTCUSDT @ take profit 75.00000 -> limit 74.00000" + # "sell 10.00000000 XRPEUR @ trailing stop +50.0000%" # trailing stop + # }, + # "txid": ['OEKVV2-IH52O-TPL6GZ'] + # } + # + # editOrder + # + # { + # "amend_id": "TJSMEH-AA67V-YUSQ6O" + # } + # + # ws - createOrder + # { + # "order_id": "OXM2QD-EALR2-YBAVEU" + # } + # + # ws - editOrder + # { + # "amend_id": "TJSMEH-AA67V-YUSQ6O", + # "order_id": "OXM2QD-EALR2-YBAVEU" + # } + # + # { + # "error": [], + # "result": { + # "open": { + # "OXVPSU-Q726F-L3SDEP": { + # "refid": null, + # "userref": 0, + # "status": "open", + # "opentm": 1706893367.4656649, + # "starttm": 0, + # "expiretm": 0, + # "descr": { + # "pair": "XRPEUR", + # "type": "sell", + # "ordertype": "trailing-stop", + # "price": "+50.0000%", + # "price2": "0", + # "leverage": "none", + # "order": "sell 10.00000000 XRPEUR @ trailing stop +50.0000%", + # "close": "" + # }, + # "vol": "10.00000000", + # "vol_exec": "0.00000000", + # "cost": "0.00000000", + # "fee": "0.00000000", + # "price": "0.00000000", + # "stopprice": "0.23424000", + # "limitprice": "0.46847000", + # "misc": "", + # "oflags": "fciq", + # "trigger": "index" + # } + # } + # } + # + # fetchOpenOrders + # + # { + # "refid": null, + # "userref": null, + # "cl_ord_id": "1234", + # "status": "open", + # "opentm": 1733815269.370054, + # "starttm": 0, + # "expiretm": 0, + # "descr": { + # "pair": "XBTUSD", + # "type": "buy", + # "ordertype": "limit", + # "price": "70000.0", + # "price2": "0", + # "leverage": "none", + # "order": "buy 0.00010000 XBTUSD @ limit 70000.0", + # "close": "" + # }, + # "vol": "0.00010000", + # "vol_exec": "0.00000000", + # "cost": "0.00000", + # "fee": "0.00000", + # "price": "0.00000", + # "stopprice": "0.00000", + # "limitprice": "0.00000", + # "misc": "", + # "oflags": "fciq" + # } + # + isUsingCost = self.safe_bool(order, 'usingCost', False) + order = self.omit(order, 'usingCost') + description = self.safe_dict(order, 'descr', {}) + orderDescriptionObj = self.safe_dict(order, 'descr') # can be null + orderDescription = None + if orderDescriptionObj is not None: + orderDescription = self.safe_string(orderDescriptionObj, 'order') + else: + orderDescription = self.safe_string(order, 'descr') + side = None + rawType = None + marketId = None + price = None + amount = None + cost = None + triggerPrice = None + if orderDescription is not None: + parts = orderDescription.split(' ') + side = self.safe_string(parts, 0) + if not isUsingCost: + amount = self.safe_string(parts, 1) + else: + cost = self.safe_string(parts, 1) + marketId = self.safe_string(parts, 2) + part4 = self.safe_string(parts, 4) + part5 = self.safe_string(parts, 5) + if part4 == 'limit' or part4 == 'market': + rawType = part4 # eg, limit, market + else: + rawType = part4 + ' ' + part5 # eg. stop loss, take profit, trailing stop + if rawType == 'stop loss' or rawType == 'take profit': + triggerPrice = self.safe_string(parts, 6) + price = self.safe_string(parts, 9) + elif rawType == 'limit': + price = self.safe_string(parts, 5) + side = self.safe_string(description, 'type', side) + rawType = self.safe_string(description, 'ordertype', rawType) # orderType has dash, e.g. trailing-stop + marketId = self.safe_string(description, 'pair', marketId) + foundMarket = self.find_market_by_altname_or_id(marketId) + symbol = None + if foundMarket is not None: + market = foundMarket + elif marketId is not None: + # delisted market ids go here + market = self.get_delisted_market_by_id(marketId) + timestamp = self.safe_timestamp(order, 'opentm') + amount = self.safe_string(order, 'vol', amount) + filled = self.safe_string(order, 'vol_exec') + fee = None + # kraken truncates the cost in the api response so we will ignore it and calculate it from average & filled + # cost = self.safe_string(order, 'cost') + price = self.safe_string(description, 'price', price) + # when type = trailing stop returns price = '+50.0000%' + if (price is not None) and (price.endswith('%') or Precise.string_equals(price, '0.00000') or Precise.string_equals(price, '0')): + price = None # self is not the price we want + if price is None: + price = self.safe_string(description, 'price2') + price = self.safe_string_2(order, 'limitprice', 'price', price) + flags = self.safe_string(order, 'oflags', '') + isPostOnly = flags.find('post') > -1 + average = self.safe_number(order, 'price') + if market is not None: + symbol = market['symbol'] + if 'fee' in order: + feeCost = self.safe_string(order, 'fee') + fee = { + 'cost': feeCost, + 'rate': None, + } + if flags.find('fciq') >= 0: + fee['currency'] = market['quote'] + elif flags.find('fcib') >= 0: + fee['currency'] = market['base'] + status = self.parse_order_status(self.safe_string(order, 'status')) + id = self.safe_string_n(order, ['id', 'txid', 'order_id', 'amend_id']) + if (id is None) or (id.startswith('[')): + txid = self.safe_list(order, 'txid') + id = self.safe_string(txid, 0) + userref = self.safe_string(order, 'userref') + clientOrderId = self.safe_string(order, 'cl_ord_id', userref) + rawTrades = self.safe_value(order, 'trades', []) + trades = [] + for i in range(0, len(rawTrades)): + rawTrade = rawTrades[i] + if isinstance(rawTrade, str): + trades.append(self.safe_trade({'id': rawTrade, 'orderId': id, 'symbol': symbol, 'info': {}})) + else: + trades.append(rawTrade) + # in #24192 PR, self field is not something consistent/actual + # triggerPrice = self.omit_zero(self.safe_string(order, 'stopprice', triggerPrice)) + stopLossPrice = None + takeProfitPrice = None + # the dashed strings are not provided from fields(eg. fetch order) + # while spaced strings from "order" sentence(when other fields not available) + if rawType is not None: + if rawType.startswith('take-profit'): + takeProfitPrice = self.safe_string(description, 'price') + price = self.omit_zero(self.safe_string(description, 'price2')) + elif rawType.startswith('stop-loss'): + stopLossPrice = self.safe_string(description, 'price') + price = self.omit_zero(self.safe_string(description, 'price2')) + elif rawType == 'take profit': + takeProfitPrice = triggerPrice + elif rawType == 'stop loss': + stopLossPrice = triggerPrice + finalType = self.parse_order_type(rawType) + # unlike from endpoints which provide eg: "take-profit-limit" + # for "space-delimited" orders we dont have market/limit suffixes, their format is + # eg: `stop loss > limit 123`, so we need to parse them manually + if self.in_array(finalType, ['stop loss', 'take profit']): + finalType = 'market' if (price is None) else 'limit' + amendId = self.safe_string(order, 'amend_id') + if amendId is not None: + isPostOnly = None + return self.safe_order({ + 'id': id, + 'clientOrderId': clientOrderId, + 'info': order, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'status': status, + 'symbol': symbol, + 'type': finalType, + 'timeInForce': None, + 'postOnly': isPostOnly, + 'side': side, + 'price': price, + 'triggerPrice': triggerPrice, + 'takeProfitPrice': takeProfitPrice, + 'stopLossPrice': stopLossPrice, + 'cost': cost, + 'amount': amount, + 'filled': filled, + 'average': average, + 'remaining': None, + 'reduceOnly': self.safe_bool_2(order, 'reduceOnly', 'reduce_only'), + 'fee': fee, + 'trades': trades, + }, market) + + def order_request(self, method: str, symbol: str, type: str, request: dict, amount: Num, price: Num = None, params={}): + clientOrderId = self.safe_string(params, 'clientOrderId') + params = self.omit(params, ['clientOrderId']) + if clientOrderId is not None: + request['cl_ord_id'] = clientOrderId + stopLossTriggerPrice = self.safe_string(params, 'stopLossPrice') + takeProfitTriggerPrice = self.safe_string(params, 'takeProfitPrice') + isStopLossTriggerOrder = stopLossTriggerPrice is not None + isTakeProfitTriggerOrder = takeProfitTriggerPrice is not None + isStopLossOrTakeProfitTrigger = isStopLossTriggerOrder or isTakeProfitTriggerOrder + trailingAmount = self.safe_string(params, 'trailingAmount') + trailingPercent = self.safe_string(params, 'trailingPercent') + trailingLimitAmount = self.safe_string(params, 'trailingLimitAmount') + trailingLimitPercent = self.safe_string(params, 'trailingLimitPercent') + isTrailingAmountOrder = trailingAmount is not None + isTrailingPercentOrder = trailingPercent is not None + isLimitOrder = type.endswith('limit') # supporting limit, stop-loss-limit, take-profit-limit, etc + isMarketOrder = type == 'market' + cost = self.safe_string(params, 'cost') + flags = self.safe_string(params, 'oflags') + params = self.omit(params, ['cost', 'oflags']) + isViqcOrder = (flags is not None) and (flags.find('viqc') > -1) # volume in quote currency + if isMarketOrder and (cost is not None or isViqcOrder): + if cost is None and (amount is not None): + request['volume'] = self.cost_to_precision(symbol, self.number_to_string(amount)) + else: + request['volume'] = self.cost_to_precision(symbol, cost) + extendedOflags = flags + ',viqc' if (flags is not None) else 'viqc' + request['oflags'] = extendedOflags + elif isLimitOrder and not isTrailingAmountOrder and not isTrailingPercentOrder: + request['price'] = self.price_to_precision(symbol, price) + reduceOnly = self.safe_bool_2(params, 'reduceOnly', 'reduce_only') + if isStopLossOrTakeProfitTrigger: + if isStopLossTriggerOrder: + request['price'] = self.price_to_precision(symbol, stopLossTriggerPrice) + if isLimitOrder: + request['ordertype'] = 'stop-loss-limit' + else: + request['ordertype'] = 'stop-loss' + elif isTakeProfitTriggerOrder: + request['price'] = self.price_to_precision(symbol, takeProfitTriggerPrice) + if isLimitOrder: + request['ordertype'] = 'take-profit-limit' + else: + request['ordertype'] = 'take-profit' + if isLimitOrder: + request['price2'] = self.price_to_precision(symbol, price) + elif isTrailingAmountOrder or isTrailingPercentOrder: + trailingPercentString = None + if trailingPercent is not None: + trailingPercentString = ('+' + trailingPercent) if (trailingPercent.endswith('%')) else ('+' + trailingPercent + '%') + trailingAmountString = '+' + trailingAmount if (trailingAmount is not None) else None # must use + for self + offset = self.safe_string(params, 'offset', '-') # can use + or - for self + trailingLimitAmountString = offset + self.number_to_string(trailingLimitAmount) if (trailingLimitAmount is not None) else None + trailingActivationPriceType = self.safe_string(params, 'trigger', 'last') + request['trigger'] = trailingActivationPriceType + if isLimitOrder or (trailingLimitAmount is not None) or (trailingLimitPercent is not None): + request['ordertype'] = 'trailing-stop-limit' + if trailingLimitPercent is not None: + trailingLimitPercentString = (offset + trailingLimitPercent) if (trailingLimitPercent.endswith('%')) else (offset + trailingLimitPercent + '%') + request['price'] = trailingPercentString + request['price2'] = trailingLimitPercentString + elif trailingLimitAmount is not None: + request['price'] = trailingAmountString + request['price2'] = trailingLimitAmountString + else: + request['ordertype'] = 'trailing-stop' + if trailingPercent is not None: + request['price'] = trailingPercentString + else: + request['price'] = trailingAmountString + if reduceOnly: + if method == 'createOrderWs': + request['reduce_only'] = True # ws request can't have stringified bool + else: + request['reduce_only'] = 'true' # not using hasattr(self, boolean) case, because the urlencodedNested transforms it into 'True' string + close = self.safe_dict(params, 'close') + if close is not None: + close = self.extend({}, close) + closePrice = self.safe_value(close, 'price') + if closePrice is not None: + close['price'] = self.price_to_precision(symbol, closePrice) + closePrice2 = self.safe_value(close, 'price2') # stopPrice + if closePrice2 is not None: + close['price2'] = self.price_to_precision(symbol, closePrice2) + request['close'] = close + timeInForce = self.safe_string_2(params, 'timeInForce', 'timeinforce') + if timeInForce is not None: + request['timeinforce'] = timeInForce + isMarket = (type == 'market') + postOnly = None + postOnly, params = self.handle_post_only(isMarket, False, params) + if postOnly: + extendedPostFlags = flags + ',post' if (flags is not None) else 'post' + request['oflags'] = extendedPostFlags + if (flags is not None) and not ('oflags' in request): + request['oflags'] = flags + params = self.omit(params, ['timeInForce', 'reduceOnly', 'stopLossPrice', 'takeProfitPrice', 'trailingAmount', 'trailingPercent', 'trailingLimitAmount', 'trailingLimitPercent', 'offset']) + return [request, params] + + def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + """ + edit a trade order + + https://docs.kraken.com/api/docs/rest-api/amend-order + + :param str id: order id + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float [amount]: how much of the currency you want to trade in units of the base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.stopLossPrice]: the price that a stop loss order is triggered at + :param float [params.takeProfitPrice]: the price that a take profit order is triggered at + :param str [params.trailingAmount]: the quote amount to trail away from the current market price + :param str [params.trailingPercent]: the percent to trail away from the current market price + :param str [params.trailingLimitAmount]: the quote amount away from the trailingAmount + :param str [params.trailingLimitPercent]: the percent away from the trailingAmount + :param str [params.offset]: '+' or '-' whether you want the trailingLimitAmount value to be positive or negative + :param boolean [params.postOnly]: if True, the order will only be posted to the order book and not executed immediately + :param str [params.clientOrderId]: the orders client order id + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' editOrder() does not support ' + market['type'] + ' orders, only spot orders are accepted') + request: dict = { + 'txid': id, + } + clientOrderId = self.safe_string_2(params, 'clientOrderId', 'cl_ord_id') + if clientOrderId is not None: + request['cl_ord_id'] = clientOrderId + params = self.omit(params, ['clientOrderId', 'cl_ord_id']) + request = self.omit(request, 'txid') + isMarket = (type == 'market') + postOnly = None + postOnly, params = self.handle_post_only(isMarket, False, params) + if postOnly: + request['post_only'] = 'true' # not using hasattr(self, boolean) case, because the urlencodedNested transforms it into 'True' string + if amount is not None: + request['order_qty'] = self.amount_to_precision(symbol, amount) + if price is not None: + request['limit_price'] = self.price_to_precision(symbol, price) + allTriggerPrices = self.safe_string_n(params, ['stopLossPrice', 'takeProfitPrice', 'trailingAmount', 'trailingPercent', 'trailingLimitAmount', 'trailingLimitPercent']) + if allTriggerPrices is not None: + offset = self.safe_string(params, 'offset') + params = self.omit(params, ['stopLossPrice', 'takeProfitPrice', 'trailingAmount', 'trailingPercent', 'trailingLimitAmount', 'trailingLimitPercent', 'offset']) + if offset is not None: + allTriggerPrices = offset + allTriggerPrices + request['trigger_price'] = allTriggerPrices + else: + request['trigger_price'] = self.price_to_precision(symbol, allTriggerPrices) + response = self.privatePostAmendOrder(self.extend(request, params)) + # + # { + # "error": [], + # "result": { + # "amend_id": "TJSMEH-AA67V-YUSQ6O" + # } + # } + # + result = self.safe_dict(response, 'result', {}) + return self.parse_order(result, market) + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://docs.kraken.com/rest/#tag/Account-Data/operation/getOrdersInfo + + :param str id: order id + :param str symbol: not used by kraken fetchOrder + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + clientOrderId = self.safe_value_2(params, 'userref', 'clientOrderId') + request: dict = { + 'trades': True, # whether or not to include trades in output(optional, default False) + 'txid': id, # do not comma separate a list of ids - use fetchOrdersByIds instead + # 'userref': 'optional', # restrict results to given user reference id(optional) + } + query = params + if clientOrderId is not None: + request['userref'] = clientOrderId + query = self.omit(params, ['userref', 'clientOrderId']) + response = self.privatePostQueryOrders(self.extend(request, query)) + # + # { + # "error":[], + # "result":{ + # "OTLAS3-RRHUF-NDWH5A":{ + # "refid":null, + # "userref":null, + # "status":"closed", + # "reason":null, + # "opentm":1586822919.3342, + # "closetm":1586822919.365, + # "starttm":0, + # "expiretm":0, + # "descr":{ + # "pair":"XBTUSDT", + # "type":"sell", + # "ordertype":"market", + # "price":"0", + # "price2":"0", + # "leverage":"none", + # "order":"sell 0.21804000 XBTUSDT @ market", + # "close":"" + # }, + # "vol":"0.21804000", + # "vol_exec":"0.21804000", + # "cost":"1493.9", + # "fee":"3.8", + # "price":"6851.5", + # "stopprice":"0.00000", + # "limitprice":"0.00000", + # "misc":"", + # "oflags":"fciq", + # "trades":["TT5UC3-GOIRW-6AZZ6R"] + # } + # } + # } + # + result = self.safe_value(response, 'result', []) + if not (id in result): + raise OrderNotFound(self.id + ' fetchOrder() could not find order id ' + id) + return self.parse_order(self.extend({'id': id}, result[id])) + + def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all the trades made from a single order + + https://docs.kraken.com/rest/#tag/Account-Data/operation/getTradesInfo + + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + orderTrades = self.safe_value(params, 'trades') + tradeIds = [] + if orderTrades is None: + raise ArgumentsRequired(self.id + " fetchOrderTrades() requires a unified order structure in the params argument or a 'trades' param(an array of trade id strings)") + else: + for i in range(0, len(orderTrades)): + orderTrade = orderTrades[i] + if isinstance(orderTrade, str): + tradeIds.append(orderTrade) + else: + tradeIds.append(orderTrade['id']) + self.load_markets() + if symbol is not None: + symbol = self.symbol(symbol) + options = self.safe_value(self.options, 'fetchOrderTrades', {}) + batchSize = self.safe_integer(options, 'batchSize', 20) + numTradeIds = len(tradeIds) + numBatches = self.parse_to_int(numTradeIds / batchSize) + numBatches = self.sum(numBatches, 1) + result = [] + for j in range(0, numBatches): + requestIds = [] + for k in range(0, batchSize): + index = self.sum(j * batchSize, k) + if index < numTradeIds: + requestIds.append(tradeIds[index]) + request: dict = { + 'txid': ','.join(requestIds), + } + response = self.privatePostQueryTrades(request) + # + # { + # "error": [], + # "result": { + # 'TIMIRG-WUNNE-RRJ6GT': { + # "ordertxid": 'OQRPN2-LRHFY-HIFA7D', + # "postxid": 'TKH2SE-M7IF5-CFI7LT', + # "pair": 'USDCUSDT', + # "time": 1586340086.457, + # "type": 'sell', + # "ordertype": 'market', + # "price": '0.99860000', + # "cost": '22.16892001', + # "fee": '0.04433784', + # "vol": '22.20000000', + # "margin": '0.00000000', + # "misc": '' + # } + # } + # } + # + rawTrades = self.safe_value(response, 'result') + ids = list(rawTrades.keys()) + for i in range(0, len(ids)): + rawTrades[ids[i]]['id'] = ids[i] + trades = self.parse_trades(rawTrades, None, since, limit) + tradesFilteredBySymbol = self.filter_by_symbol(trades, symbol) + result = self.array_concat(result, tradesFilteredBySymbol) + return result + + def fetch_orders_by_ids(self, ids, symbol: Str = None, params={}): + """ + fetch orders by the list of order id + + https://docs.kraken.com/rest/#tag/Account-Data/operation/getClosedOrders + + :param str[] [ids]: list of order id + :param str [symbol]: unified ccxt market symbol + :param dict [params]: extra parameters specific to the kraken api endpoint + :returns dict[]: a list of `order structure ` + """ + self.load_markets() + response = self.privatePostQueryOrders(self.extend({ + 'trades': True, # whether or not to include trades in output(optional, default False) + 'txid': ','.join(ids), # comma delimited list of transaction ids to query info about(20 maximum) + }, params)) + result = self.safe_value(response, 'result', {}) + orders = [] + orderIds = list(result.keys()) + for i in range(0, len(orderIds)): + id = orderIds[i] + item = result[id] + order = self.parse_order(self.extend({'id': id}, item)) + orders.append(order) + return orders + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://docs.kraken.com/api/docs/rest-api/get-trade-history + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest trade entry + :param int [params.end]: timestamp in seconds of the latest trade entry + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + request: dict = { + # 'type': 'all', # any position, closed position, closing position, no position + # 'trades': False, # whether or not to include trades related to position in output + # 'start': 1234567890, # starting unix timestamp or trade tx id of results(exclusive) + # 'end': 1234567890, # ending unix timestamp or trade tx id of results(inclusive) + # 'ofs' = result offset + } + if since is not None: + request['start'] = self.parse_to_int(since / 1000) + until = self.safe_string_n(params, ['until', 'till']) + if until is not None: + params = self.omit(params, ['until', 'till']) + untilDivided = Precise.string_div(until, '1000') + request['end'] = self.parse_to_int(Precise.string_add(untilDivided, '1')) + response = self.privatePostTradesHistory(self.extend(request, params)) + # + # { + # "error": [], + # "result": { + # "trades": { + # "GJ3NYQ-XJRTF-THZABF": { + # "ordertxid": "TKH2SE-ZIF5E-CFI7LT", + # "postxid": "OEN3VX-M7IF5-JNBJAM", + # "pair": "XICNXETH", + # "time": 1527213229.4491, + # "type": "sell", + # "ordertype": "limit", + # "price": "0.001612", + # "cost": "0.025792", + # "fee": "0.000026", + # "vol": "16.00000000", + # "margin": "0.000000", + # "leverage": "5", + # "misc": "" + # "trade_id": 68230622, + # "maker": False + # }, + # ... + # }, + # "count": 9760, + # }, + # } + # + trades = response['result']['trades'] + ids = list(trades.keys()) + for i in range(0, len(ids)): + trades[ids[i]]['id'] = ids[i] + market = None + if symbol is not None: + market = self.market(symbol) + return self.parse_trades(trades, market, since, limit) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://docs.kraken.com/api/docs/rest-api/cancel-order + + :param str id: order id + :param str [symbol]: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.clientOrderId]: the orders client order id + :param int [params.userref]: the orders user reference id + :returns dict: an `order structure ` + """ + self.load_markets() + response = None + requestId = self.safe_value(params, 'userref', id) # string or integer + params = self.omit(params, 'userref') + request: dict = { + 'txid': requestId, # order id or userref + } + clientOrderId = self.safe_string_2(params, 'clientOrderId', 'cl_ord_id') + if clientOrderId is not None: + request['cl_ord_id'] = clientOrderId + params = self.omit(params, ['clientOrderId', 'cl_ord_id']) + request = self.omit(request, 'txid') + try: + response = self.privatePostCancelOrder(self.extend(request, params)) + # + # { + # error: [], + # result: { + # count: '1' + # } + # } + # + except Exception as e: + if self.last_http_response: + if self.last_http_response.find('EOrder:Unknown order') >= 0: + raise OrderNotFound(self.id + ' cancelOrder() error ' + self.last_http_response) + raise e + return self.safe_order({ + 'info': response, + }) + + def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): + """ + cancel multiple orders + + https://docs.kraken.com/rest/#tag/Spot-Trading/operation/cancelOrderBatch + + :param str[] ids: open orders transaction ID(txid) or user reference(userref) + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an list of `order structures ` + """ + request: dict = { + 'orders': ids, + } + response = self.privatePostCancelOrderBatch(self.extend(request, params)) + # + # { + # "error": [], + # "result": { + # "count": 2 + # } + # } + # + return [ + self.safe_order({ + 'info': response, + }), + ] + + def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders + + https://docs.kraken.com/rest/#tag/Spot-Trading/operation/cancelAllOrders + + :param str symbol: unified market symbol, not used by kraken cancelAllOrders(all open orders are cancelled) + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + self.load_markets() + response = self.privatePostCancelAll(params) + # + # { + # error: [], + # result: { + # count: '1' + # } + # } + # + return [ + self.safe_order({ + 'info': response, + }), + ] + + def cancel_all_orders_after(self, timeout: Int, params={}): + """ + dead man's switch, cancel all orders after the given timeout + + https://docs.kraken.com/rest/#tag/Spot-Trading/operation/cancelAllOrdersAfter + + :param number timeout: time in milliseconds, 0 represents cancel the timer + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: the api result + """ + if timeout > 86400000: + raise BadRequest(self.id + ' cancelAllOrdersAfter timeout should be less than 86400000 milliseconds') + self.load_markets() + request: dict = { + 'timeout': (self.parse_to_int(timeout / 1000)) if (timeout > 0) else 0, + } + response = self.privatePostCancelAllOrdersAfter(self.extend(request, params)) + # + # { + # "error": [], + # "result": { + # "currentTime": "2023-03-24T17:41:56Z", + # "triggerTime": "2023-03-24T17:42:56Z" + # } + # } + # + return response + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://docs.kraken.com/api/docs/rest-api/get-open-orders + + :param str [symbol]: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.clientOrderId]: the orders client order id + :param int [params.userref]: the orders user reference id + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + request: dict = {} + if since is not None: + request['start'] = self.parse_to_int(since / 1000) + userref = self.safe_integer(params, 'userref') + if userref is not None: + request['userref'] = userref + params = self.omit(params, 'userref') + clientOrderId = self.safe_string(params, 'clientOrderId') + if clientOrderId is not None: + request['cl_ord_id'] = clientOrderId + params = self.omit(params, 'clientOrderId') + response = self.privatePostOpenOrders(self.extend(request, params)) + # + # { + # "error": [], + # "result": { + # "open": { + # "O45M52-BFD5S-YXKQOU": { + # "refid": null, + # "userref": null, + # "cl_ord_id": "1234", + # "status": "open", + # "opentm": 1733815269.370054, + # "starttm": 0, + # "expiretm": 0, + # "descr": { + # "pair": "XBTUSD", + # "type": "buy", + # "ordertype": "limit", + # "price": "70000.0", + # "price2": "0", + # "leverage": "none", + # "order": "buy 0.00010000 XBTUSD @ limit 70000.0", + # "close": "" + # }, + # "vol": "0.00010000", + # "vol_exec": "0.00000000", + # "cost": "0.00000", + # "fee": "0.00000", + # "price": "0.00000", + # "stopprice": "0.00000", + # "limitprice": "0.00000", + # "misc": "", + # "oflags": "fciq" + # } + # } + # } + # } + # + market = None + if symbol is not None: + market = self.market(symbol) + result = self.safe_dict(response, 'result', {}) + open = self.safe_dict(result, 'open', {}) + orders = [] + orderIds = list(open.keys()) + for i in range(0, len(orderIds)): + id = orderIds[i] + item = open[id] + orders.append(self.extend({'id': id}, item)) + return self.parse_orders(orders, market, since, limit) + + def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://docs.kraken.com/api/docs/rest-api/get-closed-orders + + :param str [symbol]: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest entry + :param str [params.clientOrderId]: the orders client order id + :param int [params.userref]: the orders user reference id + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + request: dict = {} + if since is not None: + request['start'] = self.parse_to_int(since / 1000) + userref = self.safe_integer(params, 'userref') + if userref is not None: + request['userref'] = userref + params = self.omit(params, 'userref') + clientOrderId = self.safe_string(params, 'clientOrderId') + if clientOrderId is not None: + request['cl_ord_id'] = clientOrderId + params = self.omit(params, 'clientOrderId') + request, params = self.handle_until_option('end', request, params) + response = self.privatePostClosedOrders(self.extend(request, params)) + # + # { + # "error":[], + # "result":{ + # "closed":{ + # "OETZYO-UL524-QJMXCT":{ + # "refid":null, + # "userref":null, + # "status":"canceled", + # "reason":"User requested", + # "opentm":1601489313.3898, + # "closetm":1601489346.5507, + # "starttm":0, + # "expiretm":0, + # "descr":{ + # "pair":"ETHUSDT", + # "type":"buy", + # "ordertype":"limit", + # "price":"330.00", + # "price2":"0", + # "leverage":"none", + # "order":"buy 0.02100000 ETHUSDT @ limit 330.00", + # "close":"" + # }, + # "vol":"0.02100000", + # "vol_exec":"0.00000000", + # "cost":"0.00000", + # "fee":"0.00000", + # "price":"0.00000", + # "stopprice":"0.00000", + # "limitprice":"0.00000", + # "misc":"", + # "oflags":"fciq" + # }, + # }, + # "count":16 + # } + # } + # + market = None + if symbol is not None: + market = self.market(symbol) + result = self.safe_dict(response, 'result', {}) + closed = self.safe_dict(result, 'closed', {}) + orders = [] + orderIds = list(closed.keys()) + for i in range(0, len(orderIds)): + id = orderIds[i] + item = closed[id] + orders.append(self.extend({'id': id}, item)) + return self.parse_orders(orders, market, since, limit) + + def parse_transaction_status(self, status: Str): + # IFEX transaction states + statuses: dict = { + 'Initial': 'pending', + 'Pending': 'pending', + 'Success': 'ok', + 'Settled': 'pending', + 'Failure': 'failed', + 'Partial': 'ok', + } + return self.safe_string(statuses, status, status) + + def parse_network(self, network): + withdrawMethods = self.safe_value(self.options, 'withdrawMethods', {}) + return self.safe_string(withdrawMethods, network, network) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fetchDeposits + # + # { + # "method": "Ether(Hex)", + # "aclass": "currency", + # "asset": "XETH", + # "refid": "Q2CANKL-LBFVEE-U4Y2WQ", + # "txid": "0x57fd704dab1a73c20e24c8696099b695d596924b401b261513cfdab23…", + # "info": "0x615f9ba7a9575b0ab4d571b2b36b1b324bd83290", + # "amount": "7.9999257900", + # "fee": "0.0000000000", + # "time": 1529223212, + # "status": "Success" + # } + # + # there can be an additional 'status-prop' field present + # deposit pending review by exchange => 'on-hold' + # the deposit is initiated by the exchange => 'return' + # + # { + # "type": 'deposit', + # "method": 'Fidor Bank AG(Wire Transfer)', + # "aclass": 'currency', + # "asset": 'ZEUR', + # "refid": 'xxx-xxx-xxx', + # "txid": '12341234', + # "info": 'BANKCODEXXX', + # "amount": '38769.08', + # "fee": '0.0000', + # "time": 1644306552, + # "status": 'Success', + # status-prop: 'on-hold' + # } + # + # + # fetchWithdrawals + # + # { + # "method": "Ether", + # "aclass": "currency", + # "asset": "XETH", + # "refid": "A2BF34S-O7LBNQ-UE4Y4O", + # "txid": "0x288b83c6b0904d8400ef44e1c9e2187b5c8f7ea3d838222d53f701a15b5c274d", + # "info": "0x7cb275a5e07ba943fee972e165d80daa67cb2dd0", + # "amount": "9.9950000000", + # "fee": "0.0050000000", + # "time": 1530481750, + # "status": "Success" + # "key":"Huobi wallet", + # "network":"Tron" + # status-prop: 'on-hold' # self field might not be present in some cases + # } + # + # withdraw + # + # { + # "refid": "AGBSO6T-UFMTTQ-I7KGS6" + # } + # + id = self.safe_string(transaction, 'refid') + txid = self.safe_string(transaction, 'txid') + timestamp = self.safe_timestamp(transaction, 'time') + currencyId = self.safe_string(transaction, 'asset') + code = self.safe_currency_code(currencyId, currency) + address = self.safe_string(transaction, 'info') + amount = self.safe_number(transaction, 'amount') + status = self.parse_transaction_status(self.safe_string(transaction, 'status')) + statusProp = self.safe_string(transaction, 'status-prop') + isOnHoldDeposit = statusProp == 'on-hold' + isCancellationRequest = statusProp == 'cancel-pending' + isOnHoldWithdrawal = statusProp == 'onhold' + if isOnHoldDeposit or isCancellationRequest or isOnHoldWithdrawal: + status = 'pending' + type = self.safe_string(transaction, 'type') # injected from the outside + feeCost = self.safe_number(transaction, 'fee') + if feeCost is None: + if type == 'deposit': + feeCost = 0 + return { + 'info': transaction, + 'id': id, + 'currency': code, + 'amount': amount, + 'network': self.parse_network(self.safe_string(transaction, 'network')), + 'address': address, + 'addressTo': None, + 'addressFrom': None, + 'tag': None, + 'tagTo': None, + 'tagFrom': None, + 'status': status, + 'type': type, + 'updated': None, + 'txid': txid, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'comment': None, + 'internal': None, + 'fee': { + 'currency': code, + 'cost': feeCost, + }, + } + + def parse_transactions_by_type(self, type, transactions, code: Str = None, since: Int = None, limit: Int = None): + result = [] + for i in range(0, len(transactions)): + transaction = self.parse_transaction(self.extend({ + 'type': type, + }, transactions[i])) + result.append(transaction) + return self.filter_by_currency_since_limit(result, code, since, limit) + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://docs.kraken.com/rest/#tag/Funding/operation/getStatusRecentDeposits + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest transaction entry + :param int [params.end]: timestamp in seconds of the latest transaction entry + :returns dict[]: a list of `transaction structures ` + """ + # https://www.kraken.com/en-us/help/api#deposit-status + self.load_markets() + request: dict = {} + if code is not None: + currency = self.currency(code) + request['asset'] = currency['id'] + if since is not None: + sinceString = self.number_to_string(since) + request['start'] = Precise.string_div(sinceString, '1000') + until = self.safe_string_n(params, ['until', 'till']) + if until is not None: + params = self.omit(params, ['until', 'till']) + untilDivided = Precise.string_div(until, '1000') + request['end'] = Precise.string_add(untilDivided, '1') + response = self.privatePostDepositStatus(self.extend(request, params)) + # + # { error: [], + # "result": [{"method": "Ether(Hex)", + # "aclass": "currency", + # "asset": "XETH", + # "refid": "Q2CANKL-LBFVEE-U4Y2WQ", + # "txid": "0x57fd704dab1a73c20e24c8696099b695d596924b401b261513cfdab23…", + # "info": "0x615f9ba7a9575b0ab4d571b2b36b1b324bd83290", + # "amount": "7.9999257900", + # "fee": "0.0000000000", + # "time": 1529223212, + # "status": "Success" }]} + # + return self.parse_transactions_by_type('deposit', response['result'], code, since, limit) + + def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://docs.kraken.com/rest/#tag/Spot-Market-Data/operation/getServerTime + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + # https://www.kraken.com/en-us/features/api#get-server-time + response = self.publicGetTime(params) + # + # { + # "error": [], + # "result": { + # "unixtime": 1591502873, + # "rfc1123": "Sun, 7 Jun 20 04:07:53 +0000" + # } + # } + # + result = self.safe_value(response, 'result', {}) + return self.safe_timestamp(result, 'unixtime') + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://docs.kraken.com/rest/#tag/Funding/operation/getStatusRecentWithdrawals + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest transaction entry + :param int [params.end]: timestamp in seconds of the latest transaction entry + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchWithdrawals', 'paginate') + if paginate: + params['cursor'] = True + return self.fetch_paginated_call_cursor('fetchWithdrawals', code, since, limit, params, 'next_cursor', 'cursor') + request: dict = {} + if code is not None: + currency = self.currency(code) + request['asset'] = currency['id'] + if since is not None: + sinceString = self.number_to_string(since) + request['start'] = Precise.string_div(sinceString, '1000') + until = self.safe_string_n(params, ['until', 'till']) + if until is not None: + params = self.omit(params, ['until', 'till']) + untilDivided = Precise.string_div(until, '1000') + request['end'] = Precise.string_add(untilDivided, '1') + response = self.privatePostWithdrawStatus(self.extend(request, params)) + # + # with no pagination + # { error: [], + # "result": [{"method": "Ether", + # "aclass": "currency", + # "asset": "XETH", + # "refid": "A2BF34S-O7LBNQ-UE4Y4O", + # "txid": "0x298c83c7b0904d8400ef43e1c9e2287b518f7ea3d838822d53f704a1565c274d", + # "info": "0x7cb275a5e07ba943fee972e165d80daa67cb2dd0", + # "amount": "9.9950000000", + # "fee": "0.0050000000", + # "time": 1530481750, + # "status": "Success" }]} + # with pagination + # { + # "error":[], + # "result":{ + # "withdrawals":[ + # { + # "method":"Tether USD(TRC20)", + # "aclass":"currency", + # "asset":"USDT", + # "refid":"BSNFZU2-MEFN4G-J3NEZV", + # "txid":"1c7a642fb7387bbc2c6a2c509fd1ae146937f4cf793b4079a4f0715e3a02615a", + # "info":"TQmdxSuC16EhFg8FZWtYgrfFRosoRF7bCp", + # "amount":"1996.50000000", + # "fee":"2.50000000", + # "time":1669126657, + # "status":"Success", + # "key":"poloniex", + # "network":"Tron" + # }, + # ... + # ], + # "next_cursor":"HgAAAAAAAABGVFRSd3k1LVF4Y0JQY05Gd0xRY0NxenFndHpybkwBAQH2AwEBAAAAAQAAAAAAAAABAAAAAAAZAAAAAAAAAA==" + # } + # } + # + rawWithdrawals = None + result = self.safe_value(response, 'result') + if not isinstance(result, list): + rawWithdrawals = self.add_pagination_cursor_to_result(result) + else: + rawWithdrawals = result + return self.parse_transactions_by_type('withdrawal', rawWithdrawals, code, since, limit) + + def add_pagination_cursor_to_result(self, result): + cursor = self.safe_string(result, 'next_cursor') + data = self.safe_value(result, 'withdrawals') + dataLength = len(data) + if cursor is not None and dataLength > 0: + last = data[dataLength - 1] + last['next_cursor'] = cursor + data[dataLength - 1] = last + return data + + def create_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + create a currency deposit address + + https://docs.kraken.com/rest/#tag/Funding/operation/getDepositAddresses + + :param str code: unified currency code of the currency for the deposit address + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + request: dict = { + 'new': 'true', + } + return self.fetch_deposit_address(code, self.extend(request, params)) + + def fetch_deposit_methods(self, code: str, params={}): + """ + fetch deposit methods for a currency associated with self account + + https://docs.kraken.com/rest/#tag/Funding/operation/getDepositMethods + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the kraken api endpoint + :returns dict: of deposit methods + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'asset': currency['id'], + } + response = self.privatePostDepositMethods(self.extend(request, params)) + # + # { + # "error":[], + # "result":[ + # {"method":"Ether(Hex)","limit":false,"gen-address":true} + # ] + # } + # + # { + # "error":[], + # "result":[ + # {"method":"Tether USD(ERC20)","limit":false,"address-setup-fee":"0.00000000","gen-address":true}, + # {"method":"Tether USD(TRC20)","limit":false,"address-setup-fee":"0.00000000","gen-address":true} + # ] + # } + # + # { + # "error":[], + # "result":[ + # {"method":"Bitcoin","limit":false,"fee":"0.0000000000","gen-address":true} + # ] + # } + # + return self.safe_value(response, 'result') + + def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://docs.kraken.com/rest/#tag/Funding/operation/getDepositAddresses + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + self.load_markets() + currency = self.currency(code) + network = self.safe_string_upper(params, 'network') + networks = self.safe_value(self.options, 'networks', {}) + network = self.safe_string(networks, network, network) # support ETH > ERC20 aliases + params = self.omit(params, 'network') + if (code == 'USDT') and (network == 'TRC20'): + code = code + '-' + network + defaultDepositMethods = self.safe_value(self.options, 'depositMethods', {}) + defaultDepositMethod = self.safe_string(defaultDepositMethods, code) + depositMethod = self.safe_string(params, 'method', defaultDepositMethod) + # if the user has specified an exchange-specific method in params + # we pass it, otherwise we take the 'network' unified param + if depositMethod is None: + depositMethods = self.fetch_deposit_methods(code) + if network is not None: + # find best matching deposit method, or fallback to the first one + for i in range(0, len(depositMethods)): + entry = self.safe_string(depositMethods[i], 'method') + if entry.find(network) >= 0: + depositMethod = entry + break + # if depositMethod was not specified, fallback to the first available deposit method + if depositMethod is None: + firstDepositMethod = self.safe_value(depositMethods, 0, {}) + depositMethod = self.safe_string(firstDepositMethod, 'method') + request: dict = { + 'asset': currency['id'], + 'method': depositMethod, + } + response = self.privatePostDepositAddresses(self.extend(request, params)) + # + # { + # "error":[], + # "result":[ + # {"address":"0x77b5051f97efa9cc52c9ad5b023a53fc15c200d3","expiretm":"0"} + # ] + # } + # + result = self.safe_value(response, 'result', []) + firstResult = self.safe_value(result, 0, {}) + if firstResult is None: + raise InvalidAddress(self.id + ' privatePostDepositAddresses() returned no addresses for ' + code) + return self.parse_deposit_address(firstResult, currency) + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + # + # { + # "address":"0x77b5051f97efa9cc52c9ad5b023a53fc15c200d3", + # "expiretm":"0" + # } + # + address = self.safe_string(depositAddress, 'address') + tag = self.safe_string(depositAddress, 'tag') + currency = self.safe_currency(None, currency) + code = currency['code'] + self.check_address(address) + return { + 'info': depositAddress, + 'currency': code, + 'network': None, + 'address': address, + 'tag': tag, + } + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://docs.kraken.com/rest/#tag/Funding/operation/withdrawFunds + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to, not required can be '' or None/none/None + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + if 'key' in params: + self.load_markets() + currency = self.currency(code) + request: dict = { + 'asset': currency['id'], + 'amount': amount, + # 'address': address, + } + if address is not None and address != '': + request['address'] = address + self.check_address(address) + response = self.privatePostWithdraw(self.extend(request, params)) + # + # { + # "error": [], + # "result": { + # "refid": "AGBSO6T-UFMTTQ-I7KGS6" + # } + # } + # + result = self.safe_dict(response, 'result', {}) + return self.parse_transaction(result, currency) + raise ExchangeError(self.id + " withdraw() requires a 'key' parameter(withdrawal key name, up on your account)") + + def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://docs.kraken.com/rest/#tag/Account-Data/operation/getOpenPositions + + :param str[] [symbols]: not used by kraken fetchPositions() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + self.load_markets() + request: dict = { + # 'txid': 'comma delimited list of transaction ids to restrict output to', + 'docalcs': 'true', # whether or not to include profit/loss calculations + 'consolidation': 'market', # what to consolidate the positions data around, market will consolidate positions based on market pair + } + response = self.privatePostOpenPositions(self.extend(request, params)) + # + # no consolidation + # + # { + # "error": [], + # "result": { + # 'TGUFMY-FLESJ-VYIX3J': { + # "ordertxid": "O3LRNU-ZKDG5-XNCDFR", + # "posstatus": "open", + # "pair": "ETHUSDT", + # "time": 1611557231.4584, + # "type": "buy", + # "ordertype": "market", + # "cost": "28.49800", + # "fee": "0.07979", + # "vol": "0.02000000", + # "vol_closed": "0.00000000", + # "margin": "14.24900", + # "terms": "0.0200% per 4 hours", + # "rollovertm": "1611571631", + # "misc": "", + # "oflags": "" + # } + # } + # } + # + # consolidation by market + # + # { + # "error": [], + # "result": [ + # { + # "pair": "ETHUSDT", + # "positions": "1", + # "type": "buy", + # "leverage": "2.00000", + # "cost": "28.49800", + # "fee": "0.07979", + # "vol": "0.02000000", + # "vol_closed": "0.00000000", + # "margin": "14.24900" + # } + # ] + # } + # + symbols = self.market_symbols(symbols) + result = self.safe_list(response, 'result') + results = self.parse_positions(result, symbols) + return self.filter_by_array_positions(results, 'symbol', symbols, False) + + def parse_position(self, position: dict, market: Market = None): + # + # { + # "pair": "ETHUSDT", + # "positions": "1", + # "type": "buy", + # "leverage": "2.00000", + # "cost": "28.49800", + # "fee": "0.07979", + # "vol": "0.02000000", + # "vol_closed": "0.00000000", + # "margin": "14.24900" + # } + # + marketId = self.safe_string(position, 'pair') + rawSide = self.safe_string(position, 'type') + side = 'long' if (rawSide == 'buy') else 'short' + return self.safe_position({ + 'info': position, + 'id': None, + 'symbol': self.safe_symbol(marketId, market), + 'notional': None, + 'marginMode': None, + 'liquidationPrice': None, + 'entryPrice': None, + 'unrealizedPnl': self.safe_number(position, 'net'), + 'realizedPnl': None, + 'percentage': None, + 'contracts': self.safe_number(position, 'vol'), + 'contractSize': None, + 'markPrice': None, + 'lastPrice': None, + 'side': side, + 'hedged': None, + 'timestamp': None, + 'datetime': None, + 'lastUpdateTimestamp': None, + 'maintenanceMargin': None, + 'maintenanceMarginPercentage': None, + 'collateral': None, + 'initialMargin': self.safe_number(position, 'margin'), + 'initialMarginPercentage': None, + 'leverage': self.safe_number(position, 'leverage'), + 'marginRatio': None, + 'stopLossPrice': None, + 'takeProfitPrice': None, + }) + + def parse_account_type(self, account): + accountByType: dict = { + 'spot': 'Spot Wallet', + 'swap': 'Futures Wallet', + 'future': 'Futures Wallet', + } + return self.safe_string(accountByType, account, account) + + def transfer_out(self, code: str, amount, params={}): + """ + transfer from spot wallet to futures wallet + + https://docs.kraken.com/rest/#tag/User-Funding/operation/walletTransfer + + :param str code: Unified currency code + :param float amount: Size of the transfer + :param dict [params]: Exchange specific parameters + :returns: a `transfer structure ` + """ + return self.transfer(code, amount, 'spot', 'swap', params) + + def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + + https://docs.kraken.com/rest/#tag/User-Funding/operation/walletTransfer + + transfers currencies between sub-accounts(only spot->swap direction is supported) + :param str code: Unified currency code + :param float amount: Size of the transfer + :param str fromAccount: 'spot' or 'Spot Wallet' + :param str toAccount: 'swap' or 'Futures Wallet' + :param dict [params]: Exchange specific parameters + :returns: a `transfer structure ` + """ + self.load_markets() + currency = self.currency(code) + fromAccount = self.parse_account_type(fromAccount) + toAccount = self.parse_account_type(toAccount) + request: dict = { + 'amount': self.currency_to_precision(code, amount), + 'from': fromAccount, + 'to': toAccount, + 'asset': currency['id'], + } + if fromAccount != 'Spot Wallet': + raise BadRequest(self.id + ' transfer cannot transfer from ' + fromAccount + ' to ' + toAccount + '. Use krakenfutures instead to transfer from the futures account.') + response = self.privatePostWalletTransfer(self.extend(request, params)) + # + # { + # "error":[ + # ], + # "result":{ + # "refid":"BOIUSIF-M7DLMN-UXZ3P5" + # } + # } + # + transfer = self.parse_transfer(response, currency) + return self.extend(transfer, { + 'amount': amount, + 'fromAccount': fromAccount, + 'toAccount': toAccount, + }) + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + # + # transfer + # + # { + # "error":[ + # ], + # "result":{ + # "refid":"BOIUSIF-M7DLMN-UXZ3P5" + # } + # } + # + result = self.safe_value(transfer, 'result', {}) + refid = self.safe_string(result, 'refid') + return { + 'info': transfer, + 'id': refid, + 'timestamp': None, + 'datetime': None, + 'currency': self.safe_string(currency, 'code'), + 'amount': None, + 'fromAccount': None, + 'toAccount': None, + 'status': 'sucess', + } + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = '/' + self.version + '/' + api + '/' + path + if api == 'public': + if params: + # urlencodeNested is used to address https://github.com/ccxt/ccxt/issues/12872 + url += '?' + self.urlencode_nested(params) + elif api == 'private': + price = self.safe_string(params, 'price') + isTriggerPercent = False + if price is not None: + isTriggerPercent = True if (price.endswith('%')) else False + isCancelOrderBatch = (path == 'CancelOrderBatch') + isBatchOrder = (path == 'AddOrderBatch') + self.check_required_credentials() + nonce = str(self.nonce()) + # urlencodeNested is used to address https://github.com/ccxt/ccxt/issues/12872 + if isCancelOrderBatch or isTriggerPercent or isBatchOrder: + body = self.json(self.extend({'nonce': nonce}, params)) + else: + body = self.urlencode_nested(self.extend({'nonce': nonce}, params)) + auth = self.encode(nonce + body) + hash = self.hash(auth, 'sha256', 'binary') + binary = self.encode(url) + binhash = self.binary_concat(binary, hash) + secret = self.base64_to_binary(self.secret) + signature = self.hmac(binhash, secret, hashlib.sha512, 'base64') + headers = { + 'API-Key': self.apiKey, + 'API-Sign': signature, + } + if isCancelOrderBatch or isTriggerPercent or isBatchOrder: + headers['Content-Type'] = 'application/json' + else: + headers['Content-Type'] = 'application/x-www-form-urlencoded' + else: + url = '/' + path + url = self.urls['api'][api] + url + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def nonce(self): + return self.milliseconds() - self.options['timeDifference'] + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if code == 520: + raise ExchangeNotAvailable(self.id + ' ' + str(code) + ' ' + reason) + if response is None: + return None + if body[0] == '{': + if not isinstance(response, str): + message = self.id + ' ' + body + if 'error' in response: + numErrors = len(response['error']) + if numErrors: + for i in range(0, len(response['error'])): + error = response['error'][i] + self.throw_exactly_matched_exception(self.exceptions['exact'], error, message) + self.throw_broadly_matched_exception(self.exceptions['broad'], error, message) + raise ExchangeError(message) + # handleCreateOrdersErrors: + if 'result' in response: + result = self.safe_dict(response, 'result', {}) + if 'orders' in result: + orders = self.safe_list(result, 'orders', []) + for i in range(0, len(orders)): + order = orders[i] + error = self.safe_string(order, 'error') + if error is not None: + self.throw_exactly_matched_exception(self.exceptions['exact'], error, message) + self.throw_broadly_matched_exception(self.exceptions['broad'], error, message) + raise ExchangeError(message) + return None diff --git a/ccxt/krakenfutures.py b/ccxt/krakenfutures.py new file mode 100644 index 0000000..1c1ba2e --- /dev/null +++ b/ccxt/krakenfutures.py @@ -0,0 +1,2782 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.krakenfutures import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currency, Int, Leverage, Leverages, LeverageTier, LeverageTiers, Market, Num, Order, OrderBook, OrderRequest, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, FundingRates, Trade, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import OrderImmediatelyFillable +from ccxt.base.errors import OrderNotFillable +from ccxt.base.errors import DuplicateOrderId +from ccxt.base.errors import ContractUnavailable +from ccxt.base.errors import DDoSProtection +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.errors import InvalidNonce +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class krakenfutures(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(krakenfutures, self).describe(), { + 'id': 'krakenfutures', + 'name': 'Kraken Futures', + 'countries': ['US'], + 'version': 'v3', + 'userAgent': None, + 'rateLimit': 600, + 'pro': True, + 'has': { + 'CORS': None, + 'spot': False, + 'margin': False, + 'swap': True, + 'future': True, + 'option': False, + 'cancelAllOrders': True, + 'cancelAllOrdersAfter': True, + 'cancelOrder': True, + 'cancelOrders': True, + 'createMarketOrder': False, + 'createOrder': True, + 'createPostOnlyOrder': True, + 'createReduceOnlyOrder': True, + 'createStopLimitOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'createTriggerOrder': True, + 'editOrder': True, + 'fetchBalance': True, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchCanceledOrders': True, + 'fetchClosedOrders': True, # https://support.kraken.com/hc/en-us/articles/360058243651-Historical-orders + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': False, + 'fetchDepositAddress': False, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchFundingHistory': None, + 'fetchFundingRate': 'emulated', + 'fetchFundingRateHistory': True, + 'fetchFundingRates': True, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLeverage': True, + 'fetchLeverages': True, + 'fetchLeverageTiers': True, + 'fetchMarketLeverageTiers': 'emulated', + 'fetchMarkets': True, + 'fetchMarkOHLCV': True, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenOrders': True, + 'fetchOrder': False, + 'fetchOrderBook': True, + 'fetchOrders': False, + 'fetchPositions': True, + 'fetchPremiumIndexOHLCV': False, + 'fetchTickers': True, + 'fetchTrades': True, + 'sandbox': True, + 'setLeverage': True, + 'setMarginMode': False, + 'transfer': True, + }, + 'urls': { + 'test': { + 'public': 'https://demo-futures.kraken.com/derivatives/api/', + 'private': 'https://demo-futures.kraken.com/derivatives/api/', + 'charts': 'https://demo-futures.kraken.com/api/charts/', + 'history': 'https://demo-futures.kraken.com/api/history/', + 'www': 'https://demo-futures.kraken.com', + }, + 'logo': 'https://user-images.githubusercontent.com/24300605/81436764-b22fd580-9172-11ea-9703-742783e6376d.jpg', + 'api': { + 'charts': 'https://futures.kraken.com/api/charts/', + 'history': 'https://futures.kraken.com/api/history/', + 'feeschedules': 'https://futures.kraken.com/api/feeschedules/', + 'public': 'https://futures.kraken.com/derivatives/api/', + 'private': 'https://futures.kraken.com/derivatives/api/', + }, + 'www': 'https://futures.kraken.com/', + 'doc': [ + 'https://docs.kraken.com/api/docs/futures-api/trading/market-data/', + ], + 'fees': 'https://support.kraken.com/hc/en-us/articles/360022835771-Transaction-fees-and-rebates-for-Kraken-Futures', + 'referral': None, + }, + 'api': { + 'public': { + 'get': [ + 'feeschedules', + 'instruments', + 'orderbook', + 'tickers', + 'history', + 'historicalfundingrates', + ], + }, + 'private': { + 'get': [ + 'feeschedules/volumes', + 'openpositions', + 'notifications', + 'accounts', + 'openorders', + 'recentorders', + 'fills', + 'transfers', + 'leveragepreferences', + 'pnlpreferences', + 'assignmentprogram/current', + 'assignmentprogram/history', + ], + 'post': [ + 'sendorder', + 'editorder', + 'cancelorder', + 'transfer', + 'batchorder', + 'cancelallorders', + 'cancelallordersafter', + 'withdrawal', # for futures wallet -> kraken spot wallet + 'assignmentprogram/add', + 'assignmentprogram/delete', + ], + 'put': [ + 'leveragepreferences', + 'pnlpreferences', + ], + }, + 'charts': { + 'get': [ + '{price_type}/{symbol}/{interval}', + ], + }, + 'history': { + 'get': [ + 'orders', + 'executions', + 'triggers', + 'accountlogcsv', + 'account-log', + 'market/{symbol}/orders', + 'market/{symbol}/executions', + ], + }, + }, + 'fees': { + 'trading': { + 'tierBased': True, + 'percentage': True, + 'taker': self.parse_number('0.0005'), + 'maker': self.parse_number('0.0002'), + 'tiers': { + 'taker': [ + [self.parse_number('0'), self.parse_number('0.0005')], + [self.parse_number('100000'), self.parse_number('0.0004')], + [self.parse_number('1000000'), self.parse_number('0.0003')], + [self.parse_number('5000000'), self.parse_number('0.00025')], + [self.parse_number('10000000'), self.parse_number('0.0002')], + [self.parse_number('20000000'), self.parse_number('0.00015')], + [self.parse_number('50000000'), self.parse_number('0.000125')], + [self.parse_number('100000000'), self.parse_number('0.0001')], + ], + 'maker': [ + [self.parse_number('0'), self.parse_number('0.0002')], + [self.parse_number('100000'), self.parse_number('0.0015')], + [self.parse_number('1000000'), self.parse_number('0.000125')], + [self.parse_number('5000000'), self.parse_number('0.0001')], + [self.parse_number('10000000'), self.parse_number('0.000075')], + [self.parse_number('20000000'), self.parse_number('0.00005')], + [self.parse_number('50000000'), self.parse_number('0.000025')], + [self.parse_number('100000000'), self.parse_number('0')], + ], + }, + }, + }, + 'exceptions': { + 'exact': { + 'apiLimitExceeded': RateLimitExceeded, + 'marketUnavailable': ContractUnavailable, + 'requiredArgumentMissing': BadRequest, + 'unavailable': ExchangeNotAvailable, + 'authenticationError': AuthenticationError, + 'accountInactive': ExchangeError, # When account has no trade history / no order history. Should self error be ignored in some cases? + 'invalidAccount': BadRequest, # the fromAccount or the toAccount are invalid + 'invalidAmount': BadRequest, + 'insufficientFunds': InsufficientFunds, + 'Bad Request': BadRequest, # The URL contains invalid characters.(Please encode the json URL parameter) + 'Unavailable': ExchangeNotAvailable, # https://github.com/ccxt/ccxt/issues/24338 + 'invalidUnit': BadRequest, + 'Json Parse Error': ExchangeError, + 'nonceBelowThreshold': InvalidNonce, + 'nonceDuplicate': InvalidNonce, + 'notFound': BadRequest, + 'Server Error': ExchangeError, + 'unknownError': ExchangeError, + }, + 'broad': { + 'invalidArgument': BadRequest, + 'nonceBelowThreshold': InvalidNonce, + 'nonceDuplicate': InvalidNonce, + }, + }, + 'precisionMode': TICK_SIZE, + 'options': { + 'access': { + 'history': { + 'GET': { + 'orders': 'private', + 'executions': 'private', + 'triggers': 'private', + 'accountlogcsv': 'private', + 'account-log': 'private', + }, + }, + }, + 'settlementCurrencies': { + 'flex': ['USDT', 'BTC', 'USD', 'GBP', 'EUR', 'USDC'], + }, + 'symbol': { + 'quoteIds': ['USD', 'XBT'], + 'reversed': False, + }, + 'versions': { + 'public': { + 'GET': { + 'historicalfundingrates': 'v4', + }, + }, + 'charts': { + 'GET': { + '{price_type}/{symbol}/{interval}': 'v1', + }, + }, + 'history': { + 'GET': { + 'orders': 'v2', + 'executions': 'v2', + 'triggers': 'v2', + 'accountlogcsv': 'v2', + }, + }, + }, + 'fetchTrades': { + 'method': 'historyGetMarketSymbolExecutions', # historyGetMarketSymbolExecutions, publicGetHistory + }, + }, + 'features': { + 'default': { + 'sandbox': True, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerPriceType': { + 'last': True, + 'mark': True, + 'index': True, + }, + 'triggerDirection': False, + 'stopLossPrice': True, + 'takeProfitPrice': True, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': False, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': { + 'max': 100, + }, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': None, + 'daysBack': None, + 'untilDays': 100000, + 'symbolRequired': False, + }, + 'fetchOrder': None, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': None, + 'daysBack': None, + 'daysBackCanceled': None, + 'untilDays': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 5000, + }, + }, + 'spot': None, + 'swap': { + 'linear': { + 'extends': 'default', + }, + 'inverse': { + 'extends': 'default', + }, + }, + 'future': { + 'linear': { + 'extends': 'default', + }, + 'inverse': { + 'extends': 'default', + }, + }, + }, + 'timeframes': { + '1m': '1m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1h', + '4h': '4h', + '12h': '12h', + '1d': '1d', + '1w': '1w', + }, + }) + + def fetch_markets(self, params={}) -> List[Market]: + """ + Fetches the available trading markets from the exchange, Multi-collateral markets are returned markets, but can be settled in multiple currencies + + https://docs.kraken.com/api/docs/futures-api/trading/get-instruments + + :param dict [params]: exchange specific params + :returns: An array of market structures + """ + response = self.publicGetInstruments(params) + # + # { + # "result": "success", + # "instruments": [ + # { + # "symbol": "fi_ethusd_180928", + # "type": "futures_inverse", # futures_vanilla # spot index + # "underlying": "rr_ethusd", + # "lastTradingTime": "2018-09-28T15:00:00.000Z", + # "tickSize": 0.1, + # "contractSize": 1, + # "tradeable": True, + # "marginLevels": [ + # { + # "contracts":0, + # "initialMargin":0.02, + # "maintenanceMargin":0.01 + # }, + # { + # "contracts":250000, + # "initialMargin":0.04, + # "maintenanceMargin":0.02 + # }, + # ... + # ], + # "isin": "GB00JVMLMP88", + # "retailMarginLevels": [ + # { + # "contracts": 0, + # "initialMargin": 0.5, + # "maintenanceMargin": 0.25 + # } + # ], + # "tags": [], + # }, + # { + # "symbol": "in_xbtusd", + # "type": "spot index", + # "tradeable":false + # } + # ] + # "serverTime": "2018-07-19T11:32:39.433Z" + # } + # + instruments = self.safe_value(response, 'instruments', []) + result = [] + for i in range(0, len(instruments)): + market = instruments[i] + id = self.safe_string(market, 'symbol') + marketType = self.safe_string(market, 'type') + type = None + index = (marketType.find(' index') >= 0) + linear = None + inverse = None + expiry = None + if not index: + linear = (marketType.find('_vanilla') >= 0) + inverse = not linear + settleTime = self.safe_string(market, 'lastTradingTime') + type = 'swap' if (settleTime is None) else 'future' + expiry = self.parse8601(settleTime) + else: + type = 'index' + swap = (type == 'swap') + future = (type == 'future') + symbol = id + split = id.split('_') + splitMarket = self.safe_string(split, 1) + baseId = splitMarket[0:len(splitMarket) - 3] + quoteId = 'usd' # always USD + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + # swap == perpetual + settle = None + settleId = None + cvtp = self.safe_string(market, 'contractValueTradePrecision') + amountPrecision = self.parse_number(self.integer_precision_to_amount(cvtp)) + pricePrecision = self.safe_number(market, 'tickSize') + contract = (swap or future or index) + swapOrFutures = (swap or future) + if swapOrFutures: + exchangeType = self.safe_string(market, 'type') + if exchangeType == 'futures_inverse': + settle = base + settleId = baseId + inverse = True + else: + settle = quote + settleId = quoteId + inverse = False + linear = not inverse + symbol = base + '/' + quote + ':' + settle + if future: + symbol = symbol + '-' + self.yymmdd(expiry) + result.append({ + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': type, + 'spot': False, + 'margin': False, + 'swap': swap, + 'future': future, + 'option': False, + 'index': index, + 'active': self.safe_bool(market, 'tradeable'), + 'contract': contract, + 'linear': linear, + 'inverse': inverse, + 'contractSize': self.safe_number(market, 'contractSize'), + 'maintenanceMarginRate': None, + 'expiry': expiry, + 'expiryDatetime': self.iso8601(expiry), + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': amountPrecision, + 'price': pricePrecision, + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': None, + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': self.parse8601(self.safe_string(market, 'openingDate')), + 'info': market, + }) + settlementCurrencies = self.options['settlementCurrencies']['flex'] + currencies = [] + for i in range(0, len(settlementCurrencies)): + code = settlementCurrencies[i] + currencies.append({ + 'id': code.lower(), + 'numericId': None, + 'code': code, + 'precision': None, + }) + self.currencies = self.map_to_safe_map(self.deep_extend(currencies, self.currencies)) + return result + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + + https://docs.kraken.com/api/docs/futures-api/trading/get-orderbook + + Fetches a list of open orders in a market + :param str symbol: Unified market symbol + :param int [limit]: Not used by krakenfutures + :param dict [params]: exchange specific params + :returns: An `order book structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = self.publicGetOrderbook(self.extend(request, params)) + # + # { + # "result": "success", + # "serverTime": "2016-02-25T09:45:53.818Z", + # "orderBook": { + # "bids": [ + # [ + # 4213, + # 2000, + # ], + # [ + # 4210, + # 4000, + # ], + # ... + # ], + # "asks": [ + # [ + # 4218, + # 4000, + # ], + # [ + # 4220, + # 5000, + # ], + # ... + # ], + # }, + # } + # + timestamp = self.parse8601(response['serverTime']) + return self.parse_order_book(response['orderBook'], symbol, timestamp) + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://docs.kraken.com/api/docs/futures-api/trading/get-tickers + + :param str[] symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an array of `ticker structures ` + """ + self.load_markets() + response = self.publicGetTickers(params) + # + # { + # "result": "success", + # "tickers": [ + # { + # "tag": 'semiannual', # 'month', 'quarter', "perpetual", "semiannual", + # "pair": "ETH:USD", + # "symbol": "fi_ethusd_220624", + # "markPrice": "2925.72", + # "bid": "2923.8", + # "bidSize": "16804", + # "ask": "2928.65", + # "askSize": "1339", + # "vol24h": "860493", + # "openInterest": "3023363.00000000", + # "open24h": "3021.25", + # "indexPrice": "2893.71", + # "last": "2942.25", + # "lastTime": "2022-02-18T14:08:15.578Z", + # "lastSize": "151", + # "suspended": False + # }, + # { + # "symbol": "in_xbtusd", # "rr_xbtusd", + # "last": "40411", + # "lastTime": "2022-02-18T14:16:28.000Z" + # }, + # ... + # ], + # "serverTime": "2022-02-18T14:16:29.440Z" + # } + # + tickers = self.safe_list(response, 'tickers') + return self.parse_tickers(tickers, symbols) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "tag": 'semiannual', # 'month', 'quarter', "perpetual", "semiannual", + # "pair": "ETH:USD", + # "symbol": "fi_ethusd_220624", + # "markPrice": "2925.72", + # "bid": "2923.8", + # "bidSize": "16804", + # "ask": "2928.65", + # "askSize": "1339", + # "vol24h": "860493", + # "openInterest": "3023363.00000000", + # "open24h": "3021.25", + # "indexPrice": "2893.71", + # "last": "2942.25", + # "lastTime": "2022-02-18T14:08:15.578Z", + # "lastSize": "151", + # "suspended": False + # } + # + # { + # "symbol": "in_xbtusd", # "rr_xbtusd", + # "last": "40411", + # "lastTime": "2022-02-18T14:16:28.000Z" + # } + # + marketId = self.safe_string(ticker, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + timestamp = self.parse8601(self.safe_string(ticker, 'lastTime')) + open = self.safe_string(ticker, 'open24h') + last = self.safe_string(ticker, 'last') + change = Precise.string_sub(last, open) + percentage = Precise.string_mul(Precise.string_div(change, open), '100') + average = Precise.string_div(Precise.string_add(open, last), '2') + volume = self.safe_string(ticker, 'vol24h') + baseVolume = None + quoteVolume = None + isIndex = self.safe_bool(market, 'index', False) + if not isIndex: + if market['linear']: + baseVolume = volume + elif market['inverse']: + quoteVolume = volume + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': None, + 'low': None, + 'bid': self.safe_string(ticker, 'bid'), + 'bidVolume': self.safe_string(ticker, 'bidSize'), + 'ask': self.safe_string(ticker, 'ask'), + 'askVolume': self.safe_string(ticker, 'askSize'), + 'vwap': None, + 'open': open, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': change, + 'percentage': percentage, + 'average': average, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'markPrice': self.safe_string(ticker, 'markPrice'), + 'indexPrice': self.safe_string(ticker, 'indexPrice'), + 'info': ticker, + }) + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + + https://docs.kraken.com/api/docs/futures-api/charts/candles + + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + market = self.market(symbol) + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate') + if paginate: + return self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params, 5000) + request: dict = { + 'symbol': market['id'], + 'price_type': self.safe_string(params, 'price', 'trade'), + 'interval': self.timeframes[timeframe], + } + params = self.omit(params, 'price') + if since is not None: + duration = self.parse_timeframe(timeframe) + request['from'] = self.parse_to_int(since / 1000) + if limit is None: + limit = 5000 + limit = min(limit, 5000) + toTimestamp = self.sum(request['from'], limit * duration - 1) + currentTimestamp = self.seconds() + request['to'] = min(toTimestamp, currentTimestamp) + elif limit is not None: + limit = min(limit, 5000) + duration = self.parse_timeframe(timeframe) + request['to'] = self.seconds() + request['from'] = self.parse_to_int(request['to'] - (duration * limit)) + response = self.chartsGetPriceTypeSymbolInterval(self.extend(request, params)) + # + # { + # "candles": [ + # { + # "time": 1645198500000, + # "open": "309.15000000000", + # "high": "309.15000000000", + # "low": "308.70000000000", + # "close": "308.85000000000", + # "volume": 0 + # } + # ], + # "more_candles": True + # } + # + candles = self.safe_list(response, 'candles') + return self.parse_ohlcvs(candles, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # { + # "time": 1645198500000, + # "open": "309.15000000000", + # "high": "309.15000000000", + # "low": "308.70000000000", + # "close": "308.85000000000", + # "volume": 0 + # } + # + return [ + self.safe_integer(ohlcv, 'time'), # unix timestamp in milliseconds + self.safe_number(ohlcv, 'open'), # open price + self.safe_number(ohlcv, 'high'), # highest price + self.safe_number(ohlcv, 'low'), # lowest price + self.safe_number(ohlcv, 'close'), # close price + self.safe_number(ohlcv, 'volume'), # trading volume, None for mark or index price + ] + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + + https://docs.kraken.com/api/docs/futures-api/trading/get-history + https://docs.kraken.com/api/docs/futures-api/history/get-public-execution-events + + Fetch a history of filled trades that self account has made + :param str symbol: Unified CCXT market symbol + :param int [since]: Timestamp in ms of earliest trade. Not used by krakenfutures except in combination with params.until + :param int [limit]: Total number of trades, cannot exceed 100 + :param dict [params]: Exchange specific params + :param int [params.until]: Timestamp in ms of latest trade + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param str [params.method]: The method to use to fetch trades. Can be 'historyGetMarketSymbolExecutions' or 'publicGetHistory' default is 'historyGetMarketSymbolExecutions' + :returns: An array of `trade structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchTrades', 'paginate') + if paginate: + return self.fetch_paginated_call_dynamic('fetchTrades', symbol, since, limit, params) + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + method = None + method, params = self.handle_option_and_params(params, 'fetchTrades', 'method', 'historyGetMarketSymbolExecutions') + rawTrades = None + isFullHistoryEndpoint = (method == 'historyGetMarketSymbolExecutions') + if isFullHistoryEndpoint: + request, params = self.handle_until_option('before', request, params) + if since is not None: + request['since'] = since + request['sort'] = 'asc' + if limit is not None: + request['count'] = limit + response = self.historyGetMarketSymbolExecutions(self.extend(request, params)) + # + # { + # "elements": [ + # { + # "uid": "a5105030-f054-44cc-98ab-30d5cae96bef", + # "timestamp": "1710150778607", + # "event": { + # "Execution": { + # "execution": { + # "uid": "2d485b71-cd28-4a1e-9364-371a127550d2", + # "makerOrder": { + # "uid": "0a25f66b-1109-49ec-93a3-d17bf9e9137e", + # "tradeable": "PF_XBTUSD", + # "direction": "Buy", + # "quantity": "0.26500", + # "timestamp": "1710150778570", + # "limitPrice": "71907", + # "orderType": "Post", + # "reduceOnly": False, + # "lastUpdateTimestamp": "1710150778570" + # }, + # "takerOrder": { + # "uid": "04de3ee0-9125-4960-bf8f-f63b577b6790", + # "tradeable": "PF_XBTUSD", + # "direction": "Sell", + # "quantity": "0.0002", + # "timestamp": "1710150778607", + # "limitPrice": "71187.00", + # "orderType": "Market", + # "reduceOnly": False, + # "lastUpdateTimestamp": "1710150778607" + # }, + # "timestamp": "1710150778607", + # "quantity": "0.0002", + # "price": "71907", + # "markPrice": "71903.32715463147", + # "limitFilled": False, + # "usdValue": "14.38" + # }, + # "takerReducedQuantity": "" + # } + # } + # }, + # ... followed by older items + # ], + # "len": "1000", + # "continuationToken": "QTexMDE0OTe33NTcyXy8xNDIzAjc1NjY5MwI=" + # } + # + elements = self.safe_list(response, 'elements', []) + # we need to reverse the list to fix chronology + rawTrades = [] + length = len(elements) + for i in range(0, length): + index = length - 1 - i + element = elements[index] + event = self.safe_dict(element, 'event', {}) + executionContainer = self.safe_dict(event, 'Execution', {}) + rawTrade = self.safe_dict(executionContainer, 'execution', {}) + rawTrades.append(rawTrade) + else: + request, params = self.handle_until_option('lastTime', request, params) + response = self.publicGetHistory(self.extend(request, params)) + # + # { + # "result": "success", + # "history": [ + # { + # "time": "2022-03-18T04:55:37.692Z", + # "trade_id": 100, + # "price": 0.7921, + # "size": 1068, + # "side": "sell", + # "type": "fill", + # "uid": "6c5da0b0-f1a8-483f-921f-466eb0388265" + # }, + # ... + # ], + # "serverTime": "2022-03-18T06:39:18.056Z" + # } + # + rawTrades = self.safe_list(response, 'history', []) + return self.parse_trades(rawTrades, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(recent trades) + # + # { + # "time": "2019-02-14T09:25:33.920Z", + # "trade_id": 100, + # "price": 3574, + # "size": 100, + # "side": "buy", + # "type": "fill" # fill, liquidation, assignment, termination + # "uid": "11c3d82c-9e70-4fe9-8115-f643f1b162d4" + # } + # + # fetchTrades(executions history) + # + # { + # "timestamp": "1710152516830", + # "price": "71927.0", + # "quantity": "0.0695", + # "markPrice": "71936.38701675525", + # "limitFilled": True, + # "usdValue": "4998.93", + # "uid": "116ae634-253f-470b-bd20-fa9d429fb8b1", + # "makerOrder": {"uid": "17bfe4de-c01e-4938-926c-617d2a2d0597", "tradeable": "PF_XBTUSD", "direction": "Buy", "quantity": "0.0695", "timestamp": "1710152515836", "limitPrice": "71927.0", "orderType": "Post", "reduceOnly": False, "lastUpdateTimestamp": "1710152515836"}, + # "takerOrder": {"uid": "d3e437b4-aa70-4108-b5cf-b1eecb9845b5", "tradeable": "PF_XBTUSD", "direction": "Sell", "quantity": "0.940100", "timestamp": "1710152516830", "limitPrice": "71915", "orderType": "IoC", "reduceOnly": False, "lastUpdateTimestamp": "1710152516830"} + # } + # + # fetchMyTrades(private) + # + # { + # "fillTime": "2016-02-25T09:47:01.000Z", + # "order_id": "c18f0c17-9971-40e6-8e5b-10df05d422f0", + # "fill_id": "522d4e08-96e7-4b44-9694-bfaea8fe215e", + # "cliOrdId": "d427f920-ec55-4c18-ba95-5fe241513b30", # OPTIONAL + # "symbol": "fi_xbtusd_180615", + # "side": "buy", + # "size": 2000, + # "price": 4255, + # "fillType": "maker" # taker, takerAfterEdit, maker, liquidation, assignee + # } + # + # execution report(createOrder, editOrder) + # + # { + # "executionId": "e1ec9f63-2338-4c44-b40a-43486c6732d7", + # "price": 7244.5, + # "amount": 10, + # "orderPriorEdit": null, + # "orderPriorExecution": { + # "orderId": "61ca5732-3478-42fe-8362-abbfd9465294", + # "cliOrdId": null, + # "type": "lmt", + # "symbol": "pi_xbtusd", + # "side": "buy", + # "quantity": 10, + # "filled": 0, + # "limitPrice": 7500, + # "reduceOnly": False, + # "timestamp": "2019-12-11T17:17:33.888Z", + # "lastUpdateTimestamp": "2019-12-11T17:17:33.888Z" + # }, + # "takerReducedQuantity": null, + # "type": "EXECUTION" + # } + # + timestamp = self.parse8601(self.safe_string_2(trade, 'time', 'fillTime')) + price = self.safe_string(trade, 'price') + amount = self.safe_string_n(trade, ['size', 'amount', 'quantity'], '0.0') + id = self.safe_string_2(trade, 'uid', 'fill_id') + if id is None: + id = self.safe_string(trade, 'executionId') + order = self.safe_string(trade, 'order_id') + marketId = self.safe_string(trade, 'symbol') + side = self.safe_string(trade, 'side') + type = None + priorEdit = self.safe_value(trade, 'orderPriorEdit') + priorExecution = self.safe_value(trade, 'orderPriorExecution') + if priorExecution is not None: + order = self.safe_string(priorExecution, 'orderId') + marketId = self.safe_string(priorExecution, 'symbol') + side = self.safe_string(priorExecution, 'side') + type = self.safe_string(priorExecution, 'type') + elif priorEdit is not None: + order = self.safe_string(priorEdit, 'orderId') + marketId = self.safe_string(priorEdit, 'symbol') + side = self.safe_string(priorEdit, 'type') + type = self.safe_string(priorEdit, 'type') + if type is not None: + type = self.parse_order_type(type) + market = self.safe_market(marketId, market) + cost = None + linear = self.safe_bool(market, 'linear') + if (amount is not None) and (price is not None) and (market is not None): + if linear: + cost = Precise.string_mul(amount, price) # in quote + else: + cost = Precise.string_div(amount, price) # in base + contractSize = self.safe_string(market, 'contractSize') + cost = Precise.string_mul(cost, contractSize) + takerOrMaker = None + fillType = self.safe_string(trade, 'fillType') + if fillType is not None: + if fillType.find('taker') >= 0: + takerOrMaker = 'taker' + elif fillType.find('maker') >= 0: + takerOrMaker = 'maker' + isHistoricalExecution = ('takerOrder' in trade) + if isHistoricalExecution: + timestamp = self.safe_integer(trade, 'timestamp') + taker = self.safe_dict(trade, 'takerOrder', {}) + if taker is not None: + side = self.safe_string_lower(taker, 'direction') + takerOrMaker = 'taker' + return self.safe_trade({ + 'info': trade, + 'id': id, + 'symbol': self.safe_string(market, 'symbol'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'order': order, + 'type': type, + 'side': side, + 'takerOrMaker': takerOrMaker, + 'price': price, + 'amount': amount if linear else None, + 'cost': cost, + 'fee': None, + }) + + def create_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + market = self.market(symbol) + symbol = market['symbol'] + type = self.safe_string(params, 'orderType', type) + timeInForce = self.safe_string(params, 'timeInForce') + postOnly = False + postOnly, params = self.handle_post_only(type == 'market', type == 'post', params) + if postOnly: + type = 'post' + elif timeInForce == 'ioc': + type = 'ioc' + elif type == 'limit': + type = 'lmt' + elif type == 'market': + type = 'mkt' + request: dict = { + 'symbol': market['id'], + 'side': side, + 'size': self.amount_to_precision(symbol, amount), + } + clientOrderId = self.safe_string_2(params, 'clientOrderId', 'cliOrdId') + if clientOrderId is not None: + request['cliOrdId'] = clientOrderId + triggerPrice = self.safe_string_2(params, 'triggerPrice', 'stopPrice') + isTriggerOrder = triggerPrice is not None + stopLossTriggerPrice = self.safe_string(params, 'stopLossPrice') + takeProfitTriggerPrice = self.safe_string(params, 'takeProfitPrice') + isStopLossTriggerOrder = stopLossTriggerPrice is not None + isTakeProfitTriggerOrder = takeProfitTriggerPrice is not None + isStopLossOrTakeProfitTrigger = isStopLossTriggerOrder or isTakeProfitTriggerOrder + triggerSignal = self.safe_string(params, 'triggerSignal', 'last') + reduceOnly = self.safe_value(params, 'reduceOnly') + if isStopLossOrTakeProfitTrigger or isTriggerOrder: + request['triggerSignal'] = triggerSignal + if isTriggerOrder: + type = 'stp' + request['stopPrice'] = self.price_to_precision(symbol, triggerPrice) + elif isStopLossOrTakeProfitTrigger: + reduceOnly = True + if isStopLossTriggerOrder: + type = 'stp' + request['stopPrice'] = self.price_to_precision(symbol, stopLossTriggerPrice) + elif isTakeProfitTriggerOrder: + type = 'take_profit' + request['stopPrice'] = self.price_to_precision(symbol, takeProfitTriggerPrice) + if reduceOnly: + request['reduceOnly'] = True + request['orderType'] = type + if price is not None: + request['limitPrice'] = self.price_to_precision(symbol, price) + params = self.omit(params, ['clientOrderId', 'timeInForce', 'triggerPrice', 'stopLossPrice', 'takeProfitPrice']) + return self.extend(request, params) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + Create an order on the exchange + + https://docs.kraken.com/api/docs/futures-api/trading/send-order + + :param str symbol: unified market symbol + :param str type: 'limit' or 'market' + :param str side: 'buy' or 'sell' + :param float amount: number of contracts + :param float [price]: limit order price + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.reduceOnly]: set if you wish the order to only reduce an existing position, any order which increases an existing position will be rejected, default is False + :param bool [params.postOnly]: set if you wish to make a postOnly order, default is False + :param str [params.clientOrderId]: UUID The order identity that is specified from the user, It must be globally unique + :param float [params.triggerPrice]: the price that a stop order is triggered at + :param float [params.stopLossPrice]: the price that a stop loss order is triggered at + :param float [params.takeProfitPrice]: the price that a take profit order is triggered at + :param str [params.triggerSignal]: for triggerPrice, stopLossPrice and takeProfitPrice orders, the trigger price type, 'last', 'mark' or 'index', default is 'last' + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + orderRequest = self.create_order_request(symbol, type, side, amount, price, params) + response = self.privatePostSendorder(orderRequest) + # + # { + # "result": "success", + # "sendStatus": { + # "order_id": "salf320-e337-47ac-b345-30sdfsalj", + # "status": "placed", + # "receivedTime": "2022-02-28T19:32:17.122Z", + # "orderEvents": [ + # { + # "order": { + # "orderId": "salf320-e337-47ac-b345-30sdfsalj", + # "cliOrdId": null, + # "type": "lmt", + # "symbol": "pi_xrpusd", + # "side": "buy", + # "quantity": 1, + # "filled": 0, + # "limitPrice": 0.7, + # "reduceOnly": False, + # "timestamp": "2022-02-28T19:32:17.122Z", + # "lastUpdateTimestamp": "2022-02-28T19:32:17.122Z" + # }, + # "reducedQuantity": null, + # "type": "PLACE" + # } + # ] + # }, + # "serverTime": "2022-02-28T19:32:17.122Z" + # } + # + sendStatus = self.safe_value(response, 'sendStatus') + status = self.safe_string(sendStatus, 'status') + self.verify_order_action_success(status, 'createOrder', ['filled']) + return self.parse_order(sendStatus, market) + + def create_orders(self, orders: List[OrderRequest], params={}): + """ + create a list of trade orders + + https://docs.kraken.com/api/docs/futures-api/trading/send-batch-order + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + ordersRequests = [] + for i in range(0, len(orders)): + rawOrder = orders[i] + marketId = self.safe_string(rawOrder, 'symbol') + type = self.safe_string(rawOrder, 'type') + side = self.safe_string(rawOrder, 'side') + amount = self.safe_value(rawOrder, 'amount') + price = self.safe_value(rawOrder, 'price') + orderParams = self.safe_value(rawOrder, 'params', {}) + extendedParams = self.extend(orderParams, params) # the request does not accept extra params since it's a list, so we're extending each order with the common params + if not ('order_tag' in extendedParams): + # order tag is mandatory so we will generate one if not provided + extendedParams['order_tag'] = self.sum(i, str(1)) # sequential counter + extendedParams['order'] = 'send' + orderRequest = self.create_order_request(marketId, type, side, amount, price, extendedParams) + ordersRequests.append(orderRequest) + request: dict = { + 'batchOrder': ordersRequests, + } + response = self.privatePostBatchorder(self.extend(request, params)) + # + # { + # "result": "success", + # "serverTime": "2023-10-24T08:40:57.339Z", + # "batchStatus": [ + # { + # "status": "requiredArgumentMissing", + # "orderEvents": [] + # }, + # { + # "status": "requiredArgumentMissing", + # "orderEvents": [] + # } + # ] + # } + # + data = self.safe_list(response, 'batchStatus', []) + return self.parse_orders(data) + + def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + """ + + https://docs.kraken.com/api/docs/futures-api/trading/edit-order-spring + + Edit an open order on the exchange + :param str id: order id + :param str symbol: Not used by Krakenfutures + :param str type: Not used by Krakenfutures + :param str side: Not used by Krakenfutures + :param float amount: Order size + :param float [price]: Price to fill order at + :param dict [params]: Exchange specific params + :returns: An `order structure ` + """ + self.load_markets() + request: dict = { + 'orderId': id, + } + if amount is not None: + request['size'] = amount + if price is not None: + request['limitPrice'] = price + response = self.privatePostEditorder(self.extend(request, params)) + status = self.safe_string(response['editStatus'], 'status') + self.verify_order_action_success(status, 'editOrder', ['filled']) + order = self.parse_order(response['editStatus']) + order['info'] = response + return order + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + + https://docs.kraken.com/api/docs/futures-api/trading/cancel-order + + Cancel an open order on the exchange + :param str id: Order id + :param str symbol: Not used by Krakenfutures + :param dict [params]: Exchange specific params + :returns: An `order structure ` + """ + self.load_markets() + response = self.privatePostCancelorder(self.extend({'order_id': id}, params)) + status = self.safe_string(self.safe_value(response, 'cancelStatus', {}), 'status') + self.verify_order_action_success(status, 'cancelOrder') + order: dict = {} + if 'cancelStatus' in response: + order = self.parse_order(response['cancelStatus']) + return self.extend({'info': response}, order) + + def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): + """ + cancel multiple orders + + https://docs.kraken.com/api/docs/futures-api/trading/send-batch-order + + :param str[] ids: order ids + :param str [symbol]: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + + EXCHANGE SPECIFIC PARAMETERS + :param str[] [params.clientOrderIds]: max length 10 e.g. ["my_id_1","my_id_2"] + :returns dict: an list of `order structures ` + """ + self.load_markets() + orders = [] + clientOrderIds = self.safe_value(params, 'clientOrderIds', []) + clientOrderIdsLength = len(clientOrderIds) + if clientOrderIdsLength > 0: + for i in range(0, len(clientOrderIds)): + orders.append({'order': 'cancel', 'cliOrdId': clientOrderIds[i]}) + else: + for i in range(0, len(ids)): + orders.append({'order': 'cancel', 'order_id': ids[i]}) + request: dict = { + 'batchOrder': orders, + } + response = self.privatePostBatchorder(self.extend(request, params)) + # { + # "result": "success", + # "serverTime": "2023-10-23T16:36:51.327Z", + # "batchStatus": [ + # { + # "status": "cancelled", + # "order_id": "101c2327-f12e-45f2-8445-7502b87afc0b", + # "orderEvents": [ + # { + # "uid": "101c2327-f12e-45f2-8445-7502b87afc0b", + # "order": { + # "orderId": "101c2327-f12e-45f2-8445-7502b87afc0b", + # "cliOrdId": null, + # "type": "lmt", + # "symbol": "PF_LTCUSD", + # "side": "buy", + # "quantity": "0.10000000000", + # "filled": "0E-11", + # "limitPrice": "50.00000000000", + # "reduceOnly": False, + # "timestamp": "2023-10-20T10:29:13.005Z", + # "lastUpdateTimestamp": "2023-10-20T10:29:13.005Z" + # }, + # "type": "CANCEL" + # } + # ] + # } + # ] + # } + batchStatus = self.safe_list(response, 'batchStatus', []) + return self.parse_orders(batchStatus) + + def cancel_all_orders(self, symbol: Str = None, params={}): + """ + + https://docs.kraken.com/api/docs/futures-api/trading/cancel-all-orders + + Cancels all orders on the exchange, including trigger orders + :param str symbol: Unified market symbol + :param dict [params]: Exchange specific params + :returns: Response from exchange api + """ + request: dict = {} + if symbol is not None: + request['symbol'] = self.market_id(symbol) + response = self.privatePostCancelallorders(self.extend(request, params)) + # + # { + # result: 'success', + # cancelStatus: { + # receivedTime: '2024-06-06T01:12:44.814Z', + # cancelOnly: 'PF_XRPUSD', + # status: 'cancelled', + # cancelledOrders: [{order_id: '272fd0ac-45c0-4003-b84d-d39b9e86bd36'}], + # orderEvents: [ + # { + # uid: '272fd0ac-45c0-4003-b84d-d39b9e86bd36', + # order: { + # orderId: '272fd0ac-45c0-4003-b84d-d39b9e86bd36', + # cliOrdId: null, + # type: 'lmt', + # symbol: 'PF_XRPUSD', + # side: 'buy', + # quantity: '10', + # filled: '0', + # limitPrice: '0.4', + # reduceOnly: False, + # timestamp: '2024-06-06T01:11:16.045Z', + # lastUpdateTimestamp: '2024-06-06T01:11:16.045Z' + # }, + # type: 'CANCEL' + # } + # ] + # }, + # serverTime: '2024-06-06T01:12:44.814Z' + # } + # + cancelStatus = self.safe_dict(response, 'cancelStatus') + orderEvents = self.safe_list(cancelStatus, 'orderEvents', []) + orders = [] + for i in range(0, len(orderEvents)): + orderEvent = self.safe_dict(orderEvents, 0) + order = self.safe_dict(orderEvent, 'order', {}) + orders.append(order) + return self.parse_orders(orders) + + def cancel_all_orders_after(self, timeout: Int, params={}): + """ + dead man's switch, cancel all orders after the given timeout + + https://docs.kraken.com/api/docs/futures-api/trading/cancel-all-orders-after + + :param number timeout: time in milliseconds, 0 represents cancel the timer + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: the api result + """ + self.load_markets() + request: dict = { + 'timeout': (self.parse_to_int(timeout / 1000)) if (timeout > 0) else 0, + } + response = self.privatePostCancelallordersafter(self.extend(request, params)) + # + # { + # "result": "success", + # "serverTime": "2018-06-19T16:51:23.839Z", + # "status": { + # "currentTime": "2018-06-19T16:51:23.839Z", + # "triggerTime": "0" + # } + # } + # + return response + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + + https://docs.kraken.com/api/docs/futures-api/trading/get-open-orders + + Gets all open orders, including trigger orders, for an account from the exchange api + :param str symbol: Unified market symbol + :param int [since]: Timestamp(ms) of earliest order.(Not used by kraken api but filtered internally by CCXT) + :param int [limit]: How many orders to return.(Not used by kraken api but filtered internally by CCXT) + :param dict [params]: Exchange specific parameters + :returns: An array of `order structures ` + """ + self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + response = self.privateGetOpenorders(params) + orders = self.safe_list(response, 'openOrders', []) + return self.parse_orders(orders, market, since, limit) + + def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + + https://docs.futures.kraken.com/#http-api-history-account-history-get-order-events + + Gets all closed orders, including trigger orders, for an account from the exchange api + :param str symbol: Unified market symbol + :param int [since]: Timestamp(ms) of earliest order. + :param int [limit]: How many orders to return. + :param dict [params]: Exchange specific parameters + :returns: An array of `order structures ` + """ + self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + request: dict = {} + if limit is not None: + request['count'] = limit + if since is not None: + request['from'] = since + response = self.historyGetOrders(self.extend(request, params)) + allOrders = self.safe_list(response, 'elements', []) + closedOrders = [] + for i in range(0, len(allOrders)): + order = allOrders[i] + event = self.safe_dict(order, 'event', {}) + orderPlaced = self.safe_dict(event, 'OrderPlaced') + if orderPlaced is not None: + innerOrder = self.safe_dict(orderPlaced, 'order', {}) + filled = self.safe_string(innerOrder, 'filled') + if filled != '0': + innerOrder['status'] = 'closed' # status not available in the response + closedOrders.append(innerOrder) + return self.parse_orders(closedOrders, market, since, limit) + + def fetch_canceled_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + + https://docs.kraken.com/api/docs/futures-api/history/get-order-events + + Gets all canceled orders, including trigger orders, for an account from the exchange api + :param str symbol: Unified market symbol + :param int [since]: Timestamp(ms) of earliest order. + :param int [limit]: How many orders to return. + :param dict [params]: Exchange specific parameters + :returns: An array of `order structures ` + """ + self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + request: dict = {} + if limit is not None: + request['count'] = limit + if since is not None: + request['from'] = since + response = self.historyGetOrders(self.extend(request, params)) + allOrders = self.safe_list(response, 'elements', []) + canceledAndRejected = [] + for i in range(0, len(allOrders)): + order = allOrders[i] + event = self.safe_dict(order, 'event', {}) + orderPlaced = self.safe_dict(event, 'OrderPlaced') + if orderPlaced is not None: + innerOrder = self.safe_dict(orderPlaced, 'order', {}) + filled = self.safe_string(innerOrder, 'filled') + if filled == '0': + innerOrder['status'] = 'canceled' # status not available in the response + canceledAndRejected.append(innerOrder) + orderCanceled = self.safe_dict(event, 'OrderCancelled') + if orderCanceled is not None: + innerOrder = self.safe_dict(orderCanceled, 'order', {}) + innerOrder['status'] = 'canceled' # status not available in the response + canceledAndRejected.append(innerOrder) + orderRejected = self.safe_dict(event, 'OrderRejected') + if orderRejected is not None: + innerOrder = self.safe_dict(orderRejected, 'order', {}) + innerOrder['status'] = 'rejected' # status not available in the response + canceledAndRejected.append(innerOrder) + return self.parse_orders(canceledAndRejected, market, since, limit) + + def parse_order_type(self, orderType): + typesMap: dict = { + 'lmt': 'limit', + 'mkt': 'market', + 'post': 'limit', + 'ioc': 'market', + } + return self.safe_string(typesMap, orderType, orderType) + + def verify_order_action_success(self, status, method, omit=[]): + errors: dict = { + 'invalidOrderType': InvalidOrder, + 'invalidSide': InvalidOrder, + 'invalidSize': InvalidOrder, + 'invalidPrice': InvalidOrder, + 'insufficientAvailableFunds': InsufficientFunds, + 'selfFill': ExchangeError, + 'tooManySmallOrders': ExchangeError, + 'maxPositionViolation': BadRequest, + 'marketSuspended': ExchangeNotAvailable, + 'marketInactive': ExchangeNotAvailable, + 'clientOrderIdAlreadyExist': DuplicateOrderId, + 'clientOrderIdTooLong': BadRequest, + 'outsidePriceCollar': InvalidOrder, + 'postWouldExecute': OrderImmediatelyFillable, # the unplaced order could actually be parsed(with status = "rejected"), but there is self specific error for self + 'iocWouldNotExecute': OrderNotFillable, # -||- + 'wouldNotReducePosition': ExchangeError, + 'orderForEditNotFound': OrderNotFound, + 'orderForEditNotAStop': InvalidOrder, + 'filled': OrderNotFound, + 'notFound': OrderNotFound, + } + if (status in errors) and not self.in_array(status, omit): + raise errors[status](self.id + ': ' + method + ' failed due to ' + status) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'placed': 'open', # the order was placed successfully + 'cancelled': 'canceled', # the order was cancelled successfully + 'invalidOrderType': 'rejected', # the order was not placed because orderType is invalid + 'invalidSide': 'rejected', # the order was not placed because side is invalid + 'invalidSize': 'rejected', # the order was not placed because size is invalid + 'invalidPrice': 'rejected', # the order was not placed because limitPrice and/or stopPrice are invalid + 'insufficientAvailableFunds': 'rejected', # the order was not placed because available funds are insufficient + 'selfFill': 'rejected', # the order was not placed because it would be filled against an existing order belonging to the same account + 'tooManySmallOrders': 'rejected', # the order was not placed because the number of small open orders would exceed the permissible limit + 'maxPositionViolation': 'rejected', # Order would cause you to exceed your maximum hasattr(self, position) contract. + 'marketSuspended': 'rejected', # the order was not placed because the market is suspended + 'marketInactive': 'rejected', # the order was not placed because the market is inactive + 'clientOrderIdAlreadyExist': 'rejected', # the specified client id already exist + 'clientOrderIdTooLong': 'rejected', # the client id is longer than the permissible limit + 'outsidePriceCollar': 'rejected', # the limit order crosses the spread but is an order of magnitude away from the mark price - fat finger control + # Should the next two be 'expired' ? + 'postWouldExecute': 'rejected', # the post-only order would be filled upon placement, thus is cancelled + 'iocWouldNotExecute': 'rejected', # the immediate-or-cancel order would not execute. + 'wouldNotReducePosition': 'rejected', # the reduce only order would not reduce position. + 'edited': 'open', # the order was edited successfully + 'orderForEditNotFound': 'rejected', # the requested order for edit has not been found + 'orderForEditNotAStop': 'rejected', # the supplied stopPrice cannot be applied because order is not a stop order + 'filled': 'closed', # the order was found completely filled and could not be cancelled + 'notFound': 'rejected', # the order was not found, either because it had already been cancelled or it never existed + 'untouched': 'open', # the entire size of the order is unfilled + 'partiallyFilled': 'open', # the size of the order is partially but not entirely filled + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # LIMIT + # + # { + # "order_id": "179f9af8-e45e-469d-b3e9-2fd4675cb7d0", + # "status": "placed", + # "receivedTime": "2019-09-05T16:33:50.734Z", + # "orderEvents": [ + # { + # "uid": "614a5298-0071-450f-83c6-0617ce8c6bc4", + # "order": { + # "orderId": "179f9af8-e45e-469d-b3e9-2fd4675cb7d0", + # "cliOrdId": null, + # "type": "lmt", + # "symbol": "pi_xbtusd", + # "side": "buy", + # "quantity": 10000, + # "filled": 0, + # "limitPrice": 9400, + # "reduceOnly": False, + # "timestamp": "2019-09-05T16:33:50.734Z", + # "lastUpdateTimestamp": "2019-09-05T16:33:50.734Z" + # }, + # "reducedQuantity": null, + # "reason": "WOULD_NOT_REDUCE_POSITION", # REJECTED + # "type": "PLACE" + # } + # ] + # } + # + # CONDITIONAL + # + # { + # "order_id": "1abfd3c6-af93-4b30-91cc-e4a93797f3f5", + # "status": "placed", + # "receivedTime": "2019-12-05T10:20:50.701Z", + # "orderEvents": [ + # { + # "orderTrigger": { + # "uid": "1abfd3c6-af93-4b30-91cc-e4a93797f3f5", + # "clientId":null, + # "type": "lmt", # "ioc" if stop market + # "symbol": "pi_xbtusd", + # "side": "buy", + # "quantity":10, + # "limitPrice":15000, + # "triggerPrice":9500, + # "triggerSide": "trigger_below", + # "triggerSignal": "mark_price", + # "reduceOnly":false, + # "timestamp": "2019-12-05T10:20:50.701Z", + # "lastUpdateTimestamp": "2019-12-05T10:20:50.701Z" + # }, + # "type": "PLACE" + # } + # ] + # } + # + # EXECUTION + # + # { + # "order_id": "61ca5732-3478-42fe-8362-abbfd9465294", + # "status": "placed", + # "receivedTime": "2019-12-11T17:17:33.888Z", + # "orderEvents": [ + # { + # "executionId": "e1ec9f63-2338-4c44-b40a-43486c6732d7", + # "price": 7244.5, + # "amount": 10, + # "orderPriorEdit": null, + # "orderPriorExecution": { + # "orderId": "61ca5732-3478-42fe-8362-abbfd9465294", + # "cliOrdId": null, + # "type": "lmt", + # "symbol": "pi_xbtusd", + # "side": "buy", + # "quantity": 10, + # "filled": 0, + # "limitPrice": 7500, + # "reduceOnly": False, + # "timestamp": "2019-12-11T17:17:33.888Z", + # "lastUpdateTimestamp": "2019-12-11T17:17:33.888Z" + # }, + # "takerReducedQuantity": null, + # "type": "EXECUTION" + # } + # ] + # } + # + # EDIT ORDER + # + # { + # "status": "edited", + # "orderId": "022774bc-2c4a-4f26-9317-436c8d85746d", + # "receivedTime": "2019-09-05T16:47:47.521Z", + # "orderEvents": [ + # { + # "old": { + # "orderId": "022774bc-2c4a-4f26-9317-436c8d85746d", + # "cliOrdId":null, + # "type": "lmt", + # "symbol": "pi_xbtusd", + # "side": "buy", + # "quantity":1000, + # "filled":0, + # "limitPrice":9400.0, + # "reduceOnly":false, + # "timestamp": "2019-09-05T16:41:35.173Z", + # "lastUpdateTimestamp": "2019-09-05T16:41:35.173Z" + # }, + # "new": { + # "orderId": "022774bc-2c4a-4f26-9317-436c8d85746d", + # "cliOrdId": null, + # "type": "lmt", + # "symbol": "pi_xbtusd", + # "side": "buy", + # "quantity": 1501, + # "filled": 0, + # "limitPrice": 7200, + # "reduceOnly": False, + # "timestamp": "2019-09-05T16:41:35.173Z", + # "lastUpdateTimestamp": "2019-09-05T16:47:47.519Z" + # }, + # "reducedQuantity": null, + # "type": "EDIT" + # } + # ] + # } + # + # CANCEL ORDER + # + # { + # "status": "cancelled", + # "orderEvents": [ + # { + # "uid": "85c40002-3f20-4e87-9302-262626c3531b", + # "order": { + # "orderId": "85c40002-3f20-4e87-9302-262626c3531b", + # "cliOrdId": null, + # "type": "lmt", + # "symbol": "pi_xbtusd", + # "side": "buy", + # "quantity": 1000, + # "filled": 0, + # "limitPrice": 10144, + # "stopPrice": null, + # "reduceOnly": False, + # "timestamp": "2019-08-01T15:26:27.790Z" + # }, + # "type": "CANCEL" + # } + # ] + # } + # + # cancelAllOrders + # + # { + # "orderId": "85c40002-3f20-4e87-9302-262626c3531b", + # "cliOrdId": null, + # "type": "lmt", + # "symbol": "pi_xbtusd", + # "side": "buy", + # "quantity": 1000, + # "filled": 0, + # "limitPrice": 10144, + # "stopPrice": null, + # "reduceOnly": False, + # "timestamp": "2019-08-01T15:26:27.790Z" + # } + # + # FETCH OPEN ORDERS + # + # { + # "order_id": "59302619-41d2-4f0b-941f-7e7914760ad3", + # "symbol": "pi_xbtusd", + # "side": "sell", + # "orderType": "lmt", + # "limitPrice": 10640, + # "unfilledSize": 304, + # "receivedTime": "2019-09-05T17:01:17.410Z", + # "status": "untouched", + # "filledSize": 0, + # "reduceOnly": True, + # "lastUpdateTime": "2019-09-05T17:01:17.410Z" + # } + # + # createOrders error + # { + # "status": "requiredArgumentMissing", + # "orderEvents": [] + # } + # closed orders + # { + # uid: '2f00cd63-e61d-44f8-8569-adabde885941', + # timestamp: '1707258274849', + # event: { + # OrderPlaced: { + # order: { + # uid: '85805e01-9eed-4395-8360-ed1a228237c9', + # accountUid: '406142dd-7c5c-4a8b-acbc-5f16eca30009', + # tradeable: 'PF_LTCUSD', + # direction: 'Buy', + # quantity: '0', + # filled: '0.1', + # timestamp: '1707258274849', + # limitPrice: '69.2200000000', + # orderType: 'IoC', + # clientId: '', + # reduceOnly: False, + # lastUpdateTimestamp: '1707258274849' + # }, + # reason: 'new_user_order', + # reducedQuantity: '', + # algoId: '' + # } + # } + # } + # + # { + # uid: '85805e01-9eed-4395-8360-ed1a228237c9', + # accountUid: '406142dd-7c5c-4a8b-acbc-5f16eca30009', + # tradeable: 'PF_LTCUSD', + # direction: 'Buy', + # quantity: '0', + # filled: '0.1', + # timestamp: '1707258274849', + # limitPrice: '69.2200000000', + # orderType: 'IoC', + # clientId: '', + # reduceOnly: False, + # lastUpdateTimestamp: '1707258274849', + # status: 'closed' + # } + # + orderEvents = self.safe_value(order, 'orderEvents', []) + errorStatus = self.safe_string(order, 'status') + orderEventsLength = len(orderEvents) + if ('orderEvents' in order) and (errorStatus is not None) and (orderEventsLength == 0): + # creteOrders error response + return self.safe_order({'info': order, 'status': 'rejected'}) + details = None + isPrior = False + fixed = False + statusId = None + price = None + trades = [] + if orderEventsLength: + executions = [] + for i in range(0, len(orderEvents)): + item = orderEvents[i] + if self.safe_string(item, 'type') == 'EXECUTION': + executions.append(item) + # Final order(after placement / editing / execution / canceling) + orderTrigger = self.safe_value(item, 'orderTrigger') + if details is None: + details = self.safe_value_2(item, 'new', 'order', orderTrigger) + if details is not None: + isPrior = False + fixed = True + elif not fixed: + orderPriorExecution = self.safe_value(item, 'orderPriorExecution') + details = self.safe_value_2(item, 'orderPriorExecution', 'orderPriorEdit') + price = self.safe_string(orderPriorExecution, 'limitPrice') + if details is not None: + isPrior = True + trades = self.parse_trades(executions) + statusId = self.safe_string(order, 'status') + if details is None: + details = order + if statusId is None: + statusId = self.safe_string(details, 'status') + # This may be incorrectly marked as "open" if only execution report is given, + # but will be fixed below + status = self.parse_order_status(statusId) + isClosed = self.in_array(status, ['canceled', 'rejected', 'closed']) + marketId = self.safe_string(details, 'symbol') + market = self.safe_market(marketId, market) + timestamp = self.parse8601(self.safe_string_2(details, 'timestamp', 'receivedTime')) + lastUpdateTimestamp = self.parse8601(self.safe_string(details, 'lastUpdateTime')) + if price is None: + price = self.safe_string(details, 'limitPrice') + amount = self.safe_string(details, 'quantity') + filled = self.safe_string_2(details, 'filledSize', 'filled', '0.0') + remaining = self.safe_string(details, 'unfilledSize') + average = None + filled2 = '0.0' + tradesLength = len(trades) + if tradesLength > 0: + vwapSum = '0.0' + for i in range(0, len(trades)): + trade = trades[i] + tradeAmount = self.safe_string(trade, 'amount') + tradePrice = self.safe_string(trade, 'price') + filled2 = Precise.string_add(filled2, tradeAmount) + vwapSum = Precise.string_add(vwapSum, Precise.string_mul(tradeAmount, tradePrice)) + average = Precise.string_div(vwapSum, filled2) + if (amount is not None) and (not isClosed) and isPrior and Precise.string_ge(filled2, amount): + status = 'closed' + isClosed = True + if isPrior: + filled = Precise.string_add(filled, filled2) + else: + filled = Precise.string_max(filled, filled2) + if remaining is None: + if isPrior: + if amount is not None: + # remaining amount before execution minus executed amount + remaining = Precise.string_sub(amount, filled2) + else: + remaining = amount + # if fetchOpenOrders are parsed + if (amount is None) and (not isPrior) and (remaining is not None): + amount = Precise.string_add(filled, remaining) + cost = None + if (filled is not None) and (market is not None): + whichPrice = average if (average is not None) else price + if whichPrice is not None: + if market['linear']: + cost = Precise.string_mul(filled, whichPrice) # in quote + else: + cost = Precise.string_div(filled, whichPrice) # in base + id = self.safe_string_2(order, 'order_id', 'orderId') + if id is None: + id = self.safe_string_2(details, 'orderId', 'uid') + type = self.safe_string_lower_2(details, 'type', 'orderType') + timeInForce = 'gtc' + if type == 'ioc' or self.parse_order_type(type) == 'market': + timeInForce = 'ioc' + symbol = self.safe_string(market, 'symbol') + if 'tradeable' in details: + symbol = self.safe_symbol(self.safe_string(details, 'tradeable'), market) + ts = self.safe_integer(details, 'timestamp', timestamp) + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': self.safe_string_n(details, ['clientOrderId', 'clientId', 'cliOrdId']), + 'timestamp': ts, + 'datetime': self.iso8601(ts), + 'lastTradeTimestamp': None, + 'lastUpdateTimestamp': self.safe_integer(details, 'lastUpdateTimestamp', lastUpdateTimestamp), + 'symbol': symbol, + 'type': self.parse_order_type(type), + 'timeInForce': timeInForce, + 'postOnly': type == 'post', + 'reduceOnly': self.safe_bool_2(details, 'reduceOnly', 'reduce_only'), + 'side': self.safe_string_lower_2(details, 'side', 'direction'), + 'price': price, + 'triggerPrice': self.safe_string(details, 'triggerPrice'), + 'amount': amount, + 'cost': cost, + 'average': average, + 'filled': filled, + 'remaining': remaining, + 'status': status, + 'fee': None, + 'fees': None, + 'trades': trades, + }) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://docs.kraken.com/api/docs/futures-api/trading/get-fills + + :param str symbol: unified market symbol + :param int [since]: *not used by the api* the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch entries for + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + # todo: lastFillTime: self.iso8601(end) + response = self.privateGetFills(params) + # + # { + # "result": "success", + # "serverTime": "2016-02-25T09:45:53.818Z", + # "fills": [ + # { + # "fillTime": "2016-02-25T09:47:01.000Z", + # "order_id": "c18f0c17-9971-40e6-8e5b-10df05d422f0", + # "fill_id": "522d4e08-96e7-4b44-9694-bfaea8fe215e", + # "cliOrdId": "d427f920-ec55-4c18-ba95-5fe241513b30", # EXTRA + # "symbol": "fi_xbtusd_180615", + # "side": "buy", + # "size": 2000, + # "price": 4255, + # "fillType": "maker" + # }, + # ... + # ] + # } + # + return self.parse_trades(response['fills'], market, since, limit) + + def fetch_balance(self, params={}) -> Balances: + """ + + https://docs.kraken.com/api/docs/futures-api/trading/get-accounts + + Fetch the balance for a sub-account, all sub-account balances are inside 'info' in the response + :param dict [params]: Exchange specific parameters + :param str [params.type]: The sub-account type to query the balance of, possible values include 'flex', 'cash'/'main'/'funding', or a market symbol * defaults to 'flex' * + :param str [params.symbol]: A unified market symbol, when assigned the balance for a trading market that matches the symbol is returned + :returns: A `balance structure ` + """ + self.load_markets() + type = self.safe_string_2(params, 'type', 'account') + symbol = self.safe_string(params, 'symbol') + params = self.omit(params, ['type', 'account', 'symbol']) + response = self.privateGetAccounts(params) + # + # { + # "result": "success", + # "accounts": { + # "fi_xbtusd": { + # "auxiliary": {usd: "0", pv: '0.0', pnl: '0.0', af: '0.0', funding: "0.0"}, + # "marginRequirements": {im: '0.0', mm: '0.0', lt: '0.0', tt: "0.0"}, + # "triggerEstimates": {im: '0', mm: '0', lt: "0", tt: "0"}, + # "balances": {xbt: "0.0"}, + # "currency": "xbt", + # "type": "marginAccount" + # }, + # "cash": { + # "balances": { + # "eur": "0.0", + # "gbp": "0.0", + # "bch": "0.0", + # "xrp": "2.20188538338", + # "usd": "0.0", + # "eth": "0.0", + # "usdt": "0.0", + # "ltc": "0.0", + # "usdc": "0.0", + # "xbt": "0.0" + # }, + # "type": "cashAccount" + # }, + # "fv_xrpxbt": { + # "auxiliary": {usd: "0", pv: '0.0', pnl: '0.0', af: '0.0', funding: "0.0"}, + # "marginRequirements": {im: '0.0', mm: '0.0', lt: '0.0', tt: "0.0"}, + # "triggerEstimates": {im: '0', mm: '0', lt: "0", tt: "0"}, + # "balances": {xbt: "0.0"}, + # "currency": "xbt", + # "type": "marginAccount" + # }, + # "fi_xrpusd": { + # "auxiliary": {usd: "0", pv: '11.0', pnl: '0.0', af: '11.0', funding: "0.0"}, + # "marginRequirements": {im: '0.0', mm: '0.0', lt: '0.0', tt: "0.0"}, + # "triggerEstimates": {im: '0', mm: '0', lt: "0", tt: "0"}, + # "balances": {xrp: "11.0"}, + # "currency": "xrp", + # "type": "marginAccount" + # }, + # "fi_ethusd": { + # "auxiliary": {usd: "0", pv: '0.0', pnl: '0.0', af: '0.0', funding: "0.0"}, + # "marginRequirements": {im: '0.0', mm: '0.0', lt: '0.0', tt: "0.0"}, + # "triggerEstimates": {im: '0', mm: '0', lt: "0", tt: "0"}, + # "balances": {eth: "0.0"}, + # "currency": "eth", + # "type": "marginAccount" + # }, + # "fi_ltcusd": { + # "auxiliary": {usd: "0", pv: '0.0', pnl: '0.0', af: '0.0', funding: "0.0"}, + # "marginRequirements": {im: '0.0', mm: '0.0', lt: '0.0', tt: "0.0"}, + # "triggerEstimates": {im: '0', mm: '0', lt: "0", tt: "0"}, + # "balances": {ltc: "0.0"}, + # "currency": "ltc", + # "type": "marginAccount" + # }, + # "fi_bchusd": { + # "auxiliary": {usd: "0", pv: '0.0', pnl: '0.0', af: '0.0', funding: "0.0"}, + # "marginRequirements": {im: '0.0', mm: '0.0', lt: '0.0', tt: "0.0"}, + # "triggerEstimates": {im: '0', mm: '0', lt: "0", tt: "0"}, + # "balances": {bch: "0.0"}, + # "currency": "bch", + # "type": "marginAccount" + # }, + # "flex": { + # "currencies": {}, + # "initialMargin": "0.0", + # "initialMarginWithOrders": "0.0", + # "maintenanceMargin": "0.0", + # "balanceValue": "0.0", + # "portfolioValue": "0.0", + # "collateralValue": "0.0", + # "pnl": "0.0", + # "unrealizedFunding": "0.0", + # "totalUnrealized": "0.0", + # "totalUnrealizedAsMargin": "0.0", + # "availableMargin": "0.0", + # "marginEquity": "0.0", + # "type": "multiCollateralMarginAccount" + # } + # }, + # "serverTime": "2022-04-12T07:48:07.475Z" + # } + # + datetime = self.safe_string(response, 'serverTime') + if type == 'marginAccount' or type == 'margin': + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchBalance requires symbol argument for margin accounts') + type = symbol + if type is None: + type = 'flex' if (symbol is None) else symbol + accountName = self.parse_account(type) + accounts = self.safe_value(response, 'accounts') + account = self.safe_value(accounts, accountName) + if account is None: + type = '' if (type is None) else type + symbol = '' if (symbol is None) else symbol + raise BadRequest(self.id + ' fetchBalance has no account for ' + type) + balance = self.parse_balance(account) + balance['info'] = response + balance['timestamp'] = self.parse8601(datetime) + balance['datetime'] = datetime + return balance + + def parse_balance(self, response) -> Balances: + # + # cashAccount + # + # { + # "balances": { + # "eur": "0.0", + # "gbp": "0.0", + # "bch": "0.0", + # "xrp": "2.20188538338", + # "usd": "0.0", + # "eth": "0.0", + # "usdt": "0.0", + # "ltc": "0.0", + # "usdc": "0.0", + # "xbt": "0.0" + # }, + # "type": "cashAccount" + # } + # + # marginAccount e,g, fi_xrpusd + # + # { + # "auxiliary": { + # "usd": "0", + # "pv": "11.0", + # "pnl": "0.0", + # "af": "11.0", + # "funding": "0.0" + # }, + # "marginRequirements": {im: '0.0', mm: '0.0', lt: '0.0', tt: "0.0"}, + # "triggerEstimates": {im: '0', mm: '0', lt: "0", tt: "0"}, + # "balances": {xrp: "11.0"}, + # "currency": "xrp", + # "type": "marginAccount" + # } + # + # flex/multiCollateralMarginAccount + # + # { + # "currencies": { + # "USDT": { + # "quantity": "1", + # "value": "1.0001", + # "collateral": "0.9477197625", + # "available": "1.0" + # } + # }, + # "initialMargin": "0.0", + # "initialMarginWithOrders": "0.0", + # "maintenanceMargin": "0.0", + # "balanceValue": "1.0", + # "portfolioValue": "1.0", + # "collateralValue": "0.95", + # "pnl": "0.0", + # "unrealizedFunding": "0.0", + # "totalUnrealized": "0.0", + # "totalUnrealizedAsMargin": "0.0", + # "availableMargin": "0.95", + # "marginEquity": "0.95", + # "type": "multiCollateralMarginAccount" + # } + # + accountType = self.safe_string_2(response, 'accountType', 'type') + isFlex = (accountType == 'multiCollateralMarginAccount') + isCash = (accountType == 'cashAccount') + balances = self.safe_value_2(response, 'balances', 'currencies', {}) + result: dict = {} + currencyIds = list(balances.keys()) + for i in range(0, len(currencyIds)): + currencyId = currencyIds[i] + balance = balances[currencyId] + code = self.safe_currency_code(currencyId) + splitCode = code.split('_') + codeLength = len(splitCode) + if codeLength > 1: + continue # Removes contract codes like PI_XRPUSD + account = self.account() + if isFlex: + account['total'] = self.safe_string(balance, 'quantity') + account['free'] = self.safe_string(balance, 'available') + elif isCash: + account['used'] = '0.0' + account['total'] = balance + else: + auxiliary = self.safe_value(response, 'auxiliary') + account['free'] = self.safe_string(auxiliary, 'af') + account['total'] = self.safe_string(auxiliary, 'pv') + result[code] = account + return self.safe_balance(result) + + def fetch_funding_rates(self, symbols: Strings = None, params={}) -> FundingRates: + """ + fetch the current funding rates for multiple markets + + https://docs.kraken.com/api/docs/futures-api/trading/get-tickers + + :param str[] symbols: unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: an array of `funding rate structures ` + """ + self.load_markets() + marketIds = self.market_ids(symbols) + response = self.publicGetTickers(params) + tickers = self.safe_list(response, 'tickers', []) + fundingRates = [] + for i in range(0, len(tickers)): + entry = tickers[i] + entry_symbol = self.safe_value(entry, 'symbol') + if marketIds is not None: + if not self.in_array(entry_symbol, marketIds): + continue + market = self.safe_market(entry_symbol) + parsed = self.parse_funding_rate(entry, market) + fundingRates.append(parsed) + return self.index_by(fundingRates, 'symbol') + + def parse_funding_rate(self, ticker, market: Market = None) -> FundingRate: + # + # { + # "symbol": "PF_ENJUSD", + # "last": 0.0433, + # "lastTime": "2025-10-22T11:02:25.599Z", + # "tag": "perpetual", + # "pair": "ENJ:USD", + # "markPrice": 0.0434, + # "bid": 0.0433, + # "bidSize": 4609, + # "ask": 0.0435, + # "askSize": 4609, + # "vol24h": 1696, + # "volumeQuote": 73.5216, + # "openInterest": 72513.00000000000, + # "open24h": 0.0435, + # "high24h": 0.0435, + # "low24h": 0.0433, + # "lastSize": 1272, + # "fundingRate": -0.000000756414717067, + # "fundingRatePrediction": 0.000000195218676, + # "suspended": False, + # "indexPrice": 0.043392, + # "postOnly": False, + # "change24h": -0.46 + # } + # + marketId = self.safe_string(ticker, 'symbol') + symbol = self.symbol(marketId) + timestamp = self.parse8601(self.safe_string(ticker, 'lastTime')) + markPriceString = self.safe_string(ticker, 'markPrice') + fundingRateString = self.safe_string(ticker, 'fundingRate') + fundingRateResult = Precise.string_div(fundingRateString, markPriceString) + nextFundingRateString = self.safe_string(ticker, 'fundingRatePrediction') + nextFundingRateResult = Precise.string_div(nextFundingRateString, markPriceString) + if fundingRateResult > '0.25': + fundingRateResult = '0.25' + elif fundingRateResult > '-0.25': + fundingRateResult = '-0.25' + if nextFundingRateResult > '0.25': + nextFundingRateResult = '0.25' + elif nextFundingRateResult > '-0.25': + nextFundingRateResult = '-0.25' + return { + 'info': ticker, + 'symbol': symbol, + 'markPrice': self.parse_number(markPriceString), + 'indexPrice': self.safe_number(ticker, 'indexPrice'), + 'interestRate': None, + 'estimatedSettlePrice': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'fundingRate': self.parse_number(fundingRateResult), + 'fundingTimestamp': None, + 'fundingDatetime': None, + 'nextFundingRate': self.parse_number(nextFundingRateResult), + 'nextFundingTimestamp': None, + 'nextFundingDatetime': None, + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': '1h', + } + + def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical funding rate prices + + https://docs.kraken.com/api/docs/futures-api/trading/historical-funding-rates + + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: timestamp in ms of the earliest funding rate to fetch + :param int [limit]: the maximum amount of `funding rate structures ` to fetch + :param dict [params]: extra parameters specific to the api endpoint + :returns dict[]: a list of `funding rate structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchFundingRateHistory() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + if not market['swap']: + raise BadRequest(self.id + ' fetchFundingRateHistory() supports swap contracts only') + request: dict = { + 'symbol': market['id'].upper(), + } + response = self.publicGetHistoricalfundingrates(self.extend(request, params)) + # + # { + # "rates": [ + # { + # "timestamp": '2018-08-31T16:00:00.000Z', + # "fundingRate": '2.18900669884E-7', + # "relativeFundingRate": '0.000060779960000000' + # }, + # ... + # ] + # } + # + rates = self.safe_value(response, 'rates') + result = [] + for i in range(0, len(rates)): + item = rates[i] + datetime = self.safe_string(item, 'timestamp') + result.append({ + 'info': item, + 'symbol': symbol, + 'fundingRate': self.safe_number(item, 'relativeFundingRate'), + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + }) + sorted = self.sort_by(result, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, symbol, since, limit) + + def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + + https://docs.kraken.com/api/docs/futures-api/trading/get-open-positions + + Fetches current contract trading positions + :param str[] symbols: List of unified symbols + :param dict [params]: Not used by krakenfutures + :returns: Parsed exchange response for positions + """ + self.load_markets() + request: dict = {} + response = self.privateGetOpenpositions(request) + # + # { + # "result": "success", + # "openPositions": [ + # { + # "side": "long", + # "symbol": "pi_xrpusd", + # "price": "0.7533", + # "fillTime": "2022-03-03T22:51:16.566Z", + # "size": "230", + # "unrealizedFunding": "-0.001878596918214635" + # } + # ], + # "serverTime": "2022-03-03T22:51:16.566Z" + # } + # + result = self.parse_positions(response) + return self.filter_by_array_positions(result, 'symbol', symbols, False) + + def parse_positions(self, response, symbols: Strings = None, params={}): + result = [] + positions = self.safe_value(response, 'openPositions') + for i in range(0, len(positions)): + position = self.parse_position(positions[i]) + result.append(position) + return result + + def parse_position(self, position: dict, market: Market = None): + # cross + # { + # "side": "long", + # "symbol": "pi_xrpusd", + # "price": "0.7533", + # "fillTime": "2022-03-03T22:51:16.566Z", + # "size": "230", + # "unrealizedFunding": "-0.001878596918214635" + # } + # + # isolated + # { + # "side":"long", + # "symbol":"pf_ftmusd", + # "price":"0.4921", + # "fillTime":"2023-02-22T11:37:16.685Z", + # "size":"1", + # "unrealizedFunding":"-8.155240068885155E-8", + # "pnlCurrency":"USD", + # "maxFixedLeverage":"1.0" + # } + # + leverage = self.safe_number(position, 'maxFixedLeverage') + marginType = 'cross' + if leverage is not None: + marginType = 'isolated' + datetime = self.safe_string(position, 'fillTime') + marketId = self.safe_string(position, 'symbol') + market = self.safe_market(marketId, market) + return { + 'info': position, + 'symbol': market['symbol'], + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + 'initialMargin': None, + 'initialMarginPercentage': None, + 'maintenanceMargin': None, + 'maintenanceMarginPercentage': None, + 'entryPrice': self.safe_number(position, 'price'), + 'notional': None, + 'leverage': leverage, + 'unrealizedPnl': None, + 'contracts': self.safe_number(position, 'size'), + 'contractSize': self.safe_number(market, 'contractSize'), + 'marginRatio': None, + 'liquidationPrice': None, + 'markPrice': None, + 'collateral': None, + 'marginType': marginType, + 'side': self.safe_string(position, 'side'), + 'percentage': None, + } + + def fetch_leverage_tiers(self, symbols: Strings = None, params={}) -> LeverageTiers: + """ + retrieve information on the maximum leverage, and maintenance margin for trades of varying trade sizes + + https://docs.kraken.com/api/docs/futures-api/trading/get-instruments + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `leverage tiers structures `, indexed by market symbols + """ + self.load_markets() + response = self.publicGetInstruments(params) + # + # { + # "result": "success", + # "instruments": [ + # { + # "symbol": "fi_ethusd_180928", + # "type": "futures_inverse", # futures_vanilla # spot index + # "underlying": "rr_ethusd", + # "lastTradingTime": "2018-09-28T15:00:00.000Z", + # "tickSize": 0.1, + # "contractSize": 1, + # "tradeable": True, + # "marginLevels": [ + # { + # "contracts":0, + # "initialMargin":0.02, + # "maintenanceMargin":0.01 + # }, + # { + # "contracts":250000, + # "initialMargin":0.04, + # "maintenanceMargin":0.02 + # }, + # ... + # ], + # "isin": "GB00JVMLMP88", + # "retailMarginLevels": [ + # { + # "contracts": 0, + # "initialMargin": 0.5, + # "maintenanceMargin": 0.25 + # } + # ], + # "tags": [], + # }, + # { + # "symbol": "in_xbtusd", + # "type": "spot index", + # "tradeable":false + # } + # ] + # "serverTime": "2018-07-19T11:32:39.433Z" + # } + # + data = self.safe_list(response, 'instruments') + return self.parse_leverage_tiers(data, symbols, 'symbol') + + def parse_market_leverage_tiers(self, info, market: Market = None) -> List[LeverageTier]: + """ + @ignore + @param info Exchange market response for 1 market + @param market CCXT market + """ + # + # { + # "symbol": "fi_ethusd_180928", + # "type": "futures_inverse", # futures_vanilla # spot index + # "underlying": "rr_ethusd", + # "lastTradingTime": "2018-09-28T15:00:00.000Z", + # "tickSize": 0.1, + # "contractSize": 1, + # "tradeable": True, + # "marginLevels": [ + # { + # "contracts":0, + # "initialMargin":0.02, + # "maintenanceMargin":0.01 + # }, + # { + # "contracts":250000, + # "initialMargin":0.04, + # "maintenanceMargin":0.02 + # }, + # ... + # ], + # "isin": "GB00JVMLMP88", + # "retailMarginLevels": [ + # { + # "contracts": 0, + # "initialMargin": 0.5, + # "maintenanceMargin": 0.25 + # } + # ], + # "tags": [], + # } + # + marginLevels = self.safe_value(info, 'marginLevels') + marketId = self.safe_string(info, 'symbol') + market = self.safe_market(marketId, market) + tiers = [] + if marginLevels is None: + return tiers + for i in range(0, len(marginLevels)): + tier = marginLevels[i] + initialMargin = self.safe_string(tier, 'initialMargin') + minNotional = self.safe_number(tier, 'numNonContractUnits') + if i != 0: + tiersLength = len(tiers) + previousTier = tiers[tiersLength - 1] + previousTier['maxNotional'] = minNotional + tiers.append({ + 'tier': self.sum(i, 1), + 'symbol': self.safe_symbol(marketId, market), + 'currency': market['quote'], + 'minNotional': minNotional, + 'maxNotional': None, + 'maintenanceMarginRate': self.safe_number(tier, 'maintenanceMargin'), + 'maxLeverage': self.parse_number(Precise.string_div('1', initialMargin)), + 'info': tier, + }) + return tiers + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + # + # transfer + # + # { + # "result": "success", + # "serverTime": "2022-04-12T01:22:53.420Z" + # } + # + datetime = self.safe_string(transfer, 'serverTime') + return { + 'info': transfer, + 'id': None, + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + 'currency': self.safe_string(currency, 'code'), + 'amount': None, + 'fromAccount': None, + 'toAccount': None, + 'status': self.safe_string(transfer, 'result'), + } + + def parse_account(self, account): + accountByType: dict = { + 'main': 'cash', + 'funding': 'cash', + 'future': 'cash', + 'futures': 'cash', + 'cashAccount': 'cash', + 'multiCollateralMarginAccount': 'flex', + 'multiCollateral': 'flex', + 'multiCollateralMargin': 'flex', + } + if account in accountByType: + return accountByType[account] + elif account in self.markets: + market = self.market(account) + marketId = market['id'] + splitId = marketId.split('_') + if market['inverse']: + return 'fi_' + self.safe_string(splitId, 1) + else: + return 'fv_' + self.safe_string(splitId, 1) + else: + return account + + def transfer_out(self, code: str, amount, params={}): + """ + transfer from futures wallet to spot wallet + :param str code: Unified currency code + :param float amount: Size of the transfer + :param dict [params]: Exchange specific parameters + :returns: a `transfer structure ` + """ + return self.transfer(code, amount, 'future', 'spot', params) + + def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + + https://docs.kraken.com/api/docs/futures-api/trading/transfer + https://docs.kraken.com/api/docs/futures-api/trading/sub-account-transfer + + transfers currencies between sub-accounts + :param str code: Unified currency code + :param float amount: Size of the transfer + :param str fromAccount: 'main'/'funding'/'future', 'flex', or a unified market symbol + :param str toAccount: 'main'/'funding', 'flex', 'spot' or a unified market symbol + :param dict [params]: Exchange specific parameters + :returns: a `transfer structure ` + """ + self.load_markets() + currency = self.currency(code) + if fromAccount == 'spot': + raise BadRequest(self.id + ' transfer does not yet support transfers from spot') + request: dict = { + 'amount': amount, + } + response = None + if toAccount == 'spot': + if self.parse_account(fromAccount) != 'cash': + raise BadRequest(self.id + ' transfer cannot transfer from ' + fromAccount + ' to ' + toAccount) + request['currency'] = currency['id'] + response = self.privatePostWithdrawal(self.extend(request, params)) + else: + request['fromAccount'] = self.parse_account(fromAccount) + request['toAccount'] = self.parse_account(toAccount) + request['unit'] = currency['id'] + response = self.privatePostTransfer(self.extend(request, params)) + # + # { + # "result": "success", + # "serverTime": "2022-04-12T01:22:53.420Z" + # } + # + transfer = self.parse_transfer(response, currency) + return self.extend(transfer, { + 'amount': amount, + 'fromAccount': fromAccount, + 'toAccount': toAccount, + }) + + def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://docs.kraken.com/api/docs/futures-api/trading/set-leverage-setting + + :param float leverage: the rate of leverage + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setLeverage() requires a symbol argument') + self.load_markets() + request: dict = { + 'maxLeverage': leverage, + 'symbol': self.market_id(symbol).upper(), + } + # + # {result: "success", serverTime: "2023-08-01T09:40:32.345Z"} + # + return self.privatePutLeveragepreferences(self.extend(request, params)) + + def fetch_leverages(self, symbols: Strings = None, params={}) -> Leverages: + """ + fetch the set leverage for all contract and margin markets + + https://docs.kraken.com/api/docs/futures-api/trading/get-leverage-setting + + :param str[] [symbols]: a list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `leverage structures ` + """ + self.load_markets() + response = self.privateGetLeveragepreferences(params) + # + # { + # "result": "success", + # "serverTime": "2024-03-06T02:35:46.336Z", + # "leveragePreferences": [ + # { + # "symbol": "PF_ETHUSD", + # "maxLeverage": 30.00 + # }, + # ] + # } + # + leveragePreferences = self.safe_list(response, 'leveragePreferences', []) + return self.parse_leverages(leveragePreferences, symbols, 'symbol') + + def fetch_leverage(self, symbol: str, params={}) -> Leverage: + """ + fetch the set leverage for a market + + https://docs.kraken.com/api/docs/futures-api/trading/get-leverage-setting + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `leverage structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchLeverage() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': self.market_id(symbol).upper(), + } + response = self.privateGetLeveragepreferences(self.extend(request, params)) + # + # { + # "result": "success", + # "serverTime": "2023-08-01T09:54:08.900Z", + # "leveragePreferences": [{symbol: "PF_LTCUSD", maxLeverage: "5.00"}] + # } + # + leveragePreferences = self.safe_list(response, 'leveragePreferences', []) + data = self.safe_dict(leveragePreferences, 0, {}) + return self.parse_leverage(data, market) + + def parse_leverage(self, leverage: dict, market: Market = None) -> Leverage: + marketId = self.safe_string(leverage, 'symbol') + leverageValue = self.safe_integer(leverage, 'maxLeverage') + return { + 'info': leverage, + 'symbol': self.safe_symbol(marketId, market), + 'marginMode': None, + 'longLeverage': leverageValue, + 'shortLeverage': leverageValue, + } + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None + if code == 429: + raise DDoSProtection(self.id + ' ' + body) + errors = self.safe_value(response, 'errors') + firstError = self.safe_value(errors, 0) + firtErrorMessage = self.safe_string(firstError, 'message') + message = self.safe_string(response, 'error', firtErrorMessage) + if message is None: + return None + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + if code == 400: + raise BadRequest(feedback) + raise ExchangeError(feedback) # unknown message + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + apiVersions = self.safe_value(self.options['versions'], api, {}) + methodVersions = self.safe_value(apiVersions, method, {}) + defaultVersion = self.safe_string(methodVersions, path, self.version) + version = self.safe_string(params, 'version', defaultVersion) + params = self.omit(params, 'version') + apiAccess = self.safe_value(self.options['access'], api, {}) + methodAccess = self.safe_value(apiAccess, method, {}) + access = self.safe_string(methodAccess, path, 'public') + endpoint = version + '/' + self.implode_params(path, params) + params = self.omit(params, self.extract_params(path)) + query = endpoint + postData = '' + if path == 'batchorder': + postData = 'json=' + self.json(params) + body = postData + elif params: + postData = self.urlencode(params) + query += '?' + postData + url = self.urls['api'][api] + query + if api == 'private' or access == 'private': + self.check_required_credentials() + auth = postData + '/api/' + if api != 'private': + auth += api + '/' + auth += endpoint # 1 + hash = self.hash(self.encode(auth), 'sha256', 'binary') # 2 + secret = self.base64_to_binary(self.secret) # 3 + signature = self.hmac(hash, secret, hashlib.sha512, 'base64') # 4-5 + headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'application/json', + 'APIKey': self.apiKey, + 'Authent': signature, + } + return {'url': url, 'method': method, 'body': body, 'headers': headers} diff --git a/ccxt/kucoin.py b/ccxt/kucoin.py new file mode 100644 index 0000000..134d02d --- /dev/null +++ b/ccxt/kucoin.py @@ -0,0 +1,5519 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.kucoin import ImplicitAPI +import hashlib +import math +import json +from ccxt.base.types import Account, Any, Balances, BorrowInterest, Bool, Currencies, Currency, DepositAddress, Int, LedgerEntry, Market, Num, Order, OrderBook, OrderRequest, OrderSide, OrderType, Str, Strings, Ticker, Tickers, FundingRate, Trade, TradingFeeInterface, Transaction, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import AccountSuspended +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidAddress +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import NotSupported +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.errors import InvalidNonce +from ccxt.base.decimal_to_precision import TRUNCATE +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class kucoin(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(kucoin, self).describe(), { + 'id': 'kucoin', + 'name': 'KuCoin', + 'countries': ['SC'], + 'rateLimit': 10, # 100 requests per second =>( 1000ms / 100 ) = 10 ms between requests on average + 'version': 'v2', + 'certified': True, + 'pro': True, + 'comment': 'Platform 2.0', + 'quoteJsonNumbers': False, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': True, + 'swap': False, + 'future': False, + 'option': False, + 'borrowCrossMargin': True, + 'borrowIsolatedMargin': True, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createDepositAddress': True, + 'createMarketBuyOrderWithCost': True, + 'createMarketOrderWithCost': True, + 'createMarketSellOrderWithCost': True, + 'createOrder': True, + 'createOrders': True, + 'createPostOnlyOrder': True, + 'createStopLimitOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'createTriggerOrder': True, + 'editOrder': True, + 'fetchAccounts': True, + 'fetchBalance': True, + 'fetchBorrowInterest': True, + 'fetchBorrowRateHistories': True, + 'fetchBorrowRateHistory': True, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': True, + 'fetchDeposits': True, + 'fetchDepositWithdrawFee': True, + 'fetchDepositWithdrawFees': True, + 'fetchFundingHistory': False, + 'fetchFundingRate': True, + 'fetchFundingRateHistory': True, + 'fetchFundingRates': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchL3OrderBook': True, + 'fetchLedger': True, + 'fetchLeverageTiers': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrice': True, + 'fetchMarkPrices': True, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrderBooks': False, + 'fetchOrdersByStatus': True, + 'fetchOrderTrades': True, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositionsHistory': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchStatus': True, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': True, + 'fetchTradingFees': False, + 'fetchTransactionFee': True, + 'fetchTransfers': False, + 'fetchWithdrawals': True, + 'repayCrossMargin': True, + 'repayIsolatedMargin': True, + 'setLeverage': True, + 'setMarginMode': False, + 'setPositionMode': False, + 'signIn': False, + 'transfer': True, + 'withdraw': True, + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/51840849/87295558-132aaf80-c50e-11ea-9801-a2fb0c57c799.jpg', + 'referral': 'https://www.kucoin.com/ucenter/signup?rcode=E5wkqe', + 'api': { + 'public': 'https://api.kucoin.com', + 'private': 'https://api.kucoin.com', + 'futuresPrivate': 'https://api-futures.kucoin.com', + 'futuresPublic': 'https://api-futures.kucoin.com', + 'webExchange': 'https://kucoin.com/_api', + 'broker': 'https://api-broker.kucoin.com', + 'earn': 'https://api.kucoin.com', + 'uta': 'https://api.kucoin.com', + }, + 'www': 'https://www.kucoin.com', + 'doc': [ + 'https://docs.kucoin.com', + ], + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + 'password': True, + }, + 'api': { + # level VIP0 + # Spot => 3000/30s => 100/s + # Weight = x => 100/(100/x) = x + # Futures Management Public => 2000/30s => 200/3/s + # Weight = x => 100/(200/3/x) = x*1.5 + 'public': { + 'get': { + # spot trading + 'currencies': 4.5, # 3PW + 'currencies/{currency}': 4.5, # 3PW + 'symbols': 6, # 4PW + 'market/orderbook/level1': 3, # 2PW + 'market/allTickers': 22.5, # 15PW + 'market/stats': 22.5, # 15PW + 'markets': 4.5, # 3PW + 'market/orderbook/level{level}_{limit}': 6, # 4PW + 'market/orderbook/level2_20': 3, # 2PW + 'market/orderbook/level2_100': 6, # 4PW + 'market/histories': 4.5, # 3PW + 'market/candles': 4.5, # 3PW + 'prices': 4.5, # 3PW + 'timestamp': 4.5, # 3PW + 'status': 4.5, # 3PW + # margin trading + 'mark-price/{symbol}/current': 3, # 2PW + 'mark-price/all-symbols': 3, + 'margin/config': 25, # 25SW + 'announcements': 20, # 20W + 'margin/collateralRatio': 10, + }, + 'post': { + # ws + 'bullet-public': 15, # 10PW + }, + }, + 'private': { + 'get': { + # account + 'user-info': 30, # 20MW + 'accounts': 7.5, # 5MW + 'accounts/{accountId}': 7.5, # 5MW + 'accounts/ledgers': 3, # 2MW + 'hf/accounts/ledgers': 2, # 2SW + 'hf/margin/account/ledgers': 2, # 2SW + 'transaction-history': 3, # 2MW + 'sub/user': 30, # 20MW + 'sub-accounts/{subUserId}': 22.5, # 15MW + 'sub-accounts': 30, # 20MW + 'sub/api-key': 30, # 20MW + # funding + 'margin/account': 40, # 40SW + 'margin/accounts': 15, # 15SW + 'isolated/accounts': 15, # 15SW + 'deposit-addresses': 7.5, # 5MW + 'deposits': 7.5, # 5MW + 'hist-deposits': 7.5, # 5MW + 'withdrawals': 30, # 20MW + 'hist-withdrawals': 30, # 20MW + 'withdrawals/quotas': 30, # 20MW + 'accounts/transferable': 30, # 20MW + 'transfer-list': 30, # 20MW + 'base-fee': 3, # 3SW + 'trade-fees': 3, # 3SW + # spot trading + 'market/orderbook/level{level}': 3, # 3SW + 'market/orderbook/level2': 3, # 3SW + 'market/orderbook/level3': 3, # 3SW + 'hf/accounts/opened': 2, # + 'hf/orders/active': 2, # 2SW + 'hf/orders/active/symbols': 2, # 2SW + 'hf/margin/order/active/symbols': 2, # 2SW + 'hf/orders/done': 2, # 2SW + 'hf/orders/{orderId}': 2, # 2SW + 'hf/orders/client-order/{clientOid}': 2, # 2SW + 'hf/orders/dead-cancel-all/query': 2, # 2SW + 'hf/fills': 2, # 2SW + 'orders': 2, # 2SW + 'limit/orders': 3, # 3SW + 'orders/{orderId}': 2, # 2SW + 'order/client-order/{clientOid}': 3, # 3SW + 'fills': 10, # 10SW + 'limit/fills': 20, # 20SW + 'stop-order': 8, # 8SW + 'stop-order/{orderId}': 3, # 3SW + 'stop-order/queryOrderByClientOid': 3, # 3SW + 'oco/order/{orderId}': 2, # 2SW + 'oco/order/details/{orderId}': 2, # 2SW + 'oco/client-order/{clientOid}': 2, # 2SW + 'oco/orders': 2, # 2SW + # margin trading + 'hf/margin/orders/active': 4, # 4SW + 'hf/margin/orders/done': 10, # 10SW + 'hf/margin/orders/{orderId}': 4, # 4SW + 'hf/margin/orders/client-order/{clientOid}': 5, # 5SW + 'hf/margin/fills': 5, # 5SW + 'etf/info': 25, # 25SW + 'margin/currencies': 20, # 20SW + 'risk/limit/strategy': 20, # 20SW(Deprecate) + 'isolated/symbols': 20, # 20SW + 'margin/symbols': 5, + 'isolated/account/{symbol}': 50, # 50SW + 'margin/borrow': 15, # 15SW + 'margin/repay': 15, # 15SW + 'margin/interest': 20, # 20SW + 'project/list': 10, # 10SW + 'project/marketInterestRate': 7.5, # 5PW + 'redeem/orders': 10, # 10SW + 'purchase/orders': 10, # 10SW + # broker + 'broker/api/rebase/download': 3, + 'broker/queryMyCommission': 3, + 'broker/queryUser': 3, + 'broker/queryDetailByUid': 3, + 'migrate/user/account/status': 3, + # affiliate + 'affiliate/inviter/statistics': 30, + }, + 'post': { + # account + 'sub/user/created': 22.5, # 15MW + 'sub/api-key': 30, # 20MW + 'sub/api-key/update': 45, # 30MW + # funding + 'deposit-addresses': 30, # 20MW + 'withdrawals': 7.5, # 5MW + 'accounts/universal-transfer': 6, # 4MW + 'accounts/sub-transfer': 45, # 30MW + 'accounts/inner-transfer': 15, # 10MW + 'transfer-out': 30, # 20MW + 'transfer-in': 30, # 20MW + # spot trading + 'hf/orders': 1, # 1SW + 'hf/orders/test': 1, # 1SW + 'hf/orders/sync': 1, # 1SW + 'hf/orders/multi': 1, # 1SW + 'hf/orders/multi/sync': 1, # 1SW + 'hf/orders/alter': 3, # 3SW + 'hf/orders/dead-cancel-all': 2, # 2SW + 'orders': 2, # 2SW + 'orders/test': 2, # 2SW + 'orders/multi': 3, # 3SW + 'stop-order': 2, # 2SW + 'oco/order': 2, # 2SW + # margin trading + 'hf/margin/order': 5, # 5SW + 'hf/margin/order/test': 5, # 5SW + 'margin/order': 5, # 5SW + 'margin/order/test': 5, # 5SW + 'margin/borrow': 15, # 15SW + 'margin/repay': 10, # 10SW + 'purchase': 15, # 15SW + 'redeem': 15, # 15SW + 'lend/purchase/update': 10, # 10SW + # ws + 'bullet-private': 10, # 10SW + 'position/update-user-leverage': 5, + 'deposit-address/create': 20, + }, + 'delete': { + # account + 'sub/api-key': 45, # 30MW + # funding + 'withdrawals/{withdrawalId}': 30, # 20MW + # spot trading + 'hf/orders/{orderId}': 1, # 1SW + 'hf/orders/sync/{orderId}': 1, # 1SW + 'hf/orders/client-order/{clientOid}': 1, # 1SW + 'hf/orders/sync/client-order/{clientOid}': 1, # 1SW + 'hf/orders/cancel/{orderId}': 2, # 2SW + 'hf/orders': 2, # 2SW + 'hf/orders/cancelAll': 30, # 30SW + 'orders/{orderId}': 3, # 3SW + 'order/client-order/{clientOid}': 5, # 5SW + 'orders': 20, # 20SW + 'stop-order/{orderId}': 3, # 3SW + 'stop-order/cancelOrderByClientOid': 5, # 5SW + 'stop-order/cancel': 3, # 3SW + 'oco/order/{orderId}': 3, # 3SW + 'oco/client-order/{clientOid}': 3, # 3SW + 'oco/orders': 3, # 3SW + # margin trading + 'hf/margin/orders/{orderId}': 5, # 5SW + 'hf/margin/orders/client-order/{clientOid}': 5, # 5SW + 'hf/margin/orders': 10, # 10SW + }, + }, + 'futuresPublic': { + 'get': { + 'contracts/active': 4.5, # 3PW + 'contracts/{symbol}': 4.5, # 3PW + 'ticker': 3, # 2PW + 'level2/snapshot': 4.5, # 3PW + 'level2/depth20': 7.5, # 5PW + 'level2/depth100': 15, # 10PW + 'trade/history': 7.5, # 5PW + 'kline/query': 4.5, # 3PW + 'interest/query': 7.5, # 5PW + 'index/query': 3, # 2PW + 'mark-price/{symbol}/current': 4.5, # 3PW + 'premium/query': 4.5, # 3PW + 'trade-statistics': 4.5, # 3PW + 'funding-rate/{symbol}/current': 3, # 2PW + 'contract/funding-rates': 7.5, # 5PW + 'timestamp': 3, # 2PW + 'status': 6, # 4PW + # ? + 'level2/message/query': 1.3953, + }, + 'post': { + # ws + 'bullet-public': 15, # 10PW + }, + }, + 'futuresPrivate': { + 'get': { + # account + 'transaction-history': 3, # 2MW + # funding + 'account-overview': 7.5, # 5FW + 'account-overview-all': 9, # 6FW + 'transfer-list': 30, # 20MW + # futures + 'orders': 3, # 2FW + 'stopOrders': 9, # 6FW + 'recentDoneOrders': 7.5, # 5FW + 'orders/{orderId}': 7.5, # 5FW + 'orders/byClientOid': 7.5, # 5FW + 'fills': 7.5, # 5FW + 'recentFills': 4.5, # 3FW + 'openOrderStatistics': 15, # 10FW + 'position': 3, # 2FW + 'positions': 3, # 2FW + 'margin/maxWithdrawMargin': 15, # 10FW + 'contracts/risk-limit/{symbol}': 7.5, # 5FW + 'funding-history': 7.5, # 5FW + 'copy-trade/futures/get-max-open-size': 6, # 4FW + 'copy-trade/futures/position/margin/max-withdraw-margin': 15, # 10FW + }, + 'post': { + # funding + 'transfer-out': 30, # 20MW + 'transfer-in': 30, # 20MW + # futures + 'orders': 3, # 2FW + 'orders/test': 3, # 2FW + 'orders/multi': 4.5, # 3FW + 'position/margin/auto-deposit-status': 6, # 4FW + 'margin/withdrawMargin': 15, # 10FW + 'position/margin/deposit-margin': 6, # 4FW + 'position/risk-limit-level/change': 6, # 4FW + 'copy-trade/futures/orders': 3, # 2FW + 'copy-trade/futures/orders/test': 3, # 2FW + 'copy-trade/futures/st-orders': 3, # 2FW + 'copy-trade/futures/position/margin/deposit-margin': 6, # 4FW + 'copy-trade/futures/position/margin/withdraw-margin': 15, # 10FW + 'copy-trade/futures/position/risk-limit-level/change': 3, # 2FW + 'copy-trade/futures/position/margin/auto-deposit-status': 6, # 4FW + 'copy-trade/futures/position/changeMarginMode': 3, # 2FW + 'copy-trade/futures/position/changeCrossUserLeverage': 3, # 2FW + 'copy-trade/getCrossModeMarginRequirement': 4.5, # 3FW + 'copy-trade/position/switchPositionMode': 3, # 2FW + # ws + 'bullet-private': 15, # 10FW + }, + 'delete': { + 'orders/{orderId}': 1.5, # 1FW + 'orders/client-order/{clientOid}': 1.5, # 1FW + 'orders': 45, # 30FW + 'stopOrders': 22.5, # 15FW + 'copy-trade/futures/orders': 1.5, # 1FW + 'copy-trade/futures/orders/client-order': 1.5, # 1FW + }, + }, + 'webExchange': { + 'get': { + 'currency/currency/chain-info': 1, # self is temporary from webApi + }, + }, + 'broker': { + 'get': { + 'broker/nd/info': 2, + 'broker/nd/account': 2, + 'broker/nd/account/apikey': 2, + 'broker/nd/rebase/download': 3, + 'asset/ndbroker/deposit/list': 1, + 'broker/nd/transfer/detail': 1, + 'broker/nd/deposit/detail': 1, + 'broker/nd/withdraw/detail': 1, + }, + 'post': { + 'broker/nd/transfer': 1, + 'broker/nd/account': 3, + 'broker/nd/account/apikey': 3, + 'broker/nd/account/update-apikey': 3, + }, + 'delete': { + 'broker/nd/account/apikey': 3, + }, + }, + 'earn': { + 'get': { + 'otc-loan/loan': 1, + 'otc-loan/accounts': 1, + 'earn/redeem-preview': 7.5, # 5EW + 'earn/saving/products': 7.5, # 5EW + 'earn/hold-assets': 7.5, # 5EW + 'earn/promotion/products': 7.5, # 5EW + 'earn/kcs-staking/products': 7.5, # 5EW + 'earn/staking/products': 7.5, # 5EW + 'earn/eth-staking/products': 7.5, # 5EW + }, + 'post': { + 'earn/orders': 7.5, # 5EW + }, + 'delete': { + 'earn/orders': 7.5, # 5EW + }, + }, + 'uta': { + 'get': { + 'market/announcement': 20, + 'market/currency': 3, + 'market/instrument': 4, + 'market/ticker': 15, + 'market/orderbook': 3, + 'market/trade': 3, + 'market/kline': 3, + 'market/funding-rate': 2, + 'market/funding-rate-history': 5, + 'market/cross-config': 25, + 'server/status': 3, + }, + }, + }, + 'timeframes': { + '1m': '1min', + '3m': '3min', + '5m': '5min', + '15m': '15min', + '30m': '30min', + '1h': '1hour', + '2h': '2hour', + '4h': '4hour', + '6h': '6hour', + '8h': '8hour', + '12h': '12hour', + '1d': '1day', + '1w': '1week', + '1M': '1month', + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'exact': { + 'Order not exist or not allow to be cancelled': OrderNotFound, + 'The order does not exist.': OrderNotFound, + 'order not exist': OrderNotFound, + 'order not exist.': OrderNotFound, # duplicated error temporarily + 'order_not_exist': OrderNotFound, # {"code":"order_not_exist","msg":"order_not_exist"} ¯\_(ツ)_/¯ + 'order_not_exist_or_not_allow_to_cancel': InvalidOrder, # {"code":"400100","msg":"order_not_exist_or_not_allow_to_cancel"} + 'Order size below the minimum requirement.': InvalidOrder, # {"code":"400100","msg":"Order size below the minimum requirement."} + 'Order size increment invalid.': InvalidOrder, # {"msg":"Order size increment invalid.","code":"600100"} + 'The withdrawal amount is below the minimum requirement.': ExchangeError, # {"code":"400100","msg":"The withdrawal amount is below the minimum requirement."} + 'Unsuccessful! Exceeded the max. funds out-transfer limit': InsufficientFunds, # {"code":"200000","msg":"Unsuccessful! Exceeded the max. funds out-transfer limit"} + 'The amount increment is invalid.': BadRequest, + 'The quantity is below the minimum requirement.': InvalidOrder, # {"msg":"The quantity is below the minimum requirement.","code":"400100"} + '400': BadRequest, + '401': AuthenticationError, + '403': NotSupported, + '404': NotSupported, + '405': NotSupported, + '415': NotSupported, + '429': RateLimitExceeded, + '500': ExchangeNotAvailable, # Internal Server Error -- We had a problem with our server. Try again later. + '503': ExchangeNotAvailable, + '101030': PermissionDenied, # {"code":"101030","msg":"You haven't yet enabled the margin trading"} + '103000': InvalidOrder, # {"code":"103000","msg":"Exceed the borrowing limit, the remaining borrowable amount is: 0USDT"} + '130101': BadRequest, # Parameter error + '130102': ExchangeError, # Maximum subscription amount has been exceeded. + '130103': OrderNotFound, # Subscription order does not exist. + '130104': ExchangeError, # Maximum number of subscription orders has been exceeded. + '130105': InsufficientFunds, # Insufficient balance. + '130106': NotSupported, # The currency does not support redemption. + '130107': ExchangeError, # Redemption amount exceeds subscription amount. + '130108': OrderNotFound, # Redemption order does not exist. + '130201': PermissionDenied, # Your account has restricted access to certain features. Please contact customer service for further assistance + '130202': ExchangeError, # The system is renewing the loan automatically. Please try again later + '130203': InsufficientFunds, # Insufficient account balance + '130204': BadRequest, # As the total lending amount for platform leverage reaches the platform's maximum position limit, the system suspends the borrowing function of leverage + '130301': InsufficientFunds, # Insufficient account balance + '130302': PermissionDenied, # Your relevant permission rights have been restricted, you can contact customer service for processing + '130303': NotSupported, # The current trading pair does not support isolated positions + '130304': NotSupported, # The trading function of the current trading pair is not enabled + '130305': NotSupported, # The current trading pair does not support cross position + '130306': NotSupported, # The account has not opened leveraged trading + '130307': NotSupported, # Please reopen the leverage agreement + '130308': InvalidOrder, # Position renewal freeze + '130309': InvalidOrder, # Position forced liquidation freeze + '130310': ExchangeError, # Abnormal leverage account status + '130311': InvalidOrder, # Failed to place an order, triggering buy limit + '130312': InvalidOrder, # Trigger global position limit, suspend buying + '130313': InvalidOrder, # Trigger global position limit, suspend selling + '130314': InvalidOrder, # Trigger the global position limit and prompt the remaining quantity available for purchase + '130315': NotSupported, # This feature has been suspended due to country restrictions + '126000': ExchangeError, # Abnormal margin trading + '126001': NotSupported, # Users currently do not support high frequency + '126002': ExchangeError, # There is a risk problem in your account and transactions are temporarily not allowed! + '126003': InvalidOrder, # The commission amount is less than the minimum transaction amount for a single commission + '126004': ExchangeError, # Trading pair does not exist or is prohibited + '126005': PermissionDenied, # This trading pair requires advanced KYC certification before trading + '126006': ExchangeError, # Trading pair is not available + '126007': ExchangeError, # Trading pair suspended + '126009': ExchangeError, # Trading pair is suspended from creating orders + '126010': ExchangeError, # Trading pair suspended order cancellation + '126011': ExchangeError, # There are too many orders in the order + '126013': InsufficientFunds, # Insufficient account balance + '126015': ExchangeError, # It is prohibited to place orders on self trading pair + '126021': NotSupported, # This digital asset does not support user participation in your region, thank you for your understanding! + '126022': InvalidOrder, # The final transaction price of your order will trigger the price protection strategy. To protect the price from deviating too much, please place an order again. + '126027': InvalidOrder, # Only limit orders are supported + '126028': InvalidOrder, # Only limit orders are supported before the specified time + '126029': InvalidOrder, # The maximum order price is: xxx + '126030': InvalidOrder, # The minimum order price is: xxx + '126033': InvalidOrder, # Duplicate order + '126034': InvalidOrder, # Failed to create take profit and stop loss order + '126036': InvalidOrder, # Failed to create margin order + '126037': ExchangeError, # Due to country and region restrictions, self function has been suspended! + '126038': ExchangeError, # Third-party service call failed(internal exception) + '126039': ExchangeError, # Third-party service call failed, reason: xxx + '126041': ExchangeError, # clientTimestamp parameter error + '126042': ExchangeError, # Exceeded maximum position limit + '126043': OrderNotFound, # Order does not exist + '126044': InvalidOrder, # clientOid duplicate + '126045': NotSupported, # This digital asset does not support user participation in your region, thank you for your understanding! + '126046': NotSupported, # This digital asset does not support your IP region, thank you for your understanding! + '126047': PermissionDenied, # Please complete identity verification + '126048': PermissionDenied, # Please complete authentication for the master account + '135005': ExchangeError, # Margin order query business abnormality + '135018': ExchangeError, # Margin order query service abnormality + '200004': InsufficientFunds, + '210014': InvalidOrder, # {"code":"210014","msg":"Exceeds the max. borrowing amount, the remaining amount you can borrow: 0USDT"} + '210021': InsufficientFunds, # {"code":"210021","msg":"Balance not enough"} + '230003': InsufficientFunds, # {"code":"230003","msg":"Balance insufficient!"} + '260000': InvalidAddress, # {"code":"260000","msg":"Deposit address already exists."} + '260100': InsufficientFunds, # {"code":"260100","msg":"account.noBalance"} + '300000': InvalidOrder, + '400000': BadSymbol, + '400001': AuthenticationError, + '400002': InvalidNonce, + '400003': AuthenticationError, + '400004': AuthenticationError, + '400005': AuthenticationError, + '400006': AuthenticationError, + '400007': AuthenticationError, + '400008': NotSupported, + '400100': InsufficientFunds, # {"msg":"account.available.amount","code":"400100"} or {"msg":"Withdrawal amount is below the minimum requirement.","code":"400100"} + '400200': InvalidOrder, # {"code":"400200","msg":"Forbidden to place an order"} + '400330': InvalidOrder, # {"msg":"Order price can't deviate from NAV by 50%","code":"400330"} + '400350': InvalidOrder, # {"code":"400350","msg":"Upper limit for holding: 10,000USDT, you can still buy 10,000USDT worth of coin."} + '400370': InvalidOrder, # {"code":"400370","msg":"Max. price: 0.02500000000000000000"} + '400400': BadRequest, # Parameter error + '400401': AuthenticationError, # User is not logged in + '400500': InvalidOrder, # {"code":"400500","msg":"Your located country/region is currently not supported for the trading of self token"} + '400600': BadSymbol, # {"code":"400600","msg":"validation.createOrder.symbolNotAvailable"} + '400760': InvalidOrder, # {"code":"400760","msg":"order price should be more than XX"} + '401000': BadRequest, # {"code":"401000","msg":"The interface has been deprecated"} + '408000': BadRequest, # Network timeout, please try again later + '411100': AccountSuspended, + '415000': BadRequest, # {"code":"415000","msg":"Unsupported Media Type"} + '400303': PermissionDenied, # {"msg":"To enjoy the full range of our products and services, we kindly request you complete the identity verification process.","code":"400303"} + '500000': ExchangeNotAvailable, # {"code":"500000","msg":"Internal Server Error"} + '260220': InvalidAddress, # {"code": "260220", "msg": "deposit.address.not.exists"} + '600100': InsufficientFunds, # {"msg":"Funds below the minimum requirement.","code":"600100"} + '600101': InvalidOrder, # {"msg":"The order funds should more then 0.1 USDT.","code":"600101"} + '900014': BadRequest, # {"code":"900014","msg":"Invalid chainId"} + }, + 'broad': { + 'Exceeded the access frequency': RateLimitExceeded, + 'require more permission': PermissionDenied, + }, + }, + 'fees': { + 'trading': { + 'tierBased': True, + 'percentage': True, + 'taker': self.parse_number('0.001'), + 'maker': self.parse_number('0.001'), + 'tiers': { + 'taker': [ + [self.parse_number('0'), self.parse_number('0.001')], + [self.parse_number('50'), self.parse_number('0.001')], + [self.parse_number('200'), self.parse_number('0.0009')], + [self.parse_number('500'), self.parse_number('0.0008')], + [self.parse_number('1000'), self.parse_number('0.0007')], + [self.parse_number('2000'), self.parse_number('0.0007')], + [self.parse_number('4000'), self.parse_number('0.0006')], + [self.parse_number('8000'), self.parse_number('0.0005')], + [self.parse_number('15000'), self.parse_number('0.00045')], + [self.parse_number('25000'), self.parse_number('0.0004')], + [self.parse_number('40000'), self.parse_number('0.00035')], + [self.parse_number('60000'), self.parse_number('0.0003')], + [self.parse_number('80000'), self.parse_number('0.00025')], + ], + 'maker': [ + [self.parse_number('0'), self.parse_number('0.001')], + [self.parse_number('50'), self.parse_number('0.0009')], + [self.parse_number('200'), self.parse_number('0.0007')], + [self.parse_number('500'), self.parse_number('0.0005')], + [self.parse_number('1000'), self.parse_number('0.0003')], + [self.parse_number('2000'), self.parse_number('0')], + [self.parse_number('4000'), self.parse_number('0')], + [self.parse_number('8000'), self.parse_number('0')], + [self.parse_number('15000'), self.parse_number('-0.00005')], + [self.parse_number('25000'), self.parse_number('-0.00005')], + [self.parse_number('40000'), self.parse_number('-0.00005')], + [self.parse_number('60000'), self.parse_number('-0.00005')], + [self.parse_number('80000'), self.parse_number('-0.00005')], + ], + }, + }, + 'funding': { + 'tierBased': False, + 'percentage': False, + 'withdraw': {}, + 'deposit': {}, + }, + }, + 'commonCurrencies': { + 'BIFI': 'BIFIF', + 'VAI': 'VAIOT', + 'WAX': 'WAXP', + 'ALT': 'APTOSLAUNCHTOKEN', + 'KALT': 'ALT', # ALTLAYER + 'FUD': 'FTX Users\' Debt', + }, + 'options': { + 'hf': None, # would be auto set to `true/false` after first load + 'version': 'v1', + 'symbolSeparator': '-', + 'fetchMyTradesMethod': 'private_get_fills', + 'timeDifference': 0, # the difference between system clock and Binance clock + 'adjustForTimeDifference': False, # controls the adjustment logic upon instantiation + 'fetchCurrencies': { + 'webApiEnable': True, # fetches from WEB + 'webApiRetries': 1, + 'webApiMuteFailure': True, + }, + 'fetchMarkets': { + 'fetchTickersFees': True, + }, + 'withdraw': { + 'includeFee': False, + }, + # endpoint versions + 'versions': { + 'public': { + 'GET': { + # spot trading + 'currencies': 'v3', + 'currencies/{currency}': 'v3', + 'symbols': 'v2', + 'mark-price/all-symbols': 'v3', + 'announcements': 'v3', + }, + }, + 'private': { + 'GET': { + # account + 'user-info': 'v2', + 'hf/margin/account/ledgers': 'v3', + 'sub/user': 'v2', + 'sub-accounts': 'v2', + # funding + 'margin/accounts': 'v3', + 'isolated/accounts': 'v3', + # 'deposit-addresses': 'v2', + 'deposit-addresses': 'v1', # 'v1' for fetchDepositAddress, 'v2' for fetchDepositAddressesByNetwork + # spot trading + 'market/orderbook/level2': 'v3', + 'market/orderbook/level3': 'v3', + 'market/orderbook/level{level}': 'v3', + 'oco/order/{orderId}': 'v3', + 'oco/order/details/{orderId}': 'v3', + 'oco/client-order/{clientOid}': 'v3', + 'oco/orders': 'v3', + # margin trading + 'hf/margin/orders/active': 'v3', + 'hf/margin/order/active/symbols': 'v3', + 'hf/margin/orders/done': 'v3', + 'hf/margin/orders/{orderId}': 'v3', + 'hf/margin/orders/client-order/{clientOid}': 'v3', + 'hf/margin/fills': 'v3', + 'etf/info': 'v3', + 'margin/currencies': 'v3', + 'margin/borrow': 'v3', + 'margin/repay': 'v3', + 'margin/interest': 'v3', + 'project/list': 'v3', + 'project/marketInterestRate': 'v3', + 'redeem/orders': 'v3', + 'purchase/orders': 'v3', + 'migrate/user/account/status': 'v3', + 'margin/symbols': 'v3', + 'affiliate/inviter/statistics': 'v2', + 'asset/ndbroker/deposit/list': 'v1', + }, + 'POST': { + # account + 'sub/user/created': 'v2', + # funding + 'accounts/universal-transfer': 'v3', + 'accounts/sub-transfer': 'v2', + 'accounts/inner-transfer': 'v2', + 'transfer-out': 'v3', + 'deposit-address/create': 'v3', + # spot trading + 'oco/order': 'v3', + # margin trading + 'hf/margin/order': 'v3', + 'hf/margin/order/test': 'v3', + 'margin/borrow': 'v3', + 'margin/repay': 'v3', + 'purchase': 'v3', + 'redeem': 'v3', + 'lend/purchase/update': 'v3', + 'position/update-user-leverage': 'v3', + 'withdrawals': 'v3', + }, + 'DELETE': { + # account + # funding + # spot trading + 'hf/margin/orders/{orderId}': 'v3', + 'hf/margin/orders/client-order/{clientOid}': 'v3', + 'hf/margin/orders': 'v3', + 'oco/order/{orderId}': 'v3', + 'oco/client-order/{clientOid}': 'v3', + 'oco/orders': 'v3', + # margin trading + }, + }, + 'futuresPrivate': { + 'POST': { + 'transfer-out': 'v3', + }, + }, + }, + 'partner': { + # the support for spot and future exchanges settings + 'spot': { + 'id': 'ccxt', + 'key': '9e58cc35-5b5e-4133-92ec-166e3f077cb8', + }, + 'future': { + 'id': 'ccxtfutures', + 'key': '1b327198-f30c-4f14-a0ac-918871282f15', + }, + # exchange-wide settings are also supported + # 'id': 'ccxt' + # 'key': '9e58cc35-5b5e-4133-92ec-166e3f077cb8', + }, + 'accountsByType': { + 'spot': 'trade', + 'margin': 'margin', + 'cross': 'margin', + 'isolated': 'isolated', + 'main': 'main', + 'funding': 'main', + 'future': 'contract', + 'swap': 'contract', + 'mining': 'pool', + 'hf': 'trade_hf', + }, + 'networks': { + 'BRC20': 'btc', + 'BTCNATIVESEGWIT': 'bech32', + 'ERC20': 'eth', + 'TRC20': 'trx', + 'HRC20': 'heco', + 'MATIC': 'matic', + 'KCC': 'kcc', # kucoin community chain + 'SOL': 'sol', + 'ALGO': 'algo', + 'EOS': 'eos', + 'BEP20': 'bsc', + 'BEP2': 'bnb', + 'ARBONE': 'arbitrum', + 'AVAXX': 'avax', + 'AVAXC': 'avaxc', + 'TLOS': 'tlos', # tlosevm is different + 'CFX': 'cfx', + 'ACA': 'aca', + 'OPTIMISM': 'optimism', + 'ONT': 'ont', + 'GLMR': 'glmr', + 'CSPR': 'cspr', + 'KLAY': 'klay', + 'XRD': 'xrd', + 'RVN': 'rvn', + 'NEAR': 'near', + 'APT': 'aptos', + 'ETHW': 'ethw', + 'TON': 'ton', + 'BCH': 'bch', + 'BSV': 'bchsv', + 'BCHA': 'bchabc', + 'OSMO': 'osmo', + 'NANO': 'nano', + 'XLM': 'xlm', + 'VET': 'vet', + 'IOST': 'iost', + 'ZIL': 'zil', + 'XRP': 'xrp', + 'TOMO': 'tomo', + 'XMR': 'xmr', + 'COTI': 'coti', + 'XTZ': 'xtz', + 'ADA': 'ada', + 'WAX': 'waxp', + 'THETA': 'theta', + 'ONE': 'one', + 'IOTEX': 'iotx', + 'NULS': 'nuls', + 'KSM': 'ksm', + 'LTC': 'ltc', + 'WAVES': 'waves', + 'DOT': 'dot', + 'STEEM': 'steem', + 'QTUM': 'qtum', + 'DOGE': 'doge', + 'FIL': 'fil', + 'XYM': 'xym', + 'FLUX': 'flux', + 'ATOM': 'atom', + 'XDC': 'xdc', + 'KDA': 'kda', + 'ICP': 'icp', + 'CELO': 'celo', + 'LSK': 'lsk', + 'VSYS': 'vsys', + 'KAR': 'kar', + 'XCH': 'xch', + 'FLOW': 'flow', + 'BAND': 'band', + 'EGLD': 'egld', + 'HBAR': 'hbar', + 'XPR': 'xpr', + 'AR': 'ar', + 'FTM': 'ftm', + 'KAVA': 'kava', + 'KMA': 'kma', + 'XEC': 'xec', + 'IOTA': 'iota', + 'HNT': 'hnt', + 'ASTR': 'astr', + 'PDEX': 'pdex', + 'METIS': 'metis', + 'ZEC': 'zec', + 'POKT': 'pokt', + 'OASYS': 'oas', + 'OASIS': 'oasis', # a.k.a. ROSE + 'ETC': 'etc', + 'AKT': 'akt', + 'FSN': 'fsn', + 'SCRT': 'scrt', + 'CFG': 'cfg', + 'ICX': 'icx', + 'KMD': 'kmd', + 'NEM': 'NEM', + 'STX': 'stx', + 'DGB': 'dgb', + 'DCR': 'dcr', + 'CKB': 'ckb', # ckb2 is just odd entry + 'ELA': 'ela', # esc might be another chain elastos smart chain + 'HYDRA': 'hydra', + 'BTM': 'btm', + 'KARDIA': 'kai', + 'SXP': 'sxp', # a.k.a. solar swipe + 'NEBL': 'nebl', + 'ZEN': 'zen', + 'SDN': 'sdn', + 'LTO': 'lto', + 'WEMIX': 'wemix', + # 'BOBA': 'boba', # tbd + 'EVER': 'ever', + 'BNC': 'bnc', + 'BNCDOT': 'bncdot', + # 'CMP': 'cmp', # todo: after consensus + 'AION': 'aion', + 'GRIN': 'grin', + 'LOKI': 'loki', + 'QKC': 'qkc', + 'TT': 'TT', + 'PIVX': 'pivx', + 'SERO': 'sero', + 'METER': 'meter', + 'STATEMINE': 'statemine', # a.k.a. RMRK + 'DVPN': 'dvpn', + 'XPRT': 'xprt', + 'MOVR': 'movr', + 'ERGO': 'ergo', + 'ABBC': 'abbc', + 'DIVI': 'divi', + 'PURA': 'pura', + 'DFI': 'dfi', + # 'NEO': 'neo', # tbd neo legacy + 'NEON3': 'neon3', + 'DOCK': 'dock', + 'TRUE': 'true', + 'CS': 'cs', + 'ORAI': 'orai', + 'BASE': 'base', + 'TARA': 'tara', + # below will be uncommented after consensus + # 'BITCOINDIAMON': 'bcd', + # 'BITCOINGOLD': 'btg', + # 'HTR': 'htr', + # 'DEROHE': 'derohe', + # 'NDAU': 'ndau', + # 'HPB': 'hpb', + # 'AXE': 'axe', + # 'BITCOINPRIVATE': 'btcp', + # 'EDGEWARE': 'edg', + # 'JUPITER': 'jup', + # 'VELAS': 'vlx', # vlxevm is different + # # 'terra' luna lunc TBD + # 'DIGITALBITS': 'xdb', + # # fra is fra-emv on kucoin + # 'PASTEL': 'psl', + # # sysevm + # 'CONCORDIUM': 'ccd', + # 'AURORA': 'aurora', + # 'PHA': 'pha', # a.k.a. khala + # 'PAL': 'pal', + # 'RSK': 'rbtc', + # 'NIX': 'nix', + # 'NIM': 'nim', + # 'NRG': 'nrg', + # 'RFOX': 'rfox', + # 'PIONEER': 'neer', + # 'PIXIE': 'pix', + # 'ALEPHZERO': 'azero', + # 'ACHAIN': 'act', # actevm is different + # 'BOSCOIN': 'bos', + # 'ELECTRONEUM': 'etn', + # 'GOCHAIN': 'go', + # 'SOPHIATX': 'sphtx', + # 'WANCHAIN': 'wan', + # 'ZEEPIN': 'zpt', + # 'MATRIXAI': 'man', + # 'METADIUM': 'meta', + # 'METAHASH': 'mhc', + # # eosc --"eosforce" tbd + # 'IOTCHAIN': 'itc', + # 'CONTENTOS': 'cos', + # 'CPCHAIN': 'cpc', + # 'INTCHAIN': 'int', + # # 'DASH': 'dash', tbd digita-cash + # 'WALTONCHAIN': 'wtc', + # 'CONSTELLATION': 'dag', + # 'ONELEDGER': 'olt', + # 'AIRDAO': 'amb', # a.k.a. AMBROSUS + # 'ENERGYWEB': 'ewt', + # 'WAVESENTERPRISE': 'west', + # 'HYPERCASH': 'hc', + # 'ENECUUM': 'enq', + # 'HAVEN': 'xhv', + # 'CHAINX': 'pcx', + # # 'FLUXOLD': 'zel', # zel seems old chain(with uppercase FLUX in kucoin UI and with id 'zel') + # 'BUMO': 'bu', + # 'DEEPONION': 'onion', + # 'ULORD': 'ut', + # 'ASCH': 'xas', + # 'SOLARIS': 'xlr', + # 'APOLLO': 'apl', + # 'PIRATECHAIN': 'arrr', + # 'ULTRA': 'uos', + # 'EMONEY': 'ngm', + # 'AURORACHAIN': 'aoa', + # 'KLEVER': 'klv', + # undetermined: xns(insolar), rhoc, luk(luniverse), kts(klimatas), bchn(bitcoin cash node), god(shallow entry), lit(litmus), + }, + 'marginModes': { + 'cross': 'MARGIN_TRADE', + 'isolated': 'MARGIN_ISOLATED_TRADE', + 'spot': 'TRADE', + }, + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': True, + 'triggerPrice': True, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': True, + 'takeProfitPrice': True, + 'attachedStopLossTakeProfit': None, # not supported + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': True, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': True, # todo implement + 'iceberg': True, # todo implement + }, + 'createOrders': { + 'max': 5, + }, + 'fetchMyTrades': { + 'marginMode': True, + 'limit': None, + 'daysBack': None, + 'untilDays': 7, # per implementation comments + 'symbolRequired': True, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': True, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOpenOrders': { + 'marginMode': True, + 'limit': 500, + 'trigger': True, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOrders': None, + 'fetchClosedOrders': { + 'marginMode': True, + 'limit': 500, + 'daysBack': None, + 'daysBackCanceled': None, + 'untilDays': 7, + 'trigger': True, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOHLCV': { + 'limit': 1500, + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + }) + + def nonce(self): + return self.milliseconds() - self.options['timeDifference'] + + def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://docs.kucoin.com/#server-time + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = self.publicGetTimestamp(params) + # + # { + # "code":"200000", + # "msg":"success", + # "data":1546837113087 + # } + # + return self.safe_integer(response, 'data') + + def fetch_status(self, params={}): + """ + the latest known information on the availability of the exchange API + + https://docs.kucoin.com/#service-status + https://www.kucoin.com/docs-new/rest/ua/get-service-status + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :param str [params.tradeType]: *uta only* set to SPOT or FUTURES + :returns dict: a `status structure ` + """ + uta = None + uta, params = self.handle_option_and_params(params, 'fetchStatus', 'uta', False) + response = None + if uta: + defaultType = self.safe_string(self.options, 'defaultType', 'spot') + defaultTradeType = 'SPOT' if (defaultType == 'spot') else 'FUTURES' + tradeType = self.safe_string_upper(params, 'tradeType', defaultTradeType) + request = { + 'tradeType': tradeType, + } + response = self.utaGetServerStatus(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "tradeType": "SPOT", + # "serverStatus": "open", + # "msg": "" + # } + # } + # + else: + response = self.publicGetStatus(params) + # + # { + # "code":"200000", + # "data":{ + # "status":"open", #open, close, cancelonly + # "msg":"upgrade match engine" #remark for operation + # } + # } + # + data = self.safe_dict(response, 'data', {}) + status = self.safe_string_2(data, 'status', 'serverStatus') + return { + 'status': 'ok' if (status == 'open') else 'maintenance', + 'updated': None, + 'eta': None, + 'url': None, + 'info': response, + } + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for kucoin + + https://docs.kucoin.com/#get-symbols-list-deprecated + https://docs.kucoin.com/#get-all-tickers + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :returns dict[]: an array of objects representing market data + """ + fetchTickersFees = None + fetchTickersFees, params = self.handle_option_and_params(params, 'fetchMarkets', 'fetchTickersFees', True) + uta = None + uta, params = self.handle_option_and_params(params, 'fetchMarkets', 'uta', False) + if uta: + return self.fetch_uta_markets(params) + promises = [] + promises.append(self.publicGetSymbols(params)) + # + # { + # "code": "200000", + # "data": [ + # { + # "symbol": "XLM-USDT", + # "name": "XLM-USDT", + # "baseCurrency": "XLM", + # "quoteCurrency": "USDT", + # "feeCurrency": "USDT", + # "market": "USDS", + # "baseMinSize": "0.1", + # "quoteMinSize": "0.01", + # "baseMaxSize": "10000000000", + # "quoteMaxSize": "99999999", + # "baseIncrement": "0.0001", + # "quoteIncrement": "0.000001", + # "priceIncrement": "0.000001", + # "priceLimitRate": "0.1", + # "isMarginEnabled": True, + # "enableTrading": True + # }, + # + credentialsSet = self.check_required_credentials(False) + requestMarginables = credentialsSet and self.safe_bool(params, 'marginables', True) + if requestMarginables: + promises.append(self.privateGetMarginSymbols(params)) # cross margin symbols + # + # { + # "code": "200000", + # "data": { + # "timestamp": 1719393213421, + # "items": [ + # { + # # same object market, with one additional field: + # "minFunds": "0.1" + # }, + # + promises.append(self.privateGetIsolatedSymbols(params)) # isolated margin symbols + # + # { + # "code": "200000", + # "data": [ + # { + # "symbol": "NKN-USDT", + # "symbolName": "NKN-USDT", + # "baseCurrency": "NKN", + # "quoteCurrency": "USDT", + # "maxLeverage": 5, + # "flDebtRatio": "0.97", + # "tradeEnable": True, + # "autoRenewMaxDebtRatio": "0.96", + # "baseBorrowEnable": True, + # "quoteBorrowEnable": True, + # "baseTransferInEnable": True, + # "quoteTransferInEnable": True, + # "baseBorrowCoefficient": "1", + # "quoteBorrowCoefficient": "1" + # }, + # + if fetchTickersFees: + promises.append(self.publicGetMarketAllTickers(params)) + # + # { + # "code": "200000", + # "data": { + # "time":1602832092060, + # "ticker":[ + # { + # "symbol": "BTC-USDT", # symbol + # "symbolName":"BTC-USDT", # Name of trading pairs, it would change after renaming + # "buy": "11328.9", # bestAsk + # "sell": "11329", # bestBid + # "changeRate": "-0.0055", # 24h change rate + # "changePrice": "-63.6", # 24h change price + # "high": "11610", # 24h highest price + # "low": "11200", # 24h lowest price + # "vol": "2282.70993217", # 24h volume,the aggregated trading volume in BTC + # "volValue": "25984946.157790431", # 24h total, the trading volume in quote currency of last 24 hours + # "last": "11328.9", # last price + # "averagePrice": "11360.66065903", # 24h average transaction price yesterday + # "takerFeeRate": "0.001", # Basic Taker Fee + # "makerFeeRate": "0.001", # Basic Maker Fee + # "takerCoefficient": "1", # Taker Fee Coefficient + # "makerCoefficient": "1" # Maker Fee Coefficient + # } + # + if credentialsSet: + # load migration status for account + promises.append(self.load_migration_status()) + responses = promises + symbolsData = self.safe_list(responses[0], 'data') + crossData = self.safe_dict(responses[1], 'data', {}) if requestMarginables else {} + crossItems = self.safe_list(crossData, 'items', []) + crossById = self.index_by(crossItems, 'symbol') + isolatedData = responses[2] if requestMarginables else {} + isolatedItems = self.safe_list(isolatedData, 'data', []) + isolatedById = self.index_by(isolatedItems, 'symbol') + tickersIdx = 3 if requestMarginables else 1 + tickersResponse = self.safe_dict(responses, tickersIdx, {}) + tickerItems = self.safe_list(self.safe_dict(tickersResponse, 'data', {}), 'ticker', []) + tickersById = self.index_by(tickerItems, 'symbol') + result = [] + for i in range(0, len(symbolsData)): + market = symbolsData[i] + id = self.safe_string(market, 'symbol') + baseId, quoteId = id.split('-') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + # quoteIncrement = self.safe_number(market, 'quoteIncrement') + ticker = self.safe_dict(tickersById, id, {}) + makerFeeRate = self.safe_string(ticker, 'makerFeeRate') + takerFeeRate = self.safe_string(ticker, 'takerFeeRate') + makerCoefficient = self.safe_string(ticker, 'makerCoefficient') + takerCoefficient = self.safe_string(ticker, 'takerCoefficient') + hasCrossMargin = (id in crossById) + hasIsolatedMargin = (id in isolatedById) + isMarginable = self.safe_bool(market, 'isMarginEnabled', False) or hasCrossMargin or hasIsolatedMargin + result.append({ + 'id': id, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': isMarginable, + 'marginModes': { + 'cross': hasCrossMargin, + 'isolated': hasIsolatedMargin, + }, + 'swap': False, + 'future': False, + 'option': False, + 'active': self.safe_bool(market, 'enableTrading'), + 'contract': False, + 'linear': None, + 'inverse': None, + 'taker': self.parse_number(Precise.string_mul(takerFeeRate, takerCoefficient)), + 'maker': self.parse_number(Precise.string_mul(makerFeeRate, makerCoefficient)), + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.safe_number(market, 'baseIncrement'), + 'price': self.safe_number(market, 'priceIncrement'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(market, 'baseMinSize'), + 'max': self.safe_number(market, 'baseMaxSize'), + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': self.safe_number(market, 'quoteMinSize'), + 'max': self.safe_number(market, 'quoteMaxSize'), + }, + }, + 'created': None, + 'info': market, + }) + if self.options['adjustForTimeDifference']: + self.load_time_difference() + return result + + def fetch_uta_markets(self, params={}) -> List[Market]: + promises = [] + promises.append(self.utaGetMarketInstrument(self.extend(params, {'tradeType': 'SPOT'}))) + # + # { + # "code": "200000", + # "data": { + # "tradeType": "SPOT", + # "list": [ + # { + # "symbol": "AVA-USDT", + # "name": "AVA-USDT", + # "baseCurrency": "AVA", + # "quoteCurrency": "USDT", + # "market": "USDS", + # "minBaseOrderSize": "0.1", + # "minQuoteOrderSize": "0.1", + # "maxBaseOrderSize": "10000000000", + # "maxQuoteOrderSize": "99999999", + # "baseOrderStep": "0.01", + # "quoteOrderStep": "0.0001", + # "tickSize": "0.0001", + # "feeCurrency": "USDT", + # "tradingStatus": "1", + # "marginMode": "2", + # "priceLimitRatio": "0.05", + # "feeCategory": 1, + # "makerFeeCoefficient": "1.00", + # "takerFeeCoefficient": "1.00", + # "st": False + # }, + # ] + # } + # } + # + promises.append(self.utaGetMarketInstrument(self.extend(params, {'tradeType': 'FUTURES'}))) + # + # { + # "code": "200000", + # "data": { + # "tradeType": "FUTURES", + # "list": [ + # { + # "symbol": "XBTUSDTM", + # "baseCurrency": "XBT", + # "quoteCurrency": "USDT", + # "maxBaseOrderSize": "1000000", + # "tickSize": "0.1", + # "tradingStatus": "1", + # "settlementCurrency": "USDT", + # "contractType": "0", + # "isInverse": False, + # "launchTime": 1585555200000, + # "expiryTime": null, + # "settlementTime": null, + # "maxPrice": "1000000.0", + # "lotSize": "1", + # "unitSize": "0.001", + # "makerFeeRate": "0.00020", + # "takerFeeRate": "0.00060", + # "settlementFeeRate": null, + # "maxLeverage": 125, + # "indexSourceExchanges": ["okex","binance","kucoin","bybit","bitmart","gateio"], + # "k": "490.0", + # "m": "300.0", + # "f": "1.3", + # "mmrLimit": "0.3", + # "mmrLevConstant": "125.0" + # }, + # ] + # } + # } + # + responses = promises + data = self.safe_dict(responses[0], 'data', {}) + contractData = self.safe_dict(responses[1], 'data', {}) + spotData = self.safe_list(data, 'list', []) + contractSymbolsData = self.safe_list(contractData, 'list', []) + symbolsData = self.array_concat(spotData, contractSymbolsData) + result = [] + for i in range(0, len(symbolsData)): + market = symbolsData[i] + id = self.safe_string(market, 'symbol') + baseId = self.safe_string(market, 'baseCurrency') + quoteId = self.safe_string(market, 'quoteCurrency') + settleId = self.safe_string(market, 'settlementCurrency') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + settle = self.safe_currency_code(settleId) + hasMargin = self.safe_string(market, 'marginMode') + isMarginable = True if (hasMargin == '1') else False + symbol = base + '/' + quote + if settle is not None: + symbol += ':' + settle + contractType = self.safe_string(market, 'contractType') + expiry = self.safe_integer(market, 'expiryTime') + active = self.safe_string(market, 'tradingStatus') + type = None + spot = False + swap = False + future = False + contract = False + linear = False + inverse = False + if contractType is not None: + contract = True + if quote == settle: + linear = True + else: + inverse = True + if contractType == '0': + type = 'swap' + swap = True + else: + type = 'future' + future = True + else: + type = 'spot' + spot = True + result.append({ + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': type, + 'spot': spot, + 'margin': isMarginable, + 'swap': swap, + 'future': future, + 'option': False, + 'active': (active == '1'), + 'contract': contract, + 'linear': linear, + 'inverse': inverse, + 'taker': self.safe_number(market, 'makerFeeRate'), + 'maker': self.safe_number(market, 'takerFeeRate'), + 'contractSize': self.safe_number(market, 'unitSize'), + 'expiry': expiry, + 'expiryDatetime': self.iso8601(expiry), + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.safe_number(market, 'lotSize'), + 'price': self.safe_number(market, 'tickSize'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': self.safe_integer(market, 'maxLeverage'), + }, + 'amount': { + 'min': self.safe_number(market, 'minBaseOrderSize'), + 'max': self.safe_number(market, 'maxBaseOrderSize'), + }, + 'price': { + 'min': None, + 'max': self.safe_number(market, 'maxPrice'), + }, + 'cost': { + 'min': self.safe_number(market, 'minQuoteOrderSize'), + 'max': self.safe_number(market, 'maxQuoteOrderSize'), + }, + }, + 'created': self.safe_integer(market, 'launchTime'), + 'info': market, + }) + if self.options['adjustForTimeDifference']: + self.load_time_difference() + return result + + def load_migration_status(self, force: bool = False): + """ + :param boolean force: load account state for non hf + loads the migration status for the account(hf or not) + + https://www.kucoin.com/docs/rest/spot-trading/spot-hf-trade-pro-account/get-user-type + + :returns any: ignore + """ + if not ('hf' in self.options) or (self.options['hf'] is None) or force: + result: dict = self.privateGetHfAccountsOpened() + self.options['hf'] = self.safe_bool(result, 'data') + return True + + def handle_hf_and_params(self, params={}): + migrated: Bool = self.safe_bool(self.options, 'hf', False) + loadedHf: Bool = None + if migrated is not None: + if migrated: + loadedHf = True + else: + loadedHf = False + hf: Bool = self.safe_bool(params, 'hf', loadedHf) + params = self.omit(params, 'hf') + return [hf, params] + + def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://docs.kucoin.com/#get-currencies + + :param dict params: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = self.publicGetCurrencies(params) + # + # { + # "code":"200000", + # "data":[ + # { + # "currency":"CSP", + # "name":"CSP", + # "fullName":"Caspian", + # "precision":8, + # "confirms":null, + # "contractAddress":null, + # "isMarginEnabled":false, + # "isDebitEnabled":false, + # "chains":[ + # { + # "chainName":"ERC20", + # "chainId": "eth" + # "withdrawalMinSize":"2999", + # "depositMinSize":null, + # "withdrawFeeRate":"0", + # "withdrawalMinFee":"2999", + # "isWithdrawEnabled":false, + # "isDepositEnabled":false, + # "confirms":12, + # "preConfirms":12, + # "withdrawPrecision": 8, + # "maxWithdraw": null, + # "maxDeposit": null, + # "needTag": False, + # "contractAddress":"0xa6446d655a0c34bc4f05042ee88170d056cbaf45", + # "depositFeeRate": "0.001", # present for some currencies/networks + # } + # ] + # }, + # ] + # } + # + currenciesData = self.safe_list(response, 'data', []) + brokenCurrencies = self.safe_list(self.options, 'brokenCurrencies', ['00', 'OPEN_ERROR', 'HUF', 'BDT']) + result: dict = {} + for i in range(0, len(currenciesData)): + entry = currenciesData[i] + id = self.safe_string(entry, 'currency') + if self.in_array(id, brokenCurrencies): + continue # skip buggy entries: https://t.me/KuCoin_API/217798 + code = self.safe_currency_code(id) + networks: dict = {} + chains = self.safe_list(entry, 'chains', []) + chainsLength = len(chains) + for j in range(0, chainsLength): + chain = chains[j] + chainId = self.safe_string(chain, 'chainId') + networkCode = self.network_id_to_code(chainId, code) + networks[networkCode] = { + 'info': chain, + 'id': chainId, + 'name': self.safe_string(chain, 'chainName'), + 'code': networkCode, + 'active': None, + 'fee': self.safe_number(chain, 'withdrawalMinFee'), + 'deposit': self.safe_bool(chain, 'isDepositEnabled'), + 'withdraw': self.safe_bool(chain, 'isWithdrawEnabled'), + 'precision': self.parse_number(self.parse_precision(self.safe_string(chain, 'withdrawPrecision'))), + 'limits': { + 'withdraw': { + 'min': self.safe_number(chain, 'withdrawalMinSize'), + 'max': self.safe_number(chain, 'maxWithdraw'), + }, + 'deposit': { + 'min': self.safe_number(chain, 'depositMinSize'), + 'max': self.safe_number(chain, 'maxDeposit'), + }, + }, + } + # kucoin has determined 'fiat' currencies with below logic + rawPrecision = self.safe_string(entry, 'precision') + precision = self.parse_number(self.parse_precision(rawPrecision)) + isFiat = chainsLength == 0 + result[code] = self.safe_currency_structure({ + 'id': id, + 'name': self.safe_string(entry, 'fullName'), + 'code': code, + 'type': 'fiat' if isFiat else 'crypto', + 'precision': precision, + 'info': entry, + 'networks': networks, + 'deposit': None, + 'withdraw': None, + 'active': None, + 'fee': None, + 'limits': None, + }) + return result + + def fetch_accounts(self, params={}) -> List[Account]: + """ + fetch all the accounts associated with a profile + + https://docs.kucoin.com/#list-accounts + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `account structures ` indexed by the account type + """ + response = self.privateGetAccounts(params) + # + # { + # "code": "200000", + # "data": [ + # { + # "balance": "0.00009788", + # "available": "0.00009788", + # "holds": "0", + # "currency": "BTC", + # "id": "5c6a4fd399a1d81c4f9cc4d0", + # "type": "trade" + # }, + # { + # "balance": "0.00000001", + # "available": "0.00000001", + # "holds": "0", + # "currency": "ETH", + # "id": "5c6a49ec99a1d819392e8e9f", + # "type": "trade" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + result = [] + for i in range(0, len(data)): + account = data[i] + accountId = self.safe_string(account, 'id') + currencyId = self.safe_string(account, 'currency') + code = self.safe_currency_code(currencyId) + type = self.safe_string(account, 'type') # main or trade + result.append({ + 'id': accountId, + 'type': type, + 'currency': code, + 'code': code, + 'info': account, + }) + return result + + def fetch_transaction_fee(self, code: str, params={}): + """ + *DEPRECATED* please use fetchDepositWithdrawFee instead + + https://docs.kucoin.com/#get-withdrawal-quotas + + :param str code: unified currency code + :param dict params: extra parameters specific to the exchange API endpoint + :returns dict: a `fee structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + } + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + if networkCode is not None: + request['chain'] = self.network_code_to_id(networkCode).lower() + response = self.privateGetWithdrawalsQuotas(self.extend(request, params)) + data = self.safe_dict(response, 'data', {}) + withdrawFees: dict = {} + withdrawFees[code] = self.safe_number(data, 'withdrawMinFee') + return { + 'info': response, + 'withdraw': withdrawFees, + 'deposit': {}, + } + + def fetch_deposit_withdraw_fee(self, code: str, params={}): + """ + fetch the fee for deposits and withdrawals + + https://docs.kucoin.com/#get-withdrawal-quotas + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.network]: The chain of currency. This only apply for multi-chain currency, and there is no need for single chain currency; you can query the chain through the response of the GET /api/v2/currencies/{currency} interface + :returns dict: a `fee structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + } + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + if networkCode is not None: + request['chain'] = self.network_code_to_id(networkCode).lower() + response = self.privateGetWithdrawalsQuotas(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "currency": "USDT", + # "limitBTCAmount": "1.00000000", + # "usedBTCAmount": "0.00000000", + # "remainAmount": "16548.072149", + # "availableAmount": "0", + # "withdrawMinFee": "25", + # "innerWithdrawMinFee": "0", + # "withdrawMinSize": "50", + # "isWithdrawEnabled": True, + # "precision": 6, + # "chain": "ERC20" + # } + # } + # + data = self.safe_dict(response, 'data') + return self.parse_deposit_withdraw_fee(data, currency) + + def parse_deposit_withdraw_fee(self, fee, currency: Currency = None): + # + # { + # "currency": "USDT", + # "limitBTCAmount": "1.00000000", + # "usedBTCAmount": "0.00000000", + # "remainAmount": "16548.072149", + # "availableAmount": "0", + # "withdrawMinFee": "25", + # "innerWithdrawMinFee": "0", + # "withdrawMinSize": "50", + # "isWithdrawEnabled": True, + # "precision": 6, + # "chain": "ERC20" + # } + # + if 'chains' in fee: + # if data obtained through `currencies` endpoint + resultNew: dict = { + 'info': fee, + 'withdraw': { + 'fee': None, + 'percentage': False, + }, + 'deposit': { + 'fee': None, + 'percentage': None, + }, + 'networks': {}, + } + chains = self.safe_list(fee, 'chains', []) + for i in range(0, len(chains)): + chain = chains[i] + networkCodeNew = self.network_id_to_code(self.safe_string(chain, 'chainId'), self.safe_string(currency, 'code')) + resultNew['networks'][networkCodeNew] = { + 'withdraw': { + 'fee': self.safe_number_2(chain, 'withdrawalMinFee', 'withdrawMinFee'), + 'percentage': False, + }, + 'deposit': { + 'fee': None, + 'percentage': None, + }, + } + return resultNew + minWithdrawFee = self.safe_number(fee, 'withdrawMinFee') + result: dict = { + 'info': fee, + 'withdraw': { + 'fee': minWithdrawFee, + 'percentage': False, + }, + 'deposit': { + 'fee': None, + 'percentage': None, + }, + 'networks': {}, + } + networkId = self.safe_string(fee, 'chain') + networkCode = self.network_id_to_code(networkId, self.safe_string(currency, 'code')) + result['networks'][networkCode] = { + 'withdraw': minWithdrawFee, + 'deposit': { + 'fee': None, + 'percentage': None, + }, + } + return result + + def is_futures_method(self, methodName, params): + # + # Helper + # @methodName(string): The name of the method + # @params(dict): The parameters passed into {methodName} + # @return: True if the method used is meant for futures trading, False otherwise + # + defaultType = self.safe_string_2(self.options, methodName, 'defaultType', 'trade') + requestedType = self.safe_string(params, 'type', defaultType) + accountsByType = self.safe_dict(self.options, 'accountsByType') + type = self.safe_string(accountsByType, requestedType) + if type is None: + keys = list(accountsByType.keys()) + raise ExchangeError(self.id + ' isFuturesMethod() type must be one of ' + ', '.join(keys)) + params = self.omit(params, 'type') + return(type == 'contract') or (type == 'future') or (type == 'futures') # * (type == 'futures') deprecated, use(type == 'future') + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "symbol": "BTC-USDT", # symbol + # "symbolName":"BTC-USDT", # Name of trading pairs, it would change after renaming + # "buy": "11328.9", # bestAsk + # "sell": "11329", # bestBid + # "changeRate": "-0.0055", # 24h change rate + # "changePrice": "-63.6", # 24h change price + # "high": "11610", # 24h highest price + # "low": "11200", # 24h lowest price + # "vol": "2282.70993217", # 24h volume,the aggregated trading volume in BTC + # "volValue": "25984946.157790431", # 24h total, the trading volume in quote currency of last 24 hours + # "last": "11328.9", # last price + # "averagePrice": "11360.66065903", # 24h average transaction price yesterday + # "takerFeeRate": "0.001", # Basic Taker Fee + # "makerFeeRate": "0.001", # Basic Maker Fee + # "takerCoefficient": "1", # Taker Fee Coefficient + # "makerCoefficient": "1" # Maker Fee Coefficient + # } + # + # { + # "trading": True, + # "symbol": "KCS-BTC", + # "buy": 0.00011, + # "sell": 0.00012, + # "sort": 100, + # "volValue": 3.13851792584, #total + # "baseCurrency": "KCS", + # "market": "BTC", + # "quoteCurrency": "BTC", + # "symbolCode": "KCS-BTC", + # "datetime": 1548388122031, + # "high": 0.00013, + # "vol": 27514.34842, + # "low": 0.0001, + # "changePrice": -1.0e-5, + # "changeRate": -0.0769, + # "lastTradedPrice": 0.00012, + # "board": 0, + # "mark": 0 + # } + # + # market/ticker ws subscription + # + # { + # "bestAsk": "62258.9", + # "bestAskSize": "0.38579986", + # "bestBid": "62258.8", + # "bestBidSize": "0.0078381", + # "price": "62260.7", + # "sequence": "1621383297064", + # "size": "0.00002841", + # "time": 1634641777363 + # } + # + # uta + # + # { + # "symbol": "BTC-USDT", + # "name": "BTC-USDT", + # "bestBidSize": "0.69207954", + # "bestBidPrice": "110417.5", + # "bestAskSize": "0.08836606", + # "bestAskPrice": "110417.6", + # "lastPrice": "110417.5", + # "size": "0.00016", + # "open": "110105.1", + # "high": "110838.9", + # "low": "109705.5", + # "baseVolume": "1882.10069442", + # "quoteVolume": "207325626.822922498" + # } + # + percentage = self.safe_string(ticker, 'changeRate') + if percentage is not None: + percentage = Precise.string_mul(percentage, '100') + last = self.safe_string_n(ticker, ['last', 'lastTradedPrice', 'lastPrice']) + last = self.safe_string(ticker, 'price', last) + marketId = self.safe_string(ticker, 'symbol') + market = self.safe_market(marketId, market, '-') + symbol = market['symbol'] + baseVolume = self.safe_string_2(ticker, 'vol', 'baseVolume') + quoteVolume = self.safe_string_2(ticker, 'volValue', 'quoteVolume') + timestamp = self.safe_integer_n(ticker, ['time', 'datetime', 'timePoint']) + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': self.safe_string_n(ticker, ['buy', 'bestBid', 'bestBidPrice']), + 'bidVolume': self.safe_string(ticker, 'bestBidSize'), + 'ask': self.safe_string_n(ticker, ['sell', 'bestAsk', 'bestAskPrice']), + 'askVolume': self.safe_string(ticker, 'bestAskSize'), + 'vwap': None, + 'open': self.safe_string(ticker, 'open'), + 'close': last, + 'last': last, + 'previousClose': None, + 'change': self.safe_string(ticker, 'changePrice'), + 'percentage': percentage, + 'average': self.safe_string(ticker, 'averagePrice'), + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'markPrice': self.safe_string(ticker, 'value'), + 'info': ticker, + }, market) + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://docs.kucoin.com/#get-all-tickers + https://www.kucoin.com/docs-new/rest/ua/get-ticker + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :param str [params.tradeType]: *uta only* set to SPOT or FUTURES + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + request: dict = {} + symbols = self.market_symbols(symbols) + uta = None + uta, params = self.handle_option_and_params(params, 'fetchTickers', 'uta', False) + response = None + if uta: + if symbols is not None: + symbol = self.safe_string(symbols, 0) + market = self.market(symbol) + type = None + type, params = self.handle_market_type_and_params('fetchTickers', market, params) + if type == 'spot': + request['tradeType'] = 'SPOT' + else: + request['tradeType'] = 'FUTURES' + else: + tradeType = self.safe_string_upper(params, 'tradeType') + if tradeType is None: + raise ArgumentsRequired(self.id + ' fetchTickers() requires a tradeType parameter for uta, either SPOT or FUTURES') + request['tradeType'] = tradeType + params = self.omit(params, 'tradeType') + response = self.utaGetMarketTicker(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "tradeType": "SPOT", + # "ts": 1762061290067, + # "list": [ + # { + # "symbol": "BTC-USDT", + # "name": "BTC-USDT", + # "bestBidSize": "0.69207954", + # "bestBidPrice": "110417.5", + # "bestAskSize": "0.08836606", + # "bestAskPrice": "110417.6", + # "lastPrice": "110417.5", + # "size": "0.00016", + # "open": "110105.1", + # "high": "110838.9", + # "low": "109705.5", + # "baseVolume": "1882.10069442", + # "quoteVolume": "207325626.822922498" + # } + # ] + # } + # } + # + else: + response = self.publicGetMarketAllTickers(params) + # + # { + # "code": "200000", + # "data": { + # "time":1602832092060, + # "ticker":[ + # { + # "symbol": "BTC-USDT", # symbol + # "symbolName":"BTC-USDT", # Name of trading pairs, it would change after renaming + # "buy": "11328.9", # bestAsk + # "sell": "11329", # bestBid + # "changeRate": "-0.0055", # 24h change rate + # "changePrice": "-63.6", # 24h change price + # "high": "11610", # 24h highest price + # "low": "11200", # 24h lowest price + # "vol": "2282.70993217", # 24h volume,the aggregated trading volume in BTC + # "volValue": "25984946.157790431", # 24h total, the trading volume in quote currency of last 24 hours + # "last": "11328.9", # last price + # "averagePrice": "11360.66065903", # 24h average transaction price yesterday + # "takerFeeRate": "0.001", # Basic Taker Fee + # "makerFeeRate": "0.001", # Basic Maker Fee + # "takerCoefficient": "1", # Taker Fee Coefficient + # "makerCoefficient": "1" # Maker Fee Coefficient + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + tickers = self.safe_list_2(data, 'ticker', 'list', []) + time = self.safe_integer_2(data, 'time', 'ts') + result: dict = {} + for i in range(0, len(tickers)): + tickers[i]['time'] = time + ticker = self.parse_ticker(tickers[i]) + symbol = self.safe_string(ticker, 'symbol') + if symbol is not None: + result[symbol] = ticker + return self.filter_by_array_tickers(result, 'symbol', symbols) + + def fetch_mark_prices(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches the mark price for multiple markets + + https://www.kucoin.com/docs/rest/margin-trading/margin-info/get-all-margin-trading-pairs-mark-prices + + :param str[] [symbols]: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + symbols = self.market_symbols(symbols) + response = self.publicGetMarkPriceAllSymbols(params) + data = self.safe_list(response, 'data', []) + return self.parse_tickers(data) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://docs.kucoin.com/#get-24hr-stats + https://www.kucoin.com/docs-new/rest/ua/get-ticker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + uta = None + uta, params = self.handle_option_and_params(params, 'fetchTicker', 'uta', False) + response = None + result = None + if uta: + type = None + type, params = self.handle_market_type_and_params('fetchTicker', market, params) + if type == 'spot': + request['tradeType'] = 'SPOT' + else: + request['tradeType'] = 'FUTURES' + response = self.utaGetMarketTicker(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "tradeType": "SPOT", + # "ts": 1762061290067, + # "list": [ + # { + # "symbol": "BTC-USDT", + # "name": "BTC-USDT", + # "bestBidSize": "0.69207954", + # "bestBidPrice": "110417.5", + # "bestAskSize": "0.08836606", + # "bestAskPrice": "110417.6", + # "lastPrice": "110417.5", + # "size": "0.00016", + # "open": "110105.1", + # "high": "110838.9", + # "low": "109705.5", + # "baseVolume": "1882.10069442", + # "quoteVolume": "207325626.822922498" + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + resultList = self.safe_list(data, 'list', []) + result = self.safe_dict(resultList, 0, {}) + else: + response = self.publicGetMarketStats(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "time": 1602832092060, # time + # "symbol": "BTC-USDT", # symbol + # "buy": "11328.9", # bestAsk + # "sell": "11329", # bestBid + # "changeRate": "-0.0055", # 24h change rate + # "changePrice": "-63.6", # 24h change price + # "high": "11610", # 24h highest price + # "low": "11200", # 24h lowest price + # "vol": "2282.70993217", # 24h volume,the aggregated trading volume in BTC + # "volValue": "25984946.157790431", # 24h total, the trading volume in quote currency of last 24 hours + # "last": "11328.9", # last price + # "averagePrice": "11360.66065903", # 24h average transaction price yesterday + # "takerFeeRate": "0.001", # Basic Taker Fee + # "makerFeeRate": "0.001", # Basic Maker Fee + # "takerCoefficient": "1", # Taker Fee Coefficient + # "makerCoefficient": "1" # Maker Fee Coefficient + # } + # } + # + result = self.safe_dict(response, 'data', {}) + return self.parse_ticker(result, market) + + def fetch_mark_price(self, symbol: str, params={}) -> Ticker: + """ + fetches the mark price for a specific market + + https://www.kucoin.com/docs/rest/margin-trading/margin-info/get-mark-price + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = self.publicGetMarkPriceSymbolCurrent(self.extend(request, params)) + # + data = self.safe_dict(response, 'data', {}) + return self.parse_ticker(data, market) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # [ + # "1545904980", # Start time of the candle cycle + # "0.058", # opening price + # "0.049", # closing price + # "0.058", # highest price + # "0.049", # lowest price + # "0.018", # base volume + # "0.000945", # quote volume + # ] + # + return [ + self.safe_timestamp(ohlcv, 0), + self.safe_number(ohlcv, 1), + self.safe_number(ohlcv, 3), + self.safe_number(ohlcv, 4), + self.safe_number(ohlcv, 2), + self.safe_number(ohlcv, 5), + ] + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://docs.kucoin.com/#get-klines + https://www.kucoin.com/docs-new/rest/ua/get-klines + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate') + if paginate: + return self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params, 1500) + market = self.market(symbol) + marketId = market['id'] + request: dict = { + 'symbol': marketId, + } + duration = self.parse_timeframe(timeframe) * 1000 + endAt = self.milliseconds() # required param + if since is not None: + request['startAt'] = self.parse_to_int(int(math.floor(since / 1000))) + if limit is None: + # https://docs.kucoin.com/#get-klines + # https://docs.kucoin.com/#details + # For each query, the system would return at most 1500 pieces of data. + # To obtain more data, please page the data by time. + limit = self.safe_integer(self.options, 'fetchOHLCVLimit', 1500) + endAt = self.sum(since, limit * duration) + elif limit is not None: + since = endAt - limit * duration + request['startAt'] = self.parse_to_int(int(math.floor(since / 1000))) + request['endAt'] = self.parse_to_int(int(math.floor(endAt / 1000))) + uta = None + uta, params = self.handle_option_and_params(params, 'fetchOHLCV', 'uta', False) + response = None + result = None + if uta: + type = None + type, params = self.handle_market_type_and_params('fetchOHLCV', market, params) + if type == 'spot': + request['tradeType'] = 'SPOT' + else: + request['tradeType'] = 'FUTURES' + request['interval'] = self.safe_string(self.timeframes, timeframe, timeframe) + response = self.utaGetMarketKline(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "tradeType": "SPOT", + # "symbol": "BTC-USDT", + # "list": [ + # ["1762240200","104581.4","104527.1","104620.1","104526.4","5.57665554","583263.661804122"], + # ["1762240140","104565.6","104581.3","104601.7","104511.3","6.48505114","677973.775916968"], + # ["1762240080","104621.5","104571.3","104704.7","104571.3","14.51713618","1519468.954060838"] + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + result = self.safe_list(data, 'list', []) + else: + request['type'] = self.safe_string(self.timeframes, timeframe, timeframe) + response = self.publicGetMarketCandles(self.extend(request, params)) + # + # { + # "code":"200000", + # "data":[ + # ["1591517700","0.025078","0.025069","0.025084","0.025064","18.9883256","0.4761861079404"], + # ["1591516800","0.025089","0.025079","0.025089","0.02506","99.4716622","2.494143499081"], + # ["1591515900","0.025079","0.02509","0.025091","0.025068","59.83701271","1.50060885172798"], + # ] + # } + # + result = self.safe_list(response, 'data', []) + return self.parse_ohlcvs(result, market, timeframe, since, limit) + + def create_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + + https://www.kucoin.com/docs/rest/funding/deposit/create-deposit-address-v3- + + create a currency deposit address + :param str code: unified currency code of the currency for the deposit address + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.network]: the blockchain network name + :returns dict: an `address structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + } + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + if networkCode is not None: + request['chain'] = self.network_code_to_id(networkCode) # docs mention "chain-name", but seems "chain-id" is used, like in "fetchDepositAddress" + response = self.privatePostDepositAddressCreate(self.extend(request, params)) + # {"code":"260000","msg":"Deposit address already exists."} + # + # { + # "code": "200000", + # "data": { + # "address": "0x2336d1834faab10b2dac44e468f2627138417431", + # "memo": null, + # "chainId": "bsc", + # "to": "MAIN", + # "expirationDate": 0, + # "currency": "BNB", + # "chainName": "BEP20" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_deposit_address(data, currency) + + def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://docs.kucoin.com/#get-deposit-addresses-v2 + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.network]: the blockchain network name + :returns dict: an `address structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + # for USDT - OMNI, ERC20, TRC20, default is ERC20 + # for BTC - Native, Segwit, TRC20, the parameters are bech32, btc, trx, default is Native + # 'chain': 'ERC20', # optional + } + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + if networkCode is not None: + request['chain'] = self.network_code_to_id(networkCode).lower() + version = self.options['versions']['private']['GET']['deposit-addresses'] + self.options['versions']['private']['GET']['deposit-addresses'] = 'v1' + response = self.privateGetDepositAddresses(self.extend(request, params)) + # BCH {"code":"200000","data":{"address":"bitcoincash:qza3m4nj9rx7l9r0cdadfqxts6f92shvhvr5ls4q7z","memo":""}} + # BTC {"code":"200000","data":{"address":"36SjucKqQpQSvsak9A7h6qzFjrVXpRNZhE","memo":""}} + self.options['versions']['private']['GET']['deposit-addresses'] = version + data = self.safe_value(response, 'data') + if data is None: + raise ExchangeError(self.id + ' fetchDepositAddress() returned an empty response, you might try to run createDepositAddress() first and try again') + return self.parse_deposit_address(data, currency) + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + address = self.safe_string(depositAddress, 'address') + # BCH/BSV is returned with a "bitcoincash:" prefix, which we cut off here and only keep the address + if address is not None: + address = address.replace('bitcoincash:', '') + code = None + if currency is not None: + code = self.safe_currency_code(currency['id']) + if code != 'NIM': + # contains spaces + self.check_address(address) + return { + 'info': depositAddress, + 'currency': code, + 'network': self.network_id_to_code(self.safe_string(depositAddress, 'chainId')), + 'address': address, + 'tag': self.safe_string(depositAddress, 'memo'), + } + + def fetch_deposit_addresses_by_network(self, code: str, params={}) -> List[DepositAddress]: + """ + + https://docs.kucoin.com/#get-deposit-addresses-v2 + + fetch the deposit address for a currency associated with self account + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an array of `address structures ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + } + version = self.options['versions']['private']['GET']['deposit-addresses'] + self.options['versions']['private']['GET']['deposit-addresses'] = 'v2' + response = self.privateGetDepositAddresses(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": [ + # { + # "address": "fr1qvus7d4d5fgxj5e7zvqe6yhxd7txm95h2and69r", + # "memo": "", + # "chain": "BTC-Segwit", + # "contractAddress": "" + # }, + # {"address":"37icNMEWbiF8ZkwUMxmfzMxi2A1MQ44bMn","memo":"","chain":"BTC","contractAddress":""}, + # {"address":"Deposit temporarily blocked","memo":"","chain":"TRC20","contractAddress":""} + # ] + # } + # + self.options['versions']['private']['GET']['deposit-addresses'] = version + chains = self.safe_list(response, 'data', []) + parsed = self.parse_deposit_addresses(chains, [currency['code']], False, { + 'currency': currency['code'], + }) + return self.index_by(parsed, 'network') + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://www.kucoin.com/docs/rest/spot-trading/market-data/get-part-order-book-aggregated- + https://www.kucoin.com/docs/rest/spot-trading/market-data/get-full-order-book-aggregated- + https://www.kucoin.com/docs-new/rest/ua/get-orderbook + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + level = self.safe_integer(params, 'level', 2) + request: dict = {'symbol': market['id']} + isAuthenticated = self.check_required_credentials(False) + uta = None + uta, params = self.handle_option_and_params(params, 'fetchOrderBook', 'uta', False) + response = None + if uta: + if limit is None: + raise ArgumentsRequired(self.id + ' fetchOrderBook() requires a limit argument for uta, either 20, 50, 100 or FULL') + request['limit'] = limit + request['symbol'] = market['id'] + type = None + type, params = self.handle_market_type_and_params('fetchOrderBook', market, params) + if type == 'spot': + request['tradeType'] = 'SPOT' + else: + request['tradeType'] = 'FUTURES' + response = self.utaGetMarketOrderbook(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "tradeType": "SPOT", + # "symbol": "BTC-USDT", + # "sequence": "23136002402", + # "bids": [ + # ["104700","10.25940068"], + # ["104698.9","0.00057076"], + # ], + # "asks": [ + # ["104700.1","1.4082106"], + # ["104700.5","0.02866269"], + # ] + # } + # } + # + elif not isAuthenticated or limit is not None: + if level == 2: + request['level'] = level + if limit is not None: + if (limit == 20) or (limit == 100): + request['limit'] = limit + else: + raise ExchangeError(self.id + ' fetchOrderBook() limit argument must be 20 or 100') + request['limit'] = limit if limit else 100 + response = self.publicGetMarketOrderbookLevelLevelLimit(self.extend(request, params)) + else: + response = self.privateGetMarketOrderbookLevel2(self.extend(request, params)) + # + # public(v1) market/orderbook/level2_20 and market/orderbook/level2_100 + # + # { + # "sequence": "3262786978", + # "time": 1550653727731, + # "bids": [ + # ["6500.12", "0.45054140"], + # ["6500.11", "0.45054140"], + # ], + # "asks": [ + # ["6500.16", "0.57753524"], + # ["6500.15", "0.57753524"], + # ] + # } + # + # private(v3) market/orderbook/level2 + # + # { + # "sequence": "3262786978", + # "time": 1550653727731, + # "bids": [ + # ["6500.12", "0.45054140"], + # ["6500.11", "0.45054140"], + # ], + # "asks": [ + # ["6500.16", "0.57753524"], + # ["6500.15", "0.57753524"], + # ] + # } + # + data = self.safe_dict(response, 'data', {}) + timestamp = self.safe_integer(data, 'time') + orderbook = self.parse_order_book(data, market['symbol'], timestamp, 'bids', 'asks', level - 2, level - 1) + orderbook['nonce'] = self.safe_integer(data, 'sequence') + return orderbook + + def handle_trigger_prices(self, params): + triggerPrice = self.safe_value_2(params, 'triggerPrice', 'stopPrice') + stopLossPrice = self.safe_value(params, 'stopLossPrice') + takeProfitPrice = self.safe_value(params, 'takeProfitPrice') + isStopLoss = stopLossPrice is not None + isTakeProfit = takeProfitPrice is not None + if (isStopLoss and isTakeProfit) or (triggerPrice and stopLossPrice) or (triggerPrice and isTakeProfit): + raise ExchangeError(self.id + ' createOrder() - you should use either triggerPrice or stopLossPrice or takeProfitPrice') + return [triggerPrice, stopLossPrice, takeProfitPrice] + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + Create an order on the exchange + + https://docs.kucoin.com/spot#place-a-new-order + https://docs.kucoin.com/spot#place-a-new-order-2 + https://docs.kucoin.com/spot#place-a-margin-order + https://docs.kucoin.com/spot-hf/#place-hf-order + https://www.kucoin.com/docs/rest/spot-trading/orders/place-order-test + https://www.kucoin.com/docs/rest/margin-trading/orders/place-margin-order-test + https://www.kucoin.com/docs/rest/spot-trading/spot-hf-trade-pro-account/sync-place-hf-order + + :param str symbol: Unified CCXT market symbol + :param str type: 'limit' or 'market' + :param str side: 'buy' or 'sell' + :param float amount: the amount of currency to trade + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: The price at which a trigger order is triggered at + :param str [params.marginMode]: 'cross', # cross(cross mode) and isolated(isolated mode), set to cross by default, the isolated mode will be released soon, stay tuned + :param str [params.timeInForce]: GTC, GTT, IOC, or FOK, default is GTC, limit orders only + :param str [params.postOnly]: Post only flag, invalid when timeInForce is IOC or FOK + + EXCHANGE SPECIFIC PARAMETERS + :param str [params.clientOid]: client order id, defaults to uuid if not passed + :param str [params.remark]: remark for the order, length cannot exceed 100 utf8 characters + :param str [params.tradeType]: 'TRADE', # TRADE, MARGIN_TRADE # not used with margin orders + limit orders --------------------------------------------------- + :param float [params.cancelAfter]: long, # cancel after n seconds, requires timeInForce to be GTT + :param bool [params.hidden]: False, # Order will not be displayed in the order book + :param bool [params.iceberg]: False, # Only a portion of the order is displayed in the order book + :param str [params.visibleSize]: self.amount_to_precision(symbol, visibleSize), # The maximum visible size of an iceberg order + market orders -------------------------------------------------- + :param str [params.funds]: # Amount of quote currency to use + stop orders ---------------------------------------------------- + :param str [params.stop]: Either loss or entry, the default is loss. Requires triggerPrice to be defined + margin orders -------------------------------------------------- + :param float [params.leverage]: Leverage size of the order + :param str [params.stp]: '', # self trade prevention, CN, CO, CB or DC + :param bool [params.autoBorrow]: False, # The system will first borrow you funds at the optimal interest rate and then place an order for you + :param bool [params.hf]: False, # True for hf order + :param bool [params.test]: set to True to test an order, no order will be created but the request will be validated + :param bool [params.sync]: set to True to use the hf sync call + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + testOrder = self.safe_bool(params, 'test', False) + params = self.omit(params, 'test') + hf = None + hf, params = self.handle_hf_and_params(params) + useSync = False + useSync, params = self.handle_option_and_params(params, 'createOrder', 'sync', False) + triggerPrice, stopLossPrice, takeProfitPrice = self.handle_trigger_prices(params) + tradeType = self.safe_string(params, 'tradeType') # keep it for backward compatibility + isTriggerOrder = (triggerPrice or stopLossPrice or takeProfitPrice) + marginResult = self.handle_margin_mode_and_params('createOrder', params) + marginMode = self.safe_string(marginResult, 0) + isMarginOrder = tradeType == 'MARGIN_TRADE' or marginMode is not None + # don't omit anything before calling createOrderRequest + orderRequest = self.create_order_request(symbol, type, side, amount, price, params) + response = None + if testOrder: + if isMarginOrder: + response = self.privatePostMarginOrderTest(orderRequest) + elif hf: + response = self.privatePostHfOrdersTest(orderRequest) + else: + response = self.privatePostOrdersTest(orderRequest) + elif isTriggerOrder: + response = self.privatePostStopOrder(orderRequest) + elif isMarginOrder: + response = self.privatePostMarginOrder(orderRequest) + elif useSync: + response = self.privatePostHfOrdersSync(orderRequest) + elif hf: + response = self.privatePostHfOrders(orderRequest) + else: + response = self.privatePostOrders(orderRequest) + # + # { + # "code": "200000", + # "data": { + # "orderId": "5bd6e9286d99522a52e458de" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data, market) + + def create_market_order_with_cost(self, symbol: str, side: OrderSide, cost: float, params={}): + """ + create a market order by providing the symbol, side and cost + + https://www.kucoin.com/docs/rest/spot-trading/orders/place-order + + :param str symbol: unified symbol of the market to create an order in + :param str side: 'buy' or 'sell' + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + req = { + 'cost': cost, + } + return self.create_order(symbol, 'market', side, cost, None, self.extend(req, params)) + + def create_market_buy_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market buy order by providing the symbol and cost + + https://www.kucoin.com/docs/rest/spot-trading/orders/place-order + + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + return self.create_market_order_with_cost(symbol, 'buy', cost, params) + + def create_market_sell_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market sell order by providing the symbol and cost + + https://www.kucoin.com/docs/rest/spot-trading/orders/place-order + + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + return self.create_market_order_with_cost(symbol, 'sell', cost, params) + + def create_orders(self, orders: List[OrderRequest], params={}): + """ + create a list of trade orders + + https://www.kucoin.com/docs/rest/spot-trading/orders/place-multiple-orders + https://www.kucoin.com/docs/rest/spot-trading/spot-hf-trade-pro-account/place-multiple-hf-orders + https://www.kucoin.com/docs/rest/spot-trading/spot-hf-trade-pro-account/sync-place-multiple-hf-orders + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.hf]: False, # True for hf orders + :param bool [params.sync]: False, # True to use the hf sync call + :returns dict: an `order structure ` + """ + self.load_markets() + ordersRequests = [] + symbol = None + for i in range(0, len(orders)): + rawOrder = orders[i] + marketId = self.safe_string(rawOrder, 'symbol') + if symbol is None: + symbol = marketId + else: + if symbol != marketId: + raise BadRequest(self.id + ' createOrders() requires all orders to have the same symbol') + type = self.safe_string(rawOrder, 'type') + if type != 'limit': + raise BadRequest(self.id + ' createOrders() only supports limit orders') + side = self.safe_string(rawOrder, 'side') + amount = self.safe_value(rawOrder, 'amount') + price = self.safe_value(rawOrder, 'price') + orderParams = self.safe_value(rawOrder, 'params', {}) + orderRequest = self.create_order_request(marketId, type, side, amount, price, orderParams) + ordersRequests.append(orderRequest) + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'orderList': ordersRequests, + } + hf = None + hf, params = self.handle_hf_and_params(params) + useSync = False + useSync, params = self.handle_option_and_params(params, 'createOrders', 'sync', False) + response = None + if useSync: + response = self.privatePostHfOrdersMultiSync(self.extend(request, params)) + elif hf: + response = self.privatePostHfOrdersMulti(self.extend(request, params)) + else: + response = self.privatePostOrdersMulti(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "data": [ + # { + # "symbol": "LTC-USDT", + # "type": "limit", + # "side": "sell", + # "price": "90", + # "size": "0.1", + # "funds": null, + # "stp": "", + # "stop": "", + # "stopPrice": null, + # "timeInForce": "GTC", + # "cancelAfter": 0, + # "postOnly": False, + # "hidden": False, + # "iceberge": False, + # "iceberg": False, + # "visibleSize": null, + # "channel": "API", + # "id": "6539148443fcf500079d15e5", + # "status": "success", + # "failMsg": null, + # "clientOid": "5c4c5398-8ab2-4b4e-af8a-e2d90ad2488f" + # }, + # } + # + data = self.safe_dict(response, 'data', {}) + data = self.safe_list(data, 'data', []) + return self.parse_orders(data) + + def market_order_amount_to_precision(self, symbol: str, amount): + market = self.market(symbol) + result = self.decimal_to_precision(amount, TRUNCATE, market['info']['quoteIncrement'], self.precisionMode, self.paddingMode) + if result == '0': + raise InvalidOrder(self.id + ' amount of ' + market['symbol'] + ' must be greater than minimum amount precision of ' + self.number_to_string(market['precision']['amount'])) + return result + + def create_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + market = self.market(symbol) + # required param, cannot be used twice + clientOrderId = self.safe_string_2(params, 'clientOid', 'clientOrderId', self.uuid()) + params = self.omit(params, ['clientOid', 'clientOrderId']) + request: dict = { + 'clientOid': clientOrderId, + 'side': side, + 'symbol': market['id'], + 'type': type, # limit or market + } + quoteAmount = self.safe_number_2(params, 'cost', 'funds') + amountString = None + costString = None + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('createOrder', params) + if type == 'market': + if quoteAmount is not None: + params = self.omit(params, ['cost', 'funds']) + # kucoin uses base precision even for quote values + costString = self.market_order_amount_to_precision(symbol, quoteAmount) + request['funds'] = costString + else: + amountString = self.amount_to_precision(symbol, amount) + request['size'] = self.amount_to_precision(symbol, amount) + else: + amountString = self.amount_to_precision(symbol, amount) + request['size'] = amountString + request['price'] = self.price_to_precision(symbol, price) + tradeType = self.safe_string(params, 'tradeType') # keep it for backward compatibility + triggerPrice, stopLossPrice, takeProfitPrice = self.handle_trigger_prices(params) + isTriggerOrder = (triggerPrice or stopLossPrice or takeProfitPrice) + isMarginOrder = tradeType == 'MARGIN_TRADE' or marginMode is not None + params = self.omit(params, ['stopLossPrice', 'takeProfitPrice', 'triggerPrice', 'stopPrice']) + if isTriggerOrder: + if triggerPrice: + request['stopPrice'] = self.price_to_precision(symbol, triggerPrice) + elif stopLossPrice or takeProfitPrice: + if stopLossPrice: + request['stop'] = 'entry' if (side == 'buy') else 'loss' + request['stopPrice'] = self.price_to_precision(symbol, stopLossPrice) + else: + request['stop'] = 'loss' if (side == 'buy') else 'entry' + request['stopPrice'] = self.price_to_precision(symbol, takeProfitPrice) + if marginMode == 'isolated': + raise BadRequest(self.id + ' createOrder does not support isolated margin for stop orders') + elif marginMode == 'cross': + request['tradeType'] = self.options['marginModes'][marginMode] + elif isMarginOrder: + if marginMode == 'isolated': + request['marginModel'] = 'isolated' + postOnly = None + postOnly, params = self.handle_post_only(type == 'market', False, params) + if postOnly: + request['postOnly'] = True + return self.extend(request, params) + + def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + """ + edit an order, kucoin currently only supports the modification of HF orders + + https://docs.kucoin.com/spot-hf/#modify-order + + :param str id: order id + :param str symbol: unified symbol of the market to create an order in + :param str type: not used + :param str side: not used + :param float amount: how much of the currency you want to trade in units of the base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.clientOrderId]: client order id, defaults to id if not passed + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + clientOrderId = self.safe_string_2(params, 'clientOid', 'clientOrderId') + if clientOrderId is not None: + request['clientOid'] = clientOrderId + else: + request['orderId'] = id + if amount is not None: + request['newSize'] = self.amount_to_precision(symbol, amount) + if price is not None: + request['newPrice'] = self.price_to_precision(symbol, price) + response = self.privatePostHfOrdersAlter(self.extend(request, params)) + # + # { + # "code":"200000", + # "data":{ + # "newOrderId":"6478d7a6c883280001e92d8b" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data, market) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://docs.kucoin.com/spot#cancel-an-order + https://docs.kucoin.com/spot#cancel-an-order-2 + https://docs.kucoin.com/spot#cancel-single-order-by-clientoid + https://docs.kucoin.com/spot#cancel-single-order-by-clientoid-2 + https://docs.kucoin.com/spot-hf/#cancel-orders-by-orderid + https://docs.kucoin.com/spot-hf/#cancel-order-by-clientoid + https://www.kucoin.com/docs/rest/spot-trading/spot-hf-trade-pro-account/sync-cancel-hf-order-by-orderid + https://www.kucoin.com/docs/rest/spot-trading/spot-hf-trade-pro-account/sync-cancel-hf-order-by-clientoid + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.trigger]: True if cancelling a stop order + :param bool [params.hf]: False, # True for hf order + :param bool [params.sync]: False, # True to use the hf sync call + :returns: Response from the exchange + """ + self.load_markets() + request: dict = {} + clientOrderId = self.safe_string_2(params, 'clientOid', 'clientOrderId') + trigger = self.safe_bool_2(params, 'stop', 'trigger', False) + hf = None + hf, params = self.handle_hf_and_params(params) + useSync = False + useSync, params = self.handle_option_and_params(params, 'cancelOrder', 'sync', False) + if hf or useSync: + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol parameter for hf orders') + market = self.market(symbol) + request['symbol'] = market['id'] + response = None + params = self.omit(params, ['clientOid', 'clientOrderId', 'stop', 'trigger']) + if clientOrderId is not None: + request['clientOid'] = clientOrderId + if trigger: + response = self.privateDeleteStopOrderCancelOrderByClientOid(self.extend(request, params)) + # + # { + # code: '200000', + # data: { + # cancelledOrderId: 'vs8lgpiuao41iaft003khbbk', + # clientOid: '123456' + # } + # } + # + elif useSync: + response = self.privateDeleteHfOrdersSyncClientOrderClientOid(self.extend(request, params)) + elif hf: + response = self.privateDeleteHfOrdersClientOrderClientOid(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "clientOid": "6d539dc614db3" + # } + # } + # + else: + response = self.privateDeleteOrderClientOrderClientOid(self.extend(request, params)) + # + # { + # code: '200000', + # data: { + # cancelledOrderId: '665e580f6660500007aba341', + # clientOid: '1234567', + # cancelledOcoOrderIds: null + # } + # } + # + response = self.safe_dict(response, 'data') + return self.parse_order(response) + else: + request['orderId'] = id + if trigger: + response = self.privateDeleteStopOrderOrderId(self.extend(request, params)) + # + # { + # code: '200000', + # data: {cancelledOrderIds: ['vs8lgpiuaco91qk8003vebu9']} + # } + # + elif useSync: + response = self.privateDeleteHfOrdersSyncOrderId(self.extend(request, params)) + elif hf: + response = self.privateDeleteHfOrdersOrderId(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "orderId": "630625dbd9180300014c8d52" + # } + # } + # + response = self.safe_dict(response, 'data') + return self.parse_order(response) + else: + response = self.privateDeleteOrdersOrderId(self.extend(request, params)) + # + # { + # code: '200000', + # data: {cancelledOrderIds: ['665e4fbe28051a0007245c41']} + # } + # + data = self.safe_dict(response, 'data') + orderIds = self.safe_list(data, 'cancelledOrderIds', []) + orderId = self.safe_string(orderIds, 0) + return self.safe_order({ + 'info': data, + 'id': orderId, + }) + + def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders + + https://docs.kucoin.com/spot#cancel-all-orders + https://docs.kucoin.com/spot#cancel-orders + https://docs.kucoin.com/spot-hf/#cancel-all-hf-orders-by-symbol + + :param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.trigger]: *invalid for isolated margin* True if cancelling all stop orders + :param str [params.marginMode]: 'cross' or 'isolated' + :param str [params.orderIds]: *stop orders only* Comma seperated order IDs + :param bool [params.hf]: False, # True for hf order + :returns: Response from the exchange + """ + self.load_markets() + request: dict = {} + trigger = self.safe_bool_2(params, 'trigger', 'stop', False) + hf = None + hf, params = self.handle_hf_and_params(params) + params = self.omit(params, 'stop') + marginMode, query = self.handle_margin_mode_and_params('cancelAllOrders', params) + if symbol is not None: + request['symbol'] = self.market_id(symbol) + if marginMode is not None: + request['tradeType'] = self.options['marginModes'][marginMode] + if marginMode == 'isolated' and trigger: + raise BadRequest(self.id + ' cancelAllOrders does not support isolated margin for stop orders') + response = None + if trigger: + response = self.privateDeleteStopOrderCancel(self.extend(request, query)) + elif hf: + if symbol is None: + response = self.privateDeleteHfOrdersCancelAll(self.extend(request, query)) + else: + response = self.privateDeleteHfOrders(self.extend(request, query)) + else: + response = self.privateDeleteOrders(self.extend(request, query)) + return [self.safe_order({'info': response})] + + def fetch_orders_by_status(self, status, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch a list of orders + + https://docs.kucoin.com/spot#list-orders + https://docs.kucoin.com/spot#list-stop-orders + https://docs.kucoin.com/spot-hf/#obtain-list-of-active-hf-orders + https://docs.kucoin.com/spot-hf/#obtain-list-of-filled-hf-orders + + :param str status: *not used for stop orders* 'open' or 'closed' + :param str symbol: unified market symbol + :param int [since]: timestamp in ms of the earliest order + :param int [limit]: max number of orders to return + :param dict [params]: exchange specific params + :param int [params.until]: end time in ms + :param str [params.side]: buy or sell + :param str [params.type]: limit, market, limit_stop or market_stop + :param str [params.tradeType]: TRADE for spot trading, MARGIN_TRADE for Margin Trading + :param int [params.currentPage]: *trigger orders only* current page + :param str [params.orderIds]: *trigger orders only* comma seperated order ID list + :param bool [params.trigger]: True if fetching a trigger order + :param bool [params.hf]: False, # True for hf order + :returns: An `array of order structures ` + """ + self.load_markets() + lowercaseStatus = status.lower() + until = self.safe_integer(params, 'until') + trigger = self.safe_bool_2(params, 'stop', 'trigger', False) + hf = None + hf, params = self.handle_hf_and_params(params) + if hf and (symbol is None): + raise ArgumentsRequired(self.id + ' fetchOrdersByStatus() requires a symbol parameter for hf orders') + params = self.omit(params, ['stop', 'trigger', 'till', 'until']) + marginMode, query = self.handle_margin_mode_and_params('fetchOrdersByStatus', params) + if lowercaseStatus == 'open': + lowercaseStatus = 'active' + elif lowercaseStatus == 'closed': + lowercaseStatus = 'done' + request: dict = { + 'status': lowercaseStatus, + } + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if since is not None: + request['startAt'] = since + if limit is not None: + request['pageSize'] = limit + if until: + request['endAt'] = until + request['tradeType'] = self.safe_string(self.options['marginModes'], marginMode, 'TRADE') + response = None + if trigger: + response = self.privateGetStopOrder(self.extend(request, query)) + elif hf: + if lowercaseStatus == 'active': + response = self.privateGetHfOrdersActive(self.extend(request, query)) + elif lowercaseStatus == 'done': + response = self.privateGetHfOrdersDone(self.extend(request, query)) + else: + response = self.privateGetOrders(self.extend(request, query)) + # + # { + # "code": "200000", + # "data": { + # "currentPage": 1, + # "pageSize": 1, + # "totalNum": 153408, + # "totalPage": 153408, + # "items": [ + # { + # "id": "5c35c02703aa673ceec2a168", #orderid + # "symbol": "BTC-USDT", #symbol + # "opType": "DEAL", # operation type,deal is pending order,cancel is cancel order + # "type": "limit", # order type,e.g. limit,markrt,stop_limit. + # "side": "buy", # transaction direction,include buy and sell + # "price": "10", # order price + # "size": "2", # order quantity + # "funds": "0", # order funds + # "dealFunds": "0.166", # deal funds + # "dealSize": "2", # deal quantity + # "fee": "0", # fee + # "feeCurrency": "USDT", # charge fee currency + # "stp": "", # self trade prevention,include CN,CO,DC,CB + # "stop": "", # stop type + # "stopTriggered": False, # stop order is triggered + # "stopPrice": "0", # stop price + # "timeInForce": "GTC", # time InForce,include GTC,GTT,IOC,FOK + # "postOnly": False, # postOnly + # "hidden": False, # hidden order + # "iceberg": False, # iceberg order + # "visibleSize": "0", # display quantity for iceberg order + # "cancelAfter": 0, # cancel orders time,requires timeInForce to be GTT + # "channel": "IOS", # order source + # "clientOid": "", # user-entered order unique mark + # "remark": "", # remark + # "tags": "", # tag order source + # "isActive": False, # status before unfilled or uncancelled + # "cancelExist": False, # order cancellation transaction record + # "createdAt": 1547026471000 # time + # }, + # ] + # } + # } + listData = self.safe_list(response, 'data') + if listData is not None: + return self.parse_orders(listData, market, since, limit) + responseData = self.safe_dict(response, 'data', {}) + orders = self.safe_list(responseData, 'items', []) + return self.parse_orders(orders, market, since, limit) + + def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://docs.kucoin.com/spot#list-orders + https://docs.kucoin.com/spot#list-stop-orders + https://docs.kucoin.com/spot-hf/#obtain-list-of-active-hf-orders + https://docs.kucoin.com/spot-hf/#obtain-list-of-filled-hf-orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: end time in ms + :param str [params.side]: buy or sell + :param str [params.type]: limit, market, limit_stop or market_stop + :param str [params.tradeType]: TRADE for spot trading, MARGIN_TRADE for Margin Trading + :param bool [params.trigger]: True if fetching a trigger order + :param bool [params.hf]: False, # True for hf order + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchClosedOrders', 'paginate') + if paginate: + return self.fetch_paginated_call_dynamic('fetchClosedOrders', symbol, since, limit, params) + return self.fetch_orders_by_status('done', symbol, since, limit, params) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://docs.kucoin.com/spot#list-orders + https://docs.kucoin.com/spot#list-stop-orders + https://docs.kucoin.com/spot-hf/#obtain-list-of-active-hf-orders + https://docs.kucoin.com/spot-hf/#obtain-list-of-filled-hf-orders + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: end time in ms + :param bool [params.trigger]: True if fetching trigger orders + :param str [params.side]: buy or sell + :param str [params.type]: limit, market, limit_stop or market_stop + :param str [params.tradeType]: TRADE for spot trading, MARGIN_TRADE for Margin Trading + :param int [params.currentPage]: *trigger orders only* current page + :param str [params.orderIds]: *trigger orders only* comma seperated order ID list + :param bool [params.hf]: False, # True for hf order + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOpenOrders', 'paginate') + if paginate: + return self.fetch_paginated_call_dynamic('fetchOpenOrders', symbol, since, limit, params) + return self.fetch_orders_by_status('active', symbol, since, limit, params) + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetch an order + + https://docs.kucoin.com/spot#get-an-order + https://docs.kucoin.com/spot#get-single-active-order-by-clientoid + https://docs.kucoin.com/spot#get-single-order-info + https://docs.kucoin.com/spot#get-single-order-by-clientoid + https://docs.kucoin.com/spot-hf/#details-of-a-single-hf-order + https://docs.kucoin.com/spot-hf/#obtain-details-of-a-single-hf-order-using-clientoid + + :param str id: Order id + :param str symbol: not sent to exchange except for trigger orders with clientOid, but used internally by CCXT to filter + :param dict [params]: exchange specific parameters + :param bool [params.trigger]: True if fetching a trigger order + :param bool [params.hf]: False, # True for hf order + :param bool [params.clientOid]: unique order id created by users to identify their orders + :returns: An `order structure ` + """ + self.load_markets() + request: dict = {} + clientOrderId = self.safe_string_2(params, 'clientOid', 'clientOrderId') + trigger = self.safe_bool_2(params, 'stop', 'trigger', False) + hf = None + hf, params = self.handle_hf_and_params(params) + market = None + if symbol is not None: + market = self.market(symbol) + if hf: + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrder() requires a symbol parameter for hf orders') + request['symbol'] = market['id'] + params = self.omit(params, ['stop', 'clientOid', 'clientOrderId', 'trigger']) + response = None + if clientOrderId is not None: + request['clientOid'] = clientOrderId + if trigger: + if symbol is not None: + request['symbol'] = market['id'] + response = self.privateGetStopOrderQueryOrderByClientOid(self.extend(request, params)) + elif hf: + response = self.privateGetHfOrdersClientOrderClientOid(self.extend(request, params)) + else: + response = self.privateGetOrderClientOrderClientOid(self.extend(request, params)) + else: + # a special case for None ids + # otherwise a wrong endpoint for all orders will be triggered + # https://github.com/ccxt/ccxt/issues/7234 + if id is None: + raise InvalidOrder(self.id + ' fetchOrder() requires an order id') + request['orderId'] = id + if trigger: + response = self.privateGetStopOrderOrderId(self.extend(request, params)) + elif hf: + response = self.privateGetHfOrdersOrderId(self.extend(request, params)) + else: + response = self.privateGetOrdersOrderId(self.extend(request, params)) + responseData = self.safe_dict(response, 'data', {}) + if isinstance(responseData, list): + responseData = self.safe_value(responseData, 0) + return self.parse_order(responseData, market) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder + # + # { + # "orderId": "63c97e47d686c5000159a656" + # } + # + # cancelOrder + # + # { + # "cancelledOrderIds": ["63c97e47d686c5000159a656"] + # } + # + # fetchOpenOrders, fetchClosedOrders + # + # { + # "id": "63c97ce8d686c500015793bb", + # "symbol": "USDC-USDT", + # "opType": "DEAL", + # "type": "limit", + # "side": "sell", + # "price": "1.05", + # "size": "1", + # "funds": "0", + # "dealFunds": "0", + # "dealSize": "0", + # "fee": "0", + # "feeCurrency": "USDT", + # "stp": "", + # "stop": "", + # "stopTriggered": False, + # "stopPrice": "0", + # "timeInForce": "GTC", + # "postOnly": False, + # "hidden": False, + # "iceberg": False, + # "visibleSize": "0", + # "cancelAfter": 0, + # "channel": "API", + # "clientOid": "d602d73f-5424-4751-bef0-8debce8f0a82", + # "remark": null, + # "tags": "partner:ccxt", + # "isActive": True, + # "cancelExist": False, + # "createdAt": 1674149096927, + # "tradeType": "TRADE" + # } + # + # stop orders(fetchOpenOrders, fetchClosedOrders) + # + # { + # "id": "vs9f6ou9e864rgq8000t4qnm", + # "symbol": "USDC-USDT", + # "userId": "613a896885d8660006151f01", + # "status": "NEW", + # "type": "market", + # "side": "sell", + # "price": null, + # "size": "1.00000000000000000000", + # "funds": null, + # "stp": null, + # "timeInForce": "GTC", + # "cancelAfter": -1, + # "postOnly": False, + # "hidden": False, + # "iceberg": False, + # "visibleSize": null, + # "channel": "API", + # "clientOid": "5d3fd727-6456-438d-9550-40d9d85eee0b", + # "remark": null, + # "tags": "partner:ccxt", + # "relatedNo": null, + # "orderTime": 1674146316994000028, + # "domainId": "kucoin", + # "tradeSource": "USER", + # "tradeType": "MARGIN_TRADE", + # "feeCurrency": "USDT", + # "takerFeeRate": "0.00100000000000000000", + # "makerFeeRate": "0.00100000000000000000", + # "createdAt": 1674146316994, + # "stop": "loss", + # "stopTriggerTime": null, + # "stopPrice": "0.97000000000000000000" + # } + # hf order + # { + # "id":"6478cf1439bdfc0001528a1d", + # "symbol":"LTC-USDT", + # "opType":"DEAL", + # "type":"limit", + # "side":"buy", + # "price":"50", + # "size":"0.1", + # "funds":"5", + # "dealSize":"0", + # "dealFunds":"0", + # "fee":"0", + # "feeCurrency":"USDT", + # "stp":null, + # "timeInForce":"GTC", + # "postOnly":false, + # "hidden":false, + # "iceberg":false, + # "visibleSize":"0", + # "cancelAfter":0, + # "channel":"API", + # "clientOid":"d4d2016b-8e3a-445c-aa5d-dc6df5d1678d", + # "remark":null, + # "tags":"partner:ccxt", + # "cancelExist":false, + # "createdAt":1685638932074, + # "lastUpdatedAt":1685639013735, + # "tradeType":"TRADE", + # "inOrderBook":true, + # "cancelledSize":"0", + # "cancelledFunds":"0", + # "remainSize":"0.1", + # "remainFunds":"5", + # "active":true + # } + # + marketId = self.safe_string(order, 'symbol') + timestamp = self.safe_integer(order, 'createdAt') + feeCurrencyId = self.safe_string(order, 'feeCurrency') + cancelExist = self.safe_bool(order, 'cancelExist', False) + responseStop = self.safe_string(order, 'stop') + trigger = responseStop is not None + stopTriggered = self.safe_bool(order, 'stopTriggered', False) + isActive = self.safe_bool_2(order, 'isActive', 'active') + responseStatus = self.safe_string(order, 'status') + status = None + if isActive is not None: + if isActive is True: + status = 'open' + else: + status = 'closed' + if trigger: + if responseStatus == 'NEW': + status = 'open' + elif not isActive and not stopTriggered: + status = 'cancelled' + if cancelExist: + status = 'canceled' + if responseStatus == 'fail': + status = 'rejected' + return self.safe_order({ + 'info': order, + 'id': self.safe_string_n(order, ['id', 'orderId', 'newOrderId', 'cancelledOrderId']), + 'clientOrderId': self.safe_string(order, 'clientOid'), + 'symbol': self.safe_symbol(marketId, market, '-'), + 'type': self.safe_string(order, 'type'), + 'timeInForce': self.safe_string(order, 'timeInForce'), + 'postOnly': self.safe_bool(order, 'postOnly'), + 'side': self.safe_string(order, 'side'), + 'amount': self.safe_string(order, 'size'), + 'price': self.safe_string(order, 'price'), # price is zero for market order, omitZero is called in safeOrder2 + 'triggerPrice': self.safe_number(order, 'stopPrice'), + 'cost': self.safe_string(order, 'dealFunds'), + 'filled': self.safe_string(order, 'dealSize'), + 'remaining': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'fee': { + 'currency': self.safe_currency_code(feeCurrencyId), + 'cost': self.safe_number(order, 'fee'), + }, + 'status': status, + 'lastTradeTimestamp': None, + 'average': self.safe_string(order, 'avgDealPrice'), + 'trades': None, + }, market) + + def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all the trades made from a single order + + https://docs.kucoin.com/#list-fills + https://docs.kucoin.com/spot-hf/#transaction-details + + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + request: dict = { + 'orderId': id, + } + return self.fetch_my_trades(symbol, since, limit, self.extend(request, params)) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + + https://docs.kucoin.com/#list-fills + https://docs.kucoin.com/spot-hf/#transaction-details + + fetch all trades made by the user + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch entries for + :param bool [params.hf]: False, # True for hf order + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchMyTrades', 'paginate') + if paginate: + return self.fetch_paginated_call_dynamic('fetchMyTrades', symbol, since, limit, params) + request: dict = {} + hf = None + hf, params = self.handle_hf_and_params(params) + if hf and symbol is None: + raise ArgumentsRequired(self.id + ' fetchMyTrades() requires a symbol parameter for hf orders') + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + method = self.options['fetchMyTradesMethod'] + parseResponseData = False + response = None + request, params = self.handle_until_option('endAt', request, params) + if hf: + # does not return trades earlier than 2019-02-18T00:00:00Z + if limit is not None: + request['limit'] = limit + if since is not None: + # only returns trades up to one week after the since param + request['startAt'] = since + response = self.privateGetHfFills(self.extend(request, params)) + elif method == 'private_get_fills': + # does not return trades earlier than 2019-02-18T00:00:00Z + if since is not None: + # only returns trades up to one week after the since param + request['startAt'] = since + response = self.privateGetFills(self.extend(request, params)) + elif method == 'private_get_limit_fills': + # does not return trades earlier than 2019-02-18T00:00:00Z + # takes no params + # only returns first 1000 trades(not only "in the last 24 hours" in the docs) + parseResponseData = True + response = self.privateGetLimitFills(self.extend(request, params)) + else: + raise ExchangeError(self.id + ' fetchMyTradesMethod() invalid method') + # + # { + # "currentPage": 1, + # "pageSize": 50, + # "totalNum": 1, + # "totalPage": 1, + # "items": [ + # { + # "symbol":"BTC-USDT", # symbol + # "tradeId":"5c35c02709e4f67d5266954e", # trade id + # "orderId":"5c35c02703aa673ceec2a168", # order id + # "counterOrderId":"5c1ab46003aa676e487fa8e3", # counter order id + # "side":"buy", # transaction direction,include buy and sell + # "liquidity":"taker", # include taker and maker + # "forceTaker":true, # forced to become taker + # "price":"0.083", # order price + # "size":"0.8424304", # order quantity + # "funds":"0.0699217232", # order funds + # "fee":"0", # fee + # "feeRate":"0", # fee rate + # "feeCurrency":"USDT", # charge fee currency + # "stop":"", # stop type + # "type":"limit", # order type, e.g. limit, market, stop_limit. + # "createdAt":1547026472000 # time + # }, + # #------------------------------------------------------ + # # v1(historical) trade response structure + # { + # "symbol": "SNOV-ETH", + # "dealPrice": "0.0000246", + # "dealValue": "0.018942", + # "amount": "770", + # "fee": "0.00001137", + # "side": "sell", + # "createdAt": 1540080199 + # "id":"5c4d389e4c8c60413f78e2e5", + # } + # ] + # } + # + data = self.safe_dict(response, 'data', {}) + trades = None + if parseResponseData: + trades = data + else: + trades = self.safe_list(data, 'items', []) + return self.parse_trades(trades, market, since, limit) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://www.kucoin.com/docs/rest/spot-trading/market-data/get-trade-histories + https://www.kucoin.com/docs-new/rest/ua/get-trades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + # pagination is not supported on the exchange side anymore + # if since is not None: + # request['startAt'] = int(math.floor(since / 1000)) + # } + # if limit is not None: + # request['pageSize'] = limit + # } + uta = None + uta, params = self.handle_option_and_params(params, 'fetchTrades', 'uta', False) + response = None + trades = None + if uta: + type = None + type, params = self.handle_market_type_and_params('fetchTrades', market, params) + if type == 'spot': + request['tradeType'] = 'SPOT' + else: + request['tradeType'] = 'FUTURES' + response = self.utaGetMarketTrade(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "tradeType": "SPOT", + # "list": [ + # { + # "sequence": "18746044393340932", + # "tradeId": "18746044393340932", + # "price": "104355.6", + # "size": "0.00011886", + # "side": "sell", + # "ts": 1762242540829000000 + # }, + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + trades = self.safe_list(data, 'list', []) + else: + response = self.publicGetMarketHistories(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": [ + # { + # "sequence": "1548764654235", + # "side": "sell", + # "size":"0.6841354", + # "price":"0.03202", + # "time":1548848575203567174 + # } + # ] + # } + # + trades = self.safe_list(response, 'data', []) + return self.parse_trades(trades, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(public) + # + # { + # "sequence": "1548764654235", + # "side": "sell", + # "size":"0.6841354", + # "price":"0.03202", + # "time":1548848575203567174 + # } + # + # { + # "sequence": "1568787654360", + # "symbol": "BTC-USDT", + # "side": "buy", + # "size": "0.00536577", + # "price": "9345", + # "takerOrderId": "5e356c4a9f1a790008f8d921", + # "time": "1580559434436443257", + # "type": "match", + # "makerOrderId": "5e356bffedf0010008fa5d7f", + # "tradeId": "5e356c4aeefabd62c62a1ece" + # } + # + # fetchMyTrades(private) v2 + # + # { + # "symbol":"BTC-USDT", + # "tradeId":"5c35c02709e4f67d5266954e", + # "orderId":"5c35c02703aa673ceec2a168", + # "counterOrderId":"5c1ab46003aa676e487fa8e3", + # "side":"buy", + # "liquidity":"taker", + # "forceTaker":true, + # "price":"0.083", + # "size":"0.8424304", + # "funds":"0.0699217232", + # "fee":"0", + # "feeRate":"0", + # "feeCurrency":"USDT", + # "stop":"", + # "type":"limit", + # "createdAt":1547026472000 + # } + # + # fetchMyTrades v2 alternative format since 2019-05-21 https://github.com/ccxt/ccxt/pull/5162 + # + # { + # "symbol": "OPEN-BTC", + # "forceTaker": False, + # "orderId": "5ce36420054b4663b1fff2c9", + # "fee": "0", + # "feeCurrency": "", + # "type": "", + # "feeRate": "0", + # "createdAt": 1558417615000, + # "size": "12.8206", + # "stop": "", + # "price": "0", + # "funds": "0", + # "tradeId": "5ce390cf6e0db23b861c6e80" + # } + # + # fetchMyTrades(private) v1(historical) + # + # { + # "symbol": "SNOV-ETH", + # "dealPrice": "0.0000246", + # "dealValue": "0.018942", + # "amount": "770", + # "fee": "0.00001137", + # "side": "sell", + # "createdAt": 1540080199 + # "id":"5c4d389e4c8c60413f78e2e5", + # } + # + # uta fetchTrades + # + # { + # "sequence": "18746044393340932", + # "tradeId": "18746044393340932", + # "price": "104355.6", + # "size": "0.00011886", + # "side": "sell", + # "ts": 1762242540829000000 + # } + # + marketId = self.safe_string(trade, 'symbol') + market = self.safe_market(marketId, market, '-') + id = self.safe_string_2(trade, 'tradeId', 'id') + orderId = self.safe_string(trade, 'orderId') + takerOrMaker = self.safe_string(trade, 'liquidity') + timestamp = self.safe_integer_2(trade, 'time', 'ts') + if timestamp is not None: + timestamp = self.parse_to_int(timestamp / 1000000) + else: + timestamp = self.safe_integer(trade, 'createdAt') + # if it's a historical v1 trade, the exchange returns timestamp in seconds + if ('dealValue' in trade) and (timestamp is not None): + timestamp = timestamp * 1000 + priceString = self.safe_string_2(trade, 'price', 'dealPrice') + amountString = self.safe_string_2(trade, 'size', 'amount') + side = self.safe_string(trade, 'side') + fee = None + feeCostString = self.safe_string(trade, 'fee') + if feeCostString is not None: + feeCurrencyId = self.safe_string(trade, 'feeCurrency') + feeCurrency = self.safe_currency_code(feeCurrencyId) + if feeCurrency is None: + feeCurrency = market['quote'] if (side == 'sell') else market['base'] + fee = { + 'cost': feeCostString, + 'currency': feeCurrency, + 'rate': self.safe_string(trade, 'feeRate'), + } + type = self.safe_string(trade, 'type') + if type == 'match': + type = None + costString = self.safe_string_2(trade, 'funds', 'dealValue') + return self.safe_trade({ + 'info': trade, + 'id': id, + 'order': orderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'type': type, + 'takerOrMaker': takerOrMaker, + 'side': side, + 'price': priceString, + 'amount': amountString, + 'cost': costString, + 'fee': fee, + }, market) + + def fetch_trading_fee(self, symbol: str, params={}) -> TradingFeeInterface: + """ + fetch the trading fees for a market + + https://www.kucoin.com/docs/rest/funding/trade-fee/trading-pair-actual-fee-spot-margin-trade_hf + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `fee structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbols': market['id'], + } + response = self.privateGetTradeFees(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": [ + # { + # "symbol": "BTC-USDT", + # "takerFeeRate": "0.001", + # "makerFeeRate": "0.001" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + first = self.safe_dict(data, 0) + marketId = self.safe_string(first, 'symbol') + return { + 'info': response, + 'symbol': self.safe_symbol(marketId, market), + 'maker': self.safe_number(first, 'makerFeeRate'), + 'taker': self.safe_number(first, 'takerFeeRate'), + 'percentage': True, + 'tierBased': True, + } + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://www.kucoin.com/docs/rest/funding/withdrawals/apply-withdraw-v3- + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.load_markets() + self.check_address(address) + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + 'toAddress': address, + 'withdrawType': 'ADDRESS', + # 'memo': tag, + # 'isInner': False, # internal transfer or external withdrawal + # 'remark': 'optional', + # 'chain': 'OMNI', # 'ERC20', 'TRC20', default is ERC20, This only apply for multi-chain currency, and there is no need for single chain currency. + } + if tag is not None: + request['memo'] = tag + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + if networkCode is not None: + request['chain'] = self.network_code_to_id(networkCode).lower() + request['amount'] = float(self.currency_to_precision(code, amount, networkCode)) + includeFee = None + includeFee, params = self.handle_option_and_params(params, 'withdraw', 'includeFee', False) + if includeFee: + request['feeDeductType'] = 'INTERNAL' + response = self.privatePostWithdrawals(self.extend(request, params)) + # + # the id is inside "data" + # + # { + # "code": 200000, + # "data": { + # "withdrawalId": "5bffb63303aa675e8bbe18f9" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_transaction(data, currency) + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'SUCCESS': 'ok', + 'PROCESSING': 'pending', + 'WALLET_PROCESSING': 'pending', + 'FAILURE': 'failed', + } + return self.safe_string(statuses, status, status) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fetchDeposits + # + # { + # "address": "0x5f047b29041bcfdbf0e4478cdfa753a336ba6989", + # "memo": "5c247c8a03aa677cea2a251d", + # "amount": 1, + # "fee": 0.0001, + # "currency": "KCS", + # "chain": "", + # "isInner": False, + # "walletTxId": "5bbb57386d99522d9f954c5a@test004", + # "status": "SUCCESS", + # "createdAt": 1544178843000, + # "updatedAt": 1544178891000 + # "remark":"foobar" + # } + # + # fetchWithdrawals + # + # { + # "id": "5c2dc64e03aa675aa263f1ac", + # "address": "0x5bedb060b8eb8d823e2414d82acce78d38be7fe9", + # "memo": "", + # "currency": "ETH", + # "chain": "", + # "amount": 1.0000000, + # "fee": 0.0100000, + # "walletTxId": "3e2414d82acce78d38be7fe9", + # "isInner": False, + # "status": "FAILURE", + # "createdAt": 1546503758000, + # "updatedAt": 1546504603000 + # "remark":"foobar" + # } + # + # withdraw + # + # { + # "withdrawalId": "5bffb63303aa675e8bbe18f9" + # } + # + currencyId = self.safe_string(transaction, 'currency') + code = self.safe_currency_code(currencyId, currency) + address = self.safe_string(transaction, 'address') + amount = self.safe_string(transaction, 'amount') + txid = self.safe_string(transaction, 'walletTxId') + if txid is not None: + txidParts = txid.split('@') + numTxidParts = len(txidParts) + if numTxidParts > 1: + if address is None: + if len(txidParts[1]) > 1: + address = txidParts[1] + txid = txidParts[0] + type = 'withdrawal' if (txid is None) else 'deposit' + rawStatus = self.safe_string(transaction, 'status') + fee = None + feeCost = self.safe_string(transaction, 'fee') + if feeCost is not None: + rate = None + if amount is not None: + rate = Precise.string_div(feeCost, amount) + fee = { + 'cost': self.parse_number(feeCost), + 'rate': self.parse_number(rate), + 'currency': code, + } + timestamp = self.safe_integer_2(transaction, 'createdAt', 'createAt') + updated = self.safe_integer(transaction, 'updatedAt') + isV1 = not ('createdAt' in transaction) + # if it's a v1 structure + if isV1: + type = 'withdrawal' if ('address' in transaction) else 'deposit' + if timestamp is not None: + timestamp = timestamp * 1000 + if updated is not None: + updated = updated * 1000 + internal = self.safe_bool(transaction, 'isInner') + tag = self.safe_string(transaction, 'memo') + return { + 'info': transaction, + 'id': self.safe_string_2(transaction, 'id', 'withdrawalId'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': self.network_id_to_code(self.safe_string(transaction, 'chain')), + 'address': address, + 'addressTo': address, + 'addressFrom': None, + 'tag': tag, + 'tagTo': tag, + 'tagFrom': None, + 'currency': code, + 'amount': self.parse_number(amount), + 'txid': txid, + 'type': type, + 'status': self.parse_transaction_status(rawStatus), + 'comment': self.safe_string(transaction, 'remark'), + 'internal': internal, + 'fee': fee, + 'updated': updated, + } + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://www.kucoin.com/docs/rest/funding/deposit/get-deposit-list + https://www.kucoin.com/docs/rest/funding/deposit/get-v1-historical-deposits-list + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch entries for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchDeposits', 'paginate') + if paginate: + return self.fetch_paginated_call_dynamic('fetchDeposits', code, since, limit, params) + request: dict = {} + currency = None + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + if limit is not None: + request['pageSize'] = limit + request, params = self.handle_until_option('endAt', request, params) + response = None + if since is not None and since < 1550448000000: + # if since is earlier than 2019-02-18T00:00:00Z + request['startAt'] = self.parse_to_int(since / 1000) + response = self.privateGetHistDeposits(self.extend(request, params)) + else: + if since is not None: + request['startAt'] = since + response = self.privateGetDeposits(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "currentPage": 1, + # "pageSize": 5, + # "totalNum": 2, + # "totalPage": 1, + # "items": [ + # #-------------------------------------------------- + # # version 2 deposit response structure + # { + # "address": "0x5f047b29041bcfdbf0e4478cdfa753a336ba6989", + # "memo": "5c247c8a03aa677cea2a251d", + # "amount": 1, + # "fee": 0.0001, + # "currency": "KCS", + # "isInner": False, + # "walletTxId": "5bbb57386d99522d9f954c5a@test004", + # "status": "SUCCESS", + # "createdAt": 1544178843000, + # "updatedAt": 1544178891000 + # "remark":"foobar" + # }, + # #-------------------------------------------------- + # # version 1(historical) deposit response structure + # { + # "currency": "BTC", + # "createAt": 1528536998, + # "amount": "0.03266638", + # "walletTxId": "55c643bc2c68d6f17266383ac1be9e454038864b929ae7cee0bc408cc5c869e8@12ffGWmMMD1zA1WbFm7Ho3JZ1w6NYXjpFk@234", + # "isInner": False, + # "status": "SUCCESS", + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + items = self.safe_list(data, 'items', []) + return self.parse_transactions(items, currency, since, limit, {'type': 'deposit'}) + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://www.kucoin.com/docs/rest/funding/withdrawals/get-withdrawals-list + https://www.kucoin.com/docs/rest/funding/withdrawals/get-v1-historical-withdrawals-list + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch entries for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchWithdrawals', 'paginate') + if paginate: + return self.fetch_paginated_call_dynamic('fetchWithdrawals', code, since, limit, params) + request: dict = {} + currency = None + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + if limit is not None: + request['pageSize'] = limit + request, params = self.handle_until_option('endAt', request, params) + response = None + if since is not None and since < 1550448000000: + # if since is earlier than 2019-02-18T00:00:00Z + request['startAt'] = self.parse_to_int(since / 1000) + response = self.privateGetHistWithdrawals(self.extend(request, params)) + else: + if since is not None: + request['startAt'] = since + response = self.privateGetWithdrawals(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "currentPage": 1, + # "pageSize": 5, + # "totalNum": 2, + # "totalPage": 1, + # "items": [ + # #-------------------------------------------------- + # # version 2 withdrawal response structure + # { + # "id": "5c2dc64e03aa675aa263f1ac", + # "address": "0x5bedb060b8eb8d823e2414d82acce78d38be7fe9", + # "memo": "", + # "currency": "ETH", + # "amount": 1.0000000, + # "fee": 0.0100000, + # "walletTxId": "3e2414d82acce78d38be7fe9", + # "isInner": False, + # "status": "FAILURE", + # "createdAt": 1546503758000, + # "updatedAt": 1546504603000 + # }, + # #-------------------------------------------------- + # # version 1(historical) withdrawal response structure + # { + # "currency": "BTC", + # "createAt": 1526723468, + # "amount": "0.534", + # "address": "33xW37ZSW4tQvg443Pc7NLCAs167Yc2XUV", + # "walletTxId": "aeacea864c020acf58e51606169240e96774838dcd4f7ce48acf38e3651323f4", + # "isInner": False, + # "status": "SUCCESS" + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + items = self.safe_list(data, 'items', []) + return self.parse_transactions(items, currency, since, limit, {'type': 'withdrawal'}) + + def parse_balance_helper(self, entry): + account = self.account() + account['used'] = self.safe_string_2(entry, 'holdBalance', 'hold') + account['free'] = self.safe_string_2(entry, 'availableBalance', 'available') + account['total'] = self.safe_string_2(entry, 'totalBalance', 'total') + debt = self.safe_string(entry, 'liability') + interest = self.safe_string(entry, 'interest') + account['debt'] = Precise.string_add(debt, interest) + return account + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://www.kucoin.com/docs/rest/account/basic-info/get-account-list-spot-margin-trade_hf + https://www.kucoin.com/docs/rest/funding/funding-overview/get-account-detail-margin + https://www.kucoin.com/docs/rest/funding/funding-overview/get-account-detail-isolated-margin + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param dict [params.marginMode]: 'cross' or 'isolated', margin type for fetching margin balance + :param dict [params.type]: extra parameters specific to the exchange API endpoint + :param dict [params.hf]: *default if False* if True, the result includes the balance of the high frequency account + :returns dict: a `balance structure ` + """ + self.load_markets() + code = self.safe_string(params, 'code') + currency = None + if code is not None: + currency = self.currency(code) + defaultType = self.safe_string_2(self.options, 'fetchBalance', 'defaultType', 'spot') + requestedType = self.safe_string(params, 'type', defaultType) + accountsByType = self.safe_dict(self.options, 'accountsByType') + type = self.safe_string(accountsByType, requestedType, requestedType) + params = self.omit(params, 'type') + hf = None + hf, params = self.handle_hf_and_params(params) + if hf and (type != 'main'): + type = 'trade_hf' + marginMode, query = self.handle_margin_mode_and_params('fetchBalance', params) + response = None + request: dict = {} + isolated = (marginMode == 'isolated') or (type == 'isolated') + cross = (marginMode == 'cross') or (type == 'margin') + if isolated: + if currency is not None: + request['balanceCurrency'] = currency['id'] + response = self.privateGetIsolatedAccounts(self.extend(request, query)) + elif cross: + response = self.privateGetMarginAccount(self.extend(request, query)) + else: + if currency is not None: + request['currency'] = currency['id'] + request['type'] = type + response = self.privateGetAccounts(self.extend(request, query)) + # + # Spot + # + # { + # "code": "200000", + # "data": [ + # { + # "balance": "0.00009788", + # "available": "0.00009788", + # "holds": "0", + # "currency": "BTC", + # "id": "5c6a4fd399a1d81c4f9cc4d0", + # "type": "trade", + # }, + # ] + # } + # + # Cross + # + # { + # "code": "200000", + # "data": { + # "debtRatio": "0", + # "accounts": [ + # { + # "currency": "USDT", + # "totalBalance": "5", + # "availableBalance": "5", + # "holdBalance": "0", + # "liability": "0", + # "maxBorrowSize": "20" + # }, + # ] + # } + # } + # + # Isolated + # + # { + # "code": "200000", + # "data": { + # "totalAssetOfQuoteCurrency": "0", + # "totalLiabilityOfQuoteCurrency": "0", + # "timestamp": 1712085661155, + # "assets": [ + # { + # "symbol": "MANA-USDT", + # "status": "EFFECTIVE", + # "debtRatio": "0", + # "baseAsset": { + # "currency": "MANA", + # "borrowEnabled": True, + # "transferInEnabled": True, + # "total": "0", + # "hold": "0", + # "available": "0", + # "liability": "0", + # "interest": "0", + # "maxBorrowSize": "0" + # }, + # "quoteAsset": { + # "currency": "USDT", + # "borrowEnabled": True, + # "transferInEnabled": True, + # "total": "0", + # "hold": "0", + # "available": "0", + # "liability": "0", + # "interest": "0", + # "maxBorrowSize": "0" + # } + # }, + # ... + # ] + # } + # } + # + data = None + result: dict = { + 'info': response, + 'timestamp': None, + 'datetime': None, + } + if isolated: + data = self.safe_dict(response, 'data', {}) + assets = self.safe_value(data, 'assets', data) + for i in range(0, len(assets)): + entry = assets[i] + marketId = self.safe_string(entry, 'symbol') + symbol = self.safe_symbol(marketId, None, '_') + base = self.safe_dict(entry, 'baseAsset', {}) + quote = self.safe_dict(entry, 'quoteAsset', {}) + baseCode = self.safe_currency_code(self.safe_string(base, 'currency')) + quoteCode = self.safe_currency_code(self.safe_string(quote, 'currency')) + subResult: dict = {} + subResult[baseCode] = self.parse_balance_helper(base) + subResult[quoteCode] = self.parse_balance_helper(quote) + result[symbol] = self.safe_balance(subResult) + elif cross: + data = self.safe_dict(response, 'data', {}) + accounts = self.safe_list(data, 'accounts', []) + for i in range(0, len(accounts)): + balance = accounts[i] + currencyId = self.safe_string(balance, 'currency') + codeInner = self.safe_currency_code(currencyId) + result[codeInner] = self.parse_balance_helper(balance) + else: + data = self.safe_list(response, 'data', []) + for i in range(0, len(data)): + balance = data[i] + balanceType = self.safe_string(balance, 'type') + if balanceType == type: + currencyId = self.safe_string(balance, 'currency') + codeInner2 = self.safe_currency_code(currencyId) + account = self.account() + account['total'] = self.safe_string(balance, 'balance') + account['free'] = self.safe_string(balance, 'available') + account['used'] = self.safe_string(balance, 'holds') + result[codeInner2] = account + returnType = result + if not isolated: + returnType = self.safe_balance(result) + return returnType + + def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://www.kucoin.com/docs/rest/funding/transfer/inner-transfer + https://docs.kucoin.com/futures/#transfer-funds-to-kucoin-main-account-2 + https://docs.kucoin.com/spot-hf/#internal-funds-transfers-in-high-frequency-trading-accounts + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account to transfer from + :param str toAccount: account to transfer to + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transfer structure ` + """ + self.load_markets() + currency = self.currency(code) + requestedAmount = self.currency_to_precision(code, amount) + fromId = self.convert_type_to_account(fromAccount) + toId = self.convert_type_to_account(toAccount) + fromIsolated = self.in_array(fromId, self.ids) + toIsolated = self.in_array(toId, self.ids) + if fromId == 'contract': + if toId != 'main': + raise ExchangeError(self.id + ' transfer() only supports transferring from futures account to main account') + request: dict = { + 'currency': currency['id'], + 'amount': requestedAmount, + } + if not ('bizNo' in params): + # it doesn't like more than 24 characters + request['bizNo'] = self.uuid22() + response = self.futuresPrivatePostTransferOut(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "applyId": "605a87217dff1500063d485d", + # "bizNo": "bcd6e5e1291f4905af84dc", + # "payAccountType": "CONTRACT", + # "payTag": "DEFAULT", + # "remark": '', + # "recAccountType": "MAIN", + # "recTag": "DEFAULT", + # "recRemark": '', + # "recSystem": "KUCOIN", + # "status": "PROCESSING", + # "currency": "XBT", + # "amount": "0.00001", + # "fee": "0", + # "sn": "573688685663948", + # "reason": '', + # "createdAt": 1616545569000, + # "updatedAt": 1616545569000 + # } + # } + # + data = self.safe_dict(response, 'data') + return self.parse_transfer(data, currency) + else: + request: dict = { + 'currency': currency['id'], + 'amount': requestedAmount, + } + if fromIsolated or toIsolated: + if self.in_array(fromId, self.ids): + request['fromTag'] = fromId + fromId = 'isolated' + if self.in_array(toId, self.ids): + request['toTag'] = toId + toId = 'isolated' + request['from'] = fromId + request['to'] = toId + if not ('clientOid' in params): + request['clientOid'] = self.uuid() + response = self.privatePostAccountsInnerTransfer(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "orderId": "605a6211e657f00006ad0ad6" + # } + # } + # + data = self.safe_dict(response, 'data') + return self.parse_transfer(data, currency) + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + # + # transfer(spot) + # + # { + # "orderId": "605a6211e657f00006ad0ad6" + # } + # + # { + # "code": "200000", + # "msg": "Failed to transfer out. The amount exceeds the upper limit" + # } + # + # transfer(futures) + # + # { + # "applyId": "605a87217dff1500063d485d", + # "bizNo": "bcd6e5e1291f4905af84dc", + # "payAccountType": "CONTRACT", + # "payTag": "DEFAULT", + # "remark": '', + # "recAccountType": "MAIN", + # "recTag": "DEFAULT", + # "recRemark": '', + # "recSystem": "KUCOIN", + # "status": "PROCESSING", + # "currency": "XBT", + # "amount": "0.00001", + # "fee": "0", + # "sn": "573688685663948", + # "reason": '', + # "createdAt": 1616545569000, + # "updatedAt": 1616545569000 + # } + # + timestamp = self.safe_integer(transfer, 'createdAt') + currencyId = self.safe_string(transfer, 'currency') + rawStatus = self.safe_string(transfer, 'status') + accountFromRaw = self.safe_string_lower(transfer, 'payAccountType') + accountToRaw = self.safe_string_lower(transfer, 'recAccountType') + accountsByType = self.safe_dict(self.options, 'accountsByType') + accountFrom = self.safe_string(accountsByType, accountFromRaw, accountFromRaw) + accountTo = self.safe_string(accountsByType, accountToRaw, accountToRaw) + return { + 'id': self.safe_string_2(transfer, 'applyId', 'orderId'), + 'currency': self.safe_currency_code(currencyId, currency), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'amount': self.safe_number(transfer, 'amount'), + 'fromAccount': accountFrom, + 'toAccount': accountTo, + 'status': self.parse_transfer_status(rawStatus), + 'info': transfer, + } + + def parse_transfer_status(self, status: Str) -> Str: + statuses: dict = { + 'PROCESSING': 'pending', + } + return self.safe_string(statuses, status, status) + + def parse_ledger_entry_type(self, type): + types: dict = { + 'Assets Transferred in After Upgrading': 'transfer', # Assets Transferred in After V1 to V2 Upgrading + 'Deposit': 'transaction', # Deposit + 'Withdrawal': 'transaction', # Withdrawal + 'Transfer': 'transfer', # Transfer + 'Trade_Exchange': 'trade', # Trade + # 'Vote for Coin': 'Vote for Coin', # Vote for Coin + 'KuCoin Bonus': 'bonus', # KuCoin Bonus + 'Referral Bonus': 'referral', # Referral Bonus + 'Rewards': 'bonus', # Activities Rewards + # 'Distribution': 'Distribution', # Distribution, such GAS by holding NEO + 'Airdrop/Fork': 'airdrop', # Airdrop/Fork + 'Other rewards': 'bonus', # Other rewards, except Vote, Airdrop, Fork + 'Fee Rebate': 'rebate', # Fee Rebate + 'Buy Crypto': 'trade', # Use credit card to buy crypto + 'Sell Crypto': 'sell', # Use credit card to sell crypto + 'Public Offering Purchase': 'trade', # Public Offering Purchase for Spotlight + # 'Send red envelope': 'Send red envelope', # Send red envelope + # 'Open red envelope': 'Open red envelope', # Open red envelope + # 'Staking': 'Staking', # Staking + # 'LockDrop Vesting': 'LockDrop Vesting', # LockDrop Vesting + # 'Staking Profits': 'Staking Profits', # Staking Profits + # 'Redemption': 'Redemption', # Redemption + 'Refunded Fees': 'fee', # Refunded Fees + 'KCS Pay Fees': 'fee', # KCS Pay Fees + 'Margin Trade': 'trade', # Margin Trade + 'Loans': 'Loans', # Loans + # 'Borrowings': 'Borrowings', # Borrowings + # 'Debt Repayment': 'Debt Repayment', # Debt Repayment + # 'Loans Repaid': 'Loans Repaid', # Loans Repaid + # 'Lendings': 'Lendings', # Lendings + # 'Pool transactions': 'Pool transactions', # Pool-X transactions + 'Instant Exchange': 'trade', # Instant Exchange + 'Sub-account transfer': 'transfer', # Sub-account transfer + 'Liquidation Fees': 'fee', # Liquidation Fees + # 'Soft Staking Profits': 'Soft Staking Profits', # Soft Staking Profits + # 'Voting Earnings': 'Voting Earnings', # Voting Earnings on Pool-X + # 'Redemption of Voting': 'Redemption of Voting', # Redemption of Voting on Pool-X + # 'Voting': 'Voting', # Voting on Pool-X + # 'Convert to KCS': 'Convert to KCS', # Convert to KCS + } + return self.safe_string(types, type, type) + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + # + # { + # "id": "611a1e7c6a053300067a88d9", #unique key for each ledger entry + # "currency": "USDT", #Currency + # "amount": "10.00059547", #The total amount of assets(fees included) involved in assets changes such, withdrawal and bonus distribution. + # "fee": "0", #Deposit or withdrawal fee + # "balance": "0", #Total assets of a currency remaining funds after transaction + # "accountType": "MAIN", #Account Type + # "bizType": "Loans Repaid", #business type + # "direction": "in", #side, in or out + # "createdAt": 1629101692950, #Creation time + # "context": "{\"borrowerUserId\":\"601ad03e50dc810006d242ea\",\"loanRepayDetailNo\":\"611a1e7cc913d000066cf7ec\"}" #Business core parameters + # } + # + id = self.safe_string(item, 'id') + currencyId = self.safe_string(item, 'currency') + code = self.safe_currency_code(currencyId, currency) + currency = self.safe_currency(currencyId, currency) + amount = self.safe_number(item, 'amount') + balanceAfter = None + # balanceAfter = self.safe_number(item, 'balance'); only returns zero string + bizType = self.safe_string(item, 'bizType') + type = self.parse_ledger_entry_type(bizType) + direction = self.safe_string(item, 'direction') + timestamp = self.safe_integer(item, 'createdAt') + datetime = self.iso8601(timestamp) + account = self.safe_string(item, 'accountType') # MAIN, TRADE, MARGIN, or CONTRACT + context = self.safe_string(item, 'context') # contains other information about the ledger entry + # + # withdrawal transaction + # + # "{\"orderId\":\"617bb2d09e7b3b000196dac8\",\"txId\":\"0x79bb9855f86b351a45cab4dc69d78ca09586a94c45dde49475722b98f401b054\"}" + # + # deposit to MAIN, trade via MAIN + # + # "{\"orderId\":\"617ab9949e7b3b0001948081\",\"txId\":\"0x7a06b16bbd6b03dbc3d96df5683b15229fc35e7184fd7179a5f3a310bd67d1fa@default@0\"}" + # + # sell trade + # + # "{\"symbol\":\"ETH-USDT\",\"orderId\":\"617adcd1eb3fa20001dd29a1\",\"tradeId\":\"617adcd12e113d2b91222ff9\"}" + # + referenceId = None + if context is not None and context != '': + try: + parsed = json.loads(context) + orderId = self.safe_string(parsed, 'orderId') + tradeId = self.safe_string(parsed, 'tradeId') + # transactions only have an orderId but for trades we wish to use tradeId + if tradeId is not None: + referenceId = tradeId + else: + referenceId = orderId + except Exception as exc: + referenceId = context + fee = None + feeCost = self.safe_string(item, 'fee') + feeCurrency = None + if feeCost != '0': + feeCurrency = code + fee = {'cost': self.parse_number(feeCost), 'currency': feeCurrency} + return self.safe_ledger_entry({ + 'info': item, + 'id': id, + 'direction': direction, + 'account': account, + 'referenceId': referenceId, + 'referenceAccount': account, + 'type': type, + 'currency': code, + 'amount': amount, + 'timestamp': timestamp, + 'datetime': datetime, + 'before': None, + 'after': balanceAfter, # None + 'status': None, + 'fee': fee, + }, currency) + + def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + + https://www.kucoin.com/docs/rest/account/basic-info/get-account-ledgers-spot-margin + https://www.kucoin.com/docs/rest/account/basic-info/get-account-ledgers-trade_hf + https://www.kucoin.com/docs/rest/account/basic-info/get-account-ledgers-margin_hf + + :param str [code]: unified currency code, default is None + :param int [since]: timestamp in ms of the earliest ledger entry, default is None + :param int [limit]: max number of ledger entries to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.hf]: default False, when True will fetch ledger entries for the high frequency trading account + :param int [params.until]: the latest time in ms to fetch entries for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict: a `ledger structure ` + """ + self.load_markets() + self.load_accounts() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchLedger', 'paginate') + hf = None + hf, params = self.handle_hf_and_params(params) + if paginate: + return self.fetch_paginated_call_dynamic('fetchLedger', code, since, limit, params) + request: dict = { + # 'currency': currency['id'], # can choose up to 10, if not provided returns for all currencies by default + # 'direction': 'in', # 'out' + # 'bizType': 'DEPOSIT', # DEPOSIT, WITHDRAW, TRANSFER, SUB_TRANSFER,TRADE_EXCHANGE, MARGIN_EXCHANGE, KUCOIN_BONUS(optional) + # 'startAt': since, + # 'endAt': exchange.milliseconds(), + } + if since is not None: + request['startAt'] = since + # atm only single currency retrieval is supported + currency = None + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + request, params = self.handle_until_option('endAt', request, params) + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchLedger', params) + response = None + if hf: + if marginMode is not None: + response = self.privateGetHfMarginAccountLedgers(self.extend(request, params)) + else: + response = self.privateGetHfAccountsLedgers(self.extend(request, params)) + else: + response = self.privateGetAccountsLedgers(self.extend(request, params)) + # + # { + # "code":"200000", + # "data":{ + # "currentPage":1, + # "pageSize":50, + # "totalNum":1, + # "totalPage":1, + # "items":[ + # { + # "id":"617cc528729f5f0001c03ceb", + # "currency":"GAS", + # "amount":"0.00000339", + # "fee":"0", + # "balance":"0", + # "accountType":"MAIN", + # "bizType":"Distribution", + # "direction":"in", + # "createdAt":1635566888183, + # "context":"{\"orderId\":\"617cc47a1c47ed0001ce3606\",\"description\":\"Holding NEO,distribute GAS(2021/10/30)\"}" + # } + # { + # "id": "611a1e7c6a053300067a88d9",//unique key + # "currency": "USDT", #Currency + # "amount": "10.00059547", #Change amount of the funds + # "fee": "0", #Deposit or withdrawal fee + # "balance": "0", #Total assets of a currency + # "accountType": "MAIN", #Account Type + # "bizType": "Loans Repaid", #business type + # "direction": "in", #side, in or out + # "createdAt": 1629101692950, #Creation time + # "context": "{\"borrowerUserId\":\"601ad03e50dc810006d242ea\",\"loanRepayDetailNo\":\"611a1e7cc913d000066cf7ec\"}" + # }, + # ] + # } + # } + # + dataList = self.safe_list(response, 'data') + if dataList is not None: + return self.parse_ledger(dataList, currency, since, limit) + data = self.safe_dict(response, 'data') + items = self.safe_list(data, 'items', []) + return self.parse_ledger(items, currency, since, limit) + + def calculate_rate_limiter_cost(self, api, method, path, params, config={}): + versions = self.safe_dict(self.options, 'versions', {}) + apiVersions = self.safe_dict(versions, api, {}) + methodVersions = self.safe_dict(apiVersions, method, {}) + defaultVersion = self.safe_string(methodVersions, path, self.options['version']) + version = self.safe_string(params, 'version', defaultVersion) + if version == 'v3' and ('v3' in config): + return config['v3'] + elif version == 'v2' and ('v2' in config): + return config['v2'] + elif version == 'v1' and ('v1' in config): + return config['v1'] + return self.safe_value(config, 'cost', 1) + + def parse_borrow_rate(self, info, currency: Currency = None): + # + # { + # "tradeId": "62db2dcaff219600012b56cd", + # "currency": "USDT", + # "size": "10", + # "dailyIntRate": "0.00003", + # "term": 7, + # "timestamp": 1658531274508488480 + # }, + # + # { + # "createdAt": 1697783812257, + # "currency": "XMR", + # "interestAmount": "0.1", + # "dayRatio": "0.001" + # } + # + timestampId = self.safe_string_2(info, 'createdAt', 'timestamp') + timestamp = self.parse_to_int(timestampId[0:13]) + currencyId = self.safe_string(info, 'currency') + return { + 'currency': self.safe_currency_code(currencyId, currency), + 'rate': self.safe_number_2(info, 'dailyIntRate', 'dayRatio'), + 'period': 86400000, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'info': info, + } + + def fetch_borrow_interest(self, code: Str = None, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[BorrowInterest]: + """ + fetch the interest owed by the user for borrowing currency for margin trading + + https://docs.kucoin.com/#get-repay-record + https://docs.kucoin.com/#query-isolated-margin-account-info + + :param str [code]: unified currency code + :param str [symbol]: unified market symbol, required for isolated margin + :param int [since]: the earliest time in ms to fetch borrrow interest for + :param int [limit]: the maximum number of structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' default is 'cross' + :returns dict[]: a list of `borrow interest structures ` + """ + self.load_markets() + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchBorrowInterest', params, 'cross') + request: dict = {} + currency = None + if code is not None: + currency = self.currency(code) + if marginMode == 'isolated': + request['balanceCurrency'] = currency['id'] + else: + request['quoteCurrency'] = currency['id'] + market = None + if symbol is not None: + market = self.market(symbol) + response = None + if marginMode == 'isolated': + response = self.privateGetIsolatedAccounts(self.extend(request, params)) + else: + response = self.privateGetMarginAccounts(self.extend(request, params)) + # + # Cross + # + # { + # "code": "200000", + # "data": { + # "totalAssetOfQuoteCurrency": "0", + # "totalLiabilityOfQuoteCurrency": "0", + # "debtRatio": "0", + # "status": "EFFECTIVE", + # "accounts": [ + # { + # "currency": "1INCH", + # "total": "0", + # "available": "0", + # "hold": "0", + # "liability": "0", + # "maxBorrowSize": "0", + # "borrowEnabled": True, + # "transferInEnabled": True + # } + # ] + # } + # } + # + # Isolated + # + # { + # "code": "200000", + # "data": { + # "totalConversionBalance": "0.02138647", + # "liabilityConversionBalance": "0.01480001", + # "assets": [ + # { + # "symbol": "MANA-USDT", + # "debtRatio": "0", + # "status": "BORROW", + # "baseAsset": { + # "currency": "MANA", + # "borrowEnabled": True, + # "repayEnabled": True, + # "transferEnabled": True, + # "borrowed": "0", + # "totalAsset": "0", + # "available": "0", + # "hold": "0", + # "maxBorrowSize": "1000" + # }, + # "quoteAsset": { + # "currency": "USDT", + # "borrowEnabled": True, + # "repayEnabled": True, + # "transferEnabled": True, + # "borrowed": "0", + # "totalAsset": "0", + # "available": "0", + # "hold": "0", + # "maxBorrowSize": "50000" + # } + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + assets = self.safe_list(data, 'assets', []) if (marginMode == 'isolated') else self.safe_list(data, 'accounts', []) + interest = self.parse_borrow_interests(assets, market) + filteredByCurrency = self.filter_by_currency_since_limit(interest, code, since, limit) + return self.filter_by_symbol_since_limit(filteredByCurrency, symbol, since, limit) + + def parse_borrow_interest(self, info: dict, market: Market = None) -> BorrowInterest: + # + # Cross + # + # { + # "currency": "1INCH", + # "total": "0", + # "available": "0", + # "hold": "0", + # "liability": "0", + # "maxBorrowSize": "0", + # "borrowEnabled": True, + # "transferInEnabled": True + # } + # + # Isolated + # + # { + # "symbol": "MANA-USDT", + # "debtRatio": "0", + # "status": "BORROW", + # "baseAsset": { + # "currency": "MANA", + # "borrowEnabled": True, + # "repayEnabled": True, + # "transferEnabled": True, + # "borrowed": "0", + # "totalAsset": "0", + # "available": "0", + # "hold": "0", + # "maxBorrowSize": "1000" + # }, + # "quoteAsset": { + # "currency": "USDT", + # "borrowEnabled": True, + # "repayEnabled": True, + # "transferEnabled": True, + # "borrowed": "0", + # "totalAsset": "0", + # "available": "0", + # "hold": "0", + # "maxBorrowSize": "50000" + # } + # } + # + marketId = self.safe_string(info, 'symbol') + marginMode = 'cross' if (marketId is None) else 'isolated' + market = self.safe_market(marketId, market) + symbol = self.safe_string(market, 'symbol') + timestamp = self.safe_integer(info, 'createdAt') + isolatedBase = self.safe_dict(info, 'baseAsset', {}) + amountBorrowed = None + interest = None + currencyId = None + if marginMode == 'isolated': + amountBorrowed = self.safe_number(isolatedBase, 'liability') + interest = self.safe_number(isolatedBase, 'interest') + currencyId = self.safe_string(isolatedBase, 'currency') + else: + amountBorrowed = self.safe_number(info, 'liability') + interest = self.safe_number(info, 'accruedInterest') + currencyId = self.safe_string(info, 'currency') + return { + 'info': info, + 'symbol': symbol, + 'currency': self.safe_currency_code(currencyId), + 'interest': interest, + 'interestRate': self.safe_number(info, 'dailyIntRate'), + 'amountBorrowed': amountBorrowed, + 'marginMode': marginMode, + 'timestamp': timestamp, # create time + 'datetime': self.iso8601(timestamp), + } + + def fetch_borrow_rate_histories(self, codes=None, since: Int = None, limit: Int = None, params={}): + """ + retrieves a history of a multiple currencies borrow interest rate at specific time slots, returns all currencies if no symbols passed, default is None + + https://www.kucoin.com/docs/rest/margin-trading/margin-trading-v3-/get-cross-isolated-margin-interest-records + + :param str[]|None codes: list of unified currency codes, default is None + :param int [since]: timestamp in ms of the earliest borrowRate, default is None + :param int [limit]: max number of borrow rate prices to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' default is 'cross' + :param int [params.until]: the latest time in ms to fetch entries for + :returns dict: a dictionary of `borrow rate structures ` indexed by the market symbol + """ + self.load_markets() + marginResult = self.handle_margin_mode_and_params('fetchBorrowRateHistories', params) + marginMode = self.safe_string(marginResult, 0, 'cross') + isIsolated = (marginMode == 'isolated') # True-isolated, False-cross + request: dict = { + 'isIsolated': isIsolated, + } + if since is not None: + request['startTime'] = since + request, params = self.handle_until_option('endTime', request, params) + if limit is not None: + request['pageSize'] = limit # default:50, min:10, max:500 + response = self.privateGetMarginInterest(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "timestamp": 1710829939673, + # "currentPage": 1, + # "pageSize": 50, + # "totalNum": 0, + # "totalPage": 0, + # "items": [ + # { + # "createdAt": 1697783812257, + # "currency": "XMR", + # "interestAmount": "0.1", + # "dayRatio": "0.001" + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'data') + rows = self.safe_list(data, 'items', []) + return self.parse_borrow_rate_histories(rows, codes, since, limit) + + def fetch_borrow_rate_history(self, code: str, since: Int = None, limit: Int = None, params={}): + """ + retrieves a history of a currencies borrow interest rate at specific time slots + + https://www.kucoin.com/docs/rest/margin-trading/margin-trading-v3-/get-cross-isolated-margin-interest-records + + :param str code: unified currency code + :param int [since]: timestamp for the earliest borrow rate + :param int [limit]: the maximum number of `borrow rate structures ` to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' default is 'cross' + :param int [params.until]: the latest time in ms to fetch entries for + :returns dict[]: an array of `borrow rate structures ` + """ + self.load_markets() + marginResult = self.handle_margin_mode_and_params('fetchBorrowRateHistories', params) + marginMode = self.safe_string(marginResult, 0, 'cross') + isIsolated = (marginMode == 'isolated') # True-isolated, False-cross + currency = self.currency(code) + request: dict = { + 'isIsolated': isIsolated, + 'currency': currency['id'], + } + if since is not None: + request['startTime'] = since + request, params = self.handle_until_option('endTime', request, params) + if limit is not None: + request['pageSize'] = limit # default:50, min:10, max:500 + response = self.privateGetMarginInterest(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "timestamp": 1710829939673, + # "currentPage": 1, + # "pageSize": 50, + # "totalNum": 0, + # "totalPage": 0, + # "items": [ + # { + # "createdAt": 1697783812257, + # "currency": "XMR", + # "interestAmount": "0.1", + # "dayRatio": "0.001" + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'data') + rows = self.safe_list(data, 'items', []) + return self.parse_borrow_rate_history(rows, code, since, limit) + + def parse_borrow_rate_histories(self, response, codes, since, limit): + # + # [ + # { + # "createdAt": 1697783812257, + # "currency": "XMR", + # "interestAmount": "0.1", + # "dayRatio": "0.001" + # } + # ] + # + borrowRateHistories: dict = {} + for i in range(0, len(response)): + item = response[i] + code = self.safe_currency_code(self.safe_string(item, 'currency')) + if codes is None or self.in_array(code, codes): + if not (code in borrowRateHistories): + borrowRateHistories[code] = [] + borrowRateStructure = self.parse_borrow_rate(item) + borrowRateHistoriesCode = borrowRateHistories[code] + borrowRateHistoriesCode.append(borrowRateStructure) + keys = list(borrowRateHistories.keys()) + for i in range(0, len(keys)): + code = keys[i] + borrowRateHistories[code] = self.filter_by_currency_since_limit(borrowRateHistories[code], code, since, limit) + return borrowRateHistories + + def borrow_cross_margin(self, code: str, amount: float, params={}): + """ + create a loan to borrow margin + + https://docs.kucoin.com/#1-margin-borrowing + + :param str code: unified currency code of the currency to borrow + :param float amount: the amount to borrow + :param dict [params]: extra parameters specific to the exchange API endpoints + :param str [params.timeInForce]: either IOC or FOK + :returns dict: a `margin loan structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + 'size': self.currency_to_precision(code, amount), + 'timeInForce': 'FOK', + } + response = self.privatePostMarginBorrow(self.extend(request, params)) + # + # { + # "success": True, + # "code": "200", + # "msg": "success", + # "retry": False, + # "data": { + # "orderNo": "5da6dba0f943c0c81f5d5db5", + # "actualSize": 10 + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_margin_loan(data, currency) + + def borrow_isolated_margin(self, symbol: str, code: str, amount: float, params={}): + """ + create a loan to borrow margin + + https://docs.kucoin.com/#1-margin-borrowing + + :param str symbol: unified market symbol, required for isolated margin + :param str code: unified currency code of the currency to borrow + :param float amount: the amount to borrow + :param dict [params]: extra parameters specific to the exchange API endpoints + :param str [params.timeInForce]: either IOC or FOK + :returns dict: a `margin loan structure ` + """ + self.load_markets() + market = self.market(symbol) + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + 'size': self.currency_to_precision(code, amount), + 'symbol': market['id'], + 'timeInForce': 'FOK', + 'isIsolated': True, + } + response = self.privatePostMarginBorrow(self.extend(request, params)) + # + # { + # "success": True, + # "code": "200", + # "msg": "success", + # "retry": False, + # "data": { + # "orderNo": "5da6dba0f943c0c81f5d5db5", + # "actualSize": 10 + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_margin_loan(data, currency) + + def repay_cross_margin(self, code: str, amount, params={}): + """ + repay borrowed margin and interest + + https://docs.kucoin.com/#2-repayment + + :param str code: unified currency code of the currency to repay + :param float amount: the amount to repay + :param dict [params]: extra parameters specific to the exchange API endpoints + :returns dict: a `margin loan structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + 'size': self.currency_to_precision(code, amount), + } + response = self.privatePostMarginRepay(self.extend(request, params)) + # + # { + # "success": True, + # "code": "200", + # "msg": "success", + # "retry": False, + # "data": { + # "orderNo": "5da6dba0f943c0c81f5d5db5", + # "actualSize": 10 + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_margin_loan(data, currency) + + def repay_isolated_margin(self, symbol: str, code: str, amount, params={}): + """ + repay borrowed margin and interest + + https://docs.kucoin.com/#2-repayment + + :param str symbol: unified market symbol + :param str code: unified currency code of the currency to repay + :param float amount: the amount to repay + :param dict [params]: extra parameters specific to the exchange API endpoints + :returns dict: a `margin loan structure ` + """ + self.load_markets() + market = self.market(symbol) + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + 'size': self.currency_to_precision(code, amount), + 'symbol': market['id'], + 'isIsolated': True, + } + response = self.privatePostMarginRepay(self.extend(request, params)) + # + # { + # "success": True, + # "code": "200", + # "msg": "success", + # "retry": False, + # "data": { + # "orderNo": "5da6dba0f943c0c81f5d5db5", + # "actualSize": 10 + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_margin_loan(data, currency) + + def parse_margin_loan(self, info, currency: Currency = None): + # + # { + # "orderNo": "5da6dba0f943c0c81f5d5db5", + # "actualSize": 10 + # } + # + timestamp = self.milliseconds() + currencyId = self.safe_string(info, 'currency') + return { + 'id': self.safe_string(info, 'orderNo'), + 'currency': self.safe_currency_code(currencyId, currency), + 'amount': self.safe_number(info, 'actualSize'), + 'symbol': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'info': info, + } + + def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}): + """ + fetch deposit and withdraw fees - *IMPORTANT* use fetchDepositWithdrawFee to get more in-depth info + + https://docs.kucoin.com/#get-currencies + + :param str[]|None codes: list of unified currency codes + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `fee structures ` + """ + self.load_markets() + response = self.publicGetCurrencies(params) + # + # [ + # { + # "currency": "CSP", + # "name": "CSP", + # "fullName": "Caspian", + # "precision": 8, + # "confirms": 12, + # "contractAddress": "0xa6446d655a0c34bc4f05042ee88170d056cbaf45", + # "withdrawalMinSize": "2000", + # "withdrawalMinFee": "1000", + # "isWithdrawEnabled": True, + # "isDepositEnabled": True, + # "isMarginEnabled": False, + # "isDebitEnabled": False + # }, + # ] + # + data = self.safe_list(response, 'data', []) + return self.parse_deposit_withdraw_fees(data, codes, 'currency') + + def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://www.kucoin.com/docs/rest/margin-trading/margin-trading-v3-/modify-leverage-multiplier + + :param int [leverage]: New leverage multiplier. Must be greater than 1 and up to two decimal places, and cannot be less than the user's current debt leverage or greater than the system's maximum leverage + :param str [symbol]: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + self.load_markets() + market = None + marketType: Str = None + marketType, params = self.handle_market_type_and_params('setLeverage', None, params) + if (symbol is not None) or marketType != 'spot': + market = self.market(symbol) + if market['contract']: + raise NotSupported(self.id + ' setLeverage currently supports only spot margin') + marginMode: Str = None + marginMode, params = self.handle_margin_mode_and_params('setLeverage', params) + if marginMode is None: + raise ArgumentsRequired(self.id + ' setLeverage requires a marginMode parameter') + request: dict = {} + if marginMode == 'isolated' and symbol is None: + raise ArgumentsRequired(self.id + ' setLeverage requires a symbol parameter for isolated margin') + if symbol is not None: + request['symbol'] = market['id'] + request['leverage'] = str(leverage) + request['isIsolated'] = (marginMode == 'isolated') + return self.privatePostPositionUpdateUserLeverage(self.extend(request, params)) + + def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate + + https://www.kucoin.com/docs-new/rest/ua/get-current-funding-rate + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = self.utaGetMarketFundingRate(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "symbol": ".XBTUSDTMFPI8H", + # "nextFundingRate": 7.4E-5, + # "fundingTime": 1762444800000, + # "fundingRateCap": 0.003, + # "fundingRateFloor": -0.003 + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_funding_rate(data, market) + + def parse_funding_rate(self, data, market: Market = None) -> FundingRate: + # + # { + # "symbol": ".XBTUSDTMFPI8H", + # "nextFundingRate": 7.4E-5, + # "fundingTime": 1762444800000, + # "fundingRateCap": 0.003, + # "fundingRateFloor": -0.003 + # } + # + fundingTimestamp = self.safe_integer(data, 'fundingTime') + marketId = self.safe_string(data, 'symbol') + return { + 'info': data, + 'symbol': self.safe_symbol(marketId, market, None, 'contract'), + 'markPrice': None, + 'indexPrice': None, + 'interestRate': None, + 'estimatedSettlePrice': None, + 'timestamp': None, + 'datetime': None, + 'fundingRate': self.safe_number(data, 'nextFundingRate'), + 'fundingTimestamp': fundingTimestamp, + 'fundingDatetime': self.iso8601(fundingTimestamp), + 'nextFundingRate': None, + 'nextFundingTimestamp': None, + 'nextFundingDatetime': None, + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': None, + } + + def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical funding rate prices + + https://www.kucoin.com/docs-new/rest/ua/get-history-funding-rate + + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: not used by kucuoinfutures + :param int [limit]: the maximum amount of `funding rate structures ` to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: end time in ms + :returns dict[]: a list of `funding rate structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchFundingRateHistory() requires a symbol argument') + if since is None: + raise ArgumentsRequired(self.id + ' fetchFundingRateHistory() requires a since argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + until = self.safe_integer(params, 'until') + params = self.omit(params, 'until') + if since is not None: + request['startAt'] = since + if until is None: + request['endAt'] = self.milliseconds() + if until is not None: + request['endAt'] = until + response = self.utaGetMarketFundingRateHistory(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "symbol": "XBTUSDTM", + # "list": [ + # { + # "fundingRate": 7.6E-5, + # "ts": 1706097600000 + # }, + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + result = self.safe_list(data, 'list', []) + return self.parse_funding_rate_histories(result, market, since, limit) + + def parse_funding_rate_history(self, info, market: Market = None): + # + # { + # "fundingRate": 7.6E-5, + # "ts": 1706097600000 + # } + # + timestamp = self.safe_integer(info, 'ts') + return { + 'info': info, + 'symbol': self.safe_symbol(None, market), + 'fundingRate': self.safe_number(info, 'fundingRate'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + # + # the v2 URL is https://openapi-v2.kucoin.com/api/v1/endpoint + # ↑ ↑ + # ↑ ↑ + # + versions = self.safe_dict(self.options, 'versions', {}) + apiVersions = self.safe_dict(versions, api, {}) + methodVersions = self.safe_dict(apiVersions, method, {}) + defaultVersion = self.safe_string(methodVersions, path, self.options['version']) + version = self.safe_string(params, 'version', defaultVersion) + params = self.omit(params, 'version') + endpoint = '/api/' + version + '/' + self.implode_params(path, params) + if api == 'webExchange': + endpoint = '/' + self.implode_params(path, params) + if api == 'earn': + endpoint = '/api/v1/' + self.implode_params(path, params) + isUtaPrivate = False + if api == 'uta': + endpoint = '/api/ua/v1/' + self.implode_params(path, params) + if path == 'market/orderbook': + isUtaPrivate = True + query = self.omit(params, self.extract_params(path)) + endpart = '' + headers = headers if (headers is not None) else {} + url = self.urls['api'][api] + if not self.is_empty(query): + if ((method == 'GET') or (method == 'DELETE')) and (path != 'orders/multi-cancel'): + endpoint += '?' + self.rawencode(query) + else: + body = self.json(query) + endpart = body + headers['Content-Type'] = 'application/json' + url = url + endpoint + isFuturePrivate = (api == 'futuresPrivate') + isPrivate = (api == 'private') + isBroker = (api == 'broker') + isEarn = (api == 'earn') + if isPrivate or isFuturePrivate or isBroker or isEarn or isUtaPrivate: + self.check_required_credentials() + timestamp = str(self.nonce()) + headers = self.extend({ + 'KC-API-KEY-VERSION': '2', + 'KC-API-KEY': self.apiKey, + 'KC-API-TIMESTAMP': timestamp, + }, headers) + apiKeyVersion = self.safe_string(headers, 'KC-API-KEY-VERSION') + if apiKeyVersion == '2': + passphrase = self.hmac(self.encode(self.password), self.encode(self.secret), hashlib.sha256, 'base64') + headers['KC-API-PASSPHRASE'] = passphrase + else: + headers['KC-API-PASSPHRASE'] = self.password + payload = timestamp + method + endpoint + endpart + signature = self.hmac(self.encode(payload), self.encode(self.secret), hashlib.sha256, 'base64') + headers['KC-API-SIGN'] = signature + partner = self.safe_dict(self.options, 'partner', {}) + partner = self.safe_value(partner, 'future', partner) if isFuturePrivate else self.safe_value(partner, 'spot', partner) + partnerId = self.safe_string(partner, 'id') + partnerSecret = self.safe_string_2(partner, 'secret', 'key') + if (partnerId is not None) and (partnerSecret is not None): + partnerPayload = timestamp + partnerId + self.apiKey + partnerSignature = self.hmac(self.encode(partnerPayload), self.encode(partnerSecret), hashlib.sha256, 'base64') + headers['KC-API-PARTNER-SIGN'] = partnerSignature + headers['KC-API-PARTNER'] = partnerId + headers['KC-API-PARTNER-VERIFY'] = 'true' + if isBroker: + brokerName = self.safe_string(partner, 'name') + if brokerName is not None: + headers['KC-BROKER-NAME'] = brokerName + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if not response: + self.throw_broadly_matched_exception(self.exceptions['broad'], body, body) + return None + # + # bad + # {"code": "400100", "msg": "validation.createOrder.clientOidIsRequired"} + # good + # {code: '200000', data: {...}} + # + errorCode = self.safe_string(response, 'code') + message = self.safe_string_2(response, 'msg', 'data', '') + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], body, feedback) + if errorCode != '200000' and errorCode != '200': + raise ExchangeError(feedback) + return None diff --git a/ccxt/kucoinfutures.py b/ccxt/kucoinfutures.py new file mode 100644 index 0000000..30c2001 --- /dev/null +++ b/ccxt/kucoinfutures.py @@ -0,0 +1,3306 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.kucoin import kucoin +from ccxt.abstract.kucoinfutures import ImplicitAPI +from ccxt.base.types import Any, Balances, Currency, DepositAddress, Int, Leverage, LeverageTier, MarginMode, MarginModification, Market, Num, Order, OrderBook, OrderRequest, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, Trade, TradingFeeInterface, Transaction, TransferEntry +from typing import List +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import AccountSuspended +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import NotSupported +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.errors import InvalidNonce +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class kucoinfutures(kucoin, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(kucoinfutures, self).describe(), { + 'id': 'kucoinfutures', + 'name': 'KuCoin Futures', + 'countries': ['SC'], + 'rateLimit': 75, + 'version': 'v1', + 'certified': True, + 'pro': True, + 'comment': 'Platform 2.0', + 'quoteJsonNumbers': False, + 'has': { + 'CORS': None, + 'spot': False, + 'margin': False, + 'swap': True, + 'future': True, + 'option': False, + 'addMargin': True, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'cancelOrders': True, + 'closeAllPositions': False, + 'closePosition': True, + 'closePositions': False, + 'createDepositAddress': True, + 'createOrder': True, + 'createOrders': True, + 'createOrderWithTakeProfitAndStopLoss': True, + 'createReduceOnlyOrder': True, + 'createStopLimitOrder': True, + 'createStopLossOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'createTakeProfitOrder': True, + 'createTriggerOrder': True, + 'fetchAccounts': True, + 'fetchBalance': True, + 'fetchBidsAsks': True, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': False, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchDepositWithdrawFee': False, + 'fetchDepositWithdrawFees': False, + 'fetchFundingHistory': True, + 'fetchFundingInterval': True, + 'fetchFundingIntervals': False, + 'fetchFundingRate': True, + 'fetchFundingRateHistory': True, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchL3OrderBook': True, + 'fetchLedger': True, + 'fetchLeverage': True, + 'fetchLeverageTiers': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': True, + 'fetchMarketLeverageTiers': True, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrice': True, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchPosition': True, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': True, + 'fetchPositionsHistory': True, + 'fetchPremiumIndexOHLCV': False, + 'fetchStatus': True, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': True, + 'fetchTransactionFee': False, + 'fetchWithdrawals': True, + 'setLeverage': False, + 'setMarginMode': True, + 'setPositionMode': True, + 'transfer': True, + 'withdraw': None, + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/147508995-9e35030a-d046-43a1-a006-6fabd981b554.jpg', + 'doc': [ + 'https://docs.kucoin.com/futures', + 'https://docs.kucoin.com', + ], + 'www': 'https://futures.kucoin.com/', + 'referral': 'https://futures.kucoin.com/?rcode=E5wkqe', + 'api': { + 'public': 'https://openapi-v2.kucoin.com', + 'private': 'https://openapi-v2.kucoin.com', + 'futuresPrivate': 'https://api-futures.kucoin.com', + 'futuresPublic': 'https://api-futures.kucoin.com', + 'webExchange': 'https://futures.kucoin.com/_api/web-front', + }, + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + 'password': True, + }, + 'api': { + 'futuresPublic': { + 'get': { + 'contracts/active': 1, + 'contracts/{symbol}': 1, + 'contracts/risk-limit/{symbol}': 1, + 'ticker': 1, + 'allTickers': 1, + 'level2/snapshot': 1.33, + 'level2/depth{limit}': 1, + 'level2/message/query': 1, + 'level3/message/query': 1, # deprecated,level3/snapshot is suggested + 'level3/snapshot': 1, # v2 + 'trade/history': 1, + 'interest/query': 1, + 'index/query': 1, + 'mark-price/{symbol}/current': 1, + 'premium/query': 1, + 'funding-rate/{symbol}/current': 1, + 'timestamp': 1, + 'status': 1, + 'kline/query': 1, + }, + 'post': { + 'bullet-public': 1, + }, + }, + 'futuresPrivate': { + 'get': { + 'account-overview': 1.33, + 'transaction-history': 4.44, + 'deposit-address': 1, + 'deposit-list': 1, + 'withdrawals/quotas': 1, + 'withdrawal-list': 1, + 'transfer-list': 1, + 'orders': 1.33, + 'stopOrders': 1, + 'recentDoneOrders': 1, + 'orders/{orderId}': 1, # ?clientOid={client-order-id} # get order by orderId + 'orders/byClientOid': 1, # ?clientOid=eresc138b21023a909e5ad59 # get order by clientOid + 'fills': 4.44, + 'recentFills': 4.44, + 'openOrderStatistics': 1, + 'position': 1, + 'positions': 4.44, + 'funding-history': 4.44, + 'sub/api-key': 1, + 'trade-statistics': 1, + 'trade-fees': 1, + 'history-positions': 1, + 'getMaxOpenSize': 1, + 'getCrossUserLeverage': 1, + 'position/getMarginMode': 1, + }, + 'post': { + 'withdrawals': 1, + 'transfer-out': 1, # v2 + 'transfer-in': 1, + 'orders': 1.33, + 'st-orders': 1.33, + 'orders/test': 1.33, + 'position/margin/auto-deposit-status': 1, + 'position/margin/deposit-margin': 1, + 'position/risk-limit-level/change': 1, + 'bullet-private': 1, + 'sub/api-key': 1, + 'sub/api-key/update': 1, + 'changeCrossUserLeverage': 1, + 'position/changeMarginMode': 1, + 'position/switchPositionMode': 1, + }, + 'delete': { + 'withdrawals/{withdrawalId}': 1, + 'cancel/transfer-out': 1, + 'orders/{orderId}': 1, + 'orders': 4.44, + 'stopOrders': 1, + 'sub/api-key': 1, + 'orders/client-order/{clientOid}': 1, + 'orders/multi-cancel': 20, + }, + }, + 'webExchange': { + 'get': { + 'contract/{symbol}/funding-rates': 1, + }, + }, + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'exact': { + '400': BadRequest, # Bad Request -- Invalid request format + '401': AuthenticationError, # Unauthorized -- Invalid API Key + '403': NotSupported, # Forbidden -- The request is forbidden + '404': NotSupported, # Not Found -- The specified resource could not be found + '405': NotSupported, # Method Not Allowed -- You tried to access the resource with an invalid method. + '415': BadRequest, # Content-Type -- application/json + '429': RateLimitExceeded, # Too Many Requests -- Access limit breached + '500': ExchangeNotAvailable, # Internal Server Error -- We had a problem with our server. Try again later. + '503': ExchangeNotAvailable, # Service Unavailable -- We're temporarily offline for maintenance. Please try again later. + '100001': OrderNotFound, # {"msg":"error.getOrder.orderNotExist","code":"100001"} + '100004': BadRequest, # {"code":"100004","msg":"Order is in not cancelable state"} + '101030': PermissionDenied, # {"code":"101030","msg":"You haven't yet enabled the margin trading"} + '200004': InsufficientFunds, + '230003': InsufficientFunds, # {"code":"230003","msg":"Balance insufficient!"} + '260100': InsufficientFunds, # {"code":"260100","msg":"account.noBalance"} + '300003': InsufficientFunds, + '300012': InvalidOrder, + '400001': AuthenticationError, # Any of KC-API-KEY, KC-API-SIGN, KC-API-TIMESTAMP, KC-API-PASSPHRASE is missing in your request header. + '400002': InvalidNonce, # KC-API-TIMESTAMP Invalid -- Time differs from server time by more than 5 seconds + '400003': AuthenticationError, # KC-API-KEY not exists + '400004': AuthenticationError, # KC-API-PASSPHRASE error + '400005': AuthenticationError, # Signature error -- Please check your signature + '400006': AuthenticationError, # The IP address is not in the API whitelist + '400007': AuthenticationError, # Access Denied -- Your API key does not have sufficient permissions to access the URI + '404000': NotSupported, # URL Not Found -- The requested resource could not be found + '400100': BadRequest, # Parameter Error -- You tried to access the resource with invalid parameters + '411100': AccountSuspended, # User is frozen -- Please contact us via support center + '500000': ExchangeNotAvailable, # Internal Server Error -- We had a problem with our server. Try again later. + '300009': InvalidOrder, # {"msg":"No open positions to close.","code":"300009"} + '330008': InsufficientFunds, # {"msg":"Your current margin and leverage have reached the maximum open limit. Please increase your margin or raise your leverage to open larger positions.","code":"330008"} + }, + 'broad': { + 'Position does not exist': OrderNotFound, # {"code":"200000", "msg":"Position does not exist"} + }, + }, + 'fees': { + 'trading': { + 'tierBased': True, + 'percentage': True, + 'taker': self.parse_number('0.0006'), + 'maker': self.parse_number('0.0002'), + 'tiers': { + 'taker': [ + [self.parse_number('0'), self.parse_number('0.0006')], + [self.parse_number('50'), self.parse_number('0.0006')], + [self.parse_number('200'), self.parse_number('0.0006')], + [self.parse_number('500'), self.parse_number('0.0005')], + [self.parse_number('1000'), self.parse_number('0.0004')], + [self.parse_number('2000'), self.parse_number('0.0004')], + [self.parse_number('4000'), self.parse_number('0.00038')], + [self.parse_number('8000'), self.parse_number('0.00035')], + [self.parse_number('15000'), self.parse_number('0.00032')], + [self.parse_number('25000'), self.parse_number('0.0003')], + [self.parse_number('40000'), self.parse_number('0.0003')], + [self.parse_number('60000'), self.parse_number('0.0003')], + [self.parse_number('80000'), self.parse_number('0.0003')], + ], + 'maker': [ + [self.parse_number('0'), self.parse_number('0.02')], + [self.parse_number('50'), self.parse_number('0.015')], + [self.parse_number('200'), self.parse_number('0.01')], + [self.parse_number('500'), self.parse_number('0.01')], + [self.parse_number('1000'), self.parse_number('0.01')], + [self.parse_number('2000'), self.parse_number('0')], + [self.parse_number('4000'), self.parse_number('0')], + [self.parse_number('8000'), self.parse_number('0')], + [self.parse_number('15000'), self.parse_number('-0.003')], + [self.parse_number('25000'), self.parse_number('-0.006')], + [self.parse_number('40000'), self.parse_number('-0.009')], + [self.parse_number('60000'), self.parse_number('-0.012')], + [self.parse_number('80000'), self.parse_number('-0.015')], + ], + }, + }, + 'funding': { + 'tierBased': False, + 'percentage': False, + 'withdraw': {}, + 'deposit': {}, + }, + }, + 'commonCurrencies': { + 'HOT': 'HOTNOW', + 'EDGE': 'DADI', # https://github.com/ccxt/ccxt/issues/5756 + 'WAX': 'WAXP', + 'TRY': 'Trias', + 'VAI': 'VAIOT', + 'XBT': 'BTC', + }, + 'timeframes': { + '1m': 1, + '3m': None, + '5m': 5, + '15m': 15, + '30m': 30, + '1h': 60, + '2h': 120, + '4h': 240, + '6h': None, + '8h': 480, + '12h': 720, + '1d': 1440, + '1w': 10080, + }, + 'options': { + 'version': 'v1', + 'symbolSeparator': '-', + 'defaultType': 'swap', + 'code': 'USDT', + 'marginModes': {}, + 'marginTypes': {}, + # endpoint versions + 'versions': { + 'futuresPrivate': { + 'GET': { + 'getMaxOpenSize': 'v2', + 'getCrossUserLeverage': 'v2', + 'position/getMarginMode': 'v2', + }, + 'POST': { + 'transfer-out': 'v2', + 'changeCrossUserLeverage': 'v2', + 'position/changeMarginMode': 'v2', + 'position/switchPositionMode': 'v2', + }, + }, + 'futuresPublic': { + 'GET': { + 'level3/snapshot': 'v2', + }, + }, + }, + 'networks': { + 'OMNI': 'omni', + 'ERC20': 'eth', + 'TRC20': 'trx', + }, + # 'code': 'BTC', + # 'fetchBalance': { + # 'code': 'BTC', + # }, + }, + 'features': { + 'spot': None, + 'forDerivs': { + 'sandbox': False, + 'createOrder': { + 'marginMode': True, + 'triggerPrice': True, + 'triggerPriceType': { + 'last': True, + 'mark': True, + 'index': True, + }, + 'triggerDirection': True, + 'stopLossPrice': True, + 'takeProfitPrice': True, + 'attachedStopLossTakeProfit': { + 'triggerPriceType': None, + 'price': True, + }, + 'timeInForce': { + 'IOC': True, + 'FOK': False, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': True, # todo implement + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': True, # todo implement + 'iceberg': True, + }, + 'createOrders': { + 'max': 20, + }, + 'fetchMyTrades': { + 'marginMode': True, + 'limit': 1000, + 'daysBack': None, + 'untilDays': 7, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 1000, + 'trigger': True, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': None, + 'daysBackCanceled': None, + 'untilDays': None, + 'trigger': True, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 500, + }, + }, + 'swap': { + 'linear': { + 'extends': 'forDerivs', + }, + 'inverse': { + 'extends': 'forDerivs', + }, + }, + 'future': { + 'linear': { + 'extends': 'forDerivs', + }, + 'inverse': { + 'extends': 'forDerivs', + }, + }, + }, + }) + + def fetch_status(self, params={}): + """ + the latest known information on the availability of the exchange API + + https://www.kucoin.com/docs/rest/futures-trading/market-data/get-service-status + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `status structure ` + """ + response = self.futuresPublicGetStatus(params) + # + # { + # "code":"200000", + # "data":{ + # "status": "open", # open, close, cancelonly + # "msg": "upgrade match engine" # remark for operation when status not open + # } + # } + # + data = self.safe_dict(response, 'data', {}) + status = self.safe_string(data, 'status') + return { + 'status': 'ok' if (status == 'open') else 'maintenance', + 'updated': None, + 'eta': None, + 'url': None, + 'info': response, + } + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for kucoinfutures + + https://www.kucoin.com/docs/rest/futures-trading/market-data/get-symbols-list + + :param dict [params]: extra parameters specific to the exchange api endpoint + :returns dict[]: an array of objects representing market data + """ + response = self.futuresPublicGetContractsActive(params) + # + # { + # "code": "200000", + # "data": { + # "symbol": "ETHUSDTM", + # "rootSymbol": "USDT", + # "type": "FFWCSX", + # "firstOpenDate": 1591086000000, + # "expireDate": null, + # "settleDate": null, + # "baseCurrency": "ETH", + # "quoteCurrency": "USDT", + # "settleCurrency": "USDT", + # "maxOrderQty": 1000000, + # "maxPrice": 1000000.0000000000, + # "lotSize": 1, + # "tickSize": 0.05, + # "indexPriceTickSize": 0.01, + # "multiplier": 0.01, + # "initialMargin": 0.01, + # "maintainMargin": 0.005, + # "maxRiskLimit": 1000000, + # "minRiskLimit": 1000000, + # "riskStep": 500000, + # "makerFeeRate": 0.00020, + # "takerFeeRate": 0.00060, + # "takerFixFee": 0.0000000000, + # "makerFixFee": 0.0000000000, + # "settlementFee": null, + # "isDeleverage": True, + # "isQuanto": True, + # "isInverse": False, + # "markMethod": "FairPrice", + # "fairMethod": "FundingRate", + # "fundingBaseSymbol": ".ETHINT8H", + # "fundingQuoteSymbol": ".USDTINT8H", + # "fundingRateSymbol": ".ETHUSDTMFPI8H", + # "indexSymbol": ".KETHUSDT", + # "settlementSymbol": "", + # "status": "Open", + # "fundingFeeRate": 0.000535, + # "predictedFundingFeeRate": 0.002197, + # "openInterest": "8724443", + # "turnoverOf24h": 341156641.03354263, + # "volumeOf24h": 74833.54000000, + # "markPrice": 4534.07, + # "indexPrice":4531.92, + # "lastTradePrice": 4545.4500000000, + # "nextFundingRateTime": 25481884, + # "maxLeverage": 100, + # "sourceExchanges": ["huobi", "Okex", "Binance", "Kucoin", "Poloniex", "Hitbtc"], + # "premiumsSymbol1M": ".ETHUSDTMPI", + # "premiumsSymbol8H": ".ETHUSDTMPI8H", + # "fundingBaseSymbol1M": ".ETHINT", + # "fundingQuoteSymbol1M": ".USDTINT", + # "lowPrice": 4456.90, + # "highPrice": 4674.25, + # "priceChgPct": 0.0046, + # "priceChg": 21.15 + # } + # } + # + result = [] + data = self.safe_list(response, 'data', []) + for i in range(0, len(data)): + market = data[i] + id = self.safe_string(market, 'symbol') + expiry = self.safe_integer(market, 'expireDate') + future = self.safe_string(market, 'nextFundingRateTime') is None + swap = not future + baseId = self.safe_string(market, 'baseCurrency') + quoteId = self.safe_string(market, 'quoteCurrency') + settleId = self.safe_string(market, 'settleCurrency') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + settle = self.safe_currency_code(settleId) + symbol = base + '/' + quote + ':' + settle + type = 'swap' + if future: + symbol = symbol + '-' + self.yymmdd(expiry, '') + type = 'future' + inverse = self.safe_value(market, 'isInverse') + status = self.safe_string(market, 'status') + multiplier = self.safe_string(market, 'multiplier') + tickSize = self.safe_number(market, 'tickSize') + lotSize = self.safe_number(market, 'lotSize') + limitAmountMin = lotSize + if limitAmountMin is None: + limitAmountMin = self.safe_number(market, 'baseMinSize') + limitAmountMax = self.safe_number(market, 'maxOrderQty') + if limitAmountMax is None: + limitAmountMax = self.safe_number(market, 'baseMaxSize') + limitPriceMax = self.safe_number(market, 'maxPrice') + if limitPriceMax is None: + baseMinSizeString = self.safe_string(market, 'baseMinSize') + quoteMaxSizeString = self.safe_string(market, 'quoteMaxSize') + limitPriceMax = self.parse_number(Precise.string_div(quoteMaxSizeString, baseMinSizeString)) + result.append({ + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': type, + 'spot': False, + 'margin': False, + 'swap': swap, + 'future': future, + 'option': False, + 'active': (status == 'Open'), + 'contract': True, + 'linear': not inverse, + 'inverse': inverse, + 'taker': self.safe_number(market, 'takerFeeRate'), + 'maker': self.safe_number(market, 'makerFeeRate'), + 'contractSize': self.parse_number(Precise.string_abs(multiplier)), + 'expiry': expiry, + 'expiryDatetime': self.iso8601(expiry), + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': lotSize, + 'price': tickSize, + }, + 'limits': { + 'leverage': { + 'min': self.parse_number('1'), + 'max': self.safe_number(market, 'maxLeverage'), + }, + 'amount': { + 'min': limitAmountMin, + 'max': limitAmountMax, + }, + 'price': { + 'min': tickSize, + 'max': limitPriceMax, + }, + 'cost': { + 'min': self.safe_number(market, 'quoteMinSize'), + 'max': self.safe_number(market, 'quoteMaxSize'), + }, + }, + 'created': self.safe_integer(market, 'firstOpenDate'), + 'info': market, + }) + return result + + def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://www.kucoin.com/docs/rest/futures-trading/market-data/get-server-time + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = self.futuresPublicGetTimestamp(params) + # + # { + # "code": "200000", + # "data": 1637385119302, + # } + # + return self.safe_integer(response, 'data') + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://www.kucoin.com/docs/rest/futures-trading/market-data/get-klines + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate') + if paginate: + return self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params, 200) + market = self.market(symbol) + marketId = market['id'] + parsedTimeframe = self.safe_integer(self.timeframes, timeframe) + request: dict = { + 'symbol': marketId, + } + if parsedTimeframe is not None: + request['granularity'] = parsedTimeframe + else: + request['granularity'] = timeframe + duration = self.parse_timeframe(timeframe) * 1000 + endAt = self.milliseconds() + if since is not None: + request['from'] = since + if limit is None: + limit = self.safe_integer(self.options, 'fetchOHLCVLimit', 200) + endAt = self.sum(since, limit * duration) + elif limit is not None: + since = endAt - limit * duration + request['from'] = since + request['to'] = endAt + response = self.futuresPublicGetKlineQuery(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": [ + # [1636459200000, 4779.3, 4792.1, 4768.7, 4770.3, 78051], + # [1636460100000, 4770.25, 4778.55, 4757.55, 4777.25, 80164], + # [1636461000000, 4777.25, 4791.45, 4774.5, 4791.3, 51555] + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_ohlcvs(data, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # [ + # "1545904980000", # Start time of the candle cycle + # "0.058", # opening price + # "0.049", # closing price + # "0.058", # highest price + # "0.049", # lowest price + # "0.018", # base volume + # "0.000945", # quote volume + # ] + # + return [ + self.safe_integer(ohlcv, 0), + self.safe_number(ohlcv, 1), + self.safe_number(ohlcv, 2), + self.safe_number(ohlcv, 3), + self.safe_number(ohlcv, 4), + self.safe_number(ohlcv, 5), + ] + + def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://www.kucoin.com/docs/rest/funding/deposit/get-deposit-address + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + self.load_markets() + currency = self.currency(code) + currencyId = currency['id'] + request: dict = { + 'currency': currencyId, # Currency,including XBT,USDT + } + response = self.futuresPrivateGetDepositAddress(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "address": "0x78d3ad1c0aa1bf068e19c94a2d7b16c9c0fcd8b1",//Deposit address + # "memo": null//Address tag. If the returned value is null, it means that the requested token has no memo. If you are to transfer funds from another platform to KuCoin Futures and if the token to be #transferred has memo(tag), you need to fill in the memo to ensure the transferred funds will be sent #to the address you specified. + # } + # } + # + data = self.safe_dict(response, 'data', {}) + address = self.safe_string(data, 'address') + if currencyId != 'NIM': + # contains spaces + self.check_address(address) + return { + 'info': response, + 'currency': currencyId, + 'network': self.safe_string(data, 'chain'), + 'address': address, + 'tag': self.safe_string(data, 'memo'), + } + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://www.kucoin.com/docs/rest/futures-trading/market-data/get-part-order-book-level-2 + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + level = self.safe_number(params, 'level') + if level != 2 and level is not None: + raise BadRequest(self.id + ' fetchOrderBook() can only return level 2') + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + if (limit == 20) or (limit == 100): + request['limit'] = limit + else: + raise BadRequest(self.id + ' fetchOrderBook() limit argument must be 20 or 100') + else: + request['limit'] = 20 + response = self.futuresPublicGetLevel2DepthLimit(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "symbol": "XBTUSDM", #Symbol + # "sequence": 100, #Ticker sequence number + # "asks": [ + # ["5000.0", 1000], #Price, quantity + # ["6000.0", 1983] #Price, quantity + # ], + # "bids": [ + # ["3200.0", 800], #Price, quantity + # ["3100.0", 100] #Price, quantity + # ], + # "ts": 1604643655040584408 # timestamp + # } + # } + # + data = self.safe_dict(response, 'data', {}) + timestamp = self.parse_to_int(self.safe_integer(data, 'ts') / 1000000) + orderbook = self.parse_order_book(data, market['symbol'], timestamp, 'bids', 'asks', 0, 1) + orderbook['nonce'] = self.safe_integer(data, 'sequence') + return orderbook + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://www.kucoin.com/docs/rest/futures-trading/market-data/get-ticker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = self.futuresPublicGetTicker(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "sequence": 1638444978558, + # "symbol": "ETHUSDTM", + # "side": "sell", + # "size": 4, + # "price": "4229.35", + # "bestBidSize": 2160, + # "bestBidPrice": "4229.0", + # "bestAskPrice": "4229.05", + # "tradeId": "61aaa8b777a0c43055fe4851", + # "ts": 1638574296209786785, + # "bestAskSize": 36, + # } + # } + # + return self.parse_ticker(response['data'], market) + + def fetch_mark_price(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://www.kucoin.com/docs/rest/futures-trading/market-data/get-current-mark-price + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = self.futuresPublicGetMarkPriceSymbolCurrent(self.extend(request, params)) + # + return self.parse_ticker(response['data'], market) + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://www.kucoin.com/docs/rest/futures-trading/market-data/get-symbols-list + + :param str[] [symbols]: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.method]: the method to use, futuresPublicGetAllTickers or futuresPublicGetContractsActive + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + symbols = self.market_symbols(symbols) + method = None + method, params = self.handle_option_and_params(params, 'fetchTickers', 'method', 'futuresPublicGetContractsActive') + response: dict = None + if method == 'futuresPublicGetAllTickers': + response = self.futuresPublicGetAllTickers(params) + else: + response = self.futuresPublicGetContractsActive(params) + # + # { + # "code": "200000", + # "data": { + # "symbol": "ETHUSDTM", + # "rootSymbol": "USDT", + # "type": "FFWCSX", + # "firstOpenDate": 1591086000000, + # "expireDate": null, + # "settleDate": null, + # "baseCurrency": "ETH", + # "quoteCurrency": "USDT", + # "settleCurrency": "USDT", + # "maxOrderQty": 1000000, + # "maxPrice": 1000000.0000000000, + # "lotSize": 1, + # "tickSize": 0.05, + # "indexPriceTickSize": 0.01, + # "multiplier": 0.01, + # "initialMargin": 0.01, + # "maintainMargin": 0.005, + # "maxRiskLimit": 1000000, + # "minRiskLimit": 1000000, + # "riskStep": 500000, + # "makerFeeRate": 0.00020, + # "takerFeeRate": 0.00060, + # "takerFixFee": 0.0000000000, + # "makerFixFee": 0.0000000000, + # "settlementFee": null, + # "isDeleverage": True, + # "isQuanto": True, + # "isInverse": False, + # "markMethod": "FairPrice", + # "fairMethod": "FundingRate", + # "fundingBaseSymbol": ".ETHINT8H", + # "fundingQuoteSymbol": ".USDTINT8H", + # "fundingRateSymbol": ".ETHUSDTMFPI8H", + # "indexSymbol": ".KETHUSDT", + # "settlementSymbol": "", + # "status": "Open", + # "fundingFeeRate": 0.000535, + # "predictedFundingFeeRate": 0.002197, + # "openInterest": "8724443", + # "turnoverOf24h": 341156641.03354263, + # "volumeOf24h": 74833.54000000, + # "markPrice": 4534.07, + # "indexPrice":4531.92, + # "lastTradePrice": 4545.4500000000, + # "nextFundingRateTime": 25481884, + # "maxLeverage": 100, + # "sourceExchanges": ["huobi", "Okex", "Binance", "Kucoin", "Poloniex", "Hitbtc"], + # "premiumsSymbol1M": ".ETHUSDTMPI", + # "premiumsSymbol8H": ".ETHUSDTMPI8H", + # "fundingBaseSymbol1M": ".ETHINT", + # "fundingQuoteSymbol1M": ".USDTINT", + # "lowPrice": 4456.90, + # "highPrice": 4674.25, + # "priceChgPct": 0.0046, + # "priceChg": 21.15 + # } + # } + # + data = self.safe_list(response, 'data') + tickers = self.parse_tickers(data, symbols) + return self.filter_by_array_tickers(tickers, 'symbol', symbols) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "symbol": "LTCUSDTM", + # "granularity": 1000, + # "timePoint": 1727967339000, + # "value": 62.37, mark price + # "indexPrice": 62.37 + # } + # + # { + # "code": "200000", + # "data": { + # "sequence": 1629930362547, + # "symbol": "ETHUSDTM", + # "side": "buy", + # "size": 130, + # "price": "4724.7", + # "bestBidSize": 5, + # "bestBidPrice": "4724.6", + # "bestAskPrice": "4724.65", + # "tradeId": "618d2a5a77a0c4431d2335f4", + # "ts": 1636641371963227600, + # "bestAskSize": 1789 + # } + # } + # + # from fetchTickers + # + # { + # symbol: "XBTUSDTM", + # rootSymbol: "USDT", + # type: "FFWCSX", + # firstOpenDate: 1585555200000, + # expireDate: null, + # settleDate: null, + # baseCurrency: "XBT", + # quoteCurrency: "USDT", + # settleCurrency: "USDT", + # maxOrderQty: 1000000, + # maxPrice: 1000000, + # lotSize: 1, + # tickSize: 0.1, + # indexPriceTickSize: 0.01, + # multiplier: 0.001, + # initialMargin: 0.008, + # maintainMargin: 0.004, + # maxRiskLimit: 100000, + # minRiskLimit: 100000, + # riskStep: 50000, + # makerFeeRate: 0.0002, + # takerFeeRate: 0.0006, + # takerFixFee: 0, + # makerFixFee: 0, + # settlementFee: null, + # isDeleverage: True, + # isQuanto: True, + # isInverse: False, + # markMethod: "FairPrice", + # fairMethod: "FundingRate", + # fundingBaseSymbol: ".XBTINT8H", + # fundingQuoteSymbol: ".USDTINT8H", + # fundingRateSymbol: ".XBTUSDTMFPI8H", + # indexSymbol: ".KXBTUSDT", + # settlementSymbol: "", + # status: "Open", + # fundingFeeRate: 0.000297, + # predictedFundingFeeRate: 0.000327, + # fundingRateGranularity: 28800000, + # openInterest: "8033200", + # turnoverOf24h: 659795309.2524643, + # volumeOf24h: 9998.54, + # markPrice: 67193.51, + # indexPrice: 67184.81, + # lastTradePrice: 67191.8, + # nextFundingRateTime: 20022985, + # maxLeverage: 125, + # premiumsSymbol1M: ".XBTUSDTMPI", + # premiumsSymbol8H: ".XBTUSDTMPI8H", + # fundingBaseSymbol1M: ".XBTINT", + # fundingQuoteSymbol1M: ".USDTINT", + # lowPrice: 64041.6, + # highPrice: 67737.3, + # priceChgPct: 0.0447, + # priceChg: 2878.7 + # } + # + marketId = self.safe_string(ticker, 'symbol') + market = self.safe_market(marketId, market, '-') + last = self.safe_string_2(ticker, 'price', 'lastTradePrice') + timestamp = self.safe_integer_product(ticker, 'ts', 0.000001) + return self.safe_ticker({ + 'symbol': market['symbol'], + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'highPrice'), + 'low': self.safe_string(ticker, 'lowPrice'), + 'bid': self.safe_string(ticker, 'bestBidPrice'), + 'bidVolume': self.safe_string(ticker, 'bestBidSize'), + 'ask': self.safe_string(ticker, 'bestAskPrice'), + 'askVolume': self.safe_string(ticker, 'bestAskSize'), + 'vwap': None, + 'open': None, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': self.safe_string(ticker, 'priceChg'), + 'percentage': self.safe_string(ticker, 'priceChgPct'), + 'average': None, + 'baseVolume': self.safe_string(ticker, 'volumeOf24h'), + 'quoteVolume': self.safe_string(ticker, 'turnoverOf24h'), + 'markPrice': self.safe_string_2(ticker, 'markPrice', 'value'), + 'indexPrice': self.safe_string(ticker, 'indexPrice'), + 'info': ticker, + }, market) + + def fetch_bids_asks(self, symbols: Strings = None, params={}): + """ + fetches the bid and ask price and volume for multiple markets + :param str[] [symbols]: unified symbols of the markets to fetch the bids and asks for, all markets are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + request = { + 'method': 'futuresPublicGetAllTickers', + } + return self.fetch_tickers(symbols, self.extend(request, params)) + + def fetch_funding_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch the history of funding payments paid and received on self account + + https://www.kucoin.com/docs/rest/futures-trading/funding-fees/get-funding-history + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch funding history for + :param int [limit]: the maximum number of funding history structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding history structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchFundingHistory() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if since is not None: + request['startAt'] = since + if limit is not None: + # * Since is ignored if limit is defined + request['maxCount'] = limit + response = self.futuresPrivateGetFundingHistory(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "dataList": [ + # { + # "id": 239471298749817, + # "symbol": "ETHUSDTM", + # "timePoint": 1638532800000, + # "fundingRate": 0.000100, + # "markPrice": 4612.8300000000, + # "positionQty": 12, + # "positionCost": 553.5396000000, + # "funding": -0.0553539600, + # "settleCurrency": "USDT" + # }, + # ... + # ], + # "hasMore": True + # } + # } + # + data = self.safe_value(response, 'data') + dataList = self.safe_list(data, 'dataList', []) + fees = [] + for i in range(0, len(dataList)): + listItem = dataList[i] + timestamp = self.safe_integer(listItem, 'timePoint') + fees.append({ + 'info': listItem, + 'symbol': symbol, + 'code': self.safe_currency_code(self.safe_string(listItem, 'settleCurrency')), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': self.safe_number(listItem, 'id'), + 'amount': self.safe_number(listItem, 'funding'), + 'fundingRate': self.safe_number(listItem, 'fundingRate'), + 'markPrice': self.safe_number(listItem, 'markPrice'), + 'positionQty': self.safe_number(listItem, 'positionQty'), + 'positionCost': self.safe_number(listItem, 'positionCost'), + }) + return fees + + def fetch_position(self, symbol: str, params={}): + """ + + https://docs.kucoin.com/futures/#get-position-details + + fetch data on an open position + :param str symbol: unified market symbol of the market the position is held in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `position structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = self.futuresPrivateGetPosition(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "id": "6505ee6eaff4070001f651c4", + # "symbol": "XBTUSDTM", + # "autoDeposit": False, + # "maintMarginReq": 0, + # "riskLimit": 200, + # "realLeverage": 0.0, + # "crossMode": False, + # "delevPercentage": 0.0, + # "currentTimestamp": 1694887534594, + # "currentQty": 0, + # "currentCost": 0.0, + # "currentComm": 0.0, + # "unrealisedCost": 0.0, + # "realisedGrossCost": 0.0, + # "realisedCost": 0.0, + # "isOpen": False, + # "markPrice": 26611.71, + # "markValue": 0.0, + # "posCost": 0.0, + # "posCross": 0, + # "posInit": 0.0, + # "posComm": 0.0, + # "posLoss": 0.0, + # "posMargin": 0.0, + # "posMaint": 0.0, + # "maintMargin": 0.0, + # "realisedGrossPnl": 0.0, + # "realisedPnl": 0.0, + # "unrealisedPnl": 0.0, + # "unrealisedPnlPcnt": 0, + # "unrealisedRoePcnt": 0, + # "avgEntryPrice": 0.0, + # "liquidationPrice": 0.0, + # "bankruptPrice": 0.0, + # "settleCurrency": "USDT", + # "maintainMargin": 0, + # "riskLimitLevel": 1 + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_position(data, market) + + def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://docs.kucoin.com/futures/#get-position-list + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + self.load_markets() + response = self.futuresPrivateGetPositions(params) + # + # { + # "code": "200000", + # "data": [ + # { + # "id": "615ba79f83a3410001cde321", + # "symbol": "ETHUSDTM", + # "autoDeposit": False, + # "maintMarginReq": 0.005, + # "riskLimit": 1000000, + # "realLeverage": 18.61, + # "crossMode": False, + # "delevPercentage": 0.86, + # "openingTimestamp": 1638563515618, + # "currentTimestamp": 1638576872774, + # "currentQty": 2, + # "currentCost": 83.64200000, + # "currentComm": 0.05018520, + # "unrealisedCost": 83.64200000, + # "realisedGrossCost": 0.00000000, + # "realisedCost": 0.05018520, + # "isOpen": True, + # "markPrice": 4225.01, + # "markValue": 84.50020000, + # "posCost": 83.64200000, + # "posCross": 0.0000000000, + # "posInit": 3.63660870, + # "posComm": 0.05236717, + # "posLoss": 0.00000000, + # "posMargin": 3.68897586, + # "posMaint": 0.50637594, + # "maintMargin": 4.54717586, + # "realisedGrossPnl": 0.00000000, + # "realisedPnl": -0.05018520, + # "unrealisedPnl": 0.85820000, + # "unrealisedPnlPcnt": 0.0103, + # "unrealisedRoePcnt": 0.2360, + # "avgEntryPrice": 4182.10, + # "liquidationPrice": 4023.00, + # "bankruptPrice": 4000.25, + # "settleCurrency": "USDT", + # "isInverse": False + # } + # ] + # } + # + data = self.safe_list(response, 'data') + return self.parse_positions(data, symbols) + + def fetch_positions_history(self, symbols: Strings = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical positions + + https://www.kucoin.com/docs/rest/futures-trading/positions/get-positions-history + + :param str[] [symbols]: list of unified market symbols + :param int [since]: the earliest time in ms to fetch position history for + :param int [limit]: the maximum number of entries to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: closing end time + :param int [params.pageId]: page id + :returns dict[]: a list of `position structure ` + """ + self.load_markets() + if limit is None: + limit = 200 + request: dict = { + 'limit': limit, + } + if since is not None: + request['from'] = since + until = self.safe_integer(params, 'until') + if until is not None: + params = self.omit(params, 'until') + request['to'] = until + response = self.futuresPrivateGetHistoryPositions(self.extend(request, params)) + # + # { + # "success": True, + # "code": "200", + # "msg": "success", + # "retry": False, + # "data": { + # "currentPage": 1, + # "pageSize": 10, + # "totalNum": 25, + # "totalPage": 3, + # "items": [ + # { + # "closeId": "300000000000000030", + # "positionId": "300000000000000009", + # "uid": 99996908309485, + # "userId": "6527d4fc8c7f3d0001f40f5f", + # "symbol": "XBTUSDM", + # "settleCurrency": "XBT", + # "leverage": "0.0", + # "type": "LIQUID_LONG", + # "side": null, + # "closeSize": null, + # "pnl": "-1.0000003793999999", + # "realisedGrossCost": "0.9993849748999999", + # "withdrawPnl": "0.0", + # "roe": null, + # "tradeFee": "0.0006154045", + # "fundingFee": "0.0", + # "openTime": 1713785751181, + # "closeTime": 1713785752784, + # "openPrice": null, + # "closePrice": null + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'data') + items = self.safe_list(data, 'items', []) + return self.parse_positions(items, symbols) + + def parse_position(self, position: dict, market: Market = None): + # + # { + # "code": "200000", + # "data": [ + # { + # "id": "615ba79f83a3410001cde321", # Position ID + # "symbol": "ETHUSDTM", # Symbol + # "autoDeposit": False, # Auto deposit margin or not + # "maintMarginReq": 0.005, # Maintenance margin requirement + # "riskLimit": 1000000, # Risk limit + # "realLeverage": 25.92, # Leverage of the order + # "crossMode": False, # Cross mode or not + # "delevPercentage": 0.76, # ADL ranking percentile + # "openingTimestamp": 1638578546031, # Open time + # "currentTimestamp": 1638578563580, # Current timestamp + # "currentQty": 2, # Current postion quantity + # "currentCost": 83.787, # Current postion value + # "currentComm": 0.0167574, # Current commission + # "unrealisedCost": 83.787, # Unrealised value + # "realisedGrossCost": 0.0, # Accumulated realised gross profit value + # "realisedCost": 0.0167574, # Current realised position value + # "isOpen": True, # Opened position or not + # "markPrice": 4183.38, # Mark price + # "markValue": 83.6676, # Mark value + # "posCost": 83.787, # Position value + # "posCross": 0.0, # added margin + # "posInit": 3.35148, # Leverage margin + # "posComm": 0.05228309, # Bankruptcy cost + # "posLoss": 0.0, # Funding fees paid out + # "posMargin": 3.40376309, # Position margin + # "posMaint": 0.50707892, # Maintenance margin + # "maintMargin": 3.28436309, # Position margin + # "realisedGrossPnl": 0.0, # Accumulated realised gross profit value + # "realisedPnl": -0.0167574, # Realised profit and loss + # "unrealisedPnl": -0.1194, # Unrealised profit and loss + # "unrealisedPnlPcnt": -0.0014, # Profit-loss ratio of the position + # "unrealisedRoePcnt": -0.0356, # Rate of return on investment + # "avgEntryPrice": 4189.35, # Average entry price + # "liquidationPrice": 4044.55, # Liquidation price + # "bankruptPrice": 4021.75, # Bankruptcy price + # "settleCurrency": "USDT", # Currency used to clear and settle the trades + # "isInverse": False + # } + # ] + # } + # position history + # { + # "closeId": "300000000000000030", + # "positionId": "300000000000000009", + # "uid": 99996908309485, + # "userId": "6527d4fc8c7f3d0001f40f5f", + # "symbol": "XBTUSDM", + # "settleCurrency": "XBT", + # "leverage": "0.0", + # "type": "LIQUID_LONG", + # "side": null, + # "closeSize": null, + # "pnl": "-1.0000003793999999", + # "realisedGrossCost": "0.9993849748999999", + # "withdrawPnl": "0.0", + # "roe": null, + # "tradeFee": "0.0006154045", + # "fundingFee": "0.0", + # "openTime": 1713785751181, + # "closeTime": 1713785752784, + # "openPrice": null, + # "closePrice": null + # } + # + symbol = self.safe_string(position, 'symbol') + market = self.safe_market(symbol, market) + timestamp = self.safe_integer(position, 'currentTimestamp') + size = self.safe_string(position, 'currentQty') + side = None + type = self.safe_string_lower(position, 'type') + if size is not None: + if Precise.string_gt(size, '0'): + side = 'long' + elif Precise.string_lt(size, '0'): + side = 'short' + elif type is not None: + if type.find('long') > -1: + side = 'long' + else: + side = 'short' + notional = Precise.string_abs(self.safe_string(position, 'posCost')) + initialMargin = self.safe_string(position, 'posInit') + initialMarginPercentage = Precise.string_div(initialMargin, notional) + # marginRatio = Precise.string_div(maintenanceRate, collateral) + unrealisedPnl = self.safe_string(position, 'unrealisedPnl') + crossMode = self.safe_value(position, 'crossMode') + # currently crossMode is always set to False and only isolated positions are supported + marginMode = None + if crossMode is not None: + marginMode = 'cross' if crossMode else 'isolated' + return self.safe_position({ + 'info': position, + 'id': self.safe_string_2(position, 'id', 'positionId'), + 'symbol': self.safe_string(market, 'symbol'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastUpdateTimestamp': self.safe_integer(position, 'closeTime'), + 'initialMargin': self.parse_number(initialMargin), + 'initialMarginPercentage': self.parse_number(initialMarginPercentage), + 'maintenanceMargin': self.safe_number(position, 'posMaint'), + 'maintenanceMarginPercentage': self.safe_number(position, 'maintMarginReq'), + 'entryPrice': self.safe_number_2(position, 'avgEntryPrice', 'openPrice'), + 'notional': self.parse_number(notional), + 'leverage': self.safe_number_2(position, 'realLeverage', 'leverage'), + 'unrealizedPnl': self.parse_number(unrealisedPnl), + 'contracts': self.parse_number(Precise.string_abs(size)), + 'contractSize': self.safe_value(market, 'contractSize'), + 'realizedPnl': self.safe_number_2(position, 'realisedPnl', 'pnl'), + 'marginRatio': None, + 'liquidationPrice': self.safe_number(position, 'liquidationPrice'), + 'markPrice': self.safe_number(position, 'markPrice'), + 'lastPrice': None, + 'collateral': self.safe_number(position, 'maintMargin'), + 'marginMode': marginMode, + 'side': side, + 'percentage': None, + 'stopLossPrice': None, + 'takeProfitPrice': None, + }) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + Create an order on the exchange + + https://www.kucoin.com/docs/rest/futures-trading/orders/place-order + https://www.kucoin.com/docs/rest/futures-trading/orders/place-take-profit-and-stop-loss-order#http-request + + :param str symbol: Unified CCXT market symbol + :param str type: 'limit' or 'market' + :param str side: 'buy' or 'sell' + :param float amount: the amount of currency to trade + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param dict [params.takeProfit]: *takeProfit object in params* containing the triggerPrice at which the attached take profit order will be triggered and the triggerPriceType + :param dict [params.stopLoss]: *stopLoss object in params* containing the triggerPrice at which the attached stop loss order will be triggered and the triggerPriceType + :param float [params.triggerPrice]: The price a trigger order is triggered at + :param float [params.stopLossPrice]: price to trigger stop-loss orders + :param float [params.takeProfitPrice]: price to trigger take-profit orders + :param bool [params.reduceOnly]: A mark to reduce the position size only. Set to False by default. Need to set the position size when reduceOnly is True. + :param str [params.timeInForce]: GTC, GTT, IOC, or FOK, default is GTC, limit orders only + :param str [params.postOnly]: Post only flag, invalid when timeInForce is IOC or FOK + :param float [params.cost]: the cost of the order in units of USDT + :param str [params.marginMode]: 'cross' or 'isolated', default is 'isolated' + :param bool [params.hedged]: *swap and future only* True for hedged mode, False for one way mode, default is False + ----------------- Exchange Specific Parameters ----------------- + :param float [params.leverage]: Leverage size of the order(mandatory param in request, default is 1) + :param str [params.clientOid]: client order id, defaults to uuid if not passed + :param str [params.remark]: remark for the order, length cannot exceed 100 utf8 characters + :param str [params.stop]: 'up' or 'down', the direction the triggerPrice is triggered from, requires triggerPrice. down: Triggers when the price reaches or goes below the triggerPrice. up: Triggers when the price reaches or goes above the triggerPrice. + :param str [params.triggerPriceType]: "last", "mark", "index" - defaults to "mark" + :param str [params.stopPriceType]: exchange-specific alternative for triggerPriceType: TP, IP or MP + :param bool [params.closeOrder]: set to True to close position + :param bool [params.test]: set to True to use the test order endpoint(does not submit order, use to validate params) + :param bool [params.forceHold]: A mark to forcely hold the funds for an order, even though it's an order to reduce the position size. This helps the order stay on the order book and not get canceled when the position size changes. Set to False by default.\ + :param str [params.positionSide]: *swap and future only* hedged two-way position side, LONG or SHORT + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + testOrder = self.safe_bool(params, 'test', False) + params = self.omit(params, 'test') + isTpAndSlOrder = (self.safe_value(params, 'stopLoss') is not None) or (self.safe_value(params, 'takeProfit') is not None) + orderRequest = self.create_contract_order_request(symbol, type, side, amount, price, params) + response = None + if testOrder: + response = self.futuresPrivatePostOrdersTest(orderRequest) + else: + if isTpAndSlOrder: + response = self.futuresPrivatePostStOrders(orderRequest) + else: + response = self.futuresPrivatePostOrders(orderRequest) + # + # { + # "code": "200000", + # "data": { + # "orderId": "619717484f1d010001510cde", + # }, + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data, market) + + def create_orders(self, orders: List[OrderRequest], params={}): + """ + create a list of trade orders + + https://www.kucoin.com/docs/rest/futures-trading/orders/place-multiple-orders + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + ordersRequests = [] + for i in range(0, len(orders)): + rawOrder = orders[i] + symbol = self.safe_string(rawOrder, 'symbol') + market = self.market(symbol) + type = self.safe_string(rawOrder, 'type') + side = self.safe_string(rawOrder, 'side') + amount = self.safe_value(rawOrder, 'amount') + price = self.safe_value(rawOrder, 'price') + orderParams = self.safe_value(rawOrder, 'params', {}) + orderRequest = self.create_contract_order_request(market['id'], type, side, amount, price, orderParams) + ordersRequests.append(orderRequest) + response = self.futuresPrivatePostOrdersMulti(ordersRequests) + # + # { + # "code": "200000", + # "data": [ + # { + # "orderId": "135241412609331200", + # "clientOid": "3d8fcc13-0b13-447f-ad30-4b3441e05213", + # "symbol": "LTCUSDTM", + # "code": "200000", + # "msg": "success" + # }, + # { + # "orderId": "135241412747743234", + # "clientOid": "b878c7ee-ae3e-4d63-a20b-038acbb7306f", + # "symbol": "LTCUSDTM", + # "code": "200000", + # "msg": "success" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_orders(data) + + def create_contract_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + market = self.market(symbol) + # required param, cannot be used twice + clientOrderId = self.safe_string_2(params, 'clientOid', 'clientOrderId', self.uuid()) + params = self.omit(params, ['clientOid', 'clientOrderId']) + request: dict = { + 'clientOid': clientOrderId, + 'side': side, + 'symbol': market['id'], + 'type': type, # limit or market + 'leverage': 1, + } + marginModeUpper = self.safe_string_upper(params, 'marginMode') + if marginModeUpper is not None: + params = self.omit(params, 'marginMode') + request['marginMode'] = marginModeUpper + cost = self.safe_string(params, 'cost') + params = self.omit(params, 'cost') + if cost is not None: + request['valueQty'] = self.cost_to_precision(symbol, cost) + else: + if amount < 1: + raise InvalidOrder(self.id + ' createOrder() minimum contract order amount is 1') + request['size'] = int(self.amount_to_precision(symbol, amount)) + triggerPrice, stopLossPrice, takeProfitPrice = self.handle_trigger_prices(params) + stopLoss = self.safe_dict(params, 'stopLoss') + takeProfit = self.safe_dict(params, 'takeProfit') + # isTpAndSl = stopLossPrice and takeProfitPrice + triggerPriceTypes: dict = { + 'mark': 'MP', + 'last': 'TP', + 'index': 'IP', + } + triggerPriceType = self.safe_string(params, 'triggerPriceType', 'mark') + triggerPriceTypeValue = self.safe_string(triggerPriceTypes, triggerPriceType, triggerPriceType) + params = self.omit(params, ['stopLossPrice', 'takeProfitPrice', 'triggerPrice', 'stopPrice', 'takeProfit', 'stopLoss']) + if triggerPrice: + request['stop'] = 'up' if (side == 'buy') else 'down' + request['stopPrice'] = self.price_to_precision(symbol, triggerPrice) + request['stopPriceType'] = triggerPriceTypeValue + elif stopLoss is not None or takeProfit is not None: + priceType = triggerPriceTypeValue + if stopLoss is not None: + slPrice = self.safe_string_2(stopLoss, 'triggerPrice', 'stopPrice') + request['triggerStopDownPrice'] = self.price_to_precision(symbol, slPrice) + priceType = self.safe_string(stopLoss, 'triggerPriceType', 'mark') + priceType = self.safe_string(triggerPriceTypes, priceType, priceType) + if takeProfit is not None: + tpPrice = self.safe_string_2(takeProfit, 'triggerPrice', 'takeProfitPrice') + request['triggerStopUpPrice'] = self.price_to_precision(symbol, tpPrice) + priceType = self.safe_string(takeProfit, 'triggerPriceType', 'mark') + priceType = self.safe_string(triggerPriceTypes, priceType, priceType) + request['stopPriceType'] = priceType + elif stopLossPrice or takeProfitPrice: + if stopLossPrice: + request['stop'] = 'up' if (side == 'buy') else 'down' + request['stopPrice'] = self.price_to_precision(symbol, stopLossPrice) + else: + request['stop'] = 'down' if (side == 'buy') else 'up' + request['stopPrice'] = self.price_to_precision(symbol, takeProfitPrice) + request['reduceOnly'] = True + request['stopPriceType'] = triggerPriceTypeValue + uppercaseType = type.upper() + timeInForce = self.safe_string_upper(params, 'timeInForce') + if uppercaseType == 'LIMIT': + if price is None: + raise ArgumentsRequired(self.id + ' createOrder() requires a price argument for limit orders') + else: + request['price'] = self.price_to_precision(symbol, price) + if timeInForce is not None: + request['timeInForce'] = timeInForce + postOnly = None + postOnly, params = self.handle_post_only(type == 'market', False, params) + if postOnly: + request['postOnly'] = True + hidden = self.safe_value(params, 'hidden') + if postOnly and (hidden is not None): + raise BadRequest(self.id + ' createOrder() does not support the postOnly parameter together with a hidden parameter') + iceberg = self.safe_value(params, 'iceberg') + if iceberg: + visibleSize = self.safe_value(params, 'visibleSize') + if visibleSize is None: + raise ArgumentsRequired(self.id + ' createOrder() requires a visibleSize parameter for iceberg orders') + reduceOnly = self.safe_bool(params, 'reduceOnly', False) + hedged = None + hedged, params = self.handle_param_bool(params, 'hedged', False) + if reduceOnly: + request['reduceOnly'] = reduceOnly + if hedged: + reduceOnlyPosSide = 'LONG' if (side == 'sell') else 'SHORT' + request['positionSide'] = reduceOnlyPosSide + else: + if hedged: + posSide = 'LONG' if (side == 'buy') else 'SHORT' + request['positionSide'] = posSide + params = self.omit(params, ['timeInForce', 'stopPrice', 'triggerPrice', 'stopLossPrice', 'takeProfitPrice', 'reduceOnly', 'hedged']) # Time in force only valid for limit orders, exchange error when gtc for market orders + return self.extend(request, params) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://www.kucoin.com/docs/rest/futures-trading/orders/cancel-futures-order-by-orderid + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.clientOrderId]: cancel order by client order id + :returns dict: An `order structure ` + """ + self.load_markets() + clientOrderId = self.safe_string_2(params, 'clientOid', 'clientOrderId') + params = self.omit(params, ['clientOrderId']) + request: dict = {} + response = None + if clientOrderId is not None: + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument when cancelling by clientOrderId') + market = self.market(symbol) + request['symbol'] = market['id'] + request['clientOid'] = clientOrderId + response = self.futuresPrivateDeleteOrdersClientOrderClientOid(self.extend(request, params)) + else: + request['orderId'] = id + response = self.futuresPrivateDeleteOrdersOrderId(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "cancelledOrderIds": [ + # "619714b8b6353000014c505a", + # ], + # }, + # } + # + return self.safe_order({'info': response}) + + def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): + """ + cancel multiple orders + + https://www.kucoin.com/docs/rest/futures-trading/orders/batch-cancel-orders + + :param str[] ids: order ids + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str[] [params.clientOrderIds]: client order ids + :returns dict: an list of `order structures ` + """ + self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + ordersRequests = [] + clientOrderIds = self.safe_list_2(params, 'clientOrderIds', 'clientOids', []) + params = self.omit(params, ['clientOrderIds', 'clientOids']) + useClientorderId = False + for i in range(0, len(clientOrderIds)): + useClientorderId = True + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrders() requires a symbol argument when cancelling by clientOrderIds') + ordersRequests.append({ + 'symbol': market['id'], + 'clientOid': self.safe_string(clientOrderIds, i), + }) + for i in range(0, len(ids)): + ordersRequests.append(ids[i]) + requestKey = 'clientOidsList' if useClientorderId else 'orderIdsList' + request: dict = {} + request[requestKey] = ordersRequests + response = self.futuresPrivateDeleteOrdersMultiCancel(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": + # [ + # { + # "orderId": "80465574458560512", + # "clientOid": null, + # "code": "200", + # "msg": "success" + # }, + # { + # "orderId": "80465575289094144", + # "clientOid": null, + # "code": "200", + # "msg": "success" + # } + # ] + # } + # + orders = self.safe_list(response, 'data', []) + return self.parse_orders(orders, market) + + def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders + + https://www.kucoin.com/docs/rest/futures-trading/orders/cancel-multiple-futures-limit-orders + https://www.kucoin.com/docs/rest/futures-trading/orders/cancel-multiple-futures-stop-orders + + :param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param dict [params.trigger]: When True, all the trigger orders will be cancelled + :returns: Response from the exchange + """ + self.load_markets() + request: dict = {} + if symbol is not None: + request['symbol'] = self.market_id(symbol) + trigger = self.safe_value_2(params, 'stop', 'trigger') + params = self.omit(params, ['stop', 'trigger']) + response = None + if trigger: + response = self.futuresPrivateDeleteStopOrders(self.extend(request, params)) + else: + response = self.futuresPrivateDeleteOrders(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "cancelledOrderIds": [ + # "619714b8b6353000014c505a", + # ], + # }, + # } + # + data = self.safe_dict(response, 'data') + return [self.safe_order({'info': data})] + + def add_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + add margin + + https://www.kucoin.com/docs/rest/futures-trading/positions/add-margin-manually + + :param str symbol: unified market symbol + :param float amount: amount of margin to add + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin structure ` + """ + self.load_markets() + market = self.market(symbol) + uuid = self.uuid() + request: dict = { + 'symbol': market['id'], + 'margin': self.amount_to_precision(symbol, amount), + 'bizNo': uuid, + } + response = self.futuresPrivatePostPositionMarginDepositMargin(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "id": "62311d26064e8f00013f2c6d", + # "symbol": "XRPUSDTM", + # "autoDeposit": False, + # "maintMarginReq": 0.01, + # "riskLimit": 200000, + # "realLeverage": 0.88, + # "crossMode": False, + # "delevPercentage": 0.4, + # "openingTimestamp": 1647385894798, + # "currentTimestamp": 1647414510672, + # "currentQty": -1, + # "currentCost": -7.658, + # "currentComm": 0.0053561, + # "unrealisedCost": -7.658, + # "realisedGrossCost": 0, + # "realisedCost": 0.0053561, + # "isOpen": True, + # "markPrice": 0.7635, + # "markValue": -7.635, + # "posCost": -7.658, + # "posCross": 1.00016084, + # "posInit": 7.658, + # "posComm": 0.00979006, + # "posLoss": 0, + # "posMargin": 8.6679509, + # "posMaint": 0.08637006, + # "maintMargin": 8.6909509, + # "realisedGrossPnl": 0, + # "realisedPnl": -0.0038335, + # "unrealisedPnl": 0.023, + # "unrealisedPnlPcnt": 0.003, + # "unrealisedRoePcnt": 0.003, + # "avgEntryPrice": 0.7658, + # "liquidationPrice": 1.6239, + # "bankruptPrice": 1.6317, + # "settleCurrency": "USDT" + # } + # } + # + # + # { + # "code":"200000", + # "msg":"Position does not exist" + # } + # + data = self.safe_value(response, 'data') + return self.extend(self.parse_margin_modification(data, market), { + 'amount': self.amount_to_precision(symbol, amount), + 'direction': 'in', + }) + + def parse_margin_modification(self, info, market: Market = None) -> MarginModification: + # + # { + # "id": "62311d26064e8f00013f2c6d", + # "symbol": "XRPUSDTM", + # "autoDeposit": False, + # "maintMarginReq": 0.01, + # "riskLimit": 200000, + # "realLeverage": 0.88, + # "crossMode": False, + # "delevPercentage": 0.4, + # "openingTimestamp": 1647385894798, + # "currentTimestamp": 1647414510672, + # "currentQty": -1, + # "currentCost": -7.658, + # "currentComm": 0.0053561, + # "unrealisedCost": -7.658, + # "realisedGrossCost": 0, + # "realisedCost": 0.0053561, + # "isOpen": True, + # "markPrice": 0.7635, + # "markValue": -7.635, + # "posCost": -7.658, + # "posCross": 1.00016084, + # "posInit": 7.658, + # "posComm": 0.00979006, + # "posLoss": 0, + # "posMargin": 8.6679509, + # "posMaint": 0.08637006, + # "maintMargin": 8.6909509, + # "realisedGrossPnl": 0, + # "realisedPnl": -0.0038335, + # "unrealisedPnl": 0.023, + # "unrealisedPnlPcnt": 0.003, + # "unrealisedRoePcnt": 0.003, + # "avgEntryPrice": 0.7658, + # "liquidationPrice": 1.6239, + # "bankruptPrice": 1.6317, + # "settleCurrency": "USDT" + # } + # + # { + # "code":"200000", + # "msg":"Position does not exist" + # } + # + id = self.safe_string(info, 'id') + market = self.safe_market(id, market) + currencyId = self.safe_string(info, 'settleCurrency') + crossMode = self.safe_value(info, 'crossMode') + mode = 'cross' if crossMode else 'isolated' + marketId = self.safe_string(market, 'symbol') + timestamp = self.safe_integer(info, 'currentTimestamp') + return { + 'info': info, + 'symbol': self.safe_symbol(marketId, market), + 'type': None, + 'marginMode': mode, + 'amount': None, + 'total': None, + 'code': self.safe_currency_code(currencyId), + 'status': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + + def fetch_orders_by_status(self, status, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches a list of orders placed on the exchange + + https://docs.kucoin.com/futures/#get-order-list + https://docs.kucoin.com/futures/#get-untriggered-stop-order-list + + :param str status: 'active' or 'closed', only 'active' is valid for stop orders + :param str symbol: unified symbol for the market to retrieve orders from + :param int [since]: timestamp in ms of the earliest order to retrieve + :param int [limit]: The maximum number of orders to retrieve + :param dict [params]: exchange specific parameters + :param bool [params.trigger]: set to True to retrieve untriggered stop orders + :param int [params.until]: End time in ms + :param str [params.side]: buy or sell + :param str [params.type]: limit or market + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns: An `array of order structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOrdersByStatus', 'paginate') + if paginate: + return self.fetch_paginated_call_dynamic('fetchOrdersByStatus', symbol, since, limit, params) + trigger = self.safe_bool_2(params, 'stop', 'trigger') + until = self.safe_integer(params, 'until') + params = self.omit(params, ['stop', 'until', 'trigger']) + if status == 'closed': + status = 'done' + elif status == 'open': + status = 'active' + request: dict = {} + if not trigger: + request['status'] = status + elif status != 'active': + raise BadRequest(self.id + ' fetchOrdersByStatus() can only fetch untriggered stop orders') + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if since is not None: + request['startAt'] = since + if until is not None: + request['endAt'] = until + response = None + if trigger: + response = self.futuresPrivateGetStopOrders(self.extend(request, params)) + else: + response = self.futuresPrivateGetOrders(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "currentPage": 1, + # "pageSize": 50, + # "totalNum": 4, + # "totalPage": 1, + # "items": [ + # { + # "id": "64507d02921f1c0001ff6892", + # "symbol": "XBTUSDTM", + # "type": "market", + # "side": "buy", + # "price": null, + # "size": 1, + # "value": "27.992", + # "dealValue": "27.992", + # "dealSize": 1, + # "stp": "", + # "stop": "", + # "stopPriceType": "", + # "stopTriggered": False, + # "stopPrice": null, + # "timeInForce": "GTC", + # "postOnly": False, + # "hidden": False, + # "iceberg": False, + # "leverage": "17", + # "forceHold": False, + # "closeOrder": False, + # "visibleSize": null, + # "clientOid": null, + # "remark": null, + # "tags": null, + # "isActive": False, + # "cancelExist": False, + # "createdAt": 1682996482000, + # "updatedAt": 1682996483062, + # "endAt": 1682996483062, + # "orderTime": 1682996482953900677, + # "settleCurrency": "USDT", + # "status": "done", + # "filledValue": "27.992", + # "filledSize": 1, + # "reduceOnly": False + # } + # ] + # } + # } + # + responseData = self.safe_dict(response, 'data', {}) + orders = self.safe_list(responseData, 'items', []) + return self.parse_orders(orders, market, since, limit) + + def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://docs.kucoin.com/futures/#get-order-list + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: end time in ms + :param str [params.side]: buy or sell + :param str [params.type]: limit, or market + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchClosedOrders', 'paginate') + if paginate: + return self.fetch_paginated_call_dynamic('fetchClosedOrders', symbol, since, limit, params) + return self.fetch_orders_by_status('done', symbol, since, limit, params) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple open orders made by the user + + https://docs.kucoin.com/futures/#get-order-list + https://docs.kucoin.com/futures/#get-untriggered-stop-order-list + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: end time in ms + :param str [params.side]: buy or sell + :param str [params.type]: limit, or market + :param boolean [params.trigger]: set to True to retrieve untriggered stop orders + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOpenOrders', 'paginate') + if paginate: + return self.fetch_paginated_call_dynamic('fetchOpenOrders', symbol, since, limit, params) + return self.fetch_orders_by_status('open', symbol, since, limit, params) + + def fetch_order(self, id: Str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://docs.kucoin.com/futures/#get-details-of-a-single-order + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + request: dict = {} + response = None + if id is None: + clientOrderId = self.safe_string_2(params, 'clientOid', 'clientOrderId') + if clientOrderId is None: + raise InvalidOrder(self.id + ' fetchOrder() requires parameter id or params.clientOid') + request['clientOid'] = clientOrderId + params = self.omit(params, ['clientOid', 'clientOrderId']) + response = self.futuresPrivateGetOrdersByClientOid(self.extend(request, params)) + else: + request['orderId'] = id + response = self.futuresPrivateGetOrdersOrderId(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "id": "64507d02921f1c0001ff6892", + # "symbol": "XBTUSDTM", + # "type": "market", + # "side": "buy", + # "price": null, + # "size": 1, + # "value": "27.992", + # "dealValue": "27.992", + # "dealSize": 1, + # "stp": "", + # "stop": "", + # "stopPriceType": "", + # "stopTriggered": False, + # "stopPrice": null, + # "timeInForce": "GTC", + # "postOnly": False, + # "hidden": False, + # "iceberg": False, + # "leverage": "17", + # "forceHold": False, + # "closeOrder": False, + # "visibleSize": null, + # "clientOid": null, + # "remark": null, + # "tags": null, + # "isActive": False, + # "cancelExist": False, + # "createdAt": 1682996482000, + # "updatedAt": 1682996483000, + # "endAt": 1682996483000, + # "orderTime": 1682996482953900677, + # "settleCurrency": "USDT", + # "status": "done", + # "filledSize": 1, + # "filledValue": "27.992", + # "reduceOnly": False + # } + # } + # + market = self.market(symbol) if (symbol is not None) else None + responseData = self.safe_dict(response, 'data') + return self.parse_order(responseData, market) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # fetchOrder, fetchOrdersByStatus + # + # { + # "id": "64507d02921f1c0001ff6892", + # "symbol": "XBTUSDTM", + # "type": "market", + # "side": "buy", + # "price": null, + # "size": 1, + # "value": "27.992", + # "dealValue": "27.992", + # "dealSize": 1, + # "stp": "", + # "stop": "", + # "stopPriceType": "", + # "stopTriggered": False, + # "stopPrice": null, + # "timeInForce": "GTC", + # "postOnly": False, + # "hidden": False, + # "iceberg": False, + # "leverage": "17", + # "forceHold": False, + # "closeOrder": False, + # "visibleSize": null, + # "clientOid": null, + # "remark": null, + # "tags": null, + # "isActive": False, + # "cancelExist": False, + # "createdAt": 1682996482000, + # "updatedAt": 1682996483062, + # "endAt": 1682996483062, + # "orderTime": 1682996482953900677, + # "settleCurrency": "USDT", + # "status": "done", + # "filledValue": "27.992", + # "filledSize": 1, + # "reduceOnly": False + # } + # + # createOrder + # + # { + # "orderId": "619717484f1d010001510cde" + # } + # + # createOrders + # + # { + # "orderId": "80465574458560512", + # "clientOid": "5c52e11203aa677f33e491", + # "symbol": "ETHUSDTM", + # "code": "200000", + # "msg": "success" + # } + # + marketId = self.safe_string(order, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + orderId = self.safe_string_2(order, 'id', 'orderId') + type = self.safe_string(order, 'type') + timestamp = self.safe_integer(order, 'createdAt') + datetime = self.iso8601(timestamp) + price = self.safe_string(order, 'price') + # price is zero for market order + # omitZero is called in safeOrder2 + side = self.safe_string(order, 'side') + feeCurrencyId = self.safe_string(order, 'feeCurrency') + feeCurrency = self.safe_currency_code(feeCurrencyId) + feeCost = self.safe_number(order, 'fee') + amount = self.safe_string(order, 'size') + filled = self.safe_string(order, 'filledSize') + cost = self.safe_string(order, 'filledValue') + average = self.safe_string(order, 'avgDealPrice') + if (average is None) and Precise.string_gt(filled, '0'): + contractSize = self.safe_string(market, 'contractSize') + if market['linear']: + average = Precise.string_div(cost, Precise.string_mul(contractSize, filled)) + else: + average = Precise.string_div(Precise.string_mul(contractSize, filled), cost) + # precision reported by their api is 8 d.p. + # average = Precise.string_div(cost, Precise.string_mul(filled, market['contractSize'])) + # bool + isActive = self.safe_value(order, 'isActive') + cancelExist = self.safe_bool(order, 'cancelExist', False) + status = None + if isActive is not None: + status = 'open' if isActive else 'closed' + status = 'canceled' if cancelExist else status + fee = None + if feeCost is not None: + fee = { + 'currency': feeCurrency, + 'cost': feeCost, + } + clientOrderId = self.safe_string(order, 'clientOid') + timeInForce = self.safe_string(order, 'timeInForce') + postOnly = self.safe_value(order, 'postOnly') + reduceOnly = self.safe_value(order, 'reduceOnly') + lastUpdateTimestamp = self.safe_integer(order, 'updatedAt') + return self.safe_order({ + 'id': orderId, + 'clientOrderId': clientOrderId, + 'symbol': symbol, + 'type': type, + 'timeInForce': timeInForce, + 'postOnly': postOnly, + 'reduceOnly': reduceOnly, + 'side': side, + 'amount': amount, + 'price': price, + 'triggerPrice': self.safe_number(order, 'stopPrice'), + 'cost': cost, + 'filled': filled, + 'remaining': None, + 'timestamp': timestamp, + 'datetime': datetime, + 'fee': fee, + 'status': status, + 'info': order, + 'lastTradeTimestamp': None, + 'lastUpdateTimestamp': lastUpdateTimestamp, + 'average': average, + 'trades': None, + }, market) + + def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate + + https://www.kucoin.com/docs/rest/futures-trading/funding-fees/get-current-funding-rate + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = self.futuresPublicGetFundingRateSymbolCurrent(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "symbol": ".ETHUSDTMFPI8H", + # "granularity": 28800000, + # "timePoint": 1637380800000, + # "value": 0.0001, + # "predictedValue": 0.0001, + # }, + # } + # + data = self.safe_dict(response, 'data', {}) + # the website displayes the previous funding rate as "funding rate" + return self.parse_funding_rate(data, market) + + def fetch_funding_interval(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate interval + + https://www.kucoin.com/docs/rest/futures-trading/funding-fees/get-current-funding-rate + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + return self.fetch_funding_rate(symbol, params) + + def parse_funding_rate(self, data, market: Market = None) -> FundingRate: + # + # { + # "symbol": ".ETHUSDTMFPI8H", + # "granularity": 28800000, + # "timePoint": 1637380800000, + # "value": 0.0001, + # "predictedValue": 0.0001, + # } + # + fundingTimestamp = self.safe_integer(data, 'timePoint') + marketId = self.safe_string(data, 'symbol') + return { + 'info': data, + 'symbol': self.safe_symbol(marketId, market, None, 'contract'), + 'markPrice': None, + 'indexPrice': None, + 'interestRate': None, + 'estimatedSettlePrice': None, + 'timestamp': None, + 'datetime': None, + 'fundingRate': self.safe_number(data, 'value'), + 'fundingTimestamp': fundingTimestamp, + 'fundingDatetime': self.iso8601(fundingTimestamp), + 'nextFundingRate': self.safe_number(data, 'predictedValue'), + 'nextFundingTimestamp': None, + 'nextFundingDatetime': None, + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': self.parse_funding_interval(self.safe_string(data, 'granularity')), + } + + def parse_funding_interval(self, interval): + intervals: dict = { + '3600000': '1h', + '14400000': '4h', + '28800000': '8h', + '57600000': '16h', + '86400000': '24h', + } + return self.safe_string(intervals, interval, interval) + + def parse_balance(self, response) -> Balances: + result: dict = { + 'info': response, + 'timestamp': None, + 'datetime': None, + } + data = self.safe_value(response, 'data') + currencyId = self.safe_string(data, 'currency') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(data, 'availableBalance') + account['total'] = self.safe_string(data, 'accountEquity') + result[code] = account + return self.safe_balance(result) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://www.kucoin.com/docs/rest/funding/funding-overview/get-account-detail-futures + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param dict [params.code]: the unified currency code to fetch the balance for, if not provided, the default .options['fetchBalance']['code'] will be used + :returns dict: a `balance structure ` + """ + self.load_markets() + # only fetches one balance at a time + defaultCode = self.safe_string(self.options, 'code') + fetchBalanceOptions = self.safe_value(self.options, 'fetchBalance', {}) + defaultCode = self.safe_string(fetchBalanceOptions, 'code', defaultCode) + code = self.safe_string(params, 'code', defaultCode) + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + } + response = self.futuresPrivateGetAccountOverview(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "accountEquity": 0.00005, + # "unrealisedPNL": 0, + # "marginBalance": 0.00005, + # "positionMargin": 0, + # "orderMargin": 0, + # "frozenFunds": 0, + # "availableBalance": 0.00005, + # "currency": "XBT" + # } + # } + # + return self.parse_balance(response) + + def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://www.kucoin.com/docs/rest/funding/transfer/transfer-to-main-or-trade-account + https://www.kucoin.com/docs/rest/funding/transfer/transfer-to-futures-account + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account to transfer from + :param str toAccount: account to transfer to + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transfer structure ` + """ + self.load_markets() + currency = self.currency(code) + amountToPrecision = self.currency_to_precision(code, amount) + request: dict = { + 'currency': self.safe_string(currency, 'id'), + 'amount': amountToPrecision, + } + toAccountString = self.parse_transfer_type(toAccount) + response = None + if toAccountString == 'TRADE' or toAccountString == 'MAIN': + request['recAccountType'] = toAccountString + response = self.futuresPrivatePostTransferOut(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "applyId": "6738754373ceee00011ec3f8", + # "bizNo": "6738754373ceee00011ec3f7", + # "payAccountType": "CONTRACT", + # "payTag": "DEFAULT", + # "remark": "", + # "recAccountType": "MAIN", + # "recTag": "DEFAULT", + # "recRemark": "", + # "recSystem": "KUCOIN", + # "status": "PROCESSING", + # "currency": "USDT", + # "amount": "5", + # "fee": "0", + # "sn": 1519769124846692, + # "reason": "", + # "createdAt": 1731753283000, + # "updatedAt": 1731753283000 + # } + # } + # + elif toAccount == 'future' or toAccount == 'swap' or toAccount == 'contract': + request['payAccountType'] = self.parse_transfer_type(fromAccount) + response = self.futuresPrivatePostTransferIn(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "applyId": "5bffb63303aa675e8bbe18f9" # Transfer-out request ID + # } + # } + # + else: + raise BadRequest(self.id + ' transfer() only supports transfers between future/swap, spot and funding accounts') + data = self.safe_dict(response, 'data', {}) + return self.extend(self.parse_transfer(data, currency), { + 'amount': self.parse_number(amountToPrecision), + 'fromAccount': fromAccount, + 'toAccount': toAccount, + }) + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + # + # transfer to spot or funding account + # + # { + # "applyId": "5bffb63303aa675e8bbe18f9" # Transfer-out request ID + # } + # + # transfer to future account + # + # { + # "applyId": "6738754373ceee00011ec3f8", + # "bizNo": "6738754373ceee00011ec3f7", + # "payAccountType": "CONTRACT", + # "payTag": "DEFAULT", + # "remark": "", + # "recAccountType": "MAIN", + # "recTag": "DEFAULT", + # "recRemark": "", + # "recSystem": "KUCOIN", + # "status": "PROCESSING", + # "currency": "USDT", + # "amount": "5", + # "fee": "0", + # "sn": 1519769124846692, + # "reason": "", + # "createdAt": 1731753283000, + # "updatedAt": 1731753283000 + # } + # + timestamp = self.safe_integer(transfer, 'updatedAt') + return { + 'id': self.safe_string(transfer, 'applyId'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'currency': self.safe_currency_code(None, currency), + 'amount': self.safe_number(transfer, 'amount'), + 'fromAccount': None, + 'toAccount': None, + 'status': self.safe_string(transfer, 'status'), + 'info': transfer, + } + + def parse_transfer_status(self, status: Str) -> Str: + statuses: dict = { + 'PROCESSING': 'pending', + } + return self.safe_string(statuses, status, status) + + def parse_transfer_type(self, transferType: Str) -> Str: + transferTypes: dict = { + 'spot': 'TRADE', + 'funding': 'MAIN', + } + return self.safe_string_upper(transferTypes, transferType, transferType) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + + https://docs.kucoin.com/futures/#get-fills + + fetch all trades made by the user + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: End time in ms + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchMyTrades', 'paginate') + if paginate: + return self.fetch_paginated_call_dynamic('fetchMyTrades', symbol, since, limit, params) + request: dict = { + # orderId(str) [optional] Fills for a specific order(other parameters can be ignored if specified) + # symbol(str) [optional] Symbol of the contract + # side(str) [optional] buy or sell + # type(str) [optional] limit, market, limit_stop or market_stop + # startAt(long) [optional] Start time(millisecond) + # endAt(long) [optional] End time(millisecond) + } + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if since is not None: + request['startAt'] = since + if limit is not None: + request['pageSize'] = min(1000, limit) + request, params = self.handle_until_option('endAt', request, params) + response = self.futuresPrivateGetFills(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "currentPage": 1, + # "pageSize": 1, + # "totalNum": 251915, + # "totalPage": 251915, + # "items": [ + # { + # "symbol": "XBTUSDM", # Ticker symbol of the contract + # "tradeId": "5ce24c1f0c19fc3c58edc47c", # Trade ID + # "orderId": "5ce24c16b210233c36ee321d", # Order ID + # "side": "sell", # Transaction side + # "liquidity": "taker", # Liquidity- taker or maker + # "price": "8302", # Filled price + # "size": 10, # Filled amount + # "value": "0.001204529", # Order value + # "feeRate": "0.0005", # Floating fees + # "fixFee": "0.00000006", # Fixed fees + # "feeCurrency": "XBT", # Charging currency + # "stop": "", # A mark to the stop order type + # "fee": "0.0000012022", # Transaction fee + # "orderType": "limit", # Order type + # "tradeType": "trade", # Trade type(trade, liquidation, ADL or settlement) + # "createdAt": 1558334496000, # Time the order created + # "settleCurrency": "XBT", # settlement currency + # "tradeTime": 1558334496000000000 # trade time in nanosecond + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + trades = self.safe_list(data, 'items', []) + return self.parse_trades(trades, market, since, limit) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://www.kucoin.com/docs/rest/futures-trading/market-data/get-transaction-history + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = self.futuresPublicGetTradeHistory(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": [ + # { + # "sequence": 32114961, + # "side": "buy", + # "size": 39, + # "price": "4001.6500000000", + # "takerOrderId": "61c20742f172110001e0ebe4", + # "makerOrderId": "61c2073fcfc88100010fcb5d", + # "tradeId": "61c2074277a0c473e69029b8", + # "ts": 1640105794099993896 # filled time + # } + # ] + # } + # + trades = self.safe_list(response, 'data', []) + return self.parse_trades(trades, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(public) + # + # { + # "sequence": 32114961, + # "side": "buy", + # "size": 39, + # "price": "4001.6500000000", + # "takerOrderId": "61c20742f172110001e0ebe4", + # "makerOrderId": "61c2073fcfc88100010fcb5d", + # "tradeId": "61c2074277a0c473e69029b8", + # "ts": 1640105794099993896 # filled time + # } + # + # fetchMyTrades(private) v2 + # + # { + # "symbol":"BTC-USDT", + # "tradeId":"5c35c02709e4f67d5266954e", + # "orderId":"5c35c02703aa673ceec2a168", + # "counterOrderId":"5c1ab46003aa676e487fa8e3", + # "side":"buy", + # "liquidity":"taker", + # "forceTaker":true, + # "price":"0.083", + # "size":"0.8424304", + # "funds":"0.0699217232", + # "fee":"0", + # "feeRate":"0", + # "feeCurrency":"USDT", + # "stop":"", + # "type":"limit", + # "createdAt":1547026472000 + # } + # + # fetchMyTrades(private) v1 + # + # { + # "symbol":"DOGEUSDTM", + # "tradeId":"620ec41a96bab27b5f4ced56", + # "orderId":"620ec41a0d1d8a0001560bd0", + # "side":"sell", + # "liquidity":"taker", + # "forceTaker":true, + # "price":"0.13969", + # "size":1, + # "value":"13.969", + # "feeRate":"0.0006", + # "fixFee":"0", + # "feeCurrency":"USDT", + # "stop":"", + # "tradeTime":1645134874858018058, + # "fee":"0.0083814", + # "settleCurrency":"USDT", + # "orderType":"market", + # "tradeType":"trade", + # "createdAt":1645134874858 + # } + # + # watchTrades + # + # { + # "makerUserId": "62286a4d720edf0001e81961", + # "symbol": "ADAUSDTM", + # "sequence": 41320766, + # "side": "sell", + # "size": 2, + # "price": 0.35904, + # "takerOrderId": "636dd9da9857ba00010cfa44", + # "makerOrderId": "636dd9c8df149d0001e62bc8", + # "takerUserId": "6180be22b6ab210001fa3371", + # "tradeId": "636dd9da0000d400d477eca7", + # "ts": 1668143578987357700 + # } + # + marketId = self.safe_string(trade, 'symbol') + market = self.safe_market(marketId, market, '-') + id = self.safe_string_2(trade, 'tradeId', 'id') + orderId = self.safe_string(trade, 'orderId') + takerOrMaker = self.safe_string(trade, 'liquidity') + timestamp = self.safe_integer(trade, 'ts') + if timestamp is not None: + timestamp = self.parse_to_int(timestamp / 1000000) + else: + timestamp = self.safe_integer(trade, 'createdAt') + # if it's a historical v1 trade, the exchange returns timestamp in seconds + if ('dealValue' in trade) and (timestamp is not None): + timestamp = timestamp * 1000 + priceString = self.safe_string_2(trade, 'price', 'dealPrice') + amountString = self.safe_string_2(trade, 'size', 'amount') + side = self.safe_string(trade, 'side') + fee = None + feeCostString = self.safe_string(trade, 'fee') + if feeCostString is not None: + feeCurrencyId = self.safe_string(trade, 'feeCurrency') + feeCurrency = self.safe_currency_code(feeCurrencyId) + if feeCurrency is None: + feeCurrency = market['quote'] if (side == 'sell') else market['base'] + fee = { + 'cost': feeCostString, + 'currency': feeCurrency, + 'rate': self.safe_string(trade, 'feeRate'), + } + type = self.safe_string_2(trade, 'type', 'orderType') + if type == 'match': + type = None + costString = self.safe_string_2(trade, 'funds', 'value') + if costString is None: + contractSize = self.safe_string(market, 'contractSize') + contractCost = Precise.string_mul(priceString, amountString) + costString = Precise.string_mul(contractCost, contractSize) + return self.safe_trade({ + 'info': trade, + 'id': id, + 'order': orderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'type': type, + 'takerOrMaker': takerOrMaker, + 'side': side, + 'price': priceString, + 'amount': amountString, + 'cost': costString, + 'fee': fee, + }, market) + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + request: dict = {} + currency = None + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + if limit is not None: + request['pageSize'] = limit + if since is not None: + request['startAt'] = since + response = self.futuresPrivateGetDepositList(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "currentPage": 1, + # "pageSize": 5, + # "totalNum": 2, + # "totalPage": 1, + # "items": [ + # { + # "address": "0x5f047b29041bcfdbf0e4478cdfa753a336ba6989", + # "memo": "5c247c8a03aa677cea2a251d", + # "amount": 1, + # "fee": 0.0001, + # "currency": "KCS", + # "isInner": False, + # "walletTxId": "5bbb57386d99522d9f954c5a@test004", + # "status": "SUCCESS", + # "createdAt": 1544178843000, + # "updatedAt": 1544178891000 + # "remark":"foobar" + # }, + # ... + # ] + # } + # } + # + responseData = response['data']['items'] + return self.parse_transactions(responseData, currency, since, limit, {'type': 'deposit'}) + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + request: dict = {} + currency = None + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + if limit is not None: + request['pageSize'] = limit + if since is not None: + request['startAt'] = since + response = self.futuresPrivateGetWithdrawalList(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "currentPage": 1, + # "pageSize": 5, + # "totalNum": 2, + # "totalPage": 1, + # "items": [ + # { + # "id": "5c2dc64e03aa675aa263f1ac", + # "address": "0x5bedb060b8eb8d823e2414d82acce78d38be7fe9", + # "memo": "", + # "currency": "ETH", + # "amount": 1.0000000, + # "fee": 0.0100000, + # "walletTxId": "3e2414d82acce78d38be7fe9", + # "isInner": False, + # "status": "FAILURE", + # "createdAt": 1546503758000, + # "updatedAt": 1546504603000 + # }, + # ... + # ] + # } + # } + # + responseData = response['data']['items'] + return self.parse_transactions(responseData, currency, since, limit, {'type': 'withdrawal'}) + + def fetch_market_leverage_tiers(self, symbol: str, params={}) -> List[LeverageTier]: + """ + retrieve information on the maximum leverage, and maintenance margin for trades of varying trade sizes for a single market + + https://www.kucoin.com/docs/rest/futures-trading/risk-limit/get-futures-risk-limit-level + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `leverage tiers structure ` + """ + self.load_markets() + market = self.market(symbol) + if not market['contract']: + raise BadRequest(self.id + ' fetchMarketLeverageTiers() supports contract markets only') + request: dict = { + 'symbol': market['id'], + } + response = self.futuresPublicGetContractsRiskLimitSymbol(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": [ + # { + # "symbol": "ETHUSDTM", + # "level": 1, + # "maxRiskLimit": 300000, + # "minRiskLimit": 0, + # "maxLeverage": 100, + # "initialMargin": 0.0100000000, + # "maintainMargin": 0.0050000000 + # }, + # ... + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_market_leverage_tiers(data, market) + + def parse_market_leverage_tiers(self, info, market: Market = None) -> List[LeverageTier]: + """ + @ignore + :param dict info: Exchange market response for 1 market + :param dict market: CCXT market + """ + # + # { + # "symbol": "ETHUSDTM", + # "level": 1, + # "maxRiskLimit": 300000, + # "minRiskLimit": 0, + # "maxLeverage": 100, + # "initialMargin": 0.0100000000, + # "maintainMargin": 0.0050000000 + # } + # + tiers = [] + for i in range(0, len(info)): + tier = info[i] + marketId = self.safe_string(tier, 'symbol') + tiers.append({ + 'tier': self.safe_number(tier, 'level'), + 'symbol': self.safe_symbol(marketId, market, None, 'contract'), + 'currency': market['base'], + 'minNotional': self.safe_number(tier, 'minRiskLimit'), + 'maxNotional': self.safe_number(tier, 'maxRiskLimit'), + 'maintenanceMarginRate': self.safe_number(tier, 'maintainMargin'), + 'maxLeverage': self.safe_number(tier, 'maxLeverage'), + 'info': tier, + }) + return tiers + + def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + + https://www.kucoin.com/docs/rest/futures-trading/funding-fees/get-public-funding-history#request-url + + fetches historical funding rate prices + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: not used by kucuoinfutures + :param int [limit]: the maximum amount of `funding rate structures ` to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: end time in ms + :returns dict[]: a list of `funding rate structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchFundingRateHistory() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'from': 0, + 'to': self.milliseconds(), + } + until = self.safe_integer(params, 'until') + params = self.omit(params, ['until']) + if since is not None: + request['from'] = since + if until is None: + request['to'] = since + 1000 * 8 * 60 * 60 * 100 + if until is not None: + request['to'] = until + if since is None: + request['to'] = until - 1000 * 8 * 60 * 60 * 100 + response = self.futuresPublicGetContractFundingRates(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": [ + # { + # "symbol": "IDUSDTM", + # "fundingRate": 2.26E-4, + # "timepoint": 1702296000000 + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_funding_rate_histories(data, market, since, limit) + + def parse_funding_rate_history(self, info, market: Market = None): + timestamp = self.safe_integer(info, 'timepoint') + marketId = self.safe_string(info, 'symbol') + return { + 'info': info, + 'symbol': self.safe_symbol(marketId, market), + 'fundingRate': self.safe_number(info, 'fundingRate'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + + def close_position(self, symbol: str, side: OrderSide = None, params={}) -> Order: + """ + closes open positions for a market + + https://www.kucoin.com/docs/rest/futures-trading/orders/place-order + + :param str symbol: Unified CCXT market symbol + :param str side: not used by kucoinfutures closePositions + :param dict [params]: extra parameters specific to the okx api endpoint + :param str [params.clientOrderId]: client order id of the order + :returns dict[]: `A list of position structures ` + """ + self.load_markets() + market = self.market(symbol) + clientOrderId = self.safe_string(params, 'clientOrderId') + testOrder = self.safe_bool(params, 'test', False) + params = self.omit(params, ['test', 'clientOrderId']) + if clientOrderId is None: + clientOrderId = self.number_to_string(self.nonce()) + request: dict = { + 'symbol': market['id'], + 'closeOrder': True, + 'clientOid': clientOrderId, + 'type': 'market', + } + response = None + if testOrder: + response = self.futuresPrivatePostOrdersTest(self.extend(request, params)) + else: + response = self.futuresPrivatePostOrders(self.extend(request, params)) + return self.parse_order(response, market) + + def fetch_trading_fee(self, symbol: str, params={}) -> TradingFeeInterface: + """ + fetch the trading fees for a market + + https://www.kucoin.com/docs/rest/funding/trade-fee/trading-pair-actual-fee-futures + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `fee structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbols': market['id'], + } + response = self.privateGetTradeFees(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "symbol": "XBTUSDTM", + # "takerFeeRate": "0.0006", + # "makerFeeRate": "0.0002" + # } + # } + # + data = self.safe_list(response, 'data', []) + first = self.safe_dict(data, 0) + marketId = self.safe_string(first, 'symbol') + return { + 'info': response, + 'symbol': self.safe_symbol(marketId, market), + 'maker': self.safe_number(first, 'makerFeeRate'), + 'taker': self.safe_number(first, 'takerFeeRate'), + 'percentage': True, + 'tierBased': True, + } + + def fetch_margin_mode(self, symbol: str, params={}) -> MarginMode: + """ + fetches the margin mode of a trading pair + + https://www.kucoin.com/docs/rest/futures-trading/positions/get-margin-mode + + :param str symbol: unified symbol of the market to fetch the margin mode for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin mode structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = self.futuresPrivateGetPositionGetMarginMode(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "symbol": "XBTUSDTM", + # "marginMode": "ISOLATED" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_margin_mode(data, market) + + def parse_margin_mode(self, marginMode: dict, market=None) -> MarginMode: + marginType = self.safe_string(marginMode, 'marginMode') + marginType = 'isolated' if (marginType == 'ISOLATED') else 'cross' + return { + 'info': marginMode, + 'symbol': market['symbol'], + 'marginMode': marginType, + } + + def set_margin_mode(self, marginMode: str, symbol: Str = None, params={}): + """ + set margin mode to 'cross' or 'isolated' + + https://www.kucoin.com/docs/rest/futures-trading/positions/modify-margin-mode + + :param str marginMode: 'cross' or 'isolated' + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setMarginMode() requires a symbol argument') + self.check_required_argument('setMarginMode', marginMode, 'marginMode', ['cross', 'isolated']) + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'marginMode': marginMode.upper(), + } + response = self.futuresPrivatePostPositionChangeMarginMode(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "symbol": "XBTUSDTM", + # "marginMode": "ISOLATED" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_margin_mode(data, market) + + def set_position_mode(self, hedged: bool, symbol: Str = None, params={}): + """ + set hedged to True or False for a market + + https://www.kucoin.com/docs-new/3475097e0 + + :param bool hedged: set to True to use two way position + :param str [symbol]: not used by bybit setPositionMode() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a response from the exchange + """ + self.load_markets() + posMode = '1' if hedged else '0' + request: dict = { + 'positionMode': posMode, + } + response = self.futuresPrivatePostPositionSwitchPositionMode(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "positionMode": 1 + # } + # } + # + return response + + def fetch_leverage(self, symbol: str, params={}) -> Leverage: + """ + fetch the set leverage for a market + + https://www.kucoin.com/docs/rest/futures-trading/positions/get-cross-margin-leverage + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `leverage structure ` + """ + marginMode = None + marginMode, params = self.handle_margin_mode_and_params(symbol, params) + if marginMode != 'cross': + raise NotSupported(self.id + ' fetchLeverage() currently supports only params["marginMode"] = "cross"') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = self.futuresPrivateGetGetCrossUserLeverage(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": { + # "symbol": "XBTUSDTM", + # "leverage": "3" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + parsed = self.parse_leverage(data, market) + return self.extend(parsed, { + 'marginMode': marginMode, + }) + + def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://www.kucoin.com/docs/rest/futures-trading/positions/modify-cross-margin-leverage + + :param float leverage: the rate of leverage + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + marginMode = None + marginMode, params = self.handle_margin_mode_and_params(symbol, params) + if marginMode != 'cross': + raise NotSupported(self.id + ' setLeverage() currently supports only params["marginMode"] = "cross"') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'leverage': str(leverage), + } + response = self.futuresPrivatePostChangeCrossUserLeverage(self.extend(request, params)) + # + # { + # "code": "200000", + # "data": True + # } + # + return self.parse_leverage(response, market) + + def parse_leverage(self, leverage: dict, market: Market = None) -> Leverage: + marketId = self.safe_string(leverage, 'symbol') + market = self.safe_market(marketId, market) + leverageNum = self.safe_integer(leverage, 'leverage') + return { + 'info': leverage, + 'symbol': market['symbol'], + 'marginMode': None, + 'longLeverage': leverageNum, + 'shortLeverage': leverageNum, + } diff --git a/ccxt/latoken.py b/ccxt/latoken.py new file mode 100644 index 0000000..b0267e9 --- /dev/null +++ b/ccxt/latoken.py @@ -0,0 +1,1770 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.latoken import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currencies, Currency, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, TradingFeeInterface, Transaction, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import AccountSuspended +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import NotSupported +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.errors import InvalidNonce +from ccxt.base.decimal_to_precision import TICK_SIZE + + +class latoken(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(latoken, self).describe(), { + 'id': 'latoken', + 'name': 'Latoken', + 'countries': ['KY'], # Cayman Islands + 'version': 'v2', + 'rateLimit': 1000, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createOrder': True, + 'createPostOnlyOrder': False, + 'createStopLimitOrder': True, + 'createStopMarketOrder': False, + 'createStopOrder': True, + 'fetchAllGreeks': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': False, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDepositsWithdrawals': True, + 'fetchDepositWithdrawFees': False, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrice': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': True, + 'fetchTradingFees': False, + 'fetchTransactions': 'emulated', + 'fetchTransfer': False, + 'fetchTransfers': True, + 'fetchUnderlyingAssets': False, + 'fetchVolatilityHistory': False, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'transfer': True, + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/61511972-24c39f00-aa01-11e9-9f7c-471f1d6e5214.jpg', + 'api': { + 'rest': 'https://api.latoken.com', + }, + 'www': 'https://latoken.com', + 'doc': [ + 'https://api.latoken.com', + ], + 'fees': 'https://latoken.com/fees', + 'referral': 'https://latoken.com/invite?r=mvgp2djk', + }, + 'api': { + 'public': { + 'get': { + 'book/{currency}/{quote}': 1, + 'chart/week': 1, + 'chart/week/{currency}/{quote}': 1, + 'currency': 1, + 'currency/available': 1, + 'currency/quotes': 1, + 'currency/{currency}': 1, + 'pair': 1, + 'pair/available': 1, + 'ticker': 1, + 'ticker/{base}/{quote}': 1, + 'time': 1, + 'trade/history/{currency}/{quote}': 1, + 'trade/fee/{currency}/{quote}': 1, + 'trade/feeLevels': 1, + 'transaction/bindings': 1, + }, + }, + 'private': { + 'get': { + 'auth/account': 1, + 'auth/account/currency/{currency}/{type}': 1, + 'auth/order': 1, + 'auth/order/getOrder/{id}': 1, + 'auth/order/pair/{currency}/{quote}': 1, + 'auth/order/pair/{currency}/{quote}/active': 1, + 'auth/stopOrder': 1, + 'auth/stopOrder/getOrder/{id}': 1, + 'auth/stopOrder/pair/{currency}/{quote}': 1, + 'auth/stopOrder/pair/{currency}/{quote}/active': 1, + 'auth/trade': 1, + 'auth/trade/pair/{currency}/{quote}': 1, + 'auth/trade/fee/{currency}/{quote}': 1, + 'auth/transaction': 1, + 'auth/transaction/bindings': 1, + 'auth/transaction/bindings/{currency}': 1, + 'auth/transaction/{id}': 1, + 'auth/transfer': 1, + }, + 'post': { + 'auth/order/cancel': 1, + 'auth/order/cancelAll': 1, + 'auth/order/cancelAll/{currency}/{quote}': 1, + 'auth/order/place': 1, + 'auth/spot/deposit': 1, + 'auth/spot/withdraw': 1, + 'auth/stopOrder/cancel': 1, + 'auth/stopOrder/cancelAll': 1, + 'auth/stopOrder/cancelAll/{currency}/{quote}': 1, + 'auth/stopOrder/place': 1, + 'auth/transaction/depositAddress': 1, + 'auth/transaction/withdraw': 1, + 'auth/transaction/withdraw/cancel': 1, + 'auth/transaction/withdraw/confirm': 1, + 'auth/transaction/withdraw/resendCode': 1, + 'auth/transfer/email': 1, + 'auth/transfer/id': 1, + 'auth/transfer/phone': 1, + }, + }, + }, + 'precisionMode': TICK_SIZE, + 'fees': { + 'trading': { + 'feeSide': 'get', + 'tierBased': False, + 'percentage': True, + 'maker': self.parse_number('0.0049'), + 'taker': self.parse_number('0.0049'), + }, + }, + 'commonCurrencies': { + 'BUX': 'Buxcoin', + 'CBT': 'Community Business Token', + 'CTC': 'CyberTronchain', + 'DMD': 'Diamond Coin', + 'FREN': 'Frenchie', + 'GDX': 'GoldenX', + 'GEC': 'Geco One', + 'GEM': 'NFTmall', + 'GMT': 'GMT Token', + 'IMC': 'IMCoin', + 'MT': 'Monarch', + 'TPAY': 'Tetra Pay', + 'TRADE': 'Smart Trade Coin', + 'TSL': 'Treasure SL', + 'UNO': 'Unobtanium', + 'WAR': 'Warrior Token', + }, + 'exceptions': { + 'exact': { + 'INTERNAL_ERROR': ExchangeError, # internal server error. You can contact our support to solve self problem. {"message":"Internal Server Error","error":"INTERNAL_ERROR","status":"FAILURE"} + 'SERVICE_UNAVAILABLE': ExchangeNotAvailable, # requested information currently not available. You can contact our support to solve self problem or retry later. + 'NOT_AUTHORIZED': AuthenticationError, # user's query not authorized. Check if you are logged in. + 'FORBIDDEN': PermissionDenied, # you don't have enough access rights. + 'BAD_REQUEST': BadRequest, # some bad request, for example bad fields values or something else. Read response message for more information. + 'NOT_FOUND': ExchangeError, # entity not found. Read message for more information. + 'ACCESS_DENIED': PermissionDenied, # access is denied. Probably you don't have enough access rights, you contact our support. + 'REQUEST_REJECTED': ExchangeError, # user's request rejected for some reasons. Check error message. + 'HTTP_MEDIA_TYPE_NOT_SUPPORTED': BadRequest, # http media type not supported. + 'MEDIA_TYPE_NOT_ACCEPTABLE': BadRequest, # media type not acceptable + 'METHOD_ARGUMENT_NOT_VALID': BadRequest, # one of method argument is invalid. Check argument types and error message for more information. + 'VALIDATION_ERROR': BadRequest, # check errors field to get reasons. + 'ACCOUNT_EXPIRED': AccountSuspended, # restore your account or create a new one. + 'BAD_CREDENTIALS': AuthenticationError, # invalid username or password. + 'COOKIE_THEFT': AuthenticationError, # cookie has been stolen. Let's try reset your cookies. + 'CREDENTIALS_EXPIRED': AccountSuspended, # credentials expired. + 'INSUFFICIENT_AUTHENTICATION': AuthenticationError, # for example, 2FA required. + 'UNKNOWN_LOCATION': AuthenticationError, # user logged from unusual location, email confirmation required. + 'TOO_MANY_REQUESTS': RateLimitExceeded, # too many requests at the time. A response header X-Rate-Limit-Remaining indicates the number of allowed request per a period. + 'INSUFFICIENT_FUNDS': InsufficientFunds, # {"message":"not enough balance on the spot account for currency(USDT), need(20.000)","error":"INSUFFICIENT_FUNDS","status":"FAILURE"} + 'ORDER_VALIDATION': InvalidOrder, # {"message":"Quantity(0) is not positive","error":"ORDER_VALIDATION","status":"FAILURE"} + 'BAD_TICKS': InvalidOrder, # {"status":"FAILURE","message":"Quantity(1.4) does not match quantity tick(10)","error":"BAD_TICKS","errors":null,"result":false} + }, + 'broad': { + 'invalid API key, signature or digest': AuthenticationError, # {"result":false,"message":"invalid API key, signature or digest","error":"BAD_REQUEST","status":"FAILURE"} + 'The API key was revoked': AuthenticationError, # {"result":false,"message":"The API key was revoked","error":"BAD_REQUEST","status":"FAILURE"} + 'request expired or bad': InvalidNonce, # {"result":false,"message":"request expired or bad / format","error":"BAD_REQUEST","status":"FAILURE"} + 'For input string': BadRequest, # {"result":false,"message":"Internal error","error":"For input string: \"NaN\"","status":"FAILURE"} + 'Unable to resolve currency by tag': BadSymbol, # {"message":"Unable to resolve currency by tag(None)","error":"NOT_FOUND","status":"FAILURE"} + "Can't find currency with tag": BadSymbol, # {"status":"FAILURE","message":"Can't find currency with tag = None","error":"NOT_FOUND","errors":null,"result":false} + 'Unable to place order because pair is in inactive state': BadSymbol, # {"message":"Unable to place order because pair is in inactive state(PAIR_STATUS_INACTIVE)","error":"ORDER_VALIDATION","status":"FAILURE"} + 'API keys are not available for': AccountSuspended, # {"result":false,"message":"API keys are not available for FROZEN user","error":"BAD_REQUEST","status":"FAILURE"} + }, + }, + 'options': { + 'defaultType': 'spot', + 'types': { + 'wallet': 'ACCOUNT_TYPE_WALLET', + 'funding': 'ACCOUNT_TYPE_WALLET', + 'spot': 'ACCOUNT_TYPE_SPOT', + }, + 'accounts': { + 'ACCOUNT_TYPE_WALLET': 'wallet', + 'ACCOUNT_TYPE_SPOT': 'spot', + }, + 'fetchTradingFee': { + 'method': 'fetchPrivateTradingFee', # or 'fetchPublicTradingFee' + }, + 'timeDifference': 0, # the difference between system clock and exchange clock + 'adjustForTimeDifference': True, # controls the adjustment logic upon instantiation + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, # todo: for non-trigger orders + 'FOK': True, + 'PO': False, + 'GTD': False, + }, + 'hedged': False, + 'selfTradePrevention': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 100000, # todo + 'untilDays': None, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': True, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOrders': None, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 100000, # todo + 'daysBackCanceled': 1, + 'untilDays': None, + 'trigger': True, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': None, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + }) + + def nonce(self): + return self.milliseconds() - self.options['timeDifference'] + + def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://api.latoken.com/doc/v2/#tag/Time/operation/currentTime + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = self.publicGetTime(params) + # + # { + # "serverTime": 1570615577321 + # } + # + return self.safe_integer(response, 'serverTime') + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for latoken + + https://api.latoken.com/doc/v2/#tag/Pair/operation/getActivePairs + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = self.publicGetPair(params) + # + # [ + # { + # "id":"dba4289b-6b46-4d94-bf55-49eec9a163ad", + # "status":"PAIR_STATUS_ACTIVE", # CURRENCY_STATUS_INACTIVE + # "baseCurrency":"fb9b53d6-bbf6-472f-b6ba-73cc0d606c9b", + # "quoteCurrency":"620f2019-33c0-423b-8a9d-cde4d7f8ef7f", + # "priceTick":"0.000000100000000000", + # "priceDecimals":7, + # "quantityTick":"0.010000000", + # "quantityDecimals":2, + # "costDisplayDecimals":7, + # "created":1572957210501, + # "minOrderQuantity":"0", + # "maxOrderCostUsd":"999999999999999999", + # "minOrderCostUsd":"0", + # "externalSymbol":"" + # } + # ] + # + if self.safe_bool(self.options, 'adjustForTimeDifference', False): + self.load_time_difference() + currencies = self.safe_dict(self.options, 'cachedCurrencies', {}) + currenciesById = self.index_by(currencies, 'id') + result = [] + for i in range(0, len(response)): + market = response[i] + id = self.safe_string(market, 'id') + # the exchange shows them inverted + baseId = self.safe_string(market, 'baseCurrency') + quoteId = self.safe_string(market, 'quoteCurrency') + baseCurrency = self.safe_dict(currenciesById, baseId) + quoteCurrency = self.safe_dict(currenciesById, quoteId) + baseCurrencyInfo = self.safe_dict(baseCurrency, 'info') + quoteCurrencyInfo = self.safe_dict(quoteCurrency, 'info') + if baseCurrencyInfo is not None and quoteCurrencyInfo is not None: + base = self.safe_currency_code(self.safe_string(baseCurrencyInfo, 'tag')) + quote = self.safe_currency_code(self.safe_string(quoteCurrencyInfo, 'tag')) + lowercaseQuote = quote.lower() + capitalizedQuote = self.capitalize(lowercaseQuote) + status = self.safe_string(market, 'status') + result.append({ + 'id': id, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': (status == 'PAIR_STATUS_ACTIVE'), # assuming True + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.safe_number(market, 'quantityTick'), + 'price': self.safe_number(market, 'priceTick'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(market, 'minOrderQuantity'), + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': self.safe_number(market, 'minOrderCost' + capitalizedQuote), + 'max': self.safe_number(market, 'maxOrderCost' + capitalizedQuote), + }, + }, + 'created': self.safe_integer(market, 'created'), + 'info': market, + }) + return result + + def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = self.publicGetCurrency(params) + # + # [ + # { + # "id":"1a075819-9e0b-48fc-8784-4dab1d186d6d", + # "status":"CURRENCY_STATUS_ACTIVE", + # "type":"CURRENCY_TYPE_ALTERNATIVE", # CURRENCY_TYPE_CRYPTO, CURRENCY_TYPE_IEO + # "name":"MyCryptoBank", + # "tag":"MCB", + # "description":"", + # "logo":"", + # "decimals":18, + # "created":1572912000000, + # "tier":1, + # "assetClass":"ASSET_CLASS_UNKNOWN", + # "minTransferAmount":0 + # }, + # { + # "id":"db02758e-2507-46a5-a805-7bc60355b3eb", + # "status":"CURRENCY_STATUS_ACTIVE", + # "type":"CURRENCY_TYPE_FUTURES_CONTRACT", + # "name":"BTC USDT Futures Contract", + # "tag":"BTCUSDT", + # "description":"", + # "logo":"", + # "decimals":8, + # "created":1589459984395, + # "tier":1, + # "assetClass":"ASSET_CLASS_UNKNOWN", + # "minTransferAmount":0 + # }, + # ] + # + result: dict = {} + for i in range(0, len(response)): + currency = response[i] + id = self.safe_string(currency, 'id') + tag = self.safe_string(currency, 'tag') + code = self.safe_currency_code(tag) + currencyType = self.safe_string(currency, 'type') + isCrypto = (currencyType == 'CURRENCY_TYPE_CRYPTO' or currencyType == 'CURRENCY_TYPE_IEO') + result[code] = self.safe_currency_structure({ + 'id': id, + 'code': code, + 'info': currency, + 'name': self.safe_string(currency, 'name'), + 'type': 'crypto' if isCrypto else 'other', + 'active': self.safe_string(currency, 'status') == 'CURRENCY_STATUS_ACTIVE', + 'deposit': None, + 'withdraw': None, + 'fee': self.safe_number(currency, 'fee'), + 'precision': self.parse_number(self.parse_precision(self.safe_string(currency, 'decimals'))), + 'limits': { + 'amount': { + 'min': self.safe_number(currency, 'minTransferAmount'), + 'max': None, + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + }, + 'networks': {}, + }) + return result + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://api.latoken.com/doc/v2/#tag/Account/operation/getBalancesByUser + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + response = self.privateGetAuthAccount(params) + # + # [ + # { + # "id": "e5852e02-8711-431c-9749-a6f5503c6dbe", + # "status": "ACCOUNT_STATUS_ACTIVE", + # "type": "ACCOUNT_TYPE_WALLET", + # "timestamp": "1635920106506", + # "currency": "0c3a106d-bde3-4c13-a26e-3fd2394529e5", + # "available": "100.000000", + # "blocked": "0.000000" + # }, + # { + # "id": "369df204-acbc-467e-a25e-b16e3cc09cf6", + # "status": "ACCOUNT_STATUS_ACTIVE", + # "type": "ACCOUNT_TYPE_SPOT", + # "timestamp": "1635920106504", + # "currency": "0c3a106d-bde3-4c13-a26e-3fd2394529e5", + # "available": "100.000000", + # "blocked": "0.000000" + # } + # ] + # + result: dict = { + 'info': response, + 'timestamp': None, + 'datetime': None, + } + maxTimestamp = None + defaultType = self.safe_string_2(self.options, 'fetchBalance', 'defaultType', 'spot') + type = self.safe_string(params, 'type', defaultType) + types = self.safe_value(self.options, 'types', {}) + accountType = self.safe_string(types, type, type) + balancesByType = self.group_by(response, 'type') + balances = self.safe_value(balancesByType, accountType, []) + for i in range(0, len(balances)): + balance = balances[i] + currencyId = self.safe_string(balance, 'currency') + timestamp = self.safe_integer(balance, 'timestamp') + if timestamp is not None: + if maxTimestamp is None: + maxTimestamp = timestamp + else: + maxTimestamp = max(maxTimestamp, timestamp) + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(balance, 'available') + account['used'] = self.safe_string(balance, 'blocked') + result[code] = account + result['timestamp'] = maxTimestamp + result['datetime'] = self.iso8601(maxTimestamp) + return self.safe_balance(result) + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://api.latoken.com/doc/v2/#tag/Order-Book/operation/getOrderBook + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'currency': market['baseId'], + 'quote': market['quoteId'], + } + if limit is not None: + request['limit'] = limit # max 1000 + response = self.publicGetBookCurrencyQuote(self.extend(request, params)) + # + # { + # "ask":[ + # {"price":"4428.76","quantity":"0.08136","cost":"360.3239136","accumulated":"360.3239136"}, + # {"price":"4429.77","quantity":"1.11786","cost":"4951.8626922","accumulated":"5312.1866058"}, + # {"price":"4430.94","quantity":"1.78418","cost":"7905.5945292","accumulated":"13217.781135"}, + # ], + # "bid":[ + # {"price":"4428.43","quantity":"0.13675","cost":"605.5878025","accumulated":"605.5878025"}, + # {"price":"4428.19","quantity":"0.03619","cost":"160.2561961","accumulated":"765.8439986"}, + # {"price":"4428.15","quantity":"0.02926","cost":"129.567669","accumulated":"895.4116676"}, + # ], + # "totalAsk":"53.14814", + # "totalBid":"112216.9029791" + # } + # + return self.parse_order_book(response, symbol, None, 'bid', 'ask', 'price', 'quantity') + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "symbol": "92151d82-df98-4d88-9a4d-284fa9eca49f/0c3a106d-bde3-4c13-a26e-3fd2394529e5", + # "baseCurrency": "92151d82-df98-4d88-9a4d-284fa9eca49f", + # "quoteCurrency": "0c3a106d-bde3-4c13-a26e-3fd2394529e5", + # "volume24h": "165723597.189022176000000000", + # "volume7d": "934505768.625109571000000000", + # "change24h": "0.0200", + # "change7d": "-6.4200", + # "amount24h": "6438.457663100000000000", + # "amount7d": "35657.785013800000000000", + # "lastPrice": "25779.16", + # "lastQuantity": "0.248403300000000000", + # "bestBid": "25778.74", + # "bestBidQuantity": "0.6520232", + # "bestAsk": "25779.17", + # "bestAskQuantity": "0.4956043", + # "updateTimestamp": "1693965231406" + # } + # + marketId = self.safe_string(ticker, 'symbol') + last = self.safe_string(ticker, 'lastPrice') + timestamp = self.safe_integer_omit_zero(ticker, 'updateTimestamp') # sometimes latoken provided '0' ts from /ticker endpoint + return self.safe_ticker({ + 'symbol': self.safe_symbol(marketId, market), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'low': None, + 'high': None, + 'bid': self.safe_string(ticker, 'bestBid'), + 'bidVolume': self.safe_string(ticker, 'bestBidQuantity'), + 'ask': self.safe_string(ticker, 'bestAsk'), + 'askVolume': self.safe_string(ticker, 'bestAskQuantity'), + 'vwap': None, + 'open': None, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': self.safe_string(ticker, 'change24h'), + 'average': None, + 'baseVolume': self.safe_string(ticker, 'amount24h'), + 'quoteVolume': self.safe_string(ticker, 'volume24h'), + 'info': ticker, + }, market) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://api.latoken.com/doc/v2/#tag/Ticker/operation/getTicker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'base': market['baseId'], + 'quote': market['quoteId'], + } + response = self.publicGetTickerBaseQuote(self.extend(request, params)) + # + # { + # "symbol": "92151d82-df98-4d88-9a4d-284fa9eca49f/0c3a106d-bde3-4c13-a26e-3fd2394529e5", + # "baseCurrency": "92151d82-df98-4d88-9a4d-284fa9eca49f", + # "quoteCurrency": "0c3a106d-bde3-4c13-a26e-3fd2394529e5", + # "volume24h": "165723597.189022176000000000", + # "volume7d": "934505768.625109571000000000", + # "change24h": "0.0200", + # "change7d": "-6.4200", + # "amount24h": "6438.457663100000000000", + # "amount7d": "35657.785013800000000000", + # "lastPrice": "25779.16", + # "lastQuantity": "0.248403300000000000", + # "bestBid": "25778.74", + # "bestBidQuantity": "0.6520232", + # "bestAsk": "25779.17", + # "bestAskQuantity": "0.4956043", + # "updateTimestamp": "1693965231406" + # } + # + return self.parse_ticker(response, market) + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://api.latoken.com/doc/v2/#tag/Ticker/operation/getAllTickers + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + response = self.publicGetTicker(params) + # + # [ + # { + # "symbol": "92151d82-df98-4d88-9a4d-284fa9eca49f/0c3a106d-bde3-4c13-a26e-3fd2394529e5", + # "baseCurrency": "92151d82-df98-4d88-9a4d-284fa9eca49f", + # "quoteCurrency": "0c3a106d-bde3-4c13-a26e-3fd2394529e5", + # "volume24h": "165723597.189022176000000000", + # "volume7d": "934505768.625109571000000000", + # "change24h": "0.0200", + # "change7d": "-6.4200", + # "amount24h": "6438.457663100000000000", + # "amount7d": "35657.785013800000000000", + # "lastPrice": "25779.16", + # "lastQuantity": "0.248403300000000000", + # "bestBid": "25778.74", + # "bestBidQuantity": "0.6520232", + # "bestAsk": "25779.17", + # "bestAskQuantity": "0.4956043", + # "updateTimestamp": "1693965231406" + # } + # ] + # + return self.parse_tickers(response, symbols) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(public) + # + # { + # "id":"c152f814-8eeb-44f0-8f3f-e5c568f2ffcf", + # "isMakerBuyer":false, + # "baseCurrency":"620f2019-33c0-423b-8a9d-cde4d7f8ef7f", + # "quoteCurrency":"0c3a106d-bde3-4c13-a26e-3fd2394529e5", + # "price":"4435.56", + # "quantity":"0.32534", + # "cost":"1443.0650904", + # "timestamp":1635854642725, + # "makerBuyer":false + # } + # + # fetchMyTrades(private) + # + # { + # "id":"02e02533-b4bf-4ba9-9271-24e2108dfbf7", + # "isMakerBuyer":false, + # "direction":"TRADE_DIRECTION_BUY", + # "baseCurrency":"620f2019-33c0-423b-8a9d-cde4d7f8ef7f", + # "quoteCurrency":"0c3a106d-bde3-4c13-a26e-3fd2394529e5", + # "price":"4564.32", + # "quantity":"0.01000", + # "cost":"45.6432", + # "fee":"0.223651680000000000", + # "order":"c9cac6a0-484c-4892-88e7-ad51b39f2ce1", + # "timestamp":1635921580399, + # "makerBuyer":false + # } + # + type = None + timestamp = self.safe_integer(trade, 'timestamp') + priceString = self.safe_string(trade, 'price') + amountString = self.safe_string(trade, 'quantity') + costString = self.safe_string(trade, 'cost') + makerBuyer = self.safe_value(trade, 'makerBuyer') + side = self.safe_string(trade, 'direction') + if side is None: + side = 'sell' if makerBuyer else 'buy' + else: + if side == 'TRADE_DIRECTION_BUY': + side = 'buy' + elif side == 'TRADE_DIRECTION_SELL': + side = 'sell' + isBuy = (side == 'buy') + takerOrMaker = 'maker' if (makerBuyer and isBuy) else 'taker' + baseId = self.safe_string(trade, 'baseCurrency') + quoteId = self.safe_string(trade, 'quoteCurrency') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + symbol = base + '/' + quote + if symbol in self.markets: + market = self.market(symbol) + id = self.safe_string(trade, 'id') + orderId = self.safe_string(trade, 'order') + feeCost = self.safe_string(trade, 'fee') + fee = None + if feeCost is not None: + fee = { + 'cost': feeCost, + 'currency': quote, + } + return self.safe_trade({ + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'id': id, + 'order': orderId, + 'type': type, + 'takerOrMaker': takerOrMaker, + 'side': side, + 'price': priceString, + 'amount': amountString, + 'cost': costString, + 'fee': fee, + }, market) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://api.latoken.com/doc/v2/#tag/Trade/operation/getTradesByPair + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'currency': market['baseId'], + 'quote': market['quoteId'], + # 'from': str(since), # milliseconds + # 'limit': limit, # default 100, limit 100 + } + if limit is not None: + request['limit'] = min(limit, 100) # default 100, limit 100 + response = self.publicGetTradeHistoryCurrencyQuote(self.extend(request, params)) + # + # [ + # {"id":"c152f814-8eeb-44f0-8f3f-e5c568f2ffcf","isMakerBuyer":false,"baseCurrency":"620f2019-33c0-423b-8a9d-cde4d7f8ef7f","quoteCurrency":"0c3a106d-bde3-4c13-a26e-3fd2394529e5","price":"4435.56","quantity":"0.32534","cost":"1443.0650904","timestamp":1635854642725,"makerBuyer":false}, + # {"id":"cfecbefb-3d11-43d7-b9d4-fa16211aad8a","isMakerBuyer":false,"baseCurrency":"620f2019-33c0-423b-8a9d-cde4d7f8ef7f","quoteCurrency":"0c3a106d-bde3-4c13-a26e-3fd2394529e5","price":"4435.13","quantity":"0.26540","cost":"1177.083502","timestamp":1635854641114,"makerBuyer":false}, + # {"id":"f43d3ec8-db94-49f3-b534-91dbc2779296","isMakerBuyer":true,"baseCurrency":"620f2019-33c0-423b-8a9d-cde4d7f8ef7f","quoteCurrency":"0c3a106d-bde3-4c13-a26e-3fd2394529e5","price":"4435.00","quantity":"0.41738","cost":"1851.0803","timestamp":1635854640323,"makerBuyer":true}, + # ] + # + return self.parse_trades(response, market, since, limit) + + def fetch_trading_fee(self, symbol: str, params={}) -> TradingFeeInterface: + """ + fetch the trading fees for a market + + https://api.latoken.com/doc/v2/#tag/Trade/operation/getFeeByPair + https://api.latoken.com/doc/v2/#tag/Trade/operation/getAuthFeeByPair + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `fee structure ` + """ + options = self.safe_value(self.options, 'fetchTradingFee', {}) + defaultMethod = self.safe_string(options, 'method', 'fetchPrivateTradingFee') + method = self.safe_string(params, 'method', defaultMethod) + params = self.omit(params, 'method') + if method == 'fetchPrivateTradingFee': + return self.fetch_private_trading_fee(symbol, params) + elif method == 'fetchPublicTradingFee': + return self.fetch_public_trading_fee(symbol, params) + else: + raise NotSupported(self.id + ' not support self method') + + def fetch_public_trading_fee(self, symbol: str, params={}): + self.load_markets() + market = self.market(symbol) + request: dict = { + 'currency': market['baseId'], + 'quote': market['quoteId'], + } + response = self.publicGetTradeFeeCurrencyQuote(self.extend(request, params)) + # + # { + # "makerFee": "0.004900000000000000", + # "takerFee": "0.004900000000000000", + # "type": "FEE_SCHEME_TYPE_PERCENT_QUOTE", + # "take": "FEE_SCHEME_TAKE_PROPORTION" + # } + # + return { + 'info': response, + 'symbol': market['symbol'], + 'maker': self.safe_number(response, 'makerFee'), + 'taker': self.safe_number(response, 'takerFee'), + 'percentage': None, + 'tierBased': None, + } + + def fetch_private_trading_fee(self, symbol: str, params={}): + self.load_markets() + market = self.market(symbol) + request: dict = { + 'currency': market['baseId'], + 'quote': market['quoteId'], + } + response = self.privateGetAuthTradeFeeCurrencyQuote(self.extend(request, params)) + # + # { + # "makerFee": "0.004900000000000000", + # "takerFee": "0.004900000000000000", + # "type": "FEE_SCHEME_TYPE_PERCENT_QUOTE", + # "take": "FEE_SCHEME_TAKE_PROPORTION" + # } + # + return { + 'info': response, + 'symbol': market['symbol'], + 'maker': self.safe_number(response, 'makerFee'), + 'taker': self.safe_number(response, 'takerFee'), + 'percentage': None, + 'tierBased': None, + } + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://api.latoken.com/doc/v2/#tag/Trade/operation/getTradesByTrader + https://api.latoken.com/doc/v2/#tag/Trade/operation/getTradesByAssetAndTrader + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + request: dict = { + # 'currency': market['baseId'], + # 'quote': market['quoteId'], + # 'from': self.milliseconds(), + # 'limit': limit, # default '100' + } + market = None + if limit is not None: + request['limit'] = limit # default 100 + response = None + if symbol is not None: + market = self.market(symbol) + request['currency'] = market['baseId'] + request['quote'] = market['quoteId'] + response = self.privateGetAuthTradePairCurrencyQuote(self.extend(request, params)) + else: + response = self.privateGetAuthTrade(self.extend(request, params)) + # + # [ + # { + # "id":"02e02533-b4bf-4ba9-9271-24e2108dfbf7", + # "isMakerBuyer":false, + # "direction":"TRADE_DIRECTION_BUY", + # "baseCurrency":"620f2019-33c0-423b-8a9d-cde4d7f8ef7f", + # "quoteCurrency":"0c3a106d-bde3-4c13-a26e-3fd2394529e5", + # "price":"4564.32", + # "quantity":"0.01000", + # "cost":"45.6432", + # "fee":"0.223651680000000000", + # "order":"c9cac6a0-484c-4892-88e7-ad51b39f2ce1", + # "timestamp":1635921580399, + # "makerBuyer":false + # } + # ] + # + return self.parse_trades(response, market, since, limit) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'ORDER_STATUS_PLACED': 'open', + 'ORDER_STATUS_CLOSED': 'closed', + 'ORDER_STATUS_CANCELLED': 'canceled', + } + return self.safe_string(statuses, status, status) + + def parse_order_type(self, status): + statuses: dict = { + 'ORDER_TYPE_MARKET': 'market', + 'ORDER_TYPE_LIMIT': 'limit', + } + return self.safe_string(statuses, status, status) + + def parse_time_in_force(self, timeInForce: Str): + timeInForces: dict = { + 'ORDER_CONDITION_GOOD_TILL_CANCELLED': 'GTC', + 'ORDER_CONDITION_IMMEDIATE_OR_CANCEL': 'IOC', + 'ORDER_CONDITION_FILL_OR_KILL': 'FOK', + } + return self.safe_string(timeInForces, timeInForce, timeInForce) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder + # + # { + # "baseCurrency": "f7dac554-8139-4ff6-841f-0e586a5984a0", + # "quoteCurrency": "a5a7a7a9-e2a3-43f9-8754-29a02f6b709b", + # "side": "BID", + # "clientOrderId": "my-wonderful-order-number-71566", + # "price": "10103.19", + # "stopPrice": "10103.19", + # "quantity": "3.21", + # "timestamp": 1568185507 + # } + # + # fetchOrder, fetchOpenOrders, fetchOrders + # + # { + # "id":"a76bd262-3560-4bfb-98ac-1cedd394f4fc", + # "status":"ORDER_STATUS_PLACED", + # "side":"ORDER_SIDE_BUY", + # "condition":"ORDER_CONDITION_GOOD_TILL_CANCELLED", + # "type":"ORDER_TYPE_LIMIT", + # "baseCurrency":"620f2019-33c0-423b-8a9d-cde4d7f8ef7f", + # "quoteCurrency":"0c3a106d-bde3-4c13-a26e-3fd2394529e5", + # "clientOrderId":"web-macos_chrome_1a6a6659-6f7c-4fac-be0b-d1d7ac06d", + # "price":"4000.00", + # "quantity":"0.01", + # "cost":"40.000000000000000000", + # "filled":"0", + # "trader":"7244bb3a-b6b2-446a-ac78-fa4bce5b59a9", + # "creator":"ORDER_CREATOR_USER", + # "creatorId":"", + # "timestamp":1635920767648 + # } + # + # cancelOrder + # + # { + # "message":"cancellation request successfully submitted", + # "status":"SUCCESS", + # "id":"a631426d-3543-45ba-941e-75f7825afb0f" + # } + # + id = self.safe_string(order, 'id') + timestamp = self.safe_integer(order, 'timestamp') + baseId = self.safe_string(order, 'baseCurrency') + quoteId = self.safe_string(order, 'quoteCurrency') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + symbol = None + if (base is not None) and (quote is not None): + symbol = base + '/' + quote + if symbol in self.markets: + market = self.market(symbol) + orderSide = self.safe_string(order, 'side') + side = None + if orderSide is not None: + parts = orderSide.split('_') + partsLength = len(parts) + side = self.safe_string_lower(parts, partsLength - 1) + type = self.parse_order_type(self.safe_string(order, 'type')) + price = self.safe_string(order, 'price') + amount = self.safe_string(order, 'quantity') + filled = self.safe_string(order, 'filled') + cost = self.safe_string(order, 'cost') + status = self.parse_order_status(self.safe_string(order, 'status')) + message = self.safe_string(order, 'message') + if message is not None: + if message.find('cancel') >= 0: + status = 'canceled' + elif message.find('accept') >= 0: + status = 'open' + clientOrderId = self.safe_string(order, 'clientOrderId') + timeInForce = self.parse_time_in_force(self.safe_string(order, 'condition')) + return self.safe_order({ + 'id': id, + 'clientOrderId': clientOrderId, + 'info': order, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'status': status, + 'symbol': symbol, + 'type': type, + 'timeInForce': timeInForce, + 'postOnly': None, + 'side': side, + 'price': price, + 'triggerPrice': self.safe_string(order, 'stopPrice'), + 'cost': cost, + 'amount': amount, + 'filled': filled, + 'average': None, + 'remaining': None, + 'fee': None, + 'trades': None, + }, market) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://api.latoken.com/doc/v2/#tag/Order/operation/getMyActiveOrdersByPair + https://api.latoken.com/doc/v2/#tag/StopOrder/operation/getMyActiveStopOrdersByPair # stop + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: True if fetching trigger orders + :returns Order[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOpenOrders() requires a symbol argument') + self.load_markets() + response = None + isTrigger = self.safe_value_2(params, 'trigger', 'stop') + params = self.omit(params, 'stop') + # privateGetAuthOrderActive doesn't work even though its listed at https://api.latoken.com/doc/v2/#tag/Order/operation/getMyActiveOrders + market = self.market(symbol) + request: dict = { + 'currency': market['baseId'], + 'quote': market['quoteId'], + } + if isTrigger: + response = self.privateGetAuthStopOrderPairCurrencyQuoteActive(self.extend(request, params)) + else: + response = self.privateGetAuthOrderPairCurrencyQuoteActive(self.extend(request, params)) + # + # [ + # { + # "id":"a76bd262-3560-4bfb-98ac-1cedd394f4fc", + # "status":"ORDER_STATUS_PLACED", + # "side":"ORDER_SIDE_BUY", + # "condition":"ORDER_CONDITION_GOOD_TILL_CANCELLED", + # "type":"ORDER_TYPE_LIMIT", + # "baseCurrency":"620f2019-33c0-423b-8a9d-cde4d7f8ef7f", + # "quoteCurrency":"0c3a106d-bde3-4c13-a26e-3fd2394529e5", + # "clientOrderId":"web-macos_chrome_1a6a6659-6f7c-4fac-be0b-d1d7ac06d", + # "price":"4000.00", + # "quantity":"0.01000", + # "cost":"40.00", + # "filled":"0.00000", + # "trader":"7244bb3a-b6b2-446a-ac78-fa4bce5b59a9", + # "creator":"USER", + # "creatorId":"", + # "timestamp":1635920767648 + # } + # ] + # + return self.parse_orders(response, market, since, limit) + + def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://api.latoken.com/doc/v2/#tag/Order/operation/getMyOrders + https://api.latoken.com/doc/v2/#tag/Order/operation/getMyOrdersByPair + https://api.latoken.com/doc/v2/#tag/StopOrder/operation/getMyStopOrders # stop + https://api.latoken.com/doc/v2/#tag/StopOrder/operation/getMyStopOrdersByPair # stop + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: True if fetching trigger orders + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + request: dict = { + # 'currency': market['baseId'], + # 'quote': market['quoteId'], + # 'from': self.milliseconds(), + # 'limit': limit, # default '100' + } + market = None + isTrigger = self.safe_value_2(params, 'trigger', 'stop') + params = self.omit(params, ['stop', 'trigger']) + if limit is not None: + request['limit'] = limit # default 100 + response = None + if symbol is not None: + market = self.market(symbol) + request['currency'] = market['baseId'] + request['quote'] = market['quoteId'] + if isTrigger: + response = self.privateGetAuthStopOrderPairCurrencyQuote(self.extend(request, params)) + else: + response = self.privateGetAuthOrderPairCurrencyQuote(self.extend(request, params)) + else: + if isTrigger: + response = self.privateGetAuthStopOrder(self.extend(request, params)) + else: + response = self.privateGetAuthOrder(self.extend(request, params)) + # + # [ + # { + # "id":"a76bd262-3560-4bfb-98ac-1cedd394f4fc", + # "status":"ORDER_STATUS_PLACED", + # "side":"ORDER_SIDE_BUY", + # "condition":"ORDER_CONDITION_GOOD_TILL_CANCELLED", + # "type":"ORDER_TYPE_LIMIT", + # "baseCurrency":"620f2019-33c0-423b-8a9d-cde4d7f8ef7f", + # "quoteCurrency":"0c3a106d-bde3-4c13-a26e-3fd2394529e5", + # "clientOrderId":"web-macos_chrome_1a6a6659-6f7c-4fac-be0b-d1d7ac06d", + # "price":"4000.00", + # "quantity":"0.01000", + # "cost":"40.00", + # "filled":"0.00000", + # "trader":"7244bb3a-b6b2-446a-ac78-fa4bce5b59a9", + # "creator":"USER", + # "creatorId":"", + # "timestamp":1635920767648 + # } + # ] + # + return self.parse_orders(response, market, since, limit) + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://api.latoken.com/doc/v2/#tag/Order/operation/getOrderById + https://api.latoken.com/doc/v2/#tag/StopOrder/operation/getStopOrderById + + :param str id: order id + :param str [symbol]: not used by latoken fetchOrder + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: True if fetching a trigger order + :returns dict: An `order structure ` + """ + self.load_markets() + request: dict = { + 'id': id, + } + isTrigger = self.safe_value_2(params, 'trigger', 'stop') + params = self.omit(params, ['stop', 'trigger']) + response = None + if isTrigger: + response = self.privateGetAuthStopOrderGetOrderId(self.extend(request, params)) + else: + response = self.privateGetAuthOrderGetOrderId(self.extend(request, params)) + # + # { + # "id":"a76bd262-3560-4bfb-98ac-1cedd394f4fc", + # "status":"ORDER_STATUS_PLACED", + # "side":"ORDER_SIDE_BUY", + # "condition":"ORDER_CONDITION_GOOD_TILL_CANCELLED", + # "type":"ORDER_TYPE_LIMIT", + # "baseCurrency":"620f2019-33c0-423b-8a9d-cde4d7f8ef7f", + # "quoteCurrency":"0c3a106d-bde3-4c13-a26e-3fd2394529e5", + # "clientOrderId":"web-macos_chrome_1a6a6659-6f7c-4fac-be0b-d1d7ac06d", + # "price":"4000.00", + # "quantity":"0.01", + # "cost":"40.000000000000000000", + # "filled":"0", + # "trader":"7244bb3a-b6b2-446a-ac78-fa4bce5b59a9", + # "creator":"ORDER_CREATOR_USER", + # "creatorId":"", + # "timestamp":1635920767648 + # } + # + return self.parse_order(response) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://api.latoken.com/doc/v2/#tag/Order/operation/placeOrder + https://api.latoken.com/doc/v2/#tag/StopOrder/operation/placeStopOrder # stop + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: the price at which a trigger order is triggered at + + EXCHANGE SPECIFIC PARAMETERS + :param str [params.condition]: "GTC", "IOC", or "FOK" + :param str [params.clientOrderId]: [0 .. 50] characters, client's custom order id(free field for your convenience) + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + uppercaseType = type.upper() + request: dict = { + 'baseCurrency': market['baseId'], + 'quoteCurrency': market['quoteId'], + 'side': side.upper(), # "BUY", "BID", "SELL", "ASK" + 'condition': 'GTC', # "GTC", "GOOD_TILL_CANCELLED", "IOC", "IMMEDIATE_OR_CANCEL", "FOK", "FILL_OR_KILL" + 'type': uppercaseType, # "LIMIT", "MARKET" + 'clientOrderId': self.uuid(), # 50 characters max + # 'price': self.price_to_precision(symbol, price), + # 'quantity': self.amount_to_precision(symbol, amount), + 'quantity': self.amount_to_precision(symbol, amount), + 'timestamp': self.seconds(), + } + if uppercaseType == 'LIMIT': + request['price'] = self.price_to_precision(symbol, price) + triggerPrice = self.safe_string_2(params, 'triggerPrice', 'stopPrice') + params = self.omit(params, ['triggerPrice', 'stopPrice']) + response = None + if triggerPrice is not None: + request['stopPrice'] = self.price_to_precision(symbol, triggerPrice) + response = self.privatePostAuthStopOrderPlace(self.extend(request, params)) + else: + response = self.privatePostAuthOrderPlace(self.extend(request, params)) + # + # { + # "baseCurrency": "f7dac554-8139-4ff6-841f-0e586a5984a0", + # "quoteCurrency": "a5a7a7a9-e2a3-43f9-8754-29a02f6b709b", + # "side": "BID", + # "clientOrderId": "my-wonderful-order-number-71566", + # "price": "10103.19", + # "stopPrice": "10103.19", + # "quantity": "3.21", + # "timestamp": 1568185507 + # } + # + return self.parse_order(response, market) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://api.latoken.com/doc/v2/#tag/Order/operation/cancelOrder + https://api.latoken.com/doc/v2/#tag/StopOrder/operation/cancelStopOrder # stop + + :param str id: order id + :param str symbol: not used by latoken cancelOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: True if cancelling a trigger order + :returns dict: An `order structure ` + """ + self.load_markets() + request: dict = { + 'id': id, + } + isTrigger = self.safe_value_2(params, 'trigger', 'stop') + params = self.omit(params, ['stop', 'trigger']) + response = None + if isTrigger: + response = self.privatePostAuthStopOrderCancel(self.extend(request, params)) + else: + response = self.privatePostAuthOrderCancel(self.extend(request, params)) + # + # { + # "id": "12345678-1234-1244-1244-123456789012", + # "message": "cancellation request successfully submitted", + # "status": "SUCCESS", + # "error": "", + # "errors": {} + # } + # + return self.parse_order(response) + + def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders in a market + + https://api.latoken.com/doc/v2/#tag/Order/operation/cancelAllOrders + https://api.latoken.com/doc/v2/#tag/Order/operation/cancelAllOrdersByPair + + :param str symbol: unified market symbol of the market to cancel orders in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: True if cancelling trigger orders + :returns dict[]: a list of `order structures ` + """ + self.load_markets() + request: dict = { + # 'currency': market['baseId'], + # 'quote': market['quoteId'], + } + market = None + isTrigger = self.safe_value_2(params, 'trigger', 'stop') + params = self.omit(params, ['stop', 'trigger']) + response = None + if symbol is not None: + market = self.market(symbol) + request['currency'] = market['baseId'] + request['quote'] = market['quoteId'] + if isTrigger: + response = self.privatePostAuthStopOrderCancelAllCurrencyQuote(self.extend(request, params)) + else: + response = self.privatePostAuthOrderCancelAllCurrencyQuote(self.extend(request, params)) + else: + if isTrigger: + response = self.privatePostAuthStopOrderCancelAll(self.extend(request, params)) + else: + response = self.privatePostAuthOrderCancelAll(self.extend(request, params)) + # + # { + # "message":"cancellation request successfully submitted", + # "status":"SUCCESS" + # } + # + return [ + self.safe_order({ + 'info': response, + }), + ] + + def fetch_transactions(self, code: Str = None, since: Int = None, limit: Int = None, params={}): + """ + @deprecated + use fetchDepositsWithdrawals instead + + https://api.latoken.com/doc/v2/#tag/Transaction/operation/getUserTransactions + + :param str code: unified currency code for the currency of the transactions, default is None + :param int [since]: timestamp in ms of the earliest transaction, default is None + :param int [limit]: max number of transactions to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `transaction structure ` + """ + self.load_markets() + request: dict = { + # 'page': '1', + # 'size': 100, + } + response = self.privateGetAuthTransaction(self.extend(request, params)) + # + # { + # "hasNext":false, + # "content":[ + # { + # "id":"fbf7d0d1-2629-4ad8-9def-7a1dba423362", + # "status":"TRANSACTION_STATUS_CONFIRMED", + # "type":"TRANSACTION_TYPE_DEPOSIT", + # "senderAddress":"", + # "recipientAddress":"0x3c46fa2e3f9023bc4897828ed173f8ecb3a554bc", + # "amount":"200.000000000000000000", + # "transactionFee":"0.000000000000000000", + # "timestamp":1635893208404, + # "transactionHash":"0x28bad3b74a042df13d64ddfbca855566a51bf7f190b8cd565c236a18d5cd493f#42", + # "blockHeight":13540262, + # "currency":"0c3a106d-bde3-4c13-a26e-3fd2394529e5", + # "memo":null, + # "paymentProvider":"a8d6d1cb-f84a-4e9d-aa82-c6a08b356ee1", + # "requiresCode":false + # } + # ], + # "first":true, + # "hasContent":true, + # "pageSize":10 + # } + # + currency = None + if code is not None: + currency = self.currency(code) + content = self.safe_list(response, 'content', []) + return self.parse_transactions(content, currency, since, limit) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # { + # "id":"fbf7d0d1-2629-4ad8-9def-7a1dba423362", + # "status":"TRANSACTION_STATUS_CONFIRMED", + # "type":"TRANSACTION_TYPE_DEPOSIT", + # "senderAddress":"", + # "recipientAddress":"0x3c46fa2e3f9023bc4897828ed173f8ecb3a554bc", + # "amount":"200.000000000000000000", + # "transactionFee":"0.000000000000000000", + # "timestamp":1635893208404, + # "transactionHash":"0x28bad3b74a042df13d64ddfbca855566a51bf7f190b8cd565c236a18d5cd493f#42", + # "blockHeight":13540262, + # "currency":"0c3a106d-bde3-4c13-a26e-3fd2394529e5", + # "memo":null, + # "paymentProvider":"a8d6d1cb-f84a-4e9d-aa82-c6a08b356ee1", + # "requiresCode":false + # } + # + id = self.safe_string(transaction, 'id') + timestamp = self.safe_integer(transaction, 'timestamp') + currencyId = self.safe_string(transaction, 'currency') + code = self.safe_currency_code(currencyId, currency) + status = self.parse_transaction_status(self.safe_string(transaction, 'status')) + amount = self.safe_number(transaction, 'amount') + addressFrom = self.safe_string(transaction, 'senderAddress') + addressTo = self.safe_string(transaction, 'recipientAddress') + txid = self.safe_string(transaction, 'transactionHash') + tagTo = self.safe_string(transaction, 'memo') + fee = { + 'currency': None, + 'cost': None, + 'rate': None, + } + feeCost = self.safe_number(transaction, 'transactionFee') + if feeCost is not None: + fee['cost'] = feeCost + fee['currency'] = code + type = self.parse_transaction_type(self.safe_string(transaction, 'type')) + return { + 'info': transaction, + 'id': id, + 'txid': txid, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': None, + 'addressFrom': addressFrom, + 'addressTo': addressTo, + 'address': addressTo, + 'tagFrom': None, + 'tagTo': tagTo, + 'tag': tagTo, + 'type': type, + 'amount': amount, + 'currency': code, + 'status': status, + 'updated': None, + 'comment': None, + 'internal': None, + 'fee': fee, + } + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'TRANSACTION_STATUS_CONFIRMED': 'ok', + 'TRANSACTION_STATUS_EXECUTED': 'ok', + 'TRANSACTION_STATUS_CHECKING': 'pending', + 'TRANSACTION_STATUS_CANCELLED': 'canceled', + 'TRANSACTION_STATUS_FAILED': 'failed', + 'TRANSACTION_STATUS_REJECTED': 'rejected', + } + return self.safe_string(statuses, status, status) + + def parse_transaction_type(self, type): + types: dict = { + 'TRANSACTION_TYPE_DEPOSIT': 'deposit', + 'TRANSACTION_TYPE_WITHDRAWAL': 'withdrawal', + } + return self.safe_string(types, type, type) + + def fetch_transfers(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[TransferEntry]: + """ + fetch a history of internal transfers made on an account + + https://api.latoken.com/doc/v2/#tag/Transfer/operation/getUsersTransfers + + :param str code: unified currency code of the currency transferred + :param int [since]: the earliest time in ms to fetch transfers for + :param int [limit]: the maximum number of transfers structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transfer structures ` + """ + self.load_markets() + currency = self.currency(code) + response = self.privateGetAuthTransfer(params) + # + # { + # "hasNext": True, + # "content": [ + # { + # "id": "ebd6312f-cb4f-45d1-9409-4b0b3027f21e", + # "status": "TRANSFER_STATUS_COMPLETED", + # "type": "TRANSFER_TYPE_WITHDRAW_SPOT", + # "fromAccount": "c429c551-adbb-4078-b74b-276bea308a36", + # "toAccount": "631c6203-bd62-4734-a04d-9b2a951f43b9", + # "transferringFunds": 1259.0321785, + # "usdValue": 1259.032179, + # "rejectReason": null, + # "timestamp": 1633515579530, + # "direction": "INTERNAL", + # "method": "TRANSFER_METHOD_UNKNOWN", + # "recipient": null, + # "sender": null, + # "currency": "0c3a106d-bde3-4c13-a26e-3fd2394529e5", + # "codeRequired": False, + # "fromUser": "ce555f3f-585d-46fb-9ae6-487f66738073", + # "toUser": "ce555f3f-585d-46fb-9ae6-487f66738073", + # "fee": 0 + # }, + # ... + # ], + # "first": True, + # "pageSize": 20, + # "hasContent": True + # } + # + transfers = self.safe_list(response, 'content', []) + return self.parse_transfers(transfers, currency, since, limit) + + def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://api.latoken.com/doc/v2/#tag/Transfer/operation/transferByEmail + https://api.latoken.com/doc/v2/#tag/Transfer/operation/transferById + https://api.latoken.com/doc/v2/#tag/Transfer/operation/transferByPhone + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account to transfer from + :param str toAccount: account to transfer to + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transfer structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + 'recipient': toAccount, + 'value': self.currency_to_precision(code, amount), + } + response = None + if toAccount.find('@') >= 0: + response = self.privatePostAuthTransferEmail(self.extend(request, params)) + elif len(toAccount) == 36: + response = self.privatePostAuthTransferId(self.extend(request, params)) + else: + response = self.privatePostAuthTransferPhone(self.extend(request, params)) + # + # { + # "id": "e6fc4ace-7750-44e4-b7e9-6af038ac7107", + # "status": "TRANSFER_STATUS_COMPLETED", + # "type": "TRANSFER_TYPE_DEPOSIT_SPOT", + # "fromAccount": "3bf61015-bf32-47a6-b237-c9f70df772ad", + # "toAccount": "355eb279-7c7e-4515-814a-575a49dc0325", + # "transferringFunds": "500000.000000000000000000", + # "usdValue": "0.000000000000000000", + # "rejectReason": "", + # "timestamp": 1576844438402, + # "direction": "INTERNAL", + # "method": "TRANSFER_METHOD_UNKNOWN", + # "recipient": "", + # "sender": "", + # "currency": "40af7879-a8cc-4576-a42d-7d2749821b58", + # "codeRequired": False, + # "fromUser": "cd555555-666d-46fb-9ae6-487f66738073", + # "toUser": "cd555555-666d-46fb-9ae6-487f66738073", + # "fee": 0 + # } + # + return self.parse_transfer(response) + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + # + # { + # "id": "e6fc4ace-7750-44e4-b7e9-6af038ac7107", + # "status": "TRANSFER_STATUS_COMPLETED", + # "type": "TRANSFER_TYPE_DEPOSIT_SPOT", + # "fromAccount": "3bf61015-bf32-47a6-b237-c9f70df772ad", + # "toAccount": "355eb279-7c7e-4515-814a-575a49dc0325", + # "transferringFunds": "500000.000000000000000000", + # "usdValue": "0.000000000000000000", + # "rejectReason": "", + # "timestamp": 1576844438402, + # "direction": "INTERNAL", + # "method": "TRANSFER_METHOD_UNKNOWN", + # "recipient": "", + # "sender": "", + # "currency": "40af7879-a8cc-4576-a42d-7d2749821b58", + # "codeRequired": False, + # "fromUser": "cd555555-666d-46fb-9ae6-487f66738073", + # "toUser": "cd555555-666d-46fb-9ae6-487f66738073", + # "fee": 0 + # } + # + timestamp = self.safe_timestamp(transfer, 'timestamp') + currencyId = self.safe_string(transfer, 'currency') + status = self.safe_string(transfer, 'status') + return { + 'info': transfer, + 'id': self.safe_string(transfer, 'id'), + 'timestamp': self.safe_integer(transfer, 'timestamp'), + 'datetime': self.iso8601(timestamp), + 'currency': self.safe_currency_code(currencyId, currency), + 'amount': self.safe_number(transfer, 'transferringFunds'), + 'fromAccount': self.safe_string(transfer, 'fromAccount'), + 'toAccount': self.safe_string(transfer, 'toAccount'), + 'status': self.parse_transfer_status(status), + } + + def parse_transfer_status(self, status: Str) -> Str: + statuses: dict = { + 'TRANSFER_STATUS_COMPLETED': 'ok', + 'TRANSFER_STATUS_PENDING': 'pending', + 'TRANSFER_STATUS_REJECTED': 'failed', + 'TRANSFER_STATUS_UNVERIFIED': 'pending', + 'TRANSFER_STATUS_CANCELLED': 'canceled', + } + return self.safe_string(statuses, status, status) + + def sign(self, path, api='public', method='GET', params=None, headers=None, body=None): + request = '/' + self.version + '/' + self.implode_params(path, params) + requestString = request + query = self.omit(params, self.extract_params(path)) + urlencodedQuery = self.urlencode(query) + if method == 'GET': + if query: + requestString += '?' + urlencodedQuery + if api == 'private': + self.check_required_credentials() + auth = method + request + urlencodedQuery + signature = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha512) + headers = { + 'X-LA-APIKEY': self.apiKey, + 'X-LA-SIGNATURE': signature, + 'X-LA-DIGEST': 'HMAC-SHA512', # HMAC-SHA384, HMAC-SHA512, optional + } + if method == 'POST': + headers['Content-Type'] = 'application/json' + body = self.json(query) + url = self.urls['api']['rest'] + requestString + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if not response: + return None + # + # {"result":false,"message":"invalid API key, signature or digest","error":"BAD_REQUEST","status":"FAILURE"} + # {"result":false,"message":"request expired or bad / format","error":"BAD_REQUEST","status":"FAILURE"} + # {"message":"Internal Server Error","error":"INTERNAL_ERROR","status":"FAILURE"} + # {"result":false,"message":"Internal error","error":"For input string: \"NaN\"","status":"FAILURE"} + # + message = self.safe_string(response, 'message') + feedback = self.id + ' ' + body + if message is not None: + self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + error = self.safe_value(response, 'error') + errorMessage = self.safe_string(error, 'message') + if (error is not None) or (errorMessage is not None): + self.throw_exactly_matched_exception(self.exceptions['exact'], error, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], body, feedback) + raise ExchangeError(feedback) # unknown message + return None diff --git a/ccxt/lbank.py b/ccxt/lbank.py new file mode 100644 index 0000000..075c20a --- /dev/null +++ b/ccxt/lbank.py @@ -0,0 +1,3012 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.lbank import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currencies, Currency, DepositAddress, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, FundingRate, FundingRates, Trade, TradingFeeInterface, TradingFees, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidAddress +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import DuplicateOrderId +from ccxt.base.errors import NotSupported +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import InvalidNonce +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class lbank(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(lbank, self).describe(), { + 'id': 'lbank', + 'name': 'LBank', + 'countries': ['CN'], + 'version': 'v2', + # 50 per second for making and cancelling orders 1000ms / 50 = 20 + # 20 per second for all other requests, cost = 50 / 20 = 2.5 + 'rateLimit': 20, + 'pro': True, + 'has': { + 'CORS': False, + 'spot': True, + 'margin': False, + 'swap': None, + 'future': False, + 'option': False, + 'addMargin': False, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'createMarketBuyOrderWithCost': True, + 'createMarketOrderWithCost': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createReduceOnlyOrder': False, + 'createStopLimitOrder': False, + 'createStopMarketOrder': False, + 'createStopOrder': False, + 'fetchBalance': True, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchClosedOrders': False, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDepositWithdrawFee': 'emulated', + 'fetchDepositWithdrawFees': True, + 'fetchFundingHistory': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': True, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLeverage': False, + 'fetchLeverageTiers': False, + 'fetchMarginMode': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchPosition': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFees': True, + 'fetchTransactionFees': True, + 'reduceMargin': False, + 'setLeverage': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'withdraw': True, + }, + 'timeframes': { + '1m': 'minute1', + '5m': 'minute5', + '15m': 'minute15', + '30m': 'minute30', + '1h': 'hour1', + '2h': 'hour2', + '4h': 'hour4', + '6h': 'hour6', + '8h': 'hour8', + '12h': 'hour12', + '1d': 'day1', + '1w': 'week1', + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/38063602-9605e28a-3302-11e8-81be-64b1e53c4cfb.jpg', + 'api': { + 'rest': 'https://api.lbank.info', + 'contract': 'https://lbkperp.lbank.com', + }, + 'api2': 'https://api.lbkex.com', + 'www': 'https://www.lbank.com', + 'doc': 'https://www.lbank.com/en-US/docs/index.html', + 'fees': 'https://support.lbank.site/hc/en-gb/articles/900000535703-Trading-Fees-From-14-00-on-April-7-2020-UTC-8-', + 'referral': 'https://www.lbank.com/login/?icode=7QCY', + }, + 'api': { + 'spot': { + 'public': { + 'get': { + 'currencyPairs': 2.5, + 'accuracy': 2.5, + 'usdToCny': 2.5, + 'assetConfigs': 2.5, + 'withdrawConfigs': 2.5 * 1.5, # frequently rate-limits, so increase self endpoint RL + 'timestamp': 2.5, + 'ticker/24hr': 2.5, + 'ticker': 2.5, + 'depth': 2.5, + 'incrDepth': 2.5, + 'trades': 2.5, + 'kline': 2.5, + # new quote endpoints + 'supplement/system_ping': 2.5, + 'supplement/incrDepth': 2.5, + 'supplement/trades': 2.5, + 'supplement/ticker/price': 2.5, + 'supplement/ticker/bookTicker': 2.5, + }, + 'post': { + 'supplement/system_status': 2.5, + }, + }, + 'private': { + 'post': { + # account + 'user_info': 2.5, + 'subscribe/get_key': 2.5, + 'subscribe/refresh_key': 2.5, + 'subscribe/destroy_key': 2.5, + 'get_deposit_address': 2.5, + 'deposit_history': 2.5, + # order + 'create_order': 1, + 'batch_create_order': 1, + 'cancel_order': 1, + 'cancel_clientOrders': 1, + 'orders_info': 2.5, + 'orders_info_history': 2.5, + 'order_transaction_detail': 2.5, + 'transaction_history': 2.5, + 'orders_info_no_deal': 2.5, + # withdraw + 'withdraw': 2.5, + 'withdrawCancel': 2.5, + 'withdraws': 2.5, + 'supplement/user_info': 2.5, + 'supplement/withdraw': 2.5, + 'supplement/deposit_history': 2.5, + 'supplement/withdraws': 2.5, + 'supplement/get_deposit_address': 2.5, + 'supplement/asset_detail': 2.5, + 'supplement/customer_trade_fee': 2.5, + 'supplement/api_Restrictions': 2.5, + # new quote endpoints + 'supplement/system_ping': 2.5, + # new order endpoints + 'supplement/create_order_test': 1, + 'supplement/create_order': 1, + 'supplement/cancel_order': 1, + 'supplement/cancel_order_by_symbol': 1, + 'supplement/orders_info': 2.5, + 'supplement/orders_info_no_deal': 2.5, + 'supplement/orders_info_history': 2.5, + 'supplement/user_info_account': 2.5, + 'supplement/transaction_history': 2.5, + }, + }, + }, + 'contract': { + 'public': { + 'get': { + 'cfd/openApi/v1/pub/getTime': 2.5, + 'cfd/openApi/v1/pub/instrument': 2.5, + 'cfd/openApi/v1/pub/marketData': 2.5, + 'cfd/openApi/v1/pub/marketOrder': 2.5, + }, + }, + }, + }, + 'fees': { + 'trading': { + 'maker': self.parse_number('0.001'), + 'taker': self.parse_number('0.001'), + }, + 'funding': { + 'withdraw': {}, + }, + }, + 'commonCurrencies': { + 'XBT': 'XBT', # not BTC! + 'HIT': 'Hiver', + 'VET_ERC20': 'VEN', + 'PNT': 'Penta', + }, + 'precisionMode': TICK_SIZE, + 'options': { + 'cacheSecretAsPem': True, + 'createMarketBuyOrderRequiresPrice': True, + 'fetchTrades': { + 'method': 'spotPublicGetTrades', # or 'spotPublicGetTradesSupplement' + }, + 'fetchTransactionFees': { # DEPRECATED, please use fetchDepositWithdrawFees + 'method': 'fetchPrivateTransactionFees', # or 'fetchPublicTransactionFees' + }, + 'fetchDepositWithdrawFees': { + 'method': 'fetchPrivateDepositWithdrawFees', # or 'fetchPublicDepositWithdrawFees' + }, + 'fetchDepositAddress': { + 'method': 'fetchDepositAddressDefault', # or fetchDepositAddressSupplement + }, + 'createOrder': { + 'method': 'spotPrivatePostSupplementCreateOrder', # or spotPrivatePostCreateOrder + }, + 'fetchOrder': { + 'method': 'fetchOrderSupplement', # or fetchOrderDefault + }, + 'fetchBalance': { + 'method': 'spotPrivatePostSupplementUserInfo', # or spotPrivatePostSupplementUserInfoAccount or spotPrivatePostUserInfo + }, + 'networks': { + 'ERC20': 'erc20', + 'ETH': 'erc20', + 'TRC20': 'trc20', + 'TRX': 'trc20', + 'OMNI': 'omni', + 'ASA': 'asa', + 'BEP20': 'bep20(bsc)', + 'BSC': 'bep20(bsc)', + 'HT': 'heco', + 'BNB': 'bep2', + 'BTC': 'btc', + 'DOGE': 'dogecoin', + 'MATIC': 'matic', + 'POLYGON': 'matic', + 'OEC': 'oec', + 'BTCTRON': 'btctron', + 'XRP': 'xrp', + # other unusual chains with number of listed currencies supported + # 'avax c-chain': 1, + # klay: 12, + # bta: 1, + # fantom: 1, + # celo: 1, + # sol: 2, + # zenith: 1, + # ftm: 5, + # bep20: 1,(single token with mis-named chain) SSS + # bitci: 1, + # sgb: 1, + # moonbeam: 1, + # ekta: 1, + # etl: 1, + # arbitrum: 1, + # tpc: 1, + # ptx: 1 + # } + }, + 'networksById': { + 'erc20': 'ERC20', + 'trc20': 'TRC20', + 'TRX': 'TRC20', + 'bep20(bsc)': 'BEP20', + 'bep20': 'BEP20', + }, + 'defaultNetworks': { + 'USDT': 'TRC20', + }, + }, + 'features': { + 'default': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': False, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': False, + 'GTD': False, + }, + 'hedged': False, + 'selfTradePrevention': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': False, + 'iceberg': False, + }, + 'createOrders': None, # todo + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 100000, # todo + 'untilDays': 2, + 'symbolRequired': True, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 200, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': 200, + 'daysBack': None, + 'untilDays': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchClosedOrders': None, # todo: through fetchOrders "status" -1: Cancelled 0: Unfilled 1: Partially filled 2: Completely filled 3: Partially filled has been cancelled 4: Cancellation is being processed + 'fetchOHLCV': { + 'limit': 2000, + }, + }, + 'spot': { + 'extends': 'default', + }, + 'swap': { + 'linear': { + 'extends': 'default', + }, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + }) + + def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://www.lbank.com/en-US/docs/index.html#get-timestamp + https://www.lbank.com/en-US/docs/contract.html#get-the-current-time + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + type = None + type, params = self.handle_market_type_and_params('fetchTime', None, params) + response = None + if type == 'swap': + response = self.contractPublicGetCfdOpenApiV1PubGetTime(params) + else: + response = self.spotPublicGetTimestamp(params) + # + # spot + # + # { + # "result": "true", + # "data": 1691789627950, + # "error_code": 0, + # "ts": 1691789627950 + # } + # + # swap + # + # { + # "data": 1691789627950, + # "error_code": 0, + # "msg": "Success", + # "result": "true", + # "success": True + # } + # + return self.safe_integer(response, 'data') + + def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = self.spotPublicGetWithdrawConfigs(params) + # + # { + # "msg": "Success", + # "result": "true", + # "data": [ + # { + # "amountScale": "4", + # "chain": "bep20(bsc)", + # "assetCode": "usdt", + # "min": "10", + # "transferAmtScale": "4", + # "canWithDraw": True, + # "fee": "0.0000", + # "minTransfer": "0.0001", + # "type": "1" + # }, + # { + # "amountScale": "4", + # "chain": "trc20", + # "assetCode": "usdt", + # "min": "1", + # "transferAmtScale": "4", + # "canWithDraw": True, + # "fee": "1.0000", + # "minTransfer": "0.0001", + # "type": "1" + # }, + # ... + # ], + # "error_code": "0", + # "ts": "1747973911431" + # } + # + currenciesData = self.safe_list(response, 'data', []) + grouped = self.group_by(currenciesData, 'assetCode') + groupedKeys = list(grouped.keys()) + result: dict = {} + for i in range(0, len(groupedKeys)): + id = str((groupedKeys[i])) # some currencies are numeric + code = self.safe_currency_code(id) + networksRaw = grouped[id] + networks = {} + for j in range(0, len(networksRaw)): + networkEntry = networksRaw[j] + networkId = self.safe_string(networkEntry, 'chain') + if networkId is None: + networkId = self.safe_string(networkEntry, 'assetCode') # use type if networkId is not present + networkCode = self.network_id_to_code(networkId) + networks[networkCode] = { + 'id': networkId, + 'network': networkCode, + 'limits': { + 'withdraw': { + 'min': self.safe_number(networkEntry, 'min'), + 'max': None, + }, + 'deposit': { + 'min': self.safe_number(networkEntry, 'minTransfer'), + 'max': None, + }, + }, + 'active': None, + 'deposit': None, + 'withdraw': self.safe_bool(networkEntry, 'canWithDraw'), + 'fee': self.safe_number(networkEntry, 'fee'), + 'precision': self.parse_number(self.parse_precision(self.safe_string(networkEntry, 'transferAmtScale'))), + 'info': networkEntry, + } + result[code] = self.safe_currency_structure({ + 'id': id, + 'code': code, + 'precision': None, + 'type': None, + 'name': None, + 'active': None, + 'deposit': None, + 'withdraw': None, + 'fee': None, + 'limits': { + 'withdraw': { + 'min': None, + 'max': None, + }, + 'deposit': { + 'min': None, + 'max': None, + }, + }, + 'networks': networks, + 'info': networksRaw, + }) + return result + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for lbank + + https://www.lbank.com/en-US/docs/index.html#trading-pairs + https://www.lbank.com/en-US/docs/contract.html#query-contract-information-list + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + marketsPromises = [ + self.fetch_spot_markets(params), + self.fetch_swap_markets(params), + ] + resolvedMarkets = marketsPromises + return self.array_concat(resolvedMarkets[0], resolvedMarkets[1]) + + def fetch_spot_markets(self, params={}): + response = self.spotPublicGetAccuracy(params) + # + # { + # "result": "true", + # "data": [ + # { + # "symbol": "btc_usdt", + # "quantityAccuracy": "4", + # "minTranQua": "0.0001", + # "priceAccuracy": "2" + # }, + # ], + # "error_code": 0, + # "ts": 1691560288484 + # } + # + data = self.safe_value(response, 'data', []) + result = [] + for i in range(0, len(data)): + market = data[i] + marketId = self.safe_string(market, 'symbol') + parts = marketId.split('_') + baseId = parts[0] + quoteId = parts[1] + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + symbol = base + '/' + quote + result.append({ + 'id': marketId, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'baseId': baseId, + 'quoteId': quoteId, + 'settle': None, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': True, + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(self.parse_precision(self.safe_string(market, 'quantityAccuracy'))), + 'price': self.parse_number(self.parse_precision(self.safe_string(market, 'priceAccuracy'))), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(market, 'minTranQua'), + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + 'info': market, + }) + return result + + def fetch_swap_markets(self, params={}): + request: dict = { + 'productGroup': 'SwapU', + } + response = self.contractPublicGetCfdOpenApiV1PubInstrument(self.extend(request, params)) + # + # { + # "data": [ + # { + # "priceLimitUpperValue": 0.2, + # "symbol": "BTCUSDT", + # "volumeTick": 0.0001, + # "indexPrice": "29707.70200000", + # "minOrderVolume": "0.0001", + # "priceTick": 0.1, + # "maxOrderVolume": "30.0", + # "baseCurrency": "BTC", + # "volumeMultiple": 1.0, + # "exchangeID": "Exchange", + # "priceCurrency": "USDT", + # "priceLimitLowerValue": 0.2, + # "clearCurrency": "USDT", + # "symbolName": "BTCUSDT", + # "defaultLeverage": 20.0, + # "minOrderCost": "5.0" + # }, + # ], + # "error_code": 0, + # "msg": "Success", + # "result": "true", + # "success": True + # } + # + data = self.safe_value(response, 'data', []) + result = [] + for i in range(0, len(data)): + market = data[i] + marketId = self.safe_string(market, 'symbol') + baseId = self.safe_string(market, 'baseCurrency') + settleId = self.safe_string(market, 'clearCurrency') + quoteId = settleId + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + settle = self.safe_currency_code(settleId) + symbol = base + '/' + quote + ':' + settle + result.append({ + 'id': marketId, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': 'swap', + 'spot': False, + 'margin': False, + 'swap': True, + 'future': False, + 'option': False, + 'active': True, + 'contract': True, + 'linear': True, + 'inverse': False, + 'contractSize': self.safe_number(market, 'volumeMultiple'), + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.safe_number(market, 'volumeTick'), + 'price': self.safe_number(market, 'priceTick'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(market, 'minOrderVolume'), + 'max': self.safe_number(market, 'maxOrderVolume'), + }, + 'price': { + 'min': self.safe_number(market, 'priceLimitLowerValue'), + 'max': self.safe_number(market, 'priceLimitUpperValue'), + }, + 'cost': { + 'min': self.safe_number(market, 'minOrderCost'), + 'max': None, + }, + }, + 'created': None, + 'info': market, + }) + return result + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # spot: fetchTicker, fetchTickers + # + # { + # "symbol": "btc_usdt", + # "ticker": { + # "high": "29695.57", + # "vol": "6890.2789", + # "low": "29110", + # "change": "0.58", + # "turnover": "202769821.06", + # "latest": "29405.98" + # }, + # "timestamp": :1692064274908 + # } + # + # swap: fetchTickers + # + # { + # "prePositionFeeRate": "0.000053", + # "volume": "2435.459", + # "symbol": "BTCUSDT", + # "highestPrice": "29446.5", + # "lowestPrice": "29362.9", + # "openPrice": "29419.5", + # "markedPrice": "29385.1", + # "turnover": "36345526.2438402", + # "lastPrice": "29387.0" + # } + # + timestamp = self.safe_integer(ticker, 'timestamp') + marketId = self.safe_string(ticker, 'symbol') + symbol = self.safe_symbol(marketId, market) + tickerData = self.safe_value(ticker, 'ticker', {}) + market = self.safe_market(marketId, market) + data = ticker if (market['contract']) else tickerData + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string_2(data, 'high', 'highestPrice'), + 'low': self.safe_string_2(data, 'low', 'lowestPrice'), + 'bid': None, + 'bidVolume': None, + 'ask': None, + 'askVolume': None, + 'vwap': None, + 'open': self.safe_string(data, 'openPrice'), + 'close': None, + 'last': self.safe_string_2(data, 'latest', 'lastPrice'), + 'previousClose': None, + 'change': None, + 'percentage': self.safe_string(data, 'change'), + 'average': None, + 'baseVolume': self.safe_string_2(data, 'vol', 'volume'), + 'quoteVolume': self.safe_string(data, 'turnover'), + 'info': ticker, + }, market) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://www.lbank.com/en-US/docs/index.html#query-current-market-data-new + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + if market['swap']: + responseForSwap = self.fetch_tickers([market['symbol']], params) + return self.safe_value(responseForSwap, market['symbol']) + request: dict = { + 'symbol': market['id'], + } + response = self.spotPublicGetTicker24hr(self.extend(request, params)) + # + # { + # "result": "true", + # "data": [ + # { + # "symbol": "btc_usdt", + # "ticker": { + # "high": "29695.57", + # "vol": "6890.2789", + # "low": "29110", + # "change": "0.58", + # "turnover": "202769821.06", + # "latest": "29405.98" + # }, + # "timestamp": :1692064274908 + # } + # ], + # "error_code": 0, + # "ts": :1692064276872 + # } + # + data = self.safe_value(response, 'data', []) + first = self.safe_dict(data, 0, {}) + return self.parse_ticker(first, market) + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://www.lbank.com/en-US/docs/index.html#query-current-market-data-new + https://www.lbank.com/en-US/docs/contract.html#query-contract-market-list + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + market = None + if symbols is not None: + symbols = self.market_symbols(symbols) + symbolsLength = len(symbols) + if symbolsLength > 0: + market = self.market(symbols[0]) + request: dict = {} + type = None + type, params = self.handle_market_type_and_params('fetchTickers', market, params) + response = None + if type == 'swap': + request['productGroup'] = 'SwapU' + response = self.contractPublicGetCfdOpenApiV1PubMarketData(self.extend(request, params)) + else: + request['symbol'] = 'all' + response = self.spotPublicGetTicker24hr(self.extend(request, params)) + # + # spot + # + # { + # "result": "true", + # "data": [ + # { + # "symbol": "btc_usdt", + # "ticker": { + # "high": "29695.57", + # "vol": "6890.2789", + # "low": "29110", + # "change": "0.58", + # "turnover": "202769821.06", + # "latest": "29405.98" + # }, + # "timestamp": :1692064274908 + # } + # ], + # "error_code": 0, + # "ts": :1692064276872 + # } + # + # swap + # + # { + # "data": [ + # { + # "prePositionFeeRate": "0.000053", + # "volume": "2435.459", + # "symbol": "BTCUSDT", + # "highestPrice": "29446.5", + # "lowestPrice": "29362.9", + # "openPrice": "29419.5", + # "markedPrice": "29385.1", + # "turnover": "36345526.2438402", + # "lastPrice": "29387.0" + # }, + # ], + # "error_code": 0, + # "msg": "Success", + # "result": "true", + # "success": True + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_tickers(data, symbols) + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://www.lbank.com/en-US/docs/index.html#query-market-depth + https://www.lbank.com/en-US/docs/contract.html#get-handicap + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + if limit is None: + limit = 60 + request: dict = { + 'symbol': market['id'], + } + type = None + type, params = self.handle_market_type_and_params('fetchOrderBook', market, params) + response = None + if type == 'swap': + request['depth'] = limit + response = self.contractPublicGetCfdOpenApiV1PubMarketOrder(self.extend(request, params)) + else: + request['size'] = limit + response = self.spotPublicGetDepth(self.extend(request, params)) + # + # spot + # + # { + # "result": "true", + # "data": { + # "asks": [ + # ["29243.37", "2.8783"], + # ["29243.39", "2.2842"], + # ["29243.4", "0.0337"] + # ], + # "bids": [ + # ["29243.36", "1.5258"], + # ["29243.34", "0.8218"], + # ["29243.28", "1.285"] + # ], + # "timestamp": :1692157328820 + # }, + # "error_code": 0, + # "ts": :1692157328820 + # } + # + # swap + # + # { + # "data": { + # "symbol": "BTCUSDT", + # "asks": [ + # { + # "volume": "14.6535", + # "price": "29234.2", + # "orders": "1" + # }, + # ], + # "bids": [ + # { + # "volume": "13.4899", + # "price": "29234.1", + # "orders": "4" + # }, + # ] + # }, + # "error_code": 0, + # "msg": "Success", + # "result": "true", + # "success": True + # } + # + orderbook = self.safe_value(response, 'data', {}) + timestamp = self.milliseconds() + if market['swap']: + return self.parse_order_book(orderbook, market['symbol'], timestamp, 'bids', 'asks', 'price', 'volume') + return self.parse_order_book(orderbook, market['symbol'], timestamp, 'bids', 'asks') + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(old) spotPublicGetTrades + # + # { + # "date_ms":1647021989789, + # "amount":0.0028, + # "price":38804.2, + # "type":"buy", + # "tid":"52d5616ee35c43019edddebe59b3e094" + # } + # + # + # fetchTrades(new) spotPublicGetTradesSupplement + # + # { + # "quoteQty":1675.048485, + # "price":0.127545, + # "qty":13133, + # "id":"3589541dc22e4357b227283650f714e2", + # "time":1648058297110, + # "isBuyerMaker":false + # } + # + # fetchMyTrades(private) + # + # { + # "orderUuid":"38b4e7a4-14f6-45fd-aba1-1a37024124a0", + # "tradeFeeRate":0.0010000000, + # "dealTime":1648500944496, + # "dealQuantity":30.00000000000000000000, + # "tradeFee":0.00453300000000000000, + # "txUuid":"11f3850cc6214ea3b495adad3a032794", + # "dealPrice":0.15111300000000000000, + # "dealVolumePrice":4.53339000000000000000, + # "tradeType":"sell_market" + # } + # + timestamp = self.safe_integer_2(trade, 'date_ms', 'time') + if timestamp is None: + timestamp = self.safe_integer(trade, 'dealTime') + amountString = self.safe_string_2(trade, 'amount', 'qty') + if amountString is None: + amountString = self.safe_string(trade, 'dealQuantity') + priceString = self.safe_string(trade, 'price') + if priceString is None: + priceString = self.safe_string(trade, 'dealPrice') + costString = self.safe_string(trade, 'quoteQty') + if costString is None: + costString = self.safe_string(trade, 'dealVolumePrice') + side = self.safe_string_2(trade, 'tradeType', 'type') + type = None + takerOrMaker = None + if side is not None: + parts = side.split('_') + side = self.safe_string(parts, 0) + typePart = self.safe_string(parts, 1) + type = 'limit' + takerOrMaker = 'taker' + if typePart is not None: + if typePart == 'market': + type = 'market' + elif typePart == 'maker': + takerOrMaker = 'maker' + id = self.safe_string_2(trade, 'tid', 'id') + if id is None: + id = self.safe_string(trade, 'txUuid') + order = self.safe_string(trade, 'orderUuid') + symbol = self.safe_symbol(None, market) + fee = None + feeCost = self.safe_string(trade, 'tradeFee') + if feeCost is not None: + fee = { + 'cost': feeCost, + 'currency': market['base'] if (side == 'buy') else market['quote'], + 'rate': self.safe_string(trade, 'tradeFeeRate'), + } + return self.safe_trade({ + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'id': id, + 'order': order, + 'type': type, + 'takerOrMaker': takerOrMaker, + 'side': side, + 'price': priceString, + 'amount': amountString, + 'cost': costString, + 'fee': fee, + 'info': trade, + }, market) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://www.lbank.com/en-US/docs/index.html#query-historical-transactions + https://www.lbank.com/en-US/docs/index.html#recent-transactions-list + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if since is not None: + request['time'] = since + if limit is not None: + request['size'] = min(limit, 600) + else: + request['size'] = 600 # max + options = self.safe_value(self.options, 'fetchTrades', {}) + defaultMethod = self.safe_string(options, 'method', 'spotPublicGetTrades') + method = self.safe_string(params, 'method', defaultMethod) + params = self.omit(params, 'method') + response = None + if method == 'spotPublicGetSupplementTrades': + response = self.spotPublicGetSupplementTrades(self.extend(request, params)) + else: + response = self.spotPublicGetTrades(self.extend(request, params)) + # + # { + # "result":"true", + # "data": [ + # { + # "date_ms":1647021989789, + # "amount":0.0028, + # "price":38804.2, + # "type":"buy", + # "tid":"52d5616ee35c43019edddebe59b3e094" + # } + # ], + # "error_code":0, + # "ts":1647021999308 + # } + # + trades = self.safe_list(response, 'data', []) + return self.parse_trades(trades, market, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # [ + # 1482311500, # timestamp + # 5423.23, # open + # 5472.80, # high + # 5516.09, # low + # 5462, # close + # 234.3250 # volume + # ], + # + return [ + self.safe_timestamp(ohlcv, 0), # timestamp + self.safe_number(ohlcv, 1), # open + self.safe_number(ohlcv, 2), # high + self.safe_number(ohlcv, 3), # low + self.safe_number(ohlcv, 4), # close + self.safe_number(ohlcv, 5), # volume + ] + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://www.lbank.com/en-US/docs/index.html#query-k-bar-data + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + # endpoint doesnt work + self.load_markets() + market = self.market(symbol) + if limit is None: + limit = 100 + else: + limit = min(limit, 2000) + if since is None: + duration = self.parse_timeframe(timeframe) + since = self.milliseconds() - (duration * 1000 * limit) + request: dict = { + 'symbol': market['id'], + 'type': self.safe_string(self.timeframes, timeframe, timeframe), + 'time': self.parse_to_int(since / 1000), + 'size': min(limit + 1, 2000), # max 2000 + } + response = self.spotPublicGetKline(self.extend(request, params)) + ohlcvs = self.safe_list(response, 'data', []) + # + # + # [ + # [ + # 1482311500, + # 5423.23, + # 5472.80, + # 5516.09, + # 5462, + # 234.3250 + # ], + # [ + # 1482311400, + # 5432.52, + # 5459.87, + # 5414.30, + # 5428.23, + # 213.7329 + # ] + # ] + # + return self.parse_ohlcvs(ohlcvs, market, timeframe, since, limit) + + def parse_balance(self, response) -> Balances: + # + # spotPrivatePostUserInfo + # + # { + # "toBtc": { + # "egc:": "0", + # "iog": "0", + # "ksm": "0", + # }, + # "freeze": { + # "egc": "0", + # "iog": "0", + # "ksm": "0" , + # }, + # "asset": { + # "egc": "0", + # "iog": "0", + # "ksm": "0", + # }, + # "free": { + # "egc": "0", + # "iog": "0", + # "ksm": "0", + # } + # } + # + # spotPrivatePostSupplementUserInfoAccount + # + # { + # "balances":[ + # { + # "asset":"lbk", + # "free":"0", + # "locked":"0" + # }, ... + # ] + # } + # + # spotPrivatePostSupplementUserInfo + # + # [ + # { + # "usableAmt":"31.45130723", + # "assetAmt":"31.45130723", + # "networkList":[ + # { + # "isDefault":true, + # "withdrawFeeRate":"", + # "name":"bep20(bsc)", + # "withdrawMin":30, + # "minLimit":0.0001, + # "minDeposit":0.0001, + # "feeAssetCode":"doge", + # "withdrawFee":"30", + # "type":1, + # "coin":"doge", + # "network":"bsc" + # }, + # { + # "isDefault":false, + # "withdrawFeeRate":"", + # "name":"dogecoin", + # "withdrawMin":10, + # "minLimit":0.0001, + # "minDeposit":10, + # "feeAssetCode":"doge", + # "withdrawFee":"10", + # "type":1, + # "coin":"doge", + # "network":"dogecoin" + # } + # ], + # "freezeAmt":"0", + # "coin":"doge" + # }, ... + # ] + # + timestamp = self.safe_integer(response, 'ts') + result: dict = { + 'info': response, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + data = self.safe_value(response, 'data') + # from spotPrivatePostUserInfo + toBtc = self.safe_value(data, 'toBtc') + if toBtc is not None: + used = self.safe_value(data, 'freeze', {}) + free = self.safe_value(data, 'free', {}) + currencies = list(free.keys()) + for i in range(0, len(currencies)): + currencyId = currencies[i] + code = self.safe_currency_code(currencyId) + account = self.account() + account['used'] = self.safe_string(used, currencyId) + account['free'] = self.safe_string(free, currencyId) + result[code] = account + return self.safe_balance(result) + # from spotPrivatePostSupplementUserInfoAccount + balances = self.safe_value(data, 'balances') + if balances is not None: + for i in range(0, len(balances)): + item = balances[i] + currencyId = self.safe_string(item, 'asset') + codeInner = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(item, 'free') + account['used'] = self.safe_string(item, 'locked') + result[codeInner] = account + return self.safe_balance(result) + # from spotPrivatePostSupplementUserInfo + isArray = isinstance(data, list) + if isArray is True: + for i in range(0, len(data)): + item = data[i] + currencyId = self.safe_string(item, 'coin') + codeInner = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(item, 'usableAmt') + account['used'] = self.safe_string(item, 'freezeAmt') + result[codeInner] = account + return self.safe_balance(result) + return None + + def parse_funding_rate(self, ticker, market: Market = None) -> FundingRate: + # { + # "symbol": "BTCUSDT", + # "highestPrice": "69495.5", + # "underlyingPrice": "68455.904", + # "lowestPrice": "68182.1", + # "openPrice": "68762.4", + # "positionFeeRate": "0.0001", + # "volume": "33534.2858", + # "markedPrice": "68434.1", + # "turnover": "1200636218.210558", + # "positionFeeTime": "28800", + # "lastPrice": "68427.3", + # "nextFeeTime": "1730736000000", + # "fundingRate": "0.0001", + # } + marketId = self.safe_string(ticker, 'symbol') + symbol = self.safe_symbol(marketId, market) + markPrice = self.safe_number(ticker, 'markedPrice') + indexPrice = self.safe_number(ticker, 'underlyingPrice') + fundingRate = self.safe_number(ticker, 'fundingRate') + fundingTime = self.safe_integer(ticker, 'nextFeeTime') + positionFeeTime = self.safe_integer(ticker, 'positionFeeTime') + intervalString = None + if positionFeeTime is not None: + interval = self.parse_to_int(positionFeeTime / 60 / 60) + intervalString = str(interval) + 'h' + return { + 'info': ticker, + 'symbol': symbol, + 'markPrice': markPrice, + 'indexPrice': indexPrice, + 'fundingRate': fundingRate, + 'fundingTimestamp': fundingTime, + 'fundingDatetime': self.iso8601(fundingTime), + 'timestamp': None, + 'datetime': None, + 'nextFundingRate': None, + 'nextFundingTimestamp': None, + 'nextFundingDatetime': None, + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': intervalString, + } + + def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate + + https://www.lbank.com/en-US/docs/contract.html#query-contract-market-list + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + self.load_markets() + market = self.market(symbol) + responseForSwap = self.fetch_funding_rates([market['symbol']], params) + return self.safe_value(responseForSwap, market['symbol']) + + def fetch_funding_rates(self, symbols: Strings = None, params={}) -> FundingRates: + """ + fetch the funding rate for multiple markets + + https://www.lbank.com/en-US/docs/contract.html#query-contract-market-list + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `funding rate structures `, indexed by market symbols + """ + self.load_markets() + symbols = self.market_symbols(symbols) + request: dict = { + 'productGroup': 'SwapU', + } + response = self.contractPublicGetCfdOpenApiV1PubMarketData(self.extend(request, params)) + # { + # "data": [ + # { + # "symbol": "BTCUSDT", + # "highestPrice": "69495.5", + # "underlyingPrice": "68455.904", + # "lowestPrice": "68182.1", + # "openPrice": "68762.4", + # "positionFeeRate": "0.0001", + # "volume": "33534.2858", + # "markedPrice": "68434.1", + # "turnover": "1200636218.210558", + # "positionFeeTime": "28800", + # "lastPrice": "68427.3", + # "nextFeeTime": "1730736000000", + # "fundingRate": "0.0001", + # } + # ], + # "error_code": "0", + # "msg": "Success", + # "result": "true", + # "success": True, + # } + data = self.safe_list(response, 'data', []) + return self.parse_funding_rates(data, symbols) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://www.lbank.com/en-US/docs/index.html#asset-information + https://www.lbank.com/en-US/docs/index.html#account-information + https://www.lbank.com/en-US/docs/index.html#get-all-coins-information + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + options = self.safe_value(self.options, 'fetchBalance', {}) + defaultMethod = self.safe_string(options, 'method', 'spotPrivatePostSupplementUserInfo') + method = self.safe_string(params, 'method', defaultMethod) + response = None + if method == 'spotPrivatePostSupplementUserInfoAccount': + response = self.spotPrivatePostSupplementUserInfoAccount() + elif method == 'spotPrivatePostUserInfo': + response = self.spotPrivatePostUserInfo() + else: + response = self.spotPrivatePostSupplementUserInfo() + # + # { + # "result": "true", + # "data": [ + # { + # "usableAmt": "14.36", + # "assetAmt": "14.36", + # "networkList": [ + # { + # "isDefault": False, + # "withdrawFeeRate": "", + # "name": "erc20", + # "withdrawMin": 30, + # "minLimit": 0.0001, + # "minDeposit": 20, + # "feeAssetCode": "usdt", + # "withdrawFee": "30", + # "type": 1, + # "coin": "usdt", + # "network": "eth" + # }, + # ... + # ], + # "freezeAmt": "0", + # "coin": "ada" + # } + # ], + # "code": 0 + # } + # + return self.parse_balance(response) + + def parse_trading_fee(self, fee: dict, market: Market = None) -> TradingFeeInterface: + # + # { + # "symbol":"skt_usdt", + # "makerCommission":"0.10", + # "takerCommission":"0.10" + # } + # + marketId = self.safe_string(fee, 'symbol') + symbol = self.safe_symbol(marketId) + return { + 'info': fee, + 'symbol': symbol, + 'maker': self.safe_number(fee, 'makerCommission'), + 'taker': self.safe_number(fee, 'takerCommission'), + 'percentage': None, + 'tierBased': None, + } + + def fetch_trading_fee(self, symbol: str, params={}) -> TradingFeeInterface: + """ + fetch the trading fees for a market + + https://www.lbank.com/en-US/docs/index.html#transaction-fee-rate-query + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `fee structure ` + """ + market = self.market(symbol) + result = self.fetch_trading_fees(self.extend(params, {'category': market['id']})) + return self.safe_dict(result, symbol) + + def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://www.lbank.com/en-US/docs/index.html#transaction-fee-rate-query + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + self.load_markets() + request: dict = {} + response = self.spotPrivatePostSupplementCustomerTradeFee(self.extend(request, params)) + fees = self.safe_value(response, 'data', []) + result: dict = {} + for i in range(0, len(fees)): + fee = self.parse_trading_fee(fees[i]) + symbol = fee['symbol'] + result[symbol] = fee + return result + + def create_market_buy_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market buy order by providing the symbol and cost + + https://www.lbank.com/en-US/docs/index.html#place-order + https://www.lbank.com/en-US/docs/index.html#place-an-order + + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' createMarketBuyOrderWithCost() supports spot orders only') + params['createMarketBuyOrderRequiresPrice'] = False + return self.create_order(symbol, 'market', 'buy', cost, None, params) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://www.lbank.com/en-US/docs/index.html#place-order + https://www.lbank.com/en-US/docs/index.html#place-an-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + clientOrderId = self.safe_string_2(params, 'custom_id', 'clientOrderId') + postOnly = self.safe_bool(params, 'postOnly', False) + timeInForce = self.safe_string_upper(params, 'timeInForce') + params = self.omit(params, ['custom_id', 'clientOrderId', 'timeInForce', 'postOnly']) + request: dict = { + 'symbol': market['id'], + } + ioc = (timeInForce == 'IOC') + fok = (timeInForce == 'FOK') + maker = (postOnly or (timeInForce == 'PO')) + if (type == 'market') and (ioc or fok or maker): + raise InvalidOrder(self.id + ' createOrder() does not allow market FOK, IOC, or postOnly orders. Only limit IOC, FOK, and postOnly orders are allowed') + if type == 'limit': + request['type'] = side + request['price'] = self.price_to_precision(symbol, price) + request['amount'] = self.amount_to_precision(symbol, amount) + if ioc: + request['type'] = side + '_' + 'ioc' + elif fok: + request['type'] = side + '_' + 'fok' + elif maker: + request['type'] = side + '_' + 'maker' + elif type == 'market': + if side == 'sell': + request['type'] = side + '_' + 'market' + request['amount'] = self.amount_to_precision(symbol, amount) + elif side == 'buy': + request['type'] = side + '_' + 'market' + quoteAmount = None + createMarketBuyOrderRequiresPrice = True + createMarketBuyOrderRequiresPrice, params = self.handle_option_and_params(params, 'createOrder', 'createMarketBuyOrderRequiresPrice', True) + cost = self.safe_number(params, 'cost') + params = self.omit(params, 'cost') + if cost is not None: + quoteAmount = self.cost_to_precision(symbol, cost) + elif createMarketBuyOrderRequiresPrice: + if price is None: + raise InvalidOrder(self.id + ' createOrder() requires the price argument for market buy orders to calculate the total cost to spend(amount * price), alternatively set the createMarketBuyOrderRequiresPrice option or param to False and pass the cost to spend in the amount argument') + else: + amountString = self.number_to_string(amount) + priceString = self.number_to_string(price) + costRequest = Precise.string_mul(amountString, priceString) + quoteAmount = self.cost_to_precision(symbol, costRequest) + else: + quoteAmount = self.cost_to_precision(symbol, amount) + # market buys require filling the price param instead of the amount param, for market buys the price is treated cost by lbank + request['price'] = quoteAmount + if clientOrderId is not None: + request['custom_id'] = clientOrderId + options = self.safe_value(self.options, 'createOrder', {}) + defaultMethod = self.safe_string(options, 'method', 'spotPrivatePostSupplementCreateOrder') + method = self.safe_string(params, 'method', defaultMethod) + params = self.omit(params, 'method') + response = None + if method == 'spotPrivatePostCreateOrder': + response = self.spotPrivatePostCreateOrder(self.extend(request, params)) + else: + response = self.spotPrivatePostSupplementCreateOrder(self.extend(request, params)) + # + # { + # "result":true, + # "data":{ + # "symbol":"doge_usdt", + # "order_id":"0cf8a3de-4597-4296-af45-be7abaa06b07" + # }, + # "error_code":0, + # "ts":1648162321043 + # } + # + result = self.safe_value(response, 'data', {}) + return self.safe_order({ + 'id': self.safe_string(result, 'order_id'), + 'info': result, + }, market) + + def parse_order_status(self, status: Str): + statuses: dict = { + '-1': 'canceled', # canceled + '0': 'open', # not traded + '1': 'open', # partial deal + '2': 'closed', # complete deal + '3': 'canceled', # filled partially and cancelled + '4': 'closed', # disposal processing + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # fetchOrderSupplement(private) + # + # { + # "cummulativeQuoteQty":0, + # "symbol":"doge_usdt", + # "executedQty":0, + # "orderId":"53d2d53e-70fb-4398-b722-f48571a5f61e", + # "origQty":1E+2, + # "price":0.05, + # "clientOrderId":null, + # "origQuoteOrderQty":5, + # "updateTime":1648163406000, + # "time":1648163139387, + # "type":"buy_maker", + # "status":-1 + # } + # + # + # fetchOrderDefault(private) + # + # { + # "symbol":"shib_usdt", + # "amount":1, + # "create_time":1649367863356, + # "price":0.0000246103, + # "avg_price":0.00002466180000000104, + # "type":"buy_market", + # "order_id":"abe8b92d-86d9-4d6d-b71e-d14f5fb53ddf", + # "custom_id": "007", # field only present if user creates it at order time + # "deal_amount":40548.54065802, + # "status":2 + # } + # + # fetchOpenOrders(private) + # + # { + # "cummulativeQuoteQty":0, + # "symbol":"doge_usdt", + # "executedQty":0, + # "orderId":"73878edf-008d-4e4c-8041-df1f1b2cd8bb", + # "origQty":100, + # "price":0.05, + # "origQuoteOrderQty":5, + # "updateTime":1648501762000, + # "time":1648501762353, + # "type":"buy", + # "status":0 + # } + # + # fetchOrders(private) + # + # { + # "cummulativeQuoteQty":0, + # "symbol":"doge_usdt", + # "executedQty":0, + # "orderId":"2cadc7cc-b5f6-486b-a5b4-d6ac49a9c186", + # "origQty":100, + # "price":0.05, + # "origQuoteOrderQty":5, + # "updateTime":1648501384000, + # "time":1648501363889, + # "type":"buy", + # "status":-1 + # } + # + # cancelOrder + # + # { + # "executedQty":0.0, + # "price":0.05, + # "origQty":100.0, + # "tradeType":"buy", + # "status":0 + # } + # + # cancelAllOrders + # + # { + # "executedQty":0.00000000000000000000, + # "orderId":"293ef71b-3e67-4962-af93-aa06990a045f", + # "price":0.05000000000000000000, + # "origQty":100.00000000000000000000, + # "tradeType":"buy", + # "status":0 + # } + # + id = self.safe_string_2(order, 'orderId', 'order_id') + clientOrderId = self.safe_string_2(order, 'clientOrderId', 'custom_id') + timestamp = self.safe_integer_2(order, 'time', 'create_time') + rawStatus = self.safe_string(order, 'status') + marketId = self.safe_string(order, 'symbol') + market = self.safe_market(marketId, market) + timeInForce = None + postOnly = False + type = 'limit' + rawType = self.safe_string_2(order, 'type', 'tradeType') # buy, sell, buy_market, sell_market, buy_maker,sell_maker,buy_ioc,sell_ioc, buy_fok, sell_fok + parts = rawType.split('_') + side = self.safe_string(parts, 0) + typePart = self.safe_string(parts, 1) # market, maker, ioc, fok or None(limit) + if typePart == 'market': + type = 'market' + if typePart == 'maker': + postOnly = True + timeInForce = 'PO' + if typePart == 'ioc': + timeInForce = 'IOC' + if typePart == 'fok': + timeInForce = 'FOK' + price = self.safe_string(order, 'price') + costString = self.safe_string(order, 'cummulativeQuoteQty') + amountString = None + if rawType != 'buy_market': + amountString = self.safe_string_2(order, 'origQty', 'amount') + filledString = self.safe_string_2(order, 'executedQty', 'deal_amount') + return self.safe_order({ + 'id': id, + 'clientOrderId': clientOrderId, + 'datetime': self.iso8601(timestamp), + 'timestamp': timestamp, + 'lastTradeTimestamp': None, + 'status': self.parse_order_status(rawStatus), + 'symbol': market['symbol'], + 'type': type, + 'timeInForce': timeInForce, + 'postOnly': postOnly, + 'side': side, + 'price': price, + 'triggerPrice': None, + 'cost': costString, + 'amount': amountString, + 'filled': filledString, + 'remaining': None, + 'trades': None, + 'fee': None, + 'info': order, + 'average': None, + }, market) + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://www.lbank.com/en-US/docs/index.html#query-order + https://www.lbank.com/en-US/docs/index.html#query-order-new + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + method = self.safe_string(params, 'method') + if method is None: + options = self.safe_value(self.options, 'fetchOrder', {}) + method = self.safe_string(options, 'method', 'fetchOrderSupplement') + if method == 'fetchOrderSupplement': + return self.fetch_order_supplement(id, symbol, params) + return self.fetch_order_default(id, symbol, params) + + def fetch_order_supplement(self, id: str, symbol: Str = None, params={}): + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrder() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'orderId': id, + } + response = self.spotPrivatePostSupplementOrdersInfo(self.extend(request, params)) + # + # { + # "result":true, + # "data":{ + # "cummulativeQuoteQty":0, + # "symbol":"doge_usdt", + # "executedQty":0, + # "orderId":"53d2d53e-70fb-4398-b722-f48571a5f61e", + # "origQty":1E+2, + # "price":0.05, + # "clientOrderId":null, + # "origQuoteOrderQty":5, + # "updateTime":1648163406000, + # "time":1648163139387, + # "type":"buy_maker", + # "status":-1 + # }, + # "error_code":0, + # "ts":1648164471827 + # } + # + result = self.safe_dict(response, 'data', {}) + return self.parse_order(result) + + def fetch_order_default(self, id: str, symbol: Str = None, params={}): + # Id can be a list of ids delimited by a comma + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrder() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'order_id': id, + } + response = self.spotPrivatePostOrdersInfo(self.extend(request, params)) + # + # { + # "result":true, + # "data":[ + # { + # "symbol":"doge_usdt", + # "amount":18, + # "create_time":1647455223186, + # "price":0, + # "avg_price":0.113344, + # "type":"sell_market", + # "order_id":"d4ca1ddd-40d9-42c1-9717-5de435865bec", + # "deal_amount":18, + # "status":2 + # } + # ], + # "error_code":0, + # "ts":1647455270776 + # } + # + result = self.safe_value(response, 'data', []) + numOrders = len(result) + if numOrders == 1: + return self.parse_order(result[0]) + else: + # parsedOrders = [] + # for i in range(0, numOrders): + # parsedOrder = self.parse_order(result[i]) + # parsedOrders.append(parsedOrder) + # } + # return parsedOrders + raise BadRequest(self.id + ' fetchOrder() can only fetch one order at a time') + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://www.lbank.com/en-US/docs/index.html#past-transaction-details + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trade structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMyTrades() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + since = self.safe_value(params, 'start_date', since) + params = self.omit(params, 'start_date') + request: dict = { + 'symbol': market['id'], + # 'start_date' Start time yyyy-mm-dd, the maximum is today, the default is yesterday + # 'end_date' Finish time yyyy-mm-dd, the maximum is today, the default is today + # 'The start': and end date of the query window is up to 2 days + # 'from' Initial transaction number inquiring + # 'direct' inquire direction,The default is the 'next' which is the positive sequence of dealing time,the 'prev' is inverted order of dealing time + # 'size' Query the number of defaults to 100 + } + if limit is not None: + request['size'] = limit + if since is not None: + request['start_date'] = self.ymd(since, '-') # max query 2 days ago + request['end_date'] = self.ymd(since + 86400000, '-') # will cover 2 days + response = self.spotPrivatePostTransactionHistory(self.extend(request, params)) + # + # { + # "result":true, + # "data":[ + # { + # "orderUuid":"38b4e7a4-14f6-45fd-aba1-1a37024124a0", + # "tradeFeeRate":0.0010000000, + # "dealTime":1648500944496, + # "dealQuantity":30.00000000000000000000, + # "tradeFee":0.00453300000000000000, + # "txUuid":"11f3850cc6214ea3b495adad3a032794", + # "dealPrice":0.15111300000000000000, + # "dealVolumePrice":4.53339000000000000000, + # "tradeType":"sell_market" + # } + # ], + # "error_code":0, + # "ts":1648509742164 + # } + # + trades = self.safe_list(response, 'data', []) + return self.parse_trades(trades, market, since, limit) + + def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://www.lbank.com/en-US/docs/index.html#query-all-orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + # default query is for canceled and completely filled orders + # does not return open orders unless specified explicitly + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrders() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + if limit is None: + limit = 100 + request: dict = { + 'symbol': market['id'], + 'current_page': 1, + 'page_length': limit, + # 'status' -1: Cancelled, 0: Unfilled, 1: Partially filled, 2: Completely filled, 3: Partially filled and cancelled, 4: Cancellation is being processed + } + response = self.spotPrivatePostSupplementOrdersInfoHistory(self.extend(request, params)) + # + # { + # "result":true, + # "data":{ + # "total":1, + # "page_length":100, + # "orders":[ + # { + # "cummulativeQuoteQty":0, + # "symbol":"doge_usdt", + # "executedQty":0, + # "orderId":"2cadc7cc-b5f6-486b-a5b4-d6ac49a9c186", + # "origQty":100, + # "price":0.05, + # "origQuoteOrderQty":5, + # "updateTime":1648501384000, + # "time":1648501363889, + # "type":"buy", + # "status":-1 + # }, ... + # ], + # "current_page":1 + # }, + # "error_code":0, + # "ts":1648505706348 + # } + # + result = self.safe_value(response, 'data', {}) + orders = self.safe_list(result, 'orders', []) + return self.parse_orders(orders, market, since, limit) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://www.lbank.com/en-US/docs/index.html#current-pending-order + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOpenOrders() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + if limit is None: + limit = 100 + request: dict = { + 'symbol': market['id'], + 'current_page': 1, + 'page_length': limit, + } + response = self.spotPrivatePostSupplementOrdersInfoNoDeal(self.extend(request, params)) + # + # { + # "result":true, + # "data":{ + # "total":1, + # "page_length":100, + # "orders":[ + # { + # "cummulativeQuoteQty":0, + # "symbol":"doge_usdt", + # "executedQty":0, + # "orderId":"73878edf-008d-4e4c-8041-df1f1b2cd8bb", + # "origQty":100, + # "price":0.05, + # "origQuoteOrderQty":5, + # "updateTime":1648501762000, + # "time":1648501762353, + # "type":"buy", + # "status":0 + # }, ... + # ], + # "current_page":1 + # }, + # "error_code":0, + # "ts":1648506110196 + # } + # + result = self.safe_value(response, 'data', {}) + orders = self.safe_list(result, 'orders', []) + return self.parse_orders(orders, market, since, limit) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://www.lbank.com/en-US/docs/index.html#cancel-order-new + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + self.load_markets() + clientOrderId = self.safe_string_2(params, 'origClientOrderId', 'clientOrderId') + params = self.omit(params, ['origClientOrderId', 'clientOrderId']) + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'orderId': id, + } + if clientOrderId is not None: + request['origClientOrderId'] = clientOrderId + response = self.spotPrivatePostSupplementCancelOrder(self.extend(request, params)) + # + # { + # "result":true, + # "data":{ + # "executedQty":0.0, + # "price":0.05, + # "origQty":100.0, + # "tradeType":"buy", + # "status":0 + # }, + # "error_code":0, + # "ts":1648501286196 + # } + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data) + + def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders in a market + + https://www.lbank.com/en-US/docs/index.html#cancel-all-pending-orders-for-a-single-trading-pair + + :param str symbol: unified market symbol of the market to cancel orders in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelAllOrders() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = self.spotPrivatePostSupplementCancelOrderBySymbol(self.extend(request, params)) + # + # { + # "result":"true", + # "data":[ + # { + # "executedQty":0.00000000000000000000, + # "orderId":"293ef71b-3e67-4962-af93-aa06990a045f", + # "price":0.05000000000000000000, + # "origQty":100.00000000000000000000, + # "tradeType":"buy", + # "status":0 + # }, + # ], + # "error_code":0, + # "ts":1648506641469 + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_orders(data) + + def get_network_code_for_currency(self, currencyCode, params): + defaultNetworks = self.safe_value(self.options, 'defaultNetworks') + defaultNetwork = self.safe_string_upper(defaultNetworks, currencyCode) + networks = self.safe_value(self.options, 'networks', {}) + network = self.safe_string_upper(params, 'network', defaultNetwork) # self line allows the user to specify either ERC20 or ETH + network = self.safe_string(networks, network, network) # handle ERC20>ETH alias + return network + + def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://www.lbank.com/en-US/docs/index.html#get-deposit-address + https://www.lbank.com/en-US/docs/index.html#the-user-obtains-the-deposit-address + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + self.load_markets() + options = self.safe_value(self.options, 'fetchDepositAddress', {}) + defaultMethod = self.safe_string(options, 'method', 'fetchDepositAddressDefault') + method = self.safe_string(params, 'method', defaultMethod) + params = self.omit(params, 'method') + response = None + if method == 'fetchDepositAddressSupplement': + response = self.fetch_deposit_address_supplement(code, params) + else: + response = self.fetch_deposit_address_default(code, params) + return response + + def fetch_deposit_address_default(self, code: str, params={}) -> DepositAddress: + self.load_markets() + currency = self.currency(code) + request: dict = { + 'assetCode': currency['id'], + } + network = self.get_network_code_for_currency(code, params) + if network is not None: + request['netWork'] = network # ... yes, really lol + params = self.omit(params, 'network') + response = self.spotPrivatePostGetDepositAddress(self.extend(request, params)) + # + # { + # "result":true, + # "data":{ + # "assetCode":"usdt", + # "address":"0xc85689d37ca650bf2f2161364cdedee21eb6ca53", + # "memo":null, + # "netWork":"bep20(bsc)" + # }, + # "error_code":0, + # "ts":1648075865103 + # } + # + result = self.safe_value(response, 'data') + address = self.safe_string(result, 'address') + tag = self.safe_string(result, 'memo') + return { + 'info': response, + 'currency': code, + 'network': self.network_id_to_code(self.safe_string(result, 'netWork')), + 'address': address, + 'tag': tag, + } + + def fetch_deposit_address_supplement(self, code: str, params={}) -> DepositAddress: + # returns the address for whatever the default network is... + self.load_markets() + currency = self.currency(code) + request: dict = { + 'coin': currency['id'], + } + networks = self.safe_value(self.options, 'networks') + network = self.safe_string_upper(params, 'network') + network = self.safe_string(networks, network, network) + if network is not None: + request['networkName'] = network + params = self.omit(params, 'network') + response = self.spotPrivatePostSupplementGetDepositAddress(self.extend(request, params)) + # + # { + # "result":true, + # "data":{ + # "address":"TDxtabCC8iQwaxUUrPcE4WL2jArGAfvQ5A", + # "memo":null, + # "coin":"usdt" + # }, + # "error_code":0, + # "ts":1648073818880 + # } + # + result = self.safe_value(response, 'data') + address = self.safe_string(result, 'address') + tag = self.safe_string(result, 'memo') + return { + 'info': response, + 'currency': code, + 'network': None, + 'address': address, + 'tag': tag, + } + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://www.lbank.com/en-US/docs/index.html#withdrawal + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.check_address(address) + self.load_markets() + fee = self.safe_string(params, 'fee') + params = self.omit(params, 'fee') + # The relevant coin network fee can be found by calling fetchDepositWithdrawFees(), note: if no network param is supplied then the default network will be used, self can also be found in fetchDepositWithdrawFees(). + self.check_required_argument('withdraw', fee, 'fee') + currency = self.currency(code) + request: dict = { + 'address': address, + 'coin': currency['id'], + 'amount': amount, + 'fee': fee, # the correct coin-network fee must be supplied, which can be found by calling fetchDepositWithdrawFees(private) + # 'networkName': defaults to the defaultNetwork of the coin which can be found in the /supplement/user_info endpoint + # 'memo': memo: memo word of bts and dct + # 'mark': Withdrawal Notes + # 'name': Remarks of the address. After hasattr(self, filling) parameter, it will be added to the withdrawal address book of the currency. + # 'withdrawOrderId': withdrawOrderId + # 'type': type=1 is for intra-site transfer + } + if tag is not None: + request['memo'] = tag + network = self.safe_string_upper_2(params, 'network', 'networkName') + params = self.omit(params, ['network', 'networkName']) + networks = self.safe_value(self.options, 'networks') + networkId = self.safe_string(networks, network, network) + if networkId is not None: + request['networkName'] = networkId + response = self.spotPrivatePostSupplementWithdraw(self.extend(request, params)) + # + # { + # "result":true, + # "data": { + # "fee":10.00000000000000000000, + # "withdrawId":1900376 + # }, + # "error_code":0, + # "ts":1648992501414 + # } + # + result = self.safe_value(response, 'data', {}) + return { + 'info': result, + 'id': self.safe_string(result, 'withdrawId'), + } + + def parse_transaction_status(self, status, type): + statuses: dict = { + 'deposit': { + '1': 'pending', + '2': 'ok', + '3': 'failed', + '4': 'canceled', + '5': 'transfer', + }, + 'withdrawal': { + '1': 'pending', + '2': 'canceled', + '3': 'failed', + '4': 'ok', + }, + } + return self.safe_string(self.safe_value(statuses, type, {}), status, status) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fetchDeposits(private) + # + # { + # "insertTime":1649012310000, + # "amount":9.00000000000000000000, + # "address":"TYASr5UV6HEcXatwdFQfmLVUqQQQMUxHLS", + # "networkName":"trc20", + # "txId":"081e4e9351dd0274922168da5f2d14ea6c495b1c3b440244f4a6dd9fe196bf2b", + # "coin":"usdt", + # "status":"2" + # } + # + # + # fetchWithdrawals(private) + # + # { + # "amount":2.00000000000000000000, + # "address":"TBjrW5JHDyPZjFc5nrRMhRWUDaJmhGhmD6", + # "fee":1.00000000000000000000, + # "networkName":"trc20", + # "coid":"usdt", + # "transferType":"数字资产提现", + # "txId":"47eeee2763ad49b8817524dacfa7d092fb58f8b0ab7e5d25473314df1a793c3d", + # "id":1902194, + # "applyTime":1649014002000, + # "status":"4" + # } + # + id = self.safe_string(transaction, 'id') + type = None + if id is None: + type = 'deposit' + else: + type = 'withdrawal' + txid = self.safe_string(transaction, 'txId') + timestamp = self.safe_integer_2(transaction, 'insertTime', 'applyTime') + address = self.safe_string(transaction, 'address') + addressFrom = None + addressTo = None + if type == 'deposit': + addressFrom = address + else: + addressTo = address + amount = self.safe_number(transaction, 'amount') + currencyId = self.safe_string_2(transaction, 'coin', 'coid') + code = self.safe_currency_code(currencyId, currency) + status = self.parse_transaction_status(self.safe_string(transaction, 'status'), type) + fee = None + feeCost = self.safe_number(transaction, 'fee') + if feeCost is not None: + fee = { + 'cost': feeCost, + 'currency': code, + } + return { + 'info': transaction, + 'id': id, + 'txid': txid, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': self.network_id_to_code(self.safe_string(transaction, 'networkName')), + 'address': address, + 'addressTo': addressTo, + 'addressFrom': addressFrom, + 'tag': None, + 'tagTo': None, + 'tagFrom': None, + 'type': type, + 'amount': amount, + 'currency': code, + 'status': status, + 'updated': None, + 'comment': None, + 'internal': (status == 'transfer'), + 'fee': fee, + } + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://www.lbank.com/en-US/docs/index.html#get-recharge-history + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + request: dict = { + # 'status': Recharge status: ("1","Applying"),("2","Recharge successful"),("3","Recharge failed"),("4","Already Cancel"),("5", "Transfer") + # 'endTime': end time, timestamp in milliseconds, default now + } + currency = None + if code is not None: + currency = self.currency(code) + request['coin'] = currency['id'] + if since is not None: + request['startTime'] = since + response = self.spotPrivatePostSupplementDepositHistory(self.extend(request, params)) + # + # { + # "result":true, + # "data": { + # "total":1, + # "depositOrders": [ + # { + # "insertTime":1649012310000, + # "amount":9.00000000000000000000, + # "address":"TYASr5UV6HEcXatwdFQfmLVUqQQQMUxHLS", + # "networkName":"trc20", + # "txId":"081e4e9351dd0274922168da5f2d14ea6c495b1c3b440244f4a6dd9fe196bf2b", + # "coin":"usdt", + # "status":"2" + # }, + # ], + # "page_length":20, + # "current_page":1 + # }, + # "error_code":0, + # "ts":1649719721758 + # } + # + data = self.safe_value(response, 'data', {}) + deposits = self.safe_list(data, 'depositOrders', []) + return self.parse_transactions(deposits, currency, since, limit) + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://www.lbank.com/en-US/docs/index.html#get-withdrawal-history + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + request: dict = { + # 'status': Recharge status: ("1","Applying"),("2","Recharge successful"),("3","Recharge failed"),("4","Already Cancel"),("5", "Transfer") + # 'endTime': end time, timestamp in milliseconds, default now + # 'withdrawOrderId': Custom withdrawal id + } + currency = None + if code is not None: + currency = self.currency(code) + request['coin'] = currency['id'] + if since is not None: + request['startTime'] = since + response = self.spotPrivatePostSupplementWithdraws(self.extend(request, params)) + # + # { + # "result":true, + # "data": { + # "total":1, + # "withdraws": [ + # { + # "amount":2.00000000000000000000, + # "address":"TBjrW5JHDyPZjFc5nrRMhRWUDaJmhGhmD6", + # "fee":1.00000000000000000000, + # "networkName":"trc20", + # "coid":"usdt", + # "transferType":"数字资产提现", + # "txId":"47eeee2763ad49b8817524dacfa7d092fb58f8b0ab7e5d25473314df1a793c3d", + # "id":1902194, + # "applyTime":1649014002000, + # "status":"4" + # }, + # ], + # "page_length":20, + # "current_page":1 + # }, + # "error_code":0, + # "ts":1649720362362 + # } + # + data = self.safe_value(response, 'data', {}) + withdraws = self.safe_list(data, 'withdraws', []) + return self.parse_transactions(withdraws, currency, since, limit) + + def fetch_transaction_fees(self, codes: Strings = None, params={}): + """ + @deprecated + please use fetchDepositWithdrawFees instead + :param str[]|None codes: not used by lbank fetchTransactionFees() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `fee structures ` + """ + # private only returns information for currencies with non-zero balance + self.load_markets() + isAuthorized = self.check_required_credentials(False) + result = None + if isAuthorized is True: + options = self.safe_value(self.options, 'fetchTransactionFees', {}) + defaultMethod = self.safe_string(options, 'method', 'fetchPrivateTransactionFees') + method = self.safe_string(params, 'method', defaultMethod) + params = self.omit(params, 'method') + if method == 'fetchPublicTransactionFees': + result = self.fetch_public_transaction_fees(params) + else: + result = self.fetch_private_transaction_fees(params) + else: + result = self.fetch_public_transaction_fees(params) + return result + + def fetch_private_transaction_fees(self, params={}): + # complete response + # incl. for coins which None in public method + self.load_markets() + response = self.spotPrivatePostSupplementUserInfo() + # + # { + # "result": "true", + # "data": [ + # { + # "usableAmt": "14.36", + # "assetAmt": "14.36", + # "networkList": [ + # { + # "isDefault": False, + # "withdrawFeeRate": "", + # "name": "erc20", + # "withdrawMin": 30, + # "minLimit": 0.0001, + # "minDeposit": 20, + # "feeAssetCode": "usdt", + # "withdrawFee": "30", + # "type": 1, + # "coin": "usdt", + # "network": "eth" + # }, + # ... + # ], + # "freezeAmt": "0", + # "coin": "ada" + # } + # ], + # "code": 0 + # } + # + result = self.safe_value(response, 'data', []) + withdrawFees: dict = {} + for i in range(0, len(result)): + entry = result[i] + currencyId = self.safe_string(entry, 'coin') + code = self.safe_currency_code(currencyId) + networkList = self.safe_value(entry, 'networkList', []) + withdrawFees[code] = {} + for j in range(0, len(networkList)): + networkEntry = networkList[j] + fee = self.safe_number(networkEntry, 'withdrawFee') + if fee is not None: + networkCode = self.network_id_to_code(self.safe_string(networkEntry, 'name')) + withdrawFees[code][networkCode] = fee + return { + 'withdraw': withdrawFees, + 'deposit': {}, + 'info': response, + } + + def fetch_public_transaction_fees(self, params={}): + # extremely incomplete response + # vast majority fees None + self.load_markets() + code = self.safe_string_2(params, 'coin', 'assetCode') + params = self.omit(params, ['coin', 'assetCode']) + request: dict = {} + if code is not None: + currency = self.currency(code) + request['assetCode'] = currency['id'] + response = self.spotPublicGetWithdrawConfigs(self.extend(request, params)) + # + # { + # "result": "true", + # "data": [ + # { + # "amountScale": "4", + # "chain": "heco", + # "assetCode": "lbk", + # "min": "200", + # "transferAmtScale": "4", + # "canWithDraw": True, + # "fee": "100", + # "minTransfer": "0.0001", + # "type": "1" + # }, + # ... + # ], + # "error_code": "0", + # "ts": "1663364435973" + # } + # + result = self.safe_value(response, 'data', []) + withdrawFees: dict = {} + for i in range(0, len(result)): + item = result[i] + canWithdraw = self.safe_value(item, 'canWithDraw') + if canWithdraw == 'true': + currencyId = self.safe_string(item, 'assetCode') + codeInner = self.safe_currency_code(currencyId) + network = self.network_id_to_code(self.safe_string(item, 'chain')) + if network is None: + network = codeInner + fee = self.safe_string(item, 'fee') + if withdrawFees[codeInner] is None: + withdrawFees[codeInner] = {} + withdrawFees[codeInner][network] = self.parse_number(fee) + return { + 'withdraw': withdrawFees, + 'deposit': {}, + 'info': response, + } + + def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}): + """ + when using private endpoint, only returns information for currencies with non-zero balance, use public method by specifying self.options['fetchDepositWithdrawFees']['method'] = 'fetchPublicDepositWithdrawFees' + + https://www.lbank.com/en-US/docs/index.html#get-all-coins-information + https://www.lbank.com/en-US/docs/index.html#withdrawal-configurations + + :param str[] [codes]: array of unified currency codes + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `fee structures ` + """ + self.load_markets() + isAuthorized = self.check_required_credentials(False) + response = None + if isAuthorized is True: + options = self.safe_value(self.options, 'fetchDepositWithdrawFees', {}) + defaultMethod = self.safe_string(options, 'method', 'fetchPrivateDepositWithdrawFees') + method = self.safe_string(params, 'method', defaultMethod) + params = self.omit(params, 'method') + if method == 'fetchPublicDepositWithdrawFees': + response = self.fetch_public_deposit_withdraw_fees(codes, params) + else: + response = self.fetch_private_deposit_withdraw_fees(codes, params) + else: + response = self.fetch_public_deposit_withdraw_fees(codes, params) + return response + + def fetch_private_deposit_withdraw_fees(self, codes=None, params={}): + # complete response + # incl. for coins which None in public method + self.load_markets() + response = self.spotPrivatePostSupplementUserInfo(params) + # + # { + # "result": "true", + # "data": [ + # { + # "usableAmt": "14.36", + # "assetAmt": "14.36", + # "networkList": [ + # { + # "isDefault": False, + # "withdrawFeeRate": "", + # "name": "erc20", + # "withdrawMin": 30, + # "minLimit": 0.0001, + # "minDeposit": 20, + # "feeAssetCode": "usdt", + # "withdrawFee": "30", + # "type": 1, + # "coin": "usdt", + # "network": "eth" + # }, + # ... + # ], + # "freezeAmt": "0", + # "coin": "ada" + # } + # ], + # "code": 0 + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_deposit_withdraw_fees(data, codes, 'coin') + + def fetch_public_deposit_withdraw_fees(self, codes=None, params={}): + # extremely incomplete response + # vast majority fees None + self.load_markets() + request: dict = {} + response = self.spotPublicGetWithdrawConfigs(self.extend(request, params)) + # + # { + # "result": "true", + # "data": [ + # { + # "amountScale": "4", + # "chain": "heco", + # "assetCode": "lbk", + # "min": "200", + # "transferAmtScale": "4", + # "canWithDraw": True, + # "fee": "100", + # "minTransfer": "0.0001", + # "type": "1" + # }, + # ... + # ], + # "error_code": "0", + # "ts": "1663364435973" + # } + # + data = self.safe_value(response, 'data', []) + return self.parse_public_deposit_withdraw_fees(data, codes) + + def parse_public_deposit_withdraw_fees(self, response, codes=None): + # + # [ + # { + # "amountScale": "4", + # "chain": "heco", + # "assetCode": "lbk", + # "min": "200", + # "transferAmtScale": "4", + # "canWithDraw": True, + # "fee": "100", + # "minTransfer": "0.0001", + # "type": "1" + # }, + # ... + # ] + # + result: dict = {} + for i in range(0, len(response)): + fee = response[i] + canWithdraw = self.safe_value(fee, 'canWithDraw') + if canWithdraw is True: + currencyId = self.safe_string(fee, 'assetCode') + code = self.safe_currency_code(currencyId) + if codes is None or self.in_array(code, codes): + withdrawFee = self.safe_number(fee, 'fee') + if withdrawFee is not None: + resultValue = self.safe_value(result, code) + if resultValue is None: + result[code] = self.deposit_withdraw_fee([fee]) + else: + resultCodeInfo = result[code]['info'] + resultCodeInfo.append(fee) + networkCode = self.network_id_to_code(self.safe_string(fee, 'chain')) + if networkCode is not None: + result[code]['networks'][networkCode] = { + 'withdraw': { + 'fee': withdrawFee, + 'percentage': None, + }, + 'deposit': { + 'fee': None, + 'percentage': None, + }, + } + else: + result[code]['withdraw'] = { + 'fee': withdrawFee, + 'percentage': None, + } + return result + + def parse_deposit_withdraw_fee(self, fee, currency: Currency = None): + # + # * only used for fetchPrivateDepositWithdrawFees + # + # { + # "usableAmt": "14.36", + # "assetAmt": "14.36", + # "networkList": [ + # { + # "isDefault": False, + # "withdrawFeeRate": "", + # "name": "erc20", + # "withdrawMin": 30, + # "minLimit": 0.0001, + # "minDeposit": 20, + # "feeAssetCode": "usdt", + # "withdrawFee": "30", + # "type": 1, + # "coin": "usdt", + # "network": "eth" + # }, + # ... + # ], + # "freezeAmt": "0", + # "coin": "ada" + # } + # + result = self.deposit_withdraw_fee(fee) + networkList = self.safe_value(fee, 'networkList', []) + for j in range(0, len(networkList)): + networkEntry = networkList[j] + networkCode = self.network_id_to_code(self.safe_string(networkEntry, 'name')) + withdrawFee = self.safe_number(networkEntry, 'withdrawFee') + isDefault = self.safe_value(networkEntry, 'isDefault') + if withdrawFee is not None: + if isDefault: + result['withdraw'] = { + 'fee': withdrawFee, + 'percentage': None, + } + result['networks'][networkCode] = { + 'withdraw': { + 'fee': withdrawFee, + 'percentage': None, + }, + 'deposit': { + 'fee': None, + 'percentage': None, + }, + } + return result + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + query = self.omit(params, self.extract_params(path)) + url = self.urls['api']['rest'] + '/' + self.version + '/' + self.implode_params(path, params) + # Every spot endpoint ends with ".do" + if api[0] == 'spot': + url += '.do' + else: + url = self.urls['api']['contract'] + '/' + self.implode_params(path, params) + if api[1] == 'public': + if query: + url += '?' + self.urlencode(self.keysort(query)) + else: + self.check_required_credentials() + timestamp = str(self.milliseconds()) + echostr = self.uuid22() + self.uuid16() + query = self.extend({ + 'api_key': self.apiKey, + }, query) + signatureMethod = None + if len(self.secret) > 32: + signatureMethod = 'RSA' + else: + signatureMethod = 'HmacSHA256' + auth = self.rawencode(self.keysort(self.extend({ + 'echostr': echostr, + 'signature_method': signatureMethod, + 'timestamp': timestamp, + }, query))) + encoded = self.encode(auth) + hash = self.hash(encoded, 'md5') + uppercaseHash = hash.upper() + sign = None + if signatureMethod == 'RSA': + cacheSecretAsPem = self.safe_bool(self.options, 'cacheSecretAsPem', True) + pem = None + if cacheSecretAsPem: + pem = self.safe_value(self.options, 'pem') + if pem is None: + pem = self.convert_secret_to_pem(self.encode(self.secret)) + self.options['pem'] = pem + else: + pem = self.convert_secret_to_pem(self.encode(self.secret)) + sign = self.rsa(uppercaseHash, pem, 'sha256') + elif signatureMethod == 'HmacSHA256': + sign = self.hmac(self.encode(uppercaseHash), self.encode(self.secret), hashlib.sha256) + query['sign'] = sign + body = self.urlencode(self.keysort(query)) + headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + 'timestamp': timestamp, + 'signature_method': signatureMethod, + 'echostr': echostr, + } + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def convert_secret_to_pem(self, secret): + lineLength = 64 + secretLength = len(secret) - 0 + numLines = self.parse_to_int(secretLength / lineLength) + numLines = self.sum(numLines, 1) + pem = "-----BEGIN PRIVATE KEY-----\n" # eslint-disable-line + for i in range(0, numLines): + start = i * lineLength + end = self.sum(start, lineLength) + pem += self.secret[start:end] + "\n" # eslint-disable-line + return pem + '-----END PRIVATE KEY-----' + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None + success = self.safe_value(response, 'result') + if success == 'false' or not success: + errorCode = self.safe_string(response, 'error_code') + message = self.safe_string({ + '10000': 'Internal error', + '10001': 'The required parameters can not be empty', + '10002': 'Validation failed', + '10003': 'Invalid parameter', + '10004': 'Request too frequent', + '10005': 'Secret key does not exist', + '10006': 'User does not exist', + '10007': 'Invalid signature', + '10008': 'Invalid Trading Pair', + '10009': 'Price and/or Amount are required for limit order', + '10010': 'Price and/or Amount must be less than minimum requirement', + # '10011': 'Market orders can not be missing the amount of the order', + # '10012': 'market sell orders can not be missing orders', + '10013': 'The amount is too small', + '10014': 'Insufficient amount of money in the account', + '10015': 'Invalid order type', + '10016': 'Insufficient account balance', + '10017': 'Server Error', + '10018': 'Page size should be between 1 and 50', + '10019': 'Cancel NO more than 3 orders in one request', + '10020': 'Volume < 0.001', + '10021': 'Price < 0.01', + '10022': 'Invalid authorization', + '10023': 'Market Order is not supported yet', + '10024': 'User cannot trade on self pair', + '10025': 'Order has been filled', + '10026': 'Order has been cancelld', + '10027': 'Order is cancelling', + '10028': 'Wrong query time', + '10029': 'from is not in the query time', + '10030': 'from do not match the transaction type of inqury', + '10031': 'echostr length must be valid and length must be from 30 to 40', + '10033': 'Failed to create order', + '10036': 'customID duplicated', + '10100': 'Has no privilege to withdraw', + '10101': 'Invalid fee rate to withdraw', + '10102': 'Too little to withdraw', + '10103': 'Exceed daily limitation of withdraw', + '10104': 'Cancel was rejected', + '10105': 'Request has been cancelled', + '10106': 'None trade time', + '10107': 'Start price exception', + '10108': 'can not create order', + '10109': 'wallet address is not mapping', + '10110': 'transfer fee is not mapping', + '10111': 'mount > 0', + '10112': 'fee is too lower', + '10113': 'transfer fee is 0', + '10600': 'intercepted by replay attacks filter, check timestamp', + '10601': 'Interface closed unavailable', + '10701': 'invalid asset code', + '10702': 'not allowed deposit', + }, errorCode, self.json(response)) + ErrorClass = self.safe_value({ + '10001': BadRequest, + '10002': AuthenticationError, + '10003': BadRequest, + '10004': RateLimitExceeded, + '10005': AuthenticationError, + '10006': AuthenticationError, + '10007': AuthenticationError, + '10008': BadSymbol, + '10009': InvalidOrder, + '10010': InvalidOrder, + '10013': InvalidOrder, + '10014': InsufficientFunds, + '10015': InvalidOrder, + '10016': InsufficientFunds, + '10017': ExchangeError, + '10018': BadRequest, + '10019': BadRequest, + '10020': BadRequest, + '10021': InvalidOrder, + '10022': PermissionDenied, # 'Invalid authorization', + '10023': InvalidOrder, # 'Market Order is not supported yet', + '10024': PermissionDenied, # 'User cannot trade on self pair', + '10025': InvalidOrder, # 'Order has been filled', + '10026': InvalidOrder, # 'Order has been cancelled', + '10027': InvalidOrder, # 'Order is cancelling', + '10028': BadRequest, # 'Wrong query time', + '10029': BadRequest, # 'from is not in the query time', + '10030': BadRequest, # 'from do not match the transaction type of inqury', + '10031': InvalidNonce, # 'echostr length must be valid and length must be from 30 to 40', + '10033': ExchangeError, # 'Failed to create order', + '10036': DuplicateOrderId, # 'customID duplicated', + '10100': PermissionDenied, # 'Has no privilege to withdraw', + '10101': BadRequest, # 'Invalid fee rate to withdraw', + '10102': InsufficientFunds, # 'Too little to withdraw', + '10103': ExchangeError, # 'Exceed daily limitation of withdraw', + '10104': ExchangeError, # 'Cancel was rejected', + '10105': ExchangeError, # 'Request has been cancelled', + '10106': BadRequest, # 'None trade time', + '10107': BadRequest, # 'Start price exception', + '10108': ExchangeError, # 'can not create order', + '10109': InvalidAddress, # 'wallet address is not mapping', + '10110': ExchangeError, # 'transfer fee is not mapping', + '10111': BadRequest, # 'mount > 0', + '10112': BadRequest, # 'fee is too lower', + '10113': BadRequest, # 'transfer fee is 0', + '10600': BadRequest, # 'intercepted by replay attacks filter, check timestamp', + '10601': ExchangeError, # 'Interface closed unavailable', + '10701': BadSymbol, # 'invalid asset code', + '10702': PermissionDenied, # 'not allowed deposit', + }, errorCode, ExchangeError) + raise ErrorClass(message) + return None diff --git a/ccxt/luno.py b/ccxt/luno.py new file mode 100644 index 0000000..a38968b --- /dev/null +++ b/ccxt/luno.py @@ -0,0 +1,1417 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.luno import ImplicitAPI +from ccxt.base.types import Account, Any, Balances, Currencies, Currency, DepositAddress, Int, LedgerEntry, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, TradingFeeInterface +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class luno(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(luno, self).describe(), { + 'id': 'luno', + 'name': 'luno', + 'countries': ['GB', 'SG', 'ZA'], + # 300 calls per minute = 5 calls per second = 1000ms / 5 = 200ms between requests + 'rateLimit': 200, + 'version': '1', + 'pro': True, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelOrder': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createDepositAddress': True, + 'createOrder': True, + 'createReduceOnlyOrder': False, + 'fetchAccounts': True, + 'fetchAllGreeks': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': True, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLedger': True, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrice': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchPosition': False, + 'fetchPositionForSymbolWs': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsForSymbolWs': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTrades': True, + 'fetchTradingFee': True, + 'fetchTradingFees': False, + 'fetchUnderlyingAssets': False, + 'fetchVolatilityHistory': False, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + }, + 'urls': { + 'referral': 'https://www.luno.com/invite/44893A', + 'logo': 'https://user-images.githubusercontent.com/1294454/27766607-8c1a69d8-5ede-11e7-930c-540b5eb9be24.jpg', + 'api': { + 'public': 'https://api.luno.com/api', + 'private': 'https://api.luno.com/api', + 'exchange': 'https://api.luno.com/api/exchange', + 'exchangePrivate': 'https://api.luno.com/api/exchange', + }, + 'www': 'https://www.luno.com', + 'doc': [ + 'https://www.luno.com/en/api', + 'https://npmjs.org/package/bitx', + 'https://github.com/bausmeier/node-bitx', + ], + }, + 'api': { + 'exchange': { + 'get': { + 'markets': 1, + }, + }, + 'exchangePrivate': { + 'get': { + 'candles': 1, + }, + }, + 'public': { + 'get': { + 'orderbook': 1, + 'orderbook_top': 1, + 'ticker': 1, + 'tickers': 1, + 'trades': 1, + }, + }, + 'private': { + 'get': { + 'accounts/{id}/pending': 1, + 'accounts/{id}/transactions': 1, + 'balance': 1, + 'beneficiaries': 1, + 'send/networks': 1, + 'fee_info': 1, + 'funding_address': 1, + 'listorders': 1, + 'listtrades': 1, + 'send_fee': 1, + 'orders/{id}': 1, + 'withdrawals': 1, + 'withdrawals/{id}': 1, + 'transfers': 1, + # GET /api/exchange/1/move + # GET /api/exchange/1/move/list_moves + # GET /api/exchange/1/candles + # GET /api/exchange/1/transfers + # GET /api/exchange/2/listorders + # GET /api/exchange/2/orders/{id} + # GET /api/exchange/3/order + }, + 'post': { + 'accounts': 1, + 'address/validate': 1, + 'postorder': 1, + 'marketorder': 1, + 'stoporder': 1, + 'funding_address': 1, + 'withdrawals': 1, + 'send': 1, + 'oauth2/grant': 1, + 'beneficiaries': 1, + # POST /api/exchange/1/move + }, + 'put': { + 'accounts/{id}/name': 1, + }, + 'delete': { + 'withdrawals/{id}': 1, + 'beneficiaries/{id}': 1, + }, + }, + }, + 'timeframes': { + '1m': 60, + '5m': 300, + '15m': 900, + '30m': 1800, + '1h': 3600, + '3h': 10800, + '4h': 14400, + '1d': 86400, + '3d': 259200, + '1w': 604800, + }, + 'fees': { + 'trading': { + 'tierBased': True, # based on volume from your primary currency(not the same for everyone) + 'percentage': True, + 'taker': self.parse_number('0.001'), + 'maker': self.parse_number('0'), + }, + }, + 'precisionMode': TICK_SIZE, + 'features': { + 'spot': { + 'sandbox': False, + 'fetchCurrencies': { + 'private': True, + }, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, # todo + 'triggerPriceType': None, + 'triggerDirection': True, # todo + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 100000, # todo + 'untilDays': 100000, # todo + 'symbolRequired': True, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 1000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 100000, + 'untilDays': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 100000, + 'daysBackCanceled': 1, + 'untilDays': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': None, + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + }) + + def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + if not self.check_required_credentials(False): + return {} + response = self.privateGetSendNetworks(params) + # + # { + # "networks": [ + # { + # "id": 0, + # "name": "Ethereum", + # "native_currency": "ETH" + # }, + # ... + # ] + # } + # + currenciesData = self.safe_list(response, 'data', []) + result: dict = {} + for i in range(0, len(currenciesData)): + networkEntry = currenciesData[i] + id = self.safe_string(networkEntry, 'native_currency') + code = self.safe_currency_code(id) + if not (code in result): + result[code] = { + 'id': id, + 'code': code, + 'precision': None, + 'type': None, + 'name': None, + 'active': None, + 'deposit': None, + 'withdraw': None, + 'fee': None, + 'limits': { + 'withdraw': { + 'min': None, + 'max': None, + }, + 'deposit': { + 'min': None, + 'max': None, + }, + }, + 'networks': {}, + 'info': {}, + } + networkId = self.safe_string(networkEntry, 'name') + networkCode = self.network_id_to_code(networkId) + result[code]['networks'][networkCode] = { + 'id': networkId, + 'network': networkCode, + 'limits': { + 'withdraw': { + 'min': None, + 'max': None, + }, + 'deposit': { + 'min': None, + 'max': None, + }, + }, + 'active': None, + 'deposit': None, + 'withdraw': None, + 'fee': None, + 'precision': None, + 'info': networkEntry, + } + # add entry in info + info = self.safe_list(result[code], 'info', []) + info.append(networkEntry) + result[code]['info'] = info + # only after all entries are formed in currencies, restructure each entry + allKeys = list(result.keys()) + for i in range(0, len(allKeys)): + code = allKeys[i] + result[code] = self.safe_currency_structure(result[code]) # self is needed after adding network entry + return result + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for luno + + https://www.luno.com/en/developers/api#tag/Market/operation/Markets + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = self.exchangeGetMarkets(params) + # + # { + # "markets":[ + # { + # "market_id":"BCHXBT", + # "trading_status":"ACTIVE", + # "base_currency":"BCH", + # "counter_currency":"XBT", + # "min_volume":"0.01", + # "max_volume":"100.00", + # "volume_scale":2, + # "min_price":"0.0001", + # "max_price":"1.00", + # "price_scale":6, + # "fee_scale":8, + # }, + # ] + # } + # + result = [] + markets = self.safe_value(response, 'markets', []) + for i in range(0, len(markets)): + market = markets[i] + id = self.safe_string(market, 'market_id') + baseId = self.safe_string(market, 'base_currency') + quoteId = self.safe_string(market, 'counter_currency') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + status = self.safe_string(market, 'trading_status') + result.append({ + 'id': id, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': (status == 'ACTIVE'), + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(self.parse_precision(self.safe_string(market, 'volume_scale'))), + 'price': self.parse_number(self.parse_precision(self.safe_string(market, 'price_scale'))), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(market, 'min_volume'), + 'max': self.safe_number(market, 'max_volume'), + }, + 'price': { + 'min': self.safe_number(market, 'min_price'), + 'max': self.safe_number(market, 'max_price'), + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + 'info': market, + }) + return result + + def fetch_accounts(self, params={}) -> List[Account]: + """ + fetch all the accounts associated with a profile + + https://www.luno.com/en/developers/api#tag/Accounts/operation/getBalances + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `account structures ` indexed by the account type + """ + response = self.privateGetBalance(params) + wallets = self.safe_value(response, 'balance', []) + result = [] + for i in range(0, len(wallets)): + account = wallets[i] + accountId = self.safe_string(account, 'account_id') + currencyId = self.safe_string(account, 'asset') + code = self.safe_currency_code(currencyId) + result.append({ + 'id': accountId, + 'type': None, + 'currency': code, + 'info': account, + }) + return result + + def parse_balance(self, response) -> Balances: + wallets = self.safe_value(response, 'balance', []) + result: dict = { + 'info': response, + 'timestamp': None, + 'datetime': None, + } + for i in range(0, len(wallets)): + wallet = wallets[i] + currencyId = self.safe_string(wallet, 'asset') + code = self.safe_currency_code(currencyId) + reserved = self.safe_string(wallet, 'reserved') + unconfirmed = self.safe_string(wallet, 'unconfirmed') + balance = self.safe_string(wallet, 'balance') + reservedUnconfirmed = Precise.string_add(reserved, unconfirmed) + balanceUnconfirmed = Precise.string_add(balance, unconfirmed) + if code in result: + result[code]['used'] = Precise.string_add(result[code]['used'], reservedUnconfirmed) + result[code]['total'] = Precise.string_add(result[code]['total'], balanceUnconfirmed) + else: + account = self.account() + account['used'] = reservedUnconfirmed + account['total'] = balanceUnconfirmed + result[code] = account + return self.safe_balance(result) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://www.luno.com/en/developers/api#tag/Accounts/operation/getBalances + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + response = self.privateGetBalance(params) + # + # { + # "balance": [ + # {'account_id': '119...1336','asset': 'XBT','balance': '0.00','reserved': '0.00',"unconfirmed": "0.00"}, + # {'account_id': '66...289','asset': 'XBT','balance': '0.00','reserved': '0.00',"unconfirmed": "0.00"}, + # {'account_id': '718...5300','asset': 'ETH','balance': '0.00','reserved': '0.00',"unconfirmed": "0.00"}, + # {'account_id': '818...7072','asset': 'ZAR','balance': '0.001417','reserved': '0.00',"unconfirmed": "0.00"}]} + # ] + # } + # + return self.parse_balance(response) + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://www.luno.com/en/developers/api#tag/Market/operation/GetOrderBookFull + https://www.luno.com/en/developers/api#tag/Market/operation/GetOrderBook + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + response = None + if limit is not None and limit <= 100: + response = self.publicGetOrderbookTop(self.extend(request, params)) + else: + response = self.publicGetOrderbook(self.extend(request, params)) + timestamp = self.safe_integer(response, 'timestamp') + return self.parse_order_book(response, market['symbol'], timestamp, 'bids', 'asks', 'price', 'volume') + + def parse_order_status(self, status: Str): + statuses: dict = { + # todo add other statuses + 'PENDING': 'open', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # { + # "base": "string", + # "completed_timestamp": "string", + # "counter": "string", + # "creation_timestamp": "string", + # "expiration_timestamp": "string", + # "fee_base": "string", + # "fee_counter": "string", + # "limit_price": "string", + # "limit_volume": "string", + # "order_id": "string", + # "pair": "string", + # "state": "PENDING", + # "type": "BID" + # } + # + timestamp = self.safe_integer(order, 'creation_timestamp') + status = self.parse_order_status(self.safe_string(order, 'state')) + status = status if (status == 'open') else status + side = None + orderType = self.safe_string(order, 'type') + if (orderType == 'ASK') or (orderType == 'SELL'): + side = 'sell' + elif (orderType == 'BID') or (orderType == 'BUY'): + side = 'buy' + marketId = self.safe_string(order, 'pair') + market = self.safe_market(marketId, market) + price = self.safe_string(order, 'limit_price') + amount = self.safe_string(order, 'limit_volume') + quoteFee = self.safe_number(order, 'fee_counter') + baseFee = self.safe_number(order, 'fee_base') + filled = self.safe_string(order, 'base') + cost = self.safe_string(order, 'counter') + fee = None + if quoteFee is not None: + fee = { + 'cost': quoteFee, + 'currency': market['quote'], + } + elif baseFee is not None: + fee = { + 'cost': baseFee, + 'currency': market['base'], + } + id = self.safe_string(order, 'order_id') + return self.safe_order({ + 'id': id, + 'clientOrderId': None, + 'datetime': self.iso8601(timestamp), + 'timestamp': timestamp, + 'lastTradeTimestamp': None, + 'status': status, + 'symbol': market['symbol'], + 'type': None, + 'timeInForce': None, + 'postOnly': None, + 'side': side, + 'price': price, + 'triggerPrice': None, + 'amount': amount, + 'filled': filled, + 'cost': cost, + 'remaining': None, + 'trades': None, + 'fee': fee, + 'info': order, + 'average': None, + }, market) + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://www.luno.com/en/developers/api#tag/Orders/operation/GetOrder + + :param str id: order id + :param str symbol: not used by luno fetchOrder + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + request: dict = { + 'id': id, + } + response = self.privateGetOrdersId(self.extend(request, params)) + return self.parse_order(response) + + def fetch_orders_by_state(self, state: Str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + self.load_markets() + request: dict = {} + market = None + if state is not None: + request['state'] = state + if symbol is not None: + market = self.market(symbol) + request['pair'] = market['id'] + response = self.privateGetListorders(self.extend(request, params)) + orders = self.safe_list(response, 'orders', []) + return self.parse_orders(orders, market, since, limit) + + def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://www.luno.com/en/developers/api#tag/Orders/operation/ListOrders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + return self.fetch_orders_by_state(None, symbol, since, limit, params) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://www.luno.com/en/developers/api#tag/Orders/operation/ListOrders + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + return self.fetch_orders_by_state('PENDING', symbol, since, limit, params) + + def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://www.luno.com/en/developers/api#tag/Orders/operation/ListOrders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + return self.fetch_orders_by_state('COMPLETE', symbol, since, limit, params) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # { + # "pair":"XBTAUD", + # "timestamp":1642201439301, + # "bid":"59972.30000000", + # "ask":"59997.99000000", + # "last_trade":"59997.99000000", + # "rolling_24_hour_volume":"1.89510000", + # "status":"ACTIVE" + # } + timestamp = self.safe_integer(ticker, 'timestamp') + marketId = self.safe_string(ticker, 'pair') + symbol = self.safe_symbol(marketId, market) + last = self.safe_string(ticker, 'last_trade') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': None, + 'low': None, + 'bid': self.safe_string(ticker, 'bid'), + 'bidVolume': None, + 'ask': self.safe_string(ticker, 'ask'), + 'askVolume': None, + 'vwap': None, + 'open': None, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': self.safe_string(ticker, 'rolling_24_hour_volume'), + 'quoteVolume': None, + 'info': ticker, + }, market) + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://www.luno.com/en/developers/api#tag/Market/operation/GetTickers + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + symbols = self.market_symbols(symbols) + response = self.publicGetTickers(params) + tickers = self.index_by(response['tickers'], 'pair') + ids = list(tickers.keys()) + result: dict = {} + for i in range(0, len(ids)): + id = ids[i] + market = self.safe_market(id) + symbol = market['symbol'] + ticker = tickers[id] + result[symbol] = self.parse_ticker(ticker, market) + return self.filter_by_array_tickers(result, 'symbol', symbols) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://www.luno.com/en/developers/api#tag/Market/operation/GetTicker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + response = self.publicGetTicker(self.extend(request, params)) + # { + # "pair":"XBTAUD", + # "timestamp":1642201439301, + # "bid":"59972.30000000", + # "ask":"59997.99000000", + # "last_trade":"59997.99000000", + # "rolling_24_hour_volume":"1.89510000", + # "status":"ACTIVE" + # } + return self.parse_ticker(response, market) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(public) + # + # { + # "sequence":276989, + # "timestamp":1648651276949, + # "price":"35773.20000000", + # "volume":"0.00300000", + # "is_buy":false + # } + # + # fetchMyTrades(private) + # + # { + # "pair":"LTCXBT", + # "sequence":3256813, + # "order_id":"BXEX6XHHDT5EGW2", + # "type":"ASK", + # "timestamp":1648652135235, + # "price":"0.002786", + # "volume":"0.10", + # "base":"0.10", + # "counter":"0.0002786", + # "fee_base":"0.0001", + # "fee_counter":"0.00", + # "is_buy":false, + # "client_order_id":"" + # } + # + # For public trade data(is_buy is True) indicates 'buy' side but for private trade data + # is_buy indicates maker or taker. The value of "type"(ASK/BID) indicate sell/buy side. + # Private trade data includes ID field which public trade data does not. + orderId = self.safe_string(trade, 'order_id') + id = self.safe_string(trade, 'sequence') + takerOrMaker = None + side = None + if orderId is not None: + type = self.safe_string(trade, 'type') + if (type == 'ASK') or (type == 'SELL'): + side = 'sell' + elif (type == 'BID') or (type == 'BUY'): + side = 'buy' + if side == 'sell' and trade['is_buy']: + takerOrMaker = 'maker' + elif side == 'buy' and not trade['is_buy']: + takerOrMaker = 'maker' + else: + takerOrMaker = 'taker' + else: + side = 'buy' if trade['is_buy'] else 'sell' + feeBaseString = self.safe_string(trade, 'fee_base') + feeCounterString = self.safe_string(trade, 'fee_counter') + feeCurrency = None + feeCost = None + if feeBaseString is not None: + if not Precise.string_equals(feeBaseString, '0.0'): + feeCurrency = market['base'] + feeCost = feeBaseString + elif feeCounterString is not None: + if not Precise.string_equals(feeCounterString, '0.0'): + feeCurrency = market['quote'] + feeCost = feeCounterString + timestamp = self.safe_integer(trade, 'timestamp') + return self.safe_trade({ + 'info': trade, + 'id': id, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'order': orderId, + 'type': None, + 'side': side, + 'takerOrMaker': takerOrMaker, + 'price': self.safe_string(trade, 'price'), + 'amount': self.safe_string_2(trade, 'volume', 'base'), + # Does not include potential fee costs + 'cost': self.safe_string(trade, 'counter'), + 'fee': { + 'cost': feeCost, + 'currency': feeCurrency, + }, + }, market) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://www.luno.com/en/developers/api#tag/Market/operation/ListTrades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + if since is not None: + request['since'] = since + response = self.publicGetTrades(self.extend(request, params)) + # + # { + # "trades":[ + # { + # "sequence":276989, + # "timestamp":1648651276949, + # "price":"35773.20000000", + # "volume":"0.00300000", + # "is_buy":false + # },... + # ] + # } + # + trades = self.safe_list(response, 'trades', []) + return self.parse_trades(trades, market, since, limit) + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}): + """ + + https://www.luno.com/en/developers/api#tag/Market/operation/GetCandles + + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict params: extra parameters specific to the luno api endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'duration': self.safe_value(self.timeframes, timeframe, timeframe), + 'pair': market['id'], + } + if since is not None: + request['since'] = self.parse_to_int(since) + else: + duration = 1000 * 1000 * self.parse_timeframe(timeframe) + request['since'] = self.milliseconds() - duration + response = self.exchangePrivateGetCandles(self.extend(request, params)) + # + # { + # "candles": [ + # { + # "timestamp": 1664055240000, + # "open": "19612.65", + # "close": "19612.65", + # "high": "19612.65", + # "low": "19612.65", + # "volume": "0.00" + # },... + # ], + # "duration": 60, + # "pair": "XBTEUR" + # } + # + ohlcvs = self.safe_list(response, 'candles', []) + return self.parse_ohlcvs(ohlcvs, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # { + # "timestamp": 1664055240000, + # "open": "19612.65", + # "close": "19612.65", + # "high": "19612.65", + # "low": "19612.65", + # "volume": "0.00" + # } + return [ + self.safe_integer(ohlcv, 'timestamp'), + self.safe_number(ohlcv, 'open'), + self.safe_number(ohlcv, 'high'), + self.safe_number(ohlcv, 'low'), + self.safe_number(ohlcv, 'close'), + self.safe_number(ohlcv, 'volume'), + ] + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://www.luno.com/en/developers/api#tag/Orders/operation/ListUserTrades + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMyTrades() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + if since is not None: + request['since'] = since + if limit is not None: + request['limit'] = limit + response = self.privateGetListtrades(self.extend(request, params)) + # + # { + # "trades":[ + # { + # "pair":"LTCXBT", + # "sequence":3256813, + # "order_id":"BXEX6XHHDT5EGW2", + # "type":"ASK", + # "timestamp":1648652135235, + # "price":"0.002786", + # "volume":"0.10", + # "base":"0.10", + # "counter":"0.0002786", + # "fee_base":"0.0001", + # "fee_counter":"0.00", + # "is_buy":false, + # "client_order_id":"" + # },... + # ] + # } + # + trades = self.safe_list(response, 'trades', []) + return self.parse_trades(trades, market, since, limit) + + def fetch_trading_fee(self, symbol: str, params={}) -> TradingFeeInterface: + """ + fetch the trading fees for a market + + https://www.luno.com/en/developers/api#tag/Orders/operation/getFeeInfo + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `fee structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + response = self.privateGetFeeInfo(self.extend(request, params)) + # + # { + # "maker_fee": "0.00250000", + # "taker_fee": "0.00500000", + # "thirty_day_volume": "0" + # } + # + return { + 'info': response, + 'symbol': symbol, + 'maker': self.safe_number(response, 'maker_fee'), + 'taker': self.safe_number(response, 'taker_fee'), + 'percentage': None, + 'tierBased': None, + } + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://www.luno.com/en/developers/api#tag/Orders/operation/PostMarketOrder + https://www.luno.com/en/developers/api#tag/Orders/operation/PostLimitOrder + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + response = None + if type == 'market': + request['type'] = side.upper() + # todo add createMarketBuyOrderRequires price logic is implemented in the other exchanges + if side == 'buy': + request['counter_volume'] = self.amount_to_precision(market['symbol'], amount) + else: + request['base_volume'] = self.amount_to_precision(market['symbol'], amount) + response = self.privatePostMarketorder(self.extend(request, params)) + else: + request['volume'] = self.amount_to_precision(market['symbol'], amount) + request['price'] = self.price_to_precision(market['symbol'], price) + request['type'] = 'BID' if (side == 'buy') else 'ASK' + response = self.privatePostPostorder(self.extend(request, params)) + return self.safe_order({ + 'info': response, + 'id': response['order_id'], + }, market) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://www.luno.com/en/developers/api#tag/Orders/operation/StopOrder + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + request: dict = { + 'order_id': id, + } + response = self.privatePostStoporder(self.extend(request, params)) + # + # { + # "success": True + # } + # + return self.safe_order({ + 'info': response, + }) + + def fetch_ledger_by_entries(self, code: Str = None, entry=None, limit=None, params={}): + # by default without entry number or limit number, return most recent entry + if entry is None: + entry = -1 + if limit is None: + limit = 1 + since = None + request: dict = { + 'min_row': entry, + 'max_row': self.sum(entry, limit), + } + return self.fetch_ledger(code, since, limit, self.extend(request, params)) + + def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + + https://www.luno.com/en/developers/api#tag/Accounts/operation/ListTransactions + + :param str [code]: unified currency code, default is None + :param int [since]: timestamp in ms of the earliest ledger entry, default is None + :param int [limit]: max number of ledger entries to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ledger structure ` + """ + self.load_markets() + self.load_accounts() + currency = None + id = self.safe_string(params, 'id') # account id + min_row = self.safe_value(params, 'min_row') + max_row = self.safe_value(params, 'max_row') + if id is None: + if code is None: + raise ArgumentsRequired(self.id + ' fetchLedger() requires a currency code argument if no account id specified in params') + currency = self.currency(code) + accountsByCurrencyCode = self.index_by(self.accounts, 'currency') + account = self.safe_value(accountsByCurrencyCode, code) + if account is None: + raise ExchangeError(self.id + ' fetchLedger() could not find account id for ' + code) + id = account['id'] + if min_row is None and max_row is None: + max_row = 0 # Default to most recent transactions + min_row = -1000 # Maximum number of records supported + elif min_row is None or max_row is None: + raise ExchangeError(self.id + " fetchLedger() require both params 'max_row' and 'min_row' or neither to be defined") + if limit is not None and max_row - min_row > limit: + if max_row <= 0: + min_row = max_row - limit + elif min_row > 0: + max_row = min_row + limit + if max_row - min_row > 1000: + raise ExchangeError(self.id + " fetchLedger() requires the params 'max_row' - 'min_row' <= 1000") + request: dict = { + 'id': id, + 'min_row': min_row, + 'max_row': max_row, + } + response = self.privateGetAccountsIdTransactions(self.extend(params, request)) + entries = self.safe_value(response, 'transactions', []) + return self.parse_ledger(entries, currency, since, limit) + + def parse_ledger_comment(self, comment): + words = comment.split(' ') + types: dict = { + 'Withdrawal': 'fee', + 'Trading': 'fee', + 'Payment': 'transaction', + 'Sent': 'transaction', + 'Deposit': 'transaction', + 'Received': 'transaction', + 'Released': 'released', + 'Reserved': 'reserved', + 'Sold': 'trade', + 'Bought': 'trade', + 'Failure': 'failed', + } + referenceId = None + firstWord = self.safe_string(words, 0) + thirdWord = self.safe_string(words, 2) + fourthWord = self.safe_string(words, 3) + type = self.safe_string(types, firstWord, None) + if (type is None) and (thirdWord == 'fee'): + type = 'fee' + if (type == 'reserved') and (fourthWord == 'order'): + referenceId = self.safe_string(words, 4) + return { + 'type': type, + 'referenceId': referenceId, + } + + def parse_ledger_entry(self, entry, currency: Currency = None) -> LedgerEntry: + # details = self.safe_value(entry, 'details', {}) + id = self.safe_string(entry, 'row_index') + account_id = self.safe_string(entry, 'account_id') + timestamp = self.safe_integer(entry, 'timestamp') + currencyId = self.safe_string(entry, 'currency') + code = self.safe_currency_code(currencyId, currency) + currency = self.safe_currency(currencyId, currency) + available_delta = self.safe_string(entry, 'available_delta') + balance_delta = self.safe_string(entry, 'balance_delta') + after = self.safe_string(entry, 'balance') + comment = self.safe_string(entry, 'description') + before = after + amount = '0.0' + result = self.parse_ledger_comment(comment) + type = result['type'] + referenceId = result['referenceId'] + direction = None + status = None + if not Precise.string_equals(balance_delta, '0.0'): + before = Precise.string_sub(after, balance_delta) + status = 'ok' + amount = Precise.string_abs(balance_delta) + elif Precise.string_lt(available_delta, '0.0'): + status = 'pending' + amount = Precise.string_abs(available_delta) + elif Precise.string_gt(available_delta, '0.0'): + status = 'canceled' + amount = Precise.string_abs(available_delta) + if Precise.string_gt(balance_delta, '0') or Precise.string_gt(available_delta, '0'): + direction = 'in' + elif Precise.string_lt(balance_delta, '0') or Precise.string_lt(available_delta, '0'): + direction = 'out' + return self.safe_ledger_entry({ + 'info': entry, + 'id': id, + 'direction': direction, + 'account': account_id, + 'referenceId': referenceId, + 'referenceAccount': None, + 'type': type, + 'currency': code, + 'amount': self.parse_to_numeric(amount), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'before': self.parse_to_numeric(before), + 'after': self.parse_to_numeric(after), + 'status': status, + 'fee': None, + }, currency) + + def create_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + create a currency deposit address + + https://www.luno.com/en/developers/api#tag/Receive/operation/createFundingAddress + + :param str code: unified currency code of the currency for the deposit address + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.name]: an optional name for the new address + :param int [params.account_id]: an optional account id for the new address + :returns dict: an `address structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'asset': currency['id'], + } + response = self.privatePostFundingAddress(self.extend(request, params)) + # + # { + # "account_id": "string", + # "address": "string", + # "address_meta": [ + # { + # "label": "string", + # "value": "string" + # } + # ], + # "asset": "string", + # "assigned_at": 0, + # "name": "string", + # "network": 0, + # "qr_code_uri": "string", + # "receive_fee": "string", + # "total_received": "string", + # "total_unconfirmed": "string" + # } + # + return self.parse_deposit_address(response, currency) + + def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://www.luno.com/en/developers/api#tag/Receive/operation/getFundingAddress + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.address]: a specific cryptocurrency address to retrieve + :returns dict: an `address structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'asset': currency['id'], + } + response = self.privateGetFundingAddress(self.extend(request, params)) + # + # { + # "account_id": "string", + # "address": "string", + # "address_meta": [ + # { + # "label": "string", + # "value": "string" + # } + # ], + # "asset": "string", + # "assigned_at": 0, + # "name": "string", + # "network": 0, + # "qr_code_uri": "string", + # "receive_fee": "string", + # "total_received": "string", + # "total_unconfirmed": "string" + # } + # + return self.parse_deposit_address(response, currency) + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + # + # { + # "account_id": "string", + # "address": "string", + # "address_meta": [ + # { + # "label": "string", + # "value": "string" + # } + # ], + # "asset": "string", + # "assigned_at": 0, + # "name": "string", + # "network": 0, + # "qr_code_uri": "string", + # "receive_fee": "string", + # "total_received": "string", + # "total_unconfirmed": "string" + # } + # + currencyId = self.safe_string_upper(depositAddress, 'currency') + code = self.safe_currency_code(currencyId, currency) + return { + 'info': depositAddress, + 'currency': code, + 'network': None, + 'address': self.safe_string(depositAddress, 'address'), + 'tag': self.safe_string(depositAddress, 'name'), + } + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = self.urls['api'][api] + '/' + self.version + '/' + self.implode_params(path, params) + query = self.omit(params, self.extract_params(path)) + if query: + url += '?' + self.urlencode(query) + if (api == 'private') or (api == 'exchangePrivate'): + self.check_required_credentials() + auth = self.string_to_base64(self.apiKey + ':' + self.secret) + headers = { + 'Authorization': 'Basic ' + auth, + } + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None + error = self.safe_value(response, 'error') + if error is not None: + raise ExchangeError(self.id + ' ' + self.json(response)) + return None diff --git a/ccxt/mercado.py b/ccxt/mercado.py new file mode 100644 index 0000000..99091ae --- /dev/null +++ b/ccxt/mercado.py @@ -0,0 +1,950 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.mercado import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currency, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Ticker, Trade, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import InvalidOrder +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class mercado(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(mercado, self).describe(), { + 'id': 'mercado', + 'name': 'Mercado Bitcoin', + 'countries': ['BR'], # Brazil + 'rateLimit': 1000, + 'version': 'v3', + 'has': { + 'CORS': True, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelOrder': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createMarketOrder': True, + 'createOrder': True, + 'createReduceOnlyOrder': False, + 'createStopLimitOrder': False, + 'createStopMarketOrder': False, + 'createStopOrder': False, + 'fetchAllGreeks': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': False, + 'fetchDepositAddress': False, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrice': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': 'emulated', + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTickers': False, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': False, + 'fetchUnderlyingAssets': False, + 'fetchVolatilityHistory': False, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'withdraw': True, + }, + 'timeframes': { + '15m': '15m', + '1h': '1h', + '3h': '3h', + '1d': '1d', + '1w': '1w', + '1M': '1M', + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/27837060-e7c58714-60ea-11e7-9192-f05e86adb83f.jpg', + 'api': { + 'public': 'https://www.mercadobitcoin.net/api', + 'private': 'https://www.mercadobitcoin.net/tapi', + 'v4Public': 'https://www.mercadobitcoin.com.br/v4', + 'v4PublicNet': 'https://api.mercadobitcoin.net/api/v4', + }, + 'www': 'https://www.mercadobitcoin.com.br', + 'doc': [ + 'https://www.mercadobitcoin.com.br/api-doc', + 'https://www.mercadobitcoin.com.br/trade-api', + ], + }, + 'api': { + 'public': { + 'get': [ + 'coins', + '{coin}/orderbook/', # last slash critical + '{coin}/ticker/', + '{coin}/trades/', + '{coin}/trades/{from}/', + '{coin}/trades/{from}/{to}', + '{coin}/day-summary/{year}/{month}/{day}/', + ], + }, + 'private': { + 'post': [ + 'cancel_order', + 'get_account_info', + 'get_order', + 'get_withdrawal', + 'list_system_messages', + 'list_orders', + 'list_orderbook', + 'place_buy_order', + 'place_sell_order', + 'place_market_buy_order', + 'place_market_sell_order', + 'withdraw_coin', + ], + }, + 'v4Public': { + 'get': [ + '{coin}/candle/', + ], + }, + 'v4PublicNet': { + 'get': [ + 'candles', + ], + }, + }, + 'fees': { + 'trading': { + 'maker': 0.003, + 'taker': 0.007, + }, + }, + 'options': { + 'limits': { + 'BTC': 0.001, + 'BCH': 0.001, + 'ETH': 0.01, + 'LTC': 0.01, + 'XRP': 0.1, + }, + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': False, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': False, + 'FOK': False, + 'PO': True, # todo + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': True, # todo + 'marketBuyRequiresPrice': True, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': None, # todo + 'daysBack': 100000, # todo + 'untilDays': 100000, # todo + 'symbolRequired': True, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': 500, + 'daysBack': 100000, + 'untilDays': 100000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchClosedOrders': None, + 'fetchOHLCV': { + 'limit': 1000, + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'precisionMode': TICK_SIZE, + }) + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for mercado + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = self.publicGetCoins(params) + # + # [ + # "BCH", + # "BTC", + # "ETH", + # "LTC", + # "XRP", + # "MBPRK01", + # "MBPRK02", + # "MBPRK03", + # "MBPRK04", + # "MBCONS01", + # "USDC", + # "WBX", + # "CHZ", + # "MBCONS02", + # "PAXG", + # "MBVASCO01", + # "LINK" + # ] + # + result = [] + amountLimits = self.safe_value(self.options, 'limits', {}) + for i in range(0, len(response)): + coin = response[i] + baseId = coin + quoteId = 'BRL' + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + id = quote + base + result.append({ + 'id': id, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': None, + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number('1e-8'), + 'price': self.parse_number('1e-5'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(amountLimits, baseId), + 'max': None, + }, + 'price': { + 'min': self.parse_number('1e-5'), + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + 'info': coin, + }) + return result + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'coin': market['base'], + } + response = self.publicGetCoinOrderbook(self.extend(request, params)) + return self.parse_order_book(response, market['symbol']) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "high":"103.96000000", + # "low":"95.00000000", + # "vol":"2227.67806598", + # "last":"97.91591000", + # "buy":"95.52760000", + # "sell":"97.91475000", + # "open":"99.79955000", + # "date":1643382606 + # } + # + symbol = self.safe_symbol(None, market) + timestamp = self.safe_timestamp(ticker, 'date') + last = self.safe_string(ticker, 'last') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': self.safe_string(ticker, 'buy'), + 'bidVolume': None, + 'ask': self.safe_string(ticker, 'sell'), + 'askVolume': None, + 'vwap': None, + 'open': None, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': self.safe_string(ticker, 'vol'), + 'quoteVolume': None, + 'info': ticker, + }, market) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'coin': market['base'], + } + response = self.publicGetCoinTicker(self.extend(request, params)) + ticker = self.safe_value(response, 'ticker', {}) + # + # { + # "ticker": { + # "high":"1549.82293000", + # "low":"1503.00011000", + # "vol":"81.82827101", + # "last":"1533.15000000", + # "buy":"1533.21018000", + # "sell":"1540.09000000", + # "open":"1524.71089000", + # "date":1643691671 + # } + # } + # + return self.parse_ticker(ticker, market) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + timestamp = self.safe_timestamp_2(trade, 'date', 'executed_timestamp') + market = self.safe_market(None, market) + id = self.safe_string_2(trade, 'tid', 'operation_id') + type = None + side = self.safe_string(trade, 'type') + price = self.safe_string(trade, 'price') + amount = self.safe_string_2(trade, 'amount', 'quantity') + feeCost = self.safe_string(trade, 'fee_rate') + fee = None + if feeCost is not None: + fee = { + 'cost': feeCost, + 'currency': None, + } + return self.safe_trade({ + 'id': id, + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'order': None, + 'type': type, + 'side': side, + 'takerOrMaker': None, + 'price': price, + 'amount': amount, + 'cost': None, + 'fee': fee, + }, market) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + method = 'publicGetCoinTrades' + request: dict = { + 'coin': market['base'], + } + if since is not None: + method += 'From' + request['from'] = self.parse_to_int(since / 1000) + to = self.safe_integer(params, 'to') + if to is not None: + method += 'To' + response = getattr(self, method)(self.extend(request, params)) + return self.parse_trades(response, market, since, limit) + + def parse_balance(self, response) -> Balances: + data = self.safe_value(response, 'response_data', {}) + balances = self.safe_value(data, 'balance', {}) + result: dict = {'info': response} + currencyIds = list(balances.keys()) + for i in range(0, len(currencyIds)): + currencyId = currencyIds[i] + code = self.safe_currency_code(currencyId) + if currencyId in balances: + balance = self.safe_value(balances, currencyId, {}) + account = self.account() + account['free'] = self.safe_string(balance, 'available') + account['total'] = self.safe_string(balance, 'total') + result[code] = account + return self.safe_balance(result) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + response = self.privatePostGetAccountInfo(params) + return self.parse_balance(response) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'coin_pair': market['id'], + } + method = self.capitalize(side) + 'Order' + if type == 'limit': + method = 'privatePostPlace' + method + request['limit_price'] = self.price_to_precision(market['symbol'], price) + request['quantity'] = self.amount_to_precision(market['symbol'], amount) + else: + method = 'privatePostPlaceMarket' + method + if side == 'buy': + if price is None: + raise InvalidOrder(self.id + ' createOrder() requires the price argument with market buy orders to calculate total order cost(amount to spend), where cost = amount * price. Supply a price argument to createOrder() call if you want the cost to be calculated for you from price and amount') + amountString = self.number_to_string(amount) + priceString = self.number_to_string(price) + cost = self.parse_to_numeric(Precise.string_mul(amountString, priceString)) + request['cost'] = self.price_to_precision(market['symbol'], cost) + else: + request['quantity'] = self.amount_to_precision(market['symbol'], amount) + response = getattr(self, method)(self.extend(request, params)) + # TODO: replace self with a call to parseOrder for unification + return self.safe_order({ + 'info': response, + 'id': str(response['response_data']['order']['order_id']), + }, market) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'coin_pair': market['id'], + 'order_id': id, + } + response = self.privatePostCancelOrder(self.extend(request, params)) + # + # { + # "response_data": { + # "order": { + # "order_id": 2176769, + # "coin_pair": "BRLBCH", + # "order_type": 2, + # "status": 3, + # "has_fills": False, + # "quantity": "0.10000000", + # "limit_price": "1996.15999", + # "executed_quantity": "0.00000000", + # "executed_price_avg": "0.00000", + # "fee": "0.00000000", + # "created_timestamp": "1536956488", + # "updated_timestamp": "1536956499", + # "operations": [] + # } + # }, + # "status_code": 100, + # "server_unix_timestamp": "1536956499" + # } + # + responseData = self.safe_value(response, 'response_data', {}) + order = self.safe_dict(responseData, 'order', {}) + return self.parse_order(order, market) + + def parse_order_status(self, status: Str): + statuses: dict = { + '2': 'open', + '3': 'canceled', + '4': 'closed', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # { + # "order_id": 4, + # "coin_pair": "BRLBTC", + # "order_type": 1, + # "status": 2, + # "has_fills": True, + # "quantity": "2.00000000", + # "limit_price": "900.00000", + # "executed_quantity": "1.00000000", + # "executed_price_avg": "900.00000", + # "fee": "0.00300000", + # "created_timestamp": "1453838494", + # "updated_timestamp": "1453838494", + # "operations": [ + # { + # "operation_id": 1, + # "quantity": "1.00000000", + # "price": "900.00000", + # "fee_rate": "0.30", + # "executed_timestamp": "1453838494", + # }, + # ], + # } + # + id = self.safe_string(order, 'order_id') + order_type = self.safe_string(order, 'order_type') + side = None + if 'order_type' in order: + side = 'buy' if (order_type == '1') else 'sell' + status = self.parse_order_status(self.safe_string(order, 'status')) + marketId = self.safe_string(order, 'coin_pair') + market = self.safe_market(marketId, market) + timestamp = self.safe_timestamp(order, 'created_timestamp') + fee = { + 'cost': self.safe_string(order, 'fee'), + 'currency': market['quote'], + } + price = self.safe_string(order, 'limit_price') + # price = self.safe_number(order, 'executed_price_avg', price) + average = self.safe_string(order, 'executed_price_avg') + amount = self.safe_string(order, 'quantity') + filled = self.safe_string(order, 'executed_quantity') + lastTradeTimestamp = self.safe_timestamp(order, 'updated_timestamp') + rawTrades = self.safe_value(order, 'operations', []) + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': lastTradeTimestamp, + 'symbol': market['symbol'], + 'type': 'limit', + 'timeInForce': None, + 'postOnly': None, + 'side': side, + 'price': price, + 'triggerPrice': None, + 'cost': None, + 'average': average, + 'amount': amount, + 'filled': filled, + 'remaining': None, + 'status': status, + 'fee': fee, + 'trades': rawTrades, + }, market) + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrder() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'coin_pair': market['id'], + 'order_id': int(id), + } + response = self.privatePostGetOrder(self.extend(request, params)) + responseData = self.safe_value(response, 'response_data', {}) + order = self.safe_dict(responseData, 'order') + return self.parse_order(order, market) + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.check_address(address) + self.load_markets() + currency = self.currency(code) + request: dict = { + 'coin': currency['id'], + 'quantity': format(amount, '.10f'), + 'address': address, + } + if code == 'BRL': + account_ref = ('account_ref' in params) + if not account_ref: + raise ArgumentsRequired(self.id + ' withdraw() requires account_ref parameter to withdraw ' + code) + elif code != 'LTC': + tx_fee = ('tx_fee' in params) + if not tx_fee: + raise ArgumentsRequired(self.id + ' withdraw() requires tx_fee parameter to withdraw ' + code) + if code == 'XRP': + if tag is None: + if not ('destination_tag' in params): + raise ArgumentsRequired(self.id + ' withdraw() requires a tag argument or destination_tag parameter to withdraw ' + code) + else: + request['destination_tag'] = tag + response = self.privatePostWithdrawCoin(self.extend(request, params)) + # + # { + # "response_data": { + # "withdrawal": { + # "id": 1, + # "coin": "BRL", + # "quantity": "300.56", + # "net_quantity": "291.68", + # "fee": "8.88", + # "account": "bco: 341, ag: 1111, cta: 23456-X", + # "status": 1, + # "created_timestamp": "1453912088", + # "updated_timestamp": "1453912088" + # } + # }, + # "status_code": 100, + # "server_unix_timestamp": "1453912088" + # } + # + responseData = self.safe_value(response, 'response_data', {}) + withdrawal = self.safe_dict(responseData, 'withdrawal') + return self.parse_transaction(withdrawal, currency) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # { + # "id": 1, + # "coin": "BRL", + # "quantity": "300.56", + # "net_quantity": "291.68", + # "fee": "8.88", + # "account": "bco: 341, ag: 1111, cta: 23456-X", + # "status": 1, + # "created_timestamp": "1453912088", + # "updated_timestamp": "1453912088" + # } + # + currency = self.safe_currency(None, currency) + return { + 'id': self.safe_string(transaction, 'id'), + 'txid': None, + 'timestamp': None, + 'datetime': None, + 'network': None, + 'addressFrom': None, + 'address': None, + 'addressTo': None, + 'amount': None, + 'type': None, + 'currency': currency['code'], + 'status': None, + 'updated': None, + 'tagFrom': None, + 'tag': None, + 'tagTo': None, + 'comment': None, + 'internal': None, + 'fee': None, + 'info': transaction, + } + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + return [ + self.safe_integer(ohlcv, 0), + self.safe_number(ohlcv, 1), + self.safe_number(ohlcv, 2), + self.safe_number(ohlcv, 3), + self.safe_number(ohlcv, 4), + self.safe_number(ohlcv, 5), + ] + + def fetch_ohlcv(self, symbol: str, timeframe: str = '15m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'resolution': self.safe_string(self.timeframes, timeframe, timeframe), + 'symbol': market['base'] + '-' + market['quote'], # exceptional endpoint, that needs custom symbol syntax + } + if limit is None: + limit = 100 # set some default limit,'s required if user doesn't provide it + if since is not None: + request['from'] = self.parse_to_int(since / 1000) + request['to'] = self.sum(request['from'], limit * self.parse_timeframe(timeframe)) + else: + request['to'] = self.seconds() + request['from'] = request['to'] - (limit * self.parse_timeframe(timeframe)) + response = self.v4PublicNetGetCandles(self.extend(request, params)) + candles = self.convert_trading_view_to_ohlcv(response, 't', 'o', 'h', 'l', 'c', 'v') + return self.parse_ohlcvs(candles, market, timeframe, since, limit) + + def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrders() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'coin_pair': market['id'], + } + response = self.privatePostListOrders(self.extend(request, params)) + responseData = self.safe_value(response, 'response_data', {}) + orders = self.safe_list(responseData, 'orders', []) + return self.parse_orders(orders, market, since, limit) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOpenOrders() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'coin_pair': market['id'], + 'status_list': '[2]', # open only + } + response = self.privatePostListOrders(self.extend(request, params)) + responseData = self.safe_value(response, 'response_data', {}) + orders = self.safe_list(responseData, 'orders', []) + return self.parse_orders(orders, market, since, limit) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMyTrades() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'coin_pair': market['id'], + 'has_fills': True, + } + response = self.privatePostListOrders(self.extend(request, params)) + responseData = self.safe_value(response, 'response_data', {}) + ordersRaw = self.safe_value(responseData, 'orders', []) + orders = self.parse_orders(ordersRaw, market, since, limit) + trades = self.orders_to_trades(orders) + return self.filter_by_symbol_since_limit(trades, market['symbol'], since, limit) + + def orders_to_trades(self, orders): + result = [] + for i in range(0, len(orders)): + trades = self.safe_value(orders[i], 'trades', []) + for y in range(0, len(trades)): + result.append(trades[y]) + return result + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = self.urls['api'][api] + '/' + query = self.omit(params, self.extract_params(path)) + if (api == 'public') or (api == 'v4Public') or (api == 'v4PublicNet'): + url += self.implode_params(path, params) + if query: + url += '?' + self.urlencode(query) + else: + self.check_required_credentials() + url += self.version + '/' + nonce = self.nonce() + body = self.urlencode(self.extend({ + 'tapi_method': path, + 'tapi_nonce': nonce, + }, params)) + auth = '/tapi/' + self.version + '/' + '?' + body + headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + 'TAPI-ID': self.apiKey, + 'TAPI-MAC': self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha512), + } + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None + # + # todo add a unified standard handleErrors with self.exceptions in describe() + # + # {"status":503,"message":"Maintenancing, try again later","result":null} + # + errorMessage = self.safe_value(response, 'error_message') + if errorMessage is not None: + raise ExchangeError(self.id + ' ' + self.json(response)) + return None diff --git a/ccxt/mexc.py b/ccxt/mexc.py new file mode 100644 index 0000000..7b63dd6 --- /dev/null +++ b/ccxt/mexc.py @@ -0,0 +1,5876 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.mexc import ImplicitAPI +import hashlib +from ccxt.base.types import Account, Any, Balances, Currencies, Currency, DepositAddress, IndexType, Int, Leverage, LeverageTier, LeverageTiers, MarginModification, Market, Num, Order, OrderBook, OrderRequest, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, Trade, TradingFeeInterface, Transaction, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import AccountSuspended +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidAddress +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import NotSupported +from ccxt.base.errors import OnMaintenance +from ccxt.base.errors import InvalidNonce +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class mexc(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(mexc, self).describe(), { + 'id': 'mexc', + 'name': 'MEXC Global', + 'countries': ['SC'], # Seychelles + 'rateLimit': 50, # default rate limit is 20 times per second + 'version': 'v3', + 'certified': True, + 'pro': True, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': True, + 'swap': True, + 'future': False, + 'option': False, + 'addMargin': True, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'cancelOrders': None, + 'closeAllPositions': False, + 'closePosition': False, + 'createDepositAddress': True, + 'createMarketBuyOrderWithCost': True, + 'createMarketOrderWithCost': True, + 'createMarketSellOrderWithCost': True, + 'createOrder': True, + 'createOrders': True, + 'createPostOnlyOrder': True, + 'createReduceOnlyOrder': True, + 'createStopLimitOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'createTriggerOrder': True, + 'deposit': None, + 'editOrder': None, + 'fetchAccounts': True, + 'fetchBalance': True, + 'fetchBidsAsks': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchCanceledOrders': True, + 'fetchClosedOrder': None, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDeposit': None, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': None, + 'fetchDepositAddressesByNetwork': True, + 'fetchDeposits': True, + 'fetchDepositWithdrawFee': 'emulated', + 'fetchDepositWithdrawFees': True, + 'fetchFundingHistory': True, + 'fetchFundingInterval': True, + 'fetchFundingIntervals': False, + 'fetchFundingRate': True, + 'fetchFundingRateHistory': True, + 'fetchFundingRates': False, + 'fetchIndexOHLCV': True, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchL2OrderBook': True, + 'fetchLedger': None, + 'fetchLedgerEntry': None, + 'fetchLeverage': True, + 'fetchLeverages': False, + 'fetchLeverageTiers': True, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarketLeverageTiers': 'emulated', + 'fetchMarkets': True, + 'fetchMarkOHLCV': True, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenOrder': None, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrderBooks': None, + 'fetchOrders': True, + 'fetchOrderTrades': True, + 'fetchPosition': 'emulated', + 'fetchPositionHistory': 'emulated', + 'fetchPositionMode': True, + 'fetchPositions': True, + 'fetchPositionsHistory': True, + 'fetchPositionsRisk': None, + 'fetchPremiumIndexOHLCV': False, + 'fetchStatus': True, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': True, + 'fetchTradingFees': False, + 'fetchTradingLimits': None, + 'fetchTransactionFee': 'emulated', + 'fetchTransactionFees': True, + 'fetchTransactions': None, + 'fetchTransfer': True, + 'fetchTransfers': True, + 'fetchWithdrawal': None, + 'fetchWithdrawals': True, + 'reduceMargin': True, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'setLeverage': True, + 'setMarginMode': True, + 'setPositionMode': True, + 'signIn': None, + 'transfer': None, + 'withdraw': True, + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/137283979-8b2a818d-8633-461b-bfca-de89e8c446b2.jpg', + 'api': { + 'spot': { + 'public': 'https://api.mexc.com', + 'private': 'https://api.mexc.com', + }, + 'spot2': { + 'public': 'https://www.mexc.com/open/api/v2', + 'private': 'https://www.mexc.com/open/api/v2', + }, + 'contract': { + 'public': 'https://contract.mexc.com/api/v1/contract', + 'private': 'https://contract.mexc.com/api/v1/private', + }, + 'broker': { + 'private': 'https://api.mexc.com/api/v3/broker', + }, + }, + 'www': 'https://www.mexc.com/', + 'doc': [ + 'https://mexcdevelop.github.io/apidocs/', + ], + 'fees': [ + 'https://www.mexc.com/fee', + ], + 'referral': 'https://www.mexc.com/register?inviteCode=mexc-1FQ1GNu1', + }, + 'api': { + 'spot': { + 'public': { + 'get': { + 'ping': 1, + 'time': 1, + 'exchangeInfo': 10, + 'depth': 1, + 'trades': 5, + 'historicalTrades': 1, + 'aggTrades': 1, + 'klines': 1, + 'avgPrice': 1, + 'ticker/24hr': 1, + 'ticker/price': 1, + 'ticker/bookTicker': 1, + 'etf/info': 1, + }, + }, + 'private': { + 'get': { + 'order': 2, + 'openOrders': 3, + 'allOrders': 10, + 'account': 10, + 'myTrades': 10, + 'tradeFee': 10, + 'sub-account/list': 1, + 'sub-account/apiKey': 1, + 'capital/config/getall': 10, + 'capital/deposit/hisrec': 1, + 'capital/withdraw/history': 1, + 'capital/withdraw/address': 10, + 'capital/deposit/address': 10, + 'capital/transfer': 1, + 'capital/transfer/tranId': 1, + 'capital/transfer/internal': 1, + 'capital/sub-account/universalTransfer': 1, + 'capital/convert': 1, + 'capital/convert/list': 1, + 'margin/loan': 1, + 'margin/allOrders': 1, + 'margin/myTrades': 1, + 'margin/openOrders': 1, + 'margin/maxTransferable': 1, + 'margin/priceIndex': 1, + 'margin/order': 1, + 'margin/isolated/account': 1, + 'margin/maxBorrowable': 1, + 'margin/repay': 1, + 'margin/isolated/pair': 1, + 'margin/forceLiquidationRec': 1, + 'margin/isolatedMarginData': 1, + 'margin/isolatedMarginTier': 1, + 'rebate/taxQuery': 1, + 'rebate/detail': 1, + 'rebate/detail/kickback': 1, + 'rebate/referCode': 1, + 'rebate/affiliate/commission': 1, + 'rebate/affiliate/withdraw': 1, + 'rebate/affiliate/commission/detail': 1, + 'mxDeduct/enable': 1, + 'userDataStream': 1, + 'selfSymbols': 1, + 'asset/internal/transfer/record': 10, + }, + 'post': { + 'order': 1, + 'order/test': 1, + 'sub-account/virtualSubAccount': 1, + 'sub-account/apiKey': 1, + 'sub-account/futures': 1, + 'sub-account/margin': 1, + 'batchOrders': 10, + 'capital/withdraw/apply': 1, + 'capital/withdraw': 1, + 'capital/transfer': 1, + 'capital/transfer/internal': 1, + 'capital/deposit/address': 1, + 'capital/sub-account/universalTransfer': 1, + 'capital/convert': 10, + 'mxDeduct/enable': 1, + 'userDataStream': 1, + }, + 'put': { + 'userDataStream': 1, + }, + 'delete': { + 'order': 1, + 'openOrders': 1, + 'sub-account/apiKey': 1, + 'margin/order': 1, + 'margin/openOrders': 1, + 'userDataStream': 1, + 'capital/withdraw': 1, + }, + }, + }, + 'contract': { + 'public': { + 'get': { + 'ping': 2, + 'detail': 100, + 'support_currencies': 2, + 'depth/{symbol}': 2, + 'depth_commits/{symbol}/{limit}': 2, + 'index_price/{symbol}': 2, + 'fair_price/{symbol}': 2, + 'funding_rate/{symbol}': 2, + 'kline/{symbol}': 2, + 'kline/index_price/{symbol}': 2, + 'kline/fair_price/{symbol}': 2, + 'deals/{symbol}': 2, + 'ticker': 2, + 'risk_reverse': 2, + 'risk_reverse/history': 2, + 'funding_rate/history': 2, + }, + }, + 'private': { + 'get': { + 'account/assets': 2, + 'account/asset/{currency}': 2, + 'account/transfer_record': 2, + 'position/list/history_positions': 2, + 'position/open_positions': 2, + 'position/funding_records': 2, + 'position/position_mode': 2, + 'order/list/open_orders/{symbol}': 2, + 'order/list/history_orders': 2, + 'order/external/{symbol}/{external_oid}': 2, + 'order/get/{order_id}': 2, + 'order/batch_query': 8, + 'order/deal_details/{order_id}': 2, + 'order/list/order_deals': 2, + 'planorder/list/orders': 2, + 'stoporder/list/orders': 2, + 'stoporder/order_details/{stop_order_id}': 2, + 'account/risk_limit': 2, # TO_DO: gets max/min position size, allowed sides, leverage, maintenance margin, initial margin, etc... + 'account/tiered_fee_rate': 2, # TO_DO: taker/maker fees for account + 'position/leverage': 2, + }, + 'post': { + 'position/change_margin': 2, + 'position/change_leverage': 2, + 'position/change_position_mode': 2, + 'order/submit': 2, + 'order/submit_batch': 40, + 'order/cancel': 2, + 'order/cancel_with_external': 2, + 'order/cancel_all': 2, + 'account/change_risk_level': 2, + 'planorder/place': 2, + 'planorder/cancel': 2, + 'planorder/cancel_all': 2, + 'stoporder/cancel': 2, + 'stoporder/cancel_all': 2, + 'stoporder/change_price': 2, + 'stoporder/change_plan_price': 2, + }, + }, + }, + 'spot2': { + 'public': { + 'get': { + 'market/symbols': 1, + 'market/coin/list': 2, + 'common/timestamp': 1, + 'common/ping': 2, + 'market/ticker': 1, + 'market/depth': 1, + 'market/deals': 1, + 'market/kline': 1, + 'market/api_default_symbols': 2, + }, + }, + 'private': { + 'get': { + 'account/info': 1, + 'order/open_orders': 1, + 'order/list': 1, + 'order/query': 1, + 'order/deals': 1, + 'order/deal_detail': 1, + 'asset/deposit/address/list': 2, + 'asset/deposit/list': 2, + 'asset/address/list': 2, + 'asset/withdraw/list': 2, + 'asset/internal/transfer/record': 10, + 'account/balance': 10, + 'asset/internal/transfer/info': 10, + 'market/api_symbols': 2, + }, + 'post': { + 'order/place': 1, + 'order/place_batch': 1, + 'order/advanced/place_batch': 1, + 'asset/withdraw': 2, + 'asset/internal/transfer': 10, + }, + 'delete': { + 'order/cancel': 1, + 'order/cancel_by_symbol': 1, + 'asset/withdraw': 2, + }, + }, + }, + 'broker': { + 'private': { + 'get': { + 'sub-account/universalTransfer': 1, + 'sub-account/list': 1, + 'sub-account/apiKey': 1, + 'capital/deposit/subAddress': 1, + 'capital/deposit/subHisrec': 1, + 'capital/deposit/subHisrec/getall': 1, + }, + 'post': { + 'sub-account/virtualSubAccount': 1, + 'sub-account/apiKey': 1, + 'capital/deposit/subAddress': 1, + 'capital/withdraw/apply': 1, + 'sub-account/universalTransfer': 1, + 'sub-account/futures': 1, + }, + 'delete': { + 'sub-account/apiKey': 1, + }, + }, + }, + }, + 'precisionMode': TICK_SIZE, + 'timeframes': { + '1m': '1m', # spot, swap + '5m': '5m', # spot, swap + '15m': '15m', # spot, swap + '30m': '30m', # spot, swap + '1h': '1h', # spot, swap + '4h': '4h', # spot, swap + '8h': '8h', # swap + '1d': '1d', # spot, swap + '1w': '1w', # swap + '1M': '1M', # spot, swap + }, + 'fees': { + 'trading': { + 'tierBased': False, + 'percentage': True, + 'maker': self.parse_number('0.002'), # maker / taker + 'taker': self.parse_number('0.002'), + }, + }, + 'options': { + 'adjustForTimeDifference': False, + 'timeDifference': 0, + 'unavailableContracts': { + 'BTC/USDT:USDT': True, + 'LTC/USDT:USDT': True, + 'ETH/USDT:USDT': True, + }, + 'fetchMarkets': { + 'types': { + 'spot': True, + 'swap': { + 'linear': True, + 'inverse': False, + }, + }, + }, + 'useCcxtTradeId': True, + 'timeframes': { + 'spot': { + '1m': '1m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '60m', + '4h': '4h', + '1d': '1d', + '1w': '1W', + '1M': '1M', + }, + 'swap': { + '1m': 'Min1', + '5m': 'Min5', + '15m': 'Min15', + '30m': 'Min30', + '1h': 'Min60', + '4h': 'Hour4', + '8h': 'Hour8', + '1d': 'Day1', + '1w': 'Week1', + '1M': 'Month1', + }, + }, + 'defaultType': 'spot', # spot, swap + 'defaultNetwork': 'ETH', + 'defaultNetworks': { + 'ETH': 'ETH', + 'USDT': 'ERC20', + 'USDC': 'ERC20', + 'BTC': 'BTC', + 'LTC': 'LTC', + }, + 'networks': { + 'ZKSYNC': 'ZKSYNCERA', + 'TRC20': 'TRX', + 'TON': 'TONCOIN', + 'ARBITRUM': 'ARB', + 'STX': 'STACKS', + 'LUNC': 'LUNA', + 'STARK': 'STARKNET', + 'APT': 'APTOS', + 'PEAQ': 'PEAQEVM', + 'AVAXC': 'AVAX_CCHAIN', + 'ERC20': 'ETH', + 'ACA': 'ACALA', + 'BEP20': 'BSC', + 'OPTIMISM': 'OP', + # 'ADA': 'Cardano(ADA)', + # 'AE': 'AE', + # 'ALGO': 'Algorand(ALGO)', + # 'ALPH': 'Alephium(ALPH)', + # 'ARB': 'Arbitrum One(ARB)', + # 'ARBONE': 'ArbitrumOne(ARB)', + 'ASTR': 'ASTAR', # ASTAREVM is different + # 'ATOM': 'Cosmos(ATOM)', + # 'AVAXC': 'Avalanche C Chain(AVAX CCHAIN)', + # 'AVAXX': 'Avalanche X Chain(AVAX XCHAIN)', + # 'AZERO': 'Aleph Zero(AZERO)', + # 'BCH': 'Bitcoin Cash(BCH)', + # 'BNCDOT': 'BNCPOLKA', + # 'BSV': 'Bitcoin SV(BSV)', + # 'BTC': 'Bitcoin(BTC)', + 'BTM': 'BTM2', + # 'CHZ': 'Chiliz Legacy Chain(CHZ)', + # 'CHZ2': 'Chiliz Chain(CHZ2)', + # 'CLORE': 'Clore.ai(CLORE)', + 'CRC20': 'CRONOS', + # 'DC': 'Dogechain(DC)', + # 'DNX': 'Dynex(DNX)', + # 'DOGE': 'Dogecoin(DOGE)', + # 'DOT': 'Polkadot(DOT)', + 'DOT': 'DOTASSETHUB', + # 'DYM': 'Dymension(DYM)', + 'ETHF': 'ETF', + 'HRC20': 'HECO', + # 'KLAY': 'Klaytn(KLAY)', + 'OASIS': 'ROSE', + 'OKC': 'OKT', + 'RSK': 'RBTC', + # 'RVN': 'Ravencoin(RVN)', + # 'SATOX': 'Satoxcoin(SATOX)', + # 'SC': 'SC', + # 'SCRT': 'SCRT', + # 'SDN': 'SDN', + # 'SGB': 'SGB', + # 'SOL': 'Solana(SOL)', + # 'STAR': 'STAR', + # 'STARK': 'Starknet(STARK)', + # 'STEEM': 'STEEM', + # 'SYS': 'SYS', + # 'TAO': 'Bittensor(TAO)', + # 'TIA': 'Celestia(TIA)', + # 'TOMO': 'TOMO', + # 'TON': 'Toncoin(TON)', + # 'TRC10': 'TRC10', + # 'TRC20': 'Tron(TRC20)', + # 'UGAS': 'UGAS(Ultrain)', + # 'VET': 'VeChain(VET)', + # 'VEX': 'Vexanium(VEX)', + # 'VSYS': 'VSYS', + # 'WAVES': 'WAVES', + # 'WAX': 'WAX', + # 'WEMIX': 'WEMIX', + # 'XCH': 'Chia(XCH)', + # 'XDC': 'XDC', + # 'XEC': 'XEC', + # 'XLM': 'Stellar(XLM)', + # 'XMR': 'Monero(XMR)', + # 'XNA': 'Neurai(XNA)', + # 'XPR': 'XPR Network', + # 'XRD': 'XRD', + # 'XRP': 'Ripple(XRP)', + # 'XTZ': 'XTZ', + # 'XVG': 'XVG', + # 'XYM': 'XYM', + # 'ZEC': 'ZEC', + # 'ZEN': 'ZEN', + # 'ZIL': 'Zilliqa(ZIL)', + # 'ZTG': 'ZTG', + # todo: uncomment below after concensus + # 'ALAYA': 'ATP', + # 'ANDUSCHAIN': 'DEB', + # 'ASSETMANTLE': 'MNTL', + # 'AXE': 'AXE', + # 'BITCOINHD': 'BHD', + # 'BITCOINVAULT': 'BTCV', + # 'BITKUB': 'KUB', + # 'BITSHARES_OLD': 'BTS', + # 'BITSHARES': 'NBS', + # 'BYTZ': 'BYTZ', + # 'CANTO': 'CANTO', # CANTOEVM + # 'CENNZ': 'CENNZ', + # 'CHAINX': 'PCX', + # 'CONCODRIUM': 'CCD', + # 'CONTENTVALUENETWORK': 'CVNT', + # 'CORTEX': 'CTXC', + # 'CYPHERIUM': 'CPH', + # 'DANGNN': 'DGC', + # 'DARWINIASMARTCHAIN': 'Darwinia Smart Chain', + # 'DHEALTH': 'DHP', + # 'DOGECOIN': ['DOGE', 'DOGECHAIN'], # todo after unification + # 'DRAC': 'DRAC', + # 'DRAKEN': 'DRK', + # 'ECOCHAIN': 'ECOC', + # 'ELECTRAPROTOCOL': 'XEP', + # 'EMERALD': 'EMERALD', # sits on top of OASIS + # 'EVMOS': 'EVMOS', # EVMOSETH is different + # 'EXOSAMA': 'SAMA', + # 'FIBOS': 'FO', + # 'FILECASH': 'FIC', + # 'FIRMACHAIN': 'FCT', + # 'FIRO': 'XZC', + # 'FNCY': 'FNCY', + # 'FRUITS': 'FRTS', + # 'GLEEC': 'GLEEC', + # 'GXCHAIN': 'GXC', + # 'HANDSHAKE': 'HNS', + # 'HPB': 'HPB', + # 'HSHARE': 'HC', + # 'HUAHUA': 'HUAHUA', + # 'HUPAYX': 'HPX', + # 'INDEXCHAIN': 'IDX', + # 'INTCHAIN': 'INT', + # 'INTEGRITEE': 'TEER', + # 'INTERLAY': 'INTR', + # 'IOEX': 'IOEX', + # 'JUNO': 'JUNO', + # 'KASPA': 'KASPA', + # 'KEKCHAIN': 'KEKCHAIN', + # 'KINTSUGI': 'KINT', + # 'KOINOS': 'KOINOS', + # 'KONSTELLATION': 'DARC', + # 'KUJIRA': 'KUJI', + # 'KULUPU': 'KLP', + # 'LBRY': 'LBC', + # 'LEDGIS': 'LED', + # 'LIGHTNINGBITCOIN': 'LBTC', + # 'LINE': 'LINE', + # 'MDNA': 'DNA', + # 'MDUKEY': 'MDU', + # 'METAMUI': 'MMUI', + # 'METAVERSE_ETP': 'ETP', + # 'METER': 'MTRG', + # 'MEVERSE': 'MEVerse', + # 'NEWTON': 'NEW', + # 'NODLE': 'NODLE', + # 'ORIGYN': 'OGY', + # 'PAC': 'PAC', + # 'PASTEL': 'PSL', + # 'PHALA': 'Khala', + # 'PLEX': 'PLEX', + # 'PMG': 'PMG', + # 'POINT': 'POINT', # POINTEVM is different + # 'PROOFOFMEMES': 'POM', + # 'PROXIMAX': 'XPX', + # 'RCHAIN': 'REV', + # 'REBUS': 'REBUS', # REBUSEVM is different + # 'RIZON': 'ATOLO', + # 'SENTINEL': 'DVPN', + # 'SERO': 'SERO', + # 'TECHPAY': 'TPC', + # 'TELOSCOIN': 'TLOS', # todo + # 'TERRA': 'LUNA2', + # 'TERRACLASSIC': 'LUNC', + # 'TLOS': 'TELOS', # todo + # 'TOMAINFO': 'TON', + # 'TONGTONG': 'TTC', + # 'TURTLECOIN': 'TRTL', + # 'ULORD': 'UT', + # 'ULTRAIN': 'UGAS', + # 'UMEE': 'UMEE', + # 'VDIMENSION': 'VOLLAR', + # 'VEXANIUM': 'VEX', + # 'VNT': 'VNT', + # 'WAYKICHAIN': 'WICC', + # 'WHITECOIN': 'XWC', + # 'WITNET': 'WIT', + # 'XDAI': 'XDAI', + # 'XX': 'XX', + # 'YAS': 'YAS', + # 'ZENITH': 'ZENITH', + # 'ZKSYNC': 'ZKSYNC', + # # 'BAJUN': '', + # OKB <> OKT(for usdt it's exception) for OKC, PMEER, FLARE, STRD, ZEL, FUND, "NONE", CRING, FREETON, QTZ (probably unique network is meant), HT, BSC(RACAV1), BSC(RACAV2), AMBROSUS, BAJUN, NOM. their individual info is at https://www.mexc.com/api/platform/asset/spot/{COINNAME} + }, + 'networksById': { + 'BNB Smart Chain(BEP20-RACAV1)': 'BSC', + 'BNB Smart Chain(BEP20-RACAV2)': 'BSC', + 'BNB Smart Chain(BEP20)': 'BSC', + 'Ethereum(ERC20)': 'ERC20', + # TODO: uncomment below after deciding unified name + # 'PEPE COIN BSC': + # 'SMART BLOCKCHAIN': + # 'f(x)Core': + # 'Syscoin Rollux': + # 'Syscoin UTXO': + # 'zkSync Era': + # 'zkSync Lite': + # 'Darwinia Smart Chain': + # 'Arbitrum One(ARB-Bridged)': + # 'Optimism(OP-Bridged)': + # 'Polygon(MATIC-Bridged)': + }, + 'recvWindow': 5 * 1000, # 5 sec, default + 'maxTimeTillEnd': 90 * 86400 * 1000 - 1, # 90 days + 'broker': 'CCXT', + }, + 'features': { + 'default': { + 'sandbox': False, + 'createOrder': { + 'marginMode': True, + 'triggerPrice': False, + 'triggerDirection': False, + 'triggerPriceType': { + 'last': False, + 'mark': False, + 'index': False, + }, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': True, # todo implement + 'trailing': False, + 'leverage': True, # todo implement + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': { + 'max': 20, + }, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 30, + 'untilDays': None, + 'symbolRequired': True, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOpenOrders': { + 'marginMode': True, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOrders': { + 'marginMode': True, + 'limit': 1000, + 'daysBack': 7, + 'untilDays': 7, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchClosedOrders': { + 'marginMode': True, + 'limit': 1000, + 'daysBack': 7, + 'daysBackCanceled': 7, + 'untilDays': 7, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOHLCV': { + 'limit': 1000, + }, + }, + 'spot': { + 'extends': 'default', + 'fetchCurrencies': { + 'private': True, + }, + }, + 'forDerivs': { + 'extends': 'default', + 'createOrder': { + 'triggerPrice': True, + 'triggerPriceType': { + 'last': True, + 'mark': True, + 'index': True, + }, + 'triggerDirection': True, # todo + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'hedged': True, + 'leverage': True, # todo + 'marketBuyByCost': False, + }, + 'createOrders': None, # todo: needs implementation https://mexcdevelop.github.io/apidocs/contract_v1_en/#order-under-maintenance:~:text=Order%20the%20contract%20in%20batch + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 90, + 'untilDays': 90, + }, + 'fetchOrder': { + 'marginMode': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 100, + 'trigger': True, + 'trailing': False, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 90, + 'untilDays': 90, + 'trigger': True, + 'trailing': False, + }, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 90, + 'daysBackCanceled': None, + 'untilDays': 90, + 'trigger': True, + 'trailing': False, + }, + 'fetchOHLCV': { + 'limit': 2000, + }, + }, + 'swap': { + 'linear': { + 'extends': 'forDerivs', + }, + 'inverse': { + 'extends': 'forDerivs', + }, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'commonCurrencies': { + 'BEYONDPROTOCOL': 'BEYOND', + 'BIFI': 'BIFIF', + 'BYN': 'BEYONDFI', + 'COFI': 'COFIX', # conflict with CoinFi + 'DFI': 'DFISTARTER', + 'DFT': 'DFUTURE', + 'DRK': 'DRK', + 'EGC': 'EGORASCREDIT', + 'FLUX1': 'FLUX', # switched places + 'FLUX': 'FLUX1', # switched places + 'FREE': 'FREEROSSDAO', # conflict with FREE Coin + 'GAS': 'GASDAO', + 'GASNEO': 'GAS', + 'GMT': 'GMTTOKEN', # Conflict with GMT(STEPN) + 'STEPN': 'GMT', # Conflict with GMT Token + 'HERO': 'STEPHERO', # conflict with Metahero + 'MIMO': 'MIMOSA', + 'PROS': 'PROSFINANCE', # conflict with Prosper + 'SIN': 'SINCITYTOKEN', + 'SOUL': 'SOULSWAP', + 'XBT': 'XBT', # restore original mapping + }, + 'exceptions': { + 'exact': { + # until mexc migrates fully to v3, it might be worth to note the version & market aside errors, not easily remove obsolete version's exceptions in future + '-1128': BadRequest, + '-2011': BadRequest, + '-1121': BadSymbol, + '10101': InsufficientFunds, # {"msg":"资金不足","code":10101} + '2009': InvalidOrder, # {"success":false,"code":2009,"message":"Position is not exists or closed."} + '2011': BadRequest, + '30004': InsufficientFunds, + '33333': BadRequest, # {"msg":"Not support transfer","code":33333} + '44444': BadRequest, + '1002': InvalidOrder, + '30019': BadRequest, + '30005': InvalidOrder, + '2003': InvalidOrder, + '2005': InsufficientFunds, + '400': BadRequest, # {"msg":"The start time cannot be earlier than 90 days","code":400} + # '500': OnMaintenance, # {"code": 500,"message": "Under maintenance, please try again later","announcement": "https://www.mexc.com/support/articles/17827791510263"} + '600': BadRequest, + '70011': PermissionDenied, # {"code":70011,"msg":"Pair user ban trade apikey."} + '88004': InsufficientFunds, # {"msg":"超出最大可借,最大可借币为:18.09833211","code":88004} + '88009': ExchangeError, # v3 {"msg":"Loan record does not exist","code":88009} + '88013': InvalidOrder, # {"msg":"最小交易额不能小于:5USDT","code":88013} + '88015': InsufficientFunds, # {"msg":"持仓不足","code":88015} + '700003': InvalidNonce, # {"code":700003,"msg":"Timestamp for self request is outside of the recvWindow."} + '26': ExchangeError, # operation not allowed + '602': AuthenticationError, # Signature verification failed + '10001': AuthenticationError, # user does not exist + '10007': BadSymbol, # {"code":10007,"msg":"bad symbol"} + '10015': BadRequest, # user id cannot be null + '10072': BadRequest, # invalid access key + '10073': BadRequest, # invalid Request-Time + '10095': InvalidOrder, # amount cannot be null + '10096': InvalidOrder, # amount decimal places is too long + '10097': InvalidOrder, # amount is error + '10098': InvalidOrder, # risk control system detected abnormal + '10099': BadRequest, # user sub account does not open + '10100': BadRequest, # self currency transfer is not supported + '10102': InvalidOrder, # amount cannot be zero or negative + '10103': ExchangeError, # self account transfer is not supported + '10200': BadRequest, # transfer operation processing + '10201': BadRequest, # transfer in failed + '10202': BadRequest, # transfer out failed + '10206': BadRequest, # transfer is disabled + '10211': BadRequest, # transfer is forbidden + '10212': BadRequest, # This withdrawal address is not on the commonly used address list or has been invalidated + '10216': ExchangeError, # no address available. Please try again later + '10219': ExchangeError, # asset flow writing failed please try again + '10222': BadRequest, # currency cannot be null + '10232': BadRequest, # currency does not exist + '10259': ExchangeError, # Intermediate account does not configured in redisredis + '10265': ExchangeError, # Due to risk control, withdrawal is unavailable, please try again later + '10268': BadRequest, # remark length is too long + '20001': ExchangeError, # subsystem is not supported + '20002': ExchangeError, # Internal system error please contact support + '22222': BadRequest, # record does not exist + '30000': ExchangeError, # suspended transaction for the symbol + '30001': InvalidOrder, # The current transaction direction is not allowed to place an order + '30002': InvalidOrder, # The minimum transaction volume cannot be less than : + '30003': InvalidOrder, # The maximum transaction volume cannot be greater than : + '30010': InvalidOrder, # no valid trade price + '30014': InvalidOrder, # invalid symbol + '30016': InvalidOrder, # trading disabled + '30018': AccountSuspended, # {"msg":"账号暂时不能下单,请联系客服","code":30018} + '30020': AuthenticationError, # no permission for the symbol + '30021': BadRequest, # invalid symbol + '30025': InvalidOrder, # no exist opponent order + '30026': BadRequest, # invalid order ids + '30027': InvalidOrder, # The currency has reached the maximum position limit, the buying is suspended + '30028': InvalidOrder, # The currency triggered the platform risk control, the selling is suspended + '30029': InvalidOrder, # Cannot exceed the maximum order limit + '30032': InvalidOrder, # Cannot exceed the maximum position + '30041': InvalidOrder, # current order type can not place order + '30087': InvalidOrder, # {"msg":"Order price exceeds allowed range","code":30087} + '60005': ExchangeError, # your account is abnormal + '700001': AuthenticationError, # {"code":700002,"msg":"Signature for self request is not valid."} # same message for expired API keys + '700002': AuthenticationError, # Signature for self request is not valid # or the API secret is incorrect + '700004': BadRequest, # Param 'origClientOrderId' or 'orderId' must be sent, but both were empty/null + '700005': InvalidNonce, # recvWindow must less than 60000 + '700006': BadRequest, # IP non white list + '700007': AuthenticationError, # No permission to access the endpoint + '700008': BadRequest, # Illegal characters found in parameter + '700013': AuthenticationError, # Invalid Content-Type v3 + '730001': BadRequest, # Pair not found + '730002': BadRequest, # Your input param is invalid + '730000': ExchangeError, # Request failed, please contact the customer service + '730003': ExchangeError, # Unsupported operation, please contact the customer service + '730100': ExchangeError, # Unusual user status + '730600': BadRequest, # Sub-account Name cannot be null + '730601': BadRequest, # Sub-account Name must be a combination of 8-32 letters and numbers + '730602': BadRequest, # Sub-account remarks cannot be null + '730700': BadRequest, # API KEY remarks cannot be null + '730701': BadRequest, # API KEY permission cannot be null + '730702': BadRequest, # API KEY permission does not exist + '730703': BadRequest, # The IP information is incorrect, and a maximum of 10 IPs are allowed to be bound only + '730704': BadRequest, # The bound IP format is incorrect, please refill + '730705': BadRequest, # At most 30 groups of Api Keys are allowed to be created only + '730706': BadRequest, # API KEY information does not exist + '730707': BadRequest, # accessKey cannot be null + '730101': BadRequest, # The user Name already exists + '140001': BadRequest, # sub account does not exist + '140002': AuthenticationError, # sub account is forbidden + }, + 'broad': { + 'Order quantity error, please try to modify.': BadRequest, # code:2011 + 'Combination of optional parameters invalid': BadRequest, # code:-2011 + 'api market order is disabled': BadRequest, # + 'Contract not allow place order!': InvalidOrder, # code:1002 + 'Oversold': InsufficientFunds, # code:30005 + 'Insufficient position': InsufficientFunds, # code:30004 + 'Insufficient balance!': InsufficientFunds, # code:2005 + 'Bid price is great than max allow price': InvalidOrder, # code:2003 + 'Invalid symbol.': BadSymbol, # code:-1121 + 'Param error!': BadRequest, # code:600 + 'maintenance': OnMaintenance, # {"code": 500,"message": "Under maintenance, please try again later","announcement": "https://www.mexc.com/support/articles/17827791510263"} + }, + }, + }) + + def fetch_status(self, params={}): + """ + the latest known information on the availability of the exchange API + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#test-connectivity + https://mexcdevelop.github.io/apidocs/contract_v1_en/#get-the-server-time + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `status structure ` + """ + marketType, query = self.handle_market_type_and_params('fetchStatus', None, params) + response = None + status = None + updated = None + if marketType == 'spot': + response = self.spotPublicGetPing(query) + # + # {} + # + keys = list(response.keys()) + length = len(keys) + status = self.json(response) if length else 'ok' + elif marketType == 'swap': + response = self.contractPublicGetPing(query) + # + # {"success":true,"code":"0","data":"1648124374985"} + # + status = 'ok' if self.safe_value(response, 'success') else self.json(response) + updated = self.safe_integer(response, 'data') + return { + 'status': status, + 'updated': updated, + 'url': None, + 'eta': None, + 'info': response, + } + + def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#check-server-time + https://mexcdevelop.github.io/apidocs/contract_v1_en/#get-the-server-time + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + marketType, query = self.handle_market_type_and_params('fetchTime', None, params) + response = None + if marketType == 'spot': + response = self.spotPublicGetTime(query) + # + # {"serverTime": "1647519277579"} + # + return self.safe_integer(response, 'serverTime') + elif marketType == 'swap': + response = self.contractPublicGetPing(query) + # + # {"success":true,"code":"0","data":"1648124374985"} + # + return self.safe_integer(response, 'data') + return None + + def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#query-the-currency-information + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + # self endpoint requires authentication + # while fetchCurrencies is a public API method by design + # therefore we check the keys here + # and fallback to generating the currencies from the markets + if not self.check_required_credentials(False): + return {} + response = self.spotPrivateGetCapitalConfigGetall(params) + # + # { + # "coin": "QANX", + # "name": "QANplatform", + # "networkList": [ + # { + # "coin": "QANX", + # "depositDesc": null, + # "depositEnable": True, + # "minConfirm": "0", + # "name": "QANplatform", + # "network": "BEP20(BSC)", + # "withdrawEnable": False, + # "withdrawFee": "42.000000000000000000", + # "withdrawIntegerMultiple": null, + # "withdrawMax": "24000000.000000000000000000", + # "withdrawMin": "20.000000000000000000", + # "sameAddress": False, + # "contract": "0xAAA7A10a8ee237ea61E8AC46C50A8Db8bCC1baaa" + # }, + # { + # "coin": "QANX", + # "depositDesc": null, + # "depositEnable": True, + # "minConfirm": "0", + # "name": "QANplatform", + # "network": "ERC20", + # "withdrawEnable": True, + # "withdrawFee": "2732.000000000000000000", + # "withdrawIntegerMultiple": null, + # "withdrawMax": "24000000.000000000000000000", + # "withdrawMin": "240.000000000000000000", + # "sameAddress": False, + # "contract": "0xAAA7A10a8ee237ea61E8AC46C50A8Db8bCC1baaa" + # } + # ] + # } + # + result: dict = {} + for i in range(0, len(response)): + currency = response[i] + id = self.safe_string(currency, 'coin') + code = self.safe_currency_code(id) + networks: dict = {} + chains = self.safe_value(currency, 'networkList', []) + for j in range(0, len(chains)): + chain = chains[j] + networkId = self.safe_string_2(chain, 'netWork', 'network') + network = self.network_id_to_code(networkId) + networks[network] = { + 'info': chain, + 'id': networkId, + 'network': network, + 'active': None, + 'deposit': self.safe_bool(chain, 'depositEnable', False), + 'withdraw': self.safe_bool(chain, 'withdrawEnable', False), + 'fee': self.safe_number(chain, 'withdrawFee'), + 'precision': None, + 'limits': { + 'withdraw': { + 'min': self.safe_string(chain, 'withdrawMin'), + 'max': self.safe_string(chain, 'withdrawMax'), + }, + }, + 'contract': self.safe_string(chain, 'contract'), + } + result[code] = self.safe_currency_structure({ + 'info': currency, + 'id': id, + 'code': code, + 'name': self.safe_string(currency, 'name'), + 'active': None, + 'deposit': None, + 'withdraw': None, + 'fee': None, + 'precision': None, + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + }, + 'type': 'crypto', + 'networks': networks, + }) + return result + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for mexc + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#exchange-information + https://mexcdevelop.github.io/apidocs/contract_v1_en/#get-the-contract-information + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + if self.options['adjustForTimeDifference']: + self.load_time_difference() + spotMarketPromise = self.fetch_spot_markets(params) + swapMarketPromise = self.fetch_swap_markets(params) + spotMarket, swapMarket = [spotMarketPromise, swapMarketPromise] + return self.array_concat(spotMarket, swapMarket) + + def fetch_spot_markets(self, params={}): + """ + @ignore + retrieves data on all spot markets for mexc + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#exchange-information + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = self.spotPublicGetExchangeInfo(params) + # + # { + # "timezone": "CST", + # "serverTime": 1647521860402, + # "rateLimits": [], + # "exchangeFilters": [], + # "symbols": [ + # { + # "symbol": "OGNUSDT", + # "status": "1", + # "baseAsset": "OGN", + # "baseAssetPrecision": "2", + # "quoteAsset": "USDT", + # "quoteAssetPrecision": "4", + # "orderTypes": [ + # "LIMIT", + # "LIMIT_MAKER" + # ], + # "baseCommissionPrecision": "2", + # "quoteCommissionPrecision": "4", + # "quoteOrderQtyMarketAllowed": False, + # "isSpotTradingAllowed": True, + # "isMarginTradingAllowed": True, + # "permissions": [ + # "SPOT", + # "MARGIN" + # ], + # "filters": [], + # "baseSizePrecision": "0.01", # self turned out to be a minimum base amount for order + # "maxQuoteAmount": "5000000", + # "makerCommission": "0.002", + # "takerCommission": "0.002" + # "quoteAmountPrecision": "5", # self turned out to be a minimum cost amount for order + # "quotePrecision": "4", # deprecated in favor of 'quoteAssetPrecision'( https://dev.binance.vision/t/what-is-the-difference-between-quoteprecision-and-quoteassetprecision/4333 ) + # # note, "icebergAllowed" & "ocoAllowed" fields were recently removed + # }, + # ] + # } + # + # Notes: + # - 'quoteAssetPrecision' & 'baseAssetPrecision' are not currency's real blockchain precision(to view currency's actual individual precision, refer to fetchCurrencies() method). + # + data = self.safe_value(response, 'symbols', []) + result = [] + for i in range(0, len(data)): + market = data[i] + id = self.safe_string(market, 'symbol') + baseId = self.safe_string(market, 'baseAsset') + quoteId = self.safe_string(market, 'quoteAsset') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + status = self.safe_string(market, 'status') + isSpotTradingAllowed = self.safe_value(market, 'isSpotTradingAllowed') + active = False + if (status == '1') and (isSpotTradingAllowed): + active = True + isMarginTradingAllowed = self.safe_value(market, 'isMarginTradingAllowed') + makerCommission = self.safe_number(market, 'makerCommission') + takerCommission = self.safe_number(market, 'takerCommission') + maxQuoteAmount = self.safe_number(market, 'maxQuoteAmount') + result.append({ + 'id': id, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': isMarginTradingAllowed, + 'swap': False, + 'future': False, + 'option': False, + 'active': active, + 'contract': False, + 'linear': None, + 'inverse': None, + 'taker': takerCommission, + 'maker': makerCommission, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(self.parse_precision(self.safe_string(market, 'baseAssetPrecision'))), + 'price': self.parse_number(self.parse_precision(self.safe_string(market, 'quoteAssetPrecision'))), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(market, 'baseSizePrecision'), + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': self.safe_number(market, 'quoteAmountPrecision'), + 'max': maxQuoteAmount, + }, + }, + 'created': None, + 'info': market, + }) + return result + + def fetch_swap_markets(self, params={}): + """ + @ignore + retrieves data on all swap markets for mexc + + https://mexcdevelop.github.io/apidocs/contract_v1_en/#get-the-contract-information + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + currentRl: number = self.rateLimit + self.set_property(self, 'rateLimit', 10) # see comment: https://github.com/ccxt/ccxt/pull/23698 + response = self.contractPublicGetDetail(params) + self.set_property(self, 'rateLimit', currentRl) + # + # { + # "success":true, + # "code":0, + # "data":[ + # { + # "symbol":"BTC_USDT", + # "displayName":"BTC_USDT永续", + # "displayNameEn":"BTC_USDT SWAP", + # "positionOpenType":3, + # "baseCoin":"BTC", + # "quoteCoin":"USDT", + # "settleCoin":"USDT", + # "contractSize":0.0001, + # "minLeverage":1, + # "maxLeverage":125, + # "priceScale":2, # seems useless atm,'s just how UI shows the price, i.e. 29583.50 for BTC/USDT:USDT, while price ticksize is 0.5 + # "volScale":0, # probably: contract amount precision + # "amountScale":4, # probably: quote currency precision + # "priceUnit":0.5, # price tick size + # "volUnit":1, # probably: contract tick size + # "minVol":1, + # "maxVol":1000000, + # "bidLimitPriceRate":0.1, + # "askLimitPriceRate":0.1, + # "takerFeeRate":0.0006, + # "makerFeeRate":0.0002, + # "maintenanceMarginRate":0.004, + # "initialMarginRate":0.008, + # "riskBaseVol":10000, + # "riskIncrVol":200000, + # "riskIncrMmr":0.004, + # "riskIncrImr":0.004, + # "riskLevelLimit":5, + # "priceCoefficientVariation":0.1, + # "indexOrigin":["BINANCE","GATEIO","HUOBI","MXC"], + # "state":0, # 0 enabled, 1 delivery, 2 completed, 3 offline, 4 pause + # "isNew":false, + # "isHot":true, + # "isHidden":false + # }, + # ] + # } + # + data = self.safe_value(response, 'data', []) + result = [] + for i in range(0, len(data)): + market = data[i] + id = self.safe_string(market, 'symbol') + baseId = self.safe_string(market, 'baseCoin') + quoteId = self.safe_string(market, 'quoteCoin') + settleId = self.safe_string(market, 'settleCoin') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + settle = self.safe_currency_code(settleId) + state = self.safe_string(market, 'state') + isLinear = quote == settle + result.append({ + 'id': id, + 'symbol': base + '/' + quote + ':' + settle, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': 'swap', + 'spot': False, + 'margin': False, + 'swap': True, + 'future': False, + 'option': False, + 'active': (state == '0'), + 'contract': True, + 'linear': isLinear, + 'inverse': not isLinear, + 'taker': self.safe_number(market, 'takerFeeRate'), + 'maker': self.safe_number(market, 'makerFeeRate'), + 'contractSize': self.safe_number(market, 'contractSize'), + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.safe_number(market, 'volUnit'), + 'price': self.safe_number(market, 'priceUnit'), + }, + 'limits': { + 'leverage': { + 'min': self.safe_number(market, 'minLeverage'), + 'max': self.safe_number(market, 'maxLeverage'), + }, + 'amount': { + 'min': self.safe_number(market, 'minVol'), + 'max': self.safe_number(market, 'maxVol'), + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + 'info': market, + }) + return result + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#order-book + https://mexcdevelop.github.io/apidocs/contract_v1_en/#get-the-contract-s-depth-information + + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['limit'] = limit + orderbook = None + if market['spot']: + response = self.spotPublicGetDepth(self.extend(request, params)) + # + # { + # "lastUpdateId": "744267132", + # "bids": [ + # ["40838.50","0.387864"], + # ["40837.95","0.008400"], + # ], + # "asks": [ + # ["40838.61","6.544908"], + # ["40838.88","0.498000"], + # ] + # } + # + spotTimestamp = self.safe_integer(response, 'timestamp') + orderbook = self.parse_order_book(response, symbol, spotTimestamp) + orderbook['nonce'] = self.safe_integer(response, 'lastUpdateId') + elif market['swap']: + response = self.contractPublicGetDepthSymbol(self.extend(request, params)) + # + # { + # "success":true, + # "code":0, + # "data":{ + # "asks":[ + # [3445.72,48379,1], + # [3445.75,34994,1], + # ], + # "bids":[ + # [3445.55,44081,1], + # [3445.51,24857,1], + # ], + # "version":2827730444, + # "timestamp":1634117846232 + # } + # } + # + data = self.safe_value(response, 'data') + timestamp = self.safe_integer(data, 'timestamp') + orderbook = self.parse_order_book(data, symbol, timestamp) + orderbook['nonce'] = self.safe_integer(data, 'version') + return orderbook + + def parse_bid_ask(self, bidask, priceKey: IndexType = 0, amountKey: IndexType = 1, countOrIdKey: IndexType = 2): + countKey = 2 + price = self.safe_number(bidask, priceKey) + amount = self.safe_number(bidask, amountKey) + count = self.safe_number(bidask, countKey) + if count is not None: + return [price, amount, count] + return [price, amount] + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#recent-trades-list + https://mexcdevelop.github.io/apidocs/spot_v3_en/#compressed-aggregate-trades-list + https://mexcdevelop.github.io/apidocs/contract_v1_en/#get-contract-transaction-data + + get the list of most recent trades for a particular symbol + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: *spot only* *since must be defined* the latest time in ms to fetch entries for + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['limit'] = limit + trades = None + if market['spot']: + until = self.safe_integer_n(params, ['endTime', 'until']) + if since is not None: + request['startTime'] = since + if until is None: + raise ArgumentsRequired(self.id + ' fetchTrades() requires an until parameter when since is provided') + if until is not None: + if since is None: + raise ArgumentsRequired(self.id + ' fetchTrades() requires a since parameter when until is provided') + request['endTime'] = until + method = self.safe_string(self.options, 'fetchTradesMethod', 'spotPublicGetAggTrades') + method = self.safe_string(params, 'method', method) # AggTrades, HistoricalTrades, Trades + params = self.omit(params, ['method']) + if method == 'spotPublicGetAggTrades': + trades = self.spotPublicGetAggTrades(self.extend(request, params)) + elif method == 'spotPublicGetHistoricalTrades': + trades = self.spotPublicGetHistoricalTrades(self.extend(request, params)) + elif method == 'spotPublicGetTrades': + trades = self.spotPublicGetTrades(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchTrades() not support self method') + # + # /trades, /historicalTrades + # + # [ + # { + # "id": null, + # "price": "40798.94", + # "qty": "0.000508", + # "quoteQty": "20.72586152", + # "time": "1647546934374", + # "isBuyerMaker": True, + # "isBestMatch": True + # }, + # ] + # + # /aggrTrades + # + # [ + # { + # "a": null, + # "f": null, + # "l": null, + # "p": "40679", + # "q": "0.001309", + # "T": 1647551328000, + # "m": True, + # "M": True + # }, + # ] + # + elif market['swap']: + response = self.contractPublicGetDealsSymbol(self.extend(request, params)) + # + # { + # "success": True, + # "code": 0, + # "data": [ + # { + # "p": 31199, + # "v": 18, + # "T": 1, + # "O": 3, + # "M": 2, + # "t": 1609831235985 + # }, + # ] + # } + # + trades = self.safe_value(response, 'data') + return self.parse_trades(trades, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + id = None + timestamp = None + orderId = None + symbol = None + fee = None + type = None + side = None + takerOrMaker = None + priceString = None + amountString = None + costString = None + # if swap + if 'v' in trade: + # + # swap: fetchTrades + # + # { + # "p": 31199, + # "v": 18, + # "T": 1, + # "O": 3, + # "M": 2, + # "t": 1609831235985 + # } + # + timestamp = self.safe_integer(trade, 't') + market = self.safe_market(None, market) + symbol = market['symbol'] + priceString = self.safe_string(trade, 'p') + amountString = self.safe_string(trade, 'v') + side = self.parse_order_side(self.safe_string(trade, 'T')) + takerOrMaker = 'taker' + else: + # + # spot: fetchTrades(for aggTrades) + # + # { + # "a": null, + # "f": null, + # "l": null, + # "p": "40679", + # "q": "0.001309", + # "T": 1647551328000, + # "m": True, + # "M": True + # } + # + # spot: fetchMyTrades, fetchOrderTrades + # + # { + # "symbol": "BTCUSDT", + # "id": "133948532984922113", + # "orderId": "133948532531949568", + # "orderListId": "-1", + # "price": "41995.51", + # "qty": "0.0002", + # "quoteQty": "8.399102", + # "commission": "0.016798204", + # "commissionAsset": "USDT", + # "time": "1647718055000", + # "isBuyer": True, + # "isMaker": False, + # "isBestMatch": True + # } + # + # swap: fetchMyTrades, fetchOrderTrades + # + # { + # "id": "299444585", + # "symbol": "STEPN_USDT", + # "side": "1", + # "vol": "1", + # "price": "2.45455", + # "feeCurrency": "USDT", + # "fee": "0.00147273", + # "timestamp": "1648924557000", + # "profit": "0", + # "category": "1", + # "orderId": "265307163526610432", + # "positionMode": "1", + # "taker": True + # } + # + marketId = self.safe_string(trade, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + id = self.safe_string_2(trade, 'id', 'a') + priceString = self.safe_string_2(trade, 'price', 'p') + orderId = self.safe_string(trade, 'orderId') + # if swap + if 'positionMode' in trade: + timestamp = self.safe_integer(trade, 'timestamp') + amountString = self.safe_string(trade, 'vol') + side = self.parse_order_side(self.safe_string(trade, 'side')) + fee = { + 'cost': self.safe_string(trade, 'fee'), + 'currency': self.safe_currency_code(self.safe_string(trade, 'feeCurrency')), + } + takerOrMaker = 'taker' if self.safe_value(trade, 'taker') else 'maker' + else: + timestamp = self.safe_integer_2(trade, 'time', 'T') + amountString = self.safe_string_2(trade, 'qty', 'q') + costString = self.safe_string(trade, 'quoteQty') + isBuyer = self.safe_value(trade, 'isBuyer') + isMaker = self.safe_value(trade, 'isMaker') + buyerMaker = self.safe_value_2(trade, 'isBuyerMaker', 'm') + if isMaker is not None: + takerOrMaker = 'maker' if isMaker else 'taker' + if isBuyer is not None: + side = 'buy' if isBuyer else 'sell' + if buyerMaker is not None: + side = 'sell' if buyerMaker else 'buy' + takerOrMaker = 'taker' + feeAsset = self.safe_string(trade, 'commissionAsset') + if feeAsset is not None: + fee = { + 'cost': self.safe_string(trade, 'commission'), + 'currency': self.safe_currency_code(feeAsset), + } + if id is None and self.safe_bool(self.options, 'useCcxtTradeId', True): + id = self.create_ccxt_trade_id(timestamp, side, amountString, priceString, takerOrMaker) + return self.safe_trade({ + 'id': id, + 'order': orderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'type': type, + 'side': side, + 'takerOrMaker': takerOrMaker, + 'price': priceString, + 'amount': amountString, + 'cost': costString, + 'fee': fee, + 'info': trade, + }, market) + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#kline-candlestick-data + https://mexcdevelop.github.io/apidocs/contract_v1_en/#k-line-data + + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest candle to fetch + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + market = self.market(symbol) + maxLimit = 1000 if (market['spot']) else 2000 + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate', False) + if paginate: + return self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params, maxLimit) + options = self.safe_value(self.options, 'timeframes', {}) + timeframes = self.safe_value(options, market['type'], {}) + timeframeValue = self.safe_string(timeframes, timeframe) + duration = self.parse_timeframe(timeframe) * 1000 + request: dict = { + 'symbol': market['id'], + 'interval': timeframeValue, + } + candles = None + if market['spot']: + until = self.safe_integer_n(params, ['until', 'endTime']) + if since is not None: + request['startTime'] = since + if until is None: + # we have to calculate it assuming we can get at most 2000 entries per request + end = self.sum(since, maxLimit * duration) + now = self.milliseconds() + request['endTime'] = min(end, now) + if limit is not None: + request['limit'] = limit + if until is not None: + params = self.omit(params, ['until']) + request['endTime'] = until + response = self.spotPublicGetKlines(self.extend(request, params)) + # + # [ + # [ + # 1640804880000, + # "47482.36", + # "47482.36", + # "47416.57", + # "47436.1", + # "3.550717", + # 1640804940000, + # "168387.3" + # ], + # ] + # + candles = response + elif market['swap']: + until = self.safe_integer_product_n(params, ['until', 'endTime'], 0.001) + if since is not None: + request['start'] = self.parse_to_int(since / 1000) + if until is not None: + params = self.omit(params, ['until']) + request['end'] = until + priceType = self.safe_string(params, 'price', 'default') + params = self.omit(params, 'price') + response = None + if priceType == 'default': + response = self.contractPublicGetKlineSymbol(self.extend(request, params)) + elif priceType == 'index': + response = self.contractPublicGetKlineIndexPriceSymbol(self.extend(request, params)) + elif priceType == 'mark': + response = self.contractPublicGetKlineFairPriceSymbol(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchOHLCV() not support self price type, [default, index, mark]') + # + # { + # "success":true, + # "code":0, + # "data":{ + # "time":[1634052300,1634052360,1634052420], + # "open":[3492.2,3491.3,3495.65], + # "close":[3491.3,3495.65,3495.2], + # "high":[3495.85,3496.55,3499.4], + # "low":[3491.15,3490.9,3494.2], + # "vol":[1740.0,351.0,314.0], + # "amount":[60793.623,12260.4885,10983.1375], + # } + # } + # + data = self.safe_value(response, 'data') + candles = self.convert_trading_view_to_ohlcv(data, 'time', 'open', 'high', 'low', 'close', 'vol') + return self.parse_ohlcvs(candles, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + return [ + self.safe_integer(ohlcv, 0), + self.safe_number(ohlcv, 1), + self.safe_number(ohlcv, 2), + self.safe_number(ohlcv, 3), + self.safe_number(ohlcv, 4), + self.safe_number(ohlcv, 5), + ] + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#24hr-ticker-price-change-statistics + https://mexcdevelop.github.io/apidocs/contract_v1_en/#get-contract-trend-data + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + request: dict = {} + market = None + isSingularMarket = False + if symbols is not None: + length = len(symbols) + isSingularMarket = length == 1 + firstSymbol = self.safe_string(symbols, 0) + market = self.market(firstSymbol) + marketType, query = self.handle_market_type_and_params('fetchTickers', market, params) + tickers = None + if isSingularMarket: + request['symbol'] = market['id'] + if marketType == 'spot': + tickers = self.spotPublicGetTicker24hr(self.extend(request, query)) + # + # [ + # { + # "symbol": "BTCUSDT", + # "priceChange": "184.34", + # "priceChangePercent": "0.00400048", + # "prevClosePrice": "46079.37", + # "lastPrice": "46263.71", + # "lastQty": "", + # "bidPrice": "46260.38", + # "bidQty": "", + # "askPrice": "46260.41", + # "askQty": "", + # "openPrice": "46079.37", + # "highPrice": "47550.01", + # "lowPrice": "45555.5", + # "volume": "1732.461487", + # "quoteVolume": null, + # "openTime": 1641349500000, + # "closeTime": 1641349582808, + # "count": null + # } + # ] + # + elif marketType == 'swap': + response = self.contractPublicGetTicker(self.extend(request, query)) + # + # { + # "success":true, + # "code":0, + # "data":[ + # { + # "symbol":"ETH_USDT", + # "lastPrice":3581.3, + # "bid1":3581.25, + # "ask1":3581.5, + # "volume24":4045530, + # "amount24":141331823.5755, + # "holdVol":5832946, + # "lower24Price":3413.4, + # "high24Price":3588.7, + # "riseFallRate":0.0275, + # "riseFallValue":95.95, + # "indexPrice":3580.7852, + # "fairPrice":3581.08, + # "fundingRate":0.000063, + # "maxBidPrice":3938.85, + # "minAskPrice":3222.7, + # "timestamp":1634162885016 + # }, + # ] + # } + # + tickers = self.safe_value(response, 'data', []) + # when it's single symbol request, the returned structure is different(singular object) for both spot & swap, thus we need to wrap inside array + if isSingularMarket: + tickers = [tickers] + return self.parse_tickers(tickers, symbols) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#24hr-ticker-price-change-statistics + https://mexcdevelop.github.io/apidocs/contract_v1_en/#get-contract-trend-data + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + marketType, query = self.handle_market_type_and_params('fetchTicker', market, params) + ticker = None + request: dict = { + 'symbol': market['id'], + } + if marketType == 'spot': + ticker = self.spotPublicGetTicker24hr(self.extend(request, query)) + # + # { + # "symbol": "BTCUSDT", + # "priceChange": "184.34", + # "priceChangePercent": "0.00400048", + # "prevClosePrice": "46079.37", + # "lastPrice": "46263.71", + # "lastQty": "", + # "bidPrice": "46260.38", + # "bidQty": "", + # "askPrice": "46260.41", + # "askQty": "", + # "openPrice": "46079.37", + # "highPrice": "47550.01", + # "lowPrice": "45555.5", + # "volume": "1732.461487", + # "quoteVolume": null, + # "openTime": 1641349500000, + # "closeTime": 1641349582808, + # "count": null + # } + # + elif marketType == 'swap': + response = self.contractPublicGetTicker(self.extend(request, query)) + # + # { + # "success":true, + # "code":0, + # "data":{ + # "symbol":"ETH_USDT", + # "lastPrice":3581.3, + # "bid1":3581.25, + # "ask1":3581.5, + # "volume24":4045530, + # "amount24":141331823.5755, + # "holdVol":5832946, + # "lower24Price":3413.4, + # "high24Price":3588.7, + # "riseFallRate":0.0275, + # "riseFallValue":95.95, + # "indexPrice":3580.7852, + # "fairPrice":3581.08, + # "fundingRate":0.000063, + # "maxBidPrice":3938.85, + # "minAskPrice":3222.7, + # "timestamp":1634162885016 + # } + # } + # + ticker = self.safe_value(response, 'data', {}) + # when it's single symbol request, the returned structure is different(singular object) for both spot & swap, thus we need to wrap inside array + return self.parse_ticker(ticker, market) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + marketId = self.safe_string(ticker, 'symbol') + market = self.safe_market(marketId, market) + timestamp = None + bid = None + ask = None + bidVolume = None + askVolume = None + baseVolume = None + quoteVolume = None + open = None + high = None + low = None + changePcnt = None + changeValue = None + prevClose = None + isSwap = self.safe_value(market, 'swap') + # if swap + if isSwap or ('timestamp' in ticker): + # + # { + # "symbol": "ETH_USDT", + # "lastPrice": 3581.3, + # "bid1": 3581.25, + # "ask1": 3581.5, + # "volume24": 4045530, + # "amount24": 141331823.5755, + # "holdVol": 5832946, + # "lower24Price": 3413.4, + # "high24Price": 3588.7, + # "riseFallRate": 0.0275, + # "riseFallValue": 95.95, + # "indexPrice": 3580.7852, + # "fairPrice": 3581.08, + # "fundingRate": 0.000063, + # "maxBidPrice": 3938.85, + # "minAskPrice": 3222.7, + # "timestamp": 1634162885016 + # } + # + timestamp = self.safe_integer(ticker, 'timestamp') + bid = self.safe_string(ticker, 'bid1') + ask = self.safe_string(ticker, 'ask1') + baseVolume = self.safe_string(ticker, 'volume24') + quoteVolume = self.safe_string(ticker, 'amount24') + high = self.safe_string(ticker, 'high24Price') + low = self.safe_string(ticker, 'lower24Price') + changeValue = self.safe_string(ticker, 'riseFallValue') + changePcnt = self.safe_string(ticker, 'riseFallRate') + changePcnt = Precise.string_mul(changePcnt, '100') + else: + # + # { + # "symbol": "BTCUSDT", + # "priceChange": "184.34", + # "priceChangePercent": "0.00400048", + # "prevClosePrice": "46079.37", + # "lastPrice": "46263.71", + # "lastQty": "", + # "bidPrice": "46260.38", + # "bidQty": "", + # "askPrice": "46260.41", + # "askQty": "", + # "openPrice": "46079.37", + # "highPrice": "47550.01", + # "lowPrice": "45555.5", + # "volume": "1732.461487", + # "quoteVolume": null, + # "openTime": 1641349500000, + # "closeTime": 1641349582808, + # "count": null + # } + # + timestamp = self.safe_integer(ticker, 'closeTime') + bid = self.safe_string(ticker, 'bidPrice') + ask = self.safe_string(ticker, 'askPrice') + bidVolume = self.safe_string(ticker, 'bidQty') + askVolume = self.safe_string(ticker, 'askQty') + if Precise.string_eq(bidVolume, '0'): + bidVolume = None + if Precise.string_eq(askVolume, '0'): + askVolume = None + baseVolume = self.safe_string(ticker, 'volume') + quoteVolume = self.safe_string(ticker, 'quoteVolume') + open = self.safe_string(ticker, 'openPrice') + high = self.safe_string(ticker, 'highPrice') + low = self.safe_string(ticker, 'lowPrice') + prevClose = self.safe_string(ticker, 'prevClosePrice') + changeValue = self.safe_string(ticker, 'priceChange') + changePcnt = self.safe_string(ticker, 'priceChangePercent') + changePcnt = Precise.string_mul(changePcnt, '100') + return self.safe_ticker({ + 'symbol': market['symbol'], + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'open': open, + 'high': high, + 'low': low, + 'close': self.safe_string(ticker, 'lastPrice'), + 'bid': bid, + 'bidVolume': bidVolume, + 'ask': ask, + 'askVolume': askVolume, + 'vwap': None, + 'previousClose': prevClose, + 'change': changeValue, + 'percentage': changePcnt, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'info': ticker, + }, market) + + def fetch_bids_asks(self, symbols: Strings = None, params={}): + """ + fetches the bid and ask price and volume for multiple markets + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#symbol-order-book-ticker + + :param str[]|None symbols: unified symbols of the markets to fetch the bids and asks for, all markets are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + market = None + isSingularMarket = False + if symbols is not None: + length = len(symbols) + isSingularMarket = length == 1 + market = self.market(symbols[0]) + marketType, query = self.handle_market_type_and_params('fetchBidsAsks', market, params) + tickers = None + if marketType == 'spot': + tickers = self.spotPublicGetTickerBookTicker(query) + # + # [ + # { + # "symbol": "AEUSDT", + # "bidPrice": "0.11001", + # "bidQty": "115.59", + # "askPrice": "0.11127", + # "askQty": "215.48" + # }, + # ] + # + elif marketType == 'swap': + raise NotSupported(self.id + ' fetchBidsAsks() is not available for ' + marketType + ' markets') + # when it's single symbol request, the returned structure is different(singular object) for both spot & swap, thus we need to wrap inside array + if isSingularMarket: + tickers = [tickers] + return self.parse_tickers(tickers, symbols) + + def create_market_buy_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market buy order by providing the symbol and cost + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#new-order + + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' createMarketBuyOrderWithCost() supports spot orders only') + req = { + 'cost': cost, + } + return self.create_order(symbol, 'market', 'buy', 0, None, self.extend(req, params)) + + def create_market_sell_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market sell order by providing the symbol and cost + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#new-order + + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' createMarketBuyOrderWithCost() supports spot orders only') + req = { + 'cost': cost, + } + return self.create_order(symbol, 'market', 'sell', 0, None, self.extend(req, params)) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#new-order + https://mexcdevelop.github.io/apidocs/contract_v1_en/#order-under-maintenance + https://mexcdevelop.github.io/apidocs/contract_v1_en/#trigger-order-under-maintenance + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: only 'isolated' is supported for spot-margin trading + :param float [params.triggerPrice]: The price at which a trigger order is triggered at + :param bool [params.postOnly]: if True, the order will only be posted if it will be a maker order + :param bool [params.reduceOnly]: *contract only* indicates if self order is to reduce the size of a position + :param bool [params.hedged]: *swap only* True for hedged mode, False for one way mode, default is False + :param str [params.timeInForce]: 'IOC' or 'FOK', default is 'GTC' + EXCHANGE SPECIFIC PARAMETERS + :param int [params.leverage]: *contract only* leverage is necessary on isolated margin + :param long [params.positionId]: *contract only* it is recommended to hasattr(self, fill) parameter when closing a position + :param str [params.externalOid]: *contract only* external order ID + :param int [params.positionMode]: *contract only* 1:hedge, 2:one-way, default: the user's current config + :param boolean [params.test]: *spot only* whether to use the test endpoint or not, default is False + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + marginMode, query = self.handle_margin_mode_and_params('createOrder', params) + if market['spot']: + return self.create_spot_order(market, type, side, amount, price, marginMode, query) + else: + return self.create_swap_order(market, type, side, amount, price, marginMode, query) + + def create_spot_order_request(self, market, type, side, amount, price=None, marginMode=None, params={}): + symbol = market['symbol'] + orderSide = side.upper() + request: dict = { + 'symbol': market['id'], + 'side': orderSide, + 'type': type.upper(), + } + if type == 'market': + cost = self.safe_number_2(params, 'cost', 'quoteOrderQty') + params = self.omit(params, 'cost') + if cost is not None: + amount = cost + request['quoteOrderQty'] = self.cost_to_precision(symbol, amount) + else: + if price is None: + request['quantity'] = self.amount_to_precision(symbol, amount) + else: + amountString = self.number_to_string(amount) + priceString = self.number_to_string(price) + quoteAmount = Precise.string_mul(amountString, priceString) + amount = quoteAmount + request['quoteOrderQty'] = self.cost_to_precision(symbol, amount) + else: + request['quantity'] = self.amount_to_precision(symbol, amount) + if price is not None: + request['price'] = self.price_to_precision(symbol, price) + clientOrderId = self.safe_string(params, 'clientOrderId') + if clientOrderId is not None: + request['newClientOrderId'] = clientOrderId + params = self.omit(params, ['type', 'clientOrderId']) + if marginMode is not None: + if marginMode != 'isolated': + raise BadRequest(self.id + ' createOrder() does not support marginMode ' + marginMode + ' for spot-margin trading') + postOnly = None + postOnly, params = self.handle_post_only(type == 'market', type == 'LIMIT_MAKER', params) + if postOnly: + request['type'] = 'LIMIT_MAKER' + tif = self.safe_string(params, 'timeInForce') + if tif is not None: + params = self.omit(params, 'timeInForce') + if tif == 'IOC': + request['type'] = 'IMMEDIATE_OR_CANCEL' + elif tif == 'FOK': + request['type'] = 'FILL_OR_KILL' + return self.extend(request, params) + + def create_spot_order(self, market, type, side, amount, price=None, marginMode=None, params={}): + """ + @ignore + create a trade order + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#new-order + + :param str market: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param str [marginMode]: only 'isolated' is supported for spot-margin trading + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.postOnly]: if True, the order will only be posted if it will be a maker order + :returns dict: an `order structure ` + """ + self.load_markets() + test = self.safe_bool(params, 'test', False) + params = self.omit(params, 'test') + request = self.create_spot_order_request(market, type, side, amount, price, marginMode, params) + response = None + if test: + response = self.spotPrivatePostOrderTest(request) + else: + response = self.spotPrivatePostOrder(request) + # + # spot + # + # { + # "symbol": "BTCUSDT", + # "orderId": "123738410679123456", + # "orderListId": -1 + # } + # + # margin + # + # { + # "symbol": "BTCUSDT", + # "orderId": "762634301354414080", + # "clientOrderId": null, + # "isIsolated": True, + # "transactTime": 1661992652132 + # } + # + order = self.parse_order(response, market) + order['side'] = side + order['type'] = type + if self.safe_string(order, 'price') is None: + order['price'] = price + if self.safe_string(order, 'amount') is None: + order['amount'] = amount + return order + + def create_swap_order(self, market, type, side, amount, price=None, marginMode=None, params={}): + """ + @ignore + create a trade order + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#new-order + https://mexcdevelop.github.io/apidocs/contract_v1_en/#order-under-maintenance + https://mexcdevelop.github.io/apidocs/contract_v1_en/#trigger-order-under-maintenance + + :param str market: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param str [marginMode]: only 'isolated' is supported for spot-margin trading + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: The price at which a trigger order is triggered at + :param bool [params.postOnly]: if True, the order will only be posted if it will be a maker order + :param bool [params.reduceOnly]: indicates if self order is to reduce the size of a position + :param bool [params.hedged]: *swap only* True for hedged mode, False for one way mode, default is False + + EXCHANGE SPECIFIC PARAMETERS + :param int [params.leverage]: leverage is necessary on isolated margin + :param long [params.positionId]: it is recommended to hasattr(self, fill) parameter when closing a position + :param str [params.externalOid]: external order ID + :param int [params.positionMode]: 1:hedge, 2:one-way, default: the user's current config + :returns dict: an `order structure ` + """ + self.load_markets() + symbol = market['symbol'] + unavailableContracts = self.safe_value(self.options, 'unavailableContracts', {}) + isContractUnavaiable = self.safe_bool(unavailableContracts, symbol, False) + if isContractUnavaiable: + raise NotSupported(self.id + ' createSwapOrder() does not support yet self symbol:' + symbol) + openType = None + if marginMode is not None: + if marginMode == 'cross': + openType = 2 + elif marginMode == 'isolated': + openType = 1 + else: + raise ArgumentsRequired(self.id + ' createSwapOrder() marginMode parameter should be either "cross" or "isolated"') + else: + openType = self.safe_integer(params, 'openType', 2) # defaulting to cross margin + if (type != 'limit') and (type != 'market') and (type != 1) and (type != 2) and (type != 3) and (type != 4) and (type != 5) and (type != 6): + raise InvalidOrder(self.id + ' createSwapOrder() order type must either limit, market, or 1 for limit orders, 2 for post-only orders, 3 for IOC orders, 4 for FOK orders, 5 for market orders or 6 to convert market price to current price') + postOnly = None + postOnly, params = self.handle_post_only(type == 'market', type == 2, params) + if postOnly: + type = 2 + elif type == 'limit': + type = 1 + elif type == 'market': + type = 6 + request: dict = { + 'symbol': market['id'], + # 'price': float(self.price_to_precision(symbol, price)), + 'vol': float(self.amount_to_precision(symbol, amount)), + # 'leverage': int, # required for isolated margin + # 'side': side, # 1 open long, 2 close short, 3 open short, 4 close long + # + # supported order types + # + # 1 limit + # 2 post only maker(PO) + # 3 transact or cancel instantly(IOC) + # 4 transact completely or cancel completely(FOK) + # 5 market orders + # 6 convert market price to current price + # + 'type': type, + 'openType': openType, # 1 isolated, 2 cross + # 'positionId': 1394650, # long, hasattr(self, filling) parameter when closing a position is recommended + # 'externalOid': clientOrderId, + # 'triggerPrice': 10.0, # Required for trigger order + # 'triggerType': 1, # Required for trigger order 1: more than or equal, 2: less than or equal + # 'executeCycle': 1, # Required for trigger order 1: 24 hours,2: 7 days + # 'trend': 1, # Required for trigger order 1: latest price, 2: fair price, 3: index price + # 'orderType': 1, # Required for trigger order 1: limit order,2:Post Only Maker,3: close or cancel instantly ,4: close or cancel completely,5: Market order + } + if (type != 5) and (type != 6) and (type != 'market'): + request['price'] = float(self.price_to_precision(symbol, price)) + if openType == 1: + leverage = self.safe_integer(params, 'leverage') + if leverage is None: + raise ArgumentsRequired(self.id + ' createSwapOrder() requires a leverage parameter for isolated margin orders') + reduceOnly = self.safe_bool(params, 'reduceOnly', False) + hedged = self.safe_bool(params, 'hedged', False) + sideInteger = None + if hedged: + if reduceOnly: + params = self.omit(params, 'reduceOnly') # hedged mode does not accept self parameter + side = 'sell' if (side == 'buy') else 'buy' + sideInteger = 1 if (side == 'buy') else 3 + request['positionMode'] = 1 + else: + if reduceOnly: + sideInteger = 2 if (side == 'buy') else 4 + else: + sideInteger = 1 if (side == 'buy') else 3 + request['side'] = sideInteger + clientOrderId = self.safe_string_2(params, 'clientOrderId', 'externalOid') + if clientOrderId is not None: + request['externalOid'] = clientOrderId + triggerPrice = self.safe_number_2(params, 'triggerPrice', 'stopPrice') + params = self.omit(params, ['clientOrderId', 'externalOid', 'postOnly', 'stopPrice', 'triggerPrice', 'hedged']) + response = None + if triggerPrice: + request['triggerPrice'] = self.price_to_precision(symbol, triggerPrice) + request['triggerType'] = self.safe_integer(params, 'triggerType', 1) + request['executeCycle'] = self.safe_integer(params, 'executeCycle', 1) + request['trend'] = self.safe_integer(params, 'trend', 1) + request['orderType'] = self.safe_integer(params, 'orderType', 1) + response = self.contractPrivatePostPlanorderPlace(self.extend(request, params)) + else: + response = self.contractPrivatePostOrderSubmit(self.extend(request, params)) + # + # Swap + # {"code":200,"data":"2ff3163e8617443cb9c6fc19d42b1ca4"} + # + # Trigger + # {"success":true,"code":0,"data":259208506303929856} + # + data = self.safe_string(response, 'data') + return self.safe_order({'id': data}, market) + + def create_orders(self, orders: List[OrderRequest], params={}): + """ + *spot only* *all orders must have the same symbol* create a list of trade orders + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#batch-orders + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to api endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + ordersRequests = [] + symbol = None + for i in range(0, len(orders)): + rawOrder = orders[i] + marketId = self.safe_string(rawOrder, 'symbol') + market = self.market(marketId) + if not market['spot']: + raise NotSupported(self.id + ' createOrders() is only supported for spot markets') + if symbol is None: + symbol = marketId + else: + if symbol != marketId: + raise BadRequest(self.id + ' createOrders() requires all orders to have the same symbol') + type = self.safe_string(rawOrder, 'type') + side = self.safe_string(rawOrder, 'side') + amount = self.safe_value(rawOrder, 'amount') + price = self.safe_value(rawOrder, 'price') + orderParams = self.safe_value(rawOrder, 'params', {}) + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('createOrder', params) + orderRequest = self.create_spot_order_request(market, type, side, amount, price, marginMode, orderParams) + ordersRequests.append(orderRequest) + request: dict = { + 'batchOrders': self.json(ordersRequests), + } + response = self.spotPrivatePostBatchOrders(request) + # + # [ + # { + # "symbol": "BTCUSDT", + # "orderId": "1196315350023612316", + # "newClientOrderId": "hio8279hbdsds", + # "orderListId": -1 + # }, + # { + # "newClientOrderId": "123456", + # "msg": "The minimum transaction volume cannot be less than:0.5USDT", + # "code": 30002 + # }, + # { + # "symbol": "BTCUSDT", + # "orderId": "1196315350023612318", + # "orderListId": -1 + # } + # ] + # + return self.parse_orders(response) + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#query-order + https://mexcdevelop.github.io/apidocs/contract_v1_en/#query-the-order-based-on-the-order-number + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: only 'isolated' is supported, for spot-margin trading + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrder() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + data = None + if market['spot']: + clientOrderId = self.safe_string(params, 'clientOrderId') + if clientOrderId is not None: + params = self.omit(params, 'clientOrderId') + request['origClientOrderId'] = clientOrderId + else: + request['orderId'] = id + marginMode, query = self.handle_margin_mode_and_params('fetchOrder', params) + if marginMode is not None: + if marginMode != 'isolated': + raise BadRequest(self.id + ' fetchOrder() does not support marginMode ' + marginMode + ' for spot-margin trading') + data = self.spotPrivateGetMarginOrder(self.extend(request, query)) + else: + data = self.spotPrivateGetOrder(self.extend(request, query)) + # + # spot + # + # { + # "symbol": "BTCUSDT", + # "orderId": "133734823834147272", + # "orderListId": "-1", + # "clientOrderId": null, + # "price": "30000", + # "origQty": "0.0002", + # "executedQty": "0", + # "cummulativeQuoteQty": "0", + # "status": "CANCELED", + # "timeInForce": null, + # "type": "LIMIT", + # "side": "BUY", + # "stopPrice": null, + # "icebergQty": null, + # "time": "1647667102000", + # "updateTime": "1647708567000", + # "isWorking": True, + # "origQuoteOrderQty": "6" + # } + # + # margin + # + # { + # "symbol": "BTCUSDT", + # "orderId": "763307297891028992", + # "orderListId": "-1", + # "clientOrderId": null, + # "price": "18000", + # "origQty": "0.0014", + # "executedQty": "0", + # "cummulativeQuoteQty": "0", + # "status": "NEW", + # "type": "LIMIT", + # "side": "BUY", + # "isIsolated": True, + # "isWorking": True, + # "time": 1662153107000, + # "updateTime": 1662153107000 + # } + # + elif market['swap']: + request['order_id'] = id + response = self.contractPrivateGetOrderGetOrderId(self.extend(request, params)) + # + # { + # "success": True, + # "code": "0", + # "data": { + # "orderId": "264995729269765120", + # "symbol": "STEPN_USDT", + # "positionId": "0", + # "price": "2.2", + # "vol": "15", + # "leverage": "20", + # "side": "1", + # "category": "1", + # "orderType": "1", + # "dealAvgPrice": "0", + # "dealVol": "0", + # "orderMargin": "2.2528", + # "takerFee": "0", + # "makerFee": "0", + # "profit": "0", + # "feeCurrency": "USDT", + # "openType": "1", + # "state": "2", + # "externalOid": "_m_0e9520c256744d64b942985189026d20", + # "errorCode": "0", + # "usedMargin": "0", + # "createTime": "1648850305236", + # "updateTime": "1648850305245", + # "positionMode": "1" + # } + # } + # + data = self.safe_value(response, 'data') + return self.parse_order(data, market) + + def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#all-orders + https://mexcdevelop.github.io/apidocs/contract_v1_en/#get-all-of-the-user-39-s-historical-orders + https://mexcdevelop.github.io/apidocs/contract_v1_en/#gets-the-trigger-order-list + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch orders for + :param str [params.marginMode]: only 'isolated' is supported, for spot-margin trading + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + until = self.safe_integer(params, 'until') + params = self.omit(params, 'until') + marketType, query = self.handle_market_type_and_params('fetchOrders', market, params) + if marketType == 'spot': + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrders() requires a symbol argument for spot market') + marginMode, queryInner = self.handle_margin_mode_and_params('fetchOrders', params) + if since is not None: + request['startTime'] = since + if until is not None: + request['endTime'] = until + if limit is not None: + request['limit'] = limit + response = None + if marginMode is not None: + if marginMode != 'isolated': + raise BadRequest(self.id + ' fetchOrders() does not support marginMode ' + marginMode + ' for spot-margin trading') + response = self.spotPrivateGetMarginAllOrders(self.extend(request, queryInner)) + else: + response = self.spotPrivateGetAllOrders(self.extend(request, queryInner)) + # + # spot + # + # [ + # { + # "symbol": "BTCUSDT", + # "orderId": "133949373632483328", + # "orderListId": "-1", + # "clientOrderId": null, + # "price": "45000", + # "origQty": "0.0002", + # "executedQty": "0", + # "cummulativeQuoteQty": "0", + # "status": "NEW", + # "timeInForce": null, + # "type": "LIMIT", + # "side": "SELL", + # "stopPrice": null, + # "icebergQty": null, + # "time": "1647718255000", + # "updateTime": "1647718255000", + # "isWorking": True, + # "origQuoteOrderQty": "9" + # }, + # ] + # + # margin + # + # [ + # { + # "symbol": "BTCUSDT", + # "orderId": "763307297891028992", + # "orderListId": "-1", + # "clientOrderId": null, + # "price": "18000", + # "origQty": "0.0014", + # "executedQty": "0", + # "cummulativeQuoteQty": "0", + # "status": "NEW", + # "type": "LIMIT", + # "side": "BUY", + # "isIsolated": True, + # "isWorking": True, + # "time": 1662153107000, + # "updateTime": 1662153107000 + # } + # ] + # + return self.parse_orders(response, market, since, limit) + else: + if since is not None: + request['start_time'] = since + end = self.safe_integer(params, 'end_time', until) + if end is None: + request['end_time'] = self.sum(since, self.options['maxTimeTillEnd']) + else: + if (end - since) > self.options['maxTimeTillEnd']: + raise BadRequest(self.id + ' end is invalid, i.e. exceeds allowed 90 days.') + else: + request['end_time'] = until + elif until is not None: + request['start_time'] = self.sum(until, self.options['maxTimeTillEnd'] * -1) + request['end_time'] = until + if limit is not None: + request['page_size'] = limit + method = self.safe_string(self.options, 'fetchOrders', 'contractPrivateGetOrderListHistoryOrders') + method = self.safe_string(query, 'method', method) + ordersOfRegular = [] + ordersOfTrigger = [] + if method == 'contractPrivateGetOrderListHistoryOrders': + response = self.contractPrivateGetOrderListHistoryOrders(self.extend(request, query)) + # + # { + # "success": True, + # "code": "0", + # "data": [ + # { + # "orderId": "265230764677709315", + # "symbol": "STEPN_USDT", + # "positionId": "0", + # "price": "2.1", + # "vol": "102", + # "leverage": "20", + # "side": "1", + # "category": "1", + # "orderType": "1", + # "dealAvgPrice": "0", + # "dealVol": "0", + # "orderMargin": "10.96704", + # "takerFee": "0", + # "makerFee": "0", + # "profit": "0", + # "feeCurrency": "USDT", + # "openType": "1", + # "state": "2", + # "externalOid": "_m_7e42f8df6b324c869e4e200397e2b00f", + # "errorCode": "0", + # "usedMargin": "0", + # "createTime": "1648906342000", + # "updateTime": "1648906342000", + # "positionMode": "1" + # }, + # ] + # } + # + ordersOfRegular = self.safe_value(response, 'data') + else: + # the Planorder endpoints work not only for stop-market orders, but also for stop-limit orders that were supposed to have a separate endpoint + response = self.contractPrivateGetPlanorderListOrders(self.extend(request, query)) + # + # { + # "success": True, + # "code": "0", + # "data": [ + # { + # "symbol": "STEPN_USDT", + # "leverage": "20", + # "side": "1", + # "vol": "13", + # "openType": "1", + # "state": "1", + # "orderType": "1", + # "errorCode": "0", + # "createTime": "1648984276000", + # "updateTime": "1648984276000", + # "id": "265557643326564352", + # "triggerType": "1", + # "triggerPrice": "3", + # "price": "2.9", # not present in stop-market, but in stop-limit order + # "executeCycle": "87600", + # "trend": "1", + # }, + # ] + # } + # + ordersOfTrigger = self.safe_value(response, 'data') + merged = self.array_concat(ordersOfTrigger, ordersOfRegular) + return self.parse_orders(merged, market, since, limit, params) + + def fetch_orders_by_ids(self, ids, symbol: Str = None, params={}): + self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + marketType, query = self.handle_market_type_and_params('fetchOrdersByIds', market, params) + if marketType == 'spot': + raise BadRequest(self.id + ' fetchOrdersByIds() is not supported for ' + marketType) + else: + request['order_ids'] = ','.join(ids) + response = self.contractPrivateGetOrderBatchQuery(self.extend(request, query)) + # + # { + # "success": True, + # "code": "0", + # "data": [ + # { + # "orderId": "265230764677709315", + # "symbol": "STEPN_USDT", + # "positionId": "0", + # "price": "2.1", + # "vol": "102", + # "leverage": "20", + # "side": "1", + # "category": "1", + # "orderType": "1", + # "dealAvgPrice": "0", + # "dealVol": "0", + # "orderMargin": "10.96704", + # "takerFee": "0", + # "makerFee": "0", + # "profit": "0", + # "feeCurrency": "USDT", + # "openType": "1", + # "state": "2", + # "externalOid": "_m_7e42f8df6b324c869e4e200397e2b00f", + # "errorCode": "0", + # "usedMargin": "0", + # "createTime": "1648906342000", + # "updateTime": "1648906342000", + # "positionMode": "1" + # } + # ] + # } + # + data = self.safe_list(response, 'data') + return self.parse_orders(data, market) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#current-open-orders + https://mexcdevelop.github.io/apidocs/contract_v1_en/#get-all-of-the-user-39-s-historical-orders + https://mexcdevelop.github.io/apidocs/contract_v1_en/#gets-the-trigger-order-list + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: only 'isolated' is supported, for spot-margin trading + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + request: dict = {} + market = None + marketType = None + if symbol is not None: + market = self.market(symbol) + marketType, params = self.handle_market_type_and_params('fetchOpenOrders', market, params) + if marketType == 'spot': + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOpenOrders() requires a symbol argument for spot market') + request['symbol'] = market['id'] + marginMode, query = self.handle_margin_mode_and_params('fetchOpenOrders', params) + response = None + if marginMode is not None: + if marginMode != 'isolated': + raise BadRequest(self.id + ' fetchOpenOrders() does not support marginMode ' + marginMode + ' for spot-margin trading') + response = self.spotPrivateGetMarginOpenOrders(self.extend(request, query)) + else: + response = self.spotPrivateGetOpenOrders(self.extend(request, query)) + # + # spot + # + # [ + # { + # "symbol": "BTCUSDT", + # "orderId": "133949373632483328", + # "orderListId": "-1", + # "clientOrderId": "", + # "price": "45000", + # "origQty": "0.0002", + # "executedQty": "0", + # "cummulativeQuoteQty": "0", + # "status": "NEW", + # "timeInForce": null, + # "type": "LIMIT", + # "side": "SELL", + # "stopPrice": null, + # "icebergQty": null, + # "time": "1647718255199", + # "updateTime": null, + # "isWorking": True, + # "origQuoteOrderQty": "9" + # } + # ] + # + # margin + # + # [ + # { + # "symbol": "BTCUSDT", + # "orderId": "764547676405633024", + # "orderListId": "-1", + # "clientOrderId": null, + # "price": "18000", + # "origQty": "0.0013", + # "executedQty": "0", + # "cummulativeQuoteQty": "0", + # "status": "NEW", + # "type": "LIMIT", + # "side": "BUY", + # "isIsolated": True, + # "isWorking": True, + # "time": 1662448836000, + # "updateTime": 1662448836000 + # } + # ] + # + return self.parse_orders(response, market, since, limit) + else: + # TO_DO: another possible way is through: open_orders/{symbol}, but have same ratelimits, and less granularity, i think historical orders are more convenient, supports more params(however, theoretically, open-orders endpoint might be sligthly fast) + return self.fetch_orders_by_state(2, symbol, since, limit, params) + + def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#all-orders + https://mexcdevelop.github.io/apidocs/contract_v1_en/#get-all-of-the-user-39-s-historical-orders + https://mexcdevelop.github.io/apidocs/contract_v1_en/#gets-the-trigger-order-list + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + return self.fetch_orders_by_state(3, symbol, since, limit, params) + + def fetch_canceled_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches information on multiple canceled orders made by the user + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#all-orders + https://mexcdevelop.github.io/apidocs/contract_v1_en/#get-all-of-the-user-39-s-historical-orders + https://mexcdevelop.github.io/apidocs/contract_v1_en/#gets-the-trigger-order-list + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: timestamp in ms of the earliest order, default is None + :param int [limit]: max number of orders to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `order structures ` + """ + return self.fetch_orders_by_state(4, symbol, since, limit, params) + + def fetch_orders_by_state(self, state, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + marketType = self.handle_market_type_and_params('fetchOrdersByState', market, params) + if marketType == 'spot': + raise NotSupported(self.id + ' fetchOrdersByState() is not supported for ' + marketType) + else: + request['states'] = state + return self.fetch_orders(symbol, since, limit, self.extend(request, params)) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#cancel-order + https://mexcdevelop.github.io/apidocs/contract_v1_en/#cancel-the-order-under-maintenance + https://mexcdevelop.github.io/apidocs/contract_v1_en/#cancel-the-stop-limit-trigger-order-under-maintenance + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: only 'isolated' is supported for spot-margin trading + :returns dict: An `order structure ` + """ + self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + marketType = None + marketType, params = self.handle_market_type_and_params('cancelOrder', market, params) + marginMode, query = self.handle_margin_mode_and_params('cancelOrder', params) + data = None + if marketType == 'spot': + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + requestInner: dict = { + 'symbol': market['id'], + } + clientOrderId = self.safe_string(params, 'clientOrderId') + if clientOrderId is not None: + params = self.omit(query, 'clientOrderId') + requestInner['origClientOrderId'] = clientOrderId + else: + requestInner['orderId'] = id + if marginMode is not None: + if marginMode != 'isolated': + raise BadRequest(self.id + ' cancelOrder() does not support marginMode ' + marginMode + ' for spot-margin trading') + data = self.spotPrivateDeleteMarginOrder(self.extend(requestInner, query)) + else: + data = self.spotPrivateDeleteOrder(self.extend(requestInner, query)) + # + # spot + # + # { + # "symbol": "BTCUSDT", + # "orderId": "133734823834447872", + # "price": "30000", + # "origQty": "0.0002", + # "type": "LIMIT", + # "side": "BUY" + # } + # + # margin + # + # [ + # { + # "symbol": "BTCUSDT", + # "orderId": "762640232574226432", + # "orderListId": "-1", + # "clientOrderId": null, + # "price": "18000", + # "origQty": "0.00147", + # "executedQty": "0", + # "cummulativeQuoteQty": "0", + # "status": "NEW", + # "type": "LIMIT", + # "side": "BUY", + # "isIsolated": True, + # "isWorking": True, + # "time": 1661994066000, + # "updateTime": 1661994066000 + # } + # ] + # + else: + # TODO: PlanorderCancel endpoint has bug atm. waiting for fix. + method = self.safe_string(self.options, 'cancelOrder', 'contractPrivatePostOrderCancel') # contractPrivatePostOrderCancel, contractPrivatePostPlanorderCancel + method = self.safe_string(query, 'method', method) + response = None + if method == 'contractPrivatePostOrderCancel': + response = self.contractPrivatePostOrderCancel([id]) # the request cannot be changed or extended. This is the only way to send. + elif method == 'contractPrivatePostPlanorderCancel': + response = self.contractPrivatePostPlanorderCancel([id]) # the request cannot be changed or extended. This is the only way to send. + else: + raise NotSupported(self.id + ' cancelOrder() not support self method') + # + # { + # "success": True, + # "code": "0", + # "data": [ + # { + # "orderId": "264995729269765120", + # "errorCode": "0", # if already canceled: "2041"; if doesn't exist: "2040" + # "errorMsg": "success", # if already canceled: "order state cannot be cancelled"; if doesn't exist: "order not exist" + # } + # ] + # } + # + data = self.safe_value(response, 'data') + order = self.safe_value(data, 0) + errorMsg = self.safe_value(order, 'errorMsg', '') + if errorMsg != 'success': + raise InvalidOrder(self.id + ' cancelOrder() the order with id ' + id + ' cannot be cancelled: ' + errorMsg) + return self.parse_order(data, market) + + def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): + """ + cancel multiple orders + + https://mexcdevelop.github.io/apidocs/contract_v1_en/#cancel-the-order-under-maintenance + + :param str[] ids: order ids + :param str symbol: unified market symbol, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an list of `order structures ` + """ + self.load_markets() + market = self.market(symbol) if (symbol is not None) else None + marketType = self.handle_market_type_and_params('cancelOrders', market, params) + if marketType == 'spot': + raise BadRequest(self.id + ' cancelOrders() is not supported for ' + marketType) + else: + response = self.contractPrivatePostOrderCancel(ids) # the request cannot be changed or extended. The only way to send. + # + # { + # "success": True, + # "code": "0", + # "data": [ + # { + # "orderId": "264995729269765120", + # "errorCode": "0", # if already canceled: "2041" + # "errorMsg": "success", # if already canceled: "order state cannot be cancelled" + # }, + # ] + # } + # + data = self.safe_list(response, 'data') + return self.parse_orders(data, market) + + def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#cancel-all-open-orders-on-a-symbol + https://mexcdevelop.github.io/apidocs/contract_v1_en/#cancel-all-orders-under-a-contract-under-maintenance + https://mexcdevelop.github.io/apidocs/contract_v1_en/#cancel-all-trigger-orders-under-maintenance + + :param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: only 'isolated' is supported for spot-margin trading + :returns dict[]: a list of `order structures ` + """ + self.load_markets() + market = self.market(symbol) if (symbol is not None) else None + request: dict = {} + marketType = None + marketType, params = self.handle_market_type_and_params('cancelAllOrders', market, params) + marginMode, query = self.handle_margin_mode_and_params('cancelAllOrders', params) + if marketType == 'spot': + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelAllOrders() requires a symbol argument on spot') + request['symbol'] = market['id'] + response = None + if marginMode is not None: + if marginMode != 'isolated': + raise BadRequest(self.id + ' cancelAllOrders() does not support marginMode ' + marginMode + ' for spot-margin trading') + response = self.spotPrivateDeleteMarginOpenOrders(self.extend(request, query)) + else: + response = self.spotPrivateDeleteOpenOrders(self.extend(request, query)) + # + # spot + # + # [ + # { + # "symbol": "BTCUSDT", + # "orderId": "133926492139692032", + # "price": "30000", + # "origQty": "0.0002", + # "type": "LIMIT", + # "side": "BUY" + # }, + # ] + # + # margin + # + # [ + # { + # "symbol": "BTCUSDT", + # "orderId": "762640232574226432", + # "orderListId": "-1", + # "clientOrderId": null, + # "price": "18000", + # "origQty": "0.00147", + # "executedQty": "0", + # "cummulativeQuoteQty": "0", + # "status": "NEW", + # "type": "LIMIT", + # "side": "BUY", + # "isIsolated": True, + # "isWorking": True, + # "time": 1661994066000, + # "updateTime": 1661994066000 + # } + # ] + # + return self.parse_orders(response, market) + else: + if symbol is not None: + request['symbol'] = market['id'] + # method can be either: contractPrivatePostOrderCancelAll or contractPrivatePostPlanorderCancelAll + # the Planorder endpoints work not only for stop-market orders but also for stop-limit orders that are supposed to have separate endpoint + method = self.safe_string(self.options, 'cancelAllOrders', 'contractPrivatePostOrderCancelAll') + method = self.safe_string(query, 'method', method) + response = None + if method == 'contractPrivatePostOrderCancelAll': + response = self.contractPrivatePostOrderCancelAll(self.extend(request, query)) + elif method == 'contractPrivatePostPlanorderCancelAll': + response = self.contractPrivatePostPlanorderCancelAll(self.extend(request, query)) + # + # { + # "success": True, + # "code": "0" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_orders(data, market) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # spot + # createOrder + # + # { + # "symbol": "FARTCOINUSDT", + # "orderId": "C02__342252993005723644225", + # "orderListId": "-1", + # "price": "1.1", + # "origQty": "6.3", + # "type": "IMMEDIATE_OR_CANCEL", + # "side": "SELL", + # "transactTime": "1745852205223" + # } + # + # unknown endpoint on spot + # + # { + # "symbol": "BTCUSDT", + # "orderId": "123738410679123456", + # "orderListId": -1 + # } + # + # margin: createOrder + # + # { + # "symbol": "BTCUSDT", + # "orderId": "762634301354414080", + # "clientOrderId": null, + # "isIsolated": True, + # "transactTime": 1661992652132 + # } + # + # spot: cancelOrder, cancelAllOrders + # + # { + # "symbol": "BTCUSDT", + # "orderId": "133926441921286144", + # "price": "30000", + # "origQty": "0.0002", + # "type": "LIMIT", + # "side": "BUY" + # } + # + # margin: cancelOrder, cancelAllOrders + # + # { + # "symbol": "BTCUSDT", + # "orderId": "762640232574226432", + # "orderListId": "-1", + # "clientOrderId": null, + # "price": "18000", + # "origQty": "0.00147", + # "executedQty": "0", + # "cummulativeQuoteQty": "0", + # "status": "NEW", + # "type": "LIMIT", + # "side": "BUY", + # "isIsolated": True, + # "isWorking": True, + # "time": 1661994066000, + # "updateTime": 1661994066000 + # } + # + # spot: fetchOrder, fetchOpenOrders, fetchOrders + # + # { + # "symbol": "BTCUSDT", + # "orderId": "133734823834147272", + # "orderListId": "-1", + # "clientOrderId": null, + # "price": "30000", + # "origQty": "0.0002", + # "executedQty": "0", + # "cummulativeQuoteQty": "0", + # "status": "CANCELED", + # "timeInForce": null, + # "type": "LIMIT", + # "side": "BUY", + # "stopPrice": null, + # "icebergQty": null, + # "time": "1647667102000", + # "updateTime": "1647708567000", + # "isWorking": True, + # "origQuoteOrderQty": "6" + # } + # + # margin: fetchOrder, fetchOrders + # + # { + # "symbol": "BTCUSDT", + # "orderId": "763307297891028992", + # "orderListId": "-1", + # "clientOrderId": null, + # "price": "18000", + # "origQty": "0.0014", + # "executedQty": "0", + # "cummulativeQuoteQty": "0", + # "status": "NEW", + # "type": "LIMIT", + # "side": "BUY", + # "isIsolated": True, + # "isWorking": True, + # "time": 1662153107000, + # "updateTime": 1662153107000 + # } + # + # swap: createOrder + # + # 2ff3163e8617443cb9c6fc19d42b1ca4 + # + # swap: fetchOrder, fetchOrders + # + # regular + # { + # "orderId": "264995729269765120", + # "symbol": "STEPN_USDT", + # "positionId": "0", + # "price": "2.2", + # "vol": "15", + # "leverage": "20", + # "side": "1", # TODO: not unified + # "category": "1", + # "orderType": "1", # TODO: not unified + # "dealAvgPrice": "0", + # "dealVol": "0", + # "orderMargin": "2.2528", + # "takerFee": "0", + # "makerFee": "0", + # "profit": "0", + # "feeCurrency": "USDT", + # "openType": "1", + # "state": "2", # TODO + # "externalOid": "_m_0e9520c256744d64b942985189026d20", + # "errorCode": "0", + # "usedMargin": "0", + # "createTime": "1648850305236", + # "updateTime": "1648850305245", + # "positionMode": "1" + # } + # + # stop + # { + # "id": "265557643326564352", + # "triggerType": "1", + # "triggerPrice": "3", + # "price": "2.9", # not present in stop-market, but in stop-limit order + # "executeCycle": "87600", + # "trend": "1", + # # below keys are same regular order structure + # "symbol": "STEPN_USDT", + # "leverage": "20", + # "side": "1", + # "vol": "13", + # "openType": "1", + # "state": "1", + # "orderType": "1", + # "errorCode": "0", + # "createTime": "1648984276000", + # "updateTime": "1648984276000", + # } + # + # createOrders error + # + # { + # "newClientOrderId": "123456", + # "msg": "The minimum transaction volume cannot be less than:0.5USDT", + # "code": 30002 + # } + # + code = self.safe_integer(order, 'code') + if code is not None: + # error upon placing multiple orders + return self.safe_order({ + 'info': order, + 'status': 'rejected', + 'clientOrderId': self.safe_string(order, 'newClientOrderId'), + }) + id = None + if isinstance(order, str): + id = order + else: + id = self.safe_string_2(order, 'orderId', 'id') + timeInForce = self.parse_order_time_in_force(self.safe_string(order, 'timeInForce')) + typeRaw = self.safe_string(order, 'type') + if timeInForce is None: + timeInForce = self.get_tif_from_raw_order_type(typeRaw) + marketId = self.safe_string(order, 'symbol') + market = self.safe_market(marketId, market) + timestamp = self.safe_integer_n(order, ['time', 'createTime', 'transactTime']) + fee = None + feeCurrency = self.safe_string(order, 'feeCurrency') + if feeCurrency is not None: + takerFee = self.safe_string(order, 'takerFee') + makerFee = self.safe_string(order, 'makerFee') + feeSum = Precise.string_add(takerFee, makerFee) + fee = { + 'currency': feeCurrency, + 'cost': self.parse_number(feeSum), + } + return self.safe_order({ + 'id': id, + 'clientOrderId': self.safe_string(order, 'clientOrderId'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'lastUpdateTimestamp': self.safe_integer(order, 'updateTime'), + 'status': self.parse_order_status(self.safe_string_2(order, 'status', 'state')), + 'symbol': market['symbol'], + 'type': self.parse_order_type(typeRaw), + 'timeInForce': timeInForce, + 'side': self.parse_order_side(self.safe_string(order, 'side')), + 'price': self.safe_number(order, 'price'), + 'triggerPrice': self.safe_number_2(order, 'stopPrice', 'triggerPrice'), + 'average': self.safe_number(order, 'dealAvgPrice'), + 'amount': self.safe_number_2(order, 'origQty', 'vol'), + 'cost': self.safe_number(order, 'cummulativeQuoteQty'), # 'cummulativeQuoteQty' vs 'origQuoteOrderQty' + 'filled': self.safe_number_2(order, 'executedQty', 'dealVol'), + 'remaining': None, + 'fee': fee, + 'trades': None, + 'info': order, + }, market) + + def parse_order_side(self, status): + statuses: dict = { + 'BUY': 'buy', + 'SELL': 'sell', + '1': 'buy', + '2': 'sell', + # contracts v1 : TODO + } + return self.safe_string(statuses, status, status) + + def parse_order_type(self, status): + statuses: dict = { + 'MARKET': 'market', + 'LIMIT': 'limit', + 'LIMIT_MAKER': 'limit', + # on spot, during submission below types are used only accepted order + 'IMMEDIATE_OR_CANCEL': 'limit', + 'FILL_OR_KILL': 'limit', + } + return self.safe_string(statuses, status, status) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'NEW': 'open', + 'FILLED': 'closed', + 'CANCELED': 'canceled', + 'PARTIALLY_FILLED': 'open', + 'PARTIALLY_CANCELED': 'canceled', + # contracts v1 + # '1': 'uninformed', # TODO: wt? + '2': 'open', + '3': 'closed', + '4': 'canceled', + # '5': 'invalid', # TODO: wt? + } + return self.safe_string(statuses, status, status) + + def parse_order_time_in_force(self, status): + statuses: dict = { + 'GTC': 'GTC', + 'FOK': 'FOK', + 'IOC': 'IOC', + } + return self.safe_string(statuses, status, status) + + def get_tif_from_raw_order_type(self, orderType: Str = None): + statuses: dict = { + 'LIMIT': 'GTC', + 'LIMIT_MAKER': 'POST_ONLY', + 'IMMEDIATE_OR_CANCEL': 'IOC', + 'FILL_OR_KILL': 'FOK', + 'MARKET': 'IOC', + } + return self.safe_string(statuses, orderType, orderType) + + def fetch_account_helper(self, type, params): + if type == 'spot': + return self.spotPrivateGetAccount(params) + # + # { + # "makerCommission": "20", + # "takerCommission": "20", + # "buyerCommission": "0", + # "sellerCommission": "0", + # "canTrade": True, + # "canWithdraw": True, + # "canDeposit": True, + # "updateTime": null, + # "accountType": "SPOT", + # "balances": [ + # { + # "asset": "BTC", + # "free": "0.002", + # "locked": "0" + # }, + # { + # "asset": "USDT", + # "free": "88.120131350620957006", + # "locked": "0" + # }, + # ], + # "permissions": [ + # "SPOT" + # ] + # } + # + elif type == 'swap': + response = self.contractPrivateGetAccountAssets(params) + # + # { + # "success":true, + # "code":0, + # "data":[ + # { + # "currency":"BSV", + # "positionMargin":0, + # "availableBalance":0, + # "cashBalance":0, + # "frozenBalance":0, + # "equity":0, + # "unrealized":0, + # "bonus":0 + # }, + # ] + # } + # + return self.safe_value(response, 'data') + return None + + def fetch_accounts(self, params={}) -> List[Account]: + """ + fetch all the accounts associated with a profile + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#account-information + https://mexcdevelop.github.io/apidocs/contract_v1_en/#get-all-informations-of-user-39-s-asset + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `account structures ` indexed by the account type + """ + # TODO: is the below endpoints suitable for fetchAccounts? + marketType, query = self.handle_market_type_and_params('fetchAccounts', None, params) + self.load_markets() + response = self.fetch_account_helper(marketType, query) + data = self.safe_value(response, 'balances', []) + result = [] + for i in range(0, len(data)): + account = data[i] + currencyId = self.safe_string_2(account, 'asset', 'currency') + code = self.safe_currency_code(currencyId) + result.append({ + 'id': self.safe_string(account, 'id'), + 'type': self.safe_string(account, 'type'), + 'code': code, + 'info': account, + }) + return result + + def fetch_trading_fee(self, symbol: str, params={}) -> TradingFeeInterface: + """ + fetch the trading fees for a market + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#query-mx-deduct-status + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `fee structure ` + """ + self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise BadRequest(self.id + ' fetchTradingFee() supports spot markets only') + request: dict = { + 'symbol': market['id'], + } + response = self.spotPrivateGetTradeFee(self.extend(request, params)) + # + # { + # "data":{ + # "makerCommission":0.003000000000000000, + # "takerCommission":0.003000000000000000 + # }, + # "code":0, + # "msg":"success", + # "timestamp":1669109672717 + # } + # + data = self.safe_dict(response, 'data', {}) + return { + 'info': data, + 'symbol': symbol, + 'maker': self.safe_number(data, 'makerCommission'), + 'taker': self.safe_number(data, 'takerCommission'), + 'percentage': None, + 'tierBased': None, + } + + def custom_parse_balance(self, response, marketType) -> Balances: + # + # spot + # + # { + # "asset": "USDT", + # "free": "0.000000000674", + # "locked": "0" + # } + # + # swap + # + # { + # "currency": "BSV", + # "positionMargin": 0, + # "availableBalance": 0, + # "cashBalance": 0, + # "frozenBalance": 0, + # "equity": 0, + # "unrealized": 0, + # "bonus": 0 + # } + # + # margin + # + # { + # "baseAsset": { + # "asset": "BTC", + # "borrowEnabled": True, + # "borrowed": "0", + # "free": "0", + # "interest": "0", + # "locked": "0", + # "netAsset": "0", + # "netAssetOfBtc": "0", + # "repayEnabled": True, + # "totalAsset": "0" + # } + # "quoteAsset": { + # "asset": "USDT", + # "borrowEnabled": True, + # "borrowed": "0", + # "free": "10", + # "interest": "0", + # "locked": "0", + # "netAsset": "10", + # "netAssetOfBtc": "0", + # "repayEnabled": True, + # "totalAsset": "10" + # } + # "symbol": "BTCUSDT", + # "isolatedCreated": True, + # "enabled": True, + # "marginLevel": "999", + # "marginRatio": "9", + # "indexPrice": "16741.137068965517241379", + # "liquidatePrice": "--", + # "liquidateRate": "--", + # "tradeEnabled": True + # } + # + wallet = None + if marketType == 'margin': + wallet = self.safe_value(response, 'assets', []) + elif marketType == 'swap': + wallet = self.safe_value(response, 'data', []) + else: + wallet = self.safe_value(response, 'balances', []) + result = {'info': response} + if marketType == 'margin': + for i in range(0, len(wallet)): + entry = wallet[i] + marketId = self.safe_string(entry, 'symbol') + symbol = self.safe_symbol(marketId, None) + base = self.safe_value(entry, 'baseAsset', {}) + quote = self.safe_value(entry, 'quoteAsset', {}) + baseCode = self.safe_currency_code(self.safe_string(base, 'asset')) + quoteCode = self.safe_currency_code(self.safe_string(quote, 'asset')) + subResult: dict = {} + subResult[baseCode] = self.parse_balance_helper(base) + subResult[quoteCode] = self.parse_balance_helper(quote) + result[symbol] = self.safe_balance(subResult) + return result + elif marketType == 'swap': + for i in range(0, len(wallet)): + entry = wallet[i] + currencyId = self.safe_string(entry, 'currency') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(entry, 'availableBalance') + account['used'] = self.safe_string(entry, 'frozenBalance') + result[code] = account + return self.safe_balance(result) + else: + for i in range(0, len(wallet)): + entry = wallet[i] + currencyId = self.safe_string(entry, 'asset') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(entry, 'free') + account['used'] = self.safe_string(entry, 'locked') + result[code] = account + return self.safe_balance(result) + + def parse_balance_helper(self, entry): + account = self.account() + account['used'] = self.safe_string(entry, 'locked') + account['free'] = self.safe_string(entry, 'free') + account['total'] = self.safe_string(entry, 'totalAsset') + debt = self.safe_string(entry, 'borrowed') + interest = self.safe_string(entry, 'interest') + account['debt'] = Precise.string_add(debt, interest) + return account + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#account-information + https://mexcdevelop.github.io/apidocs/contract_v1_en/#get-all-informations-of-user-39-s-asset + https://mexcdevelop.github.io/apidocs/spot_v3_en/#isolated-account + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.symbols]: # required for margin, market id's separated by commas + :returns dict: a `balance structure ` + """ + self.load_markets() + marketType = None + request: dict = {} + marketType, params = self.handle_market_type_and_params('fetchBalance', None, params) + marginMode = self.safe_string(params, 'marginMode') + isMargin = self.safe_bool(params, 'margin', False) + params = self.omit(params, ['margin', 'marginMode']) + response = None + if (marginMode is not None) or (isMargin) or (marketType == 'margin'): + parsedSymbols = None + symbol = self.safe_string(params, 'symbol') + if symbol is None: + symbols = self.safe_value(params, 'symbols') + if symbols is not None: + parsedSymbols = ','.join(self.market_ids(symbols)) + else: + market = self.market(symbol) + parsedSymbols = market['id'] + self.check_required_argument('fetchBalance', parsedSymbols, 'symbol or symbols') + marketType = 'margin' + request['symbols'] = parsedSymbols + params = self.omit(params, ['symbol', 'symbols']) + response = self.spotPrivateGetMarginIsolatedAccount(self.extend(request, params)) + elif marketType == 'spot': + response = self.spotPrivateGetAccount(self.extend(request, params)) + elif marketType == 'swap': + response = self.contractPrivateGetAccountAssets(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchBalance() not support self method') + # + # spot + # + # { + # "makerCommission": 0, + # "takerCommission": 20, + # "buyerCommission": 0, + # "sellerCommission": 0, + # "canTrade": True, + # "canWithdraw": True, + # "canDeposit": True, + # "updateTime": null, + # "accountType": "SPOT", + # "balances": [ + # { + # "asset": "USDT", + # "free": "0.000000000674", + # "locked": "0" + # }, + # ], + # "permissions": ["SPOT"] + # } + # + # swap + # + # { + # "success": True, + # "code": 0, + # "data": [ + # { + # "currency": "BSV", + # "positionMargin": 0, + # "availableBalance": 0, + # "cashBalance": 0, + # "frozenBalance": 0, + # "equity": 0, + # "unrealized": 0, + # "bonus": 0 + # }, + # ] + # } + # + # margin + # + # { + # "assets": [ + # { + # "baseAsset": { + # "asset": "BTC", + # "borrowEnabled": True, + # "borrowed": "0", + # "free": "0", + # "interest": "0", + # "locked": "0", + # "netAsset": "0", + # "netAssetOfBtc": "0", + # "repayEnabled": True, + # "totalAsset": "0" + # }, + # "quoteAsset": { + # "asset": "USDT", + # "borrowEnabled": True, + # "borrowed": "0", + # "free": "10", + # "interest": "0", + # "locked": "0", + # "netAsset": "10", + # "netAssetOfBtc": "0", + # "repayEnabled": True, + # "totalAsset": "10" + # }, + # "symbol": "BTCUSDT", + # "isolatedCreated": True, + # "enabled": True, + # "marginLevel": "999", + # "marginRatio": "9", + # "indexPrice": "16741.137068965517241379", + # "liquidatePrice": "--", + # "liquidateRate": "--", + # "tradeEnabled": True + # } + # ] + # } + # + return self.custom_parse_balance(response, marketType) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#account-trade-list + https://mexcdevelop.github.io/apidocs/contract_v1_en/#get-all-transaction-details-of-the-user-s-order + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch trades for + :returns Trade[]: a list of `trade structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMyTrades() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + marketType: Str = None + marketType, params = self.handle_market_type_and_params('fetchMyTrades', market, params) + request: dict = { + 'symbol': market['id'], + } + trades = None + if marketType == 'spot': + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + until = self.safe_integer(params, 'until') + if until is not None: + params = self.omit(params, 'until') + request['endTime'] = until + trades = self.spotPrivateGetMyTrades(self.extend(request, params)) + # + # spot + # + # [ + # { + # "symbol": "BTCUSDT", + # "id": "133948532984922113", + # "orderId": "133948532531949568", + # "orderListId": "-1", + # "price": "41995.51", + # "qty": "0.0002", + # "quoteQty": "8.399102", + # "commission": "0.016798204", + # "commissionAsset": "USDT", + # "time": "1647718055000", + # "isBuyer": True, + # "isMaker": False, + # "isBestMatch": True + # } + # ] + # + else: + if since is not None: + request['start_time'] = since + end = self.safe_integer(params, 'end_time') + if end is None: + request['end_time'] = self.sum(since, self.options['maxTimeTillEnd']) + if limit is not None: + request['page_size'] = limit + response = self.contractPrivateGetOrderListOrderDeals(self.extend(request, params)) + # + # { + # "success": True, + # "code": "0", + # "data": [ + # { + # "id": "299444585", + # "symbol": "STEPN_USDT", + # "side": "1", + # "vol": "1", + # "price": "2.45455", + # "feeCurrency": "USDT", + # "fee": "0.00147273", + # "timestamp": "1648924557000", + # "profit": "0", + # "category": "1", + # "orderId": "265307163526610432", + # "positionMode": "1", + # "taker": True + # } + # ] + # } + # + trades = self.safe_value(response, 'data') + return self.parse_trades(trades, market, since, limit) + + def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all the trades made from a single order + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#account-trade-list + https://mexcdevelop.github.io/apidocs/contract_v1_en/#query-the-order-based-on-the-order-number + + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + marketType, query = self.handle_market_type_and_params('fetchOrderTrades', market, params) + trades = None + if marketType == 'spot': + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrderTrades() requires a symbol argument') + request['symbol'] = market['id'] + request['orderId'] = id + trades = self.spotPrivateGetMyTrades(self.extend(request, query)) + # + # spot + # + # [ + # { + # "symbol": "BTCUSDT", + # "id": "133948532984922113", + # "orderId": "133948532531949568", + # "orderListId": "-1", + # "price": "41995.51", + # "qty": "0.0002", + # "quoteQty": "8.399102", + # "commission": "0.016798204", + # "commissionAsset": "USDT", + # "time": "1647718055000", + # "isBuyer": True, + # "isMaker": False, + # "isBestMatch": True + # } + # ] + # + else: + request['order_id'] = id + response = self.contractPrivateGetOrderDealDetailsOrderId(self.extend(request, query)) + # + # { + # "success": True, + # "code": "0", + # "data": [ + # { + # "id": "299444585", + # "symbol": "STEPN_USDT", + # "side": "1", + # "vol": "1", + # "price": "2.45455", + # "feeCurrency": "USDT", + # "fee": "0.00147273", + # "timestamp": "1648924557000", + # "profit": "0", + # "category": "1", + # "orderId": "265307163526610432", + # "positionMode": "1", + # "taker": True + # } + # ] + # } + # + trades = self.safe_value(response, 'data') + return self.parse_trades(trades, market, since, limit, query) + + def modify_margin_helper(self, symbol: str, amount, addOrReduce, params={}): + positionId = self.safe_integer(params, 'positionId') + if positionId is None: + raise ArgumentsRequired(self.id + ' modifyMarginHelper() requires a positionId parameter') + self.load_markets() + request: dict = { + 'positionId': positionId, + 'amount': amount, + 'type': addOrReduce, + } + response = self.contractPrivatePostPositionChangeMargin(self.extend(request, params)) + # + # { + # "success": True, + # "code": 0 + # } + return response + + def reduce_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + remove margin from a position + + https://mexcdevelop.github.io/apidocs/contract_v1_en/#increase-or-decrease-margin + + :param str symbol: unified market symbol + :param float amount: the amount of margin to remove + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin structure ` + """ + return self.modify_margin_helper(symbol, amount, 'SUB', params) + + def add_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + add margin + + https://mexcdevelop.github.io/apidocs/contract_v1_en/#increase-or-decrease-margin + + :param str symbol: unified market symbol + :param float amount: amount of margin to add + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin structure ` + """ + return self.modify_margin_helper(symbol, amount, 'ADD', params) + + def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://mexcdevelop.github.io/apidocs/contract_v1_en/#switch-leverage + + :param float leverage: the rate of leverage + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + self.load_markets() + request: dict = { + 'leverage': leverage, + } + positionId = self.safe_integer(params, 'positionId') + if positionId is None: + openType = self.safe_number(params, 'openType') # 1 or 2 + positionType = self.safe_number(params, 'positionType') # 1 or 2 + market = self.market(symbol) if (symbol is not None) else None + if (openType is None) or (positionType is None) or (market is None): + raise ArgumentsRequired(self.id + ' setLeverage() requires a positionId parameter or a symbol argument with openType and positionType parameters, use openType 1 or 2 for isolated or cross margin respectively, use positionType 1 or 2 for long or short positions') + else: + request['openType'] = openType + request['symbol'] = market['id'] + request['positionType'] = positionType + else: + request['positionId'] = positionId + return self.contractPrivatePostPositionChangeLeverage(self.extend(request, params)) + + def fetch_funding_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch the history of funding payments paid and received on self account + + https://mexcdevelop.github.io/apidocs/contract_v1_en/#get-details-of-user-s-funding-rate + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch funding history for + :param int [limit]: the maximum number of funding history structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding history structure ` + """ + self.load_markets() + market = None + request: dict = { + # 'symbol': market['id'], + # 'position_id': positionId, + # 'page_num': 1, + # 'page_size': limit, # default 20, max 100 + } + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if limit is not None: + request['page_size'] = limit + response = self.contractPrivateGetPositionFundingRecords(self.extend(request, params)) + # + # { + # "success": True, + # "code": 0, + # "data": { + # "pageSize": 20, + # "totalCount": 2, + # "totalPage": 1, + # "currentPage": 1, + # "resultList": [ + # { + # "id": 7423910, + # "symbol": "BTC_USDT", + # "positionType": 1, + # "positionValue": 29.30024, + # "funding": 0.00076180624, + # "rate": -0.000026, + # "settleTime": 1643299200000 + # }, + # { + # "id": 7416473, + # "symbol": "BTC_USDT", + # "positionType": 1, + # "positionValue": 28.9188, + # "funding": 0.0014748588, + # "rate": -0.000051, + # "settleTime": 1643270400000 + # } + # ] + # } + # } + # + data = self.safe_value(response, 'data', {}) + resultList = self.safe_value(data, 'resultList', []) + result = [] + for i in range(0, len(resultList)): + entry = resultList[i] + timestamp = self.safe_integer(entry, 'settleTime') + result.append({ + 'info': entry, + 'symbol': symbol, + 'code': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': self.safe_number(entry, 'id'), + 'amount': self.safe_number(entry, 'funding'), + }) + return result + + def parse_funding_rate(self, contract, market: Market = None) -> FundingRate: + # + # { + # "symbol": "BTC_USDT", + # "fundingRate": 0.000014, + # "maxFundingRate": 0.003, + # "minFundingRate": -0.003, + # "collectCycle": 8, + # "nextSettleTime": 1643241600000, + # "timestamp": 1643240373359 + # } + # + nextFundingRate = self.safe_number(contract, 'fundingRate') + nextFundingTimestamp = self.safe_integer(contract, 'nextSettleTime') + marketId = self.safe_string(contract, 'symbol') + symbol = self.safe_symbol(marketId, market, None, 'contract') + timestamp = self.safe_integer(contract, 'timestamp') + interval = self.safe_string(contract, 'collectCycle') + intervalString = None + if interval is not None: + intervalString = interval + 'h' + return { + 'info': contract, + 'symbol': symbol, + 'markPrice': None, + 'indexPrice': None, + 'interestRate': None, + 'estimatedSettlePrice': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'fundingRate': nextFundingRate, + 'fundingTimestamp': nextFundingTimestamp, + 'fundingDatetime': self.iso8601(nextFundingTimestamp), + 'nextFundingRate': None, + 'nextFundingTimestamp': None, + 'nextFundingDatetime': None, + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': intervalString, + } + + def fetch_funding_interval(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate interval + + https://mexcdevelop.github.io/apidocs/contract_v1_en/#get-contract-funding-rate + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + return self.fetch_funding_rate(symbol, params) + + def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate + + https://mexcdevelop.github.io/apidocs/contract_v1_en/#get-contract-funding-rate + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = self.contractPublicGetFundingRateSymbol(self.extend(request, params)) + # + # { + # "success": True, + # "code": 0, + # "data": { + # "symbol": "BTC_USDT", + # "fundingRate": 0.000014, + # "maxFundingRate": 0.003, + # "minFundingRate": -0.003, + # "collectCycle": 8, + # "nextSettleTime": 1643241600000, + # "timestamp": 1643240373359 + # } + # } + # + result = self.safe_value(response, 'data', {}) + return self.parse_funding_rate(result, market) + + def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical funding rate prices + + https://mexcdevelop.github.io/apidocs/contract_v1_en/#get-contract-funding-rate-history + + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: not used by mexc, but filtered internally by ccxt + :param int [limit]: mexc limit is page_size default 20, maximum is 100 + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `funding rate structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchFundingRateHistory() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + # 'page_size': limit, # optional + # 'page_num': 1, # optional, current page number, default is 1 + } + if limit is not None: + request['page_size'] = limit + response = self.contractPublicGetFundingRateHistory(self.extend(request, params)) + # + # { + # "success": True, + # "code": 0, + # "data": { + # "pageSize": 2, + # "totalCount": 21, + # "totalPage": 11, + # "currentPage": 1, + # "resultList": [ + # { + # "symbol": "BTC_USDT", + # "fundingRate": 0.000266, + # "settleTime": 1609804800000 + # }, + # { + # "symbol": "BTC_USDT", + # "fundingRate": 0.00029, + # "settleTime": 1609776000000 + # } + # ] + # } + # } + # + data = self.safe_value(response, 'data') + result = self.safe_value(data, 'resultList', []) + rates = [] + for i in range(0, len(result)): + entry = result[i] + marketId = self.safe_string(entry, 'symbol') + symbolInner = self.safe_symbol(marketId) + timestamp = self.safe_integer(entry, 'settleTime') + rates.append({ + 'info': entry, + 'symbol': symbolInner, + 'fundingRate': self.safe_number(entry, 'fundingRate'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + sorted = self.sort_by(rates, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, market['symbol'], since, limit) + + def fetch_leverage_tiers(self, symbols: Strings = None, params={}) -> LeverageTiers: + """ + retrieve information on the maximum leverage, and maintenance margin for trades of varying trade sizes, if a market has a leverage tier of 0, then the leverage tiers cannot be obtained for self market + + https://mexcdevelop.github.io/apidocs/contract_v1_en/#get-the-contract-information + + :param str[] [symbols]: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `leverage tiers structures `, indexed by market symbols + """ + self.load_markets() + symbols = self.market_symbols(symbols, 'swap', True, True) + response = self.contractPublicGetDetail(params) + # + # { + # "success":true, + # "code":0, + # "data":[ + # { + # "symbol": "BTC_USDT", + # "displayName": "BTC_USDT永续", + # "displayNameEn": "BTC_USDT SWAP", + # "positionOpenType": 3, + # "baseCoin": "BTC", + # "quoteCoin": "USDT", + # "settleCoin": "USDT", + # "contractSize": 0.0001, + # "minLeverage": 1, + # "maxLeverage": 125, + # "priceScale": 2, + # "volScale": 0, + # "amountScale": 4, + # "priceUnit": 0.5, + # "volUnit": 1, + # "minVol": 1, + # "maxVol": 1000000, + # "bidLimitPriceRate": 0.1, + # "askLimitPriceRate": 0.1, + # "takerFeeRate": 0.0006, + # "makerFeeRate": 0.0002, + # "maintenanceMarginRate": 0.004, + # "initialMarginRate": 0.008, + # "riskBaseVol": 10000, + # "riskIncrVol": 200000, + # "riskIncrMmr": 0.004, + # "riskIncrImr": 0.004, + # "riskLevelLimit": 5, + # "priceCoefficientVariation": 0.1, + # "indexOrigin": ["BINANCE","GATEIO","HUOBI","MXC"], + # "state": 0, # 0 enabled, 1 delivery, 2 completed, 3 offline, 4 pause + # "isNew": False, + # "isHot": True, + # "isHidden": False + # }, + # ... + # ] + # } + # + data = self.safe_list(response, 'data') + return self.parse_leverage_tiers(data, symbols, 'symbol') + + def parse_market_leverage_tiers(self, info, market: Market = None) -> List[LeverageTier]: + # + # { + # "symbol": "BTC_USDT", + # "displayName": "BTC_USDT永续", + # "displayNameEn": "BTC_USDT SWAP", + # "positionOpenType": 3, + # "baseCoin": "BTC", + # "quoteCoin": "USDT", + # "settleCoin": "USDT", + # "contractSize": 0.0001, + # "minLeverage": 1, + # "maxLeverage": 125, + # "priceScale": 2, + # "volScale": 0, + # "amountScale": 4, + # "priceUnit": 0.5, + # "volUnit": 1, + # "minVol": 1, + # "maxVol": 1000000, + # "bidLimitPriceRate": 0.1, + # "askLimitPriceRate": 0.1, + # "takerFeeRate": 0.0006, + # "makerFeeRate": 0.0002, + # "maintenanceMarginRate": 0.004, + # "initialMarginRate": 0.008, + # "riskBaseVol": 10000, + # "riskIncrVol": 200000, + # "riskIncrMmr": 0.004, + # "riskIncrImr": 0.004, + # "riskLevelLimit": 5, + # "priceCoefficientVariation": 0.1, + # "indexOrigin": ["BINANCE","GATEIO","HUOBI","MXC"], + # "state": 0, # 0 enabled, 1 delivery, 2 completed, 3 offline, 4 pause + # "isNew": False, + # "isHot": True, + # "isHidden": False + # } + # + marketId = self.safe_string(info, 'symbol') + maintenanceMarginRate = self.safe_string(info, 'maintenanceMarginRate') + initialMarginRate = self.safe_string(info, 'initialMarginRate') + maxVol = self.safe_string(info, 'maxVol') + riskIncrVol = self.safe_string(info, 'riskIncrVol') + riskIncrMmr = self.safe_string(info, 'riskIncrMmr') + riskIncrImr = self.safe_string(info, 'riskIncrImr') + floor = '0' + tiers = [] + quoteId = self.safe_string(info, 'quoteCoin') + if riskIncrVol == '0': + return [ + { + 'tier': 0, + 'symbol': self.safe_symbol(marketId, market, None, 'contract'), + 'currency': self.safe_currency_code(quoteId), + 'minNotional': None, + 'maxNotional': None, + 'maintenanceMarginRate': None, + 'maxLeverage': self.safe_number(info, 'maxLeverage'), + 'info': info, + }, + ] + while(Precise.string_lt(floor, maxVol)): + cap = Precise.string_add(floor, riskIncrVol) + tiers.append({ + 'tier': self.parse_number(Precise.string_div(cap, riskIncrVol)), + 'symbol': self.safe_symbol(marketId, market, None, 'contract'), + 'currency': self.safe_currency_code(quoteId), + 'minNotional': self.parse_number(floor), + 'maxNotional': self.parse_number(cap), + 'maintenanceMarginRate': self.parse_number(maintenanceMarginRate), + 'maxLeverage': self.parse_number(Precise.string_div('1', initialMarginRate)), + 'info': info, + }) + initialMarginRate = Precise.string_add(initialMarginRate, riskIncrImr) + maintenanceMarginRate = Precise.string_add(maintenanceMarginRate, riskIncrMmr) + floor = cap + return tiers + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + # + # { + # coin: "USDT", + # network: "BNB Smart Chain(BEP20)", + # address: "0x0d48003e0c27c5de62b97c9b4cdb31fdd29da619", + # memo: null + # } + # + address = self.safe_string(depositAddress, 'address') + currencyId = self.safe_string(depositAddress, 'coin') + networkId = self.safe_string(depositAddress, 'netWork') + self.check_address(address) + return { + 'info': depositAddress, + 'currency': self.safe_currency_code(currencyId, currency), + 'network': self.network_id_to_code(networkId, currencyId), + 'address': address, + 'tag': self.safe_string(depositAddress, 'memo'), + } + + def fetch_deposit_addresses_by_network(self, code: str, params={}) -> List[DepositAddress]: + """ + fetch a dictionary of addresses for a currency, indexed by network + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#deposit-address-supporting-network + + :param str code: unified currency code of the currency for the deposit address + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `address structures ` indexed by the network + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'coin': currency['id'], + } + networkCode = self.safe_string(params, 'network') + networkId = None + if networkCode is not None: + # createDepositAddress and fetchDepositAddress use a different network-id compared to withdraw + networkUnified = self.network_id_to_code(networkCode, code) + networks = self.safe_dict(currency, 'networks', {}) + if networkUnified in networks: + network = self.safe_dict(networks, networkUnified, {}) + networkInfo = self.safe_value(network, 'info', {}) + networkId = self.safe_string(networkInfo, 'network') + else: + networkId = self.network_code_to_id(networkCode, code) + if networkId is not None: + request['network'] = networkId + params = self.omit(params, 'network') + response = self.spotPrivateGetCapitalDepositAddress(self.extend(request, params)) + # + # [ + # { + # coin: "USDT", + # network: "BNB Smart Chain(BEP20)", + # address: "0x0d48003e0c27c5de62b97c9b4cdb31fdd29da619", + # memo: null + # } + # ... + # ] + # + addressStructures = self.parse_deposit_addresses(response, None, False) + return self.index_by(addressStructures, 'network') + + def create_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + create a currency deposit address + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#generate-deposit-address-supporting-network + + :param str code: unified currency code of the currency for the deposit address + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.network]: the blockchain network name + :returns dict: an `address structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'coin': currency['id'], + } + networkCode = self.safe_string(params, 'network') + if networkCode is None: + raise ArgumentsRequired(self.id + ' createDepositAddress requires a `network` parameter') + # createDepositAddress and fetchDepositAddress use a different network-id compared to withdraw + networkId = None + networkUnified = self.network_id_to_code(networkCode, code) + networks = self.safe_dict(currency, 'networks', {}) + if networkUnified in networks: + network = self.safe_dict(networks, networkUnified, {}) + networkInfo = self.safe_value(network, 'info', {}) + networkId = self.safe_string(networkInfo, 'network') + else: + networkId = self.network_code_to_id(networkCode, code) + if networkId is not None: + request['network'] = networkId + params = self.omit(params, 'network') + response = self.spotPrivatePostCapitalDepositAddress(self.extend(request, params)) + # { + # "coin": "EOS", + # "network": "EOS", + # "address": "zzqqqqqqqqqq", + # "memo": "MX10068" + # } + return self.parse_deposit_address(response, currency) + + def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#deposit-address-supporting-network + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.network]: the chain of currency, self only apply for multi-chain currency, and there is no need for single chain currency + :returns dict: an `address structure ` + """ + network = self.safe_string(params, 'network') + addressStructures = self.fetch_deposit_addresses_by_network(code, params) + result = None + if network is not None: + result = self.safe_dict(addressStructures, self.network_id_to_code(network, code)) + else: + options = self.safe_dict(self.options, 'defaultNetworks') + defaultNetworkForCurrency = self.safe_string(options, code) + if defaultNetworkForCurrency is not None: + result = self.safe_dict(addressStructures, defaultNetworkForCurrency) + else: + keys = list(addressStructures.keys()) + key = self.safe_string(keys, 0) + result = self.safe_dict(addressStructures, key) + if result is None: + raise InvalidAddress(self.id + ' fetchDepositAddress() cannot find a deposit address for ' + code + ', and network' + network + 'consider creating one using .createDepositAddress() method or in MEXC website') + return result + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#deposit-history-supporting-network + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + request: dict = { + # 'coin': currency['id'] + network example: USDT-TRX, + # 'status': 'status', + # 'startTime': since, # default 90 days + # 'endTime': self.nonce(), + # 'limit': limit, # default 1000, maximum 1000 + } + currency = None + if code is not None: + currency = self.currency(code) + request['coin'] = currency['id'] + # currently mexc does not have network names unified so for certain things we might need TRX or TRC-20 + # due to that I'm applying the network parameter directly so the user can control it on its side + rawNetwork = self.safe_string(params, 'network') + if rawNetwork is not None: + params = self.omit(params, 'network') + request['coin'] = request['coin'] + '-' + rawNetwork + if since is not None: + request['startTime'] = since + if limit is not None: + if limit > 1000: + raise ExchangeError('This exchange supports a maximum limit of 1000') + request['limit'] = limit + response = self.spotPrivateGetCapitalDepositHisrec(self.extend(request, params)) + # + # [ + # { + # "amount": "10", + # "coin": "USDC-TRX", + # "network": "TRX", + # "status": "5", + # "address": "TSMcEDDvkqY9dz8RkFnrS86U59GwEZjfvh", + # "txId": "51a8f49e6f03f2c056e71fe3291aa65e1032880be855b65cecd0595a1b8af95b:0", + # "insertTime": "1664805021000", + # "unlockConfirm": "200", + # "confirmTimes": "203", + # "memo": "xxyy1122", + # "transHash": "51a8f49e6f03f2c056e71fe3291aa65e1032880be855b65cecd0595a1b8af95b", + # "updateTime": "1664805621000", + # "netWork: "TRX" + # } + # ] + # + return self.parse_transactions(response, currency, since, limit) + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#withdraw-history-supporting-network + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + request: dict = { + # 'coin': currency['id'], + # 'status': 'status', + # 'startTime': since, # default 90 days + # 'endTime': self.nonce(), + # 'limit': limit, # default 1000, maximum 1000 + } + currency = None + if code is not None: + currency = self.currency(code) + request['coin'] = currency['id'] + if since is not None: + request['startTime'] = since + if limit is not None: + if limit > 1000: + raise ExchangeError('This exchange supports a maximum limit of 1000') + request['limit'] = limit + response = self.spotPrivateGetCapitalWithdrawHistory(self.extend(request, params)) + # + # [ + # { + # "id": "adcd1c8322154de691b815eedcd10c42", + # "txId": "0xc8c918cd69b2246db493ef6225a72ffdc664f15b08da3e25c6879b271d05e9d0:0", + # "coin": "USDC-MATIC", + # "network": "MATIC", + # "address": "0xeE6C7a415995312ED52c53a0f8f03e165e0A5D62", + # "amount": "2", + # "transferType": "0", + # "status": "7", + # "transactionFee": "1", + # "confirmNo": null, + # "applyTime": "1664882739000", + # "remark": '', + # "memo": null, + # "explorerUrl": "https://etherscan.io/tx/0xc8c918cd69b2246db493ef6225a72ffdc664f15b08da3e25c6879b271d05e9d0", + # "transHash": "0xc8c918cd69b2246db493ef6225a72ffdc664f15b08da3e25c6879b271d05e9d0", + # "updateTime": "1664882799000", + # "netWork: "MATIC" + # } + # ] + # + return self.parse_transactions(response, currency, since, limit) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fetchDeposits + # + # { + # "amount": "10", + # "coin": "USDC-TRX", + # "network": "TRX", + # "status": "5", + # "address": "TSMcEDDvkqY9dz8RkFnrS86U59GwEZjfvh", + # "txId": "51a8f49e6f03f2c056e71fe3291aa65e1032880be855b65cecd0595a1b8af95b:0", + # "insertTime": "1664805021000", + # "unlockConfirm": "200", + # "confirmTimes": "203", + # "memo": "xxyy1122", + # "transHash": "51a8f49e6f03f2c056e71fe3291aa65e1032880be855b65cecd0595a1b8af95b", + # "updateTime": "1664805621000", + # "netWork: "TRX" + # } + # + # fetchWithdrawals + # + # { + # "id": "adcd1c8322154de691b815eedcd10c42", + # "txId": "0xc8c918cd69b2246db493ef6225a72ffdc664f15b08da3e25c6879b271d05e9d0:0", + # "coin": "USDC-MATIC", + # "network": "MATIC", + # "address": "0xeE6C7a415995312ED52c53a0f8f03e165e0A5D62", + # "amount": "2", + # "transferType": "0", + # "status": "7", + # "transactionFee": "1", + # "confirmNo": null, + # "applyTime": "1664882739000", + # "remark": "", + # "memo": null, + # "explorerUrl": "https://etherscan.io/tx/0xc8c918cd69b2246db493ef6225a72ffdc664f15b08da3e25c6879b271d05e9d0", + # "transHash": "0xc8c918cd69b2246db493ef6225a72ffdc664f15b08da3e25c6879b271d05e9d0", + # "updateTime": "1664882799000", + # "netWork: "MATIC" + # } + # + # withdraw + # + # { + # "id":"25fb2831fb6d4fc7aa4094612a26c81d" + # } + # + # internal withdraw(aka internal-transfer) + # + # { + # "tranId":"ad36f0e9c9a24ae794b36fa4f152e471" + # } + # + id = self.safe_string_2(transaction, 'id', 'tranId') + type = 'deposit' if (id is None) else 'withdrawal' + timestamp = self.safe_integer_2(transaction, 'insertTime', 'applyTime') + updated = self.safe_integer(transaction, 'updateTime') + currencyId = None + currencyWithNetwork = self.safe_string(transaction, 'coin') + if currencyWithNetwork is not None: + currencyId = currencyWithNetwork.split('-')[0] + network = None + rawNetwork = self.safe_string(transaction, 'network') + if rawNetwork is not None: + network = self.network_id_to_code(rawNetwork) + code = self.safe_currency_code(currencyId, currency) + status = self.parse_transaction_status_by_type(self.safe_string(transaction, 'status'), type) + amountString = self.safe_string(transaction, 'amount') + address = self.safe_string(transaction, 'address') + txid = self.safe_string_2(transaction, 'transHash', 'txId') + fee = None + feeCostString = self.safe_string(transaction, 'transactionFee') + if feeCostString is not None: + fee = { + 'cost': self.parse_number(feeCostString), + 'currency': code, + } + if type == 'withdrawal': + # mexc withdrawal amount includes the fee + amountString = Precise.string_sub(amountString, feeCostString) + return { + 'info': transaction, + 'id': id, + 'txid': txid, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': network, + 'address': address, + 'addressTo': address, + 'addressFrom': None, + 'tag': self.safe_string(transaction, 'memo'), + 'tagTo': None, + 'tagFrom': None, + 'type': type, + 'amount': self.parse_number(amountString), + 'currency': code, + 'status': status, + 'updated': updated, + 'comment': self.safe_string(transaction, 'remark'), + 'internal': None, + 'fee': fee, + } + + def parse_transaction_status_by_type(self, status, type=None): + statusesByType: dict = { + 'deposit': { + '1': 'failed', # SMALL + '2': 'pending', # TIME_DELAY + '3': 'pending', # LARGE_DELAY + '4': 'pending', # PENDING + '5': 'ok', # SUCCESS + '6': 'pending', # AUDITING + '7': 'failed', # REJECTED + }, + 'withdrawal': { + '1': 'pending', # APPLY + '2': 'pending', # AUDITING + '3': 'pending', # WAIT + '4': 'pending', # PROCESSING + '5': 'pending', # WAIT_PACKAGING + '6': 'pending', # WAIT_CONFIRM + '7': 'ok', # SUCCESS + '8': 'failed', # FAILED + '9': 'canceled', # CANCEL + '10': 'pending', # MANUAL + }, + } + statuses = self.safe_value(statusesByType, type, {}) + return self.safe_string(statuses, status, status) + + def fetch_position(self, symbol: str, params={}): + """ + fetch data on a single open contract trade position + + https://mexcdevelop.github.io/apidocs/contract_v1_en/#get-the-user-s-history-position-information + + :param str symbol: unified market symbol of the market the position is held in, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `position structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = self.fetch_positions(None, self.extend(request, params)) + return self.safe_value(response, 0) + + def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://mexcdevelop.github.io/apidocs/contract_v1_en/#get-the-user-s-history-position-information + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + self.load_markets() + response = self.contractPrivateGetPositionOpenPositions(params) + # + # { + # "success": True, + # "code": 0, + # "data": [ + # { + # "positionId": 1394650, + # "symbol": "ETH_USDT", + # "positionType": 1, + # "openType": 1, + # "state": 1, + # "holdVol": 1, + # "frozenVol": 0, + # "closeVol": 0, + # "holdAvgPrice": 1217.3, + # "openAvgPrice": 1217.3, + # "closeAvgPrice": 0, + # "liquidatePrice": 1211.2, + # "oim": 0.1290338, + # "im": 0.1290338, + # "holdFee": 0, + # "realised": -0.0073, + # "leverage": 100, + # "createTime": 1609991676000, + # "updateTime": 1609991676000, + # "autoAddIm": False + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_positions(data, symbols) + + def parse_position(self, position: dict, market: Market = None): + # + # fetchPositions + # + # { + # "positionId": 1394650, + # "symbol": "ETH_USDT", + # "positionType": 1, + # "openType": 1, + # "state": 1, + # "holdVol": 1, + # "frozenVol": 0, + # "closeVol": 0, + # "holdAvgPrice": 1217.3, + # "openAvgPrice": 1217.3, + # "closeAvgPrice": 0, + # "liquidatePrice": 1211.2, + # "oim": 0.1290338, + # "im": 0.1290338, + # "holdFee": 0, + # "realised": -0.0073, + # "leverage": 100, + # "createTime": 1609991676000, + # "updateTime": 1609991676000, + # "autoAddIm": False + # } + # + # fetchPositionsHistory + # + # { + # positionId: '390281084', + # symbol: 'RVN_USDT', + # positionType: '1', + # openType: '2', + # state: '3', + # holdVol: '0', + # frozenVol: '0', + # closeVol: '1141', + # holdAvgPrice: '0.03491', + # holdAvgPriceFullyScale: '0.03491', + # openAvgPrice: '0.03491', + # openAvgPriceFullyScale: '0.03491', + # closeAvgPrice: '0.03494', + # liquidatePrice: '0.03433', + # oim: '0', + # im: '0', + # holdFee: '0', + # realised: '0.1829', + # leverage: '50', + # createTime: '1711512408000', + # updateTime: '1711512553000', + # autoAddIm: False, + # version: '4', + # profitRatio: '0.0227', + # newOpenAvgPrice: '0.03491', + # newCloseAvgPrice: '0.03494', + # closeProfitLoss: '0.3423', + # fee: '0.1593977', + # positionShowStatus: 'CLOSED' + # } + # + market = self.safe_market(self.safe_string(position, 'symbol'), market, None, 'swap') + symbol = market['symbol'] + contracts = self.safe_string(position, 'holdVol') + entryPrice = self.safe_number(position, 'openAvgPrice') + initialMargin = self.safe_string(position, 'im') + rawSide = self.safe_string(position, 'positionType') + side = 'long' if (rawSide == '1') else 'short' + openType = self.safe_string(position, 'margin_mode') + marginType = 'isolated' if (openType == '1') else 'cross' + leverage = self.safe_number(position, 'leverage') + liquidationPrice = self.safe_number(position, 'liquidatePrice') + timestamp = self.safe_integer(position, 'updateTime') + return self.safe_position({ + 'info': position, + 'id': None, + 'symbol': symbol, + 'contracts': self.parse_number(contracts), + 'contractSize': None, + 'entryPrice': entryPrice, + 'collateral': None, + 'side': side, + 'unrealizedPnl': None, + 'leverage': self.parse_number(leverage), + 'percentage': None, + 'marginMode': marginType, + 'notional': None, + 'markPrice': None, + 'lastPrice': None, + 'liquidationPrice': liquidationPrice, + 'initialMargin': self.parse_number(initialMargin), + 'initialMarginPercentage': None, + 'maintenanceMargin': None, + 'maintenanceMarginPercentage': None, + 'marginRatio': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'hedged': None, + 'stopLossPrice': None, + 'takeProfitPrice': None, + 'lastUpdateTimestamp': None, + }) + + def fetch_transfer(self, id: str, code: Str = None, params={}) -> TransferEntry: + """ + fetches a transfer + + https://mexcdevelop.github.io/apidocs/spot_v2_en/#internal-assets-transfer-order-inquiry + + :param str id: transfer id + :param str [code]: not used by mexc fetchTransfer + :param dict params: extra parameters specific to the exchange api endpoint + :returns dict: a `transfer structure ` + """ + marketType, query = self.handle_market_type_and_params('fetchTransfer', None, params) + self.load_markets() + if marketType == 'spot': + request: dict = { + 'transact_id': id, + } + response = self.spotPrivateGetAssetInternalTransferRecord(self.extend(request, query)) + # + # { + # "code": "200", + # "data": { + # "currency": "USDT", + # "amount": "1", + # "transact_id": "954877a2ef54499db9b28a7cf9ebcf41", + # "from": "MAIN", + # "to": "CONTRACT", + # "transact_state": "SUCCESS" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_transfer(data) + elif marketType == 'swap': + raise BadRequest(self.id + ' fetchTransfer() is not supported for ' + marketType) + return None + + def fetch_transfers(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[TransferEntry]: + """ + fetch a history of internal transfers made on an account + + https://mexcdevelop.github.io/apidocs/spot_v2_en/#get-internal-assets-transfer-records + https://mexcdevelop.github.io/apidocs/contract_v1_en/#get-the-user-39-s-asset-transfer-records + https://www.mexc.com/api-docs/spot-v3/wallet-endpoints#query-user-universal-transfer-history :param str code: unified currency code of the currency transferred + + @param code + :param int [since]: the earliest time in ms to fetch transfers for + :param int [limit]: the maximum number of transfers structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.fromAccountType]: 'SPOT' for spot wallet, 'FUTURES' for contract wallet + :param str [params.toAccountType]: 'SPOT' for spot wallet, 'FUTURES' for contract wallet + :returns dict[]: a list of `transfer structures ` + """ + marketType = None + marketType, params = self.handle_market_type_and_params('fetchTransfers', None, params) + self.load_markets() + request: dict = {} + currency = None + if code is not None: + currency = self.currency(code) + fromAccountType = None + fromAccountType, params = self.handle_option_and_params(params, 'fetchTransfers', 'fromAccountType') + accountTypes = { + 'spot': 'SPOT', + 'swap': 'FUTURES', + 'futures': 'FUTURES', + 'future': 'FUTURES', + 'margin': 'SPOT', + } + if fromAccountType is not None: + request['fromAccountType'] = self.safe_string(accountTypes, fromAccountType, fromAccountType) + else: + raise ArgumentsRequired(self.id + ' fetchTransfers() requires a fromAccountType parameter, one of "SPOT", "FUTURES"') + toAccountType = None + toAccountType, params = self.handle_option_and_params(params, 'fetchTransfers', 'toAccountType') + if toAccountType is not None: + request['toAccountType'] = self.safe_string(accountTypes, toAccountType, toAccountType) + else: + raise ArgumentsRequired(self.id + ' fetchTransfers() requires a toAccountType parameter, one of "SPOT", "FUTURES"') + resultList = [] + if marketType == 'spot': + if since is not None: + request['startTime'] = since + if limit is not None: + if limit > 100: + raise ExchangeError('This exchange supports a maximum limit of 50') + request['size'] = limit + response = self.spotPrivateGetCapitalTransfer(self.extend(request, params)) + # + # + # { + # "rows": [ + # { + # "tranId": "cdf0d2a618b5458c965baefe6b1d0859", + # "clientTranId": null, + # "asset": "USDT", + # "amount": "1", + # "fromAccountType": "FUTURES", + # "toAccountType": "SPOT", + # "symbol": null, + # "status": "SUCCESS", + # "timestamp": 1759328309000 + # } + # ], + # "total": 1 + # } + resultList = self.safe_list(response, 'rows', []) + elif marketType == 'swap': + if limit is not None: + request['page_size'] = limit + response = self.contractPrivateGetAccountTransferRecord(self.extend(request, params)) + data = self.safe_value(response, 'data') + resultList = self.safe_value(data, 'resultList') + # + # { + # "success": True, + # "code": "0", + # "data": { + # "pageSize": "20", + # "totalCount": "10", + # "totalPage": "1", + # "currentPage": "1", + # "resultList": [ + # { + # "id": "2980812", + # "txid": "fa8a1e7bf05940a3b7025856dc48d025", + # "currency": "USDT", + # "amount": "22.90213135", + # "type": "IN", + # "state": "SUCCESS", + # "createTime": "1648849076000", + # "updateTime": "1648849076000" + # }, + # ] + # } + # } + # + return self.parse_transfers(resultList, currency, since, limit) + + def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#user-universal-transfer + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account to transfer from + :param str toAccount: account to transfer to + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.symbol]: market symbol required for margin account transfers eg:BTCUSDT + :returns dict: a `transfer structure ` + """ + self.load_markets() + currency = self.currency(code) + accounts: dict = { + 'spot': 'SPOT', + 'swap': 'FUTURES', + 'future': 'FUTURES', + } + fromId = self.safe_string(accounts, fromAccount, fromAccount) + toId = self.safe_string(accounts, toAccount, toAccount) + if fromId is None: + keys = list(accounts.keys()) + raise ExchangeError(self.id + ' fromAccount must be one of ' + ', '.join(keys)) + if toId is None: + keys = list(accounts.keys()) + raise ExchangeError(self.id + ' toAccount must be one of ' + ', '.join(keys)) + request: dict = { + 'asset': currency['id'], + 'amount': amount, + 'fromAccountType': fromId, + 'toAccountType': toId, + } + if (fromId == 'ISOLATED_MARGIN') or (toId == 'ISOLATED_MARGIN'): + symbol = self.safe_string(params, 'symbol') + params = self.omit(params, 'symbol') + if symbol is None: + raise ArgumentsRequired(self.id + ' transfer() requires a symbol argument for isolated margin') + market = self.market(symbol) + request['symbol'] = market['id'] + response = self.spotPrivatePostCapitalTransfer(self.extend(request, params)) + # + # { + # "tranId": "ebb06123e6a64f4ab234b396c548d57e" + # } + # + transaction = self.parse_transfer(response, currency) + return self.extend(transaction, { + 'amount': amount, + 'fromAccount': fromAccount, + 'toAccount': toAccount, + }) + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + # + # spot: fetchTransfer + # + # { + # "currency": "USDT", + # "amount": "1", + # "transact_id": "b60c1df8e7b24b268858003f374ecb75", + # "from": "MAIN", + # "to": "CONTRACT", + # "transact_state": "WAIT" + # } + # + # swap: fetchTransfer + # + # { + # "currency": "USDT", + # "amount": "22.90213135", + # "txid": "fa8a1e7bf05940a3b7025856dc48d025", + # "id": "2980812", + # "type": "IN", + # "state": "SUCCESS", + # "createTime": "1648849076000", + # "updateTime": "1648849076000" + # } + # { + # "tranId": "cdf0d2a618b5458c965baefe6b1d0859", + # "clientTranId": null, + # "asset": "USDT", + # "amount": "1", + # "fromAccountType": "FUTURES", + # "toAccountType": "SPOT", + # "symbol": null, + # "status": "SUCCESS", + # "timestamp": 1759328309000 + # } + # + # transfer + # + # { + # "tranId": "ebb06123e6a64f4ab234b396c548d57e" + # } + # + currencyId = self.safe_string_2(transfer, 'currency', 'asset') + id = self.safe_string_n(transfer, ['transact_id', 'txid', 'tranId']) + timestamp = self.safe_integer_2(transfer, 'createTime', 'timestamp') + datetime = self.iso8601(timestamp) if (timestamp is not None) else None + direction = self.safe_string(transfer, 'type') + accountFrom = None + accountTo = None + fromAccountType = self.safe_string(transfer, 'fromAccountType') + toAccountType = self.safe_string(transfer, 'toAccountType') + if (fromAccountType is not None) and (toAccountType is not None): + accountFrom = fromAccountType + accountTo = toAccountType + elif direction is not None: + accountFrom = 'MAIN' if (direction == 'IN') else 'CONTRACT' + accountTo = 'CONTRACT' if (direction == 'IN') else 'MAIN' + else: + accountFrom = self.safe_string(transfer, 'from') + accountTo = self.safe_string(transfer, 'to') + return { + 'info': transfer, + 'id': id, + 'timestamp': timestamp, + 'datetime': datetime, + 'currency': self.safe_currency_code(currencyId, currency), + 'amount': self.safe_number(transfer, 'amount'), + 'fromAccount': self.parse_account_id(accountFrom), + 'toAccount': self.parse_account_id(accountTo), + 'status': self.parse_transfer_status(self.safe_string_n(transfer, ['transact_state', 'state', 'status'])), + } + + def parse_account_id(self, status): + statuses: dict = { + 'SPOT': 'spot', + 'FUTURES': 'swap', + 'MAIN': 'spot', + 'CONTRACT': 'swap', + } + return self.safe_string(statuses, status, status) + + def parse_transfer_status(self, status: Str) -> Str: + statuses: dict = { + 'SUCCESS': 'ok', + 'FAILED': 'failed', + 'WAIT': 'pending', + } + return self.safe_string(statuses, status, status) + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#withdraw-new + https://www.mexc.com/api-docs/spot-v3/wallet-endpoints#internal-transfer + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :param dict [params.internal]: False by default, set to True for an "internal transfer" + :param dict [params.toAccountType]: skipped by default, set to 'EMAIL|UID|MOBILE' when making an "internal transfer" + :returns dict: a `transaction structure ` + """ + self.load_markets() + currency = self.currency(code) + tag, params = self.handle_withdraw_tag_and_params(tag, params) + internal = self.safe_bool(params, 'internal', False) + if internal: + params = self.omit(params, 'internal') + requestForInternal = { + 'asset': currency['id'], + 'amount': amount, + 'toAccount': address, + } + toAccountType = self.safe_string(params, 'toAccountType') + if toAccountType is None: + raise ArgumentsRequired(self.id + ' withdraw() requires a toAccountType parameter for internal transfer to be of: EMAIL | UID | MOBILE') + responseForInternal = self.spotPrivatePostCapitalTransferInternal(self.extend(requestForInternal, params)) + # + # { + # "id":"7213fea8e94b4a5593d507237e5a555b" + # } + # + return self.parse_transaction(responseForInternal, currency) + networks = self.safe_dict(self.options, 'networks', {}) + network = self.safe_string_2(params, 'network', 'netWork') # self line allows the user to specify either ERC20 or ETH + network = self.safe_string(networks, network, network) # handle ETH > ERC-20 alias + network = self.network_code_to_id(network, currency['code']) + self.check_address(address) + request: dict = { + 'coin': currency['id'], + 'address': address, + 'amount': amount, + } + if tag is not None: + request['memo'] = tag + if network is not None: + request['netWork'] = network + params = self.omit(params, ['network', 'netWork']) + response = self.spotPrivatePostCapitalWithdraw(self.extend(request, params)) + # + # { + # "id":"7213fea8e94b4a5593d507237e5a555b" + # } + # + return self.parse_transaction(response, currency) + + def set_position_mode(self, hedged: bool, symbol: Str = None, params={}): + """ + set hedged to True or False for a market + + https://mexcdevelop.github.io/apidocs/contract_v1_en/#change-position-mode + + :param bool hedged: set to True to use dualSidePosition + :param str symbol: not used by mexc setPositionMode() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + request: dict = { + 'positionMode': 1 if hedged else 2, # 1 Hedge, 2 One-way, before changing position mode make sure that there are no active orders, planned orders, or open positions, the risk limit level will be reset to 1 + } + response = self.contractPrivatePostPositionChangePositionMode(self.extend(request, params)) + # + # { + # "success":true, + # "code":0 + # } + # + return response + + def fetch_position_mode(self, symbol: Str = None, params={}): + """ + fetchs the position mode, hedged or one way, hedged for binance is set identically for all linear markets or all inverse markets + + https://mexcdevelop.github.io/apidocs/contract_v1_en/#get-position-mode + + :param str symbol: not used by mexc fetchPositionMode + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an object detailing whether the market is in hedged or one-way mode + """ + response = self.contractPrivateGetPositionPositionMode(params) + # + # { + # "success":true, + # "code":0, + # "data":2 + # } + # + positionMode = self.safe_integer(response, 'data') + return { + 'info': response, + 'hedged': (positionMode == 1), + } + + def fetch_transaction_fees(self, codes: Strings = None, params={}): + """ + fetch deposit and withdrawal fees + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#query-the-currency-information + + :param str[]|None codes: returns fees for all currencies if None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `fee structures ` + """ + self.load_markets() + response = self.spotPrivateGetCapitalConfigGetall(params) + # + # [ + # { + # "coin": "AGLD", + # "name": "Adventure Gold", + # "networkList": [ + # { + # "coin": "AGLD", + # "depositDesc": null, + # "depositEnable": True, + # "minConfirm": "0", + # "name": "Adventure Gold", + # "network": "ERC20", + # "withdrawEnable": True, + # "withdrawFee": "10.000000000000000000", + # "withdrawIntegerMultiple": null, + # "withdrawMax": "1200000.000000000000000000", + # "withdrawMin": "20.000000000000000000", + # "sameAddress": False, + # "contract": "0x32353a6c91143bfd6c7d363b546e62a9a2489a20", + # "withdrawTips": null, + # "depositTips": null + # } + # ... + # ] + # }, + # ... + # ] + # + return self.parse_transaction_fees(response, codes) + + def parse_transaction_fees(self, response, codes=None): + withdrawFees: dict = {} + for i in range(0, len(response)): + entry = response[i] + currencyId = self.safe_string(entry, 'coin') + currency = self.safe_currency(currencyId) + code = self.safe_string(currency, 'code') + if (codes is None) or (self.in_array(code, codes)): + withdrawFees[code] = self.parse_transaction_fee(entry, currency) + return { + 'withdraw': withdrawFees, + 'deposit': {}, + 'info': response, + } + + def parse_transaction_fee(self, transaction, currency: Currency = None): + # + # { + # "coin": "AGLD", + # "name": "Adventure Gold", + # "networkList": [ + # { + # "coin": "AGLD", + # "depositDesc": null, + # "depositEnable": True, + # "minConfirm": "0", + # "name": "Adventure Gold", + # "network": "ERC20", + # "withdrawEnable": True, + # "withdrawFee": "10.000000000000000000", + # "withdrawIntegerMultiple": null, + # "withdrawMax": "1200000.000000000000000000", + # "withdrawMin": "20.000000000000000000", + # "sameAddress": False, + # "contract": "0x32353a6c91143bfd6c7d363b546e62a9a2489a20", + # "withdrawTips": null, + # "depositTips": null + # } + # ... + # ] + # } + # + networkList = self.safe_value(transaction, 'networkList', []) + result: dict = {} + for j in range(0, len(networkList)): + networkEntry = networkList[j] + networkId = self.safe_string(networkEntry, 'network') + networkCode = self.safe_string(self.options['networks'], networkId, networkId) + fee = self.safe_number(networkEntry, 'withdrawFee') + result[networkCode] = fee + return result + + def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}): + """ + fetch deposit and withdrawal fees + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#query-the-currency-information + + :param str[]|None codes: returns fees for all currencies if None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `fee structures ` + """ + self.load_markets() + response = self.spotPrivateGetCapitalConfigGetall(params) + # + # [ + # { + # "coin": "AGLD", + # "name": "Adventure Gold", + # "networkList": [ + # { + # "coin": "AGLD", + # "depositDesc": null, + # "depositEnable": True, + # "minConfirm": "0", + # "name": "Adventure Gold", + # "network": "ERC20", + # "withdrawEnable": True, + # "withdrawFee": "10.000000000000000000", + # "withdrawIntegerMultiple": null, + # "withdrawMax": "1200000.000000000000000000", + # "withdrawMin": "20.000000000000000000", + # "sameAddress": False, + # "contract": "0x32353a6c91143bfd6c7d363b546e62a9a2489a20", + # "withdrawTips": null, + # "depositTips": null + # } + # ... + # ] + # }, + # ... + # ] + # + return self.parse_deposit_withdraw_fees(response, codes, 'coin') + + def parse_deposit_withdraw_fee(self, fee, currency: Currency = None): + # + # { + # "coin": "AGLD", + # "name": "Adventure Gold", + # "networkList": [ + # { + # "coin": "AGLD", + # "depositDesc": null, + # "depositEnable": True, + # "minConfirm": "0", + # "name": "Adventure Gold", + # "network": "ERC20", + # "withdrawEnable": True, + # "withdrawFee": "10.000000000000000000", + # "withdrawIntegerMultiple": null, + # "withdrawMax": "1200000.000000000000000000", + # "withdrawMin": "20.000000000000000000", + # "sameAddress": False, + # "contract": "0x32353a6c91143bfd6c7d363b546e62a9a2489a20", + # "withdrawTips": null, + # "depositTips": null + # } + # ... + # ] + # } + # + networkList = self.safe_value(fee, 'networkList', []) + result = self.deposit_withdraw_fee(fee) + for j in range(0, len(networkList)): + networkEntry = networkList[j] + networkId = self.safe_string(networkEntry, 'network') + networkCode = self.network_id_to_code(networkId, self.safe_string(currency, 'code')) + result['networks'][networkCode] = { + 'withdraw': { + 'fee': self.safe_number(networkEntry, 'withdrawFee'), + 'percentage': None, + }, + 'deposit': { + 'fee': None, + 'percentage': None, + }, + } + return self.assign_default_deposit_withdraw_fees(result) + + def fetch_leverage(self, symbol: str, params={}) -> Leverage: + """ + fetch the set leverage for a market + + https://mexcdevelop.github.io/apidocs/contract_v1_en/#get-leverage + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `leverage structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = self.contractPrivateGetPositionLeverage(self.extend(request, params)) + # + # { + # "success": True, + # "code": 0, + # "data": [ + # { + # "level": 1, + # "maxVol": 463300, + # "mmr": 0.004, + # "imr": 0.005, + # "positionType": 1, + # "openType": 1, + # "leverage": 20, + # "limitBySys": False, + # "currentMmr": 0.004 + # }, + # { + # "level": 1, + # "maxVol": 463300, + # "mmr": 0.004, + # "imr": 0.005, + # "positionType": 2, + # "openType": 1, + # "leverage": 20, + # "limitBySys": False, + # "currentMmr": 0.004 + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_leverage(data, market) + + def parse_leverage(self, leverage: dict, market: Market = None) -> Leverage: + marginMode = None + longLeverage = None + shortLeverage = None + for i in range(0, len(leverage)): + entry = leverage[i] + openType = self.safe_integer(entry, 'openType') + positionType = self.safe_integer(entry, 'positionType') + if positionType == 1: + longLeverage = self.safe_integer(entry, 'leverage') + elif positionType == 2: + shortLeverage = self.safe_integer(entry, 'leverage') + marginMode = 'isolated' if (openType == 1) else 'cross' + return { + 'info': leverage, + 'symbol': market['symbol'], + 'marginMode': marginMode, + 'longLeverage': longLeverage, + 'shortLeverage': shortLeverage, + } + + def handle_margin_mode_and_params(self, methodName, params={}, defaultValue=None): + """ + @ignore + marginMode specified by params["marginMode"], self.options["marginMode"], self.options["defaultMarginMode"], params["margin"] = True or self.options["defaultType"] = 'margin' + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.margin]: True for trading spot-margin + :returns Array: the marginMode in lowercase + """ + defaultType = self.safe_string(self.options, 'defaultType') + isMargin = self.safe_bool(params, 'margin', False) + marginMode = None + marginMode, params = super(mexc, self).handle_margin_mode_and_params(methodName, params, defaultValue) + if (defaultType == 'margin') or (isMargin is True): + marginMode = 'isolated' + return [marginMode, params] + + def fetch_positions_history(self, symbols: Strings = None, since: Int = None, limit: Int = None, params={}) -> List[Position]: + """ + fetches historical positions + + https://mexcdevelop.github.io/apidocs/contract_v1_en/#get-the-user-s-history-position-information + + :param str[] [symbols]: unified contract symbols + :param int [since]: not used by mexc fetchPositionsHistory + :param int [limit]: the maximum amount of candles to fetch, default=1000 + :param dict [params]: extra parameters specific to the exchange api endpoint + + EXCHANGE SPECIFIC PARAMETERS + :param int [params.type]: position type,1: long, 2: short + :param int [params.page_num]: current page number, default is 1 + :returns dict[]: a list of `position structures ` + """ + self.load_markets() + request: dict = {} + if symbols is not None: + symbolsLength = len(symbols) + if symbolsLength == 1: + market = self.market(symbols[0]) + request['symbol'] = market['id'] + if limit is not None: + request['page_size'] = limit + response = self.contractPrivateGetPositionListHistoryPositions(self.extend(request, params)) + # + # { + # success: True, + # code: '0', + # data: [ + # { + # positionId: '390281084', + # symbol: 'RVN_USDT', + # positionType: '1', + # openType: '2', + # state: '3', + # holdVol: '0', + # frozenVol: '0', + # closeVol: '1141', + # holdAvgPrice: '0.03491', + # holdAvgPriceFullyScale: '0.03491', + # openAvgPrice: '0.03491', + # openAvgPriceFullyScale: '0.03491', + # closeAvgPrice: '0.03494', + # liquidatePrice: '0.03433', + # oim: '0', + # im: '0', + # holdFee: '0', + # realised: '0.1829', + # leverage: '50', + # createTime: '1711512408000', + # updateTime: '1711512553000', + # autoAddIm: False, + # version: '4', + # profitRatio: '0.0227', + # newOpenAvgPrice: '0.03491', + # newCloseAvgPrice: '0.03494', + # closeProfitLoss: '0.3423', + # fee: '0.1593977', + # positionShowStatus: 'CLOSED' + # }, + # ... + # ] + # } + # + data = self.safe_list(response, 'data') + positions = self.parse_positions(data, symbols, params) + return self.filter_by_since_limit(positions, since, limit) + + def set_margin_mode(self, marginMode: str, symbol: Str = None, params={}): + """ + set margin mode to 'cross' or 'isolated' + + https://mexcdevelop.github.io/apidocs/contract_v1_en/#switch-leverage + + :param str marginMode: 'cross' or 'isolated' + :param str [symbol]: required when there is no position, else provide params["positionId"] + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.positionId]: required when a position is set + :param str [params.direction]: "long" or "short" required when there is no position + :returns dict: response from the exchange + """ + self.load_markets() + market = self.market(symbol) + if market['spot']: + raise BadSymbol(self.id + ' setMarginMode() supports contract markets only') + marginMode = marginMode.lower() + if marginMode != 'isolated' and marginMode != 'cross': + raise BadRequest(self.id + ' setMarginMode() marginMode argument should be isolated or cross') + leverage = self.safe_integer(params, 'leverage') + if leverage is None: + raise ArgumentsRequired(self.id + ' setMarginMode() requires a leverage parameter') + direction = self.safe_string_lower_2(params, 'direction', 'positionId') + request: dict = { + 'leverage': leverage, + 'openType': 1 if (marginMode == 'isolated') else 2, + } + if symbol is not None: + request['symbol'] = market['id'] + if direction is not None: + request['positionType'] = 2 if (direction == 'short') else 1 + params = self.omit(params, 'direction') + response = self.contractPrivatePostPositionChangeLeverage(self.extend(request, params)) + # + # {success: True, code: '0'} + # + return self.parse_leverage(response, market) # tmp revert type + + def nonce(self): + return self.milliseconds() - self.safe_integer(self.options, 'timeDifference', 0) + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + section = self.safe_string(api, 0) + access = self.safe_string(api, 1) + path, params = self.resolve_path(path, params) + url = None + if section == 'spot' or section == 'broker': + if section == 'broker': + url = self.urls['api'][section][access] + '/' + path + else: + url = self.urls['api'][section][access] + '/api/' + self.version + '/' + path + urlParams = params + if access == 'private': + if section == 'broker' and ((method == 'POST') or (method == 'PUT') or (method == 'DELETE')): + urlParams = { + 'timestamp': self.nonce(), + 'recvWindow': self.safe_integer(self.options, 'recvWindow', 5000), + } + body = self.json(params) + else: + urlParams['timestamp'] = self.nonce() + urlParams['recvWindow'] = self.safe_integer(self.options, 'recvWindow', 5000) + paramsEncoded = '' + if urlParams: + paramsEncoded = self.urlencode(urlParams) + url += '?' + paramsEncoded + if access == 'private': + self.check_required_credentials() + signature = self.hmac(self.encode(paramsEncoded), self.encode(self.secret), hashlib.sha256) + url += '&' + 'signature=' + signature + headers = { + 'X-MEXC-APIKEY': self.apiKey, + 'source': self.safe_string(self.options, 'broker', 'CCXT'), + } + if (method == 'POST') or (method == 'PUT') or (method == 'DELETE'): + headers['Content-Type'] = 'application/json' + elif section == 'contract' or section == 'spot2': + url = self.urls['api'][section][access] + '/' + self.implode_params(path, params) + params = self.omit(params, self.extract_params(path)) + if access == 'public': + if params: + url += '?' + self.urlencode(params) + else: + self.check_required_credentials() + timestamp = str(self.nonce()) + auth = '' + headers = { + 'ApiKey': self.apiKey, + 'Request-Time': timestamp, + 'Content-Type': 'application/json', + 'source': self.safe_string(self.options, 'broker', 'CCXT'), + } + if method == 'POST': + auth = self.json(params) + body = auth + else: + params = self.keysort(params) + if params: + auth += self.urlencode(params) + url += '?' + auth + auth = self.apiKey + timestamp + auth + signature = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256) + headers['Signature'] = signature + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None + # spot + # {"code":-1128,"msg":"Combination of optional parameters invalid.","_extend":null} + # {"success":false,"code":123456,"message":"Order quantity error...."} + # + # contract + # + # {"code":10232,"msg":"The currency not exist"} + # {"code":10216,"msg":"No available deposit address"} + # {"success":true, "code":0, "data":1634095541710} + # + success = self.safe_bool(response, 'success', False) # v1 + if success is True: + return None + responseCode = self.safe_string(response, 'code', None) + if (responseCode is not None) and (responseCode != '200') and (responseCode != '0'): + feedback = self.id + ' ' + body + self.throw_broadly_matched_exception(self.exceptions['broad'], body, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], responseCode, feedback) + raise ExchangeError(feedback) + return None diff --git a/ccxt/modetrade.py b/ccxt/modetrade.py new file mode 100644 index 0000000..19cec11 --- /dev/null +++ b/ccxt/modetrade.py @@ -0,0 +1,2818 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.modetrade import ImplicitAPI +from ccxt.base.types import Any, Balances, Currencies, Currency, Int, LedgerEntry, Leverage, Market, Num, Order, OrderBook, OrderRequest, OrderSide, OrderType, Position, Str, Strings, FundingRate, FundingRates, Trade, TradingFees, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import NotSupported +from ccxt.base.errors import NetworkError +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class modetrade(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(modetrade, self).describe(), { + 'id': 'modetrade', + 'name': 'Mode Trade', + 'countries': ['KY'], # Cayman Islands + 'rateLimit': 100, + 'version': 'v1', + 'certified': False, + 'pro': True, + 'dex': True, + 'hostname': 'trade.mode.network', + 'has': { + 'CORS': None, + 'spot': False, + 'margin': False, + 'swap': True, + 'future': False, + 'option': False, + 'addMargin': False, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'cancelOrders': True, + 'cancelWithdraw': False, + 'closeAllPositions': False, + 'closePosition': False, + 'createConvertTrade': False, + 'createDepositAddress': False, + 'createMarketBuyOrderWithCost': False, + 'createMarketOrder': True, + 'createMarketOrderWithCost': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createOrderWithTakeProfitAndStopLoss': True, + 'createReduceOnlyOrder': True, + 'createStopLimitOrder': True, + 'createStopLossOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'createTakeProfitOrder': True, + 'createTrailingAmountOrder': False, + 'createTrailingPercentOrder': False, + 'createTriggerOrder': True, + 'fetchAccounts': False, + 'fetchBalance': True, + 'fetchCanceledOrders': False, + 'fetchClosedOrder': False, + 'fetchClosedOrders': True, + 'fetchConvertCurrencies': False, + 'fetchConvertQuote': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': False, + 'fetchDeposits': True, + 'fetchDepositsWithdrawals': True, + 'fetchFundingHistory': True, + 'fetchFundingInterval': True, + 'fetchFundingIntervals': False, + 'fetchFundingRate': True, + 'fetchFundingRateHistory': True, + 'fetchFundingRates': True, + 'fetchIndexOHLCV': False, + 'fetchLedger': True, + 'fetchLeverage': True, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterestHistory': False, + 'fetchOpenOrder': False, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchOrderTrades': True, + 'fetchPosition': True, + 'fetchPositionMode': False, + 'fetchPositions': True, + 'fetchPremiumIndexOHLCV': False, + 'fetchStatus': True, + 'fetchTicker': False, + 'fetchTickers': False, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': True, + 'fetchTransactions': 'emulated', + 'fetchTransfers': False, + 'fetchWithdrawals': True, + 'reduceMargin': False, + 'setLeverage': True, + 'setMargin': False, + 'setPositionMode': False, + 'transfer': False, + 'withdraw': True, # exchange have that endpoint disabled atm, but was once implemented in ccxt per old docs: https://kronosresearch.github.io/wootrade-documents/#token-withdraw + }, + 'timeframes': { + '1m': '1m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1h', + '4h': '4h', + '12h': '12h', + '1d': '1d', + '1w': '1w', + '1M': '1mon', + '1y': '1y', + }, + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/cec2b7f1-3b2b-4502-971b-447ee1937d6b', + 'api': { + 'public': 'https://api-evm.orderly.org', + 'private': 'https://api-evm.orderly.org', + }, + 'test': { + 'public': 'https://testnet-api-evm.orderly.org', + 'private': 'https://testnet-api-evm.orderly.org', + }, + 'www': 'https://trade.mode.network', + 'referral': { + 'url': 'https://trade.mode.network?ref=MODETRADE', + 'discount': 0.2, + }, + }, + 'api': { + 'v1': { + 'public': { + 'get': { + 'public/volume/stats': 1, + 'public/broker/name': 1, + 'public/chain_info/{broker_id}': 1, + 'public/system_info': 1, + 'public/vault_balance': 1, + 'public/insurancefund': 1, + 'public/chain_info': 1, + 'faucet/usdc': 1, + 'public/account': 1, + 'get_account': 1, + 'registration_nonce': 1, + 'get_orderly_key': 1, + 'public/liquidation': 1, + 'public/liquidated_positions': 1, + 'public/config': 1, + 'public/campaign/ranking': 10, + 'public/campaign/stats': 10, + 'public/campaign/user': 10, + 'public/campaign/stats/details': 10, + 'public/campaigns': 10, + 'public/points/leaderboard': 1, + 'client/points': 1, + 'public/points/epoch': 1, + 'public/points/epoch_dates': 1, + 'public/referral/check_ref_code': 1, + 'public/referral/verify_ref_code': 1, + 'referral/admin_info': 1, + 'referral/info': 1, + 'referral/referee_info': 1, + 'referral/referee_rebate_summary': 1, + 'referral/referee_history': 1, + 'referral/referral_history': 1, + 'referral/rebate_summary': 1, + 'client/distribution_history': 1, + 'tv/config': 1, + 'tv/history': 1, + 'tv/symbol_info': 1, + 'public/funding_rate_history': 1, + 'public/funding_rate/{symbol}': 0.33, + 'public/funding_rates': 1, + 'public/info': 1, + 'public/info/{symbol}': 1, + 'public/market_trades': 1, + 'public/token': 1, + 'public/futures': 1, + 'public/futures/{symbol}': 1, + }, + 'post': { + 'register_account': 1, + }, + }, + 'private': { + 'get': { + 'client/key_info': 6, + 'client/orderly_key_ip_restriction': 6, + 'order/{oid}': 1, + 'client/order/{client_order_id}': 1, + 'algo/order/{oid}': 1, + 'algo/client/order/{client_order_id}': 1, + 'orders': 1, + 'algo/orders': 1, + 'trade/{tid}': 1, + 'trades': 1, + 'order/{oid}/trades': 1, + 'client/liquidator_liquidations': 1, + 'liquidations': 1, + 'asset/history': 60, + 'client/holding': 1, + 'withdraw_nonce': 1, + 'settle_nonce': 1, + 'pnl_settlement/history': 1, + 'volume/user/daily': 60, + 'volume/user/stats': 60, + 'client/statistics': 60, + 'client/info': 60, + 'client/statistics/daily': 60, + 'positions': 3.33, + 'position/{symbol}': 3.33, + 'funding_fee/history': 30, + 'notification/inbox/notifications': 60, + 'notification/inbox/unread': 60, + 'volume/broker/daily': 60, + 'broker/fee_rate/default': 10, + 'broker/user_info': 10, + 'orderbook/{symbol}': 1, + 'kline': 1, + }, + 'post': { + 'orderly_key': 1, + 'client/set_orderly_key_ip_restriction': 6, + 'client/reset_orderly_key_ip_restriction': 6, + 'order': 1, + 'batch-order': 10, + 'algo/order': 1, + 'liquidation': 1, + 'claim_insurance_fund': 1, + 'withdraw_request': 1, + 'settle_pnl': 1, + 'notification/inbox/mark_read': 60, + 'notification/inbox/mark_read_all': 60, + 'client/leverage': 120, + 'client/maintenance_config': 60, + 'delegate_signer': 10, + 'delegate_orderly_key': 10, + 'delegate_settle_pnl': 10, + 'delegate_withdraw_request': 10, + 'broker/fee_rate/set': 10, + 'broker/fee_rate/set_default': 10, + 'broker/fee_rate/default': 10, + 'referral/create': 10, + 'referral/update': 10, + 'referral/bind': 10, + 'referral/edit_split': 10, + }, + 'put': { + 'order': 1, + 'algo/order': 1, + }, + 'delete': { + 'order': 1, + 'algo/order': 1, + 'client/order': 1, + 'algo/client/order': 1, + 'algo/orders': 1, + 'orders': 1, + 'batch-order': 1, + 'client/batch-order': 1, + }, + }, + }, + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + 'accountId': True, + 'privateKey': False, + }, + 'fees': { + 'trading': { + 'tierBased': True, + 'percentage': True, + 'maker': self.parse_number('0.0002'), + 'taker': self.parse_number('0.0005'), + }, + }, + 'options': { + 'sandboxMode': False, + 'brokerId': 'CCXTMODE', + 'verifyingContractAddress': '0x6F7a338F2aA472838dEFD3283eB360d4Dff5D203', + }, + 'features': { + 'default': { + 'sandbox': True, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, # todo by triggerPrice + 'takeProfitPrice': False, # todo by triggerPrice + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': True, + 'leverage': True, # todo implement + 'marketBuyByCost': False, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': False, + 'iceberg': True, # todo implement + }, + 'createOrders': { + 'max': 10, + }, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 500, + 'daysBack': None, + 'untilDays': 100000, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': True, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 500, + 'trigger': True, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 500, + 'daysBack': None, + 'daysBackCanceled': None, + 'untilDays': 100000, + 'trigger': True, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 1000, + }, + }, + 'spot': { + 'extends': 'default', + }, + 'forDerivatives': { + 'extends': 'default', + 'createOrder': { + # todo: implementation needs unification + 'triggerPriceType': None, + 'attachedStopLossTakeProfit': { + # todo: implementation needs unification + 'triggerPriceType': None, + 'price': False, + }, + }, + }, + 'swap': { + 'linear': { + 'extends': 'forDerivatives', + }, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'commonCurrencies': {}, + 'exceptions': { + 'exact': { + '-1000': ExchangeError, # UNKNOWN The data does not exist + '-1001': AuthenticationError, # INVALID_SIGNATURE The api key or secret is in wrong format. + '-1002': AuthenticationError, # UNAUTHORIZED API key or secret is invalid, it may because key have insufficient permission or the key is expired/revoked. + '-1003': RateLimitExceeded, # TOO_MANY_REQUEST Rate limit exceed. + '-1004': BadRequest, # UNKNOWN_PARAM An unknown parameter was sent. + '-1005': BadRequest, # INVALID_PARAM Some parameters are in wrong format for api. + '-1006': InvalidOrder, # RESOURCE_NOT_FOUND The data is not found in server. For example, when client try canceling a CANCELLED order, will raise self error. + '-1007': BadRequest, # DUPLICATE_REQUEST The data is already exists or your request is duplicated. + '-1008': InvalidOrder, # QUANTITY_TOO_HIGH The quantity of settlement is too high than you can request. + '-1009': InsufficientFunds, # CAN_NOT_WITHDRAWAL Can not request withdrawal settlement, you need to deposit other arrears first. + '-1011': NetworkError, # RPC_NOT_CONNECT Can not place/cancel orders, it may because internal network error. Please try again in a few seconds. + '-1012': BadRequest, # RPC_REJECT The place/cancel order request is rejected by internal module, it may because the account is in liquidation or other internal errors. Please try again in a few seconds. + '-1101': InsufficientFunds, # RISK_TOO_HIGH The risk exposure for client is too high, it may cause by sending too big order or the leverage is too low. please refer to client info to check the current exposure. + '-1102': InvalidOrder, # MIN_NOTIONAL The order value(price * size) is too small. + '-1103': InvalidOrder, # PRICE_FILTER The order price is not following the tick size rule for the symbol. + '-1104': InvalidOrder, # SIZE_FILTER The order quantity is not following the step size rule for the symbol. + '-1105': InvalidOrder, # PERCENTAGE_FILTER Price is X% too high or X% too low from the mid price. + '-1201': BadRequest, # LIQUIDATION_REQUEST_RATIO_TOO_SMALL total notional < 10000, least req ratio should = 1 + '-1202': BadRequest, # LIQUIDATION_STATUS_ERROR No need to liquidation because user margin is enough. + '29': BadRequest, # {"success":false,"code":29,"message":"Verify contract is invalid"} + '9': AuthenticationError, # {"success":false,"code":9,"message":"Address and signature do not match"} + '3': AuthenticationError, # {"success":false,"code":3,"message":"Signature error"} + '2': BadRequest, # {"success":false,"code":2,"message":"Timestamp expired"} + '15': BadRequest, # {"success":false,"code":15,"message":"BrokerId is not exist"} + }, + 'broad': { + }, + }, + 'precisionMode': TICK_SIZE, + }) + + def set_sandbox_mode(self, enable: bool): + super(modetrade, self).set_sandbox_mode(enable) + self.options['sandboxMode'] = enable + + def fetch_status(self, params={}): + """ + the latest known information on the availability of the exchange API + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/public/get-system-maintenance-status + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `status structure ` + """ + response = self.v1PublicGetPublicSystemInfo(params) + # + # { + # "success": True, + # "data": { + # "status": 0, + # "msg": "System is functioning properly." + # }, + # "timestamp": "1709274106602" + # } + # + data = self.safe_dict(response, 'data', {}) + status = self.safe_string(data, 'status') + if status is None: + status = 'error' + elif status == '0': + status = 'ok' + else: + status = 'maintenance' + return { + 'status': status, + 'updated': None, + 'eta': None, + 'url': None, + 'info': response, + } + + def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/public/get-system-maintenance-status + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = self.v1PublicGetPublicSystemInfo(params) + # + # { + # "success": True, + # "data": { + # "status": 0, + # "msg": "System is functioning properly." + # }, + # "timestamp": "1709274106602" + # } + # + return self.safe_integer(response, 'timestamp') + + def parse_market(self, market: dict) -> Market: + # + # { + # "symbol": "PERP_BTC_USDC", + # "quote_min": 123, + # "quote_max": 100000, + # "quote_tick": 0.1, + # "base_min": 0.00001, + # "base_max": 20, + # "base_tick": 0.00001, + # "min_notional": 1, + # "price_range": 0.02, + # "price_scope": 0.4, + # "std_liquidation_fee": 0.03, + # "liquidator_fee": 0.015, + # "claim_insurance_fund_discount": 0.0075, + # "funding_period": 8, + # "cap_funding": 0.000375, + # "floor_funding": -0.000375, + # "interest_rate": 0.0001, + # "created_time": 1684140107326, + # "updated_time": 1685345968053, + # "base_mmr": 0.05, + # "base_imr": 0.1, + # "imr_factor": 0.0002512, + # "liquidation_tier": "1" + # } + # + marketId = self.safe_string(market, 'symbol') + parts = marketId.split('_') + marketType = 'swap' + baseId = self.safe_string(parts, 1) + quoteId = self.safe_string(parts, 2) + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + settleId: Str = self.safe_string(parts, 2) + settle: Str = self.safe_currency_code(settleId) + symbol = base + '/' + quote + ':' + settle + return { + 'id': marketId, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': marketType, + 'spot': False, + 'margin': False, + 'swap': True, + 'future': False, + 'option': False, + 'active': None, + 'contract': True, + 'linear': True, + 'inverse': False, + 'contractSize': self.parse_number('1'), + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.safe_number(market, 'base_tick'), + 'price': self.safe_number(market, 'quote_tick'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(market, 'base_min'), + 'max': self.safe_number(market, 'base_max'), + }, + 'price': { + 'min': self.safe_number(market, 'quote_min'), + 'max': self.safe_number(market, 'quote_max'), + }, + 'cost': { + 'min': self.safe_number(market, 'min_notional'), + 'max': None, + }, + }, + 'created': self.safe_integer(market, 'created_time'), + 'info': market, + } + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for modetrade + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/public/get-available-symbols + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = self.v1PublicGetPublicInfo(params) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "rows": [ + # { + # "symbol": "PERP_BTC_USDC", + # "quote_min": 123, + # "quote_max": 100000, + # "quote_tick": 0.1, + # "base_min": 0.00001, + # "base_max": 20, + # "base_tick": 0.00001, + # "min_notional": 1, + # "price_range": 0.02, + # "price_scope": 0.4, + # "std_liquidation_fee": 0.03, + # "liquidator_fee": 0.015, + # "claim_insurance_fund_discount": 0.0075, + # "funding_period": 8, + # "cap_funding": 0.000375, + # "floor_funding": -0.000375, + # "interest_rate": 0.0001, + # "created_time": 1684140107326, + # "updated_time": 1685345968053, + # "base_mmr": 0.05, + # "base_imr": 0.1, + # "imr_factor": 0.0002512, + # "liquidation_tier": "1" + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + rows = self.safe_list(data, 'rows', []) + return self.parse_markets(rows) + + def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/public/get-token-info + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + result: dict = {} + response = self.v1PublicGetPublicToken(params) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "rows": [{ + # "token": "USDC", + # "decimals": 6, + # "minimum_withdraw_amount": 0.000001, + # "token_hash": "0xd6aca1be9729c13d677335161321649cccae6a591554772516700f986f942eaa", + # "chain_details": [{ + # "chain_id": 43113, + # "contract_address": "0x5d64c9cfb0197775b4b3ad9be4d3c7976e0d8dc3", + # "cross_chain_withdrawal_fee": 123, + # "decimals": 6, + # "withdraw_fee": 2 + # }] + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + tokenRows = self.safe_list(data, 'rows', []) + for i in range(0, len(tokenRows)): + token = tokenRows[i] + currencyId = self.safe_string(token, 'token') + networks = self.safe_list(token, 'chain_details') + code = self.safe_currency_code(currencyId) + minPrecision = None + resultingNetworks: dict = {} + for j in range(0, len(networks)): + network = networks[j] + # TODO: transform chain id to human readable name + networkId = self.safe_string(network, 'chain_id') + precision = self.parse_precision(self.safe_string(network, 'decimals')) + if precision is not None: + minPrecision = precision if (minPrecision is None) else Precise.string_min(precision, minPrecision) + resultingNetworks[networkId] = { + 'id': networkId, + 'network': networkId, + 'limits': { + 'withdraw': { + 'min': None, + 'max': None, + }, + 'deposit': { + 'min': None, + 'max': None, + }, + }, + 'active': None, + 'deposit': None, + 'withdraw': None, + 'fee': self.safe_number(network, 'withdrawal_fee'), + 'precision': self.parse_number(precision), + 'info': network, + } + result[code] = self.safe_currency_structure({ + 'id': currencyId, + 'name': currencyId, + 'code': code, + 'precision': self.parse_number(minPrecision), + 'active': None, + 'fee': None, + 'networks': resultingNetworks, + 'deposit': None, + 'withdraw': None, + 'limits': { + 'deposit': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': self.safe_number(token, 'minimum_withdraw_amount'), + 'max': None, + }, + }, + 'info': token, + }) + return result + + def parse_token_and_fee_temp(self, item, feeTokenKey, feeAmountKey): + feeCost = self.safe_string(item, feeAmountKey) + fee = None + if feeCost is not None: + feeCurrencyId = self.safe_string(item, feeTokenKey) + feeCurrencyCode = self.safe_currency_code(feeCurrencyId) + fee = { + 'cost': feeCost, + 'currency': feeCurrencyCode, + } + return fee + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # public/market_trades + # + # { + # "symbol": "PERP_ETH_USDC", + # "side": "SELL", + # "executed_price": 46222.35, + # "executed_quantity": 0.0012, + # "executed_timestamp": "1683878609166" + # } + # + # fetchOrderTrades, fetchOrder + # + # { + # "id": "99119876", + # "symbol": "PERP_BTC_USDC", + # "fee": "0.0024", + # "side": "BUY", + # "executed_timestamp": "1641481113084", + # "order_id": "87001234", + # "order_tag": "default", <-- self param only in "fetchOrderTrades" + # "executed_price": "1", + # "executed_quantity": "12", + # "fee_asset": "BTC", + # "is_maker": "1" + # } + # + isFromFetchOrder = ('id' in trade) + timestamp = self.safe_integer(trade, 'executed_timestamp') + marketId = self.safe_string(trade, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + price = self.safe_string(trade, 'executed_price') + amount = self.safe_string(trade, 'executed_quantity') + order_id = self.safe_string(trade, 'order_id') + fee = self.parse_token_and_fee_temp(trade, 'fee_asset', 'fee') + feeCost = self.safe_string(fee, 'cost') + if feeCost is not None: + fee['cost'] = feeCost + cost = Precise.string_mul(price, amount) + side = self.safe_string_lower(trade, 'side') + id = self.safe_string(trade, 'id') + takerOrMaker: Str = None + if isFromFetchOrder: + isMaker = self.safe_string(trade, 'is_maker') == '1' + takerOrMaker = 'maker' if isMaker else 'taker' + return self.safe_trade({ + 'id': id, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'side': side, + 'price': price, + 'amount': amount, + 'cost': cost, + 'order': order_id, + 'takerOrMaker': takerOrMaker, + 'type': None, + 'fee': fee, + 'info': trade, + }, market) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/public/get-market-trades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['limit'] = limit + response = self.v1PublicGetPublicMarketTrades(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "rows": [{ + # "symbol": "PERP_ETH_USDC", + # "side": "BUY", + # "executed_price": 2050, + # "executed_quantity": 1, + # "executed_timestamp": 1683878609166 + # }] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + rows = self.safe_list(data, 'rows', []) + return self.parse_trades(rows, market, since, limit) + + def parse_funding_rate(self, fundingRate, market: Market = None) -> FundingRate: + # + # { + # "symbol":"PERP_AAVE_USDT", + # "est_funding_rate":-0.00003447, + # "est_funding_rate_timestamp":1653633959001, + # "last_funding_rate":-0.00002094, + # "last_funding_rate_timestamp":1653631200000, + # "next_funding_time":1653634800000, + # "sum_unitary_funding": 521.367 + # } + # + symbol = self.safe_string(fundingRate, 'symbol') + market = self.market(symbol) + nextFundingTimestamp = self.safe_integer(fundingRate, 'next_funding_time') + estFundingRateTimestamp = self.safe_integer(fundingRate, 'est_funding_rate_timestamp') + lastFundingRateTimestamp = self.safe_integer(fundingRate, 'last_funding_rate_timestamp') + fundingTimeString = self.safe_string(fundingRate, 'last_funding_rate_timestamp') + nextFundingTimeString = self.safe_string(fundingRate, 'next_funding_time') + millisecondsInterval = Precise.string_sub(nextFundingTimeString, fundingTimeString) + return { + 'info': fundingRate, + 'symbol': market['symbol'], + 'markPrice': None, + 'indexPrice': None, + 'interestRate': self.parse_number('0'), + 'estimatedSettlePrice': None, + 'timestamp': estFundingRateTimestamp, + 'datetime': self.iso8601(estFundingRateTimestamp), + 'fundingRate': self.safe_number(fundingRate, 'est_funding_rate'), + 'fundingTimestamp': nextFundingTimestamp, + 'fundingDatetime': self.iso8601(nextFundingTimestamp), + 'nextFundingRate': None, + 'nextFundingTimestamp': None, + 'nextFundingDatetime': None, + 'previousFundingRate': self.safe_number(fundingRate, 'last_funding_rate'), + 'previousFundingTimestamp': lastFundingRateTimestamp, + 'previousFundingDatetime': self.iso8601(lastFundingRateTimestamp), + 'interval': self.parse_funding_interval(millisecondsInterval), + } + + def parse_funding_interval(self, interval): + intervals: dict = { + '3600000': '1h', + '14400000': '4h', + '28800000': '8h', + '57600000': '16h', + '86400000': '24h', + } + return self.safe_string(intervals, interval, interval) + + def fetch_funding_interval(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate interval + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/public/get-predicted-funding-rate-for-one-market + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + return self.fetch_funding_rate(symbol, params) + + def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/public/get-predicted-funding-rate-for-one-market + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = self.v1PublicGetPublicFundingRateSymbol(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "symbol": "PERP_ETH_USDC", + # "est_funding_rate": 123, + # "est_funding_rate_timestamp": 1683880020000, + # "last_funding_rate": 0.0001, + # "last_funding_rate_timestamp": 1683878400000, + # "next_funding_time": 1683907200000, + # "sum_unitary_funding": 521.367 + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_funding_rate(data, market) + + def fetch_funding_rates(self, symbols: Strings = None, params={}) -> FundingRates: + """ + fetch the current funding rate for multiple markets + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/public/get-predicted-funding-rates-for-all-markets + + :param str[] symbols: unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of `funding rate structures ` + """ + self.load_markets() + symbols = self.market_symbols(symbols) + response = self.v1PublicGetPublicFundingRates(params) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "rows": [{ + # "symbol": "PERP_ETH_USDC", + # "est_funding_rate": 123, + # "est_funding_rate_timestamp": 1683880020000, + # "last_funding_rate": 0.0001, + # "last_funding_rate_timestamp": 1683878400000, + # "next_funding_time": 1683907200000, + # "sum_unitary_funding": 521.367 + # }] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + rows = self.safe_list(data, 'rows', []) + return self.parse_funding_rates(rows, symbols) + + def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical funding rate prices + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/public/get-funding-rate-history-for-one-market + + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: timestamp in ms of the earliest funding rate to fetch + :param int [limit]: the maximum amount of `funding rate structures ` to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest funding rate + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `funding rate structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchFundingRateHistory', 'paginate') + if paginate: + return self.fetch_paginated_call_incremental('fetchFundingRateHistory', symbol, since, limit, params, 'page', 25) + request: dict = {} + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + request['symbol'] = market['id'] + if since is not None: + request['start_t'] = since + request, params = self.handle_until_option('end_t', request, params, 0.001) + response = self.v1PublicGetPublicFundingRateHistory(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "rows": [{ + # "symbol": "PERP_ETH_USDC", + # "funding_rate": 0.0001, + # "funding_rate_timestamp": 1684224000000, + # "next_funding_time": 1684252800000 + # }], + # "meta": { + # "total": 9, + # "records_per_page": 25, + # "current_page": 1 + # } + # } + # } + # + data = self.safe_dict(response, 'data', {}) + result = self.safe_list(data, 'rows', []) + rates = [] + for i in range(0, len(result)): + entry = result[i] + marketId = self.safe_string(entry, 'symbol') + timestamp = self.safe_integer(entry, 'funding_rate_timestamp') + rates.append({ + 'info': entry, + 'symbol': self.safe_symbol(marketId), + 'fundingRate': self.safe_number(entry, 'funding_rate'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + sorted = self.sort_by(rates, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, symbol, since, limit) + + def parse_income(self, income, market: Market = None): + # + # { + # "symbol": "PERP_ETH_USDC", + # "funding_rate": 0.00046875, + # "mark_price": 2100, + # "funding_fee": 0.000016, + # "payment_type": "Pay", + # "status": "Accrued", + # "created_time": 1682235722003, + # "updated_time": 1682235722003 + # } + # + marketId = self.safe_string(income, 'symbol') + symbol = self.safe_symbol(marketId, market) + amount = self.safe_string(income, 'funding_fee') + code = self.safe_currency_code('USDC') + timestamp = self.safe_integer(income, 'updated_time') + rate = self.safe_number(income, 'funding_rate') + paymentType = self.safe_string(income, 'payment_type') + amount = Precise.string_neg(amount) if (paymentType == 'Pay') else amount + return { + 'info': income, + 'symbol': symbol, + 'code': code, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': None, + 'amount': self.parse_number(amount), + 'rate': rate, + } + + def fetch_funding_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch the history of funding payments paid and received on self account + + https://orderly.network/docs/build-on-omnichain/evm-api/restful-api/private/get-funding-fee-history + + :param str [symbol]: unified market symbol + :param int [since]: the earliest time in ms to fetch funding history for + :param int [limit]: the maximum number of funding history structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict: a `funding history structure ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchFundingHistory', 'paginate') + if paginate: + return self.fetch_paginated_call_incremental('fetchFundingHistory', symbol, since, limit, params, 'page', 500) + request: dict = {} + market: Market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if since is not None: + request['start_t'] = since + until = self.safe_integer(params, 'until') # unified in milliseconds + params = self.omit(params, ['until']) + if until is not None: + request['end_t'] = until + if limit is not None: + request['size'] = min(limit, 500) + response = self.v1PrivateGetFundingFeeHistory(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "meta": { + # "total": 9, + # "records_per_page": 25, + # "current_page": 1 + # }, + # "rows": [{ + # "symbol": "PERP_ETH_USDC", + # "funding_rate": 0.00046875, + # "mark_price": 2100, + # "funding_fee": 0.000016, + # "payment_type": "Pay", + # "status": "Accrued", + # "created_time": 1682235722003, + # "updated_time": 1682235722003 + # }] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + rows = self.safe_list(data, 'rows', []) + return self.parse_incomes(rows, market, since, limit) + + def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-account-information + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + self.load_markets() + response = self.v1PrivateGetClientInfo(params) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "account_id": "", + # "email": "test@test.com", + # "account_mode": "FUTURES", + # "max_leverage": 20, + # "taker_fee_rate": 123, + # "maker_fee_rate": 123, + # "futures_taker_fee_rate": 123, + # "futures_maker_fee_rate": 123, + # "maintenance_cancel_orders": True, + # "imr_factor": { + # "PERP_BTC_USDC": 123, + # "PERP_ETH_USDC": 123, + # "PERP_NEAR_USDC": 123 + # }, + # "max_notional": { + # "PERP_BTC_USDC": 123, + # "PERP_ETH_USDC": 123, + # "PERP_NEAR_USDC": 123 + # } + # } + # } + # + data = self.safe_dict(response, 'data', {}) + maker = self.safe_string(data, 'futures_maker_fee_rate') + taker = self.safe_string(data, 'futures_taker_fee_rate') + result: dict = {} + for i in range(0, len(self.symbols)): + symbol = self.symbols[i] + result[symbol] = { + 'info': response, + 'symbol': symbol, + 'maker': self.parse_number(Precise.string_div(maker, '10000')), + 'taker': self.parse_number(Precise.string_div(taker, '10000')), + 'percentage': True, + 'tierBased': True, + } + return result + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/orderbook-snapshot + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + limit = min(limit, 1000) + request['max_level'] = limit + response = self.v1PrivateGetOrderbookSymbol(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "asks": [{ + # "price": 10669.4, + # "quantity": 1.56263218 + # }], + # "bids": [{ + # "price": 10669.4, + # "quantity": 1.56263218 + # }], + # "timestamp": 123 + # } + # } + # + data = self.safe_dict(response, 'data', {}) + timestamp = self.safe_integer(data, 'timestamp') + return self.parse_order_book(data, symbol, timestamp, 'bids', 'asks', 'price', 'quantity') + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + return [ + self.safe_integer(ohlcv, 'start_timestamp'), + self.safe_number(ohlcv, 'open'), + self.safe_number(ohlcv, 'high'), + self.safe_number(ohlcv, 'low'), + self.safe_number(ohlcv, 'close'), + self.safe_number(ohlcv, 'volume'), + ] + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-kline + + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: max=1000, max=100 when since is defined and is less than(now - (999 * (timeframe in ms))) + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'type': self.safe_string(self.timeframes, timeframe, timeframe), + } + if limit is not None: + request['limit'] = min(limit, 1000) + response = self.v1PrivateGetKline(self.extend(request, params)) + data = self.safe_dict(response, 'data', {}) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "rows": [{ + # "open": 66166.23, + # "close": 66124.56, + # "low": 66038.06, + # "high": 66176.97, + # "volume": 23.45528526, + # "amount": 1550436.21725288, + # "symbol": "PERP_BTC_USDC", + # "type": "1m", + # "start_timestamp": 1636388220000, + # "end_timestamp": 1636388280000 + # }] + # } + # } + # + rows = self.safe_list(data, 'rows', []) + return self.parse_ohlcvs(rows, market, timeframe, since, limit) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # Possible input functions: + # * createOrder + # * createOrders + # * cancelOrder + # * fetchOrder + # * fetchOrders + # isFromFetchOrder = ('order_tag' in order); TO_DO + # + # stop order after creating it: + # { + # "orderId": "1578938", + # "clientOrderId": "0", + # "algoType": "STOP_LOSS", + # "quantity": "0.1" + # } + # stop order after fetching it: + # { + # "algoOrderId": "1578958", + # "clientOrderId": "0", + # "rootAlgoOrderId": "1578958", + # "parentAlgoOrderId": "0", + # "symbol": "SPOT_LTC_USDT", + # "orderTag": "default", + # "algoType": "STOP_LOSS", + # "side": "BUY", + # "quantity": "0.1", + # "isTriggered": False, + # "triggerPrice": "100", + # "triggerStatus": "USELESS", + # "type": "LIMIT", + # "rootAlgoStatus": "CANCELLED", + # "algoStatus": "CANCELLED", + # "triggerPriceType": "MARKET_PRICE", + # "price": "75", + # "triggerTime": "0", + # "totalExecutedQuantity": "0", + # "averageExecutedPrice": "0", + # "totalFee": "0", + # "feeAsset": '', + # "reduceOnly": False, + # "createdTime": "1686149609.744", + # "updatedTime": "1686149903.362" + # } + # + timestamp = self.safe_integer_n(order, ['timestamp', 'created_time', 'createdTime']) + orderId = self.safe_string_n(order, ['order_id', 'orderId', 'algoOrderId']) + clientOrderId = self.omit_zero(self.safe_string_2(order, 'client_order_id', 'clientOrderId')) # Somehow, self always returns 0 for limit order + marketId = self.safe_string(order, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + price = self.safe_string_2(order, 'order_price', 'price') + amount = self.safe_string_2(order, 'order_quantity', 'quantity') # This is base amount + cost = self.safe_string_2(order, 'order_amount', 'amount') # This is quote amount + orderType = self.safe_string_lower_2(order, 'order_type', 'type') + status = self.safe_value_2(order, 'status', 'algoStatus') + success = self.safe_bool(order, 'success') + if success is not None: + status = 'NEW' if (success) else 'REJECTED' + side = self.safe_string_lower(order, 'side') + filled = self.omit_zero(self.safe_value_2(order, 'executed', 'totalExecutedQuantity')) + average = self.omit_zero(self.safe_string_2(order, 'average_executed_price', 'averageExecutedPrice')) + remaining = Precise.string_sub(cost, filled) + fee = self.safe_value_2(order, 'total_fee', 'totalFee') + feeCurrency = self.safe_string_2(order, 'fee_asset', 'feeAsset') + transactions = self.safe_value(order, 'Transactions') + triggerPrice = self.safe_number(order, 'triggerPrice') + takeProfitPrice: Num = None + stopLossPrice: Num = None + childOrders = self.safe_value(order, 'childOrders') + if childOrders is not None: + first = self.safe_value(childOrders, 0) + innerChildOrders = self.safe_value(first, 'childOrders', []) + innerChildOrdersLength = len(innerChildOrders) + if innerChildOrdersLength > 0: + takeProfitOrder = self.safe_value(innerChildOrders, 0) + stopLossOrder = self.safe_value(innerChildOrders, 1) + takeProfitPrice = self.safe_number(takeProfitOrder, 'triggerPrice') + stopLossPrice = self.safe_number(stopLossOrder, 'triggerPrice') + lastUpdateTimestamp = self.safe_integer_2(order, 'updatedTime', 'updated_time') + return self.safe_order({ + 'id': orderId, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'lastUpdateTimestamp': lastUpdateTimestamp, + 'status': self.parse_order_status(status), + 'symbol': symbol, + 'type': self.parse_order_type(orderType), + 'timeInForce': self.parse_time_in_force(orderType), + 'postOnly': None, # TO_DO + 'reduceOnly': self.safe_bool(order, 'reduce_only'), + 'side': side, + 'price': price, + 'triggerPrice': triggerPrice, + 'takeProfitPrice': takeProfitPrice, + 'stopLossPrice': stopLossPrice, + 'average': average, + 'amount': amount, + 'filled': filled, + 'remaining': remaining, # TO_DO + 'cost': cost, + 'trades': transactions, + 'fee': { + 'cost': fee, + 'currency': feeCurrency, + }, + 'info': order, + }, market) + + def parse_time_in_force(self, timeInForce: Str): + timeInForces: dict = { + 'ioc': 'IOC', + 'fok': 'FOK', + 'post_only': 'PO', + } + return self.safe_string(timeInForces, timeInForce, None) + + def parse_order_status(self, status: Str): + if status is not None: + statuses: dict = { + 'NEW': 'open', + 'FILLED': 'closed', + 'CANCEL_SENT': 'canceled', + 'CANCEL_ALL_SENT': 'canceled', + 'CANCELLED': 'canceled', + 'PARTIAL_FILLED': 'open', + 'REJECTED': 'rejected', + 'INCOMPLETE': 'open', + 'COMPLETED': 'closed', + } + return self.safe_string(statuses, status, status) + return status + + def parse_order_type(self, type: Str): + types: dict = { + 'LIMIT': 'limit', + 'MARKET': 'market', + 'POST_ONLY': 'limit', + } + return self.safe_string_lower(types, type, type) + + def create_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + @ignore + helper function to build the request + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency + :param float [price]: the price that the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: request to be sent to the exchange + """ + reduceOnly = self.safe_bool_2(params, 'reduceOnly', 'reduce_only') + orderType = type.upper() + market = self.market(symbol) + orderSide = side.upper() + request: dict = { + 'symbol': market['id'], + 'side': orderSide, + } + triggerPrice = self.safe_string_2(params, 'triggerPrice', 'stopPrice') + stopLoss = self.safe_value(params, 'stopLoss') + takeProfit = self.safe_value(params, 'takeProfit') + algoType = self.safe_string(params, 'algoType') + isConditional = triggerPrice is not None or stopLoss is not None or takeProfit is not None or (self.safe_value(params, 'childOrders') is not None) + isMarket = orderType == 'MARKET' + timeInForce = self.safe_string_lower(params, 'timeInForce') + postOnly = self.is_post_only(isMarket, None, params) + orderQtyKey = 'quantity' if isConditional else 'order_quantity' + priceKey = 'price' if isConditional else 'order_price' + typeKey = 'type' if isConditional else 'order_type' + request[typeKey] = orderType # LIMIT/MARKET/IOC/FOK/POST_ONLY/ASK/BID + if not isConditional: + if postOnly: + request['order_type'] = 'POST_ONLY' + elif timeInForce == 'fok': + request['order_type'] = 'FOK' + elif timeInForce == 'ioc': + request['order_type'] = 'IOC' + if reduceOnly: + request['reduce_only'] = reduceOnly + if price is not None: + request[priceKey] = self.price_to_precision(symbol, price) + if isMarket and not isConditional: + request[orderQtyKey] = self.amount_to_precision(symbol, amount) + elif algoType != 'POSITIONAL_TP_SL': + request[orderQtyKey] = self.amount_to_precision(symbol, amount) + clientOrderId = self.safe_string_n(params, ['clOrdID', 'clientOrderId', 'client_order_id']) + if clientOrderId is not None: + request['client_order_id'] = clientOrderId + if triggerPrice is not None: + request['trigger_price'] = self.price_to_precision(symbol, triggerPrice) + request['algo_type'] = 'STOP' + elif (stopLoss is not None) or (takeProfit is not None): + request['algo_type'] = 'TP_SL' + outterOrder: dict = { + 'symbol': market['id'], + 'reduce_only': False, + 'algo_type': 'POSITIONAL_TP_SL', + 'child_orders': [], + } + childOrders = outterOrder['child_orders'] + closeSide = 'SELL' if (orderSide == 'BUY') else 'BUY' + if stopLoss is not None: + stopLossPrice = self.safe_number_2(stopLoss, 'triggerPrice', 'price', stopLoss) + stopLossOrder: dict = { + 'side': closeSide, + 'algo_type': 'TP_SL', + 'trigger_price': self.price_to_precision(symbol, stopLossPrice), + 'type': 'LIMIT', + 'reduce_only': True, + } + childOrders.append(stopLossOrder) + if takeProfit is not None: + takeProfitPrice = self.safe_number_2(takeProfit, 'triggerPrice', 'price', takeProfit) + takeProfitOrder: dict = { + 'side': closeSide, + 'algo_type': 'TP_SL', + 'trigger_price': self.price_to_precision(symbol, takeProfitPrice), + 'type': 'LIMIT', + 'reduce_only': True, + } + outterOrder.append(takeProfitOrder) + request['child_orders'] = [outterOrder] + params = self.omit(params, ['reduceOnly', 'reduce_only', 'clOrdID', 'clientOrderId', 'client_order_id', 'postOnly', 'timeInForce', 'stopPrice', 'triggerPrice', 'stopLoss', 'takeProfit']) + return self.extend(request, params) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/create-order + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/create-algo-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: The price a trigger order is triggered at + :param dict [params.takeProfit]: *takeProfit object in params* containing the triggerPrice at which the attached take profit order will be triggered(perpetual swap markets only) + :param float [params.takeProfit.triggerPrice]: take profit trigger price + :param dict [params.stopLoss]: *stopLoss object in params* containing the triggerPrice at which the attached stop loss order will be triggered(perpetual swap markets only) + :param float [params.stopLoss.triggerPrice]: stop loss trigger price + :param float [params.algoType]: 'STOP'or 'TP_SL' or 'POSITIONAL_TP_SL' + :param float [params.cost]: *spot market buy only* the quote quantity that can be used alternative for the amount + :param str [params.clientOrderId]: a unique id for the order + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + request = self.create_order_request(symbol, type, side, amount, price, params) + triggerPrice = self.safe_string_2(params, 'triggerPrice', 'stopPrice') + stopLoss = self.safe_value(params, 'stopLoss') + takeProfit = self.safe_value(params, 'takeProfit') + isConditional = triggerPrice is not None or stopLoss is not None or takeProfit is not None or (self.safe_value(params, 'childOrders') is not None) + response = None + if isConditional: + response = self.v1PrivatePostAlgoOrder(request) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "order_id": 13, + # "client_order_id": "testclientid", + # "algo_type": "STOP", + # "quantity": 100.12 + # } + # } + # + else: + response = self.v1PrivatePostOrder(request) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "order_id": 13, + # "client_order_id": "testclientid", + # "order_type": "LIMIT", + # "order_price": 100.12, + # "order_quantity": 0.987654, + # "order_amount": 0.8, + # "error_message": "none" + # } + # } + # + data = self.safe_dict(response, 'data') + data['timestamp'] = self.safe_integer(response, 'timestamp') + order = self.parse_order(data, market) + order['type'] = type + return order + + def create_orders(self, orders: List[OrderRequest], params={}): + """ + *contract only* create a list of trade orders + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/batch-create-order + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + ordersRequests = [] + for i in range(0, len(orders)): + rawOrder = orders[i] + marketId = self.safe_string(rawOrder, 'symbol') + type = self.safe_string(rawOrder, 'type') + side = self.safe_string(rawOrder, 'side') + amount = self.safe_value(rawOrder, 'amount') + price = self.safe_value(rawOrder, 'price') + orderParams = self.safe_dict(rawOrder, 'params', {}) + triggerPrice = self.safe_string_2(orderParams, 'triggerPrice', 'stopPrice') + stopLoss = self.safe_value(orderParams, 'stopLoss') + takeProfit = self.safe_value(orderParams, 'takeProfit') + isConditional = triggerPrice is not None or stopLoss is not None or takeProfit is not None or (self.safe_value(orderParams, 'childOrders') is not None) + if isConditional: + raise NotSupported(self.id + ' createOrders() only support non-stop order') + orderRequest = self.create_order_request(marketId, type, side, amount, price, orderParams) + ordersRequests.append(orderRequest) + request: dict = { + 'orders': ordersRequests, + } + response = self.v1PrivatePostBatchOrder(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "rows": [{ + # "order_id": 13, + # "client_order_id": "testclientid", + # "order_type": "LIMIT", + # "order_price": 100.12, + # "order_quantity": 0.987654, + # "order_amount": 0.8, + # "error_message": "none" + # }] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + rows = self.safe_list(data, 'rows', []) + return self.parse_orders(rows) + + def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + """ + edit a trade order + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/edit-order + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/edit-algo-order + + :param str id: order id + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: The price a trigger order is triggered at + :param float [params.stopLossPrice]: price to trigger stop-loss orders + :param float [params.takeProfitPrice]: price to trigger take-profit orders + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'order_id': id, + } + triggerPrice = self.safe_string_n(params, ['triggerPrice', 'stopPrice', 'takeProfitPrice', 'stopLossPrice']) + if triggerPrice is not None: + request['triggerPrice'] = self.price_to_precision(symbol, triggerPrice) + isConditional = (triggerPrice is not None) or (self.safe_value(params, 'childOrders') is not None) + orderQtyKey = 'quantity' if isConditional else 'order_quantity' + priceKey = 'price' if isConditional else 'order_price' + if price is not None: + request[priceKey] = self.price_to_precision(symbol, price) + if amount is not None: + request[orderQtyKey] = self.amount_to_precision(symbol, amount) + params = self.omit(params, ['stopPrice', 'triggerPrice', 'takeProfitPrice', 'stopLossPrice', 'trailingTriggerPrice', 'trailingAmount', 'trailingPercent']) + response = None + if isConditional: + response = self.v1PrivatePutAlgoOrder(self.extend(request, params)) + else: + request['symbol'] = market['id'] + request['side'] = side.upper() + orderType = type.upper() + timeInForce = self.safe_string_lower(params, 'timeInForce') + isMarket = orderType == 'MARKET' + postOnly = self.is_post_only(isMarket, None, params) + if postOnly: + request['order_type'] = 'POST_ONLY' + elif timeInForce == 'fok': + request['order_type'] = 'FOK' + elif timeInForce == 'ioc': + request['order_type'] = 'IOC' + else: + request['order_type'] = orderType + clientOrderId = self.safe_string_n(params, ['clOrdID', 'clientOrderId', 'client_order_id']) + params = self.omit(params, ['clOrdID', 'clientOrderId', 'client_order_id', 'postOnly', 'timeInForce']) + if clientOrderId is not None: + request['client_order_id'] = clientOrderId + # request['side'] = side.upper() + # request['symbol'] = market['id'] + response = self.v1PrivatePutOrder(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "status": "EDIT_SENT" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + data['timestamp'] = self.safe_integer(response, 'timestamp') + return self.parse_order(data, market) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/cancel-order + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/cancel-order-by-client_order_id + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/cancel-algo-order + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/cancel-algo-order-by-client_order_id + + cancels an open order + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: whether the order is a stop/algo order + :param str [params.clientOrderId]: a unique id for the order + :returns dict: An `order structure ` + """ + trigger = self.safe_bool_2(params, 'stop', 'trigger', False) + params = self.omit(params, ['stop', 'trigger']) + if not trigger and (symbol is None): + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + self.load_markets() + market: Market = None + if symbol is not None: + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + clientOrderIdUnified = self.safe_string_2(params, 'clOrdID', 'clientOrderId') + clientOrderIdExchangeSpecific = self.safe_string(params, 'client_order_id', clientOrderIdUnified) + isByClientOrder = clientOrderIdExchangeSpecific is not None + response = None + if trigger: + if isByClientOrder: + request['client_order_id'] = clientOrderIdExchangeSpecific + params = self.omit(params, ['clOrdID', 'clientOrderId', 'client_order_id']) + response = self.v1PrivateDeleteAlgoClientOrder(self.extend(request, params)) + else: + request['order_id'] = id + response = self.v1PrivateDeleteAlgoOrder(self.extend(request, params)) + else: + if isByClientOrder: + request['client_order_id'] = clientOrderIdExchangeSpecific + params = self.omit(params, ['clOrdID', 'clientOrderId', 'client_order_id']) + response = self.v1PrivateDeleteClientOrder(self.extend(request, params)) + else: + request['order_id'] = id + response = self.v1PrivateDeleteOrder(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203988, + # "data": { + # "status": "CANCEL_SENT" + # } + # } + # + # { + # "success": True, + # "timestamp": 1702989203988, + # "status": "CANCEL_SENT" + # } + # + extendParams: dict = {'symbol': symbol} + if isByClientOrder: + extendParams['client_order_id'] = clientOrderIdExchangeSpecific + else: + extendParams['id'] = id + if trigger: + return self.extend(self.parse_order(response), extendParams) + data = self.safe_dict(response, 'data', {}) + return self.extend(self.parse_order(data), extendParams) + + def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): + """ + cancel multiple orders + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/batch-cancel-orders + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/batch-cancel-orders-by-client_order_id + + :param str[] ids: order ids + :param str [symbol]: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str[] [params.client_order_ids]: max length 10 e.g. ["my_id_1","my_id_2"], encode the double quotes. No space after comma + :returns dict: an list of `order structures ` + """ + self.load_markets() + clientOrderIds = self.safe_list_n(params, ['clOrdIDs', 'clientOrderIds', 'client_order_ids']) + params = self.omit(params, ['clOrdIDs', 'clientOrderIds', 'client_order_ids']) + request: dict = {} + response = None + if clientOrderIds: + request['client_order_ids'] = ','.join(clientOrderIds) + response = self.v1PrivateDeleteClientBatchOrder(self.extend(request, params)) + else: + request['order_ids'] = ','.join(ids) + response = self.v1PrivateDeleteBatchOrder(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "status": "CANCEL_ALL_SENT" + # } + # } + # + return [self.safe_order({ + 'info': response, + })] + + def cancel_all_orders(self, symbol: Str = None, params={}): + """ + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/cancel-all-pending-algo-orders + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/cancel-orders-in-bulk + + cancel all open orders in a market + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: whether the order is a stop/algo order + :returns dict: an list of `order structures ` + """ + self.load_markets() + trigger = self.safe_bool_2(params, 'stop', 'trigger') + params = self.omit(params, ['stop', 'trigger']) + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + response = None + if trigger: + response = self.v1PrivateDeleteAlgoOrders(self.extend(request, params)) + else: + response = self.v1PrivateDeleteOrders(self.extend(request, params)) + # trigger + # { + # "success": True, + # "timestamp": 1702989203989, + # "status": "CANCEL_ALL_SENT" + # } + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "status": "CANCEL_ALL_SENT" + # } + # } + # + return [ + self.safe_order({ + 'info': response, + }), + ] + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-order-by-order_id + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-order-by-client_order_id + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-algo-order-by-order_id + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-algo-order-by-client_order_id + + fetches information on an order made by the user + :param str id: the order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: whether the order is a stop/algo order + :param str [params.clientOrderId]: a unique id for the order + :returns dict: An `order structure ` + """ + self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + trigger = self.safe_bool_2(params, 'stop', 'trigger', False) + request: dict = {} + clientOrderId = self.safe_string_n(params, ['clOrdID', 'clientOrderId', 'client_order_id']) + params = self.omit(params, ['stop', 'trigger', 'clOrdID', 'clientOrderId', 'client_order_id']) + response = None + if trigger: + if clientOrderId: + request['client_order_id'] = clientOrderId + response = self.v1PrivateGetAlgoClientOrderClientOrderId(self.extend(request, params)) + else: + request['oid'] = id + response = self.v1PrivateGetAlgoOrderOid(self.extend(request, params)) + else: + if clientOrderId: + request['client_order_id'] = clientOrderId + response = self.v1PrivateGetClientOrderClientOrderId(self.extend(request, params)) + else: + request['oid'] = id + response = self.v1PrivateGetOrderOid(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "order_id": 78151, + # "user_id": 12345, + # "price": 0.67772, + # "type": "LIMIT", + # "quantity": 20, + # "amount": 10, + # "executed_quantity": 20, + # "total_executed_quantity": 20, + # "visible_quantity": 1, + # "symbol": "PERP_BTC_USDC", + # "side": "BUY", + # "status": "FILLED", + # "total_fee": 0.5, + # "fee_asset": "BTC", + # "client_order_id": 1, + # "average_executed_price": 0.67772, + # "created_time": 1653563963000, + # "updated_time": 1653564213000, + # "realized_pnl": 123 + # } + # } + # + orders = self.safe_dict(response, 'data', response) + return self.parse_order(orders, market) + + def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-orders + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-algo-orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: whether the order is a stop/algo order + :param boolean [params.is_triggered]: whether the order has been triggered(False by default) + :param str [params.side]: 'buy' or 'sell' + :param boolean [params.paginate]: set to True if you want to fetch orders with pagination + :param int params['until']: timestamp in ms of the latest order to fetch + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + paginate = False + isTrigger = self.safe_bool_2(params, 'stop', 'trigger', False) + maxLimit = 100 if (isTrigger) else 500 + paginate, params = self.handle_option_and_params(params, 'fetchOrders', 'paginate') + if paginate: + return self.fetch_paginated_call_incremental('fetchOrders', symbol, since, limit, params, 'page', maxLimit) + request: dict = {} + market: Market = None + params = self.omit(params, ['stop', 'trigger']) + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if since is not None: + request['start_t'] = since + if limit is not None: + request['size'] = limit + else: + request['size'] = maxLimit + if isTrigger: + request['algo_type'] = 'STOP' + request, params = self.handle_until_option('end_t', request, params) + response = None + if isTrigger: + response = self.v1PrivateGetAlgoOrders(self.extend(request, params)) + else: + response = self.v1PrivateGetOrders(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "meta": { + # "total": 9, + # "records_per_page": 25, + # "current_page": 1 + # }, + # "rows": [{ + # "order_id": 78151, + # "user_id": 12345, + # "price": 0.67772, + # "type": "LIMIT", + # "quantity": 20, + # "amount": 10, + # "executed_quantity": 20, + # "total_executed_quantity": 20, + # "visible_quantity": 1, + # "symbol": "PERP_BTC_USDC", + # "side": "BUY", + # "status": "FILLED", + # "total_fee": 0.5, + # "fee_asset": "BTC", + # "client_order_id": 1, + # "average_executed_price": 0.67772, + # "created_time": 1653563963000, + # "updated_time": 1653564213000, + # "realized_pnl": 123 + # }] + # } + # } + # + data = self.safe_value(response, 'data', response) + orders = self.safe_list(data, 'rows') + return self.parse_orders(orders, market, since, limit) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-orders + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-algo-orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: whether the order is a stop/algo order + :param boolean [params.is_triggered]: whether the order has been triggered(False by default) + :param str [params.side]: 'buy' or 'sell' + :param int params['until']: timestamp in ms of the latest order to fetch + :param boolean [params.paginate]: set to True if you want to fetch orders with pagination + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + extendedParams = self.extend(params, {'status': 'INCOMPLETE'}) + return self.fetch_orders(symbol, since, limit, extendedParams) + + def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-orders + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-algo-orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: whether the order is a stop/algo order + :param boolean [params.is_triggered]: whether the order has been triggered(False by default) + :param str [params.side]: 'buy' or 'sell' + :param int params['until']: timestamp in ms of the latest order to fetch + :param boolean [params.paginate]: set to True if you want to fetch orders with pagination + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + extendedParams = self.extend(params, {'status': 'COMPLETED'}) + return self.fetch_orders(symbol, since, limit, extendedParams) + + def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all the trades made from a single order + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-all-trades-of-specific-order + + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + self.load_markets() + market: Market = None + if symbol is not None: + market = self.market(symbol) + request: dict = { + 'oid': id, + } + response = self.v1PrivateGetOrderOidTrades(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "rows": [{ + # "id": 2, + # "symbol": "PERP_BTC_USDC", + # "fee": 0.0001, + # "fee_asset": "USDC", + # "side": "BUY", + # "order_id": 1, + # "executed_price": 123, + # "executed_quantity": 0.05, + # "executed_timestamp": 1567382401000, + # "is_maker": 1, + # "realized_pnl": 123 + # }] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + trades = self.safe_list(data, 'rows', []) + return self.parse_trades(trades, market, since, limit, params) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-trades + + fetch all trades made by the user + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: set to True if you want to fetch trades with pagination + :param int params['until']: timestamp in ms of the latest trade to fetch + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchMyTrades', 'paginate') + if paginate: + return self.fetch_paginated_call_incremental('fetchMyTrades', symbol, since, limit, params, 'page', 500) + request: dict = {} + market: Market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if since is not None: + request['start_t'] = since + if limit is not None: + request['size'] = limit + else: + request['size'] = 500 + request, params = self.handle_until_option('end_t', request, params) + response = self.v1PrivateGetTrades(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "meta": { + # "total": 9, + # "records_per_page": 25, + # "current_page": 1 + # }, + # "rows": [{ + # "id": 2, + # "symbol": "PERP_BTC_USDC", + # "fee": 0.0001, + # "fee_asset": "USDC", + # "side": "BUY", + # "order_id": 1, + # "executed_price": 123, + # "executed_quantity": 0.05, + # "executed_timestamp": 1567382401000, + # "is_maker": 1, + # "realized_pnl": 123 + # }] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + trades = self.safe_list(data, 'rows', []) + return self.parse_trades(trades, market, since, limit, params) + + def parse_balance(self, response) -> Balances: + result: dict = { + 'info': response, + } + balances = self.safe_list(response, 'holding', []) + for i in range(0, len(balances)): + balance = balances[i] + code = self.safe_currency_code(self.safe_string(balance, 'token')) + account = self.account() + account['total'] = self.safe_string(balance, 'holding') + account['used'] = self.safe_string(balance, 'frozen') + result[code] = account + return self.safe_balance(result) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-current-holding + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + response = self.v1PrivateGetClientHolding(params) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "holding": [{ + # "updated_time": 1580794149000, + # "token": "BTC", + # "holding": -28.000752, + # "frozen": 123, + # "pending_short": -2000 + # }] + # } + # } + # + data = self.safe_dict(response, 'data') + return self.parse_balance(data) + + def get_asset_history_rows(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> Any: + self.load_markets() + request: dict = {} + currency: Currency = None + if code is not None: + currency = self.currency(code) + request['balance_token'] = currency['id'] + if since is not None: + request['start_t'] = since + if limit is not None: + request['pageSize'] = limit + transactionType = self.safe_string(params, 'type') + params = self.omit(params, 'type') + if transactionType is not None: + request['type'] = transactionType + response = self.v1PrivateGetAssetHistory(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "meta": { + # "total": 9, + # "records_per_page": 25, + # "current_page": 1 + # }, + # "rows": [{ + # "id": "230707030600002", + # "tx_id": "0x4b0714c63cc7abae72bf68e84e25860b88ca651b7d27dad1e32bf4c027fa5326", + # "side": "WITHDRAW", + # "token": "USDC", + # "amount": 555, + # "fee": 123, + # "trans_status": "FAILED", + # "created_time": 1688699193034, + # "updated_time": 1688699193096, + # "chain_id": "986532" + # }] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return [currency, self.safe_list(data, 'rows', [])] + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + currencyId = self.safe_string(item, 'token') + code = self.safe_currency_code(currencyId, currency) + currency = self.safe_currency(currencyId, currency) + amount = self.safe_number(item, 'amount') + side = self.safe_string(item, 'token_side') + direction = 'in' if (side == 'DEPOSIT') else 'out' + timestamp = self.safe_integer(item, 'created_time') + fee = self.parse_token_and_fee_temp(item, 'fee_token', 'fee_amount') + return self.safe_ledger_entry({ + 'id': self.safe_string(item, 'id'), + 'currency': code, + 'account': self.safe_string(item, 'account'), + 'referenceAccount': None, + 'referenceId': self.safe_string(item, 'tx_id'), + 'status': self.parse_transaction_status(self.safe_string(item, 'status')), + 'amount': amount, + 'before': None, + 'after': None, + 'fee': fee, + 'direction': direction, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'type': self.parse_ledger_entry_type(self.safe_string(item, 'type')), + 'info': item, + }, currency) + + def parse_ledger_entry_type(self, type): + types: dict = { + 'BALANCE': 'transaction', # Funds moved in/out wallet + 'COLLATERAL': 'transfer', # Funds moved between portfolios + } + return self.safe_string(types, type, type) + + def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-asset-history + + :param str [code]: unified currency code, default is None + :param int [since]: timestamp in ms of the earliest ledger entry, default is None + :param int [limit]: max number of ledger entries to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ledger structure ` + """ + currencyRows = self.get_asset_history_rows(code, since, limit, params) + currency = self.safe_value(currencyRows, 0) + rows = self.safe_list(currencyRows, 1) + return self.parse_ledger(rows, currency, since, limit, params) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # example in fetchLedger + code = self.safe_string(transaction, 'token') + movementDirection = self.safe_string_lower(transaction, 'token_side') + if movementDirection == 'withdraw': + movementDirection = 'withdrawal' + fee = self.parse_token_and_fee_temp(transaction, 'fee_token', 'fee_amount') + addressTo = self.safe_string(transaction, 'target_address') + addressFrom = self.safe_string(transaction, 'source_address') + timestamp = self.safe_integer(transaction, 'created_time') + return { + 'info': transaction, + 'id': self.safe_string_2(transaction, 'id', 'withdraw_id'), + 'txid': self.safe_string(transaction, 'tx_id'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'address': None, + 'addressFrom': addressFrom, + 'addressTo': addressTo, + 'tag': self.safe_string(transaction, 'extra'), + 'tagFrom': None, + 'tagTo': None, + 'type': movementDirection, + 'amount': self.safe_number(transaction, 'amount'), + 'currency': code, + 'status': self.parse_transaction_status(self.safe_string(transaction, 'status')), + 'updated': self.safe_integer(transaction, 'updated_time'), + 'comment': None, + 'internal': None, + 'fee': fee, + 'network': None, + } + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'NEW': 'pending', + 'CONFIRMING': 'pending', + 'PROCESSING': 'pending', + 'COMPLETED': 'ok', + 'CANCELED': 'canceled', + } + return self.safe_string(statuses, status, status) + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-asset-history + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + request: dict = { + 'side': 'DEPOSIT', + } + return self.fetch_deposits_withdrawals(code, since, limit, self.extend(request, params)) + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-asset-history + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + request: dict = { + 'side': 'WITHDRAW', + } + return self.fetch_deposits_withdrawals(code, since, limit, self.extend(request, params)) + + def fetch_deposits_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch history of deposits and withdrawals + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-asset-history + + :param str [code]: unified currency code for the currency of the deposit/withdrawals, default is None + :param int [since]: timestamp in ms of the earliest deposit/withdrawal, default is None + :param int [limit]: max number of deposit/withdrawals to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `transaction structure ` + """ + request: dict = {} + currencyRows = self.get_asset_history_rows(code, since, limit, self.extend(request, params)) + currency = self.safe_value(currencyRows, 0) + rows = self.safe_list(currencyRows, 1) + # + # { + # "rows":[], + # "meta":{ + # "total":0, + # "records_per_page":25, + # "current_page":1 + # }, + # "success":true + # } + # + return self.parse_transactions(rows, currency, since, limit, params) + + def get_withdraw_nonce(self, params={}): + response = self.v1PrivateGetWithdrawNonce(params) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "withdraw_nonce": 1 + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.safe_number(data, 'withdraw_nonce') + + def hash_message(self, message): + return '0x' + self.hash(message, 'keccak', 'hex') + + def sign_hash(self, hash, privateKey): + signature = self.ecdsa(hash[-64:], privateKey[-64:], 'secp256k1', None) + r = signature['r'] + s = signature['s'] + v = self.int_to_base16(self.sum(27, signature['v'])) + return '0x' + r.rjust(64, '0') + s.rjust(64, '0') + v + + def sign_message(self, message, privateKey): + return self.sign_hash(self.hash_message(message), privateKey[-64:]) + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/create-withdraw-request + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + self.load_markets() + self.check_address(address) + if code is not None: + code = code.upper() + if code != 'USDC': + raise NotSupported(self.id + ' withdraw() only support USDC') + currency = self.currency(code) + verifyingContractAddress = self.safe_string(self.options, 'verifyingContractAddress') + chainId = self.safe_string(params, 'chainId') + currencyNetworks = self.safe_dict(currency, 'networks', {}) + coinNetwork = self.safe_dict(currencyNetworks, chainId, {}) + coinNetworkId = self.safe_number(coinNetwork, 'id') + if coinNetworkId is None: + raise BadRequest(self.id + ' withdraw() require chainId parameter') + withdrawNonce = self.get_withdraw_nonce(params) + nonce = self.nonce() + domain: dict = { + 'chainId': chainId, + 'name': 'Orderly', + 'verifyingContract': verifyingContractAddress, + 'version': '1', + } + messageTypes: dict = { + 'Withdraw': [ + {'name': 'brokerId', 'type': 'string'}, + {'name': 'chainId', 'type': 'uint256'}, + {'name': 'receiver', 'type': 'address'}, + {'name': 'token', 'type': 'string'}, + {'name': 'amount', 'type': 'uint256'}, + {'name': 'withdrawNonce', 'type': 'uint64'}, + {'name': 'timestamp', 'type': 'uint64'}, + ], + } + withdrawRequest: dict = { + 'brokerId': self.safe_string(self.options, 'keyBrokerId', 'mode'), + 'chainId': self.parse_to_int(chainId), + 'receiver': address, + 'token': code, + 'amount': str(amount), + 'withdrawNonce': withdrawNonce, + 'timestamp': nonce, + } + msg = self.eth_encode_structured_data(domain, messageTypes, withdrawRequest) + signature = self.sign_message(msg, self.privateKey) + request: dict = { + 'signature': signature, + 'userAddress': address, + 'verifyingContract': verifyingContractAddress, + 'message': withdrawRequest, + } + params = self.omit(params, 'chainId') + response = self.v1PrivatePostWithdrawRequest(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "withdraw_id": 123 + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_transaction(data, currency) + + def parse_leverage(self, leverage, market=None) -> Leverage: + leverageValue = self.safe_integer(leverage, 'max_leverage') + return { + 'info': leverage, + 'symbol': market['symbol'], + 'marginMode': None, + 'longLeverage': leverageValue, + 'shortLeverage': leverageValue, + } + + def fetch_leverage(self, symbol: str, params={}) -> Leverage: + """ + fetch the set leverage for a market + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-account-information + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `leverage structure ` + """ + self.load_markets() + market = self.market(symbol) + response = self.v1PrivateGetClientInfo(params) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "account_id": "", + # "email": "test@test.com", + # "account_mode": "FUTURES", + # "max_leverage": 20, + # "taker_fee_rate": 123, + # "maker_fee_rate": 123, + # "futures_taker_fee_rate": 123, + # "futures_maker_fee_rate": 123, + # "maintenance_cancel_orders": True, + # "imr_factor": { + # "PERP_BTC_USDC": 123, + # "PERP_ETH_USDC": 123, + # "PERP_NEAR_USDC": 123 + # }, + # "max_notional": { + # "PERP_BTC_USDC": 123, + # "PERP_ETH_USDC": 123, + # "PERP_NEAR_USDC": 123 + # } + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_leverage(data, market) + + def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/update-leverage-setting + + :param int [leverage]: the rate of leverage + :param str [symbol]: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + self.load_markets() + isMinLeverage = leverage < 1 + isMaxLeverage = leverage > 50 + if isMinLeverage or isMaxLeverage: + raise BadRequest(self.id + ' leverage should be between 1 and 50') + request: dict = { + 'leverage': leverage, + } + return self.v1PrivatePostClientLeverage(self.extend(request, params)) + + def parse_position(self, position: dict, market: Market = None): + # + # { + # "IMR_withdraw_orders": 0.1, + # "MMR_with_orders": 0.05, + # "average_open_price": 27908.14386047, + # "cost_position": -139329.358492, + # "est_liq_price": 117335.92899428, + # "fee_24_h": 123, + # "imr": 0.1, + # "last_sum_unitary_funding": 70.38, + # "mark_price": 27794.9, + # "mmr": 0.05, + # "pending_long_qty": 123, + # "pending_short_qty": 123, + # "pnl_24_h": 123, + # "position_qty": -5, + # "settle_price": 27865.8716984, + # "symbol": "PERP_BTC_USDC", + # "timestamp": 1685429350571, + # "unsettled_pnl": 354.858492 + # } + # + contract = self.safe_string(position, 'symbol') + market = self.safe_market(contract, market) + size = self.safe_string(position, 'position_qty') + side: Str = None + if Precise.string_gt(size, '0'): + side = 'long' + else: + side = 'short' + contractSize = self.safe_string(market, 'contractSize') + markPrice = self.safe_string(position, 'mark_price') + timestamp = self.safe_integer(position, 'timestamp') + entryPrice = self.safe_string(position, 'average_open_price') + unrealisedPnl = self.safe_string(position, 'unsettled_pnl') + size = Precise.string_abs(size) + notional = Precise.string_mul(size, markPrice) + return self.safe_position({ + 'info': position, + 'id': None, + 'symbol': self.safe_string(market, 'symbol'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastUpdateTimestamp': None, + 'initialMargin': None, + 'initialMarginPercentage': None, + 'maintenanceMargin': None, + 'maintenanceMarginPercentage': None, + 'entryPrice': self.parse_number(entryPrice), + 'notional': self.parse_number(notional), + 'leverage': None, + 'unrealizedPnl': self.parse_number(unrealisedPnl), + 'contracts': self.parse_number(size), + 'contractSize': self.parse_number(contractSize), + 'marginRatio': None, + 'liquidationPrice': self.safe_number(position, 'est_liq_price'), + 'markPrice': self.parse_number(markPrice), + 'lastPrice': None, + 'collateral': None, + 'marginMode': 'cross', + 'marginType': None, + 'side': side, + 'percentage': None, + 'hedged': None, + 'stopLossPrice': None, + 'takeProfitPrice': None, + }) + + def fetch_position(self, symbol: Str, params={}): + """ + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-one-position-info + + fetch data on an open position + :param str symbol: unified market symbol of the market the position is held in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `position structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = self.v1PrivateGetPositionSymbol(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "IMR_withdraw_orders": 0.1, + # "MMR_with_orders": 0.05, + # "average_open_price": 27908.14386047, + # "cost_position": -139329.358492, + # "est_liq_price": 117335.92899428, + # "fee_24_h": 123, + # "imr": 0.1, + # "last_sum_unitary_funding": 70.38, + # "mark_price": 27794.9, + # "mmr": 0.05, + # "pending_long_qty": 123, + # "pending_short_qty": 123, + # "pnl_24_h": 123, + # "position_qty": -5, + # "settle_price": 27865.8716984, + # "symbol": "PERP_BTC_USDC", + # "timestamp": 1685429350571, + # "unsettled_pnl": 354.858492 + # } + # } + # + data = self.safe_dict(response, 'data') + return self.parse_position(data, market) + + def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-all-positions-info + + :param str[] [symbols]: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + self.load_markets() + response = self.v1PrivateGetPositions(params) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "current_margin_ratio_with_orders": 1.2385, + # "free_collateral": 450315.09115, + # "initial_margin_ratio": 0.1, + # "initial_margin_ratio_with_orders": 0.1, + # "maintenance_margin_ratio": 0.05, + # "maintenance_margin_ratio_with_orders": 0.05, + # "margin_ratio": 1.2385, + # "open_margin_ratio": 1.2102, + # "total_collateral_value": 489865.71329, + # "total_pnl_24_h": 123, + # "rows": [{ + # "IMR_withdraw_orders": 0.1, + # "MMR_with_orders": 0.05, + # "average_open_price": 27908.14386047, + # "cost_position": -139329.358492, + # "est_liq_price": 117335.92899428, + # "fee_24_h": 123, + # "imr": 0.1, + # "last_sum_unitary_funding": 70.38, + # "mark_price": 27794.9, + # "mmr": 0.05, + # "pending_long_qty": 123, + # "pending_short_qty": 123, + # "pnl_24_h": 123, + # "position_qty": -5, + # "settle_price": 27865.8716984, + # "symbol": "PERP_BTC_USDC", + # "timestamp": 1685429350571, + # "unsettled_pnl": 354.858492 + # }] + # } + # } + # + result = self.safe_dict(response, 'data', {}) + positions = self.safe_list(result, 'rows', []) + return self.parse_positions(positions, symbols) + + def nonce(self): + return self.milliseconds() + + def sign(self, path, section='public', method='GET', params={}, headers=None, body=None): + version = section[0] + access = section[1] + pathWithParams = self.implode_params(path, params) + url = self.implode_hostname(self.urls['api'][access]) + url += '/' + version + '/' + params = self.omit(params, self.extract_params(path)) + params = self.keysort(params) + if access == 'public': + url += pathWithParams + if params: + url += '?' + self.urlencode(params) + else: + self.check_required_credentials() + isPostOrPut = method == 'POST' or method == 'PUT' + isOrder = path == 'algo/order' or path == 'order' or path == 'batch-order' + if isPostOrPut and isOrder: + isSandboxMode = self.safe_bool(self.options, 'sandboxMode', False) + if not isSandboxMode: + brokerId = self.safe_string(self.options, 'brokerId', 'CCXTMODE') + if path == 'batch-order': + ordersList = self.safe_list(params, 'orders', []) + for i in range(0, len(ordersList)): + params['orders'][i]['order_tag'] = brokerId + else: + params['order_tag'] = brokerId + params = self.keysort(params) + auth = '' + ts = str(self.nonce()) + url += pathWithParams + apiKey = self.apiKey + if apiKey.find('ed25519:') < 0: + apiKey = 'ed25519:' + apiKey + headers = { + 'orderly-account-id': self.accountId, + 'orderly-key': apiKey, + 'orderly-timestamp': ts, + } + auth = ts + method + '/' + version + '/' + pathWithParams + if method == 'POST' or method == 'PUT': + body = self.json(params) + auth += body + headers['content-type'] = 'application/json' + else: + if params: + url += '?' + self.urlencode(params) + auth += '?' + self.rawencode(params) + headers['content-type'] = 'application/x-www-form-urlencoded' + if method == 'DELETE': + body = '' + secret = self.secret + if secret.find('ed25519:') >= 0: + parts = secret.split('ed25519:') + secret = parts[1] + signature = self.eddsa(self.encode(auth), self.base58_to_binary(secret), 'ed25519') + headers['orderly-signature'] = self.urlencode_base64(self.base64_to_binary(signature)) + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if not response: + return None # fallback to default error handler + # + # 400 Bad Request {"success":false,"code":-1012,"message":"Amount is required for buy market orders when margin disabled."} + # {"code":"-1011","message":"The system is under maintenance.","success":false} + # + success = self.safe_bool(response, 'success') + errorCode = self.safe_string(response, 'code') + if not success: + feedback = self.id + ' ' + self.json(response) + self.throw_broadly_matched_exception(self.exceptions['broad'], body, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + raise ExchangeError(feedback) + return None diff --git a/ccxt/mt5.py b/ccxt/mt5.py new file mode 100644 index 0000000..c223275 --- /dev/null +++ b/ccxt/mt5.py @@ -0,0 +1,750 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.mt5 import ImplicitAPI +from ccxt.base.types import Any, Balances, BorrowInterest, Conversion, CrossBorrowRate, Currencies, Currency, DepositAddress, FundingHistory, Greeks, Int, LedgerEntry, Leverage, LeverageTier, LeverageTiers, Liquidation, LongShortRatio, Market, Num, Option, OptionChain, Order, OrderBook, OrderRequest, CancellationRequest, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, FundingRates, Trade, TradingFeeInterface, TradingFees, Transaction, MarketInterface, TransferEntry +from typing import List +from typing import List, Optional # 添加 Optional 导入 +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import AccountSuspended +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import NoChange +from ccxt.base.errors import MarginModeAlreadySet +from ccxt.base.errors import ManualInteractionNeeded +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import NotSupported +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import InvalidNonce +from ccxt.base.errors import RequestTimeout +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class mt5(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(mt5, self).describe(), { + 'id': 'mt5', + 'name': 'MT5', + 'countries': ['US'], + 'version': 'v2025.02.05-05.23', + 'rateLimit': 1000, + 'hostname': '43.167.188.220:5000', + 'pro': True, + 'options': { + 'host': '18.163.85.196', + 'port': 443, + 'connectTimeoutSeconds': 30, + }, + 'has': { + 'CORS': True, + 'spot': True, + 'margin': True, + 'swap': False, + 'future': False, + 'option': False, + 'cancelOrder': True, + 'createOrder': True, + 'fetchBalance': True, + 'fetchClosedOrders': True, + 'fetchMarkets': True, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchTicker': True, + 'fetchTickers': True, + }, + 'timeframes': { + '1m': 1, + '5m': 5, + '15m': 15, + '30m': 30, + '1h': 60, + '4h': 240, + '1d': 1440, + '1w': 10080, + '1M': 43200, + }, + 'urls': { + 'logo': '', + 'api': { + 'public': 'http://43.167.188.220:5000', # 直接使用具体地址,不使用 {hostname} + 'private': 'http://43.167.188.220:5000', + }, + 'www': 'http://43.167.188.220:5000', + 'doc': ['http://43.167.188.220:5000/index.html'], + }, + 'api': { + 'public': { + 'get': { + 'Ping': 1 + }, + }, + 'private': { + 'get': { + 'Connect': 10, + 'CheckConnect': 1, + 'Disconnect': 1, + 'Symbols': 1, + 'ServerTimezone':1, + 'AccountSummary': 1, + 'AccountDetails': 1, + 'SymbolList': 1, + 'GetQuote': 1, + 'GetQuoteMany': 1, + 'MarketWatchMany': 1, + 'OpenedOrders': 1, + 'ClosedOrders': 1, + 'OpenedOrder': 1, + 'OrderHistory': 1, + 'PriceHistory': 1, + 'OrderSend': 1, + 'OrderModify': 1, + 'OrderClose': 1, + }, + }, + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + 'hostname': True, + }, + 'commonCurrencies': {}, + 'exceptions': { + 'exact': { + 'Invalid token': AuthenticationError, + 'Connection failed': ExchangeError, + 'Invalid symbol': ExchangeError, + 'Invalid order': InvalidOrder, + 'Order not found': OrderNotFound, + }, + }, + }) + + def ping_server(self): + """测试连接""" + response = self.public_get_ping() + if response == 'OK': + return True + else: + return False + + def get_token(self): + """获取或刷新 token""" + if hasattr(self, 'token') and self.token: + try: + self.check_connect() + return self.token + except Exception: + # Token 无效,重新连接 + pass + + # 重新连接获取 token + return self.connect() + + def connect(self): + """连接到 MT5 账户并获取 token""" + params = { + 'user': self.apiKey, # apiKey 作为 user + 'password': self.secret, # secret 作为 password + 'host': self.options['host'], # 账号所分配在的MT5服务器IP + 'port': self.options['port'], # 账号所分配在的MT5服务器端口 + 'connectTimeoutSeconds': self.options['connectTimeoutSeconds'], + } + + response = self.private_get_connect(params) + + self.token = response + return self.token + + def check_connect(self): + """检查连接状态""" + params = { + 'id': self.get_token(), + } + return self.private_get_checkconnect(params) + + def disconnect(self): + """断开连接""" + if hasattr(self, 'token') and self.token: + params = { + 'id': self.token, + } + try: + self.private_get_disconnect(params) + except Exception: + pass + finally: + self.token = None + + def load_token(self): + """确保 token 已加载""" + if not hasattr(self, 'token') or not self.token: + self.get_token() + + def server_timezone(self): + """获得mt5服务器时区""" + if hasattr(self, 'timezone'): + return self.timezone + else: + self.load_token() + request = { + 'id': self.token, + } + response = self.private_get_servertimezone(request) + self.timezone = int(float(response)) + return self.timezone + + def fetch_markets(self, params={}): + """获取交易对列表 - 修复版本""" + self.load_token() + request = { + 'id': self.token, + } + + try: + response = self.private_get_symbols(self.extend(request, params)) + + markets = [] + if isinstance(response, dict): + for symbol, info in response.items(): + try: + market = self.parse_market(info) + if market and market.get('symbol'): + markets.append(market) + except Exception as e: + # 跳过解析失败的市场,继续处理其他市场 + if self.verbose: + print(f"跳过交易对 {symbol}: {e}") + continue + + # 设置市场数据 + if markets: + self.markets = {} + self.symbols = [] + for market in markets: + id = market['id'] + symbol = market['symbol'] + self.markets[id] = market + self.markets[symbol] = market + self.symbols.append(symbol) + + self.symbols = sorted(self.symbols) + self.ids = sorted(self.markets.keys()) + + return markets + + except Exception as e: + raise ExchangeError(f"获取市场数据失败: {e}") + + def parse_market(self, info): + """解析市场信息 - 更健壮的版本""" + try: + # 安全获取 symbol + if not isinstance(info, dict): + return None + + symbol = self.safe_string(info, 'currency', '') + if not symbol: + return None + + symbol = symbol.upper().strip() + + # 确保符号格式正确 (如 EURUSD, BTCUSD) + if len(symbol) < 6: + # 处理较短的符号(如黄金 XAUUSD 是6位,但可能有其他情况) + # 对于无法确定格式的符号,直接使用原始符号 + base = symbol + quote = 'USD' # 默认报价货币 + else: + # 假设标准格式是3位基础货币+3位报价货币 + base = symbol[:3] + quote = symbol[3:] + + # 安全处理精度 + digits = self.safe_integer(info, 'digits', 5) + + # 确保 digits 是整数 + if digits is not None: + try: + digits = int(digits) + except (ValueError, TypeError): + digits = 5 + + market_id = symbol + + return { + 'id': market_id, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'baseId': base, + 'quoteId': quote, + 'active': True, + 'type': 'spot', + 'spot': True, + 'margin': True, + 'precision': { + 'price': digits, + 'amount': 2, + }, + 'limits': { + 'amount': { + 'min': self.safe_number(info, 'minVolume', 0.01), + 'max': self.safe_number(info, 'maxVolume'), + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'info': info, + } + except Exception as e: + if self.verbose: + print(f"解析市场信息失败: {e}, info: {info}") + return None + + def fetch_ticker(self, symbol, params={}): + """获取行情数据""" + self.load_token() + market = self.market(symbol) + request = { + 'id': self.token, + 'symbol': market['id'], + } + + response = self.private_get_getquote(self.extend(request, params)) + return self.parse_ticker(response, market) + + def parse_ticker(self, ticker, market=None): + """解析行情数据""" + symbol = market['symbol'] if market else None + timestamp = None + if ticker.get('time'): + try: + timestamp = self.parse8601(ticker.get('time')) + except: + timestamp = None + + return { + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp) if timestamp else None, + 'high': None, + 'low': None, + 'bid': self.safe_number(ticker, 'bid'), + 'bidVolume': None, + 'ask': self.safe_number(ticker, 'ask'), + 'askVolume': None, + 'vwap': None, + 'open': None, + 'close': None, + 'last': self.safe_number(ticker, 'last'), + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': self.safe_number(ticker, 'volume'), + 'quoteVolume': None, + 'info': ticker, + } + + def fetch_tickers(self, symbols: Optional[List[str]] = None, params={}): + """获取多个交易对的行情数据""" + self.load_token() + + request = { + 'id': self.token, + } + + # 如果指定了特定的交易对 + if symbols is not None: + # 将符号列表转换为 MT5 格式(如 ['EUR/USD', 'GBP/USD'] -> ['EURUSD', 'GBPUSD']) + mt5_symbols = [] + for symbol in symbols: + market = self.market(symbol) + mt5_symbols.append(market['id']) + request['symbols'] = mt5_symbols + + try: + response = self.private_get_getquotemany(self.extend(request, params)) + return self.parse_tickers(response, symbols) + except Exception as e: + # 如果批量获取失败,回退到逐个获取 + if symbols is not None: + return self.fetch_tickers_fallback(symbols, params) + else: + raise ExchangeError(f"获取批量行情失败: {e}") + + def fetch_tickers_fallback(self, symbols, params={}): + """回退方法:逐个获取交易对行情""" + tickers = {} + for symbol in symbols: + try: + ticker = self.fetch_ticker(symbol, params) + tickers[symbol] = ticker + except Exception as e: + if self.verbose: + print(f"获取 {symbol} 行情失败: {e}") + continue + return tickers + + def parse_tickers(self, response, symbols=None): + """解析批量行情数据""" + tickers = {} + + if isinstance(response, list): + # 如果响应是数组 + for ticker_data in response: + try: + ticker = self.parse_ticker(ticker_data) + if ticker and ticker.get('symbol'): + tickers[ticker['symbol']] = ticker + except Exception as e: + if self.verbose: + print(f"解析行情数据失败: {e}") + continue + elif isinstance(response, dict): + # 如果响应是字典 + for symbol_key, ticker_data in response.items(): + try: + ticker = self.parse_ticker(ticker_data) + if ticker and ticker.get('symbol'): + tickers[ticker['symbol']] = ticker + except Exception as e: + if self.verbose: + print(f"解析行情数据失败 {symbol_key}: {e}") + continue + + # 如果指定了特定的交易对,确保返回的顺序一致 + if symbols is not None: + ordered_tickers = {} + for symbol in symbols: + if symbol in tickers: + ordered_tickers[symbol] = tickers[symbol] + return ordered_tickers + + return tickers + + def fetch_order_book(self, symbol, limit=None, params={}): + """获取订单簿""" + self.load_token() + market = self.market(symbol) + request = { + 'id': self.token, + 'symbol': market['id'], + } + response = self.private_get_getquote(self.extend(request, params)) + + # MT5 的 GetQuote 返回的是最新报价,不是完整的订单簿 + # 这里模拟一个简单的订单簿 + bid = self.safe_number(response, 'bid') + ask = self.safe_number(response, 'ask') + + return { + 'symbol': symbol, + 'bids': [[bid, 1]] if bid else [], + 'asks': [[ask, 1]] if ask else [], + 'timestamp': None, + 'datetime': None, + 'nonce': None, + } + + def fetch_balance(self, params={}): + """获取账户余额""" + self.load_token() + request = { + 'id': self.token, + } + response = self.private_get_accountsummary(self.extend(request, params)) + + return self.parse_balance(response) + + def parse_balance(self, response): + """解析余额信息""" + result = { + 'info': response, + 'timestamp': None, + 'datetime': None, + } + + currency = 'USDT' # 强制设定为 USDT + balance = self.safe_number(response, 'balance', 0.0) + margin = self.safe_number(response, 'margin', 0.0) + free_margin = self.safe_number(response, 'freeMargin', 0.0) + + result[currency] = { + 'free': free_margin, + 'used': margin, + 'total': balance, + } + + return self.safe_balance(result) + + def fetch_account_details(self, params={}): + """获取账户信息""" + self.load_token() + request = { + 'id': self.token, + } + response = self.private_get_accountdetails(self.extend(request, params)) + + return self.parse_account(response) + + def parse_account(self, response): + """解析账户信息""" + return { + 'serverName': self.safe_string(response, 'serverName'), + 'user': self.safe_string(response, 'user'), + 'host': self.safe_string(response, 'host'), + 'port': self.safe_integer(response, 'port'), + 'serverTime': self.safe_string(response, 'serverTime'), + 'serverTimeZone': self.safe_integer(response, 'serverTimeZone'), + 'company': self.safe_string(response, 'company'), + 'currency': self.safe_string(response, 'currency', 'UST'), + 'accountName': self.safe_string(response, 'accountName'), + 'group': self.safe_string(response, 'group'), + 'accountType': self.safe_string(response, 'accountType'), + 'accountLeverage': self.safe_integer(response, 'accountLeverage'), + 'accountMethod': self.safe_string(response, 'accountMethod'), + 'isInvestor': self.safe_value(response, 'isInvestor', False), + } + + def fetch_open_orders(self, symbol=None, since=None, limit=None, params={}): + """获取未平仓订单 - 修复版本""" + self.load_token() + request = { + 'id': self.token, + } + + response = self.private_get_openedorders(self.extend(request, params)) + + # 如果指定了特定交易对,进行过滤 + if symbol is not None: + market = self.market(symbol) + filtered_orders = [] + for order in response: + if isinstance(order, dict) and order.get('symbol') == market['id']: + filtered_orders.append(order) + return self.parse_orders(filtered_orders, market, since, limit) + else: + return self.parse_orders(response, None, since, limit) + + def fetch_closed_orders(self, symbol=None, since=None, limit=None, params={}): + """获取已平仓订单 - 修复版本""" + self.load_token() + request = { + 'id': self.token, + } + + response = self.private_get_closedorders(self.extend(request, params)) + # print(response) + # 如果指定了特定交易对,进行过滤 + if symbol is not None: + market = self.market(symbol) + filtered_orders = [] + for order in response: + if isinstance(order, dict) and order.get('symbol') == market['id']: + filtered_orders.append(order) + return self.parse_orders(filtered_orders, market, since, limit) + else: + return self.parse_orders(response, None, since, limit) + + def parse_order(self, order, market=None): + """解析订单信息 - 修复市场符号问题""" + try: + id = self.safe_string(order, 'ticket') + market_id = self.safe_string(order, 'symbol') + + # 安全地解析市场符号 + symbol = None + if market is not None: + symbol = market['symbol'] + elif market_id is not None: + # 修复:提供更多参数来正确解析符号 + # 假设 MT5 的符号格式是 BASEQUOTE(如 EURUSD, BTCUSD) + if len(market_id) >= 6: + # 尝试解析为 3+3 格式(如 EURUSD, GBPUSD) + base = market_id[:3] + quote = market_id[3:] + symbol = base + '/' + quote + else: + # 如果无法解析,使用原始 market_id + symbol = market_id + + timestamp = self.parse8601(self.safe_string(order, 'openTime')) + last_trade_timestamp = self.parse8601(self.safe_string(order, 'closeTime')) + + status = self.parse_order_status(self.safe_string(order, 'state')) + side = self.parse_order_side(self.safe_string(order, 'orderType')) + type = self.parse_order_type(self.safe_string(order, 'orderType')) + + price = self.safe_number(order, 'openPrice') + amount = self.safe_number(order, 'lots') + filled = self.safe_number(order, 'closeLots', 0) + + remaining = None + if amount is not None and filled is not None: + remaining = amount - filled + + cost = None + if price is not None and filled is not None: + cost = price * filled + + fee = None + fee_cost = self.safe_number(order, 'commission', 0) + if fee_cost != 0: + fee = { + 'cost': fee_cost, + 'currency': None, + } + + return self.safe_order({ + 'id': id, + 'clientOrderId': None, + 'datetime': self.iso8601(timestamp), + 'timestamp': timestamp, + 'lastTradeTimestamp': last_trade_timestamp, + 'status': status, + 'symbol': symbol, + 'type': type, + 'timeInForce': None, + 'postOnly': None, + 'side': side, + 'price': price, + 'stopPrice': None, + 'triggerPrice': None, + 'amount': amount, + 'filled': filled, + 'remaining': remaining, + 'cost': cost, + 'trades': None, + 'fee': fee, + 'info': order, + 'average': None, + }) + except Exception as e: + if self.verbose: + print(f"解析订单失败: {e}, order: {order}") + raise e + + def parse_order_status(self, status): + statuses = { + 'Started': 'open', + 'Placed': 'open', + 'Cancelled': 'canceled', + 'Partial': 'open', + 'Filled': 'closed', + 'Rejected': 'rejected', + 'Expired': 'expired', + } + return self.safe_string(statuses, status, status) + + def parse_order_side(self, side): + sides = { + 'Buy': 'buy', + 'Sell': 'sell', + 'BuyLimit': 'buy', + 'SellLimit': 'sell', + 'BuyStop': 'buy', + 'SellStop': 'sell', + } + return self.safe_string(sides, side, side) + + def parse_order_type(self, type): + types = { + 'Buy': 'market', + 'Sell': 'market', + 'BuyLimit': 'limit', + 'SellLimit': 'limit', + 'BuyStop': 'stop', + 'SellStop': 'stop', + } + return self.safe_string(types, type, type) + + def create_order(self, symbol, type, side, amount, price=None, params={}): + """创建订单""" + self.load_token() + market = self.market(symbol) + + request = { + 'id': self.token, + 'symbol': market['id'], + 'volume': amount, + } + + # 映射订单类型 + operation_map = { + 'market': { + 'buy': 'Buy', + 'sell': 'Sell', + }, + 'limit': { + 'buy': 'BuyLimit', + 'sell': 'SellLimit', + }, + 'stop': { + 'buy': 'BuyStop', + 'sell': 'SellStop', + }, + } + + if type in operation_map and side in operation_map[type]: + request['operation'] = operation_map[type][side] + else: + raise InvalidOrder(self.id + ' createOrder does not support order type ' + type + ' and side ' + side) + + if type in ['limit', 'stop'] and price is not None: + request['price'] = price + + # 处理止损止盈 + stop_loss = self.safe_number(params, 'stopLoss') + take_profit = self.safe_number(params, 'takeProfit') + if stop_loss is not None: + request['stoploss'] = stop_loss + if take_profit is not None: + request['takeprofit'] = take_profit + + response = self.private_get_ordersend(self.extend(request, params)) + return self.parse_order(response, market) + + def cancel_order(self, id, symbol=None, params={}): + """取消订单""" + self.load_token() + request = { + 'id': self.token, + 'ticket': int(id), + } + response = self.private_get_orderclose(self.extend(request, params)) + return self.parse_order(response) + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + """签名请求""" + url = self.urls['api'][api] + '/' + path + query = self.omit(params, self.extract_params(path)) + + if method == 'GET' and query: + url += '?' + self.urlencode(query) + + return { + 'url': url, + 'method': method, + 'body': body, + 'headers': headers + } \ No newline at end of file diff --git a/ccxt/myokx.py b/ccxt/myokx.py new file mode 100644 index 0000000..74f6c15 --- /dev/null +++ b/ccxt/myokx.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.okx import okx +from ccxt.abstract.myokx import ImplicitAPI +from ccxt.base.types import Any + + +class myokx(okx, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(myokx, self).describe(), { + 'id': 'myokx', + 'name': 'MyOKX(EEA)', + 'certified': False, + 'pro': True, + 'hostname': 'eea.okx.com', + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/152485636-38b19e4a-bece-4dec-979a-5982859ffc04.jpg', + 'api': { + 'rest': 'https://{hostname}', + }, + 'www': 'https://my.okx.com', + 'doc': 'https://my.okx.com/docs-v5/en/#overview', + 'fees': 'https://my.okx.com/pages/products/fees.html', + 'referral': { + 'url': 'https://www.my.okx.com/join/CCXT2023', + 'discount': 0.2, + }, + 'test': { + 'rest': 'https://{hostname}', + }, + }, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': None, + 'swap': False, + 'future': False, + 'option': False, + }, + 'features': { + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + }) diff --git a/ccxt/ndax.py b/ccxt/ndax.py new file mode 100644 index 0000000..60e400c --- /dev/null +++ b/ccxt/ndax.py @@ -0,0 +1,2511 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.ndax import ImplicitAPI +import hashlib +import json +from ccxt.base.types import Account, Any, Balances, Currencies, Currency, DepositAddress, IndexType, Int, LedgerEntry, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Ticker, Trade, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import OrderNotFound +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class ndax(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(ndax, self).describe(), { + 'id': 'ndax', + 'name': 'NDAX', + 'countries': ['CA'], # Canada + 'rateLimit': 1000, + 'pro': True, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createDepositAddress': True, + 'createOrder': True, + 'createReduceOnlyOrder': False, + 'createStopLimitOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'editOrder': True, + 'fetchAccounts': True, + 'fetchAllGreeks': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLedger': True, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrice': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchOrderTrades': True, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTickers': False, + 'fetchTime': False, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': False, + 'fetchUnderlyingAssets': False, + 'fetchVolatilityHistory': False, + 'fetchWithdrawals': True, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'sandbox': True, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'signIn': True, + 'transfer': False, + 'withdraw': True, + }, + 'timeframes': { + '1m': '60', + '5m': '300', + '15m': '900', + '30m': '1800', + '1h': '3600', + '2h': '7200', + '4h': '14400', + '6h': '21600', + '12h': '43200', + '1d': '86400', + '1w': '604800', + '1M': '2419200', + '4M': '9676800', + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/108623144-67a3ef00-744e-11eb-8140-75c6b851e945.jpg', + 'test': { + 'public': 'https://ndaxmarginstaging.cdnhop.net:8443/AP', + 'private': 'https://ndaxmarginstaging.cdnhop.net:8443/AP', + }, + 'api': { + 'public': 'https://api.ndax.io:8443/AP', + 'private': 'https://api.ndax.io:8443/AP', + }, + 'www': 'https://ndax.io', + 'doc': [ + 'https://apidoc.ndax.io/', + ], + 'fees': 'https://ndax.io/fees', + 'referral': 'https://one.ndax.io/bfQiSL', + }, + 'api': { + 'public': { + 'get': { + 'Activate2FA': 1, + 'Authenticate2FA': 1, + 'AuthenticateUser': 1, + 'GetL2Snapshot': 1, + 'GetLevel1': 1, + 'GetValidate2FARequiredEndpoints': 1, + 'LogOut': 1, + 'GetTickerHistory': 1, + 'GetProduct': 1, + 'GetProducts': 1, + 'GetInstrument': 1, + 'GetInstruments': 1, + 'Ping': 1, + 'trades': 1, # undocumented + 'GetLastTrades': 1, # undocumented + 'SubscribeLevel1': 1, + 'SubscribeLevel2': 1, + 'SubscribeTicker': 1, + 'SubscribeTrades': 1, + 'SubscribeBlockTrades': 1, + 'UnsubscribeBlockTrades': 1, + 'UnsubscribeLevel1': 1, + 'UnsubscribeLevel2': 1, + 'UnsubscribeTicker': 1, + 'UnsubscribeTrades': 1, + 'Authenticate': 1, # undocumented + }, + }, + 'private': { + 'get': { + 'GetUserAccountInfos': 1, + 'GetUserAccounts': 1, + 'GetUserAffiliateCount': 1, + 'GetUserAffiliateTag': 1, + 'GetUserConfig': 1, + 'GetAllUnredactedUserConfigsForUser': 1, + 'GetUnredactedUserConfigByKey': 1, + 'GetUserDevices': 1, + 'GetUserReportTickets': 1, + 'GetUserReportWriterResultRecords': 1, + 'GetAccountInfo': 1, + 'GetAccountPositions': 1, + 'GetAllAccountConfigs': 1, + 'GetTreasuryProductsForAccount': 1, + 'GetAccountTrades': 1, + 'GetAccountTransactions': 1, + 'GetOpenTradeReports': 1, + 'GetAllOpenTradeReports': 1, + 'GetTradesHistory': 1, + 'GetOpenOrders': 1, + 'GetOpenQuotes': 1, + 'GetOrderFee': 1, + 'GetOrderHistory': 1, + 'GetOrdersHistory': 1, + 'GetOrderStatus': 1, + 'GetOmsFeeTiers': 1, + 'GetAccountDepositTransactions': 1, + 'GetAccountWithdrawTransactions': 1, + 'GetAllDepositRequestInfoTemplates': 1, + 'GetDepositInfo': 1, + 'GetDepositRequestInfoTemplate': 1, + 'GetDeposits': 1, + 'GetDepositTicket': 1, + 'GetDepositTickets': 1, + 'GetOMSWithdrawFees': 1, + 'GetWithdrawFee': 1, + 'GetWithdraws': 1, + 'GetWithdrawTemplate': 1, + 'GetWithdrawTemplateTypes': 1, + 'GetWithdrawTicket': 1, + 'GetWithdrawTickets': 1, + }, + 'post': { + 'AddUserAffiliateTag': 1, + 'CancelUserReport': 1, + 'RegisterNewDevice': 1, + 'SubscribeAccountEvents': 1, + 'UpdateUserAffiliateTag': 1, + 'GenerateTradeActivityReport': 1, + 'GenerateTransactionActivityReport': 1, + 'GenerateTreasuryActivityReport': 1, + 'ScheduleTradeActivityReport': 1, + 'ScheduleTransactionActivityReport': 1, + 'ScheduleTreasuryActivityReport': 1, + 'CancelAllOrders': 1, + 'CancelOrder': 1, + 'CancelQuote': 1, + 'CancelReplaceOrder': 1, + 'CreateQuote': 1, + 'ModifyOrder': 1, + 'SendOrder': 1, + 'SubmitBlockTrade': 1, + 'UpdateQuote': 1, + 'CancelWithdraw': 1, + 'CreateDepositTicket': 1, + 'CreateWithdrawTicket': 1, + 'SubmitDepositTicketComment': 1, + 'SubmitWithdrawTicketComment': 1, + 'GetOrderHistoryByOrderId': 1, + }, + }, + }, + 'features': { + 'spot': { + 'sandbox': True, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerDirection': False, + 'triggerPriceType': { + 'last': True, + 'mark': False, + 'index': False, + # bid & ask + }, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': None, + # todo + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': False, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': False, + 'iceberg': True, # todo + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, # todo + 'daysBack': 100000, # todo + 'untilDays': 100000, # todo + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': None, + 'daysBack': None, + 'untilDays': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchClosedOrders': None, + 'fetchOHLCV': { + 'limit': None, + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'fees': { + 'trading': { + 'tierBased': False, + 'percentage': True, + 'maker': self.parse_number('0.002'), + 'taker': self.parse_number('0.0025'), + }, + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + 'uid': True, + # these credentials are required for signIn() and withdraw() + 'login': True, + 'password': True, + # 'twofa': True, + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'exact': { + 'Not_Enough_Funds': InsufficientFunds, # {"status":"Rejected","errormsg":"Not_Enough_Funds","errorcode":101} + 'Server Error': ExchangeError, # {"result":false,"errormsg":"Server Error","errorcode":102,"detail":null} + 'Resource Not Found': OrderNotFound, # {"result":false,"errormsg":"Resource Not Found","errorcode":104,"detail":null} + }, + 'broad': { + 'Invalid InstrumentId': BadSymbol, # {"result":false,"errormsg":"Invalid InstrumentId: 10000","errorcode":100,"detail":null} + 'This endpoint requires 2FACode along with the payload': AuthenticationError, + }, + }, + 'options': { + 'omsId': 1, + 'orderTypes': { + 'Market': 1, + 'Limit': 2, + 'StopMarket': 3, + 'StopLimit': 4, + 'TrailingStopMarket': 5, + 'TrailingStopLimit': 6, + 'BlockTrade': 7, + '1': 1, + '2': 2, + '3': 3, + '4': 4, + '5': 5, + '6': 6, + '7': 7, + }, + }, + }) + + def sign_in(self, params={}): + """ + sign in, must be called prior to using other authenticated methods + + https://apidoc.ndax.io/#authenticate2fa + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns: response from exchange + """ + self.check_required_credentials() + if self.login is None or self.password is None: + raise AuthenticationError(self.id + ' signIn() requires exchange.login, exchange.password') + request: dict = { + 'grant_type': 'client_credentials', # the only supported value + } + response = self.publicGetAuthenticate(self.extend(request, params)) + # + # { + # "Authenticated":true, + # "Requires2FA":true, + # "AuthType":"Google", + # "AddtlInfo":"", + # "Pending2FaToken": "6f5c4e66-f3ee-493e-9227-31cc0583b55f" + # } + # + sessionToken = self.safe_string(response, 'SessionToken') + if sessionToken is not None: + self.options['sessionToken'] = sessionToken + return response + pending2faToken = self.safe_string(response, 'Pending2FaToken') + if pending2faToken is not None: + if self.twofa is None: + raise AuthenticationError(self.id + ' signIn() requires exchange.twofa credentials') + self.options['pending2faToken'] = pending2faToken + request = { + 'Code': self.totp(self.twofa), + } + responseInner = self.publicGetAuthenticate2FA(self.extend(request, params)) + # + # { + # "Authenticated": True, + # "UserId":57764, + # "SessionToken":"4a2a5857-c4e5-4fac-b09e-2c4c30b591a0" + # } + # + sessionToken = self.safe_string(responseInner, 'SessionToken') + self.options['sessionToken'] = sessionToken + return responseInner + return response + + def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://apidoc.ndax.io/#getproduct + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + omsId = self.safe_integer(self.options, 'omsId', 1) + request: dict = { + 'omsId': omsId, + } + response = self.publicGetGetProducts(self.extend(request, params)) + # + # [ + # { + # "OMSId": "1", + # "ProductId": "1", + # "Product": "BTC", + # "ProductFullName": "Bitcoin", + # "MasterDataUniqueProductSymbol": "", + # "ProductType": "CryptoCurrency", + # "DecimalPlaces": "8", + # "TickSize": "0.0000000100000000000000000000", + # "DepositEnabled": True, + # "WithdrawEnabled": True, + # "NoFees": False, + # "IsDisabled": False, + # "MarginEnabled": False + # }, + # ... + # + result: dict = {} + for i in range(0, len(response)): + currency = response[i] + id = self.safe_string(currency, 'ProductId') + code = self.safe_currency_code(self.safe_string(currency, 'Product')) + ProductType = self.safe_string(currency, 'ProductType') + type = 'fiat' if (ProductType == 'NationalCurrency') else 'crypto' + if ProductType == 'Unknown': + # such currency is just a blanket entry + type = 'other' + result[code] = self.safe_currency_structure({ + 'id': id, + 'name': self.safe_string(currency, 'ProductFullName'), + 'code': code, + 'type': type, + 'precision': self.safe_number(currency, 'TickSize'), + 'info': currency, + 'active': not self.safe_bool(currency, 'IsDisabled'), + 'deposit': self.safe_bool(currency, 'DepositEnabled'), + 'withdraw': self.safe_bool(currency, 'WithdrawEnabled'), + 'fee': None, + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + }, + 'networks': {}, + 'margin': self.safe_bool(currency, 'MarginEnabled'), + }) + return result + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for ndax + + https://apidoc.ndax.io/#getinstruments + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + omsId = self.safe_integer(self.options, 'omsId', 1) + request: dict = { + 'omsId': omsId, + } + response = self.publicGetGetInstruments(self.extend(request, params)) + # + # [ + # { + # "OMSId":1, + # "InstrumentId":3, + # "Symbol":"LTCBTC", + # "Product1":3, + # "Product1Symbol":"LTC", + # "Product2":1, + # "Product2Symbol":"BTC", + # "InstrumentType":"Standard", + # "VenueInstrumentId":3, + # "VenueId":1, + # "SortIndex":0, + # "SessionStatus":"Running", + # "PreviousSessionStatus":"Stopped", + # "SessionStatusDateTime":"2020-11-25T19:42:15.245Z", + # "SelfTradePrevention":true, + # "QuantityIncrement":0.0000000100000000000000000000, + # "PriceIncrement":0.0000000100000000000000000000, + # "MinimumQuantity":0.0100000000000000000000000000, + # "MinimumPrice":0.0000010000000000000000000000, + # "VenueSymbol":"LTCBTC", + # "IsDisable":false, + # "MasterDataId":0, + # "PriceCollarThreshold":0.0000000000000000000000000000, + # "PriceCollarPercent":0.0000000000000000000000000000, + # "PriceCollarEnabled":false, + # "PriceFloorLimit":0.0000000000000000000000000000, + # "PriceFloorLimitEnabled":false, + # "PriceCeilingLimit":0.0000000000000000000000000000, + # "PriceCeilingLimitEnabled":false, + # "CreateWithMarketRunning":true, + # "AllowOnlyMarketMakerCounterParty":false, + # "PriceCollarIndexDifference":0.0000000000000000000000000000, + # "PriceCollarConvertToOtcEnabled":false, + # "PriceCollarConvertToOtcClientUserId":0, + # "PriceCollarConvertToOtcAccountId":0, + # "PriceCollarConvertToOtcThreshold":0.0000000000000000000000000000, + # "OtcConvertSizeThreshold":0.0000000000000000000000000000, + # "OtcConvertSizeEnabled":false, + # "OtcTradesPublic":true, + # "PriceTier":0 + # }, + # ] + # + return self.parse_markets(response) + + def parse_market(self, market: dict) -> Market: + id = self.safe_string(market, 'InstrumentId') + # lowercaseId = self.safe_string_lower(market, 'symbol') + baseId = self.safe_string(market, 'Product1') + quoteId = self.safe_string(market, 'Product2') + base = self.safe_currency_code(self.safe_string(market, 'Product1Symbol')) + quote = self.safe_currency_code(self.safe_string(market, 'Product2Symbol')) + sessionStatus = self.safe_string(market, 'SessionStatus') + isDisable = self.safe_value(market, 'IsDisable') + sessionRunning = (sessionStatus == 'Running') + return { + 'id': id, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': (sessionRunning and not isDisable), + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.safe_number(market, 'QuantityIncrement'), + 'price': self.safe_number(market, 'PriceIncrement'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(market, 'MinimumQuantity'), + 'max': None, + }, + 'price': { + 'min': self.safe_number(market, 'MinimumPrice'), + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + 'info': market, + } + + def parse_order_book(self, orderbook, symbol, timestamp=None, bidsKey='bids', asksKey='asks', priceKey: IndexType = 6, amountKey: IndexType = 8, countOrIdKey: IndexType = 2): + nonce = None + result: dict = { + 'symbol': symbol, + 'bids': [], + 'asks': [], + 'timestamp': None, + 'datetime': None, + 'nonce': None, + } + for i in range(0, len(orderbook)): + level = orderbook[i] + if timestamp is None: + timestamp = self.safe_integer(level, 2) + else: + newTimestamp = self.safe_integer(level, 2) + timestamp = max(timestamp, newTimestamp) + if nonce is None: + nonce = self.safe_integer(level, 0) + else: + newNonce = self.safe_integer(level, 0) + nonce = max(nonce, newNonce) + bidask = self.parse_bid_ask(level, priceKey, amountKey) + levelSide = self.safe_integer(level, 9) + side = asksKey if levelSide else bidsKey + resultSide = result[side] + resultSide.append(bidask) + result['bids'] = self.sort_by(result['bids'], 0, True) + result['asks'] = self.sort_by(result['asks'], 0) + result['timestamp'] = timestamp + result['datetime'] = self.iso8601(timestamp) + result['nonce'] = nonce + return result + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://apidoc.ndax.io/#getl2snapshot + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + omsId = self.safe_integer(self.options, 'omsId', 1) + self.load_markets() + market = self.market(symbol) + limit = 100 if (limit is None) else limit # default 100 + request: dict = { + 'omsId': omsId, + 'InstrumentId': market['id'], + 'Depth': limit, # default 100 + } + response = self.publicGetGetL2Snapshot(self.extend(request, params)) + # + # [ + # [ + # 0, # 0 MDUpdateId + # 1, # 1 Number of Unique Accounts + # 123, # 2 ActionDateTime in Posix format X 1000 + # 0, # 3 ActionType 0(New), 1(Update), 2(Delete) + # 0.0, # 4 LastTradePrice + # 0, # 5 Number of Orders + # 0.0, # 6 Price + # 0, # 7 ProductPairCode + # 0.0, # 8 Quantity + # 0, # 9 Side + # ], + # [97244115,1,1607456142963,0,19069.32,1,19069.31,8,0.140095,0], + # [97244115,0,1607456142963,0,19069.32,1,19068.64,8,0.0055,0], + # [97244115,0,1607456142963,0,19069.32,1,19068.26,8,0.021291,0], + # [97244115,1,1607456142964,0,19069.32,1,19069.32,8,0.099636,1], + # [97244115,0,1607456142964,0,19069.32,1,19069.98,8,0.1,1], + # [97244115,0,1607456142964,0,19069.32,1,19069.99,8,0.141604,1], + # ] + # + return self.parse_order_book(response, symbol) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # fetchTicker + # + # { + # "OMSId":1, + # "InstrumentId":8, + # "BestBid":19069.31, + # "BestOffer":19069.32, + # "LastTradedPx":19069.32, + # "LastTradedQty":0.0001, + # "LastTradeTime":1607040406424, + # "SessionOpen":19069.32, + # "SessionHigh":19069.32, + # "SessionLow":19069.32, + # "SessionClose":19069.32, + # "Volume":0.0001, + # "CurrentDayVolume":0.0001, + # "CurrentDayNotional":1.906932, + # "CurrentDayNumTrades":1, + # "CurrentDayPxChange":0.00, + # "Rolling24HrVolume":0.000000000000000000000000000, + # "Rolling24HrNotional":0.00000000000000000000000, + # "Rolling24NumTrades":0, + # "Rolling24HrPxChange":0, + # "TimeStamp":"1607040406425", + # "BidQty":0, + # "AskQty":0, + # "BidOrderCt":0, + # "AskOrderCt":0, + # "Rolling24HrPxChangePercent":0, + # } + # + timestamp = self.safe_integer(ticker, 'TimeStamp') + marketId = self.safe_string(ticker, 'InstrumentId') + market = self.safe_market(marketId, market) + symbol = self.safe_symbol(marketId, market) + last = self.safe_string(ticker, 'LastTradedPx') + percentage = self.safe_string(ticker, 'Rolling24HrPxChangePercent') + change = self.safe_string(ticker, 'Rolling24HrPxChange') + open = self.safe_string(ticker, 'SessionOpen') + baseVolume = self.safe_string(ticker, 'Rolling24HrVolume') + quoteVolume = self.safe_string(ticker, 'Rolling24HrNotional') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'SessionHigh'), + 'low': self.safe_string(ticker, 'SessionLow'), + 'bid': self.safe_string(ticker, 'BestBid'), + 'bidVolume': None, # self.safe_number(ticker, 'BidQty'), always shows 0 + 'ask': self.safe_string(ticker, 'BestOffer'), + 'askVolume': None, # self.safe_number(ticker, 'AskQty'), always shows 0 + 'vwap': None, + 'open': open, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': change, + 'percentage': percentage, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'info': ticker, + }, market) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://apidoc.ndax.io/#getlevel1 + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + omsId = self.safe_integer(self.options, 'omsId', 1) + self.load_markets() + market = self.market(symbol) + request: dict = { + 'omsId': omsId, + 'InstrumentId': market['id'], + } + response = self.publicGetGetLevel1(self.extend(request, params)) + # + # { + # "OMSId":1, + # "InstrumentId":8, + # "BestBid":19069.31, + # "BestOffer":19069.32, + # "LastTradedPx":19069.32, + # "LastTradedQty":0.0001, + # "LastTradeTime":1607040406424, + # "SessionOpen":19069.32, + # "SessionHigh":19069.32, + # "SessionLow":19069.32, + # "SessionClose":19069.32, + # "Volume":0.0001, + # "CurrentDayVolume":0.0001, + # "CurrentDayNotional":1.906932, + # "CurrentDayNumTrades":1, + # "CurrentDayPxChange":0.00, + # "Rolling24HrVolume":0.000000000000000000000000000, + # "Rolling24HrNotional":0.00000000000000000000000, + # "Rolling24NumTrades":0, + # "Rolling24HrPxChange":0, + # "TimeStamp":"1607040406425", + # "BidQty":0, + # "AskQty":0, + # "BidOrderCt":0, + # "AskOrderCt":0, + # "Rolling24HrPxChangePercent":0, + # } + # + return self.parse_ticker(response, market) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # [ + # 1501603632000, # 0 DateTime + # 2700.33, # 1 High + # 2687.01, # 2 Low + # 2687.01, # 3 Open + # 2687.01, # 4 Close + # 24.86100992, # 5 Volume + # 0, # 6 Inside Bid Price + # 2870.95, # 7 Inside Ask Price + # 1 # 8 InstrumentId + # ] + # + return [ + self.safe_integer(ohlcv, 0), + self.safe_number(ohlcv, 3), + self.safe_number(ohlcv, 1), + self.safe_number(ohlcv, 2), + self.safe_number(ohlcv, 4), + self.safe_number(ohlcv, 5), + ] + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://apidoc.ndax.io/#gettickerhistory + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + omsId = self.safe_integer(self.options, 'omsId', 1) + self.load_markets() + market = self.market(symbol) + request: dict = { + 'omsId': omsId, + 'InstrumentId': market['id'], + 'Interval': self.safe_string(self.timeframes, timeframe, timeframe), + } + duration = self.parse_timeframe(timeframe) + now = self.milliseconds() + if since is None: + if limit is not None: + request['FromDate'] = self.ymdhms(now - duration * limit * 1000) + request['ToDate'] = self.ymdhms(now) + else: + request['FromDate'] = self.ymdhms(since) + if limit is None: + request['ToDate'] = self.ymdhms(now) + else: + request['ToDate'] = self.ymdhms(self.sum(since, duration * limit * 1000)) + response = self.publicGetGetTickerHistory(self.extend(request, params)) + # + # [ + # [1607299260000,19069.32,19069.32,19069.32,19069.32,0,19069.31,19069.32,8,1607299200000], + # [1607299320000,19069.32,19069.32,19069.32,19069.32,0,19069.31,19069.32,8,1607299260000], + # [1607299380000,19069.32,19069.32,19069.32,19069.32,0,19069.31,19069.32,8,1607299320000], + # ] + # + return self.parse_ohlcvs(response, market, timeframe, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(public) + # + # [ + # 6913253, # 0 TradeId + # 8, # 1 ProductPairCode + # 0.03340802, # 2 Quantity + # 19116.08, # 3 Price + # 2543425077, # 4 Order1 + # 2543425482, # 5 Order2 + # 1606935922416, # 6 Tradetime + # 0, # 7 Direction + # 1, # 8 TakerSide + # 0, # 9 BlockTrade + # 0, # 10 Either Order1ClientId or Order2ClientId + # ] + # + # fetchMyTrades(private) + # + # { + # "OMSId":1, + # "ExecutionId":16916567, + # "TradeId":14476351, + # "OrderId":2543565231, + # "AccountId":449, + # "AccountName":"igor@ccxt.trade", + # "SubAccountId":0, + # "ClientOrderId":0, + # "InstrumentId":8, + # "Side":"Sell", + # "OrderType":"Market", + # "Quantity":0.1230000000000000000000000000, + # "RemainingQuantity":0.0000000000000000000000000000, + # "Price":19069.310000000000000000000000, + # "Value":2345.5251300000000000000000000, + # "CounterParty":"7", + # "OrderTradeRevision":1, + # "Direction":"NoChange", + # "IsBlockTrade":false, + # "Fee":1.1727625650000000000000000000, + # "FeeProductId":8, + # "OrderOriginator":446, + # "UserName":"igor@ccxt.trade", + # "TradeTimeMS":1607565031569, + # "MakerTaker":"Taker", + # "AdapterTradeId":0, + # "InsideBid":19069.310000000000000000000000, + # "InsideBidSize":0.2400950000000000000000000000, + # "InsideAsk":19069.320000000000000000000000, + # "InsideAskSize":0.0997360000000000000000000000, + # "IsQuote":false, + # "CounterPartyClientUserId":1, + # "NotionalProductId":2, + # "NotionalRate":1.0000000000000000000000000000, + # "NotionalValue":2345.5251300000000000000000000, + # "NotionalHoldAmount":0, + # "TradeTime":637431618315686826 + # } + # + # fetchOrderTrades + # + # { + # "Side":"Sell", + # "OrderId":2543565235, + # "Price":18600.000000000000000000000000, + # "Quantity":0.0000000000000000000000000000, + # "DisplayQuantity":0.0000000000000000000000000000, + # "Instrument":8, + # "Account":449, + # "AccountName":"igor@ccxt.trade", + # "OrderType":"Limit", + # "ClientOrderId":0, + # "OrderState":"FullyExecuted", + # "ReceiveTime":1607585844956, + # "ReceiveTimeTicks":637431826449564182, + # "LastUpdatedTime":1607585844959, + # "LastUpdatedTimeTicks":637431826449593893, + # "OrigQuantity":0.1230000000000000000000000000, + # "QuantityExecuted":0.1230000000000000000000000000, + # "GrossValueExecuted":2345.3947500000000000000000000, + # "ExecutableValue":0.0000000000000000000000000000, + # "AvgPrice":19068.250000000000000000000000, + # "CounterPartyId":0, + # "ChangeReason":"Trade", + # "OrigOrderId":2543565235, + # "OrigClOrdId":0, + # "EnteredBy":446, + # "UserName":"igor@ccxt.trade", + # "IsQuote":false, + # "InsideAsk":19069.320000000000000000000000, + # "InsideAskSize":0.0997360000000000000000000000, + # "InsideBid":19068.250000000000000000000000, + # "InsideBidSize":1.3300010000000000000000000000, + # "LastTradePrice":19068.250000000000000000000000, + # "RejectReason":"", + # "IsLockedIn":false, + # "CancelReason":"", + # "OrderFlag":"0", + # "UseMargin":false, + # "StopPrice":0.0000000000000000000000000000, + # "PegPriceType":"Unknown", + # "PegOffset":0.0000000000000000000000000000, + # "PegLimitOffset":0.0000000000000000000000000000, + # "IpAddress":"x.x.x.x", + # "ClientOrderIdUuid":null, + # "OMSId":1 + # } + # + priceString = None + amountString = None + costString = None + timestamp = None + id = None + marketId = None + side = None + orderId = None + takerOrMaker = None + fee = None + type = None + if isinstance(trade, list): + priceString = self.safe_string(trade, 3) + amountString = self.safe_string(trade, 2) + timestamp = self.safe_integer(trade, 6) + id = self.safe_string(trade, 0) + marketId = self.safe_string(trade, 1) + takerSide = self.safe_value(trade, 8) + side = 'sell' if takerSide else 'buy' + orderId = self.safe_string(trade, 4) + else: + timestamp = self.safe_integer_2(trade, 'TradeTimeMS', 'ReceiveTime') + id = self.safe_string(trade, 'TradeId') + orderId = self.safe_string_2(trade, 'OrderId', 'OrigOrderId') + marketId = self.safe_string_2(trade, 'InstrumentId', 'Instrument') + priceString = self.safe_string(trade, 'Price') + amountString = self.safe_string(trade, 'Quantity') + costString = self.safe_string_2(trade, 'Value', 'GrossValueExecuted') + takerOrMaker = self.safe_string_lower(trade, 'MakerTaker') + side = self.safe_string_lower(trade, 'Side') + type = self.safe_string_lower(trade, 'OrderType') + feeCostString = self.safe_string(trade, 'Fee') + if feeCostString is not None: + feeCurrencyId = self.safe_string(trade, 'FeeProductId') + feeCurrencyCode = self.safe_currency_code(feeCurrencyId) + fee = { + 'cost': feeCostString, + 'currency': feeCurrencyCode, + } + symbol = self.safe_symbol(marketId, market) + return self.safe_trade({ + 'info': trade, + 'id': id, + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'order': orderId, + 'type': type, + 'side': side, + 'takerOrMaker': takerOrMaker, + 'price': priceString, + 'amount': amountString, + 'cost': costString, + 'fee': fee, + }, market) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + omsId = self.safe_integer(self.options, 'omsId', 1) + self.load_markets() + market = self.market(symbol) + request: dict = { + 'omsId': omsId, + 'InstrumentId': market['id'], + } + if limit is not None: + request['Count'] = limit + response = self.publicGetGetLastTrades(self.extend(request, params)) + # + # [ + # [6913253,8,0.03340802,19116.08,2543425077,2543425482,1606935922416,0,1,0,0], + # [6913254,8,0.01391671,19117.42,2543427510,2543427811,1606935927998,1,1,0,0], + # [6913255,8,0.000006,19107.81,2543430495,2543430793,1606935933881,2,0,0,0], + # ] + # + return self.parse_trades(response, market, since, limit) + + def fetch_accounts(self, params={}) -> List[Account]: + """ + fetch all the accounts associated with a profile + + https://apidoc.ndax.io/#getuseraccounts + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `account structures ` indexed by the account type + """ + if not self.login: + raise AuthenticationError(self.id + ' fetchAccounts() requires exchange.login email credential') + omsId = self.safe_integer(self.options, 'omsId', 1) + self.check_required_credentials() + request: dict = { + 'omsId': omsId, + 'UserId': self.uid, + 'UserName': self.login, + } + response = self.privateGetGetUserAccounts(self.extend(request, params)) + # + # [449] # comma-separated list of account ids + # + result = [] + for i in range(0, len(response)): + accountId = self.safe_string(response, i) + result.append({ + 'id': accountId, + 'type': None, + 'currency': None, + 'info': accountId, + }) + return result + + def parse_balance(self, response) -> Balances: + result: dict = { + 'info': response, + 'timestamp': None, + 'datetime': None, + } + for i in range(0, len(response)): + balance = response[i] + currencyId = self.safe_string(balance, 'ProductId') + if currencyId in self.currencies_by_id: + code = self.safe_currency_code(currencyId) + account = self.account() + account['total'] = self.safe_string(balance, 'Amount') + account['used'] = self.safe_string(balance, 'Hold') + result[code] = account + return self.safe_balance(result) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://apidoc.ndax.io/#getaccountpositions + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + omsId = self.safe_integer(self.options, 'omsId', 1) + self.load_markets() + self.load_accounts() + defaultAccountId = self.safe_integer_2(self.options, 'accountId', 'AccountId') + accountId = self.safe_integer_2(params, 'accountId', 'AccountId', defaultAccountId) + if accountId is None: + accountId = int(self.accounts[0]['id']) + params = self.omit(params, ['accountId', 'AccountId']) + request: dict = { + 'omsId': omsId, + 'AccountId': accountId, + } + response = self.privateGetGetAccountPositions(self.extend(request, params)) + # + # [ + # { + # "OMSId":1, + # "AccountId":449, + # "ProductSymbol":"BTC", + # "ProductId":1, + # "Amount":10.000000000000000000000000000, + # "Hold":0, + # "PendingDeposits":0.0000000000000000000000000000, + # "PendingWithdraws":0.0000000000000000000000000000, + # "TotalDayDeposits":10.000000000000000000000000000, + # "TotalMonthDeposits":10.000000000000000000000000000, + # "TotalYearDeposits":10.000000000000000000000000000, + # "TotalDayDepositNotional":10.000000000000000000000000000, + # "TotalMonthDepositNotional":10.000000000000000000000000000, + # "TotalYearDepositNotional":10.000000000000000000000000000, + # "TotalDayWithdraws":0, + # "TotalMonthWithdraws":0, + # "TotalYearWithdraws":0, + # "TotalDayWithdrawNotional":0, + # "TotalMonthWithdrawNotional":0, + # "TotalYearWithdrawNotional":0, + # "NotionalProductId":8, + # "NotionalProductSymbol":"USDT", + # "NotionalValue":10.000000000000000000000000000, + # "NotionalHoldAmount":0, + # "NotionalRate":1 + # }, + # ] + # + return self.parse_balance(response) + + def parse_ledger_entry_type(self, type): + types: dict = { + 'Trade': 'trade', + 'Deposit': 'transaction', + 'Withdraw': 'transaction', + 'Transfer': 'transfer', + 'OrderHold': 'trade', + 'WithdrawHold': 'transaction', + 'DepositHold': 'transaction', + 'MarginHold': 'trade', + 'ManualHold': 'trade', + 'ManualEntry': 'trade', + 'MarginAcquisition': 'trade', + 'MarginRelinquish': 'trade', + 'MarginQuoteHold': 'trade', + } + return self.safe_string(types, type, type) + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + # + # { + # "TransactionId": 2663709493, + # "ReferenceId": 68, + # "OMSId": 1, + # "AccountId": 449, + # "CR": 10.000000000000000000000000000, + # "DR": 0.0000000000000000000000000000, + # "Counterparty": 3, + # "TransactionType": "Other", + # "ReferenceType": "Deposit", + # "ProductId": 1, + # "Balance": 10.000000000000000000000000000, + # "TimeStamp": 1607532331591 + # } + # + currencyId = self.safe_string(item, 'ProductId') + currency = self.safe_currency(currencyId, currency) + credit = self.safe_string(item, 'CR') + debit = self.safe_string(item, 'DR') + amount = None + direction = None + if Precise.string_lt(credit, '0'): + amount = credit + direction = 'in' + elif Precise.string_lt(debit, '0'): + amount = debit + direction = 'out' + before = None + after = self.safe_string(item, 'Balance') + if direction == 'out': + before = Precise.string_add(after, amount) + elif direction == 'in': + before = Precise.string_max('0', Precise.string_sub(after, amount)) + timestamp = self.safe_integer(item, 'TimeStamp') + return self.safe_ledger_entry({ + 'info': item, + 'id': self.safe_string(item, 'TransactionId'), + 'direction': direction, + 'account': self.safe_string(item, 'AccountId'), + 'referenceId': self.safe_string(item, 'ReferenceId'), + 'referenceAccount': self.safe_string(item, 'Counterparty'), + 'type': self.parse_ledger_entry_type(self.safe_string(item, 'ReferenceType')), + 'currency': self.safe_currency_code(currencyId, currency), + 'amount': self.parse_number(amount), + 'before': self.parse_number(before), + 'after': self.parse_number(after), + 'status': 'ok', + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'fee': None, + }, currency) + + def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + + https://apidoc.ndax.io/#getaccounttransactions + + :param str [code]: unified currency code, default is None + :param int [since]: timestamp in ms of the earliest ledger entry, default is None + :param int [limit]: max number of ledger entries to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ledger structure ` + """ + omsId = self.safe_integer(self.options, 'omsId', 1) + self.load_markets() + self.load_accounts() + defaultAccountId = self.safe_integer_2(self.options, 'accountId', 'AccountId', int(self.accounts[0]['id'])) + accountId = self.safe_integer_2(params, 'accountId', 'AccountId', defaultAccountId) + params = self.omit(params, ['accountId', 'AccountId']) + request: dict = { + 'omsId': omsId, + 'AccountId': accountId, + } + if limit is not None: + request['Depth'] = limit + response = self.privateGetGetAccountTransactions(self.extend(request, params)) + # + # [ + # { + # "TransactionId":2663709493, + # "ReferenceId":68, + # "OMSId":1, + # "AccountId":449, + # "CR":10.000000000000000000000000000, + # "DR":0.0000000000000000000000000000, + # "Counterparty":3, + # "TransactionType":"Other", + # "ReferenceType":"Deposit", + # "ProductId":1, + # "Balance":10.000000000000000000000000000, + # "TimeStamp":1607532331591 + # }, + # ] + # + currency = None + if code is not None: + currency = self.currency(code) + return self.parse_ledger(response, currency, since, limit) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'Accepted': 'open', + 'Rejected': 'rejected', + 'Working': 'open', + 'Canceled': 'canceled', + 'Expired': 'expired', + 'FullyExecuted': 'closed', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder + # + # { + # "status":"Accepted", + # "errormsg":"", + # "OrderId": 2543565231 + # } + # + # editOrder + # + # { + # "ReplacementOrderId": 1234, + # "ReplacementClOrdId": 1561, + # "OrigOrderId": 5678, + # "OrigClOrdId": 91011, + # } + # + # fetchOpenOrders, fetchClosedOrders + # + # { + # "Side":"Buy", + # "OrderId":2543565233, + # "Price":19010, + # "Quantity":0.345, + # "DisplayQuantity":0.345, + # "Instrument":8, + # "Account":449, + # "AccountName":"igor@ccxt.trade", + # "OrderType":"Limit", + # "ClientOrderId":0, + # "OrderState":"Working", + # "ReceiveTime":1607579326003, + # "ReceiveTimeTicks":637431761260028981, + # "LastUpdatedTime":1607579326005, + # "LastUpdatedTimeTicks":637431761260054714, + # "OrigQuantity":0.345, + # "QuantityExecuted":0, + # "GrossValueExecuted":0, + # "ExecutableValue":0, + # "AvgPrice":0, + # "CounterPartyId":0, + # "ChangeReason":"NewInputAccepted", + # "OrigOrderId":2543565233, + # "OrigClOrdId":0, + # "EnteredBy":446, + # "UserName":"igor@ccxt.trade", + # "IsQuote":false, + # "InsideAsk":19069.32, + # "InsideAskSize":0.099736, + # "InsideBid":19068.25, + # "InsideBidSize":1.330001, + # "LastTradePrice":19068.25, + # "RejectReason":"", + # "IsLockedIn":false, + # "CancelReason":"", + # "OrderFlag":"AddedToBook", + # "UseMargin":false, + # "StopPrice":0, + # "PegPriceType":"Unknown", + # "PegOffset":0, + # "PegLimitOffset":0, + # "IpAddress":null, + # "ClientOrderIdUuid":null, + # "OMSId":1 + # } + # + timestamp = self.safe_integer(order, 'ReceiveTime') + marketId = self.safe_string(order, 'Instrument') + return self.safe_order({ + 'id': self.safe_string_2(order, 'ReplacementOrderId', 'OrderId'), + 'clientOrderId': self.safe_string_2(order, 'ReplacementClOrdId', 'ClientOrderId'), + 'info': order, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': self.safe_integer(order, 'LastUpdatedTime'), + 'status': self.parse_order_status(self.safe_string(order, 'OrderState')), + 'symbol': self.safe_symbol(marketId, market), + 'type': self.safe_string_lower(order, 'OrderType'), + 'timeInForce': None, + 'postOnly': None, + 'side': self.safe_string_lower(order, 'Side'), + 'price': self.safe_string(order, 'Price'), + 'triggerPrice': self.parse_number(self.omit_zero(self.safe_string(order, 'StopPrice'))), + 'cost': self.safe_string(order, 'GrossValueExecuted'), + 'amount': self.safe_string(order, 'OrigQuantity'), + 'filled': self.safe_string(order, 'QuantityExecuted'), + 'average': self.safe_string(order, 'AvgPrice'), + 'remaining': None, + 'fee': None, + 'trades': None, + }, market) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://apidoc.ndax.io/#sendorder + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: the price at which a trigger order would be triggered + :param str [params.clientOrderId]: a unique id for the order + :returns dict: an `order structure ` + """ + omsId = self.safe_integer(self.options, 'omsId', 1) + self.load_markets() + self.load_accounts() + defaultAccountId = self.safe_integer_2(self.options, 'accountId', 'AccountId', int(self.accounts[0]['id'])) + accountId = self.safe_integer_2(params, 'accountId', 'AccountId', defaultAccountId) + clientOrderId = self.safe_integer_2(params, 'ClientOrderId', 'clientOrderId') + orderType = self.safe_integer(self.options['orderTypes'], self.capitalize(type)) + triggerPrice = self.safe_string(params, 'triggerPrice') + if triggerPrice is not None: + if type == 'market': + orderType = 3 + elif type == 'limit': + orderType = 4 + params = self.omit(params, ['accountId', 'AccountId', 'clientOrderId', 'ClientOrderId', 'triggerPrice']) + market = self.market(symbol) + orderSide = 0 if (side == 'buy') else 1 + request: dict = { + 'InstrumentId': int(market['id']), + 'omsId': omsId, + 'AccountId': accountId, + 'TimeInForce': 1, # 0 Unknown, 1 GTC by default, 2 OPG execute to opening price, 3 IOC immediate or canceled, 4 FOK fill-or-kill, 5 GTX good 'til executed, 6 GTD good 'til date + # 'ClientOrderId': clientOrderId, # defaults to 0 + # If self order is order A, OrderIdOCO refers to the order ID of an order B(which is not the order being created by self call). + # If order B executes, then order A created by self call is canceled. + # You can also set up order B to watch order A in the same way, but that may require an update to order B to make it watch self one, which could have implications for priority in the order book. + # See CancelReplaceOrder and ModifyOrder. + # 'OrderIdOCO': 0, # The order ID if One Cancels the Other. + # 'UseDisplayQuantity': False, # If you enter a Limit order with a reserve, you must set UseDisplayQuantity to True + 'Side': orderSide, # 0 Buy, 1 Sell, 2 Short, 3 unknown an error condition + 'Quantity': float(self.amount_to_precision(symbol, amount)), + 'OrderType': orderType, # 0 Unknown, 1 Market, 2 Limit, 3 StopMarket, 4 StopLimit, 5 TrailingStopMarket, 6 TrailingStopLimit, 7 BlockTrade + # 'PegPriceType': 3, # 1 Last, 2 Bid, 3 Ask, 4 Midpoint + # 'LimitPrice': float(self.price_to_precision(symbol, price)), + } + # If OrderType=1(Market), Side=0(Buy), and LimitPrice is supplied, the Market order will execute up to the value specified + if price is not None: + request['LimitPrice'] = float(self.price_to_precision(symbol, price)) + if clientOrderId is not None: + request['ClientOrderId'] = clientOrderId + if triggerPrice is not None: + request['StopPrice'] = triggerPrice + response = self.privatePostSendOrder(self.extend(request, params)) + # + # { + # "status":"Accepted", + # "errormsg":"", + # "OrderId": 2543565231 + # } + # + return self.parse_order(response, market) + + def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + omsId = self.safe_integer(self.options, 'omsId', 1) + self.load_markets() + self.load_accounts() + defaultAccountId = self.safe_integer_2(self.options, 'accountId', 'AccountId', int(self.accounts[0]['id'])) + accountId = self.safe_integer_2(params, 'accountId', 'AccountId', defaultAccountId) + clientOrderId = self.safe_integer_2(params, 'ClientOrderId', 'clientOrderId') + params = self.omit(params, ['accountId', 'AccountId', 'clientOrderId', 'ClientOrderId']) + market = self.market(symbol) + orderSide = 0 if (side == 'buy') else 1 + request: dict = { + 'OrderIdToReplace': int(id), + 'InstrumentId': int(market['id']), + 'omsId': omsId, + 'AccountId': accountId, + 'TimeInForce': 1, # 0 Unknown, 1 GTC by default, 2 OPG execute to opening price, 3 IOC immediate or canceled, 4 FOK fill-or-kill, 5 GTX good 'til executed, 6 GTD good 'til date + # 'ClientOrderId': clientOrderId, # defaults to 0 + # If self order is order A, OrderIdOCO refers to the order ID of an order B(which is not the order being created by self call). + # If order B executes, then order A created by self call is canceled. + # You can also set up order B to watch order A in the same way, but that may require an update to order B to make it watch self one, which could have implications for priority in the order book. + # See CancelReplaceOrder and ModifyOrder. + # 'OrderIdOCO': 0, # The order ID if One Cancels the Other. + # 'UseDisplayQuantity': False, # If you enter a Limit order with a reserve, you must set UseDisplayQuantity to True + 'Side': orderSide, # 0 Buy, 1 Sell, 2 Short, 3 unknown an error condition + 'Quantity': float(self.amount_to_precision(symbol, amount)), + 'OrderType': self.safe_integer(self.options['orderTypes'], self.capitalize(type)), # 0 Unknown, 1 Market, 2 Limit, 3 StopMarket, 4 StopLimit, 5 TrailingStopMarket, 6 TrailingStopLimit, 7 BlockTrade + # 'PegPriceType': 3, # 1 Last, 2 Bid, 3 Ask, 4 Midpoint + # 'LimitPrice': float(self.price_to_precision(symbol, price)), + } + # If OrderType=1(Market), Side=0(Buy), and LimitPrice is supplied, the Market order will execute up to the value specified + if price is not None: + request['LimitPrice'] = float(self.price_to_precision(symbol, price)) + if clientOrderId is not None: + request['ClientOrderId'] = clientOrderId + response = self.privatePostCancelReplaceOrder(self.extend(request, params)) + # + # { + # "replacementOrderId": 1234, + # "replacementClOrdId": 1561, + # "origOrderId": 5678, + # "origClOrdId": 91011, + # } + # + return self.parse_order(response, market) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://apidoc.ndax.io/#gettradeshistory + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + omsId = self.safe_integer(self.options, 'omsId', 1) + self.load_markets() + self.load_accounts() + defaultAccountId = self.safe_integer_2(self.options, 'accountId', 'AccountId', int(self.accounts[0]['id'])) + accountId = self.safe_integer_2(params, 'accountId', 'AccountId', defaultAccountId) + params = self.omit(params, ['accountId', 'AccountId']) + request: dict = { + 'omsId': omsId, + 'AccountId': accountId, + # 'InstrumentId': market['id'], + # 'TradeId': 123, # If you specify TradeId, GetTradesHistory can return all states for a single trade + # 'OrderId': 456, # If specified, the call returns all trades associated with the order + # 'UserId': integer. The ID of the logged-in user. If not specified, the call returns trades associated with the users belonging to the default account for the logged-in user of self OMS. + # 'StartTimeStamp': long integer. The historical date and time at which to begin the trade report, in POSIX format. If not specified, reverts to the start date of self account on the trading venue. + # 'EndTimeStamp': long integer. Date at which to end the trade report, in POSIX format. + # 'Depth': integer. In self case, the count of trades to return, counting from the StartIndex. If Depth is not specified, returns all trades between BeginTimeStamp and EndTimeStamp, beginning at StartIndex. + # 'StartIndex': 0 # from the most recent trade 0 and moving backwards in time + # 'ExecutionId': 123, # The ID of the individual buy or sell execution. If not specified, returns all. + } + market = None + if symbol is not None: + market = self.market(symbol) + request['InstrumentId'] = market['id'] + if since is not None: + request['StartTimeStamp'] = self.parse_to_int(since / 1000) + if limit is not None: + request['Depth'] = limit + response = self.privateGetGetTradesHistory(self.extend(request, params)) + # + # [ + # { + # "OMSId":1, + # "ExecutionId":16916567, + # "TradeId":14476351, + # "OrderId":2543565231, + # "AccountId":449, + # "AccountName":"igor@ccxt.trade", + # "SubAccountId":0, + # "ClientOrderId":0, + # "InstrumentId":8, + # "Side":"Sell", + # "OrderType":"Market", + # "Quantity":0.1230000000000000000000000000, + # "RemainingQuantity":0.0000000000000000000000000000, + # "Price":19069.310000000000000000000000, + # "Value":2345.5251300000000000000000000, + # "CounterParty":"7", + # "OrderTradeRevision":1, + # "Direction":"NoChange", + # "IsBlockTrade":false, + # "Fee":1.1727625650000000000000000000, + # "FeeProductId":8, + # "OrderOriginator":446, + # "UserName":"igor@ccxt.trade", + # "TradeTimeMS":1607565031569, + # "MakerTaker":"Taker", + # "AdapterTradeId":0, + # "InsideBid":19069.310000000000000000000000, + # "InsideBidSize":0.2400950000000000000000000000, + # "InsideAsk":19069.320000000000000000000000, + # "InsideAskSize":0.0997360000000000000000000000, + # "IsQuote":false, + # "CounterPartyClientUserId":1, + # "NotionalProductId":2, + # "NotionalRate":1.0000000000000000000000000000, + # "NotionalValue":2345.5251300000000000000000000, + # "NotionalHoldAmount":0, + # "TradeTime":637431618315686826 + # } + # ] + # + return self.parse_trades(response, market, since, limit) + + def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders + + https://apidoc.ndax.io/#cancelallorders + + :param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + omsId = self.safe_integer(self.options, 'omsId', 1) + self.load_markets() + self.load_accounts() + defaultAccountId = self.safe_integer_2(self.options, 'accountId', 'AccountId', int(self.accounts[0]['id'])) + accountId = self.safe_integer_2(params, 'accountId', 'AccountId', defaultAccountId) + params = self.omit(params, ['accountId', 'AccountId']) + request: dict = { + 'omsId': omsId, + 'AccountId': accountId, + } + if symbol is not None: + market = self.market(symbol) + request['IntrumentId'] = market['id'] + response = self.privatePostCancelAllOrders(self.extend(request, params)) + # + # { + # "result":true, + # "errormsg":null, + # "errorcode":0, + # "detail":null + # } + # + return [ + self.safe_order({ + 'info': response, + }), + ] + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://apidoc.ndax.io/#cancelorder + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.clientOrderId]: a unique id for the order + :returns dict: An `order structure ` + """ + omsId = self.safe_integer(self.options, 'omsId', 1) + self.load_markets() + self.load_accounts() + # defaultAccountId = self.safe_integer_2(self.options, 'accountId', 'AccountId', int(self.accounts[0]['id'])) + # accountId = self.safe_integer_2(params, 'accountId', 'AccountId', defaultAccountId) + # params = self.omit(params, ['accountId', 'AccountId']) + market = None + if symbol is not None: + market = self.market(symbol) + request: dict = { + 'omsId': omsId, + # 'AccountId': accountId, + } + clientOrderId = self.safe_integer_2(params, 'clientOrderId', 'ClOrderId') + if clientOrderId is not None: + request['ClOrderId'] = clientOrderId + else: + request['OrderId'] = int(id) + params = self.omit(params, ['clientOrderId', 'ClOrderId']) + response = self.privatePostCancelOrder(self.extend(request, params)) + order = self.parse_order(response, market) + return self.extend(order, { + 'id': id, + 'clientOrderId': clientOrderId, + }) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://apidoc.ndax.io/#getopenorders + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + omsId = self.safe_integer(self.options, 'omsId', 1) + self.load_markets() + self.load_accounts() + defaultAccountId = self.safe_integer_2(self.options, 'accountId', 'AccountId', int(self.accounts[0]['id'])) + accountId = self.safe_integer_2(params, 'accountId', 'AccountId', defaultAccountId) + params = self.omit(params, ['accountId', 'AccountId']) + market = None + if symbol is not None: + market = self.market(symbol) + request: dict = { + 'omsId': omsId, + 'AccountId': accountId, + } + response = self.privateGetGetOpenOrders(self.extend(request, params)) + # + # [ + # { + # "Side":"Buy", + # "OrderId":2543565233, + # "Price":19010, + # "Quantity":0.345, + # "DisplayQuantity":0.345, + # "Instrument":8, + # "Account":449, + # "AccountName":"igor@ccxt.trade", + # "OrderType":"Limit", + # "ClientOrderId":0, + # "OrderState":"Working", + # "ReceiveTime":1607579326003, + # "ReceiveTimeTicks":637431761260028981, + # "LastUpdatedTime":1607579326005, + # "LastUpdatedTimeTicks":637431761260054714, + # "OrigQuantity":0.345, + # "QuantityExecuted":0, + # "GrossValueExecuted":0, + # "ExecutableValue":0, + # "AvgPrice":0, + # "CounterPartyId":0, + # "ChangeReason":"NewInputAccepted", + # "OrigOrderId":2543565233, + # "OrigClOrdId":0, + # "EnteredBy":446, + # "UserName":"igor@ccxt.trade", + # "IsQuote":false, + # "InsideAsk":19069.32, + # "InsideAskSize":0.099736, + # "InsideBid":19068.25, + # "InsideBidSize":1.330001, + # "LastTradePrice":19068.25, + # "RejectReason":"", + # "IsLockedIn":false, + # "CancelReason":"", + # "OrderFlag":"AddedToBook", + # "UseMargin":false, + # "StopPrice":0, + # "PegPriceType":"Unknown", + # "PegOffset":0, + # "PegLimitOffset":0, + # "IpAddress":null, + # "ClientOrderIdUuid":null, + # "OMSId":1 + # } + # ] + # + return self.parse_orders(response, market, since, limit) + + def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://apidoc.ndax.io/#getorderhistory + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + omsId = self.safe_integer(self.options, 'omsId', 1) + self.load_markets() + self.load_accounts() + defaultAccountId = self.safe_integer_2(self.options, 'accountId', 'AccountId', int(self.accounts[0]['id'])) + accountId = self.safe_integer_2(params, 'accountId', 'AccountId', defaultAccountId) + params = self.omit(params, ['accountId', 'AccountId']) + request: dict = { + 'omsId': omsId, + 'AccountId': accountId, + # 'ClientOrderId': clientOrderId, + # 'OriginalOrderId': id, + # 'OriginalClientOrderId': long integer, + # 'UserId': integer, + # 'InstrumentId': market['id'], + # 'StartTimestamp': since, + # 'EndTimestamp': self.milliseconds(), + # 'Depth': limit, + # 'StartIndex': 0, + } + market = None + if symbol is not None: + market = self.market(symbol) + request['InstrumentId'] = market['id'] + if since is not None: + request['StartTimeStamp'] = self.parse_to_int(since / 1000) + if limit is not None: + request['Depth'] = limit + response = self.privateGetGetOrdersHistory(self.extend(request, params)) + # + # [ + # { + # "Side":"Buy", + # "OrderId":2543565233, + # "Price":19010.000000000000000000000000, + # "Quantity":0.0000000000000000000000000000, + # "DisplayQuantity":0.3450000000000000000000000000, + # "Instrument":8, + # "Account":449, + # "AccountName":"igor@ccxt.trade", + # "OrderType":"Limit", + # "ClientOrderId":0, + # "OrderState":"Canceled", + # "ReceiveTime":1607579326003, + # "ReceiveTimeTicks":637431761260028981, + # "LastUpdatedTime":1607580965346, + # "LastUpdatedTimeTicks":637431777653463754, + # "OrigQuantity":0.3450000000000000000000000000, + # "QuantityExecuted":0.0000000000000000000000000000, + # "GrossValueExecuted":0.0000000000000000000000000000, + # "ExecutableValue":0.0000000000000000000000000000, + # "AvgPrice":0.0000000000000000000000000000, + # "CounterPartyId":0, + # "ChangeReason":"UserModified", + # "OrigOrderId":2543565233, + # "OrigClOrdId":0, + # "EnteredBy":446, + # "UserName":"igor@ccxt.trade", + # "IsQuote":false, + # "InsideAsk":19069.320000000000000000000000, + # "InsideAskSize":0.0997360000000000000000000000, + # "InsideBid":19068.250000000000000000000000, + # "InsideBidSize":1.3300010000000000000000000000, + # "LastTradePrice":19068.250000000000000000000000, + # "RejectReason":"", + # "IsLockedIn":false, + # "CancelReason":"UserModified", + # "OrderFlag":"AddedToBook, RemovedFromBook", + # "UseMargin":false, + # "StopPrice":0.0000000000000000000000000000, + # "PegPriceType":"Unknown", + # "PegOffset":0.0000000000000000000000000000, + # "PegLimitOffset":0.0000000000000000000000000000, + # "IpAddress":"x.x.x.x", + # "ClientOrderIdUuid":null, + # "OMSId":1 + # }, + # ] + # + return self.parse_orders(response, market, since, limit) + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://apidoc.ndax.io/#getorderstatus + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + omsId = self.safe_integer(self.options, 'omsId', 1) + self.load_markets() + self.load_accounts() + defaultAccountId = self.safe_integer_2(self.options, 'accountId', 'AccountId', int(self.accounts[0]['id'])) + accountId = self.safe_integer_2(params, 'accountId', 'AccountId', defaultAccountId) + params = self.omit(params, ['accountId', 'AccountId']) + market = None + if symbol is not None: + market = self.market(symbol) + request: dict = { + 'omsId': omsId, + 'AccountId': accountId, + 'OrderId': int(id), + } + response = self.privateGetGetOrderStatus(self.extend(request, params)) + # + # { + # "Side":"Sell", + # "OrderId":2543565232, + # "Price":0.0000000000000000000000000000, + # "Quantity":0.0000000000000000000000000000, + # "DisplayQuantity":0.0000000000000000000000000000, + # "Instrument":8, + # "Account":449, + # "AccountName":"igor@ccxt.trade", + # "OrderType":"Market", + # "ClientOrderId":0, + # "OrderState":"FullyExecuted", + # "ReceiveTime":1607569475591, + # "ReceiveTimeTicks":637431662755912377, + # "LastUpdatedTime":1607569475596, + # "LastUpdatedTimeTicks":637431662755960902, + # "OrigQuantity":1.0000000000000000000000000000, + # "QuantityExecuted":1.0000000000000000000000000000, + # "GrossValueExecuted":19068.270478610000000000000000, + # "ExecutableValue":0.0000000000000000000000000000, + # "AvgPrice":19068.270478610000000000000000, + # "CounterPartyId":0, + # "ChangeReason":"Trade", + # "OrigOrderId":2543565232, + # "OrigClOrdId":0, + # "EnteredBy":446, + # "UserName":"igor@ccxt.trade", + # "IsQuote":false, + # "InsideAsk":19069.320000000000000000000000, + # "InsideAskSize":0.0997360000000000000000000000, + # "InsideBid":19069.310000000000000000000000, + # "InsideBidSize":0.2400950000000000000000000000, + # "LastTradePrice":19069.310000000000000000000000, + # "RejectReason":"", + # "IsLockedIn":false, + # "CancelReason":"", + # "OrderFlag":"0", + # "UseMargin":false, + # "StopPrice":0.0000000000000000000000000000, + # "PegPriceType":"Unknown", + # "PegOffset":0.0000000000000000000000000000, + # "PegLimitOffset":0.0000000000000000000000000000, + # "IpAddress":"x.x.x.x", + # "ClientOrderIdUuid":null, + # "OMSId":1 + # } + # + return self.parse_order(response, market) + + def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all the trades made from a single order + + https://apidoc.ndax.io/#getorderhistorybyorderid + + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + omsId = self.safe_integer(self.options, 'omsId', 1) + self.load_markets() + self.load_accounts() + # defaultAccountId = self.safe_integer_2(self.options, 'accountId', 'AccountId', int(self.accounts[0]['id'])) + # accountId = self.safe_integer_2(params, 'accountId', 'AccountId', defaultAccountId) + # params = self.omit(params, ['accountId', 'AccountId']) + market = None + if symbol is not None: + market = self.market(symbol) + request: dict = { + 'OMSId': self.parse_to_int(omsId), + # 'AccountId': accountId, + 'OrderId': int(id), + } + response = self.privatePostGetOrderHistoryByOrderId(self.extend(request, params)) + # + # [ + # { + # "Side":"Sell", + # "OrderId":2543565235, + # "Price":18600.000000000000000000000000, + # "Quantity":0.0000000000000000000000000000, + # "DisplayQuantity":0.0000000000000000000000000000, + # "Instrument":8, + # "Account":449, + # "AccountName":"igor@ccxt.trade", + # "OrderType":"Limit", + # "ClientOrderId":0, + # "OrderState":"FullyExecuted", + # "ReceiveTime":1607585844956, + # "ReceiveTimeTicks":637431826449564182, + # "LastUpdatedTime":1607585844959, + # "LastUpdatedTimeTicks":637431826449593893, + # "OrigQuantity":0.1230000000000000000000000000, + # "QuantityExecuted":0.1230000000000000000000000000, + # "GrossValueExecuted":2345.3947500000000000000000000, + # "ExecutableValue":0.0000000000000000000000000000, + # "AvgPrice":19068.250000000000000000000000, + # "CounterPartyId":0, + # "ChangeReason":"Trade", + # "OrigOrderId":2543565235, + # "OrigClOrdId":0, + # "EnteredBy":446, + # "UserName":"igor@ccxt.trade", + # "IsQuote":false, + # "InsideAsk":19069.320000000000000000000000, + # "InsideAskSize":0.0997360000000000000000000000, + # "InsideBid":19068.250000000000000000000000, + # "InsideBidSize":1.3300010000000000000000000000, + # "LastTradePrice":19068.250000000000000000000000, + # "RejectReason":"", + # "IsLockedIn":false, + # "CancelReason":"", + # "OrderFlag":"0", + # "UseMargin":false, + # "StopPrice":0.0000000000000000000000000000, + # "PegPriceType":"Unknown", + # "PegOffset":0.0000000000000000000000000000, + # "PegLimitOffset":0.0000000000000000000000000000, + # "IpAddress":"x.x.x.x", + # "ClientOrderIdUuid":null, + # "OMSId":1 + # }, + # ] + # + grouped = self.group_by(response, 'ChangeReason') + trades = self.safe_list(grouped, 'Trade', []) + return self.parse_trades(trades, market, since, limit) + + def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + omsId = self.safe_integer(self.options, 'omsId', 1) + self.load_markets() + self.load_accounts() + defaultAccountId = self.safe_integer_2(self.options, 'accountId', 'AccountId', int(self.accounts[0]['id'])) + accountId = self.safe_integer_2(params, 'accountId', 'AccountId', defaultAccountId) + params = self.omit(params, ['accountId', 'AccountId']) + currency = self.currency(code) + request: dict = { + 'omsId': omsId, + 'AccountId': accountId, + 'ProductId': currency['id'], + 'GenerateNewKey': False, + } + response = self.privateGetGetDepositInfo(self.extend(request, params)) + # + # { + # "result":true, + # "errormsg":null, + # "statuscode":0, + # "AssetManagerId":1, + # "AccountId":57922, + # "AssetId":16, + # "ProviderId":23, + # "DepositInfo":"[\"0x8A27564b5c30b91C93B1591821642420F323a210\"]" + # } + # + return self.parse_deposit_address(response, currency) + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + # + # fetchDepositAddress, createDepositAddress + # + # { + # "result":true, + # "errormsg":null, + # "statuscode":0, + # "AssetManagerId":1, + # "AccountId":449, + # "AssetId":1, + # "ProviderId":1, + # "DepositInfo":"[\"r3e95RwVsLH7yCbnMfyh7SA8FdwUJCB4S2?memo=241452010\"]" + # } + # + depositInfoString = self.safe_string(depositAddress, 'DepositInfo') + depositInfo = json.loads(depositInfoString) + depositInfoLength = len(depositInfo) + lastString = self.safe_string(depositInfo, depositInfoLength - 1) + parts = lastString.split('?memo=') + address = self.safe_string(parts, 0) + tag = self.safe_string(parts, 1) + code = None + if currency is not None: + code = currency['code'] + self.check_address(address) + return { + 'info': depositAddress, + 'currency': code, + 'network': None, + 'address': address, + 'tag': tag, + } + + def create_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + create a currency deposit address + :param str code: unified currency code of the currency for the deposit address + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + request: dict = { + 'GenerateNewKey': True, + } + return self.fetch_deposit_address(code, self.extend(request, params)) + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://apidoc.ndax.io/#getdeposits + + :param str code: unified currency code + :param int [since]: not used by ndax fetchDeposits + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + omsId = self.safe_integer(self.options, 'omsId', 1) + self.load_markets() + self.load_accounts() + defaultAccountId = self.safe_integer_2(self.options, 'accountId', 'AccountId', int(self.accounts[0]['id'])) + accountId = self.safe_integer_2(params, 'accountId', 'AccountId', defaultAccountId) + params = self.omit(params, ['accountId', 'AccountId']) + currency = None + if code is not None: + currency = self.currency(code) + request: dict = { + 'omsId': omsId, + 'AccountId': accountId, + } + response = self.privateGetGetDeposits(self.extend(request, params)) + # + # "[ + # { + # "OMSId": 1, + # "DepositId": 44, + # "AccountId": 449, + # "SubAccountId": 0, + # "ProductId": 4, + # "Amount": 200.00000000000000000000000000, + # "LastUpdateTimeStamp": 637431291261187806, + # "ProductType": "CryptoCurrency", + # "TicketStatus": "FullyProcessed", + # "DepositInfo": "{ + # "AccountProviderId":42, + # "AccountProviderName":"USDT_BSC", + # "TXId":"0x3879b02632c69482646409e991149290bc9a58e4603be63c7c2c90a843f45d2b", + # "FromAddress":"0x8894E0a0c962CB723c1976a4421c95949bE2D4E3", + # "ToAddress":"0x5428EcEB1F7Ee058f64158589e27D087149230CB" + # },", + # "DepositCode": "ab0e23d5-a9ce-4d94-865f-9ab464fb1de3", + # "TicketNumber": 71, + # "NotionalProductId": 13, + # "NotionalValue": 200.00000000000000000000000000, + # "FeeAmount": 0.0000000000000000000000000000, + # }, + # ... + # ]" + # + if isinstance(response, str): + return self.parse_transactions(json.loads(response), currency, since, limit) + return self.parse_transactions(response, currency, since, limit) + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://apidoc.ndax.io/#getwithdraws + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + omsId = self.safe_integer(self.options, 'omsId', 1) + self.load_markets() + self.load_accounts() + defaultAccountId = self.safe_integer_2(self.options, 'accountId', 'AccountId', int(self.accounts[0]['id'])) + accountId = self.safe_integer_2(params, 'accountId', 'AccountId', defaultAccountId) + params = self.omit(params, ['accountId', 'AccountId']) + currency = None + if code is not None: + currency = self.currency(code) + request: dict = { + 'omsId': omsId, + 'AccountId': accountId, + } + response = self.privateGetGetWithdraws(self.extend(request, params)) + # + # [ + # { + # "Amount": 0.0, + # "FeeAmount": 0.0, + # "NotionalValue": 0.0, + # "WithdrawId": 0, + # "AssetManagerId": 0, + # "AccountId": 0, + # "AssetId": 0, + # "TemplateForm": "{\"TemplateType\": \"TetherRPCWithdraw\",\"Comment\": \"TestWithdraw\",\"ExternalAddress\": \"ms6C3pKAAr8gRCcnVebs8VRkVrjcvqNYv3\"}", + # "TemplateFormType": "TetherRPCWithdraw", + # "omsId": 0, + # "TicketStatus": 0, + # "TicketNumber": 0, + # "WithdrawTransactionDetails": "", + # "WithdrawType": "", + # "WithdrawCode": "490b4fa3-53fc-44f4-bd29-7e16be86fba3", + # "AssetType": 0, + # "Reaccepted": True, + # "NotionalProductId": 0 + # }, + # ] + # + return self.parse_transactions(response, currency, since, limit) + + def parse_transaction_status_by_type(self, status, type=None): + statusesByType: dict = { + 'deposit': { + 'New': 'pending', # new ticket awaiting operator review + 'AdminProcessing': 'pending', # an admin is looking at the ticket + 'Accepted': 'pending', # an admin accepts the ticket + 'Rejected': 'rejected', # admin rejects the ticket + 'SystemProcessing': 'pending', # automatic processing; an unlikely status for a deposit + 'FullyProcessed': 'ok', # the deposit has concluded + 'Failed': 'failed', # the deposit has failed for some reason + 'Pending': 'pending', # Account Provider has set status to pending + 'Confirmed': 'pending', # Account Provider confirms the deposit + 'AmlProcessing': 'pending', # anti-money-laundering process underway + 'AmlAccepted': 'pending', # anti-money-laundering process successful + 'AmlRejected': 'rejected', # deposit did not stand up to anti-money-laundering process + 'AmlFailed': 'failed', # anti-money-laundering process failed/did not complete + 'LimitsAccepted': 'pending', # deposit meets limits for fiat or crypto asset + 'LimitsRejected': 'rejected', # deposit does not meet limits for fiat or crypto asset + }, + 'withdrawal': { + 'New': 'pending', # awaiting operator review + 'AdminProcessing': 'pending', # An admin is looking at the ticket + 'Accepted': 'pending', # withdrawal will proceed + 'Rejected': 'rejected', # admin or automatic rejection + 'SystemProcessing': 'pending', # automatic processing underway + 'FullyProcessed': 'ok', # the withdrawal has concluded + 'Failed': 'failed', # the withdrawal failed for some reason + 'Pending': 'pending', # the admin has placed the withdrawal in pending status + 'Pending2Fa': 'pending', # user must click 2-factor authentication confirmation link + 'AutoAccepted': 'pending', # withdrawal will be automatically processed + 'Delayed': 'pending', # waiting for funds to be allocated for the withdrawal + 'UserCanceled': 'canceled', # withdraw canceled by user or Superuser + 'AdminCanceled': 'canceled', # withdraw canceled by Superuser + 'AmlProcessing': 'pending', # anti-money-laundering process underway + 'AmlAccepted': 'pending', # anti-money-laundering process complete + 'AmlRejected': 'rejected', # withdrawal did not stand up to anti-money-laundering process + 'AmlFailed': 'failed', # withdrawal did not complete anti-money-laundering process + 'LimitsAccepted': 'pending', # withdrawal meets limits for fiat or crypto asset + 'LimitsRejected': 'rejected', # withdrawal does not meet limits for fiat or crypto asset + 'Submitted': 'pending', # withdrawal sent to Account Provider; awaiting blockchain confirmation + 'Confirmed': 'pending', # Account Provider confirms that withdrawal is on the blockchain + 'ManuallyConfirmed': 'pending', # admin has sent withdrawal via wallet or admin function directly; marks ticket; debits account + 'Confirmed2Fa': 'pending', # user has confirmed withdraw via 2-factor authentication. + }, + } + statuses = self.safe_value(statusesByType, type, {}) + return self.safe_string(statuses, status, status) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fetchDeposits + # + # { + # "OMSId": 1, + # "DepositId": 44, + # "AccountId": 449, + # "SubAccountId": 0, + # "ProductId": 4, + # "Amount": 200.00000000000000000000000000, + # "LastUpdateTimeStamp": 637431291261187806, + # "ProductType": "CryptoCurrency", + # "TicketStatus": "FullyProcessed", + # "DepositInfo": "{ + # "AccountProviderId":42, + # "AccountProviderName":"USDT_BSC", + # "TXId":"0x3879b02632c69482646409e991149290bc9a58e4603be63c7c2c90a843f45d2b", + # "FromAddress":"0x8894E0a0c962CB723c1976a4421c95949bE2D4E3", + # "ToAddress":"0x5428EcEB1F7Ee058f64158589e27D087149230CB" + # }", + # "DepositCode": "ab0e23d5-a9ce-4d94-865f-9ab464fb1de3", + # "TicketNumber": 71, + # "NotionalProductId": 13, + # "NotionalValue": 200.00000000000000000000000000, + # "FeeAmount": 0.0000000000000000000000000000, + # } + # + # fetchWithdrawals + # + # { + # "Amount": 0.0, + # "FeeAmount": 0.0, + # "NotionalValue": 0.0, + # "WithdrawId": 0, + # "AssetManagerId": 0, + # "AccountId": 0, + # "AssetId": 0, + # "TemplateForm": "{\"TemplateType\": \"TetherRPCWithdraw\",\"Comment\": \"TestWithdraw\",\"ExternalAddress\": \"ms6C3pKAAr8gRCcnVebs8VRkVrjcvqNYv3\"}", + # "TemplateFormType": "TetherRPCWithdraw", + # "omsId": 0, + # "TicketStatus": 0, + # "TicketNumber": 0, + # "WithdrawTransactionDetails": "", + # "WithdrawType": "", + # "WithdrawCode": "490b4fa3-53fc-44f4-bd29-7e16be86fba3", + # "AssetType": 0, + # "Reaccepted": True, + # "NotionalProductId": 0 + # } + # + id = None + currencyId = self.safe_string(transaction, 'ProductId') + code = self.safe_currency_code(currencyId, currency) + type = None + if 'DepositId' in transaction: + id = self.safe_string(transaction, 'DepositId') + type = 'deposit' + elif 'WithdrawId' in transaction: + id = self.safe_string(transaction, 'WithdrawId') + type = 'withdrawal' + templateForm = self.parse_json(self.safe_value_2(transaction, 'TemplateForm', 'DepositInfo')) + updated = self.safe_integer(transaction, 'LastUpdateTimeStamp') + if templateForm is not None: + updated = self.safe_integer(templateForm, 'LastUpdated', updated) + address = self.safe_string_2(templateForm, 'ExternalAddress', 'ToAddress') + timestamp = self.safe_integer(templateForm, 'TimeSubmitted') + feeCost = self.safe_number(transaction, 'FeeAmount') + transactionStatus = self.safe_string(transaction, 'TicketStatus') + fee = None + if feeCost is not None: + fee = {'currency': code, 'cost': feeCost} + return { + 'info': transaction, + 'id': id, + 'txid': self.safe_string_2(templateForm, 'TxId', 'TXId'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'address': address, + 'addressTo': address, + 'addressFrom': self.safe_string(templateForm, 'FromAddress'), + 'tag': None, + 'tagTo': None, + 'tagFrom': None, + 'type': type, + 'amount': self.safe_number(transaction, 'Amount'), + 'currency': code, + 'status': self.parse_transaction_status_by_type(transactionStatus, type), + 'updated': updated, + 'fee': fee, + 'internal': None, + 'comment': None, + 'network': None, + } + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + # self method required login, password and twofa key + sessionToken = self.safe_string(self.options, 'sessionToken') + if sessionToken is None: + raise AuthenticationError(self.id + ' call signIn() method to obtain a session token') + if self.twofa is None: + raise AuthenticationError(self.id + ' withdraw() requires exchange.twofa credentials') + self.check_address(address) + omsId = self.safe_integer(self.options, 'omsId', 1) + self.load_markets() + self.load_accounts() + defaultAccountId = self.safe_integer_2(self.options, 'accountId', 'AccountId', int(self.accounts[0]['id'])) + accountId = self.safe_integer_2(params, 'accountId', 'AccountId', defaultAccountId) + params = self.omit(params, ['accountId', 'AccountId']) + currency = self.currency(code) + withdrawTemplateTypesRequest: dict = { + 'omsId': omsId, + 'AccountId': accountId, + 'ProductId': currency['id'], + } + withdrawTemplateTypesResponse = self.privateGetGetWithdrawTemplateTypes(withdrawTemplateTypesRequest) + # + # { + # "result": True, + # "errormsg": null, + # "statuscode": "0", + # "TemplateTypes": [ + # {AccountProviderId: "14", TemplateName: "ToExternalBitcoinAddress", AccountProviderName: "BitgoRPC-BTC"}, + # {AccountProviderId: "20", TemplateName: "ToExternalBitcoinAddress", AccountProviderName: "TrezorBTC"}, + # {AccountProviderId: "31", TemplateName: "BTC", AccountProviderName: "BTC Fireblocks 1"} + # ] + # } + # + templateTypes = self.safe_value(withdrawTemplateTypesResponse, 'TemplateTypes', []) + firstTemplateType = self.safe_value(templateTypes, 0) + if firstTemplateType is None: + raise ExchangeError(self.id + ' withdraw() could not find a withdraw template type for ' + currency['code']) + templateName = self.safe_string(firstTemplateType, 'TemplateName') + withdrawTemplateRequest: dict = { + 'omsId': omsId, + 'AccountId': accountId, + 'ProductId': currency['id'], + 'TemplateType': templateName, + 'AccountProviderId': firstTemplateType['AccountProviderId'], + } + withdrawTemplateResponse = self.privateGetGetWithdrawTemplate(withdrawTemplateRequest) + # + # { + # "result": True, + # "errormsg": null, + # "statuscode": "0", + # "Template": "{\"TemplateType\":\"ToExternalBitcoinAddress\",\"Comment\":\"\",\"ExternalAddress\":\"\"}" + # } + # + template = self.safe_string(withdrawTemplateResponse, 'Template') + if template is None: + raise ExchangeError(self.id + ' withdraw() could not find a withdraw template for ' + currency['code']) + withdrawTemplate = json.loads(template) + withdrawTemplate['ExternalAddress'] = address + if tag is not None: + if 'Memo' in withdrawTemplate: + withdrawTemplate['Memo'] = tag + withdrawPayload: dict = { + 'omsId': omsId, + 'AccountId': accountId, + 'ProductId': currency['id'], + 'TemplateForm': self.json(withdrawTemplate), + 'TemplateType': templateName, + } + withdrawRequest: dict = { + 'TfaType': 'Google', + 'TFaCode': self.totp(self.twofa), + 'Payload': self.json(withdrawPayload), + } + response = self.privatePostCreateWithdrawTicket(self.deep_extend(withdrawRequest, params)) + return self.parse_transaction(response, currency) + + def nonce(self): + return self.milliseconds() + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = self.urls['api'][api] + '/' + self.implode_params(path, params) + query = self.omit(params, self.extract_params(path)) + if api == 'public': + if path == 'Authenticate': + auth = self.login + ':' + self.password + auth64 = self.string_to_base64(auth) + headers = { + 'Authorization': 'Basic ' + auth64, + # 'Content-Type': 'application/json', + } + elif path == 'Authenticate2FA': + pending2faToken = self.safe_string(self.options, 'pending2faToken') + if pending2faToken is not None: + headers = { + 'Pending2FaToken': pending2faToken, + # 'Content-Type': 'application/json', + } + query = self.omit(query, 'pending2faToken') + if query: + url += '?' + self.urlencode(query) + elif api == 'private': + self.check_required_credentials() + sessionToken = self.safe_string(self.options, 'sessionToken') + if sessionToken is None: + nonce = str(self.nonce()) + auth = nonce + self.uid + self.apiKey + signature = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256) + headers = { + 'Nonce': nonce, + 'APIKey': self.apiKey, + 'Signature': signature, + 'UserId': self.uid, + } + else: + headers = { + 'APToken': sessionToken, + } + if method == 'POST': + headers['Content-Type'] = 'application/json' + body = self.json(query) + else: + if query: + url += '?' + self.urlencode(query) + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if code == 404: + raise AuthenticationError(self.id + ' ' + body) + if response is None: + return None + # + # {"status":"Rejected","errormsg":"Not_Enough_Funds","errorcode":101} + # {"result":false,"errormsg":"Server Error","errorcode":102,"detail":null} + # + message = self.safe_string(response, 'errormsg') + if (message is not None) and (message != ''): + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], body, feedback) + raise ExchangeError(feedback) + return None diff --git a/ccxt/novadax.py b/ccxt/novadax.py new file mode 100644 index 0000000..2b58693 --- /dev/null +++ b/ccxt/novadax.py @@ -0,0 +1,1641 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.novadax import ImplicitAPI +import hashlib +from ccxt.base.types import Account, Any, Balances, Currency, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, Transaction, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import AccountNotEnabled +from ccxt.base.errors import AccountSuspended +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import OnMaintenance +from ccxt.base.errors import CancelPending +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class novadax(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(novadax, self).describe(), { + 'id': 'novadax', + 'name': 'NovaDAX', + 'countries': ['BR'], # Brazil + # 6000 weight per min => 100 weight per second => min weight = 1 + # 100 requests per second =>( 1000ms / 100 ) = 10 ms between requests on average + 'rateLimit': 10, + 'version': 'v1', + # new metainfo interface + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelOrder': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createMarketBuyOrderWithCost': True, + 'createMarketOrderWithCost': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createReduceOnlyOrder': False, + 'createStopLimitOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'fetchAccounts': True, + 'fetchAllGreeks': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': False, + 'fetchDepositAddress': False, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchDepositsWithdrawals': True, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrice': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchOrderTrades': True, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': False, + 'fetchTransactions': 'emulated', + 'fetchUnderlyingAssets': False, + 'fetchVolatilityHistory': False, + 'fetchWithdrawals': True, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'transfer': True, + 'withdraw': True, + }, + 'timeframes': { + '1m': 'ONE_MIN', + '5m': 'FIVE_MIN', + '15m': 'FIFTEEN_MIN', + '30m': 'HALF_HOU', + '1h': 'ONE_HOU', + '1d': 'ONE_DAY', + '1w': 'ONE_WEE', + '1M': 'ONE_MON', + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/92337550-2b085500-f0b3-11ea-98e7-5794fb07dd3b.jpg', + 'api': { + 'public': 'https://api.novadax.com', + 'private': 'https://api.novadax.com', + }, + 'www': 'https://www.novadax.com.br', + 'doc': [ + 'https://doc.novadax.com/pt-BR/', + ], + 'fees': 'https://www.novadax.com.br/fees-and-limits', + 'referral': 'https://www.novadax.com.br/?s=ccxt', + }, + 'api': { + 'public': { + 'get': { + 'common/symbol': 1, + 'common/symbols': 1, + 'common/timestamp': 1, + 'market/tickers': 5, + 'market/ticker': 1, + 'market/depth': 1, + 'market/trades': 5, + 'market/kline/history': 5, + }, + }, + 'private': { + 'get': { + 'orders/get': 1, + 'orders/list': 10, + 'orders/fill': 3, # not found in doc + 'orders/fills': 10, + 'account/getBalance': 1, + 'account/subs': 1, + 'account/subs/balance': 1, + 'account/subs/transfer/record': 10, + 'wallet/query/deposit-withdraw': 3, + }, + 'post': { + 'orders/create': 5, + 'orders/batch-create': 50, + 'orders/cancel': 1, + 'orders/batch-cancel': 10, + 'orders/cancel-by-symbol': 10, + 'account/subs/transfer': 5, + 'wallet/withdraw/coin': 3, + 'account/withdraw/coin': 3, # not found in doc + }, + }, + }, + 'fees': { + 'trading': { + 'tierBased': False, + 'percentage': True, + 'taker': self.parse_number('0.005'), + 'maker': self.parse_number('0.0025'), + }, + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'exact': { + 'A99999': ExchangeError, # 500 Failed Internal error + # 'A10000': ExchangeError, # 200 Success Successful request + 'A10001': BadRequest, # 400 Params error Parameter is invalid + 'A10002': ExchangeError, # 404 Api not found API used is irrelevant + 'A10003': AuthenticationError, # 403 Authentication failed Authentication is failed + 'A10004': RateLimitExceeded, # 429 Too many requests Too many requests are made + 'A10005': PermissionDenied, # 403 Kyc required Need to complete KYC firstly + 'A10006': AccountSuspended, # 403 Customer canceled Account is canceled + 'A10007': AccountNotEnabled, # 400 Account not exist Sub account does not exist + 'A10011': BadSymbol, # 400 Symbol not exist Trading symbol does not exist + 'A10012': BadSymbol, # 400 Symbol not trading Trading symbol is temporarily not available + 'A10013': OnMaintenance, # 503 Symbol maintain Trading symbol is in maintain + 'A30001': OrderNotFound, # 400 Order not found Queried order is not found + 'A30002': InvalidOrder, # 400 Order amount is too small Order amount is too small + 'A30003': InvalidOrder, # 400 Order amount is invalid Order amount is invalid + 'A30004': InvalidOrder, # 400 Order value is too small Order value is too small + 'A30005': InvalidOrder, # 400 Order value is invalid Order value is invalid + 'A30006': InvalidOrder, # 400 Order price is invalid Order price is invalid + 'A30007': InsufficientFunds, # 400 Insufficient balance The balance is insufficient + 'A30008': InvalidOrder, # 400 Order was closed The order has been executed + 'A30009': InvalidOrder, # 400 Order canceled The order has been cancelled + 'A30010': CancelPending, # 400 Order cancelling The order is being cancelled + 'A30011': InvalidOrder, # 400 Order price too high The order price is too high + 'A30012': InvalidOrder, # 400 Order price too low The order price is too low + 'A40004': InsufficientFunds, # {"code":"A40004","data":[],"message":"sub account balance Insufficient"} + }, + 'broad': { + }, + }, + 'options': { + 'fetchOHLCV': { + 'volume': 'amount', # 'amount' for base volume or 'vol' for quote volume + }, + 'transfer': { + 'fillResponseFromRequest': True, + }, + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerDirection': True, # todo + 'triggerPriceType': None, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': None, + # todo + 'timeInForce': { + 'IOC': False, + 'FOK': False, + 'PO': False, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': False, + 'iceberg': True, # todo + }, + 'createOrders': None, # todo: add implementation + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 100000, # todo + 'untilDays': 100000, # todo + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 100000, # todo + 'untilDays': 100000, # todo + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 100000, # todo + 'daysBackCanceled': 1, # todo + 'untilDays': 100000, # todo + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': None, # todo max 3000 + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + }) + + def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://doc.novadax.com/en-US/#get-current-system-time + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = self.publicGetCommonTimestamp(params) + # + # { + # "code":"A10000", + # "data":1599090512080, + # "message":"Success" + # } + # + return self.safe_integer(response, 'data') + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for novadax + + https://doc.novadax.com/en-US/#get-all-supported-trading-symbol + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = self.publicGetCommonSymbols(params) + # + # { + # "code":"A10000", + # "data":[ + # { + # "amountPrecision":8, + # "baseCurrency":"BTC", + # "minOrderAmount":"0.001", + # "minOrderValue":"25", + # "pricePrecision":2, + # "quoteCurrency":"BRL", + # "status":"ONLINE", + # "symbol":"BTC_BRL", + # "valuePrecision":2 + # }, + # ], + # "message":"Success" + # } + # + data = self.safe_value(response, 'data', []) + return self.parse_markets(data) + + def parse_market(self, market: dict) -> Market: + baseId = self.safe_string(market, 'baseCurrency') + quoteId = self.safe_string(market, 'quoteCurrency') + id = self.safe_string(market, 'symbol') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + status = self.safe_string(market, 'status') + return { + 'id': id, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': (status == 'ONLINE'), + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(self.parse_precision(self.safe_string(market, 'amountPrecision'))), + 'price': self.parse_number(self.parse_precision(self.safe_string(market, 'pricePrecision'))), + # 'cost': self.parse_number(self.parse_precision(self.safe_string(market, 'valuePrecision'))), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(market, 'minOrderAmount'), + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': self.safe_number(market, 'minOrderValue'), + 'max': None, + }, + }, + 'created': None, + 'info': market, + } + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # fetchTicker, fetchTickers + # + # { + # "ask":"61946.1", + # "baseVolume24h":"164.41930186", + # "bid":"61815", + # "high24h":"64930.72", + # "lastPrice":"61928.41", + # "low24h":"61156.32", + # "open24h":"64512.46", + # "quoteVolume24h":"10308157.95", + # "symbol":"BTC_BRL", + # "timestamp":1599091115090 + # } + # + timestamp = self.safe_integer(ticker, 'timestamp') + marketId = self.safe_string(ticker, 'symbol') + symbol = self.safe_symbol(marketId, market, '_') + open = self.safe_string(ticker, 'open24h') + last = self.safe_string(ticker, 'lastPrice') + baseVolume = self.safe_string(ticker, 'baseVolume24h') + quoteVolume = self.safe_string(ticker, 'quoteVolume24h') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high24h'), + 'low': self.safe_string(ticker, 'low24h'), + 'bid': self.safe_string(ticker, 'bid'), + 'bidVolume': None, + 'ask': self.safe_string(ticker, 'ask'), + 'askVolume': None, + 'vwap': None, + 'open': open, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'info': ticker, + }, market) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://doc.novadax.com/en-US/#get-latest-ticker-for-specific-pair + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = self.publicGetMarketTicker(self.extend(request, params)) + # + # { + # "code":"A10000", + # "data":{ + # "ask":"61946.1", + # "baseVolume24h":"164.41930186", + # "bid":"61815", + # "high24h":"64930.72", + # "lastPrice":"61928.41", + # "low24h":"61156.32", + # "open24h":"64512.46", + # "quoteVolume24h":"10308157.95", + # "symbol":"BTC_BRL", + # "timestamp":1599091115090 + # }, + # "message":"Success" + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_ticker(data, market) + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://doc.novadax.com/en-US/#get-latest-tickers-for-all-trading-pairs + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + symbols = self.market_symbols(symbols) + response = self.publicGetMarketTickers(params) + # + # { + # "code":"A10000", + # "data":[ + # { + # "ask":"61879.36", + # "baseVolume24h":"164.40955092", + # "bid":"61815", + # "high24h":"64930.72", + # "lastPrice":"61820.04", + # "low24h":"61156.32", + # "open24h":"64624.19", + # "quoteVolume24h":"10307493.92", + # "symbol":"BTC_BRL", + # "timestamp":1599091291083 + # }, + # ], + # "message":"Success" + # } + # + data = self.safe_value(response, 'data', []) + result: dict = {} + for i in range(0, len(data)): + ticker = self.parse_ticker(data[i]) + symbol = ticker['symbol'] + result[symbol] = ticker + return self.filter_by_array_tickers(result, 'symbol', symbols) + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://doc.novadax.com/en-US/#get-market-depth + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['limit'] = limit # default 10, max 20 + response = self.publicGetMarketDepth(self.extend(request, params)) + # + # { + # "code":"A10000", + # "data":{ + # "asks":[ + # ["0.037159","0.3741"], + # ["0.037215","0.2706"], + # ["0.037222","1.8459"], + # ], + # "bids":[ + # ["0.037053","0.3857"], + # ["0.036969","0.8101"], + # ["0.036953","1.5226"], + # ], + # "timestamp":1599280414448 + # }, + # "message":"Success" + # } + # + data = self.safe_value(response, 'data', {}) + timestamp = self.safe_integer(data, 'timestamp') + return self.parse_order_book(data, market['symbol'], timestamp, 'bids', 'asks') + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # public fetchTrades + # + # { + # "amount":"0.0632", + # "price":"0.037288", + # "side":"BUY", + # "timestamp":1599279694576 + # } + # + # private fetchOrderTrades + # + # { + # "id": "608717046691139584", + # "orderId": "608716957545402368", + # "symbol": "BTC_BRL", + # "side": "BUY", + # "amount": "0.0988", + # "price": "45514.76", + # "fee": "0.0000988 BTC", + # "feeAmount": "0.0000988", + # "feeCurrency": "BTC", + # "role": "MAKER", + # "timestamp": 1565171053345 + # } + # + # private fetchMyTrades(same endpoint) + # + # { + # "id": "608717046691139584", + # "orderId": "608716957545402368", + # "symbol": "BTC_BRL", + # "side": "BUY", + # "amount": "0.0988", + # "price": "45514.76", + # "fee": "0.0000988 BTC", + # "feeAmount": "0.0000988", + # "feeCurrency": "BTC", + # "role": "MAKER", + # "timestamp": 1565171053345 + # } + # + id = self.safe_string(trade, 'id') + orderId = self.safe_string(trade, 'orderId') + timestamp = self.safe_integer(trade, 'timestamp') + side = self.safe_string_lower(trade, 'side') + priceString = self.safe_string(trade, 'price') + amountString = self.safe_string(trade, 'amount') + marketId = self.safe_string(trade, 'symbol') + symbol = self.safe_symbol(marketId, market, '_') + takerOrMaker = self.safe_string_lower(trade, 'role') + feeString = self.safe_string(trade, 'fee') + fee = None + if feeString is not None: + feeCurrencyId = self.safe_string(trade, 'feeCurrency') + feeCurrencyCode = self.safe_currency_code(feeCurrencyId) + fee = { + 'cost': self.safe_string(trade, 'feeAmount'), + 'currency': feeCurrencyCode, + } + return self.safe_trade({ + 'id': id, + 'order': orderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'type': None, + 'side': side, + 'price': priceString, + 'amount': amountString, + 'cost': None, + 'takerOrMaker': takerOrMaker, + 'fee': fee, + 'info': trade, + }, market) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://doc.novadax.com/en-US/#get-recent-trades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['limit'] = limit # default 100 + response = self.publicGetMarketTrades(self.extend(request, params)) + # + # { + # "code":"A10000", + # "data":[ + # {"amount":"0.0632","price":"0.037288","side":"BUY","timestamp":1599279694576}, + # {"amount":"0.0052","price":"0.03715","side":"SELL","timestamp":1599276606852}, + # {"amount":"0.0058","price":"0.037188","side":"SELL","timestamp":1599275187812}, + # ], + # "message":"Success" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_trades(data, market, since, limit) + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://doc.novadax.com/en-US/#get-kline-data + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'unit': self.safe_string(self.timeframes, timeframe, timeframe), + } + duration = self.parse_timeframe(timeframe) + now = self.seconds() + if limit is None: + limit = 3000 # max + if since is None: + request['from'] = now - limit * duration + request['to'] = now + else: + startFrom = self.parse_to_int(since / 1000) + request['from'] = startFrom + request['to'] = self.sum(startFrom, limit * duration) + response = self.publicGetMarketKlineHistory(self.extend(request, params)) + # + # { + # "code": "A10000", + # "data": [ + # { + # "amount": 8.25709100, + # "closePrice": 62553.20, + # "count": 29, + # "highPrice": 62592.87, + # "lowPrice": 62553.20, + # "openPrice": 62554.23, + # "score": 1602501480, + # "symbol": "BTC_BRL", + # "vol": 516784.2504067500 + # } + # ], + # "message": "Success" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_ohlcvs(data, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # { + # "amount": 8.25709100, + # "closePrice": 62553.20, + # "count": 29, + # "highPrice": 62592.87, + # "lowPrice": 62553.20, + # "openPrice": 62554.23, + # "score": 1602501480, + # "symbol": "BTC_BRL", + # "vol": 516784.2504067500 + # } + # + options = self.safe_value(self.options, 'fetchOHLCV', {}) + volumeField = self.safe_string(options, 'volume', 'amount') # or vol + return [ + self.safe_timestamp(ohlcv, 'score'), + self.safe_number(ohlcv, 'openPrice'), + self.safe_number(ohlcv, 'highPrice'), + self.safe_number(ohlcv, 'lowPrice'), + self.safe_number(ohlcv, 'closePrice'), + self.safe_number(ohlcv, volumeField), + ] + + def parse_balance(self, response) -> Balances: + data = self.safe_value(response, 'data', []) + result: dict = { + 'info': response, + 'timestamp': None, + 'datetime': None, + } + for i in range(0, len(data)): + balance = data[i] + currencyId = self.safe_string(balance, 'currency') + code = self.safe_currency_code(currencyId) + account = self.account() + account['total'] = self.safe_string(balance, 'balance') + account['free'] = self.safe_string(balance, 'available') + account['used'] = self.safe_string(balance, 'hold') + result[code] = account + return self.safe_balance(result) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://doc.novadax.com/en-US/#get-account-balance + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + response = self.privateGetAccountGetBalance(params) + # + # { + # "code": "A10000", + # "data": [ + # { + # "available": "1.23", + # "balance": "0.23", + # "currency": "BTC", + # "hold": "1" + # } + # ], + # "message": "Success" + # } + # + return self.parse_balance(response) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://doc.novadax.com/en-US/#order-introduction + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.cost]: for spot market buy orders, the quote quantity that can be used alternative for the amount + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + uppercaseType = type.upper() + uppercaseSide = side.upper() + request: dict = { + 'symbol': market['id'], + 'side': uppercaseSide, # or SELL + # "amount": self.amount_to_precision(symbol, amount), + # "price": "1234.5678", # required for LIMIT and STOP orders + # "operator": "" # for stop orders, can be found in order introduction + # "stopPrice": self.price_to_precision(symbol, stopPrice), + # "accountId": "...", # subaccount id, optional + } + triggerPrice = self.safe_value_2(params, 'triggerPrice', 'stopPrice') + if triggerPrice is None: + if (uppercaseType == 'STOP_LIMIT') or (uppercaseType == 'STOP_MARKET'): + raise ArgumentsRequired(self.id + ' createOrder() requires a stopPrice parameter for ' + uppercaseType + ' orders') + else: + if uppercaseType == 'LIMIT': + uppercaseType = 'STOP_LIMIT' + elif uppercaseType == 'MARKET': + uppercaseType = 'STOP_MARKET' + defaultOperator = 'LTE' if (uppercaseSide == 'BUY') else 'GTE' + request['operator'] = self.safe_string(params, 'operator', defaultOperator) + request['stopPrice'] = self.price_to_precision(symbol, triggerPrice) + params = self.omit(params, ['triggerPrice', 'stopPrice']) + if (uppercaseType == 'LIMIT') or (uppercaseType == 'STOP_LIMIT'): + request['price'] = self.price_to_precision(symbol, price) + request['amount'] = self.amount_to_precision(symbol, amount) + elif (uppercaseType == 'MARKET') or (uppercaseType == 'STOP_MARKET'): + if uppercaseSide == 'SELL': + request['amount'] = self.amount_to_precision(symbol, amount) + elif uppercaseSide == 'BUY': + quoteAmount = None + createMarketBuyOrderRequiresPrice = True + createMarketBuyOrderRequiresPrice, params = self.handle_option_and_params(params, 'createOrder', 'createMarketBuyOrderRequiresPrice', True) + cost = self.safe_number_2(params, 'cost', 'value') + params = self.omit(params, 'cost') + if cost is not None: + quoteAmount = self.cost_to_precision(symbol, cost) + elif createMarketBuyOrderRequiresPrice: + if price is None: + raise InvalidOrder(self.id + ' createOrder() requires the price argument for market buy orders to calculate the total cost to spend(amount * price), alternatively set the createMarketBuyOrderRequiresPrice option or param to False and pass the cost to spend(quote quantity) in the amount argument') + else: + amountString = self.number_to_string(amount) + priceString = self.number_to_string(price) + costRequest = Precise.string_mul(amountString, priceString) + quoteAmount = self.cost_to_precision(symbol, costRequest) + else: + quoteAmount = self.cost_to_precision(symbol, amount) + request['value'] = quoteAmount + request['type'] = uppercaseType + response = self.privatePostOrdersCreate(self.extend(request, params)) + # + # { + # "code": "A10000", + # "data": { + # "amount": "0.001", + # "averagePrice": null, + # "filledAmount": "0", + # "filledFee": "0", + # "filledValue": "0", + # "id": "870613508008464384", + # "operator": "GTE", + # "price": "210000", + # "side": "BUY", + # "status": "SUBMITTED", + # "stopPrice": "211000", + # "symbol": "BTC_BRL", + # "timestamp": 1627612035528, + # "type": "STOP_LIMIT", + # "value": "210" + # }, + # "message": "Success" + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data, market) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://doc.novadax.com/en-US/#cancel-an-order + + :param str id: order id + :param str symbol: not used by novadax cancelOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + request: dict = { + 'id': id, + } + response = self.privatePostOrdersCancel(self.extend(request, params)) + # + # { + # "code": "A10000", + # "data": { + # "result": True + # }, + # "message": "Success" + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data) + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://doc.novadax.com/en-US/#get-order-details + + :param str id: order id + :param str symbol: not used by novadax fetchOrder + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + request: dict = { + 'id': id, + } + response = self.privateGetOrdersGet(self.extend(request, params)) + # + # { + # "code": "A10000", + # "data": { + # "id": "608695623247466496", + # "symbol": "BTC_BRL", + # "type": "MARKET", + # "side": "SELL", + # "price": null, + # "averagePrice": "0", + # "amount": "0.123", + # "filledAmount": "0", + # "value": null, + # "filledValue": "0", + # "filledFee": "0", + # "status": "REJECTED", + # "timestamp": 1565165945588 + # }, + # "message": "Success" + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data) + + def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://doc.novadax.com/en-US/#get-order-history + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + request: dict = { + # 'symbol': market['id'], + # 'status': 'SUBMITTED,PROCESSING', # SUBMITTED, PROCESSING, PARTIAL_FILLED, CANCELING, FILLED, CANCELED, REJECTED + # 'fromId': '...', # order id to begin with + # 'toId': '...', # order id to end up with + # 'fromTimestamp': since, + # 'toTimestamp': self.milliseconds(), + # 'limit': limit, # default 100, max 100 + } + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if limit is not None: + request['limit'] = limit # default 100, max 100 + if since is not None: + request['fromTimestamp'] = since + response = self.privateGetOrdersList(self.extend(request, params)) + # + # { + # "code": "A10000", + # "data": [ + # { + # "id": "608695678650028032", + # "symbol": "BTC_BRL", + # "type": "MARKET", + # "side": "SELL", + # "price": null, + # "averagePrice": "0", + # "amount": "0.123", + # "filledAmount": "0", + # "value": null, + # "filledValue": "0", + # "filledFee": "0", + # "status": "REJECTED", + # "timestamp": 1565165958796 + # }, + # ], + # "message": "Success" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_orders(data, market, since, limit) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://doc.novadax.com/en-US/#get-order-history + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + request: dict = { + 'status': 'SUBMITTED,PROCESSING,PARTIAL_FILLED,CANCELING', + } + return self.fetch_orders(symbol, since, limit, self.extend(request, params)) + + def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://doc.novadax.com/en-US/#get-order-history + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + request: dict = { + 'status': 'FILLED,CANCELED,REJECTED', + } + return self.fetch_orders(symbol, since, limit, self.extend(request, params)) + + def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all the trades made from a single order + + https://doc.novadax.com/en-US/#get-order-match-details + + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + self.load_markets() + request: dict = { + 'id': id, + } + response = self.privateGetOrdersFill(self.extend(request, params)) + market = None + if symbol is not None: + market = self.market(symbol) + data = self.safe_value(response, 'data', []) + # + # { + # "code": "A10000", + # "data": [ + # { + # "id": "608717046691139584", + # "orderId": "608716957545402368", + # "symbol": "BTC_BRL", + # "side": "BUY", + # "amount": "0.0988", + # "price": "45514.76", + # "fee": "0.0000988 BTC", + # "feeAmount": "0.0000988", + # "feeCurrency": "BTC", + # "role": "MAKER", + # "timestamp": 1565171053345 + # }, + # ], + # "message": "Success" + # } + # + return self.parse_trades(data, market, since, limit) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'SUBMITTED': 'open', + 'PROCESSING': 'open', + 'PARTIAL_FILLED': 'open', + 'CANCELING': 'open', + 'FILLED': 'closed', + 'CANCELED': 'canceled', + 'REJECTED': 'rejected', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder, fetchOrders, fetchOrder + # + # { + # "amount": "0.001", + # "averagePrice": null, + # "filledAmount": "0", + # "filledFee": "0", + # "filledValue": "0", + # "id": "870613508008464384", + # "operator": "GTE", + # "price": "210000", + # "side": "BUY", + # "status": "SUBMITTED", + # "stopPrice": "211000", + # "symbol": "BTC_BRL", + # "timestamp": 1627612035528, + # "type": "STOP_LIMIT", + # "value": "210" + # } + # + # cancelOrder + # + # { + # "result": True + # } + # + id = self.safe_string(order, 'id') + amount = self.safe_string(order, 'amount') + price = self.safe_string(order, 'price') + cost = self.safe_string_2(order, 'filledValue', 'value') + type = self.safe_string_lower(order, 'type') + side = self.safe_string_lower(order, 'side') + status = self.parse_order_status(self.safe_string(order, 'status')) + timestamp = self.safe_integer(order, 'timestamp') + average = self.safe_string(order, 'averagePrice') + filled = self.safe_string(order, 'filledAmount') + fee = None + feeCost = self.safe_number(order, 'filledFee') + if feeCost is not None: + fee = { + 'cost': feeCost, + 'currency': None, + } + marketId = self.safe_string(order, 'symbol') + symbol = self.safe_symbol(marketId, market, '_') + return self.safe_order({ + 'id': id, + 'clientOrderId': None, + 'info': order, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'symbol': symbol, + 'type': type, + 'timeInForce': None, + 'postOnly': None, + 'side': side, + 'price': price, + 'triggerPrice': self.safe_number(order, 'stopPrice'), + 'amount': amount, + 'cost': cost, + 'average': average, + 'filled': filled, + 'remaining': None, + 'status': status, + 'fee': fee, + 'trades': None, + }, market) + + def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://doc.novadax.com/en-US/#get-sub-account-transfer + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account to transfer from + :param str toAccount: account to transfer to + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transfer structure ` + """ + self.load_markets() + currency = self.currency(code) + if fromAccount != 'main' and toAccount != 'main': + raise ExchangeError(self.id + ' transfer() supports transfers between main account and subaccounts only') + # master-transfer-in = from master account to subaccount + # master-transfer-out = from subaccount to master account + type = 'master-transfer-in' if (fromAccount == 'main') else 'master-transfer-out' + request: dict = { + 'transferAmount': self.currency_to_precision(code, amount), + 'currency': currency['id'], + 'subId': toAccount if (type == 'master-transfer-in') else fromAccount, + 'transferType': type, + } + response = self.privatePostAccountSubsTransfer(self.extend(request, params)) + # + # { + # "code":"A10000", + # "message":"Success", + # "data":40 + # } + # + transfer = self.parse_transfer(response, currency) + transferOptions = self.safe_value(self.options, 'transfer', {}) + fillResponseFromRequest = self.safe_bool(transferOptions, 'fillResponseFromRequest', True) + if fillResponseFromRequest: + transfer['fromAccount'] = fromAccount + transfer['toAccount'] = toAccount + transfer['amount'] = amount + return transfer + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + # + # { + # "code":"A10000", + # "message":"Success", + # "data":40 + # } + # + id = self.safe_string(transfer, 'data') + status = self.safe_string(transfer, 'message') + currencyCode = self.safe_currency_code(None, currency) + return { + 'info': transfer, + 'id': id, + 'amount': None, + 'currency': currencyCode, + 'fromAccount': None, + 'toAccount': None, + 'timestamp': None, + 'datetime': None, + 'status': status, + } + + def parse_transfer_status(self, status: Str) -> Str: + statuses: dict = { + 'SUCCESS': 'pending', + } + return self.safe_string(statuses, status, 'failed') + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://doc.novadax.com/en-US/#send-cryptocurrencies + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.load_markets() + currency = self.currency(code) + request: dict = { + 'code': currency['id'], + 'amount': self.currency_to_precision(code, amount), + 'wallet': address, + } + if tag is not None: + request['tag'] = tag + response = self.privatePostAccountWithdrawCoin(self.extend(request, params)) + # + # { + # "code":"A10000", + # "data": "DR123", + # "message":"Success" + # } + # + return self.parse_transaction(response, currency) + + def fetch_accounts(self, params={}) -> List[Account]: + """ + fetch all the accounts associated with a profile + + https://doc.novadax.com/en-US/#get-sub-account-list + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `account structures ` indexed by the account type + """ + response = self.privateGetAccountSubs(params) + # + # { + # "code": "A10000", + # "data": [ + # { + # "subId": "CA648856083527372800", + # "state": "Normal", + # "subAccount": "003", + # "subIdentify": "003" + # } + # ], + # "message": "Success" + # } + # + data = self.safe_value(response, 'data', []) + result = [] + for i in range(0, len(data)): + account = data[i] + accountId = self.safe_string(account, 'subId') + type = self.safe_string(account, 'subAccount') + result.append({ + 'id': accountId, + 'type': type, + 'currency': None, + 'info': account, + }) + return result + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://doc.novadax.com/en-US/#wallet-records-of-deposits-and-withdraws + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + request: dict = { + 'type': 'coin_in', + } + return self.fetch_deposits_withdrawals(code, since, limit, self.extend(request, params)) + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://doc.novadax.com/en-US/#wallet-records-of-deposits-and-withdraws + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + request: dict = { + 'type': 'coin_out', + } + return self.fetch_deposits_withdrawals(code, since, limit, self.extend(request, params)) + + def fetch_deposits_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch history of deposits and withdrawals + + https://doc.novadax.com/en-US/#wallet-records-of-deposits-and-withdraws + + :param str [code]: unified currency code for the currency of the deposit/withdrawals, default is None + :param int [since]: timestamp in ms of the earliest deposit/withdrawal, default is None + :param int [limit]: max number of deposit/withdrawals to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `transaction structure ` + """ + self.load_markets() + request: dict = { + # 'currency': currency['id'], + # 'type': 'coin_in', # 'coin_out' + # 'direct': 'asc', # 'desc' + # 'size': limit, # default 100 + # 'start': id, # offset id + } + currency = None + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + if limit is not None: + request['size'] = limit + response = self.privateGetWalletQueryDepositWithdraw(self.extend(request, params)) + # + # { + # "code": "A10000", + # "data": [ + # { + # "id": "DR562339304588709888", + # "type": "COIN_IN", + # "currency": "XLM", + # "chain": "XLM", + # "address": "GCUTK7KHPJC3ZQJ3OMWWFHAK2OXIBRD4LNZQRCCOVE7A2XOPP2K5PU5Q", + # "addressTag": "1000009", + # "amount": 1.0, + # "state": "SUCCESS", + # "txHash": "39210645748822f8d4ce673c7559aa6622e6e9cdd7073bc0fcae14b1edfda5f4", + # "createdAt": 1554113737000, + # "updatedAt": 1601371273000 + # } + # ], + # "message": "Success" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_transactions(data, currency, since, limit) + + def parse_transaction_status(self, status: Str): + # Pending the record is wait broadcast to chain + # x/M confirming the comfirming state of tx, the M is total confirmings needed + # SUCCESS the record is success full + # FAIL the record failed + parts = status.split(' ') + status = self.safe_string(parts, 1, status) + statuses: dict = { + 'Pending': 'pending', + 'confirming': 'pending', + 'SUCCESS': 'ok', + 'FAIL': 'failed', + } + return self.safe_string(statuses, status, status) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # withdraw + # + # { + # "code":"A10000", + # "data": "DR123", + # "message":"Success" + # } + # + # fetchDepositsWithdrawals + # + # { + # "id": "DR562339304588709888", + # "type": "COIN_IN", + # "currency": "XLM", + # "chain": "XLM", + # "address": "GCUTK7KHPJC3ZQJ3OMWWFHAK2OXIBRD4LNZQRCCOVE7A2XOPP2K5PU5Q", + # "addressTag": "1000009", + # "amount": 1.0, + # "state": "SUCCESS", + # "txHash": "39210645748822f8d4ce673c7559aa6622e6e9cdd7073bc0fcae14b1edfda5f4", + # "createdAt": 1554113737000, + # "updatedAt": 1601371273000 + # } + # + id = self.safe_string_2(transaction, 'id', 'data') + type = self.safe_string(transaction, 'type') + if type == 'COIN_IN': + type = 'deposit' + elif type == 'COIN_OUT': + type = 'withdraw' + amount = self.safe_number(transaction, 'amount') + address = self.safe_string(transaction, 'address') + tag = self.safe_string(transaction, 'addressTag') + txid = self.safe_string(transaction, 'txHash') + timestamp = self.safe_integer(transaction, 'createdAt') + updated = self.safe_integer(transaction, 'updatedAt') + currencyId = self.safe_string(transaction, 'currency') + code = self.safe_currency_code(currencyId, currency) + status = self.parse_transaction_status(self.safe_string(transaction, 'state')) + network = self.safe_string(transaction, 'chain') + return { + 'info': transaction, + 'id': id, + 'currency': code, + 'amount': amount, + 'network': network, + 'address': address, + 'addressTo': address, + 'addressFrom': None, + 'tag': tag, + 'tagTo': tag, + 'tagFrom': None, + 'status': status, + 'type': type, + 'updated': updated, + 'txid': txid, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'comment': None, + 'internal': None, + 'fee': { + 'currency': None, + 'cost': None, + 'rate': None, + }, + } + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://doc.novadax.com/en-US/#get-order-history + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + request: dict = { + # 'orderId': id, # Order ID, string + # 'symbol': market['id'], # The trading symbol, like BTC_BRL, string + # 'fromId': fromId, # Search fill id to begin with, string + # 'toId': toId, # Search fill id to end up with, string + # 'fromTimestamp': since, # Search order fill time to begin with, in milliseconds, string + # 'toTimestamp': self.milliseconds(), # Search order fill time to end up with, in milliseconds, string + # 'limit': limit, # The number of fills to return, default 100, max 100, string + # 'accountId': subaccountId, # Sub account ID, if not informed, the fills will be return under master account, string + } + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if limit is not None: + request['limit'] = limit + if since is not None: + request['fromTimestamp'] = since + response = self.privateGetOrdersFills(self.extend(request, params)) + # + # { + # "code": "A10000", + # "data": [ + # { + # "id": "608717046691139584", + # "orderId": "608716957545402368", + # "symbol": "BTC_BRL", + # "side": "BUY", + # "amount": "0.0988", + # "price": "45514.76", + # "fee": "0.0000988 BTC", + # "feeAmount": "0.0000988", + # "feeCurrency": "BTC", + # "role": "MAKER", + # "timestamp": 1565171053345 + # }, + # ], + # "message": "Success" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_trades(data, market, since, limit) + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + request = '/' + self.version + '/' + self.implode_params(path, params) + url = self.urls['api'][api] + request + query = self.omit(params, self.extract_params(path)) + if api == 'public': + if query: + url += '?' + self.urlencode(query) + elif api == 'private': + self.check_required_credentials() + timestamp = str(self.milliseconds()) + headers = { + 'X-Nova-Access-Key': self.apiKey, + 'X-Nova-Timestamp': timestamp, + } + queryString = None + if method == 'POST': + body = self.json(query) + queryString = self.hash(self.encode(body), 'md5') + headers['Content-Type'] = 'application/json' + else: + if query: + url += '?' + self.urlencode(query) + queryString = self.urlencode(self.keysort(query)) + auth = method + "\n" + request + "\n" + queryString + "\n" + timestamp # eslint-disable-line quotes + headers['X-Nova-Signature'] = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256) + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None + # + # {"code":"A10003","data":[],"message":"Authentication failed, Invalid accessKey."} + # + errorCode = self.safe_string(response, 'code') + if errorCode != 'A10000': + message = self.safe_string(response, 'message') + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + raise ExchangeError(feedback) # unknown message + return None diff --git a/ccxt/oceanex.py b/ccxt/oceanex.py new file mode 100644 index 0000000..0e712f6 --- /dev/null +++ b/ccxt/oceanex.py @@ -0,0 +1,1093 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.oceanex import ImplicitAPI +from ccxt.base.types import Any, Balances, Currency, DepositAddress, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, OrderBooks, Trade, TradingFees +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.decimal_to_precision import TICK_SIZE + + +class oceanex(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(oceanex, self).describe(), { + 'id': 'oceanex', + 'name': 'OceanEx', + 'countries': ['BS'], # Bahamas + 'version': 'v1', + 'rateLimit': 3000, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/58385970-794e2d80-8001-11e9-889c-0567cd79b78e.jpg', + 'api': { + 'rest': 'https://api.oceanex.pro', + }, + 'www': 'https://www.oceanex.pro.com', + 'doc': 'https://api.oceanex.pro/doc/v1', + 'referral': 'https://oceanex.pro/signup?referral=VE24QX', + }, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': None, # has but unimplemented + 'future': None, + 'option': None, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'cancelOrders': True, + 'createMarketOrder': True, + 'createOrder': True, + 'fetchBalance': True, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': False, + 'fetchDepositAddress': 'emulated', + 'fetchDepositAddresses': None, + 'fetchDepositAddressesByNetwork': True, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchMarkets': True, + 'fetchOHLCV': True, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrderBooks': True, + 'fetchOrders': True, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': True, + 'fetchTransactionFees': None, + }, + 'timeframes': { + '1m': '1', + '5m': '5', + '15m': '15', + '30m': '30', + '1h': '60', + '2h': '120', + '4h': '240', + '6h': '360', + '12h': '720', + '1d': '1440', + '3d': '4320', + '1w': '10080', + }, + 'api': { + 'public': { + 'get': [ + 'markets', + 'tickers/{pair}', + 'tickers_multi', + 'order_book', + 'order_book/multi', + 'fees/trading', + 'trades', + 'timestamp', + ], + 'post': [ + 'k', + ], + }, + 'private': { + 'get': [ + 'key', + 'members/me', + 'orders', + 'orders/filter', + ], + 'post': [ + 'orders', + 'orders/multi', + 'order/delete', + 'order/delete/multi', + 'orders/clear', + '/withdraws/special/new', + '/deposit_address', + '/deposit_addresses', + '/deposit_history', + '/withdraw_history', + ], + }, + }, + 'fees': { + 'trading': { + 'tierBased': False, + 'percentage': True, + 'maker': self.parse_number('0.001'), + 'taker': self.parse_number('0.001'), + }, + }, + 'commonCurrencies': { + 'PLA': 'Plair', + }, + 'precisionMode': TICK_SIZE, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, # todo + 'triggerDirection': True, # todo + 'triggerPriceType': None, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': False, + 'FOK': False, + 'PO': False, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': False, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': None, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 100, # todo: max unknown + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 100000, # todo + 'untilDays': 100000, # todo + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 100000, # todo + 'daysBackCanceled': 1, # todo + 'untilDays': 100000, # todo + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 100, + }, + }, + # todo implement swap + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'exceptions': { + 'codes': { + '-1': BadRequest, + '-2': BadRequest, + '1001': BadRequest, + '1004': ArgumentsRequired, + '1006': AuthenticationError, + '1008': AuthenticationError, + '1010': AuthenticationError, + '1011': PermissionDenied, + '2001': AuthenticationError, + '2002': InvalidOrder, + '2004': OrderNotFound, + '9003': PermissionDenied, + }, + 'exact': { + 'market does not have a valid value': BadRequest, + 'side does not have a valid value': BadRequest, + 'Account::AccountError: Cannot lock funds': InsufficientFunds, + 'The account does not exist': AuthenticationError, + }, + }, + }) + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for oceanex + + https://api.oceanex.pro/doc/v1/#markets-post + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + request: dict = {'show_details': True} + response = self.publicGetMarkets(self.extend(request, params)) + # + # { + # "id": "xtzusdt", + # "name": "XTZ/USDT", + # "ask_precision": "8", + # "bid_precision": "8", + # "enabled": True, + # "price_precision": "4", + # "amount_precision": "3", + # "usd_precision": "4", + # "minimum_trading_amount": "1.0" + # }, + # + markets = self.safe_value(response, 'data', []) + return self.parse_markets(markets) + + def parse_market(self, market: dict) -> Market: + id = self.safe_value(market, 'id') + name = self.safe_value(market, 'name') + baseId, quoteId = name.split('/') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + baseId = baseId.lower() + quoteId = quoteId.lower() + symbol = base + '/' + quote + return { + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': None, + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(self.parse_precision(self.safe_string(market, 'amount_precision'))), + 'price': self.parse_number(self.parse_precision(self.safe_string(market, 'price_precision'))), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': None, + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': self.safe_number(market, 'minimum_trading_amount'), + 'max': None, + }, + }, + 'created': None, + 'info': market, + } + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://api.oceanex.pro/doc/v1/#ticker-post + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + response = self.publicGetTickersPair(self.extend(request, params)) + # + # { + # "code":0, + # "message":"Operation successful", + # "data": { + # "at":1559431729, + # "ticker": { + # "buy":"0.0065", + # "sell":"0.00677", + # "low":"0.00677", + # "high":"0.00677", + # "last":"0.00677", + # "vol":"2000.0" + # } + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_ticker(data, market) + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://api.oceanex.pro/doc/v1/#multiple-tickers-post + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + symbols = self.market_symbols(symbols) + if symbols is None: + symbols = self.symbols + marketIds = self.market_ids(symbols) + request: dict = {'markets': marketIds} + response = self.publicGetTickersMulti(self.extend(request, params)) + # + # { + # "code":0, + # "message":"Operation successful", + # "data": { + # "at":1559431729, + # "ticker": { + # "buy":"0.0065", + # "sell":"0.00677", + # "low":"0.00677", + # "high":"0.00677", + # "last":"0.00677", + # "vol":"2000.0" + # } + # } + # } + # + data = self.safe_value(response, 'data', []) + result: dict = {} + for i in range(0, len(data)): + ticker = data[i] + marketId = self.safe_string(ticker, 'market') + market = self.safe_market(marketId) + symbol = market['symbol'] + result[symbol] = self.parse_ticker(ticker, market) + return self.filter_by_array_tickers(result, 'symbol', symbols) + + def parse_ticker(self, data, market: Market = None): + # + # { + # "at":1559431729, + # "ticker": { + # "buy":"0.0065", + # "sell":"0.00677", + # "low":"0.00677", + # "high":"0.00677", + # "last":"0.00677", + # "vol":"2000.0" + # } + # } + # + ticker = self.safe_value(data, 'ticker', {}) + timestamp = self.safe_timestamp(data, 'at') + symbol = self.safe_symbol(None, market) + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': self.safe_string(ticker, 'buy'), + 'bidVolume': None, + 'ask': self.safe_string(ticker, 'sell'), + 'askVolume': None, + 'vwap': None, + 'open': None, + 'close': self.safe_string(ticker, 'last'), + 'last': self.safe_string(ticker, 'last'), + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': self.safe_string(ticker, 'volume'), + 'quoteVolume': None, + 'info': ticker, + }, market) + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://api.oceanex.pro/doc/v1/#order-book-post + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + if limit is not None: + request['limit'] = limit + response = self.publicGetOrderBook(self.extend(request, params)) + # + # { + # "code":0, + # "message":"Operation successful", + # "data": { + # "timestamp":1559433057, + # "asks": [ + # ["100.0","20.0"], + # ["4.74","2000.0"], + # ["1.74","4000.0"], + # ], + # "bids":[ + # ["0.0065","5482873.4"], + # ["0.00649","4781956.2"], + # ["0.00648","2876006.8"], + # ], + # } + # } + # + orderbook = self.safe_value(response, 'data', {}) + timestamp = self.safe_timestamp(orderbook, 'timestamp') + return self.parse_order_book(orderbook, symbol, timestamp) + + def fetch_order_books(self, symbols: Strings = None, limit: Int = None, params={}) -> OrderBooks: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data for multiple markets + + https://api.oceanex.pro/doc/v1/#multiple-order-books-post + + :param str[]|None symbols: list of unified market symbols, all symbols fetched if None, default is None + :param int [limit]: max number of entries per orderbook to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `order book structures ` indexed by market symbol + """ + self.load_markets() + if symbols is None: + symbols = self.symbols + marketIds = self.market_ids(symbols) + request: dict = { + 'markets': marketIds, + } + if limit is not None: + request['limit'] = limit + response = self.publicGetOrderBookMulti(self.extend(request, params)) + # + # { + # "code":0, + # "message":"Operation successful", + # "data": [ + # { + # "timestamp":1559433057, + # "market": "bagvet", + # "asks": [ + # ["100.0","20.0"], + # ["4.74","2000.0"], + # ["1.74","4000.0"], + # ], + # "bids":[ + # ["0.0065","5482873.4"], + # ["0.00649","4781956.2"], + # ["0.00648","2876006.8"], + # ], + # }, + # ..., + # ], + # } + # + data = self.safe_value(response, 'data', []) + result: dict = {} + for i in range(0, len(data)): + orderbook = data[i] + marketId = self.safe_string(orderbook, 'market') + symbol = self.safe_symbol(marketId) + timestamp = self.safe_timestamp(orderbook, 'timestamp') + result[symbol] = self.parse_order_book(orderbook, symbol, timestamp) + return result + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://api.oceanex.pro/doc/v1/#trades-post + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + if limit is not None: + request['limit'] = min(limit, 1000) + response = self.publicGetTrades(self.extend(request, params)) + # + # { + # "code":0, + # "message":"Operation successful", + # "data": [ + # { + # "id":220247666, + # "price":"3098.62", + # "volume":"0.00196", + # "funds":"6.0732952", + # "market":"ethusdt", + # "created_at":"2022-04-19T19:03:15Z", + # "created_on":1650394994, + # "side":"bid" + # }, + # ] + # } + # + data = self.safe_list(response, 'data') + return self.parse_trades(data, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(public) + # + # { + # "id":220247666, + # "price":"3098.62", + # "volume":"0.00196", + # "funds":"6.0732952", + # "market":"ethusdt", + # "created_at":"2022-04-19T19:03:15Z", + # "created_on":1650394995, + # "side":"bid" + # } + # + side = self.safe_value(trade, 'side') + if side == 'bid': + side = 'buy' + elif side == 'ask': + side = 'sell' + marketId = self.safe_value(trade, 'market') + symbol = self.safe_symbol(marketId, market) + timestamp = self.safe_timestamp(trade, 'created_on') + if timestamp is None: + timestamp = self.parse8601(self.safe_string(trade, 'created_at')) + priceString = self.safe_string(trade, 'price') + amountString = self.safe_string(trade, 'volume') + return self.safe_trade({ + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'id': self.safe_string(trade, 'id'), + 'order': None, + 'type': 'limit', + 'takerOrMaker': None, + 'side': side, + 'price': priceString, + 'amount': amountString, + 'cost': None, + 'fee': None, + }, market) + + def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://api.oceanex.pro/doc/v1/#api-server-time-post + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = self.publicGetTimestamp(params) + # + # {"code":0,"message":"Operation successful","data":1559433420} + # + return self.safe_timestamp(response, 'data') + + def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://api.oceanex.pro/doc/v1/#trading-fees-post + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + response = self.publicGetFeesTrading(params) + data = self.safe_value(response, 'data', []) + result: dict = {} + for i in range(0, len(data)): + group = data[i] + maker = self.safe_value(group, 'ask_fee', {}) + taker = self.safe_value(group, 'bid_fee', {}) + marketId = self.safe_string(group, 'market') + symbol = self.safe_symbol(marketId) + result[symbol] = { + 'info': group, + 'symbol': symbol, + 'maker': self.safe_number(maker, 'value'), + 'taker': self.safe_number(taker, 'value'), + 'percentage': True, + } + return result + + def fetch_key(self, params={}): + response = self.privateGetKey(params) + return self.safe_value(response, 'data') + + def parse_balance(self, response) -> Balances: + data = self.safe_value(response, 'data') + balances = self.safe_value(data, 'accounts', []) + result: dict = {'info': response} + for i in range(0, len(balances)): + balance = balances[i] + currencyId = self.safe_value(balance, 'currency') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(balance, 'balance') + account['used'] = self.safe_string(balance, 'locked') + result[code] = account + return self.safe_balance(result) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://api.oceanex.pro/doc/v1/#account-info-post + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + response = self.privateGetMembersMe(params) + return self.parse_balance(response) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://api.oceanex.pro/doc/v1/#new-order-post + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + 'side': side, + 'ord_type': type, + 'volume': self.amount_to_precision(symbol, amount), + } + if type == 'limit': + request['price'] = self.price_to_precision(symbol, price) + response = self.privatePostOrders(self.extend(request, params)) + data = self.safe_dict(response, 'data') + return self.parse_order(data, market) + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://api.oceanex.pro/doc/v1/#order-status-get + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + ids = [id] + request: dict = {'ids': ids} + response = self.privateGetOrders(self.extend(request, params)) + data = self.safe_value(response, 'data') + dataLength = len(data) + if data is None: + raise OrderNotFound(self.id + ' could not found matching order') + if isinstance(id, list): + orders = self.parse_orders(data, market) + return orders[0] + if dataLength == 0: + raise OrderNotFound(self.id + ' could not found matching order') + return self.parse_order(data[0], market) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://api.oceanex.pro/doc/v1/#order-status-get + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + request: dict = { + 'states': ['wait'], + } + return self.fetch_orders(symbol, since, limit, self.extend(request, params)) + + def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://api.oceanex.pro/doc/v1/#order-status-get + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + request: dict = { + 'states': ['done', 'cancel'], + } + return self.fetch_orders(symbol, since, limit, self.extend(request, params)) + + def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://api.oceanex.pro/doc/v1/#order-status-with-filters-post + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrders() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + states = self.safe_value(params, 'states', ['wait', 'done', 'cancel']) + query = self.omit(params, 'states') + request: dict = { + 'market': market['id'], + 'states': states, + 'need_price': 'True', + } + if limit is not None: + request['limit'] = limit + response = self.privateGetOrdersFilter(self.extend(request, query)) + data = self.safe_value(response, 'data', []) + result = [] + for i in range(0, len(data)): + orders = self.safe_value(data[i], 'orders', []) + status = self.parse_order_status(self.safe_value(data[i], 'state')) + parsedOrders = self.parse_orders(orders, market, since, limit, {'status': status}) + result = self.array_concat(result, parsedOrders) + return result + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # [ + # 1559232000, + # 8889.22, + # 9028.52, + # 8889.22, + # 9028.52 + # 0.3121 + # ] + return [ + self.safe_timestamp(ohlcv, 0), + self.safe_number(ohlcv, 1), + self.safe_number(ohlcv, 2), + self.safe_number(ohlcv, 3), + self.safe_number(ohlcv, 4), + self.safe_number(ohlcv, 5), + ] + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://api.oceanex.pro/doc/v1/#k-line-post + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + 'period': self.safe_string(self.timeframes, timeframe, timeframe), + } + if since is not None: + request['timestamp'] = since + if limit is not None: + request['limit'] = min(limit, 10000) + response = self.publicPostK(self.extend(request, params)) + ohlcvs = self.safe_list(response, 'data', []) + return self.parse_ohlcvs(ohlcvs, market, timeframe, since, limit) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # { + # "created_at": "2019-01-18T00:38:18Z", + # "trades_count": 0, + # "remaining_volume": "0.2", + # "price": "1001.0", + # "created_on": "1547771898", + # "side": "buy", + # "volume": "0.2", + # "state": "wait", + # "ord_type": "limit", + # "avg_price": "0.0", + # "executed_volume": "0.0", + # "id": 473797, + # "market": "veteth" + # } + # + status = self.parse_order_status(self.safe_value(order, 'state')) + marketId = self.safe_string_2(order, 'market', 'market_id') + symbol = self.safe_symbol(marketId, market) + timestamp = self.safe_timestamp(order, 'created_on') + if timestamp is None: + timestamp = self.parse8601(self.safe_string(order, 'created_at')) + price = self.safe_string(order, 'price') + average = self.safe_string(order, 'avg_price') + amount = self.safe_string(order, 'volume') + remaining = self.safe_string(order, 'remaining_volume') + filled = self.safe_string(order, 'executed_volume') + return self.safe_order({ + 'info': order, + 'id': self.safe_string(order, 'id'), + 'clientOrderId': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'symbol': symbol, + 'type': self.safe_value(order, 'ord_type'), + 'timeInForce': None, + 'postOnly': None, + 'side': self.safe_value(order, 'side'), + 'price': price, + 'triggerPrice': None, + 'average': average, + 'amount': amount, + 'remaining': remaining, + 'filled': filled, + 'status': status, + 'cost': None, + 'trades': None, + 'fee': None, + }, market) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'wait': 'open', + 'done': 'closed', + 'cancel': 'canceled', + } + return self.safe_string(statuses, status, status) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://api.oceanex.pro/doc/v1/#cancel-order-post + + :param str id: order id + :param str symbol: not used by oceanex cancelOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + response = self.privatePostOrderDelete(self.extend({'id': id}, params)) + data = self.safe_dict(response, 'data') + return self.parse_order(data) + + def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): + """ + cancel multiple orders + + https://api.oceanex.pro/doc/v1/#cancel-multiple-orders-post + + :param str[] ids: order ids + :param str symbol: not used by oceanex cancelOrders() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an list of `order structures ` + """ + self.load_markets() + response = self.privatePostOrderDeleteMulti(self.extend({'ids': ids}, params)) + data = self.safe_list(response, 'data') + return self.parse_orders(data) + + def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders + + https://api.oceanex.pro/doc/v1/#cancel-all-orders-post + + :param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + self.load_markets() + response = self.privatePostOrdersClear(params) + data = self.safe_list(response, 'data') + return self.parse_orders(data) + + def fetch_deposit_addresses_by_network(self, code: str, params={}) -> List[DepositAddress]: + """ + fetch the deposit addresses for a currency associated with self account + + https://api.oceanex.pro/doc/v1/#deposit-addresses-post + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary `address structures `, indexed by the network + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + } + response = self.privatePostDepositAddresses(self.extend(request, params)) + # + # { + # code: '0', + # message: 'Operation successful', + # data: { + # data: { + # currency_id: 'usdt', + # display_name: 'USDT', + # num_of_resources: '3', + # resources: [ + # { + # chain_name: 'TRC20', + # currency_id: 'usdt', + # address: 'TPcS7VgKMFmpRrWY82GbJzDeMnemWxEbpg', + # memo: '', + # deposit_status: 'enabled' + # }, + # ... + # ] + # } + # } + # } + # + data = self.safe_dict(response, 'data', {}) + data2 = self.safe_dict(data, 'data', {}) + resources = self.safe_list(data2, 'resources', []) + result = {} + for i in range(0, len(resources)): + resource = resources[i] + enabled = self.safe_string(resource, 'deposit_status') + if enabled == 'enabled': + parsedAddress = self.parse_deposit_address(resource, currency) + result[parsedAddress['currency']] = parsedAddress + return result + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + # + # { + # chain_name: 'TRC20', + # currency_id: 'usdt', + # address: 'TPcS7VgKMFmpRrWY82GbJzDeMnemWxEbpg', + # memo: '', + # deposit_status: 'enabled' + # } + # + address = self.safe_string(depositAddress, 'address') + self.check_address(address) + currencyId = self.safe_string(depositAddress, 'currency_id') + networkId = self.safe_string(depositAddress, 'chain_name') + return { + 'info': depositAddress, + 'currency': self.safe_currency_code(currencyId, currency), + 'network': self.network_id_to_code(networkId), + 'address': address, + 'tag': self.safe_string(depositAddress, 'memo'), + } + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = self.urls['api']['rest'] + '/' + self.version + '/' + self.implode_params(path, params) + query = self.omit(params, self.extract_params(path)) + if api == 'public': + if path == 'tickers_multi' or path == 'order_book/multi': + request = '?' + markets = self.safe_value(params, 'markets') + for i in range(0, len(markets)): + request += 'markets[]=' + markets[i] + '&' + limit = self.safe_value(params, 'limit') + if limit is not None: + request += 'limit=' + limit + url += request + elif query: + url += '?' + self.urlencode(query) + elif api == 'private': + self.check_required_credentials() + request: dict = { + 'uid': self.apiKey, + 'data': query, + } + # to set the private key: + # fs = require('fs') + # exchange.secret = fs.readFileSync('oceanex.pem', 'utf8') + jwt_token = self.jwt(request, self.encode(self.secret), 'sha256', True) + url += '?user_jwt=' + jwt_token + headers = {'Content-Type': 'application/json'} + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + # + # {"code":1011,"message":"This IP 'x.x.x.x' is not allowed","data":{}} + # + if response is None: + return None + errorCode = self.safe_string(response, 'code') + message = self.safe_string(response, 'message') + if (errorCode is not None) and (errorCode != '0'): + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['codes'], errorCode, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) + raise ExchangeError(feedback) + return None diff --git a/ccxt/okx.py b/ccxt/okx.py new file mode 100644 index 0000000..1b94aeb --- /dev/null +++ b/ccxt/okx.py @@ -0,0 +1,8650 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.okx import ImplicitAPI +import hashlib +from ccxt.base.types import Account, Any, Balances, BorrowInterest, Conversion, CrossBorrowRate, CrossBorrowRates, Currencies, Currency, DepositAddress, Greeks, Int, LedgerEntry, Leverage, LeverageTier, LongShortRatio, MarginModification, Market, Num, Option, OptionChain, Order, OrderBook, OrderRequest, CancellationRequest, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, FundingRates, OpenInterests, Trade, TradingFeeInterface, Transaction, MarketInterface, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import AccountNotEnabled +from ccxt.base.errors import AccountSuspended +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import OperationRejected +from ccxt.base.errors import ManualInteractionNeeded +from ccxt.base.errors import RestrictedLocation +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidAddress +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import ContractUnavailable +from ccxt.base.errors import NotSupported +from ccxt.base.errors import NetworkError +from ccxt.base.errors import DDoSProtection +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.errors import OnMaintenance +from ccxt.base.errors import InvalidNonce +from ccxt.base.errors import RequestTimeout +from ccxt.base.errors import CancelPending +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class okx(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(okx, self).describe(), { + 'id': 'okx', + 'name': 'OKX', + 'countries': ['CN', 'US'], + 'version': 'v5', + 'rateLimit': 100 * 1.10, # 10% tolerance because of #26973 + 'pro': True, + 'certified': True, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': True, + 'swap': True, + 'future': True, + 'option': True, + 'addMargin': True, + 'cancelAllOrders': False, + 'cancelAllOrdersAfter': True, + 'cancelOrder': True, + 'cancelOrders': True, + 'cancelOrdersForSymbols': True, + 'closeAllPositions': False, + 'closePosition': True, + 'createConvertTrade': True, + 'createDepositAddress': False, + 'createMarketBuyOrderWithCost': True, + 'createMarketSellOrderWithCost': True, + 'createOrder': True, + 'createOrders': True, + 'createOrderWithTakeProfitAndStopLoss': True, + 'createPostOnlyOrder': True, + 'createReduceOnlyOrder': True, + 'createStopLimitOrder': True, + 'createStopLossOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'createTakeProfitOrder': True, + 'createTrailingPercentOrder': True, + 'createTriggerOrder': True, + 'editOrder': True, + 'fetchAccounts': True, + 'fetchAllGreeks': True, + 'fetchBalance': True, + 'fetchBidsAsks': None, + 'fetchBorrowInterest': True, + 'fetchBorrowRateHistories': True, + 'fetchBorrowRateHistory': True, + 'fetchCanceledOrders': True, + 'fetchClosedOrder': None, + 'fetchClosedOrders': True, + 'fetchConvertCurrencies': True, + 'fetchConvertQuote': True, + 'fetchConvertTrade': True, + 'fetchConvertTradeHistory': True, + 'fetchCrossBorrowRate': True, + 'fetchCrossBorrowRates': True, + 'fetchCurrencies': True, + 'fetchDeposit': True, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': True, + 'fetchDeposits': True, + 'fetchDepositsWithdrawals': False, + 'fetchDepositWithdrawFee': 'emulated', + 'fetchDepositWithdrawFees': True, + 'fetchFundingHistory': True, + 'fetchFundingInterval': True, + 'fetchFundingIntervals': False, + 'fetchFundingRate': True, + 'fetchFundingRateHistory': True, + 'fetchFundingRates': True, + 'fetchGreeks': True, + 'fetchIndexOHLCV': True, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchL3OrderBook': False, + 'fetchLedger': True, + 'fetchLedgerEntry': None, + 'fetchLeverage': True, + 'fetchLeverageTiers': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': True, + 'fetchMarginAdjustmentHistory': True, + 'fetchMarketLeverageTiers': True, + 'fetchMarkets': True, + 'fetchMarkOHLCV': True, + 'fetchMarkPrice': True, + 'fetchMarkPrices': True, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': True, + 'fetchOpenInterestHistory': True, + 'fetchOpenInterests': True, + 'fetchOpenOrder': None, + 'fetchOpenOrders': True, + 'fetchOption': True, + 'fetchOptionChain': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrderBooks': False, + 'fetchOrders': False, + 'fetchOrderTrades': True, + 'fetchPosition': True, + 'fetchPositionHistory': 'emulated', + 'fetchPositions': True, + 'fetchPositionsForSymbol': True, + 'fetchPositionsHistory': True, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': True, + 'fetchStatus': True, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': True, + 'fetchTradingFees': False, + 'fetchTradingLimits': False, + 'fetchTransactionFee': False, + 'fetchTransactionFees': False, + 'fetchTransactions': False, + 'fetchTransfer': True, + 'fetchTransfers': True, + 'fetchUnderlyingAssets': True, + 'fetchVolatilityHistory': False, + 'fetchWithdrawal': True, + 'fetchWithdrawals': True, + 'fetchWithdrawalWhitelist': False, + 'reduceMargin': True, + 'repayCrossMargin': True, + 'sandbox': True, + 'setLeverage': True, + 'setMargin': False, + 'setMarginMode': True, + 'setPositionMode': True, + 'signIn': False, + 'transfer': True, + 'withdraw': True, + }, + 'timeframes': { + '1m': '1m', + '3m': '3m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1H', + '2h': '2H', + '4h': '4H', + '6h': '6H', + '12h': '12H', + '1d': '1D', + '1w': '1W', + '1M': '1M', + '3M': '3M', + }, + 'hostname': 'www.okx.com', # or aws.okx.com + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/152485636-38b19e4a-bece-4dec-979a-5982859ffc04.jpg', + 'api': { + 'rest': 'https://{hostname}', + }, + 'www': 'https://www.okx.com', + 'doc': 'https://www.okx.com/docs-v5/en/', + 'fees': 'https://www.okx.com/pages/products/fees.html', + 'referral': { + # old reflink 0% discount https://www.okx.com/join/1888677 + # new reflink 20% discount https://www.okx.com/join/CCXT2023 + 'url': 'https://www.okx.com/join/CCXTCOM', + 'discount': 0.2, + }, + 'test': { + 'rest': 'https://{hostname}', + }, + }, + 'api': { + 'public': { + 'get': { + 'market/books-full': 2, + 'market/tickers': 1, + 'market/ticker': 1, + 'market/index-tickers': 1, + 'market/books': 1 / 2, + 'market/books-lite': 5 / 3, + 'market/candles': 1 / 2, + 'market/history-candles': 1, + 'market/index-candles': 1, + 'market/history-index-candles': 2, + 'market/mark-price-candles': 1, + 'market/history-mark-price-candles': 2, + 'market/trades': 1 / 5, + 'market/history-trades': 2, + 'market/option/instrument-family-trades': 1, + 'market/platform-24-volume': 10, + 'market/open-oracle': 50, + 'market/exchange-rate': 20, + 'market/index-components': 1, + 'public/market-data-history': 4, + 'public/economic-calendar': 50, + 'market/block-tickers': 1, + 'market/block-ticker': 1, + 'public/block-trades': 1, + 'public/instruments': 1, + 'public/delivery-exercise-history': 1 / 2, + 'public/open-interest': 1, + 'public/funding-rate': 1, + 'public/funding-rate-history': 1, + 'public/price-limit': 1, + 'public/opt-summary': 1, + 'public/estimated-price': 2, + 'public/discount-rate-interest-free-quota': 10, + 'public/time': 2, + 'public/mark-price': 2, + 'public/position-tiers': 2, + 'public/interest-rate-loan-quota': 10, + 'public/vip-interest-rate-loan-quota': 10, + 'public/underlying': 1, + 'public/insurance-fund': 2, + 'public/convert-contract-coin': 2, + 'public/option-trades': 1, + 'public/instrument-tick-bands': 4, + 'rubik/stat/trading-data/support-coin': 4, + 'rubik/stat/taker-volume': 4, + 'rubik/stat/margin/loan-ratio': 4, + # long/short + 'rubik/stat/contracts/long-short-account-ratio': 4, + 'rubik/stat/contracts/long-short-account-ratio-contract': 4, + 'rubik/stat/contracts/open-interest-volume': 4, + 'rubik/stat/option/open-interest-volume': 4, + # put/call + 'rubik/stat/option/open-interest-volume-ratio': 4, + 'rubik/stat/option/open-interest-volume-expiry': 4, + 'rubik/stat/option/open-interest-volume-strike': 4, + 'rubik/stat/option/taker-block-volume': 4, + 'system/status': 50, + # public api + 'sprd/spreads': 1, + 'sprd/books': 1 / 2, + 'sprd/ticker': 1, + 'sprd/public-trades': 1 / 5, + 'market/sprd-ticker': 2, + 'market/sprd-candles': 2, + 'market/sprd-history-candles': 2, + 'tradingBot/grid/ai-param': 1, + 'tradingBot/grid/min-investment': 1, + 'tradingBot/public/rsi-back-testing': 1, + 'asset/exchange-list': 5 / 3, + 'finance/staking-defi/eth/apy-history': 5 / 3, + 'finance/staking-defi/sol/apy-history': 5 / 3, + 'finance/savings/lending-rate-summary': 5 / 3, + 'finance/savings/lending-rate-history': 5 / 3, + 'finance/fixed-loan/lending-offers': 10 / 3, + 'finance/fixed-loan/lending-apy-history': 10 / 3, + 'finance/fixed-loan/pending-lending-volume': 10 / 3, + # public broker + 'finance/sfp/dcd/products': 2 / 3, + # copytrading + 'copytrading/public-lead-traders': 4, + 'copytrading/public-weekly-pnl': 4, + 'copytrading/public-stats': 4, + 'copytrading/public-preference-currency': 4, + 'copytrading/public-current-subpositions': 4, + 'copytrading/public-subpositions-history': 4, + 'support/announcements-types': 20, + }, + }, + 'private': { + 'get': { + # rfq + 'rfq/counterparties': 4, + 'rfq/maker-instrument-settings': 4, + 'rfq/mmp-config': 4, + 'rfq/rfqs': 10, + 'rfq/quotes': 10, + 'rfq/trades': 4, + 'rfq/public-trades': 4, + # sprd + 'sprd/order': 1 / 3, + 'sprd/orders-pending': 1 / 3, + 'sprd/orders-history': 1 / 2, + 'sprd/orders-history-archive': 1 / 2, + 'sprd/trades': 1 / 3, + # trade + 'trade/order': 1 / 3, + 'trade/orders-pending': 1 / 3, + 'trade/orders-history': 1 / 2, + 'trade/orders-history-archive': 1, + 'trade/fills': 1 / 3, + 'trade/fills-history': 2.2, + 'trade/fills-archive': 2, + 'trade/order-algo': 1, + 'trade/orders-algo-pending': 1, + 'trade/orders-algo-history': 1, + 'trade/easy-convert-currency-list': 20, + 'trade/easy-convert-history': 20, + 'trade/one-click-repay-currency-list': 20, + 'trade/one-click-repay-currency-list-v2': 20, + 'trade/one-click-repay-history': 20, + 'trade/one-click-repay-history-v2': 20, + 'trade/account-rate-limit': 1, + # asset + 'asset/currencies': 5 / 3, + 'asset/balances': 5 / 3, + 'asset/non-tradable-assets': 5 / 3, + 'asset/asset-valuation': 10, + 'asset/transfer-state': 10, + 'asset/bills': 5 / 3, + 'asset/deposit-lightning': 5, + 'asset/deposit-address': 5 / 3, + 'asset/deposit-history': 5 / 3, + 'asset/withdrawal-history': 5 / 3, + 'asset/deposit-withdraw-status': 20, + 'asset/convert/currencies': 5 / 3, + 'asset/convert/currency-pair': 5 / 3, + 'asset/convert/history': 5 / 3, + 'asset/monthly-statement': 2, + # account + 'account/instruments': 1, + 'account/balance': 2, + 'account/positions': 2, + 'account/positions-history': 100, + 'account/account-position-risk': 2, + 'account/bills': 5 / 3, + 'account/bills-archive': 5 / 3, + 'account/bills-history-archive': 2, + 'account/config': 4, + 'account/max-size': 1, + 'account/max-avail-size': 1, + 'account/leverage-info': 1, + 'account/adjust-leverage-info': 4, + 'account/max-loan': 1, + 'account/trade-fee': 4, + 'account/interest-accrued': 4, + 'account/interest-rate': 4, + 'account/max-withdrawal': 1, + 'account/risk-state': 2, + 'account/quick-margin-borrow-repay-history': 4, + 'account/borrow-repay-history': 4, + 'account/vip-interest-accrued': 4, + 'account/vip-interest-deducted': 4, + 'account/vip-loan-order-list': 4, + 'account/vip-loan-order-detail': 4, + 'account/interest-limits': 4, + 'account/greeks': 2, + 'account/position-tiers': 2, + 'account/mmp-config': 4, + 'account/fixed-loan/borrowing-limit': 4, + 'account/fixed-loan/borrowing-quote': 5, + 'account/fixed-loan/borrowing-orders-list': 5, + 'account/spot-manual-borrow-repay': 30, + 'account/set-auto-repay': 4, + 'account/spot-borrow-repay-history': 4, + 'account/move-positions-history': 10, + # subaccount + 'users/subaccount/list': 10, + 'account/subaccount/balances': 10 / 3, + 'asset/subaccount/balances': 10 / 3, + 'account/subaccount/max-withdrawal': 1, + 'asset/subaccount/bills': 5 / 3, + 'asset/subaccount/managed-subaccount-bills': 5 / 3, + 'users/entrust-subaccount-list': 10, + 'account/subaccount/interest-limits': 4, + 'users/subaccount/apikey': 10, + # grid trading + 'tradingBot/grid/orders-algo-pending': 1, + 'tradingBot/grid/orders-algo-history': 1, + 'tradingBot/grid/orders-algo-details': 1, + 'tradingBot/grid/sub-orders': 1, + 'tradingBot/grid/positions': 1, + 'tradingBot/grid/ai-param': 1, + 'tradingBot/signal/signals': 1, + 'tradingBot/signal/orders-algo-details': 1, + 'tradingBot/signal/orders-algo-history': 1, + 'tradingBot/signal/positions': 1, + 'tradingBot/signal/positions-history': 1, + 'tradingBot/signal/sub-orders': 1, + 'tradingBot/signal/event-history': 1, + 'tradingBot/recurring/orders-algo-pending': 1, + 'tradingBot/recurring/orders-algo-history': 1, + 'tradingBot/recurring/orders-algo-details': 1, + 'tradingBot/recurring/sub-orders': 1, + # earn + 'finance/savings/balance': 5 / 3, + 'finance/savings/lending-history': 5 / 3, + 'finance/staking-defi/offers': 10 / 3, + 'finance/staking-defi/orders-active': 10 / 3, + 'finance/staking-defi/orders-history': 10 / 3, + # eth staking + 'finance/staking-defi/eth/balance': 5 / 3, + 'finance/staking-defi/eth/purchase-redeem-history': 5 / 3, + 'finance/staking-defi/eth/product-info': 3, + 'finance/staking-defi/sol/balance': 5 / 3, + 'finance/staking-defi/sol/purchase-redeem-history': 5 / 3, + # copytrading + 'copytrading/current-subpositions': 1, + 'copytrading/subpositions-history': 1, + 'copytrading/instruments': 4, + 'copytrading/profit-sharing-details': 4, + 'copytrading/total-profit-sharing': 4, + 'copytrading/unrealized-profit-sharing-details': 4, + 'copytrading/copy-settings': 4, + 'copytrading/batch-leverage-info': 4, + 'copytrading/current-lead-traders': 4, + 'copytrading/lead-traders-history': 4, + # broker + 'broker/nd/info': 10, + 'broker/nd/subaccount-info': 10, + 'broker/nd/subaccount/apikey': 10, + 'asset/broker/nd/subaccount-deposit-address': 5 / 3, + 'asset/broker/nd/subaccount-deposit-history': 4, + 'asset/broker/nd/subaccount-withdrawal-history': 4, + 'broker/nd/rebate-daily': 100, + 'broker/nd/rebate-per-orders': 300, + 'finance/sfp/dcd/order': 2, + 'finance/sfp/dcd/orders': 2, + 'broker/fd/rebate-per-orders': 300, + 'broker/fd/if-rebate': 5, + # affiliate + 'affiliate/invitee/detail': 1, + 'users/partner/if-rebate': 1, + 'support/announcements': 4, + }, + 'post': { + # rfq + 'rfq/create-rfq': 4, + 'rfq/cancel-rfq': 4, + 'rfq/cancel-batch-rfqs': 10, + 'rfq/cancel-all-rfqs': 10, + 'rfq/execute-quote': 15, + 'rfq/maker-instrument-settings': 4, + 'rfq/mmp-reset': 4, + 'rfq/mmp-config': 100, + 'rfq/create-quote': 0.4, + 'rfq/cancel-quote': 0.4, + 'rfq/cancel-batch-quotes': 10, + 'rfq/cancel-all-quotes': 10, + # sprd + 'sprd/order': 1, + 'sprd/cancel-order': 1, + 'sprd/mass-cancel': 1, + 'sprd/amend-order': 1, + 'sprd/cancel-all-after': 10, + # trade + 'trade/order': 1 / 3, + 'trade/batch-orders': 1 / 15, + 'trade/cancel-order': 1 / 3, + 'trade/cancel-batch-orders': 1 / 15, + 'trade/amend-order': 1 / 3, + 'trade/amend-batch-orders': 1 / 150, + 'trade/close-position': 1, + 'trade/fills-archive': 172800, # 5 req per day = 5/24/60/60 => 10/5*24*60*60=172800 + 'trade/order-algo': 1, + 'trade/cancel-algos': 1, + 'trade/amend-algos': 1, + 'trade/cancel-advance-algos': 1, + 'trade/easy-convert': 20, + 'trade/one-click-repay': 20, + 'trade/one-click-repay-v2': 20, + 'trade/mass-cancel': 4, + 'trade/cancel-all-after': 10, + # asset + 'asset/transfer': 10, + 'asset/withdrawal': 5 / 3, + 'asset/withdrawal-lightning': 5, + 'asset/cancel-withdrawal': 5 / 3, + 'asset/convert-dust-assets': 10, + 'asset/convert/estimate-quote': 1, + 'asset/convert/trade': 1, + 'asset/monthly-statement': 1, + # account + 'account/set-position-mode': 4, + 'account/set-leverage': 1, + 'account/position/margin-balance': 1, + 'account/set-greeks': 4, + 'account/set-isolated-mode': 4, + 'account/quick-margin-borrow-repay': 4, + 'account/borrow-repay': 5 / 3, + 'account/simulated_margin': 10, + 'account/position-builder': 10, + 'account/set-riskOffset-type': 2, + 'account/activate-option': 4, + 'account/set-auto-loan': 4, + 'account/set-account-level': 4, + 'account/mmp-reset': 4, + 'account/mmp-config': 100, + 'account/fixed-loan/borrowing-order': 5, + 'account/fixed-loan/amend-borrowing-order': 5, + 'account/fixed-loan/manual-reborrow': 5, + 'account/fixed-loan/repay-borrowing-order': 5, + 'account/bills-history-archive': 72000, # 12 req/day + 'account/move-positions': 10, + 'account/set-settle-currency': 1, + # subaccount + 'users/subaccount/modify-apikey': 10, + 'asset/subaccount/transfer': 10, + 'users/subaccount/set-transfer-out': 10, + 'account/subaccount/set-loan-allocation': 4, + 'users/subaccount/create-subaccount': 10, + 'users/subaccount/subaccount-apikey': 10, + 'users/subaccount/delete-apikey': 10, + # grid trading + 'tradingBot/grid/order-algo': 1, + 'tradingBot/grid/amend-order-algo': 1, + 'tradingBot/grid/stop-order-algo': 1, + 'tradingBot/grid/close-position': 1, + 'tradingBot/grid/cancel-close-order': 1, + 'tradingBot/grid/order-instant-trigger': 1, + 'tradingBot/grid/withdraw-income': 1, + 'tradingBot/grid/compute-margin-balance': 1, + 'tradingBot/grid/margin-balance': 1, + 'tradingBot/grid/min-investment': 1, + 'tradingBot/grid/adjust-investment': 1, + 'tradingBot/signal/create-signal': 1, + 'tradingBot/signal/order-algo': 1, + 'tradingBot/signal/stop-order-algo': 1, + 'tradingBot/signal/margin-balance': 1, + 'tradingBot/signal/amendTPSL': 1, + 'tradingBot/signal/set-instruments': 1, + 'tradingBot/signal/close-position': 1, + 'tradingBot/signal/sub-order': 1, + 'tradingBot/signal/cancel-sub-order': 1, + 'tradingBot/recurring/order-algo': 1, + 'tradingBot/recurring/amend-order-algo': 1, + 'tradingBot/recurring/stop-order-algo': 1, + # earn + 'finance/savings/purchase-redempt': 5 / 3, + 'finance/savings/set-lending-rate': 5 / 3, + 'finance/staking-defi/purchase': 3, + 'finance/staking-defi/redeem': 3, + 'finance/staking-defi/cancel': 3, + # eth staking + 'finance/staking-defi/eth/purchase': 5, + 'finance/staking-defi/eth/redeem': 5, + 'finance/staking-defi/sol/purchase': 5, + 'finance/staking-defi/sol/redeem': 5, + # copytrading + 'copytrading/algo-order': 1, + 'copytrading/close-subposition': 1, + 'copytrading/set-instruments': 4, + 'copytrading/first-copy-settings': 4, + 'copytrading/amend-copy-settings': 4, + 'copytrading/stop-copy-trading': 4, + 'copytrading/batch-set-leverage': 4, + # broker + 'broker/nd/create-subaccount': 0.25, + 'broker/nd/delete-subaccount': 1, + 'broker/nd/subaccount/apikey': 0.25, + 'broker/nd/subaccount/modify-apikey': 1, + 'broker/nd/subaccount/delete-apikey': 1, + 'broker/nd/set-subaccount-level': 4, + 'broker/nd/set-subaccount-fee-rate': 4, + 'broker/nd/set-subaccount-assets': 0.25, + 'asset/broker/nd/subaccount-deposit-address': 1, + 'asset/broker/nd/modify-subaccount-deposit-address': 5 / 3, + 'broker/nd/rebate-per-orders': 36000, + 'finance/sfp/dcd/quote': 10, + 'finance/sfp/dcd/order': 10, + 'broker/nd/report-subaccount-ip': 0.25, + 'broker/fd/rebate-per-orders': 36000, + }, + }, + }, + 'fees': { + 'trading': { + 'taker': self.parse_number('0.0015'), + 'maker': self.parse_number('0.0010'), + }, + 'spot': { + 'taker': self.parse_number('0.0015'), + 'maker': self.parse_number('0.0010'), + }, + 'future': { + 'taker': self.parse_number('0.0005'), + 'maker': self.parse_number('0.0002'), + }, + 'swap': { + 'taker': self.parse_number('0.00050'), + 'maker': self.parse_number('0.00020'), + }, + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + 'password': True, + }, + 'exceptions': { + 'exact': { + # Public error codes from 50000-53999 + # General Class + '1': ExchangeError, # Operation failed + '2': ExchangeError, # Bulk operation partially succeeded + '4088': ManualInteractionNeeded, # {"code":"4088","data":[],"msg":"You can’t trade or deposit until you’ve verified your identity again. Head to Identity Verification to complete it."} + '50000': BadRequest, # Body can not be empty + '50001': OnMaintenance, # Matching engine upgrading. Please try again later + '50002': BadRequest, # Json data format error + '50004': RequestTimeout, # Endpoint request timeout(does not indicate success or failure of order, please check order status) + '50005': ExchangeNotAvailable, # API is offline or unavailable + '50006': BadRequest, # Invalid Content_Type, please use "application/json" format + '50007': AccountSuspended, # Account blocked + '50008': AuthenticationError, # User does not exist + '50009': AccountSuspended, # Account is suspended due to ongoing liquidation + '50010': ExchangeError, # User ID can not be empty + '50011': RateLimitExceeded, # Request too frequent + '50012': ExchangeError, # Account status invalid + '50013': ExchangeNotAvailable, # System is busy, please try again later + '50014': BadRequest, # Parameter {0} can not be empty + '50015': ExchangeError, # Either parameter {0} or {1} is required + '50016': ExchangeError, # Parameter {0} does not match parameter {1} + '50017': ExchangeError, # The position is frozen due to ADL. Operation restricted + '50018': ExchangeError, # Currency {0} is frozen due to ADL. Operation restricted + '50019': ExchangeError, # The account is frozen due to ADL. Operation restricted + '50020': ExchangeError, # The position is frozen due to liquidation. Operation restricted + '50021': ExchangeError, # Currency {0} is frozen due to liquidation. Operation restricted + '50022': ExchangeError, # The account is frozen due to liquidation. Operation restricted + '50023': ExchangeError, # Funding fee frozen. Operation restricted + '50024': BadRequest, # Parameter {0} and {1} can not exist at the same time + '50025': ExchangeError, # Parameter {0} count exceeds the limit {1} + '50026': ExchangeNotAvailable, # System error, please try again later. + '50027': PermissionDenied, # The account is restricted from trading + '50028': ExchangeError, # Unable to take the order, please reach out to support center for details + '50044': BadRequest, # Must select one broker type + '50061': ExchangeError, # You've reached the maximum order rate limit for self account. + '50062': ExchangeError, # This feature is currently unavailable. + # API Class + '50100': ExchangeError, # API frozen, please contact customer service + '50101': AuthenticationError, # Broker id of APIKey does not match current environment + '50102': InvalidNonce, # Timestamp request expired + '50103': AuthenticationError, # Request header "OK_ACCESS_KEY" can not be empty + '50104': AuthenticationError, # Request header "OK_ACCESS_PASSPHRASE" can not be empty + '50105': AuthenticationError, # Request header "OK_ACCESS_PASSPHRASE" incorrect + '50106': AuthenticationError, # Request header "OK_ACCESS_SIGN" can not be empty + '50107': AuthenticationError, # Request header "OK_ACCESS_TIMESTAMP" can not be empty + '50108': ExchangeError, # Exchange ID does not exist + '50109': ExchangeError, # Exchange domain does not exist + '50110': PermissionDenied, # Invalid IP + '50111': AuthenticationError, # Invalid OK_ACCESS_KEY + '50112': AuthenticationError, # Invalid OK_ACCESS_TIMESTAMP + '50113': AuthenticationError, # Invalid signature + '50114': AuthenticationError, # Invalid authorization + '50115': BadRequest, # Invalid request method + # Trade Class + '51000': BadRequest, # Parameter {0} error + '51001': BadSymbol, # Instrument ID does not exist + '51002': BadSymbol, # Instrument ID does not match underlying index + '51003': BadRequest, # Either client order ID or order ID is required + '51004': InvalidOrder, # Order amount exceeds current tier limit + '51005': InvalidOrder, # Order amount exceeds the limit + '51006': InvalidOrder, # Order price out of the limit + '51007': InvalidOrder, # Order placement failed. Order amount should be at least 1 contract(showing up when placing an order with less than 1 contract) + '51008': InsufficientFunds, # Order placement failed due to insufficient balance or margin + '51009': AccountSuspended, # Order placement function is blocked by the platform + '51010': AccountNotEnabled, # Account level too low {"code":"1","data":[{"clOrdId":"uJrfGFth9F","ordId":"","sCode":"51010","sMsg":"The current account mode does not support self API interface. ","tag":""}],"msg":"Operation failed."} + '51011': InvalidOrder, # Duplicated order ID + '51012': BadSymbol, # Token does not exist + '51014': BadSymbol, # Index does not exist + '51015': BadSymbol, # Instrument ID does not match instrument type + '51016': InvalidOrder, # Duplicated client order ID + '51017': ExchangeError, # Borrow amount exceeds the limit + '51018': ExchangeError, # User with option account can not hold net short positions + '51019': ExchangeError, # No net long positions can be held under isolated margin mode in options + '51020': InvalidOrder, # Order amount should be greater than the min available amount + '51021': ContractUnavailable, # Contract to be listed + '51022': ContractUnavailable, # Contract suspended + '51023': ExchangeError, # Position does not exist + '51024': AccountSuspended, # Unified accountblocked + '51025': ExchangeError, # Order count exceeds the limit + '51026': BadSymbol, # Instrument type does not match underlying index + '51027': ContractUnavailable, # Contract expired + '51028': ContractUnavailable, # Contract under delivery + '51029': ContractUnavailable, # Contract is being settled + '51030': ContractUnavailable, # Funding fee is being settled + '51031': InvalidOrder, # This order price is not within the closing price range + '51046': InvalidOrder, # The take profit trigger price must be higher than the order price + '51047': InvalidOrder, # The stop loss trigger price must be lower than the order price + '51051': InvalidOrder, # Your SL price should be lower than the primary order price + '51072': InvalidOrder, # As a spot lead trader, you need to set tdMode to 'spot_isolated' when configured buying lead trade pairs + '51073': InvalidOrder, # As a spot lead trader, you need to use '/copytrading/close-subposition' for selling assets through lead trades + '51074': InvalidOrder, # Only the tdMode for lead trade pairs configured by spot lead traders can be set to 'spot_isolated' + '51090': InvalidOrder, # You can't modify the amount of an SL order placed with a TP limit order. + '51091': InvalidOrder, # All TP orders in one order must be of the same type. + '51092': InvalidOrder, # TP order prices(tpOrdPx) in one order must be different. + '51093': InvalidOrder, # TP limit order prices(tpOrdPx) in one order can't be –1(market price). + '51094': InvalidOrder, # You can't place TP limit orders in spot, margin, or options trading. + '51095': InvalidOrder, # To place TP limit orders at self endpoint, you must place an SL order at the same time. + '51096': InvalidOrder, # cxlOnClosePos needs to be True to place a TP limit order + '51098': InvalidOrder, # You can't add a new TP order to an SL order placed with a TP limit order. + '51099': InvalidOrder, # You can't place TP limit orders lead trader. + '51100': InvalidOrder, # Trading amount does not meet the min tradable amount + '51101': InvalidOrder, # Entered amount exceeds the max pending order amount(Cont) per transaction + '51102': InvalidOrder, # Entered amount exceeds the max pending count + '51103': InvalidOrder, # Entered amount exceeds the max pending order count of the underlying asset + '51104': InvalidOrder, # Entered amount exceeds the max pending order amount(Cont) of the underlying asset + '51105': InvalidOrder, # Entered amount exceeds the max order amount(Cont) of the contract + '51106': InvalidOrder, # Entered amount exceeds the max order amount(Cont) of the underlying asset + '51107': InvalidOrder, # Entered amount exceeds the max holding amount(Cont) + '51108': InvalidOrder, # Positions exceed the limit for closing out with the market price + '51109': InvalidOrder, # No available offer + '51110': InvalidOrder, # You can only place a limit order after Call Auction has started + '51111': BadRequest, # Maximum {0} orders can be placed in bulk + '51112': InvalidOrder, # Close order size exceeds your available size + '51113': RateLimitExceeded, # Market-price liquidation requests too frequent + '51115': InvalidOrder, # Cancel all pending close-orders before liquidation + '51116': InvalidOrder, # Order price or trigger price exceeds {0} + '51117': InvalidOrder, # Pending close-orders count exceeds limit + '51118': InvalidOrder, # Total amount should exceed the min amount per order + '51119': InsufficientFunds, # Order placement failed due to insufficient balance + '51120': InvalidOrder, # Order quantity is less than {0}, please try again + '51121': InvalidOrder, # Order count should be the integer multiples of the lot size + '51122': InvalidOrder, # Order price should be higher than the min price {0} + '51124': InvalidOrder, # You can only place limit orders during call auction + '51125': InvalidOrder, # Currently there are reduce + reverse position pending orders in margin trading. Please cancel all reduce + reverse position pending orders and continue + '51126': InvalidOrder, # Currently there are reduce only pending orders in margin trading.Please cancel all reduce only pending orders and continue + '51127': InsufficientFunds, # Available balance is 0 + '51128': InvalidOrder, # Multi-currency margin account can not do cross-margin trading + '51129': InvalidOrder, # The value of the position and buy order has reached the position limit, and no further buying is allowed + '51130': BadSymbol, # Fixed margin currency error + '51131': InsufficientFunds, # Insufficient balance + '51132': InvalidOrder, # Your position amount is negative and less than the minimum trading amount + '51133': InvalidOrder, # Reduce-only feature is unavailable for the spot transactions by multi-currency margin account + '51134': InvalidOrder, # Closing failed. Please check your holdings and pending orders + '51135': InvalidOrder, # Your closing price has triggered the limit price, and the max buy price is {0} + '51136': InvalidOrder, # Your closing price has triggered the limit price, and the min sell price is {0} + '51137': InvalidOrder, # Your opening price has triggered the limit price, and the max buy price is {0} + '51138': InvalidOrder, # Your opening price has triggered the limit price, and the min sell price is {0} + '51139': InvalidOrder, # Reduce-only feature is unavailable for the spot transactions by simple account + '51155': RestrictedLocation, # {"code":"1","data":[{"clOrdId":"e847xxx","ordId":"","sCode":"51155","sMsg":"You can't trade self pair or borrow self crypto due to local compliance restrictions. ","tag":"e847xxx","ts":"1753979177157"}],"inTime":"1753979177157408","msg":"All operations failed","outTime":"1753979177157874"} + '51156': BadRequest, # You're leading trades in long/short mode and can't use self API endpoint to close positions + '51159': BadRequest, # You're leading trades in buy/sell mode. If you want to place orders using self API endpoint, the orders must be in the same direction existing positions and open orders. + '51162': InvalidOrder, # You have {instrument} open orders. Cancel these orders and try again + '51163': InvalidOrder, # You hold {instrument} positions. Close these positions and try again + '51166': InvalidOrder, # Currently, we don't support leading trades with self instrument + '51174': InvalidOrder, # The number of {param0} pending orders reached the upper limit of {param1}(orders). + '51185': InvalidOrder, # The maximum value allowed per order is {maxOrderValue} USD + '51201': InvalidOrder, # Value of per market order cannot exceed 100,000 USDT + '51202': InvalidOrder, # Market - order amount exceeds the max amount + '51203': InvalidOrder, # Order amount exceeds the limit {0} + '51204': InvalidOrder, # The price for the limit order can not be empty + '51205': InvalidOrder, # Reduce-Only is not available + '51250': InvalidOrder, # Algo order price is out of the available range + '51251': InvalidOrder, # Algo order type error(when user place an iceberg order) + '51252': InvalidOrder, # Algo order price is out of the available range + '51253': InvalidOrder, # Average amount exceeds the limit of per iceberg order + '51254': InvalidOrder, # Iceberg average amount error(when user place an iceberg order) + '51255': InvalidOrder, # Limit of per iceberg order: Total amount/1000 < x <= Total amount + '51256': InvalidOrder, # Iceberg order price variance error + '51257': InvalidOrder, # Trail order callback rate error + '51258': InvalidOrder, # Trail - order placement failed. The trigger price of a sell order should be higher than the last transaction price + '51259': InvalidOrder, # Trail - order placement failed. The trigger price of a buy order should be lower than the last transaction price + '51260': InvalidOrder, # Maximum {0} pending trail - orders can be held at the same time + '51261': InvalidOrder, # Each user can hold up to {0} pending stop - orders at the same time + '51262': InvalidOrder, # Maximum {0} pending iceberg orders can be held at the same time + '51263': InvalidOrder, # Maximum {0} pending time-weighted orders can be held at the same time + '51264': InvalidOrder, # Average amount exceeds the limit of per time-weighted order + '51265': InvalidOrder, # Time-weighted order limit error + '51267': InvalidOrder, # Time-weighted order strategy initiative rate error + '51268': InvalidOrder, # Time-weighted order strategy initiative range error + '51269': InvalidOrder, # Time-weighted order interval error, the interval should be {0}<= x<={1} + '51270': InvalidOrder, # The limit of time-weighted order price variance is 0 < x <= 1% + '51271': InvalidOrder, # Sweep ratio should be 0 < x <= 100% + '51272': InvalidOrder, # Price variance should be 0 < x <= 1% + '51273': InvalidOrder, # Total amount should be more than {0} + '51274': InvalidOrder, # Total quantity of time-weighted order must be larger than single order limit + '51275': InvalidOrder, # The amount of single stop-market order can not exceed the upper limit + '51276': InvalidOrder, # Stop - Market orders cannot specify a price + '51277': InvalidOrder, # TP trigger price can not be higher than the last price + '51278': InvalidOrder, # SL trigger price can not be lower than the last price + '51279': InvalidOrder, # TP trigger price can not be lower than the last price + '51280': InvalidOrder, # SL trigger price can not be higher than the last price + '51321': InvalidOrder, # You're leading trades. Currently, we don't support leading trades with arbitrage, iceberg, or TWAP bots + '51322': InvalidOrder, # You're leading trades that have been filled at market price. We've canceled your open stop orders to close your positions + '51323': BadRequest, # You're already leading trades with take profit or stop loss settings. Cancel your existing stop orders to proceed + '51324': BadRequest, # As a lead trader, you hold positions in {instrument}. To close your positions, place orders in the amount that equals the available amount for closing + '51325': InvalidOrder, # As a lead trader, you must use market price when placing stop orders + '51327': InvalidOrder, # closeFraction is only available for futures and perpetual swaps + '51328': InvalidOrder, # closeFraction is only available for reduceOnly orders + '51329': InvalidOrder, # closeFraction is only available in NET mode + '51330': InvalidOrder, # closeFraction is only available for stop market orders + '51400': OrderNotFound, # Cancellation failed order does not exist + '51401': OrderNotFound, # Cancellation failed order is already canceled + '51402': OrderNotFound, # Cancellation failed order is already completed + '51403': InvalidOrder, # Cancellation failed order type does not support cancellation + '51404': InvalidOrder, # Order cancellation unavailable during the second phase of call auction + '51405': ExchangeError, # Cancellation failed do not have any pending orders + '51406': ExchangeError, # Canceled - order count exceeds the limit {0} + '51407': BadRequest, # Either order ID or client order ID is required + '51408': ExchangeError, # Pair ID or name does not match the order info + '51409': ExchangeError, # Either pair ID or pair name ID is required + '51410': CancelPending, # Cancellation failed order is already under cancelling status + '51500': ExchangeError, # Either order price or amount is required + '51501': ExchangeError, # Maximum {0} orders can be modified + '51502': InsufficientFunds, # Order modification failed for insufficient margin or balance + '51503': ExchangeError, # Order modification failed order does not exist + '51506': ExchangeError, # Order modification unavailable for the order type + '51508': ExchangeError, # Orders are not allowed to be modified during the call auction + '51509': ExchangeError, # Modification failed order has been canceled + '51510': ExchangeError, # Modification failed order has been completed + '51511': ExchangeError, # Modification failed order price did not meet the requirement for Post Only + '51600': ExchangeError, # Status not found + '51601': ExchangeError, # Order status and order ID cannot exist at the same time + '51602': ExchangeError, # Either order status or order ID is required + '51603': OrderNotFound, # Order does not exist + '51732': AuthenticationError, # Required user KYC level not met + '51733': AuthenticationError, # User is under risk control + '51734': AuthenticationError, # User KYC Country is not supported + '51735': ExchangeError, # Sub-account is not supported + '51736': InsufficientFunds, # Insufficient {ccy} balance + # Data class + '52000': ExchangeError, # No updates + # SPOT/MARGIN error codes 54000-54999 + '54000': ExchangeError, # Margin transactions unavailable + '54001': ExchangeError, # Only Multi-currency margin account can be set to borrow coins automatically + '54008': InvalidOrder, # This operation is disabled by the 'mass cancel order' endpoint. Please enable it using self endpoint. + '54009': InvalidOrder, # The range of {param0} should be [{param1}, {param2}]. + '54011': InvalidOrder, # 200 Pre-market trading contracts are only allowed to reduce the number of positions within 1 hour before delivery. Please modify or cancel the order. + '54072': ExchangeError, # This contract is currently view-only and not tradable. + '54073': BadRequest, # Couldn’t place order, as {param0} is at risk of depegging. Switch settlement currencies and try again. + '54074': ExchangeError, # Your settings failed have positions, bot or open orders for USD contracts. + # Trading bot Error Code from 55100 to 55999 + '55100': InvalidOrder, # Take profit % should be within the range of {parameter1}-{parameter2} + '55101': InvalidOrder, # Stop loss % should be within the range of {parameter1}-{parameter2} + '55102': InvalidOrder, # Take profit % should be greater than the current bot’s PnL% + '55103': InvalidOrder, # Stop loss % should be less than the current bot’s PnL% + '55104': InvalidOrder, # Only futures grid supports take profit or stop loss based on profit percentage + '55111': InvalidOrder, # This signal name is in use, please try a new name + '55112': InvalidOrder, # This signal does not exist + '55113': InvalidOrder, # Create signal strategies with leverage greater than the maximum leverage of the instruments + # FUNDING error codes 58000-58999 + '58000': ExchangeError, # Account type {0} does not supported when getting the sub-account balance + '58001': AuthenticationError, # Incorrect trade password + '58002': PermissionDenied, # Please activate Savings Account first + '58003': ExchangeError, # Currency type is not supported by Savings Account + '58004': AccountSuspended, # Account blocked(transfer & withdrawal endpoint: either end of the account does not authorize the transfer) + '58005': ExchangeError, # The redeemed amount must be no greater than {0} + '58006': ExchangeError, # Service unavailable for token {0} + '58007': ExchangeError, # Abnormal Assets interface. Please try again later + '58100': ExchangeError, # The trading product triggers risk control, and the platform has suspended the fund transfer-out function with related users. Please wait patiently + '58101': AccountSuspended, # Transfer suspended(transfer endpoint: either end of the account does not authorize the transfer) + '58102': RateLimitExceeded, # Too frequent transfer(transfer too frequently) + '58103': ExchangeError, # Parent account user id does not match sub-account user id + '58104': ExchangeError, # Since your P2P transaction is abnormal, you are restricted from making fund transfers. Please contact customer support to remove the restriction + '58105': ExchangeError, # Since your P2P transaction is abnormal, you are restricted from making fund transfers. Please transfer funds on our website or app to complete identity verification + '58106': ExchangeError, # Please enable the account for spot contract + '58107': ExchangeError, # Please enable the account for futures contract + '58108': ExchangeError, # Please enable the account for option contract + '58109': ExchangeError, # Please enable the account for swap contract + '58110': ExchangeError, # The contract triggers risk control, and the platform has suspended the fund transfer function of it. Please wait patiently + '58111': ExchangeError, # Funds transfer unavailable perpetual contract is charging the funding fee. Please try again later + '58112': ExchangeError, # Your fund transfer failed. Please try again later + '58114': ExchangeError, # Transfer amount must be more than 0 + '58115': ExchangeError, # Sub-account does not exist + '58116': ExchangeError, # Transfer amount exceeds the limit + '58117': ExchangeError, # Account assets are abnormal, please deal with negative assets before transferring + '58125': BadRequest, # Non-tradable assets can only be transferred from sub-accounts to main accounts + '58126': BadRequest, # Non-tradable assets can only be transferred between funding accounts + '58127': BadRequest, # Main account API Key does not support current transfer 'type' parameter. Please refer to the API documentation. + '58128': BadRequest, # Sub-account API Key does not support current transfer 'type' parameter. Please refer to the API documentation. + '58200': ExchangeError, # Withdrawal from {0} to {1} is unavailable for self currency + '58201': ExchangeError, # Withdrawal amount exceeds the daily limit + '58202': ExchangeError, # The minimum withdrawal amount for NEO is 1, and the amount must be an integer + '58203': InvalidAddress, # Please add a withdrawal address + '58204': AccountSuspended, # Withdrawal suspended + '58205': ExchangeError, # Withdrawal amount exceeds the upper limit + '58206': ExchangeError, # Withdrawal amount is lower than the lower limit + '58207': InvalidAddress, # Withdrawal failed due to address error + '58208': ExchangeError, # Withdrawal failed. Please link your email + '58209': ExchangeError, # Withdrawal failed. Withdraw feature is not available for sub-accounts + '58210': ExchangeError, # Withdrawal fee exceeds the upper limit + '58211': ExchangeError, # Withdrawal fee is lower than the lower limit(withdrawal endpoint: incorrect fee) + '58212': ExchangeError, # Withdrawal fee should be {0}% of the withdrawal amount + '58213': AuthenticationError, # Please set trading password before withdrawal + '58221': BadRequest, # Missing label of withdrawal address. + '58222': BadRequest, # Illegal withdrawal address. + '58224': BadRequest, # This type of crypto does not support on-chain withdrawing to OKX addresses. Please withdraw through internal transfers. + '58227': BadRequest, # Withdrawal of non-tradable assets can be withdrawn all at once only + '58228': BadRequest, # Withdrawal of non-tradable assets requires that the API Key must be bound to an IP + '58229': InsufficientFunds, # Insufficient funding account balance to pay fees {fee} USDT + '58300': ExchangeError, # Deposit-address count exceeds the limit + '58350': InsufficientFunds, # Insufficient balance + # Account error codes 59000-59999 + '59000': ExchangeError, # Your settings failed have positions or open orders + '59001': ExchangeError, # Switching unavailable have borrowings + '59100': ExchangeError, # You have open positions. Please cancel all open positions before changing the leverage + '59101': ExchangeError, # You have pending orders with isolated positions. Please cancel all the pending orders and adjust the leverage + '59102': ExchangeError, # Leverage exceeds the maximum leverage. Please adjust the leverage + '59103': InsufficientFunds, # Leverage is too low and no sufficient margin in your account. Please adjust the leverage + '59104': ExchangeError, # The leverage is too high. The borrowed position has exceeded the maximum position of self leverage. Please adjust the leverage + '59105': ExchangeError, # Leverage can not be less than {0}. Please adjust the leverage + '59106': ExchangeError, # The max available margin corresponding to your order tier is {0}. Please adjust your margin and place a new order + '59107': ExchangeError, # You have pending orders under the service, please modify the leverage after canceling all pending orders + '59108': InsufficientFunds, # Low leverage and insufficient margin, please adjust the leverage + '59109': ExchangeError, # Account equity less than the required margin amount after adjustment. Please adjust the leverage + '59128': InvalidOrder, # As a lead trader, you can't lead trades in {instrument} with leverage higher than {num} + '59200': InsufficientFunds, # Insufficient account balance + '59201': InsufficientFunds, # Negative account balance + '59216': BadRequest, # The position doesn't exist. Please try again + '59260': PermissionDenied, # You are not a spot lead trader yet. Complete the application on our website or app first. + '59262': PermissionDenied, # You are not a contract lead trader yet. Complete the application on our website or app first. + '59300': ExchangeError, # Margin call failed. Position does not exist + '59301': ExchangeError, # Margin adjustment failed for exceeding the max limit + '59313': ExchangeError, # Unable to repay. You haven't borrowed any {ccy} {ccyPair} in Quick margin mode. + '59401': ExchangeError, # Holdings already reached the limit + '59410': OperationRejected, # You can only borrow self crypto if it supports borrowing and borrowing is enabled. + '59411': InsufficientFunds, # Manual borrowing failed. Your account's free margin is insufficient + '59412': OperationRejected, # Manual borrowing failed. The amount exceeds your borrowing limit. + '59413': OperationRejected, # You didn't borrow self crypto. No repayment needed. + '59414': BadRequest, # Manual borrowing failed. The minimum borrowing limit is {param0}.needed. + '59500': ExchangeError, # Only the APIKey of the main account has permission + '59501': ExchangeError, # Only 50 APIKeys can be created per account + '59502': ExchangeError, # Note name cannot be duplicate with the currently created APIKey note name + '59503': ExchangeError, # Each APIKey can bind up to 20 IP addresses + '59504': ExchangeError, # The sub account does not support the withdrawal function + '59505': ExchangeError, # The passphrase format is incorrect + '59506': ExchangeError, # APIKey does not exist + '59507': ExchangeError, # The two accounts involved in a transfer must be two different sub accounts under the same parent account + '59508': AccountSuspended, # The sub account of {0} is suspended + '59515': ExchangeError, # You are currently not on the custody whitelist. Please contact customer service for assistance. + '59516': ExchangeError, # Please create the Copper custody funding account first. + '59517': ExchangeError, # Please create the Komainu custody funding account first. + '59518': ExchangeError, # You can’t create a sub-account using the API; please use the app or web. + '59519': ExchangeError, # You can’t use self function/feature while it's frozen, due to: {freezereason} + '59642': BadRequest, # Lead and copy traders can only use margin-free or single-currency margin account modes + '59643': ExchangeError, # Couldn’t switch account modes’re currently copying spot trades + '59683': ExchangeError, # Set self crypto collateral crypto before selecting it settlement currency. + '59684': BadRequest, # Borrowing isn’t supported for self currency. + '59686': BadRequest, # This crypto can’t be set settlement currency. + # WebSocket error Codes from 60000-63999 + '60001': AuthenticationError, # "OK_ACCESS_KEY" can not be empty + '60002': AuthenticationError, # "OK_ACCESS_SIGN" can not be empty + '60003': AuthenticationError, # "OK_ACCESS_PASSPHRASE" can not be empty + '60004': AuthenticationError, # Invalid OK_ACCESS_TIMESTAMP + '60005': AuthenticationError, # Invalid OK_ACCESS_KEY + '60006': InvalidNonce, # Timestamp request expired + '60007': AuthenticationError, # Invalid sign + '60008': AuthenticationError, # Login is not supported for public channels + '60009': AuthenticationError, # Login failed + '60010': AuthenticationError, # Already logged in + '60011': AuthenticationError, # Please log in + '60012': BadRequest, # Illegal request + '60013': BadRequest, # Invalid args + '60014': RateLimitExceeded, # Requests too frequent + '60015': NetworkError, # Connection closed was no data transmission in the last 30 seconds + '60016': ExchangeNotAvailable, # Buffer is full, cannot write data + '60017': BadRequest, # Invalid url path + '60018': BadRequest, # The {0} {1} {2} {3} {4} does not exist + '60019': BadRequest, # Invalid op {op} + '60020': ExchangeError, # APIKey subscription amount exceeds the limit + '60021': AccountNotEnabled, # This operation does not support multiple accounts login + '60022': AuthenticationError, # Bulk login partially succeeded + '60023': DDoSProtection, # Bulk login requests too frequent + '60024': AuthenticationError, # Wrong passphrase + '60025': ExchangeError, # Token subscription amount exceeds the limit + '60026': AuthenticationError, # Batch login by APIKey and token simultaneously is not supported + '60027': ArgumentsRequired, # Parameter {0} can not be empty + '60028': NotSupported, # The current operation is not supported by self URL + '60029': AccountNotEnabled, # Only users who are VIP5 and above in trading fee tier are allowed to subscribe to books-l2-tbt channel + '60030': AccountNotEnabled, # Only users who are VIP4 and above in trading fee tier are allowed to subscribe to books50-l2-tbt channel + '60031': AuthenticationError, # The WebSocket endpoint does not support multiple account batch login, + '60032': AuthenticationError, # API key doesn't exist, + '63999': ExchangeError, # Internal system error + '64000': BadRequest, # Subscription parameter uly is unavailable anymore, please replace uly with instFamily. More details can refer to: https://www.okx.com/help-center/changes-to-v5-api-websocket-subscription-parameter-and-url, + '64001': BadRequest, # This channel has been migrated to the business URL. Please subscribe using the new URL. More details can refer to: https://www.okx.com/help-center/changes-to-v5-api-websocket-subscription-parameter-and-url, + '64002': BadRequest, # This channel is not supported by business URL. Please use "/private" URL(for private channels), or "/public" URL(for public channels). More details can refer to: https://www.okx.com/help-center/changes-to-v5-api-websocket-subscription-parameter-and-url, + '64003': AccountNotEnabled, # Your trading fee tier doesnt meet the requirement to access self channel + '70010': BadRequest, # Timestamp parameters need to be in Unix timestamp format in milliseconds. + '70013': BadRequest, # endTs needs to be bigger than or equal to beginTs. + '70016': BadRequest, # Please specify your instrument settings for at least one instType. + '70060': BadRequest, # The account doesn’t exist or the position side is incorrect. To and from accounts must be under the same main account. + '70061': BadRequest, # To move position, please enter a position that’s opposite to your current side and is smaller than or equal to your current size. + '70062': BadRequest, # account has reached the maximum number of position transfers allowed per day. + '70064': BadRequest, # Position does not exist. + '70065': BadRequest, # Couldn’t move position. Execution price cannot be determined + '70066': BadRequest, # Moving positions isn't supported in spot mode. Switch to any other account mode and try again. + '70067': BadRequest, # Moving positions isn't supported in margin trading. + '1009': BadRequest, # Request message exceeds the maximum frame length + '4001': AuthenticationError, # Login Failed + '4002': BadRequest, # Invalid Request + '4003': RateLimitExceeded, # APIKey subscription amount exceeds the limit 100 + '4004': NetworkError, # No data received in 30s + '4005': ExchangeNotAvailable, # Buffer is full, cannot write data + '4006': BadRequest, # Abnormal disconnection + '4007': AuthenticationError, # API key has been updated or deleted. Please reconnect. + '4008': RateLimitExceeded, # The number of subscribed channels exceeds the maximum limit. + }, + 'broad': { + 'Internal Server Error': ExchangeNotAvailable, # {"code":500,"data":{},"detailMsg":"","error_code":"500","error_message":"Internal Server Error","msg":"Internal Server Error"} + 'server error': ExchangeNotAvailable, # {"code":500,"data":{},"detailMsg":"","error_code":"500","error_message":"server error 1236805249","msg":"server error 1236805249"} + }, + }, + 'httpExceptions': { + '429': ExchangeNotAvailable, # https://github.com/ccxt/ccxt/issues/9612 + }, + 'precisionMode': TICK_SIZE, + 'options': { + 'sandboxMode': False, + 'defaultNetwork': 'ERC20', + 'defaultNetworks': { + 'ETH': 'ERC20', + 'BTC': 'BTC', + 'USDT': 'TRC20', + }, + 'networks': { + 'BTC': 'Bitcoin', + 'BTCLN': 'Lightning', + 'BTCLIGHTNING': 'Lightning', + 'BEP20': 'BSC', + 'BRC20': 'BRC20', + 'ERC20': 'ERC20', + 'TRC20': 'TRC20', + 'CRC20': 'Crypto', + 'ACA': 'Acala', + 'ALGO': 'Algorand', + 'APT': 'Aptos', + 'SCROLL': 'Scroll', + 'ARBONE': 'Arbitrum One', + 'AVAXC': 'Avalanche C-Chain', + 'AVAXX': 'Avalanche X-Chain', + 'BASE': 'Base', + 'SUI': 'SUI', + 'ZKSYNCERA': 'zkSync Era', + 'LINEA': 'Linea', + 'AR': 'Arweave', + 'ASTR': 'Astar', + 'BCH': 'BitcoinCash', + 'BSV': 'Bitcoin SV', + 'ADA': 'Cardano', + 'CSPR': 'Casper', + 'CELO': 'CELO', + 'XCH': 'Chia', + # 'CHZ': 'Chiliz', TBD: Chiliz 2.0 Chain vs Chiliz Chain + 'ATOM': 'Cosmos', + 'DGB': 'Digibyte', + 'DOGE': 'Dogecoin', + 'EGLD': 'Elrond', + 'CFX': 'Conflux', # CFX_EVM is different + 'EOS': 'EOS', + 'CORE': 'CORE', + 'ETC': 'Ethereum Classic', + 'ETHW': 'EthereumPow', + # 'FTM': 'Fantom', 'Sonic' TBD + 'FIL': 'Filecoin', + 'ONE': 'Harmony', + 'HBAR': 'Hedera', + 'ICX': 'ICON', + 'ICP': 'Dfinity', + 'IOST': 'IOST', + 'IOTA': 'MIOTA', + 'KLAY': 'Klaytn', + 'KSM': 'Kusama', + 'LSK': 'Lisk', + 'LTC': 'Litecoin', + 'METIS': 'Metis', + 'MINA': 'Mina', + 'GLRM': 'Moonbeam', + 'MOVR': 'Moonriver', + 'NANO': 'Nano', + 'NEAR': 'NEAR', + 'NULS': 'NULS', + 'OASYS': 'OASYS', + 'ONT': 'Ontology', + 'OPTIMISM': 'Optimism', + # 'OP': 'Optimism', or Optimism(V2), TBD + 'LAT': 'PlatON', + 'DOT': 'Polkadot', + 'MATIC': 'Polygon', + 'RVN': 'Ravencoin', + 'XRP': 'Ripple', + 'SC': 'Siacoin', + 'SOL': 'Solana', + 'STX': 'l-Stacks', + 'XLM': 'Stellar Lumens', + 'XTZ': 'Tezos', + 'TON': 'TON', + 'THETA': 'Theta', + 'WAX': 'Wax', + 'ZIL': 'Zilliqa', + # non-supported known network: CRP. KAVA, TAIKO, BOB, GNO, BLAST, RSK, SEI, MANTLE, HYPE, RUNE, OSMO, XIN, WEMIX, HT, FSN, NEO, TLOS, CANTO, SCRT, AURORA, XMR + # others: + # "OKTC", + # "X Layer", + # "Polygon(Bridged)", + # "BTCK-OKTC", + # "ETHK-OKTC", + # "Starknet", + # "LTCK-OKTC", + # "XRPK-OKTC", + # "BCHK-OKTC", + # "ETCK-OKTC", + # "Endurance Smart Chain", + # "Berachain", + # "CELO-TOKEN", + # "CFX_EVM", + # "Cortex", + # "DAIK-OKTC", + # "Dora Vota Mainnet", + # "DOTK-OKTC", + # "DYDX", + # "AELF", + # "Enjin Relay Chain", + # "FEVM", + # "FILK-OKTC", + # "Flare", + # "Gravity Alpha Mainnet", + # "INJ", + # "Story", + # "LINKK-OKTC", + # "Terra", + # "Terra Classic", + # "Terra Classic(USTC)", + # "MERLIN Network", + # "Layer 3", + # "PI", + # "Ronin", + # "Quantum", + # "SHIBK-OKTC", + # "SUSHIK-OKTC", + # "Celestia", + # "TRXK-OKTC", + # "UNIK-OKTC", + # "Venom", + # "WBTCK-OKTC", + # "ZetaChain", + }, + 'fetchOpenInterestHistory': { + 'timeframes': { + '5m': '5m', + '1h': '1H', + '8h': '8H', + '1d': '1D', + '5M': '5m', + '1H': '1H', + '8H': '8H', + '1D': '1D', + }, + }, + 'fetchOHLCV': { + # 'type': 'Candles', # Candles or HistoryCandles, IndexCandles, MarkPriceCandles + 'timezone': 'UTC', # UTC, HK + }, + 'fetchPositions': { + 'method': 'privateGetAccountPositions', # privateGetAccountPositions or privateGetAccountPositionsHistory + }, + 'createOrder': 'privatePostTradeBatchOrders', # or 'privatePostTradeOrder' or 'privatePostTradeOrderAlgo' + 'createMarketBuyOrderRequiresPrice': False, + 'fetchMarkets': { + 'types': ['spot', 'future', 'swap', 'option'], # spot, future, swap, option + }, + 'timeDifference': 0, # the difference between system clock and exchange server clock + 'adjustForTimeDifference': False, # controls the adjustment logic upon instantiation + 'defaultType': 'spot', # 'funding', 'spot', 'margin', 'future', 'swap', 'option' + # 'fetchBalance': { + # 'type': 'spot', # 'funding', 'trading', 'spot' + # }, + 'fetchLedger': { + 'method': 'privateGetAccountBills', # privateGetAccountBills, privateGetAccountBillsArchive, privateGetAssetBills + }, + # 6: Funding account, 18: Trading account + 'fetchOrder': { + 'method': 'privateGetTradeOrder', # privateGetTradeOrdersAlgoHistory + }, + 'fetchOpenOrders': { + 'method': 'privateGetTradeOrdersPending', # privateGetTradeOrdersAlgoPending + }, + 'cancelOrders': { + 'method': 'privatePostTradeCancelBatchOrders', # privatePostTradeCancelAlgos + }, + 'fetchCanceledOrders': { + 'method': 'privateGetTradeOrdersHistory', # privateGetTradeOrdersAlgoHistory + }, + 'fetchClosedOrders': { + 'method': 'privateGetTradeOrdersHistory', # privateGetTradeOrdersAlgoHistory + }, + 'withdraw': { + # a funding password credential is required by the exchange for the + # withdraw call(not to be confused with the api password credential) + 'password': None, + 'pwd': None, # password or pwd both work + }, + 'algoOrderTypes': { + 'conditional': True, + 'trigger': True, + 'oco': True, + 'move_order_stop': True, + 'iceberg': True, + 'twap': True, + }, + 'accountsByType': { + 'funding': '6', + 'trading': '18', # unified trading account + 'spot': '18', + 'future': '18', + 'futures': '18', + 'margin': '18', + 'swap': '18', + 'option': '18', + }, + 'accountsById': { + '6': 'funding', + '18': 'trading', # unified trading account + }, + 'exchangeType': { + 'spot': 'SPOT', + 'margin': 'MARGIN', + 'swap': 'SWAP', + 'future': 'FUTURES', + 'futures': 'FUTURES', # deprecated + 'option': 'OPTION', + 'SPOT': 'SPOT', + 'MARGIN': 'MARGIN', + 'SWAP': 'SWAP', + 'FUTURES': 'FUTURES', + 'OPTION': 'OPTION', + }, + 'brokerId': '6b9ad766b55dBCDE', + }, + 'features': { + 'default': { + 'sandbox': True, + 'createOrder': { + 'marginMode': True, + 'triggerPrice': True, + 'triggerPriceType': { + 'last': True, + 'mark': True, + 'index': True, + }, + 'triggerDirection': False, + 'stopLossPrice': True, + 'takeProfitPrice': True, + 'attachedStopLossTakeProfit': { + 'triggerPriceType': { + 'last': True, + 'mark': True, + 'index': True, + }, + 'price': True, + }, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': True, + 'trailing': True, + 'iceberg': True, # todo implement + 'leverage': False, + 'selfTradePrevention': True, # todo implement + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': False, + }, + 'createOrders': { + 'max': 20, + }, + 'fetchMyTrades': { + 'marginMode': False, + 'daysBack': 90, + 'limit': 100, + 'untilDays': 10000, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': True, + 'trailing': True, + 'symbolRequired': True, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 100, + 'trigger': True, + 'trailing': True, + 'symbolRequired': False, + }, + 'fetchOrders': None, # not supported + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 90, # 3 months + 'daysBackCanceled': 1 / 12, # 2 hour + 'untilDays': None, + 'trigger': True, + 'trailing': True, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 300, # regular candles(recent & historical) both have 300 max + 'mark': 100, + 'index': 100, + }, + }, + 'spot': { + 'extends': 'default', + 'fetchCurrencies': { + 'private': True, + }, + }, + 'swap': { + 'linear': { + 'extends': 'default', + }, + 'inverse': { + 'extends': 'default', + }, + }, + 'future': { + 'linear': { + 'extends': 'default', + }, + 'inverse': { + 'extends': 'default', + }, + }, + }, + 'currencies': { + 'USD': self.safe_currency_structure({'id': 'USD', 'code': 'USD', 'precision': self.parse_number('0.0001')}), + 'EUR': self.safe_currency_structure({'id': 'EUR', 'code': 'EUR', 'precision': self.parse_number('0.0001')}), + 'AED': self.safe_currency_structure({'id': 'AED', 'code': 'AED', 'precision': self.parse_number('0.0001')}), + 'GBP': self.safe_currency_structure({'id': 'GBP', 'code': 'GBP', 'precision': self.parse_number('0.0001')}), + 'AUD': self.safe_currency_structure({'id': 'AUD', 'code': 'AUD', 'precision': self.parse_number('0.0001')}), + }, + 'commonCurrencies': { + # the exchange refers to ERC20 version of Aeternity(AEToken) + 'AE': 'AET', # https://github.com/ccxt/ccxt/issues/4981 + 'WIN': 'WINTOKEN', # https://github.com/ccxt/ccxt/issues/5701 + }, + }) + + def handle_market_type_and_params(self, methodName: str, market: Market = None, params={}, defaultValue=None) -> Any: + instType = self.safe_string(params, 'instType') + params = self.omit(params, 'instType') + type = self.safe_string(params, 'type') + if (type is None) and (instType is not None): + params['type'] = instType + return super(okx, self).handle_market_type_and_params(methodName, market, params, defaultValue) + + def convert_to_instrument_type(self, type): + exchangeTypes = self.safe_dict(self.options, 'exchangeType', {}) + return self.safe_string(exchangeTypes, type, type) + + def create_expired_option_market(self, symbol: str): + # support expired option contracts + quote = 'USD' + optionParts = symbol.split('-') + symbolBase = symbol.split('/') + base = None + if symbol.find('/') > -1: + base = self.safe_string(symbolBase, 0) + else: + base = self.safe_string(optionParts, 0) + settle = base + expiry = self.safe_string(optionParts, 2) + strike = self.safe_string(optionParts, 3) + optionType = self.safe_string(optionParts, 4) + datetime = self.convert_expire_date(expiry) + timestamp = self.parse8601(datetime) + return { + 'id': base + '-' + quote + '-' + expiry + '-' + strike + '-' + optionType, + 'symbol': base + '/' + quote + ':' + settle + '-' + expiry + '-' + strike + '-' + optionType, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': base, + 'quoteId': quote, + 'settleId': settle, + 'active': False, + 'type': 'option', + 'linear': None, + 'inverse': None, + 'spot': False, + 'swap': False, + 'future': False, + 'option': True, + 'margin': False, + 'contract': True, + 'contractSize': self.parse_number('1'), + 'expiry': timestamp, + 'expiryDatetime': datetime, + 'optionType': 'call' if (optionType == 'C') else 'put', + 'strike': self.parse_number(strike), + 'precision': { + 'amount': None, + 'price': None, + }, + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'info': None, + } + + def safe_market(self, marketId: Str = None, market: Market = None, delimiter: Str = None, marketType: Str = None) -> MarketInterface: + isOption = (marketId is not None) and ((marketId.find('-C') > -1) or (marketId.find('-P') > -1)) + if isOption and not (marketId in self.markets_by_id): + # handle expired option contracts + return self.create_expired_option_market(marketId) + return super(okx, self).safe_market(marketId, market, delimiter, marketType) + + def fetch_status(self, params={}): + """ + the latest known information on the availability of the exchange API + + https://www.okx.com/docs-v5/en/#status-get-status + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `status structure ` + """ + response = self.publicGetSystemStatus(params) + # + # Note, if there is no maintenance around, the 'data' array is empty + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "begin": "1621328400000", + # "end": "1621329000000", + # "href": "https://www.okx.com/support/hc/en-us/articles/360060882172", + # "scheDesc": "", + # "serviceType": "1", # 0 WebSocket, 1 Spot/Margin, 2 Futures, 3 Perpetual, 4 Options, 5 Trading service + # "state": "scheduled", # ongoing, completed, canceled + # "system": "classic", # classic, unified + # "title": "Classic Spot System Upgrade" + # }, + # ] + # } + # + data = self.safe_list(response, 'data', []) + dataLength = len(data) + update: dict = { + 'updated': None, + 'status': 'ok' if (dataLength == 0) else 'maintenance', + 'eta': None, + 'url': None, + 'info': response, + } + for i in range(0, len(data)): + event = data[i] + state = self.safe_string(event, 'state') + update['eta'] = self.safe_integer(event, 'end') + update['url'] = self.safe_string(event, 'href') + if state == 'ongoing': + update['status'] = 'maintenance' + elif state == 'scheduled': + update['status'] = 'ok' + elif state == 'completed': + update['status'] = 'ok' + elif state == 'canceled': + update['status'] = 'ok' + return update + + def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://www.okx.com/docs-v5/en/#public-data-rest-api-get-system-time + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = self.publicGetPublicTime(params) + # + # { + # "code": "0", + # "data": [ + # {"ts": "1621247923668"} + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + first = self.safe_dict(data, 0, {}) + return self.safe_integer(first, 'ts') + + def fetch_accounts(self, params={}) -> List[Account]: + """ + fetch all the accounts associated with a profile + + https://www.okx.com/docs-v5/en/#trading-account-rest-api-get-account-configuration + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `account structures ` indexed by the account type + """ + response = self.privateGetAccountConfig(params) + # + # { + # "code": "0", + # "data": [ + # { + # "acctLv": "2", + # "acctStpMode": "cancel_maker", + # "autoLoan": False, + # "ctIsoMode": "automatic", + # "enableSpotBorrow": False, + # "greeksType": "PA", + # "feeType": "0", + # "ip": "", + # "type": "0", + # "kycLv": "3", + # "label": "v5 test", + # "level": "Lv1", + # "levelTmp": "", + # "liquidationGear": "-1", + # "mainUid": "44705892343619584", + # "mgnIsoMode": "automatic", + # "opAuth": "1", + # "perm": "read_only,withdraw,trade", + # "posMode": "long_short_mode", + # "roleType": "0", + # "spotBorrowAutoRepay": False, + # "spotOffsetType": "", + # "spotRoleType": "0", + # "spotTraderInsts": [], + # "traderInsts": [], + # "uid": "44705892343619584", + # "settleCcy": "USDT", + # "settleCcyList": ["USD", "USDC", "USDG"], + # } + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + result = [] + for i in range(0, len(data)): + account = data[i] + accountId = self.safe_string(account, 'uid') + type = self.safe_string(account, 'acctLv') + result.append({ + 'id': accountId, + 'type': type, + 'currency': None, + 'info': account, + 'code': None, + }) + return result + + def nonce(self): + return self.milliseconds() - self.options['timeDifference'] + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for okx + + https://www.okx.com/docs-v5/en/#rest-api-public-data-get-instruments + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + if self.options['adjustForTimeDifference']: + self.load_time_difference() + types = ['spot', 'future', 'swap', 'option'] + fetchMarketsOption = self.safe_dict(self.options, 'fetchMarkets') + if fetchMarketsOption is not None: + types = self.safe_list(fetchMarketsOption, 'types', types) + else: + types = self.safe_list(self.options, 'fetchMarkets', types) # backward-support + promises = [] + result = [] + for i in range(0, len(types)): + promises.append(self.fetch_markets_by_type(types[i], params)) + promises = promises + for i in range(0, len(promises)): + result = self.array_concat(result, promises[i]) + return result + + def parse_market(self, market: dict) -> Market: + # + # { + # "alias": "", # self_week, next_week, quarter, next_quarter + # "baseCcy": "BTC", + # "category": "1", + # "ctMult": "", + # "ctType": "", # inverse, linear + # "ctVal": "", + # "ctValCcy": "", + # "expTime": "", + # "instId": "BTC-USDT", # BTC-USD-210521, CSPR-USDT-SWAP, BTC-USD-210517-44000-C + # "instType": "SPOT", # SPOT, FUTURES, SWAP, OPTION + # "lever": "10", + # "listTime": "1548133413000", + # "lotSz": "0.00000001", + # "minSz": "0.00001", + # "optType": "", + # "quoteCcy": "USDT", + # "settleCcy": "", + # "state": "live", + # "stk": "", + # "tickSz": "0.1", + # "uly": "" + # } + # + # { + # "alias": "", + # "baseCcy": "", + # "category": "1", + # "ctMult": "0.1", + # "ctType": "", + # "ctVal": "1", + # "ctValCcy": "BTC", + # "expTime": "1648195200000", + # "instId": "BTC-USD-220325-194000-P", + # "instType": "OPTION", + # "lever": "", + # "listTime": "1631262612280", + # "contTdSwTime": "1631262812280", + # "lotSz": "1", + # "minSz": "1", + # "optType": "P", + # "quoteCcy": "", + # "settleCcy": "BTC", + # "state": "live", + # "stk": "194000", + # "tickSz": "0.0005", + # "uly": "BTC-USD" + # } + # + # for swap "preopen" markets, only `instId` and `instType` are present + # + # instId: "ETH-USD_UM-SWAP", + # instType: "SWAP", + # state: "preopen", + # + id = self.safe_string(market, 'instId') + type = self.safe_string_lower(market, 'instType') + if type == 'futures': + type = 'future' + spot = (type == 'spot') + future = (type == 'future') + swap = (type == 'swap') + option = (type == 'option') + contract = swap or future or option + baseId = self.safe_string(market, 'baseCcy', '') # defaulting to '' because some weird preopen markets have empty baseId + quoteId = self.safe_string(market, 'quoteCcy', '') + settleId = self.safe_string(market, 'settleCcy') + settle = self.safe_currency_code(settleId) + underlying = self.safe_string(market, 'uly') + if (underlying is not None) and not spot: + parts = underlying.split('-') + baseId = self.safe_string(parts, 0) + quoteId = self.safe_string(parts, 1) + if ((baseId == '') or (quoteId == '')) and spot: # to fix weird preopen markets + instId = self.safe_string(market, 'instId', '') + parts = instId.split('-') + baseId = self.safe_string(parts, 0) + quoteId = self.safe_string(parts, 1) + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + symbol = base + '/' + quote + # handle preopen empty markets + if base == '' or quote == '': + symbol = id + expiry = None + strikePrice = None + optionType = None + if contract: + if settle is not None: + symbol = symbol + ':' + settle + if future: + expiry = self.safe_integer(market, 'expTime') + if expiry is not None: + ymd = self.yymmdd(expiry) + symbol = symbol + '-' + ymd + elif option: + expiry = self.safe_integer(market, 'expTime') + strikePrice = self.safe_string(market, 'stk') + optionType = self.safe_string(market, 'optType') + if expiry is not None: + ymd = self.yymmdd(expiry) + symbol = symbol + '-' + ymd + '-' + strikePrice + '-' + optionType + optionType = 'put' if (optionType == 'P') else 'call' + fees = self.safe_dict_2(self.fees, type, 'trading', {}) + maxLeverage = self.safe_string(market, 'lever', '1') + maxLeverage = Precise.string_max(maxLeverage, '1') + maxSpotCost = self.safe_number(market, 'maxMktSz') + status = self.safe_string(market, 'state') + return self.extend(fees, { + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': type, + 'spot': spot, + 'margin': spot and (Precise.string_gt(maxLeverage, '1')), + 'swap': swap, + 'future': future, + 'option': option, + 'active': status == 'live', + 'contract': contract, + 'linear': (quoteId == settleId) if contract else None, + 'inverse': (baseId == settleId) if contract else None, + 'contractSize': self.safe_number(market, 'ctVal') if contract else None, + 'expiry': expiry, + 'expiryDatetime': self.iso8601(expiry), + 'strike': self.parse_number(strikePrice), + 'optionType': optionType, + 'created': self.safe_integer_2(market, 'contTdSwTime', 'listTime'), # contTdSwTime is public trading start time, while listTime considers pre-trading too + 'precision': { + 'amount': self.safe_number(market, 'lotSz'), + 'price': self.safe_number(market, 'tickSz'), + }, + 'limits': { + 'leverage': { + 'min': self.parse_number('1'), + 'max': self.parse_number(maxLeverage), + }, + 'amount': { + 'min': self.safe_number(market, 'minSz'), + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None if contract else maxSpotCost, + }, + }, + 'info': market, + }) + + def fetch_markets_by_type(self, type, params={}): + request: dict = { + 'instType': self.convert_to_instrument_type(type), + } + if type == 'option': + optionsUnderlying = self.safe_list(self.options, 'defaultUnderlying', ['BTC-USD', 'ETH-USD']) + promises = [] + for i in range(0, len(optionsUnderlying)): + underlying = optionsUnderlying[i] + request['uly'] = underlying + promises.append(self.publicGetPublicInstruments(self.extend(request, params))) + promisesResult = promises + markets = [] + for i in range(0, len(promisesResult)): + res = self.safe_dict(promisesResult, i, {}) + options = self.safe_list(res, 'data', []) + markets = self.array_concat(markets, options) + return self.parse_markets(markets) + response = self.publicGetPublicInstruments(self.extend(request, params)) + # + # spot, future, swap, option + # + # { + # "code": "0", + # "data": [ + # { + # "alias": "", # self_week, next_week, quarter, next_quarter + # "baseCcy": "BTC", + # "category": "1", + # "ctMult": "", + # "ctType": "", # inverse, linear + # "ctVal": "", + # "ctValCcy": "", + # "expTime": "", + # "instId": "BTC-USDT", # BTC-USD-210521, CSPR-USDT-SWAP, BTC-USD-210517-44000-C + # "instType": "SPOT", # SPOT, FUTURES, SWAP, OPTION + # "lever": "10", + # "listTime": "1548133413000", + # "lotSz": "0.00000001", + # "minSz": "0.00001", + # "optType": "", + # "quoteCcy": "USDT", + # "settleCcy": "", + # "state": "live", + # "stk": "", + # "tickSz": "0.1", + # "uly": "" + # } + # ], + # "msg": "" + # } + # + dataResponse = self.safe_list(response, 'data', []) + return self.parse_markets(dataResponse) + + def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://www.okx.com/docs-v5/en/#rest-api-funding-get-currencies + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + # self endpoint requires authentication + # while fetchCurrencies is a public API method by design + # therefore we check the keys here + # and fallback to generating the currencies from the markets + isSandboxMode = self.safe_bool(self.options, 'sandboxMode', False) + if not self.check_required_credentials(False) or isSandboxMode: + return {} + # + # has['fetchCurrencies'] is currently set to True, but an unauthorized request returns + # + # {"msg":"Request header “OK_ACCESS_KEY“ can't be empty.","code":"50103"} + # + response = self.privateGetAssetCurrencies(params) + # + # { + # "code": "0", + # "data": [ + # { + # "canDep": True, + # "canInternal": False, + # "canWd": True, + # "ccy": "USDT", + # "chain": "USDT-TRC20", + # "logoLink": "https://static.coinall.ltd/cdn/assets/imgs/221/5F74EB20302D7761.png", + # "mainNet": False, + # "maxFee": "1.6", + # "maxWd": "8852150", + # "minFee": "0.8", + # "minWd": "2", + # "name": "Tether", + # "usedWdQuota": "0", + # "wdQuota": "500", + # "wdTickSz": "3" + # }, + # { + # "canDep": True, + # "canInternal": False, + # "canWd": True, + # "ccy": "USDT", + # "chain": "USDT-ERC20", + # "logoLink": "https://static.coinall.ltd/cdn/assets/imgs/221/5F74EB20302D7761.png", + # "mainNet": False, + # "maxFee": "16", + # "maxWd": "8852150", + # "minFee": "8", + # "minWd": "2", + # "name": "Tether", + # "usedWdQuota": "0", + # "wdQuota": "500", + # "wdTickSz": "3" + # }, + # ... + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + result: dict = {} + dataByCurrencyId = self.group_by(data, 'ccy') + currencyIds = list(dataByCurrencyId.keys()) + for i in range(0, len(currencyIds)): + currencyId = currencyIds[i] + currency = self.safe_currency(currencyId) + code = currency['code'] + chains = dataByCurrencyId[currencyId] + networks: dict = {} + type = 'crypto' + chainsLength = len(chains) + for j in range(0, chainsLength): + chain = chains[j] + # allow empty string for rare fiat-currencies, e.g. TRY + networkId = self.safe_string(chain, 'chain', '') # USDT-BEP20, USDT-Avalance-C, etc + if networkId == '': + # only happens for fiat 'TRY' currency + type = 'fiat' + idParts = networkId.split('-') + parts = self.array_slice(idParts, 1) + chainPart = '-'.join(parts) + networkCode = self.network_id_to_code(chainPart, currency['code']) + networks[networkCode] = { + 'id': networkId, + 'network': networkCode, + 'active': None, + 'deposit': self.safe_bool(chain, 'canDep'), + 'withdraw': self.safe_bool(chain, 'canWd'), + 'fee': self.safe_number(chain, 'fee'), + 'precision': self.parse_number(self.parse_precision(self.safe_string(chain, 'wdTickSz'))), + 'limits': { + 'withdraw': { + 'min': self.safe_number(chain, 'minWd'), + 'max': self.safe_number(chain, 'maxWd'), + }, + }, + 'info': chain, + } + firstChain = self.safe_dict(chains, 0, {}) + result[code] = self.safe_currency_structure({ + 'info': chains, + 'code': code, + 'id': currencyId, + 'name': self.safe_string(firstChain, 'name'), + 'active': None, + 'deposit': None, + 'withdraw': None, + 'fee': None, + 'precision': None, + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + }, + 'type': type, + 'networks': networks, + }) + return result + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://www.okx.com/docs-v5/en/#order-book-trading-market-data-get-order-book + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.method]: 'publicGetMarketBooksFull' or 'publicGetMarketBooks' default is 'publicGetMarketBooks' + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + } + method = None + method, params = self.handle_option_and_params(params, 'fetchOrderBook', 'method', 'publicGetMarketBooks') + if method == 'publicGetMarketBooksFull' and limit is None: + limit = 5000 + limit = 100 if (limit is None) else limit + if limit is not None: + request['sz'] = limit # max 400 + response = None + if (method == 'publicGetMarketBooksFull') or (limit > 400): + response = self.publicGetMarketBooksFull(self.extend(request, params)) + else: + response = self.publicGetMarketBooks(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "asks": [ + # ["0.07228","4.211619","0","2"], # price, amount, liquidated orders, total open orders + # ["0.0723","299.880364","0","2"], + # ["0.07231","3.72832","0","1"], + # ], + # "bids": [ + # ["0.07221","18.5","0","1"], + # ["0.0722","18.5","0","1"], + # ["0.07219","0.505407","0","1"], + # ], + # "ts": "1621438475342" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + first = self.safe_dict(data, 0, {}) + timestamp = self.safe_integer(first, 'ts') + return self.parse_order_book(first, symbol, timestamp) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "instType":"SWAP", + # "instId":"BTC-USDT-SWAP", + # "markPx":"200", + # "ts":"1597026383085" + # } + # + # { + # "instType": "SPOT", + # "instId": "ETH-BTC", + # "last": "0.07319", + # "lastSz": "0.044378", + # "askPx": "0.07322", + # "askSz": "4.2", + # "bidPx": "0.0732", + # "bidSz": "6.050058", + # "open24h": "0.07801", + # "high24h": "0.07975", + # "low24h": "0.06019", + # "volCcy24h": "11788.887619", + # "vol24h": "167493.829229", + # "ts": "1621440583784", + # "sodUtc0": "0.07872", + # "sodUtc8": "0.07345" + # } + # { + # instId: 'LTC-USDT', + # idxPx: '65.74', + # open24h: '65.37', + # high24h: '66.15', + # low24h: '64.97', + # sodUtc0: '65.68', + # sodUtc8: '65.54', + # ts: '1728467346900' + # }, + # + timestamp = self.safe_integer(ticker, 'ts') + marketId = self.safe_string(ticker, 'instId') + market = self.safe_market(marketId, market, '-') + symbol = market['symbol'] + last = self.safe_string(ticker, 'last') + open = self.safe_string(ticker, 'open24h') + spot = self.safe_bool(market, 'spot', False) + quoteVolume = self.safe_string(ticker, 'volCcy24h') if spot else None + baseVolume = self.safe_string(ticker, 'vol24h') + high = self.safe_string(ticker, 'high24h') + low = self.safe_string(ticker, 'low24h') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': high, + 'low': low, + 'bid': self.safe_string(ticker, 'bidPx'), + 'bidVolume': self.safe_string(ticker, 'bidSz'), + 'ask': self.safe_string(ticker, 'askPx'), + 'askVolume': self.safe_string(ticker, 'askSz'), + 'vwap': None, + 'open': open, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'markPrice': self.safe_string(ticker, 'markPx'), + 'indexPrice': self.safe_string(ticker, 'idxPx'), + 'info': ticker, + }, market) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://www.okx.com/docs-v5/en/#order-book-trading-market-data-get-ticker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + } + response = self.publicGetMarketTicker(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "instType": "SPOT", + # "instId": "ETH-BTC", + # "last": "0.07319", + # "lastSz": "0.044378", + # "askPx": "0.07322", + # "askSz": "4.2", + # "bidPx": "0.0732", + # "bidSz": "6.050058", + # "open24h": "0.07801", + # "high24h": "0.07975", + # "low24h": "0.06019", + # "volCcy24h": "11788.887619", + # "vol24h": "167493.829229", + # "ts": "1621440583784", + # "sodUtc0": "0.07872", + # "sodUtc8": "0.07345" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + first = self.safe_dict(data, 0, {}) + return self.parse_ticker(first, market) + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://www.okx.com/docs-v5/en/#order-book-trading-market-data-get-tickers + + :param str[] [symbols]: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + symbols = self.market_symbols(symbols) + market = self.get_market_from_symbols(symbols) + marketType = None + marketType, params = self.handle_market_type_and_params('fetchTickers', market, params) + request: dict = { + 'instType': self.convert_to_instrument_type(marketType), + } + if marketType == 'option': + defaultUnderlying = self.safe_string(self.options, 'defaultUnderlying', 'BTC-USD') + currencyId = self.safe_string_2(params, 'uly', 'marketId', defaultUnderlying) + if currencyId is None: + raise ArgumentsRequired(self.id + ' fetchTickers() requires an underlying uly or marketId parameter for options markets') + else: + request['uly'] = currencyId + response = self.publicGetMarketTickers(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "instType": "SPOT", + # "instId": "BCD-BTC", + # "last": "0.0000769", + # "lastSz": "5.4788", + # "askPx": "0.0000777", + # "askSz": "3.2197", + # "bidPx": "0.0000757", + # "bidSz": "4.7509", + # "open24h": "0.0000885", + # "high24h": "0.0000917", + # "low24h": "0.0000596", + # "volCcy24h": "9.2877", + # "vol24h": "124824.1985", + # "ts": "1621441741434", + # "sodUtc0": "0.0000905", + # "sodUtc8": "0.0000729" + # }, + # ] + # } + # + tickers = self.safe_list(response, 'data', []) + return self.parse_tickers(tickers, symbols) + + def fetch_mark_price(self, symbol: str, params={}) -> Ticker: + """ + fetches mark price for the market + + https://www.okx.com/docs-v5/en/#public-data-rest-api-get-mark-price + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + } + response = self.publicGetPublicMarkPrice(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "instId": "ETH-USDT", + # "instType": "MARGIN", + # "markPx": "2403.98", + # "ts": "1728578500703" + # } + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data') + return self.parse_ticker(self.safe_dict(data, 0), market) + + def fetch_mark_prices(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://www.okx.com/docs-v5/en/#public-data-rest-api-get-mark-price + + :param str[] [symbols]: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + symbols = self.market_symbols(symbols) + market = self.get_market_from_symbols(symbols) + marketType = None + marketType, params = self.handle_market_type_and_params('fetchTickers', market, params, 'swap') + request: dict = { + 'instType': self.convert_to_instrument_type(marketType), + } + if marketType == 'option': + defaultUnderlying = self.safe_string(self.options, 'defaultUnderlying', 'BTC-USD') + currencyId = self.safe_string_2(params, 'uly', 'marketId', defaultUnderlying) + if currencyId is None: + raise ArgumentsRequired(self.id + ' fetchMarkPrices() requires an underlying uly or marketId parameter for options markets') + else: + request['uly'] = currencyId + response = self.publicGetPublicMarkPrice(self.extend(request, params)) + tickers = self.safe_list(response, 'data', []) + return self.parse_tickers(tickers, symbols) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # public fetchTrades + # + # { + # "instId": "ETH-BTC", + # "side": "sell", + # "sz": "0.119501", + # "px": "0.07065", + # "tradeId": "15826757", + # "ts": "1621446178316" + # } + # + # option: fetchTrades + # + # { + # "fillVol": "0.46387625976562497", + # "fwdPx": "26299.754935451125", + # "indexPx": "26309.7", + # "instFamily": "BTC-USD", + # "instId": "BTC-USD-230526-26000-C", + # "markPx": "0.042386283557554236", + # "optType": "C", + # "px": "0.0415", + # "side": "sell", + # "sz": "90", + # "tradeId": "112", + # "ts": "1683907480154" + # } + # + # private fetchMyTrades + # + # { + # "side": "buy", + # "fillSz": "0.007533", + # "fillPx": "2654.98", + # "fee": "-0.000007533", + # "ordId": "317321390244397056", + # "instType": "SPOT", + # "instId": "ETH-USDT", + # "clOrdId": "", + # "posSide": "net", + # "billId": "317321390265368576", + # "tag": "0", + # "execType": "T", + # "tradeId": "107601752", + # "feeCcy": "ETH", + # "ts": "1621927314985" + # } + # + id = self.safe_string(trade, 'tradeId') + marketId = self.safe_string(trade, 'instId') + market = self.safe_market(marketId, market, '-') + symbol = market['symbol'] + timestamp = self.safe_integer(trade, 'ts') + price = self.safe_string_2(trade, 'fillPx', 'px') + amount = self.safe_string_2(trade, 'fillSz', 'sz') + side = self.safe_string(trade, 'side') + orderId = self.safe_string(trade, 'ordId') + feeCostString = self.safe_string(trade, 'fee') + fee = None + if feeCostString is not None: + feeCostSigned = Precise.string_neg(feeCostString) + feeCurrencyId = self.safe_string(trade, 'feeCcy') + feeCurrencyCode = self.safe_currency_code(feeCurrencyId) + fee = { + 'cost': feeCostSigned, + 'currency': feeCurrencyCode, + } + takerOrMaker = self.safe_string(trade, 'execType') + if takerOrMaker == 'T': + takerOrMaker = 'taker' + elif takerOrMaker == 'M': + takerOrMaker = 'maker' + return self.safe_trade({ + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'id': id, + 'order': orderId, + 'type': None, + 'takerOrMaker': takerOrMaker, + 'side': side, + 'price': price, + 'amount': amount, + 'cost': None, + 'fee': fee, + }, market) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://www.okx.com/docs-v5/en/#rest-api-market-data-get-trades + https://www.okx.com/docs-v5/en/#rest-api-public-data-get-option-trades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.method]: 'publicGetMarketTrades' or 'publicGetMarketHistoryTrades' default is 'publicGetMarketTrades' + :param boolean [params.paginate]: *only applies to publicGetMarketHistoryTrades* default False, when True will automatically paginate by calling self endpoint multiple times + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchTrades', 'paginate') + if paginate: + return self.fetch_paginated_call_cursor('fetchTrades', symbol, since, limit, params, 'tradeId', 'after', None, 100) + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + } + response = None + if market['option']: + response = self.publicGetPublicOptionTrades(self.extend(request, params)) + else: + if limit is not None: + request['limit'] = limit # default 100 + method = None + method, params = self.handle_option_and_params(params, 'fetchTrades', 'method', 'publicGetMarketTrades') + if method == 'publicGetMarketTrades': + response = self.publicGetMarketTrades(self.extend(request, params)) + elif method == 'publicGetMarketHistoryTrades': + response = self.publicGetMarketHistoryTrades(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # {"instId":"ETH-BTC","side":"sell","sz":"0.119501","px":"0.07065","tradeId":"15826757","ts":"1621446178316"}, + # {"instId":"ETH-BTC","side":"sell","sz":"0.03","px":"0.07068","tradeId":"15826756","ts":"1621446178066"}, + # {"instId":"ETH-BTC","side":"buy","sz":"0.507","px":"0.07069","tradeId":"15826755","ts":"1621446175085"}, + # ] + # } + # + # option + # + # { + # "code": "0", + # "data": [ + # { + # "fillVol": "0.46387625976562497", + # "fwdPx": "26299.754935451125", + # "indexPx": "26309.7", + # "instFamily": "BTC-USD", + # "instId": "BTC-USD-230526-26000-C", + # "markPx": "0.042386283557554236", + # "optType": "C", + # "px": "0.0415", + # "side": "sell", + # "sz": "90", + # "tradeId": "112", + # "ts": "1683907480154" + # }, + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_trades(data, market, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # [ + # "1678928760000", # timestamp + # "24341.4", # open + # "24344", # high + # "24313.2", # low + # "24323", # close + # "628", # contract volume + # "2.5819", # base volume + # "62800", # quote volume + # "0" # candlestick state + # ] + # + res = self.handle_market_type_and_params('fetchOHLCV', market, None) + type = res[0] + volumeIndex = 5 if (type == 'spot') else 6 + return [ + self.safe_integer(ohlcv, 0), + self.safe_number(ohlcv, 1), + self.safe_number(ohlcv, 2), + self.safe_number(ohlcv, 3), + self.safe_number(ohlcv, 4), + self.safe_number(ohlcv, volumeIndex), + ] + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://www.okx.com/docs-v5/en/#rest-api-market-data-get-candlesticks + https://www.okx.com/docs-v5/en/#rest-api-market-data-get-candlesticks-history + https://www.okx.com/docs-v5/en/#rest-api-market-data-get-mark-price-candlesticks + https://www.okx.com/docs-v5/en/#rest-api-market-data-get-mark-price-candlesticks-history + https://www.okx.com/docs-v5/en/#rest-api-market-data-get-index-candlesticks + https://www.okx.com/docs-v5/en/#rest-api-market-data-get-index-candlesticks-history + https://www.okx.com/docs-v5/en/#order-book-trading-market-data-get-candlesticks-history + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.price]: "mark" or "index" for mark price and index price candles + :param int [params.until]: timestamp in ms of the latest candle to fetch + :param str [params.type]: "Candles" or "HistoryCandles", default is "Candles" for recent candles, "HistoryCandles" for older candles + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + market = self.market(symbol) + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate') + if paginate: + return self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params, 200) + priceType = self.safe_string(params, 'price') + isMarkOrIndex = self.in_array(priceType, ['mark', 'index']) + params = self.omit(params, 'price') + options = self.safe_dict(self.options, 'fetchOHLCV', {}) + timezone = self.safe_string(options, 'timezone', 'UTC') + limitIsUndefined = (limit is None) + if limit is None: + limit = 100 # default 100, max 300 + else: + maxLimit = 100 if isMarkOrIndex else 300 # default 300, only 100 if 'mark' or 'index' + limit = min(limit, maxLimit) + duration = self.parse_timeframe(timeframe) + bar = self.safe_string(self.timeframes, timeframe, timeframe) + if (timezone == 'UTC') and (duration >= 21600): # if utc and timeframe >= 6h + bar += timezone.lower() + request: dict = { + 'instId': market['id'], + 'bar': bar, + 'limit': limit, + } + defaultType = 'Candles' + if since is not None: + now = self.milliseconds() + durationInMilliseconds = duration * 1000 + # switch to history candles if since is past the cutoff for current candles + historyBorder = now - ((1440 - 1) * durationInMilliseconds) + if since < historyBorder: + defaultType = 'HistoryCandles' + maxLimit = 100 if isMarkOrIndex else 300 + limit = min(limit, maxLimit) + startTime = max(since - 1, 0) + request['before'] = startTime + request['after'] = self.sum(since, durationInMilliseconds * limit) + until = self.safe_integer(params, 'until') + if until is not None: + request['after'] = until + params = self.omit(params, 'until') + defaultType = self.safe_string(options, 'type', defaultType) # Candles or HistoryCandles + type = self.safe_string(params, 'type', defaultType) + params = self.omit(params, 'type') + isHistoryCandles = (type == 'HistoryCandles') + response = None + if priceType == 'mark': + if isHistoryCandles: + response = self.publicGetMarketHistoryMarkPriceCandles(self.extend(request, params)) + else: + response = self.publicGetMarketMarkPriceCandles(self.extend(request, params)) + elif priceType == 'index': + request['instId'] = market['info']['instFamily'] # okx index candles require instFamily instead of instId + if isHistoryCandles: + response = self.publicGetMarketHistoryIndexCandles(self.extend(request, params)) + else: + response = self.publicGetMarketIndexCandles(self.extend(request, params)) + else: + if isHistoryCandles: + if limitIsUndefined and (limit == 100): + limit = 300 + request['limit'] = 300 # reassign to 300, but self whole logic needs to be simplified... + response = self.publicGetMarketHistoryCandles(self.extend(request, params)) + else: + response = self.publicGetMarketCandles(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # ["1678928760000","24341.4","24344","24313.2","24323","628","2.5819","62800","0"], + # ["1678928700000","24324.1","24347.6","24321.7","24341.4","2565","10.5401","256500","1"], + # ["1678928640000","24300.2","24324.1","24288","24324.1","3304","13.5937","330400","1"], + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_ohlcvs(data, market, timeframe, since, limit) + + def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical funding rate prices + + https://www.okx.com/docs-v5/en/#public-data-rest-api-get-funding-rate-history + + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: timestamp in ms of the earliest funding rate to fetch + :param int [limit]: the maximum amount of `funding rate structures ` to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `funding rate structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchFundingRateHistory() requires a symbol argument') + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchFundingRateHistory', 'paginate') + if paginate: + return self.fetch_paginated_call_deterministic('fetchFundingRateHistory', symbol, since, limit, '8h', params, 100) + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + } + if since is not None: + request['before'] = max(since - 1, 0) + if limit is not None: + request['limit'] = limit + response = self.publicGetPublicFundingRateHistory(self.extend(request, params)) + # + # { + # "code":"0", + # "msg":"", + # "data":[ + # { + # "instType":"SWAP", + # "instId":"BTC-USDT-SWAP", + # "fundingRate":"0.018", + # "realizedRate":"0.017", + # "fundingTime":"1597026383085" + # }, + # { + # "instType":"SWAP", + # "instId":"BTC-USDT-SWAP", + # "fundingRate":"0.018", + # "realizedRate":"0.017", + # "fundingTime":"1597026383085" + # } + # ] + # } + # + rates = [] + data = self.safe_list(response, 'data', []) + for i in range(0, len(data)): + rate = data[i] + timestamp = self.safe_integer(rate, 'fundingTime') + rates.append({ + 'info': rate, + 'symbol': self.safe_symbol(self.safe_string(rate, 'instId')), + 'fundingRate': self.safe_number(rate, 'realizedRate'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + sorted = self.sort_by(rates, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, market['symbol'], since, limit) + + def parse_balance_by_type(self, type, response): + if type == 'funding': + return self.parse_funding_balance(response) + else: + return self.parse_trading_balance(response) + + def parse_trading_balance(self, response): + result: dict = {'info': response} + data = self.safe_list(response, 'data', []) + first = self.safe_dict(data, 0, {}) + timestamp = self.safe_integer(first, 'uTime') + details = self.safe_list(first, 'details', []) + for i in range(0, len(details)): + balance = details[i] + currencyId = self.safe_string(balance, 'ccy') + code = self.safe_currency_code(currencyId) + account = self.account() + # it may be incorrect to use total, free and used for swap accounts + eq = self.safe_string(balance, 'eq') + availEq = self.safe_string(balance, 'availEq') + account['total'] = eq + if availEq is None: + account['free'] = self.safe_string(balance, 'availBal') + account['used'] = self.safe_string(balance, 'frozenBal') + else: + account['free'] = availEq + result[code] = account + result['timestamp'] = timestamp + result['datetime'] = self.iso8601(timestamp) + return self.safe_balance(result) + + def parse_funding_balance(self, response): + result: dict = {'info': response} + data = self.safe_list(response, 'data', []) + for i in range(0, len(data)): + balance = data[i] + currencyId = self.safe_string(balance, 'ccy') + code = self.safe_currency_code(currencyId) + account = self.account() + # it may be incorrect to use total, free and used for swap accounts + account['total'] = self.safe_string(balance, 'bal') + account['free'] = self.safe_string(balance, 'availBal') + account['used'] = self.safe_string(balance, 'frozenBal') + result[code] = account + return self.safe_balance(result) + + def parse_trading_fee(self, fee: dict, market: Market = None) -> TradingFeeInterface: + # https://www.okx.com/docs-v5/en/#rest-api-account-get-fee-rates + # + # { + # "category": "1", + # "delivery": "", + # "exercise": "", + # "instType": "SPOT", + # "level": "Lv1", + # "maker": "-0.0008", + # "taker": "-0.001", + # "ts": "1639043138472" + # } + # + return { + 'info': fee, + 'symbol': self.safe_symbol(None, market), + # OKX returns the fees values opposed to other exchanges, so the sign needs to be flipped + 'maker': self.parse_number(Precise.string_neg(self.safe_string_2(fee, 'maker', 'makerU'))), + 'taker': self.parse_number(Precise.string_neg(self.safe_string_2(fee, 'taker', 'takerU'))), + 'percentage': None, + 'tierBased': None, + } + + def fetch_trading_fee(self, symbol: str, params={}) -> TradingFeeInterface: + """ + fetch the trading fees for a market + + https://www.okx.com/docs-v5/en/#trading-account-rest-api-get-fee-rates + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `fee structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'instType': self.convert_to_instrument_type(market['type']), # SPOT, MARGIN, SWAP, FUTURES, OPTION + # "instId": market["id"], # only applicable to SPOT/MARGIN + # "uly": market["id"], # only applicable to FUTURES/SWAP/OPTION + # "category": "1", # 1 = Class A, 2 = Class B, 3 = Class C, 4 = Class D + } + if market['spot']: + request['instId'] = market['id'] + elif market['swap'] or market['future'] or market['option']: + request['uly'] = market['baseId'] + '-' + market['quoteId'] + else: + raise NotSupported(self.id + ' fetchTradingFee() supports spot, swap, future or option markets only') + response = self.privateGetAccountTradeFee(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "category": "1", + # "delivery": "", + # "exercise": "", + # "instType": "SPOT", + # "level": "Lv1", + # "maker": "-0.0008", + # "taker": "-0.001", + # "ts": "1639043138472" + # } + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + first = self.safe_dict(data, 0, {}) + return self.parse_trading_fee(first, market) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://www.okx.com/docs-v5/en/#funding-account-rest-api-get-balance + https://www.okx.com/docs-v5/en/#trading-account-rest-api-get-balance + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: wallet type, ['funding' or 'trading'] default is 'trading' + :returns dict: a `balance structure ` + """ + self.load_markets() + marketType, query = self.handle_market_type_and_params('fetchBalance', None, params) + request: dict = { + # 'ccy': 'BTC,ETH', # comma-separated list of currency ids + } + response = None + if marketType == 'funding': + response = self.privateGetAssetBalances(self.extend(request, query)) + else: + response = self.privateGetAccountBalance(self.extend(request, query)) + # + # { + # "code": "0", + # "data": [ + # { + # "adjEq": "", + # "details": [ + # { + # "availBal": "", + # "availEq": "28.21006347", + # "cashBal": "28.21006347", + # "ccy": "USDT", + # "crossLiab": "", + # "disEq": "28.2687404020176", + # "eq":"28 .21006347", + # "eqUsd": "28.2687404020176", + # "frozenBal": "0", + # "interest": "", + # "isoEq": "0", + # "isoLiab": "", + # "liab": "", + # "maxLoan": "", + # "mgnRatio": "", + # "notionalLever": "0", + # "ordFrozen": "0", + # "twap": "0", + # "uTime": "1621556539861", + # "upl": "0", + # "uplLiab": "" + # } + # ], + # "imr": "", + # "isoEq": "0", + # "mgnRatio": "", + # "mmr": "", + # "notionalUsd": "", + # "ordFroz": "", + # "totalEq": "28.2687404020176", + # "uTime": "1621556553510" + # } + # ], + # "msg": "" + # } + # + # { + # "code": "0", + # "data": [ + # { + # "adjEq": "", + # "details": [ + # { + # "availBal": "0.049", + # "availEq": "", + # "cashBal": "0.049", + # "ccy": "BTC", + # "crossLiab": "", + # "disEq": "1918.55678", + # "eq": "0.049", + # "eqUsd": "1918.55678", + # "frozenBal": "0", + # "interest": "", + # "isoEq": "", + # "isoLiab": "", + # "liab": "", + # "maxLoan": "", + # "mgnRatio": "", + # "notionalLever": "", + # "ordFrozen": "0", + # "twap": "0", + # "uTime": "1621973128591", + # "upl": "", + # "uplLiab": "" + # } + # ], + # "imr": "", + # "isoEq": "", + # "mgnRatio": "", + # "mmr": "", + # "notionalUsd": "", + # "ordFroz": "", + # "totalEq": "1918.55678", + # "uTime": "1622045126908" + # } + # ], + # "msg": "" + # } + # + # funding + # + # { + # "code": "0", + # "data": [ + # { + # "availBal": "0.00005426", + # "bal": 0.0000542600000000, + # "ccy": "BTC", + # "frozenBal": "0" + # } + # ], + # "msg": "" + # } + # + return self.parse_balance_by_type(marketType, response) + + def create_market_buy_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market buy order by providing the symbol and cost + + https://www.okx.com/docs-v5/en/#order-book-trading-trade-post-place-order + + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' createMarketBuyOrderWithCost() supports spot markets only') + req = { + 'createMarketBuyOrderRequiresPrice': False, + 'tgtCcy': 'quote_ccy', + } + return self.create_order(symbol, 'market', 'buy', cost, None, self.extend(req, params)) + + def create_market_sell_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market buy order by providing the symbol and cost + + https://www.okx.com/docs-v5/en/#order-book-trading-trade-post-place-order + + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' createMarketSellOrderWithCost() supports spot markets only') + req = { + 'createMarketBuyOrderRequiresPrice': False, + 'tgtCcy': 'quote_ccy', + } + return self.create_order(symbol, 'market', 'sell', cost, None, self.extend(req, params)) + + def create_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + # 'ccy': currency['id'], # only applicable to cross MARGIN orders in single-currency margin + # 'clOrdId': clientOrderId, # up to 32 characters, must be unique + # 'tag': tag, # up to 8 characters + 'side': side, + # 'posSide': 'long', # long, short, # required in the long/short mode, and can only be long or short(only for future or swap) + 'ordType': type, + # 'ordType': type, # privatePostTradeOrder: market, limit, post_only, fok, ioc, optimal_limit_ioc + # 'ordType': type, # privatePostTradeOrderAlgo: conditional, oco, trigger, move_order_stop, iceberg, twap + 'sz': self.amount_to_precision(symbol, amount), + # 'px': self.price_to_precision(symbol, price), # limit orders only + # 'reduceOnly': False, + # + # 'triggerPx': 10, # stopPrice(trigger orders) + # 'orderPx': 10, # Order price if -1, the order will be executed at the market price.(trigger orders) + # 'triggerPxType': 'last', # Conditional default is last, mark or index(trigger orders) + # + # 'tpTriggerPx': 10, # takeProfitPrice(conditional orders) + # 'tpTriggerPxType': 'last', # Conditional default is last, mark or index(conditional orders) + # 'tpOrdPx': 10, # Order price for Take-Profit orders, if -1 will be executed at market price(conditional orders) + # + # 'slTriggerPx': 10, # stopLossPrice(conditional orders) + # 'slTriggerPxType': 'last', # Conditional default is last, mark or index(conditional orders) + # 'slOrdPx': 10, # Order price for Stop-Loss orders, if -1 will be executed at market price(conditional orders) + } + spot = market['spot'] + contract = market['contract'] + triggerPrice = self.safe_value_n(params, ['triggerPrice', 'stopPrice', 'triggerPx']) + timeInForce = self.safe_string(params, 'timeInForce', 'GTC') + takeProfitPrice = self.safe_value_2(params, 'takeProfitPrice', 'tpTriggerPx') + tpOrdPx = self.safe_value(params, 'tpOrdPx', price) + tpTriggerPxType = self.safe_string(params, 'tpTriggerPxType', 'last') + stopLossPrice = self.safe_value_2(params, 'stopLossPrice', 'slTriggerPx') + slOrdPx = self.safe_value(params, 'slOrdPx', price) + slTriggerPxType = self.safe_string(params, 'slTriggerPxType', 'last') + clientOrderId = self.safe_string_2(params, 'clOrdId', 'clientOrderId') + stopLoss = self.safe_value(params, 'stopLoss') + stopLossDefined = (stopLoss is not None) + takeProfit = self.safe_value(params, 'takeProfit') + takeProfitDefined = (takeProfit is not None) + trailingPercent = self.safe_string_2(params, 'trailingPercent', 'callbackRatio') + isTrailingPercentOrder = trailingPercent is not None + trailingPrice = self.safe_string_2(params, 'trailingPrice', 'callbackSpread') + isTrailingPriceOrder = trailingPrice is not None + trigger = (triggerPrice is not None) or (type == 'trigger') + isReduceOnly = self.safe_value(params, 'reduceOnly', False) + defaultMarginMode = self.safe_string_2(self.options, 'defaultMarginMode', 'marginMode', 'cross') + marginMode = self.safe_string_2(params, 'marginMode', 'tdMode') # cross or isolated, tdMode not ommited so be extended into the request + margin = False + if (marginMode is not None) and (marginMode != 'cash'): + margin = True + else: + marginMode = defaultMarginMode + margin = self.safe_bool(params, 'margin', False) + if spot: + if margin: + defaultCurrency = market['quote'] if (side == 'buy') else market['base'] + currency = self.safe_string(params, 'ccy', defaultCurrency) + request['ccy'] = self.safe_currency_code(currency) + tradeMode = marginMode if margin else 'cash' + request['tdMode'] = tradeMode + elif contract: + if market['swap'] or market['future']: + positionSide = None + positionSide, params = self.handle_option_and_params(params, 'createOrder', 'positionSide') + if positionSide is not None: + request['posSide'] = positionSide + else: + hedged = None + hedged, params = self.handle_option_and_params(params, 'createOrder', 'hedged') + if hedged: + isBuy = (side == 'buy') + isProtective = (takeProfitPrice is not None) or (stopLossPrice is not None) or isReduceOnly + if isProtective: + # in case of protective orders, the posSide should be opposite of position side + # reduceOnly is emulated and not natively supported by the exchange + request['posSide'] = 'short' if isBuy else 'long' + if isReduceOnly: + params = self.omit(params, 'reduceOnly') + else: + request['posSide'] = 'long' if isBuy else 'short' + request['tdMode'] = marginMode + isMarketOrder = type == 'market' + postOnly = False + postOnly, params = self.handle_post_only(isMarketOrder, type == 'post_only', params) + params = self.omit(params, ['currency', 'ccy', 'marginMode', 'timeInForce', 'stopPrice', 'triggerPrice', 'clientOrderId', 'stopLossPrice', 'takeProfitPrice', 'slOrdPx', 'tpOrdPx', 'margin', 'stopLoss', 'takeProfit', 'trailingPercent']) + ioc = (timeInForce == 'IOC') or (type == 'ioc') + fok = (timeInForce == 'FOK') or (type == 'fok') + conditional = (stopLossPrice is not None) or (takeProfitPrice is not None) or (type == 'conditional') + marketIOC = (isMarketOrder and ioc) or (type == 'optimal_limit_ioc') + defaultTgtCcy = self.safe_string(self.options, 'tgtCcy', 'base_ccy') + tgtCcy = self.safe_string(params, 'tgtCcy', defaultTgtCcy) + if (not contract) and (not margin): + request['tgtCcy'] = tgtCcy + if isMarketOrder or marketIOC: + request['ordType'] = 'market' + if spot and (side == 'buy'): + # spot market buy: "sz" can refer either to base currency units or to quote currency units + # see documentation: https://www.okx.com/docs-v5/en/#rest-api-trade-place-order + if tgtCcy == 'quote_ccy': + # quote_ccy: sz refers to units of quote currency + createMarketBuyOrderRequiresPrice = True + createMarketBuyOrderRequiresPrice, params = self.handle_option_and_params(params, 'createOrder', 'createMarketBuyOrderRequiresPrice', True) + notional = self.safe_number_2(params, 'cost', 'sz') + params = self.omit(params, ['cost', 'sz']) + if createMarketBuyOrderRequiresPrice: + if price is not None: + if notional is None: + amountString = self.number_to_string(amount) + priceString = self.number_to_string(price) + quoteAmount = Precise.string_mul(amountString, priceString) + notional = self.parse_number(quoteAmount) + elif notional is None: + raise InvalidOrder(self.id + " createOrder() requires the price argument with market buy orders to calculate total order cost(amount to spend), where cost = amount * price. Supply a price argument to createOrder() call if you want the cost to be calculated for you from price and amount, or, alternatively, add .options['createMarketBuyOrderRequiresPrice'] = False and supply the total cost value in the 'amount' argument or in the 'cost' unified extra parameter or in exchange-specific 'sz' extra parameter(the exchange-specific behaviour)") + else: + notional = amount if (notional is None) else notional + request['sz'] = self.cost_to_precision(symbol, notional) + if marketIOC and contract: + request['ordType'] = 'optimal_limit_ioc' + else: + if (not trigger) and (not conditional): + request['px'] = self.price_to_precision(symbol, price) + if postOnly: + request['ordType'] = 'post_only' + elif ioc and not marketIOC: + request['ordType'] = 'ioc' + elif fok: + request['ordType'] = 'fok' + if isTrailingPercentOrder: + convertedTrailingPercent = Precise.string_div(trailingPercent, '100') + request['callbackRatio'] = convertedTrailingPercent + request['ordType'] = 'move_order_stop' + elif isTrailingPriceOrder: + request['callbackSpread'] = trailingPrice + request['ordType'] = 'move_order_stop' + elif stopLossDefined or takeProfitDefined: + attachAlgoOrd = {} + if stopLossDefined: + stopLossTriggerPrice = self.safe_value_n(stopLoss, ['triggerPrice', 'stopPrice', 'slTriggerPx']) + if stopLossTriggerPrice is None: + raise InvalidOrder(self.id + ' createOrder() requires a trigger price in params["stopLoss"]["triggerPrice"], or params["stopLoss"]["stopPrice"], or params["stopLoss"]["slTriggerPx"] for a stop loss order') + slTriggerPx = self.price_to_precision(symbol, stopLossTriggerPrice) + slOrder = {} + slOrder['slTriggerPx'] = slTriggerPx + stopLossLimitPrice = self.safe_value_n(stopLoss, ['price', 'stopLossPrice', 'slOrdPx']) + stopLossOrderType = self.safe_string(stopLoss, 'type') + if stopLossOrderType is not None: + stopLossLimitOrderType = (stopLossOrderType == 'limit') + stopLossMarketOrderType = (stopLossOrderType == 'market') + if (not stopLossLimitOrderType) and (not stopLossMarketOrderType): + raise InvalidOrder(self.id + ' createOrder() params["stopLoss"]["type"] must be either "limit" or "market"') + elif stopLossLimitOrderType: + if stopLossLimitPrice is None: + raise InvalidOrder(self.id + ' createOrder() requires a limit price in params["stopLoss"]["price"] or params["stopLoss"]["slOrdPx"] for a stop loss limit order') + else: + slOrder['slOrdPx'] = self.price_to_precision(symbol, stopLossLimitPrice) + elif stopLossOrderType == 'market': + slOrder['slOrdPx'] = '-1' + elif stopLossLimitPrice is not None: + slOrder['slOrdPx'] = self.price_to_precision(symbol, stopLossLimitPrice) # limit sl order + else: + slOrder['slOrdPx'] = '-1' # market sl order + stopLossTriggerPriceType = self.safe_string_2(stopLoss, 'triggerPriceType', 'slTriggerPxType', 'last') + if stopLossTriggerPriceType is not None: + if (stopLossTriggerPriceType != 'last') and (stopLossTriggerPriceType != 'index') and (stopLossTriggerPriceType != 'mark'): + raise InvalidOrder(self.id + ' createOrder() stop loss trigger price type must be one of "last", "index" or "mark"') + slOrder['slTriggerPxType'] = stopLossTriggerPriceType + attachAlgoOrd = self.extend(attachAlgoOrd, slOrder) + if takeProfitDefined: + takeProfitTriggerPrice = self.safe_value_n(takeProfit, ['triggerPrice', 'stopPrice', 'tpTriggerPx']) + if takeProfitTriggerPrice is None: + raise InvalidOrder(self.id + ' createOrder() requires a trigger price in params["takeProfit"]["triggerPrice"], or params["takeProfit"]["stopPrice"], or params["takeProfit"]["tpTriggerPx"] for a take profit order') + tpOrder = {} + tpOrder['tpTriggerPx'] = self.price_to_precision(symbol, takeProfitTriggerPrice) + takeProfitLimitPrice = self.safe_value_n(takeProfit, ['price', 'takeProfitPrice', 'tpOrdPx']) + takeProfitOrderType = self.safe_string_2(takeProfit, 'type', 'tpOrdKind') + if takeProfitOrderType is not None: + takeProfitLimitOrderType = (takeProfitOrderType == 'limit') + takeProfitMarketOrderType = (takeProfitOrderType == 'market') + if (not takeProfitLimitOrderType) and (not takeProfitMarketOrderType): + raise InvalidOrder(self.id + ' createOrder() params["takeProfit"]["type"] must be either "limit" or "market"') + elif takeProfitLimitOrderType: + if takeProfitLimitPrice is None: + raise InvalidOrder(self.id + ' createOrder() requires a limit price in params["takeProfit"]["price"] or params["takeProfit"]["tpOrdPx"] for a take profit limit order') + else: + tpOrder['tpOrdKind'] = takeProfitOrderType + tpOrder['tpOrdPx'] = self.price_to_precision(symbol, takeProfitLimitPrice) + elif takeProfitOrderType == 'market': + tpOrder['tpOrdPx'] = '-1' + elif takeProfitLimitPrice is not None: + tpOrder['tpOrdKind'] = 'limit' + tpOrder['tpOrdPx'] = self.price_to_precision(symbol, takeProfitLimitPrice) # limit tp order + else: + tpOrder['tpOrdPx'] = '-1' # market tp order + takeProfitTriggerPriceType = self.safe_string_2(takeProfit, 'triggerPriceType', 'tpTriggerPxType', 'last') + if takeProfitTriggerPriceType is not None: + if (takeProfitTriggerPriceType != 'last') and (takeProfitTriggerPriceType != 'index') and (takeProfitTriggerPriceType != 'mark'): + raise InvalidOrder(self.id + ' createOrder() take profit trigger price type must be one of "last", "index" or "mark"') + tpOrder['tpTriggerPxType'] = takeProfitTriggerPriceType + attachAlgoOrd = self.extend(attachAlgoOrd, tpOrder) + attachOrdKeys = list(attachAlgoOrd.keys()) + attachOrdLen = len(attachOrdKeys) + if attachOrdLen > 0: + request['attachAlgoOrds'] = [attachAlgoOrd] + # algo order details + if trigger: + request['ordType'] = 'trigger' + request['triggerPx'] = self.price_to_precision(symbol, triggerPrice) + request['orderPx'] = '-1' if isMarketOrder else self.price_to_precision(symbol, price) + elif conditional: + request['ordType'] = 'conditional' + twoWayCondition = ((takeProfitPrice is not None) and (stopLossPrice is not None)) + # if TP and SL are sent together + # 'conditional' only stop-loss order will be applied + # tpOrdKind is 'condition' which is the default + if twoWayCondition: + request['ordType'] = 'oco' + if side == 'sell': + request = self.omit(request, 'tgtCcy') + if self.safe_string(request, 'tdMode') == 'cash': + # for some reason tdMode = cash throws + # {"code":"1","data":[{"algoClOrdId":"","algoId":"","clOrdId":"","sCode":"51000","sMsg":"Parameter tdMode error ","tag":""}],"msg":""} + request['tdMode'] = marginMode + if takeProfitPrice is not None: + request['tpTriggerPx'] = self.price_to_precision(symbol, takeProfitPrice) + tpOrdPxReq = '-1' + if tpOrdPx is not None: + tpOrdPxReq = self.price_to_precision(symbol, tpOrdPx) + request['tpOrdPx'] = tpOrdPxReq + request['tpTriggerPxType'] = tpTriggerPxType + if stopLossPrice is not None: + request['slTriggerPx'] = self.price_to_precision(symbol, stopLossPrice) + slOrdPxReq = '-1' + if slOrdPx is not None: + slOrdPxReq = self.price_to_precision(symbol, slOrdPx) + request['slOrdPx'] = slOrdPxReq + request['slTriggerPxType'] = slTriggerPxType + if clientOrderId is None: + brokerId = self.safe_string(self.options, 'brokerId') + if brokerId is not None: + request['clOrdId'] = brokerId + self.uuid16() + request['tag'] = brokerId + else: + request['clOrdId'] = clientOrderId + params = self.omit(params, ['clOrdId', 'clientOrderId']) + return self.extend(request, params) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://www.okx.com/docs-v5/en/#order-book-trading-trade-post-place-order + https://www.okx.com/docs-v5/en/#order-book-trading-trade-post-place-multiple-orders + https://www.okx.com/docs-v5/en/#order-book-trading-algo-trading-post-place-algo-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.reduceOnly]: a mark to reduce the position size for margin, swap and future orders + :param bool [params.postOnly]: True to place a post only order + :param dict [params.takeProfit]: *takeProfit object in params* containing the triggerPrice at which the attached take profit order will be triggered(perpetual swap markets only) + :param float [params.takeProfit.triggerPrice]: take profit trigger price + :param float [params.takeProfit.price]: used for take profit limit orders, not used for take profit market price orders + :param str [params.takeProfit.type]: 'market' or 'limit' used to specify the take profit price type + :param dict [params.stopLoss]: *stopLoss object in params* containing the triggerPrice at which the attached stop loss order will be triggered(perpetual swap markets only) + :param float [params.stopLoss.triggerPrice]: stop loss trigger price + :param float [params.stopLoss.price]: used for stop loss limit orders, not used for stop loss market price orders + :param str [params.stopLoss.type]: 'market' or 'limit' used to specify the stop loss price type + :param str [params.positionSide]: if position mode is one-way: set to 'net', if position mode is hedge-mode: set to 'long' or 'short' + :param str [params.trailingPercent]: the percent to trail away from the current market price + :param str [params.tpOrdKind]: 'condition' or 'limit', the default is 'condition' + :param bool [params.hedged]: *swap and future only* True for hedged mode, False for one way mode + :param str [params.marginMode]: 'cross' or 'isolated', the default is 'cross' + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + request = self.create_order_request(symbol, type, side, amount, price, params) + method = self.safe_string(self.options, 'createOrder', 'privatePostTradeBatchOrders') + requestOrdType = self.safe_string(request, 'ordType') + if (requestOrdType == 'trigger') or (requestOrdType == 'conditional') or (requestOrdType == 'move_order_stop') or (type == 'move_order_stop') or (type == 'oco') or (type == 'iceberg') or (type == 'twap'): + method = 'privatePostTradeOrderAlgo' + if (method != 'privatePostTradeOrder') and (method != 'privatePostTradeOrderAlgo') and (method != 'privatePostTradeBatchOrders'): + raise ExchangeError(self.id + ' createOrder() self.options["createOrder"] must be either privatePostTradeBatchOrders or privatePostTradeOrder or privatePostTradeOrderAlgo') + if method == 'privatePostTradeBatchOrders': + # keep the request body the same + # submit a single order in an array to the batch order endpoint + # because it has a lower ratelimit + request = [request] + response = None + if method == 'privatePostTradeOrder': + response = self.privatePostTradeOrder(request) + elif method == 'privatePostTradeOrderAlgo': + response = self.privatePostTradeOrderAlgo(request) + else: + response = self.privatePostTradeBatchOrders(request) + data = self.safe_list(response, 'data', []) + first = self.safe_dict(data, 0, {}) + order = self.parse_order(first, market) + order['type'] = type + order['side'] = side + return order + + def create_orders(self, orders: List[OrderRequest], params={}): + """ + create a list of trade orders + + https://www.okx.com/docs-v5/en/#order-book-trading-trade-post-place-multiple-orders + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + ordersRequests = [] + for i in range(0, len(orders)): + rawOrder = orders[i] + marketId = self.safe_string(rawOrder, 'symbol') + type = self.safe_string(rawOrder, 'type') + side = self.safe_string(rawOrder, 'side') + amount = self.safe_value(rawOrder, 'amount') + price = self.safe_value(rawOrder, 'price') + orderParams = self.safe_dict(rawOrder, 'params', {}) + extendedParams = self.extend(orderParams, params) # the request does not accept extra params since it's a list, so we're extending each order with the common params + orderRequest = self.create_order_request(marketId, type, side, amount, price, extendedParams) + ordersRequests.append(orderRequest) + response = self.privatePostTradeBatchOrders(ordersRequests) + # { + # "code": "0", + # "data": [ + # { + # "clOrdId": "e847386590ce4dBCc7f2a1b4c4509f82", + # "ordId": "636305438765568000", + # "sCode": "0", + # "sMsg": "Order placed", + # "tag": "e847386590ce4dBC" + # }, + # { + # "clOrdId": "e847386590ce4dBC0b9993fe642d8f62", + # "ordId": "636305438765568001", + # "sCode": "0", + # "sMsg": "Order placed", + # "tag": "e847386590ce4dBC" + # } + # ], + # "inTime": "1697979038584486", + # "msg": "", + # "outTime": "1697979038586493" + # } + data = self.safe_list(response, 'data', []) + return self.parse_orders(data) + + def edit_order_request(self, id: str, symbol, type, side, amount=None, price=None, params={}): + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + } + isAlgoOrder = None + if (type == 'trigger') or (type == 'conditional') or (type == 'move_order_stop') or (type == 'oco') or (type == 'iceberg') or (type == 'twap'): + isAlgoOrder = True + clientOrderId = self.safe_string_2(params, 'clOrdId', 'clientOrderId') + if clientOrderId is not None: + if isAlgoOrder: + request['algoClOrdId'] = clientOrderId + else: + request['clOrdId'] = clientOrderId + else: + if isAlgoOrder: + request['algoId'] = id + else: + request['ordId'] = id + stopLossTriggerPrice = self.safe_value_2(params, 'stopLossPrice', 'newSlTriggerPx') + stopLossPrice = self.safe_value(params, 'newSlOrdPx') + stopLossTriggerPriceType = self.safe_string(params, 'newSlTriggerPxType', 'last') + takeProfitTriggerPrice = self.safe_value_2(params, 'takeProfitPrice', 'newTpTriggerPx') + takeProfitPrice = self.safe_value(params, 'newTpOrdPx') + takeProfitTriggerPriceType = self.safe_string(params, 'newTpTriggerPxType', 'last') + stopLoss = self.safe_value(params, 'stopLoss') + takeProfit = self.safe_value(params, 'takeProfit') + stopLossDefined = (stopLoss is not None) + takeProfitDefined = (takeProfit is not None) + if isAlgoOrder: + if (stopLossTriggerPrice is None) and (takeProfitTriggerPrice is None): + raise BadRequest(self.id + ' editOrder() requires a stopLossPrice or takeProfitPrice parameter for editing an algo order') + if stopLossTriggerPrice is not None: + if stopLossPrice is None: + raise BadRequest(self.id + ' editOrder() requires a newSlOrdPx parameter for editing an algo order') + request['newSlTriggerPx'] = self.price_to_precision(symbol, stopLossTriggerPrice) + request['newSlOrdPx'] = '-1' if (type == 'market') else self.price_to_precision(symbol, stopLossPrice) + request['newSlTriggerPxType'] = stopLossTriggerPriceType + if takeProfitTriggerPrice is not None: + if takeProfitPrice is None: + raise BadRequest(self.id + ' editOrder() requires a newTpOrdPx parameter for editing an algo order') + request['newTpTriggerPx'] = self.price_to_precision(symbol, takeProfitTriggerPrice) + request['newTpOrdPx'] = '-1' if (type == 'market') else self.price_to_precision(symbol, takeProfitPrice) + request['newTpTriggerPxType'] = takeProfitTriggerPriceType + else: + if stopLossTriggerPrice is not None: + request['newSlTriggerPx'] = self.price_to_precision(symbol, stopLossTriggerPrice) + request['newSlOrdPx'] = '-1' if (type == 'market') else self.price_to_precision(symbol, stopLossPrice) + request['newSlTriggerPxType'] = stopLossTriggerPriceType + if takeProfitTriggerPrice is not None: + request['newTpTriggerPx'] = self.price_to_precision(symbol, takeProfitTriggerPrice) + request['newTpOrdPx'] = '-1' if (type == 'market') else self.price_to_precision(symbol, takeProfitPrice) + request['newTpTriggerPxType'] = takeProfitTriggerPriceType + if stopLossDefined: + stopLossTriggerPrice = self.safe_value(stopLoss, 'triggerPrice') + stopLossPrice = self.safe_value(stopLoss, 'price') + stopLossType = self.safe_string(stopLoss, 'type') + request['newSlTriggerPx'] = self.price_to_precision(symbol, stopLossTriggerPrice) + request['newSlOrdPx'] = '-1' if (stopLossType == 'market') else self.price_to_precision(symbol, stopLossPrice) + request['newSlTriggerPxType'] = stopLossTriggerPriceType + if takeProfitDefined: + takeProfitTriggerPrice = self.safe_value(takeProfit, 'triggerPrice') + takeProfitPrice = self.safe_value(takeProfit, 'price') + takeProfitType = self.safe_string(takeProfit, 'type') + request['newTpOrdKind'] = takeProfitType if (takeProfitType == 'limit') else 'condition' + request['newTpTriggerPx'] = self.price_to_precision(symbol, takeProfitTriggerPrice) + request['newTpOrdPx'] = '-1' if (takeProfitType == 'market') else self.price_to_precision(symbol, takeProfitPrice) + request['newTpTriggerPxType'] = takeProfitTriggerPriceType + if amount is not None: + request['newSz'] = self.amount_to_precision(symbol, amount) + if not isAlgoOrder: + if price is not None: + request['newPx'] = self.price_to_precision(symbol, price) + params = self.omit(params, ['clOrdId', 'clientOrderId', 'takeProfitPrice', 'stopLossPrice', 'stopLoss', 'takeProfit', 'postOnly']) + return self.extend(request, params) + + def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + """ + edit a trade order + + https://www.okx.com/docs-v5/en/#order-book-trading-trade-post-amend-order + https://www.okx.com/docs-v5/en/#order-book-trading-algo-trading-post-amend-algo-order + + :param str id: order id + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of the currency you want to trade in units of the base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.clientOrderId]: client order id, uses id if not passed + :param float [params.stopLossPrice]: stop loss trigger price + :param float [params.newSlOrdPx]: the stop loss order price, set to stopLossPrice if the type is market + :param str [params.newSlTriggerPxType]: 'last', 'index' or 'mark' used to specify the stop loss trigger price type, default is 'last' + :param float [params.takeProfitPrice]: take profit trigger price + :param float [params.newTpOrdPx]: the take profit order price, set to takeProfitPrice if the type is market + :param str [params.newTpTriggerPxType]: 'last', 'index' or 'mark' used to specify the take profit trigger price type, default is 'last' + :param dict [params.stopLoss]: *stopLoss object in params* containing the triggerPrice at which the attached stop loss order will be triggered + :param float [params.stopLoss.triggerPrice]: stop loss trigger price + :param float [params.stopLoss.price]: used for stop loss limit orders, not used for stop loss market price orders + :param str [params.stopLoss.type]: 'market' or 'limit' used to specify the stop loss price type + :param dict [params.takeProfit]: *takeProfit object in params* containing the triggerPrice at which the attached take profit order will be triggered + :param float [params.takeProfit.triggerPrice]: take profit trigger price + :param float [params.takeProfit.price]: used for take profit limit orders, not used for take profit market price orders + :param str [params.takeProfit.type]: 'market' or 'limit' used to specify the take profit price type + :param str [params.newTpOrdKind]: 'condition' or 'limit', the default is 'condition' + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + request = self.edit_order_request(id, symbol, type, side, amount, price, params) + isAlgoOrder = None + if (type == 'trigger') or (type == 'conditional') or (type == 'move_order_stop') or (type == 'oco') or (type == 'iceberg') or (type == 'twap'): + isAlgoOrder = True + response = None + if isAlgoOrder: + response = self.privatePostTradeAmendAlgos(self.extend(request, params)) + else: + response = self.privatePostTradeAmendOrder(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "clOrdId": "e847386590ce4dBCc1a045253497a547", + # "ordId": "559176536793178112", + # "reqId": "", + # "sCode": "0", + # "sMsg": "" + # } + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + first = self.safe_dict(data, 0, {}) + order = self.parse_order(first, market) + order['type'] = type + order['side'] = side + return order + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://www.okx.com/docs-v5/en/#order-book-trading-trade-post-cancel-order + https://www.okx.com/docs-v5/en/#order-book-trading-algo-trading-post-cancel-algo-order + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: True if trigger orders + :param boolean [params.trailing]: set to True if you want to cancel a trailing order + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + trigger = self.safe_value_2(params, 'stop', 'trigger') + trailing = self.safe_bool(params, 'trailing', False) + if trigger or trailing: + orderInner = self.cancel_orders([id], symbol, params) + return self.safe_dict(orderInner, 0) + self.load_markets() + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + # 'ordId': id, # either ordId or clOrdId is required + # 'clOrdId': clientOrderId, + } + clientOrderId = self.safe_string_2(params, 'clOrdId', 'clientOrderId') + if clientOrderId is not None: + request['clOrdId'] = clientOrderId + else: + request['ordId'] = id + query = self.omit(params, ['clOrdId', 'clientOrderId']) + response = self.privatePostTradeCancelOrder(self.extend(request, query)) + # {"code":"0","data":[{"clOrdId":"","ordId":"317251910906576896","sCode":"0","sMsg":""}],"msg":""} + data = self.safe_value(response, 'data', []) + order = self.safe_dict(data, 0) + return self.parse_order(order, market) + + def parse_ids(self, ids): + """ + @ignore + :param string[]|str ids: order ids + :returns str[]: list of order ids + """ + if (ids is not None) and isinstance(ids, str): + return ids.split(',') + else: + return ids + + def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): + """ + cancel multiple orders + + https://www.okx.com/docs-v5/en/#order-book-trading-trade-post-cancel-multiple-orders + https://www.okx.com/docs-v5/en/#order-book-trading-algo-trading-post-cancel-algo-order + + :param str[] ids: order ids + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: whether the order is a stop/trigger order + :param boolean [params.trailing]: set to True if you want to cancel trailing orders + :returns dict: an list of `order structures ` + """ + # TODO : the original endpoint signature differs, according to that you can skip individual symbol and assign ids in batch. At self moment, `params` is not being used too. + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrders() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request = [] + options = self.safe_value(self.options, 'cancelOrders', {}) + defaultMethod = self.safe_string(options, 'method', 'privatePostTradeCancelBatchOrders') + method = self.safe_string(params, 'method', defaultMethod) + clientOrderIds = self.parse_ids(self.safe_value_2(params, 'clOrdId', 'clientOrderId')) + algoIds = self.parse_ids(self.safe_value(params, 'algoId')) + trigger = self.safe_value_2(params, 'stop', 'trigger') + trailing = self.safe_bool(params, 'trailing', False) + if trigger or trailing: + method = 'privatePostTradeCancelAlgos' + if clientOrderIds is None: + ids = self.parse_ids(ids) + if algoIds is not None: + for i in range(0, len(algoIds)): + request.append({ + 'algoId': algoIds[i], + 'instId': market['id'], + }) + for i in range(0, len(ids)): + if trailing or trigger: + request.append({ + 'algoId': ids[i], + 'instId': market['id'], + }) + else: + request.append({ + 'ordId': ids[i], + 'instId': market['id'], + }) + else: + for i in range(0, len(clientOrderIds)): + if trailing or trigger: + request.append({ + 'instId': market['id'], + 'algoClOrdId': clientOrderIds[i], + }) + else: + request.append({ + 'instId': market['id'], + 'clOrdId': clientOrderIds[i], + }) + response = None + if method == 'privatePostTradeCancelAlgos': + response = self.privatePostTradeCancelAlgos(request) # * dont self.extend with params, otherwise ARRAY will be turned into OBJECT + else: + response = self.privatePostTradeCancelBatchOrders(request) # * dont self.extend with params, otherwise ARRAY will be turned into OBJECT + # + # { + # "code": "0", + # "data": [ + # { + # "clOrdId": "e123456789ec4dBC1123456ba123b45e", + # "ordId": "405071912345641543", + # "sCode": "0", + # "sMsg": "" + # }, + # ... + # ], + # "msg": "" + # } + # + # Algo order + # + # { + # "code": "0", + # "data": [ + # { + # "algoId": "431375349042380800", + # "sCode": "0", + # "sMsg": "" + # } + # ], + # "msg": "" + # } + # + ordersData = self.safe_list(response, 'data', []) + return self.parse_orders(ordersData, market, None, None, params) + + def cancel_orders_for_symbols(self, orders: List[CancellationRequest], params={}): + """ + cancel multiple orders for multiple symbols + + https://www.okx.com/docs-v5/en/#order-book-trading-trade-post-cancel-multiple-orders + https://www.okx.com/docs-v5/en/#order-book-trading-algo-trading-post-cancel-algo-order + + :param CancellationRequest[] orders: each order should contain the parameters required by cancelOrder namely id and symbol, example [{"id": "a", "symbol": "BTC/USDT"}, {"id": "b", "symbol": "ETH/USDT"}] + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: whether the order is a stop/trigger order + :param boolean [params.trailing]: set to True if you want to cancel trailing orders + :returns dict: an list of `order structures ` + """ + self.load_markets() + request = [] + options = self.safe_dict(self.options, 'cancelOrders', {}) + defaultMethod = self.safe_string(options, 'method', 'privatePostTradeCancelBatchOrders') + method = self.safe_string(params, 'method', defaultMethod) + trigger = self.safe_bool_2(params, 'stop', 'trigger') + trailing = self.safe_bool(params, 'trailing', False) + isStopOrTrailing = trigger or trailing + if isStopOrTrailing: + method = 'privatePostTradeCancelAlgos' + for i in range(0, len(orders)): + order = orders[i] + id = self.safe_string(order, 'id') + clientOrderId = self.safe_string_2(order, 'clOrdId', 'clientOrderId') + symbol = self.safe_string(order, 'symbol') + market = self.market(symbol) + idKey = 'ordId' + if isStopOrTrailing: + idKey = 'algoId' + elif clientOrderId is not None: + if isStopOrTrailing: + idKey = 'algoClOrdId' + else: + idKey = 'clOrdId' + requestItem: dict = { + 'instId': market['id'], + } + requestItem[idKey] = clientOrderId if (clientOrderId is not None) else id + request.append(requestItem) + response = None + if method == 'privatePostTradeCancelAlgos': + response = self.privatePostTradeCancelAlgos(request) # * dont self.extend with params, otherwise ARRAY will be turned into OBJECT + else: + response = self.privatePostTradeCancelBatchOrders(request) # * dont self.extend with params, otherwise ARRAY will be turned into OBJECT + # + # { + # "code": "0", + # "data": [ + # { + # "clOrdId": "e123456789ec4dBC1123456ba123b45e", + # "ordId": "405071912345641543", + # "sCode": "0", + # "sMsg": "" + # }, + # ... + # ], + # "msg": "" + # } + # + # Algo order + # + # { + # "code": "0", + # "data": [ + # { + # "algoId": "431375349042380800", + # "sCode": "0", + # "sMsg": "" + # } + # ], + # "msg": "" + # } + # + ordersData = self.safe_list(response, 'data', []) + return self.parse_orders(ordersData, None, None, None, params) + + def cancel_all_orders_after(self, timeout: Int, params={}): + """ + dead man's switch, cancel all orders after the given timeout + + https://www.okx.com/docs-v5/en/#order-book-trading-trade-post-cancel-all-after + + :param number timeout: time in milliseconds, 0 represents cancel the timer + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: the api result + """ + self.load_markets() + request: dict = { + 'timeOut': self.parse_to_int(timeout / 1000) if (timeout > 0) else 0, + } + response = self.privatePostTradeCancelAllAfter(self.extend(request, params)) + # + # { + # "code":"0", + # "msg":"", + # "data":[ + # { + # "triggerTime":"1587971460", + # "ts":"1587971400" + # } + # ] + # } + # + return response + + def parse_order_status(self, status: Str): + statuses: dict = { + 'canceled': 'canceled', + 'order_failed': 'canceled', + 'live': 'open', + 'partially_filled': 'open', + 'filled': 'closed', + 'effective': 'closed', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder + # + # { + # "clOrdId": "oktswap6", + # "ordId": "312269865356374016", + # "tag": "", + # "sCode": "0", + # "sMsg": "" + # } + # + # editOrder + # + # { + # "clOrdId": "e847386590ce4dBCc1a045253497a547", + # "ordId": "559176536793178112", + # "reqId": "", + # "sCode": "0", + # "sMsg": "" + # } + # + # Spot and Swap fetchOrder, fetchOpenOrders + # + # { + # "accFillSz": "0", + # "avgPx": "", + # "cTime": "1621910749815", + # "category": "normal", + # "ccy": "", + # "clOrdId": "", + # "fee": "0", + # "feeCcy": "ETH", + # "fillPx": "", + # "fillSz": "0", + # "fillTime": "", + # "instId": "ETH-USDT", + # "instType": "SPOT", + # "lever": "", + # "ordId": "317251910906576896", + # "ordType": "limit", + # "pnl": "0", + # "posSide": "net", + # "px": "2000", + # "rebate": "0", + # "rebateCcy": "USDT", + # "side": "buy", + # "slOrdPx": "", + # "slTriggerPx": "", + # "state": "live", + # "sz": "0.001", + # "tag": "", + # "tdMode": "cash", + # "tpOrdPx": "", + # "tpTriggerPx": "", + # "tradeId": "", + # "uTime": "1621910749815" + # } + # + # watchOrders & fetchClosedOrders + # + # { + # "algoClOrdId": "", + # "algoId": "", + # "attachAlgoClOrdId": "", + # "attachAlgoOrds": [], + # "cancelSource": "", + # "cancelSourceReason": "", # not present in WS, but present in fetchClosedOrders + # "category": "normal", + # "ccy": "", # empty in WS, but eg. `USDT` in fetchClosedOrders + # "clOrdId": "", + # "cTime": "1751705801423", + # "feeCcy": "USDT", + # "instId": "LINK-USDT-SWAP", + # "instType": "SWAP", + # "isTpLimit": "false", + # "lever": "3", + # "linkedAlgoOrd": {"algoId": ""}, + # "ordId": "2657625147249614848", + # "ordType": "limit", + # "posSide": "net", + # "px": "13.142", + # "pxType": "", + # "pxUsd": "", + # "pxVol": "", + # "quickMgnType": "", + # "rebate": "0", + # "rebateCcy": "USDT", + # "reduceOnly": "true", + # "side": "sell", + # "slOrdPx": "", + # "slTriggerPx": "", + # "slTriggerPxType": "", + # "source": "", + # "stpId": "", + # "stpMode": "cancel_maker", + # "sz": "0.1", + # "tag": "", + # "tdMode": "isolated", + # "tgtCcy": "", + # "tpOrdPx": "", + # "tpTriggerPx": "", + # "tpTriggerPxType": "", + # "uTime": "1751705807467", + # "reqId": "", # field present only in WS + # "msg": "", # field present only in WS + # "amendResult": "", # field present only in WS + # "amendSource": "", # field present only in WS + # "code": "0", # field present only in WS + # "fillFwdPx": "", # field present only in WS + # "fillMarkVol": "", # field present only in WS + # "fillPxUsd": "", # field present only in WS + # "fillPxVol": "", # field present only in WS + # "lastPx": "13.142", # field present only in WS + # "notionalUsd": "1.314515408", # field present only in WS + # + # #### these below fields are empty on first omit from websocket, because of "creation" event. however, if order is executed, it also immediately sends another update with these fields filled ### + # + # "pnl": "-0.0001", + # "accFillSz": "0.1", + # "avgPx": "13.142", + # "state": "filled", + # "fee": "-0.00026284", + # "fillPx": "13.142", + # "tradeId": "293429690", + # "fillSz": "0.1", + # "fillTime": "1751705807467", + # "fillNotionalUsd": "1.314515408", # field present only in WS + # "fillPnl": "-0.0001", # field present only in WS + # "fillFee": "-0.00026284", # field present only in WS + # "fillFeeCcy": "USDT", # field present only in WS + # "execType": "M", # field present only in WS + # "fillMarkPx": "13.141", # field present only in WS + # "fillIdxPx": "13.147" # field present only in WS + # } + # + # + # Algo Order fetchOpenOrders, fetchCanceledOrders, fetchClosedOrders + # + # { + # "activePx": "", + # "activePxType": "", + # "actualPx": "", + # "actualSide": "buy", + # "actualSz": "0", + # "algoId": "431375349042380800", + # "cTime": "1649119897778", + # "callbackRatio": "", + # "callbackSpread": "", + # "ccy": "", + # "ctVal": "0.01", + # "instId": "BTC-USDT-SWAP", + # "instType": "SWAP", + # "last": "46538.9", + # "lever": "125", + # "moveTriggerPx": "", + # "notionalUsd": "467.059", + # "ordId": "", + # "ordPx": "50000", + # "ordType": "trigger", + # "posSide": "long", + # "pxLimit": "", + # "pxSpread": "", + # "pxVar": "", + # "side": "buy", + # "slOrdPx": "", + # "slTriggerPx": "", + # "slTriggerPxType": "", + # "state": "live", + # "sz": "1", + # "szLimit": "", + # "tag": "", + # "tdMode": "isolated", + # "tgtCcy": "", + # "timeInterval": "", + # "tpOrdPx": "", + # "tpTriggerPx": "", + # "tpTriggerPxType": "", + # "triggerPx": "50000", + # "triggerPxType": "last", + # "triggerTime": "", + # "uly": "BTC-USDT" + # } + # + scode = self.safe_string(order, 'sCode') + if (scode is not None) and (scode != '0'): + return self.safe_order({ + 'id': self.safe_string(order, 'ordId'), + 'clientOrderId': self.safe_string(order, 'clOrdId'), + 'status': 'rejected', + 'info': order, + }) + id = self.safe_string_2(order, 'algoId', 'ordId') + timestamp = self.safe_integer(order, 'cTime') + lastUpdateTimestamp = self.safe_integer(order, 'uTime') + lastTradeTimestamp = self.safe_integer(order, 'fillTime') + side = self.safe_string(order, 'side') + type = self.safe_string(order, 'ordType') + postOnly = None + timeInForce = None + if type == 'post_only': + postOnly = True + type = 'limit' + elif type == 'fok': + timeInForce = 'FOK' + type = 'limit' + elif type == 'ioc': + timeInForce = 'IOC' + type = 'limit' + marketId = self.safe_string(order, 'instId') + market = self.safe_market(marketId, market) + symbol = self.safe_symbol(marketId, market, '-') + filled = self.safe_string(order, 'accFillSz') + price = self.safe_string_2(order, 'px', 'ordPx') + average = self.safe_string(order, 'avgPx') + status = self.parse_order_status(self.safe_string(order, 'state')) + feeCostString = self.safe_string(order, 'fee') + amount = None + cost = None + # spot market buy: "sz" can refer either to base currency units or to quote currency units + # see documentation: https://www.okx.com/docs-v5/en/#rest-api-trade-place-order + defaultTgtCcy = self.safe_string(self.options, 'tgtCcy', 'base_ccy') + tgtCcy = self.safe_string(order, 'tgtCcy', defaultTgtCcy) + instType = self.safe_string(order, 'instType') + if (side == 'buy') and (type == 'market') and (instType == 'SPOT') and (tgtCcy == 'quote_ccy'): + # "sz" refers to the cost + cost = self.safe_string(order, 'sz') + else: + # "sz" refers to the trade currency amount + amount = self.safe_string(order, 'sz') + fee = None + if feeCostString is not None: + feeCostSigned = Precise.string_neg(feeCostString) + feeCurrencyId = self.safe_string(order, 'feeCcy') + feeCurrencyCode = self.safe_currency_code(feeCurrencyId) + fee = { + 'cost': self.parse_number(feeCostSigned), + 'currency': feeCurrencyCode, + } + clientOrderId = self.safe_string(order, 'clOrdId') + if (clientOrderId is not None) and (len(clientOrderId) < 1): + clientOrderId = None # fix empty clientOrderId string + stopLossPrice = self.safe_number_2(order, 'slTriggerPx', 'slOrdPx') + takeProfitPrice = self.safe_number_2(order, 'tpTriggerPx', 'tpOrdPx') + reduceOnlyRaw = self.safe_string(order, 'reduceOnly') + reduceOnly = False + if reduceOnly is not None: + reduceOnly = (reduceOnlyRaw == 'true') + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': lastTradeTimestamp, + 'lastUpdateTimestamp': lastUpdateTimestamp, + 'symbol': symbol, + 'type': type, + 'timeInForce': timeInForce, + 'postOnly': postOnly, + 'side': side, + 'price': price, + 'stopLossPrice': stopLossPrice, + 'takeProfitPrice': takeProfitPrice, + 'triggerPrice': self.safe_number_n(order, ['triggerPx', 'moveTriggerPx']), + 'average': average, + 'cost': cost, + 'amount': amount, + 'filled': filled, + 'remaining': None, + 'status': status, + 'fee': fee, + 'trades': None, + 'reduceOnly': reduceOnly, + }, market) + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetch an order by the id + + https://www.okx.com/docs-v5/en/#order-book-trading-trade-get-order-details + https://www.okx.com/docs-v5/en/#order-book-trading-algo-trading-get-algo-order-details + + :param str id: the order id + :param str symbol: unified market symbol + :param dict [params]: extra and exchange specific parameters + :param boolean [params.trigger]: True if fetching trigger orders + :returns: `an order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrder() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + # 'clOrdId': 'abcdef12345', # optional, [a-z0-9]{1,32} + # 'ordId': id, + # 'instType': # spot, swap, futures, margin + } + clientOrderId = self.safe_string_2(params, 'clOrdId', 'clientOrderId') + options = self.safe_value(self.options, 'fetchOrder', {}) + defaultMethod = self.safe_string(options, 'method', 'privateGetTradeOrder') + method = self.safe_string(params, 'method', defaultMethod) + trigger = self.safe_value_2(params, 'stop', 'trigger') + if trigger: + method = 'privateGetTradeOrderAlgo' + if clientOrderId is not None: + request['algoClOrdId'] = clientOrderId + else: + request['algoId'] = id + else: + if clientOrderId is not None: + request['clOrdId'] = clientOrderId + else: + request['ordId'] = id + query = self.omit(params, ['method', 'clOrdId', 'clientOrderId', 'stop', 'trigger']) + response = None + if method == 'privateGetTradeOrderAlgo': + response = self.privateGetTradeOrderAlgo(self.extend(request, query)) + else: + response = self.privateGetTradeOrder(self.extend(request, query)) + # + # Spot and Swap + # + # { + # "code": "0", + # "data": [ + # { + # "accFillSz": "0", + # "avgPx": "", + # "cTime": "1621910749815", + # "category": "normal", + # "ccy": "", + # "clOrdId": "", + # "fee": "0", + # "feeCcy": "ETH", + # "fillPx": "", + # "fillSz": "0", + # "fillTime": "", + # "instId": "ETH-USDT", + # "instType": "SPOT", + # "lever": "", + # "ordId": "317251910906576896", + # "ordType": "limit", + # "pnl": "0", + # "posSide": "net", + # "px":"20 00", + # "rebate": "0", + # "rebateCcy": "USDT", + # "side": "buy", + # "slOrdPx": "", + # "slTriggerPx": "", + # "state": "live", + # "sz":"0. 001", + # "tag": "", + # "tdMode": "cash", + # "tpOrdPx": "", + # "tpTriggerPx": "", + # "tradeId": "", + # "uTime": "1621910749815" + # } + # ], + # "msg": "" + # } + # + # Algo order + # { + # "code":"0", + # "msg":"", + # "data":[ + # { + # "instType":"FUTURES", + # "instId":"BTC-USD-200329", + # "ordId":"123445", + # "ccy":"BTC", + # "clOrdId":"", + # "algoId":"1234", + # "sz":"999", + # "closeFraction":"", + # "ordType":"oco", + # "side":"buy", + # "posSide":"long", + # "tdMode":"cross", + # "tgtCcy": "", + # "state":"effective", + # "lever":"20", + # "tpTriggerPx":"", + # "tpTriggerPxType":"", + # "tpOrdPx":"", + # "slTriggerPx":"", + # "slTriggerPxType":"", + # "triggerPx":"99", + # "triggerPxType":"last", + # "ordPx":"12", + # "actualSz":"", + # "actualPx":"", + # "actualSide":"", + # "pxVar":"", + # "pxSpread":"", + # "pxLimit":"", + # "szLimit":"", + # "tag": "adadadadad", + # "timeInterval":"", + # "callbackRatio":"", + # "callbackSpread":"", + # "activePx":"", + # "moveTriggerPx":"", + # "reduceOnly": "false", + # "triggerTime":"1597026383085", + # "last": "16012", + # "failCode": "", + # "algoClOrdId": "", + # "cTime":"1597026383000" + # } + # ] + # } + # + data = self.safe_value(response, 'data', []) + order = self.safe_dict(data, 0) + return self.parse_order(order, market) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://www.okx.com/docs-v5/en/#order-book-trading-trade-get-order-list + https://www.okx.com/docs-v5/en/#order-book-trading-algo-trading-get-algo-order-list + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.trigger]: True if fetching trigger or conditional orders + :param str [params.ordType]: "conditional", "oco", "trigger", "move_order_stop", "iceberg", or "twap" + :param str [params.algoId]: Algo ID "'433845797218942976'" + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param boolean [params.trailing]: set to True if you want to fetch trailing orders + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOpenOrders', 'paginate') + if paginate: + return self.fetch_paginated_call_dynamic('fetchOpenOrders', symbol, since, limit, params) + request: dict = { + # 'instType': 'SPOT', # SPOT, MARGIN, SWAP, FUTURES, OPTION + # 'uly': currency['id'], + # 'instId': market['id'], + # 'ordType': 'limit', # market, limit, post_only, fok, ioc, comma-separated, stop orders: conditional, oco, trigger, move_order_stop, iceberg, or twap + # 'state': 'live', # live, partially_filled + # 'after': orderId, + # 'before': orderId, + # 'limit': limit, # default 100, max 100 + } + market = None + if symbol is not None: + market = self.market(symbol) + request['instId'] = market['id'] + if limit is not None: + request['limit'] = limit # default 100, max 100 + options = self.safe_value(self.options, 'fetchOpenOrders', {}) + algoOrderTypes = self.safe_value(self.options, 'algoOrderTypes', {}) + defaultMethod = self.safe_string(options, 'method', 'privateGetTradeOrdersPending') + method = self.safe_string(params, 'method', defaultMethod) + ordType = self.safe_string(params, 'ordType') + trigger = self.safe_value_2(params, 'stop', 'trigger') + trailing = self.safe_bool(params, 'trailing', False) + if trailing or trigger or (ordType in algoOrderTypes): + method = 'privateGetTradeOrdersAlgoPending' + if trailing: + request['ordType'] = 'move_order_stop' + elif trigger and (ordType is None): + request['ordType'] = 'trigger' + query = self.omit(params, ['method', 'stop', 'trigger', 'trailing']) + response = None + if method == 'privateGetTradeOrdersAlgoPending': + response = self.privateGetTradeOrdersAlgoPending(self.extend(request, query)) + else: + response = self.privateGetTradeOrdersPending(self.extend(request, query)) + # + # { + # "code": "0", + # "data": [ + # { + # "accFillSz": "0", + # "avgPx": "", + # "cTime": "1621910749815", + # "category": "normal", + # "ccy": "", + # "clOrdId": "", + # "fee": "0", + # "feeCcy": "ETH", + # "fillPx": "", + # "fillSz": "0", + # "fillTime": "", + # "instId": "ETH-USDT", + # "instType": "SPOT", + # "lever": "", + # "ordId": "317251910906576896", + # "ordType": "limit", + # "pnl": "0", + # "posSide": "net", + # "px":"20 00", + # "rebate": "0", + # "rebateCcy": "USDT", + # "side": "buy", + # "slOrdPx": "", + # "slTriggerPx": "", + # "state": "live", + # "sz":"0. 001", + # "tag": "", + # "tdMode": "cash", + # "tpOrdPx": "", + # "tpTriggerPx": "", + # "tradeId": "", + # "uTime": "1621910749815" + # } + # ], + # "msg":"" + # } + # + # Algo order + # + # { + # "code": "0", + # "data": [ + # { + # "activePx": "", + # "activePxType": "", + # "actualPx": "", + # "actualSide": "buy", + # "actualSz": "0", + # "algoId": "431375349042380800", + # "cTime": "1649119897778", + # "callbackRatio": "", + # "callbackSpread": "", + # "ccy": "", + # "ctVal": "0.01", + # "instId": "BTC-USDT-SWAP", + # "instType": "SWAP", + # "last": "46538.9", + # "lever": "125", + # "moveTriggerPx": "", + # "notionalUsd": "467.059", + # "ordId": "", + # "ordPx": "50000", + # "ordType": "trigger", + # "posSide": "long", + # "pxLimit": "", + # "pxSpread": "", + # "pxVar": "", + # "side": "buy", + # "slOrdPx": "", + # "slTriggerPx": "", + # "slTriggerPxType": "", + # "state": "live", + # "sz": "1", + # "szLimit": "", + # "tag": "", + # "tdMode": "isolated", + # "tgtCcy": "", + # "timeInterval": "", + # "tpOrdPx": "", + # "tpTriggerPx": "", + # "tpTriggerPxType": "", + # "triggerPx": "50000", + # "triggerPxType": "last", + # "triggerTime": "", + # "uly": "BTC-USDT" + # } + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_orders(data, market, since, limit) + + def fetch_canceled_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches information on multiple canceled orders made by the user + + https://www.okx.com/docs-v5/en/#order-book-trading-trade-get-order-history-last-7-days + https://www.okx.com/docs-v5/en/#order-book-trading-algo-trading-get-algo-order-history + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: timestamp in ms of the earliest order, default is None + :param int [limit]: max number of orders to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.trigger]: True if fetching trigger or conditional orders + :param str [params.ordType]: "conditional", "oco", "trigger", "move_order_stop", "iceberg", or "twap" + :param str [params.algoId]: Algo ID "'433845797218942976'" + :param int [params.until]: timestamp in ms to fetch orders for + :param boolean [params.trailing]: set to True if you want to fetch trailing orders + :returns dict: a list of `order structures ` + """ + self.load_markets() + request: dict = { + # 'instType': type.upper(), # SPOT, MARGIN, SWAP, FUTURES, OPTION + # 'uly': currency['id'], + # 'instId': market['id'], + # 'ordType': 'limit', # market, limit, post_only, fok, ioc, comma-separated stop orders: conditional, oco, trigger, move_order_stop, iceberg, or twap + # 'state': 'canceled', # filled, canceled + # 'after': orderId, + # 'before': orderId, + # 'limit': limit, # default 100, max 100 + # 'algoId': "'433845797218942976'", # Algo order + } + market = None + if symbol is not None: + market = self.market(symbol) + request['instId'] = market['id'] + type = None + query = None + type, query = self.handle_market_type_and_params('fetchCanceledOrders', market, params) + request['instType'] = self.convert_to_instrument_type(type) + if limit is not None: + request['limit'] = limit # default 100, max 100 + request['state'] = 'canceled' + options = self.safe_value(self.options, 'fetchCanceledOrders', {}) + algoOrderTypes = self.safe_value(self.options, 'algoOrderTypes', {}) + defaultMethod = self.safe_string(options, 'method', 'privateGetTradeOrdersHistory') + method = self.safe_string(params, 'method', defaultMethod) + ordType = self.safe_string(params, 'ordType') + trigger = self.safe_value_2(params, 'stop', 'trigger') + trailing = self.safe_bool(params, 'trailing', False) + if trailing: + method = 'privateGetTradeOrdersAlgoHistory' + request['ordType'] = 'move_order_stop' + elif trigger or (ordType in algoOrderTypes): + method = 'privateGetTradeOrdersAlgoHistory' + algoId = self.safe_string(params, 'algoId') + if algoId is not None: + request['algoId'] = algoId + params = self.omit(params, 'algoId') + if trigger: + if ordType is None: + raise ArgumentsRequired(self.id + ' fetchCanceledOrders() requires an "ordType" string parameter, "conditional", "oco", "trigger", "move_order_stop", "iceberg", or "twap"') + else: + if since is not None: + request['begin'] = since + until = self.safe_integer(query, 'until') + if until is not None: + request['end'] = until + query = self.omit(query, ['until']) + send = self.omit(query, ['method', 'stop', 'trigger', 'trailing']) + response = None + if method == 'privateGetTradeOrdersAlgoHistory': + response = self.privateGetTradeOrdersAlgoHistory(self.extend(request, send)) + else: + response = self.privateGetTradeOrdersHistory(self.extend(request, send)) + # + # { + # "code": "0", + # "data": [ + # { + # "accFillSz": "0", + # "avgPx": "", + # "cTime": "1644037822494", + # "category": "normal", + # "ccy": "", + # "clOrdId": "", + # "fee": "0", + # "feeCcy": "BTC", + # "fillPx": "", + # "fillSz": "0", + # "fillTime": "", + # "instId": "BTC-USDT", + # "instType": "SPOT", + # "lever": "", + # "ordId": "410059580352409602", + # "ordType": "limit", + # "pnl": "0", + # "posSide": "net", + # "px": "30000", + # "rebate": "0", + # "rebateCcy": "USDT", + # "side": "buy", + # "slOrdPx": "", + # "slTriggerPx": "", + # "slTriggerPxType": "", + # "source": "", + # "state": "canceled", + # "sz": "0.0005452", + # "tag": "", + # "tdMode": "cash", + # "tgtCcy": "", + # "tpOrdPx": "", + # "tpTriggerPx": "", + # "tpTriggerPxType": "", + # "tradeId": "", + # "uTime": "1644038165667" + # } + # ], + # "msg": "" + # } + # + # Algo order + # + # { + # "code": "0", + # "data": [ + # { + # "activePx": "", + # "activePxType": "", + # "actualPx": "", + # "actualSide": "buy", + # "actualSz": "0", + # "algoId": "433845797218942976", + # "cTime": "1649708898523", + # "callbackRatio": "", + # "callbackSpread": "", + # "ccy": "", + # "ctVal": "0.01", + # "instId": "BTC-USDT-SWAP", + # "instType": "SWAP", + # "last": "39950.4", + # "lever": "125", + # "moveTriggerPx": "", + # "notionalUsd": "1592.1760000000002", + # "ordId": "", + # "ordPx": "29000", + # "ordType": "trigger", + # "posSide": "long", + # "pxLimit": "", + # "pxSpread": "", + # "pxVar": "", + # "side": "buy", + # "slOrdPx": "", + # "slTriggerPx": "", + # "slTriggerPxType": "", + # "state": "canceled", + # "sz": "4", + # "szLimit": "", + # "tag": "", + # "tdMode": "isolated", + # "tgtCcy": "", + # "timeInterval": "", + # "tpOrdPx": "", + # "tpTriggerPx": "", + # "tpTriggerPxType": "", + # "triggerPx": "30000", + # "triggerPxType": "last", + # "triggerTime": "", + # "uly": "BTC-USDT" + # }, + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_orders(data, market, since, limit) + + def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://www.okx.com/docs-v5/en/#order-book-trading-trade-get-order-history-last-7-days + https://www.okx.com/docs-v5/en/#order-book-trading-algo-trading-get-algo-order-history + https://www.okx.com/docs-v5/en/#order-book-trading-trade-get-order-history-last-3-months + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.trigger]: True if fetching trigger or conditional orders + :param str [params.ordType]: "conditional", "oco", "trigger", "move_order_stop", "iceberg", or "twap" + :param str [params.algoId]: Algo ID "'433845797218942976'" + :param int [params.until]: timestamp in ms to fetch orders for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param str [params.method]: method to be used, either 'privateGetTradeOrdersHistory', 'privateGetTradeOrdersHistoryArchive' or 'privateGetTradeOrdersAlgoHistory' default is 'privateGetTradeOrdersHistory' + :param boolean [params.trailing]: set to True if you want to fetch trailing orders + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchClosedOrders', 'paginate') + if paginate: + return self.fetch_paginated_call_dynamic('fetchClosedOrders', symbol, since, limit, params) + request: dict = { + # 'instType': type.upper(), # SPOT, MARGIN, SWAP, FUTURES, OPTION + # 'uly': currency['id'], + # 'instId': market['id'], + # 'ordType': 'limit', # market, limit, post_only, fok, ioc, comma-separated stop orders: conditional, oco, trigger, move_order_stop, iceberg, or twap + # 'state': 'filled', # filled, effective + # 'after': orderId, + # 'before': orderId, + # 'limit': limit, # default 100, max 100 + # 'algoId': "'433845797218942976'", # Algo order + } + market = None + if symbol is not None: + market = self.market(symbol) + request['instId'] = market['id'] + type = None + query = None + type, query = self.handle_market_type_and_params('fetchClosedOrders', market, params) + request['instType'] = self.convert_to_instrument_type(type) + if limit is not None: + request['limit'] = limit # default 100, max 100 + options = self.safe_dict(self.options, 'fetchClosedOrders', {}) + algoOrderTypes = self.safe_dict(self.options, 'algoOrderTypes', {}) + defaultMethod = self.safe_string(options, 'method', 'privateGetTradeOrdersHistory') + method = self.safe_string(params, 'method', defaultMethod) + ordType = self.safe_string(params, 'ordType') + trigger = self.safe_bool_2(params, 'stop', 'trigger') + trailing = self.safe_bool(params, 'trailing', False) + if trailing or trigger or (ordType in algoOrderTypes): + method = 'privateGetTradeOrdersAlgoHistory' + request['state'] = 'effective' + if trailing: + request['ordType'] = 'move_order_stop' + elif trigger: + if ordType is None: + request['ordType'] = 'trigger' + else: + if since is not None: + request['begin'] = since + until = self.safe_integer(query, 'until') + if until is not None: + request['end'] = until + query = self.omit(query, ['until']) + request['state'] = 'filled' + send = self.omit(query, ['method', 'stop', 'trigger', 'trailing']) + response = None + if method == 'privateGetTradeOrdersAlgoHistory': + response = self.privateGetTradeOrdersAlgoHistory(self.extend(request, send)) + elif method == 'privateGetTradeOrdersHistoryArchive': + response = self.privateGetTradeOrdersHistoryArchive(self.extend(request, send)) + else: + response = self.privateGetTradeOrdersHistory(self.extend(request, send)) + # + # { + # "code": "0", + # "data": [ + # { + # "accFillSz": "0", + # "avgPx": "", + # "cTime": "1621910749815", + # "category": "normal", + # "ccy": "", + # "clOrdId": "", + # "fee": "0", + # "feeCcy": "ETH", + # "fillPx": "", + # "fillSz": "0", + # "fillTime": "", + # "instId": "ETH-USDT", + # "instType": "SPOT", + # "lever": "", + # "ordId": "317251910906576896", + # "ordType": "limit", + # "pnl": "0", + # "posSide": "net", + # "px": "2000", + # "rebate": "0", + # "rebateCcy": "USDT", + # "side": "buy", + # "slOrdPx": "", + # "slTriggerPx": "", + # "state": "live", + # "sz": "0.001", + # "tag": "", + # "tdMode": "cash", + # "tpOrdPx": "", + # "tpTriggerPx": "", + # "tradeId": "", + # "uTime": "1621910749815" + # } + # ], + # "msg": "" + # } + # + # Algo order + # + # { + # "code": "0", + # "data": [ + # { + # "activePx": "", + # "activePxType": "", + # "actualPx": "", + # "actualSide": "buy", + # "actualSz": "0", + # "algoId": "433845797218942976", + # "cTime": "1649708898523", + # "callbackRatio": "", + # "callbackSpread": "", + # "ccy": "", + # "ctVal": "0.01", + # "instId": "BTC-USDT-SWAP", + # "instType": "SWAP", + # "last": "39950.4", + # "lever": "125", + # "moveTriggerPx": "", + # "notionalUsd": "1592.1760000000002", + # "ordId": "", + # "ordPx": "29000", + # "ordType": "trigger", + # "posSide": "long", + # "pxLimit": "", + # "pxSpread": "", + # "pxVar": "", + # "side": "buy", + # "slOrdPx": "", + # "slTriggerPx": "", + # "slTriggerPxType": "", + # "state": "effective", + # "sz": "4", + # "szLimit": "", + # "tag": "", + # "tdMode": "isolated", + # "tgtCcy": "", + # "timeInterval": "", + # "tpOrdPx": "", + # "tpTriggerPx": "", + # "tpTriggerPxType": "", + # "triggerPx": "30000", + # "triggerPxType": "last", + # "triggerTime": "", + # "uly": "BTC-USDT" + # }, + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_orders(data, market, since, limit) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://www.okx.com/docs-v5/en/#order-book-trading-trade-get-transaction-details-last-3-months + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: Timestamp in ms of the latest time to retrieve trades for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchMyTrades', 'paginate') + if paginate: + return self.fetch_paginated_call_dynamic('fetchMyTrades', symbol, since, limit, params) + request: dict = { + # 'instType': 'SPOT', # SPOT, MARGIN, SWAP, FUTURES, OPTION + # 'uly': currency['id'], + # 'instId': market['id'], + # 'ordId': orderId, + # 'after': billId, + # 'before': billId, + # 'limit': limit, # default 100, max 100 + } + market = None + if symbol is not None: + market = self.market(symbol) + request['instId'] = market['id'] + if since is not None: + request['begin'] = since + request, params = self.handle_until_option('end', request, params) + type, query = self.handle_market_type_and_params('fetchMyTrades', market, params) + request['instType'] = self.convert_to_instrument_type(type) + if (limit is not None) and (since is None): # limit = n, okx will return the n most recent results, instead of the n results after limit, so limit should only be sent when since is None + request['limit'] = limit # default 100, max 100 + response = self.privateGetTradeFillsHistory(self.extend(request, query)) + # + # { + # "code": "0", + # "data": [ + # { + # "side": "buy", + # "fillSz": "0.007533", + # "fillPx": "2654.98", + # "fee": "-0.000007533", + # "ordId": "317321390244397056", + # "instType": "SPOT", + # "instId": "ETH-USDT", + # "clOrdId": "", + # "posSide": "net", + # "billId": "317321390265368576", + # "tag": "0", + # "execType": "T", + # "tradeId": "107601752", + # "feeCcy": "ETH", + # "ts": "1621927314985" + # } + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_trades(data, market, since, limit, query) + + def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all the trades made from a single order + + https://www.okx.com/docs-v5/en/#order-book-trading-trade-get-transaction-details-last-3-months + + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + request: dict = { + # 'instrument_id': market['id'], + 'ordId': id, + # 'after': '1', # return the page after the specified page number + # 'before': '1', # return the page before the specified page number + # 'limit': limit, # optional, number of results per request, default = maximum = 100 + } + return self.fetch_my_trades(symbol, since, limit, self.extend(request, params)) + + def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered balance of the user + + https://www.okx.com/docs-v5/en/#rest-api-account-get-bills-details-last-7-days + https://www.okx.com/docs-v5/en/#rest-api-account-get-bills-details-last-3-months + https://www.okx.com/docs-v5/en/#rest-api-funding-asset-bills-details + + :param str [code]: unified currency code, default is None + :param int [since]: timestamp in ms of the earliest ledger entry, default is None + :param int [limit]: max number of ledger entries to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' + :param int [params.until]: the latest time in ms to fetch entries for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict: a `ledger structure ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchLedger', 'paginate') + if paginate: + return self.fetch_paginated_call_dynamic('fetchLedger', code, since, limit, params) + options = self.safe_dict(self.options, 'fetchLedger', {}) + method = self.safe_string(options, 'method') + method = self.safe_string(params, 'method', method) + params = self.omit(params, 'method') + request: dict = { + # 'instType': None, # 'SPOT', 'MARGIN', 'SWAP', 'FUTURES", 'OPTION' + # 'ccy': None, # currency['id'], + # 'mgnMode': None, # 'isolated', 'cross' + # 'ctType': None, # 'linear', 'inverse', only applicable to FUTURES/SWAP + # 'type': varies depending the 'method' endpoint : + # - https://www.okx.com/docs-v5/en/#rest-api-account-get-bills-details-last-7-days + # - https://www.okx.com/docs-v5/en/#rest-api-funding-asset-bills-details + # - https://www.okx.com/docs-v5/en/#rest-api-account-get-bills-details-last-3-months + # 'after': 'id', # return records earlier than the requested bill id + # 'before': 'id', # return records newer than the requested bill id + # 'limit': 100, # default 100, max 100 + } + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchLedger', params) + if marginMode is None: + marginMode = self.safe_string(params, 'mgnMode') + if method != 'privateGetAssetBills': + if marginMode is not None: + request['mgnMode'] = marginMode + type, query = self.handle_market_type_and_params('fetchLedger', None, params) + if type is not None: + request['instType'] = self.convert_to_instrument_type(type) + if limit is not None: + request['limit'] = limit + currency = None + if code is not None: + currency = self.currency(code) + request['ccy'] = currency['id'] + request, params = self.handle_until_option('end', request, params) + response = None + if method == 'privateGetAccountBillsArchive': + response = self.privateGetAccountBillsArchive(self.extend(request, query)) + elif method == 'privateGetAssetBills': + response = self.privateGetAssetBills(self.extend(request, query)) + else: + response = self.privateGetAccountBills(self.extend(request, query)) + # + # privateGetAccountBills, privateGetAccountBillsArchive + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "bal": "0.0000819307998198", + # "balChg": "-664.2679586599999802", + # "billId": "310394313544966151", + # "ccy": "USDT", + # "fee": "0", + # "from": "", + # "instId": "LTC-USDT", + # "instType": "SPOT", + # "mgnMode": "cross", + # "notes": "", + # "ordId": "310394313519800320", + # "pnl": "0", + # "posBal": "0", + # "posBalChg": "0", + # "subType": "2", + # "sz": "664.26795866", + # "to": "", + # "ts": "1620275771196", + # "type": "2" + # } + # ] + # } + # + # privateGetAssetBills + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "billId": "12344", + # "ccy": "BTC", + # "balChg": "2", + # "bal": "12", + # "type": "1", + # "ts": "1597026383085" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_ledger(data, currency, since, limit) + + def parse_ledger_entry_type(self, type): + types: dict = { + '1': 'transfer', # transfer + '2': 'trade', # trade + '3': 'trade', # delivery + '4': 'rebate', # auto token conversion + '5': 'trade', # liquidation + '6': 'transfer', # margin transfer + '7': 'trade', # interest deduction + '8': 'fee', # funding rate + '9': 'trade', # adl + '10': 'trade', # clawback + '11': 'trade', # system token conversion + } + return self.safe_string(types, type, type) + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + # + # privateGetAccountBills, privateGetAccountBillsArchive + # + # { + # "bal": "0.0000819307998198", + # "balChg": "-664.2679586599999802", + # "billId": "310394313544966151", + # "ccy": "USDT", + # "fee": "0", + # "from": "", + # "instId": "LTC-USDT", + # "instType": "SPOT", + # "mgnMode": "cross", + # "notes": "", + # "ordId": "310394313519800320", + # "pnl": "0", + # "posBal": "0", + # "posBalChg": "0", + # "subType": "2", + # "sz": "664.26795866", + # "to": "", + # "ts": "1620275771196", + # "type": "2" + # } + # + # privateGetAssetBills + # + # { + # "billId": "12344", + # "ccy": "BTC", + # "balChg": "2", + # "bal": "12", + # "type": "1", + # "ts": "1597026383085" + # } + # + currencyId = self.safe_string(item, 'ccy') + code = self.safe_currency_code(currencyId, currency) + currency = self.safe_currency(currencyId, currency) + timestamp = self.safe_integer(item, 'ts') + feeCostString = self.safe_string(item, 'fee') + fee = None + if feeCostString is not None: + fee = { + 'cost': self.parse_number(Precise.string_neg(feeCostString)), + 'currency': code, + } + marketId = self.safe_string(item, 'instId') + symbol = self.safe_symbol(marketId, None, '-') + return self.safe_ledger_entry({ + 'info': item, + 'id': self.safe_string(item, 'billId'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'account': None, + 'referenceId': self.safe_string(item, 'ordId'), + 'referenceAccount': None, + 'type': self.parse_ledger_entry_type(self.safe_string(item, 'type')), + 'currency': code, + 'symbol': symbol, + 'amount': self.safe_number(item, 'balChg'), + 'before': None, + 'after': self.safe_number(item, 'bal'), + 'status': 'ok', + 'fee': fee, + }, currency) + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + # + # { + # "addr": "okbtothemoon", + # "memo": "971668", # may be missing + # "tag":"52055", # may be missing + # "pmtId": "", # may be missing + # "ccy": "BTC", + # "to": "6", # 1 SPOT, 3 FUTURES, 6 FUNDING, 9 SWAP, 12 OPTION, 18 Unified account + # "selected": True + # } + # + # { + # "ccy":"usdt-erc20", + # "to":"6", + # "addr":"0x696abb81974a8793352cbd33aadcf78eda3cfdfa", + # "selected":true + # } + # + # { + # "chain": "ETH-OKExChain", + # "addrEx": {"comment": "6040348"}, # some currencies like TON may have self field, + # "ctAddr": "72315c", + # "ccy": "ETH", + # "to": "6", + # "addr": "0x1c9f2244d1ccaa060bd536827c18925db10db102", + # "selected": True + # } + # + address = self.safe_string(depositAddress, 'addr') + tag = self.safe_string_n(depositAddress, ['tag', 'pmtId', 'memo']) + if tag is None: + addrEx = self.safe_value(depositAddress, 'addrEx', {}) + tag = self.safe_string(addrEx, 'comment') + currencyId = self.safe_string(depositAddress, 'ccy') + currency = self.safe_currency(currencyId, currency) + code = currency['code'] + chain = self.safe_string(depositAddress, 'chain') + networks = self.safe_value(currency, 'networks', {}) + networksById = self.index_by(networks, 'id') + networkData = self.safe_value(networksById, chain) + # inconsistent naming responses from exchange + # with respect to network naming provided in currency info vs address chain-names and ids + # + # response from address endpoint: + # { + # "chain": "USDT-Polygon", + # "ctAddr": "", + # "ccy": "USDT", + # "to":"6" , + # "addr": "0x1903441e386cc49d937f6302955b5feb4286dcfa", + # "selected": True + # } + # network information from currency['networks'] field: + # Polygon: { + # info: { + # canDep: False, + # canInternal: False, + # canWd: False, + # ccy: 'USDT', + # chain: 'USDT-Polygon-Bridge', + # mainNet: False, + # maxFee: '26.879528', + # minFee: '13.439764', + # minWd: '0.001', + # name: '' + # }, + # id: 'USDT-Polygon-Bridge', + # network: 'Polygon', + # active: False, + # deposit: False, + # withdraw: False, + # fee: 13.439764, + # precision: None, + # limits: { + # withdraw: { + # min: 0.001, + # max: None + # } + # } + # }, + # + if chain == 'USDT-Polygon': + networkData = self.safe_value_2(networksById, 'USDT-Polygon-Bridge', 'USDT-Polygon') + network = self.safe_string(networkData, 'network') + networkCode = self.network_id_to_code(network, code) + self.check_address(address) + return { + 'info': depositAddress, + 'currency': code, + 'network': networkCode, + 'address': address, + 'tag': tag, + } + + def fetch_deposit_addresses_by_network(self, code: str, params={}) -> List[DepositAddress]: + """ + fetch a dictionary of addresses for a currency, indexed by network + + https://www.okx.com/docs-v5/en/#funding-account-rest-api-get-deposit-address + + :param str code: unified currency code of the currency for the deposit address + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `address structures ` indexed by the network + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'ccy': currency['id'], + } + response = self.privateGetAssetDepositAddress(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "addr": "okbtothemoon", + # "memo": "971668", # may be missing + # "tag":"52055", # may be missing + # "pmtId": "", # may be missing + # "ccy": "BTC", + # "to": "6", # 1 SPOT, 3 FUTURES, 6 FUNDING, 9 SWAP, 12 OPTION, 18 Unified account + # "selected": True + # }, + # # {"ccy":"usdt-erc20","to":"6","addr":"0x696abb81974a8793352cbd33aadcf78eda3cfdfa","selected":true}, + # # {"ccy":"usdt-trc20","to":"6","addr":"TRrd5SiSZrfQVRKm4e9SRSbn2LNTYqCjqx","selected":true}, + # # {"ccy":"usdt_okexchain","to":"6","addr":"0x696abb81974a8793352cbd33aadcf78eda3cfdfa","selected":true}, + # # {"ccy":"usdt_kip20","to":"6","addr":"0x696abb81974a8793352cbd33aadcf78eda3cfdfa","selected":true}, + # ] + # } + # + data = self.safe_list(response, 'data', []) + filtered = self.filter_by(data, 'selected', True) + parsed = self.parse_deposit_addresses(filtered, [currency['code']], False) + return self.index_by(parsed, 'network') + + def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://www.okx.com/docs-v5/en/#funding-account-rest-api-get-deposit-address + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.network]: the network name for the deposit address + :returns dict: an `address structure ` + """ + self.load_markets() + rawNetwork = self.safe_string(params, 'network') # some networks are like "Dora Vota Mainnet" + params = self.omit(params, 'network') + code = self.safe_currency_code(code) + network = self.network_id_to_code(rawNetwork, code) + response = self.fetch_deposit_addresses_by_network(code, params) + if network is not None: + result = self.safe_dict(response, network) + if result is None: + raise InvalidAddress(self.id + ' fetchDepositAddress() cannot find ' + network + ' deposit address for ' + code) + return result + codeNetwork = self.network_id_to_code(code, code) + if codeNetwork in response: + return response[codeNetwork] + # if the network is not specified, return the first address + keys = list(response.keys()) + first = self.safe_string(keys, 0) + return self.safe_dict(response, first) + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://www.okx.com/docs-v5/en/#funding-account-rest-api-withdrawal + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.check_address(address) + self.load_markets() + currency = self.currency(code) + if (tag is not None) and (len(tag) > 0): + address = address + ':' + tag + request: dict = { + 'ccy': currency['id'], + 'toAddr': address, + 'dest': '4', # 2 = OKCoin International, 3 = OKX 4 = others + 'amt': self.number_to_string(amount), + } + network = self.safe_string(params, 'network') # self line allows the user to specify either ERC20 or ETH + if network is not None: + networks = self.safe_dict(self.options, 'networks', {}) + network = self.safe_string(networks, network.upper(), network) # handle ETH>ERC20 alias + request['chain'] = currency['id'] + '-' + network + params = self.omit(params, 'network') + fee = self.safe_string(params, 'fee') + if fee is None: + currencies = self.fetch_currencies() + self.currencies = self.map_to_safe_map(self.deep_extend(self.currencies, currencies)) + targetNetwork = self.safe_dict(currency['networks'], self.network_id_to_code(network), {}) + fee = self.safe_string(targetNetwork, 'fee') + if fee is None: + raise ArgumentsRequired(self.id + ' withdraw() requires a "fee" string parameter, network transaction fee must be ≥ 0. Withdrawals to OKCoin or OKX are fee-free, please set "0". Withdrawing to external digital asset address requires network transaction fee.') + request['fee'] = self.number_to_string(fee) # withdrawals to OKCoin or OKX are fee-free, please set 0 + query = self.omit(params, ['fee']) + response = self.privatePostAssetWithdrawal(self.extend(request, query)) + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "amt": "0.1", + # "wdId": "67485", + # "ccy": "BTC" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + transaction = self.safe_dict(data, 0) + return self.parse_transaction(transaction, currency) + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://www.okx.com/docs-v5/en/#rest-api-funding-get-deposit-history + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch entries for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchDeposits', 'paginate') + if paginate: + return self.fetch_paginated_call_dynamic('fetchDeposits', code, since, limit, params) + request: dict = { + # 'ccy': currency['id'], + # 'state': 2, # 0 waiting for confirmation, 1 deposit credited, 2 deposit successful + # 'after': since, + # 'before' self.milliseconds(), + # 'limit': limit, # default 100, max 100 + } + currency = None + if code is not None: + currency = self.currency(code) + request['ccy'] = currency['id'] + if since is not None: + request['before'] = max(since - 1, 0) + if limit is not None: + request['limit'] = limit # default 100, max 100 + request, params = self.handle_until_option('after', request, params) + response = self.privateGetAssetDepositHistory(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "amt": "0.01044408", + # "txId": "1915737_3_0_0_asset", + # "ccy": "BTC", + # "from": "13801825426", + # "to": "", + # "ts": "1597026383085", + # "state": "2", + # "depId": "4703879" + # }, + # { + # "amt": "491.6784211", + # "txId": "1744594_3_184_0_asset", + # "ccy": "OKB", + # "from": "", + # "to": "", + # "ts": "1597026383085", + # "state": "2", + # "depId": "4703809" + # }, + # { + # "amt": "223.18782496", + # "txId": "6d892c669225b1092c780bf0da0c6f912fc7dc8f6b8cc53b003288624c", + # "ccy": "USDT", + # "from": "", + # "to": "39kK4XvgEuM7rX9frgyHoZkWqx4iKu1spD", + # "ts": "1597026383085", + # "state": "2", + # "depId": "4703779" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_transactions(data, currency, since, limit, params) + + def fetch_deposit(self, id: str, code: Str = None, params={}): + """ + fetch data on a currency deposit via the deposit id + + https://www.okx.com/docs-v5/en/#rest-api-funding-get-deposit-history + + :param str id: deposit id + :param str code: filter by currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + self.load_markets() + request: dict = { + 'depId': id, + } + currency = None + if code is not None: + currency = self.currency(code) + request['ccy'] = currency['id'] + response = self.privateGetAssetDepositHistory(self.extend(request, params)) + data = self.safe_value(response, 'data') + deposit = self.safe_dict(data, 0, {}) + return self.parse_transaction(deposit, currency) + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://www.okx.com/docs-v5/en/#rest-api-funding-get-withdrawal-history + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch entries for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchWithdrawals', 'paginate') + if paginate: + return self.fetch_paginated_call_dynamic('fetchWithdrawals', code, since, limit, params) + request: dict = { + # 'ccy': currency['id'], + # 'state': 2, # -3: pending cancel, -2 canceled, -1 failed, 0, pending, 1 sending, 2 sent, 3 awaiting email verification, 4 awaiting manual verification, 5 awaiting identity verification + # 'after': since, + # 'before': self.milliseconds(), + # 'limit': limit, # default 100, max 100 + } + currency = None + if code is not None: + currency = self.currency(code) + request['ccy'] = currency['id'] + if since is not None: + request['before'] = max(since - 1, 0) + if limit is not None: + request['limit'] = limit # default 100, max 100 + request, params = self.handle_until_option('after', request, params) + response = self.privateGetAssetWithdrawalHistory(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "amt": "0.094", + # "wdId": "4703879", + # "fee": "0.01000000eth", + # "txId": "0x62477bac6509a04512819bb1455e923a60dea5966c7caeaa0b24eb8fb0432b85", + # "ccy": "ETH", + # "from": "13426335357", + # "to": "0xA41446125D0B5b6785f6898c9D67874D763A1519", + # "ts": "1597026383085", + # "state": "2" + # }, + # { + # "amt": "0.01", + # "wdId": "4703879", + # "fee": "0.00000000btc", + # "txId": "", + # "ccy": "BTC", + # "from": "13426335357", + # "to": "13426335357", + # "ts": "1597026383085", + # "state": "2" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_transactions(data, currency, since, limit, params) + + def fetch_withdrawal(self, id: str, code: Str = None, params={}): + """ + fetch data on a currency withdrawal via the withdrawal id + + https://www.okx.com/docs-v5/en/#rest-api-funding-get-withdrawal-history + + :param str id: withdrawal id + :param str code: unified currency code of the currency withdrawn, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + self.load_markets() + request: dict = { + 'wdId': id, + } + currency = None + if code is not None: + currency = self.currency(code) + request['ccy'] = currency['id'] + response = self.privateGetAssetWithdrawalHistory(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "chain": "USDT-TRC20", + # "clientId": '', + # "fee": "0.8", + # "ccy": "USDT", + # "amt": "54.561", + # "txId": "00cff6ec7fa7c7d7d184bd84e82b9ff36863f07c0421188607f87dfa94e06b70", + # "from": "example@email.com", + # "to": "TEY6qjnKDyyq5jDc3DJizWLCdUySrpQ4yp", + # "state": "2", + # "ts": "1641376485000", + # "wdId": "25147041" + # } + # ], + # "msg": '' + # } + # + data = self.safe_list(response, 'data', []) + withdrawal = self.safe_dict(data, 0, {}) + return self.parse_transaction(withdrawal) + + def parse_transaction_status(self, status: Str): + # + # deposit statuses + # + # { + # "0": "waiting for confirmation", + # "1": "deposit credited", + # "2": "deposit successful" + # } + # + # withdrawal statuses + # + # { + # '-3': "pending cancel", + # "-2": "canceled", + # "-1": "failed", + # "0": "pending", + # "1": "sending", + # "2": "sent", + # "3": "awaiting email verification", + # "4": "awaiting manual verification", + # "5": "awaiting identity verification" + # } + # + statuses: dict = { + '-3': 'pending', + '-2': 'canceled', + '-1': 'failed', + '0': 'pending', + '1': 'pending', + '2': 'ok', + '3': 'pending', + '4': 'pending', + '5': 'pending', + '6': 'pending', + '7': 'pending', + '8': 'pending', + '9': 'pending', + '10': 'pending', + '12': 'pending', + '15': 'pending', + '16': 'pending', + } + return self.safe_string(statuses, status, status) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # withdraw + # + # { + # "amt": "0.1", + # "wdId": "67485", + # "ccy": "BTC" + # } + # + # fetchWithdrawals + # + # { + # "amt": "0.094", + # "wdId": "4703879", + # "fee": "0.01000000eth", + # "txId": "0x62477bac6509a04512819bb1455e923a60dea5966c7caeaa0b24eb8fb0432b85", + # "ccy": "ETH", + # "from": "13426335357", + # "to": "0xA41446125D0B5b6785f6898c9D67874D763A1519", + # "tag", + # "pmtId", + # "memo", + # "ts": "1597026383085", + # "state": "2" + # } + # + # fetchDeposits + # + # { + # "amt": "0.01044408", + # "txId": "1915737_3_0_0_asset", + # "ccy": "BTC", + # "from": "13801825426", + # "to": "", + # "ts": "1597026383085", + # "state": "2", + # "depId": "4703879" + # } + # + type = None + id = None + withdrawalId = self.safe_string(transaction, 'wdId') + addressFrom = self.safe_string(transaction, 'from') + addressTo = self.safe_string(transaction, 'to') + address = addressTo + tagTo = self.safe_string_2(transaction, 'tag', 'memo') + tagTo = self.safe_string_2(transaction, 'pmtId', tagTo) + if withdrawalId is not None: + type = 'withdrawal' + id = withdrawalId + else: + # the payment_id will appear on new deposits but appears to be removed from the response after 2 months + id = self.safe_string(transaction, 'depId') + type = 'deposit' + currencyId = self.safe_string(transaction, 'ccy') + code = self.safe_currency_code(currencyId) + amount = self.safe_number(transaction, 'amt') + status = self.parse_transaction_status(self.safe_string(transaction, 'state')) + txid = self.safe_string(transaction, 'txId') + timestamp = self.safe_integer(transaction, 'ts') + feeCost = None + if type == 'deposit': + feeCost = 0 + else: + feeCost = self.safe_number(transaction, 'fee') + # todo parse tags + return { + 'info': transaction, + 'id': id, + 'currency': code, + 'amount': amount, + 'network': None, + 'addressFrom': addressFrom, + 'addressTo': addressTo, + 'address': address, + 'tagFrom': None, + 'tagTo': tagTo, + 'tag': tagTo, + 'status': status, + 'type': type, + 'updated': None, + 'txid': txid, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'internal': None, + 'comment': None, + 'fee': { + 'currency': code, + 'cost': feeCost, + }, + } + + def fetch_leverage(self, symbol: str, params={}) -> Leverage: + """ + fetch the set leverage for a market + + https://www.okx.com/docs-v5/en/#rest-api-account-get-leverage + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' + :returns dict: a `leverage structure ` + """ + self.load_markets() + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchLeverage', params) + if marginMode is None: + marginMode = self.safe_string(params, 'mgnMode', 'cross') # cross marginMode + if (marginMode != 'cross') and (marginMode != 'isolated'): + raise BadRequest(self.id + ' fetchLeverage() requires a marginMode parameter that must be either cross or isolated') + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + 'mgnMode': marginMode, + } + response = self.privateGetAccountLeverageInfo(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "instId": "BTC-USDT-SWAP", + # "lever": "5.00000000", + # "mgnMode": "isolated", + # "posSide": "net" + # } + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_leverage(data, market) + + def parse_leverage(self, leverage: dict, market: Market = None) -> Leverage: + marketId = None + marginMode = None + longLeverage = None + shortLeverage = None + for i in range(0, len(leverage)): + entry = leverage[i] + marginMode = self.safe_string_lower(entry, 'mgnMode') + marketId = self.safe_string(entry, 'instId') + positionSide = self.safe_string_lower(entry, 'posSide') + if positionSide == 'long': + longLeverage = self.safe_integer(entry, 'lever') + elif positionSide == 'short': + shortLeverage = self.safe_integer(entry, 'lever') + else: + longLeverage = self.safe_integer(entry, 'lever') + shortLeverage = self.safe_integer(entry, 'lever') + return { + 'info': leverage, + 'symbol': self.safe_symbol(marketId, market), + 'marginMode': marginMode, + 'longLeverage': longLeverage, + 'shortLeverage': shortLeverage, + } + + def fetch_position(self, symbol: str, params={}): + """ + fetch data on a single open contract trade position + + https://www.okx.com/docs-v5/en/#rest-api-account-get-positions + + :param str symbol: unified market symbol of the market the position is held in, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.instType]: MARGIN, SWAP, FUTURES, OPTION + :returns dict: a `position structure ` + """ + self.load_markets() + market = self.market(symbol) + type, query = self.handle_market_type_and_params('fetchPosition', market, params) + request: dict = { + # instType str No Instrument type, MARGIN, SWAP, FUTURES, OPTION + 'instId': market['id'], + # posId str No Single position ID or multiple position IDs(no more than 20) separated with comma + } + if type is not None: + request['instType'] = self.convert_to_instrument_type(type) + response = self.privateGetAccountPositions(self.extend(request, query)) + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "adl": "1", + # "availPos": "1", + # "avgPx": "2566.31", + # "cTime": "1619507758793", + # "ccy": "ETH", + # "deltaBS": "", + # "deltaPA": "", + # "gammaBS": "", + # "gammaPA": "", + # "imr": "", + # "instId": "ETH-USD-210430", + # "instType": "FUTURES", + # "interest": "0", + # "last": "2566.22", + # "lever": "10", + # "liab": "", + # "liabCcy": "", + # "liqPx": "2352.8496681818233", + # "margin": "0.0003896645377994", + # "mgnMode": "isolated", + # "mgnRatio": "11.731726509588816", + # "mmr": "0.0000311811092368", + # "optVal": "", + # "pTime": "1619507761462", + # "pos": "1", + # "posCcy": "", + # "posId": "307173036051017730", + # "posSide": "long", + # "thetaBS": "", + # "thetaPA": "", + # "tradeId": "109844", + # "uTime": "1619507761462", + # "upl": "-0.0000009932766034", + # "uplRatio": "-0.0025490556801078", + # "vegaBS": "", + # "vegaPA": "" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + position = self.safe_dict(data, 0) + if position is None: + return None + return self.parse_position(position, market) + + def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + + https://www.okx.com/docs-v5/en/#rest-api-account-get-positions + https://www.okx.com/docs-v5/en/#trading-account-rest-api-get-positions-history history + + fetch all open positions + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.instType]: MARGIN, SWAP, FUTURES, OPTION + :returns dict[]: a list of `position structure ` + """ + self.load_markets() + request: dict = { + # 'instType': 'MARGIN', # optional string, MARGIN, SWAP, FUTURES, OPTION + # 'instId': market['id'], # optional string, e.g. 'BTC-USD-190927-5000-C' + # 'posId': '307173036051017730', # optional string, Single or multiple position IDs(no more than 20) separated with commas + } + if symbols is not None: + marketIds = [] + for i in range(0, len(symbols)): + entry = symbols[i] + market = self.market(entry) + marketIds.append(market['id']) + marketIdsLength = len(marketIds) + if marketIdsLength > 0: + request['instId'] = ','.join(marketIds) + fetchPositionsOptions = self.safe_dict(self.options, 'fetchPositions', {}) + method = self.safe_string(fetchPositionsOptions, 'method', 'privateGetAccountPositions') + response = None + if method == 'privateGetAccountPositionsHistory': + response = self.privateGetAccountPositionsHistory(self.extend(request, params)) + else: + response = self.privateGetAccountPositions(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "adl": "1", + # "availPos": "1", + # "avgPx": "2566.31", + # "cTime": "1619507758793", + # "ccy": "ETH", + # "deltaBS": "", + # "deltaPA": "", + # "gammaBS": "", + # "gammaPA": "", + # "imr": "", + # "instId": "ETH-USD-210430", + # "instType": "FUTURES", + # "interest": "0", + # "last": "2566.22", + # "lever": "10", + # "liab": "", + # "liabCcy": "", + # "liqPx": "2352.8496681818233", + # "margin": "0.0003896645377994", + # "mgnMode": "isolated", + # "mgnRatio": "11.731726509588816", + # "mmr": "0.0000311811092368", + # "optVal": "", + # "pTime": "1619507761462", + # "pos": "1", + # "posCcy": "", + # "posId": "307173036051017730", + # "posSide": "long", + # "thetaBS": "", + # "thetaPA": "", + # "tradeId": "109844", + # "uTime": "1619507761462", + # "upl": "-0.0000009932766034", + # "uplRatio": "-0.0025490556801078", + # "vegaBS": "", + # "vegaPA": "" + # } + # ] + # } + # + positions = self.safe_list(response, 'data', []) + result = [] + for i in range(0, len(positions)): + result.append(self.parse_position(positions[i])) + return self.filter_by_array_positions(result, 'symbol', self.market_symbols(symbols), False) + + def fetch_positions_for_symbol(self, symbol: str, params={}): + """ + + https://www.okx.com/docs-v5/en/#rest-api-account-get-positions + + fetch all open positions for specific symbol + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.instType]: MARGIN(if needed) + :returns dict[]: a list of `position structure ` + """ + return self.fetch_positions([symbol], params) + + def parse_position(self, position: dict, market: Market = None): + # + # { + # "adl": "3", + # "availPos": "1", + # "avgPx": "34131.1", + # "cTime": "1627227626502", + # "ccy": "USDT", + # "deltaBS": "", + # "deltaPA": "", + # "gammaBS": "", + # "gammaPA": "", + # "imr": "170.66093041794787", + # "instId": "BTC-USDT-SWAP", + # "instType": "SWAP", + # "interest": "0", + # "last": "34134.4", + # "lever": "2", + # "liab": "", + # "liabCcy": "", + # "liqPx": "12608.959083877446", + # "markPx": "4786.459271773621", + # "margin": "", + # "mgnMode": "cross", + # "mgnRatio": "140.49930117599155", + # "mmr": "1.3652874433435829", + # "notionalUsd": "341.5130010779638", + # "optVal": "", + # "pos": "1", + # "posCcy": "", + # "posId": "339552508062380036", + # "posSide": "long", + # "thetaBS": "", + # "thetaPA": "", + # "tradeId": "98617799", + # "uTime": "1627227626502", + # "upl": "0.0108608358957281", + # "uplRatio": "0.0000636418743944", + # "vegaBS": "", + # "vegaPA": "" + # } + # history + # { + # "cTime":"1708351230102", + # "ccy":"USDT", + # "closeAvgPx":"1.2567", + # "closeTotalPos":"40", + # "direction":"short", + # "fee":"-0.0351036", + # "fundingFee":"0", + # "instId":"SUSHI-USDT-SWAP", + # "instType":"SWAP", + # "lever":"10.0", + # "liqPenalty":"0", + # "mgnMode":"isolated", + # "openAvgPx":"1.2462", + # "openMaxPos":"40", + # "pnl":"-0.42", + # "pnlRatio":"-0.0912982667308618", + # "posId":"666159086676836352", + # "realizedPnl":"-0.4551036", + # "triggerPx":"", + # "type":"2", + # "uTime":"1708354805699", + # "uly":"SUSHI-USDT" + # } + # + marketId = self.safe_string(position, 'instId') + market = self.safe_market(marketId, market, None, 'contract') + symbol = market['symbol'] + pos = self.safe_string(position, 'pos') # 'pos' field: One way mode: 0 if position is not open, 1 if open | Two way(hedge) mode: -1 if short, 1 if long, 0 if position is not open + contractsAbs = Precise.string_abs(pos) + side = self.safe_string_2(position, 'posSide', 'direction') + hedged = side != 'net' + contracts = self.parse_number(contractsAbs) + if market['margin']: + # margin position + if side == 'net': + posCcy = self.safe_string(position, 'posCcy') + parsedCurrency = self.safe_currency_code(posCcy) + if parsedCurrency is not None: + side = 'long' if (market['base'] == parsedCurrency) else 'short' + if side is None: + side = self.safe_string(position, 'direction') + else: + if pos is not None: + if side == 'net': + if Precise.string_gt(pos, '0'): + side = 'long' + elif Precise.string_lt(pos, '0'): + side = 'short' + else: + side = None + contractSize = self.safe_number(market, 'contractSize') + contractSizeString = self.number_to_string(contractSize) + markPriceString = self.safe_string(position, 'markPx') + notionalString = self.safe_string(position, 'notionalUsd') + if market['inverse']: + notionalString = Precise.string_div(Precise.string_mul(contractsAbs, contractSizeString), markPriceString) + notional = self.parse_number(notionalString) + marginMode = self.safe_string(position, 'mgnMode') + initialMarginString = None + entryPriceString = self.safe_string_2(position, 'avgPx', 'openAvgPx') + unrealizedPnlString = self.safe_string(position, 'upl') + leverageString = self.safe_string(position, 'lever') + initialMarginPercentage = None + collateralString = None + if marginMode == 'cross': + initialMarginString = self.safe_string(position, 'imr') + collateralString = Precise.string_add(initialMarginString, unrealizedPnlString) + elif marginMode == 'isolated': + initialMarginPercentage = Precise.string_div('1', leverageString) + collateralString = self.safe_string(position, 'margin') + maintenanceMarginString = self.safe_string(position, 'mmr') + maintenanceMargin = self.parse_number(maintenanceMarginString) + maintenanceMarginPercentageString = Precise.string_div(maintenanceMarginString, notionalString) + if initialMarginPercentage is None: + initialMarginPercentage = self.parse_number(Precise.string_div(initialMarginString, notionalString, 4)) + elif initialMarginString is None: + initialMarginString = Precise.string_mul(initialMarginPercentage, notionalString) + rounder = '0.00005' # round to closest 0.01% + maintenanceMarginPercentage = self.parse_number(Precise.string_div(Precise.string_add(maintenanceMarginPercentageString, rounder), '1', 4)) + liquidationPrice = self.safe_number(position, 'liqPx') + percentageString = self.safe_string(position, 'uplRatio') + percentage = self.parse_number(Precise.string_mul(percentageString, '100')) + timestamp = self.safe_integer(position, 'cTime') + marginRatio = self.parse_number(Precise.string_div(maintenanceMarginString, collateralString, 4)) + return self.safe_position({ + 'info': position, + 'id': self.safe_string(position, 'posId'), + 'symbol': symbol, + 'notional': notional, + 'marginMode': marginMode, + 'liquidationPrice': liquidationPrice, + 'entryPrice': self.parse_number(entryPriceString), + 'unrealizedPnl': self.parse_number(unrealizedPnlString), + 'realizedPnl': self.safe_number(position, 'realizedPnl'), + 'percentage': percentage, + 'contracts': contracts, + 'contractSize': contractSize, + 'markPrice': self.parse_number(markPriceString), + 'lastPrice': self.safe_number(position, 'closeAvgPx'), + 'side': side, + 'hedged': hedged, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastUpdateTimestamp': self.safe_integer(position, 'uTime'), + 'maintenanceMargin': maintenanceMargin, + 'maintenanceMarginPercentage': maintenanceMarginPercentage, + 'collateral': self.parse_number(collateralString), + 'initialMargin': self.parse_number(initialMarginString), + 'initialMarginPercentage': self.parse_number(initialMarginPercentage), + 'leverage': self.parse_number(leverageString), + 'marginRatio': marginRatio, + 'stopLossPrice': None, + 'takeProfitPrice': None, + }) + + def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://www.okx.com/docs-v5/en/#rest-api-funding-funds-transfer + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account to transfer from + :param str toAccount: account to transfer to + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transfer structure ` + """ + self.load_markets() + currency = self.currency(code) + accountsByType = self.safe_dict(self.options, 'accountsByType', {}) + fromId = self.safe_string(accountsByType, fromAccount, fromAccount) + toId = self.safe_string(accountsByType, toAccount, toAccount) + request: dict = { + 'ccy': currency['id'], + 'amt': self.currency_to_precision(code, amount), + 'type': '0', # 0 = transfer within account by default, 1 = master account to sub-account, 2 = sub-account to master account, 3 = sub-account to master account(Only applicable to APIKey from sub-account), 4 = sub-account to sub-account + 'from': fromId, # remitting account, 6: Funding account, 18: Trading account + 'to': toId, # beneficiary account, 6: Funding account, 18: Trading account + # 'subAcct': 'sub-account-name', # optional, only required when type is 1, 2 or 4 + # 'loanTrans': False, # Whether or not borrowed coins can be transferred out under Multi-currency margin and Portfolio margin. The default is False + # 'clientId': 'client-supplied id', # A combination of case-sensitive alphanumerics, all numbers, or all letters of up to 32 characters + # 'omitPosRisk': False, # Ignore position risk. Default is False. Applicable to Portfolio margin + } + if fromId == 'master': + request['type'] = '1' + request['subAcct'] = toId + request['from'] = self.safe_string(params, 'from', '6') + request['to'] = self.safe_string(params, 'to', '6') + elif toId == 'master': + request['type'] = '2' + request['subAcct'] = fromId + request['from'] = self.safe_string(params, 'from', '6') + request['to'] = self.safe_string(params, 'to', '6') + response = self.privatePostAssetTransfer(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "transId": "754147", + # "ccy": "USDT", + # "from": "6", + # "amt": "0.1", + # "to": "18" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + rawTransfer = self.safe_dict(data, 0, {}) + return self.parse_transfer(rawTransfer, currency) + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + # + # transfer + # + # { + # "transId": "754147", + # "ccy": "USDT", + # "from": "6", + # "amt": "0.1", + # "to": "18" + # } + # + # fetchTransfer + # + # { + # "amt": "5", + # "ccy": "USDT", + # "from": "18", + # "instId": "", + # "state": "success", + # "subAcct": "", + # "to": "6", + # "toInstId": "", + # "transId": "464424732", + # "type": "0" + # } + # + # fetchTransfers + # + # { + # "bal": "70.6874353780312913", + # "balChg": "-4.0000000000000000", # negative means "to funding", positive meand "from funding" + # "billId": "588900695232225299", + # "ccy": "USDT", + # "execType": "", + # "fee": "", + # "from": "18", + # "instId": "", + # "instType": "", + # "mgnMode": "", + # "notes": "To Funding Account", + # "ordId": "", + # "pnl": "", + # "posBal": "", + # "posBalChg": "", + # "price": "0", + # "subType": "12", + # "sz": "-4", + # "to": "6", + # "ts": "1686676866989", + # "type": "1" + # } + # + id = self.safe_string_2(transfer, 'transId', 'billId') + currencyId = self.safe_string(transfer, 'ccy') + code = self.safe_currency_code(currencyId, currency) + amount = self.safe_number(transfer, 'amt') + fromAccountId = self.safe_string(transfer, 'from') + toAccountId = self.safe_string(transfer, 'to') + accountsById = self.safe_dict(self.options, 'accountsById', {}) + timestamp = self.safe_integer(transfer, 'ts') + balanceChange = self.safe_string(transfer, 'sz') + if balanceChange is not None: + amount = self.parse_number(Precise.string_abs(balanceChange)) + return { + 'info': transfer, + 'id': id, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'currency': code, + 'amount': amount, + 'fromAccount': self.safe_string(accountsById, fromAccountId), + 'toAccount': self.safe_string(accountsById, toAccountId), + 'status': self.parse_transfer_status(self.safe_string(transfer, 'state')), + } + + def parse_transfer_status(self, status: Str) -> Str: + statuses: dict = { + 'success': 'ok', + } + return self.safe_string(statuses, status, status) + + def fetch_transfer(self, id: str, code: Str = None, params={}) -> TransferEntry: + self.load_markets() + request: dict = { + 'transId': id, + # 'type': 0, # default is 0 transfer within account, 1 master to sub, 2 sub to master + } + response = self.privateGetAssetTransferState(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "amt": "5", + # "ccy": "USDT", + # "from": "18", + # "instId": "", + # "state": "success", + # "subAcct": "", + # "to": "6", + # "toInstId": "", + # "transId": "464424732", + # "type": "0" + # } + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + transfer = self.safe_dict(data, 0) + return self.parse_transfer(transfer) + + def fetch_transfers(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[TransferEntry]: + """ + fetch a history of internal transfers made on an account + + https://www.okx.com/docs-v5/en/#trading-account-rest-api-get-bills-details-last-3-months + + :param str code: unified currency code of the currency transferred + :param int [since]: the earliest time in ms to fetch transfers for + :param int [limit]: the maximum number of transfers structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transfer structures ` + """ + self.load_markets() + currency = None + request: dict = { + 'type': '1', # https://www.okx.com/docs-v5/en/#rest-api-account-get-bills-details-last-3-months + } + if code is not None: + currency = self.currency(code) + request['ccy'] = currency['id'] + if since is not None: + request['begin'] = since + if limit is not None: + request['limit'] = limit + response = self.privateGetAccountBillsArchive(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "bal": "70.6874353780312913", + # "balChg": "-4.0000000000000000", + # "billId": "588900695232225299", + # "ccy": "USDT", + # "execType": "", + # "fee": "", + # "from": "18", + # "instId": "", + # "instType": "", + # "mgnMode": "", + # "notes": "To Funding Account", + # "ordId": "", + # "pnl": "", + # "posBal": "", + # "posBalChg": "", + # "price": "0", + # "subType": "12", + # "sz": "-4", + # "to": "6", + # "ts": "1686676866989", + # "type": "1" + # }, + # ... + # ], + # "msg": "" + # } + # + transfers = self.safe_list(response, 'data', []) + return self.parse_transfers(transfers, currency, since, limit, params) + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + isArray = isinstance(params, list) + request = '/api/' + self.version + '/' + self.implode_params(path, params) + query = self.omit(params, self.extract_params(path)) + url = self.implode_hostname(self.urls['api']['rest']) + request + # type = self.getPathAuthenticationType(path) + if api == 'public': + if query: + url += '?' + self.urlencode(query) + elif api == 'private': + self.check_required_credentials() + # inject id in implicit api call + if method == 'POST' and (path == 'trade/batch-orders' or path == 'trade/order-algo' or path == 'trade/order'): + brokerId = self.safe_string(self.options, 'brokerId', '6b9ad766b55dBCDE') + if isinstance(params, list): + for i in range(0, len(params)): + entry = params[i] + clientOrderId = self.safe_string(entry, 'clOrdId') + if clientOrderId is None: + entry['clOrdId'] = brokerId + self.uuid16() + entry['tag'] = brokerId + params[i] = entry + else: + clientOrderId = self.safe_string(params, 'clOrdId') + if clientOrderId is None: + params['clOrdId'] = brokerId + self.uuid16() + params['tag'] = brokerId + timestamp = self.iso8601(self.nonce()) + headers = { + 'OK-ACCESS-KEY': self.apiKey, + 'OK-ACCESS-PASSPHRASE': self.password, + 'OK-ACCESS-TIMESTAMP': timestamp, + # 'OK-FROM': '', + # 'OK-TO': '', + # 'OK-LIMIT': '', + } + auth = timestamp + method + request + if method == 'GET': + if query: + urlencodedQuery = '?' + self.urlencode(query) + url += urlencodedQuery + auth += urlencodedQuery + else: + if isArray or query: + body = self.json(query) + auth += body + headers['Content-Type'] = 'application/json' + signature = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256, 'base64') + headers['OK-ACCESS-SIGN'] = signature + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def parse_funding_rate(self, contract, market: Market = None) -> FundingRate: + # + # { + # "fundingRate": "0.00027815", + # "fundingTime": "1634256000000", + # "instId": "BTC-USD-SWAP", + # "instType": "SWAP", + # "nextFundingRate": "0.00017", + # "nextFundingTime": "1634284800000" + # } + # ws + # { + # "fundingRate":"0.0001875391284828", + # "fundingTime":"1700726400000", + # "instId":"BTC-USD-SWAP", + # "instType":"SWAP", + # "method": "next_period", + # "maxFundingRate":"0.00375", + # "minFundingRate":"-0.00375", + # "nextFundingRate":"0.0002608059239328", + # "nextFundingTime":"1700755200000", + # "premium": "0.0001233824646391", + # "settFundingRate":"0.0001699799259033", + # "settState":"settled", + # "ts":"1700724675402" + # } + # + # in the response above nextFundingRate is actually two funding rates from now + # + nextFundingRateTimestamp = self.safe_integer(contract, 'nextFundingTime') + marketId = self.safe_string(contract, 'instId') + symbol = self.safe_symbol(marketId, market) + nextFundingRate = self.safe_number(contract, 'nextFundingRate') + fundingTime = self.safe_integer(contract, 'fundingTime') + fundingTimeString = self.safe_string(contract, 'fundingTime') + nextFundingTimeString = self.safe_string(contract, 'nextFundingTime') + millisecondsInterval = Precise.string_sub(nextFundingTimeString, fundingTimeString) + # https://www.okx.com/support/hc/en-us/articles/360053909272-Ⅸ-Introduction-to-perpetual-swap-funding-fee + # > The current interest is 0. + return { + 'info': contract, + 'symbol': symbol, + 'markPrice': None, + 'indexPrice': None, + 'interestRate': self.parse_number('0'), + 'estimatedSettlePrice': None, + 'timestamp': None, + 'datetime': None, + 'fundingRate': self.safe_number(contract, 'fundingRate'), + 'fundingTimestamp': fundingTime, + 'fundingDatetime': self.iso8601(fundingTime), + 'nextFundingRate': nextFundingRate, + 'nextFundingTimestamp': nextFundingRateTimestamp, + 'nextFundingDatetime': self.iso8601(nextFundingRateTimestamp), + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': self.parse_funding_interval(millisecondsInterval), + } + + def parse_funding_interval(self, interval): + intervals: dict = { + '3600000': '1h', + '14400000': '4h', + '28800000': '8h', + '57600000': '16h', + '86400000': '24h', + } + return self.safe_string(intervals, interval, interval) + + def fetch_funding_interval(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate interval + + https://www.okx.com/docs-v5/en/#public-data-rest-api-get-funding-rate + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + return self.fetch_funding_rate(symbol, params) + + def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate + + https://www.okx.com/docs-v5/en/#public-data-rest-api-get-funding-rate + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + self.load_markets() + market = self.market(symbol) + if not market['swap']: + raise ExchangeError(self.id + ' fetchFundingRate() is only valid for swap markets') + request: dict = { + 'instId': market['id'], + } + response = self.publicGetPublicFundingRate(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "fundingRate": "0.00027815", + # "fundingTime": "1634256000000", + # "instId": "BTC-USD-SWAP", + # "instType": "SWAP", + # "nextFundingRate": "0.00017", + # "nextFundingTime": "1634284800000" + # } + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + entry = self.safe_dict(data, 0, {}) + return self.parse_funding_rate(entry, market) + + def fetch_funding_rates(self, symbols: Strings = None, params={}) -> FundingRates: + """ + fetches the current funding rates for multiple symbols + + https://www.okx.com/docs-v5/en/#public-data-rest-api-get-funding-rate + + :param str[] symbols: unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `funding rates structure ` + """ + self.load_markets() + symbols = self.market_symbols(symbols, 'swap', True) + request: dict = {'instId': 'ANY'} + response = self.publicGetPublicFundingRate(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "fundingRate": "0.00027815", + # "fundingTime": "1634256000000", + # "instId": "BTC-USD-SWAP", + # "instType": "SWAP", + # "nextFundingRate": "0.00017", + # "nextFundingTime": "1634284800000" + # } + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_funding_rates(data, symbols) + + def fetch_funding_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch the history of funding payments paid and received on self account + + https://www.okx.com/docs-v5/en/#trading-account-rest-api-get-bills-details-last-3-months + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch funding history for + :param int [limit]: the maximum number of funding history structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding history structure ` + """ + self.load_markets() + request: dict = { + # 'instType': 'SPOT', # SPOT, MARGIN, SWAP, FUTURES, OPTION + # 'ccy': currency['id'], + # 'mgnMode': 'isolated', # isolated, cross + # 'ctType': 'linear', # linear, inverse, only applicable to FUTURES/SWAP + 'type': '8', + # + # supported values for type + # + # 1 Transfer + # 2 Trade + # 3 Delivery + # 4 Auto token conversion + # 5 Liquidation + # 6 Margin transfer + # 7 Interest deduction + # 8 Funding fee + # 9 ADL + # 10 Clawback + # 11 System token conversion + # 12 Strategy transfer + # 13 ddh + # + # 'subType': '', + # + # supported values for subType + # + # 1 Buy + # 2 Sell + # 3 Open long + # 4 Open short + # 5 Close long + # 6 Close short + # 9 Interest deduction + # 11 Transfer in + # 12 Transfer out + # 160 Manual margin increase + # 161 Manual margin decrease + # 162 Auto margin increase + # 110 Auto buy + # 111 Auto sell + # 118 System token conversion transfer in + # 119 System token conversion transfer out + # 100 Partial liquidation close long + # 101 Partial liquidation close short + # 102 Partial liquidation buy + # 103 Partial liquidation sell + # 104 Liquidation long + # 105 Liquidation short + # 106 Liquidation buy + # 107 Liquidation sell + # 110 Liquidation transfer in + # 111 Liquidation transfer out + # 125 ADL close long + # 126 ADL close short + # 127 ADL buy + # 128 ADL sell + # 131 ddh buy + # 132 ddh sell + # 170 Exercised + # 171 Counterparty exercised + # 172 Expired OTM + # 112 Delivery long + # 113 Delivery short + # 117 Delivery/Exercise clawback + # 173 Funding fee expense + # 174 Funding fee income + # 200 System transfer in + # 201 Manually transfer in + # 202 System transfer out + # 203 Manually transfer out + # + # "after": "id", # earlier than the requested bill ID + # "before": "id", # newer than the requested bill ID + # "limit": "100", # default 100, max 100 + } + if limit is not None: + request['limit'] = str(limit) # default 100, max 100 + market = None + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + if market['contract']: + if market['linear']: + request['ctType'] = 'linear' + request['ccy'] = market['quoteId'] + else: + request['ctType'] = 'inverse' + request['ccy'] = market['baseId'] + type, query = self.handle_market_type_and_params('fetchFundingHistory', market, params) + if type == 'swap': + request['instType'] = self.convert_to_instrument_type(type) + # AccountBillsArchive has the same cost but supports three months of data + response = self.privateGetAccountBillsArchive(self.extend(request, query)) + # + # { + # "bal": "0.0242946200998573", + # "balChg": "0.0000148752712240", + # "billId": "377970609204146187", + # "ccy": "ETH", + # "execType": "", + # "fee": "0", + # "from": "", + # "instId": "ETH-USD-SWAP", + # "instType": "SWAP", + # "mgnMode": "isolated", + # "notes": "", + # "ordId": "", + # "pnl": "0.000014875271224", + # "posBal": "0", + # "posBalChg": "0", + # "subType": "174", + # "sz": "9", + # "to": "", + # "ts": "1636387215588", + # "type": "8" + # } + # + data = self.safe_list(response, 'data', []) + result = [] + for i in range(0, len(data)): + entry = data[i] + timestamp = self.safe_integer(entry, 'ts') + instId = self.safe_string(entry, 'instId') + marketInner = self.safe_market(instId) + currencyId = self.safe_string(entry, 'ccy') + code = self.safe_currency_code(currencyId) + result.append({ + 'info': entry, + 'symbol': marketInner['symbol'], + 'code': code, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': self.safe_string(entry, 'billId'), + 'amount': self.safe_number(entry, 'balChg'), + }) + sorted = self.sort_by(result, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, symbol, since, limit) + + def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://www.okx.com/docs-v5/en/#rest-api-account-set-leverage + + :param float leverage: the rate of leverage + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' + :param str [params.posSide]: 'long' or 'short' or 'net' for isolated margin long/short mode on futures and swap markets, default is 'net' + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setLeverage() requires a symbol argument') + # WARNING: THIS WILL INCREASE LIQUIDATION PRICE FOR OPEN ISOLATED LONG POSITIONS + # AND DECREASE LIQUIDATION PRICE FOR OPEN ISOLATED SHORT POSITIONS + if (leverage < 1) or (leverage > 125): + raise BadRequest(self.id + ' setLeverage() leverage should be between 1 and 125') + self.load_markets() + market = self.market(symbol) + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('setLeverage', params) + if marginMode is None: + marginMode = self.safe_string(params, 'mgnMode', 'cross') # cross marginMode + if (marginMode != 'cross') and (marginMode != 'isolated'): + raise BadRequest(self.id + ' setLeverage() requires a marginMode parameter that must be either cross or isolated') + request: dict = { + 'lever': leverage, + 'mgnMode': marginMode, + 'instId': market['id'], + } + posSide = self.safe_string(params, 'posSide', 'net') + if marginMode == 'isolated': + if posSide != 'long' and posSide != 'short' and posSide != 'net': + raise BadRequest(self.id + ' setLeverage() requires the posSide argument to be either "long", "short" or "net"') + request['posSide'] = posSide + response = self.privatePostAccountSetLeverage(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "instId": "BTC-USDT-SWAP", + # "lever": "5", + # "mgnMode": "isolated", + # "posSide": "long" + # } + # ], + # "msg": "" + # } + # + return response + + def fetch_position_mode(self, symbol: Str = None, params={}): + """ + + https://www.okx.com/docs-v5/en/#trading-account-rest-api-get-account-configuration + + fetchs the position mode, hedged or one way, hedged for binance is set identically for all linear markets or all inverse markets + :param str symbol: unified symbol of the market to fetch the order book for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.accountId]: if you have multiple accounts, you must specify the account id to fetch the position mode + :returns dict: an object detailing whether the market is in hedged or one-way mode + """ + accounts = self.fetch_accounts() + length = len(accounts) + selectedAccount = None + if length > 1: + accountId = self.safe_string(params, 'accountId') + if accountId is None: + accountIds = self.get_list_from_object_values(accounts, 'id') + raise ExchangeError(self.id + ' fetchPositionMode() can not detect position mode, because you have multiple accounts. Set params["accountId"] to desired id from: ' + ', '.join(accountIds)) + else: + accountsById = self.index_by(accounts, 'id') + selectedAccount = self.safe_dict(accountsById, accountId) + else: + selectedAccount = accounts[0] + mainAccount = selectedAccount['info'] + posMode = self.safe_string(mainAccount, 'posMode') # long_short_mode, net_mode + isHedged = posMode == 'long_short_mode' + return { + 'info': mainAccount, + 'hedged': isHedged, + } + + def set_position_mode(self, hedged: bool, symbol: Str = None, params={}): + """ + set hedged to True or False for a market + + https://www.okx.com/docs-v5/en/#trading-account-rest-api-set-position-mode + + :param bool hedged: set to True to use long_short_mode, False for net_mode + :param str symbol: not used by okx setPositionMode + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + hedgeMode = None + if hedged: + hedgeMode = 'long_short_mode' + else: + hedgeMode = 'net_mode' + request: dict = { + 'posMode': hedgeMode, + } + response = self.privatePostAccountSetPositionMode(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "posMode": "net_mode" + # } + # ], + # "msg": "" + # } + # + return response + + def set_margin_mode(self, marginMode: str, symbol: Str = None, params={}): + """ + set margin mode to 'cross' or 'isolated' + + https://www.okx.com/docs-v5/en/#trading-account-rest-api-set-leverage + + :param str marginMode: 'cross' or 'isolated' + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.leverage]: leverage + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setMarginMode() requires a symbol argument') + # WARNING: THIS WILL INCREASE LIQUIDATION PRICE FOR OPEN ISOLATED LONG POSITIONS + # AND DECREASE LIQUIDATION PRICE FOR OPEN ISOLATED SHORT POSITIONS + marginMode = marginMode.lower() + if (marginMode != 'cross') and (marginMode != 'isolated'): + raise BadRequest(self.id + ' setMarginMode() marginMode must be either cross or isolated') + self.load_markets() + market = self.market(symbol) + lever = self.safe_integer_2(params, 'lever', 'leverage') + if (lever is None) or (lever < 1) or (lever > 125): + raise BadRequest(self.id + ' setMarginMode() params["lever"] should be between 1 and 125') + params = self.omit(params, ['leverage']) + request: dict = { + 'lever': lever, + 'mgnMode': marginMode, + 'instId': market['id'], + } + response = self.privatePostAccountSetLeverage(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "instId": "BTC-USDT-SWAP", + # "lever": "5", + # "mgnMode": "isolated", + # "posSide": "long" + # } + # ], + # "msg": "" + # } + # + return response + + def fetch_cross_borrow_rates(self, params={}) -> CrossBorrowRates: + """ + fetch the borrow interest rates of all currencies + + https://www.okx.com/docs-v5/en/#trading-account-rest-api-get-interest-rate + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `borrow rate structures ` + """ + self.load_markets() + response = self.privateGetAccountInterestRate(params) + # + # { + # "code": "0", + # "data": [ + # { + # "ccy": "BTC", + # "interestRate": "0.00000833" + # } + # ... + # ], + # } + # + data = self.safe_list(response, 'data', []) + rates = [] + for i in range(0, len(data)): + rates.append(self.parse_borrow_rate(data[i])) + return rates + + def fetch_cross_borrow_rate(self, code: str, params={}) -> CrossBorrowRate: + """ + fetch the rate of interest to borrow a currency for margin trading + + https://www.okx.com/docs-v5/en/#trading-account-rest-api-get-interest-rate + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `borrow rate structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'ccy': currency['id'], + } + response = self.privateGetAccountInterestRate(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "ccy": "USDT", + # "interestRate": "0.00002065" + # } + # ... + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + rate = self.safe_dict(data, 0, {}) + return self.parse_borrow_rate(rate) + + def parse_borrow_rate(self, info, currency: Currency = None): + # + # { + # "amt": "992.10341195", + # "ccy": "BTC", + # "rate": "0.01", + # "ts": "1643954400000" + # } + # + ccy = self.safe_string(info, 'ccy') + timestamp = self.safe_integer(info, 'ts') + return { + 'currency': self.safe_currency_code(ccy), + 'rate': self.safe_number_2(info, 'interestRate', 'rate'), + 'period': 86400000, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'info': info, + } + + def parse_borrow_rate_histories(self, response, codes, since, limit): + # + # [ + # { + # "amt": "992.10341195", + # "ccy": "BTC", + # "rate": "0.01", + # "ts": "1643954400000" + # }, + # ... + # ] + # + borrowRateHistories: dict = {} + for i in range(0, len(response)): + item = response[i] + code = self.safe_currency_code(self.safe_string(item, 'ccy')) + if codes is None or self.in_array(code, codes): + if not (code in borrowRateHistories): + borrowRateHistories[code] = [] + borrowRateStructure = self.parse_borrow_rate(item) + borrrowRateCode = borrowRateHistories[code] + borrrowRateCode.append(borrowRateStructure) + keys = list(borrowRateHistories.keys()) + for i in range(0, len(keys)): + code = keys[i] + borrowRateHistories[code] = self.filter_by_currency_since_limit(borrowRateHistories[code], code, since, limit) + return borrowRateHistories + + def fetch_borrow_rate_histories(self, codes=None, since: Int = None, limit: Int = None, params={}): + """ + retrieves a history of a multiple currencies borrow interest rate at specific time slots, returns all currencies if no symbols passed, default is None + + https://www.okx.com/docs-v5/en/#financial-product-savings-get-public-borrow-history-public + + :param str[]|None codes: list of unified currency codes, default is None + :param int [since]: timestamp in ms of the earliest borrowRate, default is None + :param int [limit]: max number of borrow rate prices to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `borrow rate structures ` indexed by the market symbol + """ + self.load_markets() + request: dict = { + # 'ccy': currency['id'], + # 'after': self.milliseconds(), # Pagination of data to return records earlier than the requested ts, + # 'before': since, # Pagination of data to return records newer than the requested ts, + # 'limit': limit, # default is 100 and maximum is 100 + } + if since is not None: + request['before'] = since + if limit is not None: + request['limit'] = limit + response = self.publicGetFinanceSavingsLendingRateHistory(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "amt": "992.10341195", + # "ccy": "BTC", + # "rate": "0.01", + # "ts": "1643954400000" + # }, + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_borrow_rate_histories(data, codes, since, limit) + + def fetch_borrow_rate_history(self, code: str, since: Int = None, limit: Int = None, params={}): + """ + retrieves a history of a currencies borrow interest rate at specific time slots + + https://www.okx.com/docs-v5/en/#financial-product-savings-get-public-borrow-history-public + + :param str code: unified currency code + :param int [since]: timestamp for the earliest borrow rate + :param int [limit]: the maximum number of `borrow rate structures ` to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of `borrow rate structures ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'ccy': currency['id'], + # 'after': self.milliseconds(), # Pagination of data to return records earlier than the requested ts, + # 'before': since, # Pagination of data to return records newer than the requested ts, + # 'limit': limit, # default is 100 and maximum is 100 + } + if since is not None: + request['before'] = since + if limit is not None: + request['limit'] = limit + response = self.publicGetFinanceSavingsLendingRateHistory(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "amt": "992.10341195", + # "ccy": "BTC", + # "rate": "0.01", + # "ts": "1643954400000" + # }, + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_borrow_rate_history(data, code, since, limit) + + def modify_margin_helper(self, symbol: str, amount, type, params={}) -> MarginModification: + self.load_markets() + market = self.market(symbol) + posSide = self.safe_string(params, 'posSide', 'net') + params = self.omit(params, ['posSide']) + request: dict = { + 'instId': market['id'], + 'amt': amount, + 'type': type, + 'posSide': posSide, + } + response = self.privatePostAccountPositionMarginBalance(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "amt": "0.01", + # "instId": "ETH-USD-SWAP", + # "posSide": "net", + # "type": "reduce" + # } + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + entry = self.safe_dict(data, 0, {}) + errorCode = self.safe_string(response, 'code') + return self.extend(self.parse_margin_modification(entry, market), { + 'status': 'ok' if (errorCode == '0') else 'failed', + }) + + def parse_margin_modification(self, data: dict, market: Market = None) -> MarginModification: + # + # addMargin/reduceMargin + # + # { + # "amt": "0.01", + # "instId": "ETH-USD-SWAP", + # "posSide": "net", + # "type": "reduce" + # } + # + # fetchMarginAdjustmentHistory + # + # { + # bal: '67621.4325135010619812', + # balChg: '-10.0000000000000000', + # billId: '691293628710342659', + # ccy: 'USDT', + # clOrdId: '', + # execType: '', + # fee: '0', + # fillFwdPx: '', + # fillIdxPx: '', + # fillMarkPx: '', + # fillMarkVol: '', + # fillPxUsd: '', + # fillPxVol: '', + # fillTime: '1711089244850', + # from: '', + # instId: 'XRP-USDT-SWAP', + # instType: 'SWAP', + # interest: '0', + # mgnMode: 'isolated', + # notes: '', + # ordId: '', + # pnl: '0', + # posBal: '73.12', + # posBalChg: '10.00', + # px: '', + # subType: '160', + # sz: '10', + # tag: '', + # to: '', + # tradeId: '0', + # ts: '1711089244699', + # type: '6' + # } + # + amountRaw = self.safe_string_2(data, 'amt', 'posBalChg') + typeRaw = self.safe_string(data, 'type') + type = None + if typeRaw == '6': + type = 'add' if Precise.string_gt(amountRaw, '0') else 'reduce' + else: + type = typeRaw + amount = Precise.string_abs(amountRaw) + marketId = self.safe_string(data, 'instId') + responseMarket = self.safe_market(marketId, market) + code = responseMarket['base'] if responseMarket['inverse'] else responseMarket['quote'] + timestamp = self.safe_integer(data, 'ts') + return { + 'info': data, + 'symbol': responseMarket['symbol'], + 'type': type, + 'marginMode': 'isolated', + 'amount': self.parse_number(amount), + 'code': code, + 'total': None, + 'status': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + + def reduce_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + remove margin from a position + + https://www.okx.com/docs-v5/en/#trading-account-rest-api-increase-decrease-margin + + :param str symbol: unified market symbol + :param float amount: the amount of margin to remove + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin structure ` + """ + return self.modify_margin_helper(symbol, amount, 'reduce', params) + + def add_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + add margin + + https://www.okx.com/docs-v5/en/#trading-account-rest-api-increase-decrease-margin + + :param str symbol: unified market symbol + :param float amount: amount of margin to add + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin structure ` + """ + return self.modify_margin_helper(symbol, amount, 'add', params) + + def fetch_market_leverage_tiers(self, symbol: str, params={}) -> List[LeverageTier]: + """ + retrieve information on the maximum leverage, and maintenance margin for trades of varying trade sizes for a single market + + https://www.okx.com/docs-v5/en/#rest-api-public-data-get-position-tiers + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' + :returns dict: a `leverage tiers structure ` + """ + self.load_markets() + market = self.market(symbol) + type = 'MARGIN' if market['spot'] else self.convert_to_instrument_type(market['type']) + uly = self.safe_string(market['info'], 'uly') + if not uly: + if type != 'MARGIN': + raise BadRequest(self.id + ' fetchMarketLeverageTiers() cannot fetch leverage tiers for ' + symbol) + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchMarketLeverageTiers', params) + if marginMode is None: + marginMode = self.safe_string(params, 'tdMode', 'cross') # cross marginMode + request: dict = { + 'instType': type, + 'tdMode': marginMode, + 'uly': uly, + } + if type == 'MARGIN': + request['instId'] = market['id'] + response = self.publicGetPublicPositionTiers(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "baseMaxLoan": "500", + # "imr": "0.1", + # "instId": "ETH-USDT", + # "maxLever": "10", + # "maxSz": "500", + # "minSz": "0", + # "mmr": "0.03", + # "optMgnFactor": "0", + # "quoteMaxLoan": "200000", + # "tier": "1", + # "uly": "" + # }, + # ... + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_market_leverage_tiers(data, market) + + def parse_market_leverage_tiers(self, info, market: Market = None) -> List[LeverageTier]: + """ + @ignore + :param dict info: Exchange response for 1 market + :param dict market: CCXT market + """ + # + # [ + # { + # "baseMaxLoan": "500", + # "imr": "0.1", + # "instId": "ETH-USDT", + # "maxLever": "10", + # "maxSz": "500", + # "minSz": "0", + # "mmr": "0.03", + # "optMgnFactor": "0", + # "quoteMaxLoan": "200000", + # "tier": "1", + # "uly": "" + # }, + # ... + # ] + # + tiers = [] + for i in range(0, len(info)): + tier = info[i] + marketId = self.safe_string(tier, 'instId') + tiers.append({ + 'tier': self.safe_integer(tier, 'tier'), + 'symbol': self.safe_symbol(marketId, market), + 'currency': market['quote'], + 'minNotional': self.safe_number(tier, 'minSz'), + 'maxNotional': self.safe_number(tier, 'maxSz'), + 'maintenanceMarginRate': self.safe_number(tier, 'mmr'), + 'maxLeverage': self.safe_number(tier, 'maxLever'), + 'info': tier, + }) + return tiers + + def fetch_borrow_interest(self, code: Str = None, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[BorrowInterest]: + """ + fetch the interest owed b the user for borrowing currency for margin trading + + https://www.okx.com/docs-v5/en/#rest-api-account-get-interest-accrued-data + + :param str code: the unified currency code for the currency of the interest + :param str symbol: the market symbol of an isolated margin market, if None, the interest for cross margin markets is returned + :param int [since]: timestamp in ms of the earliest time to receive interest records for + :param int [limit]: the number of `borrow interest structures ` to retrieve + :param dict [params]: exchange specific parameters + :param int [params.type]: Loan type 1 - VIP loans 2 - Market loans *Default is Market loans* + :param str [params.marginMode]: 'cross' or 'isolated' + :returns dict[]: An list of `borrow interest structures ` + """ + self.load_markets() + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchBorrowInterest', params) + if marginMode is None: + marginMode = self.safe_string(params, 'mgnMode', 'cross') # cross marginMode + request: dict = { + 'mgnMode': marginMode, + } + market = None + if code is not None: + currency = self.currency(code) + request['ccy'] = currency['id'] + if since is not None: + request['before'] = since - 1 + if limit is not None: + request['limit'] = limit + if symbol is not None: + market = self.market(symbol) + request['instId'] = market['id'] + response = self.privateGetAccountInterestAccrued(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "ccy": "USDT", + # "instId": "", + # "interest": "0.0003960833333334", + # "interestRate": "0.0000040833333333", + # "liab": "97", + # "mgnMode": "", + # "ts": "1637312400000", + # "type": "1" + # }, + # ... + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + interest = self.parse_borrow_interests(data) + return self.filter_by_currency_since_limit(interest, code, since, limit) + + def parse_borrow_interest(self, info: dict, market: Market = None) -> BorrowInterest: + instId = self.safe_string(info, 'instId') + if instId is not None: + market = self.safe_market(instId, market) + timestamp = self.safe_integer(info, 'ts') + return { + 'info': info, + 'symbol': self.safe_string(market, 'symbol'), + 'currency': self.safe_currency_code(self.safe_string(info, 'ccy')), + 'interest': self.safe_number(info, 'interest'), + 'interestRate': self.safe_number(info, 'interestRate'), + 'amountBorrowed': self.safe_number(info, 'liab'), + 'marginMode': self.safe_string(info, 'mgnMode'), + 'timestamp': timestamp, # Interest accrued time + 'datetime': self.iso8601(timestamp), + } + + def borrow_cross_margin(self, code: str, amount: float, params={}): + """ + create a loan to borrow margin(need to be VIP 5 and above) + + https://www.okx.com/docs-v5/en/#trading-account-rest-api-vip-loans-borrow-and-repay + + :param str code: unified currency code of the currency to borrow + :param float amount: the amount to borrow + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin loan structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'ccy': currency['id'], + 'amt': self.currency_to_precision(code, amount), + 'side': 'borrow', + } + response = self.privatePostAccountBorrowRepay(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "amt": "102", + # "ccy": "USDT", + # "ordId": "544199684697214976", + # "side": "borrow", + # "state": "1" + # } + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + loan = self.safe_dict(data, 0, {}) + return self.parse_margin_loan(loan, currency) + + def repay_cross_margin(self, code: str, amount, params={}): + """ + repay borrowed margin and interest + + https://www.okx.com/docs-v5/en/#trading-account-rest-api-vip-loans-borrow-and-repay + + :param str code: unified currency code of the currency to repay + :param float amount: the amount to repay + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.id]: the order ID of borrowing, it is necessary while repaying + :returns dict: a `margin loan structure ` + """ + self.load_markets() + id = self.safe_string_2(params, 'id', 'ordId') + params = self.omit(params, 'id') + if id is None: + raise ArgumentsRequired(self.id + ' repayCrossMargin() requires an id parameter') + currency = self.currency(code) + request: dict = { + 'ccy': currency['id'], + 'amt': self.currency_to_precision(code, amount), + 'side': 'repay', + 'ordId': id, + } + response = self.privatePostAccountBorrowRepay(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "amt": "102", + # "ccy": "USDT", + # "ordId": "544199684697214976", + # "side": "repay", + # "state": "1" + # } + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + loan = self.safe_dict(data, 0, {}) + return self.parse_margin_loan(loan, currency) + + def parse_margin_loan(self, info, currency: Currency = None): + # + # { + # "amt": "102", + # "availLoan": "97", + # "ccy": "USDT", + # "loanQuota": "6000000", + # "posLoan": "0", + # "side": "repay", + # "usedLoan": "97" + # } + # + currencyId = self.safe_string(info, 'ccy') + return { + 'id': None, + 'currency': self.safe_currency_code(currencyId, currency), + 'amount': self.safe_number(info, 'amt'), + 'symbol': None, + 'timestamp': None, + 'datetime': None, + 'info': info, + } + + def fetch_open_interest(self, symbol: str, params={}): + """ + Retrieves the open interest of a currency + + https://www.okx.com/docs-v5/en/#rest-api-public-data-get-open-interest + + :param str symbol: Unified CCXT market symbol + :param dict [params]: exchange specific parameters + :returns dict} an open interest structure{@link https://docs.ccxt.com/#/?id=open-interest-structure: + """ + self.load_markets() + market = self.market(symbol) + if not market['contract']: + raise BadRequest(self.id + ' fetchOpenInterest() supports contract markets only') + type = self.convert_to_instrument_type(market['type']) + uly = self.safe_string(market['info'], 'uly') + request: dict = { + 'instType': type, + 'uly': uly, + 'instId': market['id'], + } + response = self.publicGetPublicOpenInterest(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "instId": "BTC-USDT-SWAP", + # "instType": "SWAP", + # "oi": "2125419", + # "oiCcy": "21254.19", + # "ts": "1664005108969" + # } + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_open_interest(data[0], market) + + def fetch_open_interests(self, symbols: Strings = None, params={}) -> OpenInterests: + """ + Retrieves the open interests of some currencies + + https://www.okx.com/docs-v5/en/#rest-api-public-data-get-open-interest + + :param str[] symbols: Unified CCXT market symbols + :param dict [params]: exchange specific parameters + :param str params['instType']: Instrument type, options: 'SWAP', 'FUTURES', 'OPTION', default to 'SWAP' + :param str params['uly']: Underlying, Applicable to FUTURES/SWAP/OPTION, if instType is 'OPTION', either uly or instFamily is required + :param str params['instFamily']: Instrument family, Applicable to FUTURES/SWAP/OPTION, if instType is 'OPTION', either uly or instFamily is required + :returns dict: an dictionary of `open interest structures ` + """ + self.load_markets() + symbols = self.market_symbols(symbols, None, True, True) + market = None + if symbols is not None: + market = self.market(symbols[0]) + marketType = None + marketType, params = self.handle_sub_type_and_params('fetchOpenInterests', market, params, 'swap') + instType = 'SWAP' + if marketType == 'future': + instType = 'FUTURES' + elif instType == 'option': + instType = 'OPTION' + request: dict = {'instType': instType} + uly = self.safe_string(params, 'uly') + if uly is not None: + request['uly'] = uly + instFamily = self.safe_string(params, 'instFamily') + if instFamily is not None: + request['instFamily'] = instFamily + if instType == 'OPTION' and uly is None and instFamily is None: + raise BadRequest(self.id + ' fetchOpenInterests() requires either uly or instFamily parameter for OPTION markets') + response = self.publicGetPublicOpenInterest(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "instId": "BTC-USDT-SWAP", + # "instType": "SWAP", + # "oi": "2125419", + # "oiCcy": "21254.19", + # "ts": "1664005108969" + # } + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_open_interests(data, symbols) + + def fetch_open_interest_history(self, symbol: str, timeframe='1d', since: Int = None, limit: Int = None, params={}): + """ + Retrieves the open interest history of a currency + + https://www.okx.com/docs-v5/en/#rest-api-trading-data-get-contracts-open-interest-and-volume + https://www.okx.com/docs-v5/en/#rest-api-trading-data-get-options-open-interest-and-volume + + :param str symbol: Unified CCXT currency code or unified symbol + :param str timeframe: "5m", "1h", or "1d" for option only "1d" or "8h" + :param int [since]: The time in ms of the earliest record to retrieve unix timestamp + :param int [limit]: Not used by okx, but parsed internally by CCXT + :param dict [params]: Exchange specific parameters + :param int [params.until]: The time in ms of the latest record to retrieve unix timestamp + :returns: An array of `open interest structures ` + """ + options = self.safe_dict(self.options, 'fetchOpenInterestHistory', {}) + timeframes = self.safe_dict(options, 'timeframes', {}) + timeframe = self.safe_string(timeframes, timeframe, timeframe) + if timeframe != '5m' and timeframe != '1H' and timeframe != '1D': + raise BadRequest(self.id + ' fetchOpenInterestHistory cannot only use the 5m, 1h, and 1d timeframe') + self.load_markets() + # handle unified currency code or symbol + currencyId = None + market = None + if (symbol in self.markets) or (symbol in self.markets_by_id): + market = self.market(symbol) + currencyId = market['baseId'] + else: + currency = self.currency(symbol) + currencyId = currency['id'] + request: dict = { + 'ccy': currencyId, + 'period': timeframe, + } + type = None + response = None + type, params = self.handle_market_type_and_params('fetchOpenInterestHistory', market, params) + if type == 'option': + response = self.publicGetRubikStatOptionOpenInterestVolume(self.extend(request, params)) + else: + if since is not None: + request['begin'] = since + until = self.safe_integer(params, 'until') + if until is not None: + request['end'] = until + params = self.omit(params, ['until']) + response = self.publicGetRubikStatContractsOpenInterestVolume(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # [ + # "1648221300000", # timestamp + # "2183354317.945", # open interest(USD) + # "74285877.617", # volume(USD) + # ], + # ... + # ], + # "msg": '' + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_open_interests_history(data, None, since, limit) + + def parse_open_interest(self, interest, market: Market = None): + # + # fetchOpenInterestHistory + # + # [ + # "1648221300000", # timestamp + # "2183354317.945", # open interest(USD) - (coin) for options + # "74285877.617", # volume(USD) - (coin) for options + # ] + # + # fetchOpenInterest + # + # { + # "instId": "BTC-USD-230520-25500-P", + # "instType": "OPTION", + # "oi": "300", + # "oiCcy": "3", + # "oiUsd": "3", + # "ts": "1684551166251" + # } + # + id = self.safe_string(interest, 'instId') + market = self.safe_market(id, market) + time = self.safe_integer(interest, 'ts') + timestamp = self.safe_integer(interest, 0, time) + baseVolume = None + quoteVolume = None + openInterestAmount = None + openInterestValue = None + type = self.safe_string(self.options, 'defaultType') + if isinstance(interest, list): + if type == 'option': + openInterestAmount = self.safe_number(interest, 1) + baseVolume = self.safe_number(interest, 2) + else: + openInterestValue = self.safe_number(interest, 1) + quoteVolume = self.safe_number(interest, 2) + else: + baseVolume = self.safe_number(interest, 'oiCcy') + openInterestAmount = self.safe_number(interest, 'oi') + openInterestValue = self.safe_number(interest, 'oiUsd') + return self.safe_open_interest({ + 'symbol': self.safe_symbol(id), + 'baseVolume': baseVolume, # deprecated + 'quoteVolume': quoteVolume, # deprecated + 'openInterestAmount': openInterestAmount, + 'openInterestValue': openInterestValue, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'info': interest, + }, market) + + def set_sandbox_mode(self, enable: bool): + super(okx, self).set_sandbox_mode(enable) + self.options['sandboxMode'] = enable + if enable: + self.headers['x-simulated-trading'] = '1' + elif 'x-simulated-trading' in self.headers: + self.headers = self.omit(self.headers, 'x-simulated-trading') + + def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}): + """ + fetch deposit and withdraw fees + + https://www.okx.com/docs-v5/en/#rest-api-funding-get-currencies + + :param str[]|None codes: list of unified currency codes + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `fees structures ` + """ + self.load_markets() + request = {} + if codes is not None: + ids = self.currency_ids(codes) + request['ccy'] = ','.join(ids) + response = self.privateGetAssetCurrencies(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "canDep": True, + # "canInternal": False, + # "canWd": True, + # "ccy": "USDT", + # "chain": "USDT-TRC20", + # "logoLink": "https://static.coinall.ltd/cdn/assets/imgs/221/5F74EB20302D7761.png", + # "mainNet": False, + # "maxFee": "1.6", + # "maxWd": "8852150", + # "minFee": "0.8", + # "minWd": "2", + # "name": "Tether", + # "usedWdQuota": "0", + # "wdQuota": "500", + # "wdTickSz": "3" + # }, + # { + # "canDep": True, + # "canInternal": False, + # "canWd": True, + # "ccy": "USDT", + # "chain": "USDT-ERC20", + # "logoLink": "https://static.coinall.ltd/cdn/assets/imgs/221/5F74EB20302D7761.png", + # "mainNet": False, + # "maxFee": "16", + # "maxWd": "8852150", + # "minFee": "8", + # "minWd": "2", + # "name": "Tether", + # "usedWdQuota": "0", + # "wdQuota": "500", + # "wdTickSz": "3" + # }, + # ... + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data') + return self.parse_deposit_withdraw_fees(data, codes) + + def parse_deposit_withdraw_fees(self, response, codes=None, currencyIdKey=None): + # + # [ + # { + # "canDep": True, + # "canInternal": False, + # "canWd": True, + # "ccy": "USDT", + # "chain": "USDT-TRC20", + # "logoLink": "https://static.coinall.ltd/cdn/assets/imgs/221/5F74EB20302D7761.png", + # "mainNet": False, + # "maxFee": "1.6", + # "maxWd": "8852150", + # "minFee": "0.8", + # "minWd": "2", + # "name": "Tether", + # "usedWdQuota": "0", + # "wdQuota": "500", + # "wdTickSz": "3" + # } + # ] + # + depositWithdrawFees: dict = {} + codes = self.market_codes(codes) + for i in range(0, len(response)): + feeInfo = response[i] + currencyId = self.safe_string(feeInfo, 'ccy') + code = self.safe_currency_code(currencyId) + if (codes is None) or (self.in_array(code, codes)): + depositWithdrawFee = self.safe_value(depositWithdrawFees, code) + if depositWithdrawFee is None: + depositWithdrawFees[code] = self.deposit_withdraw_fee({}) + depositWithdrawFees[code]['info'][currencyId] = feeInfo + chain = self.safe_string(feeInfo, 'chain') + if chain is None: + continue + chainSplit = chain.split('-') + networkId = self.safe_value(chainSplit, 1) + withdrawFee = self.safe_number(feeInfo, 'fee') + withdrawResult: dict = { + 'fee': withdrawFee, + 'percentage': False if (withdrawFee is not None) else None, + } + depositResult: dict = { + 'fee': None, + 'percentage': None, + } + networkCode = self.network_id_to_code(networkId, code) + depositWithdrawFees[code]['networks'][networkCode] = { + 'withdraw': withdrawResult, + 'deposit': depositResult, + } + depositWithdrawCodes = list(depositWithdrawFees.keys()) + for i in range(0, len(depositWithdrawCodes)): + code = depositWithdrawCodes[i] + currency = self.currency(code) + depositWithdrawFees[code] = self.assign_default_deposit_withdraw_fees(depositWithdrawFees[code], currency) + return depositWithdrawFees + + def fetch_settlement_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical settlement records + + https://www.okx.com/docs-v5/en/#rest-api-public-data-get-delivery-exercise-history + + :param str symbol: unified market symbol to fetch the settlement history for + :param int [since]: timestamp in ms + :param int [limit]: number of records + :param dict [params]: exchange specific params + :returns dict[]: a list of `settlement history objects ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchSettlementHistory() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + type = None + type, params = self.handle_market_type_and_params('fetchSettlementHistory', market, params) + if type != 'future' and type != 'option': + raise NotSupported(self.id + ' fetchSettlementHistory() supports futures and options markets only') + request: dict = { + 'instType': self.convert_to_instrument_type(type), + 'uly': market['baseId'] + '-' + market['quoteId'], + } + if since is not None: + request['before'] = since - 1 + if limit is not None: + request['limit'] = limit + response = self.publicGetPublicDeliveryExerciseHistory(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "details": [ + # { + # "insId": "BTC-USD-230523-25750-C", + # "px": "27290.1486867000556483", + # "type": "exercised" + # }, + # ], + # "ts":"1684656000000" + # } + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + settlements = self.parse_settlements(data, market) + sorted = self.sort_by(settlements, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, market['symbol'], since, limit) + + def parse_settlement(self, settlement, market): + # + # { + # "insId": "BTC-USD-230521-28500-P", + # "px": "27081.2007345984751516", + # "type": "exercised" + # } + # + marketId = self.safe_string(settlement, 'insId') + return { + 'info': settlement, + 'symbol': self.safe_symbol(marketId, market), + 'price': self.safe_number(settlement, 'px'), + 'timestamp': None, + 'datetime': None, + } + + def parse_settlements(self, settlements, market): + # + # { + # "details": [ + # { + # "insId": "BTC-USD-230523-25750-C", + # "px": "27290.1486867000556483", + # "type": "exercised" + # }, + # ], + # "ts":"1684656000000" + # } + # + result = [] + for i in range(0, len(settlements)): + entry = settlements[i] + timestamp = self.safe_integer(entry, 'ts') + details = self.safe_list(entry, 'details', []) + for j in range(0, len(details)): + settlement = self.parse_settlement(details[j], market) + result.append(self.extend(settlement, { + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + })) + return result + + def fetch_underlying_assets(self, params={}): + """ + fetches the market ids of underlying assets for a specific contract market type + + https://www.okx.com/docs-v5/en/#public-data-rest-api-get-underlying + + :param dict [params]: exchange specific params + :param str [params.type]: the contract market type, 'option', 'swap' or 'future', the default is 'option' + :returns dict[]: a list of `underlying assets ` + """ + self.load_markets() + marketType = None + marketType, params = self.handle_market_type_and_params('fetchUnderlyingAssets', None, params) + if (marketType is None) or (marketType == 'spot'): + marketType = 'option' + if (marketType != 'option') and (marketType != 'swap') and (marketType != 'future'): + raise NotSupported(self.id + ' fetchUnderlyingAssets() supports contract markets only') + request: dict = { + 'instType': self.convert_to_instrument_type(marketType), + } + response = self.publicGetPublicUnderlying(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # [ + # "BTC-USD", + # "ETH-USD" + # ] + # ], + # "msg": "" + # } + # + underlyings = self.safe_list(response, 'data', []) + return underlyings[0] + + def fetch_greeks(self, symbol: str, params={}) -> Greeks: + """ + fetches an option contracts greeks, financial metrics used to measure the factors that affect the price of an options contract + + https://www.okx.com/docs-v5/en/#public-data-rest-api-get-option-market-data + + :param str symbol: unified symbol of the market to fetch greeks for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `greeks structure ` + """ + self.load_markets() + market = self.market(symbol) + marketId = market['id'] + optionParts = marketId.split('-') + request: dict = { + 'uly': market['info']['uly'], + 'instFamily': market['info']['instFamily'], + 'expTime': self.safe_string(optionParts, 2), + } + response = self.publicGetPublicOptSummary(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "askVol": "0", + # "bidVol": "0", + # "delta": "0.5105464486882039", + # "deltaBS": "0.7325502184143025", + # "fwdPx": "37675.80158694987186", + # "gamma": "-0.13183515090501083", + # "gammaBS": "0.000024139685826358558", + # "instId": "BTC-USD-240329-32000-C", + # "instType": "OPTION", + # "lever": "4.504428015946619", + # "markVol": "0.5916253554539876", + # "realVol": "0", + # "theta": "-0.0004202992014012855", + # "thetaBS": "-18.52354631567909", + # "ts": "1699586421976", + # "uly": "BTC-USD", + # "vega": "0.0020207455080045846", + # "vegaBS": "74.44022302387287", + # "volLv": "0.5948549730405797" + # }, + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + for i in range(0, len(data)): + entry = data[i] + entryMarketId = self.safe_string(entry, 'instId') + if entryMarketId == marketId: + return self.parse_greeks(entry, market) + return None + + def fetch_all_greeks(self, symbols: Strings = None, params={}) -> List[Greeks]: + """ + fetches all option contracts greeks, financial metrics used to measure the factors that affect the price of an options contract + + https://www.okx.com/docs-v5/en/#public-data-rest-api-get-option-market-data + + :param str[] [symbols]: unified symbols of the markets to fetch greeks for, all markets are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str params['uly']: Underlying, either uly or instFamily is required + :param str params['instFamily']: Instrument family, either uly or instFamily is required + :returns dict: a `greeks structure ` + """ + self.load_markets() + request: dict = {} + symbols = self.market_symbols(symbols, None, True, True, True) + symbolsLength = None + if symbols is not None: + symbolsLength = len(symbols) + if (symbols is None) or (symbolsLength != 1): + uly = self.safe_string(params, 'uly') + if uly is not None: + request['uly'] = uly + instFamily = self.safe_string(params, 'instFamily') + if instFamily is not None: + request['instFamily'] = instFamily + if (uly is None) and (instFamily is None): + raise BadRequest(self.id + ' fetchAllGreeks() requires either a uly or instFamily parameter') + market = None + if symbols is not None: + if symbolsLength == 1: + market = self.market(symbols[0]) + marketId = market['id'] + optionParts = marketId.split('-') + request['uly'] = market['info']['uly'] + request['instFamily'] = market['info']['instFamily'] + request['expTime'] = self.safe_string(optionParts, 2) + params = self.omit(params, ['uly', 'instFamily']) + response = self.publicGetPublicOptSummary(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "askVol": "0", + # "bidVol": "0", + # "delta": "0.5105464486882039", + # "deltaBS": "0.7325502184143025", + # "fwdPx": "37675.80158694987186", + # "gamma": "-0.13183515090501083", + # "gammaBS": "0.000024139685826358558", + # "instId": "BTC-USD-240329-32000-C", + # "instType": "OPTION", + # "lever": "4.504428015946619", + # "markVol": "0.5916253554539876", + # "realVol": "0", + # "theta": "-0.0004202992014012855", + # "thetaBS": "-18.52354631567909", + # "ts": "1699586421976", + # "uly": "BTC-USD", + # "vega": "0.0020207455080045846", + # "vegaBS": "74.44022302387287", + # "volLv": "0.5948549730405797" + # }, + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_all_greeks(data, symbols) + + def parse_greeks(self, greeks: dict, market: Market = None) -> Greeks: + # + # { + # "askVol": "0", + # "bidVol": "0", + # "delta": "0.5105464486882039", + # "deltaBS": "0.7325502184143025", + # "fwdPx": "37675.80158694987186", + # "gamma": "-0.13183515090501083", + # "gammaBS": "0.000024139685826358558", + # "instId": "BTC-USD-240329-32000-C", + # "instType": "OPTION", + # "lever": "4.504428015946619", + # "markVol": "0.5916253554539876", + # "realVol": "0", + # "theta": "-0.0004202992014012855", + # "thetaBS": "-18.52354631567909", + # "ts": "1699586421976", + # "uly": "BTC-USD", + # "vega": "0.0020207455080045846", + # "vegaBS": "74.44022302387287", + # "volLv": "0.5948549730405797" + # } + # + timestamp = self.safe_integer(greeks, 'ts') + marketId = self.safe_string(greeks, 'instId') + symbol = self.safe_symbol(marketId, market) + return { + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'delta': self.safe_number(greeks, 'delta'), + 'gamma': self.safe_number(greeks, 'gamma'), + 'theta': self.safe_number(greeks, 'theta'), + 'vega': self.safe_number(greeks, 'vega'), + 'rho': None, + 'bidSize': None, + 'askSize': None, + 'bidImpliedVolatility': self.safe_number(greeks, 'bidVol'), + 'askImpliedVolatility': self.safe_number(greeks, 'askVol'), + 'markImpliedVolatility': self.safe_number(greeks, 'markVol'), + 'bidPrice': None, + 'askPrice': None, + 'markPrice': None, + 'lastPrice': None, + 'underlyingPrice': None, + 'info': greeks, + } + + def close_position(self, symbol: str, side: OrderSide = None, params={}) -> Order: + """ + closes open positions for a market + + https://www.okx.com/docs-v5/en/#order-book-trading-trade-post-close-positions + + :param str symbol: Unified CCXT market symbol + :param str [side]: 'buy' or 'sell', leave in net mode + :param dict [params]: extra parameters specific to the okx api endpoint + :param str [params.clientOrderId]: a unique identifier for the order + :param str [params.marginMode]: 'cross' or 'isolated', default is 'cross + :param str [params.code]: *required in the case of closing cross MARGIN position for Single-currency margin* margin currency + + EXCHANGE SPECIFIC PARAMETERS + :param boolean [params.autoCxl]: whether any pending orders for closing out needs to be automatically canceled when close position via a market order. False or True, the default is False + :param str [params.tag]: order tag a combination of case-sensitive alphanumerics, all numbers, or all letters of up to 16 characters + :returns dict[]: `A list of position structures ` + """ + self.load_markets() + market = self.market(symbol) + clientOrderId = self.safe_string(params, 'clientOrderId') + code = self.safe_string(params, 'code') + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('closePosition', params, 'cross') + request: dict = { + 'instId': market['id'], + 'mgnMode': marginMode, + } + if side is not None: + if (side == 'buy'): + request['posSide'] = 'long' + elif side == 'sell': + request['posSide'] = 'short' + else: + request['posSide'] = side + if clientOrderId is not None: + request['clOrdId'] = clientOrderId + if code is not None: + currency = self.currency(code) + request['ccy'] = currency['id'] + response = self.privatePostTradeClosePosition(self.extend(request, params)) + # + # { + # "code": "1", + # "data": [ + # { + # "clOrdId":"e847386590ce4dBCe903bbc394dc88bf", + # "ordId":"", + # "sCode":"51000", + # "sMsg":"Parameter posSide error ", + # "tag":"e847386590ce4dBC" + # } + # ], + # "inTime": "1701877077101064", + # "msg": "All operations failed", + # "outTime": "1701877077102579" + # } + # + data = self.safe_list(response, 'data', []) + order = self.safe_dict(data, 0) + return self.parse_order(order, market) + + def fetch_option(self, symbol: str, params={}) -> Option: + """ + fetches option data that is commonly found in an option chain + + https://www.okx.com/docs-v5/en/#order-book-trading-market-data-get-ticker + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `option chain structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + } + response = self.publicGetMarketTicker(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "instType": "OPTION", + # "instId": "BTC-USD-241227-60000-P", + # "last": "", + # "lastSz": "0", + # "askPx": "", + # "askSz": "0", + # "bidPx": "", + # "bidSz": "0", + # "open24h": "", + # "high24h": "", + # "low24h": "", + # "volCcy24h": "0", + # "vol24h": "0", + # "ts": "1711176035035", + # "sodUtc0": "", + # "sodUtc8": "" + # } + # ] + # } + # + result = self.safe_list(response, 'data', []) + chain = self.safe_dict(result, 0, {}) + return self.parse_option(chain, None, market) + + def fetch_option_chain(self, code: str, params={}) -> OptionChain: + """ + fetches data for an underlying asset that is commonly found in an option chain + + https://www.okx.com/docs-v5/en/#order-book-trading-market-data-get-tickers + + :param str code: base currency to fetch an option chain for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.uly]: the underlying asset, can be obtained from fetchUnderlyingAssets() + :returns dict: a list of `option chain structures ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'uly': currency['code'] + '-USD', + 'instType': 'OPTION', + } + response = self.publicGetMarketTickers(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "instType": "OPTION", + # "instId": "BTC-USD-240323-52000-C", + # "last": "", + # "lastSz": "0", + # "askPx": "", + # "askSz": "0", + # "bidPx": "", + # "bidSz": "0", + # "open24h": "", + # "high24h": "", + # "low24h": "", + # "volCcy24h": "0", + # "vol24h": "0", + # "ts": "1711176207008", + # "sodUtc0": "", + # "sodUtc8": "" + # }, + # ] + # } + # + result = self.safe_list(response, 'data', []) + return self.parse_option_chain(result, None, 'instId') + + def parse_option(self, chain: dict, currency: Currency = None, market: Market = None) -> Option: + # + # { + # "instType": "OPTION", + # "instId": "BTC-USD-241227-60000-P", + # "last": "", + # "lastSz": "0", + # "askPx": "", + # "askSz": "0", + # "bidPx": "", + # "bidSz": "0", + # "open24h": "", + # "high24h": "", + # "low24h": "", + # "volCcy24h": "0", + # "vol24h": "0", + # "ts": "1711176035035", + # "sodUtc0": "", + # "sodUtc8": "" + # } + # + marketId = self.safe_string(chain, 'instId') + market = self.safe_market(marketId, market) + timestamp = self.safe_integer(chain, 'ts') + return { + 'info': chain, + 'currency': None, + 'symbol': market['symbol'], + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'impliedVolatility': None, + 'openInterest': None, + 'bidPrice': self.safe_number(chain, 'bidPx'), + 'askPrice': self.safe_number(chain, 'askPx'), + 'midPrice': None, + 'markPrice': None, + 'lastPrice': self.safe_number(chain, 'last'), + 'underlyingPrice': None, + 'change': None, + 'percentage': None, + 'baseVolume': self.safe_number(chain, 'volCcy24h'), + 'quoteVolume': None, + } + + def fetch_convert_quote(self, fromCode: str, toCode: str, amount: Num = None, params={}) -> Conversion: + """ + fetch a quote for converting from one currency to another + + https://www.okx.com/docs-v5/en/#funding-account-rest-api-estimate-quote + + :param str fromCode: the currency that you want to sell and convert from + :param str toCode: the currency that you want to buy and convert into + :param float [amount]: how much you want to trade in units of the from currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `conversion structure ` + """ + self.load_markets() + request: dict = { + 'baseCcy': fromCode.upper(), + 'quoteCcy': toCode.upper(), + 'rfqSzCcy': fromCode.upper(), + 'rfqSz': self.number_to_string(amount), + 'side': 'sell', + } + response = self.privatePostAssetConvertEstimateQuote(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "baseCcy": "ETH", + # "baseSz": "0.01023052", + # "clQReqId": "", + # "cnvtPx": "2932.40104429", + # "origRfqSz": "30", + # "quoteCcy": "USDT", + # "quoteId": "quoterETH-USDT16461885104612381", + # "quoteSz": "30", + # "quoteTime": "1646188510461", + # "rfqSz": "30", + # "rfqSzCcy": "USDT", + # "side": "buy", + # "ttlMs": "10000" + # } + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + result = self.safe_dict(data, 0, {}) + fromCurrencyId = self.safe_string(result, 'baseCcy', fromCode) + fromCurrency = self.currency(fromCurrencyId) + toCurrencyId = self.safe_string(result, 'quoteCcy', toCode) + toCurrency = self.currency(toCurrencyId) + return self.parse_conversion(result, fromCurrency, toCurrency) + + def create_convert_trade(self, id: str, fromCode: str, toCode: str, amount: Num = None, params={}) -> Conversion: + """ + convert from one currency to another + + https://www.okx.com/docs-v5/en/#funding-account-rest-api-convert-trade + + :param str id: the id of the trade that you want to make + :param str fromCode: the currency that you want to sell and convert from + :param str toCode: the currency that you want to buy and convert into + :param float [amount]: how much you want to trade in units of the from currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `conversion structure ` + """ + self.load_markets() + request: dict = { + 'quoteId': id, + 'baseCcy': fromCode, + 'quoteCcy': toCode, + 'szCcy': fromCode, + 'sz': self.number_to_string(amount), + 'side': 'sell', + } + response = self.privatePostAssetConvertTrade(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "baseCcy": "ETH", + # "clTReqId": "", + # "fillBaseSz": "0.01023052", + # "fillPx": "2932.40104429", + # "fillQuoteSz": "30", + # "instId": "ETH-USDT", + # "quoteCcy": "USDT", + # "quoteId": "quoterETH-USDT16461885104612381", + # "side": "buy", + # "state": "fullyFilled", + # "tradeId": "trader16461885203381437", + # "ts": "1646188520338" + # } + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + result = self.safe_dict(data, 0, {}) + fromCurrencyId = self.safe_string(result, 'baseCcy', fromCode) + fromCurrency = self.currency(fromCurrencyId) + toCurrencyId = self.safe_string(result, 'quoteCcy', toCode) + toCurrency = self.currency(toCurrencyId) + return self.parse_conversion(result, fromCurrency, toCurrency) + + def fetch_convert_trade(self, id: str, code: Str = None, params={}) -> Conversion: + """ + fetch the data for a conversion trade + + https://www.okx.com/docs-v5/en/#funding-account-rest-api-get-convert-history + + :param str id: the id of the trade that you want to fetch + :param str [code]: the unified currency code of the conversion trade + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `conversion structure ` + """ + self.load_markets() + request: dict = { + 'clTReqId': id, + } + response = self.privateGetAssetConvertHistory(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "clTReqId": "", + # "instId": "ETH-USDT", + # "side": "buy", + # "fillPx": "2932.401044", + # "baseCcy": "ETH", + # "quoteCcy": "USDT", + # "fillBaseSz": "0.01023052", + # "state": "fullyFilled", + # "tradeId": "trader16461885203381437", + # "fillQuoteSz": "30", + # "ts": "1646188520000" + # } + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + result = self.safe_dict(data, 0, {}) + fromCurrencyId = self.safe_string(result, 'baseCcy') + toCurrencyId = self.safe_string(result, 'quoteCcy') + fromCurrency = None + toCurrency = None + if fromCurrencyId is not None: + fromCurrency = self.currency(fromCurrencyId) + if toCurrencyId is not None: + toCurrency = self.currency(toCurrencyId) + return self.parse_conversion(result, fromCurrency, toCurrency) + + def fetch_convert_trade_history(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Conversion]: + """ + fetch the users history of conversion trades + + https://www.okx.com/docs-v5/en/#funding-account-rest-api-get-convert-history + + :param str [code]: the unified currency code + :param int [since]: the earliest time in ms to fetch conversions for + :param int [limit]: the maximum number of conversion structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest conversion to fetch + :returns dict[]: a list of `conversion structures ` + """ + self.load_markets() + request: dict = {} + request, params = self.handle_until_option('after', request, params) + if since is not None: + request['before'] = since + if limit is not None: + request['limit'] = limit + response = self.privateGetAssetConvertHistory(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "clTReqId": "", + # "instId": "ETH-USDT", + # "side": "buy", + # "fillPx": "2932.401044", + # "baseCcy": "ETH", + # "quoteCcy": "USDT", + # "fillBaseSz": "0.01023052", + # "state": "fullyFilled", + # "tradeId": "trader16461885203381437", + # "fillQuoteSz": "30", + # "ts": "1646188520000" + # } + # ], + # "msg": "" + # } + # + rows = self.safe_list(response, 'data', []) + return self.parse_conversions(rows, code, 'baseCcy', 'quoteCcy', since, limit) + + def parse_conversion(self, conversion: dict, fromCurrency: Currency = None, toCurrency: Currency = None) -> Conversion: + # + # fetchConvertQuote + # + # { + # "baseCcy": "ETH", + # "baseSz": "0.01023052", + # "clQReqId": "", + # "cnvtPx": "2932.40104429", + # "origRfqSz": "30", + # "quoteCcy": "USDT", + # "quoteId": "quoterETH-USDT16461885104612381", + # "quoteSz": "30", + # "quoteTime": "1646188510461", + # "rfqSz": "30", + # "rfqSzCcy": "USDT", + # "side": "buy", + # "ttlMs": "10000" + # } + # + # createConvertTrade + # + # { + # "baseCcy": "ETH", + # "clTReqId": "", + # "fillBaseSz": "0.01023052", + # "fillPx": "2932.40104429", + # "fillQuoteSz": "30", + # "instId": "ETH-USDT", + # "quoteCcy": "USDT", + # "quoteId": "quoterETH-USDT16461885104612381", + # "side": "buy", + # "state": "fullyFilled", + # "tradeId": "trader16461885203381437", + # "ts": "1646188520338" + # } + # + # fetchConvertTrade, fetchConvertTradeHistory + # + # { + # "clTReqId": "", + # "instId": "ETH-USDT", + # "side": "buy", + # "fillPx": "2932.401044", + # "baseCcy": "ETH", + # "quoteCcy": "USDT", + # "fillBaseSz": "0.01023052", + # "state": "fullyFilled", + # "tradeId": "trader16461885203381437", + # "fillQuoteSz": "30", + # "ts": "1646188520000" + # } + # + timestamp = self.safe_integer_2(conversion, 'quoteTime', 'ts') + fromCoin = self.safe_string(conversion, 'baseCcy') + fromCode = self.safe_currency_code(fromCoin, fromCurrency) + to = self.safe_string(conversion, 'quoteCcy') + toCode = self.safe_currency_code(to, toCurrency) + return { + 'info': conversion, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': self.safe_string_n(conversion, ['clQReqId', 'tradeId', 'quoteId']), + 'fromCurrency': fromCode, + 'fromAmount': self.safe_number_2(conversion, 'baseSz', 'fillBaseSz'), + 'toCurrency': toCode, + 'toAmount': self.safe_number_2(conversion, 'quoteSz', 'fillQuoteSz'), + 'price': self.safe_number_2(conversion, 'cnvtPx', 'fillPx'), + 'fee': None, + } + + def fetch_convert_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies that can be converted + + https://www.okx.com/docs-v5/en/#funding-account-rest-api-get-convert-currencies + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + self.load_markets() + response = self.privateGetAssetConvertCurrencies(params) + # + # { + # "code": "0", + # "data": [ + # { + # "ccy": "BTC", + # "max": "", + # "min": "" + # }, + # ], + # "msg": "" + # } + # + result: dict = {} + data = self.safe_list(response, 'data', []) + for i in range(0, len(data)): + entry = data[i] + id = self.safe_string(entry, 'ccy') + code = self.safe_currency_code(id) + result[code] = { + 'info': entry, + 'id': id, + 'code': code, + 'networks': None, + 'type': None, + 'name': None, + 'active': None, + 'deposit': None, + 'withdraw': None, + 'fee': None, + 'precision': None, + 'limits': { + 'amount': { + 'min': self.safe_number(entry, 'min'), + 'max': self.safe_number(entry, 'max'), + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + 'deposit': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + } + return result + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if not response: + return None # fallback to default error handler + # + # { + # "code": "1", + # "data": [ + # { + # "clOrdId": "", + # "ordId": "", + # "sCode": "51119", + # "sMsg": "Order placement failed due to insufficient balance. ", + # "tag": "" + # } + # ], + # "msg": "" + # }, + # { + # "code": "58001", + # "data": [], + # "msg": "Incorrect trade password" + # } + # + code = self.safe_string(response, 'code') + if (code != '0') and (code != '2'): # 2 means that bulk operation partially succeeded + feedback = self.id + ' ' + body + data = self.safe_list(response, 'data', []) + for i in range(0, len(data)): + error = data[i] + errorCode = self.safe_string(error, 'sCode') + message = self.safe_string(error, 'sMsg') + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], code, feedback) + raise ExchangeError(feedback) # unknown message + return None + + def fetch_margin_adjustment_history(self, symbol: Str = None, type: Str = None, since: Num = None, limit: Num = None, params={}) -> List[MarginModification]: + """ + fetches the history of margin added or reduced from contract isolated positions + + https://www.okx.com/docs-v5/en/#trading-account-rest-api-get-bills-details-last-7-days + https://www.okx.com/docs-v5/en/#trading-account-rest-api-get-bills-details-last-3-months + + :param str [symbol]: not used by okx fetchMarginAdjustmentHistory + :param str [type]: "add" or "reduce" + :param int [since]: the earliest time in ms to fetch margin adjustment history for + :param int [limit]: the maximum number of entries to retrieve + :param dict params: extra parameters specific to the exchange api endpoint + :param boolean [params.auto]: True if fetching auto margin increases + :returns dict[]: a list of `margin structures ` + """ + self.load_markets() + auto = self.safe_bool(params, 'auto') + if type is None: + raise ArgumentsRequired(self.id + ' fetchMarginAdjustmentHistory() requires a type argument') + isAdd = type == 'add' + subType = '160' if isAdd else '161' + if auto: + if isAdd: + subType = '162' + else: + raise BadRequest(self.id + ' cannot fetch margin adjustments for type ' + type) + request: dict = { + 'subType': subType, + 'mgnMode': 'isolated', + } + until = self.safe_integer(params, 'until') + params = self.omit(params, 'until') + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + if until is not None: + request['endTime'] = until + response = None + now = self.milliseconds() + oneWeekAgo = now - 604800000 + threeMonthsAgo = now - 7776000000 + if (since is None) or (since > oneWeekAgo): + response = self.privateGetAccountBills(self.extend(request, params)) + elif since > threeMonthsAgo: + response = self.privateGetAccountBillsArchive(self.extend(request, params)) + else: + raise BadRequest(self.id + ' fetchMarginAdjustmentHistory() cannot fetch margin adjustments older than 3 months') + # + # { + # code: '0', + # data: [ + # { + # bal: '67621.4325135010619812', + # balChg: '-10.0000000000000000', + # billId: '691293628710342659', + # ccy: 'USDT', + # clOrdId: '', + # execType: '', + # fee: '0', + # fillFwdPx: '', + # fillIdxPx: '', + # fillMarkPx: '', + # fillMarkVol: '', + # fillPxUsd: '', + # fillPxVol: '', + # fillTime: '1711089244850', + # from: '', + # instId: 'XRP-USDT-SWAP', + # instType: 'SWAP', + # interest: '0', + # mgnMode: 'isolated', + # notes: '', + # ordId: '', + # pnl: '0', + # posBal: '73.12', + # posBalChg: '10.00', + # px: '', + # subType: '160', + # sz: '10', + # tag: '', + # to: '', + # tradeId: '0', + # ts: '1711089244699', + # type: '6' + # } + # ], + # msg: '' + # } + # + data = self.safe_list(response, 'data') + modifications = self.parse_margin_modifications(data) + return self.filter_by_symbol_since_limit(modifications, symbol, since, limit) + + def fetch_positions_history(self, symbols: Strings = None, since: Int = None, limit: Int = None, params={}) -> List[Position]: + """ + fetches historical positions + + https://www.okx.com/docs-v5/en/#trading-account-rest-api-get-positions-history + + :param str [symbols]: unified market symbols + :param int [since]: timestamp in ms of the earliest position to fetch + :param int [limit]: the maximum amount of records to fetch, default=100, max=100 + :param dict params: extra parameters specific to the exchange api endpoint + :param str [params.marginMode]: "cross" or "isolated" + + EXCHANGE SPECIFIC PARAMETERS + :param str [params.instType]: margin, swap, futures or option + :param str [params.type]: the type of latest close position 1: close position partially, 2:close all, 3:liquidation, 4:partial liquidation; 5:adl, is it is the latest type if there are several types for the same position + :param str [params.posId]: position id, there is attribute expiration, the posid will be expired if it is more than 30 days after the last full close position, then position will use new posid + :param str [params.before]: timestamp in ms of the earliest position to fetch based on the last update time of the position + :param str [params.after]: timestamp in ms of the latest position to fetch based on the last update time of the position + :returns dict[]: a list of `position structures ` + """ + self.load_markets() + marginMode = self.safe_string(params, 'marginMode') + instType = self.safe_string_upper(params, 'instType') + params = self.omit(params, ['until', 'marginMode', 'instType']) + if limit is None: + limit = 100 + request: dict = { + 'limit': limit, + } + if symbols is not None: + symbolsLength = len(symbols) + if symbolsLength == 1: + market = self.market(symbols[0]) + request['instId'] = market['id'] + if marginMode is not None: + request['mgnMode'] = marginMode + if instType is not None: + request['instType'] = instType + response = self.privateGetAccountPositionsHistory(self.extend(request, params)) + # + # { + # code: '0', + # data: [ + # { + # cTime: '1708735940395', + # ccy: 'USDT', + # closeAvgPx: '0.6330444444444444', + # closeTotalPos: '27', + # direction: 'long', + # fee: '-1.69566', + # fundingFee: '-11.870404179341788', + # instId: 'XRP-USDT-SWAP', + # instType: 'SWAP', + # lever: '3.0', + # liqPenalty: '0', + # mgnMode: 'cross', + # openAvgPx: '0.623', + # openMaxPos: '15', + # pnl: '27.11999999999988', + # pnlRatio: '0.0241732402722634', + # posId: '681423155054862336', + # realizedPnl: '13.553935820658092', + # triggerPx: '', + # type: '2', + # uTime: '1711088748170', + # uly: 'XRP-USDT' + # }, + # ... + # ], + # msg: '' + # } + # + data = self.safe_list(response, 'data') + positions = self.parse_positions(data, symbols, params) + return self.filter_by_since_limit(positions, since, limit) + + def fetch_long_short_ratio_history(self, symbol: Str = None, timeframe: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LongShortRatio]: + """ + fetches the long short ratio history for a unified market symbol + + https://www.okx.com/docs-v5/en/#trading-statistics-rest-api-get-contract-long-short-ratio + + :param str symbol: unified symbol of the market to fetch the long short ratio for + :param str [timeframe]: the period for the ratio + :param int [since]: the earliest time in ms to fetch ratios for + :param int [limit]: the maximum number of long short ratio structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest ratio to fetch + :returns dict[]: an array of `long short ratio structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + } + until = self.safe_string_2(params, 'until', 'end') + params = self.omit(params, 'until') + if until is not None: + request['end'] = until + if timeframe is not None: + request['period'] = timeframe + if since is not None: + request['begin'] = since + if limit is not None: + request['limit'] = limit + response = self.publicGetRubikStatContractsLongShortAccountRatioContract(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # ["1729323600000", "0.9398602814619824"], + # ["1729323300000", "0.9398602814619824"], + # ["1729323000000", "0.9398602814619824"], + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + result = [] + for i in range(0, len(data)): + entry = data[i] + result.append({ + 'timestamp': self.safe_string(entry, 0), + 'longShortRatio': self.safe_string(entry, 1), + }) + return self.parse_long_short_ratio_history(result, market) + + def parse_long_short_ratio(self, info: dict, market: Market = None) -> LongShortRatio: + timestamp = self.safe_integer(info, 'timestamp') + symbol = None + if market is not None: + symbol = market['symbol'] + return { + 'info': info, + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'timeframe': None, + 'longShortRatio': self.safe_number(info, 'longShortRatio'), + } diff --git a/ccxt/okxus.py b/ccxt/okxus.py new file mode 100644 index 0000000..41d5c6b --- /dev/null +++ b/ccxt/okxus.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.okx import okx +from ccxt.abstract.okxus import ImplicitAPI +from ccxt.base.types import Any + + +class okxus(okx, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(okxus, self).describe(), { + 'id': 'okxus', + 'name': 'OKX(US)', + 'certified': False, + 'pro': True, + 'hostname': 'us.okx.com', + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/152485636-38b19e4a-bece-4dec-979a-5982859ffc04.jpg', + 'api': { + 'rest': 'https://{hostname}', + }, + 'www': 'https://app.okx.com', + 'doc': 'https://app.okx.com/docs-v5/en/#overview', + 'fees': 'https://app.okx.com/pages/products/fees.html', + 'referral': { + 'url': 'https://www.app.okx.com/join/CCXT2023', + 'discount': 0.2, + }, + 'test': { + 'rest': 'https://{hostname}', + }, + }, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': None, + 'swap': False, + 'future': False, + 'option': False, + }, + 'features': { + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + }) diff --git a/ccxt/onetrading.py b/ccxt/onetrading.py new file mode 100644 index 0000000..22d6e40 --- /dev/null +++ b/ccxt/onetrading.py @@ -0,0 +1,1827 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.onetrading import ImplicitAPI +from ccxt.base.types import Any, Balances, Currencies, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, TradingFees +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidAddress +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import NotSupported +from ccxt.base.errors import DDoSProtection +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class onetrading(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(onetrading, self).describe(), { + 'id': 'onetrading', + 'name': 'One Trading', + 'countries': ['AT'], # Austria + 'rateLimit': 300, + 'version': 'v1', + 'pro': True, + # new metainfo interface + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'cancelOrders': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createDepositAddress': False, + 'createOrder': True, + 'createReduceOnlyOrder': False, + 'createStopLimitOrder': True, + 'createStopMarketOrder': False, + 'createStopOrder': True, + 'fetchAccounts': False, + 'fetchAllGreeks': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDeposit': False, + 'fetchDepositAddress': False, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': False, + 'fetchDepositsWithdrawals': False, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLedger': False, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrice': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': False, + 'fetchOrderTrades': True, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': False, + 'fetchTradingFee': False, + 'fetchTradingFees': True, + 'fetchTransactionFee': False, + 'fetchTransactionFees': False, + 'fetchTransactions': False, + 'fetchTransfer': False, + 'fetchTransfers': False, + 'fetchUnderlyingAssets': False, + 'fetchVolatilityHistory': False, + 'fetchWithdrawal': False, + 'fetchWithdrawals': False, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'transfer': False, + 'withdraw': False, + }, + 'timeframes': { + '1m': '1/MINUTES', + '5m': '5/MINUTES', + '15m': '15/MINUTES', + '30m': '30/MINUTES', + '1h': '1/HOURS', + '4h': '4/HOURS', + '1d': '1/DAYS', + '1w': '1/WEEKS', + '1M': '1/MONTHS', + }, + 'urls': { + 'logo': 'https://github.com/ccxt/ccxt/assets/43336371/bdbc26fd-02f2-4ca7-9f1e-17333690bb1c', + 'api': { + 'public': 'https://api.onetrading.com/fast', + 'private': 'https://api.onetrading.com/fast', + }, + 'www': 'https://onetrading.com/', + 'doc': [ + 'https://docs.onetrading.com', + ], + 'fees': 'https://onetrading.com/fees', + }, + 'api': { + 'public': { + 'get': [ + 'currencies', + 'candlesticks/{instrument_code}', + 'fees', + 'instruments', + 'order-book/{instrument_code}', + 'market-ticker', + 'market-ticker/{instrument_code}', + 'time', + ], + }, + 'private': { + 'get': [ + 'account/balances', + 'account/fees', + 'account/orders', + 'account/orders/{order_id}', + 'account/orders/{order_id}/trades', + 'account/trades', + 'account/trades/{trade_id}', + ], + 'post': [ + 'account/orders', + ], + 'delete': [ + 'account/orders', + 'account/orders/{order_id}', + 'account/orders/client/{client_id}', + ], + }, + }, + 'fees': { + 'trading': { + 'tierBased': True, + 'percentage': True, + 'taker': self.parse_number('0.0015'), + 'maker': self.parse_number('0.001'), + 'tiers': [ + # volume in BTC + { + 'taker': [ + [self.parse_number('0'), self.parse_number('0.0015')], + [self.parse_number('100'), self.parse_number('0.0013')], + [self.parse_number('250'), self.parse_number('0.0013')], + [self.parse_number('1000'), self.parse_number('0.001')], + [self.parse_number('5000'), self.parse_number('0.0009')], + [self.parse_number('10000'), self.parse_number('0.00075')], + [self.parse_number('20000'), self.parse_number('0.00065')], + ], + 'maker': [ + [self.parse_number('0'), self.parse_number('0.001')], + [self.parse_number('100'), self.parse_number('0.001')], + [self.parse_number('250'), self.parse_number('0.0009')], + [self.parse_number('1000'), self.parse_number('0.00075')], + [self.parse_number('5000'), self.parse_number('0.0006')], + [self.parse_number('10000'), self.parse_number('0.0005')], + [self.parse_number('20000'), self.parse_number('0.0005')], + ], + }, + ], + }, + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': False, + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'exact': { + 'INVALID_CLIENT_UUID': InvalidOrder, + 'ORDER_NOT_FOUND': OrderNotFound, + 'ONLY_ONE_ERC20_ADDRESS_ALLOWED': InvalidAddress, + 'DEPOSIT_ADDRESS_NOT_USED': InvalidAddress, + 'INVALID_CREDENTIALS': AuthenticationError, + 'MISSING_CREDENTIALS': AuthenticationError, + 'INVALID_APIKEY': AuthenticationError, + 'INVALID_SCOPES': AuthenticationError, + 'INVALID_SUBJECT': AuthenticationError, + 'INVALID_ISSUER': AuthenticationError, + 'INVALID_AUDIENCE': AuthenticationError, + 'INVALID_DEVICE_ID': AuthenticationError, + 'INVALID_IP_RESTRICTION': AuthenticationError, + 'APIKEY_REVOKED': AuthenticationError, + 'APIKEY_EXPIRED': AuthenticationError, + 'SYNCHRONIZER_TOKEN_MISMATCH': AuthenticationError, + 'SESSION_EXPIRED': AuthenticationError, + 'INTERNAL_ERROR': AuthenticationError, + 'CLIENT_IP_BLOCKED': PermissionDenied, + 'MISSING_PERMISSION': PermissionDenied, + 'ILLEGAL_CHARS': BadRequest, + 'UNSUPPORTED_MEDIA_TYPE': BadRequest, + 'ACCOUNT_HISTORY_TIME_RANGE_TOO_BIG': BadRequest, + 'CANDLESTICKS_TIME_RANGE_TOO_BIG': BadRequest, + 'INVALID_INSTRUMENT_CODE': BadRequest, + 'INVALID_ORDER_TYPE': BadRequest, + 'INVALID_UNIT': BadRequest, + 'INVALID_PERIOD': BadRequest, + 'INVALID_TIME': BadRequest, + 'INVALID_DATE': BadRequest, + 'INVALID_CURRENCY': BadRequest, + 'INVALID_AMOUNT': BadRequest, + 'INVALID_PRICE': BadRequest, + 'INVALID_LIMIT': BadRequest, + 'INVALID_QUERY': BadRequest, + 'INVALID_CURSOR': BadRequest, + 'INVALID_ACCOUNT_ID': BadRequest, + 'INVALID_SIDE': InvalidOrder, + 'INVALID_ACCOUNT_HISTORY_FROM_TIME': BadRequest, + 'INVALID_ACCOUNT_HISTORY_MAX_PAGE_SIZE': BadRequest, + 'INVALID_ACCOUNT_HISTORY_TIME_PERIOD': BadRequest, + 'INVALID_ACCOUNT_HISTORY_TO_TIME': BadRequest, + 'INVALID_CANDLESTICKS_GRANULARITY': BadRequest, + 'INVALID_CANDLESTICKS_UNIT': BadRequest, + 'INVALID_ORDER_BOOK_DEPTH': BadRequest, + 'INVALID_ORDER_BOOK_LEVEL': BadRequest, + 'INVALID_PAGE_CURSOR': BadRequest, + 'INVALID_TIME_RANGE': BadRequest, + 'INVALID_TRADE_ID': BadRequest, + 'INVALID_UI_ACCOUNT_SETTINGS': BadRequest, + 'NEGATIVE_AMOUNT': InvalidOrder, + 'NEGATIVE_PRICE': InvalidOrder, + 'MIN_SIZE_NOT_SATISFIED': InvalidOrder, + 'BAD_AMOUNT_PRECISION': InvalidOrder, + 'BAD_PRICE_PRECISION': InvalidOrder, + 'BAD_TRIGGER_PRICE_PRECISION': InvalidOrder, + 'MAX_OPEN_ORDERS_EXCEEDED': BadRequest, + 'MISSING_PRICE': InvalidOrder, + 'MISSING_ORDER_TYPE': InvalidOrder, + 'MISSING_SIDE': InvalidOrder, + 'MISSING_CANDLESTICKS_PERIOD_PARAM': ArgumentsRequired, + 'MISSING_CANDLESTICKS_UNIT_PARAM': ArgumentsRequired, + 'MISSING_FROM_PARAM': ArgumentsRequired, + 'MISSING_INSTRUMENT_CODE': ArgumentsRequired, + 'MISSING_ORDER_ID': InvalidOrder, + 'MISSING_TO_PARAM': ArgumentsRequired, + 'MISSING_TRADE_ID': ArgumentsRequired, + 'INVALID_ORDER_ID': OrderNotFound, + 'NOT_FOUND': OrderNotFound, + 'INSUFFICIENT_LIQUIDITY': InsufficientFunds, + 'INSUFFICIENT_FUNDS': InsufficientFunds, + 'NO_TRADING': ExchangeNotAvailable, + 'SERVICE_UNAVAILABLE': ExchangeNotAvailable, + 'GATEWAY_TIMEOUT': ExchangeNotAvailable, + 'RATELIMIT': DDoSProtection, + 'CF_RATELIMIT': DDoSProtection, + 'INTERNAL_SERVER_ERROR': ExchangeError, + }, + 'broad': { + 'Order not found.': OrderNotFound, + }, + }, + 'commonCurrencies': { + 'MIOTA': 'IOTA', # https://github.com/ccxt/ccxt/issues/7487 + }, + # exchange-specific options + 'options': { + 'fetchTradingFees': { + 'method': 'fetchPrivateTradingFees', # or 'fetchPublicTradingFees' + }, + 'fiat': ['EUR', 'CHF'], + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': False, + 'triggerDirection': False, + 'triggerPriceType': None, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': False, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 100000, # todo + 'untilDays': 100000, # todo + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 100, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, # todo + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 100000, # todo + 'daysBackCanceled': 1 / 12, # todo + 'untilDays': 100000, # todo + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 5000, + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + }) + + def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://docs.onetrading.com/#time + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = self.publicGetTime(params) + # + # { + # "iso": "2020-07-10T05:17:26.716Z", + # "epoch_millis": 1594358246716, + # } + # + return self.safe_integer(response, 'epoch_millis') + + def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://docs.onetrading.com/#currencies + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = self.publicGetCurrencies(params) + # + # [ + # { + # "code": "USDT", + # "precision": 6, + # "unified_cryptoasset_id": 825, + # "name": "Tether USDt", + # "collateral_percentage": 0 + # }, + # ] + # + result: dict = {} + for i in range(0, len(response)): + currency = response[i] + id = self.safe_string(currency, 'code') + code = self.safe_currency_code(id) + result[code] = self.safe_currency_structure({ + 'id': id, + 'code': code, + 'name': self.safe_string(currency, 'name'), + 'info': currency, + 'active': None, + 'fee': None, + 'precision': self.parse_number(self.parse_precision(self.safe_string(currency, 'precision'))), + 'withdraw': None, + 'deposit': None, + 'limits': { + 'amount': {'min': None, 'max': None}, + 'withdraw': {'min': None, 'max': None}, + }, + 'networks': {}, + }) + return result + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for onetrading + + https://docs.onetrading.com/#instruments + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = self.publicGetInstruments(params) + # + # [ + # { + # "state": "ACTIVE", + # "base": {code: "ETH", precision: 8}, + # "quote": {code: "CHF", precision: 2}, + # "amount_precision": 4, + # "market_precision": 2, + # "min_size": "10.0" + # } + # ] + # + return self.parse_markets(response) + + def parse_market(self, market: dict) -> Market: + # + # { + # "base":{ + # "code":"BTC", + # "precision":"5" + # }, + # "quote":{ + # "code":"USDC", + # "precision":"2" + # }, + # "amount_precision":"5", + # "market_precision":"2", + # "min_size":"10.0", + # "min_price":"1000", + # "max_price":"10000000", + # "id":"BTC_USDC", + # "type":"SPOT", + # "state":"ACTIVE" + # } + # + # + # { + # "base": { + # "code": "BTC", + # "precision": 5 + # }, + # "quote": { + # "code": "EUR", + # "precision": 2 + # }, + # "amount_precision": 5, + # "market_precision": 2, + # "min_size": "10.0", + # "min_price": "1000", + # "max_price": "10000000", + # "id": "BTC_EUR_P", + # "type": "PERP", + # "state": "ACTIVE" + # } + # + baseAsset = self.safe_dict(market, 'base', {}) + quoteAsset = self.safe_dict(market, 'quote', {}) + baseId = self.safe_string(baseAsset, 'code') + quoteId = self.safe_string(quoteAsset, 'code') + id = self.safe_string(market, 'id') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + state = self.safe_string(market, 'state') + type = self.safe_string(market, 'type') + isPerp = type == 'PERP' + symbol = base + '/' + quote + if isPerp: + symbol = symbol + ':' + quote + return { + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': quote if isPerp else None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': quoteId if isPerp else None, + 'type': 'swap' if isPerp else 'spot', + 'spot': not isPerp, + 'margin': False, + 'swap': isPerp, + 'future': False, + 'option': False, + 'active': (state == 'ACTIVE'), + 'contract': isPerp, + 'linear': True if isPerp else None, + 'inverse': False if isPerp else None, + 'contractSize': self.parse_number('1') if isPerp else None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(self.parse_precision(self.safe_string(market, 'amount_precision'))), + 'price': self.parse_number(self.parse_precision(self.safe_string(market, 'market_precision'))), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': None, + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': self.safe_number(market, 'min_size'), + 'max': None, + }, + }, + 'created': None, + 'info': market, + } + + def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://docs.onetrading.com/#fee-groups + https://docs.onetrading.com/#fees + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.method]: fetchPrivateTradingFees or fetchPublicTradingFees + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + method = self.safe_string(params, 'method') + params = self.omit(params, 'method') + if method is None: + options = self.safe_value(self.options, 'fetchTradingFees', {}) + method = self.safe_string(options, 'method', 'fetchPrivateTradingFees') + if method == 'fetchPrivateTradingFees': + return self.fetch_private_trading_fees(params) + elif method == 'fetchPublicTradingFees': + return self.fetch_public_trading_fees(params) + else: + raise NotSupported(self.id + ' fetchTradingFees() does not support ' + method + ', fetchPrivateTradingFees and fetchPublicTradingFees are supported') + + def fetch_public_trading_fees(self, params={}): + self.load_markets() + response = self.publicGetFees(params) + # + # [ + # { + # 'fee_group_id': 'SPOT', + # 'display_text': 'The fee plan for spot trading.', + # 'volume_currency': 'EUR', + # 'fee_tiers': [ + # { + # 'volume': '0', + # 'fee_group_id': 'SPOT', + # 'maker_fee': '0.1000', + # 'taker_fee': '0.2000', + # }, + # { + # 'volume': '10000', + # 'fee_group_id': 'SPOT', + # 'maker_fee': '0.0400', + # 'taker_fee': '0.0800', + # }, + # ], + # }, + # { + # 'fee_group_id': 'FUTURES', + # 'display_text': 'The fee plan for futures trading.', + # 'volume_currency': 'EUR', + # 'fee_tiers': [ + # { + # 'volume': '0', + # 'fee_group_id': 'FUTURES', + # 'maker_fee': '0.1000', + # 'taker_fee': '0.2000', + # }, + # { + # 'volume': '10000', + # 'fee_group_id': 'FUTURES', + # 'maker_fee': '0.0400', + # 'taker_fee': '0.0800', + # }, + # ], + # }, + # ] + # + spotFees = self.safe_dict(response, 0, {}) + futuresFees = self.safe_dict(response, 1, {}) + spotFeeTiers = self.safe_list(spotFees, 'fee_tiers', []) + futuresFeeTiers = self.safe_list(futuresFees, 'fee_tiers', []) + spotTiers = self.parse_fee_tiers(spotFeeTiers) + futuresTiers = self.parse_fee_tiers(futuresFeeTiers) + firstSpotTier = self.safe_dict(spotTiers, 0, {}) + firstFuturesTier = self.safe_dict(futuresTiers, 0, {}) + result: dict = {} + for i in range(0, len(self.symbols)): + symbol = self.symbols[i] + market = self.market(symbol) + tierObject = firstSpotTier if (market['spot']) else firstFuturesTier + result[symbol] = { + 'info': spotFees, + 'symbol': symbol, + 'maker': self.safe_number(tierObject, 'maker_fee'), + 'taker': self.safe_number(tierObject, 'taker_fee'), + 'percentage': True, + 'tierBased': True, + 'tiers': spotTiers, + } + return result + + def fetch_private_trading_fees(self, params={}): + self.load_markets() + response = self.privateGetAccountFees(params) + # + # { + # "account_id":"b7f4e27e-b34a-493a-b0d4-4bd341a3f2e0", + # "running_volumes":[ + # { + # "fee_group_id":"SPOT", + # "volume":"0", + # "currency":"EUR" + # }, + # { + # "fee_group_id":"FUTURES", + # "volume":"0", + # "currency":"EUR" + # } + # ], + # "active_fee_tiers":[ + # { + # "fee_group_id":"SPOT", + # "volume":"0", + # "maker_fee":"0.1000", + # "taker_fee":"0.2000" + # }, + # { + # "fee_group_id":"FUTURES", + # "volume":"0", + # "maker_fee":"0.1000", + # "taker_fee":"0.2000" + # } + # ] + # } + # + activeFeeTier = self.safe_list(response, 'active_fee_tiers') + spotFees = self.safe_dict(activeFeeTier, 0, {}) + futuresFees = self.safe_dict(activeFeeTier, 1, {}) + spotMakerFee = self.safe_string(spotFees, 'maker_fee') + spotTakerFee = self.safe_string(spotFees, 'taker_fee') + spotMakerFee = Precise.string_div(spotMakerFee, '100') + spotTakerFee = Precise.string_div(spotTakerFee, '100') + # feeTiers = self.safe_value(response, 'fee_tiers') + futuresMakerFee = self.safe_string(futuresFees, 'maker_fee') + futuresTakerFee = self.safe_string(futuresFees, 'taker_fee') + futuresMakerFee = Precise.string_div(futuresMakerFee, '100') + futuresTakerFee = Precise.string_div(futuresTakerFee, '100') + result: dict = {} + # tiers = self.parse_fee_tiers(feeTiers) + for i in range(0, len(self.symbols)): + symbol = self.symbols[i] + market = self.market(symbol) + makerFee = spotMakerFee if (market['spot']) else futuresMakerFee + takerFee = spotTakerFee if (market['spot']) else futuresTakerFee + result[symbol] = { + 'info': response, + 'symbol': symbol, + 'maker': self.parse_number(makerFee), + 'taker': self.parse_number(takerFee), + 'percentage': True, + 'tierBased': True, + 'tiers': None, + } + return result + + def parse_fee_tiers(self, feeTiers, market: Market = None): + takerFees = [] + makerFees = [] + for i in range(0, len(feeTiers)): + tier = feeTiers[i] + volume = self.safe_number(tier, 'volume') + taker = self.safe_string(tier, 'taker_fee') + maker = self.safe_string(tier, 'maker_fee') + maker = Precise.string_div(maker, '100') + taker = Precise.string_div(taker, '100') + makerFees.append([volume, self.parse_number(maker)]) + takerFees.append([volume, self.parse_number(taker)]) + return { + 'maker': makerFees, + 'taker': takerFees, + } + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # fetchTicker, fetchTickers + # + # { + # "instrument_code":"BTC_EUR", + # "sequence":602562, + # "time":"2020-07-10T06:27:34.951Z", + # "state":"ACTIVE", + # "is_frozen":0, + # "quote_volume":"1695555.1783768", + # "base_volume":"205.67436", + # "last_price":"8143.91", + # "best_bid":"8143.71", + # "best_ask":"8156.9", + # "price_change":"-147.47", + # "price_change_percentage":"-1.78", + # "high":"8337.45", + # "low":"8110.0" + # } + # + timestamp = self.parse8601(self.safe_string(ticker, 'time')) + marketId = self.safe_string(ticker, 'instrument_code') + symbol = self.safe_symbol(marketId, market, '_') + last = self.safe_string(ticker, 'last_price') + percentage = self.safe_string(ticker, 'price_change_percentage') + change = self.safe_string(ticker, 'price_change') + baseVolume = self.safe_string(ticker, 'base_volume') + quoteVolume = self.safe_string(ticker, 'quote_volume') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': self.safe_string(ticker, 'best_bid'), + 'bidVolume': None, + 'ask': self.safe_string(ticker, 'best_ask'), + 'askVolume': None, + 'vwap': None, + 'open': None, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': change, + 'percentage': percentage, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'info': ticker, + }, market) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://docs.onetrading.com/#market-ticker-for-instrument + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'instrument_code': market['id'], + } + response = self.publicGetMarketTickerInstrumentCode(self.extend(request, params)) + # + # { + # "instrument_code":"BTC_EUR", + # "sequence":602562, + # "time":"2020-07-10T06:27:34.951Z", + # "state":"ACTIVE", + # "is_frozen":0, + # "quote_volume":"1695555.1783768", + # "base_volume":"205.67436", + # "last_price":"8143.91", + # "best_bid":"8143.71", + # "best_ask":"8156.9", + # "price_change":"-147.47", + # "price_change_percentage":"-1.78", + # "high":"8337.45", + # "low":"8110.0" + # } + # + return self.parse_ticker(response, market) + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://docs.onetrading.com/#market-ticker + + :param str[] [symbols]: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + symbols = self.market_symbols(symbols) + response = self.publicGetMarketTicker(params) + # + # [ + # { + # "instrument_code":"BTC_EUR", + # "sequence":602562, + # "time":"2020-07-10T06:27:34.951Z", + # "state":"ACTIVE", + # "is_frozen":0, + # "quote_volume":"1695555.1783768", + # "base_volume":"205.67436", + # "last_price":"8143.91", + # "best_bid":"8143.71", + # "best_ask":"8156.9", + # "price_change":"-147.47", + # "price_change_percentage":"-1.78", + # "high":"8337.45", + # "low":"8110.0" + # } + # ] + # + result: dict = {} + for i in range(0, len(response)): + ticker = self.parse_ticker(response[i]) + symbol = ticker['symbol'] + result[symbol] = ticker + return self.filter_by_array_tickers(result, 'symbol', symbols) + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://docs.onetrading.com/#order-book + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'instrument_code': market['id'], + # level 1 means only the best bid and ask + # level 2 is a compiled order book up to market precision + # level 3 is a full orderbook + # if you wish to get regular updates about orderbooks please use the Websocket channel + # heavy usage of self endpoint may result in limited access according to rate limits rules + # 'level': 3, # default + } + if limit is not None: + request['depth'] = limit + response = self.publicGetOrderBookInstrumentCode(self.extend(request, params)) + # + # level 1 + # + # { + # "instrument_code":"BTC_EUR", + # "time":"2020-07-10T07:39:06.343Z", + # "asks":{ + # "value":{ + # "price":"8145.29", + # "amount":"0.96538", + # "number_of_orders":1 + # } + # }, + # "bids":{ + # "value":{ + # "price":"8134.0", + # "amount":"1.5978", + # "number_of_orders":5 + # } + # } + # } + # + # level 2 + # + # { + # "instrument_code":"BTC_EUR","time":"2020-07-10T07:36:43.538Z", + # "asks":[ + # {"price":"8146.59","amount":"0.89691","number_of_orders":1}, + # {"price":"8146.89","amount":"1.92062","number_of_orders":1}, + # {"price":"8169.5","amount":"0.0663","number_of_orders":1}, + # ], + # "bids":[ + # {"price":"8143.49","amount":"0.01329","number_of_orders":1}, + # {"price":"8137.01","amount":"5.34748","number_of_orders":1}, + # {"price":"8137.0","amount":"2.0","number_of_orders":1}, + # ] + # } + # + # level 3 + # + # { + # "instrument_code":"BTC_EUR", + # "time":"2020-07-10T07:32:31.525Z", + # "bids":[ + # {"price":"8146.79","amount":"0.01537","order_id":"5d717da1-a8f4-422d-afcc-03cb6ab66825"}, + # {"price":"8139.32","amount":"3.66009","order_id":"d0715c68-f28d-4cf1-a450-d56cf650e11c"}, + # {"price":"8137.51","amount":"2.61049","order_id":"085fd6f4-e835-4ca5-9449-a8f165772e60"}, + # ], + # "asks":[ + # {"price":"8153.49","amount":"0.93384","order_id":"755d3aa3-42b5-46fa-903d-98f42e9ae6c4"}, + # {"price":"8153.79","amount":"1.80456","order_id":"62034cf3-b70d-45ff-b285-ba6307941e7c"}, + # {"price":"8167.9","amount":"0.0018","order_id":"036354e0-71cd-492f-94f2-01f7d4b66422"}, + # ] + # } + # + timestamp = self.parse8601(self.safe_string(response, 'time')) + return self.parse_order_book(response, market['symbol'], timestamp, 'bids', 'asks', 'price', 'amount') + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # { + # "instrument_code":"BTC_EUR", + # "granularity":{"unit":"HOURS","period":1}, + # "high":"9252.65", + # "low":"9115.27", + # "open":"9250.0", + # "close":"9132.35", + # "total_amount":"33.85924", + # "volume":"311958.9635744", + # "time":"2020-05-08T22:59:59.999Z", + # "last_sequence":461123 + # } + # + granularity = self.safe_value(ohlcv, 'granularity') + unit = self.safe_string(granularity, 'unit') + period = self.safe_string(granularity, 'period') + units: dict = { + 'MINUTES': 'm', + 'HOURS': 'h', + 'DAYS': 'd', + 'WEEKS': 'w', + 'MONTHS': 'M', + } + lowercaseUnit = self.safe_string(units, unit) + timeframe = period + lowercaseUnit + durationInSeconds = self.parse_timeframe(timeframe) + duration = durationInSeconds * 1000 + timestamp = self.parse8601(self.safe_string(ohlcv, 'time')) + alignedTimestamp = duration * self.parse_to_int(timestamp / duration) + options = self.safe_value(self.options, 'fetchOHLCV', {}) + volumeField = self.safe_string(options, 'volume', 'total_amount') + return [ + alignedTimestamp, + self.safe_number(ohlcv, 'open'), + self.safe_number(ohlcv, 'high'), + self.safe_number(ohlcv, 'low'), + self.safe_number(ohlcv, 'close'), + self.safe_number(ohlcv, volumeField), + ] + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://docs.onetrading.com/#candlesticks + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + market = self.market(symbol) + periodUnit = self.safe_string(self.timeframes, timeframe) + period, unit = periodUnit.split('/') + durationInSeconds = self.parse_timeframe(timeframe) + duration = durationInSeconds * 1000 + if limit is None: + limit = 1500 + request: dict = { + 'instrument_code': market['id'], + # 'from': self.iso8601(since), + # 'to': self.iso8601(self.milliseconds()), + 'period': period, + 'unit': unit, + } + if since is None: + now = self.milliseconds() + request['to'] = self.iso8601(now) + request['from'] = self.iso8601(now - limit * duration) + else: + request['from'] = self.iso8601(since) + request['to'] = self.iso8601(self.sum(since, limit * duration)) + response = self.publicGetCandlesticksInstrumentCode(self.extend(request, params)) + # + # [ + # {"instrument_code":"BTC_EUR","granularity":{"unit":"HOURS","period":1},"high":"9252.65","low":"9115.27","open":"9250.0","close":"9132.35","total_amount":"33.85924","volume":"311958.9635744","time":"2020-05-08T22:59:59.999Z","last_sequence":461123}, + # {"instrument_code":"BTC_EUR","granularity":{"unit":"HOURS","period":1},"high":"9162.49","low":"9040.0","open":"9132.53","close":"9083.69","total_amount":"26.19685","volume":"238553.7812365","time":"2020-05-08T23:59:59.999Z","last_sequence":461376}, + # {"instrument_code":"BTC_EUR","granularity":{"unit":"HOURS","period":1},"high":"9135.7","low":"9002.59","open":"9055.45","close":"9133.98","total_amount":"26.21919","volume":"238278.8724959","time":"2020-05-09T00:59:59.999Z","last_sequence":461521}, + # ] + # + ohlcv = self.safe_list(response, 'candlesticks') + return self.parse_ohlcvs(ohlcv, market, timeframe, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(public) + # + # { + # "instrument_code":"BTC_EUR", + # "price":"8137.28", + # "amount":"0.22269", + # "taker_side":"BUY", + # "volume":"1812.0908832", + # "time":"2020-07-10T14:44:32.299Z", + # "trade_timestamp":1594392272299, + # "sequence":603047 + # } + # + # fetchMyTrades, fetchOrder, fetchOpenOrders, fetchClosedOrders trades(private) + # + # { + # "fee": { + # "fee_amount": "0.0014", + # "fee_currency": "BTC", + # "fee_percentage": "0.1", + # "fee_group_id": "default", + # "fee_type": "TAKER", + # "running_trading_volume": "0.0" + # }, + # "trade": { + # "trade_id": "fdff2bcc-37d6-4a2d-92a5-46e09c868664", + # "order_id": "36bb2437-7402-4794-bf26-4bdf03526439", + # "account_id": "a4c699f6-338d-4a26-941f-8f9853bfc4b9", + # "amount": "1.4", + # "side": "BUY", + # "instrument_code": "BTC_EUR", + # "price": "7341.4", + # "time": "2019-09-27T15:05:32.564Z", + # "sequence": 48670 + # } + # } + # + feeInfo = self.safe_value(trade, 'fee', {}) + trade = self.safe_value(trade, 'trade', trade) + timestamp = self.safe_integer(trade, 'trade_timestamp') + if timestamp is None: + timestamp = self.parse8601(self.safe_string(trade, 'time')) + side = self.safe_string_lower_2(trade, 'side', 'taker_side') + priceString = self.safe_string(trade, 'price') + amountString = self.safe_string(trade, 'amount') + costString = self.safe_string(trade, 'volume') + marketId = self.safe_string(trade, 'instrument_code') + symbol = self.safe_symbol(marketId, market, '_') + feeCostString = self.safe_string(feeInfo, 'fee_amount') + takerOrMaker = None + fee = None + if feeCostString is not None: + feeCurrencyId = self.safe_string(feeInfo, 'fee_currency') + feeCurrencyCode = self.safe_currency_code(feeCurrencyId) + feeRateString = self.safe_string(feeInfo, 'fee_percentage') + fee = { + 'cost': feeCostString, + 'currency': feeCurrencyCode, + 'rate': feeRateString, + } + takerOrMaker = self.safe_string_lower(feeInfo, 'fee_type') + return self.safe_trade({ + 'id': self.safe_string_2(trade, 'trade_id', 'sequence'), + 'order': self.safe_string(trade, 'order_id'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'type': None, + 'side': side, + 'price': priceString, + 'amount': amountString, + 'cost': costString, + 'takerOrMaker': takerOrMaker, + 'fee': fee, + 'info': trade, + }, market) + + def parse_balance(self, response) -> Balances: + balances = self.safe_value(response, 'balances', []) + result: dict = {'info': response} + for i in range(0, len(balances)): + balance = balances[i] + currencyId = self.safe_string(balance, 'currency_code') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(balance, 'available') + account['used'] = self.safe_string(balance, 'locked') + result[code] = account + return self.safe_balance(result) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://docs.onetrading.com/#balances + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + response = self.privateGetAccountBalances(params) + # + # { + # "account_id":"4b95934f-55f1-460c-a525-bd5afc0cf071", + # "balances":[ + # { + # "account_id":"4b95934f-55f1-460c-a525-bd5afc0cf071", + # "currency_code":"BTC", + # "change":"10.0", + # "available":"10.0", + # "locked":"0.0", + # "sequence":142135994, + # "time":"2020-07-01T10:57:32.959Z" + # } + # ] + # } + # + return self.parse_balance(response) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'FILLED': 'open', + 'FILLED_FULLY': 'closed', + 'FILLED_CLOSED': 'canceled', + 'FILLED_REJECTED': 'rejected', + 'OPEN': 'open', + 'REJECTED': 'rejected', + 'CLOSED': 'canceled', + 'FAILED': 'failed', + 'STOP_TRIGGERED': 'triggered', + 'DONE': 'closed', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder + # + # { + # "order_id": "d5492c24-2995-4c18-993a-5b8bf8fffc0d", + # "client_id": "d75fb03b-b599-49e9-b926-3f0b6d103206", + # "account_id": "a4c699f6-338d-4a26-941f-8f9853bfc4b9", + # "instrument_code": "BTC_EUR", + # "time": "2019-08-01T08:00:44.026Z", + # "side": "BUY", + # "price": "5000", + # "amount": "1", + # "filled_amount": "0.5", + # "type": "LIMIT", + # "time_in_force": "GOOD_TILL_CANCELLED" + # } + # + # fetchOrder, fetchOpenOrders, fetchClosedOrders + # + # { + # "order": { + # "order_id": "66756a10-3e86-48f4-9678-b634c4b135b2", + # "account_id": "1eb2ad5d-55f1-40b5-bc92-7dc05869e905", + # "instrument_code": "BTC_EUR", + # "amount": "1234.5678", + # "filled_amount": "1234.5678", + # "side": "BUY", + # "type": "LIMIT", + # "status": "OPEN", + # "sequence": 123456789, + # "price": "1234.5678", + # "average_price": "1234.5678", + # "reason": "INSUFFICIENT_FUNDS", + # "time": "2019-08-24T14:15:22Z", + # "time_in_force": "GOOD_TILL_CANCELLED", + # "time_last_updated": "2019-08-24T14:15:22Z", + # "expire_after": "2019-08-24T14:15:22Z", + # "is_post_only": False, + # "time_triggered": "2019-08-24T14:15:22Z", + # "trigger_price": "1234.5678" + # }, + # "trades": [ + # { + # "fee": { + # "fee_amount": "0.0014", + # "fee_currency": "BTC", + # "fee_percentage": "0.1", + # "fee_group_id": "default", + # "fee_type": "TAKER", + # "running_trading_volume": "0.0" + # }, + # "trade": { + # "trade_id": "fdff2bcc-37d6-4a2d-92a5-46e09c868664", + # "order_id": "36bb2437-7402-4794-bf26-4bdf03526439", + # "account_id": "a4c699f6-338d-4a26-941f-8f9853bfc4b9", + # "amount": "1.4", + # "side": "BUY", + # "instrument_code": "BTC_EUR", + # "price": "7341.4", + # "time": "2019-09-27T15:05:32.564Z", + # "sequence": 48670 + # } + # } + # ] + # } + # + rawOrder = self.safe_value(order, 'order', order) + id = self.safe_string(rawOrder, 'order_id') + clientOrderId = self.safe_string(rawOrder, 'client_id') + timestamp = self.parse8601(self.safe_string(rawOrder, 'time')) + rawStatus = self.parse_order_status(self.safe_string(rawOrder, 'status')) + status = self.parse_order_status(rawStatus) + marketId = self.safe_string(rawOrder, 'instrument_code') + symbol = self.safe_symbol(marketId, market, '_') + price = self.safe_string(rawOrder, 'price') + amount = self.safe_string(rawOrder, 'amount') + filled = self.safe_string(rawOrder, 'filled_amount') + side = self.safe_string_lower(rawOrder, 'side') + type = self.safe_string_lower(rawOrder, 'type') + timeInForce = self.parse_time_in_force(self.safe_string(rawOrder, 'time_in_force')) + postOnly = self.safe_value(rawOrder, 'is_post_only') + rawTrades = self.safe_value(order, 'trades', []) + return self.safe_order({ + 'id': id, + 'clientOrderId': clientOrderId, + 'info': order, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'symbol': symbol, + 'type': self.parse_order_type(type), + 'timeInForce': timeInForce, + 'postOnly': postOnly, + 'side': side, + 'price': price, + 'triggerPrice': self.safe_number(rawOrder, 'trigger_price'), + 'amount': amount, + 'cost': None, + 'average': None, + 'filled': filled, + 'remaining': None, + 'status': status, + # 'fee': None, + 'trades': rawTrades, + }, market) + + def parse_order_type(self, type: Str): + types: dict = { + 'booked': 'limit', + } + return self.safe_string(types, type, type) + + def parse_time_in_force(self, timeInForce: Str): + timeInForces: dict = { + 'GOOD_TILL_CANCELLED': 'GTC', + 'GOOD_TILL_TIME': 'GTT', + 'IMMEDIATE_OR_CANCELLED': 'IOC', + 'FILL_OR_KILL': 'FOK', + } + return self.safe_string(timeInForces, timeInForce, timeInForce) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://docs.onetrading.com/#create-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: onetrading only does stop limit orders and does not do stop market + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + uppercaseType = type.upper() + request: dict = { + 'instrument_code': market['id'], + 'type': uppercaseType, # LIMIT, MARKET, STOP + 'side': side.upper(), # or SELL + 'amount': self.amount_to_precision(symbol, amount), + # "price": "1234.5678", # required for LIMIT and STOP orders + # "client_id": "d75fb03b-b599-49e9-b926-3f0b6d103206", # optional + # "time_in_force": "GOOD_TILL_CANCELLED", # limit orders only, GOOD_TILL_CANCELLED, GOOD_TILL_TIME, IMMEDIATE_OR_CANCELLED and FILL_OR_KILL + # "expire_after": "2020-07-02T19:40:13Z", # required for GOOD_TILL_TIME + # "is_post_only": False, # limit orders only, optional + # "trigger_price": "1234.5678" # required for stop orders + } + priceIsRequired = False + if uppercaseType == 'LIMIT' or uppercaseType == 'STOP': + priceIsRequired = True + triggerPrice = self.safe_number_n(params, ['triggerPrice', 'trigger_price', 'stopPrice']) + if triggerPrice is not None: + if uppercaseType == 'MARKET': + raise BadRequest(self.id + ' createOrder() cannot place stop market orders, only stop limit') + request['trigger_price'] = self.price_to_precision(symbol, triggerPrice) + request['type'] = 'STOP' + params = self.omit(params, ['triggerPrice', 'trigger_price', 'stopPrice']) + elif uppercaseType == 'STOP': + raise ArgumentsRequired(self.id + ' createOrder() requires a triggerPrice param for ' + type + ' orders') + if priceIsRequired: + request['price'] = self.price_to_precision(symbol, price) + clientOrderId = self.safe_string_2(params, 'clientOrderId', 'client_id') + if clientOrderId is not None: + request['client_id'] = clientOrderId + params = self.omit(params, ['clientOrderId', 'client_id']) + timeInForce = self.safe_string_2(params, 'timeInForce', 'time_in_force', 'GOOD_TILL_CANCELLED') + params = self.omit(params, 'timeInForce') + request['time_in_force'] = timeInForce + response = self.privatePostAccountOrders(self.extend(request, params)) + # + # { + # "order_id": "d5492c24-2995-4c18-993a-5b8bf8fffc0d", + # "client_id": "d75fb03b-b599-49e9-b926-3f0b6d103206", + # "account_id": "a4c699f6-338d-4a26-941f-8f9853bfc4b9", + # "instrument_code": "BTC_EUR", + # "time": "2019-08-01T08:00:44.026Z", + # "side": "BUY", + # "price": "5000", + # "amount": "1", + # "filled_amount": "0.5", + # "type": "LIMIT", + # "time_in_force": "GOOD_TILL_CANCELLED" + # } + # + return self.parse_order(response, market) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://docs.onetrading.com/#close-order-by-order-id + + :param str id: order id + :param str symbol: not used by bitmex cancelOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + clientOrderId = self.safe_string_2(params, 'clientOrderId', 'client_id') + params = self.omit(params, ['clientOrderId', 'client_id']) + method = 'privateDeleteAccountOrdersOrderId' + request: dict = {} + if clientOrderId is not None: + method = 'privateDeleteAccountOrdersClientClientId' + request['client_id'] = clientOrderId + else: + request['order_id'] = id + response = None + if method == 'privateDeleteAccountOrdersOrderId': + response = self.privateDeleteAccountOrdersOrderId(self.extend(request, params)) + else: + response = self.privateDeleteAccountOrdersClientClientId(self.extend(request, params)) + # + # responds with an empty body + # + return self.parse_order(response) + + def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders + + https://docs.onetrading.com/#close-all-orders + + :param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + self.load_markets() + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['instrument_code'] = market['id'] + response = self.privateDeleteAccountOrders(self.extend(request, params)) + # + # [ + # "a10e9bd1-8f72-4cfe-9f1b-7f1c8a9bd8ee" + # ] + # + return [self.safe_order({'info': response})] + + def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): + """ + cancel multiple orders + + https://docs.onetrading.com/#close-all-orders + + :param str[] ids: order ids + :param str symbol: unified market symbol, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an list of `order structures ` + """ + self.load_markets() + request: dict = { + 'ids': ','.join(ids), + } + response = self.privateDeleteAccountOrders(self.extend(request, params)) + # + # [ + # "a10e9bd1-8f72-4cfe-9f1b-7f1c8a9bd8ee" + # ] + # + order = self.safe_order({'info': response}) + return [order] + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://docs.onetrading.com/#get-order + + :param str id: the order id + :param str symbol: not used by onetrading fetchOrder + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + request: dict = { + 'order_id': id, + } + response = self.privateGetAccountOrdersOrderId(self.extend(request, params)) + # + # { + # "order": { + # "order_id": "36bb2437-7402-4794-bf26-4bdf03526439", + # "account_id": "a4c699f6-338d-4a26-941f-8f9853bfc4b9", + # "time_last_updated": "2019-09-27T15:05:35.096Z", + # "sequence": 48782, + # "price": "7349.2", + # "filled_amount": "100.0", + # "status": "FILLED_FULLY", + # "amount": "100.0", + # "instrument_code": "BTC_EUR", + # "side": "BUY", + # "time": "2019-09-27T15:05:32.063Z", + # "type": "MARKET" + # }, + # "trades": [ + # { + # "fee": { + # "fee_amount": "0.0014", + # "fee_currency": "BTC", + # "fee_percentage": "0.1", + # "fee_group_id": "default", + # "fee_type": "TAKER", + # "running_trading_volume": "0.0" + # }, + # "trade": { + # "trade_id": "fdff2bcc-37d6-4a2d-92a5-46e09c868664", + # "order_id": "36bb2437-7402-4794-bf26-4bdf03526439", + # "account_id": "a4c699f6-338d-4a26-941f-8f9853bfc4b9", + # "amount": "1.4", + # "side": "BUY", + # "instrument_code": "BTC_EUR", + # "price": "7341.4", + # "time": "2019-09-27T15:05:32.564Z", + # "sequence": 48670 + # } + # } + # ] + # } + # + return self.parse_order(response) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://docs.onetrading.com/#get-orders + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + request: dict = { + # 'from': self.iso8601(since), + # 'to': self.iso8601(self.milliseconds()), # max range is 100 days + # 'instrument_code': market['id'], + # 'with_cancelled_and_rejected': False, # default is False, orders which have been cancelled by the user before being filled or rejected by the system, additionally, all inactive filled orders which would return with "with_just_filled_inactive" + # 'with_just_filled_inactive': False, # orders which have been filled and are no longer open, use of "with_cancelled_and_rejected" extends "with_just_filled_inactive" and in case both are specified the latter is ignored + # 'with_just_orders': False, # do not return any trades corresponsing to the orders, it may be significanly faster and should be used if user is not interesting in trade information + # 'max_page_size': 100, + # 'cursor': 'string', # pointer specifying the position from which the next pages should be returned + } + market = None + if symbol is not None: + market = self.market(symbol) + request['instrument_code'] = market['id'] + if since is not None: + to = self.safe_string(params, 'to') + if to is None: + raise ArgumentsRequired(self.id + ' fetchOpenOrders() requires a "to" iso8601 string param with the since argument is specified, max range is 100 days') + request['from'] = self.iso8601(since) + if limit is not None: + request['max_page_size'] = limit + response = self.privateGetAccountOrders(self.extend(request, params)) + # + # { + # "order_history": [ + # { + # "order": { + # "trigger_price": "12089.88", + # "order_id": "d453ca12-c650-46dd-9dee-66910d96bfc0", + # "account_id": "ef3a5f4c-cfcd-415e-ba89-5a9abf47b28a", + # "instrument_code": "BTC_USDT", + # "time": "2019-08-23T10:02:31.663Z", + # "side": "SELL", + # "price": "10159.76", + # "average_price": "10159.76", + # "amount": "0.2", + # "filled_amount": "0.2", + # "type": "STOP", + # "sequence": 8, + # "status": "FILLED_FULLY" + # }, + # "trades": [ + # { + # "fee": { + # "fee_amount": "0.4188869", + # "fee_currency": "USDT", + # "fee_percentage": "0.1", + # "fee_group_id": "default", + # "fee_type": "TAKER", + # "running_trading_volume": "0.0" + # }, + # "trade": { + # "trade_id": "ec82896f-fd1b-4cbb-89df-a9da85ccbb4b", + # "order_id": "d453ca12-c650-46dd-9dee-66910d96bfc0", + # "account_id": "ef3a5f4c-cfcd-415e-ba89-5a9abf47b28a", + # "amount": "0.2", + # "side": "SELL", + # "instrument_code": "BTC_USDT", + # "price": "10159.76", + # "time": "2019-08-23T10:02:32.663Z", + # "sequence": 9 + # } + # } + # ] + # }, + # { + # "order": { + # "order_id": "5151a99e-f414-418f-8cf1-2568d0a63ea5", + # "account_id": "ef3a5f4c-cfcd-415e-ba89-5a9abf47b28a", + # "instrument_code": "BTC_USDT", + # "time": "2019-08-23T10:01:36.773Z", + # "side": "SELL", + # "price": "12289.88", + # "amount": "0.5", + # "filled_amount": "0.0", + # "type": "LIMIT", + # "sequence": 7, + # "status": "OPEN" + # }, + # "trades": [] + # }, + # { + # "order": { + # "order_id": "ac80d857-75e1-4733-9070-fd4288395fdc", + # "account_id": "ef3a5f4c-cfcd-415e-ba89-5a9abf47b28a", + # "instrument_code": "BTC_USDT", + # "time": "2019-08-23T10:01:25.031Z", + # "side": "SELL", + # "price": "11089.88", + # "amount": "0.1", + # "filled_amount": "0.0", + # "type": "LIMIT", + # "sequence": 6, + # "status": "OPEN" + # }, + # "trades": [] + # } + # ], + # "max_page_size": 100 + # } + # + orderHistory = self.safe_list(response, 'order_history', []) + return self.parse_orders(orderHistory, market, since, limit) + + def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://docs.onetrading.com/#get-orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + request: dict = { + 'with_cancelled_and_rejected': True, # default is False, orders which have been cancelled by the user before being filled or rejected by the system, additionally, all inactive filled orders which would return with "with_just_filled_inactive" + } + return self.fetch_open_orders(symbol, since, limit, self.extend(request, params)) + + def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all the trades made from a single order + + https://docs.onetrading.com/#trades-for-order + + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + self.load_markets() + request: dict = { + 'order_id': id, + # 'max_page_size': 100, + # 'cursor': 'string', # pointer specifying the position from which the next pages should be returned + } + if limit is not None: + request['max_page_size'] = limit + response = self.privateGetAccountOrdersOrderIdTrades(self.extend(request, params)) + # + # { + # "trade_history": [ + # { + # "trade": { + # "trade_id": "2b42efcd-d5b7-4a56-8e12-b69ffd68c5ef", + # "order_id": "66756a10-3e86-48f4-9678-b634c4b135b2", + # "account_id": "c2d0076a-c20d-41f8-9e9a-1a1d028b2b58", + # "amount": "1234.5678", + # "side": "BUY", + # "instrument_code": "BTC_EUR", + # "price": "1234.5678", + # "time": "2019-08-24T14:15:22Z", + # "price_tick_sequence": 0, + # "sequence": 123456789 + # }, + # "fee": { + # "fee_amount": "1234.5678", + # "fee_percentage": "1234.5678", + # "fee_group_id": "default", + # "running_trading_volume": "1234.5678", + # "fee_currency": "BTC", + # "fee_type": "TAKER" + # } + # } + # ], + # "max_page_size": 0, + # "cursor": "string" + # } + # + tradeHistory = self.safe_value(response, 'trade_history', []) + market = None + if symbol is not None: + market = self.market(symbol) + return self.parse_trades(tradeHistory, market, since, limit) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://docs.onetrading.com/#all-trades + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + request: dict = { + # 'from': self.iso8601(since), + # 'to': self.iso8601(self.milliseconds()), # max range is 100 days + # 'instrument_code': market['id'], + # 'max_page_size': 100, + # 'cursor': 'string', # pointer specifying the position from which the next pages should be returned + } + market = None + if symbol is not None: + market = self.market(symbol) + request['instrument_code'] = market['id'] + if since is not None: + to = self.safe_string(params, 'to') + if to is None: + raise ArgumentsRequired(self.id + ' fetchMyTrades() requires a "to" iso8601 string param with the since argument is specified, max range is 100 days') + request['from'] = self.iso8601(since) + if limit is not None: + request['max_page_size'] = limit + response = self.privateGetAccountTrades(self.extend(request, params)) + # + # { + # "trade_history": [ + # { + # "trade": { + # "trade_id": "2b42efcd-d5b7-4a56-8e12-b69ffd68c5ef", + # "order_id": "66756a10-3e86-48f4-9678-b634c4b135b2", + # "account_id": "c2d0076a-c20d-41f8-9e9a-1a1d028b2b58", + # "amount": "1234.5678", + # "side": "BUY", + # "instrument_code": "BTC_EUR", + # "price": "1234.5678", + # "time": "2019-08-24T14:15:22Z", + # "price_tick_sequence": 0, + # "sequence": 123456789 + # }, + # "fee": { + # "fee_amount": "1234.5678", + # "fee_percentage": "1234.5678", + # "fee_group_id": "default", + # "running_trading_volume": "1234.5678", + # "fee_currency": "BTC", + # "fee_type": "TAKER" + # } + # } + # ], + # "max_page_size": 0, + # "cursor": "string" + # } + # + tradeHistory = self.safe_list(response, 'trade_history', []) + return self.parse_trades(tradeHistory, market, since, limit) + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = self.urls['api'][api] + '/' + self.version + '/' + self.implode_params(path, params) + query = self.omit(params, self.extract_params(path)) + if api == 'public': + if query: + url += '?' + self.urlencode(query) + elif api == 'private': + self.check_required_credentials() + headers = { + 'Accept': 'application/json', + 'Authorization': 'Bearer ' + self.apiKey, + } + if method == 'POST': + body = self.json(query) + headers['Content-Type'] = 'application/json' + else: + if query: + url += '?' + self.urlencode(query) + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None + # + # {"error":"MISSING_FROM_PARAM"} + # {"error":"MISSING_TO_PARAM"} + # {"error":"CANDLESTICKS_TIME_RANGE_TOO_BIG"} + # + message = self.safe_string(response, 'error') + if message is not None: + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + raise ExchangeError(feedback) # unknown message + return None diff --git a/ccxt/oxfun.py b/ccxt/oxfun.py new file mode 100644 index 0000000..7a91944 --- /dev/null +++ b/ccxt/oxfun.py @@ -0,0 +1,2860 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.oxfun import ImplicitAPI +import hashlib +from ccxt.base.types import Account, Any, Balances, Bool, Currencies, Currency, DepositAddress, Int, LeverageTier, LeverageTiers, Market, Num, Order, OrderBook, OrderRequest, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, FundingRates, Trade, Transaction, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import AccountNotEnabled +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import MarketClosed +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import NotSupported +from ccxt.base.errors import OperationFailed +from ccxt.base.errors import NetworkError +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import RequestTimeout +from ccxt.base.decimal_to_precision import TICK_SIZE + + +class oxfun(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(oxfun, self).describe(), { + 'id': 'oxfun', + 'name': 'OXFUN', + 'countries': ['PA'], # Panama todo check + 'version': 'v3', + 'rateLimit': 120, # 100 requests per second and 25000 per 5 minutes + 'pro': True, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': True, + 'future': False, + 'option': False, + 'addMargin': False, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'cancelOrders': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createDepositAddress': False, + 'createMarketBuyOrderWithCost': True, + 'createMarketOrderWithCost': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createOrders': True, + 'createPostOnlyOrder': True, + 'createReduceOnlyOrder': False, + 'createStopLimitOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'deposit': False, + 'editOrder': False, + 'fetchAccounts': True, + 'fetchBalance': True, + 'fetchBidsAsks': False, + 'fetchBorrowInterest': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchCanceledOrders': False, + 'fetchClosedOrder': False, + 'fetchClosedOrders': False, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDeposit': False, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchDepositWithdrawFee': False, + 'fetchDepositWithdrawFees': False, + 'fetchFundingHistory': True, + 'fetchFundingRate': True, + 'fetchFundingRateHistory': True, + 'fetchFundingRates': True, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchL3OrderBook': False, + 'fetchLedger': False, + 'fetchLeverage': False, + 'fetchLeverageTiers': True, + 'fetchMarketLeverageTiers': 'emulated', + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterestHistory': False, + 'fetchOpenOrder': False, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrderBooks': False, + 'fetchOrders': False, + 'fetchOrderTrades': False, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': True, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchStatus': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': False, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': False, + 'fetchTradingLimits': False, + 'fetchTransactionFee': False, + 'fetchTransactionFees': False, + 'fetchTransactions': False, + 'fetchTransfers': True, + 'fetchWithdrawal': False, + 'fetchWithdrawals': True, + 'fetchWithdrawalWhitelist': False, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'sandbox': True, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'signIn': False, + 'transfer': True, + 'withdraw': True, + 'ws': True, + }, + 'timeframes': { + '1m': '60s', + '5m': '300s', + '15m': '900s', + '30m': '1800s', + '1h': '3600s', + '2h': '7200s', + '4h': '14400s', + '1d': '86400s', + }, + 'urls': { + 'logo': 'https://github.com/ccxt/ccxt/assets/43336371/6a196124-c1ee-4fae-8573-962071b61a85', + 'referral': 'https://ox.fun/register?shareAccountId=5ZUD4a7G', + 'api': { + 'public': 'https://api.ox.fun', + 'private': 'https://api.ox.fun', + }, + 'test': { + 'public': 'https://stgapi.ox.fun', + 'private': 'https://stgapi.ox.fun', + }, + 'www': 'https://ox.fun/', + 'doc': 'https://docs.ox.fun/', + 'fees': 'https://support.ox.fun/en/articles/8819866-trading-fees', + }, + 'api': { + 'public': { + 'get': { + 'v3/markets': 1, + 'v3/assets': 1, + 'v3/tickers': 1, + 'v3/funding/estimates': 1, + 'v3/candles': 1, + 'v3/depth': 1, + 'v3/markets/operational': 1, + 'v3/exchange-trades': 1, + 'v3/funding/rates': 1, + 'v3/leverage/tiers': 1, + }, + }, + 'private': { + 'get': { + 'v3/account': 1, + 'v3/account/names': 1, + 'v3/wallet': 1, # retruns only FUNDING in OX + 'v3/transfer': 1, + 'v3/balances': 1, + 'v3/positions': 1, + 'v3/funding': 1, + 'v3/deposit-addresses': 1, + 'v3/deposit': 1, + 'v3/withdrawal-addresses': 1, + 'v3/withdrawal': 1, + 'v3/withdrawal-fees': 1, + 'v3/orders/status': 1, + 'v3/orders/working': 1, + 'v3/trades': 1, + }, + 'post': { + 'v3/transfer': 1, + 'v3/withdrawal': 1, + 'v3/orders/place': 1, + }, + 'delete': { + 'v3/orders/cancel': 1, + 'v3/orders/cancel-all': 1, + }, + }, + }, + 'fees': { + 'trading': { + 'tierBased': True, + 'percentage': True, + 'maker': self.parse_number('0.00020'), + 'taker': self.parse_number('0.00070'), + 'tiers': { + 'maker': [ + [self.parse_number('0'), self.parse_number('0.00020')], + [self.parse_number('2500000'), self.parse_number('0.00010')], + [self.parse_number('25000000'), self.parse_number('0')], + ], + 'taker': [ + [self.parse_number('0'), self.parse_number('0.00070')], + [self.parse_number('2500000'), self.parse_number('0.00050')], + [self.parse_number('25000000'), self.parse_number('0.00040')], + ], + }, + }, + }, + 'precisionMode': TICK_SIZE, + # exchange-specific options + 'options': { + 'sandboxMode': False, + 'networks': { + 'BTC': 'Bitcoin', + 'ERC20': 'Ethereum', + 'AVAX': 'Avalanche', + 'SOL': 'Solana', + 'ARB': 'Arbitrum', + 'MATIC': 'Polygon', + 'FTM': 'Fantom', + 'BNB': 'BNBSmartChain', + 'OPTIMISM': 'Optimism', + }, + 'networksById': { + 'Bitcoin': 'BTC', + 'Ethereum': 'ERC20', + 'Avalanche': 'AVAX', + 'Solana': 'SOL', + 'Arbitrum': 'ARB', + 'Polygon': 'MATIC', + 'Fantom': 'FTM', + 'Base': 'BASE', + 'BNBSmartChain': 'BNB', + 'Optimism': 'OPTIMISM', + }, + }, + 'features': { + 'default': { + 'sandbox': True, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerDirection': False, + 'triggerPriceType': None, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': { + 'EXPIRE_MAKER': True, + 'EXPIRE_TAKER': True, + 'EXPIRE_BOTH': True, + 'NONE': True, + }, + 'iceberg': True, # todo + }, + 'createOrders': { + 'max': 10, # todo + }, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 500, + 'daysBack': 100000, # todo + 'untilDays': 7, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': None, # todo? + 'fetchOHLCV': { + 'limit': 500, + }, + }, + 'spot': { + 'extends': 'default', + }, + 'swap': { + 'linear': { + 'extends': 'default', + }, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'exceptions': { + 'exact': { + '-0010': OperationFailed, # {"event":null,"success":false,"message":"Validation failed","code":"0010","data":null} - failed transfer + '-429': RateLimitExceeded, # Rate limit reached + '-05001': AuthenticationError, # Your operation authority is invalid + '-10001': ExchangeError, # General networking failure + '-20000': BadRequest, # Signature is invalid + '-20001': BadRequest, # "success":false,"code":"20001","message":"marketCode is invalid" + '-20002': BadRequest, # Unexpected error, please check if your request data complies with the specification. + '-20003': NotSupported, # Unrecognized operation + '-20005': AuthenticationError, # Already logged in + '-20006': BadRequest, # Quantity must be greater than zero + '-20007': AuthenticationError, # You are accessing server too rapidly + '-20008': BadRequest, # clientOrderId must be greater than zero if provided + '-20009': BadRequest, # JSON data format is invalid + '-20010': ArgumentsRequired, # Either clientOrderId or orderId is required + '-20011': ArgumentsRequired, # marketCode is required + '-20012': ArgumentsRequired, # side is required + '-20013': ArgumentsRequired, # orderType is required + '-20014': BadRequest, # clientOrderId is not long type + '-20015': BadSymbol, # marketCode is invalid + '-20016': BadRequest, # side is invalid + '-20017': BadRequest, # orderType is invalid + '-20018': BadRequest, # timeInForce is invalid + '-20019': BadRequest, # orderId is invalid + '-20020': BadRequest, # stopPrice or limitPrice is invalid + '-20021': BadRequest, # price is invalid + '-20022': ArgumentsRequired, # price is required for LIMIT order + '-20023': ArgumentsRequired, # timestamp is required + '-20024': ExchangeError, # timestamp exceeds the threshold + '-20025': AuthenticationError, # API key is invalid + '-20026': BadRequest, # Token is invalid or expired + '-20027': BadRequest, # The length of the message exceeds the maximum length + '-20028': BadRequest, # price or stopPrice or limitPrice must be greater than zero + '-20029': BadRequest, # stopPrice must be less than limitPrice for Buy Stop Order + '-20030': BadRequest, # limitPrice must be less than stopPrice for Sell Stop Order + '-20031': MarketClosed, # The marketCode is closed for trading temporarily + '-20032': NetworkError, # Failed to submit due to timeout in server side + '-20033': BadRequest, # triggerType is invalid + '-20034': BadRequest, # The size of tag must be less than 32 + '-20050': ExchangeError, # selfTradePreventionMode is invalid + '-30001': BadRequest, # {"success":false,"code":"30001","message":"Required parameter 'marketCode' is missing"} + '-35034': AuthenticationError, # {"success":false,"code":"35034","message":"Wallet API is not functioning properly, please try again or contact support."} + '-35046': AuthenticationError, # {"success":false,"code":"35046","message":"Error. Please refresh the page."} + '-40001': ExchangeError, # Alert from the server + '-50001': ExchangeError, # Unknown server error + '-300001': AccountNotEnabled, # Invalid account status xxx, please contact administration if any questions + '-300011': InvalidOrder, # Repo market orders are not allowed during the auction window + '-300012': InvalidOrder, # Repo bids above 0 and offers below 0 are not allowed during the auction window + '-100005': OrderNotFound, # Open order not found + '-100006': InvalidOrder, # Open order is not owned by the user + '-100008': BadRequest, # Quantity cannot be less than the quantity increment xxx + '-100015': NetworkError, # recvWindow xxx has expired + '-710001': ExchangeError, # System failure, exception thrown -> xxx + '-710002': BadRequest, # The price is lower than the minimum + '-710003': BadRequest, # The price is higher than the maximum + '-710004': BadRequest, # Position quantity exceeds the limit + '-710005': InsufficientFunds, # Insufficient margin + '-710006': InsufficientFunds, # Insufficient balance + '-710007': InsufficientFunds, # Insufficient position + '-000101': NetworkError, # Internal server is unavailable temporary, try again later + '-000201': NetworkError, # Trade service is busy, try again later + }, + 'broad': { + '-20001': OperationFailed, # Operation failed, please contact system administrator + '-200050': RequestTimeout, # The market is not active + }, + }, + }) + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for bitmex + + https://docs.ox.fun/?json#get-v3-markets + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + responseFromMarkets, responseFromTickers = [self.publicGetV3Markets(params), self.publicGetV3Tickers(params)] + marketsFromMarkets = self.safe_list(responseFromMarkets, 'data', []) + # + # { + # success: True, + # data: [ + # { + # marketCode: 'OX-USD-SWAP-LIN', + # name: 'OX/USD Perp', + # referencePair: 'OX/USDT', + # base: 'OX', + # counter: 'USD', + # type: 'FUTURE', + # tickSize: '0.00001', + # minSize: '1', + # listedAt: '1704766320000', + # upperPriceBound: '0.02122', + # lowerPriceBound: '0.01142', + # markPrice: '0.01632', + # indexPrice: '0.01564', + # lastUpdatedAt: '1714762235569' + # }, + # { + # marketCode: 'BTC-USD-SWAP-LIN', + # name: 'BTC/USD Perp', + # referencePair: 'BTC/USDT', + # base: 'BTC', + # counter: 'USD', + # type: 'FUTURE', + # tickSize: '1', + # minSize: '0.0001', + # listedAt: '1704686640000', + # upperPriceBound: '67983', + # lowerPriceBound: '55621', + # markPrice: '61802', + # indexPrice: '61813', + # lastUpdatedAt: '1714762234765' + # }, + # { + # "marketCode": "MILK-OX", + # "name": "MILK/OX", + # "referencePair": "MILK/OX", + # "base": "MILK", + # "counter": "OX", + # "type": "SPOT", + # "tickSize": "0.0001", + # "minSize": "1", + # "listedAt": "1706608500000", + # "upperPriceBound": "1.0000", + # "lowerPriceBound": "-1.0000", + # "markPrice": "0.0269", + # "indexPrice": "0.0269", + # "lastUpdatedAt": "1714757402185" + # }, + # ... + # ] + # } + # + marketsFromTickers = self.safe_list(responseFromTickers, 'data', []) + # + # { + # "success": True, + # "data": [ + # { + # "marketCode": "DYM-USD-SWAP-LIN", + # "markPrice": "3.321", + # "open24h": "3.315", + # "high24h": "3.356", + # "low24h": "3.255", + # "volume24h": "0", + # "currencyVolume24h": "0", + # "openInterest": "1768.1", + # "lastTradedPrice": "3.543", + # "lastTradedQuantity": "1.0", + # "lastUpdatedAt": "1714853388102" + # }, + # ... + # ] + # } + # + markets = self.array_concat(marketsFromMarkets, marketsFromTickers) + return self.parse_markets(markets) + + def parse_markets(self, markets) -> List[Market]: + marketIds = [] + result = [] + for i in range(0, len(markets)): + market = markets[i] + marketId = self.safe_string(market, 'marketCode') + if not (self.in_array(marketId, marketIds)): + marketIds.append(marketId) + result.append(self.parse_market(market)) + return result + + def parse_market(self, market) -> Market: + id = self.safe_string(market, 'marketCode', '') + parts = id.split('-') + baseId = self.safe_string(parts, 0) + quoteId = self.safe_string(parts, 1) + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + symbol = base + '/' + quote + type = self.safe_string_lower(market, 'type', 'spot') # markets from v3/tickers are spot and have no type + settleId: Str = None + settle: Str = None + isFuture = (type == 'future') # the exchange has only perpetual futures + if isFuture: + type = 'swap' + settleId = 'OX' + settle = self.safe_currency_code('OX') + symbol = symbol + ':' + settle + isSpot = type == 'spot' + return self.safe_market_structure({ + 'id': id, + 'numericId': None, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': type, + 'spot': isSpot, + 'margin': False, + 'swap': isFuture, + 'future': False, + 'option': False, + 'active': True, + 'contract': isFuture, + 'linear': True if isFuture else None, + 'inverse': False if isFuture else None, + 'taker': self.fees['trading']['taker'], + 'maker': self.fees['trading']['maker'], + 'contractSize': 1 if isFuture else None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': None, # todo find it out + 'price': self.safe_number(market, 'tickSize'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(market, 'minSize'), + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': self.safe_integer(market, 'listedAt'), + 'index': None, + 'info': market, + }) + + def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://docs.ox.fun/?json#get-v3-assets + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = self.publicGetV3Assets(params) + # + # { + # "success": True, + # "data": [ + # { + # "asset": "OX", + # "isCollateral": True, + # "loanToValue": "1.000000000", + # "loanToValueFactor": "0.000000000", + # "networkList": [ + # { + # "network": "BNBSmartChain", + # "tokenId": "0x78a0A62Fba6Fb21A83FE8a3433d44C73a4017A6f", + # "transactionPrecision": "18", + # "isWithdrawalFeeChargedToUser": True, + # "canDeposit": True, + # "canWithdraw": False, + # "minDeposit": "0.00010", + # "minWithdrawal": "0.00010" + # }, + # { + # "network": "Polygon", + # "tokenId": "0x78a0A62Fba6Fb21A83FE8a3433d44C73a4017A6f", + # "transactionPrecision": "18", + # "isWithdrawalFeeChargedToUser": True, + # "canDeposit": True, + # "canWithdraw": False, + # "minDeposit": "0.00010", + # "minWithdrawal": "0.00010" + # }, + # ... + # ] + # }, + # { + # "asset": "BTC", + # "isCollateral": True, + # "loanToValue": "0.950000000", + # "loanToValueFactor": "0.000000000", + # "networkList": [ + # { + # "network": "Bitcoin", + # "transactionPrecision": "8", + # "isWithdrawalFeeChargedToUser": True, + # "canDeposit": True, + # "canWithdraw": True, + # "minDeposit": "0.00010", + # "minWithdrawal": "0.00010" + # } + # ] + # }, + # { + # "asset": "USDT.ARB", + # "isCollateral": True, + # "loanToValue": "0.950000000", + # "loanToValueFactor": "0.000000000", + # "networkList": [ + # { + # "network": "Arbitrum", + # "tokenId": "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", + # "transactionPrecision": "18", + # "isWithdrawalFeeChargedToUser": True, + # "canDeposit": True, + # "canWithdraw": True, + # "minDeposit": "0.00010", + # "minWithdrawal": "0.00010" + # } + # ] + # }, + # ... + # ] + # } + # + data = self.safe_list(response, 'data', []) + result: dict = {} + for i in range(0, len(data)): + currency = data[i] + fullId = self.safe_string(currency, 'asset', '') + parts = fullId.split('.') + id = parts[0] + code = self.safe_currency_code(id) + if not (code in result): + result[code] = { + 'id': id, + 'code': code, + 'precision': None, + 'type': None, + 'name': None, + 'active': None, + 'deposit': None, + 'withdraw': None, + 'fee': None, + 'limits': { + 'withdraw': { + 'min': None, + 'max': None, + }, + 'deposit': { + 'min': None, + 'max': None, + }, + }, + 'networks': {}, + 'info': [], + } + chains = self.safe_list(currency, 'networkList', []) + for j in range(0, len(chains)): + chain = chains[j] + networkId = self.safe_string(chain, 'network') + networkCode = self.network_id_to_code(networkId) + result[code]['networks'][networkCode] = { + 'id': networkId, + 'network': networkCode, + 'margin': None, + 'deposit': self.safe_bool(chain, 'canDeposit'), + 'withdraw': self.safe_bool(chain, 'canWithdraw'), + 'active': None, + 'fee': None, + 'precision': self.parse_number(self.parse_precision(self.safe_string(chain, 'transactionPrecision'))), + 'limits': { + 'deposit': { + 'min': self.safe_number(chain, 'minDeposit'), + 'max': None, + }, + 'withdraw': { + 'min': self.safe_number(chain, 'minWithdrawal'), + 'max': None, + }, + }, + 'info': chain, + } + infos = self.safe_list(result[code], 'info', []) + infos.append(currency) + result[code]['info'] = infos + # only after all entries are formed in currencies, restructure each entry + allKeys = list(result.keys()) + for i in range(0, len(allKeys)): + code = allKeys[i] + result[code] = self.safe_currency_structure(result[code]) # self is needed after adding network entry + return result + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://docs.ox.fun/?json#get-v3-tickers + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + symbols = self.market_symbols(symbols) + response = self.publicGetV3Tickers(params) + # + # { + # "success": True, + # "data": [ + # { + # "marketCode": "NII-USDT", + # "markPrice": "0", + # "open24h": "0", + # "high24h": "0", + # "low24h": "0", + # "volume24h": "0", + # "currencyVolume24h": "0", + # "openInterest": "0", + # "lastTradedPrice": "0", + # "lastTradedQuantity": "0", + # "lastUpdatedAt": "1714853388621" + # }, + # { + # "marketCode": "GEC-USDT", + # "markPrice": "0", + # "open24h": "0", + # "high24h": "0", + # "low24h": "0", + # "volume24h": "0", + # "currencyVolume24h": "0", + # "openInterest": "0", + # "lastTradedPrice": "0", + # "lastTradedQuantity": "0", + # "lastUpdatedAt": "1714853388621" + # }, + # { + # "marketCode": "DYM-USD-SWAP-LIN", + # "markPrice": "3.321", + # "open24h": "3.315", + # "high24h": "3.356", + # "low24h": "3.255", + # "volume24h": "0", + # "currencyVolume24h": "0", + # "openInterest": "1768.1", + # "lastTradedPrice": "3.543", + # "lastTradedQuantity": "1.0", + # "lastUpdatedAt": "1714853388102" + # }, + # ... + # ] + # } + # + tickers = self.safe_list(response, 'data', []) + return self.parse_tickers(tickers, symbols) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://docs.ox.fun/?json#get-v3-tickers + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'marketCode': market['id'], + } + response = self.publicGetV3Tickers(self.extend(request, params)) + # + # { + # "success": True, + # "data": [ + # { + # "marketCode": "BTC-USD-SWAP-LIN", + # "markPrice": "64276", + # "open24h": "63674", + # "high24h": "64607", + # "low24h": "62933", + # "volume24h": "306317655.80000", + # "currencyVolume24h": "48.06810", + # "openInterest": "72.39250", + # "lastTradedPrice": "64300.0", + # "lastTradedQuantity": "1.0", + # "lastUpdatedAt": "1714925196034" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + ticker = self.safe_dict(data, 0, {}) + return self.parse_ticker(ticker, market) + + def parse_ticker(self, ticker, market: Market = None) -> Ticker: + # + # { + # "marketCode": "BTC-USD-SWAP-LIN", + # "markPrice": "64276", + # "open24h": "63674", + # "high24h": "64607", + # "low24h": "62933", + # "volume24h": "306317655.80000", + # "currencyVolume24h": "48.06810", + # "openInterest": "72.39250", + # "lastTradedPrice": "64300.0", + # "lastTradedQuantity": "1.0", + # "lastUpdatedAt": "1714925196034" + # } + # + timestamp = self.safe_integer(ticker, 'lastUpdatedAt') + marketId = self.safe_string(ticker, 'marketCode') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + last = self.safe_string(ticker, 'lastTradedPrice') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high24h'), + 'low': self.safe_string(ticker, 'low24h'), + 'bid': None, + 'bidVolume': None, + 'ask': None, + 'askVolume': None, + 'vwap': None, + 'open': self.safe_string(ticker, 'open24h'), + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': self.safe_string(ticker, 'currencyVolume24h'), + 'quoteVolume': None, # the exchange returns cost in OX + 'markPrice': self.safe_string(ticker, 'markPrice'), + 'info': ticker, + }, market) + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://docs.ox.fun/?json#get-v3-candles + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch(default 24 hours ago) + :param int [limit]: the maximum amount of candles to fetch(default 200, max 500) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest candle to fetch(default now) + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + market = self.market(symbol) + timeframe = self.safe_string(self.timeframes, timeframe, timeframe) + request: dict = { + 'marketCode': market['id'], + 'timeframe': timeframe, + } + if since is not None: + request['startTime'] = since # startTime and endTime must be within 7 days of each other + if limit is not None: + request['limit'] = limit + until = self.safe_integer(params, 'until') + if until is not None: + request['endTime'] = until + params = self.omit(params, 'until') + elif since is not None: + request['endTime'] = self.sum(since, 7 * 24 * 60 * 60 * 1000) # for the exchange not to raise an exception if since is younger than 7 days + response = self.publicGetV3Candles(self.extend(request, params)) + # + # { + # "success": True, + # "timeframe": "3600s", + # "data": [ + # { + # "open": "0.03240000", + # "high": "0.03240000", + # "low": "0.03240000", + # "close": "0.03240000", + # "volume": "0", + # "currencyVolume": "0", + # "openedAt": "1714906800000" + # }, + # { + # "open": "0.03240000", + # "high": "0.03240000", + # "low": "0.03240000", + # "close": "0.03240000", + # "volume": "0", + # "currencyVolume": "0", + # "openedAt": "1714903200000" + # }, + # ... + # ] + # } + # + result = self.safe_list(response, 'data', []) + return self.parse_ohlcvs(result, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # { + # "open": "0.03240000", + # "high": "0.03240000", + # "low": "0.03240000", + # "close": "0.03240000", + # "volume": "0", + # "currencyVolume": "0", + # "openedAt": "1714906800000" + # } + # + return [ + self.safe_integer(ohlcv, 'openedAt'), + self.safe_number(ohlcv, 'open'), + self.safe_number(ohlcv, 'high'), + self.safe_number(ohlcv, 'low'), + self.safe_number(ohlcv, 'close'), + self.safe_number(ohlcv, 'currencyVolume'), + ] + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://docs.ox.fun/?json#get-v3-depth + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return(default 5, max 100) + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'marketCode': market['id'], + } + if limit is not None: + request['level'] = limit + response = self.publicGetV3Depth(self.extend(request, params)) + # + # { + # "success": True, + # "level": "5", + # "data": { + # "marketCode": "BTC-USD-SWAP-LIN", + # "lastUpdatedAt": "1714933499266", + # "asks": [ + # [64073.0, 8.4622], + # [64092.0, 8.1912], + # [64111.0, 8.0669], + # [64130.0, 11.7195], + # [64151.0, 10.1798] + # ], + # "bids": [ + # [64022.0, 10.1292], + # [64003.0, 8.1619], + # [64000.0, 1.0], + # [63984.0, 12.7724], + # [63963.0, 11.0073] + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + timestamp = self.safe_integer(data, 'lastUpdatedAt') + return self.parse_order_book(data, market['symbol'], timestamp) + + def fetch_funding_rates(self, symbols: Strings = None, params={}) -> FundingRates: + """ + fetch the current funding rates for multiple markets + + https://docs.ox.fun/?json#get-v3-funding-estimates + + :param str[] symbols: unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: an array of `funding rate structures ` + """ + self.load_markets() + symbols = self.market_symbols(symbols) + response = self.publicGetV3FundingEstimates(params) + # + # { + # "success": True, + # "data": [ + # { + # "marketCode": "OX-USD-SWAP-LIN", + # "fundingAt": "1715515200000", + # "estFundingRate": "0.000200000" + # }, + # { + # "marketCode": "BTC-USD-SWAP-LIN", + # "fundingAt": "1715515200000", + # "estFundingRate": "0.000003" + # }, + # ... + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_funding_rates(data, symbols) + + def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rates for a symbol + + https://docs.ox.fun/?json#get-v3-funding-estimates + + :param str symbol: unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: an array of `funding rate structures ` + """ + self.load_markets() + request: dict = { + 'marketCode': self.market_id(symbol), + } + response = self.publicGetV3FundingEstimates(self.extend(request, params)) + # + data = self.safe_list(response, 'data', []) + first = self.safe_dict(data, 0, {}) + return self.parse_funding_rate(first, self.market(symbol)) + + def parse_funding_rate(self, fundingRate, market: Market = None) -> FundingRate: + # + # { + # "marketCode": "OX-USD-SWAP-LIN", + # "fundingAt": "1715515200000", + # "estFundingRate": "0.000200000" + # } + # + symbol = self.safe_string(fundingRate, 'marketCode') + market = self.market(symbol) + estFundingRateTimestamp = self.safe_integer(fundingRate, 'fundingAt') + return { + 'info': fundingRate, + 'symbol': market['symbol'], + 'markPrice': None, + 'indexPrice': None, + 'interestRate': self.parse_number('0'), + 'estimatedSettlePrice': None, + 'timestamp': estFundingRateTimestamp, + 'datetime': self.iso8601(estFundingRateTimestamp), + 'fundingRate': self.safe_number(fundingRate, 'estFundingRate'), + 'fundingTimestamp': None, + 'fundingDatetime': None, + 'nextFundingRate': None, + 'nextFundingTimestamp': None, + 'nextFundingDatetime': None, + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': None, + } + + def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + Fetches the history of funding rates + + https://docs.ox.fun/?json#get-v3-funding-rates + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch(default 24 hours ago) + :param int [limit]: the maximum amount of trades to fetch(default 200, max 500) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest trade to fetch(default now) + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'marketCode': market['id'], + } + if since is not None: + request['startTime'] = since # startTime and endTime must be within 7 days of each other + if limit is not None: + request['limit'] = limit + until = self.safe_integer(params, 'until') + if until is not None: + request['endTime'] = until + params = self.omit(params, 'until') + response = self.publicGetV3FundingRates(self.extend(request, params)) + # + # { + # success: True, + # data: [ + # { + # marketCode: 'NEAR-USD-SWAP-LIN', + # fundingRate: '-0.000010000', + # createdAt: '1715428870755' + # }, + # { + # marketCode: 'ENA-USD-SWAP-LIN', + # fundingRate: '0.000150000', + # createdAt: '1715428868616' + # }, + # ... + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_funding_rate_histories(data, market, since, limit) + + def parse_funding_rate_history(self, info, market: Market = None): + # + # { + # success: True, + # data: [ + # { + # marketCode: 'NEAR-USD-SWAP-LIN', + # fundingRate: '-0.000010000', + # createdAt: '1715428870755' + # }, + # { + # marketCode: 'ENA-USD-SWAP-LIN', + # fundingRate: '0.000150000', + # createdAt: '1715428868616' + # }, + # ... + # } + # + marketId = self.safe_string(info, 'marketCode') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + timestamp = self.safe_integer(info, 'createdAt') + return { + 'info': info, + 'symbol': symbol, + 'fundingRate': self.safe_number(info, 'fundingRate'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + + def fetch_funding_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches the history of funding payments + + https://docs.ox.fun/?json#get-v3-funding + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch(default 24 hours ago) + :param int [limit]: the maximum amount of trades to fetch(default 200, max 500) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest trade to fetch(default now) + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'marketCode': market['id'], + } + if since is not None: + request['startTime'] = since # startTime and endTime must be within 7 days of each other + if limit is not None: + request['limit'] = limit + until = self.safe_integer(params, 'until') + if until is not None: + request['endTime'] = until + params = self.omit(params, 'until') + response = self.privateGetV3Funding(self.extend(request, params)) + # + # { + # success: True, + # data: [ + # { + # id: '966709913041305605', + # marketCode: 'ETH-USD-SWAP-LIN', + # payment: '-0.00430822', + # fundingRate: '0.000014', + # position: '0.001', + # indexPrice: '3077.3', + # createdAt: '1715086852890' + # }, + # { + # id: '966698111997509637', + # marketCode: 'ETH-USD-SWAP-LIN', + # payment: '-0.0067419', + # fundingRate: '0.000022', + # position: '0.001', + # indexPrice: '3064.5', + # createdAt: '1715083251516' + # }, + # ... + # ] + # } + # + result = self.safe_list(response, 'data', []) + return self.parse_incomes(result, market, since, limit) + + def parse_income(self, income, market: Market = None): + # + # { + # id: '966709913041305605', + # marketCode: 'ETH-USD-SWAP-LIN', + # payment: '-0.00430822', + # fundingRate: '0.000014', + # position: '0.001', + # indexPrice: '3077.3', + # createdAt: '1715086852890' + # }, + # + marketId = self.safe_string(income, 'marketCode') + symbol = self.safe_symbol(marketId, market) + amount = self.safe_number(income, 'payment') + code = self.safe_currency_code('OX') + id = self.safe_string(income, 'id') + timestamp = self.safe_timestamp(income, 'createdAt') + rate = self.safe_number(income, 'fundingRate') + return { + 'info': income, + 'symbol': symbol, + 'code': code, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': id, + 'amount': amount, + 'rate': rate, + } + + def fetch_leverage_tiers(self, symbols: Strings = None, params={}) -> LeverageTiers: + """ + retrieve information on the maximum leverage, and maintenance margin for trades of varying trade sizes, if a market has a leverage tier of 0, then the leverage tiers cannot be obtained for self market + + https://docs.ox.fun/?json#get-v3-leverage-tiers + + :param str[] [symbols]: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `leverage tiers structures `, indexed by market symbols + """ + self.load_markets() + response = self.publicGetV3LeverageTiers(params) + # + # { + # success: True, + # data: [ + # { + # marketCode: 'SOL-USD-SWAP-LIN', + # tiers: [ + # { + # tier: '1', + # leverage: '10', + # positionFloor: '0', + # positionCap: '200000000', + # initialMargin: '0.1', + # maintenanceMargin: '0.05', + # maintenanceAmount: '0' + # }, + # { + # tier: '2', + # leverage: '5', + # positionFloor: '200000000', + # positionCap: '280000000', + # initialMargin: '0.2', + # maintenanceMargin: '0.1', + # maintenanceAmount: '7000000' + # }, + # { + # tier: '3', + # leverage: '4', + # positionFloor: '280000000', + # positionCap: '460000000', + # initialMargin: '0.25', + # maintenanceMargin: '0.125', + # maintenanceAmount: '14000000' + # }, + # ... + # ] + # }, + # ... + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_leverage_tiers(data, symbols, 'marketCode') + + def parse_market_leverage_tiers(self, info, market: Market = None) -> List[LeverageTier]: + # + # { + # marketCode: 'SOL-USD-SWAP-LIN', + # tiers: [ + # { + # tier: '1', + # leverage: '10', + # positionFloor: '0', + # positionCap: '200000000', + # initialMargin: '0.1', + # maintenanceMargin: '0.05', + # maintenanceAmount: '0' + # ... + # ] + # }, + # + marketId = self.safe_string(info, 'marketCode') + market = self.safe_market(marketId, market) + listOfTiers = self.safe_list(info, 'tiers', []) + tiers = [] + for j in range(0, len(listOfTiers)): + tier = listOfTiers[j] + tiers.append({ + 'tier': self.safe_number(tier, 'tier'), + 'symbol': self.safe_symbol(marketId, market), + 'currency': market['settle'], + 'minNotional': self.safe_number(tier, 'positionFloor'), + 'maxNotional': self.safe_number(tier, 'positionCap'), + 'maintenanceMarginRate': self.safe_number(tier, 'maintenanceMargin'), + 'maxLeverage': self.safe_number(tier, 'leverage'), + 'info': tier, + }) + return tiers + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://docs.ox.fun/?json#get-v3-exchange-trades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch(default 24 hours ago) + :param int [limit]: the maximum amount of trades to fetch(default 200, max 500) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest trade to fetch(default now) + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'marketCode': market['id'], + } + if since is not None: + request['startTime'] = since # startTime and endTime must be within 7 days of each other + if limit is not None: + request['limit'] = limit + until = self.safe_integer(params, 'until') + if until is not None: + request['endTime'] = until + params = self.omit(params, 'until') + elif since is not None: + request['endTime'] = self.sum(since, 7 * 24 * 60 * 60 * 1000) # for the exchange not to raise an exception if since is younger than 7 days + response = self.publicGetV3ExchangeTrades(self.extend(request, params)) + # + # { + # "success": True, + # "data": [ + # { + # "marketCode": "BTC-USD-SWAP-LIN", + # "matchPrice": "63900", + # "matchQuantity": "1", + # "side": "SELL", + # "matchType": "TAKER", + # "matchedAt": "1714934112352" + # }, + # ... + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_trades(data, market, since, limit) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://docs.ox.fun/?json#get-v3-trades + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum amount of trades to fetch(default 200, max 500) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest trade to fetch(default now) + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + request: dict = {} + market: Market = None + if symbol is not None: + market = self.market(symbol) + request['marketCode'] = market['id'] + if since is not None: # startTime and endTime must be within 7 days of each other + request['startTime'] = since + if limit is not None: + request['limit'] = limit + until = self.safe_integer(params, 'until') + if until is not None: + request['endTime'] = until + params = self.omit(params, 'until') + elif since is not None: + request['endTime'] = self.sum(since, 7 * 24 * 60 * 60 * 1000) # for the exchange not to raise an exception if since is younger than 7 days + response = self.privateGetV3Trades(self.extend(request, params)) + # + # { + # "success": True, + # "data": [ + # { + # "orderId": "1000104903698", + # "clientOrderId": "1715000260094", + # "matchId": "400017129522773178", + # "marketCode": "ETH-USD-SWAP-LIN", + # "side": "BUY", + # "matchedQuantity": "0.001", + # "matchPrice": "3100.2", + # "total": "310.02", + # "orderMatchType": "MAKER", + # "feeAsset": "OX", + # "fee": "0.062004", + # "source": "0", + # "matchedAt": "1715000267420" + # } + # ] + # } + # + result = self.safe_list(response, 'data', []) + return self.parse_trades(result, market, since, limit) + + def parse_trade(self, trade, market: Market = None) -> Trade: + # + # public fetchTrades + # + # { + # "marketCode": "BTC-USD-SWAP-LIN", + # "matchPrice": "63900", + # "matchQuantity": "1", + # "side": "SELL", + # "matchType": "TAKER", + # "matchedAt": "1714934112352" + # } + # + # + # private fetchMyTrades + # + # { + # "orderId": "1000104903698", + # "clientOrderId": "1715000260094", + # "matchId": "400017129522773178", + # "marketCode": "ETH-USD-SWAP-LIN", + # "side": "BUY", + # "matchedQuantity": "0.001", + # "matchPrice": "3100.2", + # "total": "310.02", + # "orderMatchType": "MAKER", + # "feeAsset": "OX", + # "fee": "0.062004", + # "source": "0", + # "matchedAt": "1715000267420" + # } + # + marketId = self.safe_string(trade, 'marketCode') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + timestamp = self.safe_integer(trade, 'matchedAt') + fee = { + 'cost': self.safe_string(trade, 'fee'), + 'currency': self.safe_currency_code(self.safe_string(trade, 'feeAsset')), + } + return self.safe_trade({ + 'id': self.safe_string(trade, 'matchId'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'type': None, + 'order': self.safe_string(trade, 'orderId'), + 'side': self.safe_string_lower(trade, 'side'), + 'takerOrMaker': self.safe_string_lower_2(trade, 'matchType', 'orderMatchType'), + 'price': self.safe_string(trade, 'matchPrice'), + 'amount': self.safe_string_2(trade, 'matchQuantity', 'matchedQuantity'), + 'cost': None, # the exchange returns total cost in OX + 'fee': fee, + 'info': trade, + }, market) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://docs.ox.fun/?json#get-v3-balances + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.asset]: currency id, if empty the exchange returns info about all currencies + :param str [params.subAcc]: Name of sub account. If no subAcc is given, then the response contains only the account linked to the API-Key. + :returns dict: a `balance structure ` + """ + self.load_markets() + response = self.privateGetV3Balances(params) + # + # { + # "success": True, + # "data": [ + # { + # "accountId": "106490", + # "name": "main", + # "balances": [ + # { + # "asset": "OX", + # "total": "-7.55145065000", + # "available": "-71.16445065000", + # "reserved": "0", + # "lastUpdatedAt": "1715000448946" + # }, + # { + # "asset": "ETH", + # "total": "0.01", + # "available": "0.01", + # "reserved": "0", + # "lastUpdatedAt": "1714914512750" + # }, + # ... + # ] + # }, + # ... + # ] + # } + # + data = self.safe_list(response, 'data', []) + balance = data[0] + subAcc = self.safe_string(params, 'subAcc') + if subAcc is not None: + for i in range(0, len(data)): + b = data[i] + name = self.safe_string(b, 'name') + if name == subAcc: + balance = b + break + return self.parse_balance(balance) + + def parse_balance(self, balance) -> Balances: + # + # { + # "accountId": "106490", + # "name": "main", + # "balances": [ + # { + # "asset": "OX", + # "total": "-7.55145065000", + # "available": "-71.16445065000", + # "reserved": "0", + # "lastUpdatedAt": "1715000448946" + # }, + # { + # "asset": "ETH", + # "total": "0.01", + # "available": "0.01", + # "reserved": "0", + # "lastUpdatedAt": "1714914512750" + # }, + # ... + # ] + # } + # + result: dict = { + 'info': balance, + } + balances = self.safe_list(balance, 'balances', []) + for i in range(0, len(balances)): + balanceEntry = balances[i] + currencyId = self.safe_string(balanceEntry, 'asset') + code = self.safe_currency_code(currencyId) + account = self.account() + account['total'] = self.safe_string(balanceEntry, 'total') + account['free'] = self.safe_string(balanceEntry, 'available') + account['used'] = self.safe_string(balanceEntry, 'reserved') + result[code] = account + return self.safe_balance(result) + + def fetch_accounts(self, params={}) -> List[Account]: + """ + fetch subaccounts associated with a profile + + https://docs.ox.fun/?json#get-v3-account-names + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `account structures ` indexed by the account type + """ + self.load_markets() + # self endpoint can only be called using API keys paired with the parent account! Returns all active subaccounts. + response = self.privateGetV3AccountNames(params) + # + # { + # "success": True, + # "data": [ + # { + # "accountId": "106526", + # "name": "testSubAccount" + # }, + # ... + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_accounts(data, params) + + def parse_account(self, account): + # + # { + # "accountId": "106526", + # "name": "testSubAccount" + # }, + # + return { + 'id': self.safe_string(account, 'accountId'), + 'type': None, + 'code': None, + 'info': account, + } + + def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://docs.ox.fun/?json#post-v3-transfer + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account id to transfer from + :param str toAccount: account id to transfer to + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transfer structure ` + """ + # transferring funds between sub-accounts is restricted to API keys linked to the parent account. + self.load_markets() + currency = self.currency(code) + request: dict = { + 'asset': currency['id'], + 'quantity': self.currency_to_precision(code, amount), + 'fromAccount': fromAccount, + 'toAccount': toAccount, + } + response = self.privatePostV3Transfer(self.extend(request, params)) + # + # { + # timestamp: 1715430036267, + # datetime: '2024-05-11T12:20:36.267Z', + # currency: 'OX', + # amount: 10, + # fromAccount: '106464', + # toAccount: '106570', + # info: { + # asset: 'OX', + # quantity: '10', + # fromAccount: '106464', + # toAccount: '106570', + # transferredAt: '1715430036267' + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_transfer(data, currency) + + def fetch_transfers(self, code: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch a history of internal transfers made on an account + + https://docs.ox.fun/?json#get-v3-transfer + + :param str code: unified currency code of the currency transferred + :param int [since]: the earliest time in ms to fetch transfers for(default 24 hours ago) + :param int [limit]: the maximum number of transfer structures to retrieve(default 50, max 200) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch transfers for(default time now) + :returns dict[]: a list of `transfer structures ` + """ + # API keys linked to the parent account can get all account transfers, while API keys linked to a sub-account can only see transfers where the sub-account is either the "fromAccount" or "toAccount" + self.load_markets() + request: dict = {} + currency: Currency = None + if code is not None: + currency = self.currency(code) + request['asset'] = currency['id'] + if since is not None: + request['startTime'] = since # startTime and endTime must be within 7 days of each other + if limit is not None: + request['limit'] = limit + until = self.safe_integer(params, 'until') + if until is not None: + request['endTime'] = until + params = self.omit(params, 'until') + elif since is not None: + request['endTime'] = self.sum(since, 7 * 24 * 60 * 60 * 1000) # for the exchange not to raise an exception if since is younger than 7 days + response = self.privateGetV3Transfer(self.extend(request, params)) + # + # { + # "success": True, + # "data": [ + # { + # "asset": "USDT", + # "quantity": "5", + # "fromAccount": "106490", + # "toAccount": "106526", + # "id": "966706320886267905", + # "status": "COMPLETED", + # "transferredAt": "1715085756708" + # }, + # ... + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_transfers(data, currency, since, limit) + + def parse_transfer(self, transfer, currency: Currency = None): + # + # fetchTransfers + # + # { + # "asset": "USDT", + # "quantity": "5", + # "fromAccount": "106490", + # "toAccount": "106526", + # "id": "966706320886267905", + # "status": "COMPLETED", + # "transferredAt": "1715085756708" + # } + # + timestamp = self.safe_integer(transfer, 'transferredAt') + currencyId = self.safe_string(transfer, 'asset') + return { + 'id': self.safe_string(transfer, 'id'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'currency': self.safe_currency_code(currencyId, currency), + 'amount': self.safe_number(transfer, 'quantity'), + 'fromAccount': self.safe_string(transfer, 'fromAccount'), + 'toAccount': self.safe_string(transfer, 'toAccount'), + 'status': self.parse_transfer_status(self.safe_string(transfer, 'status')), + 'info': transfer, + } + + def parse_transfer_status(self, status): + statuses: dict = { + 'COMPLETED': 'ok', + } + return self.safe_string(statuses, status, status) + + def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://docs.ox.fun/?json#get-v3-deposit-addresses + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.network]: network for fetch deposit address + :returns dict: an `address structure ` + """ + networkCode = self.safe_string(params, 'network') + networkId = self.network_code_to_id(networkCode, code) + if networkId is None: + raise BadRequest(self.id + ' fetchDepositAddress() require network parameter') + self.load_markets() + currency = self.currency(code) + request: dict = { + 'asset': currency['id'], + 'network': networkId, + } + params = self.omit(params, 'network') + response = self.privateGetV3DepositAddresses(self.extend(request, params)) + # + # {"success":true,"data":{"address":"0x998dEc76151FB723963Bd8AFD517687b38D33dE8"}} + # + data = self.safe_dict(response, 'data', {}) + return self.parse_deposit_address(data, currency) + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + # + # {"address":"0x998dEc76151FB723963Bd8AFD517687b38D33dE8"} + # + address = self.safe_string(depositAddress, 'address') + self.check_address(address) + return { + 'info': depositAddress, + 'currency': currency['code'], + 'network': None, + 'address': address, + 'tag': None, + } + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://docs.ox.fun/?json#get-v3-deposit + + :param str code: unified currency code of the currency transferred + :param int [since]: the earliest time in ms to fetch transfers for(default 24 hours ago) + :param int [limit]: the maximum number of transfer structures to retrieve(default 50, max 200) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch transfers for(default time now) + :returns dict[]: a list of `transfer structures ` + """ + self.load_markets() + request: dict = {} + currency: Currency = None + if code is not None: + currency = self.currency(code) + request['asset'] = currency['id'] + if since is not None: + request['startTime'] = since # startTime and endTime must be within 7 days of each other + if limit is not None: + request['limit'] = limit + until = self.safe_integer(params, 'until') + if until is not None: + request['endTime'] = until + params = self.omit(params, 'until') + response = self.privateGetV3Deposit(self.extend(request, params)) + # + # { + # "success": True, + # "data": [ + # { + # "asset":"USDC", + # "network":"Ethereum", + # "address": "0x998dEc76151FB723963Bd8AFD517687b38D33dE8", + # "quantity":"50", + # "id":"5914", + # "status": "COMPLETED", + # "txId":"0xf5e79663830a0c6f94d46638dcfbc134566c12facf1832396f81ecb55d3c75dc", + # "creditedAt":"1714821645154" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + for i in range(0, len(data)): + data[i]['type'] = 'deposit' + return self.parse_transactions(data, currency, since, limit) + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://docs.ox.fun/?json#get-v3-withdrawal + + :param str code: unified currency code of the currency transferred + :param int [since]: the earliest time in ms to fetch transfers for(default 24 hours ago) + :param int [limit]: the maximum number of transfer structures to retrieve(default 50, max 200) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch transfers for(default time now) + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + request: dict = {} + currency: Currency = None + if code is not None: + currency = self.currency(code) + request['asset'] = currency['id'] + if since is not None: + request['startTime'] = since # startTime and endTime must be within 7 days of each other + if limit is not None: + request['limit'] = limit + until = self.safe_integer(params, 'until') + if until is not None: + request['endTime'] = until + params = self.omit(params, 'until') + response = self.privateGetV3Withdrawal(self.extend(request, params)) + # + # { + # success: True, + # data: [ + # { + # id: '968163212989431811', + # asset: 'OX', + # network: 'Arbitrum', + # address: '0x90fc1fB49a4ED8f485dd02A2a1Cf576897f6Bfc9', + # quantity: '11.7444', + # fee: '1.744400000', + # status: 'COMPLETED', + # txId: '0xe96b2d128b737fdbca927edf355cff42202e65b0fb960e64ffb9bd68c121f69f', + # requestedAt: '1715530365450', + # completedAt: '1715530527000' + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + for i in range(0, len(data)): + data[i]['type'] = 'withdrawal' + return self.parse_transactions(data, currency, since, limit) + + def parse_transactions(self, transactions, currency: Currency = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + result = [] + for i in range(0, len(transactions)): + transactions[i] = self.extend(transactions[i], params) + transaction = self.parse_transaction(transactions[i], currency) + result.append(transaction) + result = self.sort_by(result, 'timestamp') + code = currency['code'] if (currency is not None) else None + return self.filter_by_currency_since_limit(result, code, since, limit) + + def parse_transaction(self, transaction, currency: Currency = None) -> Transaction: + # + # fetchDeposits + # { + # "asset":"USDC", + # "network":"Ethereum", + # "address": "0x998dEc76151FB723963Bd8AFD517687b38D33dE8", + # "quantity":"50", + # "id":"5914", + # "status": "COMPLETED", + # "txId":"0xf5e79663830a0c6f94d46638dcfbc134566c12facf1832396f81ecb55d3c75dc", + # "creditedAt":"1714821645154" + # } + # + # fetchWithdrawals + # { + # id: '968163212989431811', + # asset: 'OX', + # network: 'Arbitrum', + # address: '0x90fc1fB49a4ED8f485dd02A2a1Cf576897f6Bfc9', + # quantity: '11.7444', + # fee: '1.744400000', + # status: 'COMPLETED', + # txId: '0xe96b2d128b737fdbca927edf355cff42202e65b0fb960e64ffb9bd68c121f69f', + # requestedAt: '1715530365450', + # completedAt: '1715530527000' + # } + # + # withdraw + # { + # "id": "968364664449302529", + # "asset": "OX", + # "network": "Arbitrum", + # "address": "0x90fc1fB49a4ED8f485dd02A2a1Cf576897f6Bfc9", + # "quantity": "10", + # "externalFee": False, + # "fee": "1.6728", + # "status": "PENDING", + # "requestedAt": "1715591843616" + # } + # + id = self.safe_string(transaction, 'id') + type = self.safe_string(transaction, 'type') + transaction = self.omit(transaction, 'type') + address: Str = None + addressTo: Str = None + status: Str = None + if type == 'deposit': + address = self.safe_string(transaction, 'address') + status = self.parse_deposit_status(self.safe_string(transaction, 'status')) + elif type == 'withdrawal': + addressTo = self.safe_string(transaction, 'address') + status = self.parse_withdrawal_status(self.safe_string(transaction, 'status')) + txid = self.safe_string(transaction, 'txId') + currencyId = self.safe_string(transaction, 'asset') + code = self.safe_currency_code(currencyId, currency) + network = self.safe_string(transaction, 'network') + networkCode = self.network_id_to_code(network) + timestamp = self.safe_integer_2(transaction, 'creditedAt', 'requestedAt') + amount = self.safe_number(transaction, 'quantity') + feeCost = self.safe_number(transaction, 'fee') + fee = None + if feeCost is not None: + fee = { + 'cost': feeCost, + 'currency': code, + } + return { + 'info': transaction, + 'id': id, + 'txid': txid, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': networkCode, + 'address': address, + 'addressTo': addressTo, + 'addressFrom': None, + 'tag': None, + 'tagTo': None, + 'tagFrom': None, + 'type': type, + 'amount': amount, + 'currency': code, + 'status': status, + 'updated': None, + 'internal': None, + 'comment': None, + 'fee': fee, + } + + def parse_deposit_status(self, status): + statuses: dict = { + 'COMPLETED': 'ok', + } + return self.safe_string(statuses, status, status) + + def parse_withdrawal_status(self, status): + statuses: dict = { + 'COMPLETED': 'ok', + 'PROCESSING': 'pending', + 'IN SWEEPING': 'pending', + 'PENDING': 'pending', + 'ON HOLD': 'pending', + 'CANCELED': 'canceled', + 'FAILED': 'failed', + } + return self.safe_string(statuses, status, status) + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://docs.ox.fun/?json#post-v3-withdrawal + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.network]: network for withdraw + :param bool [params.externalFee]: if False, then the fee is taken from the quantity, also with the burn fee for asset SOLO + + EXCHANGE SPECIFIC PARAMETERS + :param str [params.tfaType]: GOOGLE, or AUTHY_SECRET, or YUBIKEY, for 2FA + :param str [params.code]: 2FA code + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.load_markets() + currency = self.currency(code) + stringAmount = self.currency_to_precision(code, amount) + request: dict = { + 'asset': currency['id'], + 'address': address, + 'quantity': stringAmount, + } + if tag is not None: + request['memo'] = tag + networkCode: Str = None + networkCode, params = self.handle_network_code_and_params(params) + if networkCode is not None: + request['network'] = self.network_code_to_id(networkCode) + request['externalFee'] = False + response = self.privatePostV3Withdrawal(self.extend(request, params)) + # + # { + # "success": True, + # "data": { + # "id": "968364664449302529", + # "asset": "OX", + # "network": "Arbitrum", + # "address": "0x90fc1fB49a4ED8f485dd02A2a1Cf576897f6Bfc9", + # "quantity": "10", + # "externalFee": False, + # "fee": "1.6728", + # "status": "PENDING", + # "requestedAt": "1715591843616" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + data['type'] = 'withdrawal' + return self.parse_transaction(data, currency) + + def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://docs.ox.fun/?json#get-v3-positions + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.subAcc]: + :returns dict[]: a list of `position structure ` + """ + # Calling self endpoint using an API key pair linked to the parent account with the parameter "subAcc" + # allows the caller to include positions of additional sub-accounts in the response. + # This feature does not work when using API key pairs linked to a sub-account + self.load_markets() + symbols = self.market_symbols(symbols) + response = self.privateGetV3Positions(params) + # + # { + # "success": True, + # "data": [ + # { + # "accountId": "106490", + # "name": "main", + # "positions": [ + # { + # "marketCode": "BTC-USD-SWAP-LIN", + # "baseAsset": "BTC", + # "counterAsset": "USD", + # "position": "0.00010", + # "entryPrice": "64300.0", + # "markPrice": "63278", + # "positionPnl": "-10.1900", + # "estLiquidationPrice": "0", + # "lastUpdatedAt": "1714915841448" + # }, + # ... + # ] + # }, + # { + # "accountId": "106526", + # "name": "testSubAccount", + # "positions": [ + # { + # "marketCode": "ETH-USD-SWAP-LIN", + # "baseAsset": "ETH", + # "counterAsset": "USD", + # "position": "0.001", + # "entryPrice": "3080.5", + # "markPrice": "3062.0", + # "positionPnl": "-1.8500", + # "estLiquidationPrice": "0", + # "lastUpdatedAt": "1715089678013" + # }, + # ... + # ] + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + allPositions = [] + for i in range(0, len(data)): + account = data[i] + positions = self.safe_list(account, 'positions', []) + allPositions = self.array_concat(allPositions, positions) + return self.parse_positions(allPositions, symbols) + + def parse_position(self, position, market: Market = None): + # + # { + # "marketCode": "ETH-USD-SWAP-LIN", + # "baseAsset": "ETH", + # "counterAsset": "USD", + # "position": "0.001", + # "entryPrice": "3080.5", + # "markPrice": "3062.0", + # "positionPnl": "-1.8500", + # "estLiquidationPrice": "0", + # "lastUpdatedAt": "1715089678013" + # } + # + marketId = self.safe_string(position, 'marketCode') + market = self.safe_market(marketId, market) + return self.safe_position({ + 'info': position, + 'id': None, + 'symbol': market['symbol'], + 'notional': None, + 'marginMode': 'cross', + 'liquidationPrice': self.safe_number(position, 'estLiquidationPrice'), + 'entryPrice': self.safe_number(position, 'entryPrice'), + 'unrealizedPnl': self.safe_number(position, 'positionPnl'), + 'realizedPnl': None, + 'percentage': None, + 'contracts': self.safe_number(position, 'position'), + 'contractSize': None, + 'markPrice': self.safe_number(position, 'markPrice'), + 'lastPrice': None, + 'side': None, + 'hedged': None, + 'timestamp': None, + 'datetime': None, + 'lastUpdateTimestamp': self.safe_integer(position, 'lastUpdatedAt'), + 'maintenanceMargin': None, + 'maintenanceMarginPercentage': None, + 'collateral': None, + 'initialMargin': None, + 'initialMarginPercentage': None, + 'leverage': None, + 'marginRatio': None, + 'stopLossPrice': None, + 'takeProfitPrice': None, + }) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}) -> Order: + """ + create a trade order + + https://docs.ox.fun/?json#post-v3-orders-place + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market', 'limit', 'STOP_LIMIT' or 'STOP_MARKET' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.clientOrderId]: a unique id for the order + :param int [params.timestamp]: in milliseconds. If an order reaches the matching engine and the current timestamp exceeds timestamp + recvWindow, then the order will be rejected. + :param int [params.recvWindow]: in milliseconds. If an order reaches the matching engine and the current timestamp exceeds timestamp + recvWindow, then the order will be rejected. If timestamp is provided without recvWindow, then a default recvWindow of 1000ms is used. + :param str [params.responseType]: FULL or ACK + :param float [params.cost]: the quote quantity that can be used alternative for the amount for market buy orders + :param float [params.triggerPrice]: The price at which a trigger order is triggered at + :param float [params.limitPrice]: Limit price for the STOP_LIMIT order + :param bool [params.postOnly]: if True, the order will only be posted if it will be a maker order + :param str [params.timeInForce]: GTC(default), IOC, FOK, PO, MAKER_ONLY or MAKER_ONLY_REPRICE(reprices order to the best maker only price if the specified price were to lead to a taker trade) + :param str [params.selfTradePrevention]: NONE, EXPIRE_MAKER, EXPIRE_TAKER or EXPIRE_BOTH for more info check here {@link https://docs.ox.fun/?json#self-trade-prevention-modes} + :param str [params.displayQuantity]: for an iceberg order, pass both quantity and displayQuantity fields in the order request + :returns dict: an `order structure ` + """ + self.load_markets() + request: dict = { + 'responseType': self.safe_string(params, 'responseType', 'FULL'), + 'timestamp': self.safe_integer(params, 'timestamp', self.milliseconds()), + } + params = self.omit(params, ['responseType', 'timestamp']) + recvWindow = self.safe_integer(params, 'recvWindow') + if recvWindow is not None: + request['recvWindow'] = recvWindow + params = self.omit(params, 'recvWindow') + orderRequest = self.create_order_request(symbol, type, side, amount, price, params) + request['orders'] = [orderRequest] + response = self.privatePostV3OrdersPlace(request) + # + # accepted market order responseType FULL + # { + # "success": True, + # "data": [ + # { + # "notice": "OrderMatched", + # "accountId": "106490", + # "orderId": "1000109901865", + # "submitted": True, + # "clientOrderId": "0", + # "marketCode": "OX-USDT", + # "status": "FILLED", + # "side": "SELL", + # "isTriggered": False, + # "quantity": "150.0", + # "amount": "0.0", + # "remainQuantity": "0.0", + # "matchId": "100017047880451399", + # "matchPrice": "0.01465", + # "matchQuantity": "150.0", + # "feeInstrumentId": "USDT", + # "fees": "0.0015382500", + # "orderType": "MARKET", + # "createdAt": "1715592472236", + # "lastMatchedAt": "1715592472200", + # "displayQuantity": "150.0" + # } + # ] + # } + # + # accepted limit order responseType FULL + # { + # "success": True, + # "data": [ + # { + # "notice": "OrderOpened", + # "accountId": "106490", + # "orderId": "1000111482406", + # "submitted": True, + # "clientOrderId": "0", + # "marketCode": "ETH-USD-SWAP-LIN", + # "status": "OPEN", + # "side": "SELL", + # "price": "4000.0", + # "isTriggered": False, + # "quantity": "0.01", + # "amount": "0.0", + # "orderType": "LIMIT", + # "timeInForce": "GTC", + # "createdAt": "1715763507682", + # "displayQuantity": "0.01" + # } + # ] + # } + # + # accepted order responseType ACK + # { + # "success": True, + # "data": [ + # { + # "accountId": "106490", + # "orderId": "1000109892193", + # "submitted": True, + # "marketCode": "OX-USDT", + # "side": "BUY", + # "price": "0.01961", + # "isTriggered": False, + # "quantity": "100", + # "orderType": "MARKET", + # "timeInForce": "IOC", + # "createdAt": "1715591529057", + # "selfTradePreventionMode": "NONE" + # } + # ] + # } + # + # rejected order(balance insufficient) + # { + # "success": True, + # "data": [ + # { + # "code": "710001", + # "message": "System failure, exception thrown -> null", + # "submitted": False, + # "marketCode": "OX-USDT", + # "side": "BUY", + # "price": "0.01961", + # "amount": "100", + # "orderType": "MARKET", + # "timeInForce": "IOC", + # "createdAt": "1715591678835", + # "source": 11, + # "selfTradePreventionMode": "NONE" + # } + # ] + # } + # + # rejected order(bad request) + # { + # "success": True, + # "data": [ + # { + # "code": "20044", + # "message": "Amount is not supported for self order type", + # "submitted": False, + # "marketCode": "OX-USDT", + # "side": "SELL", + # "amount": "200", + # "orderType": "MARKET", + # "createdAt": "1715592079986", + # "source": 11 + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + order = self.safe_dict(data, 0, {}) + return self.parse_order(order) + + def create_orders(self, orders: List[OrderRequest], params={}) -> List[Order]: + """ + create a list of trade orders + + https://docs.ox.fun/?json#post-v3-orders-place + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.timestamp]: *for all orders* in milliseconds. If orders reach the matching engine and the current timestamp exceeds timestamp + recvWindow, then all orders will be rejected. + :param int [params.recvWindow]: *for all orders* in milliseconds. If orders reach the matching engine and the current timestamp exceeds timestamp + recvWindow, then all orders will be rejected. If timestamp is provided without recvWindow, then a default recvWindow of 1000ms is used. + :param str [params.responseType]: *for all orders* FULL or ACK + :returns dict: an `order structure ` + """ + self.load_markets() + ordersRequests = [] + for i in range(0, len(orders)): + rawOrder = orders[i] + symbol = self.safe_string(rawOrder, 'symbol') + type = self.safe_string(rawOrder, 'type') + side = self.safe_string(rawOrder, 'side') + amount = self.safe_number(rawOrder, 'amount') + price = self.safe_number(rawOrder, 'price') + orderParams = self.safe_dict(rawOrder, 'params', {}) + orderRequest = self.create_order_request(symbol, type, side, amount, price, orderParams) + ordersRequests.append(orderRequest) + request: dict = { + 'responseType': 'FULL', + 'timestamp': self.milliseconds(), + 'orders': ordersRequests, + } + response = self.privatePostV3OrdersPlace(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + return self.parse_orders(data) + + def create_order_request(self, symbol: str, type: str, side: str, amount, price=None, params={}): + """ + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market', 'limit', 'STOP_LIMIT' or 'STOP_MARKET' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.clientOrderId]: a unique id for the order + :param float [params.cost]: the quote quantity that can be used alternative for the amount for market buy orders + :param float [params.triggerPrice]: The price at which a trigger order is triggered at + :param float [params.limitPrice]: Limit price for the STOP_LIMIT order + :param bool [params.postOnly]: if True, the order will only be posted if it will be a maker order + :param str [params.timeInForce]: GTC(default), IOC, FOK, PO, MAKER_ONLY or MAKER_ONLY_REPRICE(reprices order to the best maker only price if the specified price were to lead to a taker trade) + :param str [params.selfTradePrevention]: NONE, EXPIRE_MAKER, EXPIRE_TAKER or EXPIRE_BOTH for more info check here {@link https://docs.ox.fun/?json#self-trade-prevention-modes} + :param str [params.displayQuantity]: for an iceberg order, pass both quantity and displayQuantity fields in the order request + """ + market = self.market(symbol) + request: dict = { + 'marketCode': market['id'], + 'side': side.upper(), + 'source': 1000, + } + cost = self.safe_string_2(params, 'cost', 'amount') + if cost is not None: + request['amount'] = cost # todo costToPrecision + params = self.omit(params, ['cost', 'amount']) + else: + request['quantity'] = amount # todo amountToPrecision + triggerPrice = self.safe_string_2(params, 'triggerPrice', 'stopPrice') + orderType = type.upper() + if triggerPrice is not None: + if orderType == 'MARKET': + orderType = 'STOP_MARKET' + elif orderType == 'LIMIT': + orderType = 'STOP_LIMIT' + request['stopPrice'] = triggerPrice # todo priceToPrecision + params = self.omit(params, ['triggerPrice', 'stopPrice']) + request['orderType'] = orderType + if orderType == 'STOP_LIMIT': + request['limitPrice'] = price # todo priceToPrecision + elif price is not None: + request['price'] = price # todo priceToPrecision + postOnly: Bool = None + isMarketOrder = (orderType == 'MARKET') or (orderType == 'STOP_MARKET') + postOnly, params = self.handle_post_only(isMarketOrder, False, params) + timeInForce = self.safe_string_upper(params, 'timeInForce') + if postOnly and (timeInForce != 'MAKER_ONLY_REPRICE'): + request['timeInForce'] = 'MAKER_ONLY' + selfTradePrevention = None + selfTradePrevention, params = self.handle_option_and_params(params, 'createOrder', 'selfTradePrevention') + if selfTradePrevention is not None: + request['selfTradePreventionMode'] = selfTradePrevention.upper() + return self.extend(request, params) + + def create_market_buy_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market buy order by providing the symbol and cost + + https://open.big.one/docs/spot_orders.html#create-order + + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' createMarketBuyOrderWithCost() supports spot orders only') + request: dict = { + 'cost': cost, + } + return self.create_order(symbol, 'market', 'buy', None, None, self.extend(request, params)) + + def fetch_order(self, id: str, symbol: Str = None, params={}) -> Order: + """ + + https://docs.ox.fun/?json#get-v3-orders-status + + fetches information on an order made by the user + :param str id: a unique id for the order + :param str [symbol]: not used by oxfun fetchOrder + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.clientOrderId]: the client order id of the order + :returns dict: An `order structure ` + """ + self.load_markets() + request: dict = { + 'orderId': id, + } + response = self.privateGetV3OrdersStatus(self.extend(request, params)) + # + # { + # "success": True, + # "data": { + # "orderId": "1000111762980", + # "clientOrderId": "0", + # "marketCode": "ETH-USD-SWAP-LIN", + # "status": "OPEN", + # "side": "BUY", + # "price": "2700.0", + # "isTriggered": False, + # "remainQuantity": "0.01", + # "totalQuantity": "0.01", + # "amount": "0", + # "displayQuantity": "0.01", + # "cumulativeMatchedQuantity": "0", + # "orderType": "STOP_LIMIT", + # "timeInForce": "GTC", + # "source": "11", + # "createdAt": "1715794191277" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://docs.ox.fun/?json#get-v3-orders-working + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.orderId]: a unique id for the order + :param int [params.clientOrderId]: the client order id of the order + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + request: dict = {} + market: Market = None + if symbol is not None: + market = self.market(symbol) + response = self.privateGetV3OrdersWorking(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + return self.parse_orders(data, market, since, limit) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://docs.ox.fun/?json#delete-v3-orders-cancel + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.clientOrderId]: a unique id for the order + :param int [params.timestamp]: in milliseconds + :param int [params.recvWindow]: in milliseconds + :param str [params.responseType]: 'FULL' or 'ACK' + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + market = self.market(symbol) + marketId = market['id'] + request: dict = { + 'timestamp': self.milliseconds(), + 'responseType': 'FULL', + } + orderRequest = { + 'marketCode': marketId, + 'orderId': id, + } + clientOrderId = self.safe_integer(params, 'clientOrderId') + if clientOrderId is not None: + orderRequest['clientOrderId'] = clientOrderId + request['orders'] = [orderRequest] + response = self.privateDeleteV3OrdersCancel(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + order = self.safe_dict(data, 0, {}) + return self.parse_order(order) + + def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders + + https://docs.ox.fun/?json#delete-v3-orders-cancel-all + + :param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from exchange + """ + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['marketCode'] = market['id'] + # + # { + # "success": True, + # "data": {"notice": "Orders queued for cancelation"} + # } + # + # { + # "success": True, + # "data": {"notice": "No working orders found"} + # } + # + response = self.privateDeleteV3OrdersCancelAll(self.extend(request, params)) + return [self.safe_order({'info': response})] + + def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): + """ + cancel multiple orders + + https://docs.ox.fun/?json#delete-v3-orders-cancel + + :param str[] ids: order ids + :param str [symbol]: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.timestamp]: in milliseconds + :param int [params.recvWindow]: in milliseconds + :param str [params.responseType]: 'FULL' or 'ACK' + :returns dict: an list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrders() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + marketId = market['id'] + request: dict = { + 'timestamp': self.milliseconds(), + 'responseType': 'FULL', + } + orders = [] + for i in range(0, len(ids)): + order = { + 'marketCode': marketId, + 'orderId': ids[i], + } + orders.append(order) + request['orders'] = orders + response = self.privateDeleteV3OrdersCancel(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + return self.parse_orders(data, market) + + def parse_order(self, order, market: Market = None) -> Order: + # + # accepted market order responseType FULL + # { + # "notice": "OrderMatched", + # "accountId": "106490", + # "orderId": "1000109901865", + # "submitted": True, + # "clientOrderId": "0", + # "marketCode": "OX-USDT", + # "status": "FILLED", + # "side": "SELL", + # "isTriggered": False, + # "quantity": "150.0", + # "amount": "0.0", + # "remainQuantity": "0.0", + # "matchId": "100017047880451399", + # "matchPrice": "0.01465", + # "matchQuantity": "150.0", + # "feeInstrumentId": "USDT", + # "fees": "0.0015382500", + # "orderType": "MARKET", + # "createdAt": "1715592472236", + # "lastMatchedAt": "1715592472200", + # "displayQuantity": "150.0" + # } + # + # accepted limit order responseType FULL + # { + # "notice": "OrderOpened", + # "accountId": "106490", + # "orderId": "1000111482406", + # "submitted": True, + # "clientOrderId": "0", + # "marketCode": "ETH-USD-SWAP-LIN", + # "status": "OPEN", + # "side": "SELL", + # "price": "4000.0", + # "isTriggered": False, + # "quantity": "0.01", + # "amount": "0.0", + # "orderType": "LIMIT", + # "timeInForce": "GTC", + # "createdAt": "1715763507682", + # "displayQuantity": "0.01" + # } + # + # accepted order responseType ACK + # { + # "accountId": "106490", + # "orderId": "1000109892193", + # "submitted": True, + # "marketCode": "OX-USDT", + # "side": "BUY", + # "price": "0.01961", + # "isTriggered": False, + # "quantity": "100", + # "orderType": "MARKET", + # "timeInForce": "IOC", + # "createdAt": "1715591529057", + # "selfTradePreventionMode": "NONE" + # } + # + # rejected order(balance insufficient) + # { + # "code": "710001", + # "message": "System failure, exception thrown -> null", + # "submitted": False, + # "marketCode": "OX-USDT", + # "side": "BUY", + # "price": "0.01961", + # "amount": "100", + # "orderType": "MARKET", + # "timeInForce": "IOC", + # "createdAt": "1715591678835", + # "source": 11, + # "selfTradePreventionMode": "NONE" + # } + # + # rejected order(bad request) + # { + # "code": "20044", + # "message": "Amount is not supported for self order type", + # "submitted": False, + # "marketCode": "OX-USDT", + # "side": "SELL", + # "amount": "200", + # "orderType": "MARKET", + # "createdAt": "1715592079986", + # "source": 11 + # } + # + # fetchOrder + # { + # "orderId": "1000111762980", + # "clientOrderId": "0", + # "marketCode": "ETH-USD-SWAP-LIN", + # "status": "OPEN", + # "side": "BUY", + # "price": "2700.0", + # "isTriggered": False, + # "remainQuantity": "0.01", + # "totalQuantity": "0.01", + # "amount": "0", + # "displayQuantity": "0.01", + # "cumulativeMatchedQuantity": "0", + # "orderType": "STOP_LIMIT", + # "timeInForce": "GTC", + # "source": "11", + # "createdAt": "1715794191277" + # } + # + marketId = self.safe_string(order, 'marketCode') + market = self.safe_market(marketId, market) + timestamp = self.safe_integer(order, 'createdAt') + fee = None + feeCurrency = self.safe_string(order, 'feeInstrumentId') + if feeCurrency is not None: + fee = { + 'currency': self.safe_currency_code(feeCurrency), + 'cost': self.safe_number(order, 'fees'), + } + status = self.safe_string(order, 'status') + code = self.safe_integer(order, 'code') # rejected orders have code of the error + if code is not None: + status = 'rejected' + triggerPrice = self.safe_string(order, 'stopPrice') + return self.safe_order({ + 'id': self.safe_string(order, 'orderId'), + 'clientOrderId': self.safe_string(order, 'clientOrderId'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': self.safe_integer(order, 'lastMatchedAt'), + 'lastUpdateTimestamp': self.safe_integer(order, 'lastModifiedAt'), + 'status': self.parse_order_status(status), + 'symbol': market['symbol'], + 'type': self.parse_order_type(self.safe_string(order, 'orderType')), + 'timeInForce': self.parse_order_time_in_force(self.safe_string(order, 'timeInForce')), # only for limit orders + 'side': self.safe_string_lower(order, 'side'), + 'price': self.safe_string_n(order, ['price', 'matchPrice', 'limitPrice']), + 'average': None, + 'amount': self.safe_string_2(order, 'totalQuantity', 'quantity'), + 'filled': self.safe_string_2(order, 'cumulativeMatchedQuantity', 'matchQuantity'), + 'remaining': self.safe_string(order, 'remainQuantity'), + 'triggerPrice': triggerPrice, + 'stopLossPrice': triggerPrice, + 'cost': self.omit_zero(self.safe_string(order, 'amount')), + 'trades': None, + 'fee': fee, + 'info': order, + }, market) + + def parse_order_status(self, status): + statuses: dict = { + 'OPEN': 'open', + 'PARTIALLY_FILLED': 'open', + 'PARTIAL_FILL': 'open', + 'FILLED': 'closed', + 'CANCELED': 'canceled', + 'CANCELED_BY_USER': 'canceled', + 'CANCELED_BY_MAKER_ONLY': 'rejected', + 'CANCELED_BY_FOK': 'rejected', + 'CANCELED_ALL_BY_IOC': 'rejected', + 'CANCELED_PARTIAL_BY_IOC': 'canceled', + 'CANCELED_BY_SELF_TRADE_PROTECTION': 'rejected', + } + return self.safe_string(statuses, status, status) + + def parse_order_type(self, type): + types: dict = { + 'LIMIT': 'limit', + 'STOP_LIMIT': 'limit', + 'MARKET': 'market', + 'STOP_MARKET': 'market', + } + return self.safe_string(types, type, type) + + def parse_order_time_in_force(self, type): + types: dict = { + 'GTC': 'GTC', + 'IOC': 'IOC', + 'FOK': 'FOK', + 'MAKER_ONLY': 'PO', + 'MAKER_ONLY_REPRICE': 'PO', + } + return self.safe_string(types, type, type) + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + baseUrl = self.urls['api'][api] + url = baseUrl + '/' + path + queryString = '' + if method == 'GET': + queryString = self.urlencode(params) + if len(queryString) != 0: + url += '?' + queryString + if api == 'private': + self.check_required_credentials() + timestamp = self.milliseconds() + isoDatetime = self.iso8601(timestamp) + datetimeParts = isoDatetime.split('.') + datetime = datetimeParts[0] + nonce = self.nonce() + urlParts = baseUrl.split('//') + if (method == 'POST') or (method == 'DELETE'): + body = self.json(params) + queryString = body + msgString = datetime + '\n' + str(nonce) + '\n' + method + '\n' + urlParts[1] + '\n/' + path + '\n' + queryString + signature = self.hmac(self.encode(msgString), self.encode(self.secret), hashlib.sha256, 'base64') + headers = { + 'Content-Type': 'application/json', + 'AccessKey': self.apiKey, + 'Timestamp': datetime, + 'Signature': signature, + 'Nonce': str(nonce), + } + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code, reason, url, method, headers, body, response, requestHeaders, requestBody): + if response is None: + return None + if code != 200: + responseCode = self.safe_string(response, 'code', None) + feedback = self.id + ' ' + body + self.throw_broadly_matched_exception(self.exceptions['broad'], body, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], responseCode, feedback) + raise ExchangeError(feedback) + return None diff --git a/ccxt/p2b.py b/ccxt/p2b.py new file mode 100644 index 0000000..5c536be --- /dev/null +++ b/ccxt/p2b.py @@ -0,0 +1,1316 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.p2b import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Int, Market, Num, Order, OrderSide, OrderType, Str, Strings, Ticker, Tickers +from typing import List +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.decimal_to_precision import TICK_SIZE + + +class p2b(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(p2b, self).describe(), { + 'id': 'p2b', + 'name': 'p2b', + 'countries': ['LT'], + 'rateLimit': 100, + 'version': 'v2', + 'pro': True, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelAllOrders': False, + 'cancelOrder': True, + 'cancelOrders': False, + 'closeAllPositions': False, + 'closePosition': False, + 'createDepositAddress': False, + 'createMarketOrder': False, + 'createOrder': True, + 'createOrders': False, + 'createPostOnlyOrder': False, + 'createReduceOnlyOrder': False, + 'createStopLimitOrder': False, + 'createStopMarketOrder': False, + 'createStopOrder': False, + 'fetchAccounts': False, + 'fetchAllGreeks': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchDeposit': False, + 'fetchDepositAddress': False, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': False, + 'fetchDepositsWithdrawals': False, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLedger': False, + 'fetchLedgerEntry': False, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrice': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrderBook': True, + 'fetchOrderBooks': False, + 'fetchOrders': False, + 'fetchOrderTrades': True, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTrades': True, + 'fetchTradingLimits': False, + 'fetchTransactionFee': False, + 'fetchTransactionFees': False, + 'fetchTransactions': False, + 'fetchTransfers': False, + 'fetchUnderlyingAssets': False, + 'fetchVolatilityHistory': False, + 'fetchWithdrawAddresses': False, + 'fetchWithdrawal': False, + 'fetchWithdrawals': False, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'signIn': False, + 'transfer': False, + 'withdraw': False, + }, + 'timeframes': { + '1m': '1m', + '1h': '1h', + '1d': '1d', + }, + 'urls': { + 'extension': '.json', + 'referral': 'https://p2pb2b.com?referral=ee784c53', + 'logo': 'https://github.com/ccxt/ccxt/assets/43336371/8da13a80-1f0a-49be-bb90-ff8b25164755', + 'api': { + 'public': 'https://api.p2pb2b.com/api/v2/public', + 'private': 'https://api.p2pb2b.com/api/v2', + }, + 'www': 'https://p2pb2b.com/', + 'doc': 'https://github.com/P2B-team/p2b-api-docs/blob/master/api-doc.md', + 'fees': 'https://p2pb2b.com/fee-schedule/', + }, + 'api': { + 'public': { + 'get': { + 'markets': 1, + 'market': 1, + 'tickers': 1, + 'ticker': 1, + 'book': 1, + 'history': 1, + 'depth/result': 1, + 'market/kline': 1, + }, + }, + 'private': { + 'post': { + 'account/balances': 1, + 'account/balance': 1, + 'order/new': 1, + 'order/cancel': 1, + 'orders': 1, + 'account/market_order_history': 1, + 'account/market_deal_history': 1, + 'account/order': 1, + 'account/order_history': 1, + 'account/executed_history': 1, + }, + }, + }, + 'fees': { + 'trading': { + 'tierBased': True, + 'percentage': True, + 'taker': [ + [self.parse_number('0'), self.parse_number('0.2')], + [self.parse_number('1'), self.parse_number('0.19')], + [self.parse_number('5'), self.parse_number('0.18')], + [self.parse_number('10'), self.parse_number('0.17')], + [self.parse_number('25'), self.parse_number('0.16')], + [self.parse_number('75'), self.parse_number('0.15')], + [self.parse_number('100'), self.parse_number('0.14')], + [self.parse_number('150'), self.parse_number('0.13')], + [self.parse_number('300'), self.parse_number('0.12')], + [self.parse_number('450'), self.parse_number('0.11')], + [self.parse_number('500'), self.parse_number('0.1')], + ], + 'maker': [ + [self.parse_number('0'), self.parse_number('0.2')], + [self.parse_number('1'), self.parse_number('0.18')], + [self.parse_number('5'), self.parse_number('0.16')], + [self.parse_number('10'), self.parse_number('0.14')], + [self.parse_number('25'), self.parse_number('0.12')], + [self.parse_number('75'), self.parse_number('0.1')], + [self.parse_number('100'), self.parse_number('0.08')], + [self.parse_number('150'), self.parse_number('0.06')], + [self.parse_number('300'), self.parse_number('0.04')], + [self.parse_number('450'), self.parse_number('0.02')], + [self.parse_number('500'), self.parse_number('0.01')], + ], + }, + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': False, + 'triggerDirection': False, + 'triggerPriceType': None, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': False, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 100000, # todo + 'untilDays': 1, + 'symbolRequired': True, + }, + 'fetchOrder': None, # todo + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 100, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOrders': None, # todo + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 100000, # todo + 'daysBackCanceled': 1 / 12, # todo + 'untilDays': 1, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 500, + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'commonCurrencies': { + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + '1001': AuthenticationError, # Key not provided. X-TXC-APIKEY header is missing in the request or empty. + '1002': AuthenticationError, # Payload not provided. X-TXC-PAYLOAD header is missing in the request or empty. + '1003': AuthenticationError, # Signature not provided. X-TXC-SIGNATURE header is missing in the request or empty. + '1004': AuthenticationError, # Nonce and url not provided. Request body is empty. Missing required parameters "request", "nonce". + '1005': AuthenticationError, # Invalid body data. Invalid request body + '1006': AuthenticationError, # Nonce not provided. Request body missing required parameter "nonce". + '1007': AuthenticationError, # Request not provided. Request body missing required parameter "request". + '1008': AuthenticationError, # Invalid request in body. The passed request parameter does not match the URL of self request. + '1009': AuthenticationError, # Invalid payload. The transmitted payload value(X-TXC-PAYLOAD header) does not match the request body. + '1010': AuthenticationError, # This action is unauthorized. - API key passed in the X-TXC-APIKEY header does not exist. - Access to API is not activated. Go to profile and activate access. + '1011': AuthenticationError, # This action is unauthorized. Please, enable two-factor authentication. Two-factor authentication is not activated for the user. + '1012': AuthenticationError, # Invalid nonce. Parameter "nonce" is not a number. + '1013': AuthenticationError, # Too many requests. - A request came with a repeated value of nonce. - Received more than the limited value of requests(10) within one second. + '1014': AuthenticationError, # Unauthorized request. Signature value passed(in the X-TXC-SIGNATURE header) does not match the request body. + '1015': AuthenticationError, # Temporary block. Temporary blocking. There is a cancellation of orders. + '1016': AuthenticationError, # Not unique nonce. The request was sent with a repeated parameter "nonce" within 10 seconds. + '2010': BadRequest, # Currency not found. Currency not found. + '2020': BadRequest, # Market is not available. Market is not available. + '2021': BadRequest, # Unknown market. Unknown market. + '2030': BadRequest, # Order not found. Order not found. + '2040': InsufficientFunds, # Balance not enough. Insufficient balance. + '2050': BadRequest, # Amount less than the permitted minimum. Amount less than the permitted minimum. + '2051': BadRequest, # Amount is greater than the maximum allowed. Amount exceeds the allowed maximum. + '2052': BadRequest, # Amount step size error. Amount step size error. + '2060': BadRequest, # Price less than the permitted minimum. Price is less than the permitted minimum. + '2061': BadRequest, # Price is greater than the maximum allowed. Price exceeds the allowed maximum. + '2062': BadRequest, # Price pick size error. Price pick size error. + '2070': BadRequest, # Total less than the permitted minimum. Total less than the permitted minimum. + '3001': BadRequest, # Validation exception. The given data was invalid. + '3020': BadRequest, # Invalid currency value. Incorrect parameter, check your request. + '3030': BadRequest, # Invalid market value. Incorrect "market" parameter, check your request. + '3040': BadRequest, # Invalid amount value. Incorrect "amount" parameter, check your request. + '3050': BadRequest, # Invalid price value. Incorrect "price" parameter, check your request. + '3060': BadRequest, # Invalid limit value. Incorrect "limit" parameter, check your request. + '3070': BadRequest, # Invalid offset value. Incorrect "offset" parameter, check your request. + '3080': BadRequest, # Invalid orderId value. Incorrect "orderId" parameter, check your request. + '3090': BadRequest, # Invalid lastId value. Incorrect "lastId" parameter, check your request. + '3100': BadRequest, # Invalid side value. Incorrect "side" parameter, check your request. + '3110': BadRequest, # Invalid interval value. Incorrect "interval" parameter, check your request. + '4001': ExchangeNotAvailable, # Service temporary unavailable. An unexpected system error has occurred. Try again after a while. If the error persists, please contact support. + '6010': InsufficientFunds, # Balance not enough. Insufficient balance. + }, + 'options': { + }, + }) + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for bigone + + https://github.com/P2B-team/p2b-api-docs/blob/master/api-doc.md#markets + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = self.publicGetMarkets(params) + # + # { + # "success": True, + # "errorCode": "", + # "message": "", + # "result": [ + # { + # "name": "ETH_BTC", + # "stock": "ETH", + # "money": "BTC", + # "precision": { + # "money": "6", + # "stock": "4", + # "fee": "4" + # }, + # "limits": { + # "min_amount": "0.001", + # "max_amount": "100000", + # "step_size": "0.0001", + # "min_price": "0.00001", + # "max_price": "922327", + # "tick_size": "0.00001", + # "min_total": "0.0001" + # } + # }, + # ... + # ] + # } + # + markets = self.safe_value(response, 'result', []) + return self.parse_markets(markets) + + def parse_market(self, market: dict) -> Market: + marketId = self.safe_string(market, 'name') + baseId = self.safe_string(market, 'stock') + quoteId = self.safe_string(market, 'money') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + limits = self.safe_value(market, 'limits') + maxAmount = self.safe_string(limits, 'max_amount') + maxPrice = self.safe_string(limits, 'max_price') + return { + 'id': marketId, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': True, + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.safe_number(limits, 'step_size'), + 'price': self.safe_number(limits, 'tick_size'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(limits, 'min_amount'), + 'max': self.parse_number(self.omit_zero(maxAmount)), + }, + 'price': { + 'min': self.safe_number(limits, 'min_price'), + 'max': self.parse_number(self.omit_zero(maxPrice)), + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + 'info': market, + } + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://futures-docs.poloniex.com/#get-real-time-ticker-of-all-symbols + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + response = self.publicGetTickers(params) + # + # { + # success: True, + # errorCode: '', + # message: '', + # result: { + # KNOLIX_BTC: { + # at: '1699252631', + # ticker: { + # bid: '0.0000332', + # ask: '0.0000333', + # low: '0.0000301', + # high: '0.0000338', + # last: '0.0000333', + # vol: '15.66', + # deal: '0.000501828', + # change: '10.63' + # } + # }, + # ... + # }, + # cache_time: '1699252631.103631', + # current_time: '1699252644.487566' + # } + # + result = self.safe_value(response, 'result', {}) + return self.parse_tickers(result, symbols) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://github.com/P2B-team/p2b-api-docs/blob/master/api-doc.md#ticker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + response = self.publicGetTicker(self.extend(request, params)) + # + # { + # success: True, + # errorCode: '', + # message: '', + # result: { + # bid: '0.342', + # ask: '0.3421', + # open: '0.3317', + # high: '0.3499', + # low: '0.3311', + # last: '0.3421', + # volume: '17855383.1', + # deal: '6107478.3423', + # change: '3.13' + # }, + # cache_time: '1699252953.832795', + # current_time: '1699252958.859391' + # } + # + result = self.safe_value(response, 'result', {}) + timestamp = self.safe_integer_product(response, 'cache_time', 1000) + return self.extend( + {'timestamp': timestamp, 'datetime': self.iso8601(timestamp)}, + self.parse_ticker(result, market) + ) + + def parse_ticker(self, ticker, market: Market = None): + # + # parseTickers + # + # { + # at: '1699252631', + # ticker: { + # bid: '0.0000332', + # ask: '0.0000333', + # low: '0.0000301', + # high: '0.0000338', + # last: '0.0000333', + # vol: '15.66', + # deal: '0.000501828', + # change: '10.63' + # } + # } + # + # parseTicker + # + # { + # bid: '0.342', + # ask: '0.3421', + # open: '0.3317', + # high: '0.3499', + # low: '0.3311', + # last: '0.3421', + # volume: '17855383.1', + # deal: '6107478.3423', + # change: '3.13' + # } + # + timestamp = self.safe_integer_product(ticker, 'at', 1000) + if 'ticker' in ticker: + ticker = self.safe_value(ticker, 'ticker') + last = self.safe_string(ticker, 'last') + return self.safe_ticker({ + 'symbol': self.safe_string(market, 'symbol'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': self.safe_string(ticker, 'bid'), + 'bidVolume': None, + 'ask': self.safe_string(ticker, 'ask'), + 'askVolume': None, + 'vwap': None, + 'open': self.safe_string(ticker, 'open'), + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': self.safe_string(ticker, 'change'), + 'average': None, + 'baseVolume': self.safe_string_2(ticker, 'vol', 'volume'), + 'quoteVolume': self.safe_string(ticker, 'deal'), + 'info': ticker, + }, market) + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}): + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://github.com/P2B-team/p2b-api-docs/blob/master/api-doc.md#depth-result + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + + EXCHANGE SPECIFIC PARAMETERS + :param str [params.interval]: 0(default), 0.00000001, 0.0000001, 0.000001, 0.00001, 0.0001, 0.001, 0.01, 0.1, 1 + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + if limit is not None: + request['limit'] = limit + response = self.publicGetDepthResult(self.extend(request, params)) + # + # { + # "success": True, + # "errorCode": "", + # "message": "", + # "result": { + # "asks": [ + # [ + # "4.53", # Price + # "523.95" # Amount + # ], + # ... + # ], + # "bids": [ + # [ + # "4.51", + # "244.75" + # ], + # ... + # ] + # }, + # "cache_time": 1698733470.469175, + # "current_time": 1698733470.469274 + # } + # + result = self.safe_value(response, 'result', {}) + timestamp = self.safe_integer_product(response, 'current_time', 1000) + return self.parse_order_book(result, market['symbol'], timestamp, 'bids', 'asks', 0, 1) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}): + """ + get the list of most recent trades for a particular symbol + + https://github.com/P2B-team/p2b-api-docs/blob/master/api-doc.md#history + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: 1-100, default=50 + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int params['lastId']: order id + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + lastId = self.safe_integer(params, 'lastId') + if lastId is None: + raise ArgumentsRequired(self.id + ' fetchTrades() requires an extra parameter params["lastId"]') + market = self.market(symbol) + request: dict = { + 'market': market['id'], + 'lastId': lastId, + } + if limit is not None: + request['limit'] = limit + response = self.publicGetHistory(self.extend(request, params)) + # + # { + # success: True, + # errorCode: '', + # message: '', + # result: [ + # { + # id: '7495738622', + # type: 'sell', + # time: '1699255565.445418', + # amount: '252.6', + # price: '0.3422' + # }, + # ... + # ], + # cache_time: '1699255571.413633', + # current_time: '1699255571.413828' + # } + # + result = self.safe_list(response, 'result', []) + return self.parse_trades(result, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None): + # + # fetchTrades + # + # { + # id: '7495738622', + # type: 'sell', + # time: '1699255565.445418', + # amount: '252.6', + # price: '0.3422' + # } + # + # fetchMyTrades + # + # { + # "deal_id": 7450617292, # Deal id + # "deal_time": 1698506956.66224, # Deal execution time + # "deal_order_id": 171955225751, # Deal order id + # "opposite_order_id": 171955110512, # Opposite order id + # "side": "sell", # Deal side + # "price": "0.05231", # Deal price + # "amount": "0.002", # Deal amount + # "deal": "0.00010462", # Total(price * amount) + # "deal_fee": "0.000000188316", # Deal fee + # "role": "taker", # Role. Taker or maker + # "isSelfTrade": False # is self trade + # } + # + # fetchOrderTrades + # + # { + # "id": 7429883128, # Deal id + # "time": 1698237535.41196, # Deal execution time + # "fee": "0.01755848704", # Deal fee + # "price": "34293.92", # Deal price + # "amount": "0.00032", # Deal amount + # "dealOrderId": 171366551416, # Deal order id + # "role": 1, # Deal role(1 - maker, 2 - taker) + # "deal": "10.9740544" # Total(price * amount) + # } + # + timestamp = self.safe_integer_product_2(trade, 'time', 'deal_time', 1000) + takerOrMaker = self.safe_string(trade, 'role') + if takerOrMaker == '1': + takerOrMaker = 'maker' + elif takerOrMaker == '2': + takerOrMaker = 'taker' + return self.safe_trade({ + 'info': trade, + 'id': self.safe_string_2(trade, 'id', 'deal_id'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': self.safe_string(market, 'symbol'), + 'order': self.safe_string_2(trade, 'dealOrderId', 'deal_order_id'), + 'type': None, + 'side': self.safe_string_2(trade, 'type', 'side'), + 'takerOrMaker': takerOrMaker, + 'price': self.safe_string(trade, 'price'), + 'amount': self.safe_string(trade, 'amount'), + 'cost': self.safe_string(trade, 'deal'), + 'fee': { + 'currency': market['quote'], + 'cost': self.safe_string_2(trade, 'fee', 'deal_fee'), + }, + }, market) + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}): + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://github.com/P2B-team/p2b-api-docs/blob/master/api-doc.md#kline + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: 1m, 1h, or 1d + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: 1-500, default=50 + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.offset]: default=0, with self value the last candles are returned + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + 'interval': timeframe, + } + if limit is not None: + request['limit'] = limit + response = self.publicGetMarketKline(self.extend(request, params)) + # + # { + # success: True, + # errorCode: '', + # message: '', + # result: [ + # [ + # 1699253400, # Kline open time + # '0.3429', # Open price + # '0.3427', # Close price + # '0.3429', # Highest price + # '0.3427', # Lowest price + # '1900.4', # Volume for stock currency + # '651.46278', # Volume for money currency + # 'ADA_USDT' # Market name + # ], + # ... + # ], + # cache_time: '1699256375.030292', + # current_time: '1699256375.030494' + # } + # + result = self.safe_list(response, 'result', []) + return self.parse_ohlcvs(result, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # [ + # 1699253400, # Kline open time + # '0.3429', # Open price + # '0.3427', # Close price + # '0.3429', # Highest price + # '0.3427', # Lowest price + # '1900.4', # Volume for stock currency + # '651.46278', # Volume for money currency + # 'ADA_USDT' # Market name + # ], + # + return [ + self.safe_integer_product(ohlcv, 0, 1000), + self.safe_number(ohlcv, 1), + self.safe_number(ohlcv, 3), + self.safe_number(ohlcv, 4), + self.safe_number(ohlcv, 2), + self.safe_number(ohlcv, 5), + ] + + def fetch_balance(self, params={}): + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://github.com/P2B-team/p2b-api-docs/blob/master/api-doc.md#all-balances + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + response = self.privatePostAccountBalances(params) + # + # { + # "success": True, + # "errorCode": "", + # "message": "", + # "result": { + # "USDT": { + # "available": "71.81328046", + # "freeze": "10.46103091" + # }, + # "BTC": { + # "available": "0.00135674", + # "freeze": "0.00020003" + # } + # } + # } + # + result = self.safe_value(response, 'result', {}) + return self.parse_balance(result) + + def parse_balance(self, response): + # + # { + # "USDT": { + # "available": "71.81328046", + # "freeze": "10.46103091" + # }, + # "BTC": { + # "available": "0.00135674", + # "freeze": "0.00020003" + # } + # } + # + result: dict = { + 'info': response, + } + keys = list(response.keys()) + for i in range(0, len(keys)): + currencyId = keys[i] + balance = response[currencyId] + code = self.safe_currency_code(currencyId) + used = self.safe_string(balance, 'freeze') + available = self.safe_string(balance, 'available') + account: dict = { + 'free': available, + 'used': used, + } + result[code] = account + return self.safe_balance(result) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://github.com/P2B-team/p2b-api-docs/blob/master/api-doc.md#create-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: must be 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float price: the price at which the order is to be fulfilled, in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + if type == 'market': + raise BadRequest(self.id + ' createOrder() can only accept orders with type "limit"') + market = self.market(symbol) + request: dict = { + 'market': market['id'], + 'side': side, + 'amount': self.amount_to_precision(symbol, amount), + 'price': self.price_to_precision(symbol, price), + } + response = self.privatePostOrderNew(self.extend(request, params)) + # + # { + # "success": True, + # "errorCode": "", + # "message": "", + # "result": { + # "orderId": 171906478744, # Order id + # "market": "ETH_BTC", # Market name + # "price": "0.04348", # Price + # "side": "buy", # Side + # "type": "limit", # Order type + # "timestamp": 1698484861.746517, # Order creation time + # "dealMoney": "0", # Filled total + # "dealStock": "0", # Filled amount + # "amount": "0.0277", # Original amount + # "takerFee": "0.002", # taker fee + # "makerFee": "0.002", # maker fee + # "left": "0.0277", # Unfilled amount + # "dealFee": "0" # Filled fee + # } + # } + # + result = self.safe_dict(response, 'result') + return self.parse_order(result, market) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://github.com/P2B-team/p2b-api-docs/blob/master/api-doc.md#cancel-order + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + 'orderId': id, + } + response = self.privatePostOrderCancel(self.extend(request, params)) + # + # { + # "success": True, + # "errorCode": "", + # "message": "", + # "result": { + # "orderId": 171906478744, + # "market": "ETH_BTC", + # "price": "0.04348", + # "side": "buy", + # "type": "limit", + # "timestamp": 1698484861.746517, + # "dealMoney": "0", + # "dealStock": "0", + # "amount": "0.0277", + # "takerFee": "0.002", + # "makerFee": "0.002", + # "left": "0.0277", + # "dealFee": "0" + # } + # } + # + result = self.safe_dict(response, 'result') + return self.parse_order(result) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all unfilled currently open orders + + https://github.com/P2B-team/p2b-api-docs/blob/master/api-doc.md#open-orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + + EXCHANGE SPECIFIC PARAMETERS + :param int [params.offset]: 0-10000, default=0 + :returns Order[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOpenOrders() requires the symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + if limit is not None: + request['limit'] = limit + response = self.privatePostOrders(self.extend(request, params)) + # + # { + # "success": True, + # "errorCode": "", + # "message": "", + # "result": [ + # { + # "orderId": 171913325964, + # "market": "ETH_BTC", + # "price": "0.06534", + # "side": "sell", + # "type": "limit", + # "timestamp": 1698487986.836821, + # "dealMoney": "0", + # "dealStock": "0", + # "amount": "0.0018", + # "takerFee": "0.0018", + # "makerFee": "0.0016", + # "left": "0.0018", + # "dealFee": "0" + # }, + # ... + # ] + # } + # + result = self.safe_list(response, 'result', []) + return self.parse_orders(result, market, since, limit) + + def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all the trades made from a single order + + https://github.com/P2B-team/p2b-api-docs/blob/master/api-doc.md#deals-by-order-id + + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: 1-100, default=50 + :param dict [params]: extra parameters specific to the exchange API endpoint + + EXCHANGE SPECIFIC PARAMETERS + :param int [params.offset]: 0-10000, default=0 + :returns dict[]: a list of `trade structures ` + """ + self.load_markets() + market = self.safe_market(symbol) + request: dict = { + 'orderId': id, + } + if limit is not None: + request['limit'] = limit + response = self.privatePostAccountOrder(self.extend(request, params)) + # + # { + # "success": True, + # "errorCode": "", + # "message": "", + # "result": { + # "offset": 0, + # "limit": 50, + # "records": [ + # { + # "id": 7429883128, # Deal id + # "time": 1698237535.41196, # Deal execution time + # "fee": "0.01755848704", # Deal fee + # "price": "34293.92", # Deal price + # "amount": "0.00032", # Deal amount + # "dealOrderId": 171366551416, # Deal order id + # "role": 1, # Deal role(1 - maker, 2 - taker) + # "deal": "10.9740544" # Total(price * amount) + # } + # ] + # } + # } + # + result = self.safe_value(response, 'result', {}) + records = self.safe_list(result, 'records', []) + return self.parse_trades(records, market, since, limit) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user, only the transaction records in the past 3 month can be queried, the time between since and params["until"] cannot be longer than 24 hours + + https://github.com/P2B-team/p2b-api-docs/blob/master/api-doc.md#deals-history-by-market + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for, default = params["until"] - 86400000 + :param int [limit]: 1-100, default=50 + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch orders for, default = current timestamp or since + 86400000 + + EXCHANGE SPECIFIC PARAMETERS + :param int [params.offset]: 0-10000, default=0 + :returns Trade[]: a list of `trade structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMyTrades() requires a symbol argument') + self.load_markets() + until = self.safe_integer(params, 'until') + params = self.omit(params, 'until') + if until is None: + if since is None: + until = self.milliseconds() + else: + until = since + 86400000 + if since is None: + since = until - 86400000 + if (until - since) > 86400000: + raise BadRequest(self.id + ' fetchMyTrades() the time between since and params["until"] cannot be greater than 24 hours') + market = self.market(symbol) + request: dict = { + 'market': market['id'], + 'startTime': self.parse_to_int(since / 1000), + 'endTime': self.parse_to_int(until / 1000), + } + if limit is not None: + request['limit'] = limit + response = self.privatePostAccountMarketDealHistory(self.extend(request, params)) + # + # { + # "success": True, + # "errorCode": "", + # "message": "", + # "result": { + # "total": 2, # Total records in the queried range + # "deals": [ + # { + # "deal_id": 7450617292, # Deal id + # "deal_time": 1698506956.66224, # Deal execution time + # "deal_order_id": 171955225751, # Deal order id + # "opposite_order_id": 171955110512, # Opposite order id + # "side": "sell", # Deal side + # "price": "0.05231", # Deal price + # "amount": "0.002", # Deal amount + # "deal": "0.00010462", # Total(price * amount) + # "deal_fee": "0.000000188316", # Deal fee + # "role": "taker", # Role. Taker or maker + # "isSelfTrade": False # is self trade + # }, + # ... + # ] + # } + # } + # + result = self.safe_value(response, 'result', {}) + deals = self.safe_list(result, 'deals', []) + return self.parse_trades(deals, market, since, limit) + + def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user, the time between since and params["untnil"] cannot be longer than 24 hours + + https://github.com/P2B-team/p2b-api-docs/blob/master/api-doc.md#orders-history-by-market + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for, default = params["until"] - 86400000 + :param int [limit]: 1-100, default=50 + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch orders for, default = current timestamp or since + 86400000 + + EXCHANGE SPECIFIC PARAMETERS + :param int [params.offset]: 0-10000, default=0 + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + until = self.safe_integer(params, 'until') + params = self.omit(params, 'until') + market: Market = None + if symbol is not None: + market = self.market(symbol) + if until is None: + if since is None: + until = self.milliseconds() + else: + until = since + 86400000 + if since is None: + since = until - 86400000 + if (until - since) > 86400000: + raise BadRequest(self.id + ' fetchClosedOrders() the time between since and params["until"] cannot be greater than 24 hours') + request: dict = { + 'startTime': self.parse_to_int(since / 1000), + 'endTime': self.parse_to_int(until / 1000), + } + if market is not None: + request['market'] = market['id'] + if limit is not None: + request['limit'] = limit + response = self.privatePostAccountOrderHistory(self.extend(request, params)) + # + # { + # "success": True, + # "errorCode": "", + # "message": "", + # "result": { + # "LTC_USDT": [ + # { + # "id": 173985944395, + # "amount": "0.1", + # "price": "73", + # "type": "limit", + # "side": "sell", + # "ctime": 1699436194.390845, + # "ftime": 1699436194.390847, + # "market": "LTC_USDT", + # "takerFee": "0.002", + # "makerFee": "0.002", + # "dealFee": "0.01474", + # "dealStock": "0.1", + # "dealMoney": "7.37" + # } + # ] + # } + # } + # + result = self.safe_value(response, 'result') + orders = [] + keys = list(result.keys()) + for i in range(0, len(keys)): + marketId = keys[i] + marketOrders = result[marketId] + parsedOrders = self.parse_orders(marketOrders, market, since, limit) + orders = self.array_concat(orders, parsedOrders) + return orders + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # cancelOrder, fetchOpenOrders, createOrder + # + # { + # "orderId": 171906478744, + # "market": "ETH_BTC", + # "price": "0.04348", + # "side": "buy", + # "type": "limit", + # "timestamp": 1698484861.746517, + # "dealMoney": "0", + # "dealStock": "0", + # "amount": "0.0277", + # "takerFee": "0.002", + # "makerFee": "0.002", + # "left": "0.0277", + # "dealFee": "0" + # } + # + # fetchClosedOrders + # + # { + # "id": 171366547790, # Order id + # "amount": "0.00032", # Original amount + # "price": "34293.92", # Order price + # "type": "limit", # Order type + # "side": "sell", # Order side + # "ctime": 1698237533.497241, # Order creation time + # "ftime": 1698237535.41196, # Order fill time + # "market": "BTC_USDT", # Market name + # "takerFee": "0.0018", # Taker fee + # "makerFee": "0.0016", # Market fee + # "dealFee": "0.01755848704", # Deal fee + # "dealStock": "0.00032", # Filled amount + # "dealMoney": "10.9740544" # Filled total + # } + # + timestamp = self.safe_integer_product_2(order, 'timestamp', 'ctime', 1000) + marketId = self.safe_string(order, 'market') + market = self.safe_market(marketId, market) + return self.safe_order({ + 'info': order, + 'id': self.safe_string_2(order, 'id', 'orderId'), + 'clientOrderId': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'symbol': market['symbol'], + 'type': self.safe_string(order, 'type'), + 'timeInForce': None, + 'postOnly': None, + 'side': self.safe_string(order, 'side'), + 'price': self.safe_string(order, 'price'), + 'triggerPrice': None, + 'amount': self.safe_string(order, 'amount'), + 'cost': None, + 'average': None, + 'filled': self.safe_string(order, 'dealStock'), + 'remaining': self.safe_string(order, 'left'), + 'status': None, + 'fee': { + 'currency': market['quote'], + 'cost': self.safe_string(order, 'dealFee'), + }, + 'trades': None, + }, market) + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = self.urls['api'][api] + '/' + self.implode_params(path, params) + params = self.omit(params, self.extract_params(path)) + if method == 'GET': + if params: + url += '?' + self.urlencode(params) + if api == 'private': + params['request'] = '/api/v2/' + path + params['nonce'] = str(self.nonce()) + payload = self.string_to_base64(self.json(params)) # Body json encoded in base64 + headers = { + 'Content-Type': 'application/json', + 'X-TXC-APIKEY': self.apiKey, + 'X-TXC-PAYLOAD': payload, + 'X-TXC-SIGNATURE': self.hmac(self.encode(payload), self.encode(self.secret), hashlib.sha512), + } + body = self.json(params) + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None + if code == 400: + error = self.safe_value(response, 'error') + errorCode = self.safe_string(error, 'code') + feedback = self.id + ' ' + self.json(response) + self.throw_exactly_matched_exception(self.exceptions, errorCode, feedback) + # fallback to default error handler + return None diff --git a/ccxt/paradex.py b/ccxt/paradex.py new file mode 100644 index 0000000..d25ba2f --- /dev/null +++ b/ccxt/paradex.py @@ -0,0 +1,2528 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.paradex import ImplicitAPI +from ccxt.base.types import Any, Balances, Currency, Greeks, Int, Leverage, MarginMode, Market, Num, Order, OrderBook, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, Trade, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import OperationRejected +from ccxt.base.errors import InvalidOrder +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class paradex(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(paradex, self).describe(), { + 'id': 'paradex', + 'name': 'Paradex', + 'countries': [], + 'version': 'v1', + 'rateLimit': 50, + 'certified': False, + 'pro': True, + 'dex': True, + 'has': { + 'CORS': None, + 'spot': False, + 'margin': False, + 'swap': True, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'cancelAllOrders': True, + 'cancelAllOrdersAfter': False, + 'cancelOrder': False, + 'cancelOrders': False, + 'cancelOrdersForSymbols': False, + 'closeAllPositions': False, + 'closePosition': False, + 'createMarketBuyOrderWithCost': False, + 'createMarketOrderWithCost': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createOrders': False, + 'createReduceOnlyOrder': False, + 'createStopOrder': True, + 'createTriggerOrder': True, + 'editOrder': False, + 'fetchAccounts': False, + 'fetchAllGreeks': True, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchCanceledOrders': False, + 'fetchClosedOrders': False, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': False, + 'fetchDepositAddress': False, + 'fetchDepositAddresses': False, + 'fetchDeposits': True, + 'fetchDepositWithdrawFee': False, + 'fetchDepositWithdrawFees': False, + 'fetchFundingHistory': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': True, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchLedger': False, + 'fetchLeverage': True, + 'fetchLeverageTiers': False, + 'fetchLiquidations': True, + 'fetchMarginMode': True, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMyLiquidations': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': True, + 'fetchOpenInterestHistory': False, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchOrderTrades': False, + 'fetchPosition': True, + 'fetchPositionMode': False, + 'fetchPositions': True, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchStatus': True, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': False, + 'fetchTransfer': False, + 'fetchTransfers': False, + 'fetchWithdrawal': False, + 'fetchWithdrawals': True, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'sandbox': True, + 'setLeverage': True, + 'setMarginMode': True, + 'setPositionMode': False, + 'transfer': False, + 'withdraw': False, + }, + 'timeframes': { + '1m': 1, + '3m': 3, + '5m': 5, + '15m': 15, + '30m': 30, + '1h': 60, + }, + 'hostname': 'paradex.trade', + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/84628770-784e-4ec4-a759-ec2fbb2244ea', + 'api': { + 'v1': 'https://api.prod.{hostname}/v1', + }, + 'test': { + 'v1': 'https://api.testnet.{hostname}/v1', + }, + 'www': 'https://www.paradex.trade/', + 'doc': 'https://docs.api.testnet.paradex.trade/', + 'fees': 'https://docs.paradex.trade/getting-started/trading-fees', + 'referral': 'https://app.paradex.trade/r/ccxt24', + }, + 'api': { + 'public': { + 'get': { + 'bbo/{market}': 1, + 'funding/data': 1, + 'markets': 1, + 'markets/klines': 1, + 'markets/summary': 1, + 'orderbook/{market}': 1, + 'insurance': 1, + 'referrals/config': 1, + 'system/config': 1, + 'system/state': 1, + 'system/time': 1, + 'trades': 1, + 'vaults': 1, + 'vaults/balance': 1, + 'vaults/config': 1, + 'vaults/history': 1, + 'vaults/positions': 1, + 'vaults/summary': 1, + 'vaults/transfers': 1, + }, + }, + 'private': { + 'get': { + 'account': 1, + 'account/info': 1, + 'account/history': 1, + 'account/margin': 1, + 'account/profile': 1, + 'account/subaccounts': 1, + 'balance': 1, + 'fills': 1, + 'funding/payments': 1, + 'positions': 1, + 'tradebusts': 1, + 'transactions': 1, + 'liquidations': 1, + 'orders': 1, + 'orders-history': 1, + 'orders/by_client_id/{client_id}': 1, + 'orders/{order_id}': 1, + 'points_data/{market}/{program}': 1, + 'referrals/qr-code': 1, + 'referrals/summary': 1, + 'transfers': 1, + 'algo/orders': 1, + 'algo/orders-history': 1, + 'algo/orders/{algo_id}': 1, + 'vaults/account-summary': 1, + }, + 'post': { + 'account/margin/{market}': 1, + 'account/profile/max_slippage': 1, + 'account/profile/referral_code': 1, + 'account/profile/username': 1, + 'auth': 1, + 'onboarding': 1, + 'orders': 1, + 'orders/batch': 1, + 'algo/orders': 1, + 'vaults': 1, + }, + 'put': { + 'orders/{order_id}': 1, + }, + 'delete': { + 'orders': 1, + 'orders/by_client_id/{client_id}': 1, + 'orders/{order_id}': 1, + 'algo/orders/{algo_id}': 1, + }, + }, + }, + 'fees': { + 'swap': { + 'taker': self.parse_number('0.0002'), + 'maker': self.parse_number('0.0002'), + }, + 'spot': { + 'taker': self.parse_number('0.0002'), + 'maker': self.parse_number('0.0002'), + }, + }, + 'requiredCredentials': { + 'apiKey': False, + 'secret': False, + 'walletAddress': True, + 'privateKey': True, + }, + 'exceptions': { + 'exact': { + 'VALIDATION_ERROR': AuthenticationError, + 'BINDING_ERROR': OperationRejected, + 'INTERNAL_ERROR': ExchangeError, + 'NOT_FOUND': BadRequest, + 'SERVICE_UNAVAILABLE': ExchangeError, + 'INVALID_REQUEST_PARAMETER': BadRequest, + 'ORDER_ID_NOT_FOUND': InvalidOrder, + 'ORDER_IS_CLOSED': InvalidOrder, + 'ORDER_IS_NOT_OPEN_YET': InvalidOrder, + 'CLIENT_ORDER_ID_NOT_FOUND': InvalidOrder, + 'DUPLICATED_CLIENT_ID': InvalidOrder, + 'INVALID_PRICE_PRECISION': OperationRejected, + 'INVALID_SYMBOL': OperationRejected, + 'INVALID_TOKEN': OperationRejected, + 'INVALID_ETHEREUM_ADDRESS': OperationRejected, + 'INVALID_ETHEREUM_SIGNATURE': OperationRejected, + 'INVALID_STARKNET_ADDRESS': OperationRejected, + 'INVALID_STARKNET_SIGNATURE': OperationRejected, + 'STARKNET_SIGNATURE_VERIFICATION_FAILED': AuthenticationError, + 'BAD_STARKNET_REQUEST': BadRequest, + 'ETHEREUM_SIGNER_MISMATCH': BadRequest, + 'ETHEREUM_HASH_MISMATCH': BadRequest, + 'NOT_ONBOARDED': BadRequest, + 'INVALID_TIMESTAMP': BadRequest, + 'INVALID_SIGNATURE_EXPIRATION': AuthenticationError, + 'ACCOUNT_NOT_FOUND': AuthenticationError, + 'INVALID_ORDER_SIGNATURE': AuthenticationError, + 'PUBLIC_KEY_INVALID': BadRequest, + 'UNAUTHORIZED_ETHEREUM_ADDRESS': BadRequest, + 'ETHEREUM_ADDRESS_ALREADY_ONBOARDED': BadRequest, + 'MARKET_NOT_FOUND': BadRequest, + 'ALLOWLIST_ENTRY_NOT_FOUND': BadRequest, + 'USERNAME_IN_USE': AuthenticationError, + 'GEO_IP_BLOCK': PermissionDenied, + 'ETHEREUM_ADDRESS_BLOCKED': PermissionDenied, + 'PROGRAM_NOT_FOUND': BadRequest, + 'INVALID_DASHBOARD': OperationRejected, + 'MARKET_NOT_OPEN': BadRequest, + 'INVALID_REFERRAL_CODE': OperationRejected, + 'PARENT_ADDRESS_ALREADY_ONBOARDED': BadRequest, + 'INVALID_PARENT_ACCOUNT': OperationRejected, + 'INVALID_VAULT_OPERATOR_CHAIN': OperationRejected, + 'VAULT_OPERATOR_ALREADY_ONBOARDED': OperationRejected, + 'VAULT_NAME_IN_USE': OperationRejected, + 'BATCH_SIZE_OUT_OF_RANGE': OperationRejected, + 'ISOLATED_MARKET_ACCOUNT_MISMATCH': OperationRejected, + 'POINTS_SUMMARY_NOT_FOUND': OperationRejected, + '-32700': BadRequest, # Parse error + '-32600': BadRequest, # Invalid request + '-32601': BadRequest, # Method not found + '-32602': BadRequest, # Invalid parameterss + '-32603': ExchangeError, # Internal error + '100': BadRequest, # Method error + '40110': AuthenticationError, # Malformed Bearer Token + '40111': AuthenticationError, # Invalid Bearer Token + '40112': PermissionDenied, # Geo IP blocked + }, + 'broad': { + 'missing or malformed jwt': AuthenticationError, + }, + }, + 'precisionMode': TICK_SIZE, + 'commonCurrencies': { + }, + 'options': { + 'paradexAccount': None, # add {"privateKey": "copy Paradex Private Key from UI", "publicKey": "used when onboard(optional)", "address": "copy Paradex Address from UI"} + 'broker': 'CCXT', + }, + 'features': { + 'spot': None, + 'forSwap': { + 'sandbox': True, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerDirection': True, # todo + 'triggerPriceType': None, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': False, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': False, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': True, # todo + 'iceberg': False, + }, + 'createOrders': None, # todo + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, # todo + 'daysBack': 100000, # todo + 'untilDays': 100000, # todo + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 100, # todo + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 100000, # todo + 'untilDays': 100000, # todo + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchClosedOrders': None, # todo + 'fetchOHLCV': { + 'limit': None, # todo by from/to + }, + }, + 'swap': { + 'linear': { + 'extends': 'forSwap', + }, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + }) + + def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://docs.api.testnet.paradex.trade/#get-system-time-unix-milliseconds + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = self.publicGetSystemTime(params) + # + # { + # "server_time": "1681493415023" + # } + # + return self.safe_integer(response, 'server_time') + + def fetch_status(self, params={}): + """ + the latest known information on the availability of the exchange API + + https://docs.api.testnet.paradex.trade/#get-system-state + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `status structure ` + """ + response = self.publicGetSystemState(params) + # + # { + # "status": "ok" + # } + # + status = self.safe_string(response, 'status') + return { + 'status': 'ok' if (status == 'ok') else 'maintenance', + 'updated': None, + 'eta': None, + 'url': None, + 'info': response, + } + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for bitget + + https://docs.api.testnet.paradex.trade/#list-available-markets + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = self.publicGetMarkets(params) + # + # { + # "results": [ + # { + # "symbol": "BODEN-USD-PERP", + # "base_currency": "BODEN", + # "quote_currency": "USD", + # "settlement_currency": "USDC", + # "order_size_increment": "1", + # "price_tick_size": "0.00001", + # "min_notional": "200", + # "open_at": 1717065600000, + # "expiry_at": 0, + # "asset_kind": "PERP", + # "position_limit": "2000000", + # "price_bands_width": "0.2", + # "max_open_orders": 50, + # "max_funding_rate": "0.05", + # "delta1_cross_margin_params": { + # "imf_base": "0.2", + # "imf_shift": "180000", + # "imf_factor": "0.00071", + # "mmf_factor": "0.5" + # }, + # "price_feed_id": "9LScEHse1ioZt2rUuhwiN6bmYnqpMqvZkQJDNUpxVHN5", + # "oracle_ewma_factor": "0.14999987905913592", + # "max_order_size": "520000", + # "max_funding_rate_change": "0.0005", + # "max_tob_spread": "0.2" + # } + # ] + # } + # + data = self.safe_list(response, 'results') + return self.parse_markets(data) + + def parse_market(self, market: dict) -> Market: + # + # { + # "symbol": "BODEN-USD-PERP", + # "base_currency": "BODEN", + # "quote_currency": "USD", + # "settlement_currency": "USDC", + # "order_size_increment": "1", + # "price_tick_size": "0.00001", + # "min_notional": "200", + # "open_at": 1717065600000, + # "expiry_at": 0, + # "asset_kind": "PERP", + # "position_limit": "2000000", + # "price_bands_width": "0.2", + # "max_open_orders": 50, + # "max_funding_rate": "0.05", + # "delta1_cross_margin_params": { + # "imf_base": "0.2", + # "imf_shift": "180000", + # "imf_factor": "0.00071", + # "mmf_factor": "0.5" + # }, + # "price_feed_id": "9LScEHse1ioZt2rUuhwiN6bmYnqpMqvZkQJDNUpxVHN5", + # "oracle_ewma_factor": "0.14999987905913592", + # "max_order_size": "520000", + # "max_funding_rate_change": "0.0005", + # "max_tob_spread": "0.2" + # } + # + # { + # "symbol":"BTC-USD-96000-C", + # "base_currency":"BTC", + # "quote_currency":"USD", + # "settlement_currency":"USDC", + # "order_size_increment":"0.001", + # "price_tick_size":"0.01", + # "min_notional":"100", + # "open_at":"1736764200000", + # "expiry_at":"0", + # "asset_kind":"PERP_OPTION", + # "market_kind":"cross", + # "position_limit":"10", + # "price_bands_width":"0.05", + # "iv_bands_width":"0.05", + # "max_open_orders":"100", + # "max_funding_rate":"0.02", + # "option_cross_margin_params":{ + # "imf":{ + # "long_itm":"0.2", + # "short_itm":"0.15", + # "short_otm":"0.1", + # "short_put_cap":"0.5", + # "premium_multiplier":"1" + # }, + # "mmf":{ + # "long_itm":"0.1", + # "short_itm":"0.075", + # "short_otm":"0.05", + # "short_put_cap":"0.5", + # "premium_multiplier":"0.5" + # } + # }, + # "price_feed_id":"GVXRSBjFk6e6J3NbVPXohDJetcTjaeeuykUpbQF8UoMU", + # "oracle_ewma_factor":"0.20000046249626113", + # "max_order_size":"2", + # "max_funding_rate_change":"0.02", + # "max_tob_spread":"0.2", + # "interest_rate":"0.0001", + # "clamp_rate":"0.02", + # "option_type":"CALL", + # "strike_price":"96000", + # "funding_period_hours":"24", + # "tags":[ + # ] + # } + # + assetKind = self.safe_string(market, 'asset_kind') + isOption = (assetKind == 'PERP_OPTION') + type = 'option' if (isOption) else 'swap' + isSwap = (type == 'swap') + marketId = self.safe_string(market, 'symbol') + quoteId = self.safe_string(market, 'quote_currency') + baseId = self.safe_string(market, 'base_currency') + quote = self.safe_currency_code(quoteId) + base = self.safe_currency_code(baseId) + settleId = self.safe_string(market, 'settlement_currency') + settle = self.safe_currency_code(settleId) + symbol = base + '/' + quote + ':' + settle + expiry = self.safe_integer(market, 'expiry_at') + optionType = self.safe_string(market, 'option_type') + strikePrice = self.safe_string(market, 'strike_price') + takerFee = self.parse_number('0.0003') + makerFee = self.parse_number('-0.00005') + if isOption: + optionTypeSuffix = 'C' if (optionType == 'CALL') else 'P' + symbol = symbol + '-' + strikePrice + '-' + optionTypeSuffix + makerFee = self.parse_number('0.0003') + else: + expiry = None + return self.safe_market_structure({ + 'id': marketId, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': type, + 'spot': False, + 'margin': None, + 'swap': isSwap, + 'future': False, + 'option': isOption, + 'active': self.safe_bool(market, 'enableTrading'), + 'contract': True, + 'linear': True, + 'inverse': False, + 'taker': takerFee, + 'maker': makerFee, + 'contractSize': self.parse_number('1'), + 'expiry': expiry, + 'expiryDatetime': None if (expiry == 0) else self.iso8601(expiry), + 'strike': self.parse_number(strikePrice), + 'optionType': self.safe_string_lower(market, 'option_type'), + 'precision': { + 'amount': self.safe_number(market, 'order_size_increment'), + 'price': self.safe_number(market, 'price_tick_size'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': None, + 'max': self.safe_number(market, 'max_order_size'), + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': self.safe_number(market, 'min_notional'), + 'max': None, + }, + }, + 'created': None, + 'info': market, + }) + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://docs.api.testnet.paradex.trade/#ohlcv-for-a-symbol + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest candle to fetch + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'resolution': self.safe_string(self.timeframes, timeframe, timeframe), + 'symbol': market['id'], + } + now = self.milliseconds() + duration = self.parse_timeframe(timeframe) + until = self.safe_integer_2(params, 'until', 'till', now) + params = self.omit(params, ['until', 'till']) + if since is not None: + request['start_at'] = since + if limit is not None: + request['end_at'] = self.sum(since, duration * (limit + 1) * 1000) - 1 + else: + request['end_at'] = until + else: + request['end_at'] = until + if limit is not None: + request['start_at'] = until - duration * (limit + 1) * 1000 + 1 + else: + request['start_at'] = until - duration * 101 * 1000 + 1 + response = self.publicGetMarketsKlines(self.extend(request, params)) + # + # { + # "results": [ + # [ + # 1720071900000, + # 58961.3, + # 58961.3, + # 58961.3, + # 58961.3, + # 1591 + # ] + # ] + # } + # + data = self.safe_list(response, 'results', []) + return self.parse_ohlcvs(data, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # [ + # 1720071900000, + # 58961.3, + # 58961.3, + # 58961.3, + # 58961.3, + # 1591 + # ] + # + return [ + self.safe_integer(ohlcv, 0), + self.safe_number(ohlcv, 1), + self.safe_number(ohlcv, 2), + self.safe_number(ohlcv, 3), + self.safe_number(ohlcv, 4), + self.safe_number(ohlcv, 5), + ] + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://docs.api.testnet.paradex.trade/#list-available-markets-summary + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + symbols = self.market_symbols(symbols) + request: dict = { + 'market': 'ALL', + } + response = self.publicGetMarketsSummary(self.extend(request, params)) + # + # { + # "results": [ + # { + # "symbol": "BTC-USD-PERP", + # "oracle_price": "68465.17449906", + # "mark_price": "68465.17449906", + # "last_traded_price": "68495.1", + # "bid": "68477.6", + # "ask": "69578.2", + # "volume_24h": "5815541.397939004", + # "total_volume": "584031465.525259686", + # "created_at": 1718170156580, + # "underlying_price": "67367.37268422", + # "open_interest": "162.272", + # "funding_rate": "0.01629574927887", + # "price_change_rate_24h": "0.009032" + # } + # ] + # } + # + data = self.safe_list(response, 'results', []) + return self.parse_tickers(data, symbols) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://docs.api.testnet.paradex.trade/#list-available-markets-summary + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + response = self.publicGetMarketsSummary(self.extend(request, params)) + # + # { + # "results": [ + # { + # "symbol": "BTC-USD-PERP", + # "oracle_price": "68465.17449906", + # "mark_price": "68465.17449906", + # "last_traded_price": "68495.1", + # "bid": "68477.6", + # "ask": "69578.2", + # "volume_24h": "5815541.397939004", + # "total_volume": "584031465.525259686", + # "created_at": 1718170156580, + # "underlying_price": "67367.37268422", + # "open_interest": "162.272", + # "funding_rate": "0.01629574927887", + # "price_change_rate_24h": "0.009032" + # } + # ] + # } + # + data = self.safe_list(response, 'results', []) + ticker = self.safe_dict(data, 0, {}) + return self.parse_ticker(ticker, market) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "symbol": "BTC-USD-PERP", + # "oracle_price": "68465.17449906", + # "mark_price": "68465.17449906", + # "last_traded_price": "68495.1", + # "bid": "68477.6", + # "ask": "69578.2", + # "volume_24h": "5815541.397939004", + # "total_volume": "584031465.525259686", + # "created_at": 1718170156581, + # "underlying_price": "67367.37268422", + # "open_interest": "162.272", + # "funding_rate": "0.01629574927887", + # "price_change_rate_24h": "0.009032" + # } + # + percentage = self.safe_string(ticker, 'price_change_rate_24h') + if percentage is not None: + percentage = Precise.string_mul(percentage, '100') + last = self.safe_string(ticker, 'last_traded_price') + marketId = self.safe_string(ticker, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + timestamp = self.safe_integer(ticker, 'created_at') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': None, + 'low': None, + 'bid': self.safe_string(ticker, 'bid'), + 'bidVolume': None, + 'ask': self.safe_string(ticker, 'ask'), + 'askVolume': None, + 'vwap': None, + 'open': None, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': percentage, + 'average': None, + 'baseVolume': None, + 'quoteVolume': self.safe_string(ticker, 'volume_24h'), + 'markPrice': self.safe_string(ticker, 'mark_price'), + 'info': ticker, + }, market) + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://docs.api.testnet.paradex.trade/#get-market-orderbook + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = {'market': market['id']} + response = self.publicGetOrderbookMarket(self.extend(request, params)) + # + # { + # "market": "BTC-USD-PERP", + # "seq_no": 14115975, + # "last_updated_at": 1718172538340, + # "asks": [ + # [ + # "69578.2", + # "3.019" + # ] + # ], + # "bids": [ + # [ + # "68477.6", + # "0.1" + # ] + # ] + # } + # + if limit is not None: + request['depth'] = limit + timestamp = self.safe_integer(response, 'last_updated_at') + orderbook = self.parse_order_book(response, market['symbol'], timestamp) + orderbook['nonce'] = self.safe_integer(response, 'seq_no') + return orderbook + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://docs.api.testnet.paradex.trade/#trade-tape + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch trades for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchTrades', 'paginate') + if paginate: + return self.fetch_paginated_call_cursor('fetchTrades', symbol, since, limit, params, 'next', 'cursor', None, 100) + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + if limit is not None: + request['page_size'] = limit + if since is not None: + request['start_at'] = since + request, params = self.handle_until_option('end_at', request, params) + response = self.publicGetTrades(self.extend(request, params)) + # + # { + # "next": "...", + # "prev": "...", + # "results": [ + # { + # "id": "1718154353750201703989430001", + # "market": "BTC-USD-PERP", + # "side": "BUY", + # "size": "0.026", + # "price": "69578.2", + # "created_at": 1718154353750, + # "trade_type": "FILL" + # } + # ] + # } + # + trades = self.safe_list(response, 'results', []) + for i in range(0, len(trades)): + trades[i]['next'] = self.safe_string(response, 'next') + return self.parse_trades(trades, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(public) + # + # { + # "id": "1718154353750201703989430001", + # "market": "BTC-USD-PERP", + # "side": "BUY", + # "size": "0.026", + # "price": "69578.2", + # "created_at": 1718154353750, + # "trade_type": "FILL" + # } + # + # fetchMyTrades(private) + # + # { + # "id": "1718947571560201703986670001", + # "side": "BUY", + # "liquidity": "TAKER", + # "market": "BTC-USD-PERP", + # "order_id": "1718947571540201703992340000", + # "price": "64852.9", + # "size": "0.01", + # "fee": "0.1945587", + # "fee_currency": "USDC", + # "created_at": 1718947571569, + # "remaining_size": "0", + # "client_id": "", + # "fill_type": "FILL" + # } + # + marketId = self.safe_string(trade, 'market') + market = self.safe_market(marketId, market) + id = self.safe_string(trade, 'id') + timestamp = self.safe_integer(trade, 'created_at') + priceString = self.safe_string(trade, 'price') + amountString = self.safe_string(trade, 'size') + side = self.safe_string_lower(trade, 'side') + liability = self.safe_string_lower(trade, 'liquidity', 'taker') + isTaker = liability == 'taker' + takerOrMaker = 'taker' if (isTaker) else 'maker' + currencyId = self.safe_string(trade, 'fee_currency') + code = self.safe_currency_code(currencyId) + return self.safe_trade({ + 'info': trade, + 'id': id, + 'order': self.safe_string(trade, 'order_id'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'type': None, + 'takerOrMaker': takerOrMaker, + 'side': side, + 'price': priceString, + 'amount': amountString, + 'cost': None, + 'fee': { + 'cost': self.safe_string(trade, 'fee'), + 'currency': code, + 'rate': None, + }, + }, market) + + def fetch_open_interest(self, symbol: str, params={}): + """ + retrieves the open interest of a contract trading pair + + https://docs.api.testnet.paradex.trade/#list-available-markets-summary + + :param str symbol: unified CCXT market symbol + :param dict [params]: exchange specific parameters + :returns dict} an open interest structure{@link https://docs.ccxt.com/#/?id=open-interest-structure: + """ + self.load_markets() + market = self.market(symbol) + if not market['contract']: + raise BadRequest(self.id + ' fetchOpenInterest() supports contract markets only') + request: dict = { + 'market': market['id'], + } + response = self.publicGetMarketsSummary(self.extend(request, params)) + # + # { + # "results": [ + # { + # "symbol": "BTC-USD-PERP", + # "oracle_price": "68465.17449906", + # "mark_price": "68465.17449906", + # "last_traded_price": "68495.1", + # "bid": "68477.6", + # "ask": "69578.2", + # "volume_24h": "5815541.397939004", + # "total_volume": "584031465.525259686", + # "created_at": 1718170156580, + # "underlying_price": "67367.37268422", + # "open_interest": "162.272", + # "funding_rate": "0.01629574927887", + # "price_change_rate_24h": "0.009032" + # } + # ] + # } + # + data = self.safe_list(response, 'results', []) + interest = self.safe_dict(data, 0, {}) + return self.parse_open_interest(interest, market) + + def parse_open_interest(self, interest, market: Market = None): + # + # { + # "symbol": "BTC-USD-PERP", + # "oracle_price": "68465.17449904", + # "mark_price": "68465.17449906", + # "last_traded_price": "68495.1", + # "bid": "68477.6", + # "ask": "69578.2", + # "volume_24h": "5815541.397939004", + # "total_volume": "584031465.525259686", + # "created_at": 1718170156580, + # "underlying_price": "67367.37268422", + # "open_interest": "162.272", + # "funding_rate": "0.01629574927887", + # "price_change_rate_24h": "0.009032" + # } + # + timestamp = self.safe_integer(interest, 'created_at') + marketId = self.safe_string(interest, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + return self.safe_open_interest({ + 'symbol': symbol, + 'openInterestAmount': self.safe_string(interest, 'open_interest'), + 'openInterestValue': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'info': interest, + }, market) + + def hash_message(self, message): + return '0x' + self.hash(message, 'keccak', 'hex') + + def sign_hash(self, hash, privateKey): + signature = self.ecdsa(hash[-64:], privateKey[-64:], 'secp256k1', None) + r = signature['r'] + s = signature['s'] + v = self.int_to_base16(self.sum(27, signature['v'])) + return '0x' + r.rjust(64, '0') + s.rjust(64, '0') + v + + def sign_message(self, message, privateKey): + return self.sign_hash(self.hash_message(message), privateKey[-64:]) + + def get_system_config(self): + cachedConfig: dict = self.safe_dict(self.options, 'systemConfig') + if cachedConfig is not None: + return cachedConfig + response = self.publicGetSystemConfig() + # + # { + # "starknet_gateway_url": "https://potc-testnet-sepolia.starknet.io", + # "starknet_fullnode_rpc_url": "https://pathfinder.api.testnet.paradex.trade/rpc/v0_7", + # "starknet_chain_id": "PRIVATE_SN_POTC_SEPOLIA", + # "block_explorer_url": "https://voyager.testnet.paradex.trade/", + # "paraclear_address": "0x286003f7c7bfc3f94e8f0af48b48302e7aee2fb13c23b141479ba00832ef2c6", + # "paraclear_decimals": 8, + # "paraclear_account_proxy_hash": "0x3530cc4759d78042f1b543bf797f5f3d647cde0388c33734cf91b7f7b9314a9", + # "paraclear_account_hash": "0x41cb0280ebadaa75f996d8d92c6f265f6d040bb3ba442e5f86a554f1765244e", + # "oracle_address": "0x2c6a867917ef858d6b193a0ff9e62b46d0dc760366920d631715d58baeaca1f", + # "bridged_tokens": [ + # { + # "name": "TEST USDC", + # "symbol": "USDC", + # "decimals": 6, + # "l1_token_address": "0x29A873159D5e14AcBd63913D4A7E2df04570c666", + # "l1_bridge_address": "0x8586e05adc0C35aa11609023d4Ae6075Cb813b4C", + # "l2_token_address": "0x6f373b346561036d98ea10fb3e60d2f459c872b1933b50b21fe6ef4fda3b75e", + # "l2_bridge_address": "0x46e9237f5408b5f899e72125dd69bd55485a287aaf24663d3ebe00d237fc7ef" + # } + # ], + # "l1_core_contract_address": "0x582CC5d9b509391232cd544cDF9da036e55833Af", + # "l1_operator_address": "0x11bACdFbBcd3Febe5e8CEAa75E0Ef6444d9B45FB", + # "l1_chain_id": "11155111", + # "liquidation_fee": "0.2" + # } + # + self.options['systemConfig'] = response + return response + + def prepare_paradex_domain(self, l1=False): + systemConfig = self.get_system_config() + if l1 is True: + l1D = { + 'name': 'Paradex', + 'chainId': systemConfig['l1_chain_id'], + 'version': '1', + } + return l1D + domain = { + 'name': 'Paradex', + 'chainId': systemConfig['starknet_chain_id'], + 'version': 1, + } + return domain + + def retrieve_account(self): + cachedAccount: dict = self.safe_dict(self.options, 'paradexAccount') + if cachedAccount is not None: + return cachedAccount + self.check_required_credentials() + systemConfig = self.get_system_config() + domain = self.prepare_paradex_domain(True) + messageTypes = { + 'Constant': [ + {'name': 'action', 'type': 'string'}, + ], + } + message = { + 'action': 'STARK Key', + } + msg = self.eth_encode_structured_data(domain, messageTypes, message) + signature = self.sign_message(msg, self.privateKey) + account = self.retrieve_stark_account( + signature, + systemConfig['paraclear_account_hash'], + systemConfig['paraclear_account_proxy_hash'] + ) + self.options['paradexAccount'] = account + return account + + def onboarding(self, params={}): + account = self.retrieve_account() + req = { + 'action': 'Onboarding', + } + domain = self.prepare_paradex_domain() + messageTypes = { + 'Constant': [ + {'name': 'action', 'type': 'felt'}, + ], + } + msg = self.starknet_encode_structured_data(domain, messageTypes, req, account['address']) + signature = self.starknet_sign(msg, account['privateKey']) + params['signature'] = signature + params['account'] = account['address'] + params['public_key'] = account['publicKey'] + response = self.privatePostOnboarding(params) + return response + + def authenticate_rest(self, params={}): + cachedToken = self.safe_string(self.options, 'authToken') + now = self.nonce() + if cachedToken is not None: + cachedExpires = self.safe_integer(self.options, 'expires') + if now < cachedExpires: + return cachedToken + account = self.retrieve_account() + # https://docs.paradex.trade/api-reference/general-information/authentication + expires = now + 180 + req = { + 'method': 'POST', + 'path': '/v1/auth', + 'body': '', + 'timestamp': now, + 'expiration': expires, + } + domain = self.prepare_paradex_domain() + messageTypes = { + 'Request': [ + {'name': 'method', 'type': 'felt'}, + {'name': 'path', 'type': 'felt'}, + {'name': 'body', 'type': 'felt'}, + {'name': 'timestamp', 'type': 'felt'}, + {'name': 'expiration', 'type': 'felt'}, + ], + } + msg = self.starknet_encode_structured_data(domain, messageTypes, req, account['address']) + signature = self.starknet_sign(msg, account['privateKey']) + params['signature'] = signature + params['account'] = account['address'] + params['timestamp'] = req['timestamp'] + params['expiration'] = req['expiration'] + response = self.privatePostAuth(params) + # + # { + # jwt_token: "ooooccxtooootoooootheoooomoonooooo" + # } + # + token = self.safe_string(response, 'jwt_token') + self.options['authToken'] = token + self.options['expires'] = expires + return token + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # { + # "account": "0x4638e3041366aa71720be63e32e53e1223316c7f0d56f7aa617542ed1e7512x", + # "avg_fill_price": "26000", + # "client_id": "x1234", + # "cancel_reason": "NOT_ENOUGH_MARGIN", + # "created_at": 1681493746016, + # "flags": [ + # "REDUCE_ONLY" + # ], + # "id": "123456", + # "instruction": "GTC", + # "last_updated_at": 1681493746016, + # "market": "BTC-USD-PERP", + # "price": "26000", + # "published_at": 1681493746016, + # "received_at": 1681493746016, + # "remaining_size": "0", + # "seq_no": 1681471234972000000, + # "side": "BUY", + # "size": "0.05", + # "status": "NEW", + # "stp": "EXPIRE_MAKER", + # "timestamp": 1681493746016, + # "trigger_price": "26000", + # "type": "MARKET" + # } + # + timestamp = self.safe_integer(order, 'created_at') + orderId = self.safe_string(order, 'id') + clientOrderId = self.omit_zero(self.safe_string(order, 'client_id')) + marketId = self.safe_string(order, 'market') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + price = self.safe_string(order, 'price') + amount = self.safe_string(order, 'size') + orderType = self.safe_string(order, 'type') + cancelReason = self.safe_string(order, 'cancel_reason') + status = self.safe_string(order, 'status') + if cancelReason is not None: + if cancelReason == 'NOT_ENOUGH_MARGIN' or cancelReason == 'ORDER_EXCEEDS_POSITION_LIMIT': + status = 'rejected' + else: + status = 'canceled' + side = self.safe_string_lower(order, 'side') + average = self.omit_zero(self.safe_string(order, 'avg_fill_price')) + remaining = self.omit_zero(self.safe_string(order, 'remaining_size')) + lastUpdateTimestamp = self.safe_integer(order, 'last_updated_at') + flags = self.safe_list(order, 'flags', []) + reduceOnly = None + if 'REDUCE_ONLY' in flags: + reduceOnly = True + return self.safe_order({ + 'id': orderId, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'lastUpdateTimestamp': lastUpdateTimestamp, + 'status': self.parse_order_status(status), + 'symbol': symbol, + 'type': self.parse_order_type(orderType), + 'timeInForce': self.parse_time_in_force(self.safe_string(order, 'instruction')), + 'postOnly': None, + 'reduceOnly': reduceOnly, + 'side': side, + 'price': price, + 'triggerPrice': self.safe_string(order, 'trigger_price'), + 'takeProfitPrice': None, + 'stopLossPrice': None, + 'average': average, + 'amount': amount, + 'filled': None, + 'remaining': remaining, + 'cost': None, + 'trades': None, + 'fee': { + 'cost': None, + 'currency': None, + }, + 'info': order, + }, market) + + def parse_time_in_force(self, timeInForce: Str): + timeInForces: dict = { + 'IOC': 'IOC', + 'GTC': 'GTC', + 'POST_ONLY': 'PO', + } + return self.safe_string(timeInForces, timeInForce, None) + + def parse_order_status(self, status: Str): + if status is not None: + statuses: dict = { + 'NEW': 'open', + 'UNTRIGGERED': 'open', + 'OPEN': 'open', + 'CLOSED': 'closed', + } + return self.safe_string(statuses, status, status) + return status + + def parse_order_type(self, type: Str): + types: dict = { + 'LIMIT': 'limit', + 'MARKET': 'market', + 'STOP_LIMIT': 'limit', + 'STOP_MARKET': 'market', + } + return self.safe_string_lower(types, type, type) + + def convert_short_string(self, str: str): + # TODO: add stringToBase16 in exchange + return '0x' + self.binary_to_base16(self.base64_to_binary(self.string_to_base64(str))) + + def scale_number(self, num: str): + return Precise.string_mul(num, '100000000') + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://docs.api.prod.paradex.trade/#create-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fullfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.stopPrice]: alias for triggerPrice + :param float [params.triggerPrice]: The price a trigger order is triggered at + :param float [params.stopLossPrice]: the price that a stop loss order is triggered at + :param float [params.takeProfitPrice]: the price that a take profit order is triggered at + :param str [params.timeInForce]: "GTC", "IOC", or "POST_ONLY" + :param bool [params.postOnly]: True or False + :param bool [params.reduceOnly]: Ensures that the executed order does not flip the opened position. + :param str [params.clientOrderId]: a unique id for the order + :returns dict: an `order structure ` + """ + self.authenticate_rest() + self.load_markets() + market = self.market(symbol) + reduceOnly = self.safe_bool_2(params, 'reduceOnly', 'reduce_only') + orderType = type.upper() + orderSide = side.upper() + request: dict = { + 'market': market['id'], + 'side': orderSide, + 'type': orderType, # LIMIT/MARKET/STOP_LIMIT/STOP_MARKET,STOP_LOSS_MARKET,STOP_LOSS_LIMIT,TAKE_PROFIT_MARKET,TAKE_PROFIT_LIMIT + } + triggerPrice = self.safe_string_2(params, 'triggerPrice', 'stopPrice') + stopLossPrice = self.safe_string(params, 'stopLossPrice') + takeProfitPrice = self.safe_string(params, 'takeProfitPrice') + isMarket = orderType == 'MARKET' + isTakeProfitOrder = (takeProfitPrice is not None) + isStopLossOrder = (stopLossPrice is not None) + isStopOrder = (triggerPrice is not None) or isTakeProfitOrder or isStopLossOrder + timeInForce = self.safe_string_upper(params, 'timeInForce') + postOnly = self.is_post_only(isMarket, None, params) + if not isMarket: + if postOnly: + request['instruction'] = 'POST_ONLY' + elif timeInForce == 'ioc': + request['instruction'] = 'IOC' + if price is not None: + request['price'] = self.price_to_precision(symbol, price) + clientOrderId = self.safe_string_n(params, ['clOrdID', 'clientOrderId', 'client_order_id']) + if clientOrderId is not None: + request['client_id'] = clientOrderId + sizeString = '0' + stopPrice = None + if isStopOrder: + # flags: Reduce_Only must be provided for TPSL orders. + if isMarket: + if isStopLossOrder: + stopPrice = self.price_to_precision(symbol, stopLossPrice) + reduceOnly = True + request['type'] = 'STOP_LOSS_MARKET' + elif isTakeProfitOrder: + stopPrice = self.price_to_precision(symbol, takeProfitPrice) + reduceOnly = True + request['type'] = 'TAKE_PROFIT_MARKET' + else: + stopPrice = self.price_to_precision(symbol, triggerPrice) + sizeString = self.amount_to_precision(symbol, amount) + request['type'] = 'STOP_MARKET' + else: + if isStopLossOrder: + stopPrice = self.price_to_precision(symbol, stopLossPrice) + reduceOnly = True + request['type'] = 'STOP_LOSS_LIMIT' + elif isTakeProfitOrder: + stopPrice = self.price_to_precision(symbol, takeProfitPrice) + reduceOnly = True + request['type'] = 'TAKE_PROFIT_LIMIT' + else: + stopPrice = self.price_to_precision(symbol, triggerPrice) + sizeString = self.amount_to_precision(symbol, amount) + request['type'] = 'STOP_LIMIT' + else: + sizeString = self.amount_to_precision(symbol, amount) + if stopPrice is not None: + request['trigger_price'] = stopPrice + request['size'] = sizeString + if reduceOnly: + request['flags'] = [ + 'REDUCE_ONLY', + ] + params = self.omit(params, ['reduceOnly', 'reduce_only', 'clOrdID', 'clientOrderId', 'client_order_id', 'postOnly', 'timeInForce', 'stopPrice', 'triggerPrice', 'stopLossPrice', 'takeProfitPrice']) + account = self.retrieve_account() + now = self.nonce() + orderReq = { + 'timestamp': now * 1000, + 'market': self.convert_short_string(request['market']), + 'side': '1' if (orderSide == 'BUY') else '2', + 'orderType': self.convert_short_string(request['type']), + 'size': self.scale_number(request['size']), + 'price': '0' if (isMarket) else self.scale_number(request['price']), + } + domain = self.prepare_paradex_domain() + messageTypes = { + 'Order': [ + {'name': 'timestamp', 'type': 'felt'}, + {'name': 'market', 'type': 'felt'}, + {'name': 'side', 'type': 'felt'}, + {'name': 'orderType', 'type': 'felt'}, + {'name': 'size', 'type': 'felt'}, + {'name': 'price', 'type': 'felt'}, + ], + } + msg = self.starknet_encode_structured_data(domain, messageTypes, orderReq, account['address']) + signature = self.starknet_sign(msg, account['privateKey']) + request['signature'] = signature + request['signature_timestamp'] = orderReq['timestamp'] + response = self.privatePostOrders(self.extend(request, params)) + # + # { + # "account": "0x4638e3041366aa71720be63e32e53e1223316c7f0d56f7aa617542ed1e7512x", + # "avg_fill_price": "26000", + # "cancel_reason": "NOT_ENOUGH_MARGIN", + # "client_id": "x1234", + # "created_at": 1681493746016, + # "flags": [ + # "REDUCE_ONLY" + # ], + # "id": "123456", + # "instruction": "GTC", + # "last_updated_at": 1681493746016, + # "market": "BTC-USD-PERP", + # "price": "26000", + # "published_at": 1681493746016, + # "received_at": 1681493746016, + # "remaining_size": "0", + # "seq_no": 1681471234972000000, + # "side": "BUY", + # "size": "0.05", + # "status": "NEW", + # "stp": "EXPIRE_MAKER", + # "timestamp": 1681493746016, + # "trigger_price": "26000", + # "type": "MARKET" + # } + # + order = self.parse_order(response, market) + return order + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://docs.api.prod.paradex.trade/#cancel-order + https://docs.api.prod.paradex.trade/#cancel-open-order-by-client-order-id + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.clientOrderId]: a unique id for the order + :returns dict: An `order structure ` + """ + self.authenticate_rest() + self.load_markets() + request: dict = {} + clientOrderId = self.safe_string_n(params, ['clOrdID', 'clientOrderId', 'client_order_id']) + response = None + if clientOrderId is not None: + request['client_id'] = clientOrderId + response = self.privateDeleteOrdersByClientIdClientId(self.extend(request, params)) + else: + request['order_id'] = id + response = self.privateDeleteOrdersOrderId(self.extend(request, params)) + # + # if success, no response... + # + return self.parse_order(response) + + def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders in a market + + https://docs.api.prod.paradex.trade/#cancel-all-open-orders + + :param str symbol: unified market symbol of the market to cancel orders in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelAllOrders() requires a symbol argument') + self.authenticate_rest() + self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + response = self.privateDeleteOrders(self.extend(request, params)) + # + # if success, no response... + # + return [self.safe_order({'info': response})] + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://docs.api.prod.paradex.trade/#get-order + https://docs.api.prod.paradex.trade/#get-order-by-client-id + + :param str id: the order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.clientOrderId]: a unique id for the order + :returns dict: An `order structure ` + """ + self.authenticate_rest() + self.load_markets() + request: dict = {} + clientOrderId = self.safe_string_n(params, ['clOrdID', 'clientOrderId', 'client_order_id']) + params = self.omit(params, ['clOrdID', 'clientOrderId', 'client_order_id']) + response = None + if clientOrderId is not None: + request['client_id'] = clientOrderId + response = self.privateGetOrdersByClientIdClientId(self.extend(request, params)) + else: + request['order_id'] = id + response = self.privateGetOrdersOrderId(self.extend(request, params)) + # + # { + # "id": "1718941725080201704028870000", + # "account": "0x49ddd7a564c978f6e4089ff8355b56a42b7e2d48ba282cb5aad60f04bea0ec3", + # "market": "BTC-USD-PERP", + # "side": "SELL", + # "type": "LIMIT", + # "size": "10.153", + # "remaining_size": "10.153", + # "price": "70784.5", + # "status": "CLOSED", + # "created_at": 1718941725082, + # "last_updated_at": 1718958002991, + # "timestamp": 1718941724678, + # "cancel_reason": "USER_CANCELED", + # "client_id": "", + # "seq_no": 1718958002991595738, + # "instruction": "GTC", + # "avg_fill_price": "", + # "stp": "EXPIRE_TAKER", + # "received_at": 1718958510959, + # "published_at": 1718958510960, + # "flags": [], + # "trigger_price": "0" + # } + # + return self.parse_order(response) + + def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://docs.api.prod.paradex.trade/#get-orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.side]: 'buy' or 'sell' + :param boolean [params.paginate]: set to True if you want to fetch orders with pagination + :param int params['until']: timestamp in ms of the latest order to fetch + :returns Order[]: a list of `order structures ` + """ + self.authenticate_rest() + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOrders', 'paginate') + if paginate: + return self.fetch_paginated_call_cursor('fetchOrders', symbol, since, limit, params, 'next', 'cursor', None, 50) + request: dict = {} + market: Market = None + if symbol is not None: + market = self.market(symbol) + request['market'] = market['id'] + if since is not None: + request['start_at'] = since + if limit is not None: + request['page_size'] = limit + request, params = self.handle_until_option('end_at', request, params) + response = self.privateGetOrdersHistory(self.extend(request, params)) + # + # { + # "next": "eyJmaWx0ZXIiMsIm1hcmtlciI6eyJtYXJrZXIiOiIxNjc1NjUwMDE3NDMxMTAxNjk5N=", + # "prev": "eyJmaWx0ZXIiOnsiTGltaXQiOjkwfSwidGltZSI6MTY4MTY3OTgzNzk3MTMwOTk1MywibWFya2VyIjp7Im1zMjExMD==", + # "results": [ + # { + # "account": "0x4638e3041366aa71720be63e32e53e1223316c7f0d56f7aa617542ed1e7512x", + # "avg_fill_price": "26000", + # "cancel_reason": "NOT_ENOUGH_MARGIN", + # "client_id": "x1234", + # "created_at": 1681493746016, + # "flags": [ + # "REDUCE_ONLY" + # ], + # "id": "123456", + # "instruction": "GTC", + # "last_updated_at": 1681493746016, + # "market": "BTC-USD-PERP", + # "price": "26000", + # "published_at": 1681493746016, + # "received_at": 1681493746016, + # "remaining_size": "0", + # "seq_no": 1681471234972000000, + # "side": "BUY", + # "size": "0.05", + # "status": "NEW", + # "stp": "EXPIRE_MAKER", + # "timestamp": 1681493746016, + # "trigger_price": "26000", + # "type": "MARKET" + # } + # ] + # } + # + orders = self.safe_list(response, 'results', []) + paginationCursor = self.safe_string(response, 'next') + ordersLength = len(orders) + if (paginationCursor is not None) and (ordersLength > 0): + first = orders[0] + first['next'] = paginationCursor + orders[0] = first + return self.parse_orders(orders, market, since, limit) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://docs.api.prod.paradex.trade/#paradex-rest-api-orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + self.authenticate_rest() + self.load_markets() + request: dict = {} + market: Market = None + if symbol is not None: + market = self.market(symbol) + request['market'] = market['id'] + response = self.privateGetOrders(self.extend(request, params)) + # + # { + # "results": [ + # { + # "account": "0x4638e3041366aa71720be63e32e53e1223316c7f0d56f7aa617542ed1e7512x", + # "avg_fill_price": "26000", + # "client_id": "x1234", + # "cancel_reason": "NOT_ENOUGH_MARGIN", + # "created_at": 1681493746016, + # "flags": [ + # "REDUCE_ONLY" + # ], + # "id": "123456", + # "instruction": "GTC", + # "last_updated_at": 1681493746016, + # "market": "BTC-USD-PERP", + # "price": "26000", + # "published_at": 1681493746016, + # "received_at": 1681493746016, + # "remaining_size": "0", + # "seq_no": 1681471234972000000, + # "side": "BUY", + # "size": "0.05", + # "status": "NEW", + # "stp": "EXPIRE_MAKER", + # "timestamp": 1681493746016, + # "trigger_price": "26000", + # "type": "MARKET" + # } + # ] + # } + # + orders = self.safe_list(response, 'results', []) + return self.parse_orders(orders, market, since, limit) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://docs.api.prod.paradex.trade/#list-balances + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.authenticate_rest() + self.load_markets() + response = self.privateGetBalance() + # + # { + # "results": [ + # { + # "token": "USDC", + # "size": "99980.2382266290601", + # "last_updated_at": 1718529757240 + # } + # ] + # } + # + data = self.safe_list(response, 'results', []) + return self.parse_balance(data) + + def parse_balance(self, response) -> Balances: + result: dict = {'info': response} + for i in range(0, len(response)): + balance = self.safe_dict(response, i, {}) + currencyId = self.safe_string(balance, 'token') + code = self.safe_currency_code(currencyId) + account = self.account() + account['total'] = self.safe_string(balance, 'size') + result[code] = account + return self.safe_balance(result) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://docs.api.prod.paradex.trade/#list-fills + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param int [params.until]: the latest time in ms to fetch entries for + :returns Trade[]: a list of `trade structures ` + """ + self.authenticate_rest() + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchMyTrades', 'paginate') + if paginate: + return self.fetch_paginated_call_cursor('fetchMyTrades', symbol, since, limit, params, 'next', 'cursor', None, 100) + request: dict = {} + market: Market = None + if symbol is not None: + market = self.market(symbol) + request['market'] = market['id'] + if limit is not None: + request['page_size'] = limit + if since is not None: + request['start_at'] = since + request, params = self.handle_until_option('end_at', request, params) + response = self.privateGetFills(self.extend(request, params)) + # + # { + # "next": null, + # "prev": null, + # "results": [ + # { + # "id": "1718947571560201703986670001", + # "side": "BUY", + # "liquidity": "TAKER", + # "market": "BTC-USD-PERP", + # "order_id": "1718947571540201703992340000", + # "price": "64852.9", + # "size": "0.01", + # "fee": "0.1945587", + # "fee_currency": "USDC", + # "created_at": 1718947571569, + # "remaining_size": "0", + # "client_id": "", + # "fill_type": "FILL" + # } + # ] + # } + # + trades = self.safe_list(response, 'results', []) + for i in range(0, len(trades)): + trades[i]['next'] = self.safe_string(response, 'next') + return self.parse_trades(trades, market, since, limit) + + def fetch_position(self, symbol: str, params={}): + """ + fetch data on an open position + + https://docs.api.prod.paradex.trade/#list-open-positions + + :param str symbol: unified market symbol of the market the position is held in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `position structure ` + """ + self.authenticate_rest() + self.load_markets() + market = self.market(symbol) + positions = self.fetch_positions([market['symbol']], params) + return self.safe_dict(positions, 0, {}) + + def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://docs.api.prod.paradex.trade/#list-open-positions + + :param str[] [symbols]: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + self.authenticate_rest() + self.load_markets() + symbols = self.market_symbols(symbols) + response = self.privateGetPositions() + # + # { + # "results": [ + # { + # "id": "0x49ddd7a564c978f6e4089ff8355b56a42b7e2d48ba282cb5aad60f04bea0ec3-BTC-USD-PERP", + # "market": "BTC-USD-PERP", + # "status": "OPEN", + # "side": "LONG", + # "size": "0.01", + # "average_entry_price": "64839.96053748", + # "average_entry_price_usd": "64852.9", + # "realized_pnl": "0", + # "unrealized_pnl": "-2.39677214", + # "unrealized_funding_pnl": "-0.11214013", + # "cost": "648.39960537", + # "cost_usd": "648.529", + # "cached_funding_index": "35202.1002351", + # "last_updated_at": 1718950074249, + # "last_fill_id": "1718947571560201703986670001", + # "seq_no": 1718950074249176253, + # "liquidation_price": "" + # } + # ] + # } + # + data = self.safe_list(response, 'results', []) + return self.parse_positions(data, symbols) + + def parse_position(self, position: dict, market: Market = None): + # + # { + # "id": "0x49ddd7a564c978f6e4089ff8355b56a42b7e2d48ba282cb5aad60f04bea0ec3-BTC-USD-PERP", + # "market": "BTC-USD-PERP", + # "status": "OPEN", + # "side": "LONG", + # "size": "0.01", + # "average_entry_price": "64839.96053748", + # "average_entry_price_usd": "64852.9", + # "realized_pnl": "0", + # "unrealized_pnl": "-2.39677214", + # "unrealized_funding_pnl": "-0.11214013", + # "cost": "648.39960537", + # "cost_usd": "648.529", + # "cached_funding_index": "35202.1002351", + # "last_updated_at": 1718950074249, + # "last_fill_id": "1718947571560201703986670001", + # "seq_no": 1718950074249176253, + # "liquidation_price": "" + # } + # + marketId = self.safe_string(position, 'market') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + side = self.safe_string_lower(position, 'side') + quantity = self.safe_string(position, 'size') + if side != 'long': + quantity = Precise.string_mul('-1', quantity) + timestamp = self.safe_integer(position, 'time') + return self.safe_position({ + 'info': position, + 'id': self.safe_string(position, 'id'), + 'symbol': symbol, + 'entryPrice': self.safe_string(position, 'average_entry_price'), + 'markPrice': None, + 'notional': None, + 'collateral': self.safe_string(position, 'cost'), + 'unrealizedPnl': self.safe_string(position, 'unrealized_pnl'), + 'side': side, + 'contracts': self.parse_number(quantity), + 'contractSize': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'hedged': None, + 'maintenanceMargin': None, + 'maintenanceMarginPercentage': None, + 'initialMargin': None, + 'initialMarginPercentage': None, + 'leverage': None, + 'liquidationPrice': None, + 'marginRatio': None, + 'marginMode': None, + 'percentage': None, + }) + + def fetch_liquidations(self, symbol: str, since: Int = None, limit: Int = None, params={}): + """ + retrieves the public liquidations of a trading pair + + https://docs.api.prod.paradex.trade/#list-liquidations + + :param str symbol: unified CCXT market symbol + :param int [since]: the earliest time in ms to fetch liquidations for + :param int [limit]: the maximum number of liquidation structures to retrieve + :param dict [params]: exchange specific parameters for the huobi api endpoint + :param int [params.until]: timestamp in ms of the latest liquidation + :returns dict: an array of `liquidation structures ` + """ + self.authenticate_rest() + request: dict = {} + if since is not None: + request['from'] = since + else: + request['from'] = 1 + market = self.market(symbol) + request, params = self.handle_until_option('to', request, params) + response = self.privateGetLiquidations(self.extend(request, params)) + # + # { + # "results": [ + # { + # "created_at": 1697213130097, + # "id": "0x123456789" + # } + # ] + # } + # + data = self.safe_list(response, 'results', []) + return self.parse_liquidations(data, market, since, limit) + + def parse_liquidation(self, liquidation, market: Market = None): + # + # { + # "created_at": 1697213130097, + # "id": "0x123456789" + # } + # + timestamp = self.safe_integer(liquidation, 'created_at') + return self.safe_liquidation({ + 'info': liquidation, + 'symbol': None, + 'contracts': None, + 'contractSize': None, + 'price': None, + 'side': None, + 'baseValue': None, + 'quoteValue': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://docs.api.prod.paradex.trade/#paradex-rest-api-transfers + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch entries for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `transaction structures ` + """ + self.authenticate_rest() + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchDeposits', 'paginate') + if paginate: + return self.fetch_paginated_call_cursor('fetchDeposits', code, since, limit, params, 'next', 'cursor', None, 100) + request: dict = {} + if limit is not None: + request['page_size'] = limit + if since is not None: + request['start_at'] = since + request, params = self.handle_until_option('end_at', request, params) + response = self.privateGetTransfers(self.extend(request, params)) + # + # { + # "next": null, + # "prev": null, + # "results": [ + # { + # "id": "1718940471200201703989430000", + # "account": "0x49ddd7a564c978f6e4089ff8355b56a42b7e2d48ba282cb5aad60f04bea0ec3", + # "kind": "DEPOSIT", + # "status": "COMPLETED", + # "amount": "100000", + # "token": "USDC", + # "created_at": 1718940471208, + # "last_updated_at": 1718941455546, + # "txn_hash": "0x73a415ca558a97bbdcd1c43e52b45f1e0486a0a84b3bb4958035ad6c59cb866", + # "external_txn_hash": "", + # "socialized_loss_factor": "" + # } + # ] + # } + # + rows = self.safe_list(response, 'results', []) + deposits = [] + for i in range(0, len(rows)): + row = rows[i] + if row['kind'] == 'DEPOSIT': + deposits.append(row) + return self.parse_transactions(deposits, None, since, limit) + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://docs.api.prod.paradex.trade/#paradex-rest-api-transfers + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch withdrawals for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `transaction structures ` + """ + self.authenticate_rest() + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchWithdrawals', 'paginate') + if paginate: + return self.fetch_paginated_call_cursor('fetchWithdrawals', code, since, limit, params, 'next', 'cursor', None, 100) + request: dict = {} + if limit is not None: + request['page_size'] = limit + if since is not None: + request['start_at'] = since + request, params = self.handle_until_option('end_at', request, params) + response = self.privateGetTransfers(self.extend(request, params)) + # + # { + # "next": null, + # "prev": null, + # "results": [ + # { + # "id": "1718940471200201703989430000", + # "account": "0x49ddd7a564c978f6e4089ff8355b56a42b7e2d48ba282cb5aad60f04bea0ec3", + # "kind": "DEPOSIT", + # "status": "COMPLETED", + # "amount": "100000", + # "token": "USDC", + # "created_at": 1718940471208, + # "last_updated_at": 1718941455546, + # "txn_hash": "0x73a415ca558a97bbdcd1c43e52b45f1e0486a0a84b3bb4958035ad6c59cb866", + # "external_txn_hash": "", + # "socialized_loss_factor": "" + # } + # ] + # } + # + rows = self.safe_list(response, 'results', []) + deposits = [] + for i in range(0, len(rows)): + row = rows[i] + if row['kind'] == 'WITHDRAWAL': + deposits.append(row) + return self.parse_transactions(deposits, None, since, limit) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fetchDeposits & fetchWithdrawals + # + # { + # "id": "1718940471200201703989430000", + # "account": "0x49ddd7a564c978f6e4089ff8355b56a42b7e2d48ba282cb5aad60f04bea0ec3", + # "kind": "DEPOSIT", + # "status": "COMPLETED", + # "amount": "100000", + # "token": "USDC", + # "created_at": 1718940471208, + # "last_updated_at": 1718941455546, + # "txn_hash": "0x73a415ca558a97bbdcd1c43e52b45f1e0486a0a84b3bb4958035ad6c59cb866", + # "external_txn_hash": "", + # "socialized_loss_factor": "" + # } + # + id = self.safe_string(transaction, 'id') + address = self.safe_string(transaction, 'account') + txid = self.safe_string(transaction, 'txn_hash') + currencyId = self.safe_string(transaction, 'token') + code = self.safe_currency_code(currencyId, currency) + timestamp = self.safe_integer(transaction, 'created_at') + updated = self.safe_integer(transaction, 'last_updated_at') + type = self.safe_string(transaction, 'kind') + type = 'deposit' if (type == 'DEPOSIT') else 'withdrawal' + status = self.parse_transaction_status(self.safe_string(transaction, 'status')) + amount = self.safe_number(transaction, 'amount') + return { + 'info': transaction, + 'id': id, + 'txid': txid, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': None, + 'address': address, + 'addressTo': address, + 'addressFrom': None, + 'tag': None, + 'tagTo': None, + 'tagFrom': None, + 'type': type, + 'amount': amount, + 'currency': code, + 'status': status, + 'updated': updated, + 'internal': None, + 'comment': None, + 'fee': None, + } + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'PENDING': 'pending', + 'AVAILABLE': 'pending', + 'COMPLETED': 'ok', + 'FAILED': 'failed', + } + return self.safe_string(statuses, status, status) + + def fetch_margin_mode(self, symbol: str, params={}) -> MarginMode: + """ + fetches the margin mode of a specific symbol + + https://docs.api.testnet.paradex.trade/#get-account-margin-configuration + + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin mode structure ` + """ + self.authenticate_rest() + self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + response = self.privateGetAccountMargin(self.extend(request, params)) + # + # { + # "account": "0x6343248026a845b39a8a73fbe9c7ef0a841db31ed5c61ec1446aa9d25e54dbc", + # "configs": [ + # { + # "market": "SOL-USD-PERP", + # "leverage": 50, + # "margin_type": "CROSS" + # } + # ] + # } + # + configs = self.safe_list(response, 'configs') + return self.parse_margin_mode(self.safe_dict(configs, 0), market) + + def parse_margin_mode(self, rawMarginMode: dict, market=None) -> MarginMode: + marketId = self.safe_string(rawMarginMode, 'market') + market = self.safe_market(marketId, market) + marginMode = self.safe_string_lower(rawMarginMode, 'margin_type') + return { + 'info': rawMarginMode, + 'symbol': market['symbol'], + 'marginMode': marginMode, + } + + def set_margin_mode(self, marginMode: str, symbol: Str = None, params={}): + """ + set margin mode to 'cross' or 'isolated' + + https://docs.api.testnet.paradex.trade/#set-margin-configuration + + :param str marginMode: 'cross' or 'isolated' + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.leverage]: the rate of leverage + :returns dict: response from the exchange + """ + self.check_required_argument('setMarginMode', symbol, 'symbol') + self.authenticate_rest() + self.load_markets() + market: Market = self.market(symbol) + leverage: Str = None + leverage, params = self.handle_option_and_params(params, 'setMarginMode', 'leverage', 1) + request: dict = { + 'market': market['id'], + 'leverage': leverage, + 'margin_type': self.encode_margin_mode(marginMode), + } + return self.privatePostAccountMarginMarket(self.extend(request, params)) + + def fetch_leverage(self, symbol: str, params={}) -> Leverage: + """ + fetch the set leverage for a market + + https://docs.api.testnet.paradex.trade/#get-account-margin-configuration + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `leverage structure ` + """ + self.authenticate_rest() + self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + response = self.privateGetAccountMargin(self.extend(request, params)) + # + # { + # "account": "0x6343248026a845b39a8a73fbe9c7ef0a841db31ed5c61ec1446aa9d25e54dbc", + # "configs": [ + # { + # "market": "SOL-USD-PERP", + # "leverage": 50, + # "margin_type": "CROSS" + # } + # ] + # } + # + configs = self.safe_list(response, 'configs') + return self.parse_leverage(self.safe_dict(configs, 0), market) + + def parse_leverage(self, leverage: dict, market: Market = None) -> Leverage: + marketId = self.safe_string(leverage, 'market') + market = self.safe_market(marketId, market) + marginMode = self.safe_string_lower(leverage, 'margin_type') + return { + 'info': leverage, + 'symbol': self.safe_symbol(marketId, market), + 'marginMode': marginMode, + 'longLeverage': self.safe_integer(leverage, 'leverage'), + 'shortLeverage': self.safe_integer(leverage, 'leverage'), + } + + def encode_margin_mode(self, mode): + modes = { + 'cross': 'CROSS', + 'isolated': 'ISOLATED', + } + return self.safe_string(modes, mode, mode) + + def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://docs.api.testnet.paradex.trade/#set-margin-configuration + + :param float leverage: the rate of leverage + :param str [symbol]: unified market symbol(is mandatory for swap markets) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' + :returns dict: response from the exchange + """ + self.check_required_argument('setLeverage', symbol, 'symbol') + self.authenticate_rest() + self.load_markets() + market: Market = self.market(symbol) + marginMode: Str = None + marginMode, params = self.handle_margin_mode_and_params('setLeverage', params, 'cross') + request: dict = { + 'market': market['id'], + 'leverage': leverage, + 'margin_type': self.encode_margin_mode(marginMode), + } + return self.privatePostAccountMarginMarket(self.extend(request, params)) + + def fetch_greeks(self, symbol: str, params={}) -> Greeks: + """ + fetches an option contracts greeks, financial metrics used to measure the factors that affect the price of an options contract + + https://docs.api.testnet.paradex.trade/#list-available-markets-summary + + :param str symbol: unified symbol of the market to fetch greeks for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `greeks structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + response = self.publicGetMarketsSummary(self.extend(request, params)) + # + # { + # "results": [ + # { + # "symbol": "BTC-USD-114000-P", + # "mark_price": "10835.66892602", + # "mark_iv": "0.71781855", + # "delta": "-0.98726024", + # "greeks": { + # "delta": "-0.9872602390817709", + # "gamma": "0.000004560958862297231", + # "vega": "227.11344863639806", + # "rho": "-302.0617972461581", + # "vanna": "0.06609830491614832", + # "volga": "925.9501532805552" + # }, + # "last_traded_price": "10551.5", + # "bid": "10794.9", + # "bid_iv": "0.05", + # "ask": "10887.3", + # "ask_iv": "0.8783283", + # "last_iv": "0.05", + # "volume_24h": "0", + # "total_volume": "195240.72672261014", + # "created_at": 1747644009995, + # "underlying_price": "103164.79162649", + # "open_interest": "0", + # "funding_rate": "0.000004464241170536191", + # "price_change_rate_24h": "0.074915", + # "future_funding_rate": "0.0001" + # } + # ] + # } + # + data = self.safe_list(response, 'results', []) + greeks = self.safe_dict(data, 0, {}) + return self.parse_greeks(greeks, market) + + def fetch_all_greeks(self, symbols: Strings = None, params={}) -> List[Greeks]: + """ + fetches all option contracts greeks, financial metrics used to measure the factors that affect the price of an options contract + + https://docs.api.testnet.paradex.trade/#list-available-markets-summary + + :param str[] [symbols]: unified symbols of the markets to fetch greeks for, all markets are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `greeks structure ` + """ + self.load_markets() + symbols = self.market_symbols(symbols, None, True, True, True) + request: dict = { + 'market': 'ALL', + } + response = self.publicGetMarketsSummary(self.extend(request, params)) + # + # { + # "results": [ + # { + # "symbol": "BTC-USD-114000-P", + # "mark_price": "10835.66892602", + # "mark_iv": "0.71781855", + # "delta": "-0.98726024", + # "greeks": { + # "delta": "-0.9872602390817709", + # "gamma": "0.000004560958862297231", + # "vega": "227.11344863639806", + # "rho": "-302.0617972461581", + # "vanna": "0.06609830491614832", + # "volga": "925.9501532805552" + # }, + # "last_traded_price": "10551.5", + # "bid": "10794.9", + # "bid_iv": "0.05", + # "ask": "10887.3", + # "ask_iv": "0.8783283", + # "last_iv": "0.05", + # "volume_24h": "0", + # "total_volume": "195240.72672261014", + # "created_at": 1747644009995, + # "underlying_price": "103164.79162649", + # "open_interest": "0", + # "funding_rate": "0.000004464241170536191", + # "price_change_rate_24h": "0.074915", + # "future_funding_rate": "0.0001" + # } + # ] + # } + # + results = self.safe_list(response, 'results', []) + return self.parse_all_greeks(results, symbols) + + def parse_greeks(self, greeks: dict, market: Market = None) -> Greeks: + # + # { + # "symbol": "BTC-USD-114000-P", + # "mark_price": "10835.66892602", + # "mark_iv": "0.71781855", + # "delta": "-0.98726024", + # "greeks": { + # "delta": "-0.9872602390817709", + # "gamma": "0.000004560958862297231", + # "vega": "227.11344863639806", + # "rho": "-302.0617972461581", + # "vanna": "0.06609830491614832", + # "volga": "925.9501532805552" + # }, + # "last_traded_price": "10551.5", + # "bid": "10794.9", + # "bid_iv": "0.05", + # "ask": "10887.3", + # "ask_iv": "0.8783283", + # "last_iv": "0.05", + # "volume_24h": "0", + # "total_volume": "195240.72672261014", + # "created_at": 1747644009995, + # "underlying_price": "103164.79162649", + # "open_interest": "0", + # "funding_rate": "0.000004464241170536191", + # "price_change_rate_24h": "0.074915", + # "future_funding_rate": "0.0001" + # } + # + marketId = self.safe_string(greeks, 'symbol') + market = self.safe_market(marketId, market, None, 'option') + symbol = market['symbol'] + timestamp = self.safe_integer(greeks, 'created_at') + greeksData = self.safe_dict(greeks, 'greeks', {}) + return { + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'delta': self.safe_number(greeksData, 'delta'), + 'gamma': self.safe_number(greeksData, 'gamma'), + 'theta': None, + 'vega': self.safe_number(greeksData, 'vega'), + 'rho': self.safe_number(greeksData, 'rho'), + 'vanna': self.safe_number(greeksData, 'vanna'), + 'volga': self.safe_number(greeksData, 'volga'), + 'bidSize': None, + 'askSize': None, + 'bidImpliedVolatility': self.safe_number(greeks, 'bid_iv'), + 'askImpliedVolatility': self.safe_number(greeks, 'ask_iv'), + 'markImpliedVolatility': self.safe_number(greeks, 'mark_iv'), + 'bidPrice': self.safe_number(greeks, 'bid'), + 'askPrice': self.safe_number(greeks, 'ask'), + 'markPrice': self.safe_number(greeks, 'mark_price'), + 'lastPrice': self.safe_number(greeks, 'last_traded_price'), + 'underlyingPrice': self.safe_number(greeks, 'underlying_price'), + 'info': greeks, + } + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = self.implode_hostname(self.urls['api'][self.version]) + '/' + self.implode_params(path, params) + query = self.omit(params, self.extract_params(path)) + if api == 'public': + if query: + url += '?' + self.urlencode(query) + elif api == 'private': + headers = { + 'Accept': 'application/json', + 'PARADEX-PARTNER': self.safe_string(self.options, 'broker', 'CCXT'), + } + # TODO: optimize + if path == 'auth': + headers['PARADEX-STARKNET-ACCOUNT'] = query['account'] + headers['PARADEX-STARKNET-SIGNATURE'] = query['signature'] + headers['PARADEX-TIMESTAMP'] = str(query['timestamp']) + headers['PARADEX-SIGNATURE-EXPIRATION'] = str(query['expiration']) + elif path == 'onboarding': + headers['PARADEX-ETHEREUM-ACCOUNT'] = self.walletAddress + headers['PARADEX-STARKNET-ACCOUNT'] = query['account'] + headers['PARADEX-STARKNET-SIGNATURE'] = query['signature'] + headers['PARADEX-TIMESTAMP'] = str(self.nonce()) + headers['Content-Type'] = 'application/json' + body = self.json({ + 'public_key': query['public_key'], + }) + else: + token = self.options['authToken'] + headers['Authorization'] = 'Bearer ' + token + if method == 'POST': + headers['Content-Type'] = 'application/json' + body = self.json(query) + else: + url = url + '?' + self.urlencode(query) + # headers = { + # 'Accept': 'application/json', + # 'Authorization': 'Bearer ' + self.apiKey, + # } + # if method == 'POST': + # body = self.json(query) + # headers['Content-Type'] = 'application/json' + # else: + # if query: + # url += '?' + self.urlencode(query) + # } + # } + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if not response: + return None # fallback to default error handler + # + # { + # "data": null, + # "error": "NOT_ONBOARDED", + # "message": "User has never called /onboarding endpoint" + # } + # + errorCode = self.safe_string(response, 'error') + if errorCode is not None: + feedback = self.id + ' ' + body + self.throw_broadly_matched_exception(self.exceptions['broad'], body, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + raise ExchangeError(feedback) # unknown message + return None diff --git a/ccxt/paymium.py b/ccxt/paymium.py new file mode 100644 index 0000000..de72789 --- /dev/null +++ b/ccxt/paymium.py @@ -0,0 +1,627 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.paymium import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currency, DepositAddress, Int, Market, Num, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Trade, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class paymium(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(paymium, self).describe(), { + 'id': 'paymium', + 'name': 'Paymium', + 'countries': ['FR', 'EU'], + 'rateLimit': 2000, + 'version': 'v1', + 'has': { + 'CORS': True, + 'spot': True, + 'margin': None, + 'swap': False, + 'future': False, + 'option': False, + 'cancelOrder': True, + 'createDepositAddress': True, + 'createOrder': True, + 'fetchBalance': True, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': True, + 'fetchDepositAddressesByNetwork': False, + 'fetchFundingHistory': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchIndexOHLCV': False, + 'fetchMarkOHLCV': False, + 'fetchOpenInterestHistory': False, + 'fetchOrderBook': True, + 'fetchPremiumIndexOHLCV': False, + 'fetchTicker': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': False, + 'transfer': True, + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/51840849/87153930-f0f02200-c2c0-11ea-9c0a-40337375ae89.jpg', + 'api': { + 'rest': 'https://paymium.com/api', + }, + 'www': 'https://www.paymium.com', + 'fees': 'https://www.paymium.com/page/help/fees', + 'doc': [ + 'https://github.com/Paymium/api-documentation', + 'https://www.paymium.com/page/developers', + 'https://paymium.github.io/api-documentation/', + ], + 'referral': 'https://www.paymium.com/page/sign-up?referral=eDAzPoRQFMvaAB8sf-qj', + }, + 'api': { + 'public': { + 'get': [ + 'countries', + 'currencies', + 'data/{currency}/ticker', + 'data/{currency}/trades', + 'data/{currency}/depth', + 'bitcoin_charts/{id}/trades', + 'bitcoin_charts/{id}/depth', + ], + }, + 'private': { + 'get': [ + 'user', + 'user/addresses', + 'user/addresses/{address}', + 'user/orders', + 'user/orders/{uuid}', + 'user/price_alerts', + 'merchant/get_payment/{uuid}', + ], + 'post': [ + 'user/addresses', + 'user/orders', + 'user/withdrawals', + 'user/email_transfers', + 'user/payment_requests', + 'user/price_alerts', + 'merchant/create_payment', + ], + 'delete': [ + 'user/orders/{uuid}', + 'user/orders/{uuid}/cancel', + 'user/price_alerts/{id}', + ], + }, + }, + 'markets': { + 'BTC/EUR': self.safe_market_structure({'id': 'eur', 'symbol': 'BTC/EUR', 'base': 'BTC', 'quote': 'EUR', 'baseId': 'btc', 'quoteId': 'eur', 'type': 'spot', 'spot': True}), + }, + 'fees': { + 'trading': { + 'maker': self.parse_number('-0.001'), + 'taker': self.parse_number('0.005'), + }, + }, + 'precisionMode': TICK_SIZE, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': False, + 'triggerDirection': False, + 'triggerPriceType': None, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': False, + 'FOK': False, + 'PO': False, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': True, # todo + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': None, + 'fetchOrder': None, # todo + 'fetchOpenOrders': None, # todo + 'fetchOrders': None, # todo + 'fetchClosedOrders': None, # todo + 'fetchOHLCV': None, # todo + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + }) + + def parse_balance(self, response) -> Balances: + result: dict = {'info': response} + currencies = list(self.currencies.keys()) + for i in range(0, len(currencies)): + code = currencies[i] + currency = self.currency(code) + currencyId = currency['id'] + free = 'balance_' + currencyId + if free in response: + account = self.account() + used = 'locked_' + currencyId + account['free'] = self.safe_string(response, free) + account['used'] = self.safe_string(response, used) + result[code] = account + return self.safe_balance(result) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://paymium.github.io/api-documentation/#tag/User/paths/~1user/get + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + response = self.privateGetUser(params) + return self.parse_balance(response) + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://paymium.github.io/api-documentation/#tag/Public-data/paths/~1data~1%7Bcurrency%7D~1depth/get + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'currency': market['id'], + } + response = self.publicGetDataCurrencyDepth(self.extend(request, params)) + return self.parse_order_book(response, market['symbol'], None, 'bids', 'asks', 'price', 'amount') + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "high":"33740.82", + # "low":"32185.15", + # "volume":"4.7890433", + # "bid":"33313.53", + # "ask":"33497.97", + # "midpoint":"33405.75", + # "vwap":"32802.5263553", + # "at":1643381654, + # "price":"33143.91", + # "open":"33116.86", + # "variation":"0.0817", + # "currency":"EUR", + # "trade_id":"ce2f5152-3ac5-412d-9b24-9fa72338474c", + # "size":"0.00041087" + # } + # + symbol = self.safe_symbol(None, market) + timestamp = self.safe_timestamp(ticker, 'at') + vwap = self.safe_string(ticker, 'vwap') + baseVolume = self.safe_string(ticker, 'volume') + quoteVolume = Precise.string_mul(baseVolume, vwap) + last = self.safe_string(ticker, 'price') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': self.safe_string(ticker, 'bid'), + 'bidVolume': None, + 'ask': self.safe_string(ticker, 'ask'), + 'askVolume': None, + 'vwap': vwap, + 'open': self.safe_string(ticker, 'open'), + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': self.safe_string(ticker, 'variation'), + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'info': ticker, + }, market) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://paymium.github.io/api-documentation/#tag/Public-data/paths/~1data~1%7Bcurrency%7D~1ticker/get + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'currency': market['id'], + } + ticker = self.publicGetDataCurrencyTicker(self.extend(request, params)) + # + # { + # "high":"33740.82", + # "low":"32185.15", + # "volume":"4.7890433", + # "bid":"33313.53", + # "ask":"33497.97", + # "midpoint":"33405.75", + # "vwap":"32802.5263553", + # "at":1643381654, + # "price":"33143.91", + # "open":"33116.86", + # "variation":"0.0817", + # "currency":"EUR", + # "trade_id":"ce2f5152-3ac5-412d-9b24-9fa72338474c", + # "size":"0.00041087" + # } + # + return self.parse_ticker(ticker, market) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + timestamp = self.safe_timestamp(trade, 'created_at_int') + id = self.safe_string(trade, 'uuid') + market = self.safe_market(None, market) + side = self.safe_string(trade, 'side') + price = self.safe_string(trade, 'price') + amountField = 'traded_' + market['base'].lower() + amount = self.safe_string(trade, amountField) + return self.safe_trade({ + 'info': trade, + 'id': id, + 'order': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'type': None, + 'side': side, + 'takerOrMaker': None, + 'price': price, + 'amount': amount, + 'cost': None, + 'fee': None, + }, market) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://paymium.github.io/api-documentation/#tag/Public-data/paths/~1data~1%7Bcurrency%7D~1trades/get + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'currency': market['id'], + } + response = self.publicGetDataCurrencyTrades(self.extend(request, params)) + return self.parse_trades(response, market, since, limit) + + def create_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + create a currency deposit address + + https://paymium.github.io/api-documentation/#tag/User/paths/~1user~1addresses/post + + :param str code: unified currency code of the currency for the deposit address + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + self.load_markets() + response = self.privatePostUserAddresses(params) + # + # { + # "address": "1HdjGr6WCTcnmW1tNNsHX7fh4Jr5C2PeKe", + # "valid_until": 1620041926, + # "currency": "BTC", + # "label": "Savings" + # } + # + return self.parse_deposit_address(response) + + def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://paymium.github.io/api-documentation/#tag/User/paths/~1user~1addresses~1%7Baddress%7D/get + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + self.load_markets() + request: dict = { + 'address': code, + } + response = self.privateGetUserAddressesAddress(self.extend(request, params)) + # + # { + # "address": "1HdjGr6WCTcnmW1tNNsHX7fh4Jr5C2PeKe", + # "valid_until": 1620041926, + # "currency": "BTC", + # "label": "Savings" + # } + # + return self.parse_deposit_address(response) + + def fetch_deposit_addresses(self, codes: Strings = None, params={}) -> List[DepositAddress]: + """ + fetch deposit addresses for multiple currencies and chain types + + https://paymium.github.io/api-documentation/#tag/User/paths/~1user~1addresses/get + + :param str[]|None codes: list of unified currency codes, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `address structures ` + """ + self.load_markets() + response = self.privateGetUserAddresses(params) + # + # [ + # { + # "address": "1HdjGr6WCTcnmW1tNNsHX7fh4Jr5C2PeKe", + # "valid_until": 1620041926, + # "currency": "BTC", + # "label": "Savings" + # } + # ] + # + return self.parse_deposit_addresses(response, codes) + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + # + # { + # "address": "1HdjGr6WCTcnmW1tNNsHX7fh4Jr5C2PeKe", + # "valid_until": 1620041926, + # "currency": "BTC", + # "label": "Savings" + # } + # + address = self.safe_string(depositAddress, 'address') + currencyId = self.safe_string(depositAddress, 'currency') + return { + 'info': depositAddress, + 'currency': self.safe_currency_code(currencyId, currency), + 'network': None, + 'address': address, + 'tag': None, + } + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://paymium.github.io/api-documentation/#tag/Order/paths/~1user~1orders/post + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'type': self.capitalize(type) + 'Order', + 'currency': market['id'], + 'direction': side, + 'amount': amount, + } + if type != 'market': + request['price'] = price + response = self.privatePostUserOrders(self.extend(request, params)) + return self.safe_order({ + 'info': response, + 'id': response['uuid'], + }, market) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://paymium.github.io/api-documentation/#tag/Order/paths/~1user~1orders~1%7Buuid%7D/delete + https://paymium.github.io/api-documentation/#tag/Order/paths/~1user~1orders~1%7Buuid%7D~1cancel/delete + + :param str id: order id + :param str symbol: not used by paymium cancelOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + request: dict = { + 'uuid': id, + } + response = self.privateDeleteUserOrdersUuidCancel(self.extend(request, params)) + return self.safe_order({ + 'info': response, + }) + + def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://paymium.github.io/api-documentation/#tag/Transfer/paths/~1user~1email_transfers/post + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account to transfer from + :param str toAccount: account to transfer to + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transfer structure ` + """ + self.load_markets() + currency = self.currency(code) + if toAccount.find('@') < 0: + raise ExchangeError(self.id + ' transfer() only allows transfers to an email address') + if code != 'BTC' and code != 'EUR': + raise ExchangeError(self.id + ' transfer() only allows BTC or EUR') + request: dict = { + 'currency': currency['id'], + 'amount': self.currency_to_precision(code, amount), + 'email': toAccount, + # 'comment': 'a small note explaining the transfer' + } + response = self.privatePostUserEmailTransfers(self.extend(request, params)) + # + # { + # "uuid": "968f4580-e26c-4ad8-8bcd-874d23d55296", + # "type": "Transfer", + # "currency": "BTC", + # "currency_amount": "string", + # "created_at": "2013-10-24T10:34:37.000Z", + # "updated_at": "2013-10-24T10:34:37.000Z", + # "amount": "1.0", + # "state": "executed", + # "currency_fee": "0.0", + # "btc_fee": "0.0", + # "comment": "string", + # "traded_btc": "string", + # "traded_currency": "string", + # "direction": "buy", + # "price": "string", + # "account_operations": [ + # { + # "uuid": "968f4580-e26c-4ad8-8bcd-874d23d55296", + # "amount": "1.0", + # "currency": "BTC", + # "created_at": "2013-10-24T10:34:37.000Z", + # "created_at_int": 1389094259, + # "name": "account_operation", + # "address": "1FPDBXNqSkZMsw1kSkkajcj8berxDQkUoc", + # "tx_hash": "string", + # "is_trading_account": True + # } + # ] + # } + # + return self.parse_transfer(response, currency) + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + # + # { + # "uuid": "968f4580-e26c-4ad8-8bcd-874d23d55296", + # "type": "Transfer", + # "currency": "BTC", + # "currency_amount": "string", + # "created_at": "2013-10-24T10:34:37.000Z", + # "updated_at": "2013-10-24T10:34:37.000Z", + # "amount": "1.0", + # "state": "executed", + # "currency_fee": "0.0", + # "btc_fee": "0.0", + # "comment": "string", + # "traded_btc": "string", + # "traded_currency": "string", + # "direction": "buy", + # "price": "string", + # "account_operations": [ + # { + # "uuid": "968f4580-e26c-4ad8-8bcd-874d23d55296", + # "amount": "1.0", + # "currency": "BTC", + # "created_at": "2013-10-24T10:34:37.000Z", + # "created_at_int": 1389094259, + # "name": "account_operation", + # "address": "1FPDBXNqSkZMsw1kSkkajcj8berxDQkUoc", + # "tx_hash": "string", + # "is_trading_account": True + # } + # ] + # } + # + currencyId = self.safe_string(transfer, 'currency') + updatedAt = self.safe_string(transfer, 'updated_at') + timetstamp = self.parse_date(updatedAt) + accountOperations = self.safe_value(transfer, 'account_operations') + firstOperation = self.safe_value(accountOperations, 0, {}) + status = self.safe_string(transfer, 'state') + return { + 'info': transfer, + 'id': self.safe_string(transfer, 'uuid'), + 'timestamp': timetstamp, + 'datetime': self.iso8601(timetstamp), + 'currency': self.safe_currency_code(currencyId, currency), + 'amount': self.safe_number(transfer, 'amount'), + 'fromAccount': None, + 'toAccount': self.safe_string(firstOperation, 'address'), + 'status': self.parse_transfer_status(status), + } + + def parse_transfer_status(self, status: Str) -> Str: + statuses: dict = { + 'executed': 'ok', + # what are the other statuses? + } + return self.safe_string(statuses, status, status) + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = self.urls['api']['rest'] + '/' + self.version + '/' + self.implode_params(path, params) + query = self.omit(params, self.extract_params(path)) + if api == 'public': + if query: + url += '?' + self.urlencode(query) + else: + self.check_required_credentials() + nonce = str(self.nonce()) + auth = nonce + url + headers = { + 'Api-Key': self.apiKey, + 'Api-Nonce': nonce, + } + if method == 'POST': + if query: + body = self.json(query) + auth += body + headers['Content-Type'] = 'application/json' + else: + if query: + queryString = self.urlencode(query) + auth += queryString + url += '?' + queryString + headers['Api-Signature'] = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256) + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None + errors = self.safe_value(response, 'errors') + if errors is not None: + raise ExchangeError(self.id + ' ' + self.json(response)) + return None diff --git a/ccxt/phemex.py b/ccxt/phemex.py new file mode 100644 index 0000000..99fc67b --- /dev/null +++ b/ccxt/phemex.py @@ -0,0 +1,5030 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.phemex import ImplicitAPI +import hashlib +import numbers +from ccxt.base.types import Any, Balances, Conversion, Currencies, Currency, DepositAddress, Int, LeverageTier, LeverageTiers, MarginModification, Market, Num, Order, OrderBook, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, Trade, Transaction, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import AccountSuspended +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import DuplicateOrderId +from ccxt.base.errors import DDoSProtection +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import CancelPending +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class phemex(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(phemex, self).describe(), { + 'id': 'phemex', + 'name': 'Phemex', + 'countries': ['CN'], # China + 'rateLimit': 120.5, + 'version': 'v1', + 'certified': False, + 'pro': True, + 'hostname': 'api.phemex.com', + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': True, + 'future': False, + 'option': False, + 'addMargin': False, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'closePosition': False, + 'createConvertTrade': True, + 'createOrder': True, + 'createReduceOnlyOrder': True, + 'createStopLimitOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'editOrder': True, + 'fetchBalance': True, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchClosedOrders': True, + 'fetchConvertQuote': True, + 'fetchConvertTrade': False, + 'fetchConvertTradeHistory': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchFundingHistory': True, + 'fetchFundingRate': True, + 'fetchFundingRateHistories': False, + 'fetchFundingRateHistory': True, + 'fetchFundingRates': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchLeverage': False, + 'fetchLeverageTiers': True, + 'fetchMarketLeverageTiers': 'emulated', + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': True, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchPositions': True, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': False, + 'fetchTransfers': True, + 'fetchWithdrawals': True, + 'reduceMargin': False, + 'sandbox': True, + 'setLeverage': True, + 'setMargin': True, + 'setMarginMode': True, + 'setPositionMode': True, + 'transfer': True, + 'withdraw': True, + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/85225056-221eb600-b3d7-11ea-930d-564d2690e3f6.jpg', + 'test': { + 'v1': 'https://testnet-api.phemex.com/v1', + 'v2': 'https://testnet-api.phemex.com', + 'public': 'https://testnet-api.phemex.com/exchange/public', + 'private': 'https://testnet-api.phemex.com', + }, + 'api': { + 'v1': 'https://{hostname}/v1', + 'v2': 'https://{hostname}', + 'public': 'https://{hostname}/exchange/public', + 'private': 'https://{hostname}', + }, + 'www': 'https://phemex.com', + 'doc': 'https://phemex-docs.github.io/#overview', + 'fees': 'https://phemex.com/fees-conditions', + 'referral': { + 'url': 'https://phemex.com/register?referralCode=EDNVJ', + 'discount': 0.1, + }, + }, + 'timeframes': { + '1m': '60', + '3m': '180', + '5m': '300', + '15m': '900', + '30m': '1800', + '1h': '3600', + '2h': '7200', + '3h': '10800', + '4h': '14400', + '6h': '21600', + '12h': '43200', + '1d': '86400', + '1w': '604800', + '1M': '2592000', + '3M': '7776000', + '1Y': '31104000', + }, + 'api': { + 'public': { + 'get': { + 'cfg/v2/products': 5, # spot + contracts + 'cfg/fundingRates': 5, + 'products': 5, # contracts only + 'nomics/trades': 5, # ?market=&since= + 'md/kline': 5, # ?from=1589811875&resolution=1800&symbol=sBTCUSDT&to=1592457935 + 'md/v2/kline/list': 5, # perpetual api ?symbol=&to=&from=&resolution= + 'md/v2/kline': 5, # ?symbol=&resolution=&limit= + 'md/v2/kline/last': 5, # perpetual ?symbol=&resolution=&limit= + 'md/orderbook': 5, # ?symbol= + 'md/trade': 5, # ?symbol= + 'md/spot/ticker/24hr': 5, # ?symbol= + 'exchange/public/cfg/chain-settings': 5, # ?currency= + }, + }, + 'v1': { + 'get': { + 'md/fullbook': 5, # ?symbol= + 'md/orderbook': 5, # ?symbol= + 'md/trade': 5, # ?symbol=&id= + 'md/ticker/24hr': 5, # ?symbol=&id= + 'md/ticker/24hr/all': 5, # ?id= + 'md/spot/ticker/24hr': 5, # ?symbol=&id= + 'md/spot/ticker/24hr/all': 5, # ?symbol=&id= + 'exchange/public/products': 5, # contracts only + 'api-data/public/data/funding-rate-history': 5, + }, + }, + 'v2': { + 'get': { + 'public/products': 5, + 'public/products-plus': 5, + 'md/v2/orderbook': 5, # ?symbol=&id= + 'md/v2/trade': 5, # ?symbol=&id= + 'md/v2/ticker/24hr': 5, # ?symbol=&id= + 'md/v2/ticker/24hr/all': 5, # ?id= + 'api-data/public/data/funding-rate-history': 5, + }, + }, + 'private': { + 'get': { + # spot + 'spot/orders/active': 1, # ?symbol=&orderID= + # 'spot/orders/active': 5, # ?symbol=&clOrDID= + 'spot/orders': 1, # ?symbol= + 'spot/wallets': 5, # ?currency= + 'exchange/spot/order': 5, # ?symbol=&ordStatus=ordType=&start=&end=&limit=&offset= + 'exchange/spot/order/trades': 5, # ?symbol=&start=&end=&limit=&offset= + 'exchange/order/v2/orderList': 5, # ?symbol=¤cy=&ordStatus=&ordType=&start=&end=&offset=&limit=&withCount= + 'exchange/order/v2/tradingList': 5, # ?symbol=¤cy=&execType=&offset=&limit=&withCount= + # swap + 'accounts/accountPositions': 1, # ?currency= + 'g-accounts/accountPositions': 1, # ?currency= + 'g-accounts/positions': 25, # ?currency= + 'g-accounts/risk-unit': 1, + 'api-data/futures/funding-fees': 5, # ?symbol= + 'api-data/g-futures/funding-fees': 5, # ?symbol= + 'api-data/futures/orders': 5, # ?symbol= + 'api-data/g-futures/orders': 5, # ?symbol= + 'api-data/futures/orders/by-order-id': 5, # ?symbol= + 'api-data/g-futures/orders/by-order-id': 5, # ?symbol= + 'api-data/futures/trades': 5, # ?symbol= + 'api-data/g-futures/trades': 5, # ?symbol= + 'api-data/futures/trading-fees': 5, # ?symbol= + 'api-data/g-futures/trading-fees': 5, # ?symbol= + 'api-data/futures/v2/tradeAccountDetail': 5, # ?currency=&type=&limit=&offset=&start=&end=&withCount= + 'g-orders/activeList': 1, # ?symbol= + 'orders/activeList': 1, # ?symbol= + 'exchange/order/list': 5, # ?symbol=&start=&end=&offset=&limit=&ordStatus=&withCount= + 'exchange/order': 5, # ?symbol=&orderID= + # 'exchange/order': 5, # ?symbol=&clOrdID= + 'exchange/order/trade': 5, # ?symbol=&start=&end=&limit=&offset=&withCount= + 'phemex-user/users/children': 5, # ?offset=&limit=&withCount= + 'phemex-user/wallets/v2/depositAddress': 5, # ?_t=1592722635531¤cy=USDT + 'phemex-user/wallets/tradeAccountDetail': 5, # ?bizCode=¤cy=&end=1642443347321&limit=10&offset=0&side=&start=1&type=4&withCount=true + 'phemex-deposit/wallets/api/depositAddress': 5, # ?currency=&chainName= + 'phemex-deposit/wallets/api/depositHist': 5, # ?currency=&offset=&limit=&withCount= + 'phemex-deposit/wallets/api/chainCfg': 5, # ?currency= + 'phemex-withdraw/wallets/api/withdrawHist': 5, # ?currency=&chainName=&offset=&limit=&withCount= + 'phemex-withdraw/wallets/api/asset/info': 5, # ?currency=&amount= + 'phemex-user/order/closedPositionList': 5, # ?currency=USD&limit=10&offset=0&symbol=&withCount=true + 'exchange/margins/transfer': 5, # ?start=&end=&offset=&limit=&withCount= + 'exchange/wallets/confirm/withdraw': 5, # ?code= + 'exchange/wallets/withdrawList': 5, # ?currency=&limit=&offset=&withCount= + 'exchange/wallets/depositList': 5, # ?currency=&offset=&limit= + 'exchange/wallets/v2/depositAddress': 5, # ?currency= + 'api-data/spots/funds': 5, # ?currency=&start=&end=&limit=&offset= + 'api-data/spots/orders': 5, # ?symbol= + 'api-data/spots/orders/by-order-id': 5, # ?symbol=&oderId=&clOrdID= + 'api-data/spots/pnls': 5, + 'api-data/spots/trades': 5, # ?symbol= + 'api-data/spots/trades/by-order-id': 5, # ?symbol=&oderId=&clOrdID= + 'assets/convert': 5, # ?startTime=&endTime=&limit=&offset= + # transfer + 'assets/transfer': 5, # ?currency=&start=&end=&limit=&offset= + 'assets/spots/sub-accounts/transfer': 5, # ?currency=&start=&end=&limit=&offset= + 'assets/futures/sub-accounts/transfer': 5, # ?currency=&start=&end=&limit=&offset= + 'assets/quote': 5, # ?fromCurrency=&toCurrency=&amountEv= + # deposit/withdraw + }, + 'post': { + # spot + 'spot/orders': 1, + # swap + 'orders': 1, + 'g-orders': 1, + 'positions/assign': 5, # ?symbol=&posBalance=&posBalanceEv= + 'exchange/wallets/transferOut': 5, + 'exchange/wallets/transferIn': 5, + 'exchange/margins': 5, + 'exchange/wallets/createWithdraw': 5, # ?otpCode= + 'exchange/wallets/cancelWithdraw': 5, + 'exchange/wallets/createWithdrawAddress': 5, # ?otpCode={optCode} + # transfer + 'assets/transfer': 5, + 'assets/spots/sub-accounts/transfer': 5, # for sub-account only + 'assets/futures/sub-accounts/transfer': 5, # for sub-account only + 'assets/universal-transfer': 5, # for Main account only + 'assets/convert': 5, + # withdraw + 'phemex-withdraw/wallets/api/createWithdraw': 5, # ?currency=&address=
&amount=&addressTag=&chainName= + 'phemex-withdraw/wallets/api/cancelWithdraw': 5, # ?id= + }, + 'put': { + # spot + 'spot/orders/create': 1, # ?symbol=&trigger=&clOrdID=&priceEp=&baseQtyEv="eQtyEv=&stopPxEp=&text=&side=&qtyType=&ordType=&timeInForce=&execInst= + 'spot/orders': 1, # ?symbol=&orderID=&origClOrdID=&clOrdID=&priceEp=&baseQtyEV="eQtyEv=&stopPxEp= + # swap + 'orders/replace': 1, # ?symbol=&orderID=&origClOrdID=&clOrdID=&price=&priceEp=&orderQty=&stopPx=&stopPxEp=&takeProfit=&takeProfitEp=&stopLoss=&stopLossEp=&pegOffsetValueEp=&pegPriceType= + 'g-orders/replace': 1, # ?symbol=&orderID=&origClOrdID=&clOrdID=&price=&priceEp=&orderQty=&stopPx=&stopPxEp=&takeProfit=&takeProfitEp=&stopLoss=&stopLossEp=&pegOffsetValueEp=&pegPriceType= + 'g-orders/create': 1, + 'positions/leverage': 5, # ?symbol=&leverage=&leverageEr= + 'g-positions/leverage': 5, # ?symbol=&leverage=&leverageEr= + 'g-positions/switch-pos-mode-sync': 5, # ?symbol=&targetPosMode= + 'positions/riskLimit': 5, # ?symbol=&riskLimit=&riskLimitEv= + }, + 'delete': { + # spot + 'spot/orders': 2, # ?symbol=&orderID= + 'spot/orders/all': 2, # ?symbol=&untriggered= + # 'spot/orders': 5, # ?symbol=&clOrdID= + # swap + 'orders/cancel': 1, # ?symbol=&orderID= + 'orders': 1, # ?symbol=&orderID=,, + 'orders/all': 3, # ?symbol=&untriggered=&text= + 'g-orders/cancel': 1, # ?symbol=&orderID= + 'g-orders': 1, # ?symbol=&orderID=,, + 'g-orders/all': 3, # ?symbol=&untriggered=&text= + }, + }, + }, + 'precisionMode': TICK_SIZE, + 'fees': { + 'trading': { + 'tierBased': False, + 'percentage': True, + 'taker': self.parse_number('0.001'), + 'maker': self.parse_number('0.001'), + }, + }, + 'features': { + 'default': { + 'sandbox': True, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + # todo + 'triggerPriceType': { + 'mark': True, + 'last': True, + 'index': True, + }, + 'triggerDirection': False, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'leverage': False, + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': False, + 'trailing': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 200, + 'daysBack': 100000, + 'untilDays': 2, # todo implement + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': None, + 'daysBack': None, + 'untilDays': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 200, + 'daysBack': 100000, + 'daysBackCanceled': 100000, + 'untilDays': 2, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 1000, + }, + }, + 'spot': { + 'extends': 'default', + }, + 'forDerivatives': { + 'extends': 'default', + 'createOrder': { + 'triggerDirection': True, + 'attachedStopLossTakeProfit': { + 'triggerPriceType': { + 'mark': True, + 'last': True, + 'index': True, + }, + 'price': True, + }, + 'hedged': True, + }, + 'fetchOHLCV': { + 'limit': 2000, + }, + }, + 'swap': { + 'linear': { + 'extends': 'forDerivatives', + }, + 'inverse': { + 'extends': 'forDerivatives', + }, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + }, + 'exceptions': { + 'exact': { + # not documented + '401': AuthenticationError, # {"code":"401","msg":"401 Failed to load API KEY."} + '412': BadRequest, # {"code":412,"msg":"Missing parameter - resolution","data":null} + '6001': BadRequest, # {"error":{"code":6001,"message":"invalid argument"},"id":null,"result":null} + # documented + '19999': BadRequest, # REQUEST_IS_DUPLICATED Duplicated request ID + '10001': DuplicateOrderId, # OM_DUPLICATE_ORDERID Duplicated order ID + '10002': OrderNotFound, # OM_ORDER_NOT_FOUND Cannot find order ID + '10003': CancelPending, # OM_ORDER_PENDING_CANCEL Cannot cancel while order is already in pending cancel status + '10004': CancelPending, # OM_ORDER_PENDING_REPLACE Cannot cancel while order is already in pending cancel status + '10005': CancelPending, # OM_ORDER_PENDING Cannot cancel while order is already in pending cancel status + '11001': InsufficientFunds, # TE_NO_ENOUGH_AVAILABLE_BALANCE Insufficient available balance + '11002': InvalidOrder, # TE_INVALID_RISK_LIMIT Invalid risk limit value + '11003': InsufficientFunds, # TE_NO_ENOUGH_BALANCE_FOR_NEW_RISK_LIMIT Insufficient available balance + '11004': InvalidOrder, # TE_INVALID_LEVERAGE invalid input or new leverage is over maximum allowed leverage + '11005': InsufficientFunds, # TE_NO_ENOUGH_BALANCE_FOR_NEW_LEVERAGE Insufficient available balance + '11006': ExchangeError, # TE_CANNOT_CHANGE_POSITION_MARGIN_WITHOUT_POSITION Position size is zero. Cannot change margin + '11007': ExchangeError, # TE_CANNOT_CHANGE_POSITION_MARGIN_FOR_CROSS_MARGIN Cannot change margin under CrossMargin + '11008': ExchangeError, # TE_CANNOT_REMOVE_POSITION_MARGIN_MORE_THAN_ADDED exceeds the maximum removable Margin + '11009': ExchangeError, # TE_CANNOT_REMOVE_POSITION_MARGIN_DUE_TO_UNREALIZED_PNL exceeds the maximum removable Margin + '11010': InsufficientFunds, # TE_CANNOT_ADD_POSITION_MARGIN_DUE_TO_NO_ENOUGH_AVAILABLE_BALANCE Insufficient available balance + '11011': InvalidOrder, # TE_REDUCE_ONLY_ABORT Cannot accept reduce only order + '11012': InvalidOrder, # TE_REPLACE_TO_INVALID_QTY Order quantity Error + '11013': InvalidOrder, # TE_CONDITIONAL_NO_POSITION Position size is zero. Cannot determine conditional order's quantity + '11014': InvalidOrder, # TE_CONDITIONAL_CLOSE_POSITION_WRONG_SIDE Close position conditional order has the same side + '11015': InvalidOrder, # TE_CONDITIONAL_TRIGGERED_OR_CANCELED + '11016': BadRequest, # TE_ADL_NOT_TRADING_REQUESTED_ACCOUNT Request is routed to the wrong trading engine + '11017': ExchangeError, # TE_ADL_CANNOT_FIND_POSITION Cannot find requested position on current account + '11018': ExchangeError, # TE_NO_NEED_TO_SETTLE_FUNDING The current account does not need to pay a funding fee + '11019': ExchangeError, # TE_FUNDING_ALREADY_SETTLED The current account already pays the funding fee + '11020': ExchangeError, # TE_CANNOT_TRANSFER_OUT_DUE_TO_BONUS Withdraw to wallet needs to remove all remaining bonus. However if bonus is used by position or order cost, withdraw fails. + '11021': ExchangeError, # TE_INVALID_BONOUS_AMOUNT # Grpc command cannot be negative number Invalid bonus amount + '11022': AccountSuspended, # TE_REJECT_DUE_TO_BANNED Account is banned + '11023': ExchangeError, # TE_REJECT_DUE_TO_IN_PROCESS_OF_LIQ Account is in the process of liquidation + '11024': ExchangeError, # TE_REJECT_DUE_TO_IN_PROCESS_OF_ADL Account is in the process of auto-deleverage + '11025': BadRequest, # TE_ROUTE_ERROR Request is routed to the wrong trading engine + '11026': ExchangeError, # TE_UID_ACCOUNT_MISMATCH + '11027': BadSymbol, # TE_SYMBOL_INVALID Invalid number ID or name + '11028': BadSymbol, # TE_CURRENCY_INVALID Invalid currency ID or name + '11029': ExchangeError, # TE_ACTION_INVALID Unrecognized request type + '11030': ExchangeError, # TE_ACTION_BY_INVALID + '11031': DDoSProtection, # TE_SO_NUM_EXCEEDS Number of total conditional orders exceeds the max limit + '11032': DDoSProtection, # TE_AO_NUM_EXCEEDS Number of total active orders exceeds the max limit + '11033': DuplicateOrderId, # TE_ORDER_ID_DUPLICATE Duplicated order ID + '11034': InvalidOrder, # TE_SIDE_INVALID Invalid side + '11035': InvalidOrder, # TE_ORD_TYPE_INVALID Invalid OrderType + '11036': InvalidOrder, # TE_TIME_IN_FORCE_INVALID Invalid TimeInForce + '11037': InvalidOrder, # TE_EXEC_INST_INVALID Invalid ExecType + '11038': InvalidOrder, # TE_TRIGGER_INVALID Invalid trigger type + '11039': InvalidOrder, # TE_STOP_DIRECTION_INVALID Invalid stop direction type + '11040': InvalidOrder, # TE_NO_MARK_PRICE Cannot get valid mark price to create conditional order + '11041': InvalidOrder, # TE_NO_INDEX_PRICE Cannot get valid index price to create conditional order + '11042': InvalidOrder, # TE_NO_LAST_PRICE Cannot get valid last market price to create conditional order + '11043': InvalidOrder, # TE_RISING_TRIGGER_DIRECTLY Conditional order would be triggered immediately + '11044': InvalidOrder, # TE_FALLING_TRIGGER_DIRECTLY Conditional order would be triggered immediately + '11045': InvalidOrder, # TE_TRIGGER_PRICE_TOO_LARGE Conditional order trigger price is too high + '11046': InvalidOrder, # TE_TRIGGER_PRICE_TOO_SMALL Conditional order trigger price is too low + '11047': InvalidOrder, # TE_BUY_TP_SHOULD_GT_BASE TakeProfile BUY conditional order trigger price needs to be greater than reference price + '11048': InvalidOrder, # TE_BUY_SL_SHOULD_LT_BASE StopLoss BUY condition order price needs to be less than the reference price + '11049': InvalidOrder, # TE_BUY_SL_SHOULD_GT_LIQ StopLoss BUY condition order price needs to be greater than liquidation price or it will not trigger + '11050': InvalidOrder, # TE_SELL_TP_SHOULD_LT_BASE TakeProfile SELL conditional order trigger price needs to be less than reference price + '11051': InvalidOrder, # TE_SELL_SL_SHOULD_LT_LIQ StopLoss SELL condition order price needs to be less than liquidation price or it will not trigger + '11052': InvalidOrder, # TE_SELL_SL_SHOULD_GT_BASE StopLoss SELL condition order price needs to be greater than the reference price + '11053': InvalidOrder, # TE_PRICE_TOO_LARGE + '11054': InvalidOrder, # TE_PRICE_WORSE_THAN_BANKRUPT Order price cannot be more aggressive than bankrupt price if self order has instruction to close a position + '11055': InvalidOrder, # TE_PRICE_TOO_SMALL Order price is too low + '11056': InvalidOrder, # TE_QTY_TOO_LARGE Order quantity is too large + '11057': InvalidOrder, # TE_QTY_NOT_MATCH_REDUCE_ONLY Does not allow ReduceOnly order without position + '11058': InvalidOrder, # TE_QTY_TOO_SMALL Order quantity is too small + '11059': InvalidOrder, # TE_TP_SL_QTY_NOT_MATCH_POS Position size is zero. Cannot accept any TakeProfit or StopLoss order + '11060': InvalidOrder, # TE_SIDE_NOT_CLOSE_POS TakeProfit or StopLoss order has wrong side. Cannot close position + '11061': CancelPending, # TE_ORD_ALREADY_PENDING_CANCEL Repeated cancel request + '11062': InvalidOrder, # TE_ORD_ALREADY_CANCELED Order is already canceled + '11063': InvalidOrder, # TE_ORD_STATUS_CANNOT_CANCEL Order is not able to be canceled under current status + '11064': InvalidOrder, # TE_ORD_ALREADY_PENDING_REPLACE Replace request is rejected because order is already in pending replace status + '11065': InvalidOrder, # TE_ORD_REPLACE_NOT_MODIFIED Replace request does not modify any parameters of the order + '11066': InvalidOrder, # TE_ORD_STATUS_CANNOT_REPLACE Order is not able to be replaced under current status + '11067': InvalidOrder, # TE_CANNOT_REPLACE_PRICE Market conditional order cannot change price + '11068': InvalidOrder, # TE_CANNOT_REPLACE_QTY Condtional order for closing position cannot change order quantity, since the order quantity is determined by position size already + '11069': ExchangeError, # TE_ACCOUNT_NOT_IN_RANGE The account ID in the request is not valid or is not in the range of the current process + '11070': BadSymbol, # TE_SYMBOL_NOT_IN_RANGE The symbol is invalid + '11071': InvalidOrder, # TE_ORD_STATUS_CANNOT_TRIGGER + '11072': InvalidOrder, # TE_TKFR_NOT_IN_RANGE The fee value is not valid + '11073': InvalidOrder, # TE_MKFR_NOT_IN_RANGE The fee value is not valid + '11074': InvalidOrder, # TE_CANNOT_ATTACH_TP_SL Order request cannot contain TP/SL parameters when the account already has positions + '11075': InvalidOrder, # TE_TP_TOO_LARGE TakeProfit price is too large + '11076': InvalidOrder, # TE_TP_TOO_SMALL TakeProfit price is too small + '11077': InvalidOrder, # TE_TP_TRIGGER_INVALID Invalid trigger type + '11078': InvalidOrder, # TE_SL_TOO_LARGE StopLoss price is too large + '11079': InvalidOrder, # TE_SL_TOO_SMALL StopLoss price is too small + '11080': InvalidOrder, # TE_SL_TRIGGER_INVALID Invalid trigger type + '11081': InvalidOrder, # TE_RISK_LIMIT_EXCEEDS Total potential position breaches current risk limit + '11082': InsufficientFunds, # TE_CANNOT_COVER_ESTIMATE_ORDER_LOSS The remaining balance cannot cover the potential unrealized PnL for self new order + '11083': InvalidOrder, # TE_TAKE_PROFIT_ORDER_DUPLICATED TakeProfit order already exists + '11084': InvalidOrder, # TE_STOP_LOSS_ORDER_DUPLICATED StopLoss order already exists + '11085': DuplicateOrderId, # TE_CL_ORD_ID_DUPLICATE ClOrdId is duplicated + '11086': InvalidOrder, # TE_PEG_PRICE_TYPE_INVALID PegPriceType is invalid + '11087': InvalidOrder, # TE_BUY_TS_SHOULD_LT_BASE The trailing order's StopPrice should be less than the current last price + '11088': InvalidOrder, # TE_BUY_TS_SHOULD_GT_LIQ The traling order's StopPrice should be greater than the current liquidation price + '11089': InvalidOrder, # TE_SELL_TS_SHOULD_LT_LIQ The traling order's StopPrice should be greater than the current last price + '11090': InvalidOrder, # TE_SELL_TS_SHOULD_GT_BASE The traling order's StopPrice should be less than the current liquidation price + '11091': InvalidOrder, # TE_BUY_REVERT_VALUE_SHOULD_LT_ZERO The PegOffset should be less than zero + '11092': InvalidOrder, # TE_SELL_REVERT_VALUE_SHOULD_GT_ZERO The PegOffset should be greater than zero + '11093': InvalidOrder, # TE_BUY_TTP_SHOULD_ACTIVATE_ABOVE_BASE The activation price should be greater than the current last price + '11094': InvalidOrder, # TE_SELL_TTP_SHOULD_ACTIVATE_BELOW_BASE The activation price should be less than the current last price + '11095': InvalidOrder, # TE_TRAILING_ORDER_DUPLICATED A trailing order exists already + '11096': InvalidOrder, # TE_CLOSE_ORDER_CANNOT_ATTACH_TP_SL An order to close position cannot have trailing instruction + '11097': BadRequest, # TE_CANNOT_FIND_WALLET_OF_THIS_CURRENCY This crypto is not supported + '11098': BadRequest, # TE_WALLET_INVALID_ACTION Invalid action on wallet + '11099': ExchangeError, # TE_WALLET_VID_UNMATCHED Wallet operation request has a wrong wallet vid + '11100': InsufficientFunds, # TE_WALLET_INSUFFICIENT_BALANCE Wallet has insufficient balance + '11101': InsufficientFunds, # TE_WALLET_INSUFFICIENT_LOCKED_BALANCE Locked balance in wallet is not enough for unlock/withdraw request + '11102': BadRequest, # TE_WALLET_INVALID_DEPOSIT_AMOUNT Deposit amount must be greater than zero + '11103': BadRequest, # TE_WALLET_INVALID_WITHDRAW_AMOUNT Withdraw amount must be less than zero + '11104': BadRequest, # TE_WALLET_REACHED_MAX_AMOUNT Deposit makes wallet exceed max amount allowed + '11105': InsufficientFunds, # TE_PLACE_ORDER_INSUFFICIENT_BASE_BALANCE Insufficient funds in base wallet + '11106': InsufficientFunds, # TE_PLACE_ORDER_INSUFFICIENT_QUOTE_BALANCE Insufficient funds in quote wallet + '11107': ExchangeError, # TE_CANNOT_CONNECT_TO_REQUEST_SEQ TradingEngine failed to connect with CrossEngine + '11108': InvalidOrder, # TE_CANNOT_REPLACE_OR_CANCEL_MARKET_ORDER Cannot replace/amend market order + '11109': InvalidOrder, # TE_CANNOT_REPLACE_OR_CANCEL_IOC_ORDER Cannot replace/amend ImmediateOrCancel order + '11110': InvalidOrder, # TE_CANNOT_REPLACE_OR_CANCEL_FOK_ORDER Cannot replace/amend FillOrKill order + '11111': InvalidOrder, # TE_MISSING_ORDER_ID OrderId is missing + '11112': InvalidOrder, # TE_QTY_TYPE_INVALID QtyType is invalid + '11113': BadRequest, # TE_USER_ID_INVALID UserId is invalid + '11114': InvalidOrder, # TE_ORDER_VALUE_TOO_LARGE Order value is too large + '11115': InvalidOrder, # TE_ORDER_VALUE_TOO_SMALL Order value is too small + '11116': InvalidOrder, # TE_BO_NUM_EXCEEDS Details: the total count of brakcet orders should equal or less than 5 + '11117': InvalidOrder, # TE_BO_CANNOT_HAVE_BO_WITH_DIFF_SIDE Details: all bracket orders should have the same Side. + '11118': InvalidOrder, # TE_BO_TP_PRICE_INVALID Details: bracker order take profit price is invalid + '11119': InvalidOrder, # TE_BO_SL_PRICE_INVALID Details: bracker order stop loss price is invalid + '11120': InvalidOrder, # TE_BO_SL_TRIGGER_PRICE_INVALID Details: bracker order stop loss trigger price is invalid + '11121': InvalidOrder, # TE_BO_CANNOT_REPLACE Details: cannot replace bracket order. + '11122': InvalidOrder, # TE_BO_BOTP_STATUS_INVALID Details: bracket take profit order status is invalid + '11123': InvalidOrder, # TE_BO_CANNOT_PLACE_BOTP_OR_BOSL_ORDER Details: cannot place bracket take profit order + '11124': InvalidOrder, # TE_BO_CANNOT_REPLACE_BOTP_OR_BOSL_ORDER Details: cannot place bracket stop loss order + '11125': InvalidOrder, # TE_BO_CANNOT_CANCEL_BOTP_OR_BOSL_ORDER Details: cannot cancel bracket sl/tp order + '11126': InvalidOrder, # TE_BO_DONOT_SUPPORT_API Details: doesn't support bracket order via API + '11128': InvalidOrder, # TE_BO_INVALID_EXECINST Details: ExecInst value is invalid + '11129': InvalidOrder, # TE_BO_MUST_BE_SAME_SIDE_AS_POS Details: bracket order should have the same side's side + '11130': InvalidOrder, # TE_BO_WRONG_SL_TRIGGER_TYPE Details: bracket stop loss order trigger type is invalid + '11131': InvalidOrder, # TE_BO_WRONG_TP_TRIGGER_TYPE Details: bracket take profit order trigger type is invalid + '11132': InvalidOrder, # TE_BO_ABORT_BOSL_DUE_BOTP_CREATE_FAILED Details: cancel bracket stop loss order due failed to create take profit order. + '11133': InvalidOrder, # TE_BO_ABORT_BOSL_DUE_BOPO_CANCELED Details: cancel bracket stop loss order due main order canceled. + '11134': InvalidOrder, # TE_BO_ABORT_BOTP_DUE_BOPO_CANCELED Details: cancel bracket take profit order due main order canceled. + # not documented + '30000': BadRequest, # {"code":30000,"msg":"Please double check input arguments","data":null} + '30018': BadRequest, # {"code":30018,"msg":"phemex.data.size.uplimt","data":null} + '34003': PermissionDenied, # {"code":34003,"msg":"Access forbidden","data":null} + '35104': InsufficientFunds, # {"code":35104,"msg":"phemex.spot.wallet.balance.notenough","data":null} + '39995': RateLimitExceeded, # {"code": "39995","msg": "Too many requests."} + '39996': PermissionDenied, # {"code": "39996","msg": "Access denied."} + '39997': BadSymbol, # {"code":39997,"msg":"Symbol not listed sMOVRUSDT","data":null} + }, + 'broad': { + '401 Insufficient privilege': PermissionDenied, # {"code": "401","msg": "401 Insufficient privilege."} + '401 Request IP mismatch': PermissionDenied, # {"code": "401","msg": "401 Request IP mismatch."} + 'Failed to find api-key': AuthenticationError, # {"msg":"Failed to find api-key 1c5ec63fd-660d-43ea-847a-0d3ba69e106e","code":10500} + 'Missing required parameter': BadRequest, # {"msg":"Missing required parameter","code":10500} + 'API Signature verification failed': AuthenticationError, # {"msg":"API Signature verification failed.","code":10500} + 'Api key not found': AuthenticationError, # {"msg":"Api key not found 698dc9e3-6faa-4910-9476-12857e79e198","code":"10500"} + }, + }, + 'options': { + 'brokerId': 'CCXT123456', # updated from CCXT to CCXT123456 + 'x-phemex-request-expiry': 60, # in seconds + 'createOrderByQuoteRequiresPrice': True, + 'networks': { + 'TRC20': 'TRX', + 'ERC20': 'ETH', + 'BEP20': 'BNB', + }, + 'defaultNetworks': { + 'USDT': 'ETH', + 'MKR': 'ETH', + }, + 'defaultSubType': 'linear', + 'accountsByType': { + 'spot': 'spot', + 'swap': 'future', + }, + 'stableCoins': [ + 'BUSD', + 'FEI', + 'TUSD', + 'USD', + 'USDC', + 'USDD', + 'USDP', + 'USDT', + ], + 'transfer': { + 'fillResponseFromRequest': True, + }, + 'triggerPriceTypesMap': { + 'last': 'ByLastPrice', + 'mark': 'ByMarkPrice', + 'index': 'ByIndexPrice', + 'ask': 'ByAskPrice', + 'bid': 'ByBidPrice', + }, + }, + }) + + def parse_safe_number(self, value=None): + if value is None: + return value + parts = value.split(',') + value = ''.join(parts) + parts = value.split(' ') + return self.safe_number(parts, 0) + + def parse_swap_market(self, market: dict): + # + # { + # "symbol":"BTCUSD", # + # "code":"1", + # "type":"Perpetual", + # "displaySymbol":"BTC / USD", + # "indexSymbol":".BTC", + # "markSymbol":".MBTC", + # "fundingRateSymbol":".BTCFR", + # "fundingRate8hSymbol":".BTCFR8H", + # "contractUnderlyingAssets":"USD", # or eg. `1000 SHIB` + # "settleCurrency":"BTC", + # "quoteCurrency":"USD", + # "contractSize":"1 USD", + # "lotSize":1, + # "tickSize":0.5, + # "priceScale":4, + # "ratioScale":8, + # "pricePrecision":1, + # "minPriceEp":5000, + # "maxPriceEp":10000000000, + # "maxOrderQty":1000000, + # "status":"Listed", + # "tipOrderQty":1000000, + # "listTime":"1574650800000", + # "majorSymbol":true, + # "steps":"50", + # "riskLimits":[ + # {"limit":100,"initialMargin":"1.0%","initialMarginEr":1000000,"maintenanceMargin":"0.5%","maintenanceMarginEr":500000}, + # {"limit":150,"initialMargin":"1.5%","initialMarginEr":1500000,"maintenanceMargin":"1.0%","maintenanceMarginEr":1000000}, + # {"limit":200,"initialMargin":"2.0%","initialMarginEr":2000000,"maintenanceMargin":"1.5%","maintenanceMarginEr":1500000}, + # ], + # "underlyingSymbol":".BTC", + # "baseCurrency":"BTC", + # "settlementCurrency":"BTC", + # "valueScale":8, + # "defaultLeverage":0, + # "maxLeverage":100, + # "initMarginEr":"1000000", + # "maintMarginEr":"500000", + # "defaultRiskLimitEv":10000000000, + # "deleverage":true, + # "makerFeeRateEr":-250000, + # "takerFeeRateEr":750000, + # "fundingInterval":8, + # "marketUrl":"https://phemex.com/trade/BTCUSD", + # "description":"BTCUSD is a BTC/USD perpetual contract priced on the .BTC Index. Each contract is worth 1 USD of Bitcoin. Funding is paid and received every 8 hours. At UTC time: 00:00, 08:00, 16:00.", + # } + # + id = self.safe_string(market, 'symbol') + contractUnderlyingAssets = self.safe_string(market, 'contractUnderlyingAssets') + baseId = self.safe_string(market, 'baseCurrency', contractUnderlyingAssets) + quoteId = self.safe_string(market, 'quoteCurrency') + settleId = self.safe_string(market, 'settleCurrency') + base = self.safe_currency_code(baseId) + base = base.replace(' ', '') # replace space for junction codes, eg. `1000 SHIB` + quote = self.safe_currency_code(quoteId) + settle = self.safe_currency_code(settleId) + inverse = False + if settleId != quoteId: + inverse = True + # some unhandled cases + if not ('baseCurrency' in market) and base == quote: + base = settle + priceScale = self.safe_integer(market, 'priceScale') + ratioScale = self.safe_integer(market, 'ratioScale') + valueScale = self.safe_integer(market, 'valueScale') + minPriceEp = self.safe_string(market, 'minPriceEp') + maxPriceEp = self.safe_string(market, 'maxPriceEp') + makerFeeRateEr = self.safe_string(market, 'makerFeeRateEr') + takerFeeRateEr = self.safe_string(market, 'takerFeeRateEr') + status = self.safe_string(market, 'status') + contractSizeString = self.safe_string(market, 'contractSize', ' ') + contractSize: Num = None + if settle == 'USDT': + contractSize = self.parse_number('1') + elif contractSizeString.find(' '): + # "1 USD" + # "0.005 ETH" + parts = contractSizeString.split(' ') + contractSize = self.parse_number(parts[0]) + else: + # "1.0" + contractSize = self.parse_number(contractSizeString) + return self.safe_market_structure({ + 'id': id, + 'symbol': base + '/' + quote + ':' + settle, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': 'swap', + 'spot': False, + 'margin': False, + 'swap': True, + 'future': False, + 'option': False, + 'active': status == 'Listed', + 'contract': True, + 'linear': not inverse, + 'inverse': inverse, + 'taker': self.parse_number(self.from_en(takerFeeRateEr, ratioScale)), + 'maker': self.parse_number(self.from_en(makerFeeRateEr, ratioScale)), + 'contractSize': contractSize, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'priceScale': priceScale, + 'valueScale': valueScale, + 'ratioScale': ratioScale, + 'precision': { + 'amount': self.safe_number_2(market, 'lotSize', 'qtyStepSize'), + 'price': self.safe_number(market, 'tickSize'), + }, + 'limits': { + 'leverage': { + 'min': self.parse_number('1'), + 'max': self.safe_number(market, 'maxLeverage'), + }, + 'amount': { + 'min': None, + 'max': None, + }, + 'price': { + 'min': self.parse_number(self.from_en(minPriceEp, priceScale)), + 'max': self.parse_number(self.from_en(maxPriceEp, priceScale)), + }, + 'cost': { + 'min': None, + 'max': self.parse_number(self.safe_string(market, 'maxOrderQty')), + }, + }, + 'created': None, + 'info': market, + }) + + def parse_spot_market(self, market: dict): + # + # { + # "symbol":"sBTCUSDT", + # "code":1001, + # "type":"Spot", + # "displaySymbol":"BTC / USDT", + # "quoteCurrency":"USDT", + # "priceScale":8, + # "ratioScale":8, + # "pricePrecision":2, + # "baseCurrency":"BTC", + # "baseTickSize":"0.000001 BTC", + # "baseTickSizeEv":100, + # "quoteTickSize":"0.01 USDT", + # "quoteTickSizeEv":1000000, + # "baseQtyPrecision":6, + # "quoteQtyPrecision":2, + # "minOrderValue":"10 USDT", + # "minOrderValueEv":1000000000, + # "maxBaseOrderSize":"1000 BTC", + # "maxBaseOrderSizeEv":100000000000, + # "maxOrderValue":"5,000,000 USDT", + # "maxOrderValueEv":500000000000000, + # "defaultTakerFee":"0.001", + # "defaultTakerFeeEr":100000, + # "defaultMakerFee":"0.001", + # "defaultMakerFeeEr":100000, + # "description":"BTCUSDT is a BTC/USDT spot trading pair. Minimum order value is 1 USDT", + # "status":"Listed", + # "tipOrderQty":2, + # "listTime":1589338800000, + # "buyPriceUpperLimitPct":110, + # "sellPriceLowerLimitPct":90, + # "leverage":5 + # }, + # + type = self.safe_string_lower(market, 'type') + id = self.safe_string(market, 'symbol') + quoteId = self.safe_string(market, 'quoteCurrency') + baseId = self.safe_string(market, 'baseCurrency') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + status = self.safe_string(market, 'status') + precisionAmount = self.parse_safe_number(self.safe_string(market, 'baseTickSize')) + precisionPrice = self.parse_safe_number(self.safe_string(market, 'quoteTickSize')) + return self.safe_market_structure({ + 'id': id, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': type, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': status == 'Listed', + 'contract': False, + 'linear': None, + 'inverse': None, + 'taker': self.safe_number(market, 'defaultTakerFee'), + 'maker': self.safe_number(market, 'defaultMakerFee'), + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'priceScale': self.safe_integer(market, 'priceScale'), + 'valueScale': self.safe_integer(market, 'valueScale'), + 'ratioScale': self.safe_integer(market, 'ratioScale'), + 'precision': { + 'amount': precisionAmount, + 'price': precisionPrice, + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': precisionAmount, + 'max': self.parse_safe_number(self.safe_string(market, 'maxBaseOrderSize')), + }, + 'price': { + 'min': precisionPrice, + 'max': None, + }, + 'cost': { + 'min': self.parse_safe_number(self.safe_string(market, 'minOrderValue')), + 'max': self.parse_safe_number(self.safe_string(market, 'maxOrderValue')), + }, + }, + 'created': self.safe_integer(market, 'listTime'), + 'info': market, + }) + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for phemex + + https://phemex-docs.github.io/#query-product-information-3 + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + v2ProductsPromise = self.v2GetPublicProducts(params) + # + # { + # "code":0, + # "msg":"", + # "data":{ + # "currencies":[ + # {"currency":"BTC","name":"Bitcoin","code":1,"valueScale":8,"minValueEv":1,"maxValueEv":5000000000000000000,"needAddrTag":0,"status":"Listed","displayCurrency":"BTC","inAssetsDisplay":1,"perpetual":0,"stableCoin":0,"assetsPrecision":8}, + # {"currency":"USD","name":"USD","code":2,"valueScale":4,"minValueEv":1,"maxValueEv":5000000000000000000,"needAddrTag":0,"status":"Listed","displayCurrency":"USD","inAssetsDisplay":1,"perpetual":0,"stableCoin":0,"assetsPrecision":2}, + # {"currency":"USDT","name":"TetherUS","code":3,"valueScale":8,"minValueEv":1,"maxValueEv":5000000000000000000,"needAddrTag":0,"status":"Listed","displayCurrency":"USDT","inAssetsDisplay":1,"perpetual":2,"stableCoin":1,"assetsPrecision":8}, + # ], + # "products":[ + # { + # "symbol":"BTCUSD", + # "code":1, + # "type":"Perpetual" + # "displaySymbol":"BTC / USD", + # "indexSymbol":".BTC", + # "markSymbol":".MBTC", + # "fundingRateSymbol":".BTCFR", + # "fundingRate8hSymbol":".BTCFR8H", + # "contractUnderlyingAssets":"USD", + # "settleCurrency":"BTC", + # "quoteCurrency":"USD", + # "contractSize":1.0, + # "lotSize":1, + # "tickSize":0.5, + # "priceScale":4, + # "ratioScale":8, + # "pricePrecision":1, + # "minPriceEp":5000, + # "maxPriceEp":10000000000, + # "maxOrderQty":1000000, + # "description":"BTC/USD perpetual contracts are priced on the .BTC Index. Each contract is worth 1 USD. Funding fees are paid and received every 8 hours at UTC time: 00:00, 08:00 and 16:00.", + # "status":"Listed", + # "tipOrderQty":1000000, + # "listTime":1574650800000, + # "majorSymbol":true, + # "defaultLeverage":"-10", + # "fundingInterval":28800, + # "maxLeverage":100 + # }, + # { + # "symbol":"sBTCUSDT", + # "code":1001, + # "type":"Spot", + # "displaySymbol":"BTC / USDT", + # "quoteCurrency":"USDT", + # "priceScale":8, + # "ratioScale":8, + # "pricePrecision":2, + # "baseCurrency":"BTC", + # "baseTickSize":"0.000001 BTC", + # "baseTickSizeEv":100, + # "quoteTickSize":"0.01 USDT", + # "quoteTickSizeEv":1000000, + # "baseQtyPrecision":6, + # "quoteQtyPrecision":2, + # "minOrderValue":"10 USDT", + # "minOrderValueEv":1000000000, + # "maxBaseOrderSize":"1000 BTC", + # "maxBaseOrderSizeEv":100000000000, + # "maxOrderValue":"5,000,000 USDT", + # "maxOrderValueEv":500000000000000, + # "defaultTakerFee":"0.001", + # "defaultTakerFeeEr":100000, + # "defaultMakerFee":"0.001", + # "defaultMakerFeeEr":100000, + # "description":"BTCUSDT is a BTC/USDT spot trading pair. Minimum order value is 1 USDT", + # "status":"Listed", + # "tipOrderQty":2, + # "listTime":1589338800000, + # "buyPriceUpperLimitPct":110, + # "sellPriceLowerLimitPct":90, + # "leverage":5 + # }, + # ], + # "perpProductsV2":[ + # { + # "symbol":"BTCUSDT", + # "code":41541, + # "type":"PerpetualV2", + # "displaySymbol":"BTC / USDT", + # "indexSymbol":".BTCUSDT", + # "markSymbol":".MBTCUSDT", + # "fundingRateSymbol":".BTCUSDTFR", + # "fundingRate8hSymbol":".BTCUSDTFR8H", + # "contractUnderlyingAssets":"BTC", + # "settleCurrency":"USDT", + # "quoteCurrency":"USDT", + # "tickSize":"0.1", + # "priceScale":0, + # "ratioScale":0, + # "pricePrecision":1, + # "baseCurrency":"BTC", + # "description":"BTC/USDT perpetual contracts are priced on the .BTCUSDT Index. Each contract is worth 1 BTC. Funding fees are paid and received every 8 hours at UTC time: 00:00, 08:00 and 16:00.", + # "status":"Listed", + # "tipOrderQty":0, + # "listTime":1668225600000, + # "majorSymbol":true, + # "defaultLeverage":"-10", + # "fundingInterval":28800, + # "maxLeverage":100, + # "maxOrderQtyRq":"1000", + # "maxPriceRp":"2000000000", + # "minOrderValueRv":"1", + # "minPriceRp":"1000.0", + # "qtyPrecision":3, + # "qtyStepSize":"0.001", + # "tipOrderQtyRq":"200", + # "maxOpenPosLeverage":100.0 + # }, + # ], + # "riskLimits":[ + # { + # "symbol":"BTCUSD", + # "steps":"50", + # "riskLimits":[ + # {"limit":100,"initialMargin":"1.0%","initialMarginEr":1000000,"maintenanceMargin":"0.5%","maintenanceMarginEr":500000}, + # {"limit":150,"initialMargin":"1.5%","initialMarginEr":1500000,"maintenanceMargin":"1.0%","maintenanceMarginEr":1000000}, + # {"limit":200,"initialMargin":"2.0%","initialMarginEr":2000000,"maintenanceMargin":"1.5%","maintenanceMarginEr":1500000}, + # ] + # }, + # ], + # "leverages":[ + # {"initialMargin":"1.0%","initialMarginEr":1000000,"options":[1,2,3,5,10,25,50,100]}, + # {"initialMargin":"1.5%","initialMarginEr":1500000,"options":[1,2,3,5,10,25,50,66]}, + # {"initialMargin":"2.0%","initialMarginEr":2000000,"options":[1,2,3,5,10,25,33,50]}, + # ], + # "riskLimitsV2":[ + # { + # "symbol":"BTCUSDT", + # "steps":"2000K", + # "riskLimits":[ + # {"limit":2000000,"initialMarginRr":"0.01","maintenanceMarginRr":"0.005"},, + # {"limit":4000000,"initialMarginRr":"0.015","maintenanceMarginRr":"0.0075"}, + # {"limit":6000000,"initialMarginRr":"0.02","maintenanceMarginRr":"0.01"}, + # ] + # }, + # ], + # "leveragesV2":[ + # {"options":[1.0,2.0,3.0,5.0,10.0,25.0,50.0,100.0],"initialMarginRr":"0.01"}, + # {"options":[1.0,2.0,3.0,5.0,10.0,25.0,50.0,66.67],"initialMarginRr":"0.015"}, + # {"options":[1.0,2.0,3.0,5.0,10.0,25.0,33.0,50.0],"initialMarginRr":"0.02"}, + # ], + # "ratioScale":8, + # "md5Checksum":"5c6604814d3c1bafbe602c3d11a7e8bf", + # } + # } + # + v1ProductsPromise = self.v1GetExchangePublicProducts(params) + v2Products, v1Products = [v2ProductsPromise, v1ProductsPromise] + v1ProductsData = self.safe_value(v1Products, 'data', []) + # + # { + # "code":0, + # "msg":"OK", + # "data":[ + # { + # "symbol":"BTCUSD", + # "underlyingSymbol":".BTC", + # "quoteCurrency":"USD", + # "baseCurrency":"BTC", + # "settlementCurrency":"BTC", + # "maxOrderQty":1000000, + # "maxPriceEp":100000000000000, + # "lotSize":1, + # "tickSize":"0.5", + # "contractSize":"1 USD", + # "priceScale":4, + # "ratioScale":8, + # "valueScale":8, + # "defaultLeverage":0, + # "maxLeverage":100, + # "initMarginEr":"1000000", + # "maintMarginEr":"500000", + # "defaultRiskLimitEv":10000000000, + # "deleverage":true, + # "makerFeeRateEr":-250000, + # "takerFeeRateEr":750000, + # "fundingInterval":8, + # "marketUrl":"https://phemex.com/trade/BTCUSD", + # "description":"BTCUSD is a BTC/USD perpetual contract priced on the .BTC Index. Each contract is worth 1 USD of Bitcoin. Funding is paid and received every 8 hours. At UTC time: 00:00, 08:00, 16:00.", + # "type":"Perpetual" + # }, + # ] + # } + # + v2ProductsData = self.safe_dict(v2Products, 'data', {}) + products = self.safe_list(v2ProductsData, 'products', []) + perpetualProductsV2 = self.safe_list(v2ProductsData, 'perpProductsV2', []) + products = self.array_concat(products, perpetualProductsV2) + riskLimits = self.safe_list(v2ProductsData, 'riskLimits', []) + riskLimitsV2 = self.safe_list(v2ProductsData, 'riskLimitsV2', []) + riskLimits = self.array_concat(riskLimits, riskLimitsV2) + currencies = self.safe_list(v2ProductsData, 'currencies', []) + riskLimitsById = self.index_by(riskLimits, 'symbol') + v1ProductsById = self.index_by(v1ProductsData, 'symbol') + currenciesByCode = self.index_by(currencies, 'currency') + result = [] + for i in range(0, len(products)): + market = products[i] + type = self.safe_string_lower(market, 'type') + if (type == 'perpetual') or (type == 'perpetualv2') or (type == 'perpetualpilot'): + id = self.safe_string(market, 'symbol') + riskLimitValues = self.safe_dict(riskLimitsById, id, {}) + market = self.extend(market, riskLimitValues) + v1ProductsValues = self.safe_dict(v1ProductsById, id, {}) + market = self.extend(market, v1ProductsValues) + market = self.parse_swap_market(market) + else: + baseCurrency = self.safe_string(market, 'baseCurrency') + currencyValues = self.safe_dict(currenciesByCode, baseCurrency, {}) + valueScale = self.safe_string(currencyValues, 'valueScale', '8') + market = self.extend(market, {'valueScale': valueScale}) + market = self.parse_spot_market(market) + result.append(market) + return result + + def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = self.v2GetPublicProducts(params) + # + # { + # "code":0, + # "msg":"OK", + # "data":{ + # ..., + # "currencies":[ + # {"currency":"BTC","name":"Bitcoin","code":1,"valueScale":8,"minValueEv":1,"maxValueEv":5000000000000000000,"needAddrTag":0,"status":"Listed","displayCurrency":"BTC","inAssetsDisplay":1,"perpetual":0,"stableCoin":0,"assetsPrecision":8}, + # {"currency":"USD","name":"USD","code":2,"valueScale":4,"minValueEv":1,"maxValueEv":5000000000000000000,"needAddrTag":0,"status":"Listed","displayCurrency":"USD","inAssetsDisplay":1,"perpetual":0,"stableCoin":0,"assetsPrecision":2}, + # {"currency":"USDT","name":"TetherUS","code":3,"valueScale":8,"minValueEv":1,"maxValueEv":5000000000000000000,"needAddrTag":0,"status":"Listed","displayCurrency":"USDT","inAssetsDisplay":1,"perpetual":2,"stableCoin":1,"assetsPrecision":8}, + # ], + # ... + # } + # } + data = self.safe_value(response, 'data', {}) + currencies = self.safe_value(data, 'currencies', []) + result: dict = {} + for i in range(0, len(currencies)): + currency = currencies[i] + id = self.safe_string(currency, 'currency') + code = self.safe_currency_code(id) + valueScaleString = self.safe_string(currency, 'valueScale') + valueScale = int(valueScaleString) + minValueEv = self.safe_string(currency, 'minValueEv') + maxValueEv = self.safe_string(currency, 'maxValueEv') + minAmount: Num = None + maxAmount: Num = None + precision: Num = None + if valueScale is not None: + precisionString = self.parse_precision(valueScaleString) + precision = self.parse_number(precisionString) + minAmount = self.parse_number(Precise.string_mul(minValueEv, precisionString)) + maxAmount = self.parse_number(Precise.string_mul(maxValueEv, precisionString)) + result[code] = self.safe_currency_structure({ + 'id': id, + 'info': currency, + 'code': code, + 'name': self.safe_string(currency, 'name'), + 'active': self.safe_string(currency, 'status') == 'Listed', + 'deposit': None, + 'withdraw': None, + 'fee': None, + 'precision': precision, + 'limits': { + 'amount': { + 'min': minAmount, + 'max': maxAmount, + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + }, + 'valueScale': valueScale, + 'networks': None, + 'type': 'crypto', + }) + return result + + def custom_parse_bid_ask(self, bidask, priceKey=0, amountKey=1, market: Market = None): + if market is None: + raise ArgumentsRequired(self.id + ' customParseBidAsk() requires a market argument') + amount = self.safe_string(bidask, amountKey) + if market['spot']: + amount = self.from_ev(amount, market) + return [ + self.parse_number(self.from_ep(self.safe_string(bidask, priceKey), market)), + self.parse_number(amount), + ] + + def custom_parse_order_book(self, orderbook, symbol, timestamp=None, bidsKey='bids', asksKey='asks', priceKey=0, amountKey=1, market: Market = None): + result: dict = { + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'nonce': None, + } + sides = [bidsKey, asksKey] + for i in range(0, len(sides)): + side = sides[i] + orders = [] + bidasks = self.safe_value(orderbook, side) + for k in range(0, len(bidasks)): + orders.append(self.custom_parse_bid_ask(bidasks[k], priceKey, amountKey, market)) + result[side] = orders + result[bidsKey] = self.sort_by(result[bidsKey], 0, True) + result[asksKey] = self.sort_by(result[asksKey], 0) + return result + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://github.com/phemex/phemex-api-docs/blob/master/Public-Hedged-Perpetual-API.md#queryorderbook + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + # 'id': 123456789, # optional request id + } + response = None + isStableSettled = (market['settle'] == 'USDT') or (market['settle'] == 'USDC') + if market['linear'] and isStableSettled: + response = self.v2GetMdV2Orderbook(self.extend(request, params)) + else: + if (limit is not None) and (limit <= 30): + response = self.v1GetMdOrderbook(self.extend(request, params)) + else: + response = self.v1GetMdFullbook(self.extend(request, params)) + # + # { + # "error": null, + # "id": 0, + # "result": { + # "book": { + # "asks": [ + # [23415000000, 105262000], + # [23416000000, 147914000], + # [23419000000, 160914000], + # ], + # "bids": [ + # [23360000000, 32995000], + # [23359000000, 221887000], + # [23356000000, 284599000], + # ], + # }, + # "depth": 30, + # "sequence": 1592059928, + # "symbol": "sETHUSDT", + # "timestamp": 1592387340020000955, + # "type": "snapshot" + # } + # } + # + result = self.safe_value(response, 'result', {}) + book = self.safe_value_2(result, 'book', 'orderbook_p', {}) + timestamp = self.safe_integer_product(result, 'timestamp', 0.000001) + orderbook = self.custom_parse_order_book(book, symbol, timestamp, 'bids', 'asks', 0, 1, market) + orderbook['nonce'] = self.safe_integer(result, 'sequence') + return orderbook + + def to_en(self, n, scale): + stringN = self.number_to_string(n) + precise = Precise(stringN) + precise.decimals = precise.decimals - scale + precise.reduce() + preciseString = str(precise) + return self.parse_to_numeric(preciseString) + + def to_ev(self, amount, market: dict = None): + if (amount is None) or (market is None): + return amount + return self.to_en(amount, market['valueScale']) + + def to_ep(self, price, market: Market = None): + if (price is None) or (market is None): + return price + return self.to_en(price, market['priceScale']) + + def from_en(self, en, scale): + if en is None or scale is None: + return None + precise = Precise(en) + precise.decimals = self.sum(precise.decimals, scale) + precise.reduce() + return str(precise) + + def from_ep(self, ep, market: Market = None): + if (ep is None) or (market is None): + return ep + return self.from_en(ep, self.safe_integer(market, 'priceScale')) + + def from_ev(self, ev, market: Market = None): + if (ev is None) or (market is None): + return ev + return self.from_en(ev, self.safe_integer(market, 'valueScale')) + + def from_er(self, er, market: Market = None): + if (er is None) or (market is None): + return er + return self.from_en(er, self.safe_integer(market, 'ratioScale')) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # [ + # 1592467200, # timestamp + # 300, # interval + # 23376000000, # last + # 23322000000, # open + # 23381000000, # high + # 23315000000, # low + # 23367000000, # close + # 208671000, # base volume + # 48759063370, # quote volume + # ] + # + baseVolume: Num + if (market is not None) and market['spot']: + baseVolume = self.parse_number(self.from_ev(self.safe_string(ohlcv, 7), market)) + else: + baseVolume = self.safe_number(ohlcv, 7) + return [ + self.safe_timestamp(ohlcv, 0), + self.parse_number(self.from_ep(self.safe_string(ohlcv, 3), market)), + self.parse_number(self.from_ep(self.safe_string(ohlcv, 4), market)), + self.parse_number(self.from_ep(self.safe_string(ohlcv, 5), market)), + self.parse_number(self.from_ep(self.safe_string(ohlcv, 6), market)), + baseVolume, + ] + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://github.com/phemex/phemex-api-docs/blob/master/Public-Hedged-Perpetual-API.md#querykline + https://github.com/phemex/phemex-api-docs/blob/master/Public-Contract-API-en.md#query-kline + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: *only used for USDT settled contracts, otherwise is emulated and not supported by the exchange* timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: *USDT settled/ linear swaps only* end time in ms + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + market = self.market(symbol) + userLimit = limit + request: dict = { + 'symbol': market['id'], + 'resolution': self.safe_string(self.timeframes, timeframe, timeframe), + } + until = self.safe_integer_2(params, 'until', 'to') + params = self.omit(params, ['until']) + isStableSettled = (market['settle'] == 'USDT') or (market['settle'] == 'USDC') + usesSpecialFromToEndpoint = ((market['linear'] or isStableSettled)) and ((since is not None) or (until is not None)) + maxLimit = 1000 + if usesSpecialFromToEndpoint: + maxLimit = 2000 + if limit is None: + limit = maxLimit + request['limit'] = min(limit, maxLimit) + response = None + if market['linear'] or isStableSettled: + if (until is not None) or (since is not None): + candleDuration = self.parse_timeframe(timeframe) + if since is not None: + since = int(round(since / 1000)) + request['from'] = since + else: + # when 'to' is defined since is mandatory + since = (until / 100) - (maxLimit * candleDuration) + if until is not None: + request['to'] = int(round(until / 1000)) + else: + # when since is defined 'to' is mandatory + to = since + (maxLimit * candleDuration) + now = self.seconds() + if to > now: + to = now + request['to'] = to + response = self.publicGetMdV2KlineList(self.extend(request, params)) + else: + response = self.publicGetMdV2KlineLast(self.extend(request, params)) + else: + if since is not None: + # phemex also provides kline query with from/to, however, self interface is NOT recommended and does not work properly. + # we do not send since param to the exchange, instead we calculate appropriate limit param + duration = self.parse_timeframe(timeframe) * 1000 + timeDelta = self.milliseconds() - since + limit = self.parse_to_int(timeDelta / duration) # setting limit to the number of candles after since + response = self.publicGetMdV2Kline(self.extend(request, params)) + # + # { + # "code":0, + # "msg":"OK", + # "data":{ + # "total":-1, + # "rows":[ + # [1592467200,300,23376000000,23322000000,23381000000,23315000000,23367000000,208671000,48759063370], + # [1592467500,300,23367000000,23314000000,23390000000,23311000000,23331000000,234820000,54848948710], + # [1592467800,300,23331000000,23385000000,23391000000,23326000000,23387000000,152931000,35747882250], + # ] + # } + # } + # + data = self.safe_value(response, 'data', {}) + rows = self.safe_list(data, 'rows', []) + return self.parse_ohlcvs(rows, market, timeframe, since, userLimit) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # spot + # + # { + # "askEp": 943836000000, + # "bidEp": 943601000000, + # "highEp": 955946000000, + # "lastEp": 943803000000, + # "lowEp": 924973000000, + # "openEp": 948693000000, + # "symbol": "sBTCUSDT", + # "timestamp": 1592471203505728630, + # "turnoverEv": 111822826123103, + # "volumeEv": 11880532281 + # } + # + # swap + # + # { + # "askEp": 2332500, + # "bidEp": 2331000, + # "fundingRateEr": 10000, + # "highEp": 2380000, + # "indexEp": 2329057, + # "lastEp": 2331500, + # "lowEp": 2274000, + # "markEp": 2329232, + # "openEp": 2337500, + # "openInterest": 1298050, + # "predFundingRateEr": 19921, + # "symbol": "ETHUSD", + # "timestamp": 1592474241582701416, + # "turnoverEv": 47228362330, + # "volume": 4053863 + # } + # linear swap v2 + # + # { + # "closeRp":"16820.5", + # "fundingRateRr":"0.0001", + # "highRp":"16962.1", + # "indexPriceRp":"16830.15651565", + # "lowRp":"16785", + # "markPriceRp":"16830.97534951", + # "openInterestRv":"1323.596", + # "openRp":"16851.7", + # "predFundingRateRr":"0.0001", + # "symbol":"BTCUSDT", + # "timestamp":"1672142789065593096", + # "turnoverRv":"124835296.0538", + # "volumeRq":"7406.95" + # } + # + marketId = self.safe_string(ticker, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + timestamp = self.safe_integer_product(ticker, 'timestamp', 0.000001) + last = self.from_ep(self.safe_string_2(ticker, 'lastEp', 'closeRp'), market) + quoteVolume = self.from_er(self.safe_string_2(ticker, 'turnoverEv', 'turnoverRv'), market) + baseVolume = self.safe_string(ticker, 'volume') + if baseVolume is None: + baseVolume = self.from_ev(self.safe_string_2(ticker, 'volumeEv', 'volumeRq'), market) + open = self.from_ep(self.safe_string(ticker, 'openEp'), market) + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.from_ep(self.safe_string_2(ticker, 'highEp', 'highRp'), market), + 'low': self.from_ep(self.safe_string_2(ticker, 'lowEp', 'lowRp'), market), + 'bid': self.from_ep(self.safe_string(ticker, 'bidEp'), market), + 'bidVolume': None, + 'ask': self.from_ep(self.safe_string(ticker, 'askEp'), market), + 'askVolume': None, + 'vwap': None, + 'open': open, + 'close': last, + 'last': last, + 'previousClose': None, # previous day close + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'info': ticker, + }, market) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://github.com/phemex/phemex-api-docs/blob/master/Public-Hedged-Perpetual-API.md#query24hrsticker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + # 'id': 123456789, # optional request id + } + response = None + if market['swap']: + if market['inverse'] or market['settle'] == 'USD': + response = self.v1GetMdTicker24hr(self.extend(request, params)) + else: + response = self.v2GetMdV2Ticker24hr(self.extend(request, params)) + else: + response = self.v1GetMdSpotTicker24hr(self.extend(request, params)) + # + # spot + # + # { + # "error": null, + # "id": 0, + # "result": { + # "askEp": 943836000000, + # "bidEp": 943601000000, + # "highEp": 955946000000, + # "lastEp": 943803000000, + # "lowEp": 924973000000, + # "openEp": 948693000000, + # "symbol": "sBTCUSDT", + # "timestamp": 1592471203505728630, + # "turnoverEv": 111822826123103, + # "volumeEv": 11880532281 + # } + # } + # + # swap + # + # { + # "error": null, + # "id": 0, + # "result": { + # "askEp": 2332500, + # "bidEp": 2331000, + # "fundingRateEr": 10000, + # "highEp": 2380000, + # "indexEp": 2329057, + # "lastEp": 2331500, + # "lowEp": 2274000, + # "markEp": 2329232, + # "openEp": 2337500, + # "openInterest": 1298050, + # "predFundingRateEr": 19921, + # "symbol": "ETHUSD", + # "timestamp": 1592474241582701416, + # "turnoverEv": 47228362330, + # "volume": 4053863 + # } + # } + # + result = self.safe_dict(response, 'result', {}) + return self.parse_ticker(result, market) + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://phemex-docs.github.io/#query-24-hours-ticker-for-all-symbols-2 # spot + https://phemex-docs.github.io/#query-24-ticker-for-all-symbols # linear + https://phemex-docs.github.io/#query-24-hours-ticker-for-all-symbols # inverse + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + market: Market = None + if symbols is not None: + first = self.safe_value(symbols, 0) + market = self.market(first) + type = None + type, params = self.handle_market_type_and_params('fetchTickers', market, params) + subType = None + subType, params = self.handle_sub_type_and_params('fetchTickers', market, params) + query = self.omit(params, 'type') + response = None + if type == 'spot': + response = self.v1GetMdSpotTicker24hrAll(query) + elif subType == 'inverse' or self.safe_string(market, 'settle') == 'USD': + response = self.v1GetMdTicker24hrAll(query) + else: + response = self.v2GetMdV2Ticker24hrAll(query) + result = self.safe_list(response, 'result', []) + return self.parse_tickers(result, symbols) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://github.com/phemex/phemex-api-docs/blob/master/Public-Hedged-Perpetual-API.md#querytrades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + # 'id': 123456789, # optional request id + } + response = None + isStableSettled = (market['settle'] == 'USDT') or (market['settle'] == 'USDC') + if market['linear'] and isStableSettled: + response = self.v2GetMdV2Trade(self.extend(request, params)) + else: + response = self.v1GetMdTrade(self.extend(request, params)) + # + # { + # "error": null, + # "id": 0, + # "result": { + # "sequence": 1315644947, + # "symbol": "BTCUSD", + # "trades": [ + # [1592541746712239749, 13156448570000, "Buy", 93070000, 40173], + # [1592541740434625085, 13156447110000, "Sell", 93065000, 5000], + # [1592541732958241616, 13156441390000, "Buy", 93070000, 3460], + # ], + # "type": "snapshot" + # } + # } + # + result = self.safe_value(response, 'result', {}) + trades = self.safe_value_2(result, 'trades', 'trades_p', []) + return self.parse_trades(trades, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(public) spot & contract + # + # [ + # 1592541746712239749, + # 13156448570000, + # "Buy", + # 93070000, + # 40173 + # ] + # + # fetchTrades(public) perp + # + # [ + # 1675690986063435800, + # "Sell", + # "22857.4", + # "0.269" + # ] + # + # fetchMyTrades(private) + # + # spot + # + # { + # "qtyType": "ByQuote", + # "transactTimeNs": 1589450974800550100, + # "clOrdID": "8ba59d40-df25-d4b0-14cf-0703f44e9690", + # "orderID": "b2b7018d-f02f-4c59-b4cf-051b9c2d2e83", + # "symbol": "sBTCUSDT", + # "side": "Buy", + # "priceEP": 970056000000, + # "baseQtyEv": 0, + # "quoteQtyEv": 1000000000, + # "action": "New", + # "execStatus": "MakerFill", + # "ordStatus": "Filled", + # "ordType": "Limit", + # "execInst": "None", + # "timeInForce": "GoodTillCancel", + # "stopDirection": "UNSPECIFIED", + # "tradeType": "Trade", + # "stopPxEp": 0, + # "execId": "c6bd8979-07ba-5946-b07e-f8b65135dbb1", + # "execPriceEp": 970056000000, + # "execBaseQtyEv": 103000, + # "execQuoteQtyEv": 999157680, + # "leavesBaseQtyEv": 0, + # "leavesQuoteQtyEv": 0, + # "execFeeEv": 0, + # "feeRateEr": 0 + # "baseCurrency": "BTC", + # "quoteCurrency": "USDT", + # "feeCurrency": "BTC" + # } + # + # swap + # + # { + # "transactTimeNs": 1578026629824704800, + # "symbol": "BTCUSD", + # "currency": "BTC", + # "action": "Replace", + # "side": "Sell", + # "tradeType": "Trade", + # "execQty": 700, + # "execPriceEp": 71500000, + # "orderQty": 700, + # "priceEp": 71500000, + # "execValueEv": 9790209, + # "feeRateEr": -25000, + # "execFeeEv": -2447, + # "ordType": "Limit", + # "execID": "b01671a1-5ddc-5def-b80a-5311522fd4bf", + # "orderID": "b63bc982-be3a-45e0-8974-43d6375fb626", + # "clOrdID": "uuid-1577463487504", + # "execStatus": "MakerFill" + # } + # perpetual + # { + # "accountID": 9328670003, + # "action": "New", + # "actionBy": "ByUser", + # "actionTimeNs": 1666858780876924611, + # "addedSeq": 77751555, + # "apRp": "0", + # "bonusChangedAmountRv": "0", + # "bpRp": "0", + # "clOrdID": "c0327a7d-9064-62a9-28f6-2db9aaaa04e0", + # "closedPnlRv": "0", + # "closedSize": "0", + # "code": 0, + # "cumFeeRv": "0", + # "cumQty": "0", + # "cumValueRv": "0", + # "curAccBalanceRv": "1508.489893982237", + # "curAssignedPosBalanceRv": "24.62786650928", + # "curBonusBalanceRv": "0", + # "curLeverageRr": "-10", + # "curPosSide": "Buy", + # "curPosSize": "0.043", + # "curPosTerm": 1, + # "curPosValueRv": "894.0689", + # "curRiskLimitRv": "1000000", + # "currency": "USDT", + # "cxlRejReason": 0, + # "displayQty": "0.003", + # "execFeeRv": "0", + # "execID": "00000000-0000-0000-0000-000000000000", + # "execPriceRp": "20723.7", + # "execQty": "0", + # "execSeq": 77751555, + # "execStatus": "New", + # "execValueRv": "0", + # "feeRateRr": "0", + # "leavesQty": "0.003", + # "leavesValueRv": "63.4503", + # "message": "No error", + # "ordStatus": "New", + # "ordType": "Market", + # "orderID": "fa64c6f2-47a4-4929-aab4-b7fa9bbc4323", + # "orderQty": "0.003", + # "pegOffsetValueRp": "0", + # "posSide": "Long", + # "priceRp": "21150.1", + # "relatedPosTerm": 1, + # "relatedReqNum": 11, + # "side": "Buy", + # "slTrigger": "ByMarkPrice", + # "stopLossRp": "0", + # "stopPxRp": "0", + # "symbol": "BTCUSDT", + # "takeProfitRp": "0", + # "timeInForce": "ImmediateOrCancel", + # "tpTrigger": "ByLastPrice", + # "tradeType": "Amend", + # "transactTimeNs": 1666858780881545305, + # "userID": 932867 + # } + # + # swap - USDT + # + # { + # "createdAt": 1666226932259, + # "symbol": "ETHUSDT", + # "currency": "USDT", + # "action": 1, + # "tradeType": 1, + # "execQtyRq": "0.01", + # "execPriceRp": "1271.9", + # "side": 1, + # "orderQtyRq": "0.78", + # "priceRp": "1271.9", + # "execValueRv": "12.719", + # "feeRateRr": "0.0001", + # "execFeeRv": "0.0012719", + # "ordType": 2, + # "execId": "8718cae", + # "execStatus": 6 + # } + # spot with fees paid using PT token + # "createdAt": "1714990724076", + # "symbol": "BTCUSDT", + # "currency": "USDT", + # "action": "1", + # "tradeType": "1", + # "execQtyRq": "0.003", + # "execPriceRp": "64935", + # "side": "2", + # "orderQtyRq": "0.003", + # "priceRp": "51600", + # "execValueRv": "194.805", + # "feeRateRr": "0.000495", + # "execFeeRv": "0", + # "ordType": "3", + # "execId": "XXXXXX", + # "execStatus": "7", + # "posSide": "1", + # "ptFeeRv": "0.110012249248", + # "ptPriceRp": "0.876524893" + # + priceString: Str + amountString: Str + timestamp: Int + id: Str = None + side: Str = None + costString: Str = None + type: Str = None + fee = None + feeCostString: Str = None + feeRateString: Str = None + feeCurrencyCode: Str = None + marketId = self.safe_string(trade, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + orderId: Str = None + takerOrMaker: Str = None + if isinstance(trade, list): + tradeLength = len(trade) + timestamp = self.safe_integer_product(trade, 0, 0.000001) + if tradeLength > 4: + id = self.safe_string(trade, tradeLength - 4) + side = self.safe_string_lower(trade, tradeLength - 3) + priceString = self.safe_string(trade, tradeLength - 2) + amountString = self.safe_string(trade, tradeLength - 1) + if isinstance(trade[tradeLength - 2], numbers.Real): + priceString = self.from_ep(priceString, market) + amountString = self.from_ev(amountString, market) + else: + timestamp = self.safe_integer_product(trade, 'transactTimeNs', 0.000001) + if timestamp is None: + timestamp = self.safe_integer(trade, 'createdAt') + id = self.safe_string_2(trade, 'execId', 'execID') + orderId = self.safe_string(trade, 'orderID') + if market['settle'] == 'USDT' or market['settle'] == 'USDC': + sideId = self.safe_string_lower(trade, 'side') + if (sideId == 'buy') or (sideId == 'sell'): + side = sideId + elif sideId is not None: + side = 'buy' if (sideId == '1') else 'sell' + ordType = self.safe_string(trade, 'ordType') + if ordType == '1': + type = 'market' + elif ordType == '2': + type = 'limit' + priceString = self.safe_string(trade, 'execPriceRp') + amountString = self.safe_string(trade, 'execQtyRq') + costString = self.safe_string(trade, 'execValueRv') + feeCostString = self.omit_zero(self.safe_string(trade, 'execFeeRv')) + feeRateString = self.safe_string(trade, 'feeRateRr') + if feeCostString is not None: + currencyId = self.safe_string(trade, 'currency') + feeCurrencyCode = self.safe_currency_code(currencyId) + else: + ptFeeRv = self.omit_zero(self.safe_string(trade, 'ptFeeRv')) + if ptFeeRv is not None: + feeCostString = ptFeeRv + feeCurrencyCode = 'PT' + else: + side = self.safe_string_lower(trade, 'side') + type = self.parse_order_type(self.safe_string(trade, 'ordType')) + execStatus = self.safe_string(trade, 'execStatus') + if execStatus == 'MakerFill': + takerOrMaker = 'maker' + priceString = self.from_ep(self.safe_string(trade, 'execPriceEp'), market) + amountString = self.from_ev(self.safe_string(trade, 'execBaseQtyEv'), market) + amountString = self.safe_string(trade, 'execQty', amountString) + costString = self.from_er(self.safe_string_2(trade, 'execQuoteQtyEv', 'execValueEv'), market) + feeCostString = self.from_er(self.omit_zero(self.safe_string(trade, 'execFeeEv')), market) + if feeCostString is not None: + feeRateString = self.from_er(self.safe_string(trade, 'feeRateEr'), market) + if market['spot']: + feeCurrencyCode = self.safe_currency_code(self.safe_string(trade, 'feeCurrency')) + else: + info = self.safe_value(market, 'info') + if info is not None: + settlementCurrencyId = self.safe_string(info, 'settlementCurrency') + feeCurrencyCode = self.safe_currency_code(settlementCurrencyId) + else: + feeCostString = self.safe_string(trade, 'ptFeeRv') + if feeCostString is not None: + feeCurrencyCode = 'PT' + fee = { + 'cost': feeCostString, + 'rate': feeRateString, + 'currency': feeCurrencyCode, + } + return self.safe_trade({ + 'info': trade, + 'id': id, + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'order': orderId, + 'type': type, + 'side': side, + 'takerOrMaker': takerOrMaker, + 'price': priceString, + 'amount': amountString, + 'cost': costString, + 'fee': fee, + }, market) + + def parse_spot_balance(self, response): + # + # { + # "code":0, + # "msg":"", + # "data":[ + # { + # "currency":"USDT", + # "balanceEv":0, + # "lockedTradingBalanceEv":0, + # "lockedWithdrawEv":0, + # "lastUpdateTimeNs":1592065834511322514, + # "walletVid":0 + # }, + # { + # "currency":"ETH", + # "balanceEv":0, + # "lockedTradingBalanceEv":0, + # "lockedWithdrawEv":0, + # "lastUpdateTimeNs":1592065834511322514, + # "walletVid":0 + # } + # ] + # } + # + timestamp = None + result: dict = {'info': response} + data = self.safe_value(response, 'data', []) + for i in range(0, len(data)): + balance = data[i] + currencyId = self.safe_string(balance, 'currency') + code = self.safe_currency_code(currencyId) + currency = self.safe_value(self.currencies, code, {}) + scale = self.safe_integer(currency, 'valueScale', 8) + account = self.account() + balanceEv = self.safe_string(balance, 'balanceEv') + lockedTradingBalanceEv = self.safe_string(balance, 'lockedTradingBalanceEv') + lockedWithdrawEv = self.safe_string(balance, 'lockedWithdrawEv') + total = self.from_en(balanceEv, scale) + lockedTradingBalance = self.from_en(lockedTradingBalanceEv, scale) + lockedWithdraw = self.from_en(lockedWithdrawEv, scale) + used = Precise.string_add(lockedTradingBalance, lockedWithdraw) + lastUpdateTimeNs = self.safe_integer_product(balance, 'lastUpdateTimeNs', 0.000001) + timestamp = lastUpdateTimeNs if (timestamp is None) else max(timestamp, lastUpdateTimeNs) + account['total'] = total + account['used'] = used + result[code] = account + result['timestamp'] = timestamp + result['datetime'] = self.iso8601(timestamp) + return self.safe_balance(result) + + def parse_swap_balance(self, response): + # usdt + # { + # "info": { + # "code": "0", + # "msg": '', + # "data": { + # "account": { + # "userID": "940666", + # "accountId": "9406660003", + # "currency": "USDT", + # "accountBalanceRv": "99.93143972", + # "totalUsedBalanceRv": "0.40456", + # "bonusBalanceRv": "0" + # }, + # } + # + # { + # "code":0, + # "msg":"", + # "data":{ + # "account":{ + # "accountId":6192120001, + # "currency":"BTC", + # "accountBalanceEv":1254744, + # "totalUsedBalanceEv":0, + # "bonusBalanceEv":1254744 + # } + # } + # } + # + result: dict = {'info': response} + data = self.safe_value(response, 'data', {}) + balance = self.safe_value(data, 'account', {}) + currencyId = self.safe_string(balance, 'currency') + code = self.safe_currency_code(currencyId) + currency = self.currency(code) + valueScale = self.safe_integer(currency, 'valueScale', 8) + account = self.account() + accountBalanceEv = self.safe_string_2(balance, 'accountBalanceEv', 'accountBalanceRv') + totalUsedBalanceEv = self.safe_string_2(balance, 'totalUsedBalanceEv', 'totalUsedBalanceRv') + needsConversion = (code != 'USDT') + account['total'] = self.from_en(accountBalanceEv, valueScale) if needsConversion else accountBalanceEv + account['used'] = self.from_en(totalUsedBalanceEv, valueScale) if needsConversion else totalUsedBalanceEv + result[code] = account + return self.safe_balance(result) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://phemex-docs.github.io/#query-wallets + https://github.com/phemex/phemex-api-docs/blob/master/Public-Hedged-Perpetual-API.md#query-account-positions + https://phemex-docs.github.io/#query-trading-account-and-positions + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: spot or swap + :param str [params.code]: *swap only* currency code of the balance to query(USD, USDT, etc), default is USDT + :returns dict: a `balance structure ` + """ + self.load_markets() + type = None + type, params = self.handle_market_type_and_params('fetchBalance', None, params) + code = self.safe_string(params, 'code') + params = self.omit(params, ['code']) + response = None + request: dict = {} + if (type != 'spot') and (type != 'swap'): + raise BadRequest(self.id + ' does not support ' + type + ' markets, only spot and swap') + if type == 'swap': + settle = None + settle, params = self.handle_option_and_params(params, 'fetchBalance', 'settle', 'USDT') + if code is not None or settle is not None: + coin = None + if code is not None: + coin = code + else: + coin = settle + currency = self.currency(coin) + request['currency'] = currency['id'] + if currency['id'] == 'USDT': + response = self.privateGetGAccountsAccountPositions(self.extend(request, params)) + else: + response = self.privateGetAccountsAccountPositions(self.extend(request, params)) + else: + currency = self.safe_string(params, 'currency') + if currency is None: + raise ArgumentsRequired(self.id + ' fetchBalance() requires a code parameter or a currency or settle parameter for ' + type + ' type') + response = self.privateGetSpotWallets(self.extend(request, params)) + else: + response = self.privateGetSpotWallets(self.extend(request, params)) + # + # usdt + # { + # "info": { + # "code": "0", + # "msg": '', + # "data": { + # "account": { + # "userID": "940666", + # "accountId": "9406660003", + # "currency": "USDT", + # "accountBalanceRv": "99.93143972", + # "totalUsedBalanceRv": "0.40456", + # "bonusBalanceRv": "0" + # }, + # } + # + # spot + # + # { + # "code":0, + # "msg":"", + # "data":[ + # { + # "currency":"USDT", + # "balanceEv":0, + # "lockedTradingBalanceEv":0, + # "lockedWithdrawEv":0, + # "lastUpdateTimeNs":1592065834511322514, + # "walletVid":0 + # }, + # { + # "currency":"ETH", + # "balanceEv":0, + # "lockedTradingBalanceEv":0, + # "lockedWithdrawEv":0, + # "lastUpdateTimeNs":1592065834511322514, + # "walletVid":0 + # } + # ] + # } + # + # swap + # + # { + # "code":0, + # "msg":"", + # "data":{ + # "account":{ + # "accountId":6192120001, + # "currency":"BTC", + # "accountBalanceEv":1254744, + # "totalUsedBalanceEv":0, + # "bonusBalanceEv":1254744 + # }, + # "positions":[ + # { + # "accountID":6192120001, + # "symbol":"BTCUSD", + # "currency":"BTC", + # "side":"None", + # "positionStatus":"Normal", + # "crossMargin":false, + # "leverageEr":0, + # "leverage":0E-8, + # "initMarginReqEr":1000000, + # "initMarginReq":0.01000000, + # "maintMarginReqEr":500000, + # "maintMarginReq":0.00500000, + # "riskLimitEv":10000000000, + # "riskLimit":100.00000000, + # "size":0, + # "value":0E-8, + # "valueEv":0, + # "avgEntryPriceEp":0, + # "avgEntryPrice":0E-8, + # "posCostEv":0, + # "posCost":0E-8, + # "assignedPosBalanceEv":0, + # "assignedPosBalance":0E-8, + # "bankruptCommEv":0, + # "bankruptComm":0E-8, + # "bankruptPriceEp":0, + # "bankruptPrice":0E-8, + # "positionMarginEv":0, + # "positionMargin":0E-8, + # "liquidationPriceEp":0, + # "liquidationPrice":0E-8, + # "deleveragePercentileEr":0, + # "deleveragePercentile":0E-8, + # "buyValueToCostEr":1150750, + # "buyValueToCost":0.01150750, + # "sellValueToCostEr":1149250, + # "sellValueToCost":0.01149250, + # "markPriceEp":96359083, + # "markPrice":9635.90830000, + # "markValueEv":0, + # "markValue":null, + # "unRealisedPosLossEv":0, + # "unRealisedPosLoss":null, + # "estimatedOrdLossEv":0, + # "estimatedOrdLoss":0E-8, + # "usedBalanceEv":0, + # "usedBalance":0E-8, + # "takeProfitEp":0, + # "takeProfit":null, + # "stopLossEp":0, + # "stopLoss":null, + # "realisedPnlEv":0, + # "realisedPnl":null, + # "cumRealisedPnlEv":0, + # "cumRealisedPnl":null + # } + # ] + # } + # } + # + if type == 'swap': + return self.parse_swap_balance(response) + return self.parse_spot_balance(response) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'Created': 'open', + 'Untriggered': 'open', + 'Deactivated': 'closed', + 'Triggered': 'open', + 'Rejected': 'rejected', + 'New': 'open', + 'PartiallyFilled': 'open', + 'Filled': 'closed', + 'Canceled': 'canceled', + 'Suspended': 'canceled', + '1': 'open', + '2': 'canceled', + '3': 'closed', + '4': 'canceled', + '5': 'open', + '6': 'open', + '7': 'closed', + '8': 'canceled', + } + return self.safe_string(statuses, status, status) + + def parse_order_type(self, type: Str): + types: dict = { + '1': 'market', + '2': 'limit', + '3': 'stop', + '4': 'stopLimit', + '5': 'market', + '6': 'limit', + '7': 'market', + '8': 'market', + '9': 'stopLimit', + '10': 'market', + 'Limit': 'limit', + 'Market': 'market', + } + return self.safe_string(types, type, type) + + def parse_time_in_force(self, timeInForce: Str): + timeInForces: dict = { + 'GoodTillCancel': 'GTC', + 'PostOnly': 'PO', + 'ImmediateOrCancel': 'IOC', + 'FillOrKill': 'FOK', + } + return self.safe_string(timeInForces, timeInForce, timeInForce) + + def parse_spot_order(self, order: dict, market: Market = None): + # + # spot + # + # { + # "orderID": "d1d09454-cabc-4a23-89a7-59d43363f16d", + # "clOrdID": "309bcd5c-9f6e-4a68-b775-4494542eb5cb", + # "priceEp": 0, + # "action": "New", + # "trigger": "UNSPECIFIED", + # "pegPriceType": "UNSPECIFIED", + # "stopDirection": "UNSPECIFIED", + # "bizError": 0, + # "symbol": "sBTCUSDT", + # "side": "Buy", + # "baseQtyEv": 0, + # "ordType": "Limit", + # "timeInForce": "GoodTillCancel", + # "ordStatus": "Created", + # "cumFeeEv": 0, + # "cumBaseQtyEv": 0, + # "cumQuoteQtyEv": 0, + # "leavesBaseQtyEv": 0, + # "leavesQuoteQtyEv": 0, + # "avgPriceEp": 0, + # "cumBaseAmountEv": 0, + # "cumQuoteAmountEv": 0, + # "quoteQtyEv": 0, + # "qtyType": "ByBase", + # "stopPxEp": 0, + # "pegOffsetValueEp": 0 + # } + # + # { + # "orderID":"99232c3e-3d6a-455f-98cc-2061cdfe91bc", + # "stopPxEp":0, + # "avgPriceEp":0, + # "qtyType":"ByBase", + # "leavesBaseQtyEv":0, + # "leavesQuoteQtyEv":0, + # "baseQtyEv":"1000000000", + # "feeCurrency":"4", + # "stopDirection":"UNSPECIFIED", + # "symbol":"sETHUSDT", + # "side":"Buy", + # "quoteQtyEv":250000000000, + # "priceEp":25000000000, + # "ordType":"Limit", + # "timeInForce":"GoodTillCancel", + # "ordStatus":"Rejected", + # "execStatus":"NewRejected", + # "createTimeNs":1592675305266037130, + # "cumFeeEv":0, + # "cumBaseValueEv":0, + # "cumQuoteValueEv":0 + # } + # + id = self.safe_string(order, 'orderID') + clientOrderId = self.safe_string(order, 'clOrdID') + if (clientOrderId is not None) and (len(clientOrderId) < 1): + clientOrderId = None + marketId = self.safe_string(order, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + price = self.from_ep(self.safe_string(order, 'priceEp'), market) + amount = self.from_ev(self.safe_string(order, 'baseQtyEv'), market) + remaining = self.omit_zero(self.from_ev(self.safe_string(order, 'leavesBaseQtyEv'), market)) + filled = self.from_ev(self.safe_string_2(order, 'cumBaseQtyEv', 'cumBaseValueEv'), market) + cost = self.from_er(self.safe_string_2(order, 'cumQuoteValueEv', 'quoteQtyEv'), market) + average = self.from_ep(self.safe_string(order, 'avgPriceEp'), market) + status = self.parse_order_status(self.safe_string(order, 'ordStatus')) + side = self.safe_string_lower(order, 'side') + type = self.parse_order_type(self.safe_string(order, 'ordType')) + timestamp = self.safe_integer_product_2(order, 'actionTimeNs', 'createTimeNs', 0.000001) + fee = None + feeCost = self.from_ev(self.safe_string(order, 'cumFeeEv'), market) + if feeCost is not None: + fee = { + 'cost': feeCost, + 'currency': self.safe_currency_code(self.safe_string(order, 'feeCurrency')), + } + timeInForce = self.parse_time_in_force(self.safe_string(order, 'timeInForce')) + triggerPrice = self.parse_number(self.omit_zero(self.from_ep(self.safe_string(order, 'stopPxEp')))) + postOnly = (timeInForce == 'PO') + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'symbol': symbol, + 'type': type, + 'timeInForce': timeInForce, + 'postOnly': postOnly, + 'side': side, + 'price': price, + 'triggerPrice': triggerPrice, + 'amount': amount, + 'cost': cost, + 'average': average, + 'filled': filled, + 'remaining': remaining, + 'status': status, + 'fee': fee, + 'trades': None, + }, market) + + def parse_order_side(self, side): + sides: dict = { + '1': 'buy', + '2': 'sell', + } + return self.safe_string(sides, side, side) + + def parse_swap_order(self, order, market: Market = None): + # + # { + # "bizError":0, + # "orderID":"7a1ad384-44a3-4e54-a102-de4195a29e32", + # "clOrdID":"", + # "symbol":"ETHUSD", + # "side":"Buy", + # "actionTimeNs":1592668973945065381, + # "transactTimeNs":0, + # "orderType":"Market", + # "priceEp":2267500, + # "price":226.75000000, + # "orderQty":1, + # "displayQty":0, + # "timeInForce":"ImmediateOrCancel", + # "reduceOnly":false, + # "closedPnlEv":0, + # "closedPnl":0E-8, + # "closedSize":0, + # "cumQty":0, + # "cumValueEv":0, + # "cumValue":0E-8, + # "leavesQty":1, + # "leavesValueEv":11337, + # "leavesValue":1.13370000, + # "stopDirection":"UNSPECIFIED", + # "stopPxEp":0, + # "stopPx":0E-8, + # "trigger":"UNSPECIFIED", + # "pegOffsetValueEp":0, + # "execStatus":"PendingNew", + # "pegPriceType":"UNSPECIFIED", + # "ordStatus":"Created", + # "execInst": "ReduceOnly" + # } + # + # usdt + # { + # "bizError":"0", + # "orderID":"bd720dff-5647-4596-aa4e-656bac87aaad", + # "clOrdID":"ccxt2022843dffac9477b497", + # "symbol":"LTCUSDT", + # "side":"Buy", + # "actionTimeNs":"1677667878751724052", + # "transactTimeNs":"1677667878754017434", + # "orderType":"Limit", + # "priceRp":"40", + # "orderQtyRq":"0.1", + # "displayQtyRq":"0.1", + # "timeInForce":"GoodTillCancel", + # "reduceOnly":false, + # "closedPnlRv":"0", + # "closedSizeRq":"0", + # "cumQtyRq":"0", + # "cumValueRv":"0", + # "leavesQtyRq":"0.1", + # "leavesValueRv":"4", + # "stopDirection":"UNSPECIFIED", + # "stopPxRp":"0", + # "trigger":"UNSPECIFIED", + # "pegOffsetValueRp":"0", + # "pegOffsetProportionRr":"0", + # "execStatus":"New", + # "pegPriceType":"UNSPECIFIED", + # "ordStatus":"New", + # "execInst":"None", + # "takeProfitRp":"0", + # "stopLossRp":"0" + # } + # + # v2 orderList + # { + # "createdAt":"1677686231301", + # "symbol":"LTCUSDT", + # "orderQtyRq":"0.2", + # "side":"1", + # "posSide":"3", + # "priceRp":"50", + # "execQtyRq":"0", + # "leavesQtyRq":"0.2", + # "execPriceRp":"0", + # "orderValueRv":"10", + # "leavesValueRv":"10", + # "cumValueRv":"0", + # "stopDirection":"0", + # "stopPxRp":"0", + # "trigger":"0", + # "actionBy":"1", + # "execFeeRv":"0", + # "ordType":"2", + # "ordStatus":"5", + # "clOrdId":"4b3b188", + # "orderId":"4b3b1884-87cf-4897-b596-6693b7ed84d1", + # "execStatus":"5", + # "bizError":"0", + # "totalPnlRv":null, + # "avgTransactPriceRp":null, + # "orderDetailsVos":null, + # "tradeType":"0" + # } + # + id = self.safe_string_2(order, 'orderID', 'orderId') + clientOrderId = self.safe_string_2(order, 'clOrdID', 'clOrdId') + if (clientOrderId is not None) and (len(clientOrderId) < 1): + clientOrderId = None + marketId = self.safe_string(order, 'symbol') + symbol = self.safe_symbol(marketId, market) + market = self.safe_market(marketId, market) + status = self.parse_order_status(self.safe_string(order, 'ordStatus')) + side = self.parse_order_side(self.safe_string_lower(order, 'side')) + type = self.parse_order_type(self.safe_string(order, 'orderType')) + price = self.safe_string(order, 'priceRp') + if price is None: + price = self.from_ep(self.safe_string(order, 'priceEp'), market) + amount = self.safe_number_2(order, 'orderQty', 'orderQtyRq') + filled = self.safe_number_2(order, 'cumQty', 'cumQtyRq') + remaining = self.safe_number_2(order, 'leavesQty', 'leavesQtyRq') + timestamp = self.safe_integer_product(order, 'actionTimeNs', 0.000001) + if timestamp is None: + timestamp = self.safe_integer(order, 'createdAt') + cost = self.safe_number_2(order, 'cumValue', 'cumValueRv') + lastTradeTimestamp = self.safe_integer_product(order, 'transactTimeNs', 0.000001) + if lastTradeTimestamp == 0: + lastTradeTimestamp = None + timeInForce = self.parse_time_in_force(self.safe_string(order, 'timeInForce')) + triggerPrice = self.omit_zero(self.safe_string_2(order, 'stopPx', 'stopPxRp')) + postOnly = (timeInForce == 'PO') + reduceOnly = self.safe_value(order, 'reduceOnly') + execInst = self.safe_string(order, 'execInst') + if execInst == 'ReduceOnly': + reduceOnly = True + takeProfit = self.safe_string(order, 'takeProfitRp') + stopLoss = self.safe_string(order, 'stopLossRp') + feeValue = self.omit_zero(self.safe_string(order, 'execFeeRv')) + ptFeeRv = self.omit_zero(self.safe_string(order, 'ptFeeRv')) + fee = None + if feeValue is not None: + fee = { + 'cost': feeValue, + 'currency': market['quote'], + } + elif ptFeeRv is not None: + fee = { + 'cost': ptFeeRv, + 'currency': 'PT', + } + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': clientOrderId, + 'datetime': self.iso8601(timestamp), + 'timestamp': timestamp, + 'lastTradeTimestamp': lastTradeTimestamp, + 'symbol': symbol, + 'type': type, + 'timeInForce': timeInForce, + 'postOnly': postOnly, + 'reduceOnly': reduceOnly, + 'side': side, + 'price': price, + 'triggerPrice': triggerPrice, + 'takeProfitPrice': takeProfit, + 'stopLossPrice': stopLoss, + 'amount': amount, + 'filled': filled, + 'remaining': remaining, + 'cost': cost, + 'average': None, + 'status': status, + 'fee': fee, + 'trades': None, + }) + + def parse_order(self, order: dict, market: Market = None) -> Order: + isSwap = self.safe_bool(market, 'swap', False) + hasPnl = ('closedPnl' in order) or ('closedPnlRv' in order) or ('totalPnlRv' in order) + if isSwap or hasPnl: + return self.parse_swap_order(order, market) + return self.parse_spot_order(order, market) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://github.com/phemex/phemex-api-docs/blob/master/Public-Hedged-Perpetual-API.md#place-order + https://phemex-docs.github.io/#place-order-http-put-prefered-3 + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.trigger]: trigger price for conditional orders + :param dict [params.takeProfit]: *swap only* *takeProfit object in params* containing the triggerPrice at which the attached take profit order will be triggered(perpetual swap markets only) + :param float [params.takeProfit.triggerPrice]: take profit trigger price + :param dict [params.stopLoss]: *swap only* *stopLoss object in params* containing the triggerPrice at which the attached stop loss order will be triggered(perpetual swap markets only) + :param float [params.stopLoss.triggerPrice]: stop loss trigger price + :param str [params.posSide]: *swap only* "Merged" for one way mode, "Long" for buy side of hedged mode, "Short" for sell side of hedged mode + :param bool [params.hedged]: *swap only* True for hedged mode, False for one way mode, default is False + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + requestSide = self.capitalize(side) + type = self.capitalize(type) + request: dict = { + # common + 'symbol': market['id'], + 'side': requestSide, # Sell, Buy + 'ordType': type, # Market, Limit, Stop, StopLimit, MarketIfTouched, LimitIfTouched(additionally for contract-markets: MarketAsLimit, StopAsLimit, MarketIfTouchedAsLimit) + # 'stopPxEp': self.to_ep(stopPx, market), # for conditional orders + # 'priceEp': self.to_ep(price, market), # required for limit orders + # 'timeInForce': 'GoodTillCancel', # GoodTillCancel, PostOnly, ImmediateOrCancel, FillOrKill + # ---------------------------------------------------------------- + # spot + # 'qtyType': 'ByBase', # ByBase, ByQuote + # 'quoteQtyEv': self.to_ep(cost, market), + # 'baseQtyEv': self.to_ev(amount, market), + # 'trigger': 'ByLastPrice', # required for conditional orders + # ---------------------------------------------------------------- + # swap + # 'clOrdID': self.uuid(), # max length 40 + # 'orderQty': self.amount_to_precision(amount, symbol), + # 'reduceOnly': False, + # 'closeOnTrigger': False, # implicit reduceOnly and cancel other orders in the same direction + # 'takeProfitEp': self.to_ep(takeProfit, market), + # 'stopLossEp': self.to_ep(stopLossEp, market), + # 'triggerType': 'ByMarkPrice', # ByMarkPrice, ByLastPrice + # 'pegOffsetValueEp': integer, # Trailing offset from current price. Negative value when position is long, positive when position is short + # 'pegPriceType': 'TrailingStopPeg', # TrailingTakeProfitPeg + # 'text': 'comment', + # 'posSide': Position direction - "Merged" for oneway mode , "Long" / "Short" for hedge mode + } + clientOrderId = self.safe_string_2(params, 'clOrdID', 'clientOrderId') + stopLoss = self.safe_value(params, 'stopLoss') + stopLossDefined = (stopLoss is not None) + takeProfit = self.safe_value(params, 'takeProfit') + takeProfitDefined = (takeProfit is not None) + isStableSettled = (market['settle'] == 'USDT') or (market['settle'] == 'USDC') + if clientOrderId is None: + brokerId = self.safe_string(self.options, 'brokerId', 'CCXT123456') + if brokerId is not None: + request['clOrdID'] = brokerId + self.uuid16() + else: + request['clOrdID'] = clientOrderId + params = self.omit(params, ['clOrdID', 'clientOrderId']) + triggerPrice = self.safe_string_n(params, ['stopPx', 'stopPrice', 'triggerPrice']) + if triggerPrice is not None: + if isStableSettled: + request['stopPxRp'] = self.price_to_precision(symbol, triggerPrice) + else: + request['stopPxEp'] = self.to_ep(triggerPrice, market) + params = self.omit(params, ['stopPx', 'stopPrice', 'stopLoss', 'takeProfit', 'triggerPrice']) + if market['spot']: + qtyType = self.safe_value(params, 'qtyType', 'ByBase') + if (type == 'Market') or (type == 'Stop') or (type == 'MarketIfTouched'): + if price is not None: + qtyType = 'ByQuote' + if triggerPrice is not None: + if type == 'Limit': + request['ordType'] = 'StopLimit' + elif type == 'Market': + request['ordType'] = 'Stop' + request['trigger'] = 'ByLastPrice' + request['qtyType'] = qtyType + if qtyType == 'ByQuote': + cost = self.safe_number(params, 'cost') + params = self.omit(params, 'cost') + if self.options['createOrderByQuoteRequiresPrice']: + if price is not None: + amountString = self.number_to_string(amount) + priceString = self.number_to_string(price) + quoteAmount = Precise.string_mul(amountString, priceString) + cost = self.parse_number(quoteAmount) + elif cost is None: + raise ArgumentsRequired(self.id + ' createOrder() ' + qtyType + ' requires a price argument or a cost parameter') + cost = amount if (cost is None) else cost + costString = self.number_to_string(cost) + request['quoteQtyEv'] = self.to_ev(costString, market) + else: + amountString = self.number_to_string(amount) + request['baseQtyEv'] = self.to_ev(amountString, market) + elif market['swap']: + hedged = self.safe_bool(params, 'hedged', False) + params = self.omit(params, 'hedged') + posSide = self.safe_string_lower(params, 'posSide') + if posSide is None: + if hedged: + reduceOnly = self.safe_bool(params, 'reduceOnly') + if reduceOnly: + side = 'sell' if (side == 'buy') else 'buy' + params = self.omit(params, 'reduceOnly') + posSide = 'Long' if (side == 'buy') else 'Short' + else: + posSide = 'Merged' + posSide = self.capitalize(posSide) + request['posSide'] = posSide + if isStableSettled: + request['orderQtyRq'] = amount + else: + request['orderQty'] = self.parse_to_int(amount) + if triggerPrice is not None: + triggerType = self.safe_string(params, 'triggerType', 'ByMarkPrice') + request['triggerType'] = triggerType + # set direction & exchange specific order type + triggerDirection = None + triggerDirection, params = self.handle_param_string(params, 'triggerDirection') + if triggerDirection is None: + raise ArgumentsRequired(self.id + " createOrder() also requires a 'triggerDirection' parameter with either 'ascending' or 'descending' value") + # the flow defined per https://phemex-docs.github.io/#more-order-type-examples + if triggerDirection == 'ascending' or triggerDirection == 'up': + if side == 'sell': + request['ordType'] = 'MarketIfTouched' if (type == 'Market') else 'LimitIfTouched' + elif side == 'buy': + request['ordType'] = 'Stop' if (type == 'Market') else 'StopLimit' + elif triggerDirection == 'descending' or triggerDirection == 'down': + if side == 'sell': + request['ordType'] = 'Stop' if (type == 'Market') else 'StopLimit' + elif side == 'buy': + request['ordType'] = 'MarketIfTouched' if (type == 'Market') else 'LimitIfTouched' + if stopLossDefined or takeProfitDefined: + if stopLossDefined: + stopLossTriggerPrice = self.safe_value_2(stopLoss, 'triggerPrice', 'stopPrice') + if stopLossTriggerPrice is None: + raise InvalidOrder(self.id + ' createOrder() requires a trigger price in params["stopLoss"]["triggerPrice"] for a stop loss order') + if isStableSettled: + request['stopLossRp'] = self.price_to_precision(symbol, stopLossTriggerPrice) + else: + request['stopLossEp'] = self.to_ep(stopLossTriggerPrice, market) + stopLossTriggerPriceType = self.safe_string_2(stopLoss, 'triggerPriceType', 'slTrigger') + if stopLossTriggerPriceType is not None: + request['slTrigger'] = self.safe_string(self.options['triggerPriceTypesMap'], stopLossTriggerPriceType, stopLossTriggerPriceType) + slLimitPrice = self.safe_string(stopLoss, 'price') + if slLimitPrice is not None: + request['slPxRp'] = self.price_to_precision(symbol, slLimitPrice) + if takeProfitDefined: + takeProfitTriggerPrice = self.safe_value_2(takeProfit, 'triggerPrice', 'stopPrice') + if takeProfitTriggerPrice is None: + raise InvalidOrder(self.id + ' createOrder() requires a trigger price in params["takeProfit"]["triggerPrice"] for a take profit order') + if isStableSettled: + request['takeProfitRp'] = self.price_to_precision(symbol, takeProfitTriggerPrice) + else: + request['takeProfitEp'] = self.to_ep(takeProfitTriggerPrice, market) + takeProfitTriggerPriceType = self.safe_string_2(takeProfit, 'triggerPriceType', 'tpTrigger') + if takeProfitTriggerPriceType is not None: + request['tpTrigger'] = self.safe_string(self.options['triggerPriceTypesMap'], takeProfitTriggerPriceType, takeProfitTriggerPriceType) + tpLimitPrice = self.safe_string(takeProfit, 'price') + if tpLimitPrice is not None: + request['tpPxRp'] = self.price_to_precision(symbol, tpLimitPrice) + if (type == 'Limit') or (type == 'StopLimit') or (type == 'LimitIfTouched'): + if isStableSettled: + request['priceRp'] = self.price_to_precision(symbol, price) + else: + priceString = self.number_to_string(price) + request['priceEp'] = self.to_ep(priceString, market) + takeProfitPrice = self.safe_string(params, 'takeProfitPrice') + if takeProfitPrice is not None: + if isStableSettled: + request['takeProfitRp'] = self.price_to_precision(symbol, takeProfitPrice) + else: + request['takeProfitEp'] = self.to_ep(takeProfitPrice, market) + params = self.omit(params, 'takeProfitPrice') + stopLossPrice = self.safe_string(params, 'stopLossPrice') + if stopLossPrice is not None: + if isStableSettled: + request['stopLossRp'] = self.price_to_precision(symbol, stopLossPrice) + else: + request['stopLossEp'] = self.to_ep(stopLossPrice, market) + params = self.omit(params, 'stopLossPrice') + response = None + if isStableSettled: + response = self.privatePostGOrders(self.extend(request, params)) + elif market['contract']: + response = self.privatePostOrders(self.extend(request, params)) + else: + response = self.privatePostSpotOrders(self.extend(request, params)) + # + # spot + # + # { + # "code": 0, + # "msg": "", + # "data": { + # "orderID": "d1d09454-cabc-4a23-89a7-59d43363f16d", + # "clOrdID": "309bcd5c-9f6e-4a68-b775-4494542eb5cb", + # "priceEp": 0, + # "action": "New", + # "trigger": "UNSPECIFIED", + # "pegPriceType": "UNSPECIFIED", + # "stopDirection": "UNSPECIFIED", + # "bizError": 0, + # "symbol": "sBTCUSDT", + # "side": "Buy", + # "baseQtyEv": 0, + # "ordType": "Limit", + # "timeInForce": "GoodTillCancel", + # "ordStatus": "Created", + # "cumFeeEv": 0, + # "cumBaseQtyEv": 0, + # "cumQuoteQtyEv": 0, + # "leavesBaseQtyEv": 0, + # "leavesQuoteQtyEv": 0, + # "avgPriceEp": 0, + # "cumBaseAmountEv": 0, + # "cumQuoteAmountEv": 0, + # "quoteQtyEv": 0, + # "qtyType": "ByBase", + # "stopPxEp": 0, + # "pegOffsetValueEp": 0 + # } + # } + # + # swap + # + # { + # "code":0, + # "msg":"", + # "data":{ + # "bizError":0, + # "orderID":"7a1ad384-44a3-4e54-a102-de4195a29e32", + # "clOrdID":"", + # "symbol":"ETHUSD", + # "side":"Buy", + # "actionTimeNs":1592668973945065381, + # "transactTimeNs":0, + # "orderType":"Market", + # "priceEp":2267500, + # "price":226.75000000, + # "orderQty":1, + # "displayQty":0, + # "timeInForce":"ImmediateOrCancel", + # "reduceOnly":false, + # "closedPnlEv":0, + # "closedPnl":0E-8, + # "closedSize":0, + # "cumQty":0, + # "cumValueEv":0, + # "cumValue":0E-8, + # "leavesQty":1, + # "leavesValueEv":11337, + # "leavesValue":1.13370000, + # "stopDirection":"UNSPECIFIED", + # "stopPxEp":0, + # "stopPx":0E-8, + # "trigger":"UNSPECIFIED", + # "pegOffsetValueEp":0, + # "execStatus":"PendingNew", + # "pegPriceType":"UNSPECIFIED", + # "ordStatus":"Created" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data, market) + + def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + """ + edit a trade order + + https://github.com/phemex/phemex-api-docs/blob/master/Public-Hedged-Perpetual-API.md#amend-order-by-orderid + + :param str id: cancel order id + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.posSide]: either 'Merged' or 'Long' or 'Short' + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + clientOrderId = self.safe_string_2(params, 'clientOrderId', 'clOrdID') + params = self.omit(params, ['clientOrderId', 'clOrdID']) + isStableSettled = (market['settle'] == 'USDT') or (market['settle'] == 'USDC') + if clientOrderId is not None: + request['clOrdID'] = clientOrderId + else: + request['orderID'] = id + if price is not None: + if isStableSettled: + request['priceRp'] = self.price_to_precision(market['symbol'], price) + else: + request['priceEp'] = self.to_ep(price, market) + # Note the uppercase 'V' in 'baseQtyEV' request. that is exchange's requirement at self moment. However, to avoid mistakes from user side, let's support lowercased 'baseQtyEv' too + finalQty = self.safe_string(params, 'baseQtyEv') + params = self.omit(params, ['baseQtyEv']) + if finalQty is not None: + request['baseQtyEV'] = finalQty + elif amount is not None: + if isStableSettled: + request['orderQtyRq'] = self.amount_to_precision(market['symbol'], amount) + else: + request['baseQtyEV'] = self.to_ev(amount, market) + triggerPrice = self.safe_string_n(params, ['triggerPrice', 'stopPx', 'stopPrice']) + if triggerPrice is not None: + if isStableSettled: + request['stopPxRp'] = self.price_to_precision(symbol, triggerPrice) + else: + request['stopPxEp'] = self.to_ep(triggerPrice, market) + params = self.omit(params, ['triggerPrice', 'stopPx', 'stopPrice']) + response = None + if isStableSettled: + posSide = self.safe_string(params, 'posSide') + if posSide is None: + request['posSide'] = 'Merged' + response = self.privatePutGOrdersReplace(self.extend(request, params)) + elif market['swap']: + response = self.privatePutOrdersReplace(self.extend(request, params)) + else: + response = self.privatePutSpotOrders(self.extend(request, params)) + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data, market) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://github.com/phemex/phemex-api-docs/blob/master/Public-Hedged-Perpetual-API.md#cancel-single-order-by-orderid + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.posSide]: either 'Merged' or 'Long' or 'Short' + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + clientOrderId = self.safe_string_2(params, 'clientOrderId', 'clOrdID') + params = self.omit(params, ['clientOrderId', 'clOrdID']) + if clientOrderId is not None: + request['clOrdID'] = clientOrderId + else: + request['orderID'] = id + response = None + if market['settle'] == 'USDT' or market['settle'] == 'USDC': + posSide = self.safe_string(params, 'posSide') + if posSide is None: + request['posSide'] = 'Merged' + response = self.privateDeleteGOrdersCancel(self.extend(request, params)) + elif market['swap']: + response = self.privateDeleteOrdersCancel(self.extend(request, params)) + else: + response = self.privateDeleteSpotOrders(self.extend(request, params)) + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data, market) + + def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders in a market + + https://github.com/phemex/phemex-api-docs/blob/master/Public-Hedged-Perpetual-API.md#cancelall + + :param str symbol: unified market symbol of the market to cancel orders in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelAllOrders() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + trigger = self.safe_value_2(params, 'stop', 'trigger', False) + params = self.omit(params, ['stop', 'trigger']) + request: dict = { + 'symbol': market['id'], + # 'untriggerred': False, # False to cancel non-conditional orders, True to cancel conditional orders + # 'text': 'up to 40 characters max', + } + if trigger: + request['untriggerred'] = trigger + response = None + if market['settle'] == 'USDT' or market['settle'] == 'USDC': + response = self.privateDeleteGOrdersAll(self.extend(request, params)) + # + # { + # code: '0', + # msg: '', + # data: '1' + # } + # + elif market['swap']: + response = self.privateDeleteOrdersAll(self.extend(request, params)) + # + # { + # code: '0', + # msg: '', + # data: '1' + # } + # + else: + response = self.privateDeleteSpotOrdersAll(self.extend(request, params)) + # + # { + # code: '0', + # msg: '', + # data: { + # total: '1' + # } + # } + # + return [ + self.safe_order({ + 'info': response, + }), + ] + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + + https://phemex-docs.github.io/#query-orders-by-ids + + fetches information on an order made by the user + :param str id: the order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrder() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + clientOrderId = self.safe_string_2(params, 'clientOrderId', 'clOrdID') + params = self.omit(params, ['clientOrderId', 'clOrdID']) + if clientOrderId is not None: + request['clOrdID'] = clientOrderId + else: + request['orderID'] = id + response = None + if market['settle'] == 'USDT' or market['settle'] == 'USDC': + response = self.privateGetApiDataGFuturesOrdersByOrderId(self.extend(request, params)) + elif market['spot']: + response = self.privateGetApiDataSpotsOrdersByOrderId(self.extend(request, params)) + else: + response = self.privateGetExchangeOrder(self.extend(request, params)) + data = self.safe_value(response, 'data', {}) + order = data + if isinstance(data, list): + numOrders = len(data) + if numOrders < 1: + if clientOrderId is not None: + raise OrderNotFound(self.id + ' fetchOrder() ' + symbol + ' order with clientOrderId ' + clientOrderId + ' not found') + else: + raise OrderNotFound(self.id + ' fetchOrder() ' + symbol + ' order with id ' + id + ' not found') + order = self.safe_dict(data, 0, {}) + elif market['spot']: + rows = self.safe_list(data, 'rows', []) + order = self.safe_dict(rows, 0, {}) + return self.parse_order(order, market) + + def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://github.com/phemex/phemex-api-docs/blob/master/Public-Hedged-Perpetual-API.md#queryorder + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrders() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if since is not None: + request['start'] = since + if limit is not None: + request['limit'] = limit + response = None + if market['settle'] == 'USDT' or market['settle'] == 'USDC': + request['currency'] = market['settle'] + response = self.privateGetExchangeOrderV2OrderList(self.extend(request, params)) + elif market['swap']: + response = self.privateGetExchangeOrderList(self.extend(request, params)) + else: + response = self.privateGetApiDataSpotsOrders(self.extend(request, params)) + data = self.safe_value(response, 'data', {}) + rows = self.safe_list(data, 'rows', data) + return self.parse_orders(rows, market, since, limit) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://github.com/phemex/phemex-api-docs/blob/master/Public-Hedged-Perpetual-API.md#queryopenorder + https://github.com/phemex/phemex-api-docs/blob/master/Public-Contract-API-en.md + https://github.com/phemex/phemex-api-docs/blob/master/Public-Spot-API-en.md#spotListAllOpenOrder + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOpenOrders() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = None + try: + if market['settle'] == 'USDT' or market['settle'] == 'USDC': + response = self.privateGetGOrdersActiveList(self.extend(request, params)) + elif market['swap']: + response = self.privateGetOrdersActiveList(self.extend(request, params)) + else: + response = self.privateGetSpotOrders(self.extend(request, params)) + except Exception as e: + if isinstance(e, OrderNotFound): + return [] + raise e + data = self.safe_value(response, 'data', {}) + if isinstance(data, list): + return self.parse_orders(data, market, since, limit) + else: + rows = self.safe_list(data, 'rows', []) + return self.parse_orders(rows, market, since, limit) + + def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://github.com/phemex/phemex-api-docs/blob/master/Public-Hedged-Perpetual-API.md#queryorder + https://github.com/phemex/phemex-api-docs/blob/master/Public-Contract-API-en.md#queryorder + https://github.com/phemex/phemex-api-docs/blob/master/Public-Hedgedd-Perpetual-API.md#query-closed-orders-by-symbol + https://github.com/phemex/phemex-api-docs/blob/master/Public-Spot-API-en.md#spotDataOrdersByIds + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.settle]: the settlement currency to fetch orders for + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + request: dict = { + } + if market is not None: + request['symbol'] = market['id'] + if since is not None: + request['start'] = since + if limit is not None: + request['limit'] = limit + response = None + if (symbol is None) or (self.safe_string(market, 'settle') == 'USDT'): + request['currency'] = self.safe_string(params, 'settle', 'USDT') + response = self.privateGetExchangeOrderV2OrderList(self.extend(request, params)) + elif market['swap']: + response = self.privateGetExchangeOrderList(self.extend(request, params)) + else: + response = self.privateGetExchangeSpotOrder(self.extend(request, params)) + # + # spot + # + # { + # "code":0, + # "msg":"OK", + # "data":{ + # "total":8, + # "rows":[ + # { + # "orderID":"99232c3e-3d6a-455f-98cc-2061cdfe91bc", + # "stopPxEp":0, + # "avgPriceEp":0, + # "qtyType":"ByBase", + # "leavesBaseQtyEv":0, + # "leavesQuoteQtyEv":0, + # "baseQtyEv":"1000000000", + # "feeCurrency":"4", + # "stopDirection":"UNSPECIFIED", + # "symbol":"sETHUSDT", + # "side":"Buy", + # "quoteQtyEv":250000000000, + # "priceEp":25000000000, + # "ordType":"Limit", + # "timeInForce":"GoodTillCancel", + # "ordStatus":"Rejected", + # "execStatus":"NewRejected", + # "createTimeNs":1592675305266037130, + # "cumFeeEv":0, + # "cumBaseValueEv":0, + # "cumQuoteValueEv":0 + # }, + # ] + # } + # } + # + data = self.safe_value(response, 'data', {}) + if isinstance(data, list): + return self.parse_orders(data, market, since, limit) + else: + rows = self.safe_list(data, 'rows', []) + return self.parse_orders(rows, market, since, limit) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://github.com/phemex/phemex-api-docs/blob/master/Public-Contract-API-en.md#query-user-trade + https://github.com/phemex/phemex-api-docs/blob/master/Public-Hedged-Perpetual-API.md#query-user-trade + https://github.com/phemex/phemex-api-docs/blob/master/Public-Spot-API-en.md#spotDataTradesHist + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + type = None + type, params = self.handle_market_type_and_params('fetchMyTrades', market, params) + request: dict = {} + if limit is not None: + limit = min(200, limit) + request['limit'] = limit + isUSDTSettled = (type != 'spot') and ((symbol is None) or (self.safe_string(market, 'settle') == 'USDT')) + if isUSDTSettled: + request['currency'] = 'USDT' + request['offset'] = 0 + if limit is None: + request['limit'] = 200 + elif symbol is not None: + request['symbol'] = market['id'] + if since is not None: + request['start'] = since + response = None + if isUSDTSettled: + response = self.privateGetExchangeOrderV2TradingList(self.extend(request, params)) + elif type == 'swap': + request['tradeType'] = 'Trade' + response = self.privateGetExchangeOrderTrade(self.extend(request, params)) + else: + response = self.privateGetExchangeSpotOrderTrades(self.extend(request, params)) + # + # spot + # + # { + # "code": 0, + # "msg": "OK", + # "data": { + # "total": 1, + # "rows": [ + # { + # "qtyType": "ByQuote", + # "transactTimeNs": 1589450974800550100, + # "clOrdID": "8ba59d40-df25-d4b0-14cf-0703f44e9690", + # "orderID": "b2b7018d-f02f-4c59-b4cf-051b9c2d2e83", + # "symbol": "sBTCUSDT", + # "side": "Buy", + # "priceEP": 970056000000, + # "baseQtyEv": 0, + # "quoteQtyEv": 1000000000, + # "action": "New", + # "execStatus": "MakerFill", + # "ordStatus": "Filled", + # "ordType": "Limit", + # "execInst": "None", + # "timeInForce": "GoodTillCancel", + # "stopDirection": "UNSPECIFIED", + # "tradeType": "Trade", + # "stopPxEp": 0, + # "execId": "c6bd8979-07ba-5946-b07e-f8b65135dbb1", + # "execPriceEp": 970056000000, + # "execBaseQtyEv": 103000, + # "execQuoteQtyEv": 999157680, + # "leavesBaseQtyEv": 0, + # "leavesQuoteQtyEv": 0, + # "execFeeEv": 0, + # "feeRateEr": 0 + # } + # ] + # } + # } + # + # + # swap + # + # { + # "code": 0, + # "msg": "OK", + # "data": { + # "total": 79, + # "rows": [ + # { + # "transactTimeNs": 1606054879331565300, + # "symbol": "BTCUSD", + # "currency": "BTC", + # "action": "New", + # "side": "Buy", + # "tradeType": "Trade", + # "execQty": 5, + # "execPriceEp": 182990000, + # "orderQty": 5, + # "priceEp": 183870000, + # "execValueEv": 27323, + # "feeRateEr": 75000, + # "execFeeEv": 21, + # "ordType": "Market", + # "execID": "5eee56a4-04a9-5677-8eb0-c2fe22ae3645", + # "orderID": "ee0acb82-f712-4543-a11d-d23efca73197", + # "clOrdID": "", + # "execStatus": "TakerFill" + # }, + # ] + # } + # } + # + # swap - usdt + # + # { + # "code": 0, + # "msg": "OK", + # "data": { + # "total": 4, + # "rows": [ + # { + # "createdAt": 1666226932259, + # "symbol": "ETHUSDT", + # "currency": "USDT", + # "action": 1, + # "tradeType": 1, + # "execQtyRq": "0.01", + # "execPriceRp": "1271.9", + # "side": 1, + # "orderQtyRq": "0.78", + # "priceRp": "1271.9", + # "execValueRv": "12.719", + # "feeRateRr": "0.0001", + # "execFeeRv": "0.0012719", + # "ordType": 2, + # "execId": "8718cae", + # "execStatus": 6 + # }, + # ] + # } + # } + # + data = None + if isUSDTSettled: + data = self.safe_value(response, 'data', []) + else: + data = self.safe_value(response, 'data', {}) + data = self.safe_value(data, 'rows', []) + return self.parse_trades(data, market, since, limit) + + def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.network]: the chain name to fetch the deposit address e.g. ETH, TRX, EOS, SOL, etc. + :returns dict: an `address structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + } + defaultNetworks = self.safe_dict(self.options, 'defaultNetworks') + defaultNetwork = self.safe_string_upper(defaultNetworks, code) + networks = self.safe_dict(self.options, 'networks', {}) + network = self.safe_string_upper_2(params, 'network', 'chainName', defaultNetwork) + network = self.safe_string(networks, network, network) + if network is None: + raise ArgumentsRequired(self.id + ' fetchDepositAddress() requires a network parameter') + else: + request['chainName'] = network + params = self.omit(params, 'network') + response = self.privateGetExchangeWalletsV2DepositAddress(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "OK", + # "data": { + # "address": "tb1qxel5wq5gumt", + # "tag": "", + # "notice": False, + # "accountType": 1, + # "contractName": null, + # "chainTokenUrl": null, + # "sign": null + # } + # } + # + data = self.safe_value(response, 'data', {}) + address = self.safe_string(data, 'address') + tag = self.safe_string(data, 'tag') + self.check_address(address) + return { + 'info': response, + 'currency': code, + 'network': None, + 'address': address, + 'tag': tag, + } + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + currency = None + if code is not None: + currency = self.currency(code) + response = self.privateGetExchangeWalletsDepositList(params) + # + # { + # "code":0, + # "msg":"OK", + # "data":[ + # { + # "id":29200, + # "currency":"USDT", + # "currencyCode":3, + # "txHash":"0x0bdbdc47807769a03b158d5753f54dfc58b92993d2f5e818db21863e01238e5d", + # "address":"0x5bfbf60e0fa7f63598e6cfd8a7fd3ffac4ccc6ad", + # "amountEv":3000000000, + # "confirmations":13, + # "type":"Deposit", + # "status":"Success", + # "createdAt":1592722565000 + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_transactions(data, currency, since, limit) + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + currency = None + if code is not None: + currency = self.currency(code) + response = self.privateGetExchangeWalletsWithdrawList(params) + # + # { + # "code":0, + # "msg":"OK", + # "data":[ + # { + # "address": "1Lxxxxxxxxxxx" + # "amountEv": 200000 + # "currency": "BTC" + # "currencyCode": 1 + # "expiredTime": 0 + # "feeEv": 50000 + # "rejectReason": null + # "status": "Succeed" + # "txHash": "44exxxxxxxxxxxxxxxxxxxxxx" + # "withdrawStatus: "" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_transactions(data, currency, since, limit) + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'Success': 'ok', + 'Succeed': 'ok', + 'Rejected': 'failed', + 'Security check failed': 'failed', + 'SecurityCheckFailed': 'failed', + 'Expired': 'failed', + 'Address Risk': 'failed', + 'Security Checking': 'pending', + 'SecurityChecking': 'pending', + 'Pending Review': 'pending', + 'Pending Transfer': 'pending', + 'AmlCsApporve': 'pending', + 'New': 'pending', + 'Confirmed': 'pending', + 'Cancelled': 'canceled', + } + return self.safe_string(statuses, status, status) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # withdraw + # + # { + # "id": "10000001", + # "freezeId": null, + # "address": "44exxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + # "amountRv": "100", + # "chainCode": "11", + # "chainName": "TRX", + # "currency": "USDT", + # "currencyCode": 3, + # "email": "abc@gmail.com", + # "expiredTime": "0", + # "feeRv": "1", + # "nickName": null, + # "phone": null, + # "rejectReason": "", + # "submitedAt": "1670000000000", + # "submittedAt": "1670000000000", + # "txHash": null, + # "userId": "10000001", + # "status": "Success" + # + # fetchDeposits + # + # { + # "id": "29200", + # "currency": "USDT", + # "currencyCode": "3", + # "chainName": "ETH", + # "chainCode": "4", + # "txHash": "0x0bdbdc47807769a03b158d5753f54dfc58b92993d2f5e818db21863e01238e5d", + # "address": "0x5bfbf60e0fa7f63598e6cfd8a7fd3ffac4ccc6ad", + # "amountEv": "3000000000", + # "confirmations": "13", + # "type": "Deposit", + # "status": "Success", + # "createdAt": "1592722565000", + # } + # + # fetchWithdrawals + # + # { + # "id": "10000001", + # "userId": "10000001", + # "freezeId": "10000002", + # "phone": null, + # "email": "abc@gmail.com", + # "nickName": null, + # "currency": "USDT", + # "currencyCode": "3", + # "status": "Succeed", + # "withdrawStatus": "Succeed", + # "amountEv": "8800000000", + # "feeEv": "1200000000", + # "address": "0x5xxxad", + # "txHash: "0x0xxxx5d", + # "submitedAt": "1702571922000", + # "submittedAt": "1702571922000", + # "expiredTime": "0", + # "rejectReason": null, + # "chainName": "ETH", + # "chainCode": "4", + # "proxyAddress": null + # } + # + id = self.safe_string(transaction, 'id') + address = self.safe_string(transaction, 'address') + tag = None + txid = self.safe_string(transaction, 'txHash') + currencyId = self.safe_string(transaction, 'currency') + currency = self.safe_currency(currencyId, currency) + code = currency['code'] + networkId = self.safe_string(transaction, 'chainName') + timestamp = self.safe_integer_n(transaction, ['createdAt', 'submitedAt', 'submittedAt']) + type = self.safe_string_lower(transaction, 'type') + feeCost = self.parse_number(self.from_en(self.safe_string(transaction, 'feeEv'), currency['valueScale'])) + if feeCost is None: + feeCost = self.safe_number(transaction, 'feeRv') + fee = None + if feeCost is not None: + type = 'withdrawal' + fee = { + 'cost': feeCost, + 'currency': code, + } + status = self.parse_transaction_status(self.safe_string(transaction, 'status')) + amount = self.parse_number(self.from_en(self.safe_string(transaction, 'amountEv'), currency['valueScale'])) + if amount is None: + amount = self.safe_number(transaction, 'amountRv') + return { + 'info': transaction, + 'id': id, + 'txid': txid, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': self.network_id_to_code(networkId), + 'address': address, + 'addressTo': address, + 'addressFrom': None, + 'tag': tag, + 'tagTo': tag, + 'tagFrom': None, + 'type': type, + 'amount': amount, + 'currency': code, + 'status': status, + 'updated': None, + 'comment': None, + 'internal': None, + 'fee': fee, + } + + def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://github.com/phemex/phemex-api-docs/blob/master/Public-Contract-API-en.md#query-trading-account-and-positions + https://github.com/phemex/phemex-api-docs/blob/master/Public-Hedged-Perpetual-API.md#query-account-positions + https://phemex-docs.github.io/#query-account-positions-with-unrealized-pnl + + :param str[] [symbols]: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.code]: the currency code to fetch positions for, USD, BTC or USDT, USDT is the default + :param str [params.method]: *USDT contracts only* 'privateGetGAccountsAccountPositions' or 'privateGetGAccountsAccountPositions' default is 'privateGetGAccountsAccountPositions' + :returns dict[]: a list of `position structure ` + """ + self.load_markets() + symbols = self.market_symbols(symbols) + subType = None + code = self.safe_string_2(params, 'currency', 'code', 'USDT') + params = self.omit(params, ['currency', 'code']) + settle = None + market = None + firstSymbol = self.safe_string(symbols, 0) + if firstSymbol is not None: + market = self.market(firstSymbol) + settle = market['settle'] + code = market['settle'] + else: + settle, params = self.handle_option_and_params(params, 'fetchPositions', 'settle', code) + subType, params = self.handle_sub_type_and_params('fetchPositions', market, params) + isUSDTSettled = settle == 'USDT' + if isUSDTSettled: + code = 'USDT' + elif settle == 'BTC': + code = 'BTC' + elif code is None: + code = 'USD' if (subType == 'linear') else 'BTC' + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + } + response = None + if isUSDTSettled: + method = None + method, params = self.handle_option_and_params(params, 'fetchPositions', 'method', 'privateGetGAccountsAccountPositions') + if method == 'privateGetGAccountsAccountPositions': + response = self.privateGetGAccountsAccountPositions(self.extend(request, params)) + else: + response = self.privateGetGAccountsPositions(self.extend(request, params)) + else: + response = self.privateGetAccountsAccountPositions(self.extend(request, params)) + # + # { + # "code":0,"msg":"", + # "data":{ + # "account":{ + # "accountId":6192120001, + # "currency":"BTC", + # "accountBalanceEv":1254744, + # "totalUsedBalanceEv":0, + # "bonusBalanceEv":1254744 + # }, + # "positions":[ + # { + # "accountID":6192120001, + # "symbol":"BTCUSD", + # "currency":"BTC", + # "side":"None", + # "positionStatus":"Normal", + # "crossMargin":false, + # "leverageEr":100000000, + # "leverage":1.00000000, + # "initMarginReqEr":100000000, + # "initMarginReq":1.00000000, + # "maintMarginReqEr":500000, + # "maintMarginReq":0.00500000, + # "riskLimitEv":10000000000, + # "riskLimit":100.00000000, + # "size":0, + # "value":0E-8, + # "valueEv":0, + # "avgEntryPriceEp":0, + # "avgEntryPrice":0E-8, + # "posCostEv":0, + # "posCost":0E-8, + # "assignedPosBalanceEv":0, + # "assignedPosBalance":0E-8, + # "bankruptCommEv":0, + # "bankruptComm":0E-8, + # "bankruptPriceEp":0, + # "bankruptPrice":0E-8, + # "positionMarginEv":0, + # "positionMargin":0E-8, + # "liquidationPriceEp":0, + # "liquidationPrice":0E-8, + # "deleveragePercentileEr":0, + # "deleveragePercentile":0E-8, + # "buyValueToCostEr":100225000, + # "buyValueToCost":1.00225000, + # "sellValueToCostEr":100075000, + # "sellValueToCost":1.00075000, + # "markPriceEp":135736070, + # "markPrice":13573.60700000, + # "markValueEv":0, + # "markValue":null, + # "unRealisedPosLossEv":0, + # "unRealisedPosLoss":null, + # "estimatedOrdLossEv":0, + # "estimatedOrdLoss":0E-8, + # "usedBalanceEv":0, + # "usedBalance":0E-8, + # "takeProfitEp":0, + # "takeProfit":null, + # "stopLossEp":0, + # "stopLoss":null, + # "cumClosedPnlEv":0, + # "cumFundingFeeEv":0, + # "cumTransactFeeEv":0, + # "realisedPnlEv":0, + # "realisedPnl":null, + # "cumRealisedPnlEv":0, + # "cumRealisedPnl":null + # } + # ] + # } + # } + # + data = self.safe_value(response, 'data', {}) + positions = self.safe_value(data, 'positions', []) + result = [] + for i in range(0, len(positions)): + position = positions[i] + result.append(self.parse_position(position)) + return self.filter_by_array_positions(result, 'symbol', symbols, False) + + def parse_position(self, position: dict, market: Market = None): + # + # { + # "userID": "811370", + # "accountID": "8113700002", + # "symbol": "ETHUSD", + # "currency": "USD", + # "side": "Buy", + # "positionStatus": "Normal", + # "crossMargin": False, + # "leverageEr": "200000000", + # "leverage": "2.00000000", + # "initMarginReqEr": "50000000", + # "initMarginReq": "0.50000000", + # "maintMarginReqEr": "1000000", + # "maintMarginReq": "0.01000000", + # "riskLimitEv": "5000000000", + # "riskLimit": "500000.00000000", + # "size": "1", + # "value": "22.22370000", + # "valueEv": "222237", + # "avgEntryPriceEp": "44447400", + # "avgEntryPrice": "4444.74000000", + # "posCostEv": "111202", + # "posCost": "11.12020000", + # "assignedPosBalanceEv": "111202", + # "assignedPosBalance": "11.12020000", + # "bankruptCommEv": "84", + # "bankruptComm": "0.00840000", + # "bankruptPriceEp": "22224000", + # "bankruptPrice": "2222.40000000", + # "positionMarginEv": "111118", + # "positionMargin": "11.11180000", + # "liquidationPriceEp": "22669000", + # "liquidationPrice": "2266.90000000", + # "deleveragePercentileEr": "0", + # "deleveragePercentile": "0E-8", + # "buyValueToCostEr": "50112500", + # "buyValueToCost": "0.50112500", + # "sellValueToCostEr": "50187500", + # "sellValueToCost": "0.50187500", + # "markPriceEp": "31332499", + # "markPrice": "3133.24990000", + # "markValueEv": "0", + # "markValue": null, + # "unRealisedPosLossEv": "0", + # "unRealisedPosLoss": null, + # "estimatedOrdLossEv": "0", + # "estimatedOrdLoss": "0E-8", + # "usedBalanceEv": "111202", + # "usedBalance": "11.12020000", + # "takeProfitEp": "0", + # "takeProfit": null, + # "stopLossEp": "0", + # "stopLoss": null, + # "cumClosedPnlEv": "-1546", + # "cumFundingFeeEv": "1605", + # "cumTransactFeeEv": "8438", + # "realisedPnlEv": "0", + # "realisedPnl": null, + # "cumRealisedPnlEv": "0", + # "cumRealisedPnl": null, + # "transactTimeNs": "1641571200001885324", + # "takerFeeRateEr": "0", + # "makerFeeRateEr": "0", + # "term": "6", + # "lastTermEndTimeNs": "1607711882505745356", + # "lastFundingTimeNs": "1641571200000000000", + # "curTermRealisedPnlEv": "-1567", + # "execSeq": "12112761561" + # } + # + marketId = self.safe_string(position, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + collateral = self.safe_string_2(position, 'positionMargin', 'positionMarginRv') + notionalString = self.safe_string_2(position, 'value', 'valueRv') + maintenanceMarginPercentageString = self.safe_string_2(position, 'maintMarginReq', 'maintMarginReqRr') + maintenanceMarginString = Precise.string_mul(notionalString, maintenanceMarginPercentageString) + initialMarginString = self.safe_string_2(position, 'assignedPosBalance', 'assignedPosBalanceRv') + initialMarginPercentageString = Precise.string_div(initialMarginString, notionalString) + liquidationPrice = self.safe_number_2(position, 'liquidationPrice', 'liquidationPriceRp') + markPriceString = self.safe_string_2(position, 'markPrice', 'markPriceRp') + contracts = self.safe_string_2(position, 'size', 'sizeRq') + contractSize = self.safe_value(market, 'contractSize') + contractSizeString = self.number_to_string(contractSize) + leverage = self.parse_number(Precise.string_abs((self.safe_string_2(position, 'leverage', 'leverageRr')))) + entryPriceString = self.safe_string_2(position, 'avgEntryPrice', 'avgEntryPriceRp') + rawSide = self.safe_string(position, 'side') + side = None + if rawSide is not None: + side = 'long' if (rawSide == 'Buy') else 'short' + # Inverse long contract: unRealizedPnl = (posSize * contractSize) / avgEntryPrice - (posSize * contractSize) / markPrice + # Inverse short contract: unRealizedPnl = (posSize *contractSize) / markPrice - (posSize * contractSize) / avgEntryPrice + # Linear long contract: unRealizedPnl = (posSize * contractSize) * markPrice - (posSize * contractSize) * avgEntryPrice + # Linear short contract: unRealizedPnl = (posSize * contractSize) * avgEntryPrice - (posSize * contractSize) * markPrice + priceDiff = None + if market['linear']: + if side == 'long': + priceDiff = Precise.string_sub(markPriceString, entryPriceString) + else: + priceDiff = Precise.string_sub(entryPriceString, markPriceString) + else: + # inverse + if side == 'long': + priceDiff = Precise.string_sub(Precise.string_div('1', entryPriceString), Precise.string_div('1', markPriceString)) + else: + priceDiff = Precise.string_sub(Precise.string_div('1', markPriceString), Precise.string_div('1', entryPriceString)) + unrealizedPnl = Precise.string_mul(Precise.string_mul(priceDiff, contracts), contractSizeString) + # the unrealizedPnl is only available in a specific endpoint which much higher RL limits + apiUnrealizedPnl = self.safe_string(position, 'unRealisedPnlRv', unrealizedPnl) + marginRatio = Precise.string_div(maintenanceMarginString, collateral) + isCross = self.safe_value(position, 'crossMargin') + return self.safe_position({ + 'info': position, + 'id': self.safe_string(position, 'execSeq'), + 'symbol': symbol, + 'contracts': self.parse_number(contracts), + 'contractSize': contractSize, + 'realizedPnl': self.safe_number(position, 'curTermRealisedPnlRv'), + 'unrealizedPnl': self.parse_number(apiUnrealizedPnl), + 'leverage': leverage, + 'liquidationPrice': liquidationPrice, + 'collateral': self.parse_number(collateral), + 'notional': self.parse_number(notionalString), + 'markPrice': self.parse_number(markPriceString), # markPrice lags a bit ¯\_(ツ)_/¯ + 'lastPrice': None, + 'entryPrice': self.parse_number(entryPriceString), + 'timestamp': None, + 'lastUpdateTimestamp': self.safe_integer_product(position, 'transactTimeNs', 0.000001), + 'initialMargin': self.parse_number(initialMarginString), + 'initialMarginPercentage': self.parse_number(initialMarginPercentageString), + 'maintenanceMargin': self.parse_number(maintenanceMarginString), + 'maintenanceMarginPercentage': self.parse_number(maintenanceMarginPercentageString), + 'marginRatio': self.parse_number(marginRatio), + 'datetime': None, + 'marginMode': 'cross' if isCross else 'isolated', + 'side': side, + 'hedged': self.safe_string(position, 'posMode') == 'Hedged', + 'percentage': None, + 'stopLossPrice': None, + 'takeProfitPrice': None, + }) + + def fetch_funding_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch the history of funding payments paid and received on self account + + https://github.com/phemex/phemex-api-docs/blob/master/Public-Hedged-Perpetual-API.md#futureDataFundingFeesHist + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch funding history for + :param int [limit]: the maximum number of funding history structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding history structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchFundingHistory() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + # 'limit': 20, # Page size default 20, max 200 + # 'offset': 0, # Page start default 0 + } + if limit is not None: + if limit > 200: + raise BadRequest(self.id + ' fetchFundingHistory() limit argument cannot exceed 200') + request['limit'] = limit + response = None + isStableSettled = market['settle'] == 'USDT' or market['settle'] == 'USDC' + if isStableSettled: + response = self.privateGetApiDataGFuturesFundingFees(self.extend(request, params)) + else: + response = self.privateGetApiDataFuturesFundingFees(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "OK", + # "data": { + # "rows": [ + # { + # "symbol": "BTCUSD", + # "currency": "BTC", + # "execQty": 18, # "execQty" regular, but "execQtyRq" in hedge + # "side": "Buy", + # "execPriceEp": 360086455, # "execPriceEp" regular, but "execPriceRp" in hedge + # "execValueEv": 49987, # "execValueEv" regular, but "execValueRv" in hedge + # "fundingRateEr": 10000, # "fundingRateEr" regular, but "fundingRateRr" in hedge + # "feeRateEr": 10000, # "feeRateEr" regular, but "feeRateRr" in hedge + # "execFeeEv": 5, # "execFeeEv" regular, but "execFeeRv" in hedge + # "createTime": 1651881600000 + # } + # ] + # } + # } + # + data = self.safe_value(response, 'data', {}) + rows = self.safe_value(data, 'rows', []) + result = [] + for i in range(0, len(rows)): + entry = rows[i] + timestamp = self.safe_integer(entry, 'createTime') + execFee = self.safe_string_2(entry, 'execFeeEv', 'execFeeRv') + currencyCode = self.safe_currency_code(self.safe_string(entry, 'currency')) + result.append({ + 'info': entry, + 'symbol': self.safe_string(entry, 'symbol'), + 'code': currencyCode, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': None, + 'amount': self.parse_funding_fee_to_precision(execFee, market, currencyCode), + }) + return result + + def parse_funding_fee_to_precision(self, value, market: Market = None, currencyCode: Str = None): + if value is None or currencyCode is None: + return value + # it was confirmed by phemex support, that USDT contracts use direct amounts in funding fees, while USD & INVERSE needs 'valueScale' + isStableSettled = market['settle'] == 'USDT' or market['settle'] == 'USDC' + if not isStableSettled: + currency = self.safe_currency(currencyCode) + scale = self.safe_string(currency['info'], 'valueScale') + tickPrecision = self.parse_precision(scale) + value = Precise.string_mul(value, tickPrecision) + return value + + def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + self.load_markets() + market = self.market(symbol) + if not market['swap']: + raise BadSymbol(self.id + ' fetchFundingRate() supports swap contracts only') + request: dict = { + 'symbol': market['id'], + } + response: dict = {} + if not market['linear']: + response = self.v1GetMdTicker24hr(self.extend(request, params)) + else: + response = self.v2GetMdV2Ticker24hr(self.extend(request, params)) + # + # { + # "error": null, + # "id": 0, + # "result": { + # "askEp": 2332500, + # "bidEp": 2331000, + # "fundingRateEr": 10000, + # "highEp": 2380000, + # "indexEp": 2329057, + # "lastEp": 2331500, + # "lowEp": 2274000, + # "markEp": 2329232, + # "openEp": 2337500, + # "openInterest": 1298050, + # "predFundingRateEr": 19921, + # "symbol": "ETHUSD", + # "timestamp": 1592474241582701416, + # "turnoverEv": 47228362330, + # "volume": 4053863 + # } + # } + # + result = self.safe_value(response, 'result', {}) + return self.parse_funding_rate(result, market) + + def parse_funding_rate(self, contract, market: Market = None) -> FundingRate: + # + # { + # "askEp": 2332500, + # "bidEp": 2331000, + # "fundingRateEr": 10000, + # "highEp": 2380000, + # "indexEp": 2329057, + # "lastEp": 2331500, + # "lowEp": 2274000, + # "markEp": 2329232, + # "openEp": 2337500, + # "openInterest": 1298050, + # "predFundingRateEr": 19921, + # "symbol": "ETHUSD", + # "timestamp": 1592474241582701416, + # "turnoverEv": 47228362330, + # "volume": 4053863 + # } + # + # linear swap v2 + # + # { + # "closeRp":"16820.5", + # "fundingRateRr":"0.0001", + # "highRp":"16962.1", + # "indexPriceRp":"16830.15651565", + # "lowRp":"16785", + # "markPriceRp":"16830.97534951", + # "openInterestRv":"1323.596", + # "openRp":"16851.7", + # "predFundingRateRr":"0.0001", + # "symbol":"BTCUSDT", + # "timestamp":"1672142789065593096", + # "turnoverRv":"124835296.0538", + # "volumeRq":"7406.95" + # } + # + marketId = self.safe_string(contract, 'symbol') + symbol = self.safe_symbol(marketId, market) + timestamp = self.safe_integer_product(contract, 'timestamp', 0.000001) + markEp = self.from_ep(self.safe_string(contract, 'markEp'), market) + indexEp = self.from_ep(self.safe_string(contract, 'indexEp'), market) + fundingRateEr = self.from_er(self.safe_string(contract, 'fundingRateEr'), market) + nextFundingRateEr = self.from_er(self.safe_string(contract, 'predFundingRateEr'), market) + return { + 'info': contract, + 'symbol': symbol, + 'markPrice': self.safe_number(contract, 'markPriceRp', markEp), + 'indexPrice': self.safe_number(contract, 'indexPriceRp', indexEp), + 'interestRate': None, + 'estimatedSettlePrice': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'fundingRate': self.safe_number(contract, 'fundingRateRr', fundingRateEr), + 'fundingTimestamp': None, + 'fundingDatetime': None, + 'nextFundingRate': self.safe_number(contract, 'predFundingRateRr', nextFundingRateEr), + 'nextFundingTimestamp': None, + 'nextFundingDatetime': None, + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': None, + } + + def set_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + Either adds or reduces margin in an isolated position in order to set the margin to a specific value + + https://github.com/phemex/phemex-api-docs/blob/master/Public-Contract-API-en.md#assign-position-balance-in-isolated-marign-mode + + :param str symbol: unified market symbol of the market to set margin in + :param float amount: the amount to set the margin to + :param dict [params]: parameters specific to the exchange API endpoint + :returns dict: A `margin structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'posBalanceEv': self.to_ev(amount, market), + } + response = self.privatePostPositionsAssign(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "", + # "data": "OK" + # } + # + return self.extend(self.parse_margin_modification(response, market), { + 'amount': amount, + }) + + def parse_margin_status(self, status): + statuses: dict = { + '0': 'ok', + } + return self.safe_string(statuses, status, status) + + def parse_margin_modification(self, data: dict, market: Market = None) -> MarginModification: + # + # { + # "code": 0, + # "msg": "", + # "data": "OK" + # } + # + market = self.safe_market(None, market) + inverse = self.safe_value(market, 'inverse') + codeCurrency = 'base' if inverse else 'quote' + return { + 'info': data, + 'symbol': self.safe_symbol(None, market), + 'type': 'set', + 'marginMode': 'isolated', + 'amount': None, + 'total': None, + 'code': market[codeCurrency], + 'status': self.parse_margin_status(self.safe_string(data, 'code')), + 'timestamp': None, + 'datetime': None, + } + + def set_margin_mode(self, marginMode: str, symbol: Str = None, params={}): + """ + set margin mode to 'cross' or 'isolated' + + https://phemex-docs.github.io/#set-leverage + + :param str marginMode: 'cross' or 'isolated' + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setMarginMode() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + if not market['swap']: + raise BadSymbol(self.id + ' setMarginMode() supports swap contracts only') + marginMode = marginMode.lower() + if marginMode != 'isolated' and marginMode != 'cross': + raise BadRequest(self.id + ' setMarginMode() marginMode argument should be isolated or cross') + request: dict = { + 'symbol': market['id'], + } + isCross = marginMode == 'cross' + if self.in_array(market['settle'], ['USDT', 'USDC']): + currentLeverage = self.safe_string(params, 'leverage') + if currentLeverage is None: + raise ArgumentsRequired(self.id + ' setMarginMode() requires a "leverage" parameter for USDT markets') + request['leverageRr'] = Precise.string_neg(Precise.string_abs(currentLeverage)) if isCross else Precise.string_abs(currentLeverage) + return self.privatePutGPositionsLeverage(self.extend(request, params)) + leverage = self.safe_integer(params, 'leverage') + if marginMode == 'cross': + leverage = 0 + if leverage is None: + raise ArgumentsRequired(self.id + ' setMarginMode() requires a leverage parameter') + request['leverage'] = leverage + return self.privatePutPositionsLeverage(self.extend(request, params)) + + def set_position_mode(self, hedged: bool, symbol: Str = None, params={}): + """ + set hedged to True or False for a market + + https://github.com/phemex/phemex-api-docs/blob/master/Public-Hedged-Perpetual-API.md#switch-position-mode-synchronously + + :param bool hedged: set to True to use dualSidePosition + :param str symbol: not used by binance setPositionMode() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + self.check_required_argument('setPositionMode', symbol, 'symbol') + self.load_markets() + market = self.market(symbol) + if market['settle'] != 'USDT': + raise BadSymbol(self.id + ' setPositionMode() supports USDT settled markets only') + request: dict = { + 'symbol': market['id'], + } + if hedged: + request['targetPosMode'] = 'Hedged' + else: + request['targetPosMode'] = 'OneWay' + return self.privatePutGPositionsSwitchPosModeSync(self.extend(request, params)) + + def fetch_leverage_tiers(self, symbols: Strings = None, params={}) -> LeverageTiers: + """ + retrieve information on the maximum leverage, and maintenance margin for trades of varying trade sizes + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `leverage tiers structures `, indexed by market symbols + """ + self.load_markets() + if symbols is not None: + first = self.safe_value(symbols, 0) + market = self.market(first) + if market['settle'] != 'USD': + raise BadSymbol(self.id + ' fetchLeverageTiers() supports USD settled markets only') + response = self.publicGetCfgV2Products(params) + # + # { + # "code":0, + # "msg":"OK", + # "data":{ + # "ratioScale":8, + # "currencies":[ + # {"currency":"BTC","valueScale":8,"minValueEv":1,"maxValueEv":5000000000000000000,"name":"Bitcoin"}, + # {"currency":"USD","valueScale":4,"minValueEv":1,"maxValueEv":500000000000000,"name":"USD"}, + # {"currency":"USDT","valueScale":8,"minValueEv":1,"maxValueEv":5000000000000000000,"name":"TetherUS"}, + # ], + # "products":[ + # { + # "symbol":"BTCUSD", + # "displaySymbol":"BTC / USD", + # "indexSymbol":".BTC", + # "markSymbol":".MBTC", + # "fundingRateSymbol":".BTCFR", + # "fundingRate8hSymbol":".BTCFR8H", + # "contractUnderlyingAssets":"USD", + # "settleCurrency":"BTC", + # "quoteCurrency":"USD", + # "contractSize":1.0, + # "lotSize":1, + # "tickSize":0.5, + # "priceScale":4, + # "ratioScale":8, + # "pricePrecision":1, + # "minPriceEp":5000, + # "maxPriceEp":10000000000, + # "maxOrderQty":1000000, + # "type":"Perpetual" + # }, + # { + # "symbol":"sBTCUSDT", + # "displaySymbol":"BTC / USDT", + # "quoteCurrency":"USDT", + # "pricePrecision":2, + # "type":"Spot", + # "baseCurrency":"BTC", + # "baseTickSize":"0.000001 BTC", + # "baseTickSizeEv":100, + # "quoteTickSize":"0.01 USDT", + # "quoteTickSizeEv":1000000, + # "minOrderValue":"10 USDT", + # "minOrderValueEv":1000000000, + # "maxBaseOrderSize":"1000 BTC", + # "maxBaseOrderSizeEv":100000000000, + # "maxOrderValue":"5,000,000 USDT", + # "maxOrderValueEv":500000000000000, + # "defaultTakerFee":"0.001", + # "defaultTakerFeeEr":100000, + # "defaultMakerFee":"0.001", + # "defaultMakerFeeEr":100000, + # "baseQtyPrecision":6, + # "quoteQtyPrecision":2 + # }, + # ], + # "riskLimits":[ + # { + # "symbol":"BTCUSD", + # "steps":"50", + # "riskLimits":[ + # {"limit":100,"initialMargin":"1.0%","initialMarginEr":1000000,"maintenanceMargin":"0.5%","maintenanceMarginEr":500000}, + # {"limit":150,"initialMargin":"1.5%","initialMarginEr":1500000,"maintenanceMargin":"1.0%","maintenanceMarginEr":1000000}, + # {"limit":200,"initialMargin":"2.0%","initialMarginEr":2000000,"maintenanceMargin":"1.5%","maintenanceMarginEr":1500000}, + # ] + # }, + # ], + # "leverages":[ + # {"initialMargin":"1.0%","initialMarginEr":1000000,"options":[1,2,3,5,10,25,50,100]}, + # {"initialMargin":"1.5%","initialMarginEr":1500000,"options":[1,2,3,5,10,25,50,66]}, + # {"initialMargin":"2.0%","initialMarginEr":2000000,"options":[1,2,3,5,10,25,33,50]}, + # ] + # } + # } + # + # + data = self.safe_value(response, 'data', {}) + riskLimits = self.safe_list(data, 'riskLimits') + return self.parse_leverage_tiers(riskLimits, symbols, 'symbol') + + def parse_market_leverage_tiers(self, info, market: Market = None) -> List[LeverageTier]: + """ + :param dict info: Exchange market response for 1 market + :param dict market: CCXT market + """ + # + # { + # "symbol":"BTCUSD", + # "steps":"50", + # "riskLimits":[ + # {"limit":100,"initialMargin":"1.0%","initialMarginEr":1000000,"maintenanceMargin":"0.5%","maintenanceMarginEr":500000}, + # {"limit":150,"initialMargin":"1.5%","initialMarginEr":1500000,"maintenanceMargin":"1.0%","maintenanceMarginEr":1000000}, + # {"limit":200,"initialMargin":"2.0%","initialMarginEr":2000000,"maintenanceMargin":"1.5%","maintenanceMarginEr":1500000}, + # ] + # }, + # + marketId = self.safe_string(info, 'symbol') + market = self.safe_market(marketId, market) + riskLimits = (market['info']['riskLimits']) + tiers = [] + minNotional = 0 + for i in range(0, len(riskLimits)): + tier = riskLimits[i] + maxNotional = self.safe_integer(tier, 'limit') + tiers.append({ + 'tier': self.sum(i, 1), + 'symbol': self.safe_symbol(marketId, market), + 'currency': market['settle'], + 'minNotional': minNotional, + 'maxNotional': maxNotional, + 'maintenanceMarginRate': self.safe_string(tier, 'maintenanceMargin'), + 'maxLeverage': None, + 'info': tier, + }) + minNotional = maxNotional + return tiers + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + query = self.omit(params, self.extract_params(path)) + requestPath = '/' + self.implode_params(path, params) + url = requestPath + queryString = '' + if (method == 'GET') or (method == 'DELETE') or (method == 'PUT') or (url == '/positions/assign'): + if query: + queryString = self.urlencode_with_array_repeat(query) + url += '?' + queryString + if api == 'private': + self.check_required_credentials() + timestamp = self.seconds() + xPhemexRequestExpiry = self.safe_integer(self.options, 'x-phemex-request-expiry', 60) + expiry = self.sum(timestamp, xPhemexRequestExpiry) + expiryString = str(expiry) + headers = { + 'x-phemex-access-token': self.apiKey, + 'x-phemex-request-expiry': expiryString, + } + payload = '' + if method == 'POST': + isOrderPlacement = (path == 'g-orders') or (path == 'spot/orders') or (path == 'orders') + if isOrderPlacement: + if self.safe_string(params, 'clOrdID') is None: + id = self.safe_string(self.options, 'brokerId', 'CCXT123456') + params['clOrdID'] = id + self.uuid16() + payload = self.json(params) + body = payload + headers['Content-Type'] = 'application/json' + auth = requestPath + queryString + expiryString + payload + headers['x-phemex-request-signature'] = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256) + url = self.implode_hostname(self.urls['api'][api]) + url + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://github.com/phemex/phemex-api-docs/blob/master/Public-Hedged-Perpetual-API.md#set-leverage + + :param float leverage: the rate of leverage, 100 > leverage > -100 excluding numbers between -1 to 1 + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.hedged]: set to True if hedged position mode is enabled(by default long and short leverage are set to the same value) + :param float [params.longLeverageRr]: *hedged mode only* set the leverage for long positions + :param float [params.shortLeverageRr]: *hedged mode only* set the leverage for short positions + :returns dict: response from the exchange + """ + # WARNING: THIS WILL INCREASE LIQUIDATION PRICE FOR OPEN ISOLATED LONG POSITIONS + # AND DECREASE LIQUIDATION PRICE FOR OPEN ISOLATED SHORT POSITIONS + if symbol is None: + raise ArgumentsRequired(self.id + ' setLeverage() requires a symbol argument') + if (leverage < -100) or (leverage > 100): + raise BadRequest(self.id + ' setLeverage() leverage should be between -100 and 100') + self.load_markets() + isHedged = self.safe_bool(params, 'hedged', False) + longLeverageRr = self.safe_integer(params, 'longLeverageRr') + shortLeverageRr = self.safe_integer(params, 'shortLeverageRr') + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = None + if market['settle'] == 'USDT' or market['settle'] == 'USDC': + if not isHedged and longLeverageRr is None and shortLeverageRr is None: + request['leverageRr'] = leverage + else: + longVar = longLeverageRr if (longLeverageRr is not None) else leverage + shortVar = shortLeverageRr if (shortLeverageRr is not None) else leverage + request['longLeverageRr'] = longVar + request['shortLeverageRr'] = shortVar + response = self.privatePutGPositionsLeverage(self.extend(request, params)) + else: + request['leverage'] = leverage + response = self.privatePutPositionsLeverage(self.extend(request, params)) + return response + + def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://phemex-docs.github.io/#transfer-between-spot-and-futures + https://phemex-docs.github.io/#universal-transfer-main-account-only-transfer-between-sub-to-main-main-to-sub-or-sub-to-sub + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account to transfer from + :param str toAccount: account to transfer to + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.bizType]: for transferring between main and sub-acounts either 'SPOT' or 'PERPETUAL' default is 'SPOT' + :returns dict: a `transfer structure ` + """ + self.load_markets() + currency = self.currency(code) + accountsByType = self.safe_value(self.options, 'accountsByType', {}) + fromId = self.safe_string(accountsByType, fromAccount, fromAccount) + toId = self.safe_string(accountsByType, toAccount, toAccount) + scaledAmmount = self.to_ev(amount, currency) + direction = None + transfer = None + if fromId == 'spot' and toId == 'future': + direction = 2 + elif fromId == 'future' and toId == 'spot': + direction = 1 + if direction is not None: + request: dict = { + 'currency': currency['id'], + 'moveOp': direction, + 'amountEv': scaledAmmount, + } + response = self.privatePostAssetsTransfer(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "OK", + # "data": { + # "linkKey": "8564eba4-c9ec-49d6-9b8c-2ec5001a0fb9", + # "userId": "4018340", + # "currency": "USD", + # "amountEv": "10", + # "side": "2", + # "status": "10" + # } + # } + # + data = self.safe_value(response, 'data', {}) + transfer = self.parse_transfer(data, currency) + else: # sub account transfer + request: dict = { + 'fromUserId': fromId, + 'toUserId': toId, + 'amountEv': scaledAmmount, + 'currency': currency['id'], + 'bizType': self.safe_string(params, 'bizType', 'SPOT'), + } + response = self.privatePostAssetsUniversalTransfer(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "OK", + # "data": "API-923db826-aaaa-aaaa-aaaa-4d98c3a7c9fd" + # } + # + transfer = self.parse_transfer(response) + transferOptions = self.safe_value(self.options, 'transfer', {}) + fillResponseFromRequest = self.safe_bool(transferOptions, 'fillResponseFromRequest', True) + if fillResponseFromRequest: + if transfer['fromAccount'] is None: + transfer['fromAccount'] = fromAccount + if transfer['toAccount'] is None: + transfer['toAccount'] = toAccount + if transfer['amount'] is None: + transfer['amount'] = amount + if transfer['currency'] is None: + transfer['currency'] = code + return transfer + + def fetch_transfers(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[TransferEntry]: + """ + fetch a history of internal transfers made on an account + + https://phemex-docs.github.io/#query-transfer-history + + :param str code: unified currency code of the currency transferred + :param int [since]: the earliest time in ms to fetch transfers for + :param int [limit]: the maximum number of transfers structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transfer structures ` + """ + self.load_markets() + if code is None: + raise ArgumentsRequired(self.id + ' fetchTransfers() requires a code argument') + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + } + if since is not None: + request['start'] = since + if limit is not None: + request['limit'] = limit + response = self.privateGetAssetsTransfer(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "OK", + # "data": { + # "rows": [ + # { + # "linkKey": "87c071a3-8628-4ac2-aca1-6ce0d1fad66c", + # "userId": 4148428, + # "currency": "BTC", + # "amountEv": 67932, + # "side": 2, + # "status": 10, + # "createTime": 1652832467000, + # "bizType": 10 + # } + # ] + # } + # } + # + data = self.safe_value(response, 'data', {}) + transfers = self.safe_list(data, 'rows', []) + return self.parse_transfers(transfers, currency, since, limit) + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + # + # transfer + # + # { + # "linkKey": "8564eba4-c9ec-49d6-9b8c-2ec5001a0fb9", + # "userId": "4018340", + # "currency": "USD", + # "amountEv": "10", + # "side": "2", + # "status": "10" + # } + # + # fetchTransfers + # + # { + # "linkKey": "87c071a3-8628-4ac2-aca1-6ce0d1fad66c", + # "userId": 4148428, + # "currency": "BTC", + # "amountEv": 67932, + # "side": 2, + # "status": 10, + # "createTime": 1652832467000, + # "bizType": 10 + # } + # + id = self.safe_string(transfer, 'linkKey') + status = self.safe_string(transfer, 'status') + amountEv = self.safe_string(transfer, 'amountEv') + amountTransfered = self.from_ev(amountEv) + currencyId = self.safe_string(transfer, 'currency') + code = self.safe_currency_code(currencyId, currency) + side = self.safe_integer(transfer, 'side') + fromId = None + toId = None + if side == 1: + fromId = 'swap' + toId = 'spot' + elif side == 2: + fromId = 'spot' + toId = 'swap' + timestamp = self.safe_integer(transfer, 'createTime') + return { + 'info': transfer, + 'id': id, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'currency': code, + 'amount': amountTransfered, + 'fromAccount': fromId, + 'toAccount': toId, + 'status': self.parse_transfer_status(status), + } + + def parse_transfer_status(self, status: Str) -> Str: + statuses: dict = { + '3': 'rejected', # 'Rejected', + '6': 'canceled', # 'Got error and wait for recovery', + '10': 'ok', # 'Success', + '11': 'failed', # 'Failed', + } + return self.safe_string(statuses, status, status) + + def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical funding rate prices + + https://phemex-docs.github.io/#query-funding-rate-history-2 + + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: timestamp in ms of the earliest funding rate to fetch + :param int [limit]: the maximum amount of `funding rate structures ` to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param int [params.until]: timestamp in ms of the latest funding rate + :returns dict[]: a list of `funding rate structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchFundingRateHistory() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + isUsdtSettled = market['settle'] == 'USDT' or market['settle'] == 'USDC' + if not market['swap']: + raise BadRequest(self.id + ' fetchFundingRateHistory() supports swap contracts only') + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchFundingRateHistory', 'paginate') + if paginate: + return self.fetch_paginated_call_deterministic('fetchFundingRateHistory', symbol, since, limit, '8h', params, 100) + customSymbol = None + if isUsdtSettled: + customSymbol = '.' + market['id'] + 'FR8H' # phemex requires a custom symbol for funding rate history + else: + customSymbol = '.' + market['baseId'] + 'FR8H' + request: dict = { + 'symbol': customSymbol, + } + if since is not None: + request['start'] = since + if limit is not None: + request['limit'] = limit + request, params = self.handle_until_option('end', request, params) + response = None + if isUsdtSettled: + response = self.v2GetApiDataPublicDataFundingRateHistory(self.extend(request, params)) + else: + response = self.v1GetApiDataPublicDataFundingRateHistory(self.extend(request, params)) + # + # { + # "code":"0", + # "msg":"OK", + # "data":{ + # "rows":[ + # { + # "symbol":".BTCUSDTFR8H", + # "fundingRate":"0.0001", + # "fundingTime":"1682064000000", + # "intervalSeconds":"28800" + # } + # ] + # } + # } + # + data = self.safe_value(response, 'data', {}) + rates = self.safe_value(data, 'rows') + result = [] + for i in range(0, len(rates)): + item = rates[i] + timestamp = self.safe_integer(item, 'fundingTime') + result.append({ + 'info': item, + 'symbol': symbol, + 'fundingRate': self.safe_number(item, 'fundingRate'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + sorted = self.sort_by(result, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, symbol, since, limit) + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://phemex-docs.github.io/#create-withdraw-request + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the phemex api endpoint + :param str [params.network]: unified network code + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.load_markets() + self.check_address(address) + currency = self.currency(code) + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + networkId = None + if networkCode is not None: + networkId = self.network_code_to_id(networkCode) + stableCoins = self.safe_value(self.options, 'stableCoins') + if networkId is None: + if not (self.in_array(code, stableCoins)): + networkId = currency['id'] + else: + raise ArgumentsRequired(self.id + ' withdraw() requires an extra argument params["network"]') + request: dict = { + 'currency': currency['id'], + 'address': address, + 'amount': amount, + 'chainName': networkId.upper(), + } + if tag is not None: + request['addressTag'] = tag + response = self.privatePostPhemexWithdrawWalletsApiCreateWithdraw(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "OK", + # "data": { + # "id": "10000001", + # "freezeId": null, + # "address": "44exxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + # "amountRv": "100", + # "chainCode": "11", + # "chainName": "TRX", + # "currency": "USDT", + # "currencyCode": 3, + # "email": "abc@gmail.com", + # "expiredTime": "0", + # "feeRv": "1", + # "nickName": null, + # "phone": null, + # "rejectReason": "", + # "submitedAt": "1670000000000", + # "submittedAt": "1670000000000", + # "txHash": null, + # "userId": "10000001", + # "status": "Success" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_transaction(data, currency) + + def fetch_open_interest(self, symbol: str, params={}): + """ + retrieves the open interest of a trading pair + + https://phemex-docs.github.io/#query-24-hours-ticker + + :param str symbol: unified CCXT market symbol + :param dict [params]: exchange specific parameters + :returns dict} an open interest structure{@link https://docs.ccxt.com/#/?id=open-interest-structure: + """ + self.load_markets() + market = self.market(symbol) + if not market['contract']: + raise BadRequest(self.id + ' fetchOpenInterest is only supported for contract markets.') + request: dict = { + 'symbol': market['id'], + } + response = self.v2GetMdV2Ticker24hr(self.extend(request, params)) + # + # { + # error: null, + # id: '0', + # result: { + # closeRp: '67550.1', + # fundingRateRr: '0.0001', + # highRp: '68400', + # indexPriceRp: '67567.15389794', + # lowRp: '66096.4', + # markPriceRp: '67550.1', + # openInterestRv: '1848.1144186', + # openRp: '66330', + # predFundingRateRr: '0.0001', + # symbol: 'BTCUSDT', + # timestamp: '1729114315443343001', + # turnoverRv: '228863389.3237532', + # volumeRq: '3388.5600312' + # } + # } + # + result = self.safe_dict(response, 'result') + return self.parse_open_interest(result, market) + + def parse_open_interest(self, interest, market: Market = None): + # + # { + # closeRp: '67550.1', + # fundingRateRr: '0.0001', + # highRp: '68400', + # indexPriceRp: '67567.15389794', + # lowRp: '66096.4', + # markPriceRp: '67550.1', + # openInterestRv: '1848.1144186', + # openRp: '66330', + # predFundingRateRr: '0.0001', + # symbol: 'BTCUSDT', + # timestamp: '1729114315443343001', + # turnoverRv: '228863389.3237532', + # volumeRq: '3388.5600312' + # } + # + timestamp = self.safe_integer(interest, 'timestamp') / 1000000 + id = self.safe_string(interest, 'symbol') + return self.safe_open_interest({ + 'info': interest, + 'symbol': self.safe_symbol(id, market), + 'baseVolume': self.safe_string(interest, 'volumeRq'), + 'quoteVolume': None, # deprecated + 'openInterestAmount': self.safe_string(interest, 'openInterestRv'), + 'openInterestValue': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }, market) + + def fetch_convert_quote(self, fromCode: str, toCode: str, amount: Num = None, params={}) -> Conversion: + """ + fetch a quote for converting from one currency to another + + https://phemex-docs.github.io/#rfq-quote + + :param str fromCode: the currency that you want to sell and convert from + :param str toCode: the currency that you want to buy and convert into + :param float amount: how much you want to trade in units of the from currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `conversion structure ` + """ + self.load_markets() + fromCurrency = self.currency(fromCode) + toCurrency = self.currency(toCode) + valueScale = self.safe_integer(fromCurrency, 'valueScale') + request: dict = { + 'fromCurrency': fromCode, + 'toCurrency': toCode, + 'fromAmountEv': self.to_en(amount, valueScale), + } + response = self.privateGetAssetsQuote(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "OK", + # "data": { + # "code": "GIF...AAA", + # "quoteArgs": { + # "origin": 10, + # "price": "0.00000939", + # "proceeds": "0.00000000", + # "ttlMs": 7000, + # "expireAt": 1739875826009, + # "requestAt": 1739875818009, + # "quoteAt": 1739875816594 + # } + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_conversion(data, fromCurrency, toCurrency) + + def create_convert_trade(self, id: str, fromCode: str, toCode: str, amount: Num = None, params={}) -> Conversion: + """ + convert from one currency to another + + https://phemex-docs.github.io/#convert + + :param str id: the id of the trade that you want to make + :param str fromCode: the currency that you want to sell and convert from + :param str toCode: the currency that you want to buy and convert into + :param float [amount]: how much you want to trade in units of the from currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `conversion structure ` + """ + self.load_markets() + fromCurrency = self.currency(fromCode) + toCurrency = self.currency(toCode) + valueScale = self.safe_integer(fromCurrency, 'valueScale') + request: dict = { + 'code': id, + 'fromCurrency': fromCode, + 'toCurrency': toCode, + } + if amount is not None: + request['fromAmountEv'] = self.to_en(amount, valueScale) + response = self.privatePostAssetsConvert(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "OK", + # "data": { + # "moveOp": 0, + # "fromCurrency": "USDT", + # "toCurrency": "BTC", + # "fromAmountEv": 4000000000, + # "toAmountEv": 41511, + # "linkKey": "45c8ed8e-d3f4-472d-8262-e464e8c46247", + # "status": 10 + # } + # } + # + data = self.safe_dict(response, 'data', {}) + fromCurrencyId = self.safe_string(data, 'fromCurrency') + fromResult = self.safe_currency(fromCurrencyId, fromCurrency) + toCurrencyId = self.safe_string(data, 'toCurrency') + to = self.safe_currency(toCurrencyId, toCurrency) + return self.parse_conversion(data, fromResult, to) + + def fetch_convert_trade_history(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Conversion]: + """ + fetch the users history of conversion trades + + https://phemex-docs.github.io/#query-convert-history + + :param str [code]: the unified currency code + :param int [since]: the earliest time in ms to fetch conversions for + :param int [limit]: the maximum number of conversion structures to retrieve, default 20, max 200 + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.until]: the end time in ms + :param str [params.fromCurrency]: the currency that you sold and converted from + :param str [params.toCurrency]: the currency that you bought and converted into + :returns dict[]: a list of `conversion structures ` + """ + self.load_markets() + request: dict = {} + if code is not None: + request['fromCurrency'] = code + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + request, params = self.handle_until_option('endTime', request, params) + response = self.privateGetAssetsConvert(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "OK", + # "data": { + # "total": 2, + # "rows": [ + # { + # "linkKey": "45c8ed8e-d3f4-472d-8262-e464e8c46247", + # "createTime": 1739882294000, + # "fromCurrency": "USDT", + # "toCurrency": "BTC", + # "fromAmountEv": 4000000000, + # "toAmountEv": 41511, + # "status": 10, + # "conversionRate": 1037, + # "errorCode": 0 + # }, + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + rows = self.safe_list(data, 'rows', []) + return self.parse_conversions(rows, code, 'fromCurrency', 'toCurrency', since, limit) + + def parse_conversion(self, conversion: dict, fromCurrency: Currency = None, toCurrency: Currency = None) -> Conversion: + # + # fetchConvertQuote + # + # { + # "code": "GIF...AAA", + # "quoteArgs": { + # "origin": 10, + # "price": "0.00000939", + # "proceeds": "0.00000000", + # "ttlMs": 7000, + # "expireAt": 1739875826009, + # "requestAt": 1739875818009, + # "quoteAt": 1739875816594 + # } + # } + # + # createConvertTrade + # + # { + # "moveOp": 0, + # "fromCurrency": "USDT", + # "toCurrency": "BTC", + # "fromAmountEv": 4000000000, + # "toAmountEv": 41511, + # "linkKey": "45c8ed8e-d3f4-472d-8262-e464e8c46247", + # "status": 10 + # } + # + # fetchConvertTradeHistory + # + # { + # "linkKey": "45c8ed8e-d3f4-472d-8262-e464e8c46247", + # "createTime": 1739882294000, + # "fromCurrency": "USDT", + # "toCurrency": "BTC", + # "fromAmountEv": 4000000000, + # "toAmountEv": 41511, + # "status": 10, + # "conversionRate": 1037, + # "errorCode": 0 + # } + # + quoteArgs = self.safe_dict(conversion, 'quoteArgs', {}) + requestTime = self.safe_integer(quoteArgs, 'requestAt') + timestamp = self.safe_integer(conversion, 'createTime', requestTime) + fromCoin = self.safe_string(conversion, 'fromCurrency', self.safe_string(fromCurrency, 'code')) + fromCode = self.safe_currency_code(fromCoin, fromCurrency) + toCoin = self.safe_string(conversion, 'toCurrency', self.safe_string(toCurrency, 'code')) + toCode = self.safe_currency_code(toCoin, toCurrency) + fromValueScale = self.safe_integer(fromCurrency, 'valueScale') + toValueScale = self.safe_integer(toCurrency, 'valueScale') + fromAmount = self.from_en(self.safe_string(conversion, 'fromAmountEv'), fromValueScale) + if fromAmount is None and quoteArgs is not None: + fromAmount = self.from_en(self.safe_string(quoteArgs, 'origin'), fromValueScale) + toAmount = self.from_en(self.safe_string(conversion, 'toAmountEv'), toValueScale) + if toAmount is None and quoteArgs is not None: + toAmount = self.from_en(self.safe_string(quoteArgs, 'proceeds'), toValueScale) + return { + 'info': conversion, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': self.safe_string(conversion, 'code'), + 'fromCurrency': fromCode, + 'fromAmount': self.parse_number(fromAmount), + 'toCurrency': toCode, + 'toAmount': self.parse_number(toAmount), + 'price': self.safe_number(quoteArgs, 'price'), + 'fee': None, + } + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None # fallback to default error handler + # + # {"code":30018,"msg":"phemex.data.size.uplimt","data":null} + # {"code":412,"msg":"Missing parameter - resolution","data":null} + # {"code":412,"msg":"Missing parameter - to","data":null} + # {"error":{"code":6001,"message":"invalid argument"},"id":null,"result":null} + # + error = self.safe_value(response, 'error', response) + errorCode = self.safe_string(error, 'code') + message = self.safe_string(error, 'msg') + if (errorCode is not None) and (errorCode != '0'): + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + raise ExchangeError(feedback) # unknown message + return None diff --git a/ccxt/poloniex.py b/ccxt/poloniex.py new file mode 100644 index 0000000..ed41d06 --- /dev/null +++ b/ccxt/poloniex.py @@ -0,0 +1,3552 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.poloniex import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Bool, Currencies, Currency, DepositAddress, Int, Leverage, MarginModification, Market, Num, Order, OrderBook, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, Trade, TradingFees, Transaction, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import AccountSuspended +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import NotSupported +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.errors import OnMaintenance +from ccxt.base.errors import RequestTimeout +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class poloniex(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(poloniex, self).describe(), { + 'id': 'poloniex', + 'name': 'Poloniex', + 'countries': ['US'], + # 200 requests per second for some unauthenticated market endpoints => 1000ms / 200 = 5ms between requests + 'rateLimit': 5, + 'certified': False, + 'pro': True, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': None, # has but not fully implemented + 'swap': True, + 'future': True, + 'option': False, + 'addMargin': True, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'cancelOrders': None, # not yet implemented, because RL is worse than cancelOrder + 'createDepositAddress': True, + 'createMarketBuyOrderWithCost': True, + 'createMarketOrderWithCost': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createOrders': None, # not yet implemented, because RL is worse than createOrder + 'createStopOrder': True, + 'createTriggerOrder': True, + 'editOrder': True, + 'fetchBalance': True, + 'fetchClosedOrder': False, + 'fetchClosedOrders': True, + 'fetchCurrencies': True, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchDepositsWithdrawals': True, + 'fetchDepositWithdrawFee': 'emulated', + 'fetchDepositWithdrawFees': True, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': None, # has but not implemented + 'fetchLedger': None, # has but not implemented + 'fetchLeverage': True, + 'fetchLiquidations': None, # has but not implemented + 'fetchMarginMode': False, + 'fetchMarkets': True, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterestHistory': False, + 'fetchOpenOrder': False, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrderBooks': False, + 'fetchOrderTrades': True, + 'fetchPosition': False, + 'fetchPositionMode': True, + 'fetchPositions': True, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': True, + 'fetchTransactions': 'emulated', + 'fetchTransfer': False, + 'fetchTransfers': False, + 'fetchWithdrawals': True, + 'reduceMargin': True, + 'sandbox': True, + 'setLeverage': True, + 'setPositionMode': True, + 'transfer': True, + 'withdraw': True, + }, + 'timeframes': { + '1m': 'MINUTE_1', + '5m': 'MINUTE_5', + '10m': 'MINUTE_10', # not in swap + '15m': 'MINUTE_15', + '30m': 'MINUTE_30', + '1h': 'HOUR_1', + '2h': 'HOUR_2', + '4h': 'HOUR_4', + '6h': 'HOUR_6', # not in swap + '12h': 'HOUR_12', + '1d': 'DAY_1', + '3d': 'DAY_3', + '1w': 'WEEK_1', + '1M': 'MONTH_1', # not in swap + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/27766817-e9456312-5ee6-11e7-9b3c-b628ca5626a5.jpg', + 'api': { + 'spot': 'https://api.poloniex.com', + 'swap': 'https://api.poloniex.com', + }, + 'test': { + 'spot': 'https://sand-spot-api-gateway.poloniex.com', + }, + 'www': 'https://www.poloniex.com', + 'doc': 'https://api-docs.poloniex.com/spot/', + 'fees': 'https://poloniex.com/fees', + 'referral': 'https://poloniex.com/signup?c=UBFZJRPJ', + }, + 'api': { + 'public': { + 'get': { + 'markets': 20, + 'markets/{symbol}': 1, + 'currencies': 20, + 'currencies/{currency}': 20, + 'v2/currencies': 20, + 'v2/currencies/{currency}': 20, + 'timestamp': 1, + 'markets/price': 1, + 'markets/{symbol}/price': 1, + 'markets/markPrice': 1, + 'markets/{symbol}/markPrice': 1, + 'markets/{symbol}/markPriceComponents': 1, + 'markets/{symbol}/orderBook': 1, + 'markets/{symbol}/candles': 1, + 'markets/{symbol}/trades': 20, + 'markets/ticker24h': 20, + 'markets/{symbol}/ticker24h': 20, + 'markets/collateralInfo': 1, + 'markets/{currency}/collateralInfo': 1, + 'markets/borrowRatesInfo': 1, + }, + }, + 'private': { + 'get': { + 'accounts': 4, + 'accounts/balances': 4, + 'accounts/{id}/balances': 4, + 'accounts/activity': 20, + 'accounts/transfer': 20, + 'accounts/transfer/{id}': 4, + 'feeinfo': 20, + 'accounts/interest/history': 1, + 'subaccounts': 4, + 'subaccounts/balances': 20, + 'subaccounts/{id}/balances': 4, + 'subaccounts/transfer': 20, + 'subaccounts/transfer/{id}': 4, + 'wallets/addresses': 20, + 'wallets/addresses/{currency}': 20, + 'wallets/activity': 20, + 'margin/accountMargin': 4, + 'margin/borrowStatus': 4, + 'margin/maxSize': 4, + 'orders': 20, + 'orders/{id}': 4, + 'orders/killSwitchStatus': 4, + 'smartorders': 20, + 'smartorders/{id}': 4, + 'orders/history': 20, + 'smartorders/history': 20, + 'trades': 20, + 'orders/{id}/trades': 4, + }, + 'post': { + 'accounts/transfer': 4, + 'subaccounts/transfer': 20, + 'wallets/address': 20, + 'wallets/withdraw': 20, + 'v2/wallets/withdraw': 20, + 'orders': 4, + 'orders/batch': 20, + 'orders/killSwitch': 4, + 'smartorders': 4, + }, + 'delete': { + 'orders/{id}': 4, + 'orders/cancelByIds': 20, + 'orders': 20, + 'smartorders/{id}': 4, + 'smartorders/cancelByIds': 20, + 'smartorders': 20, + }, + 'put': { + 'orders/{id}': 20, + 'smartorders/{id}': 20, + }, + }, + 'swapPublic': { + 'get': { + # 300 calls / second + 'v3/market/allInstruments': 2 / 3, + 'v3/market/instruments': 2 / 3, + 'v3/market/orderBook': 2 / 3, + 'v3/market/candles': 10, # candles have differnt RL + 'v3/market/indexPriceCandlesticks': 10, + 'v3/market/premiumIndexCandlesticks': 10, + 'v3/market/markPriceCandlesticks': 10, + 'v3/market/trades': 2 / 3, + 'v3/market/liquidationOrder': 2 / 3, + 'v3/market/tickers': 2 / 3, + 'v3/market/markPrice': 2 / 3, + 'v3/market/indexPrice': 2 / 3, + 'v3/market/indexPriceComponents': 2 / 3, + 'v3/market/fundingRate': 2 / 3, + 'v3/market/openInterest': 2 / 3, + 'v3/market/insurance': 2 / 3, + 'v3/market/riskLimit': 2 / 3, + }, + }, + 'swapPrivate': { + 'get': { + 'v3/account/balance': 4, + 'v3/account/bills': 20, + 'v3/trade/order/opens': 20, + 'v3/trade/order/trades': 20, + 'v3/trade/order/history': 20, + 'v3/trade/position/opens': 20, + 'v3/trade/position/history': 20, # todo: method for self + 'v3/position/leverages': 20, + 'v3/position/mode': 20, + }, + 'post': { + 'v3/trade/order': 4, + 'v3/trade/orders': 40, + 'v3/trade/position': 20, + 'v3/trade/positionAll': 100, + 'v3/position/leverage': 20, + 'v3/position/mode': 20, + 'v3/trade/position/margin': 20, + }, + 'delete': { + 'v3/trade/order': 2, + 'v3/trade/batchOrders': 20, + 'v3/trade/allOrders': 20, + }, + }, + }, + 'fees': { + 'trading': { + 'feeSide': 'get', + # starting from Jan 8 2020 + 'maker': self.parse_number('0.0009'), + 'taker': self.parse_number('0.0009'), + }, + 'funding': {}, + }, + 'commonCurrencies': { + 'AIR': 'AirCoin', + 'APH': 'AphroditeCoin', + 'BCC': 'BTCtalkcoin', + 'BCHABC': 'BCHABC', + 'BDG': 'Badgercoin', + 'BTM': 'Bitmark', + 'CON': 'Coino', + 'ETHTRON': 'ETH', + 'GOLD': 'GoldEagles', + 'GPUC': 'GPU', + 'HOT': 'Hotcoin', + 'ITC': 'Information Coin', + 'KEY': 'KEYCoin', + 'MASK': 'NFTX Hashmasks Index', # conflict with Mask Network + 'MEME': 'Degenerator Meme', # Degenerator Meme migrated to Meme Inu, self exchange still has the old price + 'PLX': 'ParallaxCoin', + 'REPV2': 'REP', + 'STR': 'XLM', + 'SOC': 'SOCC', + 'TRADE': 'Unitrade', + 'TRXETH': 'TRX', + 'XAP': 'API Coin', + # self is not documented in the API docs for Poloniex + # https://github.com/ccxt/ccxt/issues/7084 + # when the user calls withdraw('USDT', amount, address, tag, params) + # with params = {'currencyToWithdrawAs': 'USDTTRON'} + # or params = {'currencyToWithdrawAs': 'USDTETH'} + # fetchWithdrawals('USDT') returns the corresponding withdrawals + # with a USDTTRON or a USDTETH currency id, respectfully + # therefore we have map them back to the original code USDT + # otherwise the returned withdrawals are filtered out + 'USDTBSC': 'USDT', + 'USDTTRON': 'USDT', + 'USDTETH': 'USDT', + 'UST': 'USTC', + }, + 'options': { + 'defaultType': 'spot', + 'createMarketBuyOrderRequiresPrice': True, + 'networks': { + 'BEP20': 'BSC', + 'ERC20': 'ETH', + 'TRC20': 'TRON', + 'TRX': 'TRON', + }, + 'networksById': { + 'TRX': 'TRC20', + 'TRON': 'TRC20', + }, + 'limits': { + 'cost': { + 'min': { + 'BTC': 0.0001, + 'ETH': 0.0001, + 'USDT': 1.0, + 'TRX': 100, + 'BNB': 0.06, + 'USDC': 1.0, + 'USDJ': 1.0, + 'TUSD': 0.0001, + 'DAI': 1.0, + 'PAX': 1.0, + 'BUSD': 1.0, + }, + }, + }, + 'accountsByType': { + 'spot': 'spot', + 'future': 'futures', + }, + 'accountsById': { + 'exchange': 'spot', + 'futures': 'future', + }, + }, + 'features': { + 'default': { + 'sandbox': True, + 'createOrder': { + 'marginMode': True, # todo + 'triggerPrice': True, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'leverage': False, + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': True, # todo, only for non-trigger orders + 'trailing': False, + 'iceberg': False, + }, + 'createOrders': { + 'max': 20, + }, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 100000, + 'untilDays': 100000, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 2000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': None, # todo implement + 'fetchOHLCV': { + 'limit': 500, + }, + }, + 'spot': { + 'extends': 'default', + }, + 'forContracts': { + 'extends': 'default', + 'createOrder': { + 'marginMode': True, + 'triggerPrice': False, + 'hedged': True, + 'stpMode': True, # todo + 'marketBuyByCost': False, + }, + 'createOrders': { + 'max': 10, + }, + 'fetchOpenOrders': { + 'limit': 100, + }, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': None, + 'daysBackCanceled': 1 / 6, + 'untilDays': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchMyTrades': { + 'limit': 100, + 'untilDays': 90, + }, + }, + 'swap': { + 'linear': { + 'extends': 'forContracts', + }, + 'inverse': { + 'extends': 'forContracts', + }, + }, + 'future': { + 'linear': { + 'extends': 'forContracts', + }, + 'inverse': { + 'extends': 'forContracts', + }, + }, + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'exact': { + # General + '500': ExchangeNotAvailable, # Internal System Error + '603': RequestTimeout, # Internal Request Timeout + '601': BadRequest, # Invalid Parameter + '415': ExchangeError, # System Error + '602': ArgumentsRequired, # Missing Required Parameters + # Accounts + '21604': BadRequest, # Invalid UserId + '21600': AuthenticationError, # Account Not Found + '21605': AuthenticationError, # Invalid Account Type + '21102': ExchangeError, # Invalid Currency + '21100': AuthenticationError, # Invalid account + '21704': AuthenticationError, # Missing UserId and/or AccountId + '21700': BadRequest, # Error updating accounts + '21705': BadRequest, # Invalid currency type + '21707': ExchangeError, # Internal accounts Error + '21708': BadRequest, # Currency not available to User + '21601': AccountSuspended, # Account locked. Contact support + '21711': ExchangeError, # Currency locked. Contact support + '21709': InsufficientFunds, # Insufficient balance + '250000': ExchangeError, # Transfer error. Try again later + '250001': BadRequest, # Invalid toAccount for transfer + '250002': BadRequest, # Invalid fromAccount for transfer + '250003': BadRequest, # Invalid transfer amount + '250004': BadRequest, # Transfer is not supported + '250005': InsufficientFunds, # Insufficient transfer balance + '250008': BadRequest, # Invalid transfer currency + '250012': ExchangeError, # Futures account is not valid + # Trading + '21110': BadRequest, # Invalid quote currency + '10040': BadSymbol, # Invalid symbol + '10060': ExchangeError, # Symbol setup error + '10020': BadSymbol, # Invalid currency + '10041': BadSymbol, # Symbol frozen for trading + '21340': OnMaintenance, # No order creation/cancelation is allowed is in Maintenane Mode + '21341': InvalidOrder, # Post-only orders type allowed is in Post Only Mode + '21342': InvalidOrder, # Price is higher than highest bid is in Maintenance Mode + '21343': InvalidOrder, # Price is lower than lowest bid is in Maintenance Mode + '21351': AccountSuspended, # Trading for self account is frozen. Contact support + '21352': BadSymbol, # Trading for self currency is frozen + '21353': PermissionDenied, # Trading for US customers is not supported + '21354': PermissionDenied, # Account needs to be verified via email before trading is enabled. Contact support + '21359': OrderNotFound, # {"code" : 21359, "message" : "Order was already canceled or filled."} + '21360': InvalidOrder, # {"code" : 21360, "message" : "Order size exceeds the limit.Please enter a smaller amount and try again."} + '24106': BadRequest, # Invalid market depth + '24201': ExchangeNotAvailable, # Service busy. Try again later + # Orders + '21301': OrderNotFound, # Order not found + '21302': ExchangeError, # Batch cancel order error + '21304': ExchangeError, # Order is filled + '21305': OrderNotFound, # Order is canceled + '21307': ExchangeError, # Error during Order Cancelation + '21309': InvalidOrder, # Order price must be greater than 0 + '21310': InvalidOrder, # Order price must be less than max price + '21311': InvalidOrder, # Order price must be greater than min price + '21312': InvalidOrder, # Client orderId already exists + '21314': InvalidOrder, # Max limit of open orders(2000) exceeded + '21315': InvalidOrder, # Client orderId exceeded max length of 17 digits + '21317': InvalidOrder, # Amount must be greater than 0 + '21319': InvalidOrder, # Invalid order side + '21320': InvalidOrder, # Invalid order type + '21321': InvalidOrder, # Invalid timeInForce value + '21322': InvalidOrder, # Amount is less than minAmount trade limit + '21324': BadRequest, # Invalid account type + '21327': InvalidOrder, # Order pice must be greater than 0 + '21328': InvalidOrder, # Order quantity must be greater than 0 + '21330': InvalidOrder, # Quantity is less than minQuantity trade limit + '21335': InvalidOrder, # Invalid priceScale for self symbol + '21336': InvalidOrder, # Invalid quantityScale for self symbol + '21337': InvalidOrder, # Invalid amountScale for self symbol + '21344': InvalidOrder, # Value of limit param is greater than max value of 100 + '21345': InvalidOrder, # Value of limit param value must be greater than 0 + '21346': InvalidOrder, # Order Id must be of type Long + '21348': InvalidOrder, # Order type must be LIMIT_MAKER + '21347': InvalidOrder, # Stop price must be greater than 0 + '21349': InvalidOrder, # Order value is too large + '21350': InvalidOrder, # Amount must be greater than 1 USDT + '21355': ExchangeError, # Interval between startTime and endTime in trade/order history has exceeded 7 day limit + '21356': BadRequest, # Order size would cause too much price movement. Reduce order size. + '21721': InsufficientFunds, + '24101': BadSymbol, # Invalid symbol + '24102': InvalidOrder, # Invalid K-line type + '24103': InvalidOrder, # Invalid endTime + '24104': InvalidOrder, # Invalid amount + '24105': InvalidOrder, # Invalid startTime + '25020': InvalidOrder, # No active kill switch + # Smartorders + '25000': InvalidOrder, # Invalid userId + '25001': InvalidOrder, # Invalid parameter + '25002': InvalidOrder, # Invalid userId. + '25003': ExchangeError, # Unable to place order + '25004': InvalidOrder, # Client orderId already exists + '25005': ExchangeError, # Unable to place smart order + '25006': InvalidOrder, # OrderId and clientOrderId already exists + '25007': InvalidOrder, # Invalid orderid + '25008': InvalidOrder, # Both orderId and clientOrderId are required + '25009': ExchangeError, # Failed to cancel order + '25010': PermissionDenied, # Unauthorized to cancel order + '25011': InvalidOrder, # Failed to cancel due to invalid paramters + '25012': ExchangeError, # Failed to cancel + '25013': OrderNotFound, # Failed to cancel were not found + '25014': OrderNotFound, # Failed to cancel were not found + '25015': OrderNotFound, # Failed to cancel orders exist + '25016': ExchangeError, # Failed to cancel to release funds + '25017': ExchangeError, # No orders were canceled + '25018': BadRequest, # Invalid accountType + '25019': BadSymbol, # Invalid symbol + }, + 'broad': { + }, + }, + }) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # spot: + # + # [ + # [ + # "22814.01", + # "22937.42", + # "22832.57", + # "22937.42", + # "3916.58764051", + # "0.171199", + # "2982.64647063", + # "0.130295", + # 33, + # 0, + # "22877.449915304470460711", + # "MINUTE_5", + # 1659664800000, + # 1659665099999 + # ] + # ] + # + # contract: + # + # [ + # "84207.02", + # "84320.85", + # "84207.02", + # "84253.83", + # "3707.5395", + # "44", + # "14", + # "1740770040000", + # "1740770099999", + # ], + # + ohlcvLength = len(ohlcv) + isContract = ohlcvLength == 9 + if isContract: + return [ + self.safe_integer(ohlcv, 7), + self.safe_number(ohlcv, 2), + self.safe_number(ohlcv, 1), + self.safe_number(ohlcv, 0), + self.safe_number(ohlcv, 3), + self.safe_number(ohlcv, 5), + ] + return [ + self.safe_integer(ohlcv, 12), + self.safe_number(ohlcv, 2), + self.safe_number(ohlcv, 1), + self.safe_number(ohlcv, 0), + self.safe_number(ohlcv, 3), + self.safe_number(ohlcv, 5), + ] + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://api-docs.poloniex.com/spot/api/public/market-data#candles + https://api-docs.poloniex.com/v3/futures/api/market/get-kline-data + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate', False) + if paginate: + return self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params, 500) + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'interval': self.safe_string(self.timeframes, timeframe, timeframe), + } + keyStart = 'startTime' if market['spot'] else 'sTime' + keyEnd = 'endTime' if market['spot'] else 'eTime' + if since is not None: + request[keyStart] = since + if limit is not None: + # limit should in between 100 and 500 + request['limit'] = limit + request, params = self.handle_until_option(keyEnd, request, params) + if market['contract']: + if self.in_array(timeframe, ['10m', '1M']): + raise NotSupported(self.id + ' ' + timeframe + ' ' + market['type'] + ' fetchOHLCV is not supported') + responseRaw = self.swapPublicGetV3MarketCandles(self.extend(request, params)) + # + # { + # code: "200", + # msg: "Success", + # data: [ + # [ + # "84207.02", + # "84320.85", + # "84207.02", + # "84253.83", + # "3707.5395", + # "44", + # "14", + # "1740770040000", + # "1740770099999", + # ], + # + data = self.safe_list(responseRaw, 'data') + return self.parse_ohlcvs(data, market, timeframe, since, limit) + response = self.publicGetMarketsSymbolCandles(self.extend(request, params)) + # + # [ + # [ + # "22814.01", + # "22937.42", + # "22832.57", + # "22937.42", + # "3916.58764051", + # "0.171199", + # "2982.64647063", + # "0.130295", + # 33, + # 0, + # "22877.449915304470460711", + # "MINUTE_5", + # 1659664800000, + # 1659665099999 + # ] + # ] + # + return self.parse_ohlcvs(response, market, timeframe, since, limit) + + def load_markets(self, reload=False, params={}): + markets = super(poloniex, self).load_markets(reload, params) + currenciesByNumericId = self.safe_value(self.options, 'currenciesByNumericId') + if (currenciesByNumericId is None) or reload: + self.options['currenciesByNumericId'] = self.index_by(self.currencies, 'numericId') + return markets + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for poloniex + + https://api-docs.poloniex.com/spot/api/public/reference-data#symbol-information + https://api-docs.poloniex.com/v3/futures/api/market/get-all-product-info + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + promises = [self.fetch_spot_markets(params), self.fetch_swap_markets(params)] + results = promises + return self.array_concat(results[0], results[1]) + + def fetch_spot_markets(self, params={}) -> List[Market]: + markets = self.publicGetMarkets(params) + # + # [ + # { + # "symbol" : "BTS_BTC", + # "baseCurrencyName" : "BTS", + # "quoteCurrencyName" : "BTC", + # "displayName" : "BTS/BTC", + # "state" : "NORMAL", + # "visibleStartTime" : 1659018816626, + # "tradableStartTime" : 1659018816626, + # "symbolTradeLimit" : { + # "symbol" : "BTS_BTC", + # "priceScale" : 10, + # "quantityScale" : 0, + # "amountScale" : 8, + # "minQuantity" : "100", + # "minAmount" : "0.00001", + # "highestBid" : "0", + # "lowestAsk" : "0" + # } + # } + # ] + # + return self.parse_markets(markets) + + def fetch_swap_markets(self, params={}) -> List[Market]: + # do similar per https://api-docs.poloniex.com/v3/futures/api/market/get-product-info + response = self.swapPublicGetV3MarketAllInstruments(params) + # + # { + # "code": "200", + # "msg": "Success", + # "data": [ + # { + # "symbol": "BNB_USDT_PERP", + # "bAsset": ".PBNBUSDT", + # "bCcy": "BNB", + # "qCcy": "USDT", + # "visibleStartTime": "1620390600000", + # "tradableStartTime": "1620390600000", + # "sCcy": "USDT", + # "tSz": "0.001", + # "pxScale": "0.001,0.01,0.1,1,10", + # "lotSz": "1", + # "minSz": "1", + # "ctVal": "0.1", + # "status": "OPEN", + # "oDate": "1620287590000", + # "maxPx": "1000000", + # "minPx": "0.001", + # "maxQty": "1000000", + # "minQty": "1", + # "maxLever": "50", + # "lever": "10", + # "ctType": "LINEAR", + # "alias": "", + # "iM": "0.02", + # "mM": "0.0115", + # "mR": "2000", + # "buyLmt": "", + # "sellLmt": "", + # "ordPxRange": "0.05", + # "marketMaxQty": "2800", + # "limitMaxQty": "1000000" + # }, + # + markets = self.safe_list(response, 'data') + return self.parse_markets(markets) + + def parse_market(self, market: dict) -> Market: + if 'ctType' in market: + return self.parse_swap_market(market) + else: + return self.parse_spot_market(market) + + def parse_spot_market(self, market: dict) -> Market: + id = self.safe_string(market, 'symbol') + baseId = self.safe_string(market, 'baseCurrencyName') + quoteId = self.safe_string(market, 'quoteCurrencyName') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + state = self.safe_string(market, 'state') + active = state == 'NORMAL' + symbolTradeLimit = self.safe_value(market, 'symbolTradeLimit') + # these are known defaults + return { + 'id': id, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': active, + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(self.parse_precision(self.safe_string(symbolTradeLimit, 'quantityScale'))), + 'price': self.parse_number(self.parse_precision(self.safe_string(symbolTradeLimit, 'priceScale'))), + }, + 'limits': { + 'amount': { + 'min': self.safe_number(symbolTradeLimit, 'minQuantity'), + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': self.safe_number(symbolTradeLimit, 'minAmount'), + 'max': None, + }, + }, + 'created': self.safe_integer(market, 'tradableStartTime'), + 'info': market, + } + + def parse_swap_market(self, market: dict) -> Market: + # + # { + # "symbol": "BNB_USDT_PERP", + # "bAsset": ".PBNBUSDT", + # "bCcy": "BNB", + # "qCcy": "USDT", + # "visibleStartTime": "1620390600000", + # "tradableStartTime": "1620390600000", + # "sCcy": "USDT", + # "tSz": "0.001", + # "pxScale": "0.001,0.01,0.1,1,10", + # "lotSz": "1", + # "minSz": "1", + # "ctVal": "0.1", + # "status": "OPEN", + # "oDate": "1620287590000", + # "maxPx": "1000000", + # "minPx": "0.001", + # "maxQty": "1000000", + # "minQty": "1", + # "maxLever": "50", + # "lever": "10", + # "ctType": "LINEAR", + # "alias": "", + # "iM": "0.02", + # "mM": "0.0115", + # "mR": "2000", + # "buyLmt": "", + # "sellLmt": "", + # "ordPxRange": "0.05", + # "marketMaxQty": "2800", + # "limitMaxQty": "1000000" + # }, + # + id = self.safe_string(market, 'symbol') + baseId = self.safe_string(market, 'bCcy') + quoteId = self.safe_string(market, 'qCcy') + settleId = self.safe_string(market, 'sCcy') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + settle = self.safe_currency_code(settleId) + status = self.safe_string(market, 'status') + active = status == 'OPEN' + linear = market['ctType'] == 'LINEAR' + symbol = base + '/' + quote + if linear: + symbol += ':' + settle + else: + # actually, exchange does not have any inverse future now + symbol += ':' + base + alias = self.safe_string(market, 'alias') + type = 'swap' + if alias is not None: + type = 'future' + return { + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': 'future' if (type == 'future') else 'swap', + 'spot': False, + 'margin': False, + 'swap': type == 'swap', + 'future': type == 'future', + 'option': False, + 'active': active, + 'contract': True, + 'linear': linear, + 'inverse': not linear, + 'contractSize': self.safe_number(market, 'ctVal'), + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'taker': self.safe_number(market, 'tFee'), + 'maker': self.safe_number(market, 'mFee'), + 'precision': { + 'amount': self.safe_number(market, 'lotSz'), + 'price': self.safe_number(market, 'tSz'), + }, + 'limits': { + 'amount': { + 'min': self.safe_number(market, 'minSz'), + 'max': self.safe_number(market, 'limitMaxQty'), + }, + 'price': { + 'min': self.safe_number(market, 'minPx'), + 'max': self.safe_number(market, 'maxPx'), + }, + 'cost': { + 'min': None, + 'max': None, + }, + 'leverage': { + 'max': self.safe_number(market, 'maxLever'), + 'min': None, + }, + }, + 'created': self.safe_integer(market, 'oDate'), + 'info': market, + } + + def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://api-docs.poloniex.com/spot/api/public/reference-data#system-timestamp + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = self.publicGetTimestamp(params) + return self.safe_integer(response, 'serverTime') + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # spot: + # + # { + # "symbol" : "BTC_USDT", + # "open" : "26053.33", + # "low" : "26053.33", + # "high" : "26798.02", + # "close" : "26447.58", + # "quantity" : "6116.210188", + # "amount" : "161082122.88450926", + # "tradeCount" : "134709", + # "startTime" : "1692784440000", + # "closeTime" : "1692870839630", + # "displayName" : "BTC/USDT", + # "dailyChange" : "0.0151", + # "bid" : "26447.57", + # "bidQuantity" : "0.016313", + # "ask" : "26447.58", + # "askQuantity" : "0.068307", + # "ts" : "1692870845446", + # "markPrice" : "26444.11" + # } + # + # swap: + # + # { + # "s": "XRP_USDT_PERP", + # "o": "2.0503", + # "l": "2.0066", + # "h": "2.216", + # "c": "2.1798", + # "qty": "21090", + # "amt": "451339.65", + # "tC": "3267", + # "sT": "1740736380000", + # "cT": "1740822777559", + # "dN": "XRP/USDT/PERP", + # "dC": "0.0632", + # "bPx": "2.175", + # "bSz": "3", + # "aPx": "2.1831", + # "aSz": "111", + # "mPx": "2.1798", + # "iPx": "2.1834" + # }, + # + timestamp = self.safe_integer_2(ticker, 'ts', 'cT') + marketId = self.safe_string_2(ticker, 'symbol', 's') + market = self.safe_market(marketId) + relativeChange = self.safe_string_2(ticker, 'dailyChange', 'dc') + percentage = Precise.string_mul(relativeChange, '100') + return self.safe_ticker({ + 'id': marketId, + 'symbol': market['symbol'], + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string_2(ticker, 'high', 'h'), + 'low': self.safe_string_2(ticker, 'low', 'l'), + 'bid': self.safe_string_2(ticker, 'bid', 'bPx'), + 'bidVolume': self.safe_string_2(ticker, 'bidQuantity', 'bSz'), + 'ask': self.safe_string_2(ticker, 'ask', 'aPx'), + 'askVolume': self.safe_string_2(ticker, 'askQuantity', 'aSz'), + 'vwap': None, + 'open': self.safe_string_2(ticker, 'open', 'o'), + 'close': self.safe_string_2(ticker, 'close', 'c'), + 'previousClose': None, + 'change': None, + 'percentage': percentage, + 'average': None, + 'baseVolume': self.safe_string_2(ticker, 'quantity', 'qty'), + 'quoteVolume': self.safe_string_2(ticker, 'amount', 'amt'), + 'markPrice': self.safe_string_2(ticker, 'markPrice', 'mPx'), + 'indexPrice': self.safe_string(ticker, 'iPx'), + 'info': ticker, + }, market) + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://api-docs.poloniex.com/spot/api/public/market-data#ticker + https://api-docs.poloniex.com/v3/futures/api/market/get-market-info + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + market = None + request: dict = {} + if symbols is not None: + symbols = self.market_symbols(symbols, None, True, True, False) + symbolsLength = len(symbols) + if symbolsLength > 0: + market = self.market(symbols[0]) + if symbolsLength == 1: + request['symbol'] = market['id'] + marketType = None + marketType, params = self.handle_market_type_and_params('fetchTickers', market, params) + if marketType == 'swap': + responseRaw = self.swapPublicGetV3MarketTickers(self.extend(request, params)) + # + # { + # "code": "200", + # "msg": "Success", + # "data": [ + # { + # "s": "XRP_USDT_PERP", + # "o": "2.0503", + # "l": "2.0066", + # "h": "2.216", + # "c": "2.1798", + # "qty": "21090", + # "amt": "451339.65", + # "tC": "3267", + # "sT": "1740736380000", + # "cT": "1740822777559", + # "dN": "XRP/USDT/PERP", + # "dC": "0.0632", + # "bPx": "2.175", + # "bSz": "3", + # "aPx": "2.1831", + # "aSz": "111", + # "mPx": "2.1798", + # "iPx": "2.1834" + # }, + # + data = self.safe_list(responseRaw, 'data') + return self.parse_tickers(data, symbols) + response = self.publicGetMarketsTicker24h(params) + # + # [ + # { + # "symbol" : "BTC_USDT", + # "open" : "26053.33", + # "low" : "26053.33", + # "high" : "26798.02", + # "close" : "26447.58", + # "quantity" : "6116.210188", + # "amount" : "161082122.88450926", + # "tradeCount" : "134709", + # "startTime" : "1692784440000", + # "closeTime" : "1692870839630", + # "displayName" : "BTC/USDT", + # "dailyChange" : "0.0151", + # "bid" : "26447.57", + # "bidQuantity" : "0.016313", + # "ask" : "26447.58", + # "askQuantity" : "0.068307", + # "ts" : "1692870845446", + # "markPrice" : "26444.11" + # } + # ] + # + return self.parse_tickers(response, symbols) + + def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://api-docs.poloniex.com/spot/api/public/reference-data#currency-information + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = self.publicGetCurrencies(self.extend(params, {'includeMultiChainCurrencies': True})) + # + # [ + # { + # "USDT": { + # "id": 214, + # "name": "Tether USD", + # "description": "Sweep to Main Account", + # "type": "address", + # "withdrawalFee": "0.00000000", + # "minConf": 2, + # "depositAddress": null, + # "blockchain": "OMNI", + # "delisted": False, + # "tradingState": "NORMAL", + # "walletState": "DISABLED", + # "walletDepositState": "DISABLED", + # "walletWithdrawalState": "DISABLED", + # "supportCollateral": True, + # "supportBorrow": True, + # "parentChain": null, + # "isMultiChain": True, + # "isChildChain": False, + # "childChains": [ + # "USDTBSC", + # "USDTETH", + # "USDTSOL", + # "USDTTRON" + # ] + # } + # }, + # ... + # { + # "USDTBSC": { + # "id": 582, + # "name": "Binance-Peg BSC-USD", + # "description": "Sweep to Main Account", + # "type": "address", + # "withdrawalFee": "0.00000000", + # "minConf": 15, + # "depositAddress": null, + # "blockchain": "BSC", + # "delisted": False, + # "tradingState": "OFFLINE", + # "walletState": "ENABLED", + # "walletDepositState": "ENABLED", + # "walletWithdrawalState": "DISABLED", + # "supportCollateral": False, + # "supportBorrow": False, + # "parentChain": "USDT", + # "isMultiChain": True, + # "isChildChain": True, + # "childChains": [] + # } + # }, + # ... + # ] + # + result: dict = {} + # poloniex has a complicated structure of currencies, so we handle them differently + # at first, turn the response into a normal dictionary + currenciesDict = {} + for i in range(0, len(response)): + item = self.safe_dict(response, i) + ids = list(item.keys()) + id = self.safe_string(ids, 0) + currenciesDict[id] = item[id] + keys = list(currenciesDict.keys()) + for i in range(0, len(keys)): + id = keys[i] + entry = currenciesDict[id] + code = self.safe_currency_code(id) + # skip childChains, are collected in parentChain loop + if self.safe_bool(entry, 'isChildChain'): + continue + allChainEntries = [] + childChains = self.safe_list(entry, 'childChains', []) + if childChains is not None: + for j in range(0, len(childChains)): + childChainId = childChains[j] + childNetworkEntry = self.safe_dict(currenciesDict, childChainId) + allChainEntries.append(childNetworkEntry) + allChainEntries.append(entry) + networks: dict = {} + for j in range(0, len(allChainEntries)): + chainEntry = allChainEntries[j] + networkName = self.safe_string(chainEntry, 'blockchain') + networkCode = self.network_id_to_code(networkName, code) + specialNetworkId = self.safe_string(childChains, j, id) # in case it's primary chain, defeault to ID + networks[networkCode] = { + 'info': chainEntry, + 'id': specialNetworkId, # we need self for deposit/withdrawal, instead of friendly name + 'numericId': self.safe_integer(chainEntry, 'id'), + 'network': networkCode, + 'active': self.safe_bool(chainEntry, 'walletState'), + 'deposit': self.safe_string(chainEntry, 'walletDepositState') == 'ENABLED', + 'withdraw': self.safe_string(chainEntry, 'walletWithdrawalState') == 'ENABLED', + 'fee': self.safe_number(chainEntry, 'withdrawalFee'), + 'precision': None, + 'limits': { + 'withdraw': { + 'min': None, + 'max': None, + }, + 'deposit': { + 'min': None, + 'max': None, + }, + }, + } + result[code] = self.safe_currency_structure({ + 'info': entry, + 'code': code, + 'id': id, + 'numericId': self.safe_integer(entry, 'id'), + 'type': 'crypto', + 'name': self.safe_string(entry, 'name'), + 'active': None, + 'deposit': None, + 'withdraw': None, + 'fee': None, + 'precision': None, + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + 'deposit': { + 'min': None, + 'max': None, + }, + }, + 'networks': networks, + }) + return result + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://api-docs.poloniex.com/spot/api/public/market-data#ticker + https://api-docs.poloniex.com/v3/futures/api/market/get-market-info + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if market['contract']: + tickers = self.fetch_tickers([market['symbol']], params) + return self.safe_dict(tickers, symbol) + response = self.publicGetMarketsSymbolTicker24h(self.extend(request, params)) + # + # { + # "symbol" : "BTC_USDT", + # "open" : "26053.33", + # "low" : "26053.33", + # "high" : "26798.02", + # "close" : "26447.58", + # "quantity" : "6116.210188", + # "amount" : "161082122.88450926", + # "tradeCount" : "134709", + # "startTime" : "1692784440000", + # "closeTime" : "1692870839630", + # "displayName" : "BTC/USDT", + # "dailyChange" : "0.0151", + # "bid" : "26447.57", + # "bidQuantity" : "0.016313", + # "ask" : "26447.58", + # "askQuantity" : "0.068307", + # "ts" : "1692870845446", + # "markPrice" : "26444.11" + # } + # + return self.parse_ticker(response, market) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades + # + # spot: + # + # { + # "id" : "60014521", + # "price" : "23162.94", + # "quantity" : "0.00009", + # "amount" : "2.0846646", + # "takerSide" : "SELL", + # "ts" : 1659684602042, + # "createTime" : 1659684602036 + # } + # + # swap: + # + # { + # "id": "105807376", + # "side": "buy", + # "px": "84410.57", + # "qty": "1", + # "amt": "84.41057", + # "cT": "1740777563557", + # } + # + # fetchMyTrades + # + # spot: + # + # { + # "id": "32164924331503616", + # "symbol": "LINK_USDT", + # "accountType": "SPOT", + # "orderId": "32164923987566592", + # "side": "SELL", + # "type": "MARKET", + # "matchRole": "TAKER", + # "createTime": 1648635115525, + # "price": "11", + # "quantity": "0.5", + # "amount": "5.5", + # "feeCurrency": "USDT", + # "feeAmount": "0.007975", + # "pageId": "32164924331503616", + # "clientOrderId": "myOwnId-321" + # } + # + # swap: + # + # { + # "symbol": "BTC_USDT_PERP", + # "trdId": "105813553", + # "side": "SELL", + # "type": "TRADE", + # "mgnMode": "CROSS", + # "ordType": "MARKET", + # "clOrdId": "polo418912106147315112", + # "role": "TAKER", + # "px": "84704.9", + # "qty": "1", + # "cTime": "1740842829430", + # "uTime": "1740842829450", + # "feeCcy": "USDT", + # "feeAmt": "0.04235245", + # "deductCcy": "", + # "deductAmt": "0", + # "feeRate": "0.0005", + # "id": "418912106342654592", + # "posSide": "BOTH", + # "ordId": "418912106147315112", + # "qCcy": "USDT", + # "value": "84.7049", + # "actType": "TRADING" + # }, + # + # fetchOrderTrades(taker trades) + # + # { + # "id": "30341456333942784", + # "symbol": "LINK_USDT", + # "accountType": "SPOT", + # "orderId": "30249408733945856", + # "side": "BUY", + # "type": "LIMIT", + # "matchRole": "MAKER", + # "createTime": 1648200366864, + # "price": "3.1", + # "quantity": "1", + # "amount": "3.1", + # "feeCurrency": "LINK", + # "feeAmount": "0.00145", + # "pageId": "30341456333942784", + # "clientOrderId": "" + # } + # + id = self.safe_string_n(trade, ['id', 'tradeID', 'trdId']) + orderId = self.safe_string_2(trade, 'orderId', 'ordId') + timestamp = self.safe_integer_n(trade, ['ts', 'createTime', 'cT', 'cTime']) + marketId = self.safe_string(trade, 'symbol') + market = self.safe_market(marketId, market, '_') + symbol = market['symbol'] + side = self.safe_string_lower_2(trade, 'side', 'takerSide') + fee = None + priceString = self.safe_string_2(trade, 'price', 'px') + amountString = self.safe_string_2(trade, 'quantity', 'qty') + costString = self.safe_string_2(trade, 'amount', 'amt') + feeCurrencyId = self.safe_string_2(trade, 'feeCurrency', 'feeCcy') + feeCostString = self.safe_string_2(trade, 'feeAmount', 'feeAmt') + if feeCostString is not None: + feeCurrencyCode = self.safe_currency_code(feeCurrencyId) + fee = { + 'cost': feeCostString, + 'currency': feeCurrencyCode, + } + return self.safe_trade({ + 'id': id, + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'order': orderId, + 'type': self.safe_string_lower_2(trade, 'ordType', 'type'), # ordType should take precedence + 'side': side, + 'takerOrMaker': self.safe_string_lower_2(trade, 'matchRole', 'role'), + 'price': priceString, + 'amount': amountString, + 'cost': costString, + 'fee': fee, + }, market) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://api-docs.poloniex.com/spot/api/public/market-data#trades + https://api-docs.poloniex.com/v3/futures/api/market/get-execution-info + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['limit'] = limit # max 1000, for spot & swap + if market['contract']: + response = self.swapPublicGetV3MarketTrades(self.extend(request, params)) + # + # { + # code: "200", + # msg: "Success", + # data: [ + # { + # id: "105807320", # descending order + # side: "sell", + # px: "84383.93", + # qty: "1", + # amt: "84.38393", + # cT: "1740777074704", + # }, + # + tradesList = self.safe_list(response, 'data') + return self.parse_trades(tradesList, market, since, limit) + trades = self.publicGetMarketsSymbolTrades(self.extend(request, params)) + # + # [ + # { + # "id" : "60014521", + # "price" : "23162.94", + # "quantity" : "0.00009", + # "amount" : "2.0846646", + # "takerSide" : "SELL", + # "ts" : 1659684602042, + # "createTime" : 1659684602036 + # } + # ] + # + return self.parse_trades(trades, market, since, limit) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://api-docs.poloniex.com/spot/api/private/trade#trade-history + https://api-docs.poloniex.com/v3/futures/api/trade/get-execution-details + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch entries for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchMyTrades', 'paginate') + if paginate: + return self.fetch_paginated_call_dynamic('fetchMyTrades', symbol, since, limit, params) + market: Market = None + if symbol is not None: + market = self.market(symbol) + marketType = None + marketType, params = self.handle_market_type_and_params('fetchMyTrades', market, params) + isContract = self.in_array(marketType, ['swap', 'future']) + request: dict = { + # 'from': 12345678, # A 'trade Id'. The query begins at ‘from'. + # 'direction': 'PRE', # PRE, NEXT The direction before or after ‘from'. + } + startKey = 'sTime' if isContract else 'startTime' + endKey = 'eTime' if isContract else 'endTime' + if since is not None: + request[startKey] = since + if limit is not None: + request['limit'] = limit + if isContract and symbol is not None: + request['symbol'] = market['id'] + request, params = self.handle_until_option(endKey, request, params) + if isContract: + raw = self.swapPrivateGetV3TradeOrderTrades(self.extend(request, params)) + # + # { + # "code": "200", + # "msg": "", + # "data": [ + # { + # "symbol": "BTC_USDT_PERP", + # "trdId": "105813553", + # "side": "SELL", + # "type": "TRADE", + # "mgnMode": "CROSS", + # "ordType": "MARKET", + # "clOrdId": "polo418912106147315112", + # "role": "TAKER", + # "px": "84704.9", + # "qty": "1", + # "cTime": "1740842829430", + # "uTime": "1740842829450", + # "feeCcy": "USDT", + # "feeAmt": "0.04235245", + # "deductCcy": "", + # "deductAmt": "0", + # "feeRate": "0.0005", + # "id": "418912106342654592", + # "posSide": "BOTH", + # "ordId": "418912106147315112", + # "qCcy": "USDT", + # "value": "84.7049", + # "actType": "TRADING" + # }, + # + data = self.safe_list(raw, 'data') + return self.parse_trades(data, market, since, limit) + response = self.privateGetTrades(self.extend(request, params)) + # + # [ + # { + # "id": "32164924331503616", + # "symbol": "LINK_USDT", + # "accountType": "SPOT", + # "orderId": "32164923987566592", + # "side": "SELL", + # "type": "MARKET", + # "matchRole": "TAKER", + # "createTime": 1648635115525, + # "price": "11", + # "quantity": "0.5", + # "amount": "5.5", + # "feeCurrency": "USDT", + # "feeAmount": "0.007975", + # "pageId": "32164924331503616", + # "clientOrderId": "myOwnId-321" + # } + # ] + # + result = self.parse_trades(response, market, since, limit) + return result + + def parse_order_status(self, status: Str): + statuses: dict = { + 'NEW': 'open', + 'PARTIALLY_FILLED': 'open', + 'FILLED': 'closed', + 'PENDING_CANCEL': 'canceled', + 'PARTIALLY_CANCELED': 'canceled', + 'CANCELED': 'canceled', + 'FAILED': 'canceled', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # fetchOpenOrder + # + # { + # "id" : "7xxxxxxxxxxxxxxx6", + # "clientOrderId" : "", + # "symbol" : "ETH_USDT", + # "state" : "NEW", + # "accountType" : "SPOT", + # "side" : "BUY", + # "type" : "LIMIT", + # "timeInForce" : "GTC", + # "quantity" : "0.001", + # "price" : "1600", + # "avgPrice" : "0", + # "amount" : "0", + # "filledQuantity" : "0", + # "filledAmount" : "0", + # "createTime" : 16xxxxxxxxx26, + # "updateTime" : 16xxxxxxxxx36 + # } + # + # fetchOpenOrders(and fetchClosedOrders same for contracts) + # + # spot: + # + # { + # "id": "24993088082542592", + # "clientOrderId": "", + # "symbol": "ELON_USDC", + # "state": "NEW", + # "accountType": "SPOT", + # "side": "SELL", + # "type": "MARKET", + # "timeInForce": "GTC", + # "quantity": "1.00", + # "price": "0.00", + # "avgPrice": "0.00", + # "amount": "0.00", + # "filledQuantity": "0.00", + # "filledAmount": "0.00", + # "createTime": 1646925216548, + # "updateTime": 1646925216548 + # } + # + # contract: + # + # { + # "symbol": "BTC_USDT_PERP", + # "side": "BUY", + # "type": "LIMIT", + # "ordId": "418890767248232148", + # "clOrdId": "polo418890767248232148", + # "mgnMode": "CROSS", + # "px": "81130.13", + # "reduceOnly": False, + # "lever": "20", + # "state": "NEW", + # "source": "WEB", + # "timeInForce": "GTC", + # "tpTrgPx": "", + # "tpPx": "", + # "tpTrgPxType": "", + # "slTrgPx": "", + # "slPx": "", + # "slTrgPxType": "", + # "avgPx": "0", + # "execQty": "0", + # "execAmt": "0", + # "feeCcy": "", + # "feeAmt": "0", + # "deductCcy": "0", + # "deductAmt": "0", + # "stpMode": "NONE", # todo: selfTradePrevention + # "cTime": "1740837741523", + # "uTime": "1740840846882", + # "sz": "1", + # "posSide": "BOTH", + # "qCcy": "USDT" + # "cancelReason": "", # self field can only be in closed orders + # }, + # + # createOrder, editOrder + # + # spot: + # + # { + # "id": "29772698821328896", + # "clientOrderId": "1234Abc" + # } + # + # contract: + # + # { + # "ordId":"418876147745775616", + # "clOrdId":"polo418876147745775616" + # } + # + timestamp = self.safe_integer_n(order, ['timestamp', 'createTime', 'cTime']) + if timestamp is None: + timestamp = self.parse8601(self.safe_string(order, 'date')) + marketId = self.safe_string(order, 'symbol') + market = self.safe_market(marketId, market, '_') + symbol = market['symbol'] + resultingTrades = self.safe_value(order, 'resultingTrades') + if resultingTrades is not None: + if not isinstance(resultingTrades, list): + resultingTrades = self.safe_value(resultingTrades, self.safe_string(market, 'id', marketId)) + price = self.safe_string_n(order, ['price', 'rate', 'px']) + amount = self.safe_string_2(order, 'quantity', 'sz') + filled = self.safe_string_2(order, 'filledQuantity', 'execQty') + status = self.parse_order_status(self.safe_string(order, 'state')) + side = self.safe_string_lower(order, 'side') + rawType = self.safe_string(order, 'type') + type = self.parse_order_type(rawType) + id = self.safe_string_n(order, ['orderNumber', 'id', 'orderId', 'ordId']) + fee = None + feeCurrency = self.safe_string_2(order, 'tokenFeeCurrency', 'feeCcy') + feeCost: Str = None + feeCurrencyCode: Str = None + rate = self.safe_string(order, 'fee') + if feeCurrency is None: + feeCurrencyCode = market['base'] if (side == 'buy') else market['quote'] + else: + # poloniex accepts a 30% discount to pay fees in TRX + feeCurrencyCode = self.safe_currency_code(feeCurrency) + feeCost = self.safe_string_2(order, 'tokenFee', 'feeAmt') + if feeCost is not None: + fee = { + 'rate': rate, + 'cost': feeCost, + 'currency': feeCurrencyCode, + } + clientOrderId = self.safe_string_2(order, 'clientOrderId', 'clOrdId') + marginMode = self.safe_string_lower(order, 'mgnMode') + reduceOnly = self.safe_bool(order, 'reduceOnly') + leverage = self.safe_integer(order, 'lever') + hedged = self.safe_string(order, 'posSide') != 'BOTH' + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': self.safe_integer(order, 'updateTime'), + 'status': status, + 'symbol': symbol, + 'type': type, + 'timeInForce': self.safe_string(order, 'timeInForce'), + 'postOnly': rawType == 'LIMIT_MAKER', + 'side': side, + 'price': price, + 'triggerPrice': self.safe_string_2(order, 'triggerPrice', 'stopPrice'), + 'cost': self.safe_string(order, 'execAmt'), + 'average': self.safe_string_2(order, 'avgPrice', 'avgPx'), + 'amount': amount, + 'filled': filled, + 'remaining': None, + 'trades': resultingTrades, + 'fee': fee, + 'marginMode': marginMode, + 'reduceOnly': reduceOnly, + 'leverage': leverage, + 'hedged': hedged, + }, market) + + def parse_order_type(self, status): + statuses: dict = { + 'MARKET': 'market', + 'LIMIT': 'limit', + 'LIMIT_MAKER': 'limit', + 'STOP-LIMIT': 'limit', + 'STOP-MARKET': 'market', + } + return self.safe_string(statuses, status, status) + + def parse_open_orders(self, orders, market, result): + for i in range(0, len(orders)): + order = orders[i] + extended = self.extend(order, { + 'status': 'open', + 'type': 'limit', + 'side': order['type'], + 'price': order['rate'], + }) + result.append(self.parse_order(extended, market)) + return result + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://api-docs.poloniex.com/spot/api/private/order#open-orders + https://api-docs.poloniex.com/spot/api/private/smart-order#open-orders # trigger orders + https://api-docs.poloniex.com/v3/futures/api/trade/get-current-orders + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: set True to fetch trigger orders instead of regular orders + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + market: Market = None + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + marketType = None + marketType, params = self.handle_market_type_and_params('fetchOpenOrders', market, params) + if limit is not None: + max = 2000 if (marketType == 'spot') else 100 + request['limit'] = max(limit, max) + isTrigger = self.safe_value_2(params, 'trigger', 'stop') + params = self.omit(params, ['trigger', 'stop']) + response = None + if marketType != 'spot': + raw = self.swapPrivateGetV3TradeOrderOpens(self.extend(request, params)) + # + # { + # "code": "200", + # "msg": "", + # "data": [ + # { + # "symbol": "BTC_USDT_PERP", + # "side": "BUY", + # "type": "LIMIT", + # "ordId": "418890767248232148", + # "clOrdId": "polo418890767248232148", + # "mgnMode": "CROSS", + # "px": "81130.13", + # "reduceOnly": False, + # "lever": "20", + # "state": "NEW", + # "source": "WEB", + # "timeInForce": "GTC", + # "tpTrgPx": "", + # "tpPx": "", + # "tpTrgPxType": "", + # "slTrgPx": "", + # "slPx": "", + # "slTrgPxType": "", + # "avgPx": "0", + # "execQty": "0", + # "execAmt": "0", + # "feeCcy": "", + # "feeAmt": "0", + # "deductCcy": "0", + # "deductAmt": "0", + # "stpMode": "NONE", + # "cTime": "1740837741523", + # "uTime": "1740840846882", + # "sz": "1", + # "posSide": "BOTH", + # "qCcy": "USDT" + # }, + # + response = self.safe_list(raw, 'data') + elif isTrigger: + response = self.privateGetSmartorders(self.extend(request, params)) + else: + response = self.privateGetOrders(self.extend(request, params)) + # + # [ + # { + # "id" : "7xxxxxxxxxxxxxxx6", + # "clientOrderId" : "", + # "symbol" : "ETH_USDT", + # "state" : "NEW", + # "accountType" : "SPOT", + # "side" : "BUY", + # "type" : "LIMIT", + # "timeInForce" : "GTC", + # "quantity" : "0.001", + # "price" : "1600", + # "avgPrice" : "0", + # "amount" : "0", + # "filledQuantity" : "0", + # "filledAmount" : "0", + # "stopPrice": "3750.00", # for trigger orders + # "createTime" : 16xxxxxxxxx26, + # "updateTime" : 16xxxxxxxxx36 + # } + # ] + # + extension: dict = {'status': 'open'} + return self.parse_orders(response, market, since, limit, extension) + + def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + + https://api-docs.poloniex.com/v3/futures/api/trade/get-order-history + + fetches information on multiple closed orders made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest entry + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + market = None + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + marketType = None + marketType, params = self.handle_market_type_and_params('fetchClosedOrders', market, params, 'swap') + if marketType == 'spot': + raise NotSupported(self.id + ' fetchClosedOrders() is not supported for spot markets yet') + if limit is not None: + request['limit'] = min(200, limit) + if since is not None: + request['sTime'] = since + request, params = self.handle_until_option('eTime', request, params) + response = self.swapPrivateGetV3TradeOrderHistory(self.extend(request, params)) + # + # { + # "code": "200", + # "msg": "", + # "data": [ + # { + # "symbol": "BTC_USDT_PERP", + # "side": "SELL", + # "type": "MARKET", + # "ordId": "418912106147315712", + # "clOrdId": "polo418912106147315712", + # "mgnMode": "CROSS", + # "px": "0", + # "sz": "2", + # "lever": "20", + # "state": "FILLED", + # "cancelReason": "", + # "source": "WEB", + # "reduceOnly": "true", + # "timeInForce": "GTC", + # "tpTrgPx": "", + # "tpPx": "", + # "tpTrgPxType": "", + # "slTrgPx": "", + # "slPx": "", + # "slTrgPxType": "", + # "avgPx": "84705.56", + # "execQty": "2", + # "execAmt": "169.41112", + # "feeCcy": "USDT", + # "feeAmt": "0.08470556", + # "deductCcy": "0", + # "deductAmt": "0", + # "stpMode": "NONE", + # "cTime": "1740842829116", + # "uTime": "1740842829130", + # "posSide": "BOTH", + # "qCcy": "USDT" + # }, + # + data = self.safe_list(response, 'data', []) + return self.parse_orders(data, market, since, limit) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://api-docs.poloniex.com/spot/api/private/order#create-order + https://api-docs.poloniex.com/spot/api/private/smart-order#create-order # trigger orders + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: the price at which a trigger order is triggered at + :param float [params.cost]: *spot market buy only* the quote quantity that can be used alternative for the amount + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'side': side.upper(), # uppercase, both for spot & swap + # 'timeInForce': timeInForce, # matches unified values + # 'accountType': 'SPOT', + # 'amount': amount, + } + triggerPrice = self.safe_number_2(params, 'stopPrice', 'triggerPrice') + request, params = self.order_request(symbol, type, side, amount, request, price, params) + response = None + if market['swap'] or market['future']: + responseInitial = self.swapPrivatePostV3TradeOrder(self.extend(request, params)) + # + # {"code":200,"msg":"Success","data":{"ordId":"418876147745775616","clOrdId":"polo418876147745775616"}} + # + response = self.safe_dict(responseInitial, 'data') + elif triggerPrice is not None: + response = self.privatePostSmartorders(self.extend(request, params)) + else: + response = self.privatePostOrders(self.extend(request, params)) + # + # { + # "id" : "78923648051920896", + # "clientOrderId" : "" + # } + # + return self.parse_order(response, market) + + def order_request(self, symbol, type, side, amount, request, price=None, params={}): + triggerPrice = self.safe_number_2(params, 'stopPrice', 'triggerPrice') + market = self.market(symbol) + if market['contract']: + marginMode = None + marginMode, params = self.handle_param_string(params, 'marginMode') + if marginMode is not None: + self.check_required_argument('createOrder', marginMode, 'marginMode', ['cross', 'isolated']) + request['mgnMode'] = marginMode.upper() + hedged = None + hedged, params = self.handle_param_string(params, 'hedged') + if hedged: + if marginMode is None: + raise ArgumentsRequired(self.id + ' createOrder() requires a marginMode parameter "cross" or "isolated" for hedged orders') + if not ('posSide' in params): + raise ArgumentsRequired(self.id + ' createOrder() requires a posSide parameter "LONG" or "SHORT" for hedged orders') + upperCaseType = type.upper() + isMarket = upperCaseType == 'MARKET' + isPostOnly = self.is_post_only(isMarket, upperCaseType == 'LIMIT_MAKER', params) + params = self.omit(params, ['postOnly', 'triggerPrice', 'stopPrice']) + if triggerPrice is not None: + if not market['spot']: + raise InvalidOrder(self.id + ' createOrder() does not support trigger orders for ' + market['type'] + ' markets') + upperCaseType = 'STOP' if (price is None) else 'STOP_LIMIT' + request['stopPrice'] = triggerPrice + elif isPostOnly: + upperCaseType = 'LIMIT_MAKER' + request['type'] = upperCaseType + if isMarket: + if side == 'buy': + quoteAmount = None + createMarketBuyOrderRequiresPrice = True + createMarketBuyOrderRequiresPrice, params = self.handle_option_and_params(params, 'createOrder', 'createMarketBuyOrderRequiresPrice', True) + cost = self.safe_number(params, 'cost') + params = self.omit(params, 'cost') + if cost is not None: + quoteAmount = self.cost_to_precision(symbol, cost) + elif createMarketBuyOrderRequiresPrice and market['spot']: + if price is None: + raise InvalidOrder(self.id + ' createOrder() requires the price argument for market buy orders to calculate the total cost to spend(amount * price), alternatively set the createMarketBuyOrderRequiresPrice option or param to False and pass the cost to spend(quote quantity) in the amount argument') + else: + amountString = self.number_to_string(amount) + priceString = self.number_to_string(price) + costRequest = Precise.string_mul(amountString, priceString) + quoteAmount = self.cost_to_precision(symbol, costRequest) + else: + quoteAmount = self.cost_to_precision(symbol, amount) + amountKey = 'amount' if market['spot'] else 'sz' + request[amountKey] = quoteAmount + else: + amountKey = 'quantity' if market['spot'] else 'sz' + request[amountKey] = self.amount_to_precision(symbol, amount) + else: + amountKey = 'quantity' if market['spot'] else 'sz' + request[amountKey] = self.amount_to_precision(symbol, amount) + priceKey = 'price' if market['spot'] else 'px' + request[priceKey] = self.price_to_precision(symbol, price) + clientOrderId = self.safe_string(params, 'clientOrderId') + if clientOrderId is not None: + request['clientOrderId'] = clientOrderId + params = self.omit(params, 'clientOrderId') + # remember the timestamp before issuing the request + return [request, params] + + def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + """ + edit a trade order + + https://api-docs.poloniex.com/spot/api/private/order#cancel-replace-order + https://api-docs.poloniex.com/spot/api/private/smart-order#cancel-replace-order + + :param str id: order id + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float [amount]: how much of the currency you want to trade in units of the base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: The price at which a trigger order is triggered at + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' editOrder() does not support ' + market['type'] + ' orders, only spot orders are accepted') + request: dict = { + 'id': id, + # 'timeInForce': timeInForce, + } + triggerPrice = self.safe_number_2(params, 'stopPrice', 'triggerPrice') + request, params = self.order_request(symbol, type, side, amount, request, price, params) + response = None + if triggerPrice is not None: + response = self.privatePutSmartordersId(self.extend(request, params)) + else: + response = self.privatePutOrdersId(self.extend(request, params)) + # + # { + # "id" : "78923648051920896", + # "clientOrderId" : "" + # } + # + response = self.extend(response, { + 'side': side, + 'type': type, + }) + return self.parse_order(response, market) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + # + # @method + # @name poloniex#cancelOrder + # @description cancels an open order + # @see https://api-docs.poloniex.com/spot/api/private/order#cancel-order-by-id + # @see https://api-docs.poloniex.com/spot/api/private/smart-order#cancel-order-by-id # trigger orders + # @param {string} id order id + # @param {string} symbol unified symbol of the market the order was made in + # @param {object} [params] extra parameters specific to the exchange API endpoint + # @param {boolean} [params.trigger] True if canceling a trigger order + # @returns {object} An `order structure ` + # + self.load_markets() + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + market = self.market(symbol) + request: dict = {} + if not market['spot']: + request['symbol'] = market['id'] + request['ordId'] = id + raw = self.swapPrivateDeleteV3TradeOrder(self.extend(request, params)) + # + # { + # "code": "200", + # "msg": "Success", + # "data": { + # "ordId": "418886099910612040", + # "clOrdId": "polo418886099910612040" + # } + # } + # + return self.parse_order(self.safe_dict(raw, 'data')) + clientOrderId = self.safe_value(params, 'clientOrderId') + if clientOrderId is not None: + id = clientOrderId + request['id'] = id + isTrigger = self.safe_value_2(params, 'trigger', 'stop') + params = self.omit(params, ['clientOrderId', 'trigger', 'stop']) + response = None + if isTrigger: + response = self.privateDeleteSmartordersId(self.extend(request, params)) + else: + response = self.privateDeleteOrdersId(self.extend(request, params)) + # + # { + # "orderId":"210832697138888704", + # "clientOrderId":"", + # "state":"PENDING_CANCEL", + # "code":200, + # "message":"" + # } + # + return self.parse_order(response) + + def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders + + https://api-docs.poloniex.com/spot/api/private/order#cancel-all-orders + https://api-docs.poloniex.com/spot/api/private/smart-order#cancel-all-orders # trigger orders + https://api-docs.poloniex.com/v3/futures/api/trade/cancel-all-orders - contract markets + + :param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: True if canceling trigger orders + :returns dict[]: a list of `order structures ` + """ + self.load_markets() + request: dict = { + # 'accountTypes': 'SPOT', + 'symbols': [], + } + market: Market = None + if symbol is not None: + market = self.market(symbol) + request['symbols'] = [ + market['id'], + ] + response = None + marketType = None + marketType, params = self.handle_market_type_and_params('cancelAllOrders', market, params) + if marketType == 'swap' or marketType == 'future': + raw = self.swapPrivateDeleteV3TradeAllOrders(self.extend(request, params)) + # + # { + # "code": "200", + # "msg": "Success", + # "data": [ + # { + # "code": "200", + # "msg": "Success", + # "ordId": "418885787866388511", + # "clOrdId": "polo418885787866388511" + # } + # ] + # } + # + response = self.safe_list(raw, 'data') + return self.parse_orders(response, market) + isTrigger = self.safe_value_2(params, 'trigger', 'stop') + params = self.omit(params, ['trigger', 'stop']) + if isTrigger: + response = self.privateDeleteSmartorders(self.extend(request, params)) + else: + response = self.privateDeleteOrders(self.extend(request, params)) + # + # [ + # { + # "orderId" : "78xxxxxxxx80", + # "clientOrderId" : "", + # "state" : "NEW", + # "code" : 200, + # "message" : "" + # }, { + # "orderId" : "78xxxxxxxxx80", + # "clientOrderId" : "", + # "state" : "NEW", + # "code" : 200, + # "message" : "" + # } + # ] + # + return self.parse_orders(response, market) + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetch an order by it's id + + https://api-docs.poloniex.com/spot/api/private/order#order-details + https://api-docs.poloniex.com/spot/api/private/smart-order#open-orders # trigger orders + + :param str id: order id + :param str symbol: unified market symbol, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: True if fetching a trigger order + :returns dict: an `order structure ` + """ + self.load_markets() + id = str(id) + request: dict = { + 'id': id, + } + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + marketType = None + marketType, params = self.handle_market_type_and_params('fetchOrder', market, params) + if marketType != 'spot': + raise NotSupported(self.id + ' fetchOrder() is not supported for ' + marketType + ' markets yet') + isTrigger = self.safe_value_2(params, 'trigger', 'stop') + params = self.omit(params, ['trigger', 'stop']) + response = None + if isTrigger: + response = self.privateGetSmartordersId(self.extend(request, params)) + response = self.safe_value(response, 0) + else: + response = self.privateGetOrdersId(self.extend(request, params)) + # + # { + # "id": "21934611974062080", + # "clientOrderId": "123", + # "symbol": "TRX_USDC", + # "state": "NEW", + # "accountType": "SPOT", + # "side": "SELL", + # "type": "LIMIT", + # "timeInForce": "GTC", + # "quantity": "1.00", + # "price": "10.00", + # "avgPrice": "0.00", + # "amount": "0.00", + # "filledQuantity": "0.00", + # "filledAmount": "0.00", + # "stopPrice": "3750.00", # for trigger orders + # "createTime": 1646196019020, + # "updateTime": 1646196019020 + # } + # + order = self.parse_order(response) + order['id'] = id + return order + + def fetch_order_status(self, id: str, symbol: Str = None, params={}): + self.load_markets() + orders = self.fetch_open_orders(symbol, None, None, params) + indexed = self.index_by(orders, 'id') + return 'open' if (id in indexed) else 'closed' + + def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all the trades made from a single order + + https://api-docs.poloniex.com/spot/api/private/trade#trades-by-order-id + + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + self.load_markets() + request: dict = { + 'id': id, + } + trades = self.privateGetOrdersIdTrades(self.extend(request, params)) + # + # [ + # { + # "id": "30341456333942784", + # "symbol": "LINK_USDT", + # "accountType": "SPOT", + # "orderId": "30249408733945856", + # "side": "BUY", + # "type": "LIMIT", + # "matchRole": "MAKER", + # "createTime": 1648200366864, + # "price": "3.1", + # "quantity": "1", + # "amount": "3.1", + # "feeCurrency": "LINK", + # "feeAmount": "0.00145", + # "pageId": "30341456333942784", + # "clientOrderId": "" + # } + # ] + # + return self.parse_trades(trades) + + def parse_balance(self, response) -> Balances: + result: dict = { + 'info': response, + 'timestamp': None, + 'datetime': None, + } + # for swap + if not isinstance(response, list): + ts = self.safe_integer(response, 'uTime') + result['timestamp'] = ts + result['datetime'] = self.iso8601(ts) + details = self.safe_list(response, 'details', []) + for i in range(0, len(details)): + balance = details[i] + currencyId = self.safe_string(balance, 'ccy') + code = self.safe_currency_code(currencyId) + account = self.account() + account['total'] = self.safe_string(balance, 'avail') + account['used'] = self.safe_string(balance, 'im') + result[code] = account + return self.safe_balance(result) + # for spot + for i in range(0, len(response)): + account = self.safe_value(response, i, {}) + balances = self.safe_value(account, 'balances') + for j in range(0, len(balances)): + balance = self.safe_value(balances, j) + currencyId = self.safe_string(balance, 'currency') + code = self.safe_currency_code(currencyId) + newAccount = self.account() + newAccount['free'] = self.safe_string(balance, 'available') + newAccount['used'] = self.safe_string(balance, 'hold') + result[code] = newAccount + return self.safe_balance(result) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://api-docs.poloniex.com/spot/api/private/account#all-account-balances + https://api-docs.poloniex.com/v3/futures/api/account/balance + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + marketType = None + marketType, params = self.handle_market_type_and_params('fetchBalance', None, params) + if marketType != 'spot': + responseRaw = self.swapPrivateGetV3AccountBalance(params) + # + # { + # "code": "200", + # "msg": "", + # "data": { + # "state": "NORMAL", + # "eq": "9.98571622", + # "isoEq": "0", + # "im": "0", + # "mm": "0", + # "mmr": "0", + # "upl": "0", + # "availMgn": "9.98571622", + # "cTime": "1738093601775", + # "uTime": "1740829116236", + # "details": [ + # { + # "ccy": "USDT", + # "eq": "9.98571622", + # "isoEq": "0", + # "avail": "9.98571622", + # "trdHold": "0", + # "upl": "0", + # "isoAvail": "0", + # "isoHold": "0", + # "isoUpl": "0", + # "im": "0", + # "mm": "0", + # "mmr": "0", + # "imr": "0", + # "cTime": "1740829116236", + # "uTime": "1740829116236" + # } + # ] + # } + # } + # + data = self.safe_dict(responseRaw, 'data', {}) + return self.parse_balance(data) + request: dict = { + 'accountType': 'SPOT', + } + response = self.privateGetAccountsBalances(self.extend(request, params)) + # + # [ + # { + # "accountId" : "7xxxxxxxxxx8", + # "accountType" : "SPOT", + # "balances" : [ + # { + # "currencyId" : "214", + # "currency" : "USDT", + # "available" : "2.00", + # "hold" : "0.00" + # } + # ] + # } + # ] + # + return self.parse_balance(response) + + def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://api-docs.poloniex.com/spot/api/private/account#fee-info + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + self.load_markets() + response = self.privateGetFeeinfo(params) + # + # { + # "trxDiscount" : False, + # "makerRate" : "0.00145", + # "takerRate" : "0.00155", + # "volume30D" : "0.00" + # } + # + result: dict = {} + for i in range(0, len(self.symbols)): + symbol = self.symbols[i] + result[symbol] = { + 'info': response, + 'symbol': symbol, + 'maker': self.safe_number(response, 'makerRate'), + 'taker': self.safe_number(response, 'takerRate'), + 'percentage': True, + 'tierBased': True, + } + return result + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://api-docs.poloniex.com/spot/api/public/market-data#order-book + https://api-docs.poloniex.com/v3/futures/api/market/get-order-book + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['limit'] = limit # The default value of limit is 10. Valid limit values are: 5, 10, 20, 50, 100, 150. + if market['contract']: + request['limit'] = self.find_nearest_ceiling([5, 10, 20, 100, 150], limit) + if market['contract']: + responseRaw = self.swapPublicGetV3MarketOrderBook(self.extend(request, params)) + # + # { + # "code": 200, + # "data": { + # "asks": [["58700", "9934"], ..], + # "bids": [["58600", "9952"], ..], + # "s": "100", + # "ts": 1719974138333 + # }, + # "msg": "Success" + # } + # + data = self.safe_dict(responseRaw, 'data', {}) + ts = self.safe_integer(data, 'ts') + return self.parse_order_book(data, symbol, ts) + response = self.publicGetMarketsSymbolOrderBook(self.extend(request, params)) + # + # { + # "time" : 1659695219507, + # "scale" : "-1", + # "asks" : ["23139.82", "0.317981", "23140", "0.191091", "23170.06", "0.01", "23200", "0.107758", "23230.55", "0.01", "23247.2", "0.154", "23254", "0.005121", "23263", "0.038", "23285.4", "0.308", "23300", "0.108896"], + # "bids" : ["23139.74", "0.432092", "23139.73", "0.198592", "23123.21", "0.000886", "23123.2", "0.308", "23121.4", "0.154", "23105", "0.000789", "23100", "0.078175", "23069.1", "0.026276", "23068.83", "0.001329", "23051", "0.000048"], + # "ts" : 1659695219512 + # } + # + timestamp = self.safe_integer(response, 'time') + asks = self.safe_value(response, 'asks') + bids = self.safe_value(response, 'bids') + asksResult = [] + bidsResult = [] + for i in range(0, len(asks)): + if (i % 2) < 1: + price = self.safe_number(asks, i) + amount = self.safe_number(asks, self.sum(i, 1)) + asksResult.append([price, amount]) + for i in range(0, len(bids)): + if (i % 2) < 1: + price = self.safe_number(bids, i) + amount = self.safe_number(bids, self.sum(i, 1)) + bidsResult.append([price, amount]) + return { + 'symbol': market['symbol'], + 'bids': self.sort_by(bidsResult, 0, True), + 'asks': self.sort_by(asksResult, 0), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'nonce': None, + } + + def create_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + create a currency deposit address + + https://api-docs.poloniex.com/spot/api/private/wallet#deposit-addresses + + :param str code: unified currency code of the currency for the deposit address + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + self.load_markets() + request, extraParams, currency, networkEntry = self.prepare_request_for_deposit_address(code, params) + params = extraParams + response = self.privatePostWalletsAddress(self.extend(request, params)) + # + # { + # "address" : "0xfxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxf" + # } + # + return self.parse_deposit_address_special(response, currency, networkEntry) + + def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://api-docs.poloniex.com/spot/api/private/wallet#deposit-addresses + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + self.load_markets() + request, extraParams, currency, networkEntry = self.prepare_request_for_deposit_address(code, params) + params = extraParams + response = self.privateGetWalletsAddresses(self.extend(request, params)) + # + # { + # "USDTTRON" : "Txxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxp" + # } + # + keys = list(response.keys()) + length = len(keys) + if length < 1: + raise ExchangeError(self.id + ' fetchDepositAddress() returned an empty response, you might need to try "createDepositAddress" at first and then use "fetchDepositAddress"') + return self.parse_deposit_address_special(response, currency, networkEntry) + + def prepare_request_for_deposit_address(self, code: str, params: dict = {}) -> Any: + if not (code in self.currencies): + raise BadSymbol(self.id + ' fetchDepositAddress(): can not recognize ' + code + ' currency, you might try using unified currency-code and add provide specific "network" parameter, like: fetchDepositAddress("USDT", {"network": "TRC20"})') + currency = self.currency(code) + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + if networkCode is None: + # we need to know the network to find out the currency-junction + raise ArgumentsRequired(self.id + ' fetchDepositAddress requires a network parameter for ' + code + '.') + exchangeNetworkId = None + networkCode = self.network_id_to_code(networkCode, code) + networkEntry = self.safe_dict(currency['networks'], networkCode) + if networkEntry is not None: + exchangeNetworkId = networkEntry['id'] + else: + exchangeNetworkId = networkCode + request = { + 'currency': exchangeNetworkId, + } + return [request, params, currency, networkEntry] + + def parse_deposit_address_special(self, response, currency, networkEntry) -> DepositAddress: + address = self.safe_string(response, 'address') + if address is None: + address = self.safe_string(response, networkEntry['id']) + tag: Str = None + self.check_address(address) + if networkEntry is not None: + depositAddress = self.safe_string(networkEntry['info'], 'depositAddress') + if depositAddress is not None: + tag = address + address = depositAddress + return { + 'info': response, + 'currency': currency['code'], + 'network': self.safe_string(networkEntry, 'network'), + 'address': address, + 'tag': tag, + } + + def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://api-docs.poloniex.com/spot/api/private/account#accounts-transfer + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account to transfer from + :param str toAccount: account to transfer to + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transfer structure ` + """ + self.load_markets() + currency = self.currency(code) + accountsByType = self.safe_value(self.options, 'accountsByType', {}) + fromId = self.safe_string(accountsByType, fromAccount, fromAccount) + toId = self.safe_string(accountsByType, toAccount, fromAccount) + request: dict = { + 'amount': self.currency_to_precision(code, amount), + 'currency': currency['id'], + 'fromAccount': fromId, + 'toAccount': toId, + } + response = self.privatePostAccountsTransfer(self.extend(request, params)) + # + # { + # "transferId" : "168041074" + # } + # + return self.parse_transfer(response, currency) + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + # + # { + # "transferId" : "168041074" + # } + # + return { + 'info': transfer, + 'id': self.safe_string(transfer, 'transferId'), + 'timestamp': None, + 'datetime': None, + 'currency': self.safe_string(currency, 'id'), + 'amount': None, + 'fromAccount': None, + 'toAccount': None, + 'status': None, + } + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://api-docs.poloniex.com/spot/api/private/wallet#withdraw-currency + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.check_address(address) + request, extraParams, currency, networkEntry = self.prepare_request_for_deposit_address(code, params) + params = extraParams + request['amount'] = self.currency_to_precision(code, amount) + request['address'] = address + if tag is not None: + request['paymentId'] = tag + response = self.privatePostWalletsWithdraw(self.extend(request, params)) + # + # { + # "response": "Withdrew 1.00000000 USDT.", + # "email2FA": False, + # "withdrawalNumber": 13449869 + # } + # + withdrawResponse = { + 'response': response, + 'withdrawNetworkEntry': networkEntry, + } + return self.parse_transaction(withdrawResponse, currency) + + def fetch_transactions_helper(self, code: Str = None, since: Int = None, limit: Int = None, params={}): + self.load_markets() + year = 31104000 # 60 * 60 * 24 * 30 * 12 = one year of history, why not + now = self.seconds() + start = self.parse_to_int(since / 1000) if (since is not None) else now - 10 * year + request: dict = { + 'start': start, # UNIX timestamp, required + 'end': now, # UNIX timestamp, required + } + response = self.privateGetWalletsActivity(self.extend(request, params)) + # + # { + # "adjustments":[], + # "deposits":[ + # { + # "currency": "BTC", + # "address": "1MEtiqJWru53FhhHrfJPPvd2tC3TPDVcmW", + # "amount": "0.01063000", + # "confirmations": 1, + # "txid": "952b0e1888d6d491591facc0d37b5ebec540ac1efb241fdbc22bcc20d1822fb6", + # "timestamp": 1507916888, + # "status": "COMPLETE" + # }, + # { + # "currency": "ETH", + # "address": "0x20108ba20b65c04d82909e91df06618107460197", + # "amount": "4.00000000", + # "confirmations": 38, + # "txid": "0x4be260073491fe63935e9e0da42bd71138fdeb803732f41501015a2d46eb479d", + # "timestamp": 1525060430, + # "status": "COMPLETE" + # } + # ], + # "withdrawals":[ + # { + # "withdrawalNumber":13449869, + # "currency":"USDTTRON", # not documented in API docs, see commonCurrencies in describe() + # "address":"TXGaqPW23JdRWhsVwS2mRsGsegbdnAd3Rw", + # "amount":"1.00000000", + # "fee":"0.00000000", + # "timestamp":1591573420, + # "status":"COMPLETE: dadf427224b3d44b38a2c13caa4395e4666152556ca0b2f67dbd86a95655150f", + # "ipAddress":"x.x.x.x", + # "canCancel":0, + # "canResendEmail":0, + # "paymentID":null, + # "scope":"crypto" + # }, + # { + # "withdrawalNumber": 8224394, + # "currency": "EMC2", + # "address": "EYEKyCrqTNmVCpdDV8w49XvSKRP9N3EUyF", + # "amount": "63.10796020", + # "fee": "0.01000000", + # "timestamp": 1510819838, + # "status": "COMPLETE: d37354f9d02cb24d98c8c4fc17aa42f475530b5727effdf668ee5a43ce667fd6", + # "ipAddress": "x.x.x.x" + # }, + # { + # "withdrawalNumber": 9290444, + # "currency": "ETH", + # "address": "0x191015ff2e75261d50433fbd05bd57e942336149", + # "amount": "0.15500000", + # "fee": "0.00500000", + # "timestamp": 1514099289, + # "status": "COMPLETE: 0x12d444493b4bca668992021fd9e54b5292b8e71d9927af1f076f554e4bea5b2d", + # "ipAddress": "x.x.x.x" + # }, + # { + # "withdrawalNumber": 11518260, + # "currency": "BTC", + # "address": "8JoDXAmE1GY2LRK8jD1gmAmgRPq54kXJ4t", + # "amount": "0.20000000", + # "fee": "0.00050000", + # "timestamp": 1527918155, + # "status": "COMPLETE: 1864f4ebb277d90b0b1ff53259b36b97fa1990edc7ad2be47c5e0ab41916b5ff", + # "ipAddress": "x.x.x.x" + # } + # ] + # } + # + return response + + def fetch_deposits_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch history of deposits and withdrawals + + https://api-docs.poloniex.com/spot/api/private/wallet#wallets-activity-records + + :param str [code]: unified currency code for the currency of the deposit/withdrawals, default is None + :param int [since]: timestamp in ms of the earliest deposit/withdrawal, default is None + :param int [limit]: max number of deposit/withdrawals to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `transaction structure ` + """ + self.load_markets() + response = self.fetch_transactions_helper(code, since, limit, params) + currency: Currency = None + if code is not None: + currency = self.currency(code) + withdrawals = self.safe_value(response, 'withdrawals', []) + deposits = self.safe_value(response, 'deposits', []) + withdrawalTransactions = self.parse_transactions(withdrawals, currency, since, limit) + depositTransactions = self.parse_transactions(deposits, currency, since, limit) + transactions = self.array_concat(depositTransactions, withdrawalTransactions) + return self.filter_by_currency_since_limit(self.sort_by(transactions, 'timestamp'), code, since, limit) + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://api-docs.poloniex.com/spot/api/private/wallet#wallets-activity-records + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + response = self.fetch_transactions_helper(code, since, limit, params) + currency: Currency = None + if code is not None: + currency = self.currency(code) + withdrawals = self.safe_value(response, 'withdrawals', []) + transactions = self.parse_transactions(withdrawals, currency, since, limit) + return self.filter_by_currency_since_limit(transactions, code, since, limit) + + def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}): + """ + fetch deposit and withdraw fees + + https://api-docs.poloniex.com/spot/api/public/reference-data#currency-information + + :param str[]|None codes: list of unified currency codes + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `fees structures ` + """ + self.load_markets() + response = self.publicGetCurrencies(self.extend(params, {'includeMultiChainCurrencies': True})) + # + # [ + # { + # "1CR": { + # "id": 1, + # "name": "1CRedit", + # "description": "BTC Clone", + # "type": "address", + # "withdrawalFee": "0.01000000", + # "minConf": 10000, + # "depositAddress": null, + # "blockchain": "1CR", + # "delisted": False, + # "tradingState": "NORMAL", + # "walletState": "DISABLED", + # "parentChain": null, + # "isMultiChain": False, + # "isChildChain": False, + # "childChains": [] + # } + # } + # ] + # + data: dict = {} + for i in range(0, len(response)): + entry = response[i] + currencies = list(entry.keys()) + currencyId = self.safe_string(currencies, 0) + data[currencyId] = entry[currencyId] + return self.parse_deposit_withdraw_fees(data, codes) + + def parse_deposit_withdraw_fees(self, response, codes=None, currencyIdKey=None): + # + # { + # "1CR": { + # "id": 1, + # "name": "1CRedit", + # "description": "BTC Clone", + # "type": "address", + # "withdrawalFee": "0.01000000", + # "minConf": 10000, + # "depositAddress": null, + # "blockchain": "1CR", + # "delisted": False, + # "tradingState": "NORMAL", + # "walletState": "DISABLED", + # "parentChain": null, + # "isMultiChain": False, + # "isChildChain": False, + # "childChains": [] + # }, + # } + # + depositWithdrawFees: dict = {} + codes = self.market_codes(codes) + responseKeys = list(response.keys()) + for i in range(0, len(responseKeys)): + currencyId = responseKeys[i] + code = self.safe_currency_code(currencyId) + feeInfo = response[currencyId] + if (codes is None) or (self.in_array(code, codes)): + currency = self.currency(code) + depositWithdrawFees[code] = self.parse_deposit_withdraw_fee(feeInfo, currency) + childChains = self.safe_value(feeInfo, 'childChains') + chainsLength = len(childChains) + if chainsLength > 0: + for j in range(0, len(childChains)): + networkId = childChains[j] + networkId = networkId.replace(code, '') + networkCode = self.network_id_to_code(networkId) + networkInfo = self.safe_value(response, networkId) + networkObject: dict = {} + withdrawFee = self.safe_number(networkInfo, 'withdrawalFee') + networkObject[networkCode] = { + 'withdraw': { + 'fee': withdrawFee, + 'percentage': False if (withdrawFee is not None) else None, + }, + 'deposit': { + 'fee': None, + 'percentage': None, + }, + } + depositWithdrawFees[code]['networks'] = self.extend(depositWithdrawFees[code]['networks'], networkObject) + return depositWithdrawFees + + def parse_deposit_withdraw_fee(self, fee, currency: Currency = None): + depositWithdrawFee = self.deposit_withdraw_fee({}) + depositWithdrawFee['info'][currency['code']] = fee + networkId = self.safe_string(fee, 'blockchain') + withdrawFee = self.safe_number(fee, 'withdrawalFee') + withdrawResult: dict = { + 'fee': withdrawFee, + 'percentage': False if (withdrawFee is not None) else None, + } + depositResult: dict = { + 'fee': None, + 'percentage': None, + } + depositWithdrawFee['withdraw'] = withdrawResult + depositWithdrawFee['deposit'] = depositResult + networkCode = self.network_id_to_code(networkId) + depositWithdrawFee['networks'][networkCode] = { + 'withdraw': withdrawResult, + 'deposit': depositResult, + } + return depositWithdrawFee + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://api-docs.poloniex.com/spot/api/private/wallet#wallets-activity-records + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + response = self.fetch_transactions_helper(code, since, limit, params) + currency = None + if code is not None: + currency = self.currency(code) + deposits = self.safe_value(response, 'deposits', []) + transactions = self.parse_transactions(deposits, currency, since, limit) + return self.filter_by_currency_since_limit(transactions, code, since, limit) + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'COMPLETE': 'ok', + 'COMPLETED': 'ok', + 'AWAITING APPROVAL': 'pending', + 'AWAITING_APPROVAL': 'pending', + 'PENDING': 'pending', + 'PROCESSING': 'pending', + 'COMPLETE ERROR': 'failed', + 'COMPLETE_ERROR': 'failed', + } + return self.safe_string(statuses, status, status) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # deposits + # + # { + # "txid": "f49d489616911db44b740612d19464521179c76ebe9021af85b6de1e2f8d68cd", + # "amount": "49798.01987021", + # "status": "COMPLETE", + # "address": "DJVJZ58tJC8UeUv9Tqcdtn6uhWobouxFLT", + # "currency": "DOGE", + # "timestamp": 1524321838, + # "confirmations": 3371, + # "depositNumber": 134587098 + # } + # + # withdrawals + # + # { + # "withdrawalRequestsId": 7397527, + # "currency": "ETC", + # "address": "0x26419a62055af459d2cd69bb7392f5100b75e304", + # "amount": "13.19951600", + # "fee": "0.01000000", + # "timestamp": 1506010932, + # "status": "COMPLETED", + # "txid": "343346392f82ac16e8c2604f2a604b7b2382d0e9d8030f673821f8de4b5f5bk", + # "ipAddress": "1.2.3.4", + # "paymentID": null + # } + # + # withdraw + # + # { + # "withdrawalRequestsId": 33485231 + # } + # + # if it's being parsed from "withdraw()" method, get the original response + if 'withdrawNetworkEntry' in transaction: + transaction = transaction['response'] + timestamp = self.safe_timestamp(transaction, 'timestamp') + currencyId = self.safe_string(transaction, 'currency') + code = self.safe_currency_code(currencyId) + status = self.safe_string(transaction, 'status', 'pending') + status = self.parse_transaction_status(status) + txid = self.safe_string(transaction, 'txid') + type = 'withdrawal' if ('withdrawalRequestsId' in transaction) else 'deposit' + id = self.safe_string_2(transaction, 'withdrawalRequestsId', 'depositNumber') + address = self.safe_string(transaction, 'address') + tag = self.safe_string(transaction, 'paymentID') + amountString = self.safe_string(transaction, 'amount') + feeCostString = self.safe_string(transaction, 'fee') + if type == 'withdrawal': + amountString = Precise.string_sub(amountString, feeCostString) + return { + 'info': transaction, + 'id': id, + 'currency': code, + 'amount': self.parse_number(amountString), + 'network': None, + 'address': address, + 'addressTo': None, + 'addressFrom': None, + 'tag': tag, + 'tagTo': None, + 'tagFrom': None, + 'status': status, + 'type': type, + 'updated': None, + 'txid': txid, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'comment': None, + 'internal': None, + 'fee': { + 'currency': code, + 'cost': self.parse_number(feeCostString), + 'rate': None, + }, + } + + def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://api-docs.poloniex.com/v3/futures/api/positions/set-leverage + + :param int leverage: the rate of leverage + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setLeverage() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('setLeverage', params) + if marginMode is None: + raise ArgumentsRequired(self.id + ' setLeverage() requires a marginMode parameter "cross" or "isolated"') + hedged: Bool = None + hedged, params = self.handle_param_bool(params, 'hedged', False) + if hedged: + if not ('posSide' in params): + raise ArgumentsRequired(self.id + ' setLeverage() requires a posSide parameter for hedged mode: "LONG" or "SHORT"') + request: dict = { + 'lever': leverage, + 'mgnMode': marginMode.upper(), + 'symbol': market['id'], + } + response = self.swapPrivatePostV3PositionLeverage(self.extend(request, params)) + return response + + def fetch_leverage(self, symbol: str, params={}) -> Leverage: + """ + fetch the set leverage for a market + + https://api-docs.poloniex.com/v3/futures/api/positions/get-leverages + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `leverage structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchLeverage', params) + if marginMode is None: + raise ArgumentsRequired(self.id + ' fetchLeverage() requires a marginMode parameter "cross" or "isolated"') + request['mgnMode'] = marginMode.upper() + response = self.swapPrivateGetV3PositionLeverages(self.extend(request, params)) + # + # for one-way mode: + # + # { + # "code": "200", + # "msg": "", + # "data": [ + # { + # "symbol": "BTC_USDT_PERP", + # "lever": "10", + # "mgnMode": "CROSS", + # "posSide": "BOTH" + # } + # ] + # } + # + # for hedge: + # + # { + # "code": "200", + # "msg": "", + # "data": [ + # { + # "symbol": "BTC_USDT_PERP", + # "lever": "20", + # "mgnMode": "CROSS", + # "posSide": "SHORT" + # }, + # { + # "symbol": "BTC_USDT_PERP", + # "lever": "20", + # "mgnMode": "CROSS", + # "posSide": "LONG" + # } + # ] + # } + # + return self.parse_leverage(response, market) + + def parse_leverage(self, leverage: dict, market: Market = None) -> Leverage: + shortLeverage: Int = None + longLeverage: Int = None + marketId: Str = None + marginMode: Str = None + data = self.safe_list(leverage, 'data') + for i in range(0, len(data)): + entry = data[i] + marketId = self.safe_string(entry, 'symbol') + marginMode = self.safe_string(entry, 'mgnMode') + lever = self.safe_integer(entry, 'lever') + posSide = self.safe_string(entry, 'posSide') + if posSide == 'LONG': + longLeverage = lever + elif posSide == 'SHORT': + shortLeverage = lever + else: + longLeverage = lever + shortLeverage = lever + return { + 'info': leverage, + 'symbol': self.safe_symbol(marketId, market), + 'marginMode': marginMode, + 'longLeverage': longLeverage, + 'shortLeverage': shortLeverage, + } + + def fetch_position_mode(self, symbol: Str = None, params={}): + """ + fetchs the position mode, hedged or one way, hedged for binance is set identically for all linear markets or all inverse markets + + https://api-docs.poloniex.com/v3/futures/api/positions/position-mode-switch + + :param str symbol: unified symbol of the market to fetch the order book for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an object detailing whether the market is in hedged or one-way mode + """ + response = self.swapPrivateGetV3PositionMode(params) + # + # { + # "code": "200", + # "msg": "Success", + # "data": { + # "posMode": "ONE_WAY" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + posMode = self.safe_string(data, 'posMode') + hedged = posMode == 'HEDGE' + return { + 'info': response, + 'hedged': hedged, + } + + def set_position_mode(self, hedged: bool, symbol: Str = None, params={}): + """ + set hedged to True or False for a market + + https://api-docs.poloniex.com/v3/futures/api/positions/position-mode-switch + + :param bool hedged: set to True to use dualSidePosition + :param str symbol: not used by binance setPositionMode() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + mode = 'HEDGE' if hedged else 'ONE_WAY' + request: dict = { + 'posMode': mode, + } + response = self.swapPrivatePostV3PositionMode(self.extend(request, params)) + # + # { + # "code": "200", + # "msg": "Success", + # "data": {} + # } + # + return response + + def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://api-docs.poloniex.com/v3/futures/api/positions/get-current-position + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.standard]: whether to fetch standard contract positions + :returns dict[]: a list of `position structures ` + """ + self.load_markets() + symbols = self.market_symbols(symbols) + response = self.swapPrivateGetV3TradePositionOpens(params) + # + # { + # "code": "200", + # "msg": "", + # "data": [ + # { + # "symbol": "BTC_USDT_PERP", + # "posSide": "LONG", + # "side": "BUY", + # "mgnMode": "CROSS", + # "openAvgPx": "94193.42", + # "qty": "1", + # "availQty": "1", + # "lever": "20", + # "adl": "0.3007", + # "liqPx": "84918.201844064386317906", + # "im": "4.7047795", + # "mm": "0.56457354", + # "upl": "-0.09783", + # "uplRatio": "-0.0207", + # "pnl": "0", + # "markPx": "94095.59", + # "mgnRatio": "0.0582", + # "state": "NORMAL", + # "cTime": "1740950344401", + # "uTime": "1740950344401", + # "mgn": "4.7047795", + # "actType": "TRADING", + # "maxWAmt": "0", + # "tpTrgPx": "", + # "slTrgPx": "" + # } + # ] + # } + # + positions = self.safe_list(response, 'data', []) + return self.parse_positions(positions, symbols) + + def parse_position(self, position: dict, market: Market = None): + # + # { + # "symbol": "BTC_USDT_PERP", + # "posSide": "LONG", + # "side": "BUY", + # "mgnMode": "CROSS", + # "openAvgPx": "94193.42", + # "qty": "1", + # "availQty": "1", + # "lever": "20", + # "adl": "0.3007", + # "liqPx": "84918.201844064386317906", + # "im": "4.7047795", + # "mm": "0.56457354", + # "upl": "-0.09783", + # "uplRatio": "-0.0207", + # "pnl": "0", + # "markPx": "94095.59", + # "mgnRatio": "0.0582", + # "state": "NORMAL", + # "cTime": "1740950344401", + # "uTime": "1740950344401", + # "mgn": "4.7047795", + # "actType": "TRADING", + # "maxWAmt": "0", + # "tpTrgPx": "", + # "slTrgPx": "" + # } + # + marketId = self.safe_string(position, 'symbol') + market = self.safe_market(marketId, market) + timestamp = self.safe_integer(position, 'cTime') + marginMode = self.safe_string_lower(position, 'mgnMode') + leverage = self.safe_string(position, 'lever') + initialMargin = self.safe_string(position, 'im') + notional = Precise.string_mul(leverage, initialMargin) + qty = self.safe_string(position, 'qty') + avgPrice = self.safe_string(position, 'openAvgPx') + collateral = Precise.string_mul(qty, avgPrice) + # todo: some more fields + return self.safe_position({ + 'info': position, + 'id': None, + 'symbol': market['symbol'], + 'notional': notional, + 'marginMode': marginMode, + 'liquidationPrice': self.safe_number(position, 'liqPx'), + 'entryPrice': self.safe_number(position, 'openAvgPx'), + 'unrealizedPnl': self.safe_number(position, 'upl'), + 'percentage': None, + 'contracts': self.safe_number(position, 'qty'), + 'contractSize': None, + 'markPrice': self.safe_number(position, 'markPx'), + 'lastPrice': None, + 'side': self.safe_string_lower(position, 'posSide'), + 'hedged': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastUpdateTimestamp': None, + 'maintenanceMargin': self.safe_number(position, 'mm'), + 'maintenanceMarginPercentage': None, + 'collateral': collateral, + 'initialMargin': initialMargin, + 'initialMarginPercentage': None, + 'leverage': int(leverage), + 'marginRatio': self.safe_number(position, 'mgnRatio'), + 'stopLossPrice': self.safe_number(position, 'slTrgPx'), + 'takeProfitPrice': self.safe_number(position, 'tpTrgPx'), + }) + + def modify_margin_helper(self, symbol: str, amount, type, params={}) -> MarginModification: + self.load_markets() + market = self.market(symbol) + amount = self.amount_to_precision(symbol, amount) + request: dict = { + 'symbol': market['id'], + 'amt': Precise.string_abs(amount), + 'type': type.upper(), # 'ADD' or 'REDUCE' + } + # todo: hedged handling, tricky + if not ('posMode' in params): + request['posMode'] = 'BOTH' + response = self.swapPrivatePostV3TradePositionMargin(self.extend(request, params)) + # + # { + # "code": 200, + # "data": { + # "amt": "50", + # "lever": "20", + # "symbol": "DOT_USDT_PERP", + # "posSide": "BOTH", + # "type": "ADD" + # }, + # "msg": "Success" + # } + # + if type == 'reduce': + amount = Precise.string_abs(amount) + data = self.safe_dict(response, 'data') + return self.parse_margin_modification(data, market) + + def parse_margin_modification(self, data: dict, market: Market = None) -> MarginModification: + marketId = self.safe_string(data, 'symbol') + market = self.safe_market(marketId, market) + rawType = self.safe_string(data, 'type') + type = 'add' if (rawType == 'ADD') else 'reduce' + return { + 'info': data, + 'symbol': market['symbol'], + 'type': type, + 'marginMode': None, + 'amount': self.safe_number(data, 'amt'), + 'total': None, + 'code': None, + 'status': 'ok', + 'timestamp': None, + 'datetime': None, + } + + def reduce_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + remove margin from a position + :param str symbol: unified market symbol + :param float amount: the amount of margin to remove + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin structure ` + """ + return self.modify_margin_helper(symbol, -amount, 'reduce', params) + + def add_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + add margin + :param str symbol: unified market symbol + :param float amount: amount of margin to add + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin structure ` + """ + return self.modify_margin_helper(symbol, amount, 'add', params) + + def nonce(self): + return self.milliseconds() + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = self.urls['api']['spot'] + if self.in_array(api, ['swapPublic', 'swapPrivate']): + url = self.urls['api']['swap'] + if 'symbol' in params: + params['symbol'] = self.encode_uri_component(params['symbol']) # handle symbols like 索拉拉/USDT' + query = self.omit(params, self.extract_params(path)) + implodedPath = self.implode_params(path, params) + if api == 'public' or api == 'swapPublic': + url += '/' + implodedPath + if query: + url += '?' + self.urlencode(query) + else: + self.check_required_credentials() + timestamp = str(self.nonce()) + auth = method + "\n" # eslint-disable-line quotes + url += '/' + implodedPath + auth += '/' + implodedPath + if (method == 'POST') or (method == 'PUT') or (method == 'DELETE'): + auth += "\n" # eslint-disable-line quotes + if query: + body = self.json(query) + auth += 'requestBody=' + body + '&' + auth += 'signTimestamp=' + timestamp + else: + sortedQuery = self.extend({'signTimestamp': timestamp}, query) + sortedQuery = self.keysort(sortedQuery) + auth += "\n" + self.urlencode(sortedQuery) # eslint-disable-line quotes + if query: + url += '?' + self.urlencode(query) + signature = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256, 'base64') + headers = { + 'Content-Type': 'application/json', + 'key': self.apiKey, + 'signTimestamp': timestamp, + 'signature': signature, + } + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None + # + # { + # "code" : 21709, + # "message" : "Low available balance" + # } + # + responseCode = self.safe_string(response, 'code') + if (responseCode is not None) and (responseCode != '200'): + codeInner = response['code'] + message = self.safe_string(response, 'message') + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], codeInner, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + raise ExchangeError(feedback) # unknown message + return None diff --git a/ccxt/pro/__init__.py b/ccxt/pro/__init__.py new file mode 100644 index 0000000..16b375c --- /dev/null +++ b/ccxt/pro/__init__.py @@ -0,0 +1,212 @@ +# -*- coding: utf-8 -*- + +"""CCXT: CryptoCurrency eXchange Trading Library (Async)""" + +# ---------------------------------------------------------------------------- + +__version__ = '4.5.18' + +# ---------------------------------------------------------------------------- + +from ccxt.async_support.base.exchange import Exchange # noqa: F401 + +# CCXT Pro exchanges (now this is mainly used for importing exchanges in WS tests) + +# DO_NOT_REMOVE__ERROR_IMPORTS_START +from ccxt.base.errors import BaseError # noqa: F401 +from ccxt.base.errors import ExchangeError # noqa: F401 +from ccxt.base.errors import AuthenticationError # noqa: F401 +from ccxt.base.errors import PermissionDenied # noqa: F401 +from ccxt.base.errors import AccountNotEnabled # noqa: F401 +from ccxt.base.errors import AccountSuspended # noqa: F401 +from ccxt.base.errors import ArgumentsRequired # noqa: F401 +from ccxt.base.errors import BadRequest # noqa: F401 +from ccxt.base.errors import BadSymbol # noqa: F401 +from ccxt.base.errors import OperationRejected # noqa: F401 +from ccxt.base.errors import NoChange # noqa: F401 +from ccxt.base.errors import MarginModeAlreadySet # noqa: F401 +from ccxt.base.errors import MarketClosed # noqa: F401 +from ccxt.base.errors import ManualInteractionNeeded # noqa: F401 +from ccxt.base.errors import RestrictedLocation # noqa: F401 +from ccxt.base.errors import InsufficientFunds # noqa: F401 +from ccxt.base.errors import InvalidAddress # noqa: F401 +from ccxt.base.errors import AddressPending # noqa: F401 +from ccxt.base.errors import InvalidOrder # noqa: F401 +from ccxt.base.errors import OrderNotFound # noqa: F401 +from ccxt.base.errors import OrderNotCached # noqa: F401 +from ccxt.base.errors import OrderImmediatelyFillable # noqa: F401 +from ccxt.base.errors import OrderNotFillable # noqa: F401 +from ccxt.base.errors import DuplicateOrderId # noqa: F401 +from ccxt.base.errors import ContractUnavailable # noqa: F401 +from ccxt.base.errors import NotSupported # noqa: F401 +from ccxt.base.errors import InvalidProxySettings # noqa: F401 +from ccxt.base.errors import ExchangeClosedByUser # noqa: F401 +from ccxt.base.errors import OperationFailed # noqa: F401 +from ccxt.base.errors import NetworkError # noqa: F401 +from ccxt.base.errors import DDoSProtection # noqa: F401 +from ccxt.base.errors import RateLimitExceeded # noqa: F401 +from ccxt.base.errors import ExchangeNotAvailable # noqa: F401 +from ccxt.base.errors import OnMaintenance # noqa: F401 +from ccxt.base.errors import InvalidNonce # noqa: F401 +from ccxt.base.errors import ChecksumError # noqa: F401 +from ccxt.base.errors import RequestTimeout # noqa: F401 +from ccxt.base.errors import BadResponse # noqa: F401 +from ccxt.base.errors import NullResponse # noqa: F401 +from ccxt.base.errors import CancelPending # noqa: F401 +from ccxt.base.errors import UnsubscribeError # noqa: F401 +from ccxt.base.errors import error_hierarchy # noqa: F401 +# DO_NOT_REMOVE__ERROR_IMPORTS_END + +from ccxt.pro.alpaca import alpaca # noqa: F401 +from ccxt.pro.apex import apex # noqa: F401 +from ccxt.pro.arkham import arkham # noqa: F401 +from ccxt.pro.ascendex import ascendex # noqa: F401 +from ccxt.pro.backpack import backpack # noqa: F401 +from ccxt.pro.bequant import bequant # noqa: F401 +from ccxt.pro.binance import binance # noqa: F401 +from ccxt.pro.binancecoinm import binancecoinm # noqa: F401 +from ccxt.pro.binanceus import binanceus # noqa: F401 +from ccxt.pro.binanceusdm import binanceusdm # noqa: F401 +from ccxt.pro.bingx import bingx # noqa: F401 +from ccxt.pro.bitfinex import bitfinex # noqa: F401 +from ccxt.pro.bitget import bitget # noqa: F401 +from ccxt.pro.bithumb import bithumb # noqa: F401 +from ccxt.pro.bitmart import bitmart # noqa: F401 +from ccxt.pro.bitmex import bitmex # noqa: F401 +from ccxt.pro.bitopro import bitopro # noqa: F401 +from ccxt.pro.bitrue import bitrue # noqa: F401 +from ccxt.pro.bitstamp import bitstamp # noqa: F401 +from ccxt.pro.bittrade import bittrade # noqa: F401 +from ccxt.pro.bitvavo import bitvavo # noqa: F401 +from ccxt.pro.blockchaincom import blockchaincom # noqa: F401 +from ccxt.pro.blofin import blofin # noqa: F401 +from ccxt.pro.bybit import bybit # noqa: F401 +from ccxt.pro.cex import cex # noqa: F401 +from ccxt.pro.coinbase import coinbase # noqa: F401 +from ccxt.pro.coinbaseadvanced import coinbaseadvanced # noqa: F401 +from ccxt.pro.coinbaseexchange import coinbaseexchange # noqa: F401 +from ccxt.pro.coinbaseinternational import coinbaseinternational # noqa: F401 +from ccxt.pro.coincatch import coincatch # noqa: F401 +from ccxt.pro.coincheck import coincheck # noqa: F401 +from ccxt.pro.coinex import coinex # noqa: F401 +from ccxt.pro.coinone import coinone # noqa: F401 +from ccxt.pro.cryptocom import cryptocom # noqa: F401 +from ccxt.pro.deepcoin import deepcoin # noqa: F401 +from ccxt.pro.defx import defx # noqa: F401 +from ccxt.pro.deribit import deribit # noqa: F401 +from ccxt.pro.derive import derive # noqa: F401 +from ccxt.pro.exmo import exmo # noqa: F401 +from ccxt.pro.gate import gate # noqa: F401 +from ccxt.pro.gateio import gateio # noqa: F401 +from ccxt.pro.gemini import gemini # noqa: F401 +from ccxt.pro.hashkey import hashkey # noqa: F401 +from ccxt.pro.hitbtc import hitbtc # noqa: F401 +from ccxt.pro.hollaex import hollaex # noqa: F401 +from ccxt.pro.htx import htx # noqa: F401 +from ccxt.pro.huobi import huobi # noqa: F401 +from ccxt.pro.hyperliquid import hyperliquid # noqa: F401 +from ccxt.pro.independentreserve import independentreserve # noqa: F401 +from ccxt.pro.kraken import kraken # noqa: F401 +from ccxt.pro.krakenfutures import krakenfutures # noqa: F401 +from ccxt.pro.kucoin import kucoin # noqa: F401 +from ccxt.pro.kucoinfutures import kucoinfutures # noqa: F401 +from ccxt.pro.lbank import lbank # noqa: F401 +from ccxt.pro.luno import luno # noqa: F401 +from ccxt.pro.mexc import mexc # noqa: F401 +from ccxt.pro.modetrade import modetrade # noqa: F401 +from ccxt.pro.myokx import myokx # noqa: F401 +from ccxt.pro.ndax import ndax # noqa: F401 +from ccxt.pro.okx import okx # noqa: F401 +from ccxt.pro.okxus import okxus # noqa: F401 +from ccxt.pro.onetrading import onetrading # noqa: F401 +from ccxt.pro.oxfun import oxfun # noqa: F401 +from ccxt.pro.p2b import p2b # noqa: F401 +from ccxt.pro.paradex import paradex # noqa: F401 +from ccxt.pro.phemex import phemex # noqa: F401 +from ccxt.pro.poloniex import poloniex # noqa: F401 +from ccxt.pro.probit import probit # noqa: F401 +from ccxt.pro.toobit import toobit # noqa: F401 +from ccxt.pro.upbit import upbit # noqa: F401 +from ccxt.pro.whitebit import whitebit # noqa: F401 +from ccxt.pro.woo import woo # noqa: F401 +from ccxt.pro.woofipro import woofipro # noqa: F401 +from ccxt.pro.xt import xt # noqa: F401 +from ccxt.pro.mt5 import mt5 # noqa: F401 + +exchanges = [ + 'alpaca', + 'apex', + 'arkham', + 'ascendex', + 'backpack', + 'bequant', + 'binance', + 'binancecoinm', + 'binanceus', + 'binanceusdm', + 'bingx', + 'bitfinex', + 'bitget', + 'bithumb', + 'bitmart', + 'bitmex', + 'bitopro', + 'bitrue', + 'bitstamp', + 'bittrade', + 'bitvavo', + 'blockchaincom', + 'blofin', + 'bybit', + 'cex', + 'coinbase', + 'coinbaseadvanced', + 'coinbaseexchange', + 'coinbaseinternational', + 'coincatch', + 'coincheck', + 'coinex', + 'coinone', + 'cryptocom', + 'deepcoin', + 'defx', + 'deribit', + 'derive', + 'exmo', + 'gate', + 'gateio', + 'gemini', + 'hashkey', + 'hitbtc', + 'hollaex', + 'htx', + 'huobi', + 'hyperliquid', + 'independentreserve', + 'kraken', + 'krakenfutures', + 'kucoin', + 'kucoinfutures', + 'lbank', + 'luno', + 'mexc', + 'modetrade', + 'myokx', + 'ndax', + 'okx', + 'okxus', + 'onetrading', + 'oxfun', + 'p2b', + 'paradex', + 'phemex', + 'poloniex', + 'probit', + 'toobit', + 'upbit', + 'whitebit', + 'woo', + 'woofipro', + 'xt', + 'mt5', +] diff --git a/ccxt/pro/__pycache__/__init__.cpython-311.pyc b/ccxt/pro/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..0f07b90 Binary files /dev/null and b/ccxt/pro/__pycache__/__init__.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/alpaca.cpython-311.pyc b/ccxt/pro/__pycache__/alpaca.cpython-311.pyc new file mode 100644 index 0000000..f6fda13 Binary files /dev/null and b/ccxt/pro/__pycache__/alpaca.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/apex.cpython-311.pyc b/ccxt/pro/__pycache__/apex.cpython-311.pyc new file mode 100644 index 0000000..26bba82 Binary files /dev/null and b/ccxt/pro/__pycache__/apex.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/arkham.cpython-311.pyc b/ccxt/pro/__pycache__/arkham.cpython-311.pyc new file mode 100644 index 0000000..48ba552 Binary files /dev/null and b/ccxt/pro/__pycache__/arkham.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/ascendex.cpython-311.pyc b/ccxt/pro/__pycache__/ascendex.cpython-311.pyc new file mode 100644 index 0000000..86454ac Binary files /dev/null and b/ccxt/pro/__pycache__/ascendex.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/backpack.cpython-311.pyc b/ccxt/pro/__pycache__/backpack.cpython-311.pyc new file mode 100644 index 0000000..5673cc5 Binary files /dev/null and b/ccxt/pro/__pycache__/backpack.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/bequant.cpython-311.pyc b/ccxt/pro/__pycache__/bequant.cpython-311.pyc new file mode 100644 index 0000000..4bb9b15 Binary files /dev/null and b/ccxt/pro/__pycache__/bequant.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/binance.cpython-311.pyc b/ccxt/pro/__pycache__/binance.cpython-311.pyc new file mode 100644 index 0000000..3c68812 Binary files /dev/null and b/ccxt/pro/__pycache__/binance.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/binancecoinm.cpython-311.pyc b/ccxt/pro/__pycache__/binancecoinm.cpython-311.pyc new file mode 100644 index 0000000..28cf988 Binary files /dev/null and b/ccxt/pro/__pycache__/binancecoinm.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/binanceus.cpython-311.pyc b/ccxt/pro/__pycache__/binanceus.cpython-311.pyc new file mode 100644 index 0000000..e08a925 Binary files /dev/null and b/ccxt/pro/__pycache__/binanceus.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/binanceusdm.cpython-311.pyc b/ccxt/pro/__pycache__/binanceusdm.cpython-311.pyc new file mode 100644 index 0000000..a31d811 Binary files /dev/null and b/ccxt/pro/__pycache__/binanceusdm.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/bingx.cpython-311.pyc b/ccxt/pro/__pycache__/bingx.cpython-311.pyc new file mode 100644 index 0000000..898d9f3 Binary files /dev/null and b/ccxt/pro/__pycache__/bingx.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/bitfinex.cpython-311.pyc b/ccxt/pro/__pycache__/bitfinex.cpython-311.pyc new file mode 100644 index 0000000..5f775d7 Binary files /dev/null and b/ccxt/pro/__pycache__/bitfinex.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/bitget.cpython-311.pyc b/ccxt/pro/__pycache__/bitget.cpython-311.pyc new file mode 100644 index 0000000..bc34846 Binary files /dev/null and b/ccxt/pro/__pycache__/bitget.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/bithumb.cpython-311.pyc b/ccxt/pro/__pycache__/bithumb.cpython-311.pyc new file mode 100644 index 0000000..9ea5274 Binary files /dev/null and b/ccxt/pro/__pycache__/bithumb.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/bitmart.cpython-311.pyc b/ccxt/pro/__pycache__/bitmart.cpython-311.pyc new file mode 100644 index 0000000..5d73b8a Binary files /dev/null and b/ccxt/pro/__pycache__/bitmart.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/bitmex.cpython-311.pyc b/ccxt/pro/__pycache__/bitmex.cpython-311.pyc new file mode 100644 index 0000000..6092b7a Binary files /dev/null and b/ccxt/pro/__pycache__/bitmex.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/bitopro.cpython-311.pyc b/ccxt/pro/__pycache__/bitopro.cpython-311.pyc new file mode 100644 index 0000000..9d6285e Binary files /dev/null and b/ccxt/pro/__pycache__/bitopro.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/bitrue.cpython-311.pyc b/ccxt/pro/__pycache__/bitrue.cpython-311.pyc new file mode 100644 index 0000000..4babe26 Binary files /dev/null and b/ccxt/pro/__pycache__/bitrue.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/bitstamp.cpython-311.pyc b/ccxt/pro/__pycache__/bitstamp.cpython-311.pyc new file mode 100644 index 0000000..db2a7d8 Binary files /dev/null and b/ccxt/pro/__pycache__/bitstamp.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/bittrade.cpython-311.pyc b/ccxt/pro/__pycache__/bittrade.cpython-311.pyc new file mode 100644 index 0000000..84a027e Binary files /dev/null and b/ccxt/pro/__pycache__/bittrade.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/bitvavo.cpython-311.pyc b/ccxt/pro/__pycache__/bitvavo.cpython-311.pyc new file mode 100644 index 0000000..5711bf4 Binary files /dev/null and b/ccxt/pro/__pycache__/bitvavo.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/blockchaincom.cpython-311.pyc b/ccxt/pro/__pycache__/blockchaincom.cpython-311.pyc new file mode 100644 index 0000000..3ee3df5 Binary files /dev/null and b/ccxt/pro/__pycache__/blockchaincom.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/blofin.cpython-311.pyc b/ccxt/pro/__pycache__/blofin.cpython-311.pyc new file mode 100644 index 0000000..b1cd265 Binary files /dev/null and b/ccxt/pro/__pycache__/blofin.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/bybit.cpython-311.pyc b/ccxt/pro/__pycache__/bybit.cpython-311.pyc new file mode 100644 index 0000000..19dbf96 Binary files /dev/null and b/ccxt/pro/__pycache__/bybit.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/cex.cpython-311.pyc b/ccxt/pro/__pycache__/cex.cpython-311.pyc new file mode 100644 index 0000000..f38e02c Binary files /dev/null and b/ccxt/pro/__pycache__/cex.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/coinbase.cpython-311.pyc b/ccxt/pro/__pycache__/coinbase.cpython-311.pyc new file mode 100644 index 0000000..fbcc118 Binary files /dev/null and b/ccxt/pro/__pycache__/coinbase.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/coinbaseadvanced.cpython-311.pyc b/ccxt/pro/__pycache__/coinbaseadvanced.cpython-311.pyc new file mode 100644 index 0000000..0ba1940 Binary files /dev/null and b/ccxt/pro/__pycache__/coinbaseadvanced.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/coinbaseexchange.cpython-311.pyc b/ccxt/pro/__pycache__/coinbaseexchange.cpython-311.pyc new file mode 100644 index 0000000..c499dd1 Binary files /dev/null and b/ccxt/pro/__pycache__/coinbaseexchange.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/coinbaseinternational.cpython-311.pyc b/ccxt/pro/__pycache__/coinbaseinternational.cpython-311.pyc new file mode 100644 index 0000000..c40b4cb Binary files /dev/null and b/ccxt/pro/__pycache__/coinbaseinternational.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/coincatch.cpython-311.pyc b/ccxt/pro/__pycache__/coincatch.cpython-311.pyc new file mode 100644 index 0000000..619c595 Binary files /dev/null and b/ccxt/pro/__pycache__/coincatch.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/coincheck.cpython-311.pyc b/ccxt/pro/__pycache__/coincheck.cpython-311.pyc new file mode 100644 index 0000000..dd0f7bd Binary files /dev/null and b/ccxt/pro/__pycache__/coincheck.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/coinex.cpython-311.pyc b/ccxt/pro/__pycache__/coinex.cpython-311.pyc new file mode 100644 index 0000000..a569ff1 Binary files /dev/null and b/ccxt/pro/__pycache__/coinex.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/coinone.cpython-311.pyc b/ccxt/pro/__pycache__/coinone.cpython-311.pyc new file mode 100644 index 0000000..a0f2706 Binary files /dev/null and b/ccxt/pro/__pycache__/coinone.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/cryptocom.cpython-311.pyc b/ccxt/pro/__pycache__/cryptocom.cpython-311.pyc new file mode 100644 index 0000000..e5d5642 Binary files /dev/null and b/ccxt/pro/__pycache__/cryptocom.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/deepcoin.cpython-311.pyc b/ccxt/pro/__pycache__/deepcoin.cpython-311.pyc new file mode 100644 index 0000000..6562909 Binary files /dev/null and b/ccxt/pro/__pycache__/deepcoin.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/defx.cpython-311.pyc b/ccxt/pro/__pycache__/defx.cpython-311.pyc new file mode 100644 index 0000000..3d475b5 Binary files /dev/null and b/ccxt/pro/__pycache__/defx.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/deribit.cpython-311.pyc b/ccxt/pro/__pycache__/deribit.cpython-311.pyc new file mode 100644 index 0000000..3819e90 Binary files /dev/null and b/ccxt/pro/__pycache__/deribit.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/derive.cpython-311.pyc b/ccxt/pro/__pycache__/derive.cpython-311.pyc new file mode 100644 index 0000000..cedb73e Binary files /dev/null and b/ccxt/pro/__pycache__/derive.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/exmo.cpython-311.pyc b/ccxt/pro/__pycache__/exmo.cpython-311.pyc new file mode 100644 index 0000000..4b098f1 Binary files /dev/null and b/ccxt/pro/__pycache__/exmo.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/gate.cpython-311.pyc b/ccxt/pro/__pycache__/gate.cpython-311.pyc new file mode 100644 index 0000000..b20c85c Binary files /dev/null and b/ccxt/pro/__pycache__/gate.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/gateio.cpython-311.pyc b/ccxt/pro/__pycache__/gateio.cpython-311.pyc new file mode 100644 index 0000000..38f3c25 Binary files /dev/null and b/ccxt/pro/__pycache__/gateio.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/gemini.cpython-311.pyc b/ccxt/pro/__pycache__/gemini.cpython-311.pyc new file mode 100644 index 0000000..84e2202 Binary files /dev/null and b/ccxt/pro/__pycache__/gemini.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/hashkey.cpython-311.pyc b/ccxt/pro/__pycache__/hashkey.cpython-311.pyc new file mode 100644 index 0000000..b5dc99b Binary files /dev/null and b/ccxt/pro/__pycache__/hashkey.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/hitbtc.cpython-311.pyc b/ccxt/pro/__pycache__/hitbtc.cpython-311.pyc new file mode 100644 index 0000000..de82ce1 Binary files /dev/null and b/ccxt/pro/__pycache__/hitbtc.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/hollaex.cpython-311.pyc b/ccxt/pro/__pycache__/hollaex.cpython-311.pyc new file mode 100644 index 0000000..60c6d08 Binary files /dev/null and b/ccxt/pro/__pycache__/hollaex.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/htx.cpython-311.pyc b/ccxt/pro/__pycache__/htx.cpython-311.pyc new file mode 100644 index 0000000..e4d661d Binary files /dev/null and b/ccxt/pro/__pycache__/htx.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/huobi.cpython-311.pyc b/ccxt/pro/__pycache__/huobi.cpython-311.pyc new file mode 100644 index 0000000..f705024 Binary files /dev/null and b/ccxt/pro/__pycache__/huobi.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/hyperliquid.cpython-311.pyc b/ccxt/pro/__pycache__/hyperliquid.cpython-311.pyc new file mode 100644 index 0000000..894fc69 Binary files /dev/null and b/ccxt/pro/__pycache__/hyperliquid.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/independentreserve.cpython-311.pyc b/ccxt/pro/__pycache__/independentreserve.cpython-311.pyc new file mode 100644 index 0000000..e07b87a Binary files /dev/null and b/ccxt/pro/__pycache__/independentreserve.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/kraken.cpython-311.pyc b/ccxt/pro/__pycache__/kraken.cpython-311.pyc new file mode 100644 index 0000000..dc90d77 Binary files /dev/null and b/ccxt/pro/__pycache__/kraken.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/krakenfutures.cpython-311.pyc b/ccxt/pro/__pycache__/krakenfutures.cpython-311.pyc new file mode 100644 index 0000000..b79b395 Binary files /dev/null and b/ccxt/pro/__pycache__/krakenfutures.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/kucoin.cpython-311.pyc b/ccxt/pro/__pycache__/kucoin.cpython-311.pyc new file mode 100644 index 0000000..c336e43 Binary files /dev/null and b/ccxt/pro/__pycache__/kucoin.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/kucoinfutures.cpython-311.pyc b/ccxt/pro/__pycache__/kucoinfutures.cpython-311.pyc new file mode 100644 index 0000000..14ed405 Binary files /dev/null and b/ccxt/pro/__pycache__/kucoinfutures.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/lbank.cpython-311.pyc b/ccxt/pro/__pycache__/lbank.cpython-311.pyc new file mode 100644 index 0000000..83060e8 Binary files /dev/null and b/ccxt/pro/__pycache__/lbank.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/luno.cpython-311.pyc b/ccxt/pro/__pycache__/luno.cpython-311.pyc new file mode 100644 index 0000000..7393aa9 Binary files /dev/null and b/ccxt/pro/__pycache__/luno.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/mexc.cpython-311.pyc b/ccxt/pro/__pycache__/mexc.cpython-311.pyc new file mode 100644 index 0000000..c4c7d60 Binary files /dev/null and b/ccxt/pro/__pycache__/mexc.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/modetrade.cpython-311.pyc b/ccxt/pro/__pycache__/modetrade.cpython-311.pyc new file mode 100644 index 0000000..c742554 Binary files /dev/null and b/ccxt/pro/__pycache__/modetrade.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/mt5.cpython-311.pyc b/ccxt/pro/__pycache__/mt5.cpython-311.pyc new file mode 100644 index 0000000..11da6c5 Binary files /dev/null and b/ccxt/pro/__pycache__/mt5.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/myokx.cpython-311.pyc b/ccxt/pro/__pycache__/myokx.cpython-311.pyc new file mode 100644 index 0000000..ded7ad6 Binary files /dev/null and b/ccxt/pro/__pycache__/myokx.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/ndax.cpython-311.pyc b/ccxt/pro/__pycache__/ndax.cpython-311.pyc new file mode 100644 index 0000000..6cc0357 Binary files /dev/null and b/ccxt/pro/__pycache__/ndax.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/okx.cpython-311.pyc b/ccxt/pro/__pycache__/okx.cpython-311.pyc new file mode 100644 index 0000000..a31d1ad Binary files /dev/null and b/ccxt/pro/__pycache__/okx.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/okxus.cpython-311.pyc b/ccxt/pro/__pycache__/okxus.cpython-311.pyc new file mode 100644 index 0000000..5124db3 Binary files /dev/null and b/ccxt/pro/__pycache__/okxus.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/onetrading.cpython-311.pyc b/ccxt/pro/__pycache__/onetrading.cpython-311.pyc new file mode 100644 index 0000000..2420d65 Binary files /dev/null and b/ccxt/pro/__pycache__/onetrading.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/oxfun.cpython-311.pyc b/ccxt/pro/__pycache__/oxfun.cpython-311.pyc new file mode 100644 index 0000000..468b5da Binary files /dev/null and b/ccxt/pro/__pycache__/oxfun.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/p2b.cpython-311.pyc b/ccxt/pro/__pycache__/p2b.cpython-311.pyc new file mode 100644 index 0000000..af356a9 Binary files /dev/null and b/ccxt/pro/__pycache__/p2b.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/paradex.cpython-311.pyc b/ccxt/pro/__pycache__/paradex.cpython-311.pyc new file mode 100644 index 0000000..9bb601f Binary files /dev/null and b/ccxt/pro/__pycache__/paradex.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/phemex.cpython-311.pyc b/ccxt/pro/__pycache__/phemex.cpython-311.pyc new file mode 100644 index 0000000..07ff2d6 Binary files /dev/null and b/ccxt/pro/__pycache__/phemex.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/poloniex.cpython-311.pyc b/ccxt/pro/__pycache__/poloniex.cpython-311.pyc new file mode 100644 index 0000000..4f7bc71 Binary files /dev/null and b/ccxt/pro/__pycache__/poloniex.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/probit.cpython-311.pyc b/ccxt/pro/__pycache__/probit.cpython-311.pyc new file mode 100644 index 0000000..c5ddc4a Binary files /dev/null and b/ccxt/pro/__pycache__/probit.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/toobit.cpython-311.pyc b/ccxt/pro/__pycache__/toobit.cpython-311.pyc new file mode 100644 index 0000000..e21323d Binary files /dev/null and b/ccxt/pro/__pycache__/toobit.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/upbit.cpython-311.pyc b/ccxt/pro/__pycache__/upbit.cpython-311.pyc new file mode 100644 index 0000000..8df3ae3 Binary files /dev/null and b/ccxt/pro/__pycache__/upbit.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/whitebit.cpython-311.pyc b/ccxt/pro/__pycache__/whitebit.cpython-311.pyc new file mode 100644 index 0000000..aaa2b53 Binary files /dev/null and b/ccxt/pro/__pycache__/whitebit.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/woo.cpython-311.pyc b/ccxt/pro/__pycache__/woo.cpython-311.pyc new file mode 100644 index 0000000..33cacd2 Binary files /dev/null and b/ccxt/pro/__pycache__/woo.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/woofipro.cpython-311.pyc b/ccxt/pro/__pycache__/woofipro.cpython-311.pyc new file mode 100644 index 0000000..c1b5843 Binary files /dev/null and b/ccxt/pro/__pycache__/woofipro.cpython-311.pyc differ diff --git a/ccxt/pro/__pycache__/xt.cpython-311.pyc b/ccxt/pro/__pycache__/xt.cpython-311.pyc new file mode 100644 index 0000000..18ce383 Binary files /dev/null and b/ccxt/pro/__pycache__/xt.cpython-311.pyc differ diff --git a/ccxt/pro/alpaca.py b/ccxt/pro/alpaca.py new file mode 100644 index 0000000..5c6e13f --- /dev/null +++ b/ccxt/pro/alpaca.py @@ -0,0 +1,716 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolById, ArrayCacheByTimestamp +from ccxt.base.types import Any, Bool, Int, Order, OrderBook, Str, Ticker, Trade +from ccxt.async_support.base.ws.client import Client +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError + + +class alpaca(ccxt.async_support.alpaca): + + def describe(self) -> Any: + return self.deep_extend(super(alpaca, self).describe(), { + 'has': { + 'ws': True, + 'createOrderWithTakeProfitAndStopLossWs': False, + 'createReduceOnlyOrderWs': False, + 'createStopLossOrderWs': False, + 'createTakeProfitOrderWs': False, + 'fetchPositionForSymbolWs': False, + 'fetchPositionsForSymbolWs': False, + 'fetchPositionsWs': False, + 'fetchPositionWs': False, + 'unWatchPositions': False, + 'watchBalance': False, + 'watchLiquidations': False, + 'watchLiquidationsForSymbols': False, + 'watchMarkPrice': False, + 'watchMarkPrices': False, + 'watchMyLiquidations': False, + 'watchMyLiquidationsForSymbols': False, + 'watchMyTrades': True, + 'watchOHLCV': True, + 'watchOrderBook': True, + 'watchOrders': True, + 'watchPosition': False, + 'watchPositions': False, + 'watchTicker': True, + 'watchTickers': False, # for now + 'watchTrades': True, + }, + 'urls': { + 'api': { + 'ws': { + 'crypto': 'wss://stream.data.alpaca.markets/v1beta2/crypto', + 'trading': 'wss://api.alpaca.markets/stream', + }, + }, + 'test': { + 'ws': { + 'crypto': 'wss://stream.data.alpaca.markets/v1beta2/crypto', + 'trading': 'wss://paper-api.alpaca.markets/stream', + }, + }, + }, + 'options': { + }, + 'streaming': {}, + 'exceptions': { + 'ws': { + 'exact': { + }, + }, + }, + }) + + async def watch_ticker(self, symbol: str, params={}) -> Ticker: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://docs.alpaca.markets/docs/real-time-crypto-pricing-data#quotes + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + url = self.urls['api']['ws']['crypto'] + await self.authenticate(url) + await self.load_markets() + market = self.market(symbol) + messageHash = 'ticker:' + market['symbol'] + request: dict = { + 'action': 'subscribe', + 'quotes': [market['id']], + } + return await self.watch(url, messageHash, self.extend(request, params), messageHash) + + def handle_ticker(self, client: Client, message): + # + # { + # "T": "q", + # "S": "BTC/USDT", + # "bp": 17394.44, + # "bs": 0.021981, + # "ap": 17397.99, + # "as": 0.02, + # "t": "2022-12-16T06:07:56.611063286Z" + # ] + # + ticker = self.parse_ticker(message) + symbol = ticker['symbol'] + messageHash = 'ticker:' + symbol + self.tickers[symbol] = ticker + client.resolve(self.tickers[symbol], messageHash) + + def parse_ticker(self, ticker, market=None) -> Ticker: + # + # { + # "T": "q", + # "S": "BTC/USDT", + # "bp": 17394.44, + # "bs": 0.021981, + # "ap": 17397.99, + # "as": 0.02, + # "t": "2022-12-16T06:07:56.611063286Z" + # } + # + marketId = self.safe_string(ticker, 'S') + datetime = self.safe_string(ticker, 't') + return self.safe_ticker({ + 'symbol': self.safe_symbol(marketId, market), + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + 'high': None, + 'low': None, + 'bid': self.safe_string(ticker, 'bp'), + 'bidVolume': self.safe_string(ticker, 'bs'), + 'ask': self.safe_string(ticker, 'ap'), + 'askVolume': self.safe_string(ticker, 'as'), + 'vwap': None, + 'open': None, + 'close': None, + 'last': None, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': None, + 'quoteVolume': None, + 'info': ticker, + }, market) + + async def watch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + watches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://docs.alpaca.markets/docs/real-time-crypto-pricing-data#bars + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + url = self.urls['api']['ws']['crypto'] + await self.authenticate(url) + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + request: dict = { + 'action': 'subscribe', + 'bars': [market['id']], + } + messageHash = 'ohlcv:' + symbol + ohlcv = await self.watch(url, messageHash, self.extend(request, params), messageHash) + if self.newUpdates: + limit = ohlcv.getLimit(symbol, limit) + return self.filter_by_since_limit(ohlcv, since, limit, 0, True) + + def handle_ohlcv(self, client: Client, message): + # + # { + # "T": "b", + # "S": "BTC/USDT", + # "o": 17416.39, + # "h": 17424.82, + # "l": 17416.39, + # "c": 17424.82, + # "v": 1.341054, + # "t": "2022-12-16T06:53:00Z", + # "n": 21, + # "vw": 17421.9529234915 + # } + # + marketId = self.safe_string(message, 'S') + symbol = self.safe_symbol(marketId) + stored = self.safe_value(self.ohlcvs, symbol) + if stored is None: + limit = self.safe_integer(self.options, 'OHLCVLimit', 1000) + stored = ArrayCacheByTimestamp(limit) + self.ohlcvs[symbol] = stored + parsed = self.parse_ohlcv(message) + stored.append(parsed) + messageHash = 'ohlcv:' + symbol + client.resolve(stored, messageHash) + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://docs.alpaca.markets/docs/real-time-crypto-pricing-data#orderbooks + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return. + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + url = self.urls['api']['ws']['crypto'] + await self.authenticate(url) + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + messageHash = 'orderbook' + ':' + symbol + request: dict = { + 'action': 'subscribe', + 'orderbooks': [market['id']], + } + orderbook = await self.watch(url, messageHash, self.extend(request, params), messageHash) + return orderbook.limit() + + def handle_order_book(self, client: Client, message): + # + # snapshot + # { + # "T": "o", + # "S": "BTC/USDT", + # "t": "2022-12-16T06:35:31.585113205Z", + # "b": [{ + # "p": 17394.37, + # "s": 0.015499, + # }, + # ... + # ], + # "a": [{ + # "p": 17398.8, + # "s": 0.042919, + # }, + # ... + # ], + # "r": True, + # } + # + marketId = self.safe_string(message, 'S') + symbol = self.safe_symbol(marketId) + datetime = self.safe_string(message, 't') + timestamp = self.parse8601(datetime) + isSnapshot = self.safe_bool(message, 'r', False) + if not (symbol in self.orderbooks): + self.orderbooks[symbol] = self.order_book() + orderbook = self.orderbooks[symbol] + if isSnapshot: + snapshot = self.parse_order_book(message, symbol, timestamp, 'b', 'a', 'p', 's') + orderbook.reset(snapshot) + else: + asks = self.safe_list(message, 'a', []) + bids = self.safe_list(message, 'b', []) + self.handle_deltas(orderbook['asks'], asks) + self.handle_deltas(orderbook['bids'], bids) + orderbook['timestamp'] = timestamp + orderbook['datetime'] = datetime + messageHash = 'orderbook' + ':' + symbol + self.orderbooks[symbol] = orderbook + client.resolve(orderbook, messageHash) + + def handle_delta(self, bookside, delta): + bidAsk = self.parse_bid_ask(delta, 'p', 's') + bookside.storeArray(bidAsk) + + def handle_deltas(self, bookside, deltas): + for i in range(0, len(deltas)): + self.handle_delta(bookside, deltas[i]) + + async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + watches information on multiple trades made in a market + + https://docs.alpaca.markets/docs/real-time-crypto-pricing-data#trades + + :param str symbol: unified market symbol of the market trades were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of trade structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + url = self.urls['api']['ws']['crypto'] + await self.authenticate(url) + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + messageHash = 'trade:' + symbol + request: dict = { + 'action': 'subscribe', + 'trades': [market['id']], + } + trades = await self.watch(url, messageHash, self.extend(request, params), messageHash) + if self.newUpdates: + limit = trades.getLimit(symbol, limit) + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + def handle_trades(self, client: Client, message): + # + # { + # "T": "t", + # "S": "BTC/USDT", + # "p": 17408.8, + # "s": 0.042919, + # "t": "2022-12-16T06:43:18.327Z", + # "i": 16585162, + # "tks": "B" + # ] + # + marketId = self.safe_string(message, 'S') + symbol = self.safe_symbol(marketId) + stored = self.safe_value(self.trades, symbol) + if stored is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + stored = ArrayCache(limit) + self.trades[symbol] = stored + parsed = self.parse_trade(message) + stored.append(parsed) + messageHash = 'trade' + ':' + symbol + client.resolve(stored, messageHash) + + async def watch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + watches information on multiple trades made by the user + + https://docs.alpaca.markets/docs/websocket-streaming#trade-updates + + :param str symbol: unified market symbol of the market trades were made in + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trade structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.unifiedMargin]: use unified margin account + :returns dict[]: a list of `trade structures ` + """ + url = self.urls['api']['ws']['trading'] + await self.authenticate(url) + messageHash = 'myTrades' + await self.load_markets() + if symbol is not None: + symbol = self.symbol(symbol) + messageHash += ':' + symbol + request: dict = { + 'action': 'listen', + 'data': { + 'streams': ['trade_updates'], + }, + } + trades = await self.watch(url, messageHash, self.extend(request, params), messageHash) + if self.newUpdates: + limit = trades.getLimit(symbol, limit) + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + watches information on multiple orders made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + url = self.urls['api']['ws']['trading'] + await self.authenticate(url) + await self.load_markets() + messageHash = 'orders' + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + messageHash = 'orders:' + symbol + request: dict = { + 'action': 'listen', + 'data': { + 'streams': ['trade_updates'], + }, + } + orders = await self.watch(url, messageHash, self.extend(request, params), messageHash) + if self.newUpdates: + limit = orders.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(orders, symbol, since, limit, True) + + def handle_trade_update(self, client: Client, message): + self.handle_order(client, message) + self.handle_my_trade(client, message) + + def handle_order(self, client: Client, message): + # + # { + # "stream": "trade_updates", + # "data": { + # "event": "new", + # "timestamp": "2022-12-16T07:28:51.67621869Z", + # "order": { + # "id": "c2470331-8993-4051-bf5d-428d5bdc9a48", + # "client_order_id": "0f1f3764-107a-4d09-8b9a-d75a11738f5c", + # "created_at": "2022-12-16T02:28:51.673531798-05:00", + # "updated_at": "2022-12-16T02:28:51.678736847-05:00", + # "submitted_at": "2022-12-16T02:28:51.673015558-05:00", + # "filled_at": null, + # "expired_at": null, + # "cancel_requested_at": null, + # "canceled_at": null, + # "failed_at": null, + # "replaced_at": null, + # "replaced_by": null, + # "replaces": null, + # "asset_id": "276e2673-764b-4ab6-a611-caf665ca6340", + # "symbol": "BTC/USD", + # "asset_class": "crypto", + # "notional": null, + # "qty": "0.01", + # "filled_qty": "0", + # "filled_avg_price": null, + # "order_class": '', + # "order_type": "market", + # "type": "market", + # "side": "buy", + # "time_in_force": "gtc", + # "limit_price": null, + # "stop_price": null, + # "status": "new", + # "extended_hours": False, + # "legs": null, + # "trail_percent": null, + # "trail_price": null, + # "hwm": null + # }, + # "execution_id": "5f781a30-b9a3-4c86-b466-2175850cf340" + # } + # } + # + data = self.safe_value(message, 'data', {}) + rawOrder = self.safe_value(data, 'order', {}) + if self.orders is None: + limit = self.safe_integer(self.options, 'ordersLimit', 1000) + self.orders = ArrayCacheBySymbolById(limit) + orders = self.orders + order = self.parse_order(rawOrder) + orders.append(order) + messageHash = 'orders' + client.resolve(orders, messageHash) + messageHash = 'orders:' + order['symbol'] + client.resolve(orders, messageHash) + + def handle_my_trade(self, client: Client, message): + # + # { + # "stream": "trade_updates", + # "data": { + # "event": "new", + # "timestamp": "2022-12-16T07:28:51.67621869Z", + # "order": { + # "id": "c2470331-8993-4051-bf5d-428d5bdc9a48", + # "client_order_id": "0f1f3764-107a-4d09-8b9a-d75a11738f5c", + # "created_at": "2022-12-16T02:28:51.673531798-05:00", + # "updated_at": "2022-12-16T02:28:51.678736847-05:00", + # "submitted_at": "2022-12-16T02:28:51.673015558-05:00", + # "filled_at": null, + # "expired_at": null, + # "cancel_requested_at": null, + # "canceled_at": null, + # "failed_at": null, + # "replaced_at": null, + # "replaced_by": null, + # "replaces": null, + # "asset_id": "276e2673-764b-4ab6-a611-caf665ca6340", + # "symbol": "BTC/USD", + # "asset_class": "crypto", + # "notional": null, + # "qty": "0.01", + # "filled_qty": "0", + # "filled_avg_price": null, + # "order_class": '', + # "order_type": "market", + # "type": "market", + # "side": "buy", + # "time_in_force": "gtc", + # "limit_price": null, + # "stop_price": null, + # "status": "new", + # "extended_hours": False, + # "legs": null, + # "trail_percent": null, + # "trail_price": null, + # "hwm": null + # }, + # "execution_id": "5f781a30-b9a3-4c86-b466-2175850cf340" + # } + # } + # + data = self.safe_value(message, 'data', {}) + event = self.safe_string(data, 'event') + if event != 'fill' and event != 'partial_fill': + return + rawOrder = self.safe_value(data, 'order', {}) + myTrades = self.myTrades + if myTrades is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + myTrades = ArrayCacheBySymbolById(limit) + trade = self.parse_my_trade(rawOrder) + myTrades.append(trade) + messageHash = 'myTrades:' + trade['symbol'] + client.resolve(myTrades, messageHash) + messageHash = 'myTrades' + client.resolve(myTrades, messageHash) + + def parse_my_trade(self, trade, market=None): + # + # { + # "id": "c2470331-8993-4051-bf5d-428d5bdc9a48", + # "client_order_id": "0f1f3764-107a-4d09-8b9a-d75a11738f5c", + # "created_at": "2022-12-16T02:28:51.673531798-05:00", + # "updated_at": "2022-12-16T02:28:51.678736847-05:00", + # "submitted_at": "2022-12-16T02:28:51.673015558-05:00", + # "filled_at": null, + # "expired_at": null, + # "cancel_requested_at": null, + # "canceled_at": null, + # "failed_at": null, + # "replaced_at": null, + # "replaced_by": null, + # "replaces": null, + # "asset_id": "276e2673-764b-4ab6-a611-caf665ca6340", + # "symbol": "BTC/USD", + # "asset_class": "crypto", + # "notional": null, + # "qty": "0.01", + # "filled_qty": "0", + # "filled_avg_price": null, + # "order_class": '', + # "order_type": "market", + # "type": "market", + # "side": "buy", + # "time_in_force": "gtc", + # "limit_price": null, + # "stop_price": null, + # "status": "new", + # "extended_hours": False, + # "legs": null, + # "trail_percent": null, + # "trail_price": null, + # "hwm": null + # } + # + marketId = self.safe_string(trade, 'symbol') + datetime = self.safe_string(trade, 'filled_at') + type = self.safe_string(trade, 'type') + if type.find('limit') >= 0: + # might be limit or stop-limit + type = 'limit' + return self.safe_trade({ + 'id': self.safe_string(trade, 'i'), + 'info': trade, + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + 'symbol': self.safe_symbol(marketId, None, '/'), + 'order': self.safe_string(trade, 'id'), + 'type': type, + 'side': self.safe_string(trade, 'side'), + 'takerOrMaker': 'taker' if (type == 'market') else 'maker', + 'price': self.safe_string(trade, 'filled_avg_price'), + 'amount': self.safe_string(trade, 'filled_qty'), + 'cost': None, + 'fee': None, + }, market) + + async def authenticate(self, url, params={}): + self.check_required_credentials() + messageHash = 'authenticated' + client = self.client(url) + future = client.reusableFuture(messageHash) + authenticated = self.safe_value(client.subscriptions, messageHash) + if authenticated is None: + request = { + 'action': 'auth', + 'key': self.apiKey, + 'secret': self.secret, + } + if url == self.urls['api']['ws']['trading']: + # self auth request is being deprecated in test environment + request = { + 'action': 'authenticate', + 'data': { + 'key_id': self.apiKey, + 'secret_key': self.secret, + }, + } + self.watch(url, messageHash, request, messageHash, future) + return await future + + def handle_error_message(self, client: Client, message) -> Bool: + # + # { + # "T": "error", + # "code": 400, + # "msg": "invalid syntax" + # } + # + code = self.safe_string(message, 'code') + msg = self.safe_value(message, 'msg', {}) + raise ExchangeError(self.id + ' code: ' + code + ' message: ' + msg) + + def handle_connected(self, client: Client, message): + # + # { + # "T": "success", + # "msg": "connected" + # } + # + return message + + def handle_crypto_message(self, client: Client, message): + for i in range(0, len(message)): + data = message[i] + T = self.safe_string(data, 'T') + msg = self.safe_string(data, 'msg') + if T == 'subscription': + self.handle_subscription(client, data) + return + if T == 'success' and msg == 'connected': + self.handle_connected(client, data) + return + if T == 'success' and msg == 'authenticated': + self.handle_authenticate(client, data) + return + methods: dict = { + 'error': self.handle_error_message, + 'b': self.handle_ohlcv, + 'q': self.handle_ticker, + 't': self.handle_trades, + 'o': self.handle_order_book, + } + method = self.safe_value(methods, T) + if method is not None: + method(client, data) + + def handle_trading_message(self, client: Client, message): + stream = self.safe_string(message, 'stream') + methods: dict = { + 'authorization': self.handle_authenticate, + 'listening': self.handle_subscription, + 'trade_updates': self.handle_trade_update, + } + method = self.safe_value(methods, stream) + if method is not None: + method(client, message) + + def handle_message(self, client: Client, message): + if isinstance(message, list): + self.handle_crypto_message(client, message) + return + self.handle_trading_message(client, message) + + def handle_authenticate(self, client: Client, message): + # + # crypto + # { + # "T": "success", + # "msg": "connected" + # ] + # + # trading + # { + # "stream": "authorization", + # "data": { + # "status": "authorized", + # "action": "authenticate" + # } + # } + # error + # { + # "stream": "authorization", + # "data": { + # "action": "authenticate", + # "message": "access key verification failed", + # "status": "unauthorized" + # } + # } + # + T = self.safe_string(message, 'T') + data = self.safe_value(message, 'data', {}) + status = self.safe_string(data, 'status') + if T == 'success' or status == 'authorized': + promise = client.futures['authenticated'] + promise.resolve(message) + return + raise AuthenticationError(self.id + ' failed to authenticate.') + + def handle_subscription(self, client: Client, message): + # + # crypto + # { + # "T": "subscription", + # "trades": [], + # "quotes": ["BTC/USDT"], + # "orderbooks": [], + # "bars": [], + # "updatedBars": [], + # "dailyBars": [] + # } + # trading + # { + # "stream": "listening", + # "data": { + # "streams": ["trade_updates"] + # } + # } + # + return message diff --git a/ccxt/pro/apex.py b/ccxt/pro/apex.py new file mode 100644 index 0000000..d1ed7a6 --- /dev/null +++ b/ccxt/pro/apex.py @@ -0,0 +1,1004 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolById, ArrayCacheBySymbolBySide, ArrayCacheByTimestamp +import asyncio +import hashlib +import json +from ccxt.base.types import Any, Bool, Int, Order, OrderBook, Position, Str, Strings, Ticker, Tickers, Trade +from ccxt.async_support.base.ws.client import Client +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import NetworkError + + +class apex(ccxt.async_support.apex): + + def describe(self) -> Any: + return self.deep_extend(super(apex, self).describe(), { + 'has': { + 'ws': True, + 'watchTicker': True, + 'watchTickers': True, + 'watchOrderBook': True, + 'watchOrders': True, + 'watchTrades': True, + 'watchTradesForSymbols': False, + 'watchPositions': True, + 'watchMyTrades': True, + 'watchBalance': False, + 'watchOHLCV': True, + }, + 'urls': { + 'logo': 'https://omni.apex.exchange/assets/logo_content-CY9uyFbz.svg', + 'api': { + 'ws': { + 'public': 'wss://quote.omni.apex.exchange/realtime_public?v=2', + 'private': 'wss://quote.omni.apex.exchange/realtime_private?v=2', + }, + }, + 'test': { + 'ws': { + 'public': 'wss://qa-quote.omni.apex.exchange/realtime_public?v=2', + 'private': 'wss://qa-quote.omni.apex.exchange/realtime_private?v=2', + }, + }, + 'www': 'https://apex.exchange/', + 'doc': 'https://api-docs.pro.apex.exchange', + 'fees': 'https://apex-pro.gitbook.io/apex-pro/apex-omni-live-now/trading-perpetual-contracts/trading-fees', + 'referral': 'https://omni.apex.exchange/trade', + }, + 'options': {}, + 'streaming': { + 'ping': self.ping, + 'keepAlive': 18000, + }, + }) + + async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + watches information on multiple trades made in a market + + https://api-docs.pro.apex.exchange/#websocket-v3-for-omni-websocket-endpoint + + :param str symbol: unified market symbol of the market trades were made in + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trade structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + return await self.watch_trades_for_symbols([symbol], since, limit, params) + + async def watch_trades_for_symbols(self, symbols: List[str], since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a list of symbols + + https://api-docs.pro.apex.exchange/#websocket-v3-for-omni-websocket-endpoint + + :param str[] symbols: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + symbolsLength = len(symbols) + if symbolsLength == 0: + raise ArgumentsRequired(self.id + ' watchTradesForSymbols() requires a non-empty array of symbols') + timeStamp = str(self.milliseconds()) + url = self.urls['api']['ws']['public'] + '×tamp=' + timeStamp + topics = [] + messageHashes = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + market = self.market(symbol) + topic = 'recentlyTrade.H.' + market['id2'] + topics.append(topic) + messageHash = 'trade:' + symbol + messageHashes.append(messageHash) + trades = await self.watch_topics(url, messageHashes, topics, params) + if self.newUpdates: + first = self.safe_value(trades, 0) + tradeSymbol = self.safe_string(first, 'symbol') + limit = trades.getLimit(tradeSymbol, limit) + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + def handle_trades(self, client: Client, message): + # + # { + # "topic": "recentlyTrade.H.BTCUSDT", + # "type": "snapshot", + # "ts": 1672304486868, + # "data": [ + # { + # "T": 1672304486865, + # "s": "BTCUSDT", + # "S": "Buy", + # "v": "0.001", + # "p": "16578.50", + # "L": "PlusTick", + # "i": "20f43950-d8dd-5b31-9112-a178eb6023ef", + # "BT": False + # }, + # # sorted by newest first + # ] + # } + # + data = self.safe_value(message, 'data', {}) + topic = self.safe_string(message, 'topic') + trades = data + parts = topic.split('.') + marketId = self.safe_string(parts, 2) + market = self.safe_market(marketId, None, None) + symbol = market['symbol'] + stored = self.safe_value(self.trades, symbol) + if stored is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + stored = ArrayCache(limit) + self.trades[symbol] = stored + length = len(trades) + for j in range(0, length): + index = length - j - 1 + parsed = self.parse_ws_trade(trades[index], market) + stored.append(parsed) + messageHash = 'trade' + ':' + symbol + client.resolve(stored, messageHash) + + def parse_ws_trade(self, trade, market=None): + # + # public + # { + # "T": 1672304486865, + # "s": "BTCUSDT", + # "S": "Buy", + # "v": "0.001", + # "p": "16578.50", + # "L": "PlusTick", + # "i": "20f43950-d8dd-5b31-9112-a178eb6023af", + # "BT": False + # } + # + id = self.safe_string_n(trade, ['i', 'id', 'v']) + marketId = self.safe_string_n(trade, ['s', 'symbol']) + market = self.safe_market(marketId, market, None) + symbol = market['symbol'] + timestamp = self.safe_integer_n(trade, ['t', 'T', 'createdAt']) + side = self.safe_string_lower_n(trade, ['S', 'side']) + price = self.safe_string_n(trade, ['p', 'price']) + amount = self.safe_string_n(trade, ['q', 'v', 'size']) + return self.safe_trade({ + 'id': id, + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'order': None, + 'type': None, + 'side': side, + 'takerOrMaker': None, + 'price': price, + 'amount': amount, + 'cost': None, + 'fee': None, + }, market) + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://api-docs.pro.apex.exchange/#websocket-v3-for-omni-websocket-endpoint + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return. + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + return await self.watch_order_book_for_symbols([symbol], limit, params) + + async def watch_order_book_for_symbols(self, symbols: List[str], limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://api-docs.pro.apex.exchange/#websocket-v3-for-omni-websocket-endpoint + + :param str[] symbols: unified array of symbols + :param int [limit]: the maximum amount of order book entries to return. + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + symbolsLength = len(symbols) + if symbolsLength == 0: + raise ArgumentsRequired(self.id + ' watchOrderBookForSymbols() requires a non-empty array of symbols') + symbols = self.market_symbols(symbols) + timeStamp = str(self.milliseconds()) + url = self.urls['api']['ws']['public'] + '×tamp=' + timeStamp + topics = [] + messageHashes = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + market = self.market(symbol) + if limit is None: + limit = 25 + topic = 'orderBook' + str(limit) + '.H.' + market['id2'] + topics.append(topic) + messageHash = 'orderbook:' + symbol + messageHashes.append(messageHash) + orderbook = await self.watch_topics(url, messageHashes, topics, params) + return orderbook.limit() + + async def watch_topics(self, url, messageHashes, topics, params={}): + request: dict = { + 'op': 'subscribe', + 'args': topics, + } + message = self.extend(request, params) + return await self.watch_multiple(url, messageHashes, message, messageHashes) + + def handle_order_book(self, client: Client, message): + # + # { + # "topic": "orderbook25.H.BTCUSDT", + # "type": "snapshot", + # "ts": 1672304484978, + # "data": { + # "s": "BTCUSDT", + # "b": [ + # ..., + # [ + # "16493.50", + # "0.006" + # ], + # [ + # "16493.00", + # "0.100" + # ] + # ], + # "a": [ + # [ + # "16611.00", + # "0.029" + # ], + # [ + # "16612.00", + # "0.213" + # ], + # ], + # "u": 18521288, + # "seq": 7961638724 + # } + # } + # + type = self.safe_string(message, 'type') + isSnapshot = (type == 'snapshot') + data = self.safe_dict(message, 'data', {}) + marketId = self.safe_string(data, 's') + market = self.safe_market(marketId, None, None) + symbol = market['symbol'] + timestamp = self.safe_integer_product(message, 'ts', 0.001) + if not (symbol in self.orderbooks): + self.orderbooks[symbol] = self.order_book() + orderbook = self.orderbooks[symbol] + if isSnapshot: + snapshot = self.parse_order_book(data, symbol, timestamp, 'b', 'a') + orderbook.reset(snapshot) + else: + asks = self.safe_list(data, 'a', []) + bids = self.safe_list(data, 'b', []) + self.handle_deltas(orderbook['asks'], asks) + self.handle_deltas(orderbook['bids'], bids) + orderbook['timestamp'] = timestamp + orderbook['datetime'] = self.iso8601(timestamp) + messageHash = 'orderbook' + ':' + symbol + self.orderbooks[symbol] = orderbook + client.resolve(orderbook, messageHash) + + def handle_delta(self, bookside, delta): + bidAsk = self.parse_bid_ask(delta, 0, 1) + bookside.storeArray(bidAsk) + + def handle_deltas(self, bookside, deltas): + for i in range(0, len(deltas)): + self.handle_delta(bookside, deltas[i]) + + async def watch_ticker(self, symbol: str, params={}) -> Ticker: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://api-docs.pro.apex.exchange/#websocket-v3-for-omni-websocket-endpoint + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + timeStamp = str(self.milliseconds()) + url = self.urls['api']['ws']['public'] + '×tamp=' + timeStamp + messageHash = 'ticker:' + symbol + topic = 'instrumentInfo' + '.H.' + market['id2'] + topics = [topic] + return await self.watch_topics(url, [messageHash], topics, params) + + async def watch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list + + https://api-docs.pro.apex.exchange/#websocket-v3-for-omni-websocket-endpoint + + :param str[] symbols: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, False) + messageHashes = [] + timeStamp = str(self.milliseconds()) + url = self.urls['api']['ws']['public'] + '×tamp=' + timeStamp + topics = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + market = self.market(symbol) + topic = 'instrumentInfo' + '.H.' + market['id2'] + topics.append(topic) + messageHash = 'ticker:' + symbol + messageHashes.append(messageHash) + ticker = await self.watch_topics(url, messageHashes, topics, params) + if self.newUpdates: + result: dict = {} + result[ticker['symbol']] = ticker + return result + return self.filter_by_array(self.tickers, 'symbol', symbols) + + def handle_ticker(self, client: Client, message): + # "topic":"instrumentInfo.H.BTCUSDT", + # "type":"snapshot", + # "data":{ + # "symbol":"BTCUSDT", + # "lastPrice":"21572.5", + # "price24hPcnt":"-0.0194318181818182", + # "highPrice24h":"25306.5", + # "lowPrice24h":"17001.5", + # "turnover24h":"1334891.4545", + # "volume24h":"64.896", + # "nextFundingTime":"2022-08-26T08:00:00Z", + # "oraclePrice":"21412.060000000002752512", + # "indexPrice":"21409.82", + # "openInterest":"49.598", + # "tradeCount":"0", + # "fundingRate":"0.0000125", + # "predictedFundingRate":"0.0000125" + # }, + # "cs":44939063, + # "ts":1661500091955487 + # } + topic = self.safe_string(message, 'topic', '') + updateType = self.safe_string(message, 'type', '') + data = self.safe_dict(message, 'data', {}) + symbol = None + parsed = None + if (updateType == 'snapshot'): + parsed = self.parse_ticker(data) + symbol = parsed['symbol'] + elif updateType == 'delta': + topicParts = topic.split('.') + topicLength = len(topicParts) + marketId = self.safe_string(topicParts, topicLength - 1) + market = self.safe_market(marketId, None, None) + symbol = market['symbol'] + ticker = self.safe_dict(self.tickers, symbol, {}) + rawTicker = self.safe_dict(ticker, 'info', {}) + merged = self.extend(rawTicker, data) + parsed = self.parse_ticker(merged) + timestamp = self.safe_integer_product(message, 'ts', 0.001) + parsed['timestamp'] = timestamp + parsed['datetime'] = self.iso8601(timestamp) + self.tickers[symbol] = parsed + messageHash = 'ticker:' + symbol + client.resolve(self.tickers[symbol], messageHash) + + async def watch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + watches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://api-docs.pro.apex.exchange/#websocket-v3-for-omni-websocket-endpoint + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + params['callerMethodName'] = 'watchOHLCV' + result = await self.watch_ohlcv_for_symbols([[symbol, timeframe]], since, limit, params) + return result[symbol][timeframe] + + async def watch_ohlcv_for_symbols(self, symbolsAndTimeframes: List[List[str]], since: Int = None, limit: Int = None, params={}): + """ + watches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://api-docs.pro.apex.exchange/#websocket-v3-for-omni-websocket-endpoint + + :param str[][] symbolsAndTimeframes: array of arrays containing unified symbols and timeframes to fetch OHLCV data for, example [['BTC/USDT', '1m'], ['LTC/USDT', '5m']] + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + timeStamp = str(self.milliseconds()) + url = self.urls['api']['ws']['public'] + '×tamp=' + timeStamp + rawHashes = [] + messageHashes = [] + for i in range(0, len(symbolsAndTimeframes)): + data = symbolsAndTimeframes[i] + symbolString = self.safe_string(data, 0) + market = self.market(symbolString) + symbolString = market['id2'] + unfiedTimeframe = self.safe_string(data, 1, '1') + timeframeId = self.safe_string(self.timeframes, unfiedTimeframe, unfiedTimeframe) + rawHashes.append('candle.' + timeframeId + '.' + symbolString) + messageHashes.append('ohlcv::' + symbolString + '::' + unfiedTimeframe) + symbol, timeframe, stored = await self.watch_topics(url, messageHashes, rawHashes, params) + if self.newUpdates: + limit = stored.getLimit(symbol, limit) + filtered = self.filter_by_since_limit(stored, since, limit, 0, True) + return self.create_ohlcv_object(symbol, timeframe, filtered) + + def handle_ohlcv(self, client: Client, message): + # + # { + # "topic": "candle.5.BTCUSDT", + # "data": [ + # { + # "start": 1672324800000, + # "end": 1672325099999, + # "interval": "5", + # "open": "16649.5", + # "close": "16677", + # "high": "16677", + # "low": "16608", + # "volume": "2.081", + # "turnover": "34666.4005", + # "confirm": False, + # "timestamp": 1672324988882 + # } + # ], + # "ts": 1672324988882, + # "type": "snapshot" + # } + # + data = self.safe_value(message, 'data', {}) + topic = self.safe_string(message, 'topic') + topicParts = topic.split('.') + topicLength = len(topicParts) + timeframeId = self.safe_string(topicParts, 1) + timeframe = self.find_timeframe(timeframeId) + marketId = self.safe_string(topicParts, topicLength - 1) + isSpot = client.url.find('spot') > -1 + marketType = 'spot' if isSpot else 'contract' + market = self.safe_market(marketId, None, None, marketType) + symbol = market['symbol'] + ohlcvsByTimeframe = self.safe_value(self.ohlcvs, symbol) + if ohlcvsByTimeframe is None: + self.ohlcvs[symbol] = {} + if self.safe_value(ohlcvsByTimeframe, timeframe) is None: + limit = self.safe_integer(self.options, 'OHLCVLimit', 1000) + self.ohlcvs[symbol][timeframe] = ArrayCacheByTimestamp(limit) + stored = self.ohlcvs[symbol][timeframe] + for i in range(0, len(data)): + parsed = self.parse_ws_ohlcv(data[i]) + stored.append(parsed) + messageHash = 'ohlcv::' + symbol + '::' + timeframe + resolveData = [symbol, timeframe, stored] + client.resolve(resolveData, messageHash) + + def parse_ws_ohlcv(self, ohlcv, market=None) -> list: + # + # { + # "start": 1670363160000, + # "end": 1670363219999, + # "interval": "1", + # "open": "16987.5", + # "close": "16987.5", + # "high": "16988", + # "low": "16987.5", + # "volume": "23.511", + # "turnover": "399396.344", + # "confirm": False, + # "timestamp": 1670363219614 + # } + # + return [ + self.safe_integer(ohlcv, 'start'), + self.safe_number(ohlcv, 'open'), + self.safe_number(ohlcv, 'high'), + self.safe_number(ohlcv, 'low'), + self.safe_number(ohlcv, 'close'), + self.safe_number_2(ohlcv, 'volume', 'turnover'), + ] + + async def watch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + watches information on multiple trades made by the user + + https://api-docs.pro.apex.exchange/#private-websocket + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.unifiedMargin]: use unified margin account + :returns dict[]: a list of `order structures ` + """ + messageHash = 'myTrades' + await self.load_markets() + if symbol is not None: + symbol = self.symbol(symbol) + messageHash += ':' + symbol + timeStamp = str(self.milliseconds()) + url = self.urls['api']['ws']['private'] + '×tamp=' + timeStamp + await self.authenticate(url) + trades = await self.watch_topics(url, [messageHash], ['myTrades'], params) + if self.newUpdates: + limit = trades.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(trades, symbol, since, limit, True) + + async def watch_positions(self, symbols: Strings = None, since: Int = None, limit: Int = None, params={}) -> List[Position]: + """ + + https://api-docs.pro.apex.exchange/#private-websocket + + watch all open positions + :param str[] [symbols]: list of unified market symbols + :param int [since]: the earliest time in ms to fetch positions for + :param int [limit]: the maximum number of positions to retrieve + :param dict params: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + await self.load_markets() + messageHash = '' + if not self.is_empty(symbols): + symbols = self.market_symbols(symbols) + messageHash = '::' + ','.join(symbols) + timeStamp = str(self.milliseconds()) + url = self.urls['api']['ws']['private'] + '×tamp=' + timeStamp + messageHash = 'positions' + messageHash + client = self.client(url) + await self.authenticate(url) + self.set_positions_cache(client, symbols) + cache = self.positions + if cache is None: + snapshot = await client.future('fetchPositionsSnapshot') + return self.filter_by_symbols_since_limit(snapshot, symbols, since, limit, True) + topics = ['positions'] + newPositions = await self.watch_topics(url, [messageHash], topics, params) + if self.newUpdates: + return newPositions + return self.filter_by_symbols_since_limit(cache, symbols, since, limit, True) + + async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + watches information on multiple orders made by the user + + https://api-docs.pro.apex.exchange/#private-websocket + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + messageHash = 'orders' + if symbol is not None: + symbol = self.symbol(symbol) + messageHash += ':' + symbol + timeStamp = str(self.milliseconds()) + url = self.urls['api']['ws']['private'] + '×tamp=' + timeStamp + await self.authenticate(url) + topics = ['orders'] + orders = await self.watch_topics(url, [messageHash], topics, params) + if self.newUpdates: + limit = orders.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(orders, symbol, since, limit, True) + + def handle_my_trades(self, client: Client, lists): + # [ + # { + # "symbol":"ETH-USDT", + # "side":"BUY", + # "orderId":"2048046080", + # "fee":"0.625000", + # "liquidity":"TAKER", + # "accountId":"1024000", + # "createdAt":1652185521361, + # "isOpen":true, + # "size":"0.500", + # "price":"2500.0", + # "quoteAmount":"1250.0000", + # "id":"2048000182272", + # "updatedAt":1652185678345 + # } + # ] + if self.myTrades is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + self.myTrades = ArrayCacheBySymbolById(limit) + trades = self.myTrades + symbols: dict = {} + for i in range(0, len(lists)): + rawTrade = lists[i] + parsed = None + parsed = self.parse_ws_trade(rawTrade) + symbol = parsed['symbol'] + symbols[symbol] = True + trades.append(parsed) + keys = list(symbols.keys()) + for i in range(0, len(keys)): + currentMessageHash = 'myTrades:' + keys[i] + client.resolve(trades, currentMessageHash) + # non-symbol specific + messageHash = 'myTrades' + client.resolve(trades, messageHash) + + def handle_order(self, client: Client, lists): + # [ + # { + # "symbol":"ETH-USDT", + # "cumSuccessFillFee":"0.625000", + # "trailingPercent":"0", + # "type":"LIMIT", + # "unfillableAt":1654779600000, + # "isDeleverage":false, + # "createdAt":1652185521339, + # "price":"2500.0", + # "cumSuccessFillValue":"0", + # "id":"2048046080", + # "cancelReason":"", + # "timeInForce":1, + # "updatedAt":1652185521392, + # "limitFee":"0.625000", + # "side":"BUY", + # "clientOrderId":"522843990", + # "triggerPrice":"", + # "expiresAt":1654779600000, + # "cumSuccessFillSize":"0", + # "accountId":"1024000", + # "size":"0.500", + # "reduceOnly":false, + # "isLiquidate":false, + # "remainingSize":"0.000", + # "status":"PENDING" + # } + # ] + if self.orders is None: + limit = self.safe_integer(self.options, 'ordersLimit', 1000) + self.orders = ArrayCacheBySymbolById(limit) + orders = self.orders + symbols: dict = {} + for i in range(0, len(lists)): + parsed = None + parsed = self.parse_order(lists[i]) + symbol = parsed['symbol'] + symbols[symbol] = True + orders.append(parsed) + symbolsArray = list(symbols.keys()) + for i in range(0, len(symbolsArray)): + currentMessageHash = 'orders:' + symbolsArray[i] + client.resolve(orders, currentMessageHash) + messageHash = 'orders' + client.resolve(orders, messageHash) + + def set_positions_cache(self, client: Client, symbols: Strings = None): + if self.positions is not None: + return + messageHash = 'fetchPositionsSnapshot' + if not (messageHash in client.futures): + client.future(messageHash) + self.spawn(self.load_positions_snapshot, client, messageHash) + + async def load_positions_snapshot(self, client, messageHash): + # one ws channel gives positions for all types, for snapshot must load all positions + fetchFunctions = [ + self.fetch_positions(None), + ] + promises = await asyncio.gather(*fetchFunctions) + self.positions = ArrayCacheBySymbolBySide() + cache = self.positions + for i in range(0, len(promises)): + positions = promises[i] + for ii in range(0, len(positions)): + position = positions[ii] + cache.append(position) + # don't remove the future from the .futures cache + future = client.futures[messageHash] + future.resolve(cache) + client.resolve(cache, 'positions') + + def handle_positions(self, client, lists): + # + # [ + # { + # "symbol":"ETH-USDT", + # "exitPrice":"0", + # "side":"LONG", + # "maxSize":"2820.000", + # "sumOpen":"1.820", + # "sumClose":"0.000", + # "netFunding":"0.000000", + # "entryPrice":"2500.000000000000000000", + # "accountId":"1024000", + # "createdAt":1652179377769, + # "size":"1.820", + # "realizedPnl":"0", + # "closedAt":1652185521392, + # "updatedAt":1652185521392 + # } + # ] + # + # each account is connected to a different endpoint + # and has exactly one subscriptionhash which is the account type + if self.positions is None: + self.positions = ArrayCacheBySymbolBySide() + cache = self.positions + newPositions = [] + for i in range(0, len(lists)): + rawPosition = lists[i] + position = self.parse_position(rawPosition) + side = self.safe_string(position, 'side') + # hacky solution to handle closing positions + # without crashing, we should handle self properly later + newPositions.append(position) + if side is None or side == '': + # closing update, adding both sides to "reset" both sides + # since we don't know which side is being closed + position['side'] = 'long' + cache.append(position) + position['side'] = 'short' + cache.append(position) + position['side'] = None + else: + # regular update + cache.append(position) + messageHashes = self.find_message_hashes(client, 'positions::') + for i in range(0, len(messageHashes)): + messageHash = messageHashes[i] + parts = messageHash.split('::') + symbolsString = parts[1] + symbols = symbolsString.split(',') + positions = self.filter_by_array(newPositions, 'symbol', symbols, False) + if not self.is_empty(positions): + client.resolve(positions, messageHash) + client.resolve(newPositions, 'positions') + + async def authenticate(self, url, params={}): + self.check_required_credentials() + timestamp = str(self.milliseconds()) + request_path = '/ws/accounts' + http_method = 'GET' + messageString = (timestamp + http_method + request_path) + signature = self.hmac(self.encode(messageString), self.encode(self.string_to_base64(self.secret)), hashlib.sha256, 'base64') + messageHash = 'authenticated' + client = self.client(url) + future = client.reusableFuture(messageHash) + authenticated = self.safe_value(client.subscriptions, messageHash) + if authenticated is None: + # auth sign + request = { + 'type': 'login', + 'topics': ['ws_zk_accounts_v3'], + 'httpMethod': http_method, + 'requestPath': request_path, + 'apiKey': self.apiKey, + 'passphrase': self.password, + 'timestamp': timestamp, + 'signature': signature, + } + message = { + 'op': 'login', + 'args': [json.dumps(request)], + } + self.watch(url, messageHash, message, messageHash) + return await future + + def handle_error_message(self, client: Client, message) -> Bool: + # + # { + # "success": False, + # "ret_msg": "error:invalid op", + # "conn_id": "5e079fdd-9c7f-404d-9dbf-969d650838b5", + # "request": {op: '', args: null} + # } + # + # auth error + # + # { + # "success": False, + # "ret_msg": "error:USVC1111", + # "conn_id": "e73770fb-a0dc-45bd-8028-140e20958090", + # "request": { + # "op": "auth", + # "args": [ + # "9rFT6uR4uz9Imkw4Wx", + # "1653405853543", + # "542e71bd85597b4db0290f0ce2d13ed1fd4bb5df3188716c1e9cc69a879f7889" + # ] + # } + # + # {code: '-10009', desc: "Invalid period!"} + # + # { + # "reqId":"1", + # "retCode":170131, + # "retMsg":"Insufficient balance.", + # "op":"order.create", + # "data":{ + # + # }, + # "header":{ + # "X-Bapi-Limit":"20", + # "X-Bapi-Limit-Status":"19", + # "X-Bapi-Limit-Reset-Timestamp":"1714236608944", + # "Traceid":"3d7168a137bf32a947b7e5e6a575ac7f", + # "Timenow":"1714236608946" + # }, + # "connId":"cojifin88smerbj9t560-406" + # } + # + code = self.safe_string_n(message, ['code', 'ret_code', 'retCode']) + try: + if code is not None and code != '0': + feedback = self.id + ' ' + self.json(message) + self.throw_exactly_matched_exception(self.exceptions['exact'], code, feedback) + msg = self.safe_string_2(message, 'retMsg', 'ret_msg') + self.throw_broadly_matched_exception(self.exceptions['broad'], msg, feedback) + raise ExchangeError(feedback) + success = self.safe_value(message, 'success') + if success is not None and not success: + ret_msg = self.safe_string(message, 'ret_msg') + request = self.safe_value(message, 'request', {}) + op = self.safe_string(request, 'op') + if op == 'auth': + raise AuthenticationError('Authentication failed: ' + ret_msg) + else: + raise ExchangeError(self.id + ' ' + ret_msg) + return False + except Exception as error: + if isinstance(error, AuthenticationError): + messageHash = 'authenticated' + client.reject(error, messageHash) + if messageHash in client.subscriptions: + del client.subscriptions[messageHash] + else: + messageHash = self.safe_string(message, 'reqId') + client.reject(error, messageHash) + return True + + def handle_message(self, client: Client, message): + if self.handle_error_message(client, message): + return + topic = self.safe_string_2(message, 'topic', 'op', '') + methods: dict = { + 'ws_zk_accounts_v3': self.handle_account, + 'orderBook': self.handle_order_book, + 'depth': self.handle_order_book, + 'candle': self.handle_ohlcv, + 'kline': self.handle_ohlcv, + 'ticker': self.handle_ticker, + 'instrumentInfo': self.handle_ticker, + 'trade': self.handle_trades, + 'recentlyTrade': self.handle_trades, + 'pong': self.handle_pong, + 'auth': self.handle_authenticate, + 'ping': self.handle_ping, + } + exacMethod = self.safe_value(methods, topic) + if exacMethod is not None: + exacMethod(client, message) + return + keys = list(methods.keys()) + for i in range(0, len(keys)): + key = keys[i] + if topic.find(keys[i]) >= 0: + method = methods[key] + method(client, message) + return + # unified auth acknowledgement + type = self.safe_string(message, 'type') + if type == 'AUTH_RESP': + self.handle_authenticate(client, message) + + def ping(self, client: Client): + timeStamp = self.milliseconds() + client.lastPong = timeStamp + return { + 'args': [str(timeStamp)], + 'op': 'ping', + } + + async def pong(self, client, message): + # + # {"op": "ping", "args": ["1761069137485"]} + # + timeStamp = self.milliseconds() + try: + await client.send({'args': [str(timeStamp)], 'op': 'pong'}) + except Exception as e: + error = NetworkError(self.id + ' handlePing failed with error ' + self.json(e)) + client.reset(error) + + def handle_pong(self, client: Client, message): + # + # { + # "success": True, + # "ret_msg": "pong", + # "conn_id": "db3158a0-8960-44b9-a9de-ac350ee13158", + # "request": {op: "ping", args: null} + # } + # + # {pong: 1653296711335} + # + client.lastPong = self.safe_integer(message, 'pong') + return message + + def handle_ping(self, client: Client, message): + self.spawn(self.pong, client, message) + + def handle_account(self, client: Client, message): + contents = self.safe_dict(message, 'contents', {}) + fills = self.safe_list(contents, 'fills', []) + if fills is not None: + self.handle_my_trades(client, fills) + positions = self.safe_list(contents, 'positions', []) + if positions is not None: + self.handle_positions(client, positions) + orders = self.safe_list(contents, 'orders', []) + if orders is not None: + self.handle_order(client, orders) + + def handle_authenticate(self, client: Client, message): + # + # { + # "success": True, + # "ret_msg": '', + # "op": "auth", + # "conn_id": "ce3dpomvha7dha97tvp0-2xh" + # } + # + success = self.safe_value(message, 'success') + code = self.safe_integer(message, 'retCode') + messageHash = 'authenticated' + if success or code == 0: + future = self.safe_value(client.futures, messageHash) + future.resolve(True) + else: + error = AuthenticationError(self.id + ' ' + self.json(message)) + client.reject(error, messageHash) + if messageHash in client.subscriptions: + del client.subscriptions[messageHash] + return message + + def handle_subscription_status(self, client: Client, message): + # + # { + # "topic": "kline", + # "event": "sub", + # "params": { + # "symbol": "LTCUSDT", + # "binary": "false", + # "klineType": "1m", + # "symbolName": "LTCUSDT" + # }, + # "code": "0", + # "msg": "Success" + # } + # + return message diff --git a/ccxt/pro/arkham.py b/ccxt/pro/arkham.py new file mode 100644 index 0000000..cdb372d --- /dev/null +++ b/ccxt/pro/arkham.py @@ -0,0 +1,686 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolById, ArrayCacheBySymbolBySide, ArrayCacheByTimestamp +import hashlib +from ccxt.base.types import Any, Balances, Bool, Int, Order, OrderBook, Position, Str, Strings, Ticker, Trade +from ccxt.async_support.base.ws.client import Client +from typing import List +from ccxt.base.errors import ExchangeError + + +class arkham(ccxt.async_support.arkham): + + def describe(self) -> Any: + return self.deep_extend(super(arkham, self).describe(), { + 'has': { + 'ws': True, + 'watchTrades': True, + 'watchTradesForSymbols': False, + 'watchOrderBook': True, + 'watchOrderBookForSymbols': False, + 'watchOHLCV': True, + 'watchOHLCVForSymbols': False, + 'watchOrders': True, + 'watchMyTrades': False, + 'watchTicker': True, + 'watchTickers': False, + 'watchBalance': True, + }, + 'urls': { + 'api': { + 'ws': 'wss://arkm.com/ws', + }, + }, + 'options': { + 'watchOrderBook': { + 'depth': 100, # 5, 10, 20, 50, 100 + 'interval': 500, # 100, 200, 500, 1000 + }, + }, + 'streaming': { + 'keepAlive': 300000, # 5 minutes + }, + }) + + def handle_message(self, client: Client, message): + # + # confirmation + # + # {channel: 'confirmations', confirmationId: 'myCustomId-123'} + if self.handle_error_message(client, message): + return + methods: dict = { + 'ticker': self.handle_ticker, + 'candles': self.handle_ohlcv, + 'l2_updates': self.handle_order_book, + 'trades': self.handle_trades, + 'balances': self.handle_balance, + 'positions': self.handle_positions, + 'order_statuses': self.handle_order, + 'trigger_orders': self.handle_order, + # 'confirmations': self.handle_ticker, + } + channel = self.safe_string(message, 'channel') + if channel == 'confirmations': + return + # type = self.safe_string(message, 'type') + # if type != 'update' and type != 'snapshot': + # debugger + # } + method = self.safe_value(methods, channel) + if method is not None: + method(client, message) + + async def subscribe(self, messageHash: str, rawChannel: str, params: dict) -> Any: + subscriptionHash = messageHash + request: dict = { + 'args': { + 'channel': rawChannel, + 'params': params, + }, + 'confirmationId': self.uuid(), + 'method': 'subscribe', + } + return await self.watch(self.urls['api']['ws'], messageHash, request, subscriptionHash) + + async def watch_ticker(self, symbol: str, params={}) -> Ticker: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://arkm.com/docs#stream/ticker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + requestArg = { + 'symbol': market['id'], + } + messageHash = 'ticker::' + market['symbol'] + return await self.subscribe(messageHash, 'ticker', self.extend(params, requestArg)) + + def handle_ticker(self, client: Client, message): + # + # { + # channel: 'ticker', + # type: 'update', + # data: { + # symbol: 'BTC_USDT', + # baseSymbol: 'BTC', + # quoteSymbol: 'USDT', + # price: '118962.74', + # price24hAgo: '118780.42', + # high24h: '120327.96', + # low24h: '118217.28', + # volume24h: '32.89729', + # quoteVolume24h: '3924438.7146048', + # markPrice: '0', + # indexPrice: '118963.080293501', + # fundingRate: '0', + # nextFundingRate: '0', + # nextFundingTime: 0, + # productType: 'spot', + # openInterest: '0', + # indexCurrency: 'USDT', + # usdVolume24h: '3924438.7146048', + # openInterestUSD: '0' + # } + # } + # + data = self.safe_dict(message, 'data', {}) + marketId = self.safe_string(data, 'symbol') + market = self.safe_market(marketId, None) + symbol = market['symbol'] + ticker = self.parse_ws_ticker(data, market) + self.tickers[symbol] = ticker + client.resolve(ticker, 'ticker::' + symbol) + # if self.safe_string(message, 'dataType') == 'all@ticker': + # client.resolve(ticker, self.getMessageHash('ticker')) + # } + + def parse_ws_ticker(self, message, market=None): + # same dict api + return self.parse_ticker(message, market) + + async def watch_ohlcv(self, symbol: str, timeframe='1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + watches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://arkm.com/docs#stream/candles + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + rawTimeframe = self.safe_string(self.timeframes, timeframe, timeframe) + requestArg = { + 'symbol': market['id'], + 'duration': rawTimeframe, + } + messageHash = 'ohlcv::' + market['symbol'] + '::' + rawTimeframe + result = await self.subscribe(messageHash, 'candles', self.extend(requestArg, params)) + ohlcv = result + if self.newUpdates: + limit = ohlcv.getLimit(market['symbol'], limit) + return self.filter_by_since_limit(ohlcv, since, limit, 0, True) + + def handle_ohlcv(self, client: Client, message): + # + # { + # channel: 'candles', + # type: 'update', + # data: { + # symbol: 'BTC_USDT', + # time: '1755076380000000', + # duration: 60000000, + # open: '120073.01', + # high: '120073.01', + # low: '120073.01', + # close: '120073.01', + # volume: '0', + # quoteVolume: '0' + # } + # } + # + data = self.safe_dict(message, 'data', {}) + marketId = self.safe_string(data, 'symbol') + market = self.safe_market(marketId, None) + symbol = market['symbol'] + duration = self.safe_integer(data, 'duration') + timeframe = self.findTimeframeByDuration(duration) + messageHash = 'ohlcv::' + symbol + '::' + timeframe + self.ohlcvs[symbol] = self.safe_value(self.ohlcvs, symbol, {}) + if not (timeframe in self.ohlcvs[symbol]): + limit = self.handle_option('watchOHLCV', 'limit', 1000) + self.ohlcvs[symbol][timeframe] = ArrayCacheByTimestamp(limit) + stored = self.ohlcvs[symbol][timeframe] + parsed = self.parse_ws_ohlcv(data, market) + stored.append(parsed) + client.resolve(stored, messageHash) + return message + + def parse_ws_ohlcv(self, ohlcv, market=None) -> list: + # same api + return self.parse_ohlcv(ohlcv, market) + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://arkm.com/docs#stream/l2_updates + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + requestArg = { + 'symbol': market['id'], + 'snapshot': True, + } + messageHash = 'orderBook::' + market['symbol'] + orderbook = await self.subscribe(messageHash, 'l2_updates', self.extend(requestArg, params)) + return orderbook.limit() + + def handle_order_book(self, client: Client, message): + # + # snapshot: + # + # { + # channel: 'l2_updates', + # type: 'snapshot', + # data: { + # symbol: 'BTC_USDT', + # group: '0.01', + # asks: [ [Object], [Object], ...], + # bids: [ [Object], [Object], ...], + # lastTime: 1755115180608299 + # } + # } + # + # update: + # + # { + # channel: "l2_updates", + # type: "update", + # data: { + # symbol: "BTC_USDT", + # group: "0.01", + # side: "sell", + # size: "0.05295", + # price: "122722.76", + # revisionId: 2455511217, + # time: 1755115736475207, + # } + # } + # + data = self.safe_dict(message, 'data') + type = self.safe_string(message, 'type') + marketId = self.safe_string(data, 'symbol') + market = self.safe_market(marketId) + symbol = market['symbol'] + messageHash = 'orderBook::' + symbol + if not (symbol in self.orderbooks): + ob = self.order_book({}) + ob['symbol'] = symbol + self.orderbooks[symbol] = ob + orderbook = self.orderbooks[symbol] + if type == 'snapshot': + timestamp = self.safe_integer_product(data, 'lastTime', 0.001) + parsedOrderBook = self.parse_order_book(data, symbol, timestamp, 'bids', 'asks', 'price', 'size') + orderbook.reset(parsedOrderBook) + elif type == 'update': + timestamp = self.safe_integer_product(data, 'time', 0.001) + side = self.safe_string(data, 'side') + bookside = orderbook['bids'] if (side == 'buy') else orderbook['asks'] + self.handle_delta(bookside, data) + orderbook['timestamp'] = timestamp + orderbook['datetime'] = self.iso8601(timestamp) + self.orderbooks[symbol] = orderbook + client.resolve(self.orderbooks[symbol], messageHash) + + def handle_delta(self, bookside, delta): + bidAsk = self.parse_bid_ask(delta, 'price', 'size') + bookside.storeArray(bidAsk) + + async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + watches information on multiple trades made in a market + + https://arkm.com/docs#stream/trades + + :param str symbol: unified market symbol of the market trades were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of trade structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + requestArg = { + 'symbol': market['id'], + } + messageHash = 'trade::' + market['symbol'] + trades = await self.subscribe(messageHash, 'trades', self.extend(requestArg, params)) + if self.newUpdates: + limit = trades.getLimit(market['symbol'], limit) + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + def handle_trades(self, client: Client, message): + # + # { + # channel: 'trades', + # type: 'update', + # data: { + # symbol: 'BTC_USDT', + # revisionId: 2643896903, + # size: '0.00261', + # price: '118273.2', + # takerSide: 'buy', + # time: 1755200320146389 + # } + # } + # + data = self.safe_dict(message, 'data') + marketId = self.safe_string(data, 'symbol') + symbol = self.safe_symbol(marketId) + if not (symbol in self.trades): + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + self.trades[symbol] = ArrayCache(limit) + parsed = self.parse_ws_trade(data) + stored = self.trades[symbol] + stored.append(parsed) + client.resolve(stored, 'trade::' + symbol) + + def parse_ws_trade(self, trade, market=None): + # same api + return self.parse_trade(trade, market) + + async def authenticate(self, params={}): + self.check_required_credentials() + expires = (self.milliseconds() + self.safe_integer(self.options, 'requestExpiration', 5000)) * 1000 # need macroseconds + wsOptions: dict = self.safe_dict(self.options, 'ws', {}) + authenticated = self.safe_string(wsOptions, 'token') + if authenticated is None: + method = 'GET' + bodyStr = '' + path = 'ws' + payload = self.apiKey + str(expires) + method.upper() + '/' + path + bodyStr + decodedSecret = self.base64_to_binary(self.secret) + signature = self.hmac(self.encode(payload), decodedSecret, hashlib.sha256, 'base64') + defaultOptions: dict = { + 'ws': { + 'options': { + 'headers': { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Arkham-Api-Key': self.apiKey, + 'Arkham-Expires': str(expires), + 'Arkham-Signature': signature, + }, + }, + }, + } + self.extend_exchange_options(defaultOptions) + self.client(self.urls['api']['ws']) + + async def watch_balance(self, params={}) -> Balances: + """ + watch balance and get the amount of funds available for trading or funds locked in orders + + https://arkm.com/docs#stream/balances + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: spot or contract if not provided self.options['defaultType'] is used + :returns dict: a `balance structure ` + """ + await self.authenticate() + await self.load_markets() + requestArg = { + 'snapshot': True, + } + messageHash = 'balances' + result = await self.subscribe(messageHash, 'balances', self.extend(requestArg, params)) + return result + + def handle_balance(self, client: Client, message): + # + # snapshot: + # + # { + # channel: 'balances', + # type: 'snapshot', + # data: [ + # { + # subaccountId: 0, + # symbol: 'USDT', + # balance: '7.035335375', + # free: '7.035335375', + # priceUSDT: '1', + # balanceUSDT: '7.035335375', + # freeUSDT: '7.035335375', + # lastUpdateReason: 'withdrawalFee', + # lastUpdateTime: '1753905990432678', + # lastUpdateId: 250483404, + # lastUpdateAmount: '-2' + # }, + # { + # subaccountId: 0, + # symbol: 'SOL', + # balance: '0.03', + # free: '0.03', + # priceUSDT: '197.37823276', + # balanceUSDT: '5.921346982', + # freeUSDT: '5.921346982', + # lastUpdateReason: 'orderFill', + # lastUpdateTime: '1753777760560164', + # lastUpdateId: 248588190, + # lastUpdateAmount: '0.03' + # } + # ] + # } + # + # update: + # + # { + # channel: 'balances', + # type: 'update', + # data: { + # subaccountId: 0, + # symbol: 'USDT', + # balance: '7.028357615', + # free: '7.028357615', + # priceUSDT: '1', + # balanceUSDT: '7.028357615', + # freeUSDT: '7.028357615', + # lastUpdateReason: 'tradingFee', + # lastUpdateTime: '1755240882544056', + # lastUpdateId: 2697860787, + # lastUpdateAmount: '-0.00697776' + # } + # } + # + type = self.safe_string(message, 'type') + parsed = {} + if type == 'snapshot': + # response same api + data = self.safe_list(message, 'data') + parsed = self.parse_ws_balance(data) + parsed['info'] = message + self.balance = parsed + else: + data = self.safe_dict(message, 'data') + balancesArray = [data] + parsed = self.parse_ws_balance(balancesArray) + currencyId = self.safe_string(data, 'symbol') + code = self.safe_currency_code(currencyId) + self.balance[code] = parsed[code] + messageHash = 'balances' + client.resolve(self.safe_balance(self.balance), messageHash) + + def parse_ws_balance(self, balance): + # same api + return self.parse_balance(balance) + + async def watch_positions(self, symbols: Strings = None, since: Int = None, limit: Int = None, params={}) -> List[Position]: + """ + + https://arkm.com/docs#stream/positions + + watch all open positions + :param str[] [symbols]: list of unified market symbols + :param int [since]: the earliest time in ms to fetch positions for + :param int [limit]: the maximum number of positions to retrieve + :param dict params: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + await self.authenticate() + await self.load_markets() + messageHash = 'positions' + if not self.is_empty(symbols): + symbols = self.market_symbols(symbols) + messageHash += '::' + ','.join(symbols) + self.positions = ArrayCacheBySymbolBySide() + requestArg = { + 'snapshot': False, # no need for initial snapshot, it's done in REST api + } + newPositions = await self.subscribe(messageHash, 'positions', self.extend(requestArg, params)) + if self.newUpdates: + return newPositions + return self.filter_by_symbols_since_limit(self.positions, symbols, since, limit, True) + + def handle_positions(self, client, message): + # + # snapshot: + # + # { + # channel: 'positions', + # type: 'snapshot', + # data: [ + # { + # subaccountId: 0, + # symbol: 'SOL_USDT_PERP', + # base: '0.059', + # quote: '-11.50618', + # openBuySize: '0', + # openSellSize: '0', + # openBuyNotional: '0', + # openSellNotional: '0', + # lastUpdateReason: 'orderFill', + # lastUpdateTime: '1755251065621402', + # lastUpdateId: 2709589783, + # lastUpdateBaseDelta: '0.059', + # lastUpdateQuoteDelta: '-11.50618', + # breakEvenPrice: '195.02', + # markPrice: '195', + # value: '11.505', + # pnl: '-0.00118', + # initialMargin: '1.1505', + # maintenanceMargin: '0.6903', + # averageEntryPrice: '195.02' + # } + # ] + # } + # + newPositions = [] + if self.positions is None: + self.positions = {} + type = self.safe_string(message, 'type') + if type == 'snapshot': + data = self.safe_list(message, 'data', []) + for i in range(0, len(data)): + position = self.parse_ws_position(data[i]) + if self.safe_integer(position, 'entryPrice') != 0: + newPositions.append(position) + symbol = self.safe_string(position, 'symbol') + self.positions[symbol] = position + else: + data = self.safe_dict(message, 'data') + position = self.parse_ws_position(data) + symbol = self.safe_string(position, 'symbol') + self.positions[symbol] = position + newPositions.append(position) + messageHashes = self.find_message_hashes(client, 'positions::') + for i in range(0, len(messageHashes)): + messageHash = messageHashes[i] + parts = messageHash.split('::') + symbolsString = parts[1] + symbols = symbolsString.split(',') + positions = self.filter_by_array(newPositions, 'symbol', symbols, False) + if not self.is_empty(positions): + client.resolve(positions, messageHash) + length = len(newPositions) + if length > 0: + client.resolve(newPositions, 'positions') + + def parse_ws_positions(self, positions: List[Any], symbols: List[str] = None, params={}) -> List[Position]: + symbols = self.market_symbols(symbols) + positions = self.to_array(positions) + result = [] + for i in range(0, len(positions)): + position = self.extend(self.parse_ws_position(positions[i], None), params) + result.append(position) + return self.filter_by_array_positions(result, 'symbol', symbols, False) + + def parse_ws_position(self, position, market=None): + # same api + return self.parse_position(position, market) + + async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + watches information on multiple orders made by the user + + https://arkm.com/docs#stream/order_statuses + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + await self.authenticate() + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + requestArg = { + 'snapshot': False, + } + isTriggerOrder = False + isTriggerOrder, params = self.handle_option_and_params(params, 'watchOrders', 'trigger', False) + rawChannel = 'trigger_orders' if isTriggerOrder else 'order_statuses' + messageHash = 'orders' + if symbol is not None: + messageHash += '::' + market['symbol'] + messageHash += '::' + rawChannel + orders = await self.subscribe(messageHash, rawChannel, self.extend(requestArg, params)) + if self.newUpdates: + limit = orders.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(orders, symbol, since, limit, True) + + def handle_order(self, client: Client, message): + # + # { + # channel: "order_statuses", + # type: "update", + # data: { + # orderId: 4200775347657, + # userId: 2959880, + # subaccountId: 0, + # symbol: "ARKM_USDT_PERP", + # time: "1755253639782186", + # side: "buy", + # type: "limitGtc", + # size: "10", + # price: "0.5", + # postOnly: False, + # reduceOnly: False, + # executedSize: "0", + # status: "cancelled", + # avgPrice: "0", + # executedNotional: "0", + # creditFeePaid: "0", + # marginBonusFeePaid: "0", + # quoteFeePaid: "0", + # arkmFeePaid: "0", + # revisionId: 2752963990, + # lastTime: "1755272026403545", + # clientOrderId: "", + # lastSize: "0", + # lastPrice: "0", + # lastCreditFee: "0", + # lastMarginBonusFee: "0", + # lastQuoteFee: "0", + # lastArkmFee: "0", + # } + # } + # + channel = self.safe_string(message, 'channel') + data = self.safe_dict(message, 'data') + if self.orders is None: + limit = self.safe_integer(self.options, 'ordersLimit', 1000) + self.orders = ArrayCacheBySymbolById(limit) + orders = self.orders + order = self.parse_ws_order(data) + orders.append(order) + client.resolve(orders, 'orders') + client.resolve(orders, 'orders::' + order['symbol'] + '::' + channel) + client.resolve(orders, 'orders::' + channel) + + def parse_ws_order(self, order, market=None) -> Order: + # same api + return self.parse_order(order, market) + + def handle_error_message(self, client: Client, response) -> Bool: + # + # error example: + # + # { + # "id": "30005", + # "name": "InvalidNotional", + # "message": "order validation failed: invalid notional: notional 0.25 is less than min notional 1" + # } + # + message = self.safe_string(response, 'message') + if message is not None: + body = self.json(response) + errorCode = self.safe_string(response, 'id') + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + raise ExchangeError(self.id + ' ' + body) + return False diff --git a/ccxt/pro/ascendex.py b/ccxt/pro/ascendex.py new file mode 100644 index 0000000..b779eb0 --- /dev/null +++ b/ccxt/pro/ascendex.py @@ -0,0 +1,964 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolById, ArrayCacheByTimestamp +import hashlib +from ccxt.base.types import Any, Balances, Bool, Int, Order, OrderBook, Str, Trade +from ccxt.async_support.base.ws.client import Client +from typing import List +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import NetworkError + + +class ascendex(ccxt.async_support.ascendex): + + def describe(self) -> Any: + return self.deep_extend(super(ascendex, self).describe(), { + 'has': { + 'ws': True, + 'watchBalance': True, + 'watchOHLCV': True, + 'watchOrderBook': True, + 'watchOrders': True, + 'watchTicker': False, + 'watchTrades': True, + 'watchTradesForSymbols': True, + }, + 'urls': { + 'api': { + 'ws': { + 'public': 'wss://ascendex.com:443/api/pro/v2/stream', + 'private': 'wss://ascendex.com:443/{accountGroup}/api/pro/v2/stream', + }, + }, + 'test': { + 'ws': { + 'public': 'wss://api-test.ascendex-sandbox.com:443/api/pro/v2/stream', + 'private': 'wss://api-test.ascendex-sandbox.com:443/{accountGroup}/api/pro/v2/stream', + }, + }, + }, + 'options': { + 'tradesLimit': 1000, + 'ordersLimit': 1000, + 'OHLCVLimit': 1000, + 'categoriesAccount': { + 'cash': 'spot', + 'futures': 'swap', + 'margin': 'margin', + }, + }, + }) + + async def watch_public(self, messageHash, params={}): + url = self.urls['api']['ws']['public'] + id = self.nonce() + request: dict = { + 'id': str(id), + 'op': 'sub', + } + message = self.extend(request, params) + return await self.watch(url, messageHash, message, messageHash) + + async def watch_public_multiple(self, messageHashes, params={}): + url = self.urls['api']['ws']['public'] + id = self.nonce() + request: dict = { + 'id': str(id), + 'op': 'sub', + } + message = self.extend(request, params) + return await self.watch_multiple(url, messageHashes, message, messageHashes) + + async def watch_private(self, channel, messageHash, params={}): + await self.load_accounts() + accountGroup = self.safe_string(self.options, 'account-group') + url = self.urls['api']['ws']['private'] + url = self.implode_params(url, {'accountGroup': accountGroup}) + id = self.nonce() + request: dict = { + 'id': str(id), + 'op': 'sub', + 'ch': channel, + } + message = self.extend(request, params) + await self.authenticate(url, params) + return await self.watch(url, messageHash, message, channel) + + async def watch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + watches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://ascendex.github.io/ascendex-pro-api/#channel-bar-data + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + if (limit is None) or (limit > 1440): + limit = 100 + interval = self.safe_string(self.timeframes, timeframe, timeframe) + channel = 'bar' + ':' + interval + ':' + market['id'] + params = { + 'ch': channel, + } + ohlcv = await self.watch_public(channel, params) + if self.newUpdates: + limit = ohlcv.getLimit(symbol, limit) + return self.filter_by_since_limit(ohlcv, since, limit, 0, True) + + def handle_ohlcv(self, client: Client, message): + # + # { + # "m": "bar", + # "s": "ASD/USDT", + # "data": { + # "i": "1", + # "ts": 1575398940000, + # "o": "0.04993", + # "c": "0.04970", + # "h": "0.04993", + # "l": "0.04970", + # "v": "8052" + # } + # } + # + marketId = self.safe_string(message, 's') + symbol = self.safe_symbol(marketId) + channel = self.safe_string(message, 'm') + data = self.safe_value(message, 'data', {}) + interval = self.safe_string(data, 'i') + messageHash = channel + ':' + interval + ':' + marketId + timeframe = self.find_timeframe(interval) + market = self.market(symbol) + parsed = self.parse_ohlcv(message, market) + self.ohlcvs[symbol] = self.safe_value(self.ohlcvs, symbol, {}) + stored = self.safe_value(self.ohlcvs[symbol], timeframe) + if stored is None: + limit = self.safe_integer(self.options, 'OHLCVLimit', 1000) + stored = ArrayCacheByTimestamp(limit) + self.ohlcvs[symbol][timeframe] = stored + stored.append(parsed) + client.resolve(stored, messageHash) + return message + + async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://ascendex.github.io/ascendex-pro-api/#channel-market-trades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + return await self.watch_trades_for_symbols([symbol], since, limit, params) + + async def watch_trades_for_symbols(self, symbols: List[str], since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a list of symbols + + https://ascendex.github.io/ascendex-pro-api/#channel-market-trades + + :param str[] symbols: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.name]: the name of the method to call, 'trade' or 'aggTrade', default is 'trade' + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, False, True, True) + marketIds = [] + messageHashes = [] + if symbols is not None: + for i in range(0, len(symbols)): + market = self.market(symbols[i]) + marketIds.append(market['id']) + messageHashes.append('trades:' + market['id']) + channel = 'trades:' + ','.join(marketIds) + params = self.extend(params, { + 'ch': channel, + }) + trades = await self.watch_public_multiple(messageHashes, params) + if self.newUpdates: + first = self.safe_value(trades, 0) + tradeSymbol = self.safe_string(first, 'symbol') + limit = trades.getLimit(tradeSymbol, limit) + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + def handle_trades(self, client: Client, message): + # + # { + # "m": "trades", + # "symbol": "BTC/USDT", + # "data": [ + # { + # "p": "40744.28", + # "q": "0.00150", + # "ts": 1647514330758, + # "bm": True, + # "seqnum": 72057633465800320 + # } + # ] + # } + # + marketId = self.safe_string(message, 'symbol') + symbol = self.safe_symbol(marketId) + channel = self.safe_string(message, 'm') + messageHash = channel + ':' + marketId + market = self.market(symbol) + rawData = self.safe_value(message, 'data') + if rawData is None: + rawData = [] + trades = self.parse_trades(rawData, market) + tradesArray = self.safe_value(self.trades, symbol) + if tradesArray is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + tradesArray = ArrayCache(limit) + for i in range(0, len(trades)): + tradesArray.append(trades[i]) + self.trades[symbol] = tradesArray + client.resolve(tradesArray, messageHash) + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://ascendex.github.io/ascendex-pro-api/#channel-level-2-order-book-updates + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + channel = 'depth' + ':' + market['id'] + params = self.extend(params, { + 'ch': channel, + }) + orderbook = await self.watch_public(channel, params) + return orderbook.limit() + + async def watch_order_book_snapshot(self, symbol: str, limit: Int = None, params={}): + await self.load_markets() + market = self.market(symbol) + action = 'depth-snapshot' + channel = action + ':' + market['id'] + params = self.extend(params, { + 'action': action, + 'args': { + 'symbol': market['id'], + }, + 'op': 'req', + }) + orderbook = await self.watch_public(channel, params) + return orderbook.limit() + + async def fetch_order_book_snapshot_custom(self, symbol: str, limit: Int = None, params={}): + restOrderBook = await self.fetch_rest_order_book_safe(symbol, limit, params) + if not (symbol in self.orderbooks): + self.orderbooks[symbol] = self.order_book() + orderbook = self.orderbooks[symbol] + orderbook.reset(restOrderBook) + return orderbook + + def handle_order_book_snapshot(self, client: Client, message): + # + # { + # "m": "depth", + # "symbol": "BTC/USDT", + # "data": { + # "ts": 1647520500149, + # "seqnum": 28590487626, + # "asks": [ + # [Array], [Array], [Array], + # [Array], [Array], [Array], + # ], + # "bids": [ + # [Array], [Array], [Array], + # [Array], [Array], [Array], + # ] + # } + # } + # + marketId = self.safe_string(message, 'symbol') + symbol = self.safe_symbol(marketId) + channel = self.safe_string(message, 'm') + messageHash = channel + ':' + symbol + orderbook = self.orderbooks[symbol] + data = self.safe_value(message, 'data') + snapshot = self.parse_order_book(data, symbol) + snapshot['nonce'] = self.safe_integer(data, 'seqnum') + orderbook.reset(snapshot) + # unroll the accumulated deltas + messages = orderbook.cache + for i in range(0, len(messages)): + messageItem = messages[i] + self.handle_order_book_message(client, messageItem, orderbook) + self.orderbooks[symbol] = orderbook + client.resolve(orderbook, messageHash) + + def handle_order_book(self, client: Client, message): + # + # { + # "m": "depth", + # "symbol": "BTC/USDT", + # "data": { + # "ts": 1647515136144, + # "seqnum": 28590470736, + # "asks": [[Array], [Array]], + # "bids": [[Array], [Array], [Array], [Array], [Array], [Array]] + # } + # } + # + channel = self.safe_string(message, 'm') + marketId = self.safe_string(message, 'symbol') + symbol = self.safe_symbol(marketId) + messageHash = channel + ':' + marketId + if not (symbol in self.orderbooks): + self.orderbooks[symbol] = self.order_book({}) + orderbook = self.orderbooks[symbol] + if orderbook['nonce'] is None: + orderbook.cache.append(message) + else: + self.handle_order_book_message(client, message, orderbook) + client.resolve(orderbook, messageHash) + + def handle_delta(self, bookside, delta): + # + # ["40990.47","0.01619"], + # + price = self.safe_float(delta, 0) + amount = self.safe_float(delta, 1) + bookside.store(price, amount) + + def handle_deltas(self, bookside, deltas): + for i in range(0, len(deltas)): + self.handle_delta(bookside, deltas[i]) + + def handle_order_book_message(self, client: Client, message, orderbook): + # + # { + # "m":"depth", + # "symbol":"BTC/USDT", + # "data":{ + # "ts":1647527417715, + # "seqnum":28590257013, + # "asks":[ + # ["40990.47","0.01619"], + # ["41021.21","0"], + # ["41031.59","0.06096"] + # ], + # "bids":[ + # ["40990.46","0.76114"], + # ["40985.18","0"] + # ] + # } + # } + # + data = self.safe_value(message, 'data', {}) + seqNum = self.safe_integer(data, 'seqnum') + if seqNum > orderbook['nonce']: + asks = self.safe_value(data, 'asks', []) + bids = self.safe_value(data, 'bids', []) + self.handle_deltas(orderbook['asks'], asks) + self.handle_deltas(orderbook['bids'], bids) + orderbook['nonce'] = seqNum + timestamp = self.safe_integer(data, 'ts') + orderbook['timestamp'] = timestamp + orderbook['datetime'] = self.iso8601(timestamp) + return orderbook + + async def watch_balance(self, params={}) -> Balances: + """ + watch balance and get the amount of funds available for trading or funds locked in orders + + https://ascendex.github.io/ascendex-pro-api/#channel-order-and-balance + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + type, query = self.handle_market_type_and_params('watchBalance', None, params) + channel = None + messageHash = None + if (type == 'spot') or (type == 'margin'): + accountCategories = self.safe_value(self.options, 'accountCategories', {}) + accountCategory = self.safe_string(accountCategories, type, 'cash') # cash, margin, + accountCategory = accountCategory.upper() + channel = 'order:' + accountCategory # order and balance share the same channel + messageHash = 'balance:' + type + else: + channel = 'futures-account-update' + messageHash = 'balance:swap' + return await self.watch_private(channel, messageHash, query) + + def handle_balance(self, client: Client, message): + # + # cash account + # + # { + # "m": "balance", + # "accountId": "cshQtyfq8XLAA9kcf19h8bXHbAwwoqEo", + # "ac": "CASH", + # "data": { + # "a" : "USDT", + # "sn": 8159798, + # "tb": "600", + # "ab": "600" + # } + # } + # + # margin account + # + # { + # "m": "balance", + # "accountId": "marOxpKJV83dxTRx0Eyxpa0gxc4Txt0P", + # "ac": "MARGIN", + # "data": { + # "a" : "USDT", + # "sn" : 8159802, + # "tb" : "400", # total Balance + # "ab" : "400", # available balance + # "brw": "0", # borrowws + # "int": "0" # interest + # } + # } + # + # futures + # { + # "m" : "futures-account-update", # message + # "e" : "ExecutionReport", # event type + # "t" : 1612508562129, # time + # "acc" : "futures-account-id", # account ID + # "at" : "FUTURES", # account type + # "sn" : 23128, # sequence number, + # "id" : "r177710001cbU3813942147C5kbFGOan", + # "col": [ + # { + # "a": "USDT", # asset code + # "b": "1000000", # balance + # "f": "1" # discount factor + # } + # ], + # (...) + # + channel = self.safe_string(message, 'm') + result = None + type = None + if (channel == 'order') or (channel == 'futures-order'): + data = self.safe_value(message, 'data') + marketId = self.safe_string(data, 's') + market = self.safe_market(marketId) + baseAccount = self.account() + baseAccount['free'] = self.safe_string(data, 'bab') + baseAccount['total'] = self.safe_string(data, 'btb') + quoteAccount = self.account() + quoteAccount['free'] = self.safe_string(data, 'qab') + quoteAccount['total'] = self.safe_string(data, 'qtb') + if market['contract']: + type = 'swap' + result = self.safe_value(self.balance, type, {}) + else: + type = market['type'] + result = self.safe_value(self.balance, type, {}) + result[market['base']] = baseAccount + result[market['quote']] = quoteAccount + else: + accountType = self.safe_string_lower_2(message, 'ac', 'at') + categoriesAccounts = self.safe_value(self.options, 'categoriesAccount') + type = self.safe_string(categoriesAccounts, accountType, 'spot') + result = self.safe_value(self.balance, type, {}) + data = self.safe_value(message, 'data') + balances = None + if data is None: + balances = self.safe_value(message, 'col') + else: + balances = [data] + for i in range(0, len(balances)): + balance = balances[i] + code = self.safe_currency_code(self.safe_string(balance, 'a')) + account = self.account() + account['free'] = self.safe_string(balance, 'ab') + account['total'] = self.safe_string_2(balance, 'tb', 'b') + result[code] = account + messageHash = 'balance' + ':' + type + client.resolve(self.safe_balance(result), messageHash) + + async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + + https://ascendex.github.io/ascendex-pro-api/#channel-order-and-balance + + watches information on multiple orders made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + type, query = self.handle_market_type_and_params('watchOrders', market, params) + messageHash = None + channel = None + if type != 'spot' and type != 'margin': + channel = 'futures-order' + messageHash = 'order:FUTURES' + else: + accountCategories = self.safe_value(self.options, 'accountCategories', {}) + accountCategory = self.safe_string(accountCategories, type, 'cash') # cash, margin + accountCategory = accountCategory.upper() + messageHash = 'order' + ':' + accountCategory + channel = messageHash + if symbol is not None: + messageHash = messageHash + ':' + symbol + orders = await self.watch_private(channel, messageHash, query) + if self.newUpdates: + limit = orders.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(orders, symbol, since, limit, True) + + def handle_order(self, client: Client, message): + # + # spot order + # { + # "m": "order", + # "accountId": "cshF5SlR9ukAXoDOuXbND4dVpBMw9gzH", + # "ac": "CASH", + # "data": { + # "sn": 19399016185, + # "orderId": "r17f9d7983faU7223046196CMlrj3bfC", + # "s": "LTC/USDT", + # "ot": "Limit", + # "t": 1647614461160, + # "p": "50", + # "q": "0.1", + # "sd": "Buy", + # "st": "New", + # "ap": "0", + # "cfq": "0", + # "sp": '', + # "err": '', + # "btb": "0", + # "bab": "0", + # "qtb": "8", + # "qab": "2.995", + # "cf": "0", + # "fa": "USDT", + # "ei": "NULL_VAL" + # } + # } + # + # futures order + # { + # "m": "futures-order", + # "sn": 19399927636, + # "e": "ExecutionReport", + # "a": "futF5SlR9ukAXoDOuXbND4dVpBMw9gzH", # account id + # "ac": "FUTURES", + # "t": 1647622515434, # last execution time + # (...) + # } + # + accountType = self.safe_string(message, 'ac') + messageHash = 'order:' + accountType + data = self.safe_value(message, 'data', message) + order = self.parse_ws_order(data) + if self.orders is None: + limit = self.safe_integer(self.options, 'ordersLimit', 1000) + self.orders = ArrayCacheBySymbolById(limit) + orders = self.orders + orders.append(order) + symbolMessageHash = messageHash + ':' + order['symbol'] + client.resolve(orders, symbolMessageHash) + client.resolve(orders, messageHash) + + def parse_ws_order(self, order, market=None): + # + # spot order + # { + # "sn": 19399016185, #sequence number + # "orderId": "r17f9d7983faU7223046196CMlrj3bfC", + # "s": "LTC/USDT", + # "ot": "Limit", # order type + # "t": 1647614461160, # last execution timestamp + # "p": "50", # price + # "q": "0.1", # quantity + # "sd": "Buy", # side + # "st": "New", # status + # "ap": "0", # average fill price + # "cfq": "0", # cumulated fill quantity + # "sp": '', # stop price + # "err": '', + # "btb": "0", # base asset total balance + # "bab": "0", # base asset available balance + # "qtb": "8", # quote asset total balance + # "qab": "2.995", # quote asset available balance + # "cf": "0", # cumulated commission + # "fa": "USDT", # fee asset + # "ei": "NULL_VAL" + # } + # + # futures order + # { + # "m": "futures-order", + # "sn": 19399927636, + # "e": "ExecutionReport", + # "a": "futF5SlR9ukAXoDOuXbND4dVpBMw9gzH", # account id + # "ac": "FUTURES", + # "t": 1647622515434, # last execution time + # "ct": 1647622515413, # order creation time + # "orderId": "r17f9df469b1U7223046196Okf5Kbmd", + # "sd": "Buy", # side + # "ot": "Limit", # order type + # "ei": "NULL_VAL", + # "q": "1", # quantity + # "p": "50", #price + # "sp": "0", # stopPrice + # "spb": '', # stopTrigger + # "s": "LTC-PERP", # symbol + # "st": "New", # state + # "err": '', + # "lp": "0", # last filled price + # "lq": "0", # last filled quantity(base asset) + # "ap": "0", # average filled price + # "cfq": "0", # cummulative filled quantity(base asset) + # "f": "0", # commission fee of the current execution + # "cf": "0", # cumulative commission fee + # "fa": "USDT", # fee asset + # "psl": "0", + # "pslt": "market", + # "ptp": "0", + # "ptpt": "market" + # } + # + status = self.parse_order_status(self.safe_string(order, 'st')) + marketId = self.safe_string(order, 's') + timestamp = self.safe_integer(order, 't') + symbol = self.safe_symbol(marketId, market, '/') + lastTradeTimestamp = self.safe_integer(order, 't') + price = self.safe_string(order, 'p') + amount = self.safe_string(order, 'q') + average = self.safe_string(order, 'ap') + filled = self.safe_string(order, 'cfq') + id = self.safe_string(order, 'orderId') + type = self.safe_string_lower(order, 'ot') + side = self.safe_string_lower(order, 'sd') + feeCost = self.safe_number(order, 'cf') + fee = None + if feeCost is not None: + feeCurrencyId = self.safe_string(order, 'fa') + feeCurrencyCode = self.safe_currency_code(feeCurrencyId) + fee = { + 'cost': feeCost, + 'currency': feeCurrencyCode, + } + stopPrice = self.parse_number(self.omit_zero(self.safe_string(order, 'sp'))) + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': lastTradeTimestamp, + 'symbol': symbol, + 'type': type, + 'timeInForce': None, + 'postOnly': None, + 'side': side, + 'price': price, + 'stopPrice': stopPrice, + 'triggerPrice': stopPrice, + 'amount': amount, + 'cost': None, + 'average': average, + 'filled': filled, + 'remaining': None, + 'status': status, + 'fee': fee, + 'trades': None, + }, market) + + def handle_error_message(self, client: Client, message) -> Bool: + # + # { + # "m": "disconnected", + # "code": 100005, + # "reason": "INVALID_WS_REQUEST_DATA", + # "info": "Session is disconnected due to missing pong message from the client" + # } + # + errorCode = self.safe_integer(message, 'code') + try: + if errorCode is not None: + feedback = self.id + ' ' + self.json(message) + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + messageString = self.safe_value(message, 'message') + if messageString is not None: + self.throw_broadly_matched_exception(self.exceptions['broad'], messageString, feedback) + return False + except Exception as e: + if isinstance(e, AuthenticationError): + messageHash = 'authenticated' + client.reject(e, messageHash) + if messageHash in client.subscriptions: + del client.subscriptions[messageHash] + else: + client.reject(e) + return True + + def handle_authenticate(self, client: Client, message): + # + # {m: "auth", id: "1647605234", code: 0} + # + messageHash = 'authenticated' + client.resolve(message, messageHash) + + def handle_message(self, client: Client, message): + if self.handle_error_message(client, message): + return + # + # {m: "ping", hp: 3} + # + # {m: "sub", ch: "bar:BTC/USDT", code: 0} + # + # {m: 'sub', id: "1647515701", ch: "depth:BTC/USDT", code: 0} + # + # {m: "connected", type: "unauth"} + # + # {m: "auth", id: "1647605234", code: 0} + # + # order or balance sub + # { + # "m": "sub", + # "id": "1647605952", + # "ch": "order:cshF5SlR9ukAXoDOuXbND4dVpBMw9gzH", or futures-order + # "code": 0 + # } + # + # ohlcv + # { + # "m": "bar", + # "s": "BTC/USDT", + # "data": { + # "i": "1", + # "ts": 1647510060000, + # "o": "40813.93", + # "c": "40804.57", + # "h": "40814.21", + # "l": "40804.56", + # "v": "0.01537" + # } + # } + # + # trades + # + # { + # "m": "trades", + # "symbol": "BTC/USDT", + # "data": [ + # { + # "p": "40762.26", + # "q": "0.01500", + # "ts": 1647514306759, + # "bm": True, + # "seqnum": 72057633465795180 + # } + # ] + # } + # + # orderbook deltas + # + # { + # "m":"depth", + # "symbol":"BTC/USDT", + # "data":{ + # "ts":1647527417715, + # "seqnum":28590257013, + # "asks":[ + # ["40990.47","0.01619"], + # ["41021.21","0"], + # ["41031.59","0.06096"] + # ], + # "bids":[ + # ["40990.46","0.76114"], + # ["40985.18","0"] + # ] + # } + # } + # + # orderbook snapshot + # { + # "m": "depth-snapshot", + # "symbol": "BTC/USDT", + # "data": { + # "ts": 1647525938513, + # "seqnum": 28590504772, + # "asks": [ + # [Array], [Array], [Array], [Array], [Array], [Array], [Array], + # [Array], [Array], [Array], [Array], [Array], [Array], [Array], + # [Array], [Array], [Array], [Array], [Array], [Array], [Array], + # (...) + # ] + # } + # + # spot order update + # { + # "m": "order", + # "accountId": "cshQtyfq8XLAA9kcf19h8bXHbAwwoqDo", + # "ac": "CASH", + # "data": { + # "s": "BTC/USDT", + # "sn": 8159711, + # "sd": "Buy", + # "ap": "0", + # "bab": "2006.5974027", + # "btb": "2006.5974027", + # "cf": "0", + # "cfq": "0", + # (...) + # } + # } + # future order update + # { + # "m": "futures-order", + # "sn": 19404258063, + # "e": "ExecutionReport", + # "a": "futF5SlR9ukAXoDOuXbND4dVpBMw9gzH", + # "ac": "FUTURES", + # "t": 1647681792543, + # "ct": 1647622515413, + # "orderId": "r17f9df469b1U7223046196Okf5KbmdL", + # (...) + # "ptpt": "None" + # } + # + # balance update cash + # { + # "m": "balance", + # "accountId": "cshQtyfq8XLAA9kcf19h8bXHbAwwoqDo", + # "ac": "CASH", + # "data": { + # "a" : "USDT", + # "sn": 8159798, + # "tb": "600", + # "ab": "600" + # } + # } + # + # balance update margin + # { + # "m": "balance", + # "accountId": "marOxpKJV83dxTRx0Eyxpa0gxc4Txt0P", + # "ac": "MARGIN", + # "data": { + # "a" : "USDT", + # "sn" : 8159802, + # "tb" : "400", + # "ab" : "400", + # "brw": "0", + # "int": "0" + # } + # } + # + subject = self.safe_string(message, 'm') + methods: dict = { + 'ping': self.handle_ping, + 'auth': self.handle_authenticate, + 'sub': self.handle_subscription_status, + 'depth': self.handle_order_book, + 'depth-snapshot': self.handle_order_book_snapshot, + 'trades': self.handle_trades, + 'bar': self.handle_ohlcv, + 'balance': self.handle_balance, + 'futures-account-update': self.handle_balance, + } + method = self.safe_value(methods, subject) + if method is not None: + method(client, message) + if (subject == 'order') or (subject == 'futures-order'): + # self.handle_order(client, message) + # balance updates may be in the order structure + # they may also be standalone balance updates related to account transfers + self.handle_order(client, message) + if subject == 'order': + self.handle_balance(client, message) + + def handle_subscription_status(self, client: Client, message): + # + # {m: "sub", ch: "bar:BTC/USDT", code: 0} + # + # {m: 'sub', id: "1647515701", ch: "depth:BTC/USDT", code: 0} + # + channel = self.safe_string(message, 'ch', '') + if channel.find('depth') > -1 and not (channel.find('depth-snapshot') > -1): + self.handle_order_book_subscription(client, message) + return message + + def handle_order_book_subscription(self, client: Client, message): + channel = self.safe_string(message, 'ch') + parts = channel.split(':') + marketId = parts[1] + market = self.safe_market(marketId) + symbol = market['symbol'] + if symbol in self.orderbooks: + del self.orderbooks[symbol] + self.orderbooks[symbol] = self.order_book({}) + if self.options['defaultType'] == 'swap' or market['contract']: + self.spawn(self.fetch_order_book_snapshot_custom, symbol) + else: + self.spawn(self.watch_order_book_snapshot, symbol) + + async def pong(self, client, message): + # + # {m: "ping", hp: 3} + # + try: + await client.send({'op': 'pong', 'hp': self.safe_integer(message, 'hp')}) + except Exception as e: + error = NetworkError(self.id + ' handlePing failed with error ' + self.json(e)) + client.reset(error) + + def handle_ping(self, client: Client, message): + self.spawn(self.pong, client, message) + + async def authenticate(self, url, params={}): + self.check_required_credentials() + messageHash = 'authenticated' + client = self.client(url) + future = self.safe_value(client.subscriptions, messageHash) + if future is None: + timestamp = str(self.milliseconds()) + urlParts = url.split('/') + partsLength = len(urlParts) + path = self.safe_string(urlParts, partsLength - 1) + version = self.safe_string(urlParts, partsLength - 2) + auth = timestamp + '+' + version + '/' + path + secret = self.base64_to_binary(self.secret) + signature = self.hmac(self.encode(auth), secret, hashlib.sha256, 'base64') + request: dict = { + 'op': 'auth', + 'id': str(self.nonce()), + 't': timestamp, + 'key': self.apiKey, + 'sig': signature, + } + future = await self.watch(url, messageHash, self.extend(request, params), messageHash) + client.subscriptions[messageHash] = future + return future diff --git a/ccxt/pro/backpack.py b/ccxt/pro/backpack.py new file mode 100644 index 0000000..5f24af6 --- /dev/null +++ b/ccxt/pro/backpack.py @@ -0,0 +1,1240 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolById, ArrayCacheByTimestamp +from ccxt.base.types import Any, Bool, Int, Market, Order, OrderBook, Position, Str, Strings, Ticker, Tickers, Trade +from ccxt.async_support.base.ws.client import Client +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import ArgumentsRequired + + +class backpack(ccxt.async_support.backpack): + + def describe(self) -> Any: + return self.deep_extend(super(backpack, self).describe(), { + 'has': { + 'ws': True, + 'watchBalance': False, + 'watchBidsAsks': True, + 'watchMyTrades': False, + 'watchOHLCV': True, + 'watchOHLCVForSymbols': True, + 'watchOrderBook': True, + 'watchOrderBookForSymbols': True, + 'watchOrders': True, + 'watchPositions': True, + 'watchTicker': True, + 'watchTickers': True, + 'watchTrades': True, + 'watchTradesForSymbols': True, + 'unwatchBidsAsks': True, + 'unwatchOHLCV': True, + 'unwatchOHLCVForSymbols': True, + 'unwatchOrderBook': True, + 'unwatchOrderBookForSymbols': True, + 'unwatchTicker': True, + 'unwatchTickers': True, + 'unWatchTrades': True, + 'unWatchTradesForSymbols': True, + 'unWatchOrders': True, + 'unWatchPositions': True, + }, + 'urls': { + 'api': { + 'ws': { + 'public': 'wss://ws.backpack.exchange', + 'private': 'wss://ws.backpack.exchange', + }, + }, + }, + 'options': { + 'timeframes': { + }, + }, + 'streaming': { + 'ping': self.ping, + 'keepAlive': 119000, + }, + }) + + async def watch_public(self, topics, messageHashes, params={}, unwatch=False): + await self.load_markets() + url = self.urls['api']['ws']['public'] + method = 'UNSUBSCRIBE' if unwatch else 'SUBSCRIBE' + request: dict = { + 'method': method, + 'params': topics, + } + message = self.deep_extend(request, params) + if unwatch: + self.handle_unsubscriptions(url, messageHashes, message) + return None + return await self.watch_multiple(url, messageHashes, message, messageHashes) + + async def watch_private(self, topics, messageHashes, params={}, unwatch=False): + self.check_required_credentials() + url = self.urls['api']['ws']['private'] + instruction = 'subscribe' + ts = str(self.nonce()) + method = 'UNSUBSCRIBE' if unwatch else 'SUBSCRIBE' + recvWindow = self.safe_string_2(self.options, 'recvWindow', 'X-Window', '5000') + payload = 'instruction=' + instruction + '&' + 'timestamp=' + ts + '&window=' + recvWindow + secretBytes = self.base64_to_binary(self.secret) + seed = self.array_slice(secretBytes, 0, 32) + signature = self.eddsa(self.encode(payload), seed, 'ed25519') + request: dict = { + 'method': method, + 'params': topics, + 'signature': [self.apiKey, signature, ts, recvWindow], + } + message = self.deep_extend(request, params) + if unwatch: + self.handle_unsubscriptions(url, messageHashes, message) + return None + return await self.watch_multiple(url, messageHashes, message, messageHashes) + + def handle_unsubscriptions(self, url: str, messageHashes: List[str], message: dict): + client = self.client(url) + self.watch_multiple(url, messageHashes, message, messageHashes) + for i in range(0, len(messageHashes)): + messageHash = messageHashes[i] + subMessageHash = messageHash.replace('unsubscribe:', '') + self.clean_unsubscription(client, subMessageHash, messageHash) + if messageHash.find('ticker') >= 0: + symbol = messageHash.replace('unsubscribe:ticker:', '') + if symbol in self.tickers: + del self.tickers[symbol] + elif messageHash.find('bidask') >= 0: + symbol = messageHash.replace('unsubscribe:bidask:', '') + if symbol in self.bidsasks: + del self.bidsasks[symbol] + elif messageHash.find('candles') >= 0: + splitHashes = messageHash.split(':') + symbol = self.safe_string(splitHashes, 2) + timeframe = self.safe_string(splitHashes, 3) + if symbol in self.ohlcvs: + if timeframe in self.ohlcvs[symbol]: + del self.ohlcvs[symbol][timeframe] + elif messageHash.find('orderbook') >= 0: + symbol = messageHash.replace('unsubscribe:orderbook:', '') + if symbol in self.orderbooks: + del self.orderbooks[symbol] + elif messageHash.find('trades') >= 0: + symbol = messageHash.replace('unsubscribe:trades:', '') + if symbol in self.trades: + del self.trades[symbol] + elif messageHash.find('orders') >= 0: + if messageHash == 'unsubscribe:orders': + cache = self.orders + keys = list(cache.keys()) + for j in range(0, len(keys)): + symbol = keys[j] + del self.orders[symbol] + else: + symbol = messageHash.replace('unsubscribe:orders:', '') + if symbol in self.orders: + del self.orders[symbol] + elif messageHash.find('positions') >= 0: + if messageHash == 'unsubscribe:positions': + cache = self.positions + keys = list(cache.keys()) + for j in range(0, len(keys)): + symbol = keys[j] + del self.positions[symbol] + else: + symbol = messageHash.replace('unsubscribe:positions:', '') + if symbol in self.positions: + del self.positions[symbol] + + async def watch_ticker(self, symbol: str, params={}) -> Ticker: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://docs.backpack.exchange/#tag/Streams/Public/Ticker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + topic = 'ticker' + '.' + market['id'] + messageHash = 'ticker' + ':' + symbol + return await self.watch_public([topic], [messageHash], params) + + async def un_watch_ticker(self, symbol: str, params={}) -> Any: + """ + unWatches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://docs.backpack.exchange/#tag/Streams/Public/Ticker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + return await self.un_watch_tickers([symbol], params) + + async def watch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list + + https://docs.backpack.exchange/#tag/Streams/Public/Ticker + + :param str[] symbols: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, False) + messageHashes = [] + topics = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + marketId = self.market_id(symbol) + messageHashes.append('ticker:' + symbol) + topics.append('ticker.' + marketId) + await self.watch_public(topics, messageHashes, params) + return self.filter_by_array(self.tickers, 'symbol', symbols) + + async def un_watch_tickers(self, symbols: Strings = None, params={}) -> Any: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list + + https://docs.backpack.exchange/#tag/Streams/Public/Ticker + + :param str[] symbols: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, False) + topics = [] + messageHashes = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + marketId = self.market_id(symbol) + topics.append('ticker.' + marketId) + messageHashes.append('unsubscribe:ticker:' + symbol) + return await self.watch_public(topics, messageHashes, params, True) + + def handle_ticker(self, client: Client, message): + # + # { + # data: { + # E: '1754176123312507', + # V: '19419526.742584', + # c: '3398.57', + # e: 'ticker', + # h: '3536.65', + # l: '3371.8', + # n: 17152, + # o: '3475.45', + # s: 'ETH_USDC', + # v: '5573.5827' + # }, + # stream: 'bookTicker.ETH_USDC' + # } + # + ticker = self.safe_dict(message, 'data', {}) + marketId = self.safe_string(ticker, 's') + market = self.safe_market(marketId) + symbol = self.safe_symbol(marketId, market) + parsedTicker = self.parse_ws_ticker(ticker, market) + messageHash = 'ticker' + ':' + symbol + self.tickers[symbol] = parsedTicker + client.resolve(parsedTicker, messageHash) + + def parse_ws_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # E: '1754178406415232', + # V: '19303818.6923', + # c: '3407.54', + # e: 'ticker', + # h: '3536.65', + # l: '3369.18', + # n: 17272, + # o: '3481.71', + # s: 'ETH_USDC', + # v: '5542.3911' + # } + # + microseconds = self.safe_integer(ticker, 'E') + timestamp = self.parse_to_int(microseconds / 1000) + marketId = self.safe_string(ticker, 's') + market = self.safe_market(marketId, market) + symbol = self.safe_symbol(marketId, market) + last = self.safe_string(ticker, 'c') + open = self.safe_string(ticker, 'o') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_number(ticker, 'h'), + 'low': self.safe_number(ticker, 'l'), + 'bid': None, + 'bidVolume': None, + 'ask': None, + 'askVolume': None, + 'vwap': None, + 'open': open, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': self.safe_string(ticker, 'v'), + 'quoteVolume': self.safe_string(ticker, 'V'), + 'info': ticker, + }, market) + + async def watch_bids_asks(self, symbols: Strings = None, params={}) -> Tickers: + """ + watches best bid & ask for symbols + + https://docs.backpack.exchange/#tag/Streams/Public/Book-ticker + + :param str[] symbols: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, False) + topics = [] + messageHashes = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + marketId = self.market_id(symbol) + topics.append('bookTicker.' + marketId) + messageHashes.append('bidask:' + symbol) + await self.watch_public(topics, messageHashes, params) + return self.filter_by_array(self.bidsasks, 'symbol', symbols) + + async def un_watch_bids_asks(self, symbols: Strings = None, params={}) -> Any: + """ + unWatches best bid & ask for symbols + :param str[] symbols: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, False) + topics = [] + messageHashes = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + marketId = self.market_id(symbol) + topics.append('bookTicker.' + marketId) + messageHashes.append('unsubscribe:bidask:' + symbol) + return await self.watch_public(topics, messageHashes, params, True) + + def handle_bid_ask(self, client: Client, message): + # + # { + # data: { + # A: '0.4087', + # B: '0.0020', + # E: '1754517402450016', + # T: '1754517402449064', + # a: '3667.50', + # b: '3667.49', + # e: 'bookTicker', + # s: 'ETH_USDC', + # u: 1328288557 + # }, + # stream: 'bookTicker.ETH_USDC' + # } + data = self.safe_dict(message, 'data', {}) + marketId = self.safe_string(data, 's') + market = self.safe_market(marketId) + symbol = self.safe_symbol(marketId, market) + parsedBidAsk = self.parse_ws_bid_ask(data, market) + messageHash = 'bidask' + ':' + symbol + self.bidsasks[symbol] = parsedBidAsk + client.resolve(parsedBidAsk, messageHash) + + def parse_ws_bid_ask(self, ticker, market=None): + # + # { + # A: '0.4087', + # B: '0.0020', + # E: '1754517402450016', + # T: '1754517402449064', + # a: '3667.50', + # b: '3667.49', + # e: 'bookTicker', + # s: 'ETH_USDC', + # u: 1328288557 + # } + # + marketId = self.safe_string(ticker, 's') + market = self.safe_market(marketId, market) + symbol = self.safe_string(market, 'symbol') + microseconds = self.safe_integer(ticker, 'E') + timestamp = self.parse_to_int(microseconds / 1000) + ask = self.safe_string(ticker, 'a') + askVolume = self.safe_string(ticker, 'A') + bid = self.safe_string(ticker, 'b') + bidVolume = self.safe_string(ticker, 'B') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'ask': ask, + 'askVolume': askVolume, + 'bid': bid, + 'bidVolume': bidVolume, + 'info': ticker, + }, market) + + async def watch_ohlcv(self, symbol: str, timeframe='1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + watches historical candlestick data containing the open, high, low, close price, and the volume of a market + + https://docs.backpack.exchange/#tag/Streams/Public/K-Line + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + result = await self.watch_ohlcv_for_symbols([[symbol, timeframe]], since, limit, params) + return result[symbol][timeframe] + + async def un_watch_ohlcv(self, symbol: str, timeframe: str = '1m', params={}) -> Any: + """ + watches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://docs.backpack.exchange/#tag/Streams/Public/K-Line + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + return await self.un_watch_ohlcv_for_symbols([[symbol, timeframe]], params) + + async def watch_ohlcv_for_symbols(self, symbolsAndTimeframes: List[List[str]], since: Int = None, limit: Int = None, params={}): + """ + watches historical candlestick data containing the open, high, low, close price, and the volume of a market + + https://docs.backpack.exchange/#tag/Streams/Public/K-Line + + :param str[][] symbolsAndTimeframes: array of arrays containing unified symbols and timeframes to fetch OHLCV data for, example [['BTC/USDT', '1m'], ['LTC/USDT', '5m']] + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + symbolsLength = len(symbolsAndTimeframes) + if symbolsLength == 0 or not isinstance(symbolsAndTimeframes[0], list): + raise ArgumentsRequired(self.id + " watchOHLCVForSymbols() requires a an array of symbols and timeframes, like ['ETH/USDC', '1m']") + await self.load_markets() + topics = [] + messageHashes = [] + for i in range(0, len(symbolsAndTimeframes)): + symbolAndTimeframe = symbolsAndTimeframes[i] + marketId = self.safe_string(symbolAndTimeframe, 0) + market = self.market(marketId) + tf = self.safe_string(symbolAndTimeframe, 1) + interval = self.safe_string(self.timeframes, tf, tf) + topics.append('kline.' + interval + '.' + market['id']) + messageHashes.append('candles:' + market['symbol'] + ':' + interval) + symbol, timeframe, candles = await self.watch_public(topics, messageHashes, params) + if self.newUpdates: + limit = candles.getLimit(symbol, limit) + filtered = self.filter_by_since_limit(candles, since, limit, 0, True) + return self.create_ohlcv_object(symbol, timeframe, filtered) + + async def un_watch_ohlcv_for_symbols(self, symbolsAndTimeframes: List[List[str]], params={}) -> Any: + """ + unWatches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://docs.backpack.exchange/#tag/Streams/Public/K-Line + + :param str[][] symbolsAndTimeframes: array of arrays containing unified symbols and timeframes to fetch OHLCV data for, example [['BTC/USDT', '1m'], ['LTC/USDT', '5m']] + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + symbolsLength = len(symbolsAndTimeframes) + if symbolsLength == 0 or not isinstance(symbolsAndTimeframes[0], list): + raise ArgumentsRequired(self.id + " unWatchOHLCVForSymbols() requires a an array of symbols and timeframes, like ['ETH/USDC', '1m']") + await self.load_markets() + topics = [] + messageHashes = [] + for i in range(0, len(symbolsAndTimeframes)): + symbolAndTimeframe = symbolsAndTimeframes[i] + marketId = self.safe_string(symbolAndTimeframe, 0) + market = self.market(marketId) + tf = self.safe_string(symbolAndTimeframe, 1) + interval = self.safe_string(self.timeframes, tf, tf) + topics.append('kline.' + interval + '.' + market['id']) + messageHashes.append('unsubscribe:candles:' + market['symbol'] + ':' + interval) + return await self.watch_public(topics, messageHashes, params, True) + + def handle_ohlcv(self, client: Client, message): + # + # { + # data: { + # E: '1754519557526056', + # T: '2025-08-07T00:00:00', + # X: False, + # c: '3680.520000000', + # e: 'kline', + # h: '3681.370000000', + # l: '3667.650000000', + # n: 255, + # o: '3670.150000000', + # s: 'ETH_USDC', + # t: '2025-08-06T22:00:00', + # v: '62.2621000' + # }, + # stream: 'kline.2h.ETH_USDC' + # } + # + data = self.safe_dict(message, 'data', {}) + marketId = self.safe_string(data, 's') + market = self.market(marketId) + symbol = market['symbol'] + stream = self.safe_string(message, 'stream') + parts = stream.split('.') + timeframe = self.safe_string(parts, 1) + if not (symbol in self.ohlcvs): + self.ohlcvs[symbol] = {} + if not (timeframe in self.ohlcvs[symbol]): + limit = self.safe_integer(self.options, 'OHLCVLimit', 1000) + stored = ArrayCacheByTimestamp(limit) + self.ohlcvs[symbol][timeframe] = stored + ohlcv = self.ohlcvs[symbol][timeframe] + parsed = self.parse_ws_ohlcv(data) + ohlcv.append(parsed) + messageHash = 'candles:' + symbol + ':' + timeframe + client.resolve([symbol, timeframe, ohlcv], messageHash) + + def parse_ws_ohlcv(self, ohlcv, market=None) -> list: + # + # { + # E: '1754519557526056', + # T: '2025-08-07T00:00:00', + # X: False, + # c: '3680.520000000', + # e: 'kline', + # h: '3681.370000000', + # l: '3667.650000000', + # n: 255, + # o: '3670.150000000', + # s: 'ETH_USDC', + # t: '2025-08-06T22:00:00', + # v: '62.2621000' + # }, + # + return [ + self.parse8601(self.safe_string(ohlcv, 'T')), + self.safe_number(ohlcv, 'o'), + self.safe_number(ohlcv, 'h'), + self.safe_number(ohlcv, 'l'), + self.safe_number(ohlcv, 'c'), + self.safe_number(ohlcv, 'v'), + ] + + async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + watches information on multiple trades made in a market + + https://docs.backpack.exchange/#tag/Streams/Public/Trade + + :param str symbol: unified symbol of the market to fetch the ticker for + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trade structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + return await self.watch_trades_for_symbols([symbol], since, limit, params) + + async def un_watch_trades(self, symbol: str, params={}) -> Any: + """ + unWatches from the stream channel + + https://docs.backpack.exchange/#tag/Streams/Public/Trade + + :param str symbol: unified symbol of the market to fetch trades for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + return await self.un_watch_trades_for_symbols([symbol], params) + + async def watch_trades_for_symbols(self, symbols: List[str], since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + watches information on multiple trades made in a market + + https://docs.backpack.exchange/#tag/Streams/Public/Trade + + :param str[] symbols: unified symbol of the market to fetch trades for + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trade structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + symbolsLength = len(symbols) + if symbolsLength == 0: + raise ArgumentsRequired(self.id + ' watchTradesForSymbols() requires a non-empty array of symbols') + topics = [] + messageHashes = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + marketId = self.market_id(symbol) + topics.append('trade.' + marketId) + messageHashes.append('trades:' + symbol) + trades = await self.watch_public(topics, messageHashes, params) + if self.newUpdates: + first = self.safe_value(trades, 0) + tradeSymbol = self.safe_string(first, 'symbol') + limit = trades.getLimit(tradeSymbol, limit) + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + async def un_watch_trades_for_symbols(self, symbols: List[str], params={}) -> Any: + """ + unWatches from the stream channel + + https://docs.backpack.exchange/#tag/Streams/Public/Trade + + :param str[] symbols: unified symbol of the market to fetch trades for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + symbolsLength = len(symbols) + if symbolsLength == 0: + raise ArgumentsRequired(self.id + ' unWatchTradesForSymbols() requires a non-empty array of symbols') + topics = [] + messageHashes = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + marketId = self.market_id(symbol) + topics.append('trade.' + marketId) + messageHashes.append('unsubscribe:trades:' + symbol) + return await self.watch_public(topics, messageHashes, params, True) + + def handle_trades(self, client: Client, message): + # + # { + # data: { + # E: '1754601477746429', + # T: '1754601477744000', + # a: '5121860761', + # b: '5121861755', + # e: 'trade', + # m: False, + # p: '3870.25', + # q: '0.0008', + # s: 'ETH_USDC_PERP', + # t: 10782547 + # }, + # stream: 'trade.ETH_USDC_PERP' + # } + # + data = self.safe_dict(message, 'data', {}) + marketId = self.safe_string(data, 's') + market = self.market(marketId) + symbol = market['symbol'] + if not (symbol in self.trades): + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + stored = ArrayCache(limit) + self.trades[symbol] = stored + cache = self.trades[symbol] + trade = self.parse_ws_trade(data, market) + cache.append(trade) + messageHash = 'trades:' + symbol + client.resolve(cache, messageHash) + client.resolve(cache, 'trades') + + def parse_ws_trade(self, trade, market=None): + # + # { + # E: '1754601477746429', + # T: '1754601477744000', + # a: '5121860761', + # b: '5121861755', + # e: 'trade', + # m: False, + # p: '3870.25', + # q: '0.0008', + # s: 'ETH_USDC_PERP', + # t: 10782547 + # } + # + microseconds = self.safe_integer(trade, 'E') + timestamp = self.parse_to_int(microseconds / 1000) + id = self.safe_string(trade, 't') + marketId = self.safe_string(trade, 's') + market = self.safe_market(marketId, market) + isMaker = self.safe_bool(trade, 'm') + side = 'sell' if isMaker else 'buy' + takerOrMaker = 'maker' if isMaker else 'taker' + price = self.safe_string(trade, 'p') + amount = self.safe_string(trade, 'q') + orderId = None + if side == 'buy': + orderId = self.safe_string(trade, 'b') + else: + orderId = self.safe_string(trade, 'a') + return self.safe_trade({ + 'info': trade, + 'id': id, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'order': orderId, + 'type': None, + 'side': side, + 'takerOrMaker': takerOrMaker, + 'price': price, + 'amount': amount, + 'cost': None, + 'fee': { + 'currency': None, + 'cost': None, + }, + }, market) + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://docs.backpack.exchange/#tag/Streams/Public/Depth + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + return await self.watch_order_book_for_symbols([symbol], limit, params) + + async def watch_order_book_for_symbols(self, symbols: List[str], limit: Int = None, params={}) -> OrderBook: + """ + + https://docs.backpack.exchange/#tag/Streams/Public/Depth + + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str[] symbols: unified array of symbols + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.method]: either '/market/level2' or '/spotMarket/level2Depth5' or '/spotMarket/level2Depth50' default is '/market/level2' + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, False) + marketIds = self.market_ids(symbols) + messageHashes = [] + topics = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + messageHashes.append('orderbook:' + symbol) + marketId = marketIds[i] + topic = 'depth.' + marketId + topics.append(topic) + orderbook = await self.watch_public(topics, messageHashes, params) + return orderbook.limit() # todo check if limit is needed + + async def un_watch_order_book(self, symbol: str, params={}) -> Any: + """ + unWatches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str symbol: unified array of symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + return await self.un_watch_order_book_for_symbols([symbol], params) + + async def un_watch_order_book_for_symbols(self, symbols: List[str], params={}) -> Any: + """ + unWatches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str[] symbols: unified array of symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.method]: either '/market/level2' or '/spotMarket/level2Depth5' or '/spotMarket/level2Depth50' default is '/market/level2' + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, False) + marketIds = self.market_ids(symbols) + messageHashes = [] + topics = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + messageHashes.append('unsubscribe:orderbook:' + symbol) + marketId = marketIds[i] + topic = 'depth.' + marketId + topics.append(topic) + return await self.watch_public(topics, messageHashes, params, True) + + def handle_order_book(self, client: Client, message): + # + # initial snapshot is fetched with ccxt's fetchOrderBook + # the feed does not include a snapshot, just the deltas + # + # { + # "data": { + # "E": "1754903057555305", + # "T": "1754903057554352", + # "U": 1345937436, + # "a": [], + # "b": [], + # "e": "depth", + # "s": "ETH_USDC", + # "u": 1345937436 + # }, + # "stream": "depth.ETH_USDC" + # } + # + data = self.safe_dict(message, 'data', {}) + marketId = self.safe_string(data, 's') + symbol = self.safe_symbol(marketId) + if not (symbol in self.orderbooks): + self.orderbooks[symbol] = self.order_book() + storedOrderBook = self.orderbooks[symbol] + nonce = self.safe_integer(storedOrderBook, 'nonce') + deltaNonce = self.safe_integer(data, 'u') + messageHash = 'orderbook:' + symbol + if nonce is None: + cacheLength = len(storedOrderBook.cache) + # the rest API is very delayed + # usually it takes at least 9 deltas to resolve + snapshotDelay = self.handle_option('watchOrderBook', 'snapshotDelay', 10) + if cacheLength == snapshotDelay: + self.spawn(self.load_order_book, client, messageHash, symbol, None, {}) + storedOrderBook.cache.append(data) + return + elif nonce > deltaNonce: + return + self.handle_delta(storedOrderBook, data) + client.resolve(storedOrderBook, messageHash) + + def handle_delta(self, orderbook, delta): + timestamp = self.parse_to_int(self.safe_integer(delta, 'T') / 1000) + orderbook['timestamp'] = timestamp + orderbook['datetime'] = self.iso8601(timestamp) + orderbook['nonce'] = self.safe_integer(delta, 'u') + bids = self.safe_list(delta, 'b', []) + asks = self.safe_list(delta, 'a', []) + storedBids = orderbook['bids'] + storedAsks = orderbook['asks'] + self.handle_bid_asks(storedBids, bids) + self.handle_bid_asks(storedAsks, asks) + + def handle_bid_asks(self, bookSide, bidAsks): + for i in range(0, len(bidAsks)): + bidAsk = self.parse_bid_ask(bidAsks[i]) + bookSide.storeArray(bidAsk) + + def get_cache_index(self, orderbook, cache): + # + # {"E":"1759338824897386","T":"1759338824895616","U":1662976171,"a":[],"b":[["117357.0","0.00000"]],"e":"depth","s":"BTC_USDC_PERP","u":1662976171} + firstDelta = self.safe_dict(cache, 0) + nonce = self.safe_integer(orderbook, 'nonce') + firstDeltaStart = self.safe_integer(firstDelta, 'U') + if nonce < firstDeltaStart - 1: + return -1 + for i in range(0, len(cache)): + delta = cache[i] + deltaStart = self.safe_integer(delta, 'U') + deltaEnd = self.safe_integer(delta, 'u') + if (nonce >= deltaStart - 1) and (nonce < deltaEnd): + return i + return len(cache) + + async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + watches information on multiple orders made by the user + + https://docs.backpack.exchange/#tag/Streams/Private/Order-update + + :param str [symbol]: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + topic = 'account.orderUpdate' + messageHash = 'orders' + if market is not None: + topic = 'account.orderUpdate.' + market['id'] + messageHash = 'orders:' + symbol + orders = await self.watch_private([topic], [messageHash], params) + if self.newUpdates: + limit = orders.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(orders, symbol, since, limit, True) + + async def un_watch_orders(self, symbol: Str = None, params={}) -> Any: + """ + unWatches information on multiple orders made by the user + + https://docs.backpack.exchange/#tag/Streams/Private/Order-update + + :param str [symbol]: unified market symbol of the market orders were made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + topic = 'account.orderUpdate' + messageHash = 'unsubscribe:orders' + if market is not None: + topic = 'account.orderUpdate.' + market['id'] + messageHash = 'unsubscribe:orders:' + symbol + return await self.watch_private([topic], [messageHash], params, True) + + def handle_order(self, client: Client, message): + # + # { + # data: { + # E: '1754939110175843', + # O: 'USER', + # Q: '4.30', + # S: 'Bid', + # T: '1754939110174703', + # V: 'RejectTaker', + # X: 'New', + # Z: '0', + # e: 'orderAccepted', + # f: 'GTC', + # i: '5406825793', + # o: 'MARKET', + # q: '0.0010', + # r: False, + # s: 'ETH_USDC', + # t: null, + # z: '0' + # }, + # stream: 'account.orderUpdate.ETH_USDC' + # } + # + messageHash = 'orders' + data = self.safe_dict(message, 'data', {}) + marketId = self.safe_string(data, 's') + market = self.safe_market(marketId) + symbol = market['symbol'] + parsed = self.parse_ws_order(data, market) + orders = self.orders + if orders is None: + limit = self.safe_integer(self.options, 'ordersLimit', 1000) + orders = ArrayCacheBySymbolById(limit) + self.orders = orders + orders.append(parsed) + client.resolve(orders, messageHash) + symbolSpecificMessageHash = messageHash + ':' + symbol + client.resolve(orders, symbolSpecificMessageHash) + + def parse_ws_order(self, order, market=None): + # + # { + # E: '1754939110175879', + # L: '4299.16', + # N: 'ETH', + # O: 'USER', + # Q: '4.30', + # S: 'Bid', + # T: '1754939110174705', + # V: 'RejectTaker', + # X: 'Filled', + # Z: '4.299160', + # e: 'orderFill', + # f: 'GTC', + # i: '5406825793', + # l: '0.0010', + # m: False, + # n: '0.000001', + # o: 'MARKET', + # q: '0.0010', + # r: False, + # s: 'ETH_USDC', + # t: 2888471, + # z: '0.0010' + # }, + # + id = self.safe_string(order, 'i') + clientOrderId = self.safe_string(order, 'c') + microseconds = self.safe_integer(order, 'E') + timestamp = self.parse_to_int(microseconds / 1000) + status = self.parse_ws_order_status(self.safe_string(order, 'X'), market) + marketId = self.safe_string(order, 's') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + type = self.safe_string_lower(order, 'o') + timeInForce = self.safe_string(order, 'f') + side = self.parse_ws_order_side(self.safe_string(order, 'S')) + price = self.safe_string(order, 'p') + triggerPrice = self.safe_number(order, 'P') + amount = self.safe_string(order, 'q') + cost = self.safe_string(order, 'Z') + filled = self.safe_string(order, 'l') + fee = None + feeCurrency = self.safe_string(order, 'N') + if feeCurrency is not None: + fee = { + 'currency': feeCurrency, + 'cost': None, + } + return self.safe_order({ + 'id': id, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'status': status, + 'symbol': symbol, + 'type': type, + 'timeInForce': timeInForce, + 'side': side, + 'price': price, + 'stopPrice': None, + 'triggerPrice': triggerPrice, + 'average': None, + 'amount': amount, + 'cost': cost, + 'filled': filled, + 'remaining': None, + 'fee': fee, + 'trades': None, + 'info': order, + }, market) + + def parse_ws_order_status(self, status, market=None): + statuses: dict = { + 'New': 'open', + 'Filled': 'closed', + 'Cancelled': 'canceled', + 'Expired': 'canceled', + 'PartiallyFilled': 'open', + 'TriggerPending': 'open', + 'TriggerFailed': 'rejected', + } + return self.safe_string(statuses, status, status) + + def parse_ws_order_side(self, side: Str): + sides: dict = { + 'Bid': 'buy', + 'Ask': 'sell', + } + return self.safe_string(sides, side, side) + + async def watch_positions(self, symbols: Strings = None, since: Int = None, limit: Int = None, params={}) -> List[Position]: + """ + watch all open positions + + https://docs.backpack.exchange/#tag/Streams/Private/Position-update + + :param str[] [symbols]: list of unified market symbols to watch positions for + :param int [since]: the earliest time in ms to fetch positions for + :param int [limit]: the maximum number of positions to retrieve + :param dict params: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + messageHashes = [] + topics = [] + if symbols is not None: + for i in range(0, len(symbols)): + symbol = symbols[i] + messageHashes.append('positions' + ':' + symbol) + topics.append('account.positionUpdate.' + self.market_id(symbol)) + else: + messageHashes.append('positions') + topics.append('account.positionUpdate') + positions = await self.watch_private(topics, messageHashes, params) + if self.newUpdates: + return positions + return self.filter_by_symbols_since_limit(self.positions, symbols, since, limit, True) + + async def un_watch_positions(self, symbols: Strings = None, params={}) -> List[Any]: + """ + unWatches from the stream channel + + https://docs.backpack.exchange/#tag/Streams/Private/Position-update + + :param str[] [symbols]: list of unified market symbols to watch positions for + :param dict params: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + messageHashes = [] + topics = [] + if symbols is not None: + for i in range(0, len(symbols)): + symbol = symbols[i] + messageHashes.append('unsubscribe:positions' + ':' + symbol) + topics.append('account.positionUpdate.' + self.market_id(symbol)) + else: + messageHashes.append('unsubscribe:positions') + topics.append('account.positionUpdate') + return await self.watch_private(topics, messageHashes, params, True) + + def handle_positions(self, client, message): + # + # { + # data: { + # B: '4236.36', + # E: '1754943862040486', + # M: '4235.88650933', + # P: '-0.000473', + # Q: '0.0010', + # T: '1754943862040487', + # b: '4238.479', + # e: 'positionOpened', + # f: '0.02', + # i: 5411399049, + # l: '0', + # m: '0.0125', + # n: '4.23588650933', + # p: '0', + # q: '0.0010', + # s: 'ETH_USDC_PERP' + # }, + # stream: 'account.positionUpdate' + # } + # + messageHash = 'positions' + data = self.safe_dict(message, 'data', {}) + if self.positions is None: + self.positions = ArrayCacheBySymbolById() + cache = self.positions + parsedPosition = self.parse_ws_position(data) + microseconds = self.safe_integer(data, 'E') + timestamp = self.parse_to_int(microseconds / 1000) + parsedPosition['timestamp'] = timestamp + parsedPosition['datetime'] = self.iso8601(timestamp) + cache.append(parsedPosition) + symbolSpecificMessageHash = messageHash + ':' + parsedPosition['symbol'] + client.resolve([parsedPosition], messageHash) + client.resolve([parsedPosition], symbolSpecificMessageHash) + + def parse_ws_position(self, position, market=None): + # + # { + # B: '4236.36', + # E: '1754943862040486', + # M: '4235.88650933', + # P: '-0.000473', + # Q: '0.0010', + # T: '1754943862040487', + # b: '4238.479', + # e: 'positionOpened', + # f: '0.02', + # i: 5411399049, + # l: '0', + # m: '0.0125', + # n: '4.23588650933', + # p: '0', + # q: '0.0010', + # s: 'ETH_USDC_PERP' + # } + # + id = self.safe_string(position, 'i') + marketId = self.safe_string(position, 's') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + notional = self.safe_string(position, 'n') + liquidationPrice = self.safe_string(position, 'l') + entryPrice = self.safe_string(position, 'b') + realizedPnl = self.safe_string(position, 'p') + unrealisedPnl = self.safe_string(position, 'P') + contracts = self.safe_string(position, 'Q') + markPrice = self.safe_string(position, 'M') + netQuantity = self.safe_number(position, 'q') + hedged = False + side = 'long' + if netQuantity < 0: + side = 'short' + if netQuantity is None: + hedged = None + side = None + microseconds = self.safe_integer(position, 'E') + timestamp = self.parse_to_int(microseconds / 1000) + maintenanceMarginPercentage = self.safe_number(position, 'm') + initialMarginPercentage = self.safe_number(position, 'f') + return self.safe_position({ + 'info': position, + 'id': id, + 'symbol': symbol, + 'notional': notional, + 'marginMode': None, + 'liquidationPrice': liquidationPrice, + 'entryPrice': entryPrice, + 'realizedPnl': realizedPnl, + 'unrealizedPnl': unrealisedPnl, + 'percentage': None, + 'contracts': contracts, + 'contractSize': None, + 'markPrice': markPrice, + 'side': side, + 'hedged': hedged, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'maintenanceMargin': None, + 'maintenanceMarginPercentage': maintenanceMarginPercentage, + 'collateral': None, + 'initialMargin': None, + 'initialMarginPercentage': initialMarginPercentage, + 'leverage': None, + 'marginRatio': None, + }) + + def handle_message(self, client: Client, message): + if not self.handle_error_message(client, message): + return + data = self.safe_dict(message, 'data') + event = self.safe_string(data, 'e') + if event == 'ticker': + self.handle_ticker(client, message) + elif event == 'bookTicker': + self.handle_bid_ask(client, message) + elif event == 'kline': + self.handle_ohlcv(client, message) + elif event == 'trade': + self.handle_trades(client, message) + elif event == 'depth': + self.handle_order_book(client, message) + elif event == 'orderAccepted' or event == 'orderUpdate' or event == 'orderFill' or event == 'orderCancelled' or event == 'orderExpired' or event == 'orderModified' or event == 'triggerPlaced' or event == 'triggerFailed': + self.handle_order(client, message) + elif event == 'positionAdjusted' or event == 'positionOpened' or event == 'positionClosed' or event == 'positionUpdated': + self.handle_positions(client, message) + + def handle_error_message(self, client: Client, message) -> Bool: + # + # { + # id: null, + # error: { + # code: 4006, + # message: 'Invalid stream' + # } + # } + # + error = self.safe_dict(message, 'error', {}) + code = self.safe_integer(error, 'code') + try: + if code is not None: + msg = self.safe_string(error, 'message') + raise ExchangeError(self.id + ' ' + msg) + return True + except Exception as e: + client.reject(e) + return True diff --git a/ccxt/pro/bequant.py b/ccxt/pro/bequant.py new file mode 100644 index 0000000..bbf99b3 --- /dev/null +++ b/ccxt/pro/bequant.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.pro.hitbtc import hitbtc +from ccxt.base.types import Any + +import ccxt.async_support.hitbtc as hitbtcRest + +import ccxt.async_support.bequant as bequantRest + + +class bequant(hitbtc): + + def describe(self) -> Any: + # eslint-disable-next-line new-cap + describeExtended = self.get_describe_for_extended_ws_exchange(bequantRest(), hitbtcRest(), super(bequant, self).describe()) + return self.deep_extend(describeExtended, { + 'id': 'bequant', + 'name': 'Bequant', + 'countries': ['MT'], # Malta + 'pro': True, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/55248342-a75dfe00-525a-11e9-8aa2-05e9dca943c6.jpg', + 'api': { + 'public': 'https://api.bequant.io/api/3', + 'private': 'https://api.bequant.io/api/3', + 'ws': { + 'public': 'wss://api.bequant.io/api/3/ws/public', + 'private': 'wss://api.bequant.io/api/3/ws/trading', + }, + }, + 'www': 'https://bequant.io', + 'doc': [ + 'https://api.bequant.io/', + ], + 'fees': [ + 'https://bequant.io/fees-and-limits', + ], + 'referral': 'https://bequant.io', + }, + }) diff --git a/ccxt/pro/binance.py b/ccxt/pro/binance.py new file mode 100644 index 0000000..6996548 --- /dev/null +++ b/ccxt/pro/binance.py @@ -0,0 +1,4201 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolById, ArrayCacheBySymbolBySide, ArrayCacheByTimestamp +import hashlib +from ccxt.base.types import Any, Balances, Int, Liquidation, Num, Order, OrderBook, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, Trade +from ccxt.async_support.base.ws.client import Client +from typing import List +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import NotSupported +from ccxt.base.errors import ChecksumError +from ccxt.base.precise import Precise + + +class binance(ccxt.async_support.binance): + + def describe(self) -> Any: + superDescribe = super(binance, self).describe() + return self.deep_extend(superDescribe, self.describe_data()) + + def describe_data(self): + return { + 'has': { + 'ws': True, + 'watchBalance': True, + 'watchLiquidations': True, + 'watchLiquidationsForSymbols': True, + 'watchMyLiquidations': True, + 'watchMyLiquidationsForSymbols': True, + 'watchBidsAsks': True, + 'watchMyTrades': True, + 'watchOHLCV': True, + 'watchOHLCVForSymbols': True, + 'watchOrderBook': True, + 'watchOrderBookForSymbols': True, + 'watchOrders': True, + 'watchOrdersForSymbols': True, + 'watchPositions': True, + 'watchTicker': True, + 'watchTickers': True, + 'watchMarkPrices': True, + 'watchMarkPrice': True, + 'watchTrades': True, + 'watchTradesForSymbols': True, + 'createOrderWs': True, + 'editOrderWs': True, + 'cancelOrderWs': True, + 'cancelOrdersWs': False, + 'cancelAllOrdersWs': True, + 'fetchBalanceWs': True, + 'fetchDepositsWs': False, + 'fetchMarketsWs': False, + 'fetchMyTradesWs': True, + 'fetchOHLCVWs': True, + 'fetchOrderBookWs': True, + 'fetchOpenOrdersWs': True, + 'fetchOrderWs': True, + 'fetchOrdersWs': True, + 'fetchPositionWs': True, + 'fetchPositionForSymbolWs': True, + 'fetchPositionsWs': True, + 'fetchTickerWs': True, + 'fetchTradesWs': True, + 'fetchTradingFeesWs': False, + 'fetchWithdrawalsWs': False, + 'unWatchTicker': True, + 'unWatchTickers': True, + 'unWatchOHLCV': True, + 'unWatchOHLCVForSymbols': True, + 'unWatchOrderBook': True, + 'unWatchOrderBookForSymbols': True, + 'unWatchTrades': True, + 'unWatchTradesForSymbols': True, + 'unWatchMyTrades': False, + 'unWatchOrders': False, + 'unWatchPositions': False, + 'unWatchMarkPrices': True, + 'unWatchMarkPrice': True, + }, + 'urls': { + 'test': { + 'ws': { + 'spot': 'wss://stream.testnet.binance.vision/ws', + 'margin': 'wss://stream.testnet.binance.vision/ws', + 'future': 'wss://fstream.binancefuture.com/ws', + 'delivery': 'wss://dstream.binancefuture.com/ws', + 'ws-api': { + 'spot': 'wss://ws-api.testnet.binance.vision/ws-api/v3', + 'future': 'wss://testnet.binancefuture.com/ws-fapi/v1', + 'delivery': 'wss://testnet.binancefuture.com/ws-dapi/v1', + }, + }, + }, + 'demo': { + 'ws': { + 'spot': 'wss://demo-stream.binance.com/ws', + 'margin': 'wss://demo-stream.binance.com/ws', + 'future': 'wss://fstream.binancefuture.com/ws', + 'delivery': 'wss://dstream.binancefuture.com/ws', + 'ws-api': { + 'spot': 'wss://demo-ws-api.binance.com/ws-api/v3', + 'future': 'wss://testnet.binancefuture.com/ws-fapi/v1', + 'delivery': 'wss://testnet.binancefuture.com/ws-dapi/v1', + }, + }, + }, + 'api': { + 'ws': { + 'spot': 'wss://stream.binance.com:9443/ws', + 'margin': 'wss://stream.binance.com:9443/ws', + 'future': 'wss://fstream.binance.com/ws', + 'delivery': 'wss://dstream.binance.com/ws', + 'ws-api': { + 'spot': 'wss://ws-api.binance.com:443/ws-api/v3', + 'future': 'wss://ws-fapi.binance.com/ws-fapi/v1', + 'delivery': 'wss://ws-dapi.binance.com/ws-dapi/v1', + }, + 'papi': 'wss://fstream.binance.com/pm/ws', + }, + }, + 'doc': 'https://developers.binance.com/en', + }, + 'streaming': { + 'keepAlive': 180000, + }, + 'options': { + 'returnRateLimits': False, + 'streamLimits': { + 'spot': 50, # max 1024 + 'margin': 50, # max 1024 + 'future': 50, # max 200 + 'delivery': 50, # max 200 + }, + 'subscriptionLimitByStream': { + 'spot': 200, + 'margin': 200, + 'future': 200, + 'delivery': 200, + }, + 'streamBySubscriptionsHash': self.create_safe_dictionary(), + 'streamIndex': -1, + # get updates every 1000ms or 100ms + # or every 0ms in real-time for futures + 'watchOrderBookRate': 100, + 'liquidationsLimit': 1000, + 'myLiquidationsLimit': 1000, + 'tradesLimit': 1000, + 'ordersLimit': 1000, + 'OHLCVLimit': 1000, + 'requestId': self.create_safe_dictionary(), + 'watchOrderBookLimit': 1000, # default limit + 'watchTrades': { + 'name': 'trade', # 'trade' or 'aggTrade' + }, + 'watchTicker': { + 'name': 'ticker', # ticker or miniTicker or ticker_ + }, + 'watchTickers': { + 'name': 'ticker', # ticker or miniTicker or ticker_ + }, + 'watchOHLCV': { + 'name': 'kline', # or indexPriceKline or markPriceKline(coin-m futures) + }, + 'watchOrderBook': { + 'maxRetries': 3, + 'checksum': True, + }, + 'watchBalance': { + 'fetchBalanceSnapshot': False, # or True + 'awaitBalanceSnapshot': True, # whether to wait for the balance snapshot before providing updates + }, + 'watchLiquidationsForSymbols': { + 'defaultType': 'swap', + }, + 'watchPositions': { + 'fetchPositionsSnapshot': True, # or False + 'awaitPositionsSnapshot': True, # whether to wait for the positions snapshot before providing updates + }, + 'wallet': 'wb', # wb = wallet balance, cw = cross balance + 'listenKeyRefreshRate': 1200000, # 20 mins + 'ws': { + 'cost': 5, + }, + 'tickerChannelsMap': { + '24hrTicker': 'ticker', + '24hrMiniTicker': 'miniTicker', + 'markPriceUpdate': 'markPrice', + # rolling window tickers + '1hTicker': 'ticker_1h', + '4hTicker': 'ticker_4h', + '1dTicker': 'ticker_1d', + 'bookTicker': 'bookTicker', + }, + }, + } + + def request_id(self, url): + options = self.safe_dict(self.options, 'requestId', self.create_safe_dictionary()) + previousValue = self.safe_integer(options, url, 0) + newValue = self.sum(previousValue, 1) + self.options['requestId'][url] = newValue + return newValue + + def is_spot_url(self, client: Client): + return(client.url.find('/stream') > -1) or (client.url.find('demo-stream') > -1) + + def stream(self, type: Str, subscriptionHash: Str, numSubscriptions=1): + streamBySubscriptionsHash = self.safe_dict(self.options, 'streamBySubscriptionsHash', self.create_safe_dictionary()) + stream = self.safe_string(streamBySubscriptionsHash, subscriptionHash) + if stream is None: + streamIndex = self.safe_integer(self.options, 'streamIndex', -1) + streamLimits = self.safe_value(self.options, 'streamLimits') + streamLimit = self.safe_integer(streamLimits, type) + streamIndex = streamIndex + 1 + normalizedIndex = streamIndex % streamLimit + self.options['streamIndex'] = streamIndex + stream = self.number_to_string(normalizedIndex) + self.options['streamBySubscriptionsHash'][subscriptionHash] = stream + subscriptionsByStreams = self.safe_value(self.options, 'numSubscriptionsByStream') + if subscriptionsByStreams is None: + self.options['numSubscriptionsByStream'] = self.create_safe_dictionary() + subscriptionsByStream = self.safe_integer(self.options['numSubscriptionsByStream'], stream, 0) + newNumSubscriptions = subscriptionsByStream + numSubscriptions + subscriptionLimitByStream = self.safe_integer(self.options['subscriptionLimitByStream'], type, 200) + if newNumSubscriptions > subscriptionLimitByStream: + raise BadRequest(self.id + ' reached the limit of subscriptions by stream. Increase the number of streams, or increase the stream limit or subscription limit by stream if the exchange allows.') + self.options['numSubscriptionsByStream'][stream] = subscriptionsByStream + numSubscriptions + return stream + + async def watch_liquidations(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Liquidation]: + """ + watch the public liquidations of a trading pair + + https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/Liquidation-Order-Streams + https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/Liquidation-Order-Streams + + :param str symbol: unified CCXT market symbol + :param int [since]: the earliest time in ms to fetch liquidations for + :param int [limit]: the maximum number of liquidation structures to retrieve + :param dict [params]: exchange specific parameters for the bitmex api endpoint + :returns dict: an array of `liquidation structures ` + """ + return await self.watch_liquidations_for_symbols([symbol], since, limit, params) + + async def watch_liquidations_for_symbols(self, symbols: List[str], since: Int = None, limit: Int = None, params={}) -> List[Liquidation]: + """ + watch the public liquidations of a trading pair + + https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/All-Market-Liquidation-Order-Streams + https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/All-Market-Liquidation-Order-Streams + + :param str[] symbols: list of unified market symbols + :param int [since]: the earliest time in ms to fetch liquidations for + :param int [limit]: the maximum number of liquidation structures to retrieve + :param dict [params]: exchange specific parameters for the bitmex api endpoint + :returns dict: an array of `liquidation structures ` + """ + await self.load_markets() + subscriptionHashes = [] + messageHashes = [] + streamHash = 'liquidations' + symbols = self.market_symbols(symbols, None, True, True) + if self.is_empty(symbols): + subscriptionHashes.append('!' + 'forceOrder@arr') + messageHashes.append('liquidations') + else: + for i in range(0, len(symbols)): + market = self.market(symbols[i]) + subscriptionHashes.append(market['lowercaseId'] + '@forceOrder') + messageHashes.append('liquidations::' + symbols[i]) + streamHash += '::' + ','.join(symbols) + firstMarket = self.get_market_from_symbols(symbols) + type = None + type, params = self.handle_market_type_and_params('watchLiquidationsForSymbols', firstMarket, params) + if type == 'spot': + raise BadRequest(self.id + ' watchLiquidationsForSymbols is not supported for spot symbols') + subType = None + subType, params = self.handle_sub_type_and_params('watchLiquidationsForSymbols', firstMarket, params) + if self.isLinear(type, subType): + type = 'future' + elif self.isInverse(type, subType): + type = 'delivery' + numSubscriptions = len(subscriptionHashes) + url = self.urls['api']['ws'][type] + '/' + self.stream(type, streamHash, numSubscriptions) + requestId = self.request_id(url) + request = { + 'method': 'SUBSCRIBE', + 'params': subscriptionHashes, + 'id': requestId, + } + subscribe = { + 'id': requestId, + } + newLiquidations = await self.watch_multiple(url, messageHashes, self.extend(request, params), subscriptionHashes, subscribe) + if self.newUpdates: + return newLiquidations + return self.filter_by_symbols_since_limit(self.liquidations, symbols, since, limit, True) + + def handle_liquidation(self, client: Client, message): + # + # future + # { + # "e":"forceOrder", + # "E":1698871323061, + # "o":{ + # "s":"BTCUSDT", + # "S":"BUY", + # "o":"LIMIT", + # "f":"IOC", + # "q":"1.437", + # "p":"35100.81", + # "ap":"34959.70", + # "X":"FILLED", + # "l":"1.437", + # "z":"1.437", + # "T":1698871323059 + # } + # } + # delivery + # { + # "e":"forceOrder", # Event Type + # "E": 1591154240950, # Event Time + # "o":{ + # "s":"BTCUSD_200925", # Symbol + # "ps": "BTCUSD", # Pair + # "S":"SELL", # Side + # "o":"LIMIT", # Order Type + # "f":"IOC", # Time in Force + # "q":"1", # Original Quantity + # "p":"9425.5", # Price + # "ap":"9496.5", # Average Price + # "X":"FILLED", # Order Status + # "l":"1", # Order Last Filled Quantity + # "z":"1", # Order Filled Accumulated Quantity + # "T": 1591154240949, # Order Trade Time + # } + # } + # + rawLiquidation = self.safe_value(message, 'o', {}) + marketId = self.safe_string(rawLiquidation, 's') + market = self.safe_market(marketId, None, '', 'contract') + symbol = market['symbol'] + liquidation = self.parse_ws_liquidation(rawLiquidation, market) + liquidations = self.safe_value(self.liquidations, symbol) + if liquidations is None: + limit = self.safe_integer(self.options, 'liquidationsLimit', 1000) + liquidations = ArrayCache(limit) + liquidations.append(liquidation) + self.liquidations[symbol] = liquidations + client.resolve([liquidation], 'liquidations') + client.resolve([liquidation], 'liquidations::' + symbol) + + def parse_ws_liquidation(self, liquidation, market=None): + # + # future + # { + # "s":"BTCUSDT", + # "S":"BUY", + # "o":"LIMIT", + # "f":"IOC", + # "q":"1.437", + # "p":"35100.81", + # "ap":"34959.70", + # "X":"FILLED", + # "l":"1.437", + # "z":"1.437", + # "T":1698871323059 + # } + # delivery + # { + # "s":"BTCUSD_200925", # Symbol + # "ps": "BTCUSD", # Pair + # "S":"SELL", # Side + # "o":"LIMIT", # Order Type + # "f":"IOC", # Time in Force + # "q":"1", # Original Quantity + # "p":"9425.5", # Price + # "ap":"9496.5", # Average Price + # "X":"FILLED", # Order Status + # "l":"1", # Order Last Filled Quantity + # "z":"1", # Order Filled Accumulated Quantity + # "T": 1591154240949, # Order Trade Time + # } + # myLiquidation + # { + # "s":"BTCUSDT", # Symbol + # "c":"TEST", # Client Order Id + # # special client order id: + # # starts with "autoclose-": liquidation order + # # "adl_autoclose": ADL auto close order + # # "settlement_autoclose-": settlement order for delisting or delivery + # "S":"SELL", # Side + # "o":"TRAILING_STOP_MARKET", # Order Type + # "f":"GTC", # Time in Force + # "q":"0.001", # Original Quantity + # "p":"0", # Original Price + # "ap":"0", # Average Price + # "sp":"7103.04", # Stop Price. Please ignore with TRAILING_STOP_MARKET order + # "x":"NEW", # Execution Type + # "X":"NEW", # Order Status + # "i":8886774, # Order Id + # "l":"0", # Order Last Filled Quantity + # "z":"0", # Order Filled Accumulated Quantity + # "L":"0", # Last Filled Price + # "N":"USDT", # Commission Asset, will not push if no commission + # "n":"0", # Commission, will not push if no commission + # "T":1568879465650, # Order Trade Time + # "t":0, # Trade Id + # "b":"0", # Bids Notional + # "a":"9.91", # Ask Notional + # "m":false, # Is self trade the maker side? + # "R":false, # Is self reduce only + # "wt":"CONTRACT_PRICE", # Stop Price Working Type + # "ot":"TRAILING_STOP_MARKET",// Original Order Type + # "ps":"LONG", # Position Side + # "cp":false, # If Close-All, pushed with conditional order + # "AP":"7476.89", # Activation Price, only puhed with TRAILING_STOP_MARKET order + # "cr":"5.0", # Callback Rate, only puhed with TRAILING_STOP_MARKET order + # "pP": False, # If price protection is turned on + # "si": 0, # ignore + # "ss": 0, # ignore + # "rp":"0", # Realized Profit of the trade + # "V":"EXPIRE_TAKER", # STP mode + # "pm":"OPPONENT", # Price match mode + # "gtd":0 # TIF GTD order auto cancel time + # } + # + marketId = self.safe_string(liquidation, 's') + market = self.safe_market(marketId, market, None, 'swap') + timestamp = self.safe_integer(liquidation, 'T') + return self.safe_liquidation({ + 'info': liquidation, + 'symbol': self.safe_symbol(marketId, market), + 'contracts': self.safe_number(liquidation, 'l'), + 'contractSize': self.safe_number(market, 'contractSize'), + 'price': self.safe_number(liquidation, 'ap'), + 'side': self.safe_string_lower(liquidation, 'S'), + 'baseValue': None, + 'quoteValue': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + + async def watch_my_liquidations(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Liquidation]: + """ + watch the private liquidations of a trading pair + + https://developers.binance.com/docs/derivatives/usds-margined-futures/user-data-streams/Event-Order-Update + https://developers.binance.com/docs/derivatives/coin-margined-futures/user-data-streams/Event-Order-Update + + :param str symbol: unified CCXT market symbol + :param int [since]: the earliest time in ms to fetch liquidations for + :param int [limit]: the maximum number of liquidation structures to retrieve + :param dict [params]: exchange specific parameters for the bitmex api endpoint + :returns dict: an array of `liquidation structures ` + """ + return self.watch_my_liquidations_for_symbols([symbol], since, limit, params) + + async def watch_my_liquidations_for_symbols(self, symbols: List[str], since: Int = None, limit: Int = None, params={}) -> List[Liquidation]: + """ + watch the private liquidations of a trading pair + + https://developers.binance.com/docs/derivatives/usds-margined-futures/user-data-streams/Event-Order-Update + https://developers.binance.com/docs/derivatives/coin-margined-futures/user-data-streams/Event-Order-Update + + :param str[] symbols: list of unified market symbols + :param int [since]: the earliest time in ms to fetch liquidations for + :param int [limit]: the maximum number of liquidation structures to retrieve + :param dict [params]: exchange specific parameters for the bitmex api endpoint + :returns dict: an array of `liquidation structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, True, True, True) + market = self.get_market_from_symbols(symbols) + messageHashes = ['myLiquidations'] + if not self.is_empty(symbols): + for i in range(0, len(symbols)): + symbol = symbols[i] + messageHashes.append('myLiquidations::' + symbol) + type = None + type, params = self.handle_market_type_and_params('watchMyLiquidationsForSymbols', market, params) + subType = None + subType, params = self.handle_sub_type_and_params('watchMyLiquidationsForSymbols', market, params) + if self.isLinear(type, subType): + type = 'future' + elif self.isInverse(type, subType): + type = 'delivery' + await self.authenticate(params) + url = self.urls['api']['ws'][type] + '/' + self.options[type]['listenKey'] + message = None + newLiquidations = await self.watch_multiple(url, messageHashes, message, [type]) + if self.newUpdates: + return newLiquidations + return self.filter_by_symbols_since_limit(self.liquidations, symbols, since, limit) + + def handle_my_liquidation(self, client: Client, message): + # + # { + # "s":"BTCUSDT", # Symbol + # "c":"TEST", # Client Order Id + # # special client order id: + # # starts with "autoclose-": liquidation order + # # "adl_autoclose": ADL auto close order + # # "settlement_autoclose-": settlement order for delisting or delivery + # "S":"SELL", # Side + # "o":"TRAILING_STOP_MARKET", # Order Type + # "f":"GTC", # Time in Force + # "q":"0.001", # Original Quantity + # "p":"0", # Original Price + # "ap":"0", # Average Price + # "sp":"7103.04", # Stop Price. Please ignore with TRAILING_STOP_MARKET order + # "x":"NEW", # Execution Type + # "X":"NEW", # Order Status + # "i":8886774, # Order Id + # "l":"0", # Order Last Filled Quantity + # "z":"0", # Order Filled Accumulated Quantity + # "L":"0", # Last Filled Price + # "N":"USDT", # Commission Asset, will not push if no commission + # "n":"0", # Commission, will not push if no commission + # "T":1568879465650, # Order Trade Time + # "t":0, # Trade Id + # "b":"0", # Bids Notional + # "a":"9.91", # Ask Notional + # "m":false, # Is self trade the maker side? + # "R":false, # Is self reduce only + # "wt":"CONTRACT_PRICE", # Stop Price Working Type + # "ot":"TRAILING_STOP_MARKET",// Original Order Type + # "ps":"LONG", # Position Side + # "cp":false, # If Close-All, pushed with conditional order + # "AP":"7476.89", # Activation Price, only puhed with TRAILING_STOP_MARKET order + # "cr":"5.0", # Callback Rate, only puhed with TRAILING_STOP_MARKET order + # "pP": False, # If price protection is turned on + # "si": 0, # ignore + # "ss": 0, # ignore + # "rp":"0", # Realized Profit of the trade + # "V":"EXPIRE_TAKER", # STP mode + # "pm":"OPPONENT", # Price match mode + # "gtd":0 # TIF GTD order auto cancel time + # } + # + orderType = self.safe_string(message, 'o') + if orderType != 'LIQUIDATION': + return + marketId = self.safe_string(message, 's') + market = self.safe_market(marketId, None, None, 'swap') + symbol = self.safe_symbol(marketId, market) + liquidation = self.parse_ws_liquidation(message, market) + myLiquidations = self.safe_value(self.myLiquidations, symbol) + if myLiquidations is None: + limit = self.safe_integer(self.options, 'myLiquidationsLimit', 1000) + myLiquidations = ArrayCache(limit) + myLiquidations.append(liquidation) + self.myLiquidations[symbol] = myLiquidations + client.resolve([liquidation], 'myLiquidations') + client.resolve([liquidation], 'myLiquidations::' + symbol) + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://developers.binance.com/docs/binance-spot-api-docs/web-socket-streams#partial-book-depth-streams + https://developers.binance.com/docs/binance-spot-api-docs/web-socket-streams#diff-depth-stream + https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/Partial-Book-Depth-Streams + https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/Diff-Book-Depth-Streams + https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/Partial-Book-Depth-Streams + https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/Diff-Book-Depth-Streams + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + # + # todo add support for -snapshots(depth) + # https://github.com/binance-exchange/binance-official-api-docs/blob/master/web-socket-streams.md#partial-book-depth-streams # @depth@100ms or @depth(1000ms) + # valid are 5, 10, or 20 + # + # default 100, max 1000, valid limits 5, 10, 20, 50, 100, 500, 1000 + # + # notice the differences between trading futures and spot trading + # the algorithms use different urls in step 1 + # delta caching and merging also differs in steps 4, 5, 6 + # + # spot/margin + # https://binance-docs.github.io/apidocs/spot/en/#how-to-manage-a-local-order-book-correctly + # + # 1. Open a stream to wss://stream.binance.com:9443/ws/bnbbtc@depth. + # 2. Buffer the events you receive from the stream. + # 3. Get a depth snapshot from https://www.binance.com/api/v1/depth?symbol=BNBBTC&limit=1000 . + # 4. Drop any event where u is <= lastUpdateId in the snapshot. + # 5. The first processed event should have U <= lastUpdateId+1 AND u >= lastUpdateId+1. + # 6. While listening to the stream, each new event's U should be equal to the previous event's u+1. + # 7. The data in each event is the absolute quantity for a price level. + # 8. If the quantity is 0, remove the price level. + # 9. Receiving an event that removes a price level that is not in your local order book can happen and is normal. + # + # futures + # https://binance-docs.github.io/apidocs/futures/en/#how-to-manage-a-local-order-book-correctly + # + # 1. Open a stream to wss://fstream.binance.com/stream?streams=btcusdt@depth. + # 2. Buffer the events you receive from the stream. For same price, latest received update covers the previous one. + # 3. Get a depth snapshot from https://fapi.binance.com/fapi/v1/depth?symbol=BTCUSDT&limit=1000 . + # 4. Drop any event where u is < lastUpdateId in the snapshot. + # 5. The first processed event should have U <= lastUpdateId AND u >= lastUpdateId + # 6. While listening to the stream, each new event's pu should be equal to the previous event's u, otherwise initialize the process from step 3. + # 7. The data in each event is the absolute quantity for a price level. + # 8. If the quantity is 0, remove the price level. + # 9. Receiving an event that removes a price level that is not in your local order book can happen and is normal. + # + return await self.watch_order_book_for_symbols([symbol], limit, params) + + async def watch_order_book_for_symbols(self, symbols: List[str], limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://developers.binance.com/docs/binance-spot-api-docs/web-socket-streams#partial-book-depth-streams + https://developers.binance.com/docs/binance-spot-api-docs/web-socket-streams#diff-depth-stream + https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/Partial-Book-Depth-Streams + https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/Diff-Book-Depth-Streams + https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/Partial-Book-Depth-Streams + https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/Diff-Book-Depth-Streams + + :param str[] symbols: unified array of symbols + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, False, True, True) + firstMarket = self.market(symbols[0]) + type = firstMarket['type'] + if firstMarket['contract']: + type = 'future' if firstMarket['linear'] else 'delivery' + name = 'depth' + streamHash = 'multipleOrderbook' + if symbols is not None: + symbolsLength = len(symbols) + if symbolsLength > 200: + raise BadRequest(self.id + ' watchOrderBookForSymbols() accepts 200 symbols at most. To watch more symbols call watchOrderBookForSymbols() multiple times') + streamHash += '::' + ','.join(symbols) + watchOrderBookRate = self.safe_string(self.options, 'watchOrderBookRate', '100') + subParams = [] + messageHashes = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + market = self.market(symbol) + messageHashes.append('orderbook::' + symbol) + subscriptionHash = market['lowercaseId'] + '@' + name + symbolHash = subscriptionHash + '@' + watchOrderBookRate + 'ms' + subParams.append(symbolHash) + messageHashesLength = len(messageHashes) + url = self.urls['api']['ws'][type] + '/' + self.stream(type, streamHash, messageHashesLength) + requestId = self.request_id(url) + request: dict = { + 'method': 'SUBSCRIBE', + 'params': subParams, + 'id': requestId, + } + subscription: dict = { + 'id': str(requestId), + 'name': name, + 'symbols': symbols, + 'method': self.handle_order_book_subscription, + 'limit': limit, + 'type': type, + 'params': params, + } + orderbook = await self.watch_multiple(url, messageHashes, self.extend(request, params), messageHashes, subscription) + return orderbook.limit() + + async def un_watch_order_book_for_symbols(self, symbols: List[str], params={}) -> Any: + """ + unWatches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://developers.binance.com/docs/binance-spot-api-docs/web-socket-streams#partial-book-depth-streams + https://developers.binance.com/docs/binance-spot-api-docs/web-socket-streams#diff-depth-stream + https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/Partial-Book-Depth-Streams + https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/Diff-Book-Depth-Streams + https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/Partial-Book-Depth-Streams + https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/Diff-Book-Depth-Streams + + :param str[] symbols: unified array of symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, False, True, True) + firstMarket = self.market(symbols[0]) + type = firstMarket['type'] + if firstMarket['contract']: + type = 'future' if firstMarket['linear'] else 'delivery' + name = 'depth' + streamHash = 'multipleOrderbook' + if symbols is not None: + streamHash += '::' + ','.join(symbols) + watchOrderBookRate = self.safe_string(self.options, 'watchOrderBookRate', '100') + subParams = [] + subMessageHashes = [] + messageHashes = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + market = self.market(symbol) + subMessageHashes.append('orderbook::' + symbol) + messageHashes.append('unsubscribe:orderbook:' + symbol) + subscriptionHash = market['lowercaseId'] + '@' + name + symbolHash = subscriptionHash + '@' + watchOrderBookRate + 'ms' + subParams.append(symbolHash) + messageHashesLength = len(subMessageHashes) + url = self.urls['api']['ws'][type] + '/' + self.stream(type, streamHash, messageHashesLength) + requestId = self.request_id(url) + request: dict = { + 'method': 'UNSUBSCRIBE', + 'params': subParams, + 'id': requestId, + } + subscription: dict = { + 'unsubscribe': True, + 'id': str(requestId), + 'symbols': symbols, + 'subMessageHashes': subMessageHashes, + 'messageHashes': messageHashes, + 'topic': 'orderbook', + } + return await self.watch_multiple(url, messageHashes, self.extend(request, params), messageHashes, subscription) + + async def un_watch_order_book(self, symbol: str, params={}) -> Any: + """ + unWatches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://developers.binance.com/docs/binance-spot-api-docs/web-socket-streams#partial-book-depth-streams + https://developers.binance.com/docs/binance-spot-api-docs/web-socket-streams#diff-depth-stream + https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/Partial-Book-Depth-Streams + https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/Diff-Book-Depth-Streams + https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/Partial-Book-Depth-Streams + https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/Diff-Book-Depth-Streams + + :param str symbol: unified array of symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + return await self.un_watch_order_book_for_symbols([symbol], params) + + async def fetch_order_book_ws(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://developers.binance.com/docs/binance-spot-api-docs/websocket-api/market-data-requests#order-book + https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/websocket-api/Order-Book + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + payload: dict = { + 'symbol': market['id'], + } + if limit is not None: + payload['limit'] = limit + marketType = self.get_market_type('fetchOrderBookWs', market, params) + if marketType != 'future': + raise BadRequest(self.id + ' fetchOrderBookWs only supports swap markets') + url = self.urls['api']['ws']['ws-api'][marketType] + requestId = self.request_id(url) + messageHash = str(requestId) + returnRateLimits = False + returnRateLimits, params = self.handle_option_and_params(params, 'createOrderWs', 'returnRateLimits', False) + payload['returnRateLimits'] = returnRateLimits + params = self.omit(params, 'test') + message: dict = { + 'id': messageHash, + 'method': 'depth', + 'params': self.sign_params(self.extend(payload, params)), + } + subscription: dict = { + 'method': self.handle_fetch_order_book, + } + orderbook = await self.watch(url, messageHash, message, messageHash, subscription) + orderbook['symbol'] = market['symbol'] + return orderbook + + def handle_fetch_order_book(self, client: Client, message): + # + # { + # "id":"51e2affb-0aba-4821-ba75-f2625006eb43", + # "status":200, + # "result":{ + # "lastUpdateId":1027024, + # "E":1589436922972, + # "T":1589436922959, + # "bids":[ + # [ + # "4.00000000", + # "431.00000000" + # ] + # ], + # "asks":[ + # [ + # "4.00000200", + # "12.00000000" + # ] + # ] + # } + # } + # + messageHash = self.safe_string(message, 'id') + result = self.safe_dict(message, 'result') + timestamp = self.safe_integer(result, 'T') + orderbook = self.parse_order_book(result, None, timestamp) + orderbook['nonce'] = self.safe_integer_2(result, 'lastUpdateId', 'u') + client.resolve(orderbook, messageHash) + + async def fetch_order_book_snapshot(self, client, message, subscription): + symbol = self.safe_string(subscription, 'symbol') + messageHash = 'orderbook::' + symbol + try: + defaultLimit = self.safe_integer(self.options, 'watchOrderBookLimit', 1000) + type = self.safe_value(subscription, 'type') + limit = self.safe_integer(subscription, 'limit', defaultLimit) + params = self.safe_value(subscription, 'params') + # 3. Get a depth snapshot from https://www.binance.com/api/v1/depth?symbol=BNBBTC&limit=1000 . + # todo: self is a synch blocking call - make it async + # default 100, max 1000, valid limits 5, 10, 20, 50, 100, 500, 1000 + snapshot = await self.fetch_rest_order_book_safe(symbol, limit, params) + if self.safe_value(self.orderbooks, symbol) is None: + # if the orderbook is dropped before the snapshot is received + return + orderbook = self.orderbooks[symbol] + orderbook.reset(snapshot) + # unroll the accumulated deltas + messages = orderbook.cache + orderbook.cache = [] + for i in range(0, len(messages)): + messageItem = messages[i] + U = self.safe_integer(messageItem, 'U') + u = self.safe_integer(messageItem, 'u') + pu = self.safe_integer(messageItem, 'pu') + if type == 'future': + # 4. Drop any event where u is < lastUpdateId in the snapshot + if u < orderbook['nonce']: + continue + # 5. The first processed event should have U <= lastUpdateId AND u >= lastUpdateId + if (U <= orderbook['nonce']) and (u >= orderbook['nonce']) or (pu == orderbook['nonce']): + self.handle_order_book_message(client, messageItem, orderbook) + else: + # 4. Drop any event where u is <= lastUpdateId in the snapshot + if u <= orderbook['nonce']: + continue + # 5. The first processed event should have U <= lastUpdateId+1 AND u >= lastUpdateId+1 + if ((U - 1) <= orderbook['nonce']) and ((u - 1) >= orderbook['nonce']): + self.handle_order_book_message(client, messageItem, orderbook) + self.orderbooks[symbol] = orderbook + client.resolve(orderbook, messageHash) + except Exception as e: + del client.subscriptions[messageHash] + client.reject(e, messageHash) + + def handle_delta(self, bookside, delta): + price = self.safe_float(delta, 0) + amount = self.safe_float(delta, 1) + bookside.store(price, amount) + + def handle_deltas(self, bookside, deltas): + for i in range(0, len(deltas)): + self.handle_delta(bookside, deltas[i]) + + def handle_order_book_message(self, client: Client, message, orderbook): + u = self.safe_integer(message, 'u') + self.handle_deltas(orderbook['asks'], self.safe_value(message, 'a', [])) + self.handle_deltas(orderbook['bids'], self.safe_value(message, 'b', [])) + orderbook['nonce'] = u + timestamp = self.safe_integer(message, 'E') + orderbook['timestamp'] = timestamp + orderbook['datetime'] = self.iso8601(timestamp) + return orderbook + + def handle_order_book(self, client: Client, message): + # + # initial snapshot is fetched with ccxt's fetchOrderBook + # the feed does not include a snapshot, just the deltas + # + # { + # "e": "depthUpdate", # Event type + # "E": 1577554482280, # Event time + # "s": "BNBBTC", # Symbol + # "U": 157, # First update ID in event + # "u": 160, # Final update ID in event + # "b": [ # bids + # ["0.0024", "10"], # price, size + # ], + # "a": [ # asks + # ["0.0026", "100"], # price, size + # ] + # } + # + isSpot = self.is_spot_url(client) + marketType = 'spot' if (isSpot) else 'swap' + marketId = self.safe_string(message, 's') + market = self.safe_market(marketId, None, None, marketType) + symbol = market['symbol'] + messageHash = 'orderbook::' + symbol + if not (symbol in self.orderbooks): + # + # https://github.com/ccxt/ccxt/issues/6672 + # + # Sometimes Binance sends the first delta before the subscription + # confirmation arrives. At that point the orderbook is not + # initialized yet and the snapshot has not been requested yet + # therefore it is safe to drop these premature messages. + # + return + orderbook = self.orderbooks[symbol] + nonce = self.safe_integer(orderbook, 'nonce') + if nonce is None: + # 2. Buffer the events you receive from the stream. + orderbook.cache.append(message) + else: + try: + U = self.safe_integer(message, 'U') + u = self.safe_integer(message, 'u') + pu = self.safe_integer(message, 'pu') + if pu is None: + # spot + # 4. Drop any event where u is <= lastUpdateId in the snapshot + if u > orderbook['nonce']: + timestamp = self.safe_integer(orderbook, 'timestamp') + conditional = None + if timestamp is None: + # 5. The first processed event should have U <= lastUpdateId+1 AND u >= lastUpdateId+1 + conditional = ((U - 1) <= orderbook['nonce']) and ((u - 1) >= orderbook['nonce']) + else: + # 6. While listening to the stream, each new event's U should be equal to the previous event's u+1. + conditional = ((U - 1) == orderbook['nonce']) + if conditional: + self.handle_order_book_message(client, message, orderbook) + if nonce < orderbook['nonce']: + client.resolve(orderbook, messageHash) + else: + checksum = self.handle_option('watchOrderBook', 'checksum', True) + if checksum: + # todo: client.reject from handleOrderBookMessage properly + raise ChecksumError(self.id + ' ' + self.orderbook_checksum_message(symbol)) + else: + # future + # 4. Drop any event where u is < lastUpdateId in the snapshot + if u >= orderbook['nonce']: + # 5. The first processed event should have U <= lastUpdateId AND u >= lastUpdateId + # 6. While listening to the stream, each new event's pu should be equal to the previous event's u, otherwise initialize the process from step 3 + if (U <= orderbook['nonce']) or (pu == orderbook['nonce']): + self.handle_order_book_message(client, message, orderbook) + if nonce <= orderbook['nonce']: + client.resolve(orderbook, messageHash) + else: + checksum = self.handle_option('watchOrderBook', 'checksum', True) + if checksum: + # todo: client.reject from handleOrderBookMessage properly + raise ChecksumError(self.id + ' ' + self.orderbook_checksum_message(symbol)) + except Exception as e: + del self.orderbooks[symbol] + del client.subscriptions[messageHash] + client.reject(e, messageHash) + + def handle_order_book_subscription(self, client: Client, message, subscription): + defaultLimit = self.safe_integer(self.options, 'watchOrderBookLimit', 1000) + # messageHash = self.safe_string(subscription, 'messageHash') + symbolOfSubscription = self.safe_string(subscription, 'symbol') # watchOrderBook + symbols = self.safe_value(subscription, 'symbols', [symbolOfSubscription]) # watchOrderBookForSymbols + limit = self.safe_integer(subscription, 'limit', defaultLimit) + # handle list of symbols + for i in range(0, len(symbols)): + symbol = symbols[i] + if symbol in self.orderbooks: + del self.orderbooks[symbol] + self.orderbooks[symbol] = self.order_book({}, limit) + subscription = self.extend(subscription, {'symbol': symbol}) + # fetch the snapshot in a separate async call + self.spawn(self.fetch_order_book_snapshot, client, message, subscription) + + def handle_subscription_status(self, client: Client, message): + # + # { + # "result": null, + # "id": 1574649734450 + # } + # + id = self.safe_string(message, 'id') + subscriptionsById = self.index_by(client.subscriptions, 'id') + subscription = self.safe_value(subscriptionsById, id, {}) + method = self.safe_value(subscription, 'method') + if method is not None: + method(client, message, subscription) + isUnSubMessage = self.safe_bool(subscription, 'unsubscribe', False) + if isUnSubMessage: + self.handle_un_subscription(client, subscription) + return message + + def handle_un_subscription(self, client: Client, subscription: dict): + messageHashes = self.safe_list(subscription, 'messageHashes', []) + subMessageHashes = self.safe_list(subscription, 'subMessageHashes', []) + for j in range(0, len(messageHashes)): + unsubHash = messageHashes[j] + subHash = subMessageHashes[j] + self.clean_unsubscription(client, subHash, unsubHash) + self.clean_cache(subscription) + + async def watch_trades_for_symbols(self, symbols: List[str], since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a list of symbols + + https://developers.binance.com/docs/binance-spot-api-docs/websocket-api/market-data-requests#aggregate-trades + https://developers.binance.com/docs/binance-spot-api-docs/websocket-api/market-data-requests#recent-trades + https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/Aggregate-Trade-Streams + https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/Aggregate-Trade-Streams + + :param str[] symbols: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.name]: the name of the method to call, 'trade' or 'aggTrade', default is 'trade' + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, False, True, True) + streamHash = 'multipleTrades' + if symbols is not None: + symbolsLength = len(symbols) + if symbolsLength > 200: + raise BadRequest(self.id + ' watchTradesForSymbols() accepts 200 symbols at most. To watch more symbols call watchTradesForSymbols() multiple times') + streamHash += '::' + ','.join(symbols) + name = None + name, params = self.handle_option_and_params(params, 'watchTradesForSymbols', 'name', 'trade') + params = self.omit(params, 'callerMethodName') + firstMarket = self.market(symbols[0]) + type = firstMarket['type'] + if firstMarket['contract']: + type = 'future' if firstMarket['linear'] else 'delivery' + messageHashes = [] + subParams = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + market = self.market(symbol) + messageHashes.append('trade::' + symbol) + rawHash = market['lowercaseId'] + '@' + name + subParams.append(rawHash) + query = self.omit(params, 'type') + subParamsLength = len(subParams) + url = self.urls['api']['ws'][type] + '/' + self.stream(type, streamHash, subParamsLength) + requestId = self.request_id(url) + request: dict = { + 'method': 'SUBSCRIBE', + 'params': subParams, + 'id': requestId, + } + subscribe: dict = { + 'id': requestId, + } + trades = await self.watch_multiple(url, messageHashes, self.extend(request, query), messageHashes, subscribe) + if self.newUpdates: + first = self.safe_value(trades, 0) + tradeSymbol = self.safe_string(first, 'symbol') + limit = trades.getLimit(tradeSymbol, limit) + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + async def un_watch_trades_for_symbols(self, symbols: List[str], params={}) -> Any: + """ + unsubscribes from the trades channel + + https://developers.binance.com/docs/binance-spot-api-docs/websocket-api/market-data-requests#aggregate-trades + https://developers.binance.com/docs/binance-spot-api-docs/websocket-api/market-data-requests#recent-trades + https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/Aggregate-Trade-Streams + https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/Aggregate-Trade-Streams + + :param str[] symbols: unified symbol of the market to fetch trades for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.name]: the name of the method to call, 'trade' or 'aggTrade', default is 'trade' + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, False, True, True) + streamHash = 'multipleTrades' + if symbols is not None: + symbolsLength = len(symbols) + if symbolsLength > 200: + raise BadRequest(self.id + ' watchTradesForSymbols() accepts 200 symbols at most. To watch more symbols call watchTradesForSymbols() multiple times') + streamHash += '::' + ','.join(symbols) + name = None + name, params = self.handle_option_and_params(params, 'watchTradesForSymbols', 'name', 'trade') + params = self.omit(params, 'callerMethodName') + firstMarket = self.market(symbols[0]) + type = firstMarket['type'] + if firstMarket['contract']: + type = 'future' if firstMarket['linear'] else 'delivery' + subMessageHashes = [] + subParams = [] + messageHashes = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + market = self.market(symbol) + subMessageHashes.append('trade::' + symbol) + messageHashes.append('unsubscribe:trade:' + symbol) + rawHash = market['lowercaseId'] + '@' + name + subParams.append(rawHash) + query = self.omit(params, 'type') + subParamsLength = len(subParams) + url = self.urls['api']['ws'][type] + '/' + self.stream(type, streamHash, subParamsLength) + requestId = self.request_id(url) + request: dict = { + 'method': 'UNSUBSCRIBE', + 'params': subParams, + 'id': requestId, + } + subscription: dict = { + 'unsubscribe': True, + 'id': str(requestId), + 'subMessageHashes': subMessageHashes, + 'messageHashes': messageHashes, + 'symbols': symbols, + 'topic': 'trades', + } + return await self.watch_multiple(url, messageHashes, self.extend(request, query), messageHashes, subscription) + + async def un_watch_trades(self, symbol: str, params={}) -> Any: + """ + unsubscribes from the trades channel + + https://developers.binance.com/docs/binance-spot-api-docs/websocket-api/market-data-requests#aggregate-trades + https://developers.binance.com/docs/binance-spot-api-docs/websocket-api/market-data-requests#recent-trades + https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/Aggregate-Trade-Streams + https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/Aggregate-Trade-Streams + + :param str symbol: unified symbol of the market to fetch trades for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.name]: the name of the method to call, 'trade' or 'aggTrade', default is 'trade' + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + return await self.un_watch_trades_for_symbols([symbol], params) + + async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://developers.binance.com/docs/binance-spot-api-docs/websocket-api/market-data-requests#aggregate-trades + https://developers.binance.com/docs/binance-spot-api-docs/websocket-api/market-data-requests#recent-trades + https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/Aggregate-Trade-Streams + https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/Aggregate-Trade-Streams + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.name]: the name of the method to call, 'trade' or 'aggTrade', default is 'trade' + :returns dict[]: a list of `trade structures ` + """ + params['callerMethodName'] = 'watchTrades' + return await self.watch_trades_for_symbols([symbol], since, limit, params) + + def parse_ws_trade(self, trade, market=None) -> Trade: + # + # public watchTrades + # + # { + # "e": "trade", # event type + # "E": 1579481530911, # event time + # "s": "ETHBTC", # symbol + # "t": 158410082, # trade id + # "p": "0.01914100", # price + # "q": "0.00700000", # quantity + # "b": 586187049, # buyer order id + # "a": 586186710, # seller order id + # "T": 1579481530910, # trade time + # "m": False, # is the buyer the market maker + # "M": True # binance docs say it should be ignored + # } + # + # { + # "e": "aggTrade", # Event type + # "E": 123456789, # Event time + # "s": "BNBBTC", # Symbol + # "a": 12345, # Aggregate trade ID + # "p": "0.001", # Price + # "q": "100", # Quantity + # "f": 100, # First trade ID + # "l": 105, # Last trade ID + # "T": 123456785, # Trade time + # "m": True, # Is the buyer the market maker? + # "M": True # Ignore + # } + # + # private watchMyTrades spot + # + # { + # "e": "executionReport", + # "E": 1611063861489, + # "s": "BNBUSDT", + # "c": "m4M6AD5MF3b1ERe65l4SPq", + # "S": "BUY", + # "o": "MARKET", + # "f": "GTC", + # "q": "2.00000000", + # "p": "0.00000000", + # "P": "0.00000000", + # "F": "0.00000000", + # "g": -1, + # "C": '', + # "x": "TRADE", + # "X": "PARTIALLY_FILLED", + # "r": "NONE", + # "i": 1296882607, + # "l": "0.33200000", + # "z": "0.33200000", + # "L": "46.86600000", + # "n": "0.00033200", + # "N": "BNB", + # "T": 1611063861488, + # "t": 109747654, + # "I": 2696953381, + # "w": False, + # "m": False, + # "M": True, + # "O": 1611063861488, + # "Z": "15.55951200", + # "Y": "15.55951200", + # "Q": "0.00000000" + # } + # + # private watchMyTrades future/delivery + # + # { + # "s": "BTCUSDT", + # "c": "pb2jD6ZQHpfzSdUac8VqMK", + # "S": "SELL", + # "o": "MARKET", + # "f": "GTC", + # "q": "0.001", + # "p": "0", + # "ap": "33468.46000", + # "sp": "0", + # "x": "TRADE", + # "X": "FILLED", + # "i": 13351197194, + # "l": "0.001", + # "z": "0.001", + # "L": "33468.46", + # "n": "0.00027086", + # "N": "BNB", + # "T": 1612095165362, + # "t": 458032604, + # "b": "0", + # "a": "0", + # "m": False, + # "R": False, + # "wt": "CONTRACT_PRICE", + # "ot": "MARKET", + # "ps": "BOTH", + # "cp": False, + # "rp": "0.00335000", + # "pP": False, + # "si": 0, + # "ss": 0 + # } + # + executionType = self.safe_string(trade, 'x') + isTradeExecution = (executionType == 'TRADE') + if not isTradeExecution: + return self.parse_trade(trade, market) + id = self.safe_string_2(trade, 't', 'a') + timestamp = self.safe_integer(trade, 'T') + price = self.safe_string_2(trade, 'L', 'p') + amount = self.safe_string(trade, 'q') + if isTradeExecution: + amount = self.safe_string(trade, 'l', amount) + cost = self.safe_string(trade, 'Y') + if cost is None: + if (price is not None) and (amount is not None): + cost = Precise.string_mul(price, amount) + marketId = self.safe_string(trade, 's') + marketType = 'contract' if ('ps' in trade) else 'spot' + symbol = self.safe_symbol(marketId, None, None, marketType) + side = self.safe_string_lower(trade, 'S') + takerOrMaker = None + orderId = self.safe_string(trade, 'i') + if 'm' in trade: + if side is None: + side = 'sell' if trade['m'] else 'buy' # self is reversed intentionally + takerOrMaker = 'maker' if trade['m'] else 'taker' + fee = None + feeCost = self.safe_string(trade, 'n') + if feeCost is not None: + feeCurrencyId = self.safe_string(trade, 'N') + feeCurrencyCode = self.safe_currency_code(feeCurrencyId) + fee = { + 'cost': feeCost, + 'currency': feeCurrencyCode, + } + type = self.safe_string_lower(trade, 'o') + return self.safe_trade({ + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'id': id, + 'order': orderId, + 'type': type, + 'takerOrMaker': takerOrMaker, + 'side': side, + 'price': price, + 'amount': amount, + 'cost': cost, + 'fee': fee, + }) + + def handle_trade(self, client: Client, message): + # the trade streams push raw trade information in real-time + # each trade has a unique buyer and seller + isSpot = self.is_spot_url(client) + marketType = 'spot' if (isSpot) else 'contract' + marketId = self.safe_string(message, 's') + market = self.safe_market(marketId, None, None, marketType) + symbol = market['symbol'] + messageHash = 'trade::' + symbol + trade = self.parse_ws_trade(message, market) + tradesArray = self.safe_value(self.trades, symbol) + if tradesArray is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + tradesArray = ArrayCache(limit) + tradesArray.append(trade) + self.trades[symbol] = tradesArray + client.resolve(tradesArray, messageHash) + + async def watch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + watches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://developers.binance.com/docs/binance-spot-api-docs/websocket-api/market-data-requests#klines + https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/Kline-Candlestick-Streams + https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/Kline-Candlestick-Streams + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param dict [params.timezone]: if provided, kline intervals are interpreted in that timezone instead of UTC, example '+08:00' + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + params['callerMethodName'] = 'watchOHLCV' + result = await self.watch_ohlcv_for_symbols([[symbol, timeframe]], since, limit, params) + return result[symbol][timeframe] + + async def watch_ohlcv_for_symbols(self, symbolsAndTimeframes: List[List[str]], since: Int = None, limit: Int = None, params={}): + """ + watches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://developers.binance.com/docs/binance-spot-api-docs/websocket-api/market-data-requests#klines + https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/Kline-Candlestick-Streams + https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/Kline-Candlestick-Streams + + :param str[][] symbolsAndTimeframes: array of arrays containing unified symbols and timeframes to fetch OHLCV data for, example [['BTC/USDT', '1m'], ['LTC/USDT', '5m']] + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param dict [params.timezone]: if provided, kline intervals are interpreted in that timezone instead of UTC, example '+08:00' + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + klineType = None + klineType, params = self.handle_param_string_2(params, 'channel', 'name', 'kline') + symbols = self.get_list_from_object_values(symbolsAndTimeframes, 0) + marketSymbols = self.market_symbols(symbols, None, False, False, True) + firstMarket = self.market(marketSymbols[0]) + type = firstMarket['type'] + if firstMarket['contract']: + type = 'future' if firstMarket['linear'] else 'delivery' + isSpot = (type == 'spot') + timezone = None + timezone, params = self.handle_param_string(params, 'timezone', None) + isUtc8 = (timezone is not None) and ((timezone == '+08:00') or Precise.string_eq(timezone, '8')) + rawHashes = [] + messageHashes = [] + for i in range(0, len(symbolsAndTimeframes)): + symAndTf = symbolsAndTimeframes[i] + symbolString = symAndTf[0] + timeframeString = symAndTf[1] + interval = self.safe_string(self.timeframes, timeframeString, timeframeString) + market = self.market(symbolString) + marketId = market['lowercaseId'] + if klineType == 'indexPriceKline': + # weird behavior for index price kline we can't use the perp suffix + marketId = marketId.replace('_perp', '') + shouldUseUTC8 = (isUtc8 and isSpot) + suffix = '@+08:00' + utcSuffix = suffix if shouldUseUTC8 else '' + rawHashes.append(marketId + '@' + klineType + '_' + interval + utcSuffix) + messageHashes.append('ohlcv::' + market['symbol'] + '::' + timeframeString) + url = self.urls['api']['ws'][type] + '/' + self.stream(type, 'multipleOHLCV') + requestId = self.request_id(url) + request = { + 'method': 'SUBSCRIBE', + 'params': rawHashes, + 'id': requestId, + } + subscribe = { + 'id': requestId, + } + params = self.omit(params, 'callerMethodName') + res = await self.watch_multiple(url, messageHashes, self.extend(request, params), messageHashes, subscribe) + symbol, timeframe, candles = res + if self.newUpdates: + limit = candles.getLimit(symbol, limit) + filtered = self.filter_by_since_limit(candles, since, limit, 0, True) + return self.create_ohlcv_object(symbol, timeframe, filtered) + + async def un_watch_ohlcv_for_symbols(self, symbolsAndTimeframes: List[List[str]], params={}) -> Any: + """ + unWatches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://developers.binance.com/docs/binance-spot-api-docs/websocket-api/market-data-requests#klines + https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/Kline-Candlestick-Streams + https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/Kline-Candlestick-Streams + + :param str[][] symbolsAndTimeframes: array of arrays containing unified symbols and timeframes to fetch OHLCV data for, example [['BTC/USDT', '1m'], ['LTC/USDT', '5m']] + :param dict [params]: extra parameters specific to the exchange API endpoint + :param dict [params.timezone]: if provided, kline intervals are interpreted in that timezone instead of UTC, example '+08:00' + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + klineType = None + klineType, params = self.handle_param_string_2(params, 'channel', 'name', 'kline') + symbols = self.get_list_from_object_values(symbolsAndTimeframes, 0) + marketSymbols = self.market_symbols(symbols, None, False, False, True) + firstMarket = self.market(marketSymbols[0]) + type = firstMarket['type'] + if firstMarket['contract']: + type = 'future' if firstMarket['linear'] else 'delivery' + isSpot = (type == 'spot') + timezone = None + timezone, params = self.handle_param_string(params, 'timezone', None) + isUtc8 = (timezone is not None) and ((timezone == '+08:00') or Precise.string_eq(timezone, '8')) + rawHashes = [] + subMessageHashes = [] + messageHashes = [] + for i in range(0, len(symbolsAndTimeframes)): + symAndTf = symbolsAndTimeframes[i] + symbolString = symAndTf[0] + timeframeString = symAndTf[1] + interval = self.safe_string(self.timeframes, timeframeString, timeframeString) + market = self.market(symbolString) + marketId = market['lowercaseId'] + if klineType == 'indexPriceKline': + # weird behavior for index price kline we can't use the perp suffix + marketId = marketId.replace('_perp', '') + shouldUseUTC8 = (isUtc8 and isSpot) + suffix = '@+08:00' + utcSuffix = suffix if shouldUseUTC8 else '' + rawHashes.append(marketId + '@' + klineType + '_' + interval + utcSuffix) + subMessageHashes.append('ohlcv::' + market['symbol'] + '::' + timeframeString) + messageHashes.append('unsubscribe::ohlcv::' + market['symbol'] + '::' + timeframeString) + url = self.urls['api']['ws'][type] + '/' + self.stream(type, 'multipleOHLCV') + requestId = self.request_id(url) + request = { + 'method': 'UNSUBSCRIBE', + 'params': rawHashes, + 'id': requestId, + } + subscribe = { + 'unsubscribe': True, + 'id': str(requestId), + 'symbols': symbols, + 'symbolsAndTimeframes': symbolsAndTimeframes, + 'subMessageHashes': subMessageHashes, + 'messageHashes': messageHashes, + 'topic': 'ohlcv', + } + params = self.omit(params, 'callerMethodName') + return await self.watch_multiple(url, messageHashes, self.extend(request, params), messageHashes, subscribe) + + async def un_watch_ohlcv(self, symbol: str, timeframe: str = '1m', params={}) -> Any: + """ + unWatches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://developers.binance.com/docs/binance-spot-api-docs/websocket-api/market-data-requests#klines + https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/Kline-Candlestick-Streams + https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/Kline-Candlestick-Streams + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param dict [params]: extra parameters specific to the exchange API endpoint + :param dict [params.timezone]: if provided, kline intervals are interpreted in that timezone instead of UTC, example '+08:00' + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + params['callerMethodName'] = 'watchOHLCV' + return await self.un_watch_ohlcv_for_symbols([[symbol, timeframe]], params) + + def handle_ohlcv(self, client: Client, message): + # + # { + # "e": "kline", + # "E": 1579482921215, + # "s": "ETHBTC", + # "k": { + # "t": 1579482900000, + # "T": 1579482959999, + # "s": "ETHBTC", + # "i": "1m", + # "f": 158411535, + # "L": 158411550, + # "o": "0.01913200", + # "c": "0.01913500", + # "h": "0.01913700", + # "l": "0.01913200", + # "v": "5.08400000", + # "n": 16, + # "x": False, + # "q": "0.09728060", + # "V": "3.30200000", + # "Q": "0.06318500", + # "B": "0" + # } + # } + # + event = self.safe_string(message, 'e') + eventMap: dict = { + 'indexPrice_kline': 'indexPriceKline', + 'markPrice_kline': 'markPriceKline', + } + event = self.safe_string(eventMap, event, event) + kline = self.safe_value(message, 'k') + marketId = self.safe_string_2(kline, 's', 'ps') + if event == 'indexPriceKline': + # indexPriceKline doesn't have the _PERP suffix + marketId = self.safe_string(message, 'ps') + interval = self.safe_string(kline, 'i') + # use a reverse lookup in a static map instead + unifiedTimeframe = self.find_timeframe(interval) + parsed = [ + self.safe_integer(kline, 't'), + self.safe_float(kline, 'o'), + self.safe_float(kline, 'h'), + self.safe_float(kline, 'l'), + self.safe_float(kline, 'c'), + self.safe_float(kline, 'v'), + ] + isSpot = self.is_spot_url(client) + marketType = 'spot' if (isSpot) else 'contract' + symbol = self.safe_symbol(marketId, None, None, marketType) + messageHash = 'ohlcv::' + symbol + '::' + unifiedTimeframe + self.ohlcvs[symbol] = self.safe_value(self.ohlcvs, symbol, {}) + stored = self.safe_value(self.ohlcvs[symbol], unifiedTimeframe) + if stored is None: + limit = self.safe_integer(self.options, 'OHLCVLimit', 1000) + stored = ArrayCacheByTimestamp(limit) + self.ohlcvs[symbol][unifiedTimeframe] = stored + stored.append(parsed) + resolveData = [symbol, unifiedTimeframe, stored] + client.resolve(resolveData, messageHash) + + async def fetch_ticker_ws(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.method]: method to use can be ticker.price or ticker.book + :param boolean [params.returnRateLimits]: return the rate limits for the exchange + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + payload: dict = { + 'symbol': market['id'], + } + type = self.get_market_type('fetchTickerWs', market, params) + if type != 'future': + raise BadRequest(self.id + ' fetchTickerWs only supports swap markets') + url = self.urls['api']['ws']['ws-api'][type] + requestId = self.request_id(url) + messageHash = str(requestId) + subscription: dict = { + 'method': self.handle_ticker_ws, + } + returnRateLimits = False + returnRateLimits, params = self.handle_option_and_params(params, 'fetchTickerWs', 'returnRateLimits', False) + payload['returnRateLimits'] = returnRateLimits + params = self.omit(params, 'test') + method = None + method, params = self.handle_option_and_params(params, 'fetchTickerWs', 'method', 'ticker.book') + message: dict = { + 'id': messageHash, + 'method': method, + 'params': self.sign_params(self.extend(payload, params)), + } + ticker = await self.watch(url, messageHash, message, messageHash, subscription) + return ticker + + async def fetch_ohlcv_ws(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + query historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://developers.binance.com/docs/binance-spot-api-docs/websocket-api/market-data-requests#klines + + :param str symbol: unified symbol of the market to query OHLCV data for + :param str timeframe: the length of time each candle represents + :param int since: timestamp in ms of the earliest candle to fetch + :param int limit: the maximum amount of candles to fetch + :param dict params: extra parameters specific to the exchange API endpoint + :param int params['until']: timestamp in ms of the earliest candle to fetch + + EXCHANGE SPECIFIC PARAMETERS + :param str params['timeZone']: default=0(UTC) + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + marketType = self.get_market_type('fetchOHLCVWs', market, params) + if marketType != 'spot' and marketType != 'future': + raise BadRequest(self.id + ' fetchOHLCVWs only supports spot or swap markets') + url = self.urls['api']['ws']['ws-api'][marketType] + requestId = self.request_id(url) + messageHash = str(requestId) + returnRateLimits = False + returnRateLimits, params = self.handle_option_and_params(params, 'fetchOHLCVWs', 'returnRateLimits', False) + payload: dict = { + 'symbol': self.market_id(symbol), + 'returnRateLimits': returnRateLimits, + 'interval': self.timeframes[timeframe], + } + until = self.safe_integer(params, 'until') + params = self.omit(params, 'until') + if since is not None: + payload['startTime'] = since + if limit is not None: + payload['limit'] = limit + if until is not None: + payload['endTime'] = until + message: dict = { + 'id': messageHash, + 'method': 'klines', + 'params': self.extend(payload, params), + } + subscription: dict = { + 'method': self.handle_fetch_ohlcv, + } + return await self.watch(url, messageHash, message, messageHash, subscription) + + def handle_fetch_ohlcv(self, client: Client, message): + # + # { + # "id": "1dbbeb56-8eea-466a-8f6e-86bdcfa2fc0b", + # "status": 200, + # "result": [ + # [ + # 1655971200000, # Kline open time + # "0.01086000", # Open price + # "0.01086600", # High price + # "0.01083600", # Low price + # "0.01083800", # Close price + # "2290.53800000", # Volume + # 1655974799999, # Kline close time + # "24.85074442", # Quote asset volume + # 2283, # Number of trades + # "1171.64000000", # Taker buy base asset volume + # "12.71225884", # Taker buy quote asset volume + # "0" # Unused field, ignore + # ] + # ], + # "rateLimits": [ + # { + # "rateLimitType": "REQUEST_WEIGHT", + # "interval": "MINUTE", + # "intervalNum": 1, + # "limit": 6000, + # "count": 2 + # } + # ] + # } + # + result = self.safe_list(message, 'result') + parsed = self.parse_ohlcvs(result) + # use a reverse lookup in a static map instead + messageHash = self.safe_string(message, 'id') + client.resolve(parsed, messageHash) + + async def watch_ticker(self, symbol: str, params={}) -> Ticker: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://developers.binance.com/docs/binance-spot-api-docs/web-socket-streams#individual-symbol-mini-ticker-stream + https://developers.binance.com/docs/binance-spot-api-docs/web-socket-streams#all-market-mini-tickers-stream + https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/Individual-Symbol-Ticker-Streams + https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/All-Market-Mini-Tickers-Stream + https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/All-Market-Mini-Tickers-Stream + https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/Individual-Symbol-Ticker-Streams + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.name]: stream to use can be ticker or miniTicker + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbol = self.symbol(symbol) + tickers = await self.watch_tickers([symbol], self.extend(params, {'callerMethodName': 'watchTicker'})) + return tickers[symbol] + + async def watch_mark_price(self, symbol: str, params={}) -> Ticker: + """ + watches a mark price for a specific market + + https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/Mark-Price-Stream + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.use1sFreq]: *default is True* if set to True, the mark price will be updated every second, otherwise every 3 seconds + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbol = self.symbol(symbol) + tickers = await self.watch_mark_prices([symbol], self.extend(params, {'callerMethodName': 'watchMarkPrice'})) + return tickers[symbol] + + async def watch_mark_prices(self, symbols: Strings = None, params={}) -> Tickers: + """ + watches the mark price for all markets + + https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/Mark-Price-Stream-for-All-market + + :param str[] symbols: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.use1sFreq]: *default is True* if set to True, the mark price will be updated every second, otherwise every 3 seconds + :returns dict: a `ticker structure ` + """ + channelName = None + # for now watchmarkPrice uses the same messageHash + # so it's impossible to watch both at the same time + # refactor self to use different messageHashes + channelName, params = self.handle_option_and_params(params, 'watchMarkPrices', 'name', 'markPrice') + newTickers = await self.watch_multi_ticker_helper('watchMarkPrices', channelName, symbols, params) + if self.newUpdates: + return newTickers + return self.filter_by_array(self.tickers, 'symbol', symbols) + + async def watch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list + + https://developers.binance.com/docs/binance-spot-api-docs/web-socket-streams#individual-symbol-mini-ticker-stream + https://developers.binance.com/docs/binance-spot-api-docs/web-socket-streams#all-market-mini-tickers-stream + https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/Individual-Symbol-Ticker-Streams + https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/All-Market-Mini-Tickers-Stream + https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/All-Market-Mini-Tickers-Stream + https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/Individual-Symbol-Ticker-Streams + + :param str[] symbols: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + channelName = None + channelName, params = self.handle_option_and_params(params, 'watchTickers', 'name', 'ticker') + if channelName == 'bookTicker': + raise BadRequest(self.id + ' deprecation notice - to subscribe for bids-asks, use watch_bids_asks() method instead') + newTickers = await self.watch_multi_ticker_helper('watchTickers', channelName, symbols, params) + if self.newUpdates: + return newTickers + return self.filter_by_array(self.tickers, 'symbol', symbols) + + async def un_watch_tickers(self, symbols: Strings = None, params={}) -> Any: + """ + unWatches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list + + https://developers.binance.com/docs/binance-spot-api-docs/web-socket-streams#individual-symbol-mini-ticker-stream + https://developers.binance.com/docs/binance-spot-api-docs/web-socket-streams#all-market-mini-tickers-stream + https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/Individual-Symbol-Ticker-Streams + https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/All-Market-Mini-Tickers-Stream + https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/All-Market-Mini-Tickers-Stream + https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/Individual-Symbol-Ticker-Streams + + :param str[] symbols: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + channelName = None + channelName, params = self.handle_option_and_params(params, 'watchTickers', 'name', 'ticker') + if channelName == 'bookTicker': + raise BadRequest(self.id + ' deprecation notice - to subscribe for bids-asks, use watch_bids_asks() method instead') + return await self.watch_multi_ticker_helper('unWatchTickers', channelName, symbols, params, True) + + async def un_watch_mark_prices(self, symbols: Strings = None, params={}) -> Any: + """ + unWatches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list + + https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/Mark-Price-Stream + + :param str[] symbols: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + channelName = None + channelName, params = self.handle_option_and_params(params, 'watchMarkPrices', 'name', 'markPrice') + await self.load_markets() + return await self.watch_multi_ticker_helper('unWatchMarkPrices', channelName, symbols, params, True) + + async def un_watch_mark_price(self, symbol: str, params={}) -> Any: + """ + unWatches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list + + https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/Mark-Price-Stream + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + return await self.un_watch_mark_prices([symbol], params) + + async def un_watch_ticker(self, symbol: str, params={}) -> Any: + """ + unWatches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list + + https://developers.binance.com/docs/binance-spot-api-docs/web-socket-streams#individual-symbol-mini-ticker-stream + https://developers.binance.com/docs/binance-spot-api-docs/web-socket-streams#all-market-mini-tickers-stream + https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/Individual-Symbol-Ticker-Streams + https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/All-Market-Mini-Tickers-Stream + https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/All-Market-Mini-Tickers-Stream + https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/Individual-Symbol-Ticker-Streams + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + return await self.un_watch_tickers([symbol], params) + + async def watch_bids_asks(self, symbols: Strings = None, params={}) -> Tickers: + """ + watches best bid & ask for symbols + + https://developers.binance.com/docs/binance-spot-api-docs/websocket-api/market-data-requests#symbol-order-book-ticker + https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/All-Book-Tickers-Stream + https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/All-Book-Tickers-Stream + + :param str[] symbols: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, True, False, True) + result = await self.watch_multi_ticker_helper('watchBidsAsks', 'bookTicker', symbols, params) + if self.newUpdates: + return result + return self.filter_by_array(self.bidsasks, 'symbol', symbols) + + async def watch_multi_ticker_helper(self, methodName, channelName: str, symbols: Strings = None, params={}, isUnsubscribe: bool = False): + await self.load_markets() + symbols = self.market_symbols(symbols, None, True, False, True) + isBidAsk = (channelName == 'bookTicker') + isMarkPrice = (channelName == 'markPrice') + use1sFreq = self.safe_bool(params, 'use1sFreq', True) + firstMarket = None + marketType = None + symbolsDefined = (symbols is not None) + if symbolsDefined: + firstMarket = self.market(symbols[0]) + defaultMarket = 'swap' if (isMarkPrice) else None + marketType, params = self.handle_market_type_and_params(methodName, firstMarket, params, defaultMarket) + subType = None + subType, params = self.handle_sub_type_and_params(methodName, firstMarket, params) + rawMarketType = None + if self.isLinear(marketType, subType): + rawMarketType = 'future' + elif self.isInverse(marketType, subType): + rawMarketType = 'delivery' + elif marketType == 'spot': + rawMarketType = marketType + else: + raise NotSupported(self.id + ' ' + methodName + '() does not support options markets') + if isMarkPrice and not self.in_array(marketType, ['swap', 'future']): + raise NotSupported(self.id + ' ' + methodName + '() does not support ' + marketType + ' markets yet') + subscriptionArgs = [] + messageHashes = [] + unsubscribeMessageHashes = [] + suffix = '' + if isMarkPrice: + suffix = '@1s' if (use1sFreq) else '' + unifiedPrefix: Str = None + if isBidAsk: + unifiedPrefix = 'bidask' + elif isMarkPrice: + unifiedPrefix = 'markPrice' + else: + unifiedPrefix = 'ticker' + if symbolsDefined: + for i in range(0, len(symbols)): + symbol = symbols[i] + market = self.market(symbol) + subscriptionArgs.append(market['lowercaseId'] + '@' + channelName + suffix) + messageHashes.append(unifiedPrefix + ':' + channelName + '@' + symbol) + if isUnsubscribe: + unsubscribeMessageHashes.append('unsubscribe::' + unifiedPrefix + ':' + channelName + '@' + symbol) + else: + if isBidAsk: + if marketType == 'spot': + raise ArgumentsRequired(self.id + ' ' + methodName + '() requires symbols for self channel for spot markets') + subscriptionArgs.append('!' + channelName) + elif isMarkPrice: + subscriptionArgs.append('!' + channelName + '@arr' + suffix) + else: + subscriptionArgs.append('!' + channelName + '@arr') + messageHashes.append(unifiedPrefix + 's:' + channelName) + unsubscribeMessageHashes.append('unsubscribe::' + channelName) + streamHash = channelName + if symbolsDefined: + streamHash = channelName + '::' + ','.join(symbols) + url = self.urls['api']['ws'][rawMarketType] + '/' + self.stream(rawMarketType, streamHash) + requestId = self.request_id(url) + request: dict = { + 'method': 'UNSUBSCRIBE' if isUnsubscribe else 'SUBSCRIBE', + 'params': subscriptionArgs, + 'id': requestId, + } + hashes = messageHashes + subscription: dict = { + 'id': requestId, + } + if isUnsubscribe: + subscription = { + 'unsubscribe': True, + 'id': str(requestId), + 'subMessageHashes': messageHashes, + 'messageHashes': unsubscribeMessageHashes, + 'symbols': symbols, + 'topic': 'ticker', + } + hashes = unsubscribeMessageHashes + result = await self.watch_multiple(url, hashes, self.deep_extend(request, params), hashes, subscription) + if isUnsubscribe: + return result + # for efficiency, we have two type of returned structure here - if symbols array was provided, then individual + # ticker dict comes in, otherwise all-tickers dict comes in + if not symbolsDefined: + return result + else: + newDict: dict = {} + newDict[result['symbol']] = result + return newDict + + def parse_ws_ticker(self, message, marketType): + # markPrice + # { + # "e": "markPriceUpdate", # Event type + # "E": 1562305380000, # Event time + # "s": "BTCUSDT", # Symbol + # "p": "11794.15000000", # Mark price + # "i": "11784.62659091", # Index price + # "P": "11784.25641265", # Estimated Settle Price, only useful in the last hour before the settlement starts + # "r": "0.00038167", # Funding rate + # "T": 1562306400000 # Next funding time + # } + # + # ticker + # { + # "e": "24hrTicker", # event type + # "E": 1579485598569, # event time + # "s": "ETHBTC", # symbol + # "p": "-0.00004000", # price change + # "P": "-0.209", # price change percent + # "w": "0.01920495", # weighted average price + # "x": "0.01916500", # the price of the first trade before the 24hr rolling window + # "c": "0.01912500", # last(closing) price + # "Q": "0.10400000", # last quantity + # "b": "0.01912200", # best bid + # "B": "4.10400000", # best bid quantity + # "a": "0.01912500", # best ask + # "A": "0.00100000", # best ask quantity + # "o": "0.01916500", # open price + # "h": "0.01956500", # high price + # "l": "0.01887700", # low price + # "v": "173518.11900000", # base volume + # "q": "3332.40703994", # quote volume + # "O": 1579399197842, # open time + # "C": 1579485597842, # close time + # "F": 158251292, # first trade id + # "L": 158414513, # last trade id + # "n": 163222, # total number of trades + # } + # + # miniTicker + # { + # "e": "24hrMiniTicker", + # "E": 1671617114585, + # "s": "MOBBUSD", + # "c": "0.95900000", + # "o": "0.91200000", + # "h": "1.04000000", + # "l": "0.89400000", + # "v": "2109995.32000000", + # "q": "2019254.05788000" + # } + # fetchTickerWs + # { + # "symbol":"BTCUSDT", + # "price":"72606.70", + # "time":1712526204284 + # } + # fetchTickerWs - ticker.book + # { + # "lastUpdateId":1027024, + # "symbol":"BTCUSDT", + # "bidPrice":"4.00000000", + # "bidQty":"431.00000000", + # "askPrice":"4.00000200", + # "askQty":"9.00000000", + # "time":1589437530011, + # } + # + marketId = self.safe_string_2(message, 's', 'symbol') + symbol = self.safe_symbol(marketId, None, None, marketType) + event = self.safe_string(message, 'e', 'bookTicker') + if event == '24hrTicker': + event = 'ticker' + if event == 'markPriceUpdate': + # handle self separately because some fields clash with the ticker fields + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': self.safe_integer(message, 'E'), + 'datetime': self.iso8601(self.safe_integer(message, 'E')), + 'info': message, + 'markPrice': self.safe_string(message, 'p'), + 'indexPrice': self.safe_string(message, 'i'), + }) + timestamp = None + if event == 'bookTicker': + # take the event timestamp, if available, for spot tickers it is not + timestamp = self.safe_integer_2(message, 'E', 'time') + else: + # take the timestamp of the closing price for candlestick streams + timestamp = self.safe_integer_n(message, ['C', 'E', 'time']) + market = self.safe_market(marketId, None, None, marketType) + last = self.safe_string_2(message, 'c', 'price') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(message, 'h'), + 'low': self.safe_string(message, 'l'), + 'bid': self.safe_string_2(message, 'b', 'bidPrice'), + 'bidVolume': self.safe_string_2(message, 'B', 'bidQty'), + 'ask': self.safe_string_2(message, 'a', 'askPrice'), + 'askVolume': self.safe_string_2(message, 'A', 'askQty'), + 'vwap': self.safe_string(message, 'w'), + 'open': self.safe_string(message, 'o'), + 'close': last, + 'last': last, + 'previousClose': self.safe_string(message, 'x'), # previous day close + 'change': self.safe_string(message, 'p'), + 'percentage': self.safe_string(message, 'P'), + 'average': None, + 'baseVolume': self.safe_string(message, 'v'), + 'quoteVolume': self.safe_string(message, 'q'), + 'info': message, + }, market) + + def handle_ticker_ws(self, client: Client, message): + # + # ticker.price + # { + # "id":"1", + # "status":200, + # "result":{ + # "symbol":"BTCUSDT", + # "price":"73178.50", + # "time":1712527052374 + # } + # } + # ticker.book + # { + # "id":"9d32157c-a556-4d27-9866-66760a174b57", + # "status":200, + # "result":{ + # "lastUpdateId":1027024, + # "symbol":"BTCUSDT", + # "bidPrice":"4.00000000", + # "bidQty":"431.00000000", + # "askPrice":"4.00000200", + # "askQty":"9.00000000", + # "time":1589437530011 # Transaction time + # } + # } + # + messageHash = self.safe_string(message, 'id') + result = self.safe_value(message, 'result', {}) + ticker = self.parse_ws_ticker(result, 'future') + client.resolve(ticker, messageHash) + + def handle_bids_asks(self, client: Client, message): + # + # arrives one symbol dict or array of symbol dicts + # + # { + # "u": 7488717758, + # "s": "BTCUSDT", + # "b": "28621.74000000", + # "B": "1.43278800", + # "a": "28621.75000000", + # "A": "2.52500800" + # } + # + self.handle_tickers_and_bids_asks(client, message, 'bidasks') + + def handle_tickers(self, client: Client, message): + # + # arrives one symbol dict or array of symbol dicts + # + # { + # "e": "24hrTicker", # event type + # "E": 1579485598569, # event time + # "s": "ETHBTC", # symbol + # "p": "-0.00004000", # price change + # "P": "-0.209", # price change percent + # "w": "0.01920495", # weighted average price + # "x": "0.01916500", # the price of the first trade before the 24hr rolling window + # "c": "0.01912500", # last(closing) price + # "Q": "0.10400000", # last quantity + # "b": "0.01912200", # best bid + # "B": "4.10400000", # best bid quantity + # "a": "0.01912500", # best ask + # "A": "0.00100000", # best ask quantity + # "o": "0.01916500", # open price + # "h": "0.01956500", # high price + # "l": "0.01887700", # low price + # "v": "173518.11900000", # base volume + # "q": "3332.40703994", # quote volume + # "O": 1579399197842, # open time + # "C": 1579485597842, # close time + # "F": 158251292, # first trade id + # "L": 158414513, # last trade id + # "n": 163222, # total number of trades + # } + # + self.handle_tickers_and_bids_asks(client, message, 'tickers') + + def handle_mark_prices(self, client: Client, message): + self.handle_tickers_and_bids_asks(client, message, 'markPrices') + + def handle_tickers_and_bids_asks(self, client: Client, message, methodType): + isSpot = self.is_spot_url(client) + marketType = 'spot' if (isSpot) else 'contract' + isBidAsk = (methodType == 'bidasks') + isMarkPrice = (methodType == 'markPrices') + unifiedPrefix: Str = None + if isBidAsk: + unifiedPrefix = 'bidask' + elif isMarkPrice: + unifiedPrefix = 'markPrice' + else: + unifiedPrefix = 'ticker' + channelName = None + resolvedMessageHashes = [] + rawTickers = [] + newTickers: dict = {} + if isinstance(message, list): + rawTickers = message + else: + rawTickers.append(message) + for i in range(0, len(rawTickers)): + ticker = rawTickers[i] + event = self.safe_string(ticker, 'e') + if isBidAsk: + event = 'bookTicker' # in `handleMessage`, bookTicker doesn't have identifier, so manually set here + channelName = self.safe_string(self.options['tickerChannelsMap'], event, event) + if channelName is None: + continue + parsedTicker = self.parse_ws_ticker(ticker, marketType) + symbol = parsedTicker['symbol'] + newTickers[symbol] = parsedTicker + if isBidAsk: + self.bidsasks[symbol] = parsedTicker + else: + self.tickers[symbol] = parsedTicker + messageHash = unifiedPrefix + ':' + channelName + '@' + symbol + resolvedMessageHashes.append(messageHash) + client.resolve(parsedTicker, messageHash) + # resolve batch endpoint + length = len(resolvedMessageHashes) + if length > 0: + batchMessageHash = unifiedPrefix + 's:' + channelName + client.resolve(newTickers, batchMessageHash) + + def sign_params(self, params={}): + self.check_required_credentials() + defaultRecvWindow = self.safe_integer(self.options, 'recvWindow') + if defaultRecvWindow is not None: + params['recvWindow'] = defaultRecvWindow + recvWindow = self.safe_integer(params, 'recvWindow') + if recvWindow is not None: + params['recvWindow'] = recvWindow + extendedParams = self.extend({ + 'timestamp': self.nonce(), + 'apiKey': self.apiKey, + }, params) + extendedParams = self.keysort(extendedParams) + query = self.rawencode(extendedParams) + signature = None + if self.secret.find('PRIVATE KEY') > -1: + if len(self.secret) > 120: + signature = self.rsa(query, self.secret, 'sha256') + else: + signature = self.eddsa(self.encode(query), self.secret, 'ed25519') + else: + signature = self.hmac(self.encode(query), self.encode(self.secret), hashlib.sha256) + extendedParams['signature'] = signature + return extendedParams + + async def ensure_user_data_stream_ws_subscribe_signature(self, marketType: str = 'spot'): + """ + watches best bid & ask for symbols + @param marketType {string} only support on 'spot' + + {@link https://developers.binance.com/docs/binance-spot-api-docs/websocket-api/user-data-stream-requests#subscribe-to-user-data-stream-through-signature-subscription-user_data Binance User Data Stream Documentation} + + :returns: Promise The subscription ID for the user data stream + """ + url = self.urls['api']['ws']['ws-api'][marketType] + client = self.client(url) + subscriptions = client.subscriptions + subscriptionsKeys = list(subscriptions.keys()) + accountType = self.get_account_type_from_subscriptions(subscriptionsKeys) + if accountType == marketType: + return + client.subscriptions[marketType] = True + requestId = self.request_id(url) + messageHash = str(requestId) + message: dict = { + 'id': messageHash, + 'method': 'userDataStream.subscribe.signature', + 'params': self.sign_params({}), + } + subscription: dict = { + 'id': messageHash, + 'method': self.handle_user_data_stream_subscribe, + 'subscription': marketType, + } + await self.watch(url, messageHash, message, messageHash, subscription) + + def handle_user_data_stream_subscribe(self, client: Client, message): + # + # { + # "id": 1, + # "status": 200, + # "result": { + # "subscriptionId": 0 + # } + # } + # + messageHash = self.safe_string(message, 'id') + subscriptions = client.subscriptions + subscriptionsKeys = list(subscriptions.keys()) + accountType = self.get_account_type_from_subscriptions(subscriptionsKeys) + result = self.safe_dict(message, 'result', {}) + subscriptionId = self.safe_integer(result, 'subscriptionId') + if subscriptionId is None: + del client.subscriptions[accountType] + client.reject(message, accountType) + client.resolve(message, messageHash) + + async def authenticate(self, params={}): + time = self.milliseconds() + type = None + type, params = self.handle_market_type_and_params('authenticate', None, params) + subType = None + subType, params = self.handle_sub_type_and_params('authenticate', None, params) + isPortfolioMargin = None + isPortfolioMargin, params = self.handle_option_and_params_2(params, 'authenticate', 'papi', 'portfolioMargin', False) + if self.isLinear(type, subType): + type = 'future' + elif self.isInverse(type, subType): + type = 'delivery' + # For spot use WebSocket API signature subscription + if type == 'spot': + await self.ensure_user_data_stream_ws_subscribe_signature('spot') + return + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('authenticate', params) + isIsolatedMargin = (marginMode == 'isolated') + isCrossMargin = (marginMode == 'cross') or (marginMode is None) + symbol = self.safe_string(params, 'symbol') + params = self.omit(params, 'symbol') + options = self.safe_value(self.options, type, {}) + lastAuthenticatedTime = self.safe_integer(options, 'lastAuthenticatedTime', 0) + listenKeyRefreshRate = self.safe_integer(self.options, 'listenKeyRefreshRate', 1200000) + delay = self.sum(listenKeyRefreshRate, 10000) + if time - lastAuthenticatedTime > delay: + response = None + if isPortfolioMargin: + response = await self.papiPostListenKey(params) + params = self.extend(params, {'portfolioMargin': True}) + elif type == 'future': + response = await self.fapiPrivatePostListenKey(params) + elif type == 'delivery': + response = await self.dapiPrivatePostListenKey(params) + elif type == 'margin' and isCrossMargin: + response = await self.sapiPostUserDataStream(params) + elif isIsolatedMargin: + if symbol is None: + raise ArgumentsRequired(self.id + ' authenticate() requires a symbol argument for isolated margin mode') + marketId = self.market_id(symbol) + params = self.extend(params, {'symbol': marketId}) + response = await self.sapiPostUserDataStreamIsolated(params) + else: + response = await self.publicPostUserDataStream(params) + self.options[type] = self.extend(options, { + 'listenKey': self.safe_string(response, 'listenKey'), + 'lastAuthenticatedTime': time, + }) + self.delay(listenKeyRefreshRate, self.keep_alive_listen_key, params) + + async def keep_alive_listen_key(self, params={}): + # https://binance-docs.github.io/apidocs/spot/en/#listen-key-spot + type = self.safe_string_2(self.options, 'defaultType', 'authenticate', 'spot') + type = self.safe_string(params, 'type', type) + isPortfolioMargin = None + isPortfolioMargin, params = self.handle_option_and_params_2(params, 'keepAliveListenKey', 'papi', 'portfolioMargin', False) + subTypeInfo = self.handle_sub_type_and_params('keepAliveListenKey', None, params) + subType = subTypeInfo[0] + if self.isLinear(type, subType): + type = 'future' + elif self.isInverse(type, subType): + type = 'delivery' + options = self.safe_value(self.options, type, {}) + listenKey = self.safe_string(options, 'listenKey') + if listenKey is None: + # A network error happened: we can't renew a listen key that does not exist. + return + request: dict = {} + symbol = self.safe_string(params, 'symbol') + params = self.omit(params, ['type', 'symbol']) + time = self.milliseconds() + try: + if isPortfolioMargin: + await self.papiPutListenKey(self.extend(request, params)) + params = self.extend(params, {'portfolioMargin': True}) + elif type == 'future': + await self.fapiPrivatePutListenKey(self.extend(request, params)) + elif type == 'delivery': + await self.dapiPrivatePutListenKey(self.extend(request, params)) + else: + request['listenKey'] = listenKey + if type == 'margin': + request['symbol'] = symbol + await self.sapiPutUserDataStream(self.extend(request, params)) + else: + await self.publicPutUserDataStream(self.extend(request, params)) + except Exception as error: + urlType = type + if isPortfolioMargin: + urlType = 'papi' + url = self.urls['api']['ws'][urlType] + '/' + self.options[type]['listenKey'] + client = self.client(url) + messageHashes = list(client.futures.keys()) + for i in range(0, len(messageHashes)): + messageHash = messageHashes[i] + client.reject(error, messageHash) + self.options[type] = self.extend(options, { + 'listenKey': None, + 'lastAuthenticatedTime': 0, + }) + return + self.options[type] = self.extend(options, { + 'listenKey': listenKey, + 'lastAuthenticatedTime': time, + }) + # whether or not to schedule another listenKey keepAlive request + clients = list(self.clients.values()) + listenKeyRefreshRate = self.safe_integer(self.options, 'listenKeyRefreshRate', 1200000) + for i in range(0, len(clients)): + client = clients[i] + subscriptionKeys = list(client.subscriptions.keys()) + for j in range(0, len(subscriptionKeys)): + subscribeType = subscriptionKeys[j] + if subscribeType == type: + self.delay(listenKeyRefreshRate, self.keep_alive_listen_key, params) + return + + def set_balance_cache(self, client: Client, type, isPortfolioMargin=False): + if (type in client.subscriptions) and (type in self.balance): + return + options = self.safe_value(self.options, 'watchBalance') + fetchBalanceSnapshot = self.safe_bool(options, 'fetchBalanceSnapshot', False) + if fetchBalanceSnapshot: + messageHash = type + ':fetchBalanceSnapshot' + if not (messageHash in client.futures): + client.future(messageHash) + self.spawn(self.load_balance_snapshot, client, messageHash, type, isPortfolioMargin) + else: + self.balance[type] = {} + + async def load_balance_snapshot(self, client, messageHash, type, isPortfolioMargin): + params: dict = { + 'type': type, + } + if isPortfolioMargin: + params['portfolioMargin'] = True + response = await self.fetch_balance(params) + self.balance[type] = self.extend(response, self.safe_value(self.balance, type, {})) + # don't remove the future from the .futures cache + future = client.futures[messageHash] + future.resolve() + client.resolve(self.balance[type], type + ':balance') + + async def fetch_balance_ws(self, params={}) -> Balances: + """ + fetch balance and get the amount of funds available for trading or funds locked in orders + + https://developers.binance.com/docs/derivatives/usds-margined-futures/account/websocket-api/Futures-Account-Balance + https://developers.binance.com/docs/binance-spot-api-docs/websocket-api/account-requests#account-information-user_data + https://developers.binance.com/docs/derivatives/coin-margined-futures/account/websocket-api + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str|None [params.type]: 'future', 'delivery', 'savings', 'funding', or 'spot' + :param str|None [params.marginMode]: 'cross' or 'isolated', for margin trading, uses self.options.defaultMarginMode if not passed, defaults to None/None/None + :param str[]|None [params.symbols]: unified market symbols, only used in isolated margin mode + :param str|None [params.method]: method to use. Can be account.balance, account.status, v2/account.balance or v2/account.status + :returns dict: a `balance structure ` + """ + await self.load_markets() + type = self.get_market_type('fetchBalanceWs', None, params) + if type != 'spot' and type != 'future' and type != 'delivery': + raise BadRequest(self.id + ' fetchBalanceWs only supports spot or swap markets') + url = self.urls['api']['ws']['ws-api'][type] + requestId = self.request_id(url) + messageHash = str(requestId) + returnRateLimits = False + returnRateLimits, params = self.handle_option_and_params(params, 'fetchBalanceWs', 'returnRateLimits', False) + payload: dict = { + 'returnRateLimits': returnRateLimits, + } + method = None + method, params = self.handle_option_and_params(params, 'fetchBalanceWs', 'method', 'account.status') + message: dict = { + 'id': messageHash, + 'method': method, + 'params': self.sign_params(self.extend(payload, params)), + } + subscription: dict = { + 'method': self.handle_account_status_ws if (method == 'account.status') else self.handle_balance_ws, + } + return await self.watch(url, messageHash, message, messageHash, subscription) + + def handle_balance_ws(self, client: Client, message): + # + # + messageHash = self.safe_string(message, 'id') + rawBalance = None + if isinstance(message['result'], list): + # account.balance + rawBalance = self.safe_list(message, 'result', []) + else: + # account.status + result = self.safe_dict(message, 'result', {}) + rawBalance = self.safe_list(result, 'assets', []) + parsedBalances = self.parseBalanceCustom(rawBalance) + client.resolve(parsedBalances, messageHash) + + def handle_account_status_ws(self, client: Client, message): + # + # spot + # { + # "id": "605a6d20-6588-4cb9-afa0-b0ab087507ba", + # "status": 200, + # "result": { + # "makerCommission": 15, + # "takerCommission": 15, + # "buyerCommission": 0, + # "sellerCommission": 0, + # "canTrade": True, + # "canWithdraw": True, + # "canDeposit": True, + # "commissionRates": { + # "maker": "0.00150000", + # "taker": "0.00150000", + # "buyer": "0.00000000", + # "seller": "0.00000000" + # }, + # "brokered": False, + # "requireSelfTradePrevention": False, + # "updateTime": 1660801833000, + # "accountType": "SPOT", + # "balances": [{ + # "asset": "BNB", + # "free": "0.00000000", + # "locked": "0.00000000" + # }, + # { + # "asset": "BTC", + # "free": "1.3447112", + # "locked": "0.08600000" + # }, + # { + # "asset": "USDT", + # "free": "1021.21000000", + # "locked": "0.00000000" + # } + # ], + # "permissions": [ + # "SPOT" + # ] + # } + # } + # swap + # + messageHash = self.safe_string(message, 'id') + result = self.safe_dict(message, 'result', {}) + parsedBalances = self.parseBalanceCustom(result) + client.resolve(parsedBalances, messageHash) + + async def fetch_position_ws(self, symbol: str, params={}) -> List[Position]: + """ + fetch data on an open position + + https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/websocket-api/Position-Information + + :param str symbol: unified market symbol of the market the position is held in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `position structure ` + """ + return await self.fetch_positions_ws([symbol], params) + + async def fetch_positions_ws(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/websocket-api/Position-Information + https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/websocket-api/Position-Information + + :param str[] [symbols]: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.returnRateLimits]: set to True to return rate limit informations, defaults to False. + :param str|None [params.method]: method to use. Can be account.position or v2/account.position + :returns dict[]: a list of `position structure ` + """ + await self.load_markets() + payload: dict = {} + market = None + symbols = self.market_symbols(symbols, 'swap', True, True, True) + if symbols is not None: + symbolsLength = len(symbols) + if symbolsLength == 1: + market = self.market(symbols[0]) + payload['symbol'] = market['id'] + type = self.get_market_type('fetchPositionsWs', market, params) + if type != 'future' and type != 'delivery': + raise BadRequest(self.id + ' fetchPositionsWs only supports swap markets') + url = self.urls['api']['ws']['ws-api'][type] + requestId = self.request_id(url) + messageHash = str(requestId) + returnRateLimits = False + returnRateLimits, params = self.handle_option_and_params(params, 'fetchPositionsWs', 'returnRateLimits', False) + payload['returnRateLimits'] = returnRateLimits + method = None + method, params = self.handle_option_and_params(params, 'fetchPositionsWs', 'method', 'account.position') + message: dict = { + 'id': messageHash, + 'method': method, + 'params': self.sign_params(self.extend(payload, params)), + } + subscription: dict = { + 'method': self.handle_positions_ws, + } + result = await self.watch(url, messageHash, message, messageHash, subscription) + return self.filter_by_array_positions(result, 'symbol', symbols, False) + + def handle_positions_ws(self, client: Client, message): + # + # { + # id: '1', + # status: 200, + # result: [ + # { + # symbol: 'BTCUSDT', + # positionAmt: '-0.014', + # entryPrice: '42901.1', + # breakEvenPrice: '30138.83333142', + # markPrice: '71055.98470333', + # unRealizedProfit: '-394.16838584', + # liquidationPrice: '137032.02272908', + # leverage: '123', + # maxNotionalValue: '50000', + # marginType: 'cross', + # isolatedMargin: '0.00000000', + # isAutoAddMargin: 'false', + # positionSide: 'BOTH', + # notional: '-994.78378584', + # isolatedWallet: '0', + # updateTime: 1708906343111, + # isolated: False, + # adlQuantile: 2 + # }, + # ... + # ] + # } + # + # + messageHash = self.safe_string(message, 'id') + result = self.safe_list(message, 'result', []) + positions = [] + for i in range(0, len(result)): + parsed = self.parse_position_risk(result[i]) + entryPrice = self.safe_string(parsed, 'entryPrice') + if (entryPrice != '0') and (entryPrice != '0.0') and (entryPrice != '0.00000000'): + positions.append(parsed) + client.resolve(positions, messageHash) + + async def watch_balance(self, params={}) -> Balances: + """ + watch balance and get the amount of funds available for trading or funds locked in orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.portfolioMargin]: set to True if you would like to watch the balance of a portfolio margin account + :returns dict: a `balance structure ` + """ + await self.load_markets() + await self.authenticate(params) + defaultType = self.safe_string(self.options, 'defaultType', 'spot') + type = self.safe_string(params, 'type', defaultType) + subType = None + subType, params = self.handle_sub_type_and_params('watchBalance', None, params) + isPortfolioMargin = None + isPortfolioMargin, params = self.handle_option_and_params_2(params, 'watchBalance', 'papi', 'portfolioMargin', False) + if self.isLinear(type, subType): + type = 'future' + elif self.isInverse(type, subType): + type = 'delivery' + url = '' + urlType = type + if type == 'spot': + # route to WebSocket API connection where the user data stream is subscribed + url = self.urls['api']['ws']['ws-api'][type] + else: + if isPortfolioMargin: + urlType = 'papi' + url = self.urls['api']['ws'][urlType] + '/' + self.options[type]['listenKey'] + client = self.client(url) + self.set_balance_cache(client, type, isPortfolioMargin) + self.set_positions_cache(client, type, None, isPortfolioMargin) + options = self.safe_dict(self.options, 'watchBalance') + fetchBalanceSnapshot = self.safe_bool(options, 'fetchBalanceSnapshot', False) + awaitBalanceSnapshot = self.safe_bool(options, 'awaitBalanceSnapshot', True) + if fetchBalanceSnapshot and awaitBalanceSnapshot: + await client.future(type + ':fetchBalanceSnapshot') + messageHash = type + ':balance' + message = None + return await self.watch(url, messageHash, message, type) + + def handle_balance(self, client: Client, message): + # + # sent upon a balance update not related to orders + # + # { + # "e": "balanceUpdate", + # "E": 1629352505586, + # "a": "IOTX", + # "d": "0.43750000", + # "T": 1629352505585 + # } + # + # sent upon creating or filling an order + # + # { + # "e": "outboundAccountPosition", # Event type + # "E": 1564034571105, # Event Time + # "u": 1564034571073, # Time of last account update + # "B": [ # Balances Array + # { + # "a": "ETH", # Asset + # "f": "10000.000000", # Free + # "l": "0.000000" # Locked + # } + # ] + # } + # + # future/delivery + # + # { + # "e": "ACCOUNT_UPDATE", # Event Type + # "E": 1564745798939, # Event Time + # "T": 1564745798938 , # Transaction + # "i": "SfsR", # Account Alias + # "a": { # Update Data + # "m":"ORDER", # Event reason type + # "B":[ # Balances + # { + # "a":"BTC", # Asset + # "wb":"122624.12345678", # Wallet Balance + # "cw":"100.12345678" # Cross Wallet Balance + # }, + # ], + # "P":[ + # { + # "s":"BTCUSD_200925", # Symbol + # "pa":"0", # Position Amount + # "ep":"0.0", # Entry Price + # "cr":"200", #(Pre-fee) Accumulated Realized + # "up":"0", # Unrealized PnL + # "mt":"isolated", # Margin Type + # "iw":"0.00000000", # Isolated Wallet(if isolated position) + # "ps":"BOTH" # Position Side + # }, + # ] + # } + # } + # externalLockUpdate + # { + # "e": "externalLockUpdate", # Event Type + # "E": 1581557507324, # Event Time + # "a": "NEO", # Asset + # "d": "10.00000000", # Delta + # "T": 1581557507268 # Transaction Time + # } + # + wallet = self.safe_string(self.options, 'wallet', 'wb') # cw for cross wallet + # each account is connected to a different endpoint + subscriptions = client.subscriptions + subscriptionsKeys = list(subscriptions.keys()) + accountType = self.get_account_type_from_subscriptions(subscriptionsKeys) + messageHash = accountType + ':balance' + if self.balance[accountType] is None: + self.balance[accountType] = {} + self.balance[accountType]['info'] = message + event = self.safe_string(message, 'e') + if event == 'balanceUpdate': + currencyId = self.safe_string(message, 'a') + code = self.safe_currency_code(currencyId) + account = self.account() + delta = self.safe_string(message, 'd') + if code in self.balance[accountType]: + previousValue = self.balance[accountType][code]['free'] + if not isinstance(previousValue, str): + previousValue = self.number_to_string(previousValue) + account['free'] = Precise.string_add(previousValue, delta) + else: + account['free'] = delta + self.balance[accountType][code] = account + else: + message = self.safe_dict(message, 'a', message) + B = self.safe_list(message, 'B') + for i in range(0, len(B)): + entry = B[i] + currencyId = self.safe_string(entry, 'a') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(entry, 'f') + account['used'] = self.safe_string(entry, 'l') + account['total'] = self.safe_string(entry, wallet) + self.balance[accountType][code] = account + timestamp = self.safe_integer(message, 'E') + self.balance[accountType]['timestamp'] = timestamp + self.balance[accountType]['datetime'] = self.iso8601(timestamp) + self.balance[accountType] = self.safe_balance(self.balance[accountType]) + client.resolve(self.balance[accountType], messageHash) + + def get_account_type_from_subscriptions(self, subscriptions: List[str]) -> str: + accountType = '' + for i in range(0, len(subscriptions)): + subscription = subscriptions[i] + if (subscription == 'spot') or (subscription == 'margin') or (subscription == 'future') or (subscription == 'delivery'): + accountType = subscription + break + return accountType + + def get_market_type(self, method, market, params={}): + type = None + type, params = self.handle_market_type_and_params(method, market, params) + subType = None + subType, params = self.handle_sub_type_and_params(method, market, params) + if self.isLinear(type, subType): + type = 'future' + elif self.isInverse(type, subType): + type = 'delivery' + return type + + async def create_order_ws(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}) -> Order: + """ + create a trade order + + https://developers.binance.com/docs/binance-spot-api-docs/websocket-api/trading-requests#place-new-order-trade + https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/websocket-api/New-Order + https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/websocket-api + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float|None [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean params['test']: test order, default False + :param boolean params['returnRateLimits']: set to True to return rate limit information, default False + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + marketType = self.get_market_type('createOrderWs', market, params) + if marketType != 'spot' and marketType != 'future' and marketType != 'delivery': + raise BadRequest(self.id + ' createOrderWs only supports spot or swap markets') + url = self.urls['api']['ws']['ws-api'][marketType] + requestId = self.request_id(url) + messageHash = str(requestId) + sor = self.safe_bool_2(params, 'sor', 'SOR', False) + params = self.omit(params, 'sor', 'SOR') + payload = self.create_order_request(symbol, type, side, amount, price, params) + returnRateLimits = False + returnRateLimits, params = self.handle_option_and_params(params, 'createOrderWs', 'returnRateLimits', False) + payload['returnRateLimits'] = returnRateLimits + test = self.safe_bool(params, 'test', False) + params = self.omit(params, 'test') + message: dict = { + 'id': messageHash, + 'method': 'order.place', + 'params': self.sign_params(self.extend(payload, params)), + } + if test: + if sor: + message['method'] = 'sor.order.test' + else: + message['method'] = 'order.test' + subscription: dict = { + 'method': self.handle_order_ws, + } + return await self.watch(url, messageHash, message, messageHash, subscription) + + def handle_order_ws(self, client: Client, message): + # + # { + # "id": 1, + # "status": 200, + # "result": { + # "symbol": "BTCUSDT", + # "orderId": 7663053, + # "orderListId": -1, + # "clientOrderId": "x-R4BD3S82d8959d0f5114499487a614", + # "transactTime": 1687642291434, + # "price": "25000.00000000", + # "origQty": "0.00100000", + # "executedQty": "0.00000000", + # "cummulativeQuoteQty": "0.00000000", + # "status": "NEW", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "side": "BUY", + # "workingTime": 1687642291434, + # "fills": [], + # "selfTradePreventionMode": "NONE" + # }, + # "rateLimits": [ + # { + # "rateLimitType": "ORDERS", + # "interval": "SECOND", + # "intervalNum": 10, + # "limit": 50, + # "count": 1 + # }, + # { + # "rateLimitType": "ORDERS", + # "interval": "DAY", + # "intervalNum": 1, + # "limit": 160000, + # "count": 1 + # }, + # { + # "rateLimitType": "REQUEST_WEIGHT", + # "interval": "MINUTE", + # "intervalNum": 1, + # "limit": 1200, + # "count": 12 + # } + # ] + # } + # + messageHash = self.safe_string(message, 'id') + result = self.safe_dict(message, 'result', {}) + order = self.parse_order(result) + client.resolve(order, messageHash) + + def handle_orders_ws(self, client: Client, message): + # + # { + # "id": 1, + # "status": 200, + # "result": [{ + # "symbol": "BTCUSDT", + # "orderId": 7665584, + # "orderListId": -1, + # "clientOrderId": "x-R4BD3S82b54769abdd3e4b57874c52", + # "price": "26000.00000000", + # "origQty": "0.00100000", + # "executedQty": "0.00000000", + # "cummulativeQuoteQty": "0.00000000", + # "status": "NEW", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "side": "BUY", + # "stopPrice": "0.00000000", + # "icebergQty": "0.00000000", + # "time": 1687642884646, + # "updateTime": 1687642884646, + # "isWorking": True, + # "workingTime": 1687642884646, + # "origQuoteOrderQty": "0.00000000", + # "selfTradePreventionMode": "NONE" + # }, + # ... + # ], + # "rateLimits": [{ + # "rateLimitType": "REQUEST_WEIGHT", + # "interval": "MINUTE", + # "intervalNum": 1, + # "limit": 1200, + # "count": 14 + # }] + # } + # + messageHash = self.safe_string(message, 'id') + result = self.safe_list(message, 'result', []) + orders = self.parse_orders(result) + client.resolve(orders, messageHash) + + async def edit_order_ws(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}) -> Order: + """ + edit a trade order + + https://developers.binance.com/docs/binance-spot-api-docs/websocket-api/trading-requests#cancel-and-replace-order-trade + https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/websocket-api/Modify-Order + https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/websocket-api/Modify-Order + + :param str id: order id + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of the currency you want to trade in units of the base currency + :param float|None [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + marketType = self.get_market_type('editOrderWs', market, params) + if marketType != 'spot' and marketType != 'future' and marketType != 'delivery': + raise BadRequest(self.id + ' editOrderWs only supports spot or swap markets') + url = self.urls['api']['ws']['ws-api'][marketType] + requestId = self.request_id(url) + messageHash = str(requestId) + isSwap = (marketType == 'future' or marketType == 'delivery') + payload = None + if marketType == 'spot': + payload = self.editSpotOrderRequest(id, symbol, type, side, amount, price, params) + elif isSwap: + payload = self.editContractOrderRequest(id, symbol, type, side, amount, price, params) + returnRateLimits = False + returnRateLimits, params = self.handle_option_and_params(params, 'editOrderWs', 'returnRateLimits', False) + payload['returnRateLimits'] = returnRateLimits + message: dict = { + 'id': messageHash, + 'method': 'order.modify' if (isSwap) else 'order.cancelReplace', + 'params': self.sign_params(self.extend(payload, params)), + } + subscription: dict = { + 'method': self.handle_edit_order_ws, + } + return await self.watch(url, messageHash, message, messageHash, subscription) + + def handle_edit_order_ws(self, client: Client, message): + # + # spot + # { + # "id": 1, + # "status": 200, + # "result": { + # "cancelResult": "SUCCESS", + # "newOrderResult": "SUCCESS", + # "cancelResponse": { + # "symbol": "BTCUSDT", + # "origClientOrderId": "x-R4BD3S82813c5d7ffa594104917de2", + # "orderId": 7665177, + # "orderListId": -1, + # "clientOrderId": "mbrnbQsQhtCXCLY45d5q7S", + # "price": "26000.00000000", + # "origQty": "0.00100000", + # "executedQty": "0.00000000", + # "cummulativeQuoteQty": "0.00000000", + # "status": "CANCELED", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "side": "BUY", + # "selfTradePreventionMode": "NONE" + # }, + # "newOrderResponse": { + # "symbol": "BTCUSDT", + # "orderId": 7665584, + # "orderListId": -1, + # "clientOrderId": "x-R4BD3S82b54769abdd3e4b57874c52", + # "transactTime": 1687642884646, + # "price": "26000.00000000", + # "origQty": "0.00100000", + # "executedQty": "0.00000000", + # "cummulativeQuoteQty": "0.00000000", + # "status": "NEW", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "side": "BUY", + # "workingTime": 1687642884646, + # "fills": [], + # "selfTradePreventionMode": "NONE" + # } + # }, + # "rateLimits": [{ + # "rateLimitType": "ORDERS", + # "interval": "SECOND", + # "intervalNum": 10, + # "limit": 50, + # "count": 1 + # }, + # { + # "rateLimitType": "ORDERS", + # "interval": "DAY", + # "intervalNum": 1, + # "limit": 160000, + # "count": 3 + # }, + # { + # "rateLimitType": "REQUEST_WEIGHT", + # "interval": "MINUTE", + # "intervalNum": 1, + # "limit": 1200, + # "count": 12 + # } + # ] + # } + # swap + # { + # "id":"1", + # "status":200, + # "result":{ + # "orderId":667061487, + # "symbol":"LTCUSDT", + # "status":"NEW", + # "clientOrderId":"x-xcKtGhcu91a74c818749ee42c0f70", + # "price":"82.00", + # "avgPrice":"0.00", + # "origQty":"1.000", + # "executedQty":"0.000", + # "cumQty":"0.000", + # "cumQuote":"0.00000", + # "timeInForce":"GTC", + # "type":"LIMIT", + # "reduceOnly":false, + # "closePosition":false, + # "side":"BUY", + # "positionSide":"BOTH", + # "stopPrice":"0.00", + # "workingType":"CONTRACT_PRICE", + # "priceProtect":false, + # "origType":"LIMIT", + # "priceMatch":"NONE", + # "selfTradePreventionMode":"NONE", + # "goodTillDate":0, + # "updateTime":1712918927511 + # } + # } + # + messageHash = self.safe_string(message, 'id') + result = self.safe_dict(message, 'result', {}) + newSpotOrder = self.safe_dict(result, 'newOrderResponse') + order = None + if newSpotOrder is not None: + order = self.parse_order(newSpotOrder) + else: + order = self.parse_order(result) + client.resolve(order, messageHash) + + async def cancel_order_ws(self, id: str, symbol: Str = None, params={}) -> Order: + """ + cancel multiple orders + + https://developers.binance.com/docs/binance-spot-api-docs/websocket-api/trading-requests#cancel-order-trade + https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/websocket-api/Cancel-Order + https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/websocket-api/Cancel-Order + + :param str id: order id + :param str [symbol]: unified market symbol, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str|None [params.cancelRestrictions]: Supported values: ONLY_NEW - Cancel will succeed if the order status is NEW. ONLY_PARTIALLY_FILLED - Cancel will succeed if order status is PARTIALLY_FILLED. + :returns dict: an list of `order structures ` + """ + await self.load_markets() + if symbol is None: + raise BadRequest(self.id + ' cancelOrderWs requires a symbol') + market = self.market(symbol) + type = self.get_market_type('cancelOrderWs', market, params) + url = self.urls['api']['ws']['ws-api'][type] + requestId = self.request_id(url) + messageHash = str(requestId) + returnRateLimits = False + returnRateLimits, params = self.handle_option_and_params(params, 'cancelOrderWs', 'returnRateLimits', False) + payload: dict = { + 'symbol': self.market_id(symbol), + 'returnRateLimits': returnRateLimits, + } + clientOrderId = self.safe_string_2(params, 'origClientOrderId', 'clientOrderId') + if clientOrderId is not None: + payload['origClientOrderId'] = clientOrderId + else: + payload['orderId'] = self.parse_to_int(id) + params = self.omit(params, ['origClientOrderId', 'clientOrderId']) + message: dict = { + 'id': messageHash, + 'method': 'order.cancel', + 'params': self.sign_params(self.extend(payload, params)), + } + subscription: dict = { + 'method': self.handle_order_ws, + } + return await self.watch(url, messageHash, message, messageHash, subscription) + + async def cancel_all_orders_ws(self, symbol: Str = None, params={}): + """ + cancel all open orders in a market + + https://developers.binance.com/docs/binance-spot-api-docs/websocket-api/trading-requests#cancel-open-orders-trade + + :param str [symbol]: unified market symbol of the market to cancel orders in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + market = self.market(symbol) + type = self.get_market_type('cancelAllOrdersWs', market, params) + if type != 'spot': + raise BadRequest(self.id + ' cancelAllOrdersWs only supports spot markets') + url = self.urls['api']['ws']['ws-api'][type] + requestId = self.request_id(url) + messageHash = str(requestId) + returnRateLimits = False + returnRateLimits, params = self.handle_option_and_params(params, 'cancelAllOrdersWs', 'returnRateLimits', False) + payload: dict = { + 'symbol': self.market_id(symbol), + 'returnRateLimits': returnRateLimits, + } + message: dict = { + 'id': messageHash, + 'method': 'openOrders.cancelAll', + 'params': self.sign_params(self.extend(payload, params)), + } + subscription: dict = { + 'method': self.handle_orders_ws, + } + return await self.watch(url, messageHash, message, messageHash, subscription) + + async def fetch_order_ws(self, id: str, symbol: Str = None, params={}) -> Order: + """ + fetches information on an order made by the user + + https://developers.binance.com/docs/binance-spot-api-docs/websocket-api/trading-requests#query-order-user_data + https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/websocket-api/Query-Order + https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/websocket-api/Query-Order + + :param str id: order id + :param str [symbol]: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + if symbol is None: + raise BadRequest(self.id + ' cancelOrderWs requires a symbol') + market = self.market(symbol) + type = self.get_market_type('fetchOrderWs', market, params) + if type != 'spot' and type != 'future' and type != 'delivery': + raise BadRequest(self.id + ' fetchOrderWs only supports spot or swap markets') + url = self.urls['api']['ws']['ws-api'][type] + requestId = self.request_id(url) + messageHash = str(requestId) + returnRateLimits = False + returnRateLimits, params = self.handle_option_and_params(params, 'fetchOrderWs', 'returnRateLimits', False) + payload: dict = { + 'symbol': self.market_id(symbol), + 'returnRateLimits': returnRateLimits, + } + clientOrderId = self.safe_string_2(params, 'origClientOrderId', 'clientOrderId') + if clientOrderId is not None: + payload['origClientOrderId'] = clientOrderId + else: + payload['orderId'] = self.parse_to_int(id) + message: dict = { + 'id': messageHash, + 'method': 'order.status', + 'params': self.sign_params(self.extend(payload, params)), + } + subscription: dict = { + 'method': self.handle_order_ws, + } + return await self.watch(url, messageHash, message, messageHash, subscription) + + async def fetch_orders_ws(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://developers.binance.com/docs/binance-spot-api-docs/websocket-api/trading-requests#order-lists + + :param str symbol: unified market symbol of the market orders were made in + :param int|None [since]: the earliest time in ms to fetch orders for + :param int|None [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.orderId]: order id to begin at + :param int [params.startTime]: earliest time in ms to retrieve orders for + :param int [params.endTime]: latest time in ms to retrieve orders for + :param int [params.limit]: the maximum number of order structures to retrieve + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + if symbol is None: + raise BadRequest(self.id + ' fetchOrdersWs requires a symbol') + market = self.market(symbol) + type = self.get_market_type('fetchOrdersWs', market, params) + if type != 'spot': + raise BadRequest(self.id + ' fetchOrdersWs only supports spot markets') + url = self.urls['api']['ws']['ws-api'][type] + requestId = self.request_id(url) + messageHash = str(requestId) + returnRateLimits = False + returnRateLimits, params = self.handle_option_and_params(params, 'fetchOrderWs', 'returnRateLimits', False) + payload: dict = { + 'symbol': self.market_id(symbol), + 'returnRateLimits': returnRateLimits, + } + message: dict = { + 'id': messageHash, + 'method': 'allOrders', + 'params': self.sign_params(self.extend(payload, params)), + } + subscription: dict = { + 'method': self.handle_orders_ws, + } + orders = await self.watch(url, messageHash, message, messageHash, subscription) + return self.filter_by_symbol_since_limit(orders, symbol, since, limit) + + async def fetch_closed_orders_ws(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch closed orders + + https://developers.binance.com/docs/binance-spot-api-docs/websocket-api/trading-requests#order-lists + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + orders = await self.fetch_orders_ws(symbol, since, limit, params) + closedOrders = [] + for i in range(0, len(orders)): + order = orders[i] + if order['status'] == 'closed': + closedOrders.append(order) + return closedOrders + + async def fetch_open_orders_ws(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://developers.binance.com/docs/binance-spot-api-docs/websocket-api/trading-requests#current-open-orders-user_data + + :param str symbol: unified market symbol + :param int|None [since]: the earliest time in ms to fetch open orders for + :param int|None [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + market = self.market(symbol) + type = self.get_market_type('fetchOpenOrdersWs', market, params) + if type != 'spot' and type != 'future': + raise BadRequest(self.id + ' fetchOpenOrdersWs only supports spot or swap markets') + url = self.urls['api']['ws']['ws-api'][type] + requestId = self.request_id(url) + messageHash = str(requestId) + returnRateLimits = False + returnRateLimits, params = self.handle_option_and_params(params, 'fetchOrderWs', 'returnRateLimits', False) + payload: dict = { + 'returnRateLimits': returnRateLimits, + } + if symbol is not None: + payload['symbol'] = self.market_id(symbol) + message: dict = { + 'id': messageHash, + 'method': 'openOrders.status', + 'params': self.sign_params(self.extend(payload, params)), + } + subscription: dict = { + 'method': self.handle_orders_ws, + } + orders = await self.watch(url, messageHash, message, messageHash, subscription) + return self.filter_by_symbol_since_limit(orders, symbol, since, limit) + + async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + watches information on multiple orders made by the user + + https://developers.binance.com/docs/binance-spot-api-docs/user-data-stream#order-update + https://developers.binance.com/docs/margin_trading/trade-data-stream/Event-Order-Update + https://developers.binance.com/docs/derivatives/usds-margined-futures/user-data-streams/Event-Order-Update + + :param str symbol: unified market symbol of the market the orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str|None [params.marginMode]: 'cross' or 'isolated', for spot margin + :param boolean [params.portfolioMargin]: set to True if you would like to watch portfolio margin account orders + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + messageHash = 'orders' + market = None + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + messageHash += ':' + symbol + type = None + type, params = self.handle_market_type_and_params('watchOrders', market, params) + subType = None + subType, params = self.handle_sub_type_and_params('watchOrders', market, params) + if self.isLinear(type, subType): + type = 'future' + elif self.isInverse(type, subType): + type = 'delivery' + params = self.extend(params, {'type': type, 'symbol': symbol, 'subType': subType}) # needed inside authenticate for isolated margin + await self.authenticate(params) + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('watchOrders', params) + urlType = type + if (type == 'margin') or ((type == 'spot') and (marginMode is not None)): + urlType = 'spot' # spot-margin shares the same stream spot + isPortfolioMargin = None + isPortfolioMargin, params = self.handle_option_and_params_2(params, 'watchOrders', 'papi', 'portfolioMargin', False) + url = '' + if type == 'spot': + # route orders to ws-api user data stream + url = self.urls['api']['ws']['ws-api'][type] + else: + if isPortfolioMargin: + urlType = 'papi' + url = self.urls['api']['ws'][urlType] + '/' + self.options[type]['listenKey'] + client = self.client(url) + self.set_balance_cache(client, type, isPortfolioMargin) + self.set_positions_cache(client, type, None, isPortfolioMargin) + message = None + orders = await self.watch(url, messageHash, message, type) + if self.newUpdates: + limit = orders.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(orders, symbol, since, limit, True) + + def parse_ws_order(self, order, market=None): + # + # spot + # + # { + # "e": "executionReport", # Event type + # "E": 1499405658658, # Event time + # "s": "ETHBTC", # Symbol + # "c": "mUvoqJxFIILMdfAW5iGSOW", # Client order ID + # "S": "BUY", # Side + # "o": "LIMIT", # Order type + # "f": "GTC", # Time in force + # "q": "1.00000000", # Order quantity + # "p": "0.10264410", # Order price + # "P": "0.00000000", # Stop price + # "F": "0.00000000", # Iceberg quantity + # "g": -1, # OrderListId + # "C": null, # Original client order ID; This is the ID of the order being canceled + # "x": "NEW", # Current execution type + # "X": "NEW", # Current order status + # "r": "NONE", # Order reject reason; will be an error code. + # "i": 4293153, # Order ID + # "l": "0.00000000", # Last executed quantity + # "z": "0.00000000", # Cumulative filled quantity + # "L": "0.00000000", # Last executed price + # "n": "0", # Commission amount + # "N": null, # Commission asset + # "T": 1499405658657, # Transaction time + # "t": -1, # Trade ID + # "I": 8641984, # Ignore + # "w": True, # Is the order on the book? + # "m": False, # Is self trade the maker side? + # "M": False, # Ignore + # "O": 1499405658657, # Order creation time + # "Z": "0.00000000", # Cumulative quote asset transacted quantity + # "Y": "0.00000000" # Last quote asset transacted quantity(i.e. lastPrice * lastQty), + # "Q": "0.00000000" # Quote Order Qty + # } + # + # future + # + # { + # "s":"BTCUSDT", # Symbol + # "c":"TEST", # Client Order Id + # # special client order id: + # # starts with "autoclose-": liquidation order + # # "adl_autoclose": ADL auto close order + # "S":"SELL", # Side + # "o":"TRAILING_STOP_MARKET", # Order Type + # "f":"GTC", # Time in Force + # "q":"0.001", # Original Quantity + # "p":"0", # Original Price + # "ap":"0", # Average Price + # "sp":"7103.04", # Stop Price. Please ignore with TRAILING_STOP_MARKET order + # "x":"NEW", # Execution Type + # "X":"NEW", # Order Status + # "i":8886774, # Order Id + # "l":"0", # Order Last Filled Quantity + # "z":"0", # Order Filled Accumulated Quantity + # "L":"0", # Last Filled Price + # "N":"USDT", # Commission Asset, will not push if no commission + # "n":"0", # Commission, will not push if no commission + # "T":1568879465651, # Order Trade Time + # "t":0, # Trade Id + # "b":"0", # Bids Notional + # "a":"9.91", # Ask Notional + # "m":false, # Is self trade the maker side? + # "R":false, # Is self reduce only + # "wt":"CONTRACT_PRICE", # Stop Price Working Type + # "ot":"TRAILING_STOP_MARKET", # Original Order Type + # "ps":"LONG", # Position Side + # "cp":false, # If Close-All, pushed with conditional order + # "AP":"7476.89", # Activation Price, only puhed with TRAILING_STOP_MARKET order + # "cr":"5.0", # Callback Rate, only puhed with TRAILING_STOP_MARKET order + # "rp":"0" # Realized Profit of the trade + # } + # + executionType = self.safe_string(order, 'x') + orderId = self.safe_string(order, 'i') + marketId = self.safe_string(order, 's') + marketType = 'contract' if ('ps' in order) else 'spot' + symbol = self.safe_symbol(marketId, None, None, marketType) + timestamp = self.safe_integer(order, 'O') + T = self.safe_integer(order, 'T') + lastTradeTimestamp = None + if executionType == 'NEW' or executionType == 'AMENDMENT' or executionType == 'CANCELED': + if timestamp is None: + timestamp = T + elif executionType == 'TRADE': + lastTradeTimestamp = T + lastUpdateTimestamp = T + fee = None + feeCost = self.safe_string(order, 'n') + if (feeCost is not None) and (Precise.string_gt(feeCost, '0')): + feeCurrencyId = self.safe_string(order, 'N') + feeCurrency = self.safe_currency_code(feeCurrencyId) + fee = { + 'cost': feeCost, + 'currency': feeCurrency, + } + price = self.safe_string(order, 'p') + amount = self.safe_string(order, 'q') + side = self.safe_string_lower(order, 'S') + type = self.safe_string_lower(order, 'o') + filled = self.safe_string(order, 'z') + cost = self.safe_string(order, 'Z') + average = self.safe_string(order, 'ap') + rawStatus = self.safe_string(order, 'X') + status = self.parse_order_status(rawStatus) + trades = None + clientOrderId = self.safe_string(order, 'C') + if (clientOrderId is None) or (len(clientOrderId) == 0): + clientOrderId = self.safe_string(order, 'c') + stopPrice = self.safe_string_2(order, 'P', 'sp') + timeInForce = self.safe_string(order, 'f') + if timeInForce == 'GTX': + # GTX means "Good Till Crossing" and is an equivalent way of saying Post Only + timeInForce = 'PO' + return self.safe_order({ + 'info': order, + 'symbol': symbol, + 'id': orderId, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': lastTradeTimestamp, + 'lastUpdateTimestamp': lastUpdateTimestamp, + 'type': type, + 'timeInForce': timeInForce, + 'postOnly': None, + 'reduceOnly': self.safe_bool(order, 'R'), + 'side': side, + 'price': price, + 'stopPrice': stopPrice, + 'triggerPrice': stopPrice, + 'amount': amount, + 'cost': cost, + 'average': average, + 'filled': filled, + 'remaining': None, + 'status': status, + 'fee': fee, + 'trades': trades, + }) + + def handle_order_update(self, client: Client, message): + # + # spot + # + # { + # "e": "executionReport", # Event type + # "E": 1499405658658, # Event time + # "s": "ETHBTC", # Symbol + # "c": "mUvoqJxFIILMdfAW5iGSOW", # Client order ID + # "S": "BUY", # Side + # "o": "LIMIT", # Order type + # "f": "GTC", # Time in force + # "q": "1.00000000", # Order quantity + # "p": "0.10264410", # Order price + # "P": "0.00000000", # Stop price + # "F": "0.00000000", # Iceberg quantity + # "g": -1, # OrderListId + # "C": null, # Original client order ID; This is the ID of the order being canceled + # "x": "NEW", # Current execution type + # "X": "NEW", # Current order status + # "r": "NONE", # Order reject reason; will be an error code. + # "i": 4293153, # Order ID + # "l": "0.00000000", # Last executed quantity + # "z": "0.00000000", # Cumulative filled quantity + # "L": "0.00000000", # Last executed price + # "n": "0", # Commission amount + # "N": null, # Commission asset + # "T": 1499405658657, # Transaction time + # "t": -1, # Trade ID + # "I": 8641984, # Ignore + # "w": True, # Is the order on the book? + # "m": False, # Is self trade the maker side? + # "M": False, # Ignore + # "O": 1499405658657, # Order creation time + # "Z": "0.00000000", # Cumulative quote asset transacted quantity + # "Y": "0.00000000" # Last quote asset transacted quantity(i.e. lastPrice * lastQty), + # "Q": "0.00000000" # Quote Order Qty + # } + # + # future + # + # { + # "e":"ORDER_TRADE_UPDATE", # Event Type + # "E":1568879465651, # Event Time + # "T":1568879465650, # Trasaction Time + # "o": { + # "s":"BTCUSDT", # Symbol + # "c":"TEST", # Client Order Id + # # special client order id: + # # starts with "autoclose-": liquidation order + # # "adl_autoclose": ADL auto close order + # "S":"SELL", # Side + # "o":"TRAILING_STOP_MARKET", # Order Type + # "f":"GTC", # Time in Force + # "q":"0.001", # Original Quantity + # "p":"0", # Original Price + # "ap":"0", # Average Price + # "sp":"7103.04", # Stop Price. Please ignore with TRAILING_STOP_MARKET order + # "x":"NEW", # Execution Type + # "X":"NEW", # Order Status + # "i":8886774, # Order Id + # "l":"0", # Order Last Filled Quantity + # "z":"0", # Order Filled Accumulated Quantity + # "L":"0", # Last Filled Price + # "N":"USDT", # Commission Asset, will not push if no commission + # "n":"0", # Commission, will not push if no commission + # "T":1568879465651, # Order Trade Time + # "t":0, # Trade Id + # "b":"0", # Bids Notional + # "a":"9.91", # Ask Notional + # "m":false, # Is self trade the maker side? + # "R":false, # Is self reduce only + # "wt":"CONTRACT_PRICE", # Stop Price Working Type + # "ot":"TRAILING_STOP_MARKET", # Original Order Type + # "ps":"LONG", # Position Side + # "cp":false, # If Close-All, pushed with conditional order + # "AP":"7476.89", # Activation Price, only puhed with TRAILING_STOP_MARKET order + # "cr":"5.0", # Callback Rate, only puhed with TRAILING_STOP_MARKET order + # "rp":"0" # Realized Profit of the trade + # } + # } + # + e = self.safe_string(message, 'e') + if e == 'ORDER_TRADE_UPDATE': + message = self.safe_dict(message, 'o', message) + self.handle_my_trade(client, message) + self.handle_order(client, message) + self.handle_my_liquidation(client, message) + + async def watch_positions(self, symbols: Strings = None, since: Int = None, limit: Int = None, params={}) -> List[Position]: + """ + watch all open positions + :param str[]|None symbols: list of unified market symbols + :param number [since]: since timestamp + :param number [limit]: limit + :param dict params: extra parameters specific to the exchange API endpoint + :param boolean [params.portfolioMargin]: set to True if you would like to watch positions in a portfolio margin account + :returns dict[]: a list of `position structure ` + """ + await self.load_markets() + market = None + messageHash = '' + symbols = self.market_symbols(symbols) + if not self.is_empty(symbols): + market = self.get_market_from_symbols(symbols) + messageHash = '::' + ','.join(symbols) + type = None + type, params = self.handle_market_type_and_params('watchPositions', market, params) + if type == 'spot' or type == 'margin': + type = 'future' + subType = None + subType, params = self.handle_sub_type_and_params('watchPositions', market, params) + if self.isLinear(type, subType): + type = 'future' + elif self.isInverse(type, subType): + type = 'delivery' + marketTypeObject: dict = {} + marketTypeObject['type'] = type + marketTypeObject['subType'] = subType + await self.authenticate(self.extend(marketTypeObject, params)) + messageHash = type + ':positions' + messageHash + isPortfolioMargin = None + isPortfolioMargin, params = self.handle_option_and_params_2(params, 'watchPositions', 'papi', 'portfolioMargin', False) + urlType = type + if isPortfolioMargin: + urlType = 'papi' + url = self.urls['api']['ws'][urlType] + '/' + self.options[type]['listenKey'] + client = self.client(url) + self.set_balance_cache(client, type, isPortfolioMargin) + self.set_positions_cache(client, type, symbols, isPortfolioMargin) + fetchPositionsSnapshot = self.handle_option('watchPositions', 'fetchPositionsSnapshot', True) + awaitPositionsSnapshot = self.handle_option('watchPositions', 'awaitPositionsSnapshot', True) + cache = self.safe_value(self.positions, type) + if fetchPositionsSnapshot and awaitPositionsSnapshot and cache is None: + snapshot = await client.future(type + ':fetchPositionsSnapshot') + return self.filter_by_symbols_since_limit(snapshot, symbols, since, limit, True) + newPositions = await self.watch(url, messageHash, None, type) + if self.newUpdates: + return newPositions + return self.filter_by_symbols_since_limit(cache, symbols, since, limit, True) + + def set_positions_cache(self, client: Client, type, symbols: Strings = None, isPortfolioMargin=False): + if type == 'spot': + return + if self.positions is None: + self.positions = {} + if type in self.positions: + return + fetchPositionsSnapshot = self.handle_option('watchPositions', 'fetchPositionsSnapshot', False) + if fetchPositionsSnapshot: + messageHash = type + ':fetchPositionsSnapshot' + if not (messageHash in client.futures): + client.future(messageHash) + self.spawn(self.load_positions_snapshot, client, messageHash, type, isPortfolioMargin) + else: + self.positions[type] = ArrayCacheBySymbolBySide() + + async def load_positions_snapshot(self, client, messageHash, type, isPortfolioMargin): + params: dict = { + 'type': type, + } + if isPortfolioMargin: + params['portfolioMargin'] = True + positions = await self.fetch_positions(None, params) + self.positions[type] = ArrayCacheBySymbolBySide() + cache = self.positions[type] + for i in range(0, len(positions)): + position = positions[i] + contracts = self.safe_number(position, 'contracts', 0) + if contracts > 0: + cache.append(position) + # don't remove the future from the .futures cache + future = client.futures[messageHash] + future.resolve(cache) + client.resolve(cache, type + ':position') + + def handle_positions(self, client, message): + # + # { + # e: 'ACCOUNT_UPDATE', + # T: 1667881353112, + # E: 1667881353115, + # a: { + # B: [{ + # a: 'USDT', + # wb: '1127.95750089', + # cw: '1040.82091149', + # bc: '0' + # }], + # P: [{ + # s: 'BTCUSDT', + # pa: '-0.089', + # ep: '19700.03933', + # cr: '-1260.24809979', + # up: '1.53058860', + # mt: 'isolated', + # iw: '87.13658940', + # ps: 'BOTH', + # ma: 'USDT' + # }], + # m: 'ORDER' + # } + # } + # + # each account is connected to a different endpoint + # and has exactly one subscriptionhash which is the account type + subscriptions = client.subscriptions + subscriptionsKeys = list(subscriptions.keys()) + accountType = self.get_account_type_from_subscriptions(subscriptionsKeys) + if self.positions is None: + self.positions = {} + if not (accountType in self.positions): + self.positions[accountType] = ArrayCacheBySymbolBySide() + cache = self.positions[accountType] + data = self.safe_dict(message, 'a', {}) + rawPositions = self.safe_list(data, 'P', []) + newPositions = [] + for i in range(0, len(rawPositions)): + rawPosition = rawPositions[i] + position = self.parse_ws_position(rawPosition) + timestamp = self.safe_integer(message, 'E') + position['timestamp'] = timestamp + position['datetime'] = self.iso8601(timestamp) + newPositions.append(position) + cache.append(position) + messageHashes = self.find_message_hashes(client, accountType + ':positions::') + for i in range(0, len(messageHashes)): + messageHash = messageHashes[i] + parts = messageHash.split('::') + symbolsString = parts[1] + symbols = symbolsString.split(',') + positions = self.filter_by_array(newPositions, 'symbol', symbols, False) + if not self.is_empty(positions): + client.resolve(positions, messageHash) + client.resolve(newPositions, accountType + ':positions') + + def parse_ws_position(self, position, market=None): + # + # { + # "s": "BTCUSDT", # Symbol + # "pa": "0", # Position Amount + # "ep": "0.00000", # Entry Price + # "cr": "200", #(Pre-fee) Accumulated Realized + # "up": "0", # Unrealized PnL + # "mt": "isolated", # Margin Type + # "iw": "0.00000000", # Isolated Wallet(if isolated position) + # "ps": "BOTH" # Position Side + # } + # + marketId = self.safe_string(position, 's') + contracts = self.safe_string(position, 'pa') + contractsAbs = Precise.string_abs(self.safe_string(position, 'pa')) + positionSide = self.safe_string_lower(position, 'ps') + hedged = True + if positionSide == 'both': + hedged = False + if not Precise.string_eq(contracts, '0'): + if Precise.string_lt(contracts, '0'): + positionSide = 'short' + else: + positionSide = 'long' + return self.safe_position({ + 'info': position, + 'id': None, + 'symbol': self.safe_symbol(marketId, None, None, 'swap'), + 'notional': None, + 'marginMode': self.safe_string(position, 'mt'), + 'liquidationPrice': None, + 'entryPrice': self.safe_number(position, 'ep'), + 'unrealizedPnl': self.safe_number(position, 'up'), + 'percentage': None, + 'contracts': self.parse_number(contractsAbs), + 'contractSize': None, + 'markPrice': None, + 'side': positionSide, + 'hedged': hedged, + 'timestamp': None, + 'datetime': None, + 'maintenanceMargin': None, + 'maintenanceMarginPercentage': None, + 'collateral': None, + 'initialMargin': None, + 'initialMarginPercentage': None, + 'leverage': None, + 'marginRatio': None, + }) + + async def fetch_my_trades_ws(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + fetch all trades made by the user + + https://developers.binance.com/docs/binance-spot-api-docs/websocket-api/account-requests#account-trade-history-user_data + + :param str symbol: unified market symbol + :param int|None [since]: the earliest time in ms to fetch trades for + :param int|None [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.endTime]: the latest time in ms to fetch trades for + :param int [params.fromId]: first trade Id to fetch + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + if symbol is None: + raise BadRequest(self.id + ' fetchMyTradesWs requires a symbol') + market = self.market(symbol) + type = self.get_market_type('fetchMyTradesWs', market, params) + if type != 'spot' and type != 'future': + raise BadRequest(self.id + ' fetchMyTradesWs does not support ' + type + ' markets') + url = self.urls['api']['ws']['ws-api'][type] + requestId = self.request_id(url) + messageHash = str(requestId) + returnRateLimits = False + returnRateLimits, params = self.handle_option_and_params(params, 'fetchMyTradesWs', 'returnRateLimits', False) + payload: dict = { + 'symbol': self.market_id(symbol), + 'returnRateLimits': returnRateLimits, + } + if since is not None: + payload['startTime'] = since + if limit is not None: + payload['limit'] = limit + fromId = self.safe_integer(params, 'fromId') + if fromId is not None and since is not None: + raise BadRequest(self.id + ' fetchMyTradesWs does not support fetching by both fromId and since parameters at the same time') + message: dict = { + 'id': messageHash, + 'method': 'myTrades', + 'params': self.sign_params(self.extend(payload, params)), + } + subscription: dict = { + 'method': self.handle_trades_ws, + } + trades = await self.watch(url, messageHash, message, messageHash, subscription) + return self.filter_by_symbol_since_limit(trades, symbol, since, limit) + + async def fetch_trades_ws(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + fetch all trades made by the user + + https://developers.binance.com/docs/binance-spot-api-docs/websocket-api/market-data-requests#recent-trades + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve, default=500, max=1000 + :param dict [params]: extra parameters specific to the exchange API endpoint + + EXCHANGE SPECIFIC PARAMETERS + :param int [params.fromId]: trade ID to begin at + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + type = self.get_market_type('fetchTradesWs', market, params) + if type != 'spot' and type != 'future': + raise BadRequest(self.id + ' fetchTradesWs does not support ' + type + ' markets') + url = self.urls['api']['ws']['ws-api'][type] + requestId = self.request_id(url) + messageHash = str(requestId) + returnRateLimits = False + returnRateLimits, params = self.handle_option_and_params(params, 'fetchTradesWs', 'returnRateLimits', False) + payload: dict = { + 'symbol': self.market_id(symbol), + 'returnRateLimits': returnRateLimits, + } + if limit is not None: + payload['limit'] = limit + message: dict = { + 'id': messageHash, + 'method': 'trades.historical', + 'params': self.extend(payload, params), + } + subscription: dict = { + 'method': self.handle_trades_ws, + } + trades = await self.watch(url, messageHash, message, messageHash, subscription) + return self.filter_by_since_limit(trades, since, limit) + + def handle_trades_ws(self, client: Client, message): + # + # fetchMyTradesWs + # + # { + # "id": "f4ce6a53-a29d-4f70-823b-4ab59391d6e8", + # "status": 200, + # "result": [ + # { + # "symbol": "BTCUSDT", + # "id": 1650422481, + # "orderId": 12569099453, + # "orderListId": -1, + # "price": "23416.10000000", + # "qty": "0.00635000", + # "quoteQty": "148.69223500", + # "commission": "0.00000000", + # "commissionAsset": "BNB", + # "time": 1660801715793, + # "isBuyer": False, + # "isMaker": True, + # "isBestMatch": True + # }, + # ... + # ], + # } + # + # fetchTradesWs + # + # { + # "id": "f4ce6a53-a29d-4f70-823b-4ab59391d6e8", + # "status": 200, + # "result": [ + # { + # "id": 0, + # "price": "0.00005000", + # "qty": "40.00000000", + # "quoteQty": "0.00200000", + # "time": 1500004800376, + # "isBuyerMaker": True, + # "isBestMatch": True + # } + # ... + # ], + # } + # + messageHash = self.safe_string(message, 'id') + result = self.safe_list(message, 'result', []) + trades = self.parse_trades(result) + client.resolve(trades, messageHash) + + async def watch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + watches information on multiple trades made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.portfolioMargin]: set to True if you would like to watch trades in a portfolio margin account + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + type = None + market = None + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + type, params = self.handle_market_type_and_params('watchMyTrades', market, params) + subType = None + subType, params = self.handle_sub_type_and_params('watchMyTrades', market, params) + if self.isLinear(type, subType): + type = 'future' + elif self.isInverse(type, subType): + type = 'delivery' + messageHash = 'myTrades' + if symbol is not None: + symbol = self.symbol(symbol) + messageHash += ':' + symbol + params = self.extend(params, {'type': market['type'], 'symbol': symbol}) + await self.authenticate(self.extend({'type': type, 'subType': subType}, params)) + urlType = type # we don't change type because the listening key is different + if type == 'margin': + urlType = 'spot' # spot-margin shares the same stream spot + isPortfolioMargin = None + isPortfolioMargin, params = self.handle_option_and_params_2(params, 'watchMyTrades', 'papi', 'portfolioMargin', False) + url = '' + if type == 'spot': + url = self.urls['api']['ws']['ws-api'][type] + else: + if isPortfolioMargin: + urlType = 'papi' + url = self.urls['api']['ws'][urlType] + '/' + self.options[type]['listenKey'] + client = self.client(url) + self.set_balance_cache(client, type, isPortfolioMargin) + self.set_positions_cache(client, type, None, isPortfolioMargin) + message = None + trades = await self.watch(url, messageHash, message, type) + if self.newUpdates: + limit = trades.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(trades, symbol, since, limit, True) + + def handle_my_trade(self, client: Client, message): + messageHash = 'myTrades' + executionType = self.safe_string(message, 'x') + if executionType == 'TRADE': + trade = self.parse_ws_trade(message) + orderId = self.safe_string(trade, 'order') + tradeFee = self.safe_dict(trade, 'fee', {}) + tradeFee = self.extend({}, tradeFee) + symbol = self.safe_string(trade, 'symbol') + if orderId is not None and tradeFee is not None and symbol is not None: + cachedOrders = self.orders + if cachedOrders is not None: + orders = self.safe_value(cachedOrders.hashmap, symbol, {}) + order = self.safe_value(orders, orderId) + if order is not None: + # accumulate order fees + fees = self.safe_value(order, 'fees') + fee = self.safe_value(order, 'fee') + if not self.is_empty(fees): + insertNewFeeCurrency = True + for i in range(0, len(fees)): + orderFee = fees[i] + if orderFee['currency'] == tradeFee['currency']: + feeCost = self.sum(tradeFee['cost'], orderFee['cost']) + order['fees'][i]['cost'] = float(self.currency_to_precision(tradeFee['currency'], feeCost)) + insertNewFeeCurrency = False + break + if insertNewFeeCurrency: + order['fees'].append(tradeFee) + elif fee is not None: + if fee['currency'] == tradeFee['currency']: + feeCost = self.sum(fee['cost'], tradeFee['cost']) + order['fee']['cost'] = float(self.currency_to_precision(tradeFee['currency'], feeCost)) + elif fee['currency'] is None: + order['fee'] = tradeFee + else: + order['fees'] = [fee, tradeFee] + order['fee'] = None + else: + order['fee'] = tradeFee + # save self trade in the order + orderTrades = self.safe_list(order, 'trades', []) + orderTrades.append(trade) + order['trades'] = orderTrades + # don't append twice cause it breaks newUpdates mode + # self order already exists in the cache + if self.myTrades is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + self.myTrades = ArrayCacheBySymbolById(limit) + myTrades = self.myTrades + myTrades.append(trade) + client.resolve(self.myTrades, messageHash) + messageHashSymbol = messageHash + ':' + symbol + client.resolve(self.myTrades, messageHashSymbol) + + def handle_order(self, client: Client, message): + parsed = self.parse_ws_order(message) + symbol = self.safe_string(parsed, 'symbol') + orderId = self.safe_string(parsed, 'id') + if symbol is not None: + if self.orders is None: + limit = self.safe_integer(self.options, 'ordersLimit', 1000) + self.orders = ArrayCacheBySymbolById(limit) + cachedOrders = self.orders + orders = self.safe_value(cachedOrders.hashmap, symbol, {}) + order = self.safe_value(orders, orderId) + if order is not None: + fee = self.safe_value(order, 'fee') + if fee is not None: + parsed['fee'] = fee + fees = self.safe_value(order, 'fees') + if fees is not None: + parsed['fees'] = fees + parsed['trades'] = self.safe_value(order, 'trades') + timestamp = self.safe_integer(parsed, 'timestamp') + if timestamp is None: + parsed['timestamp'] = self.safe_integer(order, 'timestamp') + parsed['datetime'] = self.safe_string(order, 'datetime') + cachedOrders.append(parsed) + messageHash = 'orders' + symbolSpecificMessageHash = 'orders:' + symbol + client.resolve(cachedOrders, messageHash) + client.resolve(cachedOrders, symbolSpecificMessageHash) + + def handle_acount_update(self, client, message): + self.handle_balance(client, message) + self.handle_positions(client, message) + + def handle_ws_error(self, client: Client, message): + # + # { + # "error": { + # "code": 2, + # "msg": "Invalid request: invalid stream" + # }, + # "id": 1 + # } + # + id = self.safe_string(message, 'id') + rejected = False + error = self.safe_dict(message, 'error', {}) + code = self.safe_integer(error, 'code') + msg = self.safe_string(error, 'msg') + try: + self.handle_errors(code, msg, client.url, '', {}, self.json(error), error, {}, {}) + except Exception as e: + rejected = True + # private endpoint uses id + client.reject(e, id) + # public endpoint stores messageHash in subscriptions + subscriptionKeys = list(client.subscriptions.keys()) + for i in range(0, len(subscriptionKeys)): + subscriptionHash = subscriptionKeys[i] + subscriptionId = self.safe_string(client.subscriptions[subscriptionHash], 'id') + subscription = self.safe_string(client.subscriptions[subscriptionHash], 'subscription') + if id == subscriptionId: + client.reject(e, subscriptionHash) + if subscription is not None: + del client.subscriptions[subscription] + if not rejected: + client.reject(message, id) + # reset connection if 5xx error + codeString = self.safe_string(error, 'code') + if (codeString is not None) and (codeString[0] == '5'): + client.reset(message) + + def handle_event_stream_terminated(self, client: Client, message): + # + # { + # e: 'eventStreamTerminated', + # E: 1757896885229 + # } + # + event = self.safe_string(message, 'e') + subscriptions = client.subscriptions + subscriptionsKeys = list(subscriptions.keys()) + accountType = self.get_account_type_from_subscriptions(subscriptionsKeys) + if event == 'eventStreamTerminated': + del client.subscriptions[accountType] + client.reject(message, accountType) + + def handle_message(self, client: Client, message): + # handle WebSocketAPI + eventMsg = self.safe_dict(message, 'event') + if eventMsg is not None: + message = eventMsg + status = self.safe_string(message, 'status') + error = self.safe_value(message, 'error') + if (error is not None) or (status is not None and status != '200'): + self.handle_ws_error(client, message) + return + # user subscription wraps message in subscriptionId and event + id = self.safe_string(message, 'id') + subscriptions = self.safe_value(client.subscriptions, id) + method = self.safe_value(subscriptions, 'method') + if method is not None: + method(client, message) + return + # handle other APIs + methods: dict = { + 'depthUpdate': self.handle_order_book, + 'trade': self.handle_trade, + 'aggTrade': self.handle_trade, + 'kline': self.handle_ohlcv, + 'markPrice_kline': self.handle_ohlcv, + 'indexPrice_kline': self.handle_ohlcv, + '1hTicker@arr': self.handle_tickers, + '4hTicker@arr': self.handle_tickers, + '1dTicker@arr': self.handle_tickers, + '24hrTicker@arr': self.handle_tickers, + '24hrMiniTicker@arr': self.handle_tickers, + '1hTicker': self.handle_tickers, + '4hTicker': self.handle_tickers, + '1dTicker': self.handle_tickers, + '24hrTicker': self.handle_tickers, + '24hrMiniTicker': self.handle_tickers, + 'markPriceUpdate': self.handle_mark_prices, + 'markPriceUpdate@arr': self.handle_mark_prices, + 'bookTicker': self.handle_bids_asks, # there is no "bookTicker@arr" endpoint + 'outboundAccountPosition': self.handle_balance, + 'balanceUpdate': self.handle_balance, + 'ACCOUNT_UPDATE': self.handle_acount_update, + 'executionReport': self.handle_order_update, + 'ORDER_TRADE_UPDATE': self.handle_order_update, + 'forceOrder': self.handle_liquidation, + 'eventStreamTerminated': self.handle_event_stream_terminated, + 'externalLockUpdate': self.handle_balance, + } + event = self.safe_string(message, 'e') + if isinstance(message, list): + data = message[0] + event = self.safe_string(data, 'e') + '@arr' + method = self.safe_value(methods, event) + if method is None: + requestId = self.safe_string(message, 'id') + if requestId is not None: + self.handle_subscription_status(client, message) + return + # special case for the real-time bookTicker, since it comes without an event identifier + # + # { + # "u": 7488717758, + # "s": "BTCUSDT", + # "b": "28621.74000000", + # "B": "1.43278800", + # "a": "28621.75000000", + # "A": "2.52500800" + # } + # + if event is None and ('a' in message) and ('b' in message): + self.handle_bids_asks(client, message) + else: + method(client, message) diff --git a/ccxt/pro/binancecoinm.py b/ccxt/pro/binancecoinm.py new file mode 100644 index 0000000..494eed7 --- /dev/null +++ b/ccxt/pro/binancecoinm.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.pro.binance import binance +from ccxt.base.types import Any + +import ccxt.async_support.binancecoinm as binancecoinmRest + + +class binancecoinm(binance): + + def describe(self) -> Any: + # eslint-disable-next-line new-cap + restInstance = binancecoinmRest() + restDescribe = restInstance.describe() + extended = self.deep_extend(super(binancecoinm, self).describe(), restDescribe) + return self.deep_extend(extended, { + 'id': 'binancecoinm', + 'name': 'Binance COIN-M', + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/117738721-668c8d80-b205-11eb-8c49-3fad84c4a07f.jpg', + 'doc': 'https://developers.binance.com/en', + }, + 'options': { + 'fetchMarkets': { + 'types': ['inverse'], + }, + 'defaultSubType': 'inverse', + }, + }) diff --git a/ccxt/pro/binanceus.py b/ccxt/pro/binanceus.py new file mode 100644 index 0000000..5c2575d --- /dev/null +++ b/ccxt/pro/binanceus.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.pro.binance import binance +from ccxt.base.types import Any + +import ccxt.async_support.binanceus as binanceusRest + + +class binanceus(binance): + + def describe(self) -> Any: + # eslint-disable-next-line new-cap + restInstance = binanceusRest() + restDescribe = restInstance.describe() + parentWsDescribe = super(binanceus, self).describe_data() + extended = self.deep_extend(restDescribe, parentWsDescribe) + return self.deep_extend(extended, { + 'id': 'binanceus', + 'name': 'Binance US', + 'countries': ['US'], # US + 'certified': False, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/65177307-217b7c80-da5f-11e9-876e-0b748ba0a358.jpg', + 'api': { + 'ws': { + 'spot': 'wss://stream.binance.us:9443/ws', + }, + 'web': 'https://www.binance.us', + 'sapi': 'https://api.binance.us/sapi/v1', + 'wapi': 'https://api.binance.us/wapi/v3', + 'public': 'https://api.binance.us/api/v3', + 'private': 'https://api.binance.us/api/v3', + 'v3': 'https://api.binance.us/api/v3', + 'v1': 'https://api.binance.us/api/v1', + }, + 'www': 'https://www.binance.us', + 'referral': 'https://www.binance.us/?ref=35005074', + 'doc': 'https://github.com/binance-us/binance-official-api-docs', + 'fees': 'https://www.binance.us/en/fee/schedule', + }, + 'has': { + 'createOrderWithTakeProfitAndStopLossWs': False, + 'createReduceOnlyOrderWs': False, + 'createStopLossOrderWs': False, + 'createTakeProfitOrderWs': False, + 'fetchPositionForSymbolWs': False, + 'fetchPositionsForSymbolWs': False, + 'fetchPositionsWs': False, + 'fetchPositionWs': False, + 'unWatchPositions': False, + 'watchLiquidations': False, + 'watchLiquidationsForSymbols': False, + 'watchMarkPrice': False, + 'watchMarkPrices': False, + 'watchMyLiquidations': False, + 'watchMyLiquidationsForSymbols': False, + 'watchPosition': False, + 'watchPositions': False, + }, + 'options': { + 'fetchCurrencies': False, + 'quoteOrderQty': False, + 'defaultType': 'spot', + 'fetchMarkets': { + 'types': ['spot'], + }, + }, + }) diff --git a/ccxt/pro/binanceusdm.py b/ccxt/pro/binanceusdm.py new file mode 100644 index 0000000..e116d5d --- /dev/null +++ b/ccxt/pro/binanceusdm.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.pro.binance import binance +from ccxt.base.types import Any +from ccxt.base.errors import InvalidOrder + + +class binanceusdm(binance): + + def describe(self) -> Any: + return self.deep_extend(super(binanceusdm, self).describe(), { + 'id': 'binanceusdm', + 'name': 'Binance USDⓈ-M', + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/117738721-668c8d80-b205-11eb-8c49-3fad84c4a07f.jpg', + 'doc': 'https://developers.binance.com/en', + }, + 'options': { + 'fetchMarkets': { + 'types': ['linear'], + }, + 'defaultSubType': 'linear', + }, + # https://binance-docs.github.io/apidocs/futures/en/#error-codes + # https://developers.binance.com/docs/derivatives/usds-margined-futures/error-code + 'exceptions': { + 'exact': { + '-5021': InvalidOrder, # {"code":-5021,"msg":"Due to the order could not be filled immediately, the FOK order has been rejected."} + '-5022': InvalidOrder, # {"code":-5022,"msg":"Due to the order could not be executed, the Post Only order will be rejected."} + '-5028': InvalidOrder, # {"code":-5028,"msg":"Timestamp for self request is outside of the ME recvWindow."} + }, + }, + }) diff --git a/ccxt/pro/bingx.py b/ccxt/pro/bingx.py new file mode 100644 index 0000000..dad7d49 --- /dev/null +++ b/ccxt/pro/bingx.py @@ -0,0 +1,1459 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolById, ArrayCacheByTimestamp +from ccxt.base.types import Any, Balances, Int, Market, Order, OrderBook, Str, Ticker, Trade +from ccxt.async_support.base.ws.client import Client +from typing import List +from ccxt.base.errors import BadRequest +from ccxt.base.errors import NotSupported +from ccxt.base.errors import NetworkError + + +class bingx(ccxt.async_support.bingx): + + def describe(self) -> Any: + return self.deep_extend(super(bingx, self).describe(), { + 'has': { + 'ws': True, + 'watchTrades': True, + 'watchTradesForSymbols': False, + 'watchOrderBook': True, + 'watchOrderBookForSymbols': False, # no longer supported + 'watchOHLCV': True, + 'watchOHLCVForSymbols': False, # no longer supported + 'watchOrders': True, + 'watchMyTrades': True, + 'watchTicker': True, + 'watchTickers': False, # no longer supported + 'watchBalance': True, + 'unWatchOHLCV': True, + 'unWatchOrderBook': True, + 'unWatchTicker': True, + 'unWatchTrades': True, + }, + 'urls': { + 'api': { + 'ws': { + 'spot': 'wss://open-api-ws.bingx.com/market', + 'linear': 'wss://open-api-swap.bingx.com/swap-market', + 'inverse': 'wss://open-api-cswap-ws.bingx.com/market', + }, + }, + }, + 'options': { + 'listenKeyRefreshRate': 3540000, # 1 hour(59 mins so we have 1 min to renew the token) + 'ws': { + 'gunzip': True, + }, + 'swap': { + 'timeframes': { + '1m': '1m', + '3m': '3m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1h', + '2h': '2h', + '4h': '4h', + '6h': '6h', + '12h': '12h', + '1d': '1d', + '3d': '3d', + '1w': '1w', + '1M': '1M', + }, + }, + 'spot': { + 'timeframes': { + '1m': '1min', + '5m': '5min', + '15m': '15min', + '30m': '30min', + '1h': '60min', + '1d': '1day', + }, + }, + 'watchBalance': { + 'fetchBalanceSnapshot': True, # needed to be True to keep track of used and free balance + 'awaitBalanceSnapshot': False, # whether to wait for the balance snapshot before providing updates + }, + 'watchOrderBook': { + 'depth': 100, # 5, 10, 20, 50, 100 + # 'interval': 500, # 100, 200, 500, 1000 + }, + 'watchTrades': { + 'ignoreDuplicates': True, + }, + }, + 'streaming': { + 'keepAlive': 1800000, # 30 minutes + }, + }) + + async def un_watch(self, messageHash: str, subMessageHash: str, subscribeHash: str, dataType: str, topic: str, market: Market, methodName: str, params={}) -> Any: + marketType = None + subType = None + url = None + marketType, params = self.handle_market_type_and_params(methodName, market, params) + subType, params = self.handle_sub_type_and_params(methodName, market, params, 'linear') + if marketType == 'swap': + url = self.safe_string(self.urls['api']['ws'], subType) + else: + url = self.safe_string(self.urls['api']['ws'], marketType) + id = self.uuid() + request: dict = { + 'id': id, + 'dataType': dataType, + 'reqType': 'unsub', + } + symbols = [] + if market is not None: + symbols.append(market['symbol']) + subscription: dict = { + 'unsubscribe': True, + 'id': id, + 'subMessageHashes': [subMessageHash], + 'messageHashes': [messageHash], + 'symbols': symbols, + 'topic': topic, + } + symbolsAndTimeframes = self.safe_list(params, 'symbolsAndTimeframes') + if symbolsAndTimeframes is not None: + subscription['symbolsAndTimeframes'] = symbolsAndTimeframes + params = self.omit(params, 'symbolsAndTimeframes') + return await self.watch(url, messageHash, self.extend(request, params), subscribeHash, subscription) + + async def watch_ticker(self, symbol: str, params={}) -> Ticker: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://bingx-api.github.io/docs/#/en-us/spot/socket/market.html#Subscribe%20to%2024-hour%20Price%20Change + https://bingx-api.github.io/docs/#/en-us/swapV2/socket/market.html#Subscribe%20to%2024-hour%20price%20changes + https://bingx-api.github.io/docs/#/en-us/cswap/socket/market.html#Subscribe%20to%2024-Hour%20Price%20Change + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + marketType = None + subType = None + url = None + marketType, params = self.handle_market_type_and_params('watchTicker', market, params) + subType, params = self.handle_sub_type_and_params('watchTicker', market, params, 'linear') + if marketType == 'swap': + url = self.safe_string(self.urls['api']['ws'], subType) + else: + url = self.safe_string(self.urls['api']['ws'], marketType) + dataType = market['id'] + '@ticker' + messageHash = self.get_message_hash('ticker', market['symbol']) + uuid = self.uuid() + request: dict = { + 'id': uuid, + 'dataType': dataType, + } + if marketType == 'swap': + request['reqType'] = 'sub' + subscription: dict = { + 'unsubscribe': False, + 'id': uuid, + } + return await self.watch(url, messageHash, self.extend(request, params), messageHash, subscription) + + async def un_watch_ticker(self, symbol: str, params={}) -> Any: + """ + unWatches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list + + https://bingx-api.github.io/docs/#/en-us/spot/socket/market.html#Subscribe%20to%2024-hour%20Price%20Change + https://bingx-api.github.io/docs/#/en-us/swapV2/socket/market.html#Subscribe%20to%2024-hour%20price%20changes + https://bingx-api.github.io/docs/#/en-us/cswap/socket/market.html#Subscribe%20to%2024-Hour%20Price%20Change + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + dataType = market['id'] + '@ticker' + subMessageHash = self.get_message_hash('ticker', market['symbol']) + messageHash = 'unsubscribe::' + subMessageHash + topic = 'ticker' + methodName = 'unWatchTicker' + return await self.un_watch(messageHash, subMessageHash, messageHash, dataType, topic, market, methodName, params) + + def handle_ticker(self, client: Client, message): + # + # swap + # + # { + # "code": 0, + # "dataType": "BTC-USDT@ticker", + # "data": { + # "e": "24hTicker", + # "E": 1706498923556, + # "s": "BTC-USDT", + # "p": "346.4", + # "P": "0.82", + # "c": "42432.5", + # "L": "0.0529", + # "h": "42855.4", + # "l": "41578.3", + # "v": "64310.9754", + # "q": "2728360284.15", + # "o": "42086.1", + # "O": 1706498922655, + # "C": 1706498883023, + # "A": "42437.8", + # "a": "1.4160", + # "B": "42437.1", + # "b": "2.5747" + # } + # } + # + # spot + # + # { + # "code": 0, + # "timestamp": 1706506795473, + # "data": { + # "e": "24hTicker", + # "E": 1706506795472, + # "s": "BTC-USDT", + # "p": -372.12, + # "P": "-0.87%", + # "o": 42548.95, + # "h": 42696.1, + # "l": 41621.29, + # "c": 42176.83, + # "v": 4943.33, + # "q": 208842236.5, + # "O": 1706420395472, + # "C": 1706506795472, + # "A": 42177.23, + # "a": 5.14484, + # "B": 42176.38, + # "b": 5.36117 + # } + # } + # + data = self.safe_value(message, 'data', {}) + marketId = self.safe_string(data, 's') + # marketId = messageHash.split('@')[0] + isSwap = client.url.find('swap') >= 0 + marketType = 'swap' if isSwap else 'spot' + market = self.safe_market(marketId, None, None, marketType) + symbol = market['symbol'] + ticker = self.parse_ws_ticker(data, market) + self.tickers[symbol] = ticker + client.resolve(ticker, self.get_message_hash('ticker', symbol)) + if self.safe_string(message, 'dataType') == 'all@ticker': + client.resolve(ticker, self.get_message_hash('ticker')) + + def parse_ws_ticker(self, message, market=None): + # + # { + # "e": "24hTicker", + # "E": 1706498923556, + # "s": "BTC-USDT", + # "p": "346.4", + # "P": "0.82", + # "c": "42432.5", + # "L": "0.0529", + # "h": "42855.4", + # "l": "41578.3", + # "v": "64310.9754", + # "q": "2728360284.15", + # "o": "42086.1", + # "O": 1706498922655, + # "C": 1706498883023, + # "A": "42437.8", + # "a": "1.4160", + # "B": "42437.1", + # "b": "2.5747" + # } + # + timestamp = self.safe_integer(message, 'C') + marketId = self.safe_string(message, 's') + market = self.safe_market(marketId, market) + close = self.safe_string(message, 'c') + return self.safe_ticker({ + 'symbol': market['symbol'], + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(message, 'h'), + 'low': self.safe_string(message, 'l'), + 'bid': self.safe_string(message, 'B'), + 'bidVolume': self.safe_string(message, 'b'), + 'ask': self.safe_string(message, 'A'), + 'askVolume': self.safe_string(message, 'a'), + 'vwap': None, + 'open': self.safe_string(message, 'o'), + 'close': close, + 'last': close, + 'previousClose': None, + 'change': self.safe_string(message, 'p'), + 'percentage': None, + 'average': None, + 'baseVolume': self.safe_string(message, 'v'), + 'quoteVolume': self.safe_string(message, 'q'), + 'info': message, + }, market) + + def get_order_book_limit_by_market_type(self, marketType: str, limit: Int = None): + if limit is None: + limit = 100 + else: + if marketType == 'swap' or marketType == 'future': + limit = self.find_nearest_ceiling([5, 10, 20, 50, 100], limit) + elif marketType == 'spot': + limit = self.find_nearest_ceiling([20, 100], limit) + return limit + + def get_message_hash(self, unifiedChannel: str, symbol: Str = None, extra: Str = None): + hash = unifiedChannel + if symbol is not None: + hash += '::' + symbol + else: + hash += 's' # tickers, orderbooks, ohlcvs ... + if extra is not None: + hash += '::' + extra + return hash + + async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + watches information on multiple trades made in a market + + https://bingx-api.github.io/docs/#/en-us/spot/socket/market.html#Subscription%20transaction%20by%20transaction + https://bingx-api.github.io/docs/#/en-us/swapV2/socket/market.html#Subscribe%20the%20Latest%20Trade%20Detail + https://bingx-api.github.io/docs/#/en-us/cswap/socket/market.html#Subscription%20transaction%20by%20transaction + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + marketType = None + subType = None + url = None + marketType, params = self.handle_market_type_and_params('watchTrades', market, params) + subType, params = self.handle_sub_type_and_params('watchTrades', market, params, 'linear') + if marketType == 'swap': + url = self.safe_string(self.urls['api']['ws'], subType) + else: + url = self.safe_string(self.urls['api']['ws'], marketType) + rawHash = market['id'] + '@trade' + messageHash = 'trade::' + symbol + uuid = self.uuid() + request: dict = { + 'id': uuid, + 'dataType': rawHash, + } + if marketType == 'swap': + request['reqType'] = 'sub' + subscription: dict = { + 'unsubscribe': False, + 'id': uuid, + } + trades = await self.watch(url, messageHash, self.extend(request, params), messageHash, subscription) + if self.newUpdates: + limit = trades.getLimit(symbol, limit) + result = self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + if self.handle_option('watchTrades', 'ignoreDuplicates', True): + filtered = self.remove_repeated_trades_from_array(result) + filtered = self.sort_by(filtered, 'timestamp') + return filtered + return result + + async def un_watch_trades(self, symbol: str, params={}) -> Any: + """ + unsubscribes from the trades channel + + https://bingx-api.github.io/docs/#/en-us/spot/socket/market.html#Subscription%20transaction%20by%20transaction + https://bingx-api.github.io/docs/#/en-us/swapV2/socket/market.html#Subscribe%20the%20Latest%20Trade%20Detail + https://bingx-api.github.io/docs/#/en-us/cswap/socket/market.html#Subscription%20transaction%20by%20transaction + + :param str symbol: unified symbol of the market to fetch trades for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.name]: the name of the method to call, 'trade' or 'aggTrade', default is 'trade' + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + dataType = market['id'] + '@trade' + subMessageHash = self.get_message_hash('trade', market['symbol']) + messageHash = 'unsubscribe::' + subMessageHash + topic = 'trades' + methodName = 'unWatchTrades' + return await self.un_watch(messageHash, subMessageHash, messageHash, dataType, topic, market, methodName, params) + + def handle_trades(self, client: Client, message): + # + # spot: first snapshot + # + # { + # "id": "d83b78ce-98be-4dc2-b847-12fe471b5bc5", + # "code": 0, + # "msg": "SUCCESS", + # "timestamp": 1690214699854 + # } + # + # spot: subsequent updates + # + # { + # "code": 0, + # "data": { + # "E": 1690214529432, + # "T": 1690214529386, + # "e": "trade", + # "m": True, + # "p": "29110.19", + # "q": "0.1868", + # "s": "BTC-USDT", + # "t": "57903921" + # }, + # "dataType": "BTC-USDT@trade", + # "success": True + # } + # + # linear swap: first snapshot + # + # { + # "id": "2aed93b1-6e1e-4038-aeba-f5eeaec2ca48", + # "code": 0, + # "msg": '', + # "dataType": '', + # "data": null + # } + # + # linear swap: subsequent updates + # + # { + # "code": 0, + # "dataType": "BTC-USDT@trade", + # "data": [ + # { + # "q": "0.0421", + # "p": "29023.5", + # "T": 1690221401344, + # "m": False, + # "s": "BTC-USDT" + # }, + # ... + # ] + # } + # + # inverse swap: first snapshot + # + # { + # "code": 0, + # "id": "a2e482ca-f71b-42f8-a83a-8ff85a713e64", + # "msg": "SUCCESS", + # "timestamp": 1722920589426 + # } + # + # inverse swap: subsequent updates + # + # { + # "code": 0, + # "dataType": "BTC-USD@trade", + # "data": { + # "e": "trade", + # "E": 1722920589665, + # "s": "BTC-USD", + # "t": "39125001", + # "p": "55360.0", + # "q": "1", + # "T": 1722920589582, + # "m": False + # } + # } + # + data = self.safe_value(message, 'data', []) + rawHash = self.safe_string(message, 'dataType') + marketId = rawHash.split('@')[0] + isSwap = client.url.find('swap') >= 0 + marketType = 'swap' if isSwap else 'spot' + market = self.safe_market(marketId, None, None, marketType) + symbol = market['symbol'] + messageHash = 'trade::' + symbol + trades = None + if isinstance(data, list): + trades = self.parse_trades(data, market) + else: + trades = [self.parse_trade(data, market)] + stored = self.safe_value(self.trades, symbol) + if stored is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + stored = ArrayCache(limit) + self.trades[symbol] = stored + for j in range(0, len(trades)): + stored.append(trades[j]) + client.resolve(stored, messageHash) + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://bingx-api.github.io/docs/#/en-us/spot/socket/market.html#Subscribe%20Market%20Depth%20Data + https://bingx-api.github.io/docs/#/en-us/swapV2/socket/market.html#Subscribe%20Market%20Depth%20Data + https://bingx-api.github.io/docs/#/en-us/cswap/socket/market.html#Subscribe%20to%20Limited%20Depth + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + marketType = None + subType = None + url = None + marketType, params = self.handle_market_type_and_params('watchOrderBook', market, params) + subType, params = self.handle_sub_type_and_params('watchOrderBook', market, params, 'linear') + if marketType == 'swap': + url = self.safe_string(self.urls['api']['ws'], subType) + else: + url = self.safe_string(self.urls['api']['ws'], marketType) + options = self.safe_dict(self.options, 'watchOrderBook', {}) + depth = self.safe_integer(options, 'depth', 100) + subscriptionHash = market['id'] + '@' + 'depth' + self.number_to_string(depth) + messageHash = self.get_message_hash('orderbook', market['symbol']) + uuid = self.uuid() + request: dict = { + 'id': uuid, + 'dataType': subscriptionHash, + } + if marketType == 'swap': + request['reqType'] = 'sub' + subscriptionArgs: dict = {} + if market['inverse']: + subscriptionArgs = { + 'id': uuid, + 'unsubscribe': False, + 'count': limit, + 'params': params, + } + else: + subscriptionArgs = { + 'id': uuid, + 'unsubscribe': False, + 'level': limit, + 'params': params, + } + orderbook = await self.watch(url, messageHash, self.deep_extend(request, params), subscriptionHash, subscriptionArgs) + return orderbook.limit() + + async def un_watch_order_book(self, symbol: str, params={}) -> Any: + """ + unWatches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://bingx-api.github.io/docs/#/en-us/spot/socket/market.html#Subscribe%20Market%20Depth%20Data + https://bingx-api.github.io/docs/#/en-us/swapV2/socket/market.html#Subscribe%20Market%20Depth%20Data + https://bingx-api.github.io/docs/#/en-us/cswap/socket/market.html#Subscribe%20to%20Limited%20Depth + + :param str symbol: unified symbol of the market + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + options = self.safe_dict(self.options, 'watchOrderBook', {}) + depth = self.safe_integer(options, 'depth', 100) + subMessageHash = market['id'] + '@' + 'depth' + self.number_to_string(depth) + messageHash = 'unsubscribe::' + subMessageHash + topic = 'orderbook' + methodName = 'unWatchOrderBook' + return await self.un_watch(messageHash, subMessageHash, messageHash, subMessageHash, topic, market, methodName, params) + + def handle_delta(self, bookside, delta): + price = self.safe_float_2(delta, 0, 'p') + amount = self.safe_float_2(delta, 1, 'a') + bookside.store(price, amount) + + def handle_order_book(self, client: Client, message): + # + # spot + # + # { + # "code":0, + # "data": + # { + # "asks":[ + # ["84119.73","0.000011"], + # ["84116.52","0.000014"], + # ["84116.40","0.000039"] + # ], + # "bids":[ + # ["83656.98","2.570805"], + # ["83655.51","0.000347"], + # ["83654.59","0.000082"] + # ], + # "lastUpdateId":13565694850 + # }, + # "dataType":"BTC-USDT@depth100", + # "success":true, + # "timestamp":1743241379958 + # } + # + # linear swap + # + # { + # "code":0, + # "dataType":"BTC-USDT@depth100@500ms", + # "ts":1743241563651, + # "data": + # { + # "bids":[ + # ["83363.2","0.1908"], + # ["83360.0","0.0003"], + # ["83356.5","0.0245"], + # ], + # "asks":[ + # ["83495.0","0.0024"], + # ["83490.0","0.0001"], + # ["83488.0","0.0004"], + # ] + # } + # } + # + # inverse swap + # + # { + # "code":0, + # "dataType":"BTC-USD@depth100", + # "data":{ + # "symbol":"BTC-USD", + # "bids":[ + # {"p":"83411.2","a":"2.979216","v":"2485.0"}, + # {"p":"83411.1","a":"1.592114","v":"1328.0"}, + # {"p":"83410.8","a":"2.656730","v":"2216.0"}, + # ], + # "asks":[ + # {"p":"88200.0","a":"0.344671","v":"304.0"}, + # {"p":"88023.8","a":"0.045442","v":"40.0"}, + # {"p":"88001.0","a":"0.003409","v":"3.0"}, + # ], + # "aggPrecision":"0.1", + # "timestamp":1743242290710 + # } + # } + # + data = self.safe_dict(message, 'data', {}) + dataType = self.safe_string(message, 'dataType') + parts = dataType.split('@') + firstPart = parts[0] + isAllEndpoint = (firstPart == 'all') + marketId = self.safe_string(data, 'symbol', firstPart) + isSwap = client.url.find('swap') >= 0 + marketType = 'swap' if isSwap else 'spot' + market = self.safe_market(marketId, None, None, marketType) + symbol = market['symbol'] + orderbook = self.safe_value(self.orderbooks, symbol) + if orderbook is None: + # limit = [5, 10, 20, 50, 100] + subscriptionHash = dataType + subscription = client.subscriptions[subscriptionHash] + limit = self.safe_integer(subscription, 'limit') + self.orderbooks[symbol] = self.order_book({}, limit) + orderbook = self.orderbooks[symbol] + snapshot = None + timestamp = self.safe_integer_2(message, 'timestamp', 'ts') + timestamp = self.safe_integer_2(data, 'timestamp', 'ts', timestamp) + if market['inverse']: + snapshot = self.parse_order_book(data, symbol, timestamp, 'bids', 'asks', 'p', 'a') + else: + snapshot = self.parse_order_book(data, symbol, timestamp, 'bids', 'asks', 0, 1) + nonce = self.safe_integer(data, 'lastUpdateId') + snapshot['nonce'] = nonce + orderbook.reset(snapshot) + messageHash = self.get_message_hash('orderbook', symbol) + client.resolve(orderbook, messageHash) + # resolve for "all" + if isAllEndpoint: + messageHashForAll = self.get_message_hash('orderbook') + client.resolve(orderbook, messageHashForAll) + + def parse_ws_ohlcv(self, ohlcv, market=None) -> list: + # + # { + # "c": "28909.0", + # "o": "28915.4", + # "h": "28915.4", + # "l": "28896.1", + # "v": "27.6919", + # "T": 1696687499999, + # "t": 1696687440000 + # } + # + # for spot, opening-time(t) is used instead of closing-time(T), to be compatible with fetchOHLCV + # for linear swap,(T) is the opening time + timestamp = 't' if (market['spot']) else 'T' + if market['swap']: + timestamp = 't' if (market['inverse']) else 'T' + return [ + self.safe_integer(ohlcv, timestamp), + self.safe_number(ohlcv, 'o'), + self.safe_number(ohlcv, 'h'), + self.safe_number(ohlcv, 'l'), + self.safe_number(ohlcv, 'c'), + self.safe_number(ohlcv, 'v'), + ] + + def handle_ohlcv(self, client: Client, message): + # + # spot: + # + # { + # "code": 0, + # "data": { + # "E": 1696687498608, + # "K": { + # "T": 1696687499999, + # "c": "27917.829", + # "h": "27918.427", + # "i": "1min", + # "l": "27917.7", + # "n": 262, + # "o": "27917.91", + # "q": "25715.359197", + # "s": "BTC-USDT", + # "t": 1696687440000, + # "v": "0.921100" + # }, + # "e": "kline", + # "s": "BTC-USDT" + # }, + # "dataType": "BTC-USDT@kline_1min", + # "success": True + # } + # + # linear swap: + # + # { + # "code": 0, + # "dataType": "BTC-USDT@kline_1m", + # "s": "BTC-USDT", + # "data": [ + # { + # "c": "28909.0", + # "o": "28915.4", + # "h": "28915.4", + # "l": "28896.1", + # "v": "27.6919", + # "T": 1690907580000 + # } + # ] + # } + # + # inverse swap: + # + # { + # "code": 0, + # "timestamp": 1723769354547, + # "dataType": "BTC-USD@kline_1m", + # "data": { + # "t": 1723769340000, + # "o": 57485.1, + # "c": 57468, + # "l": 57464.9, + # "h": 57485.1, + # "a": 0.189663, + # "v": 109, + # "u": 92, + # "s": "BTC-USD" + # } + # } + # + isSwap = client.url.find('swap') >= 0 + dataType = self.safe_string(message, 'dataType') + parts = dataType.split('@') + firstPart = parts[0] + isAllEndpoint = (firstPart == 'all') + marketId = self.safe_string(message, 's', firstPart) + marketType = 'swap' if isSwap else 'spot' + market = self.safe_market(marketId, None, None, marketType) + candles = None + if isSwap: + if market['inverse']: + candles = [self.safe_dict(message, 'data', {})] + else: + candles = self.safe_list(message, 'data', []) + else: + data = self.safe_dict(message, 'data', {}) + candles = [self.safe_dict(data, 'K', {})] + symbol = market['symbol'] + self.ohlcvs[symbol] = self.safe_value(self.ohlcvs, symbol, {}) + rawTimeframe = dataType.split('_')[1] + marketOptions = self.safe_dict(self.options, marketType) + timeframes = self.safe_dict(marketOptions, 'timeframes', {}) + unifiedTimeframe = self.find_timeframe(rawTimeframe, timeframes) + if self.safe_value(self.ohlcvs[symbol], rawTimeframe) is None: + subscriptionHash = dataType + subscription = client.subscriptions[subscriptionHash] + limit = self.safe_integer(subscription, 'limit') + self.ohlcvs[symbol][unifiedTimeframe] = ArrayCacheByTimestamp(limit) + stored = self.ohlcvs[symbol][unifiedTimeframe] + for i in range(0, len(candles)): + candle = candles[i] + parsed = self.parse_ws_ohlcv(candle, market) + stored.append(parsed) + resolveData = [symbol, unifiedTimeframe, stored] + messageHash = self.get_message_hash('ohlcv', symbol, unifiedTimeframe) + client.resolve(resolveData, messageHash) + # resolve for "all" + if isAllEndpoint: + messageHashForAll = self.get_message_hash('ohlcv', None, unifiedTimeframe) + client.resolve(resolveData, messageHashForAll) + + async def watch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + watches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://bingx-api.github.io/docs/#/en-us/spot/socket/market.html#K-line%20Streams + https://bingx-api.github.io/docs/#/en-us/swapV2/socket/market.html#Subscribe%20K-Line%20Data + https://bingx-api.github.io/docs/#/en-us/cswap/socket/market.html#Subscribe%20to%20Latest%20Trading%20Pair%20K-Line + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + marketType = None + subType = None + url = None + marketType, params = self.handle_market_type_and_params('watchOHLCV', market, params) + subType, params = self.handle_sub_type_and_params('watchOHLCV', market, params, 'linear') + if marketType == 'swap': + url = self.safe_string(self.urls['api']['ws'], subType) + else: + url = self.safe_string(self.urls['api']['ws'], marketType) + if url is None: + raise BadRequest(self.id + ' watchOHLCV is not supported for ' + marketType + ' markets.') + options = self.safe_value(self.options, marketType, {}) + timeframes = self.safe_value(options, 'timeframes', {}) + rawTimeframe = self.safe_string(timeframes, timeframe, timeframe) + messageHash = self.get_message_hash('ohlcv', market['symbol'], timeframe) + subscriptionHash = market['id'] + '@kline_' + rawTimeframe + uuid = self.uuid() + request: dict = { + 'id': uuid, + 'dataType': subscriptionHash, + } + if marketType == 'swap': + request['reqType'] = 'sub' + subscriptionArgs: dict = { + 'id': uuid, + 'unsubscribe': False, + 'interval': rawTimeframe, + 'params': params, + } + result = await self.watch(url, messageHash, self.extend(request, params), subscriptionHash, subscriptionArgs) + ohlcv = result[2] + if self.newUpdates: + limit = ohlcv.getLimit(symbol, limit) + return self.filter_by_since_limit(ohlcv, since, limit, 0, True) + + async def un_watch_ohlcv(self, symbol: str, timeframe: str = '1m', params={}) -> Any: + """ + unWatches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://bingx-api.github.io/docs/#/en-us/spot/socket/market.html#K-line%20Streams + https://bingx-api.github.io/docs/#/en-us/swapV2/socket/market.html#Subscribe%20K-Line%20Data + https://bingx-api.github.io/docs/#/en-us/cswap/socket/market.html#Subscribe%20to%20Latest%20Trading%20Pair%20K-Line + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + options = self.safe_value(self.options, market['type'], {}) + timeframes = self.safe_value(options, 'timeframes', {}) + rawTimeframe = self.safe_string(timeframes, timeframe, timeframe) + subMessageHash = market['id'] + '@kline_' + rawTimeframe + messageHash = 'unsubscribe::' + subMessageHash + topic = 'ohlcv' + methodName = 'unWatchOHLCV' + symbolsAndTimeframes = [[market['symbol'], timeframe]] + params['symbolsAndTimeframes'] = symbolsAndTimeframes + return await self.un_watch(messageHash, subMessageHash, messageHash, subMessageHash, topic, market, methodName, params) + + async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + watches information on multiple orders made by the user + + https://bingx-api.github.io/docs/#/en-us/spot/socket/account.html#Subscription%20order%20update%20data + https://bingx-api.github.io/docs/#/en-us/swapV2/socket/account.html#Order%20update%20push + https://bingx-api.github.io/docs/#/en-us/cswap/socket/account.html#Order%20update%20push + + :param str [symbol]: unified market symbol of the market orders are made in + :param int [since]: the earliest time in ms to watch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + await self.authenticate() + type = None + subType = None + market = None + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + type, params = self.handle_market_type_and_params('watchOrders', market, params) + subType, params = self.handle_sub_type_and_params('watchOrders', market, params, 'linear') + isSpot = (type == 'spot') + spotHash = 'spot:private' + swapHash = 'swap:private' + subscriptionHash = spotHash if isSpot else swapHash + spotMessageHash = 'spot:order' + swapMessageHash = 'swap:order' + messageHash = spotMessageHash if isSpot else swapMessageHash + if market is not None: + messageHash += ':' + symbol + uuid = self.uuid() + baseUrl = None + request = None + if type == 'swap': + if subType == 'inverse': + raise NotSupported(self.id + ' watchOrders is not supported for inverse swap markets yet') + baseUrl = self.safe_string(self.urls['api']['ws'], subType) + else: + baseUrl = self.safe_string(self.urls['api']['ws'], type) + request = { + 'id': uuid, + 'reqType': 'sub', + 'dataType': 'spot.executionReport', + } + url = baseUrl + '?listenKey=' + self.options['listenKey'] + subscription: dict = { + 'unsubscribe': False, + 'id': uuid, + } + orders = await self.watch(url, messageHash, request, subscriptionHash, subscription) + if self.newUpdates: + limit = orders.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(orders, symbol, since, limit, True) + + async def watch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + watches information on multiple trades made by the user + + https://bingx-api.github.io/docs/#/en-us/spot/socket/account.html#Subscription%20order%20update%20data + https://bingx-api.github.io/docs/#/en-us/swapV2/socket/account.html#Order%20update%20push + https://bingx-api.github.io/docs/#/en-us/cswap/socket/account.html#Order%20update%20push + + :param str [symbol]: unified market symbol of the market the trades are made in + :param int [since]: the earliest time in ms to watch trades for + :param int [limit]: the maximum number of trade structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + await self.authenticate() + type = None + subType = None + market = None + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + type, params = self.handle_market_type_and_params('watchMyTrades', market, params) + subType, params = self.handle_sub_type_and_params('watchMyTrades', market, params, 'linear') + isSpot = (type == 'spot') + spotHash = 'spot:private' + swapHash = 'swap:private' + subscriptionHash = spotHash if isSpot else swapHash + spotMessageHash = 'spot:mytrades' + swapMessageHash = 'swap:mytrades' + messageHash = spotMessageHash if isSpot else swapMessageHash + if market is not None: + messageHash += ':' + symbol + uuid = self.uuid() + baseUrl = None + request = None + if type == 'swap': + if subType == 'inverse': + raise NotSupported(self.id + ' watchMyTrades is not supported for inverse swap markets yet') + baseUrl = self.safe_string(self.urls['api']['ws'], subType) + else: + baseUrl = self.safe_string(self.urls['api']['ws'], type) + request = { + 'id': uuid, + 'reqType': 'sub', + 'dataType': 'spot.executionReport', + } + url = baseUrl + '?listenKey=' + self.options['listenKey'] + subscription: dict = { + 'unsubscribe': False, + 'id': uuid, + } + trades = await self.watch(url, messageHash, request, subscriptionHash, subscription) + if self.newUpdates: + limit = trades.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(trades, symbol, since, limit, True) + + async def watch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://bingx-api.github.io/docs/#/en-us/spot/socket/account.html#Subscription%20account%20balance%20push + https://bingx-api.github.io/docs/#/en-us/swapV2/socket/account.html#Account%20balance%20and%20position%20update%20push + https://bingx-api.github.io/docs/#/en-us/cswap/socket/account.html#Account%20balance%20and%20position%20update%20push + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + await self.authenticate() + type = None + subType = None + type, params = self.handle_market_type_and_params('watchBalance', None, params) + subType, params = self.handle_sub_type_and_params('watchBalance', None, params, 'linear') + isSpot = (type == 'spot') + spotSubHash = 'spot:balance' + swapSubHash = 'swap:private' + spotMessageHash = 'spot:balance' + swapMessageHash = 'swap:balance' + messageHash = spotMessageHash if isSpot else swapMessageHash + subscriptionHash = spotSubHash if isSpot else swapSubHash + request = None + baseUrl = None + uuid = self.uuid() + if type == 'swap': + if subType == 'inverse': + raise NotSupported(self.id + ' watchBalance is not supported for inverse swap markets yet') + baseUrl = self.safe_string(self.urls['api']['ws'], subType) + else: + baseUrl = self.safe_string(self.urls['api']['ws'], type) + request = { + 'id': uuid, + 'dataType': 'ACCOUNT_UPDATE', + } + url = baseUrl + '?listenKey=' + self.options['listenKey'] + client = self.client(url) + self.set_balance_cache(client, type, subType, subscriptionHash, params) + fetchBalanceSnapshot = None + awaitBalanceSnapshot = None + fetchBalanceSnapshot, params = self.handle_option_and_params(params, 'watchBalance', 'fetchBalanceSnapshot', True) + awaitBalanceSnapshot, params = self.handle_option_and_params(params, 'watchBalance', 'awaitBalanceSnapshot', False) + if fetchBalanceSnapshot and awaitBalanceSnapshot: + await client.future(type + ':fetchBalanceSnapshot') + subscription: dict = { + 'unsubscribe': False, + 'id': uuid, + } + return await self.watch(url, messageHash, request, subscriptionHash, subscription) + + def set_balance_cache(self, client: Client, type, subType, subscriptionHash, params): + if subscriptionHash in client.subscriptions: + return + fetchBalanceSnapshot = self.handle_option_and_params(params, 'watchBalance', 'fetchBalanceSnapshot', True) + if fetchBalanceSnapshot: + messageHash = type + ':fetchBalanceSnapshot' + if not (messageHash in client.futures): + client.future(messageHash) + self.spawn(self.load_balance_snapshot, client, messageHash, type, subType) + else: + self.balance[type] = {} + + async def load_balance_snapshot(self, client, messageHash, type, subType): + response = await self.fetch_balance({'type': type, 'subType': subType}) + self.balance[type] = self.extend(response, self.safe_value(self.balance, type, {})) + # don't remove the future from the .futures cache + future = client.futures[messageHash] + future.resolve() + client.resolve(self.balance[type], type + ':balance') + + def handle_error_message(self, client, message): + # + # {code: 100400, msg: '', timestamp: 1696245808833} + # + # { + # "code": 100500, + # "id": "9cd37d32-da98-440b-bd04-37e7dbcf51ad", + # "msg": '', + # "timestamp": 1696245842307 + # } + code = self.safe_string(message, 'code') + try: + if code is not None: + feedback = self.id + ' ' + self.json(message) + self.throw_exactly_matched_exception(self.exceptions['exact'], code, feedback) + except Exception as e: + client.reject(e) + return True + + async def keep_alive_listen_key(self, params={}): + listenKey = self.safe_string(self.options, 'listenKey') + if listenKey is None: + # A network error happened: we can't renew a listen key that does not exist. + return + try: + await self.userAuthPrivatePutUserDataStream({'listenKey': listenKey}) # self.extend the expiry + except Exception as error: + types = ['spot', 'linear', 'inverse'] + for i in range(0, len(types)): + type = types[i] + url = self.urls['api']['ws'][type] + '?listenKey=' + listenKey + client = self.client(url) + messageHashes = list(client.futures.keys()) + for j in range(0, len(messageHashes)): + messageHash = messageHashes[j] + client.reject(error, messageHash) + self.options['listenKey'] = None + self.options['lastAuthenticatedTime'] = 0 + return + # whether or not to schedule another listenKey keepAlive request + listenKeyRefreshRate = self.safe_integer(self.options, 'listenKeyRefreshRate', 3600000) + self.delay(listenKeyRefreshRate, self.keep_alive_listen_key, params) + + async def authenticate(self, params={}): + time = self.milliseconds() + lastAuthenticatedTime = self.safe_integer(self.options, 'lastAuthenticatedTime', 0) + listenKeyRefreshRate = self.safe_integer(self.options, 'listenKeyRefreshRate', 3600000) # 1 hour + if time - lastAuthenticatedTime > listenKeyRefreshRate: + response = await self.userAuthPrivatePostUserDataStream() + self.options['listenKey'] = self.safe_string(response, 'listenKey') + self.options['lastAuthenticatedTime'] = time + self.delay(listenKeyRefreshRate, self.keep_alive_listen_key, params) + + async def pong(self, client, message): + # + # spot + # { + # "ping": "5963ba3db76049b2870f9a686b2ebaac", + # "time": "2023-10-02T18:51:55.089+0800" + # } + # swap + # Ping + # + try: + if message == 'Ping': + await client.send('Pong') + else: + ping = self.safe_string(message, 'ping') + time = self.safe_string(message, 'time') + await client.send({ + 'pong': ping, + 'time': time, + }) + except Exception as e: + error = NetworkError(self.id + ' pong failed with error ' + self.json(e)) + client.reset(error) + + def handle_order(self, client, message): + # + # { + # "code": 0, + # "dataType": "spot.executionReport", + # "data": { + # "e": "executionReport", + # "E": 1694680212947, + # "s": "LTC-USDT", + # "S": "BUY", + # "o": "LIMIT", + # "q": 0.1, + # "p": 50, + # "x": "NEW", + # "X": "PENDING", + # "i": 1702238305204043800, + # "l": 0, + # "z": 0, + # "L": 0, + # "n": 0, + # "N": "", + # "T": 0, + # "t": 0, + # "O": 1694680212676, + # "Z": 0, + # "Y": 0, + # "Q": 0, + # "m": False + # } + # } + # + # { + # "code": 0, + # "dataType": "spot.executionReport", + # "data": { + # "e": "executionReport", + # "E": 1694681809302, + # "s": "LTC-USDT", + # "S": "BUY", + # "o": "MARKET", + # "q": 0, + # "p": 62.29, + # "x": "TRADE", + # "X": "FILLED", + # "i": "1702245001712369664", + # "l": 0.0802, + # "z": 0.0802, + # "L": 62.308, + # "n": -0.0000802, + # "N": "LTC", + # "T": 1694681809256, + # "t": 38259147, + # "O": 1694681809248, + # "Z": 4.9971016, + # "Y": 4.9971016, + # "Q": 5, + # "m": False + # } + # } + # swap + # { + # "e": "ORDER_TRADE_UPDATE", + # "E": 1696843635475, + # "o": { + # "s": "LTC-USDT", + # "c": "", + # "i": "1711312357852147712", + # "S": "BUY", + # "o": "MARKET", + # "q": "0.10000000", + # "p": "64.35010000", + # "ap": "64.36000000", + # "x": "TRADE", + # "X": "FILLED", + # "N": "USDT", + # "n": "-0.00321800", + # "T": 0, + # "wt": "MARK_PRICE", + # "ps": "LONG", + # "rp": "0.00000000", + # "z": "0.10000000" + # } + # } + # + isSpot = ('dataType' in message) + data = self.safe_value_2(message, 'data', 'o', {}) + if self.orders is None: + limit = self.safe_integer(self.options, 'ordersLimit', 1000) + self.orders = ArrayCacheBySymbolById(limit) + stored = self.orders + parsedOrder = self.parse_order(data) + stored.append(parsedOrder) + symbol = parsedOrder['symbol'] + spotHash = 'spot:order' + swapHash = 'swap:order' + messageHash = spotHash if (isSpot) else swapHash + client.resolve(stored, messageHash) + client.resolve(stored, messageHash + ':' + symbol) + + def handle_my_trades(self, client: Client, message): + # + # + # { + # "code": 0, + # "dataType": "spot.executionReport", + # "data": { + # "e": "executionReport", + # "E": 1694681809302, + # "s": "LTC-USDT", + # "S": "BUY", + # "o": "MARKET", + # "q": 0, + # "p": 62.29, + # "x": "TRADE", + # "X": "FILLED", + # "i": "1702245001712369664", + # "l": 0.0802, + # "z": 0.0802, + # "L": 62.308, + # "n": -0.0000802, + # "N": "LTC", + # "T": 1694681809256, + # "t": 38259147, + # "O": 1694681809248, + # "Z": 4.9971016, + # "Y": 4.9971016, + # "Q": 5, + # "m": False + # } + # } + # + # swap + # { + # "e": "ORDER_TRADE_UPDATE", + # "E": 1696843635475, + # "o": { + # "s": "LTC-USDT", + # "c": "", + # "i": "1711312357852147712", + # "S": "BUY", + # "o": "MARKET", + # "q": "0.10000000", + # "p": "64.35010000", + # "ap": "64.36000000", + # "x": "TRADE", + # "X": "FILLED", + # "N": "USDT", + # "n": "-0.00321800", + # "T": 0, + # "wt": "MARK_PRICE", + # "ps": "LONG", + # "rp": "0.00000000", + # "z": "0.10000000" + # } + # } + # + isSpot = ('dataType' in message) + result = self.safe_dict_2(message, 'data', 'o', {}) + cachedTrades = self.myTrades + if cachedTrades is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + cachedTrades = ArrayCacheBySymbolById(limit) + self.myTrades = cachedTrades + type = 'spot' if isSpot else 'swap' + marketId = self.safe_string(result, 's') + market = self.safe_market(marketId, None, '-', type) + parsed = self.parse_trade(result, market) + symbol = parsed['symbol'] + spotHash = 'spot:mytrades' + swapHash = 'swap:mytrades' + messageHash = spotHash if isSpot else swapHash + cachedTrades.append(parsed) + client.resolve(cachedTrades, messageHash) + client.resolve(cachedTrades, messageHash + ':' + symbol) + + def handle_balance(self, client: Client, message): + # spot + # { + # "e":"ACCOUNT_UPDATE", + # "E":1696242817000, + # "T":1696242817142, + # "a":{ + # "B":[ + # { + # "a":"USDT", + # "bc":"-1.00000000000000000000", + # "cw":"86.59497382000000050000", + # "wb":"86.59497382000000050000" + # } + # ], + # "m":"ASSET_TRANSFER" + # } + # } + # swap + # { + # "e":"ACCOUNT_UPDATE", + # "E":1696244249320, + # "a":{ + # "m":"WITHDRAW", + # "B":[ + # { + # "a":"USDT", + # "wb":"49.81083984", + # "cw":"49.81083984", + # "bc":"-1.00000000" + # } + # ], + # "P":[ + # ] + # } + # } + # + a = self.safe_dict(message, 'a', {}) + data = self.safe_list(a, 'B', []) + timestamp = self.safe_integer_2(message, 'T', 'E') + type = 'swap' if ('P' in a) else 'spot' + if not (type in self.balance): + self.balance[type] = {} + self.balance[type]['info'] = data + self.balance[type]['timestamp'] = timestamp + self.balance[type]['datetime'] = self.iso8601(timestamp) + for i in range(0, len(data)): + balance = data[i] + currencyId = self.safe_string(balance, 'a') + code = self.safe_currency_code(currencyId) + account = self.account() + account['info'] = balance + account['used'] = self.safe_string(balance, 'lk') + account['free'] = self.safe_string(balance, 'wb') + self.balance[type][code] = account + self.balance[type] = self.safe_balance(self.balance[type]) + client.resolve(self.balance[type], type + ':balance') + + def handle_message(self, client: Client, message): + if not self.handle_error_message(client, message): + return + # public subscriptions + if (message == 'Ping') or ('ping' in message): + self.spawn(self.pong, client, message) + return + dataType = self.safe_string(message, 'dataType', '') + if dataType.find('@depth') >= 0: + self.handle_order_book(client, message) + return + if dataType.find('@ticker') >= 0: + self.handle_ticker(client, message) + return + if dataType.find('@trade') >= 0: + self.handle_trades(client, message) + return + if dataType.find('@kline') >= 0: + self.handle_ohlcv(client, message) + return + if dataType.find('executionReport') >= 0: + data = self.safe_value(message, 'data', {}) + type = self.safe_string(data, 'x') + if type == 'TRADE': + self.handle_my_trades(client, message) + self.handle_order(client, message) + return + e = self.safe_string(message, 'e') + if e == 'ACCOUNT_UPDATE': + self.handle_balance(client, message) + if e == 'ORDER_TRADE_UPDATE': + self.handle_order(client, message) + data = self.safe_value(message, 'o', {}) + type = self.safe_string(data, 'x') + status = self.safe_string(data, 'X') + if (type == 'TRADE') and (status == 'FILLED'): + self.handle_my_trades(client, message) + msgData = self.safe_value(message, 'data') + msgEvent = self.safe_string(msgData, 'e') + if msgEvent == '24hTicker': + self.handle_ticker(client, message) + if dataType == '' and msgEvent is None and e is None: + self.handle_subscription_status(client, message) + + def handle_subscription_status(self, client: Client, message): + # + # { + # "code": 0, + # "id": "b6ed9cb4-f3d0-4641-ac3f-f59eb47a3abd", + # "msg": "SUCCESS", + # "timestamp": 1759225965363 + # } + # + id = self.safe_string(message, 'id') + subscriptionsById = self.index_by(client.subscriptions, 'id') + subscription = self.safe_dict(subscriptionsById, id, {}) + isUnSubMessage = self.safe_bool(subscription, 'unsubscribe', False) + if isUnSubMessage: + self.handle_un_subscription(client, subscription) + return message + + def handle_un_subscription(self, client: Client, subscription: dict): + messageHashes = self.safe_list(subscription, 'messageHashes', []) + subMessageHashes = self.safe_list(subscription, 'subMessageHashes', []) + for i in range(0, len(messageHashes)): + unsubHash = messageHashes[i] + subHash = subMessageHashes[i] + self.clean_unsubscription(client, subHash, unsubHash) + self.clean_cache(subscription) diff --git a/ccxt/pro/bitfinex.py b/ccxt/pro/bitfinex.py new file mode 100644 index 0000000..93ac461 --- /dev/null +++ b/ccxt/pro/bitfinex.py @@ -0,0 +1,1218 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolById, ArrayCacheByTimestamp +import hashlib +from ccxt.base.types import Any, Balances, Int, Order, OrderBook, Str, Ticker, Trade +from ccxt.async_support.base.ws.client import Client +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import ChecksumError +from ccxt.base.precise import Precise + + +class bitfinex(ccxt.async_support.bitfinex): + + def describe(self) -> Any: + return self.deep_extend(super(bitfinex, self).describe(), { + 'has': { + 'ws': True, + 'watchTicker': True, + 'watchTickers': False, + 'watchOrderBook': True, + 'watchTrades': True, + 'watchTradesForSymbols': False, + 'watchMyTrades': True, + 'watchBalance': True, + 'watchOHLCV': True, + 'watchOrders': True, + 'unWatchTicker': True, + 'unWatchTrades': True, + 'unWatchOHLCV': True, + 'unWatchOrderBook': True, + }, + 'urls': { + 'api': { + 'ws': { + 'public': 'wss://api-pub.bitfinex.com/ws/2', + 'private': 'wss://api.bitfinex.com/ws/2', + }, + }, + }, + 'options': { + 'watchOrderBook': { + 'prec': 'P0', + 'freq': 'F0', + 'checksum': True, + }, + 'ordersLimit': 1000, + }, + }) + + async def subscribe(self, channel, symbol, params={}): + await self.load_markets() + market = self.market(symbol) + marketId = market['id'] + url = self.urls['api']['ws']['public'] + client = self.client(url) + messageHash = channel + ':' + marketId + request: dict = { + 'event': 'subscribe', + 'channel': channel, + 'symbol': marketId, + } + result = await self.watch(url, messageHash, self.deep_extend(request, params), messageHash, {'checksum': False}) + checksum = self.safe_bool(self.options, 'checksum', True) + if checksum and (channel == 'book'): + sub = client.subscriptions[messageHash] + if sub and not sub['checksum']: + client.subscriptions[messageHash]['checksum'] = True + await client.send({ + 'event': 'conf', + 'flags': 131072, + }) + return result + + async def un_subscribe(self, channel, topic, symbol, params={}): + await self.load_markets() + market = self.market(symbol) + marketId = market['id'] + url = self.urls['api']['ws']['public'] + client = self.client(url) + subMessageHash = channel + ':' + marketId + messageHash = 'unsubscribe:' + channel + ':' + marketId + unSubTopic = 'unsubscribe' + ':' + topic + ':' + symbol + channelId = self.safe_string(client.subscriptions, unSubTopic) + request: dict = { + 'event': 'unsubscribe', + 'chanId': channelId, + } + unSubChanMsg = 'unsubscribe:' + channelId + client.subscriptions[unSubChanMsg] = subMessageHash + subscription = { + 'messageHashes': [messageHash], + 'subMessageHashes': [subMessageHash], + 'topic': topic, + 'unsubscribe': True, + 'symbols': [symbol], + } + return await self.watch(url, messageHash, self.deep_extend(request, params), messageHash, subscription) + + async def subscribe_private(self, messageHash): + await self.load_markets() + await self.authenticate() + url = self.urls['api']['ws']['private'] + return await self.watch(url, messageHash, None, 1) + + async def watch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + watches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + interval = self.safe_string(self.timeframes, timeframe, timeframe) + channel = 'candles' + key = 'trade:' + interval + ':' + market['id'] + messageHash = channel + ':' + interval + ':' + market['id'] + request: dict = { + 'event': 'subscribe', + 'channel': channel, + 'key': key, + } + url = self.urls['api']['ws']['public'] + # not using subscribe here because self message has a different format + ohlcv = await self.watch(url, messageHash, self.deep_extend(request, params), messageHash) + if self.newUpdates: + limit = ohlcv.getLimit(symbol, limit) + return self.filter_by_since_limit(ohlcv, since, limit, 0, True) + + async def un_watch_ohlcv(self, symbol: str, timeframe: str = '1m', params={}): + """ + unWatches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns bool: True if successfully unsubscribed, False otherwise + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + interval = self.safe_string(self.timeframes, timeframe, timeframe) + channel = 'candles' + subMessageHash = channel + ':' + interval + ':' + market['id'] + messageHash = 'unsubscribe:' + subMessageHash + url = self.urls['api']['ws']['public'] + client = self.client(url) + subId = 'unsubscribe:trade:' + interval + ':' + market['id'] # trade here because we use the key + channelId = self.safe_string(client.subscriptions, subId) + request: dict = { + 'event': 'unsubscribe', + 'chanId': channelId, + } + unSubChanMsg = 'unsubscribe:' + channelId + client.subscriptions[unSubChanMsg] = subMessageHash + subscription = { + 'messageHashes': [messageHash], + 'subMessageHashes': [subMessageHash], + 'topic': 'ohlcv', + 'unsubscribe': True, + 'symbols': [symbol], + } + return await self.watch(url, messageHash, self.deep_extend(request, params), messageHash, subscription) + + def handle_ohlcv(self, client: Client, message, subscription): + # + # initial snapshot + # [ + # 341527, # channel id + # [ + # [ + # 1654705860000, # timestamp + # 1802.6, # open + # 1800.3, # close + # 1802.8, # high + # 1800.3, # low + # 86.49588236 # volume + # ], + # [ + # 1654705800000, + # 1803.6, + # 1802.6, + # 1804.9, + # 1802.3, + # 74.6348086 + # ], + # [ + # 1654705740000, + # 1802.5, + # 1803.2, + # 1804.4, + # 1802.4, + # 23.61801085 + # ] + # ] + # ] + # + # update + # [ + # 211171, + # [ + # 1654705680000, + # 1801, + # 1802.4, + # 1802.9, + # 1800.4, + # 23.91911091 + # ] + # ] + # + data = self.safe_value(message, 1, []) + ohlcvs = None + first = self.safe_value(data, 0) + if isinstance(first, list): + # snapshot + ohlcvs = data + else: + # update + ohlcvs = [data] + channel = self.safe_value(subscription, 'channel') + key = self.safe_string(subscription, 'key') + keyParts = key.split(':') + interval = self.safe_string(keyParts, 1) + marketId = key + marketId = marketId.replace('trade:', '') + marketId = marketId.replace(interval + ':', '') + market = self.safe_market(marketId) + timeframe = self.find_timeframe(interval) + symbol = market['symbol'] + messageHash = channel + ':' + interval + ':' + marketId + self.ohlcvs[symbol] = self.safe_value(self.ohlcvs, symbol, {}) + stored = self.safe_value(self.ohlcvs[symbol], timeframe) + if stored is None: + limit = self.safe_integer(self.options, 'OHLCVLimit', 1000) + stored = ArrayCacheByTimestamp(limit) + self.ohlcvs[symbol][timeframe] = stored + ohlcvsLength = len(ohlcvs) + for i in range(0, ohlcvsLength): + ohlcv = ohlcvs[ohlcvsLength - i - 1] + parsed = self.parse_ohlcv(ohlcv, market) + stored.append(parsed) + client.resolve(stored, messageHash) + + async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + trades = await self.subscribe('trades', symbol, params) + if self.newUpdates: + limit = trades.getLimit(symbol, limit) + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + async def un_watch_trades(self, symbol: str, params={}): + """ + unWatches the list of most recent trades for a particular symbol + :param str symbol: unified symbol of the market to fetch trades for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + return await self.un_subscribe('trades', 'trades', symbol, params) + + async def watch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + watches information on multiple trades made by the user + :param str symbol: unified market symbol of the market trades were made in + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trade structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + messageHash = 'myTrade' + if symbol is not None: + market = self.market(symbol) + messageHash += ':' + market['id'] + trades = await self.subscribe_private(messageHash) + if self.newUpdates: + limit = trades.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(trades, symbol, since, limit, True) + + async def watch_ticker(self, symbol: str, params={}) -> Ticker: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + return await self.subscribe('ticker', symbol, params) + + async def un_watch_ticker(self, symbol: str, params={}): + """ + unWatches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + return await self.un_subscribe('ticker', 'ticker', symbol, params) + + def handle_my_trade(self, client: Client, message, subscription={}): + # + # trade execution + # [ + # 0, + # "te", # or tu + # [ + # 1133411090, + # "tLTCUST", + # 1655110144598, + # 97084883506, + # 0.1, + # 42.821, + # "EXCHANGE MARKET", + # 42.799, + # -1, + # null, + # null, + # 1655110144596 + # ] + # ] + # + name = 'myTrade' + data = self.safe_value(message, 2) + trade = self.parse_ws_trade(data) + symbol = trade['symbol'] + market = self.market(symbol) + messageHash = name + ':' + market['id'] + if self.myTrades is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + self.myTrades = ArrayCacheBySymbolById(limit) + tradesArray = self.myTrades + tradesArray.append(trade) + self.myTrades = tradesArray + # generic subscription + client.resolve(tradesArray, name) + # specific subscription + client.resolve(tradesArray, messageHash) + + def handle_trades(self, client: Client, message, subscription): + # + # initial snapshot + # + # [ + # 188687, # channel id + # [ + # [1128060675, 1654701572690, 0.00217533, 1815.3], # id, mts, amount, price + # [1128060665, 1654701551231, -0.00280472, 1814.1], + # [1128060664, 1654701550996, -0.00364444, 1814.1], + # [1128060656, 1654701527730, -0.00265203, 1814.2], + # [1128060647, 1654701505193, 0.00262395, 1815.2], + # [1128060642, 1654701484656, -0.13411443, 1816], + # [1128060641, 1654701484656, -0.00088557, 1816], + # [1128060639, 1654701478326, -0.002, 1816], + # ] + # ] + # update + # + # [ + # 360141, + # "te", + # [ + # 1128060969, # id + # 1654702500098, # mts + # 0.00325131, # amount positive buy, negative sell + # 1818.5, # price + # ], + # ] + # + # + channel = self.safe_value(subscription, 'channel') + marketId = self.safe_string(subscription, 'symbol') + market = self.safe_market(marketId) + messageHash = channel + ':' + marketId + tradesLimit = self.safe_integer(self.options, 'tradesLimit', 1000) + symbol = market['symbol'] + stored = self.safe_value(self.trades, symbol) + if stored is None: + stored = ArrayCache(tradesLimit) + self.trades[symbol] = stored + messageLength = len(message) + if messageLength == 2: + # initial snapshot + trades = self.safe_list(message, 1, []) + # needs to be reversed to make chronological order + length = len(trades) + for i in range(0, length): + index = length - i - 1 + parsed = self.parse_ws_trade(trades[index], market) + stored.append(parsed) + else: + # update + type = self.safe_string(message, 1) + if type == 'tu': + # don't resolve for a duplicate update + # since te and tu updates are duplicated on the public stream + return + trade = self.safe_value(message, 2, []) + parsed = self.parse_ws_trade(trade, market) + stored.append(parsed) + client.resolve(stored, messageHash) + + def parse_ws_trade(self, trade, market=None): + # + # [ + # 1128060969, # id + # 1654702500098, # mts + # 0.00325131, # amount positive buy, negative sell + # 1818.5, # price + # ] + # + # trade execution + # + # [ + # 1133411090, # id + # "tLTCUST", # symbol + # 1655110144598, # create ms + # 97084883506, # order id + # 0.1, # amount + # 42.821, # price + # "EXCHANGE MARKET", # order type + # 42.799, # order price + # -1, # maker + # null, # fee + # null, # fee currency + # 1655110144596 # cid + # ] + # + # trade update + # + # [ + # 1133411090, + # "tLTCUST", + # 1655110144598, + # 97084883506, + # 0.1, + # 42.821, + # "EXCHANGE MARKET", + # 42.799, + # -1, + # -0.0002, + # "LTC", + # 1655110144596 + # ] + # + numFields = len(trade) + isPublic = numFields <= 8 + marketId = self.safe_string(trade, 1) if (not isPublic) else None + market = self.safe_market(marketId, market) + createdKey = 1 if isPublic else 2 + priceKey = 3 if isPublic else 5 + amountKey = 2 if isPublic else 4 + marketId = market['id'] + type = self.safe_string(trade, 6) + if type is not None: + if type.find('LIMIT') > -1: + type = 'limit' + elif type.find('MARKET') > -1: + type = 'market' + orderId = self.safe_string(trade, 3) if (not isPublic) else None + id = self.safe_string(trade, 0) + timestamp = self.safe_integer(trade, createdKey) + price = self.safe_string(trade, priceKey) + amountString = self.safe_string(trade, amountKey) + amount = self.parse_number(Precise.string_abs(amountString)) + side = None + if amount is not None: + side = 'buy' if Precise.string_gt(amountString, '0') else 'sell' + symbol = self.safe_symbol(marketId, market) + feeValue = self.safe_string(trade, 9) + fee = None + if feeValue is not None: + currencyId = self.safe_string(trade, 10) + code = self.safe_currency_code(currencyId) + fee = { + 'cost': feeValue, + 'currency': code, + } + maker = self.safe_integer(trade, 8) + takerOrMaker = None + if maker is not None: + takerOrMaker = 'taker' if (maker == -1) else 'maker' + return self.safe_trade({ + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'id': id, + 'order': orderId, + 'type': type, + 'takerOrMaker': takerOrMaker, + 'side': side, + 'price': price, + 'amount': amount, + 'cost': None, + 'fee': fee, + }, market) + + def handle_ticker(self, client: Client, message, subscription): + # + # [ + # 340432, # channel ID + # [ + # 236.62, # 1 BID float Price of last highest bid + # 9.0029, # 2 BID_SIZE float Size of the last highest bid + # 236.88, # 3 ASK float Price of last lowest ask + # 7.1138, # 4 ASK_SIZE float Size of the last lowest ask + # -1.02, # 5 DAILY_CHANGE float Amount that the last price has changed since yesterday + # 0, # 6 DAILY_CHANGE_PERC float Amount that the price has changed expressed in percentage terms + # 236.52, # 7 LAST_PRICE float Price of the last trade. + # 5191.36754297, # 8 VOLUME float Daily volume + # 250.01, # 9 HIGH float Daily high + # 220.05, # 10 LOW float Daily low + # ] + # ] + # + ticker = self.safe_value(message, 1) + marketId = self.safe_string(subscription, 'symbol') + market = self.safe_market(marketId) + symbol = self.safe_symbol(marketId) + parsed = self.parse_ws_ticker(ticker, market) + channel = 'ticker' + messageHash = channel + ':' + marketId + self.tickers[symbol] = parsed + client.resolve(parsed, messageHash) + + def parse_ws_ticker(self, ticker, market=None): + # + # [ + # 236.62, # 1 BID float Price of last highest bid + # 9.0029, # 2 BID_SIZE float Size of the last highest bid + # 236.88, # 3 ASK float Price of last lowest ask + # 7.1138, # 4 ASK_SIZE float Size of the last lowest ask + # -1.02, # 5 DAILY_CHANGE float Amount that the last price has changed since yesterday + # 0, # 6 DAILY_CHANGE_PERC float Amount that the price has changed expressed in percentage terms + # 236.52, # 7 LAST_PRICE float Price of the last trade. + # 5191.36754297, # 8 VOLUME float Daily volume + # 250.01, # 9 HIGH float Daily high + # 220.05, # 10 LOW float Daily low + # ] + # + market = self.safe_market(None, market) + symbol = market['symbol'] + last = self.safe_string(ticker, 6) + change = self.safe_string(ticker, 4) + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': None, + 'datetime': None, + 'high': self.safe_string(ticker, 8), + 'low': self.safe_string(ticker, 9), + 'bid': self.safe_string(ticker, 0), + 'bidVolume': self.safe_string(ticker, 1), + 'ask': self.safe_string(ticker, 2), + 'askVolume': self.safe_string(ticker, 3), + 'vwap': None, + 'open': None, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': change, + 'percentage': self.safe_string(ticker, 5), + 'average': None, + 'baseVolume': self.safe_string(ticker, 7), + 'quoteVolume': None, + 'info': ticker, + }, market) + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + if limit is not None: + if (limit != 25) and (limit != 100): + raise ExchangeError(self.id + ' watchOrderBook limit argument must be None, 25 or 100') + options = self.safe_value(self.options, 'watchOrderBook', {}) + prec = self.safe_string(options, 'prec', 'P0') + freq = self.safe_string(options, 'freq', 'F0') + request: dict = { + 'prec': prec, # string, level of price aggregation, 'P0', 'P1', 'P2', 'P3', 'P4', default P0 + 'freq': freq, # string, frequency of updates 'F0' = realtime, 'F1' = 2 seconds, default is 'F0' + } + if limit is not None: + request['len'] = limit # string, number of price points, '25', '100', default = '25' + orderbook = await self.subscribe('book', symbol, self.deep_extend(request, params)) + return orderbook.limit() + + def handle_order_book(self, client: Client, message, subscription): + # + # first message(snapshot) + # + # [ + # 18691, # channel id + # [ + # [7364.8, 10, 4.354802], # price, count, size > 0 = bid + # [7364.7, 1, 0.00288831], + # [7364.3, 12, 0.048], + # [7364.9, 3, -0.42028976], # price, count, size < 0 = ask + # [7365, 1, -0.25], + # [7365.5, 1, -0.00371937], + # ] + # ] + # + # subsequent updates + # + # [ + # 358169, # channel id + # [ + # 1807.1, # price + # 0, # cound + # 1 # size + # ] + # ] + # + marketId = self.safe_string(subscription, 'symbol') + symbol = self.safe_symbol(marketId) + channel = 'book' + messageHash = channel + ':' + marketId + prec = self.safe_string(subscription, 'prec', 'P0') + isRaw = (prec == 'R0') + # if it is an initial snapshot + if not (symbol in self.orderbooks): + limit = self.safe_integer(subscription, 'len') + if isRaw: + # raw order books + self.orderbooks[symbol] = self.indexed_order_book({}, limit) + else: + # P0, P1, P2, P3, P4 + self.orderbooks[symbol] = self.counted_order_book({}, limit) + orderbook = self.orderbooks[symbol] + if isRaw: + deltas = message[1] + for i in range(0, len(deltas)): + delta = deltas[i] + delta2 = delta[2] + size = -delta2 if (delta2 < 0) else delta2 + side = 'asks' if (delta2 < 0) else 'bids' + bookside = orderbook[side] + idString = self.safe_string(delta, 0) + price = self.safe_float(delta, 1) + bookside.storeArray([price, size, idString]) + else: + deltas = message[1] + for i in range(0, len(deltas)): + delta = deltas[i] + amount = self.safe_number(delta, 2) + counter = self.safe_number(delta, 1) + price = self.safe_number(delta, 0) + size = -amount if (amount < 0) else amount + side = 'asks' if (amount < 0) else 'bids' + bookside = orderbook[side] + bookside.storeArray([price, size, counter]) + orderbook['symbol'] = symbol + client.resolve(orderbook, messageHash) + else: + orderbook = self.orderbooks[symbol] + deltas = message[1] + orderbookItem = self.orderbooks[symbol] + if isRaw: + price = self.safe_string(deltas, 1) + deltas2 = deltas[2] + size = -deltas2 if (deltas2 < 0) else deltas2 + side = 'asks' if (deltas2 < 0) else 'bids' + bookside = orderbookItem[side] + # price = 0 means that you have to remove the order from your book + amount = size if Precise.string_gt(price, '0') else '0' + idString = self.safe_string(deltas, 0) + bookside.storeArray([self.parse_number(price), self.parse_number(amount), idString]) + else: + amount = self.safe_string(deltas, 2) + counter = self.safe_string(deltas, 1) + price = self.safe_string(deltas, 0) + size = Precise.string_neg(amount) if Precise.string_lt(amount, '0') else amount + side = 'asks' if Precise.string_lt(amount, '0') else 'bids' + bookside = orderbookItem[side] + bookside.storeArray([self.parse_number(price), self.parse_number(size), self.parse_number(counter)]) + client.resolve(orderbook, messageHash) + + def handle_checksum(self, client: Client, message, subscription): + # + # [173904, "cs", -890884919] + # + marketId = self.safe_string(subscription, 'symbol') + symbol = self.safe_symbol(marketId) + channel = 'book' + messageHash = channel + ':' + marketId + book = self.safe_value(self.orderbooks, symbol) + if book is None: + return + depth = 25 # covers the first 25 bids and asks + stringArray = [] + bids = book['bids'] + asks = book['asks'] + prec = self.safe_string(subscription, 'prec', 'P0') + isRaw = (prec == 'R0') + idToCheck = 2 if isRaw else 0 + # pepperoni pizza from bitfinex + for i in range(0, depth): + bid = self.safe_value(bids, i) + ask = self.safe_value(asks, i) + if bid is not None: + stringArray.append(self.number_to_string(bids[i][idToCheck])) + stringArray.append(self.number_to_string(bids[i][1])) + if ask is not None: + stringArray.append(self.number_to_string(asks[i][idToCheck])) + aski1 = asks[i][1] + stringArray.append(self.number_to_string(-aski1)) + payload = ':'.join(stringArray) + localChecksum = self.crc32(payload, True) + responseChecksum = self.safe_integer(message, 2) + if responseChecksum != localChecksum: + del client.subscriptions[messageHash] + del self.orderbooks[symbol] + checksum = self.handle_option('watchOrderBook', 'checksum', True) + if checksum: + error = ChecksumError(self.id + ' ' + self.orderbook_checksum_message(symbol)) + client.reject(error, messageHash) + + async def watch_balance(self, params={}) -> Balances: + """ + watch balance and get the amount of funds available for trading or funds locked in orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: spot or contract if not provided self.options['defaultType'] is used + :returns dict: a `balance structure ` + """ + await self.load_markets() + balanceType = self.safe_string(params, 'wallet', 'exchange') # exchange, margin + params = self.omit(params, 'wallet') + messageHash = 'balance:' + balanceType + return await self.subscribe_private(messageHash) + + def handle_balance(self, client: Client, message, subscription): + # + # snapshot(exchange + margin together) + # [ + # 0, + # "ws", + # [ + # [ + # "exchange", + # "LTC", + # 0.05479727, + # 0, + # null, + # "Trading fees for 0.05 LTC(LTCUST) @ 51.872 on BFX(0.2%)", + # null, + # ] + # [ + # "margin", + # "USTF0", + # 11.960650700086292, + # 0, + # null, + # "Trading fees for 0.1 LTCF0(LTCF0:USTF0) @ 51.844 on BFX(0.065%)", + # null, + # ], + # ], + # ] + # + # spot + # [ + # 0, + # "wu", + # [ + # "exchange", + # "LTC", # currency + # 0.06729727, # wallet balance + # 0, # unsettled balance + # 0.06729727, # available balance might be null + # "Exchange 0.4 LTC for UST @ 65.075", + # { + # "reason": "TRADE", + # "order_id": 96596397973, + # "order_id_oppo": 96596632735, + # "trade_price": "65.075", + # "trade_amount": "-0.4", + # "order_cid": 1654636218766, + # "order_gid": null + # } + # ] + # ] + # + # margin + # + # [ + # "margin", + # "USTF0", + # 11.960650700086292, # total + # 0, + # 6.776250700086292, # available + # "Trading fees for 0.1 LTCF0(LTCF0:USTF0) @ 51.844 on BFX(0.065%)", + # null + # ] + # + updateType = self.safe_value(message, 1) + data = None + if updateType == 'ws': + data = self.safe_value(message, 2) + else: + data = [self.safe_value(message, 2)] + updatedTypes: dict = {} + for i in range(0, len(data)): + rawBalance = data[i] + currencyId = self.safe_string(rawBalance, 1) + code = self.safe_currency_code(currencyId) + balance = self.parse_ws_balance(rawBalance) + balanceType = self.safe_string(rawBalance, 0) + oldBalance = self.safe_value(self.balance, balanceType, {}) + oldBalance[code] = balance + oldBalance['info'] = message + self.balance[balanceType] = self.safe_balance(oldBalance) + updatedTypes[balanceType] = True + updatesKeys = list(updatedTypes.keys()) + for i in range(0, len(updatesKeys)): + type = updatesKeys[i] + messageHash = 'balance:' + type + client.resolve(self.balance[type], messageHash) + + def parse_ws_balance(self, balance): + # + # [ + # "exchange", + # "LTC", + # 0.05479727, # balance + # 0, + # null, # available null if not calculated yet + # "Trading fees for 0.05 LTC(LTCUST) @ 51.872 on BFX(0.2%)", + # null, + # ] + # + totalBalance = self.safe_string(balance, 2) + availableBalance = self.safe_string(balance, 4) + account = self.account() + if availableBalance is not None: + account['free'] = availableBalance + account['total'] = totalBalance + return account + + def handle_system_status(self, client: Client, message): + # + # { + # "event": "info", + # "version": 2, + # "serverId": "e293377e-7bb7-427e-b28c-5db045b2c1d1", + # "platform": {status: 1}, # 1 for operative, 0 for maintenance + # } + # + return message + + def handle_unsubscription_status(self, client: Client, message): + # + # { + # "event": "unsubscribed", + # "status": "OK", + # "chanId": CHANNEL_ID + # } + # + channelId = self.safe_string(message, 'chanId') + unSubChannel = 'unsubscribe:' + channelId + subMessageHash = self.safe_string(client.subscriptions, unSubChannel) + subscription = self.safe_dict(client.subscriptions, 'unsubscribe:' + subMessageHash) + del client.subscriptions[unSubChannel] + messageHashes = self.safe_list(subscription, 'messageHashes', []) + subMessageHashes = self.safe_list(subscription, 'subMessageHashes', []) + for i in range(0, len(messageHashes)): + messageHash = messageHashes[i] + subHash = subMessageHashes[i] + self.clean_unsubscription(client, subHash, messageHash) + self.clean_cache(subscription) + return True + + def handle_subscription_status(self, client: Client, message): + # + # { + # "event": "subscribed", + # "channel": "book", + # "chanId": 67473, + # "symbol": "tBTCUSD", + # "prec": "P0", + # "freq": "F0", + # "len": "25", + # "pair": "BTCUSD" + # } + # + # { + # event: 'subscribed', + # channel: 'candles', + # chanId: 128306, + # key: 'trade:1m:tBTCUST' + # } + # + channelId = self.safe_string(message, 'chanId') + client.subscriptions[channelId] = message + # store the opposite direction too for unWatch + mappings: dict = { + 'book': 'orderbook', + 'candles': 'ohlcv', + 'ticker': 'ticker', + 'trades': 'trades', + } + unifiedChannel = self.safe_string(mappings, self.safe_string(message, 'channel')) + if 'key' in message: + # handle ohlcv differently because the message is different + key = self.safe_string(message, 'key') + subKeyId = 'unsubscribe:' + key + client.subscriptions[subKeyId] = channelId + else: + marketId = self.safe_string(message, 'symbol') + symbol = self.safe_symbol(marketId) + if unifiedChannel is not None: + subId = 'unsubscribe:' + unifiedChannel + ':' + symbol + client.subscriptions[subId] = channelId + return message + + async def authenticate(self, params={}): + url = self.urls['api']['ws']['private'] + client = self.client(url) + messageHash = 'authenticated' + future = client.reusableFuture(messageHash) + authenticated = self.safe_value(client.subscriptions, messageHash) + if authenticated is None: + nonce = self.milliseconds() + payload = 'AUTH' + str(nonce) + signature = self.hmac(self.encode(payload), self.encode(self.secret), hashlib.sha384, 'hex') + event = 'auth' + request: dict = { + 'apiKey': self.apiKey, + 'authSig': signature, + 'authNonce': nonce, + 'authPayload': payload, + 'event': event, + } + message = self.extend(request, params) + self.watch(url, messageHash, message, messageHash) + return await future + + def handle_authentication_message(self, client: Client, message): + messageHash = 'authenticated' + status = self.safe_string(message, 'status') + if status == 'OK': + # we resolve the future here permanently so authentication only happens once + future = self.safe_value(client.futures, messageHash) + future.resolve(True) + else: + error = AuthenticationError(self.json(message)) + client.reject(error, messageHash) + # allows further authentication attempts + if messageHash in client.subscriptions: + del client.subscriptions[messageHash] + + async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + watches information on multiple orders made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + messageHash = 'orders' + if symbol is not None: + market = self.market(symbol) + messageHash += ':' + market['id'] + orders = await self.subscribe_private(messageHash) + if self.newUpdates: + limit = orders.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(orders, symbol, since, limit, True) + + def handle_orders(self, client: Client, message, subscription): + # + # limit order + # [ + # 0, + # "on", # ou or oc + # [ + # 96923856256, # order id + # null, # gid + # 1655029337026, # cid + # "tLTCUST", # symbol + # 1655029337027, # created timestamp + # 1655029337029, # updated timestamp + # 0.1, # amount + # 0.1, # amount_orig + # "EXCHANGE LIMIT", # order type + # null, # type_prev + # null, # mts_tif + # null, # placeholder + # 0, # flags + # "ACTIVE", # status + # null, + # null, + # 30, # price + # 0, # price average + # 0, # price_trailling + # 0, # price_aux_limit + # null, + # null, + # null, + # 0, # notify + # 0, + # null, + # null, + # null, + # "BFX", + # null, + # null, + # ] + # ] + # + data = self.safe_value(message, 2, []) + messageType = self.safe_string(message, 1) + if self.orders is None: + limit = self.safe_integer(self.options, 'ordersLimit', 1000) + self.orders = ArrayCacheBySymbolById(limit) + orders = self.orders + symbolIds: dict = {} + if messageType == 'os': + snapshotLength = len(data) + if snapshotLength == 0: + return + for i in range(0, len(data)): + value = data[i] + parsed = self.parse_ws_order(value) + symbol = parsed['symbol'] + symbolIds[symbol] = True + orders.append(parsed) + else: + parsed = self.parse_ws_order(data) + orders.append(parsed) + symbol = parsed['symbol'] + symbolIds[symbol] = True + name = 'orders' + client.resolve(self.orders, name) + keys = list(symbolIds.keys()) + for i in range(0, len(keys)): + symbol = keys[i] + market = self.market(symbol) + messageHash = name + ':' + market['id'] + client.resolve(self.orders, messageHash) + + def parse_ws_order_status(self, status): + statuses: dict = { + 'ACTIVE': 'open', + 'CANCELED': 'canceled', + 'EXECUTED': 'closed', + 'PARTIALLY': 'open', + } + return self.safe_string(statuses, status, status) + + def parse_ws_order(self, order, market=None): + # + # [ + # 97084883506, # order id + # null, + # 1655110144596, # clientOrderId + # "tLTCUST", # symbol + # 1655110144596, # created timestamp + # 1655110144598, # updated timestamp + # 0, # amount + # 0.1, # amount_orig negative if sell order + # "EXCHANGE MARKET", # type + # null, + # null, + # null, + # 0, + # "EXECUTED @ 42.821(0.1)", # status + # null, + # null, + # 42.799, # price + # 42.821, # price average + # 0, # price trailling + # 0, # price_aux_limit + # null, + # null, + # null, + # 0, + # 0, + # null, + # null, + # null, + # "BFX", + # null, + # null, + # {} + # ] + # + id = self.safe_string(order, 0) + clientOrderId = self.safe_string(order, 1) + marketId = self.safe_string(order, 3) + symbol = self.safe_symbol(marketId) + market = self.safe_market(symbol) + amount = self.safe_string(order, 7) + side = 'buy' + if Precise.string_lt(amount, '0'): + amount = Precise.string_abs(amount) + side = 'sell' + remaining = Precise.string_abs(self.safe_string(order, 6)) + type = self.safe_string(order, 8) + if type.find('LIMIT') > -1: + type = 'limit' + elif type.find('MARKET') > -1: + type = 'market' + rawState = self.safe_string(order, 13) + stateParts = rawState.split(' ') + trimmedStatus = self.safe_string(stateParts, 0) + status = self.parse_ws_order_status(trimmedStatus) + price = self.safe_string(order, 16) + timestamp = self.safe_integer_2(order, 5, 4) + average = self.safe_string(order, 17) + stopPrice = self.omit_zero(self.safe_string(order, 18)) + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'symbol': symbol, + 'type': type, + 'side': side, + 'price': price, + 'stopPrice': stopPrice, + 'triggerPrice': stopPrice, + 'average': average, + 'amount': amount, + 'remaining': remaining, + 'filled': None, + 'status': status, + 'fee': None, + 'cost': None, + 'trades': None, + }, market) + + def handle_message(self, client: Client, message): + channelId = self.safe_string(message, 0) + # + # [ + # 1231, + # "hb", + # ] + # + # auth message + # { + # "event": "auth", + # "status": "OK", + # "chanId": 0, + # "userId": 3159883, + # "auth_id": "ac7108e7-2f26-424d-9982-c24700dc02ca", + # "caps": { + # "orders": {read: 1, write: 1}, + # "account": {read: 1, write: 1}, + # "funding": {read: 1, write: 1}, + # "history": {read: 1, write: 0}, + # "wallets": {read: 1, write: 1}, + # "withdraw": {read: 0, write: 1}, + # "positions": {read: 1, write: 1}, + # "ui_withdraw": {read: 0, write: 0} + # } + # } + # + if isinstance(message, list): + if message[1] == 'hb': + return # skip heartbeats within subscription channels for now + subscription = self.safe_value(client.subscriptions, channelId, {}) + channel = self.safe_string(subscription, 'channel') + name = self.safe_string(message, 1) + publicMethods: dict = { + 'book': self.handle_order_book, + 'cs': self.handle_checksum, + 'candles': self.handle_ohlcv, + 'ticker': self.handle_ticker, + 'trades': self.handle_trades, + } + privateMethods: dict = { + 'os': self.handle_orders, + 'ou': self.handle_orders, + 'on': self.handle_orders, + 'oc': self.handle_orders, + 'wu': self.handle_balance, + 'ws': self.handle_balance, + 'tu': self.handle_my_trade, + } + method = None + if channelId == '0': + method = self.safe_value(privateMethods, name) + else: + method = self.safe_value_2(publicMethods, name, channel) + if method is not None: + method(client, message, subscription) + else: + event = self.safe_string(message, 'event') + if event is not None: + methods: dict = { + 'info': self.handle_system_status, + 'subscribed': self.handle_subscription_status, + 'unsubscribed': self.handle_unsubscription_status, + 'auth': self.handle_authentication_message, + } + method = self.safe_value(methods, event) + if method is not None: + method(client, message) diff --git a/ccxt/pro/bitget.py b/ccxt/pro/bitget.py new file mode 100644 index 0000000..07840bd --- /dev/null +++ b/ccxt/pro/bitget.py @@ -0,0 +1,2712 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolById, ArrayCacheBySymbolBySide, ArrayCacheByTimestamp +import hashlib +from ccxt.base.types import Any, Balances, Bool, Int, Order, OrderBook, Position, Str, Strings, Ticker, Tickers, Trade +from ccxt.async_support.base.ws.client import Client +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import ChecksumError +from ccxt.base.errors import UnsubscribeError +from ccxt.base.precise import Precise + + +class bitget(ccxt.async_support.bitget): + + def describe(self) -> Any: + return self.deep_extend(super(bitget, self).describe(), { + 'has': { + 'ws': True, + 'createOrderWs': False, + 'editOrderWs': False, + 'fetchOpenOrdersWs': False, + 'fetchOrderWs': False, + 'cancelOrderWs': False, + 'cancelOrdersWs': False, + 'cancelAllOrdersWs': False, + 'watchBalance': True, + 'watchMyTrades': True, + 'watchOHLCV': True, + 'watchOHLCVForSymbols': False, + 'watchOrderBook': True, + 'watchOrderBookForSymbols': True, + 'watchOrders': True, + 'watchTicker': True, + 'watchTickers': True, + 'watchBidsAsks': True, + 'watchTrades': True, + 'watchTradesForSymbols': True, + 'watchPositions': True, + }, + 'urls': { + 'api': { + 'ws': { + 'public': 'wss://ws.bitget.com/v2/ws/public', + 'private': 'wss://ws.bitget.com/v2/ws/private', + 'utaPublic': 'wss://ws.bitget.com/v3/ws/public', + 'utaPrivate': 'wss://ws.bitget.com/v3/ws/private', + }, + 'demo': { + 'public': 'wss://wspap.bitget.com/v2/ws/public', + 'private': 'wss://wspap.bitget.com/v2/ws/private', + 'utaPublic': 'wss://wspap.bitget.com/v3/ws/public', + 'utaPrivate': 'wss://wspap.bitget.com/v3/ws/private', + }, + }, + }, + 'options': { + 'tradesLimit': 1000, + 'OHLCVLimit': 1000, + # WS timeframes differ from REST timeframes + 'timeframes': { + '1m': '1m', + '3m': '3m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1H', + '4h': '4H', + '6h': '6H', + '12h': '12H', + '1d': '1D', + '1w': '1W', + }, + 'watchOrderBook': { + 'checksum': True, + }, + 'watchTrades': { + 'ignoreDuplicates': True, + }, + }, + 'streaming': { + 'ping': self.ping, + }, + 'exceptions': { + 'ws': { + 'exact': { + '30001': BadRequest, # {"event":"error","code":30001,"msg":"instType:sp,channel:candleNone,instId:BTCUSDT doesn't exist"} + '30002': AuthenticationError, # illegal request + '30003': BadRequest, # invalid op + '30004': AuthenticationError, # requires login + '30005': AuthenticationError, # login failed + '30006': RateLimitExceeded, # too many requests + '30007': RateLimitExceeded, # request over limit,connection close + '30011': AuthenticationError, # invalid ACCESS_KEY + '30012': AuthenticationError, # invalid ACCESS_PASSPHRASE + '30013': AuthenticationError, # invalid ACCESS_TIMESTAMP + '30014': BadRequest, # Request timestamp expired + '30015': AuthenticationError, # {event: 'error', code: 30015, msg: 'Invalid sign'} + '30016': BadRequest, # {event: 'error', code: 30016, msg: 'Param error'} + }, + 'broad': {}, + }, + }, + }) + + def get_inst_type(self, market, uta: bool = False, params={}): + if (uta is None) or not uta: + uta, params = self.handle_option_and_params(params, 'getInstType', 'uta', False) + instType = None + if market is None: + instType, params = self.handleProductTypeAndParams(None, params) + elif (market['swap']) or (market['future']): + instType, params = self.handleProductTypeAndParams(market, params) + else: + instType = 'SPOT' + instypeAux = None + instypeAux, params = self.handle_option_and_params(params, 'getInstType', 'instType', instType) + instType = instypeAux + if uta: + instType = instType.lower() + return [instType, params] + + async def watch_ticker(self, symbol: str, params={}) -> Ticker: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://www.bitget.com/api-doc/spot/websocket/public/Tickers-Channel + https://www.bitget.com/api-doc/contract/websocket/public/Tickers-Channel + https://www.bitget.com/api-doc/uta/websocket/public/Tickers-Channel + + :param str symbol: unified symbol of the market to watch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + messageHash = 'ticker:' + symbol + instType = None + uta = None + uta, params = self.handle_option_and_params(params, 'watchTicker', 'uta', False) + instType, params = self.get_inst_type(market, uta, params) + args: dict = { + 'instType': instType, + } + topicOrChannel = 'topic' if uta else 'channel' + symbolOrInstId = 'symbol' if uta else 'instId' + args[topicOrChannel] = 'ticker' + args[symbolOrInstId] = market['id'] + return await self.watch_public(messageHash, args, params) + + async def un_watch_ticker(self, symbol: str, params={}) -> Any: + """ + unsubscribe from the ticker channel + + https://www.bitget.com/api-doc/spot/websocket/public/Tickers-Channel + https://www.bitget.com/api-doc/contract/websocket/public/Tickers-Channel + + :param str symbol: unified symbol of the market to unwatch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns any: status of the unwatch request + """ + await self.load_markets() + return await self.un_watch_channel(symbol, 'ticker', 'ticker', params) + + async def watch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list + + https://www.bitget.com/api-doc/spot/websocket/public/Tickers-Channel + https://www.bitget.com/api-doc/contract/websocket/public/Tickers-Channel + https://www.bitget.com/api-doc/uta/websocket/public/Tickers-Channel + + :param str[] symbols: unified symbol of the market to watch the tickers for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, False) + market = self.market(symbols[0]) + instType = None + uta = None + uta, params = self.handle_option_and_params(params, 'watchTickers', 'uta', False) + instType, params = self.get_inst_type(market, uta, params) + topics = [] + messageHashes = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + marketInner = self.market(symbol) + args: dict = { + 'instType': instType, + } + topicOrChannel = 'topic' if uta else 'channel' + symbolOrInstId = 'symbol' if uta else 'instId' + args[topicOrChannel] = 'ticker' + args[symbolOrInstId] = marketInner['id'] + topics.append(args) + messageHashes.append('ticker:' + symbol) + tickers = await self.watch_public_multiple(messageHashes, topics, params) + if self.newUpdates: + result: dict = {} + result[tickers['symbol']] = tickers + return result + return self.filter_by_array(self.tickers, 'symbol', symbols) + + def handle_ticker(self, client: Client, message): + # + # default + # + # { + # "action": "snapshot", + # "arg": { + # "instType": "SPOT", + # "channel": "ticker", + # "instId": "BTCUSDT" + # }, + # "data": [ + # { + # "instId": "BTCUSDT", + # "lastPr": "43528.19", + # "open24h": "42267.78", + # "high24h": "44490.00", + # "low24h": "41401.53", + # "change24h": "0.03879", + # "bidPr": "43528", + # "askPr": "43528.01", + # "bidSz": "0.0334", + # "askSz": "0.1917", + # "baseVolume": "15002.4216", + # "quoteVolume": "648006446.7164", + # "openUtc": "44071.18", + # "changeUtc24h": "-0.01232", + # "ts": "1701842994338" + # } + # ], + # "ts": 1701842994341 + # } + # + # uta + # + # { + # "action": "snapshot", + # "arg": {"instType": "spot", topic: "ticker", symbol: "BTCUSDT"}, + # "data": [ + # { + # "highPrice24h": "120255.61", + # "lowPrice24h": "116145.88", + # "openPrice24h": "118919.38", + # "lastPrice": "119818.83", + # "turnover24h": "215859996.272276", + # "volume24h": "1819.756798", + # "bid1Price": "119811.26", + # "ask1Price": "119831.18", + # "bid1Size": "0.008732", + # "ask1Size": "0.004297", + # "price24hPcnt": "0.02002" + # } + # ], + # "ts": 1753230479687 + # } + # + self.handle_bid_ask(client, message) + ticker = self.parse_ws_ticker(message) + symbol = ticker['symbol'] + self.tickers[symbol] = ticker + messageHash = 'ticker:' + symbol + client.resolve(ticker, messageHash) + + def parse_ws_ticker(self, message, market=None): + # + # spot + # + # { + # "action": "snapshot", + # "arg": { + # "instType": "SPOT", + # "channel": "ticker", + # "instId": "BTCUSDT" + # }, + # "data": [ + # { + # "instId": "BTCUSDT", + # "lastPr": "43528.19", + # "open24h": "42267.78", + # "high24h": "44490.00", + # "low24h": "41401.53", + # "change24h": "0.03879", + # "bidPr": "43528", + # "askPr": "43528.01", + # "bidSz": "0.0334", + # "askSz": "0.1917", + # "baseVolume": "15002.4216", + # "quoteVolume": "648006446.7164", + # "openUtc": "44071.18", + # "changeUtc24h": "-0.01232", + # "ts": "1701842994338" + # } + # ], + # "ts": 1701842994341 + # } + # + # contract + # + # { + # "action": "snapshot", + # "arg": { + # "instType": "USDT-FUTURES", + # "channel": "ticker", + # "instId": "BTCUSDT" + # }, + # "data": [ + # { + # "instId": "BTCUSDT", + # "lastPr": "43480.4", + # "bidPr": "43476.3", + # "askPr": "43476.8", + # "bidSz": "0.1", + # "askSz": "3.055", + # "open24h": "42252.3", + # "high24h": "44518.2", + # "low24h": "41387.0", + # "change24h": "0.03875", + # "fundingRate": "0.000096", + # "nextFundingTime": "1701849600000", + # "markPrice": "43476.4", + # "indexPrice": "43478.4", + # "holdingAmount": "50670.787", + # "baseVolume": "120187.104", + # "quoteVolume": "5167385048.693", + # "openUtc": "44071.4", + # "symbolType": "1", + # "symbol": "BTCUSDT", + # "deliveryPrice": "0", + # "ts": "1701843962811" + # } + # ], + # "ts": 1701843962812 + # } + # + # uta + # + # { + # "action": "snapshot", + # "arg": {"instType": "spot", topic: "ticker", symbol: "BTCUSDT"}, + # "data": [ + # { + # "highPrice24h": "120255.61", + # "lowPrice24h": "116145.88", + # "openPrice24h": "118919.38", + # "lastPrice": "119818.83", + # "turnover24h": "215859996.272276", + # "volume24h": "1819.756798", + # "bid1Price": "119811.26", + # "ask1Price": "119831.18", + # "bid1Size": "0.008732", + # "ask1Size": "0.004297", + # "price24hPcnt": "0.02002" + # } + # ], + # "ts": 1753230479687 + # } + # + arg = self.safe_value(message, 'arg', {}) + data = self.safe_value(message, 'data', []) + ticker = self.safe_value(data, 0, {}) + utaTimestamp = self.safe_integer(message, 'ts') + timestamp = self.safe_integer(ticker, 'ts', utaTimestamp) + instType = self.safe_string_lower(arg, 'instType') + marketType = 'spot' if (instType == 'spot') else 'contract' + utaMarketId = self.safe_string(arg, 'symbol') + marketId = self.safe_string(ticker, 'instId', utaMarketId) + market = self.safe_market(marketId, market, None, marketType) + close = self.safe_string_2(ticker, 'lastPr', 'lastPrice') + changeDecimal = self.safe_string(ticker, 'change24h', '') + change = self.safe_string(ticker, 'price24hPcnt', Precise.string_mul(changeDecimal, '100')) + return self.safe_ticker({ + 'symbol': market['symbol'], + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string_2(ticker, 'high24h', 'highPrice24h'), + 'low': self.safe_string_2(ticker, 'low24h', 'lowPrice24h'), + 'bid': self.safe_string_2(ticker, 'bidPr', 'bid1Price'), + 'bidVolume': self.safe_string_2(ticker, 'bidSz', 'bid1Size'), + 'ask': self.safe_string_2(ticker, 'askPr', 'ask1Price'), + 'askVolume': self.safe_string_2(ticker, 'askSz', 'ask1Size'), + 'vwap': None, + 'open': self.safe_string_2(ticker, 'open24h', 'openPrice24h'), + 'close': close, + 'last': close, + 'previousClose': None, + 'change': None, + 'percentage': change, + 'average': None, + 'baseVolume': self.safe_string_2(ticker, 'baseVolume', 'volume24h'), + 'quoteVolume': self.safe_string_2(ticker, 'quoteVolume', 'turnover24h'), + 'info': ticker, + }, market) + + async def watch_bids_asks(self, symbols: Strings = None, params={}) -> Tickers: + """ + watches best bid & ask for symbols + + https://www.bitget.com/api-doc/spot/websocket/public/Tickers-Channel + https://www.bitget.com/api-doc/contract/websocket/public/Tickers-Channel + https://www.bitget.com/api-doc/uta/websocket/public/Tickers-Channel + + :param str[] symbols: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, False) + market = self.market(symbols[0]) + instType = None + uta = None + uta, params = self.handle_option_and_params(params, 'watchBidsAsks', 'uta', False) + instType, params = self.get_inst_type(market, uta, params) + topics = [] + messageHashes = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + marketInner = self.market(symbol) + args: dict = { + 'instType': instType, + } + topicOrChannel = 'topic' if uta else 'channel' + symbolOrInstId = 'symbol' if uta else 'instId' + args[topicOrChannel] = 'ticker' + args[symbolOrInstId] = marketInner['id'] + topics.append(args) + messageHashes.append('bidask:' + symbol) + tickers = await self.watch_public_multiple(messageHashes, topics, params) + if self.newUpdates: + result: dict = {} + result[tickers['symbol']] = tickers + return result + return self.filter_by_array(self.bidsasks, 'symbol', symbols) + + def handle_bid_ask(self, client: Client, message): + ticker = self.parse_ws_bid_ask(message) + symbol = ticker['symbol'] + self.bidsasks[symbol] = ticker + messageHash = 'bidask:' + symbol + client.resolve(ticker, messageHash) + + def parse_ws_bid_ask(self, message, market=None): + arg = self.safe_value(message, 'arg', {}) + data = self.safe_value(message, 'data', []) + ticker = self.safe_value(data, 0, {}) + utaTimestamp = self.safe_integer(message, 'ts') + timestamp = self.safe_integer(ticker, 'ts', utaTimestamp) + instType = self.safe_string_lower(arg, 'instType') + marketType = 'spot' if (instType == 'spot') else 'contract' + utaMarketId = self.safe_string(arg, 'symbol') + marketId = self.safe_string(ticker, 'instId', utaMarketId) + market = self.safe_market(marketId, market, None, marketType) + return self.safe_ticker({ + 'symbol': market['symbol'], + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'ask': self.safe_string_2(ticker, 'askPr', 'ask1Price'), + 'askVolume': self.safe_string_2(ticker, 'askSz', 'ask1Size'), + 'bid': self.safe_string_2(ticker, 'bidPr', 'bid1Price'), + 'bidVolume': self.safe_string_2(ticker, 'bidSz', 'bid1Size'), + 'info': ticker, + }, market) + + async def watch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + watches historical candlestick data containing the open, high, low, close price, and the volume of a market + + https://www.bitget.com/api-doc/spot/websocket/public/Candlesticks-Channel + https://www.bitget.com/api-doc/contract/websocket/public/Candlesticks-Channel + https://www.bitget.com/api-doc/uta/websocket/public/Candlesticks-Channel + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + timeframes = self.safe_value(self.options, 'timeframes') + interval = self.safe_string(timeframes, timeframe) + messageHash = None + instType = None + uta = None + uta, params = self.handle_option_and_params(params, 'watchOHLCV', 'uta', False) + instType, params = self.get_inst_type(market, uta, params) + args: dict = { + 'instType': instType, + } + if uta: + args['topic'] = 'kline' + args['symbol'] = market['id'] + args['interval'] = interval + params = self.extend(params, {'uta': True}) + messageHash = 'kline:' + symbol + else: + args['channel'] = 'candle' + interval + args['instId'] = market['id'] + messageHash = 'candles:' + timeframe + ':' + symbol + ohlcv = await self.watch_public(messageHash, args, params) + if self.newUpdates: + limit = ohlcv.getLimit(symbol, limit) + return self.filter_by_since_limit(ohlcv, since, limit, 0, True) + + async def un_watch_ohlcv(self, symbol: str, timeframe: str = '1m', params={}) -> Any: + """ + unsubscribe from the ohlcv channel + + https://www.bitget.com/api-doc/spot/websocket/public/Candlesticks-Channel + https://www.bitget.com/api-doc/contract/websocket/public/Candlesticks-Channel + https://www.bitget.com/api-doc/uta/websocket/public/Candlesticks-Channel + + :param str symbol: unified symbol of the market to unwatch the ohlcv for + :param str [timeframe]: the period for the ratio, default is 1 minute + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + timeframes = self.safe_dict(self.options, 'timeframes') + interval = self.safe_string(timeframes, timeframe) + channel = None + market = None + if symbol is not None: + market = self.market(symbol) + instType = None + messageHash = None + uta = None + uta, params = self.handle_option_and_params(params, 'unWatchOHLCV', 'uta', False) + instType, params = self.get_inst_type(market, uta, params) + args: dict = { + 'instType': instType, + } + if uta: + channel = 'kline' + args['topic'] = channel + args['symbol'] = market['id'] + args['interval'] = interval + params = self.extend(params, {'uta': True}) + params['interval'] = interval + messageHash = channel + symbol + else: + channel = 'candle' + interval + args['channel'] = channel + args['instId'] = market['id'] + messageHash = 'candles:' + interval + return await self.un_watch_channel(symbol, channel, messageHash, params) + + def handle_ohlcv(self, client: Client, message): + # + # { + # "action": "snapshot", + # "arg": { + # "instType": "SPOT", + # "channel": "candle1m", + # "instId": "BTCUSDT" + # }, + # "data": [ + # [ + # "1701871620000", + # "44080.23", + # "44080.23", + # "44028.5", + # "44028.51", + # "9.9287", + # "437404.105512", + # "437404.105512" + # ], + # [ + # "1701871680000", + # "44028.51", + # "44108.11", + # "44028.5", + # "44108.11", + # "17.139", + # "755436.870643", + # "755436.870643" + # ], + # ], + # "ts": 1701901610417 + # } + # + # uta + # + # { + # "action": "snapshot", + # "arg": { + # "instType": "usdt-futures", + # "topic": "kline", + # "symbol": "BTCUSDT", + # "interval": "1m" + # }, + # "data": [ + # { + # "start": "1755564480000", + # "open": "116286", + # "close": "116256.2", + # "high": "116310.2", + # "low": "116232.8", + # "volume": "39.7062", + # "turnover": "4616746.46654" + # }, + # ], + # "ts": 1755594421877 + # } + # + arg = self.safe_value(message, 'arg', {}) + instType = self.safe_string_lower(arg, 'instType') + marketType = 'spot' if (instType == 'spot') else 'contract' + marketId = self.safe_string_2(arg, 'instId', 'symbol') + market = self.safe_market(marketId, None, None, marketType) + symbol = market['symbol'] + self.ohlcvs[symbol] = self.safe_value(self.ohlcvs, symbol, {}) + channel = self.safe_string_2(arg, 'channel', 'topic') + interval = self.safe_string(arg, 'interval') + isUta = None + if interval is None: + isUta = False + interval = channel.replace('candle', '') + else: + isUta = True + timeframes = self.safe_value(self.options, 'timeframes') + timeframe = self.find_timeframe(interval, timeframes) + stored = self.safe_value(self.ohlcvs[symbol], timeframe) + if stored is None: + limit = self.safe_integer(self.options, 'OHLCVLimit', 1000) + stored = ArrayCacheByTimestamp(limit) + self.ohlcvs[symbol][timeframe] = stored + data = self.safe_value(message, 'data', []) + for i in range(0, len(data)): + parsed = self.parse_ws_ohlcv(data[i], market) + stored.append(parsed) + messageHash = None + if isUta: + messageHash = 'kline:' + symbol + else: + messageHash = 'candles:' + timeframe + ':' + symbol + client.resolve(stored, messageHash) + + def parse_ws_ohlcv(self, ohlcv, market=None) -> list: + # + # [ + # "1701871620000", # timestamp + # "44080.23", # open + # "44080.23", # high + # "44028.5", # low + # "44028.51", # close + # "9.9287", # base volume + # "437404.105512", # quote volume + # "437404.105512" # USDT volume + # ] + # + # uta + # + # { + # "start": "1755564480000", + # "open": "116286", + # "close": "116256.2", + # "high": "116310.2", + # "low": "116232.8", + # "volume": "39.7062", + # "turnover": "4616746.46654" + # } + # + volumeIndex = 6 if (market['inverse']) else 5 + return [ + self.safe_integer_2(ohlcv, 'start', 0), + self.safe_number_2(ohlcv, 'open', 1), + self.safe_number_2(ohlcv, 'high', 2), + self.safe_number_2(ohlcv, 'low', 3), + self.safe_number_2(ohlcv, 'close', 4), + self.safe_number_2(ohlcv, 'volume', volumeIndex), + ] + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://www.bitget.com/api-doc/spot/websocket/public/Depth-Channel + https://www.bitget.com/api-doc/contract/websocket/public/Order-Book-Channel + https://www.bitget.com/api-doc/uta/websocket/public/Order-Book-Channel + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + return await self.watch_order_book_for_symbols([symbol], limit, params) + + async def un_watch_order_book(self, symbol: str, params={}) -> Any: + """ + unsubscribe from the orderbook channel + + https://www.bitget.com/api-doc/spot/websocket/public/Depth-Channel + https://www.bitget.com/api-doc/contract/websocket/public/Order-Book-Channel + https://www.bitget.com/api-doc/uta/websocket/public/Order-Book-Channel + + :param str symbol: unified symbol of the market to fetch the order book for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.limit]: orderbook limit, default is None + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + channel = 'books' + limit = self.safe_integer(params, 'limit') + if (limit == 1) or (limit == 5) or (limit == 15) or (limit == 50): + params = self.omit(params, 'limit') + channel += str(limit) + return await self.un_watch_channel(symbol, channel, 'orderbook', params) + + async def un_watch_channel(self, symbol: str, channel: str, messageHashTopic: str, params={}) -> Any: + await self.load_markets() + market = self.market(symbol) + messageHash = 'unsubscribe:' + messageHashTopic + ':' + market['symbol'] + instType = None + uta = None + uta, params = self.handle_option_and_params(params, 'unWatchChannel', 'uta', False) + instType, params = self.get_inst_type(market, uta, params) + args: dict = { + 'instType': instType, + } + if uta: + args['topic'] = channel + args['symbol'] = market['id'] + args['interval'] = self.safe_string(params, 'interval', '1m') + params = self.extend(params, {'uta': True}) + params = self.omit(params, 'interval') + else: + args['channel'] = channel + args['instId'] = market['id'] + return await self.un_watch_public(messageHash, args, params) + + async def watch_order_book_for_symbols(self, symbols: List[str], limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://www.bitget.com/api-doc/spot/websocket/public/Depth-Channel + https://www.bitget.com/api-doc/contract/websocket/public/Order-Book-Channel + https://www.bitget.com/api-doc/uta/websocket/public/Order-Book-Channel + + :param str[] symbols: unified array of symbols + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + channel = 'books' + incrementalFeed = True + if (limit == 1) or (limit == 5) or (limit == 15) or (limit == 50): + channel += str(limit) + incrementalFeed = False + topics = [] + messageHashes = [] + uta = None + uta, params = self.handle_option_and_params(params, 'watchOrderBookForSymbols', 'uta', False) + for i in range(0, len(symbols)): + symbol = symbols[i] + market = self.market(symbol) + instType = None + instType, params = self.get_inst_type(market, uta, params) + args: dict = { + 'instType': instType, + } + topicOrChannel = 'topic' if uta else 'channel' + symbolOrInstId = 'symbol' if uta else 'instId' + args[topicOrChannel] = channel + args[symbolOrInstId] = market['id'] + topics.append(args) + messageHashes.append('orderbook:' + symbol) + if uta: + params['uta'] = True + orderbook = await self.watch_public_multiple(messageHashes, topics, params) + if incrementalFeed: + return orderbook.limit() + else: + return orderbook + + def handle_order_book(self, client: Client, message): + # + # { + # "action":"snapshot", + # "arg":{ + # "instType":"SPOT", + # "channel":"books5", + # "instId":"BTCUSDT" + # }, + # "data":[ + # { + # "asks":[ + # ["21041.11","0.0445"], + # ["21041.16","0.0411"], + # ["21041.21","0.0421"], + # ["21041.26","0.0811"], + # ["21041.65","1.9465"] + # ], + # "bids":[ + # ["21040.76","0.0417"], + # ["21040.71","0.0434"], + # ["21040.66","0.1141"], + # ["21040.61","0.3004"], + # ["21040.60","1.3357"] + # ], + # "checksum": -1367582038, + # "ts":"1656413855484" + # } + # ] + # } + # + # { + # "action": "snapshot", + # "arg": {"instType": "usdt-futures", "topic": "books", "symbol": "BTCUSDT"}, + # "data": [ + # { + # "a": [Array], + # "b": [Array], + # "checksum": 0, + # "pseq": 0, + # "seq": "1343064377779269632", + # "ts": "1755937421270" + # } + # ], + # "ts": 1755937421337 + # } + # + arg = self.safe_value(message, 'arg') + channel = self.safe_string_2(arg, 'channel', 'topic') + instType = self.safe_string_lower(arg, 'instType') + marketType = 'spot' if (instType == 'spot') else 'contract' + marketId = self.safe_string_2(arg, 'instId', 'symbol') + market = self.safe_market(marketId, None, None, marketType) + symbol = market['symbol'] + messageHash = 'orderbook:' + symbol + data = self.safe_value(message, 'data') + rawOrderBook = self.safe_value(data, 0) + timestamp = self.safe_integer(rawOrderBook, 'ts') + incrementalBook = channel == 'books' + if incrementalBook: + # storedOrderBook = self.safe_value(self.orderbooks, symbol) + if not (symbol in self.orderbooks): + # ob = self.order_book({}) + ob = self.counted_order_book({}) + ob['symbol'] = symbol + self.orderbooks[symbol] = ob + storedOrderBook = self.orderbooks[symbol] + asks = self.safe_list_2(rawOrderBook, 'asks', 'a', []) + bids = self.safe_list_2(rawOrderBook, 'bids', 'b', []) + self.handle_deltas(storedOrderBook['asks'], asks) + self.handle_deltas(storedOrderBook['bids'], bids) + storedOrderBook['timestamp'] = timestamp + storedOrderBook['datetime'] = self.iso8601(timestamp) + checksum = self.handle_option('watchOrderBook', 'checksum', True) + isSnapshot = self.safe_string(message, 'action') == 'snapshot' # snapshot does not have a checksum + if not isSnapshot and checksum: + storedAsks = storedOrderBook['asks'] + storedBids = storedOrderBook['bids'] + asksLength = len(storedAsks) + bidsLength = len(storedBids) + payloadArray = [] + for i in range(0, 25): + if i < bidsLength: + payloadArray.append(storedBids[i][2][0]) + payloadArray.append(storedBids[i][2][1]) + if i < asksLength: + payloadArray.append(storedAsks[i][2][0]) + payloadArray.append(storedAsks[i][2][1]) + payload = ':'.join(payloadArray) + calculatedChecksum = self.crc32(payload, True) + responseChecksum = self.safe_integer(rawOrderBook, 'checksum') + if calculatedChecksum != responseChecksum: + # if messageHash in client.subscriptions: + # # del client.subscriptions[messageHash] + # # del self.orderbooks[symbol] + # } + self.spawn(self.handle_check_sum_error, client, symbol, messageHash) + return + else: + orderbook = self.order_book({}) + parsedOrderbook = self.parse_order_book(rawOrderBook, symbol, timestamp) + orderbook.reset(parsedOrderbook) + self.orderbooks[symbol] = orderbook + client.resolve(self.orderbooks[symbol], messageHash) + + async def handle_check_sum_error(self, client: Client, symbol: str, messageHash: str): + await self.un_watch_order_book(symbol) + error = ChecksumError(self.id + ' ' + self.orderbook_checksum_message(symbol)) + client.reject(error, messageHash) + + def handle_delta(self, bookside, delta): + bidAsk = self.parse_bid_ask(delta, 0, 1) + # we store the string representations in the orderbook for checksum calculation + # self simplifies the code for generating checksums do not need to do any complex number transformations + bidAsk.append(delta) + bookside.storeArray(bidAsk) + + def handle_deltas(self, bookside, deltas): + for i in range(0, len(deltas)): + self.handle_delta(bookside, deltas[i]) + + async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://www.bitget.com/api-doc/spot/websocket/public/Trades-Channel + https://www.bitget.com/api-doc/contract/websocket/public/New-Trades-Channel + https://www.bitget.com/api-doc/uta/websocket/public/New-Trades-Channel + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :returns dict[]: a list of `trade structures ` + """ + return await self.watch_trades_for_symbols([symbol], since, limit, params) + + async def watch_trades_for_symbols(self, symbols: List[str], since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://www.bitget.com/api-doc/spot/websocket/public/Trades-Channel + https://www.bitget.com/api-doc/contract/websocket/public/New-Trades-Channel + https://www.bitget.com/api-doc/uta/websocket/public/New-Trades-Channel + + :param str[] symbols: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :returns dict[]: a list of `trade structures ` + """ + symbolsLength = len(symbols) + if symbolsLength == 0: + raise ArgumentsRequired(self.id + ' watchTradesForSymbols() requires a non-empty array of symbols') + await self.load_markets() + symbols = self.market_symbols(symbols) + uta = None + uta, params = self.handle_option_and_params(params, 'watchTradesForSymbols', 'uta', False) + topics = [] + messageHashes = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + market = self.market(symbol) + instType = None + instType, params = self.get_inst_type(market, uta, params) + args: dict = { + 'instType': instType, + } + topicOrChannel = 'topic' if uta else 'channel' + symbolOrInstId = 'symbol' if uta else 'instId' + args[topicOrChannel] = 'publicTrade' if uta else 'trade' + args[symbolOrInstId] = market['id'] + topics.append(args) + messageHashes.append('trade:' + symbol) + if uta: + params = self.extend(params, {'uta': True}) + trades = await self.watch_public_multiple(messageHashes, topics, params) + if self.newUpdates: + first = self.safe_value(trades, 0) + tradeSymbol = self.safe_string(first, 'symbol') + limit = trades.getLimit(tradeSymbol, limit) + result = self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + if self.handle_option('watchTrades', 'ignoreDuplicates', True): + filtered = self.remove_repeated_trades_from_array(result) + filtered = self.sort_by(filtered, 'timestamp') + return filtered + return result + + async def un_watch_trades(self, symbol: str, params={}) -> Any: + """ + unsubscribe from the trades channel + + https://www.bitget.com/api-doc/spot/websocket/public/Trades-Channel + https://www.bitget.com/api-doc/contract/websocket/public/New-Trades-Channel + https://www.bitget.com/api-doc/uta/websocket/public/New-Trades-Channel + + :param str symbol: unified symbol of the market to unwatch the trades for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :returns any: status of the unwatch request + """ + uta = None + uta, params = self.handle_option_and_params(params, 'unWatchTrades', 'uta', False) + channelTopic = 'publicTrade' if uta else 'trade' + return await self.un_watch_channel(symbol, channelTopic, 'trade', params) + + def handle_trades(self, client: Client, message): + # + # { + # "action": "snapshot", + # "arg": {"instType": "SPOT", "channel": "trade", "instId": "BTCUSDT"}, + # "data": [ + # { + # "ts": "1701910980366", + # "price": "43854.01", + # "size": "0.0535", + # "side": "buy", + # "tradeId": "1116461060594286593" + # }, + # ], + # "ts": 1701910980730 + # } + # + # uta + # + # { + # "action": "snapshot", + # "arg": {"instType": "spot", "topic": "publicTrade", "symbol": "BTCUSDT"}, + # "data": [ + # { + # "T": "1756287827920", + # "P": "110878.5", + # "v": "0.07", + # "S": "buy", + # "L": "1344534089797185550" + # "i": "1344534089797185549" + # }, + # ], + # "ts": 1701910980730 + # } + # + arg = self.safe_value(message, 'arg', {}) + instType = self.safe_string_lower(arg, 'instType') + marketType = 'spot' if (instType == 'spot') else 'contract' + marketId = self.safe_string_2(arg, 'instId', 'symbol') + market = self.safe_market(marketId, None, None, marketType) + symbol = market['symbol'] + stored = self.safe_value(self.trades, symbol) + if stored is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + stored = ArrayCache(limit) + self.trades[symbol] = stored + data = self.safe_list(message, 'data', []) + length = len(data) + # fix chronological order by reversing + for i in range(0, length): + index = length - i - 1 + rawTrade = data[index] + parsed = self.parse_ws_trade(rawTrade, market) + stored.append(parsed) + messageHash = 'trade:' + symbol + client.resolve(stored, messageHash) + + def parse_ws_trade(self, trade, market=None): + # + # { + # "ts": "1701910980366", + # "price": "43854.01", + # "size": "0.0535", + # "side": "buy", + # "tradeId": "1116461060594286593" + # } + # swap private + # + # { + # "orderId": "1169142761031114781", + # "tradeId": "1169142761312637004", + # "symbol": "LTCUSDT", + # "orderType": "market", + # "side": "buy", + # "price": "80.87", + # "baseVolume": "0.1", + # "quoteVolume": "8.087", + # "profit": "0", + # "tradeSide": "open", + # "posMode": "hedge_mode", + # "tradeScope": "taker", + # "feeDetail": [ + # { + # "feeCoin": "USDT", + # "deduction": "no", + # "totalDeductionFee": "0", + # "totalFee": "-0.0048522" + # } + # ], + # "cTime": "1714471276596", + # "uTime": "1714471276596" + # } + # spot private + # { + # "orderId": "1169142457356959747", + # "tradeId": "1169142457636958209", + # "symbol": "LTCUSDT", + # "orderType": "market", + # "side": "buy", + # "priceAvg": "81.069", + # "size": "0.074", + # "amount": "5.999106", + # "tradeScope": "taker", + # "feeDetail": [ + # { + # "feeCoin": "LTC", + # "deduction": "no", + # "totalDeductionFee": "0", + # "totalFee": "0.000074" + # } + # ], + # "cTime": "1714471204194", + # "uTime": "1714471204194" + # } + # + # uta private + # + # { + # "symbol": "BTCUSDT", + # "orderType": "market", + # "updatedTime": "1736378720623", + # "side": "buy", + # "orderId": "1288888888888888888", + # "execPnl": "0", + # "feeDetail": [ + # { + # "feeCoin": "USDT", + # "fee": "0.569958" + # } + # ], + # "execTime": "1736378720623", + # "tradeScope": "taker", + # "tradeSide": "open", + # "execId": "1288888888888888888", + # "execLinkId": "1288888888888888888", + # "execPrice": "94993", + # "holdSide": "long", + # "execValue": "949.93", + # "category": "USDT-FUTURES", + # "execQty": "0.01", + # "clientOid": "1288888888888888889" + # uta + # + # { + # "i": "1344534089797185549", # Fill execution ID + # "L": "1344534089797185550", # Execution correlation ID + # "p": "110878.5", # Fill price + # "v": "0.07", # Fill size + # "S": "buy", # Fill side + # "T": "1756287827920" # Fill timestamp + # } + # + instId = self.safe_string_2(trade, 'symbol', 'instId') + posMode = self.safe_string(trade, 'posMode') + category = self.safe_string(trade, 'category') + defaultType = None + if category is not None: + defaultType = 'contract' if (category != 'SPOT') else 'spot' + else: + defaultType = 'contract' if (posMode is not None) else 'spot' + if market is None: + market = self.safe_market(instId, None, None, defaultType) + timestamp = self.safe_integer_n(trade, ['uTime', 'cTime', 'ts', 'T', 'execTime']) + feeDetail = self.safe_list(trade, 'feeDetail', []) + first = self.safe_dict(feeDetail, 0) + fee = None + if first is not None: + feeCurrencyId = self.safe_string(first, 'feeCoin') + feeCurrencyCode = self.safe_currency_code(feeCurrencyId) + fee = { + 'cost': Precise.string_abs(self.safe_string_2(first, 'totalFee', 'fee')), + 'currency': feeCurrencyCode, + } + return self.safe_trade({ + 'info': trade, + 'id': self.safe_string_n(trade, ['tradeId', 'i', 'execId']), + 'order': self.safe_string_2(trade, 'orderId', 'L'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'type': self.safe_string(trade, 'orderType'), + 'side': self.safe_string_2(trade, 'side', 'S'), + 'takerOrMaker': self.safe_string(trade, 'tradeScope'), + 'price': self.safe_string_n(trade, ['priceAvg', 'price', 'execPrice', 'P']), + 'amount': self.safe_string_n(trade, ['size', 'baseVolume', 'execQty', 'v']), + 'cost': self.safe_string_n(trade, ['amount', 'quoteVolume', 'execValue']), + 'fee': fee, + }, market) + + async def watch_positions(self, symbols: Strings = None, since: Int = None, limit: Int = None, params={}) -> List[Position]: + """ + watch all open positions + + https://www.bitget.com/api-doc/contract/websocket/private/Positions-Channel + https://www.bitget.com/api-doc/uta/websocket/private/Positions-Channel + + :param str[]|None symbols: list of unified market symbols + :param int [since]: the earliest time in ms to fetch positions for + :param int [limit]: the maximum number of positions to retrieve + :param dict params: extra parameters specific to the exchange API endpoint + :param str [params.instType]: one of 'USDT-FUTURES', 'USDC-FUTURES', 'COIN-FUTURES', 'SUSDT-FUTURES', 'SUSDC-FUTURES' or 'SCOIN-FUTURES', default is 'USDT-FUTURES' + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :returns dict[]: a list of `position structure ` + """ + await self.load_markets() + market = None + messageHash = '' + subscriptionHash = 'positions' + instType = 'USDT-FUTURES' + uta = None + uta, params = self.handle_option_and_params(params, 'watchPositions', 'uta', False) + symbols = self.market_symbols(symbols) + if not self.is_empty(symbols): + market = self.get_market_from_symbols(symbols) + instType, params = self.get_inst_type(market, uta, params) + if uta: + instType = 'UTA' + messageHash = instType + ':positions' + messageHash + args: dict = { + 'instType': instType, + } + topicOrChannel = 'topic' if uta else 'channel' + channel = 'position' if uta else 'positions' + args[topicOrChannel] = channel + if not uta: + args['instId'] = 'default' + else: + params = self.extend(params, {'uta': True}) + newPositions = await self.watch_private(messageHash, subscriptionHash, args, params) + if self.newUpdates: + return newPositions + return self.filter_by_symbols_since_limit(newPositions, symbols, since, limit, True) + + def handle_positions(self, client: Client, message): + # + # { + # "action": "snapshot", + # "arg": { + # "instType": "USDT-FUTURES", + # "channel": "positions", + # "instId": "default" + # }, + # "data": [ + # { + # "posId": "926036334386778112", + # "instId": "BTCUSDT", + # "marginCoin": "USDT", + # "marginSize": "2.19245", + # "marginMode": "crossed", + # "holdSide": "long", + # "posMode": "hedge_mode", + # "total": "0.001", + # "available": "0.001", + # "frozen": "0", + # "openPriceAvg": "43849", + # "leverage": 20, + # "achievedProfits": "0", + # "unrealizedPL": "-0.0032", + # "unrealizedPLR": "-0.00145955438", + # "liquidationPrice": "17629.684814834", + # "keepMarginRate": "0.004", + # "marginRate": "0.007634649185", + # "cTime": "1652331666985", + # "uTime": "1701913016923", + # "autoMargin": "off" + # }, + # ... + # ] + # "ts": 1701913043767 + # } + # + # uta + # + # { + # "data": [ + # { + # "symbol": "BTCUSDT", + # "leverage": "20", + # "openFeeTotal": "", + # "mmr": "", + # "breakEvenPrice": "", + # "available": "0", + # "liqPrice": "", + # "marginMode": "crossed", + # "unrealisedPnl": "0", + # "markPrice": "94987.1", + # "createdTime": "1736378720620", + # "avgPrice": "0", + # "totalFundingFee": "0", + # "updatedTime": "1736378720620", + # "marginCoin": "USDT", + # "frozen": "0", + # "profitRate": "", + # "closeFeeTotal": "", + # "marginSize": "0", + # "curRealisedPnl": "0", + # "size": "0", + # "positionStatus": "ended", + # "posSide": "long", + # "holdMode": "hedge_mode" + # } + # ], + # "arg": { + # "instType": "UTA", + # "topic": "position" + # }, + # "action": "snapshot", + # "ts": 1730711666652 + # } + # + arg = self.safe_dict(message, 'arg', {}) + instType = self.safe_string(arg, 'instType', '') + if self.positions is None: + self.positions = {} + action = self.safe_string(message, 'action') + if not (instType in self.positions) or (action == 'snapshot'): + self.positions[instType] = ArrayCacheBySymbolBySide() + cache = self.positions[instType] + rawPositions = self.safe_list(message, 'data', []) + newPositions = [] + for i in range(0, len(rawPositions)): + rawPosition = rawPositions[i] + marketId = self.safe_string_2(rawPosition, 'instId', 'symbol') + market = self.safe_market(marketId, None, None, 'contract') + position = self.parse_ws_position(rawPosition, market) + newPositions.append(position) + cache.append(position) + messageHashes = self.find_message_hashes(client, instType + ':positions::') + for i in range(0, len(messageHashes)): + messageHash = messageHashes[i] + parts = messageHash.split('::') + symbolsString = parts[1] + symbols = symbolsString.split(',') + positions = self.filter_by_array(newPositions, 'symbol', symbols, False) + if not self.is_empty(positions): + client.resolve(positions, messageHash) + client.resolve(newPositions, instType + ':positions') + + def parse_ws_position(self, position, market=None): + # + # { + # "posId": "926036334386778112", + # "instId": "BTCUSDT", + # "marginCoin": "USDT", + # "marginSize": "2.19245", + # "marginMode": "crossed", + # "holdSide": "long", + # "posMode": "hedge_mode", + # "total": "0.001", + # "available": "0.001", + # "frozen": "0", + # "openPriceAvg": "43849", + # "leverage": 20, + # "achievedProfits": "0", + # "unrealizedPL": "-0.0032", + # "unrealizedPLR": "-0.00145955438", + # "liquidationPrice": "17629.684814834", + # "keepMarginRate": "0.004", + # "marginRate": "0.007634649185", + # "cTime": "1652331666985", + # "uTime": "1701913016923", + # "autoMargin": "off" + # } + # + # uta + # + # { + # "symbol": "BTCUSDT", + # "leverage": "20", + # "openFeeTotal": "", + # "mmr": "", + # "breakEvenPrice": "", + # "available": "0", + # "liqPrice": "", + # "marginMode": "crossed", + # "unrealisedPnl": "0", + # "markPrice": "94987.1", + # "createdTime": "1736378720620", + # "avgPrice": "0", + # "totalFundingFee": "0", + # "updatedTime": "1736378720620", + # "marginCoin": "USDT", + # "frozen": "0", + # "profitRate": "", + # "closeFeeTotal": "", + # "marginSize": "0", + # "curRealisedPnl": "0", + # "size": "0", + # "positionStatus": "ended", + # "posSide": "long", + # "holdMode": "hedge_mode" + # } + # + marketId = self.safe_string_2(position, 'instId', 'symbol') + marginModeId = self.safe_string(position, 'marginMode') + marginMode = self.get_supported_mapping(marginModeId, { + 'crossed': 'cross', + 'isolated': 'isolated', + }) + hedgedId = self.safe_string_2(position, 'posMode', 'holdMode') + hedged = True if (hedgedId == 'hedge_mode') else False + timestamp = self.safe_integer_n(position, ['updatedTime', 'uTime', 'cTime', 'createdTime']) + percentageDecimal = self.safe_string_2(position, 'unrealizedPLR', 'profitRate') + percentage = Precise.string_mul(percentageDecimal, '100') + contractSize = None + if market is not None: + contractSize = market['contractSize'] + return self.safe_position({ + 'info': position, + 'id': self.safe_string(position, 'posId'), + 'symbol': self.safe_symbol(marketId, market, None, 'contract'), + 'notional': None, + 'marginMode': marginMode, + 'liquidationPrice': self.safe_number_2(position, 'liquidationPrice', 'liqPrice'), + 'entryPrice': self.safe_number_2(position, 'openPriceAvg', 'avgPrice'), + 'unrealizedPnl': self.safe_number_2(position, 'unrealizedPL', 'unrealisedPnl'), + 'percentage': self.parse_number(percentage), + 'contracts': self.safe_number_2(position, 'total', 'size'), + 'contractSize': contractSize, + 'markPrice': self.safe_number(position, 'markPrice'), + 'side': self.safe_string_2(position, 'holdSide', 'posSide'), + 'hedged': hedged, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'maintenanceMargin': None, + 'maintenanceMarginPercentage': self.safe_number_2(position, 'keepMarginRate', 'mmr'), + 'collateral': self.safe_number(position, 'available'), + 'initialMargin': self.safe_number(position, 'marginSize'), + 'initialMarginPercentage': None, + 'leverage': self.safe_number(position, 'leverage'), + 'marginRatio': self.safe_number(position, 'marginRate'), + }) + + async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + watches information on multiple orders made by the user + + https://www.bitget.com/api-doc/spot/websocket/private/Order-Channel + https://www.bitget.com/api-doc/spot/websocket/private/Plan-Order-Channel + https://www.bitget.com/api-doc/contract/websocket/private/Order-Channel + https://www.bitget.com/api-doc/contract/websocket/private/Plan-Order-Channel + https://www.bitget.com/api-doc/margin/cross/websocket/private/Cross-Orders + https://www.bitget.com/api-doc/margin/isolated/websocket/private/Isolate-Orders + https://www.bitget.com/api-doc/uta/websocket/private/Order-Channel + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: *contract only* set to True for watching trigger orders + :param str [params.marginMode]: 'isolated' or 'cross' for watching spot margin orders] + :param str [params.type]: 'spot', 'swap' + :param str [params.subType]: 'linear', 'inverse' + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + market = None + marketId = None + isTrigger = None + isTrigger, params = self.is_trigger_order(params) + messageHash = 'triggerOrder' if (isTrigger) else 'order' + subscriptionHash = 'order:trades' + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + marketId = market['id'] + messageHash = messageHash + ':' + symbol + uta = None + uta, params = self.handle_option_and_params(params, 'watchOrders', 'uta', False) + productType = self.safe_string(params, 'productType') + type = None + type, params = self.handle_market_type_and_params('watchOrders', market, params) + subType = None + subType, params = self.handle_sub_type_and_params('watchOrders', market, params, 'linear') + if (type == 'spot' or type == 'margin') and (symbol is None): + marketId = 'default' + if (productType is None) and (type != 'spot') and (symbol is None): + messageHash = messageHash + ':' + subType + elif productType == 'USDT-FUTURES': + messageHash = messageHash + ':linear' + elif productType == 'COIN-FUTURES': + messageHash = messageHash + ':inverse' + elif productType == 'USDC-FUTURES': + messageHash = messageHash + ':usdcfutures' # non unified channel + instType = None + if market is None and type == 'spot': + instType = 'SPOT' + else: + instType, params = self.get_inst_type(market, uta, params) + if type == 'spot' and (symbol is not None): + subscriptionHash = subscriptionHash + ':' + symbol + if isTrigger: + subscriptionHash = subscriptionHash + ':stop' # we don't want to re-use the same subscription hash for stop orders + instId = marketId if (type == 'spot' or type == 'margin') else 'default' # different from other streams here the 'rest' id is required for spot markets, contract markets require default here + channel = 'orders-algo' if isTrigger else 'orders' + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('watchOrders', params) + if marginMode is not None: + instType = 'MARGIN' + messageHash = messageHash + ':' + marginMode + if marginMode == 'isolated': + channel = 'orders-isolated' + else: + channel = 'orders-crossed' + if uta: + instType = 'UTA' + channel = 'order' + subscriptionHash = subscriptionHash + ':' + instType + args: dict = { + 'instType': instType, + } + topicOrChannel = 'topic' if uta else 'channel' + args[topicOrChannel] = channel + if not uta: + args['instId'] = instId + else: + params = self.extend(params, {'uta': True}) + orders = await self.watch_private(messageHash, subscriptionHash, args, params) + if self.newUpdates: + limit = orders.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(orders, symbol, since, limit, True) + + def handle_order(self, client: Client, message): + # + # spot + # + # { + # "action": "snapshot", + # "arg": {"instType": "SPOT", "channel": "orders", "instId": "BTCUSDT"}, + # "data": [ + # # see all examples in parseWsOrder + # ], + # "ts": 1701923297285 + # } + # + # contract + # + # { + # "action": "snapshot", + # "arg": {"instType": "USDT-FUTURES", "channel": "orders", "instId": "default"}, + # "data": [ + # # see all examples in parseWsOrder + # ], + # "ts": 1701920595879 + # } + # + # isolated and cross margin + # + # { + # "action": "snapshot", + # "arg": {"instType": "MARGIN", "channel": "orders-crossed", "instId": "BTCUSDT"}, + # "data": [ + # # see examples in parseWsOrder + # ], + # "ts": 1701923982497 + # } + # + # uta + # + # { + # "action": "snapshot", + # "arg": { + # "instType": "UTA", + # "topic": "order" + # }, + # "data": [ + # { + # "category": "usdt-futures", + # "symbol": "BTCUSDT", + # "orderId": "xxx", + # "clientOid": "xxx", + # "price": "", + # "qty": "0.001", + # "amount": "1000", + # "holdMode": "hedge_mode", + # "holdSide": "long", + # "tradeSide": "open", + # "orderType": "market", + # "timeInForce": "gtc", + # "side": "buy", + # "marginMode": "crossed", + # "marginCoin": "USDT", + # "reduceOnly": "no", + # "cumExecQty": "0.001", + # "cumExecValue": "83.1315", + # "avgPrice": "83131.5", + # "totalProfit": "0", + # "orderStatus": "filled", + # "cancelReason": "", + # "leverage": "20", + # "feeDetail": [ + # { + # "feeCoin": "USDT", + # "fee": "0.0332526" + # } + # ], + # "createdTime": "1742367838101", + # "updatedTime": "1742367838115", + # "stpMode": "none" + # } + # ], + # "ts": 1742367838124 + # } + # + arg = self.safe_dict(message, 'arg', {}) + channel = self.safe_string_2(arg, 'channel', 'topic') + instType = self.safe_string_lower(arg, 'instType') + argInstId = self.safe_string(arg, 'instId') + marketType = None + if instType == 'spot': + marketType = 'spot' + elif instType == 'margin': + marketType = 'spot' + else: + marketType = 'contract' + data = self.safe_list(message, 'data', []) + first = self.safe_dict(data, 0, {}) + category = self.safe_string_lower(first, 'category', instType) + isLinearSwap = (category == 'usdt-futures') + isInverseSwap = (category == 'coin-futures') + isUSDCFutures = (category == 'usdc-futures') + if self.orders is None: + limit = self.safe_integer(self.options, 'ordersLimit', 1000) + self.orders = ArrayCacheBySymbolById(limit) + self.triggerOrders = ArrayCacheBySymbolById(limit) + isTrigger = (channel == 'orders-algo') or (channel == 'ordersAlgo') + stored = self.triggerOrders if isTrigger else self.orders + messageHash = 'triggerOrder' if isTrigger else 'order' + marketSymbols: dict = {} + for i in range(0, len(data)): + order = data[i] + marketId = self.safe_string_2(order, 'instId', 'symbol', argInstId) + market = self.safe_market(marketId, None, None, marketType) + parsed = self.parse_ws_order(order, market) + stored.append(parsed) + symbol = parsed['symbol'] + marketSymbols[symbol] = True + keys = list(marketSymbols.keys()) + for i in range(0, len(keys)): + symbol = keys[i] + innerMessageHash = messageHash + ':' + symbol + if channel == 'orders-crossed': + innerMessageHash = innerMessageHash + ':cross' + elif channel == 'orders-isolated': + innerMessageHash = innerMessageHash + ':isolated' + client.resolve(stored, innerMessageHash) + client.resolve(stored, messageHash) + if isLinearSwap: + client.resolve(stored, 'order:linear') + if isInverseSwap: + client.resolve(stored, 'order:inverse') + if isUSDCFutures: + client.resolve(stored, 'order:usdcfutures') + + def parse_ws_order(self, order, market=None): + # + # spot + # + # { + # instId: 'EOSUSDT', + # orderId: '1171779081105780739', + # price: '0.81075', # limit price, field not present for market orders + # clientOid: 'a2330139-1d04-4d78-98be-07de3cfd1055', + # notional: '5.675250', # self is not cost! but notional + # newSize: '7.0000', # self is not cost! quanity(for limit order or market sell) or cost(for market buy order) + # size: '5.6752', # self is not cost, neither quanity, but notional! self field for "spot" can be ignored at all + # # Note: for limit order(even filled) we don't have cost value in response, only in market order + # orderType: 'limit', # limit, market + # force: 'gtc', + # side: 'buy', + # accBaseVolume: '0.0000', # in case of 'filled', self would be set(for limit orders, self is the only indicator of the amount filled) + # priceAvg: '0.00000', # in case of 'filled', self would be set + # status: 'live', # live, filled, partially_filled + # cTime: '1715099824215', + # uTime: '1715099824215', + # feeDetail: [], + # enterPointSource: 'API' + # #### trigger order has these additional fields: #### + # "triggerPrice": "35100", + # "price": "35100", # self is same price + # "executePrice": "35123", # self is limit price + # "triggerType": "fill_price", + # "planType": "amount", + # #### in case order had a partial fill: #### + # fillPrice: '35123', + # tradeId: '1171775539946528779', + # baseVolume: '7', # field present in market order + # fillTime: '1715098979937', + # fillFee: '-0.0069987', + # fillFeeCoin: 'BTC', + # tradeScope: 'T', + # } + # + # contract + # + # { + # accBaseVolume: '0', # total amount filled during lifetime for order + # cTime: '1715065875539', + # clientOid: '1171636690041344003', + # enterPointSource: 'API', + # feeDetail: [{ + # "feeCoin": "USDT", + # "fee": "-0.162003" + # }], + # force: 'gtc', + # instId: 'SEOSSUSDT', + # leverage: '10', + # marginCoin: 'USDT', + # marginMode: 'crossed', + # notionalUsd: '10.4468', + # orderId: '1171636690028761089', + # orderType: 'market', + # posMode: 'hedge_mode', # one_way_mode, hedge_mode + # posSide: 'short', # short, long, net + # price: '0', # zero for market order + # reduceOnly: 'no', + # side: 'sell', + # size: '13', # self is contracts amount + # status: 'live', # live, filled, cancelled + # tradeSide: 'open', + # uTime: '1715065875539' + # #### when filled order is incoming, these additional fields are present too: ### + # baseVolume: '9', # amount filled for the incoming update/trade + # accBaseVolume: '13', # i.e. 9 has been filled from 13 amount(self value is same as 'size') + # fillFee: '-0.0062712', + # fillFeeCoin: 'SUSDT', + # fillNotionalUsd: '10.452', + # fillPrice: '0.804', + # fillTime: '1715065875605', + # pnl: '0', + # priceAvg: '0.804', + # tradeId: '1171636690314407937', + # tradeScope: 'T', + # #### trigger order has these additional fields: + # "triggerPrice": "0.800000000", + # "price": "0.800000000", # <-- self is same price, actual limit-price is not present in initial response + # "triggerType": "mark_price", + # "triggerTime": "1715082796679", + # "planType": "pl", + # "actualSize": "0.000000000", + # "stopSurplusTriggerType": "fill_price", + # "stopLossTriggerType": "fill_price", + # } + # + # isolated and cross margin + # + # { + # enterPointSource: "web", + # feeDetail: [ + # { + # feeCoin: "AAVE", + # deduction: "no", + # totalDeductionFee: "0", + # totalFee: "-0.00010740", + # }, + # ], + # force: "gtc", + # orderType: "limit", + # price: "93.170000000", + # fillPrice: "93.170000000", + # baseSize: "0.110600000", # total amount of order + # quoteSize: "10.304602000", # total cost of order(independently if order is filled or pending) + # baseVolume: "0.107400000", # filled amount of order(during order's lifecycle, and not for self specific incoming update) + # fillTotalAmount: "10.006458000", # filled cost of order(during order's lifecycle, and not for self specific incoming update) + # side: "buy", + # status: "partially_filled", + # cTime: "1717875017306", + # clientOid: "b57afe789a06454e9c560a2aab7f7201", + # loanType: "auto-loan", + # orderId: "1183419084588060673", + # } + # + # uta + # + # { + # "category": "usdt-futures", + # "symbol": "BTCUSDT", + # "orderId": "xxx", + # "clientOid": "xxx", + # "price": "", + # "qty": "0.001", + # "amount": "1000", + # "holdMode": "hedge_mode", + # "holdSide": "long", + # "tradeSide": "open", + # "orderType": "market", + # "timeInForce": "gtc", + # "side": "buy", + # "marginMode": "crossed", + # "marginCoin": "USDT", + # "reduceOnly": "no", + # "cumExecQty": "0.001", + # "cumExecValue": "83.1315", + # "avgPrice": "83131.5", + # "totalProfit": "0", + # "orderStatus": "filled", + # "cancelReason": "", + # "leverage": "20", + # "feeDetail": [ + # { + # "feeCoin": "USDT", + # "fee": "0.0332526" + # } + # ], + # "createdTime": "1742367838101", + # "updatedTime": "1742367838115", + # "stpMode": "none" + # } + # + isSpot = not ('posMode' in order) + isMargin = ('loanType' in order) + category = self.safe_string_lower(order, 'category') + if category == 'spot': + isSpot = True + if category == 'margin': + isMargin = True + marketId = self.safe_string_2(order, 'instId', 'symbol') + market = self.safe_market(marketId, market) + timestamp = self.safe_integer_2(order, 'cTime', 'createdTime') + symbol = market['symbol'] + rawStatus = self.safe_string_2(order, 'status', 'orderStatus') + orderFee = self.safe_value(order, 'feeDetail', []) + fee = self.safe_value(orderFee, 0) + feeAmount = self.safe_string(fee, 'fee') + feeObject = None + if feeAmount is not None: + feeCurrency = self.safe_string(fee, 'feeCoin') + feeObject = { + 'cost': self.parse_number(Precise.string_abs(feeAmount)), + 'currency': self.safe_currency_code(feeCurrency), + } + triggerPrice = self.safe_number(order, 'triggerPrice') + isTriggerOrder = (triggerPrice is not None) + price = None + if not isTriggerOrder: + price = self.safe_number(order, 'price') + elif isSpot and isTriggerOrder: + # for spot trigger order, limit price is self + price = self.safe_number(order, 'executePrice') + avgPrice = self.omit_zero(self.safe_string_lower_n(order, ['priceAvg', 'fillPrice', 'avgPrice'])) + side = self.safe_string(order, 'side') + type = self.safe_string(order, 'orderType') + accBaseVolume = self.omit_zero(self.safe_string_2(order, 'accBaseVolume', 'cumExecQty')) + newSizeValue = self.omit_zero(self.safe_string_2(order, 'newSize', 'cumExecValue')) + isMarketOrder = (type == 'market') + isBuy = (side == 'buy') + totalAmount = None + filledAmount = None + cost = None + remaining = None + totalFilled = self.safe_string_2(order, 'accBaseVolume', 'cumExecQty') + if isSpot: + if isMargin: + totalAmount = self.safe_string_2(order, 'baseSize', 'qty') + totalFilled = self.safe_string_2(order, 'baseVolume', 'cumExecQty') + cost = self.safe_string_2(order, 'fillTotalAmount', 'cumExecValue') + else: + partialFillAmount = self.safe_string(order, 'baseVolume') + if partialFillAmount is not None: + filledAmount = partialFillAmount + else: + filledAmount = totalFilled + if isMarketOrder: + if isBuy: + totalAmount = accBaseVolume + cost = newSizeValue + else: + totalAmount = newSizeValue + # we don't have cost for market-sell order + else: + totalAmount = self.safe_string_2(order, 'newSize', 'qty') + # we don't have cost for limit order + else: + # baseVolume should not be used for "amount" for contracts ! + filledAmount = self.safe_string_2(order, 'baseVolume', 'cumExecQty') + totalAmount = self.safe_string_2(order, 'size', 'qty') + cost = self.safe_string_2(order, 'fillNotionalUsd', 'cumExecValue') + remaining = Precise.string_sub(totalAmount, totalFilled) + return self.safe_order({ + 'info': order, + 'symbol': symbol, + 'id': self.safe_string(order, 'orderId'), + 'clientOrderId': self.safe_string(order, 'clientOid'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': self.safe_integer_2(order, 'uTime', 'updatedTime'), + 'type': type, + 'timeInForce': self.safe_string_upper_2(order, 'force', 'timeInForce'), + 'postOnly': None, + 'side': side, + 'price': price, + 'triggerPrice': triggerPrice, + 'amount': totalAmount, + 'cost': cost, + 'average': avgPrice, + 'filled': filledAmount, + 'remaining': remaining, + 'status': self.parse_ws_order_status(rawStatus), + 'fee': feeObject, + 'trades': None, + }, market) + + def parse_ws_order_status(self, status): + statuses: dict = { + 'live': 'open', + 'partially_filled': 'open', + 'filled': 'closed', + 'cancelled': 'canceled', + 'not_trigger': 'open', + } + return self.safe_string(statuses, status, status) + + async def watch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + watches trades made by the user + + https://www.bitget.com/api-doc/contract/websocket/private/Fill-Channel + https://www.bitget.com/api-doc/uta/websocket/private/Fill-Channel + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + market = None + messageHash = 'myTrades' + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + messageHash = messageHash + ':' + symbol + type = None + type, params = self.handle_market_type_and_params('watchMyTrades', market, params) + instType = None + uta = None + uta, params = self.handle_option_and_params(params, 'watchMyTrades', 'uta', False) + if market is None and type == 'spot': + instType = 'SPOT' + else: + instType, params = self.get_inst_type(market, uta, params) + if uta: + instType = 'UTA' + subscriptionHash = 'fill:' + instType + args: dict = { + 'instType': instType, + } + topicOrChannel = 'topic' if uta else 'channel' + args[topicOrChannel] = 'fill' + if not uta: + args['instId'] = 'default' + else: + params = self.extend(params, {'uta': True}) + trades = await self.watch_private(messageHash, subscriptionHash, args, params) + if self.newUpdates: + limit = trades.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(trades, symbol, since, limit, True) + + def handle_my_trades(self, client: Client, message): + # + # spot + # { + # "action": "snapshot", + # "arg": { + # "instType": "SPOT", + # "channel": "fill", + # "instId": "default" + # }, + # "data": [ + # { + # "orderId": "1169142457356959747", + # "tradeId": "1169142457636958209", + # "symbol": "LTCUSDT", + # "orderType": "market", + # "side": "buy", + # "priceAvg": "81.069", + # "size": "0.074", + # "amount": "5.999106", + # "tradeScope": "taker", + # "feeDetail": [ + # { + # "feeCoin": "LTC", + # "deduction": "no", + # "totalDeductionFee": "0", + # "totalFee": "0.000074" + # } + # ], + # "cTime": "1714471204194", + # "uTime": "1714471204194" + # } + # ], + # "ts": 1714471204270 + # } + # swap + # { + # "action": "snapshot", + # "arg": { + # "instType": "USDT-FUTURES", + # "channel": "fill", + # "instId": "default" + # }, + # "data": [ + # { + # "orderId": "1169142761031114781", + # "tradeId": "1169142761312637004", + # "symbol": "LTCUSDT", + # "orderType": "market", + # "side": "buy", + # "price": "80.87", + # "baseVolume": "0.1", + # "quoteVolume": "8.087", + # "profit": "0", + # "tradeSide": "open", + # "posMode": "hedge_mode", + # "tradeScope": "taker", + # "feeDetail": [ + # { + # "feeCoin": "USDT", + # "deduction": "no", + # "totalDeductionFee": "0", + # "totalFee": "-0.0048522" + # } + # ], + # "cTime": "1714471276596", + # "uTime": "1714471276596" + # } + # ], + # "ts": 1714471276629 + # } + # + # uta + # + # { + # "data": [ + # { + # "symbol": "BTCUSDT", + # "orderType": "market", + # "updatedTime": "1736378720623", + # "side": "buy", + # "orderId": "1288888888888888888", + # "execPnl": "0", + # "feeDetail": [ + # { + # "feeCoin": "USDT", + # "fee": "0.569958" + # } + # ], + # "execTime": "1736378720623", + # "tradeScope": "taker", + # "tradeSide": "open", + # "execId": "1288888888888888888", + # "execLinkId": "1288888888888888888", + # "execPrice": "94993", + # "holdSide": "long", + # "execValue": "949.93", + # "category": "USDT-FUTURES", + # "execQty": "0.01", + # "clientOid": "1288888888888888889" + # } + # ], + # "arg": { + # "instType": "UTA", + # "topic": "fill" + # }, + # "action": "snapshot", + # "ts": 1733904123981 + # } + # + if self.myTrades is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + self.myTrades = ArrayCache(limit) + stored = self.myTrades + data = self.safe_list(message, 'data', []) + length = len(data) + messageHash = 'myTrades' + for i in range(0, length): + trade = data[i] + parsed = self.parse_ws_trade(trade) + stored.append(parsed) + symbol = parsed['symbol'] + symbolSpecificMessageHash = 'myTrades:' + symbol + client.resolve(stored, symbolSpecificMessageHash) + client.resolve(stored, messageHash) + + async def watch_balance(self, params={}) -> Balances: + """ + watch balance and get the amount of funds available for trading or funds locked in orders + + https://www.bitget.com/api-doc/spot/websocket/private/Account-Channel + https://www.bitget.com/api-doc/contract/websocket/private/Account-Channel + https://www.bitget.com/api-doc/margin/cross/websocket/private/Margin-Cross-Account-Assets + https://www.bitget.com/api-doc/margin/isolated/websocket/private/Margin-isolated-account-assets + https://www.bitget.com/api-doc/uta/websocket/private/Account-Channel + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: spot or contract if not provided self.options['defaultType'] is used + :param str [params.instType]: one of 'SPOT', 'MARGIN', 'USDT-FUTURES', 'USDC-FUTURES', 'COIN-FUTURES', 'SUSDT-FUTURES', 'SUSDC-FUTURES' or 'SCOIN-FUTURES' + :param str [params.marginMode]: 'isolated' or 'cross' for watching spot margin balances + :param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False + :returns dict: a `balance structure ` + """ + uta = None + uta, params = self.handle_option_and_params(params, 'watchBalance', 'uta', False) + type = None + type, params = self.handle_market_type_and_params('watchBalance', None, params) + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('watchBalance', params) + instType = None + channel = 'account' + if (type == 'swap') or (type == 'future'): + instType = 'USDT-FUTURES' + elif marginMode is not None: + instType = 'MARGIN' + if not uta: + if marginMode == 'isolated': + channel = 'account-isolated' + else: + channel = 'account-crossed' + elif not uta: + instType = 'SPOT' + instType, params = self.handle_option_and_params(params, 'watchBalance', 'instType', instType) + if uta: + instType = 'UTA' + args: dict = { + 'instType': instType, + } + topicOrChannel = 'topic' if uta else 'channel' + args[topicOrChannel] = channel + if not uta: + args['coin'] = 'default' + else: + params = self.extend(params, {'uta': True}) + messageHash = 'balance:' + instType.lower() + return await self.watch_private(messageHash, messageHash, args, params) + + def handle_balance(self, client: Client, message): + # + # spot + # + # { + # "action": "snapshot", + # "arg": {"instType": "SPOT", "channel": "account", "coin": "default"}, + # "data": [ + # { + # "coin": "USDT", + # "available": "19.1430952856087", + # "frozen": "7", + # "locked": "0", + # "limitAvailable": "0", + # "uTime": "1701931970487" + # }, + # ], + # "ts": 1701931970487 + # } + # + # swap + # + # { + # "action": "snapshot", + # "arg": {"instType": "USDT-FUTURES", "channel": "account", "coin": "default"}, + # "data": [ + # { + # "marginCoin": "USDT", + # "frozen": "5.36581500", + # "available": "26.14309528", + # "maxOpenPosAvailable": "20.77728028", + # "maxTransferOut": "20.77728028", + # "equity": "26.14309528", + # "usdtEquity": "26.143095285166" + # } + # ], + # "ts": 1701932570822 + # } + # + # margin + # + # { + # "action": "snapshot", + # "arg": {"instType": "MARGIN", "channel": "account-crossed", "coin": "default"}, + # "data": [ + # { + # "uTime": "1701933110544", + # "id": "1096916799926710272", + # "coin": "USDT", + # "available": "16.24309528", + # "borrow": "0.00000000", + # "frozen": "9.90000000", + # "interest": "0.00000000", + # "coupon": "0.00000000" + # } + # ], + # "ts": 1701933110544 + # } + # + # uta + # + # { + # "data": [{ + # "unrealisedPnL": "-10116.55", + # "totalEquity": "4976919.05", + # "positionMgnRatio": "0", + # "mmr": "408.08", + # "effEquity": "4847952.35", + # "imr": "17795.97", + # "mgnRatio": "0", + # "coin": [{ + # "debts": "0", + # "balance": "0.9992", + # "available": "0.9992", + # "borrow": "0", + # "locked": "0", + # "equity": "0.9992", + # "coin": "ETH", + # "usdValue": "2488.667472" + # }] + # }], + # "arg": { + # "instType": "UTA", + # "topic": "account" + # }, + # "action": "snapshot", + # "ts": 1740546523244 + # } + # + arg = self.safe_dict(message, 'arg', {}) + instType = self.safe_string_lower(arg, 'instType') + data = self.safe_value(message, 'data', []) + for i in range(0, len(data)): + rawBalance = data[i] + if instType == 'uta': + coins = self.safe_list(rawBalance, 'coin', []) + for j in range(0, len(coins)): + entry = coins[j] + currencyId = self.safe_string(entry, 'coin') + code = self.safe_currency_code(currencyId) + account = self.balance[code] if (code in self.balance) else self.account() + borrow = self.safe_string(entry, 'borrow') + debts = self.safe_string(entry, 'debts') + if (borrow is not None) or (debts is not None): + account['debt'] = Precise.string_add(borrow, debts) + account['free'] = self.safe_string(entry, 'available') + account['used'] = self.safe_string(entry, 'locked') + account['total'] = self.safe_string(entry, 'balance') + self.balance[code] = account + else: + currencyId = self.safe_string_2(rawBalance, 'coin', 'marginCoin') + code = self.safe_currency_code(currencyId) + account = self.balance[code] if (code in self.balance) else self.account() + borrow = self.safe_string(rawBalance, 'borrow') + if borrow is not None: + interest = self.safe_string(rawBalance, 'interest') + account['debt'] = Precise.string_add(borrow, interest) + freeQuery = 'maxTransferOut' if ('maxTransferOut' in rawBalance) else 'available' + account['free'] = self.safe_string(rawBalance, freeQuery) + account['total'] = self.safe_string(rawBalance, 'equity') + account['used'] = self.safe_string(rawBalance, 'frozen') + self.balance[code] = account + self.balance = self.safe_balance(self.balance) + messageHash = 'balance:' + instType + client.resolve(self.balance, messageHash) + + async def watch_public(self, messageHash, args, params={}): + uta = None + url = None + uta, params = self.handle_option_and_params(params, 'watchPublic', 'uta', False) + if uta: + url = self.urls['api']['ws']['utaPublic'] + else: + url = self.urls['api']['ws']['public'] + sandboxMode = self.safe_bool_2(self.options, 'sandboxMode', 'sandbox', False) + if sandboxMode: + instType = self.safe_string(args, 'instType') + if (instType != 'SCOIN-FUTURES') and (instType != 'SUSDT-FUTURES') and (instType != 'SUSDC-FUTURES'): + if uta: + url = self.urls['api']['demo']['utaPublic'] + else: + url = self.urls['api']['demo']['public'] + request: dict = { + 'op': 'subscribe', + 'args': [args], + } + message = self.extend(request, params) + return await self.watch(url, messageHash, message, messageHash) + + async def un_watch_public(self, messageHash, args, params={}): + uta = None + url = None + uta, params = self.handle_option_and_params(params, 'unWatchPublic', 'uta', False) + if uta: + url = self.urls['api']['ws']['utaPublic'] + else: + url = self.urls['api']['ws']['public'] + sandboxMode = self.safe_bool_2(self.options, 'sandboxMode', 'sandbox', False) + if sandboxMode: + instType = self.safe_string(args, 'instType') + if (instType != 'SCOIN-FUTURES') and (instType != 'SUSDT-FUTURES') and (instType != 'SUSDC-FUTURES'): + if uta: + url = self.urls['api']['demo']['utaPublic'] + else: + url = self.urls['api']['demo']['public'] + request: dict = { + 'op': 'unsubscribe', + 'args': [args], + } + message = self.extend(request, params) + return await self.watch(url, messageHash, message, messageHash) + + async def watch_public_multiple(self, messageHashes, argsArray, params={}): + uta = None + url = None + uta, params = self.handle_option_and_params(params, 'watchPublicMultiple', 'uta', False) + if uta: + url = self.urls['api']['ws']['utaPublic'] + else: + url = self.urls['api']['ws']['public'] + sandboxMode = self.safe_bool_2(self.options, 'sandboxMode', 'sandbox', False) + if sandboxMode: + argsArrayFirst = self.safe_dict(argsArray, 0, {}) + instType = self.safe_string(argsArrayFirst, 'instType') + if (instType != 'SCOIN-FUTURES') and (instType != 'SUSDT-FUTURES') and (instType != 'SUSDC-FUTURES'): + if uta: + url = self.urls['api']['demo']['utaPublic'] + else: + url = self.urls['api']['demo']['public'] + request: dict = { + 'op': 'subscribe', + 'args': argsArray, + } + message = self.extend(request, params) + return await self.watch_multiple(url, messageHashes, message, messageHashes) + + async def authenticate(self, params={}): + self.check_required_credentials() + url = self.safe_string(params, 'url') + client = self.client(url) + messageHash = 'authenticated' + future = client.reusableFuture(messageHash) + authenticated = self.safe_value(client.subscriptions, messageHash) + if authenticated is None: + timestamp = str(self.seconds()) + auth = timestamp + 'GET' + '/user/verify' + signature = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256, 'base64') + operation = 'login' + request: dict = { + 'op': operation, + 'args': [ + { + 'apiKey': self.apiKey, + 'passphrase': self.password, + 'timestamp': timestamp, + 'sign': signature, + }, + ], + } + message = self.extend(request, params) + self.watch(url, messageHash, message, messageHash) + return await future + + async def watch_private(self, messageHash, subscriptionHash, args, params={}): + uta = None + url = None + uta, params = self.handle_option_and_params(params, 'watchPrivate', 'uta', False) + if uta: + url = self.urls['api']['ws']['utaPrivate'] + else: + url = self.urls['api']['ws']['private'] + sandboxMode = self.safe_bool_2(self.options, 'sandboxMode', 'sandbox', False) + if sandboxMode: + instType = self.safe_string(args, 'instType') + if (instType != 'SCOIN-FUTURES') and (instType != 'SUSDT-FUTURES') and (instType != 'SUSDC-FUTURES'): + if uta: + url = self.urls['api']['demo']['utaPrivate'] + else: + url = self.urls['api']['demo']['private'] + await self.authenticate({'url': url}) + request: dict = { + 'op': 'subscribe', + 'args': [args], + } + message = self.extend(request, params) + return await self.watch(url, messageHash, message, subscriptionHash) + + def handle_authenticate(self, client: Client, message): + # + # {event: "login", code: 0} + # + messageHash = 'authenticated' + future = self.safe_value(client.futures, messageHash) + future.resolve(True) + + def handle_error_message(self, client: Client, message) -> Bool: + # + # {event: "error", code: 30015, msg: "Invalid sign"} + # + event = self.safe_string(message, 'event') + try: + if event == 'error': + code = self.safe_string(message, 'code') + feedback = self.id + ' ' + self.json(message) + self.throw_exactly_matched_exception(self.exceptions['ws']['exact'], code, feedback) + msg = self.safe_string(message, 'msg', '') + self.throw_broadly_matched_exception(self.exceptions['ws']['broad'], msg, feedback) + raise ExchangeError(feedback) + return False + except Exception as e: + if isinstance(e, AuthenticationError): + messageHash = 'authenticated' + client.reject(e, messageHash) + if messageHash in client.subscriptions: + del client.subscriptions[messageHash] + else: + # Note: if error happens on a subscribe event, user will have to close exchange to resubscribe. Issue #19041 + client.reject(e) + return True + + def handle_message(self, client: Client, message): + # + # { + # "action": "snapshot", + # "arg": {instType: 'SPOT', channel: "ticker", instId: "BTCUSDT"}, + # "data": [ + # { + # "instId": "BTCUSDT", + # "last": "21150.53", + # "open24h": "20759.65", + # "high24h": "21202.29", + # "low24h": "20518.82", + # "bestBid": "21150.500000", + # "bestAsk": "21150.600000", + # "baseVolume": "25402.1961", + # "quoteVolume": "530452554.2156", + # "ts": 1656408934044, + # "labeId": 0 + # } + # ] + # } + # pong message + # "pong" + # + # login + # + # {event: "login", code: 0} + # + # subscribe + # + # { + # "event": "subscribe", + # "arg": {instType: 'SPOT', channel: "account", instId: "default"} + # } + # unsubscribe + # { + # "op":"unsubscribe", + # "args":[ + # { + # "instType":"USDT-FUTURES", + # "channel":"ticker", + # "instId":"BTCUSDT" + # } + # ] + # } + # + # uta + # + # { + # "action": "snapshot", + # "arg": {"instType": "spot", topic: "ticker", symbol: "BTCUSDT"}, + # "data": [ + # { + # "highPrice24h": "120255.61", + # "lowPrice24h": "116145.88", + # "openPrice24h": "118919.38", + # "lastPrice": "119818.83", + # "turnover24h": "215859996.272276", + # "volume24h": "1819.756798", + # "bid1Price": "119811.26", + # "ask1Price": "119831.18", + # "bid1Size": "0.008732", + # "ask1Size": "0.004297", + # "price24hPcnt": "0.02002" + # } + # ], + # "ts": 1753230479687 + # } + # + # unsubscribe + # + # { + # "event": "unsubscribe", + # "arg": { + # "instType": "spot", + # "topic": "kline", + # "symbol": "BTCUSDT", + # "interval": "1m" + # } + # } + # + if self.handle_error_message(client, message): + return + content = self.safe_string(message, 'message') + if content == 'pong': + self.handle_pong(client, message) + return + if message == 'pong': + self.handle_pong(client, message) + return + event = self.safe_string(message, 'event') + if event == 'login': + self.handle_authenticate(client, message) + return + if event == 'subscribe': + self.handle_subscription_status(client, message) + return + if event == 'unsubscribe': + self.handle_un_subscription_status(client, message) + return + methods: dict = { + 'ticker': self.handle_ticker, + 'trade': self.handle_trades, + 'publicTrade': self.handle_trades, + 'fill': self.handle_my_trades, + 'order': self.handle_order, + 'orders': self.handle_order, + 'ordersAlgo': self.handle_order, + 'orders-algo': self.handle_order, + 'orders-crossed': self.handle_order, + 'orders-isolated': self.handle_order, + 'account': self.handle_balance, + 'position': self.handle_positions, + 'positions': self.handle_positions, + 'account-isolated': self.handle_balance, + 'account-crossed': self.handle_balance, + 'kline': self.handle_ohlcv, + } + arg = self.safe_value(message, 'arg', {}) + topic = self.safe_value_2(arg, 'channel', 'topic', '') + method = self.safe_value(methods, topic) + if method is not None: + method(client, message) + if topic.find('candle') >= 0: + self.handle_ohlcv(client, message) + if topic.find('books') >= 0: + self.handle_order_book(client, message) + + def ping(self, client: Client): + return 'ping' + + def handle_pong(self, client: Client, message): + client.lastPong = self.milliseconds() + return message + + def handle_subscription_status(self, client: Client, message): + # + # { + # "event": "subscribe", + # "arg": {instType: 'SPOT', channel: "account", instId: "default"} + # } + # + return message + + def handle_order_book_un_subscription(self, client: Client, message): + # + # {"event":"unsubscribe","arg":{"instType":"SPOT","channel":"books","instId":"BTCUSDT"}} + # + arg = self.safe_dict(message, 'arg', {}) + instType = self.safe_string_lower(arg, 'instType') + type = 'spot' if (instType == 'spot') else 'contract' + instId = self.safe_string(arg, 'instId') + market = self.safe_market(instId, None, None, type) + symbol = market['symbol'] + messageHash = 'unsubscribe:orderbook:' + market['symbol'] + subMessageHash = 'orderbook:' + symbol + if symbol in self.orderbooks: + del self.orderbooks[symbol] + if subMessageHash in client.subscriptions: + del client.subscriptions[subMessageHash] + if messageHash in client.subscriptions: + del client.subscriptions[messageHash] + error = UnsubscribeError(self.id + ' orderbook ' + symbol) + if subMessageHash in client.futures: + client.reject(error, subMessageHash) + client.resolve(True, messageHash) + + def handle_trades_un_subscription(self, client: Client, message): + # + # {"event":"unsubscribe","arg":{"instType":"SPOT","channel":"trade","instId":"BTCUSDT"}} + # + arg = self.safe_dict(message, 'arg', {}) + instType = self.safe_string_lower(arg, 'instType') + type = 'spot' if (instType == 'spot') else 'contract' + instId = self.safe_string_2(arg, 'instId', 'symbol') + market = self.safe_market(instId, None, None, type) + symbol = market['symbol'] + messageHash = 'unsubscribe:trade:' + market['symbol'] + subMessageHash = 'trade:' + symbol + if symbol in self.trades: + del self.trades[symbol] + if subMessageHash in client.subscriptions: + del client.subscriptions[subMessageHash] + if messageHash in client.subscriptions: + del client.subscriptions[messageHash] + error = UnsubscribeError(self.id + ' trades ' + symbol) + if subMessageHash in client.futures: + client.reject(error, subMessageHash) + client.resolve(True, messageHash) + + def handle_ticker_un_subscription(self, client: Client, message): + # + # {"event":"unsubscribe","arg":{"instType":"SPOT","channel":"trade","instId":"BTCUSDT"}} + # + arg = self.safe_dict(message, 'arg', {}) + instType = self.safe_string_lower(arg, 'instType') + type = 'spot' if (instType == 'spot') else 'contract' + instId = self.safe_string_2(arg, 'instId', 'symbol') + market = self.safe_market(instId, None, None, type) + symbol = market['symbol'] + messageHash = 'unsubscribe:ticker:' + market['symbol'] + subMessageHash = 'ticker:' + symbol + if symbol in self.tickers: + del self.tickers[symbol] + if subMessageHash in client.subscriptions: + del client.subscriptions[subMessageHash] + if messageHash in client.subscriptions: + del client.subscriptions[messageHash] + error = UnsubscribeError(self.id + ' ticker ' + symbol) + if subMessageHash in client.futures: + client.reject(error, subMessageHash) + client.resolve(True, messageHash) + + def handle_ohlcv_un_subscription(self, client: Client, message): + # + # {"event":"unsubscribe","arg":{"instType":"SPOT","channel":"candle1m","instId":"BTCUSDT"}} + # + # UTA + # + # {"event":"unsubscribe","arg":{"instType":"spot","topic":"kline","symbol":"BTCUSDT","interval":"1m"}} + # + arg = self.safe_dict(message, 'arg', {}) + instType = self.safe_string_lower(arg, 'instType') + type = 'spot' if (instType == 'spot') else 'contract' + instId = self.safe_string_2(arg, 'instId', 'symbol') + channel = self.safe_string_2(arg, 'channel', 'topic') + interval = self.safe_string(arg, 'interval') + isUta = None + if interval is None: + isUta = False + interval = channel.replace('candle', '') + else: + isUta = True + timeframes = self.safe_value(self.options, 'timeframes') + timeframe = self.find_timeframe(interval, timeframes) + market = self.safe_market(instId, None, None, type) + symbol = market['symbol'] + messageHash = None + subMessageHash = None + if isUta: + messageHash = 'unsubscribe:kline:' + symbol + subMessageHash = 'kline:' + symbol + else: + messageHash = 'unsubscribe:candles:' + timeframe + ':' + symbol + subMessageHash = 'candles:' + timeframe + ':' + symbol + if symbol in self.ohlcvs: + if timeframe in self.ohlcvs[symbol]: + del self.ohlcvs[symbol][timeframe] + self.clean_unsubscription(client, subMessageHash, messageHash) + + def handle_un_subscription_status(self, client: Client, message): + # + # { + # "op":"unsubscribe", + # "args":[ + # { + # "instType":"USDT-FUTURES", + # "channel":"ticker", + # "instId":"BTCUSDT" + # }, + # { + # "instType":"USDT-FUTURES", + # "channel":"candle1m", + # "instId":"BTCUSDT" + # } + # ] + # } + # or + # {"event":"unsubscribe","arg":{"instType":"SPOT","channel":"books","instId":"BTCUSDT"}} + # + argsList = self.safe_list(message, 'args') + if argsList is None: + argsList = [self.safe_dict(message, 'arg', {})] + for i in range(0, len(argsList)): + arg = argsList[i] + channel = self.safe_string_2(arg, 'channel', 'topic') + if channel == 'books': + # for now only unWatchOrderBook is supporteod + self.handle_order_book_un_subscription(client, message) + elif (channel == 'trade') or (channel == 'publicTrade'): + self.handle_trades_un_subscription(client, message) + elif channel == 'ticker': + self.handle_ticker_un_subscription(client, message) + elif channel.startswith('candle'): + self.handle_ohlcv_un_subscription(client, message) + elif channel.startswith('kline'): + self.handle_ohlcv_un_subscription(client, message) + return message diff --git a/ccxt/pro/bithumb.py b/ccxt/pro/bithumb.py new file mode 100644 index 0000000..be2d7b2 --- /dev/null +++ b/ccxt/pro/bithumb.py @@ -0,0 +1,622 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolById +from ccxt.base.types import Any, Balances, Bool, Int, Order, OrderBook, Str, Strings, Ticker, Tickers, Trade +from ccxt.async_support.base.ws.client import Client +from typing import List +from ccxt.base.errors import ExchangeError + + +class bithumb(ccxt.async_support.bithumb): + + def describe(self) -> Any: + return self.deep_extend(super(bithumb, self).describe(), { + 'has': { + 'ws': True, + 'watchBalance': True, + 'watchOrders': True, + 'watchTicker': True, + 'watchTickers': True, + 'watchTrades': True, + 'watchOrderBook': True, + 'watchOHLCV': False, + }, + 'urls': { + 'api': { + 'ws': { + 'public': 'wss://pubwss.bithumb.com/pub/ws', # v1.2.0 + 'publicV2': 'wss://ws-api.bithumb.com/websocket/v1', # v2.1.5 + 'privateV2': 'wss://ws-api.bithumb.com/websocket/v1/private', # v2.1.5 + }, + }, + }, + 'options': {}, + 'streaming': {}, + 'exceptions': {}, + }) + + async def watch_ticker(self, symbol: str, params={}) -> Ticker: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://apidocs.bithumb.com/v1.2.0/reference/%EB%B9%97%EC%8D%B8-%EA%B1%B0%EB%9E%98%EC%86%8C-%EC%A0%95%EB%B3%B4-%EC%88%98%EC%8B%A0 + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.channel]: the channel to subscribe to, tickers by default. Can be tickers, sprd-tickers, index-tickers, block-tickers + :returns dict: a `ticker structure ` + """ + url = self.urls['api']['ws']['public'] + await self.load_markets() + market = self.market(symbol) + messageHash = 'ticker:' + market['symbol'] + request: dict = { + 'type': 'ticker', + 'symbols': [market['base'] + '_' + market['quote']], + 'tickTypes': [self.safe_string(params, 'tickTypes', '24H')], + } + return await self.watch(url, messageHash, self.extend(request, params), messageHash) + + async def watch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list + + https://apidocs.bithumb.com/v1.2.0/reference/%EB%B9%97%EC%8D%B8-%EA%B1%B0%EB%9E%98%EC%86%8C-%EC%A0%95%EB%B3%B4-%EC%88%98%EC%8B%A0 + + :param str[] symbols: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + url = self.urls['api']['ws']['public'] + marketIds = [] + messageHashes = [] + symbols = self.market_symbols(symbols, None, False, True, True) + for i in range(0, len(symbols)): + symbol = symbols[i] + market = self.market(symbol) + marketIds.append(market['base'] + '_' + market['quote']) + messageHashes.append('ticker:' + market['symbol']) + request: dict = { + 'type': 'ticker', + 'symbols': marketIds, + 'tickTypes': [self.safe_string(params, 'tickTypes', '24H')], + } + message = self.extend(request, params) + newTicker = await self.watch_multiple(url, messageHashes, message, messageHashes) + if self.newUpdates: + result: dict = {} + result[newTicker['symbol']] = newTicker + return result + return self.filter_by_array(self.tickers, 'symbol', symbols) + + def handle_ticker(self, client: Client, message): + # + # { + # "type" : "ticker", + # "content" : { + # "symbol" : "BTC_KRW", # 통화코드 + # "tickType" : "24H", # 변동 기준시간- 30M, 1H, 12H, 24H, MID + # "date" : "20200129", # 일자 + # "time" : "121844", # 시간 + # "openPrice" : "2302", # 시가 + # "closePrice" : "2317", # 종가 + # "lowPrice" : "2272", # 저가 + # "highPrice" : "2344", # 고가 + # "value" : "2831915078.07065789", # 누적거래금액 + # "volume" : "1222314.51355788", # 누적거래량 + # "sellVolume" : "760129.34079004", # 매도누적거래량 + # "buyVolume" : "462185.17276784", # 매수누적거래량 + # "prevClosePrice" : "2326", # 전일종가 + # "chgRate" : "0.65", # 변동률 + # "chgAmt" : "15", # 변동금액 + # "volumePower" : "60.80" # 체결강도 + # } + # } + # + content = self.safe_dict(message, 'content', {}) + marketId = self.safe_string(content, 'symbol') + symbol = self.safe_symbol(marketId, None, '_') + ticker = self.parse_ws_ticker(content) + messageHash = 'ticker:' + symbol + self.tickers[symbol] = ticker + client.resolve(self.tickers[symbol], messageHash) + + def parse_ws_ticker(self, ticker, market=None): + # + # { + # "symbol" : "BTC_KRW", # 통화코드 + # "tickType" : "24H", # 변동 기준시간- 30M, 1H, 12H, 24H, MID + # "date" : "20200129", # 일자 + # "time" : "121844", # 시간 + # "openPrice" : "2302", # 시가 + # "closePrice" : "2317", # 종가 + # "lowPrice" : "2272", # 저가 + # "highPrice" : "2344", # 고가 + # "value" : "2831915078.07065789", # 누적거래금액 + # "volume" : "1222314.51355788", # 누적거래량 + # "sellVolume" : "760129.34079004", # 매도누적거래량 + # "buyVolume" : "462185.17276784", # 매수누적거래량 + # "prevClosePrice" : "2326", # 전일종가 + # "chgRate" : "0.65", # 변동률 + # "chgAmt" : "15", # 변동금액 + # "volumePower" : "60.80" # 체결강도 + # } + # + date = self.safe_string(ticker, 'date', '') + time = self.safe_string(ticker, 'time', '') + datetime = date[0:4] + '-' + date[4:6] + '-' + date[6:8] + 'T' + time[0:2] + ':' + time[2:4] + ':' + time[4:6] + marketId = self.safe_string(ticker, 'symbol') + return self.safe_ticker({ + 'symbol': self.safe_symbol(marketId, market, '_'), + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + 'high': self.safe_string(ticker, 'highPrice'), + 'low': self.safe_string(ticker, 'lowPrice'), + 'bid': None, + 'bidVolume': self.safe_string(ticker, 'buyVolume'), + 'ask': None, + 'askVolume': self.safe_string(ticker, 'sellVolume'), + 'vwap': None, + 'open': self.safe_string(ticker, 'openPrice'), + 'close': self.safe_string(ticker, 'closePrice'), + 'last': None, + 'previousClose': self.safe_string(ticker, 'prevClosePrice'), + 'change': self.safe_string(ticker, 'chgAmt'), + 'percentage': self.safe_string(ticker, 'chgRate'), + 'average': None, + 'baseVolume': self.safe_string(ticker, 'volume'), + 'quoteVolume': self.safe_string(ticker, 'value'), + 'info': ticker, + }, market) + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + + https://apidocs.bithumb.com/v1.2.0/reference/%EB%B9%97%EC%8D%B8-%EA%B1%B0%EB%9E%98%EC%86%8C-%EC%A0%95%EB%B3%B4-%EC%88%98%EC%8B%A0 + + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + url = self.urls['api']['ws']['public'] + market = self.market(symbol) + symbol = market['symbol'] + messageHash = 'orderbook' + ':' + symbol + request: dict = { + 'type': 'orderbookdepth', + 'symbols': [market['base'] + '_' + market['quote']], + } + orderbook = await self.watch(url, messageHash, self.extend(request, params), messageHash) + return orderbook.limit() + + def handle_order_book(self, client: Client, message): + # + # { + # "type" : "orderbookdepth", + # "content" : { + # "list" : [ + # { + # "symbol" : "BTC_KRW", + # "orderType" : "ask", # 주문타입 – bid / ask + # "price" : "10593000", # 호가 + # "quantity" : "1.11223318", # 잔량 + # "total" : "3" # 건수 + # }, + # {"symbol" : "BTC_KRW", "orderType" : "ask", "price" : "10596000", "quantity" : "0.5495", "total" : "8"}, + # {"symbol" : "BTC_KRW", "orderType" : "ask", "price" : "10598000", "quantity" : "18.2085", "total" : "10"}, + # {"symbol" : "BTC_KRW", "orderType" : "bid", "price" : "10532000", "quantity" : "0", "total" : "0"}, + # {"symbol" : "BTC_KRW", "orderType" : "bid", "price" : "10572000", "quantity" : "2.3324", "total" : "4"}, + # {"symbol" : "BTC_KRW", "orderType" : "bid", "price" : "10571000", "quantity" : "1.469", "total" : "3"}, + # {"symbol" : "BTC_KRW", "orderType" : "bid", "price" : "10569000", "quantity" : "0.5152", "total" : "2"} + # ], + # "datetime":1580268255864325 # 일시 + # } + # } + # + content = self.safe_dict(message, 'content', {}) + list = self.safe_list(content, 'list', []) + first = self.safe_dict(list, 0, {}) + marketId = self.safe_string(first, 'symbol') + symbol = self.safe_symbol(marketId, None, '_') + timestampStr = self.safe_string(content, 'datetime') + timestamp = self.parse_to_int(timestampStr[0:13]) + if not (symbol in self.orderbooks): + ob = self.order_book() + ob['symbol'] = symbol + self.orderbooks[symbol] = ob + orderbook = self.orderbooks[symbol] + self.handle_deltas(orderbook, list) + orderbook['timestamp'] = timestamp + orderbook['datetime'] = self.iso8601(timestamp) + messageHash = 'orderbook' + ':' + symbol + client.resolve(orderbook, messageHash) + + def handle_delta(self, orderbook, delta): + # + # { + # symbol: "ETH_BTC", + # orderType: "bid", + # price: "0.07349517", + # quantity: "0", + # total: "0", + # } + # + sideId = self.safe_string(delta, 'orderType') + side = 'bids' if (sideId == 'bid') else 'asks' + bidAsk = self.parse_bid_ask(delta, 'price', 'quantity') + orderbookSide = orderbook[side] + orderbookSide.storeArray(bidAsk) + + def handle_deltas(self, orderbook, deltas): + for i in range(0, len(deltas)): + self.handle_delta(orderbook, deltas[i]) + + async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://apidocs.bithumb.com/v1.2.0/reference/%EB%B9%97%EC%8D%B8-%EA%B1%B0%EB%9E%98%EC%86%8C-%EC%A0%95%EB%B3%B4-%EC%88%98%EC%8B%A0 + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + url = self.urls['api']['ws']['public'] + market = self.market(symbol) + symbol = market['symbol'] + messageHash = 'trade:' + symbol + request: dict = { + 'type': 'transaction', + 'symbols': [market['base'] + '_' + market['quote']], + } + trades = await self.watch(url, messageHash, self.extend(request, params), messageHash) + if self.newUpdates: + limit = trades.getLimit(symbol, limit) + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + def handle_trades(self, client, message): + # + # { + # "type" : "transaction", + # "content" : { + # "list" : [ + # { + # "symbol" : "BTC_KRW", + # "buySellGb" : "1", + # "contPrice" : "10579000", + # "contQty" : "0.01", + # "contAmt" : "105790.00", + # "contDtm" : "2020-01-29 12:24:18.830039", + # "updn" : "dn" + # } + # ] + # } + # } + # + content = self.safe_dict(message, 'content', {}) + rawTrades = self.safe_list(content, 'list', []) + for i in range(0, len(rawTrades)): + rawTrade = rawTrades[i] + marketId = self.safe_string(rawTrade, 'symbol') + symbol = self.safe_symbol(marketId, None, '_') + if not (symbol in self.trades): + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + stored = ArrayCache(limit) + self.trades[symbol] = stored + trades = self.trades[symbol] + parsed = self.parse_ws_trade(rawTrade) + trades.append(parsed) + messageHash = 'trade' + ':' + symbol + client.resolve(trades, messageHash) + + def parse_ws_trade(self, trade, market=None): + # + # { + # "symbol" : "BTC_KRW", + # "buySellGb" : "1", + # "contPrice" : "10579000", + # "contQty" : "0.01", + # "contAmt" : "105790.00", + # "contDtm" : "2020-01-29 12:24:18.830038", + # "updn" : "dn" + # } + # + marketId = self.safe_string(trade, 'symbol') + datetime = self.safe_string(trade, 'contDtm') + # that date is not UTC iso8601, but exchange's local time, -9hr difference + timestamp = self.parse8601(datetime) - 32400000 + sideId = self.safe_string(trade, 'buySellGb') + return self.safe_trade({ + 'id': None, + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': self.safe_symbol(marketId, market, '_'), + 'order': None, + 'type': None, + 'side': 'buy' if (sideId == '1') else 'sell', + 'takerOrMaker': None, + 'price': self.safe_string(trade, 'contPrice'), + 'amount': self.safe_string(trade, 'contQty'), + 'cost': self.safe_string(trade, 'contAmt'), + 'fee': None, + }, market) + + def handle_error_message(self, client: Client, message) -> Bool: + # + # { + # "status" : "5100", + # "resmsg" : "Invalid Filter Syntax" + # } + # + if not ('status' in message): + return True + errorCode = self.safe_string(message, 'status') + try: + if errorCode != '0000': + msg = self.safe_string(message, 'resmsg') + raise ExchangeError(self.id + ' ' + msg) + return True + except Exception as e: + client.reject(e) + return True + + async def watch_balance(self, params={}) -> Balances: + """ + watch balance and get the amount of funds available for trading or funds locked in orders + + https://apidocs.bithumb.com/v2.1.5/reference/%EB%82%B4-%EC%9E%90%EC%82%B0-myasset + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + await self.authenticate() + url = self.urls['api']['ws']['privateV2'] + messageHash = 'myAsset' + request = [ + {'ticket': 'ccxt'}, + {'type': messageHash}, + ] + balance = await self.watch(url, messageHash, request, messageHash) + return balance + + def handle_balance(self, client: Client, message): + # + # { + # "type": "myAsset", + # "assets": [ + # { + # "currency": "KRW", + # "balance": "2061832.35", + # "locked": "3824127.3" + # } + # ], + # "asset_timestamp": 1727052537592, + # "timestamp": 1727052537687, + # "stream_type": "REALTIME" + # } + # + messageHash = 'myAsset' + assets = self.safe_list(message, 'assets', []) + if self.balance is None: + self.balance = {} + for i in range(0, len(assets)): + asset = assets[i] + currencyId = self.safe_string(asset, 'currency') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(asset, 'balance') + account['used'] = self.safe_string(asset, 'locked') + self.balance[code] = account + self.balance['info'] = message + timestamp = self.safe_integer(message, 'timestamp') + self.balance['timestamp'] = timestamp + self.balance['datetime'] = self.iso8601(timestamp) + self.balance = self.safe_balance(self.balance) + client.resolve(self.balance, messageHash) + + async def authenticate(self, params={}): + self.check_required_credentials() + wsOptions: dict = self.safe_dict(self.options, 'ws', {}) + authenticated = self.safe_string(wsOptions, 'token') + if authenticated is None: + payload: dict = { + 'access_key': self.apiKey, + 'nonce': self.uuid(), + 'timestamp': self.milliseconds(), + } + jwtToken = self.jwt(payload, self.encode(self.secret), 'sha256') + wsOptions['token'] = jwtToken + wsOptions['options'] = { + 'headers': { + 'authorization': 'Bearer ' + jwtToken, + }, + } + self.options['ws'] = wsOptions + url = self.urls['api']['ws']['privateV2'] + client = self.client(url) + return client + + async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + watches information on multiple orders made by the user + + https://apidocs.bithumb.com/v2.1.5/reference/%EB%82%B4-%EC%A3%BC%EB%AC%B8-%EB%B0%8F-%EC%B2%B4%EA%B2%B0-myorder + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str[] [params.codes]: market codes to filter orders + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + await self.authenticate() + url = self.urls['api']['ws']['privateV2'] + messageHash = 'myOrder' + codes = self.safe_list(params, 'codes', []) + request = [ + {'ticket': 'ccxt'}, + {'type': messageHash, 'codes': codes}, + ] + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + messageHash = messageHash + ':' + symbol + orders = await self.watch(url, messageHash, request, messageHash) + if self.newUpdates: + limit = orders.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(orders, symbol, since, limit, True) + + def handle_orders(self, client: Client, message): + # + # { + # "type": "myOrder", + # "code": "KRW-BTC", + # "uuid": "C0101000000001818113", + # "ask_bid": "BID", + # "order_type": "limit", + # "state": "trade", + # "trade_uuid": "C0101000000001744207", + # "price": 1927000, + # "volume": 0.4697, + # "remaining_volume": 0.0803, + # "executed_volume": 0.4697, + # "trades_count": 1, + # "reserved_fee": 0, + # "remaining_fee": 0, + # "paid_fee": 0, + # "executed_funds": 905111.9000, + # "trade_timestamp": 1727052318148, + # "order_timestamp": 1727052318074, + # "timestamp": 1727052318369, + # "stream_type": "REALTIME" + # } + # + messageHash = 'myOrder' + parsed = self.parse_ws_order(message) + symbol = self.safe_string(parsed, 'symbol') + # orderId = self.safe_string(parsed, 'id') + if self.orders is None: + limit = self.safe_integer(self.options, 'ordersLimit', 1000) + self.orders = ArrayCacheBySymbolById(limit) + cachedOrders = self.orders + cachedOrders.append(parsed) + client.resolve(cachedOrders, messageHash) + symbolSpecificMessageHash = messageHash + ':' + symbol + client.resolve(cachedOrders, symbolSpecificMessageHash) + + def parse_ws_order(self, order, market=None): + # + # { + # "type": "myOrder", + # "code": "KRW-BTC", + # "uuid": "C0101000000001818113", + # "ask_bid": "BID", + # "order_type": "limit", + # "state": "trade", + # "trade_uuid": "C0101000000001744207", + # "price": 1927000, + # "volume": 0.4697, + # "remaining_volume": 0.0803, + # "executed_volume": 0.4697, + # "trades_count": 1, + # "reserved_fee": 0, + # "remaining_fee": 0, + # "paid_fee": 0, + # "executed_funds": 905111.9000, + # "trade_timestamp": 1727052318148, + # "order_timestamp": 1727052318074, + # "timestamp": 1727052318369, + # "stream_type": "REALTIME" + # } + # + marketId = self.safe_string(order, 'code') + symbol = self.safe_symbol(marketId, market, '-') + timestamp = self.safe_integer(order, 'order_timestamp') + sideId = self.safe_string(order, 'ask_bid') + side = ('buy') if (sideId == 'BID') else ('sell') + typeId = self.safe_string(order, 'order_type') + type = None + if typeId == 'limit': + type = 'limit' + elif typeId == 'price': + type = 'market' + elif typeId == 'market': + type = 'market' + stateId = self.safe_string(order, 'state') + status = None + if stateId == 'wait': + status = 'open' + elif stateId == 'trade': + status = 'open' + elif stateId == 'done': + status = 'closed' + elif stateId == 'cancel': + status = 'canceled' + price = self.safe_string(order, 'price') + amount = self.safe_string(order, 'volume') + remaining = self.safe_string(order, 'remaining_volume') + filled = self.safe_string(order, 'executed_volume') + cost = self.safe_string(order, 'executed_funds') + feeCost = self.safe_string(order, 'paid_fee') + fee = None + if feeCost is not None: + marketForFee = self.safe_market(marketId, market) + feeCurrency = self.safe_string(marketForFee, 'quote') + fee = { + 'cost': feeCost, + 'currency': feeCurrency, + } + return self.safe_order({ + 'info': order, + 'id': self.safe_string(order, 'uuid'), + 'clientOrderId': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': self.safe_integer(order, 'trade_timestamp'), + 'symbol': symbol, + 'type': type, + 'timeInForce': None, + 'postOnly': None, + 'side': side, + 'price': price, + 'stopPrice': None, + 'triggerPrice': None, + 'amount': amount, + 'cost': cost, + 'average': None, + 'filled': filled, + 'remaining': remaining, + 'status': status, + 'fee': fee, + 'trades': None, + }, market) + + def handle_message(self, client: Client, message): + if not self.handle_error_message(client, message): + return + topic = self.safe_string(message, 'type') + if topic is not None: + methods: dict = { + 'ticker': self.handle_ticker, + 'orderbookdepth': self.handle_order_book, + 'transaction': self.handle_trades, + 'myAsset': self.handle_balance, + 'myOrder': self.handle_orders, + } + method = self.safe_value(methods, topic) + if method is not None: + method(client, message) diff --git a/ccxt/pro/bitmart.py b/ccxt/pro/bitmart.py new file mode 100644 index 0000000..81ad0da --- /dev/null +++ b/ccxt/pro/bitmart.py @@ -0,0 +1,1578 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolById, ArrayCacheBySymbolBySide, ArrayCacheByTimestamp +from ccxt.async_support.base.ws.order_book_side import Asks, Bids +import hashlib +from ccxt.base.types import Any, Balances, Bool, Int, Market, Order, OrderBook, Position, Str, Strings, Ticker, Tickers, Trade +from ccxt.async_support.base.ws.client import Client +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import NotSupported + + +class bitmart(ccxt.async_support.bitmart): + + def describe(self) -> Any: + return self.deep_extend(super(bitmart, self).describe(), { + 'has': { + 'createOrderWs': False, + 'editOrderWs': False, + 'fetchOpenOrdersWs': False, + 'fetchOrderWs': False, + 'cancelOrderWs': False, + 'cancelOrdersWs': False, + 'cancelAllOrdersWs': False, + 'ws': True, + 'watchBalance': True, + 'watchTicker': True, + 'watchTickers': True, + 'watchBidsAsks': True, + 'watchOrderBook': True, + 'watchOrderBookForSymbols': True, + 'watchOrders': True, + 'watchTrades': True, + 'watchTradesForSymbols': True, + 'watchOHLCV': True, + 'watchPosition': 'emulated', + 'watchPositions': True, + }, + 'urls': { + 'api': { + 'ws': { + 'spot': { + 'public': 'wss://ws-manager-compress.{hostname}/api?protocol=1.1', + 'private': 'wss://ws-manager-compress.{hostname}/user?protocol=1.1', + }, + 'swap': { + 'public': 'wss://openapi-ws-v2.{hostname}/api?protocol=1.1', + 'private': 'wss://openapi-ws-v2.{hostname}/user?protocol=1.1', + }, + }, + }, + }, + 'options': { + 'defaultType': 'spot', + 'watchBalance': { + 'fetchBalanceSnapshot': True, # or False + 'awaitBalanceSnapshot': False, # whether to wait for the balance snapshot before providing updates + }, + # + # orderbook channels can have: + # - 'depth5', 'depth20', 'depth50' # these endpoints emit full Orderbooks once in every 500ms + # - 'depth/increase100' # self endpoint is preferred, because it emits once in 100ms. however, when self value is chosen, it only affects spot-market, but contracts markets automatically `depth50` will be being used + 'watchOrderBook': { + 'depth': 'depth/increase100', + }, + 'watchOrderBookForSymbols': { + 'depth': 'depth/increase100', + }, + 'watchTrades': { + 'ignoreDuplicates': True, + }, + 'ws': { + 'inflate': True, + }, + 'timeframes': { + '1m': '1m', + '3m': '3m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '45m': '45m', + '1h': '1H', + '2h': '2H', + '3h': '3H', + '4h': '4H', + '1d': '1D', + '1w': '1W', + '1M': '1M', + }, + }, + 'streaming': { + 'keepAlive': 15000, + }, + }) + + async def subscribe(self, channel, symbol, type, params={}): + market = self.market(symbol) + url = self.implode_hostname(self.urls['api']['ws'][type]['public']) + request = {} + messageHash = None + if type == 'spot': + messageHash = 'spot/' + channel + ':' + market['id'] + request = { + 'op': 'subscribe', + 'args': [messageHash], + } + else: + messageHash = 'futures/' + channel + ':' + market['id'] + speed = self.safe_string(params, 'speed') + if speed is not None: + params = self.omit(params, 'speed') + messageHash += ':' + speed + request = { + 'action': 'subscribe', + 'args': [messageHash], + } + return await self.watch(url, messageHash, self.deep_extend(request, params), messageHash) + + async def subscribe_multiple(self, channel: str, type: str, symbols: Strings = None, params={}): + symbols = self.market_symbols(symbols, type, False, True) + url = self.implode_hostname(self.urls['api']['ws'][type]['public']) + channelType = 'spot' if (type == 'spot') else 'futures' + actionType = 'op' if (type == 'spot') else 'action' + rawSubscriptions = [] + messageHashes = [] + for i in range(0, len(symbols)): + market = self.market(symbols[i]) + message = channelType + '/' + channel + ':' + market['id'] + rawSubscriptions.append(message) + messageHashes.append(channel + ':' + market['symbol']) + # exclusion, futures "tickers" need one generic request for all symbols + # if (type != 'spot') and (channel == 'ticker'): + # rawSubscriptions = [channelType + '/' + channel] + # } + # Exchange update from 2025-02-11 supports subscription by trading pair for swap + request: dict = { + 'args': rawSubscriptions, + } + request[actionType] = 'subscribe' + return await self.watch_multiple(url, messageHashes, self.deep_extend(request, params), rawSubscriptions) + + async def watch_balance(self, params={}) -> Balances: + """ + + https://developer-pro.bitmart.com/en/spot/#private-balance-change + https://developer-pro.bitmart.com/en/futuresv2/#private-assets-channel + + watch balance and get the amount of funds available for trading or funds locked in orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + type = 'spot' + type, params = self.handle_market_type_and_params('watchBalance', None, params) + await self.authenticate(type, params) + request = {} + if type == 'spot': + request = { + 'op': 'subscribe', + 'args': ['spot/user/balance:BALANCE_UPDATE'], + } + else: + request = { + 'action': 'subscribe', + 'args': ['futures/asset:USDT', 'futures/asset:BTC', 'futures/asset:ETH'], + } + messageHash = 'balance:' + type + url = self.implode_hostname(self.urls['api']['ws'][type]['private']) + client = self.client(url) + self.set_balance_cache(client, type, messageHash) + fetchBalanceSnapshot = None + awaitBalanceSnapshot = None + fetchBalanceSnapshot, params = self.handle_option_and_params(self.options, 'watchBalance', 'fetchBalanceSnapshot', True) + awaitBalanceSnapshot, params = self.handle_option_and_params(self.options, 'watchBalance', 'awaitBalanceSnapshot', False) + if fetchBalanceSnapshot and awaitBalanceSnapshot: + await client.future(type + ':fetchBalanceSnapshot') + return await self.watch(url, messageHash, self.deep_extend(request, params), messageHash) + + def set_balance_cache(self, client: Client, type, subscribeHash): + if subscribeHash in client.subscriptions: + return + options = self.safe_value(self.options, 'watchBalance') + snapshot = self.safe_bool(options, 'fetchBalanceSnapshot', True) + if snapshot: + messageHash = type + ':' + 'fetchBalanceSnapshot' + if not (messageHash in client.futures): + client.future(messageHash) + self.spawn(self.load_balance_snapshot, client, messageHash, type) + self.balance[type] = {} + # without self comment, transpilation breaks for some reason... + + async def load_balance_snapshot(self, client, messageHash, type): + response = await self.fetch_balance({'type': type}) + self.balance[type] = self.extend(response, self.safe_value(self.balance, type, {})) + # don't remove the future from the .futures cache + future = client.futures[messageHash] + future.resolve() + client.resolve(self.balance[type], 'balance:' + type) + + def handle_balance(self, client: Client, message): + # + # spot + # { + # "data":[ + # { + # "balance_details":[ + # { + # "av_bal":"0.206000000000000000000000000000", + # "ccy":"LTC", + # "fz_bal":"0.100000000000000000000000000000" + # } + # ], + # "event_time":"1701632345416", + # "event_type":"TRANSACTION_COMPLETED" + # } + # ], + # "table":"spot/user/balance" + # } + # swap + # { + # group: 'futures/asset:USDT', + # data: { + # currency: 'USDT', + # available_balance: '37.19688649135', + # position_deposit: '0.788687546', + # frozen_balance: '0' + # } + # } + # + channel = self.safe_string_2(message, 'table', 'group') + data = self.safe_value(message, 'data') + if data is None: + return + isSpot = (channel.find('spot') >= 0) + type = 'spot' if isSpot else 'swap' + self.balance[type]['info'] = message + if isSpot: + if not isinstance(data, list): + return + for i in range(0, len(data)): + timestamp = self.safe_integer(message, 'event_time') + self.balance[type]['timestamp'] = timestamp + self.balance[type]['datetime'] = self.iso8601(timestamp) + balanceDetails = self.safe_value(data[i], 'balance_details', []) + for ii in range(0, len(balanceDetails)): + rawBalance = balanceDetails[i] + account = self.account() + currencyId = self.safe_string(rawBalance, 'ccy') + code = self.safe_currency_code(currencyId) + account['free'] = self.safe_string(rawBalance, 'av_bal') + account['used'] = self.safe_string(rawBalance, 'fz_bal') + self.balance[type][code] = account + else: + currencyId = self.safe_string(data, 'currency') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(data, 'available_balance') + account['used'] = self.safe_string(data, 'frozen_balance') + self.balance[type][code] = account + self.balance[type] = self.safe_balance(self.balance[type]) + messageHash = 'balance:' + type + client.resolve(self.balance[type], messageHash) + + async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + + https://developer-pro.bitmart.com/en/spot/#public-trade-channel + https://developer-pro.bitmart.com/en/futuresv2/#public-trade-channel + + get the list of most recent trades for a particular symbol + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + return await self.watch_trades_for_symbols([symbol], since, limit, params) + + async def watch_trades_for_symbols(self, symbols: List[str], since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + + https://developer-pro.bitmart.com/en/spot/#public-trade-channel + + get the list of most recent trades for a list of symbols + :param str[] symbols: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + marketType = None + symbols, marketType, params = self.get_params_for_multiple_sub('watchTradesForSymbols', symbols, limit, params) + channelName = 'trade' + trades = await self.subscribe_multiple(channelName, marketType, symbols, params) + if self.newUpdates: + first = self.safe_dict(trades, 0) + tradeSymbol = self.safe_string(first, 'symbol') + limit = trades.getLimit(tradeSymbol, limit) + result = self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + if self.handle_option('watchTrades', 'ignoreDuplicates', True): + filtered = self.remove_repeated_trades_from_array(result) + filtered = self.sort_by(filtered, 'timestamp') + return filtered + return result + + def get_params_for_multiple_sub(self, methodName: str, symbols: List[str], limit: Int = None, params={}): + symbols = self.market_symbols(symbols, None, False, True) + length = len(symbols) + if length > 20: + raise NotSupported(self.id + ' ' + methodName + '() accepts a maximum of 20 symbols in one request') + market = self.market(symbols[0]) + marketType = None + marketType, params = self.handle_market_type_and_params(methodName, market, params) + return [symbols, marketType, params] + + async def watch_ticker(self, symbol: str, params={}) -> Ticker: + """ + + https://developer-pro.bitmart.com/en/spot/#public-ticker-channel + https://developer-pro.bitmart.com/en/futuresv2/#public-ticker-channel + + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbol = self.symbol(symbol) + tickers = await self.watch_tickers([symbol], params) + return tickers[symbol] + + async def watch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + + https://developer-pro.bitmart.com/en/spot/#public-ticker-channel + https://developer-pro.bitmart.com/en/futuresv2/#public-ticker-channel + + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list + :param str[] symbols: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.get_market_from_symbols(symbols) + marketType = None + marketType, params = self.handle_market_type_and_params('watchTickers', market, params) + ticker = await self.subscribe_multiple('ticker', marketType, symbols, params) + if self.newUpdates: + tickers: dict = {} + tickers[ticker['symbol']] = ticker + return tickers + return self.filter_by_array(self.tickers, 'symbol', symbols) + + async def watch_bids_asks(self, symbols: Strings = None, params={}) -> Tickers: + """ + + https://developer-pro.bitmart.com/en/spot/#public-ticker-channel + https://developer-pro.bitmart.com/en/futuresv2/#public-ticker-channel + + watches best bid & ask for symbols + :param str[] symbols: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, False) + firstMarket = self.get_market_from_symbols(symbols) + marketType = None + marketType, params = self.handle_market_type_and_params('watchBidsAsks', firstMarket, params) + url = self.implode_hostname(self.urls['api']['ws'][marketType]['public']) + channelType = 'spot' if (marketType == 'spot') else 'futures' + actionType = 'op' if (marketType == 'spot') else 'action' + rawSubscriptions = [] + messageHashes = [] + for i in range(0, len(symbols)): + market = self.market(symbols[i]) + rawSubscriptions.append(channelType + '/ticker:' + market['id']) + messageHashes.append('bidask:' + symbols[i]) + if marketType != 'spot': + rawSubscriptions = [channelType + '/ticker'] + request: dict = { + 'args': rawSubscriptions, + } + request[actionType] = 'subscribe' + newTickers = await self.watch_multiple(url, messageHashes, request, rawSubscriptions) + if self.newUpdates: + tickers: dict = {} + tickers[newTickers['symbol']] = newTickers + return tickers + return self.filter_by_array(self.bidsasks, 'symbol', symbols) + + def handle_bid_ask(self, client: Client, message): + table = self.safe_string(message, 'table') + isSpot = (table is not None) + rawTickers = [] + if isSpot: + rawTickers = self.safe_list(message, 'data', []) + else: + rawTickers = [self.safe_value(message, 'data', {})] + if not len(rawTickers): + return + for i in range(0, len(rawTickers)): + ticker = self.parse_ws_bid_ask(rawTickers[i]) + symbol = ticker['symbol'] + self.bidsasks[symbol] = ticker + messageHash = 'bidask:' + symbol + client.resolve(ticker, messageHash) + + def parse_ws_bid_ask(self, ticker, market=None): + marketId = self.safe_string(ticker, 'symbol') + market = self.safe_market(marketId, market) + symbol = self.safe_string(market, 'symbol') + timestamp = self.safe_integer(ticker, 'ms_t') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'ask': self.safe_string_2(ticker, 'ask_px', 'ask_price'), + 'askVolume': self.safe_string_2(ticker, 'ask_sz', 'ask_vol'), + 'bid': self.safe_string_2(ticker, 'bid_px', 'bid_price'), + 'bidVolume': self.safe_string_2(ticker, 'bid_sz', 'bid_vol'), + 'info': ticker, + }, market) + + async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + watches information on multiple orders made by the user + + https://developer-pro.bitmart.com/en/spot/#private-order-progress + https://developer-pro.bitmart.com/en/futuresv2/#private-order-channel + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + market = None + messageHash = 'orders' + if symbol is not None: + symbol = self.symbol(symbol) + market = self.market(symbol) + messageHash = 'orders::' + symbol + type = 'spot' + type, params = self.handle_market_type_and_params('watchOrders', market, params) + await self.authenticate(type, params) + request = None + if type == 'spot': + argsRequest = 'spot/user/order:' + if symbol is not None: + argsRequest += market['id'] + else: + argsRequest = 'spot/user/orders:ALL_SYMBOLS' + request = { + 'op': 'subscribe', + 'args': [argsRequest], + } + else: + request = { + 'action': 'subscribe', + 'args': ['futures/order'], + } + url = self.implode_hostname(self.urls['api']['ws'][type]['private']) + newOrders = await self.watch(url, messageHash, self.deep_extend(request, params), messageHash) + if self.newUpdates: + return newOrders + return self.filter_by_symbol_since_limit(self.orders, symbol, since, limit, True) + + def handle_orders(self, client: Client, message): + # + # spot + # { + # "data":[ + # { + # "symbol": "LTC_USDT", + # "notional": '', + # "side": "buy", + # "last_fill_time": "0", + # "ms_t": "1646216634000", + # "type": "limit", + # "filled_notional": "0.000000000000000000000000000000", + # "last_fill_price": "0", + # "size": "0.500000000000000000000000000000", + # "price": "50.000000000000000000000000000000", + # "last_fill_count": "0", + # "filled_size": "0.000000000000000000000000000000", + # "margin_trading": "0", + # "state": "8", + # "order_id": "24807076628", + # "order_type": "0" + # } + # ], + # "table":"spot/user/order" + # } + # swap + # { + # "group":"futures/order", + # "data":[ + # { + # "action":2, + # "order":{ + # "order_id":"2312045036986775", + # "client_order_id":"", + # "price":"71.61707928", + # "size":"1", + # "symbol":"LTCUSDT", + # "state":1, + # "side":4, + # "type":"market", + # "leverage":"1", + # "open_type":"cross", + # "deal_avg_price":"0", + # "deal_size":"0", + # "create_time":1701625324646, + # "update_time":1701625324640, + # "plan_order_id":"", + # "last_trade":null + # } + # } + # ] + # } + # + orders = self.safe_value(message, 'data') + if orders is None: + return + ordersLength = len(orders) + newOrders = [] + symbols: dict = {} + if ordersLength > 0: + limit = self.safe_integer(self.options, 'ordersLimit', 1000) + if self.orders is None: + self.orders = ArrayCacheBySymbolById(limit) + stored = self.orders + for i in range(0, len(orders)): + order = self.parse_ws_order(orders[i]) + stored.append(order) + newOrders.append(order) + symbol = order['symbol'] + symbols[symbol] = True + messageHash = 'orders' + symbolKeys = list(symbols.keys()) + for i in range(0, len(symbolKeys)): + symbol = symbolKeys[i] + symbolSpecificMessageHash = messageHash + '::' + symbol + client.resolve(newOrders, symbolSpecificMessageHash) + client.resolve(newOrders, messageHash) + + def parse_ws_order(self, order: dict, market: Market = None): + # + # spot + # { + # "symbol": "LTC_USDT", + # "notional": '', + # "side": "buy", + # "last_fill_time": "0", + # "ms_t": "1646216634000", + # "type": "limit", + # "filled_notional": "0.000000000000000000000000000000", + # "last_fill_price": "0", + # "size": "0.500000000000000000000000000000", + # "price": "50.000000000000000000000000000000", + # "last_fill_count": "0", + # "filled_size": "0.000000000000000000000000000000", + # "margin_trading": "0", + # "state": "8", + # "order_id": "24807076628", + # "order_type": "0" + # } + # swap + # { + # "action":2, + # "order":{ + # "order_id":"2312045036986775", + # "client_order_id":"", + # "price":"71.61707928", + # "size":"1", + # "symbol":"LTCUSDT", + # "state":1, + # "side":4, + # "type":"market", + # "leverage":"1", + # "open_type":"cross", + # "deal_avg_price":"0", + # "deal_size":"0", + # "create_time":1701625324646, + # "update_time":1701625324640, + # "plan_order_id":"", + # "last_trade":null + # } + # } + # + action = self.safe_number(order, 'action') + isSpot = (action is None) + if isSpot: + marketId = self.safe_string(order, 'symbol') + market = self.safe_market(marketId, market, '_', 'spot') + id = self.safe_string(order, 'order_id') + clientOrderId = self.safe_string(order, 'clientOid') + price = self.safe_string(order, 'price') + filled = self.safe_string(order, 'filled_size') + amount = self.safe_string(order, 'size') + type = self.safe_string(order, 'type') + rawState = self.safe_string(order, 'state') + status = self.parse_order_status_by_type(market['type'], rawState) + timestamp = self.safe_integer(order, 'ms_t') + symbol = market['symbol'] + side = self.safe_string_lower(order, 'side') + return self.safe_order({ + 'info': order, + 'symbol': symbol, + 'id': id, + 'clientOrderId': clientOrderId, + 'timestamp': None, + 'datetime': None, + 'lastTradeTimestamp': timestamp, + 'type': type, + 'timeInForce': None, + 'postOnly': None, + 'side': side, + 'price': price, + 'stopPrice': None, + 'triggerPrice': None, + 'amount': amount, + 'cost': None, + 'average': None, + 'filled': filled, + 'remaining': None, + 'status': status, + 'fee': None, + 'trades': None, + }, market) + else: + orderInfo = self.safe_value(order, 'order') + marketId = self.safe_string(orderInfo, 'symbol') + symbol = self.safe_symbol(marketId, market, '', 'swap') + orderId = self.safe_string(orderInfo, 'order_id') + timestamp = self.safe_integer(orderInfo, 'create_time') + updatedTimestamp = self.safe_integer(orderInfo, 'update_time') + lastTrade = self.safe_value(orderInfo, 'last_trade') + cachedOrders = self.orders + orders = self.safe_value(cachedOrders.hashmap, symbol, {}) + cachedOrder = self.safe_value(orders, orderId) + trades = None + if cachedOrder is not None: + trades = self.safe_value(order, 'trades') + if lastTrade is not None: + if trades is None: + trades = [] + trades.append(lastTrade) + return self.safe_order({ + 'info': order, + 'symbol': symbol, + 'id': orderId, + 'clientOrderId': self.safe_string(orderInfo, 'client_order_id'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': updatedTimestamp, + 'type': self.safe_string(orderInfo, 'type'), + 'timeInForce': None, + 'postOnly': None, + 'side': self.parse_ws_order_side(self.safe_string(orderInfo, 'side')), + 'price': self.safe_string(orderInfo, 'price'), + 'stopPrice': None, + 'triggerPrice': None, + 'amount': self.safe_string(orderInfo, 'size'), + 'cost': None, + 'average': self.safe_string(orderInfo, 'deal_avg_price'), + 'filled': self.safe_string(orderInfo, 'deal_size'), + 'remaining': None, + 'status': self.parse_ws_order_status(self.safe_string(order, 'action')), + 'fee': None, + 'trades': trades, + }, market) + + def parse_ws_order_status(self, statusId): + statuses: dict = { + '1': 'closed', # match deal + '2': 'open', # submit order + '3': 'canceled', # cancel order + '4': 'closed', # liquidate cancel order + '5': 'canceled', # adl cancel order + '6': 'open', # part liquidate + '7': 'open', # bankrupty order + '8': 'closed', # passive adl match deal + '9': 'closed', # active adl match deal + } + return self.safe_string(statuses, statusId, statusId) + + def parse_ws_order_side(self, sideId): + sides: dict = { + '1': 'buy', # buy_open_long + '2': 'buy', # buy_close_short + '3': 'sell', # sell_close_long + '4': 'sell', # sell_open_short + } + return self.safe_string(sides, sideId, sideId) + + async def watch_positions(self, symbols: Strings = None, since: Int = None, limit: Int = None, params={}) -> List[Position]: + """ + + https://developer-pro.bitmart.com/en/futures/#private-position-channel + + watch all open positions + :param str[]|None symbols: list of unified market symbols + :param int [since]: the earliest time in ms to fetch positions + :param int [limit]: the maximum number of positions to retrieve + :param dict params: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + await self.load_markets() + type = 'swap' + await self.authenticate(type, params) + symbols = self.market_symbols(symbols, 'swap', True, True, False) + messageHash = 'positions' + if symbols is not None: + messageHash += '::' + ','.join(symbols) + subscriptionHash = 'futures/position' + request: dict = { + 'action': 'subscribe', + 'args': ['futures/position'], + } + url = self.implode_hostname(self.urls['api']['ws'][type]['private']) + newPositions = await self.watch(url, messageHash, self.deep_extend(request, params), subscriptionHash) + if self.newUpdates: + return newPositions + return self.filter_by_symbols_since_limit(self.positions, symbols, since, limit) + + def handle_positions(self, client: Client, message): + # + # { + # "group":"futures/position", + # "data":[ + # { + # "symbol":"LTCUSDT", + # "hold_volume":"5", + # "position_type":2, + # "open_type":2, + # "frozen_volume":"0", + # "close_volume":"0", + # "hold_avg_price":"71.582", + # "close_avg_price":"0", + # "open_avg_price":"71.582", + # "liquidate_price":"0", + # "create_time":1701623327513, + # "update_time":1701627620439 + # }, + # { + # "symbol":"LTCUSDT", + # "hold_volume":"6", + # "position_type":1, + # "open_type":2, + # "frozen_volume":"0", + # "close_volume":"0", + # "hold_avg_price":"71.681666666666666667", + # "close_avg_price":"0", + # "open_avg_price":"71.681666666666666667", + # "liquidate_price":"0", + # "create_time":1701621167225, + # "update_time":1701628152614 + # } + # ] + # } + # + data = self.safe_value(message, 'data', []) + if self.positions is None: + self.positions = ArrayCacheBySymbolBySide() + cache = self.positions + newPositions = [] + for i in range(0, len(data)): + rawPosition = data[i] + position = self.parse_ws_position(rawPosition) + newPositions.append(position) + cache.append(position) + messageHashes = self.find_message_hashes(client, 'positions::') + for i in range(0, len(messageHashes)): + messageHash = messageHashes[i] + parts = messageHash.split('::') + symbolsString = parts[1] + symbols = symbolsString.split(',') + positions = self.filter_by_array(newPositions, 'symbol', symbols, False) + if not self.is_empty(positions): + client.resolve(positions, messageHash) + client.resolve(newPositions, 'positions') + + def parse_ws_position(self, position, market: Market = None): + # + # { + # "symbol":"LTCUSDT", + # "hold_volume":"6", + # "position_type":1, + # "open_type":2, + # "frozen_volume":"0", + # "close_volume":"0", + # "hold_avg_price":"71.681666666666666667", + # "close_avg_price":"0", + # "open_avg_price":"71.681666666666666667", + # "liquidate_price":"0", + # "create_time":1701621167225, + # "update_time":1701628152614 + # } + # + marketId = self.safe_string(position, 'symbol') + market = self.safe_market(marketId, market, None, 'swap') + symbol = market['symbol'] + openTimestamp = self.safe_integer(position, 'create_time') + timestamp = self.safe_integer(position, 'update_time') + side = self.safe_integer(position, 'position_type') + marginModeId = self.safe_integer(position, 'open_type') + return self.safe_position({ + 'info': position, + 'id': None, + 'symbol': symbol, + 'timestamp': openTimestamp, + 'datetime': self.iso8601(openTimestamp), + 'lastUpdateTimestamp': timestamp, + 'hedged': None, + 'side': 'long' if (side == 1) else 'short', + 'contracts': self.safe_number(position, 'hold_volume'), + 'contractSize': self.safe_number(market, 'contractSize'), + 'entryPrice': self.safe_number(position, 'open_avg_price'), + 'markPrice': self.safe_number(position, 'hold_avg_price'), + 'lastPrice': None, + 'notional': None, + 'leverage': None, + 'collateral': None, + 'initialMargin': None, + 'initialMarginPercentage': None, + 'maintenanceMargin': None, + 'maintenanceMarginPercentage': None, + 'unrealizedPnl': None, + 'realizedPnl': None, + 'liquidationPrice': self.safe_number(position, 'liquidate_price'), + 'marginMode': 'isolated' if (marginModeId == 1) else 'cross', + 'percentage': None, + 'marginRatio': None, + 'stopLossPrice': None, + 'takeProfitPrice': None, + }) + + def handle_trade(self, client: Client, message): + # + # spot + # { + # "table": "spot/trade", + # "data": [ + # { + # "price": "52700.50", + # "s_t": 1630982050, + # "side": "buy", + # "size": "0.00112", + # "symbol": "BTC_USDT" + # }, + # ] + # } + # + # swap + # { + # "group":"futures/trade:BTCUSDT", + # "data":[ + # { + # "trade_id":6798697637, + # "symbol":"BTCUSDT", + # "deal_price":"39735.8", + # "deal_vol":"2", + # "way":1, + # "created_at":"2023-12-03T15:48:23.517518538Z", + # "m": True, + # } + # ] + # } + # + data = self.safe_value(message, 'data') + if data is None: + return + symbol = None + length = len(data) + isSwap = ('group' in message) + if isSwap: + # in swap, chronologically decreasing: 1709536849322, 1709536848954, + for i in range(0, length): + index = length - i - 1 + symbol = self.handle_trade_loop(data[index]) + else: + # in spot, chronologically increasing: 1709536771200, 1709536771226, + for i in range(0, length): + symbol = self.handle_trade_loop(data[i]) + client.resolve(self.trades[symbol], 'trade:' + symbol) + + def handle_trade_loop(self, entry): + trade = self.parse_ws_trade(entry) + symbol = trade['symbol'] + tradesLimit = self.safe_integer(self.options, 'tradesLimit', 1000) + if self.safe_value(self.trades, symbol) is None: + self.trades[symbol] = ArrayCache(tradesLimit) + stored = self.trades[symbol] + stored.append(trade) + return symbol + + def parse_ws_trade(self, trade: dict, market: Market = None): + # + # spot + # { + # "ms_t": 1740320841473, + # "price": "2806.54", + # "s_t": 1740320841, + # "side": "sell", + # "size": "0.77598", + # "symbol": "ETH_USDT" + # } + # + # swap + # { + # "trade_id": "3000000245258661", + # "symbol": "ETHUSDT", + # "deal_price": "2811.1", + # "deal_vol": "1858", + # "way": 2, + # "m": True, + # "created_at": "2025-02-23T13:59:59.646490751Z" + # } + # + marketId = self.safe_string(trade, 'symbol') + market = self.safe_market(marketId, market) + timestamp = self.safe_integer(trade, 'ms_t') + datetime: Str = None + if timestamp is None: + datetime = self.safe_string(trade, 'created_at') + timestamp = self.parse8601(datetime) + else: + datetime = self.iso8601(timestamp) + takerOrMaker = None # True for public trades + side = self.safe_string(trade, 'side') + buyerMaker = self.safe_bool(trade, 'm') + if buyerMaker is not None: + if side is None: + if buyerMaker: + side = 'sell' + else: + side = 'buy' + takerOrMaker = 'taker' + return self.safe_trade({ + 'info': trade, + 'id': self.safe_string(trade, 'trade_id'), + 'order': None, + 'timestamp': timestamp, + 'datetime': datetime, + 'symbol': market['symbol'], + 'type': None, + 'side': side, + 'price': self.safe_string_2(trade, 'price', 'deal_price'), + 'amount': self.safe_string_2(trade, 'size', 'deal_vol'), + 'cost': None, + 'takerOrMaker': takerOrMaker, + 'fee': None, + }, market) + + def handle_ticker(self, client: Client, message): + # + # { + # "data": [ + # { + # "base_volume_24h": "78615593.81", + # "high_24h": "52756.97", + # "last_price": "52638.31", + # "low_24h": "50991.35", + # "open_24h": "51692.03", + # "s_t": 1630981727, + # "symbol": "BTC_USDT" + # } + # ], + # "table": "spot/ticker" + # } + # + # { + # "data": { + # "symbol": "ETHUSDT", + # "last_price": "2807.73", + # "volume_24": "2227011952", + # "range": "0.0273398194664491", + # "mark_price": "2807.5", + # "index_price": "2808.71047619", + # "ask_price": "2808.04", + # "ask_vol": "7371", + # "bid_price": "2807.28", + # "bid_vol": "3561" + # }, + # "group": "futures/ticker:ETHUSDT@100ms" + # } + # + self.handle_bid_ask(client, message) + table = self.safe_string(message, 'table') + isSpot = (table is not None) + rawTickers = [] + if isSpot: + rawTickers = self.safe_list(message, 'data', []) + else: + rawTickers = [self.safe_value(message, 'data', {})] + if not len(rawTickers): + return + for i in range(0, len(rawTickers)): + ticker = self.parse_ticker(rawTickers[i]) if isSpot else self.parse_ws_swap_ticker(rawTickers[i]) + symbol = ticker['symbol'] + self.tickers[symbol] = ticker + messageHash = 'ticker:' + symbol + client.resolve(ticker, messageHash) + + def parse_ws_swap_ticker(self, ticker, market: Market = None): + # + # { + # "symbol": "ETHUSDT", + # "last_price": "2807.73", + # "volume_24": "2227011952", + # "range": "0.0273398194664491", + # "mark_price": "2807.5", + # "index_price": "2808.71047619", + # "ask_price": "2808.04", + # "ask_vol": "7371", + # "bid_price": "2807.28", + # "bid_vol": "3561" + # } + # + marketId = self.safe_string(ticker, 'symbol') + return self.safe_ticker({ + 'symbol': self.safe_symbol(marketId, market, '', 'swap'), + 'timestamp': None, + 'datetime': None, + 'high': None, + 'low': None, + 'bid': self.safe_string(ticker, 'bid_price'), + 'bidVolume': self.safe_string(ticker, 'bid_vol'), + 'ask': self.safe_string(ticker, 'ask_price'), + 'askVolume': self.safe_string(ticker, 'ask_vol'), + 'vwap': None, + 'open': None, + 'close': None, + 'last': self.safe_string(ticker, 'last_price'), + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': None, + 'quoteVolume': self.safe_string(ticker, 'volume_24'), + 'info': ticker, + 'markPrice': self.safe_string(ticker, 'mark_price'), + 'indexPrice': self.safe_string(ticker, 'index_price'), + }, market) + + async def watch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + + https://developer-pro.bitmart.com/en/spot/#public-kline-channel + https://developer-pro.bitmart.com/en/futuresv2/#public-klinebin-channel + + watches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + symbol = self.symbol(symbol) + market = self.market(symbol) + type = 'spot' + type, params = self.handle_market_type_and_params('watchOrderBook', market, params) + timeframes = self.safe_value(self.options, 'timeframes', {}) + interval = self.safe_string(timeframes, timeframe) + name = None + if type == 'spot': + name = 'kline' + interval + else: + name = 'klineBin' + interval + ohlcv = await self.subscribe(name, symbol, type, params) + if self.newUpdates: + limit = ohlcv.getLimit(symbol, limit) + return self.filter_by_since_limit(ohlcv, since, limit, 0, True) + + def handle_ohlcv(self, client: Client, message): + # + # { + # "data": [ + # { + # "candle": [ + # 1631056350, + # "46532.83", + # "46555.71", + # "46511.41", + # "46555.71", + # "0.25" + # ], + # "symbol": "BTC_USDT" + # } + # ], + # "table": "spot/kline1m" + # } + # swap + # { + # "group":"futures/klineBin1m:BTCUSDT", + # "data":{ + # "symbol":"BTCUSDT", + # "items":[ + # { + # "o":"39635.8", + # "h":"39636", + # "l":"39614.4", + # "c":"39629.7", + # "v":"31852", + # "ts":1701617761 + # } + # ] + # } + # } + # + channel = self.safe_string_2(message, 'table', 'group') + isSpot = (channel.find('spot') >= 0) + data = self.safe_value(message, 'data') + if data is None: + return + parts = channel.split('/') + part1 = self.safe_string(parts, 1, '') + interval = part1.replace('kline', '') + interval = interval.replace('Bin', '') + intervalParts = interval.split(':') + interval = self.safe_string(intervalParts, 0) + # use a reverse lookup in a static map instead + timeframes = self.safe_value(self.options, 'timeframes', {}) + timeframe = self.find_timeframe(interval, timeframes) + duration = self.parse_timeframe(timeframe) + durationInMs = duration * 1000 + if isSpot: + for i in range(0, len(data)): + marketId = self.safe_string(data[i], 'symbol') + market = self.safe_market(marketId) + symbol = market['symbol'] + rawOHLCV = self.safe_value(data[i], 'candle') + parsed = self.parse_ohlcv(rawOHLCV, market) + parsed[0] = self.parse_to_int(parsed[0] / durationInMs) * durationInMs + self.ohlcvs[symbol] = self.safe_value(self.ohlcvs, symbol, {}) + stored = self.safe_value(self.ohlcvs[symbol], timeframe) + if stored is None: + limit = self.safe_integer(self.options, 'OHLCVLimit', 1000) + stored = ArrayCacheByTimestamp(limit) + self.ohlcvs[symbol][timeframe] = stored + stored.append(parsed) + messageHash = channel + ':' + marketId + client.resolve(stored, messageHash) + else: + marketId = self.safe_string(data, 'symbol') + market = self.safe_market(marketId, None, None, 'swap') + symbol = market['symbol'] + items = self.safe_value(data, 'items', []) + self.ohlcvs[symbol] = self.safe_value(self.ohlcvs, symbol, {}) + stored = self.safe_value(self.ohlcvs[symbol], timeframe) + if stored is None: + limit = self.safe_integer(self.options, 'OHLCVLimit', 1000) + stored = ArrayCacheByTimestamp(limit) + self.ohlcvs[symbol][timeframe] = stored + for i in range(0, len(items)): + candle = items[i] + parsed = self.parse_ohlcv(candle, market) + stored.append(parsed) + client.resolve(stored, channel) + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + + https://developer-pro.bitmart.com/en/spot/#public-depth-all-channel + https://developer-pro.bitmart.com/en/spot/#public-depth-increase-channel + https://developer-pro.bitmart.com/en/futuresv2/#public-depth-channel + + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.speed]: *futures only* '100ms' or '200ms' + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + options = self.safe_value(self.options, 'watchOrderBook', {}) + depth = self.safe_string(options, 'depth', 'depth/increase100') + symbol = self.symbol(symbol) + market = self.market(symbol) + type = 'spot' + type, params = self.handle_market_type_and_params('watchOrderBook', market, params) + if type == 'swap' and depth == 'depth/increase100': + depth = 'depth50' + orderbook = await self.subscribe(depth, symbol, type, params) + return orderbook.limit() + + def handle_delta(self, bookside, delta): + price = self.safe_float(delta, 0) + amount = self.safe_float(delta, 1) + bookside.store(price, amount) + + def handle_deltas(self, bookside, deltas): + for i in range(0, len(deltas)): + self.handle_delta(bookside, deltas[i]) + + def handle_order_book_message(self, client: Client, message, orderbook): + # + # { + # "asks": [ + # ['46828.38', "0.21847"], + # ['46830.68', "0.08232"], + # ['46832.08', "0.09285"], + # ['46837.82', "0.02028"], + # ['46839.43', "0.15068"] + # ], + # "bids": [ + # ['46820.78', "0.00444"], + # ['46814.33', "0.00234"], + # ['46813.50', "0.05021"], + # ['46808.14', "0.00217"], + # ['46808.04', "0.00013"] + # ], + # "ms_t": 1631044962431, + # "symbol": "BTC_USDT" + # } + # + asks = self.safe_list(message, 'asks', []) + bids = self.safe_list(message, 'bids', []) + self.handle_deltas(orderbook['asks'], asks) + self.handle_deltas(orderbook['bids'], bids) + timestamp = self.safe_integer(message, 'ms_t') + marketId = self.safe_string(message, 'symbol') + symbol = self.safe_symbol(marketId) + orderbook['symbol'] = symbol + orderbook['timestamp'] = timestamp + orderbook['datetime'] = self.iso8601(timestamp) + return orderbook + + def handle_order_book(self, client: Client, message): + # + # spot depth-all + # + # { + # "data": [ + # { + # "asks": [ + # ['46828.38', "0.21847"], + # ['46830.68', "0.08232"], + # ... + # ], + # "bids": [ + # ['46820.78', "0.00444"], + # ['46814.33', "0.00234"], + # ... + # ], + # "ms_t": 1631044962431, + # "symbol": "BTC_USDT" + # } + # ], + # "table": "spot/depth5" + # } + # + # spot increse depth snapshot + # + # { + # "data":[ + # { + # "asks":[ + # ["43652.52", "0.02039"], + # ... + # ], + # "bids":[ + # ["43652.51", "0.00500"], + # ... + # ], + # "ms_t":1703376836487, + # "symbol":"BTC_USDT", + # "type":"snapshot", # or update + # "version":2141731 + # } + # ], + # "table":"spot/depth/increase100" + # } + # + # swap + # + # { + # "group":"futures/depth50:BTCUSDT", + # "data":{ + # "symbol":"BTCUSDT", + # "way":1, + # "depths":[ + # { + # "price":"39509.8", + # "vol":"2379" + # }, + # { + # "price":"39509.6", + # "vol":"6815" + # }, + # ... + # ], + # "ms_t":1701566021194 + # } + # } + # + isSpot = ('table' in message) + datas = [] + if isSpot: + datas = self.safe_list(message, 'data', datas) + else: + orderBookEntry = self.safe_dict(message, 'data') + if orderBookEntry is not None: + datas.append(orderBookEntry) + length = len(datas) + if length <= 0: + return + channelName = self.safe_string_2(message, 'table', 'group') + # find limit subscribed to + limitsToCheck = ['100', '50', '20', '10', '5'] + limit = 0 + for i in range(0, len(limitsToCheck)): + limitString = limitsToCheck[i] + if channelName.find(limitString) >= 0: + limit = self.parse_to_int(limitString) + break + if isSpot: + channel = channelName.replace('spot/', '') + for i in range(0, len(datas)): + update = datas[i] + marketId = self.safe_string(update, 'symbol') + symbol = self.safe_symbol(marketId) + if not (symbol in self.orderbooks): + ob = self.order_book({}, limit) + ob['symbol'] = symbol + self.orderbooks[symbol] = ob + orderbook = self.orderbooks[symbol] + type = self.safe_string(update, 'type') + if (type == 'snapshot') or (not(channelName.find('increase') >= 0)): + orderbook.reset({}) + self.handle_order_book_message(client, update, orderbook) + timestamp = self.safe_integer(update, 'ms_t') + if orderbook['timestamp'] is None: + orderbook['timestamp'] = timestamp + orderbook['datetime'] = self.iso8601(timestamp) + messageHash = channelName + ':' + marketId + client.resolve(orderbook, messageHash) + # resolve ForSymbols + messageHashForMulti = channel + ':' + symbol + client.resolve(orderbook, messageHashForMulti) + else: + tableParts = channelName.split(':') + channel = tableParts[0].replace('futures/', '') + data = datas[0] # contract markets always contain only one member + depths = data['depths'] + marketId = self.safe_string(data, 'symbol') + symbol = self.safe_symbol(marketId) + if not (symbol in self.orderbooks): + ob = self.order_book({}, limit) + ob['symbol'] = symbol + self.orderbooks[symbol] = ob + orderbook = self.orderbooks[symbol] + way = self.safe_integer(data, 'way') + side = 'bids' if (way == 1) else 'asks' + if way == 1: + orderbook[side] = Bids([], limit) + else: + orderbook[side] = Asks([], limit) + for i in range(0, len(depths)): + depth = depths[i] + price = self.safe_number(depth, 'price') + amount = self.safe_number(depth, 'vol') + orderbookSide = self.safe_value(orderbook, side) + orderbookSide.store(price, amount) + bidsLength = len(orderbook['bids']) + asksLength = len(orderbook['asks']) + if (bidsLength == 0) or (asksLength == 0): + return + timestamp = self.safe_integer(data, 'ms_t') + orderbook['timestamp'] = timestamp + orderbook['datetime'] = self.iso8601(timestamp) + messageHash = channelName + client.resolve(orderbook, messageHash) + # resolve ForSymbols + messageHashForMulti = channel + ':' + symbol + client.resolve(orderbook, messageHashForMulti) + + async def watch_order_book_for_symbols(self, symbols: List[str], limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://developer-pro.bitmart.com/en/spot/#public-depth-increase-channel + + :param str[] symbols: unified array of symbols + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.depth]: the type of order book to subscribe to, default is 'depth/increase100', also accepts 'depth5' or 'depth20' or depth50 + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + type = None + symbols, type, params = self.get_params_for_multiple_sub('watchOrderBookForSymbols', symbols, limit, params) + channel = None + channel, params = self.handle_option_and_params(params, 'watchOrderBookForSymbols', 'depth', 'depth/increase100') + if type == 'swap' and channel == 'depth/increase100': + channel = 'depth50' + orderbook = await self.subscribe_multiple(channel, type, symbols, params) + return orderbook.limit() + + async def authenticate(self, type, params={}): + self.check_required_credentials() + url = self.implode_hostname(self.urls['api']['ws'][type]['private']) + messageHash = 'authenticated' + client = self.client(url) + future = client.reusableFuture(messageHash) + authenticated = self.safe_value(client.subscriptions, messageHash) + if authenticated is None: + timestamp = str(self.milliseconds()) + memo = self.uid + path = 'bitmart.WebSocket' + auth = timestamp + '#' + memo + '#' + path + signature = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256) + request = None + if type == 'spot': + request = { + 'op': 'login', + 'args': [ + self.apiKey, + timestamp, + signature, + ], + } + else: + request = { + 'action': 'access', + 'args': [ + self.apiKey, + timestamp, + signature, + 'web', + ], + } + message = self.extend(request, params) + self.watch(url, messageHash, message, messageHash) + return await future + + def handle_subscription_status(self, client: Client, message): + # + # {"event":"subscribe","channel":"spot/depth:BTC-USDT"} + # + return message + + def handle_authenticate(self, client: Client, message): + # + # spot + # {event: "login"} + # swap + # {action: 'access', success: True} + # + messageHash = 'authenticated' + future = self.safe_value(client.futures, messageHash) + future.resolve(True) + + def handle_error_message(self, client: Client, message) -> Bool: + # + # {event: "error", message: "Invalid sign", errorCode: 30013} + # {"event":"error","message":"Unrecognized request: {\"event\":\"subscribe\",\"channel\":\"spot/depth:BTC-USDT\"}","errorCode":30039} + # { + # action: '', + # group: 'futures/trade:BTCUSDT', + # success: False, + # request: {action: '', args: ['futures/trade:BTCUSDT']}, + # error: 'Invalid action [] for group [futures/trade:BTCUSDT]' + # } + # + errorCode = self.safe_string(message, 'errorCode') + error = self.safe_string(message, 'error') + try: + if errorCode is not None or error is not None: + feedback = self.id + ' ' + self.json(message) + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + messageString = self.safe_value(message, 'message', error) + self.throw_broadly_matched_exception(self.exceptions['broad'], messageString, feedback) + action = self.safe_string(message, 'action') + if action == 'access': + raise AuthenticationError(feedback) + raise ExchangeError(feedback) + return False + except Exception as e: + if (isinstance(e, AuthenticationError)): + messageHash = 'authenticated' + client.reject(e, messageHash) + if messageHash in client.subscriptions: + del client.subscriptions[messageHash] + client.reject(e) + return True + + def handle_message(self, client: Client, message): + if self.handle_error_message(client, message): + return + # + # {"event":"error","message":"Unrecognized request: {\"event\":\"subscribe\",\"channel\":\"spot/depth:BTC-USDT\"}","errorCode":30039} + # + # subscribe events on spot: + # + # {"event":"subscribe", "topic":"spot/kline1m:BTC_USDT"} + # + # subscribe on contracts: + # + # {"action":"subscribe", "group":"futures/klineBin1m:BTCUSDT", "success":true, "request":{"action":"subscribe", "args":["futures/klineBin1m:BTCUSDT"]}} + # + # regular updates - spot + # + # { + # "table": "spot/depth", + # "action": "partial", + # "data": [ + # { + # "instrument_id": "BTC-USDT", + # "asks": [ + # ["5301.8", "0.03763319", "1"], + # ["5302.4", "0.00305", "2"], + # ], + # "bids": [ + # ["5301.7", "0.58911427", "6"], + # ["5301.6", "0.01222922", "4"], + # ], + # "timestamp": "2020-03-16T03:25:00.440Z", + # "checksum": -2088736623 + # } + # ] + # } + # + # regular updates - contracts + # + # { + # group: "futures/klineBin1m:BTCUSDT", + # data: { + # symbol: "BTCUSDT", + # items: [{o: "67944.7", "h": ....}], + # }, + # } + # + # {data: '', table: "spot/user/order"} + # + # the only realiable way(for both spot & swap) is to check 'data' key + isDataUpdate = ('data' in message) + if not isDataUpdate: + event = self.safe_string_2(message, 'event', 'action') + if event is not None: + methods: dict = { + # 'info': self.handleSystemStatus, + 'login': self.handle_authenticate, + 'access': self.handle_authenticate, + 'subscribe': self.handle_subscription_status, + } + method = self.safe_value(methods, event) + if method is not None: + method(client, message) + else: + channel = self.safe_string_2(message, 'table', 'group') + methods: dict = { + 'depth': self.handle_order_book, + 'ticker': self.handle_ticker, + 'trade': self.handle_trade, + 'kline': self.handle_ohlcv, + 'order': self.handle_orders, + 'position': self.handle_positions, + 'balance': self.handle_balance, + 'asset': self.handle_balance, + } + keys = list(methods.keys()) + for i in range(0, len(keys)): + key = keys[i] + if channel.find(key) >= 0: + method = self.safe_value(methods, key) + method(client, message) diff --git a/ccxt/pro/bitmex.py b/ccxt/pro/bitmex.py new file mode 100644 index 0000000..be86300 --- /dev/null +++ b/ccxt/pro/bitmex.py @@ -0,0 +1,1694 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolById, ArrayCacheBySymbolBySide, ArrayCacheByTimestamp +import hashlib +from ccxt.base.types import Any, Balances, Bool, Int, Liquidation, Order, OrderBook, Position, Str, Strings, Ticker, Tickers, Trade +from ccxt.async_support.base.ws.client import Client +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import RateLimitExceeded + + +class bitmex(ccxt.async_support.bitmex): + + def describe(self) -> Any: + return self.deep_extend(super(bitmex, self).describe(), { + 'has': { + 'ws': True, + 'watchBalance': True, + 'watchLiquidations': True, + 'watchLiquidationsForSymbols': True, + 'watchMyLiquidations': None, + 'watchMyLiquidationsForSymbols': None, + 'watchMyTrades': True, + 'watchOHLCV': True, + 'watchOrderBook': True, + 'watchOrderBookForSymbols': True, + 'watchOrders': True, + 'watchPostions': True, + 'watchTicker': True, + 'watchTickers': True, + 'watchTrades': True, + 'watchTradesForSymbols': True, + }, + 'urls': { + 'test': { + 'ws': 'wss://ws.testnet.bitmex.com/realtime', + }, + 'api': { + 'ws': 'wss://ws.bitmex.com/realtime', + }, + }, + # 'versions': { + # 'ws': '0.2.0', + # }, + 'options': { + 'watchOrderBookLevel': 'orderBookL2', # 'orderBookL2' = L2 full order book, 'orderBookL2_25' = L2 top 25, 'orderBook10' L3 top 10 + 'tradesLimit': 1000, + 'OHLCVLimit': 1000, + }, + 'exceptions': { + 'ws': { + 'exact': { + }, + 'broad': { + 'Rate limit exceeded': RateLimitExceeded, + }, + }, + }, + }) + + async def watch_ticker(self, symbol: str, params={}) -> Ticker: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://www.bitmex.com/app/wsAPI#Subscriptions + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbol = self.symbol(symbol) + tickers = await self.watch_tickers([symbol], params) + return tickers[symbol] + + async def watch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list + + https://www.bitmex.com/app/wsAPI#Subscriptions + + :param str[] symbols: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, True) + name = 'instrument' + url = self.urls['api']['ws'] + messageHashes = [] + rawSubscriptions = [] + if symbols is not None: + for i in range(0, len(symbols)): + symbol = symbols[i] + market = self.market(symbol) + subscription = name + ':' + market['id'] + rawSubscriptions.append(subscription) + messageHash = 'ticker:' + symbol + messageHashes.append(messageHash) + else: + rawSubscriptions.append(name) + messageHashes.append('alltickers') + request: dict = { + 'op': 'subscribe', + 'args': rawSubscriptions, + } + ticker = await self.watch_multiple(url, messageHashes, self.extend(request, params), rawSubscriptions) + if self.newUpdates: + result: dict = {} + result[ticker['symbol']] = ticker + return result + return self.filter_by_array(self.tickers, 'symbol', symbols) + + def handle_ticker(self, client: Client, message): + # + # { + # "table": "instrument", + # "action": "partial", + # "keys": ["symbol"], + # "types": { + # "symbol": "symbol", + # "rootSymbol": "symbol", + # "state": "symbol", + # "typ": "symbol", + # "listing": "timestamp", + # "front": "timestamp", + # "expiry": "timestamp", + # "settle": "timestamp", + # "relistInterval": "timespan", + # "inverseLeg": "symbol", + # "sellLeg": "symbol", + # "buyLeg": "symbol", + # "optionStrikePcnt": "float", + # "optionStrikeRound": "float", + # "optionStrikePrice": "float", + # "optionMultiplier": "float", + # "positionCurrency": "symbol", + # "underlying": "symbol", + # "quoteCurrency": "symbol", + # "underlyingSymbol": "symbol", + # "reference": "symbol", + # "referenceSymbol": "symbol", + # "calcInterval": "timespan", + # "publishInterval": "timespan", + # "publishTime": "timespan", + # "maxOrderQty": "long", + # "maxPrice": "float", + # "lotSize": "long", + # "tickSize": "float", + # "multiplier": "long", + # "settlCurrency": "symbol", + # "underlyingToPositionMultiplier": "long", + # "underlyingToSettleMultiplier": "long", + # "quoteToSettleMultiplier": "long", + # "isQuanto": "boolean", + # "isInverse": "boolean", + # "initMargin": "float", + # "maintMargin": "float", + # "riskLimit": "long", + # "riskStep": "long", + # "limit": "float", + # "capped": "boolean", + # "taxed": "boolean", + # "deleverage": "boolean", + # "makerFee": "float", + # "takerFee": "float", + # "settlementFee": "float", + # "insuranceFee": "float", + # "fundingBaseSymbol": "symbol", + # "fundingQuoteSymbol": "symbol", + # "fundingPremiumSymbol": "symbol", + # "fundingTimestamp": "timestamp", + # "fundingInterval": "timespan", + # "fundingRate": "float", + # "indicativeFundingRate": "float", + # "rebalanceTimestamp": "timestamp", + # "rebalanceInterval": "timespan", + # "openingTimestamp": "timestamp", + # "closingTimestamp": "timestamp", + # "sessionInterval": "timespan", + # "prevClosePrice": "float", + # "limitDownPrice": "float", + # "limitUpPrice": "float", + # "bankruptLimitDownPrice": "float", + # "bankruptLimitUpPrice": "float", + # "prevTotalVolume": "long", + # "totalVolume": "long", + # "volume": "long", + # "volume24h": "long", + # "prevTotalTurnover": "long", + # "totalTurnover": "long", + # "turnover": "long", + # "turnover24h": "long", + # "homeNotional24h": "float", + # "foreignNotional24h": "float", + # "prevPrice24h": "float", + # "vwap": "float", + # "highPrice": "float", + # "lowPrice": "float", + # "lastPrice": "float", + # "lastPriceProtected": "float", + # "lastTickDirection": "symbol", + # "lastChangePcnt": "float", + # "bidPrice": "float", + # "midPrice": "float", + # "askPrice": "float", + # "impactBidPrice": "float", + # "impactMidPrice": "float", + # "impactAskPrice": "float", + # "hasLiquidity": "boolean", + # "openInterest": "long", + # "openValue": "long", + # "fairMethod": "symbol", + # "fairBasisRate": "float", + # "fairBasis": "float", + # "fairPrice": "float", + # "markMethod": "symbol", + # "markPrice": "float", + # "indicativeTaxRate": "float", + # "indicativeSettlePrice": "float", + # "optionUnderlyingPrice": "float", + # "settledPrice": "float", + # "timestamp": "timestamp" + # }, + # "foreignKeys": { + # "inverseLeg": "instrument", + # "sellLeg": "instrument", + # "buyLeg": "instrument" + # }, + # "attributes": {symbol: "unique"}, + # "filter": {symbol: "XBTUSD"}, + # "data": [ + # { + # "symbol": "XBTUSD", + # "rootSymbol": "XBT", + # "state": "Open", + # "typ": "FFWCSX", + # "listing": "2016-05-13T12:00:00.000Z", + # "front": "2016-05-13T12:00:00.000Z", + # "expiry": null, + # "settle": null, + # "relistInterval": null, + # "inverseLeg": '', + # "sellLeg": '', + # "buyLeg": '', + # "optionStrikePcnt": null, + # "optionStrikeRound": null, + # "optionStrikePrice": null, + # "optionMultiplier": null, + # "positionCurrency": "USD", + # "underlying": "XBT", + # "quoteCurrency": "USD", + # "underlyingSymbol": "XBT=", + # "reference": "BMEX", + # "referenceSymbol": ".BXBT", + # "calcInterval": null, + # "publishInterval": null, + # "publishTime": null, + # "maxOrderQty": 10000000, + # "maxPrice": 1000000, + # "lotSize": 1, + # "tickSize": 0.5, + # "multiplier": -100000000, + # "settlCurrency": "XBt", + # "underlyingToPositionMultiplier": null, + # "underlyingToSettleMultiplier": -100000000, + # "quoteToSettleMultiplier": null, + # "isQuanto": False, + # "isInverse": True, + # "initMargin": 0.01, + # "maintMargin": 0.005, + # "riskLimit": 20000000000, + # "riskStep": 10000000000, + # "limit": null, + # "capped": False, + # "taxed": True, + # "deleverage": True, + # "makerFee": -0.00025, + # "takerFee": 0.00075, + # "settlementFee": 0, + # "insuranceFee": 0, + # "fundingBaseSymbol": ".XBTBON8H", + # "fundingQuoteSymbol": ".USDBON8H", + # "fundingPremiumSymbol": ".XBTUSDPI8H", + # "fundingTimestamp": "2020-01-29T12:00:00.000Z", + # "fundingInterval": "2000-01-01T08:00:00.000Z", + # "fundingRate": 0.000597, + # "indicativeFundingRate": 0.000652, + # "rebalanceTimestamp": null, + # "rebalanceInterval": null, + # "openingTimestamp": "2020-01-29T11:00:00.000Z", + # "closingTimestamp": "2020-01-29T12:00:00.000Z", + # "sessionInterval": "2000-01-01T01:00:00.000Z", + # "prevClosePrice": 9063.96, + # "limitDownPrice": null, + # "limitUpPrice": null, + # "bankruptLimitDownPrice": null, + # "bankruptLimitUpPrice": null, + # "prevTotalVolume": 1989881049026, + # "totalVolume": 1990196740950, + # "volume": 315691924, + # "volume24h": 4491824765, + # "prevTotalTurnover": 27865497128425564, + # "totalTurnover": 27868891594857150, + # "turnover": 3394466431587, + # "turnover24h": 48863390064843, + # "homeNotional24h": 488633.9006484273, + # "foreignNotional24h": 4491824765, + # "prevPrice24h": 9091, + # "vwap": 9192.8663, + # "highPrice": 9440, + # "lowPrice": 8886, + # "lastPrice": 9287, + # "lastPriceProtected": 9287, + # "lastTickDirection": "PlusTick", + # "lastChangePcnt": 0.0216, + # "bidPrice": 9286, + # "midPrice": 9286.25, + # "askPrice": 9286.5, + # "impactBidPrice": 9285.9133, + # "impactMidPrice": 9286.75, + # "impactAskPrice": 9287.6382, + # "hasLiquidity": True, + # "openInterest": 967826984, + # "openValue": 10432207060536, + # "fairMethod": "FundingRate", + # "fairBasisRate": 0.6537149999999999, + # "fairBasis": 0.33, + # "fairPrice": 9277.2, + # "markMethod": "FairPrice", + # "markPrice": 9277.2, + # "indicativeTaxRate": 0, + # "indicativeSettlePrice": 9276.87, + # "optionUnderlyingPrice": null, + # "settledPrice": null, + # "timestamp": "2020-01-29T11:31:37.114Z" + # } + # ] + # } + # + data = self.safe_list(message, 'data', []) + tickers: dict = {} + for i in range(0, len(data)): + update = data[i] + marketId = self.safe_string(update, 'symbol') + symbol = self.safe_symbol(marketId) + if not (symbol in self.tickers): + self.tickers[symbol] = self.parse_ticker({}) + updatedTicker = self.parse_ticker(update) + fullParsedTicker = self.deep_extend(self.tickers[symbol], updatedTicker) + tickers[symbol] = fullParsedTicker + self.tickers[symbol] = fullParsedTicker + messageHash = 'ticker:' + symbol + client.resolve(fullParsedTicker, messageHash) + client.resolve(fullParsedTicker, 'alltickers') + return message + + async def watch_liquidations(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Liquidation]: + """ + watch the public liquidations of a trading pair + + https://www.bitmex.com/app/wsAPI#Liquidation + + :param str symbol: unified CCXT market symbol + :param int [since]: the earliest time in ms to fetch liquidations for + :param int [limit]: the maximum number of liquidation structures to retrieve + :param dict [params]: exchange specific parameters for the bitmex api endpoint + :returns dict: an array of `liquidation structures ` + """ + return self.watch_liquidations_for_symbols([symbol], since, limit, params) + + async def watch_liquidations_for_symbols(self, symbols: List[str], since: Int = None, limit: Int = None, params={}) -> List[Liquidation]: + """ + watch the public liquidations of a trading pair + + https://www.bitmex.com/app/wsAPI#Liquidation + + :param str[] symbols: + :param int [since]: the earliest time in ms to fetch liquidations for + :param int [limit]: the maximum number of liquidation structures to retrieve + :param dict [params]: exchange specific parameters for the bitmex api endpoint + :returns dict: an array of `liquidation structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, True, True) + messageHashes = [] + subscriptionHashes = [] + if self.is_empty(symbols): + subscriptionHashes.append('liquidation') + messageHashes.append('liquidations') + else: + for i in range(0, len(symbols)): + symbol = symbols[i] + market = self.market(symbol) + subscriptionHashes.append('liquidation:' + market['id']) + messageHashes.append('liquidations::' + symbol) + url = self.urls['api']['ws'] + request = { + 'op': 'subscribe', + 'args': subscriptionHashes, + } + newLiquidations = await self.watch_multiple(url, messageHashes, self.deep_extend(request, params), subscriptionHashes) + if self.newUpdates: + return newLiquidations + return self.filter_by_symbols_since_limit(self.liquidations, symbols, since, limit, True) + + def handle_liquidation(self, client: Client, message): + # + # { + # "table":"liquidation", + # "action":"partial", + # "keys":[ + # "orderID" + # ], + # "types":{ + # "orderID":"guid", + # "symbol":"symbol", + # "side":"symbol", + # "price":"float", + # "leavesQty":"long" + # }, + # "filter":{}, + # "data":[ + # { + # "orderID":"e0a568ee-7830-4428-92c3-73e82b9576ce", + # "symbol":"XPLAUSDT", + # "side":"Sell", + # "price":0.206, + # "leavesQty":340 + # } + # ] + # } + # + rawLiquidations = self.safe_value(message, 'data', []) + newLiquidations = [] + for i in range(0, len(rawLiquidations)): + rawLiquidation = rawLiquidations[i] + liquidation = self.parse_liquidation(rawLiquidation) + symbol = liquidation['symbol'] + liquidations = self.safe_value(self.liquidations, symbol) + if liquidations is None: + limit = self.safe_integer(self.options, 'liquidationsLimit', 1000) + liquidations = ArrayCache(limit) + liquidations.append(liquidation) + self.liquidations[symbol] = liquidations + newLiquidations.append(liquidation) + client.resolve(newLiquidations, 'liquidations') + liquidationsBySymbol = self.index_by(newLiquidations, 'symbol') + symbols = list(liquidationsBySymbol.keys()) + for i in range(0, len(symbols)): + symbol = symbols[i] + client.resolve(liquidationsBySymbol[symbol], 'liquidations::' + symbol) + + async def watch_balance(self, params={}) -> Balances: + """ + watch balance and get the amount of funds available for trading or funds locked in orders + + https://www.bitmex.com/app/wsAPI#Subscriptions + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + await self.authenticate() + messageHash = 'margin' + url = self.urls['api']['ws'] + request: dict = { + 'op': 'subscribe', + 'args': [ + messageHash, + ], + } + return await self.watch(url, messageHash, self.extend(request, params), messageHash) + + def handle_balance(self, client: Client, message): + # + # { + # "table": "margin", + # "action": "partial", + # "keys": ["account"], + # "types": { + # "account": "long", + # "currency": "symbol", + # "riskLimit": "long", + # "prevState": "symbol", + # "state": "symbol", + # "action": "symbol", + # "amount": "long", + # "pendingCredit": "long", + # "pendingDebit": "long", + # "confirmedDebit": "long", + # "prevRealisedPnl": "long", + # "prevUnrealisedPnl": "long", + # "grossComm": "long", + # "grossOpenCost": "long", + # "grossOpenPremium": "long", + # "grossExecCost": "long", + # "grossMarkValue": "long", + # "riskValue": "long", + # "taxableMargin": "long", + # "initMargin": "long", + # "maintMargin": "long", + # "sessionMargin": "long", + # "targetExcessMargin": "long", + # "varMargin": "long", + # "realisedPnl": "long", + # "unrealisedPnl": "long", + # "indicativeTax": "long", + # "unrealisedProfit": "long", + # "syntheticMargin": "long", + # "walletBalance": "long", + # "marginBalance": "long", + # "marginBalancePcnt": "float", + # "marginLeverage": "float", + # "marginUsedPcnt": "float", + # "excessMargin": "long", + # "excessMarginPcnt": "float", + # "availableMargin": "long", + # "withdrawableMargin": "long", + # "timestamp": "timestamp", + # "grossLastValue": "long", + # "commission": "float" + # }, + # "foreignKeys": {}, + # "attributes": {account: "sorted"}, + # "filter": {account: 1455728}, + # "data": [ + # { + # "account": 1455728, + # "currency": "XBt", + # "riskLimit": 1000000000000, + # "prevState": '', + # "state": '', + # "action": '', + # "amount": 263542, + # "pendingCredit": 0, + # "pendingDebit": 0, + # "confirmedDebit": 0, + # "prevRealisedPnl": 0, + # "prevUnrealisedPnl": 0, + # "grossComm": 0, + # "grossOpenCost": 0, + # "grossOpenPremium": 0, + # "grossExecCost": 0, + # "grossMarkValue": 0, + # "riskValue": 0, + # "taxableMargin": 0, + # "initMargin": 0, + # "maintMargin": 0, + # "sessionMargin": 0, + # "targetExcessMargin": 0, + # "varMargin": 0, + # "realisedPnl": 0, + # "unrealisedPnl": 0, + # "indicativeTax": 0, + # "unrealisedProfit": 0, + # "syntheticMargin": null, + # "walletBalance": 263542, + # "marginBalance": 263542, + # "marginBalancePcnt": 1, + # "marginLeverage": 0, + # "marginUsedPcnt": 0, + # "excessMargin": 263542, + # "excessMarginPcnt": 1, + # "availableMargin": 263542, + # "withdrawableMargin": 263542, + # "timestamp": "2020-08-03T12:01:01.246Z", + # "grossLastValue": 0, + # "commission": null + # } + # ] + # } + # + data = self.safe_value(message, 'data') + balance = self.parse_balance(data) + self.balance = self.extend(self.balance, balance) + messageHash = self.safe_string(message, 'table') + client.resolve(self.balance, messageHash) + + def handle_trades(self, client: Client, message): + # + # initial snapshot + # + # { + # "table": "trade", + # "action": "partial", + # "keys": [], + # "types": { + # "timestamp": "timestamp", + # "symbol": "symbol", + # "side": "symbol", + # "size": "long", + # "price": "float", + # "tickDirection": "symbol", + # "trdMatchID": "guid", + # "grossValue": "long", + # "homeNotional": "float", + # "foreignNotional": "float" + # }, + # "foreignKeys": {symbol: "instrument", side: "side"}, + # "attributes": {timestamp: "sorted", symbol: "grouped"}, + # "filter": {symbol: "XBTUSD"}, + # "data": [ + # { + # "timestamp": "2020-01-30T17:03:07.854Z", + # "symbol": "XBTUSD", + # "side": "Buy", + # "size": 15000, + # "price": 9378, + # "tickDirection": "ZeroPlusTick", + # "trdMatchID": "5b426e7f-83d1-2c80-295d-ee995b8ceb4a", + # "grossValue": 159945000, + # "homeNotional": 1.59945, + # "foreignNotional": 15000 + # } + # ] + # } + # + # updates + # + # { + # "table": "trade", + # "action": "insert", + # "data": [ + # { + # "timestamp": "2020-01-30T17:31:40.160Z", + # "symbol": "XBTUSD", + # "side": "Sell", + # "size": 37412, + # "price": 9521.5, + # "tickDirection": "ZeroMinusTick", + # "trdMatchID": "a4bfc6bc-6cf1-1a11-622e-270eef8ca5c7", + # "grossValue": 392938236, + # "homeNotional": 3.92938236, + # "foreignNotional": 37412 + # } + # ] + # } + # + table = 'trade' + data = self.safe_value(message, 'data', []) + dataByMarketIds = self.group_by(data, 'symbol') + marketIds = list(dataByMarketIds.keys()) + for i in range(0, len(marketIds)): + marketId = marketIds[i] + market = self.safe_market(marketId) + symbol = market['symbol'] + messageHash = table + ':' + symbol + trades = self.parse_trades(dataByMarketIds[marketId], market) + stored = self.safe_value(self.trades, symbol) + if stored is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + stored = ArrayCache(limit) + self.trades[symbol] = stored + for j in range(0, len(trades)): + stored.append(trades[j]) + client.resolve(stored, messageHash) + + async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://www.bitmex.com/app/wsAPI#Subscriptions + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + return await self.watch_trades_for_symbols([symbol], since, limit, params) + + async def authenticate(self, params={}): + url = self.urls['api']['ws'] + client = self.client(url) + messageHash = 'authenticated' + future = client.reusableFuture(messageHash) + authenticated = self.safe_value(client.subscriptions, messageHash) + if authenticated is None: + self.check_required_credentials() + timestamp = self.milliseconds() + payload = 'GET' + '/realtime' + str(timestamp) + signature = self.hmac(self.encode(payload), self.encode(self.secret), hashlib.sha256) + request: dict = { + 'op': 'authKeyExpires', + 'args': [ + self.apiKey, + timestamp, + signature, + ], + } + message = self.extend(request, params) + self.watch(url, messageHash, message, messageHash) + return await future + + def handle_authentication_message(self, client: Client, message): + authenticated = self.safe_bool(message, 'success', False) + messageHash = 'authenticated' + if authenticated: + # we resolve the future here permanently so authentication only happens once + future = self.safe_value(client.futures, messageHash) + future.resolve(True) + else: + error = AuthenticationError(self.json(message)) + client.reject(error, messageHash) + if messageHash in client.subscriptions: + del client.subscriptions[messageHash] + + async def watch_positions(self, symbols: Strings = None, since: Int = None, limit: Int = None, params={}) -> List[Position]: + """ + watch all open positions + + https://www.bitmex.com/app/wsAPI#Subscriptions + + :param str[]|None symbols: list of unified market symbols + :param int [since]: the earliest time in ms to watch positions for + :param int [limit]: the maximum number of positions to retrieve + :param dict params: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + await self.load_markets() + await self.authenticate() + subscriptionHash = 'position' + messageHash = 'positions' + if not self.is_empty(symbols): + messageHash = '::' + ','.join(symbols) + url = self.urls['api']['ws'] + request: dict = { + 'op': 'subscribe', + 'args': [ + subscriptionHash, + ], + } + newPositions = await self.watch(url, messageHash, request, subscriptionHash) + if self.newUpdates: + return newPositions + return self.filter_by_symbols_since_limit(self.positions, symbols, since, limit, True) + + def handle_positions(self, client, message): + # + # partial + # { + # table: 'position', + # action: 'partial', + # keys: ['account', 'symbol'], + # types: { + # account: 'long', + # symbol: 'symbol', + # currency: 'symbol', + # underlying: 'symbol', + # quoteCurrency: 'symbol', + # commission: 'float', + # initMarginReq: 'float', + # maintMarginReq: 'float', + # riskLimit: 'long', + # leverage: 'float', + # crossMargin: 'boolean', + # deleveragePercentile: 'float', + # rebalancedPnl: 'long', + # prevRealisedPnl: 'long', + # prevUnrealisedPnl: 'long', + # openingQty: 'long', + # openOrderBuyQty: 'long', + # openOrderBuyCost: 'long', + # openOrderBuyPremium: 'long', + # openOrderSellQty: 'long', + # openOrderSellCost: 'long', + # openOrderSellPremium: 'long', + # currentQty: 'long', + # currentCost: 'long', + # currentComm: 'long', + # realisedCost: 'long', + # unrealisedCost: 'long', + # grossOpenPremium: 'long', + # isOpen: 'boolean', + # markPrice: 'float', + # markValue: 'long', + # riskValue: 'long', + # homeNotional: 'float', + # foreignNotional: 'float', + # posState: 'symbol', + # posCost: 'long', + # posCross: 'long', + # posComm: 'long', + # posLoss: 'long', + # posMargin: 'long', + # posMaint: 'long', + # initMargin: 'long', + # maintMargin: 'long', + # realisedPnl: 'long', + # unrealisedPnl: 'long', + # unrealisedPnlPcnt: 'float', + # unrealisedRoePcnt: 'float', + # avgCostPrice: 'float', + # avgEntryPrice: 'float', + # breakEvenPrice: 'float', + # marginCallPrice: 'float', + # liquidationPrice: 'float', + # bankruptPrice: 'float', + # timestamp: 'timestamp' + # }, + # filter: {account: 412475}, + # data: [ + # { + # account: 412475, + # symbol: 'XBTUSD', + # currency: 'XBt', + # underlying: 'XBT', + # quoteCurrency: 'USD', + # commission: 0.00075, + # initMarginReq: 0.01, + # maintMarginReq: 0.0035, + # riskLimit: 20000000000, + # leverage: 100, + # crossMargin: True, + # deleveragePercentile: 1, + # rebalancedPnl: 0, + # prevRealisedPnl: 0, + # prevUnrealisedPnl: 0, + # openingQty: 400, + # openOrderBuyQty: 0, + # openOrderBuyCost: 0, + # openOrderBuyPremium: 0, + # openOrderSellQty: 0, + # openOrderSellCost: 0, + # openOrderSellPremium: 0, + # currentQty: 400, + # currentCost: -912269, + # currentComm: 684, + # realisedCost: 0, + # unrealisedCost: -912269, + # grossOpenPremium: 0, + # isOpen: True, + # markPrice: 43772, + # markValue: -913828, + # riskValue: 913828, + # homeNotional: 0.00913828, + # foreignNotional: -400, + # posCost: -912269, + # posCross: 1559, + # posComm: 694, + # posLoss: 0, + # posMargin: 11376, + # posMaint: 3887, + # initMargin: 0, + # maintMargin: 9817, + # realisedPnl: -684, + # unrealisedPnl: -1559, + # unrealisedPnlPcnt: -0.0017, + # unrealisedRoePcnt: -0.1709, + # avgCostPrice: 43846.7643, + # avgEntryPrice: 43846.7643, + # breakEvenPrice: 43880, + # marginCallPrice: 20976, + # liquidationPrice: 20976, + # bankruptPrice: 20941, + # timestamp: '2023-12-07T00:09:00.709Z' + # } + # ] + # } + # update + # { + # table: 'position', + # action: 'update', + # data: [ + # { + # account: 412475, + # symbol: 'XBTUSD', + # currency: 'XBt', + # currentQty: 400, + # markPrice: 43772.75, + # markValue: -913812, + # riskValue: 913812, + # homeNotional: 0.00913812, + # posCross: 1543, + # posComm: 693, + # posMargin: 11359, + # posMaint: 3886, + # maintMargin: 9816, + # unrealisedPnl: -1543, + # unrealisedRoePcnt: -0.1691, + # liquidationPrice: 20976, + # timestamp: '2023-12-07T00:09:10.760Z' + # } + # ] + # } + # + if self.positions is None: + self.positions = ArrayCacheBySymbolBySide() + cache = self.positions + rawPositions = self.safe_value(message, 'data', []) + newPositions = [] + for i in range(0, len(rawPositions)): + rawPosition = rawPositions[i] + position = self.parse_position(rawPosition) + newPositions.append(position) + cache.append(position) + messageHashes = self.find_message_hashes(client, 'positions::') + for i in range(0, len(messageHashes)): + messageHash = messageHashes[i] + parts = messageHash.split('::') + symbolsString = parts[1] + symbols = symbolsString.split(',') + positions = self.filter_by_array(newPositions, 'symbol', symbols, False) + if not self.is_empty(positions): + client.resolve(positions, messageHash) + client.resolve(newPositions, 'positions') + + async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + watches information on multiple orders made by the user + + https://www.bitmex.com/app/wsAPI#Subscriptions + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + await self.authenticate() + name = 'order' + subscriptionHash = name + messageHash = name + if symbol is not None: + symbol = self.symbol(symbol) + messageHash += ':' + symbol + url = self.urls['api']['ws'] + request: dict = { + 'op': 'subscribe', + 'args': [ + subscriptionHash, + ], + } + orders = await self.watch(url, messageHash, request, subscriptionHash) + if self.newUpdates: + limit = orders.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(orders, symbol, since, limit, True) + + def handle_orders(self, client: Client, message): + # + # { + # "table": "order", + # "action": "partial", + # "keys": ["orderID"], + # "types": { + # "orderID": "guid", + # "clOrdID": "string", + # "clOrdLinkID": "symbol", + # "account": "long", + # "symbol": "symbol", + # "side": "symbol", + # "simpleOrderQty": "float", + # "orderQty": "long", + # "price": "float", + # "displayQty": "long", + # "stopPx": "float", + # "pegOffsetValue": "float", + # "pegPriceType": "symbol", + # "currency": "symbol", + # "settlCurrency": "symbol", + # "ordType": "symbol", + # "timeInForce": "symbol", + # "execInst": "symbol", + # "contingencyType": "symbol", + # "exDestination": "symbol", + # "ordStatus": "symbol", + # "triggered": "symbol", + # "workingIndicator": "boolean", + # "ordRejReason": "symbol", + # "simpleLeavesQty": "float", + # "leavesQty": "long", + # "simpleCumQty": "float", + # "cumQty": "long", + # "avgPx": "float", + # "multiLegReportingType": "symbol", + # "text": "string", + # "transactTime": "timestamp", + # "timestamp": "timestamp" + # }, + # "foreignKeys": {symbol: 'instrument', side: "side", ordStatus: "ordStatus"}, + # "attributes": { + # "orderID": "grouped", + # "account": "grouped", + # "ordStatus": "grouped", + # "workingIndicator": "grouped" + # }, + # "filter": {account: 1455728}, + # "data": [ + # { + # "orderID": "56222c7a-9956-413a-82cf-99f4812c214b", + # "clOrdID": '', + # "clOrdLinkID": '', + # "account": 1455728, + # "symbol": "XBTUSD", + # "side": "Sell", + # "simpleOrderQty": null, + # "orderQty": 1, + # "price": 40000, + # "displayQty": null, + # "stopPx": null, + # "pegOffsetValue": null, + # "pegPriceType": '', + # "currency": "USD", + # "settlCurrency": "XBt", + # "ordType": "Limit", + # "timeInForce": "GoodTillCancel", + # "execInst": '', + # "contingencyType": '', + # "exDestination": "XBME", + # "ordStatus": "New", + # "triggered": '', + # "workingIndicator": True, + # "ordRejReason": '', + # "simpleLeavesQty": null, + # "leavesQty": 1, + # "simpleCumQty": null, + # "cumQty": 0, + # "avgPx": null, + # "multiLegReportingType": "SingleSecurity", + # "text": "Submitted via API.", + # "transactTime": "2021-01-02T21:38:49.246Z", + # "timestamp": "2021-01-02T21:38:49.246Z" + # } + # ] + # } + # + # { + # "table": "order", + # "action": "insert", + # "data": [ + # { + # "orderID": "fa993d8e-f7e4-46ed-8097-04f8e9393585", + # "clOrdID": '', + # "clOrdLinkID": '', + # "account": 1455728, + # "symbol": "XBTUSD", + # "side": "Sell", + # "simpleOrderQty": null, + # "orderQty": 1, + # "price": 40000, + # "displayQty": null, + # "stopPx": null, + # "pegOffsetValue": null, + # "pegPriceType": '', + # "currency": "USD", + # "settlCurrency": "XBt", + # "ordType": "Limit", + # "timeInForce": "GoodTillCancel", + # "execInst": '', + # "contingencyType": '', + # "exDestination": "XBME", + # "ordStatus": "New", + # "triggered": '', + # "workingIndicator": True, + # "ordRejReason": '', + # "simpleLeavesQty": null, + # "leavesQty": 1, + # "simpleCumQty": null, + # "cumQty": 0, + # "avgPx": null, + # "multiLegReportingType": "SingleSecurity", + # "text": "Submitted via API.", + # "transactTime": "2021-01-02T23:49:02.286Z", + # "timestamp": "2021-01-02T23:49:02.286Z" + # } + # ] + # } + # + # + # + # { + # "table": "order", + # "action": "update", + # "data": [ + # { + # "orderID": "fa993d8e-f7e4-46ed-8097-04f8e9393585", + # "ordStatus": "Canceled", + # "workingIndicator": False, + # "leavesQty": 0, + # "text": "Canceled: Canceled via API.\nSubmitted via API.", + # "timestamp": "2021-01-02T23:50:51.272Z", + # "clOrdID": '', + # "account": 1455728, + # "symbol": "XBTUSD" + # } + # ] + # } + # + data = self.safe_value(message, 'data', []) + messageHash = 'order' + # initial subscription response with multiple orders + dataLength = len(data) + if dataLength > 0: + if self.orders is None: + limit = self.safe_integer(self.options, 'ordersLimit', 1000) + self.orders = ArrayCacheBySymbolById(limit) + stored = self.orders + symbols: dict = {} + for i in range(0, dataLength): + currentOrder = data[i] + orderId = self.safe_string(currentOrder, 'orderID') + previousOrder = self.safe_value(stored.hashmap, orderId) + rawOrder = currentOrder + if previousOrder is not None: + rawOrder = self.extend(previousOrder['info'], currentOrder) + order = self.parse_order(rawOrder) + stored.append(order) + symbol = order['symbol'] + symbols[symbol] = True + client.resolve(self.orders, messageHash) + keys = list(symbols.keys()) + for i in range(0, len(keys)): + symbol = keys[i] + client.resolve(self.orders, messageHash + ':' + symbol) + + async def watch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + watches information on multiple trades made by the user + + https://www.bitmex.com/app/wsAPI#Subscriptions + + :param str symbol: unified market symbol of the market trades were made in + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trade structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + await self.authenticate() + name = 'execution' + subscriptionHash = name + messageHash = name + if symbol is not None: + symbol = self.symbol(symbol) + messageHash += ':' + symbol + url = self.urls['api']['ws'] + request: dict = { + 'op': 'subscribe', + 'args': [ + subscriptionHash, + ], + } + trades = await self.watch(url, messageHash, request, subscriptionHash) + if self.newUpdates: + limit = trades.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(trades, symbol, since, limit, True) + + def handle_my_trades(self, client: Client, message): + # + # { + # "table":"execution", + # "action":"insert", + # "data":[ + # { + # "execID":"0193e879-cb6f-2891-d099-2c4eb40fee21", + # "orderID":"00000000-0000-0000-0000-000000000000", + # "clOrdID":"", + # "clOrdLinkID":"", + # "account":2, + # "symbol":"XBTUSD", + # "side":"Sell", + # "lastQty":1, + # "lastPx":1134.37, + # "underlyingLastPx":null, + # "lastMkt":"XBME", + # "lastLiquidityInd":"RemovedLiquidity", + # "simpleOrderQty":null, + # "orderQty":1, + # "price":1134.37, + # "displayQty":null, + # "stopPx":null, + # "pegOffsetValue":null, + # "pegPriceType":"", + # "currency":"USD", + # "settlCurrency":"XBt", + # "execType":"Trade", + # "ordType":"Limit", + # "timeInForce":"ImmediateOrCancel", + # "execInst":"", + # "contingencyType":"", + # "exDestination":"XBME", + # "ordStatus":"Filled", + # "triggered":"", + # "workingIndicator":false, + # "ordRejReason":"", + # "simpleLeavesQty":0, + # "leavesQty":0, + # "simpleCumQty":0.001, + # "cumQty":1, + # "avgPx":1134.37, + # "commission":0.00075, + # "tradePublishIndicator":"DoNotPublishTrade", + # "multiLegReportingType":"SingleSecurity", + # "text":"Liquidation", + # "trdMatchID":"7f4ab7f6-0006-3234-76f4-ae1385aad00f", + # "execCost":88155, + # "execComm":66, + # "homeNotional":-0.00088155, + # "foreignNotional":1, + # "transactTime":"2017-04-04T22:07:46.035Z", + # "timestamp":"2017-04-04T22:07:46.035Z" + # } + # ] + # } + # + messageHash = self.safe_string(message, 'table') + data = self.safe_value(message, 'data', []) + dataByExecType = self.group_by(data, 'execType') + rawTrades = self.safe_value(dataByExecType, 'Trade', []) + trades = self.parse_trades(rawTrades) + if self.myTrades is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + self.myTrades = ArrayCacheBySymbolById(limit) + stored = self.myTrades + symbols: dict = {} + for j in range(0, len(trades)): + trade = trades[j] + symbol = trade['symbol'] + stored.append(trade) + symbols[symbol] = trade + numTrades = len(trades) + if numTrades > 0: + client.resolve(stored, messageHash) + keys = list(symbols.keys()) + for i in range(0, len(keys)): + client.resolve(stored, messageHash + ':' + keys[i]) + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://www.bitmex.com/app/wsAPI#OrderBookL2 + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + return await self.watch_order_book_for_symbols([symbol], limit, params) + + async def watch_order_book_for_symbols(self, symbols: List[str], limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://www.bitmex.com/app/wsAPI#OrderBookL2 + + :param str[] symbols: unified array of symbols + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + table = None + if limit is None: + table = self.safe_string(self.options, 'watchOrderBookLevel', 'orderBookL2') + elif limit == 25: + table = 'orderBookL2_25' + elif limit == 10: + table = 'orderBookL10' + else: + raise ExchangeError(self.id + ' watchOrderBookForSymbols limit argument must be None(L2), 25(L2) or 10(L3)') + await self.load_markets() + symbols = self.market_symbols(symbols) + topics = [] + messageHashes = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + market = self.market(symbol) + topic = table + ':' + market['id'] + topics.append(topic) + messageHash = table + ':' + symbol + messageHashes.append(messageHash) + url = self.urls['api']['ws'] + request: dict = { + 'op': 'subscribe', + 'args': topics, + } + orderbook = await self.watch_multiple(url, messageHashes, self.deep_extend(request, params), topics) + return orderbook.limit() + + async def watch_trades_for_symbols(self, symbols: List[str], since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a list of symbols + + https://www.bitmex.com/app/wsAPI#Subscriptions + + :param str[] symbols: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, False) + table = 'trade' + topics = [] + messageHashes = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + market = self.market(symbol) + topic = table + ':' + market['id'] + topics.append(topic) + messageHash = table + ':' + symbol + messageHashes.append(messageHash) + url = self.urls['api']['ws'] + request: dict = { + 'op': 'subscribe', + 'args': topics, + } + trades = await self.watch_multiple(url, messageHashes, self.deep_extend(request, params), topics) + if self.newUpdates: + first = self.safe_value(trades, 0) + tradeSymbol = self.safe_string(first, 'symbol') + limit = trades.getLimit(tradeSymbol, limit) + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + async def watch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + watches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://www.bitmex.com/app/wsAPI#Subscriptions + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + table = 'tradeBin' + self.safe_string(self.timeframes, timeframe, timeframe) + messageHash = table + ':' + market['id'] + url = self.urls['api']['ws'] + request: dict = { + 'op': 'subscribe', + 'args': [ + messageHash, + ], + } + ohlcv = await self.watch(url, messageHash, self.extend(request, params), messageHash) + if self.newUpdates: + limit = ohlcv.getLimit(symbol, limit) + return self.filter_by_since_limit(ohlcv, since, limit, 0, True) + + def handle_ohlcv(self, client: Client, message): + # + # { + # "table": "tradeBin1m", + # "action": "partial", + # "keys": [], + # "types": { + # "timestamp": "timestamp", + # "symbol": "symbol", + # "open": "float", + # "high": "float", + # "low": "float", + # "close": "float", + # "trades": "long", + # "volume": "long", + # "vwap": "float", + # "lastSize": "long", + # "turnover": "long", + # "homeNotional": "float", + # "foreignNotional": "float" + # }, + # "foreignKeys": {symbol: "instrument"}, + # "attributes": {timestamp: "sorted", symbol: "grouped"}, + # "filter": {symbol: "XBTUSD"}, + # "data": [ + # { + # "timestamp": "2020-02-03T01:13:00.000Z", + # "symbol": "XBTUSD", + # "open": 9395, + # "high": 9395.5, + # "low": 9394.5, + # "close": 9395, + # "trades": 221, + # "volume": 839204, + # "vwap": 9394.9643, + # "lastSize": 1874, + # "turnover": 8932641535, + # "homeNotional": 89.32641534999999, + # "foreignNotional": 839204 + # } + # ] + # } + # + # + # { + # "table": "tradeBin1m", + # "action": "insert", + # "data": [ + # { + # "timestamp": "2020-02-03T18:28:00.000Z", + # "symbol": "XBTUSD", + # "open": 9256, + # "high": 9256.5, + # "low": 9256, + # "close": 9256, + # "trades": 29, + # "volume": 79057, + # "vwap": 9256.688, + # "lastSize": 100, + # "turnover": 854077082, + # "homeNotional": 8.540770820000002, + # "foreignNotional": 79057 + # } + # ] + # } + # + table = self.safe_string(message, 'table') + interval = table.replace('tradeBin', '') + timeframe = self.find_timeframe(interval) + duration = self.parse_timeframe(timeframe) + candles = self.safe_value(message, 'data', []) + results: dict = {} + for i in range(0, len(candles)): + candle = candles[i] + marketId = self.safe_string(candle, 'symbol') + market = self.safe_market(marketId) + symbol = market['symbol'] + messageHash = table + ':' + market['id'] + result = [ + self.parse8601(self.safe_string(candle, 'timestamp')) - duration * 1000, + None, # set open price to None, see: https://github.com/ccxt/ccxt/pull/21356#issuecomment-1969565862 + self.safe_float(candle, 'high'), + self.safe_float(candle, 'low'), + self.safe_float(candle, 'close'), + self.safe_float(candle, 'volume'), + ] + self.ohlcvs[symbol] = self.safe_value(self.ohlcvs, symbol, {}) + stored = self.safe_value(self.ohlcvs[symbol], timeframe) + if stored is None: + limit = self.safe_integer(self.options, 'OHLCVLimit', 1000) + stored = ArrayCacheByTimestamp(limit) + self.ohlcvs[symbol][timeframe] = stored + stored.append(result) + results[messageHash] = stored + messageHashes = list(results.keys()) + for i in range(0, len(messageHashes)): + messageHash = messageHashes[i] + client.resolve(results[messageHash], messageHash) + + async def watch_heartbeat(self, params={}): + await self.load_markets() + event = 'heartbeat' + url = self.urls['api']['ws'] + return await self.watch(url, event) + + def handle_order_book(self, client: Client, message): + # + # first snapshot + # + # { + # "table": "orderBookL2", + # "action": "partial", + # "keys": ['symbol', "id", "side"], + # "types": { + # "symbol": "symbol", + # "id": "long", + # "side": "symbol", + # "size": "long", + # "price": "float" + # }, + # "foreignKeys": {symbol: "instrument", side: "side"}, + # "attributes": {symbol: "parted", id: "sorted"}, + # "filter": {symbol: "XBTUSD"}, + # "data": [ + # {symbol: "XBTUSD", id: 8700000100, side: "Sell", size: 1, price: 999999}, + # {symbol: "XBTUSD", id: 8700000200, side: "Sell", size: 3, price: 999998}, + # {symbol: "XBTUSD", id: 8716991250, side: "Sell", size: 26, price: 830087.5}, + # {symbol: "XBTUSD", id: 8728701950, side: "Sell", size: 1720, price: 712980.5}, + # ] + # } + # + # subsequent updates + # + # { + # "table": "orderBookL2", + # "action": "update", + # "data": [ + # { + # "table": "orderBookL2", + # "action": "insert", + # "data": [ + # { + # "symbol": "ETH_USDT", + # "id": 85499965912, + # "side": "Buy", + # "size": 83000000, + # "price": 1704.4, + # "timestamp": "2023-03-26T22:29:00.299Z" + # } + # ] + # } + # ... + # ] + # } + # + action = self.safe_string(message, 'action') + table = self.safe_string(message, 'table') + if table is None: + return # protecting from weird updates + data = self.safe_value(message, 'data', []) + # if it's an initial snapshot + if action == 'partial': + filter = self.safe_dict(message, 'filter', {}) + marketId = self.safe_value(filter, 'symbol') + if marketId is None: + return # protecting from weird update + market = self.safe_market(marketId) + symbol = market['symbol'] + if table == 'orderBookL2': + self.orderbooks[symbol] = self.indexed_order_book() + elif table == 'orderBookL2_25': + self.orderbooks[symbol] = self.indexed_order_book({}, 25) + elif table == 'orderBook10': + self.orderbooks[symbol] = self.indexed_order_book({}, 10) + orderbook = self.orderbooks[symbol] + orderbook['symbol'] = symbol + for i in range(0, len(data)): + price = self.safe_float(data[i], 'price') + size = self.convertFromRawQuantity(symbol, self.safe_string(data[i], 'size')) + id = self.safe_string(data[i], 'id') + side = self.safe_string(data[i], 'side') + side = 'bids' if (side == 'Buy') else 'asks' + bookside = orderbook[side] + bookside.storeArray([price, size, id]) + datetime = self.safe_string(data[i], 'timestamp') + orderbook['timestamp'] = self.parse8601(datetime) + orderbook['datetime'] = datetime + messageHash = table + ':' + symbol + client.resolve(orderbook, messageHash) + else: + numUpdatesByMarketId: dict = {} + for i in range(0, len(data)): + marketId = self.safe_value(data[i], 'symbol') + if marketId is None: + return # protecting from weird update + if not (marketId in numUpdatesByMarketId): + numUpdatesByMarketId[marketId] = 0 + numUpdatesByMarketId[marketId] = self.sum(numUpdatesByMarketId, 1) + market = self.safe_market(marketId) + symbol = market['symbol'] + orderbook = self.orderbooks[symbol] + price = self.safe_number(data[i], 'price') + size = 0 if (action == 'delete') else self.convertFromRawQuantity(symbol, self.safe_string(data[i], 'size', '0')) + id = self.safe_string(data[i], 'id') + side = self.safe_string(data[i], 'side') + side = 'bids' if (side == 'Buy') else 'asks' + bookside = orderbook[side] + bookside.storeArray([price, size, id]) + datetime = self.safe_string(data[i], 'timestamp') + orderbook['timestamp'] = self.parse8601(datetime) + orderbook['datetime'] = datetime + marketIds = list(numUpdatesByMarketId.keys()) + for i in range(0, len(marketIds)): + marketId = marketIds[i] + market = self.safe_market(marketId) + symbol = market['symbol'] + messageHash = table + ':' + symbol + orderbook = self.orderbooks[symbol] + client.resolve(orderbook, messageHash) + + def handle_system_status(self, client: Client, message): + # + # todo answer the question whether handleSystemStatus should be renamed + # and unified for any usage pattern that + # involves system status and maintenance updates + # + # { + # "info": "Welcome to the BitMEX Realtime API.", + # "version": "2019-11-22T00:24:37.000Z", + # "timestamp": "2019-11-23T09:02:27.771Z", + # "docs": "https://www.bitmex.com/app/wsAPI", + # "limit": {remaining: 39} + # } + # + return message + + def handle_subscription_status(self, client: Client, message): + # + # { + # "success": True, + # "subscribe": "orderBookL2:XBTUSD", + # "request": {op: "subscribe", args: ["orderBookL2:XBTUSD"]} + # } + # + return message + + def handle_error_message(self, client: Client, message) -> Bool: + # + # generic error format + # + # {"error": errorMessage} + # + # examples + # + # { + # "status": 429, + # "error": "Rate limit exceeded, retry in 1 seconds.", + # "meta": {"retryAfter": 1}, + # "request": {"op": "subscribe", "args": "orderBook"}, + # } + # + # {"error": "Rate limit exceeded, retry in 29 seconds."} + # + error = self.safe_string(message, 'error') + if error is not None: + request = self.safe_value(message, 'request', {}) + args = self.safe_value(request, 'args', []) + numArgs = len(args) + if numArgs > 0: + messageHash = args[0] + broad = self.exceptions['ws']['broad'] + broadKey = self.find_broadly_matched_key(broad, error) + exception = None + if broadKey is None: + exception = ExchangeError(error) # c# requirement for now + else: + exception = broad[broadKey](error) + client.reject(exception, messageHash) + return False + return True + + def handle_message(self, client: Client, message): + # + # { + # "info": "Welcome to the BitMEX Realtime API.", + # "version": "2019-11-22T00:24:37.000Z", + # "timestamp": "2019-11-23T09:04:42.569Z", + # "docs": "https://www.bitmex.com/app/wsAPI", + # "limit": {remaining: 38} + # } + # + # { + # "success": True, + # "subscribe": "orderBookL2:XBTUSD", + # "request": {op: "subscribe", args: ["orderBookL2:XBTUSD"]} + # } + # + # { + # "table": "orderBookL2", + # "action": "update", + # "data": [ + # {symbol: "XBTUSD", id: 8799284800, side: "Sell", size: 721000}, + # {symbol: "XBTUSD", id: 8799285100, side: "Sell", size: 70590}, + # {symbol: "XBTUSD", id: 8799285550, side: "Sell", size: 217652}, + # {symbol: "XBTUSD", id: 8799285850, side: "Sell", size: 105578}, + # {symbol: "XBTUSD", id: 8799286350, side: "Sell", size: 172093}, + # {symbol: "XBTUSD", id: 8799286650, side: "Sell", size: 201125}, + # {symbol: "XBTUSD", id: 8799288950, side: "Buy", size: 47552}, + # {symbol: "XBTUSD", id: 8799289250, side: "Buy", size: 78217}, + # {symbol: "XBTUSD", id: 8799289700, side: "Buy", size: 193677}, + # {symbol: "XBTUSD", id: 8799290000, side: "Buy", size: 818161}, + # {symbol: "XBTUSD", id: 8799290500, side: "Buy", size: 218806}, + # {symbol: "XBTUSD", id: 8799290800, side: "Buy", size: 102946} + # ] + # } + # + if self.handle_error_message(client, message): + table = self.safe_string(message, 'table') + methods: dict = { + 'orderBookL2': self.handle_order_book, + 'orderBookL2_25': self.handle_order_book, + 'orderBook10': self.handle_order_book, + 'instrument': self.handle_ticker, + 'trade': self.handle_trades, + 'tradeBin1m': self.handle_ohlcv, + 'tradeBin5m': self.handle_ohlcv, + 'tradeBin1h': self.handle_ohlcv, + 'tradeBin1d': self.handle_ohlcv, + 'order': self.handle_orders, + 'execution': self.handle_my_trades, + 'margin': self.handle_balance, + 'liquidation': self.handle_liquidation, + 'position': self.handle_positions, + } + method = self.safe_value(methods, table) + if method is None: + request = self.safe_value(message, 'request', {}) + op = self.safe_value(request, 'op') + if op == 'authKeyExpires': + self.handle_authentication_message(client, message) + else: + method(client, message) diff --git a/ccxt/pro/bitopro.py b/ccxt/pro/bitopro.py new file mode 100644 index 0000000..b94e189 --- /dev/null +++ b/ccxt/pro/bitopro.py @@ -0,0 +1,457 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolById +import hashlib +from ccxt.base.types import Any, Balances, Int, Market, OrderBook, Str, Ticker, Trade +from ccxt.async_support.base.ws.client import Client +from typing import List +from ccxt.base.errors import ExchangeError + + +class bitopro(ccxt.async_support.bitopro): + + def describe(self) -> Any: + return self.deep_extend(super(bitopro, self).describe(), { + 'has': { + 'ws': True, + 'watchBalance': True, + 'watchMyTrades': True, + 'watchOHLCV': False, + 'watchOrderBook': True, + 'watchOrders': False, + 'watchTicker': True, + 'watchTickers': False, + 'watchTrades': True, + 'watchTradesForSymbols': False, + }, + 'urls': { + 'ws': { + 'public': 'wss://stream.bitopro.com:443/ws/v1/pub', + 'private': 'wss://stream.bitopro.com:443/ws/v1/pub/auth', + }, + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + 'login': True, + }, + 'options': { + 'tradesLimit': 1000, + 'ordersLimit': 1000, + 'ws': { + 'options': { + # headers is required for the authentication + 'headers': {}, + }, + }, + }, + }) + + async def watch_public(self, path, messageHash, marketId): + url = self.urls['ws']['public'] + '/' + path + '/' + marketId + return await self.watch(url, messageHash, None, messageHash) + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://github.com/bitoex/bitopro-offical-api-docs/blob/master/ws/public/order_book_stream.md + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + if limit is not None: + if (limit != 5) and (limit != 10) and (limit != 20) and (limit != 50) and (limit != 100) and (limit != 500) and (limit != 1000): + raise ExchangeError(self.id + ' watchOrderBook limit argument must be None, 5, 10, 20, 50, 100, 500 or 1000') + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + messageHash = 'ORDER_BOOK' + ':' + symbol + endPart = None + if limit is None: + endPart = market['id'] + else: + endPart = market['id'] + ':' + self.number_to_string(limit) + orderbook = await self.watch_public('order-books', messageHash, endPart) + return orderbook.limit() + + def handle_order_book(self, client: Client, message): + # + # { + # "event": "ORDER_BOOK", + # "timestamp": 1650121915308, + # "datetime": "2022-04-16T15:11:55.308Z", + # "pair": "BTC_TWD", + # "limit": 5, + # "scale": 0, + # "bids": [ + # {price: "1188178", amount: '0.0425', count: 1, total: "0.0425"}, + # ], + # "asks": [ + # { + # "price": "1190740", + # "amount": "0.40943964", + # "count": 1, + # "total": "0.40943964" + # }, + # ] + # } + # + marketId = self.safe_string(message, 'pair') + market = self.safe_market(marketId, None, '_') + symbol = market['symbol'] + event = self.safe_string(message, 'event') + messageHash = event + ':' + symbol + orderbook = self.safe_value(self.orderbooks, symbol) + if orderbook is None: + orderbook = self.order_book({}) + timestamp = self.safe_integer(message, 'timestamp') + snapshot = self.parse_order_book(message, symbol, timestamp, 'bids', 'asks', 'price', 'amount') + orderbook.reset(snapshot) + client.resolve(orderbook, messageHash) + + async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://github.com/bitoex/bitopro-offical-api-docs/blob/master/ws/public/trade_stream.md + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + messageHash = 'TRADE' + ':' + symbol + trades = await self.watch_public('trades', messageHash, market['id']) + if self.newUpdates: + limit = trades.getLimit(symbol, limit) + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + def handle_trade(self, client: Client, message): + # + # { + # "event": "TRADE", + # "timestamp": 1650116346665, + # "datetime": "2022-04-16T13:39:06.665Z", + # "pair": "BTC_TWD", + # "data": [ + # { + # "event": '', + # "datetime": '', + # "pair": '', + # "timestamp": 1650116227, + # "price": "1189429", + # "amount": "0.0153127", + # "isBuyer": True + # }, + # ] + # } + # + marketId = self.safe_string(message, 'pair') + market = self.safe_market(marketId, None, '_') + symbol = market['symbol'] + event = self.safe_string(message, 'event') + messageHash = event + ':' + symbol + rawData = self.safe_value(message, 'data', []) + trades = self.parse_trades(rawData, market) + tradesCache = self.safe_value(self.trades, symbol) + if tradesCache is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + tradesCache = ArrayCache(limit) + for i in range(0, len(trades)): + tradesCache.append(trades[i]) + self.trades[symbol] = tradesCache + client.resolve(tradesCache, messageHash) + + async def watch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + watches information on multiple trades made by the user + + https://github.com/bitoex/bitopro-offical-api-docs/blob/master/ws/private/matches_stream.md + + :param str symbol: unified market symbol of the market trades were made in + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trade structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + self.check_required_credentials() + await self.load_markets() + messageHash = 'USER_TRADE' + if symbol is not None: + market = self.market(symbol) + messageHash = messageHash + ':' + market['symbol'] + url = self.urls['ws']['private'] + '/' + 'user-trades' + self.authenticate(url) + trades = await self.watch(url, messageHash, None, messageHash) + if self.newUpdates: + limit = trades.getLimit(symbol, limit) + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + def handle_my_trade(self, client: Client, message): + # + # { + # "event": "USER_TRADE", + # "timestamp": 1694667358782, + # "datetime": "2023-09-14T12:55:58.782Z", + # "data": { + # "base": "usdt", + # "quote": "twd", + # "side": "ask", + # "price": "32.039", + # "volume": "1", + # "fee": "6407800", + # "feeCurrency": "twd", + # "transactionTimestamp": 1694667358, + # "eventTimestamp": 1694667358, + # "orderID": 390733918, + # "orderType": "LIMIT", + # "matchID": "bd07673a-94b1-419e-b5ee-d7b723261a5d", + # "isMarket": False, + # "isMaker": False + # } + # } + # + data = self.safe_value(message, 'data', {}) + baseId = self.safe_string(data, 'base') + quoteId = self.safe_string(data, 'quote') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + symbol = self.symbol(base + '/' + quote) + messageHash = self.safe_string(message, 'event') + if self.myTrades is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + self.myTrades = ArrayCacheBySymbolById(limit) + trades = self.myTrades + parsed = self.parse_ws_trade(data) + trades.append(parsed) + client.resolve(trades, messageHash) + client.resolve(trades, messageHash + ':' + symbol) + + def parse_ws_trade(self, trade: dict, market: Market = None) -> Trade: + # + # { + # "base": "usdt", + # "quote": "twd", + # "side": "ask", + # "price": "32.039", + # "volume": "1", + # "fee": "6407800", + # "feeCurrency": "twd", + # "transactionTimestamp": 1694667358, + # "eventTimestamp": 1694667358, + # "orderID": 390733918, + # "orderType": "LIMIT", + # "matchID": "bd07673a-94b1-419e-b5ee-d7b723261a5d", + # "isMarket": False, + # "isMaker": False + # } + # + id = self.safe_string(trade, 'matchID') + orderId = self.safe_string(trade, 'orderID') + timestamp = self.safe_timestamp(trade, 'transactionTimestamp') + baseId = self.safe_string(trade, 'base') + quoteId = self.safe_string(trade, 'quote') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + symbol = self.symbol(base + '/' + quote) + market = self.safe_market(symbol, market) + price = self.safe_string(trade, 'price') + type = self.safe_string_lower(trade, 'orderType') + side = self.safe_string(trade, 'side') + if side is not None: + if side == 'ask': + side = 'sell' + elif side == 'bid': + side = 'buy' + amount = self.safe_string(trade, 'volume') + fee = None + feeAmount = self.safe_string(trade, 'fee') + feeSymbol = self.safe_currency_code(self.safe_string(trade, 'feeCurrency')) + if feeAmount is not None: + fee = { + 'cost': feeAmount, + 'currency': feeSymbol, + 'rate': None, + } + isMaker = self.safe_value(trade, 'isMaker') + takerOrMaker = None + if isMaker is not None: + if isMaker: + takerOrMaker = 'maker' + else: + takerOrMaker = 'taker' + return self.safe_trade({ + 'id': id, + 'info': trade, + 'order': orderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'takerOrMaker': takerOrMaker, + 'type': type, + 'side': side, + 'price': price, + 'amount': amount, + 'cost': None, + 'fee': fee, + }, market) + + async def watch_ticker(self, symbol: str, params={}) -> Ticker: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://github.com/bitoex/bitopro-offical-api-docs/blob/master/ws/public/ticker_stream.md + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + messageHash = 'TICKER' + ':' + symbol + return await self.watch_public('tickers', messageHash, market['id']) + + def handle_ticker(self, client: Client, message): + # + # { + # "event": "TICKER", + # "timestamp": 1650119165710, + # "datetime": "2022-04-16T14:26:05.710Z", + # "pair": "BTC_TWD", + # "lastPrice": "1189110", + # "lastPriceUSD": "40919.1328", + # "lastPriceTWD": "1189110", + # "isBuyer": True, + # "priceChange24hr": "1.23", + # "volume24hr": "7.2090", + # "volume24hrUSD": "294985.5375", + # "volume24hrTWD": "8572279", + # "high24hr": "1193656", + # "low24hr": "1179321" + # } + # + marketId = self.safe_string(message, 'pair') + # market-ids are lowercase in REST API and uppercase in WS API + market = self.safe_market(marketId.lower(), None, '_') + symbol = market['symbol'] + event = self.safe_string(message, 'event') + messageHash = event + ':' + symbol + result = self.parse_ticker(message, market) + result['symbol'] = self.safe_string(market, 'symbol') # symbol returned from REST's parseTicker is distorted for WS, so re-set it from market object + timestamp = self.safe_integer(message, 'timestamp') + result['timestamp'] = timestamp + result['datetime'] = self.iso8601(timestamp) # we shouldn't set "datetime" string provided by server, values are obviously wrong offset from UTC + self.tickers[symbol] = result + client.resolve(result, messageHash) + + def authenticate(self, url): + if (self.clients is not None) and (url in self.clients): + return + self.check_required_credentials() + nonce = self.milliseconds() + rawData = self.json({ + 'nonce': nonce, + 'identity': self.login, + }) + payload = self.string_to_base64(rawData) + signature = self.hmac(self.encode(payload), self.encode(self.secret), hashlib.sha384) + defaultOptions: dict = { + 'ws': { + 'options': { + 'headers': {}, + }, + }, + } + # self.options = self.extend(defaultOptions, self.options) + self.extend_exchange_options(defaultOptions) + originalHeaders = self.options['ws']['options']['headers'] + headers: dict = { + 'X-BITOPRO-API': 'ccxt', + 'X-BITOPRO-APIKEY': self.apiKey, + 'X-BITOPRO-PAYLOAD': payload, + 'X-BITOPRO-SIGNATURE': signature, + } + self.options['ws']['options']['headers'] = headers + # instantiate client + self.client(url) + self.options['ws']['options']['headers'] = originalHeaders + + async def watch_balance(self, params={}) -> Balances: + """ + watch balance and get the amount of funds available for trading or funds locked in orders + + https://github.com/bitoex/bitopro-offical-api-docs/blob/master/ws/private/user_balance_stream.md + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.check_required_credentials() + await self.load_markets() + messageHash = 'ACCOUNT_BALANCE' + url = self.urls['ws']['private'] + '/' + 'account-balance' + self.authenticate(url) + return await self.watch(url, messageHash, None, messageHash) + + def handle_balance(self, client: Client, message): + # + # { + # "event": "ACCOUNT_BALANCE", + # "timestamp": 1650450505715, + # "datetime": "2022-04-20T10:28:25.715Z", + # "data": { + # "ADA": { + # "currency": "ADA", + # "amount": "0", + # "available": "0", + # "stake": "0", + # "tradable": True + # }, + # } + # } + # + event = self.safe_string(message, 'event') + data = self.safe_value(message, 'data') + timestamp = self.safe_integer(message, 'timestamp') + datetime = self.safe_string(message, 'datetime') + currencies = list(data.keys()) + result: dict = { + 'info': data, + 'timestamp': timestamp, + 'datetime': datetime, + } + for i in range(0, len(currencies)): + currency = self.safe_string(currencies, i) + balance = self.safe_value(data, currency) + currencyId = self.safe_string(balance, 'currency') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(balance, 'available') + account['total'] = self.safe_string(balance, 'amount') + result[code] = account + self.balance = self.safe_balance(result) + client.resolve(self.balance, event) + + def handle_message(self, client: Client, message): + methods: dict = { + 'TRADE': self.handle_trade, + 'TICKER': self.handle_ticker, + 'ORDER_BOOK': self.handle_order_book, + 'ACCOUNT_BALANCE': self.handle_balance, + 'USER_TRADE': self.handle_my_trade, + } + event = self.safe_string(message, 'event') + method = self.safe_value(methods, event) + if method is not None: + method(client, message) diff --git a/ccxt/pro/bitrue.py b/ccxt/pro/bitrue.py new file mode 100644 index 0000000..992cddf --- /dev/null +++ b/ccxt/pro/bitrue.py @@ -0,0 +1,447 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCacheBySymbolById +from ccxt.base.types import Any, Balances, Int, Order, OrderBook, Str +from ccxt.async_support.base.ws.client import Client +from typing import List + + +class bitrue(ccxt.async_support.bitrue): + + def describe(self) -> Any: + return self.deep_extend(super(bitrue, self).describe(), { + 'has': { + 'ws': True, + 'watchBalance': True, + 'watchTicker': False, + 'watchTickers': False, + 'watchTrades': False, + 'watchMyTrades': False, + 'watchOrders': True, + 'watchOrderBook': True, + 'watchOHLCV': False, + }, + 'urls': { + 'api': { + 'open': 'https://open.bitrue.com', + 'ws': { + 'public': 'wss://ws.bitrue.com/market/ws', + 'private': 'wss://wsapi.bitrue.com', + }, + }, + }, + 'api': { + 'open': { + 'v1': { + 'private': { + 'post': { + 'poseidon/api/v1/listenKey': 1, + }, + 'put': { + 'poseidon/api/v1/listenKey/{listenKey}': 1, + }, + 'delete': { + 'poseidon/api/v1/listenKey/{listenKey}': 1, + }, + }, + }, + }, + }, + 'options': { + 'listenKeyRefreshRate': 1800000, # 30 mins + 'ws': { + 'gunzip': True, + }, + }, + }) + + async def watch_balance(self, params={}) -> Balances: + """ + watch balance and get the amount of funds available for trading or funds locked in orders + + https://github.com/Bitrue-exchange/Spot-official-api-docs#balance-update + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + url = await self.authenticate() + messageHash = 'balance' + message: dict = { + 'event': 'sub', + 'params': { + 'channel': 'user_balance_update', + }, + } + request = self.deep_extend(message, params) + return await self.watch(url, messageHash, request, messageHash) + + def handle_balance(self, client: Client, message): + # + # { + # "e": "BALANCE", + # "x": "OutboundAccountPositionTradeEvent", + # "E": 1657799510175, + # "I": "302274978401288200", + # "i": 1657799510175, + # "B": [{ + # "a": "btc", + # "F": "0.0006000000000000", + # "T": 1657799510000, + # "f": "0.0006000000000000", + # "t": 0 + # }, + # { + # "a": "usdt", + # "T": 0, + # "L": "0.0000000000000000", + # "l": "-11.8705317318000000", + # "t": 1657799510000 + # } + # ], + # "u": 1814396 + # } + # + # { + # "e": "BALANCE", + # "x": "OutboundAccountPositionOrderEvent", + # "E": 1670051332478, + # "I": "353662845694083072", + # "i": 1670051332478, + # "B": [ + # { + # "a": "eth", + # "F": "0.0400000000000000", + # "T": 1670051332000, + # "f": "-0.0100000000000000", + # "L": "0.0100000000000000", + # "l": "0.0100000000000000", + # "t": 1670051332000 + # } + # ], + # "u": 2285311 + # } + # + balances = self.safe_value(message, 'B', []) + self.parse_ws_balances(balances) + messageHash = 'balance' + client.resolve(self.balance, messageHash) + + def parse_ws_balances(self, balances): + # + # [{ + # "a": "btc", + # "F": "0.0006000000000000", + # "T": 1657799510000, + # "f": "0.0006000000000000", + # "t": 0 + # }, + # { + # "a": "usdt", + # "T": 0, + # "L": "0.0000000000000000", + # "l": "-11.8705317318000000", + # "t": 1657799510000 + # }] + # + self.balance['info'] = balances + for i in range(0, len(balances)): + balance = balances[i] + currencyId = self.safe_string(balance, 'a') + code = self.safe_currency_code(currencyId) + account = self.account() + free = self.safe_string(balance, 'F') + used = self.safe_string(balance, 'L') + balanceUpdateTime = self.safe_integer(balance, 'T', 0) + lockBalanceUpdateTime = self.safe_integer(balance, 't', 0) + updateFree = balanceUpdateTime != 0 + updateUsed = lockBalanceUpdateTime != 0 + if updateFree or updateUsed: + if updateFree: + account['free'] = free + if updateUsed: + account['used'] = used + self.balance[code] = account + self.balance = self.safe_balance(self.balance) + + async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + watches information on user orders + + https://github.com/Bitrue-exchange/Spot-official-api-docs#order-update + + :param str symbol: + :param int [since]: timestamp in ms of the earliest order + :param int [limit]: the maximum amount of orders to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order structure ` indexed by market symbols + """ + await self.load_markets() + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + url = await self.authenticate() + messageHash = 'orders' + message: dict = { + 'event': 'sub', + 'params': { + 'channel': 'user_order_update', + }, + } + request = self.deep_extend(message, params) + orders = await self.watch(url, messageHash, request, messageHash) + if self.newUpdates: + limit = orders.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(orders, symbol, since, limit, True) + + def handle_order(self, client: Client, message): + # + # { + # "e": "ORDER", + # "i": 16122802798, + # "E": 1657882521876, + # "I": "302623154710888464", + # "u": 1814396, + # "s": "btcusdt", + # "S": 2, + # "o": 1, + # "q": "0.0005", + # "p": "60000", + # "X": 0, + # "x": 1, + # "z": "0", + # "n": "0", + # "N": "usdt", + # "O": 1657882521876, + # "L": "0", + # "l": "0", + # "Y": "0" + # } + # + parsed = self.parse_ws_order(message) + if self.orders is None: + limit = self.safe_integer(self.options, 'ordersLimit', 1000) + self.orders = ArrayCacheBySymbolById(limit) + orders = self.orders + orders.append(parsed) + messageHash = 'orders' + client.resolve(self.orders, messageHash) + + def parse_ws_order(self, order, market=None): + # + # { + # "e": "ORDER", + # "i": 16122802798, + # "E": 1657882521876, + # "I": "302623154710888464", + # "u": 1814396, + # "s": "btcusdt", + # "S": 2, + # "o": 1, + # "q": "0.0005", + # "p": "60000", + # "X": 0, + # "x": 1, + # "z": "0", + # "n": "0", + # "N": "usdt", + # "O": 1657882521876, + # "L": "0", + # "l": "0", + # "Y": "0" + # } + # + timestamp = self.safe_integer(order, 'E') + marketId = self.safe_string_upper(order, 's') + typeId = self.safe_string(order, 'o') + sideId = self.safe_integer(order, 'S') + # 1: buy + # 2: sell + side = 'buy' if (sideId == 1) else 'sell' + statusId = self.safe_string(order, 'X') + feeCurrencyId = self.safe_string(order, 'N') + return self.safe_order({ + 'info': order, + 'id': self.safe_string(order, 'i'), + 'clientOrderId': self.safe_string(order, 'c'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': self.safe_integer(order, 'T'), + 'symbol': self.safe_symbol(marketId, market), + 'type': self.parse_ws_order_type(typeId), + 'timeInForce': None, + 'postOnly': None, + 'side': side, + 'price': self.safe_string(order, 'p'), + 'triggerPrice': None, + 'amount': self.safe_string(order, 'q'), + 'cost': self.safe_string(order, 'Y'), + 'average': None, + 'filled': self.safe_string(order, 'z'), + 'remaining': None, + 'status': self.parse_ws_order_status(statusId), + 'fee': { + 'currency': self.safe_currency_code(feeCurrencyId), + 'cost': self.safe_number(order, 'n'), + }, + }, market) + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + messageHash = 'orderbook:' + symbol + marketIdLowercase = market['id'].lower() + channel = 'market_' + marketIdLowercase + '_simple_depth_step0' + url = self.urls['api']['ws']['public'] + message: dict = { + 'event': 'sub', + 'params': { + 'cb_id': marketIdLowercase, + 'channel': channel, + }, + } + request = self.deep_extend(message, params) + return await self.watch(url, messageHash, request, messageHash) + + def handle_order_book(self, client: Client, message): + # + # { + # "channel": "market_ethbtc_simple_depth_step0", + # "ts": 1670056708670, + # "tick": { + # "buys": [ + # [ + # "0.075170", + # "67.153" + # ], + # [ + # "0.075169", + # "17.195" + # ], + # [ + # "0.075166", + # "29.788" + # ], + # ] + # "asks": [ + # [ + # "0.075171", + # "0.256" + # ], + # [ + # "0.075172", + # "0.160" + # ], + # ] + # } + # } + # + channel = self.safe_string(message, 'channel') + parts = channel.split('_') + marketId = self.safe_string_upper(parts, 1) + market = self.safe_market(marketId) + symbol = market['symbol'] + timestamp = self.safe_integer(message, 'ts') + tick = self.safe_value(message, 'tick', {}) + if not (symbol in self.orderbooks): + self.orderbooks[symbol] = self.order_book() + orderbook = self.orderbooks[symbol] + snapshot = self.parse_order_book(tick, symbol, timestamp, 'buys', 'asks') + orderbook.reset(snapshot) + messageHash = 'orderbook:' + symbol + client.resolve(orderbook, messageHash) + + def parse_ws_order_type(self, typeId): + types: dict = { + '1': 'limit', + '2': 'market', + '3': 'limit', + } + return self.safe_string(types, typeId, typeId) + + def parse_ws_order_status(self, status): + statuses: dict = { + '0': 'open', # The order has not been accepted by the engine. + '1': 'open', # The order has been accepted by the engine. + '2': 'closed', # The order has been completed. + '3': 'open', # A part of the order has been filled. + '4': 'canceled', # The order has been canceled. + '7': 'open', # Stop order placed. + } + return self.safe_string(statuses, status, status) + + def handle_ping(self, client: Client, message): + self.spawn(self.pong, client, message) + + async def pong(self, client, message): + # + # { + # "ping": 1670057540627 + # } + # + time = self.safe_integer(message, 'ping') + pong: dict = { + 'pong': time, + } + await client.send(pong) + + def handle_message(self, client: Client, message): + if 'channel' in message: + self.handle_order_book(client, message) + elif 'ping' in message: + self.handle_ping(client, message) + else: + event = self.safe_string(message, 'e') + handlers: dict = { + 'BALANCE': self.handle_balance, + 'ORDER': self.handle_order, + } + handler = self.safe_value(handlers, event) + if handler is not None: + handler(client, message) + + async def authenticate(self, params={}): + listenKey = self.safe_value(self.options, 'listenKey') + if listenKey is None: + response = await self.openV1PrivatePostPoseidonApiV1ListenKey(params) + # + # { + # "msg": "succ", + # "code": 200, + # "data": { + # "listenKey": "7d1ec51340f499d85bb33b00a96ef680bda28869d5c3374a444c5ca4847d1bf0" + # } + # } + # + data = self.safe_value(response, 'data', {}) + key = self.safe_string(data, 'listenKey') + self.options['listenKey'] = key + self.options['listenKeyUrl'] = self.urls['api']['ws']['private'] + '/stream?listenKey=' + key + refreshTimeout = self.safe_integer(self.options, 'listenKeyRefreshRate', 1800000) + self.delay(refreshTimeout, self.keep_alive_listen_key) + return self.options['listenKeyUrl'] + + async def keep_alive_listen_key(self, params={}): + listenKey = self.safe_string(self.options, 'listenKey') + request: dict = { + 'listenKey': listenKey, + } + try: + await self.openV1PrivatePutPoseidonApiV1ListenKeyListenKey(self.extend(request, params)) + # + # ಠ_ಠ + # { + # "msg": "succ", + # "code": "200" + # } + # + except Exception as error: + self.options['listenKey'] = None + self.options['listenKeyUrl'] = None + return + refreshTimeout = self.safe_integer(self.options, 'listenKeyRefreshRate', 1800000) + self.delay(refreshTimeout, self.keep_alive_listen_key) diff --git a/ccxt/pro/bitstamp.py b/ccxt/pro/bitstamp.py new file mode 100644 index 0000000..ff8619d --- /dev/null +++ b/ccxt/pro/bitstamp.py @@ -0,0 +1,555 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolById +from ccxt.base.types import Any, Bool, Int, Order, OrderBook, Str, Trade +from ccxt.async_support.base.ws.client import Client +from typing import List +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.precise import Precise + + +class bitstamp(ccxt.async_support.bitstamp): + + def describe(self) -> Any: + return self.deep_extend(super(bitstamp, self).describe(), { + 'has': { + 'ws': True, + 'watchOrderBook': True, + 'watchOrders': True, + 'watchTrades': True, + 'watchTradesForSymbols': False, + 'watchOHLCV': False, + 'watchTicker': False, + 'watchTickers': False, + }, + 'urls': { + 'api': { + 'ws': 'wss://ws.bitstamp.net', + }, + }, + 'options': { + 'expiresIn': '', + 'userId': '', + 'wsSessionToken': '', + 'watchOrderBook': { + 'snapshotDelay': 6, + 'snapshotMaxRetries': 3, + }, + 'tradesLimit': 1000, + 'OHLCVLimit': 1000, + }, + 'exceptions': { + 'exact': { + '4009': AuthenticationError, + }, + }, + }) + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + messageHash = 'orderbook:' + symbol + channel = 'diff_order_book_' + market['id'] + url = self.urls['api']['ws'] + request: dict = { + 'event': 'bts:subscribe', + 'data': { + 'channel': channel, + }, + } + message = self.extend(request, params) + orderbook = await self.watch(url, messageHash, message, messageHash) + return orderbook.limit() + + def handle_order_book(self, client: Client, message): + # + # initial snapshot is fetched with ccxt's fetchOrderBook + # the feed does not include a snapshot, just the deltas + # + # { + # "data": { + # "timestamp": "1583656800", + # "microtimestamp": "1583656800237527", + # "bids": [ + # ["8732.02", "0.00002478", "1207590500704256"], + # ["8729.62", "0.01600000", "1207590502350849"], + # ["8727.22", "0.01800000", "1207590504296448"], + # ], + # "asks": [ + # ["8735.67", "2.00000000", "1207590693249024"], + # ["8735.67", "0.01700000", "1207590693634048"], + # ["8735.68", "1.53294500", "1207590692048896"], + # ], + # }, + # "event": "data", + # "channel": "diff_order_book_btcusd" + # } + # + channel = self.safe_string(message, 'channel') + parts = channel.split('_') + marketId = self.safe_string(parts, 3) + symbol = self.safe_symbol(marketId) + storedOrderBook = self.safe_value(self.orderbooks, symbol) + nonce = self.safe_value(storedOrderBook, 'nonce') + delta = self.safe_value(message, 'data') + deltaNonce = self.safe_integer(delta, 'microtimestamp') + messageHash = 'orderbook:' + symbol + if nonce is None: + cacheLength = len(storedOrderBook.cache) + # the rest API is very delayed + # usually it takes at least 4-5 deltas to resolve + snapshotDelay = self.handle_option('watchOrderBook', 'snapshotDelay', 6) + if cacheLength == snapshotDelay: + self.spawn(self.load_order_book, client, messageHash, symbol, None, {}) + storedOrderBook.cache.append(delta) + return + elif nonce >= deltaNonce: + return + self.handle_delta(storedOrderBook, delta) + client.resolve(storedOrderBook, messageHash) + + def handle_delta(self, orderbook, delta): + timestamp = self.safe_timestamp(delta, 'timestamp') + orderbook['timestamp'] = timestamp + orderbook['datetime'] = self.iso8601(timestamp) + orderbook['nonce'] = self.safe_integer(delta, 'microtimestamp') + bids = self.safe_value(delta, 'bids', []) + asks = self.safe_value(delta, 'asks', []) + storedBids = orderbook['bids'] + storedAsks = orderbook['asks'] + self.handle_bid_asks(storedBids, bids) + self.handle_bid_asks(storedAsks, asks) + + def handle_bid_asks(self, bookSide, bidAsks): + for i in range(0, len(bidAsks)): + bidAsk = self.parse_bid_ask(bidAsks[i]) + bookSide.storeArray(bidAsk) + + def get_cache_index(self, orderbook, deltas): + # we will consider it a fail + firstElement = deltas[0] + firstElementNonce = self.safe_integer(firstElement, 'microtimestamp') + nonce = self.safe_integer(orderbook, 'nonce') + if nonce < firstElementNonce: + return -1 + for i in range(0, len(deltas)): + delta = deltas[i] + deltaNonce = self.safe_integer(delta, 'microtimestamp') + if deltaNonce == nonce: + return i + 1 + return len(deltas) + + async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + messageHash = 'trades:' + symbol + url = self.urls['api']['ws'] + channel = 'live_trades_' + market['id'] + request: dict = { + 'event': 'bts:subscribe', + 'data': { + 'channel': channel, + }, + } + message = self.extend(request, params) + trades = await self.watch(url, messageHash, message, messageHash) + if self.newUpdates: + limit = trades.getLimit(symbol, limit) + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + def parse_ws_trade(self, trade, market=None): + # + # { + # "buy_order_id": 1211625836466176, + # "amount_str": "1.08000000", + # "timestamp": "1584642064", + # "microtimestamp": "1584642064685000", + # "id": 108637852, + # "amount": 1.08, + # "sell_order_id": 1211625840754689, + # "price_str": "6294.77", + # "type": 1, + # "price": 6294.77 + # } + # + microtimestamp = self.safe_integer(trade, 'microtimestamp') + id = self.safe_string(trade, 'id') + timestamp = self.parse_to_int(microtimestamp / 1000) + price = self.safe_string(trade, 'price') + amount = self.safe_string(trade, 'amount') + symbol = market['symbol'] + sideRaw = self.safe_integer(trade, 'type') + side = 'buy' if (sideRaw == 0) else 'sell' + return self.safe_trade({ + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'id': id, + 'order': None, + 'type': None, + 'takerOrMaker': None, + 'side': side, + 'price': price, + 'amount': amount, + 'cost': None, + 'fee': None, + }, market) + + def handle_trade(self, client: Client, message): + # + # { + # "data": { + # "buy_order_id": 1207733769326592, + # "amount_str": "0.14406384", + # "timestamp": "1583691851", + # "microtimestamp": "1583691851934000", + # "id": 106833903, + # "amount": 0.14406384, + # "sell_order_id": 1207733765476352, + # "price_str": "8302.92", + # "type": 0, + # "price": 8302.92 + # }, + # "event": "trade", + # "channel": "live_trades_btcusd" + # } + # + # the trade streams push raw trade information in real-time + # each trade has a unique buyer and seller + channel = self.safe_string(message, 'channel') + parts = channel.split('_') + marketId = self.safe_string(parts, 2) + market = self.safe_market(marketId) + symbol = market['symbol'] + messageHash = 'trades:' + symbol + data = self.safe_value(message, 'data') + trade = self.parse_ws_trade(data, market) + tradesArray = self.safe_value(self.trades, symbol) + if tradesArray is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + tradesArray = ArrayCache(limit) + self.trades[symbol] = tradesArray + tradesArray.append(trade) + client.resolve(tradesArray, messageHash) + + async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + watches information on multiple orders made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' watchOrders() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + channel = 'private-my_orders' + messageHash = channel + '_' + market['id'] + subscription: dict = { + 'symbol': symbol, + 'limit': limit, + 'type': channel, + 'params': params, + } + orders = await self.subscribe_private(subscription, messageHash, params) + if self.newUpdates: + limit = orders.getLimit(symbol, limit) + return self.filter_by_since_limit(orders, since, limit, 'timestamp', True) + + def handle_orders(self, client: Client, message): + # + # { + # "data":{ + # "id":"1463471322288128", + # "id_str":"1463471322288128", + # "order_type":1, + # "datetime":"1646127778", + # "microtimestamp":"1646127777950000", + # "amount":0.05, + # "amount_str":"0.05000000", + # "price":1000, + # "price_str":"1000.00" + # }, + # "channel":"private-my_orders_ltcusd-4848701", + # "event": "order_deleted" # field only present for cancelOrder + # } + # + channel = self.safe_string(message, 'channel') + order = self.safe_value(message, 'data', {}) + limit = self.safe_integer(self.options, 'ordersLimit', 1000) + if self.orders is None: + self.orders = ArrayCacheBySymbolById(limit) + stored = self.orders + subscription = self.safe_value(client.subscriptions, channel) + symbol = self.safe_string(subscription, 'symbol') + market = self.market(symbol) + order['event'] = self.safe_string(message, 'event') + parsed = self.parse_ws_order(order, market) + stored.append(parsed) + client.resolve(self.orders, channel) + + def parse_ws_order(self, order, market=None): + # + # { + # "id": "1894876776091648", + # "id_str": "1894876776091648", + # "order_type": 0, + # "order_subtype": 0, + # "datetime": "1751451375", + # "microtimestamp": "1751451375070000", + # "amount": 1.1, + # "amount_str": "1.10000000", + # "amount_traded": "0", + # "amount_at_create": "1.10000000", + # "price": 10.23, + # "price_str": "10.23", + # "is_liquidation": False, + # "trade_account_id": 0 + # } + # + id = self.safe_string(order, 'id_str') + orderTypeRaw = self.safe_string_lower(order, 'order_type') + side = 'sell' if (orderTypeRaw == '1') else 'buy' + orderSubTypeRaw = self.safe_string_lower(order, 'order_subtype') # https://www.bitstamp.net/websocket/v2/#:~:text=order_subtype + orderType: Str = None + timeInForce: Str = None + if orderSubTypeRaw == '0': + orderType = 'limit' + elif orderSubTypeRaw == '2': + orderType = 'market' + elif orderSubTypeRaw == '4': + orderType = 'limit' + timeInForce = 'IOC' + elif orderSubTypeRaw == '6': + orderType = 'limit' + timeInForce = 'FOK' + elif orderSubTypeRaw == '8': + orderType = 'limit' + timeInForce = 'GTD' + price = self.safe_string(order, 'price_str') + amount = self.safe_string(order, 'amount_str') + filled = self.safe_string(order, 'amount_traded') + event = self.safe_string(order, 'event') + status = None + if Precise.string_eq(filled, amount): + status = 'closed' + elif event == 'order_deleted': + status = 'canceled' + timestamp = self.safe_timestamp(order, 'datetime') + market = self.safe_market(None, market) + symbol = market['symbol'] + return self.safe_order({ + 'info': order, + 'symbol': symbol, + 'id': id, + 'clientOrderId': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'type': orderType, + 'timeInForce': timeInForce, + 'postOnly': None, + 'side': side, + 'price': price, + 'stopPrice': None, + 'triggerPrice': None, + 'amount': amount, + 'cost': None, + 'average': None, + 'filled': filled, + 'remaining': None, + 'status': status, + 'fee': None, + 'trades': None, + }, market) + + def handle_order_book_subscription(self, client: Client, message): + channel = self.safe_string(message, 'channel') + parts = channel.split('_') + marketId = self.safe_string(parts, 3) + symbol = self.safe_symbol(marketId) + self.orderbooks[symbol] = self.order_book() + + def handle_subscription_status(self, client: Client, message): + # + # { + # "event": "bts:subscription_succeeded", + # "channel": "detail_order_book_btcusd", + # "data": {}, + # } + # { + # "event": "bts:subscription_succeeded", + # "channel": "private-my_orders_ltcusd-4848701", + # "data": {} + # } + # + channel = self.safe_string(message, 'channel') + if channel.find('order_book') > -1: + self.handle_order_book_subscription(client, message) + + def handle_subject(self, client: Client, message): + # + # { + # "data": { + # "timestamp": "1583656800", + # "microtimestamp": "1583656800237527", + # "bids": [ + # ["8732.02", "0.00002478", "1207590500704256"], + # ["8729.62", "0.01600000", "1207590502350849"], + # ["8727.22", "0.01800000", "1207590504296448"], + # ], + # "asks": [ + # ["8735.67", "2.00000000", "1207590693249024"], + # ["8735.67", "0.01700000", "1207590693634048"], + # ["8735.68", "1.53294500", "1207590692048896"], + # ], + # }, + # "event": "data", + # "channel": "detail_order_book_btcusd" + # } + # + # private order + # { + # "data":{ + # "id":"1463471322288128", + # "id_str":"1463471322288128", + # "order_type":1, + # "datetime":"1646127778", + # "microtimestamp":"1646127777950000", + # "amount":0.05, + # "amount_str":"0.05000000", + # "price":1000, + # "price_str":"1000.00" + # }, + # "channel":"private-my_orders_ltcusd-4848701", + # "event": "order_deleted" # field only present for cancelOrder + # } + # + channel = self.safe_string(message, 'channel') + methods: dict = { + 'live_trades': self.handle_trade, + 'diff_order_book': self.handle_order_book, + 'private-my_orders': self.handle_orders, + } + keys = list(methods.keys()) + for i in range(0, len(keys)): + key = keys[i] + if channel.find(key) > -1: + method = methods[key] + method(client, message) + + def handle_error_message(self, client: Client, message) -> Bool: + # { + # "event": "bts:error", + # "channel": '', + # "data": {code: 4009, message: "Connection is unauthorized."} + # } + event = self.safe_string(message, 'event') + if event == 'bts:error': + feedback = self.id + ' ' + self.json(message) + data = self.safe_value(message, 'data', {}) + code = self.safe_number(data, 'code') + self.throw_exactly_matched_exception(self.exceptions['exact'], code, feedback) + return True + + def handle_message(self, client: Client, message): + if not self.handle_error_message(client, message): + return + # + # { + # "event": "bts:subscription_succeeded", + # "channel": "detail_order_book_btcusd", + # "data": {}, + # } + # + # { + # "data": { + # "timestamp": "1583656800", + # "microtimestamp": "1583656800237527", + # "bids": [ + # ["8732.02", "0.00002478", "1207590500704256"], + # ["8729.62", "0.01600000", "1207590502350849"], + # ["8727.22", "0.01800000", "1207590504296448"], + # ], + # "asks": [ + # ["8735.67", "2.00000000", "1207590693249024"], + # ["8735.67", "0.01700000", "1207590693634048"], + # ["8735.68", "1.53294500", "1207590692048896"], + # ], + # }, + # "event": "data", + # "channel": "detail_order_book_btcusd" + # } + # + # { + # "event": "bts:subscription_succeeded", + # "channel": "private-my_orders_ltcusd-4848701", + # "data": {} + # } + # + event = self.safe_string(message, 'event') + if event == 'bts:subscription_succeeded': + self.handle_subscription_status(client, message) + else: + self.handle_subject(client, message) + + async def authenticate(self, params={}): + self.check_required_credentials() + time = self.milliseconds() + expiresIn = self.safe_integer(self.options, 'expiresIn') + if (expiresIn is None) or (time > expiresIn): + response = await self.privatePostWebsocketsToken(params) + # + # { + # "valid_sec":60, + # "token":"siPaT4m6VGQCdsDCVbLBemiphHQs552e", + # "user_id":4848701 + # } + # + sessionToken = self.safe_string(response, 'token') + if sessionToken is not None: + userId = self.safe_string(response, 'user_id') + validity = self.safe_integer_product(response, 'valid_sec', 1000) + self.options['expiresIn'] = self.sum(time, validity) + self.options['userId'] = userId + self.options['wsSessionToken'] = sessionToken + + async def subscribe_private(self, subscription, messageHash, params={}): + url = self.urls['api']['ws'] + await self.authenticate() + messageHash += '-' + self.options['userId'] + request: dict = { + 'event': 'bts:subscribe', + 'data': { + 'channel': messageHash, + 'auth': self.options['wsSessionToken'], + }, + } + subscription['messageHash'] = messageHash + return await self.watch(url, messageHash, self.extend(request, params), messageHash, subscription) diff --git a/ccxt/pro/bittrade.py b/ccxt/pro/bittrade.py new file mode 100644 index 0000000..777876c --- /dev/null +++ b/ccxt/pro/bittrade.py @@ -0,0 +1,571 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheByTimestamp +from ccxt.base.types import Any, Bool, Int, OrderBook, Ticker, Trade +from ccxt.async_support.base.ws.client import Client +from typing import List +from ccxt.base.errors import ExchangeError + + +class bittrade(ccxt.async_support.bittrade): + + def describe(self) -> Any: + return self.deep_extend(super(bittrade, self).describe(), { + 'has': { + 'ws': True, + 'watchOrderBook': True, + 'watchTickers': False, # for now + 'watchTicker': True, + 'watchTrades': True, + 'watchTradesForSymbols': False, + 'watchBalance': False, # for now + 'watchOHLCV': True, + }, + 'urls': { + 'api': { + 'ws': { + 'api': { + 'public': 'wss://{hostname}/ws', + 'private': 'wss://{hostname}/ws/v2', + }, + }, + }, + }, + 'options': { + 'tradesLimit': 1000, + 'OHLCVLimit': 1000, + 'api': 'api', # or api-aws for clients hosted on AWS + 'ws': { + 'gunzip': True, + }, + }, + }) + + def request_id(self): + requestId = self.sum(self.safe_integer(self.options, 'requestId', 0), 1) + self.options['requestId'] = requestId + return str(requestId) + + async def watch_ticker(self, symbol: str, params={}) -> Ticker: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + # only supports a limit of 150 at self time + messageHash = 'market.' + market['id'] + '.detail' + api = self.safe_string(self.options, 'api', 'api') + hostname: dict = {'hostname': self.hostname} + url = self.implode_params(self.urls['api']['ws'][api]['public'], hostname) + requestId = self.request_id() + request: dict = { + 'sub': messageHash, + 'id': requestId, + } + subscription: dict = { + 'id': requestId, + 'messageHash': messageHash, + 'symbol': symbol, + 'params': params, + } + return await self.watch(url, messageHash, self.extend(request, params), messageHash, subscription) + + def handle_ticker(self, client: Client, message): + # + # { + # "ch": "market.btcusdt.detail", + # "ts": 1583494163784, + # "tick": { + # "id": 209988464418, + # "low": 8988, + # "high": 9155.41, + # "open": 9078.91, + # "close": 9136.46, + # "vol": 237813910.5928412, + # "amount": 26184.202558551195, + # "version": 209988464418, + # "count": 265673 + # } + # } + # + tick = self.safe_value(message, 'tick', {}) + ch = self.safe_string(message, 'ch') + parts = ch.split('.') + marketId = self.safe_string(parts, 1) + market = self.safe_market(marketId) + ticker = self.parse_ticker(tick, market) + timestamp = self.safe_value(message, 'ts') + ticker['timestamp'] = timestamp + ticker['datetime'] = self.iso8601(timestamp) + symbol = ticker['symbol'] + self.tickers[symbol] = ticker + client.resolve(ticker, ch) + return message + + async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + # only supports a limit of 150 at self time + messageHash = 'market.' + market['id'] + '.trade.detail' + api = self.safe_string(self.options, 'api', 'api') + hostname: dict = {'hostname': self.hostname} + url = self.implode_params(self.urls['api']['ws'][api]['public'], hostname) + requestId = self.request_id() + request: dict = { + 'sub': messageHash, + 'id': requestId, + } + subscription: dict = { + 'id': requestId, + 'messageHash': messageHash, + 'symbol': symbol, + 'params': params, + } + trades = await self.watch(url, messageHash, self.extend(request, params), messageHash, subscription) + if self.newUpdates: + limit = trades.getLimit(symbol, limit) + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + def handle_trades(self, client: Client, message): + # + # { + # "ch": "market.btcusdt.trade.detail", + # "ts": 1583495834011, + # "tick": { + # "id": 105004645372, + # "ts": 1583495833751, + # "data": [ + # { + # "id": 1.050046453727319e+22, + # "ts": 1583495833751, + # "tradeId": 102090727790, + # "amount": 0.003893, + # "price": 9150.01, + # "direction": "sell" + # } + # ] + # } + # } + # + tick = self.safe_value(message, 'tick', {}) + data = self.safe_value(tick, 'data', {}) + ch = self.safe_string(message, 'ch') + parts = ch.split('.') + marketId = self.safe_string(parts, 1) + market = self.safe_market(marketId) + symbol = market['symbol'] + tradesCache = self.safe_value(self.trades, symbol) + if tradesCache is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + tradesCache = ArrayCache(limit) + self.trades[symbol] = tradesCache + for i in range(0, len(data)): + trade = self.parse_trade(data[i], market) + tradesCache.append(trade) + client.resolve(tradesCache, ch) + return message + + async def watch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + watches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + interval = self.safe_string(self.timeframes, timeframe, timeframe) + messageHash = 'market.' + market['id'] + '.kline.' + interval + api = self.safe_string(self.options, 'api', 'api') + hostname: dict = {'hostname': self.hostname} + url = self.implode_params(self.urls['api']['ws'][api]['public'], hostname) + requestId = self.request_id() + request: dict = { + 'sub': messageHash, + 'id': requestId, + } + subscription: dict = { + 'id': requestId, + 'messageHash': messageHash, + 'symbol': symbol, + 'timeframe': timeframe, + 'params': params, + } + ohlcv = await self.watch(url, messageHash, self.extend(request, params), messageHash, subscription) + if self.newUpdates: + limit = ohlcv.getLimit(symbol, limit) + return self.filter_by_since_limit(ohlcv, since, limit, 0, True) + + def handle_ohlcv(self, client: Client, message): + # + # { + # "ch": "market.btcusdt.kline.1min", + # "ts": 1583501786794, + # "tick": { + # "id": 1583501760, + # "open": 9094.5, + # "close": 9094.51, + # "low": 9094.5, + # "high": 9094.51, + # "amount": 0.44639786263800907, + # "vol": 4059.76919054, + # "count": 16 + # } + # } + # + ch = self.safe_string(message, 'ch') + parts = ch.split('.') + marketId = self.safe_string(parts, 1) + market = self.safe_market(marketId) + symbol = market['symbol'] + interval = self.safe_string(parts, 3) + timeframe = self.find_timeframe(interval) + self.ohlcvs[symbol] = self.safe_value(self.ohlcvs, symbol, {}) + stored = self.safe_value(self.ohlcvs[symbol], timeframe) + if stored is None: + limit = self.safe_integer(self.options, 'OHLCVLimit', 1000) + stored = ArrayCacheByTimestamp(limit) + self.ohlcvs[symbol][timeframe] = stored + tick = self.safe_value(message, 'tick') + parsed = self.parse_ohlcv(tick, market) + stored.append(parsed) + client.resolve(stored, ch) + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + if (limit is not None) and (limit != 150): + raise ExchangeError(self.id + ' watchOrderBook accepts limit = 150 only') + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + # only supports a limit of 150 at self time + limit = 150 if (limit is None) else limit + messageHash = 'market.' + market['id'] + '.mbp.' + str(limit) + api = self.safe_string(self.options, 'api', 'api') + hostname: dict = {'hostname': self.hostname} + url = self.implode_params(self.urls['api']['ws'][api]['public'], hostname) + requestId = self.request_id() + request: dict = { + 'sub': messageHash, + 'id': requestId, + } + subscription: dict = { + 'id': requestId, + 'messageHash': messageHash, + 'symbol': symbol, + 'limit': limit, + 'params': params, + 'method': self.handle_order_book_subscription, + } + orderbook = await self.watch(url, messageHash, self.extend(request, params), messageHash, subscription) + return orderbook.limit() + + def handle_order_book_snapshot(self, client: Client, message, subscription): + # + # { + # "id": 1583473663565, + # "rep": "market.btcusdt.mbp.150", + # "status": "ok", + # "data": { + # "seqNum": 104999417756, + # "bids": [ + # [9058.27, 0], + # [9058.43, 0], + # [9058.99, 0], + # ], + # "asks": [ + # [9084.27, 0.2], + # [9085.69, 0], + # [9085.81, 0], + # ] + # } + # } + # + symbol = self.safe_string(subscription, 'symbol') + messageHash = self.safe_string(subscription, 'messageHash') + orderbook = self.orderbooks[symbol] + data = self.safe_value(message, 'data') + snapshot = self.parse_order_book(data, symbol) + snapshot['nonce'] = self.safe_integer(data, 'seqNum') + orderbook.reset(snapshot) + # unroll the accumulated deltas + messages = orderbook.cache + for i in range(0, len(messages)): + self.handle_order_book_message(client, messages[i], orderbook) + self.orderbooks[symbol] = orderbook + client.resolve(orderbook, messageHash) + + async def watch_order_book_snapshot(self, client, message, subscription): + messageHash = self.safe_string(subscription, 'messageHash') + try: + symbol = self.safe_string(subscription, 'symbol') + limit = self.safe_integer(subscription, 'limit') + params = self.safe_value(subscription, 'params') + api = self.safe_string(self.options, 'api', 'api') + hostname: dict = {'hostname': self.hostname} + url = self.implode_params(self.urls['api']['ws'][api]['public'], hostname) + requestId = self.request_id() + request: dict = { + 'req': messageHash, + 'id': requestId, + } + # self is a temporary subscription by a specific requestId + # it has a very short lifetime until the snapshot is received over ws + snapshotSubscription: dict = { + 'id': requestId, + 'messageHash': messageHash, + 'symbol': symbol, + 'limit': limit, + 'params': params, + 'method': self.handle_order_book_snapshot, + } + orderbook = await self.watch(url, requestId, request, requestId, snapshotSubscription) + return orderbook.limit() + except Exception as e: + del client.subscriptions[messageHash] + client.reject(e, messageHash) + return None + + def handle_delta(self, bookside, delta): + price = self.safe_float(delta, 0) + amount = self.safe_float(delta, 1) + bookside.store(price, amount) + + def handle_deltas(self, bookside, deltas): + for i in range(0, len(deltas)): + self.handle_delta(bookside, deltas[i]) + + def handle_order_book_message(self, client: Client, message, orderbook): + # + # { + # "ch": "market.btcusdt.mbp.150", + # "ts": 1583472025885, + # "tick": { + # "seqNum": 104998984994, + # "prevSeqNum": 104998984977, + # "bids": [ + # [9058.27, 0], + # [9058.43, 0], + # [9058.99, 0], + # ], + # "asks": [ + # [9084.27, 0.2], + # [9085.69, 0], + # [9085.81, 0], + # ] + # } + # } + # + tick = self.safe_value(message, 'tick', {}) + seqNum = self.safe_integer(tick, 'seqNum') + prevSeqNum = self.safe_integer(tick, 'prevSeqNum') + if (prevSeqNum <= orderbook['nonce']) and (seqNum > orderbook['nonce']): + asks = self.safe_value(tick, 'asks', []) + bids = self.safe_value(tick, 'bids', []) + self.handle_deltas(orderbook['asks'], asks) + self.handle_deltas(orderbook['bids'], bids) + orderbook['nonce'] = seqNum + timestamp = self.safe_integer(message, 'ts') + orderbook['timestamp'] = timestamp + orderbook['datetime'] = self.iso8601(timestamp) + return orderbook + + def handle_order_book(self, client: Client, message): + # + # deltas + # + # { + # "ch": "market.btcusdt.mbp.150", + # "ts": 1583472025885, + # "tick": { + # "seqNum": 104998984994, + # "prevSeqNum": 104998984977, + # "bids": [ + # [9058.27, 0], + # [9058.43, 0], + # [9058.99, 0], + # ], + # "asks": [ + # [9084.27, 0.2], + # [9085.69, 0], + # [9085.81, 0], + # ] + # } + # } + # + messageHash = self.safe_string(message, 'ch') + ch = self.safe_value(message, 'ch') + parts = ch.split('.') + marketId = self.safe_string(parts, 1) + symbol = self.safe_symbol(marketId) + orderbook = self.orderbooks[symbol] + if orderbook['nonce'] is None: + orderbook.cache.append(message) + else: + self.handle_order_book_message(client, message, orderbook) + client.resolve(orderbook, messageHash) + + def handle_order_book_subscription(self, client: Client, message, subscription): + symbol = self.safe_string(subscription, 'symbol') + limit = self.safe_integer(subscription, 'limit') + if symbol in self.orderbooks: + del self.orderbooks[symbol] + self.orderbooks[symbol] = self.order_book({}, limit) + # watch the snapshot in a separate async call + self.spawn(self.watch_order_book_snapshot, client, message, subscription) + + def handle_subscription_status(self, client: Client, message): + # + # { + # "id": 1583414227, + # "status": "ok", + # "subbed": "market.btcusdt.mbp.150", + # "ts": 1583414229143 + # } + # + id = self.safe_string(message, 'id') + subscriptionsById = self.index_by(client.subscriptions, 'id') + subscription = self.safe_value(subscriptionsById, id) + if subscription is not None: + method = self.safe_value(subscription, 'method') + if method is not None: + return method(client, message, subscription) + # clean up + if id in client.subscriptions: + del client.subscriptions[id] + return message + + def handle_system_status(self, client: Client, message): + # + # todo: answer the question whether handleSystemStatus should be renamed + # and unified for any usage pattern that + # involves system status and maintenance updates + # + # { + # "id": "1578090234088", # connectId + # "type": "welcome", + # } + # + return message + + def handle_subject(self, client: Client, message): + # + # { + # "ch": "market.btcusdt.mbp.150", + # "ts": 1583472025885, + # "tick": { + # "seqNum": 104998984994, + # "prevSeqNum": 104998984977, + # "bids": [ + # [9058.27, 0], + # [9058.43, 0], + # [9058.99, 0], + # ], + # "asks": [ + # [9084.27, 0.2], + # [9085.69, 0], + # [9085.81, 0], + # ] + # } + # } + # + ch = self.safe_value(message, 'ch') + parts = ch.split('.') + type = self.safe_string(parts, 0) + if type == 'market': + methodName = self.safe_string(parts, 2) + methods: dict = { + 'mbp': self.handle_order_book, + 'detail': self.handle_ticker, + 'trade': self.handle_trades, + 'kline': self.handle_ohlcv, + # ... + } + method = self.safe_value(methods, methodName) + if method is not None: + method(client, message) + + async def pong(self, client, message): + # + # {ping: 1583491673714} + # + await client.send({'pong': self.safe_integer(message, 'ping')}) + + def handle_ping(self, client: Client, message): + self.spawn(self.pong, client, message) + + def handle_error_message(self, client: Client, message) -> Bool: + # + # { + # "ts": 1586323747018, + # "status": "error", + # 'err-code': "bad-request", + # 'err-msg': "invalid mbp.150.symbol linkusdt", + # "id": "2" + # } + # + status = self.safe_string(message, 'status') + if status == 'error': + id = self.safe_string(message, 'id') + subscriptionsById = self.index_by(client.subscriptions, 'id') + subscription = self.safe_value(subscriptionsById, id) + if subscription is not None: + errorCode = self.safe_string(message, 'err-code') + try: + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, self.json(message)) + except Exception as e: + messageHash = self.safe_string(subscription, 'messageHash') + client.reject(e, messageHash) + client.reject(e, id) + if id in client.subscriptions: + del client.subscriptions[id] + return False + return message + + def handle_message(self, client: Client, message): + if self.handle_error_message(client, message): + # + # {"id":1583414227,"status":"ok","subbed":"market.btcusdt.mbp.150","ts":1583414229143} + # + # ________________________ + # + # sometimes bittrade responds with half of a JSON response like + # + # " {"ch":"market.ethbtc.m " + # + # self is passed to handleMessage string since it failed to be decoded + # + if self.safe_string(message, 'id') is not None: + self.handle_subscription_status(client, message) + elif self.safe_string(message, 'ch') is not None: + # route by channel aka topic aka subject + self.handle_subject(client, message) + elif self.safe_string(message, 'ping') is not None: + self.handle_ping(client, message) diff --git a/ccxt/pro/bitvavo.py b/ccxt/pro/bitvavo.py new file mode 100644 index 0000000..31f4d59 --- /dev/null +++ b/ccxt/pro/bitvavo.py @@ -0,0 +1,1394 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolById, ArrayCacheByTimestamp +import hashlib +from ccxt.base.types import Any, Balances, Bool, Currencies, Int, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, TradingFees +from ccxt.async_support.base.ws.client import Client +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import ArgumentsRequired + + +class bitvavo(ccxt.async_support.bitvavo): + + def describe(self) -> Any: + return self.deep_extend(super(bitvavo, self).describe(), { + 'has': { + 'ws': True, + 'cancelOrdersWs': False, + 'fetchTradesWs': False, + 'watchOrderBook': True, + 'watchTrades': True, + 'watchTicker': True, + 'watchTickers': True, + 'watchBidsAsks': True, + 'watchOHLCV': True, + 'watchOrders': True, + 'watchMyTrades': True, + 'cancelAllOrdersWs': True, + 'cancelOrderWs': True, + 'createOrderWs': True, + 'createStopLimitOrderWs': True, + 'createStopMarketOrderWs': True, + 'createStopOrderWs': True, + 'editOrderWs': True, + 'fetchBalanceWs': True, + 'fetchCurrenciesWS': True, + 'fetchDepositAddressWs': False, + 'fetchDepositsWs': True, + 'fetchDepositWithdrawFeesWs': False, + 'fetchMyTradesWs': True, + 'fetchOHLCVWs': True, + 'fetchOpenOrdersWs': True, + 'fetchOrderWs': True, + 'fetchOrderBookWs': False, + 'fetchOrdersWs': True, + 'fetchTickerWs': False, + 'fetchTickersWs': False, + 'fetchTimeWs': False, + 'fetchTradingFeesWs': True, + 'fetchWithdrawalsWs': True, + 'withdrawWs': True, + }, + 'urls': { + 'api': { + 'ws': 'wss://ws.bitvavo.com/v2', + }, + }, + 'options': { + 'supressMultipleWsRequestsError': False, # if True, will not raise an error when using the same messageHash for more than one request. By making False you may receive responses from different requests on the same action + 'tradesLimit': 1000, + 'ordersLimit': 1000, + 'OHLCVLimit': 1000, + }, + }) + + async def watch_public(self, name, symbol, params={}): + await self.load_markets() + market = self.market(symbol) + messageHash = name + '@' + market['id'] + url = self.urls['api']['ws'] + request: dict = { + 'action': 'subscribe', + 'channels': [ + { + 'name': name, + 'markets': [ + market['id'], + ], + }, + ], + } + message = self.extend(request, params) + return await self.watch(url, messageHash, message, messageHash) + + async def watch_public_multiple(self, methodName, channelName: str, symbols, params={}): + await self.load_markets() + symbols = self.market_symbols(symbols) + messageHashes = [methodName] + args = [] + for i in range(0, len(symbols)): + market = self.market(symbols[i]) + args.append(market['id']) + url = self.urls['api']['ws'] + request: dict = { + 'action': 'subscribe', + 'channels': [ + { + 'name': channelName, + 'markets': args, + }, + ], + } + message = self.extend(request, params) + return await self.watch_multiple(url, messageHashes, message, messageHashes) + + async def watch_ticker(self, symbol: str, params={}) -> Ticker: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://docs.bitvavo.com/#tag/Market-data-subscription-WebSocket/paths/~1subscribeTicker24h/post + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + return await self.watch_public('ticker24h', symbol, params) + + async def watch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list + + https://docs.bitvavo.com/#tag/Market-data-subscription-WebSocket/paths/~1subscribeTicker24h/post + + :param str[] [symbols]: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, False) + channel = 'ticker24h' + tickers = await self.watch_public_multiple(channel, channel, symbols, params) + return self.filter_by_array(tickers, 'symbol', symbols) + + def handle_ticker(self, client: Client, message): + # + # { + # "event": "ticker24h", + # "data": [ + # { + # "market": "ETH-EUR", + # "open": "193.5", + # "high": "202.72", + # "low": "192.46", + # "last": "199.01", + # "volume": "3587.05020246", + # "volumeQuote": "708030.17", + # "bid": "199.56", + # "bidSize": "4.14730802", + # "ask": "199.57", + # "askSize": "6.13642074", + # "timestamp": 1590770885217 + # } + # ] + # } + # + self.handle_bid_ask(client, message) + event = self.safe_string(message, 'event') + tickers = self.safe_value(message, 'data', []) + result = [] + for i in range(0, len(tickers)): + data = tickers[i] + marketId = self.safe_string(data, 'market') + market = self.safe_market(marketId, None, '-') + messageHash = event + '@' + marketId + ticker = self.parse_ticker(data, market) + symbol = ticker['symbol'] + self.tickers[symbol] = ticker + result.append(ticker) + client.resolve(ticker, messageHash) + client.resolve(result, event) + + async def watch_bids_asks(self, symbols: Strings = None, params={}) -> Tickers: + """ + watches best bid & ask for symbols + + https://docs.bitvavo.com/#tag/Market-data-subscription-WebSocket/paths/~1subscribeTicker24h/post + + :param str[] symbols: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, False) + channel = 'ticker24h' + tickers = await self.watch_public_multiple('bidask', channel, symbols, params) + return self.filter_by_array(tickers, 'symbol', symbols) + + def handle_bid_ask(self, client: Client, message): + event = 'bidask' + tickers = self.safe_value(message, 'data', []) + result = [] + for i in range(0, len(tickers)): + data = tickers[i] + ticker = self.parse_ws_bid_ask(data) + symbol = ticker['symbol'] + self.bidsasks[symbol] = ticker + result.append(ticker) + messageHash = event + ':' + symbol + client.resolve(ticker, messageHash) + client.resolve(result, event) + + def parse_ws_bid_ask(self, ticker, market=None): + marketId = self.safe_string(ticker, 'market') + market = self.safe_market(marketId, None, '-') + symbol = self.safe_string(market, 'symbol') + timestamp = self.safe_integer(ticker, 'timestamp') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'ask': self.safe_number(ticker, 'ask'), + 'askVolume': self.safe_number(ticker, 'askSize'), + 'bid': self.safe_number(ticker, 'bid'), + 'bidVolume': self.safe_number(ticker, 'bidSize'), + 'info': ticker, + }, market) + + async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + symbol = self.symbol(symbol) + trades = await self.watch_public('trades', symbol, params) + if self.newUpdates: + limit = trades.getLimit(symbol, limit) + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + def handle_trade(self, client: Client, message): + # + # { + # "event": "trade", + # "timestamp": 1590779594547, + # "market": "ETH-EUR", + # "id": "450c3298-f082-4461-9e2c-a0262cc7cc2e", + # "amount": "0.05026233", + # "price": "198.46", + # "side": "buy" + # } + # + marketId = self.safe_string(message, 'market') + market = self.safe_market(marketId, None, '-') + symbol = market['symbol'] + name = 'trades' + messageHash = name + '@' + marketId + trade = self.parse_trade(message, market) + tradesArray = self.safe_value(self.trades, symbol) + if tradesArray is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + tradesArray = ArrayCache(limit) + tradesArray.append(trade) + self.trades[symbol] = tradesArray + client.resolve(tradesArray, messageHash) + + async def watch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + watches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + name = 'candles' + marketId = market['id'] + interval = self.safe_string(self.timeframes, timeframe, timeframe) + messageHash = name + '@' + marketId + '_' + interval + url = self.urls['api']['ws'] + request: dict = { + 'action': 'subscribe', + 'channels': [ + { + 'name': 'candles', + 'interval': [interval], + 'markets': [marketId], + }, + ], + } + message = self.extend(request, params) + ohlcv = await self.watch(url, messageHash, message, messageHash) + if self.newUpdates: + limit = ohlcv.getLimit(symbol, limit) + return self.filter_by_since_limit(ohlcv, since, limit, 0, True) + + def handle_fetch_ohlcv(self, client: Client, message): + # + # { + # action: 'getCandles', + # response: [ + # [1690325820000, '26453', '26453', '26436', '26447', '0.01626246'], + # [1690325760000, '26454', '26454', '26453', '26453', '0.00037707'] + # ] + # } + # + response = self.safe_value(message, 'response') + ohlcv = self.parse_ohlcvs(response, None, None, None) + messageHash = self.safe_string(message, 'requestId') + client.resolve(ohlcv, messageHash) + + def handle_ohlcv(self, client: Client, message): + # + # { + # "event": "candle", + # "market": "BTC-EUR", + # "interval": "1m", + # "candle": [ + # [ + # 1590797160000, + # "8480.9", + # "8480.9", + # "8480.9", + # "8480.9", + # "0.01038628" + # ] + # ] + # } + # + name = 'candles' + marketId = self.safe_string(message, 'market') + market = self.safe_market(marketId, None, '-') + symbol = market['symbol'] + interval = self.safe_string(message, 'interval') + # use a reverse lookup in a static map instead + timeframe = self.find_timeframe(interval) + messageHash = name + '@' + marketId + '_' + interval + candles = self.safe_value(message, 'candle') + self.ohlcvs[symbol] = self.safe_value(self.ohlcvs, symbol, {}) + stored = self.safe_value(self.ohlcvs[symbol], timeframe) + if stored is None: + limit = self.safe_integer(self.options, 'OHLCVLimit', 1000) + stored = ArrayCacheByTimestamp(limit) + self.ohlcvs[symbol][timeframe] = stored + for i in range(0, len(candles)): + candle = candles[i] + parsed = self.parse_ohlcv(candle, market) + stored.append(parsed) + client.resolve(stored, messageHash) + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + name = 'book' + messageHash = name + '@' + market['id'] + url = self.urls['api']['ws'] + request: dict = { + 'action': 'subscribe', + 'channels': [ + { + 'name': name, + 'markets': [ + market['id'], + ], + }, + ], + } + subscription: dict = { + 'messageHash': messageHash, + 'name': name, + 'symbol': symbol, + 'marketId': market['id'], + 'method': self.handle_order_book_subscription, + 'limit': limit, + 'params': params, + } + message = self.extend(request, params) + orderbook = await self.watch(url, messageHash, message, messageHash, subscription) + return orderbook.limit() + + def handle_delta(self, bookside, delta): + price = self.safe_float(delta, 0) + amount = self.safe_float(delta, 1) + bookside.store(price, amount) + + def handle_deltas(self, bookside, deltas): + for i in range(0, len(deltas)): + self.handle_delta(bookside, deltas[i]) + + def handle_order_book_message(self, client: Client, message, orderbook): + # + # { + # "event": "book", + # "market": "BTC-EUR", + # "nonce": 36947383, + # "bids": [ + # ["8477.8", "0"] + # ], + # "asks": [ + # ["8550.9", "0"] + # ] + # } + # + nonce = self.safe_integer(message, 'nonce') + if nonce > orderbook['nonce']: + self.handle_deltas(orderbook['asks'], self.safe_value(message, 'asks', [])) + self.handle_deltas(orderbook['bids'], self.safe_value(message, 'bids', [])) + orderbook['nonce'] = nonce + return orderbook + + def handle_order_book(self, client: Client, message): + # + # { + # "event": "book", + # "market": "BTC-EUR", + # "nonce": 36729561, + # "bids": [ + # ["8513.3", "0"], + # ['8518.8', "0.64236203"], + # ['8513.6', "0.32435481"], + # ], + # "asks": [] + # } + # + event = self.safe_string(message, 'event') + marketId = self.safe_string(message, 'market') + market = self.safe_market(marketId, None, '-') + symbol = market['symbol'] + messageHash = event + '@' + market['id'] + orderbook = self.safe_value(self.orderbooks, symbol) + if orderbook is None: + return + if orderbook['nonce'] is None: + subscription = self.safe_value(client.subscriptions, messageHash, {}) + watchingOrderBookSnapshot = self.safe_value(subscription, 'watchingOrderBookSnapshot') + if watchingOrderBookSnapshot is None: + subscription['watchingOrderBookSnapshot'] = True + client.subscriptions[messageHash] = subscription + options = self.safe_value(self.options, 'watchOrderBookSnapshot', {}) + delay = self.safe_integer(options, 'delay', self.rateLimit) + # fetch the snapshot in a separate async call after a warmup delay + self.delay(delay, self.watch_order_book_snapshot, client, message, subscription) + orderbook.cache.append(message) + else: + self.handle_order_book_message(client, message, orderbook) + client.resolve(orderbook, messageHash) + + async def watch_order_book_snapshot(self, client, message, subscription): + params = self.safe_value(subscription, 'params') + marketId = self.safe_string(subscription, 'marketId') + name = 'getBook' + messageHash = name + '@' + marketId + url = self.urls['api']['ws'] + request: dict = { + 'action': name, + 'market': marketId, + } + orderbook = await self.watch(url, messageHash, self.extend(request, params), messageHash, subscription) + return orderbook.limit() + + def handle_order_book_snapshot(self, client: Client, message): + # + # { + # "action": "getBook", + # "response": { + # "market": "BTC-EUR", + # "nonce": 36946120, + # "bids": [ + # ['8494.9', "0.24399521"], + # ['8494.8', "0.34884085"], + # ['8493.9', "0.14535128"], + # ], + # "asks": [ + # ["8495", "0.46982463"], + # ['8495.1', "0.12178267"], + # ['8496.2', "0.21924143"], + # ] + # } + # } + # + response = self.safe_value(message, 'response') + if response is None: + return + marketId = self.safe_string(response, 'market') + symbol = self.safe_symbol(marketId, None, '-') + name = 'book' + messageHash = name + '@' + marketId + orderbook = self.orderbooks[symbol] + snapshot = self.parse_order_book(response, symbol) + snapshot['nonce'] = self.safe_integer(response, 'nonce') + orderbook.reset(snapshot) + # unroll the accumulated deltas + messages = orderbook.cache + for i in range(0, len(messages)): + messageItem = messages[i] + self.handle_order_book_message(client, messageItem, orderbook) + self.orderbooks[symbol] = orderbook + client.resolve(orderbook, messageHash) + + def handle_order_book_subscription(self, client: Client, message, subscription): + symbol = self.safe_string(subscription, 'symbol') + limit = self.safe_integer(subscription, 'limit') + if symbol in self.orderbooks: + del self.orderbooks[symbol] + self.orderbooks[symbol] = self.order_book({}, limit) + + def handle_order_book_subscriptions(self, client: Client, message, marketIds): + name = 'book' + for i in range(0, len(marketIds)): + marketId = self.safe_string(marketIds, i) + symbol = self.safe_symbol(marketId, None, '-') + messageHash = name + '@' + marketId + if not (symbol in self.orderbooks): + subscription = self.safe_value(client.subscriptions, messageHash) + method = self.safe_value(subscription, 'method') + if method is not None: + method(client, message, subscription) + + async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + watches information on multiple orders made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' watchOrders() requires a symbol argument') + await self.load_markets() + await self.authenticate() + market = self.market(symbol) + symbol = market['symbol'] + marketId = market['id'] + url = self.urls['api']['ws'] + name = 'account' + messageHash = 'order:' + symbol + request: dict = { + 'action': 'subscribe', + 'channels': [ + { + 'name': name, + 'markets': [marketId], + }, + ], + } + orders = await self.watch(url, messageHash, request, messageHash) + if self.newUpdates: + limit = orders.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(orders, symbol, since, limit, True) + + async def watch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + watches information on multiple trades made by the user + :param str symbol: unified market symbol of the market trades were made in + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trade structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' watchMyTrades() requires a symbol argument') + await self.load_markets() + await self.authenticate() + market = self.market(symbol) + symbol = market['symbol'] + marketId = market['id'] + url = self.urls['api']['ws'] + name = 'account' + messageHash = 'myTrades:' + symbol + request: dict = { + 'action': 'subscribe', + 'channels': [ + { + 'name': name, + 'markets': [marketId], + }, + ], + } + trades = await self.watch(url, messageHash, request, messageHash) + if self.newUpdates: + limit = trades.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(trades, symbol, since, limit, True) + + async def create_order_ws(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}) -> Order: + """ + create a trade order + + https://docs.bitvavo.com/#tag/Orders/paths/~1order/post + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float price: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the bitvavo api endpoint + :param str [params.timeInForce]: "GTC", "IOC", or "PO" + :param float [params.stopPrice]: The price at which a trigger order is triggered at + :param float [params.triggerPrice]: The price at which a trigger order is triggered at + :param bool [params.postOnly]: If True, the order will only be posted to the order book and not executed immediately + :param float [params.stopLossPrice]: The price at which a stop loss order is triggered at + :param float [params.takeProfitPrice]: The price at which a take profit order is triggered at + :param str [params.triggerType]: "price" + :param str [params.triggerReference]: "lastTrade", "bestBid", "bestAsk", "midPrice" Only for stop orders: Use self to determine which parameter will trigger the order + :param str [params.selfTradePrevention]: "decrementAndCancel", "cancelOldest", "cancelNewest", "cancelBoth" + :param bool [params.disableMarketProtection]: don't cancel if the next fill price is 10% worse than the best fill price + :param bool [params.responseRequired]: Set self to 'false' when only an acknowledgement of success or failure is required, self is faster. + :returns dict: an `order structure ` + """ + await self.load_markets() + await self.authenticate() + request = self.create_order_request(symbol, type, side, amount, price, params) + return await self.watch_request('privateCreateOrder', request) + + async def edit_order_ws(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}) -> Order: + """ + edit a trade order + + https://docs.bitvavo.com/#tag/Orders/paths/~1order/put + + :param str id: cancel order id + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float [amount]: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the bitvavo api endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + await self.authenticate() + request = self.edit_order_request(id, symbol, type, side, amount, price, params) + return await self.watch_request('privateUpdateOrder', request) + + async def cancel_order_ws(self, id: str, symbol: Str = None, params={}): + """ + + https://docs.bitvavo.com/#tag/Orders/paths/~1order/delete + + cancels an open order + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the bitvavo api endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + await self.authenticate() + request = self.cancel_order_request(id, symbol, params) + return await self.watch_request('privateCancelOrder', request) + + async def cancel_all_orders_ws(self, symbol: Str = None, params={}): + """ + + https://docs.bitvavo.com/#tag/Orders/paths/~1orders/delete + + cancel all open orders + :param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None + :param dict [params]: extra parameters specific to the bitvavo api endpoint + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + await self.authenticate() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['market'] = market['id'] + return await self.watch_request('privateCancelOrders', self.extend(request, params)) + + def handle_multiple_orders(self, client: Client, message): + # + # { + # action: 'privateCancelOrders', + # response: [{ + # orderId: 'd71df826-1130-478a-8741-d219128675b0' + # }] + # } + # + # action = self.safe_string(message, 'action') + response = self.safe_list(message, 'response') + # firstRawOrder = self.safe_value(response, 0, {}) + # marketId = self.safe_string(firstRawOrder, 'market') + orders = self.parse_orders(response) + # messageHash = self.build_message_hash(action, {'market': marketId}) + # client.resolve(orders, messageHash) + # messageHash = self.build_message_hash(action, message) + messageHash = self.safe_string(message, 'requestId') + client.resolve(orders, messageHash) + + async def fetch_order_ws(self, id: str, symbol: Str = None, params={}) -> Order: + """ + + https://docs.bitvavo.com/#tag/General/paths/~1assets/get + + fetches information on an order made by the user + :param str id: the order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the bitvavo api endpoint + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrder() requires a symbol argument') + await self.load_markets() + await self.authenticate() + market = self.market(symbol) + request: dict = { + 'orderId': id, + 'market': market['id'], + } + return await self.watch_request('privateGetOrder', self.extend(request, params)) + + async def fetch_orders_ws(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + + https://docs.bitvavo.com/#tag/Orders/paths/~1orders/get + + fetches information on multiple orders made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of orde structures to retrieve + :param dict [params]: extra parameters specific to the bitvavo api endpoint + :returns Order[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrdersWs() requires a symbol argument') + await self.load_markets() + await self.authenticate() + request = self.fetchOrdersRequest(symbol, since, limit, params) + orders = await self.watch_request('privateGetOrders', request) + return self.filter_by_symbol_since_limit(orders, symbol, since, limit) + + def request_id(self): + ts = str(self.milliseconds()) + randomNumber = self.rand_number(4) + randomPart = str(randomNumber) + return int(ts + randomPart) + + async def watch_request(self, action, request): + messageHash = self.request_id() + messageHashStr = str(messageHash) + request['action'] = action + request['requestId'] = messageHash + url = self.urls['api']['ws'] + return await self.watch(url, messageHashStr, request, messageHashStr) + + async def fetch_open_orders_ws(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the bitvavo api endpoint + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + await self.authenticate() + request: dict = { + # 'market': market['id'], # rate limit 25 without a market, 1 with market specified + } + market = None + if symbol is not None: + market = self.market(symbol) + request['market'] = market['id'] + orders = await self.watch_request('privateGetOrdersOpen', self.extend(request, params)) + return self.filter_by_symbol_since_limit(orders, symbol, since, limit) + + async def fetch_my_trades_ws(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + + https://docs.bitvavo.com/#tag/Trades + + fetch all trades made by the user + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the bitvavo api endpoint + :returns Trade[]: a list of `trade structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMyTradesWs() requires a symbol argument') + await self.load_markets() + await self.authenticate() + request = self.fetchMyTradesRequest(symbol, since, limit, params) + myTrades = await self.watch_request('privateGetTrades', request) + return self.filter_by_symbol_since_limit(myTrades, symbol, since, limit) + + def handle_my_trades(self, client: Client, message): + # + # { + # action: 'privateGetTrades', + # response: [ + # { + # "id": "108c3633-0276-4480-a902-17a01829deae", + # "orderId": "1d671998-3d44-4df4-965f-0d48bd129a1b", + # "timestamp": 1542967486256, + # "market": "BTC-EUR", + # "side": "buy", + # "amount": "0.005", + # "price": "5000.1", + # "taker": True, + # "fee": "0.03", + # "feeCurrency": "EUR", + # "settled": True + # } + # ] + # } + # + # + # action = self.safe_string(message, 'action') + response = self.safe_list(message, 'response') + # marketId = self.safe_string(firstRawTrade, 'market') + trades = self.parse_trades(response, None, None, None) + # messageHash = self.build_message_hash(action, {'market': marketId}) + messageHash = self.safe_string(message, 'requestId') + client.resolve(trades, messageHash) + + async def withdraw_ws(self, code: str, amount: float, address: str, tag: Str = None, params={}): + """ + make a withdrawal + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the bitvavo api endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.check_address(address) + await self.load_markets() + await self.authenticate() + request = self.withdrawRequest(code, amount, address, tag, params) + return await self.watch_request('privateWithdrawAssets', request) + + def handle_withdraw(self, client: Client, message): + # + # { + # action: 'privateWithdrawAssets', + # response: { + # "success": True, + # "symbol": "BTC", + # "amount": "1.5" + # } + # } + # + # action = self.safe_string(message, 'action') + # messageHash = self.build_message_hash(action, message) + messageHash = self.safe_string(message, 'requestId') + response = self.safe_value(message, 'response') + withdraw = self.parse_transaction(response) + client.resolve(withdraw, messageHash) + + async def fetch_withdrawals_ws(self, code: Str = None, since: Int = None, limit: Int = None, params={}): + """ + + https://docs.bitvavo.com/#tag/Account/paths/~1withdrawalHistory/get + + fetch all withdrawals made from an account + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the bitvavo api endpoint + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + await self.authenticate() + request = self.fetchWithdrawalsRequest(code, since, limit, params) + withdraws = await self.watch_request('privateGetWithdrawalHistory', request) + return self.filter_by_currency_since_limit(withdraws, code, since, limit) + + def handle_withdraws(self, client: Client, message): + # + # { + # action: 'privateGetWithdrawalHistory', + # response: [{ + # timestamp: 1689792085000, + # symbol: 'BTC', + # amount: '0.0009', + # fee: '0', + # status: 'completed', + # txId: '7dbadc658d7d59c129de1332c55ee8e08d0ab74432faae03b417b9809c819d1f' + # }, + # ... + # ] + # } + # + # action = self.safe_string(message, 'action') + # messageHash = self.build_message_hash(action, message) + response = self.safe_list(message, 'response') + messageHash = self.safe_string(message, 'requestId') + withdrawals = self.parse_transactions(response, None, None, None, {'type': 'withdrawal'}) + client.resolve(withdrawals, messageHash) + + async def fetch_ohlcv_ws(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + + https://docs.bitvavo.com/#tag/Market-Data/paths/~1{market}~1candles/get + + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the bitvavo api endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + request = self.fetchOHLCVRequest(symbol, timeframe, since, limit, params) + action = 'getCandles' + ohlcv = await self.watch_request(action, request) + return self.filter_by_since_limit(ohlcv, since, limit, 0, True) + + async def fetch_deposits_ws(self, code: Str = None, since: Int = None, limit: Int = None, params={}): + """ + + https://docs.bitvavo.com/#tag/Account/paths/~1depositHistory/get + + fetch all deposits made to an account + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the bitvavo api endpoint + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + await self.authenticate() + request = self.fetchDepositsRequest(code, since, limit, params) + deposits = await self.watch_request('privateGetDepositHistory', request) + return self.filter_by_currency_since_limit(deposits, code, since, limit) + + def handle_deposits(self, client: Client, message): + # + # { + # action: 'privateGetDepositHistory', + # response: [{ + # timestamp: 1689792085000, + # symbol: 'BTC', + # amount: '0.0009', + # fee: '0', + # status: 'completed', + # txId: '7dbadc658d7d59c129de1332c55ee8e08d0ab74432faae03b417b9809c819d1f' + # }, + # ... + # ] + # } + # + response = self.safe_value(message, 'response') + deposits = self.parse_transactions(response, None, None, None, {'type': 'deposit'}) + messageHash = self.safe_string(message, 'requestId') + client.resolve(deposits, messageHash) + + async def fetch_trading_fees_ws(self, params={}) -> TradingFees: + """ + + https://docs.bitvavo.com/#tag/Account/paths/~1account/get + + fetch the trading fees for multiple markets + :param dict [params]: extra parameters specific to the bitvavo api endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + await self.load_markets() + await self.authenticate() + return await self.watch_request('privateGetAccount', params) + + async def fetch_markets_ws(self, params={}): + """ + + https://docs.bitvavo.com/#tag/General/paths/~1markets/get + + retrieves data on all markets for bitvavo + :param dict [params]: extra parameters specific to the exchange api endpoint + :returns dict[]: an array of objects representing market data + """ + return await self.watch_request('getMarkets', params) + + async def fetch_currencies_ws(self, params={}) -> Currencies: + """ + + https://docs.bitvavo.com/#tag/General/paths/~1assets/get + + fetches all available currencies on an exchange + :param dict [params]: extra parameters specific to the bitvavo api endpoint + :returns dict: an associative dictionary of currencies + """ + await self.load_markets() + return await self.watch_request('getAssets', params) + + def handle_fetch_currencies(self, client: Client, message): + # + # { + # action: 'getAssets', + # response: [{ + # symbol: '1INCH', + # name: '1inch', + # decimals: 8, + # depositFee: '0', + # depositConfirmations: 64, + # depositStatus: 'OK', + # withdrawalFee: '13', + # withdrawalMinAmount: '13', + # withdrawalStatus: 'OK', + # networks: [Array], + # message: '' + # }, + # ... + # ] + # } + # + messageHash = self.safe_string(message, 'requestId') + response = self.safe_value(message, 'response') + currencies = self.parse_currencies(response) + client.resolve(currencies, messageHash) + + def handle_trading_fees(self, client, message): + # + # { + # action: 'privateGetAccount', + # response: { + # fees: { + # taker: '0.0025', + # maker: '0.0015', + # volume: '1693.74' + # } + # } + # } + # + messageHash = self.safe_string(message, 'requestId') + response = self.safe_value(message, 'response') + fees = self.parse_trading_fees(response) + client.resolve(fees, messageHash) + + async def fetch_balance_ws(self, params={}) -> Balances: + """ + + https://docs.bitvavo.com/#tag/Account/paths/~1balance/get + + query for balance and get the amount of funds available for trading or funds locked in orders + :param dict [params]: extra parameters specific to the bitvavo api endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + await self.authenticate() + return await self.watch_request('privateGetBalance', params) + + def handle_fetch_balance(self, client: Client, message): + # + # { + # action: 'privateGetBalance', + # response: [{ + # symbol: 'ADA', + # available: '0', + # inOrder: '0' + # }, + # ... + # ] + # } + # + messageHash = self.safe_string(message, 'requestId') + response = self.safe_value(message, 'response', []) + balance = self.parse_balance(response) + client.resolve(balance, messageHash) + + def handle_single_order(self, client: Client, message): + # + # { + # action: 'privateCreateOrder', + # response: { + # orderId: 'd71df826-1130-478a-8741-d219128675b0', + # market: 'BTC-EUR', + # created: 1689792749748, + # updated: 1689792749748, + # status: 'new', + # side: 'sell', + # orderType: 'limit', + # amount: '0.0002', + # amountRemaining: '0.0002', + # price: '37000', + # onHold: '0.0002', + # onHoldCurrency: 'BTC', + # filledAmount: '0', + # filledAmountQuote: '0', + # feePaid: '0', + # feeCurrency: 'EUR', + # fills: [], + # selfTradePrevention: 'decrementAndCancel', + # visible: True, + # timeInForce: 'GTC', + # postOnly: False + # } + # } + # + response = self.safe_value(message, 'response', {}) + order = self.parse_order(response) + messageHash = self.safe_string(message, 'requestId') + client.resolve(order, messageHash) + + def handle_markets(self, client: Client, message): + # + # { + # action: 'getMarkets', + # response: [{ + # market: '1INCH-EUR', + # status: 'trading', + # base: '1INCH', + # quote: 'EUR', + # pricePrecision: 5, + # minOrderInBaseAsset: '2', + # minOrderInQuoteAsset: '5', + # maxOrderInBaseAsset: '1000000000', + # maxOrderInQuoteAsset: '1000000000', + # orderTypes: [Array] + # }, + # ... + # ] + # } + # + response = self.safe_value(message, 'response', {}) + markets = self.parse_markets(response) + messageHash = self.safe_string(message, 'requestId') + client.resolve(markets, messageHash) + + def build_message_hash(self, action, params={}): + methods: dict = { + 'privateCreateOrder': self.action_and_market_message_hash, + 'privateUpdateOrder': self.action_and_order_id_message_hash, + 'privateCancelOrder': self.action_and_order_id_message_hash, + 'privateGetOrder': self.action_and_order_id_message_hash, + 'privateGetTrades': self.action_and_market_message_hash, + } + method = self.safe_value(methods, action) + messageHash = action + if method is not None: + messageHash = method(action, params) + return messageHash + + def action_and_market_message_hash(self, action, params={}): + symbol = self.safe_string(params, 'market', '') + return action + symbol + + def action_and_order_id_message_hash(self, action, params={}): + orderId = self.safe_string(params, 'orderId') + if orderId is None: + raise ExchangeError(self.id + ' privateUpdateOrderMessageHash requires a orderId parameter') + return action + orderId + + def handle_order(self, client: Client, message): + # + # { + # "event": "order", + # "orderId": "f0e5180f-9497-4d05-9dc2-7056e8a2de9b", + # "market": "ETH-EUR", + # "created": 1590948500319, + # "updated": 1590948500319, + # "status": "new", + # "side": "sell", + # "orderType": "limit", + # "amount": "0.1", + # "amountRemaining": "0.1", + # "price": "300", + # "onHold": "0.1", + # "onHoldCurrency": "ETH", + # "selfTradePrevention": "decrementAndCancel", + # "visible": True, + # "timeInForce": "GTC", + # "postOnly": False + # } + # + marketId = self.safe_string(message, 'market') + market = self.safe_market(marketId, None, '-') + symbol = market['symbol'] + messageHash = 'order:' + symbol + order = self.parse_order(message, market) + if self.orders is None: + limit = self.safe_integer(self.options, 'ordersLimit', 1000) + self.orders = ArrayCacheBySymbolById(limit) + orders = self.orders + orders.append(order) + client.resolve(self.orders, messageHash) + + def handle_my_trade(self, client: Client, message): + # + # { + # "event": "fill", + # "timestamp": 1590964470132, + # "market": "ETH-EUR", + # "orderId": "85d082e1-eda4-4209-9580-248281a29a9a", + # "fillId": "861d2da5-aa93-475c-8d9a-dce431bd4211", + # "side": "sell", + # "amount": "0.1", + # "price": "211.46", + # "taker": True, + # "fee": "0.056", + # "feeCurrency": "EUR" + # } + # + marketId = self.safe_string(message, 'market') + market = self.safe_market(marketId, None, '-') + symbol = market['symbol'] + messageHash = 'myTrades:' + symbol + trade = self.parse_trade(message, market) + if self.myTrades is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + self.myTrades = ArrayCache(limit) + tradesArray = self.myTrades + tradesArray.append(trade) + client.resolve(tradesArray, messageHash) + + def handle_subscription_status(self, client: Client, message): + # + # { + # "event": "subscribed", + # "subscriptions": { + # "book": ["BTC-EUR"] + # } + # } + # + subscriptions = self.safe_value(message, 'subscriptions', {}) + methods: dict = { + 'book': self.handle_order_book_subscriptions, + } + names = list(subscriptions.keys()) + for i in range(0, len(names)): + name = names[i] + method = self.safe_value(methods, name) + if method is not None: + subscription = self.safe_value(subscriptions, name) + method(client, message, subscription) + return message + + async def authenticate(self, params={}): + url = self.urls['api']['ws'] + client = self.client(url) + messageHash = 'authenticated' + future = self.safe_value(client.subscriptions, messageHash) + if future is None: + timestamp = self.milliseconds() + stringTimestamp = str(timestamp) + auth = stringTimestamp + 'GET/' + self.version + '/websocket' + signature = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256) + action = 'authenticate' + request: dict = { + 'action': action, + 'key': self.apiKey, + 'signature': signature, + 'timestamp': timestamp, + } + message = self.extend(request, params) + future = await self.watch(url, messageHash, message, messageHash) + client.subscriptions[messageHash] = future + return future + + def handle_authentication_message(self, client: Client, message): + # + # { + # "event": "authenticate", + # "authenticated": True + # } + # + messageHash = 'authenticated' + authenticated = self.safe_bool(message, 'authenticated', False) + if authenticated: + # we resolve the future here permanently so authentication only happens once + client.resolve(message, messageHash) + else: + error = AuthenticationError(self.json(message)) + client.reject(error, messageHash) + # allows further authentication attempts + if messageHash in client.subscriptions: + del client.subscriptions[messageHash] + + def handle_error_message(self, client: Client, message) -> Bool: + # + # { + # action: 'privateCreateOrder', + # market: 'BTC-EUR', + # errorCode: 217, + # error: 'Minimum order size in quote currency is 5 EUR or 0.001 BTC.' + # } + # { + # action: 'privateCreateOrder', + # requestId: '17317539426571916', + # market: 'USDT-EUR', + # errorCode: 216, + # error: 'You do not have sufficient balance to complete self operation.' + # } + # + error = self.safe_string(message, 'error') + code = self.safe_integer(error, 'errorCode') + action = self.safe_string(message, 'action') + buildMessage = self.build_message_hash(action, message) + messageHash = self.safe_string(message, 'requestId', buildMessage) + rejected = False + try: + self.handle_errors(code, error, client.url, '', {}, error, message, {}, {}) + except Exception as e: + rejected = True + client.reject(e, messageHash) + if not rejected: + client.reject(message, messageHash) + return True + return None + + def handle_message(self, client: Client, message): + # + # { + # "event": "subscribed", + # "subscriptions": { + # "book": ["BTC-EUR"] + # } + # } + # + # { + # "event": "book", + # "market": "BTC-EUR", + # "nonce": 36729561, + # "bids": [ + # ["8513.3", "0"], + # ['8518.8', "0.64236203"], + # ['8513.6', "0.32435481"], + # ], + # "asks": [] + # } + # + # { + # "action": "getBook", + # "response": { + # "market": "BTC-EUR", + # "nonce": 36946120, + # "bids": [ + # ['8494.9', "0.24399521"], + # ['8494.8', "0.34884085"], + # ['8493.9', "0.14535128"], + # ], + # "asks": [ + # ["8495", "0.46982463"], + # ['8495.1', "0.12178267"], + # ['8496.2', "0.21924143"], + # ] + # } + # } + # + # { + # "event": "authenticate", + # "authenticated": True + # } + # + error = self.safe_string(message, 'error') + if error is not None: + self.handle_error_message(client, message) + methods: dict = { + 'subscribed': self.handle_subscription_status, + 'book': self.handle_order_book, + 'getBook': self.handle_order_book_snapshot, + 'trade': self.handle_trade, + 'candle': self.handle_ohlcv, + 'ticker24h': self.handle_ticker, + 'authenticate': self.handle_authentication_message, + 'order': self.handle_order, + 'fill': self.handle_my_trade, + 'privateCreateOrder': self.handle_single_order, + 'privateUpdateOrder': self.handle_single_order, + 'privateGetBalance': self.handle_fetch_balance, + 'privateCancelOrders': self.handle_multiple_orders, + 'privateGetOrders': self.handle_multiple_orders, + 'privateGetOrder': self.handle_single_order, + 'privateCancelOrder': self.handle_single_order, + 'privateGetOrdersOpen': self.handle_multiple_orders, + 'privateGetAccount': self.handle_trading_fees, + 'privateGetDepositHistory': self.handle_deposits, + 'privateGetWithdrawalHistory': self.handle_withdraws, + 'privateWithdrawAssets': self.handle_withdraw, + 'privateGetTrades': self.handle_my_trades, + 'getAssets': self.handle_fetch_currencies, + 'getCandles': self.handle_fetch_ohlcv, + 'getMarkets': self.handle_markets, + } + event = self.safe_string(message, 'event') + method = self.safe_value(methods, event) + if method is None: + action = self.safe_string(message, 'action') + method = self.safe_value(methods, action) + if method is not None: + method(client, message) + else: + method(client, message) diff --git a/ccxt/pro/blockchaincom.py b/ccxt/pro/blockchaincom.py new file mode 100644 index 0000000..94b4fec --- /dev/null +++ b/ccxt/pro/blockchaincom.py @@ -0,0 +1,751 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolById, ArrayCacheByTimestamp +from ccxt.base.types import Any, Balances, Int, Order, OrderBook, Str, Ticker, Trade +from ccxt.async_support.base.ws.client import Client +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import NotSupported + + +class blockchaincom(ccxt.async_support.blockchaincom): + + def describe(self) -> Any: + return self.deep_extend(super(blockchaincom, self).describe(), { + 'has': { + 'ws': True, + 'watchBalance': True, + 'watchTicker': True, + 'watchTickers': False, + 'watchTrades': True, + 'watchTradesForSymbols': False, + 'watchMyTrades': False, + 'watchOrders': True, + 'watchOrderBook': True, + 'watchOHLCV': True, + }, + 'urls': { + 'api': { + 'ws': 'wss://ws.blockchain.info/mercury-gateway/v1/ws', + }, + }, + 'options': { + 'ws': { + 'options': { + 'headers': { + 'Origin': 'https://exchange.blockchain.com', + }, + }, + 'noOriginHeader': False, + }, + }, + 'streaming': { + }, + 'exceptions': { + }, + 'timeframes': { + '1m': '60', + '5m': '300', + '15m': '900', + '1h': '3600', + '6h': '21600', + '1d': '86400', + }, + }) + + async def watch_balance(self, params={}) -> Balances: + """ + watch balance and get the amount of funds available for trading or funds locked in orders + + https://exchange.blockchain.com/api/#balances + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.authenticate(params) + messageHash = 'balance' + url = self.urls['api']['ws'] + subscribe: dict = { + 'action': 'subscribe', + 'channel': 'balances', + } + request = self.deep_extend(subscribe, params) + return await self.watch(url, messageHash, request, messageHash, request) + + def handle_balance(self, client: Client, message): + # + # subscribed + # { + # "seqnum": 1, + # "event": "subscribed", + # "channel": "balances", + # "local_currency": "USD", + # "batching": False + # } + # snapshot + # { + # "seqnum": 2, + # "event": "snapshot", + # "channel": "balances", + # "balances": [ + # { + # "currency": "BTC", + # "balance": 0.00366963, + # "available": 0.00266963, + # "balance_local": 38.746779155, + # "available_local": 28.188009155, + # "rate": 10558.77 + # }, + # ... + # ], + # "total_available_local": 65.477864168, + # "total_balance_local": 87.696634168 + # } + # + event = self.safe_string(message, 'event') + if event == 'subscribed': + return + result: dict = {'info': message} + balances = self.safe_value(message, 'balances', []) + for i in range(0, len(balances)): + entry = balances[i] + currencyId = self.safe_string(entry, 'currency') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(entry, 'available') + account['total'] = self.safe_string(entry, 'balance') + result[code] = account + messageHash = 'balance' + self.balance = self.safe_balance(result) + client.resolve(self.balance, messageHash) + + async def watch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + watches historical candlestick data containing the open, high, low, and close price, and the volume of a market. + + https://exchange.blockchain.com/api/#prices + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents. Allows '1m', '5m', '15m', '1h', '6h' '1d'. Can only watch one timeframe per symbol. + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + interval = self.safe_string(self.timeframes, timeframe, timeframe) + messageHash = 'ohlcv:' + symbol + request = { + 'action': 'subscribe', + 'channel': 'prices', + 'symbol': market['id'], + 'granularity': self.parse_number(interval), + } + request = self.deep_extend(request, params) + url = self.urls['api']['ws'] + ohlcv = await self.watch(url, messageHash, request, messageHash, request) + if self.newUpdates: + limit = ohlcv.getLimit(symbol, limit) + return self.filter_by_since_limit(ohlcv, since, limit, 0, True) + + def handle_ohlcv(self, client: Client, message): + # + # subscribed + # { + # "seqnum": 0, + # "event": "subscribed", + # "channel": "prices", + # "symbol": "BTC-USDT", + # "granularity": 60 + # } + # + # updated + # { + # "seqnum": 1, + # "event": "updated", + # "channel": "prices", + # "symbol": "BTC-USD", + # "price": [1660085580000, 23185.215, 23185.935, 23164.79, 23169.97, 0] + # } + # + event = self.safe_string(message, 'event') + if event == 'rejected': + jsonMessage = self.json(message) + raise ExchangeError(self.id + ' ' + jsonMessage) + elif event == 'updated': + marketId = self.safe_string(message, 'symbol') + symbol = self.safe_symbol(marketId, None, '-') + messageHash = 'ohlcv:' + symbol + request = self.safe_value(client.subscriptions, messageHash) + timeframeId = self.safe_number(request, 'granularity') + timeframe = self.find_timeframe(timeframeId) + ohlcv = self.safe_value(message, 'price', []) + self.ohlcvs[symbol] = self.safe_value(self.ohlcvs, symbol, {}) + stored = self.safe_value(self.ohlcvs[symbol], timeframe) + if stored is None: + limit = self.safe_integer(self.options, 'OHLCVLimit', 1000) + stored = ArrayCacheByTimestamp(limit) + self.ohlcvs[symbol][timeframe] = stored + stored.append(ohlcv) + client.resolve(stored, messageHash) + elif event != 'subscribed': + raise NotSupported(self.id + ' ' + self.json(message)) + + async def watch_ticker(self, symbol: str, params={}) -> Ticker: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://exchange.blockchain.com/api/#ticker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + url = self.urls['api']['ws'] + messageHash = 'ticker:' + symbol + request = { + 'action': 'subscribe', + 'channel': 'ticker', + 'symbol': market['id'], + } + request = self.deep_extend(request, params) + return await self.watch(url, messageHash, request, messageHash) + + def handle_ticker(self, client: Client, message): + # + # subscribed + # { + # "seqnum": 0, + # "event": "subscribed", + # "channel": "ticker", + # "symbol": "BTC-USD" + # } + # snapshot + # { + # "seqnum": 1, + # "event": "snapshot", + # "channel": "ticker", + # "symbol": "BTC-USD", + # "price_24h": 23071.4, + # "volume_24h": 236.28398636, + # "last_trade_price": 23936.4, + # "mark_price": 23935.335240262 + # } + # update + # { + # "seqnum": 2, + # "event": "updated", + # "channel": "ticker", + # "symbol": "BTC-USD", + # "mark_price": 23935.242443617 + # } + # + event = self.safe_string(message, 'event') + marketId = self.safe_string(message, 'symbol') + market = self.safe_market(marketId) + symbol = market['symbol'] + ticker = None + if event == 'subscribed': + return + elif event == 'snapshot': + ticker = self.parse_ticker(message, market) + elif event == 'updated': + lastTicker = self.safe_value(self.tickers, symbol) + ticker = self.parse_ws_updated_ticker(message, lastTicker, market) + messageHash = 'ticker:' + symbol + self.tickers[symbol] = ticker + client.resolve(ticker, messageHash) + + def parse_ws_updated_ticker(self, ticker, lastTicker=None, market=None): + # + # { + # "seqnum": 2, + # "event": "updated", + # "channel": "ticker", + # "symbol": "BTC-USD", + # "mark_price": 23935.242443617 + # } + # + marketId = self.safe_string(ticker, 'symbol') + symbol = self.safe_symbol(marketId, None, '-') + last = self.safe_string(ticker, 'mark_price') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': None, + 'datetime': None, + 'high': None, + 'low': None, + 'bid': None, + 'bidVolume': None, + 'ask': None, + 'askVolume': None, + 'vwap': None, + 'open': self.safe_string(lastTicker, 'open'), + 'close': None, + 'last': last, + 'previousClose': self.safe_string(lastTicker, 'close'), + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': self.safe_string(lastTicker, 'baseVolume'), + 'quoteVolume': None, + 'info': self.extend(self.safe_value(lastTicker, 'info', {}), ticker), + }, market) + + async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://exchange.blockchain.com/api/#trades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + url = self.urls['api']['ws'] + messageHash = 'trades:' + symbol + request = { + 'action': 'subscribe', + 'channel': 'trades', + 'symbol': market['id'], + } + request = self.deep_extend(request, params) + trades = await self.watch(url, messageHash, request, messageHash, request) + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + def handle_trades(self, client: Client, message): + # + # subscribed + # { + # "seqnum": 0, + # "event": "subscribed", + # "channel": "trades", + # "symbol": "BTC-USDT" + # } + # updates + # { + # "seqnum": 1, + # "event": "updated", + # "channel": "trades", + # "symbol": "BTC-USDT", + # "timestamp": "2022-08-08T17:23:48.163096Z", + # "side": "sell", + # "qty": 0.083523, + # "price": 23940.67, + # "trade_id": "563078810223444" + # } + # + event = self.safe_string(message, 'event') + if event != 'updated': + return + marketId = self.safe_string(message, 'symbol') + symbol = self.safe_symbol(marketId) + market = self.safe_market(marketId) + messageHash = 'trades:' + symbol + stored = self.safe_value(self.trades, symbol) + if stored is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + stored = ArrayCache(limit) + self.trades[symbol] = stored + parsed = self.parse_ws_trade(message, market) + stored.append(parsed) + self.trades[symbol] = stored + client.resolve(self.trades[symbol], messageHash) + + def parse_ws_trade(self, trade, market=None): + # + # { + # "seqnum": 1, + # "event": "updated", + # "channel": "trades", + # "symbol": "BTC-USDT", + # "timestamp": "2022-08-08T17:23:48.163096Z", + # "side": "sell", + # "qty": 0.083523, + # "price": 23940.67, + # "trade_id": "563078810223444" + # } + # + marketId = self.safe_string(trade, 'symbol') + datetime = self.safe_string(trade, 'timestamp') + return self.safe_trade({ + 'id': self.safe_string(trade, 'trade_id'), + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + 'symbol': self.safe_symbol(marketId, market, '-'), + 'order': None, + 'type': None, + 'side': self.safe_string(trade, 'side'), + 'takerOrMaker': None, + 'price': self.safe_string(trade, 'price'), + 'amount': self.safe_string(trade, 'qty'), + 'cost': None, + 'fee': None, + 'info': trade, + }, market) + + async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + watches information on multiple orders made by the user + + https://exchange.blockchain.com/api/#mass-order-status-request-ordermassstatusrequest + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + await self.authenticate() + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + url = self.urls['api']['ws'] + message: dict = { + 'action': 'subscribe', + 'channel': 'trading', + } + messageHash = 'orders' + request = self.deep_extend(message, params) + orders = await self.watch(url, messageHash, request, messageHash) + if self.newUpdates: + limit = orders.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(orders, symbol, since, limit, True) + + def handle_orders(self, client: Client, message): + # + # { + # "seqnum": 1, + # "event": "rejected", + # "channel": "trading", + # "text": "Not subscribed to channel" + # } + # snapshot + # { + # "seqnum": 2, + # "event": "snapshot", + # "channel": "trading", + # "orders": [ + # { + # "orderID": "562965341621940", + # "gwOrderId": 181011136260, + # "clOrdID": "016caf67f7a94508webd", + # "symbol": "BTC-USD", + # "side": "sell", + # "ordType": "limit", + # "orderQty": 0.000675, + # "leavesQty": 0.000675, + # "cumQty": 0, + # "avgPx": 0, + # "ordStatus": "open", + # "timeInForce": "GTC", + # "text": "New order", + # "execType": "0", + # "execID": "21415965325", + # "transactTime": "2022-08-08T23:31:00.550795Z", + # "msgType": 8, + # "lastPx": 0, + # "lastShares": 0, + # "tradeId": "0", + # "fee": 0, + # "price": 30000, + # "marginOrder": False, + # "closePositionOrder": False + # } + # ], + # "positions": [] + # } + # update + # { + # "seqnum": 3, + # "event": "updated", + # "channel": "trading", + # "orderID": "562965341621940", + # "gwOrderId": 181011136260, + # "clOrdID": "016caf67f7a94508webd", + # "symbol": "BTC-USD", + # "side": "sell", + # "ordType": "limit", + # "orderQty": 0.000675, + # "leavesQty": 0.000675, + # "cumQty": 0, + # "avgPx": 0, + # "ordStatus": "cancelled", + # "timeInForce": "GTC", + # "text": "Canceled by User", + # "execType": "4", + # "execID": "21416034921", + # "transactTime": "2022-08-08T23:33:25.727785Z", + # "msgType": 8, + # "lastPx": 0, + # "lastShares": 0, + # "tradeId": "0", + # "fee": 0, + # "price": 30000, + # "marginOrder": False, + # "closePositionOrder": False + # } + # + event = self.safe_string(message, 'event') + messageHash = 'orders' + cachedOrders = self.orders + if cachedOrders is None: + limit = self.safe_integer(self.options, 'ordersLimit', 1000) + self.orders = ArrayCacheBySymbolById(limit) + if event == 'subscribed': + return + elif event == 'rejected': + raise ExchangeError(self.id + ' ' + self.json(message)) + elif event == 'snapshot': + orders = self.safe_value(message, 'orders', []) + for i in range(0, len(orders)): + order = orders[i] + parsedOrder = self.parse_ws_order(order) + cachedOrders.append(parsedOrder) + elif event == 'updated': + parsedOrder = self.parse_ws_order(message) + cachedOrders.append(parsedOrder) + self.orders = cachedOrders + client.resolve(self.orders, messageHash) + + def parse_ws_order(self, order, market=None): + # + # { + # "seqnum": 3, + # "event": "updated", + # "channel": "trading", + # "orderID": "562965341621940", + # "gwOrderId": 181011136260, + # "clOrdID": "016caf67f7a94508webd", + # "symbol": "BTC-USD", + # "side": "sell", + # "ordType": "limit", + # "orderQty": 0.000675, + # "leavesQty": 0.000675, + # "cumQty": 0, + # "avgPx": 0, + # "ordStatus": "cancelled", + # "timeInForce": "GTC", + # "text": "Canceled by User", + # "execType": "4", + # "execID": "21416034921", + # "transactTime": "2022-08-08T23:33:25.727785Z", + # "msgType": 8, + # "lastPx": 0, + # "lastShares": 0, + # "tradeId": "0", + # "fee": 0, + # "price": 30000, + # "marginOrder": False, + # "closePositionOrder": False + # } + # + datetime = self.safe_string(order, 'transactTime') + status = self.safe_string(order, 'ordStatus') + marketId = self.safe_string(order, 'symbol') + market = self.safe_market(marketId, market) + tradeId = self.safe_string(order, 'tradeId') + trades = [] + if tradeId != '0': + trades.append({'id': tradeId}) + return self.safe_order({ + 'id': self.safe_string(order, 'orderID'), + 'clientOrderId': self.safe_string(order, 'clOrdID'), + 'datetime': datetime, + 'timestamp': self.parse8601(datetime), + 'status': self.parse_ws_order_status(status), + 'symbol': self.safe_symbol(marketId, market), + 'type': self.safe_string(order, 'ordType'), # limit, market, stop, stopLimit, trailingStop, fillOrKill + 'timeInForce': self.safe_string(order, 'timeInForce'), + 'postOnly': self.safe_string(order, 'execInst') == 'ALO', + 'side': self.safe_string(order, 'side'), + 'price': self.safe_string(order, 'price'), + 'stopPrice': self.safe_string(order, 'stopPx'), + 'cost': None, + 'amount': self.safe_string(order, 'orderQty'), + 'filled': self.safe_string(order, 'cumQty'), + 'remaining': self.safe_string(order, 'leavesQty'), + 'trades': trades, + 'fee': { + 'rate': None, + 'cost': self.safe_number(order, 'fee'), + 'currency': self.safe_string(market, 'quote'), + }, + 'info': order, + 'lastTradeTimestamp': None, + 'average': self.safe_string(order, 'avgPx'), + }, market) + + def parse_ws_order_status(self, status): + statuses: dict = { + 'pending': 'open', + 'open': 'open', + 'rejected': 'rejected', + 'cancelled': 'canceled', + 'filled': 'closed', + 'partial': 'open', + 'expired': 'expired', + } + return self.safe_string(statuses, status, status) + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://exchange.blockchain.com/api/#l2-order-book + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dictConstructor [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: accepts l2 or l3 for level 2 or level 3 order book + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + url = self.urls['api']['ws'] + type = self.safe_string(params, 'type', 'l2') + params = self.omit(params, 'type') + messageHash = 'orderbook:' + symbol + ':' + type + subscribe: dict = { + 'action': 'subscribe', + 'channel': type, + 'symbol': market['id'], + } + request = self.deep_extend(subscribe, params) + orderbook = await self.watch(url, messageHash, request, messageHash) + return orderbook.limit() + + def handle_order_book(self, client: Client, message): + # + # subscribe + # { + # "seqnum": 0, + # "event": "subscribed", + # "channel": "l2", + # "symbol": "BTC-USDT", + # "batching": False + # } + # snapshot + # { + # "seqnum": 1, + # "event": "snapshot", + # "channel": "l2", + # "symbol": "BTC-USDT", + # "bids": [ + # {num: 1, px: 0.01, qty: 22}, + # ], + # "asks": [ + # {num: 1, px: 23840.26, qty: 0.25}, + # ], + # "timestamp": "2022-08-08T22:03:19.071870Z" + # } + # update + # { + # "seqnum": 2, + # "event": "updated", + # "channel": "l2", + # "symbol": "BTC-USDT", + # "bids": [], + # "asks": [{num: 1, px: 23855.06, qty: 1.04786347}], + # "timestamp": "2022-08-08T22:03:19.014680Z" + # } + # + event = self.safe_string(message, 'event') + if event == 'subscribed': + return + type = self.safe_string(message, 'channel') + marketId = self.safe_string(message, 'symbol') + symbol = self.safe_symbol(marketId) + messageHash = 'orderbook:' + symbol + ':' + type + datetime = self.safe_string(message, 'timestamp') + timestamp = self.parse8601(datetime) + if self.safe_value(self.orderbooks, symbol) is None: + self.orderbooks[symbol] = self.counted_order_book() + orderbook = self.orderbooks[symbol] + if event == 'snapshot': + snapshot = self.parse_order_book(message, symbol, timestamp, 'bids', 'asks', 'px', 'qty', 'num') + orderbook.reset(snapshot) + elif event == 'updated': + asks = self.safe_list(message, 'asks', []) + bids = self.safe_list(message, 'bids', []) + self.handle_deltas(orderbook['asks'], asks) + self.handle_deltas(orderbook['bids'], bids) + orderbook['timestamp'] = timestamp + orderbook['datetime'] = datetime + else: + raise NotSupported(self.id + ' watchOrderBook() does not support ' + event + ' yet') + client.resolve(orderbook, messageHash) + + def handle_delta(self, bookside, delta): + bookArray = self.parse_bid_ask(delta, 'px', 'qty', 'num') + bookside.storeArray(bookArray) + + def handle_deltas(self, bookside, deltas): + for i in range(0, len(deltas)): + self.handle_delta(bookside, deltas[i]) + + def handle_message(self, client: Client, message): + channel = self.safe_string(message, 'channel') + handlers: dict = { + 'ticker': self.handle_ticker, + 'trades': self.handle_trades, + 'prices': self.handle_ohlcv, + 'l2': self.handle_order_book, + 'l3': self.handle_order_book, + 'auth': self.handle_authentication_message, + 'balances': self.handle_balance, + 'trading': self.handle_orders, + } + handler = self.safe_value(handlers, channel) + if handler is not None: + handler(client, message) + return + raise NotSupported(self.id + ' received an unsupported message: ' + self.json(message)) + + def handle_authentication_message(self, client: Client, message): + # + # { + # "seqnum": 0, + # "event": "subscribed", + # "channel": "auth", + # "readOnly": False + # } + # + event = self.safe_string(message, 'event') + if event != 'subscribed': + raise AuthenticationError(self.id + ' received an authentication error: ' + self.json(message)) + future = self.safe_value(client.futures, 'authenticated') + if future is not None: + future.resolve(True) + + async def authenticate(self, params={}): + url = self.urls['api']['ws'] + client = self.client(url) + messageHash = 'authenticated' + future = client.reusableFuture(messageHash) + isAuthenticated = self.safe_value(client.subscriptions, messageHash) + if isAuthenticated is None: + self.check_required_credentials() + request: dict = { + 'action': 'subscribe', + 'channel': 'auth', + 'token': self.secret, + } + return self.watch(url, messageHash, self.extend(request, params), messageHash) + return await future diff --git a/ccxt/pro/blofin.py b/ccxt/pro/blofin.py new file mode 100644 index 0000000..68da0a4 --- /dev/null +++ b/ccxt/pro/blofin.py @@ -0,0 +1,711 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolById, ArrayCacheBySymbolBySide, ArrayCacheByTimestamp +import hashlib +from ccxt.base.types import Any, Balances, Int, Market, Order, OrderBook, Position, Str, Strings, Ticker, Tickers, Trade +from ccxt.async_support.base.ws.client import Client +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import NotSupported + + +class blofin(ccxt.async_support.blofin): + + def describe(self) -> Any: + return self.deep_extend(super(blofin, self).describe(), { + 'has': { + 'ws': True, + 'watchTrades': True, + 'watchTradesForSymbols': True, + 'watchOrderBook': True, + 'watchOrderBookForSymbols': True, + 'watchTicker': True, + 'watchTickers': True, + 'watchBidsAsks': True, + 'watchOHLCV': True, + 'watchOHLCVForSymbols': True, + 'watchOrders': True, + 'watchOrdersForSymbols': True, + 'watchPositions': True, + }, + 'urls': { + 'api': { + 'ws': { + 'swap': { + 'public': 'wss://openapi.blofin.com/ws/public', + 'private': 'wss://openapi.blofin.com/ws/private', + }, + }, + }, + 'test': { + 'ws': { + 'swap': { + 'public': 'wss://demo-trading-openapi.blofin.com/ws/public', + 'private': 'wss://demo-trading-openapi.blofin.com/ws/private', + }, + }, + }, + }, + 'options': { + 'defaultType': 'swap', + 'tradesLimit': 1000, + # orderbook channel can be one from: + # - "books": 200 depth levels will be pushed in the initial full snapshot. Incremental data will be pushed every 100 ms for the changes in the order book during that period of time. + # - "books5": 5 depth levels snapshot will be pushed every time. Snapshot data will be pushed every 100 ms when there are changes in the 5 depth levels snapshot. + 'watchOrderBook': { + 'channel': 'books', + }, + 'watchOrderBookForSymbols': { + 'channel': 'books', + }, + }, + 'streaming': { + 'ping': self.ping, + 'keepAlive': 25000, # 30 seconds max + }, + }) + + def ping(self, client): + return 'ping' + + def handle_pong(self, client: Client, message): + # + # 'pong' + # + client.lastPong = self.milliseconds() + + async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://docs.blofin.com/index.html#ws-trades-channel + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + params['callerMethodName'] = 'watchTrades' + return await self.watch_trades_for_symbols([symbol], since, limit, params) + + async def watch_trades_for_symbols(self, symbols: List[str], since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a list of symbols + + https://docs.blofin.com/index.html#ws-trades-channel + + :param str[] symbols: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + trades = await self.watch_multiple_wrapper(True, 'trades', 'watchTradesForSymbols', symbols, params) + if self.newUpdates: + firstMarket = self.safe_dict(trades, 0) + firstSymbol = self.safe_string(firstMarket, 'symbol') + limit = trades.getLimit(firstSymbol, limit) + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + def handle_trades(self, client: Client, message): + # + # { + # arg: { + # channel: "trades", + # instId: "DOGE-USDT", + # }, + # data : [ + # , + # ... + # ] + # } + # + arg = self.safe_dict(message, 'arg') + channelName = self.safe_string(arg, 'channel') + data = self.safe_list(message, 'data') + if data is None: + return + for i in range(0, len(data)): + rawTrade = data[i] + trade = self.parse_ws_trade(rawTrade) + symbol = trade['symbol'] + stored = self.safe_value(self.trades, symbol) + if stored is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + stored = ArrayCache(limit) + self.trades[symbol] = stored + stored.append(trade) + messageHash = channelName + ':' + symbol + client.resolve(stored, messageHash) + + def parse_ws_trade(self, trade, market: Market = None) -> Trade: + return self.parse_trade(trade, market) + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://docs.blofin.com/index.html#ws-order-book-channel + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + params['callerMethodName'] = 'watchOrderBook' + return await self.watch_order_book_for_symbols([symbol], limit, params) + + async def watch_order_book_for_symbols(self, symbols: List[str], limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://docs.blofin.com/index.html#ws-order-book-channel + + :param str[] symbols: unified array of symbols + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.depth]: the type of order book to subscribe to, default is 'depth/increase100', also accepts 'depth5' or 'depth20' or depth50 + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + callerMethodName = None + callerMethodName, params = self.handle_param_string(params, 'callerMethodName', 'watchOrderBookForSymbols') + channelName = None + channelName, params = self.handle_option_and_params(params, callerMethodName, 'channel', 'books') + # due to some problem, temporarily disable other channels + if channelName != 'books': + raise NotSupported(self.id + ' ' + callerMethodName + '() at self moment ' + channelName + ' is not supported, coming soon') + orderbook = await self.watch_multiple_wrapper(True, channelName, callerMethodName, symbols, params) + return orderbook.limit() + + def handle_order_book(self, client: Client, message): + # + # { + # arg: { + # channel: "books", + # instId: "DOGE-USDT", + # }, + # action: "snapshot", # can be 'snapshot' or 'update' + # data: { + # asks: [ [0.08096, 1], [0.08097, 123], ... ], + # bids: [ [0.08095, 4], [0.08094, 237], ... ], + # ts: "1707491587909", + # prevSeqId: "0", # in case of 'update' there will be some value, less then seqId + # seqId: "3374250786", + # }, + # } + # + arg = self.safe_dict(message, 'arg') + channelName = self.safe_string(arg, 'channel') + data = self.safe_dict(message, 'data') + marketId = self.safe_string(arg, 'instId') + market = self.safe_market(marketId) + symbol = market['symbol'] + messageHash = channelName + ':' + symbol + if not (symbol in self.orderbooks): + self.orderbooks[symbol] = self.order_book() + orderbook = self.orderbooks[symbol] + timestamp = self.safe_integer(data, 'ts') + action = self.safe_string(message, 'action') + if action == 'snapshot': + orderBookSnapshot = self.parse_order_book(data, symbol, timestamp) + orderBookSnapshot['nonce'] = self.safe_integer(data, 'seqId') + orderbook.reset(orderBookSnapshot) + else: + asks = self.safe_list(data, 'asks', []) + bids = self.safe_list(data, 'bids', []) + self.handle_deltas_with_keys(orderbook['asks'], asks) + self.handle_deltas_with_keys(orderbook['bids'], bids) + orderbook['timestamp'] = timestamp + orderbook['datetime'] = self.iso8601(timestamp) + self.orderbooks[symbol] = orderbook + client.resolve(orderbook, messageHash) + + async def watch_ticker(self, symbol: str, params={}) -> Ticker: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://docs.blofin.com/index.html#ws-tickers-channel + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + params['callerMethodName'] = 'watchTicker' + market = self.market(symbol) + symbol = market['symbol'] + result = await self.watch_tickers([symbol], params) + return result[symbol] + + async def watch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list + + https://docs.blofin.com/index.html#ws-tickers-channel + + :param str[] symbols: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + if symbols is None: + raise NotSupported(self.id + ' watchTickers() requires a list of symbols') + ticker = await self.watch_multiple_wrapper(True, 'tickers', 'watchTickers', symbols, params) + if self.newUpdates: + tickers = {} + tickers[ticker['symbol']] = ticker + return tickers + return self.filter_by_array(self.tickers, 'symbol', symbols) + + def handle_ticker(self, client: Client, message): + # + # message + # + # { + # arg: { + # channel: "tickers", + # instId: "DOGE-USDT", + # }, + # data: [ + # + # ], + # } + # + self.handle_bid_ask(client, message) + arg = self.safe_dict(message, 'arg') + channelName = self.safe_string(arg, 'channel') + data = self.safe_list(message, 'data') + for i in range(0, len(data)): + ticker = self.parse_ws_ticker(data[i]) + symbol = ticker['symbol'] + messageHash = channelName + ':' + symbol + self.tickers[symbol] = ticker + client.resolve(self.tickers[symbol], messageHash) + + def parse_ws_ticker(self, ticker, market: Market = None) -> Ticker: + return self.parse_ticker(ticker, market) + + async def watch_bids_asks(self, symbols: Strings = None, params={}) -> Tickers: + """ + watches best bid & ask for symbols + + https://docs.blofin.com/index.html#ws-tickers-channel + + :param str[] symbols: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, False) + firstMarket = self.market(symbols[0]) + channel = 'tickers' + marketType = None + marketType, params = self.handle_market_type_and_params('watchBidsAsks', firstMarket, params) + url = self.implode_hostname(self.urls['api']['ws'][marketType]['public']) + messageHashes = [] + args = [] + for i in range(0, len(symbols)): + market = self.market(symbols[i]) + messageHashes.append('bidask:' + market['symbol']) + args.append({ + 'channel': channel, + 'instId': market['id'], + }) + request = self.get_subscription_request(args) + ticker = await self.watch_multiple(url, messageHashes, self.deep_extend(request, params), messageHashes) + if self.newUpdates: + tickers = {} + tickers[ticker['symbol']] = ticker + return tickers + return self.filter_by_array(self.bidsasks, 'symbol', symbols) + + def handle_bid_ask(self, client: Client, message): + data = self.safe_list(message, 'data') + for i in range(0, len(data)): + ticker = self.parse_ws_bid_ask(data[i]) + symbol = ticker['symbol'] + messageHash = 'bidask:' + symbol + self.bidsasks[symbol] = ticker + client.resolve(ticker, messageHash) + + def parse_ws_bid_ask(self, ticker, market=None): + marketId = self.safe_string(ticker, 'instId') + market = self.safe_market(marketId, market, '-') + symbol = self.safe_string(market, 'symbol') + timestamp = self.safe_integer(ticker, 'ts') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'ask': self.safe_string(ticker, 'askPrice'), + 'askVolume': self.safe_string(ticker, 'askSize'), + 'bid': self.safe_string(ticker, 'bidPrice'), + 'bidVolume': self.safe_string(ticker, 'bidSize'), + 'info': ticker, + }, market) + + async def watch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + watches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + params['callerMethodName'] = 'watchOHLCV' + result = await self.watch_ohlcv_for_symbols([[symbol, timeframe]], since, limit, params) + return result[symbol][timeframe] + + async def watch_ohlcv_for_symbols(self, symbolsAndTimeframes: List[List[str]], since: Int = None, limit: Int = None, params={}): + """ + watches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://docs.blofin.com/index.html#ws-candlesticks-channel + + :param str[][] symbolsAndTimeframes: array of arrays containing unified symbols and timeframes to fetch OHLCV data for, example [['BTC/USDT', '1m'], ['LTC/USDT', '5m']] + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + symbolsLength = len(symbolsAndTimeframes) + if symbolsLength == 0 or not isinstance(symbolsAndTimeframes[0], list): + raise ArgumentsRequired(self.id + " watchOHLCVForSymbols() requires a an array of symbols and timeframes, like [['BTC/USDT', '1m'], ['LTC/USDT', '5m']]") + await self.load_markets() + symbol, timeframe, candles = await self.watch_multiple_wrapper(True, 'candle', 'watchOHLCVForSymbols', symbolsAndTimeframes, params) + if self.newUpdates: + limit = candles.getLimit(symbol, limit) + filtered = self.filter_by_since_limit(candles, since, limit, 0, True) + return self.create_ohlcv_object(symbol, timeframe, filtered) + + def handle_ohlcv(self, client: Client, message): + # + # message + # + # { + # arg: { + # channel: "candle1m", + # instId: "DOGE-USDT", + # }, + # data: [ + # [same object in REST example] + # ], + # } + # + arg = self.safe_dict(message, 'arg') + channelName = self.safe_string(arg, 'channel') + data = self.safe_list(message, 'data') + marketId = self.safe_string(arg, 'instId') + market = self.safe_market(marketId) + symbol = market['symbol'] + interval = channelName.replace('candle', '') + unifiedTimeframe = self.find_timeframe(interval) + self.ohlcvs[symbol] = self.safe_dict(self.ohlcvs, symbol, {}) + stored = self.safe_value(self.ohlcvs[symbol], unifiedTimeframe) + if stored is None: + limit = self.safe_integer(self.options, 'OHLCVLimit', 1000) + stored = ArrayCacheByTimestamp(limit) + self.ohlcvs[symbol][unifiedTimeframe] = stored + for i in range(0, len(data)): + candle = data[i] + parsed = self.parse_ohlcv(candle, market) + stored.append(parsed) + resolveData = [symbol, unifiedTimeframe, stored] + messageHash = 'candle' + interval + ':' + symbol + client.resolve(resolveData, messageHash) + + async def watch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://docs.blofin.com/index.html#ws-account-channel + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + await self.authenticate() + marketType = None + marketType, params = self.handle_market_type_and_params('watchBalance', None, params) + if marketType == 'spot': + raise NotSupported(self.id + ' watchBalance() is not supported for spot markets yet') + messageHash = marketType + ':balance' + sub = { + 'channel': 'account', + } + request = self.get_subscription_request([sub]) + url = self.implode_hostname(self.urls['api']['ws'][marketType]['private']) + return await self.watch(url, messageHash, self.deep_extend(request, params), messageHash) + + def handle_balance(self, client: Client, message): + # + # { + # arg: { + # channel: "account", + # }, + # data: , + # } + # + marketType = 'swap' # for now + if not (marketType in self.balance): + self.balance[marketType] = {} + self.balance[marketType] = self.parse_ws_balance(message) + messageHash = marketType + ':balance' + client.resolve(self.balance[marketType], messageHash) + + def parse_ws_balance(self, message): + return self.parse_balance(message) + + async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + watches information on multiple orders made by the user + + https://docs.blofin.com/index.html#ws-order-channel + https://docs.blofin.com/index.html#ws-algo-orders-channel + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: set to True for trigger orders + :returns dict[]: a list of [order structures]{@link https://docs.ccxt.com/#/?id=order-structure + """ + params['callerMethodName'] = 'watchOrders' + symbolsArray = [symbol] if (symbol is not None) else [] + return await self.watch_orders_for_symbols(symbolsArray, since, limit, params) + + async def watch_orders_for_symbols(self, symbols: List[str], since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + watches information on multiple orders made by the user across multiple symbols + + https://docs.blofin.com/index.html#ws-order-channel + https://docs.blofin.com/index.html#ws-algo-orders-channel + + :param str[] symbols: + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: set to True for trigger orders + :returns dict[]: a list of [order structures]{@link https://docs.ccxt.com/#/?id=order-structure + """ + await self.authenticate() + await self.load_markets() + trigger = self.safe_value_2(params, 'stop', 'trigger') + params = self.omit(params, ['stop', 'trigger']) + channel = 'orders-algo' if trigger else 'orders' + orders = await self.watch_multiple_wrapper(False, channel, 'watchOrdersForSymbols', symbols, params) + if self.newUpdates: + first = self.safe_value(orders, 0) + tradeSymbol = self.safe_string(first, 'symbol') + limit = orders.getLimit(tradeSymbol, limit) + return self.filter_by_since_limit(orders, since, limit, 'timestamp', True) + + def handle_orders(self, client: Client, message): + # + # { + # action: 'update', + # arg: {channel: 'orders'}, + # data: [ + # + # ] + # } + # + if self.orders is None: + limit = self.safe_integer(self.options, 'ordersLimit', 1000) + self.orders = ArrayCacheBySymbolById(limit) + orders = self.orders + arg = self.safe_dict(message, 'arg') + channelName = self.safe_string(arg, 'channel') + data = self.safe_list(message, 'data') + for i in range(0, len(data)): + order = self.parse_ws_order(data[i]) + symbol = order['symbol'] + messageHash = channelName + ':' + symbol + orders.append(order) + client.resolve(orders, messageHash) + client.resolve(orders, channelName) + + def parse_ws_order(self, order, market: Market = None) -> Order: + return self.parse_order(order, market) + + async def watch_positions(self, symbols: Strings = None, since: Int = None, limit: Int = None, params={}) -> List[Position]: + """ + + https://docs.blofin.com/index.html#ws-positions-channel + + watch all open positions + :param str[]|None symbols: list of unified market symbols + :param int [since]: the earliest time in ms to fetch positions for + :param int [limit]: the maximum number of positions to retrieve + :param dict params: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + await self.authenticate() + await self.load_markets() + newPositions = await self.watch_multiple_wrapper(False, 'positions', 'watchPositions', symbols, params) + if self.newUpdates: + return newPositions + return self.filter_by_symbols_since_limit(self.positions, symbols, since, limit) + + def handle_positions(self, client: Client, message): + # + # { + # arg: {channel: 'positions'}, + # data: [ + # + # ] + # } + # + if self.positions is None: + self.positions = ArrayCacheBySymbolBySide() + cache = self.positions + arg = self.safe_dict(message, 'arg') + channelName = self.safe_string(arg, 'channel') + data = self.safe_list(message, 'data') + newPositions = [] + for i in range(0, len(data)): + position = self.parse_ws_position(data[i]) + newPositions.append(position) + cache.append(position) + messageHash = channelName + ':' + position['symbol'] + client.resolve(position, messageHash) + + def parse_ws_position(self, position, market: Market = None) -> Position: + return self.parse_position(position, market) + + async def watch_multiple_wrapper(self, isPublic: bool, channelName: str, callerMethodName: str, symbolsArray: List[Any] = None, params={}): + # underlier method for all watch-multiple symbols + await self.load_markets() + callerMethodName, params = self.handle_param_string(params, 'callerMethodName', callerMethodName) + # if OHLCV method are being called, then symbols would be symbolsAndTimeframes(multi-dimensional) array + isOHLCV = (channelName == 'candle') + symbols = self.get_list_from_object_values(symbolsArray, 0) if isOHLCV else symbolsArray + symbols = self.market_symbols(symbols, None, True, True) + firstMarket = None + firstSymbol = self.safe_string(symbols, 0) + if firstSymbol is not None: + firstMarket = self.market(firstSymbol) + marketType = None + marketType, params = self.handle_market_type_and_params(callerMethodName, firstMarket, params) + if marketType != 'swap': + raise NotSupported(self.id + ' ' + callerMethodName + '() does not support ' + marketType + ' markets yet') + rawSubscriptions = [] + messageHashes = [] + if symbols is None: + symbols = [] + symbolsLength = len(symbols) + if symbolsLength > 0: + for i in range(0, len(symbols)): + current = symbols[i] + market = None + channel = channelName + if isOHLCV: + market = self.market(current) + tfArray = symbolsArray[i] + tf = tfArray[1] + interval = self.safe_string(self.timeframes, tf, tf) + channel += interval + else: + market = self.market(current) + topic = { + 'channel': channel, + 'instId': market['id'], + } + rawSubscriptions.append(topic) + messageHashes.append(channel + ':' + market['symbol']) + else: + rawSubscriptions.append({'channel': channelName}) + messageHashes.append(channelName) + # private channel are difference, they only need plural channel name for multiple symbols + if self.in_array(channelName, ['orders', 'orders-algo', 'positions']): + rawSubscriptions = [{'channel': channelName}] + request = self.get_subscription_request(rawSubscriptions) + privateOrPublic = 'public' if isPublic else 'private' + url = self.implode_hostname(self.urls['api']['ws'][marketType][privateOrPublic]) + return await self.watch_multiple(url, messageHashes, self.deep_extend(request, params), messageHashes) + + def get_subscription_request(self, args): + return { + 'op': 'subscribe', + 'args': args, + } + + def handle_message(self, client: Client, message): + # + # message examples + # + # { + # arg: { + # channel: "trades", + # instId: "DOGE-USDT", + # }, + # event: "subscribe" + # } + # + # incoming data updates' examples can be seen under each handler method + # + methods = { + # public + 'pong': self.handle_pong, + 'trades': self.handle_trades, + 'books': self.handle_order_book, + 'tickers': self.handle_ticker, + 'candle': self.handle_ohlcv, # candle1m, candle5m, etc + # private + 'account': self.handle_balance, + 'orders': self.handle_orders, + 'orders-algo': self.handle_orders, + 'positions': self.handle_positions, + } + method = None + if message == 'pong': + method = self.safe_value(methods, 'pong') + else: + event = self.safe_string(message, 'event') + if event == 'subscribe': + return + elif event == 'login': + future = self.safe_value(client.futures, 'authenticate_hash') + future.resolve(True) + return + elif event == 'error': + raise ExchangeError(self.id + ' error: ' + self.json(message)) + arg = self.safe_dict(message, 'arg') + channelName = self.safe_string(arg, 'channel') + method = self.safe_value(methods, channelName) + if not method and channelName.find('candle') >= 0: + method = methods['candle'] + if method: + method(client, message) + + async def authenticate(self, params={}): + self.check_required_credentials() + milliseconds = self.milliseconds() + messageHash = 'authenticate_hash' + timestamp = str(milliseconds) + nonce = 'n_' + timestamp + auth = '/users/self/verify' + 'GET' + timestamp + '' + nonce + signature = self.string_to_base64(self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256)) + request = { + 'op': 'login', + 'args': [ + { + 'apiKey': self.apiKey, + 'passphrase': self.password, + 'timestamp': timestamp, + 'nonce': nonce, + 'sign': signature, + }, + ], + } + marketType = 'swap' # for now + url = self.implode_hostname(self.urls['api']['ws'][marketType]['private']) + await self.watch(url, messageHash, self.deep_extend(request, params), messageHash) diff --git a/ccxt/pro/bybit.py b/ccxt/pro/bybit.py new file mode 100644 index 0000000..7d17779 --- /dev/null +++ b/ccxt/pro/bybit.py @@ -0,0 +1,2443 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolById, ArrayCacheBySymbolBySide, ArrayCacheByTimestamp +import asyncio +import hashlib +from ccxt.base.types import Any, Balances, Bool, Int, Liquidation, Num, Order, OrderBook, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, Trade +from ccxt.async_support.base.ws.client import Client +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import NotSupported + + +class bybit(ccxt.async_support.bybit): + + def describe(self) -> Any: + return self.deep_extend(super(bybit, self).describe(), { + 'has': { + 'ws': True, + 'createOrderWs': True, + 'editOrderWs': True, + 'fetchOpenOrdersWs': False, + 'fetchOrderWs': False, + 'cancelOrderWs': True, + 'cancelOrdersWs': False, + 'cancelAllOrdersWs': False, + 'fetchTradesWs': False, + 'fetchBalanceWs': False, + 'watchBalance': True, + 'watchBidsAsks': True, + 'watchLiquidations': True, + 'watchLiquidationsForSymbols': False, + 'watchMyLiquidations': False, + 'watchMyLiquidationsForSymbols': False, + 'watchMyTrades': True, + 'watchOHLCV': True, + 'watchOHLCVForSymbols': True, + 'watchOrderBook': True, + 'watchOrderBookForSymbols': True, + 'watchOrders': True, + 'watchTicker': True, + 'watchTickers': True, + 'watchTrades': True, + 'watchPositions': True, + 'watchTradesForSymbols': True, + 'unWatchTicker': True, + 'unWatchTickers': True, + 'unWatchOHLCV': True, + 'unWatchOHLCVForSymbols': True, + 'unWatchOrderBook': True, + 'unWatchOrderBookForSymbols': True, + 'unWatchTrades': True, + 'unWatchTradesForSymbols': True, + 'unWatchMyTrades': True, + 'unWatchOrders': True, + 'unWatchPositions': True, + }, + 'urls': { + 'api': { + 'ws': { + 'public': { + 'spot': 'wss://stream.{hostname}/v5/public/spot', + 'inverse': 'wss://stream.{hostname}/v5/public/inverse', + 'option': 'wss://stream.{hostname}/v5/public/option', + 'linear': 'wss://stream.{hostname}/v5/public/linear', + }, + 'private': { + 'spot': { + 'unified': 'wss://stream.{hostname}/v5/private', + 'nonUnified': 'wss://stream.{hostname}/spot/private/v3', + }, + 'contract': 'wss://stream.{hostname}/v5/private', + 'usdc': 'wss://stream.{hostname}/trade/option/usdc/private/v1', + 'trade': 'wss://stream.bybit.com/v5/trade', + }, + }, + }, + 'test': { + 'ws': { + 'public': { + 'spot': 'wss://stream-testnet.{hostname}/v5/public/spot', + 'inverse': 'wss://stream-testnet.{hostname}/v5/public/inverse', + 'linear': 'wss://stream-testnet.{hostname}/v5/public/linear', + 'option': 'wss://stream-testnet.{hostname}/v5/public/option', + }, + 'private': { + 'spot': { + 'unified': 'wss://stream-testnet.{hostname}/v5/private', + 'nonUnified': 'wss://stream-testnet.{hostname}/spot/private/v3', + }, + 'contract': 'wss://stream-testnet.{hostname}/v5/private', + 'usdc': 'wss://stream-testnet.{hostname}/trade/option/usdc/private/v1', + 'trade': 'wss://stream-testnet.bybit.com/v5/trade', + }, + }, + }, + 'demotrading': { + 'ws': { + 'public': { + 'spot': 'wss://stream.{hostname}/v5/public/spot', + 'inverse': 'wss://stream.{hostname}/v5/public/inverse', + 'option': 'wss://stream.{hostname}/v5/public/option', + 'linear': 'wss://stream.{hostname}/v5/public/linear', + }, + 'private': { + 'spot': { + 'unified': 'wss://stream-demo.{hostname}/v5/private', + 'nonUnified': 'wss://stream-demo.{hostname}/spot/private/v3', + }, + 'contract': 'wss://stream-demo.{hostname}/v5/private', + 'usdc': 'wss://stream-demo.{hostname}/trade/option/usdc/private/v1', + 'trade': 'wss://stream-demo.bybit.com/v5/trade', + }, + }, + }, + }, + 'options': { + 'watchTicker': { + 'name': 'tickers', # 'tickers' for 24hr statistical ticker or 'tickers_lt' for leverage token ticker + }, + 'watchPositions': { + 'fetchPositionsSnapshot': True, # or False + 'awaitPositionsSnapshot': True, # whether to wait for the positions snapshot before providing updates + }, + 'watchMyTrades': { + # filter execType: https://bybit-exchange.github.io/docs/api-explorer/v5/position/execution + 'filterExecTypes': [ + 'Trade', 'AdlTrade', 'BustTrade', 'Settle', + ], + }, + 'spot': { + 'timeframes': { + '1m': '1m', + '3m': '3m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1h', + '2h': '2h', + '4h': '4h', + '6h': '6h', + '12h': '12h', + '1d': '1d', + '1w': '1w', + '1M': '1M', + }, + }, + 'contract': { + 'timeframes': { + '1m': '1', + '3m': '3', + '5m': '5', + '15m': '15', + '30m': '30', + '1h': '60', + '2h': '120', + '4h': '240', + '6h': '360', + '12h': '720', + '1d': 'D', + '1w': 'W', + '1M': 'M', + }, + }, + }, + 'streaming': { + 'ping': self.ping, + 'keepAlive': 18000, + }, + }) + + def request_id(self): + requestId = self.sum(self.safe_integer(self.options, 'requestId', 0), 1) + self.options['requestId'] = requestId + return requestId + + async def get_url_by_market_type(self, symbol: Str = None, isPrivate=False, method: Str = None, params={}): + accessibility = 'private' if isPrivate else 'public' + isUsdcSettled = None + isSpot = None + type = None + market = None + url = self.urls['api']['ws'] + if symbol is not None: + market = self.market(symbol) + isUsdcSettled = market['settle'] == 'USDC' + type = market['type'] + else: + type, params = self.handle_market_type_and_params(method, None, params) + defaultSettle = self.safe_string(self.options, 'defaultSettle') + defaultSettle = self.safe_string_2(params, 'settle', 'defaultSettle', defaultSettle) + isUsdcSettled = (defaultSettle == 'USDC') + isSpot = (type == 'spot') + if isPrivate: + unified = await self.isUnifiedEnabled() + isUnifiedMargin = self.safe_bool(unified, 0, False) + isUnifiedAccount = self.safe_bool(unified, 1, False) + if isUsdcSettled and not isUnifiedMargin and not isUnifiedAccount: + url = url[accessibility]['usdc'] + else: + url = url[accessibility]['contract'] + else: + if isSpot: + url = url[accessibility]['spot'] + elif (type == 'swap') or (type == 'future'): + subType = None + subType, params = self.handle_sub_type_and_params(method, market, params, 'linear') + url = url[accessibility][subType] + else: + # option + url = url[accessibility]['option'] + url = self.implode_hostname(url) + return url + + def clean_params(self, params): + params = self.omit(params, ['type', 'subType', 'settle', 'defaultSettle', 'unifiedMargin']) + return params + + async def create_order_ws(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://bybit-exchange.github.io/docs/v5/order/create-order + https://bybit-exchange.github.io/docs/v5/websocket/trade/guideline#createamendcancel-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.timeInForce]: "GTC", "IOC", "FOK" + :param bool [params.postOnly]: True or False whether the order is post-only + :param bool [params.reduceOnly]: True or False whether the order is reduce-only + :param str [params.positionIdx]: *contracts only* 0 for one-way mode, 1 buy side of hedged mode, 2 sell side of hedged mode + :param boolean [params.isLeverage]: *unified spot only* False then spot trading True then margin trading + :param str [params.tpslMode]: *contract only* 'full' or 'partial' + :param str [params.mmp]: *option only* market maker protection + :param str [params.triggerDirection]: *contract only* the direction for trigger orders, 'above' or 'below' + :param float [params.triggerPrice]: The price at which a trigger order is triggered at + :param float [params.stopLossPrice]: The price at which a stop loss order is triggered at + :param float [params.takeProfitPrice]: The price at which a take profit order is triggered at + :param dict [params.takeProfit]: *takeProfit object in params* containing the triggerPrice at which the attached take profit order will be triggered + :param float [params.takeProfit.triggerPrice]: take profit trigger price + :param dict [params.stopLoss]: *stopLoss object in params* containing the triggerPrice at which the attached stop loss order will be triggered + :param float [params.stopLoss.triggerPrice]: stop loss trigger price + :param str [params.trailingAmount]: the quote amount to trail away from the current market price + :param str [params.trailingTriggerPrice]: the price to trigger a trailing order, default uses the price argument + :returns dict: an `order structure ` + """ + await self.load_markets() + orderRequest = self.create_order_request(symbol, type, side, amount, price, params, True) + url = self.urls['api']['ws']['private']['trade'] + await self.authenticate(url) + requestId = str(self.request_id()) + request: dict = { + 'op': 'order.create', + 'reqId': requestId, + 'args': [ + orderRequest, + ], + 'header': { + 'X-BAPI-TIMESTAMP': str(self.milliseconds()), + 'X-BAPI-RECV-WINDOW': str(self.options['recvWindow']), + }, + } + return await self.watch(url, requestId, request, requestId, True) + + async def edit_order_ws(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + """ + edit a trade order + + https://bybit-exchange.github.io/docs/v5/order/amend-order + https://bybit-exchange.github.io/docs/v5/websocket/trade/guideline#createamendcancel-order + + :param str id: cancel order id + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float price: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: The price that a trigger order is triggered at + :param float [params.stopLossPrice]: The price that a stop loss order is triggered at + :param float [params.takeProfitPrice]: The price that a take profit order is triggered at + :param dict [params.takeProfit]: *takeProfit object in params* containing the triggerPrice that the attached take profit order will be triggered + :param float [params.takeProfit.triggerPrice]: take profit trigger price + :param dict [params.stopLoss]: *stopLoss object in params* containing the triggerPrice that the attached stop loss order will be triggered + :param float [params.stopLoss.triggerPrice]: stop loss trigger price + :param str [params.triggerBy]: 'IndexPrice', 'MarkPrice' or 'LastPrice', default is 'LastPrice', required if no initial value for triggerPrice + :param str [params.slTriggerBy]: 'IndexPrice', 'MarkPrice' or 'LastPrice', default is 'LastPrice', required if no initial value for stopLoss + :param str [params.tpTriggerby]: 'IndexPrice', 'MarkPrice' or 'LastPrice', default is 'LastPrice', required if no initial value for takeProfit + :returns dict: an `order structure ` + """ + await self.load_markets() + orderRequest = self.edit_order_request(id, symbol, type, side, amount, price, params) + url = self.urls['api']['ws']['private']['trade'] + await self.authenticate(url) + requestId = str(self.request_id()) + request: dict = { + 'op': 'order.amend', + 'reqId': requestId, + 'args': [ + orderRequest, + ], + 'header': { + 'X-BAPI-TIMESTAMP': str(self.milliseconds()), + 'X-BAPI-RECV-WINDOW': str(self.options['recvWindow']), + }, + } + return await self.watch(url, requestId, request, requestId, True) + + async def cancel_order_ws(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://bybit-exchange.github.io/docs/v5/order/cancel-order + https://bybit-exchange.github.io/docs/v5/websocket/trade/guideline#createamendcancel-order + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: *spot only* whether the order is a trigger order + :param str [params.orderFilter]: *spot only* 'Order' or 'StopOrder' or 'tpslOrder' + :returns dict: An `order structure ` + """ + await self.load_markets() + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrderWs() requires a symbol argument') + orderRequest = self.cancel_order_request(id, symbol, params) + url = self.urls['api']['ws']['private']['trade'] + await self.authenticate(url) + requestId = str(self.request_id()) + if 'orderFilter' in orderRequest: + del orderRequest['orderFilter'] + request: dict = { + 'op': 'order.cancel', + 'reqId': requestId, + 'args': [ + orderRequest, + ], + 'header': { + 'X-BAPI-TIMESTAMP': str(self.milliseconds()), + 'X-BAPI-RECV-WINDOW': str(self.options['recvWindow']), + }, + } + return await self.watch(url, requestId, request, requestId, True) + + async def watch_ticker(self, symbol: str, params={}) -> Ticker: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://bybit-exchange.github.io/docs/v5/websocket/public/ticker + https://bybit-exchange.github.io/docs/v5/websocket/public/etp-ticker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + messageHash = 'ticker:' + symbol + url = await self.get_url_by_market_type(symbol, False, 'watchTicker', params) + params = self.clean_params(params) + options = self.safe_value(self.options, 'watchTicker', {}) + topic = self.safe_string(options, 'name', 'tickers') + if not market['spot'] and topic != 'tickers': + raise BadRequest(self.id + ' watchTicker() only supports name tickers for contract markets') + topic += '.' + market['id'] + topics = [topic] + return await self.watch_topics(url, [messageHash], topics, params) + + async def watch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list + + https://bybit-exchange.github.io/docs/v5/websocket/public/ticker + https://bybit-exchange.github.io/docs/v5/websocket/public/etp-ticker + + :param str[] symbols: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, False) + messageHashes = [] + url = await self.get_url_by_market_type(symbols[0], False, 'watchTickers', params) + params = self.clean_params(params) + options = self.safe_value(self.options, 'watchTickers', {}) + topic = self.safe_string(options, 'name', 'tickers') + marketIds = self.market_ids(symbols) + topics = [] + for i in range(0, len(marketIds)): + marketId = marketIds[i] + topics.append(topic + '.' + marketId) + messageHashes.append('ticker:' + symbols[i]) + ticker = await self.watch_topics(url, messageHashes, topics, params) + if self.newUpdates: + result: dict = {} + result[ticker['symbol']] = ticker + return result + return self.filter_by_array(self.tickers, 'symbol', symbols) + + async def un_watch_tickers(self, symbols: Strings = None, params={}) -> Any: + """ + unWatches a price ticker + + https://bybit-exchange.github.io/docs/v5/websocket/public/ticker + https://bybit-exchange.github.io/docs/v5/websocket/public/etp-ticker + + :param str[] symbols: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, False) + options = self.safe_value(self.options, 'watchTickers', {}) + topic = self.safe_string(options, 'name', 'tickers') + messageHashes = [] + subMessageHashes = [] + marketIds = self.market_ids(symbols) + topics = [] + for i in range(0, len(marketIds)): + marketId = marketIds[i] + symbol = symbols[i] + topics.append(topic + '.' + marketId) + subMessageHashes.append('ticker:' + symbol) + messageHashes.append('unsubscribe:ticker:' + symbol) + url = await self.get_url_by_market_type(symbols[0], False, 'watchTickers', params) + return await self.un_watch_topics(url, 'ticker', symbols, messageHashes, subMessageHashes, topics, params) + + async def un_watch_ticker(self, symbol: str, params={}) -> Any: + """ + unWatches a price ticker + + https://bybit-exchange.github.io/docs/v5/websocket/public/ticker + https://bybit-exchange.github.io/docs/v5/websocket/public/etp-ticker + + :param str[] symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + return await self.un_watch_tickers([symbol], params) + + def handle_ticker(self, client: Client, message): + # + # linear + # { + # "topic": "tickers.BTCUSDT", + # "type": "snapshot", + # "data": { + # "symbol": "BTCUSDT", + # "tickDirection": "PlusTick", + # "price24hPcnt": "0.017103", + # "lastPrice": "17216.00", + # "prevPrice24h": "16926.50", + # "highPrice24h": "17281.50", + # "lowPrice24h": "16915.00", + # "prevPrice1h": "17238.00", + # "markPrice": "17217.33", + # "indexPrice": "17227.36", + # "openInterest": "68744.761", + # "openInterestValue": "1183601235.91", + # "turnover24h": "1570383121.943499", + # "volume24h": "91705.276", + # "nextFundingTime": "1673280000000", + # "fundingRate": "-0.000212", + # "bid1Price": "17215.50", + # "bid1Size": "84.489", + # "ask1Price": "17216.00", + # "ask1Size": "83.020" + # }, + # "cs": 24987956059, + # "ts": 1673272861686 + # } + # + # option + # { + # "id": "tickers.BTC-6JAN23-17500-C-2480334983-1672917511074", + # "topic": "tickers.BTC-6JAN23-17500-C", + # "ts": 1672917511074, + # "data": { + # "symbol": "BTC-6JAN23-17500-C", + # "bidPrice": "0", + # "bidSize": "0", + # "bidIv": "0", + # "askPrice": "10", + # "askSize": "5.1", + # "askIv": "0.514", + # "lastPrice": "10", + # "highPrice24h": "25", + # "lowPrice24h": "5", + # "markPrice": "7.86976724", + # "indexPrice": "16823.73", + # "markPriceIv": "0.4896", + # "underlyingPrice": "16815.1", + # "openInterest": "49.85", + # "turnover24h": "446802.8473", + # "volume24h": "26.55", + # "totalVolume": "86", + # "totalTurnover": "1437431", + # "delta": "0.047831", + # "gamma": "0.00021453", + # "vega": "0.81351067", + # "theta": "-19.9115368", + # "predictedDeliveryPrice": "0", + # "change24h": "-0.33333334" + # }, + # "type": "snapshot" + # } + # + # spot + # { + # "topic": "tickers.BTCUSDT", + # "ts": 1673853746003, + # "type": "snapshot", + # "cs": 2588407389, + # "data": { + # "symbol": "BTCUSDT", + # "lastPrice": "21109.77", + # "highPrice24h": "21426.99", + # "lowPrice24h": "20575", + # "prevPrice24h": "20704.93", + # "volume24h": "6780.866843", + # "turnover24h": "141946527.22907118", + # "price24hPcnt": "0.0196", + # "usdIndexPrice": "21120.2400136" + # } + # } + # + # lt ticker + # { + # "topic": "tickers_lt.EOS3LUSDT", + # "ts": 1672325446847, + # "type": "snapshot", + # "data": { + # "symbol": "EOS3LUSDT", + # "lastPrice": "0.41477848043290448", + # "highPrice24h": "0.435285472510871305", + # "lowPrice24h": "0.394601507960931382", + # "prevPrice24h": "0.431502290172376349", + # "price24hPcnt": "-0.0388" + # } + # } + # swap delta + # { + # "topic":"tickers.AAVEUSDT", + # "type":"delta", + # "data":{ + # "symbol":"AAVEUSDT", + # "bid1Price":"112.89", + # "bid1Size":"2.12", + # "ask1Price":"112.90", + # "ask1Size":"5.02" + # }, + # "cs":78039939929, + # "ts":1709210212704 + # } + # + topic = self.safe_string(message, 'topic', '') + updateType = self.safe_string(message, 'type', '') + data = self.safe_dict(message, 'data', {}) + isSpot = self.safe_string(data, 'usdIndexPrice') is not None + type = 'spot' if isSpot else 'contract' + symbol = None + parsed = None + if (updateType == 'snapshot'): + parsed = self.parse_ticker(data) + symbol = parsed['symbol'] + elif updateType == 'delta': + topicParts = topic.split('.') + topicLength = len(topicParts) + marketId = self.safe_string(topicParts, topicLength - 1) + market = self.safe_market(marketId, None, None, type) + symbol = market['symbol'] + # update the info in place + ticker = self.safe_dict(self.tickers, symbol, {}) + rawTicker = self.safe_dict(ticker, 'info', {}) + merged = self.extend(rawTicker, data) + parsed = self.parse_ticker(merged) + timestamp = self.safe_integer(message, 'ts') + parsed['timestamp'] = timestamp + parsed['datetime'] = self.iso8601(timestamp) + self.tickers[symbol] = parsed + messageHash = 'ticker:' + symbol + client.resolve(self.tickers[symbol], messageHash) + + async def watch_bids_asks(self, symbols: Strings = None, params={}) -> Tickers: + """ + watches best bid & ask for symbols + + https://bybit-exchange.github.io/docs/v5/websocket/public/orderbook + + :param str[] symbols: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, False) + messageHashes = [] + url = await self.get_url_by_market_type(symbols[0], False, 'watchBidsAsks', params) + params = self.clean_params(params) + marketIds = self.market_ids(symbols) + topics = [] + for i in range(0, len(marketIds)): + marketId = marketIds[i] + topic = 'orderbook.1.' + marketId + topics.append(topic) + messageHashes.append('bidask:' + symbols[i]) + ticker = await self.watch_topics(url, messageHashes, topics, params) + if self.newUpdates: + return ticker + return self.filter_by_array(self.bidsasks, 'symbol', symbols) + + def parse_ws_bid_ask(self, orderbook, market=None): + timestamp = self.safe_integer(orderbook, 'timestamp') + bids = self.sort_by(self.aggregate(orderbook['bids']), 0) + asks = self.sort_by(self.aggregate(orderbook['asks']), 0) + bestBid = self.safe_list(bids, 0, []) + bestAsk = self.safe_list(asks, 0, []) + return self.safe_ticker({ + 'symbol': market['symbol'], + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'ask': self.safe_number(bestAsk, 0), + 'askVolume': self.safe_number(bestAsk, 1), + 'bid': self.safe_number(bestBid, 0), + 'bidVolume': self.safe_number(bestBid, 1), + 'info': orderbook, + }, market) + + async def watch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + watches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://bybit-exchange.github.io/docs/v5/websocket/public/kline + https://bybit-exchange.github.io/docs/v5/websocket/public/etp-kline + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + params['callerMethodName'] = 'watchOHLCV' + result = await self.watch_ohlcv_for_symbols([[symbol, timeframe]], since, limit, params) + return result[symbol][timeframe] + + async def watch_ohlcv_for_symbols(self, symbolsAndTimeframes: List[List[str]], since: Int = None, limit: Int = None, params={}): + """ + watches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://bybit-exchange.github.io/docs/v5/websocket/public/kline + https://bybit-exchange.github.io/docs/v5/websocket/public/etp-kline + + :param str[][] symbolsAndTimeframes: array of arrays containing unified symbols and timeframes to fetch OHLCV data for, example [['BTC/USDT', '1m'], ['LTC/USDT', '5m']] + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + symbols = self.get_list_from_object_values(symbolsAndTimeframes, 0) + marketSymbols = self.market_symbols(symbols, None, False, True, True) + firstSymbol = marketSymbols[0] + url = await self.get_url_by_market_type(firstSymbol, False, 'watchOHLCVForSymbols', params) + rawHashes = [] + messageHashes = [] + for i in range(0, len(symbolsAndTimeframes)): + data = symbolsAndTimeframes[i] + symbolString = self.safe_string(data, 0) + market = self.market(symbolString) + symbolString = market['symbol'] + unfiedTimeframe = self.safe_string(data, 1) + timeframeId = self.safe_string(self.timeframes, unfiedTimeframe, unfiedTimeframe) + rawHashes.append('kline.' + timeframeId + '.' + market['id']) + messageHashes.append('ohlcv::' + symbolString + '::' + unfiedTimeframe) + symbol, timeframe, stored = await self.watch_topics(url, messageHashes, rawHashes, params) + if self.newUpdates: + limit = stored.getLimit(symbol, limit) + filtered = self.filter_by_since_limit(stored, since, limit, 0, True) + return self.create_ohlcv_object(symbol, timeframe, filtered) + + async def un_watch_ohlcv_for_symbols(self, symbolsAndTimeframes: List[List[str]], params={}) -> Any: + """ + unWatches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://bybit-exchange.github.io/docs/v5/websocket/public/kline + https://bybit-exchange.github.io/docs/v5/websocket/public/etp-kline + + :param str[][] symbolsAndTimeframes: array of arrays containing unified symbols and timeframes to fetch OHLCV data for, example [['BTC/USDT', '1m'], ['LTC/USDT', '5m']] + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + symbols = self.get_list_from_object_values(symbolsAndTimeframes, 0) + marketSymbols = self.market_symbols(symbols, None, False, True, True) + firstSymbol = marketSymbols[0] + url = await self.get_url_by_market_type(firstSymbol, False, 'watchOHLCVForSymbols', params) + rawHashes = [] + subMessageHashes = [] + messageHashes = [] + for i in range(0, len(symbolsAndTimeframes)): + data = symbolsAndTimeframes[i] + symbolString = self.safe_string(data, 0) + market = self.market(symbolString) + symbolString = market['symbol'] + unfiedTimeframe = self.safe_string(data, 1) + timeframeId = self.safe_string(self.timeframes, unfiedTimeframe, unfiedTimeframe) + rawHashes.append('kline.' + timeframeId + '.' + market['id']) + subMessageHashes.append('ohlcv::' + symbolString + '::' + unfiedTimeframe) + messageHashes.append('unsubscribe::ohlcv::' + symbolString + '::' + unfiedTimeframe) + subExtension = { + 'symbolsAndTimeframes': symbolsAndTimeframes, + } + return await self.un_watch_topics(url, 'ohlcv', symbols, messageHashes, subMessageHashes, rawHashes, params, subExtension) + + async def un_watch_ohlcv(self, symbol: str, timeframe: str = '1m', params={}) -> Any: + """ + unWatches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://bybit-exchange.github.io/docs/v5/websocket/public/kline + https://bybit-exchange.github.io/docs/v5/websocket/public/etp-kline + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + params['callerMethodName'] = 'watchOHLCV' + return await self.un_watch_ohlcv_for_symbols([[symbol, timeframe]], params) + + def handle_ohlcv(self, client: Client, message): + # + # { + # "topic": "kline.5.BTCUSDT", + # "data": [ + # { + # "start": 1672324800000, + # "end": 1672325099999, + # "interval": "5", + # "open": "16649.5", + # "close": "16677", + # "high": "16677", + # "low": "16608", + # "volume": "2.081", + # "turnover": "34666.4005", + # "confirm": False, + # "timestamp": 1672324988882 + # } + # ], + # "ts": 1672324988882, + # "type": "snapshot" + # } + # + data = self.safe_value(message, 'data', {}) + topic = self.safe_string(message, 'topic') + topicParts = topic.split('.') + topicLength = len(topicParts) + timeframeId = self.safe_string(topicParts, 1) + timeframe = self.find_timeframe(timeframeId) + marketId = self.safe_string(topicParts, topicLength - 1) + isSpot = client.url.find('spot') > -1 + marketType = 'spot' if isSpot else 'contract' + market = self.safe_market(marketId, None, None, marketType) + symbol = market['symbol'] + ohlcvsByTimeframe = self.safe_value(self.ohlcvs, symbol) + if ohlcvsByTimeframe is None: + self.ohlcvs[symbol] = {} + if self.safe_value(ohlcvsByTimeframe, timeframe) is None: + limit = self.safe_integer(self.options, 'OHLCVLimit', 1000) + self.ohlcvs[symbol][timeframe] = ArrayCacheByTimestamp(limit) + stored = self.ohlcvs[symbol][timeframe] + for i in range(0, len(data)): + parsed = self.parse_ws_ohlcv(data[i], market) + stored.append(parsed) + messageHash = 'ohlcv::' + symbol + '::' + timeframe + resolveData = [symbol, timeframe, stored] + client.resolve(resolveData, messageHash) + + def parse_ws_ohlcv(self, ohlcv, market=None) -> list: + # + # { + # "start": 1670363160000, + # "end": 1670363219999, + # "interval": "1", + # "open": "16987.5", + # "close": "16987.5", + # "high": "16988", + # "low": "16987.5", + # "volume": "23.511", + # "turnover": "399396.344", + # "confirm": False, + # "timestamp": 1670363219614 + # } + # + volumeIndex = 'turnover' if (market['inverse']) else 'volume' + return [ + self.safe_integer(ohlcv, 'start'), + self.safe_number(ohlcv, 'open'), + self.safe_number(ohlcv, 'high'), + self.safe_number(ohlcv, 'low'), + self.safe_number(ohlcv, 'close'), + self.safe_number(ohlcv, volumeIndex), + ] + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://bybit-exchange.github.io/docs/v5/websocket/public/orderbook + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return. + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + return await self.watch_order_book_for_symbols([symbol], limit, params) + + async def watch_order_book_for_symbols(self, symbols: List[str], limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://bybit-exchange.github.io/docs/v5/websocket/public/orderbook + + :param str[] symbols: unified array of symbols + :param int [limit]: the maximum amount of order book entries to return. + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + symbolsLength = len(symbols) + if symbolsLength == 0: + raise ArgumentsRequired(self.id + ' watchOrderBookForSymbols() requires a non-empty array of symbols') + symbols = self.market_symbols(symbols) + url = await self.get_url_by_market_type(symbols[0], False, 'watchOrderBook', params) + params = self.clean_params(params) + market = self.market(symbols[0]) + if limit is None: + limit = 50 if (market['spot']) else 500 + if market['option']: + limit = 100 + else: + limits = { + 'spot': [1, 50, 200, 1000], + 'option': [25, 100], + 'default': [1, 50, 200, 500, 1000], + } + selectedLimits = self.safe_list_2(limits, market['type'], 'default') + if not self.in_array(limit, selectedLimits): + raise BadRequest(self.id + ' watchOrderBookForSymbols(): for ' + market['type'] + ' markets limit can be one of: ' + self.json(selectedLimits)) + topics = [] + messageHashes = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + marketId = self.market_id(symbol) + topic = 'orderbook.' + str(limit) + '.' + marketId + topics.append(topic) + messageHash = 'orderbook:' + symbol + messageHashes.append(messageHash) + orderbook = await self.watch_topics(url, messageHashes, topics, params) + return orderbook.limit() + + async def un_watch_order_book_for_symbols(self, symbols: List[str], params={}) -> Any: + """ + unsubscribe from the orderbook channel + + https://bybit-exchange.github.io/docs/v5/websocket/public/orderbook + + :param str[] symbols: unified symbol of the market to unwatch the trades for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.limit]: orderbook limit, default is None + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, False) + channel = 'orderbook.' + limit = self.safe_integer(params, 'limit') + if limit is not None: + params = self.omit(params, 'limit') + else: + firstMarket = self.market(symbols[0]) + limit = 50 if firstMarket['spot'] else 500 + channel += str(limit) + subMessageHashes = [] + messageHashes = [] + topics = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + market = self.market(symbol) + marketId = market['id'] + topic = channel + '.' + marketId + messageHashes.append('unsubscribe:orderbook:' + symbol) + subMessageHashes.append('orderbook:' + symbol) + topics.append(topic) + url = await self.get_url_by_market_type(symbols[0], False, 'watchOrderBook', params) + return await self.un_watch_topics(url, 'orderbook', symbols, messageHashes, subMessageHashes, topics, params) + + async def un_watch_order_book(self, symbol: str, params={}) -> Any: + """ + unsubscribe from the orderbook channel + + https://bybit-exchange.github.io/docs/v5/websocket/public/orderbook + + :param str symbol: symbol of the market to unwatch the trades for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.limit]: orderbook limit, default is None + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + return await self.un_watch_order_book_for_symbols([symbol], params) + + def handle_order_book(self, client: Client, message): + # + # { + # "topic": "orderbook.50.BTCUSDT", + # "type": "snapshot", + # "ts": 1672304484978, + # "data": { + # "s": "BTCUSDT", + # "b": [ + # ..., + # [ + # "16493.50", + # "0.006" + # ], + # [ + # "16493.00", + # "0.100" + # ] + # ], + # "a": [ + # [ + # "16611.00", + # "0.029" + # ], + # [ + # "16612.00", + # "0.213" + # ], + # ], + # "u": 18521288, + # "seq": 7961638724 + # } + # } + # + topic = self.safe_string(message, 'topic') + limit = topic.split('.')[1] + isSpot = client.url.find('spot') >= 0 + type = self.safe_string(message, 'type') + isSnapshot = (type == 'snapshot') + data = self.safe_dict(message, 'data', {}) + marketId = self.safe_string(data, 's') + marketType = 'spot' if isSpot else 'contract' + market = self.safe_market(marketId, None, None, marketType) + symbol = market['symbol'] + timestamp = self.safe_integer(message, 'ts') + if not (symbol in self.orderbooks): + self.orderbooks[symbol] = self.order_book() + orderbook = self.orderbooks[symbol] + orderbook['symbol'] = symbol + if isSnapshot: + snapshot = self.parse_order_book(data, symbol, timestamp, 'b', 'a') + orderbook.reset(snapshot) + else: + asks = self.safe_list(data, 'a', []) + bids = self.safe_list(data, 'b', []) + self.handle_deltas(orderbook['asks'], asks) + self.handle_deltas(orderbook['bids'], bids) + orderbook['timestamp'] = timestamp + orderbook['datetime'] = self.iso8601(timestamp) + messageHash = 'orderbook' + ':' + symbol + self.orderbooks[symbol] = orderbook + client.resolve(orderbook, messageHash) + if limit == '1': + bidask = self.parse_ws_bid_ask(self.orderbooks[symbol], market) + newBidsAsks: dict = {} + newBidsAsks[symbol] = bidask + self.bidsasks[symbol] = bidask + client.resolve(newBidsAsks, 'bidask:' + symbol) + + def handle_delta(self, bookside, delta): + bidAsk = self.parse_bid_ask(delta, 0, 1) + bookside.storeArray(bidAsk) + + def handle_deltas(self, bookside, deltas): + for i in range(0, len(deltas)): + self.handle_delta(bookside, deltas[i]) + + async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + watches information on multiple trades made in a market + + https://bybit-exchange.github.io/docs/v5/websocket/public/trade + + :param str symbol: unified market symbol of the market trades were made in + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trade structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + return await self.watch_trades_for_symbols([symbol], since, limit, params) + + async def watch_trades_for_symbols(self, symbols: List[str], since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a list of symbols + + https://bybit-exchange.github.io/docs/v5/websocket/public/trade + + :param str[] symbols: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + symbolsLength = len(symbols) + if symbolsLength == 0: + raise ArgumentsRequired(self.id + ' watchTradesForSymbols() requires a non-empty array of symbols') + params = self.clean_params(params) + url = await self.get_url_by_market_type(symbols[0], False, 'watchTrades', params) + topics = [] + messageHashes = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + market = self.market(symbol) + topic = 'publicTrade.' + market['id'] + topics.append(topic) + messageHash = 'trade:' + symbol + messageHashes.append(messageHash) + trades = await self.watch_topics(url, messageHashes, topics, params) + if self.newUpdates: + first = self.safe_value(trades, 0) + tradeSymbol = self.safe_string(first, 'symbol') + limit = trades.getLimit(tradeSymbol, limit) + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + async def un_watch_trades_for_symbols(self, symbols: List[str], params={}) -> Any: + """ + unsubscribe from the trades channel + + https://bybit-exchange.github.io/docs/v5/websocket/public/trade + + :param str[] symbols: unified symbol of the market to unwatch the trades for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns any: status of the unwatch request + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, False, True) + url = await self.get_url_by_market_type(symbols[0], False, 'unWatchTradesForSymbols', params) + messageHashes = [] + topics = [] + subMessageHashes = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + market = self.market(symbol) + topic = 'publicTrade.' + market['id'] + topics.append(topic) + messageHash = 'unsubscribe:trade:' + symbol + messageHashes.append(messageHash) + subMessageHashes.append('trade:' + symbol) + return await self.un_watch_topics(url, 'trades', symbols, messageHashes, subMessageHashes, topics, params) + + async def un_watch_trades(self, symbol: str, params={}) -> Any: + """ + unsubscribe from the trades channel + + https://bybit-exchange.github.io/docs/v5/websocket/public/trade + + :param str symbol: unified symbol of the market to unwatch the trades for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns any: status of the unwatch request + """ + await self.load_markets() + return await self.un_watch_trades_for_symbols([symbol], params) + + def handle_trades(self, client: Client, message): + # + # { + # "topic": "publicTrade.BTCUSDT", + # "type": "snapshot", + # "ts": 1672304486868, + # "data": [ + # { + # "T": 1672304486865, + # "s": "BTCUSDT", + # "S": "Buy", + # "v": "0.001", + # "p": "16578.50", + # "L": "PlusTick", + # "i": "20f43950-d8dd-5b31-9112-a178eb6023af", + # "BT": False + # } + # ] + # } + # + data = self.safe_value(message, 'data', {}) + topic = self.safe_string(message, 'topic') + trades = data + parts = topic.split('.') + isSpot = client.url.find('spot') >= 0 + marketType = 'spot' if (isSpot) else 'contract' + marketId = self.safe_string(parts, 1) + market = self.safe_market(marketId, None, None, marketType) + symbol = market['symbol'] + stored = self.safe_value(self.trades, symbol) + if stored is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + stored = ArrayCache(limit) + self.trades[symbol] = stored + for j in range(0, len(trades)): + parsed = self.parse_ws_trade(trades[j], market) + stored.append(parsed) + messageHash = 'trade' + ':' + symbol + client.resolve(stored, messageHash) + + def parse_ws_trade(self, trade, market=None): + # + # public + # { + # "T": 1672304486865, + # "s": "BTCUSDT", + # "S": "Buy", + # "v": "0.001", + # "p": "16578.50", + # "L": "PlusTick", + # "i": "20f43950-d8dd-5b31-9112-a178eb6023af", + # "BT": False + # } + # + # spot private + # { + # "e": "ticketInfo", + # "E": "1662348310386", + # "s": "BTCUSDT", + # "q": "0.001007", + # "t": "1662348310373", + # "p": "19842.02", + # "T": "2100000000002220938", + # "o": "1238261807653647872", + # "c": "spotx008", + # "O": "1238225004531834368", + # "a": "533287", + # "A": "642908", + # "m": False, + # "S": "BUY" + # } + # + id = self.safe_string_n(trade, ['i', 'T', 'v']) + isContract = ('BT' in trade) + marketType = 'contract' if isContract else 'spot' + if market is not None: + marketType = market['type'] + marketId = self.safe_string(trade, 's') + market = self.safe_market(marketId, market, None, marketType) + symbol = market['symbol'] + timestamp = self.safe_integer_2(trade, 't', 'T') + side = self.safe_string_lower(trade, 'S') + takerOrMaker = None + m = self.safe_value(trade, 'm') + if side is None: + side = 'buy' if m else 'sell' + else: + # spot private + takerOrMaker = m + price = self.safe_string(trade, 'p') + amount = self.safe_string_2(trade, 'q', 'v') + orderId = self.safe_string(trade, 'o') + return self.safe_trade({ + 'id': id, + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'order': orderId, + 'type': None, + 'side': side, + 'takerOrMaker': takerOrMaker, + 'price': price, + 'amount': amount, + 'cost': None, + 'fee': None, + }, market) + + def get_private_type(self, url): + if url.find('spot') >= 0: + return 'spot' + elif url.find('v5/private') >= 0: + return 'unified' + else: + return 'usdc' + + async def watch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + watches information on multiple trades made by the user + + https://bybit-exchange.github.io/docs/v5/websocket/private/execution + https://bybit-exchange.github.io/docs/v5/websocket/private/fast-execution + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.unifiedMargin]: use unified margin account + :param boolean [params.executionFast]: use fast execution + :returns dict[]: a list of `order structures ` + """ + method = 'watchMyTrades' + messageHash = 'myTrades' + await self.load_markets() + if symbol is not None: + symbol = self.symbol(symbol) + messageHash += ':' + symbol + url = await self.get_url_by_market_type(symbol, True, method, params) + await self.authenticate(url) + topicByMarket: dict = { + 'spot': 'ticketInfo', + 'unified': 'execution', + 'usdc': 'user.openapi.perp.trade', + } + topic = self.safe_value(topicByMarket, self.get_private_type(url)) + executionFast = False + executionFast, params = self.handle_option_and_params(params, 'watchMyTrades', 'executionFast', False) + if executionFast: + topic = 'execution.fast' + trades = await self.watch_topics(url, [messageHash], [topic], params) + if self.newUpdates: + limit = trades.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(trades, symbol, since, limit, True) + + async def un_watch_my_trades(self, symbol: Str = None, params={}) -> Any: + """ + unWatches information on multiple trades made by the user + + https://bybit-exchange.github.io/docs/v5/websocket/private/execution + https://bybit-exchange.github.io/docs/v5/websocket/private/fast-execution + + :param str symbol: unified market symbol of the market orders were made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.unifiedMargin]: use unified margin account + :param boolean [params.executionFast]: use fast execution + :returns dict[]: a list of `order structures ` + """ + method = 'watchMyTrades' + messageHash = 'unsubscribe:myTrades' + subHash = 'myTrades' + await self.load_markets() + if symbol is not None: + raise NotSupported(self.id + ' unWatchMyTrades() does not support a symbol parameter, you must unwatch all my trades') + url = await self.get_url_by_market_type(symbol, True, method, params) + await self.authenticate(url) + topicByMarket: dict = { + 'spot': 'ticketInfo', + 'unified': 'execution', + 'usdc': 'user.openapi.perp.trade', + } + topic = self.safe_value(topicByMarket, self.get_private_type(url)) + executionFast = False + executionFast, params = self.handle_option_and_params(params, 'watchMyTrades', 'executionFast', False) + if executionFast: + topic = 'execution.fast' + return await self.un_watch_topics(url, 'myTrades', [], [messageHash], [subHash], [topic], params) + + def handle_my_trades(self, client: Client, message): + # + # spot + # { + # "type": "snapshot", + # "topic": "ticketInfo", + # "ts": "1662348310388", + # "data": [ + # { + # "e": "ticketInfo", + # "E": "1662348310386", + # "s": "BTCUSDT", + # "q": "0.001007", + # "t": "1662348310373", + # "p": "19842.02", + # "T": "2100000000002220938", + # "o": "1238261807653647872", + # "c": "spotx008", + # "O": "1238225004531834368", + # "a": "533287", + # "A": "642908", + # "m": False, + # "S": "BUY" + # } + # ] + # } + # unified + # { + # "id": "592324803b2785-26fa-4214-9963-bdd4727f07be", + # "topic": "execution", + # "creationTime": 1672364174455, + # "data": [ + # { + # "category": "linear", + # "symbol": "XRPUSDT", + # "execFee": "0.005061", + # "execId": "7e2ae69c-4edf-5800-a352-893d52b446aa", + # "execPrice": "0.3374", + # "execQty": "25", + # "execType": "Trade", + # "execValue": "8.435", + # "isMaker": False, + # "feeRate": "0.0006", + # "tradeIv": "", + # "markIv": "", + # "blockTradeId": "", + # "markPrice": "0.3391", + # "indexPrice": "", + # "underlyingPrice": "", + # "leavesQty": "0", + # "orderId": "f6e324ff-99c2-4e89-9739-3086e47f9381", + # "orderLinkId": "", + # "orderPrice": "0.3207", + # "orderQty": "25", + # "orderType": "Market", + # "stopOrderType": "UNKNOWN", + # "side": "Sell", + # "execTime": "1672364174443", + # "isLeverage": "0" + # } + # ] + # } + # + # execution.fast + # + # { + # "topic": "execution.fast", + # "creationTime": 1757405601981, + # "data": [ + # { + # "category": "linear", + # "symbol": "BTCUSDT", + # "execId": "ffcac6ac-7571-536d-a28a-847dd7d08a0f", + # "execPrice": "112529.6", + # "execQty": "0.001", + # "orderId": "6e25ab73-7a55-4ae7-adc2-8ea95f167c85", + # "isMaker": False, + # "orderLinkId": "test-00001", + # "side": "Buy", + # "execTime": "1757405601977", + # "seq": 9515624038 + # } + # ] + # } + # + topic = self.safe_string(message, 'topic') + spot = topic == 'ticketInfo' + executionFast = topic == 'execution.fast' + data = self.safe_value(message, 'data', []) + if not isinstance(data, list): + data = self.safe_value(data, 'result', []) + if self.myTrades is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + self.myTrades = ArrayCacheBySymbolById(limit) + trades = self.myTrades + symbols: dict = {} + filterExecTypes = self.handle_option('watchMyTrades', 'filterExecTypes', []) + for i in range(0, len(data)): + rawTrade = data[i] + parsed = None + if spot and not executionFast: + parsed = self.parse_ws_trade(rawTrade) + else: + # filter unified trades + execType = self.safe_string(rawTrade, 'execType', '') + if executionFast: + execType = 'Trade' + if not self.in_array(execType, filterExecTypes): + continue + parsed = self.parse_trade(rawTrade) + symbol = parsed['symbol'] + symbols[symbol] = True + trades.append(parsed) + keys = list(symbols.keys()) + for i in range(0, len(keys)): + currentMessageHash = 'myTrades:' + keys[i] + client.resolve(trades, currentMessageHash) + # non-symbol specific + messageHash = 'myTrades' + client.resolve(trades, messageHash) + + async def watch_positions(self, symbols: Strings = None, since: Int = None, limit: Int = None, params={}) -> List[Position]: + """ + + https://bybit-exchange.github.io/docs/v5/websocket/private/position + + watch all open positions + :param str[] [symbols]: list of unified market symbols + :param int [since]: the earliest time in ms to fetch positions for + :param int [limit]: the maximum number of positions to retrieve + :param dict params: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + await self.load_markets() + method = 'watchPositions' + messageHash = '' + if not self.is_empty(symbols): + symbols = self.market_symbols(symbols) + messageHash = '::' + ','.join(symbols) + firstSymbol = self.safe_string(symbols, 0) + url = await self.get_url_by_market_type(firstSymbol, True, method, params) + messageHash = 'positions' + messageHash + client = self.client(url) + await self.authenticate(url) + self.set_positions_cache(client, symbols) + cache = self.positions + fetchPositionsSnapshot = self.handle_option('watchPositions', 'fetchPositionsSnapshot', True) + awaitPositionsSnapshot = self.handle_option('watchPositions', 'awaitPositionsSnapshot', True) + if fetchPositionsSnapshot and awaitPositionsSnapshot and cache is None: + snapshot = await client.future('fetchPositionsSnapshot') + return self.filter_by_symbols_since_limit(snapshot, symbols, since, limit, True) + topics = ['position'] + newPositions = await self.watch_topics(url, [messageHash], topics, params) + if self.newUpdates: + return newPositions + return self.filter_by_symbols_since_limit(cache, symbols, since, limit, True) + + def set_positions_cache(self, client: Client, symbols: Strings = None): + if self.positions is not None: + return + fetchPositionsSnapshot = self.handle_option('watchPositions', 'fetchPositionsSnapshot', True) + if fetchPositionsSnapshot: + messageHash = 'fetchPositionsSnapshot' + if not (messageHash in client.futures): + client.future(messageHash) + self.spawn(self.load_positions_snapshot, client, messageHash) + else: + self.positions = ArrayCacheBySymbolBySide() + + async def load_positions_snapshot(self, client, messageHash): + # one ws channel gives positions for all types, for snapshot must load all positions + fetchFunctions = [ + self.fetch_positions(None, {'type': 'swap', 'subType': 'linear'}), + self.fetch_positions(None, {'type': 'swap', 'subType': 'inverse'}), + ] + promises = await asyncio.gather(*fetchFunctions) + self.positions = ArrayCacheBySymbolBySide() + cache = self.positions + for i in range(0, len(promises)): + positions = promises[i] + for ii in range(0, len(positions)): + position = positions[ii] + cache.append(position) + # don't remove the future from the .futures cache + future = client.futures[messageHash] + future.resolve(cache) + client.resolve(cache, 'position') + + def handle_positions(self, client, message): + # + # { + # topic: 'position', + # id: '504b2671629b08e3c4f6960382a59363:3bc4028023786545:0:01', + # creationTime: 1694566055295, + # data: [{ + # bustPrice: '15.00', + # category: 'inverse', + # createdTime: '1670083436351', + # cumRealisedPnl: '0.00011988', + # entryPrice: '19358.58553268', + # leverage: '10', + # liqPrice: '15.00', + # markPrice: '25924.00', + # positionBalance: '0.0000156', + # positionIdx: 0, + # positionMM: '0.001', + # positionIM: '0.0000015497', + # positionStatus: 'Normal', + # positionValue: '0.00015497', + # riskId: 1, + # riskLimitValue: '150', + # side: 'Buy', + # size: '3', + # stopLoss: '0.00', + # symbol: 'BTCUSD', + # takeProfit: '0.00', + # tpslMode: 'Full', + # tradeMode: 0, + # autoAddMargin: 1, + # trailingStop: '0.00', + # unrealisedPnl: '0.00003925', + # updatedTime: '1694566055293', + # adlRankIndicator: 3 + # }] + # } + # + # each account is connected to a different endpoint + # and has exactly one subscriptionhash which is the account type + if self.positions is None: + self.positions = ArrayCacheBySymbolBySide() + cache = self.positions + newPositions = [] + rawPositions = self.safe_value(message, 'data', []) + for i in range(0, len(rawPositions)): + rawPosition = rawPositions[i] + position = self.parse_position(rawPosition) + side = self.safe_string(position, 'side') + # hacky solution to handle closing positions + # without crashing, we should handle self properly later + newPositions.append(position) + if side is None or side == '': + # closing update, adding both sides to "reset" both sides + # since we don't know which side is being closed + position['side'] = 'long' + cache.append(position) + position['side'] = 'short' + cache.append(position) + position['side'] = None + else: + # regular update + cache.append(position) + messageHashes = self.find_message_hashes(client, 'positions::') + for i in range(0, len(messageHashes)): + messageHash = messageHashes[i] + parts = messageHash.split('::') + symbolsString = parts[1] + symbols = symbolsString.split(',') + positions = self.filter_by_array(newPositions, 'symbol', symbols, False) + if not self.is_empty(positions): + client.resolve(positions, messageHash) + client.resolve(newPositions, 'positions') + + async def un_watch_positions(self, symbols: Strings = None, params={}) -> Any: + """ + unWatches all open positions + + https://bybit-exchange.github.io/docs/v5/websocket/private/position + + :param str[] [symbols]: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: status of the unwatch request + """ + await self.load_markets() + method = 'watchPositions' + messageHash = 'unsubscribe:positions' + subHash = 'positions' + if not self.is_empty(symbols): + raise NotSupported(self.id + ' unWatchPositions() does not support a symbol parameter, you must unwatch all orders') + url = await self.get_url_by_market_type(None, True, method, params) + await self.authenticate(url) + topics = ['position'] + return await self.un_watch_topics(url, 'positions', symbols, [messageHash], [subHash], topics, params) + + async def watch_liquidations(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Liquidation]: + """ + watch the public liquidations of a trading pair + + https://bybit-exchange.github.io/docs/v5/websocket/public/liquidation + + :param str symbol: unified CCXT market symbol + :param int [since]: the earliest time in ms to fetch liquidations for + :param int [limit]: the maximum number of liquidation structures to retrieve + :param dict [params]: exchange specific parameters for the bitmex api endpoint + :param str [params.method]: exchange specific method, supported: liquidation, allLiquidation + :returns dict: an array of `liquidation structures ` + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + url = await self.get_url_by_market_type(symbol, False, 'watchLiquidations', params) + params = self.clean_params(params) + method = None + method, params = self.handle_option_and_params(params, 'watchLiquidations', 'method', 'liquidation') + messageHash = 'liquidations::' + symbol + topic = method + '.' + market['id'] + newLiquidation = await self.watch_topics(url, [messageHash], [topic], params) + if self.newUpdates: + return newLiquidation + return self.filter_by_symbols_since_limit(self.liquidations, [symbol], since, limit, True) + + def handle_liquidation(self, client: Client, message): + # + # { + # "data": { + # "price": "0.03803", + # "side": "Buy", + # "size": "1637", + # "symbol": "GALAUSDT", + # "updatedTime": 1673251091822 + # }, + # "topic": "liquidation.GALAUSDT", + # "ts": 1673251091822, + # "type": "snapshot" + # } + # + # { + # "topic": "allLiquidation.ROSEUSDT", + # "type": "snapshot", + # "ts": 1739502303204, + # "data": [ + # { + # "T": 1739502302929, + # "s": "ROSEUSDT", + # "S": "Sell", + # "v": "20000", + # "p": "0.04499" + # } + # ] + # } + # + if isinstance(message['data'], list): + rawLiquidations = self.safe_list(message, 'data', []) + for i in range(0, len(rawLiquidations)): + rawLiquidation = rawLiquidations[i] + marketId = self.safe_string(rawLiquidation, 's') + market = self.safe_market(marketId, None, '', 'contract') + symbol = market['symbol'] + liquidation = self.parse_ws_liquidation(rawLiquidation, market) + liquidations = self.safe_value(self.liquidations, symbol) + if liquidations is None: + limit = self.safe_integer(self.options, 'liquidationsLimit', 1000) + liquidations = ArrayCache(limit) + liquidations.append(liquidation) + self.liquidations[symbol] = liquidations + client.resolve([liquidation], 'liquidations') + client.resolve([liquidation], 'liquidations::' + symbol) + else: + rawLiquidation = self.safe_dict(message, 'data', {}) + marketId = self.safe_string(rawLiquidation, 'symbol') + market = self.safe_market(marketId, None, '', 'contract') + symbol = market['symbol'] + liquidation = self.parse_ws_liquidation(rawLiquidation, market) + liquidations = self.safe_value(self.liquidations, symbol) + if liquidations is None: + limit = self.safe_integer(self.options, 'liquidationsLimit', 1000) + liquidations = ArrayCache(limit) + liquidations.append(liquidation) + self.liquidations[symbol] = liquidations + client.resolve([liquidation], 'liquidations') + client.resolve([liquidation], 'liquidations::' + symbol) + + def parse_ws_liquidation(self, liquidation, market=None): + # + # { + # "price": "0.03803", + # "side": "Buy", + # "size": "1637", + # "symbol": "GALAUSDT", + # "updatedTime": 1673251091822 + # } + # + # { + # "T": 1739502302929, + # "s": "ROSEUSDT", + # "S": "Sell", + # "v": "20000", + # "p": "0.04499" + # } + # + marketId = self.safe_string_2(liquidation, 'symbol', 's') + market = self.safe_market(marketId, market, '', 'contract') + timestamp = self.safe_integer_2(liquidation, 'updatedTime', 'T') + return self.safe_liquidation({ + 'info': liquidation, + 'symbol': market['symbol'], + 'contracts': self.safe_number_2(liquidation, 'size', 'v'), + 'contractSize': self.safe_number(market, 'contractSize'), + 'price': self.safe_number_2(liquidation, 'price', 'p'), + 'side': self.safe_string_lower(liquidation, 'side', 'S'), + 'baseValue': None, + 'quoteValue': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + + async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + watches information on multiple orders made by the user + + https://bybit-exchange.github.io/docs/v5/websocket/private/order + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + method = 'watchOrders' + messageHash = 'orders' + if symbol is not None: + symbol = self.symbol(symbol) + messageHash += ':' + symbol + url = await self.get_url_by_market_type(symbol, True, method, params) + await self.authenticate(url) + topicsByMarket: dict = { + 'spot': ['order', 'stopOrder'], + 'unified': ['order'], + 'usdc': ['user.openapi.perp.order'], + } + topics = self.safe_value(topicsByMarket, self.get_private_type(url)) + orders = await self.watch_topics(url, [messageHash], topics, params) + if self.newUpdates: + limit = orders.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(orders, symbol, since, limit, True) + + async def un_watch_orders(self, symbol: Str = None, params={}) -> Any: + """ + unWatches information on multiple orders made by the user + + https://bybit-exchange.github.io/docs/v5/websocket/private/order + + :param str symbol: unified market symbol of the market orders were made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.unifiedMargin]: use unified margin account + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + method = 'watchOrders' + messageHash = 'unsubscribe:orders' + subHash = 'orders' + if symbol is not None: + raise NotSupported(self.id + ' unWatchOrders() does not support a symbol parameter, you must unwatch all orders') + url = await self.get_url_by_market_type(symbol, True, method, params) + await self.authenticate(url) + topicsByMarket: dict = { + 'spot': ['order', 'stopOrder'], + 'unified': ['order'], + 'usdc': ['user.openapi.perp.order'], + } + topics = self.safe_value(topicsByMarket, self.get_private_type(url)) + return await self.un_watch_topics(url, 'orders', [], [messageHash], [subHash], topics, params) + + def handle_order_ws(self, client: Client, message): + # + # { + # "reqId":"1", + # "retCode":0, + # "retMsg":"OK", + # "op":"order.create", + # "data":{ + # "orderId":"1673523595617593600", + # "orderLinkId":"1673523595617593601" + # }, + # "header":{ + # "X-Bapi-Limit":"20", + # "X-Bapi-Limit-Status":"19", + # "X-Bapi-Limit-Reset-Timestamp":"1714235558880", + # "Traceid":"584a06d373f2fdcb3a4dfdd81d27df11", + # "Timenow":"1714235558881" + # }, + # "connId":"cojidqec0hv9fgvhtbt0-40e" + # } + # + messageHash = self.safe_string(message, 'reqId') + data = self.safe_dict(message, 'data') + order = self.parse_order(data) + client.resolve(order, messageHash) + + def handle_order(self, client: Client, message): + # + # spot + # { + # "type": "snapshot", + # "topic": "order", + # "ts": "1662348310441", + # "data": [ + # { + # "e": "order", + # "E": "1662348310441", + # "s": "BTCUSDT", + # "c": "spotx008", + # "S": "BUY", + # "o": "MARKET_OF_QUOTE", + # "f": "GTC", + # "q": "20", + # "p": "0", + # "X": "CANCELED", + # "i": "1238261807653647872", + # "M": "1238225004531834368", + # "l": "0.001007", + # "z": "0.001007", + # "L": "19842.02", + # "n": "0", + # "N": "BTC", + # "u": True, + # "w": True, + # "m": False, + # "O": "1662348310368", + # "Z": "19.98091414", + # "A": "0", + # "C": False, + # "v": "0", + # "d": "NO_LIQ", + # "t": "2100000000002220938" + # } + # ] + # } + # unified + # { + # "id": "5923240c6880ab-c59f-420b-9adb-3639adc9dd90", + # "topic": "order", + # "creationTime": 1672364262474, + # "data": [ + # { + # "symbol": "ETH-30DEC22-1400-C", + # "orderId": "5cf98598-39a7-459e-97bf-76ca765ee020", + # "side": "Sell", + # "orderType": "Market", + # "cancelType": "UNKNOWN", + # "price": "72.5", + # "qty": "1", + # "orderIv": "", + # "timeInForce": "IOC", + # "orderStatus": "Filled", + # "orderLinkId": "", + # "lastPriceOnCreated": "", + # "reduceOnly": False, + # "leavesQty": "", + # "leavesValue": "", + # "cumExecQty": "1", + # "cumExecValue": "75", + # "avgPrice": "75", + # "blockTradeId": "", + # "positionIdx": 0, + # "cumExecFee": "0.358635", + # "createdTime": "1672364262444", + # "updatedTime": "1672364262457", + # "rejectReason": "EC_NoError", + # "stopOrderType": "", + # "triggerPrice": "", + # "takeProfit": "", + # "stopLoss": "", + # "tpTriggerBy": "", + # "slTriggerBy": "", + # "triggerDirection": 0, + # "triggerBy": "", + # "closeOnTrigger": False, + # "category": "option" + # } + # ] + # } + # + if self.orders is None: + limit = self.safe_integer(self.options, 'ordersLimit', 1000) + self.orders = ArrayCacheBySymbolById(limit) + orders = self.orders + rawOrders = self.safe_value(message, 'data', []) + first = self.safe_value(rawOrders, 0, {}) + category = self.safe_string(first, 'category') + isSpot = category == 'spot' + if not isSpot: + rawOrders = self.safe_value(rawOrders, 'result', rawOrders) + symbols: dict = {} + for i in range(0, len(rawOrders)): + parsed = self.parse_order(rawOrders[i]) + # if isSpot: + # parsed = self.parseWsSpotOrder(rawOrders[i]) + # else: + # parsed = self.parse_order(rawOrders[i]) + # } + symbol = parsed['symbol'] + symbols[symbol] = True + orders.append(parsed) + symbolsArray = list(symbols.keys()) + for i in range(0, len(symbolsArray)): + currentMessageHash = 'orders:' + symbolsArray[i] + client.resolve(orders, currentMessageHash) + messageHash = 'orders' + client.resolve(orders, messageHash) + + async def watch_balance(self, params={}) -> Balances: + """ + watch balance and get the amount of funds available for trading or funds locked in orders + + https://bybit-exchange.github.io/docs/v5/websocket/private/wallet + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + method = 'watchBalance' + messageHash = 'balances' + type = None + type, params = self.handle_market_type_and_params('watchBalance', None, params) + subType = None + subType, params = self.handle_sub_type_and_params('watchBalance', None, params) + unified = await self.isUnifiedEnabled() + isUnifiedMargin = self.safe_bool(unified, 0, False) + isUnifiedAccount = self.safe_bool(unified, 1, False) + url = await self.get_url_by_market_type(None, True, method, params) + await self.authenticate(url) + topicByMarket: dict = { + 'spot': 'outboundAccountInfo', + 'unified': 'wallet', + } + if isUnifiedAccount: + # unified account + if subType == 'inverse': + messageHash += ':contract' + else: + messageHash += ':unified' + if not isUnifiedMargin and not isUnifiedAccount: + # normal account using v5 + if type == 'spot': + messageHash += ':spot' + else: + messageHash += ':contract' + if isUnifiedMargin: + # unified margin account using v5 + if type == 'spot': + messageHash += ':spot' + else: + if subType == 'linear': + messageHash += ':unified' + else: + messageHash += ':contract' + topics = [self.safe_value(topicByMarket, self.get_private_type(url))] + return await self.watch_topics(url, [messageHash], topics, params) + + def handle_balance(self, client: Client, message): + # + # spot + # { + # "type": "snapshot", + # "topic": "outboundAccountInfo", + # "ts": "1662107217641", + # "data": [ + # { + # "e": "outboundAccountInfo", + # "E": "1662107217640", + # "T": True, + # "W": True, + # "D": True, + # "B": [ + # { + # "a": "USDT", + # "f": "176.81254174", + # "l": "201.575" + # } + # ] + # } + # ] + # } + # unified + # { + # "id": "5923242c464be9-25ca-483d-a743-c60101fc656f", + # "topic": "wallet", + # "creationTime": 1672364262482, + # "data": [ + # { + # "accountIMRate": "0.016", + # "accountMMRate": "0.003", + # "totalEquity": "12837.78330098", + # "totalWalletBalance": "12840.4045924", + # "totalMarginBalance": "12837.78330188", + # "totalAvailableBalance": "12632.05767702", + # "totalPerpUPL": "-2.62129051", + # "totalInitialMargin": "205.72562486", + # "totalMaintenanceMargin": "39.42876721", + # "coin": [ + # { + # "coin": "USDC", + # "equity": "200.62572554", + # "usdValue": "200.62572554", + # "walletBalance": "201.34882644", + # "availableToWithdraw": "0", + # "availableToBorrow": "1500000", + # "borrowAmount": "0", + # "accruedInterest": "0", + # "totalOrderIM": "0", + # "totalPositionIM": "202.99874213", + # "totalPositionMM": "39.14289747", + # "unrealisedPnl": "74.2768991", + # "cumRealisedPnl": "-209.1544627", + # "bonus": "0" + # }, + # { + # "coin": "BTC", + # "equity": "0.06488393", + # "usdValue": "1023.08402268", + # "walletBalance": "0.06488393", + # "availableToWithdraw": "0.06488393", + # "availableToBorrow": "2.5", + # "borrowAmount": "0", + # "accruedInterest": "0", + # "totalOrderIM": "0", + # "totalPositionIM": "0", + # "totalPositionMM": "0", + # "unrealisedPnl": "0", + # "cumRealisedPnl": "0", + # "bonus": "0" + # }, + # { + # "coin": "ETH", + # "equity": "0", + # "usdValue": "0", + # "walletBalance": "0", + # "availableToWithdraw": "0", + # "availableToBorrow": "26", + # "borrowAmount": "0", + # "accruedInterest": "0", + # "totalOrderIM": "0", + # "totalPositionIM": "0", + # "totalPositionMM": "0", + # "unrealisedPnl": "0", + # "cumRealisedPnl": "0", + # "bonus": "0" + # }, + # { + # "coin": "USDT", + # "equity": "11726.64664904", + # "usdValue": "11613.58597018", + # "walletBalance": "11728.54414904", + # "availableToWithdraw": "11723.92075829", + # "availableToBorrow": "2500000", + # "borrowAmount": "0", + # "accruedInterest": "0", + # "totalOrderIM": "0", + # "totalPositionIM": "2.72589075", + # "totalPositionMM": "0.28576575", + # "unrealisedPnl": "-1.8975", + # "cumRealisedPnl": "0.64782276", + # "bonus": "0" + # }, + # { + # "coin": "EOS3L", + # "equity": "215.0570412", + # "usdValue": "0", + # "walletBalance": "215.0570412", + # "availableToWithdraw": "215.0570412", + # "availableToBorrow": "0", + # "borrowAmount": "0", + # "accruedInterest": "", + # "totalOrderIM": "0", + # "totalPositionIM": "0", + # "totalPositionMM": "0", + # "unrealisedPnl": "0", + # "cumRealisedPnl": "0", + # "bonus": "0" + # }, + # { + # "coin": "BIT", + # "equity": "1.82", + # "usdValue": "0.48758257", + # "walletBalance": "1.82", + # "availableToWithdraw": "1.82", + # "availableToBorrow": "0", + # "borrowAmount": "0", + # "accruedInterest": "", + # "totalOrderIM": "0", + # "totalPositionIM": "0", + # "totalPositionMM": "0", + # "unrealisedPnl": "0", + # "cumRealisedPnl": "0", + # "bonus": "0" + # } + # ], + # "accountType": "UNIFIED" + # } + # ] + # } + # + if self.balance is None: + self.balance = {} + messageHash = 'balance' + topic = self.safe_value(message, 'topic') + info = None + rawBalances = [] + account = None + if topic == 'outboundAccountInfo': + account = 'spot' + data = self.safe_value(message, 'data', []) + for i in range(0, len(data)): + B = self.safe_value(data[i], 'B', []) + rawBalances = self.array_concat(rawBalances, B) + info = rawBalances + if topic == 'wallet': + data = self.safe_value(message, 'data', {}) + for i in range(0, len(data)): + result = self.safe_value(data, 0, {}) + account = self.safe_string_lower(result, 'accountType') + rawBalances = self.array_concat(rawBalances, self.safe_value(result, 'coin', [])) + info = data + for i in range(0, len(rawBalances)): + self.parse_ws_balance(rawBalances[i], account) + if account is not None: + if self.safe_value(self.balance, account) is None: + self.balance[account] = {} + self.balance[account]['info'] = info + timestamp = self.safe_integer(message, 'ts') + self.balance[account]['timestamp'] = timestamp + self.balance[account]['datetime'] = self.iso8601(timestamp) + self.balance[account] = self.safe_balance(self.balance[account]) + messageHash = 'balances:' + account + client.resolve(self.balance[account], messageHash) + else: + self.balance['info'] = info + timestamp = self.safe_integer(message, 'ts') + self.balance['timestamp'] = timestamp + self.balance['datetime'] = self.iso8601(timestamp) + self.balance = self.safe_balance(self.balance) + messageHash = 'balances' + client.resolve(self.balance, messageHash) + + def parse_ws_balance(self, balance, accountType=None): + # + # spot + # { + # "a": "USDT", + # "f": "176.81254174", + # "l": "201.575" + # } + # unified + # { + # "coin": "BTC", + # "equity": "0.06488393", + # "usdValue": "1023.08402268", + # "walletBalance": "0.06488393", + # "availableToWithdraw": "0.06488393", + # "availableToBorrow": "2.5", + # "borrowAmount": "0", + # "accruedInterest": "0", + # "totalOrderIM": "0", + # "totalPositionIM": "0", + # "totalPositionMM": "0", + # "unrealisedPnl": "0", + # "cumRealisedPnl": "0", + # "bonus": "0" + # } + # + account = self.account() + currencyId = self.safe_string_2(balance, 'a', 'coin') + code = self.safe_currency_code(currencyId) + account['free'] = self.safe_string_n(balance, ['availableToWithdraw', 'f', 'free', 'availableToWithdraw']) + account['used'] = self.safe_string_2(balance, 'l', 'locked') + account['total'] = self.safe_string(balance, 'walletBalance') + if accountType is not None: + if self.safe_value(self.balance, accountType) is None: + self.balance[accountType] = {} + self.balance[accountType][code] = account + else: + self.balance[code] = account + + async def watch_topics(self, url, messageHashes, topics, params={}): + request: dict = { + 'op': 'subscribe', + 'req_id': self.request_id(), + 'args': topics, + } + message = self.extend(request, params) + return await self.watch_multiple(url, messageHashes, message, messageHashes) + + async def un_watch_topics(self, url: str, topic: str, symbols: Strings, messageHashes: List[str], subMessageHashes: List[str], topics, params={}, subExtension={}): + reqId = self.request_id() + request: dict = { + 'op': 'unsubscribe', + 'req_id': reqId, + 'args': topics, + } + subscription = { + 'id': reqId, + 'topic': topic, + 'messageHashes': messageHashes, + 'subMessageHashes': subMessageHashes, + 'symbols': symbols, + } + message = self.extend(request, params) + return await self.watch_multiple(url, messageHashes, message, messageHashes, self.extend(subscription, subExtension)) + + async def authenticate(self, url, params={}): + self.check_required_credentials() + messageHash = 'authenticated' + client = self.client(url) + future = client.reusableFuture(messageHash) + authenticated = self.safe_value(client.subscriptions, messageHash) + if authenticated is None: + expiresInt = self.milliseconds() + 10000 + expires = self.number_to_string(expiresInt) + path = 'GET/realtime' + auth = path + expires + signature = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256, 'hex') + request: dict = { + 'op': 'auth', + 'args': [ + self.apiKey, expires, signature, + ], + } + message = self.extend(request, params) + self.watch(url, messageHash, message, messageHash) + return await future + + def handle_error_message(self, client: Client, message) -> Bool: + # + # { + # "success": False, + # "ret_msg": "error:invalid op", + # "conn_id": "5e079fdd-9c7f-404d-9dbf-969d650838b5", + # "request": {op: '', args: null} + # } + # + # auth error + # + # { + # "success": False, + # "ret_msg": "error:USVC1111", + # "conn_id": "e73770fb-a0dc-45bd-8028-140e20958090", + # "request": { + # "op": "auth", + # "args": [ + # "9rFT6uR4uz9Imkw4Wx", + # "1653405853543", + # "542e71bd85597b4db0290f0ce2d13ed1fd4bb5df3188716c1e9cc69a879f7889" + # ] + # } + # + # {code: '-10009', desc: "Invalid period!"} + # + # { + # "reqId":"1", + # "retCode":170131, + # "retMsg":"Insufficient balance.", + # "op":"order.create", + # "data":{ + # + # }, + # "header":{ + # "X-Bapi-Limit":"20", + # "X-Bapi-Limit-Status":"19", + # "X-Bapi-Limit-Reset-Timestamp":"1714236608944", + # "Traceid":"3d7168a137bf32a947b7e5e6a575ac7f", + # "Timenow":"1714236608946" + # }, + # "connId":"cojifin88smerbj9t560-406" + # } + # + code = self.safe_string_n(message, ['code', 'ret_code', 'retCode']) + try: + if code is not None and code != '0': + feedback = self.id + ' ' + self.json(message) + self.throw_exactly_matched_exception(self.exceptions['exact'], code, feedback) + msg = self.safe_string_2(message, 'retMsg', 'ret_msg') + self.throw_broadly_matched_exception(self.exceptions['broad'], msg, feedback) + raise ExchangeError(feedback) + success = self.safe_value(message, 'success') + if success is not None and not success: + ret_msg = self.safe_string(message, 'ret_msg') + request = self.safe_value(message, 'request', {}) + op = self.safe_string(request, 'op') + if op == 'auth': + raise AuthenticationError('Authentication failed: ' + ret_msg) + else: + raise ExchangeError(self.id + ' ' + ret_msg) + return False + except Exception as error: + if isinstance(error, AuthenticationError): + messageHash = 'authenticated' + client.reject(error, messageHash) + if messageHash in client.subscriptions: + del client.subscriptions[messageHash] + else: + messageHash = self.safe_string(message, 'reqId') + client.reject(error, messageHash) + return True + + def handle_message(self, client: Client, message): + topic = self.safe_string_2(message, 'topic', 'op', '') + if self.handle_error_message(client, message): + return + # contract pong + ret_msg = self.safe_string(message, 'ret_msg') + if (ret_msg == 'pong') or (topic == 'pong'): + self.handle_pong(client, message) + return + # spot pong + pong = self.safe_integer(message, 'pong') + if pong is not None: + self.handle_pong(client, message) + return + # pong + event = self.safe_string(message, 'event') + if event == 'sub' or (topic == 'subscribe'): + self.handle_subscription_status(client, message) + return + methods: dict = { + 'orderbook': self.handle_order_book, + 'kline': self.handle_ohlcv, + 'order': self.handle_order, + 'stopOrder': self.handle_order, + 'ticker': self.handle_ticker, + 'trade': self.handle_trades, + 'publicTrade': self.handle_trades, + 'depth': self.handle_order_book, + 'wallet': self.handle_balance, + 'outboundAccountInfo': self.handle_balance, + 'execution': self.handle_my_trades, + 'execution.fast': self.handle_my_trades, + 'ticketInfo': self.handle_my_trades, + 'user.openapi.perp.trade': self.handle_my_trades, + 'position': self.handle_positions, + 'liquidation': self.handle_liquidation, + 'allLiquidation': self.handle_liquidation, + 'pong': self.handle_pong, + 'order.create': self.handle_order_ws, + 'order.amend': self.handle_order_ws, + 'order.cancel': self.handle_order_ws, + 'auth': self.handle_authenticate, + 'unsubscribe': self.handle_un_subscribe, + } + exacMethod = self.safe_value(methods, topic) + if exacMethod is not None: + exacMethod(client, message) + return + keys = list(methods.keys()) + for i in range(0, len(keys)): + key = keys[i] + if topic.find(keys[i]) >= 0: + method = methods[key] + method(client, message) + return + # unified auth acknowledgement + type = self.safe_string(message, 'type') + if type == 'AUTH_RESP': + self.handle_authenticate(client, message) + + def ping(self, client: Client): + return { + 'req_id': self.request_id(), + 'op': 'ping', + } + + def handle_pong(self, client: Client, message): + # + # { + # "success": True, + # "ret_msg": "pong", + # "conn_id": "db3158a0-8960-44b9-a9de-ac350ee13158", + # "request": {op: "ping", args: null} + # } + # + # {pong: 1653296711335} + # + # + # { + # "req_id": "2", + # "op": "pong", + # "args": ["1757405570352"], + # "conn_id": "d266o6hqo29sqmnq4vk0-1yus1" + # } + # + client.lastPong = self.safe_integer(message, 'pong') + return message + + def handle_authenticate(self, client: Client, message): + # + # { + # "success": True, + # "ret_msg": '', + # "op": "auth", + # "conn_id": "ce3dpomvha7dha97tvp0-2xh" + # } + # + # { + # "retCode":0, + # "retMsg":"OK", + # "op":"auth", + # "connId":"cojifin88smerbj9t560-404" + # } + # + # { + # "success": True, + # "ret_msg": "", + # "op": "auth", + # "conn_id": "d266o6hqo29sqmnq4vk0-1yus1" + # } + # + success = self.safe_value(message, 'success') + code = self.safe_integer(message, 'retCode') + messageHash = 'authenticated' + if success or code == 0: + future = self.safe_value(client.futures, messageHash) + future.resolve(True) + else: + error = AuthenticationError(self.id + ' ' + self.json(message)) + client.reject(error, messageHash) + if messageHash in client.subscriptions: + del client.subscriptions[messageHash] + return message + + def handle_subscription_status(self, client: Client, message): + # + # { + # "topic": "kline", + # "event": "sub", + # "params": { + # "symbol": "LTCUSDT", + # "binary": "false", + # "klineType": "1m", + # "symbolName": "LTCUSDT" + # }, + # "code": "0", + # "msg": "Success" + # } + # + return message + + def handle_un_subscribe(self, client: Client, message): + # + # {"success":true,"ret_msg":"","conn_id":"7188110e-6908-41e9-b863-6365127e92ad","req_id":"3","op":"unsubscribe"} + # + # client.subscription will be something like: + # { + # "publicTrade.LTCUSDT":true, + # "publicTrade.ADAUSDT":true, + # "unsubscribe:trade:LTC/USDT:USDT": { + # "id":4, + # "subHash": "trade:LTC/USDT" + # }, + # } + reqId = self.safe_string(message, 'req_id') + keys = list(client.subscriptions.keys()) + for i in range(0, len(keys)): + messageHash = keys[i] + if not (messageHash in client.subscriptions): + continue + # the previous iteration can have deleted the messageHash from the subscriptions + if messageHash.startswith('unsubscribe'): + subscription = client.subscriptions[messageHash] + subId = self.safe_string(subscription, 'id') + if reqId != subId: + continue + messageHashes = self.safe_list(subscription, 'messageHashes', []) + subMessageHashes = self.safe_list(subscription, 'subMessageHashes', []) + for j in range(0, len(messageHashes)): + unsubHash = messageHashes[j] + subHash = subMessageHashes[j] + usePrefix = (subHash == 'orders') or (subHash == 'myTrades') or (subHash == 'positions') + self.clean_unsubscription(client, subHash, unsubHash, usePrefix) + self.clean_cache(subscription) + return message diff --git a/ccxt/pro/cex.py b/ccxt/pro/cex.py new file mode 100644 index 0000000..6a51514 --- /dev/null +++ b/ccxt/pro/cex.py @@ -0,0 +1,1476 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolById, ArrayCacheByTimestamp +import hashlib +from ccxt.base.types import Any, Balances, Bool, Int, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade +from ccxt.async_support.base.ws.client import Client +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.precise import Precise + + +class cex(ccxt.async_support.cex): + + def describe(self) -> Any: + return self.deep_extend(super(cex, self).describe(), { + 'has': { + 'ws': True, + 'watchBalance': True, + 'watchTicker': True, + 'watchTickers': True, + 'watchTrades': True, + 'watchTradesForSymbols': False, + 'watchMyTrades': True, + 'watchOrders': True, + 'watchOrderBook': True, + 'watchOHLCV': True, + 'watchPosition': None, + 'createOrderWs': True, + 'editOrderWs': True, + 'cancelOrderWs': True, + 'cancelOrdersWs': True, + 'fetchOrderWs': True, + 'fetchOpenOrdersWs': True, + 'fetchTickerWs': True, + 'fetchBalanceWs': True, + }, + 'urls': { + 'api': { + 'ws': 'wss://ws.cex.io/ws', + }, + }, + 'options': { + 'orderbook': {}, + }, + 'streaming': { + }, + 'exceptions': { + }, + }) + + def request_id(self): + requestId = self.sum(self.safe_integer(self.options, 'requestId', 0), 1) + self.options['requestId'] = requestId + return str(requestId) + + async def watch_balance(self, params={}) -> Balances: + """ + watch balance and get the amount of funds available for trading or funds locked in orders + + https://cex.io/websocket-api#get-balance + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.authenticate(params) + messageHash = self.request_id() + url = self.urls['api']['ws'] + subscribe: dict = { + 'e': 'get-balance', + 'data': {}, + 'oid': self.request_id(), + } + request = self.deep_extend(subscribe, params) + return await self.watch(url, messageHash, request, messageHash, request) + + def handle_balance(self, client: Client, message): + # + # { + # "e": "get-balance", + # "data": { + # "balance": { + # "BTC": "0.00000000", + # "USD": "0.00", + # ... + # }, + # "obalance": { + # "BTC": "0.00000000", + # "USD": "0.00", + # ... + # }, + # "time": 1663761159605 + # }, + # "oid": 1, + # "ok": "ok" + # } + # + data = self.safe_value(message, 'data', {}) + freeBalance = self.safe_value(data, 'balance', {}) + usedBalance = self.safe_value(data, 'obalance', {}) + result: dict = { + 'info': data, + } + currencyIds = list(freeBalance.keys()) + for i in range(0, len(currencyIds)): + currencyId = currencyIds[i] + account = self.account() + account['free'] = self.safe_string(freeBalance, currencyId) + account['used'] = self.safe_string(usedBalance, currencyId) + code = self.safe_currency_code(currencyId) + result[code] = account + self.balance = self.safe_balance(result) + messageHash = self.safe_string(message, 'oid') + client.resolve(self.balance, messageHash) + + async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol. Note: can only watch one symbol at a time. + + https://cex.io/websocket-api#old-pair-room + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + url = self.urls['api']['ws'] + messageHash = 'trades' + subscriptionHash = 'old:' + symbol + self.options['currentWatchTradeSymbol'] = symbol # exchange supports only 1 symbol for self watchTrades channel + client = self.safe_value(self.clients, url) + if client is not None: + subscriptionKeys = list(client.subscriptions.keys()) + for i in range(0, len(subscriptionKeys)): + subscriptionKey = subscriptionKeys[i] + if subscriptionKey == subscriptionHash: + continue + subscriptionKey = subscriptionKey[0:3] + if subscriptionKey == 'old': + raise ExchangeError(self.id + ' watchTrades() only supports watching one symbol at a time.') + message: dict = { + 'e': 'subscribe', + 'rooms': ['pair-' + market['base'] + '-' + market['quote']], + } + request = self.deep_extend(message, params) + trades = await self.watch(url, messageHash, request, subscriptionHash) + # assing symbol to the trades does not contain symbol information + for i in range(0, len(trades)): + trades[i]['symbol'] = symbol + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + def handle_trades_snapshot(self, client: Client, message): + # + # { + # "e": "history", + # "data": [ + # 'buy:1710255706095:444444:71222.2:14892622' + # 'sell:1710255658251:42530:71300:14892621' + # 'buy:1710252424241:87913:72800:14892620' + # ... timestamp descending + # ] + # } + # + data = self.safe_list(message, 'data', []) + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + stored = ArrayCache(limit) + symbol = self.safe_string(self.options, 'currentWatchTradeSymbol') + if symbol is None: + return + market = self.market(symbol) + dataLength = len(data) + for i in range(0, dataLength): + index = dataLength - 1 - i + rawTrade = data[index] + parsed = self.parse_ws_old_trade(rawTrade, market) + stored.append(parsed) + messageHash = 'trades' + self.trades = stored # trades don't have symbol + client.resolve(self.trades, messageHash) + + def parse_ws_old_trade(self, trade, market=None): + # + # snapshot trade + # "sell:1665467367741:3888551:19058.8:14541219" + # update trade + # ['buy', '1665467516704', '98070', "19057.7", "14541220"] + # + if not isinstance(trade, list): + trade = trade.split(':') + side = self.safe_string(trade, 0) + timestamp = self.safe_integer(trade, 1) + amount = self.safe_string(trade, 2) + price = self.safe_string(trade, 3) + id = self.safe_string(trade, 4) + return self.safe_trade({ + 'info': trade, + 'id': id, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': self.safe_string(market, 'symbol'), + 'type': None, + 'side': side, + 'order': None, + 'takerOrMaker': None, + 'price': price, + 'amount': amount, + 'cost': None, + 'fee': None, + }, market) + + def handle_trade(self, client: Client, message): + # + # { + # "e": "history-update", + # "data": [ + # ['buy', '1665467516704', '98070', "19057.7", "14541220"] + # ] + # } + # + data = self.safe_value(message, 'data', []) + stored = self.trades # to do fix self, self.trades is not meant to be used like self + dataLength = len(data) + for i in range(0, dataLength): + index = dataLength - 1 - i + rawTrade = data[index] + parsed = self.parse_ws_old_trade(rawTrade) + stored.append(parsed) + messageHash = 'trades' + self.trades = stored + client.resolve(self.trades, messageHash) + + async def watch_ticker(self, symbol: str, params={}) -> Ticker: + """ + + https://cex.io/websocket-api#ticker-subscription + + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.method]: public or private + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + url = self.urls['api']['ws'] + messageHash = 'ticker:' + symbol + method = self.safe_string(params, 'method', 'private') # default to private because the specified ticker is received quicker + message = { + 'e': 'subscribe', + 'rooms': [ + 'tickers', + ], + } + subscriptionHash = 'tickers' + if method == 'private': + await self.authenticate() + message = { + 'e': 'ticker', + 'data': [ + market['baseId'], market['quoteId'], + ], + 'oid': self.request_id(), + } + subscriptionHash = 'ticker:' + symbol + request = self.deep_extend(message, params) + return await self.watch(url, messageHash, request, subscriptionHash) + + async def watch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + + https://cex.io/websocket-api#ticker-subscription + + watches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + url = self.urls['api']['ws'] + messageHash = 'tickers' + message: dict = { + 'e': 'subscribe', + 'rooms': [ + 'tickers', + ], + } + request = self.deep_extend(message, params) + ticker = await self.watch(url, messageHash, request, messageHash) + tickerSymbol = ticker['symbol'] + if symbols is not None and not self.in_array(tickerSymbol, symbols): + return await self.watch_tickers(symbols, params) + if self.newUpdates: + result: dict = {} + result[tickerSymbol] = ticker + return result + return self.filter_by_array(self.tickers, 'symbol', symbols) + + async def fetch_ticker_ws(self, symbol: str, params={}) -> Ticker: + """ + + https://docs.cex.io/#ws-api-ticker-deprecated + + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the cex api endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + url = self.urls['api']['ws'] + messageHash = self.request_id() + request = self.extend({ + 'e': 'ticker', + 'oid': messageHash, + 'data': [market['base'], market['quote']], + }, params) + return await self.watch(url, messageHash, request, messageHash) + + def handle_ticker(self, client: Client, message): + # + # { + # "e": "tick", + # "data": { + # "symbol1": "LRC", + # "symbol2": "USD", + # "price": "0.305", + # "open24": "0.301", + # "volume": "241421.641700" + # } + # } + # + data = self.safe_value(message, 'data', {}) + ticker = self.parse_ws_ticker(data) + symbol = ticker['symbol'] + if symbol is None: + return + self.tickers[symbol] = ticker + messageHash = 'ticker:' + symbol + client.resolve(ticker, messageHash) + client.resolve(ticker, 'tickers') + messageHash = self.safe_string(message, 'oid') + if messageHash is not None: + client.resolve(ticker, messageHash) + + def parse_ws_ticker(self, ticker, market=None): + # + # public + # { + # "symbol1": "LRC", + # "symbol2": "USD", + # "price": "0.305", + # "open24": "0.301", + # "volume": "241421.641700" + # } + # private + # { + # "timestamp": "1663764969", + # "low": "18756.3", + # "high": "19200", + # "last": "19200", + # "volume": "0.94735907", + # "volume30d": "64.61299999", + # "bid": 19217.2, + # "ask": 19247.5, + # "priceChange": "44.3", + # "priceChangePercentage": "0.23", + # "pair": ["BTC", "USDT"] + # } + pair = self.safe_value(ticker, 'pair', []) + baseId = self.safe_string(ticker, 'symbol1') + if baseId is None: + baseId = self.safe_string(pair, 0) + quoteId = self.safe_string(ticker, 'symbol2') + if quoteId is None: + quoteId = self.safe_string(pair, 1) + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + symbol = base + '/' + quote + timestamp = self.safe_integer(ticker, 'timestamp') + if timestamp is not None: + timestamp = timestamp * 1000 + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': self.safe_string(ticker, 'bid'), + 'bidVolume': None, + 'ask': self.safe_string(ticker, 'ask'), + 'askVolume': None, + 'vwap': None, + 'open': self.safe_string(ticker, 'open24'), + 'close': None, + 'last': self.safe_string_2(ticker, 'price', 'last'), + 'previousClose': None, + 'change': self.safe_string(ticker, 'priceChange'), + 'percentage': self.safe_string(ticker, 'priceChangePercentage'), + 'average': None, + 'baseVolume': None, + 'quoteVolume': self.safe_string(ticker, 'volume'), + 'info': ticker, + }, market) + + async def fetch_balance_ws(self, params={}) -> Balances: + """ + + https://docs.cex.io/#ws-api-get-balance + + query for balance and get the amount of funds available for trading or funds locked in orders + :param dict [params]: extra parameters specific to the cex api endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + await self.authenticate() + url = self.urls['api']['ws'] + messageHash = self.request_id() + request = self.extend({ + 'e': 'get-balance', + 'oid': messageHash, + }, params) + return await self.watch(url, messageHash, request, messageHash) + + async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + get the list of orders associated with the user. Note: In CEX.IO system, orders can be present in trade engine or in archive database. There can be time periods(~2 seconds or more), when order is done/canceled, but still not moved to archive database. That means, you cannot see it using calls: archived-orders/open-orders. + + https://docs.cex.io/#ws-api-open-orders + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' watchOrders() requires a symbol argument') + await self.load_markets() + await self.authenticate(params) + url = self.urls['api']['ws'] + market = self.market(symbol) + symbol = market['symbol'] + messageHash = 'orders:' + symbol + message: dict = { + 'e': 'open-orders', + 'data': { + 'pair': [ + market['baseId'], + market['quoteId'], + ], + }, + 'oid': symbol, + } + request = self.deep_extend(message, params) + orders = await self.watch(url, messageHash, request, messageHash, request) + if self.newUpdates: + limit = orders.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(orders, symbol, since, limit, True) + + async def watch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of trades associated with the user. Note: In CEX.IO system, orders can be present in trade engine or in archive database. There can be time periods(~2 seconds or more), when order is done/canceled, but still not moved to archive database. That means, you cannot see it using calls: archived-orders/open-orders. + + https://docs.cex.io/#ws-api-open-orders + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' watchMyTrades() requires a symbol argument') + await self.load_markets() + await self.authenticate(params) + url = self.urls['api']['ws'] + market = self.market(symbol) + messageHash = 'myTrades:' + market['symbol'] + subscriptionHash = 'orders:' + market['symbol'] + message: dict = { + 'e': 'open-orders', + 'data': { + 'pair': [ + market['baseId'], + market['quoteId'], + ], + }, + 'oid': market['symbol'], + } + request = self.deep_extend(message, params) + orders = await self.watch(url, messageHash, request, subscriptionHash, request) + return self.filter_by_symbol_since_limit(orders, market['symbol'], since, limit) + + def handle_transaction(self, client: Client, message): + data = self.safe_value(message, 'data') + symbol2 = self.safe_string(data, 'symbol2') + if symbol2 is None: + return + self.handle_order_update(client, message) + self.handle_my_trades(client, message) + + def handle_my_trades(self, client: Client, message): + # + # { + # "e": "tx", + # "data": { + # "d": "order:59091012956:a:USD", + # "c": "user:up105393824:a:USD", + # "a": "0.01", + # "ds": 0, + # "cs": "15.27", + # "user": "up105393824", + # "symbol": "USD", + # "order": 59091012956, + # "amount": "-18.49", + # "type": "buy", + # "time": "2022-09-24T19:36:18.466Z", + # "balance": "15.27", + # "id": "59091012966" + # } + # } + # { + # "e": "tx", + # "data": { + # "d": "order:59091012956:a:BTC", + # "c": "user:up105393824:a:BTC", + # "a": "0.00096420", + # "ds": 0, + # "cs": "0.00096420", + # "user": "up105393824", + # "symbol": "BTC", + # "symbol2": "USD", + # "amount": "0.00096420", + # "buy": 59091012956, + # "order": 59091012956, + # "sell": 59090796005, + # "price": 19135, + # "type": "buy", + # "time": "2022-09-24T19:36:18.466Z", + # "balance": "0.00096420", + # "fee_amount": "0.05", + # "id": "59091012962" + # } + # } + data = self.safe_value(message, 'data', {}) + stored = self.myTrades + if stored is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + stored = ArrayCacheBySymbolById(limit) + self.myTrades = stored + trade = self.parse_ws_trade(data) + stored.append(trade) + messageHash = 'myTrades:' + trade['symbol'] + client.resolve(stored, messageHash) + + def parse_ws_trade(self, trade, market=None): + # + # { + # "d": "order:59091012956:a:BTC", + # "c": "user:up105393824:a:BTC", + # "a": "0.00096420", + # "ds": 0, + # "cs": "0.00096420", + # "user": "up105393824", + # "symbol": "BTC", + # "symbol2": "USD", + # "amount": "0.00096420", + # "buy": 59091012956, + # "order": 59091012956, + # "sell": 59090796005, + # "price": 19135, + # "type": "buy", + # "time": "2022-09-24T19:36:18.466Z", + # "balance": "0.00096420", + # "fee_amount": "0.05", + # "id": "59091012962" + # } + # Note symbol and symbol2 are inverse on sell and ammount is in symbol currency. + # + side = self.safe_string(trade, 'type') + price = self.safe_string(trade, 'price') + datetime = self.safe_string(trade, 'time') + baseId = self.safe_string(trade, 'symbol') + quoteId = self.safe_string(trade, 'symbol2') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + symbol = base + '/' + quote + amount = self.safe_string(trade, 'amount') + if side == 'sell': + symbol = quote + '/' + base + amount = Precise.string_div(amount, price) # due to rounding errors amount in not exact to trade + parsedTrade: dict = { + 'id': self.safe_string(trade, 'id'), + 'order': self.safe_string(trade, 'order'), + 'info': trade, + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + 'symbol': symbol, + 'type': None, + 'side': side, + 'takerOrMaker': None, + 'price': price, + 'cost': None, + 'amount': amount, + 'fee': None, + } + fee = self.safe_string(trade, 'fee_amount') + if fee is not None: + parsedTrade['fee'] = { + 'cost': fee, + 'currency': quote, + 'rate': None, + } + return self.safe_trade(parsedTrade, market) + + def handle_order_update(self, client: Client, message): + # + # partialExecution + # { + # "e": "order", + # "data": { + # "id": "150714937", + # "remains": "1000000", + # "price": "17513", + # "amount": 2000000, As Precision + # "time": "1654506118448", + # "type": "buy", + # "pair": { + # "symbol1": "BTC", + # "symbol2": "USD" + # }, + # "fee": "0.15" + # } + # } + # canceled order + # { + # "e": "order", + # "data": { + # "id": "6310857", + # "remains": "200000000" + # "fremains": "2.00000000" + # "cancel": True, + # "pair": { + # "symbol1": "BTC", + # "symbol2": "USD" + # } + # } + # } + # fulfilledOrder + # { + # "e": "order", + # "data": { + # "id": "59098421630", + # "remains": "0", + # "pair": { + # "symbol1": "BTC", + # "symbol2": "USD" + # } + # } + # } + # { + # "e": "tx", + # "data": { + # "d": "order:59425993014:a:BTC", + # "c": "user:up105393824:a:BTC", + # "a": "0.00098152", + # "ds": 0, + # "cs": "0.00098152", + # "user": "up105393824", + # "symbol": "BTC", + # "symbol2": "USD", + # "amount": "0.00098152", + # "buy": 59425993014, + # "order": 59425993014, + # "sell": 59425986168, + # "price": 19306.6, + # "type": "buy", + # "time": "2022-10-02T01:11:15.148Z", + # "balance": "0.00098152", + # "fee_amount": "0.05", + # "id": "59425993020" + # } + # } + # + data = self.safe_value(message, 'data', {}) + isTransaction = self.safe_string(message, 'e') == 'tx' + orderId = self.safe_string_2(data, 'id', 'order') + remains = self.safe_string(data, 'remains') + baseId = self.safe_string(data, 'symbol') + quoteId = self.safe_string(data, 'symbol2') + pair = self.safe_value(data, 'pair') + if pair is not None: + baseId = self.safe_string(pair, 'symbol1') + quoteId = self.safe_string(pair, 'symbol2') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + symbol = base + '/' + quote + market = self.safe_market(symbol) + remains = self.currency_from_precision(base, remains) + if self.orders is None: + limit = self.safe_integer(self.options, 'ordersLimit', 1000) + self.orders = ArrayCacheBySymbolById(limit) + storedOrders = self.orders + ordersBySymbol = self.safe_value(storedOrders.hashmap, symbol, {}) + order = self.safe_value(ordersBySymbol, orderId) + if order is None: + order = self.parse_ws_order_update(data, market) + order['remaining'] = remains + canceled = self.safe_bool(data, 'cancel', False) + if canceled: + order['status'] = 'canceled' + if isTransaction: + order['status'] = 'closed' + fee = self.safe_number(data, 'fee') + if fee is not None: + order['fee'] = { + 'cost': fee, + 'currency': quote, + 'rate': None, + } + timestamp = self.safe_integer(data, 'time') + order['timestamp'] = timestamp + order['datetime'] = self.iso8601(timestamp) + order = self.safe_order(order) + storedOrders.append(order) + messageHash = 'orders:' + symbol + client.resolve(storedOrders, messageHash) + + def parse_ws_order_update(self, order, market=None): + # + # { + # "id": "150714937", + # "remains": "1000000", + # "price": "17513", + # "amount": 2000000, As Precision + # "time": "1654506118448", + # "type": "buy", + # "pair": { + # "symbol1": "BTC", + # "symbol2": "USD" + # }, + # "fee": "0.15" + # } + # transaction + # { + # "d": "order:59425993014:a:BTC", + # "c": "user:up105393824:a:BTC", + # "a": "0.00098152", + # "ds": 0, + # "cs": "0.00098152", + # "user": "up105393824", + # "symbol": "BTC", + # "symbol2": "USD", + # "amount": "0.00098152", + # "buy": 59425993014, + # "order": 59425993014, + # "sell": 59425986168, + # "price": 19306.6, + # "type": "buy", + # "time": "2022-10-02T01:11:15.148Z", + # "balance": "0.00098152", + # "fee_amount": "0.05", + # "id": "59425993020" + # } + # + isTransaction = self.safe_value(order, 'd') is not None + remainsPrecision = self.safe_string(order, 'remains') + remaining = None + if remainsPrecision is not None: + remaining = self.currency_from_precision(market['base'], remainsPrecision) + amount = self.safe_string(order, 'amount') + if not isTransaction: + self.currency_from_precision(market['base'], amount) + baseId = self.safe_string(order, 'symbol') + quoteId = self.safe_string(order, 'symbol2') + pair = self.safe_value(order, 'pair') + if pair is not None: + baseId = self.safe_string(order, 'symbol1') + quoteId = self.safe_string(order, 'symbol2') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + symbol = None + if base is not None and quote is not None: + symbol = base + '/' + quote + market = self.safe_market(symbol, market) + time = self.safe_integer(order, 'time', self.milliseconds()) + timestamp = time + if isTransaction: + timestamp = self.parse8601(time) + canceled = self.safe_bool(order, 'cancel', False) + status = 'open' + if canceled: + status = 'canceled' + elif isTransaction: + status = 'closed' + parsedOrder: dict = { + 'id': self.safe_string_2(order, 'id', 'order'), + 'clientOrderId': None, + 'info': order, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'status': status, + 'symbol': symbol, + 'type': None, + 'timeInForce': None, + 'postOnly': None, + 'side': self.safe_string(order, 'type'), + 'price': self.safe_number(order, 'price'), + 'stopPrice': None, + 'triggerPrice': None, + 'average': None, + 'cost': None, + 'amount': amount, + 'filled': None, + 'remaining': remaining, + 'fee': { + 'cost': self.safe_number_2(order, 'fee', 'fee_amount'), + 'currency': quote, + 'rate': None, + }, + 'trades': None, + } + if isTransaction: + parsedOrder['trades'] = self.parse_ws_trade(order, market) + return self.safe_order(parsedOrder, market) + + def from_precision(self, amount, scale): + if amount is None: + return None + precise = Precise(amount) + precise.decimals = self.sum(precise.decimals, scale) + precise.reduce() + return str(precise) + + def currency_from_precision(self, currency, amount): + scale = self.safe_integer(self.currencies[currency], 'precision', 0) + return self.from_precision(amount, scale) + + def handle_orders_snapshot(self, client: Client, message): + # + # { + # "e": "open-orders", + # "data": [{ + # "id": "59098421630", + # "time": "1664062285425", + # "type": "buy", + # "price": "18920", + # "amount": "0.00100000", + # "pending": "0.00100000" + # }], + # "oid": 1, + # "ok": "ok" + # } + # + symbol = self.safe_string(message, 'oid') # symbol is set in watchOrders + rawOrders = self.safe_value(message, 'data', []) + myOrders = self.orders + if self.orders is None: + limit = self.safe_integer(self.options, 'ordersLimit', 1000) + myOrders = ArrayCacheBySymbolById(limit) + for i in range(0, len(rawOrders)): + rawOrder = rawOrders[i] + market = self.safe_market(symbol) + order = self.parse_order(rawOrder, market) + order['status'] = 'open' + myOrders.append(order) + self.orders = myOrders + messageHash = 'orders:' + symbol + ordersLength = len(myOrders) + if ordersLength > 0: + client.resolve(myOrders, messageHash) + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://cex.io/websocket-api#orderbook-subscribe + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + await self.authenticate() + market = self.market(symbol) + symbol = market['symbol'] + url = self.urls['api']['ws'] + messageHash = 'orderbook:' + symbol + depth = 0 if (limit is None) else limit + subscribe: dict = { + 'e': 'order-book-subscribe', + 'data': { + 'pair': [ + market['baseId'], + market['quoteId'], + ], + 'subscribe': True, + 'depth': depth, + }, + 'oid': self.request_id(), + } + request = self.deep_extend(subscribe, params) + orderbook = await self.watch(url, messageHash, request, messageHash) + return orderbook.limit() + + def handle_order_book_snapshot(self, client: Client, message): + # + # { + # "e": "order-book-subscribe", + # "data": { + # "timestamp": 1663762032, + # "timestamp_ms": 1663762031680, + # "bids": [ + # [241.947, 155.91626], + # [241, 154], + # ], + # "asks": [ + # [242.947, 155.91626], + # [243, 154], ], + # "pair": "BTC:USDT", + # "id": 616267120, + # "sell_total": "13.59066946", + # "buy_total": "163553.625948" + # }, + # "oid": "1", + # "ok": "ok" + # } + # + data = self.safe_value(message, 'data', {}) + pair = self.safe_string(data, 'pair') + symbol = self.pair_to_symbol(pair) + messageHash = 'orderbook:' + symbol + timestamp = self.safe_integer_2(data, 'timestamp_ms', 'timestamp') + incrementalId = self.safe_integer(data, 'id') + orderbook = self.order_book({}) + snapshot = self.parse_order_book(data, symbol, timestamp, 'bids', 'asks') + snapshot['nonce'] = incrementalId + orderbook.reset(snapshot) + self.options['orderbook'][symbol] = { + 'incrementalId': incrementalId, + } + self.orderbooks[symbol] = orderbook + client.resolve(orderbook, messageHash) + + def pair_to_symbol(self, pair): + parts = pair.split(':') + baseId = self.safe_string(parts, 0) + quoteId = self.safe_string(parts, 1) + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + symbol = base + '/' + quote + return symbol + + def handle_order_book_update(self, client: Client, message): + # + # { + # "e": "md_update", + # "data": { + # "id": 616267121, + # "pair": "BTC:USDT", + # "time": 1663762031719, + # "bids": [], + # "asks": [ + # [122, 23] + # ] + # } + # } + # + data = self.safe_value(message, 'data', {}) + incrementalId = self.safe_integer(data, 'id') + pair = self.safe_string(data, 'pair', '') + symbol = self.pair_to_symbol(pair) + storedOrderBook = self.safe_value(self.orderbooks, symbol) + messageHash = 'orderbook:' + symbol + if incrementalId != storedOrderBook['nonce'] + 1: + del client.subscriptions[messageHash] + client.reject(self.id + ' watchOrderBook() skipped a message', messageHash) + return + timestamp = self.safe_integer(data, 'time') + asks = self.safe_value(data, 'asks', []) + bids = self.safe_value(data, 'bids', []) + self.handle_deltas(storedOrderBook['asks'], asks) + self.handle_deltas(storedOrderBook['bids'], bids) + storedOrderBook['timestamp'] = timestamp + storedOrderBook['datetime'] = self.iso8601(timestamp) + storedOrderBook['nonce'] = incrementalId + client.resolve(storedOrderBook, messageHash) + + def handle_delta(self, bookside, delta): + bidAsk = self.parse_bid_ask(delta, 0, 1) + bookside.storeArray(bidAsk) + + def handle_deltas(self, bookside, deltas): + for i in range(0, len(deltas)): + self.handle_delta(bookside, deltas[i]) + + async def watch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + + https://cex.io/websocket-api#minute-data + + watches historical candlestick data containing the open, high, low, and close price, and the volume of a market. It will return the last 120 minutes with the selected timeframe and then 1m candle updates after that. + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents. + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + messageHash = 'ohlcv:' + symbol + url = self.urls['api']['ws'] + request: dict = { + 'e': 'init-ohlcv', + 'i': timeframe, + 'rooms': [ + 'pair-' + market['baseId'] + '-' + market['quoteId'], + ], + } + ohlcv = await self.watch(url, messageHash, self.extend(request, params), messageHash) + if self.newUpdates: + limit = ohlcv.getLimit(symbol, limit) + return self.filter_by_since_limit(ohlcv, since, limit, 0, True) + + def handle_init_ohlcv(self, client: Client, message): + # + # { + # "e": "init-ohlcv-data", + # "data": [ + # [ + # 1663660680, + # "19396.4", + # "19396.4", + # "19396.4", + # "19396.4", + # "1262861" + # ], + # ... + # ], + # "pair": "BTC:USDT" + # } + # + pair = self.safe_string(message, 'pair') + parts = pair.split(':') + baseId = self.safe_string(parts, 0) + quoteId = self.safe_string(parts, 1) + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + symbol = base + '/' + quote + market = self.safe_market(symbol) + messageHash = 'ohlcv:' + symbol + data = self.safe_value(message, 'data', []) + limit = self.safe_integer(self.options, 'OHLCVLimit', 1000) + stored = ArrayCacheByTimestamp(limit) + sorted = self.sort_by(data, 0) + for i in range(0, len(sorted)): + stored.append(self.parse_ohlcv(sorted[i], market)) + if not (symbol in self.ohlcvs): + self.ohlcvs[symbol] = {} + self.ohlcvs[symbol]['unknown'] = stored + client.resolve(stored, messageHash) + + def handle_ohlcv24(self, client: Client, message): + # + # { + # "e": "ohlcv24", + # "data": ['18793.2', '19630', '18793.2', "19104.1", "314157273"], + # "pair": "BTC:USDT" + # } + # + return message + + def handle_ohlcv1m(self, client: Client, message): + # + # { + # "e": "ohlcv1m", + # "data": { + # "pair": "BTC:USD", + # "time": "1665436800", + # "o": "19279.6", + # "h": "19279.6", + # "l": "19266.7", + # "c": "19266.7", + # "v": 3343884, + # "d": 3343884 + # } + # } + # + data = self.safe_value(message, 'data', {}) + pair = self.safe_string(data, 'pair') + symbol = self.pair_to_symbol(pair) + messageHash = 'ohlcv:' + symbol + ohlcv = [ + self.safe_timestamp(data, 'time'), + self.safe_number(data, 'o'), + self.safe_number(data, 'h'), + self.safe_number(data, 'l'), + self.safe_number(data, 'c'), + self.safe_number(data, 'v'), + ] + stored = self.safe_value(self.ohlcvs, symbol) + stored.append(ohlcv) + client.resolve(stored, messageHash) + + def handle_ohlcv(self, client: Client, message): + # + # { + # "e": "ohlcv", + # "data": [ + # [1665461100, '19068.2', '19068.2', '19068.2', "19068.2", 268478] + # ], + # "pair": "BTC:USD" + # } + # + data = self.safe_value(message, 'data', []) + pair = self.safe_string(message, 'pair') + symbol = self.pair_to_symbol(pair) + messageHash = 'ohlcv:' + symbol + # stored = self.safe_value(self.ohlcvs, symbol) + stored = self.ohlcvs[symbol]['unknown'] + for i in range(0, len(data)): + ohlcv = [ + self.safe_timestamp(data[i], 0), + self.safe_number(data[i], 1), + self.safe_number(data[i], 2), + self.safe_number(data[i], 3), + self.safe_number(data[i], 4), + self.safe_number(data[i], 5), + ] + stored.append(ohlcv) + dataLength = len(data) + if dataLength > 0: + client.resolve(stored, messageHash) + + async def fetch_order_ws(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://docs.cex.io/#ws-api-get-order + + :param str id: the order id + :param str symbol: not used by cex fetchOrder + :param dict [params]: extra parameters specific to the cex api endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + await self.authenticate() + market = None + if symbol is not None: + market = self.market(symbol) + data = self.extend({ + 'order_id': str(id), + }, params) + url = self.urls['api']['ws'] + messageHash = self.request_id() + request: dict = { + 'e': 'get-order', + 'oid': messageHash, + 'data': data, + } + response = await self.watch(url, messageHash, request, messageHash) + return self.parse_order(response, market) + + async def fetch_open_orders_ws(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + + https://docs.cex.io/#ws-api-open-orders + + fetch all unfilled currently open orders + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the cex api endpoint + :returns Order[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOpenOrdersWs requires a symbol.') + await self.load_markets() + await self.authenticate() + market = self.market(symbol) + url = self.urls['api']['ws'] + messageHash = self.request_id() + data = self.extend({ + 'pair': [market['baseId'], market['quoteId']], + }, params) + request: dict = { + 'e': 'open-orders', + 'oid': messageHash, + 'data': data, + } + response = await self.watch(url, messageHash, request, messageHash) + return self.parse_orders(response, market, since, limit, params) + + async def create_order_ws(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}) -> Order: + """ + + https://docs.cex.io/#ws-api-order-placement + + create a trade order + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float price: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the kraken api endpoint + :param boolean [params.maker_only]: Optional, maker only places an order only if offers best sell(<= max) or buy(>= max) price for self pair, if not order placement will be rejected with an error - "Order is not maker" + :returns dict: an `order structure ` + """ + if price is None: + raise BadRequest(self.id + ' createOrderWs requires a price argument') + await self.load_markets() + await self.authenticate() + market = self.market(symbol) + url = self.urls['api']['ws'] + messageHash = self.request_id() + data = self.extend({ + 'pair': [market['baseId'], market['quoteId']], + 'amount': amount, + 'price': price, + 'type': side, + }, params) + request: dict = { + 'e': 'place-order', + 'oid': messageHash, + 'data': data, + } + rawOrder = await self.watch(url, messageHash, request, messageHash) + return self.parse_order(rawOrder, market) + + async def edit_order_ws(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}) -> Order: + """ + edit a trade order + + https://docs.cex.io/#ws-api-cancel-replace + + :param str id: order id + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of the currency you want to trade in units of the base currency + :param float|None [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the cex api endpoint + :returns dict: an `order structure ` + """ + if amount is None: + raise ArgumentsRequired(self.id + ' editOrder() requires a amount argument') + if price is None: + raise ArgumentsRequired(self.id + ' editOrder() requires a price argument') + await self.load_markets() + await self.authenticate() + market = self.market(symbol) + data = self.extend({ + 'pair': [market['baseId'], market['quoteId']], + 'type': side, + 'amount': amount, + 'price': price, + 'order_id': id, + }, params) + messageHash = self.request_id() + url = self.urls['api']['ws'] + request: dict = { + 'e': 'cancel-replace-order', + 'oid': messageHash, + 'data': data, + } + response = await self.watch(url, messageHash, request, messageHash, messageHash) + return self.parse_order(response, market) + + async def cancel_order_ws(self, id: str, symbol: Str = None, params={}): + """ + + https://docs.cex.io/#ws-api-order-cancel + + cancels an open order + :param str id: order id + :param str symbol: not used by cex cancelOrder() + :param dict [params]: extra parameters specific to the cex api endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + await self.authenticate() + market = None + if symbol is not None: + market = self.market(symbol) + data = self.extend({ + 'order_id': id, + }, params) + messageHash = self.request_id() + url = self.urls['api']['ws'] + request: dict = { + 'e': 'cancel-order', + 'oid': messageHash, + 'data': data, + } + response = await self.watch(url, messageHash, request, messageHash, messageHash) + return self.parse_order(response, market) + + async def cancel_orders_ws(self, ids: List[str], symbol: Str = None, params={}): + """ + cancel multiple orders + + https://docs.cex.io/#ws-api-mass-cancel-place + + :param str[] ids: order ids + :param str symbol: not used by cex cancelOrders() + :param dict [params]: extra parameters specific to the cex api endpoint + :returns dict: a list of `order structures ` + """ + if symbol is not None: + raise BadRequest(self.id + ' cancelOrderWs does not allow filtering by symbol') + await self.load_markets() + await self.authenticate() + messageHash = self.request_id() + data = self.extend({ + 'cancel-orders': ids, + }, params) + url = self.urls['api']['ws'] + request: dict = { + 'e': 'mass-cancel-place-orders', + 'oid': messageHash, + 'data': data, + } + response = await self.watch(url, messageHash, request, messageHash, messageHash) + # + # { + # "cancel-orders": [{ + # "order_id": 69202557979, + # "fremains": "0.15000000" + # }], + # "place-orders": [], + # "placed-cancelled": [] + # } + # + canceledOrders = self.safe_value(response, 'cancel-orders') + return self.parse_orders(canceledOrders, None, None, None, params) + + def resolve_data(self, client: Client, message): + # + # "e": "open-orders", + # "data": [ + # { + # "id": "2477098", + # "time": "1435927928618", + # "type": "buy", + # "price": "241.9477", + # "amount": "0.02000000", + # "pending": "0.02000000" + # }, + # ... + # ], + # "oid": "1435927928274_9_open-orders", + # "ok": "ok" + # } + # + data = self.safe_value(message, 'data') + messageHash = self.safe_string(message, 'oid') + client.resolve(data, messageHash) + + def handle_connected(self, client: Client, message): + # + # { + # "e": "connected" + # } + # + return message + + def handle_error_message(self, client: Client, message) -> Bool: + # + # { + # "e": "get-balance", + # "data": {error: "Please Login"}, + # "oid": 1, + # "ok": "error" + # } + # + try: + data = self.safe_value(message, 'data', {}) + error = self.safe_string(data, 'error') + event = self.safe_string(message, 'e', '') + feedback = self.id + ' ' + event + ' ' + error + self.throw_exactly_matched_exception(self.exceptions['exact'], error, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], error, feedback) + raise ExchangeError(feedback) + except Exception as error: + messageHash = self.safe_string(message, 'oid') + future = self.safe_value(client['futures'], messageHash) + if future is not None: + client.reject(error, messageHash) + return True + else: + raise error + + def handle_message(self, client: Client, message): + ok = self.safe_string(message, 'ok') + if ok == 'error': + self.handle_error_message(client, message) + return + event = self.safe_string(message, 'e') + handlers: dict = { + 'auth': self.handle_authentication_message, + 'connected': self.handle_connected, + 'tick': self.handle_ticker, + 'ticker': self.handle_ticker, + 'init-ohlcv-data': self.handle_init_ohlcv, + 'ohlcv24': self.handle_ohlcv24, + 'ohlcv1m': self.handle_ohlcv1m, + 'ohlcv': self.handle_ohlcv, + 'get-balance': self.handle_balance, + 'order-book-subscribe': self.handle_order_book_snapshot, + 'md_update': self.handle_order_book_update, + 'open-orders': self.resolve_data, + 'order': self.handle_order_update, + 'history-update': self.handle_trade, + 'history': self.handle_trades_snapshot, + 'tx': self.handle_transaction, + 'place-order': self.resolve_data, + 'cancel-replace-order': self.resolve_data, + 'cancel-order': self.resolve_data, + 'mass-cancel-place-orders': self.resolve_data, + 'get-order': self.resolve_data, + } + handler = self.safe_value(handlers, event) + if handler is not None: + handler(client, message) + + def handle_authentication_message(self, client: Client, message): + # + # { + # "e": "auth", + # "data": { + # "ok": "ok" + # }, + # "ok": "ok", + # "timestamp":1448034593 + # } + # + future = self.safe_value(client.futures, 'authenticated') + if future is not None: + future.resolve(True) + + async def authenticate(self, params={}): + url = self.urls['api']['ws'] + client = self.client(url) + messageHash = 'authenticated' + future = client.reusableFuture('authenticated') + authenticated = self.safe_value(client.subscriptions, messageHash) + if authenticated is None: + self.check_required_credentials() + nonce = str(self.seconds()) + auth = nonce + self.apiKey + signature = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256) + request: dict = { + 'e': 'auth', + 'auth': { + 'key': self.apiKey, + 'signature': signature.upper(), + 'timestamp': nonce, + }, + } + self.watch(url, messageHash, self.extend(request, params), messageHash) + return await future diff --git a/ccxt/pro/coinbase.py b/ccxt/pro/coinbase.py new file mode 100644 index 0000000..72232d3 --- /dev/null +++ b/ccxt/pro/coinbase.py @@ -0,0 +1,926 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCacheBySymbolById +import hashlib +from ccxt.base.types import Any, Int, Order, OrderBook, Str, Strings, Ticker, Tickers, Trade +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import ArgumentsRequired + + +class coinbase(ccxt.async_support.coinbase): + + def describe(self) -> Any: + return self.deep_extend(super(coinbase, self).describe(), { + 'has': { + 'ws': True, + 'cancelAllOrdersWs': False, + 'cancelOrdersWs': False, + 'cancelOrderWs': False, + 'createOrderWs': False, + 'editOrderWs': False, + 'fetchBalanceWs': False, + 'fetchOpenOrdersWs': False, + 'fetchOrderWs': False, + 'fetchTradesWs': False, + 'watchBalance': False, + 'watchMyTrades': False, + 'watchOHLCV': False, + 'watchOrderBook': True, + 'watchOrderBookForSymbols': True, + 'watchOrders': True, + 'watchTicker': True, + 'watchTickers': True, + 'watchTrades': True, + 'watchTradesForSymbols': True, + 'unWatchTicker': True, + 'unWatchTickers': True, + 'unWatchTrades': True, + 'unWatchOrders': True, + 'unWatchTradesForSymbols': True, + }, + 'urls': { + 'api': { + 'ws': 'wss://advanced-trade-ws.coinbase.com', + }, + }, + 'options': { + 'tradesLimit': 1000, + 'ordersLimit': 1000, + 'myTradesLimit': 1000, + 'sides': { + 'bid': 'bids', + 'offer': 'asks', + }, + }, + }) + + async def subscribe(self, name: str, isPrivate: bool, symbol=None, params={}): + """ + @ignore + subscribes to a websocket channel + + https://docs.cloud.coinbase.com/advanced-trade-api/docs/ws-overview#subscribe + + :param str name: the name of the channel + :param boolean isPrivate: whether the channel is private or not + :param str [symbol]: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: subscription to a websocket channel + """ + await self.load_markets() + market = None + messageHash = name + productIds = [] + if isinstance(symbol, list): + symbols = self.market_symbols(symbol) + marketIds = self.market_ids(symbols) + productIds = marketIds + messageHash = messageHash + '::' + ','.join(symbol) + elif symbol is not None: + market = self.market(symbol) + messageHash = name + '::' + symbol + productIds = [market['id']] + url = self.urls['api']['ws'] + subscribe = { + 'type': 'subscribe', + 'product_ids': productIds, + 'channel': name, + # 'api_key': self.apiKey, + # 'timestamp': timestamp, + # 'signature': self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256), + } + if isPrivate: + subscribe = self.extend(subscribe, self.create_ws_auth(name, productIds)) + return await self.watch(url, messageHash, subscribe, messageHash) + + async def un_subscribe(self, topic: str, name: str, isPrivate: bool, symbol=None): + """ + @ignore + unSubscribes to a websocket channel + + https://docs.cloud.coinbase.com/advanced-trade-api/docs/ws-overview#subscribe + + :param str topic: unified topic + :param str name: the name of the channel + :param boolean isPrivate: whether the channel is private or not + :param str [symbol]: unified market symbol + :returns dict: subscription to a websocket channel + """ + await self.load_markets() + if self.safe_bool(self.options, 'unSubscriptionPending', False): + raise ExchangeError(self.id + ' another unSubscription is pending, coinbase does not support concurrent unSubscriptions') + self.options['unSubscriptionPending'] = True + market = None + watchMessageHash = name + unWatchMessageHash = 'unsubscribe:' + name + productIds = [] + if isinstance(symbol, list): + symbols = self.market_symbols(symbol) + marketIds = self.market_ids(symbols) + productIds = marketIds + watchMessageHash = watchMessageHash + '::' + ','.join(symbol) + unWatchMessageHash = unWatchMessageHash + '::' + ','.join(symbol) + elif symbol is not None: + market = self.market(symbol) + watchMessageHash = name + '::' + symbol + unWatchMessageHash = unWatchMessageHash + '::' + symbol + productIds = [market['id']] + url = self.urls['api']['ws'] + # '{"type": "unsubscribe", "product_ids": ["BTC-USD", "ETH-USD"], "channel": "ticker"}' + message = { + 'type': 'unsubscribe', + 'product_ids': productIds, + 'channel': name, + } + subscription = { + 'messageHashes': [unWatchMessageHash], + 'subMessageHashes': [watchMessageHash], + 'topic': topic, + 'unsubscribe': True, + 'symbols': [symbol], + } + if isPrivate: + message = self.extend(message, self.create_ws_auth(name, productIds)) + self.options['unSubscription'] = subscription + res = await self.watch(url, unWatchMessageHash, message, unWatchMessageHash, subscription) + self.options['unSubscriptionPending'] = False + self.options['unSubscription'] = None + return res + + async def subscribe_multiple(self, name: str, isPrivate: bool, symbols: Strings = None, params={}): + """ + @ignore + subscribes to a websocket channel + + https://docs.cloud.coinbase.com/advanced-trade-api/docs/ws-overview#subscribe + + :param str name: the name of the channel + :param boolean isPrivate: whether the channel is private or not + :param str[] [symbols]: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: subscription to a websocket channel + """ + await self.load_markets() + productIds = [] + messageHashes = [] + symbols = self.market_symbols(symbols, None, False) + for i in range(0, len(symbols)): + symbol = symbols[i] + market = self.market(symbol) + marketId = market['id'] + productIds.append(marketId) + messageHashes.append(name + '::' + symbol) + url = self.urls['api']['ws'] + subscribe = { + 'type': 'subscribe', + 'product_ids': productIds, + 'channel': name, + } + if isPrivate: + subscribe = self.extend(subscribe, self.create_ws_auth(name, productIds)) + return await self.watch_multiple(url, messageHashes, subscribe, messageHashes) + + async def un_subscribe_multiple(self, topic: str, name: str, isPrivate: bool, symbols: Strings = None, params={}): + """ + @ignore + unsubscribes to a websocket channel + + https://docs.cloud.coinbase.com/advanced-trade-api/docs/ws-overview#subscribe + + :param str topic: unified topic + :param str name: the name of the channel + :param boolean isPrivate: whether the channel is private or not + :param str[] [symbols]: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: subscription to a websocket channel + """ + if self.safe_bool(self.options, 'unSubscriptionPending', False): + raise ExchangeError(self.id + ' another unSubscription is pending, coinbase does not support concurrent unSubscriptions') + self.options['unSubscriptionPending'] = True + await self.load_markets() + productIds = [] + watchMessageHashes = [] + unWatchMessageHashes = [] + symbols = self.market_symbols(symbols, None, False) + for i in range(0, len(symbols)): + symbol = symbols[i] + market = self.market(symbol) + marketId = market['id'] + productIds.append(marketId) + watchMessageHashes.append(name + '::' + symbol) + unWatchMessageHashes.append('unsubscribe:' + name + '::' + symbol) + url = self.urls['api']['ws'] + message = { + 'type': 'unsubscribe', + 'product_ids': productIds, + 'channel': name, + } + if isPrivate: + message = self.extend(message, self.create_ws_auth(name, productIds)) + subscription = { + 'messageHashes': unWatchMessageHashes, + 'subMessageHashes': watchMessageHashes, + 'topic': topic, + 'unsubscribe': True, + 'symbols': symbols, + } + self.options['unSubscription'] = subscription + res = await self.watch_multiple(url, unWatchMessageHashes, message, unWatchMessageHashes, subscription) + self.options['unSubscriptionPending'] = False + self.options['unSubscription'] = None + return res + + def create_ws_auth(self, name: str, productIds: List[str]): + subscribe: dict = {} + timestamp = self.number_to_string(self.seconds()) + self.check_required_credentials() + isCloudAPiKey = (self.apiKey.find('organizations/') >= 0) or (self.secret.startswith('-----BEGIN')) + auth = timestamp + name + ','.join(productIds) + if not isCloudAPiKey: + subscribe['api_key'] = self.apiKey + subscribe['timestamp'] = timestamp + subscribe['signature'] = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256) + else: + if self.apiKey.startswith('-----BEGIN'): + raise ArgumentsRequired(self.id + ' apiKey should contain the name(eg: organizations/3b910e93....) and not the public key') + currentToken = self.safe_string(self.options, 'wsToken') + tokenTimestamp = self.safe_integer(self.options, 'wsTokenTimestamp', 0) + seconds = self.seconds() + if currentToken is None or tokenTimestamp + 120 < seconds: + # we should generate new token + token = self.create_auth_token(seconds) + self.options['wsToken'] = token + self.options['wsTokenTimestamp'] = seconds + subscribe['jwt'] = self.safe_string(self.options, 'wsToken') + return subscribe + + async def watch_ticker(self, symbol: str, params={}) -> Ticker: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://docs.cloud.coinbase.com/advanced-trade-api/docs/ws-channels#ticker-channel + + :param str [symbol]: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + name = 'ticker' + return await self.subscribe(name, False, symbol, params) + + async def un_watch_ticker(self, symbol: str, params={}) -> Ticker: + """ + stops watching a price ticker + + https://docs.cloud.coinbase.com/advanced-trade-api/docs/ws-channels#ticker-channel + + :param str [symbol]: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + name = 'ticker' + return await self.un_subscribe('ticker', name, False, symbol) + + async def watch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://docs.cloud.coinbase.com/advanced-trade-api/docs/ws-channels#ticker-batch-channel + + :param str[] [symbols]: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + if symbols is None: + symbols = self.symbols + name = 'ticker_batch' + ticker = await self.subscribe_multiple(name, False, symbols, params) + if self.newUpdates: + tickers = {} + symbol = ticker['symbol'] + tickers[symbol] = ticker + return tickers + return self.tickers + + async def un_watch_tickers(self, symbols: Strings = None, params={}) -> Any: + """ + stop watching + + https://docs.cloud.coinbase.com/advanced-trade-api/docs/ws-channels#ticker-batch-channel + + :param str[] [symbols]: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + if symbols is None: + symbols = self.symbols + return await self.un_subscribe_multiple('ticker', 'ticker_batch', False, symbols) + + def handle_tickers(self, client, message): + # + # { + # "channel": "ticker", + # "client_id": "", + # "timestamp": "2023-02-09T20:30:37.167359596Z", + # "sequence_num": 0, + # "events": [ + # { + # "type": "snapshot", + # "tickers": [ + # { + # "type": "ticker", + # "product_id": "BTC-USD", + # "price": "21932.98", + # "volume_24_h": "16038.28770938", + # "low_24_h": "21835.29", + # "high_24_h": "23011.18", + # "low_52_w": "15460", + # "high_52_w": "48240", + # "price_percent_chg_24_h": "-4.15775596190603" + # new 2024-04-12 + # "best_bid":"21835.29", + # "best_bid_quantity": "0.02000000", + # "best_ask":"23011.18", + # "best_ask_quantity": "0.01500000" + # } + # ] + # } + # ] + # } + # + # { + # "channel": "ticker_batch", + # "client_id": "", + # "timestamp": "2023-03-01T12:15:18.382173051Z", + # "sequence_num": 0, + # "events": [ + # { + # "type": "snapshot", + # "tickers": [ + # { + # "type": "ticker", + # "product_id": "DOGE-USD", + # "price": "0.08212", + # "volume_24_h": "242556423.3", + # "low_24_h": "0.07989", + # "high_24_h": "0.08308", + # "low_52_w": "0.04908", + # "high_52_w": "0.1801", + # "price_percent_chg_24_h": "0.50177456859626" + # new 2024-04-12 + # "best_bid":"0.07989", + # "best_bid_quantity": "500.0", + # "best_ask":"0.08308", + # "best_ask_quantity": "300.0" + # } + # ] + # } + # ] + # } + # + # note! seems coinbase might also send empty data like: + # + # { + # "channel": "ticker_batch", + # "client_id": "", + # "timestamp": "2024-05-24T18:22:24.546809523Z", + # "sequence_num": 1, + # "events": [ + # { + # "type": "snapshot", + # "tickers": [ + # { + # "type": "ticker", + # "product_id": "", + # "price": "", + # "volume_24_h": "", + # "low_24_h": "", + # "high_24_h": "", + # "low_52_w": "", + # "high_52_w": "", + # "price_percent_chg_24_h": "" + # } + # ] + # } + # ] + # } + # + # + channel = self.safe_string(message, 'channel') + events = self.safe_list(message, 'events', []) + datetime = self.safe_string(message, 'timestamp') + timestamp = self.parse8601(datetime) + newTickers = [] + for i in range(0, len(events)): + tickersObj = events[i] + tickers = self.safe_list(tickersObj, 'tickers', []) + for j in range(0, len(tickers)): + ticker = tickers[j] + wsMarketId = self.safe_string(ticker, 'product_id') + if wsMarketId is None: + continue + result = self.parse_ws_ticker(ticker) + result['timestamp'] = timestamp + result['datetime'] = datetime + symbol = result['symbol'] + self.tickers[symbol] = result + newTickers.append(result) + messageHash = channel + '::' + symbol + client.resolve(result, messageHash) + self.try_resolve_usdc(client, messageHash, result) + + def parse_ws_ticker(self, ticker, market=None): + # + # { + # "type": "ticker", + # "product_id": "DOGE-USD", + # "price": "0.08212", + # "volume_24_h": "242556423.3", + # "low_24_h": "0.07989", + # "high_24_h": "0.08308", + # "low_52_w": "0.04908", + # "high_52_w": "0.1801", + # "price_percent_chg_24_h": "0.50177456859626" + # new 2024-04-12 + # "best_bid":"0.07989", + # "best_bid_quantity": "500.0", + # "best_ask":"0.08308", + # "best_ask_quantity": "300.0" + # } + # + marketId = self.safe_string(ticker, 'product_id') + timestamp = None + last = self.safe_number(ticker, 'price') + return self.safe_ticker({ + 'info': ticker, + 'symbol': self.safe_symbol(marketId, market, '-'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high_24_h'), + 'low': self.safe_string(ticker, 'low_24_h'), + 'bid': self.safe_string(ticker, 'best_bid'), + 'bidVolume': self.safe_string(ticker, 'best_bid_quantity'), + 'ask': self.safe_string(ticker, 'best_ask'), + 'askVolume': self.safe_string(ticker, 'best_ask_quantity'), + 'vwap': None, + 'open': None, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': self.safe_string(ticker, 'price_percent_chg_24_h'), + 'average': None, + 'baseVolume': self.safe_string(ticker, 'volume_24_h'), + 'quoteVolume': None, + }) + + async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://docs.cloud.coinbase.com/advanced-trade-api/docs/ws-channels#market-trades-channel + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + symbol = self.symbol(symbol) + name = 'market_trades' + trades = await self.subscribe(name, False, symbol, params) + if self.newUpdates: + limit = trades.getLimit(symbol, limit) + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + async def un_watch_trades(self, symbol: str, params={}) -> Any: + """ + stops watching the list of most recent trades for a particular symbol + + https://docs.cloud.coinbase.com/advanced-trade-api/docs/ws-channels#market-trades-channel + + :param str symbol: unified symbol of the market to fetch trades for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + name = 'market_trades' + return await self.un_subscribe('trades', name, False, symbol) + + async def watch_trades_for_symbols(self, symbols: List[str], since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://docs.cloud.coinbase.com/advanced-trade-api/docs/ws-channels#market-trades-channel + + :param str[] symbols: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + name = 'market_trades' + trades = await self.subscribe_multiple(name, False, symbols, params) + if self.newUpdates: + first = self.safe_dict(trades, 0) + tradeSymbol = self.safe_string(first, 'symbol') + limit = trades.getLimit(tradeSymbol, limit) + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + async def un_watch_trades_for_symbols(self, symbols: List[str], params={}) -> Any: + """ + get the list of most recent trades for a particular symbol + + https://docs.cloud.coinbase.com/advanced-trade-api/docs/ws-channels#market-trades-channel + + :param str[] symbols: unified symbol of the market to fetch trades for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + name = 'market_trades' + return await self.un_subscribe_multiple('trades', name, False, symbols, params) + + async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + watches information on multiple orders made by the user + + https://docs.cloud.coinbase.com/advanced-trade-api/docs/ws-channels#user-channel + + :param str [symbol]: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + name = 'user' + orders = await self.subscribe(name, True, symbol, params) + if self.newUpdates: + limit = orders.getLimit(symbol, limit) + return self.filter_by_since_limit(orders, since, limit, 'timestamp', True) + + async def un_watch_orders(self, symbol: Str = None, params={}) -> Any: + """ + stops watching information on multiple orders made by the user + + https://docs.cloud.coinbase.com/advanced-trade-api/docs/ws-channels#user-channel + + :param str [symbol]: unified market symbol of the market orders were made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + name = 'user' + return await self.un_subscribe('orders', name, True, self.symbol(symbol)) + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://docs.cloud.coinbase.com/advanced-trade-api/docs/ws-channels#level2-channel + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + name = 'level2' + market = self.market(symbol) + symbol = market['symbol'] + orderbook = await self.subscribe(name, False, symbol, params) + return orderbook.limit() + + async def un_watch_order_book(self, symbol: str, params={}) -> Any: + """ + stops watching information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://docs.cloud.coinbase.com/advanced-trade-api/docs/ws-channels#level2-channel + + :param str symbol: unified symbol of the market to fetch the order book for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + symbol = self.symbol(symbol) + name = 'level2' + return await self.un_subscribe('orderbook', name, False, symbol) + + async def watch_order_book_for_symbols(self, symbols: List[str], limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://docs.cloud.coinbase.com/advanced-trade-api/docs/ws-channels#level2-channel + + :param str[] symbols: unified array of symbols + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + name = 'level2' + orderbook = await self.subscribe_multiple(name, False, symbols, params) + return orderbook.limit() + + def handle_trade(self, client, message): + # + # { + # "channel": "market_trades", + # "client_id": "", + # "timestamp": "2023-02-09T20:19:35.39625135Z", + # "sequence_num": 0, + # "events": [ + # { + # "type": "snapshot", + # "trades": [ + # { + # "trade_id": "000000000", + # "product_id": "ETH-USD", + # "price": "1260.01", + # "size": "0.3", + # "side": "BUY", + # "time": "2019-08-14T20:42:27.265Z", + # } + # ] + # } + # ] + # } + # + events = self.safe_list(message, 'events') + event = self.safe_value(events, 0) + trades = self.safe_list(event, 'trades') + trade = self.safe_dict(trades, 0) + marketId = self.safe_string(trade, 'product_id') + symbol = self.safe_symbol(marketId) + messageHash = 'market_trades::' + symbol + tradesArray = self.safe_value(self.trades, symbol) + if tradesArray is None: + tradesLimit = self.safe_integer(self.options, 'tradesLimit', 1000) + tradesArray = ArrayCacheBySymbolById(tradesLimit) + self.trades[symbol] = tradesArray + for i in range(0, len(events)): + currentEvent = events[i] + currentTrades = self.safe_list(currentEvent, 'trades') + for j in range(0, len(currentTrades)): + item = currentTrades[i] + tradesArray.append(self.parse_trade(item)) + client.resolve(tradesArray, messageHash) + self.try_resolve_usdc(client, messageHash, tradesArray) + + def handle_order(self, client, message): + # + # { + # "channel": "user", + # "client_id": "", + # "timestamp": "2023-02-09T20:33:57.609931463Z", + # "sequence_num": 0, + # "events": [ + # { + # "type": "snapshot", + # "orders": [ + # { + # "order_id": "XXX", + # "client_order_id": "YYY", + # "cumulative_quantity": "0", + # "leaves_quantity": "0.000994", + # "avg_price": "0", + # "total_fees": "0", + # "status": "OPEN", + # "product_id": "BTC-USD", + # "creation_time": "2022-12-07T19:42:18.719312Z", + # "order_side": "BUY", + # "order_type": "Limit" + # }, + # ] + # } + # ] + # } + # + events = self.safe_list(message, 'events') + marketIds = [] + if self.orders is None: + limit = self.safe_integer(self.options, 'ordersLimit', 1000) + self.orders = ArrayCacheBySymbolById(limit) + for i in range(0, len(events)): + event = events[i] + responseOrders = self.safe_list(event, 'orders') + for j in range(0, len(responseOrders)): + responseOrder = responseOrders[j] + parsed = self.parse_ws_order(responseOrder) + cachedOrders = self.orders + marketId = self.safe_string(responseOrder, 'product_id') + if not (marketId in marketIds): + marketIds.append(marketId) + cachedOrders.append(parsed) + for i in range(0, len(marketIds)): + marketId = marketIds[i] + symbol = self.safe_symbol(marketId) + messageHash = 'user::' + symbol + client.resolve(self.orders, messageHash) + self.try_resolve_usdc(client, messageHash, self.orders) + client.resolve(self.orders, 'user') + + def parse_ws_order(self, order, market=None): + # + # { + # "order_id": "XXX", + # "client_order_id": "YYY", + # "cumulative_quantity": "0", + # "leaves_quantity": "0.000994", + # "avg_price": "0", + # "total_fees": "0", + # "status": "OPEN", + # "product_id": "BTC-USD", + # "creation_time": "2022-12-07T19:42:18.719312Z", + # "order_side": "BUY", + # "order_type": "Limit" + # } + # + id = self.safe_string(order, 'order_id') + clientOrderId = self.safe_string(order, 'client_order_id') + marketId = self.safe_string(order, 'product_id') + datetime = self.safe_string_2(order, 'time', 'creation_time') + market = self.safe_market(marketId, market) + stopPrice = self.safe_string(order, 'stop_price') + return self.safe_order({ + 'info': order, + 'symbol': self.safe_string(market, 'symbol'), + 'id': id, + 'clientOrderId': clientOrderId, + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + 'lastTradeTimestamp': None, + 'type': self.safe_string(order, 'order_type'), + 'timeInForce': None, + 'postOnly': None, + 'side': self.safe_string_2(order, 'side', 'order_side'), + 'price': self.safe_string(order, 'limit_price'), + 'stopPrice': stopPrice, + 'triggerPrice': stopPrice, + 'amount': self.safe_string(order, 'cumulative_quantity'), + 'cost': self.omit_zero(self.safe_string(order, 'filled_value')), + 'average': self.safe_string(order, 'avg_price'), + 'filled': self.safe_string(order, 'cumulative_quantity'), + 'remaining': self.safe_string(order, 'leaves_quantity'), + 'status': self.safe_string_lower(order, 'status'), + 'fee': { + 'amount': self.safe_string(order, 'total_fees'), + 'currency': self.safe_string(market, 'quote'), + }, + 'trades': None, + }) + + def handle_order_book_helper(self, orderbook, updates): + for i in range(0, len(updates)): + trade = updates[i] + sideId = self.safe_string(trade, 'side') + side = self.safe_string(self.options['sides'], sideId) + price = self.safe_number(trade, 'price_level') + amount = self.safe_number(trade, 'new_quantity') + orderbookSide = orderbook[side] + orderbookSide.store(price, amount) + + def handle_order_book(self, client, message): + # + # { + # "channel": "l2_data", + # "client_id": "", + # "timestamp": "2023-02-09T20:32:50.714964855Z", + # "sequence_num": 0, + # "events": [ + # { + # "type": "snapshot", + # "product_id": "BTC-USD", + # "updates": [ + # { + # "side": "bid", + # "event_time": "1970-01-01T00:00:00Z", + # "price_level": "21921.74", + # "new_quantity": "0.06317902" + # }, + # { + # "side": "bid", + # "event_time": "1970-01-01T00:00:00Z", + # "price_level": "21921.3", + # "new_quantity": "0.02" + # }, + # ] + # } + # ] + # } + # + events = self.safe_list(message, 'events') + datetime = self.safe_string(message, 'timestamp') + for i in range(0, len(events)): + event = events[i] + updates = self.safe_list(event, 'updates', []) + marketId = self.safe_string(event, 'product_id') + # sometimes we subscribe to BTC/USDC and coinbase returns BTC/USD, are aliases + market = self.safe_market(marketId) + symbol = market['symbol'] + messageHash = 'level2::' + symbol + subscription = self.safe_value(client.subscriptions, messageHash, {}) + limit = self.safe_integer(subscription, 'limit') + type = self.safe_string(event, 'type') + if type == 'snapshot': + self.orderbooks[symbol] = self.order_book({}, limit) + # unknown bug, can't reproduce, but sometimes orderbook is None + if not (symbol in self.orderbooks) and self.orderbooks[symbol] is None: + continue + orderbook = self.orderbooks[symbol] + self.handle_order_book_helper(orderbook, updates) + orderbook['timestamp'] = self.parse8601(datetime) + orderbook['datetime'] = datetime + orderbook['symbol'] = symbol + client.resolve(orderbook, messageHash) + self.try_resolve_usdc(client, messageHash, orderbook) + + def try_resolve_usdc(self, client, messageHash, result): + if messageHash.endswith('/USD') or messageHash.endswith('-USD'): + client.resolve(result, messageHash + 'C') # when subscribing to BTC/USDC and coinbase returns BTC/USD, so resolve USDC too + + def handle_subscription_status(self, client, message): + # + # { + # "type": "subscriptions", + # "channels": [ + # { + # "name": "level2", + # "product_ids": ["ETH-BTC"] + # } + # ] + # } + # + # + # { + # channel: 'subscriptions', + # client_id: '', + # timestamp: '2025-09-15T17:02:49.90120868Z', + # sequence_num: 3, + # events: [{subscriptions: {}}] + # } + # + events = self.safe_list(message, 'events', []) + firstEvent = self.safe_value(events, 0, {}) + isUnsub = ('subscriptions' in firstEvent) + subKeys = list(firstEvent['subscriptions'].keys()) + subKeysLength = len(subKeys) + if isUnsub and subKeysLength == 0: + unSubObject = self.safe_dict(self.options, 'unSubscription', {}) + messageHashes = self.safe_list(unSubObject, 'messageHashes', []) + subMessageHashes = self.safe_list(unSubObject, 'subMessageHashes', []) + for i in range(0, len(messageHashes)): + messageHash = messageHashes[i] + subHash = subMessageHashes[i] + self.clean_unsubscription(client, subHash, messageHash) + self.clean_cache(unSubObject) + return message + + def handle_heartbeats(self, client, message): + # although the subscription takes a product_ids parameter(i.e. symbol), + # there is no(clear) way of mapping the message back to the symbol. + # + # { + # "channel": "heartbeats", + # "client_id": "", + # "timestamp": "2023-06-23T20:31:26.122969572Z", + # "sequence_num": 0, + # "events": [ + # { + # "current_time": "2023-06-23 20:31:56.121961769 +0000 UTC m=+91717.525857105", + # "heartbeat_counter": "3049" + # } + # ] + # } + # + return message + + def handle_message(self, client, message): + channel = self.safe_string(message, 'channel') + methods: dict = { + 'subscriptions': self.handle_subscription_status, + 'ticker': self.handle_tickers, + 'ticker_batch': self.handle_tickers, + 'market_trades': self.handle_trade, + 'user': self.handle_order, + 'l2_data': self.handle_order_book, + 'heartbeats': self.handle_heartbeats, + } + type = self.safe_string(message, 'type') + if type == 'error': + errorMessage = self.safe_string(message, 'message') + raise ExchangeError(errorMessage) + method = self.safe_value(methods, channel) + if method: + method(client, message) diff --git a/ccxt/pro/coinbaseadvanced.py b/ccxt/pro/coinbaseadvanced.py new file mode 100644 index 0000000..8c53135 --- /dev/null +++ b/ccxt/pro/coinbaseadvanced.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.pro.coinbase import coinbase +from ccxt.base.types import Any + + +class coinbaseadvanced(coinbase): + + def describe(self) -> Any: + return self.deep_extend(super(coinbaseadvanced, self).describe(), { + 'id': 'coinbaseadvanced', + 'name': 'Coinbase Advanced', + 'alias': True, + }) diff --git a/ccxt/pro/coinbaseexchange.py b/ccxt/pro/coinbaseexchange.py new file mode 100644 index 0000000..2d1f772 --- /dev/null +++ b/ccxt/pro/coinbaseexchange.py @@ -0,0 +1,895 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolById +import hashlib +from ccxt.base.types import Any, Bool, Int, Order, OrderBook, Str, Strings, Ticker, Tickers, Trade +from ccxt.async_support.base.ws.client import Client +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol + + +class coinbaseexchange(ccxt.async_support.coinbaseexchange): + + def describe(self) -> Any: + return self.deep_extend(super(coinbaseexchange, self).describe(), { + 'has': { + 'ws': True, + 'watchOHLCV': False, # missing on the exchange side + 'watchOrderBook': True, + 'watchOrderBookForSymbols': True, + 'watchTicker': True, + 'watchTickers': True, + 'watchTrades': True, + 'watchTradesForSymbols': True, + 'watchMyTradesForSymbols': True, + 'watchBalance': False, + 'watchStatus': False, # for now + 'watchOrders': True, + 'watchOrdersForSymbols': True, + 'watchMyTrades': True, + }, + 'urls': { + 'api': { + 'ws': 'wss://ws-feed.exchange.coinbase.com', + }, + 'test': { + 'ws': 'wss://ws-feed-public.sandbox.exchange.coinbase.com', + }, + }, + 'options': { + 'tradesLimit': 1000, + 'ordersLimit': 1000, + 'myTradesLimit': 1000, + }, + }) + + def authenticate(self): + self.check_required_credentials() + path = '/users/self/verify' + nonce = self.nonce() + payload = str(nonce) + 'GET' + path + signature = self.hmac(self.encode(payload), self.base64_to_binary(self.secret), hashlib.sha256, 'base64') + return { + 'timestamp': nonce, + 'key': self.apiKey, + 'signature': signature, + 'passphrase': self.password, + } + + async def subscribe(self, name, symbol=None, messageHashStart=None, params={}): + await self.load_markets() + market = None + messageHash = messageHashStart + productIds = [] + if symbol is not None: + market = self.market(symbol) + messageHash += ':' + market['id'] + productIds.append(market['id']) + url = self.urls['api']['ws'] + if 'signature' in params: + # need to distinguish between public trades and user trades + url = url + '?' + subscribe: dict = { + 'type': 'subscribe', + 'product_ids': productIds, + 'channels': [ + name, + ], + } + request = self.extend(subscribe, params) + return await self.watch(url, messageHash, request, messageHash) + + async def subscribe_multiple(self, name, symbols=[], messageHashStart=None, params={}): + await self.load_markets() + market = None + symbols = self.market_symbols(symbols) + messageHashes = [] + productIds = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + market = self.market(symbol) + productIds.append(market['id']) + messageHashes.append(messageHashStart + ':' + market['symbol']) + url = self.urls['api']['ws'] + if 'signature' in params: + # need to distinguish between public trades and user trades + url = url + '?' + subscribe: dict = { + 'type': 'subscribe', + 'product_ids': productIds, + 'channels': [ + name, + ], + } + request = self.extend(subscribe, params) + return await self.watch_multiple(url, messageHashes, request, messageHashes) + + async def watch_ticker(self, symbol: str, params={}) -> Ticker: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + name = 'ticker' + return await self.subscribe(name, symbol, name, params) + + async def watch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list + :param str[] [symbols]: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.channel]: the channel to subscribe to, tickers by default. Can be tickers, sprd-tickers, index-tickers, block-tickers + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbolsLength = len(symbols) + if symbolsLength == 0: + raise BadSymbol(self.id + ' watchTickers requires a non-empty symbols array') + channel = 'ticker' + messageHash = 'ticker' + ticker = await self.subscribe_multiple(channel, symbols, messageHash, params) + if self.newUpdates: + result: dict = {} + result[ticker['symbol']] = ticker + return result + return self.filter_by_array(self.tickers, 'symbol', symbols) + + async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + symbol = self.symbol(symbol) + name = 'matches' + trades = await self.subscribe(name, symbol, name, params) + if self.newUpdates: + limit = trades.getLimit(symbol, limit) + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + async def watch_trades_for_symbols(self, symbols: List[str], since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + :param str[] symbols: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + symbolsLength = len(symbols) + if symbolsLength == 0: + raise BadRequest(self.id + ' watchTradesForSymbols() requires a non-empty array of symbols') + await self.load_markets() + symbols = self.market_symbols(symbols) + name = 'matches' + trades = await self.subscribe_multiple(name, symbols, name, params) + if self.newUpdates: + first = self.safe_value(trades, 0) + tradeSymbol = self.safe_string(first, 'symbol') + limit = trades.getLimit(tradeSymbol, limit) + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + async def watch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + watches information on multiple trades made by the user + :param str symbol: unified market symbol of the market trades were made in + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trade structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' watchMyTrades() requires a symbol argument') + await self.load_markets() + symbol = self.symbol(symbol) + name = 'user' + messageHash = 'myTrades' + authentication = self.authenticate() + trades = await self.subscribe(name, symbol, messageHash, self.extend(params, authentication)) + if self.newUpdates: + limit = trades.getLimit(symbol, limit) + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + async def watch_my_trades_for_symbols(self, symbols: List[str], since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + watches information on multiple trades made by the user + :param str[] symbols: unified symbol of the market to fetch trades for + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trade structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + symbols = self.market_symbols(symbols, None, False) + await self.load_markets() + name = 'user' + messageHash = 'myTrades' + authentication = self.authenticate() + trades = await self.subscribe_multiple(name, symbols, messageHash, self.extend(params, authentication)) + if self.newUpdates: + first = self.safe_value(trades, 0) + tradeSymbol = self.safe_string(first, 'symbol') + limit = trades.getLimit(tradeSymbol, limit) + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + async def watch_orders_for_symbols(self, symbols: List[str], since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + watches information on multiple orders made by the user + :param str[] symbols: unified symbol of the market to fetch orders for + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of trade structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, False) + name = 'user' + messageHash = 'orders' + authentication = self.authenticate() + orders = await self.subscribe_multiple(name, symbols, messageHash, self.extend(params, authentication)) + if self.newUpdates: + first = self.safe_value(orders, 0) + tradeSymbol = self.safe_string(first, 'symbol') + limit = orders.getLimit(tradeSymbol, limit) + return self.filter_by_since_limit(orders, since, limit, 'timestamp', True) + + async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + watches information on multiple orders made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + if symbol is None: + raise BadSymbol(self.id + ' watchMyTrades requires a symbol') + await self.load_markets() + symbol = self.symbol(symbol) + name = 'user' + messageHash = 'orders' + authentication = self.authenticate() + orders = await self.subscribe(name, symbol, messageHash, self.extend(params, authentication)) + if self.newUpdates: + limit = orders.getLimit(symbol, limit) + return self.filter_by_since_limit(orders, since, limit, 'timestamp', True) + + async def watch_order_book_for_symbols(self, symbols: List[str], limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str[] symbols: unified array of symbols + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + symbolsLength = len(symbols) + if symbolsLength == 0: + raise BadRequest(self.id + ' watchOrderBookForSymbols() requires a non-empty array of symbols') + name = 'level2' + await self.load_markets() + symbols = self.market_symbols(symbols) + marketIds = self.market_ids(symbols) + messageHashes = [] + for i in range(0, symbolsLength): + marketId = marketIds[i] + messageHashes.append(name + ':' + marketId) + url = self.urls['api']['ws'] + subscribe: dict = { + 'type': 'subscribe', + 'product_ids': marketIds, + 'channels': [ + name, + ], + } + request = self.extend(subscribe, params) + subscription: dict = { + 'messageHash': name, + 'symbols': symbols, + 'marketIds': marketIds, + 'limit': limit, + } + authentication = self.authenticate() + orderbook = await self.watch_multiple(url, messageHashes, self.extend(request, authentication), messageHashes, subscription) + return orderbook.limit() + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + name = 'level2' + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + messageHash = name + ':' + market['id'] + url = self.urls['api']['ws'] + subscribe: dict = { + 'type': 'subscribe', + 'product_ids': [ + market['id'], + ], + 'channels': [ + name, + ], + } + request = self.extend(subscribe, params) + subscription: dict = { + 'messageHash': messageHash, + 'symbol': symbol, + 'marketId': market['id'], + 'limit': limit, + } + authentication = self.authenticate() + orderbook = await self.watch(url, messageHash, self.extend(request, authentication), messageHash, subscription) + return orderbook.limit() + + def handle_trade(self, client: Client, message): + # + # { + # "type": "match", + # "trade_id": 82047307, + # "maker_order_id": "0f358725-2134-435e-be11-753912a326e0", + # "taker_order_id": "252b7002-87a3-425c-ac73-f5b9e23f3caf", + # "side": "sell", + # "size": "0.00513192", + # "price": "9314.78", + # "product_id": "BTC-USD", + # "sequence": 12038915443, + # "time": "2020-01-31T20:03:41.158814Z" + # } + # + marketId = self.safe_string(message, 'product_id') + if marketId is not None: + trade = self.parse_ws_trade(message) + symbol = trade['symbol'] + # the exchange sends type = 'match' + # but requires 'matches' upon subscribing + # therefore we resolve 'matches' here instead of 'match' + type = 'matches' + messageHash = type + ':' + marketId + tradesArray = self.safe_value(self.trades, symbol) + if tradesArray is None: + tradesLimit = self.safe_integer(self.options, 'tradesLimit', 1000) + tradesArray = ArrayCache(tradesLimit) + self.trades[symbol] = tradesArray + tradesArray.append(trade) + client.resolve(tradesArray, messageHash) + return message + + def handle_my_trade(self, client: Client, message): + marketId = self.safe_string(message, 'product_id') + if marketId is not None: + trade = self.parse_ws_trade(message) + type = 'myTrades' + messageHash = type + ':' + marketId + tradesArray = self.myTrades + if tradesArray is None: + limit = self.safe_integer(self.options, 'myTradesLimit', 1000) + tradesArray = ArrayCacheBySymbolById(limit) + self.myTrades = tradesArray + tradesArray.append(trade) + client.resolve(tradesArray, messageHash) + return message + + def parse_ws_trade(self, trade, market=None): + # + # private trades + # { + # "type": "match", + # "trade_id": 10, + # "sequence": 50, + # "maker_order_id": "ac928c66-ca53-498f-9c13-a110027a60e8", + # "taker_order_id": "132fb6ae-456b-4654-b4e0-d681ac05cea1", + # "time": "2014-11-07T08:19:27.028459Z", + # "product_id": "BTC-USD", + # "size": "5.23512", + # "price": "400.23", + # "side": "sell", + # "taker_user_id: "5844eceecf7e803e259d0365", + # "user_id": "5844eceecf7e803e259d0365", + # "taker_profile_id": "765d1549-9660-4be2-97d4-fa2d65fa3352", + # "profile_id": "765d1549-9660-4be2-97d4-fa2d65fa3352", + # "taker_fee_rate": "0.005" + # } + # + # { + # "type": "match", + # "trade_id": 10, + # "sequence": 50, + # "maker_order_id": "ac928c66-ca53-498f-9c13-a110027a60e8", + # "taker_order_id": "132fb6ae-456b-4654-b4e0-d681ac05cea1", + # "time": "2014-11-07T08:19:27.028459Z", + # "product_id": "BTC-USD", + # "size": "5.23512", + # "price": "400.23", + # "side": "sell", + # "maker_user_id: "5844eceecf7e803e259d0365", + # "maker_id": "5844eceecf7e803e259d0365", + # "maker_profile_id": "765d1549-9660-4be2-97d4-fa2d65fa3352", + # "profile_id": "765d1549-9660-4be2-97d4-fa2d65fa3352", + # "maker_fee_rate": "0.001" + # } + # + # public trades + # { + # "type": "received", + # "time": "2014-11-07T08:19:27.028459Z", + # "product_id": "BTC-USD", + # "sequence": 10, + # "order_id": "d50ec984-77a8-460a-b958-66f114b0de9b", + # "size": "1.34", + # "price": "502.1", + # "side": "buy", + # "order_type": "limit" + # } + parsed = super(coinbaseexchange, self).parse_trade(trade) + feeRate = None + isMaker = False + if 'maker_fee_rate' in trade: + isMaker = True + parsed['takerOrMaker'] = 'maker' + feeRate = self.safe_number(trade, 'maker_fee_rate') + else: + parsed['takerOrMaker'] = 'taker' + feeRate = self.safe_number(trade, 'taker_fee_rate') + # side always represents the maker side of the trade + # so if we're taker, we invert it + currentSide = parsed['side'] + parsed['side'] = self.safe_string({ + 'buy': 'sell', + 'sell': 'buy', + }, currentSide, currentSide) + idKey = 'maker_order_id' if isMaker else 'taker_order_id' + parsed['order'] = self.safe_string(trade, idKey) + market = self.market(parsed['symbol']) + feeCurrency = market['quote'] + feeCost = None + if (parsed['cost'] is not None) and (feeRate is not None): + cost = self.safe_number(parsed, 'cost') + feeCost = cost * feeRate + parsed['fee'] = { + 'rate': feeRate, + 'cost': feeCost, + 'currency': feeCurrency, + } + return parsed + + def parse_ws_order_status(self, status): + statuses: dict = { + 'filled': 'closed', + 'canceled': 'canceled', + } + return self.safe_string(statuses, status, 'open') + + def handle_order(self, client: Client, message): + # + # Order is created + # + # { + # "type": "received", + # "side": "sell", + # "product_id": "BTC-USDC", + # "time": "2021-03-05T16:42:21.878177Z", + # "sequence": 5641953814, + # "profile_id": "774ee0ce-fdda-405f-aa8d-47189a14ba0a", + # "user_id": "54fc141576dcf32596000133", + # "order_id": "11838707-bf9c-4d65-8cec-b57c9a7cab42", + # "order_type": "limit", + # "size": "0.0001", + # "price": "50000", + # "client_oid": "a317abb9-2b30-4370-ebfe-0deecb300180" + # } + # + # { + # "type": "received", + # "time": "2014-11-09T08:19:27.028459Z", + # "product_id": "BTC-USD", + # "sequence": 12, + # "order_id": "dddec984-77a8-460a-b958-66f114b0de9b", + # "funds": "3000.234", + # "side": "buy", + # "order_type": "market" + # } + # + # Order is on the order book + # + # { + # "type": "open", + # "side": "sell", + # "product_id": "BTC-USDC", + # "time": "2021-03-05T16:42:21.878177Z", + # "sequence": 5641953815, + # "profile_id": "774ee0ce-fdda-405f-aa8d-47189a14ba0a", + # "user_id": "54fc141576dcf32596000133", + # "price": "50000", + # "order_id": "11838707-bf9c-4d65-8cec-b57c9a7cab42", + # "remaining_size": "0.0001" + # } + # + # Order is partially or completely filled + # + # { + # "type": "match", + # "side": "sell", + # "product_id": "BTC-USDC", + # "time": "2021-03-05T16:37:13.396107Z", + # "sequence": 5641897876, + # "profile_id": "774ee0ce-fdda-405f-aa8d-47189a14ba0a", + # "user_id": "54fc141576dcf32596000133", + # "trade_id": 5455505, + # "maker_order_id": "e5f5754d-70a3-4346-95a6-209bcb503629", + # "taker_order_id": "88bf7086-7b15-40ff-8b19-ab4e08516d69", + # "size": "0.00021019", + # "price": "47338.46", + # "taker_profile_id": "774ee0ce-fdda-405f-aa8d-47189a14ba0a", + # "taker_user_id": "54fc141576dcf32596000133", + # "taker_fee_rate": "0.005" + # } + # + # Order is canceled / closed + # + # { + # "type": "done", + # "side": "buy", + # "product_id": "BTC-USDC", + # "time": "2021-03-05T16:37:13.396107Z", + # "sequence": 5641897877, + # "profile_id": "774ee0ce-fdda-405f-aa8d-47189a14ba0a", + # "user_id": "54fc141576dcf32596000133", + # "order_id": "88bf7086-7b15-40ff-8b19-ab4e08516d69", + # "reason": "filled" + # } + # + currentOrders = self.orders + if currentOrders is None: + limit = self.safe_integer(self.options, 'ordersLimit', 1000) + currentOrders = ArrayCacheBySymbolById(limit) + self.orders = currentOrders + type = self.safe_string(message, 'type') + marketId = self.safe_string(message, 'product_id') + if marketId is not None: + messageHash = 'orders:' + marketId + symbol = self.safe_symbol(marketId) + orderId = self.safe_string(message, 'order_id') + makerOrderId = self.safe_string(message, 'maker_order_id') + takerOrderId = self.safe_string(message, 'taker_order_id') + orders = self.orders + previousOrders = self.safe_value(orders.hashmap, symbol, {}) + previousOrder = self.safe_value(previousOrders, orderId) + if previousOrder is None: + previousOrder = self.safe_value_2(previousOrders, makerOrderId, takerOrderId) + if previousOrder is None: + parsed = self.parse_ws_order(message) + orders.append(parsed) + client.resolve(orders, messageHash) + else: + sequence = self.safe_integer(message, 'sequence') + previousInfo = self.safe_value(previousOrder, 'info', {}) + previousSequence = self.safe_integer(previousInfo, 'sequence') + if (previousSequence is None) or (sequence > previousSequence): + if type == 'match': + trade = self.parse_ws_trade(message) + if previousOrder['trades'] is None: + previousOrder['trades'] = [] + previousOrder['trades'].append(trade) + previousOrder['lastTradeTimestamp'] = trade['timestamp'] + totalCost = 0 + totalAmount = 0 + trades = previousOrder['trades'] + for i in range(0, len(trades)): + tradeEntry = trades[i] + totalCost = self.sum(totalCost, tradeEntry['cost']) + totalAmount = self.sum(totalAmount, tradeEntry['amount']) + if totalAmount > 0: + previousOrder['average'] = totalCost / totalAmount + previousOrder['cost'] = totalCost + if previousOrder['filled'] is not None: + previousOrder['filled'] += trade['amount'] + if previousOrder['amount'] is not None: + previousOrder['remaining'] = previousOrder['amount'] - previousOrder['filled'] + if previousOrder['fee'] is None: + previousOrder['fee'] = { + 'cost': 0, + 'currency': trade['fee']['currency'], + } + if (previousOrder['fee']['cost'] is not None) and (trade['fee']['cost'] is not None): + previousOrder['fee']['cost'] = self.sum(previousOrder['fee']['cost'], trade['fee']['cost']) + # update the newUpdates count + orders.append(previousOrder) + client.resolve(orders, messageHash) + elif (type == 'received') or (type == 'done'): + info = self.extend(previousOrder['info'], message) + order = self.parse_ws_order(info) + keys = list(order.keys()) + # update the reference + for i in range(0, len(keys)): + key = keys[i] + if order[key] is not None: + previousOrder[key] = order[key] + # update the newUpdates count + orders.append(previousOrder) + client.resolve(orders, messageHash) + + def parse_ws_order(self, order, market=None): + id = self.safe_string(order, 'order_id') + clientOrderId = self.safe_string(order, 'client_oid') + marketId = self.safe_string(order, 'product_id') + symbol = self.safe_symbol(marketId) + side = self.safe_string(order, 'side') + price = self.safe_number(order, 'price') + amount = self.safe_number_2(order, 'size', 'funds') + time = self.safe_string(order, 'time') + timestamp = self.parse8601(time) + reason = self.safe_string(order, 'reason') + status = self.parse_ws_order_status(reason) + orderType = self.safe_string(order, 'order_type') + remaining = self.safe_number(order, 'remaining_size') + type = self.safe_string(order, 'type') + filled = None + if (amount is not None) and (remaining is not None): + filled = amount - remaining + elif type == 'received': + filled = 0 + if amount is not None: + remaining = amount - filled + return self.safe_order({ + 'info': order, + 'symbol': symbol, + 'id': id, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'type': orderType, + 'timeInForce': None, + 'postOnly': None, + 'side': side, + 'price': price, + 'stopPrice': None, + 'triggerPrice': None, + 'amount': amount, + 'cost': None, + 'average': None, + 'filled': filled, + 'remaining': remaining, + 'status': status, + 'fee': None, + 'trades': None, + }) + + def handle_ticker(self, client: Client, message): + # + # { + # "type": "ticker", + # "sequence": 12042642428, + # "product_id": "BTC-USD", + # "price": "9380.55", + # "open_24h": "9450.81000000", + # "volume_24h": "9611.79166047", + # "low_24h": "9195.49000000", + # "high_24h": "9475.19000000", + # "volume_30d": "327812.00311873", + # "best_bid": "9380.54", + # "best_ask": "9380.55", + # "side": "buy", + # "time": "2020-02-01T01:40:16.253563Z", + # "trade_id": 82062566, + # "last_size": "0.41969131" + # } + # + marketId = self.safe_string(message, 'product_id') + if marketId is not None: + ticker = self.parse_ticker(message) + symbol = ticker['symbol'] + self.tickers[symbol] = ticker + messageHash = 'ticker:' + symbol + idMessageHash = 'ticker:' + marketId + client.resolve(ticker, messageHash) + client.resolve(ticker, idMessageHash) + return message + + def parse_ticker(self, ticker, market=None) -> Ticker: + # + # { + # "type": "ticker", + # "sequence": 7388547310, + # "product_id": "BTC-USDT", + # "price": "22345.67", + # "open_24h": "22308.13", + # "volume_24h": "470.21123644", + # "low_24h": "22150", + # "high_24h": "22495.15", + # "volume_30d": "25713.98401605", + # "best_bid": "22345.67", + # "best_bid_size": "0.10647825", + # "best_ask": "22349.68", + # "best_ask_size": "0.03131702", + # "side": "sell", + # "time": "2023-03-04T03:37:20.799258Z", + # "trade_id": 11586478, + # "last_size": "0.00352175" + # } + # + type = self.safe_string(ticker, 'type') + if type is None: + return super(coinbaseexchange, self).parse_ticker(ticker, market) + marketId = self.safe_string(ticker, 'product_id') + symbol = self.safe_symbol(marketId, market, '-') + timestamp = self.parse8601(self.safe_string(ticker, 'time')) + last = self.safe_string(ticker, 'price') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high_24h'), + 'low': self.safe_string(ticker, 'low_24h'), + 'bid': self.safe_string(ticker, 'best_bid'), + 'bidVolume': self.safe_string(ticker, 'best_bid_size'), + 'ask': self.safe_string(ticker, 'best_ask'), + 'askVolume': self.safe_string(ticker, 'best_ask_size'), + 'vwap': None, + 'open': self.safe_string(ticker, 'open_24h'), + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': self.safe_string(ticker, 'volume_24h'), + 'quoteVolume': None, + 'info': ticker, + }) + + def handle_delta(self, bookside, delta): + price = self.safe_number(delta, 0) + amount = self.safe_number(delta, 1) + bookside.store(price, amount) + + def handle_deltas(self, bookside, deltas): + for i in range(0, len(deltas)): + self.handle_delta(bookside, deltas[i]) + + def handle_order_book(self, client: Client, message): + # + # first message(snapshot) + # + # { + # "type": "snapshot", + # "product_id": "BTC-USD", + # "bids": [ + # ["10101.10", "0.45054140"] + # ], + # "asks": [ + # ["10102.55", "0.57753524"] + # ] + # } + # + # subsequent updates + # + # { + # "type": "l2update", + # "product_id": "BTC-USD", + # "time": "2019-08-14T20:42:27.265Z", + # "changes": [ + # ["buy", "10101.80000000", "0.162567"] + # ] + # } + # + type = self.safe_string(message, 'type') + marketId = self.safe_string(message, 'product_id') + market = self.safe_market(marketId, None, '-') + symbol = market['symbol'] + name = 'level2' + messageHash = name + ':' + marketId + subscription = self.safe_value(client.subscriptions, messageHash, {}) + limit = self.safe_integer(subscription, 'limit') + if type == 'snapshot': + self.orderbooks[symbol] = self.order_book({}, limit) + orderbook = self.orderbooks[symbol] + self.handle_deltas(orderbook['asks'], self.safe_value(message, 'asks', [])) + self.handle_deltas(orderbook['bids'], self.safe_value(message, 'bids', [])) + orderbook['timestamp'] = None + orderbook['datetime'] = None + orderbook['symbol'] = symbol + client.resolve(orderbook, messageHash) + elif type == 'l2update': + orderbook = self.orderbooks[symbol] + timestamp = self.parse8601(self.safe_string(message, 'time')) + changes = self.safe_value(message, 'changes', []) + sides: dict = { + 'sell': 'asks', + 'buy': 'bids', + } + for i in range(0, len(changes)): + change = changes[i] + key = self.safe_string(change, 0) + side = self.safe_string(sides, key) + price = self.safe_number(change, 1) + amount = self.safe_number(change, 2) + bookside = orderbook[side] + bookside.store(price, amount) + orderbook['timestamp'] = timestamp + orderbook['datetime'] = self.iso8601(timestamp) + client.resolve(orderbook, messageHash) + + def handle_subscription_status(self, client: Client, message): + # + # { + # "type": "subscriptions", + # "channels": [ + # { + # "name": "level2", + # "product_ids": ["ETH-BTC"] + # } + # ] + # } + # + return message + + def handle_error_message(self, client: Client, message) -> Bool: + # + # { + # "type": "error", + # "message": "error message", + # /* ...""" + # } + # + # auth error + # + # { + # "type": "error", + # "message": "Authentication Failed", + # "reason": "{"message":"Invalid API Key"}" + # } + # + errMsg = self.safe_string(message, 'message') + reason = self.safe_string(message, 'reason') + try: + if errMsg == 'Authentication Failed': + raise AuthenticationError('Authentication failed: ' + reason) + else: + raise ExchangeError(self.id + ' ' + reason) + except Exception as error: + client.reject(error) + return True + + def handle_message(self, client: Client, message): + type = self.safe_string(message, 'type') + methods: dict = { + 'snapshot': self.handle_order_book, + 'l2update': self.handle_order_book, + 'subscribe': self.handle_subscription_status, + 'ticker': self.handle_ticker, + 'received': self.handle_order, + 'open': self.handle_order, + 'change': self.handle_order, + 'done': self.handle_order, + 'error': self.handle_error_message, + } + length = len(client.url) - 0 + authenticated = client.url[length - 1] == '?' + method = self.safe_value(methods, type) + if method is None: + if type == 'match': + if authenticated: + self.handle_my_trade(client, message) + self.handle_order(client, message) + else: + self.handle_trade(client, message) + else: + method(client, message) diff --git a/ccxt/pro/coinbaseinternational.py b/ccxt/pro/coinbaseinternational.py new file mode 100644 index 0000000..45ab4dd --- /dev/null +++ b/ccxt/pro/coinbaseinternational.py @@ -0,0 +1,776 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheByTimestamp +import hashlib +from ccxt.base.types import Any, Bool, Int, Market, OrderBook, Strings, Ticker, Tickers, FundingRate, FundingRates, Trade +from ccxt.async_support.base.ws.client import Client +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import NotSupported + + +class coinbaseinternational(ccxt.async_support.coinbaseinternational): + + def describe(self) -> Any: + return self.deep_extend(super(coinbaseinternational, self).describe(), { + 'has': { + 'ws': True, + 'watchTrades': True, + 'watchTradesForSymbols': True, + 'watchOrderBook': True, + 'watchOrderBookForSymbols': True, + 'watchTicker': True, + 'watchBalance': False, + 'watchMyTrades': False, + 'watchOHLCV': True, + 'watchOHLCVForSymbols': False, + 'watchOrders': False, + 'watchOrdersForSymbols': False, + 'watchPositions': False, + 'watchTickers': True, + 'createOrderWs': False, + 'editOrderWs': False, + 'cancelOrderWs': False, + 'cancelOrdersWs': False, + 'cancelAllOrdersWs': False, + 'fetchOrderWs': False, + 'fetchOrdersWs': False, + 'fetchBalanceWs': False, + 'fetchMyTradesWs': False, + }, + 'urls': { + 'api': { + 'ws': 'wss://ws-md.international.coinbase.com', + }, + 'test': { + 'ws': 'wss://ws-md.n5e2.coinbase.com', + }, + }, + 'options': { + 'watchTicker': { + 'channel': 'LEVEL1', # 'INSTRUMENTS' or 'RISK' + }, + 'tradesLimit': 1000, + 'ordersLimit': 1000, + 'myTradesLimit': 1000, + 'timeframes': { + '1m': 'CANDLES_ONE_MINUTE', + '5m': 'CANDLES_FIVE_MINUTES', + '30m': 'CANDLES_THIRTY_MINUTES', + '1h': 'CANDLES_ONE_HOUR', + '2h': 'CANDLES_TWO_HOURS', + '1d': 'CANDLES_ONE_DAY', + }, + }, + 'exceptions': { + 'exact': { + 'Unable to authenticate': AuthenticationError, + }, + }, + }) + + async def subscribe(self, name: str, symbols: Strings = None, params={}): + """ + @ignore + subscribes to a websocket channel + + https://docs.cloud.coinbase.com/intx/docs/websocket-overview#subscribe + + :param str name: the name of the channel + :param str[] [symbols]: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: subscription to a websocket channel + """ + await self.load_markets() + self.check_required_credentials() + market = None + messageHash = name + productIds = None + if symbols is None: + symbols = self.get_active_symbols() + symbolsLength = len(symbols) + messageHashes = [] + if symbolsLength > 1: + parsedSymbols = self.market_symbols(symbols) + marketIds = self.market_ids(parsedSymbols) + productIds = marketIds + for i in range(0, len(parsedSymbols)): + messageHashes.append(name + '::' + parsedSymbols[i]) + # messageHash = messageHash + '::' + ','.join(parsedSymbols) + elif symbolsLength == 1: + market = self.market(symbols[0]) + messageHash = name + '::' + market['symbol'] + productIds = [market['id']] + url = self.urls['api']['ws'] + if url is None: + raise NotSupported(self.id + ' is not supported in sandbox environment') + timestamp = str(self.nonce()) + auth = timestamp + self.apiKey + 'CBINTLMD' + self.password + signature = self.hmac(self.encode(auth), self.base64_to_binary(self.secret), hashlib.sha256, 'base64') + subscribe: dict = { + 'type': 'SUBSCRIBE', + # 'product_ids': productIds, + 'channels': [name], + 'time': timestamp, + 'key': self.apiKey, + 'passphrase': self.password, + 'signature': signature, + } + if productIds is not None: + subscribe['product_ids'] = productIds + if symbolsLength > 1: + return await self.watch_multiple(url, messageHashes, self.extend(subscribe, params), messageHashes) + return await self.watch(url, messageHash, self.extend(subscribe, params), messageHash) + + async def subscribe_multiple(self, name: str, symbols: Strings = None, params={}): + """ + @ignore + subscribes to a websocket channel using watchMultiple + + https://docs.cloud.coinbase.com/intx/docs/websocket-overview#subscribe + + :param str name: the name of the channel + :param string|str[] [symbols]: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: subscription to a websocket channel + """ + await self.load_markets() + self.check_required_credentials() + if self.is_empty(symbols): + symbols = self.symbols + else: + symbols = self.market_symbols(symbols) + messageHashes = [] + productIds = [] + for i in range(0, len(symbols)): + marketId = self.market_id(symbols[i]) + symbol = self.symbol(marketId) + productIds.append(marketId) + messageHashes.append(name + '::' + symbol) + url = self.urls['api']['ws'] + if url is None: + raise NotSupported(self.id + ' is not supported in sandbox environment') + timestamp = self.number_to_string(self.seconds()) + auth = timestamp + self.apiKey + 'CBINTLMD' + self.password + signature = self.hmac(self.encode(auth), self.base64_to_binary(self.secret), hashlib.sha256, 'base64') + subscribe: dict = { + 'type': 'SUBSCRIBE', + 'time': timestamp, + 'product_ids': productIds, + 'channels': [name], + 'key': self.apiKey, + 'passphrase': self.password, + 'signature': signature, + } + return await self.watch_multiple(url, messageHashes, self.extend(subscribe, params), messageHashes) + + async def watch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + watch the current funding rate + + https://docs.cloud.coinbase.com/intx/docs/websocket-channels#funding-channel + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + await self.load_markets() + return await self.subscribe('RISK', [symbol], params) + + async def watch_funding_rates(self, symbols: List[str], params={}) -> FundingRates: + """ + watch the funding rate for multiple markets + + https://docs.cloud.coinbase.com/intx/docs/websocket-channels#funding-channel + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `funding rates structures `, indexe by market symbols + """ + await self.load_markets() + fundingRate = await self.subscribe_multiple('RISK', symbols, params) + symbol = self.safe_string(fundingRate, 'symbol') + if self.newUpdates: + result: dict = {} + result[symbol] = fundingRate + return result + return self.filter_by_array(self.fundingRates, 'symbol', symbols) + + async def watch_ticker(self, symbol: str, params={}) -> Ticker: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://docs.cloud.coinbase.com/intx/docs/websocket-channels#instruments-channel + + :param str [symbol]: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.channel]: the channel to watch, 'LEVEL1' or 'INSTRUMENTS', default is 'LEVEL1' + :returns dict: a `ticker structure ` + """ + await self.load_markets() + channel = None + channel, params = self.handle_option_and_params(params, 'watchTicker', 'channel', 'LEVEL1') + return await self.subscribe(channel, [symbol], params) + + def get_active_symbols(self): + symbols = self.symbols + output = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + market = self.markets[symbol] + if market['active']: + output.append(symbol) + return output + + async def watch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://docs.cloud.coinbase.com/intx/docs/websocket-channels#instruments-channel + + :param str[] [symbols]: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.channel]: the channel to watch, 'LEVEL1' or 'INSTRUMENTS', default is 'INSTLEVEL1UMENTS' + :returns dict: a `ticker structure ` + """ + await self.load_markets() + channel = None + channel, params = self.handle_option_and_params(params, 'watchTickers', 'channel', 'LEVEL1') + ticker = await self.subscribe(channel, symbols, params) + if self.newUpdates: + result: dict = {} + result[ticker['symbol']] = ticker + return result + return self.filter_by_array(self.tickers, 'symbol', symbols) + + def handle_instrument(self, client: Client, message): + # + # { + # "sequence": 1, + # "product_id": "ETH-PERP", + # "instrument_type": "PERP", + # "base_asset_name": "ETH", + # "quote_asset_name": "USDC", + # "base_increment": "0.0001", + # "quote_increment": "0.01", + # "avg_daily_quantity": "43.0", + # "avg_daily_volume": "80245.2", + # "total_30_day_quantity":"1443.0", + # "total_30_day_volume":"3040449.0", + # "total_24_hour_quantity":"48.1", + # "total_24_hour_volume":"101348.3", + # "base_imf": "0.2", + # "min_quantity": "0.0001", + # "position_size_limit": "500", + # "funding_interval": "60000000000", + # "trading_state": "trading", + # "last_update_time": "2023-05-04T11:16:33.016Z", + # "time": "2023-05-10T14:58:47.000Z", + # "channel":"INSTRUMENTS", + # "type":"SNAPSHOT" + # } + ticker = self.parse_ws_instrument(message) + channel = self.safe_string(message, 'channel') + client.resolve(ticker, channel) + client.resolve(ticker, channel + '::' + ticker['symbol']) + + def parse_ws_instrument(self, ticker: dict, market=None): + # + # { + # "sequence": 1, + # "product_id": "ETH-PERP", + # "instrument_type": "PERP", + # "base_asset_name": "ETH", + # "quote_asset_name": "USDC", + # "base_increment": "0.0001", + # "quote_increment": "0.01", + # "avg_daily_quantity": "43.0", + # "avg_daily_volume": "80245.2", + # "total_30_day_quantity":"1443.0", + # "total_30_day_volume":"3040449.0", + # "total_24_hour_quantity":"48.1", + # "total_24_hour_volume":"101348.3", + # "base_imf": "0.2", + # "min_quantity": "0.0001", + # "position_size_limit": "500", + # "funding_interval": "60000000000", + # "trading_state": "trading", + # "last_update_time": "2023-05-04T11:16:33.016Z", + # "time": "2023-05-10T14:58:47.000Z", + # "channel":"INSTRUMENTS", + # "type":"SNAPSHOT" + # } + # instruments + # { + # sequence: 0, + # instrument_type: 'PERP', + # instrument_mode: 'standard', + # base_asset_name: 'BTC', + # quote_asset_name: 'USDC', + # base_increment: '0.0001', + # quote_increment: '0.1', + # avg_daily_quantity: '502.8845', + # avg_daily_volume: '3.1495242961566668E7', + # total30_day_quantity: '15086.535', + # total30_day_volume: '9.44857288847E8', + # total24_hour_quantity: '5.0', + # total24_hour_volume: '337016.5', + # base_imf: '0.1', + # min_quantity: '0.0001', + # position_size_limit: '800', + # funding_interval: '3600000000000', + # trading_state: 'trading', + # last_updated_time: '2024-07-30T15:00:00Z', + # default_initial_margin: '0.2', + # base_asset_multiplier: '1.0', + # channel: 'INSTRUMENTS', + # type: 'SNAPSHOT', + # time: '2024-07-30T15:26:56.766Z', + # } + # + marketId = self.safe_string(ticker, 'product_id') + datetime = self.safe_string(ticker, 'time') + return self.safe_ticker({ + 'info': ticker, + 'symbol': self.safe_symbol(marketId, market, '-'), + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + 'high': None, + 'low': None, + 'bid': None, + 'bidVolume': None, + 'ask': None, + 'askVolume': None, + 'vwap': None, + 'open': None, + 'close': None, + 'last': None, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': self.safe_string_2(ticker, 'total_24_hour_quantity', 'total24_hour_quantity'), + 'quoteVolume': self.safe_string_2(ticker, 'total_24_hour_volume', 'total24_hour_volume'), + }) + + def handle_ticker(self, client: Client, message): + # + # snapshot + # { + # "sequence": 0, + # "product_id": "BTC-PERP", + # "time": "2023-05-10T14:58:47.000Z", + # "bid_price": "28787.8", + # "bid_qty": "0.466", # One side book + # "channel": "LEVEL1", + # "type": "SNAPSHOT" + # } + # update + # { + # "sequence": 1, + # "product_id": "BTC-PERP", + # "time": "2023-05-10T14:58:47.547Z", + # "bid_price": "28787.8", + # "bid_qty": "0.466", + # "ask_price": "28788.8", + # "ask_qty": "1.566", + # "channel": "LEVEL1", + # "type": "UPDATE" + # } + # + ticker = self.parse_ws_ticker(message) + channel = self.safe_string(message, 'channel') + client.resolve(ticker, channel) + client.resolve(ticker, channel + '::' + ticker['symbol']) + + def parse_ws_ticker(self, ticker: object, market: Market = None) -> Ticker: + # + # { + # "sequence": 1, + # "product_id": "BTC-PERP", + # "time": "2023-05-10T14:58:47.547Z", + # "bid_price": "28787.8", + # "bid_qty": "0.466", + # "ask_price": "28788.8", + # "ask_qty": "1.566", + # "channel": "LEVEL1", + # "type": "UPDATE" + # } + # + datetime = self.safe_string(ticker, 'time') + marketId = self.safe_string(ticker, 'product_id') + return self.safe_ticker({ + 'info': ticker, + 'symbol': self.safe_symbol(marketId, market), + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + 'bid': self.safe_number(ticker, 'bid_price'), + 'bidVolume': self.safe_number(ticker, 'bid_qty'), + 'ask': self.safe_number(ticker, 'ask_price'), + 'askVolume': self.safe_number(ticker, 'ask_qty'), + 'high': None, + 'low': None, + 'open': None, + 'close': None, + 'last': None, + 'change': None, + 'percentage': None, + 'average': None, + 'vwap': None, + 'baseVolume': None, + 'quoteVolume': None, + 'previousClose': None, + }) + + async def watch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + watches historical candlestick data containing the open, high, low, close price, and the volume of a market + + https://docs.cdp.coinbase.com/intx/docs/websocket-channels#candles-channel + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + options = self.safe_dict(self.options, 'timeframes', {}) + interval = self.safe_string(options, timeframe, timeframe) + ohlcv = await self.subscribe(interval, [symbol], params) + if self.newUpdates: + limit = ohlcv.getLimit(symbol, limit) + return self.filter_by_since_limit(ohlcv, since, limit, 0, True) + + def handle_ohlcv(self, client: Client, message): + # + # { + # "sequence": 0, + # "product_id": "BTC-PERP", + # "channel": "CANDLES_ONE_MINUTE", + # "type": "SNAPSHOT", + # "candles": [ + # { + # "time": "2023-05-10T14:58:47.000Z", + # "low": "28787.8", + # "high": "28788.8", + # "open": "28788.8", + # "close": "28787.8", + # "volume": "0.466" + # }, + # ] + # } + # + messageHash = self.safe_string(message, 'channel') + marketId = self.safe_string(message, 'product_id') + market = self.safe_market(marketId) + symbol = market['symbol'] + timeframe = self.find_timeframe(messageHash) + self.ohlcvs[symbol] = self.safe_value(self.ohlcvs, symbol, {}) + if self.safe_value(self.ohlcvs[symbol], timeframe) is None: + limit = self.safe_integer(self.options, 'OHLCVLimit', 1000) + self.ohlcvs[symbol][timeframe] = ArrayCacheByTimestamp(limit) + stored = self.ohlcvs[symbol][timeframe] + data = self.safe_list(message, 'candles', []) + for i in range(0, len(data)): + tick = data[i] + parsed = self.parse_ohlcv(tick, market) + stored.append(parsed) + client.resolve(stored, messageHash + '::' + symbol) + + async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://docs.cloud.coinbase.com/intx/docs/websocket-channels#match-channel + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + return await self.watch_trades_for_symbols([symbol], since, limit, params) + + async def watch_trades_for_symbols(self, symbols: List[str], since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a list of symbols + :param str[] symbols: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, False, True, True) + trades = await self.subscribe_multiple('MATCH', symbols, params) + if self.newUpdates: + first = self.safe_dict(trades, 0) + tradeSymbol = self.safe_string(first, 'symbol') + limit = trades.getLimit(tradeSymbol, limit) + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + def handle_trade(self, client, message): + # + # { + # "sequence": 0, + # "product_id": "BTC-PERP", + # "time": "2023-05-10T14:58:47.002Z", + # "match_id": "177101110052388865", + # "trade_qty": "0.006", + # "aggressor_side": "BUY", + # "trade_price": "28833.1", + # "channel": "MATCH", + # "type": "UPDATE" + # } + # + trade = self.parse_ws_trade(message) + symbol = trade['symbol'] + channel = self.safe_string(message, 'channel') + if not (symbol in self.trades): + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + tradesArrayCache = ArrayCache(limit) + self.trades[symbol] = tradesArrayCache + tradesArray = self.trades[symbol] + tradesArray.append(trade) + self.trades[symbol] = tradesArray + client.resolve(tradesArray, channel) + client.resolve(tradesArray, channel + '::' + trade['symbol']) + return message + + def parse_ws_trade(self, trade, market=None): + # + # { + # "sequence": 0, + # "product_id": "BTC-PERP", + # "time": "2023-05-10T14:58:47.002Z", + # "match_id": "177101110052388865", + # "trade_qty": "0.006", + # "aggressor_side": "BUY", + # "trade_price": "28833.1", + # "channel": "MATCH", + # "type": "UPDATE" + # } + marketId = self.safe_string_2(trade, 'symbol', 'product_id') + datetime = self.safe_string(trade, 'time') + return self.safe_trade({ + 'info': trade, + 'id': self.safe_string(trade, 'match_id'), + 'order': None, + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + 'symbol': self.safe_symbol(marketId, market), + 'type': None, + 'side': self.safe_string_lower(trade, 'agressor_side'), + 'takerOrMaker': None, + 'price': self.safe_string(trade, 'trade_price'), + 'amount': self.safe_string(trade, 'trade_qty'), + 'cost': None, + 'fee': None, + }) + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://docs.cloud.coinbase.com/intx/docs/websocket-channels#level2-channel + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + return await self.watch_order_book_for_symbols([symbol], limit, params) + + async def watch_order_book_for_symbols(self, symbols: List[str], limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://docs.cloud.coinbase.com/intx/docs/websocket-channels#level2-channel + + :param str[] symbols: + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + return await self.subscribe_multiple('LEVEL2', symbols, params) + + def handle_order_book(self, client, message): + # + # snapshot + # { + # "sequence": 0, + # "product_id": "BTC-PERP", + # "time": "2023-05-10T14:58:47.000Z", + # "bids": [ + # ["29100", "0.02"], + # ["28950", "0.01"], + # ["28900", "0.01"] + # ], + # "asks": [ + # ["29267.8", "18"], + # ["29747.6", "18"], + # ["30227.4", "9"] + # ], + # "channel": "LEVEL2", + # "type": "SNAPSHOT", + # } + # update + # { + # "sequence": 1, + # "product_id": "BTC-PERP", + # "time": "2023-05-10T14:58:47.375Z", + # "changes": [ + # [ + # "BUY", + # "28787.7", + # "6" + # ] + # ], + # "channel": "LEVEL2", + # "type": "UPDATE" + # } + # + type = self.safe_string(message, 'type') + marketId = self.safe_string(message, 'product_id') + symbol = self.safe_symbol(marketId) + datetime = self.safe_string(message, 'time') + channel = self.safe_string(message, 'channel') + if not (symbol in self.orderbooks): + limit = self.safe_integer(self.options, 'watchOrderBookLimit', 1000) + self.orderbooks[symbol] = self.order_book({}, limit) + orderbook = self.orderbooks[symbol] + if type == 'SNAPSHOT': + parsedSnapshot = self.parse_order_book(message, symbol, None, 'bids', 'asks') + orderbook.reset(parsedSnapshot) + orderbook['symbol'] = symbol + else: + changes = self.safe_list(message, 'changes', []) + self.handle_deltas(orderbook, changes) + orderbook['nonce'] = self.safe_integer(message, 'sequence') + orderbook['datetime'] = datetime + orderbook['timestamp'] = self.parse8601(datetime) + self.orderbooks[symbol] = orderbook + client.resolve(orderbook, channel + '::' + symbol) + + def handle_delta(self, orderbook, delta): + rawSide = self.safe_string_lower(delta, 0) + side = 'bids' if (rawSide == 'buy') else 'asks' + price = self.safe_float(delta, 1) + amount = self.safe_float(delta, 2) + bookside = orderbook[side] + bookside.store(price, amount) + + def handle_deltas(self, orderbook, deltas): + for i in range(0, len(deltas)): + self.handle_delta(orderbook, deltas[i]) + + def handle_subscription_status(self, client, message): + # + # { + # "channels": [ + # { + # "name": "MATCH", + # "product_ids": [ + # "BTC-PERP", + # "ETH-PERP" + # ] + # }, + # { + # "name": "INSTRUMENTS", + # "product_ids": [ + # "BTC-PERP", + # "ETH-PERP" + # ] + # } + # ], + # "authenticated": True, + # "channel": "SUBSCRIPTIONS", + # "type": "SNAPSHOT", + # "time": "2023-05-30T16:53:46.847Z" + # } + # + return message + + def handle_funding_rate(self, client: Client, message): + # + # snapshot + # { + # "sequence": 0, + # "product_id": "BTC-PERP", + # "time": "2023-05-10T14:58:47.000Z", + # "funding_rate": "0.001387", + # "is_final": True, + # "channel": "FUNDING", + # "type": "SNAPSHOT" + # } + # update + # { + # "sequence": 1, + # "product_id": "BTC-PERP", + # "time": "2023-05-10T15:00:00.000Z", + # "funding_rate": "0.001487", + # "is_final": False, + # "channel": "FUNDING", + # "type": "UPDATE" + # } + # + channel = self.safe_string(message, 'channel') + fundingRate = self.parse_funding_rate(message) + self.fundingRates[fundingRate['symbol']] = fundingRate + client.resolve(fundingRate, channel + '::' + fundingRate['symbol']) + + def handle_error_message(self, client: Client, message) -> Bool: + # + # { + # message: 'Failed to subscribe', + # reason: 'Unable to authenticate', + # channel: 'SUBSCRIPTIONS', + # type: 'REJECT' + # } + # + type = self.safe_string(message, 'type') + if type != 'REJECT': + return False + reason = self.safe_string(message, 'reason') + errMsg = self.safe_string(message, 'message') + try: + feedback = self.id + ' ' + errMsg + reason + self.throw_exactly_matched_exception(self.exceptions['exact'], reason, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], reason, feedback) + raise ExchangeError(feedback) + except Exception as e: + client.reject(e) + return True + + def handle_message(self, client, message): + if self.handle_error_message(client, message): + return + channel = self.safe_string(message, 'channel', '') + methods: dict = { + 'SUBSCRIPTIONS': self.handle_subscription_status, + 'INSTRUMENTS': self.handle_instrument, + 'LEVEL1': self.handle_ticker, + 'MATCH': self.handle_trade, + 'LEVEL2': self.handle_order_book, + 'FUNDING': self.handle_funding_rate, + 'RISK': self.handle_ticker, + } + type = self.safe_string(message, 'type') + if type == 'error': + errorMessage = self.safe_string(message, 'message') + raise ExchangeError(errorMessage) + if channel.find('CANDLES') > -1: + self.handle_ohlcv(client, message) + method = self.safe_value(methods, channel) + if method is not None: + method(client, message) diff --git a/ccxt/pro/coincatch.py b/ccxt/pro/coincatch.py new file mode 100644 index 0000000..def9a85 --- /dev/null +++ b/ccxt/pro/coincatch.py @@ -0,0 +1,1463 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolById, ArrayCacheBySymbolBySide, ArrayCacheByTimestamp +import hashlib +from ccxt.base.types import Any, Balances, Bool, Int, Market, Order, OrderBook, Position, Str, Strings, Ticker, Tickers, Trade +from ccxt.async_support.base.ws.client import Client +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import NotSupported +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import ChecksumError +from ccxt.base.errors import UnsubscribeError +from ccxt.base.precise import Precise + + +class coincatch(ccxt.async_support.coincatch): + + def describe(self) -> Any: + return self.deep_extend(super(coincatch, self).describe(), { + 'has': { + 'ws': True, + 'watchTrades': True, + 'watchTradesForSymbols': True, + 'watchOrderBook': True, + 'watchOrderBookForSymbols': True, + 'watchOHLCV': True, + 'watchOHLCVForSymbols': False, # todo + 'watchOrders': True, + 'watchMyTrades': False, + 'watchTicker': True, + 'watchTickers': True, + 'watchBalance': True, + 'watchPositions': True, + }, + 'urls': { + 'api': { + 'ws': { + 'public': 'wss://ws.coincatch.com/public/v1/stream', + 'private': 'wss://ws.coincatch.com/private/v1/stream', + }, + }, + }, + 'options': { + 'tradesLimit': 1000, + 'OHLCVLimit': 200, + 'timeframesForWs': { + '1m': '1m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1H', + '4h': '4H', + '12h': '12H', + '1d': '1D', + '1w': '1W', + }, + 'watchOrderBook': { + 'checksum': True, + }, + }, + 'streaming': { + 'ping': self.ping, + }, + 'exceptions': { + 'ws': { + 'exact': { + '30001': BadRequest, # Channel does not exist + '30002': AuthenticationError, # illegal request + '30003': BadRequest, # invalid op + '30004': AuthenticationError, # User needs to log in + '30005': AuthenticationError, # login failed + '30006': RateLimitExceeded, # request too many + '30007': RateLimitExceeded, # request over limit,connection close + '30011': AuthenticationError, # invalid ACCESS_KEY + '30012': AuthenticationError, # invalid ACCESS_PASSPHRASE + '30013': AuthenticationError, # invalid ACCESS_TIMESTAMP + '30014': BadRequest, # Request timestamp expired + '30015': AuthenticationError, # {event: 'error', code: 30015, msg: 'Invalid sign'} + }, + 'broad': {}, + }, + }, + }) + + def get_market_from_arg(self, entry): + instId = self.safe_string(entry, 'instId') + instType = self.safe_string(entry, 'instType') + baseAndQuote = self.parseSpotMarketId(instId) + baseId = baseAndQuote['baseId'] + quoteId = baseAndQuote['quoteId'] + suffix = '_SPBL' # spot suffix + if instType == 'mc': + if quoteId == 'USD': + suffix = '_DMCBL' + else: + suffix = '_UMCBL' + marketId = self.safe_currency_code(baseId) + self.safe_currency_code(quoteId) + suffix + return self.safeMarketCustom(marketId) + + async def authenticate(self, params={}): + self.check_required_credentials() + url = self.urls['api']['ws']['private'] + client = self.client(url) + messageHash = 'authenticated' + future = client.reusableFuture(messageHash) + authenticated = self.safe_value(client.subscriptions, messageHash) + if authenticated is None: + timestamp = str(self.seconds()) + auth = timestamp + 'GET' + '/user/verify' + signature = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256, 'base64') + operation = 'login' + request: dict = { + 'op': operation, + 'args': [ + { + 'apiKey': self.apiKey, + 'passphrase': self.password, + 'timestamp': timestamp, + 'sign': signature, + }, + ], + } + message = self.extend(request, params) + self.watch(url, messageHash, message, messageHash) + return await future + + async def watch_public(self, messageHash, subscribeHash, args, params={}): + url = self.urls['api']['ws']['public'] + request: dict = { + 'op': 'subscribe', + 'args': [args], + } + message = self.extend(request, params) + return await self.watch(url, messageHash, message, subscribeHash) + + async def un_watch_public(self, messageHash, args, params={}): + url = self.urls['api']['ws']['public'] + request: dict = { + 'op': 'unsubscribe', + 'args': [args], + } + message = self.extend(request, params) + return await self.watch(url, messageHash, message, messageHash) + + async def watch_private(self, messageHash, subscribeHash, args, params={}): + await self.authenticate() + url = self.urls['api']['ws']['private'] + request: dict = { + 'op': 'subscribe', + 'args': [args], + } + message = self.extend(request, params) + return await self.watch(url, messageHash, message, subscribeHash) + + async def watch_private_multiple(self, messageHashes, subscribeHashes, args, params={}): + await self.authenticate() + url = self.urls['api']['ws']['private'] + request: dict = { + 'op': 'subscribe', + 'args': args, + } + message = self.extend(request, params) + return await self.watch_multiple(url, messageHashes, message, subscribeHashes) + + def handle_authenticate(self, client: Client, message): + # + # {event: "login", code: 0} + # + messageHash = 'authenticated' + future = self.safe_value(client.futures, messageHash) + future.resolve(True) + + async def watch_public_multiple(self, messageHashes, subscribeHashes, argsArray, params={}): + url = self.urls['api']['ws']['public'] + request: dict = { + 'op': 'subscribe', + 'args': argsArray, + } + message = self.extend(request, params) + return await self.watch_multiple(url, messageHashes, message, subscribeHashes) + + async def un_watch_channel(self, symbol: str, channel: str, messageHashTopic: str, params={}) -> Any: + await self.load_markets() + market = self.market(symbol) + instType, instId = self.get_public_inst_type_and_id(market) + messageHash = 'unsubscribe:' + messageHashTopic + ':' + symbol + args: dict = { + 'instType': instType, + 'channel': channel, + 'instId': instId, + } + return await self.un_watch_public(messageHash, args, params) + + def get_public_inst_type_and_id(self, market: Market): + instId = market['baseId'] + market['quoteId'] + instType = None + if market['spot']: + instType = 'SP' + elif market['swap']: + instType = 'MC' + else: + raise NotSupported(self.id + ' supports only spot and swap markets') + return [instType, instId] + + def handle_dmcbl_market_by_message_hashes(self, market: Market, hash: str, client: Client, timeframe: Str = None): + marketId = market['id'] + messageHashes = self.find_message_hashes(client, hash) + # the exchange counts DMCBL markets same market with different quote currencies + # for example symbols ETHUSD:ETH and ETH/USD:BTC both have the same marketId ETHUSD_DMCBL + # we need to check all markets with the same marketId to find the correct market that is in messageHashes + marketsWithCurrentId = self.safe_list(self.markets_by_id, marketId, []) + suffix = '' + if timeframe is not None: + suffix = ':' + timeframe + for i in range(0, len(marketsWithCurrentId)): + market = marketsWithCurrentId[i] + symbol = market['symbol'] + messageHash = hash + symbol + suffix + if self.in_array(messageHash, messageHashes): + return market + return market + + async def watch_ticker(self, symbol: str, params={}) -> Ticker: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://coincatch.github.io/github.io/en/spot/#tickers-channel + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.instType]: the type of the instrument to fetch the ticker for, 'SP' for spot markets, 'MC' for futures markets(default is 'SP') + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + instType, instId = self.get_public_inst_type_and_id(market) + channel = 'ticker' + messageHash = channel + ':' + symbol + args: dict = { + 'instType': instType, + 'channel': channel, + 'instId': instId, + } + return await self.watch_public(messageHash, messageHash, args, params) + + async def un_watch_ticker(self, symbol: str, params={}) -> Any: + """ + unsubscribe from the ticker channel + + https://coincatch.github.io/github.io/en/mix/#tickers-channel + + :param str symbol: unified symbol of the market to unwatch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns any: status of the unwatch request + """ + await self.load_markets() + return await self.un_watch_channel(symbol, 'ticker', 'ticker', params) + + async def watch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list + + https://coincatch.github.io/github.io/en/mix/#tickers-channel + + :param str[] symbols: unified symbol of the market to watch the tickers for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + if symbols is None: + symbols = self.symbols + topics = [] + messageHashes = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + market = self.market(symbol) + instType, instId = self.get_public_inst_type_and_id(market) + args: dict = { + 'instType': instType, + 'channel': 'ticker', + 'instId': instId, + } + topics.append(args) + messageHashes.append('ticker:' + symbol) + tickers = await self.watch_public_multiple(messageHashes, messageHashes, topics, params) + if self.newUpdates: + result: dict = {} + result[tickers['symbol']] = tickers + return result + return self.filter_by_array(self.tickers, 'symbol', symbols) + + def handle_ticker(self, client: Client, message): + # + # action: 'snapshot', + # arg: {instType: 'sp', channel: 'ticker', instId: 'ETHUSDT'}, + # data: [ + # { + # instId: 'ETHUSDT', + # last: '2421.06', + # open24h: '2416.93', + # high24h: '2441.47', + # low24h: '2352.99', + # bestBid: '2421.03', + # bestAsk: '2421.06', + # baseVolume: '9445.2043', + # quoteVolume: '22807159.1148', + # ts: 1728131730687, + # labeId: 0, + # openUtc: '2414.50', + # chgUTC: '0.00272', + # bidSz: '3.866', + # askSz: '0.124' + # } + # ], + # ts: 1728131730688 + # + arg = self.safe_dict(message, 'arg', {}) + market = self.get_market_from_arg(arg) + marketId = market['id'] + hash = 'ticker:' + if marketId.find('_DMCBL') >= 0: + market = self.handle_dmcbl_market_by_message_hashes(market, hash, client) + data = self.safe_list(message, 'data', []) + ticker = self.parse_ws_ticker(self.safe_dict(data, 0, {}), market) + symbol = market['symbol'] + self.tickers[symbol] = ticker + messageHash = hash + symbol + client.resolve(self.tickers[symbol], messageHash) + + def parse_ws_ticker(self, ticker, market=None): + # + # spot + # { + # instId: 'ETHUSDT', + # last: '2421.06', + # open24h: '2416.93', + # high24h: '2441.47', + # low24h: '2352.99', + # bestBid: '2421.03', + # bestAsk: '2421.06', + # baseVolume: '9445.2043', + # quoteVolume: '22807159.1148', + # ts: 1728131730687, + # labeId: 0, + # openUtc: '2414.50', + # chgUTC: '0.00272', + # bidSz: '3.866', + # askSz: '0.124' + # } + # + # swap + # { + # instId: 'ETHUSDT', + # last: '2434.47', + # bestAsk: '2434.48', + # bestBid: '2434.47', + # high24h: '2471.68', + # low24h: '2400.01', + # priceChangePercent: '0.00674', + # capitalRate: '0.000082', + # nextSettleTime: 1728489600000, + # systemTime: 1728471993602, + # markPrice: '2434.46', + # indexPrice: '2435.44', + # holding: '171450.25', + # baseVolume: '1699298.91', + # quoteVolume: '4144522832.32', + # openUtc: '2439.67', + # chgUTC: '-0.00213', + # symbolType: 1, + # symbolId: 'ETHUSDT_UMCBL', + # deliveryPrice: '0', + # bidSz: '26.12', + # askSz: '49.6' + # } + # + last = self.safe_string(ticker, 'last') + timestamp = self.safe_integer_2(ticker, 'ts', 'systemTime') + return self.safe_ticker({ + 'symbol': market['symbol'], + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high24h'), + 'low': self.safe_string(ticker, 'low24h'), + 'bid': self.safe_string(ticker, 'bestBid'), + 'bidVolume': self.safe_string(ticker, 'bidSz'), + 'ask': self.safe_string(ticker, 'bestAsk'), + 'askVolume': self.safe_string(ticker, 'askSz'), + 'vwap': None, + 'open': self.safe_string_2(ticker, 'open24h', 'openUtc'), + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': Precise.string_mul(self.safe_string(ticker, 'chgUTC'), '100'), + 'average': None, + 'baseVolume': self.safe_number(ticker, 'baseVolume'), + 'quoteVolume': self.safe_number(ticker, 'quoteVolume'), + 'indexPrice': self.safe_string(ticker, 'indexPrice'), + 'markPrice': self.safe_string(ticker, 'markPrice'), + 'info': ticker, + }, market) + + async def watch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + watches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://coincatch.github.io/github.io/en/spot/#candlesticks-channel + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch(not including) + :param int [limit]: the maximum amount of candles to fetch(not including) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.instType]: the type of the instrument to fetch the OHLCV data for, 'SP' for spot markets, 'MC' for futures markets(default is 'SP') + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + timeframes = self.options['timeframesForWs'] + channel = 'candle' + self.safe_string(timeframes, timeframe) + instType, instId = self.get_public_inst_type_and_id(market) + args: dict = { + 'instType': instType, + 'channel': channel, + 'instId': instId, + } + messageHash = 'ohlcv:' + symbol + ':' + timeframe + ohlcv = await self.watch_public(messageHash, messageHash, args, params) + if self.newUpdates: + limit = ohlcv.getLimit(symbol, limit) + return self.filter_by_since_limit(ohlcv, since, limit, 0, True) + + async def un_watch_ohlcv(self, symbol: str, timeframe: str = '1m', params={}) -> Any: + """ + unsubscribe from the ohlcv channel + + https://www.bitget.com/api-doc/spot/websocket/public/Candlesticks-Channel + + :param str symbol: unified symbol of the market to unwatch the ohlcv for + @param timeframe + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + timeframes = self.options['timeframesForWs'] + interval = self.safe_string(timeframes, timeframe) + channel = 'candle' + interval + return await self.un_watch_channel(symbol, channel, 'ohlcv:' + interval, params) + + def handle_ohlcv(self, client: Client, message): + # + # { + # action: 'update', + # arg: {instType: 'sp', channel: 'candle1D', instId: 'ETHUSDT'}, + # data: [ + # [ + # '1728316800000', + # '2474.5', + # '2478.21', + # '2459.8', + # '2463.51', + # '86.0551' + # ] + # ], + # ts: 1728317607657 + # } + # + arg = self.safe_dict(message, 'arg', {}) + market = self.get_market_from_arg(arg) + marketId = market['id'] + hash = 'ohlcv:' + data = self.safe_list(message, 'data', []) + channel = self.safe_string(arg, 'channel') + klineType = channel[6:] + timeframe = self.find_timeframe(klineType) + if marketId.find('_DMCBL') >= 0: + market = self.handle_dmcbl_market_by_message_hashes(market, hash, client, timeframe) + symbol = market['symbol'] + if not (symbol in self.ohlcvs): + self.ohlcvs[symbol] = {} + if not (timeframe in self.ohlcvs[symbol]): + limit = self.safe_integer(self.options, 'OHLCVLimit', 1000) + self.ohlcvs[symbol][timeframe] = ArrayCacheByTimestamp(limit) + stored = self.ohlcvs[symbol][timeframe] + for i in range(0, len(data)): + candle = self.safe_list(data, i, []) + parsed = self.parse_ws_ohlcv(candle, market) + stored.append(parsed) + messageHash = hash + symbol + ':' + timeframe + client.resolve(stored, messageHash) + + def parse_ws_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # [ + # '1728316800000', + # '2474.5', + # '2478.21', + # '2459.8', + # '2463.51', + # '86.0551' + # ] + # + return [ + self.safe_integer(ohlcv, 0), + self.safe_number(ohlcv, 1), + self.safe_number(ohlcv, 2), + self.safe_number(ohlcv, 3), + self.safe_number(ohlcv, 4), + self.safe_number(ohlcv, 5), + ] + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://coincatch.github.io/github.io/en/spot/#depth-channel + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + return await self.watch_order_book_for_symbols([symbol], limit, params) + + async def un_watch_order_book(self, symbol: str, params={}) -> Any: + """ + unsubscribe from the orderbook channel + + https://coincatch.github.io/github.io/en/spot/#depth-channel + + :param str symbol: unified symbol of the market to fetch the order book for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.limit]: orderbook limit, default is None + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + channel = 'books' + limit = self.safe_integer(params, 'limit') + if (limit == 5) or (limit == 15): + params = self.omit(params, 'limit') + channel += str(limit) + return await self.un_watch_channel(symbol, channel, channel, params) + + async def watch_order_book_for_symbols(self, symbols: List[str], limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://coincatch.github.io/github.io/en/spot/#depth-channel + + @param symbols + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + channel = 'books' + topics = [] + messageHashes = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + market = self.market(symbol) + instType, instId = self.get_public_inst_type_and_id(market) + args: dict = { + 'instType': instType, + 'channel': channel, + 'instId': instId, + } + topics.append(args) + messageHashes.append(channel + ':' + symbol) + orderbook = await self.watch_public_multiple(messageHashes, messageHashes, topics, params) + return orderbook.limit() + + def handle_order_book(self, client: Client, message): + # + # { + # action: 'update', + # arg: {instType: 'sp', channel: 'books', instId: 'ETHUSDT'}, + # data: [ + # { + # asks: [[2507.07, 0.4248]], + # bids: [[2507.05, 0.1198]], + # checksum: -1400923312, + # ts: '1728339446908' + # } + # ], + # ts: 1728339446908 + # } + # + arg = self.safe_dict(message, 'arg', {}) + market = self.get_market_from_arg(arg) + marketId = market['id'] + hash = 'books:' + if marketId.find('_DMCBL') >= 0: + market = self.handle_dmcbl_market_by_message_hashes(market, hash, client) + symbol = market['symbol'] + channel = self.safe_string(arg, 'channel') + messageHash = hash + symbol + data = self.safe_list(message, 'data', []) + rawOrderBook = self.safe_dict(data, 0) + timestamp = self.safe_integer(rawOrderBook, 'ts') + incrementalBook = channel + if incrementalBook: + if not (symbol in self.orderbooks): + ob = self.counted_order_book({}) + ob['symbol'] = symbol + self.orderbooks[symbol] = ob + storedOrderBook = self.orderbooks[symbol] + asks = self.safe_list(rawOrderBook, 'asks', []) + bids = self.safe_list(rawOrderBook, 'bids', []) + self.handle_deltas(storedOrderBook['asks'], asks) + self.handle_deltas(storedOrderBook['bids'], bids) + storedOrderBook['timestamp'] = timestamp + storedOrderBook['datetime'] = self.iso8601(timestamp) + checksum = self.safe_bool(self.options, 'checksum', True) + isSnapshot = self.safe_string(message, 'action') == 'snapshot' + if not isSnapshot and checksum: + storedAsks = storedOrderBook['asks'] + storedBids = storedOrderBook['bids'] + asksLength = len(storedAsks) + bidsLength = len(storedBids) + payloadArray = [] + for i in range(0, 25): + if i < bidsLength: + payloadArray.append(storedBids[i][2][0]) + payloadArray.append(storedBids[i][2][1]) + if i < asksLength: + payloadArray.append(storedAsks[i][2][0]) + payloadArray.append(storedAsks[i][2][1]) + payload = ':'.join(payloadArray) + calculatedChecksum = self.crc32(payload, True) + responseChecksum = self.safe_integer(rawOrderBook, 'checksum') + if calculatedChecksum != responseChecksum: + self.spawn(self.handle_check_sum_error, client, symbol, messageHash) + return + else: + orderbook = self.order_book({}) + parsedOrderbook = self.parse_order_book(rawOrderBook, symbol, timestamp) + orderbook.reset(parsedOrderbook) + self.orderbooks[symbol] = orderbook + client.resolve(self.orderbooks[symbol], messageHash) + + async def handle_check_sum_error(self, client: Client, symbol: str, messageHash: str): + await self.un_watch_order_book(symbol) + error = ChecksumError(self.id + ' ' + self.orderbook_checksum_message(symbol)) + client.reject(error, messageHash) + + def handle_delta(self, bookside, delta): + bidAsk = self.parse_bid_ask(delta, 0, 1) + bidAsk.append(delta) + bookside.storeArray(bidAsk) + + def handle_deltas(self, bookside, deltas): + for i in range(0, len(deltas)): + self.handle_delta(bookside, deltas[i]) + + async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://coincatch.github.io/github.io/en/spot/#trades-channel + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + return await self.watch_trades_for_symbols([symbol], since, limit, params) + + async def watch_trades_for_symbols(self, symbols: List[str], since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + watches information on multiple trades made in a market + + https://coincatch.github.io/github.io/en/spot/#trades-channel + + @param symbols + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of trade structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + symbolsLength = len(symbols) + if symbolsLength == 0: + raise ArgumentsRequired(self.id + ' watchTradesForSymbols() requires a non-empty array of symbols') + await self.load_markets() + symbols = self.market_symbols(symbols) + topics = [] + messageHashes = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + market = self.market(symbol) + instType, instId = self.get_public_inst_type_and_id(market) + args: dict = { + 'instType': instType, + 'channel': 'trade', + 'instId': instId, + } + topics.append(args) + messageHashes.append('trade:' + symbol) + trades = await self.watch_public_multiple(messageHashes, messageHashes, topics, params) + if self.newUpdates: + first = self.safe_dict(trades, 0) + tradeSymbol = self.safe_string(first, 'symbol') + limit = trades.getLimit(tradeSymbol, limit) + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + async def un_watch_trades(self, symbol: str, params={}) -> Any: + """ + unsubscribe from the trades channel + + https://coincatch.github.io/github.io/en/spot/#trades-channel + + :param str symbol: unified symbol of the market to unwatch the trades for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns any: status of the unwatch request + """ + await self.load_markets() + return await self.un_watch_channel(symbol, 'trade', 'trade', params) + + def handle_trades(self, client: Client, message): + # + # { + # action: 'update', + # arg: {instType: 'sp', channel: 'trade', instId: 'ETHUSDT'}, + # data: [['1728341807469', '2421.41', '0.478', 'sell']], + # ts: 1728341807482 + # } + # + arg = self.safe_dict(message, 'arg', {}) + market = self.get_market_from_arg(arg) + marketId = market['id'] + hash = 'trade:' + if marketId.find('_DMCBL') >= 0: + market = self.handle_dmcbl_market_by_message_hashes(market, hash, client) + symbol = market['symbol'] + if not (symbol in self.trades): + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + self.trades[symbol] = ArrayCache(limit) + stored = self.trades[symbol] + data = self.safe_list(message, 'data', []) + if data is not None: + data = self.sort_by(data, 0) + for i in range(0, len(data)): + trade = self.safe_list(data, i) + parsed = self.parse_ws_trade(trade, market) + stored.append(parsed) + messageHash = hash + symbol + client.resolve(stored, messageHash) + + def parse_ws_trade(self, trade, market=None) -> Trade: + # + # [ + # '1728341807469', + # '2421.41', + # '0.478', + # 'sell' + # ] + # + timestamp = self.safe_integer(trade, 0) + return self.safe_trade({ + 'id': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'side': self.safe_string_lower(trade, 3), + 'price': self.safe_string(trade, 1), + 'amount': self.safe_string(trade, 2), + 'cost': None, + 'takerOrMaker': None, + 'type': None, + 'order': None, + 'fee': None, + 'info': trade, + }, market) + + async def watch_balance(self, params={}) -> Balances: + """ + watch balance and get the amount of funds available for trading or funds locked in orders + + https://coincatch.github.io/github.io/en/spot/#account-channel + https://coincatch.github.io/github.io/en/mix/#account-channel + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: 'spot' or 'swap'(default is 'spot') + :param str [params.instType]: *swap only* 'umcbl' or 'dmcbl'(default is 'umcbl') + :returns dict: a `balance structure ` + """ + type = None + type, params = self.handle_market_type_and_params('watchBalance', None, params) + instType = 'spbl' # must be lower case for spot + if type == 'swap': + instType = 'umcbl' + channel = 'account' + instType, params = self.handle_option_and_params(params, 'watchBalance', 'instType', instType) + args: dict = { + 'instType': instType, + 'channel': channel, + 'instId': 'default', + } + messageHash = 'balance:' + instType.lower() + return await self.watch_private(messageHash, messageHash, args, params) + + def handle_balance(self, client: Client, message): + # + # spot + # { + # action: 'snapshot', + # arg: {instType: 'spbl', channel: 'account', instId: 'default'}, + # data: [ + # { + # coinId: '3', + # coinName: 'ETH', + # available: '0.0000832', + # frozen: '0', + # lock: '0' + # } + # ], + # ts: 1728464548725 + # } + # + # # swap + # { + # action: 'snapshot', + # arg: {instType: 'dmcbl', channel: 'account', instId: 'default'}, + # data: [ + # { + # marginCoin: 'ETH', + # locked: '0.00000000', + # available: '0.00001203', + # maxOpenPosAvailable: '0.00001203', + # maxTransferOut: '0.00001203', + # equity: '0.00001203', + # usdtEquity: '0.029092328738', + # coinDisplayName: 'ETH' + # } + # ], + # ts: 1728650777643 + # } + # + data = self.safe_list(message, 'data', []) + for i in range(0, len(data)): + rawBalance = data[i] + currencyId = self.safe_string_2(rawBalance, 'coinName', 'marginCoin') + code = self.safe_currency_code(currencyId) + account = self.balance[code] if (code in self.balance) else self.account() + freeQuery = 'maxTransferOut' if ('maxTransferOut' in rawBalance) else 'available' + account['free'] = self.safe_string(rawBalance, freeQuery) + account['total'] = self.safe_string(rawBalance, 'equity') + account['used'] = self.safe_string(rawBalance, 'frozen') + self.balance[code] = account + self.balance = self.safe_balance(self.balance) + arg = self.safe_dict(message, 'arg') + instType = self.safe_string_lower(arg, 'instType') + messageHash = 'balance:' + instType + client.resolve(self.balance, messageHash) + + async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + watches information on multiple orders made by the user + + https://coincatch.github.io/github.io/en/spot/#order-channel + https://coincatch.github.io/github.io/en/mix/#order-channel + https://coincatch.github.io/github.io/en/mix/#plan-order-channel + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: 'spot' or 'swap' + :param str [params.instType]: *swap only* 'umcbl' or 'dmcbl'(default is 'umcbl') + :param bool [params.trigger]: *swap only* whether to watch trigger orders(default is False) + :returns dict[]: a list of `order structures ` + """ + methodName = 'watchOrders' + await self.load_markets() + market = None + marketId = None + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + marketId = market['id'] + marketType = None + marketType, params = self.handle_market_type_and_params(methodName, market, params) + instType = 'spbl' + instId = marketId + if marketType == 'spot': + if symbol is None: + raise ArgumentsRequired(self.id + ' ' + methodName + '() requires a symbol argument for ' + marketType + ' markets.') + else: + instId = 'default' + instType = 'umcbl' + if symbol is None: + instType, params = self.handle_option_and_params(params, methodName, 'instType', instType) + else: + if marketId.find('_DMCBL') >= 0: + instType = 'dmcbl' + channel = 'orders' + isTrigger = self.safe_bool(params, 'trigger') + if isTrigger: + channel = 'ordersAlgo' # channel does not return any data + params = self.omit(params, 'trigger') + args: dict = { + 'instType': instType, + 'channel': channel, + 'instId': instId, + } + messageHash = 'orders' + if symbol is not None: + messageHash += ':' + symbol + orders = await self.watch_private(messageHash, messageHash, args, params) + if self.newUpdates: + limit = orders.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(orders, symbol, since, limit, True) + + def handle_order(self, client: Client, message): + # + # spot + # + # { + # action: 'snapshot', + # arg: {instType: 'spbl', channel: 'orders', instId: 'ETHUSDT_SPBL'}, + # data: [ + # { + # instId: 'ETHUSDT_SPBL', + # ordId: '1228627925964996608', + # clOrdId: 'f0cccf74-c535-4523-a53d-dbe3b9958559', + # px: '2000', + # sz: '0.001', + # notional: '2', + # ordType: 'limit', + # force: 'normal', + # side: 'buy', + # accFillSz: '0', + # avgPx: '0', + # status: 'new', + # cTime: 1728653645030, + # uTime: 1728653645030, + # orderFee: [], + # eps: 'API' + # } + # ], + # ts: 1728653645046 + # } + # + # swap + # + # { + # action: 'snapshot', + # arg: {instType: 'umcbl', channel: 'orders', instId: 'default'}, + # data: [ + # { + # accFillSz: '0', + # cTime: 1728653796976, + # clOrdId: '1228628563272753152', + # eps: 'API', + # force: 'normal', + # hM: 'single_hold', + # instId: 'ETHUSDT_UMCBL', + # lever: '5', + # low: False, + # notionalUsd: '20', + # ordId: '1228628563188867072', + # ordType: 'limit', + # orderFee: [], + # posSide: 'net', + # px: '2000', + # side: 'buy', + # status: 'new', + # sz: '0.01', + # tS: 'buy_single', + # tdMode: 'cross', + # tgtCcy: 'USDT', + # uTime: 1728653796976 + # } + # ], + # ts: 1728653797002 + # } + # + # + arg = self.safe_dict(message, 'arg', {}) + instType = self.safe_string(arg, 'instType') + argInstId = self.safe_string(arg, 'instId') + marketType = None + if instType == 'spbl': + marketType = 'spot' + else: + marketType = 'swap' + data = self.safe_list(message, 'data', []) + if self.orders is None: + limit = self.safe_integer(self.options, 'ordersLimit', 1000) + self.orders = ArrayCacheBySymbolById(limit) + hash = 'orders' + stored = self.orders + symbol: Str = None + for i in range(0, len(data)): + order = data[i] + marketId = self.safe_string(order, 'instId', argInstId) + market = self.safe_market(marketId, None, None, marketType) + parsed = self.parse_ws_order(order, market) + stored.append(parsed) + symbol = parsed['symbol'] + messageHash = 'orders:' + symbol + client.resolve(stored, messageHash) + client.resolve(stored, hash) + + def parse_ws_order(self, order: dict, market: Market = None) -> Order: + # + # spot + # { + # instId: 'ETHUSDT_SPBL', + # ordId: '1228627925964996608', + # clOrdId: 'f0cccf74-c535-4523-a53d-dbe3b9958559', + # px: '2000', + # sz: '0.001', + # notional: '2', + # ordType: 'limit', + # force: 'normal', + # side: 'buy', + # accFillSz: '0', + # avgPx: '0', + # status: 'new', + # cTime: 1728653645030, + # uTime: 1728653645030, + # orderFee: orderFee: [{fee: '0', feeCcy: 'USDT'}], + # eps: 'API' + # } + # + # swap + # { + # accFillSz: '0', + # cTime: 1728653796976, + # clOrdId: '1228628563272753152', + # eps: 'API', + # force: 'normal', + # hM: 'single_hold', + # instId: 'ETHUSDT_UMCBL', + # lever: '5', + # low: False, + # notionalUsd: '20', + # ordId: '1228628563188867072', + # ordType: 'limit', + # orderFee: [{fee: '0', feeCcy: 'USDT'}], + # posSide: 'net', + # px: '2000', + # side: 'buy', + # status: 'new', + # sz: '0.01', + # tS: 'buy_single', + # tdMode: 'cross', + # tgtCcy: 'USDT', + # uTime: 1728653796976 + # } + # + marketId = self.safe_string(order, 'instId') + settleId = self.safe_string(order, 'tgtCcy') + market = self.safeMarketCustom(marketId, market, settleId) + timestamp = self.safe_integer(order, 'cTime') + symbol = market['symbol'] + rawStatus = self.safe_string(order, 'status') + orderFee = self.safe_list(order, 'orderFee', []) + fee = self.safe_dict(orderFee, 0) + feeCost = Precise.string_mul(self.safe_string(fee, 'fee'), '-1') + feeCurrency = self.safe_string(fee, 'feeCcy') + price = self.omit_zero(self.safe_string(order, 'px')) + priceAvg = self.omit_zero(self.safe_string(order, 'avgPx')) + if price is None: + price = priceAvg + type = self.safe_string_lower(order, 'ordType') + return self.safe_order({ + 'id': self.safe_string(order, 'ordId'), + 'clientOrderId': self.safe_string(order, 'clOrdId'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'lastUpdateTimestamp': self.safe_integer(order, 'uTime'), + 'status': self.parse_order_status(rawStatus), + 'symbol': symbol, + 'type': type, + 'timeInForce': self.parseOrderTimeInForce(self.safe_string_lower(order, 'force')), + 'side': self.safe_string_lower(order, 'side'), + 'price': price, + 'average': self.safe_string(order, 'avgPx'), + 'amount': self.safe_string(order, 'sz'), + 'filled': self.safe_string(order, 'accFillSz'), + 'remaining': None, + 'triggerPrice': None, + 'takeProfitPrice': None, + 'stopLossPrice': None, + 'cost': self.safe_string(order, 'notional'), + 'trades': None, + 'fee': { + 'currency': feeCurrency, + 'cost': feeCost, + }, + 'reduceOnly': self.safe_bool(order, 'low'), + 'postOnly': None, + 'info': order, + }, market) + + async def watch_positions(self, symbols: Strings = None, since: Int = None, limit: Int = None, params={}) -> List[Position]: + """ + watch all open positions + + https://coincatch.github.io/github.io/en/mix/#positions-channel + + :param str[]|None symbols: list of unified market symbols + @param since + @param limit + :param dict params: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, 'swap') + messageHashes = [] + hash = 'positions' + instTypes = [] + if symbols is not None: + for i in range(0, len(symbols)): + symbol = symbols[i] + market = self.market(symbol) + instType = self.get_private_inst_type(market) + if not self.in_array(instType, instTypes): + instTypes.append(instType) + messageHashes.append(hash + '::' + symbol) + else: + instTypes = ['umcbl', 'dmcbl'] + messageHashes.append(hash) + args = [] + subscribeHashes = [] + for i in range(0, len(instTypes)): + instType = instTypes[i] + arg: dict = { + 'instType': instType, + 'channel': hash, + 'instId': 'default', + } + subscribeHashes.append(hash + '::' + instType) + args.append(arg) + newPositions = await self.watch_private_multiple(messageHashes, subscribeHashes, args, params) + if self.newUpdates: + return newPositions + return self.filter_by_symbols_since_limit(newPositions, symbols, since, limit, True) + + def get_private_inst_type(self, market: Market): + marketId = market['id'] + if marketId.find('_DMCBL') >= 0: + return 'dmcbl' + return 'umcbl' + + def handle_positions(self, client: Client, message): + # + # { + # action: 'snapshot', + # arg: {instType: 'umcbl', channel: 'positions', instId: 'default'}, + # data: [ + # { + # posId: '1221355728745619456', + # instId: 'ETHUSDT_UMCBL', + # instName: 'ETHUSDT', + # marginCoin: 'USDT', + # margin: '5.27182', + # marginMode: 'crossed', + # holdSide: 'long', + # holdMode: 'single_hold', + # total: '0.01', + # available: '0.01', + # locked: '0', + # averageOpenPrice: '2635.91', + # leverage: 5, + # achievedProfits: '0', + # upl: '-0.0267', + # uplRate: '-0.005064664576', + # liqPx: '-3110.66866033', + # keepMarginRate: '0.0033', + # marginRate: '0.002460827254', + # cTime: '1726919818102', + # uTime: '1728919604312', + # markPrice: '2633.24', + # autoMargin: 'off' + # } + # ], + # ts: 1728919604329 + # } + # + if self.positions is None: + self.positions = ArrayCacheBySymbolBySide() + cache = self.positions + rawPositions = self.safe_value(message, 'data', []) + dataLength = len(rawPositions) + if dataLength == 0: + return + newPositions = [] + symbols = [] + for i in range(0, len(rawPositions)): + rawPosition = rawPositions[i] + position = self.parse_ws_position(rawPosition) + symbols.append(position['symbol']) + newPositions.append(position) + cache.append(position) + hash = 'positions' + messageHashes = self.find_message_hashes(client, hash) + for i in range(0, len(messageHashes)): + messageHash = messageHashes[i] + parts = messageHash.split('::') + symbol = parts[1] + if self.in_array(symbol, symbols): + positionsForSymbol = [] + for j in range(0, len(newPositions)): + position = newPositions[j] + if position['symbol'] == symbol: + positionsForSymbol.append(position) + client.resolve(positionsForSymbol, messageHash) + client.resolve(newPositions, hash) + + def parse_ws_position(self, position, market=None): + # + # { + # posId: '1221355728745619456', + # instId: 'ETHUSDT_UMCBL', + # instName: 'ETHUSDT', + # marginCoin: 'USDT', + # margin: '5.27182', + # marginMode: 'crossed', + # holdSide: 'long', + # holdMode: 'single_hold', + # total: '0.01', + # available: '0.01', + # locked: '0', + # averageOpenPrice: '2635.91', + # leverage: 5, + # achievedProfits: '0', + # upl: '-0.0267', + # uplRate: '-0.005064664576', + # liqPx: '-3110.66866033', + # keepMarginRate: '0.0033', + # marginRate: '0.002460827254', + # cTime: '1726919818102', + # uTime: '1728919604312', + # markPrice: '2633.24', + # autoMargin: 'off' + # } + # + marketId = self.safe_string(position, 'symbol') + settleId = self.safe_string(position, 'marginCoin') + market = self.safeMarketCustom(marketId, market, settleId) + timestamp = self.safe_integer(position, 'cTime') + marginModeId = self.safe_string(position, 'marginMode') + marginMode = self.get_supported_mapping(marginModeId, { + 'crossed': 'cross', + 'isolated': 'isolated', + }) + isHedged: Bool = None + holdMode = self.safe_string(position, 'holdMode') + if holdMode == 'double_hold': + isHedged = True + elif holdMode == 'single_hold': + isHedged = False + percentageDecimal = self.safe_string(position, 'uplRate') + percentage = Precise.string_mul(percentageDecimal, '100') + margin = self.safe_number(position, 'margin') + return self.safe_position({ + 'symbol': market['symbol'], + 'id': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'contracts': self.safe_number(position, 'total'), + 'contractSize': None, + 'side': self.safe_string_lower(position, 'holdSide'), + 'notional': margin, # todo check + 'leverage': self.safe_integer(position, 'leverage'), + 'unrealizedPnl': self.safe_number(position, 'upl'), + 'realizedPnl': self.safe_number(position, 'achievedProfits'), + 'collateral': None, # todo check + 'entryPrice': self.safe_number(position, 'averageOpenPrice'), + 'markPrice': self.safe_number(position, 'markPrice'), + 'liquidationPrice': self.safe_number(position, 'liqPx'), + 'marginMode': marginMode, + 'hedged': isHedged, + 'maintenanceMargin': None, # todo check + 'maintenanceMarginPercentage': self.safe_number(position, 'keepMarginRate'), + 'initialMargin': margin, # todo check + 'initialMarginPercentage': None, + 'marginRatio': self.safe_number(position, 'marginRate'), + 'lastUpdateTimestamp': self.safe_integer(position, 'uTime'), + 'lastPrice': None, + 'stopLossPrice': None, + 'takeProfitPrice': None, + 'percentage': percentage, + 'info': position, + }) + + def handle_error_message(self, client: Client, message) -> Bool: + # + # {event: "error", code: 30001, msg: "Channel does not exist"} + # + event = self.safe_string(message, 'event') + try: + if event == 'error': + code = self.safe_string(message, 'code') + feedback = self.id + ' ' + self.json(message) + self.throw_exactly_matched_exception(self.exceptions['ws']['exact'], code, feedback) + msg = self.safe_string(message, 'msg', '') + self.throw_broadly_matched_exception(self.exceptions['ws']['broad'], msg, feedback) + raise ExchangeError(feedback) + return False + except Exception as e: + if isinstance(e, AuthenticationError): + messageHash = 'authenticated' + client.reject(e, messageHash) + if messageHash in client.subscriptions: + del client.subscriptions[messageHash] + else: + client.reject(e) + return True + + def handle_message(self, client: Client, message): + # todo handle with subscribe and unsubscribe + if self.handle_error_message(client, message): + return + content = self.safe_string(message, 'message') + if content == 'pong': + self.handle_pong(client, message) + return + if message == 'pong': + self.handle_pong(client, message) + return + event = self.safe_string(message, 'event') + if event == 'login': + self.handle_authenticate(client, message) + return + if event == 'subscribe': + self.handle_subscription_status(client, message) + return + if event == 'unsubscribe': + self.handle_un_subscription_status(client, message) + return + data = self.safe_dict(message, 'arg', {}) + channel = self.safe_string(data, 'channel') + if channel == 'ticker': + self.handle_ticker(client, message) + if channel.find('candle') >= 0: + self.handle_ohlcv(client, message) + if channel.find('books') >= 0: + self.handle_order_book(client, message) + if channel == 'trade': + self.handle_trades(client, message) + if channel == 'account': + self.handle_balance(client, message) + if (channel == 'orders') or (channel == 'ordersAlgo'): + self.handle_order(client, message) + if channel == 'positions': + self.handle_positions(client, message) + + def ping(self, client: Client): + return 'ping' + + def handle_pong(self, client: Client, message): + client.lastPong = self.milliseconds() + return message + + def handle_subscription_status(self, client: Client, message): + return message + + def handle_un_subscription_status(self, client: Client, message): + argsList = self.safe_list(message, 'args') + if argsList is None: + argsList = [self.safe_dict(message, 'arg', {})] + for i in range(0, len(argsList)): + arg = argsList[i] + channel = self.safe_string(arg, 'channel') + if channel == 'books': + self.handle_order_book_un_subscription(client, message) + elif channel == 'trade': + self.handle_trades_un_subscription(client, message) + elif channel == 'ticker': + self.handle_ticker_un_subscription(client, message) + elif channel.startswith('candle'): + self.handle_ohlcv_un_subscription(client, message) + return message + + def handle_order_book_un_subscription(self, client: Client, message): + arg = self.safe_dict(message, 'arg', {}) + instType = self.safe_string_lower(arg, 'instType') + type = 'spot' if (instType == 'sp') else 'swap' + instId = self.safe_string(arg, 'instId') + market = self.safe_market(instId, None, None, type) + symbol = market['symbol'] + messageHash = 'unsubscribe:orderbook:' + market['symbol'] + subMessageHash = 'orderbook:' + symbol + if symbol in self.orderbooks: + del self.orderbooks[symbol] + if subMessageHash in client.subscriptions: + del client.subscriptions[subMessageHash] + if messageHash in client.subscriptions: + del client.subscriptions[messageHash] + error = UnsubscribeError(self.id + ' orderbook ' + symbol) + client.reject(error, subMessageHash) + client.resolve(True, messageHash) + + def handle_trades_un_subscription(self, client: Client, message): + arg = self.safe_dict(message, 'arg', {}) + instType = self.safe_string_lower(arg, 'instType') + type = 'spot' if (instType == 'sp') else 'swap' + instId = self.safe_string(arg, 'instId') + market = self.safe_market(instId, None, None, type) + symbol = market['symbol'] + messageHash = 'unsubscribe:trade:' + market['symbol'] + subMessageHash = 'trade:' + symbol + if symbol in self.trades: + del self.trades[symbol] + if subMessageHash in client.subscriptions: + del client.subscriptions[subMessageHash] + if messageHash in client.subscriptions: + del client.subscriptions[messageHash] + error = UnsubscribeError(self.id + ' trades ' + symbol) + client.reject(error, subMessageHash) + client.resolve(True, messageHash) + + def handle_ticker_un_subscription(self, client: Client, message): + arg = self.safe_dict(message, 'arg', {}) + instType = self.safe_string_lower(arg, 'instType') + type = 'spot' if (instType == 'sp') else 'swap' + instId = self.safe_string(arg, 'instId') + market = self.safe_market(instId, None, None, type) + symbol = market['symbol'] + messageHash = 'unsubscribe:ticker:' + market['symbol'] + subMessageHash = 'ticker:' + symbol + if symbol in self.tickers: + del self.tickers[symbol] + if subMessageHash in client.subscriptions: + del client.subscriptions[subMessageHash] + if messageHash in client.subscriptions: + del client.subscriptions[messageHash] + error = UnsubscribeError(self.id + ' ticker ' + symbol) + client.reject(error, subMessageHash) + client.resolve(True, messageHash) + + def handle_ohlcv_un_subscription(self, client: Client, message): + arg = self.safe_dict(message, 'arg', {}) + instType = self.safe_string_lower(arg, 'instType') + type = 'spot' if (instType == 'sp') else 'swap' + instId = self.safe_string(arg, 'instId') + channel = self.safe_string(arg, 'channel') + interval = channel.replace('candle', '') + timeframes = self.safe_dict(self.options, 'timeframesForWs') + timeframe = self.find_timeframe(interval, timeframes) + market = self.safe_market(instId, None, None, type) + symbol = market['symbol'] + messageHash = 'unsubscribe:ohlcv:' + timeframe + ':' + market['symbol'] + subMessageHash = 'ohlcv:' + symbol + ':' + timeframe + if symbol in self.ohlcvs: + if timeframe in self.ohlcvs[symbol]: + del self.ohlcvs[symbol][timeframe] + self.clean_unsubscription(client, subMessageHash, messageHash) diff --git a/ccxt/pro/coincheck.py b/ccxt/pro/coincheck.py new file mode 100644 index 0000000..a97a3d4 --- /dev/null +++ b/ccxt/pro/coincheck.py @@ -0,0 +1,204 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCache +from ccxt.base.types import Any, Int, Market, OrderBook, Trade +from ccxt.async_support.base.ws.client import Client +from typing import List +from ccxt.base.errors import AuthenticationError + + +class coincheck(ccxt.async_support.coincheck): + + def describe(self) -> Any: + return self.deep_extend(super(coincheck, self).describe(), { + 'has': { + 'ws': True, + 'watchOrderBook': True, + 'watchOrders': False, + 'watchTrades': True, + 'watchTradesForSymbols': False, + 'watchOHLCV': False, + 'watchTicker': False, + 'watchTickers': False, + }, + 'urls': { + 'api': { + 'ws': 'wss://ws-api.coincheck.com/', + }, + }, + 'options': { + 'expiresIn': '', + 'userId': '', + 'wsSessionToken': '', + 'watchOrderBook': { + 'snapshotDelay': 6, + 'snapshotMaxRetries': 3, + }, + 'tradesLimit': 1000, + 'OHLCVLimit': 1000, + }, + 'exceptions': { + 'exact': { + '4009': AuthenticationError, + }, + }, + }) + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://coincheck.com/documents/exchange/api#websocket-order-book + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + messageHash = 'orderbook:' + market['symbol'] + url = self.urls['api']['ws'] + request: dict = { + 'type': 'subscribe', + 'channel': market['id'] + '-orderbook', + } + message = self.extend(request, params) + orderbook = await self.watch(url, messageHash, message, messageHash) + return orderbook.limit() + + def handle_order_book(self, client, message): + # + # [ + # "btc_jpy", + # { + # "bids": [ + # [ + # "6288279.0", + # "0" + # ] + # ], + # "asks": [ + # [ + # "6290314.0", + # "0" + # ] + # ], + # "last_update_at": "1705396097" + # } + # ] + # + symbol = self.symbol(self.safe_string(message, 0)) + data = self.safe_value(message, 1, {}) + timestamp = self.safe_timestamp(data, 'last_update_at') + snapshot = self.parse_order_book(data, symbol, timestamp) + orderbook = self.safe_value(self.orderbooks, symbol) + if orderbook is None: + orderbook = self.order_book(snapshot) + self.orderbooks[symbol] = orderbook + else: + orderbook = self.orderbooks[symbol] + orderbook.reset(snapshot) + messageHash = 'orderbook:' + symbol + client.resolve(orderbook, messageHash) + + async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + watches information on multiple trades made in a market + + https://coincheck.com/documents/exchange/api#websocket-trades + + :param str symbol: unified market symbol of the market trades were made in + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trade structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + messageHash = 'trade:' + market['symbol'] + url = self.urls['api']['ws'] + request: dict = { + 'type': 'subscribe', + 'channel': market['id'] + '-trades', + } + message = self.extend(request, params) + trades = await self.watch(url, messageHash, message, messageHash) + if self.newUpdates: + limit = trades.getLimit(symbol, limit) + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + def handle_trades(self, client: Client, message): + # + # [ + # [ + # "1663318663", # transaction timestamp(unix time) + # "2357062", # transaction ID + # "btc_jpy", # pair + # "2820896.0", # transaction rate + # "5.0", # transaction amount + # "sell", # order side + # "1193401", # ID of the Taker + # "2078767" # ID of the Maker + # ] + # ] + # + first = self.safe_value(message, 0, []) + symbol = self.symbol(self.safe_string(first, 2)) + stored = self.safe_value(self.trades, symbol) + if stored is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + stored = ArrayCache(limit) + self.trades[symbol] = stored + for i in range(0, len(message)): + data = self.safe_value(message, i) + trade = self.parse_ws_trade(data) + stored.append(trade) + messageHash = 'trade:' + symbol + client.resolve(stored, messageHash) + + def parse_ws_trade(self, trade: dict, market: Market = None) -> Trade: + # + # [ + # "1663318663", # transaction timestamp(unix time) + # "2357062", # transaction ID + # "btc_jpy", # pair + # "2820896.0", # transaction rate + # "5.0", # transaction amount + # "sell", # order side + # "1193401", # ID of the Taker + # "2078767" # ID of the Maker + # ] + # + symbol = self.symbol(self.safe_string(trade, 2)) + timestamp = self.safe_timestamp(trade, 0) + side = self.safe_string(trade, 5) + priceString = self.safe_string(trade, 3) + amountString = self.safe_string(trade, 4) + return self.safe_trade({ + 'id': self.safe_string(trade, 1), + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'order': None, + 'symbol': symbol, + 'type': None, + 'side': side, + 'takerOrMaker': None, + 'price': priceString, + 'amount': amountString, + 'cost': None, + 'fee': None, + }, market) + + def handle_message(self, client: Client, message): + data = self.safe_value(message, 0) + if not isinstance(data, list): + self.handle_order_book(client, message) + else: + self.handle_trades(client, message) diff --git a/ccxt/pro/coinex.py b/ccxt/pro/coinex.py new file mode 100644 index 0000000..ad57ecd --- /dev/null +++ b/ccxt/pro/coinex.py @@ -0,0 +1,1365 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolById +import hashlib +from ccxt.base.types import Any, Balances, Int, Order, OrderBook, Str, Strings, Ticker, Tickers, Trade +from ccxt.async_support.base.ws.client import Client +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import NotSupported +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.errors import RequestTimeout + + +class coinex(ccxt.async_support.coinex): + + def describe(self) -> Any: + return self.deep_extend(super(coinex, self).describe(), { + 'has': { + 'ws': True, + 'watchBalance': True, + 'watchBidsAsks': True, + 'watchTicker': True, + 'watchTickers': True, + 'watchTrades': True, + 'watchTradesForSymbols': True, + 'watchMyTrades': True, + 'watchOrders': True, + 'watchOrderBook': True, + 'watchOrderBookForSymbols': True, + 'watchOHLCV': False, + 'fetchOHLCVWs': False, + }, + 'urls': { + 'api': { + 'ws': { + 'spot': 'wss://socket.coinex.com/v2/spot/', + 'swap': 'wss://socket.coinex.com/v2/futures/', + }, + }, + }, + 'options': { + 'ws': { + 'gunzip': True, + }, + 'timeframes': { + '1m': 60, + '3m': 180, + '5m': 300, + '15m': 900, + '30m': 1800, + '1h': 3600, + '2h': 7200, + '4h': 14400, + '6h': 21600, + '12h': 43200, + '1d': 86400, + '3d': 259200, + '1w': 604800, + }, + 'account': 'spot', + 'watchOrderBook': { + 'limits': [5, 10, 20, 50], + 'defaultLimit': 50, + 'aggregations': ['1000', '100', '10', '1', '0', '0.1', '0.01', '0.001', '0.0001', '0.00001', '0.000001', '0.0000001', '0.00000001', '0.000000001', '0.0000000001', '0.00000000001'], + 'defaultAggregation': '0', + }, + }, + 'streaming': { + }, + 'exceptions': { + 'exact': { + '20001': BadRequest, # Invalid argument + '20002': NotSupported, # Method unavailable + '21001': AuthenticationError, # Authentication required + '21002': AuthenticationError, # Incorrect signature + '23001': RequestTimeout, # Request service timeout + '23002': RateLimitExceeded, # Requests too frequently + '24001': ExchangeError, # Internal error + '24002': ExchangeNotAvailable, # Service unavailable temporarily + '30001': BadRequest, # Invalid argument + '30002': NotSupported, # Method unavailable + '31001': AuthenticationError, # Authentication required + '31002': AuthenticationError, # Incorrect signature + '33001': RequestTimeout, # Request service timeout + '33002': RateLimitExceeded, # Requests too frequently + '34001': ExchangeError, # Internal error + '34002': ExchangeNotAvailable, # Service unavailable temporarily + }, + 'broad': {}, + }, + }) + + def request_id(self): + requestId = self.sum(self.safe_integer(self.options, 'requestId', 0), 1) + self.options['requestId'] = requestId + return requestId + + def handle_ticker(self, client: Client, message): + # + # spot + # + # { + # "method": "state.update", + # "data": { + # "state_list": [ + # { + # "market": "LATUSDT", + # "last": "0.008157", + # "open": "0.008286", + # "close": "0.008157", + # "high": "0.008390", + # "low": "0.008106", + # "volume": "807714.49139758", + # "volume_sell": "286170.69645599", + # "volume_buy": "266161.23236408", + # "value": "6689.21644207", + # "period": 86400 + # }, + # ] + # }, + # "id": null + # } + # + # swap + # + # { + # "method": "state.update", + # "data": { + # "state_list": [ + # { + # "market": "ETHUSD_SIGNPRICE", + # "last": "1892.29", + # "open": "1884.62", + # "close": "1892.29", + # "high": "1894.09", + # "low": "1863.72", + # "volume": "0", + # "value": "0", + # "volume_sell": "0", + # "volume_buy": "0", + # "open_interest_size": "0", + # "insurance_fund_size": "0", + # "latest_funding_rate": "0", + # "next_funding_rate": "0", + # "latest_funding_time": 0, + # "next_funding_time": 0, + # "period": 86400 + # }, + # ] + # ], + # "id": null + # } + # + defaultType = self.safe_string(self.options, 'defaultType') + data = self.safe_dict(message, 'data', {}) + rawTickers = self.safe_list(data, 'state_list', []) + newTickers = {} + for i in range(0, len(rawTickers)): + entry = rawTickers[i] + marketId = self.safe_string(entry, 'market') + symbol = self.safe_symbol(marketId, None, None, defaultType) + market = self.safe_market(marketId, None, None, defaultType) + parsedTicker = self.parse_ws_ticker(entry, market) + self.tickers[symbol] = parsedTicker + newTickers[symbol] = parsedTicker + messageHashes = self.find_message_hashes(client, 'tickers::') + for i in range(0, len(messageHashes)): + messageHash = messageHashes[i] + parts = messageHash.split('::') + symbolsString = parts[1] + symbols = symbolsString.split(',') + tickers = self.filter_by_array(newTickers, 'symbol', symbols) + tickersSymbols = list(tickers.keys()) + numTickers = len(tickersSymbols) + if numTickers > 0: + client.resolve(tickers, messageHash) + client.resolve(newTickers, 'tickers') + + def parse_ws_ticker(self, ticker, market=None): + # + # spot + # + # { + # "market": "LATUSDT", + # "last": "0.008157", + # "open": "0.008286", + # "close": "0.008157", + # "high": "0.008390", + # "low": "0.008106", + # "volume": "807714.49139758", + # "volume_sell": "286170.69645599", + # "volume_buy": "266161.23236408", + # "value": "6689.21644207", + # "period": 86400 + # } + # + # swap + # + # { + # "market": "ETHUSD_SIGNPRICE", + # "last": "1892.29", + # "open": "1884.62", + # "close": "1892.29", + # "high": "1894.09", + # "low": "1863.72", + # "volume": "0", + # "value": "0", + # "volume_sell": "0", + # "volume_buy": "0", + # "open_interest_size": "0", + # "insurance_fund_size": "0", + # "latest_funding_rate": "0", + # "next_funding_rate": "0", + # "latest_funding_time": 0, + # "next_funding_time": 0, + # "period": 86400 + # } + # + defaultType = self.safe_string(self.options, 'defaultType') + marketId = self.safe_string(ticker, 'market') + return self.safe_ticker({ + 'symbol': self.safe_symbol(marketId, market, None, defaultType), + 'timestamp': None, + 'datetime': None, + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': None, + 'bidVolume': self.safe_string(ticker, 'volume_buy'), + 'ask': None, + 'askVolume': self.safe_string(ticker, 'volume_sell'), + 'vwap': None, + 'open': self.safe_string(ticker, 'open'), + 'close': self.safe_string(ticker, 'close'), + 'last': self.safe_string(ticker, 'last'), + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': self.safe_string(ticker, 'volume'), + 'quoteVolume': self.safe_string(ticker, 'value'), + 'info': ticker, + }, market) + + async def watch_balance(self, params={}) -> Balances: + """ + watch balance and get the amount of funds available for trading or funds locked in orders + + https://docs.coinex.com/api/v2/assets/balance/ws/spot_balance + https://docs.coinex.com/api/v2/assets/balance/ws/futures_balance + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + type = None + type, params = self.handle_market_type_and_params('watchBalance', None, params, 'spot') + await self.authenticate(type) + url = self.urls['api']['ws'][type] + # coinex throws a closes the websocket when subscribing over 1422 currencies, therefore we filter out inactive currencies + activeCurrencies = self.filter_by(self.currencies_by_id, 'active', True) + activeCurrenciesById = self.index_by(activeCurrencies, 'id') + currencies = list(activeCurrenciesById.keys()) + if currencies is None: + currencies = [] + messageHash = 'balances' + if type == 'spot': + messageHash += ':spot' + else: + messageHash += ':swap' + subscribe: dict = { + 'method': 'balance.subscribe', + 'params': {'ccy_list': currencies}, + 'id': self.request_id(), + } + request = self.deep_extend(subscribe, params) + return await self.watch(url, messageHash, request, messageHash) + + def handle_balance(self, client: Client, message): + # + # spot + # + # { + # "method": "balance.update", + # "data": { + # "balance_list": [ + # { + # "margin_market": "BTCUSDT", + # "ccy": "BTC", + # "available": "44.62207740", + # "frozen": "0.00000000", + # "updated_at": 1689152421692 + # }, + # ] + # }, + # "id": null + # } + # + # swap + # + # { + # "method": "balance.update", + # "data": { + # "balance_list": [ + # { + # "ccy": "USDT", + # "available": "97.92470982756335000001", + # "frozen": "0.00000000000000000000", + # "margin": "0.61442700000000000000", + # "transferrable": "97.92470982756335000001", + # "unrealized_pnl": "-0.00807000000000000000", + # "equity": "97.92470982756335000001" + # }, + # ] + # }, + # "id": null + # } + # + if self.balance is None: + self.balance = {} + data = self.safe_dict(message, 'data', {}) + balances = self.safe_list(data, 'balance_list', []) + firstEntry = balances[0] + updated = self.safe_integer(firstEntry, 'updated_at') + unrealizedPnl = self.safe_string(firstEntry, 'unrealized_pnl') + isSpot = (updated is not None) + isSwap = (unrealizedPnl is not None) + info = None + account = None + rawBalances = [] + if isSpot: + account = 'spot' + for i in range(0, len(balances)): + rawBalances = self.array_concat(rawBalances, balances) + info = rawBalances + if isSwap: + account = 'swap' + for i in range(0, len(balances)): + rawBalances = self.array_concat(rawBalances, balances) + info = rawBalances + for i in range(0, len(rawBalances)): + entry = rawBalances[i] + self.parse_ws_balance(entry, account) + messageHash = None + if account is not None: + if self.safe_value(self.balance, account) is None: + self.balance[account] = {} + self.balance[account]['info'] = info + self.balance[account] = self.safe_balance(self.balance[account]) + messageHash = 'balances:' + account + client.resolve(self.balance[account], messageHash) + + def parse_ws_balance(self, balance, accountType=None): + # + # spot + # + # { + # "margin_market": "BTCUSDT", + # "ccy": "BTC", + # "available": "44.62207740", + # "frozen": "0.00000000", + # "updated_at": 1689152421692 + # } + # + # swap + # + # { + # "ccy": "USDT", + # "available": "97.92470982756335000001", + # "frozen": "0.00000000000000000000", + # "margin": "0.61442700000000000000", + # "transferrable": "97.92470982756335000001", + # "unrealized_pnl": "-0.00807000000000000000", + # "equity": "97.92470982756335000001" + # } + # + account = self.account() + currencyId = self.safe_string(balance, 'ccy') + code = self.safe_currency_code(currencyId) + account['free'] = self.safe_string(balance, 'available') + account['used'] = self.safe_string(balance, 'frozen') + if accountType is not None: + if self.safe_value(self.balance, accountType) is None: + self.balance[accountType] = {} + self.balance[accountType][code] = account + else: + self.balance[code] = account + + async def watch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + watches information on multiple trades made by the user + + https://docs.coinex.com/api/v2/spot/deal/ws/user-deals + https://docs.coinex.com/api/v2/futures/deal/ws/user-deals + + :param str [symbol]: unified symbol of the market the trades were made in + :param int [since]: the earliest time in ms to watch trades + :param int [limit]: the maximum number of trade structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + type = None + type, params = self.handle_market_type_and_params('watchMyTrades', market, params, 'spot') + await self.authenticate(type) + url = self.urls['api']['ws'][type] + subscribedSymbols = [] + messageHash = 'myTrades' + if market is not None: + messageHash += ':' + symbol + subscribedSymbols.append(market['id']) + else: + if type == 'spot': + messageHash += ':spot' + else: + messageHash += ':swap' + message: dict = { + 'method': 'user_deals.subscribe', + 'params': {'market_list': subscribedSymbols}, + 'id': self.request_id(), + } + request = self.deep_extend(message, params) + trades = await self.watch(url, messageHash, request, messageHash) + if self.newUpdates: + limit = trades.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(trades, symbol, since, limit, True) + + def handle_my_trades(self, client: Client, message): + # + # { + # "method": "user_deals.update", + # "data": { + # "deal_id": 3514376759, + # "created_at": 1689152421692, + # "market": "BTCUSDT", + # "side": "buy", + # "order_id": 8678890, + # "margin_market": "BTCUSDT", + # "price": "30718.42", + # "amount": "0.00000325", + # "role": "taker", + # "fee": "0.0299", + # "fee_ccy": "USDT" + # }, + # "id": null + # } + # + data = self.safe_dict(message, 'data', {}) + marketId = self.safe_string(data, 'market') + isSpot = client.url.find('spot') > -1 + defaultType = 'spot' if isSpot else 'swap' + market = self.safe_market(marketId, None, None, defaultType) + symbol = market['symbol'] + messageHash = 'myTrades:' + symbol + messageWithType = 'myTrades:' + market['type'] + stored = self.safe_value(self.trades, symbol) + if stored is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + stored = ArrayCache(limit) + self.trades[symbol] = stored + parsed = self.parse_ws_trade(data, market) + stored.append(parsed) + self.trades[symbol] = stored + client.resolve(self.trades[symbol], messageWithType) + client.resolve(self.trades[symbol], messageHash) + + def handle_trades(self, client: Client, message): + # + # spot + # + # { + # "method": "deals.update", + # "data": { + # "market": "BTCUSDT", + # "deal_list": [ + # { + # "deal_id": 3514376759, + # "created_at": 1689152421692, + # "side": "buy", + # "price": "30718.42", + # "amount": "0.00000325" + # }, + # ] + # }, + # "id": null + # } + # + # swap + # + # { + # "method": "deals.update", + # "data": { + # "market": "BTCUSDT", + # "deal_list": [ + # { + # "deal_id": 3514376759, + # "created_at": 1689152421692, + # "side": "buy", + # "price": "30718.42", + # "amount": "0.00000325" + # }, + # ] + # }, + # "id": null + # } + # + data = self.safe_dict(message, 'data', {}) + trades = self.safe_list(data, 'deal_list', []) + marketId = self.safe_string(data, 'market') + isSpot = client.url.find('spot') > -1 + defaultType = 'spot' if isSpot else 'swap' + market = self.safe_market(marketId, None, None, defaultType) + symbol = market['symbol'] + messageHash = 'trades:' + symbol + stored = self.safe_value(self.trades, symbol) + if stored is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + stored = ArrayCache(limit) + self.trades[symbol] = stored + for i in range(0, len(trades)): + trade = trades[i] + parsed = self.parse_ws_trade(trade, market) + stored.append(parsed) + self.trades[symbol] = stored + client.resolve(self.trades[symbol], messageHash) + + def parse_ws_trade(self, trade, market=None): + # + # spot watchTrades + # + # { + # "deal_id": 3514376759, + # "created_at": 1689152421692, + # "side": "buy", + # "price": "30718.42", + # "amount": "0.00000325" + # } + # + # swap watchTrades + # + # { + # "deal_id": 3514376759, + # "created_at": 1689152421692, + # "side": "buy", + # "price": "30718.42", + # "amount": "0.00000325" + # } + # + # spot and swap watchMyTrades + # + # { + # "deal_id": 3514376759, + # "created_at": 1689152421692, + # "market": "BTCUSDT", + # "side": "buy", + # "order_id": 8678890, + # "margin_market": "BTCUSDT", + # "price": "30718.42", + # "amount": "0.00000325", + # "role": "taker", + # "fee": "0.0299", + # "fee_ccy": "USDT" + # } + # + timestamp = self.safe_integer(trade, 'created_at') + isSpot = ('margin_market' in trade) + defaultType = 'spot' if isSpot else 'swap' + marketId = self.safe_string(trade, 'market') + market = self.safe_market(marketId, market, None, defaultType) + fee: dict = {} + feeCost = self.omit_zero(self.safe_string(trade, 'fee')) + if feeCost is not None: + feeCurrencyId = self.safe_string(trade, 'fee_ccy', market['quote']) + fee = { + 'currency': self.safe_currency_code(feeCurrencyId), + 'cost': feeCost, + } + return self.safe_trade({ + 'id': self.safe_string(trade, 'deal_id'), + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': self.safe_symbol(marketId, market, None, defaultType), + 'order': self.safe_string(trade, 'order_id'), + 'type': None, + 'side': self.safe_string(trade, 'side'), + 'takerOrMaker': self.safe_string(trade, 'role'), + 'price': self.safe_string(trade, 'price'), + 'amount': self.safe_string(trade, 'amount'), + 'cost': None, + 'fee': fee, + }, market) + + async def watch_ticker(self, symbol: str, params={}) -> Ticker: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://docs.coinex.com/api/v2/spot/market/ws/market + https://docs.coinex.com/api/v2/futures/market/ws/market-state + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + tickers = await self.watch_tickers([symbol], params) + return tickers[market['symbol']] + + async def watch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list + + https://docs.coinex.com/api/v2/spot/market/ws/market + https://docs.coinex.com/api/v2/futures/market/ws/market-state + + :param str[] symbols: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + marketIds = self.market_ids(symbols) + market = None + messageHashes = [] + symbolsDefined = (symbols is not None) + if symbolsDefined: + for i in range(0, len(symbols)): + symbol = symbols[i] + market = self.market(symbol) + messageHashes.append('tickers::' + market['symbol']) + else: + marketIds = [] + messageHashes.append('tickers') + type = None + type, params = self.handle_market_type_and_params('watchTickers', market, params) + url = self.urls['api']['ws'][type] + subscriptionHashes = ['all@ticker'] + subscribe: dict = { + 'method': 'state.subscribe', + 'params': {'market_list': marketIds}, + 'id': self.request_id(), + } + result = await self.watch_multiple(url, messageHashes, self.deep_extend(subscribe, params), subscriptionHashes) + if self.newUpdates: + return result + return self.filter_by_array(self.tickers, 'symbol', symbols) + + async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://docs.coinex.com/api/v2/spot/market/ws/market-deals + https://docs.coinex.com/api/v2/futures/market/ws/market-deals + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + params['callerMethodName'] = 'watchTrades' + return await self.watch_trades_for_symbols([symbol], since, limit, params) + + async def watch_trades_for_symbols(self, symbols: List[str], since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + watch the most recent trades for a list of symbols + + https://docs.coinex.com/api/v2/spot/market/ws/market-deals + https://docs.coinex.com/api/v2/futures/market/ws/market-deals + + :param str[] symbols: unified symbols of the markets to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + subscribedSymbols = [] + messageHashes = [] + market = None + callerMethodName = None + callerMethodName, params = self.handle_param_string(params, 'callerMethodName', 'watchTradesForSymbols') + symbolsDefined = (symbols is not None) + if symbolsDefined: + for i in range(0, len(symbols)): + symbol = symbols[i] + market = self.market(symbol) + subscribedSymbols.append(market['id']) + messageHashes.append('trades:' + market['symbol']) + else: + messageHashes.append('trades') + type = None + type, params = self.handle_market_type_and_params(callerMethodName, market, params) + url = self.urls['api']['ws'][type] + # subscriptionHashes = ['trades'] + subscribe: dict = { + 'method': 'deals.subscribe', + 'params': {'market_list': subscribedSymbols}, + 'id': self.request_id(), + } + trades = await self.watch_multiple(url, messageHashes, self.deep_extend(subscribe, params), messageHashes) + if self.newUpdates: + return trades + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + async def watch_order_book_for_symbols(self, symbols: List[str], limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://docs.coinex.com/api/v2/spot/market/ws/market-depth + https://docs.coinex.com/api/v2/futures/market/ws/market-depth + + :param str[] symbols: unified array of symbols + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + watchOrderBookSubscriptions: dict = {} + messageHashes = [] + market = None + type = None + callerMethodName = None + callerMethodName, params = self.handle_param_string(params, 'callerMethodName', 'watchOrderBookForSymbols') + options = self.safe_dict(self.options, 'watchOrderBook', {}) + limits = self.safe_list(options, 'limits', []) + if limit is None: + limit = self.safe_integer(options, 'defaultLimit', 50) + if not self.in_array(limit, limits): + raise NotSupported(self.id + ' watchOrderBookForSymbols() limit must be one of ' + ', '.join(limits)) + defaultAggregation = self.safe_string(options, 'defaultAggregation', '0') + aggregations = self.safe_list(options, 'aggregations', []) + aggregation = self.safe_string(params, 'aggregation', defaultAggregation) + if not self.in_array(aggregation, aggregations): + raise NotSupported(self.id + ' watchOrderBookForSymbols() aggregation must be one of ' + ', '.join(aggregations)) + params = self.omit(params, 'aggregation') + symbolsDefined = (symbols is not None) + if not symbolsDefined: + raise ArgumentsRequired(self.id + ' watchOrderBookForSymbols() requires a symbol argument') + for i in range(0, len(symbols)): + symbol = symbols[i] + market = self.market(symbol) + messageHashes.append('orderbook:' + market['symbol']) + watchOrderBookSubscriptions[symbol] = [market['id'], limit, aggregation, True] + type, params = self.handle_market_type_and_params(callerMethodName, market, params) + marketList = list(watchOrderBookSubscriptions.values()) + subscribe: dict = { + 'method': 'depth.subscribe', + 'params': {'market_list': marketList}, + 'id': self.request_id(), + } + # subscriptionHashes = self.hash(self.encode(self.json(watchOrderBookSubscriptions)), 'sha256') + url = self.urls['api']['ws'][type] + orderbooks = await self.watch_multiple(url, messageHashes, self.deep_extend(subscribe, params), messageHashes) + if self.newUpdates: + return orderbooks + return orderbooks.limit() + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://docs.coinex.com/api/v2/spot/market/ws/market-depth + https://docs.coinex.com/api/v2/futures/market/ws/market-depth + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + params['callerMethodName'] = 'watchOrderBook' + return await self.watch_order_book_for_symbols([symbol], limit, params) + + def handle_delta(self, bookside, delta): + bidAsk = self.parse_bid_ask(delta, 0, 1) + bookside.storeArray(bidAsk) + + def handle_deltas(self, bookside, deltas): + for i in range(0, len(deltas)): + self.handle_delta(bookside, deltas[i]) + + def handle_order_book(self, client: Client, message): + # + # { + # "method": "depth.update", + # "data": { + # "market": "BTCUSDT", + # "is_full": True, + # "depth": { + # "asks": [ + # [ + # "30740.00", + # "0.31763545" + # ], + # ], + # "bids": [ + # [ + # "30736.00", + # "0.04857373" + # ], + # ], + # "last": "30746.28", + # "updated_at": 1689152421692, + # "checksum": 2578768879 + # } + # }, + # "id": null + # } + # + isSpot = client.url.find('spot') > -1 + defaultType = 'spot' if isSpot else 'swap' + data = self.safe_dict(message, 'data', {}) + depth = self.safe_dict(data, 'depth', {}) + marketId = self.safe_string(data, 'market') + market = self.safe_market(marketId, None, None, defaultType) + symbol = market['symbol'] + name = 'orderbook' + messageHash = name + ':' + symbol + timestamp = self.safe_integer(depth, 'updated_at') + currentOrderBook = self.safe_value(self.orderbooks, symbol) + fullOrderBook = self.safe_bool(data, 'is_full', False) + if fullOrderBook: + snapshot = self.parse_order_book(depth, symbol, timestamp) + if currentOrderBook is None: + self.orderbooks[symbol] = self.order_book(snapshot) + else: + orderbook = self.orderbooks[symbol] + orderbook.reset(snapshot) + else: + asks = self.safe_list(depth, 'asks', []) + bids = self.safe_list(depth, 'bids', []) + self.handle_deltas(currentOrderBook['asks'], asks) + self.handle_deltas(currentOrderBook['bids'], bids) + currentOrderBook['nonce'] = timestamp + currentOrderBook['timestamp'] = timestamp + currentOrderBook['datetime'] = self.iso8601(timestamp) + self.orderbooks[symbol] = currentOrderBook + # self.checkOrderBookChecksum(self.orderbooks[symbol]) + client.resolve(self.orderbooks[symbol], messageHash) + + async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + watches information on multiple orders made by the user + + https://docs.coinex.com/api/v2/spot/order/ws/user-order + https://docs.coinex.com/api/v2/futures/order/ws/user-order + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.trigger]: if the orders to watch are trigger orders or not + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + trigger = self.safe_bool_2(params, 'trigger', 'stop') + params = self.omit(params, ['trigger', 'stop']) + messageHash = 'orders' + market = None + marketList = None + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + type = None + type, params = self.handle_market_type_and_params('watchOrders', market, params, 'spot') + await self.authenticate(type) + if symbol is not None: + marketList = [market['id']] + messageHash += ':' + symbol + else: + marketList = [] + if type == 'spot': + messageHash += ':spot' + else: + messageHash += ':swap' + method = None + if trigger: + method = 'stop.subscribe' + else: + method = 'order.subscribe' + message: dict = { + 'method': method, + 'params': {'market_list': marketList}, + 'id': self.request_id(), + } + url = self.urls['api']['ws'][type] + request = self.deep_extend(message, params) + orders = await self.watch(url, messageHash, request, messageHash, request) + if self.newUpdates: + limit = orders.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(orders, symbol, since, limit, True) + + def handle_orders(self, client: Client, message): + # + # spot + # + # { + # "method": "order.update", + # "data": { + # "event": "put", + # "order": { + # "order_id": 12750, + # "market": "BTCUSDT", + # "margin_market": "BTCUSDT", + # "type": "limit", + # "side": "buy", + # "price": "5999.00", + # "amount": "1.50000000", + # "unfill_amount": "1.50000000", + # "fill_value": "1.50000000", + # "taker_fee_rate": "0.0001", + # "maker_fee_rate": "0.0001", + # "base_ccy_fee": "0.0001", + # "quote_ccy_fee": "0.0001", + # "discount_ccy_fee": "0.0001", + # "last_fill_amount": "0", + # "last_fill_price": "0", + # "client_id": "buy1_1234", + # "created_at": 1689152421692, + # "updated_at": 1689152421692, + # } + # }, + # "id": null + # } + # + # spot stop + # + # { + # "method": "stop.update", + # "data": { + # "event": 1, + # "stop": { + # "stop_id": 102067022299, + # "market": "BTCUSDT", + # "margin_market": "BTCUSDT", + # "type": "limit", + # "side": "buy", + # "price": "20000.00", + # "amount": "0.10000000", + # "trigger_price": "20000.00", + # "trigger_direction": "lower", + # "taker_fee_rate": "0.0016", + # "maker_fee_rate": "0.0016", + # "status": "active_success", + # "client_id": "", + # "created_at": 1689152996689, + # "updated_at": 1689152996689, + # } + # }, + # "id": null + # } + # + # swap + # + # { + # "method": "order.update", + # "data": { + # "event": "put", + # "order": { + # "order_id": 98388656341, + # "stop_id": 0, + # "market": "BTCUSDT", + # "side": "buy", + # "type": "limit", + # "amount": "0.0010", + # "price": "50000.00", + # "unfilled_amount": "0.0010", + # "filled_amount": "0", + # "filled_value": "0", + # "fee": "0", + # "fee_ccy": "USDT", + # "taker_fee_rate": "0.00046", + # "maker_fee_rate": "0.00000000000000000000", + # "client_id": "", + # "last_filled_amount": "0.0010", + # "last_filled_price": "30721.35", + # "created_at": 1689145715129, + # "updated_at": 1689145715129 + # } + # }, + # "id": null + # } + # + # swap stop + # + # { + # "method": "stop.update", + # "data": { + # "event": "put", + # "stop": { + # "stop_id": 98389557871, + # "market": "BTCUSDT", + # "side": "sell", + # "type": "limit", + # "price": "20000.00", + # "amount": "0.0100", + # "trigger_price": "20000.00", + # "trigger_direction": "higer", + # "trigger_price_type": "index_price", + # "taker_fee_rate": "0.00046", + # "maker_fee_rate": "0.00026", + # "client_id": "", + # "created_at": 1689146382674, + # "updated_at": 1689146382674 + # } + # }, + # "id": null + # } + # + data = self.safe_dict(message, 'data', {}) + order = self.safe_dict_2(data, 'order', 'stop', {}) + parsedOrder = self.parse_ws_order(order) + symbol = parsedOrder['symbol'] + market = self.market(symbol) + if self.orders is None: + limit = self.safe_integer(self.options, 'ordersLimit', 1000) + self.orders = ArrayCacheBySymbolById(limit) + orders = self.orders + orders.append(parsedOrder) + messageHash = 'orders' + messageWithType = messageHash + ':' + market['type'] + client.resolve(self.orders, messageWithType) + messageHash += ':' + symbol + client.resolve(self.orders, messageHash) + + def parse_ws_order(self, order, market=None): + # + # spot + # + # { + # "order_id": 12750, + # "market": "BTCUSDT", + # "margin_market": "BTCUSDT", + # "type": "limit", + # "side": "buy", + # "price": "5999.00", + # "amount": "1.50000000", + # "unfill_amount": "1.50000000", + # "fill_value": "1.50000000", + # "taker_fee_rate": "0.0001", + # "maker_fee_rate": "0.0001", + # "base_ccy_fee": "0.0001", + # "quote_ccy_fee": "0.0001", + # "discount_ccy_fee": "0.0001", + # "last_fill_amount": "0", + # "last_fill_price": "0", + # "client_id": "buy1_1234", + # "created_at": 1689152421692, + # "updated_at": 1689152421692, + # } + # + # spot stop + # + # { + # "stop_id": 102067022299, + # "market": "BTCUSDT", + # "margin_market": "BTCUSDT", + # "type": "limit", + # "side": "buy", + # "price": "20000.00", + # "amount": "0.10000000", + # "trigger_price": "20000.00", + # "trigger_direction": "lower", + # "taker_fee_rate": "0.0016", + # "maker_fee_rate": "0.0016", + # "status": "active_success", + # "client_id": "", + # "created_at": 1689152996689, + # "updated_at": 1689152996689, + # } + # + # swap + # + # { + # "order_id": 98388656341, + # "stop_id": 0, + # "market": "BTCUSDT", + # "side": "buy", + # "type": "limit", + # "amount": "0.0010", + # "price": "50000.00", + # "unfilled_amount": "0.0010", + # "filled_amount": "0", + # "filled_value": "0", + # "fee": "0", + # "fee_ccy": "USDT", + # "taker_fee_rate": "0.00046", + # "maker_fee_rate": "0.00000000000000000000", + # "client_id": "", + # "last_filled_amount": "0.0010", + # "last_filled_price": "30721.35", + # "created_at": 1689145715129, + # "updated_at": 1689145715129 + # } + # + # swap stop + # + # { + # "stop_id": 98389557871, + # "market": "BTCUSDT", + # "side": "sell", + # "type": "limit", + # "price": "20000.00", + # "amount": "0.0100", + # "trigger_price": "20000.00", + # "trigger_direction": "higer", + # "trigger_price_type": "index_price", + # "taker_fee_rate": "0.00046", + # "maker_fee_rate": "0.00026", + # "client_id": "", + # "created_at": 1689146382674, + # "updated_at": 1689146382674 + # } + # + timestamp = self.safe_integer(order, 'created_at') + marketId = self.safe_string(order, 'market') + status = self.safe_string(order, 'status') + isSpot = ('margin_market' in order) + defaultType = 'spot' if isSpot else 'swap' + market = self.safe_market(marketId, market, None, defaultType) + fee = None + feeCost = self.omit_zero(self.safe_string_2(order, 'fee', 'quote_ccy_fee')) + if feeCost is not None: + feeCurrencyId = self.safe_string(order, 'fee_ccy', market['quote']) + fee = { + 'currency': self.safe_currency_code(feeCurrencyId), + 'cost': feeCost, + } + return self.safe_order({ + 'info': order, + 'id': self.safe_string_2(order, 'order_id', 'stop_id'), + 'clientOrderId': self.safe_string(order, 'client_id'), + 'datetime': self.iso8601(timestamp), + 'timestamp': timestamp, + 'lastTradeTimestamp': self.safe_integer(order, 'updated_at'), + 'symbol': market['symbol'], + 'type': self.safe_string(order, 'type'), + 'timeInForce': None, + 'postOnly': None, + 'side': self.safe_string(order, 'side'), + 'price': self.safe_string(order, 'price'), + 'stopPrice': self.safe_string(order, 'trigger_price'), + 'triggerPrice': self.safe_string(order, 'trigger_price'), + 'amount': self.safe_string(order, 'amount'), + 'filled': self.safe_string_2(order, 'filled_amount', 'fill_value'), + 'remaining': self.safe_string_2(order, 'unfilled_amount', 'unfill_amount'), + 'cost': None, + 'average': None, + 'status': self.parse_ws_order_status(status), + 'fee': fee, + 'trades': None, + }, market) + + def parse_ws_order_status(self, status): + statuses: dict = { + 'active_success': 'open', + 'active_fail': 'canceled', + 'cancel': 'canceled', + } + return self.safe_string(statuses, status, status) + + async def watch_bids_asks(self, symbols: Strings = None, params={}) -> Tickers: + """ + watches best bid & ask for symbols + + https://docs.coinex.com/api/v2/spot/market/ws/market-bbo + https://docs.coinex.com/api/v2/futures/market/ws/market-bbo + + :param str[] [symbols]: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + marketIds = self.market_ids(symbols) + messageHashes = [] + market = None + symbolsDefined = (symbols is not None) + if symbolsDefined: + for i in range(0, len(symbols)): + symbol = symbols[i] + market = self.market(symbol) + messageHashes.append('bidsasks:' + market['symbol']) + else: + messageHashes.append('bidsasks') + type = None + type, params = self.handle_market_type_and_params('watchBidsAsks', market, params) + url = self.urls['api']['ws'][type] + subscriptionHashes = ['all@bidsasks'] + subscribe: dict = { + 'method': 'bbo.subscribe', + 'params': {'market_list': marketIds}, + 'id': self.request_id(), + } + result = await self.watch_multiple(url, messageHashes, self.deep_extend(subscribe, params), subscriptionHashes) + if self.newUpdates: + return result + return self.filter_by_array(self.bidsasks, 'symbol', symbols) + + def handle_bid_ask(self, client: Client, message): + # + # { + # "method": "bbo.update", + # "data": { + # "market": "BTCUSDT", + # "updated_at": 1656660154, + # "best_bid_price": "20000", + # "best_bid_size": "0.1", + # "best_ask_price": "20001", + # "best_ask_size": "0.15" + # }, + # "id": null + # } + # + data = self.safe_dict(message, 'data', {}) + parsedTicker = self.parse_ws_bid_ask(data) + symbol = parsedTicker['symbol'] + self.bidsasks[symbol] = parsedTicker + messageHash = 'bidsasks:' + symbol + client.resolve(parsedTicker, messageHash) + + def parse_ws_bid_ask(self, ticker, market=None): + # + # { + # "market": "BTCUSDT", + # "updated_at": 1656660154, + # "best_bid_price": "20000", + # "best_bid_size": "0.1", + # "best_ask_price": "20001", + # "best_ask_size": "0.15" + # } + # + defaultType = self.safe_string(self.options, 'defaultType') + marketId = self.safe_string(ticker, 'market') + market = self.safe_market(marketId, market, None, defaultType) + timestamp = self.safe_integer(ticker, 'updated_at') + return self.safe_ticker({ + 'symbol': self.safe_symbol(marketId, market, None, defaultType), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'ask': self.safe_number(ticker, 'best_ask_price'), + 'askVolume': self.safe_number(ticker, 'best_ask_size'), + 'bid': self.safe_number(ticker, 'best_bid_price'), + 'bidVolume': self.safe_number(ticker, 'best_bid_size'), + 'info': ticker, + }, market) + + def handle_message(self, client: Client, message): + method = self.safe_string(message, 'method') + error = self.safe_string(message, 'message') + if error is not None: + self.handle_errors(1, '', client.url, method, {}, self.json(error), message, {}, {}) + handlers: dict = { + 'state.update': self.handle_ticker, + 'balance.update': self.handle_balance, + 'deals.update': self.handle_trades, + 'user_deals.update': self.handle_my_trades, + 'depth.update': self.handle_order_book, + 'order.update': self.handle_orders, + 'stop.update': self.handle_orders, + 'bbo.update': self.handle_bid_ask, + } + handler = self.safe_value(handlers, method) + if handler is not None: + handler(client, message) + return + self.handle_subscription_status(client, message) + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None + # + # {"id": 1, "code": 20001, "message": "invalid argument"} + # {"id": 2, "code": 21001, "message": "require auth"} + # {"id": 1, "code": 21002, "message": "Signature Incorrect"} + # + message = self.safe_string_lower(response, 'message') + isErrorMessage = (message is not None) and (message != 'ok') + errorCode = self.safe_string(response, 'code') + isErrorCode = (errorCode is not None) and (errorCode != '0') + if isErrorCode or isErrorMessage: + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + raise ExchangeError(feedback) + return None + + def handle_authentication_message(self, client: Client, message): + # + # success + # + # { + # "id": 1, + # "code": 0, + # "message": "OK" + # } + # + # fail + # + # { + # "id": 1, + # "code": 21002, + # "message": "" + # } + # + status = self.safe_string_lower(message, 'message') + errorCode = self.safe_string(message, 'code') + messageHash = 'authenticated' + if (status == 'ok') or (errorCode == '0'): + future = self.safe_value(client.futures, messageHash) + future.resolve(True) + else: + error = AuthenticationError(self.json(message)) + client.reject(error, messageHash) + if messageHash in client.subscriptions: + del client.subscriptions[messageHash] + + def handle_subscription_status(self, client: Client, message): + id = self.safe_integer(message, 'id') + subscription = self.safe_value(client.subscriptions, id) + if subscription is not None: + futureIndex = self.safe_string(subscription, 'future') + future = self.safe_value(client.futures, futureIndex) + if future is not None: + future.resolve(True) + del client.subscriptions[id] + + async def authenticate(self, type: str): + url = self.urls['api']['ws'][type] + client = self.client(url) + time = self.milliseconds() + timestamp = str(time) + messageHash = 'authenticated' + future = client.reusableFuture(messageHash) + authenticated = self.safe_value(client.subscriptions, messageHash) + if authenticated is not None: + return await future + requestId = self.request_id() + subscribe: dict = { + 'id': requestId, + 'future': messageHash, + } + hmac = self.hmac(self.encode(timestamp), self.encode(self.secret), hashlib.sha256, 'hex') + request: dict = { + 'id': requestId, + 'method': 'server.sign', + 'params': { + 'access_id': self.apiKey, + 'signed_str': hmac.lower(), + 'timestamp': time, + }, + } + self.watch(url, messageHash, request, requestId, subscribe) + client.subscriptions[messageHash] = True + return await future diff --git a/ccxt/pro/coinone.py b/ccxt/pro/coinone.py new file mode 100644 index 0000000..5f887c5 --- /dev/null +++ b/ccxt/pro/coinone.py @@ -0,0 +1,402 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCache +from ccxt.base.types import Any, Bool, Int, Market, OrderBook, Ticker, Trade +from ccxt.async_support.base.ws.client import Client +from typing import List +from ccxt.base.errors import AuthenticationError + + +class coinone(ccxt.async_support.coinone): + + def describe(self) -> Any: + return self.deep_extend(super(coinone, self).describe(), { + 'has': { + 'ws': True, + 'watchOrderBook': True, + 'watchOrders': False, + 'watchTrades': True, + 'watchTradesForSymbols': False, + 'watchOHLCV': False, + 'watchTicker': True, + 'watchTickers': False, + }, + 'urls': { + 'api': { + 'ws': 'wss://stream.coinone.co.kr', + }, + }, + 'options': { + 'expiresIn': '', + 'userId': '', + 'wsSessionToken': '', + 'watchOrderBook': { + 'snapshotDelay': 6, + 'snapshotMaxRetries': 3, + }, + 'tradesLimit': 1000, + 'OHLCVLimit': 1000, + }, + 'exceptions': { + 'exact': { + '4009': AuthenticationError, + }, + }, + 'streaming': { + 'ping': self.ping, + 'keepAlive': 20000, + }, + }) + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://docs.coinone.co.kr/reference/public-websocket-orderbook + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + messageHash = 'orderbook:' + market['symbol'] + url = self.urls['api']['ws'] + request: dict = { + 'request_type': 'SUBSCRIBE', + 'channel': 'ORDERBOOK', + 'topic': { + 'quote_currency': market['quote'], + 'target_currency': market['base'], + }, + } + message = self.extend(request, params) + orderbook = await self.watch(url, messageHash, message, messageHash) + return orderbook.limit() + + def handle_order_book(self, client, message): + # + # { + # "response_type": "DATA", + # "channel": "ORDERBOOK", + # "data": { + # "quote_currency": "KRW", + # "target_currency": "BTC", + # "timestamp": 1705288918649, + # "id": "1705288918649001", + # "asks": [ + # { + # "price": "58412000", + # "qty": "0.59919807" + # } + # ], + # "bids": [ + # { + # "price": "58292000", + # "qty": "0.1045" + # } + # ] + # } + # } + # + data = self.safe_value(message, 'data', {}) + baseId = self.safe_string_upper(data, 'target_currency') + quoteId = self.safe_string_upper(data, 'quote_currency') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + symbol = self.symbol(base + '/' + quote) + timestamp = self.safe_integer(data, 'timestamp') + orderbook = self.safe_value(self.orderbooks, symbol) + if orderbook is None: + orderbook = self.order_book() + else: + orderbook.reset() + orderbook['symbol'] = symbol + asks = self.safe_value(data, 'asks', []) + bids = self.safe_value(data, 'bids', []) + self.handle_deltas(orderbook['asks'], asks) + self.handle_deltas(orderbook['bids'], bids) + orderbook['timestamp'] = timestamp + orderbook['datetime'] = self.iso8601(timestamp) + messageHash = 'orderbook:' + symbol + self.orderbooks[symbol] = orderbook + client.resolve(orderbook, messageHash) + + def handle_delta(self, bookside, delta): + bidAsk = self.parse_bid_ask(delta, 'price', 'qty') + bookside.storeArray(bidAsk) + + async def watch_ticker(self, symbol: str, params={}) -> Ticker: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://docs.coinone.co.kr/reference/public-websocket-ticker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + messageHash = 'ticker:' + market['symbol'] + url = self.urls['api']['ws'] + request: dict = { + 'request_type': 'SUBSCRIBE', + 'channel': 'TICKER', + 'topic': { + 'quote_currency': market['quote'], + 'target_currency': market['base'], + }, + } + message = self.extend(request, params) + return await self.watch(url, messageHash, message, messageHash) + + def handle_ticker(self, client: Client, message): + # + # { + # "response_type": "DATA", + # "channel": "TICKER", + # "data": { + # "quote_currency": "KRW", + # "target_currency": "BTC", + # "timestamp": 1705301117198, + # "quote_volume": "19521465345.504", + # "target_volume": "334.81445168", + # "high": "58710000", + # "low": "57276000", + # "first": "57293000", + # "last": "58532000", + # "volume_power": "100", + # "ask_best_price": "58537000", + # "ask_best_qty": "0.1961", + # "bid_best_price": "58532000", + # "bid_best_qty": "0.00009258", + # "id": "1705301117198001", + # "yesterday_high": "59140000", + # "yesterday_low": "57273000", + # "yesterday_first": "58897000", + # "yesterday_last": "57301000", + # "yesterday_quote_volume": "12967227517.4262", + # "yesterday_target_volume": "220.09232233" + # } + # } + # + data = self.safe_value(message, 'data', {}) + ticker = self.parse_ws_ticker(data) + symbol = ticker['symbol'] + self.tickers[symbol] = ticker + messageHash = 'ticker:' + symbol + client.resolve(self.tickers[symbol], messageHash) + + def parse_ws_ticker(self, ticker, market: Market = None) -> Ticker: + # + # { + # "quote_currency": "KRW", + # "target_currency": "BTC", + # "timestamp": 1705301117198, + # "quote_volume": "19521465345.504", + # "target_volume": "334.81445168", + # "high": "58710000", + # "low": "57276000", + # "first": "57293000", + # "last": "58532000", + # "volume_power": "100", + # "ask_best_price": "58537000", + # "ask_best_qty": "0.1961", + # "bid_best_price": "58532000", + # "bid_best_qty": "0.00009258", + # "id": "1705301117198001", + # "yesterday_high": "59140000", + # "yesterday_low": "57273000", + # "yesterday_first": "58897000", + # "yesterday_last": "57301000", + # "yesterday_quote_volume": "12967227517.4262", + # "yesterday_target_volume": "220.09232233" + # } + # + timestamp = self.safe_integer(ticker, 'timestamp') + last = self.safe_string(ticker, 'last') + baseId = self.safe_string(ticker, 'target_currency') + quoteId = self.safe_string(ticker, 'quote_currency') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + symbol = self.symbol(base + '/' + quote) + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': self.safe_number(ticker, 'bid_best_price'), + 'bidVolume': self.safe_number(ticker, 'bid_best_qty'), + 'ask': self.safe_number(ticker, 'ask_best_price'), + 'askVolume': self.safe_number(ticker, 'ask_best_qty'), + 'vwap': None, + 'open': self.safe_string(ticker, 'first'), + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': self.safe_string(ticker, 'target_volume'), + 'quoteVolume': self.safe_string(ticker, 'quote_volume'), + 'info': ticker, + }, market) + + async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + watches information on multiple trades made in a market + + https://docs.coinone.co.kr/reference/public-websocket-trade + + :param str symbol: unified market symbol of the market trades were made in + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trade structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + messageHash = 'trade:' + market['symbol'] + url = self.urls['api']['ws'] + request: dict = { + 'request_type': 'SUBSCRIBE', + 'channel': 'TRADE', + 'topic': { + 'quote_currency': market['quote'], + 'target_currency': market['base'], + }, + } + message = self.extend(request, params) + trades = await self.watch(url, messageHash, message, messageHash) + if self.newUpdates: + limit = trades.getLimit(market['symbol'], limit) + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + def handle_trades(self, client: Client, message): + # + # { + # "response_type": "DATA", + # "channel": "TRADE", + # "data": { + # "quote_currency": "KRW", + # "target_currency": "BTC", + # "id": "1705303667916001", + # "timestamp": 1705303667916, + # "price": "58490000", + # "qty": "0.0008", + # "is_seller_maker": False + # } + # } + # + data = self.safe_value(message, 'data', {}) + trade = self.parse_ws_trade(data) + symbol = trade['symbol'] + stored = self.safe_value(self.trades, symbol) + if stored is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + stored = ArrayCache(limit) + self.trades[symbol] = stored + stored.append(trade) + messageHash = 'trade:' + symbol + client.resolve(stored, messageHash) + + def parse_ws_trade(self, trade: dict, market: Market = None) -> Trade: + # + # { + # "quote_currency": "KRW", + # "target_currency": "BTC", + # "id": "1705303667916001", + # "timestamp": 1705303667916, + # "price": "58490000", + # "qty": "0.0008", + # "is_seller_maker": False + # } + # + baseId = self.safe_string_upper(trade, 'target_currency') + quoteId = self.safe_string_upper(trade, 'quote_currency') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + symbol = base + '/' + quote + timestamp = self.safe_integer(trade, 'timestamp') + market = self.safe_market(symbol, market) + isSellerMaker = self.safe_value(trade, 'is_seller_maker') + side = None + if isSellerMaker is not None: + side = 'sell' if isSellerMaker else 'buy' + priceString = self.safe_string(trade, 'price') + amountString = self.safe_string(trade, 'qty') + return self.safe_trade({ + 'id': self.safe_string(trade, 'id'), + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'order': None, + 'symbol': market['symbol'], + 'type': None, + 'side': side, + 'takerOrMaker': None, + 'price': priceString, + 'amount': amountString, + 'cost': None, + 'fee': None, + }, market) + + def handle_error_message(self, client: Client, message) -> Bool: + # + # { + # "response_type": "ERROR", + # "error_code": 160012, + # "message": "Invalid Topic" + # } + # + type = self.safe_string(message, 'response_type', '') + if type == 'ERROR': + return True + return False + + def handle_message(self, client: Client, message): + if self.handle_error_message(client, message): + return + type = self.safe_string(message, 'response_type') + if type == 'PONG': + self.handle_pong(client, message) + return + if type == 'DATA': + topic = self.safe_string(message, 'channel', '') + methods: dict = { + 'ORDERBOOK': self.handle_order_book, + 'TICKER': self.handle_ticker, + 'TRADE': self.handle_trades, + } + exacMethod = self.safe_value(methods, topic) + if exacMethod is not None: + exacMethod(client, message) + return + keys = list(methods.keys()) + for i in range(0, len(keys)): + key = keys[i] + if topic.find(keys[i]) >= 0: + method = methods[key] + method(client, message) + return + + def ping(self, client: Client): + return { + 'request_type': 'PING', + } + + def handle_pong(self, client: Client, message): + # + # { + # "response_type":"PONG" + # } + # + client.lastPong = self.milliseconds() + return message diff --git a/ccxt/pro/cryptocom.py b/ccxt/pro/cryptocom.py new file mode 100644 index 0000000..1c0508f --- /dev/null +++ b/ccxt/pro/cryptocom.py @@ -0,0 +1,1350 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolById, ArrayCacheBySymbolBySide, ArrayCacheByTimestamp +import hashlib +from ccxt.base.types import Any, Balances, Bool, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, Trade +from ccxt.async_support.base.ws.client import Client +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import NetworkError +from ccxt.base.errors import ChecksumError + + +class cryptocom(ccxt.async_support.cryptocom): + + def describe(self) -> Any: + return self.deep_extend(super(cryptocom, self).describe(), { + 'has': { + 'ws': True, + 'watchBalance': True, + 'watchTicker': True, + 'watchTickers': True, + 'watchBidsAsks': True, + 'watchMyTrades': True, + 'watchTrades': True, + 'watchTradesForSymbols': True, + 'watchOrderBook': True, + 'watchOrderBookForSymbols': True, + 'watchOrders': True, + 'watchOHLCV': True, + 'watchPositions': True, + 'createOrderWs': True, + 'cancelOrderWs': True, + 'cancelAllOrders': True, + 'editOrderWs': True, + }, + 'urls': { + 'api': { + 'ws': { + 'public': 'wss://stream.crypto.com/exchange/v1/market', + 'private': 'wss://stream.crypto.com/exchange/v1/user', + }, + }, + 'test': { + 'public': 'wss://uat-stream.3ona.co/exchange/v1/market', + 'private': 'wss://uat-stream.3ona.co/exchange/v1/user', + }, + }, + 'options': { + 'watchPositions': { + 'fetchPositionsSnapshot': True, # or False + 'awaitPositionsSnapshot': True, # whether to wait for the positions snapshot before providing updates + }, + 'watchOrderBook': { + 'checksum': True, + }, + }, + 'streaming': { + }, + }) + + async def pong(self, client, message): + # { + # "id": 1587523073344, + # "method": "public/heartbeat", + # "code": 0 + # } + try: + await client.send({'id': self.safe_integer(message, 'id'), 'method': 'public/respond-heartbeat'}) + except Exception as e: + error = NetworkError(self.id + ' pong failed with error ' + self.json(e)) + client.reset(error) + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#book-instrument_name + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.bookSubscriptionType]: The subscription type. Allowed values: SNAPSHOT full snapshot. This is the default if not specified. SNAPSHOT_AND_UPDATE delta updates + :param int [params.bookUpdateFrequency]: Book update interval in ms. Allowed values: 100 for snapshot subscription 10 for delta subscription + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + return await self.watch_order_book_for_symbols([symbol], limit, params) + + async def un_watch_order_book(self, symbol: str, params={}) -> Any: + """ + unWatches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#book-instrument_name + + :param str symbol: unified symbol of the market to fetch the order book for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.bookSubscriptionType]: The subscription type. Allowed values: SNAPSHOT full snapshot. This is the default if not specified. SNAPSHOT_AND_UPDATE delta updates + :param int [params.bookUpdateFrequency]: Book update interval in ms. Allowed values: 100 for snapshot subscription 10 for delta subscription + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + return await self.un_watch_order_book_for_symbols([symbol], params) + + async def watch_order_book_for_symbols(self, symbols: List[str], limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#book-instrument_name + + :param str[] symbols: unified array of symbols + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.bookSubscriptionType]: The subscription type. Allowed values: SNAPSHOT full snapshot. This is the default if not specified. SNAPSHOT_AND_UPDATE delta updates + :param int [params.bookUpdateFrequency]: Book update interval in ms. Allowed values: 100 for snapshot subscription 10 for delta subscription + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + topics = [] + messageHashes = [] + if not limit: + limit = 50 + topicParams = self.safe_value(params, 'params') + if topicParams is None: + params['params'] = {} + bookSubscriptionType = None + bookSubscriptionType2 = None + bookSubscriptionType, params = self.handle_option_and_params(params, 'watchOrderBook', 'bookSubscriptionType', 'SNAPSHOT_AND_UPDATE') + bookSubscriptionType2, params = self.handle_option_and_params(params, 'watchOrderBookForSymbols', 'bookSubscriptionType', bookSubscriptionType) + params['params']['bookSubscriptionType'] = bookSubscriptionType2 + bookUpdateFrequency = None + bookUpdateFrequency2 = None + bookUpdateFrequency, params = self.handle_option_and_params(params, 'watchOrderBook', 'bookUpdateFrequency') + bookUpdateFrequency2, params = self.handle_option_and_params(params, 'watchOrderBookForSymbols', 'bookUpdateFrequency', bookUpdateFrequency) + if bookUpdateFrequency2 is not None: + params['params']['bookSubscriptionType'] = bookUpdateFrequency2 + for i in range(0, len(symbols)): + symbol = symbols[i] + market = self.market(symbol) + currentTopic = 'book' + '.' + market['id'] + '.' + str(limit) + messageHash = 'orderbook:' + market['symbol'] + messageHashes.append(messageHash) + topics.append(currentTopic) + orderbook = await self.watch_public_multiple(messageHashes, topics, params) + return orderbook.limit() + + async def un_watch_order_book_for_symbols(self, symbols: List[str], params={}) -> OrderBook: + """ + unWatches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#book-instrument_name + + :param str[] symbols: unified array of symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.limit]: orderbook limit, default is 50 + :param str [params.bookSubscriptionType]: The subscription type. Allowed values: SNAPSHOT full snapshot. This is the default if not specified. SNAPSHOT_AND_UPDATE delta updates + :param int [params.bookUpdateFrequency]: Book update interval in ms. Allowed values: 100 for snapshot subscription 10 for delta subscription + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + topics = [] + subMessageHashes = [] + messageHashes = [] + limit = self.safe_integer(params, 'limit', 50) + topicParams = self.safe_value(params, 'params') + if topicParams is None: + params['params'] = {} + bookSubscriptionType = None + bookSubscriptionType2 = None + bookSubscriptionType, params = self.handle_option_and_params(params, 'watchOrderBook', 'bookSubscriptionType', 'SNAPSHOT_AND_UPDATE') + bookSubscriptionType2, params = self.handle_option_and_params(params, 'watchOrderBookForSymbols', 'bookSubscriptionType', bookSubscriptionType) + params['params']['bookSubscriptionType'] = bookSubscriptionType2 + bookUpdateFrequency = None + bookUpdateFrequency2 = None + bookUpdateFrequency, params = self.handle_option_and_params(params, 'watchOrderBook', 'bookUpdateFrequency') + bookUpdateFrequency2, params = self.handle_option_and_params(params, 'watchOrderBookForSymbols', 'bookUpdateFrequency', bookUpdateFrequency) + if bookUpdateFrequency2 is not None: + params['params']['bookSubscriptionType'] = bookUpdateFrequency2 + for i in range(0, len(symbols)): + symbol = symbols[i] + market = self.market(symbol) + currentTopic = 'book' + '.' + market['id'] + '.' + str(limit) + messageHash = 'orderbook:' + market['symbol'] + subMessageHashes.append(messageHash) + messageHashes.append('unsubscribe:' + messageHash) + topics.append(currentTopic) + return await self.un_watch_public_multiple('orderbook', symbols, messageHashes, subMessageHashes, topics, params) + + def handle_delta(self, bookside, delta): + price = self.safe_float(delta, 0) + amount = self.safe_float(delta, 1) + count = self.safe_integer(delta, 2) + bookside.storeArray([price, amount, count]) + + def handle_deltas(self, bookside, deltas): + for i in range(0, len(deltas)): + self.handle_delta(bookside, deltas[i]) + + def handle_order_book(self, client: Client, message): + # + # snapshot + # { + # "instrument_name":"LTC_USDT", + # "subscription":"book.LTC_USDT.150", + # "channel":"book", + # "depth":150, + # "data": [ + # { + # "bids": [ + # [122.21, 0.74041, 4] + # ], + # "asks": [ + # [122.29, 0.00002, 1] + # ] + # "t": 1648123943803, + # "s":754560122 + # } + # ] + # } + # update + # { + # "instrument_name":"BTC_USDT", + # "subscription":"book.BTC_USDT.50", + # "channel":"book.update", + # "depth":50, + # "data":[ + # { + # "update":{ + # "asks":[ + # [ + # "43755.46", + # "0.10000", + # "1" + # ], + # ... + # ], + # "bids":[ + # [ + # "43737.46", + # "0.14096", + # "1" + # ], + # ... + # ] + # }, + # "t":1704484068898, + # "tt":1704484068892, + # "u":78795598253024, + # "pu":78795598162080, + # "cs":-781431132 + # } + # ] + # } + # + marketId = self.safe_string(message, 'instrument_name') + market = self.safe_market(marketId) + symbol = market['symbol'] + data = self.safe_value(message, 'data') + data = self.safe_value(data, 0) + timestamp = self.safe_integer(data, 't') + if not (symbol in self.orderbooks): + limit = self.safe_integer(message, 'depth') + self.orderbooks[symbol] = self.counted_order_book({}, limit) + orderbook = self.orderbooks[symbol] + channel = self.safe_string(message, 'channel') + nonce = self.safe_integer_2(data, 'u', 's') + books = data + if channel == 'book': # snapshot + orderbook.reset({}) + orderbook['symbol'] = symbol + orderbook['timestamp'] = timestamp + orderbook['datetime'] = self.iso8601(timestamp) + orderbook['nonce'] = nonce + else: + books = self.safe_value(data, 'update', {}) + previousNonce = self.safe_integer(data, 'pu') + currentNonce = orderbook['nonce'] + if currentNonce != previousNonce: + checksum = self.handle_option('watchOrderBook', 'checksum', True) + if checksum: + raise ChecksumError(self.id + ' ' + self.orderbook_checksum_message(symbol)) + self.handle_deltas(orderbook['asks'], self.safe_value(books, 'asks', [])) + self.handle_deltas(orderbook['bids'], self.safe_value(books, 'bids', [])) + orderbook['nonce'] = nonce + self.orderbooks[symbol] = orderbook + messageHash = 'orderbook:' + symbol + client.resolve(orderbook, messageHash) + + async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#trade-instrument_name + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + return await self.watch_trades_for_symbols([symbol], since, limit, params) + + async def un_watch_trades(self, symbol: str, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#trade-instrument_name + + :param str symbol: unified symbol of the market to fetch trades for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + return await self.un_watch_trades_for_symbols([symbol], params) + + async def watch_trades_for_symbols(self, symbols: List[str], since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#trade-instrument_name + + :param str[] symbols: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + topics = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + market = self.market(symbol) + currentTopic = 'trade' + '.' + market['id'] + topics.append(currentTopic) + trades = await self.watch_public_multiple(topics, topics, params) + if self.newUpdates: + first = self.safe_value(trades, 0) + tradeSymbol = self.safe_string(first, 'symbol') + limit = trades.getLimit(tradeSymbol, limit) + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + async def un_watch_trades_for_symbols(self, symbols: List[str], params={}) -> Any: + """ + get the list of most recent trades for a particular symbol + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#trade-instrument_name + + :param str[] [symbols]: list of unified market symbols to unwatch trades for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + topics = [] + messageHashes = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + market = self.market(symbol) + currentTopic = 'trade' + '.' + market['id'] + messageHashes.append('unsubscribe:trades:' + market['symbol']) + topics.append(currentTopic) + return await self.un_watch_public_multiple('trades', symbols, messageHashes, topics, topics, params) + + def handle_trades(self, client: Client, message): + # + # { + # "code": 0, + # "method": "subscribe", + # "result": { + # "instrument_name": "BTC_USDT", + # "subscription": "trade.BTC_USDT", + # "channel": "trade", + # "data": [ + # { + # "dataTime":1648122434405, + # "d":"2358394540212355488", + # "s":"SELL", + # "p":42980.85, + # "q":0.002325, + # "t":1648122434404, + # "i":"BTC_USDT" + # } + # (...) + # ] + # } + # + channel = self.safe_string(message, 'channel') + marketId = self.safe_string(message, 'instrument_name') + symbolSpecificMessageHash = self.safe_string(message, 'subscription') + market = self.safe_market(marketId) + symbol = market['symbol'] + stored = self.safe_value(self.trades, symbol) + if stored is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + stored = ArrayCache(limit) + self.trades[symbol] = stored + data = self.safe_value(message, 'data', []) + dataLength = len(data) + if dataLength == 0: + return + parsedTrades = self.parse_trades(data, market) + for j in range(0, len(parsedTrades)): + stored.append(parsedTrades[j]) + channelReplaced = channel.replace('.' + marketId, '') + client.resolve(stored, symbolSpecificMessageHash) + client.resolve(stored, channelReplaced) + + async def watch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + watches information on multiple trades made by the user + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#user-trade-instrument_name + + :param str symbol: unified market symbol of the market trades were made in + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trade structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + messageHash = 'user.trade' + messageHash = (messageHash + '.' + market['id']) if (market is not None) else messageHash + trades = await self.watch_private_subscribe(messageHash, params) + if self.newUpdates: + limit = trades.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(trades, symbol, since, limit, True) + + async def watch_ticker(self, symbol: str, params={}) -> Ticker: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#ticker-instrument_name + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + messageHash = 'ticker' + '.' + market['id'] + return await self.watch_public(messageHash, params) + + async def un_watch_ticker(self, symbol: str, params={}) -> Any: + """ + unWatches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#ticker-instrument_name + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + subMessageHash = 'ticker' + '.' + market['id'] + messageHash = 'unsubscribe:ticker:' + market['symbol'] + return await self.un_watch_public_multiple('ticker', [market['symbol']], [messageHash], [subMessageHash], [subMessageHash], params) + + async def watch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#ticker-instrument_name + + :param str[] symbols: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, False) + messageHashes = [] + marketIds = self.market_ids(symbols) + for i in range(0, len(marketIds)): + marketId = marketIds[i] + messageHashes.append('ticker.' + marketId) + url = self.urls['api']['ws']['public'] + id = self.nonce() + request: dict = { + 'method': 'subscribe', + 'params': { + 'channels': messageHashes, + }, + 'nonce': id, + } + ticker = await self.watch_multiple(url, messageHashes, self.extend(request, params), messageHashes) + if self.newUpdates: + result: dict = {} + result[ticker['symbol']] = ticker + return result + return self.filter_by_array(self.tickers, 'symbol', symbols) + + async def un_watch_tickers(self, symbols: Strings = None, params={}) -> Any: + """ + unWatches a price ticker + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#ticker-instrument_name + + :param str[] symbols: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, False) + messageHashes = [] + subMessageHashes = [] + marketIds = self.market_ids(symbols) + for i in range(0, len(marketIds)): + marketId = marketIds[i] + symbol = symbols[i] + subMessageHashes.append('ticker.' + marketId) + messageHashes.append('unsubscribe:ticker:' + symbol) + return await self.un_watch_public_multiple('ticker', symbols, messageHashes, subMessageHashes, subMessageHashes, params) + + def handle_ticker(self, client: Client, message): + # + # { + # "instrument_name": "ETHUSD-PERP", + # "subscription": "ticker.ETHUSD-PERP", + # "channel": "ticker", + # "data": [ + # { + # "h": "2400.20", + # "l": "2277.10", + # "a": "2335.25", + # "c": "-0.0022", + # "b": "2335.10", + # "bs": "5.4000", + # "k": "2335.16", + # "ks": "1.9970", + # "i": "ETHUSD-PERP", + # "v": "1305697.6462", + # "vv": "3058704939.17", + # "oi": "161646.3614", + # "t": 1726069647560 + # } + # ] + # } + # + self.handle_bid_ask(client, message) + messageHash = self.safe_string(message, 'subscription') + marketId = self.safe_string(message, 'instrument_name') + market = self.safe_market(marketId) + data = self.safe_value(message, 'data', []) + for i in range(0, len(data)): + ticker = data[i] + parsed = self.parse_ws_ticker(ticker, market) + symbol = parsed['symbol'] + self.tickers[symbol] = parsed + client.resolve(parsed, messageHash) + + def parse_ws_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "h": "2400.20", + # "l": "2277.10", + # "a": "2335.25", + # "c": "-0.0022", + # "b": "2335.10", + # "bs": "5.4000", + # "k": "2335.16", + # "ks": "1.9970", + # "i": "ETHUSD-PERP", + # "v": "1305697.6462", + # "vv": "3058704939.17", + # "oi": "161646.3614", + # "t": 1726069647560 + # } + # + timestamp = self.safe_integer(ticker, 't') + marketId = self.safe_string(ticker, 'i') + market = self.safe_market(marketId, market, '_') + quote = self.safe_string(market, 'quote') + last = self.safe_string(ticker, 'a') + return self.safe_ticker({ + 'symbol': market['symbol'], + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_number(ticker, 'h'), + 'low': self.safe_number(ticker, 'l'), + 'bid': self.safe_number(ticker, 'b'), + 'bidVolume': self.safe_number(ticker, 'bs'), + 'ask': self.safe_number(ticker, 'k'), + 'askVolume': self.safe_number(ticker, 'ks'), + 'vwap': None, + 'open': None, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': self.safe_string(ticker, 'c'), + 'average': None, + 'baseVolume': self.safe_string(ticker, 'v'), + 'quoteVolume': self.safe_string(ticker, 'vv') if (quote == 'USD') else None, + 'info': ticker, + }, market) + + async def watch_bids_asks(self, symbols: Strings = None, params={}) -> Tickers: + """ + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#ticker-instrument_name + + watches best bid & ask for symbols + :param str[] symbols: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, False) + messageHashes = [] + topics = [] + marketIds = self.market_ids(symbols) + for i in range(0, len(marketIds)): + marketId = marketIds[i] + messageHashes.append('bidask.' + symbols[i]) + topics.append('ticker.' + marketId) + url = self.urls['api']['ws']['public'] + id = self.nonce() + request: dict = { + 'method': 'subscribe', + 'params': { + 'channels': topics, + }, + 'nonce': id, + } + newTickers = await self.watch_multiple(url, messageHashes, self.extend(request, params), messageHashes) + if self.newUpdates: + tickers: dict = {} + tickers[newTickers['symbol']] = newTickers + return tickers + return self.filter_by_array(self.bidsasks, 'symbol', symbols) + + def handle_bid_ask(self, client: Client, message): + data = self.safe_list(message, 'data', []) + ticker = self.safe_dict(data, 0, {}) + parsedTicker = self.parse_ws_bid_ask(ticker) + symbol = parsedTicker['symbol'] + self.bidsasks[symbol] = parsedTicker + messageHash = 'bidask.' + symbol + client.resolve(parsedTicker, messageHash) + + def parse_ws_bid_ask(self, ticker, market=None): + marketId = self.safe_string(ticker, 'i') + market = self.safe_market(marketId, market) + symbol = self.safe_string(market, 'symbol') + timestamp = self.safe_integer(ticker, 't') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'ask': self.safe_string(ticker, 'k'), + 'askVolume': self.safe_string(ticker, 'ks'), + 'bid': self.safe_string(ticker, 'b'), + 'bidVolume': self.safe_string(ticker, 'bs'), + 'info': ticker, + }, market) + + async def watch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + watches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#candlestick-time_frame-instrument_name + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + interval = self.safe_string(self.timeframes, timeframe, timeframe) + messageHash = 'candlestick' + '.' + interval + '.' + market['id'] + ohlcv = await self.watch_public(messageHash, params) + if self.newUpdates: + limit = ohlcv.getLimit(symbol, limit) + return self.filter_by_since_limit(ohlcv, since, limit, 0, True) + + async def un_watch_ohlcv(self, symbol: str, timeframe: str = '1m', params={}) -> Any: + """ + unWatches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#candlestick-time_frame-instrument_name + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + interval = self.safe_string(self.timeframes, timeframe, timeframe) + subMessageHash = 'candlestick' + '.' + interval + '.' + market['id'] + messageHash = 'unsubscribe:ohlcv:' + market['symbol'] + ':' + timeframe + subExtend = { + 'symbolsAndTimeframes': [[market['symbol'], timeframe]], + } + return await self.un_watch_public_multiple('ohlcv', [market['symbol']], [messageHash], [subMessageHash], [subMessageHash], params, subExtend) + + def handle_ohlcv(self, client: Client, message): + # + # { + # "instrument_name": "BTC_USDT", + # "subscription": "candlestick.1m.BTC_USDT", + # "channel": "candlestick", + # "depth": 300, + # "interval": "1m", + # "data": [[Object]] + # } + # + messageHash = self.safe_string(message, 'subscription') + marketId = self.safe_string(message, 'instrument_name') + market = self.safe_market(marketId) + symbol = market['symbol'] + interval = self.safe_string(message, 'interval') + timeframe = self.find_timeframe(interval) + self.ohlcvs[symbol] = self.safe_value(self.ohlcvs, symbol, {}) + stored = self.safe_value(self.ohlcvs[symbol], timeframe) + if stored is None: + limit = self.safe_integer(self.options, 'OHLCVLimit', 1000) + stored = ArrayCacheByTimestamp(limit) + self.ohlcvs[symbol][timeframe] = stored + data = self.safe_value(message, 'data') + for i in range(0, len(data)): + tick = data[i] + parsed = self.parse_ohlcv(tick, market) + stored.append(parsed) + client.resolve(stored, messageHash) + + async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + watches information on multiple orders made by the user + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#user-order-instrument_name + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + messageHash = 'user.order' + messageHash = (messageHash + '.' + market['id']) if (market is not None) else messageHash + orders = await self.watch_private_subscribe(messageHash, params) + if self.newUpdates: + limit = orders.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(orders, symbol, since, limit, True) + + def handle_orders(self, client: Client, message, subscription=None): + # + # { + # "method": "subscribe", + # "result": { + # "instrument_name": "ETH_CRO", + # "subscription": "user.order.ETH_CRO", + # "channel": "user.order", + # "data": [ + # { + # "status": "ACTIVE", + # "side": "BUY", + # "price": 1, + # "quantity": 1, + # "order_id": "366455245775097673", + # "client_oid": "my_order_0002", + # "create_time": 1588758017375, + # "update_time": 1588758017411, + # "type": "LIMIT", + # "instrument_name": "ETH_CRO", + # "cumulative_quantity": 0, + # "cumulative_value": 0, + # "avg_price": 0, + # "fee_currency": "CRO", + # "time_in_force":"GOOD_TILL_CANCEL" + # } + # ], + # "channel": "user.order.ETH_CRO" + # } + # } + # + channel = self.safe_string(message, 'channel') + symbolSpecificMessageHash = self.safe_string(message, 'subscription') + orders = self.safe_value(message, 'data', []) + ordersLength = len(orders) + if ordersLength > 0: + if self.orders is None: + limit = self.safe_integer(self.options, 'ordersLimit', 1000) + self.orders = ArrayCacheBySymbolById(limit) + stored = self.orders + parsed = self.parse_orders(orders) + for i in range(0, len(parsed)): + stored.append(parsed[i]) + client.resolve(stored, symbolSpecificMessageHash) + # non-symbol specific + client.resolve(stored, channel) # channel might have a symbol-specific suffix + client.resolve(stored, 'user.order') + + async def watch_positions(self, symbols: Strings = None, since: Int = None, limit: Int = None, params={}) -> List[Position]: + """ + watch all open positions + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#user-position_balance + + :param str[] [symbols]: list of unified market symbols to watch positions for + :param int [since]: the earliest time in ms to fetch positions for + :param int [limit]: the maximum number of positions to retrieve + :param dict params: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + await self.load_markets() + await self.authenticate() + url = self.urls['api']['ws']['private'] + id = self.nonce() + request: dict = { + 'method': 'subscribe', + 'params': { + 'channels': ['user.position_balance'], + }, + 'nonce': id, + } + messageHash = 'positions' + symbols = self.market_symbols(symbols) + if not self.is_empty(symbols): + messageHash = '::' + ','.join(symbols) + client = self.client(url) + self.set_positions_cache(client, symbols) + fetchPositionsSnapshot = self.handle_option('watchPositions', 'fetchPositionsSnapshot', True) + awaitPositionsSnapshot = self.handle_option('watchPositions', 'awaitPositionsSnapshot', True) + if fetchPositionsSnapshot and awaitPositionsSnapshot and self.positions is None: + snapshot = await client.future('fetchPositionsSnapshot') + return self.filter_by_symbols_since_limit(snapshot, symbols, since, limit, True) + newPositions = await self.watch(url, messageHash, self.extend(request, params)) + if self.newUpdates: + return newPositions + return self.filter_by_symbols_since_limit(self.positions, symbols, since, limit, True) + + def set_positions_cache(self, client: Client, type, symbols: Strings = None): + fetchPositionsSnapshot = self.handle_option('watchPositions', 'fetchPositionsSnapshot', False) + if fetchPositionsSnapshot: + messageHash = 'fetchPositionsSnapshot' + if not (messageHash in client.futures): + client.future(messageHash) + self.spawn(self.load_positions_snapshot, client, messageHash) + else: + self.positions = ArrayCacheBySymbolBySide() + + async def load_positions_snapshot(self, client, messageHash): + positions = await self.fetch_positions() + self.positions = ArrayCacheBySymbolBySide() + cache = self.positions + for i in range(0, len(positions)): + position = positions[i] + contracts = self.safe_number(position, 'contracts', 0) + if contracts > 0: + cache.append(position) + # don't remove the future from the .futures cache + future = client.futures[messageHash] + future.resolve(cache) + client.resolve(cache, 'positions') + + def handle_positions(self, client, message): + # + # { + # "subscription": "user.position_balance", + # "channel": "user.position_balance", + # "data": [{ + # "balances": [{ + # "instrument_name": "USD", + # "quantity": "8.9979961950886", + # "update_timestamp_ms": 1695598760597, + # }], + # "positions": [{ + # "account_id": "96a0edb1-afb5-4c7c-af89-5cb610319e2c", + # "instrument_name": "LTCUSD-PERP", + # "type": "PERPETUAL_SWAP", + # "quantity": "1.8", + # "cost": "114.766", + # "open_position_pnl": "-0.0216206", + # "session_pnl": "0.00962994", + # "update_timestamp_ms": 1695598760597, + # "open_pos_cost": "114.766", + # }], + # }], + # } + # + # each account is connected to a different endpoint + # and has exactly one subscriptionhash which is the account type + data = self.safe_value(message, 'data', []) + firstData = self.safe_value(data, 0, {}) + rawPositions = self.safe_value(firstData, 'positions', []) + if self.positions is None: + self.positions = ArrayCacheBySymbolBySide() + cache = self.positions + newPositions = [] + for i in range(0, len(rawPositions)): + rawPosition = rawPositions[i] + position = self.parse_position(rawPosition) + newPositions.append(position) + cache.append(position) + messageHashes = self.find_message_hashes(client, 'positions::') + for i in range(0, len(messageHashes)): + messageHash = messageHashes[i] + parts = messageHash.split('::') + symbolsString = parts[1] + symbols = symbolsString.split(',') + positions = self.filter_by_array(newPositions, 'symbol', symbols, False) + if not self.is_empty(positions): + client.resolve(positions, messageHash) + client.resolve(newPositions, 'positions') + + async def watch_balance(self, params={}) -> Balances: + """ + watch balance and get the amount of funds available for trading or funds locked in orders + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#user-balance + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + messageHash = 'user.balance' + return await self.watch_private_subscribe(messageHash, params) + + def handle_balance(self, client: Client, message): + # + # { + # "id": 1, + # "method": "subscribe", + # "code": 0, + # "result": { + # "subscription": "user.balance", + # "channel": "user.balance", + # "data": [ + # { + # "total_available_balance": "5.84684368", + # "total_margin_balance": "5.84684368", + # "total_initial_margin": "0", + # "total_maintenance_margin": "0", + # "total_position_cost": "0", + # "total_cash_balance": "6.44412101", + # "total_collateral_value": "5.846843685", + # "total_session_unrealized_pnl": "0", + # "instrument_name": "USD", + # "total_session_realized_pnl": "0", + # "position_balances": [ + # { + # "quantity": "0.0002119875", + # "reserved_qty": "0", + # "collateral_weight": "0.9", + # "collateral_amount": "5.37549592", + # "market_value": "5.97277325", + # "max_withdrawal_balance": "0.00021198", + # "instrument_name": "BTC", + # "hourly_interest_rate": "0" + # }, + # ], + # "total_effective_leverage": "0", + # "position_limit": "3000000", + # "used_position_limit": "0", + # "total_borrow": "0", + # "margin_score": "0", + # "is_liquidating": False, + # "has_risk": False, + # "terminatable": True + # } + # ] + # } + # } + # + messageHash = self.safe_string(message, 'subscription') + data = self.safe_value(message, 'data', []) + positionBalances = self.safe_value(data[0], 'position_balances', []) + self.balance['info'] = data + for i in range(0, len(positionBalances)): + balance = positionBalances[i] + currencyId = self.safe_string(balance, 'instrument_name') + code = self.safe_currency_code(currencyId) + account = self.account() + account['total'] = self.safe_string(balance, 'quantity') + account['used'] = self.safe_string(balance, 'reserved_qty') + self.balance[code] = account + self.balance = self.safe_balance(self.balance) + client.resolve(self.balance, messageHash) + messageHashRequest = self.safe_string(message, 'id') + client.resolve(self.balance, messageHashRequest) + + async def create_order_ws(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}) -> Order: + """ + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#private-create-order + + create a trade order + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + params = self.create_order_request(symbol, type, side, amount, price, params) + request: dict = { + 'method': 'private/create-order', + 'params': params, + } + messageHash = self.nonce() + return await self.watch_private_request(messageHash, request) + + async def edit_order_ws(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}) -> Order: + """ + edit a trade order + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#private-amend-order + + :param str id: order id + :param str symbol: unified market symbol of the order to edit + :param str [type]: not used by cryptocom editOrder + :param str [side]: not used by cryptocom editOrder + :param float amount:(mandatory) how much of the currency you want to trade in units of the base currency + :param float price:(mandatory) the price for the order, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.clientOrderId]: the original client order id of the order to edit, required if id is not provided + :returns dict: an `order structure ` + """ + await self.load_markets() + params = self.edit_order_request(id, symbol, amount, price, params) + request: dict = { + 'method': 'private/amend-order', + 'params': params, + } + messageHash = self.nonce() + return await self.watch_private_request(messageHash, request) + + def handle_order(self, client: Client, message): + # + # { + # "id": 1, + # "method": "private/create-order", + # "code": 0, + # "result": { + # "client_oid": "c5f682ed-7108-4f1c-b755-972fcdca0f02", + # "order_id": "18342311" + # } + # } + # + messageHash = self.safe_string(message, 'id') + rawOrder = self.safe_value(message, 'result', {}) + order = self.parse_order(rawOrder) + client.resolve(order, messageHash) + + async def cancel_order_ws(self, id: str, symbol: Str = None, params={}) -> Order: + """ + cancels an open order + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#private-cancel-order + + :param str id: the order id of the order to cancel + :param str [symbol]: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + await self.load_markets() + params = self.extend({ + 'order_id': id, + }, params) + request: dict = { + 'method': 'private/cancel-order', + 'params': params, + } + messageHash = self.nonce() + return await self.watch_private_request(messageHash, request) + + async def cancel_all_orders_ws(self, symbol: Str = None, params={}): + """ + cancel all open orders + + https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#private-cancel-all-orders + + :param str symbol: unified market symbol of the orders to cancel + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict} Returns exchange raw message {@link https://docs.ccxt.com/#/?id=order-structure: + """ + await self.load_markets() + market = None + request: dict = { + 'method': 'private/cancel-all-orders', + 'params': self.extend({}, params), + } + if symbol is not None: + market = self.market(symbol) + request['params']['instrument_name'] = market['id'] + messageHash = self.nonce() + return await self.watch_private_request(messageHash, request) + + def handle_cancel_all_orders(self, client: Client, message): + # + # { + # "id": 1688914586647, + # "method": "private/cancel-all-orders", + # "code": 0 + # } + # + messageHash = self.safe_string(message, 'id') + client.resolve(message, messageHash) + + async def watch_public(self, messageHash, params={}): + url = self.urls['api']['ws']['public'] + id = self.nonce() + request: dict = { + 'method': 'subscribe', + 'params': { + 'channels': [messageHash], + }, + 'nonce': id, + } + message = self.extend(request, params) + return await self.watch(url, messageHash, message, messageHash) + + async def watch_public_multiple(self, messageHashes, topics, params={}): + url = self.urls['api']['ws']['public'] + id = self.nonce() + request: dict = { + 'method': 'subscribe', + 'params': { + 'channels': topics, + }, + 'nonce': id, + } + message = self.deep_extend(request, params) + return await self.watch_multiple(url, messageHashes, message, messageHashes) + + async def un_watch_public_multiple(self, topic: str, symbols: List[str], messageHashes: List[str], subMessageHashes: List[str], topics: List[str], params={}, subExtend={}): + url = self.urls['api']['ws']['public'] + id = self.nonce() + request: dict = { + 'method': 'unsubscribe', + 'params': { + 'channels': topics, + }, + 'nonce': id, + 'id': str(id), + } + subscription = { + 'id': str(id), + 'topic': topic, + 'symbols': symbols, + 'subMessageHashes': subMessageHashes, + 'messageHashes': messageHashes, + } + message = self.deep_extend(request, params) + return await self.watch_multiple(url, messageHashes, message, messageHashes, self.extend(subscription, subExtend)) + + async def watch_private_request(self, nonce, params={}): + await self.authenticate() + url = self.urls['api']['ws']['private'] + request: dict = { + 'id': nonce, + 'nonce': nonce, + } + message = self.extend(request, params) + return await self.watch(url, str(nonce), message, True) + + async def watch_private_subscribe(self, messageHash, params={}): + await self.authenticate() + url = self.urls['api']['ws']['private'] + id = self.nonce() + request: dict = { + 'method': 'subscribe', + 'params': { + 'channels': [messageHash], + }, + 'nonce': id, + } + message = self.extend(request, params) + return await self.watch(url, messageHash, message, messageHash) + + def handle_error_message(self, client: Client, message) -> Bool: + # + # { + # "id": 0, + # "code": 10004, + # "method": "subscribe", + # "message": "invalid channel {"channels":["trade.BTCUSD-PERP"]}" + # } + # + id = self.safe_string(message, 'id') + errorCode = self.safe_string(message, 'code') + try: + if errorCode and errorCode != '0': + feedback = self.id + ' ' + self.json(message) + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + messageString = self.safe_value(message, 'message') + if messageString is not None: + self.throw_broadly_matched_exception(self.exceptions['broad'], messageString, feedback) + raise ExchangeError(feedback) + return False + except Exception as e: + if isinstance(e, AuthenticationError): + messageHash = 'authenticated' + client.reject(e, messageHash) + if messageHash in client.subscriptions: + del client.subscriptions[messageHash] + else: + client.reject(e, id) + return True + + def handle_subscribe(self, client: Client, message): + methods: dict = { + 'candlestick': self.handle_ohlcv, + 'ticker': self.handle_ticker, + 'trade': self.handle_trades, + 'book': self.handle_order_book, + 'book.update': self.handle_order_book, + 'user.order': self.handle_orders, + 'user.trade': self.handle_trades, + 'user.balance': self.handle_balance, + 'user.position_balance': self.handle_positions, + } + result = self.safe_value_2(message, 'result', 'info') + channel = self.safe_string(result, 'channel') + if (channel is not None) and channel.find('user.trade') > -1: + # channel might be user.trade.BTC_USDT + self.handle_trades(client, result) + if (channel is not None) and channel.startswith('user.order'): + # channel might be user.order.BTC_USDT + self.handle_orders(client, result) + method = self.safe_value(methods, channel) + if method is not None: + method(client, result) + + def handle_message(self, client: Client, message): + # + # ping + # { + # "id": 1587523073344, + # "method": "public/heartbeat", + # "code": 0 + # } + # auth + # {id: 1648132625434, method: "public/auth", code: 0} + # ohlcv + # { + # "code": 0, + # "method": "subscribe", + # "result": { + # "instrument_name": "BTC_USDT", + # "subscription": "candlestick.1m.BTC_USDT", + # "channel": "candlestick", + # "depth": 300, + # "interval": "1m", + # "data": [[Object]] + # } + # } + # ticker + # { + # "info":{ + # "instrument_name":"BTC_USDT", + # "subscription":"ticker.BTC_USDT", + # "channel":"ticker", + # "data":[{}] + # + # handle unsubscribe + # {"id":1725448572836,"method":"unsubscribe","code":0} + # + if self.handle_error_message(client, message): + return + method = self.safe_string(message, 'method') + methods: dict = { + '': self.handle_ping, + 'public/heartbeat': self.handle_ping, + 'public/auth': self.handle_authenticate, + 'private/create-order': self.handle_order, + 'private/amend-order': self.handle_order, + 'private/cancel-order': self.handle_order, + 'private/cancel-all-orders': self.handle_cancel_all_orders, + 'private/close-position': self.handle_order, + 'subscribe': self.handle_subscribe, + 'unsubscribe': self.handle_unsubscribe, + } + callMethod = self.safe_value(methods, method) + if callMethod is not None: + callMethod(client, message) + + async def authenticate(self, params={}): + self.check_required_credentials() + url = self.urls['api']['ws']['private'] + client = self.client(url) + messageHash = 'authenticated' + future = client.reusableFuture(messageHash) + authenticated = self.safe_value(client.subscriptions, messageHash) + if authenticated is None: + method = 'public/auth' + nonce = str(self.nonce()) + auth = method + nonce + self.apiKey + nonce + signature = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256) + request: dict = { + 'id': nonce, + 'nonce': nonce, + 'method': method, + 'api_key': self.apiKey, + 'sig': signature, + } + message = self.extend(request, params) + self.watch(url, messageHash, message, messageHash) + return await future + + def handle_ping(self, client: Client, message): + self.spawn(self.pong, client, message) + + def handle_authenticate(self, client: Client, message): + # + # {id: 1648132625434, method: "public/auth", code: 0} + # + future = self.safe_value(client.futures, 'authenticated') + future.resolve(True) + + def handle_unsubscribe(self, client: Client, message): + id = self.safe_string(message, 'id') + keys = list(client.subscriptions.keys()) + for i in range(0, len(keys)): + messageHash = keys[i] + if not (messageHash in client.subscriptions): + continue + # the previous iteration can have deleted the messageHash from the subscriptions + if messageHash.startswith('unsubscribe'): + subscription = client.subscriptions[messageHash] + subId = self.safe_string(subscription, 'id') + if id != subId: + continue + messageHashes = self.safe_list(subscription, 'messageHashes', []) + subMessageHashes = self.safe_list(subscription, 'subMessageHashes', []) + for j in range(0, len(messageHashes)): + unsubHash = messageHashes[j] + subHash = subMessageHashes[j] + self.clean_unsubscription(client, subHash, unsubHash) + self.clean_cache(subscription) diff --git a/ccxt/pro/deepcoin.py b/ccxt/pro/deepcoin.py new file mode 100644 index 0000000..28217aa --- /dev/null +++ b/ccxt/pro/deepcoin.py @@ -0,0 +1,1166 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolById, ArrayCacheBySymbolBySide, ArrayCacheByTimestamp +from ccxt.base.types import Any, Int, Market, Order, OrderBook, Position, Str, Strings, Ticker, Trade +from ccxt.async_support.base.ws.client import Client +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import BadRequest + + +class deepcoin(ccxt.async_support.deepcoin): + + def describe(self) -> Any: + return self.deep_extend(super(deepcoin, self).describe(), { + 'has': { + 'ws': True, + 'watchTicker': True, + 'watchMarkPrice': False, + 'watchMarkPrices': False, + 'watchTickers': False, + 'watchBidsAsks': False, + 'watchOrderBook': True, + 'watchTrades': True, + 'watchTradesForSymbols': False, + 'watchOrderBookForSymbols': False, + 'watchBalance': False, + 'watchLiquidations': False, + 'watchLiquidationsForSymbols': False, + 'watchMyLiquidations': False, + 'watchMyLiquidationsForSymbols': False, + 'watchOHLCV': True, + 'watchOHLCVForSymbols': False, + 'watchOrders': True, + 'watchMyTrades': True, + 'watchPositions': True, + 'watchFundingRate': False, + 'watchFundingRates': False, + 'createOrderWs': False, + 'editOrderWs': False, + 'cancelOrderWs': False, + 'cancelOrdersWs': False, + 'cancelAllOrdersWs': False, + 'unWatchTicker': True, + 'unWatchTrades': True, + 'unWatchOHLCV': True, + 'unWatchOrderBook': True, + }, + 'urls': { + 'api': { + 'ws': { + 'public': { + 'spot': 'wss://stream.deepcoin.com/streamlet/trade/public/spot?platform=api', + 'swap': 'wss://stream.deepcoin.com/streamlet/trade/public/swap?platform=api', + }, + 'private': 'wss://stream.deepcoin.com/v1/private', + }, + }, + }, + 'options': { + 'lastRequestId': None, + 'listenKey': None, + 'listenKeyExpiryTimestamp': None, + 'authenticate': { + 'method': 'privateGetDeepcoinListenkeyExtend', # refresh existing listen key or 'privateGetDeepcoinListenkeyAcquire' - get a new one + }, + 'timeframes': { + '1m': '1m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1h', + '4h': '4h', + '12h': '12h', + '1d': '1d', + '1w': '1w', + '1M': '1o', + '1y': '1y', + }, + }, + 'streaming': { + 'ping': self.ping, + }, + }) + + def ping(self, client: Client): + url = client.url + if url.find('private') >= 0: + client.lastPong = self.milliseconds() + # prevent automatic disconnects on private channel + return 'ping' + + def handle_pong(self, client: Client, message): + client.lastPong = self.milliseconds() + return message + + def request_id(self): + previousValue = self.safe_integer(self.options, 'lastRequestId', 0) + newValue = self.sum(previousValue, 1) + self.options['lastRequestId'] = newValue + return newValue + + def create_public_request(self, market: Market, requestId: float, topicID: str, suffix: str = '', unWatch: bool = False): + marketId = market['symbol'] # spot markets use symbol with slash + if market['type'] == 'swap': + marketId = market['baseId'] + market['quoteId'] # swap markets use symbol without slash + action = '1' # subscribe + if unWatch: + action = '0' # unsubscribe + request = { + 'sendTopicAction': { + 'Action': action, + 'FilterValue': 'DeepCoin_' + marketId + suffix, + 'LocalNo': requestId, + 'ResumeNo': -1, # -1 from the end, 0 from the beginning + 'TopicID': topicID, + }, + } + return request + + async def watch_public(self, market: Market, messageHash: str, topicID: str, params: dict = {}, suffix: str = '') -> Any: + url = self.urls['api']['ws']['public'][market['type']] + requestId = self.request_id() + request = self.create_public_request(market, requestId, topicID, suffix) + subscription = { + 'subHash': messageHash, + 'id': requestId, + } + return await self.watch(url, messageHash, self.deep_extend(request, params), messageHash, subscription) + + async def un_watch_public(self, market: Market, messageHash: str, topicID: str, params: dict = {}, subscription: dict = {}, suffix: str = '') -> Any: + url = self.urls['api']['ws']['public'][market['type']] + requestId = self.request_id() + client = self.client(url) + existingSubscription = self.safe_dict(client.subscriptions, messageHash) + if existingSubscription is None: + raise BadRequest(self.id + ' no subscription for ' + messageHash) + subId = self.safe_integer(existingSubscription, 'id') + request = self.create_public_request(market, subId, topicID, suffix, True) # unsubscribe message uses the same id original subscribe message + unsubHash = 'unsubscribe::' + messageHash + subscription = self.extend(subscription, { + 'subHash': messageHash, + 'unsubHash': unsubHash, + 'symbols': [market['symbol']], + 'id': requestId, + }) + return await self.watch(url, unsubHash, self.deep_extend(request, params), unsubHash, subscription) + + async def watch_private(self, messageHash: str, params: dict = {}) -> Any: + listenKey = await self.authenticate() + url = self.urls['api']['ws']['private'] + '?listenKey=' + listenKey + return await self.watch(url, messageHash, None, 'private', params) + + async def authenticate(self, params={}): + self.check_required_credentials() + time = self.milliseconds() + listenKeyExpiryTimestamp = self.safe_integer(self.options, 'listenKeyExpiryTimestamp', time) + expired = (time - listenKeyExpiryTimestamp) > 60000 # 1 minute before expiry + listenKey = self.safe_string(self.options, 'listenKey') + response = None + if listenKey is None: + response = await self.privateGetDeepcoinListenkeyAcquire(params) + elif expired: + method = self.safe_string(self.options, 'method', 'privateGetDeepcoinListenkeyExtend') + getNewKey = (method == 'privateGetDeepcoinListenkeyAcquire') + if getNewKey: + response = await self.privateGetDeepcoinListenkeyAcquire(params) + else: + request: dict = { + 'listenkey': listenKey, + } + response = await self.privateGetDeepcoinListenkeyExtend(self.extend(request, params)) + if response is not None: + data = self.safe_dict(response, 'data', {}) + listenKey = self.safe_string(data, 'listenkey') + listenKeyExpiryTimestamp = self.safe_timestamp(data, 'expire_time') + self.options['listenKey'] = listenKey + self.options['listenKeyExpiryTimestamp'] = listenKeyExpiryTimestamp + return listenKey + + async def watch_ticker(self, symbol: str, params={}) -> Ticker: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://www.deepcoin.com/docs/publicWS/latestMarketData + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + messageHash = 'ticker' + '::' + market['symbol'] + return await self.watch_public(market, messageHash, '7', params) + + async def un_watch_ticker(self, symbol: str, params={}) -> Any: + """ + unWatches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://www.deepcoin.com/docs/publicWS/latestMarketData + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + messageHash = 'ticker' + '::' + market['symbol'] + subscription = { + 'topic': 'ticker', + } + return await self.un_watch_public(market, messageHash, '7', params, subscription) + + def handle_ticker(self, client: Client, message): + # + # a: 'PO', + # m: 'Success', + # tt: 1760913034780, + # mt: 1760913034780, + # r: [ + # { + # d: { + # I: 'BTC/USDT', + # U: 1760913034742, + # PF: 0, + # E: 0, + # O: 108479.9, + # H: 109449.9, + # L: 108238, + # V: 789.3424915, + # T: 43003872.3705223, + # N: 109345, + # M: 87294.7, + # D: 0, + # V2: 3086.4496105, + # T2: 332811624.339836, + # F: 0, + # C: 0, + # BP1: 109344.9, + # AP1: 109345.2 + # } + # } + # ] + # + response = self.safe_list(message, 'r', []) + first = self.safe_dict(response, 0, {}) + data = self.safe_dict(first, 'd', {}) + marketId = self.safe_string(data, 'I') + market = self.safe_market(marketId, None, '/') + symbol = self.safe_symbol(marketId, market) + parsedTicker = self.parse_ws_ticker(data, market) + messageHash = 'ticker' + '::' + symbol + self.tickers[symbol] = parsedTicker + client.resolve(parsedTicker, messageHash) + + def parse_ws_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # I: 'BTC/USDT', + # U: 1760913034742, + # PF: 0, + # E: 0, + # O: 108479.9, + # H: 109449.9, + # L: 108238, + # V: 789.3424915, + # T: 43003872.3705223, + # N: 109345, + # M: 87294.7, + # D: 0, + # V2: 3086.4496105, + # T2: 332811624.339836, + # F: 0, + # C: 0, + # BP1: 109344.9, + # AP1: 109345.2 + # } + # + timestamp = self.safe_integer(ticker, 'U') + high = self.safe_number(ticker, 'H') + low = self.safe_number(ticker, 'L') + open = self.safe_number(ticker, 'O') + last = self.safe_number(ticker, 'N') + bid = self.safe_number(ticker, 'BP1') + ask = self.safe_number(ticker, 'AP1') + baseVolume = self.safe_number(ticker, 'V') + quoteVolume = self.safe_number(ticker, 'T') + if market['inverse']: + temp = baseVolume + baseVolume = quoteVolume + quoteVolume = temp + return self.safe_ticker({ + 'symbol': market['symbol'], + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': high, + 'low': low, + 'bid': bid, + 'bidVolume': None, + 'ask': ask, + 'askVolume': None, + 'vwap': None, + 'open': open, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'info': ticker, + }, market) + + async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + watches information on multiple trades made in a market + + https://www.deepcoin.com/docs/publicWS/lastTransactions + + :param str symbol: unified market symbol of the market trades were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of trade structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + messageHash = 'trades' + '::' + market['symbol'] + trades = await self.watch_public(market, messageHash, '2', params) + if self.newUpdates: + limit = trades.getLimit(symbol, limit) + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + async def un_watch_trades(self, symbol: str, params={}): + """ + unWatches the list of most recent trades for a particular symbol + + https://www.deepcoin.com/docs/publicWS/lastTransactions + + :param str symbol: unified symbol of the market to fetch trades for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + messageHash = 'trades' + '::' + market['symbol'] + subscription = { + 'topic': 'trades', + } + return await self.un_watch_public(market, messageHash, '2', params, subscription) + + def handle_trades(self, client: Client, message): + # + # { + # "a": "PMT", + # "b": 0, + # "tt": 1760968672380, + # "mt": 1760968672380, + # "r": [ + # { + # "d": { + # "TradeID": "1001056452325378", + # "I": "BTC/USDT", + # "D": "1", + # "P": 111061, + # "V": 0.00137, + # "T": 1760968672 + # } + # } + # ] + # } + # + response = self.safe_list(message, 'r', []) + first = self.safe_dict(response, 0, {}) + data = self.safe_dict(first, 'd', {}) + marketId = self.safe_string(data, 'I') + market = self.safe_market(marketId, None, '/') + symbol = self.safe_symbol(marketId, market) + if not (symbol in self.trades): + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + self.trades[symbol] = ArrayCache(limit) + strored = self.trades[symbol] + if data is not None: + trade = self.parse_ws_trade(data, market) + strored.append(trade) + messageHash = 'trades' + '::' + symbol + client.resolve(strored, messageHash) + + def parse_ws_trade(self, trade: dict, market: Market = None) -> Trade: + # + # watchTrades + # { + # "TradeID": "1001056452325378", + # "I": "BTC/USDT", + # "D": "1", + # "P": 111061, + # "V": 0.00137, + # "T": 1760968672 + # } + # + # watchMyTrades + # { + # "A": "9256245", + # "CC": "USDT", + # "CP": 0, + # "D": "0", + # "F": 0.152, + # "I": "DOGE/USDT", + # "IT": 1761048103, + # "M": "9256245", + # "OS": "1001437462198486", + # "P": 0.19443, + # "T": 14.77668, + # "TI": "1001056459096708", + # "TT": 1761048103, + # "V": 76, + # "f": "DOGE", + # "l": 1, + # "m": "1", + # "o": "0" + # } + # + direction = self.safe_string(trade, 'D') + timestamp = self.safe_timestamp_2(trade, 'TT', 'T') + matchRole = self.safe_string(trade, 'm') + fee = None + feeCost = self.safe_string(trade, 'F') + if feeCost is not None: + fee = { + 'cost': feeCost, + 'currency': self.safe_currency_code(self.safe_string(trade, 'f')), + } + return self.safe_trade({ + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'id': self.safe_string_2(trade, 'TradeID', 'TI'), + 'order': self.safe_string(trade, 'OS'), + 'type': None, + 'takerOrMaker': self.handle_taker_or_maker(matchRole), + 'side': self.parse_trade_side(direction), + 'price': self.safe_string(trade, 'P'), + 'amount': self.safe_string(trade, 'V'), + 'cost': self.safe_string(trade, 'T'), + 'fee': fee, + }, market) + + def parse_trade_side(self, direction: Str) -> Str: + sides = { + '0': 'buy', + '1': 'sell', + } + return self.safe_string(sides, direction, direction) + + def handle_taker_or_maker(self, matchRole: Str) -> Str: + roles = { + '0': 'maker', + '1': 'taker', + } + return self.safe_string(roles, matchRole, matchRole) + + async def watch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + watches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://www.deepcoin.com/docs/publicWS/KLines + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str [timeframe]: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + timeframes = self.safe_dict(self.options, 'timeframes', {}) + interval = self.safe_string(timeframes, timeframe, timeframe) + messageHash = 'ohlcv' + '::' + symbol + '::' + timeframe + suffix = '_' + interval + ohlcv = await self.watch_public(market, messageHash, '11', params, suffix) + if self.newUpdates: + limit = ohlcv.getLimit(symbol, limit) + return self.filter_by_since_limit(ohlcv, since, limit, 0, True) + + async def un_watch_ohlcv(self, symbol: str, timeframe: str = '1m', params={}) -> Any: + """ + watches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://docs.backpack.exchange/#tag/Streams/Public/K-Line + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str [timeframe]: the length of time each candle represents + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + timeframes = self.safe_dict(self.options, 'timeframes', {}) + interval = self.safe_string(timeframes, timeframe, timeframe) + messageHash = 'ohlcv' + '::' + symbol + '::' + timeframe + suffix = '_' + interval + subscription = { + 'topic': 'ohlcv', + 'symbolsAndTimeframes': [[symbol, timeframe]], + } + return await self.un_watch_public(market, messageHash, '11', params, subscription, suffix) + + def handle_ohlcv(self, client: Client, message): + # + # { + # "a": "PK", + # "tt": 1760972831580, + # "mt": 1760972831580, + # "r": [ + # { + # "d": { + # "I": "BTC/USDT", + # "P": "1m", + # "B": 1760972820, + # "O": 111373, + # "C": 111382.9, + # "H": 111382.9, + # "L": 111373, + # "V": 0.2414172, + # "M": 26888.19693324 + # }, + # "t": "LK" + # } + # ] + # } + # + response = self.safe_list(message, 'r', []) + first = self.safe_dict(response, 0, {}) + data = self.safe_dict(first, 'd', {}) + marketId = self.safe_string(data, 'I') + market = self.safe_market(marketId, None, '/') + symbol = self.safe_symbol(marketId, market) + interval = self.safe_string(data, 'P') + timeframe = self.find_timeframe(interval) + if not (symbol in self.ohlcvs): + self.ohlcvs[symbol] = {} + if not (timeframe in self.ohlcvs[symbol]): + limit = self.safe_integer(self.options, 'OHLCVLimit', 1000) + self.ohlcvs[symbol][timeframe] = ArrayCacheByTimestamp(limit) + stored = self.ohlcvs[symbol][timeframe] + if data is not None: + ohlcv = self.parse_ws_ohlcv(data, market) + stored.append(ohlcv) + messageHash = 'ohlcv' + '::' + symbol + '::' + timeframe + client.resolve(stored, messageHash) + + def parse_ws_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # { + # "I": "BTC/USDT", + # "P": "1m", + # "B": 1760972820, + # "O": 111373, + # "C": 111382.9, + # "H": 111382.9, + # "L": 111373, + # "V": 0.2414172, + # "M": 26888.19693324 + # } + # + return [ + self.safe_timestamp(ohlcv, 'B'), + self.safe_number(ohlcv, 'O'), + self.safe_number(ohlcv, 'H'), + self.safe_number(ohlcv, 'L'), + self.safe_number(ohlcv, 'C'), + self.safe_number(ohlcv, 'V'), + ] + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://www.deepcoin.com/docs/publicWS/25LevelIncrementalMarketData + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return. + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + messageHash = 'orderbook' + '::' + market['symbol'] + suffix = '_0.1' + orderbook = await self.watch_public(market, messageHash, '25', params, suffix) + return orderbook.limit() + + async def un_watch_order_book(self, symbol: str, params={}) -> Any: + """ + unWatches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://www.deepcoin.com/docs/publicWS/25LevelIncrementalMarketData + + :param str symbol: unified array of symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + messageHash = 'orderbook' + '::' + market['symbol'] + suffix = '_0.1' + subscription = { + 'topic': 'orderbook', + } + return await self.un_watch_public(market, messageHash, '25', params, subscription, suffix) + + def handle_order_book(self, client: Client, message): + # + # { + # "a": "PMO", + # "t": "i", # i - update, f - snapshot + # "r": [ + # { + # "d": {"I": "ETH/USDT", "D": "1", "P": 4021, "V": 54.39979} + # }, + # { + # "d": {"I": "ETH/USDT", "D": "0", "P": 4021.1, "V": 49.56724} + # } + # ], + # "tt": 1760975816446, + # "mt": 1760975816446 + # } + # + response = self.safe_list(message, 'r', []) + first = self.safe_dict(response, 0, {}) + data = self.safe_dict(first, 'd', {}) + marketId = self.safe_string(data, 'I') + market = self.safe_market(marketId, None, '/') + symbol = self.safe_symbol(marketId, market) + if not (symbol in self.orderbooks): + self.orderbooks[symbol] = self.order_book() + orderbook = self.orderbooks[symbol] + type = self.safe_string(message, 't') + if orderbook['timestamp'] is None: + if type == 'f': + # snapshot + self.handle_order_book_snapshot(client, message) + else: + # cache the updates until the snapshot is received + orderbook.cache.append(message) + else: + self.handle_order_book_message(client, message, orderbook) + messageHash = 'orderbook' + '::' + symbol + client.resolve(orderbook, messageHash) + + def handle_order_book_snapshot(self, client: Client, message): + entries = self.safe_list(message, 'r', []) + first = self.safe_dict(entries, 0, {}) + data = self.safe_dict(first, 'd', {}) + marketId = self.safe_string(data, 'I') + market = self.safe_market(marketId, None, '/') + symbol = self.safe_symbol(marketId, market) + orderbook = self.orderbooks[symbol] + orderedEntries: dict = { + 'bids': [], + 'asks': [], + } + for i in range(0, len(entries)): + entry = entries[i] + entryData = self.safe_dict(entry, 'd', {}) + side = self.safe_string(entryData, 'D') + price = self.safe_number(entryData, 'P') + volume = self.safe_number(entryData, 'V') + if side == '0': + # bid + orderedEntries['bids'].append([price, volume]) + elif side == '1': + # ask + orderedEntries['asks'].append([price, volume]) + timestamp = self.safe_integer(message, 'mt') + snapshot = self.parse_order_book(orderedEntries, symbol, timestamp) + orderbook.reset(snapshot) + cachedMessages = orderbook.cache + for j in range(0, len(cachedMessages)): + cachedMessage = cachedMessages[j] + self.handle_order_book_message(client, cachedMessage, orderbook) + orderbook.cache = [] + messageHash = 'orderbook' + '::' + symbol + client.resolve(orderbook, messageHash) + + def handle_order_book_message(self, client: Client, message, orderbook): + # { + # "a": "PMO", + # "t": "i", # i - update, f - snapshot + # "r": [ + # { + # "d": {"I": "ETH/USDT", "D": "1", "P": 4021, "V": 54.39979} + # }, + # { + # "d": {"I": "ETH/USDT", "D": "0", "P": 4021.1, "V": 49.56724} + # } + # ], + # "tt": 1760975816446, + # "mt": 1760975816446 + # } + # + timestamp = self.safe_integer(message, 'mt') + if timestamp > orderbook['timestamp']: + response = self.safe_list(message, 'r', []) + self.handle_deltas(orderbook, response) + orderbook['timestamp'] = timestamp + orderbook['datetime'] = self.iso8601(timestamp) + + def handle_delta(self, orderbook, entry): + data = self.safe_dict(entry, 'd', {}) + bids = orderbook['bids'] + asks = orderbook['asks'] + side = self.safe_string(data, 'D') + price = self.safe_number(data, 'P') + volume = self.safe_number(data, 'V') + if side == '0': + # bid + bids.store(price, volume) + elif side == '1': + # ask + asks.store(price, volume) + + async def watch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + watches information on multiple trades made by the user + + https://www.deepcoin.com/docs/privateWS/Trade + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + messageHash = 'myTrades' + await self.load_markets() + if symbol is not None: + symbol = self.symbol(symbol) + messageHash += '::' + symbol + trades = await self.watch_private(messageHash, params) + if self.newUpdates: + limit = trades.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(trades, symbol, since, limit, True) + + def handle_my_trade(self, client: Client, message): + # + # { + # "action": "PushTrade", + # "result": [ + # { + # "table": "Trade", + # "data": { + # "A": "9256245", + # "CC": "USDT", + # "CP": 0, + # "D": "0", + # "F": 0.152, + # "I": "DOGE/USDT", + # "IT": 1761048103, + # "M": "9256245", + # "OS": "1001437462198486", + # "P": 0.19443, + # "T": 14.77668, + # "TI": "1001056459096708", + # "TT": 1761048103, + # "V": 76, + # "f": "DOGE", + # "l": 1, + # "m": "1", + # "o": "0" + # } + # } + # ] + # } + # + result = self.safe_list(message, 'result', []) + first = self.safe_dict(result, 0, {}) + data = self.safe_dict(first, 'data', {}) + marketId = self.safe_string(data, 'I') + market = self.safe_market(marketId, None, '/') + symbol = self.safe_symbol(marketId, market) + messageHash = 'myTrades' + symbolMessageHash = messageHash + '::' + symbol + if (messageHash in client.futures) or (symbolMessageHash in client.futures): + if self.myTrades is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + self.myTrades = ArrayCacheBySymbolById(limit) + stored = self.myTrades + parsed = self.parse_ws_trade(data, market) + stored.append(parsed) + client.resolve(stored, messageHash) + client.resolve(stored, symbolMessageHash) + + async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + watches information on multiple orders made by the user + + https://www.deepcoin.com/docs/privateWS/order + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + messageHash = 'orders' + await self.load_markets() + if symbol is not None: + symbol = self.symbol(symbol) + messageHash += '::' + symbol + orders = await self.watch_private(messageHash, params) + if self.newUpdates: + limit = orders.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(orders, symbol, since, limit, True) + + def handle_order(self, client: Client, message): + # + # { + # "action": "PushOrder", + # "result": [ + # { + # "table": "Order", + # "data": { + # "D": "0", + # "I": "DOGE/USDT", + # "IT": 1761051006, + # "L": "1001437480817468", + # "OPT": "4", + # "OS": "1001437480817468", + # "OT": "0", + # "Or": "1", + # "P": 0.19537, + # "T": 14.84128, + # "U": 1761051006, + # "V": 76, + # "VT": 76, + # "i": 1, + # "l": 1, + # "o": "0", + # "p": "0", + # "t": 0.19528 + # } + # } + # ] + # } + # + result = self.safe_list(message, 'result', []) + first = self.safe_dict(result, 0, {}) + data = self.safe_dict(first, 'data', {}) + marketId = self.safe_string(data, 'I') + market = self.safe_market(marketId, None, '/') + symbol = self.safe_symbol(marketId, market) + messageHash = 'orders' + symbolMessageHash = messageHash + '::' + symbol + if (messageHash in client.futures) or (symbolMessageHash in client.futures): + if self.orders is None: + limit = self.safe_integer(self.options, 'ordersLimit', 1000) + self.orders = ArrayCacheBySymbolById(limit) + parsed = self.parse_ws_order(data, market) + self.orders.append(parsed) + client.resolve(self.orders, messageHash) + client.resolve(self.orders, symbolMessageHash) + + def parse_ws_order(self, order, market: Market = None) -> Order: + # + # { + # "D": "0", + # "I": "DOGE/USDT", + # "IT": 1761051006, + # "L": "1001437480817468", + # "OPT": "4", + # "OS": "1001437480817468", + # "OT": "0", + # "Or": "1", + # "P": 0.19537, + # "T": 14.84128, + # "U": 1761051006, + # "V": 76, + # "VT": 76, + # "i": 1, + # "l": 1, + # "o": "0", + # "p": "0", + # "t": 0.19528 + # } + # + state = self.safe_string(order, 'Or') + timestamp = self.safe_timestamp(order, 'IT') + direction = self.safe_string(order, 'D') + return self.safe_order({ + 'id': self.safe_string(order, 'OS'), + 'clientOrderId': None, + 'datetime': self.iso8601(timestamp), + 'timestamp': timestamp, + 'lastTradeTimestamp': None, + 'lastUpdateTimestamp': self.safe_timestamp(order, 'U'), + 'status': self.parse_ws_order_status(state), + 'symbol': market['symbol'], + 'type': None, + 'timeInForce': None, + 'side': self.parse_trade_side(direction), + 'price': self.safe_string(order, 'P'), + 'average': self.safe_string(order, 't'), + 'amount': self.safe_string(order, 'V'), + 'filled': self.safe_string(order, 'VT'), + 'remaining': None, + 'triggerPrice': None, + 'takeProfitPrice': self.safe_string(order, 'TPT'), + 'stopLossPrice': self.safe_string(order, 'SLT'), + 'cost': self.safe_string(order, 'T'), + 'trades': None, + 'fee': None, + 'reduceOnly': None, + 'postOnly': None, + 'info': order, + }, market) + + def parse_ws_order_status(self, status: Str) -> Str: + statuses = { + '1': 'closed', + '4': 'open', + '6': 'canceled', + } + return self.safe_string(statuses, status, status) + + async def watch_positions(self, symbols: Strings = None, since: Int = None, limit: Int = None, params={}) -> List[Position]: + """ + watch all open positions + + https://www.deepcoin.com/docs/privateWS/Position + + :param str[] [symbols]: list of unified market symbols to watch positions for + :param int [since]: the earliest time in ms to fetch positions for + :param int [limit]: the maximum number of positions to retrieve + :param dict params: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + await self.load_markets() + listenKey = await self.authenticate() + symbols = self.market_symbols(symbols) + messageHash = 'positions' + messageHashes = [] + if symbols is not None: + for i in range(0, len(symbols)): + symbol = symbols[i] + symbolMessageHash = messageHash + '::' + symbol + messageHashes.append(symbolMessageHash) + else: + messageHashes.append(messageHash) + url = self.urls['api']['ws']['private'] + '?listenKey=' + listenKey + positions = await self.watch_multiple(url, messageHashes, params, ['private']) + if self.newUpdates: + return positions + return self.filter_by_symbols_since_limit(self.positions, symbols, since, limit, True) + + def handle_position(self, client: Client, message): + # + # { + # "action": "PushPosition", + # "result": [ + # { + # "table": "Position", + # "data": { + # "A": "9256245", + # "CP": 0, + # "I": "DOGE/USDT", + # "M": "9256245", + # "OP": 0.198845, + # "Po": 151.696, + # "U": 1761058213, + # "i": 1, + # "l": 1, + # "p": "0", + # "u": 0 + # } + # } + # ] + # } + # + result = self.safe_list(message, 'result', []) + first = self.safe_dict(result, 0, {}) + data = self.safe_dict(first, 'data', {}) + marketId = self.safe_string(data, 'I') + market = self.safe_market(marketId, None, '/') + symbol = self.safe_symbol(marketId, market) + messageHash = 'positions' + symbolMessageHash = messageHash + '::' + symbol + if (messageHash in client.futures) or (symbolMessageHash in client.futures): + if self.positions is None: + self.positions = ArrayCacheBySymbolBySide() + parsed = self.parse_ws_position(data, market) + self.positions.append(parsed) + client.resolve(self.positions, messageHash) + client.resolve(self.positions, symbolMessageHash) + + def parse_ws_position(self, position, market: Market = None) -> Position: + # + # { + # "A": "9256245", + # "CP": 0, + # "I": "DOGE/USDT", + # "M": "9256245", + # "OP": 0.198845, + # "Po": 151.696, + # "U": 1761058213, + # "i": 1, + # "l": 1, + # "p": "0", + # "u": 0 + # } + # + timestamp = self.safe_integer(position, 'U') + direction = self.safe_string(position, 'p') + marginMode = self.safe_string(position, 'i') + return self.safe_position({ + 'symbol': market['symbol'], + 'id': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'contracts': self.safe_string(position, 'Po'), + 'contractSize': None, + 'side': self.parse_position_side(direction), + 'notional': None, + 'leverage': self.omit_zero(self.safe_string(position, 'l')), + 'unrealizedPnl': None, + 'realizedPnl': None, + 'collateral': None, + 'entryPrice': self.safe_string(position, 'OP'), + 'markPrice': None, + 'liquidationPrice': None, + 'marginMode': self.parse_ws_margin_mode(marginMode), + 'hedged': True, + 'maintenanceMargin': self.safe_string(position, 'u'), + 'maintenanceMarginPercentage': None, + 'initialMargin': None, + 'initialMarginPercentage': None, + 'marginRatio': None, + 'lastUpdateTimestamp': None, + 'lastPrice': None, + 'stopLossPrice': None, + 'takeProfitPrice': None, + 'percentage': None, + 'info': position, + }) + + def parse_position_side(self, direction: Str) -> Str: + if direction is None: + return direction + directions = { + '0': 'long', + '1': 'short', + } + return self.safe_string(directions, direction, direction) + + def parse_ws_margin_mode(self, marginMode: Str) -> Str: + if marginMode is None: + return marginMode + modes = { + '0': 'isolated', + '1': 'cross', + } + return self.safe_string(modes, marginMode, marginMode) + + def handle_message(self, client: Client, message): + if message == 'pong': + self.handle_pong(client, message) + else: + m = self.safe_string(message, 'm') + if (m is not None) and (m != 'Success'): + self.handle_error_message(client, message) + action = self.safe_string_2(message, 'a', 'action') + if action == 'RecvTopicAction': + self.handle_subscription_status(client, message) + elif action == 'PO': + self.handle_ticker(client, message) + elif action == 'PMT': + self.handle_trades(client, message) + elif action == 'PK': + self.handle_ohlcv(client, message) + elif action == 'PMO': + self.handle_order_book(client, message) + elif action == 'PushTrade': + self.handle_my_trade(client, message) + elif action == 'PushOrder': + self.handle_order(client, message) + elif action == 'PushPosition': + self.handle_position(client, message) + + def handle_subscription_status(self, client: Client, message): + # + # { + # "a": "RecvTopicAction", + # "m": "Success", + # "r": [ + # { + # "d": { + # "A": "0", + # "L": 1, + # "T": "7", + # "F": "DeepCoin_BTC/USDT", + # "R": -1 + # } + # } + # ] + # } + # + response = self.safe_list(message, 'r', []) + first = self.safe_dict(response, 0, {}) + data = self.safe_dict(first, 'd', {}) + action = self.safe_string(data, 'A') # 1 = subscribe, 0 = unsubscribe + if action == '0': + subscriptionsById = self.index_by(client.subscriptions, 'id') + subId = self.safe_integer(data, 'L') + subscription = self.safe_dict(subscriptionsById, subId, {}) # original watch subscription + subHash = self.safe_string(subscription, 'subHash') + unsubHash = 'unsubscribe::' + subHash + unsubsciption = self.safe_dict(client.subscriptions, unsubHash, {}) # unWatch subscription + self.handle_un_subscription(client, unsubsciption) + + def handle_un_subscription(self, client: Client, subscription: dict): + subHash = self.safe_string(subscription, 'subHash') + unsubHash = self.safe_string(subscription, 'unsubHash') + self.clean_unsubscription(client, subHash, unsubHash) + self.clean_cache(subscription) + + def handle_error_message(self, client: Client, message): + # + # { + # "a": "RecvTopicAction", + # "m": "subscription cluster does not "exist": BTC/USD", + # "r": [ + # { + # "d": { + # "A": "1", + # "L": 1, + # "T": "7", + # "F": "DeepCoin_BTC/USD", + # "R": -1 + # } + # } + # ] + # } + # + messageText = self.safe_string(message, 'm', '') + response = self.safe_list(message, 'r', []) + first = self.safe_dict(response, 0, {}) + data = self.safe_dict(first, 'd', {}) + requestId = self.safe_integer(data, 'L') + subscriptionsById = self.index_by(client.subscriptions, 'id') + subscription = self.safe_dict(subscriptionsById, requestId, {}) + messageHash = self.safe_string(subscription, 'subHash') + feedback = self.id + ' ' + self.json(message) + try: + self.throw_exactly_matched_exception(self.exceptions['exact'], messageText, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], messageText, feedback) + raise ExchangeError(feedback) + except Exception as e: + client.reject(e, messageHash) diff --git a/ccxt/pro/defx.py b/ccxt/pro/defx.py new file mode 100644 index 0000000..40fc803 --- /dev/null +++ b/ccxt/pro/defx.py @@ -0,0 +1,831 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolById, ArrayCacheByTimestamp +from ccxt.base.types import Any, Balances, Int, Order, OrderBook, Position, Str, Strings, Ticker, Tickers, Trade +from ccxt.async_support.base.ws.client import Client +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import ArgumentsRequired + + +class defx(ccxt.async_support.defx): + + def describe(self) -> Any: + return self.deep_extend(super(defx, self).describe(), { + 'has': { + 'ws': True, + 'watchBalance': True, + 'watchTicker': True, + 'watchTickers': True, + 'watchBidsAsks': True, + 'watchTrades': True, + 'watchTradesForSymbols': True, + 'watchMyTrades': False, + 'watchOrders': True, + 'watchOrderBook': True, + 'watchOrderBookForSymbols': True, + 'watchOHLCV': True, + 'watchOHLCVForSymbols': True, + }, + 'urls': { + 'test': { + 'ws': { + 'public': 'wss://stream.testnet.defx.com/pricefeed', + 'private': 'wss://ws.testnet.defx.com/user', + }, + }, + 'api': { + 'ws': { + 'public': 'wss://marketfeed.api.defx.com/pricefeed', + 'private': 'wss://userfeed.api.defx.com/user', + }, + }, + }, + 'options': { + 'listenKeyRefreshRate': 3540000, # 1 hour(59 mins so we have 1min to renew the token) + 'ws': { + 'timeframes': { + '1m': '1m', + '3m': '3m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1h', + '2h': '2h', + '4h': '4h', + '12h': '12h', + '1d': '1d', + '1w': '1w', + '1M': '1M', + }, + }, + }, + 'streaming': { + }, + 'exceptions': { + }, + }) + + async def watch_public(self, topics, messageHashes, params={}): + await self.load_markets() + url = self.urls['api']['ws']['public'] + request: dict = { + 'method': 'SUBSCRIBE', + 'topics': topics, + } + message = self.extend(request, params) + return await self.watch_multiple(url, messageHashes, message, messageHashes) + + async def un_watch_public(self, topics, messageHashes, params={}): + await self.load_markets() + url = self.urls['api']['ws']['public'] + request: dict = { + 'method': 'UNSUBSCRIBE', + 'topics': topics, + } + message = self.extend(request, params) + return await self.watch_multiple(url, messageHashes, message, messageHashes) + + async def watch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + watches historical candlestick data containing the open, high, low, close price, and the volume of a market + + https://www.postman.com/defxcode/defx-public-apis/collection/667939a1b5d8069c13d614e9 + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + result = await self.watch_ohlcv_for_symbols([[symbol, timeframe]], since, limit, params) + return result[symbol][timeframe] + + async def un_watch_ohlcv(self, symbol: str, timeframe: str = '1m', params={}) -> Any: + """ + watches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://www.postman.com/defxcode/defx-public-apis/collection/667939a1b5d8069c13d614e9 + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + return await self.un_watch_ohlcv_for_symbols([[symbol, timeframe]], params) + + async def watch_ohlcv_for_symbols(self, symbolsAndTimeframes: List[List[str]], since: Int = None, limit: Int = None, params={}): + """ + watches historical candlestick data containing the open, high, low, close price, and the volume of a market + + https://www.postman.com/defxcode/defx-public-apis/collection/667939a1b5d8069c13d614e9 + + :param str[][] symbolsAndTimeframes: array of arrays containing unified symbols and timeframes to fetch OHLCV data for, example [['BTC/USDT', '1m'], ['LTC/USDT', '5m']] + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + symbolsLength = len(symbolsAndTimeframes) + if symbolsLength == 0 or not isinstance(symbolsAndTimeframes[0], list): + raise ArgumentsRequired(self.id + " watchOHLCVForSymbols() requires a an array of symbols and timeframes, like [['BTC/USDT', '1m'], ['LTC/USDT', '5m']]") + await self.load_markets() + topics = [] + messageHashes = [] + for i in range(0, len(symbolsAndTimeframes)): + symbolAndTimeframe = symbolsAndTimeframes[i] + marketId = self.safe_string(symbolAndTimeframe, 0) + market = self.market(marketId) + tf = self.safe_string(symbolAndTimeframe, 1) + interval = self.safe_string(self.timeframes, tf, tf) + topics.append('symbol:' + market['id'] + ':ohlc:' + interval) + messageHashes.append('candles:' + interval + ':' + market['symbol']) + symbol, timeframe, candles = await self.watch_public(topics, messageHashes, params) + if self.newUpdates: + limit = candles.getLimit(symbol, limit) + filtered = self.filter_by_since_limit(candles, since, limit, 0, True) + return self.create_ohlcv_object(symbol, timeframe, filtered) + + async def un_watch_ohlcv_for_symbols(self, symbolsAndTimeframes: List[List[str]], params={}) -> Any: + """ + unWatches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://www.postman.com/defxcode/defx-public-apis/collection/667939a1b5d8069c13d614e9 + + :param str[][] symbolsAndTimeframes: array of arrays containing unified symbols and timeframes to fetch OHLCV data for, example [['BTC/USDT', '1m'], ['LTC/USDT', '5m']] + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + symbolsLength = len(symbolsAndTimeframes) + if symbolsLength == 0 or not isinstance(symbolsAndTimeframes[0], list): + raise ArgumentsRequired(self.id + " unWatchOHLCVForSymbols() requires a an array of symbols and timeframes, like [['BTC/USDT', '1m'], ['LTC/USDT', '5m']]") + await self.load_markets() + topics = [] + messageHashes = [] + for i in range(0, len(symbolsAndTimeframes)): + symbolAndTimeframe = symbolsAndTimeframes[i] + marketId = self.safe_string(symbolAndTimeframe, 0) + market = self.market(marketId) + tf = self.safe_string(symbolAndTimeframe, 1) + interval = self.safe_string(self.timeframes, tf, tf) + topics.append('symbol:' + market['id'] + ':ohlc:' + interval) + messageHashes.append('candles:' + interval + ':' + market['symbol']) + return await self.un_watch_public(topics, messageHashes, params) + + def handle_ohlcv(self, client: Client, message): + # + # { + # "topic": "symbol:BTC_USDC:ohlc:3m", + # "event": "ohlc", + # "timestamp": 1730794277104, + # "data": { + # "symbol": "BTC_USDC", + # "window": "3m", + # "open": "57486.90000000", + # "high": "57486.90000000", + # "low": "57486.90000000", + # "close": "57486.90000000", + # "volume": "0.000", + # "quoteAssetVolume": "0.00000000", + # "takerBuyAssetVolume": "0.000", + # "takerBuyQuoteAssetVolume": "0.00000000", + # "numberOfTrades": 0, + # "start": 1730794140000, + # "end": 1730794320000, + # "isClosed": False + # } + # } + # + data = self.safe_dict(message, 'data', {}) + marketId = self.safe_string(data, 'symbol') + market = self.market(marketId) + symbol = market['symbol'] + timeframe = self.safe_string(data, 'window') + if not (symbol in self.ohlcvs): + self.ohlcvs[symbol] = {} + if not (timeframe in self.ohlcvs[symbol]): + limit = self.safe_integer(self.options, 'OHLCVLimit', 1000) + stored = ArrayCacheByTimestamp(limit) + self.ohlcvs[symbol][timeframe] = stored + ohlcv = self.ohlcvs[symbol][timeframe] + parsed = self.parse_ohlcv(data) + ohlcv.append(parsed) + messageHash = 'candles:' + timeframe + ':' + symbol + client.resolve([symbol, timeframe, ohlcv], messageHash) + + async def watch_ticker(self, symbol: str, params={}) -> Ticker: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://www.postman.com/defxcode/defx-public-apis/collection/667939a1b5d8069c13d614e9 + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + topic = 'symbol:' + market['id'] + ':24hrTicker' + messageHash = 'ticker:' + symbol + return await self.watch_public([topic], [messageHash], params) + + async def un_watch_ticker(self, symbol: str, params={}) -> Any: + """ + unWatches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://www.postman.com/defxcode/defx-public-apis/collection/667939a1b5d8069c13d614e9 + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.channel]: the channel to subscribe to, tickers by default. Can be tickers, sprd-tickers, index-tickers, block-tickers + :returns dict: a `ticker structure ` + """ + return await self.un_watch_tickers([symbol], params) + + async def watch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list + + https://www.postman.com/defxcode/defx-public-apis/collection/667939a1b5d8069c13d614e9 + + :param str[] [symbols]: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, False) + topics = [] + messageHashes = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + marketId = self.market_id(symbol) + topics.append('symbol:' + marketId + ':24hrTicker') + messageHashes.append('ticker:' + symbol) + await self.watch_public(topics, messageHashes, params) + return self.filter_by_array(self.tickers, 'symbol', symbols) + + async def un_watch_tickers(self, symbols: Strings = None, params={}) -> Any: + """ + unWatches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list + + https://www.postman.com/defxcode/defx-public-apis/collection/667939a1b5d8069c13d614e9 + + :param str[] [symbols]: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, False) + topics = [] + messageHashes = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + marketId = self.market_id(symbol) + topics.append('symbol:' + marketId + ':24hrTicker') + messageHashes.append('ticker:' + symbol) + return await self.un_watch_public(topics, messageHashes, params) + + def handle_ticker(self, client: Client, message): + # + # { + # "topic": "symbol:BTC_USDC:24hrTicker", + # "event": "24hrTicker", + # "timestamp": 1730862543095, + # "data": { + # "symbol": "BTC_USDC", + # "priceChange": "17114.70000000", + # "priceChangePercent": "29.77", + # "weightedAvgPrice": "6853147668", + # "lastPrice": "74378.90000000", + # "lastQty": "0.107", + # "bestBidPrice": "61987.60000000", + # "bestBidQty": "0.005", + # "bestAskPrice": "84221.60000000", + # "bestAskQty": "0.015", + # "openPrice": "57486.90000000", + # "highPrice": "88942.60000000", + # "lowPrice": "47364.20000000", + # "volume": "28.980", + # "quoteVolume": "1986042.19424035", + # "openTime": 1730776080000, + # "closeTime": 1730862540000, + # "openInterestBase": "67.130", + # "openInterestQuote": "5008005.40800000" + # } + # } + # + self.handle_bid_ask(client, message) + data = self.safe_dict(message, 'data', {}) + parsedTicker = self.parse_ticker(data) + symbol = parsedTicker['symbol'] + timestamp = self.safe_integer(message, 'timestamp') + parsedTicker['timestamp'] = timestamp + parsedTicker['datetime'] = self.iso8601(timestamp) + self.tickers[symbol] = parsedTicker + messageHash = 'ticker:' + symbol + client.resolve(parsedTicker, messageHash) + + async def watch_bids_asks(self, symbols: Strings = None, params={}) -> Tickers: + """ + watches best bid & ask for symbols + + https://www.postman.com/defxcode/defx-public-apis/collection/667939a1b5d8069c13d614e9 + + :param str[] symbols: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, False) + topics = [] + messageHashes = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + marketId = self.market_id(symbol) + topics.append('symbol:' + marketId + ':24hrTicker') + messageHashes.append('bidask:' + symbol) + await self.watch_public(topics, messageHashes, params) + return self.filter_by_array(self.bidsasks, 'symbol', symbols) + + def handle_bid_ask(self, client: Client, message): + data = self.safe_dict(message, 'data', {}) + parsedTicker = self.parse_ws_bid_ask(data) + symbol = parsedTicker['symbol'] + timestamp = self.safe_integer(message, 'timestamp') + parsedTicker['timestamp'] = timestamp + parsedTicker['datetime'] = self.iso8601(timestamp) + self.bidsasks[symbol] = parsedTicker + messageHash = 'bidask:' + symbol + client.resolve(parsedTicker, messageHash) + + def parse_ws_bid_ask(self, ticker, market=None): + marketId = self.safe_string(ticker, 'symbol') + market = self.safe_market(marketId, market) + symbol = self.safe_string(market, 'symbol') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': None, + 'datetime': None, + 'ask': self.safe_string(ticker, 'bestAskPrice'), + 'askVolume': self.safe_string(ticker, 'bestAskQty'), + 'bid': self.safe_string(ticker, 'bestBidPrice'), + 'bidVolume': self.safe_string(ticker, 'bestBidQty'), + 'info': ticker, + }, market) + + async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + watches information on multiple trades made in a market + + https://www.postman.com/defxcode/defx-public-apis/collection/667939a1b5d8069c13d614e9 + + :param str symbol: unified symbol of the market to fetch the ticker for + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trade structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + return await self.watch_trades_for_symbols([symbol], since, limit, params) + + async def un_watch_trades(self, symbol: str, params={}) -> Any: + """ + unWatches from the stream channel + + https://www.postman.com/defxcode/defx-public-apis/collection/667939a1b5d8069c13d614e9 + + :param str symbol: unified symbol of the market to fetch trades for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + return await self.un_watch_trades_for_symbols([symbol], params) + + async def watch_trades_for_symbols(self, symbols: List[str], since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + watches information on multiple trades made in a market + + https://www.postman.com/defxcode/defx-public-apis/collection/667939a1b5d8069c13d614e9 + + :param str[] symbols: unified symbol of the market to fetch trades for + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trade structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + symbolsLength = len(symbols) + if symbolsLength == 0: + raise ArgumentsRequired(self.id + ' watchTradesForSymbols() requires a non-empty array of symbols') + topics = [] + messageHashes = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + marketId = self.market_id(symbol) + topics.append('symbol:' + marketId + ':trades') + messageHashes.append('trade:' + symbol) + trades = await self.watch_public(topics, messageHashes, params) + if self.newUpdates: + first = self.safe_value(trades, 0) + tradeSymbol = self.safe_string(first, 'symbol') + limit = trades.getLimit(tradeSymbol, limit) + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + async def un_watch_trades_for_symbols(self, symbols: List[str], params={}) -> Any: + """ + unWatches from the stream channel + + https://www.postman.com/defxcode/defx-public-apis/collection/667939a1b5d8069c13d614e9 + + :param str[] symbols: unified symbol of the market to fetch trades for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + symbolsLength = len(symbols) + if symbolsLength == 0: + raise ArgumentsRequired(self.id + ' unWatchTradesForSymbols() requires a non-empty array of symbols') + topics = [] + messageHashes = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + marketId = self.market_id(symbol) + topics.append('symbol:' + marketId + ':trades') + messageHashes.append('trade:' + symbol) + return await self.un_watch_public(topics, messageHashes, params) + + def handle_trades(self, client: Client, message): + # + # { + # "topic": "symbol:SOL_USDC:trades", + # "event": "trades", + # "timestamp": 1730967426331, + # "data": { + # "buyerMaker": True, + # "price": "188.38700000", + # "qty": "1.00", + # "symbol": "SOL_USDC", + # "timestamp": 1730967426328 + # } + # } + # + data = self.safe_dict(message, 'data', {}) + parsedTrade = self.parse_trade(data) + symbol = parsedTrade['symbol'] + if not (symbol in self.trades): + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + stored = ArrayCache(limit) + self.trades[symbol] = stored + trades = self.trades[symbol] + trades.append(parsedTrade) + messageHash = 'trade:' + symbol + client.resolve(trades, messageHash) + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://www.postman.com/defxcode/defx-public-apis/collection/667939a1b5d8069c13d614e9 + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + return await self.watch_order_book_for_symbols([symbol], limit, params) + + async def un_watch_order_book(self, symbol: str, params={}) -> Any: + """ + unWatches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://www.postman.com/defxcode/defx-public-apis/collection/667939a1b5d8069c13d614e9 + + :param str symbol: unified array of symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + return await self.un_watch_order_book_for_symbols([symbol], params) + + async def watch_order_book_for_symbols(self, symbols: List[str], limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://www.postman.com/defxcode/defx-public-apis/collection/667939a1b5d8069c13d614e9 + + :param str[] symbols: unified array of symbols + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + symbolsLength = len(symbols) + if symbolsLength == 0: + raise ArgumentsRequired(self.id + ' watchOrderBookForSymbols() requires a non-empty array of symbols') + symbols = self.market_symbols(symbols) + topics = [] + messageHashes = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + marketId = self.market_id(symbol) + topics.append('symbol:' + marketId + ':depth:20:0.001') + messageHashes.append('orderbook:' + symbol) + orderbook = await self.watch_public(topics, messageHashes, params) + return orderbook.limit() + + async def un_watch_order_book_for_symbols(self, symbols: List[str], params={}) -> Any: + """ + unWatches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://www.postman.com/defxcode/defx-public-apis/collection/667939a1b5d8069c13d614e9 + + :param str[] symbols: unified array of symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + symbolsLength = len(symbols) + if symbolsLength == 0: + raise ArgumentsRequired(self.id + ' unWatchOrderBookForSymbols() requires a non-empty array of symbols') + symbols = self.market_symbols(symbols) + topics = [] + messageHashes = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + marketId = self.market_id(symbol) + topics.append('symbol:' + marketId + ':depth:20:0.001') + messageHashes.append('orderbook:' + symbol) + return await self.un_watch_public(topics, messageHashes, params) + + def handle_order_book(self, client: Client, message): + # + # { + # "topic": "symbol:SOL_USDC:depth:20:0.01", + # "event": "depth", + # "timestamp": 1731030695319, + # "data": { + # "symbol": "SOL_USDC", + # "timestamp": 1731030695319, + # "lastTradeTimestamp": 1731030275258, + # "level": "20", + # "slab": "0.01", + # "bids": [ + # { + # "price": "198.27000000", + # "qty": "1.52" + # } + # ], + # "asks": [ + # { + # "price": "198.44000000", + # "qty": "6.61" + # } + # ] + # } + # } + # + data = self.safe_dict(message, 'data', {}) + marketId = self.safe_string(data, 'symbol') + market = self.market(marketId) + symbol = market['symbol'] + timestamp = self.safe_integer(data, 'timestamp') + snapshot = self.parse_order_book(data, symbol, timestamp, 'bids', 'asks', 'price', 'qty') + if not (symbol in self.orderbooks): + ob = self.order_book(snapshot) + self.orderbooks[symbol] = ob + orderbook = self.orderbooks[symbol] + orderbook.reset(snapshot) + messageHash = 'orderbook:' + symbol + client.resolve(orderbook, messageHash) + + async def keep_alive_listen_key(self, params={}): + listenKey = self.safe_string(self.options, 'listenKey') + if listenKey is None: + # A network error happened: we can't renew a listen key that does not exist. + return + try: + await self.v1PrivatePutApiUsersSocketListenKeysListenKey({'listenKey': listenKey}) # self.extend the expiry + except Exception as error: + url = self.urls['api']['ws']['private'] + '?listenKey=' + listenKey + client = self.client(url) + messageHashes = list(client.futures.keys()) + for j in range(0, len(messageHashes)): + messageHash = messageHashes[j] + client.reject(error, messageHash) + self.options['listenKey'] = None + self.options['lastAuthenticatedTime'] = 0 + return + # whether or not to schedule another listenKey keepAlive request + listenKeyRefreshRate = self.safe_integer(self.options, 'listenKeyRefreshRate', 3540000) + self.delay(listenKeyRefreshRate, self.keep_alive_listen_key, params) + + async def authenticate(self, params={}): + time = self.milliseconds() + lastAuthenticatedTime = self.safe_integer(self.options, 'lastAuthenticatedTime', 0) + listenKeyRefreshRate = self.safe_integer(self.options, 'listenKeyRefreshRate', 3540000) # 1 hour + if time - lastAuthenticatedTime > listenKeyRefreshRate: + response = await self.v1PrivatePostApiUsersSocketListenKeys() + self.options['listenKey'] = self.safe_string(response, 'listenKey') + self.options['lastAuthenticatedTime'] = time + self.delay(listenKeyRefreshRate, self.keep_alive_listen_key, params) + + async def watch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://www.postman.com/defxcode/defx-public-apis/ws-raw-request/667939b2f00f79161bb47809 + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + await self.authenticate() + baseUrl = self.urls['api']['ws']['private'] + messageHash = 'WALLET_BALANCE_UPDATE' + url = baseUrl + '?listenKey=' + self.options['listenKey'] + return await self.watch(url, messageHash, None, messageHash) + + def handle_balance(self, client: Client, message): + # + # { + # "event": "WALLET_BALANCE_UPDATE", + # "timestamp": 1711015961397, + # "data": { + # "asset": "USDC", "balance": "27.64712963" + # } + # } + # + messageHash = self.safe_string(message, 'event') + data = self.safe_dict(message, 'data', []) + timestamp = self.safe_integer(message, 'timestamp') + if self.balance is None: + self.balance = {} + self.balance['info'] = data + self.balance['timestamp'] = timestamp + self.balance['datetime'] = self.iso8601(timestamp) + currencyId = self.safe_string(data, 'asset') + code = self.safe_currency_code(currencyId) + account = self.balance[code] if (code in self.balance) else self.account() + account['free'] = self.safe_string(data, 'balance') + self.balance[code] = account + self.balance = self.safe_balance(self.balance) + client.resolve(self.balance, messageHash) + + async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + watches information on multiple orders made by the user + + https://www.postman.com/defxcode/defx-public-apis/ws-raw-request/667939b2f00f79161bb47809 + + :param str [symbol]: unified market symbol of the market the orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + await self.authenticate() + baseUrl = self.urls['api']['ws']['private'] + messageHash = 'orders' + if symbol is not None: + market = self.market(symbol) + messageHash += ':' + market['symbol'] + url = baseUrl + '?listenKey=' + self.options['listenKey'] + orders = await self.watch(url, messageHash, None, messageHash) + if self.newUpdates: + limit = orders.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(orders, symbol, since, limit, True) + + def handle_order(self, client: Client, message): + # + # { + # "event": "ORDER_UPDATE", + # "timestamp": 1731417961446, + # "data": { + # "orderId": "766738557656630928", + # "symbol": "SOL_USDC", + # "side": "SELL", + # "type": "MARKET", + # "status": "FILLED", + # "clientOrderId": "0193208d-717b-7811-a80e-c036e220ad9b", + # "reduceOnly": False, + # "postOnly": False, + # "timeInForce": "GTC", + # "isTriggered": False, + # "createdAt": "2024-11-12T13:26:00.829Z", + # "updatedAt": "2024-11-12T13:26:01.436Z", + # "avgPrice": "209.60000000", + # "cumulativeQuote": "104.80000000", + # "totalFee": "0.05764000", + # "executedQty": "0.50", + # "origQty": "0.50", + # "role": "TAKER", + # "pnl": "0.00000000", + # "lastFillPnL": "0.00000000", + # "lastFillPrice": "209.60000000", + # "lastFillQty": "0.50", + # "linkedOrderParentType": null, + # "workingType": null + # } + # } + # + channel = 'orders' + data = self.safe_dict(message, 'data', {}) + if self.orders is None: + limit = self.safe_integer(self.options, 'ordersLimit', 1000) + self.orders = ArrayCacheBySymbolById(limit) + orders = self.orders + parsedOrder = self.parse_order(data) + orders.append(parsedOrder) + messageHash = channel + ':' + parsedOrder['symbol'] + client.resolve(orders, channel) + client.resolve(orders, messageHash) + + async def watch_positions(self, symbols: Strings = None, since: Int = None, limit: Int = None, params={}) -> List[Position]: + """ + watch all open positions + + https://www.postman.com/defxcode/defx-public-apis/ws-raw-request/667939b2f00f79161bb47809 + + :param str[]|None symbols: list of unified market symbols + :param number [since]: since timestamp + :param number [limit]: limit + :param dict params: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + await self.load_markets() + await self.authenticate() + symbols = self.market_symbols(symbols) + baseUrl = self.urls['api']['ws']['private'] + channel = 'positions' + url = baseUrl + '?listenKey=' + self.options['listenKey'] + newPosition = None + if symbols is not None: + messageHashes = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + messageHashes.append(channel + ':' + symbol) + newPosition = await self.watch_multiple(url, messageHashes, None, messageHashes) + else: + newPosition = await self.watch(url, channel, None, channel) + if self.newUpdates: + return newPosition + return self.filter_by_symbols_since_limit(self.positions, symbols, since, limit, True) + + def handle_positions(self, client, message): + # + # { + # "event": "POSITION_UPDATE", + # "timestamp": 1731417961456, + # "data": { + # "positionId": "0193208d-735d-7fe9-90bd-8bc6d6bc1eda", + # "createdAt": 1289847904328, + # "symbol": "SOL_USDC", + # "positionSide": "SHORT", + # "entryPrice": "209.60000000", + # "quantity": "0.50", + # "status": "ACTIVE", + # "marginAsset": "USDC", + # "marginAmount": "15.17475649", + # "realizedPnL": "0.00000000" + # } + # } + # + channel = 'positions' + data = self.safe_dict(message, 'data', {}) + if self.positions is None: + self.positions = ArrayCacheBySymbolById() + cache = self.positions + parsedPosition = self.parse_position(data) + timestamp = self.safe_integer(message, 'timestamp') + parsedPosition['timestamp'] = timestamp + parsedPosition['datetime'] = self.iso8601(timestamp) + cache.append(parsedPosition) + messageHash = channel + ':' + parsedPosition['symbol'] + client.resolve([parsedPosition], channel) + client.resolve([parsedPosition], messageHash) + + def handle_message(self, client: Client, message): + error = self.safe_string(message, 'code') + if error is not None: + errorMsg = self.safe_string(message, 'msg') + raise ExchangeError(self.id + ' ' + errorMsg) + event = self.safe_string(message, 'event') + if event is not None: + methods: dict = { + 'ohlc': self.handle_ohlcv, + '24hrTicker': self.handle_ticker, + 'trades': self.handle_trades, + 'depth': self.handle_order_book, + 'WALLET_BALANCE_UPDATE': self.handle_balance, + 'ORDER_UPDATE': self.handle_order, + 'POSITION_UPDATE': self.handle_positions, + } + exacMethod = self.safe_value(methods, event) + if exacMethod is not None: + exacMethod(client, message) diff --git a/ccxt/pro/deribit.py b/ccxt/pro/deribit.py new file mode 100644 index 0000000..0ba5f87 --- /dev/null +++ b/ccxt/pro/deribit.py @@ -0,0 +1,1029 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolById, ArrayCacheByTimestamp +import hashlib +from ccxt.base.types import Any, Balances, Int, Order, OrderBook, Str, Strings, Ticker, Tickers, Trade +from ccxt.async_support.base.ws.client import Client +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import NotSupported + + +class deribit(ccxt.async_support.deribit): + + def describe(self) -> Any: + return self.deep_extend(super(deribit, self).describe(), { + 'has': { + 'ws': True, + 'watchBalance': True, + 'watchTicker': True, + 'watchTickers': True, + 'watchBidsAsks': True, + 'watchTrades': True, + 'watchTradesForSymbols': True, + 'watchMyTrades': True, + 'watchOrders': True, + 'watchOrderBook': True, + 'watchOrderBookForSymbols': True, + 'watchOHLCV': True, + 'watchOHLCVForSymbols': True, + }, + 'urls': { + 'test': { + 'ws': 'wss://test.deribit.com/ws/api/v2', + }, + 'api': { + 'ws': 'wss://www.deribit.com/ws/api/v2', + }, + }, + 'options': { + 'ws': { + 'timeframes': { + '1m': '1', + '3m': '3', + '5m': '5', + '15m': '15', + '30m': '30', + '1h': '60', + '2h': '120', + '4h': '180', + '6h': '360', + '12h': '720', + '1d': '1D', + }, + # watchTrades replacement + 'watchTradesForSymbols': { + 'interval': '100ms', # 100ms, agg2, raw + }, + # watchOrderBook replacement + 'watchOrderBookForSymbols': { + 'interval': '100ms', # 100ms, agg2, raw + 'useDepthEndpoint': False, # if True, it will use the {books.group.depth.interval} endpoint instead of the {books.interval} endpoint + 'depth': '20', # 1, 10, 20 + 'group': 'none', # none, 1, 2, 5, 10, 25, 100, 250 + }, + }, + 'currencies': ['BTC', 'ETH', 'SOL', 'USDC'], + }, + 'streaming': { + }, + 'exceptions': { + }, + }) + + def request_id(self): + requestId = self.sum(self.safe_integer(self.options, 'requestId', 0), 1) + self.options['requestId'] = requestId + return requestId + + async def watch_balance(self, params={}) -> Balances: + """ + + https://docs.deribit.com/#user-portfolio-currency + + watch balance and get the amount of funds available for trading or funds locked in orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.authenticate(params) + messageHash = 'balance' + url = self.urls['api']['ws'] + currencies = self.safe_value(self.options, 'currencies', []) + channels = [] + for i in range(0, len(currencies)): + currencyCode = currencies[i] + channels.append('user.portfolio.' + currencyCode) + subscribe: dict = { + 'jsonrpc': '2.0', + 'method': 'private/subscribe', + 'params': { + 'channels': channels, + }, + 'id': self.request_id(), + } + request = self.deep_extend(subscribe, params) + return await self.watch(url, messageHash, request, messageHash, request) + + def handle_balance(self, client: Client, message): + # + # subscription + # { + # "jsonrpc": "2.0", + # "method": "subscription", + # "params": { + # "channel": "user.portfolio.btc", + # "data": { + # "total_pl": 0, + # "session_upl": 0, + # "session_rpl": 0, + # "projected_maintenance_margin": 0, + # "projected_initial_margin": 0, + # "projected_delta_total": 0, + # "portfolio_margining_enabled": False, + # "options_vega": 0, + # "options_value": 0, + # "options_theta": 0, + # "options_session_upl": 0, + # "options_session_rpl": 0, + # "options_pl": 0, + # "options_gamma": 0, + # "options_delta": 0, + # "margin_balance": 0.0015, + # "maintenance_margin": 0, + # "initial_margin": 0, + # "futures_session_upl": 0, + # "futures_session_rpl": 0, + # "futures_pl": 0, + # "fee_balance": 0, + # "estimated_liquidation_ratio_map": {}, + # "estimated_liquidation_ratio": 0, + # "equity": 0.0015, + # "delta_total_map": {}, + # "delta_total": 0, + # "currency": "BTC", + # "balance": 0.0015, + # "available_withdrawal_funds": 0.0015, + # "available_funds": 0.0015 + # } + # } + # } + # + params = self.safe_value(message, 'params', {}) + data = self.safe_value(params, 'data', {}) + self.balance['info'] = data + currencyId = self.safe_string(data, 'currency') + currencyCode = self.safe_currency_code(currencyId) + balance = self.parse_balance(data) + self.balance[currencyCode] = balance + messageHash = 'balance' + client.resolve(self.balance, messageHash) + + async def watch_ticker(self, symbol: str, params={}) -> Ticker: + """ + + https://docs.deribit.com/#ticker-instrument_name-interval + + watches a price ticker, a statistical calculation with the information for a specific market. + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.interval]: specify aggregation and frequency of notifications. Possible values: 100ms, raw + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + url = self.urls['api']['ws'] + interval = self.safe_string(params, 'interval', '100ms') + params = self.omit(params, 'interval') + await self.load_markets() + if interval == 'raw': + await self.authenticate() + channel = 'ticker.' + market['id'] + '.' + interval + message: dict = { + 'jsonrpc': '2.0', + 'method': 'public/subscribe', + 'params': { + 'channels': ['ticker.' + market['id'] + '.' + interval], + }, + 'id': self.request_id(), + } + request = self.deep_extend(message, params) + return await self.watch(url, channel, request, channel, request) + + async def watch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + + https://docs.deribit.com/#ticker-instrument_name-interval + + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list + :param str[] [symbols]: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.interval]: specify aggregation and frequency of notifications. Possible values: 100ms, raw + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, False) + url = self.urls['api']['ws'] + interval = self.safe_string(params, 'interval', '100ms') + params = self.omit(params, 'interval') + await self.load_markets() + if interval == 'raw': + await self.authenticate() + channels = [] + for i in range(0, len(symbols)): + market = self.market(symbols[i]) + channels.append('ticker.' + market['id'] + '.' + interval) + message: dict = { + 'jsonrpc': '2.0', + 'method': 'public/subscribe', + 'params': { + 'channels': channels, + }, + 'id': self.request_id(), + } + request = self.deep_extend(message, params) + newTickers = await self.watch_multiple(url, channels, request, channels, request) + if self.newUpdates: + tickers: dict = {} + tickers[newTickers['symbol']] = newTickers + return tickers + return self.filter_by_array(self.tickers, 'symbol', symbols) + + def handle_ticker(self, client: Client, message): + # + # { + # "jsonrpc": "2.0", + # "method": "subscription", + # "params": { + # "channel": "ticker.BTC_USDC-PERPETUAL.raw", + # "data": { + # "timestamp": 1655393725040, + # "stats": [Object], + # "state": "open", + # "settlement_price": 21729.5891, + # "open_interest": 164.501, + # "min_price": 20792.9376, + # "max_price": 21426.225, + # "mark_price": 21109.555, + # "last_price": 21132, + # "instrument_name": "BTC_USDC-PERPETUAL", + # "index_price": 21122.3937, + # "funding_8h": -0.00022427, + # "estimated_delivery_price": 21122.3937, + # "current_funding": -0.00010782, + # "best_bid_price": 21106, + # "best_bid_amount": 1.143, + # "best_ask_price": 21113, + # "best_ask_amount": 0.327 + # } + # } + # } + # + params = self.safe_value(message, 'params', {}) + data = self.safe_value(params, 'data', {}) + marketId = self.safe_string(data, 'instrument_name') + symbol = self.safe_symbol(marketId) + ticker = self.parse_ticker(data) + messageHash = self.safe_string(params, 'channel') + self.tickers[symbol] = ticker + client.resolve(ticker, messageHash) + + async def watch_bids_asks(self, symbols: Strings = None, params={}) -> Tickers: + """ + + https://docs.deribit.com/#quote-instrument_name + + watches best bid & ask for symbols + :param str[] [symbols]: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, False) + url = self.urls['api']['ws'] + channels = [] + for i in range(0, len(symbols)): + market = self.market(symbols[i]) + channels.append('quote.' + market['id']) + message: dict = { + 'jsonrpc': '2.0', + 'method': 'public/subscribe', + 'params': { + 'channels': channels, + }, + 'id': self.request_id(), + } + request = self.deep_extend(message, params) + newTickers = await self.watch_multiple(url, channels, request, channels, request) + if self.newUpdates: + tickers: dict = {} + tickers[newTickers['symbol']] = newTickers + return tickers + return self.filter_by_array(self.bidsasks, 'symbol', symbols) + + def handle_bid_ask(self, client: Client, message): + # + # { + # "jsonrpc": "2.0", + # "method": "subscription", + # "params": { + # "channel": "quote.BTC_USDT", + # "data": { + # "best_bid_amount": 0.026, + # "best_ask_amount": 0.026, + # "best_bid_price": 63908, + # "best_ask_price": 63940, + # "instrument_name": "BTC_USDT", + # "timestamp": 1727765131750 + # } + # } + # } + # + params = self.safe_dict(message, 'params', {}) + data = self.safe_dict(params, 'data', {}) + ticker = self.parse_ws_bid_ask(data) + symbol = ticker['symbol'] + self.bidsasks[symbol] = ticker + messageHash = self.safe_string(params, 'channel') + client.resolve(ticker, messageHash) + + def parse_ws_bid_ask(self, ticker, market=None): + marketId = self.safe_string(ticker, 'instrument_name') + market = self.safe_market(marketId, market) + symbol = self.safe_string(market, 'symbol') + timestamp = self.safe_integer(ticker, 'timestamp') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'ask': self.safe_string(ticker, 'best_ask_price'), + 'askVolume': self.safe_string(ticker, 'best_ask_amount'), + 'bid': self.safe_string(ticker, 'best_bid_price'), + 'bidVolume': self.safe_string(ticker, 'best_bid_amount'), + 'info': ticker, + }, market) + + async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://docs.deribit.com/#trades-instrument_name-interval + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.interval]: specify aggregation and frequency of notifications. Possible values: 100ms, raw + :returns dict[]: a list of `trade structures ` + """ + params['callerMethodName'] = 'watchTrades' + return await self.watch_trades_for_symbols([symbol], since, limit, params) + + async def watch_trades_for_symbols(self, symbols: List[str], since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a list of symbols + + https://docs.deribit.com/#trades-instrument_name-interval + + :param str[] symbols: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + interval = None + interval, params = self.handle_option_and_params(params, 'watchTradesForSymbols', 'interval', '100ms') + if interval == 'raw': + await self.authenticate() + trades = await self.watch_multiple_wrapper('trades', interval, symbols, params) + if self.newUpdates: + first = self.safe_dict(trades, 0) + tradeSymbol = self.safe_string(first, 'symbol') + limit = trades.getLimit(tradeSymbol, limit) + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + def handle_trades(self, client: Client, message): + # + # { + # "jsonrpc": "2.0", + # "method": "subscription", + # "params": { + # "channel": "trades.BTC_USDC-PERPETUAL.100ms", + # "data": [{ + # "trade_seq": 501899, + # "trade_id": "USDC-2436803", + # "timestamp": 1655397355998, + # "tick_direction": 2, + # "price": 21026, + # "mark_price": 21019.9719, + # "instrument_name": "BTC_USDC-PERPETUAL", + # "index_price": 21031.7847, + # "direction": "buy", + # "amount": 0.049 + # }] + # } + # } + # + params = self.safe_dict(message, 'params', {}) + channel = self.safe_string(params, 'channel', '') + parts = channel.split('.') + marketId = self.safe_string(parts, 1) + interval = self.safe_string(parts, 2) + symbol = self.safe_symbol(marketId) + market = self.safe_market(marketId) + trades = self.safe_list(params, 'data', []) + if self.safe_value(self.trades, symbol) is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + self.trades[symbol] = ArrayCache(limit) + stored = self.trades[symbol] + for i in range(0, len(trades)): + trade = trades[i] + parsed = self.parse_trade(trade, market) + stored.append(parsed) + self.trades[symbol] = stored + messageHash = 'trades|' + symbol + '|' + interval + client.resolve(self.trades[symbol], messageHash) + + async def watch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of trades associated with the user + + https://docs.deribit.com/#user-trades-instrument_name-interval + + :param str symbol: unified symbol of the market to fetch trades for. Use 'any' to watch all trades + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.interval]: specify aggregation and frequency of notifications. Possible values: 100ms, raw + :returns dict[]: a list of `trade structures ` + """ + await self.authenticate(params) + if symbol is not None: + await self.load_markets() + symbol = self.symbol(symbol) + url = self.urls['api']['ws'] + interval = self.safe_string(params, 'interval', 'raw') + params = self.omit(params, 'interval') + channel = 'user.trades.any.any.' + interval + message: dict = { + 'jsonrpc': '2.0', + 'method': 'private/subscribe', + 'params': { + 'channels': [channel], + }, + 'id': self.request_id(), + } + request = self.deep_extend(message, params) + trades = await self.watch(url, channel, request, channel, request) + return self.filter_by_symbol_since_limit(trades, symbol, since, limit, True) + + def handle_my_trades(self, client: Client, message): + # + # { + # "jsonrpc": "2.0", + # "method": "subscription", + # "params": { + # "channel": "user.trades.any.any.raw", + # "data": [{ + # "trade_seq": 149546319, + # "trade_id": "219381310", + # "timestamp": 1655421193564, + # "tick_direction": 0, + # "state": "filled", + # "self_trade": False, + # "reduce_only": False, + # "profit_loss": 0, + # "price": 20236.5, + # "post_only": False, + # "order_type": "market", + # "order_id": "46108941243", + # "matching_id": null, + # "mark_price": 20233.96, + # "liquidity": "T", + # "instrument_name": "BTC-PERPETUAL", + # "index_price": 20253.31, + # "fee_currency": "BTC", + # "fee": 2.5e-7, + # "direction": "buy", + # "amount": 10 + # }] + # } + # } + # + params = self.safe_value(message, 'params', {}) + channel = self.safe_string(params, 'channel', '') + trades = self.safe_value(params, 'data', []) + cachedTrades = self.myTrades + if cachedTrades is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + cachedTrades = ArrayCacheBySymbolById(limit) + parsed = self.parse_trades(trades) + marketIds: dict = {} + for i in range(0, len(parsed)): + trade = parsed[i] + cachedTrades.append(trade) + symbol = trade['symbol'] + marketIds[symbol] = True + client.resolve(cachedTrades, channel) + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + + https://docs.deribit.com/#book-instrument_name-group-depth-interval + + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.interval]: Frequency of notifications. Events will be aggregated over self interval. Possible values: 100ms, raw + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + params['callerMethodName'] = 'watchOrderBook' + return await self.watch_order_book_for_symbols([symbol], limit, params) + + async def watch_order_book_for_symbols(self, symbols: List[str], limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://docs.deribit.com/#book-instrument_name-group-depth-interval + + :param str[] symbols: unified array of symbols + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + interval = None + interval, params = self.handle_option_and_params(params, 'watchOrderBookForSymbols', 'interval', '100ms') + if interval == 'raw': + await self.authenticate() + descriptor = '' + useDepthEndpoint = None # for more info, see comment in .options + useDepthEndpoint, params = self.handle_option_and_params(params, 'watchOrderBookForSymbols', 'useDepthEndpoint', False) + if useDepthEndpoint: + depth = None + depth, params = self.handle_option_and_params(params, 'watchOrderBookForSymbols', 'depth', '20') + group = None + group, params = self.handle_option_and_params(params, 'watchOrderBookForSymbols', 'group', 'none') + descriptor = group + '.' + depth + '.' + interval + else: + descriptor = interval + orderbook = await self.watch_multiple_wrapper('book', descriptor, symbols, params) + return orderbook.limit() + + def handle_order_book(self, client: Client, message): + # + # snapshot + # { + # "jsonrpc": "2.0", + # "method": "subscription", + # "params": { + # "channel": "book.BTC_USDC-PERPETUAL.raw", + # "data": { + # "type": "snapshot", + # "timestamp": 1655395057025, + # "instrument_name": "BTC_USDC-PERPETUAL", + # "change_id": 1550694837, + # "bids": [ + # ["new", 20987, 0.487], + # ["new", 20986, 0.238], + # ], + # "asks": [ + # ["new", 20999, 0.092], + # ["new", 21000, 1.238], + # ] + # } + # } + # } + # + # change + # { + # "jsonrpc": "2.0", + # "method": "subscription", + # "params": { + # "channel": "book.BTC_USDC-PERPETUAL.raw", + # "data": { + # "type": "change", + # "timestamp": 1655395168086, + # "prev_change_id": 1550724481, + # "instrument_name": "BTC_USDC-PERPETUAL", + # "change_id": 1550724483, + # "bids": [ + # ["new", 20977, 0.109], + # ["delete", 20975, 0] + # ], + # "asks": [] + # } + # } + # } + # + params = self.safe_value(message, 'params', {}) + data = self.safe_value(params, 'data', {}) + channel = self.safe_string(params, 'channel') + parts = channel.split('.') + descriptor = '' + partsLength = len(parts) + isDetailed = partsLength == 5 + if isDetailed: + group = self.safe_string(parts, 2) + depth = self.safe_string(parts, 3) + interval = self.safe_string(parts, 4) + descriptor = group + '.' + depth + '.' + interval + else: + interval = self.safe_string(parts, 2) + descriptor = interval + marketId = self.safe_string(data, 'instrument_name') + symbol = self.safe_symbol(marketId) + timestamp = self.safe_integer(data, 'timestamp') + if not (symbol in self.orderbooks): + self.orderbooks[symbol] = self.counted_order_book() + storedOrderBook = self.orderbooks[symbol] + asks = self.safe_list(data, 'asks', []) + bids = self.safe_list(data, 'bids', []) + self.handle_deltas(storedOrderBook['asks'], asks) + self.handle_deltas(storedOrderBook['bids'], bids) + storedOrderBook['nonce'] = timestamp + storedOrderBook['timestamp'] = timestamp + storedOrderBook['datetime'] = self.iso8601(timestamp) + storedOrderBook['symbol'] = symbol + self.orderbooks[symbol] = storedOrderBook + messageHash = 'book|' + symbol + '|' + descriptor + client.resolve(storedOrderBook, messageHash) + + def clean_order_book(self, data): + bids = self.safe_list(data, 'bids', []) + asks = self.safe_list(data, 'asks', []) + cleanedBids = [] + for i in range(0, len(bids)): + cleanedBids.append([bids[i][1], bids[i][2]]) + cleanedAsks = [] + for i in range(0, len(asks)): + cleanedAsks.append([asks[i][1], asks[i][2]]) + data['bids'] = cleanedBids + data['asks'] = cleanedAsks + return data + + def handle_delta(self, bookside, delta): + price = delta[1] + amount = delta[2] + if delta[0] == 'new' or delta[0] == 'change': + bookside.storeArray([price, amount, 1]) + elif delta[0] == 'delete': + bookside.storeArray([price, amount, 0]) + + def handle_deltas(self, bookside, deltas): + for i in range(0, len(deltas)): + self.handle_delta(bookside, deltas[i]) + + async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + + https://docs.deribit.com/#user-orders-instrument_name-raw + + watches information on multiple orders made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + await self.authenticate(params) + if symbol is not None: + symbol = self.symbol(symbol) + url = self.urls['api']['ws'] + currency = self.safe_string(params, 'currency', 'any') + interval = self.safe_string(params, 'interval', 'raw') + kind = self.safe_string(params, 'kind', 'any') + params = self.omit(params, 'interval', 'currency', 'kind') + channel = 'user.orders.' + kind + '.' + currency + '.' + interval + message: dict = { + 'jsonrpc': '2.0', + 'method': 'private/subscribe', + 'params': { + 'channels': [channel], + }, + 'id': self.request_id(), + } + request = self.deep_extend(message, params) + orders = await self.watch(url, channel, request, channel, request) + if self.newUpdates: + limit = orders.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(orders, symbol, since, limit, True) + + def handle_orders(self, client: Client, message): + # Does not return a snapshot of current orders + # + # { + # "jsonrpc": "2.0", + # "method": "subscription", + # "params": { + # "channel": "user.orders.any.any.raw", + # "data": { + # "web": True, + # "time_in_force": "good_til_cancelled", + # "replaced": False, + # "reduce_only": False, + # "profit_loss": 0, + # "price": 50000, + # "post_only": False, + # "order_type": "limit", + # "order_state": "open", + # "order_id": "46094375191", + # "max_show": 10, + # "last_update_timestamp": 1655401625037, + # "label": '', + # "is_liquidation": False, + # "instrument_name": "BTC-PERPETUAL", + # "filled_amount": 0, + # "direction": "sell", + # "creation_timestamp": 1655401625037, + # "commission": 0, + # "average_price": 0, + # "api": False, + # "amount": 10 + # } + # } + # } + # + if self.orders is None: + limit = self.safe_integer(self.options, 'ordersLimit', 1000) + self.orders = ArrayCacheBySymbolById(limit) + params = self.safe_value(message, 'params', {}) + channel = self.safe_string(params, 'channel', '') + data = self.safe_value(params, 'data', {}) + orders = [] + if isinstance(data, list): + orders = self.parse_orders(data) + else: + order = self.parse_order(data) + orders = [order] + cachedOrders = self.orders + for i in range(0, len(orders)): + cachedOrders.append(orders[i]) + client.resolve(self.orders, channel) + + async def watch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + + https://docs.deribit.com/#chart-trades-instrument_name-resolution + + watches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + symbol = self.symbol(symbol) + ohlcvs = await self.watch_ohlcv_for_symbols([[symbol, timeframe]], since, limit, params) + return ohlcvs[symbol][timeframe] + + async def watch_ohlcv_for_symbols(self, symbolsAndTimeframes: List[List[str]], since: Int = None, limit: Int = None, params={}): + """ + watches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://docs.deribit.com/#chart-trades-instrument_name-resolution + + :param str[][] symbolsAndTimeframes: array of arrays containing unified symbols and timeframes to fetch OHLCV data for, example [['BTC/USDT', '1m'], ['LTC/USDT', '5m']] + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + symbolsLength = len(symbolsAndTimeframes) + if symbolsLength == 0 or not isinstance(symbolsAndTimeframes[0], list): + raise ArgumentsRequired(self.id + " watchOHLCVForSymbols() requires a an array of symbols and timeframes, like [['BTC/USDT', '1m'], ['LTC/USDT', '5m']]") + symbol, timeframe, candles = await self.watch_multiple_wrapper('chart.trades', None, symbolsAndTimeframes, params) + if self.newUpdates: + limit = candles.getLimit(symbol, limit) + filtered = self.filter_by_since_limit(candles, since, limit, 0, True) + return self.create_ohlcv_object(symbol, timeframe, filtered) + + def handle_ohlcv(self, client: Client, message): + # + # { + # "jsonrpc": "2.0", + # "method": "subscription", + # "params": { + # "channel": "chart.trades.BTC_USDC-PERPETUAL.1", + # "data": { + # "volume": 0, + # "tick": 1655403420000, + # "open": 20951, + # "low": 20951, + # "high": 20951, + # "cost": 0, + # "close": 20951 + # } + # } + # } + # + params = self.safe_dict(message, 'params', {}) + channel = self.safe_string(params, 'channel', '') + parts = channel.split('.') + marketId = self.safe_string(parts, 2) + rawTimeframe = self.safe_string(parts, 3) + market = self.safe_market(marketId) + symbol = market['symbol'] + wsOptions = self.safe_dict(self.options, 'ws', {}) + timeframes = self.safe_dict(wsOptions, 'timeframes', {}) + unifiedTimeframe = self.find_timeframe(rawTimeframe, timeframes) + self.ohlcvs[symbol] = self.safe_dict(self.ohlcvs, symbol, {}) + if self.safe_value(self.ohlcvs[symbol], unifiedTimeframe) is None: + limit = self.safe_integer(self.options, 'OHLCVLimit', 1000) + self.ohlcvs[symbol][unifiedTimeframe] = ArrayCacheByTimestamp(limit) + stored = self.ohlcvs[symbol][unifiedTimeframe] + ohlcv = self.safe_dict(params, 'data', {}) + # data contains a single OHLCV candle + parsed = self.parse_ws_ohlcv(ohlcv, market) + stored.append(parsed) + self.ohlcvs[symbol][unifiedTimeframe] = stored + resolveData = [symbol, unifiedTimeframe, stored] + messageHash = 'chart.trades|' + symbol + '|' + rawTimeframe + client.resolve(resolveData, messageHash) + + def parse_ws_ohlcv(self, ohlcv, market=None) -> list: + # + # { + # "c": "28909.0", + # "o": "28915.4", + # "h": "28915.4", + # "l": "28896.1", + # "v": "27.6919", + # "T": 1696687499999, + # "t": 1696687440000 + # } + # + return [ + self.safe_integer(ohlcv, 'tick'), + self.safe_number(ohlcv, 'open'), + self.safe_number(ohlcv, 'high'), + self.safe_number(ohlcv, 'low'), + self.safe_number(ohlcv, 'close'), + self.safe_number(ohlcv, 'volume'), + ] + + async def watch_multiple_wrapper(self, channelName: str, channelDescriptor: Str, symbolsArray=None, params={}): + await self.load_markets() + url = self.urls['api']['ws'] + rawSubscriptions = [] + messageHashes = [] + isOHLCV = (channelName == 'chart.trades') + symbols = self.get_list_from_object_values(symbolsArray, 0) if isOHLCV else symbolsArray + self.market_symbols(symbols, None, False) + for i in range(0, len(symbolsArray)): + current = symbolsArray[i] + market = None + if isOHLCV: + market = self.market(current[0]) + unifiedTf = current[1] + rawTf = self.safe_string(self.timeframes, unifiedTf, unifiedTf) + channelDescriptor = rawTf + else: + market = self.market(current) + message = channelName + '.' + market['id'] + '.' + channelDescriptor + rawSubscriptions.append(message) + messageHashes.append(channelName + '|' + market['symbol'] + '|' + channelDescriptor) + request: dict = { + 'jsonrpc': '2.0', + 'method': 'public/subscribe', + 'params': { + 'channels': rawSubscriptions, + }, + 'id': self.request_id(), + } + extendedRequest = self.deep_extend(request, params) + maxMessageByteLimit = 32768 - 1 # 'Message Too Big: limit 32768B' + jsonedText = self.json(extendedRequest) + if len(jsonedText) >= maxMessageByteLimit: + raise ExchangeError(self.id + ' requested subscription length over limit, try to reduce symbols amount') + return await self.watch_multiple(url, messageHashes, extendedRequest, rawSubscriptions) + + def handle_message(self, client: Client, message): + # + # error + # { + # "jsonrpc": "2.0", + # "id": 1, + # "error": { + # "message": "Invalid params", + # "data": { + # "reason": "invalid format", + # "param": "nonce" + # }, + # "code": -32602 + # }, + # "usIn": "1655391709417993", + # "usOut": "1655391709418049", + # "usDiff": 56, + # "testnet": False + # } + # + # subscribe + # { + # "jsonrpc": "2.0", + # "id": 2, + # "result": ["ticker.BTC_USDC-PERPETUAL.raw"], + # "usIn": "1655393625889396", + # "usOut": "1655393625889518", + # "usDiff": 122, + # "testnet": False + # } + # + # notification + # { + # "jsonrpc": "2.0", + # "method": "subscription", + # "params": { + # "channel": "ticker.BTC_USDC-PERPETUAL.raw", + # "data": { + # "timestamp": 1655393724752, + # "stats": [Object], + # "state": "open", + # "settlement_price": 21729.5891, + # "open_interest": 164.501, + # "min_price": 20792.9001, + # "max_price": 21426.1864, + # "mark_price": 21109.4757, + # "last_price": 21132, + # "instrument_name": "BTC_USDC-PERPETUAL", + # "index_price": 21122.3937, + # "funding_8h": -0.00022427, + # "estimated_delivery_price": 21122.3937, + # "current_funding": -0.00011158, + # "best_bid_price": 21106, + # "best_bid_amount": 1.143, + # "best_ask_price": 21113, + # "best_ask_amount": 0.402 + # } + # } + # } + # + error = self.safe_value(message, 'error') + if error is not None: + raise ExchangeError(self.id + ' ' + self.json(error)) + params = self.safe_value(message, 'params') + channel = self.safe_string(params, 'channel') + if channel is not None: + parts = channel.split('.') + channelId = self.safe_string(parts, 0) + userHandlers: dict = { + 'trades': self.handle_my_trades, + 'portfolio': self.handle_balance, + 'orders': self.handle_orders, + } + handlers: dict = { + 'ticker': self.handle_ticker, + 'quote': self.handle_bid_ask, + 'book': self.handle_order_book, + 'trades': self.handle_trades, + 'chart': self.handle_ohlcv, + 'user': self.safe_value(userHandlers, self.safe_string(parts, 1)), + } + handler = self.safe_value(handlers, channelId) + if handler is not None: + handler(client, message) + return + raise NotSupported(self.id + ' no handler found for self message ' + self.json(message)) + result = self.safe_value(message, 'result', {}) + accessToken = self.safe_string(result, 'access_token') + if accessToken is not None: + self.handle_authentication_message(client, message) + + def handle_authentication_message(self, client: Client, message): + # + # { + # "jsonrpc": "2.0", + # "id": 1, + # "result": { + # "token_type": "bearer", + # "scope": "account:read_write block_trade:read_write connection custody:read_write mainaccount name:ccxt trade:read_write wallet:read_write", + # "refresh_token": "1686927372328.1EzFBRmt.logRQWXkPA1oE_Tk0gRsls9Hau7YN6a321XUBnxvR4x6cryhbkKcniUJU-czA8_zKXrqQGpQmfoDwhLIjIsWCvRuu6otbg-LKWlrtTX1GQqLcPaTTHAdZGTMV-HM8HiS03QBd9MIXWRfF53sKj2hdR9nZPZ6MH1XrkpAZPB_peuEEB9wlcc3elzWEZFtCmiy1fnQ8TPHwAJMt3nuUmEcMLt_-F554qrsg_-I66D9xMiifJj4dBemdPfV_PkGPRIwIoKlxDjyv2-xfCw-4eKyo6Hu1m2h6gT1DPOTxSXcBgfBQjpi-_uY3iAIj7U6xjC46PHthEdquhEuCTZl7UfCRZSAWwZA", + # "expires_in": 31536000, + # "access_token": "1686923272328.1CkwEx-u.qHradpIulmuoeboKMEi8PkQ1_4DF8yFE2zywBTtkD32sruVC53b1HwL5OWRuh2nYAndXff4xuXIMRkkEfMAFCeq24prihxxinoS8DDVkKBxedGx4CUPJFeXjmh7wuRGqQOLg1plXOpbF3fwF2KPEkAuETwcpcVY6K9HUVjutNRfxFe2TR7CvuS9x8TATvoPeu7H1ezYl-LkKSaRifdTXuwituXgp4oDbPRyQLniEBWuYF9rY7qbABxuOJlXI1VZ63u7Bh0mGWei-KeVeqHGNpy6OgrFRPXPxa9_U7vaxCyHW3zZ9959TQ1QUMLWtUX-NLBEv3BT5eCieW9HORYIOKfsgkpd3" + # }, + # "usIn": "1655391872327712", + # "usOut": "1655391872328515", + # "usDiff": 803, + # "testnet": False + # } + # + messageHash = 'authenticated' + client.resolve(message, messageHash) + return message + + async def authenticate(self, params={}): + url = self.urls['api']['ws'] + client = self.client(url) + time = self.milliseconds() + timeString = self.number_to_string(time) + nonce = timeString + messageHash = 'authenticated' + future = self.safe_value(client.subscriptions, messageHash) + if future is None: + self.check_required_credentials() + requestId = self.request_id() + lineBreak = "\n" # eslint-disable-line quotes + signature = self.hmac(self.encode(timeString + lineBreak + nonce + lineBreak), self.encode(self.secret), hashlib.sha256) + request: dict = { + 'jsonrpc': '2.0', + 'id': requestId, + 'method': 'public/auth', + 'params': { + 'grant_type': 'client_signature', + 'client_id': self.apiKey, + 'timestamp': time, + 'signature': signature, + 'nonce': nonce, + 'data': '', + }, + } + future = await self.watch(url, messageHash, self.extend(request, params), messageHash) + client.subscriptions[messageHash] = future + return future diff --git a/ccxt/pro/derive.py b/ccxt/pro/derive.py new file mode 100644 index 0000000..55d1a42 --- /dev/null +++ b/ccxt/pro/derive.py @@ -0,0 +1,704 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolById +from ccxt.base.types import Any, Bool, Int, Order, OrderBook, Str, Ticker, Trade +from ccxt.async_support.base.ws.client import Client +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import UnsubscribeError + + +class derive(ccxt.async_support.derive): + + def describe(self) -> Any: + return self.deep_extend(super(derive, self).describe(), { + 'has': { + 'ws': False, + 'watchBalance': False, + 'watchMyTrades': True, + 'watchOHLCV': False, + 'watchOrderBook': True, + 'watchOrders': True, + 'watchTicker': True, + 'watchTickers': False, + 'watchBidsAsks': False, + 'watchTrades': True, + 'watchTradesForSymbols': False, + 'watchPositions': False, + }, + 'urls': { + 'api': { + 'ws': 'wss://api.lyra.finance/ws', + }, + 'test': { + 'ws': 'wss://api-demo.lyra.finance/ws', + }, + }, + 'options': { + 'tradesLimit': 1000, + 'ordersLimit': 1000, + 'requestId': {}, + }, + 'streaming': { + 'keepAlive': 9000, + }, + 'exceptions': { + 'ws': { + 'exact': {}, + }, + }, + }) + + def request_id(self, url): + options = self.safe_value(self.options, 'requestId', {}) + previousValue = self.safe_integer(options, url, 0) + newValue = self.sum(previousValue, 1) + self.options['requestId'][url] = newValue + return newValue + + async def watch_public(self, messageHash, message, subscription): + url = self.urls['api']['ws'] + requestId = self.request_id(url) + request = self.extend(message, { + 'id': requestId, + }) + subscription = self.extend(subscription, { + 'id': requestId, + 'method': 'subscribe', + }) + return await self.watch(url, messageHash, request, messageHash, subscription) + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + + https://docs.derive.xyz/reference/orderbook-instrument_name-group-depth + + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return. + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + if limit is None: + limit = 10 + market = self.market(symbol) + topic = 'orderbook.' + market['id'] + '.10.' + self.number_to_string(limit) + request: dict = { + 'method': 'subscribe', + 'params': { + 'channels': [ + topic, + ], + }, + } + subscription: dict = { + 'name': topic, + 'symbol': symbol, + 'limit': limit, + 'params': params, + } + orderbook = await self.watch_public(topic, request, subscription) + return orderbook.limit() + + def handle_order_book(self, client: Client, message): + # + # { + # method: 'subscription', + # params: { + # channel: 'orderbook.BTC-PERP.10.1', + # data: { + # timestamp: 1738331231506, + # instrument_name: 'BTC-PERP', + # publish_id: 628419, + # bids: [['104669', '40']], + # asks: [['104736', '40']] + # } + # } + # } + # + params = self.safe_dict(message, 'params') + data = self.safe_dict(params, 'data') + marketId = self.safe_string(data, 'instrument_name') + market = self.safe_market(marketId) + symbol = market['symbol'] + topic = self.safe_string(params, 'channel') + if not (symbol in self.orderbooks): + defaultLimit = self.safe_integer(self.options, 'watchOrderBookLimit', 1000) + subscription = client.subscriptions[topic] + limit = self.safe_integer(subscription, 'limit', defaultLimit) + self.orderbooks[symbol] = self.order_book({}, limit) + orderbook = self.orderbooks[symbol] + timestamp = self.safe_integer(data, 'timestamp') + snapshot = self.parse_order_book(data, symbol, timestamp, 'bids', 'asks') + orderbook.reset(snapshot) + client.resolve(orderbook, topic) + + async def watch_ticker(self, symbol: str, params={}) -> Ticker: + """ + + https://docs.derive.xyz/reference/ticker-instrument_name-interval + + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + topic = 'ticker.' + market['id'] + '.100' + request: dict = { + 'method': 'subscribe', + 'params': { + 'channels': [ + topic, + ], + }, + } + subscription: dict = { + 'name': topic, + 'symbol': symbol, + 'params': params, + } + return await self.watch_public(topic, request, subscription) + + def handle_ticker(self, client: Client, message): + # + # { + # method: 'subscription', + # params: { + # channel: 'ticker.BTC-PERP.100', + # data: { + # timestamp: 1738485104439, + # instrument_ticker: { + # instrument_type: 'perp', + # instrument_name: 'BTC-PERP', + # scheduled_activation: 1701840228, + # scheduled_deactivation: '9223372036854775807', + # is_active: True, + # tick_size: '0.1', + # minimum_amount: '0.01', + # maximum_amount: '10000', + # amount_step: '0.001', + # mark_price_fee_rate_cap: '0', + # maker_fee_rate: '0.0001', + # taker_fee_rate: '0.0003', + # base_fee: '0.1', + # base_currency: 'BTC', + # quote_currency: 'USD', + # option_details: null, + # perp_details: { + # index: 'BTC-USD', + # max_rate_per_hour: '0.004', + # min_rate_per_hour: '-0.004', + # static_interest_rate: '0.0000125', + # aggregate_funding: '10581.779418721074588722', + # funding_rate: '0.000024792239208858' + # }, + # erc20_details: null, + # base_asset_address: '0xDBa83C0C654DB1cd914FA2710bA743e925B53086', + # base_asset_sub_id: '0', + # pro_rata_fraction: '0', + # fifo_min_allocation: '0', + # pro_rata_amount_step: '0.1', + # best_ask_amount: '0.131', + # best_ask_price: '99898.6', + # best_bid_amount: '0.056', + # best_bid_price: '99889.1', + # five_percent_bid_depth: '11.817', + # five_percent_ask_depth: '9.116', + # option_pricing: null, + # index_price: '99883.8', + # mark_price: '99897.52408421244763303548098', + # stats: { + # contract_volume: '92.395', + # num_trades: '2924', + # open_interest: '33.743468027373780786', + # high: '102320.4', + # low: '99064.3', + # percent_change: '-0.021356', + # usd_change: '-2178' + # }, + # timestamp: 1738485165881, + # min_price: '97939.1', + # max_price: '101895.2' + # } + # } + # } + # } + # + params = self.safe_dict(message, 'params') + rawData = self.safe_dict(params, 'data') + data = self.safe_dict(rawData, 'instrument_ticker') + topic = self.safe_value(params, 'channel') + ticker = self.parse_ticker(data) + self.tickers[ticker['symbol']] = ticker + client.resolve(ticker, topic) + return message + + async def un_watch_order_book(self, symbol: str, params={}) -> Any: + """ + unsubscribe from the orderbook channel + :param str symbol: unified symbol of the market to fetch the order book for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.limit]: orderbook limit, default is None + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + limit = self.safe_integer(params, 'limit') + if limit is None: + limit = 10 + market = self.market(symbol) + topic = 'orderbook.' + market['id'] + '.10.' + self.number_to_string(limit) + messageHash = 'unwatch' + topic + request: dict = { + 'method': 'unsubscribe', + 'params': { + 'channels': [ + topic, + ], + }, + } + subscription: dict = { + 'name': topic, + } + return await self.un_watch_public(messageHash, request, subscription) + + async def un_watch_trades(self, symbol: str, params={}) -> Any: + """ + unsubscribe from the trades channel + :param str symbol: unified symbol of the market to unwatch the trades for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns any: status of the unwatch request + """ + await self.load_markets() + market = self.market(symbol) + topic = 'trades.' + market['id'] + messageHah = 'unwatch' + topic + request: dict = { + 'method': 'unsubscribe', + 'params': { + 'channels': [ + topic, + ], + }, + } + subscription: dict = { + 'name': topic, + } + return await self.un_watch_public(messageHah, request, subscription) + + async def un_watch_public(self, messageHash, message, subscription): + url = self.urls['api']['ws'] + requestId = self.request_id(url) + request = self.extend(message, { + 'id': requestId, + }) + subscription = self.extend(subscription, { + 'id': requestId, + 'method': 'unsubscribe', + }) + return await self.watch(url, messageHash, request, messageHash, subscription) + + def handle_order_book_un_subscription(self, client: Client, topic): + parsedTopic = topic.split('.') + marketId = self.safe_string(parsedTopic, 1) + market = self.safe_market(marketId) + symbol = market['symbol'] + if symbol in self.orderbooks: + del self.orderbooks[symbol] + if topic in client.subscriptions: + del client.subscriptions[topic] + error = UnsubscribeError(self.id + ' orderbook ' + symbol) + client.reject(error, topic) + client.resolve(error, 'unwatch' + topic) + + def handle_trades_un_subscription(self, client: Client, topic): + parsedTopic = topic.split('.') + marketId = self.safe_string(parsedTopic, 1) + market = self.safe_market(marketId) + symbol = market['symbol'] + if symbol in self.orderbooks: + del self.trades[symbol] + if topic in client.subscriptions: + del client.subscriptions[topic] + error = UnsubscribeError(self.id + ' trades ' + symbol) + client.reject(error, topic) + client.resolve(error, 'unwatch' + topic) + + def handle_un_subscribe(self, client: Client, message): + # + # { + # id: 1, + # result: { + # status: {'orderbook.BTC-PERP.10.10': 'ok'}, + # remaining_subscriptions: [] + # } + # } + # + result = self.safe_dict(message, 'result') + status = self.safe_dict(result, 'status') + if status is not None: + topics = list(status.keys()) + for i in range(0, len(topics)): + topic = topics[i] + if topic.find('orderbook') >= 0: + self.handle_order_book_un_subscription(client, topic) + elif topic.find('trades') >= 0: + self.handle_trades_un_subscription(client, topic) + return message + + async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + watches information on multiple trades made in a market + + https://docs.derive.xyz/reference/trades-instrument_name + + :param str symbol: unified market symbol of the market trades were made in + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trade structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + topic = 'trades.' + market['id'] + request: dict = { + 'method': 'subscribe', + 'params': { + 'channels': [ + topic, + ], + }, + } + subscription: dict = { + 'name': topic, + 'symbol': symbol, + 'params': params, + } + trades = await self.watch_public(topic, request, subscription) + if self.newUpdates: + limit = trades.getLimit(market['symbol'], limit) + return self.filter_by_symbol_since_limit(trades, symbol, since, limit, True) + + def handle_trade(self, client: Client, message): + # + # + params = self.safe_dict(message, 'params') + data = self.safe_dict(params, 'data') + topic = self.safe_value(params, 'channel') + parsedTopic = topic.split('.') + marketId = self.safe_string(parsedTopic, 1) + market = self.safe_market(marketId) + symbol = market['symbol'] + tradesArray = self.safe_value(self.trades, symbol) + if tradesArray is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + tradesArray = ArrayCache(limit) + for i in range(0, len(data)): + trade = self.parse_trade(data[i]) + tradesArray.append(trade) + self.trades[symbol] = tradesArray + client.resolve(tradesArray, topic) + + async def authenticate(self, params={}): + self.check_required_credentials() + url = self.urls['api']['ws'] + client = self.client(url) + messageHash = 'authenticated' + future = client.reusableFuture(messageHash) + authenticated = self.safe_value(client.subscriptions, messageHash) + if authenticated is None: + requestId = self.request_id(url) + now = str(self.milliseconds()) + signature = self.signMessage(now, self.privateKey) + deriveWalletAddress = self.safe_string(self.options, 'deriveWalletAddress') + request: dict = { + 'id': requestId, + 'method': 'public/login', + 'params': { + 'wallet': deriveWalletAddress, + 'timestamp': now, + 'signature': signature, + }, + } + # subscription: Dict = { + # 'name': topic, + # 'symbol': symbol, + # 'params': params, + # } + message = self.extend(request, params) + self.watch(url, messageHash, message, messageHash, message) + return await future + + async def watch_private(self, messageHash, message, subscription): + await self.authenticate() + url = self.urls['api']['ws'] + requestId = self.request_id(url) + request = self.extend(message, { + 'id': requestId, + }) + subscription = self.extend(subscription, { + 'id': requestId, + 'method': 'subscribe', + }) + return await self.watch(url, messageHash, request, messageHash, subscription) + + async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + + https://docs.derive.xyz/reference/subaccount_id-orders + + watches information on multiple orders made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.subaccount_id]: *required* the subaccount id + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + subaccountId = None + subaccountId, params = self.handleDeriveSubaccountId('watchOrders', params) + topic = self.number_to_string(subaccountId) + '.orders' + messageHash = topic + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + messageHash += ':' + symbol + request: dict = { + 'method': 'subscribe', + 'params': { + 'channels': [ + topic, + ], + }, + } + subscription: dict = { + 'name': topic, + 'params': params, + } + message = self.extend(request, params) + orders = await self.watch_private(messageHash, message, subscription) + if self.newUpdates: + limit = orders.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(orders, symbol, since, limit, True) + + def handle_order(self, client: Client, message): + # + # { + # method: 'subscription', + # params: { + # channel: '130837.orders', + # data: [ + # { + # subaccount_id: 130837, + # order_id: '1f44c564-5658-4b69-b8c4-4019924207d5', + # instrument_name: 'BTC-PERP', + # direction: 'buy', + # label: 'test1234', + # quote_id: null, + # creation_timestamp: 1738578974146, + # last_update_timestamp: 1738578974146, + # limit_price: '10000', + # amount: '0.01', + # filled_amount: '0', + # average_price: '0', + # order_fee: '0', + # order_type: 'limit', + # time_in_force: 'post_only', + # order_status: 'untriggered', + # max_fee: '219', + # signature_expiry_sec: 1746354973, + # nonce: 1738578973570, + # signer: '0x30CB7B06AdD6749BbE146A6827502B8f2a79269A', + # signature: '0xc6927095f74a0d3b1aeef8c0579d120056530479f806e9d2e6616df742a8934c69046361beae833b32b25c0145e318438d7d1624bb835add956f63aa37192f571c', + # cancel_reason: '', + # mmp: False, + # is_transfer: False, + # replaced_order_id: null, + # trigger_type: 'stoploss', + # trigger_price_type: 'mark', + # trigger_price: '102800', + # trigger_reject_message: null + # } + # ] + # } + # } + # + params = self.safe_dict(message, 'params') + topic = self.safe_string(params, 'channel') + rawOrders = self.safe_list(params, 'data') + for i in range(0, len(rawOrders)): + data = rawOrders[i] + parsed = self.parse_order(data) + symbol = self.safe_string(parsed, 'symbol') + orderId = self.safe_string(parsed, 'id') + if symbol is not None: + if self.orders is None: + limit = self.safe_integer(self.options, 'ordersLimit', 1000) + self.orders = ArrayCacheBySymbolById(limit) + cachedOrders = self.orders + orders = self.safe_value(cachedOrders.hashmap, symbol, {}) + order = self.safe_value(orders, orderId) + if order is not None: + fee = self.safe_value(order, 'fee') + if fee is not None: + parsed['fee'] = fee + fees = self.safe_value(order, 'fees') + if fees is not None: + parsed['fees'] = fees + parsed['trades'] = self.safe_value(order, 'trades') + parsed['timestamp'] = self.safe_integer(order, 'timestamp') + parsed['datetime'] = self.safe_string(order, 'datetime') + cachedOrders.append(parsed) + messageHashSymbol = topic + ':' + symbol + client.resolve(self.orders, messageHashSymbol) + client.resolve(self.orders, topic) + + async def watch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + + https://docs.derive.xyz/reference/subaccount_id-trades + + watches information on multiple trades made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.subaccount_id]: *required* the subaccount id + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + subaccountId = None + subaccountId, params = self.handleDeriveSubaccountId('watchMyTrades', params) + topic = self.number_to_string(subaccountId) + '.trades' + messageHash = topic + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + messageHash += ':' + symbol + request: dict = { + 'method': 'subscribe', + 'params': { + 'channels': [ + topic, + ], + }, + } + subscription: dict = { + 'name': topic, + 'params': params, + } + message = self.extend(request, params) + trades = await self.watch_private(messageHash, message, subscription) + if self.newUpdates: + limit = trades.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(trades, symbol, since, limit, True) + + def handle_my_trade(self, client: Client, message): + # + # + myTrades = self.myTrades + if myTrades is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + myTrades = ArrayCacheBySymbolById(limit) + params = self.safe_dict(message, 'params') + topic = self.safe_string(params, 'channel') + rawTrades = self.safe_list(params, 'data') + for i in range(0, len(rawTrades)): + trade = self.parse_trade(message) + myTrades.append(trade) + client.resolve(myTrades, topic) + messageHash = topic + trade['symbol'] + client.resolve(myTrades, messageHash) + + def handle_error_message(self, client: Client, message) -> Bool: + # + # { + # id: '690c6276-0fc6-4121-aafa-f28bf5adedcb', + # error: {code: -32600, message: 'Invalid Request'} + # } + # + if not ('error' in message): + return False + errorMessage = self.safe_dict(message, 'error') + errorCode = self.safe_string(errorMessage, 'code') + try: + if errorCode is not None: + feedback = self.id + ' ' + self.json(message) + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + raise ExchangeError(feedback) + return False + except Exception as error: + if isinstance(error, AuthenticationError): + messageHash = 'authenticated' + client.reject(error, messageHash) + if messageHash in client.subscriptions: + del client.subscriptions[messageHash] + else: + client.reject(error) + return True + + def handle_message(self, client: Client, message): + if self.handle_error_message(client, message): + return + methods: dict = { + 'orderbook': self.handle_order_book, + 'ticker': self.handle_ticker, + 'trades': self.handle_trade, + 'orders': self.handle_order, + 'mytrades': self.handle_my_trade, + } + event = None + params = self.safe_dict(message, 'params') + if params is not None: + channel = self.safe_string(params, 'channel') + if channel is not None: + parsedChannel = channel.split('.') + if (channel.find('orders') >= 0) or channel.find('trades') > 0: + event = self.safe_string(parsedChannel, 1) + # {subaccounr_id}.trades + if event == 'trades': + event = 'mytrades' + else: + event = self.safe_string(parsedChannel, 0) + method = self.safe_value(methods, event) + if method is not None: + method(client, message) + return + if 'id' in message: + id = self.safe_string(message, 'id') + subscriptionsById = self.index_by(client.subscriptions, 'id') + subscription = self.safe_value(subscriptionsById, id, {}) + if 'method' in subscription: + if subscription['method'] == 'public/login': + self.handle_auth(client, message) + elif subscription['method'] == 'unsubscribe': + self.handle_un_subscribe(client, message) + # could handleSubscribe + + def handle_auth(self, client: Client, message): + # + # { + # id: 1, + # result: [130837] + # } + # + messageHash = 'authenticated' + ids = self.safe_list(message, 'result') + if len(ids) > 0: + # client.resolve(message, messageHash) + future = self.safe_value(client.futures, 'authenticated') + future.resolve(True) + else: + error = AuthenticationError(self.json(message)) + client.reject(error, messageHash) + # allows further authentication attempts + if messageHash in client.subscriptions: + del client.subscriptions['authenticated'] diff --git a/ccxt/pro/exmo.py b/ccxt/pro/exmo.py new file mode 100644 index 0000000..8fa50e7 --- /dev/null +++ b/ccxt/pro/exmo.py @@ -0,0 +1,862 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolById +import hashlib +from ccxt.base.types import Any, Balances, Int, Market, Order, OrderBook, Str, Strings, Ticker, Tickers, Trade +from ccxt.async_support.base.ws.client import Client +from typing import List +from ccxt.base.errors import NotSupported + + +class exmo(ccxt.async_support.exmo): + + def describe(self) -> Any: + return self.deep_extend(super(exmo, self).describe(), { + 'has': { + 'ws': True, + 'watchBalance': True, + 'watchTicker': True, + 'watchTickers': True, + 'watchTrades': True, + 'watchMyTrades': True, + 'watchOrders': True, + 'watchOrderBook': True, + 'watchOHLCV': False, + }, + 'urls': { + 'api': { + 'ws': { + 'public': 'wss://ws-api.exmo.com:443/v1/public', + 'spot': 'wss://ws-api.exmo.com:443/v1/private', + 'margin': 'wss://ws-api.exmo.com:443/v1/margin/private', + }, + }, + }, + 'options': { + }, + 'streaming': { + }, + 'exceptions': { + }, + }) + + def request_id(self): + requestId = self.sum(self.safe_integer(self.options, 'requestId', 0), 1) + self.options['requestId'] = requestId + return requestId + + async def watch_balance(self, params={}) -> Balances: + """ + watch balance and get the amount of funds available for trading or funds locked in orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.authenticate(params) + type, query = self.handle_market_type_and_params('watchBalance', None, params) + messageHash = 'balance:' + type + url = self.urls['api']['ws'][type] + subscribe: dict = { + 'method': 'subscribe', + 'topics': [type + '/wallet'], + 'id': self.request_id(), + } + request = self.deep_extend(subscribe, query) + return await self.watch(url, messageHash, request, messageHash, request) + + def handle_balance(self, client: Client, message): + # + # spot + # { + # "ts": 1654208766007, + # "event": "snapshot", + # "topic": "spot/wallet", + # "data": { + # "balances": { + # "ADA": "0", + # "ALGO": "0", + # ... + # }, + # "reserved": { + # "ADA": "0", + # "ALGO": "0", + # ... + # } + # } + # } + # + # margin + # { + # "ts": 1624370076651, + # "event": "snapshot", + # "topic": "margin/wallets", + # "data": { + # "RUB": { + # "balance": "1000000", + # "used": "0", + # "free": "1000000" + # }, + # "USD": { + # "balance": "1000000", + # "used": "1831.925", + # "free": "998168.075" + # } + # } + # } + # { + # "ts": 1624370185720, + # "event": "update", + # "topic": "margin/wallets", + # "data": { + # "USD": { + # "balance": "1000123", + # "used": "1831.925", + # "free": "998291.075" + # } + # } + # } + # + topic = self.safe_string(message, 'topic') + parts = topic.split('/') + type = self.safe_string(parts, 0) + if type == 'spot': + self.parse_spot_balance(message) + elif type == 'margin': + self.parse_margin_balance(message) + messageHash = 'balance:' + type + client.resolve(self.balance, messageHash) + + def parse_spot_balance(self, message): + # + # { + # "balances": { + # "BTC": "3", + # "USD": "1000", + # "RUB": "0" + # }, + # "reserved": { + # "BTC": "0.5", + # "DASH": "0", + # "RUB": "0" + # } + # } + # + event = self.safe_string(message, 'event') + data = self.safe_value(message, 'data') + self.balance['info'] = data + if event == 'snapshot': + balances = self.safe_value(data, 'balances', {}) + reserved = self.safe_value(data, 'reserved', {}) + currencies = list(balances.keys()) + for i in range(0, len(currencies)): + currencyId = currencies[i] + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(balances, currencyId) + account['used'] = self.safe_string(reserved, currencyId) + self.balance[code] = account + elif event == 'update': + currencyId = self.safe_string(data, 'currency') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(data, 'balance') + account['used'] = self.safe_string(data, 'reserved') + self.balance[code] = account + self.balance = self.safe_balance(self.balance) + + def parse_margin_balance(self, message): + # + # { + # "RUB": { + # "balance": "1000000", + # "used": "0", + # "free": "1000000" + # }, + # "USD": { + # "balance": "1000000", + # "used": "1831.925", + # "free": "998168.075" + # } + # } + # + data = self.safe_value(message, 'data') + self.balance['info'] = data + currencies = list(data.keys()) + for i in range(0, len(currencies)): + currencyId = currencies[i] + code = self.safe_currency_code(currencyId) + wallet = self.safe_value(data, currencyId) + account = self.account() + account['free'] = self.safe_string(wallet, 'free') + account['used'] = self.safe_string(wallet, 'used') + account['total'] = self.safe_string(wallet, 'balance') + self.balance[code] = account + self.balance = self.safe_balance(self.balance) + + async def watch_ticker(self, symbol: str, params={}) -> Ticker: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#fd8f47bc-8517-43c0-bb60-1d61a86d4471 + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + url = self.urls['api']['ws']['public'] + messageHash = 'ticker:' + symbol + message: dict = { + 'method': 'subscribe', + 'topics': [ + 'spot/ticker:' + market['id'], + ], + 'id': self.request_id(), + } + request = self.deep_extend(message, params) + return await self.watch(url, messageHash, request, messageHash, request) + + async def watch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#fd8f47bc-8517-43c0-bb60-1d61a86d4471 + + :param str[] [symbols]: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, False) + messageHashes = [] + args = [] + for i in range(0, len(symbols)): + market = self.market(symbols[i]) + messageHashes.append('ticker:' + market['symbol']) + args.append('spot/ticker:' + market['id']) + url = self.urls['api']['ws']['public'] + message: dict = { + 'method': 'subscribe', + 'topics': args, + 'id': self.request_id(), + } + request = self.deep_extend(message, params) + await self.watch_multiple(url, messageHashes, request, messageHashes, request) + return self.filter_by_array(self.tickers, 'symbol', symbols) + + def handle_ticker(self, client: Client, message): + # + # spot + # { + # "ts": 1654205085473, + # "event": "update", + # "topic": "spot/ticker:BTC_USDT", + # "data": { + # "buy_price": "30285.84", + # "sell_price": "30299.97", + # "last_trade": "30295.01", + # "high": "30386.7", + # "low": "29542.76", + # "avg": "29974.16178449", + # "vol": "118.79538518", + # "vol_curr": "3598907.38200826", + # "updated": 1654205084 + # } + # } + # + topic = self.safe_string(message, 'topic') + topicParts = topic.split(':') + marketId = self.safe_string(topicParts, 1) + symbol = self.safe_symbol(marketId) + ticker = self.safe_value(message, 'data', {}) + market = self.safe_market(marketId) + parsedTicker = self.parse_ticker(ticker, market) + messageHash = 'ticker:' + symbol + self.tickers[symbol] = parsedTicker + client.resolve(parsedTicker, messageHash) + + async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + url = self.urls['api']['ws']['public'] + messageHash = 'trades:' + symbol + message: dict = { + 'method': 'subscribe', + 'topics': [ + 'spot/trades:' + market['id'], + ], + 'id': self.request_id(), + } + request = self.deep_extend(message, params) + trades = await self.watch(url, messageHash, request, messageHash, request) + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + def handle_trades(self, client: Client, message): + # + # { + # "ts": 1654206084001, + # "event": "update", + # "topic": "spot/trades:BTC_USDT", + # "data": [{ + # "trade_id": 389704729, + # "type": "sell", + # "price": "30310.95", + # "quantity": "0.0197", + # "amount": "597.125715", + # "date": 1654206083 + # }] + # } + # + topic = self.safe_string(message, 'topic') + parts = topic.split(':') + marketId = self.safe_string(parts, 1) + symbol = self.safe_symbol(marketId) + market = self.safe_market(marketId) + trades = self.safe_value(message, 'data', []) + messageHash = 'trades:' + symbol + stored = self.safe_value(self.trades, symbol) + if stored is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + stored = ArrayCache(limit) + self.trades[symbol] = stored + for i in range(0, len(trades)): + trade = trades[i] + parsed = self.parse_trade(trade, market) + stored.append(parsed) + self.trades[symbol] = stored + client.resolve(self.trades[symbol], messageHash) + + async def watch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of trades associated with the user + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + await self.authenticate(params) + type, query = self.handle_market_type_and_params('watchMyTrades', None, params) + url = self.urls['api']['ws'][type] + messageHash = None + if symbol is None: + messageHash = 'myTrades:' + type + else: + market = self.market(symbol) + symbol = market['symbol'] + messageHash = 'myTrades:' + market['symbol'] + message: dict = { + 'method': 'subscribe', + 'topics': [ + type + '/user_trades', + ], + 'id': self.request_id(), + } + request = self.deep_extend(message, query) + trades = await self.watch(url, messageHash, request, messageHash, request) + return self.filter_by_symbol_since_limit(trades, symbol, since, limit, True) + + def handle_my_trades(self, client: Client, message): + # + # spot + # { + # "ts": 1654210290219, + # "event": "update", + # "topic": "spot/user_trades", + # "data": { + # "trade_id": 389715807, + # "type": "buy", + # "price": "30527.77", + # "quantity": "0.0001", + # "amount": "3.052777", + # "date": 1654210290, + # "order_id": 27352777112, + # "client_id": 0, + # "pair": "BTC_USDT", + # "exec_type": "taker", + # "commission_amount": "0.0000001", + # "commission_currency": "BTC", + # "commission_percent": "0.1" + # } + # } + # + # margin + # { + # "ts":1624369720168, + # "event":"snapshot", + # "topic":"margin/user_trades", + # "data":[ + # { + # "trade_id":"692844278081167054", + # "trade_dt":"1624369773990729200", + # "type":"buy", + # "order_id":"692844278081167033", + # "pair":"BTC_USD", + # "quantity":"0.1", + # "price":"36638.5", + # "is_maker":false + # } + # ] + # } + # { + # "ts":1624370368612, + # "event":"update", + # "topic":"margin/user_trades", + # "data":{ + # "trade_id":"692844278081167693", + # "trade_dt":"1624370368569092500", + # "type":"buy", + # "order_id":"692844278081167674", + # "pair":"BTC_USD", + # "quantity":"0.1", + # "price":"36638.5", + # "is_maker":false + # } + # } + # + topic = self.safe_string(message, 'topic') + parts = topic.split('/') + type = self.safe_string(parts, 0) + messageHash = 'myTrades:' + type + event = self.safe_string(message, 'event') + rawTrades = [] + myTrades = None + if self.myTrades is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + myTrades = ArrayCacheBySymbolById(limit) + self.myTrades = myTrades + else: + myTrades = self.myTrades + if event == 'snapshot': + rawTrades = self.safe_value(message, 'data', []) + elif event == 'update': + rawTrade = self.safe_value(message, 'data', {}) + rawTrades = [rawTrade] + trades = self.parse_trades(rawTrades) + symbols: dict = {} + for j in range(0, len(trades)): + trade = trades[j] + myTrades.append(trade) + symbols[trade['symbol']] = True + symbolKeys = list(symbols.keys()) + for i in range(0, len(symbolKeys)): + symbol = symbolKeys[i] + symbolSpecificMessageHash = 'myTrades:' + symbol + client.resolve(myTrades, symbolSpecificMessageHash) + client.resolve(myTrades, messageHash) + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + url = self.urls['api']['ws']['public'] + messageHash = 'orderbook:' + symbol + params = self.omit(params, 'aggregation') + subscribe: dict = { + 'method': 'subscribe', + 'id': self.request_id(), + 'topics': [ + 'spot/order_book_updates:' + market['id'], + ], + } + request = self.deep_extend(subscribe, params) + orderbook = await self.watch(url, messageHash, request, messageHash) + return orderbook.limit() + + def handle_order_book(self, client: Client, message): + # + # { + # "ts": 1574427585174, + # "event": "snapshot", + # "topic": "spot/order_book_updates:BTC_USD", + # "data": { + # "ask": [ + # ["100", "3", "300"], + # ["200", "4", "800"] + # ], + # "bid": [ + # ["99", "2", "198"], + # ["98", "1", "98"] + # ] + # } + # } + # + # { + # "ts": 1574427585174, + # "event": "update", + # "topic": "spot/order_book_updates:BTC_USD", + # "data": { + # "ask": [ + # ["100", "1", "100"], + # ["200", "2", "400"] + # ], + # "bid": [ + # ["99", "1", "99"], + # ["98", "0", "0"] + # ] + # } + # } + # + topic = self.safe_string(message, 'topic') + parts = topic.split(':') + marketId = self.safe_string(parts, 1) + symbol = self.safe_symbol(marketId) + orderBook = self.safe_value(message, 'data', {}) + messageHash = 'orderbook:' + symbol + timestamp = self.safe_integer(message, 'ts') + if not (symbol in self.orderbooks): + self.orderbooks[symbol] = self.order_book({}) + orderbook = self.orderbooks[symbol] + event = self.safe_string(message, 'event') + if event == 'snapshot': + snapshot = self.parse_order_book(orderBook, symbol, timestamp, 'bid', 'ask') + orderbook.reset(snapshot) + else: + asks = self.safe_list(orderBook, 'ask', []) + bids = self.safe_list(orderBook, 'bid', []) + self.handle_deltas(orderbook['asks'], asks) + self.handle_deltas(orderbook['bids'], bids) + orderbook['timestamp'] = timestamp + orderbook['datetime'] = self.iso8601(timestamp) + client.resolve(orderbook, messageHash) + + def handle_delta(self, bookside, delta): + bidAsk = self.parse_bid_ask(delta, 0, 1) + bookside.storeArray(bidAsk) + + def handle_deltas(self, bookside, deltas): + for i in range(0, len(deltas)): + self.handle_delta(bookside, deltas[i]) + + async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + + https://documenter.getpostman.com/view/10287440/SzYXWKPi#85f7bc03-b1c9-4cd2-bd22-8fd422272825 + https://documenter.getpostman.com/view/10287440/SzYXWKPi#95e4ed18-1791-4e6d-83ad-cbfe9be1051c + + watches information on multiple orders made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + await self.authenticate(params) + type, query = self.handle_market_type_and_params('watchOrders', None, params) + url = self.urls['api']['ws'][type] + messageHash = None + if symbol is None: + messageHash = 'orders:' + type + else: + market = self.market(symbol) + symbol = market['symbol'] + messageHash = 'orders:' + market['symbol'] + message: dict = { + 'method': 'subscribe', + 'topics': [ + type + '/orders', + ], + 'id': self.request_id(), + } + request = self.deep_extend(message, query) + orders = await self.watch(url, messageHash, request, messageHash, request) + return self.filter_by_symbol_since_limit(orders, symbol, since, limit, True) + + def handle_orders(self, client: Client, message): + # + # spot + # { + # "ts": 1574427585174, + # "event": "snapshot", + # "topic": "spot/orders", + # "data": [ + # { + # "order_id": "14", + # "client_id":"100500", + # "created": "1574427585", + # "pair": "BTC_USD", + # "price": "7750", + # "quantity": "0.1", + # "amount": "775", + # "original_quantity": "0.1", + # "original_amount": "775", + # "type": "sell", + # "status": "open" + # } + # ] + # } + # + # margin + # { + # "ts":1624371281773, + # "event":"snapshot", + # "topic":"margin/orders", + # "data":[ + # { + # "order_id":"692844278081168665", + # "created":"1624371250919761600", + # "type":"limit_buy", + # "previous_type":"limit_buy", + # "pair":"BTC_USD", + # "leverage":"2", + # "price":"10000", + # "stop_price":"0", + # "distance":"0", + # "trigger_price":"10000", + # "init_quantity":"0.1", + # "quantity":"0.1", + # "funding_currency":"USD", + # "funding_quantity":"1000", + # "funding_rate":"0", + # "client_id":"111111", + # "expire":0, + # "src":1, + # "comment":"comment1", + # "updated":1624371250938136600, + # "status":"active" + # } + # ] + # } + # + topic = self.safe_string(message, 'topic') + parts = topic.split('/') + type = self.safe_string(parts, 0) + messageHash = 'orders:' + type + event = self.safe_string(message, 'event') + if self.orders is None: + limit = self.safe_integer(self.options, 'ordersLimit', 1000) + self.orders = ArrayCacheBySymbolById(limit) + cachedOrders = self.orders + rawOrders = [] + if event == 'snapshot': + rawOrders = self.safe_value(message, 'data', []) + elif event == 'update': + rawOrder = self.safe_dict(message, 'data', {}) + rawOrders.append(rawOrder) + symbols: dict = {} + for j in range(0, len(rawOrders)): + order = self.parse_ws_order(rawOrders[j]) + cachedOrders.append(order) + symbols[order['symbol']] = True + symbolKeys = list(symbols.keys()) + for i in range(0, len(symbolKeys)): + symbol = symbolKeys[i] + symbolSpecificMessageHash = 'orders:' + symbol + client.resolve(cachedOrders, symbolSpecificMessageHash) + client.resolve(cachedOrders, messageHash) + + def parse_ws_order(self, order: dict, market: Market = None) -> Order: + # + # { + # order_id: '43226756791', + # client_id: 0, + # created: '1730371416', + # type: 'market_buy', + # pair: 'TRX_USD', + # quantity: '0', + # original_quantity: '30', + # status: 'cancelled', + # last_trade_id: '726480870', + # last_trade_price: '0.17', + # last_trade_quantity: '30' + # } + # + id = self.safe_string(order, 'order_id') + timestamp = self.safe_timestamp(order, 'created') + orderType = self.safe_string(order, 'type') + side = self.parseSide(orderType) + marketId = self.safe_string(order, 'pair') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + amount = self.safe_string(order, 'quantity') + if amount is None: + amountField = 'in_amount' if (side == 'buy') else 'out_amount' + amount = self.safe_string(order, amountField) + price = self.safe_string(order, 'price') + clientOrderId = self.omit_zero(self.safe_string(order, 'client_id')) + triggerPrice = self.omit_zero(self.safe_string(order, 'stop_price')) + type = None + if (orderType != 'buy') and (orderType != 'sell'): + type = orderType + trades = None + if 'last_trade_id' in order: + trade = self.parse_ws_trade(order, market) + trades = [trade] + return self.safe_order({ + 'id': id, + 'clientOrderId': clientOrderId, + 'datetime': self.iso8601(timestamp), + 'timestamp': timestamp, + 'lastTradeTimestamp': None, + 'status': self.parseStatus(self.safe_string(order, 'status')), + 'symbol': symbol, + 'type': type, + 'timeInForce': None, + 'postOnly': None, + 'side': side, + 'price': price, + 'stopPrice': triggerPrice, + 'triggerPrice': triggerPrice, + 'cost': None, + 'amount': self.safe_string(order, 'original_quantity'), + 'filled': None, + 'remaining': self.safe_string(order, 'quantity'), + 'average': None, + 'trades': trades, + 'fee': None, + 'info': order, + }, market) + + def parse_ws_trade(self, trade: dict, market: Market = None) -> Trade: + id = self.safe_string(trade, 'order_id') + orderType = self.safe_string(trade, 'type') + side = self.parseSide(orderType) + marketId = self.safe_string(trade, 'pair') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + type = None + if (orderType != 'buy') and (orderType != 'sell'): + type = orderType + return self.safe_trade({ + 'id': self.safe_string(trade, 'last_trade_id'), + 'symbol': symbol, + 'order': id, + 'type': type, + 'side': side, + 'price': self.safe_string(trade, 'last_trade_price'), + 'amount': self.safe_string(trade, 'last_trade_quantity'), + 'cost': None, + 'fee': None, + }, market) + + def handle_message(self, client: Client, message): + # + # { + # "ts": 1654206362552, + # "event": "info", + # "code": 1, + # "message": "connection established", + # "session_id": "7548931b-c2a4-45dd-8d71-877881a7251a" + # } + # + # { + # "ts": 1654206491399, + # "event": "subscribed", + # "id": 1, + # "topic": "spot/ticker:BTC_USDT" + # } + event = self.safe_string(message, 'event') + events: dict = { + 'logged_in': self.handle_authentication_message, + 'info': self.handle_info, + 'subscribed': self.handle_subscribed, + } + eventHandler = self.safe_value(events, event) + if eventHandler is not None: + eventHandler(client, message) + return + if (event == 'update') or (event == 'snapshot'): + topic = self.safe_string(message, 'topic') + if topic is not None: + parts = topic.split(':') + channel = self.safe_string(parts, 0) + handlers: dict = { + 'spot/ticker': self.handle_ticker, + 'spot/wallet': self.handle_balance, + 'margin/wallet': self.handle_balance, + 'margin/wallets': self.handle_balance, + 'spot/trades': self.handle_trades, + 'margin/trades': self.handle_trades, + 'spot/order_book_updates': self.handle_order_book, + 'spot/orders': self.handle_orders, + 'margin/orders': self.handle_orders, + 'spot/user_trades': self.handle_my_trades, + 'margin/user_trades': self.handle_my_trades, + } + handler = self.safe_value(handlers, channel) + if handler is not None: + handler(client, message) + return + raise NotSupported(self.id + ' received an unsupported message: ' + self.json(message)) + + def handle_subscribed(self, client: Client, message): + # + # { + # "method": "subscribe", + # "id": 2, + # "topics": ["spot/orders"] + # } + # + return message + + def handle_info(self, client: Client, message): + # + # { + # "ts": 1654215731659, + # "event": "info", + # "code": 1, + # "message": "connection established", + # "session_id": "4c496262-e259-4c27-b805-f20b46209c17" + # } + # + return message + + def handle_authentication_message(self, client: Client, message): + # + # { + # "method": "login", + # "id": 1, + # "api_key": "K-************************", + # "sign": "******************************************************************", + # "nonce": 1654215729887 + # } + # + messageHash = 'authenticated' + client.resolve(message, messageHash) + + async def authenticate(self, params={}): + messageHash = 'authenticated' + type, query = self.handle_market_type_and_params('authenticate', None, params) + url = self.urls['api']['ws'][type] + client = self.client(url) + future = self.safe_value(client.subscriptions, messageHash) + if future is None: + time = self.milliseconds() + self.check_required_credentials() + requestId = self.request_id() + signData = self.apiKey + str(time) + sign = self.hmac(self.encode(signData), self.encode(self.secret), hashlib.sha512, 'base64') + request: dict = { + 'method': 'login', + 'id': requestId, + 'api_key': self.apiKey, + 'sign': sign, + 'nonce': time, + } + message = self.extend(request, query) + future = await self.watch(url, messageHash, message, messageHash) + client.subscriptions[messageHash] = future + return future diff --git a/ccxt/pro/gate.py b/ccxt/pro/gate.py new file mode 100644 index 0000000..3734541 --- /dev/null +++ b/ccxt/pro/gate.py @@ -0,0 +1,1974 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolById, ArrayCacheBySymbolBySide, ArrayCacheByTimestamp +import hashlib +from ccxt.base.types import Any, Balances, Bool, Int, Liquidation, Market, MarketType, Num, Order, OrderBook, OrderRequest, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, Trade +from ccxt.async_support.base.ws.client import Client +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import NotSupported +from ccxt.base.errors import ChecksumError +from ccxt.base.precise import Precise + + +class gate(ccxt.async_support.gate): + + def describe(self) -> Any: + return self.deep_extend(super(gate, self).describe(), { + 'has': { + 'ws': True, + 'cancelAllOrdersWs': True, + 'cancelOrderWs': True, + 'createMarketBuyOrderWithCostWs': True, + 'createMarketOrderWs': True, + 'createMarketOrderWithCostWs': False, + 'createMarketSellOrderWithCostWs': False, + 'createOrderWs': True, + 'createOrdersWs': True, + 'createPostOnlyOrderWs': True, + 'createReduceOnlyOrderWs': True, + 'createStopLimitOrderWs': True, + 'createStopLossOrderWs': True, + 'createStopMarketOrderWs': False, + 'createStopOrderWs': True, + 'createTakeProfitOrderWs': True, + 'createTriggerOrderWs': True, + 'editOrderWs': True, + 'fetchOrderWs': True, + 'fetchOrdersWs': False, + 'fetchOpenOrdersWs': True, + 'fetchClosedOrdersWs': True, + 'watchOrderBook': True, + 'watchBidsAsks': True, + 'watchTicker': True, + 'watchTickers': True, + 'watchTrades': True, + 'watchTradesForSymbols': True, + 'watchMyTrades': True, + 'watchOHLCV': True, + 'watchBalance': True, + 'watchOrders': True, + 'watchLiquidations': False, + 'watchLiquidationsForSymbols': False, + 'watchMyLiquidations': True, + 'watchMyLiquidationsForSymbols': True, + 'watchPositions': True, + }, + 'urls': { + 'api': { + 'ws': 'wss://ws.gate.io/v4', + 'spot': 'wss://api.gateio.ws/ws/v4/', + 'swap': { + 'usdt': 'wss://fx-ws.gateio.ws/v4/ws/usdt', + 'btc': 'wss://fx-ws.gateio.ws/v4/ws/btc', + }, + 'future': { + 'usdt': 'wss://fx-ws.gateio.ws/v4/ws/delivery/usdt', + 'btc': 'wss://fx-ws.gateio.ws/v4/ws/delivery/btc', + }, + 'option': { + 'usdt': 'wss://op-ws.gateio.live/v4/ws/usdt', + 'btc': 'wss://op-ws.gateio.live/v4/ws/btc', + }, + }, + 'test': { + 'swap': { + 'usdt': 'wss://fx-ws-testnet.gateio.ws/v4/ws/usdt', + 'btc': 'wss://fx-ws-testnet.gateio.ws/v4/ws/btc', + }, + 'future': { + 'usdt': 'wss://fx-ws-testnet.gateio.ws/v4/ws/usdt', + 'btc': 'wss://fx-ws-testnet.gateio.ws/v4/ws/btc', + }, + 'option': { + 'usdt': 'wss://op-ws-testnet.gateio.live/v4/ws/usdt', + 'btc': 'wss://op-ws-testnet.gateio.live/v4/ws/btc', + }, + }, + }, + 'options': { + 'tradesLimit': 1000, + 'OHLCVLimit': 1000, + 'watchTradesSubscriptions': {}, + 'watchTickerSubscriptions': {}, + 'watchOrderBookSubscriptions': {}, + 'watchTicker': { + 'name': 'tickers', # or book_ticker + }, + 'watchOrderBook': { + 'interval': '100ms', + 'snapshotDelay': 10, # how many deltas to cache before fetching a snapshot + 'snapshotMaxRetries': 3, + 'checksum': True, + }, + 'watchBalance': { + 'settle': 'usdt', # or btc + 'spot': 'spot.balances', # spot.margin_balances, spot.funding_balances or spot.cross_balances + }, + 'watchPositions': { + 'fetchPositionsSnapshot': True, # or False + 'awaitPositionsSnapshot': True, # whether to wait for the positions snapshot before providing updates + }, + }, + 'exceptions': { + 'ws': { + 'exact': { + '1': BadRequest, + '2': BadRequest, + '4': AuthenticationError, + '6': AuthenticationError, + '11': AuthenticationError, + }, + 'broad': {}, + }, + }, + }) + + async def create_order_ws(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + + https://www.gate.io/docs/developers/apiv4/ws/en/#order-place + https://www.gate.io/docs/developers/futures/ws/en/#order-place + + Create an order on the exchange + :param str symbol: Unified CCXT market symbol + :param str type: 'limit' or 'market' *"market" is contract only* + :param str side: 'buy' or 'sell' + :param float amount: the amount of currency to trade + :param float [price]: *ignored in "market" orders* the price at which the order is to be fulfilled at in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.stopPrice]: The price at which a trigger order is triggered at + :param str [params.timeInForce]: "GTC", "IOC", or "PO" + :param float [params.stopLossPrice]: The price at which a stop loss order is triggered at + :param float [params.takeProfitPrice]: The price at which a take profit order is triggered at + :param str [params.marginMode]: 'cross' or 'isolated' - marginMode for margin trading if not provided self.options['defaultMarginMode'] is used + :param int [params.iceberg]: Amount to display for the iceberg order, Null or 0 for normal orders, Set to -1 to hide the order completely + :param str [params.text]: User defined information + :param str [params.account]: *spot and margin only* "spot", "margin" or "cross_margin" + :param bool [params.auto_borrow]: *margin only* Used in margin or cross margin trading to allow automatic loan of insufficient amount if balance is not enough + :param str [params.settle]: *contract only* Unified Currency Code for settle currency + :param bool [params.reduceOnly]: *contract only* Indicates if self order is to reduce the size of a position + :param bool [params.close]: *contract only* Set to close the position, with size set to 0 + :param bool [params.auto_size]: *contract only* Set side to close dual-mode position, close_long closes the long side, while close_short the short one, size also needs to be set to 0 + :param int [params.price_type]: *contract only* 0 latest deal price, 1 mark price, 2 index price + :param float [params.cost]: *spot market buy only* the quote quantity that can be used alternative for the amount + :returns dict|None: `An order structure ` + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + messageType = self.get_type_by_market(market) + channel = messageType + '.order_place' + url = self.get_url_by_market(market) + params['textIsRequired'] = True + request = self.create_order_request(symbol, type, side, amount, price, params) + await self.authenticate(url, messageType) + rawOrder = await self.request_private(url, request, channel) + order = self.parse_order(rawOrder, market) + return order + + async def create_orders_ws(self, orders: List[OrderRequest], params={}): + """ + create a list of trade orders + + https://www.gate.io/docs/developers/futures/ws/en/#order-batch-place + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + request = self.createOrdersRequest(orders, params) + firstOrder = orders[0] + market = self.market(firstOrder['symbol']) + if market['swap'] is not True: + raise NotSupported(self.id + ' createOrdersWs is not supported for swap markets') + messageType = self.get_type_by_market(market) + channel = messageType + '.order_batch_place' + url = self.get_url_by_market(market) + await self.authenticate(url, messageType) + rawOrders = await self.request_private(url, request, channel) + return self.parse_orders(rawOrders, market) + + async def cancel_all_orders_ws(self, symbol: Str = None, params={}): + """ + cancel all open orders + + https://www.gate.io/docs/developers/futures/ws/en/#cancel-all-open-orders-matched + https://www.gate.io/docs/developers/apiv4/ws/en/#order-cancel-all-with-specified-currency-pair + + :param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.channel]: the channel to use, defaults to spot.order_cancel_cp or futures.order_cancel_cp + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + market = None if (symbol is None) else self.market(symbol) + trigger = self.safe_bool_2(params, 'stop', 'trigger') + messageType = self.get_type_by_market(market) + channel = messageType + '.order_cancel_cp' + channel, params = self.handle_option_and_params(params, 'cancelAllOrdersWs', 'channel', channel) + url = self.get_url_by_market(market) + params = self.omit(params, ['stop', 'trigger']) + type, query = self.handle_market_type_and_params('cancelAllOrders', market, params) + request, requestParams = self.multiOrderSpotPrepareRequest(market, trigger, query) if (type == 'spot') else self.prepareRequest(market, type, query) + await self.authenticate(url, messageType) + rawOrders = await self.request_private(url, self.extend(request, requestParams), channel) + return self.parse_orders(rawOrders, market) + + async def cancel_order_ws(self, id: str, symbol: Str = None, params={}): + """ + Cancels an open order + + https://www.gate.io/docs/developers/apiv4/ws/en/#order-cancel + https://www.gate.io/docs/developers/futures/ws/en/#order-cancel + + :param str id: Order id + :param str symbol: Unified market symbol + :param dict [params]: Parameters specified by the exchange api + :param bool [params.trigger]: True if the order to be cancelled is a trigger order + :returns: An `order structure ` + """ + await self.load_markets() + market = None if (symbol is None) else self.market(symbol) + trigger = self.safe_value_n(params, ['is_stop_order', 'stop', 'trigger'], False) + params = self.omit(params, ['is_stop_order', 'stop', 'trigger']) + type, query = self.handle_market_type_and_params('cancelOrder', market, params) + request, requestParams = self.spotOrderPrepareRequest(market, trigger, query) if (type == 'spot' or type == 'margin') else self.prepareRequest(market, type, query) + messageType = self.get_type_by_market(market) + channel = messageType + '.order_cancel' + url = self.get_url_by_market(market) + await self.authenticate(url, messageType) + request['order_id'] = str(id) + res = await self.request_private(url, self.extend(request, requestParams), channel) + return self.parse_order(res, market) + + async def edit_order_ws(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + """ + edit a trade order, gate currently only supports the modification of the price or amount fields + + https://www.gate.io/docs/developers/apiv4/ws/en/#order-amend + https://www.gate.io/docs/developers/futures/ws/en/#order-amend + + :param str id: order id + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of the currency you want to trade in units of the base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + extendedRequest = self.edit_order_request(id, symbol, type, side, amount, price, params) + messageType = self.get_type_by_market(market) + channel = messageType + '.order_amend' + url = self.get_url_by_market(market) + await self.authenticate(url, messageType) + rawOrder = await self.request_private(url, extendedRequest, channel) + return self.parse_order(rawOrder, market) + + async def fetch_order_ws(self, id: str, symbol: Str = None, params={}): + """ + Retrieves information on an order + + https://www.gate.io/docs/developers/apiv4/ws/en/#order-status + https://www.gate.io/docs/developers/futures/ws/en/#order-status + + :param str id: Order id + :param str symbol: Unified market symbol, *required for spot and margin* + :param dict [params]: Parameters specified by the exchange api + :param bool [params.trigger]: True if the order being fetched is a trigger order + :param str [params.marginMode]: 'cross' or 'isolated' - marginMode for margin trading if not provided self.options['defaultMarginMode'] is used + :param str [params.type]: 'spot', 'swap', or 'future', if not provided self.options['defaultMarginMode'] is used + :param str [params.settle]: 'btc' or 'usdt' - settle currency for perpetual swap and future - market settle currency is used if symbol is not None, default="usdt" for swap and "btc" for future + :returns: An `order structure ` + """ + await self.load_markets() + market = None if (symbol is None) else self.market(symbol) + request, requestParams = self.fetchOrderRequest(id, symbol, params) + messageType = self.get_type_by_market(market) + channel = messageType + '.order_status' + url = self.get_url_by_market(market) + await self.authenticate(url, messageType) + rawOrder = await self.request_private(url, self.extend(request, requestParams), channel) + return self.parse_order(rawOrder, market) + + async def fetch_open_orders_ws(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://www.gate.io/docs/developers/futures/ws/en/#order-list + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + return await self.fetch_orders_by_status_ws('open', symbol, since, limit, params) + + async def fetch_closed_orders_ws(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://www.gate.io/docs/developers/futures/ws/en/#order-list + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + return await self.fetch_orders_by_status_ws('finished', symbol, since, limit, params) + + async def fetch_orders_by_status_ws(self, status: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + + https://www.gate.io/docs/developers/futures/ws/en/#order-list + + fetches information on multiple orders made by the user by status + :param str status: requested order status + :param str symbol: unified market symbol of the market orders were made in + :param int|None [since]: the earliest time in ms to fetch orders for + :param int|None [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.orderId]: order id to begin at + :param int [params.limit]: the maximum number of order structures to retrieve + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + if market['swap'] is not True: + raise NotSupported(self.id + ' fetchOrdersByStatusWs is only supported by swap markets. Use rest API for other markets') + request, requestParams = self.prepareOrdersByStatusRequest(status, symbol, since, limit, params) + newRequest = self.omit(request, ['settle']) + messageType = self.get_type_by_market(market) + channel = messageType + '.order_list' + url = self.get_url_by_market(market) + await self.authenticate(url, messageType) + rawOrders = await self.request_private(url, self.extend(newRequest, requestParams), channel) + orders = self.parse_orders(rawOrders, market) + return self.filter_by_symbol_since_limit(orders, symbol, since, limit) + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://www.gate.com/docs/developers/apiv4/ws/en/#order-book-channel + https://www.gate.com/docs/developers/apiv4/ws/en/#order-book-v2-api + https://www.gate.com/docs/developers/futures/ws/en/#order-book-api + https://www.gate.com/docs/developers/futures/ws/en/#order-book-v2-api + https://www.gate.com/docs/developers/delivery/ws/en/#order-book-api + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + marketId = market['id'] + interval, query = self.handle_option_and_params(params, 'watchOrderBook', 'interval', '100ms') + messageType = self.get_type_by_market(market) + channel = messageType + '.order_book_update' + messageHash = 'orderbook' + ':' + symbol + url = self.get_url_by_market(market) + payload = [marketId, interval] + if limit is None: + limit = 100 # max 100 atm + if market['contract']: + stringLimit = str(limit) + payload.append(stringLimit) + subscription: dict = { + 'symbol': symbol, + 'limit': limit, + } + orderbook = await self.subscribe_public(url, messageHash, payload, channel, query, subscription) + return orderbook.limit() + + async def un_watch_order_book(self, symbol: str, params={}) -> Any: + """ + unWatches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str symbol: unified symbol of the market to fetch the order book for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + marketId = market['id'] + interval = '100ms' + interval, params = self.handle_option_and_params(params, 'watchOrderBook', 'interval', interval) + messageType = self.get_type_by_market(market) + channel = messageType + '.order_book_update' + subMessageHash = 'orderbook' + ':' + symbol + messageHash = 'unsubscribe:orderbook' + ':' + symbol + url = self.get_url_by_market(market) + payload = [marketId, interval] + limit = self.safe_integer(params, 'limit', 100) + if market['contract']: + stringLimit = str(limit) + payload.append(stringLimit) + return await self.un_subscribe_public_multiple(url, 'orderbook', [symbol], [messageHash], [subMessageHash], payload, channel, params) + + def handle_order_book_subscription(self, client: Client, message, subscription): + symbol = self.safe_string(subscription, 'symbol') + limit = self.safe_integer(subscription, 'limit') + self.orderbooks[symbol] = self.order_book({}, limit) + + def handle_order_book(self, client: Client, message): + # + # spot + # + # { + # "time": 1650189272, + # "channel": "spot.order_book_update", + # "event": "update", + # "result": { + # "t": 1650189272515, + # "e": "depthUpdate", + # "E": 1650189272, + # "s": "GMT_USDT", + # "U": 140595902, + # "u": 140595902, + # "b": [ + # ['2.51518', "228.119"], + # ['2.50587', "1510.11"], + # ['2.49944', "67.6"], + # ], + # "a": [ + # ['2.5182', "4.199"], + # ["2.51926", "1874"], + # ['2.53528', "96.529"], + # ] + # } + # } + # + # swap + # + # { + # "id": null, + # "time": 1650188898, + # "channel": "futures.order_book_update", + # "event": "update", + # "error": null, + # "result": { + # "t": 1650188898938, + # "s": "GMT_USDT", + # "U": 1577718307, + # "u": 1577719254, + # "b": [ + # {p: "2.5178", s: 0}, + # {p: "2.5179", s: 0}, + # {p: "2.518", s: 0}, + # ], + # "a": [ + # {p: "2.52", s: 0}, + # {p: "2.5201", s: 0}, + # {p: "2.5203", s: 0}, + # ] + # } + # } + # + channel = self.safe_string(message, 'channel') + channelParts = channel.split('.') + rawMarketType = self.safe_string(channelParts, 0) + isSpot = rawMarketType == 'spot' + marketType = 'spot' if isSpot else 'contract' + delta = self.safe_value(message, 'result') + deltaStart = self.safe_integer(delta, 'U') + deltaEnd = self.safe_integer(delta, 'u') + marketId = self.safe_string(delta, 's') + symbol = self.safe_symbol(marketId, None, '_', marketType) + messageHash = 'orderbook:' + symbol + storedOrderBook = self.safe_value(self.orderbooks, symbol, self.order_book({})) + nonce = self.safe_integer(storedOrderBook, 'nonce') + if nonce is None: + cacheLength = 0 + if storedOrderBook is not None: + cacheLength = len(storedOrderBook.cache) + snapshotDelay = self.handle_option('watchOrderBook', 'snapshotDelay', 10) + waitAmount = snapshotDelay if isSpot else 0 + if cacheLength == waitAmount: + # max limit is 100 + subscription = client.subscriptions[messageHash] + limit = self.safe_integer(subscription, 'limit') + self.spawn(self.load_order_book, client, messageHash, symbol, limit, {}) # needed for c#, number of args needs to match + storedOrderBook.cache.append(delta) + return + elif nonce >= deltaEnd: + return + elif nonce >= deltaStart - 1: + self.handle_delta(storedOrderBook, delta) + else: + del client.subscriptions[messageHash] + del self.orderbooks[symbol] + checksum = self.handle_option('watchOrderBook', 'checksum', True) + if checksum: + error = ChecksumError(self.id + ' ' + self.orderbook_checksum_message(symbol)) + client.reject(error, messageHash) + client.resolve(storedOrderBook, messageHash) + + def get_cache_index(self, orderBook, cache): + nonce = self.safe_integer(orderBook, 'nonce') + firstDelta = cache[0] + firstDeltaStart = self.safe_integer(firstDelta, 'U') + if nonce < firstDeltaStart: + return -1 + for i in range(0, len(cache)): + delta = cache[i] + deltaStart = self.safe_integer(delta, 'U') + deltaEnd = self.safe_integer(delta, 'u') + if (nonce >= deltaStart - 1) and (nonce < deltaEnd): + return i + return len(cache) + + def handle_bid_asks(self, bookSide, bidAsks): + for i in range(0, len(bidAsks)): + bidAsk = bidAsks[i] + if isinstance(bidAsk, list): + bookSide.storeArray(self.parse_bid_ask(bidAsk)) + else: + price = self.safe_float(bidAsk, 'p') + amount = self.safe_float(bidAsk, 's') + bookSide.store(price, amount) + + def handle_delta(self, orderbook, delta): + timestamp = self.safe_integer(delta, 't') + orderbook['timestamp'] = timestamp + orderbook['datetime'] = self.iso8601(timestamp) + orderbook['nonce'] = self.safe_integer(delta, 'u') + bids = self.safe_value(delta, 'b', []) + asks = self.safe_value(delta, 'a', []) + storedBids = orderbook['bids'] + storedAsks = orderbook['asks'] + self.handle_bid_asks(storedBids, bids) + self.handle_bid_asks(storedAsks, asks) + + async def watch_ticker(self, symbol: str, params={}) -> Ticker: + """ + + https://www.gate.io/docs/developers/apiv4/ws/en/#tickers-channel + + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + params['callerMethodName'] = 'watchTicker' + result = await self.watch_tickers([symbol], params) + return self.safe_value(result, symbol) + + async def watch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + + https://www.gate.io/docs/developers/apiv4/ws/en/#tickers-channel + + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list + :param str[] symbols: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + return await self.subscribe_watch_tickers_and_bids_asks(symbols, 'watchTickers', self.extend({'method': 'tickers'}, params)) + + def handle_ticker(self, client: Client, message): + # + # { + # "time": 1649326221, + # "channel": "spot.tickers", + # "event": "update", + # "result": { + # "currency_pair": "BTC_USDT", + # "last": "43444.82", + # "lowest_ask": "43444.82", + # "highest_bid": "43444.81", + # "change_percentage": "-4.0036", + # "base_volume": "5182.5412425462", + # "quote_volume": "227267634.93123952", + # "high_24h": "47698", + # "low_24h": "42721.03" + # } + # } + # + self.handle_ticker_and_bid_ask('ticker', client, message) + + async def watch_bids_asks(self, symbols: Strings = None, params={}) -> Tickers: + """ + + https://www.gate.io/docs/developers/apiv4/ws/en/#best-bid-or-ask-price + https://www.gate.io/docs/developers/apiv4/ws/en/#order-book-channel + + watches best bid & ask for symbols + :param str[] symbols: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + return await self.subscribe_watch_tickers_and_bids_asks(symbols, 'watchBidsAsks', self.extend({'method': 'book_ticker'}, params)) + + def handle_bid_ask(self, client: Client, message): + # + # { + # "time": 1671363004, + # "time_ms": 1671363004235, + # "channel": "spot.book_ticker", + # "event": "update", + # "result": { + # "t": 1671363004228, + # "u": 9793320464, + # "s": "BTC_USDT", + # "b": "16716.8", + # "B": "0.0134", + # "a": "16716.9", + # "A": "0.0353" + # } + # } + # + self.handle_ticker_and_bid_ask('bidask', client, message) + + async def subscribe_watch_tickers_and_bids_asks(self, symbols: Strings = None, callerMethodName: Str = None, params={}) -> Tickers: + await self.load_markets() + callerMethodName, params = self.handle_param_string(params, 'callerMethodName', callerMethodName) + symbols = self.market_symbols(symbols, None, False) + market = self.market(symbols[0]) + messageType = self.get_type_by_market(market) + marketIds = self.market_ids(symbols) + channelName = None + channelName, params = self.handle_option_and_params(params, callerMethodName, 'method') + url = self.get_url_by_market(market) + channel = messageType + '.' + channelName + isWatchTickers = callerMethodName.find('watchTicker') >= 0 + prefix = 'ticker' if isWatchTickers else 'bidask' + messageHashes = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + messageHashes.append(prefix + ':' + symbol) + tickerOrBidAsk = await self.subscribe_public_multiple(url, messageHashes, marketIds, channel, params) + if self.newUpdates: + items: dict = {} + items[tickerOrBidAsk['symbol']] = tickerOrBidAsk + return items + result = self.tickers if isWatchTickers else self.bidsasks + return self.filter_by_array(result, 'symbol', symbols, True) + + def handle_ticker_and_bid_ask(self, objectName: str, client: Client, message): + channel = self.safe_string(message, 'channel') + parts = channel.split('.') + rawMarketType = self.safe_string(parts, 0) + marketType = 'contract' if (rawMarketType == 'futures') else 'spot' + result = self.safe_value(message, 'result') + results = [] + if isinstance(result, list): + results = self.safe_list(message, 'result', []) + else: + rawTicker = self.safe_dict(message, 'result', {}) + results = [rawTicker] + isTicker = (objectName == 'ticker') # whether ticker or bid-ask + for i in range(0, len(results)): + rawTicker = results[i] + marketId = self.safe_string(rawTicker, 's') + market = self.safe_market(marketId, None, '_', marketType) + parsedItem = self.parse_ticker(rawTicker, market) + symbol = parsedItem['symbol'] + if isTicker: + self.tickers[symbol] = parsedItem + else: + self.bidsasks[symbol] = parsedItem + messageHash = objectName + ':' + symbol + client.resolve(parsedItem, messageHash) + + async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + return await self.watch_trades_for_symbols([symbol], since, limit, params) + + async def watch_trades_for_symbols(self, symbols: List[str], since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + :param str[] symbols: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + marketIds = self.market_ids(symbols) + market = self.market(symbols[0]) + messageType = self.get_type_by_market(market) + channel = messageType + '.trades' + messageHashes = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + messageHashes.append('trades:' + symbol) + url = self.get_url_by_market(market) + trades = await self.subscribe_public_multiple(url, messageHashes, marketIds, channel, params) + if self.newUpdates: + first = self.safe_value(trades, 0) + tradeSymbol = self.safe_string(first, 'symbol') + limit = trades.getLimit(tradeSymbol, limit) + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + async def un_watch_trades_for_symbols(self, symbols: List[str], params={}) -> Any: + """ + get the list of most recent trades for a particular symbol + :param str[] symbols: unified symbol of the market to fetch trades for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + marketIds = self.market_ids(symbols) + market = self.market(symbols[0]) + messageType = self.get_type_by_market(market) + channel = messageType + '.trades' + subMessageHashes = [] + messageHashes = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + subMessageHashes.append('trades:' + symbol) + messageHashes.append('unsubscribe:trades:' + symbol) + url = self.get_url_by_market(market) + return await self.un_subscribe_public_multiple(url, 'trades', symbols, messageHashes, subMessageHashes, marketIds, channel, params) + + async def un_watch_trades(self, symbol: str, params={}) -> Any: + """ + get the list of most recent trades for a particular symbol + :param str symbol: unified symbol of the market to fetch trades for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + return await self.un_watch_trades_for_symbols([symbol], params) + + def handle_trades(self, client: Client, message): + # + # { + # "time": 1648725035, + # "channel": "spot.trades", + # "event": "update", + # "result": [{ + # "id": 3130257995, + # "create_time": 1648725035, + # "create_time_ms": "1648725035923.0", + # "side": "sell", + # "currency_pair": "LTC_USDT", + # "amount": "0.0116", + # "price": "130.11" + # }] + # } + # + result = self.safe_value(message, 'result') + if not isinstance(result, list): + result = [result] + parsedTrades = self.parse_trades(result) + for i in range(0, len(parsedTrades)): + trade = parsedTrades[i] + symbol = trade['symbol'] + cachedTrades = self.safe_value(self.trades, symbol) + if cachedTrades is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + cachedTrades = ArrayCache(limit) + self.trades[symbol] = cachedTrades + cachedTrades.append(trade) + hash = 'trades:' + symbol + client.resolve(cachedTrades, hash) + + async def watch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + watches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + marketId = market['id'] + interval = self.safe_string(self.timeframes, timeframe, timeframe) + messageType = self.get_type_by_market(market) + channel = messageType + '.candlesticks' + messageHash = 'candles:' + interval + ':' + market['symbol'] + url = self.get_url_by_market(market) + payload = [interval, marketId] + ohlcv = await self.subscribe_public(url, messageHash, payload, channel, params) + if self.newUpdates: + limit = ohlcv.getLimit(symbol, limit) + return self.filter_by_since_limit(ohlcv, since, limit, 0, True) + + def handle_ohlcv(self, client: Client, message): + # + # { + # "time": 1606292600, + # "channel": "spot.candlesticks", + # "event": "update", + # "result": { + # "t": "1606292580", # total volume + # "v": "2362.32035", # volume + # "c": "19128.1", # close + # "h": "19128.1", # high + # "l": "19128.1", # low + # "o": "19128.1", # open + # "n": "1m_BTC_USDT" # sub + # } + # } + # + channel = self.safe_string(message, 'channel') + channelParts = channel.split('.') + rawMarketType = self.safe_string(channelParts, 0) + marketType = 'spot' if (rawMarketType == 'spot') else 'contract' + result = self.safe_value(message, 'result') + if not isinstance(result, list): + result = [result] + marketIds: dict = {} + for i in range(0, len(result)): + ohlcv = result[i] + subscription = self.safe_string(ohlcv, 'n', '') + parts = subscription.split('_') + timeframe = self.safe_string(parts, 0) + timeframeId = self.find_timeframe(timeframe) + prefix = timeframe + '_' + marketId = subscription.replace(prefix, '') + symbol = self.safe_symbol(marketId, None, '_', marketType) + parsed = self.parse_ohlcv(ohlcv) + self.ohlcvs[symbol] = self.safe_value(self.ohlcvs, symbol, {}) + stored = self.safe_value(self.ohlcvs[symbol], timeframe) + if stored is None: + limit = self.safe_integer(self.options, 'OHLCVLimit', 1000) + stored = ArrayCacheByTimestamp(limit) + self.ohlcvs[symbol][timeframeId] = stored + stored.append(parsed) + marketIds[symbol] = timeframe + keys = list(marketIds.keys()) + for i in range(0, len(keys)): + symbol = keys[i] + timeframe = marketIds[symbol] + interval = self.find_timeframe(timeframe) + hash = 'candles' + ':' + interval + ':' + symbol + stored = self.safe_value(self.ohlcvs[symbol], interval) + client.resolve(stored, hash) + + async def watch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + watches information on multiple trades made by the user + :param str symbol: unified market symbol of the market trades were made in + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trade structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + subType = None + type = None + marketId = '!' + 'all' + market = None + if symbol is not None: + market = self.market(symbol) + marketId = market['id'] + type, params = self.handle_market_type_and_params('watchMyTrades', market, params) + subType, params = self.handle_sub_type_and_params('watchMyTrades', market, params) + messageType = self.get_supported_mapping(type, { + 'spot': 'spot', + 'margin': 'spot', + 'future': 'futures', + 'swap': 'futures', + 'option': 'options', + }) + channel = messageType + '.usertrades' + messageHash = 'myTrades' + if symbol is not None: + messageHash += ':' + symbol + isInverse = (subType == 'inverse') + url = self.get_url_by_market_type(type, isInverse) + payload = [marketId] + # uid required for non spot markets + requiresUid = (type != 'spot') + trades = await self.subscribe_private(url, messageHash, payload, channel, params, requiresUid) + if self.newUpdates: + limit = trades.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(trades, symbol, since, limit, True) + + def handle_my_trades(self, client: Client, message): + # + # { + # "time": 1543205083, + # "channel": "futures.usertrades", + # "event": "update", + # "error": null, + # "result": [ + # { + # "id": "3335259", + # "create_time": 1628736848, + # "create_time_ms": 1628736848321, + # "contract": "BTC_USD", + # "order_id": "4872460", + # "size": 1, + # "price": "40000.4", + # "role": "maker" + # } + # ] + # } + # + result = self.safe_value(message, 'result', []) + tradesLength = len(result) + if tradesLength == 0: + return + cachedTrades = self.myTrades + if cachedTrades is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + cachedTrades = ArrayCacheBySymbolById(limit) + self.myTrades = cachedTrades + parsed = self.parse_trades(result) + marketIds: dict = {} + for i in range(0, len(parsed)): + trade = parsed[i] + cachedTrades.append(trade) + symbol = trade['symbol'] + marketIds[symbol] = True + keys = list(marketIds.keys()) + for i in range(0, len(keys)): + market = keys[i] + hash = 'myTrades:' + market + client.resolve(cachedTrades, hash) + client.resolve(cachedTrades, 'myTrades') + + async def watch_balance(self, params={}) -> Balances: + """ + watch balance and get the amount of funds available for trading or funds locked in orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + type = None + subType = None + type, params = self.handle_market_type_and_params('watchBalance', None, params) + subType, params = self.handle_sub_type_and_params('watchBalance', None, params) + isInverse = (subType == 'inverse') + url = self.get_url_by_market_type(type, isInverse) + requiresUid = (type != 'spot') + channelType = self.get_supported_mapping(type, { + 'spot': 'spot', + 'margin': 'spot', + 'future': 'futures', + 'swap': 'futures', + 'option': 'options', + }) + channel = channelType + '.balances' + messageHash = type + '.balance' + return await self.subscribe_private(url, messageHash, None, channel, params, requiresUid) + + def handle_balance(self, client: Client, message): + # + # spot order fill + # { + # "time": 1653664351, + # "channel": "spot.balances", + # "event": "update", + # "result": [ + # { + # "timestamp": "1653664351", + # "timestamp_ms": "1653664351017", + # "user": "10406147", + # "currency": "LTC", + # "change": "-0.0002000000000000", + # "total": "0.09986000000000000000", + # "available": "0.09986000000000000000" + # } + # ] + # } + # + # account transfer + # + # { + # "id": null, + # "time": 1653665088, + # "channel": "futures.balances", + # "event": "update", + # "error": null, + # "result": [ + # { + # "balance": 25.035008537, + # "change": 25, + # "text": "-", + # "time": 1653665088, + # "time_ms": 1653665088286, + # "type": "dnw", + # "user": "10406147" + # } + # ] + # } + # + # swap order fill + # { + # "id": null, + # "time": 1653665311, + # "channel": "futures.balances", + # "event": "update", + # "error": null, + # "result": [ + # { + # "balance": 20.031873037, + # "change": -0.0031355, + # "text": "LTC_USDT:165551103273", + # "time": 1653665311, + # "time_ms": 1653665311437, + # "type": "fee", + # "user": "10406147" + # } + # ] + # } + # + result = self.safe_value(message, 'result', []) + timestamp = self.safe_integer(message, 'time_ms') + self.balance['info'] = result + self.balance['timestamp'] = timestamp + self.balance['datetime'] = self.iso8601(timestamp) + for i in range(0, len(result)): + rawBalance = result[i] + account = self.account() + currencyId = self.safe_string(rawBalance, 'currency', 'USDT') # when not present it is USDT + code = self.safe_currency_code(currencyId) + account['free'] = self.safe_string(rawBalance, 'available') + account['total'] = self.safe_string_2(rawBalance, 'total', 'balance') + self.balance[code] = account + channel = self.safe_string(message, 'channel') + parts = channel.split('.') + rawType = self.safe_string(parts, 0) + channelType = self.get_supported_mapping(rawType, { + 'spot': 'spot', + 'futures': 'swap', + 'options': 'option', + }) + messageHash = channelType + '.balance' + self.balance = self.safe_balance(self.balance) + client.resolve(self.balance, messageHash) + + async def watch_positions(self, symbols: Strings = None, since: Int = None, limit: Int = None, params={}) -> List[Position]: + """ + + https://www.gate.io/docs/developers/futures/ws/en/#positions-subscription + https://www.gate.io/docs/developers/delivery/ws/en/#positions-subscription + https://www.gate.io/docs/developers/options/ws/en/#positions-channel + + watch all open positions + :param str[] [symbols]: list of unified market symbols to watch positions for + :param int [since]: the earliest time in ms to fetch positions for + :param int [limit]: the maximum number of positions to retrieve + :param dict params: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + await self.load_markets() + market = None + symbols = self.market_symbols(symbols) + payload = ['!' + 'all'] + if not self.is_empty(symbols): + market = self.get_market_from_symbols(symbols) + type = None + query = None + type, query = self.handle_market_type_and_params('watchPositions', market, params) + if type == 'spot': + type = 'swap' + typeId = self.get_supported_mapping(type, { + 'future': 'futures', + 'swap': 'futures', + 'option': 'options', + }) + messageHash = type + ':positions' + if not self.is_empty(symbols): + messageHash += '::' + ','.join(symbols) + channel = typeId + '.positions' + subType = None + subType, query = self.handle_sub_type_and_params('watchPositions', market, query) + isInverse = (subType == 'inverse') + url = self.get_url_by_market_type(type, isInverse) + client = self.client(url) + self.set_positions_cache(client, type, symbols) + fetchPositionsSnapshot = self.handle_option('watchPositions', 'fetchPositionsSnapshot', True) + awaitPositionsSnapshot = self.handle_option('watchPositions', 'awaitPositionsSnapshot', True) + cache = self.safe_value(self.positions, type) + if fetchPositionsSnapshot and awaitPositionsSnapshot and cache is None: + return await client.future(type + ':fetchPositionsSnapshot') + positions = await self.subscribe_private(url, messageHash, payload, channel, query, True) + if self.newUpdates: + return positions + return self.filter_by_symbols_since_limit(self.positions[type], symbols, since, limit, True) + + def set_positions_cache(self, client: Client, type, symbols: Strings = None): + if self.positions is None: + self.positions = {} + if type in self.positions: + return + fetchPositionsSnapshot = self.handle_option('watchPositions', 'fetchPositionsSnapshot', False) + if fetchPositionsSnapshot: + messageHash = type + ':fetchPositionsSnapshot' + if not (messageHash in client.futures): + client.future(messageHash) + self.spawn(self.load_positions_snapshot, client, messageHash, type) + else: + self.positions[type] = ArrayCacheBySymbolBySide() + + async def load_positions_snapshot(self, client, messageHash, type): + positions = await self.fetch_positions(None, {'type': type}) + self.positions[type] = ArrayCacheBySymbolBySide() + cache = self.positions[type] + for i in range(0, len(positions)): + position = positions[i] + contracts = self.safe_number(position, 'contracts', 0) + if contracts > 0: + cache.append(position) + # don't remove the future from the .futures cache + future = client.futures[messageHash] + future.resolve(cache) + client.resolve(cache, type + ':position') + + def handle_positions(self, client, message): + # + # { + # time: 1693158497, + # time_ms: 1693158497204, + # channel: 'futures.positions', + # event: 'update', + # result: [{ + # contract: 'XRP_USDT', + # cross_leverage_limit: 0, + # entry_price: 0.5253, + # history_pnl: 0, + # history_point: 0, + # last_close_pnl: 0, + # leverage: 0, + # leverage_max: 50, + # liq_price: 0.0361, + # maintenance_rate: 0.01, + # margin: 4.89609962852, + # mode: 'single', + # realised_pnl: -0.0026265, + # realised_point: 0, + # risk_limit: 500000, + # size: 1, + # time: 1693158497, + # time_ms: 1693158497195, + # update_id: 1, + # user: '10444586' + # }] + # } + # + type = self.get_market_type_by_url(client.url) + data = self.safe_value(message, 'result', []) + cache = self.positions[type] + newPositions = [] + for i in range(0, len(data)): + rawPosition = data[i] + position = self.parse_position(rawPosition) + symbol = self.safe_string(position, 'symbol') + side = self.safe_string(position, 'side') + # Control when position is closed no side is returned + if side is None: + prevLongPosition = self.safe_dict(cache, symbol + 'long') + if prevLongPosition is not None: + position['side'] = prevLongPosition['side'] + newPositions.append(position) + cache.append(position) + prevShortPosition = self.safe_dict(cache, symbol + 'short') + if prevShortPosition is not None: + position['side'] = prevShortPosition['side'] + newPositions.append(position) + cache.append(position) + # if no prev position is found, default to long + if prevLongPosition is None and prevShortPosition is None: + position['side'] = 'long' + newPositions.append(position) + cache.append(position) + else: + newPositions.append(position) + cache.append(position) + messageHashes = self.find_message_hashes(client, type + ':positions::') + for i in range(0, len(messageHashes)): + messageHash = messageHashes[i] + parts = messageHash.split('::') + symbolsString = parts[1] + symbols = symbolsString.split(',') + positions = self.filter_by_array(newPositions, 'symbol', symbols, False) + if not self.is_empty(positions): + client.resolve(positions, messageHash) + client.resolve(newPositions, type + ':positions') + + async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + watches information on multiple orders made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: spot, margin, swap, future, or option. Required if listening to all symbols. + :param boolean [params.isInverse]: if future, listen to inverse or linear contracts + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + type = None + query = None + type, query = self.handle_market_type_and_params('watchOrders', market, params) + typeId = self.get_supported_mapping(type, { + 'spot': 'spot', + 'margin': 'spot', + 'future': 'futures', + 'swap': 'futures', + 'option': 'options', + }) + channel = typeId + '.orders' + messageHash = 'orders' + payload = ['!' + 'all'] + if symbol is not None: + messageHash += ':' + market['id'] + payload = [market['id']] + subType = None + subType, query = self.handle_sub_type_and_params('watchOrders', market, query) + isInverse = (subType == 'inverse') + url = self.get_url_by_market_type(type, isInverse) + # uid required for non spot markets + requiresUid = (type != 'spot') + orders = await self.subscribe_private(url, messageHash, payload, channel, query, requiresUid) + if self.newUpdates: + limit = orders.getLimit(symbol, limit) + return self.filter_by_since_limit(orders, since, limit, 'timestamp', True) + + def handle_order(self, client: Client, message): + # + # { + # "time": 1605175506, + # "channel": "spot.orders", + # "event": "update", + # "result": [ + # { + # "id": "30784435", + # "user": 123456, + # "text": "t-abc", + # "create_time": "1605175506", + # "create_time_ms": "1605175506123", + # "update_time": "1605175506", + # "update_time_ms": "1605175506123", + # "event": "put", + # "currency_pair": "BTC_USDT", + # "type": "limit", + # "account": "spot", + # "side": "sell", + # "amount": "1", + # "price": "10001", + # "time_in_force": "gtc", + # "left": "1", + # "filled_total": "0", + # "fee": "0", + # "fee_currency": "USDT", + # "point_fee": "0", + # "gt_fee": "0", + # "gt_discount": True, + # "rebated_fee": "0", + # "rebated_fee_currency": "USDT" + # } + # ] + # } + # + orders = self.safe_value(message, 'result', []) + limit = self.safe_integer(self.options, 'ordersLimit', 1000) + if self.orders is None: + self.orders = ArrayCacheBySymbolById(limit) + stored = self.orders + marketIds: dict = {} + parsedOrders = self.parse_orders(orders) + for i in range(0, len(parsedOrders)): + parsed = parsedOrders[i] + # inject order status + info = self.safe_value(parsed, 'info') + event = self.safe_string(info, 'event') + if event == 'put' or event == 'update': + parsed['status'] = 'open' + elif event == 'finish': + status = self.safe_string(parsed, 'status') + if status is None: + left = self.safe_integer(info, 'left') + parsed['status'] = 'closed' if (left == 0) else 'canceled' + stored.append(parsed) + symbol = parsed['symbol'] + market = self.market(symbol) + marketIds[market['id']] = True + keys = list(marketIds.keys()) + for i in range(0, len(keys)): + messageHash = 'orders:' + keys[i] + client.resolve(self.orders, messageHash) + client.resolve(self.orders, 'orders') + + async def watch_my_liquidations(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Liquidation]: + """ + watch the public liquidations of a trading pair + + https://www.gate.io/docs/developers/futures/ws/en/#liquidates-api + https://www.gate.io/docs/developers/delivery/ws/en/#liquidates-api + https://www.gate.io/docs/developers/options/ws/en/#liquidates-channel + + :param str symbol: unified CCXT market symbol + :param int [since]: the earliest time in ms to fetch liquidations for + :param int [limit]: the maximum number of liquidation structures to retrieve + :param dict [params]: exchange specific parameters for the bitmex api endpoint + :returns dict: an array of `liquidation structures ` + """ + return self.watch_my_liquidations_for_symbols([symbol], since, limit, params) + + async def watch_my_liquidations_for_symbols(self, symbols: List[str], since: Int = None, limit: Int = None, params={}) -> List[Liquidation]: + """ + watch the private liquidations of a trading pair + + https://www.gate.io/docs/developers/futures/ws/en/#liquidates-api + https://www.gate.io/docs/developers/delivery/ws/en/#liquidates-api + https://www.gate.io/docs/developers/options/ws/en/#liquidates-channel + + :param str[] symbols: unified CCXT market symbols + :param int [since]: the earliest time in ms to fetch liquidations for + :param int [limit]: the maximum number of liquidation structures to retrieve + :param dict [params]: exchange specific parameters for the gate api endpoint + :returns dict: an array of `liquidation structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, True, True) + market = self.get_market_from_symbols(symbols) + type = None + query = None + type, query = self.handle_market_type_and_params('watchMyLiquidationsForSymbols', market, params) + typeId = self.get_supported_mapping(type, { + 'future': 'futures', + 'swap': 'futures', + 'option': 'options', + }) + subType = None + subType, query = self.handle_sub_type_and_params('watchMyLiquidationsForSymbols', market, query) + isInverse = (subType == 'inverse') + url = self.get_url_by_market_type(type, isInverse) + payload = [] + messageHash = '' + if self.is_empty(symbols): + if typeId != 'futures' and not isInverse: + raise BadRequest(self.id + ' watchMyLiquidationsForSymbols() does not support listening to all symbols, you must call watchMyLiquidations() instead for each symbol you wish to watch.') + messageHash = 'myLiquidations' + payload.append('not all') + else: + symbolsLength = len(symbols) + if symbolsLength != 1: + raise BadRequest(self.id + ' watchMyLiquidationsForSymbols() only allows one symbol at a time. To listen to several symbols call watchMyLiquidationsForSymbols() several times.') + messageHash = 'myLiquidations::' + symbols[0] + payload.append(market['id']) + channel = typeId + '.liquidates' + newLiquidations = await self.subscribe_private(url, messageHash, payload, channel, query, True) + if self.newUpdates: + return newLiquidations + return self.filter_by_symbols_since_limit(self.liquidations, symbols, since, limit, True) + + def handle_liquidation(self, client: Client, message): + # + # future / delivery + # { + # "channel":"futures.liquidates", + # "event":"update", + # "time":1541505434, + # "time_ms":1541505434123, + # "result":[ + # { + # "entry_price":209, + # "fill_price":215.1, + # "left":0, + # "leverage":0.0, + # "liq_price":213, + # "margin":0.007816722941, + # "mark_price":213, + # "order_id":4093362, + # "order_price":215.1, + # "size":-124, + # "time":1541486601, + # "time_ms":1541486601123, + # "contract":"BTC_USD", + # "user":"1040xxxx" + # } + # ] + # } + # option + # { + # "channel":"options.liquidates", + # "event":"update", + # "time":1630654851, + # "result":[ + # { + # "user":"1xxxx", + # "init_margin":1190, + # "maint_margin":1042.5, + # "order_margin":0, + # "time":1639051907, + # "time_ms":1639051907000 + # } + # ] + # } + # + rawLiquidations = self.safe_list(message, 'result', []) + newLiquidations = [] + for i in range(0, len(rawLiquidations)): + rawLiquidation = rawLiquidations[i] + liquidation = self.parse_ws_liquidation(rawLiquidation) + symbol = self.safe_string(liquidation, 'symbol') + liquidations = self.safe_value(self.liquidations, symbol) + if liquidations is None: + limit = self.safe_integer(self.options, 'liquidationsLimit', 1000) + liquidations = ArrayCache(limit) + liquidations.append(liquidation) + self.liquidations[symbol] = liquidations + client.resolve(liquidations, 'myLiquidations::' + symbol) + client.resolve(newLiquidations, 'myLiquidations') + + def parse_ws_liquidation(self, liquidation, market=None): + # + # future / delivery + # { + # "entry_price": 209, + # "fill_price": 215.1, + # "left": 0, + # "leverage": 0.0, + # "liq_price": 213, + # "margin": 0.007816722941, + # "mark_price": 213, + # "order_id": 4093362, + # "order_price": 215.1, + # "size": -124, + # "time": 1541486601, + # "time_ms": 1541486601123, + # "contract": "BTC_USD", + # "user": "1040xxxx" + # } + # option + # { + # "user": "1xxxx", + # "init_margin": 1190, + # "maint_margin": 1042.5, + # "order_margin": 0, + # "time": 1639051907, + # "time_ms": 1639051907000 + # } + # + marketId = self.safe_string(liquidation, 'contract') + market = self.safe_market(marketId, market) + timestamp = self.safe_integer(liquidation, 'time_ms') + originalSize = self.safe_string(liquidation, 'size') + left = self.safe_string(liquidation, 'left') + amount = Precise.string_abs(Precise.string_sub(originalSize, left)) + return self.safe_liquidation({ + 'info': liquidation, + 'symbol': self.safe_symbol(marketId, market), + 'contracts': self.parse_number(amount), + 'contractSize': self.safe_number(market, 'contractSize'), + 'price': self.safe_number(liquidation, 'fill_price'), + 'baseValue': None, + 'quoteValue': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + + def handle_error_message(self, client: Client, message) -> Bool: + # + # { + # "time": 1647274664, + # "channel": "futures.orders", + # "event": "subscribe", + # "error": {code: 2, message: "unknown contract BTC_USDT_20220318"}, + # } + # { + # "time": 1647276473, + # "channel": "futures.orders", + # "event": "subscribe", + # "error": { + # "code": 4, + # "message": "{"label":"INVALID_KEY","message":"Invalid key provided"}\n" + # }, + # "result": null + # } + # { + # header: { + # response_time: '1718551891329', + # status: '400', + # channel: 'spot.order_place', + # event: 'api', + # client_id: '81.34.68.6-0xc16375e2c0', + # conn_id: '9539116e0e09678f' + # }, + # data: {errs: {label: 'AUTHENTICATION_FAILED', message: 'Not login'}}, + # request_id: '10406147' + # } + # { + # "time": 1739853211, + # "time_ms": 1739853211201, + # "id": 1, + # "conn_id": "62f2c1dabbe186d7", + # "trace_id": "cdb02a8c0b61086b2fe6f8fad2f98c54", + # "channel": "spot.trades", + # "event": "subscribe", + # "payload": [ + # "LUNARLENS_USDT", + # "ETH_USDT" + # ], + # "error": { + # "code": 2, + # "message": "unknown currency pair: LUNARLENS_USDT" + # }, + # "result": { + # "status": "fail" + # }, + # "requestId": "cdb02a8c0b61086b2fe6f8fad2f98c54" + # } + # + data = self.safe_dict(message, 'data') + errs = self.safe_dict(data, 'errs') + error = self.safe_dict(message, 'error', errs) + code = self.safe_string_2(error, 'code', 'label') + id = self.safe_string_n(message, ['id', 'requestId', 'request_id']) + if error is not None: + messageHash = self.safe_string(client.subscriptions, id) + try: + self.throw_exactly_matched_exception(self.exceptions['ws']['exact'], code, self.json(message)) + self.throw_exactly_matched_exception(self.exceptions['exact'], code, self.json(errs)) + errorMessage = self.safe_string(error, 'message', self.safe_string(errs, 'message')) + self.throw_broadly_matched_exception(self.exceptions['ws']['broad'], errorMessage, self.json(message)) + raise ExchangeError(self.json(message)) + except Exception as e: + client.reject(e, messageHash) + if (messageHash is not None) and (messageHash in client.subscriptions): + del client.subscriptions[messageHash] + # remove subscriptions for watchSymbols + channel = self.safe_string(message, 'channel') + if (channel is not None) and (channel.find('.') > 0): + parsedChannel = channel.split('.') + payload = self.safe_list(message, 'payload', []) + for i in range(0, len(payload)): + marketType = parsedChannel[0] == 'swap' if 'futures' else parsedChannel[0] + symbol = self.safe_symbol(payload[i], None, '_', marketType) + messageHashSymbol = parsedChannel[1] + ':' + symbol + if (messageHashSymbol is not None) and (messageHashSymbol in client.subscriptions): + del client.subscriptions[messageHashSymbol] + if (id is not None) and (id in client.subscriptions): + del client.subscriptions[id] + return True + return False + + def handle_balance_subscription(self, client: Client, message, subscription=None): + self.balance = {} + + def handle_subscription_status(self, client: Client, message): + channel = self.safe_string(message, 'channel') + methods: dict = { + 'balance': self.handle_balance_subscription, + 'spot.order_book_update': self.handle_order_book_subscription, + 'futures.order_book_update': self.handle_order_book_subscription, + } + id = self.safe_string(message, 'id') + if channel in methods: + subscriptionHash = self.safe_string(client.subscriptions, id) + subscription = self.safe_value(client.subscriptions, subscriptionHash) + method = methods[channel] + method(client, message, subscription) + if id in client.subscriptions: + del client.subscriptions[id] + + def handle_un_subscribe(self, client: Client, message): + # + # { + # "time":1725534679, + # "time_ms":1725534679786, + # "id":2, + # "conn_id":"fac539b443fd7002", + # "trace_id":"efe1d282b630b4aa266b84bee177791a", + # "channel":"spot.trades", + # "event":"unsubscribe", + # "payload":[ + # "LTC_USDT" + # ], + # "result":{ + # "status":"success" + # }, + # "requestId":"efe1d282b630b4aa266b84bee177791a" + # } + # + id = self.safe_string(message, 'id') + keys = list(client.subscriptions.keys()) + for i in range(0, len(keys)): + messageHash = keys[i] + if not (messageHash in client.subscriptions): + continue + # the previous iteration can have deleted the messageHash from the subscriptions + if messageHash.startswith('unsubscribe'): + subscription = client.subscriptions[messageHash] + subId = self.safe_string(subscription, 'id') + if id != subId: + continue + messageHashes = self.safe_list(subscription, 'messageHashes', []) + subMessageHashes = self.safe_list(subscription, 'subMessageHashes', []) + for j in range(0, len(messageHashes)): + unsubHash = messageHashes[j] + subHash = subMessageHashes[j] + self.clean_unsubscription(client, subHash, unsubHash) + self.clean_cache(subscription) + + def clean_cache(self, subscription: dict): + topic = self.safe_string(subscription, 'topic', '') + symbols = self.safe_list(subscription, 'symbols', []) + symbolsLength = len(symbols) + if topic == 'ohlcv': + symbolsAndTimeFrames = self.safe_list(subscription, 'symbolsAndTimeframes', []) + for i in range(0, len(symbolsAndTimeFrames)): + symbolAndTimeFrame = symbolsAndTimeFrames[i] + symbol = self.safe_string(symbolAndTimeFrame, 0) + timeframe = self.safe_string(symbolAndTimeFrame, 1) + del self.ohlcvs[symbol][timeframe] + elif symbolsLength > 0: + for i in range(0, len(symbols)): + symbol = symbols[i] + if topic.endswith('trades'): + del self.trades[symbol] + elif topic == 'orderbook': + del self.orderbooks[symbol] + elif topic == 'ticker': + del self.tickers[symbol] + else: + if topic.endswith('trades'): + # don't reset self.myTrades directly here + # because in c# we need to use a different object + keys = list(self.trades.keys()) + for i in range(0, len(keys)): + del self.trades[keys[i]] + + def handle_message(self, client: Client, message): + # + # subscribe + # { + # "time": 1649062304, + # "id": 1649062303, + # "channel": "spot.candlesticks", + # "event": "subscribe", + # "result": {status: "success"} + # } + # + # candlestick + # { + # "time": 1649063328, + # "channel": "spot.candlesticks", + # "event": "update", + # "result": { + # "t": "1649063280", + # "v": "58932.23174896", + # "c": "45966.47", + # "h": "45997.24", + # "l": "45966.47", + # "o": "45975.18", + # "n": "1m_BTC_USDT", + # "a": "1.281699" + # } + # } + # + # orders + # { + # "time": 1630654851, + # "channel": "options.orders", or futures.orders or spot.orders + # "event": "update", + # "result": [ + # { + # "contract": "BTC_USDT-20211130-65000-C", + # "create_time": 1637897000, + # (...) + # ] + # } + # orderbook + # { + # "time": 1649770525, + # "channel": "spot.order_book_update", + # "event": "update", + # "result": { + # "t": 1649770525653, + # "e": "depthUpdate", + # "E": 1649770525, + # "s": "LTC_USDT", + # "U": 2622525645, + # "u": 2622525665, + # "b": [ + # [Array], [Array], + # [Array], [Array], + # [Array], [Array], + # [Array], [Array], + # [Array], [Array], + # [Array] + # ], + # "a": [ + # [Array], [Array], + # [Array], [Array], + # [Array], [Array], + # [Array], [Array], + # [Array], [Array], + # [Array] + # ] + # } + # } + # + # balance update + # + # { + # "time": 1653664351, + # "channel": "spot.balances", + # "event": "update", + # "result": [ + # { + # "timestamp": "1653664351", + # "timestamp_ms": "1653664351017", + # "user": "10406147", + # "currency": "LTC", + # "change": "-0.0002000000000000", + # "total": "0.09986000000000000000", + # "available": "0.09986000000000000000" + # } + # ] + # } + # + if self.handle_error_message(client, message): + return + event = self.safe_string(message, 'event') + if event == 'subscribe': + self.handle_subscription_status(client, message) + return + if event == 'unsubscribe': + self.handle_un_subscribe(client, message) + return + channel = self.safe_string(message, 'channel', '') + channelParts = channel.split('.') + channelType = self.safe_value(channelParts, 1) + v4Methods: dict = { + 'usertrades': self.handle_my_trades, + 'candlesticks': self.handle_ohlcv, + 'orders': self.handle_order, + 'positions': self.handle_positions, + 'tickers': self.handle_ticker, + 'book_ticker': self.handle_bid_ask, + 'trades': self.handle_trades, + 'order_book_update': self.handle_order_book, + 'balances': self.handle_balance, + 'liquidates': self.handle_liquidation, + } + method = self.safe_value(v4Methods, channelType) + if method is not None: + method(client, message) + requestId = self.safe_string(message, 'request_id') + if requestId == 'authenticated': + self.handle_authentication_message(client, message) + return + if requestId is not None: + data = self.safe_dict(message, 'data') + # use safeValue may be Array or an Object + result = self.safe_value(data, 'result') + ack = self.safe_bool(message, 'ack') + if ack is not True: + client.resolve(result, requestId) + + def get_url_by_market(self, market): + baseUrl = self.urls['api'][market['type']] + if market['contract']: + return baseUrl['usdt'] if market['linear'] else baseUrl['btc'] + else: + return baseUrl + + def get_type_by_market(self, market: Market): + if market['spot']: + return 'spot' + elif market['option']: + return 'options' + else: + return 'futures' + + def get_url_by_market_type(self, type: MarketType, isInverse=False): + api = self.urls['api'] + url = api[type] + if (type == 'swap') or (type == 'future'): + return url['btc'] if isInverse else url['usdt'] + else: + return url + + def get_market_type_by_url(self, url: str): + findBy: dict = { + 'op-': 'option', + 'delivery': 'future', + 'fx': 'swap', + } + keys = list(findBy.keys()) + for i in range(0, len(keys)): + key = keys[i] + value = findBy[key] + if url.find(key) >= 0: + return value + return 'spot' + + def request_id(self): + # their support said that reqid must be an int32, not documented + reqid = self.sum(self.safe_integer(self.options, 'reqid', 0), 1) + self.options['reqid'] = reqid + return reqid + + async def subscribe_public(self, url, messageHash, payload, channel, params={}, subscription=None): + requestId = self.request_id() + time = self.seconds() + request: dict = { + 'id': requestId, + 'time': time, + 'channel': channel, + 'event': 'subscribe', + 'payload': payload, + } + if subscription is not None: + client = self.client(url) + if not (messageHash in client.subscriptions): + tempSubscriptionHash = str(requestId) + client.subscriptions[tempSubscriptionHash] = messageHash + message = self.extend(request, params) + return await self.watch(url, messageHash, message, messageHash, subscription) + + async def subscribe_public_multiple(self, url, messageHashes, payload, channel, params={}): + requestId = self.request_id() + time = self.seconds() + request: dict = { + 'id': requestId, + 'time': time, + 'channel': channel, + 'event': 'subscribe', + 'payload': payload, + } + message = self.extend(request, params) + return await self.watch_multiple(url, messageHashes, message, messageHashes) + + async def un_subscribe_public_multiple(self, url, topic, symbols, messageHashes, subMessageHashes, payload, channel, params={}): + requestId = self.request_id() + time = self.seconds() + request: dict = { + 'id': requestId, + 'time': time, + 'channel': channel, + 'event': 'unsubscribe', + 'payload': payload, + } + sub = { + 'id': str(requestId), + 'topic': topic, + 'unsubscribe': True, + 'messageHashes': messageHashes, + 'subMessageHashes': subMessageHashes, + 'symbols': symbols, + } + message = self.extend(request, params) + return await self.watch_multiple(url, messageHashes, message, messageHashes, sub) + + async def authenticate(self, url, messageType): + channel = messageType + '.login' + client = self.client(url) + messageHash = 'authenticated' + future = client.reusableFuture(messageHash) + authenticated = self.safe_value(client.subscriptions, messageHash) + if authenticated is None: + return await self.request_private(url, {}, channel, messageHash) + return future + + def handle_authentication_message(self, client: Client, message): + messageHash = 'authenticated' + future = self.safe_value(client.futures, messageHash) + future.resolve(True) + + async def request_private(self, url, reqParams, channel, requestId: Str = None): + self.check_required_credentials() + # uid is required for some subscriptions only so it's not a part of required credentials + event = 'api' + if requestId is None: + reqId = self.request_id() + requestId = str(reqId) + messageHash = requestId + time = self.seconds() + # unfortunately, PHP demands double quotes for the escaped newline symbol + signatureString = "\n".join([event, channel, self.json(reqParams), str(time)]) # eslint-disable-line quotes + signature = self.hmac(self.encode(signatureString), self.encode(self.secret), hashlib.sha512, 'hex') + payload: dict = { + 'req_id': requestId, + 'timestamp': str(time), + 'api_key': self.apiKey, + 'signature': signature, + 'req_param': reqParams, + } + if (channel == 'spot.order_place') or (channel == 'futures.order_place'): + payload['req_header'] = { + 'X-Gate-Channel-Id': 'ccxt', + } + request: dict = { + 'id': requestId, + 'time': time, + 'channel': channel, + 'event': event, + 'payload': payload, + } + return await self.watch(url, messageHash, request, messageHash, requestId) + + async def subscribe_private(self, url, messageHash, payload, channel, params, requiresUid=False): + self.check_required_credentials() + # uid is required for some subscriptions only so it's not a part of required credentials + if requiresUid: + if self.uid is None or len(self.uid) == 0: + raise ArgumentsRequired(self.id + ' requires uid to subscribe') + idArray = [self.uid] + if payload is None: + payload = idArray + else: + payload = self.array_concat(idArray, payload) + time = self.seconds() + event = 'subscribe' + signaturePayload = 'channel=' + channel + '&' + 'event=' + event + '&' + 'time=' + str(time) + signature = self.hmac(self.encode(signaturePayload), self.encode(self.secret), hashlib.sha512, 'hex') + auth: dict = { + 'method': 'api_key', + 'KEY': self.apiKey, + 'SIGN': signature, + } + requestId = self.request_id() + request: dict = { + 'id': requestId, + 'time': time, + 'channel': channel, + 'event': event, + 'auth': auth, + } + if payload is not None: + request['payload'] = payload + client = self.client(url) + if not (messageHash in client.subscriptions): + tempSubscriptionHash = str(requestId) + # in case of authenticationError we will throw + client.subscriptions[tempSubscriptionHash] = messageHash + message = self.extend(request, params) + return await self.watch(url, messageHash, message, messageHash, messageHash) diff --git a/ccxt/pro/gateio.py b/ccxt/pro/gateio.py new file mode 100644 index 0000000..efa5297 --- /dev/null +++ b/ccxt/pro/gateio.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.pro.gate import gate +from ccxt.base.types import Any + + +class gateio(gate): + + def describe(self) -> Any: + return self.deep_extend(super(gateio, self).describe(), { + 'alias': True, + 'id': 'gateio', + }) diff --git a/ccxt/pro/gemini.py b/ccxt/pro/gemini.py new file mode 100644 index 0000000..f333b85 --- /dev/null +++ b/ccxt/pro/gemini.py @@ -0,0 +1,885 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolById, ArrayCacheByTimestamp +import hashlib +from ccxt.base.types import Any, Int, Order, OrderBook, Str, Strings, Tickers, Trade +from ccxt.async_support.base.ws.client import Client +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import NotSupported +from ccxt.base.precise import Precise + + +class gemini(ccxt.async_support.gemini): + + def describe(self) -> Any: + return self.deep_extend(super(gemini, self).describe(), { + 'has': { + 'ws': True, + 'watchBalance': False, + 'watchTicker': False, + 'watchTickers': False, + 'watchBidsAsks': True, + 'watchTrades': True, + 'watchTradesForSymbols': True, + 'watchMyTrades': False, + 'watchOrders': True, + 'watchOrderBook': True, + 'watchOrderBookForSymbols': True, + 'watchOHLCV': True, + }, + 'hostname': 'api.gemini.com', + 'urls': { + 'api': { + 'ws': 'wss://api.gemini.com', + }, + 'test': { + 'ws': 'wss://api.sandbox.gemini.com', + }, + }, + }) + + async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + watch the list of most recent trades for a particular symbol + + https://docs.gemini.com/websocket-api/#market-data-version-2 + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + messageHash = 'trades:' + market['symbol'] + marketId = market['id'] + request: dict = { + 'type': 'subscribe', + 'subscriptions': [ + { + 'name': 'l2', + 'symbols': [ + marketId.upper(), + ], + }, + ], + } + subscribeHash = 'l2:' + market['symbol'] + url = self.urls['api']['ws'] + '/v2/marketdata' + trades = await self.watch(url, messageHash, request, subscribeHash) + if self.newUpdates: + limit = trades.getLimit(market['symbol'], limit) + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + async def watch_trades_for_symbols(self, symbols: List[str], since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + + https://docs.gemini.com/websocket-api/#multi-market-data + + get the list of most recent trades for a list of symbols + :param str[] symbols: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + trades = await self.helper_for_watch_multiple_construct('trades', symbols, params) + if self.newUpdates: + first = self.safe_list(trades, 0) + tradeSymbol = self.safe_string(first, 'symbol') + limit = trades.getLimit(tradeSymbol, limit) + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + def parse_ws_trade(self, trade, market=None) -> Trade: + # + # regular v2 trade + # + # { + # "type": "trade", + # "symbol": "BTCUSD", + # "event_id": 122258166738, + # "timestamp": 1655330221424, + # "price": "22269.14", + # "quantity": "0.00004473", + # "side": "buy" + # } + # + # multi data trade + # + # { + # "type": "trade", + # "symbol": "ETHUSD", + # "tid": "1683002242170204", # self is not TS, but somewhat ID + # "price": "2299.24", + # "amount": "0.002662", + # "makerSide": "bid" + # } + # + timestamp = self.safe_integer(trade, 'timestamp') + id = self.safe_string_2(trade, 'event_id', 'tid') + priceString = self.safe_string(trade, 'price') + amountString = self.safe_string_2(trade, 'quantity', 'amount') + side = self.safe_string_lower(trade, 'side') + if side is None: + marketSide = self.safe_string_lower(trade, 'makerSide') + if marketSide == 'bid': + side = 'sell' + elif marketSide == 'ask': + side = 'buy' + marketId = self.safe_string_lower(trade, 'symbol') + symbol = self.safe_symbol(marketId, market) + return self.safe_trade({ + 'id': id, + 'order': None, + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'type': None, + 'side': side, + 'takerOrMaker': None, + 'price': priceString, + 'cost': None, + 'amount': amountString, + 'fee': None, + }, market) + + def handle_trade(self, client: Client, message): + # + # { + # "type": "trade", + # "symbol": "BTCUSD", + # "event_id": 122278173770, + # "timestamp": 1655335880981, + # "price": "22530.80", + # "quantity": "0.04", + # "side": "buy" + # } + # + trade = self.parse_ws_trade(message) + symbol = trade['symbol'] + tradesLimit = self.safe_integer(self.options, 'tradesLimit', 1000) + stored = self.safe_value(self.trades, symbol) + if stored is None: + stored = ArrayCache(tradesLimit) + self.trades[symbol] = stored + stored.append(trade) + messageHash = 'trades:' + symbol + client.resolve(stored, messageHash) + + def handle_trades(self, client: Client, message): + # + # { + # "type": "l2_updates", + # "symbol": "BTCUSD", + # "changes": [ + # ["buy", '22252.37', "0.02"], + # ["buy", '22251.61', "0.04"], + # ["buy", '22251.60', "0.04"], + # # some asks + # ], + # "trades": [ + # {type: 'trade', symbol: 'BTCUSD', event_id: 122258166738, timestamp: 1655330221424, price: '22269.14', quantity: "0.00004473", side: "buy"}, + # {type: 'trade', symbol: 'BTCUSD', event_id: 122258141090, timestamp: 1655330213216, price: '22250.00', quantity: "0.00704098", side: "buy"}, + # {type: 'trade', symbol: 'BTCUSD', event_id: 122258118291, timestamp: 1655330206753, price: '22250.00', quantity: "0.03", side: "buy"}, + # ], + # "auction_events": [ + # { + # "type": "auction_result", + # "symbol": "BTCUSD", + # "time_ms": 1655323200000, + # "result": "failure", + # "highest_bid_price": "21590.88", + # "lowest_ask_price": "21602.30", + # "collar_price": "21634.73" + # }, + # { + # "type": "auction_indicative", + # "symbol": "BTCUSD", + # "time_ms": 1655323185000, + # "result": "failure", + # "highest_bid_price": "21661.90", + # "lowest_ask_price": "21663.78", + # "collar_price": "21662.845" + # }, + # ] + # } + # + marketId = self.safe_string_lower(message, 'symbol') + market = self.safe_market(marketId) + trades = self.safe_value(message, 'trades') + if trades is not None: + symbol = market['symbol'] + tradesLimit = self.safe_integer(self.options, 'tradesLimit', 1000) + stored = self.safe_value(self.trades, symbol) + if stored is None: + stored = ArrayCache(tradesLimit) + self.trades[symbol] = stored + for i in range(0, len(trades)): + trade = self.parse_ws_trade(trades[i], market) + stored.append(trade) + messageHash = 'trades:' + symbol + client.resolve(stored, messageHash) + + def handle_trades_for_multidata(self, client: Client, trades, timestamp: Int): + if trades is not None: + tradesLimit = self.safe_integer(self.options, 'tradesLimit', 1000) + storesForSymbols: dict = {} + for i in range(0, len(trades)): + marketId = trades[i]['symbol'] + market = self.safe_market(marketId.lower()) + symbol = market['symbol'] + trade = self.parse_ws_trade(trades[i], market) + trade['timestamp'] = timestamp + trade['datetime'] = self.iso8601(timestamp) + stored = self.safe_value(self.trades, symbol) + if stored is None: + stored = ArrayCache(tradesLimit) + self.trades[symbol] = stored + stored.append(trade) + storesForSymbols[symbol] = stored + symbols = list(storesForSymbols.keys()) + for i in range(0, len(symbols)): + symbol = symbols[i] + stored = storesForSymbols[symbol] + messageHash = 'trades:' + symbol + client.resolve(stored, messageHash) + + async def watch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + watches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://docs.gemini.com/websocket-api/#candles-data-feed + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + timeframeId = self.safe_string(self.timeframes, timeframe, timeframe) + request: dict = { + 'type': 'subscribe', + 'subscriptions': [ + { + 'name': 'candles_' + timeframeId, + 'symbols': [ + market['id'].upper(), + ], + }, + ], + } + messageHash = 'ohlcv:' + market['symbol'] + ':' + timeframeId + url = self.urls['api']['ws'] + '/v2/marketdata' + ohlcv = await self.watch(url, messageHash, request, messageHash) + if self.newUpdates: + limit = ohlcv.getLimit(symbol, limit) + return self.filter_by_since_limit(ohlcv, since, limit, 0, True) + + def handle_ohlcv(self, client: Client, message): + # + # { + # "type": "candles_15m_updates", + # "symbol": "BTCUSD", + # "changes": [ + # [ + # 1561054500000, + # 9350.18, + # 9358.35, + # 9350.18, + # 9355.51, + # 2.07 + # ], + # [ + # 1561053600000, + # 9357.33, + # 9357.33, + # 9350.18, + # 9350.18, + # 1.5900161 + # ] + # ... + # ] + # } + # + type = self.safe_string(message, 'type', '') + timeframeId = type[8:] + timeframeEndIndex = timeframeId.find('_') + timeframeId = timeframeId[0:timeframeEndIndex] + marketId = self.safe_string(message, 'symbol', '').lower() + market = self.safe_market(marketId) + symbol = self.safe_symbol(marketId, market) + changes = self.safe_value(message, 'changes', []) + timeframe = self.find_timeframe(timeframeId) + ohlcvsBySymbol = self.safe_value(self.ohlcvs, symbol) + if ohlcvsBySymbol is None: + self.ohlcvs[symbol] = {} + stored = self.safe_value(self.ohlcvs[symbol], timeframe) + if stored is None: + limit = self.safe_integer(self.options, 'OHLCVLimit', 1000) + stored = ArrayCacheByTimestamp(limit) + self.ohlcvs[symbol][timeframe] = stored + changesLength = len(changes) + # reverse order of array to store candles in ascending order + for i in range(0, changesLength): + index = changesLength - i - 1 + parsed = self.parse_ohlcv(changes[index], market) + stored.append(parsed) + messageHash = 'ohlcv:' + symbol + ':' + timeframeId + client.resolve(stored, messageHash) + return message + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://docs.gemini.com/websocket-api/#market-data-version-2 + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + messageHash = 'orderbook:' + market['symbol'] + marketId = market['id'] + request: dict = { + 'type': 'subscribe', + 'subscriptions': [ + { + 'name': 'l2', + 'symbols': [ + marketId.upper(), + ], + }, + ], + } + subscribeHash = 'l2:' + market['symbol'] + url = self.urls['api']['ws'] + '/v2/marketdata' + orderbook = await self.watch(url, messageHash, request, subscribeHash) + return orderbook.limit() + + def handle_order_book(self, client: Client, message): + changes = self.safe_value(message, 'changes', []) + marketId = self.safe_string_lower(message, 'symbol') + market = self.safe_market(marketId) + symbol = market['symbol'] + messageHash = 'orderbook:' + symbol + # orderbook = self.safe_value(self.orderbooks, symbol) + if not (symbol in self.orderbooks): + self.orderbooks[symbol] = self.order_book() + orderbook = self.orderbooks[symbol] + for i in range(0, len(changes)): + delta = changes[i] + price = self.safe_number(delta, 1) + size = self.safe_number(delta, 2) + side = 'bids' if (delta[0] == 'buy') else 'asks' + bookside = orderbook[side] + bookside.store(price, size) + orderbook[side] = bookside + orderbook['symbol'] = symbol + self.orderbooks[symbol] = orderbook + client.resolve(orderbook, messageHash) + + async def watch_order_book_for_symbols(self, symbols: List[str], limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://docs.gemini.com/websocket-api/#multi-market-data + + :param str[] symbols: unified array of symbols + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + orderbook = await self.helper_for_watch_multiple_construct('orderbook', symbols, params) + return orderbook.limit() + + async def watch_bids_asks(self, symbols: Strings = None, params={}) -> Tickers: + """ + watches best bid & ask for symbols + + https://docs.gemini.com/websocket-api/#multi-market-data + + :param str[] symbols: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + return await self.helper_for_watch_multiple_construct('bidsasks', symbols, params) + + def handle_bids_asks_for_multidata(self, client: Client, rawBidAskChanges, timestamp: Int, nonce: Int): + # + # { + # eventId: '1683002916916153', + # events: [ + # { + # price: '50945.37', + # reason: 'top-of-book', + # remaining: '0.0', + # side: 'bid', + # symbol: 'BTCUSDT', + # type: 'change' + # }, + # { + # price: '50947.75', + # reason: 'top-of-book', + # remaining: '0.11725', + # side: 'bid', + # symbol: 'BTCUSDT', + # type: 'change' + # } + # ], + # socket_sequence: 322, + # timestamp: 1708674495, + # timestampms: 1708674495174, + # type: 'update' + # } + # + marketId = rawBidAskChanges[0]['symbol'] + market = self.safe_market(marketId.lower()) + symbol = market['symbol'] + if not (symbol in self.bidsasks): + self.bidsasks[symbol] = self.parse_ticker({}) + self.bidsasks[symbol]['symbol'] = symbol + currentBidAsk = self.bidsasks[symbol] + messageHash = 'bidsasks:' + symbol + # last update always overwrites the previous state and is the latest state + for i in range(0, len(rawBidAskChanges)): + entry = rawBidAskChanges[i] + rawSide = self.safe_string(entry, 'side') + price = self.safe_number(entry, 'price') + sizeString = self.safe_string(entry, 'remaining') + if Precise.string_eq(sizeString, '0'): + continue + size = self.parse_number(sizeString) + if rawSide == 'bid': + currentBidAsk['bid'] = price + currentBidAsk['bidVolume'] = size + else: + currentBidAsk['ask'] = price + currentBidAsk['askVolume'] = size + currentBidAsk['timestamp'] = timestamp + currentBidAsk['datetime'] = self.iso8601(timestamp) + currentBidAsk['info'] = rawBidAskChanges + bidsAsksDict = {} + bidsAsksDict[symbol] = currentBidAsk + self.bidsasks[symbol] = currentBidAsk + client.resolve(bidsAsksDict, messageHash) + + async def helper_for_watch_multiple_construct(self, itemHashName: str, symbols: List[str] = None, params={}): + await self.load_markets() + if symbols is None: + raise NotSupported(self.id + ' watchMultiple requires at least one symbol') + symbols = self.market_symbols(symbols, None, False, True, True) + firstMarket = self.market(symbols[0]) + if not firstMarket['spot'] and not firstMarket['linear']: + raise NotSupported(self.id + ' watchMultiple supports only spot or linear-swap symbols') + messageHashes = [] + marketIds = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + messageHash = itemHashName + ':' + symbol + messageHashes.append(messageHash) + market = self.market(symbol) + marketIds.append(market['id']) + queryStr = ','.join(marketIds) + url = self.urls['api']['ws'] + '/v1/multimarketdata?symbols=' + queryStr + '&heartbeat=true&' + if itemHashName == 'orderbook': + url += 'trades=false&bids=true&offers=true' + elif itemHashName == 'bidsasks': + url += 'trades=false&bids=true&offers=true&top_of_book=true' + elif itemHashName == 'trades': + url += 'trades=true&bids=false&offers=false' + return await self.watch_multiple(url, messageHashes, None) + + def handle_order_book_for_multidata(self, client: Client, rawOrderBookChanges, timestamp: Int, nonce: Int): + # + # rawOrderBookChanges + # + # [ + # { + # delta: "4105123935484.817624", + # price: "0.000000001", + # reason: "initial", # initial|cancel|place + # remaining: "4105123935484.817624", + # side: "bid", # bid|ask + # symbol: "SHIBUSD", + # type: "change", # seems always change + # }, + # ... + # + marketId = rawOrderBookChanges[0]['symbol'] + market = self.safe_market(marketId.lower()) + symbol = market['symbol'] + messageHash = 'orderbook:' + symbol + if not (symbol in self.orderbooks): + ob = self.order_book() + self.orderbooks[symbol] = ob + orderbook = self.orderbooks[symbol] + bids = orderbook['bids'] + asks = orderbook['asks'] + for i in range(0, len(rawOrderBookChanges)): + entry = rawOrderBookChanges[i] + price = self.safe_number(entry, 'price') + size = self.safe_number(entry, 'remaining') + rawSide = self.safe_string(entry, 'side') + if rawSide == 'bid': + bids.store(price, size) + else: + asks.store(price, size) + orderbook['bids'] = bids + orderbook['asks'] = asks + orderbook['symbol'] = symbol + orderbook['nonce'] = nonce + orderbook['timestamp'] = timestamp + orderbook['datetime'] = self.iso8601(timestamp) + self.orderbooks[symbol] = orderbook + client.resolve(orderbook, messageHash) + + def handle_l2_updates(self, client: Client, message): + # + # { + # "type": "l2_updates", + # "symbol": "BTCUSD", + # "changes": [ + # ["buy", '22252.37', "0.02"], + # ["buy", '22251.61', "0.04"], + # ["buy", '22251.60', "0.04"], + # # some asks + # ], + # "trades": [ + # {type: 'trade', symbol: 'BTCUSD', event_id: 122258166738, timestamp: 1655330221424, price: '22269.14', quantity: "0.00004473", side: "buy"}, + # {type: 'trade', symbol: 'BTCUSD', event_id: 122258141090, timestamp: 1655330213216, price: '22250.00', quantity: "0.00704098", side: "buy"}, + # {type: 'trade', symbol: 'BTCUSD', event_id: 122258118291, timestamp: 1655330206753, price: '22250.00', quantity: "0.03", side: "buy"}, + # ], + # "auction_events": [ + # { + # "type": "auction_result", + # "symbol": "BTCUSD", + # "time_ms": 1655323200000, + # "result": "failure", + # "highest_bid_price": "21590.88", + # "lowest_ask_price": "21602.30", + # "collar_price": "21634.73" + # }, + # { + # "type": "auction_indicative", + # "symbol": "BTCUSD", + # "time_ms": 1655323185000, + # "result": "failure", + # "highest_bid_price": "21661.90", + # "lowest_ask_price": "21663.79", + # "collar_price": "21662.845" + # }, + # ] + # } + # + self.handle_order_book(client, message) + self.handle_trades(client, message) + + async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + watches information on multiple orders made by the user + + https://docs.gemini.com/websocket-api/#order-events + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + url = self.urls['api']['ws'] + '/v1/order/events?eventTypeFilter=initial&eventTypeFilter=accepted&eventTypeFilter=rejected&eventTypeFilter=fill&eventTypeFilter=cancelled&eventTypeFilter=booked' + await self.load_markets() + authParams: dict = { + 'url': url, + } + await self.authenticate(authParams) + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + messageHash = 'orders' + orders = await self.watch(url, messageHash, None, messageHash) + if self.newUpdates: + limit = orders.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(orders, symbol, since, limit, True) + + def handle_heartbeat(self, client: Client, message): + # + # { + # "type": "heartbeat", + # "timestampms": 1659740268958, + # "sequence": 7, + # "trace_id": "25b3d92476dd3a9a5c03c9bd9e0a0dba", + # "socket_sequence": 7 + # } + # + client.lastPong = self.milliseconds() + return message + + def handle_subscription(self, client: Client, message): + # + # { + # "type": "subscription_ack", + # "accountId": 19433282, + # "subscriptionId": "orderevents-websocket-25b3d92476dd3a9a5c03c9bd9e0a0dba", + # "symbolFilter": [], + # "apiSessionFilter": [], + # "eventTypeFilter": [] + # } + # + return message + + def handle_order(self, client: Client, message): + # + # [ + # { + # "type": "accepted", + # "order_id": "134150423884", + # "event_id": "134150423886", + # "account_name": "primary", + # "client_order_id": "1659739406916", + # "api_session": "account-pnBFSS0XKGvDamX4uEIt", + # "symbol": "batbtc", + # "side": "sell", + # "order_type": "exchange limit", + # "timestamp": "1659739407", + # "timestampms": 1659739407576, + # "is_live": True, + # "is_cancelled": False, + # "is_hidden": False, + # "original_amount": "1", + # "price": "1", + # "socket_sequence": 139 + # } + # ] + # + messageHash = 'orders' + if self.orders is None: + limit = self.safe_integer(self.options, 'ordersLimit', 1000) + self.orders = ArrayCacheBySymbolById(limit) + orders = self.orders + for i in range(0, len(message)): + order = self.parse_ws_order(message[i]) + orders.append(order) + client.resolve(self.orders, messageHash) + + def parse_ws_order(self, order, market=None): + # + # { + # "type": "accepted", + # "order_id": "134150423884", + # "event_id": "134150423886", + # "account_name": "primary", + # "client_order_id": "1659739406916", + # "api_session": "account-pnBFSS0XKGvDamX4uEIt", + # "symbol": "batbtc", + # "side": "sell", + # "order_type": "exchange limit", + # "timestamp": "1659739407", + # "timestampms": 1659739407576, + # "is_live": True, + # "is_cancelled": False, + # "is_hidden": False, + # "original_amount": "1", + # "price": "1", + # "socket_sequence": 139 + # } + # + timestamp = self.safe_integer(order, 'timestampms') + status = self.safe_string(order, 'type') + marketId = self.safe_string(order, 'symbol') + typeId = self.safe_string(order, 'order_type') + behavior = self.safe_string(order, 'behavior') + timeInForce = 'GTC' + postOnly = False + if behavior == 'immediate-or-cancel': + timeInForce = 'IOC' + elif behavior == 'fill-or-kill': + timeInForce = 'FOK' + elif behavior == 'maker-or-cancel': + timeInForce = 'PO' + postOnly = True + return self.safe_order({ + 'id': self.safe_string(order, 'order_id'), + 'clientOrderId': self.safe_string(order, 'client_order_id'), + 'info': order, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'status': self.parse_ws_order_status(status), + 'symbol': self.safe_symbol(marketId, market), + 'type': self.parse_ws_order_type(typeId), + 'timeInForce': timeInForce, + 'postOnly': postOnly, + 'side': self.safe_string(order, 'side'), + 'price': self.safe_number(order, 'price'), + 'stopPrice': None, + 'average': self.safe_number(order, 'avg_execution_price'), + 'cost': None, + 'amount': self.safe_number(order, 'original_amount'), + 'filled': self.safe_number(order, 'executed_amount'), + 'remaining': self.safe_number(order, 'remaining_amount'), + 'fee': None, + 'trades': None, + }, market) + + def parse_ws_order_status(self, status): + statuses: dict = { + 'accepted': 'open', + 'booked': 'open', + 'fill': 'closed', + 'cancelled': 'canceled', + 'cancel_rejected': 'rejected', + 'rejected': 'rejected', + } + return self.safe_string(statuses, status, status) + + def parse_ws_order_type(self, type): + types: dict = { + 'exchange limit': 'limit', + 'market buy': 'market', + 'market sell': 'market', + } + return self.safe_string(types, type, type) + + def handle_error(self, client: Client, message): + # + # { + # "reason": "NoValidTradingPairs", + # "result": "error" + # } + # + raise ExchangeError(self.json(message)) + + def handle_message(self, client: Client, message): + # + # public + # { + # "type": "trade", + # "symbol": "BTCUSD", + # "event_id": 122278173770, + # "timestamp": 1655335880981, + # "price": "22530.80", + # "quantity": "0.04", + # "side": "buy" + # } + # + # private + # [ + # { + # "type": "accepted", + # "order_id": "134150423884", + # "event_id": "134150423886", + # "account_name": "primary", + # "client_order_id": "1659739406916", + # "api_session": "account-pnBFSS0XKGvDamX4uEIt", + # "symbol": "batbtc", + # "side": "sell", + # "order_type": "exchange limit", + # "timestamp": "1659739407", + # "timestampms": 1659739407576, + # "is_live": True, + # "is_cancelled": False, + # "is_hidden": False, + # "original_amount": "1", + # "price": "1", + # "socket_sequence": 139 + # } + # ] + # + isArray = isinstance(message, list) + if isArray: + self.handle_order(client, message) + return + reason = self.safe_string(message, 'reason') + if reason == 'error': + self.handle_error(client, message) + methods: dict = { + 'l2_updates': self.handle_l2_updates, + 'trade': self.handle_trade, + 'subscription_ack': self.handle_subscription, + 'heartbeat': self.handle_heartbeat, + } + type = self.safe_string(message, 'type', '') + if type.find('candles') >= 0: + self.handle_ohlcv(client, message) + return + method = self.safe_value(methods, type) + if method is not None: + method(client, message) + # handle multimarketdata + if type == 'update': + ts = self.safe_integer(message, 'timestampms', self.milliseconds()) + eventId = self.safe_integer(message, 'eventId') + events = self.safe_list(message, 'events') + orderBookItems = [] + bidaskItems = [] + collectedEventsOfTrades = [] + eventsLength = len(events) + for i in range(0, len(events)): + event = events[i] + eventType = self.safe_string(event, 'type') + isOrderBook = (eventType == 'change') and ('side' in event) and self.in_array(event['side'], ['ask', 'bid']) + eventReason = self.safe_string(event, 'reason') + isBidAsk = (eventReason == 'top-of-book') or (isOrderBook and (eventReason == 'initial') and eventsLength == 2) + if isBidAsk: + bidaskItems.append(event) + elif isOrderBook: + orderBookItems.append(event) + elif eventType == 'trade': + collectedEventsOfTrades.append(events[i]) + lengthBa = len(bidaskItems) + if lengthBa > 0: + self.handle_bids_asks_for_multidata(client, bidaskItems, ts, eventId) + lengthOb = len(orderBookItems) + if lengthOb > 0: + self.handle_order_book_for_multidata(client, orderBookItems, ts, eventId) + lengthTrades = len(collectedEventsOfTrades) + if lengthTrades > 0: + self.handle_trades_for_multidata(client, collectedEventsOfTrades, ts) + + async def authenticate(self, params={}): + url = self.safe_string(params, 'url') + if (self.clients is not None) and (url in self.clients): + return + self.check_required_credentials() + startIndex = len(self.urls['api']['ws']) + urlParamsIndex = url.find('?') + urlLength = len(url) + endIndex = urlParamsIndex if (urlParamsIndex >= 0) else urlLength + request = url[startIndex:endIndex] + payload: dict = { + 'request': request, + 'nonce': self.nonce(), + } + b64 = self.string_to_base64(self.json(payload)) + signature = self.hmac(self.encode(b64), self.encode(self.secret), hashlib.sha384, 'hex') + defaultOptions: dict = { + 'ws': { + 'options': { + 'headers': {}, + }, + }, + } + # self.options = self.extend(defaultOptions, self.options) + self.extend_exchange_options(defaultOptions) + originalHeaders = self.options['ws']['options']['headers'] + headers: dict = { + 'X-GEMINI-APIKEY': self.apiKey, + 'X-GEMINI-PAYLOAD': b64, + 'X-GEMINI-SIGNATURE': signature, + } + self.options['ws']['options']['headers'] = headers + self.client(url) + self.options['ws']['options']['headers'] = originalHeaders diff --git a/ccxt/pro/hashkey.py b/ccxt/pro/hashkey.py new file mode 100644 index 0000000..7df317f --- /dev/null +++ b/ccxt/pro/hashkey.py @@ -0,0 +1,802 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolById, ArrayCacheBySymbolBySide, ArrayCacheByTimestamp +from ccxt.base.types import Any, Balances, Bool, Int, Market, Order, OrderBook, Position, Str, Strings, Ticker, Trade +from ccxt.async_support.base.ws.client import Client +from typing import List + + +class hashkey(ccxt.async_support.hashkey): + + def describe(self) -> Any: + return self.deep_extend(super(hashkey, self).describe(), { + 'has': { + 'ws': True, + 'watchBalance': True, + 'watchMyTrades': True, + 'watchOHLCV': True, + 'watchOrderBook': True, + 'watchOrders': True, + 'watchTicker': True, + 'watchTrades': True, + 'watchTradesForSymbols': False, + 'watchPositions': False, + }, + 'urls': { + 'api': { + 'ws': { + 'public': 'wss://stream-glb.hashkey.com/quote/ws/v1', + 'private': 'wss://stream-glb.hashkey.com/api/v1/ws', + }, + 'test': { + 'ws': { + 'public': 'wss://stream-glb.sim.hashkeydev.com/quote/ws/v1', + 'private': 'wss://stream-glb.sim.hashkeydev.com/api/v1/ws', + }, + }, + }, + }, + 'options': { + 'listenKeyRefreshRate': 3600000, + 'listenKey': None, + 'watchBalance': { + 'fetchBalanceSnapshot': True, # or False + 'awaitBalanceSnapshot': False, # whether to wait for the balance snapshot before providing updates + }, + }, + 'streaming': { + 'keepAlive': 10000, + }, + }) + + async def wath_public(self, market: Market, topic: str, messageHash: str, params={}): + request: dict = { + 'symbol': market['id'], + 'topic': topic, + 'event': 'sub', + } + url = self.urls['api']['ws']['public'] + return await self.watch(url, messageHash, self.deep_extend(request, params), messageHash) + + async def watch_private(self, messageHash): + listenKey = await self.authenticate() + url = self.get_private_url(listenKey) + return await self.watch(url, messageHash, None, messageHash) + + def get_private_url(self, listenKey): + return self.urls['api']['ws']['private'] + '/' + listenKey + + async def watch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + watches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://hashkeyglobal-apidoc.readme.io/reference/websocket-api#public-stream + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.binary]: True or False - default False + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + interval = self.safe_string(self.timeframes, timeframe, timeframe) + topic = 'kline_' + interval + messageHash = 'ohlcv:' + symbol + ':' + timeframe + ohlcv = await self.wath_public(market, topic, messageHash, params) + if self.newUpdates: + limit = ohlcv.getLimit(symbol, limit) + return self.filter_by_since_limit(ohlcv, since, limit, 0, True) + + def handle_ohlcv(self, client: Client, message): + # + # { + # "symbol": "DOGEUSDT", + # "symbolName": "DOGEUSDT", + # "topic": "kline", + # "params": { + # "realtimeInterval": "24h", + # "klineType": "1m" + # }, + # "data": [ + # { + # "t": 1722861660000, + # "s": "DOGEUSDT", + # "sn": "DOGEUSDT", + # "c": "0.08389", + # "h": "0.08389", + # "l": "0.08389", + # "o": "0.08389", + # "v": "0" + # } + # ], + # "f": True, + # "sendTime": 1722861664258, + # "shared": False + # } + # + marketId = self.safe_string(message, 'symbol') + market = self.safe_market(marketId) + symbol = self.safe_symbol(marketId, market) + if not (symbol in self.ohlcvs): + self.ohlcvs[symbol] = {} + params = self.safe_dict(message, 'params') + klineType = self.safe_string(params, 'klineType') + timeframe = self.find_timeframe(klineType) + if not (timeframe in self.ohlcvs[symbol]): + limit = self.safe_integer(self.options, 'OHLCVLimit', 1000) + self.ohlcvs[symbol][timeframe] = ArrayCacheByTimestamp(limit) + data = self.safe_list(message, 'data', []) + stored = self.ohlcvs[symbol][timeframe] + for i in range(0, len(data)): + candle = self.safe_dict(data, i, {}) + parsed = self.parse_ws_ohlcv(candle, market) + stored.append(parsed) + messageHash = 'ohlcv:' + symbol + ':' + timeframe + client.resolve(stored, messageHash) + + def parse_ws_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # { + # "t": 1722861660000, + # "s": "DOGEUSDT", + # "sn": "DOGEUSDT", + # "c": "0.08389", + # "h": "0.08389", + # "l": "0.08389", + # "o": "0.08389", + # "v": "0" + # } + # + return [ + self.safe_integer(ohlcv, 't'), + self.safe_number(ohlcv, 'o'), + self.safe_number(ohlcv, 'h'), + self.safe_number(ohlcv, 'l'), + self.safe_number(ohlcv, 'c'), + self.safe_number(ohlcv, 'v'), + ] + + async def watch_ticker(self, symbol: str, params={}) -> Ticker: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://hashkeyglobal-apidoc.readme.io/reference/websocket-api#public-stream + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.binary]: True or False - default False + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + topic = 'realtimes' + messageHash = 'ticker:' + symbol + return await self.wath_public(market, topic, messageHash, params) + + def handle_ticker(self, client: Client, message): + # + # { + # "symbol": "ETHUSDT", + # "symbolName": "ETHUSDT", + # "topic": "realtimes", + # "params": { + # "realtimeInterval": "24h" + # }, + # "data": [ + # { + # "t": 1722864411064, + # "s": "ETHUSDT", + # "sn": "ETHUSDT", + # "c": "2195", + # "h": "2918.85", + # "l": "2135.5", + # "o": "2915.78", + # "v": "666.5019", + # "qv": "1586902.757079", + # "m": "-0.2472", + # "e": 301 + # } + # ], + # "f": False, + # "sendTime": 1722864411086, + # "shared": False + # } + # + data = self.safe_list(message, 'data', []) + ticker = self.parse_ticker(self.safe_dict(data, 0)) + symbol = ticker['symbol'] + messageHash = 'ticker:' + symbol + self.tickers[symbol] = ticker + client.resolve(self.tickers[symbol], messageHash) + + async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + watches information on multiple trades made in a market + + https://hashkeyglobal-apidoc.readme.io/reference/websocket-api#public-stream + + :param str symbol: unified market symbol of the market trades were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of trade structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.binary]: True or False - default False + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + topic = 'trade' + messageHash = 'trades:' + symbol + trades = await self.wath_public(market, topic, messageHash, params) + if self.newUpdates: + limit = trades.getLimit(symbol, limit) + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + def handle_trades(self, client: Client, message): + # + # { + # "symbol": "ETHUSDT", + # "symbolName": "ETHUSDT", + # "topic": "trade", + # "params": { + # "realtimeInterval": "24h" + # }, + # "data": [ + # { + # "v": "1745922896272048129", + # "t": 1722866228075, + # "p": "2340.41", + # "q": "0.0132", + # "m": True + # }, + # ... + # ], + # "f": True, + # "sendTime": 1722869464248, + # "channelId": "668498fffeba4108-00000001-00113184-562e27d215e43f9c-c188b319", + # "shared": False + # } + # + marketId = self.safe_string(message, 'symbol') + market = self.safe_market(marketId) + symbol = market['symbol'] + if not (symbol in self.trades): + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + self.trades[symbol] = ArrayCache(limit) + stored = self.trades[symbol] + data = self.safe_list(message, 'data') + if data is not None: + data = self.sort_by(data, 't') + for i in range(0, len(data)): + trade = self.safe_dict(data, i) + parsed = self.parse_ws_trade(trade, market) + stored.append(parsed) + messageHash = 'trades' + ':' + symbol + client.resolve(stored, messageHash) + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://hashkeyglobal-apidoc.readme.io/reference/websocket-api#public-stream + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return. + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + topic = 'depth' + messageHash = 'orderbook:' + symbol + orderbook = await self.wath_public(market, topic, messageHash, params) + return orderbook.limit() + + def handle_order_book(self, client: Client, message): + # + # { + # "symbol": "ETHUSDT", + # "symbolName": "ETHUSDT", + # "topic": "depth", + # "params": {"realtimeInterval": "24h"}, + # "data": [ + # { + # "e": 301, + # "s": "ETHUSDT", + # "t": 1722873144371, + # "v": "84661262_18", + # "b": [ + # ["1650", "0.0864"], + # ... + # ], + # "a": [ + # ["4085", "0.0074"], + # ... + # ], + # "o": 0 + # } + # ], + # "f": False, + # "sendTime": 1722873144589, + # "channelId": "2265aafffe68b588-00000001-0011510c-9e9ca710b1500854-551830bd", + # "shared": False + # } + # + marketId = self.safe_string(message, 'symbol') + symbol = self.safe_symbol(marketId) + messageHash = 'orderbook:' + symbol + if not (symbol in self.orderbooks): + self.orderbooks[symbol] = self.order_book({}) + orderbook = self.orderbooks[symbol] + data = self.safe_list(message, 'data', []) + dataEntry = self.safe_dict(data, 0) + timestamp = self.safe_integer(dataEntry, 't') + snapshot = self.parse_order_book(dataEntry, symbol, timestamp, 'b', 'a') + orderbook.reset(snapshot) + orderbook['nonce'] = self.safe_integer(message, 'id') + self.orderbooks[symbol] = orderbook + client.resolve(orderbook, messageHash) + + async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + watches information on multiple orders made by the user + + https://hashkeyglobal-apidoc.readme.io/reference/websocket-api#private-stream + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + messageHash = 'orders' + if symbol is not None: + symbol = self.symbol(symbol) + messageHash = messageHash + ':' + symbol + orders = await self.watch_private(messageHash) + if self.newUpdates: + limit = orders.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(orders, symbol, since, limit, True) + + def handle_order(self, client: Client, message): + # + # swap + # { + # "e": "contractExecutionReport", + # "E": "1723037391181", + # "s": "ETHUSDT-PERPETUAL", + # "c": "1723037389677", + # "S": "BUY_OPEN", + # "o": "LIMIT", + # "f": "IOC", + # "q": "1", + # "p": "2561.75", + # "X": "FILLED", + # "i": "1747358716129257216", + # "l": "1", + # "z": "1", + # "L": "2463.36", + # "n": "0.001478016", + # "N": "USDT", + # "u": True, + # "w": True, + # "m": False, + # "O": "1723037391140", + # "Z": "2463.36", + # "C": False, + # "v": "5", + # "reqAmt": "0", + # "d": "1747358716255075840", + # "r": "0", + # "V": "2463.36", + # "P": "0", + # "lo": False, + # "lt": "" + # } + # + if self.orders is None: + limit = self.safe_integer(self.options, 'ordersLimit', 1000) + self.orders = ArrayCacheBySymbolById(limit) + parsed = self.parse_ws_order(message) + orders = self.orders + orders.append(parsed) + messageHash = 'orders' + client.resolve(orders, messageHash) + symbol = parsed['symbol'] + symbolSpecificMessageHash = messageHash + ':' + symbol + client.resolve(orders, symbolSpecificMessageHash) + + def parse_ws_order(self, order: dict, market: Market = None) -> Order: + marketId = self.safe_string(order, 's') + market = self.safe_market(marketId, market) + timestamp = self.safe_integer(order, 'O') + side = self.safe_string_lower(order, 'S') + reduceOnly: Bool = None + side, reduceOnly = self.parseOrderSideAndReduceOnly(side) + type = self.parseOrderType(self.safe_string(order, 'o')) + timeInForce = self.safe_string(order, 'f') + postOnly: Bool = None + type, timeInForce, postOnly = self.parseOrderTypeTimeInForceAndPostOnly(type, timeInForce) + if market['contract']: # swap orders are always have type 'LIMIT', thus we can not define the correct type + type = None + return self.safe_order({ + 'id': self.safe_string(order, 'i'), + 'clientOrderId': self.safe_string(order, 'c'), + 'datetime': self.iso8601(timestamp), + 'timestamp': timestamp, + 'lastTradeTimestamp': None, + 'lastUpdateTimestamp': None, + 'status': self.parse_order_status(self.safe_string(order, 'X')), + 'symbol': market['symbol'], + 'type': type, + 'timeInForce': timeInForce, + 'side': side, + 'price': self.safe_string(order, 'p'), + 'average': self.safe_string(order, 'V'), + 'amount': self.omit_zero(self.safe_string(order, 'q')), + 'filled': self.safe_string(order, 'z'), + 'remaining': self.safe_string(order, 'r'), + 'stopPrice': None, + 'triggerPrice': None, + 'takeProfitPrice': None, + 'stopLossPrice': None, + 'cost': self.omit_zero(self.safe_string(order, 'Z')), + 'trades': None, + 'fee': { + 'currency': self.safe_currency_code(self.safe_string(order, 'N')), + 'amount': self.omit_zero(self.safe_string(order, 'n')), + }, + 'reduceOnly': reduceOnly, + 'postOnly': postOnly, + 'info': order, + }, market) + + async def watch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + watches information on multiple trades made by the user + + https://hashkeyglobal-apidoc.readme.io/reference/websocket-api#private-stream + + :param str symbol: unified market symbol of the market trades were made in + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trade structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + messageHash = 'myTrades' + if symbol is not None: + symbol = self.symbol(symbol) + messageHash += ':' + symbol + trades = await self.watch_private(messageHash) + if self.newUpdates: + limit = trades.getLimit(symbol, limit) + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + def handle_my_trade(self, client: Client, message, subscription={}): + # + # { + # "e": "ticketInfo", + # "E": "1723037391156", + # "s": "ETHUSDT-PERPETUAL", + # "q": "1.00", + # "t": "1723037391147", + # "p": "2463.36", + # "T": "1747358716187197441", + # "o": "1747358716129257216", + # "c": "1723037389677", + # "a": "1735619524953226496", + # "m": False, + # "S": "BUY" + # } + # + if self.myTrades is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + self.myTrades = ArrayCacheBySymbolById(limit) + tradesArray = self.myTrades + parsed = self.parse_ws_trade(message) + tradesArray.append(parsed) + self.myTrades = tradesArray + messageHash = 'myTrades' + client.resolve(tradesArray, messageHash) + symbol = parsed['symbol'] + symbolSpecificMessageHash = messageHash + ':' + symbol + client.resolve(tradesArray, symbolSpecificMessageHash) + + def parse_ws_trade(self, trade, market=None) -> Trade: + # + # watchTrades + # { + # "v": "1745922896272048129", + # "t": 1722866228075, + # "p": "2340.41", + # "q": "0.0132", + # "m": True + # } + # + # watchMyTrades + # { + # "e": "ticketInfo", + # "E": "1723037391156", + # "s": "ETHUSDT-PERPETUAL", + # "q": "1.00", + # "t": "1723037391147", + # "p": "2463.36", + # "T": "1747358716187197441", + # "o": "1747358716129257216", + # "c": "1723037389677", + # "a": "1735619524953226496", + # "m": False, + # "S": "BUY" + # } + # + marketId = self.safe_string(trade, 's') + market = self.safe_market(marketId, market) + timestamp = self.safe_integer(trade, 't') + isMaker = self.safe_bool(trade, 'm') + takerOrMaker: Str = None + if isMaker is not None: + if isMaker: + takerOrMaker = 'maker' + else: + takerOrMaker = 'taker' + return self.safe_trade({ + 'id': self.safe_string_2(trade, 'v', 'T'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'side': self.safe_string_lower(trade, 'S'), + 'price': self.safe_string(trade, 'p'), + 'amount': self.safe_string(trade, 'q'), + 'cost': None, + 'takerOrMaker': takerOrMaker, + 'type': None, + 'order': self.safe_string(trade, 'o'), + 'fee': None, + 'info': trade, + }, market) + + async def watch_positions(self, symbols: Strings = None, since: Int = None, limit: Int = None, params={}) -> List[Position]: + """ + + https://hashkeyglobal-apidoc.readme.io/reference/websocket-api#private-stream + + watch all open positions + :param str[] [symbols]: list of unified market symbols to watch positions for + :param int [since]: the earliest time in ms to fetch positions for + :param int [limit]: the maximum number of positions to retrieve + :param dict params: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + await self.load_markets() + listenKey = await self.authenticate() + symbols = self.market_symbols(symbols) + messageHash = 'positions' + messageHashes = [] + if symbols is None: + messageHashes.append(messageHash) + else: + for i in range(0, len(symbols)): + symbol = symbols[i] + messageHashes.append(messageHash + ':' + symbol) + url = self.get_private_url(listenKey) + positions = await self.watch_multiple(url, messageHashes, None, messageHashes) + if self.newUpdates: + return positions + return self.filter_by_symbols_since_limit(self.positions, symbols, since, limit, True) + + def handle_position(self, client: Client, message): + # + # { + # "e": "outboundContractPositionInfo", + # "E": "1723084699801", + # "A": "1735619524953226496", + # "s": "ETHUSDT-PERPETUAL", + # "S": "LONG", + # "p": "2429.6", + # "P": "2", + # "a": "2", + # "f": "10760.14", + # "m": "1.0085", + # "r": "-0.0029", + # "up": "0.0478", + # "pr": "0.0492", + # "pv": "4.8592", + # "v": "5.00", + # "mt": "CROSS", + # "mm": "0.0367" + # } + # + if self.positions is None: + self.positions = ArrayCacheBySymbolBySide() + positions = self.positions + parsed = self.parse_ws_position(message) + positions.append(parsed) + messageHash = 'positions' + client.resolve(parsed, messageHash) + symbol = parsed['symbol'] + client.resolve(parsed, messageHash + ':' + symbol) + + def parse_ws_position(self, position, market: Market = None) -> Position: + marketId = self.safe_string(position, 's') + market = self.safe_market(marketId) + timestamp = self.safe_integer(position, 'E') + return self.safe_position({ + 'symbol': market['symbol'], + 'id': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'contracts': self.safe_number(position, 'P'), + 'contractSize': None, + 'side': self.safe_string_lower(position, 'S'), + 'notional': self.safe_number(position, 'pv'), + 'leverage': self.safe_integer(position, 'v'), + 'unrealizedPnl': self.safe_number(position, 'up'), + 'realizedPnl': self.safe_number(position, 'r'), + 'collateral': None, + 'entryPrice': self.safe_number(position, 'p'), + 'markPrice': None, + 'liquidationPrice': self.safe_number(position, 'f'), + 'marginMode': self.safe_string_lower(position, 'mt'), + 'hedged': True, + 'maintenanceMargin': self.safe_number(position, 'mm'), + 'maintenanceMarginPercentage': None, + 'initialMargin': self.safe_number(position, 'm'), # todo check + 'initialMarginPercentage': None, + 'marginRatio': None, + 'lastUpdateTimestamp': None, + 'lastPrice': None, + 'stopLossPrice': None, + 'takeProfitPrice': None, + 'percentage': None, + 'info': position, + }) + + async def watch_balance(self, params={}) -> Balances: + """ + watch balance and get the amount of funds available for trading or funds locked in orders + + https://hashkeyglobal-apidoc.readme.io/reference/websocket-api#private-stream + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: 'spot' or 'swap' - the type of the market to watch balance for(default 'spot') + :returns dict: a `balance structure ` + """ + listenKey = await self.authenticate() + await self.load_markets() + type = 'spot' + type, params = self.handle_market_type_and_params('watchBalance', None, params, type) + messageHash = 'balance:' + type + url = self.get_private_url(listenKey) + client = self.client(url) + self.set_balance_cache(client, type, messageHash) + fetchBalanceSnapshot = None + awaitBalanceSnapshot = None + fetchBalanceSnapshot, params = self.handle_option_and_params(self.options, 'watchBalance', 'fetchBalanceSnapshot', True) + awaitBalanceSnapshot, params = self.handle_option_and_params(self.options, 'watchBalance', 'awaitBalanceSnapshot', False) + if fetchBalanceSnapshot and awaitBalanceSnapshot: + await client.future(type + ':fetchBalanceSnapshot') + return await self.watch(url, messageHash, None, messageHash) + + def set_balance_cache(self, client: Client, type, subscribeHash): + if subscribeHash in client.subscriptions: + return + options = self.safe_dict(self.options, 'watchBalance') + snapshot = self.safe_bool(options, 'fetchBalanceSnapshot', True) + if snapshot: + messageHash = type + ':' + 'fetchBalanceSnapshot' + if not (messageHash in client.futures): + client.future(messageHash) + self.spawn(self.load_balance_snapshot, client, messageHash, type) + self.balance[type] = {} + # without self comment, transpilation breaks for some reason... + + async def load_balance_snapshot(self, client, messageHash, type): + response = await self.fetch_balance({'type': type}) + self.balance[type] = self.extend(response, self.safe_value(self.balance, type, {})) + # don't remove the future from the .futures cache + future = client.futures[messageHash] + future.resolve() + client.resolve(self.balance[type], 'balance:' + type) + + def handle_balance(self, client: Client, message): + # + # { + # "e": "outboundContractAccountInfo", # event type + # # outboundContractAccountInfo + # "E": "1714717314118", # event time + # "T": True, # can trade + # "W": True, # can withdraw + # "D": True, # can deposit + # "B": [ # balances changed + # { + # "a": "USDT", # asset + # "f": "474960.65", # free amount + # "l": "24835.178056020383226869", # locked amount + # "r": "" # to be released + # } + # ] + # } + # + event = self.safe_string(message, 'e') + data = self.safe_list(message, 'B', []) + balanceUpdate = self.safe_dict(data, 0) + isSpot = event == 'outboundAccountInfo' + type = 'spot' if isSpot else 'swap' + if not (type in self.balance): + self.balance[type] = {} + self.balance[type]['info'] = message + currencyId = self.safe_string(balanceUpdate, 'a') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(balanceUpdate, 'f') + account['used'] = self.safe_string(balanceUpdate, 'l') + self.balance[type][code] = account + self.balance[type] = self.safe_balance(self.balance[type]) + messageHash = 'balance:' + type + client.resolve(self.balance[type], messageHash) + + async def authenticate(self, params={}): + listenKey = self.safe_string(self.options, 'listenKey') + if listenKey is not None: + return listenKey + response = await self.privatePostApiV1UserDataStream(params) + # + # { + # "listenKey": "atbNEcWnBqnmgkfmYQeTuxKTpTStlZzgoPLJsZhzAOZTbAlxbHqGNWiYaUQzMtDz" + # } + # + listenKey = self.safe_string(response, 'listenKey') + self.options['listenKey'] = listenKey + listenKeyRefreshRate = self.safe_integer(self.options, 'listenKeyRefreshRate', 3600000) + self.delay(listenKeyRefreshRate, self.keep_alive_listen_key, listenKey, params) + return listenKey + + async def keep_alive_listen_key(self, listenKey, params={}): + if listenKey is None: + return + request: dict = { + 'listenKey': listenKey, + } + try: + await self.privatePutApiV1UserDataStream(self.extend(request, params)) + listenKeyRefreshRate = self.safe_integer(self.options, 'listenKeyRefreshRate', 1200000) + self.delay(listenKeyRefreshRate, self.keep_alive_listen_key, listenKey, params) + except Exception as error: + url = self.get_private_url(listenKey) + client = self.client(url) + self.options['listenKey'] = None + client.reject(error) + del self.clients[url] + + def handle_message(self, client: Client, message): + if isinstance(message, list): + message = self.safe_dict(message, 0, {}) + topic = self.safe_string_2(message, 'topic', 'e') + if topic == 'kline': + self.handle_ohlcv(client, message) + elif topic == 'realtimes': + self.handle_ticker(client, message) + elif topic == 'trade': + self.handle_trades(client, message) + elif topic == 'depth': + self.handle_order_book(client, message) + elif (topic == 'contractExecutionReport') or (topic == 'executionReport'): + self.handle_order(client, message) + elif topic == 'ticketInfo': + self.handle_my_trade(client, message) + elif topic == 'outboundContractPositionInfo': + self.handle_position(client, message) + elif (topic == 'outboundAccountInfo') or (topic == 'outboundContractAccountInfo'): + self.handle_balance(client, message) diff --git a/ccxt/pro/hitbtc.py b/ccxt/pro/hitbtc.py new file mode 100644 index 0000000..b32b3e9 --- /dev/null +++ b/ccxt/pro/hitbtc.py @@ -0,0 +1,1321 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolById, ArrayCacheByTimestamp +import hashlib +from ccxt.base.types import Any, Balances, Int, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade +from ccxt.async_support.base.ws.client import Client +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import NotSupported + + +class hitbtc(ccxt.async_support.hitbtc): + + def describe(self) -> Any: + return self.deep_extend(super(hitbtc, self).describe(), { + 'has': { + 'ws': True, + 'watchTicker': True, + 'watchTickers': True, + 'watchBidsAsks': True, + 'watchTrades': True, + 'watchTradesForSymbols': False, + 'watchOrderBook': True, + 'watchBalance': True, + 'watchOrders': True, + 'watchOHLCV': True, + 'watchMyTrades': False, + 'createOrderWs': True, + 'cancelOrderWs': True, + 'fetchOpenOrdersWs': True, + 'cancelAllOrdersWs': True, + }, + 'urls': { + 'api': { + 'ws': { + 'public': 'wss://api.hitbtc.com/api/3/ws/public', + 'private': 'wss://api.hitbtc.com/api/3/ws/trading', + }, + }, + 'test': { + 'ws': { + 'public': 'wss://api.demo.hitbtc.com/api/3/ws/public', + 'private': 'wss://api.demo.hitbtc.com/api/3/ws/trading', + }, + }, + }, + 'options': { + 'tradesLimit': 1000, + 'watchTicker': { + 'method': 'ticker/{speed}', # 'ticker/{speed}' or 'ticker/price/{speed}' + }, + 'watchTickers': { + 'method': 'ticker/{speed}', # 'ticker/{speed}','ticker/price/{speed}', 'ticker/{speed}/batch', or 'ticker/{speed}/price/batch'' + }, + 'watchBidsAsks': { + 'method': 'orderbook/top/{speed}', # 'orderbook/top/{speed}', 'orderbook/top/{speed}/batch' + }, + 'watchOrderBook': { + 'method': 'orderbook/full', # 'orderbook/full', 'orderbook/{depth}/{speed}', 'orderbook/{depth}/{speed}/batch' + }, + }, + 'timeframes': { + '1m': 'M1', + '3m': 'M3', + '5m': 'M5', + '15m': 'M15', + '30m': 'M30', + '1h': 'H1', + '4h': 'H4', + '1d': 'D1', + '1w': 'D7', + '1M': '1M', + }, + 'streaming': { + 'keepAlive': 4000, + }, + }) + + async def authenticate(self): + """ + @ignore + authenticates the user to access private web socket channels + + https://api.hitbtc.com/#socket-authentication + + :returns dict: response from exchange + """ + self.check_required_credentials() + url = self.urls['api']['ws']['private'] + messageHash = 'authenticated' + client = self.client(url) + future = client.reusableFuture(messageHash) + authenticated = self.safe_value(client.subscriptions, messageHash) + if authenticated is None: + timestamp = self.milliseconds() + signature = self.hmac(self.encode(self.number_to_string(timestamp)), self.encode(self.secret), hashlib.sha256, 'hex') + request: dict = { + 'method': 'login', + 'params': { + 'type': 'HS256', + 'api_key': self.apiKey, + 'timestamp': timestamp, + 'signature': signature, + }, + } + self.watch(url, messageHash, request, messageHash) + # + # { + # "jsonrpc": "2.0", + # "result": True + # } + # + # # Failure to return results + # + # { + # "jsonrpc": "2.0", + # "error": { + # "code": 1002, + # "message": "Authorization is required or has been failed", + # "description": "invalid signature format" + # } + # } + # + return await future + + async def subscribe_public(self, name: str, messageHashPrefix: str, symbols: Strings = None, params={}): + """ + @ignore + :param str name: websocket endpoint name + :param str messageHashPrefix: prefix for the message hash + :param str[] [symbols]: unified CCXT symbol(s) + :param dict [params]: extra parameters specific to the hitbtc api + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + isBatch = name.find('batch') >= 0 + url = self.urls['api']['ws']['public'] + messageHashes = [] + if symbols is not None and not isBatch: + for i in range(0, len(symbols)): + messageHashes.append(messageHashPrefix + '::' + symbols[i]) + else: + messageHashes.append(messageHashPrefix) + subscribe: dict = { + 'method': 'subscribe', + 'id': self.nonce(), + 'ch': name, + } + request = self.extend(subscribe, params) + return await self.watch_multiple(url, messageHashes, request, messageHashes) + + async def subscribe_private(self, name: str, symbol: Str = None, params={}): + """ + @ignore + :param str name: websocket endpoint name + :param str [symbol]: unified CCXT symbol + :param dict [params]: extra parameters specific to the hitbtc api + """ + await self.load_markets() + await self.authenticate() + url = self.urls['api']['ws']['private'] + splitName = name.split('_subscribe') + messageHash = self.safe_string(splitName, 0) + if symbol is not None: + messageHash = messageHash + '::' + symbol + subscribe: dict = { + 'method': name, + 'params': params, + 'id': self.nonce(), + } + return await self.watch(url, messageHash, subscribe, messageHash) + + async def trade_request(self, name: str, params={}): + """ + @ignore + :param str name: websocket endpoint name + :param dict [params]: extra parameters specific to the hitbtc api + """ + await self.load_markets() + await self.authenticate() + url = self.urls['api']['ws']['private'] + messageHash = str(self.nonce()) + subscribe: dict = { + 'method': name, + 'params': params, + 'id': messageHash, + } + return await self.watch(url, messageHash, subscribe, messageHash) + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://api.hitbtc.com/#subscribe-to-full-order-book + https://api.hitbtc.com/#subscribe-to-partial-order-book + https://api.hitbtc.com/#subscribe-to-partial-order-book-in-batches + https://api.hitbtc.com/#subscribe-to-top-of-book + https://api.hitbtc.com/#subscribe-to-top-of-book-in-batches + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.method]: 'orderbook/full', 'orderbook/{depth}/{speed}', 'orderbook/{depth}/{speed}/batch' + :param int [params.depth]: 5 , 10, or 20(default) + :param int [params.speed]: 100(default), 500, or 1000 + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + options = self.safe_value(self.options, 'watchOrderBook') + defaultMethod = self.safe_string(options, 'method', 'orderbook/full') + name = self.safe_string_2(params, 'method', 'defaultMethod', defaultMethod) + depth = self.safe_string(params, 'depth', '20') + speed = self.safe_string(params, 'depth', '100') + if name == 'orderbook/{depth}/{speed}': + name = 'orderbook/D' + depth + '/' + speed + 'ms' + elif name == 'orderbook/{depth}/{speed}/batch': + name = 'orderbook/D' + depth + '/' + speed + 'ms/batch' + market = self.market(symbol) + request: dict = { + 'params': { + 'symbols': [market['id']], + }, + } + orderbook = await self.subscribe_public(name, 'orderbooks', [symbol], self.deep_extend(request, params)) + return orderbook.limit() + + def handle_order_book(self, client: Client, message): + # + # { + # "ch": "orderbook/full", # Channel + # "snapshot": { + # "ETHBTC": { + # "t": 1626866578796, # Timestamp in milliseconds + # "s": 27617207, # Sequence number + # "a": [ # Asks + # ["0.060506", "0"], + # ["0.060549", "12.6431"], + # ["0.060570", "0"], + # ["0.060612", "0"] + # ], + # "b": [ # Bids + # ["0.060439", "4.4095"], + # ["0.060414", "0"], + # ["0.060407", "7.3349"], + # ["0.060390", "0"] + # ] + # } + # } + # } + # + snapshot = self.safe_dict(message, 'snapshot') + update = self.safe_dict(message, 'update') + data = snapshot if snapshot else update + type = 'snapshot' if snapshot else 'update' + marketIds = list(data.keys()) + for i in range(0, len(marketIds)): + marketId = marketIds[i] + market = self.safe_market(marketId) + symbol = market['symbol'] + item = data[marketId] + messageHash = 'orderbooks::' + symbol + if not (symbol in self.orderbooks): + subscription = self.safe_dict(client.subscriptions, messageHash, {}) + limit = self.safe_integer(subscription, 'limit') + self.orderbooks[symbol] = self.order_book({}, limit) + orderbook = self.orderbooks[symbol] + timestamp = self.safe_integer(item, 't') + nonce = self.safe_integer(item, 's') + if type == 'snapshot': + parsedSnapshot = self.parse_order_book(item, symbol, timestamp, 'b', 'a') + orderbook.reset(parsedSnapshot) + else: + asks = self.safe_list(item, 'a', []) + bids = self.safe_list(item, 'b', []) + self.handle_deltas(orderbook['asks'], asks) + self.handle_deltas(orderbook['bids'], bids) + orderbook['timestamp'] = timestamp + orderbook['datetime'] = self.iso8601(timestamp) + orderbook['nonce'] = nonce + orderbook['symbol'] = symbol + self.orderbooks[symbol] = orderbook + client.resolve(orderbook, messageHash) + + def handle_delta(self, bookside, delta): + price = self.safe_number(delta, 0) + amount = self.safe_number(delta, 1) + bookside.store(price, amount) + + def handle_deltas(self, bookside, deltas): + for i in range(0, len(deltas)): + self.handle_delta(bookside, deltas[i]) + + async def watch_ticker(self, symbol: str, params={}) -> Ticker: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://api.hitbtc.com/#subscribe-to-ticker + https://api.hitbtc.com/#subscribe-to-ticker-in-batches + https://api.hitbtc.com/#subscribe-to-mini-ticker + https://api.hitbtc.com/#subscribe-to-mini-ticker-in-batches + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.method]: 'ticker/{speed}'(default), or 'ticker/price/{speed}' + :param str [params.speed]: '1s'(default), or '3s' + :returns dict: a `ticker structure ` + """ + ticker = await self.watch_tickers([symbol], params) + return self.safe_value(ticker, symbol) + + async def watch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + :param str[] [symbols]: + :param dict params: extra parameters specific to the exchange API endpoint + :param str params['method']: 'ticker/{speed}' ,'ticker/price/{speed}', 'ticker/{speed}/batch'(default), or 'ticker/{speed}/price/batch'' + :param str params['speed']: '1s'(default), or '3s' + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + options = self.safe_value(self.options, 'watchTicker') + defaultMethod = self.safe_string(options, 'method', 'ticker/{speed}/batch') + method = self.safe_string_2(params, 'method', 'defaultMethod', defaultMethod) + speed = self.safe_string(params, 'speed', '1s') + name = self.implode_params(method, {'speed': speed}) + params = self.omit(params, ['method', 'speed']) + marketIds = [] + if symbols is None: + marketIds.append('*') + else: + for i in range(0, len(symbols)): + marketId = self.market_id(symbols[i]) + marketIds.append(marketId) + request: dict = { + 'params': { + 'symbols': marketIds, + }, + } + newTickers = await self.subscribe_public(name, 'tickers', symbols, self.deep_extend(request, params)) + if self.newUpdates: + if not isinstance(newTickers, list): + tickers: dict = {} + tickers[newTickers['symbol']] = newTickers + return tickers + return self.filter_by_array(newTickers, 'symbol', symbols) + + def handle_ticker(self, client: Client, message): + # + # { + # "ch": "ticker/1s", + # "data": { + # "ETHBTC": { + # "t": 1614815872000, # Timestamp in milliseconds + # "a": "0.031175", # Best ask + # "A": "0.03329", # Best ask quantity + # "b": "0.031148", # Best bid + # "B": "0.10565", # Best bid quantity + # "c": "0.031210", # Last price + # "o": "0.030781", # Open price + # "h": "0.031788", # High price + # "l": "0.030733", # Low price + # "v": "62.587", # Base asset volume + # "q": "1.951420577", # Quote asset volume + # "p": "0.000429", # Price change + # "P": "1.39", # Price change percent + # "L": 1182694927 # Last trade identifier + # } + # } + # } + # + # { + # "ch": "ticker/price/1s", + # "data": { + # "BTCUSDT": { + # "t": 1614815872030, + # "o": "32636.79", + # "c": "32085.51", + # "h": "33379.92", + # "l": "30683.28", + # "v": "11.90667", + # "q": "384081.1955629" + # } + # } + # } + # + data = self.safe_value(message, 'data', {}) + marketIds = list(data.keys()) + result = [] + topic = 'tickers' + for i in range(0, len(marketIds)): + marketId = marketIds[i] + market = self.safe_market(marketId) + symbol = market['symbol'] + ticker = self.parse_ws_ticker(data[marketId], market) + self.tickers[symbol] = ticker + result.append(ticker) + messageHash = topic + '::' + symbol + client.resolve(ticker, messageHash) + client.resolve(result, topic) + + def parse_ws_ticker(self, ticker, market=None): + # + # { + # "t": 1614815872000, # Timestamp in milliseconds + # "a": "0.031175", # Best ask + # "A": "0.03329", # Best ask quantity + # "b": "0.031148", # Best bid + # "B": "0.10565", # Best bid quantity + # "c": "0.031210", # Last price + # "o": "0.030781", # Open price + # "h": "0.031788", # High price + # "l": "0.030733", # Low price + # "v": "62.587", # Base asset volume + # "q": "1.951420577", # Quote asset volume + # "p": "0.000429", # Price change + # "P": "1.39", # Price change percent + # "L": 1182694927 # Last trade identifier + # } + # + # { + # "t": 1614815872030, + # "o": "32636.79", + # "c": "32085.51", + # "h": "33379.92", + # "l": "30683.28", + # "v": "11.90667", + # "q": "384081.1955629" + # } + # + timestamp = self.safe_integer(ticker, 't') + symbol = self.safe_symbol(None, market) + last = self.safe_string(ticker, 'c') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'h'), + 'low': self.safe_string(ticker, 'l'), + 'bid': self.safe_string(ticker, 'b'), + 'bidVolume': self.safe_string(ticker, 'B'), + 'ask': self.safe_string(ticker, 'a'), + 'askVolume': self.safe_string(ticker, 'A'), + 'vwap': None, + 'open': self.safe_string(ticker, 'o'), + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': self.safe_string(ticker, 'v'), + 'quoteVolume': self.safe_string(ticker, 'q'), + 'info': ticker, + }, market) + + async def watch_bids_asks(self, symbols: Strings = None, params={}) -> Tickers: + """ + watches best bid & ask for symbols + + https://api.hitbtc.com/#subscribe-to-top-of-book + + :param str[] symbols: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.method]: 'orderbook/top/{speed}' or 'orderbook/top/{speed}/batch(default)' + :param str [params.speed]: '100ms'(default) or '500ms' or '1000ms' + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, False) + options = self.safe_value(self.options, 'watchBidsAsks') + defaultMethod = self.safe_string(options, 'method', 'orderbook/top/{speed}/batch') + method = self.safe_string_2(params, 'method', 'defaultMethod', defaultMethod) + speed = self.safe_string(params, 'speed', '100ms') + name = self.implode_params(method, {'speed': speed}) + params = self.omit(params, ['method', 'speed']) + marketIds = self.market_ids(symbols) + request: dict = { + 'params': { + 'symbols': marketIds, + }, + } + newTickers = await self.subscribe_public(name, 'bidask', symbols, self.deep_extend(request, params)) + if self.newUpdates: + if not isinstance(newTickers, list): + tickers: dict = {} + tickers[newTickers['symbol']] = newTickers + return tickers + return self.filter_by_array(newTickers, 'symbol', symbols) + + def handle_bid_ask(self, client: Client, message): + # + # { + # "ch": "orderbook/top/100ms", # or 'orderbook/top/100ms/batch' + # "data": { + # "BTCUSDT": { + # "t": 1727276919771, + # "a": "63931.45", + # "A": "0.02879", + # "b": "63926.97", + # "B": "0.00100" + # } + # } + # } + # + data = self.safe_dict(message, 'data', {}) + marketIds = list(data.keys()) + result = [] + topic = 'bidask' + for i in range(0, len(marketIds)): + marketId = marketIds[i] + market = self.safe_market(marketId) + symbol = market['symbol'] + ticker = self.parse_ws_bid_ask(data[marketId], market) + self.bidsasks[symbol] = ticker + result.append(ticker) + messageHash = topic + '::' + symbol + client.resolve(ticker, messageHash) + client.resolve(result, topic) + + def parse_ws_bid_ask(self, ticker, market=None): + timestamp = self.safe_integer(ticker, 't') + return self.safe_ticker({ + 'symbol': market['symbol'], + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'ask': self.safe_string(ticker, 'a'), + 'askVolume': self.safe_string(ticker, 'A'), + 'bid': self.safe_string(ticker, 'b'), + 'bidVolume': self.safe_string(ticker, 'B'), + 'info': ticker, + }, market) + + async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://api.hitbtc.com/#subscribe-to-trades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'params': { + 'symbols': [market['id']], + }, + } + if limit is not None: + request['limit'] = limit + name = 'trades' + trades = await self.subscribe_public(name, 'trades', [symbol], self.deep_extend(request, params)) + if self.newUpdates: + limit = trades.getLimit(symbol, limit) + return self.filter_by_since_limit(trades, since, limit, 'timestamp') + + def handle_trades(self, client: Client, message): + # + # { + # "result": { + # "ch": "trades", # Channel + # "subscriptions": ["ETHBTC", "BTCUSDT"] + # }, + # "id": 123 + # } + # + # Notification snapshot + # + # { + # "ch": "trades", # Channel + # "snapshot": { + # "BTCUSDT": [{ + # "t": 1626861109494, # Timestamp in milliseconds + # "i": 1555634969, # Trade identifier + # "p": "30881.96", # Price + # "q": "12.66828", # Quantity + # "s": "buy" # Side + # }] + # } + # } + # + # Notification update + # + # { + # "ch": "trades", + # "update": { + # "BTCUSDT": [{ + # "t": 1626861123552, + # "i": 1555634969, + # "p": "30877.68", + # "q": "0.00006", + # "s": "sell" + # }] + # } + # } + # + data = self.safe_value_2(message, 'snapshot', 'update', {}) + marketIds = list(data.keys()) + for i in range(0, len(marketIds)): + marketId = marketIds[i] + market = self.safe_market(marketId) + tradesLimit = self.safe_integer(self.options, 'tradesLimit', 1000) + symbol = market['symbol'] + stored = self.safe_value(self.trades, symbol) + if stored is None: + stored = ArrayCache(tradesLimit) + self.trades[symbol] = stored + trades = self.parse_ws_trades(data[marketId], market) + for j in range(0, len(trades)): + stored.append(trades[j]) + messageHash = 'trades::' + symbol + client.resolve(stored, messageHash) + return message + + def parse_ws_trades(self, trades, market: object = None, since: Int = None, limit: Int = None, params={}): + trades = self.to_array(trades) + result = [] + for i in range(0, len(trades)): + trade = self.extend(self.parse_ws_trade(trades[i], market), params) + result.append(trade) + result = self.sort_by_2(result, 'timestamp', 'id') + symbol = self.safe_string(market, 'symbol') + return self.filter_by_symbol_since_limit(result, symbol, since, limit) + + def parse_ws_trade(self, trade, market=None): + # + # { + # "t": 1626861123552, # Timestamp in milliseconds + # "i": 1555634969, # Trade identifier + # "p": "30877.68", # Price + # "q": "0.00006", # Quantity + # "s": "sell" # Side + # } + # + timestamp = self.safe_integer(trade, 't') + return self.safe_trade({ + 'info': trade, + 'id': self.safe_string(trade, 'i'), + 'order': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': self.safe_string(market, 'symbol'), + 'type': None, + 'side': self.safe_string(trade, 's'), + 'takerOrMaker': None, + 'price': self.safe_string(trade, 'p'), + 'amount': self.safe_string(trade, 'q'), + 'cost': None, + 'fee': None, + }, market) + + async def watch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + watches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://api.hitbtc.com/#subscribe-to-candles + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str [timeframe]: the length of time each candle represents + :param int [since]: not used by hitbtc watchOHLCV + :param int [limit]: 0 – 1000, default value = 0(no history returned) + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + period = self.safe_string(self.timeframes, timeframe, timeframe) + name = 'candles/' + period + market = self.market(symbol) + request: dict = { + 'params': { + 'symbols': [market['id']], + }, + } + if limit is not None: + request['params']['limit'] = limit + ohlcv = await self.subscribe_public(name, 'candles', [symbol], self.deep_extend(request, params)) + if self.newUpdates: + limit = ohlcv.getLimit(symbol, limit) + return self.filter_by_since_limit(ohlcv, since, limit, 0) + + def handle_ohlcv(self, client: Client, message): + # + # { + # "ch": "candles/M1", # Channel + # "snapshot": { + # "BTCUSDT": [{ + # "t": 1626860340000, # Message timestamp + # "o": "30881.95", # Open price + # "c": "30890.96", # Last price + # "h": "30900.8", # High price + # "l": "30861.27", # Low price + # "v": "1.27852", # Base asset volume + # "q": "39493.9021811" # Quote asset volume + # } + # ... + # ] + # } + # } + # + # { + # "ch": "candles/M1", + # "update": { + # "ETHBTC": [{ + # "t": 1626860880000, + # "o": "0.060711", + # "c": "0.060749", + # "h": "0.060749", + # "l": "0.060711", + # "v": "12.2800", + # "q": "0.7455339675" + # }] + # } + # } + # + data = self.safe_value_2(message, 'snapshot', 'update', {}) + marketIds = list(data.keys()) + channel = self.safe_string(message, 'ch') + splitChannel = channel.split('/') + period = self.safe_string(splitChannel, 1) + timeframe = self.find_timeframe(period) + for i in range(0, len(marketIds)): + marketId = marketIds[i] + market = self.safe_market(marketId) + symbol = market['symbol'] + self.ohlcvs[symbol] = self.safe_value(self.ohlcvs, symbol, {}) + stored = self.safe_value(self.ohlcvs[symbol], timeframe) + if stored is None: + limit = self.safe_integer(self.options, 'OHLCVLimit', 1000) + stored = ArrayCacheByTimestamp(limit) + self.ohlcvs[symbol][timeframe] = stored + ohlcvs = self.parse_ws_ohlcvs(data[marketId], market) + for j in range(0, len(ohlcvs)): + stored.append(ohlcvs[j]) + messageHash = 'candles::' + symbol + client.resolve(stored, messageHash) + return message + + def parse_ws_ohlcv(self, ohlcv, market=None) -> list: + # + # { + # "t": 1626860340000, # Message timestamp + # "o": "30881.95", # Open price + # "c": "30890.96", # Last price + # "h": "30900.8", # High price + # "l": "30861.27", # Low price + # "v": "1.27852", # Base asset volume + # "q": "39493.9021811" # Quote asset volume + # } + # + return [ + self.safe_integer(ohlcv, 't'), + self.safe_number(ohlcv, 'o'), + self.safe_number(ohlcv, 'h'), + self.safe_number(ohlcv, 'l'), + self.safe_number(ohlcv, 'c'), + self.safe_number(ohlcv, 'v'), + ] + + async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + watches information on multiple orders made by the user + + https://api.hitbtc.com/#subscribe-to-reports + https://api.hitbtc.com/#subscribe-to-reports-2 + https://api.hitbtc.com/#subscribe-to-reports-3 + + :param str [symbol]: unified CCXT market symbol + :param int [since]: timestamp in ms of the earliest order to fetch + :param int [limit]: the maximum amount of orders to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + marketType = None + market = None + if symbol is not None: + market = self.market(symbol) + marketType, params = self.handle_market_type_and_params('watchOrders', market, params) + name = self.get_supported_mapping(marketType, { + 'spot': 'spot_subscribe', + 'margin': 'margin_subscribe', + 'swap': 'futures_subscribe', + 'future': 'futures_subscribe', + }) + orders = await self.subscribe_private(name, symbol, params) + if self.newUpdates: + limit = orders.getLimit(symbol, limit) + return self.filter_by_since_limit(orders, since, limit, 'timestamp') + + def handle_order(self, client: Client, message): + # + # { + # "jsonrpc": "2.0", + # "method": "spot_order", # "margin_order", "future_order" + # "params": { + # "id": 584244931496, + # "client_order_id": "b5acd79c0a854b01b558665bcf379456", + # "symbol": "BTCUSDT", + # "side": "buy", + # "status": "new", + # "type": "limit", + # "time_in_force": "GTC", + # "quantity": "0.01000", + # "quantity_cumulative": "0", + # "price": "0.01", # only updates and snapshots + # "post_only": False, + # "reduce_only": False, # only margin and contract + # "display_quantity": "0", # only updates and snapshot + # "created_at": "2021-07-02T22:52:32.864Z", + # "updated_at": "2021-07-02T22:52:32.864Z", + # "trade_id": 1361977606, # only trades + # "trade_quantity": "0.00001", # only trades + # "trade_price": "49595.04", # only trades + # "trade_fee": "0.001239876000", # only trades + # "trade_taker": True, # only trades, only spot + # "trade_position_id": 485308, # only trades, only margin + # "report_type": "new" # "trade", "status"(snapshot) + # } + # } + # + # { + # "jsonrpc": "2.0", + # "method": "spot_orders", # "margin_orders", "future_orders" + # "params": [ + # { + # "id": 584244931496, + # "client_order_id": "b5acd79c0a854b01b558665bcf379456", + # "symbol": "BTCUSDT", + # "side": "buy", + # "status": "new", + # "type": "limit", + # "time_in_force": "GTC", + # "quantity": "0.01000", + # "quantity_cumulative": "0", + # "price": "0.01", # only updates and snapshots + # "post_only": False, + # "reduce_only": False, # only margin and contract + # "display_quantity": "0", # only updates and snapshot + # "created_at": "2021-07-02T22:52:32.864Z", + # "updated_at": "2021-07-02T22:52:32.864Z", + # "trade_id": 1361977606, # only trades + # "trade_quantity": "0.00001", # only trades + # "trade_price": "49595.04", # only trades + # "trade_fee": "0.001239876000", # only trades + # "trade_taker": True, # only trades, only spot + # "trade_position_id": 485308, # only trades, only margin + # "report_type": "new" # "trade", "status"(snapshot) + # } + # ] + # } + # + if self.orders is None: + limit = self.safe_integer(self.options, 'ordersLimit') + self.orders = ArrayCacheBySymbolById(limit) + data = self.safe_value(message, 'params', []) + if isinstance(data, list): + for i in range(0, len(data)): + order = data[i] + self.handle_order_helper(client, message, order) + else: + self.handle_order_helper(client, message, data) + return message + + def handle_order_helper(self, client: Client, message, order): + orders = self.orders + marketId = self.safe_string_lower_2(order, 'instrument', 'symbol') + method = self.safe_string(message, 'method') + splitMethod = method.split('_order') + messageHash = self.safe_string(splitMethod, 0) + symbol = self.safe_symbol(marketId) + parsed = self.parse_order(order) + orders.append(parsed) + client.resolve(orders, messageHash) + client.resolve(orders, messageHash + '::' + symbol) + + def parse_ws_order_trade(self, trade, market=None): + # + # { + # "id": 584244931496, + # "client_order_id": "b5acd79c0a854b01b558665bcf379456", + # "symbol": "BTCUSDT", + # "side": "buy", + # "status": "new", + # "type": "limit", + # "time_in_force": "GTC", + # "quantity": "0.01000", + # "quantity_cumulative": "0", + # "price": "0.01", # only updates and snapshots + # "post_only": False, + # "reduce_only": False, # only margin and contract + # "display_quantity": "0", # only updates and snapshot + # "created_at": "2021-07-02T22:52:32.864Z", + # "updated_at": "2021-07-02T22:52:32.864Z", + # "trade_id": 1361977606, # only trades + # "trade_quantity": "0.00001", # only trades + # "trade_price": "49595.04", # only trades + # "trade_fee": "0.001239876000", # only trades + # "trade_taker": True, # only trades, only spot + # "trade_position_id": 485308, # only trades, only margin + # "report_type": "new" # "trade", "status"(snapshot) + # } + # + timestamp = self.safe_integer(trade, 'created_at') + marketId = self.safe_string(trade, 'symbol') + return self.safe_trade({ + 'info': trade, + 'id': self.safe_string(trade, 'trade_id'), + 'order': self.safe_string(trade, 'id'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': self.safe_market(marketId, market), + 'type': None, + 'side': self.safe_string(trade, 'side'), + 'takerOrMaker': self.safe_string(trade, 'trade_taker'), + 'price': self.safe_string(trade, 'trade_price'), + 'amount': self.safe_string(trade, 'trade_quantity'), + 'cost': None, + 'fee': { + 'cost': self.safe_string(trade, 'trade_fee'), + 'currency': None, + 'rate': None, + }, + }, market) + + def parse_ws_order(self, order, market=None): + # + # { + # "id": 584244931496, + # "client_order_id": "b5acd79c0a854b01b558665bcf379456", + # "symbol": "BTCUSDT", + # "side": "buy", + # "status": "new", + # "type": "limit", + # "time_in_force": "GTC", + # "quantity": "0.01000", + # "quantity_cumulative": "0", + # "price": "0.01", # only updates and snapshots + # "post_only": False, + # "reduce_only": False, # only margin and contract + # "display_quantity": "0", # only updates and snapshot + # "created_at": "2021-07-02T22:52:32.864Z", + # "updated_at": "2021-07-02T22:52:32.864Z", + # "trade_id": 1361977606, # only trades + # "trade_quantity": "0.00001", # only trades + # "trade_price": "49595.04", # only trades + # "trade_fee": "0.001239876000", # only trades + # "trade_taker": True, # only trades, only spot + # "trade_position_id": 485308, # only trades, only margin + # "report_type": "new" # "trade", "status"(snapshot) + # } + # + timestamp = self.safe_string(order, 'created_at') + marketId = self.safe_string(order, 'symbol') + market = self.safe_market(marketId, market) + tradeId = self.safe_string(order, 'trade_id') + trades = None + if tradeId is not None: + trade = self.parse_ws_order_trade(order, market) + trades = [trade] + rawStatus = self.safe_string(order, 'status') + report_type = self.safe_string(order, 'report_type') + parsedStatus = None + if report_type == 'canceled': + parsedStatus = self.parse_order_status(report_type) + else: + parsedStatus = self.parse_order_status(rawStatus) + return self.safe_order({ + 'info': order, + 'id': self.safe_string(order, 'id'), + 'clientOrderId': self.safe_string(order, 'client_order_id'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'symbol': market['symbol'], + 'price': self.safe_string(order, 'price'), + 'amount': self.safe_string(order, 'quantity'), + 'type': self.safe_string(order, 'type'), + 'side': self.safe_string_upper(order, 'side'), + 'timeInForce': self.safe_string(order, 'time_in_force'), + 'postOnly': self.safe_string(order, 'post_only'), + 'reduceOnly': self.safe_value(order, 'reduce_only'), + 'filled': None, + 'remaining': None, + 'cost': None, + 'status': parsedStatus, + 'average': None, + 'trades': trades, + 'fee': None, + }, market) + + async def watch_balance(self, params={}) -> Balances: + """ + watches balance updates, cannot subscribe to margin account balances + + https://api.hitbtc.com/#subscribe-to-spot-balances + https://api.hitbtc.com/#subscribe-to-futures-balances + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: 'spot', 'swap', or 'future' + + EXCHANGE SPECIFIC PARAMETERS + :param str [params.mode]: 'updates' or 'batches'(default), 'updates' = messages arrive after balance updates, 'batches' = messages arrive at equal intervals if there were any updates + :returns dict[]: a list of `balance structures ` + """ + await self.load_markets() + type = None + type, params = self.handle_market_type_and_params('watchBalance', None, params) + name = self.get_supported_mapping(type, { + 'spot': 'spot_balance_subscribe', + 'swap': 'futures_balance_subscribe', + 'future': 'futures_balance_subscribe', + }) + mode = self.safe_string(params, 'mode', 'batches') + params = self.omit(params, 'mode') + request: dict = { + 'mode': mode, + } + return await self.subscribe_private(name, None, self.extend(request, params)) + + async def create_order_ws(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}) -> Order: + """ + create a trade order + + https://api.hitbtc.com/#create-new-spot-order + https://api.hitbtc.com/#create-margin-order + https://api.hitbtc.com/#create-futures-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' only 'isolated' is supported for spot-margin, swap supports both, default is 'cross' + :param bool [params.margin]: True for creating a margin order + :param float [params.triggerPrice]: The price at which a trigger order is triggered at + :param bool [params.postOnly]: if True, the order will only be posted to the order book and not executed immediately + :param str [params.timeInForce]: "GTC", "IOC", "FOK", "Day", "GTD" + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + request = None + marketType = None + marketType, params = self.handle_market_type_and_params('createOrder', market, params) + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('createOrder', params) + request, params = self.create_order_request(market, marketType, type, side, amount, price, marginMode, params) + request = self.extend(request, params) + if marketType == 'swap': + return await self.trade_request('futures_new_order', request) + elif (marketType == 'margin') or (marginMode is not None): + return await self.trade_request('margin_new_order', request) + else: + return await self.trade_request('spot_new_order', request) + + async def cancel_order_ws(self, id: str, symbol: Str = None, params={}) -> Order: + """ + + https://api.hitbtc.com/#cancel-spot-order-2 + https://api.hitbtc.com/#cancel-futures-order-2 + https://api.hitbtc.com/#cancel-margin-order-2 + + cancels an open order + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' only 'isolated' is supported + :param bool [params.margin]: True for canceling a margin order + :returns dict: An `order structure ` + """ + await self.load_markets() + market = None + request = { + 'client_order_id': id, + } + if symbol is not None: + market = self.market(symbol) + marketType = None + marketType, params = self.handle_market_type_and_params('cancelOrderWs', market, params) + marginMode, query = self.handle_margin_mode_and_params('cancelOrderWs', params) + request = self.extend(request, query) + if marketType == 'swap': + return await self.trade_request('futures_cancel_order', request) + elif (marketType == 'margin') or (marginMode is not None): + return await self.trade_request('margin_cancel_order', request) + else: + return await self.trade_request('spot_cancel_order', request) + + async def cancel_all_orders_ws(self, symbol: Str = None, params={}) -> List[Order]: + """ + + https://api.hitbtc.com/#cancel-spot-orders + https://api.hitbtc.com/#cancel-futures-order-3 + + cancel all open orders + :param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' only 'isolated' is supported + :param bool [params.margin]: True for canceling margin orders + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + marketType = None + marketType, params = self.handle_market_type_and_params('cancelAllOrdersWs', market, params) + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('cancelAllOrdersWs', params) + if marketType == 'swap': + return await self.trade_request('futures_cancel_orders', params) + elif (marketType == 'margin') or (marginMode is not None): + raise NotSupported(self.id + ' cancelAllOrdersWs is not supported for margin orders') + else: + return await self.trade_request('spot_cancel_orders', params) + + async def fetch_open_orders_ws(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + + https://api.hitbtc.com/#get-active-futures-orders-2 + https://api.hitbtc.com/#get-margin-orders + https://api.hitbtc.com/#get-active-spot-orders + + fetch all unfilled currently open orders + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' only 'isolated' is supported + :param bool [params.margin]: True for fetching open margin orders + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + market = None + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + marketType = None + marketType, params = self.handle_market_type_and_params('fetchOpenOrdersWs', market, params) + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchOpenOrdersWs', params) + if marketType == 'swap': + return await self.trade_request('futures_get_orders', request) + elif (marketType == 'margin') or (marginMode is not None): + return await self.trade_request('margin_get_orders', request) + else: + return await self.trade_request('spot_get_orders', request) + + def handle_balance(self, client: Client, message): + # + # { + # "jsonrpc": "2.0", + # "method": "futures_balance", + # "params": [ + # { + # "currency": "BCN", + # "available": "100.000000000000", + # "reserved": "0", + # "reserved_margin": "0" + # }, + # ... + # ] + # } + # + messageHash = self.safe_string(message, 'method') + params = self.safe_value(message, 'params') + balance = self.parse_balance(params) + self.balance = self.deep_extend(self.balance, balance) + client.resolve(self.balance, messageHash) + + def handle_notification(self, client: Client, message): + # + # {jsonrpc: "2.0", result: True, id: null} + # + return message + + def handle_order_request(self, client: Client, message): + # + # createOrderWs, cancelOrderWs + # + # { + # "jsonrpc": "2.0", + # "result": { + # "id": 1130310696965, + # "client_order_id": "OPC2oyHSkEBqIpPtniLqeW-597hUL3Yo", + # "symbol": "ADAUSDT", + # "side": "buy", + # "status": "new", + # "type": "limit", + # "time_in_force": "GTC", + # "quantity": "4", + # "quantity_cumulative": "0", + # "price": "0.3300000", + # "post_only": False, + # "created_at": "2023-11-17T14:58:15.903Z", + # "updated_at": "2023-11-17T14:58:15.903Z", + # "original_client_order_id": "d6b645556af740b1bd1683400fd9cbce", # spot_replace_order only + # "report_type": "new" + # "margin_mode": "isolated", # margin and future only + # "reduce_only": False, # margin and future only + # }, + # "id": 1700233093414 + # } + # + messageHash = self.safe_string(message, 'id') + result = self.safe_value(message, 'result', {}) + if isinstance(result, list): + parsedOrders = [] + for i in range(0, len(result)): + parsedOrder = self.parse_ws_order(result[i]) + parsedOrders.append(parsedOrder) + client.resolve(parsedOrders, messageHash) + else: + parsedOrder = self.parse_ws_order(result) + client.resolve(parsedOrder, messageHash) + return message + + def handle_message(self, client: Client, message): + if self.handle_error(client, message): + return + channel = self.safe_string_2(message, 'ch', 'method') + if channel is not None: + splitChannel = channel.split('/') + channel = self.safe_string(splitChannel, 0) + if channel == 'orderbook': + channel2 = self.safe_string(splitChannel, 1) + if channel2 is not None and channel2 == 'top': + channel = 'orderbook/top' + methods: dict = { + 'candles': self.handle_ohlcv, + 'ticker': self.handle_ticker, + 'trades': self.handle_trades, + 'orderbook': self.handle_order_book, + 'orderbook/top': self.handle_bid_ask, + 'spot_order': self.handle_order, + 'spot_orders': self.handle_order, + 'margin_order': self.handle_order, + 'margin_orders': self.handle_order, + 'futures_order': self.handle_order, + 'futures_orders': self.handle_order, + 'spot_balance': self.handle_balance, + 'futures_balance': self.handle_balance, + } + method = self.safe_value(methods, channel) + if method is not None: + method(client, message) + else: + result = self.safe_value(message, 'result') + clientOrderId = self.safe_string(result, 'client_order_id') + if clientOrderId is not None: + self.handle_order_request(client, message) + if (result is True) and not ('id' in message): + self.handle_authenticate(client, message) + if isinstance(result, list): + # to do improve self, not very reliable right now + first = self.safe_value(result, 0, {}) + arrayLength = len(result) + if (arrayLength == 0) or ('client_order_id' in first): + self.handle_order_request(client, message) + + def handle_authenticate(self, client: Client, message): + # + # { + # "jsonrpc": "2.0", + # "result": True + # } + # + success = self.safe_value(message, 'result') + messageHash = 'authenticated' + if success: + future = self.safe_value(client.futures, messageHash) + future.resolve(True) + else: + error = AuthenticationError(self.id + ' ' + self.json(message)) + client.reject(error, messageHash) + if messageHash in client.subscriptions: + del client.subscriptions[messageHash] + return message + + def handle_error(self, client: Client, message): + # + # { + # jsonrpc: '2.0', + # error: { + # code: 20001, + # message: 'Insufficient funds', + # description: 'Check that the funds are sufficient, given commissions' + # }, + # id: 1700228604325 + # } + # + error = self.safe_value(message, 'error') + if error is not None: + try: + code = self.safe_value(error, 'code') + errorMessage = self.safe_string(error, 'message') + description = self.safe_string(error, 'description') + feedback = self.id + ' ' + description + self.throw_exactly_matched_exception(self.exceptions['exact'], code, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], errorMessage, feedback) + raise ExchangeError(feedback) # unknown message + except Exception as e: + if isinstance(e, AuthenticationError): + messageHash = 'authenticated' + client.reject(e, messageHash) + if messageHash in client.subscriptions: + del client.subscriptions[messageHash] + else: + id = self.safe_string(message, 'id') + client.reject(e, id) + return True + return None diff --git a/ccxt/pro/hollaex.py b/ccxt/pro/hollaex.py new file mode 100644 index 0000000..bf9ac6d --- /dev/null +++ b/ccxt/pro/hollaex.py @@ -0,0 +1,579 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolById +import hashlib +from ccxt.base.types import Any, Balances, Bool, Int, Order, OrderBook, Str, Trade +from ccxt.async_support.base.ws.client import Client +from typing import List +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol + + +class hollaex(ccxt.async_support.hollaex): + + def describe(self) -> Any: + return self.deep_extend(super(hollaex, self).describe(), { + 'has': { + 'ws': True, + 'watchBalance': True, + 'watchMyTrades': False, + 'watchOHLCV': False, + 'watchOrderBook': True, + 'watchOrders': True, + 'watchTicker': False, + 'watchTickers': False, # for now + 'watchTrades': True, + 'watchTradesForSymbols': False, + }, + 'urls': { + 'api': { + 'ws': 'wss://api.hollaex.com/stream', + }, + 'test': { + 'ws': 'wss://api.sandbox.hollaex.com/stream', + }, + }, + 'options': { + 'watchBalance': { + # 'api-expires': None, + }, + 'watchOrders': { + # 'api-expires': None, + }, + }, + 'streaming': { + 'ping': self.ping, + }, + 'exceptions': { + 'ws': { + 'exact': { + 'Bearer or HMAC authentication required': BadSymbol, # {error: 'Bearer or HMAC authentication required'} + 'Error: wrong input': BadRequest, # {error: 'Error: wrong input'} + }, + }, + }, + }) + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://apidocs.hollaex.com/#sending-receiving-messages + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + messageHash = 'orderbook' + ':' + market['id'] + orderbook = await self.watch_public(messageHash, params) + return orderbook.limit() + + def handle_order_book(self, client: Client, message): + # + # { + # "topic":"orderbook", + # "action":"partial", + # "symbol":"ltc-usdt", + # "data":{ + # "bids":[ + # [104.29, 5.2264], + # [103.86,1.3629], + # [101.82,0.5942] + # ], + # "asks":[ + # [104.81,9.5531], + # [105.54,0.6416], + # [106.18,1.4141], + # ], + # "timestamp":"2022-04-12T08:17:05.932Z" + # }, + # "time":1649751425 + # } + # + marketId = self.safe_string(message, 'symbol') + channel = self.safe_string(message, 'topic') + market = self.safe_market(marketId) + symbol = market['symbol'] + data = self.safe_value(message, 'data') + timestamp = self.safe_string(data, 'timestamp') + timestampMs = self.parse8601(timestamp) + snapshot = self.parse_order_book(data, symbol, timestampMs) + orderbook = None + if not (symbol in self.orderbooks): + orderbook = self.order_book(snapshot) + self.orderbooks[symbol] = orderbook + else: + orderbook = self.orderbooks[symbol] + orderbook.reset(snapshot) + messageHash = channel + ':' + marketId + client.resolve(orderbook, messageHash) + + async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://apidocs.hollaex.com/#sending-receiving-messages + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + messageHash = 'trade' + ':' + market['id'] + trades = await self.watch_public(messageHash, params) + if self.newUpdates: + limit = trades.getLimit(symbol, limit) + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + def handle_trades(self, client: Client, message): + # + # { + # "topic": "trade", + # "action": "partial", + # "symbol": "btc-usdt", + # "data": [ + # { + # "size": 0.05145, + # "price": 41977.9, + # "side": "buy", + # "timestamp": "2022-04-11T09:40:10.881Z" + # }, + # ] + # } + # + channel = self.safe_string(message, 'topic') + marketId = self.safe_string(message, 'symbol') + market = self.safe_market(marketId) + symbol = market['symbol'] + stored = self.safe_value(self.trades, symbol) + if stored is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + stored = ArrayCache(limit) + self.trades[symbol] = stored + data = self.safe_value(message, 'data', []) + parsedTrades = self.parse_trades(data, market) + for j in range(0, len(parsedTrades)): + stored.append(parsedTrades[j]) + messageHash = channel + ':' + marketId + client.resolve(stored, messageHash) + client.resolve(stored, channel) + + async def watch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + watches information on multiple trades made by the user + + https://apidocs.hollaex.com/#sending-receiving-messages + + :param str symbol: unified market symbol of the market trades were made in + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trade structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + messageHash = 'usertrade' + market = None + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + messageHash += ':' + market['id'] + trades = await self.watch_private(messageHash, params) + if self.newUpdates: + limit = trades.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(trades, symbol, since, limit, True) + + def handle_my_trades(self, client: Client, message, subscription=None): + # + # { + # "topic":"usertrade", + # "action":"insert", + # "user_id":"103", + # "symbol":"xht-usdt", + # "data":[ + # { + # "size":1, + # "side":"buy", + # "price":0.24, + # "symbol":"xht-usdt", + # "timestamp":"2022-05-13T09:30:15.014Z", + # "order_id":"6065a66e-e9a4-44a3-9726-4f8fa54b6bb6", + # "fee":0.001, + # "fee_coin":"xht", + # "is_same":true + # } + # ], + # "time":1652434215 + # } + # + channel = self.safe_string(message, 'topic') + rawTrades = self.safe_value(message, 'data') + # usually the first message is an empty array + # when the user does not have any trades yet + dataLength = len(rawTrades) + if dataLength == 0: + return + if self.myTrades is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + self.myTrades = ArrayCache(limit) + stored = self.myTrades + marketIds: dict = {} + for i in range(0, len(rawTrades)): + trade = rawTrades[i] + parsed = self.parse_trade(trade) + stored.append(parsed) + symbol = trade['symbol'] + market = self.market(symbol) + marketId = market['id'] + marketIds[marketId] = True + # non-symbol specific + client.resolve(self.myTrades, channel) + keys = list(marketIds.keys()) + for i in range(0, len(keys)): + marketId = keys[i] + messageHash = channel + ':' + marketId + client.resolve(self.myTrades, messageHash) + + async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + watches information on multiple orders made by the user + + https://apidocs.hollaex.com/#sending-receiving-messages + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + messageHash = 'order' + market = None + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + messageHash += ':' + market['id'] + orders = await self.watch_private(messageHash, params) + if self.newUpdates: + limit = orders.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(orders, symbol, since, limit, True) + + def handle_order(self, client: Client, message, subscription=None): + # + # { + # "topic": "order", + # "action": "insert", + # "user_id": 155328, + # "symbol": "ltc-usdt", + # "data": { + # "symbol": "ltc-usdt", + # "side": "buy", + # "size": 0.05, + # "type": "market", + # "price": 0, + # "fee_structure": {maker: 0.1, taker: 0.1}, + # "fee_coin": "ltc", + # "id": "ce38fd48-b336-400b-812b-60c636454231", + # "created_by": 155328, + # "filled": 0.05, + # "method": "market", + # "created_at": "2022-04-11T14:09:00.760Z", + # "updated_at": "2022-04-11T14:09:00.760Z", + # "status": "filled" + # }, + # "time": 1649686140 + # } + # + # { + # "topic":"order", + # "action":"partial", + # "user_id":155328, + # "data":[ + # { + # "created_at":"2022-05-13T08:19:07.694Z", + # "fee":0, + # "meta":{ + # + # }, + # "symbol":"ltc-usdt", + # "side":"buy", + # "size":0.1, + # "type":"limit", + # "price":55, + # "fee_structure":{ + # "maker":0.1, + # "taker":0.1 + # }, + # "fee_coin":"ltc", + # "id":"d5e77182-ad4c-4ac9-8ce4-a97f9b43e33c", + # "created_by":155328, + # "filled":0, + # "status":"new", + # "updated_at":"2022-05-13T08:19:07.694Z", + # "stop":null + # } + # ], + # "time":1652430035 + # } + # + channel = self.safe_string(message, 'topic') + data = self.safe_value(message, 'data', {}) + # usually the first message is an empty array + dataLength = len(data) + if dataLength == 0: + return + if self.orders is None: + limit = self.safe_integer(self.options, 'ordersLimit', 1000) + self.orders = ArrayCacheBySymbolById(limit) + stored = self.orders + rawOrders = None + if not isinstance(data, list): + rawOrders = [data] + else: + rawOrders = data + marketIds: dict = {} + for i in range(0, len(rawOrders)): + order = rawOrders[i] + parsed = self.parse_order(order) + stored.append(parsed) + symbol = order['symbol'] + market = self.market(symbol) + marketId = market['id'] + marketIds[marketId] = True + # non-symbol specific + client.resolve(self.orders, channel) + keys = list(marketIds.keys()) + for i in range(0, len(keys)): + marketId = keys[i] + messageHash = channel + ':' + marketId + client.resolve(self.orders, messageHash) + + async def watch_balance(self, params={}) -> Balances: + """ + watch balance and get the amount of funds available for trading or funds locked in orders + + https://apidocs.hollaex.com/#sending-receiving-messages + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + messageHash = 'wallet' + return await self.watch_private(messageHash, params) + + def handle_balance(self, client: Client, message): + # + # { + # "topic": "wallet", + # "action": "partial", + # "user_id": 155328, + # "data": { + # "eth_balance": 0, + # "eth_available": 0, + # "usdt_balance": 18.94344188, + # "usdt_available": 18.94344188, + # "ltc_balance": 0.00005, + # "ltc_available": 0.00005, + # }, + # "time": 1649687396 + # } + # + messageHash = self.safe_string(message, 'topic') + data = self.safe_value(message, 'data') + keys = list(data.keys()) + timestamp = self.safe_timestamp(message, 'time') + self.balance['info'] = data + self.balance['timestamp'] = timestamp + self.balance['datetime'] = self.iso8601(timestamp) + for i in range(0, len(keys)): + key = keys[i] + parts = key.split('_') + currencyId = self.safe_string(parts, 0) + code = self.safe_currency_code(currencyId) + account = self.balance[code] if (code in self.balance) else self.account() + second = self.safe_string(parts, 1) + freeOrTotal = 'free' if (second == 'available') else 'total' + account[freeOrTotal] = self.safe_string(data, key) + self.balance[code] = account + self.balance = self.safe_balance(self.balance) + client.resolve(self.balance, messageHash) + + async def watch_public(self, messageHash, params={}): + url = self.urls['api']['ws'] + request: dict = { + 'op': 'subscribe', + 'args': [messageHash], + } + message = self.extend(request, params) + return await self.watch(url, messageHash, message, messageHash) + + async def watch_private(self, messageHash, params={}): + self.check_required_credentials() + expires = self.safe_string(self.options, 'ws-expires') + if expires is None: + timeout = int((self.timeout / str(1000))) + expires = self.sum(self.seconds(), timeout) + expires = str(expires) + # we need to memoize these values to avoid generating a new url on each method execution + # that would trigger a new connection on each received message + self.options['ws-expires'] = expires + url = self.urls['api']['ws'] + auth = 'CONNECT' + '/stream' + expires + signature = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256) + authParams: dict = { + 'api-key': self.apiKey, + 'api-signature': signature, + 'api-expires': expires, + } + signedUrl = url + '?' + self.urlencode(authParams) + request: dict = { + 'op': 'subscribe', + 'args': [messageHash], + } + message = self.extend(request, params) + return await self.watch(signedUrl, messageHash, message, messageHash) + + def handle_error_message(self, client: Client, message) -> Bool: + # + # {error: "Bearer or HMAC authentication required"} + # {error: "Error: wrong input"} + # + error = self.safe_integer(message, 'error') + try: + if error is not None: + feedback = self.id + ' ' + self.json(message) + self.throw_exactly_matched_exception(self.exceptions['ws']['exact'], error, feedback) + except Exception as e: + if isinstance(e, AuthenticationError): + return False + return message + + def handle_message(self, client: Client, message): + # + # pong + # + # {message: "pong"} + # + # trade + # + # { + # "topic": "trade", + # "action": "partial", + # "symbol": "btc-usdt", + # "data": [ + # { + # "size": 0.05145, + # "price": 41977.9, + # "side": "buy", + # "timestamp": "2022-04-11T09:40:10.881Z" + # }, + # ] + # } + # + # orderbook + # + # { + # "topic": "orderbook", + # "action": "partial", + # "symbol": "ltc-usdt", + # "data": { + # "bids": [ + # [104.29, 5.2264], + # [103.86,1.3629], + # [101.82,0.5942] + # ], + # "asks": [ + # [104.81,9.5531], + # [105.54,0.6416], + # [106.18,1.4141], + # ], + # "timestamp": "2022-04-11T10:37:01.227Z" + # }, + # "time": 1649673421 + # } + # + # order + # + # { + # "topic": "order", + # "action": "insert", + # "user_id": 155328, + # "symbol": "ltc-usdt", + # "data": { + # "symbol": "ltc-usdt", + # "side": "buy", + # "size": 0.05, + # "type": "market", + # "price": 0, + # "fee_structure": {maker: 0.1, taker: 0.1}, + # "fee_coin": "ltc", + # "id": "ce38fd48-b336-400b-812b-60c636454231", + # "created_by": 155328, + # "filled": 0.05, + # "method": "market", + # "created_at": "2022-04-11T14:09:00.760Z", + # "updated_at": "2022-04-11T14:09:00.760Z", + # "status": "filled" + # }, + # "time": 1649686140 + # } + # + # balance + # + # { + # "topic": "wallet", + # "action": "partial", + # "user_id": 155328, + # "data": { + # "eth_balance": 0, + # "eth_available": 0, + # "usdt_balance": 18.94344188, + # "usdt_available": 18.94344188, + # "ltc_balance": 0.00005, + # "ltc_available": 0.00005, + # } + # } + # + if not self.handle_error_message(client, message): + return + content = self.safe_string(message, 'message') + if content == 'pong': + self.handle_pong(client, message) + return + methods: dict = { + 'trade': self.handle_trades, + 'orderbook': self.handle_order_book, + 'order': self.handle_order, + 'wallet': self.handle_balance, + 'usertrade': self.handle_my_trades, + } + topic = self.safe_value(message, 'topic') + method = self.safe_value(methods, topic) + if method is not None: + method(client, message) + + def ping(self, client: Client): + # hollaex does not support built-in ws protocol-level ping-pong + return {'op': 'ping'} + + def handle_pong(self, client: Client, message): + client.lastPong = self.milliseconds() + return message + + def on_error(self, client: Client, error): + self.options['ws-expires'] = None + super(hollaex, self).on_error(client, error) + + def on_close(self, client: Client, error): + self.options['ws-expires'] = None + super(hollaex, self).on_close(client, error) diff --git a/ccxt/pro/htx.py b/ccxt/pro/htx.py new file mode 100644 index 0000000..296bfa1 --- /dev/null +++ b/ccxt/pro/htx.py @@ -0,0 +1,2409 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolById, ArrayCacheBySymbolBySide, ArrayCacheByTimestamp +import hashlib +from ccxt.base.types import Any, Balances, Bool, Int, Market, Order, OrderBook, Position, Str, Strings, Ticker, Trade +from ccxt.async_support.base.ws.client import Client +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import NetworkError +from ccxt.base.errors import InvalidNonce +from ccxt.base.errors import ChecksumError + + +class htx(ccxt.async_support.htx): + + def describe(self) -> Any: + return self.deep_extend(super(htx, self).describe(), { + 'has': { + 'ws': True, + 'createOrderWs': False, + 'editOrderWs': False, + 'fetchOpenOrdersWs': False, + 'fetchOrderWs': False, + 'cancelOrderWs': False, + 'cancelOrdersWs': False, + 'cancelAllOrdersWs': False, + 'fetchTradesWs': False, + 'fetchBalanceWs': False, + 'watchOrderBook': True, + 'watchOrders': True, + 'watchTickers': False, + 'watchTicker': True, + 'watchTrades': True, + 'watchTradesForSymbols': False, + 'watchMyTrades': True, + 'watchBalance': True, + 'watchOHLCV': True, + 'unwatchTicker': True, + 'unwatchOHLCV': True, + 'unwatchTrades': True, + 'unwatchOrderBook': True, + }, + 'urls': { + 'api': { + 'ws': { + 'api': { + 'spot': { + 'public': 'wss://{hostname}/ws', + 'private': 'wss://{hostname}/ws/v2', + 'feed': 'wss://{hostname}/feed', + }, + 'future': { + 'linear': { + 'public': 'wss://api.hbdm.com/linear-swap-ws', + 'private': 'wss://api.hbdm.com/linear-swap-notification', + }, + 'inverse': { + 'public': 'wss://api.hbdm.com/ws', + 'private': 'wss://api.hbdm.com/notification', + }, + }, + 'swap': { + 'inverse': { + 'public': 'wss://api.hbdm.com/swap-ws', + 'private': 'wss://api.hbdm.com/swap-notification', + }, + 'linear': { + 'public': 'wss://api.hbdm.com/linear-swap-ws', + 'private': 'wss://api.hbdm.com/linear-swap-notification', + }, + }, + }, + # these settings work faster for clients hosted on AWS + 'api-aws': { + 'spot': { + 'public': 'wss://api-aws.huobi.pro/ws', + 'private': 'wss://api-aws.huobi.pro/ws/v2', + 'feed': 'wss://{hostname}/feed', + }, + 'future': { + 'linear': { + 'public': 'wss://api.hbdm.vn/linear-swap-ws', + 'private': 'wss://api.hbdm.vn/linear-swap-notification', + }, + 'inverse': { + 'public': 'wss://api.hbdm.vn/ws', + 'private': 'wss://api.hbdm.vn/notification', + }, + }, + 'swap': { + 'linear': { + 'public': 'wss://api.hbdm.vn/linear-swap-ws', + 'private': 'wss://api.hbdm.vn/linear-swap-notification', + }, + 'inverse': { + 'public': 'wss://api.hbdm.vn/swap-ws', + 'private': 'wss://api.hbdm.vn/swap-notification', + }, + }, + }, + }, + }, + }, + 'options': { + 'tradesLimit': 1000, + 'OHLCVLimit': 1000, + 'api': 'api', # or api-aws for clients hosted on AWS + 'watchOrderBook': { + 'maxRetries': 3, + 'checksum': True, + 'depth': 150, # 150 or 20 + }, + 'ws': { + 'gunzip': True, + }, + 'watchTicker': { + 'name': 'market.{marketId}.detail', # 'market.{marketId}.bbo' or 'market.{marketId}.ticker' + }, + }, + 'exceptions': { + 'ws': { + 'exact': { + 'bad-request': BadRequest, # { ts: 1586323747018, status: 'error', 'err-code': 'bad-request', err-msg': 'invalid mbp.150.symbol linkusdt', id: '2'} + '2002': AuthenticationError, # {action: 'sub', code: 2002, ch: 'accounts.update#2', message: 'invalid.auth.state'} + '2021': BadRequest, + '2001': BadSymbol, # {action: 'sub', code: 2001, ch: 'orders#2ltcusdt', message: 'invalid.symbol'} + '2011': BadSymbol, # {op: 'sub', cid: '1649149285', topic: 'orders_cross.ltc-usdt', 'err-code': 2011, 'err-msg': "Contract doesn't exist.", ts: 1649149287637} + '2040': BadRequest, # {op: 'sub', cid: '1649152947', 'err-code': 2040, 'err-msg': 'Missing required parameter.', ts: 1649152948684} + '4007': BadRequest, # {op: 'sub', cid: '1', topic: 'accounts_unify.USDT', 'err-code': 4007, 'err-msg': 'Non - single account user is not available, please check through the cross and isolated account asset interface', ts: 1698419318540} + }, + }, + }, + }) + + def request_id(self): + requestId = self.sum(self.safe_integer(self.options, 'requestId', 0), 1) + self.options['requestId'] = requestId + return str(requestId) + + async def watch_ticker(self, symbol: str, params={}) -> Ticker: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://www.htx.com/en-us/opend/newApiPages/?id=7ec53561-7773-11ed-9966-0242ac110003 + https://www.htx.com/en-us/opend/newApiPages/?id=28c33ab2-77ae-11ed-9966-0242ac110003 + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + options = self.safe_dict(self.options, 'watchTicker', {}) + topic = self.safe_string(options, 'name', 'market.{marketId}.detail') + if topic == 'market.{marketId}.ticker' and market['type'] != 'spot': + raise BadRequest(self.id + ' watchTicker() with name market.{marketId}.ticker is only allowed for spot markets, use market.{marketId}.detail instead') + messageHash = self.implode_params(topic, {'marketId': market['id']}) + url = self.get_url_by_market_type(market['type'], market['linear']) + return await self.subscribe_public(url, symbol, messageHash, None, params) + + async def un_watch_ticker(self, symbol: str, params={}) -> Any: + """ + unWatches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list + + https://www.htx.com/en-us/opend/newApiPages/?id=7ec53561-7773-11ed-9966-0242ac110003 + https://www.htx.com/en-us/opend/newApiPages/?id=28c33ab2-77ae-11ed-9966-0242ac110003 + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + topic = 'ticker' + options = self.safe_dict(self.options, 'watchTicker', {}) + channel = self.safe_string(options, 'name', 'market.{marketId}.detail') + if channel == 'market.{marketId}.ticker' and market['type'] != 'spot': + raise BadRequest(self.id + ' watchTicker() with name market.{marketId}.ticker is only allowed for spot markets, use market.{marketId}.detail instead') + subMessageHash = self.implode_params(channel, {'marketId': market['id']}) + return await self.unsubscribe_public(market, subMessageHash, topic, params) + + def handle_ticker(self, client: Client, message): + # + # "market.btcusdt.detail" + # { + # "ch": "market.btcusdt.detail", + # "ts": 1583494163784, + # "tick": { + # "id": 209988464418, + # "low": 8988, + # "high": 9155.41, + # "open": 9078.91, + # "close": 9136.46, + # "vol": 237813910.5928412, + # "amount": 26184.202558551195, + # "version": 209988464418, + # "count": 265673 + # } + # } + # "market.btcusdt.bbo" + # { + # "ch": "market.btcusdt.bbo", + # "ts": 1671941599613, + # "tick": { + # "seqId": 161499562790, + # "ask": 16829.51, + # "askSize": 0.707776, + # "bid": 16829.5, + # "bidSize": 1.685945, + # "quoteTime": 1671941599612, + # "symbol": "btcusdt" + # } + # } + # + tick = self.safe_value(message, 'tick', {}) + ch = self.safe_string(message, 'ch') + parts = ch.split('.') + marketId = self.safe_string(parts, 1) + market = self.safe_market(marketId) + ticker = self.parse_ticker(tick, market) + timestamp = self.safe_value(message, 'ts') + ticker['timestamp'] = timestamp + ticker['datetime'] = self.iso8601(timestamp) + symbol = ticker['symbol'] + self.tickers[symbol] = ticker + client.resolve(ticker, ch) + return message + + async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://www.htx.com/en-us/opend/newApiPages/?id=7ec53b69-7773-11ed-9966-0242ac110003 + https://www.htx.com/en-us/opend/newApiPages/?id=28c33c21-77ae-11ed-9966-0242ac110003 + https://www.htx.com/en-us/opend/newApiPages/?id=28c33cfe-77ae-11ed-9966-0242ac110003 + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + messageHash = 'market.' + market['id'] + '.trade.detail' + url = self.get_url_by_market_type(market['type'], market['linear']) + trades = await self.subscribe_public(url, symbol, messageHash, None, params) + if self.newUpdates: + limit = trades.getLimit(symbol, limit) + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + async def un_watch_trades(self, symbol: str, params={}) -> Any: + """ + unWatches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list + + https://www.htx.com/en-us/opend/newApiPages/?id=7ec53b69-7773-11ed-9966-0242ac110003 + https://www.htx.com/en-us/opend/newApiPages/?id=28c33c21-77ae-11ed-9966-0242ac110003 + https://www.htx.com/en-us/opend/newApiPages/?id=28c33cfe-77ae-11ed-9966-0242ac110003 + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + topic = 'trades' + options = self.safe_dict(self.options, 'watchTrades', {}) + channel = self.safe_string(options, 'name', 'market.{marketId}.trade.detail') + subMessageHash = self.implode_params(channel, {'marketId': market['id']}) + return await self.unsubscribe_public(market, subMessageHash, topic, params) + + def handle_trades(self, client: Client, message): + # + # { + # "ch": "market.btcusdt.trade.detail", + # "ts": 1583495834011, + # "tick": { + # "id": 105004645372, + # "ts": 1583495833751, + # "data": [ + # { + # "id": 1.050046453727319e+22, + # "ts": 1583495833751, + # "tradeId": 102090727790, + # "amount": 0.003893, + # "price": 9150.01, + # "direction": "sell" + # } + # ] + # } + # } + # + tick = self.safe_value(message, 'tick', {}) + data = self.safe_value(tick, 'data', {}) + ch = self.safe_string(message, 'ch') + parts = ch.split('.') + marketId = self.safe_string(parts, 1) + market = self.safe_market(marketId) + symbol = market['symbol'] + tradesCache = self.safe_value(self.trades, symbol) + if tradesCache is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + tradesCache = ArrayCache(limit) + self.trades[symbol] = tradesCache + for i in range(0, len(data)): + trade = self.parse_trade(data[i], market) + tradesCache.append(trade) + client.resolve(tradesCache, ch) + return message + + async def watch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + watches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://www.htx.com/en-us/opend/newApiPages/?id=7ec53241-7773-11ed-9966-0242ac110003 + https://www.htx.com/en-us/opend/newApiPages/?id=28c3346a-77ae-11ed-9966-0242ac110003 + https://www.htx.com/en-us/opend/newApiPages/?id=28c33563-77ae-11ed-9966-0242ac110003 + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + interval = self.safe_string(self.timeframes, timeframe, timeframe) + messageHash = 'market.' + market['id'] + '.kline.' + interval + url = self.get_url_by_market_type(market['type'], market['linear']) + ohlcv = await self.subscribe_public(url, symbol, messageHash, None, params) + if self.newUpdates: + limit = ohlcv.getLimit(symbol, limit) + return self.filter_by_since_limit(ohlcv, since, limit, 0, True) + + async def un_watch_ohlcv(self, symbol: str, timeframe: str = '1m', params={}) -> Any: + """ + unWatches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://www.htx.com/en-us/opend/newApiPages/?id=7ec53241-7773-11ed-9966-0242ac110003 + https://www.htx.com/en-us/opend/newApiPages/?id=28c3346a-77ae-11ed-9966-0242ac110003 + https://www.htx.com/en-us/opend/newApiPages/?id=28c33563-77ae-11ed-9966-0242ac110003 + + :param str symbol: unified symbol of the market + :param str timeframe: the length of time each candle represents + :param dict [params]: extra parameters specific to the exchange API endpoint + :param dict [params.timezone]: if provided, kline intervals are interpreted in that timezone instead of UTC, example '+08:00' + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + interval = self.safe_string(self.timeframes, timeframe, timeframe) + subMessageHash = 'market.' + market['id'] + '.kline.' + interval + topic = 'ohlcv' + params['symbolsAndTimeframes'] = [[market['symbol'], timeframe]] + return await self.unsubscribe_public(market, subMessageHash, topic, params) + + def handle_ohlcv(self, client: Client, message): + # + # { + # "ch": "market.btcusdt.kline.1min", + # "ts": 1583501786794, + # "tick": { + # "id": 1583501760, + # "open": 9094.5, + # "close": 9094.51, + # "low": 9094.5, + # "high": 9094.51, + # "amount": 0.44639786263800907, + # "vol": 4059.76919054, + # "count": 16 + # } + # } + # + ch = self.safe_string(message, 'ch') + parts = ch.split('.') + marketId = self.safe_string(parts, 1) + market = self.safe_market(marketId) + symbol = market['symbol'] + interval = self.safe_string(parts, 3) + timeframe = self.find_timeframe(interval) + self.ohlcvs[symbol] = self.safe_value(self.ohlcvs, symbol, {}) + stored = self.safe_value(self.ohlcvs[symbol], timeframe) + if stored is None: + limit = self.safe_integer(self.options, 'OHLCVLimit', 1000) + stored = ArrayCacheByTimestamp(limit) + self.ohlcvs[symbol][timeframe] = stored + tick = self.safe_value(message, 'tick') + parsed = self.parse_ohlcv(tick, market) + stored.append(parsed) + client.resolve(stored, ch) + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + + https://huobiapi.github.io/docs/dm/v1/en/#subscribe-market-depth-data + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#subscribe-incremental-market-depth-data + https://huobiapi.github.io/docs/usdt_swap/v1/en/#general-subscribe-incremental-market-depth-data + + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + allowedLimits = [5, 20, 150, 400] + # 2) 5-level/20-level incremental MBP is a tick by tick feed, + # which means whenever there is an order book change at that level, it pushes an update + # 150-levels/400-level incremental MBP feed is based on the gap + # between two snapshots at 100ms interval. + options = self.safe_dict(self.options, 'watchOrderBook', {}) + if limit is None: + limit = self.safe_integer(options, 'depth', 150) + if not self.in_array(limit, allowedLimits): + raise ExchangeError(self.id + ' watchOrderBook market accepts limits of 5, 20, 150 or 400 only') + messageHash = None + if market['spot']: + messageHash = 'market.' + market['id'] + '.mbp.' + self.number_to_string(limit) + else: + messageHash = 'market.' + market['id'] + '.depth.size_' + self.number_to_string(limit) + '.high_freq' + url = self.get_url_by_market_type(market['type'], market['linear'], False, True) + method = self.handle_order_book_subscription + if not market['spot']: + params = self.extend(params) + params['data_type'] = 'incremental' + method = None + orderbook = await self.subscribe_public(url, symbol, messageHash, method, params) + return orderbook.limit() + + async def un_watch_order_book(self, symbol: str, params={}) -> Any: + """ + unsubscribe from the orderbook channel + + https://huobiapi.github.io/docs/dm/v1/en/#subscribe-market-depth-data + https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#subscribe-incremental-market-depth-data + https://huobiapi.github.io/docs/usdt_swap/v1/en/#general-subscribe-incremental-market-depth-data + + :param str symbol: unified symbol of the market to fetch the order book for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.limit]: orderbook limit, default is None + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + topic = 'orderbook' + options = self.safe_dict(self.options, 'watchOrderBook', {}) + depth = self.safe_integer(options, 'depth', 150) + subMessageHash = None + if market['spot']: + subMessageHash = 'market.' + market['id'] + '.mbp.' + self.number_to_string(depth) + else: + subMessageHash = 'market.' + market['id'] + '.depth.size_' + self.number_to_string(depth) + '.high_freq' + if not (market['spot']): + params['data_type'] = 'incremental' + return await self.unsubscribe_public(market, subMessageHash, topic, params) + + def handle_order_book_snapshot(self, client: Client, message, subscription): + # + # { + # "id": 1583473663565, + # "rep": "market.btcusdt.mbp.150", + # "status": "ok", + # "ts": 1698359289261, + # "data": { + # "seqNum": 104999417756, + # "bids": [ + # [9058.27, 0], + # [9058.43, 0], + # [9058.99, 0], + # ], + # "asks": [ + # [9084.27, 0.2], + # [9085.69, 0], + # [9085.81, 0], + # ] + # } + # } + # + symbol = self.safe_string(subscription, 'symbol') + messageHash = self.safe_string(subscription, 'messageHash') + id = self.safe_string(message, 'id') + lastTimestamp = self.safe_integer(subscription, 'lastTimestamp') + try: + orderbook = self.orderbooks[symbol] + data = self.safe_value(message, 'data') + messages = orderbook.cache + firstMessage = self.safe_value(messages, 0, {}) + snapshot = self.parse_order_book(data, symbol) + tick = self.safe_value(firstMessage, 'tick') + sequence = self.safe_integer(tick, 'prevSeqNum') + nonce = self.safe_integer(data, 'seqNum') + snapshot['nonce'] = nonce + snapshotTimestamp = self.safe_integer(message, 'ts') + subscription['lastTimestamp'] = snapshotTimestamp + snapshotLimit = self.safe_integer(subscription, 'limit') + snapshotOrderBook = self.order_book(snapshot, snapshotLimit) + client.resolve(snapshotOrderBook, id) + if (sequence is None) or (nonce < sequence): + maxAttempts = self.handle_option('watchOrderBook', 'maxRetries', 3) + numAttempts = self.safe_integer(subscription, 'numAttempts', 0) + # retry to synchronize if we have not reached maxAttempts yet + if numAttempts < maxAttempts: + # safety guard + if messageHash in client.subscriptions: + numAttempts = self.sum(numAttempts, 1) + delayTime = self.sum(1000, lastTimestamp - snapshotTimestamp) + subscription['numAttempts'] = numAttempts + client.subscriptions[messageHash] = subscription + self.delay(delayTime, self.watch_order_book_snapshot, client, message, subscription) + else: + # raise upon failing to synchronize in maxAttempts + raise InvalidNonce(self.id + ' failed to synchronize WebSocket feed with the snapshot for symbol ' + symbol + ' in ' + str(maxAttempts) + ' attempts') + else: + orderbook.reset(snapshot) + # unroll the accumulated deltas + for i in range(0, len(messages)): + self.handle_order_book_message(client, messages[i]) + orderbook.cache = [] + self.orderbooks[symbol] = orderbook + client.resolve(orderbook, messageHash) + except Exception as e: + del client.subscriptions[messageHash] + del self.orderbooks[symbol] + client.reject(e, messageHash) + + async def watch_order_book_snapshot(self, client, message, subscription): + messageHash = self.safe_string(subscription, 'messageHash') + symbol = self.safe_string(subscription, 'symbol') + limit = self.safe_integer(subscription, 'limit') + timestamp = self.safe_integer(message, 'ts') + params = self.safe_value(subscription, 'params') + attempts = self.safe_integer(subscription, 'numAttempts', 0) + market = self.market(symbol) + url = self.get_url_by_market_type(market['type'], market['linear'], False, True) + requestId = self.request_id() + request: dict = { + 'req': messageHash, + 'id': requestId, + } + # self is a temporary subscription by a specific requestId + # it has a very short lifetime until the snapshot is received over ws + snapshotSubscription: dict = { + 'id': requestId, + 'messageHash': messageHash, + 'symbol': symbol, + 'limit': limit, + 'params': params, + 'numAttempts': attempts, + 'lastTimestamp': timestamp, + 'method': self.handle_order_book_snapshot, + } + try: + orderbook = await self.watch(url, requestId, request, requestId, snapshotSubscription) + return orderbook.limit() + except Exception as e: + del client.subscriptions[messageHash] + client.reject(e, messageHash) + return None + + def handle_delta(self, bookside, delta): + price = self.safe_float(delta, 0) + amount = self.safe_float(delta, 1) + bookside.store(price, amount) + + def handle_deltas(self, bookside, deltas): + for i in range(0, len(deltas)): + self.handle_delta(bookside, deltas[i]) + + def handle_order_book_message(self, client: Client, message): + # spot markets + # + # { + # "ch": "market.btcusdt.mbp.150", + # "ts": 1583472025885, + # "tick": { + # "seqNum": 104998984994, + # "prevSeqNum": 104998984977, + # "bids": [ + # [9058.27, 0], + # [9058.43, 0], + # [9058.99, 0], + # ], + # "asks": [ + # [9084.27, 0.2], + # [9085.69, 0], + # [9085.81, 0], + # ] + # } + # } + # + # non-spot market update + # + # { + # "ch":"market.BTC220218.depth.size_150.high_freq", + # "tick":{ + # "asks":[], + # "bids":[ + # [43445.74,1], + # [43444.48,0], + # [40593.92,9] + # ], + # "ch":"market.BTC220218.depth.size_150.high_freq", + # "event":"update", + # "id":152727500274, + # "mrid":152727500274, + # "ts":1645023376098, + # "version":37536690 + # }, + # "ts":1645023376098 + # } + # non-spot market snapshot + # + # { + # "ch":"market.BTC220218.depth.size_150.high_freq", + # "tick":{ + # "asks":[ + # [43445.74,1], + # [43444.48,0], + # [40593.92,9] + # ], + # "bids":[ + # [43445.74,1], + # [43444.48,0], + # [40593.92,9] + # ], + # "ch":"market.BTC220218.depth.size_150.high_freq", + # "event":"snapshot", + # "id":152727500274, + # "mrid":152727500274, + # "ts":1645023376098, + # "version":37536690 + # }, + # "ts":1645023376098 + # } + # + ch = self.safe_value(message, 'ch') + parts = ch.split('.') + marketId = self.safe_string(parts, 1) + market = self.safe_market(marketId) + symbol = market['symbol'] + orderbook = self.orderbooks[symbol] + tick = self.safe_value(message, 'tick', {}) + seqNum = self.safe_integer(tick, 'seqNum') + prevSeqNum = self.safe_integer(tick, 'prevSeqNum') + event = self.safe_string(tick, 'event') + version = self.safe_integer(tick, 'version') + timestamp = self.safe_integer(message, 'ts') + if event == 'snapshot': + snapshot = self.parse_order_book(tick, symbol, timestamp) + orderbook.reset(snapshot) + orderbook['nonce'] = version + if (prevSeqNum is not None) and prevSeqNum > orderbook['nonce']: + checksum = self.handle_option('watchOrderBook', 'checksum', True) + if checksum: + raise ChecksumError(self.id + ' ' + self.orderbook_checksum_message(symbol)) + spotConditon = market['spot'] and (prevSeqNum == orderbook['nonce']) + nonSpotCondition = market['contract'] and (version - 1 == orderbook['nonce']) + if spotConditon or nonSpotCondition: + asks = self.safe_value(tick, 'asks', []) + bids = self.safe_value(tick, 'bids', []) + self.handle_deltas(orderbook['asks'], asks) + self.handle_deltas(orderbook['bids'], bids) + orderbook['nonce'] = seqNum if spotConditon else version + orderbook['timestamp'] = timestamp + orderbook['datetime'] = self.iso8601(timestamp) + + def handle_order_book(self, client: Client, message): + # + # deltas + # + # spot markets + # + # { + # "ch": "market.btcusdt.mbp.150", + # "ts": 1583472025885, + # "tick": { + # "seqNum": 104998984994, + # "prevSeqNum": 104998984977, + # "bids": [ + # [9058.27, 0], + # [9058.43, 0], + # [9058.99, 0], + # ], + # "asks": [ + # [9084.27, 0.2], + # [9085.69, 0], + # [9085.81, 0], + # ] + # } + # } + # + # non spot markets + # + # { + # "ch":"market.BTC220218.depth.size_150.high_freq", + # "tick":{ + # "asks":[], + # "bids":[ + # [43445.74,1], + # [43444.48,0], + # [40593.92,9] + # ], + # "ch":"market.BTC220218.depth.size_150.high_freq", + # "event":"update", + # "id":152727500274, + # "mrid":152727500274, + # "ts":1645023376098, + # "version":37536690 + # }, + # "ts":1645023376098 + # } + # + messageHash = self.safe_string(message, 'ch') + tick = self.safe_dict(message, 'tick') + event = self.safe_string(tick, 'event') + ch = self.safe_string(message, 'ch') + parts = ch.split('.') + marketId = self.safe_string(parts, 1) + symbol = self.safe_symbol(marketId) + if not (symbol in self.orderbooks): + size = self.safe_string(parts, 3) + sizeParts = size.split('_') + limit = self.safe_integer(sizeParts, 1) + self.orderbooks[symbol] = self.order_book({}, limit) + orderbook = self.orderbooks[symbol] + if (event is None) and (orderbook['nonce'] is None): + orderbook.cache.append(message) + else: + self.handle_order_book_message(client, message) + client.resolve(orderbook, messageHash) + + def handle_order_book_subscription(self, client: Client, message, subscription): + symbol = self.safe_string(subscription, 'symbol') + market = self.market(symbol) + limit = self.safe_integer(subscription, 'limit') + self.orderbooks[symbol] = self.order_book({}, limit) + if market['spot']: + self.spawn(self.watch_order_book_snapshot, client, message, subscription) + + async def watch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + watches information on multiple trades made by the user + + https://www.htx.com/en-us/opend/newApiPages/?id=7ec53dd5-7773-11ed-9966-0242ac110003 + + :param str symbol: unified market symbol of the market trades were made in + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trade structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + self.check_required_credentials() + await self.load_markets() + type = None + marketId = '*' # wildcard + market = None + messageHash = None + channel = None + trades = None + subType = None + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + type = market['type'] + subType = 'linear' if market['linear'] else 'inverse' + marketId = market['lowercaseId'] + else: + type = self.safe_string(self.options, 'defaultType', 'spot') + type = self.safe_string(params, 'type', type) + subType = self.safe_string_2(self.options, 'subType', 'defaultSubType', 'linear') + subType = self.safe_string(params, 'subType', subType) + params = self.omit(params, ['type', 'subType']) + if type == 'spot': + mode = None + if mode is None: + mode = self.safe_string_2(self.options, 'watchMyTrades', 'mode', '0') + mode = self.safe_string(params, 'mode', mode) + params = self.omit(params, 'mode') + messageHash = 'trade.clearing' + '#' + marketId + '#' + mode + channel = messageHash + else: + channelAndMessageHash = self.get_order_channel_and_message_hash(type, subType, market, params) + channel = self.safe_string(channelAndMessageHash, 0) + orderMessageHash = self.safe_string(channelAndMessageHash, 1) + # we will take advantage of the order messageHash because already handles stuff + # like symbol/margin/subtype/type variations + messageHash = orderMessageHash + ':' + 'trade' + trades = await self.subscribe_private(channel, messageHash, type, subType, params) + if self.newUpdates: + limit = trades.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(trades, symbol, since, limit, True) + + def get_order_channel_and_message_hash(self, type, subType, market=None, params={}): + messageHash = None + channel = None + orderType = self.safe_string(self.options, 'orderType', 'orders') # orders or matchOrders + orderType = self.safe_string(params, 'orderType', orderType) + params = self.omit(params, 'orderType') + marketCode = market['lowercaseId'].lower() if (market is not None) else None + baseId = market['baseId'] if (market is not None) else None + prefix = orderType + messageHash = prefix + if subType == 'linear': + # USDT Margined Contracts Example: LTC/USDT:USDT + marginMode = self.safe_string(params, 'margin', 'cross') + marginPrefix = prefix + '_cross' if (marginMode == 'cross') else prefix + messageHash = marginPrefix + if marketCode is not None: + messageHash += '.' + marketCode + channel = messageHash + else: + channel = marginPrefix + '.' + '*' + elif type == 'future': + # inverse futures Example: BCH/USD:BCH-220408 + if baseId is not None: + channel = prefix + '.' + baseId.lower() + messageHash = channel + else: + channel = prefix + '.' + '*' + else: + # inverse swaps: Example: BTC/USD:BTC + if marketCode is not None: + channel = prefix + '.' + marketCode + messageHash = channel + else: + channel = prefix + '.' + '*' + return [channel, messageHash] + + async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + watches information on multiple orders made by the user + + https://www.htx.com/en-us/opend/newApiPages/?id=7ec53c8f-7773-11ed-9966-0242ac110003 + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + type = None + subType = None + market = None + suffix = '*' # wildcard + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + type = market['type'] + suffix = market['lowercaseId'] + subType = 'linear' if market['linear'] else 'inverse' + else: + type = self.safe_string(self.options, 'defaultType', 'spot') + type = self.safe_string(params, 'type', type) + subType = self.safe_string_2(self.options, 'subType', 'defaultSubType', 'linear') + subType = self.safe_string(params, 'subType', subType) + params = self.omit(params, ['type', 'subType']) + messageHash = None + channel = None + if type == 'spot': + messageHash = 'orders' + '#' + suffix + channel = messageHash + else: + channelAndMessageHash = self.get_order_channel_and_message_hash(type, subType, market, params) + channel = self.safe_string(channelAndMessageHash, 0) + messageHash = self.safe_string(channelAndMessageHash, 1) + orders = await self.subscribe_private(channel, messageHash, type, subType, params) + if self.newUpdates: + limit = orders.getLimit(symbol, limit) + return self.filter_by_since_limit(orders, since, limit, 'timestamp', True) + + def handle_order(self, client: Client, message): + # + # spot + # + # for new order creation + # + # { + # "action":"push", + # "ch":"orders#btcusdt", # or "orders#*" for global subscriptions + # "data": { + # "orderStatus": "submitted", + # "eventType": "creation", + # "totalTradeAmount": 0 # for "submitted" order status + # "orderCreateTime": 1645116048355, # only when `submitted` status + # "orderSource": "spot-web", + # "accountId": 44234548, + # "orderPrice": "100", + # "orderSize": "0.05", + # "symbol": "ethusdt", + # "type": "buy-limit", + # "orderId": "478861479986886", + # "clientOrderId": '', + # } + # } + # + # for filled order, additional fields are present: + # + # "orderStatus": "filled", + # "eventType": "trade", + # "totalTradeAmount": "5.9892649859", + # "tradePrice": "0.676669", + # "tradeVolume": "8.8511", + # "tradeTime": 1760427775894, + # "aggressor": False, + # "execAmt": "8.8511", + # "tradeId": 100599712781, + # "remainAmt": "0", + # + # spot wrapped trade + # + # { + # "action": "push", + # "ch": "orders#ltcusdt", + # "data": { + # "tradePrice": "130.01", + # "tradeVolume": "0.0385", + # "tradeTime": 1648714741525, + # "aggressor": True, + # "execAmt": "0.0385", + # "orderSource": "spot-web", + # "orderSize": "0.0385", + # "remainAmt": "0", + # "tradeId": 101541578884, + # "symbol": "ltcusdt", + # "type": "sell-market", + # "eventType": "trade", + # "clientOrderId": '', + # "orderStatus": "filled", + # "orderId": 509835753860328 + # } + # } + # + # non spot order + # + # { + # "contract_type": "swap", + # "pair": "LTC-USDT", + # "business_type": "swap", + # "op": "notify", + # "topic": "orders_cross.ltc-usdt", + # "ts": 1650354508696, + # "symbol": "LTC", + # "contract_code": "LTC-USDT", + # "volume": 1, + # "price": 110.34, + # "order_price_type": "lightning", + # "direction": "sell", + # "offset": "close", + # "status": 6, + # "lever_rate": 1, + # "order_id": "966002354015051776", + # "order_id_str": "966002354015051776", + # "client_order_id": null, + # "order_source": "web", + # "order_type": 1, + # "created_at": 1650354508649, + # "trade_volume": 1, + # "trade_turnover": 11.072, + # "fee": -0.005536, + # "trade_avg_price": 110.72, + # "margin_frozen": 0, + # "profit": -0.045, + # "trade": [ + # { + # "trade_fee": -0.005536, + # "fee_asset": "USDT", + # "real_profit": 0.473, + # "profit": -0.045, + # "trade_id": 86678766507, + # "id": "86678766507-966002354015051776-1", + # "trade_volume": 1, + # "trade_price": 110.72, + # "trade_turnover": 11.072, + # "created_at": 1650354508656, + # "role": "taker" + # } + # ], + # "canceled_at": 0, + # "fee_asset": "USDT", + # "margin_asset": "USDT", + # "uid": "359305390", + # "liquidation_type": "0", + # "margin_mode": "cross", + # "margin_account": "USDT", + # "is_tpsl": 0, + # "real_profit": 0.473, + # "trade_partition": "USDT", + # "reduce_only": 1 + # } + # + # + messageHash = self.safe_string_2(message, 'ch', 'topic') + data = self.safe_value(message, 'data') + marketId = self.safe_string(message, 'contract_code') + if marketId is None: + marketId = self.safe_string(data, 'symbol') + market = self.safe_market(marketId) + parsedOrder = None + if data is not None: + # spot updates + eventType = self.safe_string(data, 'eventType') + if eventType == 'trade': + # when a spot order is filled we get an update message + # with the trade info + parsedTrade = self.parse_order_trade(data, market) + # inject trade in existing order by faking an order object + orderId = self.safe_string(parsedTrade, 'order') + trades = [parsedTrade] + status = self.parse_order_status(self.safe_string_2(data, 'orderStatus', 'status', 'closed')) + filled = self.safe_string(data, 'execAmt') + remaining = self.safe_string(data, 'remainAmt') + order: dict = { + 'id': orderId, + 'trades': trades, + 'status': status, + 'symbol': market['symbol'], + 'filled': self.parse_number(filled), + 'remaining': self.parse_number(remaining), + 'price': self.safe_number(data, 'orderPrice'), + 'amount': self.safe_number(data, 'orderSize'), + 'info': data, + } + parsedOrder = order + else: + parsedOrder = self.parse_ws_order(data, market) + else: + # contract branch + parsedOrder = self.parse_ws_order(message, market) + rawTrades = self.safe_value(message, 'trade', []) + tradesLength = len(rawTrades) + if tradesLength > 0: + tradesObject: dict = { + 'trades': rawTrades, + 'ch': messageHash, + 'symbol': marketId, + } + # inject order params in every trade + extendTradeParams: dict = { + 'order': self.safe_string(parsedOrder, 'id'), + 'type': self.safe_string(parsedOrder, 'type'), + 'side': self.safe_string(parsedOrder, 'side'), + } + # trades arrive inside an order update + # we're forwarding them to handleMyTrade + # so they can be properly resolved + self.handle_my_trade(client, tradesObject, extendTradeParams) + if self.orders is None: + limit = self.safe_integer(self.options, 'ordersLimit', 1000) + self.orders = ArrayCacheBySymbolById(limit) + cachedOrders = self.orders + cachedOrders.append(parsedOrder) + client.resolve(self.orders, messageHash) + # when we make a global subscription(for contracts only) our message hash can't have a symbol/currency attached + # so we're removing it here + genericMessageHash = messageHash.replace('.' + market['lowercaseId'], '') + lowerCaseBaseId = self.safe_string_lower(market, 'baseId') + genericMessageHash = genericMessageHash.replace('.' + lowerCaseBaseId, '') + client.resolve(self.orders, genericMessageHash) + + def parse_ws_order(self, order, market=None): + # + # spot + # + # { + # "orderSource": "spot-web", + # "orderCreateTime": 1645116048355, # creating only + # "accountId": 44234548, + # "orderPrice": "100", + # "orderSize": "0.05", + # "orderValue": "3.71676361", # market-buy only + # "symbol": "ethusdt", + # "type": "buy-limit", + # "orderId": "478861479986886", + # "eventType": "creation", + # "clientOrderId": '', + # "orderStatus": "submitted" + # "lastActTime":1645118621810 # except creating + # "execAmt":"0" + # } + # + # swap order + # + # { + # "contract_type": "swap", + # "pair": "LTC-USDT", + # "business_type": "swap", + # "op": "notify", + # "topic": "orders_cross.ltc-usdt", + # "ts": 1648717911384, + # "symbol": "LTC", + # "contract_code": "LTC-USDT", + # "volume": 1, + # "price": 129.13, + # "order_price_type": "lightning", + # "direction": "sell", + # "offset": "close", + # "status": 6, + # "lever_rate": 5, + # "order_id": "959137967397068800", + # "order_id_str": "959137967397068800", + # "client_order_id": null, + # "order_source": "web", + # "order_type": 1, + # "created_at": 1648717911344, + # "trade_volume": 1, + # "trade_turnover": 12.952, + # "fee": -0.006476, + # "trade_avg_price": 129.52, + # "margin_frozen": 0, + # "profit": -0.005, + # "trade": [ + # { + # "trade_fee": -0.006476, + # "fee_asset": "USDT", + # "real_profit": -0.005, + # "profit": -0.005, + # "trade_id": 83619995370, + # "id": "83619995370-959137967397068800-1", + # "trade_volume": 1, + # "trade_price": 129.52, + # "trade_turnover": 12.952, + # "created_at": 1648717911352, + # "role": "taker" + # } + # ], + # "canceled_at": 0, + # "fee_asset": "USDT", + # "margin_asset": "USDT", + # "uid": "359305390", + # "liquidation_type": "0", + # "margin_mode": "cross", + # "margin_account": "USDT", + # "is_tpsl": 0, + # "real_profit": -0.005, + # "trade_partition": "USDT", + # "reduce_only": 1 + # } + # + # { + # "op":"notify", + # "topic":"orders.ada", + # "ts":1604388667226, + # "symbol":"ADA", + # "contract_type":"quarter", + # "contract_code":"ADA201225", + # "volume":1, + # "price":0.0905, + # "order_price_type":"post_only", + # "direction":"sell", + # "offset":"open", + # "status":6, + # "lever_rate":20, + # "order_id":773207641127878656, + # "order_id_str":"773207641127878656", + # "client_order_id":null, + # "order_source":"web", + # "order_type":1, + # "created_at":1604388667146, + # "trade_volume":1, + # "trade_turnover":10, + # "fee":-0.022099447513812154, + # "trade_avg_price":0.0905, + # "margin_frozen":0, + # "profit":0, + # "trade":[], + # "canceled_at":0, + # "fee_asset":"ADA", + # "uid":"123456789", + # "liquidation_type":"0", + # "is_tpsl": 0, + # "real_profit": 0 + # } + # + lastTradeTimestamp = self.safe_integer_2(order, 'lastActTime', 'ts') + created = self.safe_integer(order, 'orderCreateTime') + marketId = self.safe_string_2(order, 'contract_code', 'symbol') + market = self.safe_market(marketId, market) + symbol = self.safe_symbol(marketId, market) + amount = self.safe_string_2(order, 'orderSize', 'volume') + status = self.parse_order_status(self.safe_string_2(order, 'orderStatus', 'status')) + id = self.safe_string_2(order, 'orderId', 'order_id') + clientOrderId = self.safe_string_2(order, 'clientOrderId', 'client_order_id') + price = self.safe_string_2(order, 'orderPrice', 'price') + filled = self.safe_string(order, 'execAmt') + typeSide = self.safe_string(order, 'type') + feeCost = self.safe_string(order, 'fee') + fee = None + if feeCost is not None: + feeCurrencyId = self.safe_string(order, 'fee_asset') + fee = { + 'cost': feeCost, + 'currency': self.safe_currency_code(feeCurrencyId), + } + avgPrice = self.safe_string(order, 'trade_avg_price') + rawTrades = self.safe_value(order, 'trade') + typeSideParts = [] + if typeSide is not None: + typeSideParts = typeSide.split('-') + type = self.safe_string_lower(typeSideParts, 1) + if type is None: + type = self.safe_string(order, 'order_price_type') + side = self.safe_string_lower(typeSideParts, 0) + if side is None: + side = self.safe_string(order, 'direction') + cost = self.safe_string(order, 'orderValue') + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': clientOrderId, + 'timestamp': created, + 'datetime': self.iso8601(created), + 'lastTradeTimestamp': lastTradeTimestamp, + 'status': status, + 'symbol': symbol, + 'type': type, + 'timeInForce': None, + 'postOnly': None, + 'side': side, + 'price': price, + 'amount': amount, + 'filled': filled, + 'remaining': None, + 'cost': cost, + 'fee': fee, + 'average': avgPrice, + 'trades': rawTrades, + }, market) + + def parse_order_trade(self, trade, market=None): + # spot private wrapped trade + # + # { + # "tradePrice": "130.01", + # "tradeVolume": "0.0385", + # "tradeTime": 1648714741525, + # "aggressor": True, + # "execAmt": "0.0385", + # "orderSource": "spot-web", + # "orderSize": "0.0385", + # "remainAmt": "0", + # "tradeId": 101541578884, + # "symbol": "ltcusdt", + # "type": "sell-market", + # "eventType": "trade", + # "clientOrderId": '', + # "orderStatus": "filled", + # "orderId": 509835753860328 + # } + # + market = self.safe_market(None, market) + symbol = market['symbol'] + tradeId = self.safe_string(trade, 'tradeId') + price = self.safe_string(trade, 'tradePrice') + amount = self.safe_string(trade, 'tradeVolume') + order = self.safe_string(trade, 'orderId') + timestamp = self.safe_integer(trade, 'tradeTime') + type = self.safe_string(trade, 'type') + side = None + if type is not None: + typeParts = type.split('-') + side = typeParts[0] + type = typeParts[1] + aggressor = self.safe_value(trade, 'aggressor') + takerOrMaker = None + if aggressor is not None: + takerOrMaker = 'taker' if aggressor else 'maker' + return self.safe_trade({ + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'id': tradeId, + 'order': order, + 'type': type, + 'takerOrMaker': takerOrMaker, + 'side': side, + 'price': price, + 'amount': amount, + 'cost': None, + 'fee': None, + }, market) + + async def watch_positions(self, symbols: Strings = None, since: Int = None, limit: Int = None, params={}) -> List[Position]: + """ + + https://www.huobi.com/en-in/opend/newApiPages/?id=8cb7de1c-77b5-11ed-9966-0242ac110003 + https://www.huobi.com/en-in/opend/newApiPages/?id=8cb7df0f-77b5-11ed-9966-0242ac110003 + https://www.huobi.com/en-in/opend/newApiPages/?id=28c34a7d-77ae-11ed-9966-0242ac110003 + https://www.huobi.com/en-in/opend/newApiPages/?id=5d5156b5-77b6-11ed-9966-0242ac110003 + + watch all open positions. Note: huobi has one channel for each marginMode and type + :param str[]|None symbols: list of unified market symbols + @param since + @param limit + :param dict params: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + await self.load_markets() + market = None + messageHash = '' + if not self.is_empty(symbols): + market = self.get_market_from_symbols(symbols) + messageHash = '::' + ','.join(symbols) + type = None + subType = None + if market is not None: + type = market['type'] + subType = 'linear' if market['linear'] else 'inverse' + else: + type, params = self.handle_market_type_and_params('watchPositions', market, params) + if type == 'spot': + type = 'future' + subType, params = self.handle_option_and_params(params, 'watchPositions', 'subType', subType) + symbols = self.market_symbols(symbols) + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('watchPositions', params, 'cross') + isLinear = (subType == 'linear') + url = self.get_url_by_market_type(type, isLinear, True) + messageHash = marginMode + ':positions' + messageHash + channel = 'positions_cross.*' if (marginMode == 'cross') else 'positions.*' + newPositions = await self.subscribe_private(channel, messageHash, type, subType, params) + if self.newUpdates: + return newPositions + return self.filter_by_symbols_since_limit(self.positions[url][marginMode], symbols, since, limit, False) + + def handle_positions(self, client, message): + # + # { + # op: 'notify', + # topic: 'positions_cross', + # ts: 1696767149650, + # event: 'snapshot', + # data: [ + # { + # contract_type: 'swap', + # pair: 'BTC-USDT', + # business_type: 'swap', + # liquidation_price: null, + # symbol: 'BTC', + # contract_code: 'BTC-USDT', + # volume: 1, + # available: 1, + # frozen: 0, + # cost_open: 27802.2, + # cost_hold: 27802.2, + # profit_unreal: 0.0175, + # profit_rate: 0.000629446590557581, + # profit: 0.0175, + # margin_asset: 'USDT', + # position_margin: 27.8197, + # lever_rate: 1, + # direction: 'buy', + # last_price: 27819.7, + # margin_mode: 'cross', + # margin_account: 'USDT', + # trade_partition: 'USDT', + # position_mode: 'dual_side' + # }, + # ] + # } + # + url = client.url + topic = self.safe_string(message, 'topic', '') + marginMode = 'cross' if (topic == 'positions_cross') else 'isolated' + if self.positions is None: + self.positions = {} + clientPositions = self.safe_value(self.positions, url) + if clientPositions is None: + self.positions[url] = {} + clientMarginModePositions = self.safe_value(clientPositions, marginMode) + if clientMarginModePositions is None: + self.positions[url][marginMode] = ArrayCacheBySymbolBySide() + cache = self.positions[url][marginMode] + rawPositions = self.safe_value(message, 'data', []) + newPositions = [] + timestamp = self.safe_integer(message, 'ts') + for i in range(0, len(rawPositions)): + rawPosition = rawPositions[i] + position = self.parse_position(rawPosition) + position['timestamp'] = timestamp + position['datetime'] = self.iso8601(timestamp) + newPositions.append(position) + cache.append(position) + messageHashes = self.find_message_hashes(client, marginMode + ':positions::') + for i in range(0, len(messageHashes)): + messageHash = messageHashes[i] + parts = messageHash.split('::') + symbolsString = parts[1] + symbols = symbolsString.split(',') + positions = self.filter_by_array(newPositions, 'symbol', symbols, False) + if not self.is_empty(positions): + client.resolve(positions, messageHash) + client.resolve(newPositions, marginMode + ':positions') + + async def watch_balance(self, params={}) -> Balances: + """ + watch balance and get the amount of funds available for trading or funds locked in orders + + https://www.htx.com/en-us/opend/newApiPages/?id=7ec52e28-7773-11ed-9966-0242ac110003 + https://www.htx.com/en-us/opend/newApiPages/?id=10000084-77b7-11ed-9966-0242ac110003 + https://www.htx.com/en-us/opend/newApiPages/?id=8cb7dcca-77b5-11ed-9966-0242ac110003 + https://www.htx.com/en-us/opend/newApiPages/?id=28c34995-77ae-11ed-9966-0242ac110003 + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + type = None + type, params = self.handle_market_type_and_params('watchBalance', None, params) + subType = None + subType, params = self.handle_sub_type_and_params('watchBalance', None, params, 'linear') + isUnifiedAccount = self.safe_value_2(params, 'isUnifiedAccount', 'unified', False) + params = self.omit(params, ['isUnifiedAccount', 'unified']) + await self.load_markets() + messageHash = None + channel = None + marginMode = None + if type == 'spot': + mode = self.safe_string_2(self.options, 'watchBalance', 'mode', '2') + mode = self.safe_string(params, 'mode', mode) + messageHash = 'accounts.update' + '#' + mode + channel = messageHash + else: + symbol = self.safe_string(params, 'symbol') + currency = self.safe_string(params, 'currency') + market = self.market(symbol) if (symbol is not None) else None + currencyCode = self.currency(currency) if (currency is not None) else None + marginMode = self.safe_string(params, 'margin', 'cross') + params = self.omit(params, ['currency', 'symbol', 'margin']) + prefix = 'accounts' + messageHash = prefix + if subType == 'linear': + if isUnifiedAccount: + # usdt contracts account + prefix = 'accounts_unify' + messageHash = prefix + channel = prefix + '.' + 'usdt' + else: + # usdt contracts account + prefix = prefix + '_cross' if (marginMode == 'cross') else prefix + messageHash = prefix + if marginMode == 'isolated': + # isolated margin only allows filtering by symbol3 + if symbol is not None: + messageHash += '.' + market['id'] + channel = messageHash + else: + # subscribe to all + channel = prefix + '.' + '*' + else: + # cross margin + if currencyCode is not None: + channel = prefix + '.' + currencyCode['id'] + messageHash = channel + else: + # subscribe to all + channel = prefix + '.' + '*' + elif type == 'future': + # inverse futures account + if currencyCode is not None: + messageHash += '.' + currencyCode['id'] + channel = messageHash + else: + # subscribe to all + channel = prefix + '.' + '*' + else: + # inverse swaps account + if market is not None: + messageHash += '.' + market['id'] + channel = messageHash + else: + # subscribe to all + channel = prefix + '.' + '*' + subscriptionParams: dict = { + 'type': type, + 'subType': subType, + 'margin': marginMode, + } + # we are differentiating the channel from the messageHash for global subscriptions(*) + # because huobi returns a different topic than the topic sent. Example: we send + # "accounts.*" and "accounts" is returned so we're setting channel = "accounts.*" and + # messageHash = "accounts" allowing handleBalance to freely resolve the topic in the message + return await self.subscribe_private(channel, messageHash, type, subType, params, subscriptionParams) + + def handle_balance(self, client: Client, message): + # spot + # + # { + # "action": "push", + # "ch": "accounts.update#0", + # "data": { + # "currency": "btc", + # "accountId": 123456, + # "balance": "23.111", + # "available": "2028.699426619837209087", + # "changeType": "transfer", + # "accountType":"trade", + # "seqNum": "86872993928", + # "changeTime": 1568601800000 + # } + # } + # + # inverse future + # + # { + # "op":"notify", + # "topic":"accounts.ada", + # "ts":1604388667226, + # "event":"order.match", + # "data":[ + # { + # "symbol":"ADA", + # "margin_balance":446.417641681222726716, + # "margin_static":445.554085945257745136, + # "margin_position":11.049723756906077348, + # "margin_frozen":0, + # "margin_available":435.367917924316649368, + # "profit_real":21.627049781983019459, + # "profit_unreal":0.86355573596498158, + # "risk_rate":40.000796572150656768, + # "liquidation_price":0.018674308027108984, + # "withdraw_available":423.927036163274725677, + # "lever_rate":20, + # "adjust_factor":0.4 + # } + # ], + # "uid":"123456789" + # } + # + # usdt / linear future, swap + # + # { + # "op":"notify", + # "topic":"accounts.btc-usdt", # or "accounts" for global subscriptions + # "ts":1603711370689, + # "event":"order.open", + # "data":[ + # { + # "margin_mode":"cross", + # "margin_account":"USDT", + # "margin_asset":"USDT", + # "margin_balance":30.959342395, + # "margin_static":30.959342395, + # "margin_position":0, + # "margin_frozen":10, + # "profit_real":0, + # "profit_unreal":0, + # "withdraw_available":20.959342395, + # "risk_rate":153.796711975, + # "position_mode":"dual_side", + # "contract_detail":[ + # { + # "symbol":"LTC", + # "contract_code":"LTC-USDT", + # "margin_position":0, + # "margin_frozen":0, + # "margin_available":20.959342395, + # "profit_unreal":0, + # "liquidation_price":null, + # "lever_rate":1, + # "adjust_factor":0.01, + # "contract_type":"swap", + # "pair":"LTC-USDT", + # "business_type":"swap", + # "trade_partition":"USDT" + # }, + # ], + # "futures_contract_detail":[], + # } + # ] + # } + # + # inverse future + # + # { + # "op":"notify", + # "topic":"accounts.ada", + # "ts":1604388667226, + # "event":"order.match", + # "data":[ + # { + # "symbol":"ADA", + # "margin_balance":446.417641681222726716, + # "margin_static":445.554085945257745136, + # "margin_position":11.049723756906077348, + # "margin_frozen":0, + # "margin_available":435.367917924316649368, + # "profit_real":21.627049781983019459, + # "profit_unreal":0.86355573596498158, + # "risk_rate":40.000796572150656768, + # "liquidation_price":0.018674308027108984, + # "withdraw_available":423.927036163274725677, + # "lever_rate":20, + # "adjust_factor":0.4 + # } + # ], + # "uid":"123456789" + # } + # + channel = self.safe_string(message, 'ch') + data = self.safe_value(message, 'data', []) + timestamp = self.safe_integer(data, 'changeTime', self.safe_integer(message, 'ts')) + self.balance['timestamp'] = timestamp + self.balance['datetime'] = self.iso8601(timestamp) + self.balance['info'] = data + if channel is not None: + # spot balance + currencyId = self.safe_string(data, 'currency') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(data, 'available') + account['total'] = self.safe_string(data, 'balance') + self.balance[code] = account + self.balance = self.safe_balance(self.balance) + client.resolve(self.balance, channel) + else: + # contract balance + dataLength = len(data) + if dataLength == 0: + return + first = self.safe_value(data, 0, {}) + topic = self.safe_string(message, 'topic') + splitTopic = topic.split('.') + messageHash = self.safe_string(splitTopic, 0) + subscription = self.safe_value_2(client.subscriptions, messageHash, messageHash + '.*') + if subscription is None: + # if subscription not found means that we subscribed to a specific currency/symbol + # and we use the first data entry to find it + # Example: topic = 'accounts' + # client.subscription hash = 'accounts.usdt' + # we do 'accounts' + '.' + data[0]]['margin_asset'] to get it + currencyId = self.safe_string_2(first, 'margin_asset', 'symbol') + messageHash += '.' + currencyId.lower() + subscription = self.safe_value(client.subscriptions, messageHash) + type = self.safe_string(subscription, 'type') + subType = self.safe_string(subscription, 'subType') + if topic == 'accounts_unify': + # { + # "margin_asset": "USDT", + # "margin_static": 10, + # "cross_margin_static": 10, + # "margin_balance": 10, + # "cross_profit_unreal": 0, + # "margin_frozen": 0, + # "withdraw_available": 10, + # "cross_risk_rate": null, + # "cross_swap": [], + # "cross_future": [], + # "isolated_swap": [] + # } + marginAsset = self.safe_string(first, 'margin_asset') + code = self.safe_currency_code(marginAsset) + marginFrozen = self.safe_string(first, 'margin_frozen') + unifiedAccount = self.account() + unifiedAccount['free'] = self.safe_string(first, 'withdraw_available') + unifiedAccount['used'] = marginFrozen + self.balance[code] = unifiedAccount + self.balance = self.safe_balance(self.balance) + client.resolve(self.balance, 'accounts_unify') + elif subType == 'linear': + margin = self.safe_string(subscription, 'margin') + if margin == 'cross': + fieldName = 'futures_contract_detail' if (type == 'future') else 'contract_detail' + balances = self.safe_value(first, fieldName, []) + balancesLength = len(balances) + if balancesLength > 0: + for i in range(0, len(balances)): + balance = balances[i] + marketId = self.safe_string_2(balance, 'contract_code', 'margin_account') + market = self.safe_market(marketId) + currencyId = self.safe_string(balance, 'margin_asset') + currency = self.safe_currency(currencyId) + code = self.safe_string(market, 'settle', currency['code']) + # the exchange outputs positions for delisted markets + # https://www.huobi.com/support/en-us/detail/74882968522337 + # we skip it if the market was delisted + if code is not None: + account = self.account() + account['free'] = self.safe_string_2(balance, 'margin_balance', 'margin_available') + account['used'] = self.safe_string(balance, 'margin_frozen') + accountsByCode: dict = {} + accountsByCode[code] = account + symbol = market['symbol'] + self.balance[symbol] = self.safe_balance(accountsByCode) + else: + # isolated margin + for i in range(0, len(data)): + isolatedBalance = data[i] + account = self.account() + account['free'] = self.safe_string(isolatedBalance, 'margin_balance', 'margin_available') + account['used'] = self.safe_string(isolatedBalance, 'margin_frozen') + currencyId = self.safe_string_2(isolatedBalance, 'margin_asset', 'symbol') + code = self.safe_currency_code(currencyId) + self.balance[code] = account + self.balance = self.safe_balance(self.balance) + else: + # inverse branch + for i in range(0, len(data)): + balance = data[i] + currencyId = self.safe_string(balance, 'symbol') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(balance, 'margin_available') + account['used'] = self.safe_string(balance, 'margin_frozen') + self.balance[code] = account + self.balance = self.safe_balance(self.balance) + client.resolve(self.balance, messageHash) + + def handle_subscription_status(self, client: Client, message): + # + # { + # "id": 1583414227, + # "status": "ok", + # "subbed": "market.btcusdt.mbp.150", + # "ts": 1583414229143 + # } + # + # unsubscribe + # { + # "id": "2", + # "status": "ok", + # "unsubbed": "market.BTC-USDT-251003.detail", + # "ts": 1759329276980 + # } + # + id = self.safe_string(message, 'id') + subscriptionsById = self.index_by(client.subscriptions, 'id') + subscription = self.safe_dict(subscriptionsById, id) + if subscription is not None: + method = self.safe_value(subscription, 'method') + if method is not None: + method(client, message, subscription) + # return; commented out to clean up + # clean up + if id in client.subscriptions: + del client.subscriptions[id] + if 'unsubbed' in message: + self.handle_un_subscription(client, subscription) + + def handle_un_subscription(self, client: Client, subscription: dict): + messageHashes = self.safe_list(subscription, 'messageHashes', []) + subMessageHashes = self.safe_list(subscription, 'subMessageHashes', []) + for i in range(0, len(messageHashes)): + unsubHash = messageHashes[i] + subHash = subMessageHashes[i] + self.clean_unsubscription(client, subHash, unsubHash) + self.clean_cache(subscription) + + def handle_system_status(self, client: Client, message): + # + # todo: answer the question whether handleSystemStatus should be renamed + # and unified for any usage pattern that + # involves system status and maintenance updates + # + # { + # "id": "1578090234088", # connectId + # "type": "welcome", + # } + # + return message + + def handle_subject(self, client: Client, message): + # spot + # { + # "ch": "market.btcusdt.mbp.150", + # "ts": 1583472025885, + # "tick": { + # "seqNum": 104998984994, + # "prevSeqNum": 104998984977, + # "bids": [ + # [9058.27, 0], + # [9058.43, 0], + # [9058.99, 0], + # ], + # "asks": [ + # [9084.27, 0.2], + # [9085.69, 0], + # [9085.81, 0], + # ] + # } + # } + # non spot + # + # { + # "ch":"market.BTC220218.depth.size_150.high_freq", + # "tick":{ + # "asks":[], + # "bids":[ + # [43445.74,1], + # [43444.48,0], + # [40593.92,9] + # ], + # "ch":"market.BTC220218.depth.size_150.high_freq", + # "event":"update", + # "id":152727500274, + # "mrid":152727500274, + # "ts":1645023376098, + # "version":37536690 + # }, + # "ts":1645023376098 + # } + # + # spot private trade + # + # { + # "action":"push", + # "ch":"trade.clearing#ltcusdt#1", + # "data":{ + # "eventType":"trade", + # "symbol":"ltcusdt", + # # ... + # }, + # } + # + # spot order + # + # { + # "action":"push", + # "ch":"orders#btcusdt", + # "data": { + # "orderSide":"buy", + # "lastActTime":1583853365586, + # "clientOrderId":"abc123", + # "orderStatus":"rejected", + # "symbol":"btcusdt", + # "eventType":"trigger", + # "errCode": 2002, + # "errMessage":"invalid.client.order.id(NT)" + # } + # } + # + # contract order + # + # { + # "op":"notify", + # "topic":"orders.ada", + # "ts":1604388667226, + # # ? + # } + # + ch = self.safe_value(message, 'ch', '') + parts = ch.split('.') + type = self.safe_string(parts, 0) + if type == 'market': + methodName = self.safe_string(parts, 2) + methods: dict = { + 'depth': self.handle_order_book, + 'mbp': self.handle_order_book, + 'detail': self.handle_ticker, + 'bbo': self.handle_ticker, + 'ticker': self.handle_ticker, + 'trade': self.handle_trades, + 'kline': self.handle_ohlcv, + } + method = self.safe_value(methods, methodName) + if method is not None: + method(client, message) + return + # private spot subjects + privateParts = ch.split('#') + privateType = self.safe_string(privateParts, 0, '') + if privateType == 'trade.clearing': + self.handle_my_trade(client, message) + return + if privateType.find('accounts.update') >= 0: + self.handle_balance(client, message) + return + if privateType == 'orders': + self.handle_order(client, message) + return + # private contract subjects + op = self.safe_string(message, 'op') + if op == 'notify': + topic = self.safe_string(message, 'topic', '') + if topic.find('orders') >= 0: + self.handle_order(client, message) + if topic.find('account') >= 0: + self.handle_balance(client, message) + if topic.find('positions') >= 0: + self.handle_positions(client, message) + + async def pong(self, client, message): + # + # {ping: 1583491673714} + # {action: "ping", data: {ts: 1645108204665}} + # {op: "ping", ts: "1645202800015"} + # + try: + ping = self.safe_integer(message, 'ping') + if ping is not None: + await client.send({'pong': ping}) + return + action = self.safe_string(message, 'action') + if action == 'ping': + data = self.safe_value(message, 'data') + pingTs = self.safe_integer(data, 'ts') + await client.send({'action': 'pong', 'data': {'ts': pingTs}}) + return + op = self.safe_string(message, 'op') + if op == 'ping': + pingTs = self.safe_integer(message, 'ts') + await client.send({'op': 'pong', 'ts': pingTs}) + except Exception as e: + error = NetworkError(self.id + ' pong failed ' + self.json(e)) + client.reset(error) + + def handle_ping(self, client: Client, message): + self.spawn(self.pong, client, message) + + def handle_authenticate(self, client: Client, message): + # + # spot + # + # { + # "action": "req", + # "code": 200, + # "ch": "auth", + # "data": {} + # } + # + # non spot + # + # { + # "op": "auth", + # "type": "api", + # "err-code": 0, + # "ts": 1645200307319, + # "data": {"user-id": "35930539"} + # } + # + promise = client.futures['auth'] + promise.resolve(message) + + def handle_error_message(self, client: Client, message) -> Bool: + # + # { + # "action": "sub", + # "code": 2002, + # "ch": "accounts.update#2", + # "message": "invalid.auth.state" + # } + # + # { + # "ts": 1586323747018, + # "status": "error", + # 'err-code': "bad-request", + # 'err-msg': "invalid mbp.150.symbol linkusdt", + # "id": "2" + # } + # + # { + # "op": "sub", + # "cid": "1", + # "topic": "accounts_unify.USDT", + # "err-code": 4007, + # 'err-msg': "Non - single account user is not available, please check through the cross and isolated account asset interface", + # "ts": 1698419490189 + # } + # { + # "action":"req", + # "code":2002, + # "ch":"auth", + # "message":"auth.fail" + # } + # + status = self.safe_string(message, 'status') + if status == 'error': + id = self.safe_string(message, 'id') + subscriptionsById = self.index_by(client.subscriptions, 'id') + subscription = self.safe_value(subscriptionsById, id) + if subscription is not None: + errorCode = self.safe_string(message, 'err-code') + try: + self.throw_exactly_matched_exception(self.exceptions['ws']['exact'], errorCode, self.json(message)) + raise ExchangeError(self.json(message)) + except Exception as e: + messageHash = self.safe_string(subscription, 'messageHash') + client.reject(e, messageHash) + client.reject(e, id) + if id in client.subscriptions: + del client.subscriptions[id] + return False + code = self.safe_string_2(message, 'code', 'err-code') + if code is not None and ((code != '200') and (code != '0')): + feedback = self.id + ' ' + self.json(message) + try: + self.throw_exactly_matched_exception(self.exceptions['ws']['exact'], code, feedback) + raise ExchangeError(feedback) + except Exception as e: + if isinstance(e, AuthenticationError): + client.reject(e, 'auth') + method = 'auth' + if method in client.subscriptions: + del client.subscriptions[method] + return False + else: + client.reject(e) + return True + + def handle_message(self, client: Client, message): + if self.handle_error_message(client, message): + # + # {"id":1583414227,"status":"ok","subbed":"market.btcusdt.mbp.150","ts":1583414229143} + # + # first ping format + # + # {"ping": 1645106821667} + # + # second ping format + # + # {"action":"ping","data":{"ts":1645106821667}} + # + # third pong format + # + # + # auth spot + # + # { + # "action": "req", + # "code": 200, + # "ch": "auth", + # "data": {} + # } + # + # auth non spot + # + # { + # "op": "auth", + # "type": "api", + # "err-code": 0, + # "ts": 1645200307319, + # "data": {"user-id": "35930539"} + # } + # + # trade + # + # { + # "action":"push", + # "ch":"trade.clearing#ltcusdt#1", + # "data":{ + # "eventType":"trade", + # # ? + # } + # } + # + if 'id' in message: + self.handle_subscription_status(client, message) + return + if 'action' in message: + action = self.safe_string(message, 'action') + if action == 'ping': + self.handle_ping(client, message) + return + if action == 'sub': + self.handle_subscription_status(client, message) + return + if 'ch' in message: + if message['ch'] == 'auth': + self.handle_authenticate(client, message) + return + else: + # route by channel aka topic aka subject + self.handle_subject(client, message) + return + if 'op' in message: + op = self.safe_string(message, 'op') + if op == 'ping': + self.handle_ping(client, message) + return + if op == 'auth': + self.handle_authenticate(client, message) + return + if op == 'sub': + self.handle_subscription_status(client, message) + return + if op == 'notify': + self.handle_subject(client, message) + return + if 'ping' in message: + self.handle_ping(client, message) + + def handle_my_trade(self, client: Client, message, extendParams={}): + # + # spot + # + # { + # "action":"push", + # "ch":"trade.clearing#ltcusdt#1", + # "data":{ + # "eventType":"trade", + # "symbol":"ltcusdt", + # "orderId":"478862728954426", + # "orderSide":"buy", + # "orderType":"buy-market", + # "accountId":44234548, + # "source":"spot-web", + # "orderValue":"5.01724137", + # "orderCreateTime":1645124660365, + # "orderStatus":"filled", + # "feeCurrency":"ltc", + # "tradePrice":"118.89", + # "tradeVolume":"0.042200701236437042", + # "aggressor":true, + # "tradeId":101539740584, + # "tradeTime":1645124660368, + # "transactFee":"0.000041778694224073", + # "feeDeduct":"0", + # "feeDeductType":"" + # } + # } + # + # contract + # + # { + # "symbol": "ADA/USDT:USDT" + # "ch": "orders_cross.ada-usdt" + # "trades": [ + # { + # "trade_fee":-0.022099447513812154, + # "fee_asset":"ADA", + # "trade_id":113913755890, + # "id":"113913755890-773207641127878656-1", + # "trade_volume":1, + # "trade_price":0.0905, + # "trade_turnover":10, + # "created_at":1604388667194, + # "profit":0, + # "real_profit": 0, + # "role":"maker" + # } + # ], + # } + # + if self.myTrades is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + self.myTrades = ArrayCacheBySymbolById(limit) + cachedTrades = self.myTrades + messageHash = self.safe_string(message, 'ch') + if messageHash is not None: + data = self.safe_value(message, 'data') + if data is not None: + parsed = self.parse_ws_trade(data) + symbol = self.safe_string(parsed, 'symbol') + if symbol is not None: + cachedTrades.append(parsed) + client.resolve(self.myTrades, messageHash) + else: + # self trades object is artificially created + # in handleOrder + rawTrades = self.safe_value(message, 'trades', []) + marketId = self.safe_value(message, 'symbol') + market = self.market(marketId) + for i in range(0, len(rawTrades)): + trade = rawTrades[i] + parsedTrade = self.parse_trade(trade, market) + # add extra params(side, type, ...) coming from the order + parsedTrade = self.extend(parsedTrade, extendParams) + cachedTrades.append(parsedTrade) + # messageHash here is the orders one, so + # we have to recreate the trades messageHash = orderMessageHash + ':' + 'trade' + tradesHash = messageHash + ':' + 'trade' + client.resolve(self.myTrades, tradesHash) + # when we make an global order sub we have to send the channel like self + # ch = orders_cross.* and we store messageHash = 'orders_cross' + # however it is returned with the specific order update symbol: ch = orders_cross.btc-usd + # since self is a global sub, our messageHash does not specify any symbol(ex: orders_cross:trade) + # so we must remove it + genericOrderHash = messageHash.replace('.' + market['lowercaseId'], '') + lowerCaseBaseId = self.safe_string_lower(market, 'baseId') + genericOrderHash = genericOrderHash.replace('.' + lowerCaseBaseId, '') + genericTradesHash = genericOrderHash + ':' + 'trade' + client.resolve(self.myTrades, genericTradesHash) + + def parse_ws_trade(self, trade, market=None): + # spot private + # + # { + # "eventType":"trade", + # "symbol":"ltcusdt", + # "orderId":"478862728954426", + # "orderSide":"buy", + # "orderType":"buy-market", + # "accountId":44234548, + # "source":"spot-web", + # "orderValue":"5.01724137", + # "orderCreateTime":1645124660365, + # "orderStatus":"filled", + # "feeCurrency":"ltc", + # "tradePrice":"118.89", + # "tradeVolume":"0.042200701236437042", + # "aggressor":true, + # "tradeId":101539740584, + # "tradeTime":1645124660368, + # "transactFee":"0.000041778694224073", + # "feeDeduct":"0", + # "feeDeductType":"" + # } + # + symbol = self.safe_symbol(self.safe_string(trade, 'symbol')) + side = self.safe_string_2(trade, 'side', 'orderSide') + tradeId = self.safe_string(trade, 'tradeId') + price = self.safe_string(trade, 'tradePrice') + amount = self.safe_string(trade, 'tradeVolume') + order = self.safe_string(trade, 'orderId') + timestamp = self.safe_integer(trade, 'tradeTime') + market = self.market(symbol) + orderType = self.safe_string(trade, 'orderType') + aggressor = self.safe_value(trade, 'aggressor') + takerOrMaker = None + if aggressor is not None: + takerOrMaker = 'taker' if aggressor else 'maker' + type = None + orderTypeParts = [] + if orderType is not None: + orderTypeParts = orderType.split('-') + type = self.safe_string(orderTypeParts, 1) + fee = None + feeCurrency = self.safe_currency_code(self.safe_string(trade, 'feeCurrency')) + if feeCurrency is not None: + fee = { + 'cost': self.safe_string(trade, 'transactFee'), + 'currency': feeCurrency, + } + return self.safe_trade({ + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'id': tradeId, + 'order': order, + 'type': type, + 'takerOrMaker': takerOrMaker, + 'side': side, + 'price': price, + 'amount': amount, + 'cost': None, + 'fee': fee, + }, market) + + def get_url_by_market_type(self, type, isLinear=True, isPrivate=False, isFeed=False): + api = self.safe_string(self.options, 'api', 'api') + hostname: dict = {'hostname': self.hostname} + hostnameURL = None + url = None + if type == 'spot': + if isPrivate: + hostnameURL = self.urls['api']['ws'][api]['spot']['private'] + else: + if isFeed: + hostnameURL = self.urls['api']['ws'][api]['spot']['feed'] + else: + hostnameURL = self.urls['api']['ws'][api]['spot']['public'] + url = self.implode_params(hostnameURL, hostname) + else: + baseUrl = self.urls['api']['ws'][api][type] + subTypeUrl = baseUrl['linear'] if isLinear else baseUrl['inverse'] + url = subTypeUrl['private'] if isPrivate else subTypeUrl['public'] + return url + + async def subscribe_public(self, url, symbol, messageHash, method=None, params={}): + requestId = self.request_id() + request: dict = { + 'sub': messageHash, + 'id': requestId, + } + subscription: dict = { + 'id': requestId, + 'messageHash': messageHash, + 'symbol': symbol, + 'params': params, + } + if method is not None: + subscription['method'] = method + return await self.watch(url, messageHash, self.extend(request, params), messageHash, subscription) + + async def unsubscribe_public(self, market: Market, subMessageHash: str, topic: str, params={}): + requestId = self.request_id() + request: dict = { + 'unsub': subMessageHash, + 'id': requestId, + } + messageHash = 'unsubscribe::' + subMessageHash + isFeed = (topic == 'orderbook') + url = self.get_url_by_market_type(market['type'], market['linear'], False, isFeed) + subscription: dict = { + 'unsubscribe': True, + 'id': requestId, + 'subMessageHashes': [subMessageHash], + 'messageHashes': [messageHash], + 'symbols': [market['symbol']], + 'topic': topic, + } + symbolsAndTimeframes = self.safe_list(params, 'symbolsAndTimeframes') + if symbolsAndTimeframes is not None: + subscription['symbolsAndTimeframes'] = symbolsAndTimeframes + params = self.omit(params, 'symbolsAndTimeframes') + return await self.watch(url, messageHash, self.extend(request, params), messageHash, subscription) + + async def subscribe_private(self, channel, messageHash, type, subtype, params={}, subscriptionParams={}): + requestId = self.request_id() + subscription: dict = { + 'id': requestId, + 'messageHash': messageHash, + 'params': params, + } + extendedSubsription = self.extend(subscription, subscriptionParams) + request = None + if type == 'spot': + request = { + 'action': 'sub', + 'ch': channel, + } + else: + request = { + 'op': 'sub', + 'topic': channel, + 'cid': requestId, + } + isLinear = subtype == 'linear' + url = self.get_url_by_market_type(type, isLinear, True) + hostname = self.urls['hostnames']['spot'] if (type == 'spot') else self.urls['hostnames']['contract'] + authParams: dict = { + 'type': type, + 'url': url, + 'hostname': hostname, + } + await self.authenticate(authParams) + return await self.watch(url, messageHash, self.extend(request, params), channel, extendedSubsription) + + async def authenticate(self, params={}): + url = self.safe_string(params, 'url') + hostname = self.safe_string(params, 'hostname') + type = self.safe_string(params, 'type') + if url is None or hostname is None or type is None: + raise ArgumentsRequired(self.id + ' authenticate requires a url, hostname and type argument') + self.check_required_credentials() + messageHash = 'auth' + relativePath = url.replace('wss://' + hostname, '') + client = self.client(url) + future = client.reusableFuture(messageHash) + authenticated = self.safe_value(client.subscriptions, messageHash) + if authenticated is None: + timestamp = self.ymdhms(self.milliseconds(), 'T') + signatureParams = None + if type == 'spot': + signatureParams = { + 'accessKey': self.apiKey, + 'signatureMethod': 'HmacSHA256', + 'signatureVersion': '2.1', + 'timestamp': timestamp, + } + else: + signatureParams = { + 'AccessKeyId': self.apiKey, + 'SignatureMethod': 'HmacSHA256', + 'SignatureVersion': '2', + 'Timestamp': timestamp, + } + signatureParams = self.keysort(signatureParams) + auth = self.urlencode(signatureParams, True) # True required in go + payload = "\n".join(['GET', hostname, relativePath, auth]) # eslint-disable-line quotes + signature = self.hmac(self.encode(payload), self.encode(self.secret), hashlib.sha256, 'base64') + request = None + if type == 'spot': + newParams: dict = { + 'authType': 'api', + 'accessKey': self.apiKey, + 'signatureMethod': 'HmacSHA256', + 'signatureVersion': '2.1', + 'timestamp': timestamp, + 'signature': signature, + } + request = { + 'params': newParams, + 'action': 'req', + 'ch': 'auth', + } + else: + request = { + 'op': 'auth', + 'type': 'api', + 'AccessKeyId': self.apiKey, + 'SignatureMethod': 'HmacSHA256', + 'SignatureVersion': '2', + 'Timestamp': timestamp, + 'Signature': signature, + } + requestId = self.request_id() + subscription: dict = { + 'id': requestId, + 'messageHash': messageHash, + 'params': params, + } + self.watch(url, messageHash, request, messageHash, subscription) + return await future diff --git a/ccxt/pro/huobi.py b/ccxt/pro/huobi.py new file mode 100644 index 0000000..c3b5a9c --- /dev/null +++ b/ccxt/pro/huobi.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.pro.htx import htx +from ccxt.base.types import Any + + +class huobi(htx): + + def describe(self) -> Any: + return self.deep_extend(super(huobi, self).describe(), { + 'alias': True, + 'id': 'huobi', + }) diff --git a/ccxt/pro/hyperliquid.py b/ccxt/pro/hyperliquid.py new file mode 100644 index 0000000..034c653 --- /dev/null +++ b/ccxt/pro/hyperliquid.py @@ -0,0 +1,1099 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolById, ArrayCacheByTimestamp +from ccxt.base.types import Any, Bool, Int, Market, Num, Order, OrderBook, OrderRequest, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade +from ccxt.async_support.base.ws.client import Client +from typing import List + + +class hyperliquid(ccxt.async_support.hyperliquid): + + def describe(self) -> Any: + return self.deep_extend(super(hyperliquid, self).describe(), { + 'has': { + 'ws': True, + 'cancelOrderWs': True, + 'cancelOrdersWs': True, + 'createOrderWs': True, + 'createOrdersWs': True, + 'editOrderWs': True, + 'watchBalance': False, + 'watchMyTrades': True, + 'watchOHLCV': True, + 'watchOrderBook': True, + 'watchOrders': True, + 'watchTicker': True, + 'watchTickers': True, + 'watchTrades': True, + 'watchTradesForSymbols': False, + 'watchPosition': False, + }, + 'urls': { + 'api': { + 'ws': { + 'public': 'wss://api.hyperliquid.xyz/ws', + }, + }, + 'test': { + 'ws': { + 'public': 'wss://api.hyperliquid-testnet.xyz/ws', + }, + }, + }, + 'options': { + }, + 'streaming': { + 'ping': self.ping, + 'keepAlive': 20000, + }, + 'exceptions': { + 'ws': { + 'exact': { + }, + }, + }, + }) + + async def create_orders_ws(self, orders: List[OrderRequest], params={}): + """ + create a list of trade orders using WebSocket post request + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#place-an-order + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + url = self.urls['api']['ws']['public'] + ordersRequest = self.createOrdersRequest(orders, params) + wrapped = self.wrap_as_post_action(ordersRequest) + request = self.safe_dict(wrapped, 'request', {}) + requestId = self.safe_string(wrapped, 'requestId') + response = await self.watch(url, requestId, request, requestId) + responseOjb = self.safe_dict(response, 'response', {}) + data = self.safe_dict(responseOjb, 'data', {}) + statuses = self.safe_list(data, 'statuses', []) + return self.parse_orders(statuses, None) + + async def create_order_ws(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order using WebSocket post request + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#place-an-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.timeInForce]: 'Gtc', 'Ioc', 'Alo' + :param bool [params.postOnly]: True or False whether the order is post-only + :param bool [params.reduceOnly]: True or False whether the order is reduce-only + :param float [params.triggerPrice]: The price at which a trigger order is triggered at + :param str [params.clientOrderId]: client order id,(optional 128 bit hex string e.g. 0x1234567890abcdef1234567890abcdef) + :param str [params.slippage]: the slippage for market order + :param str [params.vaultAddress]: the vault address for order + :returns dict: an `order structure ` + """ + await self.load_markets() + order, globalParams = self.parseCreateEditOrderArgs(None, symbol, type, side, amount, price, params) + orders = await self.create_orders_ws([order], globalParams) + ordersLength = len(orders) + if ordersLength == 0: + # not sure why but it is happening sometimes + return self.safe_order({}) + parsedOrder = orders[0] + return parsedOrder + + async def edit_order_ws(self, id: str, symbol: str, type: str, side: str, amount: Num = None, price: Num = None, params={}): + """ + edit a trade order + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#modify-multiple-orders + + :param str id: cancel order id + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.timeInForce]: 'Gtc', 'Ioc', 'Alo' + :param bool [params.postOnly]: True or False whether the order is post-only + :param bool [params.reduceOnly]: True or False whether the order is reduce-only + :param float [params.triggerPrice]: The price at which a trigger order is triggered at + :param str [params.clientOrderId]: client order id,(optional 128 bit hex string e.g. 0x1234567890abcdef1234567890abcdef) + :param str [params.vaultAddress]: the vault address for order + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + url = self.urls['api']['ws']['public'] + order, globalParams = self.parseCreateEditOrderArgs(id, symbol, type, side, amount, price, params) + postRequest = self.editOrdersRequest([order], globalParams) + wrapped = self.wrap_as_post_action(postRequest) + request = self.safe_dict(wrapped, 'request', {}) + requestId = self.safe_string(wrapped, 'requestId') + response = await self.watch(url, requestId, request, requestId) + # response is the same self.edit_order + responseObject = self.safe_dict(response, 'response', {}) + dataObject = self.safe_dict(responseObject, 'data', {}) + statuses = self.safe_list(dataObject, 'statuses', []) + first = self.safe_dict(statuses, 0, {}) + parsedOrder = self.parse_order(first, market) + return parsedOrder + + async def cancel_orders_ws(self, ids: List[str], symbol: Str = None, params={}): + """ + cancel multiple orders using WebSocket post request + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/websocket/post-requests + + :param str[] ids: list of order ids to cancel + :param str symbol: unified symbol of the market the orders were made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str[] [params.clientOrderId]: list of client order ids to cancel instead of order ids + :param str [params.vaultAddress]: the vault address for order cancellation + :returns dict[]: a list of `order structures ` + """ + self.check_required_credentials() + await self.load_markets() + request = self.cancelOrdersRequest(ids, symbol, params) + url = self.urls['api']['ws']['public'] + wrapped = self.wrap_as_post_action(request) + wsRequest = self.safe_dict(wrapped, 'request', {}) + requestId = self.safe_string(wrapped, 'requestId') + response = await self.watch(url, requestId, wsRequest, requestId) + responseObj = self.safe_dict(response, 'response', {}) + data = self.safe_dict(responseObj, 'data', {}) + statuses = self.safe_list(data, 'statuses', []) + orders = [] + for i in range(0, len(statuses)): + status = statuses[i] + orders.append(self.safe_order({ + 'info': status, + 'status': status, + })) + return orders + + async def cancel_order_ws(self, id: str, symbol: Str = None, params={}): + """ + cancel a single order using WebSocket post request + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/websocket/post-requests + + :param str id: order id to cancel + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.clientOrderId]: client order id to cancel instead of order id + :param str [params.vaultAddress]: the vault address for order cancellation + :returns dict: an `order structure ` + """ + orders = await self.cancel_orders_ws([id], symbol, params) + return self.safe_dict(orders, 0) + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/websocket/subscriptions + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + messageHash = 'orderbook:' + symbol + url = self.urls['api']['ws']['public'] + request: dict = { + 'method': 'subscribe', + 'subscription': { + 'type': 'l2Book', + 'coin': market['baseName'] if market['swap'] else market['id'], + }, + } + message = self.extend(request, params) + orderbook = await self.watch(url, messageHash, message, messageHash) + return orderbook.limit() + + async def un_watch_order_book(self, symbol: str, params={}) -> Any: + """ + unWatches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/websocket/subscriptions + + :param str symbol: unified symbol of the market to fetch the order book for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + subMessageHash = 'orderbook:' + symbol + messageHash = 'unsubscribe:' + subMessageHash + url = self.urls['api']['ws']['public'] + id = str(self.nonce()) + request: dict = { + 'id': id, + 'method': 'unsubscribe', + 'subscription': { + 'type': 'l2Book', + 'coin': market['baseName'] if market['swap'] else market['id'], + }, + } + message = self.extend(request, params) + return await self.watch(url, messageHash, message, messageHash) + + def handle_order_book(self, client, message): + # + # { + # "channel": "l2Book", + # "data": { + # "coin": "BTC", + # "time": 1710131872708, + # "levels": [ + # [ + # { + # "px": "68674.0", + # "sz": "0.97139", + # "n": 4 + # } + # ], + # [ + # { + # "px": "68675.0", + # "sz": "0.04396", + # "n": 1 + # } + # ] + # ] + # } + # } + # + entry = self.safe_dict(message, 'data', {}) + coin = self.safe_string(entry, 'coin') + marketId = self.coinToMarketId(coin) + market = self.market(marketId) + symbol = market['symbol'] + rawData = self.safe_list(entry, 'levels', []) + data: dict = { + 'bids': self.safe_list(rawData, 0, []), + 'asks': self.safe_list(rawData, 1, []), + } + timestamp = self.safe_integer(entry, 'time') + snapshot = self.parse_order_book(data, symbol, timestamp, 'bids', 'asks', 'px', 'sz') + if not (symbol in self.orderbooks): + ob = self.order_book(snapshot) + self.orderbooks[symbol] = ob + orderbook = self.orderbooks[symbol] + orderbook.reset(snapshot) + messageHash = 'orderbook:' + symbol + client.resolve(orderbook, messageHash) + + async def watch_ticker(self, symbol: str, params={}) -> Ticker: + """ + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/websocket/subscriptions + + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + market = self.market(symbol) + symbol = market['symbol'] + tickers = await self.watch_tickers([symbol], params) + return tickers[symbol] + + async def watch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/websocket/subscriptions + + :param str[] symbols: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, True) + messageHash = 'tickers' + url = self.urls['api']['ws']['public'] + request: dict = { + 'method': 'subscribe', + 'subscription': { + 'type': 'webData2', # allMids + 'user': '0x0000000000000000000000000000000000000000', + }, + } + tickers = await self.watch(url, messageHash, self.extend(request, params), messageHash) + if self.newUpdates: + return self.filter_by_array_tickers(tickers, 'symbol', symbols) + return self.tickers + + async def un_watch_tickers(self, symbols: Strings = None, params={}) -> Any: + """ + unWatches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/websocket/subscriptions + + :param str[] symbols: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, True) + subMessageHash = 'tickers' + messageHash = 'unsubscribe:' + subMessageHash + url = self.urls['api']['ws']['public'] + request: dict = { + 'method': 'unsubscribe', + 'subscription': { + 'type': 'webData2', # allMids + 'user': '0x0000000000000000000000000000000000000000', + }, + } + return await self.watch(url, messageHash, self.extend(request, params), messageHash) + + async def watch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + watches information on multiple trades made by the user + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/websocket/subscriptions + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.user]: user address, will default to self.walletAddress if not provided + :returns dict[]: a list of `order structures ` + """ + userAddress = None + userAddress, params = self.handlePublicAddress('watchMyTrades', params) + await self.load_markets() + messageHash = 'myTrades' + if symbol is not None: + symbol = self.symbol(symbol) + messageHash += ':' + symbol + url = self.urls['api']['ws']['public'] + request: dict = { + 'method': 'subscribe', + 'subscription': { + 'type': 'userFills', + 'user': userAddress, + }, + } + message = self.extend(request, params) + trades = await self.watch(url, messageHash, message, messageHash) + if self.newUpdates: + limit = trades.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(trades, symbol, since, limit, True) + + def handle_ws_tickers(self, client: Client, message): + # + # { + # "channel": "webData2", + # "data": { + # "meta": { + # "universe": [ + # { + # "szDecimals": 5, + # "name": "BTC", + # "maxLeverage": 50, + # "onlyIsolated": False + # }, + # ... + # ], + # }, + # "assetCtxs": [ + # { + # "funding": "0.00003005", + # "openInterest": "2311.50778", + # "prevDayPx": "63475.0", + # "dayNtlVlm": "468043329.64289033", + # "premium": "0.00094264", + # "oraclePx": "64712.0", + # "markPx": "64774.0", + # "midPx": "64773.5", + # "impactPxs": [ + # "64773.0", + # "64774.0" + # ] + # }, + # ... + # ], + # "spotAssetCtxs": [ + # { + # "prevDayPx": "0.20937", + # "dayNtlVlm": "11188888.61984999", + # "markPx": "0.19722", + # "midPx": "0.197145", + # "circulatingSupply": "598760557.12072003", + # "coin": "PURR/USDC" + # }, + # ... + # ], + # } + # } + # + # spot + rawData = self.safe_dict(message, 'data', {}) + spotAssets = self.safe_list(rawData, 'spotAssetCtxs', []) + parsedTickers = [] + for i in range(0, len(spotAssets)): + assetObject = spotAssets[i] + marketId = self.safe_string(assetObject, 'coin') + market = self.safe_market(marketId, None, None, 'spot') + symbol = market['symbol'] + ticker = self.parse_ws_ticker(assetObject, market) + parsedTickers.append(ticker) + self.tickers[symbol] = ticker + # perpetuals + meta = self.safe_dict(rawData, 'meta', {}) + universe = self.safe_list(meta, 'universe', []) + assetCtxs = self.safe_list(rawData, 'assetCtxs', []) + for i in range(0, len(universe)): + data = self.extend( + self.safe_dict(universe, i, {}), + self.safe_dict(assetCtxs, i, {}) + ) + id = data['name'] + '/USDC:USDC' + market = self.safe_market(id, None, None, 'swap') + symbol = market['symbol'] + ticker = self.parse_ws_ticker(data, market) + self.tickers[symbol] = ticker + parsedTickers.append(ticker) + tickers = self.index_by(parsedTickers, 'symbol') + client.resolve(tickers, 'tickers') + + def parse_ws_ticker(self, rawTicker, market: Market = None) -> Ticker: + return self.parse_ticker(rawTicker, market) + + def handle_my_trades(self, client: Client, message): + # + # { + # "channel": "userFills", + # "data": { + # "isSnapshot": True, + # "user": "0x15f43d1f2dee81424afd891943262aa90f22cc2a", + # "fills": [ + # { + # "coin": "BTC", + # "px": "72528.0", + # "sz": "0.11693", + # "side": "A", + # "time": 1710208712815, + # "startPosition": "0.11693", + # "dir": "Close Long", + # "closedPnl": "-0.81851", + # "hash": "0xc5adaf35f8402750c218040b0a7bc301130051521273b6f398b3caad3e1f3f5f", + # "oid": 7484888874, + # "crossed": True, + # "fee": "2.968244", + # "liquidationMarkPx": null, + # "tid": 567547935839686, + # "cloid": null + # } + # ] + # } + # } + # + entry = self.safe_dict(message, 'data', {}) + if self.myTrades is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + self.myTrades = ArrayCacheBySymbolById(limit) + trades = self.myTrades + symbols: dict = {} + data = self.safe_list(entry, 'fills', []) + dataLength = len(data) + if dataLength == 0: + return + for i in range(0, len(data)): + rawTrade = data[i] + parsed = self.parse_ws_trade(rawTrade) + symbol = parsed['symbol'] + symbols[symbol] = True + trades.append(parsed) + keys = list(symbols.keys()) + for i in range(0, len(keys)): + currentMessageHash = 'myTrades:' + keys[i] + client.resolve(trades, currentMessageHash) + # non-symbol specific + messageHash = 'myTrades' + client.resolve(trades, messageHash) + + async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + # s + # @method + # @name hyperliquid#watchTrades + # @description watches information on multiple trades made in a market + # @see https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/websocket/subscriptions + # @param {string} symbol unified market symbol of the market trades were made in + # @param {int} [since] the earliest time in ms to fetch trades for + # @param {int} [limit] the maximum number of trade structures to retrieve + # @param {object} [params] extra parameters specific to the exchange API endpoint + # @returns {object[]} a list of `trade structures ` + # + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + messageHash = 'trade:' + symbol + url = self.urls['api']['ws']['public'] + request: dict = { + 'method': 'subscribe', + 'subscription': { + 'type': 'trades', + 'coin': market['baseName'] if market['swap'] else market['id'], + }, + } + message = self.extend(request, params) + trades = await self.watch(url, messageHash, message, messageHash) + if self.newUpdates: + limit = trades.getLimit(symbol, limit) + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + async def un_watch_trades(self, symbol: str, params={}) -> Any: + """ + unWatches information on multiple trades made in a market + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/websocket/subscriptions + + :param str symbol: unified market symbol of the market trades were made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + subMessageHash = 'trade:' + symbol + messageHash = 'unsubscribe:' + subMessageHash + url = self.urls['api']['ws']['public'] + request: dict = { + 'method': 'unsubscribe', + 'subscription': { + 'type': 'trades', + 'coin': market['baseName'] if market['swap'] else market['id'], + }, + } + message = self.extend(request, params) + return await self.watch(url, messageHash, message, messageHash) + + def handle_trades(self, client: Client, message): + # + # { + # "channel": "trades", + # "data": [ + # { + # "coin": "BTC", + # "side": "A", + # "px": "68517.0", + # "sz": "0.005", + # "time": 1710125266669, + # "hash": "0xc872699f116e012186620407fc08a802015e0097c5cce74710697f7272e6e959", + # "tid": 981894269203506 + # } + # ] + # } + # + entry = self.safe_list(message, 'data', []) + first = self.safe_dict(entry, 0, {}) + coin = self.safe_string(first, 'coin') + marketId = self.coinToMarketId(coin) + market = self.market(marketId) + symbol = market['symbol'] + if not (symbol in self.trades): + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + stored = ArrayCache(limit) + self.trades[symbol] = stored + trades = self.trades[symbol] + for i in range(0, len(entry)): + data = self.safe_dict(entry, i) + trade = self.parse_ws_trade(data) + trades.append(trade) + messageHash = 'trade:' + symbol + client.resolve(trades, messageHash) + + def parse_ws_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchMyTrades + # + # { + # "coin": "BTC", + # "px": "72528.0", + # "sz": "0.11693", + # "side": "A", + # "time": 1710208712815, + # "startPosition": "0.11693", + # "dir": "Close Long", + # "closedPnl": "-0.81851", + # "hash": "0xc5adaf35f8402750c218040b0a7bc301130051521273b6f398b3caad3e1f3f5f", + # "oid": 7484888874, + # "crossed": True, + # "fee": "2.968244", + # "liquidationMarkPx": null, + # "tid": 567547935839686, + # "cloid": null + # } + # + # fetchTrades + # + # { + # "coin": "BTC", + # "side": "A", + # "px": "68517.0", + # "sz": "0.005", + # "time": 1710125266669, + # "hash": "0xc872699f116e012186620407fc08a802015e0097c5cce74710697f7272e6e959", + # "tid": 981894269203506 + # } + # + timestamp = self.safe_integer(trade, 'time') + price = self.safe_string(trade, 'px') + amount = self.safe_string(trade, 'sz') + coin = self.safe_string(trade, 'coin') + marketId = self.coinToMarketId(coin) + market = self.safe_market(marketId, None) + symbol = market['symbol'] + id = self.safe_string(trade, 'tid') + side = self.safe_string(trade, 'side') + if side is not None: + side = 'sell' if (side == 'A') else 'buy' + fee = self.safe_string(trade, 'fee') + return self.safe_trade({ + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'id': id, + 'order': self.safe_string(trade, 'oid'), + 'type': None, + 'side': side, + 'takerOrMaker': None, + 'price': price, + 'amount': amount, + 'cost': None, + 'fee': {'cost': fee, 'currency': 'USDC'}, + }, market) + + async def watch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + watches historical candlestick data containing the open, high, low, close price, and the volume of a market + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/websocket/subscriptions + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + url = self.urls['api']['ws']['public'] + request: dict = { + 'method': 'subscribe', + 'subscription': { + 'type': 'candle', + 'coin': market['baseName'] if market['swap'] else market['id'], + 'interval': timeframe, + }, + } + messageHash = 'candles:' + timeframe + ':' + symbol + message = self.extend(request, params) + ohlcv = await self.watch(url, messageHash, message, messageHash) + if self.newUpdates: + limit = ohlcv.getLimit(symbol, limit) + return self.filter_by_since_limit(ohlcv, since, limit, 0, True) + + async def un_watch_ohlcv(self, symbol: str, timeframe: str = '1m', params={}) -> Any: + """ + watches historical candlestick data containing the open, high, low, close price, and the volume of a market + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/websocket/subscriptions + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + url = self.urls['api']['ws']['public'] + request: dict = { + 'method': 'unsubscribe', + 'subscription': { + 'type': 'candle', + 'coin': market['baseName'] if market['swap'] else market['id'], + 'interval': timeframe, + }, + } + subMessageHash = 'candles:' + timeframe + ':' + symbol + messagehash = 'unsubscribe:' + subMessageHash + message = self.extend(request, params) + return await self.watch(url, messagehash, message, messagehash) + + def handle_ohlcv(self, client: Client, message): + # + # { + # channel: 'candle', + # data: { + # t: 1710146280000, + # T: 1710146339999, + # s: 'BTC', + # i: '1m', + # o: '71400.0', + # c: '71411.0', + # h: '71422.0', + # l: '71389.0', + # v: '1.20407', + # n: 20 + # } + # } + # + data = self.safe_dict(message, 'data', {}) + base = self.safe_string(data, 's') + marketId = self.coinToMarketId(base) + symbol = self.safe_symbol(marketId) + timeframe = self.safe_string(data, 'i') + if not (symbol in self.ohlcvs): + self.ohlcvs[symbol] = {} + if not (timeframe in self.ohlcvs[symbol]): + limit = self.safe_integer(self.options, 'OHLCVLimit', 1000) + stored = ArrayCacheByTimestamp(limit) + self.ohlcvs[symbol][timeframe] = stored + ohlcv = self.ohlcvs[symbol][timeframe] + parsed = self.parse_ohlcv(data) + ohlcv.append(parsed) + messageHash = 'candles:' + timeframe + ':' + symbol + client.resolve(ohlcv, messageHash) + + def handle_ws_post(self, client: Client, message: dict): + # { + # channel: "post", + # data: { + # id: , + # response: { + # type: "info" | "action" | "error", + # payload: {...} + # } + # } + data = self.safe_dict(message, 'data') + id = self.safe_string(data, 'id') + response = self.safe_dict(data, 'response') + payload = self.safe_dict(response, 'payload') + client.resolve(payload, id) + + async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + watches information on multiple orders made by the user + + https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/websocket/subscriptions + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.user]: user address, will default to self.walletAddress if not provided + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + userAddress = None + userAddress, params = self.handlePublicAddress('watchOrders', params) + market = None + messageHash = 'order' + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + messageHash = messageHash + ':' + symbol + url = self.urls['api']['ws']['public'] + request: dict = { + 'method': 'subscribe', + 'subscription': { + 'type': 'orderUpdates', + 'user': userAddress, + }, + } + message = self.extend(request, params) + orders = await self.watch(url, messageHash, message, messageHash) + if self.newUpdates: + limit = orders.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(orders, symbol, since, limit, True) + + def handle_order(self, client: Client, message): + # + # { + # channel: 'orderUpdates', + # data: [ + # { + # order: { + # coin: 'BTC', + # side: 'B', + # limitPx: '30000.0', + # sz: '0.001', + # oid: 7456484275, + # timestamp: 1710163596492, + # origSz: '0.001' + # }, + # status: 'open', + # statusTimestamp: 1710163596492 + # } + # ] + # } + # + data = self.safe_list(message, 'data', []) + if self.orders is None: + limit = self.safe_integer(self.options, 'ordersLimit', 1000) + self.orders = ArrayCacheBySymbolById(limit) + dataLength = len(data) + if dataLength == 0: + return + stored = self.orders + messageHash = 'order' + marketSymbols: dict = {} + for i in range(0, len(data)): + rawOrder = data[i] + order = self.parse_order(rawOrder) + stored.append(order) + symbol = self.safe_string(order, 'symbol') + marketSymbols[symbol] = True + keys = list(marketSymbols.keys()) + for i in range(0, len(keys)): + symbol = keys[i] + innerMessageHash = messageHash + ':' + symbol + client.resolve(stored, innerMessageHash) + client.resolve(stored, messageHash) + + def handle_error_message(self, client: Client, message) -> Bool: + # + # { + # "channel": "post", + # "data": { + # "id": 1, + # "response": { + # "type": "action", + # "payload": { + # "status": "ok", + # "response": { + # "type": "order", + # "data": { + # "statuses": [ + # { + # "error": "Order price cannot be more than 80% away from the reference price" + # } + # ] + # } + # } + # } + # } + # } + # } + # + # { + # "channel": "error", + # "data": "Error parsing JSON into valid websocket request: {\"type\": \"allMids\"}" + # } + # + channel = self.safe_string(message, 'channel', '') + if channel == 'error': + ret_msg = self.safe_string(message, 'data', '') + errorMsg = self.id + ' ' + ret_msg + client.reject(errorMsg) + return True + data = self.safe_dict(message, 'data', {}) + id = self.safe_string(message, 'id') + if id is None: + id = self.safe_string(data, 'id') + response = self.safe_dict(data, 'response', {}) + payload = self.safe_dict(response, 'payload', {}) + status = self.safe_string(payload, 'status') + if status is not None and status != 'ok': + errorMsg = self.id + ' ' + self.json(payload) + client.reject(errorMsg, id) + return True + type = self.safe_string(payload, 'type') + if type == 'error': + error = self.id + ' ' + self.json(payload) + client.reject(error, id) + return True + try: + self.handle_errors(0, '', '', '', {}, self.json(payload), payload, {}, {}) + except Exception as e: + client.reject(e, id) + return True + return False + + def handle_order_book_unsubscription(self, client: Client, subscription: dict): + # + # "subscription":{ + # "type":"l2Book", + # "coin":"BTC", + # "nSigFigs":5, + # "mantissa":null + # } + # + coin = self.safe_string(subscription, 'coin') + marketId = self.coinToMarketId(coin) + symbol = self.safe_symbol(marketId) + subMessageHash = 'orderbook:' + symbol + messageHash = 'unsubscribe:' + subMessageHash + self.clean_unsubscription(client, subMessageHash, messageHash) + if symbol in self.orderbooks: + del self.orderbooks[symbol] + + def handle_trades_unsubscription(self, client: Client, subscription: dict): + # + coin = self.safe_string(subscription, 'coin') + marketId = self.coinToMarketId(coin) + symbol = self.safe_symbol(marketId) + subMessageHash = 'trade:' + symbol + messageHash = 'unsubscribe:' + subMessageHash + self.clean_unsubscription(client, subMessageHash, messageHash) + if symbol in self.trades: + del self.trades[symbol] + + def handle_tickers_unsubscription(self, client: Client, subscription: dict): + # + subMessageHash = 'tickers' + messageHash = 'unsubscribe:' + subMessageHash + self.clean_unsubscription(client, subMessageHash, messageHash) + symbols = list(self.tickers.keys()) + for i in range(0, len(symbols)): + del self.tickers[symbols[i]] + + def handle_ohlcv_unsubscription(self, client: Client, subscription: dict): + coin = self.safe_string(subscription, 'coin') + marketId = self.coinToMarketId(coin) + symbol = self.safe_symbol(marketId) + interval = self.safe_string(subscription, 'interval') + timeframe = self.find_timeframe(interval) + subMessageHash = 'candles:' + timeframe + ':' + symbol + messageHash = 'unsubscribe:' + subMessageHash + self.clean_unsubscription(client, subMessageHash, messageHash) + if symbol in self.ohlcvs: + if timeframe in self.ohlcvs[symbol]: + del self.ohlcvs[symbol][timeframe] + + def handle_subscription_response(self, client: Client, message): + # { + # "channel":"subscriptionResponse", + # "data":{ + # "method":"unsubscribe", + # "subscription":{ + # "type":"l2Book", + # "coin":"BTC", + # "nSigFigs":5, + # "mantissa":null + # } + # } + # } + # + # { + # "channel":"subscriptionResponse", + # "data":{ + # "method":"unsubscribe", + # "subscription":{ + # "type":"trades", + # "coin":"PURR/USDC" + # } + # } + # } + # + data = self.safe_dict(message, 'data', {}) + method = self.safe_string(data, 'method') + if method == 'unsubscribe': + subscription = self.safe_dict(data, 'subscription', {}) + type = self.safe_string(subscription, 'type') + if type == 'l2Book': + self.handle_order_book_unsubscription(client, subscription) + elif type == 'trades': + self.handle_trades_unsubscription(client, subscription) + elif type == 'webData2': + self.handle_tickers_unsubscription(client, subscription) + elif type == 'candle': + self.handle_ohlcv_unsubscription(client, subscription) + + def handle_message(self, client: Client, message): + # + # { + # "channel":"subscriptionResponse", + # "data":{ + # "method":"unsubscribe", + # "subscription":{ + # "type":"l2Book", + # "coin":"BTC", + # "nSigFigs":5, + # "mantissa":null + # } + # } + # } + # + if self.handle_error_message(client, message): + return + topic = self.safe_string(message, 'channel', '') + methods: dict = { + 'pong': self.handle_pong, + 'trades': self.handle_trades, + 'l2Book': self.handle_order_book, + 'candle': self.handle_ohlcv, + 'orderUpdates': self.handle_order, + 'userFills': self.handle_my_trades, + 'webData2': self.handle_ws_tickers, + 'post': self.handle_ws_post, + 'subscriptionResponse': self.handle_subscription_response, + } + exacMethod = self.safe_value(methods, topic) + if exacMethod is not None: + exacMethod(client, message) + return + keys = list(methods.keys()) + for i in range(0, len(keys)): + key = keys[i] + if topic.find(keys[i]) >= 0: + method = methods[key] + method(client, message) + return + + def ping(self, client: Client): + return { + 'method': 'ping', + } + + def handle_pong(self, client: Client, message): + # + # { + # "channel": "pong" + # } + # + client.lastPong = self.safe_integer(message, 'pong') + return message + + def request_id(self) -> float: + requestId = self.sum(self.safe_integer(self.options, 'requestId', 0), 1) + self.options['requestId'] = requestId + return requestId + + def wrap_as_post_action(self, request: dict) -> dict: + requestId = self.request_id() + return { + 'requestId': requestId, + 'request': { + 'method': 'post', + 'id': requestId, + 'request': { + 'type': 'action', + 'payload': request, + }, + }, + } diff --git a/ccxt/pro/independentreserve.py b/ccxt/pro/independentreserve.py new file mode 100644 index 0000000..11cd685 --- /dev/null +++ b/ccxt/pro/independentreserve.py @@ -0,0 +1,274 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCache +from ccxt.base.types import Any, Int, OrderBook, Trade +from ccxt.async_support.base.ws.client import Client +from typing import List +from ccxt.base.errors import NotSupported +from ccxt.base.errors import ChecksumError + + +class independentreserve(ccxt.async_support.independentreserve): + + def describe(self) -> Any: + return self.deep_extend(super(independentreserve, self).describe(), { + 'has': { + 'ws': True, + 'watchBalance': False, + 'watchTicker': False, + 'watchTickers': False, + 'watchTrades': True, + 'watchTradesForSymbols': False, + 'watchMyTrades': False, + 'watchOrders': False, + 'watchOrderBook': True, + 'watchOHLCV': False, + }, + 'urls': { + 'api': { + 'ws': 'wss://websockets.independentreserve.com', + }, + }, + 'options': { + 'watchOrderBook': { + 'checksum': True, # TODO: currently only working for snapshot + }, + }, + 'streaming': { + }, + 'exceptions': { + }, + }) + + async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + url = self.urls['api']['ws'] + '?subscribe=ticker-' + market['base'] + '-' + market['quote'] + messageHash = 'trades:' + symbol + trades = await self.watch(url, messageHash, None, messageHash) + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + def handle_trades(self, client: Client, message): + # + # { + # "Channel": "ticker-btc-usd", + # "Nonce": 130, + # "Data": { + # "TradeGuid": "7a669f2a-d564-472b-8493-6ef982eb1e96", + # "Pair": "btc-aud", + # "TradeDate": "2023-02-12T10:04:13.0804889+11:00", + # "Price": 31640, + # "Volume": 0.00079029, + # "BidGuid": "ba8a78b5-be69-4d33-92bb-9df0daa6314e", + # "OfferGuid": "27d20270-f21f-4c25-9905-152e70b2f6ec", + # "Side": "Buy" + # }, + # "Time": 1676156653111, + # "Event": "Trade" + # } + # + data = self.safe_value(message, 'Data', {}) + marketId = self.safe_string(data, 'Pair') + symbol = self.safe_symbol(marketId, None, '-') + messageHash = 'trades:' + symbol + stored = self.safe_value(self.trades, symbol) + if stored is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + stored = ArrayCache(limit) + self.trades[symbol] = stored + trade = self.parse_ws_trade(data) + stored.append(trade) + self.trades[symbol] = stored + client.resolve(self.trades[symbol], messageHash) + + def parse_ws_trade(self, trade, market=None): + # + # { + # "TradeGuid": "2f316718-0d0b-4e33-a30c-c2c06f3cfb34", + # "Pair": "xbt-aud", + # "TradeDate": "2023-02-12T09:22:35.4207494+11:00", + # "Price": 31573.8, + # "Volume": 0.05, + # "BidGuid": "adb63d74-4c02-47f9-9cc3-f287e3b48ab6", + # "OfferGuid": "b94d9bc4-addd-4633-a18f-69cf7e1b6f47", + # "Side": "Buy" + # } + # + datetime = self.safe_string(trade, 'TradeDate') + marketId = self.safe_string(market, 'Pair') + return self.safe_trade({ + 'info': trade, + 'id': self.safe_string(trade, 'TradeGuid'), + 'order': self.safe_string(trade, 'orderNo'), + 'symbol': self.safe_symbol(marketId, market, '-'), + 'side': self.safe_string_lower(trade, 'Side'), + 'type': None, + 'takerOrMaker': None, + 'price': self.safe_string(trade, 'Price'), + 'amount': self.safe_string(trade, 'Volume'), + 'cost': None, + 'fee': None, + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + }, market) + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + if limit is None: + limit = 100 + limitString = self.number_to_string(limit) + url = self.urls['api']['ws'] + '/orderbook/' + limitString + '?subscribe=' + market['base'] + '-' + market['quote'] + messageHash = 'orderbook:' + symbol + ':' + limitString + subscription: dict = { + 'receivedSnapshot': False, + } + orderbook = await self.watch(url, messageHash, None, messageHash, subscription) + return orderbook.limit() + + def handle_order_book(self, client: Client, message): + # + # { + # "Channel": "orderbook/1/eth/aud", + # "Data": { + # "Bids": [ + # { + # "Price": 2198.09, + # "Volume": 0.16143952, + # }, + # ], + # "Offers": [ + # { + # "Price": 2201.25, + # "Volume": 15, + # }, + # ], + # "Crc32": 1519697650, + # }, + # "Time": 1676150558254, + # "Event": "OrderBookSnapshot", + # } + # + event = self.safe_string(message, 'Event') + channel = self.safe_string(message, 'Channel') + parts = channel.split('/') + depth = self.safe_string(parts, 1) + baseId = self.safe_string(parts, 2) + quoteId = self.safe_string(parts, 3) + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + symbol = base + '/' + quote + orderBook = self.safe_dict(message, 'Data', {}) + messageHash = 'orderbook:' + symbol + ':' + depth + subscription = self.safe_value(client.subscriptions, messageHash, {}) + receivedSnapshot = self.safe_bool(subscription, 'receivedSnapshot', False) + timestamp = self.safe_integer(message, 'Time') + # orderbook = self.safe_value(self.orderbooks, symbol) + if not (symbol in self.orderbooks): + self.orderbooks[symbol] = self.order_book({}) + orderbook = self.orderbooks[symbol] + if event == 'OrderBookSnapshot': + snapshot = self.parse_order_book(orderBook, symbol, timestamp, 'Bids', 'Offers', 'Price', 'Volume') + orderbook.reset(snapshot) + subscription['receivedSnapshot'] = True + else: + asks = self.safe_list(orderBook, 'Offers', []) + bids = self.safe_list(orderBook, 'Bids', []) + self.handle_deltas(orderbook['asks'], asks) + self.handle_deltas(orderbook['bids'], bids) + orderbook['timestamp'] = timestamp + orderbook['datetime'] = self.iso8601(timestamp) + checksum = self.handle_option('watchOrderBook', 'checksum', True) + if checksum and receivedSnapshot: + storedAsks = orderbook['asks'] + storedBids = orderbook['bids'] + asksLength = len(storedAsks) + bidsLength = len(storedBids) + payload = '' + for i in range(0, 10): + if i < bidsLength: + payload = payload + self.value_to_checksum(storedBids[i][0]) + self.value_to_checksum(storedBids[i][1]) + for i in range(0, 10): + if i < asksLength: + payload = payload + self.value_to_checksum(storedAsks[i][0]) + self.value_to_checksum(storedAsks[i][1]) + calculatedChecksum = self.crc32(payload, True) + responseChecksum = self.safe_integer(orderBook, 'Crc32') + if calculatedChecksum != responseChecksum: + error = ChecksumError(self.id + ' ' + self.orderbook_checksum_message(symbol)) + del client.subscriptions[messageHash] + del self.orderbooks[symbol] + client.reject(error, messageHash) + return + if receivedSnapshot: + client.resolve(orderbook, messageHash) + + def value_to_checksum(self, value): + result = format(value, '.8f') + result = result.replace('.', '') + # remove leading zeros + result = self.parse_number(result) + result = self.number_to_string(result) + return result + + def handle_delta(self, bookside, delta): + bidAsk = self.parse_bid_ask(delta, 'Price', 'Volume') + bookside.storeArray(bidAsk) + + def handle_deltas(self, bookside, deltas): + for i in range(0, len(deltas)): + self.handle_delta(bookside, deltas[i]) + + def handle_heartbeat(self, client: Client, message): + # + # { + # "Time": 1676156208182, + # "Event": "Heartbeat" + # } + # + return message + + def handle_subscriptions(self, client: Client, message): + # + # { + # "Data": ["ticker-btc-sgd"], + # "Time": 1676157556223, + # "Event": "Subscriptions" + # } + # + return message + + def handle_message(self, client: Client, message): + event = self.safe_string(message, 'Event') + handlers: dict = { + 'Subscriptions': self.handle_subscriptions, + 'Heartbeat': self.handle_heartbeat, + 'Trade': self.handle_trades, + 'OrderBookSnapshot': self.handle_order_book, + 'OrderBookChange': self.handle_order_book, + } + handler = self.safe_value(handlers, event) + if handler is not None: + handler(client, message) + return + raise NotSupported(self.id + ' received an unsupported message: ' + self.json(message)) diff --git a/ccxt/pro/kraken.py b/ccxt/pro/kraken.py new file mode 100644 index 0000000..67853bc --- /dev/null +++ b/ccxt/pro/kraken.py @@ -0,0 +1,1517 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolById, ArrayCacheByTimestamp +from ccxt.base.types import Any, Balances, Bool, Int, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade +from ccxt.async_support.base.ws.client import Client +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import AccountSuspended +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import NotSupported +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.errors import ChecksumError +from ccxt.base.precise import Precise + + +class kraken(ccxt.async_support.kraken): + + def describe(self) -> Any: + return self.deep_extend(super(kraken, self).describe(), { + 'has': { + 'ws': True, + 'watchBalance': True, + 'watchMyTrades': True, + 'watchOHLCV': True, + 'watchOrderBook': True, + 'watchOrderBookForSymbols': True, + 'watchOrders': True, + 'watchTicker': True, + 'watchTickers': True, + 'watchBidsAsks': True, + 'watchTrades': True, + 'watchTradesForSymbols': True, + 'createOrderWs': True, + 'editOrderWs': True, + 'cancelOrderWs': True, + 'cancelOrdersWs': True, + 'cancelAllOrdersWs': True, + # 'watchHeartbeat': True, + # 'watchStatus': True, + }, + 'urls': { + 'api': { + 'ws': { + 'public': 'wss://ws.kraken.com', + 'private': 'wss://ws-auth.kraken.com', + 'privateV2': 'wss://ws-auth.kraken.com/v2', + 'publicV2': 'wss://ws.kraken.com/v2', + 'beta': 'wss://beta-ws.kraken.com', + 'beta-private': 'wss://beta-ws-auth.kraken.com', + }, + }, + }, + # 'versions': { + # 'ws': '0.2.0', + # }, + 'options': { + 'tradesLimit': 1000, + 'OHLCVLimit': 1000, + 'ordersLimit': 1000, + 'symbolsByOrderId': {}, + 'watchOrderBook': { + 'checksum': False, + }, + }, + 'streaming': { + 'ping': self.ping, + 'keepAlive': 6000, + }, + 'exceptions': { + 'ws': { + 'exact': { + 'Event(s) not found': BadRequest, + }, + 'broad': { + 'Already subscribed': BadRequest, + 'Currency pair not in ISO 4217-A3 format': BadSymbol, + 'Currency pair not supported': BadSymbol, + 'Malformed request': BadRequest, + 'Pair field must be an array': BadRequest, + 'Pair field unsupported for self subscription type': BadRequest, + 'Pair(s) not found': BadSymbol, + 'Subscription book depth must be an integer': BadRequest, + 'Subscription depth not supported': BadRequest, + 'Subscription field must be an object': BadRequest, + 'Subscription name invalid': BadRequest, + 'Subscription object unsupported field': BadRequest, + 'Subscription ohlc interval must be an integer': BadRequest, + 'Subscription ohlc interval not supported': BadRequest, + 'Subscription ohlc requires interval': BadRequest, + 'EAccount:Invalid permissions': PermissionDenied, + 'EAuth:Account temporary disabled': AccountSuspended, + 'EAuth:Account unconfirmed': AuthenticationError, + 'EAuth:Rate limit exceeded': RateLimitExceeded, + 'EAuth:Too many requests': RateLimitExceeded, + 'EDatabase: Internal error(to be deprecated)': ExchangeError, + 'EGeneral:Internal error[:]': ExchangeError, + 'EGeneral:Invalid arguments': BadRequest, + 'EOrder:Cannot open opposing position': InvalidOrder, + 'EOrder:Cannot open position': InvalidOrder, + 'EOrder:Insufficient funds(insufficient user funds)': InsufficientFunds, + 'EOrder:Insufficient margin(exchange does not have sufficient funds to allow margin trading)': InsufficientFunds, + 'EOrder:Invalid price': InvalidOrder, + 'EOrder:Margin allowance exceeded': InvalidOrder, + 'EOrder:Margin level too low': InvalidOrder, + 'EOrder:Margin position size exceeded(client would exceed the maximum position size for self pair)': InvalidOrder, + 'EOrder:Order minimum not met(volume too low)': InvalidOrder, + 'EOrder:Orders limit exceeded': InvalidOrder, + 'EOrder:Positions limit exceeded': InvalidOrder, + 'EOrder:Rate limit exceeded': RateLimitExceeded, + 'EOrder:Scheduled orders limit exceeded': InvalidOrder, + 'EOrder:Unknown position': OrderNotFound, + 'EOrder:Unknown order': OrderNotFound, + 'EOrder:Invalid order': InvalidOrder, + 'EService:Deadline elapsed': ExchangeNotAvailable, + 'EService:Market in cancel_only mode': NotSupported, + 'EService:Market in limit_only mode': NotSupported, + 'EService:Market in post_only mode': NotSupported, + 'EService:Unavailable': ExchangeNotAvailable, + 'ETrade:Invalid request': BadRequest, + 'ESession:Invalid session': AuthenticationError, + }, + }, + }, + }) + + def order_request_ws(self, method: str, symbol: str, type: str, request: dict, amount: Num, price: Num = None, params={}): + isLimitOrder = type.endswith('limit') # supporting limit, stop-loss-limit, take-profit-limit, etc + if isLimitOrder: + if price is None: + raise ArgumentsRequired(self.id + ' limit orders require a price argument') + request['params']['limit_price'] = self.parse_to_numeric(self.price_to_precision(symbol, price)) + isMarket = (type == 'market') + postOnly = None + postOnly, params = self.handle_post_only(isMarket, False, params) + if postOnly: + request['params']['post_only'] = True + clientOrderId = self.safe_string(params, 'clientOrderId') + if clientOrderId is not None: + request['params']['cl_ord_id'] = clientOrderId + cost = self.safe_string(params, 'cost') + if cost is not None: + request['params']['order_qty'] = self.parse_to_numeric(self.cost_to_precision(symbol, cost)) + stopLoss = self.safe_dict(params, 'stopLoss', {}) + takeProfit = self.safe_dict(params, 'takeProfit', {}) + presetStopLoss = self.safe_string(stopLoss, 'triggerPrice') + presetTakeProfit = self.safe_string(takeProfit, 'triggerPrice') + presetStopLossLimit = self.safe_string(stopLoss, 'price') + presetTakeProfitLimit = self.safe_string(takeProfit, 'price') + isPresetStopLoss = presetStopLoss is not None + isPresetTakeProfit = presetTakeProfit is not None + stopLossPrice = self.safe_string(params, 'stopLossPrice') + takeProfitPrice = self.safe_string(params, 'takeProfitPrice') + isStopLossPriceOrder = stopLossPrice is not None + isTakeProfitPriceOrder = takeProfitPrice is not None + trailingAmount = self.safe_string(params, 'trailingAmount') + trailingPercent = self.safe_string(params, 'trailingPercent') + trailingLimitAmount = self.safe_string(params, 'trailingLimitAmount') + trailingLimitPercent = self.safe_string(params, 'trailingLimitPercent') + isTrailingAmountOrder = trailingAmount is not None + isTrailingPercentOrder = trailingPercent is not None + isTrailingLimitAmountOrder = trailingLimitAmount is not None + isTrailingLimitPercentOrder = trailingLimitPercent is not None + offset = self.safe_string(params, 'offset', '') # can set self to - for minus + trailingAmountString = offset + self.number_to_string(trailingAmount) if (trailingAmount is not None) else None + trailingPercentString = offset + self.number_to_string(trailingPercent) if (trailingPercent is not None) else None + trailingLimitAmountString = offset + self.number_to_string(trailingLimitAmount) if (trailingLimitAmount is not None) else None + trailingLimitPercentString = offset + self.number_to_string(trailingLimitPercent) if (trailingLimitPercent is not None) else None + priceType = 'pct' if (isTrailingPercentOrder or isTrailingLimitPercentOrder) else 'quote' + if method == 'createOrderWs': + reduceOnly = self.safe_bool(params, 'reduceOnly') + if reduceOnly: + request['params']['reduce_only'] = True + timeInForce = self.safe_string_lower(params, 'timeInForce') + if timeInForce is not None: + request['params']['time_in_force'] = timeInForce + params = self.omit(params, ['reduceOnly', 'timeInForce']) + if isStopLossPriceOrder or isTakeProfitPriceOrder or isTrailingAmountOrder or isTrailingPercentOrder or isTrailingLimitAmountOrder or isTrailingLimitPercentOrder: + request['params']['triggers'] = {} + if isPresetStopLoss or isPresetTakeProfit: + request['params']['conditional'] = {} + if isPresetStopLoss: + request['params']['conditional']['order_type'] = 'stop-loss' + request['params']['conditional']['trigger_price'] = self.parse_to_numeric(self.price_to_precision(symbol, presetStopLoss)) + elif isPresetTakeProfit: + request['params']['conditional']['order_type'] = 'take-profit' + request['params']['conditional']['trigger_price'] = self.parse_to_numeric(self.price_to_precision(symbol, presetTakeProfit)) + if presetStopLossLimit is not None: + request['params']['conditional']['order_type'] = 'stop-loss-limit' + request['params']['conditional']['limit_price'] = self.parse_to_numeric(self.price_to_precision(symbol, presetStopLossLimit)) + elif presetTakeProfitLimit is not None: + request['params']['conditional']['order_type'] = 'take-profit-limit' + request['params']['conditional']['limit_price'] = self.parse_to_numeric(self.price_to_precision(symbol, presetTakeProfitLimit)) + params = self.omit(params, ['stopLoss', 'takeProfit']) + elif isStopLossPriceOrder or isTakeProfitPriceOrder: + if isStopLossPriceOrder: + request['params']['triggers']['price'] = self.parse_to_numeric(self.price_to_precision(symbol, stopLossPrice)) + if isLimitOrder: + request['params']['order_type'] = 'stop-loss-limit' + else: + request['params']['order_type'] = 'stop-loss' + else: + request['params']['triggers']['price'] = self.parse_to_numeric(self.price_to_precision(symbol, takeProfitPrice)) + if isLimitOrder: + request['params']['order_type'] = 'take-profit-limit' + else: + request['params']['order_type'] = 'take-profit' + elif isTrailingAmountOrder or isTrailingPercentOrder or isTrailingLimitAmountOrder or isTrailingLimitPercentOrder: + request['params']['triggers']['price_type'] = priceType + if not isLimitOrder and (isTrailingAmountOrder or isTrailingPercentOrder): + request['params']['order_type'] = 'trailing-stop' + if isTrailingAmountOrder: + request['params']['triggers']['price'] = self.parse_to_numeric(trailingAmountString) + else: + request['params']['triggers']['price'] = self.parse_to_numeric(trailingPercentString) + else: + # trailing limit orders are not conventionally supported because the static limit_price_type param is not available for trailing-stop-limit orders + request['params']['limit_price_type'] = priceType + request['params']['order_type'] = 'trailing-stop-limit' + if isTrailingLimitAmountOrder: + request['params']['triggers']['price'] = self.parse_to_numeric(trailingLimitAmountString) + else: + request['params']['triggers']['price'] = self.parse_to_numeric(trailingLimitPercentString) + elif method == 'editOrderWs': + if isPresetStopLoss or isPresetTakeProfit: + raise NotSupported(self.id + ' editing the stopLoss and takeProfit on existing orders is currently not supported') + if isStopLossPriceOrder or isTakeProfitPriceOrder: + if isStopLossPriceOrder: + request['params']['trigger_price'] = self.parse_to_numeric(self.price_to_precision(symbol, stopLossPrice)) + else: + request['params']['trigger_price'] = self.parse_to_numeric(self.price_to_precision(symbol, takeProfitPrice)) + elif isTrailingAmountOrder or isTrailingPercentOrder or isTrailingLimitAmountOrder or isTrailingLimitPercentOrder: + request['params']['trigger_price_type'] = priceType + if not isLimitOrder and (isTrailingAmountOrder or isTrailingPercentOrder): + if isTrailingAmountOrder: + request['params']['trigger_price'] = self.parse_to_numeric(trailingAmountString) + else: + request['params']['trigger_price'] = self.parse_to_numeric(trailingPercentString) + else: + request['params']['limit_price_type'] = priceType + if isTrailingLimitAmountOrder: + request['params']['trigger_price'] = self.parse_to_numeric(trailingLimitAmountString) + else: + request['params']['trigger_price'] = self.parse_to_numeric(trailingLimitPercentString) + params = self.omit(params, ['clientOrderId', 'cost', 'offset', 'stopLossPrice', 'takeProfitPrice', 'trailingAmount', 'trailingPercent', 'trailingLimitAmount', 'trailingLimitPercent']) + return [request, params] + + async def create_order_ws(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}) -> Order: + """ + create a trade order + + https://docs.kraken.com/api/docs/websocket-v2/add_order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + token = await self.authenticate() + market = self.market(symbol) + url = self.urls['api']['ws']['privateV2'] + requestId = self.request_id() + messageHash = self.number_to_string(requestId) + request: dict = { + 'method': 'add_order', + 'params': { + 'order_type': type, + 'side': side, + 'order_qty': self.parse_to_numeric(self.amount_to_precision(symbol, amount)), + 'symbol': market['symbol'], + 'token': token, + }, + 'req_id': requestId, + } + request, params = self.order_request_ws('createOrderWs', symbol, type, request, amount, price, params) + return await self.watch(url, messageHash, self.extend(request, params), messageHash) + + def handle_create_edit_order(self, client, message): + # + # createOrder + # { + # "method": "add_order", + # "req_id": 1, + # "result": { + # "order_id": "OXM2QD-EALR2-YBAVEU" + # }, + # "success": True, + # "time_in": "2025-05-13T10:12:13.876173Z", + # "time_out": "2025-05-13T10:12:13.890137Z" + # } + # + # editOrder + # { + # "method": "amend_order", + # "req_id": 1, + # "result": { + # "amend_id": "TYDLSQ-OYNYU-3MNRER", + # "order_id": "OGL7HR-SWFO4-NRQTHO" + # }, + # "success": True, + # "time_in": "2025-05-14T13:54:10.840342Z", + # "time_out": "2025-05-14T13:54:10.855046Z" + # } + # + result = self.safe_dict(message, 'result', {}) + order = self.parse_order(result) + messageHash = self.safe_string_2(message, 'reqid', 'req_id') + client.resolve(order, messageHash) + + async def edit_order_ws(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}) -> Order: + """ + edit a trade order + + https://docs.kraken.com/api/docs/websocket-v2/amend_order + + :param str id: order id + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of the currency you want to trade in units of the base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + token = await self.authenticate() + url = self.urls['api']['ws']['privateV2'] + requestId = self.request_id() + messageHash = self.number_to_string(requestId) + request: dict = { + 'method': 'amend_order', + 'params': { + 'order_id': id, + 'order_qty': self.parse_to_numeric(self.amount_to_precision(symbol, amount)), + 'token': token, + }, + 'req_id': requestId, + } + request, params = self.order_request_ws('editOrderWs', symbol, type, request, amount, price, params) + return await self.watch(url, messageHash, self.extend(request, params), messageHash) + + async def cancel_orders_ws(self, ids: List[str], symbol: Str = None, params={}): + """ + cancel multiple orders + + https://docs.kraken.com/api/docs/websocket-v2/cancel_order + + :param str[] ids: order ids + :param str [symbol]: unified market symbol, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an list of `order structures ` + """ + if symbol is not None: + raise NotSupported(self.id + ' cancelOrdersWs() does not support cancelling orders for a specific symbol.') + await self.load_markets() + token = await self.authenticate() + url = self.urls['api']['ws']['privateV2'] + requestId = self.request_id() + messageHash = self.number_to_string(requestId) + request: dict = { + 'method': 'cancel_order', + 'params': { + 'order_id': ids, + 'token': token, + }, + 'req_id': requestId, + } + return await self.watch(url, messageHash, self.extend(request, params), messageHash) + + async def cancel_order_ws(self, id: str, symbol: Str = None, params={}) -> Order: + """ + cancels an open order + + https://docs.kraken.com/api/docs/websocket-v2/cancel_order + + :param str id: order id + :param str [symbol]: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + if symbol is not None: + raise NotSupported(self.id + ' cancelOrderWs() does not support cancelling orders for a specific symbol.') + await self.load_markets() + token = await self.authenticate() + url = self.urls['api']['ws']['privateV2'] + requestId = self.request_id() + messageHash = self.number_to_string(requestId) + request: dict = { + 'method': 'cancel_order', + 'params': { + 'order_id': [id], + 'token': token, + }, + 'req_id': requestId, + } + return await self.watch(url, messageHash, self.extend(request, params), messageHash) + + def handle_cancel_order(self, client, message): + # + # { + # "method": "cancel_order", + # "req_id": 123456789, + # "result": { + # "order_id": "OKAGJC-YHIWK-WIOZWG" + # }, + # "success": True, + # "time_in": "2023-09-21T14:36:57.428972Z", + # "time_out": "2023-09-21T14:36:57.437952Z" + # } + # + reqId = self.safe_string(message, 'req_id') + client.resolve(message, reqId) + + async def cancel_all_orders_ws(self, symbol: Str = None, params={}) -> List[Order]: + """ + cancel all open orders + + https://docs.kraken.com/api/docs/websocket-v2/cancel_all + + :param str [symbol]: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + if symbol is not None: + raise NotSupported(self.id + ' cancelAllOrdersWs() does not support cancelling orders in a specific market.') + await self.load_markets() + token = await self.authenticate() + url = self.urls['api']['ws']['privateV2'] + requestId = self.request_id() + messageHash = self.number_to_string(requestId) + request: dict = { + 'method': 'cancel_all', + 'params': { + 'token': token, + }, + 'req_id': requestId, + } + return await self.watch(url, messageHash, self.extend(request, params), messageHash) + + def handle_cancel_all_orders(self, client, message): + # + # { + # "method": "cancel_all", + # "req_id": 123456789, + # "result": { + # "count": 1 + # }, + # "success": True, + # "time_in": "2023-09-21T14:36:57.428972Z", + # "time_out": "2023-09-21T14:36:57.437952Z" + # } + # + reqId = self.safe_string(message, 'req_id') + client.resolve(message, reqId) + + def handle_ticker(self, client, message): + # + # { + # "channel": "ticker", + # "type": "snapshot", + # "data": [ + # { + # "symbol": "BTC/USD", + # "bid": 108359.8, + # "bid_qty": 0.01362603, + # "ask": 108359.9, + # "ask_qty": 17.17988863, + # "last": 108359.8, + # "volume": 2158.32346723, + # "vwap": 108894.5, + # "low": 106824, + # "high": 111300, + # "change": -2679.9, + # "change_pct": -2.41 + # } + # ] + # } + # + data = self.safe_list(message, 'data', []) + ticker = data[0] + symbol = self.safe_string(ticker, 'symbol') + messageHash = self.get_message_hash('ticker', None, symbol) + vwap = self.safe_string(ticker, 'vwap') + quoteVolume = None + baseVolume = self.safe_string(ticker, 'volume') + if baseVolume is not None and vwap is not None: + quoteVolume = Precise.string_mul(baseVolume, vwap) + last = self.safe_string(ticker, 'last') + result = self.safe_ticker({ + 'symbol': symbol, + 'timestamp': None, + 'datetime': None, + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': self.safe_string(ticker, 'bid'), + 'bidVolume': self.safe_string(ticker, 'bid_qty'), + 'ask': self.safe_string(ticker, 'ask'), + 'askVolume': self.safe_string(ticker, 'ask_qty'), + 'vwap': vwap, + 'open': None, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': self.safe_string(ticker, 'change'), + 'percentage': self.safe_string(ticker, 'change_pct'), + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'info': ticker, + }) + self.tickers[symbol] = result + client.resolve(result, messageHash) + + def handle_trades(self, client: Client, message): + # + # { + # "channel": "trade", + # "type": "update", + # "data": [ + # { + # "symbol": "MATIC/USD", + # "side": "sell", + # "price": 0.5117, + # "qty": 40.0, + # "ord_type": "market", + # "trade_id": 4665906, + # "timestamp": "2023-09-25T07:49:37.708706Z" + # } + # ] + # } + # + data = self.safe_list(message, 'data', []) + trade = data[0] + symbol = self.safe_string(trade, 'symbol') + messageHash = self.get_message_hash('trade', None, symbol) + stored = self.safe_value(self.trades, symbol) + if stored is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + stored = ArrayCache(limit) + self.trades[symbol] = stored + market = self.market(symbol) + parsed = self.parse_trades(data, market) + for i in range(0, len(parsed)): + stored.append(parsed[i]) + client.resolve(stored, messageHash) + + def handle_ohlcv(self, client: Client, message): + # + # { + # "channel": "ohlc", + # "type": "update", + # "timestamp": "2023-10-04T16:26:30.524394914Z", + # "data": [ + # { + # "symbol": "MATIC/USD", + # "open": 0.5624, + # "high": 0.5628, + # "low": 0.5622, + # "close": 0.5627, + # "trades": 12, + # "volume": 30927.68066226, + # "vwap": 0.5626, + # "interval_begin": "2023-10-04T16:25:00.000000000Z", + # "interval": 5, + # "timestamp": "2023-10-04T16:30:00.000000Z" + # } + # ] + # } + # + data = self.safe_list(message, 'data', []) + first = data[0] + marketId = self.safe_string(first, 'symbol') + symbol = self.safe_symbol(marketId) + if not (symbol in self.ohlcvs): + self.ohlcvs[symbol] = {} + interval = self.safe_integer(first, 'interval') + timeframe = self.find_timeframe(interval) + messageHash = self.get_message_hash('ohlcv', None, symbol) + stored = self.safe_value(self.ohlcvs[symbol], timeframe) + self.ohlcvs[symbol] = self.safe_value(self.ohlcvs, symbol, {}) + if stored is None: + limit = self.safe_integer(self.options, 'OHLCVLimit', 1000) + stored = ArrayCacheByTimestamp(limit) + self.ohlcvs[symbol][timeframe] = stored + ohlcvsLength = len(data) + for i in range(0, ohlcvsLength): + candle = data[ohlcvsLength - i - 1] + datetime = self.safe_string(candle, 'timestamp') + timestamp = self.parse8601(datetime) + parsed = [ + timestamp, + self.safe_string(candle, 'open'), + self.safe_string(candle, 'high'), + self.safe_string(candle, 'low'), + self.safe_string(candle, 'close'), + self.safe_string(candle, 'volume'), + ] + stored.append(parsed) + client.resolve(stored, messageHash) + + def request_id(self): + # their support said that reqid must be an int32, not documented + reqid = self.sum(self.safe_integer(self.options, 'reqid', 0), 1) + self.options['reqid'] = reqid + return reqid + + async def watch_ticker(self, symbol: str, params={}) -> Ticker: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://docs.kraken.com/api/docs/websocket-v2/ticker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbol = self.symbol(symbol) + tickers = await self.watch_tickers([symbol], params) + return tickers[symbol] + + async def watch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://docs.kraken.com/api/docs/websocket-v2/ticker + + :param str[] symbols: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, False) + ticker = await self.watch_multi_helper('ticker', 'ticker', symbols, None, params) + if self.newUpdates: + result: dict = {} + result[ticker['symbol']] = ticker + return result + return self.filter_by_array(self.tickers, 'symbol', symbols) + + async def watch_bids_asks(self, symbols: Strings = None, params={}) -> Tickers: + """ + watches best bid & ask for symbols + + https://docs.kraken.com/api/docs/websocket-v2/ticker + + :param str[] symbols: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, False) + params['event_trigger'] = 'bbo' + ticker = await self.watch_multi_helper('bidask', 'ticker', symbols, None, params) + if self.newUpdates: + result: dict = {} + result[ticker['symbol']] = ticker + return result + return self.filter_by_array(self.bidsasks, 'symbol', symbols) + + async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://docs.kraken.com/api/docs/websocket-v2/trade + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + return await self.watch_trades_for_symbols([symbol], since, limit, params) + + async def watch_trades_for_symbols(self, symbols: List[str], since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a list of symbols + + https://docs.kraken.com/api/docs/websocket-v2/trade + + :param str[] symbols: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + trades = await self.watch_multi_helper('trade', 'trade', symbols, None, params) + if self.newUpdates: + first = self.safe_list(trades, 0) + tradeSymbol = self.safe_string(first, 'symbol') + limit = trades.getLimit(tradeSymbol, limit) + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://docs.kraken.com/api/docs/websocket-v2/book + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + return await self.watch_order_book_for_symbols([symbol], limit, params) + + async def watch_order_book_for_symbols(self, symbols: List[str], limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://docs.kraken.com/api/docs/websocket-v2/book + + :param str[] symbols: unified array of symbols + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + requiredParams: dict = {} + if limit is not None: + if self.in_array(limit, [10, 25, 100, 500, 1000]): + requiredParams['depth'] = limit # default 10, valid options 10, 25, 100, 500, 1000 + else: + raise NotSupported(self.id + ' watchOrderBook accepts limit values of 10, 25, 100, 500 and 1000 only') + orderbook = await self.watch_multi_helper('orderbook', 'book', symbols, {'limit': limit}, self.extend(requiredParams, params)) + return orderbook.limit() + + async def watch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + watches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://docs.kraken.com/api/docs/websocket-v2/ohlc + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + name = 'ohlc' + market = self.market(symbol) + symbol = market['symbol'] + url = self.urls['api']['ws']['publicV2'] + requestId = self.request_id() + messageHash = self.get_message_hash('ohlcv', None, symbol) + subscribe: dict = { + 'method': 'subscribe', + 'params': { + 'channel': name, + 'symbol': [symbol], + 'interval': self.safe_value(self.timeframes, timeframe, timeframe), + }, + 'req_id': requestId, + } + request = self.deep_extend(subscribe, params) + ohlcv = await self.watch(url, messageHash, request, messageHash) + if self.newUpdates: + limit = ohlcv.getLimit(symbol, limit) + return self.filter_by_since_limit(ohlcv, since, limit, 'timestamp', True) + + async def load_markets(self, reload=False, params={}): + markets = await super(kraken, self).load_markets(reload, params) + marketsByWsName = self.safe_value(self.options, 'marketsByWsName') + if (marketsByWsName is None) or reload: + marketsByWsName = {} + for i in range(0, len(self.symbols)): + symbol = self.symbols[i] + market = self.markets[symbol] + info = self.safe_value(market, 'info', {}) + wsName = self.safe_string(info, 'wsname') + marketsByWsName[wsName] = market + self.options['marketsByWsName'] = marketsByWsName + return markets + + def ping(self, client: Client): + url = client.url + request = {} + if url.find('v2') >= 0: + request['method'] = 'ping' + else: + request['event'] = 'ping' + return request + + def handle_pong(self, client: Client, message): + client.lastPong = self.milliseconds() + return message + + async def watch_heartbeat(self, params={}): + await self.load_markets() + event = 'heartbeat' + url = self.urls['api']['ws']['publicV2'] + return await self.watch(url, event) + + def handle_heartbeat(self, client: Client, message): + # + # every second(approx) if no other updates are sent + # + # {"channel": "heartbeat"} + # + event = self.safe_string(message, 'channel') + client.resolve(message, event) + + def handle_order_book(self, client: Client, message): + # + # first message(snapshot) + # + # { + # "channel": "book", + # "type": "snapshot", + # "data": [ + # { + # "symbol": "MATIC/USD", + # "bids": [ + # { + # "price": 0.5666, + # "qty": 4831.75496356 + # }, + # { + # "price": 0.5665, + # "qty": 6658.22734739 + # } + # ], + # "asks": [ + # { + # "price": 0.5668, + # "qty": 4410.79769741 + # }, + # { + # "price": 0.5669, + # "qty": 4655.40412487 + # } + # ], + # "checksum": 2439117997 + # } + # ] + # } + # + # subsequent updates + # + # { + # "channel": "book", + # "type": "update", + # "data": [ + # { + # "symbol": "MATIC/USD", + # "bids": [ + # { + # "price": 0.5657, + # "qty": 1098.3947558 + # } + # ], + # "asks": [], + # "checksum": 2114181697, + # "timestamp": "2023-10-06T17:35:55.440295Z" + # } + # ] + # } + # + type = self.safe_string(message, 'type') + data = self.safe_list(message, 'data', []) + first = self.safe_dict(data, 0, {}) + symbol = self.safe_string(first, 'symbol') + a = self.safe_value(first, 'asks', []) + b = self.safe_value(first, 'bids', []) + c = self.safe_integer(first, 'checksum') + messageHash = self.get_message_hash('orderbook', None, symbol) + orderbook = None + if type == 'update': + orderbook = self.orderbooks[symbol] + storedAsks = orderbook['asks'] + storedBids = orderbook['bids'] + if a is not None: + self.custom_handle_deltas(storedAsks, a) + if b is not None: + self.custom_handle_deltas(storedBids, b) + datetime = self.safe_string(first, 'timestamp') + orderbook['symbol'] = symbol + orderbook['timestamp'] = self.parse8601(datetime) + orderbook['datetime'] = datetime + else: + # snapshot + depth = len(a) + self.orderbooks[symbol] = self.order_book({}, depth) + orderbook = self.orderbooks[symbol] + keys = ['asks', 'bids'] + for i in range(0, len(keys)): + key = keys[i] + bookside = orderbook[key] + deltas = self.safe_value(first, key, []) + if len(deltas) > 0: + self.custom_handle_deltas(bookside, deltas) + orderbook['symbol'] = symbol + orderbook.limit() + # checksum temporarily disabled because the exchange checksum was not reliable + checksum = self.handle_option('watchOrderBook', 'checksum', False) + if checksum: + payloadArray = [] + if c is not None: + checkAsks = orderbook['asks'] + checkBids = orderbook['bids'] + # checkAsks = asks.map((elem) => [elem['price'], elem['qty']]) + # checkBids = bids.map((elem) => [elem['price'], elem['qty']]) + for i in range(0, 10): + currentAsk = self.safe_value(checkAsks, i, {}) + formattedAsk = self.format_number(currentAsk[0]) + self.format_number(currentAsk[1]) + payloadArray.append(formattedAsk) + for i in range(0, 10): + currentBid = self.safe_value(checkBids, i, {}) + formattedBid = self.format_number(currentBid[0]) + self.format_number(currentBid[1]) + payloadArray.append(formattedBid) + payload = ''.join(payloadArray) + localChecksum = self.crc32(payload, False) + if localChecksum != c: + error = ChecksumError(self.id + ' ' + self.orderbook_checksum_message(symbol)) + del client.subscriptions[messageHash] + del self.orderbooks[symbol] + client.reject(error, messageHash) + return + client.resolve(orderbook, messageHash) + + def custom_handle_deltas(self, bookside, deltas): + # sortOrder = True if (key == 'bids') else False + for j in range(0, len(deltas)): + delta = deltas[j] + price = self.safe_number(delta, 'price') + amount = self.safe_number(delta, 'qty') + bookside.store(price, amount) + # if amount == 0: + # index = bookside.findIndex((x: Int) => x[0] == price) + # bookside.splice(index, 1) + # else: + # bookside.store(price, amount) + # } + # bookside = self.sort_by(bookside, 0, sortOrder) + # bookside[0:9] + + def format_number(self, data): + parts = data.split('.') + integer = self.safe_string(parts, 0) + decimals = self.safe_string(parts, 1, '') + joinedResult = integer + decimals + i = 0 + while(joinedResult[i] == '0'): + i += 1 + if i > 0: + joinedResult = joinedResult[i:] + return joinedResult + + def handle_system_status(self, client: Client, message): + # + # todo: answer the question whether handleSystemStatus should be renamed + # and unified for any usage pattern that + # involves system status and maintenance updates + # + # { + # "connectionID": 15527282728335292000, + # "event": "systemStatus", + # "status": "online", # online|maintenance|(custom status tbd) + # "version": "0.2.0" + # } + # + # v2 + # { + # channel: 'status', + # type: 'update', + # data: [ + # { + # version: '2.0.10', + # system: 'online', + # api_version: 'v2', + # connection_id: 6447481662169813000 + # } + # ] + # } + # + return message + + async def authenticate(self, params={}): + url = self.urls['api']['ws']['private'] + client = self.client(url) + authenticated = 'authenticated' + subscription = self.safe_value(client.subscriptions, authenticated) + now = self.seconds() + start = self.safe_integer(subscription, 'start') + expires = self.safe_integer(subscription, 'expires') + if (subscription is None) or ((subscription is not None) and (start + expires) <= now): + # https://docs.kraken.com/api/docs/rest-api/get-websockets-token + response = await self.privatePostGetWebSocketsToken(params) + # + # { + # "error":[], + # "result":{ + # "token":"xeAQ\/RCChBYNVh53sTv1yZ5H4wIbwDF20PiHtTF+4UI", + # "expires":900 + # } + # } + # + subscription = self.safe_dict(response, 'result') + subscription['start'] = now + client.subscriptions[authenticated] = subscription + return self.safe_string(subscription, 'token') + + async def watch_private(self, name, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + await self.load_markets() + token = await self.authenticate() + subscriptionHash = 'executions' + messageHash = name + if symbol is not None: + symbol = self.symbol(symbol) + messageHash += ':' + symbol + url = self.urls['api']['ws']['privateV2'] + requestId = self.request_id() + subscribe: dict = { + 'method': 'subscribe', + 'params': { + 'channel': 'executions', + 'token': token, + }, + 'req_id': requestId, + } + if params is not None: + subscribe['params'] = self.deep_extend(subscribe['params'], params) + result = await self.watch(url, messageHash, subscribe, subscriptionHash) + if self.newUpdates: + limit = result.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(result, symbol, since, limit) + + async def watch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + watches information on multiple trades made by the user + + https://docs.kraken.com/api/docs/websocket-v2/executions + + :param str symbol: unified market symbol of the market trades were made in + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trade structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + params['snap_trades'] = True + return await self.watch_private('myTrades', symbol, since, limit, params) + + def handle_my_trades(self, client: Client, message, subscription=None): + # + # { + # "channel": "executions", + # "type": "update", + # "data": [ + # { + # "order_id": "O6NTZC-K6FRH-ATWBCK", + # "exec_id": "T5DIUI-5N4KO-Z5BPXK", + # "exec_type": "trade", + # "trade_id": 8253473, + # "symbol": "USDC/USD", + # "side": "sell", + # "last_qty": 15.44, + # "last_price": 1.0002, + # "liquidity_ind": "t", + # "cost": 15.443088, + # "order_userref": 0, + # "order_status": "filled", + # "order_type": "market", + # "fee_usd_equiv": 0.03088618, + # "fees": [ + # { + # "asset": "USD", + # "qty": 0.3458 + # } + # ] + # } + # ], + # "sequence": 10 + # } + # + allTrades = self.safe_list(message, 'data', []) + allTradesLength = len(allTrades) + if allTradesLength > 0: + if self.myTrades is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + self.myTrades = ArrayCache(limit) + stored = self.myTrades + symbols: dict = {} + for i in range(0, len(allTrades)): + trade = self.safe_dict(allTrades, i, {}) + parsed = self.parse_ws_trade(trade) + stored.append(parsed) + symbol = parsed['symbol'] + symbols[symbol] = True + name = 'myTrades' + client.resolve(self.myTrades, name) + keys = list(symbols.keys()) + for i in range(0, len(keys)): + messageHash = name + ':' + keys[i] + client.resolve(self.myTrades, messageHash) + + def parse_ws_trade(self, trade, market=None): + # + # { + # "order_id": "O6NTZC-K6FRH-ATWBCK", + # "exec_id": "T5DIUI-5N4KO-Z5BPXK", + # "exec_type": "trade", + # "trade_id": 8253473, + # "symbol": "USDC/USD", + # "side": "sell", + # "last_qty": 15.44, + # "last_price": 1.0002, + # "liquidity_ind": "t", + # "cost": 15.443088, + # "order_userref": 0, + # "order_status": "filled", + # "order_type": "market", + # "fee_usd_equiv": 0.03088618, + # "fees": [ + # { + # "asset": "USD", + # "qty": 0.3458 + # } + # ] + # } + # + symbol = self.safe_string(trade, 'symbol') + if market is not None: + symbol = market['symbol'] + fee = None + if 'fees' in trade: + fees = self.safe_list(trade, 'fees', []) + firstFee = self.safe_dict(fees, 0, {}) + fee = { + 'cost': self.safe_number(firstFee, 'qty'), + 'currency': self.safe_string(firstFee, 'asset'), + } + datetime = self.safe_string(trade, 'timestamp') + liquidityIndicator = self.safe_string(trade, 'liquidity_ind') + takerOrMaker = 'taker' if (liquidityIndicator == 't') else 'maker' + return { + 'info': trade, + 'id': self.safe_string(trade, 'exec_id'), + 'order': self.safe_string(trade, 'order_id'), + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + 'symbol': symbol, + 'type': self.safe_string(trade, 'order_type'), + 'side': self.safe_string(trade, 'side'), + 'takerOrMaker': takerOrMaker, + 'price': self.safe_number(trade, 'last_price'), + 'amount': self.safe_number(trade, 'last_qty'), + 'cost': self.safe_number(trade, 'cost'), + 'fee': fee, + } + + async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + watches information on multiple orders made by the user + + https://docs.kraken.com/api/docs/websocket-v2/executions + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of orde structures to retrieve + :param dict [params]: maximum number of orderic to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + return await self.watch_private('orders', symbol, since, limit, self.extend(params, {'snap_orders': True})) + + def handle_orders(self, client: Client, message, subscription=None): + # + # { + # "channel": "executions", + # "type": "update", + # "data": [ + # { + # "order_id": "OK4GJX-KSTLS-7DZZO5", + # "order_userref": 3, + # "symbol": "BTC/USD", + # "order_qty": 0.005, + # "cum_cost": 0.0, + # "time_in_force": "GTC", + # "exec_type": "pending_new", + # "side": "sell", + # "order_type": "limit", + # "limit_price_type": "static", + # "limit_price": 26500.0, + # "stop_price": 0.0, + # "order_status": "pending_new", + # "fee_usd_equiv": 0.0, + # "fee_ccy_pref": "fciq", + # "timestamp": "2023-09-22T10:33:05.709950Z" + # } + # ], + # "sequence": 8 + # } + # + allOrders = self.safe_list(message, 'data', []) + allOrdersLength = len(allOrders) + if allOrdersLength > 0: + limit = self.safe_integer(self.options, 'ordersLimit', 1000) + if self.orders is None: + self.orders = ArrayCacheBySymbolById(limit) + stored = self.orders + symbols: dict = {} + for i in range(0, len(allOrders)): + order = self.safe_dict(allOrders, i, {}) + id = self.safe_string(order, 'order_id') + parsed = self.parse_ws_order(order) + symbol = self.safe_string(order, 'symbol') + previousOrders = self.safe_value(stored.hashmap, symbol) + previousOrder = self.safe_value(previousOrders, id) + newOrder = parsed + if previousOrder is not None: + newRawOrder = self.extend(previousOrder['info'], newOrder['info']) + newOrder = self.parse_ws_order(newRawOrder) + length = len(stored) + if length == limit and (previousOrder is None): + first = stored[0] + symbolsByOrderId = self.safe_value(self.options, 'symbolsByOrderId', {}) + if first['id'] in symbolsByOrderId: + del symbolsByOrderId[first['id']] + stored.append(newOrder) + if symbol is not None: + symbols[symbol] = True + name = 'orders' + client.resolve(self.orders, name) + keys = list(symbols.keys()) + for i in range(0, len(keys)): + messageHash = name + ':' + keys[i] + client.resolve(self.orders, messageHash) + + def parse_ws_order(self, order, market=None): + # + # watchOrders + # + # open order + # { + # "order_id": "OK4GJX-KSTLS-7DZZO5", + # "order_userref": 3, + # "symbol": "BTC/USD", + # "order_qty": 0.005, + # "cum_cost": 0.0, + # "time_in_force": "GTC", + # "exec_type": "pending_new", + # "side": "sell", + # "order_type": "limit", + # "limit_price_type": "static", + # "limit_price": 26500.0, + # "stop_price": 0.0, + # "order_status": "pending_new", + # "fee_usd_equiv": 0.0, + # "fee_ccy_pref": "fciq", + # "timestamp": "2023-09-22T10:33:05.709950Z" + # } + # + # canceled order + # + # { + # "timestamp": "2025-10-11T15:11:47.695226Z", + # "order_status": "canceled", + # "exec_type": "canceled", + # "order_userref": 0, + # "order_id": "OGAB7Y-BKX5F-PTK5RW", + # "cum_qty": 0, + # "cum_cost": 0, + # "fee_usd_equiv": 0, + # "avg_price": 0, + # "cancel_reason": "User requested", + # "reason": "User requested" + # } + # + fee = { + 'cost': self.safe_string(order, 'fee_usd_equiv'), + 'currency': 'USD', + } + stopPrice = self.safe_string(order, 'stop_price') + datetime = self.safe_string(order, 'timestamp') + return self.safe_order({ + 'id': self.safe_string(order, 'order_id'), + 'clientOrderId': self.safe_string(order, 'order_userref'), + 'info': order, + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + 'lastTradeTimestamp': None, + 'status': self.parse_order_status(self.safe_string(order, 'order_status')), + 'symbol': self.safe_string(order, 'symbol'), + 'type': self.safe_string(order, 'order_type'), + 'timeInForce': self.safe_string(order, 'time_in_force'), + 'postOnly': None, + 'side': self.safe_string(order, 'side'), + 'price': self.safe_string(order, 'limit_price'), + 'stopPrice': stopPrice, + 'triggerPrice': stopPrice, + 'cost': self.safe_string(order, 'cum_cost'), + 'amount': self.safe_string_2(order, 'order_qty', 'cum_qty'), + 'filled': None, + 'average': self.safe_string(order, 'avg_price'), + 'remaining': None, + 'fee': fee, + 'trades': None, + }) + + async def watch_multi_helper(self, unifiedName: str, channelName: str, symbols: Strings = None, subscriptionArgs=None, params={}): + await self.load_markets() + # symbols are required + symbols = self.market_symbols(symbols, None, False, True, False) + messageHashes = [] + for i in range(0, len(symbols)): + eventTrigger = self.safe_string(params, 'event_trigger') + if eventTrigger is not None: + messageHashes.append(self.get_message_hash(channelName, None, self.symbol(symbols[i]))) + else: + messageHashes.append(self.get_message_hash(unifiedName, None, self.symbol(symbols[i]))) + request: dict = { + 'method': 'subscribe', + 'params': { + 'channel': channelName, + 'symbol': symbols, + }, + 'req_id': self.request_id(), + } + request['params'] = self.deep_extend(request['params'], params) + url = self.urls['api']['ws']['publicV2'] + return await self.watch_multiple(url, messageHashes, request, messageHashes, subscriptionArgs) + + async def watch_balance(self, params={}) -> Balances: + """ + watch balance and get the amount of funds available for trading or funds locked in orders + + https://docs.kraken.com/api/docs/websocket-v2/balances + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + token = await self.authenticate() + messageHash = 'balances' + url = self.urls['api']['ws']['privateV2'] + requestId = self.request_id() + subscribe: dict = { + 'method': 'subscribe', + 'req_id': requestId, + 'params': { + 'channel': 'balances', + 'token': token, + }, + } + request = self.deep_extend(subscribe, params) + return await self.watch(url, messageHash, request, messageHash) + + def handle_balance(self, client: Client, message): + # + # { + # "channel": "balances", + # "data": [ + # { + # "asset": "BTC", + # "asset_class": "currency", + # "balance": 1.2, + # "wallets": [ + # { + # "type": "spot", + # "id": "main", + # "balance": 1.2 + # } + # ] + # } + # ], + # "type": "snapshot", + # "sequence": 1 + # } + # + data = self.safe_list(message, 'data', []) + result: dict = {'info': message} + for i in range(0, len(data)): + currencyId = self.safe_string(data[i], 'asset') + code = self.safe_currency_code(currencyId) + account = self.account() + eq = self.safe_string(data[i], 'balance') + account['total'] = eq + result[code] = account + type = 'spot' + balance = self.safe_balance(result) + oldBalance = self.safe_value(self.balance, type, {}) + newBalance = self.deep_extend(oldBalance, balance) + self.balance[type] = self.safe_balance(newBalance) + channel = self.safe_string(message, 'channel') + client.resolve(self.balance[type], channel) + + def get_message_hash(self, unifiedElementName: str, subChannelName: Str = None, symbol: Str = None): + # unifiedElementName can be : orderbook, trade, ticker, bidask ... + # subChannelName only applies to channel that needs specific variation(i.e. depth_50, depth_100..) to be selected + withSymbol = symbol is not None + messageHash = unifiedElementName + if not withSymbol: + messageHash += 's' + else: + messageHash += '@' + symbol + if subChannelName is not None: + messageHash += '#' + subChannelName + return messageHash + + def handle_subscription_status(self, client: Client, message): + # + # public + # + # { + # "channelID": 210, + # "channelName": "book-10", + # "event": "subscriptionStatus", + # "reqid": 1574146735269, + # "pair": "ETH/XBT", + # "status": "subscribed", + # "subscription": {depth: 10, name: "book"} + # } + # + # private + # + # { + # "channelName": "openOrders", + # "event": "subscriptionStatus", + # "reqid": 1, + # "status": "subscribed", + # "subscription": {maxratecount: 125, name: "openOrders"} + # } + # + channelId = self.safe_string(message, 'channelID') + if channelId is not None: + client.subscriptions[channelId] = message + # requestId = self.safe_string(message, "reqid") + # if requestId in client.futures: + # del client.futures[requestId] + # } + + def handle_error_message(self, client: Client, message) -> Bool: + # + # { + # "errorMessage": "Currency pair not in ISO 4217-A3 format foobar", + # "event": "subscriptionStatus", + # "pair": "foobar", + # "reqid": 1574146735269, + # "status": "error", + # "subscription": {name: "ticker"} + # } + # + # v2 + # { + # "error": "Unsupported field: 'price' for the given msg type: add order", + # "method": "add_order", + # "success": False, + # "time_in": "2025-05-13T08:59:44.803511Z", + # "time_out": "2025-05-13T08:59:44.803542Z' + # } + # + errorMessage = self.safe_string_2(message, 'errorMessage', 'error') + if errorMessage is not None: + requestId = self.safe_string_2(message, 'reqid', 'req_id') + broad = self.exceptions['ws']['broad'] + broadKey = self.find_broadly_matched_key(broad, errorMessage) + exception = None + if broadKey is None: + exception = ExchangeError(errorMessage) # c# requirement to convert the errorMessage to string + else: + exception = broad[broadKey](errorMessage) + if requestId is not None: + client.reject(exception, requestId) + return False + return True + + def handle_message(self, client: Client, message): + channel = self.safe_string(message, 'channel') + if channel is not None: + if channel == 'executions': + data = self.safe_list(message, 'data', []) + first = self.safe_dict(data, 0, {}) + execType = self.safe_string(first, 'exec_type') + channel = 'myTrades' if (execType == 'trade') else 'orders' + methods: dict = { + 'balances': self.handle_balance, + 'book': self.handle_order_book, + 'ohlc': self.handle_ohlcv, + 'ticker': self.handle_ticker, + 'trade': self.handle_trades, + # private + 'myTrades': self.handle_my_trades, + 'orders': self.handle_orders, + } + method = self.safe_value(methods, channel) + if method is not None: + method(client, message) + if self.handle_error_message(client, message): + event = self.safe_string_2(message, 'event', 'method') + methods: dict = { + 'heartbeat': self.handle_heartbeat, + 'systemStatus': self.handle_system_status, + 'subscriptionStatus': self.handle_subscription_status, + 'add_order': self.handle_create_edit_order, + 'amend_order': self.handle_create_edit_order, + 'cancel_order': self.handle_cancel_order, + 'cancel_all': self.handle_cancel_all_orders, + 'pong': self.handle_pong, + } + method = self.safe_value(methods, event) + if method is not None: + method(client, message) diff --git a/ccxt/pro/krakenfutures.py b/ccxt/pro/krakenfutures.py new file mode 100644 index 0000000..c31f176 --- /dev/null +++ b/ccxt/pro/krakenfutures.py @@ -0,0 +1,1519 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolById +import hashlib +from ccxt.base.types import Any, Balances, Bool, Int, Order, OrderBook, Position, Str, Strings, Ticker, Tickers, Trade +from ccxt.async_support.base.ws.client import Client +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.precise import Precise + + +class krakenfutures(ccxt.async_support.krakenfutures): + + def describe(self) -> Any: + return self.deep_extend(super(krakenfutures, self).describe(), { + 'has': { + 'ws': True, + 'cancelAllOrdersWs': False, + 'cancelOrdersWs': False, + 'cancelOrderWs': False, + 'createOrderWs': False, + 'editOrderWs': False, + 'fetchBalanceWs': False, + 'fetchOpenOrdersWs': False, + 'fetchOrderWs': False, + 'fetchTradesWs': False, + 'watchOHLCV': False, + 'watchOrderBook': True, + 'watchOrderBookForSymbols': True, + 'watchTicker': True, + 'watchTickers': True, + 'watchBidsAsks': True, + 'watchTrades': True, + 'watchTradesForSymbols': True, + 'watchBalance': True, + # 'watchStatus': True, # https://docs.futures.kraken.com/#websocket-api-public-feeds-heartbeat + 'watchOrders': True, + 'watchMyTrades': True, + 'watchPositions': True, + }, + 'urls': { + 'api': { + 'ws': 'wss://futures.kraken.com/ws/v1', + }, + 'test': { + 'ws': 'wss://demo-futures.kraken.com/ws/v1', + }, + }, + 'options': { + 'tradesLimit': 1000, + 'ordersLimit': 1000, + 'OHLCVLimit': 1000, + 'connectionLimit': 100, # https://docs.futures.kraken.com/#websocket-api-websocket-api-introduction-subscriptions-limits + 'requestLimit': 100, # per second + 'fetchBalance': { + 'type': None, + }, + }, + 'streaming': { + 'keepAlive': 30000, + }, + }) + + async def authenticate(self, params={}): + """ + @ignore + authenticates the user to access private web socket channels + + https://docs.futures.kraken.com/#websocket-api-public-feeds-challenge + + :returns dict: response from exchange + """ + self.check_required_credentials() + # Hash the challenge with the SHA-256 algorithm + # Base64-decode your api_secret + # Use the result of step 2 to hash the result of step 1 with the HMAC-SHA-512 algorithm + # Base64-encode the result of step 3 + url = self.urls['api']['ws'] + messageHash = 'challenge' + client = self.client(url) + future = client.reusableFuture(messageHash) + authenticated = self.safe_value(client.subscriptions, messageHash) + if authenticated is None: + request: dict = { + 'event': 'challenge', + 'api_key': self.apiKey, + } + message = self.extend(request, params) + self.watch(url, messageHash, message, messageHash) + return await future + + async def watch_order_book_for_symbols(self, symbols: List[str], limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://docs.futures.kraken.com/#websocket-api-public-feeds-challenge + + :param str[] symbols: unified array of symbols + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + orderbook = await self.watch_multi_helper('orderbook', 'book', symbols, {'limit': limit}, params) + return orderbook.limit() + + async def subscribe_public(self, name: str, symbols: List[str], params={}): + """ + @ignore + Connects to a websocket channel + :param str name: name of the channel + :param str[] symbols: CCXT market symbols + :param dict [params]: extra parameters specific to the krakenfutures api + :returns dict: data from the websocket stream + """ + await self.load_markets() + url = self.urls['api']['ws'] + subscribe: dict = { + 'event': 'subscribe', + 'feed': name, + } + marketIds = [] + messageHash = name + if symbols is None: + symbols = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + marketIds.append(self.market_id(symbol)) + length = len(symbols) + if length == 1: + market = self.market(marketIds[0]) + messageHash = messageHash + ':' + market['symbol'] + subscribe['product_ids'] = marketIds + request = self.extend(subscribe, params) + return await self.watch(url, messageHash, request, messageHash) + + async def subscribe_private(self, name: str, messageHash: str, params={}): + """ + @ignore + Connects to a websocket channel + :param str name: name of the channel + :param str messageHash: unique identifier for the message + :param dict [params]: extra parameters specific to the krakenfutures api + :returns dict: data from the websocket stream + """ + await self.load_markets() + await self.authenticate() + url = self.urls['api']['ws'] + subscribe: dict = { + 'event': 'subscribe', + 'feed': name, + 'api_key': self.apiKey, + 'original_challenge': self.options['challenge'], + 'signed_challenge': self.options['signedChallenge'], + } + request = self.extend(subscribe, params) + return await self.watch(url, messageHash, request, messageHash) + + async def watch_ticker(self, symbol: str, params={}) -> Ticker: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://docs.futures.kraken.com/#websocket-api-public-feeds-ticker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbol = self.symbol(symbol) + tickers = await self.watch_tickers([symbol], params) + return tickers[symbol] + + async def watch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://docs.futures.kraken.com/#websocket-api-public-feeds-ticker + + :param str[] symbols: unified symbols of the markets to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, False) + ticker = await self.watch_multi_helper('ticker', 'ticker', symbols, None, params) + if self.newUpdates: + result: dict = {} + result[ticker['symbol']] = ticker + return result + return self.filter_by_array(self.tickers, 'symbol', symbols) + + async def watch_bids_asks(self, symbols: Strings = None, params={}) -> Tickers: + """ + + https://docs.futures.kraken.com/#websocket-api-public-feeds-ticker-lite + + watches best bid & ask for symbols + :param str[] symbols: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + ticker = await self.watch_multi_helper('bidask', 'ticker_lite', symbols, None, params) + if self.newUpdates: + result: dict = {} + result[ticker['symbol']] = ticker + return result + return self.filter_by_array(self.bidsasks, 'symbol', symbols) + + async def watch_trades(self, symbol: Str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://docs.futures.kraken.com/#websocket-api-public-feeds-trade + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + return await self.watch_trades_for_symbols([symbol], since, limit, params) + + async def watch_trades_for_symbols(self, symbols: List[str], since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + + https://docs.futures.kraken.com/#websocket-api-public-feeds-trade + + get the list of most recent trades for a list of symbols + :param str[] symbols: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + trades = await self.watch_multi_helper('trade', 'trade', symbols, None, params) + if self.newUpdates: + first = self.safe_list(trades, 0) + tradeSymbol = self.safe_string(first, 'symbol') + limit = trades.getLimit(tradeSymbol, limit) + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://docs.futures.kraken.com/#websocket-api-public-feeds-book + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: not used by krakenfutures watchOrderBook + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + return await self.watch_order_book_for_symbols([symbol], limit, params) + + async def watch_positions(self, symbols: Strings = None, since: Int = None, limit: Int = None, params={}) -> List[Position]: + """ + + https://docs.futures.kraken.com/#websocket-api-private-feeds-open-positions + + watch all open positions + :param str[]|None symbols: list of unified market symbols + @param since + @param limit + :param dict params: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + await self.load_markets() + messageHash = '' + symbols = self.market_symbols(symbols) + if not self.is_empty(symbols): + messageHash = '::' + ','.join(symbols) + messageHash = 'positions' + messageHash + newPositions = await self.subscribe_private('open_positions', messageHash, params) + if self.newUpdates: + return newPositions + return self.filter_by_symbols_since_limit(self.positions, symbols, since, limit, True) + + def handle_positions(self, client, message): + # + # { + # feed: 'open_positions', + # account: '3b111acc-4fcc-45be-a622-57e611fe9f7f', + # positions: [ + # { + # instrument: 'PF_LTCUSD', + # balance: 0.5, + # pnl: -0.8628305877699987, + # entry_price: 70.53, + # mark_price: 68.80433882446, + # index_price: 68.8091, + # liquidation_threshold: 0, + # effective_leverage: 0.007028866753648637, + # return_on_equity: -1.2233525985679834, + # unrealized_funding: 0.0000690610530935388, + # initial_margin: 0.7053, + # initial_margin_with_orders: 0.7053, + # maintenance_margin: 0.35265, + # pnl_currency: 'USD' + # } + # ], + # seq: 0, + # timestamp: 1698608414910 + # } + # + if self.positions is None: + self.positions = ArrayCacheBySymbolById() + cache = self.positions + rawPositions = self.safe_value(message, 'positions', []) + newPositions = [] + for i in range(0, len(rawPositions)): + rawPosition = rawPositions[i] + position = self.parse_ws_position(rawPosition) + timestamp = self.safe_integer(message, 'timestamp') + position['timestamp'] = timestamp + position['datetime'] = self.iso8601(timestamp) + newPositions.append(position) + cache.append(position) + messageHashes = self.find_message_hashes(client, 'positions::') + for i in range(0, len(messageHashes)): + messageHash = messageHashes[i] + parts = messageHash.split('::') + symbolsString = parts[1] + symbols = symbolsString.split(',') + positions = self.filter_by_array(newPositions, 'symbol', symbols, False) + if not self.is_empty(positions): + client.resolve(positions, messageHash) + client.resolve(newPositions, 'positions') + + def parse_ws_position(self, position, market=None): + # + # { + # instrument: 'PF_LTCUSD', + # balance: 0.5, + # pnl: -0.8628305877699987, + # entry_price: 70.53, + # mark_price: 68.80433882446, + # index_price: 68.8091, + # liquidation_threshold: 0, + # effective_leverage: 0.007028866753648637, + # return_on_equity: -1.2233525985679834, + # unrealized_funding: 0.0000690610530935388, + # initial_margin: 0.7053, + # initial_margin_with_orders: 0.7053, + # maintenance_margin: 0.35265, + # pnl_currency: 'USD' + # } + # + marketId = self.safe_string(position, 'instrument') + hedged = 'both' + balance = self.safe_number(position, 'balance') + side = 'long' if (balance > 0) else 'short' + return self.safe_position({ + 'info': position, + 'id': None, + 'symbol': self.safe_symbol(marketId), + 'notional': None, + 'marginMode': None, + 'liquidationPrice': self.safe_number(position, 'liquidation_threshold'), + 'entryPrice': self.safe_number(position, 'entry_price'), + 'unrealizedPnl': self.safe_number(position, 'pnl'), + 'percentage': self.safe_number(position, 'return_on_equity'), + 'contracts': self.parse_number(Precise.string_abs(self.number_to_string(balance))), + 'contractSize': None, + 'markPrice': self.safe_number(position, 'mark_price'), + 'side': side, + 'hedged': hedged, + 'timestamp': None, + 'datetime': None, + 'maintenanceMargin': self.safe_number(position, 'maintenance_margin'), + 'maintenanceMarginPercentage': None, + 'collateral': None, + 'initialMargin': self.safe_number(position, 'initial_margin'), + 'initialMarginPercentage': None, + 'leverage': None, + 'marginRatio': None, + }) + + async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + watches information on multiple orders made by the user + + https://docs.futures.kraken.com/#websocket-api-private-feeds-open-orders + https://docs.futures.kraken.com/#websocket-api-private-feeds-open-orders-verbose + + :param str symbol: not used by krakenfutures watchOrders + :param int [since]: not used by krakenfutures watchOrders + :param int [limit]: not used by krakenfutures watchOrders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + name = 'open_orders' + messageHash = 'orders' + if symbol is not None: + market = self.market(symbol) + messageHash += ':' + market['symbol'] + orders = await self.subscribe_private(name, messageHash, params) + if self.newUpdates: + limit = orders.getLimit(symbol, limit) + return self.filter_by_since_limit(orders, since, limit, 'timestamp', True) + + async def watch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + watches information on multiple trades made by the user + + https://docs.futures.kraken.com/#websocket-api-private-feeds-fills + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + name = 'fills' + messageHash = 'myTrades' + if symbol is not None: + market = self.market(symbol) + messageHash += ':' + market['symbol'] + trades = await self.subscribe_private(name, messageHash, params) + if self.newUpdates: + limit = trades.getLimit(symbol, limit) + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + async def watch_balance(self, params={}) -> Balances: + """ + watches information on the user's account balance + + https://docs.futures.kraken.com/#websocket-api-private-feeds-balances + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.account]: can be either 'futures' or 'flex_futures' + :returns dict} a object of wallet types each with a balance structure {@link https://docs.ccxt.com/#/?id=balance-structure: + """ + await self.load_markets() + name = 'balances' + messageHash = name + account = None + account, params = self.handle_option_and_params(params, 'watchBalance', 'account') + if account is not None: + if account != 'futures' and account != 'flex_futures': + raise ArgumentsRequired(self.id + ' watchBalance account must be either \'futures\' or \'flex_futures\'') + messageHash += ':' + account + return await self.subscribe_private(name, messageHash, params) + + def handle_trade(self, client: Client, message): + # + # snapshot + # + # { + # "feed": "trade_snapshot", + # "product_id": "PI_XBTUSD", + # "trades": [ + # { + # "feed": "trade", + # "product_id": "PI_XBTUSD", + # "uid": "caa9c653-420b-4c24-a9f2-462a054d86f1", + # "side": "sell", + # "type": "fill", + # "seq": 655508, + # "time": 1612269657781, + # "qty": 440, + # "price": 34893 + # }, + # ... + # ] + # } + # + # update + # + # { + # "feed": "trade", + # "product_id": "PI_XBTUSD", + # "uid": "05af78ac-a774-478c-a50c-8b9c234e071e", + # "side": "sell", + # "type": "fill", + # "seq": 653355, + # "time": 1612266317519, + # "qty": 15000, + # "price": 34969.5 + # } + # + channel = self.safe_string(message, 'feed') + marketId = self.safe_string(message, 'product_id') + if marketId is not None: + market = self.market(marketId) + symbol = market['symbol'] + messageHash = self.get_message_hash('trade', None, symbol) + if self.safe_list(self.trades, symbol) is None: + tradesLimit = self.safe_integer(self.options, 'tradesLimit', 1000) + self.trades[symbol] = ArrayCache(tradesLimit) + tradesArray = self.trades[symbol] + if channel == 'trade_snapshot': + trades = self.safe_list(message, 'trades', []) + length = len(trades) + for i in range(0, length): + index = length - 1 - i # need reverse to correct chronology + item = trades[index] + trade = self.parse_ws_trade(item) + tradesArray.append(trade) + else: + trade = self.parse_ws_trade(message) + tradesArray.append(trade) + client.resolve(tradesArray, messageHash) + + def parse_ws_trade(self, trade, market=None): + # + # { + # "feed": "trade", + # "product_id": "PI_XBTUSD", + # "uid": "caa9c653-420b-4c24-a9f1-462a054d86f1", + # "side": "sell", + # "type": "fill", + # "seq": 655508, + # "time": 1612269657781, + # "qty": 440, + # "price": 34893 + # } + # + marketId = self.safe_string(trade, 'product_id') + market = self.safe_market(marketId, market) + timestamp = self.safe_integer(trade, 'time') + return self.safe_trade({ + 'info': trade, + 'id': self.safe_string(trade, 'uid'), + 'symbol': self.safe_string(market, 'symbol'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'order': None, + 'type': self.safe_string(trade, 'type'), + 'side': self.safe_string(trade, 'side'), + 'takerOrMaker': 'taker', + 'price': self.safe_string(trade, 'price'), + 'amount': self.safe_string(trade, 'qty'), + 'cost': None, + 'fee': { + 'rate': None, + 'cost': None, + 'currency': None, + }, + }, market) + + def parse_ws_order_trade(self, trade, market=None): + # + # { + # "symbol": "BTC_USDT", + # "type": "LIMIT", + # "quantity": "1", + # "orderId": "32471407854219264", + # "tradeFee": "0", + # "clientOrderId": "", + # "accountType": "SPOT", + # "feeCurrency": "", + # "eventType": "place", + # "source": "API", + # "side": "BUY", + # "filledQuantity": "0", + # "filledAmount": "0", + # "matchRole": "MAKER", + # "state": "NEW", + # "tradeTime": 0, + # "tradeAmount": "0", + # "orderAmount": "0", + # "createTime": 1648708186922, + # "price": "47112.1", + # "tradeQty": "0", + # "tradePrice": "0", + # "tradeId": "0", + # "ts": 1648708187469 + # } + # + timestamp = self.safe_integer(trade, 'tradeTime') + marketId = self.safe_string(trade, 'symbol') + return self.safe_trade({ + 'info': trade, + 'id': self.safe_string(trade, 'tradeId'), + 'symbol': self.safe_symbol(marketId, market), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'order': self.safe_string(trade, 'orderId'), + 'type': self.safe_string_lower(trade, 'type'), + 'side': self.safe_string(trade, 'side'), + 'takerOrMaker': self.safe_string(trade, 'matchRole'), + 'price': self.safe_string(trade, 'price'), + 'amount': self.safe_string(trade, 'tradeAmount'), # ? tradeQty? + 'cost': None, + 'fee': { + 'rate': None, + 'cost': self.safe_string(trade, 'tradeFee'), + 'currency': self.safe_string(trade, 'feeCurrency'), + }, + }, market) + + def handle_order(self, client: Client, message): + # + # update(verbose) + # + # { + # "feed": "open_orders_verbose", + # "order": { + # "instrument": "PI_XBTUSD", + # "time": 1567597581495, + # "last_update_time": 1567597581495, + # "qty": 102.0, + # "filled": 0.0, + # "limit_price": 10601.0, + # "stop_price": 0.0, + # "type": "limit", + # "order_id": "fa9806c9-cba9-4661-9f31-8c5fd045a95d", + # "direction": 0, + # "reduce_only": False + # }, + # "is_cancel": True, + # "reason": "post_order_failed_because_it_would_be_filled" + # } + # + # update + # + # { + # "feed": "open_orders", + # "order": { + # "instrument": "PI_XBTUSD", + # "time": 1567702877410, + # "last_update_time": 1567702877410, + # "qty": 304.0, + # "filled": 0.0, + # "limit_price": 10640.0, + # "stop_price": 0.0, + # "type": "limit", + # "order_id": "59302619-41d2-4f0b-941f-7e7914760ad3", + # "direction": 1, + # "reduce_only": True + # }, + # "is_cancel": False, + # "reason": "new_placed_order_by_user" + # } + # { + # "feed": "open_orders", + # "order_id": "ea8a7144-37db-449b-bb4a-b53c814a0f43", + # "is_cancel": True, + # "reason": "cancelled_by_user" + # } + # + # { + # "feed": 'open_orders', + # "order": { + # "instrument": 'PF_XBTUSD', + # "time": 1698159920097, + # "last_update_time": 1699835622988, + # "qty": 1.1, + # "filled": 0, + # "limit_price": 20000, + # "stop_price": 0, + # "type": 'limit', + # "order_id": '0eaf02b0-855d-4451-a3b7-e2b3070c1fa4', + # "direction": 0, + # "reduce_only": False + # }, + # "is_cancel": False, + # "reason": 'edited_by_user' + # } + # + orders = self.orders + if orders is None: + limit = self.safe_integer(self.options, 'ordersLimit') + orders = ArrayCacheBySymbolById(limit) + self.orders = orders + order = self.safe_value(message, 'order') + if order is not None: + marketId = self.safe_string(order, 'instrument') + messageHash = 'orders' + symbol = self.safe_symbol(marketId) + orderId = self.safe_string(order, 'order_id') + previousOrders = self.safe_value(orders.hashmap, symbol, {}) + previousOrder = self.safe_value(previousOrders, orderId) + reason = self.safe_string(message, 'reason') + if (previousOrder is None) or (reason == 'edited_by_user'): + parsed = self.parse_ws_order(order) + orders.append(parsed) + client.resolve(orders, messageHash) + client.resolve(orders, messageHash + ':' + symbol) + else: + trade = self.parse_ws_trade(order) + if previousOrder['trades'] is None: + previousOrder['trades'] = [] + previousOrder['trades'].append(trade) + previousOrder['lastTradeTimestamp'] = trade['timestamp'] + totalCost = '0' + totalAmount = '0' + trades = previousOrder['trades'] + for i in range(0, len(trades)): + currentTrade = trades[i] + totalCost = Precise.string_add(totalCost, self.number_to_string(currentTrade['cost'])) + totalAmount = Precise.string_add(totalAmount, self.number_to_string(currentTrade['amount'])) + if Precise.string_gt(totalAmount, '0'): + previousOrder['average'] = Precise.string_div(totalCost, totalAmount) + previousOrder['cost'] = totalCost + if previousOrder['filled'] is not None: + stringOrderFilled = self.number_to_string(previousOrder['filled']) + previousOrder['filled'] = Precise.string_add(stringOrderFilled, self.number_to_string(trade['amount'])) + if previousOrder['amount'] is not None: + previousOrder['remaining'] = Precise.string_sub(self.number_to_string(previousOrder['amount']), stringOrderFilled) + if previousOrder['fee'] is None: + previousOrder['fee'] = { + 'rate': None, + 'cost': '0', + 'currency': self.number_to_string(trade['fee']['currency']), + } + if (previousOrder['fee']['cost'] is not None) and (trade['fee']['cost'] is not None): + stringOrderCost = self.number_to_string(previousOrder['fee']['cost']) + stringTradeCost = self.number_to_string(trade['fee']['cost']) + previousOrder['fee']['cost'] = Precise.string_add(stringOrderCost, stringTradeCost) + # update the newUpdates count + orders.append(self.safe_order(previousOrder)) + client.resolve(orders, messageHash + ':' + symbol) + client.resolve(orders, messageHash) + else: + isCancel = self.safe_value(message, 'is_cancel') + if isCancel: + # get order without symbol + for i in range(0, len(orders)): + currentOrder = orders[i] + if currentOrder['id'] == message['order_id']: + orders[i] = self.extend(currentOrder, { + 'status': 'canceled', + }) + client.resolve(orders, 'orders') + client.resolve(orders, 'orders:' + currentOrder['symbol']) + break + return message + + def handle_order_snapshot(self, client: Client, message): + # + # verbose + # + # { + # "feed": "open_orders_verbose_snapshot", + # "account": "0f9c23b8-63e2-40e4-9592-6d5aa57c12ba", + # "orders": [ + # { + # "instrument": "PI_XBTUSD", + # "time": 1567428848005, + # "last_update_time": 1567428848005, + # "qty": 100.0, + # "filled": 0.0, + # "limit_price": 8500.0, + # "stop_price": 0.0, + # "type": "limit", + # "order_id": "566942c8-a3b5-4184-a451-622b09493129", + # "direction": 0, + # "reduce_only": False + # }, + # ... + # ] + # } + # + # regular + # + # { + # "feed": "open_orders_snapshot", + # "account": "e258dba9-4dd4-4da5-bfef-75beb91c098e", + # "orders": [ + # { + # "instrument": "PI_XBTUSD", + # "time": 1612275024153, + # "last_update_time": 1612275024153, + # "qty": 1000, + # "filled": 0, + # "limit_price": 34900, + # "stop_price": 13789, + # "type": "stop", + # "order_id": "723ba95f-13b7-418b-8fcf-ab7ba6620555", + # "direction": 1, + # "reduce_only": False, + # "triggerSignal": "last" + # }, + # ... + # ] + # } + orders = self.safe_value(message, 'orders', []) + limit = self.safe_integer(self.options, 'ordersLimit') + self.orders = ArrayCacheBySymbolById(limit) + symbols: dict = {} + cachedOrders = self.orders + for i in range(0, len(orders)): + order = orders[i] + parsed = self.parse_ws_order(order) + symbol = parsed['symbol'] + symbols[symbol] = True + cachedOrders.append(parsed) + length = len(self.orders) + if length > 0: + client.resolve(self.orders, 'orders') + keys = list(symbols.keys()) + for i in range(0, len(keys)): + symbol = keys[i] + messageHash = 'orders:' + symbol + client.resolve(self.orders, messageHash) + + def parse_ws_order(self, order, market=None): + # + # update + # + # { + # "feed": "open_orders_verbose", + # "order": { + # "instrument": "PI_XBTUSD", + # "time": 1567597581495, + # "last_update_time": 1567597581495, + # "qty": 102.0, + # "filled": 0.0, + # "limit_price": 10601.0, + # "stop_price": 0.0, + # "type": "limit", + # "order_id": "fa9806c9-cba9-4661-9f31-8c5fd045a95d", + # "direction": 0, + # "reduce_only": False + # }, + # "is_cancel": True, + # "reason": "post_order_failed_because_it_would_be_filled" + # } + # + # snapshot + # + # { + # "instrument": "PI_XBTUSD", + # "time": 1567597581495, + # "last_update_time": 1567597581495, + # "qty": 102.0, + # "filled": 0.0, + # "limit_price": 10601.0, + # "stop_price": 0.0, + # "type": "limit", + # "order_id": "fa9806c9-cba9-4661-9f31-8c5fd045a95d", + # "direction": 0, + # "reduce_only": False + # } + # + isCancelled = self.safe_value(order, 'is_cancel') + unparsedOrder = order + status = None + if isCancelled is not None: + unparsedOrder = self.safe_value(order, 'order') + if isCancelled is True: + status = 'cancelled' + marketId = self.safe_string(unparsedOrder, 'instrument') + timestamp = self.safe_string(unparsedOrder, 'time') + direction = self.safe_integer(unparsedOrder, 'direction') + return self.safe_order({ + 'info': order, + 'symbol': self.safe_symbol(marketId, market), + 'id': self.safe_string(unparsedOrder, 'order_id'), + 'clientOrderId': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'type': self.safe_string(unparsedOrder, 'type'), + 'timeInForce': None, + 'postOnly': None, + 'side': 'buy' if (direction == 0) else 'sell', + 'price': self.safe_string(unparsedOrder, 'limit_price'), + 'stopPrice': self.safe_string(unparsedOrder, 'stop_price'), + 'triggerPrice': self.safe_string(unparsedOrder, 'stop_price'), + 'amount': self.safe_string(unparsedOrder, 'qty'), + 'cost': None, + 'average': None, + 'filled': self.safe_string(unparsedOrder, 'filled'), + 'remaining': None, + 'status': status, + 'fee': { + 'rate': None, + 'cost': None, + 'currency': None, + }, + 'trades': None, + }) + + def handle_ticker(self, client: Client, message): + # + # { + # "time": 1680811086487, + # "product_id": "PI_XBTUSD", + # "funding_rate": 7.792297e-12, + # "funding_rate_prediction": -4.2671095e-11, + # "relative_funding_rate": 2.18013888889e-7, + # "relative_funding_rate_prediction": -0.0000011974, + # "next_funding_rate_time": 1680811200000, + # "feed": "ticker", + # "bid": 28060, + # "ask": 28070, + # "bid_size": 2844, + # "ask_size": 1902, + # "volume": 19628180, + # "dtm": 0, + # "leverage": "50x", + # "index": 28062.14, + # "premium": 0, + # "last": 28053.5, + # "change": -0.7710945651981715, + # "suspended": False, + # "tag": "perpetual", + # "pair": "XBT:USD", + # "openInterest": 28875946, + # "markPrice": 28064.92082724592, + # "maturityTime": 0, + # "post_only": False, + # "volumeQuote": 19628180 + # } + # + marketId = self.safe_string(message, 'product_id') + if marketId is not None: + ticker = self.parse_ws_ticker(message) + symbol = ticker['symbol'] + self.tickers[symbol] = ticker + messageHash = self.get_message_hash('ticker', None, symbol) + client.resolve(ticker, messageHash) + + def handle_bid_ask(self, client: Client, message): + # + # { + # "feed": "ticker_lite", + # "product_id": "FI_ETHUSD_210625", + # "bid": 1753.45, + # "ask": 1760.35, + # "change": 13.448175559936647, + # "premium": 9.1, + # "volume": 6899673.0, + # "tag": "semiannual", + # "pair": "ETH:USD", + # "dtm": 141, + # "maturityTime": 1624633200000, + # "volumeQuote": 6899673.0 + # } + # + marketId = self.safe_string(message, 'product_id') + if marketId is not None: + ticker = self.parse_ws_ticker(message) + symbol = ticker['symbol'] + self.bidsasks[symbol] = ticker + messageHash = self.get_message_hash('bidask', None, symbol) + client.resolve(ticker, messageHash) + + def parse_ws_ticker(self, ticker, market=None): + # + # { + # "time": 1680811086487, + # "product_id": "PI_XBTUSD", + # "funding_rate": 7.792297e-12, + # "funding_rate_prediction": -4.2671095e-11, + # "relative_funding_rate": 2.18013888889e-7, + # "relative_funding_rate_prediction": -0.0000011974, + # "next_funding_rate_time": 1680811200000, + # "feed": "ticker", + # "bid": 28060, + # "ask": 28070, + # "bid_size": 2844, + # "ask_size": 1902, + # "volume": 19628180, + # "dtm": 0, + # "leverage": "50x", + # "index": 28062.14, + # "premium": 0, + # "last": 28053.5, + # "change": -0.7710945651981715, + # "suspended": False, + # "tag": "perpetual", + # "pair": "XBT:USD", + # "openInterest": 28875946, + # "markPrice": 28064.92082724592, + # "maturityTime": 0, + # "post_only": False, + # "volumeQuote": 19628180 + # } + # + # ticker_lite + # + # { + # "feed": "ticker_lite", + # "product_id": "FI_ETHUSD_210625", + # "bid": 1753.45, + # "ask": 1760.35, + # "change": 13.448175559936647, + # "premium": 9.1, + # "volume": 6899673.0, + # "tag": "semiannual", + # "pair": "ETH:USD", + # "dtm": 141, + # "maturityTime": 1624633200000, + # "volumeQuote": 6899673.0 + # } + # + marketId = self.safe_string(ticker, 'product_id') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + timestamp = self.parse8601(self.safe_string(ticker, 'lastTime')) + last = self.safe_string(ticker, 'last') + return self.safe_ticker({ + 'info': ticker, + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': None, + 'low': None, + 'bid': self.safe_string(ticker, 'bid'), + 'bidVolume': self.safe_string(ticker, 'bid_size'), + 'ask': self.safe_string(ticker, 'ask'), + 'askVolume': self.safe_string(ticker, 'ask_size'), + 'vwap': None, + 'open': None, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': self.safe_string(ticker, 'change'), + 'percentage': None, + 'average': None, + 'baseVolume': self.safe_string(ticker, 'volume'), + 'quoteVolume': self.safe_string(ticker, 'volumeQuote'), + 'markPrice': self.safe_string(ticker, 'markPrice'), + 'indexPrice': self.safe_string(ticker, 'index'), + }) + + def handle_order_book_snapshot(self, client: Client, message): + # + # { + # "feed": "book_snapshot", + # "product_id": "PI_XBTUSD", + # "timestamp": 1612269825817, + # "seq": 326072249, + # "tickSize": null, + # "bids": [ + # { + # "price": 34892.5, + # "qty": 6385 + # }, + # { + # "price": 34892, + # "qty": 10924 + # }, + # ], + # "asks": [ + # { + # "price": 34911.5, + # "qty": 20598 + # }, + # { + # "price": 34912, + # "qty": 2300 + # }, + # ] + # } + # + marketId = self.safe_string(message, 'product_id') + market = self.safe_market(marketId) + symbol = market['symbol'] + messageHash = self.get_message_hash('orderbook', None, symbol) + subscription = self.safe_dict(client.subscriptions, messageHash, {}) + limit = self.safe_integer(subscription, 'limit') + timestamp = self.safe_integer(message, 'timestamp') + self.orderbooks[symbol] = self.order_book({}, limit) + orderbook = self.orderbooks[symbol] + bids = self.safe_list(message, 'bids') + asks = self.safe_list(message, 'asks') + for i in range(0, len(bids)): + bid = bids[i] + price = self.safe_number(bid, 'price') + qty = self.safe_number(bid, 'qty') + bidsSide = orderbook['bids'] + bidsSide.store(price, qty) + for i in range(0, len(asks)): + ask = asks[i] + price = self.safe_number(ask, 'price') + qty = self.safe_number(ask, 'qty') + asksSide = orderbook['asks'] + asksSide.store(price, qty) + orderbook['timestamp'] = timestamp + orderbook['datetime'] = self.iso8601(timestamp) + orderbook['symbol'] = symbol + client.resolve(orderbook, messageHash) + + def handle_order_book(self, client: Client, message): + # + # { + # "feed": "book", + # "product_id": "PI_XBTUSD", + # "side": "sell", + # "seq": 326094134, + # "price": 34981, + # "qty": 0, + # "timestamp": 1612269953629 + # } + # + marketId = self.safe_string(message, 'product_id') + market = self.safe_market(marketId) + symbol = market['symbol'] + messageHash = self.get_message_hash('orderbook', None, symbol) + orderbook = self.orderbooks[symbol] + side = self.safe_string(message, 'side') + price = self.safe_number(message, 'price') + qty = self.safe_number(message, 'qty') + timestamp = self.safe_integer(message, 'timestamp') + if side == 'sell': + asks = orderbook['asks'] + asks.store(price, qty) + else: + bids = orderbook['bids'] + bids.store(price, qty) + orderbook['timestamp'] = timestamp + orderbook['datetime'] = self.iso8601(timestamp) + client.resolve(orderbook, messageHash) + + def handle_balance(self, client: Client, message): + # + # snapshot + # + # { + # "feed": "balances_snapshot", + # "account": "4a012c31-df95-484a-9473-d51e4a0c4ae7", + # "holding": { + # "USDT": 4997.5012493753, + # "XBT": 0.1285407184, + # ... + # }, + # "futures": { + # "F-ETH:EUR": { + # "name": "F-ETH:EUR", + # "pair": "ETH/EUR", + # "unit": "EUR", + # "portfolio_value": 0.0, + # "balance": 0.0, + # "maintenance_margin": 0.0, + # "initial_margin": 0.0, + # "available": 0.0, + # "unrealized_funding": 0.0, + # "pnl": 0.0 + # }, + # ... + # }, + # "flex_futures": { + # "currencies": { + # "USDT": { + # "quantity": 0.0, + # "value": 0.0, + # "collateral_value": 0.0, + # "available": 0.0, + # "haircut": 0.0, + # "conversion_spread": 0.0 + # }, + # ... + # }, + # "balance_value":0.0, + # "portfolio_value":0.0, + # "collateral_value":0.0, + # "initial_margin":0.0, + # "initial_margin_without_orders":0.0, + # "maintenance_margin":0.0, + # "pnl":0.0, + # "unrealized_funding":0.0, + # "total_unrealized":0.0, + # "total_unrealized_as_margin":0.0, + # "margin_equity":0.0, + # "available_margin":0.0 + # "isolated":{ + # }, + # "cross":{ + # "balance_value":9963.66, + # "portfolio_value":9963.66, + # "collateral_value":9963.66, + # "initial_margin":0.0, + # "initial_margin_without_orders":0.0, + # "maintenance_margin":0.0, + # "pnl":0.0, + # "unrealized_funding":0.0, + # "total_unrealized":0.0, + # "total_unrealized_as_margin":0.0, + # "margin_equity":9963.66, + # "available_margin":9963.66, + # "effective_leverage":0.0 + # }, + # }, + # "timestamp":1640995200000, + # "seq":0 + # } + # + # update + # + # Holding Wallet + # + # { + # "feed": "balances", + # "account": "7a641082-55c7-4411-a85f-930ec2e09617", + # "holding": { + # "USD": 5000.0 + # }, + # "futures": {}, + # "timestamp": 1640995200000, + # "seq": 83 + # } + # + # Multi-Collateral + # + # { + # "feed": "balances" + # "account": "7a641082-55c7-4411-a85f-930ec2e09617" + # "flex_futures": { + # "currencies": { + # "USDT": { + # "quantity": 0.0, + # "value": 0.0, + # "collateral_value": 0.0, + # "available": 0.0, + # "haircut": 0.0, + # "conversion_spread": 0.0 + # }, + # ... + # }, + # "balance_value": 5000.0, + # "portfolio_value": 5000.0, + # "collateral_value": 5000.0, + # "initial_margin": 0.0, + # "initial_margin_without_orders": 0.0, + # "maintenance_margin": 0.0, + # "pnl": 0.0, + # "unrealized_funding": 0.0, + # "total_unrealized": 0.0, + # "total_unrealized_as_margin": 0.0, + # "margin_equity": 5000.0, + # "available_margin": 5000.0 + # }, + # "timestamp": 1640995200000, + # "seq": 1 + # } + # + # Sample Single-Collateral Balance Delta + # + # { + # "feed": "balances", + # "account": "7a641082-55c7-4411-a85f-930ec2e09617", + # "holding": {}, + # "futures": { + # "F-XBT:USD": { + # "name": "F-XBT:USD", + # "pair": "XBT/USD", + # "unit": "XBT", + # "portfolio_value": 0.1219368845, + # "balance": 0.1219368845, + # "maintenance_margin": 0.0, + # "initial_margin": 0.0, + # "available": 0.1219368845, + # "unrealized_funding": 0.0, + # "pnl": 0.0 + # } + # }, + # "timestamp": 1640995200000, + # "seq": 2 + # } + # + holding = self.safe_value(message, 'holding') + futures = self.safe_value(message, 'futures') + flexFutures = self.safe_value(message, 'flex_futures') + messageHash = 'balances' + timestamp = self.safe_integer(message, 'timestamp') + if holding is not None: + holdingKeys = list(holding.keys()) # cashAccount + holdingResult: dict = { + 'info': message, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + for i in range(0, len(holdingKeys)): + key = holdingKeys[i] + code = self.safe_currency_code(key) + newAccount = self.account() + newAccount['total'] = self.safe_string(holding, key) + holdingResult[code] = newAccount + self.balance['cash'] = holdingResult + self.balance['cash'] = self.safe_balance(self.balance['cash']) + client.resolve(holdingResult, messageHash) + if futures is not None: + futuresKeys = list(futures.keys()) # marginAccount + futuresResult: dict = { + 'info': message, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + for i in range(0, len(futuresKeys)): + key = futuresKeys[i] + symbol = self.safe_symbol(key) + newAccount = self.account() + future = self.safe_value(futures, key) + currencyId = self.safe_string(future, 'unit') + code = self.safe_currency_code(currencyId) + newAccount['free'] = self.safe_string(future, 'available') + newAccount['used'] = self.safe_string(future, 'initial_margin') + newAccount['total'] = self.safe_string(future, 'balance') + futuresResult[symbol] = {} + futuresResult[symbol][code] = newAccount + self.balance['margin'] = futuresResult + self.balance['margin'] = self.safe_balance(self.balance['margin']) + client.resolve(self.balance['margin'], messageHash + 'futures') + if flexFutures is not None: + flexFutureCurrencies = self.safe_value(flexFutures, 'currencies', {}) + flexFuturesKeys = list(flexFutureCurrencies.keys()) # multi-collateral margin account + flexFuturesResult: dict = { + 'info': message, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + for i in range(0, len(flexFuturesKeys)): + key = flexFuturesKeys[i] + flexFuture = self.safe_value(flexFutureCurrencies, key) + code = self.safe_currency_code(key) + newAccount = self.account() + newAccount['free'] = self.safe_string(flexFuture, 'available') + newAccount['used'] = self.safe_string(flexFuture, 'collateral_value') + newAccount['total'] = self.safe_string(flexFuture, 'quantity') + flexFuturesResult[code] = newAccount + self.balance['flex'] = flexFuturesResult + self.balance['flex'] = self.safe_balance(self.balance['flex']) + client.resolve(self.balance['flex'], messageHash + 'flex_futures') + client.resolve(self.balance, messageHash) + + def handle_my_trades(self, client: Client, message): + # + # { + # "feed": "fills_snapshot", + # "account": "DemoUser", + # "fills": [ + # { + # "instrument": "FI_XBTUSD_200925", + # "time": 1600256910739, + # "price": 10937.5, + # "seq": 36, + # "buy": True, + # "qty": 5000.0, + # "order_id": "9e30258b-5a98-4002-968a-5b0e149bcfbf", + # "cli_ord_id": "8b58d9da-fcaf-4f60-91bc-9973a3eba48d", # only on update, not on snapshot + # "fill_id": "cad76f07-814e-4dc6-8478-7867407b6bff", + # "fill_type": "maker", + # "fee_paid": -0.00009142857, + # "fee_currency": "BTC", + # "taker_order_type": "ioc", + # "order_type": "limit" + # }, + # ... + # ] + # } + # + trades = self.safe_value(message, 'fills', []) + stored = self.myTrades + if stored is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + stored = ArrayCacheBySymbolById(limit) + self.myTrades = stored + tradeSymbols: dict = {} + for i in range(0, len(trades)): + trade = trades[i] + parsedTrade = self.parse_ws_my_trade(trade) + tradeSymbols[parsedTrade['symbol']] = True + stored.append(parsedTrade) + tradeSymbolKeys = list(tradeSymbols.keys()) + for i in range(0, len(tradeSymbolKeys)): + symbol = tradeSymbolKeys[i] + messageHash = 'myTrades:' + symbol + client.resolve(stored, messageHash) + client.resolve(stored, 'myTrades') + + def parse_ws_my_trade(self, trade, market=None): + # + # { + # "instrument": "FI_XBTUSD_200925", + # "time": 1600256910739, + # "price": 10937.5, + # "seq": 36, + # "buy": True, + # "qty": 5000.0, + # "order_id": "9e30258b-5a98-4002-968a-5b0e149bcfbf", + # "cli_ord_id": "8b58d9da-fcaf-4f60-91bc-9973a3eba48d", # only on update, not on snapshot + # "fill_id": "cad76f07-814e-4dc6-8478-7867407b6bff", + # "fill_type": "maker", + # "fee_paid": -0.00009142857, + # "fee_currency": "BTC", + # "taker_order_type": "ioc", + # "order_type": "limit" + # } + # + timestamp = self.safe_integer(trade, 'time') + marketId = self.safe_string(trade, 'instrument') + market = self.safe_market(marketId, market) + isBuy = self.safe_value(trade, 'buy') + feeCurrencyId = self.safe_string(trade, 'fee_currency') + return self.safe_trade({ + 'info': trade, + 'id': self.safe_string(trade, 'fill_id'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': self.safe_string(market, 'symbol'), + 'order': self.safe_string(trade, 'order_id'), + 'type': self.safe_string(trade, 'type'), + 'side': 'buy' if isBuy else 'sell', + 'takerOrMaker': self.safe_string(trade, 'fill_type'), + 'price': self.safe_string(trade, 'price'), + 'amount': self.safe_string(trade, 'qty'), + 'cost': None, + 'fee': { + 'currency': self.safe_currency_code(feeCurrencyId), + 'cost': self.safe_string(trade, 'fee_paid'), + 'rate': None, + }, + }) + + async def watch_multi_helper(self, unifiedName: str, channelName: str, symbols: Strings = None, subscriptionArgs=None, params={}): + await self.load_markets() + # symbols are required + symbols = self.market_symbols(symbols, None, False, True, False) + messageHashes = [] + for i in range(0, len(symbols)): + messageHashes.append(self.get_message_hash(unifiedName, None, self.symbol(symbols[i]))) + marketIds = self.market_ids(symbols) + request: dict = { + 'event': 'subscribe', + 'feed': channelName, + 'product_ids': marketIds, + } + url = self.urls['api']['ws'] + return await self.watch_multiple(url, messageHashes, self.extend(request, params), messageHashes, subscriptionArgs) + + def get_message_hash(self, unifiedElementName: str, subChannelName: Str = None, symbol: Str = None): + # unifiedElementName can be : orderbook, trade, ticker, bidask ... + # subChannelName only applies to channel that needs specific variation(i.e. depth_50, depth_100..) to be selected + withSymbol = symbol is not None + messageHash = unifiedElementName + if not withSymbol: + messageHash += 's' + else: + messageHash += ':' + symbol + if subChannelName is not None: + messageHash += '#' + subChannelName + return messageHash + + def handle_error_message(self, client: Client, message) -> Bool: + # + # { + # event: 'alert', + # message: 'Failed to subscribe to authenticated feed' + # } + # + errMsg = self.safe_string(message, 'message') + try: + raise ExchangeError(self.id + ' ' + errMsg) + except Exception as error: + client.reject(error) + return False + + def handle_message(self, client, message): + event = self.safe_string(message, 'event') + if event == 'challenge': + self.handle_authenticate(client, message) + elif event == 'alert': + self.handle_error_message(client, message) + elif event == 'pong': + client.lastPong = self.milliseconds() + elif event is None: + feed = self.safe_string(message, 'feed') + methods: dict = { + 'ticker': self.handle_ticker, + 'ticker_lite': self.handle_bid_ask, + 'trade': self.handle_trade, + 'trade_snapshot': self.handle_trade, + # 'heartbeat': self.handleStatus, + 'book': self.handle_order_book, + 'book_snapshot': self.handle_order_book_snapshot, + 'open_orders_verbose': self.handle_order, + 'open_orders_verbose_snapshot': self.handle_order_snapshot, + 'fills': self.handle_my_trades, + 'fills_snapshot': self.handle_my_trades, + 'open_orders': self.handle_order, + 'open_orders_snapshot': self.handle_order_snapshot, + 'balances': self.handle_balance, + 'balances_snapshot': self.handle_balance, + 'open_positions': self.handle_positions, + } + method = self.safe_value(methods, feed) + if method is not None: + method(client, message) + + def handle_authenticate(self, client: Client, message): + """ + @ignore + https://docs.futures.kraken.com/#websocket-api-websocket-api-introduction-sign-challenge-challenge + """ + # + # { + # "event": "challenge", + # "message": "226aee50-88fc-4618-a42a-34f7709570b2" + # } + # + event = self.safe_value(message, 'event') + messageHash = 'challenge' + if event != 'error': + challenge = self.safe_value(message, 'message') + hashedChallenge = self.hash(self.encode(challenge), 'sha256', 'binary') + base64Secret = self.base64_to_binary(self.secret) + signature = self.hmac(hashedChallenge, base64Secret, hashlib.sha512, 'base64') + self.options['challenge'] = challenge + self.options['signedChallenge'] = signature + future = self.safe_value(client.futures, messageHash) + future.resolve(True) + else: + error = AuthenticationError(self.id + ' ' + self.json(message)) + client.reject(error, messageHash) + if messageHash in client.subscriptions: + del client.subscriptions[messageHash] + return message diff --git a/ccxt/pro/kucoin.py b/ccxt/pro/kucoin.py new file mode 100644 index 0000000..9c99b3e --- /dev/null +++ b/ccxt/pro/kucoin.py @@ -0,0 +1,1418 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolById, ArrayCacheByTimestamp +from ccxt.base.types import Any, Balances, Bool, Int, Order, OrderBook, Str, Strings, Ticker, Tickers, Trade +from ccxt.async_support.base.ws.client import Client +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import ArgumentsRequired + + +class kucoin(ccxt.async_support.kucoin): + + def describe(self) -> Any: + return self.deep_extend(super(kucoin, self).describe(), { + 'has': { + 'ws': True, + 'createOrderWs': False, + 'editOrderWs': False, + 'fetchOpenOrdersWs': False, + 'fetchOrderWs': False, + 'cancelOrderWs': False, + 'cancelOrdersWs': False, + 'cancelAllOrdersWs': False, + 'watchBidsAsks': True, + 'watchOrderBook': True, + 'watchOrders': True, + 'watchMyTrades': True, + 'watchTickers': True, + 'watchTicker': True, + 'watchTrades': True, + 'watchTradesForSymbols': True, + 'watchOrderBookForSymbols': True, + 'watchBalance': True, + 'watchOHLCV': True, + 'unWatchTicker': True, + 'unWatchOHLCV': True, + 'unWatchOrderBook': True, + 'unWatchTrades': True, + 'unWatchhTradesForSymbols': True, + }, + 'options': { + 'tradesLimit': 1000, + 'watchTicker': { + 'name': 'market/snapshot', # market/ticker + }, + 'watchOrderBook': { + 'snapshotDelay': 5, + 'snapshotMaxRetries': 3, + 'method': '/market/level2', # '/spotMarket/level2Depth5' or '/spotMarket/level2Depth50' + }, + 'watchMyTrades': { + 'method': '/spotMarket/tradeOrders', # or '/spot/tradeFills' + }, + }, + 'streaming': { + # kucoin does not support built-in ws protocol-level ping-pong + # instead it requires a custom json-based text ping-pong + # https://docs.kucoin.com/#ping + 'ping': self.ping, + }, + }) + + async def negotiate(self, privateChannel, params={}): + connectId = 'private' if privateChannel else 'public' + urls = self.safe_value(self.options, 'urls', {}) + future = self.safe_value(urls, connectId) + if future is not None: + return await future + # we store an awaitable to the url + # so that multiple calls don't asynchronously + # fetch different urls and overwrite each other + urls[connectId] = self.spawn(self.negotiate_helper, privateChannel, params) + self.options['urls'] = urls + future = urls[connectId] + return await future + + async def negotiate_helper(self, privateChannel, params={}): + response = None + connectId = 'private' if privateChannel else 'public' + try: + if privateChannel: + response = await self.privatePostBulletPrivate(params) + # + # { + # "code": "200000", + # "data": { + # "instanceServers": [ + # { + # "pingInterval": 50000, + # "endpoint": "wss://push-private.kucoin.com/endpoint", + # "protocol": "websocket", + # "encrypt": True, + # "pingTimeout": 10000 + # } + # ], + # "token": "2neAiuYvAU61ZDXANAGAsiL4-iAExhsBXZxftpOeh_55i3Ysy2q2LEsEWU64mdzUOPusi34M_wGoSf7iNyEWJ1UQy47YbpY4zVdzilNP-Bj3iXzrjjGlWtiYB9J6i9GjsxUuhPw3BlrzazF6ghq4Lzf7scStOz3KkxjwpsOBCH4=.WNQmhZQeUKIkh97KYgU0Lg==" + # } + # } + # + else: + response = await self.publicPostBulletPublic(params) + data = self.safe_value(response, 'data', {}) + instanceServers = self.safe_value(data, 'instanceServers', []) + firstInstanceServer = self.safe_value(instanceServers, 0) + pingInterval = self.safe_integer(firstInstanceServer, 'pingInterval') + endpoint = self.safe_string(firstInstanceServer, 'endpoint') + token = self.safe_string(data, 'token') + result = endpoint + '?' + self.urlencode({ + 'token': token, + 'privateChannel': privateChannel, + 'connectId': connectId, + }) + client = self.client(result) + client.keepAlive = pingInterval + return result + except Exception as e: + future = self.safe_value(self.options['urls'], connectId) + future.reject(e) + del self.options['urls'][connectId] + return None + + def request_id(self): + requestId = self.sum(self.safe_integer(self.options, 'requestId', 0), 1) + self.options['requestId'] = requestId + return requestId + + async def subscribe(self, url, messageHash, subscriptionHash, params={}, subscription=None): + requestId = str(self.request_id()) + request: dict = { + 'id': requestId, + 'type': 'subscribe', + 'topic': subscriptionHash, + 'response': True, + } + message = self.extend(request, params) + client = self.client(url) + if not (subscriptionHash in client.subscriptions): + client.subscriptions[requestId] = subscriptionHash + return await self.watch(url, messageHash, message, subscriptionHash, subscription) + + async def un_subscribe(self, url, messageHash, topic, subscriptionHash, params={}, subscription: dict = None): + return await self.un_subscribe_multiple(url, [messageHash], topic, [subscriptionHash], params, subscription) + + async def subscribe_multiple(self, url, messageHashes, topic, subscriptionHashes, params={}, subscription=None): + requestId = str(self.request_id()) + request: dict = { + 'id': requestId, + 'type': 'subscribe', + 'topic': topic, + 'response': True, + } + message = self.extend(request, params) + client = self.client(url) + for i in range(0, len(subscriptionHashes)): + subscriptionHash = subscriptionHashes[i] + if not (subscriptionHash in client.subscriptions): + client.subscriptions[requestId] = subscriptionHash + return await self.watch_multiple(url, messageHashes, message, subscriptionHashes, subscription) + + async def un_subscribe_multiple(self, url, messageHashes, topic, subscriptionHashes, params={}, subscription: dict = None): + requestId = str(self.request_id()) + request: dict = { + 'id': requestId, + 'type': 'unsubscribe', + 'topic': topic, + 'response': True, + } + message = self.extend(request, params) + if subscription is not None: + subscription[requestId] = requestId + client = self.client(url) + for i in range(0, len(subscriptionHashes)): + subscriptionHash = subscriptionHashes[i] + if not (subscriptionHash in client.subscriptions): + client.subscriptions[requestId] = subscriptionHash + return await self.watch_multiple(url, messageHashes, message, subscriptionHashes, subscription) + + async def watch_ticker(self, symbol: str, params={}) -> Ticker: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://www.kucoin.com/docs/websocket/spot-trading/public-channels/market-snapshot + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + url = await self.negotiate(False) + method, query = self.handle_option_and_params(params, 'watchTicker', 'method', '/market/snapshot') + topic = method + ':' + market['id'] + messageHash = 'ticker:' + symbol + return await self.subscribe(url, messageHash, topic, query) + + async def un_watch_ticker(self, symbol: str, params={}) -> Ticker: + """ + unWatches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://www.kucoin.com/docs/websocket/spot-trading/public-channels/market-snapshot + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + url = await self.negotiate(False) + method = None + method, params = self.handle_option_and_params(params, 'watchTicker', 'method', '/market/snapshot') + topic = method + ':' + market['id'] + messageHash = 'unsubscribe:ticker:' + symbol + subMessageHash = 'ticker:' + symbol + subscription = { + 'messageHashes': [messageHash], + 'subMessageHashes': [subMessageHash], + 'topic': 'trades', + 'unsubscribe': True, + 'symbols': [symbol], + } + return await self.un_subscribe(url, messageHash, topic, subMessageHash, params, subscription) + + async def watch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + + https://www.kucoin.com/docs/websocket/spot-trading/public-channels/ticker + + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list + :param str[] symbols: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.method]: either '/market/snapshot' or '/market/ticker' default is '/market/ticker' + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + messageHash = 'tickers' + method = None + method, params = self.handle_option_and_params(params, 'watchTickers', 'method', '/market/ticker') + messageHashes = [] + topics = [] + if symbols is not None: + for i in range(0, len(symbols)): + symbol = symbols[i] + messageHashes.append('ticker:' + symbol) + market = self.market(symbol) + topics.append(method + ':' + market['id']) + url = await self.negotiate(False) + tickers = None + if symbols is None: + allTopic = method + ':all' + tickers = await self.subscribe(url, messageHash, allTopic, params) + if self.newUpdates: + return tickers + else: + marketIds = self.market_ids(symbols) + symbolsTopic = method + ':' + ','.join(marketIds) + tickers = await self.subscribe_multiple(url, messageHashes, symbolsTopic, topics, params) + if self.newUpdates: + newDict: dict = {} + newDict[tickers['symbol']] = tickers + return newDict + return self.filter_by_array(self.tickers, 'symbol', symbols) + + def handle_ticker(self, client: Client, message): + # + # market/snapshot + # + # updates come in every 2 sec unless there + # were no changes since the previous update + # + # { + # "data": { + # "sequence": "1545896669291", + # "data": { + # "trading": True, + # "symbol": "KCS-BTC", + # "buy": 0.00011, + # "sell": 0.00012, + # "sort": 100, + # "volValue": 3.13851792584, # total + # "baseCurrency": "KCS", + # "market": "BTC", + # "quoteCurrency": "BTC", + # "symbolCode": "KCS-BTC", + # "datetime": 1548388122031, + # "high": 0.00013, + # "vol": 27514.34842, + # "low": 0.0001, + # "changePrice": -1.0e-5, + # "changeRate": -0.0769, + # "lastTradedPrice": 0.00012, + # "board": 0, + # "mark": 0 + # } + # }, + # "subject": "trade.snapshot", + # "topic": "/market/snapshot:KCS-BTC", + # "type": "message" + # } + # + # market/ticker + # + # { + # "type": "message", + # "topic": "/market/ticker:BTC-USDT", + # "subject": "trade.ticker", + # "data": { + # "bestAsk": "62163", + # "bestAskSize": "0.99011388", + # "bestBid": "62162.9", + # "bestBidSize": "0.04794181", + # "price": "62162.9", + # "sequence": "1621383371852", + # "size": "0.00832274", + # "time": 1634641987564 + # } + # } + # + topic = self.safe_string(message, 'topic') + market = None + if topic is not None: + parts = topic.split(':') + first = self.safe_string(parts, 1) + marketId = None + if first == 'all': + marketId = self.safe_string(message, 'subject') + else: + marketId = first + market = self.safe_market(marketId, market, '-') + data = self.safe_value(message, 'data', {}) + rawTicker = self.safe_value(data, 'data', data) + ticker = self.parse_ticker(rawTicker, market) + symbol = ticker['symbol'] + self.tickers[symbol] = ticker + messageHash = 'ticker:' + symbol + client.resolve(ticker, messageHash) + # watchTickers + allTickers: dict = {} + allTickers[symbol] = ticker + client.resolve(allTickers, 'tickers') + + async def watch_bids_asks(self, symbols: Strings = None, params={}) -> Tickers: + """ + + https://www.kucoin.com/docs/websocket/spot-trading/public-channels/level1-bbo-market-data + + watches best bid & ask for symbols + :param str[] symbols: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + ticker = await self.watch_multi_helper('watchBidsAsks', '/spotMarket/level1:', symbols, params) + if self.newUpdates: + tickers: dict = {} + tickers[ticker['symbol']] = ticker + return tickers + return self.filter_by_array(self.bidsasks, 'symbol', symbols) + + async def watch_multi_helper(self, methodName, channelName: str, symbols: Strings = None, params={}): + await self.load_markets() + symbols = self.market_symbols(symbols, None, False, True, False) + length = len(symbols) + if length > 100: + raise ArgumentsRequired(self.id + ' ' + methodName + '() accepts a maximum of 100 symbols') + messageHashes = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + market = self.market(symbol) + messageHashes.append('bidask@' + market['symbol']) + url = await self.negotiate(False) + marketIds = self.market_ids(symbols) + joined = ','.join(marketIds) + requestId = str(self.request_id()) + request: dict = { + 'id': requestId, + 'type': 'subscribe', + 'topic': channelName + joined, + 'response': True, + } + message = self.extend(request, params) + return await self.watch_multiple(url, messageHashes, message, messageHashes) + + def handle_bid_ask(self, client: Client, message): + # + # arrives one symbol dict + # + # { + # topic: '/spotMarket/level1:ETH-USDT', + # type: 'message', + # data: { + # asks: ['3347.42', '2.0778387'], + # bids: ['3347.41', '6.0411697'], + # timestamp: 1712231142085 + # }, + # subject: 'level1' + # } + # + parsedTicker = self.parse_ws_bid_ask(message) + symbol = parsedTicker['symbol'] + self.bidsasks[symbol] = parsedTicker + messageHash = 'bidask@' + symbol + client.resolve(parsedTicker, messageHash) + + def parse_ws_bid_ask(self, ticker, market=None): + topic = self.safe_string(ticker, 'topic') + parts = topic.split(':') + marketId = parts[1] + market = self.safe_market(marketId, market) + symbol = self.safe_string(market, 'symbol') + data = self.safe_dict(ticker, 'data', {}) + ask = self.safe_list(data, 'asks', []) + bid = self.safe_list(data, 'bids', []) + timestamp = self.safe_integer(data, 'timestamp') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'ask': self.safe_number(ask, 0), + 'askVolume': self.safe_number(ask, 1), + 'bid': self.safe_number(bid, 0), + 'bidVolume': self.safe_number(bid, 1), + 'info': ticker, + }, market) + + async def watch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + watches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://www.kucoin.com/docs/websocket/spot-trading/public-channels/klines + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + url = await self.negotiate(False) + market = self.market(symbol) + symbol = market['symbol'] + period = self.safe_string(self.timeframes, timeframe, timeframe) + topic = '/market/candles:' + market['id'] + '_' + period + messageHash = 'candles:' + symbol + ':' + timeframe + ohlcv = await self.subscribe(url, messageHash, topic, params) + if self.newUpdates: + limit = ohlcv.getLimit(symbol, limit) + return self.filter_by_since_limit(ohlcv, since, limit, 0, True) + + async def un_watch_ohlcv(self, symbol: str, timeframe: str = '1m', params={}) -> List[list]: + """ + unWatches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://www.kucoin.com/docs/websocket/spot-trading/public-channels/klines + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + url = await self.negotiate(False) + market = self.market(symbol) + symbol = market['symbol'] + period = self.safe_string(self.timeframes, timeframe, timeframe) + topic = '/market/candles:' + market['id'] + '_' + period + messageHash = 'unsubscribe:candles:' + symbol + ':' + timeframe + subMessageHash = 'candles:' + symbol + ':' + timeframe + subscription = { + 'messageHashes': [messageHash], + 'subMessageHashes': [subMessageHash], + 'topic': 'ohlcv', + 'unsubscribe': True, + 'symbols': [symbol], + } + return await self.un_subscribe(url, messageHash, topic, messageHash, params, subscription) + + def handle_ohlcv(self, client: Client, message): + # + # { + # "data": { + # "symbol": "BTC-USDT", + # "candles": [ + # "1624881240", + # "34138.8", + # "34121.6", + # "34138.8", + # "34097.9", + # "3.06097133", + # "104430.955068564" + # ], + # "time": 1624881284466023700 + # }, + # "subject": "trade.candles.update", + # "topic": "/market/candles:BTC-USDT_1min", + # "type": "message" + # } + # + data = self.safe_value(message, 'data', {}) + marketId = self.safe_string(data, 'symbol') + candles = self.safe_value(data, 'candles', []) + topic = self.safe_string(message, 'topic') + parts = topic.split('_') + interval = self.safe_string(parts, 1) + # use a reverse lookup in a static map instead + timeframe = self.find_timeframe(interval) + market = self.safe_market(marketId) + symbol = market['symbol'] + messageHash = 'candles:' + symbol + ':' + timeframe + self.ohlcvs[symbol] = self.safe_value(self.ohlcvs, symbol, {}) + stored = self.safe_value(self.ohlcvs[symbol], timeframe) + if stored is None: + limit = self.safe_integer(self.options, 'OHLCVLimit', 1000) + stored = ArrayCacheByTimestamp(limit) + self.ohlcvs[symbol][timeframe] = stored + ohlcv = self.parse_ohlcv(candles, market) + stored.append(ohlcv) + client.resolve(stored, messageHash) + + async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://www.kucoin.com/docs/websocket/spot-trading/public-channels/match-execution-data + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + return await self.watch_trades_for_symbols([symbol], since, limit, params) + + async def watch_trades_for_symbols(self, symbols: List[str], since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://www.kucoin.com/docs/websocket/spot-trading/public-channels/match-execution-data + + :param str[] symbols: + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + symbolsLength = len(symbols) + if symbolsLength == 0: + raise ArgumentsRequired(self.id + ' watchTradesForSymbols() requires a non-empty array of symbols') + await self.load_markets() + symbols = self.market_symbols(symbols) + marketIds = self.market_ids(symbols) + url = await self.negotiate(False) + messageHashes = [] + subscriptionHashes = [] + topic = '/market/match:' + ','.join(marketIds) + for i in range(0, len(symbols)): + symbol = symbols[i] + messageHashes.append('trades:' + symbol) + marketId = marketIds[i] + subscriptionHashes.append('/market/match:' + marketId) + trades = await self.subscribe_multiple(url, messageHashes, topic, subscriptionHashes, params) + if self.newUpdates: + first = self.safe_value(trades, 0) + tradeSymbol = self.safe_string(first, 'symbol') + limit = trades.getLimit(tradeSymbol, limit) + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + async def un_watch_trades_for_symbols(self, symbols: List[str], params={}) -> Any: + """ + unWatches trades stream + + https://www.kucoin.com/docs/websocket/spot-trading/public-channels/match-execution-data + + :param str symbols: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, False) + marketIds = self.market_ids(symbols) + url = await self.negotiate(False) + messageHashes = [] + subscriptionHashes = [] + topic = '/market/match:' + ','.join(marketIds) + for i in range(0, len(symbols)): + symbol = symbols[i] + messageHashes.append('unsubscribe:trades:' + symbol) + subscriptionHashes.append('trades:' + symbol) + subscription = { + 'messageHashes': messageHashes, + 'subMessageHashes': subscriptionHashes, + 'topic': 'trades', + 'unsubscribe': True, + 'symbols': symbols, + } + return await self.un_subscribe_multiple(url, messageHashes, topic, messageHashes, params, subscription) + + async def un_watch_trades(self, symbol: str, params={}) -> Any: + """ + unWatches trades stream + + https://www.kucoin.com/docs/websocket/spot-trading/public-channels/match-execution-data + + :param str symbol: unified symbol of the market to fetch trades for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + return await self.un_watch_trades_for_symbols([symbol], params) + + def handle_trade(self, client: Client, message): + # + # { + # "data": { + # "sequence": "1568787654360", + # "symbol": "BTC-USDT", + # "side": "buy", + # "size": "0.00536577", + # "price": "9345", + # "takerOrderId": "5e356c4a9f1a790008f8d921", + # "time": "1580559434436443257", + # "type": "match", + # "makerOrderId": "5e356bffedf0010008fa5d7f", + # "tradeId": "5e356c4aeefabd62c62a1ece" + # }, + # "subject": "trade.l3match", + # "topic": "/market/match:BTC-USDT", + # "type": "message" + # } + # + data = self.safe_value(message, 'data', {}) + trade = self.parse_trade(data) + symbol = trade['symbol'] + messageHash = 'trades:' + symbol + trades = self.safe_value(self.trades, symbol) + if trades is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + trades = ArrayCache(limit) + self.trades[symbol] = trades + trades.append(trade) + client.resolve(trades, messageHash) + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + + https://www.kucoin.com/docs/websocket/spot-trading/public-channels/level1-bbo-market-data + https://www.kucoin.com/docs/websocket/spot-trading/public-channels/level2-market-data + https://www.kucoin.com/docs/websocket/spot-trading/public-channels/level2-5-best-ask-bid-orders + https://www.kucoin.com/docs/websocket/spot-trading/public-channels/level2-50-best-ask-bid-orders + + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.method]: either '/market/level2' or '/spotMarket/level2Depth5' or '/spotMarket/level2Depth50' default is '/market/level2' + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + # + # https://docs.kucoin.com/#level-2-market-data + # + # 1. After receiving the websocket Level 2 data flow, cache the data. + # 2. Initiate a REST request to get the snapshot data of Level 2 order book. + # 3. Playback the cached Level 2 data flow. + # 4. Apply the new Level 2 data flow to the local snapshot to ensure that + # the sequence of the new Level 2 update lines up with the sequence of + # the previous Level 2 data. Discard all the message prior to that + # sequence, and then playback the change to snapshot. + # 5. Update the level2 full data based on sequence according to the + # size. If the price is 0, ignore the messages and update the sequence. + # If the size=0, update the sequence and remove the price of which the + # size is 0 out of level 2. Fr other cases, please update the price. + # + return await self.watch_order_book_for_symbols([symbol], limit, params) + + async def un_watch_order_book(self, symbol: str, params={}) -> Any: + """ + + https://www.kucoin.com/docs/websocket/spot-trading/public-channels/level1-bbo-market-data + https://www.kucoin.com/docs/websocket/spot-trading/public-channels/level2-market-data + https://www.kucoin.com/docs/websocket/spot-trading/public-channels/level2-5-best-ask-bid-orders + https://www.kucoin.com/docs/websocket/spot-trading/public-channels/level2-50-best-ask-bid-orders + + unWatches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str symbol: unified symbol of the market to fetch the order book for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.method]: either '/market/level2' or '/spotMarket/level2Depth5' or '/spotMarket/level2Depth50' default is '/market/level2' + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + return await self.un_watch_order_book_for_symbols([symbol], params) + + async def watch_order_book_for_symbols(self, symbols: List[str], limit: Int = None, params={}) -> OrderBook: + """ + + https://www.kucoin.com/docs/websocket/spot-trading/public-channels/level1-bbo-market-data + https://www.kucoin.com/docs/websocket/spot-trading/public-channels/level2-market-data + https://www.kucoin.com/docs/websocket/spot-trading/public-channels/level2-5-best-ask-bid-orders + https://www.kucoin.com/docs/websocket/spot-trading/public-channels/level2-50-best-ask-bid-orders + + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str[] symbols: unified array of symbols + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.method]: either '/market/level2' or '/spotMarket/level2Depth5' or '/spotMarket/level2Depth50' default is '/market/level2' + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + symbolsLength = len(symbols) + if symbolsLength == 0: + raise ArgumentsRequired(self.id + ' watchOrderBookForSymbols() requires a non-empty array of symbols') + if limit is not None: + if (limit != 20) and (limit != 100) and (limit != 50) and (limit != 5): + raise ExchangeError(self.id + " watchOrderBook 'limit' argument must be None, 5, 20, 50 or 100") + await self.load_markets() + symbols = self.market_symbols(symbols) + marketIds = self.market_ids(symbols) + url = await self.negotiate(False) + method: Str = None + method, params = self.handle_option_and_params(params, 'watchOrderBook', 'method', '/market/level2') + if (limit == 5) or (limit == 50): + method = '/spotMarket/level2Depth' + str(limit) + topic = method + ':' + ','.join(marketIds) + messageHashes = [] + subscriptionHashes = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + messageHashes.append('orderbook:' + symbol) + marketId = marketIds[i] + subscriptionHashes.append(method + ':' + marketId) + subscription = {} + if method == '/market/level2': # other streams return the entire orderbook, so we don't need to fetch the snapshot through REST + subscription = { + 'method': self.handle_order_book_subscription, + 'symbols': symbols, + 'limit': limit, + } + orderbook = await self.subscribe_multiple(url, messageHashes, topic, subscriptionHashes, params, subscription) + return orderbook.limit() + + async def un_watch_order_book_for_symbols(self, symbols: List[str], params={}) -> Any: + """ + + https://www.kucoin.com/docs/websocket/spot-trading/public-channels/level1-bbo-market-data + https://www.kucoin.com/docs/websocket/spot-trading/public-channels/level2-market-data + https://www.kucoin.com/docs/websocket/spot-trading/public-channels/level2-5-best-ask-bid-orders + https://www.kucoin.com/docs/websocket/spot-trading/public-channels/level2-50-best-ask-bid-orders + + unWatches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str[] symbols: unified array of symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.method]: either '/market/level2' or '/spotMarket/level2Depth5' or '/spotMarket/level2Depth50' default is '/market/level2' + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + limit = self.safe_integer(params, 'limit') + params = self.omit(params, 'limit') + await self.load_markets() + symbols = self.market_symbols(symbols, None, False) + marketIds = self.market_ids(symbols) + url = await self.negotiate(False) + method: Str = None + method, params = self.handle_option_and_params(params, 'watchOrderBook', 'method', '/market/level2') + if (limit == 5) or (limit == 50): + method = '/spotMarket/level2Depth' + str(limit) + topic = method + ':' + ','.join(marketIds) + messageHashes = [] + subscriptionHashes = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + messageHashes.append('unsubscribe:orderbook:' + symbol) + subscriptionHashes.append('orderbook:' + symbol) + subscription = { + 'messageHashes': messageHashes, + 'symbols': symbols, + 'unsubscribe': True, + 'topic': 'orderbook', + 'subMessageHashes': subscriptionHashes, + } + return await self.un_subscribe_multiple(url, messageHashes, topic, messageHashes, params, subscription) + + def handle_order_book(self, client: Client, message): + # + # initial snapshot is fetched with ccxt's fetchOrderBook + # the feed does not include a snapshot, just the deltas + # + # { + # "type":"message", + # "topic":"/market/level2:BTC-USDT", + # "subject":"trade.l2update", + # "data":{ + # "sequenceStart":1545896669105, + # "sequenceEnd":1545896669106, + # "symbol":"BTC-USDT", + # "changes": { + # "asks": [["6","1","1545896669105"]], # price, size, sequence + # "bids": [["4","1","1545896669106"]] + # } + # } + # } + # + # { + # "topic": "/spotMarket/level2Depth5:BTC-USDT", + # "type": "message", + # "data": { + # "asks": [ + # [ + # "42815.6", + # "1.24016245" + # ] + # ], + # "bids": [ + # [ + # "42815.5", + # "0.08652716" + # ] + # ], + # "timestamp": 1707204474018 + # }, + # "subject": "level2" + # } + # + data = self.safe_value(message, 'data') + subject = self.safe_string(message, 'subject') + topic = self.safe_string(message, 'topic') + topicParts = topic.split(':') + topicSymbol = self.safe_string(topicParts, 1) + topicChannel = self.safe_string(topicParts, 0) + marketId = self.safe_string(data, 'symbol', topicSymbol) + symbol = self.safe_symbol(marketId, None, '-') + messageHash = 'orderbook:' + symbol + # orderbook = self.safe_dict(self.orderbooks, symbol) + if subject == 'level2': + if not (symbol in self.orderbooks): + self.orderbooks[symbol] = self.order_book() + else: + orderbook = self.orderbooks[symbol] + orderbook.reset() + self.orderbooks[symbol]['symbol'] = symbol + else: + if not (symbol in self.orderbooks): + self.orderbooks[symbol] = self.order_book() + orderbook = self.orderbooks[symbol] + nonce = self.safe_integer(orderbook, 'nonce') + deltaEnd = self.safe_integer_2(data, 'sequenceEnd', 'timestamp') + if nonce is None: + cacheLength = len(orderbook.cache) + subscriptions = list(client.subscriptions.keys()) + subscription = None + for i in range(0, len(subscriptions)): + key = subscriptions[i] + if (key.find(topicSymbol) >= 0) and (key.find(topicChannel) >= 0): + subscription = client.subscriptions[key] + break + limit = self.safe_integer(subscription, 'limit') + snapshotDelay = self.handle_option('watchOrderBook', 'snapshotDelay', 5) + if cacheLength == snapshotDelay: + self.spawn(self.load_order_book, client, messageHash, symbol, limit, {}) + orderbook.cache.append(data) + return + elif nonce >= deltaEnd: + return + self.handle_delta(self.orderbooks[symbol], data) + client.resolve(self.orderbooks[symbol], messageHash) + + def get_cache_index(self, orderbook, cache): + firstDelta = self.safe_value(cache, 0) + nonce = self.safe_integer(orderbook, 'nonce') + firstDeltaStart = self.safe_integer(firstDelta, 'sequenceStart') + if nonce < firstDeltaStart - 1: + return -1 + for i in range(0, len(cache)): + delta = cache[i] + deltaStart = self.safe_integer(delta, 'sequenceStart') + deltaEnd = self.safe_integer(delta, 'sequenceEnd') + if (nonce >= deltaStart - 1) and (nonce < deltaEnd): + return i + return len(cache) + + def handle_delta(self, orderbook, delta): + timestamp = self.safe_integer_2(delta, 'time', 'timestamp') + orderbook['nonce'] = self.safe_integer(delta, 'sequenceEnd', timestamp) + orderbook['timestamp'] = timestamp + orderbook['datetime'] = self.iso8601(timestamp) + changes = self.safe_value(delta, 'changes', delta) + bids = self.safe_value(changes, 'bids', []) + asks = self.safe_value(changes, 'asks', []) + storedBids = orderbook['bids'] + storedAsks = orderbook['asks'] + self.handle_bid_asks(storedBids, bids) + self.handle_bid_asks(storedAsks, asks) + + def handle_bid_asks(self, bookSide, bidAsks): + for i in range(0, len(bidAsks)): + bidAsk = self.parse_bid_ask(bidAsks[i]) + bookSide.storeArray(bidAsk) + + def handle_order_book_subscription(self, client: Client, message, subscription): + limit = self.safe_integer(subscription, 'limit') + symbols = self.safe_value(subscription, 'symbols') + if symbols is None: + symbol = self.safe_string(subscription, 'symbol') + self.orderbooks[symbol] = self.order_book({}, limit) + else: + for i in range(0, len(symbols)): + symbol = symbols[i] + self.orderbooks[symbol] = self.order_book({}, limit) + # moved snapshot initialization to handleOrderBook to fix + # https://github.com/ccxt/ccxt/issues/6820 + # the general idea is to fetch the snapshot after the first delta + # but not before, because otherwise we cannot synchronize the feed + + def handle_subscription_status(self, client: Client, message): + # + # { + # "id": "1578090438322", + # "type": "ack" + # } + # + id = self.safe_string(message, 'id') + if not (id in client.subscriptions): + return + subscriptionHash = self.safe_string(client.subscriptions, id) + subscription = self.safe_value(client.subscriptions, subscriptionHash) + del client.subscriptions[id] + method = self.safe_value(subscription, 'method') + if method is not None: + method(client, message, subscription) + isUnSub = self.safe_bool(subscription, 'unsubscribe', False) + if isUnSub: + messageHashes = self.safe_list(subscription, 'messageHashes', []) + subMessageHashes = self.safe_list(subscription, 'subMessageHashes', []) + for i in range(0, len(messageHashes)): + messageHash = messageHashes[i] + subHash = subMessageHashes[i] + self.clean_unsubscription(client, subHash, messageHash) + self.clean_cache(subscription) + + def handle_system_status(self, client: Client, message): + # + # todo: answer the question whether handleSystemStatus should be renamed + # and unified for any usage pattern that + # involves system status and maintenance updates + # + # { + # "id": "1578090234088", # connectId + # "type": "welcome", + # } + # + return message + + async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + watches information on multiple orders made by the user + + https://www.kucoin.com/docs/websocket/spot-trading/private-channels/private-order-change + https://www.kucoin.com/docs/websocket/spot-trading/private-channels/stop-order-event + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: trigger orders are watched if True + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + trigger = self.safe_value_2(params, 'stop', 'trigger') + params = self.omit(params, ['stop', 'trigger']) + url = await self.negotiate(True) + topic = '/spotMarket/advancedOrders' if trigger else '/spotMarket/tradeOrders' + request: dict = { + 'privateChannel': True, + } + messageHash = 'orders' + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + messageHash = messageHash + ':' + symbol + orders = await self.subscribe(url, messageHash, topic, self.extend(request, params)) + if self.newUpdates: + limit = orders.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(orders, symbol, since, limit, True) + + def parse_ws_order_status(self, status): + statuses: dict = { + 'open': 'open', + 'filled': 'closed', + 'match': 'open', + 'update': 'open', + 'canceled': 'canceled', + 'cancel': 'canceled', + 'TRIGGERED': 'triggered', + } + return self.safe_string(statuses, status, status) + + def parse_ws_order(self, order, market=None): + # + # /spotMarket/tradeOrders + # + # { + # "symbol": "XCAD-USDT", + # "orderType": "limit", + # "side": "buy", + # "orderId": "6249167327218b000135e749", + # "type": "canceled", + # "orderTime": 1648957043065280224, + # "size": "100.452", + # "filledSize": "0", + # "price": "2.9635", + # "clientOid": "buy-XCAD-USDT-1648957043010159", + # "remainSize": "0", + # "status": "done", + # "ts": 1648957054031001037 + # } + # + # /spotMarket/advancedOrders + # + # { + # "createdAt": 1589789942337, + # "orderId": "5ec244f6a8a75e0009958237", + # "orderPrice": "0.00062", + # "orderType": "stop", + # "side": "sell", + # "size": "1", + # "stop": "entry", + # "stopPrice": "0.00062", + # "symbol": "KCS-BTC", + # "tradeType": "TRADE", + # "triggerSuccess": True, + # "ts": 1589790121382281286, + # "type": "triggered" + # } + # + rawType = self.safe_string(order, 'type') + status = self.parse_ws_order_status(rawType) + timestamp = self.safe_integer_2(order, 'orderTime', 'createdAt') + marketId = self.safe_string(order, 'symbol') + market = self.safe_market(marketId, market) + triggerPrice = self.safe_string(order, 'stopPrice') + triggerSuccess = self.safe_value(order, 'triggerSuccess') + triggerFail = (triggerSuccess is not True) and (triggerSuccess is not None) # TODO: updated to triggerSuccess == False once transpiler transpiles it correctly + if (status == 'triggered') and triggerFail: + status = 'canceled' + return self.safe_order({ + 'info': order, + 'symbol': market['symbol'], + 'id': self.safe_string(order, 'orderId'), + 'clientOrderId': self.safe_string(order, 'clientOid'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'type': self.safe_string_lower(order, 'orderType'), + 'timeInForce': None, + 'postOnly': None, + 'side': self.safe_string_lower(order, 'side'), + 'price': self.safe_string_2(order, 'price', 'orderPrice'), + 'stopPrice': triggerPrice, + 'triggerPrice': triggerPrice, + 'amount': self.safe_string(order, 'size'), + 'cost': None, + 'average': None, + 'filled': self.safe_string(order, 'filledSize'), + 'remaining': None, + 'status': status, + 'fee': None, + 'trades': None, + }, market) + + def handle_order(self, client: Client, message): + # + # Trigger Orders + # + # { + # "createdAt": 1692745706437, + # "error": "Balance insufficient!", # not always there + # "orderId": "vs86kp757vlda6ni003qs70v", + # "orderPrice": "0.26", + # "orderType": "stop", + # "side": "sell", + # "size": "5", + # "stop": "loss", + # "stopPrice": "0.26", + # "symbol": "ADA-USDT", + # "tradeType": "TRADE", + # "triggerSuccess": False, # not always there + # "ts": "1692745706442929298", + # "type": "open" + # } + # + messageHash = 'orders' + data = self.safe_value(message, 'data') + tradeId = self.safe_string(data, 'tradeId') + if tradeId is not None: + self.handle_my_trade(client, message) + parsed = self.parse_ws_order(data) + symbol = self.safe_string(parsed, 'symbol') + orderId = self.safe_string(parsed, 'id') + triggerPrice = self.safe_value(parsed, 'triggerPrice') + isTriggerOrder = (triggerPrice is not None) + if self.orders is None: + limit = self.safe_integer(self.options, 'ordersLimit', 1000) + self.orders = ArrayCacheBySymbolById(limit) + self.triggerOrders = ArrayCacheBySymbolById(limit) + cachedOrders = self.triggerOrders if isTriggerOrder else self.orders + orders = self.safe_value(cachedOrders.hashmap, symbol, {}) + order = self.safe_value(orders, orderId) + if order is not None: + # todo add others to calculate average etc + if order['status'] == 'closed': + parsed['status'] = 'closed' + cachedOrders.append(parsed) + client.resolve(cachedOrders, messageHash) + symbolSpecificMessageHash = messageHash + ':' + symbol + client.resolve(cachedOrders, symbolSpecificMessageHash) + + async def watch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + watches information on multiple trades made by the user + + https://www.kucoin.com/docs/websocket/spot-trading/private-channels/private-order-change + + :param str symbol: unified market symbol of the market trades were made in + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trade structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.method]: '/spotMarket/tradeOrders' or '/spot/tradeFills' default is '/spotMarket/tradeOrders' + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + url = await self.negotiate(True) + topic: Str = None + topic, params = self.handle_option_and_params(params, 'watchMyTrades', 'method', '/spotMarket/tradeOrders') + request: dict = { + 'privateChannel': True, + } + messageHash = 'myTrades' + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + messageHash = messageHash + ':' + market['symbol'] + trades = await self.subscribe(url, messageHash, topic, self.extend(request, params)) + if self.newUpdates: + limit = trades.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(trades, symbol, since, limit, True) + + def handle_my_trade(self, client: Client, message): + # + # { + # "type": "message", + # "topic": "/spotMarket/tradeOrders", + # "subject": "orderChange", + # "channelType": "private", + # "data": { + # "symbol": "KCS-USDT", + # "orderType": "limit", + # "side": "sell", + # "orderId": "5efab07953bdea00089965fa", + # "liquidity": "taker", + # "type": "match", + # "feeType": "takerFee", + # "orderTime": 1670329987026, + # "size": "0.1", + # "filledSize": "0.1", + # "price": "0.938", + # "matchPrice": "0.96738", + # "matchSize": "0.1", + # "tradeId": "5efab07a4ee4c7000a82d6d9", + # "clientOid": "1593487481000313", + # "remainSize": "0", + # "status": "match", + # "ts": 1670329987311000000 + # } + # } + # + if self.myTrades is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + self.myTrades = ArrayCacheBySymbolById(limit) + data = self.safe_dict(message, 'data') + parsed = self.parse_ws_trade(data) + myTrades = self.myTrades + myTrades.append(parsed) + messageHash = 'myTrades' + client.resolve(self.myTrades, messageHash) + symbolSpecificMessageHash = messageHash + ':' + parsed['symbol'] + client.resolve(self.myTrades, symbolSpecificMessageHash) + + def parse_ws_trade(self, trade, market=None): + # + # /spotMarket/tradeOrders + # + # { + # "symbol": "KCS-USDT", + # "orderType": "limit", + # "side": "sell", + # "orderId": "5efab07953bdea00089965fa", + # "liquidity": "taker", + # "type": "match", + # "feeType": "takerFee", + # "orderTime": 1670329987026, + # "size": "0.1", + # "filledSize": "0.1", + # "price": "0.938", + # "matchPrice": "0.96738", + # "matchSize": "0.1", + # "tradeId": "5efab07a4ee4c7000a82d6d9", + # "clientOid": "1593487481000313", + # "remainSize": "0", + # "status": "match", + # "ts": 1670329987311000000 + # } + # + # /spot/tradeFills + # + # { + # "fee": 0.00262148, + # "feeCurrency": "USDT", + # "feeRate": 0.001, + # "orderId": "62417436b29df8000183df2f", + # "orderType": "market", + # "price": 131.074, + # "side": "sell", + # "size": 0.02, + # "symbol": "LTC-USDT", + # "time": "1648456758734571745", + # "tradeId": "624174362e113d2f467b3043" + # } + # + marketId = self.safe_string(trade, 'symbol') + market = self.safe_market(marketId, market, '-') + symbol = market['symbol'] + type = self.safe_string(trade, 'orderType') + side = self.safe_string(trade, 'side') + tradeId = self.safe_string(trade, 'tradeId') + price = self.safe_string(trade, 'matchPrice') + amount = self.safe_string(trade, 'matchSize') + if price is None: + # /spot/tradeFills + price = self.safe_string(trade, 'price') + amount = self.safe_string(trade, 'size') + order = self.safe_string(trade, 'orderId') + timestamp = self.safe_integer_product_2(trade, 'ts', 'time', 0.000001) + feeCurrency = market['quote'] + feeRate = self.safe_string(trade, 'feeRate') + feeCost = self.safe_string(trade, 'fee') + return self.safe_trade({ + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'id': tradeId, + 'order': order, + 'type': type, + 'takerOrMaker': self.safe_string(trade, 'liquidity'), + 'side': side, + 'price': price, + 'amount': amount, + 'cost': None, + 'fee': { + 'cost': feeCost, + 'rate': feeRate, + 'currency': feeCurrency, + }, + }, market) + + async def watch_balance(self, params={}) -> Balances: + """ + watch balance and get the amount of funds available for trading or funds locked in orders + + https://www.kucoin.com/docs/websocket/spot-trading/private-channels/account-balance-change + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + url = await self.negotiate(True) + topic = '/account/balance' + request: dict = { + 'privateChannel': True, + } + messageHash = 'balance' + return await self.subscribe(url, messageHash, topic, self.extend(request, params)) + + def handle_balance(self, client: Client, message): + # + # { + # "id":"6217a451294b030001e3a26a", + # "type":"message", + # "topic":"/account/balance", + # "userId":"6217707c52f97f00012a67db", + # "channelType":"private", + # "subject":"account.balance", + # "data":{ + # "accountId":"62177fe67810720001db2f18", + # "available":"89", + # "availableChange":"-30", + # "currency":"USDT", + # "hold":"0", + # "holdChange":"0", + # "relationContext":{ + # }, + # "relationEvent":"main.transfer", + # "relationEventId":"6217a451294b030001e3a26a", + # "time":"1645716561816", + # "total":"89" + # } + # + data = self.safe_value(message, 'data', {}) + messageHash = 'balance' + currencyId = self.safe_string(data, 'currency') + relationEvent = self.safe_string(data, 'relationEvent') + requestAccountType = None + if relationEvent is not None: + relationEventParts = relationEvent.split('.') + requestAccountType = self.safe_string(relationEventParts, 0) + selectedType = self.safe_string_2(self.options, 'watchBalance', 'defaultType', 'trade') # trade, main, margin or other + accountsByType = self.safe_value(self.options, 'accountsByType') + uniformType = self.safe_string(accountsByType, requestAccountType, 'trade') + if not (uniformType in self.balance): + self.balance[uniformType] = {} + self.balance[uniformType]['info'] = data + timestamp = self.safe_integer(data, 'time') + self.balance[uniformType]['timestamp'] = timestamp + self.balance[uniformType]['datetime'] = self.iso8601(timestamp) + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(data, 'available') + account['used'] = self.safe_string(data, 'hold') + account['total'] = self.safe_string(data, 'total') + self.balance[uniformType][code] = account + self.balance[uniformType] = self.safe_balance(self.balance[uniformType]) + if uniformType == selectedType: + client.resolve(self.balance[uniformType], messageHash) + + def handle_subject(self, client: Client, message): + # + # { + # "type":"message", + # "topic":"/market/level2:BTC-USDT", + # "subject":"trade.l2update", + # "data":{ + # "sequenceStart":1545896669105, + # "sequenceEnd":1545896669106, + # "symbol":"BTC-USDT", + # "changes": { + # "asks": [["6","1","1545896669105"]], # price, size, sequence + # "bids": [["4","1","1545896669106"]] + # } + # } + # } + # + topic = self.safe_string(message, 'topic') + if topic == '/market/ticker:all': + self.handle_ticker(client, message) + return + subject = self.safe_string(message, 'subject') + methods: dict = { + 'level1': self.handle_bid_ask, + 'level2': self.handle_order_book, + 'trade.l2update': self.handle_order_book, + 'trade.ticker': self.handle_ticker, + 'trade.snapshot': self.handle_ticker, + 'trade.l3match': self.handle_trade, + 'trade.candles.update': self.handle_ohlcv, + 'account.balance': self.handle_balance, + 'orderChange': self.handle_order, + 'stopOrder': self.handle_order, + '/spot/tradeFills': self.handle_my_trade, + } + method = self.safe_value(methods, subject) + if method is not None: + method(client, message) + + def ping(self, client: Client): + # kucoin does not support built-in ws protocol-level ping-pong + # instead it requires a custom json-based text ping-pong + # https://docs.kucoin.com/#ping + id = str(self.request_id()) + return { + 'id': id, + 'type': 'ping', + } + + def handle_pong(self, client: Client, message): + client.lastPong = self.milliseconds() + # https://docs.kucoin.com/#ping + + def handle_error_message(self, client: Client, message) -> Bool: + # + # { + # "id": "1", + # "type": "error", + # "code": 415, + # "data": "type is not supported" + # } + # + data = self.safe_string(message, 'data', '') + if data == 'token is expired': + type = 'public' + if client.url.find('connectId=private') >= 0: + type = 'private' + self.options['urls'][type] = None + self.handle_errors(1, '', client.url, '', {}, data, message, {}, {}) + return False + + def handle_message(self, client: Client, message): + type = self.safe_string(message, 'type') + methods: dict = { + # 'heartbeat': self.handleHeartbeat, + 'welcome': self.handle_system_status, + 'ack': self.handle_subscription_status, + 'message': self.handle_subject, + 'pong': self.handle_pong, + 'error': self.handle_error_message, + } + method = self.safe_value(methods, type) + if method is not None: + method(client, message) diff --git a/ccxt/pro/kucoinfutures.py b/ccxt/pro/kucoinfutures.py new file mode 100644 index 0000000..177f840 --- /dev/null +++ b/ccxt/pro/kucoinfutures.py @@ -0,0 +1,1225 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolById, ArrayCacheByTimestamp +from ccxt.base.types import Any, Balances, Bool, Int, Order, OrderBook, Position, Str, Strings, Ticker, Tickers, Trade +from ccxt.async_support.base.ws.client import Client +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import ArgumentsRequired + + +class kucoinfutures(ccxt.async_support.kucoinfutures): + + def describe(self) -> Any: + return self.deep_extend(super(kucoinfutures, self).describe(), { + 'has': { + 'ws': True, + 'watchLiquidations': False, + 'watchLiquidatinsForSymbols': False, + 'watchMyLiquidations': None, + 'watchMyLiquidationsForSymbols': None, + 'watchTicker': True, + 'watchTickers': True, + 'watchBidsAsks': True, + 'watchTrades': True, + 'watchOHLCV': True, + 'watchOrderBook': True, + 'watchOrders': True, + 'watchBalance': True, + 'watchPosition': True, + 'watchPositions': False, + 'watchPositionForSymbols': False, + 'watchTradesForSymbols': True, + 'watchOrderBookForSymbols': True, + }, + 'options': { + 'timeframes': { + '1m': '1min', + '3m': '1min', + '5m': '5min', + '15m': '15min', + '30m': '30min', + '1h': '1hour', + '2h': '2hour', + '4h': '4hour', + '8h': '8hour', + '12h': '12hour', + '1d': '1day', + '1w': '1week', + '1M': '1month', + }, + 'accountsByType': { + 'swap': 'future', + 'cross': 'margin', + # 'spot': , + # 'margin': , + # 'main': , + # 'funding': , + # 'future': , + # 'mining': , + # 'trade': , + # 'contract': , + # 'pool': , + }, + 'tradesLimit': 1000, + 'watchOrderBook': { + 'snapshotDelay': 20, + 'snapshotMaxRetries': 3, + }, + 'watchPosition': { + 'fetchPositionSnapshot': True, # or False + 'awaitPositionSnapshot': True, # whether to wait for the position snapshot before providing updates + }, + }, + 'streaming': { + # kucoin does not support built-in ws protocol-level ping-pong + # instead it requires a custom json-based text ping-pong + # https://docs.kucoin.com/#ping + 'ping': self.ping, + }, + }) + + async def negotiate(self, privateChannel, params={}): + connectId = 'private' if privateChannel else 'public' + urls = self.safe_value(self.options, 'urls', {}) + future = self.safe_value(urls, connectId) + if future is not None: + return await future + # we store an awaitable to the url + # so that multiple calls don't asynchronously + # fetch different urls and overwrite each other + urls[connectId] = self.spawn(self.negotiate_helper, privateChannel, params) # we have to wait here otherwsie in c# will not work + self.options['urls'] = urls + future = urls[connectId] + return await future + + async def negotiate_helper(self, privateChannel, params={}): + response = None + connectId = 'private' if privateChannel else 'public' + try: + if privateChannel: + response = await self.futuresPrivatePostBulletPrivate(params) + # + # { + # "code": "200000", + # "data": { + # "instanceServers": [ + # { + # "pingInterval": 50000, + # "endpoint": "wss://push-private.kucoin.com/endpoint", + # "protocol": "websocket", + # "encrypt": True, + # "pingTimeout": 10000 + # } + # ], + # "token": "2neAiuYvAU61ZDXANAGAsiL4-iAExhsBXZxftpOeh_55i3Ysy2q2LEsEWU64mdzUOPusi34M_wGoSf7iNyEWJ1UQy47YbpY4zVdzilNP-Bj3iXzrjjGlWtiYB9J6i9GjsxUuhPw3BlrzazF6ghq4Lzf7scStOz3KkxjwpsOBCH4=.WNQmhZQeUKIkh97KYgU0Lg==" + # } + # } + # + else: + response = await self.futuresPublicPostBulletPublic(params) + data = self.safe_value(response, 'data', {}) + instanceServers = self.safe_value(data, 'instanceServers', []) + firstInstanceServer = self.safe_value(instanceServers, 0) + pingInterval = self.safe_integer(firstInstanceServer, 'pingInterval') + endpoint = self.safe_string(firstInstanceServer, 'endpoint') + token = self.safe_string(data, 'token') + result = endpoint + '?' + self.urlencode({ + 'token': token, + 'privateChannel': privateChannel, + 'connectId': connectId, + }) + client = self.client(result) + client.keepAlive = pingInterval + return result + except Exception as e: + future = self.safe_value(self.options['urls'], connectId) + future.reject(e) + del self.options['urls'][connectId] + return None + + def request_id(self): + requestId = self.sum(self.safe_integer(self.options, 'requestId', 0), 1) + self.options['requestId'] = requestId + return requestId + + async def subscribe(self, url, messageHash, subscriptionHash, subscription, params={}): + requestId = str(self.request_id()) + request: dict = { + 'id': requestId, + 'type': 'subscribe', + 'topic': subscriptionHash, + 'response': True, + } + message = self.extend(request, params) + subscriptionRequest: dict = { + 'id': requestId, + } + if subscription is None: + subscription = subscriptionRequest + else: + subscription = self.extend(subscriptionRequest, subscription) + return await self.watch(url, messageHash, message, subscriptionHash, subscription) + + async def subscribe_multiple(self, url, messageHashes, topic, subscriptionHashes, subscriptionArgs, params={}): + requestId = str(self.request_id()) + request: dict = { + 'id': requestId, + 'type': 'subscribe', + 'topic': topic, + 'response': True, + } + return await self.watch_multiple(url, messageHashes, self.extend(request, params), subscriptionHashes, subscriptionArgs) + + async def un_subscribe_multiple(self, url, messageHashes, topic, subscriptionHashes, params={}, subscription: dict = None): + requestId = str(self.request_id()) + request: dict = { + 'id': requestId, + 'type': 'unsubscribe', + 'topic': topic, + 'response': True, + } + message = self.extend(request, params) + if subscription is not None: + subscription[requestId] = requestId + client = self.client(url) + for i in range(0, len(subscriptionHashes)): + subscriptionHash = subscriptionHashes[i] + if not (subscriptionHash in client.subscriptions): + client.subscriptions[requestId] = subscriptionHash + return await self.watch_multiple(url, messageHashes, message, subscriptionHashes, subscription) + + async def watch_ticker(self, symbol: str, params={}) -> Ticker: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://www.kucoin.com/docs/websocket/futures-trading/public-channels/get-ticker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + params['callerMethodName'] = 'watchTicker' + tickers = await self.watch_tickers([symbol], params) + return tickers[symbol] + + async def watch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list + :param str[] symbols: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + ticker = await self.watch_multi_request('watchTickers', '/contractMarket/ticker:', symbols, params) + if self.newUpdates: + tickers: dict = {} + tickers[ticker['symbol']] = ticker + return tickers + return self.filter_by_array(self.tickers, 'symbol', symbols) + + def handle_ticker(self, client: Client, message): + # + # ticker(v1) + # + # { + # "subject": "ticker", + # "topic": "/contractMarket/ticker:XBTUSDM", + # "data": { + # "symbol": "XBTUSDM", #Market of the symbol + # "sequence": 45, #Sequence number which is used to judge the continuity of the pushed messages + # "side": "sell", #Transaction side of the last traded taker order + # "price": "3600.0", #Filled price + # "size": 16, #Filled quantity + # "tradeId": "5c9dcf4170744d6f5a3d32fb", #Order ID + # "bestBidSize": 795, #Best bid size + # "bestBidPrice": "3200.0", #Best bid + # "bestAskPrice": "3600.0", #Best ask size + # "bestAskSize": 284, #Best ask + # "ts": 1553846081210004941 #Filled time - nanosecond + # } + # } + # + data = self.safe_value(message, 'data', {}) + marketId = self.safe_value(data, 'symbol') + market = self.safe_market(marketId, None, '-') + ticker = self.parse_ticker(data, market) + self.tickers[market['symbol']] = ticker + client.resolve(ticker, self.get_message_hash('ticker', market['symbol'])) + + async def watch_bids_asks(self, symbols: Strings = None, params={}) -> Tickers: + """ + + https://www.kucoin.com/docs/websocket/futures-trading/public-channels/get-ticker-v2 + + watches best bid & ask for symbols + :param str[] symbols: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + ticker = await self.watch_multi_request('watchBidsAsks', '/contractMarket/tickerV2:', symbols, params) + if self.newUpdates: + tickers: dict = {} + tickers[ticker['symbol']] = ticker + return tickers + return self.filter_by_array(self.bidsasks, 'symbol', symbols) + + async def watch_multi_request(self, methodName, channelName: str, symbols: Strings = None, params={}): + await self.load_markets() + methodName, params = self.handle_param_string(params, 'callerMethodName', methodName) + isBidsAsks = (methodName == 'watchBidsAsks') + symbols = self.market_symbols(symbols, None, False, True, False) + length = len(symbols) + if length > 100: + raise ArgumentsRequired(self.id + ' ' + methodName + '() accepts a maximum of 100 symbols') + messageHashes = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + market = self.market(symbol) + prefix = 'bidask' if isBidsAsks else 'ticker' + messageHashes.append(self.get_message_hash(prefix, market['symbol'])) + url = await self.negotiate(False) + marketIds = self.market_ids(symbols) + joined = ','.join(marketIds) + requestId = str(self.request_id()) + request: dict = { + 'id': requestId, + 'type': 'subscribe', + 'topic': channelName + joined, + 'response': True, + } + subscription: dict = { + 'id': requestId, + } + return await self.watch_multiple(url, messageHashes, self.extend(request, params), messageHashes, subscription) + + def handle_bid_ask(self, client: Client, message): + # + # arrives one symbol dict + # + # { + # "subject": "tickerV2", + # "topic": "/contractMarket/tickerV2:XBTUSDM", + # "data": { + # "symbol": "XBTUSDM", #Market of the symbol + # "bestBidSize": 795, # Best bid size + # "bestBidPrice": 3200.0, # Best bid + # "bestAskPrice": 3600.0, # Best ask + # "bestAskSize": 284, # Best ask size + # "ts": 1553846081210004941 # Filled time - nanosecond + # } + # } + # + parsedTicker = self.parse_ws_bid_ask(message) + symbol = parsedTicker['symbol'] + self.bidsasks[symbol] = parsedTicker + client.resolve(parsedTicker, self.get_message_hash('bidask', symbol)) + + def parse_ws_bid_ask(self, ticker, market=None): + data = self.safe_dict(ticker, 'data', {}) + marketId = self.safe_string(data, 'symbol') + market = self.safe_market(marketId, market) + symbol = self.safe_string(market, 'symbol') + timestamp = self.safe_integer_product(data, 'ts', 0.000001) + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'ask': self.safe_number(data, 'bestAskPrice'), + 'askVolume': self.safe_number(data, 'bestAskSize'), + 'bid': self.safe_number(data, 'bestBidPrice'), + 'bidVolume': self.safe_number(data, 'bestBidSize'), + 'info': ticker, + }, market) + + async def watch_position(self, symbol: Str = None, params={}) -> Position: + """ + watch open positions for a specific symbol + + https://docs.kucoin.com/futures/#position-change-events + + :param str|None symbol: unified market symbol + :param dict params: extra parameters specific to the exchange API endpoint + :returns dict: a `position structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' watchPosition() requires a symbol argument') + await self.load_markets() + url = await self.negotiate(True) + market = self.market(symbol) + topic = '/contract/position:' + market['id'] + request: dict = { + 'privateChannel': True, + } + messageHash = 'position:' + market['symbol'] + client = self.client(url) + self.set_position_cache(client, symbol) + fetchPositionSnapshot = self.handle_option('watchPosition', 'fetchPositionSnapshot', True) + awaitPositionSnapshot = self.handle_option('watchPosition', 'awaitPositionSnapshot', True) + currentPosition = self.get_current_position(symbol) + if fetchPositionSnapshot and awaitPositionSnapshot and currentPosition is None: + snapshot = await client.future('fetchPositionSnapshot:' + symbol) + return snapshot + return await self.subscribe(url, messageHash, topic, None, self.extend(request, params)) + + def get_current_position(self, symbol): + if self.positions is None: + return None + cache = self.positions.hashmap + symbolCache = self.safe_value(cache, symbol, {}) + values = list(symbolCache.values()) + return self.safe_value(values, 0) + + def set_position_cache(self, client: Client, symbol: str): + fetchPositionSnapshot = self.handle_option('watchPosition', 'fetchPositionSnapshot', False) + if fetchPositionSnapshot: + messageHash = 'fetchPositionSnapshot:' + symbol + if not (messageHash in client.futures): + client.future(messageHash) + self.spawn(self.load_position_snapshot, client, messageHash, symbol) + + async def load_position_snapshot(self, client, messageHash, symbol): + position = await self.fetch_position(symbol) + self.positions = ArrayCacheBySymbolById() + cache = self.positions + cache.append(position) + # don't remove the future from the .futures cache + future = client.futures[messageHash] + future.resolve(cache) + client.resolve(position, 'position:' + symbol) + + def handle_position(self, client: Client, message): + # + # Position Changes Caused Operations + # { + # "type": "message", + # "userId": "5c32d69203aa676ce4b543c7", # Deprecated, will detele later + # "channelType": "private", + # "topic": "/contract/position:XBTUSDM", + # "subject": "position.change", + # "data": { + # "realisedGrossPnl": 0E-8, #Accumulated realised profit and loss + # "symbol": "XBTUSDM", #Symbol + # "crossMode": False, #Cross mode or not + # "liquidationPrice": 1000000.0, #Liquidation price + # "posLoss": 0E-8, #Manually added margin amount + # "avgEntryPrice": 7508.22, #Average entry price + # "unrealisedPnl": -0.00014735, #Unrealised profit and loss + # "markPrice": 7947.83, #Mark price + # "posMargin": 0.00266779, #Position margin + # "autoDeposit": False, #Auto deposit margin or not + # "riskLimit": 100000, #Risk limit + # "unrealisedCost": 0.00266375, #Unrealised value + # "posComm": 0.00000392, #Bankruptcy cost + # "posMaint": 0.00001724, #Maintenance margin + # "posCost": 0.00266375, #Position value + # "maintMarginReq": 0.005, #Maintenance margin rate + # "bankruptPrice": 1000000.0, #Bankruptcy price + # "realisedCost": 0.00000271, #Currently accumulated realised position value + # "markValue": 0.00251640, #Mark value + # "posInit": 0.00266375, #Position margin + # "realisedPnl": -0.00000253, #Realised profit and losts + # "maintMargin": 0.00252044, #Position margin + # "realLeverage": 1.06, #Leverage of the order + # "changeReason": "positionChange", #changeReason:marginChange、positionChange、liquidation、autoAppendMarginStatusChange、adl + # "currentCost": 0.00266375, #Current position value + # "openingTimestamp": 1558433191000, #Open time + # "currentQty": -20, #Current position + # "delevPercentage": 0.52, #ADL ranking percentile + # "currentComm": 0.00000271, #Current commission + # "realisedGrossCost": 0E-8, #Accumulated reliased gross profit value + # "isOpen": True, #Opened position or not + # "posCross": 1.2E-7, #Manually added margin + # "currentTimestamp": 1558506060394, #Current timestamp + # "unrealisedRoePcnt": -0.0553, #Rate of return on investment + # "unrealisedPnlPcnt": -0.0553, #Position profit and loss ratio + # "settleCurrency": "XBT" #Currency used to clear and settle the trades + # } + # } + # Position Changes Caused by Mark Price + # { + # "userId": "5cd3f1a7b7ebc19ae9558591", # Deprecated, will detele later + # "topic": "/contract/position:XBTUSDM", + # "subject": "position.change", + # "data": { + # "markPrice": 7947.83, #Mark price + # "markValue": 0.00251640, #Mark value + # "maintMargin": 0.00252044, #Position margin + # "realLeverage": 10.06, #Leverage of the order + # "unrealisedPnl": -0.00014735, #Unrealised profit and lost + # "unrealisedRoePcnt": -0.0553, #Rate of return on investment + # "unrealisedPnlPcnt": -0.0553, #Position profit and loss ratio + # "delevPercentage": 0.52, #ADL ranking percentile + # "currentTimestamp": 1558087175068, #Current timestamp + # "settleCurrency": "XBT" #Currency used to clear and settle the trades + # } + # } + # Funding Settlement + # { + # "userId": "xbc453tg732eba53a88ggyt8c", # Deprecated, will detele later + # "topic": "/contract/position:XBTUSDM", + # "subject": "position.settlement", + # "data": { + # "fundingTime": 1551770400000, #Funding time + # "qty": 100, #Position siz + # "markPrice": 3610.85, #Settlement price + # "fundingRate": -0.002966, #Funding rate + # "fundingFee": -296, #Funding fees + # "ts": 1547697294838004923, #Current time(nanosecond) + # "settleCurrency": "XBT" #Currency used to clear and settle the trades + # } + # } + # Adjustmet result of risk limit level + # { + # "userId": "xbc453tg732eba53a88ggyt8c", + # "topic": "/contract/position:ADAUSDTM", + # "subject": "position.adjustRiskLimit", + # "data": { + # "success": True, # Successful or not + # "riskLimitLevel": 1, # Current risk limit level + # "msg": "" # Failure reason + # } + # } + # + topic = self.safe_string(message, 'topic', '') + parts = topic.split(':') + marketId = self.safe_string(parts, 1) + symbol = self.safe_symbol(marketId, None, '') + cache = self.positions + currentPosition = self.get_current_position(symbol) + messageHash = 'position:' + symbol + data = self.safe_value(message, 'data', {}) + newPosition = self.parse_position(data) + keys = list(newPosition.keys()) + for i in range(0, len(keys)): + key = keys[i] + if newPosition[key] is None: + del newPosition[key] + position = self.extend(currentPosition, newPosition) + cache.append(position) + client.resolve(position, messageHash) + + async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://docs.kucoin.com/futures/#execution-data + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + return await self.watch_trades_for_symbols([symbol], since, limit, params) + + async def watch_trades_for_symbols(self, symbols: List[str], since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + :param str[] symbols: + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + symbolsLength = len(symbols) + if symbolsLength == 0: + raise ArgumentsRequired(self.id + ' watchTradesForSymbols() requires a non-empty array of symbols') + await self.load_markets() + symbols = self.market_symbols(symbols) + url = await self.negotiate(False) + symbols = self.market_symbols(symbols) + marketIds = self.market_ids(symbols) + topic = '/contractMarket/execution:' + ','.join(marketIds) + subscriptionHashes = [] + messageHashes = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + marketId = marketIds[i] + messageHashes.append('trades:' + symbol) + subscriptionHashes.append('/contractMarket/execution:' + marketId) + trades = await self.subscribe_multiple(url, messageHashes, topic, subscriptionHashes, None, params) + if self.newUpdates: + first = self.safe_value(trades, 0) + tradeSymbol = self.safe_string(first, 'symbol') + limit = trades.getLimit(tradeSymbol, limit) + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + async def un_watch_trades(self, symbol: str, params={}) -> Any: + """ + unWatches trades stream + + https://docs.kucoin.com/futures/#execution-data + + :param str symbol: unified symbol of the market to fetch trades for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + return await self.un_watch_trades_for_symbols([symbol], params) + + async def un_watch_trades_for_symbols(self, symbols: List[str], params={}) -> Any: + """ + get the list of most recent trades for a particular symbol + :param str[] symbols: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, False) + url = await self.negotiate(False) + symbols = self.market_symbols(symbols) + marketIds = self.market_ids(symbols) + topic = '/contractMarket/execution:' + ','.join(marketIds) + subscriptionHashes = [] + messageHashes = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + messageHashes.append('unsubscribe:trades:' + symbol) + subscriptionHashes.append('trades:' + symbol) + subscription = { + 'messageHashes': messageHashes, + 'subMessageHashes': subscriptionHashes, + 'topic': 'trades', + 'unsubscribe': True, + 'symbols': symbols, + } + return await self.un_subscribe_multiple(url, messageHashes, topic, messageHashes, params, subscription) + + def handle_trade(self, client: Client, message): + # + # { + # "type": "message", + # "topic": "/contractMarket/execution:ADAUSDTM", + # "subject": "match", + # "data": { + # "makerUserId": "62286a4d720edf0001e81961", + # "symbol": "ADAUSDTM", + # "sequence": 41320766, + # "side": "sell", + # "size": 2, + # "price": 0.35904, + # "takerOrderId": "636dd9da9857ba00010cfa44", + # "makerOrderId": "636dd9c8df149d0001e62bc8", + # "takerUserId": "6180be22b6ab210001fa3371", + # "tradeId": "636dd9da0000d400d477eca7", + # "ts": 1668143578987357700 + # } + # } + # + data = self.safe_value(message, 'data', {}) + trade = self.parse_trade(data) + symbol = trade['symbol'] + trades = self.safe_value(self.trades, symbol) + if trades is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + trades = ArrayCache(limit) + self.trades[symbol] = trades + trades.append(trade) + messageHash = 'trades:' + symbol + client.resolve(trades, messageHash) + return message + + async def watch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + + https://www.kucoin.com/docs/websocket/futures-trading/public-channels/klines + + watches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + symbol = self.symbol(symbol) + url = await self.negotiate(False) + marketId = self.market_id(symbol) + timeframes = self.safe_dict(self.options, 'timeframes') + timeframeId = self.safe_string(timeframes, timeframe, timeframe) + topic = '/contractMarket/limitCandle:' + marketId + '_' + timeframeId + messageHash = 'ohlcv::' + symbol + '_' + timeframe + ohlcv = await self.subscribe(url, messageHash, topic, None, params) + if self.newUpdates: + limit = ohlcv.getLimit(symbol, limit) + return self.filter_by_since_limit(ohlcv, since, limit, 0, True) + + def handle_ohlcv(self, client: Client, message): + # + # { + # "topic":"/contractMarket/limitCandle:LTCUSDTM_1min", + # "type":"message", + # "data":{ + # "symbol":"LTCUSDTM", + # "candles":[ + # "1715470980", + # "81.38", + # "81.38", + # "81.38", + # "81.38", + # "61.0", + # "61" + # ], + # "time":1715470994801 + # }, + # "subject":"candle.stick" + # } + # + topic = self.safe_string(message, 'topic') + parts = topic.split('_') + timeframeId = self.safe_string(parts, 1) + data = self.safe_dict(message, 'data') + timeframes = self.safe_dict(self.options, 'timeframes') + timeframe = self.find_timeframe(timeframeId, timeframes) + marketId = self.safe_string(data, 'symbol') + symbol = self.safe_symbol(marketId) + messageHash = 'ohlcv::' + symbol + '_' + timeframe + ohlcv = self.safe_list(data, 'candles') + parsed = [ + self.safe_integer(ohlcv, 0), + self.safe_number(ohlcv, 1), + self.safe_number(ohlcv, 2), + self.safe_number(ohlcv, 3), + self.safe_number(ohlcv, 4), + self.safe_number(ohlcv, 6), # Note value 5 is incorrect and will be fixed in subsequent versions of kucoin + ] + self.ohlcvs[symbol] = self.safe_dict(self.ohlcvs, symbol, {}) + if not (timeframe in self.ohlcvs[symbol]): + limit = self.safe_integer(self.options, 'OHLCVLimit', 1000) + self.ohlcvs[symbol][timeframe] = ArrayCacheByTimestamp(limit) + stored = self.ohlcvs[symbol][timeframe] + stored.append(parsed) + client.resolve(stored, messageHash) + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + 1. After receiving the websocket Level 2 data flow, cache the data. + 2. Initiate a REST request to get the snapshot data of Level 2 order book. + 3. Playback the cached Level 2 data flow. + 4. Apply the new Level 2 data flow to the local snapshot to ensure that the sequence of the new Level 2 update lines up with the sequence of the previous Level 2 data. Discard all the message prior to that sequence, and then playback the change to snapshot. + 5. Update the level2 full data based on sequence according to the size. If the price is 0, ignore the messages and update the sequence. If the size=0, update the sequence and remove the price of which the size is 0 out of level 2. For other cases, please update the price. + 6. If the sequence of the newly pushed message does not line up to the sequence of the last message, you could pull through REST Level 2 message request to get the updated messages. Please note that the difference between the start and end parameters cannot exceed 500. + + https://docs.kucoin.com/futures/#level-2-market-data + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + return await self.watch_order_book_for_symbols([symbol], limit, params) + + async def watch_order_book_for_symbols(self, symbols: List[str], limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://docs.kucoin.com/futures/#level-2-market-data + + :param str[] symbols: unified array of symbols + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + symbolsLength = len(symbols) + if symbolsLength == 0: + raise ArgumentsRequired(self.id + ' watchOrderBookForSymbols() requires a non-empty array of symbols') + if limit is not None: + if (limit != 20) and (limit != 100): + raise ExchangeError(self.id + " watchOrderBook 'limit' argument must be None, 20 or 100") + await self.load_markets() + symbols = self.market_symbols(symbols) + marketIds = self.market_ids(symbols) + url = await self.negotiate(False) + topic = '/contractMarket/level2:' + ','.join(marketIds) + subscriptionArgs: dict = { + 'limit': limit, + } + subscriptionHashes = [] + messageHashes = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + marketId = marketIds[i] + messageHashes.append('orderbook:' + symbol) + subscriptionHashes.append('/contractMarket/level2:' + marketId) + orderbook = await self.subscribe_multiple(url, messageHashes, topic, subscriptionHashes, subscriptionArgs, params) + return orderbook.limit() + + async def un_watch_order_book(self, symbol: str, params={}) -> Any: + """ + unWatches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://docs.kucoin.com/futures/#level-2-market-data + + :param str symbol: unified symbol of the market to fetch the order book for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + return await self.un_watch_order_book_for_symbols([symbol], params) + + async def un_watch_order_book_for_symbols(self, symbols: List[str], params={}) -> Any: + """ + unWatches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str[] symbols: unified array of symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + marketIds = self.market_ids(symbols) + url = await self.negotiate(False) + topic = '/contractMarket/level2:' + ','.join(marketIds) + subscriptionHashes = [] + messageHashes = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + messageHashes.append('unsubscribe:orderbook:' + symbol) + subscriptionHashes.append('orderbook:' + symbol) + subscription = { + 'messageHashes': messageHashes, + 'symbols': symbols, + 'unsubscribe': True, + 'topic': 'orderbook', + 'subMessageHashes': subscriptionHashes, + } + return await self.un_subscribe_multiple(url, messageHashes, topic, messageHashes, params, subscription) + + def handle_delta(self, orderbook, delta): + orderbook['nonce'] = self.safe_integer(delta, 'sequence') + timestamp = self.safe_integer(delta, 'timestamp') + orderbook['timestamp'] = timestamp + orderbook['datetime'] = self.iso8601(timestamp) + change = self.safe_value(delta, 'change', {}) + splitChange = change.split(',') + price = self.safe_number(splitChange, 0) + side = self.safe_string(splitChange, 1) + quantity = self.safe_number(splitChange, 2) + type = 'bids' if (side == 'buy') else 'asks' + value = [price, quantity] + if type == 'bids': + storedBids = orderbook['bids'] + storedBids.storeArray(value) + else: + storedAsks = orderbook['asks'] + storedAsks.storeArray(value) + + def handle_deltas(self, bookside, deltas): + for i in range(0, len(deltas)): + self.handle_delta(bookside, deltas[i]) + + def handle_order_book(self, client: Client, message): + # + # initial snapshot is fetched with ccxt's fetchOrderBook + # the feed does not include a snapshot, just the deltas + # + # { + # "type": "message", + # "topic": "/contractMarket/level2:ADAUSDTM", + # "subject": "level2", + # "data": { + # "sequence": 1668059586457, + # "change": "0.34172,sell,456", # type, side, quantity + # "timestamp": 1668573023223 + # } + # } + # + data = self.safe_value(message, 'data') + topic = self.safe_string(message, 'topic') + topicParts = topic.split(':') + marketId = self.safe_string(topicParts, 1) + symbol = self.safe_symbol(marketId, None, '-') + messageHash = 'orderbook:' + symbol + if not (symbol in self.orderbooks): + subscriptionArgs = self.safe_dict(client.subscriptions, topic, {}) + limit = self.safe_integer(subscriptionArgs, 'limit') + self.orderbooks[symbol] = self.order_book({}, limit) + storedOrderBook = self.orderbooks[symbol] + nonce = self.safe_integer(storedOrderBook, 'nonce') + deltaEnd = self.safe_integer(data, 'sequence') + if nonce is None: + cacheLength = len(storedOrderBook.cache) + topicPartsNew = topic.split(':') + topicSymbol = self.safe_string(topicPartsNew, 1) + topicChannel = self.safe_string(topicPartsNew, 0) + subscriptions = list(client.subscriptions.keys()) + subscription = None + for i in range(0, len(subscriptions)): + key = subscriptions[i] + if (key.find(topicSymbol) >= 0) and (key.find(topicChannel) >= 0): + subscription = client.subscriptions[key] + break + limit = self.safe_integer(subscription, 'limit') + snapshotDelay = self.handle_option('watchOrderBook', 'snapshotDelay', 5) + if cacheLength == snapshotDelay: + self.spawn(self.load_order_book, client, messageHash, symbol, limit, {}) + storedOrderBook.cache.append(data) + return + elif nonce >= deltaEnd: + return + self.handle_delta(storedOrderBook, data) + client.resolve(storedOrderBook, messageHash) + + def get_cache_index(self, orderbook, cache): + firstDelta = self.safe_value(cache, 0) + nonce = self.safe_integer(orderbook, 'nonce') + firstDeltaStart = self.safe_integer(firstDelta, 'sequence') + if nonce < firstDeltaStart - 1: + return -1 + for i in range(0, len(cache)): + delta = cache[i] + deltaStart = self.safe_integer(delta, 'sequence') + if nonce < deltaStart - 1: + return i + return len(cache) + + def handle_system_status(self, client: Client, message): + # + # todo: answer the question whether handleSystemStatus should be renamed + # and unified for any usage pattern that + # involves system status and maintenance updates + # + # { + # "id": "1578090234088", # connectId + # "type": "welcome", + # } + # + return message + + async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + watches information on multiple orders made by the user + + https://docs.kucoin.com/futures/#trade-orders-according-to-the-market + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + url = await self.negotiate(True) + topic = '/contractMarket/tradeOrders' + request: dict = { + 'privateChannel': True, + } + messageHash = 'orders' + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + messageHash = messageHash + ':' + symbol + orders = await self.subscribe(url, messageHash, topic, None, self.extend(request, params)) + if self.newUpdates: + limit = orders.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(orders, symbol, since, limit, True) + + def parse_ws_order_status(self, status): + statuses: dict = { + 'open': 'open', + 'filled': 'closed', + 'match': 'open', + 'update': 'open', + 'canceled': 'canceled', + } + return self.safe_string(statuses, status, status) + + def parse_ws_order(self, order, market=None): + # + # "symbol": "XCAD-USDT", + # { + # "orderType": "limit", + # "side": "buy", + # "orderId": "6249167327218b000135e749", + # "type": "canceled", + # "orderTime": 1648957043065280224, + # "size": "100.452", + # "filledSize": "0", + # "price": "2.9635", + # "clientOid": "buy-XCAD-USDT-1648957043010159", + # "remainSize": "0", + # "status": "done", + # "ts": 1648957054031001037 + # } + # + id = self.safe_string(order, 'orderId') + clientOrderId = self.safe_string(order, 'clientOid') + orderType = self.safe_string_lower(order, 'orderType') + price = self.safe_string(order, 'price') + filled = self.safe_string(order, 'filledSize') + amount = self.safe_string(order, 'size') + rawType = self.safe_string(order, 'type') + status = self.parse_ws_order_status(rawType) + timestamp = self.safe_integer_product(order, 'orderTime', 0.000001) + marketId = self.safe_string(order, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + side = self.safe_string_lower(order, 'side') + return self.safe_order({ + 'info': order, + 'symbol': symbol, + 'id': id, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'type': orderType, + 'timeInForce': None, + 'postOnly': None, + 'side': side, + 'price': price, + 'stopPrice': None, + 'amount': amount, + 'cost': None, + 'average': None, + 'filled': filled, + 'remaining': None, + 'status': status, + 'fee': None, + 'trades': None, + }, market) + + def handle_order(self, client: Client, message): + messageHash = 'orders' + data = self.safe_value(message, 'data') + parsed = self.parse_ws_order(data) + symbol = self.safe_string(parsed, 'symbol') + orderId = self.safe_string(parsed, 'id') + if symbol is not None: + if self.orders is None: + limit = self.safe_integer(self.options, 'ordersLimit', 1000) + self.orders = ArrayCacheBySymbolById(limit) + cachedOrders = self.orders + orders = self.safe_value(cachedOrders.hashmap, symbol, {}) + order = self.safe_value(orders, orderId) + if order is not None: + # todo add others to calculate average etc + stopPrice = self.safe_value(order, 'stopPrice') + if stopPrice is not None: + parsed['stopPrice'] = stopPrice + if order['status'] == 'closed': + parsed['status'] = 'closed' + cachedOrders.append(parsed) + client.resolve(self.orders, messageHash) + symbolSpecificMessageHash = messageHash + ':' + symbol + client.resolve(self.orders, symbolSpecificMessageHash) + + async def watch_balance(self, params={}) -> Balances: + """ + watch balance and get the amount of funds available for trading or funds locked in orders + + https://docs.kucoin.com/futures/#account-balance-events + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + url = await self.negotiate(True) + topic = '/contractAccount/wallet' + request: dict = { + 'privateChannel': True, + } + subscription: dict = { + 'method': self.handle_balance_subscription, + } + messageHash = 'balance' + return await self.subscribe(url, messageHash, topic, subscription, self.extend(request, params)) + + def handle_balance(self, client: Client, message): + # + # { + # "id": "6375553193027a0001f6566f", + # "type": "message", + # "topic": "/contractAccount/wallet", + # "userId": "613a896885d8660006151f01", + # "channelType": "private", + # "subject": "availableBalance.change", + # "data": { + # "currency": "USDT", + # "holdBalance": "0.0000000000", + # "availableBalance": "14.0350281903", + # "timestamp": "1668633905657" + # } + # } + # + data = self.safe_value(message, 'data', {}) + self.balance['info'] = data + currencyId = self.safe_string(data, 'currency') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(data, 'availableBalance') + account['used'] = self.safe_string(data, 'holdBalance') + self.balance[code] = account + self.balance = self.safe_balance(self.balance) + client.resolve(self.balance, 'balance') + + def handle_balance_subscription(self, client: Client, message, subscription): + self.spawn(self.fetch_balance_snapshot, client, message) + + async def fetch_balance_snapshot(self, client, message): + await self.load_markets() + self.check_required_credentials() + messageHash = 'balance' + selectedType = self.safe_string_2(self.options, 'watchBalance', 'defaultType', 'swap') # spot, margin, main, funding, future, mining, trade, contract, pool + params: dict = { + 'type': selectedType, + } + snapshot = await self.fetch_balance(params) + # + # { + # "info": { + # "code": "200000", + # "data": { + # "accountEquity": 0.0350281903, + # "unrealisedPNL": 0, + # "marginBalance": 0.0350281903, + # "positionMargin": 0, + # "orderMargin": 0, + # "frozenFunds": 0, + # "availableBalance": 0.0350281903, + # "currency": "USDT" + # } + # }, + # "timestamp": None, + # "datetime": None, + # "USDT": { + # "free": 0.0350281903, + # "used": 0, + # "total": 0.0350281903 + # }, + # "free": { + # "USDT": 0.0350281903 + # }, + # "used": { + # "USDT": 0 + # }, + # "total": { + # "USDT": 0.0350281903 + # } + # } + # + keys = list(snapshot.keys()) + for i in range(0, len(keys)): + code = keys[i] + if code != 'free' and code != 'used' and code != 'total' and code != 'timestamp' and code != 'datetime' and code != 'info': + self.balance[code] = snapshot[code] + self.balance['info'] = self.safe_value(snapshot, 'info', {}) + client.resolve(self.balance, messageHash) + + def handle_subject(self, client: Client, message): + # + # { + # "type": "message", + # "topic": "/contractMarket/level2:ADAUSDTM", + # "subject": "level2", + # "data": { + # "sequence": 1668059586457, + # "change": "0.34172,sell,456", # type, side, quantity + # "timestamp": 1668573023223 + # } + # } + # + subject = self.safe_string(message, 'subject') + methods: dict = { + 'level2': self.handle_order_book, + 'ticker': self.handle_ticker, + 'candle.stick': self.handle_ohlcv, + 'tickerV2': self.handle_bid_ask, + 'availableBalance.change': self.handle_balance, + 'match': self.handle_trade, + 'orderChange': self.handle_order, + 'orderUpdated': self.handle_order, + 'position.change': self.handle_position, + 'position.settlement': self.handle_position, + 'position.adjustRiskLimit': self.handle_position, + } + method = self.safe_value(methods, subject) + if method is not None: + method(client, message) + + def get_message_hash(self, elementName: str, symbol: Str = None): + # elementName can be 'ticker', 'bidask', ... + if symbol is not None: + return elementName + '@' + symbol + else: + return elementName + 's@all' + + def ping(self, client: Client): + # kucoin does not support built-in ws protocol-level ping-pong + # instead it requires a custom json-based text ping-pong + # https://docs.kucoin.com/#ping + id = str(self.request_id()) + return { + 'id': id, + 'type': 'ping', + } + + def handle_pong(self, client: Client, message): + # https://docs.kucoin.com/#ping + client.lastPong = self.milliseconds() + return message + + def handle_error_message(self, client: Client, message) -> Bool: + # + # { + # "id": "64d8732c856851144bded10d", + # "type": "error", + # "code": 401, + # "data": "token is expired" + # } + # + data = self.safe_string(message, 'data', '') + if data == 'token is expired': + type = 'public' + if client.url.find('connectId=private') >= 0: + type = 'private' + self.options['urls'][type] = None + self.handle_errors(1, '', client.url, '', {}, data, message, {}, {}) + return True + + def handle_subscription_status(self, client: Client, message): + # + # { + # "id": "1578090438322", + # "type": "ack" + # } + # + id = self.safe_string(message, 'id') + if not (id in client.subscriptions): + return + subscriptionHash = self.safe_string(client.subscriptions, id) + subscription = self.safe_value(client.subscriptions, subscriptionHash) + del client.subscriptions[id] + method = self.safe_value(subscription, 'method') + if method is not None: + method(client, message, subscription) + isUnSub = self.safe_bool(subscription, 'unsubscribe', False) + if isUnSub: + messageHashes = self.safe_list(subscription, 'messageHashes', []) + subMessageHashes = self.safe_list(subscription, 'subMessageHashes', []) + for i in range(0, len(messageHashes)): + messageHash = messageHashes[i] + subHash = subMessageHashes[i] + self.clean_unsubscription(client, subHash, messageHash) + self.clean_cache(subscription) + + def handle_message(self, client: Client, message): + type = self.safe_string(message, 'type') + methods: dict = { + # 'heartbeat': self.handleHeartbeat, + 'welcome': self.handle_system_status, + 'message': self.handle_subject, + 'pong': self.handle_pong, + 'error': self.handle_error_message, + 'ack': self.handle_subscription_status, + } + method = self.safe_value(methods, type) + if method is not None: + method(client, message) diff --git a/ccxt/pro/lbank.py b/ccxt/pro/lbank.py new file mode 100644 index 0000000..46ef568 --- /dev/null +++ b/ccxt/pro/lbank.py @@ -0,0 +1,924 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolById, ArrayCacheByTimestamp +import math +from ccxt.base.types import Any, Balances, Int, Order, OrderBook, Str, Ticker, Trade +from ccxt.async_support.base.ws.client import Client +from typing import List +from ccxt.base.errors import ExchangeError + + +class lbank(ccxt.async_support.lbank): + + def describe(self) -> Any: + return self.deep_extend(super(lbank, self).describe(), { + 'has': { + 'ws': True, + 'fetchOHLCVWs': True, + 'fetchOrderBookWs': True, + 'fetchTickerWs': True, + 'fetchTradesWs': True, + 'watchBalance': True, + 'watchTicker': True, + 'watchTickers': False, + 'watchTrades': True, + 'watchTradesForSymbols': False, + 'watchMyTrades': False, + 'watchOrders': True, + 'watchOrderBook': True, + 'watchOHLCV': True, + }, + 'urls': { + 'api': { + 'ws': 'wss://www.lbkex.net/ws/V2/', + }, + }, + 'options': { + 'watchOHLCV': { + 'timeframes': { + '1m': '1min', + '5m': '5min', + '15m': '15min', + '30m': '30min', + '1h': '1hr', + '4h': '4hr', + '1d': 'day', + '1w': 'week', + '1M': 'month', + '1y': 'year', + }, + }, + }, + 'streaming': { + }, + 'exceptions': { + }, + }) + + def request_id(self): + previousValue = self.safe_integer(self.options, 'requestId', 0) + newValue = self.sum(previousValue, 1) + self.options['requestId'] = newValue + return newValue + + async def fetch_ohlcv_ws(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + + https://www.lbank.com/en-US/docs/index.html#request-amp-subscription-instruction + + watches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + url = self.urls['api']['ws'] + watchOHLCVOptions = self.safe_value(self.options, 'watchOHLCV', {}) + timeframes = self.safe_value(watchOHLCVOptions, 'timeframes', {}) + timeframeId = self.safe_string(timeframes, timeframe, timeframe) + messageHash = 'fetchOHLCV:' + market['symbol'] + ':' + timeframeId + message: dict = { + 'action': 'request', + 'request': 'kbar', + 'kbar': timeframeId, + 'pair': market['id'], + } + if since is not None: + message['start'] = self.parse_to_int(int(math.floor(since / 1000))) + if limit is not None: + message['size'] = limit + request = self.deep_extend(message, params) + requestId = self.request_id() + return await self.watch(url, messageHash, request, requestId, request) + + async def watch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + + https://www.lbank.com/en-US/docs/index.html#subscription-of-k-line-data + + watches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + watchOHLCVOptions = self.safe_value(self.options, 'watchOHLCV', {}) + timeframes = self.safe_value(watchOHLCVOptions, 'timeframes', {}) + timeframeId = self.safe_string(timeframes, timeframe, timeframe) + messageHash = 'ohlcv:' + market['symbol'] + ':' + timeframeId + url = self.urls['api']['ws'] + subscribe: dict = { + 'action': 'subscribe', + 'subscribe': 'kbar', + 'kbar': timeframeId, + 'pair': market['id'], + } + request = self.deep_extend(subscribe, params) + ohlcv = await self.watch(url, messageHash, request, messageHash) + if self.newUpdates: + limit = ohlcv.getLimit(symbol, limit) + return self.filter_by_since_limit(ohlcv, since, limit, 0, True) + + def handle_ohlcv(self, client, message): + # + # request + # { + # "records":[ + # [ + # 1705364400, + # 42614, + # 42624.57, + # 42532.15, + # 42537.43, + # 13.2615, + # 564568.931565, + # 433 + # ] + # ], + # "columns":[ + # "timestamp", + # "open", + # "high", + # "low", + # "close", + # "volume", + # "turnover", + # "count" + # ], + # "SERVER":"V2", + # "count":1, + # "kbar":"5min", + # "type":"kbar", + # "pair":"btc_usdt", + # "TS":"2024-01-16T08:29:41.718" + # } + # subscribe + # { + # SERVER: 'V2', + # kbar: { + # a: 26415.891476, + # c: 19315.51, + # t: '2022-10-02T12:44:00.000', + # v: 1.3676, + # h: 19316.66, + # slot: '1min', + # l: 19315.51, + # n: 1, + # o: 19316.66 + # }, + # type: 'kbar', + # pair: 'btc_usdt', + # TS: '2022-10-02T12:44:15.865' + # } + # + marketId = self.safe_string(message, 'pair') + symbol = self.safe_symbol(marketId, None, '_') + watchOHLCVOptions = self.safe_value(self.options, 'watchOHLCV', {}) + timeframes = self.safe_value(watchOHLCVOptions, 'timeframes', {}) + records = self.safe_value(message, 'records') + if records is not None: # from request + rawOHLCV = self.safe_value(records, 0, []) + parsed = [ + self.safe_integer(rawOHLCV, 0), + self.safe_number(rawOHLCV, 1), + self.safe_number(rawOHLCV, 2), + self.safe_number(rawOHLCV, 3), + self.safe_number(rawOHLCV, 4), + self.safe_number(rawOHLCV, 5), + ] + timeframeId = self.safe_string(message, 'kbar') + timeframe = self.find_timeframe(timeframeId, timeframes) + self.ohlcvs[symbol] = self.safe_value(self.ohlcvs, symbol, {}) + stored = self.safe_value(self.ohlcvs[symbol], timeframe) + if stored is None: + limit = self.safe_integer(self.options, 'OHLCVLimit', 1000) + stored = ArrayCacheByTimestamp(limit) + self.ohlcvs[symbol][timeframe] = stored + stored.append(parsed) + messageHash = 'fetchOHLCV:' + symbol + ':' + timeframeId + client.resolve(stored, messageHash) + else: # from subscription + rawOHLCV = self.safe_value(message, 'kbar', {}) + timeframeId = self.safe_string(rawOHLCV, 'slot') + datetime = self.safe_string(rawOHLCV, 't') + parsed = [ + self.parse8601(datetime), + self.safe_number(rawOHLCV, 'o'), + self.safe_number(rawOHLCV, 'h'), + self.safe_number(rawOHLCV, 'l'), + self.safe_number(rawOHLCV, 'c'), + self.safe_number(rawOHLCV, 'v'), + ] + timeframe = self.find_timeframe(timeframeId, timeframes) + self.ohlcvs[symbol] = self.safe_value(self.ohlcvs, symbol, {}) + stored = self.safe_value(self.ohlcvs[symbol], timeframe) + if stored is None: + limit = self.safe_integer(self.options, 'OHLCVLimit', 1000) + stored = ArrayCacheByTimestamp(limit) + self.ohlcvs[symbol][timeframe] = stored + stored.append(parsed) + messageHash = 'ohlcv:' + symbol + ':' + timeframeId + client.resolve(stored, messageHash) + + async def fetch_ticker_ws(self, symbol: str, params={}) -> Ticker: + """ + + https://www.lbank.com/en-US/docs/index.html#request-amp-subscription-instruction + + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the cex api endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + url = self.urls['api']['ws'] + messageHash = 'fetchTicker:' + market['symbol'] + message: dict = { + 'action': 'request', + 'request': 'tick', + 'pair': market['id'], + } + request = self.deep_extend(message, params) + requestId = self.request_id() + return await self.watch(url, messageHash, request, requestId, request) + + async def watch_ticker(self, symbol: str, params={}) -> Ticker: + """ + + https://www.lbank.com/en-US/docs/index.html#market + + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict params: extra parameters specific to the lbank api endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + url = self.urls['api']['ws'] + messageHash = 'ticker:' + market['symbol'] + message: dict = { + 'action': 'subscribe', + 'subscribe': 'tick', + 'pair': market['id'], + } + request = self.deep_extend(message, params) + return await self.watch(url, messageHash, request, messageHash, request) + + def handle_ticker(self, client, message): + # + # { + # "tick":{ + # "to_cny":76643.5, + # "high":0.02719761, + # "vol":497529.7686, + # "low":0.02603071, + # "change":2.54, + # "usd":299.12, + # "to_usd":11083.66, + # "dir":"sell", + # "turnover":13224.0186, + # "latest":0.02698749, + # "cny":2068.41 + # }, + # "type":"tick", + # "pair":"eth_btc", + # "SERVER":"V2", + # "TS":"2019-07-01T11:33:55.188" + # } + # + marketId = self.safe_string(message, 'pair') + symbol = self.safe_symbol(marketId) + market = self.safe_market(marketId) + parsedTicker = self.parse_ws_ticker(message, market) + self.tickers[symbol] = parsedTicker + messageHash = 'ticker:' + symbol + client.resolve(parsedTicker, messageHash) + messageHash = 'fetchTicker:' + symbol + client.resolve(parsedTicker, messageHash) + + def parse_ws_ticker(self, ticker, market=None): + # + # { + # "tick":{ + # "to_cny":76643.5, + # "high":0.02719761, + # "vol":497529.7686, + # "low":0.02603071, + # "change":2.54, + # "usd":299.12, + # "to_usd":11083.66, + # "dir":"sell", + # "turnover":13224.0186, + # "latest":0.02698749, + # "cny":2068.41 + # }, + # "type":"tick", + # "pair":"eth_btc", + # "SERVER":"V2", + # "TS":"2019-07-01T11:33:55.188" + # } + # + marketId = self.safe_string(ticker, 'pair') + symbol = self.safe_symbol(marketId, market) + datetime = self.safe_string(ticker, 'TS') + tickerData = self.safe_value(ticker, 'tick') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + 'high': self.safe_string(tickerData, 'high'), + 'low': self.safe_string(tickerData, 'low'), + 'bid': None, + 'bidVolume': None, + 'ask': None, + 'askVolume': None, + 'vwap': None, + 'open': None, + 'close': None, + 'last': self.safe_string(tickerData, 'latest'), + 'previousClose': None, + 'change': None, + 'percentage': self.safe_string(tickerData, 'change'), + 'average': None, + 'baseVolume': self.safe_string(tickerData, 'vol'), + 'quoteVolume': self.safe_string(tickerData, 'turnover'), + 'info': ticker, + }, market) + + async def fetch_trades_ws(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://www.lbank.com/en-US/docs/index.html#request-amp-subscription-instruction + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + url = self.urls['api']['ws'] + messageHash = 'fetchTrades:' + market['symbol'] + if limit is None: + limit = 10 + message: dict = { + 'action': 'request', + 'request': 'trade', + 'pair': market['id'], + 'size': limit, + } + request = self.deep_extend(message, params) + requestId = self.request_id() + return await self.watch(url, messageHash, request, requestId, request) + + async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + + https://www.lbank.com/en-US/docs/index.html#trade-record + + get the list of most recent trades for a particular symbol + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + url = self.urls['api']['ws'] + messageHash = 'trades:' + market['symbol'] + message: dict = { + 'action': 'subscribe', + 'subscribe': 'trade', + 'pair': market['id'], + } + request = self.deep_extend(message, params) + trades = await self.watch(url, messageHash, request, messageHash, request) + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + def handle_trades(self, client, message): + # + # request + # { + # columns: ['timestamp', 'price', 'volume', 'direction'], + # SERVER: 'V2', + # count: 100, + # trades: [], + # type: 'trade', + # pair: 'btc_usdt', + # TS: '2024-01-16T08:48:24.470' + # } + # subscribe + # { + # "trade":{ + # "volume":6.3607, + # "amount":77148.9303, + # "price":12129, + # "direction":"sell", # buy, sell, buy_market, sell_market, buy_maker, sell_maker, buy_ioc, sell_ioc, buy_fok, sell_fok + # "TS":"2019-06-28T19:55:49.460" + # }, + # "type":"trade", + # "pair":"btc_usdt", + # "SERVER":"V2", + # "TS":"2019-06-28T19:55:49.466" + # } + # + marketId = self.safe_string(message, 'pair') + symbol = self.safe_symbol(marketId) + market = self.safe_market(marketId) + stored = self.safe_value(self.trades, symbol) + if stored is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + stored = ArrayCache(limit) + self.trades[symbol] = stored + rawTrade = self.safe_value(message, 'trade') + rawTrades = self.safe_value(message, 'trades', [rawTrade]) + for i in range(0, len(rawTrades)): + trade = self.parse_ws_trade(rawTrades[i], market) + trade['symbol'] = symbol + stored.append(trade) + self.trades[symbol] = stored + messageHash = 'trades:' + symbol + client.resolve(self.trades[symbol], messageHash) + messageHash = 'fetchTrades:' + symbol + client.resolve(self.trades[symbol], messageHash) + + def parse_ws_trade(self, trade, market=None): + # + # request + # ['timestamp', 'price', 'volume', 'direction'] + # subscribe + # { + # "volume":6.3607, + # "amount":77148.9303, + # "price":12129, + # "direction":"sell", # buy, sell, buy_market, sell_market, buy_maker, sell_maker, buy_ioc, sell_ioc, buy_fok, sell_fok + # "TS":"2019-06-28T19:55:49.460" + # } + # + timestamp = self.safe_integer(trade, 0) + datetime = (self.iso8601(timestamp)) if (timestamp is not None) else (self.safe_string(trade, 'TS')) + if timestamp is None: + timestamp = self.parse8601(datetime) + rawSide = self.safe_string_2(trade, 'direction', 3) + parts = rawSide.split('_') + firstPart = self.safe_string(parts, 0) + secondPart = self.safe_string(parts, 1) + side = firstPart + # reverse if it was 'maker' + if secondPart is not None and secondPart == 'maker': + side = 'sell' if (side == 'buy') else 'buy' + return self.safe_trade({ + 'timestamp': timestamp, + 'datetime': datetime, + 'symbol': None, + 'id': None, + 'order': None, + 'type': None, + 'takerOrMaker': None, + 'side': side, + 'price': self.safe_string_2(trade, 'price', 1), + 'amount': self.safe_string_2(trade, 'volume', 2), + 'cost': self.safe_string(trade, 'amount'), + 'fee': None, + 'info': trade, + }, market) + + async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + + https://www.lbank.com/en-US/docs/index.html#update-subscribed-orders + + get the list of trades associated with the user + :param str [symbol]: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict params: extra parameters specific to the lbank api endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + key = await self.authenticate(params) + url = self.urls['api']['ws'] + messageHash = None + pair = 'all' + if symbol is None: + messageHash = 'orders:all' + else: + market = self.market(symbol) + symbol = self.symbol(symbol) + messageHash = 'orders:' + market['symbol'] + pair = market['id'] + message: dict = { + 'action': 'subscribe', + 'subscribe': 'orderUpdate', + 'subscribeKey': key, + 'pair': pair, + } + request = self.deep_extend(message, params) + orders = await self.watch(url, messageHash, request, messageHash, request) + return self.filter_by_symbol_since_limit(orders, symbol, since, limit, True) + + def handle_orders(self, client, message): + # + # { + # "orderUpdate":{ + # "amount":"0.003", + # "orderStatus":2, + # "price":"0.02455211", + # "role":"maker", + # "updateTime":1561704577786, + # "uuid":"d0db191d-xxxxx-4418-xxxxx-fbb1xxxx2ea9", + # "txUuid":"da88f354d5xxxxxxa12128aa5bdcb3", + # "volumePrice":"0.00007365633" + # }, + # "pair":"eth_btc", + # "type":"orderUpdate", + # "SERVER":"V2", + # "TS":"2019-06-28T14:49:37.816" + # } + # + marketId = self.safe_string(message, 'pair') + symbol = self.safe_symbol(marketId, None, '_') + myOrders = None + if self.orders is None: + limit = self.safe_integer(self.options, 'ordersLimit', 1000) + myOrders = ArrayCacheBySymbolById(limit) + else: + myOrders = self.orders + order = self.parse_ws_order(message) + myOrders.append(order) + self.orders = myOrders + client.resolve(myOrders, 'orders') + messageHash = 'orders:' + symbol + client.resolve(myOrders, messageHash) + + def parse_ws_order(self, order, market=None): + # + # { + # "orderUpdate":{ + # "amount":"0.003", + # "orderStatus":2, + # "price":"0.02455211", + # "role":"maker", + # "updateTime":1561704577786, + # "uuid":"d0db191d-xxxxx-4418-xxxxx-fbb1xxxx2ea9", + # "txUuid":"da88f354d5xxxxxxa12128aa5bdcb3", + # "volumePrice":"0.00007365633" + # }, + # "pair":"eth_btc", + # "type":"orderUpdate", + # "SERVER":"V2", + # "TS":"2019-06-28T14:49:37.816" + # } + # { + # "SERVER": "V2", + # "orderUpdate": { + # "accAmt": "0", + # "amount": "0", + # "avgPrice": "0", + # "customerID": "", + # "orderAmt": "5", + # "orderPrice": "0.009834", + # "orderStatus": 0, + # "price": "0.009834", + # "remainAmt": "5", + # "role": "taker", + # "symbol": "lbk_usdt", + # "type": "buy_market", + # "updateTime": 1705676718532, + # "uuid": "9b94ab2d-a510-4abe-a784-44a9d9c38ec7", + # "volumePrice": "0" + # }, + # "type": "orderUpdate", + # "pair": "lbk_usdt", + # "TS": "2024-01-19T23:05:18.548" + # } + # + orderUpdate = self.safe_value(order, 'orderUpdate', {}) + rawType = self.safe_string(orderUpdate, 'type', '') + typeParts = rawType.split('_') + side = self.safe_string(typeParts, 0) + exchangeType = self.safe_string(typeParts, 1) + type = None + if rawType != 'buy' and rawType != 'sell': + type = 'market' if (exchangeType == 'market') else 'limit' + marketId = self.safe_string(order, 'pair') + symbol = self.safe_symbol(marketId, market, '_') + timestamp = self.safe_integer(orderUpdate, 'updateTime') + status = self.safe_string(orderUpdate, 'orderStatus') + orderAmount = self.safe_string(orderUpdate, 'orderAmt') + cost = None + if (type == 'market') and (side == 'buy'): + cost = orderAmount + return self.safe_order({ + 'info': order, + 'id': self.safe_string(orderUpdate, 'uuid'), + 'clientOrderId': self.safe_string(orderUpdate, 'customerID'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'lastUpdateTimestamp': self.safe_integer(orderUpdate, 'updateTime'), + 'symbol': symbol, + 'type': type, + 'side': side, + 'price': self.safe_string_2(orderUpdate, 'price', 'orderPrice'), + 'stopPrice': None, + 'average': self.safe_string(orderUpdate, 'avgPrice'), + 'amount': self.safe_string_2(orderUpdate, 'amount', 'orderAmt'), + 'remaining': self.safe_string(orderUpdate, 'remainAmt'), + 'filled': self.safe_string(orderUpdate, 'accAmt'), + 'status': self.parse_ws_order_status(status), + 'fee': None, + 'cost': cost, + 'trades': None, + }, market) + + def parse_ws_order_status(self, status): + statuses: dict = { + '-1': 'canceled', # Withdrawn + '0': 'open', # Unsettled + '1': 'open', # Partial sale + '2': 'closed', # Completed + '4': 'closed', # Withrawing + } + return self.safe_string(statuses, status, status) + + async def watch_balance(self, params={}) -> Balances: + """ + watch balance and get the amount of funds available for trading or funds locked in orders + + https://www.lbank.com/docs/index.html#update-subscribed-asset + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + key = await self.authenticate(params) + url = self.urls['api']['ws'] + messageHash = 'balance' + message: dict = { + 'action': 'subscribe', + 'subscribe': 'assetUpdate', + 'subscribeKey': key, + } + request = self.deep_extend(message, params) + return await self.watch(url, messageHash, request, messageHash, request) + + def handle_balance(self, client: Client, message): + # + # { + # "data": { + # "asset": "114548.31881315", + # "assetCode": "usdt", + # "free": "97430.6739041", + # "freeze": "17117.64490905", + # "time": 1627300043270, + # "type": "ORDER_CREATE" + # }, + # "SERVER": "V2", + # "type": "assetUpdate", + # "TS": "2021-07-26T19:48:03.548" + # } + # + data = self.safe_dict(message, 'data', {}) + timestamp = self.parse8601(self.safe_string(message, 'TS')) + datetime = self.iso8601(timestamp) + self.balance['info'] = data + self.balance['timestamp'] = timestamp + self.balance['datetime'] = datetime + currencyId = self.safe_string(data, 'assetCode') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(data, 'free') + account['used'] = self.safe_string(data, 'freeze') + account['total'] = self.safe_string(data, 'asset') + self.balance[code] = account + self.balance = self.safe_balance(self.balance) + client.resolve(self.balance, 'balance') + + async def fetch_order_book_ws(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + + https://www.lbank.com/en-US/docs/index.html#request-amp-subscription-instruction + + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str symbol: unified symbol of the market to fetch the order book for + :param int|None limit: the maximum amount of order book entries to return + :param dict params: extra parameters specific to the lbank api endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + url = self.urls['api']['ws'] + messageHash = 'fetchOrderbook:' + market['symbol'] + if limit is None: + limit = 100 + subscribe: dict = { + 'action': 'request', + 'request': 'depth', + 'depth': limit, + 'pair': market['id'], + } + request = self.deep_extend(subscribe, params) + orderbook = await self.watch(url, messageHash, request, messageHash) + return orderbook.limit() + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + + https://www.lbank.com/en-US/docs/index.html#market-depth + + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str symbol: unified symbol of the market to fetch the order book for + :param int|None limit: the maximum amount of order book entries to return + :param dict params: extra parameters specific to the lbank api endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + url = self.urls['api']['ws'] + messageHash = 'orderbook:' + market['symbol'] + params = self.omit(params, 'aggregation') + if limit is None: + limit = 100 + subscribe: dict = { + 'action': 'subscribe', + 'subscribe': 'depth', + 'depth': limit, + 'pair': market['id'], + } + request = self.deep_extend(subscribe, params) + orderbook = await self.watch(url, messageHash, request, messageHash) + return orderbook.limit() + + def handle_order_book(self, client, message): + # + # request + # { + # "SERVER":"V2", + # "asks":[ + # [ + # 42585.84, + # 1.4422 + # ], + # ... + # ], + # "bids":[ + # [ + # 42585.83, + # 1.8054 + # ], + # ,,, + # ], + # "count":100, + # "type":"depth", + # "pair":"btc_usdt", + # "TS":"2024-01-16T08:26:00.413" + # } + # subscribe + # { + # "depth": { + # "asks": [ + # [ + # 0.0252, + # 0.5833 + # ], + # [ + # 0.025215, + # 4.377 + # ], + # ... + # ], + # "bids": [ + # [ + # 0.025135, + # 3.962 + # ], + # [ + # 0.025134, + # 3.46 + # ], + # ... + # ] + # }, + # "count": 100, + # "type": "depth", + # "pair": "eth_btc", + # "SERVER": "V2", + # "TS": "2019-06-28T17:49:22.722" + # } + # + marketId = self.safe_string(message, 'pair') + symbol = self.safe_symbol(marketId) + orderBook = self.safe_value(message, 'depth', message) + datetime = self.safe_string(message, 'TS') + timestamp = self.parse8601(datetime) + # orderbook = self.safe_value(self.orderbooks, symbol) + if not (symbol in self.orderbooks): + self.orderbooks[symbol] = self.order_book({}) + orderbook = self.orderbooks[symbol] + snapshot = self.parse_order_book(orderBook, symbol, timestamp, 'bids', 'asks') + orderbook.reset(snapshot) + messageHash = 'orderbook:' + symbol + client.resolve(orderbook, messageHash) + messageHash = 'fetchOrderbook:' + symbol + client.resolve(orderbook, messageHash) + + def handle_error_message(self, client, message): + # + # { + # SERVER: 'V2', + # message: "Missing parameter ['kbar']", + # status: 'error', + # TS: '2024-01-16T08:09:43.314' + # } + # + errMsg = self.safe_string(message, 'message', '') + error = ExchangeError(self.id + ' ' + errMsg) + client.reject(error) + + async def handle_ping(self, client: Client, message): + # + # {ping: 'a13a939c-5f25-4e06-9981-93cb3b890707', action: 'ping'} + # + pingId = self.safe_string(message, 'ping') + try: + await client.send({ + 'action': 'pong', + 'pong': pingId, + }) + except Exception as e: + self.on_error(client, e) + + def handle_message(self, client, message): + status = self.safe_string(message, 'status') + if status == 'error': + self.handle_error_message(client, message) + return + type = self.safe_string_2(message, 'type', 'action') + if type == 'ping': + self.spawn(self.handle_ping, client, message) + return + handlers: dict = { + 'kbar': self.handle_ohlcv, + 'depth': self.handle_order_book, + 'trade': self.handle_trades, + 'tick': self.handle_ticker, + 'orderUpdate': self.handle_orders, + 'assetUpdate': self.handle_balance, + } + handler = self.safe_value(handlers, type) + if handler is not None: + handler(client, message) + + async def authenticate(self, params={}): + # when we implement more private streams, we need to refactor the authentication + # to be concurent-safe and respect the same authentication token + url = self.urls['api']['ws'] + client = self.client(url) + now = self.milliseconds() + messageHash = 'authenticated' + authenticated = self.safe_value(client.subscriptions, messageHash) + if authenticated is None: + self.check_required_credentials() + response = await self.spotPrivatePostSubscribeGetKey(params) + # + # {"result":true,"data":"4e9958623e6006bd7b13ff9f36c03b36132f0f8da37f70b14ff2c4eab1fe0c97","error_code":0,"ts":1705602277198} + # + result = self.safe_value(response, 'result') + if result is not True: + raise ExchangeError(self.id + ' failed to get subscribe key') + client.subscriptions['authenticated'] = { + 'key': self.safe_string(response, 'data'), + 'expires': self.sum(now, 3300000), # SubscribeKey lasts one hour, refresh it every 55 minutes + } + else: + expires = self.safe_integer(authenticated, 'expires', 0) + if expires < now: + request: dict = { + 'subscribeKey': authenticated['key'], + } + response = await self.spotPrivatePostSubscribeRefreshKey(self.extend(request, params)) + # + # {"result": "true"} + # + result = self.safe_string(response, 'result') + if result != 'true': + raise ExchangeError(self.id + ' failed to refresh the SubscribeKey') + client['subscriptions']['authenticated']['expires'] = self.sum(now, 3300000) # SubscribeKey lasts one hour, refresh it 5 minutes before it expires + return client.subscriptions['authenticated']['key'] diff --git a/ccxt/pro/luno.py b/ccxt/pro/luno.py new file mode 100644 index 0000000..668e595 --- /dev/null +++ b/ccxt/pro/luno.py @@ -0,0 +1,307 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCache +from ccxt.base.types import Any, IndexType, Int, OrderBook, Trade +from ccxt.async_support.base.ws.client import Client +from typing import List + + +class luno(ccxt.async_support.luno): + + def describe(self) -> Any: + return self.deep_extend(super(luno, self).describe(), { + 'has': { + 'ws': True, + 'watchTicker': False, + 'watchTickers': False, + 'watchTrades': True, + 'watchTradesForSymbols': False, + 'watchMyTrades': False, + 'watchOrders': None, # is in beta + 'watchOrderBook': True, + 'watchOHLCV': False, + }, + 'urls': { + 'api': { + 'ws': 'wss://ws.luno.com/api/1', + }, + }, + 'options': { + 'sequenceNumbers': {}, + }, + 'streaming': { + }, + 'exceptions': { + }, + }) + + async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://www.luno.com/en/developers/api#tag/Streaming-API + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + self.check_required_credentials() + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + subscriptionHash = '/stream/' + market['id'] + subscription: dict = {'symbol': symbol} + url = self.urls['api']['ws'] + subscriptionHash + messageHash = 'trades:' + symbol + subscribe: dict = { + 'api_key_id': self.apiKey, + 'api_key_secret': self.secret, + } + request = self.deep_extend(subscribe, params) + trades = await self.watch(url, messageHash, request, subscriptionHash, subscription) + if self.newUpdates: + limit = trades.getLimit(symbol, limit) + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + def handle_trades(self, client: Client, message, subscription): + # + # { + # "sequence": "110980825", + # "trade_updates": [], + # "create_update": { + # "order_id": "BXHSYXAUMH8C2RW", + # "type": "ASK", + # "price": "24081.09000000", + # "volume": "0.07780000" + # }, + # "delete_update": null, + # "status_update": null, + # "timestamp": 1660598775360 + # } + # + rawTrades = self.safe_value(message, 'trade_updates', []) + length = len(rawTrades) + if length == 0: + return + symbol = subscription['symbol'] + market = self.market(symbol) + messageHash = 'trades:' + symbol + stored = self.safe_value(self.trades, symbol) + if stored is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + stored = ArrayCache(limit) + self.trades[symbol] = stored + for i in range(0, len(rawTrades)): + rawTrade = rawTrades[i] + trade = self.parse_trade(rawTrade, market) + stored.append(trade) + self.trades[symbol] = stored + client.resolve(self.trades[symbol], messageHash) + + def parse_trade(self, trade, market=None) -> Trade: + # + # watchTrades(public) + # + # { + # "base": "69.00000000", + # "counter": "113.6499000000000000", + # "maker_order_id": "BXEEU4S2BWF5WRB", + # "taker_order_id": "BXKNCSF7JDHXY3H", + # "order_id": "BXEEU4S2BWF5WRB" + # } + # + return self.safe_trade({ + 'info': trade, + 'id': None, + 'timestamp': None, + 'datetime': None, + 'symbol': market['symbol'], + 'order': None, + 'type': None, + 'side': None, + # takerOrMaker has no meaning for public trades + 'takerOrMaker': None, + 'price': None, + 'amount': self.safe_string(trade, 'base'), + 'cost': self.safe_string(trade, 'counter'), + 'fee': None, + }, market) + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dictConstructor [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: accepts l2 or l3 for level 2 or level 3 order book + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.check_required_credentials() + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + subscriptionHash = '/stream/' + market['id'] + subscription: dict = {'symbol': symbol} + url = self.urls['api']['ws'] + subscriptionHash + messageHash = 'orderbook:' + symbol + subscribe: dict = { + 'api_key_id': self.apiKey, + 'api_key_secret': self.secret, + } + request = self.deep_extend(subscribe, params) + orderbook = await self.watch(url, messageHash, request, subscriptionHash, subscription) + return orderbook.limit() + + def handle_order_book(self, client: Client, message, subscription): + # + # { + # "sequence": "24352", + # "asks": [{ + # "id": "BXMC2CJ7HNB88U4", + # "price": "1234.00", + # "volume": "0.93" + # }], + # "bids": [{ + # "id": "BXMC2CJ7HNB88U5", + # "price": "1201.00", + # "volume": "1.22" + # }], + # "status": "ACTIVE", + # "timestamp": 1528884331021 + # } + # + # update + # { + # "sequence": "110980825", + # "trade_updates": [], + # "create_update": { + # "order_id": "BXHSYXAUMH8C2RW", + # "type": "ASK", + # "price": "24081.09000000", + # "volume": "0.07780000" + # }, + # "delete_update": null, + # "status_update": null, + # "timestamp": 1660598775360 + # } + # + symbol = subscription['symbol'] + messageHash = 'orderbook:' + symbol + timestamp = self.safe_integer(message, 'timestamp') + if not (symbol in self.orderbooks): + self.orderbooks[symbol] = self.indexed_order_book({}) + asks = self.safe_value(message, 'asks') + if asks is not None: + snapshot = self.custom_parse_order_book(message, symbol, timestamp, 'bids', 'asks', 'price', 'volume', 'id') + self.orderbooks[symbol] = self.indexed_order_book(snapshot) + else: + ob = self.orderbooks[symbol] + self.handle_delta(ob, message) + ob['timestamp'] = timestamp + ob['datetime'] = self.iso8601(timestamp) + orderbook = self.orderbooks[symbol] + nonce = self.safe_integer(message, 'sequence') + orderbook['nonce'] = nonce + client.resolve(orderbook, messageHash) + + def custom_parse_order_book(self, orderbook, symbol, timestamp=None, bidsKey='bids', asksKey: IndexType = 'asks', priceKey: IndexType = 'price', amountKey: IndexType = 'volume', countOrIdKey: IndexType = 2): + bids = self.parse_bids_asks(self.safe_value(orderbook, bidsKey, []), priceKey, amountKey, countOrIdKey) + asks = self.parse_bids_asks(self.safe_value(orderbook, asksKey, []), priceKey, amountKey, countOrIdKey) + return { + 'symbol': symbol, + 'bids': self.sort_by(bids, 0, True), + 'asks': self.sort_by(asks, 0), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'nonce': None, + } + + def parse_bids_asks(self, bidasks, priceKey: IndexType = 'price', amountKey: IndexType = 'volume', thirdKey: IndexType = 2): + bidasks = self.to_array(bidasks) + result = [] + for i in range(0, len(bidasks)): + result.append(self.custom_parse_bid_ask(bidasks[i], priceKey, amountKey, thirdKey)) + return result + + def custom_parse_bid_ask(self, bidask, priceKey: IndexType = 'price', amountKey: IndexType = 'volume', thirdKey: IndexType = 2): + price = self.safe_number(bidask, priceKey) + amount = self.safe_number(bidask, amountKey) + result = [price, amount] + if thirdKey is not None: + thirdValue = self.safe_string(bidask, thirdKey) + result.append(thirdValue) + return result + + def handle_delta(self, orderbook, message): + # + # create + # { + # "sequence": "110980825", + # "trade_updates": [], + # "create_update": { + # "order_id": "BXHSYXAUMH8C2RW", + # "type": "ASK", + # "price": "24081.09000000", + # "volume": "0.07780000" + # }, + # "delete_update": null, + # "status_update": null, + # "timestamp": 1660598775360 + # } + # del # { + # "sequence": "110980825", + # "trade_updates": [], + # "create_update": null, + # "delete_update": { + # "order_id": "BXMC2CJ7HNB88U4" + # }, + # "status_update": null, + # "timestamp": 1660598775360 + # } + # trade + # { + # "sequence": "110980825", + # "trade_updates": [ + # { + # "base": "0.1", + # "counter": "5232.00", + # "maker_order_id": "BXMC2CJ7HNB88U4", + # "taker_order_id": "BXMC2CJ7HNB88U5" + # } + # ], + # "create_update": null, + # "delete_update": null, + # "status_update": null, + # "timestamp": 1660598775360 + # } + # + createUpdate = self.safe_value(message, 'create_update') + asksOrderSide = orderbook['asks'] + bidsOrderSide = orderbook['bids'] + if createUpdate is not None: + bidAskArray = self.custom_parse_bid_ask(createUpdate, 'price', 'volume', 'order_id') + type = self.safe_string(createUpdate, 'type') + if type == 'ASK': + asksOrderSide.storeArray(bidAskArray) + elif type == 'BID': + bidsOrderSide.storeArray(bidAskArray) + deleteUpdate = self.safe_value(message, 'delete_update') + if deleteUpdate is not None: + orderId = self.safe_string(deleteUpdate, 'order_id') + asksOrderSide.storeArray([0, 0, orderId]) + bidsOrderSide.storeArray([0, 0, orderId]) + + def handle_message(self, client: Client, message): + if message == '': + return + subscriptions = list(client.subscriptions.values()) + handlers = [self.handle_order_book, self.handle_trades] + for j in range(0, len(handlers)): + handler = handlers[j] + handler(client, message, subscriptions[0]) diff --git a/ccxt/pro/mexc.py b/ccxt/pro/mexc.py new file mode 100644 index 0000000..1e80dcf --- /dev/null +++ b/ccxt/pro/mexc.py @@ -0,0 +1,1900 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolById, ArrayCacheByTimestamp +import hashlib +from ccxt.base.types import Any, Balances, Int, Order, OrderBook, Str, Strings, Ticker, Tickers, Trade +from ccxt.async_support.base.ws.client import Client +from typing import List +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import NotSupported + + +class mexc(ccxt.async_support.mexc): + + def describe(self) -> Any: + return self.deep_extend(super(mexc, self).describe(), { + 'has': { + 'ws': True, + 'cancelAllOrdersWs': False, + 'cancelOrdersWs': False, + 'cancelOrderWs': False, + 'createOrderWs': False, + 'editOrderWs': False, + 'fetchBalanceWs': False, + 'fetchOpenOrdersWs': False, + 'fetchOrderWs': False, + 'fetchTradesWs': False, + 'watchBalance': True, + 'watchMyTrades': True, + 'watchOHLCV': True, + 'watchOrderBook': True, + 'watchOrders': True, + 'watchTicker': True, + 'watchTickers': True, + 'watchBidsAsks': True, + 'watchTrades': True, + 'watchTradesForSymbols': False, + 'unWatchTicker': True, + 'unWatchTickers': True, + 'unWatchBidsAsks': True, + 'unWatchOHLCV': True, + 'unWatchOrderBook': True, + 'unWatchTrades': True, + }, + 'urls': { + 'api': { + 'ws': { + 'spot': 'wss://wbs-api.mexc.com/ws', + 'swap': 'wss://contract.mexc.com/edge', + }, + }, + }, + 'options': { + 'listenKeyRefreshRate': 1200000, + 'decompressBinary': False, + # TODO add reset connection after #16754 is merged + 'timeframes': { + '1m': 'Min1', + '5m': 'Min5', + '15m': 'Min15', + '30m': 'Min30', + '1h': 'Min60', + '4h': 'Hour4', + '8h': 'Hour8', + '1d': 'Day1', + '1w': 'Week1', + '1M': 'Month1', + }, + 'watchOrderBook': { + 'snapshotDelay': 25, + 'snapshotMaxRetries': 3, + }, + 'listenKey': None, + }, + 'streaming': { + 'ping': self.ping, + 'keepAlive': 8000, + }, + 'exceptions': { + }, + }) + + async def watch_ticker(self, symbol: str, params={}) -> Ticker: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#individual-symbol-book-ticker-streams + https://mexcdevelop.github.io/apidocs/contract_v1_en/#public-channels + https://mexcdevelop.github.io/apidocs/spot_v3_en/#miniticker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.miniTicker]: set to True for using the miniTicker endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + messageHash = 'ticker:' + market['symbol'] + if market['spot']: + channel = 'spot@public.aggre.bookTicker.v3.api.pb@100ms@' + market['id'] + return await self.watch_spot_public(channel, messageHash, params) + else: + channel = 'sub.ticker' + requestParams: dict = { + 'symbol': market['id'], + } + return await self.watch_swap_public(channel, messageHash, requestParams, params) + + def handle_ticker(self, client: Client, message): + # + # swap + # + # { + # "symbol": "BTC_USDT", + # "data": { + # "symbol": "BTC_USDT", + # "lastPrice": 76376.1, + # "riseFallRate": -0.0006, + # "fairPrice": 76374.4, + # "indexPrice": 76385.8, + # "volume24": 962062810, + # "amount24": 7344207079.96768, + # "maxBidPrice": 84024.3, + # "minAskPrice": 68747.2, + # "lower24Price": 75620.2, + # "high24Price": 77210, + # "timestamp": 1731137509138, + # "bid1": 76376.2, + # "ask1": 76376.3, + # "holdVol": 95479623, + # "riseFallValue": -46.5, + # "fundingRate": 0.0001, + # "zone": "UTC+8", + # "riseFallRates": [-0.0006, 0.1008, 0.2262, 0.2628, 0.2439, 1.0564], + # "riseFallRatesOfTimezone": [0.0065, -0.0013, -0.0006] + # }, + # "channel": "push.ticker", + # "ts": 1731137509138 + # } + # + # spot + # + # { + # "c": "spot@public.bookTicker.v3.api@BTCUSDT", + # "d": { + # "A": "4.70432", + # "B": "6.714863", + # "a": "20744.54", + # "b": "20744.17" + # }, + # "s": "BTCUSDT", + # "t": 1678643605721 + # } + # + # spot miniTicker + # + # { + # "d": { + # "s": "BTCUSDT", + # "p": "76522", + # "r": "0.0012", + # "tr": "0.0012", + # "h": "77196.3", + # "l": "75630.77", + # "v": "584664223.92", + # "q": "7666.720258", + # "lastRT": "-1", + # "MT": "0", + # "NV": "--", + # "t": "1731135533126" + # }, + # "c": "spot@public.miniTicker.v3.api@BTCUSDT@UTC+8", + # "t": 1731135533126, + # "s": "BTCUSDT" + # } + # + self.handle_bid_ask(client, message) + rawTicker = self.safe_dict_n(message, ['d', 'data', 'publicAggreBookTicker']) + marketId = self.safe_string_2(message, 's', 'symbol') + timestamp = self.safe_integer_2(message, 't', 'sendTime') + market = self.safe_market(marketId) + symbol = market['symbol'] + ticker = None + if market['spot']: + ticker = self.parse_ws_ticker(rawTicker, market) + ticker['timestamp'] = timestamp + ticker['datetime'] = self.iso8601(timestamp) + else: + ticker = self.parse_ticker(rawTicker, market) + self.tickers[symbol] = ticker + messageHash = 'ticker:' + symbol + client.resolve(ticker, messageHash) + + async def watch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#individual-symbol-book-ticker-streams + https://mexcdevelop.github.io/apidocs/contract_v1_en/#public-channels + https://mexcdevelop.github.io/apidocs/spot_v3_en/#minitickers + + :param str[] symbols: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.miniTicker]: set to True for using the miniTicker endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None) + messageHashes = [] + firstSymbol = self.safe_string(symbols, 0) + market = None + if firstSymbol is not None: + market = self.market(firstSymbol) + type = None + type, params = self.handle_market_type_and_params('watchTickers', market, params) + isSpot = (type == 'spot') + url = self.urls['api']['ws']['spot'] if (isSpot) else self.urls['api']['ws']['swap'] + request: dict = {} + if isSpot: + raise NotSupported(self.id + ' watchTickers does not support spot markets') + # miniTicker = False + # miniTicker, params = self.handle_option_and_params(params, 'watchTickers', 'miniTicker') + # topics = [] + # if not miniTicker: + # if symbols is None: + # raise ArgumentsRequired(self.id + ' watchTickers required symbols argument for the bookTicker channel') + # } + # marketIds = self.market_ids(symbols) + # for i in range(0, len(marketIds)): + # marketId = marketIds[i] + # messageHashes.append('ticker:' + symbols[i]) + # channel = 'spot@public.bookTicker.v3.api@' + marketId + # topics.append(channel) + # } + # else: + # topics.append('spot@public.miniTickers.v3.api@UTC+8') + # if symbols is None: + # messageHashes.append('spot:ticker') + # else: + # for i in range(0, len(symbols)): + # messageHashes.append('ticker:' + symbols[i]) + # } + # } + # } + # request['method'] = 'SUBSCRIPTION' + # request['params'] = topics + else: + request['method'] = 'sub.tickers' + request['params'] = {} + messageHashes.append('ticker') + ticker = await self.watch_multiple(url, messageHashes, self.extend(request, params), messageHashes) + if isSpot and self.newUpdates: + result: dict = {} + result[ticker['symbol']] = ticker + return result + return self.filter_by_array(self.tickers, 'symbol', symbols) + + def handle_tickers(self, client: Client, message): + # + # swap + # + # { + # "channel": "push.tickers", + # "data": [ + # { + # "symbol": "ETH_USDT", + # "lastPrice": 2324.5, + # "riseFallRate": 0.0356, + # "fairPrice": 2324.32, + # "indexPrice": 2325.44, + # "volume24": 25868309, + # "amount24": 591752573.9792, + # "maxBidPrice": 2557.98, + # "minAskPrice": 2092.89, + # "lower24Price": 2239.39, + # "high24Price": 2332.59, + # "timestamp": 1725872514111 + # } + # ], + # "ts": 1725872514111 + # } + # + # spot + # + # { + # "c": "spot@public.bookTicker.v3.api@BTCUSDT", + # "d": { + # "A": "4.70432", + # "B": "6.714863", + # "a": "20744.54", + # "b": "20744.17" + # }, + # "s": "BTCUSDT", + # "t": 1678643605721 + # } + # + # spot miniTicker + # + # { + # "d": { + # "s": "BTCUSDT", + # "p": "76522", + # "r": "0.0012", + # "tr": "0.0012", + # "h": "77196.3", + # "l": "75630.77", + # "v": "584664223.92", + # "q": "7666.720258", + # "lastRT": "-1", + # "MT": "0", + # "NV": "--", + # "t": "1731135533126" + # }, + # "c": "spot@public.miniTicker.v3.api@BTCUSDT@UTC+8", + # "t": 1731135533126, + # "s": "BTCUSDT" + # } + # + data = self.safe_list_2(message, 'data', 'd') + channel = self.safe_string(message, 'c', '') + marketId = self.safe_string(message, 's') + market = self.safe_market(marketId) + channelStartsWithSpot = channel.startswith('spot') + marketIdIsUndefined = marketId is None + isSpot = channelStartsWithSpot if marketIdIsUndefined else market['spot'] + spotPrefix = 'spot:' + messageHashPrefix = spotPrefix if isSpot else '' + topic = messageHashPrefix + 'ticker' + result = [] + for i in range(0, len(data)): + entry = data[i] + ticker = None + if isSpot: + ticker = self.parse_ws_ticker(entry, market) + else: + ticker = self.parse_ticker(entry) + symbol = ticker['symbol'] + self.tickers[symbol] = ticker + result.append(ticker) + messageHash = 'ticker:' + symbol + client.resolve(ticker, messageHash) + client.resolve(result, topic) + + def parse_ws_ticker(self, ticker, market=None): + # protobuf ticker + # "bidprice": "93387.28", # Best bid price + # "bidquantity": "3.73485", # Best bid quantity + # "askprice": "93387.29", # Best ask price + # "askquantity": "7.669875" # Best ask quantity + # + # spot + # + # { + # "A": "4.70432", + # "B": "6.714863", + # "a": "20744.54", + # "b": "20744.17" + # } + # + # spot miniTicker + # + # { + # "s": "BTCUSDT", + # "p": "76521", + # "r": "0.0012", + # "tr": "0.0012", + # "h": "77196.3", + # "l": "75630.77", + # "v": "584664223.92", + # "q": "7666.720258", + # "lastRT": "-1", + # "MT": "0", + # "NV": "--", + # "t": "1731135533126" + # } + # + marketId = self.safe_string(ticker, 's') + timestamp = self.safe_integer(ticker, 't') + price = self.safe_string(ticker, 'p') + return self.safe_ticker({ + 'info': ticker, + 'symbol': self.safe_symbol(marketId, market), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'open': None, + 'high': self.safe_number(ticker, 'h'), + 'low': self.safe_number(ticker, 'l'), + 'close': price, + 'last': price, + 'bid': self.safe_number_2(ticker, 'b', 'bidPrice'), + 'bidVolume': self.safe_number_2(ticker, 'B', 'bidQuantity'), + 'ask': self.safe_number_2(ticker, 'a', 'askPrice'), + 'askVolume': self.safe_number_2(ticker, 'A', 'askQuantity'), + 'vwap': None, + 'previousClose': None, + 'change': None, + 'percentage': self.safe_number(ticker, 'tr'), + 'average': None, + 'baseVolume': self.safe_number(ticker, 'v'), + 'quoteVolume': self.safe_number(ticker, 'q'), + }, market) + + async def watch_bids_asks(self, symbols: Strings = None, params={}) -> Tickers: + """ + + https://mexcdevelop.github.io/apidocs/spot_v3_en/#individual-symbol-book-ticker-streams + + watches best bid & ask for symbols + :param str[] symbols: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, True, False, True) + marketType = None + if symbols is None: + raise ArgumentsRequired(self.id + ' watchBidsAsks required symbols argument') + markets = self.markets_for_symbols(symbols) + marketType, params = self.handle_market_type_and_params('watchBidsAsks', markets[0], params) + isSpot = marketType == 'spot' + if not isSpot: + raise NotSupported(self.id + ' watchBidsAsks only support spot market') + messageHashes = [] + topics = [] + for i in range(0, len(symbols)): + if isSpot: + market = self.market(symbols[i]) + topics.append('spot@public.aggre.bookTicker.v3.api.pb@100ms@' + market['id']) + messageHashes.append('bidask:' + symbols[i]) + url = self.urls['api']['ws']['spot'] + request: dict = { + 'method': 'SUBSCRIPTION', + 'params': topics, + } + ticker = await self.watch_multiple(url, messageHashes, self.extend(request, params), messageHashes) + if self.newUpdates: + tickers: dict = {} + tickers[ticker['symbol']] = ticker + return tickers + return self.filter_by_array(self.bidsasks, 'symbol', symbols) + + def handle_bid_ask(self, client: Client, message): + # + # { + # "c": "spot@public.bookTicker.v3.api@BTCUSDT", + # "d": { + # "A": "4.70432", + # "B": "6.714863", + # "a": "20744.54", + # "b": "20744.17" + # }, + # "s": "BTCUSDT", + # "t": 1678643605721 + # } + # + parsedTicker = self.parse_ws_bid_ask(message) + symbol = self.safe_string(parsedTicker, 'symbol') + if symbol is None: + return + self.bidsasks[symbol] = parsedTicker + messageHash = 'bidask:' + symbol + client.resolve(parsedTicker, messageHash) + + def parse_ws_bid_ask(self, ticker, market=None): + data = self.safe_dict(ticker, 'd') + marketId = self.safe_string(ticker, 's') + market = self.safe_market(marketId, market) + symbol = self.safe_string(market, 'symbol') + timestamp = self.safe_integer(ticker, 't') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'ask': self.safe_number(data, 'a'), + 'askVolume': self.safe_number(data, 'A'), + 'bid': self.safe_number(data, 'b'), + 'bidVolume': self.safe_number(data, 'B'), + 'info': ticker, + }, market) + + async def watch_spot_public(self, channel, messageHash, params={}): + unsubscribed = self.safe_bool(params, 'unsubscribed', False) + params = self.omit(params, ['unsubscribed']) + url = self.urls['api']['ws']['spot'] + method = 'UNSUBSCRIPTION' if (unsubscribed) else 'SUBSCRIPTION' + request: dict = { + 'method': method, + 'params': [channel], + } + return await self.watch(url, messageHash, self.extend(request, params), messageHash) + + async def watch_spot_private(self, channel, messageHash, params={}): + self.check_required_credentials() + listenKey = await self.authenticate(channel) + url = self.urls['api']['ws']['spot'] + '?listenKey=' + listenKey + request: dict = { + 'method': 'SUBSCRIPTION', + 'params': [channel], + } + return await self.watch(url, messageHash, self.extend(request, params), channel) + + async def watch_swap_public(self, channel, messageHash, requestParams, params={}): + url = self.urls['api']['ws']['swap'] + request: dict = { + 'method': channel, + 'param': requestParams, + } + message = self.extend(request, params) + return await self.watch(url, messageHash, message, messageHash) + + async def watch_swap_private(self, messageHash, params={}): + self.check_required_credentials() + channel = 'login' + url = self.urls['api']['ws']['swap'] + timestamp = str(self.milliseconds()) + payload = self.apiKey + timestamp + signature = self.hmac(self.encode(payload), self.encode(self.secret), hashlib.sha256) + request: dict = { + 'method': channel, + 'param': { + 'apiKey': self.apiKey, + 'signature': signature, + 'reqTime': timestamp, + }, + } + message = self.extend(request, params) + return await self.watch(url, messageHash, message, channel) + + async def watch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + + https://www.mexc.com/api-docs/spot-v3/websocket-market-streams#trade-streams + + watches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + timeframes = self.safe_value(self.options, 'timeframes', {}) + timeframeId = self.safe_string(timeframes, timeframe) + messageHash = 'candles:' + symbol + ':' + timeframe + ohlcv = None + if market['spot']: + channel = 'spot@public.kline.v3.api.pb@' + market['id'] + '@' + timeframeId + ohlcv = await self.watch_spot_public(channel, messageHash, params) + else: + channel = 'sub.kline' + requestParams: dict = { + 'symbol': market['id'], + 'interval': timeframeId, + } + ohlcv = await self.watch_swap_public(channel, messageHash, requestParams, params) + if self.newUpdates: + limit = ohlcv.getLimit(symbol, limit) + return self.filter_by_since_limit(ohlcv, since, limit, 0, True) + + def handle_ohlcv(self, client: Client, message): + # + # spot + # + # { + # "d": { + # "e": "spot@public.kline.v3.api", + # "k": { + # "t": 1678642261, + # "o": 20626.94, + # "c": 20599.69, + # "h": 20626.94, + # "l": 20597.06, + # "v": 27.678686, + # "a": 570332.77, + # "T": 1678642320, + # "i": "Min1" + # } + # }, + # "c": "spot@public.kline.v3.api@BTCUSDT@Min1", + # "t": 1678642276459, + # "s": "BTCUSDT" + # } + # + # swap + # + # { + # "channel": "push.kline", + # "data": { + # "a": 325653.3287, + # "c": 38839, + # "h": 38909.5, + # "interval": "Min1", + # "l": 38833, + # "o": 38901.5, + # "q": 83808, + # "rc": 38839, + # "rh": 38909.5, + # "rl": 38833, + # "ro": 38909.5, + # "symbol": "BTC_USDT", + # "t": 1651230660 + # }, + # "symbol": "BTC_USDT", + # "ts": 1651230713067 + # } + # protobuf + # { + # "channel":"spot@public.kline.v3.api.pb@BTCUSDT@Min1", + # "symbol":"BTCUSDT", + # "symbolId":"2fb942154ef44a4ab2ef98c8afb6a4a7", + # "createTime":"1754737941062", + # "publicSpotKline":{ + # "interval":"Min1", + # "windowStart":"1754737920", + # "openingPrice":"117317.31", + # "closingPrice":"117325.26", + # "highestPrice":"117341", + # "lowestPrice":"117317.3", + # "volume":"3.12599854", + # "amount":"366804.43", + # "windowEnd":"1754737980" + # } + # } + # + parsed: dict = None + symbol: Str = None + timeframe: Str = None + if 'publicSpotKline' in message: + symbol = self.symbol(self.safe_string(message, 'symbol')) + data = self.safe_dict(message, 'publicSpotKline', {}) + timeframeId = self.safe_string(data, 'interval') + timeframe = self.find_timeframe(timeframeId, self.options['timeframes']) + parsed = self.parse_ws_ohlcv(data, self.safe_market(symbol)) + else: + d = self.safe_value_2(message, 'd', 'data', {}) + rawOhlcv = self.safe_value(d, 'k', d) + timeframeId = self.safe_string_2(rawOhlcv, 'i', 'interval') + timeframes = self.safe_value(self.options, 'timeframes', {}) + timeframe = self.find_timeframe(timeframeId, timeframes) + marketId = self.safe_string_2(message, 's', 'symbol') + market = self.safe_market(marketId) + symbol = market['symbol'] + parsed = self.parse_ws_ohlcv(rawOhlcv, market) + messageHash = 'candles:' + symbol + ':' + timeframe + self.ohlcvs[symbol] = self.safe_value(self.ohlcvs, symbol, {}) + stored = self.safe_value(self.ohlcvs[symbol], timeframe) + if stored is None: + limit = self.safe_integer(self.options, 'OHLCVLimit', 1000) + stored = ArrayCacheByTimestamp(limit) + self.ohlcvs[symbol][timeframe] = stored + stored.append(parsed) + client.resolve(stored, messageHash) + + def parse_ws_ohlcv(self, ohlcv, market=None) -> list: + # + # spot + # + # { + # "t": 1678642260, + # "o": 20626.94, + # "c": 20599.69, + # "h": 20626.94, + # "l": 20597.06, + # "v": 27.678686, + # "a": 570332.77, + # "T": 1678642320, + # "i": "Min1" + # } + # + # swap + # { + # "symbol": "BTC_USDT", + # "interval": "Min1", + # "t": 1680055080, + # "o": 27301.9, + # "c": 27301.8, + # "h": 27301.9, + # "l": 27301.8, + # "a": 8.19054, + # "q": 3, + # "ro": 27301.8, + # "rc": 27301.8, + # "rh": 27301.8, + # "rl": 27301.8 + # } + # protobuf + # + # "interval":"Min1", + # "windowStart":"1754737920", + # "openingPrice":"117317.31", + # "closingPrice":"117325.26", + # "highestPrice":"117341", + # "lowestPrice":"117317.3", + # "volume":"3.12599854", + # "amount":"366804.43", + # "windowEnd":"1754737980" + # + return [ + self.safe_timestamp_2(ohlcv, 't', 'windowStart'), + self.safe_number_2(ohlcv, 'o', 'openingPrice'), + self.safe_number_2(ohlcv, 'h', 'highestPrice'), + self.safe_number_2(ohlcv, 'l', 'lowestPrice'), + self.safe_number_2(ohlcv, 'c', 'closingPrice'), + self.safe_number_2(ohlcv, 'v', 'volume'), + ] + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + + https://www.mexc.com/api-docs/spot-v3/websocket-market-streams#trade-streams + https://mexcdevelop.github.io/apidocs/contract_v1_en/#public-channels + + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.frequency]: the frequency of the order book updates, default is '10ms', can be '100ms' or '10ms + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + messageHash = 'orderbook:' + symbol + orderbook = None + if market['spot']: + frequency = None + frequency, params = self.handle_option_and_params(params, 'watchOrderBook', 'frequency', '100ms') + channel = 'spot@public.aggre.depth.v3.api.pb@' + frequency + '@' + market['id'] + orderbook = await self.watch_spot_public(channel, messageHash, params) + else: + channel = 'sub.depth' + requestParams: dict = { + 'symbol': market['id'], + } + orderbook = await self.watch_swap_public(channel, messageHash, requestParams, params) + return orderbook.limit() + + def handle_order_book_subscription(self, client: Client, message): + # spot + # {id: 0, code: 0, msg: "spot@public.increase.depth.v3.api@BTCUSDT"} + # + msg = self.safe_string(message, 'msg') + parts = msg.split('@') + marketId = self.safe_string(parts, 2) + symbol = self.safe_symbol(marketId) + self.orderbooks[symbol] = self.order_book({}) + + def get_cache_index(self, orderbook, cache): + # return the first index of the cache that can be applied to the orderbook or -1 if not possible + nonce = self.safe_integer(orderbook, 'nonce') + firstDelta = self.safe_value(cache, 0) + firstDeltaNonce = self.safe_integer_n(firstDelta, ['r', 'version', 'fromVersion']) + if nonce < firstDeltaNonce - 1: + return -1 + for i in range(0, len(cache)): + delta = cache[i] + deltaNonce = self.safe_integer_n(delta, ['r', 'version', 'fromVersion']) + if deltaNonce >= nonce: + return i + return len(cache) + + def handle_order_book(self, client: Client, message): + # + # spot + # { + # "c": "spot@public.increase.depth.v3.api@BTCUSDT", + # "d": { + # "asks": [{ + # "p": "20290.89", + # "v": "0.000000" + # }], + # "e": "spot@public.increase.depth.v3.api", + # "r": "3407459756" + # }, + # "s": "BTCUSDT", + # "t": 1661932660144 + # } + # + # + # + # swap + # { + # "channel":"push.depth", + # "data":{ + # "asks":[ + # [ + # 39146.5, + # 11264, + # 1 + # ] + # ], + # "bids":[ + # [ + # 39144, + # 35460, + # 1 + # ] + # ], + # "end":4895965272, + # "begin":4895965271 + # }, + # "symbol":"BTC_USDT", + # "ts":1651239652372 + # } + # protofbuf + # { + # "channel":"spot@public.aggre.depth.v3.api.pb@100ms@BTCUSDT", + # "symbol":"BTCUSDT", + # "sendTime":"1754741322152", + # "publicAggreDepths":{ + # "asks":[ + # { + # "price":"117145.49", + # "quantity":"0" + # } + # ], + # "bids":[ + # { + # "price":"117053.41", + # "quantity":"1.86837271" + # } + # ], + # "eventType":"spot@public.aggre.depth.v3.api.pb@100ms", + # "fromVersion":"43296363236", + # "toVersion":"43296363255" + # } + # } + # + data = self.safe_dict_n(message, ['d', 'data', 'publicAggreDepths']) + marketId = self.safe_string_2(message, 's', 'symbol') + symbol = self.safe_symbol(marketId) + messageHash = 'orderbook:' + symbol + subscription = self.safe_value(client.subscriptions, messageHash) + limit = self.safe_integer(subscription, 'limit') + if not (symbol in self.orderbooks): + self.orderbooks[symbol] = self.order_book() + storedOrderBook = self.orderbooks[symbol] + nonce = self.safe_integer(storedOrderBook, 'nonce') + shouldReturn = False + if nonce is None: + cacheLength = len(storedOrderBook.cache) + snapshotDelay = self.handle_option('watchOrderBook', 'snapshotDelay', 25) + if cacheLength == snapshotDelay: + self.spawn(self.load_order_book, client, messageHash, symbol, limit, {}) + storedOrderBook.cache.append(data) + return + try: + self.handle_delta(storedOrderBook, data) + timestamp = self.safe_integer_n(message, ['t', 'ts', 'sendTime']) + storedOrderBook['timestamp'] = timestamp + storedOrderBook['datetime'] = self.iso8601(timestamp) + except Exception as e: + del client.subscriptions[messageHash] + client.reject(e, messageHash) + # return + shouldReturn = True + if shouldReturn: + return # go requirement + client.resolve(storedOrderBook, messageHash) + + def handle_bookside_delta(self, bookside, bidasks): + # + # [{ + # "p": "20290.89", + # "v": "0.000000" + # }] + # + for i in range(0, len(bidasks)): + bidask = bidasks[i] + if isinstance(bidask, list): + bookside.storeArray(bidask) + else: + price = self.safe_float_2(bidask, 'p', 'price') + amount = self.safe_float_2(bidask, 'v', 'quantity') + bookside.store(price, amount) + + def handle_delta(self, orderbook, delta): + existingNonce = self.safe_integer(orderbook, 'nonce') + deltaNonce = self.safe_integer_n(delta, ['r', 'version', 'fromVersion']) + if deltaNonce < existingNonce: + # even when doing < comparison, self happens: https://app.travis-ci.com/github/ccxt/ccxt/builds/269234741#L1809 + # so, we just skip old updates + return + orderbook['nonce'] = deltaNonce + asks = self.safe_list(delta, 'asks', []) + bids = self.safe_list(delta, 'bids', []) + asksOrderSide = orderbook['asks'] + bidsOrderSide = orderbook['bids'] + self.handle_bookside_delta(asksOrderSide, asks) + self.handle_bookside_delta(bidsOrderSide, bids) + + async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + + https://www.mexc.com/api-docs/spot-v3/websocket-market-streams#trade-streams + https://mexcdevelop.github.io/apidocs/contract_v1_en/#public-channels + + get the list of most recent trades for a particular symbol + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + messageHash = 'trades:' + symbol + trades = None + if market['spot']: + channel = 'spot@public.aggre.deals.v3.api.pb@100ms@' + market['id'] + trades = await self.watch_spot_public(channel, messageHash, params) + else: + channel = 'sub.deal' + requestParams: dict = { + 'symbol': market['id'], + } + trades = await self.watch_swap_public(channel, messageHash, requestParams, params) + if self.newUpdates: + limit = trades.getLimit(symbol, limit) + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + def handle_trades(self, client: Client, message): + # protobuf + # { + # "channel": "spot@public.aggre.deals.v3.api.pb@100ms@BTCUSDT", + # "publicdeals": { + # "dealsList": [ + # { + # "price": "93220.00", # Trade price + # "quantity": "0.04438243", # Trade quantity + # "tradetype": 2, # Trade type(1: Buy, 2: Sell) + # "time": 1736409765051 # Trade time + # } + # ], + # "eventtype": "spot@public.aggre.deals.v3.api.pb@100ms" # Event type + # }, + # "symbol": "BTCUSDT", # Trading pair + # "sendtime": 1736409765052 # Event time + # } + # + # { + # "c": "spot@public.deals.v3.api@BTCUSDT", + # "d": { + # "deals": [{ + # "p": "20382.70", + # "v": "0.043800", + # "S": 1, + # "t": 1678593222456, + # },], + # "e": "spot@public.deals.v3.api", + # }, + # "s": "BTCUSDT", + # "t": 1678593222460, + # } + # + # swap + # { + # "symbol": "BTC_USDT", + # "data": [ + # { + # "p": 114350.4, + # "v": 4, + # "T": 2, + # "O": 3, + # "M": 2, + # "t": 1760368563597 + # } + # ], + # "channel": "push.deal", + # "ts": 1680055941870 + # } + # + marketId = self.safe_string_2(message, 's', 'symbol') + market = self.safe_market(marketId) + symbol = market['symbol'] + messageHash = 'trades:' + symbol + stored = self.safe_value(self.trades, symbol) + if stored is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + stored = ArrayCache(limit) + self.trades[symbol] = stored + d = self.safe_dict_n(message, ['d', 'publicAggreDeals']) + trades = self.safe_list_2(d, 'deals', 'dealsList', [d]) + if d is None: + trades = self.safe_list(message, 'data', []) + for j in range(0, len(trades)): + parsedTrade = None + if market['spot']: + parsedTrade = self.parse_ws_trade(trades[j], market) + else: + parsedTrade = self.parse_trade(trades[j], market) + stored.append(parsedTrade) + client.resolve(stored, messageHash) + + async def watch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + + https://www.mexc.com/api-docs/spot-v3/websocket-user-data-streams#spot-account-deals + https://mexcdevelop.github.io/apidocs/contract_v1_en/#private-channels + + watches information on multiple trades made by the user + :param str symbol: unified market symbol of the market trades were made in + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trade structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + messageHash = 'myTrades' + market = None + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + messageHash = messageHash + ':' + symbol + type = None + type, params = self.handle_market_type_and_params('watchMyTrades', market, params) + trades = None + if type == 'spot': + channel = 'spot@private.deals.v3.api.pb' + trades = await self.watch_spot_private(channel, messageHash, params) + else: + trades = await self.watch_swap_private(messageHash, params) + if self.newUpdates: + limit = trades.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(trades, symbol, since, limit, True) + + def handle_my_trade(self, client: Client, message, subscription=None): + # + # { + # "c": "spot@private.deals.v3.api", + # "d": { + # "p": "22339.99", + # "v": "0.000235", + # "S": 1, + # "T": 1678670940695, + # "t": "9f6a47fb926442e496c5c4c104076ae3", + # "c": '', + # "i": "e2b9835d1b6745f8a10ab74a81a16d50", + # "m": 0, + # "st": 0 + # }, + # "s": "BTCUSDT", + # "t": 1678670940700 + # } + # { + # channel: "spot@private.deals.v3.api.pb", + # symbol: "MXUSDT", + # sendTime: 1736417034332, + # privateDeals { + # price: "3.6962", + # quantity: "1", + # amount: "3.6962", + # tradeType: 2, + # tradeId: "505979017439002624X1", + # orderId: "C02__505979017439002624115", + # feeAmount: "0.0003998377369698171", + # feeCurrency: "MX", + # time: 1736417034280 + # } + # } + # + messageHash = 'myTrades' + data = self.safe_dict_n(message, ['d', 'data', 'privateDeals']) + futuresMarketId = self.safe_string(data, 'symbol') + marketId = self.safe_string_2(message, 's', 'symbol', futuresMarketId) + market = self.safe_market(marketId) + symbol = market['symbol'] + trade = None + if market['spot']: + trade = self.parse_ws_trade(data, market) + else: + trade = self.parse_trade(data, market) + trades = self.myTrades + if trades is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + trades = ArrayCacheBySymbolById(limit) + self.myTrades = trades + trades.append(trade) + client.resolve(trades, messageHash) + symbolSpecificMessageHash = messageHash + ':' + symbol + client.resolve(trades, symbolSpecificMessageHash) + + def parse_ws_trade(self, trade, market=None): + # + # public trade(protobuf) + # { + # "p": "20382.70", + # "v": "0.043800", + # "S": 1, + # "t": 1678593222456, + # } + # private trade + # { + # "S": 1, + # "T": 1661938980268, + # "c": "", + # "i": "c079b0fcb80a46e8b128b281ce4e4f38", + # "m": 1, + # "p": "1.008", + # "st": 0, + # "t": "4079b1522a0b40e7919f609e1ea38d44", + # "v": "5" + # } + # + # d: { + # p: '1.0005', + # v: '5.71', + # a: '5.712855', + # S: 1, + # T: 1714325698237, + # t: 'edafcd9fdc2f426e82443d114691f724', + # c: '', + # i: 'C02__413321238354677760043', + # m: 0, + # st: 0, + # n: '0.005712855', + # N: 'USDT' + # } + # protobuf + # + # { + # price: "3.6962", + # quantity: "1", + # amount: "3.6962", + # tradeType: 2, + # tradeId: "505979017439002624X1", + # orderId: "C02__505979017439002624115", + # feeAmount: "0.0003998377369698171", + # feeCurrency: "MX", + # time: 1736417034280 + # } + # + timestamp = self.safe_integer_2(trade, 'T', 'time') + tradeId = self.safe_string_2(trade, 't', 'tradeId') + if timestamp is None: + timestamp = self.safe_integer(trade, 't') + tradeId = None + priceString = self.safe_string_2(trade, 'p', 'price') + amountString = self.safe_string_2(trade, 'v', 'quantity') + rawSide = self.safe_string_2(trade, 'S', 'tradeType') + side = 'buy' if (rawSide == '1') else 'sell' + isMaker = self.safe_integer(trade, 'm') + feeAmount = self.safe_string_2(trade, 'n', 'feeAmount') + feeCurrencyId = self.safe_string_2(trade, 'N', 'feeCurrency') + return self.safe_trade({ + 'info': trade, + 'id': tradeId, + 'order': self.safe_string_2(trade, 'i', 'orderId'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': self.safe_symbol(None, market), + 'type': None, + 'side': side, + 'takerOrMaker': 'maker' if (isMaker) else 'taker', + 'price': priceString, + 'amount': amountString, + 'cost': self.safe_string(trade, 'amount'), + 'fee': { + 'cost': feeAmount, + 'currency': self.safe_currency_code(feeCurrencyId), + }, + }, market) + + async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + + https://www.mexc.com/api-docs/spot-v3/websocket-user-data-streams#spot-account-orders + https://mexcdevelop.github.io/apidocs/spot_v3_en/#margin-account-orders + + watches information on multiple orders made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str|None params['type']: the type of orders to retrieve, can be 'spot' or 'margin' + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + messageHash = 'orders' + market = None + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + messageHash = messageHash + ':' + symbol + type = None + type, params = self.handle_market_type_and_params('watchOrders', market, params) + orders = None + if type == 'spot': + channel = 'spot@private.orders.v3.api.pb' + orders = await self.watch_spot_private(channel, messageHash, params) + else: + orders = await self.watch_swap_private(messageHash, params) + if self.newUpdates: + limit = orders.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(orders, symbol, since, limit, True) + + def handle_order(self, client: Client, message): + # + # spot + # { + # "c": "spot@private.orders.v3.api", + # "d": { + # "A":8.0, + # "O":1661938138000, + # "S":1, + # "V":10, + # "a":8, + # "c":"", + # "i":"e03a5c7441e44ed899466a7140b71391", + # "m":0, + # "o":1, + # "p":0.8, + # "s":1, + # "v":10, + # "ap":0, + # "cv":0, + # "ca":0 + # }, + # "s": "MXUSDT", + # "t": 1661938138193 + # } + # spot - stop + # { + # "c": "spot@private.orders.v3.api", + # "d": { + # "N":"USDT", + # "O":1661938853715, + # "P":0.9, + # "S":1, + # "T":"LE", + # "i":"f6d82e5f41d745f59fe9d3cafffd80b5", + # "o":100, + # "p":1.01, + # "s":"NEW", + # "v":6 + # }, + # "s": "MXUSDT", + # "t": 1661938853727 + # } + # margin + # { + # "c": "margin@private.orders.v3.api", + # "d":{ + # "O":1661938138000, + # "p":"0.8", + # "a":"8", + # "v":"10", + # "da":"0", + # "dv":"0", + # "A":"8.0", + # "V":"10", + # "n": "0", + # "N": "USDT", + # "S":1, + # "o":1, + # "s":1, + # "i":"e03a5c7441e44ed899466a7140b71391", + # }, + # "s": "MXUSDT", + # "t":1661938138193 + # } + # protobuf + # { + # channel: "spot@private.orders.v3.api.pb", + # symbol: "MXUSDT", + # sendTime: 1736417034281, + # privateOrders {} + # } + # + messageHash = 'orders' + data = self.safe_dict_n(message, ['d', 'data', 'privateOrders']) + futuresMarketId = self.safe_string(data, 'symbol') + marketId = self.safe_string_2(message, 's', 'symbol', futuresMarketId) + market = self.safe_market(marketId) + symbol = market['symbol'] + parsed = None + if market['spot']: + parsed = self.parse_ws_order(data, market) + else: + parsed = self.parse_order(data, market) + orders = self.orders + if orders is None: + limit = self.safe_integer(self.options, 'ordersLimit', 1000) + orders = ArrayCacheBySymbolById(limit) + self.orders = orders + orders.append(parsed) + client.resolve(orders, messageHash) + symbolSpecificMessageHash = messageHash + ':' + symbol + client.resolve(orders, symbolSpecificMessageHash) + + def parse_ws_order(self, order, market=None): + # + # spot + # { + # "A":8.0, + # "O":1661938138000, + # "S":1, + # "V":10, + # "a":8, + # "c":"", + # "i":"e03a5c7441e44ed899466a7140b71391", + # "m":0, + # "o":1, + # "p":0.8, + # "s":1, + # "v":10, + # "ap":0, + # "cv":0, + # "ca":0 + # } + # spot - stop + # { + # "N":"USDT", + # "O":1661938853715, + # "P":0.9, + # "S":1, + # "T":"LE", + # "i":"f6d82e5f41d745f59fe9d3cafffd80b5", + # "o":100, + # "p":1.01, + # "s":"NEW", + # "v":6 + # } + # margin + # { + # "O":1661938138000, + # "p":"0.8", + # "a":"8", + # "v":"10", + # "da":"0", + # "dv":"0", + # "A":"8.0", + # "V":"10", + # "n": "0", + # "N": "USDT", + # "S":1, + # "o":1, + # "s":1, + # "i":"e03a5c7441e44ed899466a7140b71391", + # } + # protofbuf spot order + # { + # "id":"C02__583905164440776704043", + # "price":"0.001053", + # "quantity":"2000", + # "amount":"0", + # "avgPrice":"0.001007", + # "orderType":5, + # "tradeType":1, + # "remainAmount":"0.092", + # "remainQuantity":"0", + # "lastDealQuantity":"2000", + # "cumulativeQuantity":"2000", + # "cumulativeAmount":"2.014", + # "status":2, + # "createTime":"1754996075502" + # } + # + timestamp = self.safe_integer(order, 'createTime') + side = self.safe_string(order, 'tradeType') + status = self.safe_string(order, 'status') + type = self.safe_string(order, 'orderType') + fee = None + feeCurrency = self.safe_string(order, 'N') + if feeCurrency is not None: + fee = { + 'currency': feeCurrency, + 'cost': None, + } + return self.safe_order({ + 'id': self.safe_string(order, 'id'), + 'clientOrderId': self.safe_string(order, 'clientId'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'status': self.parse_ws_order_status(status, market), + 'symbol': self.safe_symbol(None, market), + 'type': self.parse_ws_order_type(type), + 'timeInForce': self.parse_ws_time_in_force(type), + 'side': 'buy' if (side == '1') else 'sell', + 'price': self.safe_string(order, 'price'), + 'stopPrice': None, + 'triggerPrice': None, + 'average': self.safe_string(order, 'avgPrice'), + 'amount': self.safe_string(order, 'quantity'), + 'cost': self.safe_string(order, 'amount'), + 'filled': self.safe_string(order, 'cumulativeQuantity'), + 'remaining': self.safe_string(order, 'remainQuantity'), + 'fee': fee, + 'trades': None, + 'info': order, + }, market) + + def parse_ws_order_status(self, status, market=None): + statuses: dict = { + '1': 'open', # new order + '2': 'closed', # filled + '3': 'open', # partially filled + '4': 'canceled', # canceled + '5': 'closed', # partially filled then canceled + 'NEW': 'open', + 'CANCELED': 'canceled', + 'EXECUTED': 'closed', + 'FAILED': 'rejected', + } + return self.safe_string(statuses, status, status) + + def parse_ws_order_type(self, type): + types: dict = { + '1': 'limit', # LIMIT_ORDER + '2': 'limit', # POST_ONLY + '3': None, # IMMEDIATE_OR_CANCEL + '4': None, # FILL_OR_KILL + '5': 'market', # MARKET_ORDER + '100': 'limit', # STOP_LIMIT + } + return self.safe_string(types, type) + + def parse_ws_time_in_force(self, timeInForce): + timeInForceIds: dict = { + '1': 'GTC', # LIMIT_ORDER + '2': 'PO', # POST_ONLY + '3': 'IOC', # IMMEDIATE_OR_CANCEL + '4': 'FOK', # FILL_OR_KILL + '5': 'GTC', # MARKET_ORDER + '100': 'GTC', # STOP_LIMIT + } + return self.safe_string(timeInForceIds, timeInForce) + + async def watch_balance(self, params={}) -> Balances: + """ + + https://www.mexc.com/api-docs/spot-v3/websocket-user-data-streams#spot-account-update + + watch balance and get the amount of funds available for trading or funds locked in orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + type = None + type, params = self.handle_market_type_and_params('watchBalance', None, params) + messageHash = 'balance:' + type + if type == 'spot': + channel = 'spot@private.account.v3.api.pb' + return await self.watch_spot_private(channel, messageHash, params) + else: + return await self.watch_swap_private(messageHash, params) + + def handle_balance(self, client: Client, message): + # + # spot + # + # { + # channel: "spot@private.account.v3.api.pb", + # createTime: "1758134605364", + # sendTime: "1758134605373", + # privateAccount: { + # vcoinName: "USDT", + # coinId: "128f589271cb4951b03e71e6323eb7be", + # balanceAmount: "0.006016465074677006", + # balanceAmountChange: "-4.4022", + # frozenAmount: "4.4022", + # frozenAmountChange: "4.4022", + # type: "ENTRUST_PLACE", + # time: "1758134605364", + # } + # } + # + # + # swap balance + # + # { + # "channel": "push.personal.asset", + # "data": { + # "availableBalance": 67.2426683348, + # "bonus": 0, + # "currency": "USDT", + # "frozenBalance": 0, + # "positionMargin": 1.36945756 + # }, + # "ts": 1680059188190 + # } + # + channel = self.safe_string(message, 'channel') + type = 'spot' if (channel == 'spot@private.account.v3.api.pb') else 'swap' + messageHash = 'balance:' + type + data = self.safe_dict_n(message, ['data', 'privateAccount']) + futuresTimestamp = self.safe_integer_2(message, 'ts', 'createTime') + timestamp = self.safe_integer_2(data, 'time', futuresTimestamp) + if not (type in self.balance): + self.balance[type] = {} + self.balance[type]['info'] = data + self.balance[type]['timestamp'] = timestamp + self.balance[type]['datetime'] = self.iso8601(timestamp) + currencyId = self.safe_string_n(data, ['currency', 'vcoinName']) + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string_2(data, 'balanceAmount', 'availableBalance') + account['used'] = self.safe_string_n(data, ['frozenBalance', 'frozenAmount']) + self.balance[type][code] = account + self.balance[type] = self.safe_balance(self.balance[type]) + client.resolve(self.balance[type], messageHash) + + async def un_watch_ticker(self, symbol: str, params={}) -> Any: + """ + unWatches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + messageHash = 'unsubscribe:ticker:' + market['symbol'] + url = None + channel = None + if market['spot']: + channel = 'spot@public.aggre.bookTicker.v3.api.pb@100ms@' + market['id'] + url = self.urls['api']['ws']['spot'] + params['unsubscribed'] = True + self.watch_spot_public(channel, messageHash, params) + else: + channel = 'unsub.ticker' + requestParams: dict = { + 'symbol': market['id'], + } + url = self.urls['api']['ws']['swap'] + self.watch_swap_public(channel, messageHash, requestParams, params) + client = self.client(url) + self.handle_unsubscriptions(client, [messageHash]) + return None + + async def un_watch_tickers(self, symbols: Strings = None, params={}) -> Any: + """ + unWatches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list + :param str[] symbols: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None) + messageHashes = [] + firstSymbol = self.safe_string(symbols, 0) + market = None + if firstSymbol is not None: + market = self.market(firstSymbol) + type = None + type, params = self.handle_market_type_and_params('watchTickers', market, params) + isSpot = (type == 'spot') + url = self.urls['api']['ws']['spot'] if (isSpot) else self.urls['api']['ws']['swap'] + request: dict = {} + if isSpot: + raise NotSupported(self.id + ' watchTickers does not support spot markets') + # miniTicker = False + # miniTicker, params = self.handle_option_and_params(params, 'watchTickers', 'miniTicker') + # topics = [] + # if not miniTicker: + # if symbols is None: + # raise ArgumentsRequired(self.id + ' watchTickers required symbols argument for the bookTicker channel') + # } + # marketIds = self.market_ids(symbols) + # for i in range(0, len(marketIds)): + # marketId = marketIds[i] + # messageHashes.append('unsubscribe:ticker:' + symbols[i]) + # channel = 'spot@public.bookTicker.v3.api@' + marketId + # topics.append(channel) + # } + # else: + # topics.append('spot@public.miniTickers.v3.api@UTC+8') + # if symbols is None: + # messageHashes.append('unsubscribe:spot:ticker') + # else: + # for i in range(0, len(symbols)): + # messageHashes.append('unsubscribe:ticker:' + symbols[i]) + # } + # } + # } + # request['method'] = 'UNSUBSCRIPTION' + # request['params'] = topics + else: + request['method'] = 'unsub.tickers' + request['params'] = {} + messageHashes.append('unsubscribe:ticker') + client = self.client(url) + self.watch_multiple(url, messageHashes, self.extend(request, params), messageHashes) + self.handle_unsubscriptions(client, messageHashes) + return None + + async def un_watch_bids_asks(self, symbols: Strings = None, params={}) -> Any: + """ + unWatches best bid & ask for symbols + :param str[] symbols: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, True, False, True) + marketType = None + if symbols is None: + raise ArgumentsRequired(self.id + ' watchBidsAsks required symbols argument') + markets = self.markets_for_symbols(symbols) + marketType, params = self.handle_market_type_and_params('watchBidsAsks', markets[0], params) + isSpot = marketType == 'spot' + if not isSpot: + raise NotSupported(self.id + ' watchBidsAsks only support spot market') + messageHashes = [] + topics = [] + for i in range(0, len(symbols)): + if isSpot: + market = self.market(symbols[i]) + topics.append('spot@public.aggre.bookTicker.v3.api.pb@100ms@' + market['id']) + messageHashes.append('unsubscribe:bidask:' + symbols[i]) + url = self.urls['api']['ws']['spot'] + request: dict = { + 'method': 'UNSUBSCRIPTION', + 'params': topics, + } + client = self.client(url) + self.watch_multiple(url, messageHashes, self.extend(request, params), messageHashes) + self.handle_unsubscriptions(client, messageHashes) + return None + + async def un_watch_ohlcv(self, symbol: str, timeframe: str = '1m', params={}) -> Any: + """ + unWatches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param dict [params]: extra parameters specific to the exchange API endpoint + :param dict [params.timezone]: if provided, kline intervals are interpreted in that timezone instead of UTC, example '+08:00' + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + timeframes = self.safe_value(self.options, 'timeframes', {}) + timeframeId = self.safe_string(timeframes, timeframe) + messageHash = 'unsubscribe:candles:' + symbol + ':' + timeframe + url = None + if market['spot']: + url = self.urls['api']['ws']['spot'] + channel = 'spot@public.kline.v3.api.pb@' + market['id'] + '@' + timeframeId + params['unsubscribed'] = True + self.watch_spot_public(channel, messageHash, params) + else: + url = self.urls['api']['ws']['swap'] + channel = 'unsub.kline' + requestParams: dict = { + 'symbol': market['id'], + 'interval': timeframeId, + } + self.watch_swap_public(channel, messageHash, requestParams, params) + client = self.client(url) + self.handle_unsubscriptions(client, [messageHash]) + return None + + async def un_watch_order_book(self, symbol: str, params={}) -> Any: + """ + unWatches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str symbol: unified array of symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.frequency]: the frequency of the order book updates, default is '10ms', can be '100ms' or '10ms + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + messageHash = 'unsubscribe:orderbook:' + symbol + url = None + if market['spot']: + url = self.urls['api']['ws']['spot'] + frequency = None + frequency, params = self.handle_option_and_params(params, 'watchOrderBook', 'frequency', '100ms') + channel = 'spot@public.aggre.depth.v3.api.pb@' + frequency + '@' + market['id'] + params['unsubscribed'] = True + self.watch_spot_public(channel, messageHash, params) + else: + url = self.urls['api']['ws']['swap'] + channel = 'unsub.depth' + requestParams: dict = { + 'symbol': market['id'], + } + self.watch_swap_public(channel, messageHash, requestParams, params) + client = self.client(url) + self.handle_unsubscriptions(client, [messageHash]) + return None + + async def un_watch_trades(self, symbol: str, params={}) -> Any: + """ + unsubscribes from the trades channel + :param str symbol: unified symbol of the market to fetch trades for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.name]: the name of the method to call, 'trade' or 'aggTrade', default is 'trade' + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + messageHash = 'unsubscribe:trades:' + symbol + url = None + if market['spot']: + url = self.urls['api']['ws']['spot'] + channel = 'spot@public.aggre.deals.v3.api.pb@100ms@' + market['id'] + params['unsubscribed'] = True + self.watch_spot_public(channel, messageHash, params) + else: + url = self.urls['api']['ws']['swap'] + channel = 'unsub.deal' + requestParams: dict = { + 'symbol': market['id'], + } + self.watch_swap_public(channel, messageHash, requestParams, params) + client = self.client(url) + self.handle_unsubscriptions(client, [messageHash]) + return None + + def handle_unsubscriptions(self, client: Client, messageHashes: List[str]): + for i in range(0, len(messageHashes)): + messageHash = messageHashes[i] + subMessageHash = messageHash.replace('unsubscribe:', '') + self.clean_unsubscription(client, subMessageHash, messageHash) + if messageHash.find('ticker') >= 0: + symbol = messageHash.replace('unsubscribe:ticker:', '') + if symbol.find('unsubscribe') >= 0: + # unWatchTickers + symbols = list(self.tickers.keys()) + for j in range(0, len(symbols)): + del self.tickers[symbols[j]] + elif symbol in self.tickers: + del self.tickers[symbol] + elif messageHash.find('bidask') >= 0: + symbol = messageHash.replace('unsubscribe:bidask:', '') + if symbol in self.bidsasks: + del self.bidsasks[symbol] + elif messageHash.find('candles') >= 0: + splitHashes = messageHash.split(':') + symbol = self.safe_string(splitHashes, 2) + if len(splitHashes) > 4: + symbol += ':' + self.safe_string(splitHashes, 3) + if symbol in self.ohlcvs: + del self.ohlcvs[symbol] + elif messageHash.find('orderbook') >= 0: + symbol = messageHash.replace('unsubscribe:orderbook:', '') + if symbol in self.orderbooks: + del self.orderbooks[symbol] + elif messageHash.find('trades') >= 0: + symbol = messageHash.replace('unsubscribe:trades:', '') + if symbol in self.trades: + del self.trades[symbol] + + async def authenticate(self, subscriptionHash, params={}): + # we only need one listenKey since ccxt shares connections + listenKey = self.safe_string(self.options, 'listenKey') + if listenKey is not None: + return listenKey + response = await self.spotPrivatePostUserDataStream(params) + # + # { + # "listenKey": "pqia91ma19a5s61cv6a81va65sdf19v8a65a1a5s61cv6a81va65sdf19v8a65a1" + # } + # + listenKey = self.safe_string(response, 'listenKey') + self.options['listenKey'] = listenKey + listenKeyRefreshRate = self.safe_integer(self.options, 'listenKeyRefreshRate', 1200000) + self.delay(listenKeyRefreshRate, self.keep_alive_listen_key, listenKey, params) + return listenKey + + async def keep_alive_listen_key(self, listenKey, params={}): + if listenKey is None: + return + request: dict = { + 'listenKey': listenKey, + } + try: + await self.spotPrivatePutUserDataStream(self.extend(request, params)) + listenKeyRefreshRate = self.safe_integer(self.options, 'listenKeyRefreshRate', 1200000) + self.delay(listenKeyRefreshRate, self.keep_alive_listen_key, listenKey, params) + except Exception as error: + url = self.urls['api']['ws']['spot'] + '?listenKey=' + listenKey + client = self.client(url) + self.options['listenKey'] = None + client.reject(error) + del self.clients[url] + + def handle_pong(self, client: Client, message): + client.lastPong = self.milliseconds() + return message + + def handle_subscription_status(self, client: Client, message): + # + # { + # "id": 0, + # "code": 0, + # "msg": "spot@public.increase.depth.v3.api@BTCUSDT" + # } + # Set the default to an empty string if the message is empty during the test. + msg = self.safe_string(message, 'msg', '') + if msg == 'PONG': + self.handle_pong(client, message) + elif msg.find('@') > -1: + parts = msg.split('@') + channel = self.safe_string(parts, 1) + methods: dict = { + 'public.increase.depth.v3.api': self.handle_order_book_subscription, + 'public.aggre.depth.v3.api.pb': self.handle_order_book_subscription, + } + method = self.safe_value(methods, channel) + if method is not None: + method(client, message) + + def handle_protobuf_message(self, client: Client, message): + # protobuf message decoded + # { + # "channel":"spot@public.kline.v3.api.pb@BTCUSDT@Min1", + # "symbol":"BTCUSDT", + # "symbolId":"2fb942154ef44a4ab2ef98c8afb6a4a7", + # "createTime":"1754737941062", + # "publicSpotKline":{ + # "interval":"Min1", + # "windowStart":"1754737920", + # "openingPrice":"117317.31", + # "closingPrice":"117325.26", + # "highestPrice":"117341", + # "lowestPrice":"117317.3", + # "volume":"3.12599854", + # "amount":"366804.43", + # "windowEnd":"1754737980" + # } + # } + channel = self.safe_string(message, 'channel') + channelParts = channel.split('@') + channelId = self.safe_string(channelParts, 1) + if channelId == 'public.kline.v3.api.pb': + self.handle_ohlcv(client, message) + elif channelId == 'public.aggre.deals.v3.api.pb': + self.handle_trades(client, message) + elif channelId == 'public.aggre.bookTicker.v3.api.pb': + self.handle_ticker(client, message) + elif channelId == 'public.aggre.depth.v3.api.pb': + self.handle_order_book(client, message) + elif channelId == 'private.account.v3.api.pb': + self.handle_balance(client, message) + elif channelId == 'private.deals.v3.api.pb': + self.handle_my_trade(client, message) + elif channelId == 'private.orders.v3.api.pb': + self.handle_order(client, message) + return True + + def handle_message(self, client: Client, message): + if isinstance(message, str): + if message == 'Invalid listen key': + error = AuthenticationError(self.id + ' invalid listen key') + client.reject(error) + return + if self.is_binary_message(message): + message = self.decode_proto_msg(message) + self.handle_protobuf_message(client, message) + return + if 'msg' in message: + self.handle_subscription_status(client, message) + return + c = self.safe_string(message, 'c') + channel = None + if c is None: + channel = self.safe_string(message, 'channel') + else: + parts = c.split('@') + channel = self.safe_string(parts, 1) + methods: dict = { + 'public.deals.v3.api': self.handle_trades, + 'push.deal': self.handle_trades, + 'public.kline.v3.api': self.handle_ohlcv, + 'push.kline': self.handle_ohlcv, + 'public.bookTicker.v3.api': self.handle_ticker, + 'public.miniTicker.v3.api': self.handle_ticker, + 'public.miniTickers.v3.api': self.handle_tickers, + 'push.ticker': self.handle_ticker, + 'push.tickers': self.handle_tickers, + 'public.increase.depth.v3.api': self.handle_order_book, + 'push.depth': self.handle_order_book, + 'private.orders.v3.api': self.handle_order, + 'push.personal.order': self.handle_order, + 'private.account.v3.api': self.handle_balance, + 'push.personal.asset': self.handle_balance, + 'private.deals.v3.api': self.handle_my_trade, + 'push.personal.order.deal': self.handle_my_trade, + 'pong': self.handle_pong, + } + if channel in methods: + method = methods[channel] + method(client, message) + + def ping(self, client: Client): + return {'method': 'ping'} diff --git a/ccxt/pro/modetrade.py b/ccxt/pro/modetrade.py new file mode 100644 index 0000000..50bc49b --- /dev/null +++ b/ccxt/pro/modetrade.py @@ -0,0 +1,1271 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolById, ArrayCacheBySymbolBySide, ArrayCacheByTimestamp +from ccxt.base.types import Any, Balances, Bool, Int, Order, OrderBook, Position, Str, Strings, Ticker, Tickers, Trade +from ccxt.async_support.base.ws.client import Client +from typing import List +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import NotSupported +from ccxt.base.precise import Precise + + +class modetrade(ccxt.async_support.modetrade): + + def describe(self) -> Any: + return self.deep_extend(super(modetrade, self).describe(), { + 'has': { + 'ws': True, + 'watchBalance': True, + 'watchMyTrades': True, + 'watchOHLCV': True, + 'watchOrderBook': True, + 'watchOrders': True, + 'watchTicker': True, + 'watchTickers': True, + 'watchBidsAsks': True, + 'watchTrades': True, + 'watchTradesForSymbols': False, + 'watchPositions': True, + }, + 'urls': { + 'api': { + 'ws': { + 'public': 'wss://ws-evm.orderly.org/ws/stream', + 'private': 'wss://ws-private-evm.orderly.org/v2/ws/private/stream', + }, + }, + 'test': { + 'ws': { + 'public': 'wss://testnet-ws-evm.orderly.org/ws/stream', + 'private': 'wss://testnet-ws-private-evm.orderly.org/v2/ws/private/stream', + }, + }, + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + 'accountId': True, + }, + 'options': { + 'tradesLimit': 1000, + 'ordersLimit': 1000, + 'requestId': {}, + 'watchPositions': { + 'fetchPositionsSnapshot': True, # or False + 'awaitPositionsSnapshot': True, # whether to wait for the positions snapshot before providing updates + }, + }, + 'streaming': { + 'ping': self.ping, + 'keepAlive': 10000, + }, + 'exceptions': { + 'ws': { + 'exact': { + 'Auth is needed.': AuthenticationError, + }, + }, + }, + }) + + def request_id(self, url): + options = self.safe_dict(self.options, 'requestId', {}) + previousValue = self.safe_integer(options, url, 0) + newValue = self.sum(previousValue, 1) + self.options['requestId'][url] = newValue + return newValue + + async def watch_public(self, messageHash, message): + # the default id + id = 'OqdphuyCtYWxwzhxyLLjOWNdFP7sQt8RPWzmb5xY' + if self.accountId is not None and self.accountId != '': + id = self.accountId + url = self.urls['api']['ws']['public'] + '/' + id + requestId = self.request_id(url) + subscribe: dict = { + 'id': requestId, + } + request = self.extend(subscribe, message) + return await self.watch(url, messageHash, request, messageHash, subscribe) + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + + https://orderly.network/docs/build-on-evm/evm-api/websocket-api/public/orderbook + + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return. + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + name = 'orderbook' + market = self.market(symbol) + topic = market['id'] + '@' + name + request: dict = { + 'event': 'subscribe', + 'topic': topic, + } + message = self.extend(request, params) + orderbook = await self.watch_public(topic, message) + return orderbook.limit() + + def handle_order_book(self, client: Client, message): + # + # { + # "topic": "PERP_BTC_USDC@orderbook", + # "ts": 1650121915308, + # "data": { + # "symbol": "PERP_BTC_USDC", + # "bids": [ + # [ + # 0.30891, + # 2469.98 + # ] + # ], + # "asks": [ + # [ + # 0.31075, + # 2379.63 + # ] + # ] + # } + # } + # + data = self.safe_dict(message, 'data', {}) + marketId = self.safe_string(data, 'symbol') + market = self.safe_market(marketId) + symbol = market['symbol'] + topic = self.safe_string(message, 'topic') + if not (symbol in self.orderbooks): + self.orderbooks[symbol] = self.order_book() + orderbook = self.orderbooks[symbol] + timestamp = self.safe_integer(message, 'ts') + snapshot = self.parse_order_book(data, symbol, timestamp, 'bids', 'asks') + orderbook.reset(snapshot) + client.resolve(orderbook, topic) + + async def watch_ticker(self, symbol: str, params={}) -> Ticker: + """ + + https://orderly.network/docs/build-on-evm/evm-api/websocket-api/public/24-hour-ticker + + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + name = 'ticker' + market = self.market(symbol) + symbol = market['symbol'] + topic = market['id'] + '@' + name + request: dict = { + 'event': 'subscribe', + 'topic': topic, + } + message = self.extend(request, params) + return await self.watch_public(topic, message) + + def parse_ws_ticker(self, ticker, market=None): + # + # { + # "symbol": "PERP_BTC_USDC", + # "open": 19441.5, + # "close": 20147.07, + # "high": 20761.87, + # "low": 19320.54, + # "volume": 2481.103, + # "amount": 50037935.0286, + # "count": 3689 + # } + # + return self.safe_ticker({ + 'symbol': self.safe_symbol(None, market), + 'timestamp': None, + 'datetime': None, + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': None, + 'bidVolume': None, + 'ask': None, + 'askVolume': None, + 'vwap': None, + 'open': self.safe_string(ticker, 'open'), + 'close': self.safe_string(ticker, 'close'), + 'last': None, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': self.safe_string(ticker, 'volume'), + 'quoteVolume': self.safe_string(ticker, 'amount'), + 'info': ticker, + }, market) + + def handle_ticker(self, client: Client, message): + # + # { + # "topic": "PERP_BTC_USDC@ticker", + # "ts": 1657120017000, + # "data": { + # "symbol": "PERP_BTC_USDC", + # "open": 19441.5, + # "close": 20147.07, + # "high": 20761.87, + # "low": 19320.54, + # "volume": 2481.103, + # "amount": 50037935.0286, + # "count": 3689 + # } + # } + # + data = self.safe_dict(message, 'data', {}) + topic = self.safe_string(message, 'topic') + marketId = self.safe_string(data, 'symbol') + market = self.safe_market(marketId) + timestamp = self.safe_integer(message, 'ts') + data['date'] = timestamp + ticker = self.parse_ws_ticker(data, market) + ticker['symbol'] = market['symbol'] + self.tickers[market['symbol']] = ticker + client.resolve(ticker, topic) + return message + + async def watch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + + https://orderly.network/docs/build-on-evm/evm-api/websocket-api/public/24-hour-tickers + + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list + :param str[] symbols: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + name = 'tickers' + topic = name + request: dict = { + 'event': 'subscribe', + 'topic': topic, + } + message = self.extend(request, params) + tickers = await self.watch_public(topic, message) + return self.filter_by_array(tickers, 'symbol', symbols) + + def handle_tickers(self, client: Client, message): + # + # { + # "topic":"tickers", + # "ts":1618820615000, + # "data":[ + # { + # "symbol":"PERP_NEAR_USDC", + # "open":16.297, + # "close":17.183, + # "high":24.707, + # "low":11.997, + # "volume":0, + # "amount":0, + # "count":0 + # }, + # ... + # ] + # } + # + topic = self.safe_string(message, 'topic') + data = self.safe_list(message, 'data', []) + timestamp = self.safe_integer(message, 'ts') + result = [] + for i in range(0, len(data)): + marketId = self.safe_string(data[i], 'symbol') + market = self.safe_market(marketId) + ticker = self.parse_ws_ticker(self.extend(data[i], {'date': timestamp}), market) + self.tickers[market['symbol']] = ticker + result.append(ticker) + client.resolve(result, topic) + + async def watch_bids_asks(self, symbols: Strings = None, params={}) -> Tickers: + """ + + https://orderly.network/docs/build-on-evm/evm-api/websocket-api/public/bbos + + watches best bid & ask for symbols + :param str[] symbols: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + name = 'bbos' + topic = name + request: dict = { + 'event': 'subscribe', + 'topic': topic, + } + message = self.extend(request, params) + tickers = await self.watch_public(topic, message) + return self.filter_by_array(tickers, 'symbol', symbols) + + def handle_bid_ask(self, client: Client, message): + # + # { + # "topic": "bbos", + # "ts": 1726212495000, + # "data": [ + # { + # "symbol": "PERP_BTC_USDC", + # "ask": 0.16570, + # "askSize": 4224, + # "bid": 0.16553, + # "bidSize": 6645 + # } + # ] + # } + # + topic = self.safe_string(message, 'topic') + data = self.safe_list(message, 'data', []) + timestamp = self.safe_integer(message, 'ts') + result = [] + for i in range(0, len(data)): + ticker = self.parse_ws_bid_ask(self.extend(data[i], {'ts': timestamp})) + self.tickers[ticker['symbol']] = ticker + result.append(ticker) + client.resolve(result, topic) + + def parse_ws_bid_ask(self, ticker, market=None): + marketId = self.safe_string(ticker, 'symbol') + market = self.safe_market(marketId, market) + symbol = self.safe_string(market, 'symbol') + timestamp = self.safe_integer(ticker, 'ts') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'ask': self.safe_string(ticker, 'ask'), + 'askVolume': self.safe_string(ticker, 'askSize'), + 'bid': self.safe_string(ticker, 'bid'), + 'bidVolume': self.safe_string(ticker, 'bidSize'), + 'info': ticker, + }, market) + + async def watch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + watches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://orderly.network/docs/build-on-evm/evm-api/websocket-api/public/k-line + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + if (timeframe != '1m') and (timeframe != '5m') and (timeframe != '15m') and (timeframe != '30m') and (timeframe != '1h') and (timeframe != '1d') and (timeframe != '1w') and (timeframe != '1M'): + raise NotSupported(self.id + ' watchOHLCV timeframe argument must be 1m, 5m, 15m, 30m, 1h, 1d, 1w, 1M') + market = self.market(symbol) + interval = self.safe_string(self.timeframes, timeframe, timeframe) + name = 'kline' + topic = market['id'] + '@' + name + '_' + interval + request: dict = { + 'event': 'subscribe', + 'topic': topic, + } + message = self.extend(request, params) + ohlcv = await self.watch_public(topic, message) + if self.newUpdates: + limit = ohlcv.getLimit(market['symbol'], limit) + return self.filter_by_since_limit(ohlcv, since, limit, 0, True) + + def handle_ohlcv(self, client: Client, message): + # + # { + # "topic":"PERP_BTC_USDC@kline_1m", + # "ts":1618822432146, + # "data":{ + # "symbol":"PERP_BTC_USDC", + # "type":"1m", + # "open":56948.97, + # "close":56891.76, + # "high":56948.97, + # "low":56889.06, + # "volume":44.00947568, + # "amount":2504584.9, + # "startTime":1618822380000, + # "endTime":1618822440000 + # } + # } + # + data = self.safe_dict(message, 'data', {}) + topic = self.safe_string(message, 'topic') + marketId = self.safe_string(data, 'symbol') + market = self.safe_market(marketId) + symbol = market['symbol'] + interval = self.safe_string(data, 'type') + timeframe = self.find_timeframe(interval) + parsed = [ + self.safe_integer(data, 'startTime'), + self.safe_number(data, 'open'), + self.safe_number(data, 'high'), + self.safe_number(data, 'low'), + self.safe_number(data, 'close'), + self.safe_number(data, 'volume'), + ] + self.ohlcvs[symbol] = self.safe_value(self.ohlcvs, symbol, {}) + stored = self.safe_value(self.ohlcvs[symbol], timeframe) + if stored is None: + limit = self.safe_integer(self.options, 'OHLCVLimit', 1000) + stored = ArrayCacheByTimestamp(limit) + self.ohlcvs[symbol][timeframe] = stored + ohlcvCache = self.ohlcvs[symbol][timeframe] + ohlcvCache.append(parsed) + client.resolve(ohlcvCache, topic) + + async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + watches information on multiple trades made in a market + + https://orderly.network/docs/build-on-evm/evm-api/websocket-api/public/trade + + :param str symbol: unified market symbol of the market trades were made in + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trade structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + topic = market['id'] + '@trade' + request: dict = { + 'event': 'subscribe', + 'topic': topic, + } + message = self.extend(request, params) + trades = await self.watch_public(topic, message) + if self.newUpdates: + limit = trades.getLimit(market['symbol'], limit) + return self.filter_by_symbol_since_limit(trades, symbol, since, limit, True) + + def handle_trade(self, client: Client, message): + # + # { + # "topic":"PERP_ADA_USDC@trade", + # "ts":1618820361552, + # "data":{ + # "symbol":"PERP_ADA_USDC", + # "price":1.27988, + # "size":300, + # "side":"BUY", + # } + # } + # + topic = self.safe_string(message, 'topic') + timestamp = self.safe_integer(message, 'ts') + data = self.safe_dict(message, 'data', {}) + marketId = self.safe_string(data, 'symbol') + market = self.safe_market(marketId) + symbol = market['symbol'] + trade = self.parse_ws_trade(self.extend(data, {'timestamp': timestamp}), market) + if not (symbol in self.trades): + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + stored = ArrayCache(limit) + self.trades[symbol] = stored + trades = self.trades[symbol] + trades.append(trade) + self.trades[symbol] = trades + client.resolve(trades, topic) + + def parse_ws_trade(self, trade, market=None): + # + # { + # "symbol":"PERP_ADA_USDC", + # "timestamp":1618820361552, + # "price":1.27988, + # "size":300, + # "side":"BUY", + # } + # private stream + # { + # symbol: 'PERP_XRP_USDC', + # clientOrderId: '', + # orderId: 1167632251, + # type: 'MARKET', + # side: 'BUY', + # quantity: 20, + # price: 0, + # tradeId: '1715179456664012', + # executedPrice: 0.5276, + # executedQuantity: 20, + # fee: 0.006332, + # feeAsset: 'USDC', + # totalExecutedQuantity: 20, + # avgPrice: 0.5276, + # averageExecutedPrice: 0.5276, + # status: 'FILLED', + # reason: '', + # totalFee: 0.006332, + # visible: 0, + # visibleQuantity: 0, + # timestamp: 1715179456660, + # orderTag: 'CCXT', + # createdTime: 1715179456656, + # maker: False + # } + # + marketId = self.safe_string(trade, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + price = self.safe_string_2(trade, 'executedPrice', 'price') + amount = self.safe_string_2(trade, 'executedQuantity', 'size') + cost = Precise.string_mul(price, amount) + side = self.safe_string_lower(trade, 'side') + timestamp = self.safe_integer(trade, 'timestamp') + takerOrMaker = None + maker = self.safe_bool(trade, 'maker') + if maker is not None: + takerOrMaker = 'maker' if maker else 'taker' + fee = None + feeValue = self.safe_string(trade, 'fee') + if feeValue is not None: + fee = { + 'cost': feeValue, + 'currency': self.safe_currency_code(self.safe_string(trade, 'feeAsset')), + } + return self.safe_trade({ + 'id': self.safe_string(trade, 'tradeId'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'side': side, + 'price': price, + 'amount': amount, + 'cost': cost, + 'order': self.safe_string(trade, 'orderId'), + 'takerOrMaker': takerOrMaker, + 'type': self.safe_string_lower(trade, 'type'), + 'fee': fee, + 'info': trade, + }, market) + + def handle_auth(self, client: Client, message): + # + # { + # "event": "auth", + # "success": True, + # "ts": 1657463158812 + # } + # + messageHash = 'authenticated' + success = self.safe_value(message, 'success') + if success: + # client.resolve(message, messageHash) + future = self.safe_value(client.futures, 'authenticated') + future.resolve(True) + else: + error = AuthenticationError(self.json(message)) + client.reject(error, messageHash) + # allows further authentication attempts + if messageHash in client.subscriptions: + del client.subscriptions['authenticated'] + + async def authenticate(self, params={}): + self.check_required_credentials() + url = self.urls['api']['ws']['private'] + '/' + self.accountId + client = self.client(url) + messageHash = 'authenticated' + event = 'auth' + future = client.reusableFuture(messageHash) + authenticated = self.safe_value(client.subscriptions, messageHash) + if authenticated is None: + ts = str(self.nonce()) + auth = ts + secret = self.secret + if secret.find('ed25519:') >= 0: + parts = secret.split('ed25519:') + secret = parts[1] + signature = self.eddsa(self.encode(auth), self.base58_to_binary(secret), 'ed25519') + request: dict = { + 'event': event, + 'params': { + 'orderly_key': self.apiKey, + 'sign': signature, + 'timestamp': ts, + }, + } + message = self.extend(request, params) + self.watch(url, messageHash, message, messageHash) + return await future + + async def watch_private(self, messageHash, message, params={}): + await self.authenticate(params) + url = self.urls['api']['ws']['private'] + '/' + self.accountId + requestId = self.request_id(url) + subscribe: dict = { + 'id': requestId, + } + request = self.extend(subscribe, message) + return await self.watch(url, messageHash, request, messageHash, subscribe) + + async def watch_private_multiple(self, messageHashes, message, params={}): + await self.authenticate(params) + url = self.urls['api']['ws']['private'] + '/' + self.accountId + requestId = self.request_id(url) + subscribe: dict = { + 'id': requestId, + } + request = self.extend(subscribe, message) + return await self.watch_multiple(url, messageHashes, request, messageHashes, subscribe) + + async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + watches information on multiple orders made by the user + + https://orderly.network/docs/build-on-evm/evm-api/websocket-api/private/execution-report + https://orderly.network/docs/build-on-evm/evm-api/websocket-api/private/algo-execution-report + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.trigger]: True if trigger order + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + trigger = self.safe_bool_2(params, 'stop', 'trigger', False) + topic = 'algoexecutionreport' if (trigger) else 'executionreport' + params = self.omit(params, ['stop', 'trigger']) + messageHash = topic + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + messageHash += ':' + symbol + request: dict = { + 'event': 'subscribe', + 'topic': topic, + } + message = self.extend(request, params) + orders = await self.watch_private(messageHash, message) + if self.newUpdates: + limit = orders.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(orders, symbol, since, limit, True) + + async def watch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + watches information on multiple trades made by the user + + https://orderly.network/docs/build-on-evm/evm-api/websocket-api/private/execution-report + https://orderly.network/docs/build-on-evm/evm-api/websocket-api/private/algo-execution-report + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.trigger]: True if trigger order + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + trigger = self.safe_bool_2(params, 'stop', 'trigger', False) + topic = 'algoexecutionreport' if (trigger) else 'executionreport' + params = self.omit(params, 'stop') + messageHash = 'myTrades' + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + messageHash += ':' + symbol + request: dict = { + 'event': 'subscribe', + 'topic': topic, + } + message = self.extend(request, params) + orders = await self.watch_private(messageHash, message) + if self.newUpdates: + limit = orders.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(orders, symbol, since, limit, True) + + def parse_ws_order(self, order, market=None): + # + # { + # "symbol": "PERP_BTC_USDT", + # "clientOrderId": 0, + # "orderId": 52952826, + # "type": "LIMIT", + # "side": "SELL", + # "quantity": 0.01, + # "price": 22000, + # "tradeId": 0, + # "executedPrice": 0, + # "executedQuantity": 0, + # "fee": 0, + # "feeAsset": "USDT", + # "totalExecutedQuantity": 0, + # "status": "NEW", + # "reason": '', + # "orderTag": "default", + # "totalFee": 0, + # "visible": 0.01, + # "timestamp": 1657515556799, + # "reduceOnly": False, + # "maker": False + # } + # algo order + # { + # "symbol":"PERP_MATIC_USDC", + # "rootAlgoOrderId":123, + # "parentAlgoOrderId":123, + # "algoOrderId":123, + # "orderTag":"some tags", + # "algoType": "STOP", + # "clientOrderId":"client_id", + # "type":"LIMIT", + # "side":"BUY", + # "quantity":7029.0, + # "price":0.7699, + # "tradeId":0, + # "triggerTradePrice":0, + # "triggerTime":1234567, + # "triggered": False, + # "activated": False, + # "executedPrice":0.0, + # "executedQuantity":0.0, + # "fee":0.0, + # "feeAsset":"USDC", + # "totalExecutedQuantity":0.0, + # "averageExecutedQuantity":0.0, + # "avgPrice":0, + # "triggerPrice":0.0, + # "triggerPriceType":"STOP", + # "isActivated": False, + # "status":"NEW", + # "rootAlgoStatus": "FILLED", + # "algoStatus": "FILLED", + # "reason":"", + # "totalFee":0.0, + # "visible": 7029.0, + # "visibleQuantity":7029.0, + # "timestamp":1704679472448, + # "maker":false, + # "isMaker":false, + # "createdTime":1704679472448 + # } + # + orderId = self.safe_string(order, 'orderId') + marketId = self.safe_string(order, 'symbol') + market = self.market(marketId) + symbol = market['symbol'] + timestamp = self.safe_integer(order, 'timestamp') + fee = { + 'cost': self.safe_string(order, 'totalFee'), + 'currency': self.safe_string(order, 'feeAsset'), + } + priceString = self.safe_string(order, 'price') + price = self.safe_number(order, 'price') + avgPrice = self.safe_number(order, 'avgPrice') + if Precise.string_eq(priceString, '0') and (avgPrice is not None): + price = avgPrice + amount = self.safe_string(order, 'quantity') + side = self.safe_string_lower(order, 'side') + type = self.safe_string_lower(order, 'type') + filled = self.safe_number(order, 'totalExecutedQuantity') + totalExecQuantity = self.safe_string(order, 'totalExecutedQuantity') + remaining = amount + if Precise.string_ge(amount, totalExecQuantity): + remaining = Precise.string_sub(remaining, totalExecQuantity) + rawStatus = self.safe_string(order, 'status') + status = self.parse_order_status(rawStatus) + trades = None + clientOrderId = self.safe_string(order, 'clientOrderId') + triggerPrice = self.safe_number(order, 'triggerPrice') + return self.safe_order({ + 'info': order, + 'symbol': symbol, + 'id': orderId, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': timestamp, + 'type': type, + 'timeInForce': None, + 'postOnly': None, + 'side': side, + 'price': price, + 'stopPrice': triggerPrice, + 'triggerPrice': triggerPrice, + 'amount': amount, + 'cost': None, + 'average': None, + 'filled': filled, + 'remaining': remaining, + 'status': status, + 'fee': fee, + 'trades': trades, + }) + + def handle_order_update(self, client: Client, message): + # + # { + # "topic": "executionreport", + # "ts": 1657515556799, + # "data": { + # "symbol": "PERP_BTC_USDT", + # "clientOrderId": 0, + # "orderId": 52952826, + # "type": "LIMIT", + # "side": "SELL", + # "quantity": 0.01, + # "price": 22000, + # "tradeId": 0, + # "executedPrice": 0, + # "executedQuantity": 0, + # "fee": 0, + # "feeAsset": "USDT", + # "totalExecutedQuantity": 0, + # "status": "NEW", + # "reason": '', + # "orderTag": "default", + # "totalFee": 0, + # "visible": 0.01, + # "timestamp": 1657515556799, + # "maker": False + # } + # } + # + topic = self.safe_string(message, 'topic') + data = self.safe_value(message, 'data') + if isinstance(data, list): + # algoexecutionreport + for i in range(0, len(data)): + order = data[i] + tradeId = self.omit_zero(self.safe_string(data, 'tradeId')) + if tradeId is not None: + self.handle_my_trade(client, order) + self.handle_order(client, order, topic) + else: + # executionreport + tradeId = self.omit_zero(self.safe_string(data, 'tradeId')) + if tradeId is not None: + self.handle_my_trade(client, data) + self.handle_order(client, data, topic) + + def handle_order(self, client: Client, message, topic): + parsed = self.parse_ws_order(message) + symbol = self.safe_string(parsed, 'symbol') + orderId = self.safe_string(parsed, 'id') + if symbol is not None: + if self.orders is None: + limit = self.safe_integer(self.options, 'ordersLimit', 1000) + self.orders = ArrayCacheBySymbolById(limit) + cachedOrders = self.orders + orders = self.safe_dict(cachedOrders.hashmap, symbol, {}) + order = self.safe_dict(orders, orderId) + if order is not None: + fee = self.safe_value(order, 'fee') + if fee is not None: + parsed['fee'] = fee + fees = self.safe_list(order, 'fees') + if fees is not None: + parsed['fees'] = fees + parsed['trades'] = self.safe_list(order, 'trades') + parsed['timestamp'] = self.safe_integer(order, 'timestamp') + parsed['datetime'] = self.safe_string(order, 'datetime') + cachedOrders.append(parsed) + client.resolve(self.orders, topic) + messageHashSymbol = topic + ':' + symbol + client.resolve(self.orders, messageHashSymbol) + + def handle_my_trade(self, client: Client, message): + # + # { + # symbol: 'PERP_XRP_USDC', + # clientOrderId: '', + # orderId: 1167632251, + # type: 'MARKET', + # side: 'BUY', + # quantity: 20, + # price: 0, + # tradeId: '1715179456664012', + # executedPrice: 0.5276, + # executedQuantity: 20, + # fee: 0.006332, + # feeAsset: 'USDC', + # totalExecutedQuantity: 20, + # avgPrice: 0.5276, + # averageExecutedPrice: 0.5276, + # status: 'FILLED', + # reason: '', + # totalFee: 0.006332, + # visible: 0, + # visibleQuantity: 0, + # timestamp: 1715179456660, + # orderTag: 'CCXT', + # createdTime: 1715179456656, + # maker: False + # } + # + messageHash = 'myTrades' + marketId = self.safe_string(message, 'symbol') + market = self.safe_market(marketId) + symbol = market['symbol'] + trade = self.parse_ws_trade(message, market) + trades = self.myTrades + if trades is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + trades = ArrayCacheBySymbolById(limit) + self.myTrades = trades + trades.append(trade) + client.resolve(trades, messageHash) + symbolSpecificMessageHash = messageHash + ':' + symbol + client.resolve(trades, symbolSpecificMessageHash) + + async def watch_positions(self, symbols: Strings = None, since: Int = None, limit: Int = None, params={}) -> List[Position]: + """ + + https://orderly.network/docs/build-on-evm/evm-api/websocket-api/private/position-push + + watch all open positions + :param str[] [symbols]: list of unified market symbols + @param since timestamp in ms of the earliest position to fetch + @param limit the maximum number of positions to fetch + :param dict params: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + await self.load_markets() + messageHashes = [] + symbols = self.market_symbols(symbols) + if not self.is_empty(symbols): + for i in range(0, len(symbols)): + symbol = symbols[i] + messageHashes.append('positions::' + symbol) + else: + messageHashes.append('positions') + url = self.urls['api']['ws']['private'] + '/' + self.accountId + client = self.client(url) + self.set_positions_cache(client, symbols) + fetchPositionsSnapshot = self.handle_option('watchPositions', 'fetchPositionsSnapshot', True) + awaitPositionsSnapshot = self.handle_option('watchPositions', 'awaitPositionsSnapshot', True) + if fetchPositionsSnapshot and awaitPositionsSnapshot and self.positions is None: + snapshot = await client.future('fetchPositionsSnapshot') + return self.filter_by_symbols_since_limit(snapshot, symbols, since, limit, True) + request: dict = { + 'event': 'subscribe', + 'topic': 'position', + } + newPositions = await self.watch_private_multiple(messageHashes, request, params) + if self.newUpdates: + return newPositions + return self.filter_by_symbols_since_limit(self.positions, symbols, since, limit, True) + + def set_positions_cache(self, client: Client, type, symbols: Strings = None): + fetchPositionsSnapshot = self.handle_option('watchPositions', 'fetchPositionsSnapshot', False) + if fetchPositionsSnapshot: + messageHash = 'fetchPositionsSnapshot' + if not (messageHash in client.futures): + client.future(messageHash) + self.spawn(self.load_positions_snapshot, client, messageHash) + else: + self.positions = ArrayCacheBySymbolBySide() + + async def load_positions_snapshot(self, client, messageHash): + positions = await self.fetch_positions() + self.positions = ArrayCacheBySymbolBySide() + cache = self.positions + for i in range(0, len(positions)): + position = positions[i] + contracts = self.safe_string(position, 'contracts', '0') + if Precise.string_gt(contracts, '0'): + cache.append(position) + # don't remove the future from the .futures cache + future = client.futures[messageHash] + future.resolve(cache) + client.resolve(cache, 'positions') + + def handle_positions(self, client, message): + # + # { + # "topic":"position", + # "ts":1705292345255, + # "data":{ + # "positions":[ + # { + # "symbol":"PERP_ETH_USDC", + # "positionQty":3.1408, + # "costPosition":5706.51952, + # "lastSumUnitaryFunding":0.804, + # "sumUnitaryFundingVersion":0, + # "pendingLongQty":0.0, + # "pendingShortQty":-1.0, + # "settlePrice":1816.9, + # "averageOpenPrice":1804.51490427, + # "unsettledPnl":-2.79856, + # "pnl24H":-338.90179488, + # "fee24H":4.242423, + # "markPrice":1816.2, + # "estLiqPrice":0.0, + # "version":179967, + # "imrwithOrders":0.1, + # "mmrwithOrders":0.05, + # "mmr":0.05, + # "imr":0.1, + # "timestamp":1685154032762 + # } + # ] + # } + # } + # + data = self.safe_dict(message, 'data', {}) + rawPositions = self.safe_list(data, 'positions', []) + if self.positions is None: + self.positions = ArrayCacheBySymbolBySide() + cache = self.positions + newPositions = [] + for i in range(0, len(rawPositions)): + rawPosition = rawPositions[i] + marketId = self.safe_string(rawPosition, 'symbol') + market = self.safe_market(marketId) + position = self.parse_ws_position(rawPosition, market) + newPositions.append(position) + cache.append(position) + messageHash = 'positions::' + market['symbol'] + client.resolve(position, messageHash) + client.resolve(newPositions, 'positions') + + def parse_ws_position(self, position, market=None): + # + # { + # "symbol":"PERP_ETH_USDC", + # "positionQty":3.1408, + # "costPosition":5706.51952, + # "lastSumUnitaryFunding":0.804, + # "sumUnitaryFundingVersion":0, + # "pendingLongQty":0.0, + # "pendingShortQty":-1.0, + # "settlePrice":1816.9, + # "averageOpenPrice":1804.51490427, + # "unsettledPnl":-2.79856, + # "pnl24H":-338.90179488, + # "fee24H":4.242423, + # "markPrice":1816.2, + # "estLiqPrice":0.0, + # "version":179967, + # "imrwithOrders":0.1, + # "mmrwithOrders":0.05, + # "mmr":0.05, + # "imr":0.1, + # "timestamp":1685154032762 + # } + # + contract = self.safe_string(position, 'symbol') + market = self.safe_market(contract, market) + size = self.safe_string(position, 'positionQty') + side: Str = None + if Precise.string_gt(size, '0'): + side = 'long' + else: + side = 'short' + contractSize = self.safe_string(market, 'contractSize') + markPrice = self.safe_string(position, 'markPrice') + timestamp = self.safe_integer(position, 'timestamp') + entryPrice = self.safe_string(position, 'averageOpenPrice') + unrealisedPnl = self.safe_string(position, 'unsettledPnl') + size = Precise.string_abs(size) + notional = Precise.string_mul(size, markPrice) + return self.safe_position({ + 'info': position, + 'id': None, + 'symbol': self.safe_string(market, 'symbol'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastUpdateTimestamp': None, + 'initialMargin': None, + 'initialMarginPercentage': None, + 'maintenanceMargin': None, + 'maintenanceMarginPercentage': None, + 'entryPrice': self.parse_number(entryPrice), + 'notional': self.parse_number(notional), + 'leverage': None, + 'unrealizedPnl': self.parse_number(unrealisedPnl), + 'contracts': self.parse_number(size), + 'contractSize': self.parse_number(contractSize), + 'marginRatio': None, + 'liquidationPrice': self.safe_number(position, 'estLiqPrice'), + 'markPrice': self.parse_number(markPrice), + 'lastPrice': None, + 'collateral': None, + 'marginMode': 'cross', + 'marginType': None, + 'side': side, + 'percentage': None, + 'hedged': None, + 'stopLossPrice': None, + 'takeProfitPrice': None, + }) + + async def watch_balance(self, params={}) -> Balances: + """ + watch balance and get the amount of funds available for trading or funds locked in orders + + https://orderly.network/docs/build-on-evm/evm-api/websocket-api/private/balance + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + topic = 'balance' + messageHash = topic + request: dict = { + 'event': 'subscribe', + 'topic': topic, + } + message = self.extend(request, params) + return await self.watch_private(messageHash, message) + + def handle_balance(self, client, message): + # + # { + # "topic":"balance", + # "ts":1651836695254, + # "data":{ + # "balances":{ + # "USDC":{ + # "holding":5555815.47398272, + # "frozen":0, + # "interest":0, + # "pendingShortQty":0, + # "pendingExposure":0, + # "pendingLongQty":0, + # "pendingLongExposure":0, + # "version":894, + # "staked":51370692, + # "unbonding":0, + # "vault":0, + # "averageOpenPrice":0.00000574, + # "pnl24H":0, + # "fee24H":0.01914, + # "markPrice":0.31885 + # } + # } + # } + # } + # + data = self.safe_dict(message, 'data', {}) + balances = self.safe_dict(data, 'balances', {}) + keys = list(balances.keys()) + ts = self.safe_integer(message, 'ts') + self.balance['info'] = data + self.balance['timestamp'] = ts + self.balance['datetime'] = self.iso8601(ts) + for i in range(0, len(keys)): + key = keys[i] + value = balances[key] + code = self.safe_currency_code(key) + account = self.balance[code] if (code in self.balance) else self.account() + total = self.safe_string(value, 'holding') + used = self.safe_string(value, 'frozen') + account['total'] = total + account['used'] = used + account['free'] = Precise.string_sub(total, used) + self.balance[code] = account + self.balance = self.safe_balance(self.balance) + client.resolve(self.balance, 'balance') + + def handle_error_message(self, client: Client, message) -> Bool: + # + # {"id":"1","event":"subscribe","success":false,"ts":1710780997216,"errorMsg":"Auth is needed."} + # + if not ('success' in message): + return False + success = self.safe_bool(message, 'success') + if success: + return False + errorMessage = self.safe_string(message, 'errorMsg') + try: + if errorMessage is not None: + feedback = self.id + ' ' + self.json(message) + self.throw_exactly_matched_exception(self.exceptions['exact'], errorMessage, feedback) + return False + except Exception as error: + if isinstance(error, AuthenticationError): + messageHash = 'authenticated' + client.reject(error, messageHash) + if messageHash in client.subscriptions: + del client.subscriptions[messageHash] + else: + client.reject(error) + return True + + def handle_message(self, client: Client, message): + if self.handle_error_message(client, message): + return + methods: dict = { + 'ping': self.handle_ping, + 'pong': self.handle_pong, + 'subscribe': self.handle_subscribe, + 'orderbook': self.handle_order_book, + 'ticker': self.handle_ticker, + 'tickers': self.handle_tickers, + 'kline': self.handle_ohlcv, + 'trade': self.handle_trade, + 'auth': self.handle_auth, + 'executionreport': self.handle_order_update, + 'algoexecutionreport': self.handle_order_update, + 'position': self.handle_positions, + 'balance': self.handle_balance, + 'bbos': self.handle_bid_ask, + } + event = self.safe_string(message, 'event') + method = self.safe_value(methods, event) + if method is not None: + method(client, message) + return + topic = self.safe_string(message, 'topic') + if topic is not None: + method = self.safe_value(methods, topic) + if method is not None: + method(client, message) + return + splitTopic = topic.split('@') + splitLength = len(splitTopic) + if splitLength == 2: + name = self.safe_string(splitTopic, 1) + method = self.safe_value(methods, name) + if method is not None: + method(client, message) + return + splitName = name.split('_') + splitNameLength = len(splitTopic) + if splitNameLength == 2: + method = self.safe_value(methods, self.safe_string(splitName, 0)) + if method is not None: + method(client, message) + + def ping(self, client: Client): + return {'event': 'ping'} + + def handle_ping(self, client: Client, message): + return {'event': 'pong'} + + def handle_pong(self, client: Client, message): + # + # {event: "pong", ts: 1614667590000} + # + client.lastPong = self.milliseconds() + return message + + def handle_subscribe(self, client: Client, message): + # + # { + # "id": "666888", + # "event": "subscribe", + # "success": True, + # "ts": 1657117712212 + # } + # + return message diff --git a/ccxt/pro/mt5.py b/ccxt/pro/mt5.py new file mode 100644 index 0000000..01b6ea3 --- /dev/null +++ b/ccxt/pro/mt5.py @@ -0,0 +1,213 @@ +# -*- coding: utf-8 -*- + +from ccxt.async_support.mt5 import mt5 as mt5Parent +from ccxt.base.errors import ExchangeError, ArgumentsRequired +from ccxt.async_support.base.ws.client import Client +import asyncio +from typing import Optional, Dict, Any, List + + +class mt5(mt5Parent): + def describe(self): + return self.deep_extend(super(mt5, self).describe(), { + 'has': { + 'ws': True, + 'watchBalance': True, + 'watchOrders': True, + 'watchTicker': True, + 'watchOHLCV': True, + }, + 'urls': { + 'api': { + 'ws': 'ws://43.167.188.220:5000', + }, + }, + 'options': { + 'tradesLimit': 1000, + 'ordersLimit': 1000, + 'OHLCVLimit': 1000, + }, + 'streaming': { + 'ping': True, + 'maxPingPongMisses': 2, + }, + }) + + async def watch_balance(self, params={}): + """ + 监听余额变化 + """ + if not hasattr(self, 'token') or not self.token: + await self.get_token() + + url = self.urls['api']['ws'] + '/OnOrderProfit' + request = { + 'id': self.token, + } + message_hash = 'balance' + return await self.watch(url, message_hash, request, params) + + async def watch_orders(self, symbol: Optional[str] = None, since: Optional[int] = None, limit: Optional[int] = None, params={}): + """ + 监听订单变化 + """ + if not hasattr(self, 'token') or not self.token: + await self.get_token() + + url = self.urls['api']['ws'] + '/OnOrderUpdate' + request = { + 'id': self.token, + } + message_hash = 'orders' + if symbol is not None: + symbol = self.symbol(symbol) + message_hash += ':' + symbol + + orders = await self.watch(url, message_hash, request, params) + if self.newUpdates: + limit = orders.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(orders, symbol, since, limit, True) + + async def watch_ticker(self, symbol: str, params={}): + """ + 监听行情变化 + """ + await self.load_markets() + market = self.market(symbol) + + if not hasattr(self, 'token') or not self.token: + await self.get_token() + + url = self.urls['api']['ws'] + '/OnQuote' + request = { + 'id': self.token, + 'symbol': market['id'], + } + message_hash = 'ticker:' + market['symbol'] + return await self.watch(url, message_hash, request, params) + + async def watch_ohlcv(self, symbol: str, timeframe='1m', since: Optional[int] = None, limit: Optional[int] = None, params={}): + """ + 监听K线数据 + """ + await self.load_markets() + market = self.market(symbol) + + if not hasattr(self, 'token') or not self.token: + await self.get_token() + + url = self.urls['api']['ws'] + '/OnOhlc' + request = { + 'id': self.token, + 'symbol': market['id'], + 'timeframe': self.timeframes[timeframe], + } + message_hash = 'ohlcv:' + market['symbol'] + ':' + timeframe + ohlcv = await self.watch(url, message_hash, request, params) + if self.newUpdates: + limit = ohlcv.getLimit(symbol, limit) + return self.filter_by_since_limit(ohlcv, since, limit, 0, True) + + def handle_balance(self, client: Client, message): + """处理余额更新""" + message_hash = 'balance' + balance = self.parse_balance(message) + self.balance = balance + client.resolve(balance, message_hash) + + def handle_orders(self, client: Client, message): + """处理订单更新""" + message_hash = 'orders' + + if self.orders is None: + self.orders = ArrayCacheBySymbolById() + + orders = self.parse_ws_orders(message) + for order in orders: + self.orders.append(order) + + client.resolve(self.orders, message_hash) + + def handle_ticker(self, client: Client, message): + """处理行情更新""" + ticker = self.parse_ws_ticker(message) + symbol = ticker['symbol'] + message_hash = 'ticker:' + symbol + self.tickers[symbol] = ticker + client.resolve(ticker, message_hash) + + def handle_ohlcv(self, client: Client, message): + """处理K线更新""" + symbol = self.safe_string(message, 'symbol') + timeframe = self.safe_string(message, 'timeframe', '1m') + message_hash = 'ohlcv:' + symbol + ':' + timeframe + + if self.ohlcvs is None: + self.ohlcvs = {} + if symbol not in self.ohlcvs: + self.ohlcvs[symbol] = {} + + stored = self.ohlcvs[symbol][timeframe] + if stored is None: + limit = self.safe_integer(self.options, 'OHLCVLimit', 1000) + stored = ArrayCacheByTimestamp(limit) + self.ohlcvs[symbol][timeframe] = stored + + ohlcv = self.parse_ws_ohlcv(message) + stored.append(ohlcv) + client.resolve(stored, message_hash) + + def parse_ws_orders(self, message): + """解析WebSocket订单数据""" + orders = self.safe_value(message, 'data', []) + result = [] + for order_data in orders: + order = self.parse_ws_order(order_data) + result.append(order) + return result + + def parse_ws_order(self, order_data): + """解析单个WebSocket订单""" + return self.parse_order(order_data) + + def parse_ws_ticker(self, message): + """解析WebSocket行情数据""" + return self.parse_ticker(message) + + def parse_ws_ohlcv(self, message): + """解析WebSocket K线数据""" + return [ + self.parse8601(self.safe_string(message, 'time')), + self.safe_number(message, 'open'), + self.safe_number(message, 'high'), + self.safe_number(message, 'low'), + self.safe_number(message, 'close'), + self.safe_number(message, 'volume', 0), + ] + + def handle_message(self, client: Client, message): + """处理所有WebSocket消息""" + error_code = self.safe_string(message, 'errorCode') + if error_code is not None: + self.handle_error_message(client, message) + return + + # 根据消息类型路由处理 + message_type = self.safe_string(message, 'type') + + if message_type == 'OnOrderProfit': + self.handle_balance(client, message) + elif message_type == 'OnOrderUpdate': + self.handle_orders(client, message) + elif message_type == 'OnQuote': + self.handle_ticker(client, message) + elif message_type == 'OnOhlc': + self.handle_ohlcv(client, message) + elif 'OpenedOrders' in message: + self.handle_orders(client, message) + + def handle_error_message(self, client: Client, message): + """处理错误消息""" + error_code = self.safe_string(message, 'errorCode') + error_message = self.safe_string(message, 'message') + raise ExchangeError(f"MT5 WebSocket error {error_code}: {error_message}") \ No newline at end of file diff --git a/ccxt/pro/mt5_bak.py b/ccxt/pro/mt5_bak.py new file mode 100644 index 0000000..1e1589a --- /dev/null +++ b/ccxt/pro/mt5_bak.py @@ -0,0 +1,312 @@ +# -*- coding: utf-8 -*- + +from ccxt.async_support.mt5 import mt5 as mt5Parent +from ccxt.base.errors import ExchangeError, ArgumentsRequired +from ccxt.async_support.base.ws.client import Client +import asyncio +from typing import Optional, Dict, Any, List + + +class mt5(mt5Parent): + def describe(self): + return self.deep_extend(super(mt5, self).describe(), { + 'platinum': True, + 'hostname': '43.167.188.220:5000', + 'has': { + # 专业版特有功能 + 'watchPosition': True, + 'watchOrder': True, + 'watchLeverage': True, + 'watchMargin': True, + 'advancedAPI': True, + 'batchOrders': True, + 'modifyOrder': True, + 'closePosition': True, + 'setLeverage': True, + 'setMargin': True, + }, + 'urls': { + 'api': { + 'ws': 'ws://{hostname}', + }, + }, + 'options': { + 'defaultType': 'spot', + 'watchOrder': { + 'symbol': None, + 'orderId': None, + }, + 'watchPosition': { + 'symbol': None, + }, + }, + }) + + + + + # 专业版特有方法 + async def watch_order(self, params={}): + """ + 监听特定订单的更新(专业版特有) + """ + + if not self.token: + await self.get_token() + + try: + await self.load_markets() + except Exception as e: + print(f"错误: {e}") + + # await self.fetch_markets() + + # return + request: dict = { + 'id': self.token, + } + endpoint = self.api['wsEndpoint']['order'] + url = self.implode_hostname(self.urls['api']['ws']) + '/' + endpoint + url += '?' + self.urlencode(request) + return await self.watch(url,'order_position') + + def handle_message(self, client: Client, message): + # print(message) + errorCode = self.safe_string(message, 'errorCode') + if errorCode is not None: + self.handle_error_message(client, message) + return + + # 处理 MT5 特定的消息 + message_type = self.safe_string(message, 'type') + + + if message_type == 'OpenedOrders': + print("111111111111111") + self.handle_opened_orders(client, message) + elif message_type == self.api['wsEndpoint']['orderbook']: + self.handle_orderbook(client, message) + elif message_type == self.api['wsEndpoint']['quote']: + self.handle_ticker(client, message) + + def handle_opened_orders(self, client, message): + """ + 处理 OpenedOrders 消息 + """ + orders_data = self.safe_value(message, 'data', []) + message_id = self.safe_string(message, 'id') + timestamp = self.safe_integer(message, 'timestampUTC') + parsed_orders = [] + for order_data in orders_data: + # 解析每个订单 + order = self.parse_ws_order(order_data) + parsed_orders.append(order) + + # 分发到不同的消息流 + order_id = order['id'] + symbol = order['symbol'] + + # 更新特定订单 + client.resolve(order, 'order:' + order_id) + + # 更新符号特定的订单列表 + client.resolve([order], 'orders:' + symbol) + + print('-------------parsed_orders:',parsed_orders) + # 更新所有订单列表 + client.resolve(parsed_orders, 'orders') + + # 如果有请求ID,也解析到该请求 + if message_id: + client.resolve(parsed_orders, 'request:' + message_id) + + def handle_orderbook(self, client, message): + """处理深度数据""" + orderbook = self.parse_ws_order_book(message) + symbol = orderbook['symbol'] + message_hash = 'orderbook:' + symbol + client.resolve(orderbook, message_hash) + + def parse_ws_order(self, order, market=None): + """ + 解析 MT5 WebSocket 订单数据为 CCXT 标准格式 + """ + # 提取订单基本信息 + ticket = self.safe_integer(order, 'ticket') + symbol = self.safe_string(order, 'symbol') + order_type = self.safe_string(order, 'orderType') + deal_type = self.safe_string(order, 'dealType') + state = self.safe_string(order, 'state') + lots = self.safe_number(order, 'lots') + contract_size = self.safe_number(order, 'contractSize', 1.0) + open_price = self.safe_number(order, 'openPrice') + close_price = self.safe_number(order, 'closePrice') + profit = self.safe_number(order, 'profit') + print("ggeggegge========") + # 获取市场信息 + market = self.market(symbol) if market is None else market + + # 解析时间戳 + open_time_str = self.safe_string(order, 'openTime') + open_timestamp = self.safe_integer(order, 'openTimestampUTC') + + # 如果 openTimestampUTC 不存在,尝试解析 openTime 字符串 + if open_timestamp is None and open_time_str: + try: + # 解析格式: "2025-11-15T04:06:06.994" + open_timestamp = self.parse8601(open_time_str) + except: + open_timestamp = None + + # 解析订单状态 + status = self.parse_order_status(state) + + # 解析订单方向 + side = self.parse_order_side(order_type, deal_type) + + # 解析订单类型 + order_type_parsed = self.parse_order_type(order_type) + + # 计算数量 (lots * contractSize) + amount = lots * contract_size if (lots is not None and contract_size is not None) else None + + # 解析成交数量 + volume = self.safe_integer(order, 'volume', 0) + close_volume = self.safe_integer(order, 'closeVolume', 0) + + # 对于已成交订单,filled 应该是 volume + filled = volume + remaining = 0 # MT5 中订单要么完全成交,要么没有 + + # 解析止盈止损 + stop_loss = self.safe_number(order, 'stopLoss') + take_profit = self.safe_number(order, 'takeProfit') + + # 构建标准订单对象 + result = { + 'id': str(ticket), + 'clientOrderId': None, + 'datetime': self.iso8601(open_timestamp) if open_timestamp else None, + 'timestamp': open_timestamp, + 'lastTradeTimestamp': None, + 'status': status, + 'symbol': market['symbol'] if market else symbol, + 'type': order_type_parsed, + 'side': side, + 'price': open_price, + 'amount': amount, + 'filled': filled, + 'remaining': remaining, + 'cost': None, # 可以计算: amount * price + 'average': None, + 'fee': { + 'currency': market['quote'] if market else 'USD', + 'cost': self.safe_number(order, 'fee', 0), + }, + 'trades': None, + 'info': order, + } + + # 计算成本 + if amount is not None and open_price is not None: + result['cost'] = amount * open_price + + # 添加 MT5 特定字段 + result['stopLoss'] = stop_loss + result['takeProfit'] = take_profit + result['profit'] = profit + result['commission'] = self.safe_number(order, 'commission', 0) + result['swap'] = self.safe_number(order, 'swap', 0) + result['comment'] = self.safe_string(order, 'comment', '') + print("-----------result:",result) + return result + + def parse_order_status(self, status): + """ + 解析 MT5 订单状态 + """ + statuses = { + 'Filled': 'closed', # 已成交 + 'PartialFilled': 'open', # 部分成交 + 'Pending': 'open', # 挂单中 + 'Cancelled': 'canceled', # 已取消 + 'Rejected': 'rejected', # 已拒绝 + 'Expired': 'expired', # 已过期 + # 根据您的数据添加更多状态映射 + 'Active': 'open', + 'Closed': 'closed', + } + return self.safe_string(statuses, status, status.lower() if status else 'unknown') + + def parse_order_side(self, order_type, deal_type): + """ + 解析订单方向 + """ + # 优先使用 deal_type,因为它更准确 + if deal_type: + if 'Buy' in deal_type: + return 'buy' + elif 'Sell' in deal_type: + return 'sell' + + # 其次使用 order_type + if order_type: + if order_type == 'Buy': + return 'buy' + elif order_type == 'Sell': + return 'sell' + elif 'Buy' in order_type: + return 'buy' + elif 'Sell' in order_type: + return 'sell' + + return 'unknown' + + def parse_order_type(self, order_type): + """ + 解析订单类型 + """ + if not order_type: + return 'market' # 默认类型 + + order_type_lower = order_type.lower() + + if 'limit' in order_type_lower: + return 'limit' + elif 'stop' in order_type_lower: + return 'stop' + elif 'market' in order_type_lower: + return 'market' + else: + # 根据 dealType 判断 + return 'market' # 默认为市价单 + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + """签名请求 URL 构建""" + endpoint = '/' + self.implode_params(path, params) + url = self.implode_hostname(self.urls['api'][api]) + endpoint + headers = headers if (headers is not None) else {} + + # 对于 GET 请求,将参数添加到查询字符串 + if method == 'GET' and params: + # 特殊处理数组参数 + query_params = {} + for key, value in params.items(): + if isinstance(value, list): + # 对于数组参数,可能需要特殊编码 + query_params[key] = ','.join(value) + else: + query_params[key] = value + + url += '?' + self.urlencode(query_params) + elif method == 'GET' and params: + url += '?' + self.urlencode(params) + + # print(f"🔧 Debug: Final URL: {url}") + + return { + 'url': url, + 'method': method, + 'body': body, + 'headers': headers + } diff --git a/ccxt/pro/myokx.py b/ccxt/pro/myokx.py new file mode 100644 index 0000000..ebc6248 --- /dev/null +++ b/ccxt/pro/myokx.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.pro.okx import okx +from ccxt.base.types import Any + + +class myokx(okx): + + def describe(self) -> Any: + return self.deep_extend(super(myokx, self).describe(), { + 'id': 'myokx', + 'name': 'MyOKX(EEA)', + 'hostname': 'eea.okx.com', + 'urls': { + 'api': { + 'rest': 'https://{hostname}', + 'ws': 'wss://wseea.okx.com:8443/ws/v5', + }, + 'www': 'https://my.okx.com', + 'doc': 'https://my.okx.com/docs-v5/en/#overview', + 'fees': 'https://my.okx.com/pages/products/fees.html', + 'referral': { + 'url': 'https://www.my.okx.com/join/CCXT2023', + 'discount': 0.2, + }, + 'test': { + 'ws': 'wss://wseeapap.okx.com:8443/ws/v5', + }, + }, + 'has': { + 'swap': False, + 'future': False, + 'option': False, + }, + }) diff --git a/ccxt/pro/ndax.py b/ccxt/pro/ndax.py new file mode 100644 index 0000000..384ebcc --- /dev/null +++ b/ccxt/pro/ndax.py @@ -0,0 +1,519 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCache +import json +from ccxt.base.types import Any, Int, OrderBook, Ticker, Trade +from ccxt.async_support.base.ws.client import Client +from typing import List + + +class ndax(ccxt.async_support.ndax): + + def describe(self) -> Any: + return self.deep_extend(super(ndax, self).describe(), { + 'has': { + 'ws': True, + 'watchOrderBook': True, + 'watchTrades': True, + 'watchTradesForSymbols': False, + 'watchTicker': True, + 'watchOHLCV': True, + }, + 'urls': { + 'test': { + 'ws': 'wss://ndaxmarginstaging.cdnhop.net:10456/WSAdminGatewa/', + }, + 'api': { + 'ws': 'wss://api.ndax.io/WSGateway', + }, + }, + # 'options': { + # 'tradesLimit': 1000, + # 'ordersLimit': 1000, + # 'OHLCVLimit': 1000, + # }, + }) + + def request_id(self): + requestId = self.sum(self.safe_integer(self.options, 'requestId', 0), 1) + self.options['requestId'] = requestId + return requestId + + async def watch_ticker(self, symbol: str, params={}) -> Ticker: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://apidoc.ndax.io/#subscribelevel1 + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + omsId = self.safe_integer(self.options, 'omsId', 1) + await self.load_markets() + market = self.market(symbol) + name = 'SubscribeLevel1' + messageHash = name + ':' + market['id'] + url = self.urls['api']['ws'] + requestId = self.request_id() + payload: dict = { + 'OMSId': omsId, + 'InstrumentId': int(market['id']), # conditionally optional + # 'Symbol': market['info']['symbol'], # conditionally optional + } + request: dict = { + 'm': 0, # message type, 0 request, 1 reply, 2 subscribe, 3 event, unsubscribe, 5 error + 'i': requestId, # sequence number identifies an individual request or request-and-response pair, to your application + 'n': name, # function name is the name of the function being called or that the server is responding to, the server echoes your call + 'o': self.json(payload), # JSON-formatted string containing the data being sent with the message + } + message = self.extend(request, params) + return await self.watch(url, messageHash, message, messageHash) + + def handle_ticker(self, client: Client, message): + payload = self.safe_value(message, 'o', {}) + # + # { + # "OMSId": 1, + # "InstrumentId": 1, + # "BestBid": 6423.57, + # "BestOffer": 6436.53, + # "LastTradedPx": 6423.57, + # "LastTradedQty": 0.96183964, + # "LastTradeTime": 1534862990343, + # "SessionOpen": 6249.64, + # "SessionHigh": 11111, + # "SessionLow": 4433, + # "SessionClose": 6249.64, + # "Volume": 0.96183964, + # "CurrentDayVolume": 3516.31668185, + # "CurrentDayNumTrades": 8529, + # "CurrentDayPxChange": 173.93, + # "CurrentNotional": 0.0, + # "Rolling24HrNotional": 0.0, + # "Rolling24HrVolume": 4319.63870783, + # "Rolling24NumTrades": 10585, + # "Rolling24HrPxChange": -0.4165607307408487, + # "TimeStamp": "1534862990358" + # } + # + ticker = self.parse_ticker(payload) + symbol = ticker['symbol'] + market = self.market(symbol) + self.tickers[symbol] = ticker + name = 'SubscribeLevel1' + messageHash = name + ':' + market['id'] + client.resolve(ticker, messageHash) + + async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://apidoc.ndax.io/#subscribetrades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + omsId = self.safe_integer(self.options, 'omsId', 1) + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + name = 'SubscribeTrades' + messageHash = name + ':' + market['id'] + url = self.urls['api']['ws'] + requestId = self.request_id() + payload: dict = { + 'OMSId': omsId, + 'InstrumentId': int(market['id']), # conditionally optional + 'IncludeLastCount': 100, # the number of previous trades to retrieve in the immediate snapshot, 100 by default + } + request: dict = { + 'm': 0, # message type, 0 request, 1 reply, 2 subscribe, 3 event, unsubscribe, 5 error + 'i': requestId, # sequence number identifies an individual request or request-and-response pair, to your application + 'n': name, # function name is the name of the function being called or that the server is responding to, the server echoes your call + 'o': self.json(payload), # JSON-formatted string containing the data being sent with the message + } + message = self.extend(request, params) + trades = await self.watch(url, messageHash, message, messageHash) + if self.newUpdates: + limit = trades.getLimit(symbol, limit) + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + def handle_trades(self, client: Client, message): + payload = self.safe_value(message, 'o', []) + # + # initial snapshot + # + # [ + # [ + # 6913253, # 0 TradeId + # 8, # 1 ProductPairCode + # 0.03340802, # 2 Quantity + # 19116.08, # 3 Price + # 2543425077, # 4 Order1 + # 2543425482, # 5 Order2 + # 1606935922416, # 6 Tradetime + # 0, # 7 Direction + # 1, # 8 TakerSide + # 0, # 9 BlockTrade + # 0, # 10 Either Order1ClientId or Order2ClientId + # ] + # ] + # + name = 'SubscribeTrades' + updates: dict = {} + for i in range(0, len(payload)): + trade = self.parse_trade(payload[i]) + symbol = trade['symbol'] + tradesArray = self.safe_value(self.trades, symbol) + if tradesArray is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + tradesArray = ArrayCache(limit) + tradesArray.append(trade) + self.trades[symbol] = tradesArray + updates[symbol] = True + symbols = list(updates.keys()) + for i in range(0, len(symbols)): + symbol = symbols[i] + market = self.market(symbol) + messageHash = name + ':' + market['id'] + tradesArray = self.safe_value(self.trades, symbol) + client.resolve(tradesArray, messageHash) + + async def watch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + watches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://apidoc.ndax.io/#subscribeticker + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + omsId = self.safe_integer(self.options, 'omsId', 1) + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + name = 'SubscribeTicker' + messageHash = name + ':' + timeframe + ':' + market['id'] + url = self.urls['api']['ws'] + requestId = self.request_id() + payload: dict = { + 'OMSId': omsId, + 'InstrumentId': int(market['id']), # conditionally optional + 'Interval': int(self.safe_string(self.timeframes, timeframe, timeframe)), + 'IncludeLastCount': 100, # the number of previous candles to retrieve in the immediate snapshot, 100 by default + } + request: dict = { + 'm': 0, # message type, 0 request, 1 reply, 2 subscribe, 3 event, unsubscribe, 5 error + 'i': requestId, # sequence number identifies an individual request or request-and-response pair, to your application + 'n': name, # function name is the name of the function being called or that the server is responding to, the server echoes your call + 'o': self.json(payload), # JSON-formatted string containing the data being sent with the message + } + message = self.extend(request, params) + ohlcv = await self.watch(url, messageHash, message, messageHash) + if self.newUpdates: + limit = ohlcv.getLimit(symbol, limit) + return self.filter_by_since_limit(ohlcv, since, limit, 0, True) + + def handle_ohlcv(self, client: Client, message): + # + # { + # "m": 1, + # "i": 1, + # "n": "SubscribeTicker", + # "o": [[1608284160000,23113.52,23070.88,23075.76,23075.39,162.44964300,23075.38,23075.39,8,1608284100000]], + # } + # + payload = self.safe_value(message, 'o', []) + # + # [ + # [ + # 1501603632000, # 0 DateTime + # 2700.33, # 1 High + # 2687.01, # 2 Low + # 2687.01, # 3 Open + # 2687.01, # 4 Close + # 24.86100992, # 5 Volume + # 0, # 6 Inside Bid Price + # 2870.95, # 7 Inside Ask Price + # 1 # 8 InstrumentId + # 1608290188062.7678, # 9 candle timestamp + # ] + # ] + # + updates: dict = {} + for i in range(0, len(payload)): + ohlcv = payload[i] + marketId = self.safe_string(ohlcv, 8) + market = self.safe_market(marketId) + symbol = market['symbol'] + updates[marketId] = {} + self.ohlcvs[symbol] = self.safe_value(self.ohlcvs, symbol, {}) + keys = list(self.timeframes.keys()) + for j in range(0, len(keys)): + timeframe = keys[j] + interval = self.safe_string(self.timeframes, timeframe, timeframe) + duration = int(interval) * 1000 + timestamp = self.safe_integer(ohlcv, 0) + parsed = [ + self.parse_to_int((timestamp / duration) * duration), + self.safe_float(ohlcv, 3), + self.safe_float(ohlcv, 1), + self.safe_float(ohlcv, 2), + self.safe_float(ohlcv, 4), + self.safe_float(ohlcv, 5), + ] + stored = self.safe_value(self.ohlcvs[symbol], timeframe, []) + length = len(stored) + if length and (parsed[0] == stored[length - 1][0]): + previous = stored[length - 1] + stored[length - 1] = [ + parsed[0], + previous[1], + max(parsed[1], previous[1]), + min(parsed[2], previous[2]), + parsed[4], + self.sum(parsed[5], previous[5]), + ] + updates[marketId][timeframe] = True + else: + if length and (parsed[0] < stored[length - 1][0]): + continue + else: + stored.append(parsed) + limit = self.safe_integer(self.options, 'OHLCVLimit', 1000) + if length >= limit: + stored.pop(0) + updates[marketId][timeframe] = True + self.ohlcvs[symbol][timeframe] = stored + name = 'SubscribeTicker' + marketIds = list(updates.keys()) + for i in range(0, len(marketIds)): + marketId = marketIds[i] + timeframes = list(updates[marketId].keys()) + for j in range(0, len(timeframes)): + timeframe = timeframes[j] + messageHash = name + ':' + timeframe + ':' + marketId + market = self.safe_market(marketId) + symbol = market['symbol'] + stored = self.safe_value(self.ohlcvs[symbol], timeframe, []) + client.resolve(stored, messageHash) + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://apidoc.ndax.io/#subscribelevel2 + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + omsId = self.safe_integer(self.options, 'omsId', 1) + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + name = 'SubscribeLevel2' + messageHash = name + ':' + market['id'] + url = self.urls['api']['ws'] + requestId = self.request_id() + limit = 100 if (limit is None) else limit + payload: dict = { + 'OMSId': omsId, + 'InstrumentId': int(market['id']), # conditionally optional + # 'Symbol': market['info']['symbol'], # conditionally optional + 'Depth': limit, # default 100 + } + request: dict = { + 'm': 0, # message type, 0 request, 1 reply, 2 subscribe, 3 event, unsubscribe, 5 error + 'i': requestId, # sequence number identifies an individual request or request-and-response pair, to your application + 'n': name, # function name is the name of the function being called or that the server is responding to, the server echoes your call + 'o': self.json(payload), # JSON-formatted string containing the data being sent with the message + } + subscription: dict = { + 'id': requestId, + 'messageHash': messageHash, + 'name': name, + 'symbol': symbol, + 'marketId': market['id'], + 'method': self.handle_order_book_subscription, + 'limit': limit, + 'params': params, + } + message = self.extend(request, params) + orderbook = await self.watch(url, messageHash, message, messageHash, subscription) + return orderbook.limit() + + def handle_order_book(self, client: Client, message): + # + # { + # "m": 3, + # "i": 2, + # "n": "Level2UpdateEvent", + # "o": [[2,1,1608208308265,0,20782.49,1,25000,8,1,1]] + # } + # + payload = self.safe_value(message, 'o', []) + # + # [ + # 0, # 0 MDUpdateId + # 1, # 1 Number of Unique Accounts + # 123, # 2 ActionDateTime in Posix format X 1000 + # 0, # 3 ActionType 0(New), 1(Update), 2(Delete) + # 0.0, # 4 LastTradePrice + # 0, # 5 Number of Orders + # 0.0, # 6 Price + # 0, # 7 ProductPairCode + # 0.0, # 8 Quantity + # 0, # 9 Side + # ], + # + firstBidAsk = self.safe_value(payload, 0, []) + marketId = self.safe_string(firstBidAsk, 7) + if marketId is None: + return + market = self.safe_market(marketId) + symbol = market['symbol'] + orderbook = self.safe_value(self.orderbooks, symbol) + if orderbook is None: + return + timestamp = None + nonce = None + for i in range(0, len(payload)): + bidask = payload[i] + if timestamp is None: + timestamp = self.safe_integer(bidask, 2) + else: + newTimestamp = self.safe_integer(bidask, 2) + timestamp = max(timestamp, newTimestamp) + if nonce is None: + nonce = self.safe_integer(bidask, 0) + else: + newNonce = self.safe_integer(bidask, 0) + nonce = max(nonce, newNonce) + # 0 new, 1 update, 2 remove + type = self.safe_integer(bidask, 3) + price = self.safe_float(bidask, 6) + amount = self.safe_float(bidask, 8) + side = self.safe_integer(bidask, 9) + # 0 buy, 1 sell, 2 short reserved for future use, 3 unknown + orderbookSide = orderbook['bids'] if (side == 0) else orderbook['asks'] + # 0 new, 1 update, 2 remove + if type == 0: + orderbookSide.store(price, amount) + elif type == 1: + orderbookSide.store(price, amount) + elif type == 2: + orderbookSide.store(price, 0) + orderbook['nonce'] = nonce + orderbook['timestamp'] = timestamp + orderbook['datetime'] = self.iso8601(timestamp) + name = 'SubscribeLevel2' + messageHash = name + ':' + marketId + self.orderbooks[symbol] = orderbook + client.resolve(orderbook, messageHash) + + def handle_order_book_subscription(self, client: Client, message, subscription): + # + # { + # "m": 1, + # "i": 1, + # "n": "SubscribeLevel2", + # "o": [[1,1,1608204295901,0,20782.49,1,18200,8,1,0]] + # } + # + payload = self.safe_value(message, 'o', []) + # + # [ + # [ + # 0, # 0 MDUpdateId + # 1, # 1 Number of Unique Accounts + # 123, # 2 ActionDateTime in Posix format X 1000 + # 0, # 3 ActionType 0(New), 1(Update), 2(Delete) + # 0.0, # 4 LastTradePrice + # 0, # 5 Number of Orders + # 0.0, # 6 Price + # 0, # 7 ProductPairCode + # 0.0, # 8 Quantity + # 0, # 9 Side + # ], + # ] + # + symbol = self.safe_string(subscription, 'symbol') + snapshot = self.parse_order_book(payload, symbol) + limit = self.safe_integer(subscription, 'limit') + orderbook = self.order_book(snapshot, limit) + self.orderbooks[symbol] = orderbook + messageHash = self.safe_string(subscription, 'messageHash') + client.resolve(orderbook, messageHash) + + def handle_subscription_status(self, client: Client, message): + # + # { + # "m": 1, + # "i": 1, + # "n": "SubscribeLevel2", + # "o": "[[1,1,1608204295901,0,20782.49,1,18200,8,1,0]]" + # } + # + subscriptionsById = self.index_by(client.subscriptions, 'id') + id = self.safe_integer(message, 'i') + subscription = self.safe_value(subscriptionsById, id) + if subscription is not None: + method = self.safe_value(subscription, 'method') + if method is not None: + method(client, message, subscription) + + def handle_message(self, client: Client, message): + # + # { + # "m": 0, # message type, 0 request, 1 reply, 2 subscribe, 3 event, unsubscribe, 5 error + # "i": 0, # sequence number identifies an individual request or request-and-response pair, to your application + # "n":"function name", # function name is the name of the function being called or that the server is responding to, the server echoes your call + # "o":"payload", # JSON-formatted string containing the data being sent with the message + # } + # + # { + # "m": 1, + # "i": 1, + # "n": "SubscribeLevel2", + # "o": "[[1,1,1608204295901,0,20782.49,1,18200,8,1,0]]" + # } + # + # { + # "m": 3, + # "i": 2, + # "n": "Level2UpdateEvent", + # "o": "[[2,1,1608208308265,0,20782.49,1,25000,8,1,1]]" + # } + # + payload = self.safe_string(message, 'o') + if payload is None: + return + message['o'] = json.loads(payload) + methods: dict = { + 'SubscribeLevel2': self.handle_subscription_status, + 'SubscribeLevel1': self.handle_ticker, + 'Level2UpdateEvent': self.handle_order_book, + 'Level1UpdateEvent': self.handle_ticker, + 'SubscribeTrades': self.handle_trades, + 'TradeDataUpdateEvent': self.handle_trades, + 'SubscribeTicker': self.handle_ohlcv, + 'TickerDataUpdateEvent': self.handle_ohlcv, + } + event = self.safe_string(message, 'n') + method = self.safe_value(methods, event) + if method is not None: + method(client, message) diff --git a/ccxt/pro/okx.py b/ccxt/pro/okx.py new file mode 100644 index 0000000..a4bd730 --- /dev/null +++ b/ccxt/pro/okx.py @@ -0,0 +1,2365 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolById, ArrayCacheBySymbolBySide, ArrayCacheByTimestamp +import hashlib +from ccxt.base.types import Any, Balances, Bool, Int, Liquidation, Num, Order, OrderBook, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, FundingRates, Trade +from ccxt.async_support.base.ws.client import Client +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import InvalidNonce +from ccxt.base.errors import ChecksumError + + +class okx(ccxt.async_support.okx): + + def describe(self) -> Any: + return self.deep_extend(super(okx, self).describe(), { + 'has': { + 'ws': True, + 'watchTicker': True, + 'watchMarkPrice': True, + 'watchMarkPrices': True, + 'watchTickers': True, + 'watchBidsAsks': True, + 'watchOrderBook': True, + 'watchTrades': True, + 'watchTradesForSymbols': True, + 'watchOrderBookForSymbols': True, + 'watchBalance': True, + 'watchLiquidations': 'emulated', + 'watchLiquidationsForSymbols': True, + 'watchMyLiquidations': 'emulated', + 'watchMyLiquidationsForSymbols': True, + 'watchOHLCV': True, + 'watchOHLCVForSymbols': True, + 'watchOrders': True, + 'watchMyTrades': True, + 'watchPositions': True, + 'watchFundingRate': True, + 'watchFundingRates': True, + 'createOrderWs': True, + 'editOrderWs': True, + 'cancelOrderWs': True, + 'cancelOrdersWs': True, + 'cancelAllOrdersWs': True, + }, + 'urls': { + 'api': { + 'ws': 'wss://ws.okx.com:8443/ws/v5', + }, + 'test': { + 'ws': 'wss://wspap.okx.com:8443/ws/v5', + }, + }, + 'options': { + 'watchOrderBook': { + 'checksum': True, + # + # bbo-tbt + # 1. Newly added channel that sends tick-by-tick Level 1 data + # 2. All API users can subscribe + # 3. Public depth channel, verification not required + # + # books-l2-tbt + # 1. Only users who're VIP5 and above can subscribe + # 2. Identity verification required before subscription + # + # books50-l2-tbt + # 1. Only users who're VIP4 and above can subscribe + # 2. Identity verification required before subscription + # + # books + # 1. All API users can subscribe + # 2. Public depth channel, verification not required + # + # books5 + # 1. All API users can subscribe + # 2. Public depth channel, verification not required + # 3. Data feeds will be delivered every 100ms(vs. every 200ms now) + # + 'depth': 'books', + }, + 'watchBalance': 'spot', # margin, futures, swap + 'watchTicker': { + 'channel': 'tickers', # tickers, sprd-tickers, index-tickers, block-tickers + }, + 'watchTickers': { + 'channel': 'tickers', # tickers, sprd-tickers, index-tickers, block-tickers + }, + 'watchOrders': { + 'type': 'ANY', # SPOT, MARGIN, SWAP, FUTURES, OPTION, ANY + }, + 'watchMyTrades': { + 'type': 'ANY', # SPOT, MARGIN, SWAP, FUTURES, OPTION, ANY + }, + 'createOrderWs': { + 'op': 'batch-orders', # order, batch-orders + }, + 'editOrderWs': { + 'op': 'amend-order', # amend-order, batch-amend-orders + }, + 'ws': { + # 'inflate': True, + }, + }, + 'streaming': { + # okex does not support built-in ws protocol-level ping-pong + # instead it requires a custom text-based ping-pong + 'ping': self.ping, + 'keepAlive': 18000, + }, + }) + + def get_url(self, channel: str, access='public'): + # for context: https://www.okx.com/help-center/changes-to-v5-api-websocket-subscription-parameter-and-url + isSandbox = self.options['sandboxMode'] + sandboxSuffix = '?brokerId=9999' if isSandbox else '' + isBusiness = (access == 'business') + isPublic = (access == 'public') + url = self.urls['api']['ws'] + if isBusiness or (channel.find('candle') > -1) or (channel == 'orders-algo'): + return url + '/business' + sandboxSuffix + elif isPublic: + return url + '/public' + sandboxSuffix + return url + '/private' + sandboxSuffix + + async def subscribe_multiple(self, access, channel, symbols: Strings = None, params={}): + await self.load_markets() + if symbols is None: + symbols = self.symbols + symbols = self.market_symbols(symbols) + url = self.get_url(channel, access) + messageHashes = [] + args = [] + for i in range(0, len(symbols)): + marketId = self.market_id(symbols[i]) + arg: dict = { + 'channel': channel, + 'instId': marketId, + } + args.append(self.extend(arg, params)) + messageHashes.append(channel + '::' + symbols[i]) + request: dict = { + 'op': 'subscribe', + 'args': args, + } + return await self.watch_multiple(url, messageHashes, request, messageHashes) + + async def subscribe(self, access, messageHash, channel, symbol, params={}): + await self.load_markets() + url = self.get_url(channel, access) + firstArgument: dict = { + 'channel': channel, + } + if symbol is not None: + market = self.market(symbol) + messageHash += ':' + market['id'] + firstArgument['instId'] = market['id'] + request: dict = { + 'op': 'subscribe', + 'args': [ + self.deep_extend(firstArgument, params), + ], + } + return await self.watch(url, messageHash, request, messageHash) + + async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + + https://www.okx.com/docs-v5/en/#order-book-trading-market-data-ws-trades-channel + https://www.okx.com/docs-v5/en/#order-book-trading-market-data-ws-all-trades-channel + + get the list of most recent trades for a particular symbol + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + return await self.watch_trades_for_symbols([symbol], since, limit, params) + + async def watch_trades_for_symbols(self, symbols: List[str], since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + + https://www.okx.com/docs-v5/en/#order-book-trading-market-data-ws-trades-channel + https://www.okx.com/docs-v5/en/#order-book-trading-market-data-ws-all-trades-channel + + get the list of most recent trades for a particular symbol + :param str symbols: + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.channel]: the channel to subscribe to, trades by default. Can be 'trades' and 'trades-all' + :returns dict[]: a list of `trade structures ` + """ + symbolsLength = len(symbols) + if symbolsLength == 0: + raise ArgumentsRequired(self.id + ' watchTradesForSymbols() requires a non-empty array of symbols') + await self.load_markets() + symbols = self.market_symbols(symbols) + channel = None + channel, params = self.handle_option_and_params(params, 'watchTrades', 'channel', 'trades') + topics = [] + messageHashes = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + messageHashes.append(channel + ':' + symbol) + marketId = self.market_id(symbol) + topic: dict = { + 'channel': channel, + 'instId': marketId, + } + topics.append(topic) + request: dict = { + 'op': 'subscribe', + 'args': topics, + } + access = 'public' + if channel == 'trades-all': + access = 'business' + await self.authenticate({'access': access}) + url = self.get_url(channel, access) + trades = await self.watch_multiple(url, messageHashes, request, messageHashes) + if self.newUpdates: + first = self.safe_value(trades, 0) + tradeSymbol = self.safe_string(first, 'symbol') + limit = trades.getLimit(tradeSymbol, limit) + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + async def un_watch_trades_for_symbols(self, symbols: List[str], params={}) -> Any: + """ + unWatches from the stream channel + :param str[] symbols: + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.channel]: the channel to subscribe to, trades by default. Can be trades, trades-all + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, False) + channel = None + channel, params = self.handle_option_and_params(params, 'watchTrades', 'channel', 'trades') + topics = [] + messageHashes = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + messageHashes.append('unsubscribe:' + channel + symbol) + marketId = self.market_id(symbol) + topic: dict = { + 'channel': channel, + 'instId': marketId, + } + topics.append(topic) + request: dict = { + 'op': 'unsubscribe', + 'args': topics, + } + access = 'public' + if channel == 'trades-all': + access = 'business' + await self.authenticate({'access': access}) + url = self.get_url(channel, access) + return await self.watch_multiple(url, messageHashes, request, messageHashes) + + async def un_watch_trades(self, symbol: str, params={}) -> Any: + """ + unWatches from the stream channel + :param str symbol: unified symbol of the market to fetch trades for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + return await self.un_watch_trades_for_symbols([symbol], params) + + def handle_trades(self, client: Client, message): + # + # { + # "arg": {channel: "trades", instId: "BTC-USDT"}, + # "data": [ + # { + # "instId": "BTC-USDT", + # "tradeId": "216970876", + # "px": "31684.5", + # "sz": "0.00001186", + # "side": "buy", + # "ts": "1626531038288" + # } + # ] + # } + # { + # "arg": { + # "channel": "trades-all", + # "instId": "BTC-USDT" + # }, + # "data": [ + # { + # "instId": "BTC-USDT", + # "tradeId": "130639474", + # "px": "42219.9", + # "sz": "0.12060306", + # "side": "buy", + # "source": "0", + # "ts": "1630048897897" + # } + # ] + # } + # + arg = self.safe_value(message, 'arg', {}) + channel = self.safe_string(arg, 'channel') + marketId = self.safe_string(arg, 'instId') + symbol = self.safe_symbol(marketId) + data = self.safe_value(message, 'data', []) + tradesLimit = self.safe_integer(self.options, 'tradesLimit', 1000) + for i in range(0, len(data)): + trade = self.parse_trade(data[i]) + messageHash = channel + ':' + symbol + stored = self.safe_value(self.trades, symbol) + if stored is None: + stored = ArrayCache(tradesLimit) + self.trades[symbol] = stored + stored.append(trade) + client.resolve(stored, messageHash) + + async def watch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + watch the current funding rate + + https://www.okx.com/docs-v5/en/#public-data-websocket-funding-rate-channel + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + symbol = self.symbol(symbol) + fr = await self.watch_funding_rates([symbol], params) + return fr[symbol] + + async def watch_funding_rates(self, symbols: List[str], params={}) -> FundingRates: + """ + watch the funding rate for multiple markets + + https://www.okx.com/docs-v5/en/#public-data-websocket-funding-rate-channel + + :param str[] symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `funding rates structures `, indexe by market symbols + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + channel = 'funding-rate' + topics = [] + messageHashes = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + messageHashes.append(channel + ':' + symbol) + marketId = self.market_id(symbol) + topic: dict = { + 'channel': channel, + 'instId': marketId, + } + topics.append(topic) + request: dict = { + 'op': 'subscribe', + 'args': topics, + } + url = self.get_url(channel, 'public') + fundingRate = await self.watch_multiple(url, messageHashes, request, messageHashes) + if self.newUpdates: + symbol = self.safe_string(fundingRate, 'symbol') + result: dict = {} + result[symbol] = fundingRate + return result + return self.filter_by_array(self.fundingRates, 'symbol', symbols) + + def handle_funding_rate(self, client: Client, message): + # + # "data":[ + # { + # "fundingRate":"0.0001875391284828", + # "fundingTime":"1700726400000", + # "instId":"BTC-USD-SWAP", + # "instType":"SWAP", + # "method": "next_period", + # "maxFundingRate":"0.00375", + # "minFundingRate":"-0.00375", + # "nextFundingRate":"0.0002608059239328", + # "nextFundingTime":"1700755200000", + # "premium": "0.0001233824646391", + # "settFundingRate":"0.0001699799259033", + # "settState":"settled", + # "ts":"1700724675402" + # } + # ] + # + data = self.safe_list(message, 'data', []) + for i in range(0, len(data)): + rawfr = data[i] + fundingRate = self.parse_funding_rate(rawfr) + symbol = fundingRate['symbol'] + self.fundingRates[symbol] = fundingRate + client.resolve(fundingRate, 'funding-rate' + ':' + fundingRate['symbol']) + + async def watch_ticker(self, symbol: str, params={}) -> Ticker: + """ + + https://www.okx.com/docs-v5/en/#order-book-trading-market-data-ws-tickers-channel + + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.channel]: the channel to subscribe to, tickers by default. Can be tickers, sprd-tickers, index-tickers, block-tickers + :returns dict: a `ticker structure ` + """ + channel = None + channel, params = self.handle_option_and_params(params, 'watchTicker', 'channel', 'tickers') + params['channel'] = channel + market = self.market(symbol) + symbol = market['symbol'] + ticker = await self.watch_tickers([symbol], params) + return self.safe_value(ticker, symbol) + + async def un_watch_ticker(self, symbol: str, params={}) -> Any: + """ + + https://www.okx.com/docs-v5/en/#order-book-trading-market-data-ws-tickers-channel + + unWatches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.channel]: the channel to subscribe to, tickers by default. Can be tickers, sprd-tickers, index-tickers, block-tickers + :returns dict: a `ticker structure ` + """ + return await self.un_watch_tickers([symbol], params) + + async def watch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + + https://www.okx.com/docs-v5/en/#order-book-trading-market-data-ws-tickers-channel + + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list + :param str[] [symbols]: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.channel]: the channel to subscribe to, tickers by default. Can be tickers, sprd-tickers, index-tickers, block-tickers + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, False) + channel = None + channel, params = self.handle_option_and_params(params, 'watchTickers', 'channel', 'tickers') + newTickers = await self.subscribe_multiple('public', channel, symbols, params) + if self.newUpdates: + return newTickers + return self.filter_by_array(self.tickers, 'symbol', symbols) + + async def watch_mark_price(self, symbol: str, params={}) -> Ticker: + """ + + https://www.okx.com/docs-v5/en/#public-data-websocket-mark-price-channel + + watches a mark price + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.channel]: the channel to subscribe to, tickers by default. Can be tickers, sprd-tickers, index-tickers, block-tickers + :returns dict: a `ticker structure ` + """ + channel = None + channel, params = self.handle_option_and_params(params, 'watchMarkPrice', 'channel', 'mark-price') + params['channel'] = channel + market = self.market(symbol) + symbol = market['symbol'] + ticker = await self.watch_mark_prices([symbol], params) + return ticker[symbol] + + async def watch_mark_prices(self, symbols: Strings = None, params={}) -> Tickers: + """ + + https://www.okx.com/docs-v5/en/#public-data-websocket-mark-price-channel + + watches mark prices + :param str[] [symbols]: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.channel]: the channel to subscribe to, tickers by default. Can be tickers, sprd-tickers, index-tickers, block-tickers + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, False) + channel = None + channel, params = self.handle_option_and_params(params, 'watchMarkPrices', 'channel', 'mark-price') + newTickers = await self.subscribe_multiple('public', channel, symbols, params) + if self.newUpdates: + return newTickers + return self.filter_by_array(self.tickers, 'symbol', symbols) + + async def un_watch_tickers(self, symbols: Strings = None, params={}) -> Any: + """ + + https://www.okx.com/docs-v5/en/#order-book-trading-market-data-ws-tickers-channel + + unWatches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list + :param str[] [symbols]: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.channel]: the channel to subscribe to, tickers by default. Can be tickers, sprd-tickers, index-tickers, block-tickers + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, False) + channel = None + channel, params = self.handle_option_and_params(params, 'watchTickers', 'channel', 'tickers') + topics = [] + messageHashes = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + messageHashes.append('unsubscribe:ticker:' + symbol) + marketId = self.market_id(symbol) + topic: dict = { + 'channel': channel, + 'instId': marketId, + } + topics.append(topic) + request: dict = { + 'op': 'unsubscribe', + 'args': topics, + } + url = self.get_url(channel, 'public') + return await self.watch_multiple(url, messageHashes, request, messageHashes) + + def handle_ticker(self, client: Client, message): + # + # { + # "arg": {channel: "tickers", instId: "BTC-USDT"}, + # "data": [ + # { + # "instType": "SPOT", + # "instId": "BTC-USDT", + # "last": "31500.1", + # "lastSz": "0.00001754", + # "askPx": "31500.1", + # "askSz": "0.00998144", + # "bidPx": "31500", + # "bidSz": "3.05652439", + # "open24h": "31697", + # "high24h": "32248", + # "low24h": "31165.6", + # "sodUtc0": "31385.5", + # "sodUtc8": "32134.9", + # "volCcy24h": "503403597.38138519", + # "vol24h": "15937.10781721", + # "ts": "1626526618762" + # } + # ] + # } + # + self.handle_bid_ask(client, message) + arg = self.safe_value(message, 'arg', {}) + marketId = self.safe_string(arg, 'instId') + market = self.safe_market(marketId, None, '-') + symbol = market['symbol'] + channel = self.safe_string(arg, 'channel') + data = self.safe_value(message, 'data', []) + newTickers: dict = {} + for i in range(0, len(data)): + ticker = self.parse_ticker(data[i]) + self.tickers[symbol] = ticker + newTickers[symbol] = ticker + messageHash = channel + '::' + symbol + client.resolve(newTickers, messageHash) + + async def watch_bids_asks(self, symbols: Strings = None, params={}) -> Tickers: + """ + + https://www.okx.com/docs-v5/en/#order-book-trading-market-data-ws-tickers-channel + + watches best bid & ask for symbols + :param str[] symbols: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, False) + channel = None + channel, params = self.handle_option_and_params(params, 'watchBidsAsks', 'channel', 'tickers') + url = self.get_url(channel, 'public') + messageHashes = [] + args = [] + for i in range(0, len(symbols)): + marketId = self.market_id(symbols[i]) + arg: dict = { + 'channel': channel, + 'instId': marketId, + } + args.append(self.extend(arg, params)) + messageHashes.append('bidask::' + symbols[i]) + request: dict = { + 'op': 'subscribe', + 'args': args, + } + newTickers = await self.watch_multiple(url, messageHashes, request, messageHashes) + if self.newUpdates: + tickers: dict = {} + tickers[newTickers['symbol']] = newTickers + return tickers + return self.filter_by_array(self.bidsasks, 'symbol', symbols) + + def handle_bid_ask(self, client: Client, message): + # + # { + # "arg": {channel: "tickers", instId: "BTC-USDT"}, + # "data": [ + # { + # "instType": "SPOT", + # "instId": "BTC-USDT", + # "last": "31500.1", + # "lastSz": "0.00001754", + # "askPx": "31500.1", + # "askSz": "0.00998144", + # "bidPx": "31500", + # "bidSz": "3.05652439", + # "open24h": "31697", + # "high24h": "32248", + # "low24h": "31165.6", + # "sodUtc0": "31385.5", + # "sodUtc8": "32134.9", + # "volCcy24h": "503403597.38138519", + # "vol24h": "15937.10781721", + # "ts": "1626526618762" + # } + # ] + # } + # + data = self.safe_list(message, 'data', []) + ticker = self.safe_dict(data, 0, {}) + parsedTicker = self.parse_ws_bid_ask(ticker) + symbol = parsedTicker['symbol'] + self.bidsasks[symbol] = parsedTicker + messageHash = 'bidask::' + symbol + client.resolve(parsedTicker, messageHash) + + def parse_ws_bid_ask(self, ticker, market=None): + marketId = self.safe_string(ticker, 'instId') + market = self.safe_market(marketId, market) + symbol = self.safe_string(market, 'symbol') + timestamp = self.safe_integer(ticker, 'ts') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'ask': self.safe_string(ticker, 'askPx'), + 'askVolume': self.safe_string(ticker, 'askSz'), + 'bid': self.safe_string(ticker, 'bidPx'), + 'bidVolume': self.safe_string(ticker, 'bidSz'), + 'info': ticker, + }, market) + + async def watch_liquidations_for_symbols(self, symbols: List[str], since: Int = None, limit: Int = None, params={}) -> List[Liquidation]: + """ + watch the public liquidations of a trading pair + + https://www.okx.com/docs-v5/en/#public-data-websocket-liquidation-orders-channel + + :param str symbols: + :param int [since]: the earliest time in ms to fetch liquidations for + :param int [limit]: the maximum number of liquidation structures to retrieve + :param dict [params]: exchange specific parameters for the okx api endpoint + :returns dict: an array of `liquidation structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, True, True) + messageHash = 'liquidations' + messageHashes = [] + if symbols is not None: + for i in range(0, len(symbols)): + symbol = symbols[i] + messageHashes.append(messageHash + '::' + symbol) + else: + messageHashes.append(messageHash) + market = self.get_market_from_symbols(symbols) + type = None + type, params = self.handle_market_type_and_params('watchliquidationsForSymbols', market, params) + channel = 'liquidation-orders' + if type == 'spot': + type = 'SWAP' + elif type == 'future': + type = 'futures' + uppercaseType = type.upper() + request = { + 'op': 'subscribe', + 'args': [ + { + 'channel': channel, + 'instType': uppercaseType, + }, + ], + } + url = self.get_url(channel, 'public') + newLiquidations = await self.watch_multiple(url, messageHashes, request, messageHashes) + if self.newUpdates: + return newLiquidations + return self.filter_by_symbols_since_limit(self.liquidations, symbols, since, limit, True) + + def handle_liquidation(self, client: Client, message): + # + # { + # "arg": { + # "channel": "liquidation-orders", + # "instType": "SWAP" + # }, + # "data": [ + # { + # "details": [ + # { + # "bkLoss": "0", + # "bkPx": "0.007831", + # "ccy": "", + # "posSide": "short", + # "side": "buy", + # "sz": "13", + # "ts": "1692266434010" + # } + # ], + # "instFamily": "IOST-USDT", + # "instId": "IOST-USDT-SWAP", + # "instType": "SWAP", + # "uly": "IOST-USDT" + # } + # ] + # } + # + rawLiquidations = self.safe_list(message, 'data', []) + for i in range(0, len(rawLiquidations)): + rawLiquidation = rawLiquidations[i] + liquidation = self.parse_ws_liquidation(rawLiquidation) + symbol = self.safe_string(liquidation, 'symbol') + liquidations = self.safe_value(self.liquidations, symbol) + if liquidations is None: + limit = self.safe_integer(self.options, 'liquidationsLimit', 1000) + liquidations = ArrayCache(limit) + liquidations.append(liquidation) + self.liquidations[symbol] = liquidations + client.resolve([liquidation], 'liquidations') + client.resolve([liquidation], 'liquidations::' + symbol) + + async def watch_my_liquidations_for_symbols(self, symbols: List[str], since: Int = None, limit: Int = None, params={}) -> List[Liquidation]: + """ + watch the private liquidations of a trading pair + + https://www.okx.com/docs-v5/en/#trading-account-websocket-balance-and-position-channel + + :param str[] symbols: + :param int [since]: the earliest time in ms to fetch liquidations for + :param int [limit]: the maximum number of liquidation structures to retrieve + :param dict [params]: exchange specific parameters for the okx api endpoint + :returns dict: an array of `liquidation structures ` + """ + await self.load_markets() + isTrigger = self.safe_value_2(params, 'stop', 'trigger', False) + params = self.omit(params, ['stop', 'trigger']) + await self.authenticate({'access': 'business' if isTrigger else 'private'}) + symbols = self.market_symbols(symbols, None, True, True) + messageHash = 'myLiquidations' + messageHashes = [] + if symbols is not None: + for i in range(0, len(symbols)): + symbol = symbols[i] + messageHashes.append(messageHash + '::' + symbol) + else: + messageHashes.append(messageHash) + channel = 'balance_and_position' + request: dict = { + 'op': 'subscribe', + 'args': [ + { + 'channel': channel, + }, + ], + } + url = self.get_url(channel, 'private') + newLiquidations = await self.watch_multiple(url, messageHashes, self.deep_extend(request, params), messageHashes) + if self.newUpdates: + return newLiquidations + return self.filter_by_symbols_since_limit(self.liquidations, symbols, since, limit, True) + + def handle_my_liquidation(self, client: Client, message): + # + # { + # "arg": { + # "channel": "balance_and_position", + # "uid": "77982378738415879" + # }, + # "data": [{ + # "pTime": "1597026383085", + # "eventType": "snapshot", + # "balData": [{ + # "ccy": "BTC", + # "cashBal": "1", + # "uTime": "1597026383085" + # }], + # "posData": [{ + # "posId": "1111111111", + # "tradeId": "2", + # "instId": "BTC-USD-191018", + # "instType": "FUTURES", + # "mgnMode": "cross", + # "posSide": "long", + # "pos": "10", + # "ccy": "BTC", + # "posCcy": "", + # "avgPx": "3320", + # "uTIme": "1597026383085" + # }], + # "trades": [{ + # "instId": "BTC-USD-191018", + # "tradeId": "2", + # }] + # }] + # } + # + rawLiquidations = self.safe_list(message, 'data', []) + for i in range(0, len(rawLiquidations)): + rawLiquidation = rawLiquidations[i] + eventType = self.safe_string(rawLiquidation, 'eventType') + if eventType != 'liquidation': + return + liquidation = self.parse_ws_my_liquidation(rawLiquidation) + symbol = self.safe_string(liquidation, 'symbol') + liquidations = self.safe_value(self.liquidations, symbol) + if liquidations is None: + limit = self.safe_integer(self.options, 'myLiquidationsLimit', 1000) + liquidations = ArrayCache(limit) + liquidations.append(liquidation) + self.liquidations[symbol] = liquidations + client.resolve([liquidation], 'myLiquidations') + client.resolve([liquidation], 'myLiquidations::' + symbol) + + def parse_ws_my_liquidation(self, liquidation, market=None): + # + # { + # "pTime": "1597026383085", + # "eventType": "snapshot", + # "balData": [{ + # "ccy": "BTC", + # "cashBal": "1", + # "uTime": "1597026383085" + # }], + # "posData": [{ + # "posId": "1111111111", + # "tradeId": "2", + # "instId": "BTC-USD-191018", + # "instType": "FUTURES", + # "mgnMode": "cross", + # "posSide": "long", + # "pos": "10", + # "ccy": "BTC", + # "posCcy": "", + # "avgPx": "3320", + # "uTIme": "1597026383085" + # }], + # "trades": [{ + # "instId": "BTC-USD-191018", + # "tradeId": "2", + # }] + # } + # + posData = self.safe_list(liquidation, 'posData', []) + firstPosData = self.safe_dict(posData, 0, {}) + marketId = self.safe_string(firstPosData, 'instId') + market = self.safe_market(marketId, market) + timestamp = self.safe_integer(firstPosData, 'uTIme') + return self.safe_liquidation({ + 'info': liquidation, + 'symbol': self.safe_symbol(marketId, market), + 'contracts': self.safe_number(firstPosData, 'pos'), + 'contractSize': self.safe_number(market, 'contractSize'), + 'price': self.safe_number(liquidation, 'avgPx'), + 'baseValue': None, + 'quoteValue': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + + def parse_ws_liquidation(self, liquidation, market=None): + # + # public liquidation + # { + # "details": [ + # { + # "bkLoss": "0", + # "bkPx": "0.007831", + # "ccy": "", + # "posSide": "short", + # "side": "buy", + # "sz": "13", + # "ts": "1692266434010" + # } + # ], + # "instFamily": "IOST-USDT", + # "instId": "IOST-USDT-SWAP", + # "instType": "SWAP", + # "uly": "IOST-USDT" + # } + # + details = self.safe_list(liquidation, 'details', []) + liquidationDetails = self.safe_dict(details, 0, {}) + marketId = self.safe_string(liquidation, 'instId') + market = self.safe_market(marketId, market) + timestamp = self.safe_integer(liquidationDetails, 'ts') + return self.safe_liquidation({ + 'info': liquidation, + 'symbol': self.safe_symbol(marketId, market), + 'contracts': self.safe_number(liquidationDetails, 'sz'), + 'contractSize': self.safe_number(market, 'contractSize'), + 'price': self.safe_number(liquidationDetails, 'bkPx'), + 'side': self.safe_string(liquidationDetails, 'side'), + 'baseValue': None, + 'quoteValue': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + + async def watch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + watches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + symbol = self.symbol(symbol) + interval = self.safe_string(self.timeframes, timeframe, timeframe) + name = 'candle' + interval + ohlcv = await self.subscribe('public', name, name, symbol, params) + if self.newUpdates: + limit = ohlcv.getLimit(symbol, limit) + return self.filter_by_since_limit(ohlcv, since, limit, 0, True) + + async def un_watch_ohlcv(self, symbol: str, timeframe: str = '1m', params={}) -> Any: + """ + watches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + return await self.un_watch_ohlcv_for_symbols([[symbol, timeframe]], params) + + async def watch_ohlcv_for_symbols(self, symbolsAndTimeframes: List[List[str]], since: Int = None, limit: Int = None, params={}): + """ + watches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str[][] symbolsAndTimeframes: array of arrays containing unified symbols and timeframes to fetch OHLCV data for, example [['BTC/USDT', '1m'], ['LTC/USDT', '5m']] + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + symbolsLength = len(symbolsAndTimeframes) + if symbolsLength == 0 or not isinstance(symbolsAndTimeframes[0], list): + raise ArgumentsRequired(self.id + " watchOHLCVForSymbols() requires a an array of symbols and timeframes, like [['BTC/USDT', '1m'], ['LTC/USDT', '5m']]") + await self.load_markets() + topics = [] + messageHashes = [] + for i in range(0, len(symbolsAndTimeframes)): + symbolAndTimeframe = symbolsAndTimeframes[i] + sym = symbolAndTimeframe[0] + tf = symbolAndTimeframe[1] + marketId = self.market_id(sym) + interval = self.safe_string(self.timeframes, tf, tf) + channel = 'candle' + interval + topic: dict = { + 'channel': channel, + 'instId': marketId, + } + topics.append(topic) + messageHashes.append('multi:' + channel + ':' + sym) + request: dict = { + 'op': 'subscribe', + 'args': topics, + } + url = self.get_url('candle', 'public') + symbol, timeframe, candles = await self.watch_multiple(url, messageHashes, request, messageHashes) + if self.newUpdates: + limit = candles.getLimit(symbol, limit) + filtered = self.filter_by_since_limit(candles, since, limit, 0, True) + return self.create_ohlcv_object(symbol, timeframe, filtered) + + async def un_watch_ohlcv_for_symbols(self, symbolsAndTimeframes: List[List[str]], params={}) -> Any: + """ + unWatches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str[][] symbolsAndTimeframes: array of arrays containing unified symbols and timeframes to fetch OHLCV data for, example [['BTC/USDT', '1m'], ['LTC/USDT', '5m']] + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + symbolsLength = len(symbolsAndTimeframes) + if symbolsLength == 0 or not isinstance(symbolsAndTimeframes[0], list): + raise ArgumentsRequired(self.id + " watchOHLCVForSymbols() requires a an array of symbols and timeframes, like [['BTC/USDT', '1m'], ['LTC/USDT', '5m']]") + await self.load_markets() + topics = [] + messageHashes = [] + for i in range(0, len(symbolsAndTimeframes)): + symbolAndTimeframe = symbolsAndTimeframes[i] + sym = symbolAndTimeframe[0] + tf = symbolAndTimeframe[1] + marketId = self.market_id(sym) + interval = self.safe_string(self.timeframes, tf, tf) + channel = 'candle' + interval + topic: dict = { + 'channel': channel, + 'instId': marketId, + } + topics.append(topic) + messageHashes.append('unsubscribe:multi:' + channel + ':' + sym) + request: dict = { + 'op': 'unsubscribe', + 'args': topics, + } + url = self.get_url('candle', 'public') + return await self.watch_multiple(url, messageHashes, request, messageHashes) + + def handle_ohlcv(self, client: Client, message): + # + # { + # "arg": {channel: "candle1m", instId: "BTC-USDT"}, + # "data": [ + # [ + # "1626690720000", + # "31334", + # "31334", + # "31334", + # "31334", + # "0.0077", + # "241.2718" + # ] + # ] + # } + # + arg = self.safe_value(message, 'arg', {}) + channel = self.safe_string(arg, 'channel') + data = self.safe_value(message, 'data', []) + marketId = self.safe_string(arg, 'instId') + market = self.safe_market(marketId) + symbol = market['symbol'] + interval = channel.replace('candle', '') + # use a reverse lookup in a static map instead + timeframe = self.find_timeframe(interval) + for i in range(0, len(data)): + parsed = self.parse_ohlcv(data[i], market) + self.ohlcvs[symbol] = self.safe_value(self.ohlcvs, symbol, {}) + stored = self.safe_value(self.ohlcvs[symbol], timeframe) + if stored is None: + limit = self.safe_integer(self.options, 'OHLCVLimit', 1000) + stored = ArrayCacheByTimestamp(limit) + self.ohlcvs[symbol][timeframe] = stored + stored.append(parsed) + messageHash = channel + ':' + market['id'] + client.resolve(stored, messageHash) + # for multiOHLCV we need special object, to other "multi" + # methods, because OHLCV response item does not contain symbol + # or timeframe, thus otherwise it would be unrecognizable + messageHashForMulti = 'multi:' + channel + ':' + symbol + client.resolve([symbol, timeframe, stored], messageHashForMulti) + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + + https://www.okx.com/docs-v5/en/#order-book-trading-market-data-ws-order-book-channel + + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.depth]: okx order book depth, can be books, books5, books-l2-tbt, books50-l2-tbt, bbo-tbt + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + # + # bbo-tbt + # 1. Newly added channel that sends tick-by-tick Level 1 data + # 2. All API users can subscribe + # 3. Public depth channel, verification not required + # + # books-l2-tbt + # 1. Only users who're VIP5 and above can subscribe + # 2. Identity verification required before subscription + # + # books50-l2-tbt + # 1. Only users who're VIP4 and above can subscribe + # 2. Identity verification required before subscription + # + # books + # 1. All API users can subscribe + # 2. Public depth channel, verification not required + # + # books5 + # 1. All API users can subscribe + # 2. Public depth channel, verification not required + # 3. Data feeds will be delivered every 100ms(vs. every 200ms now) + # + return await self.watch_order_book_for_symbols([symbol], limit, params) + + async def watch_order_book_for_symbols(self, symbols: List[str], limit: Int = None, params={}) -> OrderBook: + """ + + https://www.okx.com/docs-v5/en/#order-book-trading-market-data-ws-order-book-channel + + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str[] symbols: unified array of symbols + :param int [limit]: 1,5, 400, 50(l2-tbt, vip4+) or 40000(vip5+) the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.depth]: okx order book depth, can be books, books5, books-l2-tbt, books50-l2-tbt, bbo-tbt + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + depth = None + depth, params = self.handle_option_and_params(params, 'watchOrderBook', 'depth', 'books') + if limit is not None: + if limit == 1: + depth = 'bbo-tbt' + elif limit > 1 and limit <= 5: + depth = 'books5' + elif limit == 50: + depth = 'books50-l2-tbt' # Make sure you have VIP4 and above + elif limit == 400: + depth = 'books' + if (depth == 'books-l2-tbt') or (depth == 'books50-l2-tbt'): + if not self.check_required_credentials(False): + raise AuthenticationError(self.id + ' watchOrderBook/watchOrderBookForSymbols requires authentication for self depth. Add credentials or change the depth option to books or books5') + await self.authenticate({'access': 'public'}) + topics = [] + messageHashes = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + messageHashes.append(depth + ':' + symbol) + marketId = self.market_id(symbol) + topic: dict = { + 'channel': depth, + 'instId': marketId, + } + topics.append(topic) + request: dict = { + 'op': 'subscribe', + 'args': topics, + } + url = self.get_url(depth, 'public') + orderbook = await self.watch_multiple(url, messageHashes, request, messageHashes) + return orderbook.limit() + + async def un_watch_order_book_for_symbols(self, symbols: List[str], params={}) -> Any: + """ + + https://www.okx.com/docs-v5/en/#order-book-trading-market-data-ws-order-book-channel + + unWatches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str[] symbols: unified array of symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.limit]: the maximum amount of order book entries to return + :param str [params.depth]: okx order book depth, can be books, books5, books-l2-tbt, books50-l2-tbt, bbo-tbt + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, False) + depth = None + depth, params = self.handle_option_and_params(params, 'watchOrderBook', 'depth', 'books') + limit = self.safe_integer(params, 'limit') + if limit is not None: + if limit == 1: + depth = 'bbo-tbt' + elif limit > 1 and limit <= 5: + depth = 'books5' + elif limit == 50: + depth = 'books50-l2-tbt' # Make sure you have VIP4 and above + elif limit == 400: + depth = 'books' + topics = [] + subMessageHashes = [] + messageHashes = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + subMessageHashes.append(depth + ':' + symbol) + messageHashes.append('unsubscribe:orderbook:' + symbol) + marketId = self.market_id(symbol) + topic: dict = { + 'channel': depth, + 'instId': marketId, + } + topics.append(topic) + request: dict = { + 'op': 'unsubscribe', + 'args': topics, + } + url = self.get_url(depth, 'public') + return await self.watch_multiple(url, messageHashes, request, messageHashes) + + async def un_watch_order_book(self, symbol: str, params={}) -> Any: + """ + + https://www.okx.com/docs-v5/en/#order-book-trading-market-data-ws-order-book-channel + + unWatches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str symbol: unified array of symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.limit]: the maximum amount of order book entries to return + :param str [params.depth]: okx order book depth, can be books, books5, books-l2-tbt, books50-l2-tbt, bbo-tbt + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + return await self.un_watch_order_book_for_symbols([symbol], params) + + def handle_delta(self, bookside, delta): + # + # [ + # "31685", # price + # "0.78069158", # amount + # "0", # liquidated orders + # "17" # orders + # ] + # + price = self.safe_float(delta, 0) + amount = self.safe_float(delta, 1) + bookside.store(price, amount) + + def handle_deltas(self, bookside, deltas): + for i in range(0, len(deltas)): + self.handle_delta(bookside, deltas[i]) + + def handle_order_book_message(self, client: Client, message, orderbook, messageHash, market=None): + # + # { + # "asks": [ + # ['31738.3', '0.05973179', "0", "3"], + # ['31738.5', '0.11035404', "0", "2"], + # ['31739.6', '0.01', "0", "1"], + # ], + # "bids": [ + # ['31738.2', '0.67557666', "0", "9"], + # ['31738', '0.02466947', "0", "2"], + # ['31736.3', '0.01705046', "0", "2"], + # ], + # "instId": "BTC-USDT", + # "ts": "1626537446491" + # "checksum": -855196043, + # "prevSeqId": 123456, + # "seqId": 123457 + # } + # + asks = self.safe_value(message, 'asks', []) + bids = self.safe_value(message, 'bids', []) + storedAsks = orderbook['asks'] + storedBids = orderbook['bids'] + self.handle_deltas(storedAsks, asks) + self.handle_deltas(storedBids, bids) + marketId = self.safe_string(message, 'instId') + symbol = self.safe_symbol(marketId, market) + checksum = self.handle_option('watchOrderBook', 'checksum', True) + seqId = self.safe_integer(message, 'seqId') + if checksum: + prevSeqId = self.safe_integer(message, 'prevSeqId') + nonce = orderbook['nonce'] + asksLength = len(storedAsks) + bidsLength = len(storedBids) + payloadArray = [] + for i in range(0, 25): + if i < bidsLength: + payloadArray.append(self.number_to_string(storedBids[i][0])) + payloadArray.append(self.number_to_string(storedBids[i][1])) + if i < asksLength: + payloadArray.append(self.number_to_string(storedAsks[i][0])) + payloadArray.append(self.number_to_string(storedAsks[i][1])) + payload = ':'.join(payloadArray) + responseChecksum = self.safe_integer(message, 'checksum') + localChecksum = self.crc32(payload, True) + error = None + if prevSeqId != -1 and nonce != prevSeqId: + error = InvalidNonce(self.id + ' watchOrderBook received invalid nonce') + if responseChecksum != localChecksum: + error = ChecksumError(self.id + ' ' + self.orderbook_checksum_message(symbol)) + if error is not None: + del client.subscriptions[messageHash] + if symbol is not None: + del self.orderbooks[symbol] + client.reject(error, messageHash) + timestamp = self.safe_integer(message, 'ts') + orderbook['nonce'] = seqId + orderbook['timestamp'] = timestamp + orderbook['datetime'] = self.iso8601(timestamp) + return orderbook + + def handle_order_book(self, client: Client, message): + # + # snapshot + # + # { + # "arg": {channel: 'books-l2-tbt', instId: "BTC-USDT"}, + # "action": "snapshot", + # "data": [ + # { + # "asks": [ + # ['31685', '0.78069158', "0", "17"], + # ['31685.1', '0.0001', "0", "1"], + # ['31685.6', '0.04543165', "0", "1"], + # ], + # "bids": [ + # ['31684.9', '0.01', "0", "1"], + # ['31682.9', '0.0001', "0", "1"], + # ['31680.7', '0.01', "0", "1"], + # ], + # "ts": "1626532416403", + # "checksum": -1023440116 + # } + # ] + # } + # + # update + # + # { + # "arg": {channel: 'books-l2-tbt', instId: "BTC-USDT"}, + # "action": "update", + # "data": [ + # { + # "asks": [ + # ['31657.7', '0', "0", "0"], + # ['31659.7', '0.01', "0", "1"], + # ['31987.3', '0.01', "0", "1"] + # ], + # "bids": [ + # ['31642.9', '0.50296385', "0", "4"], + # ['31639.9', '0', "0", "0"], + # ['31638.7', '0.01', "0", "1"], + # ], + # "ts": "1626535709008", + # "checksum": 830931827 + # } + # ] + # } + # + # books5 + # + # { + # "arg": {channel: "books5", instId: "BTC-USDT"}, + # "data": [ + # { + # "asks": [ + # ['31738.3', '0.05973179', "0", "3"], + # ['31738.5', '0.11035404', "0", "2"], + # ['31739.6', '0.01', "0", "1"], + # ], + # "bids": [ + # ['31738.2', '0.67557666', "0", "9"], + # ['31738', '0.02466947', "0", "2"], + # ['31736.3', '0.01705046', "0", "2"], + # ], + # "instId": "BTC-USDT", + # "ts": "1626537446491" + # } + # ] + # } + # + # bbo-tbt + # + # { + # "arg":{ + # "channel":"bbo-tbt", + # "instId":"BTC-USDT" + # }, + # "data":[ + # { + # "asks":[["36232.2","1.8826134","0","17"]], + # "bids":[["36232.1","0.00572212","0","2"]], + # "ts":"1651826598363" + # } + # ] + # } + # + arg = self.safe_dict(message, 'arg', {}) + channel = self.safe_string(arg, 'channel') + action = self.safe_string(message, 'action') + data = self.safe_list(message, 'data', []) + marketId = self.safe_string(arg, 'instId') + market = self.safe_market(marketId) + symbol = market['symbol'] + depths: dict = { + 'bbo-tbt': 1, + 'books': 400, + 'books5': 5, + 'books-l2-tbt': 400, + 'books50-l2-tbt': 50, + } + limit = self.safe_integer(depths, channel) + messageHash = channel + ':' + symbol + if action == 'snapshot': + for i in range(0, len(data)): + update = data[i] + orderbook = self.order_book({}, limit) + self.orderbooks[symbol] = orderbook + orderbook['symbol'] = symbol + self.handle_order_book_message(client, update, orderbook, messageHash) + client.resolve(orderbook, messageHash) + elif action == 'update': + if symbol in self.orderbooks: + orderbook = self.orderbooks[symbol] + for i in range(0, len(data)): + update = data[i] + self.handle_order_book_message(client, update, orderbook, messageHash, market) + client.resolve(orderbook, messageHash) + elif (channel == 'books5') or (channel == 'bbo-tbt'): + if not (symbol in self.orderbooks): + self.orderbooks[symbol] = self.order_book({}, limit) + orderbook = self.orderbooks[symbol] + for i in range(0, len(data)): + update = data[i] + timestamp = self.safe_integer(update, 'ts') + snapshot = self.parse_order_book(update, symbol, timestamp, 'bids', 'asks', 0, 1) + orderbook.reset(snapshot) + client.resolve(orderbook, messageHash) + return message + + async def authenticate(self, params={}): + self.check_required_credentials() + access = self.safe_string(params, 'access', 'private') + params = self.omit(params, ['access']) + url = self.get_url('users', access) + messageHash = 'authenticated' + client = self.client(url) + future = client.reusableFuture(messageHash) + authenticated = self.safe_value(client.subscriptions, messageHash) + if authenticated is None: + timestamp = str(self.seconds()) + method = 'GET' + path = '/users/self/verify' + auth = timestamp + method + path + signature = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256, 'base64') + operation = 'login' + request: dict = { + 'op': operation, + 'args': [ + { + 'apiKey': self.apiKey, + 'passphrase': self.password, + 'timestamp': timestamp, + 'sign': signature, + }, + ], + } + # Only add params['access'] to prevent sending custom parameters, such. + if 'access' in params: + request['access'] = params['access'] + self.watch(url, messageHash, request, messageHash) + return await future + + async def watch_balance(self, params={}) -> Balances: + """ + watch balance and get the amount of funds available for trading or funds locked in orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + await self.authenticate() + return await self.subscribe('private', 'account', 'account', None, params) + + def handle_balance_and_position(self, client: Client, message): + self.handle_my_liquidation(client, message) + + def handle_balance(self, client: Client, message): + # + # { + # "arg": {channel: "account"}, + # "data": [ + # { + # "adjEq": '', + # "details": [ + # { + # "availBal": '', + # "availEq": "8.21009913", + # "cashBal": "8.21009913", + # "ccy": "USDT", + # "coinUsdPrice": "0.99994", + # "crossLiab": '', + # "disEq": "8.2096065240522", + # "eq": "8.21009913", + # "eqUsd": "8.2096065240522", + # "frozenBal": "0", + # "interest": '', + # "isoEq": "0", + # "isoLiab": '', + # "liab": '', + # "maxLoan": '', + # "mgnRatio": '', + # "notionalLever": "0", + # "ordFrozen": "0", + # "twap": "0", + # "uTime": "1621927314996", + # "upl": "0" + # }, + # ], + # "imr": '', + # "isoEq": "0", + # "mgnRatio": '', + # "mmr": '', + # "notionalUsd": '', + # "ordFroz": '', + # "totalEq": "22.1930992296832", + # "uTime": "1626692120916" + # } + # ] + # } + # + arg = self.safe_value(message, 'arg', {}) + channel = self.safe_string(arg, 'channel') + type = 'spot' + balance = self.parseTradingBalance(message) + oldBalance = self.safe_value(self.balance, type, {}) + newBalance = self.deep_extend(oldBalance, balance) + self.balance[type] = self.safe_balance(newBalance) + client.resolve(self.balance[type], channel) + + def order_to_trade(self, order, market=None): + info = self.safe_value(order, 'info', {}) + timestamp = self.safe_integer(info, 'fillTime') + feeMarketId = self.safe_string(info, 'fillFeeCcy') + isTaker = self.safe_string(info, 'execType', '') == 'T' + return self.safe_trade({ + 'info': info, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': self.safe_string(order, 'symbol'), + 'id': self.safe_string(info, 'tradeId'), + 'order': self.safe_string(order, 'id'), + 'type': self.safe_string(order, 'type'), + 'takerOrMaker': 'taker' if (isTaker) else 'maker', + 'side': self.safe_string(order, 'side'), + 'price': self.safe_number(info, 'fillPx'), + 'amount': self.safe_number(info, 'fillSz'), + 'cost': self.safe_number(order, 'cost'), + 'fee': { + 'cost': self.safe_number(info, 'fillFee'), + 'currency': self.safe_currency_code(feeMarketId), + }, + }, market) + + async def watch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + watches information on multiple trades made by the user + + https://www.okx.com/docs-v5/en/#order-book-trading-trade-ws-order-channel + + :param str [symbol]: unified market symbol of the market trades were made in + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trade structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.trigger]: True if fetching trigger or conditional trades + :param str [params.type]: 'spot', 'swap', 'future', 'option', 'ANY', 'SPOT', 'MARGIN', 'SWAP', 'FUTURES' or 'OPTION' + :param str [params.marginMode]: 'cross' or 'isolated', for automatically setting the type to spot margin + :returns dict[]: a list of `trade structures ` + """ + # By default, receive order updates from any instrument type + type = None + type, params = self.handle_option_and_params(params, 'watchMyTrades', 'type', 'ANY') + isTrigger = self.safe_bool_2(params, 'trigger', 'stop', False) + params = self.omit(params, ['trigger', 'stop']) + await self.load_markets() + await self.authenticate({'access': 'business' if isTrigger else 'private'}) + channel = 'orders-algo' if isTrigger else 'orders' + messageHash = channel + '::myTrades' + market = None + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + type = market['type'] + messageHash = messageHash + '::' + symbol + if type == 'future': + type = 'futures' + uppercaseType = type.upper() + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('watchMyTrades', params) + if uppercaseType == 'SPOT': + if marginMode is not None: + uppercaseType = 'MARGIN' + request: dict = { + 'instType': uppercaseType, + } + orders = await self.subscribe('private', messageHash, channel, None, self.extend(request, params)) + if self.newUpdates: + limit = orders.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(orders, symbol, since, limit, True) + + async def watch_positions(self, symbols: Strings = None, since: Int = None, limit: Int = None, params={}) -> List[Position]: + """ + + https://www.okx.com/docs-v5/en/#trading-account-websocket-positions-channel + + watch all open positions + :param str[]|None symbols: list of unified market symbols + @param since + @param limit + :param dict params: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + await self.load_markets() + await self.authenticate(params) + symbols = self.market_symbols(symbols) + request: dict = { + 'instType': 'ANY', + } + channel = 'positions' + newPositions = None + if symbols is None: + arg: dict = { + 'channel': 'positions', + 'instType': 'ANY', + } + args = [self.extend(arg, params)] + nonSymbolRequest: dict = { + 'op': 'subscribe', + 'args': args, + } + url = self.get_url(channel, 'private') + newPositions = await self.watch(url, channel, nonSymbolRequest, channel) + else: + newPositions = await self.subscribe_multiple('private', channel, symbols, self.extend(request, params)) + if self.newUpdates: + return newPositions + return self.filter_by_symbols_since_limit(self.positions, symbols, since, limit, True) + + def handle_positions(self, client, message): + # + # { + # arg: { + # channel: 'positions', + # instType: 'ANY', + # instId: 'XRP-USDT-SWAP', + # uid: '464737184507959869' + # }, + # data: [{ + # adl: '1', + # availPos: '', + # avgPx: '0.52668', + # baseBal: '', + # baseBorrowed: '', + # baseInterest: '', + # bizRefId: '', + # bizRefType: '', + # cTime: '1693151444408', + # ccy: 'USDT', + # closeOrderAlgo: [], + # deltaBS: '', + # deltaPA: '', + # gammaBS: '', + # gammaPA: '', + # idxPx: '0.52683', + # imr: '17.564000000000004', + # instId: 'XRP-USDT-SWAP', + # instType: 'SWAP', + # interest: '', + # last: '0.52691', + # lever: '3', + # liab: '', + # liabCcy: '', + # liqPx: '0.3287514731020614', + # margin: '', + # markPx: '0.52692', + # mgnMode: 'cross', + # mgnRatio: '69.00363001456147', + # mmr: '0.26346', + # notionalUsd: '52.68620388000001', + # optVal: '', + # pTime: '1693151906023', + # pendingCloseOrdLiabVal: '', + # pos: '1', + # posCcy: '', + # posId: '616057041198907393', + # posSide: 'net', + # quoteBal: '', + # quoteBorrowed: '', + # quoteInterest: '', + # spotInUseAmt: '', + # spotInUseCcy: '', + # thetaBS: '', + # thetaPA: '', + # tradeId: '138745402', + # uTime: '1693151444408', + # upl: '0.0240000000000018', + # uplLastPx: '0.0229999999999952', + # uplRatio: '0.0013670539986328', + # uplRatioLastPx: '0.001310093415356', + # usdPx: '', + # vegaBS: '', + # vegaPA: '' + # }] + # } + # + arg = self.safe_value(message, 'arg', {}) + marketId = self.safe_string(arg, 'instId') + market = self.safe_market(marketId, None, '-') + symbol = market['symbol'] + channel = self.safe_string(arg, 'channel', '') + data = self.safe_value(message, 'data', []) + if self.positions is None: + self.positions = ArrayCacheBySymbolBySide() + cache = self.positions + newPositions = [] + for i in range(0, len(data)): + rawPosition = data[i] + position = self.parse_position(rawPosition) + if position['contracts'] == 0: + position['side'] = 'long' + shortPosition = self.clone(position) + shortPosition['side'] = 'short' + cache.append(shortPosition) + newPositions.append(shortPosition) + newPositions.append(position) + cache.append(position) + messageHash = channel + if symbol is not None: + messageHash = channel + '::' + symbol + client.resolve(newPositions, messageHash) + + async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + watches information on multiple orders made by the user + + https://www.okx.com/docs-v5/en/#order-book-trading-trade-ws-order-channel + + :param str [symbol]: unified market symbol of the market the orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.trigger]: True if fetching trigger or conditional orders + :param str [params.type]: 'spot', 'swap', 'future', 'option', 'ANY', 'SPOT', 'MARGIN', 'SWAP', 'FUTURES' or 'OPTION' + :param str [params.marginMode]: 'cross' or 'isolated', for automatically setting the type to spot margin + :returns dict[]: a list of `order structures ` + """ + type = None + # By default, receive order updates from any instrument type + type, params = self.handle_option_and_params(params, 'watchOrders', 'type', 'ANY') + isTrigger = self.safe_value_2(params, 'stop', 'trigger', False) + params = self.omit(params, ['stop', 'trigger']) + await self.load_markets() + await self.authenticate({'access': 'business' if isTrigger else 'private'}) + market = None + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + type = market['type'] + if type == 'future': + type = 'futures' + uppercaseType = type.upper() + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('watchOrders', params) + if uppercaseType == 'SPOT': + if marginMode is not None: + uppercaseType = 'MARGIN' + request: dict = { + 'instType': uppercaseType, + } + channel = 'orders-algo' if isTrigger else 'orders' + orders = await self.subscribe('private', channel, channel, symbol, self.extend(request, params)) + if self.newUpdates: + limit = orders.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(orders, symbol, since, limit, True) + + def handle_orders(self, client: Client, message, subscription=None): + # + # { + # "arg":{ + # "channel":"orders", + # "instType":"SPOT" + # }, + # "data":[ + # { + # "accFillSz":"0", + # "amendResult":"", + # "avgPx":"", + # "cTime":"1634548275191", + # "category":"normal", + # "ccy":"", + # "clOrdId":"e847386590ce4dBC330547db94a08ba0", + # "code":"0", + # "execType":"", + # "fee":"0", + # "feeCcy":"USDT", + # "fillFee":"0", + # "fillFeeCcy":"", + # "fillNotionalUsd":"", + # "fillPx":"", + # "fillSz":"0", + # "fillTime":"", + # "instId":"ETH-USDT", + # "instType":"SPOT", + # "lever":"", + # "msg":"", + # "notionalUsd":"451.4516256", + # "ordId":"370257534141235201", + # "ordType":"limit", + # "pnl":"0", + # "posSide":"", + # "px":"60000", + # "rebate":"0", + # "rebateCcy":"ETH", + # "reqId":"", + # "side":"sell", + # "slOrdPx":"", + # "slTriggerPx":"", + # "state":"live", + # "sz":"0.007526", + # "tag":"", + # "tdMode":"cash", + # "tgtCcy":"", + # "tpOrdPx":"", + # "tpTriggerPx":"", + # "tradeId":"", + # "uTime":"1634548275191" + # } + # ] + # } + # + self.handle_my_trades(client, message) + arg = self.safe_value(message, 'arg', {}) + channel = self.safe_string(arg, 'channel') + orders = self.safe_value(message, 'data', []) + ordersLength = len(orders) + if ordersLength > 0: + limit = self.safe_integer(self.options, 'ordersLimit', 1000) + if self.orders is None: + self.orders = ArrayCacheBySymbolById(limit) + self.triggerOrders = ArrayCacheBySymbolById(limit) + stored = self.triggerOrders if (channel == 'orders-algo') else self.orders + marketIds = [] + parsed = self.parse_orders(orders) + for i in range(0, len(parsed)): + order = parsed[i] + stored.append(order) + symbol = order['symbol'] + market = self.market(symbol) + marketIds.append(market['id']) + client.resolve(stored, channel) + for i in range(0, len(marketIds)): + messageHash = channel + ':' + marketIds[i] + client.resolve(stored, messageHash) + + def handle_my_trades(self, client: Client, message): + # + # { + # "arg":{ + # "channel":"orders", + # "instType":"SPOT" + # }, + # "data":[ + # { + # "accFillSz":"0", + # "amendResult":"", + # "avgPx":"", + # "cTime":"1634548275191", + # "category":"normal", + # "ccy":"", + # "clOrdId":"e847386590ce4dBC330547db94a08ba0", + # "code":"0", + # "execType":"", + # "fee":"0", + # "feeCcy":"USDT", + # "fillFee":"0", + # "fillFeeCcy":"", + # "fillNotionalUsd":"", + # "fillPx":"", + # "fillSz":"0", + # "fillTime":"", + # "instId":"ETH-USDT", + # "instType":"SPOT", + # "lever":"", + # "msg":"", + # "notionalUsd":"451.4516256", + # "ordId":"370257534141235201", + # "ordType":"limit", + # "pnl":"0", + # "posSide":"", + # "px":"60000", + # "rebate":"0", + # "rebateCcy":"ETH", + # "reqId":"", + # "side":"sell", + # "slOrdPx":"", + # "slTriggerPx":"", + # "state":"live", + # "sz":"0.007526", + # "tag":"", + # "tdMode":"cash", + # "tgtCcy":"", + # "tpOrdPx":"", + # "tpTriggerPx":"", + # "tradeId":"", + # "uTime":"1634548275191" + # } + # ] + # } + # + arg = self.safe_value(message, 'arg', {}) + channel = self.safe_string(arg, 'channel') + rawOrders = self.safe_value(message, 'data', []) + filteredOrders = [] + # filter orders with no last trade id + for i in range(0, len(rawOrders)): + rawOrder = rawOrders[i] + tradeId = self.safe_string(rawOrder, 'tradeId', '') + if len(tradeId) > 0: + order = self.parse_order(rawOrder) + filteredOrders.append(order) + tradesLength = len(filteredOrders) + if tradesLength == 0: + return + if self.myTrades is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + self.myTrades = ArrayCacheBySymbolById(limit) + myTrades = self.myTrades + symbols: dict = {} + for i in range(0, len(filteredOrders)): + rawTrade = filteredOrders[i] + trade = self.order_to_trade(rawTrade) + myTrades.append(trade) + symbol = trade['symbol'] + symbols[symbol] = True + messageHash = channel + '::myTrades' + client.resolve(self.myTrades, messageHash) + tradeSymbols = list(symbols.keys()) + for i in range(0, len(tradeSymbols)): + symbolMessageHash = messageHash + '::' + tradeSymbols[i] + client.resolve(self.myTrades, symbolMessageHash) + + def request_id(self): + ts = str(self.milliseconds()) + randomNumber = self.rand_number(4) + randomPart = str(randomNumber) + return ts + randomPart + + async def create_order_ws(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}) -> Order: + """ + + https://www.okx.com/docs-v5/en/#websocket-api-trade-place-order + + create a trade order + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float|None [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean params['test']: test order, default False + :returns dict: an `order structure ` + """ + await self.load_markets() + await self.authenticate() + url = self.get_url('private', 'private') + messageHash = self.request_id() + op = None + op, params = self.handle_option_and_params(params, 'createOrderWs', 'op', 'batch-orders') + args = self.create_order_request(symbol, type, side, amount, price, params) + ordType = self.safe_string(args, 'ordType') + if (ordType == 'trigger') or (ordType == 'conditional') or (type == 'oco') or (type == 'move_order_stop') or (type == 'iceberg') or (type == 'twap'): + raise BadRequest(self.id + ' createOrderWs() does not support algo trading. self.options["createOrderWs"]["op"] must be either order or batch-order') + if (op != 'order') and (op != 'batch-orders'): + raise BadRequest(self.id + ' createOrderWs() does not support algo trading. self.options["createOrderWs"]["op"] must be either order or privatePostTradeOrder or privatePostTradeOrderAlgo') + request: dict = { + 'id': messageHash, + 'op': op, + 'args': [args], + } + return await self.watch(url, messageHash, request, messageHash) + + def handle_place_orders(self, client: Client, message): + # + # batch-orders/order/cancel-order + # { + # "id": "1689281055", + # "op": "batch-orders", + # "code": "0", + # "msg": '', + # "data": [{ + # "tag": "e847386590ce4dBC", + # "ordId": "599823446566084608", + # "clOrdId": "e847386590ce4dBCb939511604f394b0", + # "sCode": "0", + # "sMsg": "Order successfully placed." + # }, + # ... + # ] + # } + # + messageHash = self.safe_string(message, 'id') + args = self.safe_value(message, 'data', []) + # filter out partial errors + args = self.filter_by(args, 'sCode', '0') + # if empty means request failed and handle error + if self.is_empty(args): + method = self.safe_string(message, 'op') + stringMsg = self.json(message) + self.handle_errors(1, '', client.url, method, {}, stringMsg, message, {}, {}) + orders = self.parse_orders(args, None, None, None) + first = self.safe_dict(orders, 0, {}) + client.resolve(first, messageHash) + + async def edit_order_ws(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}) -> Order: + """ + edit a trade order + + https://www.okx.com/docs-v5/en/#order-book-trading-trade-ws-amend-order + https://www.okx.com/docs-v5/en/#order-book-trading-trade-ws-amend-multiple-orders + + :param str id: order id + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of the currency you want to trade in units of the base currency + :param float|None [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + await self.authenticate() + url = self.get_url('private', 'private') + messageHash = self.request_id() + op = None + op, params = self.handle_option_and_params(params, 'editOrderWs', 'op', 'amend-order') + args = self.edit_order_request(id, symbol, type, side, amount, price, params) + request: dict = { + 'id': messageHash, + 'op': op, + 'args': [args], + } + return await self.watch(url, messageHash, self.extend(request, params), messageHash) + + async def cancel_order_ws(self, id: str, symbol: Str = None, params={}) -> Order: + """ + + https://okx-docs.github.io/apidocs/websocket_api/en/#cancel-order-trade + + cancel multiple orders + :param str id: order id + :param str symbol: unified market symbol, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.clOrdId]: client order id + :returns dict: an list of `order structures ` + """ + if symbol is None: + raise BadRequest(self.id + ' cancelOrderWs() requires a symbol argument') + await self.load_markets() + await self.authenticate() + url = self.get_url('private', 'private') + messageHash = self.request_id() + clientOrderId = self.safe_string_2(params, 'clOrdId', 'clientOrderId') + params = self.omit(params, ['clientOrderId', 'clOrdId']) + arg: dict = { + 'instId': self.market_id(symbol), + } + if clientOrderId is not None: + arg['clOrdId'] = clientOrderId + else: + arg['ordId'] = id + request: dict = { + 'id': messageHash, + 'op': 'cancel-order', + 'args': [self.extend(arg, params)], + } + return await self.watch(url, messageHash, request, messageHash) + + async def cancel_orders_ws(self, ids: List[str], symbol: Str = None, params={}): + """ + + https://www.okx.com/docs-v5/en/#order-book-trading-trade-ws-mass-cancel-order + + cancel multiple orders + :param str[] ids: order ids + :param str symbol: unified market symbol, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an list of `order structures ` + """ + idsLength: number = len(ids) + if idsLength > 20: + raise BadRequest(self.id + ' cancelOrdersWs() accepts up to 20 ids at a time') + if symbol is None: + raise BadRequest(self.id + ' cancelOrdersWs() requires a symbol argument') + await self.load_markets() + await self.authenticate() + url = self.get_url('private', 'private') + messageHash = self.request_id() + args = [] + for i in range(0, idsLength): + arg: dict = { + 'instId': self.market_id(symbol), + 'ordId': ids[i], + } + args.append(arg) + request: dict = { + 'id': messageHash, + 'op': 'batch-cancel-orders', + 'args': args, + } + return await self.watch(url, messageHash, self.deep_extend(request, params), messageHash) + + async def cancel_all_orders_ws(self, symbol: Str = None, params={}) -> List[Order]: + """ + + https://docs.okx.com/websockets/#message-cancelAll + + cancel all open orders of a type. Only applicable to Option in Portfolio Margin mode, and MMP privilege is required. + :param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + if symbol is None: + raise BadRequest(self.id + ' cancelAllOrdersWs() requires a symbol argument') + await self.load_markets() + await self.authenticate() + market = self.market(symbol) + if market['type'] != 'option': + raise BadRequest(self.id + ' cancelAllOrdersWs is only applicable to Option in Portfolio Margin mode, and MMP privilege is required.') + url = self.get_url('private', 'private') + messageHash = self.request_id() + request: dict = { + 'id': messageHash, + 'op': 'mass-cancel', + 'args': [self.extend({ + 'instType': 'OPTION', + 'instFamily': market['id'], + }, params)], + } + return await self.watch(url, messageHash, request, messageHash) + + def handle_cancel_all_orders(self, client: Client, message): + # + # { + # "id": "1512", + # "op": "mass-cancel", + # "data": [ + # { + # "result": True + # } + # ], + # "code": "0", + # "msg": "" + # } + # + messageHash = self.safe_string(message, 'id') + data = self.safe_value(message, 'data', []) + client.resolve(data, messageHash) + + def handle_subscription_status(self, client: Client, message): + # + # {event: 'subscribe', arg: {channel: "tickers", instId: "BTC-USDT"}} + # + # channel = self.safe_string(message, "channel") + # client.subscriptions[channel] = message + return message + + def handle_authenticate(self, client: Client, message): + # + # {event: "login", success: True} + # + future = self.safe_value(client.futures, 'authenticated') + future.resolve(True) + + def ping(self, client: Client): + # OKX does not support the built-in WebSocket protocol-level ping-pong. + # Instead, it requires a custom text-based ping-pong mechanism. + return 'ping' + + def handle_pong(self, client: Client, message): + client.lastPong = self.milliseconds() + return message + + def handle_error_message(self, client: Client, message) -> Bool: + # + # {event: 'error', msg: "Illegal request: {"op":"subscribe","args":["spot/ticker:BTC-USDT"]}", code: "60012"} + # {event: 'error", msg: "channel:ticker,instId:BTC-USDT doesn"t exist", code: "60018"} + # {"event":"error","msg":"Illegal request: {\\"id\\":\\"17321173472466905\\",\\"op\\":\\"amend-order\\",\\"args\\":[{\\"instId\\":\\"ETH-USDC\\",\\"ordId\\":\\"2000345622407479296\\",\\"newSz\\":\\"0.050857\\",\\"newPx\\":\\"2949.4\\",\\"postOnly\\":true}],\\"postOnly\\":true}","code":"60012","connId":"0808af6c"} + # + errorCode = self.safe_string(message, 'code') + try: + if errorCode and errorCode != '0': + feedback = self.id + ' ' + self.json(message) + if errorCode != '1': + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + messageString = self.safe_value(message, 'msg') + if messageString is not None: + self.throw_broadly_matched_exception(self.exceptions['broad'], messageString, feedback) + else: + data = self.safe_list(message, 'data', []) + for i in range(0, len(data)): + d = data[i] + errorCode = self.safe_string(d, 'sCode') + if errorCode is not None: + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + messageString = self.safe_value(message, 'sMsg') + if messageString is not None: + self.throw_broadly_matched_exception(self.exceptions['broad'], messageString, feedback) + raise ExchangeError(feedback) + except Exception as e: + # if the message contains an id, it means it is a response to a request + # so we only reject that promise, instead of deleting all futures, destroying the authentication future + id = self.safe_string(message, 'id') + if id is None: + # try to parse it from the stringified json inside msg + msg = self.safe_string(message, 'msg') + if msg is not None and msg.startswith('Illegal request: {'): + stringifiedJson = msg.replace('Illegal request: ', '') + parsedJson = self.parse_json(stringifiedJson) + id = self.safe_string(parsedJson, 'id') + if id is not None: + client.reject(e, id) + return False + client.reject(e) + return False + return True + + def handle_message(self, client: Client, message): + if not self.handle_error_message(client, message): + return + # + # {event: 'subscribe', arg: {channel: "tickers", instId: "BTC-USDT"}} + # {event: 'login", msg: '", code: "0"} + # + # { + # "arg": {channel: "tickers", instId: "BTC-USDT"}, + # "data": [ + # { + # "instType": "SPOT", + # "instId": "BTC-USDT", + # "last": "31500.1", + # "lastSz": "0.00001754", + # "askPx": "31500.1", + # "askSz": "0.00998144", + # "bidPx": "31500", + # "bidSz": "3.05652439", + # "open24h": "31697", + # "high24h": "32248", + # "low24h": "31165.6", + # "sodUtc0": "31385.5", + # "sodUtc8": "32134.9", + # "volCcy24h": "503403597.38138519", + # "vol24h": "15937.10781721", + # "ts": "1626526618762" + # } + # ] + # } + # + # {event: 'error', msg: "Illegal request: {"op":"subscribe","args":["spot/ticker:BTC-USDT"]}", code: "60012"} + # {event: 'error", msg: "channel:ticker,instId:BTC-USDT doesn"t exist", code: "60018"} + # {event: 'error', msg: "Invalid OK_ACCESS_KEY", code: "60005"} + # { + # "event": "error", + # "msg": "Illegal request: {"op":"login","args":["de89b035-b233-44b2-9a13-0ccdd00bda0e","7KUcc8YzQhnxBE3K","1626691289","H57N99mBt5NvW8U19FITrPdOxycAERFMaapQWRqLaSE="]}", + # "code": "60012" + # } + # + # + # + if message == 'pong': + self.handle_pong(client, message) + return + # table = self.safe_string(message, 'table') + # if table is None: + event = self.safe_string_2(message, 'event', 'op') + if event is not None: + methods: dict = { + # 'info': self.handleSystemStatus, + # 'book': 'handleOrderBook', + 'login': self.handle_authenticate, + 'subscribe': self.handle_subscription_status, + 'unsubscribe': self.handle_unsubscription, + 'order': self.handle_place_orders, + 'batch-orders': self.handle_place_orders, + 'amend-order': self.handle_place_orders, + 'batch-amend-orders': self.handle_place_orders, + 'cancel-order': self.handle_place_orders, + 'mass-cancel': self.handle_cancel_all_orders, + } + method = self.safe_value(methods, event) + if method is not None: + method(client, message) + else: + arg = self.safe_value(message, 'arg', {}) + channel = self.safe_string(arg, 'channel') + methods: dict = { + 'bbo-tbt': self.handle_order_book, # newly added channel that sends tick-by-tick Level 1 data, all API users can subscribe, public depth channel, verification not required + 'books': self.handle_order_book, # all API users can subscribe, public depth channel, verification not required + 'books5': self.handle_order_book, # all API users can subscribe, public depth channel, verification not required, data feeds will be delivered every 100ms(vs. every 200ms now) + 'books50-l2-tbt': self.handle_order_book, # only users who're VIP4 and above can subscribe, identity verification required before subscription + 'books-l2-tbt': self.handle_order_book, # only users who're VIP5 and above can subscribe, identity verification required before subscription + 'tickers': self.handle_ticker, + 'mark-price': self.handle_ticker, + 'positions': self.handle_positions, + 'index-tickers': self.handle_ticker, + 'sprd-tickers': self.handle_ticker, + 'block-tickers': self.handle_ticker, + 'trades': self.handle_trades, + 'trades-all': self.handle_trades, + 'account': self.handle_balance, + 'funding-rate': self.handle_funding_rate, + # 'margin_account': self.handle_balance, + 'orders': self.handle_orders, + 'orders-algo': self.handle_orders, + 'liquidation-orders': self.handle_liquidation, + 'balance_and_position': self.handle_balance_and_position, + } + method = self.safe_value(methods, channel) + if method is None: + if channel.find('candle') == 0: + self.handle_ohlcv(client, message) + else: + method(client, message) + + def handle_un_subscription_trades(self, client: Client, symbol: str, channel: str): + subMessageHash = channel + ':' + symbol + messageHash = 'unsubscribe:' + subMessageHash + self.clean_unsubscription(client, subMessageHash, messageHash) + if symbol in self.trades: + del self.trades[symbol] + + def handle_unsubscription_order_book(self, client: Client, symbol: str, channel: str): + subMessageHash = channel + ':' + symbol + messageHash = 'unsubscribe:orderbook:' + symbol + self.clean_unsubscription(client, subMessageHash, messageHash) + if symbol in self.orderbooks: + del self.orderbooks[symbol] + + def handle_unsubscription_ohlcv(self, client: Client, symbol: str, channel: str): + tf = channel.replace('candle', '') + timeframe = self.find_timeframe(tf) + subMessageHash = 'multi:' + channel + ':' + symbol + messageHash = 'unsubscribe:' + subMessageHash + self.clean_unsubscription(client, subMessageHash, messageHash) + if timeframe in self.ohlcvs[symbol]: + del self.ohlcvs[symbol][timeframe] + + def handle_unsubscription_ticker(self, client: Client, symbol: str, channel): + subMessageHash = channel + '::' + symbol + messageHash = 'unsubscribe:ticker:' + symbol + self.clean_unsubscription(client, subMessageHash, messageHash) + if symbol in self.tickers: + del self.tickers[symbol] + + def handle_unsubscription(self, client: Client, message): + # + # { + # "event": "unsubscribe", + # "arg": { + # "channel": "tickers", + # "instId": "LTC-USD-200327" + # }, + # "connId": "a4d3ae55" + # } + # arg might be an array or list + arg = self.safe_dict(message, 'arg', {}) + channel = self.safe_string(arg, 'channel', '') + marketId = self.safe_string(arg, 'instId') + symbol = self.safe_symbol(marketId) + if channel == 'trades' or channel == 'trades-all': + self.handle_un_subscription_trades(client, symbol, channel) + elif channel.startswith('bbo') or channel.startswith('book'): + self.handle_unsubscription_order_book(client, symbol, channel) + elif channel.find('tickers') > -1: + self.handle_unsubscription_ticker(client, symbol, channel) + elif channel.startswith('candle'): + self.handle_unsubscription_ohlcv(client, symbol, channel) diff --git a/ccxt/pro/okxus.py b/ccxt/pro/okxus.py new file mode 100644 index 0000000..5ebc0ba --- /dev/null +++ b/ccxt/pro/okxus.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.pro.okx import okx +from ccxt.base.types import Any + + +class okxus(okx): + + def describe(self) -> Any: + return self.deep_extend(super(okxus, self).describe(), { + 'id': 'okxus', + 'name': 'OKX(US)', + 'hostname': 'us.okx.com', + 'urls': { + 'api': { + 'rest': 'https://{hostname}', + 'ws': 'wss://wsus.okx.com:8443/ws/v5', + }, + 'www': 'https://app.okx.com', + 'doc': 'https://app.okx.com/docs-v5/en/#overview', + 'fees': 'https://app.okx.com/pages/products/fees.html', + 'referral': { + 'url': 'https://www.app.okx.com/join/CCXT2023', + 'discount': 0.2, + }, + 'test': { + 'ws': 'wss://wsuspap.okx.com:8443/ws/v5', + }, + }, + 'has': { + 'swap': False, + 'future': False, + 'option': False, + }, + }) diff --git a/ccxt/pro/onetrading.py b/ccxt/pro/onetrading.py new file mode 100644 index 0000000..7543a37 --- /dev/null +++ b/ccxt/pro/onetrading.py @@ -0,0 +1,1292 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCacheBySymbolById, ArrayCacheByTimestamp +from ccxt.base.types import Any, Balances, Bool, Int, Order, OrderBook, Str, Strings, Ticker, Tickers, Trade +from ccxt.async_support.base.ws.client import Client +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import NotSupported +from ccxt.base.precise import Precise + + +class onetrading(ccxt.async_support.onetrading): + + def describe(self) -> Any: + return self.deep_extend(super(onetrading, self).describe(), { + 'has': { + 'ws': True, + 'watchBalance': True, + 'watchTicker': True, + 'watchTickers': True, + 'watchTrades': False, + 'watchTradesForSymbols': False, + 'watchMyTrades': True, + 'watchOrders': True, + 'watchOrderBook': True, + 'watchOHLCV': True, + }, + 'urls': { + 'api': { + 'ws': 'wss://streams.onetrading.com/', + }, + }, + 'options': { + 'bp_remaining_quota': 200, + 'timeframes': { + '1m': { + 'unit': 'MINUTES', + 'period': 1, + }, + '5m': { + 'unit': 'MINUTES', + 'period': 5, + }, + '15m': { + 'unit': 'MINUTES', + 'period': 15, + }, + '30m': { + 'unit': 'MINUTES', + 'period': 30, + }, + '1h': { + 'unit': 'HOURS', + 'period': 1, + }, + '4h': { + 'unit': 'HOURS', + 'period': 4, + }, + '1d': { + 'unit': 'DAYS', + 'period': 1, + }, + '1w': { + 'unit': 'WEEKS', + 'period': 1, + }, + '1M': { + 'unit': 'MONTHS', + 'period': 1, + }, + }, + }, + 'streaming': { + }, + 'exceptions': { + }, + }) + + async def watch_balance(self, params={}) -> Balances: + """ + + https://developers.bitpanda.com/exchange/#account-history-channel + + watch balance and get the amount of funds available for trading or funds locked in orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.authenticate(params) + url = self.urls['api']['ws'] + messageHash = 'balance' + subscribeHash = 'ACCOUNT_HISTORY' + bpRemainingQuota = self.safe_integer(self.options, 'bp_remaining_quota', 200) + subscribe: dict = { + 'type': 'SUBSCRIBE', + 'bp_remaining_quota': bpRemainingQuota, + 'channels': [ + { + 'name': 'ACCOUNT_HISTORY', + }, + ], + } + request = self.deep_extend(subscribe, params) + return await self.watch(url, messageHash, request, subscribeHash, request) + + def handle_balance_snapshot(self, client, message): + # + # snapshot + # { + # "account_id": "b355abb8-aaae-4fae-903c-c60ff74723c6", + # "type": "BALANCES_SNAPSHOT", + # "channel_name": "ACCOUNT_HISTORY", + # "time": "2019-04-01T13:39:17.155Z", + # "balances": [{ + # "account_id": "b355abb8-aaae-4fae-903c-c60ff74723c6", + # "currency_code": "BTC", + # "change": "0.5", + # "available": "10.0", + # "locked": "1.1234567", + # "sequence": 1, + # "time": "2019-04-01T13:39:17.155Z" + # }, + # { + # "account_id": "b355abb8-aaae-4fae-903c-c60ff74723c6", + # "currency_code": "ETH", + # "change": "0.5", + # "available": "10.0", + # "locked": "1.1234567", + # "sequence": 2, + # "time": "2019-04-01T13:39:17.155Z" + # } + # ] + # } + # + self.balance = self.parse_balance(message) + messageHash = 'balance' + client.resolve(self.balance, messageHash) + + async def watch_ticker(self, symbol: str, params={}) -> Ticker: + """ + + https://developers.bitpanda.com/exchange/#market-ticker-channel + + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + subscriptionHash = 'MARKET_TICKER' + messageHash = 'ticker.' + symbol + request: dict = { + 'type': 'SUBSCRIBE', + 'channels': [ + { + 'name': 'MARKET_TICKER', + 'price_points_mode': 'INLINE', + }, + ], + } + return await self.watch_many(messageHash, request, subscriptionHash, [symbol], params) + + async def watch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + + https://developers.bitpanda.com/exchange/#market-ticker-channel + + watches price tickers, a statistical calculation with the information for all markets or those specified. + :param str symbols: unified symbols of the markets to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an array of `ticker structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + if symbols is None: + symbols = [] + subscriptionHash = 'MARKET_TICKER' + messageHash = 'tickers' + request: dict = { + 'type': 'SUBSCRIBE', + 'channels': [ + { + 'name': 'MARKET_TICKER', + 'price_points_mode': 'INLINE', + }, + ], + } + tickers = await self.watch_many(messageHash, request, subscriptionHash, symbols, params) + return self.filter_by_array(tickers, 'symbol', symbols) + + def handle_ticker(self, client: Client, message): + # + # { + # "ticker_updates": [{ + # "instrument": "ETH_BTC", + # "last_price": "0.053752", + # "price_change": "0.000623", + # "price_change_percentage": "1.17", + # "high": "0.055", + # "low": "0.052662", + # "volume": "6.3821593247" + # }], + # "channel_name": "MARKET_TICKER", + # "type": "MARKET_TICKER_UPDATES", + # "time": "2022-06-23T16:41:00.004162Z" + # } + # + tickers = self.safe_value(message, 'ticker_updates', []) + datetime = self.safe_string(message, 'time') + for i in range(0, len(tickers)): + ticker = tickers[i] + marketId = self.safe_string(ticker, 'instrument') + symbol = self.safe_symbol(marketId) + self.tickers[symbol] = self.parse_ws_ticker(ticker) + timestamp = self.parse8601(datetime) + self.tickers[symbol]['timestamp'] = timestamp + self.tickers[symbol]['datetime'] = self.iso8601(timestamp) + client.resolve(self.tickers[symbol], 'ticker.' + symbol) + client.resolve(self.tickers, 'tickers') + + def parse_ws_ticker(self, ticker, market=None): + # + # { + # "instrument": "ETH_BTC", + # "last_price": "0.053752", + # "price_change": "-0.000623", + # "price_change_percentage": "-1.17", + # "high": "0.055", + # "low": "0.052662", + # "volume": "6.3821593247" + # } + # + marketId = self.safe_string(ticker, 'instrument') + return self.safe_ticker({ + 'symbol': self.safe_symbol(marketId, market), + 'timestamp': None, + 'datetime': None, + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': None, + 'bidVolume': None, + 'ask': None, + 'askVolume': None, + 'vwap': None, + 'open': None, + 'close': self.safe_string(ticker, 'last_price'), + 'last': self.safe_string(ticker, 'last_price'), + 'previousClose': None, + 'change': self.safe_string(ticker, 'price_change'), + 'percentage': self.safe_string(ticker, 'price_change_percentage'), + 'average': None, + 'baseVolume': None, + 'quoteVolume': self.safe_number(ticker, 'volume'), + 'info': ticker, + }, market) + + async def watch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + + https://developers.bitpanda.com/exchange/#account-history-channel + + get the list of trades associated with the user + :param str symbol: unified symbol of the market to fetch trades for. Use 'any' to watch all trades + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + messageHash = 'myTrades' + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + messageHash += ':' + symbol + await self.authenticate(params) + url = self.urls['api']['ws'] + subscribeHash = 'ACCOUNT_HISTORY' + bpRemainingQuota = self.safe_integer(self.options, 'bp_remaining_quota', 200) + subscribe: dict = { + 'type': 'SUBSCRIBE', + 'bp_remaining_quota': bpRemainingQuota, + 'channels': [ + { + 'name': 'ACCOUNT_HISTORY', + }, + ], + } + request = self.deep_extend(subscribe, params) + trades = await self.watch(url, messageHash, request, subscribeHash, request) + if self.newUpdates: + limit = trades.getLimit(symbol, limit) + trades = self.filter_by_symbol_since_limit(trades, symbol, since, limit) + numTrades = len(trades) + if numTrades == 0: + return await self.watch_my_trades(symbol, since, limit, params) + return trades + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + + https://developers.bitpanda.com/exchange/#market-ticker-channel + + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + messageHash = 'book:' + symbol + subscriptionHash = 'ORDER_BOOK' + depth = 0 + if limit is not None: + depth = limit + request: dict = { + 'type': 'SUBSCRIBE', + 'channels': [ + { + 'name': 'ORDER_BOOK', + 'depth': depth, + }, + ], + } + orderbook = await self.watch_many(messageHash, request, subscriptionHash, [symbol], params) + return orderbook.limit() + + def handle_order_book(self, client: Client, message): + # + # snapshot + # { + # "instrument_code": "ETH_BTC", + # "bids": [ + # ['0.053595', "4.5352"], + # ... + # ], + # "asks": [ + # ['0.055455', "0.2821"], + # ... + # ], + # "channel_name": "ORDER_BOOK", + # "type": "ORDER_BOOK_SNAPSHOT", + # "time": "2022-06-23T15:38:02.196282Z" + # } + # + # update + # { + # "instrument_code": "ETH_BTC", + # "changes": [ + # ["BUY", '0.053593', "8.0587"] + # ], + # "channel_name": "ORDER_BOOK", + # "type": "ORDER_BOOK_UPDATE", + # "time": "2022-06-23T15:38:02.751301Z" + # } + # + type = self.safe_string(message, 'type') + marketId = self.safe_string(message, 'instrument_code') + symbol = self.safe_symbol(marketId) + dateTime = self.safe_string(message, 'time') + timestamp = self.parse8601(dateTime) + channel = 'book:' + symbol + orderbook = self.safe_value(self.orderbooks, symbol) + if orderbook is None: + orderbook = self.order_book({}) + if type == 'ORDER_BOOK_SNAPSHOT': + snapshot = self.parse_order_book(message, symbol, timestamp, 'bids', 'asks') + orderbook.reset(snapshot) + elif type == 'ORDER_BOOK_UPDATE': + changes = self.safe_value(message, 'changes', []) + self.handle_deltas(orderbook, changes) + else: + raise NotSupported(self.id + ' watchOrderBook() did not recognize message type ' + type) + orderbook['nonce'] = timestamp + orderbook['timestamp'] = timestamp + orderbook['datetime'] = self.iso8601(timestamp) + self.orderbooks[symbol] = orderbook + client.resolve(orderbook, channel) + + def handle_delta(self, orderbook, delta): + # + # ['BUY', "0.053595", "0"] + # + bidAsk = self.parse_bid_ask(delta, 1, 2) + type = self.safe_string(delta, 0) + if type == 'BUY': + bids = orderbook['bids'] + bids.storeArray(bidAsk) + elif type == 'SELL': + asks = orderbook['asks'] + asks.storeArray(bidAsk) + else: + raise NotSupported(self.id + ' watchOrderBook() received unknown change type ' + self.json(delta)) + + def handle_deltas(self, orderbook, deltas): + # + # [ + # ['BUY', "0.053593", "0"], + # ['SELL', "0.053698", "0"] + # ] + # + for i in range(0, len(deltas)): + self.handle_delta(orderbook, deltas[i]) + + async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + + https://developers.bitpanda.com/exchange/#account-history-channel + + watches information on multiple orders made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.channel]: can listen to orders using ACCOUNT_HISTORY or TRADING + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + messageHash = 'orders' + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + messageHash += ':' + symbol + await self.authenticate(params) + url = self.urls['api']['ws'] + subscribeHash = self.safe_string(params, 'channel', 'ACCOUNT_HISTORY') + bpRemainingQuota = self.safe_integer(self.options, 'bp_remaining_quota', 200) + subscribe: dict = { + 'type': 'SUBSCRIBE', + 'bp_remaining_quota': bpRemainingQuota, + 'channels': [ + { + 'name': subscribeHash, + }, + ], + } + request = self.deep_extend(subscribe, params) + orders = await self.watch(url, messageHash, request, subscribeHash, request) + if self.newUpdates: + limit = orders.getLimit(symbol, limit) + orders = self.filter_by_symbol_since_limit(orders, symbol, since, limit) + numOrders = len(orders) + if numOrders == 0: + return await self.watch_orders(symbol, since, limit, params) + return orders + + def handle_trading(self, client: Client, message): + # + # { + # "order_book_sequence": 892925263, + # "side": "BUY", + # "amount": "0.00046", + # "trade_id": "d67b9b69-ab76-480f-9ba3-b33582202836", + # "matched_as": "TAKER", + # "matched_amount": "0.00046", + # "matched_price": "22231.08", + # "instrument_code": "BTC_EUR", + # "order_id": "7b39f316-0a71-4bfd-adda-3062e6f0bd37", + # "remaining": "0.0", + # "channel_name": "TRADING", + # "type": "FILL", + # "time": "2022-07-21T12:41:22.883341Z" + # } + # + # { + # "status": "CANCELLED", + # "order_book_sequence": 892928424, + # "amount": "0.0003", + # "side": "SELL", + # "price": "50338.65", + # "instrument_code": "BTC_EUR", + # "order_id": "b3994a08-a9e8-4a79-a08b-33e3480382df", + # "remaining": "0.0003", + # "channel_name": "TRADING", + # "type": "DONE", + # "time": "2022-07-21T12:44:24.267000Z" + # } + # + # { + # "order_book_sequence": 892934476, + # "side": "SELL", + # "amount": "0.00051", + # "price": "22349.02", + # "instrument_code": "BTC_EUR", + # "order_id": "1c6c585c-ec3d-4b94-9292-6c3d04a31dc8", + # "remaining": "0.00051", + # "channel_name": "TRADING", + # "type": "BOOKED", + # "time": "2022-07-21T12:50:10.093000Z" + # } + # + if self.orders is None: + limit = self.safe_integer(self.options, 'ordersLimit', 1000) + self.orders = ArrayCacheBySymbolById(limit) + order = self.parse_trading_order(message) + orders = self.orders + orders.append(order) + client.resolve(self.orders, 'orders:' + order['symbol']) + client.resolve(self.orders, 'orders') + + def parse_trading_order(self, order, market=None): + # + # { + # "order_book_sequence": 892925263, + # "side": "BUY", + # "amount": "0.00046", + # "trade_id": "d67b9b69-ab76-480f-9ba3-b33582202836", + # "matched_as": "TAKER", + # "matched_amount": "0.00046", + # "matched_price": "22231.08", + # "instrument_code": "BTC_EUR", + # "order_id": "7b39f316-0a71-4bfd-adda-3062e6f0bd37", + # "remaining": "0.0", + # "channel_name": "TRADING", + # "type": "FILL", + # "time": "2022-07-21T12:41:22.883341Z" + # } + # + # { + # "status": "CANCELLED", + # "order_book_sequence": 892928424, + # "amount": "0.0003", + # "side": "SELL", + # "price": "50338.65", + # "instrument_code": "BTC_EUR", + # "order_id": "b3994a08-a9e8-4a79-a08b-33e3480382df", + # "remaining": "0.0003", + # "channel_name": "TRADING", + # "type": "DONE", + # "time": "2022-07-21T12:44:24.267000Z" + # } + # + # { + # "order_book_sequence": 892934476, + # "side": "SELL", + # "amount": "0.00051", + # "price": "22349.02", + # "instrument_code": "BTC_EUR", + # "order_id": "1c6c585c-ec3d-4b94-9292-6c3d04a31dc8", + # "remaining": "0.00051", + # "channel_name": "TRADING", + # "type": "BOOKED", + # "time": "2022-07-21T12:50:10.093000Z" + # } + # + # { + # "type":"UPDATE", + # "channel_name": "TRADING", + # "instrument_code": "BTC_EUR", + # "order_id": "1e842f13-762a-4745-9f3b-07f1b43e7058", + # "client_id": "d75fb03b-b599-49e9-b926-3f0b6d103206", + # "time": "2020-01-11T01:01:01.999Z", + # "remaining": "1.23456", + # "order_book_sequence": 42, + # "status": "APPLIED", + # "amount": "1.35756", + # "amount_delta": "0.123", + # "modification_id": "cc0eed67-aecc-4fb4-a625-ff3890ceb4cc" + # } + # tracked + # { + # "type": "STOP_TRACKED", + # "channel_name": "TRADING", + # "instrument_code": "BTC_EUR", + # "order_id": "1e842f13-762a-4745-9f3b-07f1b43e7058", + # "client_id": "d75fb03b-b599-49e9-b926-3f0b6d103206", + # "time": "2020-01-11T01:01:01.999Z", + # "remaining": "1.23456", + # "order_book_sequence": 42, + # "trigger_price": "12345.67", + # "current_price": "11111.11" + # } + # + # { + # "type": "STOP_TRIGGERED", + # "channel_name": "TRADING", + # "instrument_code": "BTC_EUR", + # "order_id": "1e842f13-762a-4745-9f3b-07f1b43e7058", + # "client_id": "d75fb03b-b599-49e9-b926-3f0b6d103206", + # "time": "2020-01-11T01:01:01.999Z", + # "remaining": "1.23456", + # "order_book_sequence": 42, + # "price": "13333.33" + # } + # + datetime = self.safe_string(order, 'time') + marketId = self.safe_string(order, 'instrument_code') + symbol = self.safe_symbol(marketId, market, '_') + return self.safe_order({ + 'id': self.safe_string(order, 'order_id'), + 'clientOrderId': self.safe_string(order, 'client_id'), + 'info': order, + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + 'lastTradeTimestamp': None, + 'symbol': symbol, + 'type': None, + 'timeInForce': None, + 'postOnly': None, + 'side': self.safe_string_lower(order, 'side'), + 'price': self.safe_number_2(order, 'price', 'matched_price'), + 'stopPrice': self.safe_number(order, 'trigger_price'), + 'amount': self.safe_number(order, 'amount'), + 'cost': None, + 'average': None, + 'filled': None, + 'remaining': self.safe_string(order, 'remaining'), + 'status': self.parse_trading_order_status(self.safe_string(order, 'status')), + 'fee': None, + 'trades': None, + }, market) + + def parse_trading_order_status(self, status): + statuses: dict = { + 'CANCELLED': 'canceled', + 'SELF_TRADE': 'rejected', + 'FILLED_FULLY': 'closed', + 'INSUFFICIENT_FUNDS': 'rejected', + 'INSUFFICIENT_LIQUIDITY': 'rejected', + 'TIME_TO_MARKET_EXCEEDED': 'rejected', + 'LAST_PRICE_UNKNOWN': 'rejected', + } + return self.safe_string(statuses, status, status) + + def handle_orders(self, client: Client, message): + # + # snapshot + # { + # "account_id": "4920221a-48dc-423e-b336-bb65baccc7bd", + # "orders": [{ + # "order": { + # "order_id": "30e2de8f-9a34-472f-bcf8-3af4b7757626", + # "account_holder": "49202c1a-48dc-423e-b336-bb65baccc7bd", + # "account_id": "49202c1a-48dc-423e-b336-bb65baccc7bd", + # "instrument_code": "BTC_EUR", + # "time": "2022-06-28T06:10:02.587345Z", + # "side": "SELL", + # "price": "19645.48", + # "amount": "0.00052", + # "filled_amount": "0.00052", + # "type": "MARKET", + # "sequence": 7633339971, + # "status": "FILLED_FULLY", + # "average_price": "19645.48", + # "is_post_only": False, + # "order_book_sequence": 866885897, + # "time_last_updated": "2022-06-28T06:10:02.766983Z", + # "update_modification_sequence": 866885897 + # }, + # "trades": [{ + # "fee": { + # "fee_amount": "0.01532347", + # "fee_currency": "EUR", + # "fee_percentage": "0.15", + # "fee_group_id": "default", + # "fee_type": "TAKER", + # "running_trading_volume": "0.0", + # "collection_type": "STANDARD" + # }, + # "trade": { + # "trade_id": "d83e302e-0b3a-4269-aa7d-ecf007cbe577", + # "order_id": "30e2de8f-9a34-472f-bcf8-3af4b7757626", + # "account_holder": "49203c1a-48dc-423e-b336-bb65baccc7bd", + # "account_id": "4920221a-48dc-423e-b336-bb65baccc7bd", + # "amount": "0.00052", + # "side": "SELL", + # "instrument_code": "BTC_EUR", + # "price": "19645.48", + # "time": "2022-06-28T06:10:02.693246Z", + # "price_tick_sequence": 0, + # "sequence": 7633339971 + # } + # }] + # }], + # "channel_name": "ACCOUNT_HISTORY", + # "type": "INACTIVE_ORDERS_SNAPSHOT", + # "time": "2022-06-28T06:11:52.469242Z" + # } + # + # + if self.orders is None: + limit = self.safe_integer(self.options, 'ordersLimit', 1000) + self.orders = ArrayCacheBySymbolById(limit) + if self.myTrades is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + self.myTrades = ArrayCacheBySymbolById(limit) + rawOrders = self.safe_value(message, 'orders', []) + rawOrdersLength = len(rawOrders) + if rawOrdersLength == 0: + return + orders = self.orders + for i in range(0, len(rawOrders)): + order = self.parse_order(rawOrders[i]) + symbol = self.safe_string(order, 'symbol', '') + orders.append(order) + client.resolve(self.orders, 'orders:' + symbol) + rawTrades = self.safe_value(rawOrders[i], 'trades', []) + for ii in range(0, len(rawTrades)): + trade = self.parse_trade(rawTrades[ii]) + symbol = self.safe_string(trade, 'symbol', symbol) + self.myTrades.append(trade) + client.resolve(self.myTrades, 'myTrades:' + symbol) + client.resolve(self.orders, 'orders') + client.resolve(self.myTrades, 'myTrades') + + def handle_account_update(self, client: Client, message): + # + # order created + # { + # "account_id": "49302c1a-48dc-423e-b336-bb65baccc7bd", + # "sequence": 7658332018, + # "update": { + # "type": "ORDER_CREATED", + # "activity": "TRADING", + # "account_holder": "43202c1a-48dc-423e-b336-bb65baccc7bd", + # "account_id": "49202c1a-48dc-423e-b336-bb65baccc7bd", + # "order_id": "8893fd69-5ebd-496b-aaa4-269b4c18aa77", + # "time": "2022-06-29T04:33:29.661257Z", + # "order": { + # "time_in_force": "GOOD_TILL_CANCELLED", + # "is_post_only": False, + # "order_id": "8892fd69-5ebd-496b-aaa4-269b4c18aa77", + # "account_holder": "43202c1a-48dc-423e-b336-bb65baccc7bd", + # "account_id": "49302c1a-48dc-423e-b336-bb65baccc7bd", + # "instrument_code": "BTC_EUR", + # "time": "2022-06-29T04:33:29.656896Z", + # "side": "SELL", + # "price": "50338.65", + # "amount": "0.00021", + # "filled_amount": "0.0", + # "type": "LIMIT" + # }, + # "locked": { + # "currency_code": "BTC", + # "amount": "0.00021", + # "new_available": "0.00017", + # "new_locked": "0.00021" + # }, + # "id": "26e9c36a-b231-4bb0-a686-aa915a2fc9e6", + # "sequence": 7658332018 + # }, + # "channel_name": "ACCOUNT_HISTORY", + # "type": "ACCOUNT_UPDATE", + # "time": "2022-06-29T04:33:29.684517Z" + # } + # + # order rejected + # { + # "account_id": "49302c1a-48dc-423e-b336-bb65baccc7bd", + # "sequence": 7658332018, + # "update": { + # "id": "d3fe6025-5b27-4df6-a957-98b8d131cb9d", + # "type": "ORDER_REJECTED", + # "activity": "TRADING", + # "account_id": "b355abb8-aaae-4fae-903c-c60ff74723c6", + # "sequence": 0, + # "timestamp": "2018-08-01T13:39:15.590Z", + # "reason": "INSUFFICIENT_FUNDS", + # "order_id": "6f991342-da2c-45c6-8830-8bf519cfc8cc", + # "client_id": "fb497387-8223-4111-87dc-66a86f98a7cf", + # "unlocked": { + # "currency_code": "BTC", + # "amount": "1.5", + # "new_locked": "2.0", + # "new_available": "1.5" + # } + # } + # } + # + # order closed + # { + # "account_id": "49202c1a-48dc-423e-b336-bb65baccc7bd", + # "sequence": 7658471216, + # "update": { + # "type": "ORDER_CLOSED", + # "activity": "TRADING", + # "account_holder": "49202c1a-48dc-423e-b336-bb65baccc7bd", + # "account_id": "49202c1a-48dc-423e-b336-bb65baccc7bd", + # "time": "2022-06-29T04:43:57.169616Z", + # "order_id": "8892fd69-5ebd-496b-aaa4-269b4c18aa77", + # "unlocked": { + # "currency_code": "BTC", + # "amount": "0.00021", + # "new_available": "0.00038", + # "new_locked": "0.0" + # }, + # "order_book_sequence": 867964191, + # "id": "26c5e1d7-65ba-4a11-a661-14c0130ff484", + # "sequence": 7658471216 + # }, + # "channel_name": "ACCOUNT_HISTORY", + # "type": "ACCOUNT_UPDATE", + # "time": "2022-06-29T04:43:57.182153Z" + # } + # + # trade settled + # { + # "account_id": "49202c1a-48dc-423e-b336-bb65baccc7bd", + # "sequence": 7658502878, + # "update": { + # "type": "TRADE_SETTLED", + # "activity": "TRADING", + # "account_holder": "49202c1a-48dc-423e-b336-bb65baccc7bd", + # "account_id": "49202c1a-48dc-423e-b336-bb65baccc7bd", + # "time": "2022-06-29T04:46:12.933091Z", + # "order_id": "ad19951a-b616-401d-a062-8d0609f038a4", + # "order_book_sequence": 867965579, + # "filled_amount": "0.00052", + # "order": { + # "amount": "0.00052", + # "filled_amount": "0.00052" + # }, + # "trade": { + # "trade_id": "21039eb9-2df0-4227-be2d-0ea9b691ac66", + # "order_id": "ad19951a-b616-401d-a062-8d0609f038a4", + # "account_holder": "49202c1a-48dc-423e-b336-bb65baccc7bd", + # "account_id": "49202c1a-48dc-423e-b336-bb65baccc7bd", + # "amount": "0.00052", + # "side": "BUY", + # "instrument_code": "BTC_EUR", + # "price": "19309.29", + # "time": "2022-06-29T04:46:12.870581Z", + # "price_tick_sequence": 0 + # }, + # "fee": { + # "fee_amount": "0.00000078", + # "fee_currency": "BTC", + # "fee_percentage": "0.15", + # "fee_group_id": "default", + # "fee_type": "TAKER", + # "running_trading_volume": "0.00052", + # "collection_type": "STANDARD" + # }, + # "spent": { + # "currency_code": "EUR", + # "amount": "10.0408308", + # "new_available": "0.0", + # "new_locked": "0.15949533" + # }, + # "credited": { + # "currency_code": "BTC", + # "amount": "0.00051922", + # "new_available": "0.00089922", + # "new_locked": "0.0" + # }, + # "unlocked": { + # "currency_code": "EUR", + # "amount": "0.0", + # "new_available": "0.0", + # "new_locked": "0.15949533" + # }, + # "id": "22b40199-2508-4176-8a14-d4785c933444", + # "sequence": 7658502878 + # }, + # "channel_name": "ACCOUNT_HISTORY", + # "type": "ACCOUNT_UPDATE", + # "time": "2022-06-29T04:46:12.941837Z" + # } + # + # Trade Settled with BEST fee collection enabled + # { + # "account_id": "49302c1a-48dc-423e-b336-bb65baccc7bd", + # "sequence": 7658951984, + # "update": { + # "id": "70e00504-d892-456f-9aae-4da7acb36aac", + # "sequence": 361792, + # "order_book_sequence": 123456, + # "type": "TRADE_SETTLED", + # "activity": "TRADING", + # "account_id": "379a12c0-4560-11e9-82fe-2b25c6f7d123", + # "time": "2019-10-22T12:09:55.731Z", + # "order_id": "9fcdd91c-7f6e-45f4-9956-61cddba55de5", + # "client_id": "fb497387-8223-4111-87dc-66a86f98a7cf", + # "order": { + # "amount": "0.5", + # "filled_amount": "0.5" + # }, + # "trade": { + # "trade_id": "a828b63e-b2cb-48f0-8d99-8fc22cf98e08", + # "order_id": "9fcdd91c-7f6e-45f4-9956-61cddba55de5", + # "account_id": "379a12c0-4560-11e9-82fe-2b25c6f7d123", + # "amount": "0.5", + # "side": "BUY", + # "instrument_code": "BTC_EUR", + # "price": "7451.6", + # "time": "2019-10-22T12:09:55.667Z" + # }, + # "fee": { + # "fee_amount": "23.28625", + # "fee_currency": "BEST", + # "fee_percentage": "0.075", + # "fee_group_id": "default", + # "fee_type": "TAKER", + # "running_trading_volume": "0.10058", + # "collection_type": "BEST", + # "applied_best_eur_rate": "1.04402" + # }, + # "spent": { + # "currency_code": "EUR", + # "amount": "3725.8", + # "new_available": "14517885.0675703028781", + # "new_locked": "2354.882" + # }, + # "spent_on_fees": { + # "currency_code": "BEST", + # "amount": "23.28625", + # "new_available": "9157.31375", + # "new_locked": "0.0" + # }, + # "credited": { + # "currency_code": "BTC", + # "amount": "0.5", + # "new_available": "5839.89633700481", + # "new_locked": "0.0" + # }, + # "unlocked": { + # "currency_code": "EUR", + # "amount": "0.15", + # "new_available": "14517885.0675703028781", + # "new_locked": "2354.882" + # } + # } + # "channel_name": "ACCOUNT_HISTORY", + # "type": "ACCOUNT_UPDATE", + # "time": "2022-06-29T05:18:51.760338Z" + # } + # + if self.orders is None: + limit = self.safe_integer(self.options, 'ordersLimit', 1000) + self.orders = ArrayCacheBySymbolById(limit) + if self.myTrades is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + self.myTrades = ArrayCacheBySymbolById(limit) + symbol = None + orders = self.orders + update = self.safe_value(message, 'update', {}) + updateType = self.safe_string(update, 'type') + if updateType == 'ORDER_REJECTED' or updateType == 'ORDER_CLOSED' or updateType == 'STOP_ORDER_TRIGGERED': + orderId = self.safe_string(update, 'order_id') + datetime = self.safe_string_2(update, 'time', 'timestamp') + previousOrderArray = self.filter_by_array(self.orders, 'id', orderId, False) + previousOrder = self.safe_value(previousOrderArray, 0, {}) + symbol = previousOrder['symbol'] + filled = self.safe_string(update, 'filled_amount') + status = self.parse_ws_order_status(updateType) + if updateType == 'ORDER_CLOSED' and Precise.string_eq(filled, '0'): + status = 'canceled' + orderObject: dict = { + 'id': orderId, + 'symbol': symbol, + 'status': status, + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + } + orders.append(orderObject) + else: + parsed = self.parse_order(update) + symbol = self.safe_string(parsed, 'symbol', '') + orders.append(parsed) + client.resolve(self.orders, 'orders:' + symbol) + client.resolve(self.orders, 'orders') + # update balance + balanceKeys = ['locked', 'unlocked', 'spent', 'spent_on_fees', 'credited', 'deducted'] + for i in range(0, len(balanceKeys)): + newBalance = self.safe_value(update, balanceKeys[i]) + if newBalance is not None: + self.update_balance(newBalance) + client.resolve(self.balance, 'balance') + # update trades + if updateType == 'TRADE_SETTLED': + parsed = self.parse_trade(update) + symbol = self.safe_string(parsed, 'symbol', '') + myTrades = self.myTrades + myTrades.append(parsed) + client.resolve(self.myTrades, 'myTrades:' + symbol) + client.resolve(self.myTrades, 'myTrades') + + def parse_ws_order_status(self, status): + statuses: dict = { + 'ORDER_REJECTED': 'rejected', + 'ORDER_CLOSED': 'closed', + 'STOP_ORDER_TRIGGERED': 'triggered', + } + return self.safe_string(statuses, status, status) + + def update_balance(self, balance): + # + # { + # "currency_code": "EUR", + # "amount": "0.0", + # "new_available": "0.0", + # "new_locked": "0.15949533" + # } + # + currencyId = self.safe_string(balance, 'currency_code') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(balance, 'new_available') + account['used'] = self.safe_string(balance, 'new_locked') + self.balance[code] = account + self.balance = self.safe_balance(self.balance) + + async def watch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + + https://developers.bitpanda.com/exchange/#candlesticks-channel + + watches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + marketId = market['id'] + url = self.urls['api']['ws'] + timeframes = self.safe_value(self.options, 'timeframes', {}) + timeframeId = self.safe_value(timeframes, timeframe) + if timeframeId is None: + raise NotSupported(self.id + ' self interval is not supported, please provide one of the supported timeframes') + messageHash = 'ohlcv.' + symbol + '.' + timeframe + subscriptionHash = 'CANDLESTICKS' + client = self.safe_value(self.clients, url) + type = 'SUBSCRIBE' + subscription = {} + if client is not None: + subscription = self.safe_value(client.subscriptions, subscriptionHash) + if subscription is not None: + ohlcvMarket = self.safe_value(subscription, marketId, {}) + marketSubscribed = self.safe_bool(ohlcvMarket, timeframe, False) + if not marketSubscribed: + type = 'UPDATE_SUBSCRIPTION' + client.subscriptions[subscriptionHash] = None + else: + subscription = {} + subscriptionMarketId = self.safe_value(subscription, marketId) + if subscriptionMarketId is None: + subscription[marketId] = {} + subscription[marketId][timeframe] = True + properties = [] + marketIds = list(subscription.keys()) + for i in range(0, len(marketIds)): + marketIdtimeframes = list(subscription[marketIds[i]].keys()) + for ii in range(0, len(marketIdtimeframes)): + marketTimeframeId = self.safe_value(timeframes, timeframe) + property: dict = { + 'instrument_code': marketIds[i], + 'time_granularity': marketTimeframeId, + } + properties.append(property) + request: dict = { + 'type': type, + 'channels': [ + { + 'name': 'CANDLESTICKS', + 'properties': properties, + }, + ], + } + ohlcv = await self.watch(url, messageHash, self.deep_extend(request, params), subscriptionHash, subscription) + if self.newUpdates: + limit = ohlcv.getLimit(symbol, limit) + return self.filter_by_since_limit(ohlcv, since, limit, 0, True) + + def handle_ohlcv(self, client: Client, message): + # + # snapshot + # { + # "instrument_code": "BTC_EUR", + # "granularity": {unit: "MONTHS", period: 1}, + # "high": "29750.81", + # "low": "16764.59", + # "open": "29556.02", + # "close": "20164.55", + # "volume": "107518944.610659", + # "last_sequence": 2275507, + # "channel_name": "CANDLESTICKS", + # "type": "CANDLESTICK_SNAPSHOT", + # "time": "2022-06-30T23:59:59.999000Z" + # } + # + # update + # { + # "instrument_code": "BTC_EUR", + # "granularity": { + # "unit": "MINUTES", + # "period": 1 + # }, + # "high": "20164.16", + # "low": "20164.16", + # "open": "20164.16", + # "close": "20164.16", + # "volume": "3645.2768448", + # "last_sequence": 2275511, + # "channel_name": "CANDLESTICKS", + # "type": "CANDLESTICK", + # "time": "2022-06-24T21:20:59.999000Z" + # } + # + marketId = self.safe_string(message, 'instrument_code') + symbol = self.safe_symbol(marketId) + dateTime = self.safe_string(message, 'time') + timeframeId = self.safe_value(message, 'granularity') + timeframes = self.safe_value(self.options, 'timeframes', {}) + timeframe = self.find_timeframe(timeframeId, timeframes) + channel = 'ohlcv.' + symbol + '.' + timeframe + parsed = [ + self.parse8601(dateTime), + self.safe_number(message, 'open'), + self.safe_number(message, 'high'), + self.safe_number(message, 'low'), + self.safe_number(message, 'close'), + self.safe_number(message, 'volume'), + ] + self.ohlcvs[symbol] = self.safe_value(self.ohlcvs, symbol, {}) + stored = self.safe_value(self.ohlcvs[symbol], timeframe) + if stored is None: + limit = self.safe_integer(self.options, 'OHLCVLimit', 1000) + stored = ArrayCacheByTimestamp(limit) + stored.append(parsed) + self.ohlcvs[symbol][timeframe] = stored + client.resolve(stored, channel) + + def find_timeframe(self, timeframe, timeframes=None): + timeframes = timeframes or self.timeframes + keys = list(timeframes.keys()) + for i in range(0, len(keys)): + key = keys[i] + if timeframes[key]['unit'] == timeframe['unit'] and timeframes[key]['period'] == timeframe['period']: + return key + return None + + def handle_subscriptions(self, client: Client, message): + # + # { + # "channels": [{ + # "instrument_codes": [Array], + # "depth": 0, + # "name": "ORDER_BOOK" + # }], + # "type": "SUBSCRIPTIONS", + # "time": "2022-06-23T15:36:26.948282Z" + # } + # + return message + + def handle_heartbeat(self, client: Client, message): + # + # { + # "subscription": "SYSTEM", + # "channel_name": "SYSTEM", + # "type": "HEARTBEAT", + # "time": "2022-06-23T16:31:49.170224Z" + # } + # + return message + + def handle_error_message(self, client: Client, message) -> Bool: + # + # { + # "error": "MALFORMED_JSON", + # "channel_name": "SYSTEM", + # "type": "ERROR", + # "time": "2022-06-23T15:38:25.470391Z" + # } + # + raise ExchangeError(self.id + ' ' + self.json(message)) + + def handle_message(self, client: Client, message): + error = self.safe_value(message, 'error') + if error is not None: + self.handle_error_message(client, message) + return + type = self.safe_value(message, 'type') + handlers: dict = { + 'ORDER_BOOK_UPDATE': self.handle_order_book, + 'ORDER_BOOK_SNAPSHOT': self.handle_order_book, + 'ACTIVE_ORDERS_SNAPSHOT': self.handle_orders, + 'INACTIVE_ORDERS_SNAPSHOT': self.handle_orders, + 'ACCOUNT_UPDATE': self.handle_account_update, + 'BALANCES_SNAPSHOT': self.handle_balance_snapshot, + 'SUBSCRIPTIONS': self.handle_subscriptions, + 'SUBSCRIPTION_UPDATED': self.handle_subscriptions, + 'PRICE_TICK': self.handle_ticker, + 'PRICE_TICK_HISTORY': self.handle_subscriptions, + 'HEARTBEAT': self.handle_heartbeat, + 'MARKET_TICKER_UPDATES': self.handle_ticker, + 'PRICE_POINT_UPDATES': self.handle_price_point_updates, + 'CANDLESTICK_SNAPSHOT': self.handle_ohlcv, + 'CANDLESTICK': self.handle_ohlcv, + 'AUTHENTICATED': self.handle_authentication_message, + 'FILL': self.handle_trading, + 'DONE': self.handle_trading, + 'BOOKED': self.handle_trading, + 'UPDATE': self.handle_trading, + 'TRACKED': self.handle_trading, + 'TRIGGERED': self.handle_trading, + 'STOP_TRACKED': self.handle_trading, + 'STOP_TRIGGERED': self.handle_trading, + } + handler = self.safe_value(handlers, type) + if handler is not None: + handler(client, message) + + def handle_price_point_updates(self, client: Client, message): + # + # { + # "channel_name": "MARKET_TICKER", + # "type": "PRICE_POINT_UPDATES", + # "time": "2019-03-01T10:59:59.999Z", + # "price_updates": [{ + # "instrument": "BTC_EUR", + # "prices": [{ + # "time": "2019-03-01T08:59:59.999Z", + # "close_price": "3580.6" + # }, + # ... + # ] + # }, + # ... + # ] + # } + # + return message + + def handle_authentication_message(self, client: Client, message): + # + # { + # "channel_name": "SYSTEM", + # "type": "AUTHENTICATED", + # "time": "2022-06-24T20:45:25.447488Z" + # } + # + future = self.safe_value(client.futures, 'authenticated') + if future is not None: + future.resolve(True) + return message + + async def watch_many(self, messageHash, request, subscriptionHash, symbols: Strings = [], params={}): + marketIds = [] + numSymbols = len(symbols) + if numSymbols == 0: + marketIds = list(self.markets_by_id.keys()) + else: + marketIds = self.market_ids(symbols) + url = self.urls['api']['ws'] + client = self.safe_value(self.clients, url) + type = 'SUBSCRIBE' + subscription = {} + if client is not None: + subscription = self.safe_value(client.subscriptions, subscriptionHash) + if subscription is not None: + for i in range(0, len(marketIds)): + marketId = marketIds[i] + marketSubscribed = self.safe_bool(subscription, marketId, False) + if not marketSubscribed: + type = 'UPDATE_SUBSCRIPTION' + client.subscriptions[subscriptionHash] = None + else: + subscription = {} + for i in range(0, len(marketIds)): + marketId = marketIds[i] + subscription[marketId] = True + request['type'] = type + request['channels'][0]['instrument_codes'] = list(subscription.keys()) + return await self.watch(url, messageHash, self.deep_extend(request, params), subscriptionHash, subscription) + + async def authenticate(self, params={}): + url = self.urls['api']['ws'] + client = self.client(url) + messageHash = 'authenticated' + future = client.reusableFuture('authenticated') + authenticated = self.safe_value(client.subscriptions, messageHash) + if authenticated is None: + self.check_required_credentials() + request: dict = { + 'type': 'AUTHENTICATE', + 'api_token': self.apiKey, + } + self.watch(url, messageHash, self.extend(request, params), messageHash) + return await future diff --git a/ccxt/pro/oxfun.py b/ccxt/pro/oxfun.py new file mode 100644 index 0000000..085084f --- /dev/null +++ b/ccxt/pro/oxfun.py @@ -0,0 +1,1054 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolById, ArrayCacheBySymbolBySide, ArrayCacheByTimestamp +import hashlib +from ccxt.base.types import Any, Balances, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, Trade +from ccxt.async_support.base.ws.client import Client +from typing import List +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest + + +class oxfun(ccxt.async_support.oxfun): + + def describe(self) -> Any: + return self.deep_extend(super(oxfun, self).describe(), { + 'has': { + 'ws': True, + 'watchTrades': True, + 'watchTradesForSymbols': True, + 'watchOrderBook': True, + 'watchOrderBookForSymbols': True, + 'watchOHLCV': True, + 'watchOHLCVForSymbols': True, + 'watchOrders': True, + 'watchMyTrades': False, + 'watchTicker': True, + 'watchTickers': True, + 'watchBidsAsks': True, + 'watchBalance': True, + 'createOrderWs': True, + 'editOrderWs': True, + 'cancelOrderWs': True, + 'cancelOrdersWs': True, + }, + 'urls': { + 'api': { + 'ws': 'wss://api.ox.fun/v2/websocket', + 'test': 'wss://stgapi.ox.fun/v2/websocket', + }, + }, + 'options': { + 'timeframes': { + '1m': '60s', + '3m': '180s', + '5m': '300s', + '15m': '900s', + '30m': '1800s', + '1h': '3600s', + '2h': '7200s', + '4h': '14400s', + '6h': '21600s', + '12h': '43200s', + '1d': '86400s', + }, + 'watchOrderBook': { + 'channel': 'depth', # depth, depthL5, depthL10, depthL25 + }, + }, + 'streaming': { + 'ping': self.ping, + 'keepAlive': 50000, + }, + }) + + async def subscribe_multiple(self, messageHashes, argsArray, params={}): + url = self.urls['api']['ws'] + request: dict = { + 'op': 'subscribe', + 'args': argsArray, + } + return await self.watch_multiple(url, messageHashes, self.extend(request, params), messageHashes) + + async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + watches information on multiple trades made in a market + + https://docs.ox.fun/?json#trade + + :param str symbol: unified market symbol of the market trades were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of trade structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int|str [params.tag]: If given it will be echoed in the reply and the max size of tag is 32 + :returns dict[]: a list of `trade structures ` + """ + return await self.watch_trades_for_symbols([symbol], since, limit, params) + + async def watch_trades_for_symbols(self, symbols: List[str], since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://docs.ox.fun/?json#trade + + :param str[] symbols: + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int|str [params.tag]: If given it will be echoed in the reply and the max size of tag is 32 + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, False) + args = [] + messageHashes = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + messageHash = 'trades' + ':' + symbol + messageHashes.append(messageHash) + marketId = self.market_id(symbol) + arg = 'trade:' + marketId + args.append(arg) + trades = await self.subscribe_multiple(messageHashes, args, params) + if self.newUpdates: + first = self.safe_dict(trades, 0, {}) + tradeSymbol = self.safe_string(first, 'symbol') + limit = trades.getLimit(tradeSymbol, limit) + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + def handle_trades(self, client: Client, message): + # + # { + # table: 'trade', + # data: [ + # { + # side: 'SELL', + # quantity: '0.074', + # matchType: 'TAKER', + # price: '3079.5', + # marketCode: 'ETH-USD-SWAP-LIN', + # tradeId: '400017157974517783', + # timestamp: '1716124156643' + # } + # ] + # } + # + data = self.safe_list(message, 'data', []) + for i in range(0, len(data)): + trade = self.safe_dict(data, i, {}) + parsedTrade = self.parse_ws_trade(trade) + symbol = self.safe_string(parsedTrade, 'symbol') + messageHash = 'trades:' + symbol + if not (symbol in self.trades): + tradesLimit = self.safe_integer(self.options, 'tradesLimit', 1000) + self.trades[symbol] = ArrayCache(tradesLimit) + stored = self.trades[symbol] + stored.append(parsedTrade) + client.resolve(stored, messageHash) + + def parse_ws_trade(self, trade, market=None) -> Trade: + # + # { + # side: 'SELL', + # quantity: '0.074', + # matchType: 'TAKER', + # price: '3079.5', + # marketCode: 'ETH-USD-SWAP-LIN', + # tradeId: '400017157974517783', + # timestamp: '1716124156643' + # } + # + marketId = self.safe_string(trade, 'marketCode') + market = self.safe_market(marketId, market) + timestamp = self.safe_integer(trade, 'timestamp') + return self.safe_trade({ + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'id': self.safe_string(trade, 'tradeId'), + 'order': None, + 'type': None, + 'takerOrMaker': self.safe_string_lower(trade, 'matchType'), + 'side': self.safe_string_lower(trade, 'side'), + 'price': self.safe_number(trade, 'price'), + 'amount': self.safe_number(trade, 'quantity'), + 'cost': None, + 'fee': None, + }) + + async def watch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + watches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://docs.ox.fun/?json#candles + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int|str [params.tag]: If given it will be echoed in the reply and the max size of tag is 32 + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + timeframes = self.safe_dict(self.options, 'timeframes', {}) + interval = self.safe_string(timeframes, timeframe, timeframe) + args = 'candles' + interval + ':' + market['id'] + messageHash = 'ohlcv:' + symbol + ':' + timeframe + url = self.urls['api']['ws'] + request: dict = { + 'op': 'subscribe', + 'args': [args], + } + ohlcvs = await self.watch(url, messageHash, self.extend(request, params), messageHash) + if self.newUpdates: + limit = ohlcvs.getLimit(symbol, limit) + return self.filter_by_since_limit(ohlcvs, since, limit, 0, True) + + async def watch_ohlcv_for_symbols(self, symbolsAndTimeframes: List[List[str]], since: Int = None, limit: Int = None, params={}): + """ + watches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://docs.ox.fun/?json#candles + + :param str[][] symbolsAndTimeframes: array of arrays containing unified symbols and timeframes to fetch OHLCV data for, example [['BTC/USDT', '1m'], ['LTC/USDT', '5m']] + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int|str [params.tag]: If given it will be echoed in the reply and the max size of tag is 32 + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + symbolsLength = len(symbolsAndTimeframes) + if symbolsLength == 0 or not isinstance(symbolsAndTimeframes[0], list): + raise ArgumentsRequired(self.id + " watchOHLCVForSymbols() requires a an array of symbols and timeframes, like [['BTC/USDT:OX', '1m'], ['OX/USDT', '5m']]") + await self.load_markets() + args = [] + messageHashes = [] + timeframes = self.safe_dict(self.options, 'timeframes', {}) + for i in range(0, len(symbolsAndTimeframes)): + symbolAndTimeframe = symbolsAndTimeframes[i] + sym = symbolAndTimeframe[0] + tf = symbolAndTimeframe[1] + marketId = self.market_id(sym) + interval = self.safe_string(timeframes, tf, tf) + arg = 'candles' + interval + ':' + marketId + args.append(arg) + messageHash = 'multi:ohlcv:' + sym + ':' + tf + messageHashes.append(messageHash) + symbol, timeframe, candles = await self.subscribe_multiple(messageHashes, args, params) + if self.newUpdates: + limit = candles.getLimit(symbol, limit) + filtered = self.filter_by_since_limit(candles, since, limit, 0, True) + return self.create_ohlcv_object(symbol, timeframe, filtered) + + def handle_ohlcv(self, client: Client, message): + # + # { + # "table": "candles60s", + # "data": [ + # { + # "marketCode": "BTC-USD-SWAP-LIN", + # "candle": [ + # "1594313762698", #timestamp + # "9633.1", #open + # "9693.9", #high + # "9238.1", #low + # "9630.2", #close + # "45247", #volume in OX + # "5.3" #volume in Contracts + # ] + # } + # ] + # } + # + table = self.safe_string(message, 'table') + parts = table.split('candles') + timeframeId = self.safe_string(parts, 1, '') + timeframe = self.find_timeframe(timeframeId) + messageData = self.safe_list(message, 'data', []) + data = self.safe_dict(messageData, 0, {}) + marketId = self.safe_string(data, 'marketCode') + market = self.safe_market(marketId) + symbol = self.safe_symbol(marketId, market) + if not (symbol in self.ohlcvs): + self.ohlcvs[symbol] = {} + if not (timeframe in self.ohlcvs[symbol]): + limit = self.safe_integer(self.options, 'OHLCVLimit', 1000) + self.ohlcvs[symbol][timeframe] = ArrayCacheByTimestamp(limit) + candle = self.safe_list(data, 'candle', []) + parsed = self.parse_ws_ohlcv(candle, market) + stored = self.ohlcvs[symbol][timeframe] + stored.append(parsed) + messageHash = 'ohlcv:' + symbol + ':' + timeframe + client.resolve(stored, messageHash) + # for multiOHLCV we need special object, to other "multi" + # methods, because OHLCV response item does not contain symbol + # or timeframe, thus otherwise it would be unrecognizable + messageHashForMulti = 'multi:' + messageHash + client.resolve([symbol, timeframe, stored], messageHashForMulti) + + def parse_ws_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # [ + # "1594313762698", #timestamp + # "9633.1", #open + # "9693.9", #high + # "9238.1", #low + # "9630.2", #close + # "45247", #volume in OX + # "5.3" #volume in Contracts + # ] + # + return [ + self.safe_integer(ohlcv, 0), + self.safe_number(ohlcv, 1), + self.safe_number(ohlcv, 2), + self.safe_number(ohlcv, 3), + self.safe_number(ohlcv, 4), + self.safe_number(ohlcv, 6), + ] + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://docs.ox.fun/?json#fixed-size-order-book + https://docs.ox.fun/?json#full-order-book + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + return await self.watch_order_book_for_symbols([symbol], limit, params) + + async def watch_order_book_for_symbols(self, symbols: List[str], limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://docs.ox.fun/?json#fixed-size-order-book + https://docs.ox.fun/?json#full-order-book + + :param str[] symbols: unified array of symbols + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int|str [params.tag]: If given it will be echoed in the reply and the max size of tag is 32 + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + channel = 'depth' + options = self.safe_dict(self.options, 'watchOrderBook', {}) + defaultChannel = self.safe_string(options, 'channel') + if defaultChannel is not None: + channel = defaultChannel + elif limit is not None: + if limit <= 5: + channel = 'depthL5' + elif limit <= 10: + channel = 'depthL10' + elif limit <= 25: + channel = 'depthL25' + args = [] + messageHashes = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + messageHash = 'orderbook:' + symbol + messageHashes.append(messageHash) + marketId = self.market_id(symbol) + arg = channel + ':' + marketId + args.append(arg) + orderbook = await self.subscribe_multiple(messageHashes, args, params) + return orderbook.limit() + + def handle_order_book(self, client: Client, message): + # + # { + # "table": "depth", + # "data": { + # "seqNum": "100170478917895032", + # "asks": [ + # [0.01, 100500], + # ... + # ], + # "bids": [ + # [69.69696, 69], + # ... + # ], + # "checksum": 261021645, + # "marketCode": "OX-USDT", + # "timestamp": 1716204786184 + # }, + # "action": "partial" + # } + # + data = self.safe_dict(message, 'data', {}) + marketId = self.safe_string(data, 'marketCode') + symbol = self.safe_symbol(marketId) + timestamp = self.safe_integer(data, 'timestamp') + messageHash = 'orderbook:' + symbol + if not (symbol in self.orderbooks): + self.orderbooks[symbol] = self.order_book({}) + orderbook = self.orderbooks[symbol] + snapshot = self.parse_order_book(data, symbol, timestamp, 'asks', 'bids') + orderbook.reset(snapshot) + orderbook['nonce'] = self.safe_integer(data, 'seqNum') + self.orderbooks[symbol] = orderbook + client.resolve(orderbook, messageHash) + + async def watch_ticker(self, symbol: str, params={}) -> Ticker: + """ + + https://docs.ox.fun/?json#ticker + + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int|str [params.tag]: If given it will be echoed in the reply and the max size of tag is 32 + :returns dict: a `ticker structure ` + """ + ticker = await self.watch_tickers([symbol], params) + return self.safe_value(ticker, symbol) + + async def watch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + + https://docs.ox.fun/?json#ticker + + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list + :param str[] [symbols]: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int|str [params.tag]: If given it will be echoed in the reply and the max size of tag is 32 + :returns dict: a `ticker structure ` + """ + await self.load_markets() + allSymbols = (symbols is None) + sym = symbols + args = [] + if allSymbols: + sym = self.symbols + args.append('ticker:all') + messageHashes = [] + for i in range(0, len(sym)): + symbol = sym[i] + messageHash = 'tickers' + ':' + symbol + messageHashes.append(messageHash) + marketId = self.market_id(symbol) + if not allSymbols: + args.append('ticker:' + marketId) + newTicker = await self.subscribe_multiple(messageHashes, args, params) + if self.newUpdates: + result = {} + result[newTicker['symbol']] = newTicker + return result + return self.filter_by_array(self.tickers, 'symbol', symbols) + + def handle_ticker(self, client: Client, message): + # + # { + # "table": "ticker", + # "data": [ + # { + # "last": "3088.6", + # "open24h": "3087.2", + # "high24h": "3142.0", + # "low24h": "3053.9", + # "volume24h": "450512672.1800", + # "currencyVolume24h": "1458.579", + # "openInterest": "3786.801", + # "marketCode": "ETH-USD-SWAP-LIN", + # "timestamp": "1716212747050", + # "lastQty": "0.813", + # "markPrice": "3088.6", + # "lastMarkPrice": "3088.6", + # "indexPrice": "3086.5" + # }, + # ... + # ] + # } + # + data = self.safe_list(message, 'data', []) + for i in range(0, len(data)): + rawTicker = self.safe_dict(data, i, {}) + ticker = self.parse_ticker(rawTicker) + symbol = ticker['symbol'] + messageHash = 'tickers:' + symbol + self.tickers[symbol] = ticker + client.resolve(ticker, messageHash) + + async def watch_bids_asks(self, symbols: Strings = None, params={}) -> Tickers: + """ + + https://docs.ox.fun/?json#best-bid-ask + + watches best bid & ask for symbols + :param str[] symbols: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, False) + messageHashes = [] + args = [] + for i in range(0, len(symbols)): + market = self.market(symbols[i]) + args.append('bestBidAsk:' + market['id']) + messageHashes.append('bidask:' + market['symbol']) + newTickers = await self.subscribe_multiple(messageHashes, args, params) + if self.newUpdates: + tickers: dict = {} + tickers[newTickers['symbol']] = newTickers + return tickers + return self.filter_by_array(self.bidsasks, 'symbol', symbols) + + def handle_bid_ask(self, client: Client, message): + # + # { + # "table": "bestBidAsk", + # "data": { + # "ask": [ + # 19045.0, + # 1.0 + # ], + # "checksum": 3790706311, + # "marketCode": "BTC-USD-SWAP-LIN", + # "bid": [ + # 19015.0, + # 1.0 + # ], + # "timestamp": "1665456882928" + # } + # } + # + data = self.safe_dict(message, 'data', {}) + parsedTicker = self.parse_ws_bid_ask(data) + symbol = parsedTicker['symbol'] + self.bidsasks[symbol] = parsedTicker + messageHash = 'bidask:' + symbol + client.resolve(parsedTicker, messageHash) + + def parse_ws_bid_ask(self, ticker, market=None): + marketId = self.safe_string(ticker, 'marketCode') + market = self.safe_market(marketId, market) + symbol = self.safe_string(market, 'symbol') + timestamp = self.safe_integer(ticker, 'timestamp') + ask = self.safe_list(ticker, 'ask', []) + bid = self.safe_list(ticker, 'bid', []) + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'ask': self.safe_number(ask, 0), + 'askVolume': self.safe_number(ask, 1), + 'bid': self.safe_number(bid, 0), + 'bidVolume': self.safe_number(bid, 1), + 'info': ticker, + }, market) + + async def watch_balance(self, params={}) -> Balances: + """ + + https://docs.ox.fun/?json#balance-channel + + watch balance and get the amount of funds available for trading or funds locked in orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int|str [params.tag]: If given it will be echoed in the reply and the max size of tag is 32 + :returns dict: a `balance structure ` + """ + await self.load_markets() + self.authenticate() + args = 'balance:all' + messageHash = 'balance' + url = self.urls['api']['ws'] + request: dict = { + 'op': 'subscribe', + 'args': [args], + } + return await self.watch(url, messageHash, self.extend(request, params), messageHash) + + def handle_balance(self, client, message): + # + # { + # "table": "balance", + # "accountId": "106464", + # "timestamp": "1716549132780", + # "tradeType": "PORTFOLIO", + # "data": [ + # { + # "instrumentId": "xOX", + # "total": "23.375591220", + # "available": "23.375591220", + # "reserved": "0", + # "quantityLastUpdated": "1716509744262", + # "locked": "0" + # }, + # ... + # ] + # } + # + balances = self.safe_list(message, 'data') + timestamp = self.safe_integer(message, 'timestamp') + self.balance['info'] = message + self.balance['timestamp'] = timestamp + self.balance['datetime'] = self.iso8601(timestamp) + for i in range(0, len(balances)): + balance = self.safe_dict(balances, i, {}) + currencyId = self.safe_string(balance, 'instrumentId') + code = self.safe_currency_code(currencyId) + if not (code in self.balance): + self.balance[code] = self.account() + account = self.balance[code] + account['total'] = self.safe_string(balance, 'total') + account['used'] = self.safe_string(balance, 'reserved') + account['free'] = self.safe_string(balance, 'available') + self.balance[code] = account + self.balance = self.safe_balance(self.balance) + client.resolve(self.balance, 'balance') + + async def watch_positions(self, symbols: Strings = None, since: Int = None, limit: Int = None, params={}) -> List[Position]: + """ + + https://docs.ox.fun/?json#position-channel + + watch all open positions + :param str[]|None symbols: list of unified market symbols + @param since + @param limit + :param dict params: extra parameters specific to the exchange API endpoint + :param int|str [params.tag]: If given it will be echoed in the reply and the max size of tag is 32 + :returns dict[]: a list of `position structure ` + """ + await self.load_markets() + await self.authenticate() + allSymbols = (symbols is None) + sym = symbols + args = [] + if allSymbols: + sym = self.symbols + args.append('position:all') + messageHashes = [] + for i in range(0, len(sym)): + symbol = sym[i] + messageHash = 'positions' + ':' + symbol + messageHashes.append(messageHash) + marketId = self.market_id(symbol) + if not allSymbols: + args.append('position:' + marketId) + newPositions = await self.subscribe_multiple(messageHashes, args, params) + if self.newUpdates: + return newPositions + return self.filter_by_symbols_since_limit(self.positions, symbols, since, limit, True) + + def handle_positions(self, client: Client, message): + # + # { + # "table": "position", + # "accountId": "106464", + # "timestamp": "1716550771582", + # "data": [ + # { + # "instrumentId": "ETH-USD-SWAP-LIN", + # "quantity": "0.01", + # "lastUpdated": "1716550757299", + # "contractValCurrency": "ETH", + # "entryPrice": "3709.6", + # "positionPnl": "-5.000", + # "estLiquidationPrice": "743.4", + # "margin": "0", + # "leverage": "0" + # } + # ] + # } + # + if self.positions is None: + self.positions = ArrayCacheBySymbolBySide() + cache = self.positions + data = self.safe_list(message, 'data', []) + for i in range(0, len(data)): + rawPosition = self.safe_dict(data, i, {}) + position = self.parse_ws_position(rawPosition) + symbol = position['symbol'] + messageHash = 'positions:' + symbol + cache.append(position) + client.resolve(position, messageHash) + + def parse_ws_position(self, position, market: Market = None): + # + # { + # "instrumentId": "ETH-USD-SWAP-LIN", + # "quantity": "0.01", + # "lastUpdated": "1716550757299", + # "contractValCurrency": "ETH", + # "entryPrice": "3709.6", + # "positionPnl": "-5.000", + # "estLiquidationPrice": "743.4", + # "margin": "0", # Currently always reports 0 + # "leverage": "0" # Currently always reports 0 + # } + # + marketId = self.safe_string(position, 'instrumentId') + market = self.safe_market(marketId, market) + return self.safe_position({ + 'info': position, + 'id': None, + 'symbol': market['symbol'], + 'notional': None, + 'marginMode': 'cross', + 'liquidationPrice': self.safe_number(position, 'estLiquidationPrice'), + 'entryPrice': self.safe_number(position, 'entryPrice'), + 'unrealizedPnl': self.safe_number(position, 'positionPnl'), + 'realizedPnl': None, + 'percentage': None, + 'contracts': self.safe_number(position, 'quantity'), + 'contractSize': None, + 'markPrice': None, + 'lastPrice': None, + 'side': None, + 'hedged': None, + 'timestamp': None, + 'datetime': None, + 'lastUpdateTimestamp': self.safe_integer(position, 'lastUpdated'), + 'maintenanceMargin': None, + 'maintenanceMarginPercentage': None, + 'collateral': None, + 'initialMargin': None, + 'initialMarginPercentage': None, + 'leverage': None, + 'marginRatio': None, + 'stopLossPrice': None, + 'takeProfitPrice': None, + }) + + async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + watches information on multiple orders made by the user + + https://docs.ox.fun/?json#order-channel + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int|str [params.tag]: If given it will be echoed in the reply and the max size of tag is 32 + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + await self.authenticate() + messageHash = 'orders' + args = 'order:' + market = self.safe_market(symbol) + if symbol is None: + args += 'all' + else: + messageHash += ':' + symbol + args += ':' + market['id'] + request: dict = { + 'op': 'subscribe', + 'args': [ + args, + ], + } + url = self.urls['api']['ws'] + orders = await self.watch(url, messageHash, request, messageHash) + if self.newUpdates: + limit = orders.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(orders, symbol, since, limit, True) + + def handle_orders(self, client: Client, message): + # + # { + # "table": "order", + # "data": [ + # { + # "accountId": "106464", + # "clientOrderId": "1716713676233", + # "orderId": "1000116921319", + # "price": "1000.0", + # "quantity": "0.01", + # "amount": "0.0", + # "side": "BUY", + # "status": "OPEN", + # "marketCode": "ETH-USD-SWAP-LIN", + # "timeInForce": "MAKER_ONLY", + # "timestamp": "1716713677834", + # "remainQuantity": "0.01", + # "limitPrice": "1000.0", + # "notice": "OrderOpened", + # "orderType": "LIMIT", + # "isTriggered": "false", + # "displayQuantity": "0.01" + # } + # ] + # } + # + data = self.safe_list(message, 'data', []) + messageHash = 'orders' + if self.orders is None: + limit = self.safe_integer(self.options, 'ordersLimit', 1000) + self.orders = ArrayCacheBySymbolById(limit) + orders = self.orders + for i in range(0, len(data)): + order = self.safe_dict(data, i, {}) + parsedOrder = self.parse_order(order) + orders.append(parsedOrder) + messageHash += ':' + parsedOrder['symbol'] + client.resolve(self.orders, messageHash) + + async def create_order_ws(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}) -> Order: + """ + + https://docs.ox.fun/?json#order-commands + + create a trade order + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market', 'limit', 'STOP_LIMIT' or 'STOP_MARKET' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.clientOrderId]: a unique id for the order + :param int [params.timestamp]: in milliseconds. If an order reaches the matching engine and the current timestamp exceeds timestamp + recvWindow, then the order will be rejected. + :param int [params.recvWindow]: in milliseconds. If an order reaches the matching engine and the current timestamp exceeds timestamp + recvWindow, then the order will be rejected. If timestamp is provided without recvWindow, then a default recvWindow of 1000ms is used. + :param float [params.cost]: the quote quantity that can be used alternative for the amount for market buy orders + :param float [params.triggerPrice]: The price at which a trigger order is triggered at + :param float [params.limitPrice]: Limit price for the STOP_LIMIT order + :param bool [params.postOnly]: if True, the order will only be posted if it will be a maker order + :param str [params.timeInForce]: GTC(default), IOC, FOK, PO, MAKER_ONLY or MAKER_ONLY_REPRICE(reprices order to the best maker only price if the specified price were to lead to a taker trade) + :param str [params.selfTradePreventionMode]: NONE, EXPIRE_MAKER, EXPIRE_TAKER or EXPIRE_BOTH for more info check here {@link https://docs.ox.fun/?json#self-trade-prevention-modes} + :param str [params.displayQuantity]: for an iceberg order, pass both quantity and displayQuantity fields in the order request + :returns dict: an `order structure ` + """ + await self.load_markets() + await self.authenticate() + messageHash = str(self.nonce()) + request: dict = { + 'op': 'placeorder', + 'tag': messageHash, + } + params = self.omit(params, 'tag') + orderRequest: dict = self.create_order_request(symbol, type, side, amount, price, params) + timestamp = self.safe_integer(orderRequest, 'timestamp') + if timestamp is None: + orderRequest['timestamp'] = self.milliseconds() + request['data'] = orderRequest + url = self.urls['api']['ws'] + return await self.watch(url, messageHash, request, messageHash) + + async def edit_order_ws(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}) -> Order: + """ + edit a trade order + + https://docs.ox.fun/?json#modify-order + + :param str id: order id + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of the currency you want to trade in units of the base currency + :param float|None [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.timestamp]: in milliseconds. If an order reaches the matching engine and the current timestamp exceeds timestamp + recvWindow, then the order will be rejected. + :param int [params.recvWindow]: in milliseconds. If an order reaches the matching engine and the current timestamp exceeds timestamp + recvWindow, then the order will be rejected. If timestamp is provided without recvWindow, then a default recvWindow of 1000ms is used. + :returns dict: an `order structure ` + """ + await self.load_markets() + await self.authenticate() + messageHash = str(self.nonce()) + request: dict = { + 'op': 'modifyorder', + 'tag': messageHash, + } + params = self.omit(params, 'tag') + orderRequest: dict = self.create_order_request(symbol, type, side, amount, price, params) + orderRequest = self.extend(orderRequest, {'orderId': id}) + timestamp = self.safe_integer(orderRequest, 'timestamp') + if timestamp is None: + orderRequest['timestamp'] = self.milliseconds() + request['data'] = orderRequest + url = self.urls['api']['ws'] + return await self.watch(url, messageHash, request, messageHash) + + def handle_place_orders(self, client: Client, message): + # + # { + # "event": "placeorder", + # "submitted": True, + # "tag": "1716934577", + # "timestamp": "1716932973899", + # "data": { + # "marketCode": "ETH-USD-SWAP-LIN", + # "side": "BUY", + # "orderType": "LIMIT", + # "quantity": "0.010", + # "timeInForce": "GTC", + # "price": "400.0", + # "limitPrice": "400.0", + # "orderId": "1000117429736", + # "source": 13 + # } + # } + # + # + # Failure response format + # { + # "event": "placeorder", + # "submitted": False, + # "message": "JSON data format is invalid", + # "code": "20009", + # "timestamp": "1716932877381" + # } + # + messageHash = self.safe_string(message, 'tag') + submitted = self.safe_bool(message, 'submitted') + # filter out partial errors + if not submitted: + method = self.safe_string(message, 'event') + stringMsg = self.json(message) + code = self.safe_integer(message, 'code') + self.handle_errors(code, '', client.url, method, {}, stringMsg, message, {}, {}) + data = self.safe_value(message, 'data', {}) + order = self.parse_order(data) + client.resolve(order, messageHash) + + async def cancel_order_ws(self, id: str, symbol: Str = None, params={}) -> Order: + """ + + https://docs.ox.fun/?json#cancel-order + + cancels an open order + :param str id: order id + :param str symbol: unified market symbol, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrderWs() requires a symbol argument') + await self.load_markets() + await self.authenticate() + messageHash = str(self.nonce()) + data: dict = { + 'marketCode': self.market_id(symbol), + 'orderId': id, + } + request: dict = { + 'op': 'cancelorder', + 'tag': messageHash, + 'data': data, + } + url = self.urls['api']['ws'] + return await self.watch(url, messageHash, request, messageHash) + + async def cancel_orders_ws(self, ids: List[str], symbol: Str = None, params={}): + """ + + https://www.okx.com/docs-v5/en/#order-book-trading-trade-ws-mass-cancel-order + + cancel multiple orders + :param str[] ids: order ids + :param str symbol: unified market symbol, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an list of `order structures ` + """ + idsLength: number = len(ids) + if idsLength > 20: + raise BadRequest(self.id + ' cancelOrdersWs() accepts up to 20 ids at a time') + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrdersWs() requires a symbol argument') + await self.load_markets() + await self.authenticate() + messageHash = str(self.nonce()) + marketId = self.market_id(symbol) + dataArray = [] + for i in range(0, idsLength): + data: dict = { + 'instId': marketId, + 'ordId': ids[i], + } + dataArray.append(data) + request: dict = { + 'op': 'cancelorders', + 'tag': messageHash, + 'dataArray': dataArray, + } + url = self.urls['api']['ws'] + return await self.watch(url, messageHash, self.deep_extend(request, params), messageHash) + + async def authenticate(self, params={}): + url = self.urls['api']['ws'] + client = self.client(url) + messageHash = 'authenticated' + future = client.reusableFuture(messageHash) + authenticated = self.safe_dict(client.subscriptions, messageHash) + if authenticated is None: + self.check_required_credentials() + timestamp = self.milliseconds() + payload = str(timestamp) + 'GET/auth/self/verify' + signature = self.hmac(self.encode(payload), self.encode(self.secret), hashlib.sha256, 'base64') + request: dict = { + 'op': 'login', + 'data': { + 'apiKey': self.apiKey, + 'timestamp': timestamp, + 'signature': signature, + }, + } + message = self.extend(request, params) + self.watch(url, messageHash, message, messageHash) + return await future + + def handle_authentication_message(self, client: Client, message): + authenticated = self.safe_bool(message, 'success', False) + messageHash = 'authenticated' + if authenticated: + # we resolve the future here permanently so authentication only happens once + future = self.safe_dict(client.futures, messageHash) + future.resolve(True) + else: + error = AuthenticationError(self.json(message)) + client.reject(error, messageHash) + if messageHash in client.subscriptions: + del client.subscriptions[messageHash] + + def ping(self, client: Client): + return 'ping' + + def handle_pong(self, client: Client, message): + client.lastPong = self.milliseconds() + return message + + def handle_message(self, client: Client, message): + if message == 'pong': + self.handle_pong(client, message) + return + table = self.safe_string(message, 'table') + data = self.safe_list(message, 'data', []) + event = self.safe_string(message, 'event') + if (table is not None) and (data is not None): + if table == 'trade': + self.handle_trades(client, message) + if table == 'ticker': + self.handle_ticker(client, message) + if table.find('candles') > -1: + self.handle_ohlcv(client, message) + if table.find('depth') > -1: + self.handle_order_book(client, message) + if table.find('balance') > -1: + self.handle_balance(client, message) + if table.find('position') > -1: + self.handle_positions(client, message) + if table.find('order') > -1: + self.handle_orders(client, message) + if table == 'bestBidAsk': + self.handle_bid_ask(client, message) + else: + if event == 'login': + self.handle_authentication_message(client, message) + if (event == 'placeorder') or (event == 'modifyorder') or (event == 'cancelorder'): + self.handle_place_orders(client, message) diff --git a/ccxt/pro/p2b.py b/ccxt/pro/p2b.py new file mode 100644 index 0000000..a1aa1ec --- /dev/null +++ b/ccxt/pro/p2b.py @@ -0,0 +1,485 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheByTimestamp +from ccxt.base.types import Any, Bool, Int, OrderBook, Strings, Ticker, Tickers, Trade +from ccxt.async_support.base.ws.client import Client +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import BadRequest + + +class p2b(ccxt.async_support.p2b): + + def describe(self) -> Any: + return self.deep_extend(super(p2b, self).describe(), { + 'has': { + 'ws': True, + 'cancelAllOrdersWs': False, + 'cancelOrdersWs': False, + 'cancelOrderWs': False, + 'createOrderWs': False, + 'editOrderWs': False, + 'fetchBalanceWs': False, + 'fetchOpenOrdersWs': False, + 'fetchOrderWs': False, + 'fetchTradesWs': False, + 'watchBalance': False, + 'watchMyTrades': False, + 'watchOHLCV': True, + 'watchOrderBook': True, + 'watchOrders': False, + # 'watchStatus': True, + 'watchTicker': True, + 'watchTickers': True, + 'watchTrades': True, + 'watchTradesForSymbols': True, + }, + 'urls': { + 'api': { + 'ws': 'wss://apiws.p2pb2b.com/', + }, + }, + 'options': { + 'OHLCVLimit': 1000, + 'tradesLimit': 1000, + 'timeframes': { + '15m': 900, + '30m': 1800, + '1h': 3600, + '1d': 86400, + }, + 'watchTicker': { + 'name': 'state', # or 'price' + }, + 'watchTickers': { + 'name': 'state', # or 'price' + }, + 'tickerSubs': self.create_safe_dictionary(), + }, + 'streaming': { + 'ping': self.ping, + }, + }) + + async def subscribe(self, name: str, messageHash: str, request, params={}): + """ + @ignore + Connects to a websocket channel + :param str name: name of the channel + :param str messageHash: string to look up in handler + :param str[]|float[] request: endpoint parameters + :param dict [params]: extra parameters specific to the p2b api + :returns dict: data from the websocket stream + """ + url = self.urls['api']['ws'] + subscribe: dict = { + 'method': name, + 'params': request, + 'id': self.milliseconds(), + } + query = self.extend(subscribe, params) + return await self.watch(url, messageHash, query, messageHash) + + async def watch_ohlcv(self, symbol: str, timeframe: str = '15m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + watches historical candlestick data containing the open, high, low, and close price, and the volume of a market. Can only subscribe to one timeframe at a time for each symbol + + https://github.com/P2B-team/P2B-WSS-Public/blob/main/wss_documentation.md#kline-candlestick + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: 15m, 30m, 1h or 1d + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + timeframes = self.safe_value(self.options, 'timeframes', {}) + channel = self.safe_integer(timeframes, timeframe) + if channel is None: + raise BadRequest(self.id + ' watchOHLCV cannot take a timeframe of ' + timeframe) + market = self.market(symbol) + request = [ + market['id'], + channel, + ] + messageHash = 'kline::' + market['symbol'] + ohlcv = await self.subscribe('kline.subscribe', messageHash, request, params) + if self.newUpdates: + limit = ohlcv.getLimit(symbol, limit) + return self.filter_by_since_limit(ohlcv, since, limit, 0, True) + + async def watch_ticker(self, symbol: str, params={}) -> Ticker: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://github.com/P2B-team/P2B-WSS-Public/blob/main/wss_documentation.md#last-price + https://github.com/P2B-team/P2B-WSS-Public/blob/main/wss_documentation.md#market-status + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param dict [params.method]: 'state'(default) or 'price' + :returns dict: a `ticker structure ` + """ + await self.load_markets() + watchTickerOptions = self.safe_dict(self.options, 'watchTicker') + name = self.safe_string(watchTickerOptions, 'name', 'state') # or price + name, params = self.handle_option_and_params(params, 'method', 'name', name) + market = self.market(symbol) + symbol = market['symbol'] + self.options['tickerSubs'][market['id']] = True # we need to re-subscribe to all tickers upon watching a new ticker + tickerSubs = self.options['tickerSubs'] + request = list(tickerSubs.keys()) + messageHash = name + '::' + market['symbol'] + return await self.subscribe(name + '.subscribe', messageHash, request, params) + + async def watch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + + https://github.com/P2B-team/P2B-WSS-Public/blob/main/wss_documentation.md#last-price + https://github.com/P2B-team/P2B-WSS-Public/blob/main/wss_documentation.md#market-status + + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list + :param str[] [symbols]: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param dict [params.method]: 'state'(default) or 'price' + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, False) + watchTickerOptions = self.safe_dict(self.options, 'watchTicker') + name = self.safe_string(watchTickerOptions, 'name', 'state') # or price + name, params = self.handle_option_and_params(params, 'method', 'name', name) + messageHashes = [] + args = [] + for i in range(0, len(symbols)): + market = self.market(symbols[i]) + messageHashes.append(name + '::' + market['symbol']) + args.append(market['id']) + url = self.urls['api']['ws'] + request: dict = { + 'method': name + '.subscribe', + 'params': args, + 'id': self.milliseconds(), + } + await self.watch_multiple(url, messageHashes, self.extend(request, params), messageHashes) + return self.filter_by_array(self.tickers, 'symbol', symbols) + + async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://github.com/P2B-team/P2B-WSS-Public/blob/main/wss_documentation.md#deals + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + return await self.watch_trades_for_symbols([symbol], since, limit, params) + + async def watch_trades_for_symbols(self, symbols: List[str], since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a list of symbols + + https://github.com/P2B-team/P2B-WSS-Public/blob/main/wss_documentation.md#deals + + :param str[] symbols: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, False, True, True) + messageHashes = [] + if symbols is not None: + for i in range(0, len(symbols)): + messageHashes.append('deals::' + symbols[i]) + marketIds = self.market_ids(symbols) + url = self.urls['api']['ws'] + subscribe: dict = { + 'method': 'deals.subscribe', + 'params': marketIds, + 'id': self.milliseconds(), + } + query = self.extend(subscribe, params) + trades = await self.watch_multiple(url, messageHashes, query, messageHashes) + if self.newUpdates: + first = self.safe_value(trades, 0) + tradeSymbol = self.safe_string(first, 'symbol') + limit = trades.getLimit(tradeSymbol, limit) + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://github.com/P2B-team/P2B-WSS-Public/blob/main/wss_documentation.md#depth-of-market + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: 1-100, default=100 + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.interval]: 0, 0.00000001, 0.0000001, 0.000001, 0.00001, 0.0001, 0.001, 0.01, 0.1, interval of precision for order, default=0.001 + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + name = 'depth.subscribe' + messageHash = 'orderbook::' + market['symbol'] + interval = self.safe_string(params, 'interval', '0.001') + if limit is None: + limit = 100 + request = [ + market['id'], + limit, + interval, + ] + orderbook = await self.subscribe(name, messageHash, request, params) + return orderbook.limit() + + def handle_ohlcv(self, client: Client, message): + # + # { + # "method": "kline.update", + # "params": [ + # [ + # 1657648800, # Kline start time + # "0.054146", # Kline open price + # "0.053938", # Kline close price(current price) + # "0.054146", # Kline high price + # "0.053911", # Kline low price + # "596.4674", # Volume for stock currency + # "32.2298758767", # Volume for money currency + # "ETH_BTC" # Market + # ] + # ], + # "id": null + # } + # + data = self.safe_list(message, 'params') + data = self.safe_list(data, 0) + method = self.safe_string(message, 'method') + splitMethod = method.split('.') + channel = self.safe_string(splitMethod, 0) + marketId = self.safe_string(data, 7) + market = self.safe_market(marketId) + timeframes = self.safe_dict(self.options, 'timeframes', {}) + timeframe = self.find_timeframe(channel, timeframes) + symbol = self.safe_string(market, 'symbol') + messageHash = channel + '::' + symbol + parsed = self.parse_ohlcv(data, market) + self.ohlcvs[symbol] = self.safe_value(self.ohlcvs, symbol, {}) + stored = self.safe_value(self.ohlcvs[symbol], timeframe) + if symbol is not None: + if stored is None: + limit = self.safe_integer(self.options, 'OHLCVLimit', 1000) + stored = ArrayCacheByTimestamp(limit) + self.ohlcvs[symbol][timeframe] = stored + stored.append(parsed) + client.resolve(stored, messageHash) + return message + + def handle_trade(self, client: Client, message): + # + # { + # "method": "deals.update", + # "params": [ + # "ETH_BTC", + # [ + # { + # "id": 4503032979, # Order_id + # "amount": "0.103", + # "type": "sell", # Side + # "time": 1657661950.8487639, # Creation time + # "price": "0.05361" + # }, + # ... + # ] + # ], + # "id": null + # } + # + data = self.safe_list(message, 'params', []) + trades = self.safe_list(data, 1) + marketId = self.safe_string(data, 0) + market = self.safe_market(marketId) + symbol = self.safe_string(market, 'symbol') + tradesArray = self.safe_value(self.trades, symbol) + if tradesArray is None: + tradesLimit = self.safe_integer(self.options, 'tradesLimit', 1000) + tradesArray = ArrayCache(tradesLimit) + self.trades[symbol] = tradesArray + for i in range(0, len(trades)): + item = trades[i] + trade = self.parse_trade(item, market) + tradesArray.append(trade) + messageHash = 'deals::' + symbol + client.resolve(tradesArray, messageHash) + return message + + def handle_ticker(self, client: Client, message): + # + # state + # + # { + # "method": "state.update", + # "params": [ + # "ETH_BTC", + # { + # "high": "0.055774", # High price for the last 24h + # "close": "0.053679", # Close price for the last 24h + # "low": "0.053462", # Low price for the last 24h + # "period": 86400, # Period 24h + # "last": "0.053679", # Last price for the last 24h + # "volume": "38463.6132", # Stock volume for the last 24h + # "open": "0.055682", # Open price for the last 24h + # "deal": "2091.0038055314" # Money volume for the last 24h + # } + # ], + # "id": null + # } + # + # price + # + # { + # "method": "price.update", + # "params": [ + # "ETH_BTC", # market + # "0.053836" # last price + # ], + # "id": null + # } + # + data = self.safe_list(message, 'params', []) + marketId = self.safe_string(data, 0) + market = self.safe_market(marketId) + method = self.safe_string(message, 'method') + splitMethod = method.split('.') + messageHashStart = self.safe_string(splitMethod, 0) + tickerData = self.safe_dict(data, 1) + ticker = None + if method == 'price.update': + lastPrice = self.safe_string(data, 1) + ticker = self.safe_ticker({ + 'last': lastPrice, + 'close': lastPrice, + 'symbol': market['symbol'], + }) + else: + ticker = self.parse_ticker(tickerData, market) + symbol = ticker['symbol'] + self.tickers[symbol] = ticker + messageHash = messageHashStart + '::' + symbol + client.resolve(ticker, messageHash) + return message + + def handle_order_book(self, client: Client, message): + # + # { + # "method": "depth.update", + # "params": [ + # False, # True - all records, False - new records + # { + # "asks": [ # side + # [ + # "19509.81", # price + # "0.277" # amount + # ] + # ] + # }, + # "BTC_USDT" + # ], + # "id": null + # } + # + params = self.safe_list(message, 'params', []) + data = self.safe_dict(params, 1) + asks = self.safe_list(data, 'asks') + bids = self.safe_list(data, 'bids') + marketId = self.safe_string(params, 2) + market = self.safe_market(marketId) + symbol = market['symbol'] + messageHash = 'orderbook::' + market['symbol'] + subscription = self.safe_value(client.subscriptions, messageHash, {}) + limit = self.safe_integer(subscription, 'limit') + orderbook = self.safe_value(self.orderbooks, symbol) + if orderbook is None: + self.orderbooks[symbol] = self.order_book({}, limit) + orderbook = self.orderbooks[symbol] + if bids is not None: + for i in range(0, len(bids)): + bid = self.safe_value(bids, i) + price = self.safe_number(bid, 0) + amount = self.safe_number(bid, 1) + bookSide = orderbook['bids'] + bookSide.store(price, amount) + if asks is not None: + for i in range(0, len(asks)): + ask = self.safe_value(asks, i) + price = self.safe_number(ask, 0) + amount = self.safe_number(ask, 1) + bookside = orderbook['asks'] + bookside.store(price, amount) + orderbook['symbol'] = symbol + client.resolve(orderbook, messageHash) + + def handle_message(self, client: Client, message): + if self.handle_error_message(client, message): + return + result = self.safe_string(message, 'result') + if result == 'pong': + self.handle_pong(client, message) + return + method = self.safe_string(message, 'method') + methods: dict = { + 'depth.update': self.handle_order_book, + 'price.update': self.handle_ticker, + 'kline.update': self.handle_ohlcv, + 'state.update': self.handle_ticker, + 'deals.update': self.handle_trade, + } + endpoint = self.safe_value(methods, method) + if endpoint is not None: + endpoint(client, message) + + def handle_error_message(self, client: Client, message) -> Bool: + error = self.safe_string(message, 'error') + if error is not None: + raise ExchangeError(self.id + ' error: ' + self.json(error)) + return False + + def ping(self, client: Client): + """ + https://github.com/P2B-team/P2B-WSS-Public/blob/main/wss_documentation.md#ping + @param client + """ + return { + 'method': 'server.ping', + 'params': [], + 'id': self.milliseconds(), + } + + def handle_pong(self, client: Client, message): + # + # { + # error: null, + # result: 'pong', + # id: 1706539608030 + # } + # + client.lastPong = self.safe_integer(message, 'id') + return message + + def on_error(self, client: Client, error): + self.options['tickerSubs'] = self.create_safe_dictionary() + super(p2b, self).on_error(client, error) + + def on_close(self, client: Client, error): + self.options['tickerSubs'] = self.create_safe_dictionary() + super(p2b, self).on_close(client, error) diff --git a/ccxt/pro/paradex.py b/ccxt/pro/paradex.py new file mode 100644 index 0000000..22cdab1 --- /dev/null +++ b/ccxt/pro/paradex.py @@ -0,0 +1,352 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCache +from ccxt.base.types import Any, Bool, Int, OrderBook, Strings, Ticker, Tickers, Trade +from ccxt.async_support.base.ws.client import Client +from typing import List + + +class paradex(ccxt.async_support.paradex): + + def describe(self) -> Any: + return self.deep_extend(super(paradex, self).describe(), { + 'has': { + 'ws': True, + 'watchTicker': True, + 'watchTickers': True, + 'watchOrderBook': True, + 'watchOrders': False, + 'watchTrades': True, + 'watchTradesForSymbols': False, + 'watchBalance': False, + 'watchOHLCV': False, + }, + 'urls': { + 'logo': 'https://x.com/tradeparadex/photo', + 'api': { + 'ws': 'wss://ws.api.prod.paradex.trade/v1', + }, + 'test': { + 'ws': 'wss://ws.api.testnet.paradex.trade/v1', + }, + 'www': 'https://www.paradex.trade/', + 'doc': 'https://docs.api.testnet.paradex.trade/', + 'fees': 'https://docs.paradex.trade/getting-started/trading-fees', + 'referral': '', + }, + 'options': {}, + 'streaming': {}, + }) + + async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://docs.api.testnet.paradex.trade/#sub-trades-market_symbol-operation + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + messageHash = 'trades.' + if symbol is not None: + market = self.market(symbol) + messageHash += market['id'] + else: + messageHash += 'ALL' + url = self.urls['api']['ws'] + request: dict = { + 'jsonrpc': '2.0', + 'method': 'subscribe', + 'params': { + 'channel': messageHash, + }, + } + trades = await self.watch(url, messageHash, self.deep_extend(request, params), messageHash) + if self.newUpdates: + limit = trades.getLimit(symbol, limit) + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + def handle_trade(self, client: Client, message): + # + # { + # "jsonrpc": "2.0", + # "method": "subscription", + # "params": { + # "channel": "trades.ALL", + # "data": { + # "id": "1718179273230201709233240002", + # "market": "kBONK-USD-PERP", + # "side": "BUY", + # "size": "34028", + # "price": "0.028776", + # "created_at": 1718179273230, + # "trade_type": "FILL" + # } + # } + # } + # + params = self.safe_dict(message, 'params', {}) + data = self.safe_dict(params, 'data', {}) + parsedTrade = self.parse_trade(data) + symbol = parsedTrade['symbol'] + messageHash = self.safe_string(params, 'channel') + stored = self.safe_value(self.trades, symbol) + if stored is None: + stored = ArrayCache(self.safe_integer(self.options, 'tradesLimit', 1000)) + self.trades[symbol] = stored + stored.append(parsedTrade) + client.resolve(stored, messageHash) + return message + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://docs.api.testnet.paradex.trade/#sub-order_book-market_symbol-snapshot-15-refresh_rate-operation + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + messageHash = 'order_book.' + market['id'] + '.snapshot@15@100ms' + url = self.urls['api']['ws'] + request: dict = { + 'jsonrpc': '2.0', + 'method': 'subscribe', + 'params': { + 'channel': messageHash, + }, + } + orderbook = await self.watch(url, messageHash, self.deep_extend(request, params), messageHash) + return orderbook.limit() + + def handle_order_book(self, client: Client, message): + # + # { + # "jsonrpc": "2.0", + # "method": "subscription", + # "params": { + # "channel": "order_book.BTC-USD-PERP.snapshot@15@50ms", + # "data": { + # "seq_no": 14127815, + # "market": "BTC-USD-PERP", + # "last_updated_at": 1718267837265, + # "update_type": "s", + # "inserts": [ + # { + # "side": "BUY", + # "price": "67629.7", + # "size": "0.992" + # }, + # { + # "side": "SELL", + # "price": "69378.6", + # "size": "3.137" + # } + # ], + # "updates": [], + # "deletes": [] + # } + # } + # } + # + params = self.safe_dict(message, 'params', {}) + data = self.safe_dict(params, 'data', {}) + marketId = self.safe_string(data, 'market') + market = self.safe_market(marketId) + timestamp = self.safe_integer(data, 'last_updated_at') + symbol = market['symbol'] + if not (symbol in self.orderbooks): + self.orderbooks[symbol] = self.order_book() + orderbookData = { + 'bids': [], + 'asks': [], + } + inserts = self.safe_list(data, 'inserts') + for i in range(0, len(inserts)): + insert = self.safe_dict(inserts, i) + side = self.safe_string(insert, 'side') + price = self.safe_string(insert, 'price') + size = self.safe_string(insert, 'size') + if side == 'BUY': + orderbookData['bids'].append([price, size]) + else: + orderbookData['asks'].append([price, size]) + orderbook = self.orderbooks[symbol] + snapshot = self.parse_order_book(orderbookData, symbol, timestamp, 'bids', 'asks') + snapshot['nonce'] = self.safe_number(data, 'seq_no') + orderbook.reset(snapshot) + messageHash = self.safe_string(params, 'channel') + client.resolve(orderbook, messageHash) + + async def watch_ticker(self, symbol: str, params={}) -> Ticker: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://docs.api.testnet.paradex.trade/#sub-markets_summary-operation + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbol = self.symbol(symbol) + channel = 'markets_summary' + url = self.urls['api']['ws'] + request: dict = { + 'jsonrpc': '2.0', + 'method': 'subscribe', + 'params': { + 'channel': channel, + }, + } + messageHash = channel + '.' + symbol + return await self.watch(url, messageHash, self.deep_extend(request, params), messageHash) + + async def watch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list + + https://docs.api.testnet.paradex.trade/#sub-markets_summary-operation + + :param str[] symbols: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + channel = 'markets_summary' + url = self.urls['api']['ws'] + request: dict = { + 'jsonrpc': '2.0', + 'method': 'subscribe', + 'params': { + 'channel': channel, + }, + } + messageHashes = [] + if isinstance(symbols, list): + for i in range(0, len(symbols)): + messageHash = channel + '.' + symbols[i] + messageHashes.append(messageHash) + else: + messageHashes.append(channel) + newTickers = await self.watch_multiple(url, messageHashes, self.deep_extend(request, params), messageHashes) + if self.newUpdates: + result: dict = {} + result[newTickers['symbol']] = newTickers + return result + return self.filter_by_array(self.tickers, 'symbol', symbols) + + def handle_ticker(self, client: Client, message): + # + # { + # "jsonrpc": "2.0", + # "method": "subscription", + # "params": { + # "channel": "markets_summary", + # "data": { + # "symbol": "ORDI-USD-PERP", + # "oracle_price": "49.80885481", + # "mark_price": "49.80885481", + # "last_traded_price": "62.038", + # "bid": "49.822", + # "ask": "58.167", + # "volume_24h": "0", + # "total_volume": "54542628.66054200416", + # "created_at": 1718334307698, + # "underlying_price": "47.93", + # "open_interest": "6999.5", + # "funding_rate": "0.03919997509811", + # "price_change_rate_24h": "" + # } + # } + # } + # + params = self.safe_dict(message, 'params', {}) + data = self.safe_dict(params, 'data', {}) + marketId = self.safe_string(data, 'symbol') + market = self.safe_market(marketId) + symbol = market['symbol'] + channel = self.safe_string(params, 'channel') + messageHash = channel + '.' + symbol + ticker = self.parse_ticker(data, market) + self.tickers[symbol] = ticker + client.resolve(ticker, channel) + client.resolve(ticker, messageHash) + return message + + def handle_error_message(self, client: Client, message) -> Bool: + # + # { + # "jsonrpc": "2.0", + # "id": 0, + # "error": { + # "code": -32600, + # "message": "invalid subscribe request", + # "data": "invalid channel" + # }, + # "usIn": 1718179125962419, + # "usDiff": 76, + # "usOut": 1718179125962495 + # } + # + error = self.safe_dict(message, 'error') + if error is None: + return True + else: + errorCode = self.safe_string(error, 'code') + if errorCode is not None: + feedback = self.id + ' ' + self.json(error) + self.throw_exactly_matched_exception(self.exceptions['exact'], '-32600', feedback) + messageString = self.safe_value(error, 'message') + if messageString is not None: + self.throw_broadly_matched_exception(self.exceptions['broad'], messageString, feedback) + return False + + def handle_message(self, client: Client, message): + if not self.handle_error_message(client, message): + return + # + # { + # "jsonrpc": "2.0", + # "method": "subscription", + # "params": { + # "channel": "trades.ALL", + # "data": { + # "id": "1718179273230201709233240002", + # "market": "kBONK-USD-PERP", + # "side": "BUY", + # "size": "34028", + # "price": "0.028776", + # "created_at": 1718179273230, + # "trade_type": "FILL" + # } + # } + # } + # + data = self.safe_dict(message, 'params') + if data is not None: + channel = self.safe_string(data, 'channel') + parts = channel.split('.') + name = self.safe_string(parts, 0) + methods: dict = { + 'trades': self.handle_trade, + 'order_book': self.handle_order_book, + 'markets_summary': self.handle_ticker, + # ... + } + method = self.safe_value(methods, name) + if method is not None: + method(client, message) diff --git a/ccxt/pro/phemex.py b/ccxt/pro/phemex.py new file mode 100644 index 0000000..4ad2aae --- /dev/null +++ b/ccxt/pro/phemex.py @@ -0,0 +1,1496 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolById, ArrayCacheByTimestamp +import hashlib +from ccxt.base.types import Any, Balances, Int, Order, OrderBook, Str, Strings, Ticker, Tickers, Trade +from ccxt.async_support.base.ws.client import Client +from typing import List +from ccxt.base.errors import AuthenticationError +from ccxt.base.precise import Precise + + +class phemex(ccxt.async_support.phemex): + + def describe(self) -> Any: + return self.deep_extend(super(phemex, self).describe(), { + 'has': { + 'ws': True, + 'watchTicker': True, + 'watchTickers': True, + 'watchTrades': True, + 'watchMyTrades': True, + 'watchOrders': True, + 'watchOrderBook': True, + 'watchOHLCV': True, + 'watchPositions': None, # TODO + # mutli-endpoints are not supported: https://github.com/ccxt/ccxt/pull/21490 + 'watchOrderBookForSymbols': False, + 'watchTradesForSymbols': False, + 'watchOHLCVForSymbols': False, + 'watchBalance': True, + }, + 'urls': { + 'test': { + 'ws': 'wss://testnet-api.phemex.com/ws', + }, + 'api': { + 'ws': 'wss://ws.phemex.com', + }, + }, + 'options': { + 'tradesLimit': 1000, + 'OHLCVLimit': 1000, + }, + 'streaming': { + 'keepAlive': 9000, + }, + }) + + def from_en(self, en, scale): + if en is None: + return None + precise = Precise(en) + precise.decimals = self.sum(precise.decimals, scale) + precise.reduce() + return str(precise) + + def from_ep(self, ep, market=None): + if (ep is None) or (market is None): + return ep + return self.from_en(ep, self.safe_integer(market, 'priceScale')) + + def from_ev(self, ev, market=None): + if (ev is None) or (market is None): + return ev + return self.from_en(ev, self.safe_integer(market, 'valueScale')) + + def from_er(self, er, market=None): + if (er is None) or (market is None): + return er + return self.from_en(er, self.safe_integer(market, 'ratioScale')) + + def request_id(self): + requestId = self.sum(self.safe_integer(self.options, 'requestId', 0), 1) + self.options['requestId'] = requestId + return requestId + + def parse_swap_ticker(self, ticker, market=None): + # + # { + # "close": 442800, + # "fundingRate": 10000, + # "high": 445400, + # "indexPrice": 442621, + # "low": 428400, + # "markPrice": 442659, + # "open": 432200, + # "openInterest": 744183, + # "predFundingRate": 10000, + # "symbol": "LTCUSD", + # "turnover": 8133238294, + # "volume": 934292 + # } + # + marketId = self.safe_string(ticker, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + timestamp = self.safe_integer_product(ticker, 'timestamp', 0.000001) + lastString = self.from_ep(self.safe_string(ticker, 'close'), market) + last = self.parse_number(lastString) + quoteVolume = self.parse_number(self.from_ev(self.safe_string(ticker, 'turnover'), market)) + baseVolume = self.parse_number(self.from_ev(self.safe_string(ticker, 'volume'), market)) + change = None + percentage = None + average = None + openString = self.omit_zero(self.from_ep(self.safe_string(ticker, 'open'), market)) + open = self.parse_number(openString) + if (openString is not None) and (lastString is not None): + change = self.parse_number(Precise.string_sub(lastString, openString)) + average = self.parse_number(Precise.string_div(Precise.string_add(lastString, openString), '2')) + percentage = self.parse_number(Precise.string_mul(Precise.string_sub(Precise.string_div(lastString, openString), '1'), '100')) + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.parse_number(self.from_ep(self.safe_string(ticker, 'high'), market)), + 'low': self.parse_number(self.from_ep(self.safe_string(ticker, 'low'), market)), + 'bid': None, + 'bidVolume': None, + 'ask': None, + 'askVolume': None, + 'vwap': None, + 'open': open, + 'close': last, + 'last': last, + 'previousClose': None, # previous day close + 'change': change, + 'percentage': percentage, + 'average': average, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'markPrice': self.parse_number(self.from_ep(self.safe_string(ticker, 'markPrice'), market)), + 'indexPrice': self.parse_number(self.from_ep(self.safe_string(ticker, 'indexPrice'), market)), + 'info': ticker, + }) + + def parse_perpetual_ticker(self, ticker, market=None): + # + # [ + # "STXUSDT", + # "0.64649", + # "0.8628", + # "0.61215", + # "0.71737", + # "4519387", + # "3210827.98166", + # "697635", + # "0.71720205", + # "0.71720205", + # "0.0001", + # "0.0001", + # ] + # + marketId = self.safe_string(ticker, 0) + market = self.safe_market(marketId, market) + symbol = market['symbol'] + lastString = self.from_ep(self.safe_string(ticker, 4), market) + last = self.parse_number(lastString) + quoteVolume = self.parse_number(self.from_ev(self.safe_string(ticker, 6), market)) + baseVolume = self.parse_number(self.from_ev(self.safe_string(ticker, 5), market)) + change = None + percentage = None + average = None + openString = self.omit_zero(self.from_ep(self.safe_string(ticker, 1), market)) + open = self.parse_number(openString) + if (openString is not None) and (lastString is not None): + change = self.parse_number(Precise.string_sub(lastString, openString)) + average = self.parse_number(Precise.string_div(Precise.string_add(lastString, openString), '2')) + percentage = self.parse_number(Precise.string_mul(Precise.string_sub(Precise.string_div(lastString, openString), '1'), '100')) + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': None, + 'datetime': None, + 'high': self.parse_number(self.from_ep(self.safe_string(ticker, 2), market)), + 'low': self.parse_number(self.from_ep(self.safe_string(ticker, 3), market)), + 'bid': None, + 'bidVolume': None, + 'ask': None, + 'askVolume': None, + 'vwap': None, + 'open': open, + 'close': last, + 'last': last, + 'previousClose': None, # previous day close + 'change': change, + 'percentage': percentage, + 'average': average, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'info': ticker, + }) + + def handle_ticker(self, client: Client, message): + # + # { + # "spot_market24h": { + # "askEp": 958148000000, + # "bidEp": 957884000000, + # "highEp": 962000000000, + # "lastEp": 958220000000, + # "lowEp": 928049000000, + # "openEp": 935597000000, + # "symbol": "sBTCUSDT", + # "turnoverEv": 146074214388978, + # "volumeEv": 15492228900 + # }, + # "timestamp": 1592847265888272100 + # } + # + # swap + # + # { + # "market24h": { + # "close": 442800, + # "fundingRate": 10000, + # "high": 445400, + # "indexPrice": 442621, + # "low": 428400, + # "markPrice": 442659, + # "open": 432200, + # "openInterest": 744183, + # "predFundingRate": 10000, + # "symbol": "LTCUSD", + # "turnover": 8133238294, + # "volume": 934292 + # }, + # "timestamp": 1592845585373374500 + # } + # + # perpetual + # + # { + # "data": [ + # [ + # "STXUSDT", + # "0.64649", + # "0.8628", + # "0.61215", + # "0.71737", + # "4519387", + # "3210827.98166", + # "697635", + # "0.71720205", + # "0.71720205", + # "0.0001", + # "0.0001", + # ], + # ... + # ], + # "fields": [ + # "symbol", + # "openRp", + # "highRp", + # "lowRp", + # "lastRp", + # "volumeRq", + # "turnoverRv", + # "openInterestRv", + # "indexRp", + # "markRp", + # "fundingRateRr", + # "predFundingRateRr", + # ], + # "method": "perp_market24h_pack_p.update", + # "timestamp": "1677094918686806209", + # "type": "snapshot", + # } + # + tickers = [] + if 'market24h' in message: + ticker = self.safe_value(message, 'market24h') + tickers.append(self.parse_swap_ticker(ticker)) + elif 'spot_market24h' in message: + ticker = self.safe_value(message, 'spot_market24h') + tickers.append(self.parse_ticker(ticker)) + elif 'data' in message: + data = self.safe_value(message, 'data', []) + for i in range(0, len(data)): + tickers.append(self.parse_perpetual_ticker(data[i])) + for i in range(0, len(tickers)): + ticker = tickers[i] + symbol = ticker['symbol'] + messageHash = 'ticker:' + symbol + timestamp = self.safe_integer_product(message, 'timestamp', 0.000001) + ticker['timestamp'] = timestamp + ticker['datetime'] = self.iso8601(timestamp) + self.tickers[symbol] = ticker + client.resolve(ticker, messageHash) + + async def watch_balance(self, params={}) -> Balances: + """ + + https://github.com/phemex/phemex-api-docs/blob/master/Public-Hedged-Perpetual-API.md#subscribe-account-order-position-aop + https://github.com/phemex/phemex-api-docs/blob/master/Public-Contract-API-en.md#subscribe-account-order-position-aop + https://github.com/phemex/phemex-api-docs/blob/master/Public-Spot-API-en.md#subscribe-wallet-order-messages + + watch balance and get the amount of funds available for trading or funds locked in orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.settle]: set to USDT to use hedged perpetual api + :returns dict: a `balance structure ` + """ + await self.load_markets() + type = None + type, params = self.handle_market_type_and_params('watchBalance', None, params) + usePerpetualApi = self.safe_string(params, 'settle') == 'USDT' + messageHash = ':balance' + messageHash = 'perpetual' + messageHash if usePerpetualApi else type + messageHash + return await self.subscribe_private(type, messageHash, params) + + def handle_balance(self, type, client, message): + # spot + # [ + # { + # "balanceEv": 0, + # "currency": "BTC", + # "lastUpdateTimeNs": "1650442638722099092", + # "lockedTradingBalanceEv": 0, + # "lockedWithdrawEv": 0, + # "userID": 2647224 + # }, + # { + # "balanceEv": 1154232337, + # "currency": "USDT", + # "lastUpdateTimeNs": "1650442617610017597", + # "lockedTradingBalanceEv": 0, + # "lockedWithdrawEv": 0, + # "userID": 2647224 + # } + # ] + # swap + # [ + # { + # "accountBalanceEv": 0, + # "accountID": 26472240001, + # "bonusBalanceEv": 0, + # "currency": "BTC", + # "totalUsedBalanceEv": 0, + # "userID": 2647224 + # } + # ] + # perpetual + # [ + # { + # "accountBalanceRv": "1508.452588802237", + # "accountID": 9328670003, + # "bonusBalanceRv": "0", + # "currency": "USDT", + # "totalUsedBalanceRv": "343.132599666883", + # "userID": 932867 + # } + # ] + # + self.balance['info'] = message + for i in range(0, len(message)): + balance = message[i] + currencyId = self.safe_string(balance, 'currency') + code = self.safe_currency_code(currencyId) + currency = self.safe_value(self.currencies, code, {}) + scale = self.safe_integer(currency, 'valueScale', 8) + account = self.account() + used = self.safe_string(balance, 'totalUsedBalanceRv') + if used is None: + usedEv = self.safe_string(balance, 'totalUsedBalanceEv') + if usedEv is None: + lockedTradingBalanceEv = self.safe_string(balance, 'lockedTradingBalanceEv') + lockedWithdrawEv = self.safe_string_2(balance, 'lockedWithdrawEv', 'lockedWithdrawRv') + usedEv = Precise.string_add(lockedTradingBalanceEv, lockedWithdrawEv) + used = self.from_en(usedEv, scale) + total = self.safe_string(balance, 'accountBalanceRv') + if total is None: + totalEv = self.safe_string_2(balance, 'accountBalanceEv', 'balanceEv') + total = self.from_en(totalEv, scale) + account['used'] = used + account['total'] = total + self.balance[code] = account + self.balance = self.safe_balance(self.balance) + messageHash = type + ':balance' + client.resolve(self.balance, messageHash) + + def handle_trades(self, client: Client, message): + # + # { + # "sequence": 1795484727, + # "symbol": "sBTCUSDT", + # "trades": [ + # [1592891002064516600, "Buy", 964020000000, 1431000], + # [1592890978987934500, "Sell", 963704000000, 1401800], + # [1592890972918701800, "Buy", 963938000000, 2018600], + # ], + # "type": "snapshot" + # } + # perpetual + # { + # "sequence": 1230197759, + # "symbol": "BTCUSDT", + # "trades_p": [ + # [ + # 1677094244729433000, + # "Buy", + # "23800.4", + # "2.455", + # ], + # ], + # "type": "snapshot", + # } + # + name = 'trade' + marketId = self.safe_string(message, 'symbol') + market = self.safe_market(marketId) + symbol = market['symbol'] + messageHash = name + ':' + symbol + stored = self.safe_value(self.trades, symbol) + if stored is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + stored = ArrayCache(limit) + self.trades[symbol] = stored + trades = self.safe_value_2(message, 'trades', 'trades_p', []) + parsed = self.parse_trades(trades, market) + for i in range(0, len(parsed)): + stored.append(parsed[i]) + client.resolve(stored, messageHash) + + def handle_ohlcv(self, client: Client, message): + # + # { + # "kline": [ + # [1592905200, 60, 960688000000, 960709000000, 960709000000, 960400000000, 960400000000, 848100, 8146756046], + # [1592905140, 60, 960718000000, 960716000000, 960717000000, 960560000000, 960688000000, 4284900, 41163743512], + # [1592905080, 60, 960513000000, 960684000000, 960718000000, 960684000000, 960718000000, 4880500, 46887494349], + # ], + # "sequence": 1804401474, + # "symbol": "sBTCUSDT", + # "type": "snapshot" + # } + # perpetual + # { + # "kline_p": [ + # [ + # 1677094560, + # 60, + # "23746.2", + # "23746.1", + # "23757.6", + # "23736.9", + # "23754.8", + # "34.273", + # "813910.208", + # ], + # ], + # "sequence": 1230786017, + # "symbol": "BTCUSDT", + # "type": "incremental", + # } + # + marketId = self.safe_string(message, 'symbol') + market = self.safe_market(marketId) + symbol = market['symbol'] + candles = self.safe_value_2(message, 'kline', 'kline_p', []) + first = self.safe_value(candles, 0, []) + interval = self.safe_string(first, 1) + timeframe = self.find_timeframe(interval) + if timeframe is not None: + messageHash = 'kline:' + timeframe + ':' + symbol + ohlcvs = self.parse_ohlcvs(candles, market) + self.ohlcvs[symbol] = self.safe_value(self.ohlcvs, symbol, {}) + stored = self.safe_value(self.ohlcvs[symbol], timeframe) + if stored is None: + limit = self.safe_integer(self.options, 'OHLCVLimit', 1000) + stored = ArrayCacheByTimestamp(limit) + self.ohlcvs[symbol][timeframe] = stored + for i in range(0, len(ohlcvs)): + candle = ohlcvs[i] + stored.append(candle) + client.resolve(stored, messageHash) + + async def watch_ticker(self, symbol: str, params={}) -> Ticker: + """ + + https://github.com/phemex/phemex-api-docs/blob/master/Public-Hedged-Perpetual-API.md#subscribe-24-hours-ticker + https://github.com/phemex/phemex-api-docs/blob/master/Public-Contract-API-en.md#subscribe-24-hours-ticker + https://github.com/phemex/phemex-api-docs/blob/master/Public-Spot-API-en.md#subscribe-24-hours-ticker + + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + isSwap = market['swap'] + settleIsUSDT = market['settle'] == 'USDT' + name = 'spot_market24h' + if isSwap: + name = 'perp_market24h_pack_p' if settleIsUSDT else 'market24h' + url = self.urls['api']['ws'] + requestId = self.request_id() + subscriptionHash = name + '.subscribe' + messageHash = 'ticker:' + symbol + subscribe: dict = { + 'method': subscriptionHash, + 'id': requestId, + 'params': [], + } + request = self.deep_extend(subscribe, params) + return await self.watch(url, messageHash, request, subscriptionHash) + + async def watch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + + https://github.com/phemex/phemex-api-docs/blob/master/Public-Hedged-Perpetual-API.md#subscribe-24-hours-ticker + https://github.com/phemex/phemex-api-docs/blob/master/Public-Contract-API-en.md#subscribe-24-hours-ticker + https://github.com/phemex/phemex-api-docs/blob/master/Public-Spot-API-en.md#subscribe-24-hours-ticker + + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list + :param str[] [symbols]: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.channel]: the channel to subscribe to, tickers by default. Can be tickers, sprd-tickers, index-tickers, block-tickers + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, False) + first = symbols[0] + market = self.market(first) + isSwap = market['swap'] + settleIsUSDT = market['settle'] == 'USDT' + name = 'spot_market24h' + if isSwap: + name = 'perp_market24h_pack_p' if settleIsUSDT else 'market24h' + url = self.urls['api']['ws'] + requestId = self.request_id() + subscriptionHash = name + '.subscribe' + messageHashes = [] + for i in range(0, len(symbols)): + messageHashes.append('ticker:' + symbols[i]) + subscribe: dict = { + 'method': subscriptionHash, + 'id': requestId, + 'params': [], + } + request = self.deep_extend(subscribe, params) + ticker = await self.watch_multiple(url, messageHashes, request, messageHashes) + if self.newUpdates: + result: dict = {} + result[ticker['symbol']] = ticker + return result + return self.filter_by_array(self.tickers, 'symbol', symbols) + + async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + + https://github.com/phemex/phemex-api-docs/blob/master/Public-Hedged-Perpetual-API.md#subscribe-trade + https://github.com/phemex/phemex-api-docs/blob/master/Public-Contract-API-en.md#subscribe-trade + https://github.com/phemex/phemex-api-docs/blob/master/Public-Spot-API-en.md#subscribe-trade + + get the list of most recent trades for a particular symbol + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + url = self.urls['api']['ws'] + requestId = self.request_id() + isSwap = market['swap'] + settleIsUSDT = market['settle'] == 'USDT' + name = 'trade_p' if (isSwap and settleIsUSDT) else 'trade' + messageHash = 'trade:' + symbol + method = name + '.subscribe' + subscribe: dict = { + 'method': method, + 'id': requestId, + 'params': [ + market['id'], + ], + } + request = self.deep_extend(subscribe, params) + trades = await self.watch(url, messageHash, request, messageHash) + if self.newUpdates: + limit = trades.getLimit(symbol, limit) + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + + https://github.com/phemex/phemex-api-docs/blob/master/Public-Spot-API-en.md#subscribe-orderbook + https://github.com/phemex/phemex-api-docs/blob/master/Public-Hedged-Perpetual-API.md#subscribe-orderbook-for-new-model + https://github.com/phemex/phemex-api-docs/blob/master/Public-Contract-API-en.md#subscribe-30-levels-orderbook + https://github.com/phemex/phemex-api-docs/blob/master/Public-Contract-API-en.md#subscribe-full-orderbook + + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + url = self.urls['api']['ws'] + requestId = self.request_id() + isSwap = market['swap'] + settleIsUSDT = market['settle'] == 'USDT' + name = 'orderbook_p' if (isSwap and settleIsUSDT) else 'orderbook' + messageHash = 'orderbook:' + symbol + method = name + '.subscribe' + subscribe: dict = { + 'method': method, + 'id': requestId, + 'params': [ + market['id'], + ], + } + request = self.deep_extend(subscribe, params) + orderbook = await self.watch(url, messageHash, request, messageHash) + return orderbook.limit() + + async def watch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + + https://github.com/phemex/phemex-api-docs/blob/master/Public-Hedged-Perpetual-API.md#subscribe-kline + https://github.com/phemex/phemex-api-docs/blob/master/Public-Contract-API-en.md#subscribe-kline + https://github.com/phemex/phemex-api-docs/blob/master/Public-Spot-API-en.md#subscribe-kline + + watches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + url = self.urls['api']['ws'] + requestId = self.request_id() + isSwap = market['swap'] + settleIsUSDT = market['settle'] == 'USDT' + name = 'kline_p' if (isSwap and settleIsUSDT) else 'kline' + messageHash = 'kline:' + timeframe + ':' + symbol + method = name + '.subscribe' + subscribe: dict = { + 'method': method, + 'id': requestId, + 'params': [ + market['id'], + self.safe_integer(self.timeframes, timeframe), + ], + } + request = self.deep_extend(subscribe, params) + ohlcv = await self.watch(url, messageHash, request, messageHash) + if self.newUpdates: + limit = ohlcv.getLimit(symbol, limit) + return self.filter_by_since_limit(ohlcv, since, limit, 0, True) + + def custom_handle_delta(self, bookside, delta, market=None): + bidAsk = self.custom_parse_bid_ask(delta, 0, 1, market) + bookside.storeArray(bidAsk) + + def custom_handle_deltas(self, bookside, deltas, market=None): + for i in range(0, len(deltas)): + self.custom_handle_delta(bookside, deltas[i], market) + + def handle_order_book(self, client: Client, message): + # + # { + # "book": { + # "asks": [ + # [960316000000, 6993800], + # [960318000000, 13183000], + # [960319000000, 9170200], + # ], + # "bids": [ + # [959941000000, 8385300], + # [959939000000, 10296600], + # [959930000000, 3672400], + # ] + # }, + # "depth": 30, + # "sequence": 1805784701, + # "symbol": "sBTCUSDT", + # "timestamp": 1592908460404461600, + # "type": "snapshot" + # } + # perpetual + # { + # "depth": 30, + # "orderbook_p": { + # "asks": [ + # [ + # "23788.5", + # "0.13", + # ], + # ], + # "bids": [ + # [ + # "23787.8", + # "1.836", + # ], + # ], + # }, + # "sequence": 1230347368, + # "symbol": "BTCUSDT", + # "timestamp": "1677093457306978852", + # "type": "snapshot", + # } + # + marketId = self.safe_string(message, 'symbol') + market = self.safe_market(marketId) + symbol = market['symbol'] + type = self.safe_string(message, 'type') + depth = self.safe_integer(message, 'depth') + name = 'orderbook' + messageHash = name + ':' + symbol + nonce = self.safe_integer(message, 'sequence') + timestamp = self.safe_integer_product(message, 'timestamp', 0.000001) + if type == 'snapshot': + book = self.safe_value_2(message, 'book', 'orderbook_p', {}) + snapshot = self.custom_parse_order_book(book, symbol, timestamp, 'bids', 'asks', 0, 1, market) + snapshot['nonce'] = nonce + orderbook = self.order_book(snapshot, depth) + self.orderbooks[symbol] = orderbook + client.resolve(orderbook, messageHash) + else: + if symbol in self.orderbooks: + orderbook = self.orderbooks[symbol] + changes = self.safe_dict_2(message, 'book', 'orderbook_p', {}) + asks = self.safe_list(changes, 'asks', []) + bids = self.safe_list(changes, 'bids', []) + self.custom_handle_deltas(orderbook['asks'], asks, market) + self.custom_handle_deltas(orderbook['bids'], bids, market) + orderbook['nonce'] = nonce + orderbook['timestamp'] = timestamp + orderbook['datetime'] = self.iso8601(timestamp) + self.orderbooks[symbol] = orderbook + client.resolve(orderbook, messageHash) + + async def watch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + watches information on multiple trades made by the user + :param str symbol: unified market symbol of the market trades were made in + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trade structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + market = None + type = None + messageHash = 'trades:' + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + messageHash = messageHash + market['symbol'] + if market['settle'] == 'USDT': + params = self.extend(params) + params['settle'] = 'USDT' + type, params = self.handle_market_type_and_params('watchMyTrades', market, params) + if symbol is None: + settle = self.safe_string(params, 'settle') + messageHash = (messageHash + 'perpetual') if (settle == 'USDT') else (messageHash + type) + trades = await self.subscribe_private(type, messageHash, params) + if self.newUpdates: + limit = trades.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(trades, symbol, since, limit, True) + + def handle_my_trades(self, client: Client, message): + # + # swap + # [ + # { + # "avgPriceEp":4138763000000, + # "baseCurrency":"BTC", + # "baseQtyEv":0, + # "clOrdID":"7956e0be-e8be-93a0-2887-ca504d85cda2", + # "execBaseQtyEv":30100, + # "execFeeEv":31, + # "execID":"d3b10cfa-84e3-5752-828e-78a79617e598", + # "execPriceEp":4138763000000, + # "execQuoteQtyEv":1245767663, + # "feeCurrency":"BTC", + # "lastLiquidityInd":"RemovedLiquidity", + # "ordType":"Market", + # "orderID":"34a4b1a8-ac3a-4580-b3e6-a6d039f27195", + # "priceEp":4549022000000, + # "qtyType":"ByQuote", + # "quoteCurrency":"USDT", + # "quoteQtyEv":1248000000, + # "side":"Buy", + # "symbol":"sBTCUSDT", + # "tradeType":"Trade", + # "transactTimeNs":"1650442617609928764", + # "userID":2647224 + # } + # ] + # perpetual + # [ + # { + # "accountID": 9328670003, + # "action": "New", + # "actionBy": "ByUser", + # "actionTimeNs": 1666858780876924611, + # "addedSeq": 77751555, + # "apRp": "0", + # "bonusChangedAmountRv": "0", + # "bpRp": "0", + # "clOrdID": "c0327a7d-9064-62a9-28f6-2db9aaaa04e0", + # "closedPnlRv": "0", + # "closedSize": "0", + # "code": 0, + # "cumFeeRv": "0", + # "cumQty": "0", + # "cumValueRv": "0", + # "curAccBalanceRv": "1508.489893982237", + # "curAssignedPosBalanceRv": "24.62786650928", + # "curBonusBalanceRv": "0", + # "curLeverageRr": "-10", + # "curPosSide": "Buy", + # "curPosSize": "0.043", + # "curPosTerm": 1, + # "curPosValueRv": "894.0689", + # "curRiskLimitRv": "1000000", + # "currency": "USDT", + # "cxlRejReason": 0, + # "displayQty": "0.003", + # "execFeeRv": "0", + # "execID": "00000000-0000-0000-0000-000000000000", + # "execPriceRp": "20723.7", + # "execQty": "0", + # "execSeq": 77751555, + # "execStatus": "New", + # "execValueRv": "0", + # "feeRateRr": "0", + # "leavesQty": "0.003", + # "leavesValueRv": "63.4503", + # "message": "No error", + # "ordStatus": "New", + # "ordType": "Market", + # "orderID": "fa64c6f2-47a4-4929-aab4-b7fa9bbc4323", + # "orderQty": "0.003", + # "pegOffsetValueRp": "0", + # "posSide": "Long", + # "priceRp": "21150.1", + # "relatedPosTerm": 1, + # "relatedReqNum": 11, + # "side": "Buy", + # "slTrigger": "ByMarkPrice", + # "stopLossRp": "0", + # "stopPxRp": "0", + # "symbol": "BTCUSDT", + # "takeProfitRp": "0", + # "timeInForce": "ImmediateOrCancel", + # "tpTrigger": "ByLastPrice", + # "tradeType": "Amend", + # "transactTimeNs": 1666858780881545305, + # "userID": 932867 + # }, + # ... + # ] + # + channel = 'trades' + tradesLength = len(message) + if tradesLength == 0: + return + cachedTrades = self.myTrades + if cachedTrades is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + cachedTrades = ArrayCacheBySymbolById(limit) + marketIds: dict = {} + type = None + for i in range(0, len(message)): + rawTrade = message[i] + marketId = self.safe_string(rawTrade, 'symbol') + market = self.safe_market(marketId) + parsed = self.parse_trade(rawTrade) + cachedTrades.append(parsed) + symbol = parsed['symbol'] + if type is None: + type = 'perpetual' if (market['settle'] == 'USDT') else market['type'] + marketIds[symbol] = True + keys = list(marketIds.keys()) + for i in range(0, len(keys)): + market = keys[i] + hash = channel + ':' + market + client.resolve(cachedTrades, hash) + # generic subscription + messageHash = channel + ':' + type + client.resolve(cachedTrades, messageHash) + + async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + watches information on multiple orders made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + messageHash = 'orders:' + market = None + type = None + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + messageHash = messageHash + market['symbol'] + if market['settle'] == 'USDT': + params = self.extend(params) + params['settle'] = 'USDT' + type, params = self.handle_market_type_and_params('watchOrders', market, params) + isUSDTSettled = self.safe_string(params, 'settle') == 'USDT' + if symbol is None: + messageHash = (messageHash + 'perpetual') if (isUSDTSettled) else (messageHash + type) + orders = await self.subscribe_private(type, messageHash, params) + if self.newUpdates: + limit = orders.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(orders, symbol, since, limit, True) + + def handle_orders(self, client: Client, message): + # spot update + # { + # "closed":[ + # { + # "action":"New", + # "avgPriceEp":4138763000000, + # "baseCurrency":"BTC", + # "baseQtyEv":0, + # "bizError":0, + # "clOrdID":"7956e0be-e8be-93a0-2887-ca504d85cda2", + # "createTimeNs":"1650442617606017583", + # "cumBaseQtyEv":30100, + # "cumFeeEv":31, + # "cumQuoteQtyEv":1245767663, + # "cxlRejReason":0, + # "feeCurrency":"BTC", + # "leavesBaseQtyEv":0, + # "leavesQuoteQtyEv":0, + # "ordStatus":"Filled", + # "ordType":"Market", + # "orderID":"34a4b1a8-ac3a-4580-b3e6-a6d039f27195", + # "pegOffsetValueEp":0, + # "priceEp":4549022000000, + # "qtyType":"ByQuote", + # "quoteCurrency":"USDT", + # "quoteQtyEv":1248000000, + # "side":"Buy", + # "stopPxEp":0, + # "symbol":"sBTCUSDT", + # "timeInForce":"ImmediateOrCancel", + # "tradeType":"Trade", + # "transactTimeNs":"1650442617609928764", + # "triggerTimeNs":0, + # "userID":2647224 + # } + # ], + # "fills":[ + # { + # "avgPriceEp":4138763000000, + # "baseCurrency":"BTC", + # "baseQtyEv":0, + # "clOrdID":"7956e0be-e8be-93a0-2887-ca504d85cda2", + # "execBaseQtyEv":30100, + # "execFeeEv":31, + # "execID":"d3b10cfa-84e3-5752-828e-78a79617e598", + # "execPriceEp":4138763000000, + # "execQuoteQtyEv":1245767663, + # "feeCurrency":"BTC", + # "lastLiquidityInd":"RemovedLiquidity", + # "ordType":"Market", + # "orderID":"34a4b1a8-ac3a-4580-b3e6-a6d039f27195", + # "priceEp":4549022000000, + # "qtyType":"ByQuote", + # "quoteCurrency":"USDT", + # "quoteQtyEv":1248000000, + # "side":"Buy", + # "symbol":"sBTCUSDT", + # "tradeType":"Trade", + # "transactTimeNs":"1650442617609928764", + # "userID":2647224 + # } + # ], + # "open":[ + # { + # "action":"New", + # "avgPriceEp":0, + # "baseCurrency":"LTC", + # "baseQtyEv":0, + # "bizError":0, + # "clOrdID":"2c0e5eb5-efb7-60d3-2e5f-df175df412ef", + # "createTimeNs":"1650446670073853755", + # "cumBaseQtyEv":0, + # "cumFeeEv":0, + # "cumQuoteQtyEv":0, + # "cxlRejReason":0, + # "feeCurrency":"LTC", + # "leavesBaseQtyEv":0, + # "leavesQuoteQtyEv":1000000000, + # "ordStatus":"New", + # "ordType":"Limit", + # "orderID":"d2aad92f-50f5-441a-957b-8184b146e3fb", + # "pegOffsetValueEp":0, + # "priceEp":5000000000, + # "qtyType":"ByQuote", + # "quoteCurrency":"USDT", + # "quoteQtyEv":1000000000, + # "side":"Buy", + # } + # ] + # }, + # perpetual + # [ + # { + # "accountID": 40183400003, + # "action": "New", + # "actionBy": "ByUser", + # "actionTimeNs": "1674110665380190869", + # "addedSeq": 678760103, + # "apRp": "0", + # "bonusChangedAmountRv": "0", + # "bpRp": "0", + # "clOrdID": '', + # "cl_req_code": 0, + # "closedPnlRv": "0", + # "closedSize": "0", + # "code": 0, + # "cumFeeRv": "0", + # "cumQty": "0.001", + # "cumValueRv": "20.849", + # "curAccBalanceRv": "19.9874906", + # "curAssignedPosBalanceRv": "0", + # "curBonusBalanceRv": "0", + # "curLeverageRr": "-10", + # "curPosSide": "Buy", + # "curPosSize": "0.001", + # "curPosTerm": 1, + # "curPosValueRv": "20.849", + # "curRiskLimitRv": "1000000", + # "currency": "USDT", + # "cxlRejReason": 0, + # "displayQty": "0.001", + # "execFeeRv": "0.0125094", + # "execID": "b88d2950-04a2-52d8-8927-346059900242", + # "execPriceRp": "20849", + # "execQty": "0.001", + # "execSeq": 678760103, + # "execStatus": "TakerFill", + # "execValueRv": "20.849", + # "feeRateRr": "0.0006", + # "lastLiquidityInd": "RemovedLiquidity", + # "leavesQty": "0", + # "leavesValueRv": "0", + # "message": "No error", + # "ordStatus": "Filled", + # "ordType": "Market", + # "orderID": "79620ed2-54c6-4645-a35c-7057e687c576", + # "orderQty": "0.001", + # "pegOffsetProportionRr": "0", + # "pegOffsetValueRp": "0", + # "posSide": "Long", + # "priceRp": "21476.3", + # "relatedPosTerm": 1, + # "relatedReqNum": 4, + # "side": "Buy", + # "slTrigger": "ByMarkPrice", + # "stopLossRp": "0", + # "stopPxRp": "0", + # "symbol": "BTCUSDT", + # "takeProfitRp": "0", + # "timeInForce": "ImmediateOrCancel", + # "tpTrigger": "ByLastPrice", + # "tradeType": "Trade", + # "transactTimeNs": "1674110665387882268", + # "userID": 4018340 + # }, + # ... + # ] + # + trades = [] + parsedOrders = [] + if ('closed' in message) or ('fills' in message) or ('open' in message): + closed = self.safe_value(message, 'closed', []) + open = self.safe_value(message, 'open', []) + orders = self.array_concat(open, closed) + ordersLength = len(orders) + if ordersLength == 0: + return + trades = self.safe_value(message, 'fills', []) + for i in range(0, len(orders)): + rawOrder = orders[i] + parsedOrder = self.parse_order(rawOrder) + parsedOrders.append(parsedOrder) + else: + messageLength = len(message) + if messageLength == 0: + return + for i in range(0, len(message)): + update = message[i] + action = self.safe_string(update, 'action') + if (action is not None) and (action != 'Cancel'): + # order + trade info together + trades.append(update) + parsedOrder = self.parse_ws_swap_order(update) + parsedOrders.append(parsedOrder) + self.handle_my_trades(client, trades) + limit = self.safe_integer(self.options, 'ordersLimit', 1000) + marketIds: dict = {} + if self.orders is None: + self.orders = ArrayCacheBySymbolById(limit) + type = None + stored = self.orders + for i in range(0, len(parsedOrders)): + parsed = parsedOrders[i] + stored.append(parsed) + symbol = parsed['symbol'] + market = self.market(symbol) + if type is None: + isUsdt = market['settle'] == 'USDT' + type = 'perpetual' if isUsdt else market['type'] + marketIds[symbol] = True + keys = list(marketIds.keys()) + for i in range(0, len(keys)): + currentMessageHash = 'orders' + ':' + keys[i] + client.resolve(self.orders, currentMessageHash) + # resolve generic subscription(spot or swap) + messageHash = 'orders:' + type + client.resolve(self.orders, messageHash) + + def parse_ws_swap_order(self, order, market=None): + # + # swap + # { + # "accountID":26472240002, + # "action":"Cancel", + # "actionBy":"ByUser", + # "actionTimeNs":"1650450096104760797", + # "addedSeq":26975849309, + # "bonusChangedAmountEv":0, + # "clOrdID":"d9675963-5e4e-6fc8-898a-ec8b934c1c61", + # "closedPnlEv":0, + # "closedSize":0, + # "code":0, + # "cumQty":0, + # "cumValueEv":0, + # "curAccBalanceEv":400079, + # "curAssignedPosBalanceEv":0, + # "curBonusBalanceEv":0, + # "curLeverageEr":0, + # "curPosSide":"None", + # "curPosSize":0, + # "curPosTerm":1, + # "curPosValueEv":0, + # "curRiskLimitEv":5000000000, + # "currency":"USD", + # "cxlRejReason":0, + # "displayQty":0, + # "execFeeEv":0, + # "execID":"00000000-0000-0000-0000-000000000000", + # "execPriceEp":0, + # "execQty":1, + # "execSeq":26975862338, + # "execStatus":"Canceled", + # "execValueEv":0, + # "feeRateEr":0, + # "leavesQty":0, + # "leavesValueEv":0, + # "message":"No error", + # "ordStatus":"Canceled", + # "ordType":"Limit", + # "orderID":"8141deb9-8f94-48f6-9421-a4e3a791537b", + # "orderQty":1, + # "pegOffsetValueEp":0, + # "priceEp":9521, + # "relatedPosTerm":1, + # "relatedReqNum":4, + # "side":"Buy", + # "slTrigger":"ByMarkPrice", + # "stopLossEp":0, + # "stopPxEp":0, + # "symbol":"ADAUSD", + # "takeProfitEp":0, + # "timeInForce":"GoodTillCancel", + # "tpTrigger":"ByLastPrice", + # "transactTimeNs":"1650450096108143014", + # "userID":2647224 + # } + # perpetual + # { + # "accountID": 40183400003, + # "action": "New", + # "actionBy": "ByUser", + # "actionTimeNs": "1674110665380190869", + # "addedSeq": 678760103, + # "apRp": "0", + # "bonusChangedAmountRv": "0", + # "bpRp": "0", + # "clOrdID": '', + # "cl_req_code": 0, + # "closedPnlRv": "0", + # "closedSize": "0", + # "code": 0, + # "cumFeeRv": "0", + # "cumQty": "0.001", + # "cumValueRv": "20.849", + # "curAccBalanceRv": "19.9874906", + # "curAssignedPosBalanceRv": "0", + # "curBonusBalanceRv": "0", + # "curLeverageRr": "-10", + # "curPosSide": "Buy", + # "curPosSize": "0.001", + # "curPosTerm": 1, + # "curPosValueRv": "20.849", + # "curRiskLimitRv": "1000000", + # "currency": "USDT", + # "cxlRejReason": 0, + # "displayQty": "0.001", + # "execFeeRv": "0.0125094", + # "execID": "b88d2950-04a2-52d8-8927-346059900242", + # "execPriceRp": "20849", + # "execQty": "0.001", + # "execSeq": 678760103, + # "execStatus": "TakerFill", + # "execValueRv": "20.849", + # "feeRateRr": "0.0006", + # "lastLiquidityInd": "RemovedLiquidity", + # "leavesQty": "0", + # "leavesValueRv": "0", + # "message": "No error", + # "ordStatus": "Filled", + # "ordType": "Market", + # "orderID": "79620ed2-54c6-4645-a35c-7057e687c576", + # "orderQty": "0.001", + # "pegOffsetProportionRr": "0", + # "pegOffsetValueRp": "0", + # "posSide": "Long", + # "priceRp": "21476.3", + # "relatedPosTerm": 1, + # "relatedReqNum": 4, + # "side": "Buy", + # "slTrigger": "ByMarkPrice", + # "stopLossRp": "0", + # "stopPxRp": "0", + # "symbol": "BTCUSDT", + # "takeProfitRp": "0", + # "timeInForce": "ImmediateOrCancel", + # "tpTrigger": "ByLastPrice", + # "tradeType": "Trade", + # "transactTimeNs": "1674110665387882268", + # "userID": 4018340 + # } + # + id = self.safe_string(order, 'orderID') + clientOrderId = self.safe_string(order, 'clOrdID') + if (clientOrderId is not None) and (len(clientOrderId) < 1): + clientOrderId = None + marketId = self.safe_string(order, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + status = self.parse_order_status(self.safe_string(order, 'ordStatus')) + side = self.safe_string_lower(order, 'side') + type = self.parseOrderType(self.safe_string(order, 'ordType')) + price = self.safe_string(order, 'priceRp', self.from_ep(self.safe_string(order, 'priceEp'), market)) + amount = self.safe_string(order, 'orderQty') + filled = self.safe_string(order, 'cumQty') + remaining = self.safe_string(order, 'leavesQty') + timestamp = self.safe_integer_product(order, 'actionTimeNs', 0.000001) + cost = self.safe_string(order, 'cumValueRv', self.from_ev(self.safe_string(order, 'cumValueEv'), market)) + lastTradeTimestamp = self.safe_integer_product(order, 'transactTimeNs', 0.000001) + if lastTradeTimestamp == 0: + lastTradeTimestamp = None + timeInForce = self.parse_time_in_force(self.safe_string(order, 'timeInForce')) + stopPrice = self.safe_string(order, 'stopPx') + postOnly = (timeInForce == 'PO') + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': clientOrderId, + 'datetime': self.iso8601(timestamp), + 'timestamp': timestamp, + 'lastTradeTimestamp': lastTradeTimestamp, + 'symbol': symbol, + 'type': type, + 'timeInForce': timeInForce, + 'postOnly': postOnly, + 'side': side, + 'price': price, + 'stopPrice': stopPrice, + 'triggerPrice': stopPrice, + 'amount': amount, + 'filled': filled, + 'remaining': remaining, + 'cost': cost, + 'average': None, + 'status': status, + 'fee': None, + 'trades': None, + }, market) + + def handle_message(self, client: Client, message): + # private spot update + # { + # "orders": {closed: [], fills: [], open: []}, + # "sequence": 40435835, + # "timestamp": "1650443245600839241", + # "type": "snapshot", + # "wallets": [ + # { + # "balanceEv": 0, + # "currency": "BTC", + # "lastUpdateTimeNs": "1650442638722099092", + # "lockedTradingBalanceEv": 0, + # "lockedWithdrawEv": 0, + # "userID": 2647224 + # }, + # { + # "balanceEv": 1154232337, + # "currency": "USDT", + # "lastUpdateTimeNs": "1650442617610017597", + # "lockedTradingBalanceEv": 0, + # "lockedWithdrawEv": 0, + # "userID": 2647224 + # } + # ] + # } + # private swap update + # { + # "sequence": 83839628, + # "timestamp": "1650382581827447829", + # "type": "snapshot", + # "accounts": [ + # { + # "accountBalanceEv": 0, + # "accountID": 26472240001, + # "bonusBalanceEv": 0, + # "currency": "BTC", + # "totalUsedBalanceEv": 0, + # "userID": 2647224 + # } + # ], + # "orders": [], + # "positions": [ + # { + # "accountID": 26472240001, + # "assignedPosBalanceEv": 0, + # "avgEntryPriceEp": 0, + # "bankruptCommEv": 0, + # "bankruptPriceEp": 0, + # "buyLeavesQty": 0, + # "buyLeavesValueEv": 0, + # "buyValueToCostEr": 1150750, + # "createdAtNs": 0, + # "crossSharedBalanceEv": 0, + # "cumClosedPnlEv": 0, + # "cumFundingFeeEv": 0, + # "cumTransactFeeEv": 0, + # "curTermRealisedPnlEv": 0, + # "currency": "BTC", + # "dataVer": 2, + # "deleveragePercentileEr": 0, + # "displayLeverageEr": 10000000000, + # "estimatedOrdLossEv": 0, + # "execSeq": 0, + # "freeCostEv": 0, + # "freeQty": 0, + # "initMarginReqEr": 1000000, + # "lastFundingTime": "1640601827712091793", + # "lastTermEndTime": 0, + # "leverageEr": 0, + # "liquidationPriceEp": 0, + # "maintMarginReqEr": 500000, + # "makerFeeRateEr": 0, + # "markPriceEp": 507806777, + # "orderCostEv": 0, + # "posCostEv": 0, + # "positionMarginEv": 0, + # "positionStatus": "Normal", + # "riskLimitEv": 10000000000, + # "sellLeavesQty": 0, + # "sellLeavesValueEv": 0, + # "sellValueToCostEr": 1149250, + # "side": "None", + # "size": 0, + # "symbol": "BTCUSD", + # "takerFeeRateEr": 0, + # "term": 1, + # "transactTimeNs": 0, + # "unrealisedPnlEv": 0, + # "updatedAtNs": 0, + # "usedBalanceEv": 0, + # "userID": 2647224, + # "valueEv": 0 + # } + # ] + # } + id = self.safe_string(message, 'id') + if id in client.subscriptions: + method = client.subscriptions[id] + del client.subscriptions[id] + if method is not True: + method(client, message) + return + methodName = self.safe_string(message, 'method', '') + if ('market24h' in message) or ('spot_market24h' in message) or (methodName.find('perp_market24h_pack_p') >= 0): + self.handle_ticker(client, message) + return + elif ('trades' in message) or ('trades_p' in message): + self.handle_trades(client, message) + return + elif ('kline' in message) or ('kline_p' in message): + self.handle_ohlcv(client, message) + return + elif ('book' in message) or ('orderbook_p' in message): + self.handle_order_book(client, message) + return + if ('orders' in message) or ('orders_p' in message): + orders = self.safe_value_2(message, 'orders', 'orders_p', {}) + self.handle_orders(client, orders) + if ('accounts' in message) or ('accounts_p' in message) or ('wallets' in message): + type = 'swap' if ('accounts' in message) else 'spot' + if 'accounts_p' in message: + type = 'perpetual' + accounts = self.safe_value_n(message, ['accounts', 'accounts_p', 'wallets'], []) + self.handle_balance(type, client, accounts) + + def handle_authenticate(self, client: Client, message): + # + # { + # "error": null, + # "id": 1234, + # "result": { + # "status": "success" + # } + # } + # + result = self.safe_value(message, 'result') + status = self.safe_string(result, 'status') + messageHash = 'authenticated' + if status == 'success': + client.resolve(message, messageHash) + else: + error = AuthenticationError(self.id + ' ' + self.json(message)) + client.reject(error, messageHash) + if messageHash in client.subscriptions: + del client.subscriptions[messageHash] + + async def subscribe_private(self, type, messageHash, params={}): + await self.load_markets() + await self.authenticate() + url = self.urls['api']['ws'] + requestId = self.seconds() + settleIsUSDT = (self.safe_value(params, 'settle', '') == 'USDT') + params = self.omit(params, 'settle') + channel = 'aop.subscribe' + if type == 'spot': + channel = 'wo.subscribe' + if settleIsUSDT: + channel = 'aop_p.subscribe' + request = { + 'id': requestId, + 'method': channel, + 'params': [], + } + request = self.extend(request, params) + return await self.watch(url, messageHash, request, channel) + + async def authenticate(self, params={}): + self.check_required_credentials() + url = self.urls['api']['ws'] + client = self.client(url) + requestId = self.request_id() + messageHash = 'authenticated' + future = self.safe_value(client.subscriptions, messageHash) + if future is None: + expiryDelta = self.safe_integer(self.options, 'expires', 120) + expiration = self.seconds() + expiryDelta + payload = self.apiKey + str(expiration) + signature = self.hmac(self.encode(payload), self.encode(self.secret), hashlib.sha256) + method = 'user.auth' + request: dict = { + 'method': method, + 'params': ['API', self.apiKey, signature, expiration], + 'id': requestId, + } + subscriptionHash = str(requestId) + message = self.extend(request, params) + if not (messageHash in client.subscriptions): + client.subscriptions[subscriptionHash] = self.handle_authenticate + future = await self.watch(url, messageHash, message, messageHash) + client.subscriptions[messageHash] = future + return future diff --git a/ccxt/pro/poloniex.py b/ccxt/pro/poloniex.py new file mode 100644 index 0000000..7d82575 --- /dev/null +++ b/ccxt/pro/poloniex.py @@ -0,0 +1,1249 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolById, ArrayCacheByTimestamp +import hashlib +from ccxt.base.types import Any, Balances, Bool, Int, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade +from ccxt.async_support.base.ws.client import Client +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import BadRequest +from ccxt.base.errors import InvalidOrder +from ccxt.base.precise import Precise + + +class poloniex(ccxt.async_support.poloniex): + + def describe(self) -> Any: + return self.deep_extend(super(poloniex, self).describe(), { + 'has': { + 'ws': True, + 'watchOHLCV': True, + 'watchOrderBook': True, + 'watchTicker': True, + 'watchTickers': True, + 'watchTrades': True, + 'watchTradesForSymbols': True, + 'watchBalance': True, + 'watchStatus': False, + 'watchOrders': True, + 'watchMyTrades': True, + 'createOrderWs': True, + 'editOrderWs': False, + 'fetchOpenOrdersWs': False, + 'fetchOrderWs': False, + 'cancelOrderWs': True, + 'cancelOrdersWs': True, + 'cancelAllOrdersWs': True, + 'fetchTradesWs': False, + 'fetchBalanceWs': False, + }, + 'urls': { + 'api': { + 'ws': { + 'public': 'wss://ws.poloniex.com/ws/public', + 'private': 'wss://ws.poloniex.com/ws/private', + }, + }, + }, + 'options': { + 'createMarketBuyOrderRequiresPrice': True, + 'tradesLimit': 1000, + 'ordersLimit': 1000, + 'OHLCVLimit': 1000, + 'watchOrderBook': { + 'name': 'book_lv2', # can also be 'book' + }, + 'connectionsLimit': 2000, # 2000 public, 2000 private, 4000 total, only for subscribe events, unsubscribe not restricted + 'requestsLimit': 500, # per second, only for subscribe events, unsubscribe not restricted + 'timeframes': { + '1m': 'candles_minute_1', + '5m': 'candles_minute_5', + '10m': 'candles_minute_10', + '15m': 'candles_minute_15', + '30m': 'candles_minute_30', + '1h': 'candles_hour_1', + '2h': 'candles_hour_2', + '4h': 'candles_hour_4', + '6h': 'candles_hour_6', + '12h': 'candles_hour_12', + '1d': 'candles_day_1', + '3d': 'candles_day_3', + '1w': 'candles_week_1', + '1M': 'candles_month_1', + }, + }, + 'streaming': { + 'keepAlive': 15000, + 'ping': self.ping, + }, + }) + + async def authenticate(self, params={}): + """ + @ignore + authenticates the user to access private web socket channels + + https://api-docs.poloniex.com/spot/websocket/authentication + + :returns dict: response from exchange + """ + self.check_required_credentials() + timestamp = self.number_to_string(self.milliseconds()) + url = self.urls['api']['ws']['private'] + messageHash = 'authenticated' + client = self.client(url) + future = self.safe_value(client.subscriptions, messageHash) + if future is None: + accessPath = '/ws' + requestString = 'GET\n' + accessPath + '\nsignTimestamp=' + timestamp + signature = self.hmac(self.encode(requestString), self.encode(self.secret), hashlib.sha256, 'base64') + request: dict = { + 'event': 'subscribe', + 'channel': ['auth'], + 'params': { + 'key': self.apiKey, + 'signTimestamp': timestamp, + 'signature': signature, + 'signatureMethod': 'HmacSHA256', # optional + 'signatureVersion': '2', # optional + }, + } + message = self.extend(request, params) + future = await self.watch(url, messageHash, message, messageHash) + # + # { + # "data": { + # "success": True, + # "ts": 1645597033915 + # }, + # "channel": "auth" + # } + # + # # Failure to return results + # + # { + # "data": { + # "success": False, + # "message": "Authentication failed!", + # "ts": 1646276295075 + # }, + # "channel": "auth" + # } + # + client.subscriptions[messageHash] = future + return future + + async def subscribe(self, name: str, messageHash: str, isPrivate: bool, symbols: Strings = None, params={}): + """ + @ignore + Connects to a websocket channel + :param str name: name of the channel + :param str messageHash: unique identifier for the message + :param boolean isPrivate: True for the authenticated url, False for the public url + :param str[] [symbols]: CCXT market symbols + :param dict [params]: extra parameters specific to the poloniex api + :returns dict: data from the websocket stream + """ + publicOrPrivate = 'private' if isPrivate else 'public' + url = self.urls['api']['ws'][publicOrPrivate] + subscribe: dict = { + 'event': 'subscribe', + 'channel': [ + name, + ], + } + marketIds = [] + if self.is_empty(symbols): + marketIds.append('all') + else: + messageHash = messageHash + '::' + ','.join(symbols) + marketIds = self.market_ids(symbols) + if name != 'balances': + subscribe['symbols'] = marketIds + request = self.extend(subscribe, params) + return await self.watch(url, messageHash, request, messageHash) + + async def trade_request(self, name: str, params={}): + """ + @ignore + Connects to a websocket channel + :param str name: name of the channel + :param dict [params]: extra parameters specific to the poloniex api + :returns dict: data from the websocket stream + """ + url = self.urls['api']['ws']['private'] + messageHash = str(self.nonce()) + subscribe: dict = { + 'id': messageHash, + 'event': name, + 'params': params, + } + return await self.watch(url, messageHash, subscribe, messageHash) + + async def create_order_ws(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}) -> Order: + """ + + https://api-docs.poloniex.com/spot/websocket/trade-request#create-order + + create a trade order + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the poloniex api endpoint + :param str [params.timeInForce]: GTC(default), IOC, FOK + :param str [params.clientOrderId]: Maximum 64-character length.* + :param float [params.cost]: *spot market buy only* the quote quantity that can be used alternative for the amount + + EXCHANGE SPECIFIC PARAMETERS + :param str [params.amount]: quote units for the order + :param boolean [params.allowBorrow]: allow order to be placed by borrowing funds(Default: False) + :param str [params.stpMode]: self-trade prevention, defaults to expire_taker, none: enable self-trade; expire_taker: taker order will be canceled when self-trade happens + :param str [params.slippageTolerance]: used to control the maximum slippage ratio, the value range is greater than 0 and less than 1 + :returns dict: an `order structure ` + """ + await self.load_markets() + await self.authenticate() + market = self.market(symbol) + uppercaseType = type.upper() + uppercaseSide = side.upper() + isPostOnly = self.is_post_only(uppercaseType == 'MARKET', uppercaseType == 'LIMIT_MAKER', params) + if isPostOnly: + uppercaseType = 'LIMIT_MAKER' + request: dict = { + 'symbol': market['id'], + 'side': side.upper(), + 'type': type.upper(), + } + if (uppercaseType == 'MARKET') and (uppercaseSide == 'BUY'): + quoteAmount = None + createMarketBuyOrderRequiresPrice = True + createMarketBuyOrderRequiresPrice, params = self.handle_option_and_params(params, 'createOrder', 'createMarketBuyOrderRequiresPrice', True) + cost = self.safe_number(params, 'cost') + params = self.omit(params, 'cost') + if cost is not None: + quoteAmount = self.cost_to_precision(symbol, cost) + elif createMarketBuyOrderRequiresPrice: + if price is None: + raise InvalidOrder(self.id + ' createOrder() requires the price argument for market buy orders to calculate the total cost to spend(amount * price), alternatively set the createMarketBuyOrderRequiresPrice option or param to False and pass the cost to spend(quote quantity) in the amount argument') + else: + amountString = self.number_to_string(amount) + priceString = self.number_to_string(price) + costRequest = Precise.string_mul(amountString, priceString) + quoteAmount = self.cost_to_precision(symbol, costRequest) + else: + quoteAmount = self.cost_to_precision(symbol, amount) + request['amount'] = quoteAmount + else: + request['quantity'] = self.amount_to_precision(market['symbol'], amount) + if price is not None: + request['price'] = self.price_to_precision(symbol, price) + orders = await self.trade_request('createOrder', self.extend(request, params)) + order = self.safe_dict(orders, 0) + return order + + async def cancel_order_ws(self, id: str, symbol: Str = None, params={}): + """ + + https://api-docs.poloniex.com/spot/websocket/trade-request#cancel-multiple-orders + + cancel multiple orders + :param str id: order id + :param str [symbol]: unified market symbol + :param dict [params]: extra parameters specific to the poloniex api endpoint + :param str [params.clientOrderId]: client order id + :returns dict: an list of `order structures ` + """ + clientOrderId = self.safe_string(params, 'clientOrderId') + if clientOrderId is not None: + clientOrderIds = self.safe_value(params, 'clientOrderId', []) + params['clientOrderIds'] = self.array_concat(clientOrderIds, [clientOrderId]) + orders = await self.cancel_orders_ws([id], symbol, params) + order = self.safe_dict(orders, 0) + return order + + async def cancel_orders_ws(self, ids: List[str], symbol: Str = None, params={}): + """ + + https://api-docs.poloniex.com/spot/websocket/trade-request#cancel-multiple-orders + + cancel multiple orders + :param str[] ids: order ids + :param str symbol: unified market symbol, default is None + :param dict [params]: extra parameters specific to the poloniex api endpoint + :param str[] [params.clientOrderIds]: client order ids + :returns dict: an list of `order structures ` + """ + await self.load_markets() + await self.authenticate() + request: dict = { + 'orderIds': ids, + } + return await self.trade_request('cancelOrders', self.extend(request, params)) + + async def cancel_all_orders_ws(self, symbol: Str = None, params={}) -> List[Order]: + """ + + https://api-docs.poloniex.com/spot/websocket/trade-request#cancel-all-orders + + cancel all open orders of a type. Only applicable to Option in Portfolio Margin mode, and MMP privilege is required. + :param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None + :param dict [params]: extra parameters specific to the poloniex api endpoint + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + await self.authenticate() + return await self.trade_request('cancelAllOrders', params) + + def handle_order_request(self, client: Client, message): + # + # { + # "id": "1234567", + # "data": [{ + # "orderId": 205343650954092544, + # "clientOrderId": "", + # "message": "", + # "code": 200 + # }] + # } + # + messageHash = self.safe_string(message, 'id') + data = self.safe_value(message, 'data', []) + orders = [] + for i in range(0, len(data)): + order = data[i] + parsedOrder = self.parse_ws_order(order) + orders.append(parsedOrder) + client.resolve(orders, messageHash) + + async def watch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + watches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://api-docs.poloniex.com/spot/websocket/market-data#candlesticks + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + timeframes = self.safe_value(self.options, 'timeframes', {}) + channel = self.safe_string(timeframes, timeframe, timeframe) + if channel is None: + raise BadRequest(self.id + ' watchOHLCV cannot take a timeframe of ' + timeframe) + ohlcv = await self.subscribe(channel, channel, False, [symbol], params) + if self.newUpdates: + limit = ohlcv.getLimit(symbol, limit) + return self.filter_by_since_limit(ohlcv, since, limit, 0, True) + + async def watch_ticker(self, symbol: str, params={}) -> Ticker: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://api-docs.poloniex.com/spot/websocket/market-data#ticker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbol = self.symbol(symbol) + tickers = await self.watch_tickers([symbol], params) + return self.safe_value(tickers, symbol) + + async def watch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://api-docs.poloniex.com/spot/websocket/market-data#ticker + + :param str[] symbols: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + name = 'ticker' + symbols = self.market_symbols(symbols) + newTickers = await self.subscribe(name, name, False, symbols, params) + if self.newUpdates: + return newTickers + return self.filter_by_array(self.tickers, 'symbol', symbols) + + async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://api-docs.poloniex.com/spot/websocket/market-data#trades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + return await self.watch_trades_for_symbols([symbol], since, limit, params) + + async def watch_trades_for_symbols(self, symbols: List[str], since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a list of symbols + + https://api-docs.poloniex.com/spot/websocket/market-data#trades + + :param str[] symbols: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, False, True, True) + name = 'trades' + url = self.urls['api']['ws']['public'] + marketIds = self.market_ids(symbols) + subscribe: dict = { + 'event': 'subscribe', + 'channel': [ + name, + ], + 'symbols': marketIds, + } + request = self.extend(subscribe, params) + messageHashes = [] + if symbols is not None: + for i in range(0, len(symbols)): + messageHashes.append(name + '::' + symbols[i]) + trades = await self.watch_multiple(url, messageHashes, request, messageHashes) + if self.newUpdates: + first = self.safe_value(trades, 0) + tradeSymbol = self.safe_string(first, 'symbol') + limit = trades.getLimit(tradeSymbol, limit) + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://api-docs.poloniex.com/spot/websocket/market-data#book-level-2 + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: not used by poloniex watchOrderBook + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + watchOrderBookOptions = self.safe_value(self.options, 'watchOrderBook') + name = self.safe_string(watchOrderBookOptions, 'name', 'book_lv2') + name, params = self.handle_option_and_params(params, 'method', 'name', name) + orderbook = await self.subscribe(name, name, False, [symbol], params) + return orderbook.limit() + + async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + watches information on multiple orders made by the user + + https://api-docs.poloniex.com/spot/websocket/order + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: not used by poloniex watchOrders + :param int [limit]: not used by poloniex watchOrders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + name = 'orders' + await self.authenticate() + if symbol is not None: + symbol = self.symbol(symbol) + symbols = None if (symbol is None) else [symbol] + orders = await self.subscribe(name, name, True, symbols, params) + if self.newUpdates: + limit = orders.getLimit(symbol, limit) + return self.filter_by_since_limit(orders, since, limit, 'timestamp', True) + + async def watch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + watches information on multiple trades made by the user using orders stream + + https://api-docs.poloniex.com/spot/websocket/order + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: not used by poloniex watchMyTrades + :param int [limit]: not used by poloniex watchMyTrades + :param dict [params]: extra parameters specific to the poloniex strean + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + name = 'orders' + messageHash = 'myTrades' + await self.authenticate() + if symbol is not None: + symbol = self.symbol(symbol) + symbols = None if (symbol is None) else [symbol] + trades = await self.subscribe(name, messageHash, True, symbols, params) + if self.newUpdates: + limit = trades.getLimit(symbol, limit) + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + async def watch_balance(self, params={}) -> Balances: + """ + watch balance and get the amount of funds available for trading or funds locked in orders + + https://api-docs.poloniex.com/spot/websocket/balance + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + name = 'balances' + await self.authenticate() + return await self.subscribe(name, name, True, None, params) + + def parse_ws_ohlcv(self, ohlcv, market=None) -> list: + # + # { + # "symbol": "BTC_USDT", + # "amount": "840.7240416", + # "high": "24832.35", + # "quantity": "0.033856", + # "tradeCount": 1, + # "low": "24832.35", + # "closeTime": 1676942519999, + # "startTime": 1676942460000, + # "close": "24832.35", + # "open": "24832.35", + # "ts": 1676942492072 + # } + # + return [ + self.safe_integer(ohlcv, 'startTime'), + self.safe_number(ohlcv, 'open'), + self.safe_number(ohlcv, 'high'), + self.safe_number(ohlcv, 'low'), + self.safe_number(ohlcv, 'close'), + self.safe_number(ohlcv, 'quantity'), + ] + + def handle_ohlcv(self, client: Client, message): + # + # { + # "channel": "candles_minute_1", + # "data": [ + # { + # "symbol": "BTC_USDT", + # "amount": "840.7240416", + # "high": "24832.35", + # "quantity": "0.033856", + # "tradeCount": 1, + # "low": "24832.35", + # "closeTime": 1676942519999, + # "startTime": 1676942460000, + # "close": "24832.35", + # "open": "24832.35", + # "ts": 1676942492072 + # } + # ] + # } + # + data = self.safe_value(message, 'data') + data = self.safe_value(data, 0) + channel = self.safe_string(message, 'channel') + marketId = self.safe_string(data, 'symbol') + symbol = self.safe_symbol(marketId) + market = self.safe_market(symbol) + timeframes = self.safe_value(self.options, 'timeframes', {}) + timeframe = self.find_timeframe(channel, timeframes) + messageHash = channel + '::' + symbol + parsed = self.parse_ws_ohlcv(data, market) + self.ohlcvs[symbol] = self.safe_value(self.ohlcvs, symbol, {}) + stored = self.safe_value(self.ohlcvs[symbol], timeframe) + if symbol is not None: + if stored is None: + limit = self.safe_integer(self.options, 'OHLCVLimit', 1000) + stored = ArrayCacheByTimestamp(limit) + self.ohlcvs[symbol][timeframe] = stored + stored.append(parsed) + client.resolve(stored, messageHash) + return message + + def handle_trade(self, client: Client, message): + # + # { + # "channel": "trades", + # "data": [ + # { + # "symbol": "BTC_USDT", + # "amount": "13.41634893", + # "quantity": "0.000537", + # "takerSide": "buy", + # "createTime": 1676950548834, + # "price": "24983.89", + # "id": "62486976", + # "ts": 1676950548839 + # } + # ] + # } + # + data = self.safe_value(message, 'data', []) + for i in range(0, len(data)): + item = data[i] + marketId = self.safe_string(item, 'symbol') + if marketId is not None: + trade = self.parse_ws_trade(item) + symbol = trade['symbol'] + type = 'trades' + messageHash = type + '::' + symbol + tradesArray = self.safe_value(self.trades, symbol) + if tradesArray is None: + tradesLimit = self.safe_integer(self.options, 'tradesLimit', 1000) + tradesArray = ArrayCache(tradesLimit) + self.trades[symbol] = tradesArray + tradesArray.append(trade) + client.resolve(tradesArray, messageHash) + return message + + def parse_ws_trade(self, trade, market=None): + # + # handleTrade + # + # { + # "symbol": "BTC_USDT", + # "amount": "13.41634893", + # "quantity": "0.000537", + # "takerSide": "buy", + # "createTime": 1676950548834, + # "price": "24983.89", + # "id": "62486976", + # "ts": 1676950548839 + # } + # + # private trade + # { + # "orderId":"186250258089635840", + # "tradeId":"62036513", + # "clientOrderId":"", + # "accountType":"SPOT", + # "eventType":"trade", + # "symbol":"ADA_USDT", + # "side":"SELL", + # "type":"MARKET", + # "price":"0", + # "quantity":"3", + # "state":"FILLED", + # "createTime":1685371921891, + # "tradeTime":1685371921908, + # "tradePrice":"0.37694", + # "tradeQty":"3", + # "feeCurrency":"USDT", + # "tradeFee":"0.00226164", + # "tradeAmount":"1.13082", + # "filledQuantity":"3", + # "filledAmount":"1.13082", + # "ts":1685371921945, + # "source":"WEB", + # "orderAmount":"0", + # "matchRole":"TAKER" + # } + # + marketId = self.safe_string(trade, 'symbol') + market = self.safe_market(marketId, market) + timestamp = self.safe_integer(trade, 'createTime') + takerMaker = self.safe_string_lower_2(trade, 'matchRole', 'taker') + return self.safe_trade({ + 'info': trade, + 'id': self.safe_string_2(trade, 'id', 'tradeId'), + 'symbol': self.safe_string(market, 'symbol'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'order': self.safe_string(trade, 'orderId'), + 'type': self.safe_string_lower(trade, 'type'), + 'side': self.safe_string_lower_2(trade, 'takerSide', 'side'), + 'takerOrMaker': takerMaker, + 'price': self.omit_zero(self.safe_string_2(trade, 'tradePrice', 'price')), + 'amount': self.omit_zero(self.safe_string_2(trade, 'filledQuantity', 'quantity')), + 'cost': self.safe_string_2(trade, 'amount', 'filledAmount'), + 'fee': { + 'rate': None, + 'cost': self.safe_string(trade, 'tradeFee'), + 'currency': self.safe_string(trade, 'feeCurrency'), + }, + }, market) + + def parse_status(self, status): + statuses: dict = { + 'NEW': 'open', + 'PARTIALLY_FILLED': 'open', + 'FILLED': 'closed', + 'PENDING_CANCEL': 'open', + 'PARTIALLY_CANCELED': 'open', + 'CANCELED': 'canceled', + # FAILED + } + return self.safe_string(statuses, status, status) + + def parse_ws_order_trade(self, trade, market=None): + # + # { + # "symbol": "BTC_USDT", + # "type": "LIMIT", + # "quantity": "1", + # "orderId": "32471407854219264", + # "tradeFee": "0", + # "clientOrderId": "", + # "accountType": "SPOT", + # "feeCurrency": "", + # "eventType": "place", + # "source": "API", + # "side": "BUY", + # "filledQuantity": "0", + # "filledAmount": "0", + # "matchRole": "MAKER", + # "state": "NEW", + # "tradeTime": 0, + # "tradeAmount": "0", + # "orderAmount": "0", + # "createTime": 1648708186922, + # "price": "47112.1", + # "tradeQty": "0", + # "tradePrice": "0", + # "tradeId": "0", + # "ts": 1648708187469 + # } + # + timestamp = self.safe_integer(trade, 'tradeTime') + marketId = self.safe_string(trade, 'symbol') + return self.safe_trade({ + 'info': trade, + 'id': self.safe_string(trade, 'tradeId'), + 'symbol': self.safe_symbol(marketId, market), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'order': self.safe_string(trade, 'orderId'), + 'type': self.safe_string_lower(trade, 'type'), + 'side': self.safe_string(trade, 'side'), + 'takerOrMaker': self.safe_string_lower(trade, 'matchRole'), + 'price': self.safe_string(trade, 'price'), + 'amount': self.safe_string(trade, 'tradeAmount'), + 'cost': None, + 'fee': { + 'rate': None, + 'cost': self.safe_string(trade, 'tradeFee'), + 'currency': self.safe_string(trade, 'feeCurrency'), + }, + }, market) + + def handle_order(self, client: Client, message): + # + # Order is created + # + # { + # "channel": "orders", + # "data": [ + # { + # "symbol": "BTC_USDT", + # "type": "LIMIT", + # "quantity": "1", + # "orderId": "32471407854219264", + # "tradeFee": "0", + # "clientOrderId": "", + # "accountType": "SPOT", + # "feeCurrency": "", + # "eventType": "place", + # "source": "API", + # "side": "BUY", + # "filledQuantity": "0", + # "filledAmount": "0", + # "matchRole": "MAKER", + # "state": "NEW", + # "tradeTime": 0, + # "tradeAmount": "0", + # "orderAmount": "0", + # "createTime": 1648708186922, + # "price": "47112.1", + # "tradeQty": "0", + # "tradePrice": "0", + # "tradeId": "0", + # "ts": 1648708187469 + # } + # ] + # } + # + data = self.safe_value(message, 'data', []) + orders = self.orders + if orders is None: + limit = self.safe_integer(self.options, 'ordersLimit') + orders = ArrayCacheBySymbolById(limit) + self.orders = orders + marketIds = [] + for i in range(0, len(data)): + order = self.safe_value(data, i) + marketId = self.safe_string(order, 'symbol') + eventType = self.safe_string(order, 'eventType') + if marketId is not None: + symbol = self.safe_symbol(marketId) + orderId = self.safe_string(order, 'orderId') + clientOrderId = self.safe_string(order, 'clientOrderId') + if eventType == 'place' or eventType == 'canceled': + parsed = self.parse_ws_order(order) + orders.append(parsed) + else: + previousOrders = self.safe_value(orders.hashmap, symbol, {}) + previousOrder = self.safe_value_2(previousOrders, orderId, clientOrderId) + trade = self.parse_ws_trade(order) + self.handle_my_trades(client, trade) + if previousOrder['trades'] is None: + previousOrder['trades'] = [] + previousOrder['trades'].append(trade) + previousOrder['lastTradeTimestamp'] = trade['timestamp'] + totalCost = '0' + totalAmount = '0' + previousOrderTrades = previousOrder['trades'] + for j in range(0, len(previousOrderTrades)): + previousOrderTrade = previousOrderTrades[j] + cost = self.number_to_string(previousOrderTrade['cost']) + amount = self.number_to_string(previousOrderTrade['amount']) + totalCost = Precise.string_add(totalCost, cost) + totalAmount = Precise.string_add(totalAmount, amount) + if Precise.string_gt(totalAmount, '0'): + previousOrder['average'] = self.parse_number(Precise.string_div(totalCost, totalAmount)) + previousOrder['cost'] = self.parse_number(totalCost) + if previousOrder['filled'] is not None: + tradeAmount = self.number_to_string(trade['amount']) + previousOrderFilled = self.number_to_string(previousOrder['filled']) + previousOrderFilled = Precise.string_add(previousOrderFilled, tradeAmount) + previousOrder['filled'] = previousOrderFilled + if previousOrder['amount'] is not None: + previousOrderAmount = self.number_to_string(previousOrder['amount']) + previousOrder['remaining'] = self.parse_number(Precise.string_sub(previousOrderAmount, previousOrderFilled)) + if previousOrder['fee'] is None: + previousOrder['fee'] = { + 'rate': None, + 'cost': 0, + 'currency': trade['fee']['currency'], + } + if (previousOrder['fee']['cost'] is not None) and (trade['fee']['cost'] is not None): + stringOrderCost = self.number_to_string(previousOrder['fee']['cost']) + stringTradeCost = self.number_to_string(trade['fee']['cost']) + previousOrder['fee']['cost'] = Precise.string_add(stringOrderCost, stringTradeCost) + rawState = self.safe_string(order, 'state') + state = self.parse_status(rawState) + previousOrder['status'] = state + # update the newUpdates count + orders.append(previousOrder) + marketIds.append(marketId) + for i in range(0, len(marketIds)): + marketId = marketIds[i] + market = self.market(marketId) + symbol = market['symbol'] + messageHash = 'orders::' + symbol + client.resolve(orders, messageHash) + client.resolve(orders, 'orders') + return message + + def parse_ws_order(self, order, market=None): + # + # { + # "symbol": "BTC_USDT", + # "type": "LIMIT", + # "quantity": "1", + # "orderId": "32471407854219264", + # "tradeFee": "0", + # "clientOrderId": "", + # "accountType": "SPOT", + # "feeCurrency": "", + # "eventType": "place", + # "source": "API", + # "side": "BUY", + # "filledQuantity": "0", + # "filledAmount": "0", + # "matchRole": "MAKER", + # "state": "NEW", + # "tradeTime": 0, + # "tradeAmount": "0", + # "orderAmount": "0", + # "createTime": 1648708186922, + # "price": "47112.1", + # "tradeQty": "0", + # "tradePrice": "0", + # "tradeId": "0", + # "ts": 1648708187469 + # } + # + id = self.safe_string(order, 'orderId') + clientOrderId = self.safe_string(order, 'clientOrderId') + marketId = self.safe_string(order, 'symbol') + timestamp = self.safe_string(order, 'ts') + filledAmount = self.safe_string(order, 'filledAmount') + status = self.safe_string(order, 'state') + trades = None + if not Precise.string_eq(filledAmount, '0'): + trades = [] + trade = self.parse_ws_order_trade(order) + trades.append(trade) + return self.safe_order({ + 'info': order, + 'symbol': self.safe_symbol(marketId, market), + 'id': id, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'type': self.safe_string(order, 'type'), + 'timeInForce': None, + 'postOnly': None, + 'side': self.safe_string(order, 'side'), + 'price': self.safe_string(order, 'price'), + 'stopPrice': None, + 'triggerPrice': None, + 'amount': self.safe_string(order, 'quantity'), + 'cost': None, + 'average': None, + 'filled': filledAmount, + 'remaining': self.safe_string(order, 'remaining_size'), + 'status': self.parse_status(status), + 'fee': { + 'rate': None, + 'cost': self.safe_string(order, 'tradeFee'), + 'currency': self.safe_string(order, 'feeCurrency'), + }, + 'trades': trades, + }) + + def handle_ticker(self, client: Client, message): + # + # { + # "channel": "ticker", + # "data": [ + # { + # "symbol": "BTC_USDT", + # "startTime": 1677280800000, + # "open": "23154.32", + # "high": "23212.21", + # "low": "22761.01", + # "close": "23148.86", + # "quantity": "105.179566", + # "amount": "2423161.17436702", + # "tradeCount": 17582, + # "dailyChange": "-0.0002", + # "markPrice": "23151.09", + # "closeTime": 1677367197924, + # "ts": 1677367251090 + # } + # ] + # } + # + data = self.safe_value(message, 'data', []) + newTickers: dict = {} + for i in range(0, len(data)): + item = data[i] + marketId = self.safe_string(item, 'symbol') + if marketId is not None: + ticker = self.parse_ticker(item) + symbol = ticker['symbol'] + self.tickers[symbol] = ticker + newTickers[symbol] = ticker + messageHashes = self.find_message_hashes(client, 'ticker::') + for i in range(0, len(messageHashes)): + messageHash = messageHashes[i] + parts = messageHash.split('::') + symbolsString = parts[1] + symbols = symbolsString.split(',') + tickers = self.filter_by_array(newTickers, 'symbol', symbols) + if not self.is_empty(tickers): + client.resolve(tickers, messageHash) + client.resolve(newTickers, 'ticker') + return message + + def handle_order_book(self, client: Client, message): + # + # snapshot + # + # { + # "channel": "book_lv2", + # "data": [ + # { + # "symbol": "BTC_USDT", + # "createTime": 1677368876253, + # "asks": [ + # ["5.65", "0.02"], + # ... + # ], + # "bids": [ + # ["6.16", "0.6"], + # ... + # ], + # "lastId": 164148724, + # "id": 164148725, + # "ts": 1677368876316 + # } + # ], + # "action": "snapshot" + # } + # + # update + # + # { + # "channel": "book_lv2", + # "data": [ + # { + # "symbol": "BTC_USDT", + # "createTime": 1677368876882, + # "asks": [ + # ["6.35", "3"] + # ], + # "bids": [ + # ["5.65", "0.02"] + # ], + # "lastId": 164148725, + # "id": 164148726, + # "ts": 1677368876890 + # } + # ], + # "action": "update" + # } + # + data = self.safe_value(message, 'data', []) + type = self.safe_string(message, 'action') + snapshot = type == 'snapshot' + update = type == 'update' + for i in range(0, len(data)): + item = data[i] + marketId = self.safe_string(item, 'symbol') + market = self.safe_market(marketId) + symbol = market['symbol'] + name = 'book_lv2' + messageHash = name + '::' + symbol + subscription = self.safe_value(client.subscriptions, messageHash, {}) + limit = self.safe_integer(subscription, 'limit') + timestamp = self.safe_integer(item, 'ts') + asks = self.safe_value(item, 'asks') + bids = self.safe_value(item, 'bids') + if snapshot or update: + if snapshot: + self.orderbooks[symbol] = self.order_book({}, limit) + orderbook = self.orderbooks[symbol] + if bids is not None: + for j in range(0, len(bids)): + bid = self.safe_value(bids, j) + price = self.safe_number(bid, 0) + amount = self.safe_number(bid, 1) + bidsSide = orderbook['bids'] + bidsSide.store(price, amount) + if asks is not None: + for j in range(0, len(asks)): + ask = self.safe_value(asks, j) + price = self.safe_number(ask, 0) + amount = self.safe_number(ask, 1) + asksSide = orderbook['asks'] + asksSide.store(price, amount) + orderbook['symbol'] = symbol + orderbook['timestamp'] = timestamp + orderbook['datetime'] = self.iso8601(timestamp) + client.resolve(orderbook, messageHash) + + def handle_balance(self, client: Client, message): + # + # { + # "channel": "balances", + # "data": [ + # { + # "changeTime": 1657312008411, + # "accountId": "1234", + # "accountType": "SPOT", + # "eventType": "place_order", + # "available": "9999999983.668", + # "currency": "BTC", + # "id": 60018450912695040, + # "userId": 12345, + # "hold": "16.332", + # "ts": 1657312008443 + # } + # ] + # } + # + data = self.safe_value(message, 'data', []) + messageHash = 'balances' + self.balance = self.parse_ws_balance(data) + client.resolve(self.balance, messageHash) + + def parse_ws_balance(self, response): + # + # [ + # { + # "changeTime": 1657312008411, + # "accountId": "1234", + # "accountType": "SPOT", + # "eventType": "place_order", + # "available": "9999999983.668", + # "currency": "BTC", + # "id": 60018450912695040, + # "userId": 12345, + # "hold": "16.332", + # "ts": 1657312008443 + # } + # ] + # + firstBalance = self.safe_value(response, 0, {}) + timestamp = self.safe_integer(firstBalance, 'ts') + result: dict = { + 'info': response, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + for i in range(0, len(response)): + balance = self.safe_value(response, i) + currencyId = self.safe_string(balance, 'currency') + code = self.safe_currency_code(currencyId) + newAccount = self.account() + newAccount['free'] = self.safe_string(balance, 'available') + newAccount['used'] = self.safe_string(balance, 'hold') + result[code] = newAccount + return self.safe_balance(result) + + def handle_my_trades(self, client: Client, parsedTrade): + # emulated using the orders' stream + messageHash = 'myTrades' + symbol = parsedTrade['symbol'] + if self.myTrades is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + self.myTrades = ArrayCacheBySymbolById(limit) + trades = self.myTrades + trades.append(parsedTrade) + client.resolve(trades, messageHash) + symbolMessageHash = messageHash + ':' + symbol + client.resolve(trades, symbolMessageHash) + + def handle_pong(self, client: Client): + client.lastPong = self.milliseconds() + + def handle_message(self, client: Client, message): + if self.handle_error_message(client, message): + return + type = self.safe_string(message, 'channel') + event = self.safe_string(message, 'event') + if event == 'pong': + client.lastPong = self.milliseconds() + methods: dict = { + 'candles_minute_1': self.handle_ohlcv, + 'candles_minute_5': self.handle_ohlcv, + 'candles_minute_10': self.handle_ohlcv, + 'candles_minute_15': self.handle_ohlcv, + 'candles_minute_30': self.handle_ohlcv, + 'candles_hour_1': self.handle_ohlcv, + 'candles_hour_2': self.handle_ohlcv, + 'candles_hour_4': self.handle_ohlcv, + 'candles_hour_6': self.handle_ohlcv, + 'candles_hour_12': self.handle_ohlcv, + 'candles_day_1': self.handle_ohlcv, + 'candles_day_3': self.handle_ohlcv, + 'candles_week_1': self.handle_ohlcv, + 'candles_month_1': self.handle_ohlcv, + 'book': self.handle_order_book, + 'book_lv2': self.handle_order_book, + 'ticker': self.handle_ticker, + 'trades': self.handle_trade, + 'orders': self.handle_order, + 'balances': self.handle_balance, + 'createOrder': self.handle_order_request, + 'cancelOrder': self.handle_order_request, + 'cancelAllOrders': self.handle_order_request, + 'auth': self.handle_authenticate, + } + method = self.safe_value(methods, type) + if type == 'auth': + self.handle_authenticate(client, message) + elif type is None: + self.handle_order_request(client, message) + else: + data = self.safe_value(message, 'data', []) + dataLength = len(data) + if dataLength > 0: + method(client, message) + + def handle_error_message(self, client: Client, message) -> Bool: + # + # { + # message: 'Invalid channel value ["ordersss"]', + # event: 'error' + # } + # + # { + # "orderId": 0, + # "clientOrderId": null, + # "message": "Currency trade disabled", + # "code": 21352 + # } + # + # { + # "event": "error", + # "message": "Platform in maintenance mode" + # } + # { + # "id":"1722386782048", + # "data":[ + # { + # "orderId":0, + # "clientOrderId":null, + # "message":"available insufficient", + # "code":21721 + # } + # ] + # } + # + id = self.safe_string(message, 'id') + event = self.safe_string(message, 'event') + data = self.safe_list(message, 'data') + first = self.safe_dict(data, 0) + orderId = self.safe_string(first, 'orderId') + if (event == 'error') or (orderId == '0'): + try: + error = self.safe_string(first, 'message') + code = self.safe_string(first, 'code') + feedback = self.id + ' ' + self.json(message) + self.throw_exactly_matched_exception(self.exceptions['exact'], code, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], error, feedback) + raise ExchangeError(feedback) + except Exception as e: + if isinstance(e, AuthenticationError): + messageHash = 'authenticated' + client.reject(e, messageHash) + if messageHash in client.subscriptions: + del client.subscriptions[messageHash] + else: + client.reject(e, id) + return True + return False + + def handle_authenticate(self, client: Client, message): + # + # { + # "success": True, + # "ret_msg": '', + # "op": "auth", + # "conn_id": "ce3dpomvha7dha97tvp0-2xh" + # } + # + data = self.safe_value(message, 'data') + success = self.safe_value(data, 'success') + messageHash = 'authenticated' + if success: + client.resolve(message, messageHash) + else: + error = AuthenticationError(self.id + ' ' + self.json(message)) + client.reject(error, messageHash) + if messageHash in client.subscriptions: + del client.subscriptions[messageHash] + return message + + def ping(self, client: Client): + return { + 'event': 'ping', + } diff --git a/ccxt/pro/probit.py b/ccxt/pro/probit.py new file mode 100644 index 0000000..2169b49 --- /dev/null +++ b/ccxt/pro/probit.py @@ -0,0 +1,558 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolById +from ccxt.base.types import Any, Balances, Bool, Int, Order, OrderBook, Str, Ticker, Trade +from ccxt.async_support.base.ws.client import Client +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import NotSupported + + +class probit(ccxt.async_support.probit): + + def describe(self) -> Any: + return self.deep_extend(super(probit, self).describe(), { + 'has': { + 'ws': True, + 'watchBalance': True, + 'watchTicker': True, + 'watchTickers': False, + 'watchTrades': True, + 'watchTradesForSymbols': False, + 'watchMyTrades': True, + 'watchOrders': True, + 'watchOrderBook': True, + 'watchOHLCV': False, + }, + 'urls': { + 'api': { + 'ws': 'wss://api.probit.com/api/exchange/v1/ws', + }, + 'test': { + 'ws': 'wss://demo-api.probit.com/api/exchange/v1/ws', + }, + }, + 'options': { + 'watchOrderBook': { + 'filter': 'order_books_l2', + 'interval': 100, # or 500 + }, + }, + 'streaming': { + }, + }) + + async def watch_balance(self, params={}) -> Balances: + """ + watch balance and get the amount of funds available for trading or funds locked in orders + + https://docs-en.probit.com/reference/balance-1 + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.authenticate(params) + messageHash = 'balance' + return await self.subscribe_private(messageHash, 'balance', params) + + def handle_balance(self, client: Client, message): + # + # { + # "channel": "balance", + # "reset": False, + # "data": { + # "USDT": { + # "available": "15", + # "total": "15" + # } + # } + # } + # + messageHash = 'balance' + self.parse_ws_balance(message) + client.resolve(self.balance, messageHash) + + def parse_ws_balance(self, message): + # + # { + # "channel": "balance", + # "reset": False, + # "data": { + # "USDT": { + # "available": "15", + # "total": "15" + # } + # } + # } + # + reset = self.safe_bool(message, 'reset', False) + data = self.safe_value(message, 'data', {}) + currencyIds = list(data.keys()) + if reset: + self.balance = {} + for i in range(0, len(currencyIds)): + currencyId = currencyIds[i] + entry = data[currencyId] + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(entry, 'available') + account['total'] = self.safe_string(entry, 'total') + self.balance[code] = account + self.balance = self.safe_balance(self.balance) + + async def watch_ticker(self, symbol: str, params={}) -> Ticker: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://docs-en.probit.com/reference/marketdata + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.interval]: Unit time to synchronize market information(ms). Available units: 100, 500 + :returns dict: a `ticker structure ` + """ + channel = 'ticker' + return await self.subscribe_public('watchTicker', symbol, 'ticker', channel, params) + + def handle_ticker(self, client: Client, message): + # + # { + # "channel": "marketdata", + # "market_id": "BTC-USDT", + # "status": "ok", + # "lag": 0, + # "ticker": { + # "time": "2022-07-21T14:18:04.000Z", + # "last": "22591.3", + # "low": "22500.1", + # "high": "39790.7", + # "change": "-1224", + # "base_volume": "1002.32005445", + # "quote_volume": "23304489.385351021" + # }, + # "reset": True + # } + # + marketId = self.safe_string(message, 'market_id') + symbol = self.safe_symbol(marketId) + ticker = self.safe_value(message, 'ticker', {}) + market = self.safe_market(marketId) + parsedTicker = self.parse_ticker(ticker, market) + messageHash = 'ticker:' + symbol + self.tickers[symbol] = parsedTicker + client.resolve(parsedTicker, messageHash) + + async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://docs-en.probit.com/reference/trade_history + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.interval]: Unit time to synchronize market information(ms). Available units: 100, 500 + :returns dict[]: a list of `trade structures ` + """ + channel = 'recent_trades' + symbol = self.safe_symbol(symbol) + trades = await self.subscribe_public('watchTrades', symbol, 'trades', channel, params) + if self.newUpdates: + limit = trades.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(trades, symbol, since, limit, True) + + def handle_trades(self, client: Client, message): + # + # { + # "channel": "marketdata", + # "market_id": "BTC-USDT", + # "status": "ok", + # "lag": 0, + # "recent_trades": [ + # { + # "id": "BTC-USDT:8010233", + # "price": "22701.4", + # "quantity": "0.011011", + # "time": "2022-07-21T13:40:40.983Z", + # "side": "buy", + # "tick_direction": "up" + # } + # ... + # ] + # "reset": True + # } + # + marketId = self.safe_string(message, 'market_id') + symbol = self.safe_symbol(marketId) + market = self.safe_market(marketId) + trades = self.safe_value(message, 'recent_trades', []) + if self.safe_bool(message, 'reset', False): + return # see comment in handleMessage + messageHash = 'trades:' + symbol + stored = self.safe_value(self.trades, symbol) + if stored is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + stored = ArrayCache(limit) + self.trades[symbol] = stored + for i in range(0, len(trades)): + trade = trades[i] + parsed = self.parse_trade(trade, market) + stored.append(parsed) + self.trades[symbol] = stored + client.resolve(self.trades[symbol], messageHash) + + async def watch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of trades associated with the user + + https://docs-en.probit.com/reference/trade_history + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + await self.authenticate(params) + messageHash = 'trades' + if symbol is not None: + symbol = self.safe_symbol(symbol) + messageHash = messageHash + ':' + symbol + trades = await self.subscribe_private(messageHash, 'trade_history', params) + if self.newUpdates: + limit = trades.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(trades, symbol, since, limit, True) + + def handle_my_trades(self, client: Client, message): + # + # { + # "channel": "trade_history", + # "reset": False, + # "data": [{ + # "id": "BTC-USDT:8010722", + # "order_id": "4124999207", + # "side": "buy", + # "fee_amount": "0.0134999868096", + # "fee_currency_id": "USDT", + # "status": "settled", + # "price": "23136.7", + # "quantity": "0.00032416", + # "cost": "7.499992672", + # "time": "2022-07-21T17:09:33.056Z", + # "market_id": "BTC-USDT" + # }] + # } + # + rawTrades = self.safe_value(message, 'data', []) + length = len(rawTrades) + if length == 0: + return + if self.safe_bool(message, 'reset', False): + return # see comment in handleMessage + messageHash = 'trades' + stored = self.myTrades + if stored is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + stored = ArrayCacheBySymbolById(limit) + self.myTrades = stored + trades = self.parse_trades(rawTrades) + tradeSymbols: dict = {} + for j in range(0, len(trades)): + trade = trades[j] + # don't include 'executed' state, because it's just blanket state of the trade, emited before actual trade event + if self.safe_string(trade['info'], 'status') == 'executed': + continue + tradeSymbols[trade['symbol']] = True + stored.append(trade) + unique = list(tradeSymbols.keys()) + uniqueLength = len(unique) + if uniqueLength == 0: + return + for i in range(0, len(unique)): + symbol = unique[i] + symbolSpecificMessageHash = messageHash + ':' + symbol + client.resolve(stored, symbolSpecificMessageHash) + client.resolve(stored, messageHash) + + async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + watches information on an order made by the user + + https://docs-en.probit.com/reference/open_order + + :param str symbol: unified symbol of the market the order was made in + :param int [since]: timestamp in ms of the earliest order to watch + :param int [limit]: the maximum amount of orders to watch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.channel]: choose what channel to use. Can open_order or order_history. + :returns dict: An `order structure ` + """ + await self.authenticate(params) + messageHash = 'orders' + if symbol is not None: + symbol = self.safe_symbol(symbol) + messageHash = messageHash + ':' + symbol + orders = await self.subscribe_private(messageHash, 'open_order', params) + if self.newUpdates: + limit = orders.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(orders, symbol, since, limit, True) + + def handle_orders(self, client: Client, message): + # + # { + # "channel": "order_history", + # "reset": True, + # "data": [{ + # "id": "4124999207", + # "user_id": "633dc56a-621b-4680-8a4e-85a823499b6d", + # "market_id": "BTC-USDT", + # "type": "market", + # "side": "buy", + # "limit_price": "0", + # "time_in_force": "ioc", + # "filled_cost": "7.499992672", + # "filled_quantity": "0.00032416", + # "open_quantity": "0", + # "status": "filled", + # "time": "2022-07-21T17:09:33.056Z", + # "client_order_id": '', + # "cost": "7.5" + # }, + # ... + # ] + # } + # + rawOrders = self.safe_value(message, 'data', []) + length = len(rawOrders) + if length == 0: + return + messageHash = 'orders' + reset = self.safe_bool(message, 'reset', False) + stored = self.orders + if stored is None or reset: + limit = self.safe_integer(self.options, 'ordersLimit', 1000) + stored = ArrayCacheBySymbolById(limit) + self.orders = stored + orderSymbols: dict = {} + for i in range(0, len(rawOrders)): + rawOrder = rawOrders[i] + order = self.parse_order(rawOrder) + orderSymbols[order['symbol']] = True + stored.append(order) + unique = list(orderSymbols.keys()) + for i in range(0, len(unique)): + symbol = unique[i] + symbolSpecificMessageHash = messageHash + ':' + symbol + client.resolve(stored, symbolSpecificMessageHash) + client.resolve(stored, messageHash) + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://docs-en.probit.com/reference/marketdata + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + channel = None + channel, params = self.handle_option_and_params(params, 'watchOrderBook', 'filter', 'order_books') + orderbook = await self.subscribe_public('watchOrderBook', symbol, 'orderbook', channel, params) + return orderbook.limit() + + async def subscribe_private(self, messageHash, channel, params): + url = self.urls['api']['ws'] + subscribe: dict = { + 'type': 'subscribe', + 'channel': channel, + } + request = self.extend(subscribe, params) + subscribeHash = messageHash + return await self.watch(url, messageHash, request, subscribeHash) + + async def subscribe_public(self, methodName: str, symbol: str, dataType, filter, params={}): + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + url = self.urls['api']['ws'] + client = self.client(url) + subscribeHash = 'marketdata:' + symbol + messageHash = dataType + ':' + symbol + filters = {} + if subscribeHash in client.subscriptions: + # already subscribed + filters = client.subscriptions[subscribeHash] + if not (filter in filters): + # resubscribe + del client.subscriptions[subscribeHash] + filters[filter] = True + keys = list(filters.keys()) + interval = None + interval, params = self.handle_option_and_params(params, methodName, 'interval', 100) + request: dict = { + 'type': 'subscribe', + 'channel': 'marketdata', + 'market_id': market['id'], + 'filter': keys, + 'interval': interval, + } + request = self.extend(request, params) + return await self.watch(url, messageHash, request, subscribeHash, filters) + + def handle_order_book(self, client: Client, message, orderBook): + # + # { + # "channel": "marketdata", + # "market_id": "BTC-USDT", + # "status": "ok", + # "lag": 0, + # "order_books": [ + # {side: "buy", price: '1420.7', quantity: "0.057"}, + # ... + # ], + # "reset": True + # } + # + marketId = self.safe_string(message, 'market_id') + symbol = self.safe_symbol(marketId) + dataBySide = self.group_by(orderBook, 'side') + messageHash = 'orderbook:' + symbol + # orderbook = self.safe_value(self.orderbooks, symbol) + if not (symbol in self.orderbooks): + self.orderbooks[symbol] = self.order_book({}) + orderbook = self.orderbooks[symbol] + reset = self.safe_bool(message, 'reset', False) + if reset: + snapshot = self.parse_order_book(dataBySide, symbol, None, 'buy', 'sell', 'price', 'quantity') + orderbook.reset(snapshot) + else: + self.handle_delta(orderbook, dataBySide) + client.resolve(orderbook, messageHash) + + def handle_bid_asks(self, bookSide, bidAsks): + for i in range(0, len(bidAsks)): + bidAsk = bidAsks[i] + parsed = self.parse_bid_ask(bidAsk, 'price', 'quantity') + bookSide.storeArray(parsed) + + def handle_delta(self, orderbook, delta): + storedBids = orderbook['bids'] + storedAsks = orderbook['asks'] + asks = self.safe_value(delta, 'sell', []) + bids = self.safe_value(delta, 'buy', []) + self.handle_bid_asks(storedBids, bids) + self.handle_bid_asks(storedAsks, asks) + + def handle_error_message(self, client: Client, message) -> Bool: + # + # { + # "errorCode": "INVALID_ARGUMENT", + # "message": '', + # "details": { + # "interval": "invalid" + # } + # } + # + code = self.safe_string(message, 'errorCode') + errMessage = self.safe_string(message, 'message', '') + details = self.safe_value(message, 'details') + feedback = self.id + ' ' + code + ' ' + errMessage + ' ' + self.json(details) + if 'exact' in self.exceptions: + self.throw_exactly_matched_exception(self.exceptions['exact'], code, feedback) + if 'broad' in self.exceptions: + self.throw_broadly_matched_exception(self.exceptions['broad'], errMessage, feedback) + raise ExchangeError(feedback) + + def handle_authenticate(self, client: Client, message): + # + # {type: "authorization", result: "ok"} + # + result = self.safe_string(message, 'result') + future = client.subscriptions['authenticated'] + if result == 'ok': + messageHash = 'authenticated' + client.resolve(message, messageHash) + else: + future.reject(message) + del client.subscriptions['authenticated'] + + def handle_market_data(self, client: Client, message): + ticker = self.safe_value(message, 'ticker') + if ticker is not None: + self.handle_ticker(client, message) + trades = self.safe_value(message, 'recent_trades', []) + tradesLength = len(trades) + if tradesLength: + self.handle_trades(client, message) + orderBook = self.safe_value_n(message, ['order_books', 'order_books_l1', 'order_books_l2', 'order_books_l3', 'order_books_l4'], []) + orderBookLength = len(orderBook) + if orderBookLength: + self.handle_order_book(client, message, orderBook) + + def handle_message(self, client: Client, message): + # + # { + # "errorCode": "INVALID_ARGUMENT", + # "message": '', + # "details": { + # "interval": "invalid" + # } + # } + # + # Note about 'reset' field + # 'reset': True field - it happens once after initial subscription, which just returns old items by the moment of subscription(like "fetchMyTrades" does) + # + errorCode = self.safe_string(message, 'errorCode') + if errorCode is not None: + self.handle_error_message(client, message) + return + type = self.safe_string(message, 'type') + if type == 'authorization': + self.handle_authenticate(client, message) + return + handlers: dict = { + 'marketdata': self.handle_market_data, + 'balance': self.handle_balance, + 'trade_history': self.handle_my_trades, + 'open_order': self.handle_orders, + 'order_history': self.handle_orders, + } + channel = self.safe_string(message, 'channel') + handler = self.safe_value(handlers, channel) + if handler is not None: + handler(client, message) + return + error = NotSupported(self.id + ' handleMessage: unknown message: ' + self.json(message)) + client.reject(error) + + async def authenticate(self, params={}): + url = self.urls['api']['ws'] + client = self.client(url) + messageHash = 'authenticated' + expires = self.safe_integer(self.options, 'expires', 0) + future = self.safe_value(client.subscriptions, messageHash) + if (future is None) or (self.milliseconds() > expires): + response = await self.sign_in() + # + # { + # "access_token": "0ttDv/2hTTn3bLi8GP1gKaneiEQ6+0hOBenPrxNQt2s=", + # "token_type": "bearer", + # "expires_in": 900 + # } + # + accessToken = self.safe_string(response, 'access_token') + request: dict = { + 'type': 'authorization', + 'token': accessToken, + } + future = await self.watch(url, messageHash, self.extend(request, params), messageHash) + client.subscriptions[messageHash] = future + return future diff --git a/ccxt/pro/test/Exchange/test_un_watch_positions.py b/ccxt/pro/test/Exchange/test_un_watch_positions.py new file mode 100644 index 0000000..8c57620 --- /dev/null +++ b/ccxt/pro/test/Exchange/test_un_watch_positions.py @@ -0,0 +1,70 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +async def create_order_after_delay(exchange): + await exchange.sleep(3000) + await exchange.create_order('BTC/USDT:USDT', 'market', 'buy', 0.001) + + +async def test_un_watch_positions(exchange, skipped_properties, symbol): + method = 'unWatchPositions' + exchange.set_sandbox_mode(True) + # First, we need to subscribe to positions to test the unsubscribe functionality + positions_subscription = None + try: + # First call uses snapshot + positions_subscription = await exchange.watch_positions() + # trigger a position update + exchange.spawn(create_order_after_delay, exchange) + # Second call uses subscription + positions_subscription = await exchange.watch_positions() + except Exception as e: + if not test_shared_methods.is_temporary_failure(e): + raise e + # If we can't subscribe, we can't test unsubscribe, so skip this test + return False + # Verify that we have a subscription + assert isinstance(positions_subscription, list), exchange.id + ' ' + method + ' requires a valid positions subscription to test unsubscribe' + # Assert unWatchPositions for one symbol is not supported + error_response = None + try: + error_response = await exchange.un_watch_positions([symbol]) + except Exception as e: + error_response = e + assert error_response is not None, exchange.id + ' ' + method + ' must throw an error when unwatching a specific symbol, returned ' + exchange.json(error_response) + # Test unwatching all positions (without specific symbols) + response_all = None + try: + response_all = await exchange.un_watch_positions() + except Exception as e: + if not test_shared_methods.is_temporary_failure(e): + raise e + raise e + # Verify the response for unwatching all positions + assert response_all is not None, exchange.id + ' ' + method + ' must return a response when unwatching all positions, returned ' + exchange.json(response_all) + # Test that we can resubscribe after unwatching (to ensure cleanup was proper) + resubscribe_response = None + try: + resubscribe_response = await exchange.watch_positions() + exchange.spawn(create_order_after_delay, exchange) + resubscribe_response = await exchange.watch_positions() + except Exception as e: + if not test_shared_methods.is_temporary_failure(e): + raise e + raise Error(exchange.id + ' ' + method + ' failed to resubscribe after unwatch, indicating potential cleanup issues') + # Verify resubscription works + assert isinstance(resubscribe_response, list), exchange.id + ' ' + method + ' must allow resubscription after unwatch, returned ' + exchange.json(resubscribe_response) + return True diff --git a/ccxt/pro/test/Exchange/test_watch_balance.py b/ccxt/pro/test/Exchange/test_watch_balance.py new file mode 100644 index 0000000..c8abba8 --- /dev/null +++ b/ccxt/pro/test/Exchange/test_watch_balance.py @@ -0,0 +1,36 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_balance # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +async def test_watch_balance(exchange, skipped_properties, code): + method = 'watchBalance' + now = exchange.milliseconds() + ends = now + 15000 + while now < ends: + response = None + success = True + try: + response = await exchange.watch_balance() + except Exception as e: + if not test_shared_methods.is_temporary_failure(e): + raise e + now = exchange.milliseconds() + # continue; + success = False + if success is False: + continue + test_balance(exchange, skipped_properties, method, response) + now = exchange.milliseconds() diff --git a/ccxt/pro/test/Exchange/test_watch_bids_asks.py b/ccxt/pro/test/Exchange/test_watch_bids_asks.py new file mode 100644 index 0000000..55e3ed9 --- /dev/null +++ b/ccxt/pro/test/Exchange/test_watch_bids_asks.py @@ -0,0 +1,62 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +import asyncio +from ccxt.base.errors import ArgumentsRequired # noqa E402 +from ccxt.test.exchange.base import test_ticker # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +async def test_watch_bids_asks(exchange, skipped_properties, symbol): + without_symbol = test_watch_bids_asks_helper(exchange, skipped_properties, None) + with_symbol = test_watch_bids_asks_helper(exchange, skipped_properties, [symbol]) + await asyncio.gather(*[with_symbol, without_symbol]) + + +async def test_watch_bids_asks_helper(exchange, skipped_properties, arg_symbols, arg_params={}): + method = 'watchBidsAsks' + now = exchange.milliseconds() + ends = now + 15000 + while now < ends: + success = True + should_return = False + response = None + try: + response = await exchange.watch_bids_asks(arg_symbols, arg_params) + except Exception as e: + # for some exchanges, multi symbol methods might require symbols array to be present, so + # so, if method throws "arguments-required" exception, we don't fail test, but just skip silently, + # because tests will make a second call of this method with symbols array + if (isinstance(e, ArgumentsRequired)) and (arg_symbols is None or len(arg_symbols) == 0): + # todo: provide random symbols to try + # return false; + should_return = True + elif not test_shared_methods.is_temporary_failure(e): + raise e + now = exchange.milliseconds() + # continue; + success = False + if should_return: + return False + if success: + assert isinstance(response, dict), exchange.id + ' ' + method + ' ' + exchange.json(arg_symbols) + ' must return an object. ' + exchange.json(response) + values = list(response.values()) + checked_symbol = None + if arg_symbols is not None and len(arg_symbols) == 1: + checked_symbol = arg_symbols[0] + test_shared_methods.assert_non_emtpy_array(exchange, skipped_properties, method, values, checked_symbol) + for i in range(0, len(values)): + ticker = values[i] + test_ticker(exchange, skipped_properties, method, ticker, checked_symbol) + now = exchange.milliseconds() + return True diff --git a/ccxt/pro/test/Exchange/test_watch_liquidations.py b/ccxt/pro/test/Exchange/test_watch_liquidations.py new file mode 100644 index 0000000..48253ea --- /dev/null +++ b/ccxt/pro/test/Exchange/test_watch_liquidations.py @@ -0,0 +1,47 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.base.errors import NetworkError # noqa E402 +from ccxt.test.exchange.base import test_liquidation # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +async def test_watch_liquidations(exchange, skipped_properties, symbol): + # log (symbol.green, 'watching trades...') + method = 'watchLiquidations' + # we have to skip some exchanges here due to the frequency of trading + skipped_exchanges = [] + if exchange.in_array(exchange.id, skipped_exchanges): + print(exchange.id, method + '() test skipped') + return False + if not exchange.has[method]: + print(exchange.id, 'does not support', method + '() method') + return False + response = None + now = int(time.time() * 1000) + ends = now + 10000 + while now < ends: + try: + response = await exchange[method](symbol) + now = int(time.time() * 1000) + is_array = isinstance(response, list) + assert is_array, 'response must be an array' + print(exchange.iso8601(now), exchange.id, symbol, method, len(list(response.values())), 'liquidations') + # log.noLocate (asTable (response)) + for i in range(0, len(response)): + test_liquidation(exchange, skipped_properties, method, response[i], symbol) + except Exception as e: + if not (isinstance(e, NetworkError)): + raise e + now = int(time.time() * 1000) + return response diff --git a/ccxt/pro/test/Exchange/test_watch_liquidations_for_symbols.py b/ccxt/pro/test/Exchange/test_watch_liquidations_for_symbols.py new file mode 100644 index 0000000..a4db82c --- /dev/null +++ b/ccxt/pro/test/Exchange/test_watch_liquidations_for_symbols.py @@ -0,0 +1,45 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.base.errors import NetworkError # noqa E402 +from ccxt.test.exchange.base import test_liquidation # noqa E402 + +async def test_watch_liquidations_for_symbols(exchange, skipped_properties, symbol): + method = 'watchLiquidationsForSymbols' + # we have to skip some exchanges here due to the frequency of trading + skipped_exchanges = [] + if exchange.in_array(exchange.id, skipped_exchanges): + print(exchange.id, method + '() test skipped') + return False + if not exchange.has[method]: + print(exchange.id, method + '() is not supported') + return False + response = None + now = int(time.time() * 1000) + ends = now + 10000 + while now < ends: + try: + response = await exchange[method]([symbol]) + now = int(time.time() * 1000) + is_array = isinstance(response, list) + assert is_array, 'response must be an array' + print(exchange.iso8601(now), exchange.id, symbol, method, len(list(response.values())), 'liquidations') + # log.noLocate (asTable (response)) + for i in range(0, len(response)): + test_liquidation(exchange, skipped_properties, method, response[i], symbol) + except Exception as e: + if not (isinstance(e, NetworkError)): + raise e + now = int(time.time() * 1000) + return response diff --git a/ccxt/pro/test/Exchange/test_watch_my_trades.py b/ccxt/pro/test/Exchange/test_watch_my_trades.py new file mode 100644 index 0000000..ca64e67 --- /dev/null +++ b/ccxt/pro/test/Exchange/test_watch_my_trades.py @@ -0,0 +1,39 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_trade # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +async def test_watch_my_trades(exchange, skipped_properties, symbol): + method = 'watchMyTrades' + now = exchange.milliseconds() + ends = now + 15000 + while now < ends: + success = True + response = None + try: + response = await exchange.watch_my_trades(symbol) + except Exception as e: + if not test_shared_methods.is_temporary_failure(e): + raise e + now = exchange.milliseconds() + # continue; + success = False + if success: + test_shared_methods.assert_non_emtpy_array(exchange, skipped_properties, method, response, symbol) + now = exchange.milliseconds() + for i in range(0, len(response)): + test_trade(exchange, skipped_properties, method, response[i], symbol, now) + test_shared_methods.assert_timestamp_order(exchange, method, symbol, response) + return True diff --git a/ccxt/pro/test/Exchange/test_watch_ohlcv.py b/ccxt/pro/test/Exchange/test_watch_ohlcv.py new file mode 100644 index 0000000..da307d1 --- /dev/null +++ b/ccxt/pro/test/Exchange/test_watch_ohlcv.py @@ -0,0 +1,47 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_ohlcv # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +async def test_watch_ohlcv(exchange, skipped_properties, symbol): + method = 'watchOHLCV' + now = exchange.milliseconds() + ends = now + 15000 + timeframe_keys = list(exchange.timeframes.keys()) + assert len(timeframe_keys), exchange.id + ' ' + method + ' - no timeframes found' + # prefer 1m timeframe if available, otherwise return the first one + chosen_timeframe_key = '1m' + if not exchange.in_array(chosen_timeframe_key, timeframe_keys): + chosen_timeframe_key = timeframe_keys[0] + limit = 10 + duration = exchange.parse_timeframe(chosen_timeframe_key) + since = exchange.milliseconds() - duration * limit * 1000 - 1000 + while now < ends: + response = None + success = True + try: + response = await exchange.watch_ohlcv(symbol, chosen_timeframe_key, since, limit) + except Exception as e: + if not test_shared_methods.is_temporary_failure(e): + raise e + now = exchange.milliseconds() + # continue; + success = False + if success: + test_shared_methods.assert_non_emtpy_array(exchange, skipped_properties, method, response, symbol) + now = exchange.milliseconds() + for i in range(0, len(response)): + test_ohlcv(exchange, skipped_properties, method, response[i], symbol, now) + return True diff --git a/ccxt/pro/test/Exchange/test_watch_ohlcv_for_symbols.py b/ccxt/pro/test/Exchange/test_watch_ohlcv_for_symbols.py new file mode 100644 index 0000000..9d3c158 --- /dev/null +++ b/ccxt/pro/test/Exchange/test_watch_ohlcv_for_symbols.py @@ -0,0 +1,54 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_ohlcv # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +async def test_watch_ohlcv_for_symbols(exchange, skipped_properties, symbol): + method = 'watchOHLCVForSymbols' + now = exchange.milliseconds() + ends = now + 15000 + timeframe_keys = list(exchange.timeframes.keys()) + assert len(timeframe_keys), exchange.id + ' ' + method + ' - no timeframes found' + # prefer 1m timeframe if available, otherwise return the first one + chosen_timeframe_key = '1m' + if not exchange.in_array(chosen_timeframe_key, timeframe_keys): + chosen_timeframe_key = timeframe_keys[0] + limit = 10 + duration = exchange.parse_timeframe(chosen_timeframe_key) + since = exchange.milliseconds() - duration * limit * 1000 - 1000 + while now < ends: + response = None + success = True + try: + response = await exchange.watch_ohlcv_for_symbols([[symbol, chosen_timeframe_key]], since, limit) + except Exception as e: + if not test_shared_methods.is_temporary_failure(e): + raise e + now = exchange.milliseconds() + # continue; + success = False + if success: + assertion_message = exchange.id + ' ' + method + ' ' + symbol + ' ' + chosen_timeframe_key + ' | ' + exchange.json(response) + assert isinstance(response, dict), 'Response must be a dictionary. ' + assertion_message + assert symbol in response, 'Response should contain the symbol as key. ' + assertion_message + symbol_obj = response[symbol] + assert isinstance(symbol_obj, dict), 'Response.Symbol should be a dictionary. ' + assertion_message + assert chosen_timeframe_key in symbol_obj, 'Response.symbol should contain the timeframe key. ' + assertion_message + ohlcvs = symbol_obj[chosen_timeframe_key] + assert isinstance(ohlcvs, list), 'Response.symbol.timeframe should be an array. ' + assertion_message + now = exchange.milliseconds() + for i in range(0, len(ohlcvs)): + test_ohlcv(exchange, skipped_properties, method, ohlcvs[i], symbol, now) + return True diff --git a/ccxt/pro/test/Exchange/test_watch_order_book.py b/ccxt/pro/test/Exchange/test_watch_order_book.py new file mode 100644 index 0000000..cd028d6 --- /dev/null +++ b/ccxt/pro/test/Exchange/test_watch_order_book.py @@ -0,0 +1,38 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_order_book # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +async def test_watch_order_book(exchange, skipped_properties, symbol): + method = 'watchOrderBook' + now = exchange.milliseconds() + ends = now + 15000 + while now < ends: + response = None + success = True + try: + response = await exchange.watch_order_book(symbol) + except Exception as e: + if not test_shared_methods.is_temporary_failure(e): + raise e + now = exchange.milliseconds() + # continue; + success = False + if success: + # [ response, skippedProperties ] = fixPhpObjectArray (exchange, response, skippedProperties); + assert isinstance(response, dict), exchange.id + ' ' + method + ' ' + symbol + ' must return an object. ' + exchange.json(response) + now = exchange.milliseconds() + test_order_book(exchange, skipped_properties, method, response, symbol) + return True diff --git a/ccxt/pro/test/Exchange/test_watch_order_book_for_symbols.py b/ccxt/pro/test/Exchange/test_watch_order_book_for_symbols.py new file mode 100644 index 0000000..06c40c0 --- /dev/null +++ b/ccxt/pro/test/Exchange/test_watch_order_book_for_symbols.py @@ -0,0 +1,41 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.base.errors import InvalidNonce # noqa E402 +from ccxt.test.exchange.base import test_order_book # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +async def test_watch_order_book_for_symbols(exchange, skipped_properties, symbols): + method = 'watchOrderBookForSymbols' + now = exchange.milliseconds() + ends = now + 15000 + while now < ends: + response = None + success = True + try: + response = await exchange.watch_order_book_for_symbols(symbols) + except Exception as e: + # temporary fix for InvalidNonce for c# + if not test_shared_methods.is_temporary_failure(e) and not (isinstance(e, InvalidNonce)): + raise e + now = exchange.milliseconds() + # continue; + success = False + if success: + # [ response, skippedProperties ] = fixPhpObjectArray (exchange, response, skippedProperties); + assert isinstance(response, dict), exchange.id + ' ' + method + ' ' + exchange.json(symbols) + ' must return an object. ' + exchange.json(response) + now = exchange.milliseconds() + test_shared_methods.assert_in_array(exchange, skipped_properties, method, response, 'symbol', symbols) + test_order_book(exchange, skipped_properties, method, response, None) + return True diff --git a/ccxt/pro/test/Exchange/test_watch_orders.py b/ccxt/pro/test/Exchange/test_watch_orders.py new file mode 100644 index 0000000..6a35de3 --- /dev/null +++ b/ccxt/pro/test/Exchange/test_watch_orders.py @@ -0,0 +1,39 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_order # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +async def test_watch_orders(exchange, skipped_properties, symbol): + method = 'watchOrders' + now = exchange.milliseconds() + ends = now + 15000 + while now < ends: + response = None + success = True + try: + response = await exchange.watch_orders(symbol) + except Exception as e: + if not test_shared_methods.is_temporary_failure(e): + raise e + now = exchange.milliseconds() + # continue; + success = False + if success: + test_shared_methods.assert_non_emtpy_array(exchange, skipped_properties, method, response, symbol) + now = exchange.milliseconds() + for i in range(0, len(response)): + test_order(exchange, skipped_properties, method, response[i], symbol, now) + test_shared_methods.assert_timestamp_order(exchange, method, symbol, response) + return True diff --git a/ccxt/pro/test/Exchange/test_watch_position.py b/ccxt/pro/test/Exchange/test_watch_position.py new file mode 100644 index 0000000..d8d3c99 --- /dev/null +++ b/ccxt/pro/test/Exchange/test_watch_position.py @@ -0,0 +1,37 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_position # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +async def test_watch_position(exchange, skipped_properties, symbol): + method = 'watchPosition' + now = exchange.milliseconds() + ends = now + 15000 + while now < ends: + response = None + success = True + try: + response = await exchange.watch_position(symbol) + except Exception as e: + if not test_shared_methods.is_temporary_failure(e): + raise e + now = exchange.milliseconds() + # continue; + success = False + if success: + assert isinstance(response, dict), exchange.id + ' ' + method + ' ' + symbol + ' must return an object. ' + exchange.json(response) + now = exchange.milliseconds() + test_position(exchange, skipped_properties, method, response, None, now) + return True diff --git a/ccxt/pro/test/Exchange/test_watch_positions.py b/ccxt/pro/test/Exchange/test_watch_positions.py new file mode 100644 index 0000000..191a668 --- /dev/null +++ b/ccxt/pro/test/Exchange/test_watch_positions.py @@ -0,0 +1,60 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_position # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +async def test_watch_positions(exchange, skipped_properties, symbol): + method = 'watchPositions' + now = exchange.milliseconds() + ends = now + 15000 + while now < ends: + response = None + success = True + try: + response = await exchange.watch_positions([symbol]) + except Exception as e: + if not test_shared_methods.is_temporary_failure(e): + raise e + now = exchange.milliseconds() + # continue; + success = False + if success: + test_shared_methods.assert_non_emtpy_array(exchange, skipped_properties, method, response, symbol) + now = exchange.milliseconds() + for i in range(0, len(response)): + test_position(exchange, skipped_properties, method, response[i], None, now) + test_shared_methods.assert_timestamp_order(exchange, method, symbol, response) + # + # Test with specific symbol + # + positions_for_symbols = None + success2 = True + try: + positions_for_symbols = await exchange.watch_positions([symbol]) + except Exception as e: + if not test_shared_methods.is_temporary_failure(e): + raise e + now = exchange.milliseconds() + # continue; + success2 = False + if success2: + assert isinstance(positions_for_symbols, list), exchange.id + ' ' + method + ' must return an array, returned ' + exchange.json(positions_for_symbols) + # max theoretical 4 positions: two for one-way-mode and two for two-way mode + assert len(positions_for_symbols) <= 4, exchange.id + ' ' + method + ' positions length for particular symbol should be less than 4, returned ' + exchange.json(positions_for_symbols) + now = exchange.milliseconds() + for i in range(0, len(positions_for_symbols)): + test_position(exchange, skipped_properties, method, positions_for_symbols[i], symbol, now) + test_shared_methods.assert_timestamp_order(exchange, method, symbol, positions_for_symbols) + return True diff --git a/ccxt/pro/test/Exchange/test_watch_ticker.py b/ccxt/pro/test/Exchange/test_watch_ticker.py new file mode 100644 index 0000000..91df54f --- /dev/null +++ b/ccxt/pro/test/Exchange/test_watch_ticker.py @@ -0,0 +1,37 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_ticker # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +async def test_watch_ticker(exchange, skipped_properties, symbol): + method = 'watchTicker' + now = exchange.milliseconds() + ends = now + 15000 + while now < ends: + response = None + success = True + try: + response = await exchange.watch_ticker(symbol) + except Exception as e: + if not test_shared_methods.is_temporary_failure(e): + raise e + now = exchange.milliseconds() + # continue; + success = False + if success: + assert isinstance(response, dict), exchange.id + ' ' + method + ' ' + symbol + ' must return an object. ' + exchange.json(response) + now = exchange.milliseconds() + test_ticker(exchange, skipped_properties, method, response, symbol) + return True diff --git a/ccxt/pro/test/Exchange/test_watch_tickers.py b/ccxt/pro/test/Exchange/test_watch_tickers.py new file mode 100644 index 0000000..de771ac --- /dev/null +++ b/ccxt/pro/test/Exchange/test_watch_tickers.py @@ -0,0 +1,64 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +import asyncio +from ccxt.base.errors import ArgumentsRequired # noqa E402 +from ccxt.test.exchange.base import test_ticker # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +async def test_watch_tickers(exchange, skipped_properties, symbol): + without_symbol = test_watch_tickers_helper(exchange, skipped_properties, None) + with_symbol = test_watch_tickers_helper(exchange, skipped_properties, [symbol]) + await asyncio.gather(*[with_symbol, without_symbol]) + + +async def test_watch_tickers_helper(exchange, skipped_properties, arg_symbols, arg_params={}): + method = 'watchTickers' + now = exchange.milliseconds() + ends = now + 15000 + while now < ends: + response = None + success = True + should_return = False + try: + response = await exchange.watch_tickers(arg_symbols, arg_params) + except Exception as e: + # for some exchanges, specifically watchTickers method not subscribe + # to "all tickers" itself, and it requires symbols to be set + # so, in such case, if it's arguments-required exception, we don't + # mark tests as failed, but just skip them + if (isinstance(e, ArgumentsRequired)) and (arg_symbols is None or len(arg_symbols) == 0): + # todo: provide random symbols to try + # return; + # return false; + should_return = True + elif not test_shared_methods.is_temporary_failure(e): + raise e + now = exchange.milliseconds() + # continue; + success = False + if should_return: + return False + if success: + assert isinstance(response, dict), exchange.id + ' ' + method + ' ' + exchange.json(arg_symbols) + ' must return an object. ' + exchange.json(response) + values = list(response.values()) + checked_symbol = None + if arg_symbols is not None and len(arg_symbols) == 1: + checked_symbol = arg_symbols[0] + test_shared_methods.assert_non_emtpy_array(exchange, skipped_properties, method, values, checked_symbol) + for i in range(0, len(values)): + ticker = values[i] + test_ticker(exchange, skipped_properties, method, ticker, checked_symbol) + now = exchange.milliseconds() + return True diff --git a/ccxt/pro/test/Exchange/test_watch_trades.py b/ccxt/pro/test/Exchange/test_watch_trades.py new file mode 100644 index 0000000..4dfaaa2 --- /dev/null +++ b/ccxt/pro/test/Exchange/test_watch_trades.py @@ -0,0 +1,39 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_trade # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +async def test_watch_trades(exchange, skipped_properties, symbol): + method = 'watchTrades' + now = exchange.milliseconds() + ends = now + 15000 + while now < ends: + response = None + success = True + try: + response = await exchange.watch_trades(symbol) + except Exception as e: + if not test_shared_methods.is_temporary_failure(e): + raise e + now = exchange.milliseconds() + # continue; + success = False + if success: + test_shared_methods.assert_non_emtpy_array(exchange, skipped_properties, method, response) + now = exchange.milliseconds() + for i in range(0, len(response)): + test_trade(exchange, skipped_properties, method, response[i], symbol, now) + if not ('timestampSort' in skipped_properties): + test_shared_methods.assert_timestamp_order(exchange, method, symbol, response) diff --git a/ccxt/pro/test/Exchange/test_watch_trades_for_symbols.py b/ccxt/pro/test/Exchange/test_watch_trades_for_symbols.py new file mode 100644 index 0000000..a1c3ca7 --- /dev/null +++ b/ccxt/pro/test/Exchange/test_watch_trades_for_symbols.py @@ -0,0 +1,42 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_trade # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +async def test_watch_trades_for_symbols(exchange, skipped_properties, symbols): + method = 'watchTradesForSymbols' + now = exchange.milliseconds() + ends = now + 15000 + while now < ends: + response = None + success = True + try: + response = await exchange.watch_trades_for_symbols(symbols) + except Exception as e: + if not test_shared_methods.is_temporary_failure(e): + raise e + now = exchange.milliseconds() + if success: + assert isinstance(response, list), exchange.id + ' ' + method + ' ' + exchange.json(symbols) + ' must return an array. ' + exchange.json(response) + now = exchange.milliseconds() + symbol = None + for i in range(0, len(response)): + trade = response[i] + symbol = trade['symbol'] + test_trade(exchange, skipped_properties, method, trade, symbol, now) + test_shared_methods.assert_in_array(exchange, skipped_properties, method, trade, 'symbol', symbols) + if not ('timestamp' in skipped_properties): + test_shared_methods.assert_timestamp_order(exchange, method, symbol, response) + return True diff --git a/ccxt/pro/test/base/.gitkeep b/ccxt/pro/test/base/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/ccxt/pro/test/base/test_abnormal_close.py b/ccxt/pro/test/base/test_abnormal_close.py new file mode 100644 index 0000000..4cf8c34 --- /dev/null +++ b/ccxt/pro/test/base/test_abnormal_close.py @@ -0,0 +1,72 @@ +import asyncio +import os +import sys + + +# ------------------------------------------------------------------------------ + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +import ccxt.pro as ccxt +import logging +import psutil +import socket + + +KILL_AFTER = 20 + + +async def tcp_kill_after(seconds): + await asyncio.sleep(seconds) + logging.debug("tcp_kill") + current_process = psutil.Process(os.getpid()) + connections = current_process.net_connections(kind='tcp') + for conn in connections: + if conn.status == psutil.CONN_ESTABLISHED: + try: + sock = socket.fromfd(conn.fd, socket.AF_INET, socket.SOCK_STREAM) + local_address = conn.laddr.ip + local_port = conn.laddr.port + sock.shutdown(socket.SHUT_RDWR) + sock.close() + logging.info(f"Connection closed: {local_address}:{local_port} -> {conn.raddr.ip}:{conn.raddr.port}") + except Exception as e: + logging.error(f"Error closing connection: {e}") + + +async def test_abnormal_close(): + print('test_abnormal_close') + received_network_error = False + ex = ccxt.binance({ + # 'verbose': True + }) + ex.set_sandbox_mode(True) + asyncio.create_task(tcp_kill_after(15)) + start_time = ex.seconds() + while True: + if ex.seconds() - start_time > KILL_AFTER: + break + try: + logging.info("calling watch_trades") + await ex.watch_trades('BTC/USDT:USDT') + logging.info("watch_trades returned") + except ccxt.NetworkError as e: + logging.info(f"received network error: {e}") + if not received_network_error: + received_network_error = True + else: + break + except Exception as e: + logging.info(f"received unexpected exception: {e}") + assert received_network_error, "Failed to receive network error" + print("test_abnormal_close between watch calls") + await ex.watch_trades('BTC/USDT:USDT') + await tcp_kill_after(1) + await asyncio.sleep(2) + await ex.watch_trades('BTC/USDT:USDT') + await ex.close() + +if __name__ == '__main__': + logging.basicConfig(level=logging.DEBUG) + asyncio.run(test_abnormal_close()) diff --git a/ccxt/pro/test/base/test_cache.py b/ccxt/pro/test/base/test_cache.py new file mode 100644 index 0000000..437899d --- /dev/null +++ b/ccxt/pro/test/base/test_cache.py @@ -0,0 +1,616 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheByTimestamp, ArrayCacheBySymbolById, ArrayCacheBySymbolBySide # noqa: F402 + + + + + +def equals(a, b): + return a == b + +# ---------------------------------------------------------------------------- +def test_ws_cache(): + array_cache = ArrayCache(3) + array_cache.append({ + 'symbol': 'BTC/USDT', + 'data': 1, + }) + array_cache.append({ + 'symbol': 'BTC/USDT', + 'data': 2, + }) + array_cache.append({ + 'symbol': 'BTC/USDT', + 'data': 3, + }) + array_cache.append({ + 'symbol': 'BTC/USDT', + 'data': 4, + }) + assert(equals(array_cache, [{ + 'symbol': 'BTC/USDT', + 'data': 2, +}, { + 'symbol': 'BTC/USDT', + 'data': 3, +}, { + 'symbol': 'BTC/USDT', + 'data': 4, +}])) + array_cache.append({ + 'symbol': 'BTC/USDT', + 'data': 5, + }) + array_cache.append({ + 'symbol': 'BTC/USDT', + 'data': 6, + }) + array_cache.append({ + 'symbol': 'BTC/USDT', + 'data': 7, + }) + array_cache.append({ + 'symbol': 'BTC/USDT', + 'data': 8, + }) + assert(equals(array_cache, [{ + 'symbol': 'BTC/USDT', + 'data': 6, +}, { + 'symbol': 'BTC/USDT', + 'data': 7, +}, { + 'symbol': 'BTC/USDT', + 'data': 8, +}])) + array_cache.clear() + array_cache.append({ + 'symbol': 'BTC/USDT', + 'data': 1, + }) + assert(equals(array_cache, [{ + 'symbol': 'BTC/USDT', + 'data': 1, +}])) + # ---------------------------------------------------------------------------- + arraycache2 = ArrayCache(1) + arraycache2.append({ + 'symbol': 'BTC/USDT', + 'data': 1, + }) + arraycache2.append({ + 'symbol': 'BTC/USDT', + 'data': 2, + }) + assert(equals(arraycache2, [{ + 'symbol': 'BTC/USDT', + 'data': 2, +}])) + # ---------------------------------------------------------------------------- + timestamp_cache = ArrayCacheByTimestamp() + ohlcv1 = [100, 1, 2, 3] + ohlcv2 = [200, 5, 6, 7] + timestamp_cache.append(ohlcv1) + timestamp_cache.append(ohlcv2) + assert equals(timestamp_cache, [ohlcv1, ohlcv2]) + modify2 = [200, 10, 11, 12] + timestamp_cache.append(modify2) + assert equals(timestamp_cache, [ohlcv1, modify2]) + # ---------------------------------------------------------------------------- + cache_symbol_id = ArrayCacheBySymbolById() + object1 = { + 'symbol': 'BTC/USDT', + 'id': 'abcdef', + 'i': 1, + } + object2 = { + 'symbol': 'ETH/USDT', + 'id': 'qwerty', + 'i': 2, + } + object3 = { + 'symbol': 'BTC/USDT', + 'id': 'abcdef', + 'i': 3, + } + cache_symbol_id.append(object1) + cache_symbol_id.append(object2) + cache_symbol_id.append(object3) # should update index 0 + assert equals(cache_symbol_id, [object2, object3]) + # ---------------------------------------------------------------------------- + cache_symbol_id_5 = ArrayCacheBySymbolById(5) + for i in range(1, 11): + cache_symbol_id_5.append({ + 'symbol': 'BTC/USDT', + 'id': str(i), + 'i': i, + }) + assert(equals(cache_symbol_id_5, [{ + 'symbol': 'BTC/USDT', + 'id': '6', + 'i': 6, +}, { + 'symbol': 'BTC/USDT', + 'id': '7', + 'i': 7, +}, { + 'symbol': 'BTC/USDT', + 'id': '8', + 'i': 8, +}, { + 'symbol': 'BTC/USDT', + 'id': '9', + 'i': 9, +}, { + 'symbol': 'BTC/USDT', + 'id': '10', + 'i': 10, +}])) + for i in range(1, 11): + cache_symbol_id_5.append({ + 'symbol': 'BTC/USDT', + 'id': str(i), + 'i': i + 10, + }) + assert(equals(cache_symbol_id_5, [{ + 'symbol': 'BTC/USDT', + 'id': '6', + 'i': 16, +}, { + 'symbol': 'BTC/USDT', + 'id': '7', + 'i': 17, +}, { + 'symbol': 'BTC/USDT', + 'id': '8', + 'i': 18, +}, { + 'symbol': 'BTC/USDT', + 'id': '9', + 'i': 19, +}, { + 'symbol': 'BTC/USDT', + 'id': '10', + 'i': 20, +}])) + middle = { + 'symbol': 'BTC/USDT', + 'id': '8', + 'i': 28, + } + cache_symbol_id_5.append(middle) + assert(equals(cache_symbol_id_5, [{ + 'symbol': 'BTC/USDT', + 'id': '6', + 'i': 16, +}, { + 'symbol': 'BTC/USDT', + 'id': '7', + 'i': 17, +}, { + 'symbol': 'BTC/USDT', + 'id': '9', + 'i': 19, +}, { + 'symbol': 'BTC/USDT', + 'id': '10', + 'i': 20, +}, { + 'symbol': 'BTC/USDT', + 'id': '8', + 'i': 28, +}])) + other_middle = { + 'symbol': 'BTC/USDT', + 'id': '7', + 'i': 27, + } + cache_symbol_id_5.append(other_middle) + assert(equals(cache_symbol_id_5, [{ + 'symbol': 'BTC/USDT', + 'id': '6', + 'i': 16, +}, { + 'symbol': 'BTC/USDT', + 'id': '9', + 'i': 19, +}, { + 'symbol': 'BTC/USDT', + 'id': '10', + 'i': 20, +}, { + 'symbol': 'BTC/USDT', + 'id': '8', + 'i': 28, +}, { + 'symbol': 'BTC/USDT', + 'id': '7', + 'i': 27, +}])) + for i in range(30, 33): + cache_symbol_id_5.append({ + 'symbol': 'BTC/USDT', + 'id': str(i), + 'i': i + 10, + }) + assert(equals(cache_symbol_id_5, [{ + 'symbol': 'BTC/USDT', + 'id': '8', + 'i': 28, +}, { + 'symbol': 'BTC/USDT', + 'id': '7', + 'i': 27, +}, { + 'symbol': 'BTC/USDT', + 'id': '30', + 'i': 40, +}, { + 'symbol': 'BTC/USDT', + 'id': '31', + 'i': 41, +}, { + 'symbol': 'BTC/USDT', + 'id': '32', + 'i': 42, +}])) + first = { + 'symbol': 'BTC/USDT', + 'id': '8', + 'i': 38, + } + cache_symbol_id_5.append(first) + assert(equals(cache_symbol_id_5, [{ + 'symbol': 'BTC/USDT', + 'id': '7', + 'i': 27, +}, { + 'symbol': 'BTC/USDT', + 'id': '30', + 'i': 40, +}, { + 'symbol': 'BTC/USDT', + 'id': '31', + 'i': 41, +}, { + 'symbol': 'BTC/USDT', + 'id': '32', + 'i': 42, +}, { + 'symbol': 'BTC/USDT', + 'id': '8', + 'i': 38, +}])) + another = { + 'symbol': 'BTC/USDT', + 'id': '30', + 'i': 50, + } + cache_symbol_id_5.append(another) + assert(equals(cache_symbol_id_5, [{ + 'symbol': 'BTC/USDT', + 'id': '7', + 'i': 27, +}, { + 'symbol': 'BTC/USDT', + 'id': '31', + 'i': 41, +}, { + 'symbol': 'BTC/USDT', + 'id': '32', + 'i': 42, +}, { + 'symbol': 'BTC/USDT', + 'id': '8', + 'i': 38, +}, { + 'symbol': 'BTC/USDT', + 'id': '30', + 'i': 50, +}])) + # ---------------------------------------------------------------------------- + # test ArrayCacheBySymbolById limit with symbol set + symbol = 'BTC/USDT' + cache_symbol_id_2 = ArrayCacheBySymbolById() + initial_length = 5 + for i in range(0, initial_length): + cache_symbol_id_2.append({ + 'symbol': symbol, + 'id': str(i), + 'i': i, + }) + limited = cache_symbol_id_2.get_limit(symbol, None) + assert initial_length == limited + # ---------------------------------------------------------------------------- + cache_symbol_id_3 = ArrayCacheBySymbolById() + append_items_length = 3 + for i in range(0, append_items_length): + cache_symbol_id_3.append({ + 'symbol': symbol, + 'id': str(i), + 'i': i, + }) + outside_limit = 5 + limited = cache_symbol_id_3.get_limit(symbol, outside_limit) + assert append_items_length == limited + outside_limit = 2 # if limit < newsUpdate that should be returned + limited = cache_symbol_id_3.get_limit(symbol, outside_limit) + assert outside_limit == limited + # ---------------------------------------------------------------------------- + # test ArrayCacheBySymbolById limit with symbol undefined + symbol = 'BTC/USDT' + cache_symbol_id_4 = ArrayCacheBySymbolById() + initial_length = 5 + for i in range(0, initial_length): + cache_symbol_id_4.append({ + 'symbol': symbol, + 'id': str(i), + 'i': i, + }) + limited = cache_symbol_id_4.get_limit(None, None) + assert initial_length == limited + # ---------------------------------------------------------------------------- + cache_symbol_id_6 = ArrayCacheBySymbolById() + append_items_length = 3 + for i in range(0, append_items_length): + cache_symbol_id_6.append({ + 'symbol': symbol, + 'id': str(i), + 'i': i, + }) + outside_limit = 5 + limited = cache_symbol_id_6.get_limit(symbol, outside_limit) + assert append_items_length == limited + outside_limit = 2 # if limit < newsUpdate that should be returned + limited = cache_symbol_id_6.get_limit(symbol, outside_limit) + assert outside_limit == limited + # ---------------------------------------------------------------------------- + # test ArrayCacheBySymbolById, same order should not increase the limit + cache_symbol_id_7 = ArrayCacheBySymbolById() + symbol = 'BTC/USDT' + other_symbol = 'ETH/USDT' + cache_symbol_id_7.append({ + 'symbol': symbol, + 'id': 'singleId', + 'i': 3, + }) + cache_symbol_id_7.append({ + 'symbol': symbol, + 'id': 'singleId', + 'i': 3, + }) + cache_symbol_id_7.append({ + 'symbol': other_symbol, + 'id': 'singleId', + 'i': 3, + }) + outside_limit = 5 + limited = cache_symbol_id_7.get_limit(symbol, outside_limit) + limited2 = cache_symbol_id_7.get_limit(None, outside_limit) + assert limited == 1 + assert limited2 == 2 + # ---------------------------------------------------------------------------- + # test testLimitArrayCacheByTimestamp limit + timestamp_cache_2 = ArrayCacheByTimestamp() + initial_length = 5 + for i in range(0, initial_length): + timestamp_cache_2.append([i * 10, i * 10, i * 10, i * 10]) + limited = timestamp_cache_2.get_limit(None, None) + assert initial_length == limited + append_items_length = 3 + for i in range(0, append_items_length): + timestamp_cache_2.append([i * 4, i * 4, i * 4, i * 4]) + outside_limit = 5 + limited = timestamp_cache_2.get_limit(None, outside_limit) + assert append_items_length == limited + outside_limit = 2 # if limit < newsUpdate that should be returned + limited = timestamp_cache_2.get_limit(None, outside_limit) + assert outside_limit == limited + # ---------------------------------------------------------------------------- + # test ArrayCacheBySymbolById, watch all orders, same symbol and order id gets updated + cache_symbol_id_8 = ArrayCacheBySymbolById() + symbol = 'BTC/USDT' + outside_limit = 5 + cache_symbol_id_8.append({ + 'symbol': symbol, + 'id': 'oneId', + 'i': 3, + }) # create first order + cache_symbol_id_8.get_limit(None, outside_limit) # watch all orders + cache_symbol_id_8.append({ + 'symbol': symbol, + 'id': 'oneId', + 'i': 4, + }) # first order is closed + cache_symbol_id_8.get_limit(None, outside_limit) # watch all orders + cache_symbol_id_8.append({ + 'symbol': symbol, + 'id': 'twoId', + 'i': 5, + }) # create second order + cache_symbol_id_8.get_limit(None, outside_limit) # watch all orders + cache_symbol_id_8.append({ + 'symbol': symbol, + 'id': 'twoId', + 'i': 6, + }) # second order is closed + limited = cache_symbol_id_8.get_limit(None, outside_limit) # watch all orders + assert limited == 1 # one new update + # ---------------------------------------------------------------------------- + # test ArrayCacheBySymbolById, watch all orders, and watchOrders (symbol) work independently + cache_symbol_id_9 = ArrayCacheBySymbolById() + symbol = 'BTC/USDT' + symbol2 = 'ETH/USDT' + outside_limit = 5 + cache_symbol_id_9.append({ + 'symbol': symbol, + 'id': 'one', + 'i': 1, + }) # create first order + cache_symbol_id_9.append({ + 'symbol': symbol2, + 'id': 'two', + 'i': 1, + }) # create second order + assert cache_symbol_id_9.get_limit(None, outside_limit) == 2 # watch all orders + assert cache_symbol_id_9.get_limit(symbol, outside_limit) == 1 # watch by symbol + cache_symbol_id_9.append({ + 'symbol': symbol, + 'id': 'one', + 'i': 2, + }) # update first order + cache_symbol_id_9.append({ + 'symbol': symbol2, + 'id': 'two', + 'i': 2, + }) # update second order + assert cache_symbol_id_9.get_limit(symbol, outside_limit) == 1 # watch by symbol + assert cache_symbol_id_9.get_limit(None, outside_limit) == 2 # watch all orders + cache_symbol_id_9.append({ + 'symbol': symbol2, + 'id': 'two', + 'i': 3, + }) # update second order + cache_symbol_id_9.append({ + 'symbol': symbol2, + 'id': 'three', + 'i': 3, + }) # create third order + assert cache_symbol_id_9.get_limit(None, outside_limit) == 2 # watch all orders + # ---------------------------------------------------------------------------- + # test ArrayCacheBySymbolBySide, watch all positions, same symbol and side id gets updated + cache_symbol_side = ArrayCacheBySymbolBySide() + symbol = 'BTC/USDT' + outside_limit = 5 + cache_symbol_side.append({ + 'symbol': symbol, + 'side': 'short', + 'contracts': 1, + }) # create first position + cache_symbol_side.append({ + 'symbol': symbol, + 'side': 'short', + 'contracts': 0, + }) # first position is closed + assert cache_symbol_side.get_limit(symbol, outside_limit) == 1 # limit position + cache_symbol_side.append({ + 'symbol': symbol, + 'side': 'short', + 'contracts': 1, + }) # create first position + assert cache_symbol_side.get_limit(symbol, outside_limit) == 1 # watch all positions + # ---------------------------------------------------------------------------- + # test ArrayCacheBySymbolBySide, watch all positions, same symbol and side id gets updated + cache_symbol_side_2 = ArrayCacheBySymbolBySide() + symbol = 'BTC/USDT' + outside_limit = 5 + cache_symbol_side_2.append({ + 'symbol': symbol, + 'side': 'short', + 'contracts': 1, + }) # create first position + assert cache_symbol_side_2.get_limit(None, outside_limit) == 1 # watch all positions + cache_symbol_side_2.append({ + 'symbol': symbol, + 'side': 'short', + 'contracts': 0, + }) # first position is closed + assert cache_symbol_side_2.get_limit(None, outside_limit) == 1 # watch all positions + cache_symbol_side_2.append({ + 'symbol': symbol, + 'side': 'long', + 'contracts': 3, + }) # create second position + assert cache_symbol_side_2.get_limit(None, outside_limit) == 1 # watch all positions + cache_symbol_side_2.append({ + 'symbol': symbol, + 'side': 'long', + 'contracts': 2, + }) # second position is reduced + cache_symbol_side_2.append({ + 'symbol': symbol, + 'side': 'long', + 'contracts': 1, + }) # second position is reduced + assert cache_symbol_side_2.get_limit(None, outside_limit) == 1 # watch all orders + # ---------------------------------------------------------------------------- + # test ArrayCacheBySymbolBySide, watchPositions, and watchPosition (symbol) work independently + cache_symbol_side_3 = ArrayCacheBySymbolBySide() + symbol = 'BTC/USDT' + symbol2 = 'ETH/USDT' + cache_symbol_side_3.append({ + 'symbol': symbol, + 'side': 'short', + 'contracts': 1, + }) # create first position + cache_symbol_side_3.append({ + 'symbol': symbol2, + 'side': 'long', + 'contracts': 1, + }) # create second position + assert cache_symbol_side_3.get_limit(None, outside_limit) == 2 # watch all positions + assert cache_symbol_side_3.get_limit(symbol, outside_limit) == 1 # watch by symbol + cache_symbol_side_3.append({ + 'symbol': symbol, + 'side': 'short', + 'contracts': 2, + }) # update first position + cache_symbol_side_3.append({ + 'symbol': symbol2, + 'side': 'long', + 'contracts': 2, + }) # update second position + assert cache_symbol_side_3.get_limit(symbol, outside_limit) == 1 # watch by symbol + assert cache_symbol_side_3.get_limit(None, outside_limit) == 2 # watch all positions + cache_symbol_side_3.append({ + 'symbol': symbol2, + 'side': 'long', + 'contracts': 3, + }) # update second position + assert cache_symbol_side_3.get_limit(None, outside_limit) == 1 # watch all positions + # ---------------------------------------------------------------------------- + # test ArrayCacheBySymbolBySide, watchPositions does not override + cache_symbol_side_4 = ArrayCacheBySymbolBySide() + symbol = 'BTC/USDT' + symbol2 = 'ETH/USDT' + symbol3 = 'XRP/USDT' + cache_symbol_side_4.append({ + 'symbol': symbol, + 'side': 'long', + 'contracts': 1, + }) # create first position + cache_symbol_side_4.append({ + 'symbol': symbol2, + 'side': 'long', + 'contracts': 2, + }) # create second position + cache_symbol_side_4.append({ + 'symbol': symbol3, + 'side': 'long', + 'contracts': 3, + }) # create short position + assert cache_symbol_side_4[0]['symbol'] == symbol + assert cache_symbol_side_4[1]['symbol'] == symbol2 + cache_symbol_side_4.append({ + 'symbol': symbol2, + 'side': 'long', + 'contracts': 4, + }) # update first position + assert cache_symbol_side_4[0]['contracts'] == 1 and cache_symbol_side_4[0]['symbol'] == symbol + assert cache_symbol_side_4[1]['contracts'] == 3 and cache_symbol_side_4[1]['symbol'] == symbol3 + assert cache_symbol_side_4[2]['contracts'] == 4 and cache_symbol_side_4[2]['symbol'] == symbol2 + array_length = len(cache_symbol_side_4) + assert array_length == 3 diff --git a/ccxt/pro/test/base/test_close.py b/ccxt/pro/test/base/test_close.py new file mode 100644 index 0000000..45784c2 --- /dev/null +++ b/ccxt/pro/test/base/test_close.py @@ -0,0 +1,184 @@ +import os +import sys +import asyncio + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +from asyncio import sleep, gather +import ccxt.pro + +async def watch_ticker_loop(exchange): + try: + while True: + ticker = await exchange.watch_ticker('BTC/USDT') + print('ticker received') + except Exception as e: + print(f"{e}") + raise e + + +async def watch_trades_for_symbols_loop(exchange): + try: + while True: + trades = await exchange.watch_trades_for_symbols(['BTC/USDT', 'ETH/USDT', 'LTC/USDT']) + print('trades received') + except Exception as e: + raise e + + +async def close_after(exchange, ms): + await sleep(ms) + await exchange.close() + + +async def test_ws_close(): + exchange = ccxt.pro.binance() + # exchange.verbose = True + # -------------------------------------------- + print('---- Testing exchange.close(): No future awaiting, should close with no errors') + await exchange.watch_ticker('BTC/USD') + print('ticker received') + await exchange.close() + print('PASSED - exchange closed with no errors') + # -------------------------------------------- + print('---- Testing exchange.close(): Open watch multiple, resolve, should close with no errors') + await exchange.watch_trades_for_symbols(['BTC/USD', 'ETH/USD', 'LTC/USD']) + print('ticker received') + await exchange.close() + print('PASSED - exchange closed with no errors') + # -------------------------------------------- + print('---- Testing exchange.close(): Awaiting future should throw ClosedByUser') + try: + await gather(close_after(exchange, 4), watch_ticker_loop(exchange)) + assert False, "Expected Future rejected with ClosedByUser" + except asyncio.CancelledError: + assert True + except Exception as e: + print(f"Unexpected exception: {e}") + assert False + await exchange.close() # Added to ensure close finishes correctly + # -------------------------------------------- + print('---- Testing exchange.close(): Call watch_multiple unhandled futures are canceled') + try: + await gather(close_after(exchange, 4), watch_trades_for_symbols_loop(exchange)) + assert False, "Expected ExchangeClosedByUser error" + except asyncio.CancelledError: + assert True + except Exception as e: + print(f"Unexpected exception: {e}") + assert False + await exchange.close() + # -------------------------------------------- + await test_cancelled_task_no_invalid_state(exchange) + # -------------------------------------------- + await test_unwatch_tickers_after_cancellation(exchange) + +async def test_cancelled_task_no_invalid_state(exchange): + """ + Connects to a real exchange, starts two watch_ticker tasks, cancels one, + and ensures no InvalidStateError or unhandled exception occurs when messages are received after cancellation. + """ + print('---- Testing cancelled task does not raise InvalidStateError') + symbols = ['BTC/USDT', 'ETH/USDT'] + received = {s: 0 for s in symbols} + errors = [] + + async def watch_ticker_task(symbol): + try: + while True: + ticker = await exchange.watch_ticker(symbol) + received[symbol] += 1 + print(f"[TICKER] {symbol} received: {ticker['datetime'] if 'datetime' in ticker else ticker}") + except asyncio.CancelledError: + print(f"[CANCELLED] {symbol} task cancelled") + except Exception as e: + print(f"[ERROR] {symbol}: {e}") + errors.append(e) + + # Start both tasks + task_btc = asyncio.create_task(watch_ticker_task(symbols[0])) + task_eth = asyncio.create_task(watch_ticker_task(symbols[1])) + + # Let both run for a bit + await asyncio.sleep(5) + + # Cancel ETH/USDT task + print("Cancelling ETH/USDT task...") + task_eth.cancel() + try: + await task_eth + except asyncio.CancelledError: + print("ETH/USDT task cancelled and awaited") + + # Let BTC/USDT keep running, and see if any errors occur for ETH/USDT + await asyncio.sleep(5) + + # Check for InvalidStateError or any unhandled exception + for err in errors: + assert not isinstance(err, asyncio.InvalidStateError), f"InvalidStateError occurred: {err}" + print("No InvalidStateError occurred after cancelling ETH/USDT task.") + + # Cleanup + task_btc.cancel() + await exchange.close() + +async def test_unwatch_tickers_after_cancellation(exchange): + """ + Tests the specific case where un_watch_tickers() is called after cancelling tasks, + which was causing InvalidStateError. This reproduces the user's reported issue, but for tickers. + """ + print('---- Testing un_watch_tickers() after task cancellation does not raise InvalidStateError') + + async def watch_tickers_task(symbols): + try: + while True: + tickers = await exchange.watch_tickers(symbols) + print(f"[TICKERS] {symbols} received: {list(tickers.keys()) if hasattr(tickers, 'keys') else tickers}") + except asyncio.CancelledError: + print(f"[CANCELLED] {symbols} tickers task cancelled") + except Exception as e: + assert False, (f"[ERROR] {symbols} tickers: {e}") + + # Start both tasks for different symbols + print("Starting BTC/USDT tickers watch...") + task_btc = asyncio.create_task(watch_tickers_task(['BTC/USDT'])) + print("Starting ETH/USDT tickers watch...") + task_eth = asyncio.create_task(watch_tickers_task(['ETH/USDT'])) + + # Let both run for a bit + print("Letting tasks run for 5 seconds...") + await asyncio.sleep(5) + + # Cancel ETH/USDT task + print("Cancelling ETH/USDT task...") + task_eth.cancel() + try: + await task_eth + except asyncio.CancelledError: + print("ETH/USDT task cancelled and awaited") + + # Let BTC/USDT keep running + print("Letting BTC/USDT task continue for 5 seconds...") + await asyncio.sleep(5) + + # This is the critical part - calling un_watch_tickers() after cancellation + print("Calling un_watch_tickers() after task cancellation...") + try: + await exchange.un_watch_tickers() + print("un_watch_tickers() completed successfully") + except asyncio.InvalidStateError as e: + assert False, f"InvalidStateError occurred: {e}" + except Exception as e: + print(f"Unexpected exception during un_watch_tickers(): {e}") + + # Let it run a bit more to see if any errors occur + print("Letting it run for 5 more seconds...") + await asyncio.sleep(5) + + # Cleanup + task_btc.cancel() + await exchange.close() + print("PASSED - un_watch_tickers() after cancellation test completed successfully") + +asyncio.run(test_ws_close()) diff --git a/ccxt/pro/test/base/test_future.py b/ccxt/pro/test/base/test_future.py new file mode 100644 index 0000000..770523d --- /dev/null +++ b/ccxt/pro/test/base/test_future.py @@ -0,0 +1,194 @@ +import asyncio +import os +import sys + +# Assuming the structure of test_shared_methods based on common unittest methods + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +from ccxt import ExchangeClosedByUser +from ccxt.async_support.base.ws.future import Future + +# Helper functions +async def resolve_later(future, result, delay): + await asyncio.sleep(delay) + future.resolve(result) + +async def cancel_later(future, delay): + await asyncio.sleep(delay) + future.cancel() + +async def reject_later(future, err, delay): + await asyncio.sleep(delay) + future.reject(err) + +async def test_resolve_before(): + print("test_resolve") + future = Future() + expected_result = "test" + future.resolve(expected_result) + assert future.done(), "Future is not marked as done" + assert future.result() == expected_result, f"Expected result '{expected_result}', got '{future.result()}'" + +async def test_reject(): + print("test_reject") + future = Future() + test_exception = Exception("test error") + future.reject(test_exception) + assert future.done(), "Future is not marked as done" + try: + future.result() + assert False, "Expected an exception but none was raised" + except Exception as e: + assert str(e) == "test error", f"Expected 'test error', got '{str(e)}'" + +async def test_race_success_before(): + print("test_race_success") + future1 = Future() + future2 = Future() + race_future = Future.race([future1, future2]) + future1.resolve("first") + result = await race_future + future2.cancel() + assert result == "first", f"Expected 'first', got '{result}'" + +async def test_race_success_after(): + print("test_race_success") + future1 = Future() + future2 = Future() + race_future = Future.race([future1, future2]) + asyncio.create_task(resolve_later(future1, "first", 0.01)) + result = await race_future + future2.cancel() + assert result == "first", f"Expected 'first', got '{result}'" + +async def test_race_return_first_exception(): + print("test_race_return_first_exception") + future1 = Future() + race_future = Future.race([future1]) + future1.reject(Exception("Error in future1")) + try: + await race_future + assert False, "Expected an exception but none was raised" + except Exception as e: + assert str(e) == "Error in future1", f"Expected 'Error in future1', got '{str(e)}'" + +async def test_await_canceled_future(): + print("test_await_canceled_future") + future = Future() + try: + future.cancel() + await future + assert False, "Expected an exception but none was raised" + except asyncio.CancelledError as e: + assert isinstance(e, asyncio.CancelledError), "Expected asyncio.CancelledError" + +async def test_cancel(): + print("test_cancel") + future = Future() + asyncio.create_task(cancel_later(future, 0.1)) + try: + await future + assert False, "Expected an exception but none was raised" + except asyncio.CancelledError as e: + assert isinstance(e, asyncio.CancelledError), "Expected asyncio.CancelledError" + +async def test_race_cancel(): + print("test_race_cancel") + try: + future1 = Future() + future2 = Future() + race_future = Future.race([future1, future2]) + race_future.cancel() + future1.resolve("success") + await race_future + assert False, "Expected a cancelledError" + except asyncio.CancelledError: + assert True + +async def test_race_mixed_outcomes(): + print("test_race_mixed_outcome") + future1 = Future() + future2 = Future() + race_future = Future.race([future1, future2]) + future1.resolve("first") + task = asyncio.create_task(reject_later(future2, Exception("Error in future2"), 0.1)) + result = await race_future + assert result == "first", f"Expected 'first', got '{result}'" + task.cancel() + future2.cancel() + +async def test_race_with_wait_for_timeout(): + print("test_race_with_wait_for_timeout") + future1 = Future() + + task = asyncio.create_task(resolve_later(future1, "completed first", 2)) + + # Attempt to race the futures with a timeout shorter than their resolution time + try: + race_future = Future.race([future1]) + await asyncio.wait_for(race_future, timeout=1) # Timeout is set deliberately short + assert False, "Expected a timeout but race_future completed" + except asyncio.TimeoutError: + # Expected outcome, the race_future should not complete within the timeout + assert True + await task + +async def test_race_with_wait_for_completion(): + print("test_race_with_wait_for_completion") + future1 = Future() + future2 = Future() + + task = asyncio.create_task(resolve_later(future1, "completed first", 0.1)) + + # Race the futures with a timeout longer than necessary + try: + race_future = Future.race([future1, future2]) + result = await asyncio.wait_for(race_future, timeout=1) + assert result == "completed first", f"Unexpected race result: {result}" + except asyncio.TimeoutError: + assert False, "Did not expect a timeout" + await task + +async def test_race_with_precompleted_future(): + print("test_race_with_precompleted_future") + future1 = Future() + future2 = Future() + future1.resolve("immediate success") + # Immediately resolved future before race call + race_future = Future.race([future1, future2]) + result = await race_future + assert result == "immediate success", "Race did not correctly prioritize already completed future." + +async def test_closed_by_user(): + print("test_closed_by_user") + future1 = Future() + future2 = Future() + race_future = Future.race([future1, future2]) + task1 = asyncio.create_task(reject_later(future1, ExchangeClosedByUser(), 0.1)) + task2 = asyncio.create_task(reject_later(future2, ExchangeClosedByUser(), 0.1)) + try: + await race_future + assert False, "Expected an ExchangeClosedByUser" + except ExchangeClosedByUser: + assert True + assert task1.done() + assert task2.done() + except Exception as e: + assert False, f"Received Exception {e}" + +async def test_ws_future(): + await test_resolve_before() + await test_reject() + await test_race_success_before() + await test_race_return_first_exception() + await test_cancel() + await test_await_canceled_future() + await test_race_cancel() + await test_race_mixed_outcomes() + await test_race_with_wait_for_timeout() + await test_race_with_wait_for_completion() + await test_race_with_precompleted_future() + await test_closed_by_user() + diff --git a/ccxt/pro/test/base/test_order_book.py b/ccxt/pro/test/base/test_order_book.py new file mode 100644 index 0000000..d822eb3 --- /dev/null +++ b/ccxt/pro/test/base/test_order_book.py @@ -0,0 +1,345 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + + + +from ccxt.async_support.base.ws.order_book import OrderBook, IndexedOrderBook, CountedOrderBook # noqa: F402 + + + + + +def equals(a, b): + return a == b + +# -------------------------------------------------------------------------------------------------------------------- +def test_ws_order_book(): + order_book_input = { + 'bids': [[10, 10], [9.1, 11], [8.2, 12], [7.3, 13], [6.4, 14], [4.5, 13], [4.5, 0]], + 'asks': [[16.6, 10], [15.5, 11], [14.4, 12], [13.3, 13], [12.2, 14], [11.1, 13]], + 'timestamp': 1574827239000, + 'nonce': 69, + 'symbol': None, + } + order_book_target = { + 'bids': [[10, 10], [9.1, 11], [8.2, 12], [7.3, 13], [6.4, 14]], + 'asks': [[11.1, 13], [12.2, 14], [13.3, 13], [14.4, 12], [15.5, 11], [16.6, 10]], + 'timestamp': 1574827239000, + 'datetime': '2019-11-27T04:00:39.000Z', + 'nonce': 69, + 'symbol': None, + } + store_bid = { + 'bids': [[10, 10], [9.1, 11], [8.2, 12], [7.3, 13], [6.4, 14], [3, 4]], + 'asks': [[11.1, 13], [12.2, 14], [13.3, 13], [14.4, 12], [15.5, 11], [16.6, 10]], + 'timestamp': 1574827239000, + 'datetime': '2019-11-27T04:00:39.000Z', + 'nonce': 69, + 'symbol': None, + } + limited_order_book_target = { + 'bids': [[10, 10], [9.1, 11], [8.2, 12], [7.3, 13], [6.4, 14]], + 'asks': [[11.1, 13], [12.2, 14], [13.3, 13], [14.4, 12], [15.5, 11]], + 'timestamp': 1574827239000, + 'datetime': '2019-11-27T04:00:39.000Z', + 'nonce': 69, + 'symbol': None, + } + limited_deleted_order_book_target = { + 'bids': [[10, 10], [9.1, 11], [8.2, 12], [7.3, 13], [6.4, 14]], + 'asks': [[11.1, 13], [12.2, 14], [13.3, 13], [14.4, 12]], + 'timestamp': 1574827239000, + 'datetime': '2019-11-27T04:00:39.000Z', + 'nonce': 69, + 'symbol': None, + } + indexed_order_book_input = { + 'bids': [[10, 10, '1234'], [9.1, 11, '1235'], [8.2, 12, '1236'], [7.3, 13, '1237'], [6.4, 14, '1238'], [4.5, 13, '1239']], + 'asks': [[16.6, 10, '1240'], [15.5, 11, '1241'], [14.4, 12, '1242'], [13.3, 13, '1243'], [12.2, 14, '1244'], [11.1, 13, '1244']], + 'timestamp': 1574827239000, + 'nonce': 69, + 'symbol': None, + } + indexed_order_book_target = { + 'bids': [[10, 10, '1234'], [9.1, 11, '1235'], [8.2, 12, '1236'], [7.3, 13, '1237'], [6.4, 14, '1238'], [4.5, 13, '1239']], + 'asks': [[11.1, 13, '1244'], [13.3, 13, '1243'], [14.4, 12, '1242'], [15.5, 11, '1241'], [16.6, 10, '1240']], + 'timestamp': 1574827239000, + 'datetime': '2019-11-27T04:00:39.000Z', + 'nonce': 69, + 'symbol': None, + } + limited_indexed_order_book_target = { + 'bids': [[10, 10, '1234'], [9.1, 11, '1235'], [8.2, 12, '1236'], [7.3, 13, '1237'], [6.4, 14, '1238']], + 'asks': [[11.1, 13, '1244'], [13.3, 13, '1243'], [14.4, 12, '1242'], [15.5, 11, '1241'], [16.6, 10, '1240']], + 'timestamp': 1574827239000, + 'datetime': '2019-11-27T04:00:39.000Z', + 'nonce': 69, + 'symbol': None, + } + # const incrementalIndexedOrderBookTarget = { + # 'bids': [ [ 10.0, 10, '1234' ], [ 9.1, 11, '1235' ], [ 8.2, 12, '1236' ], [ 7.3, 13, '1237' ], [ 6.4, 14, '1238' ], [ 4.5, 13, '1239' ] ], + # 'asks': [ [ 11.1, 27, '1244' ], [ 13.3, 13, '1243' ], [ 14.4, 12, '1242' ], [ 15.5, 11, '1241' ], [ 16.6, 10, '1240' ] ], + # 'timestamp': 1574827239000, + # 'datetime': '2019-11-27T04:00:39.000Z', + # 'nonce': 69, + # 'symbol': undefined, + # }; + # const incrementalIndexedOrderBookDeletedTarget = { + # 'bids': [ [ 9.1, 11, '1235' ], [ 8.2, 12, '1236' ], [ 7.3, 13, '1237' ], [ 6.4, 14, '1238' ], [ 4.5, 13, '1239' ] ], + # 'asks': [ [ 11.1, 27, '1244' ], [ 13.3, 13, '1243' ], [ 14.4, 12, '1242' ], [ 15.5, 11, '1241' ], [ 16.6, 10, '1240' ] ], + # 'timestamp': 1574827239000, + # 'datetime': '2019-11-27T04:00:39.000Z', + # 'nonce': 69, + # 'symbol': undefined, + # }; + # const limitedIncrementalIndexedOrderBookTarget = { + # 'bids': [ [ 10.0, 10, '1234' ], [ 9.1, 11, '1235' ], [ 8.2, 12, '1236' ], [ 7.3, 13, '1237' ], [ 6.4, 14, '1238' ] ], + # 'asks': [ [ 11.1, 27, '1244' ], [ 13.3, 13, '1243' ], [ 14.4, 12, '1242' ], [ 15.5, 11, '1241' ], [ 16.6, 10, '1240' ] ], + # 'timestamp': 1574827239000, + # 'datetime': '2019-11-27T04:00:39.000Z', + # 'nonce': 69, + # 'symbol': undefined, + # }; + # const storedIncrementalIndexedOrderBookTarget = { + # 'bids': [ [ 10.0, 13, '1234' ], [ 9.1, 11, '1235' ], [ 8.2, 12, '1236' ], [ 7.3, 13, '1237' ], [ 6.4, 14, '1238' ], [ 4.5, 13, '1239' ] ], + # 'asks': [ [ 11.1, 27, '1244' ], [ 13.3, 13, '1243' ], [ 14.4, 12, '1242' ], [ 15.5, 11, '1241' ], [ 16.6, 10, '1240' ] ], + # 'timestamp': 1574827239000, + # 'datetime': '2019-11-27T04:00:39.000Z', + # 'nonce': 69, + # 'symbol': undefined, + # }; + # const anotherStoredIncrementalIndexedOrderBookTarget = { + # 'bids': [ [ 10.2, 13, '1234' ], [ 9.1, 11, '1235' ], [ 8.2, 12, '1236' ], [ 7.3, 13, '1237' ], [ 6.4, 14, '1238' ], [ 4.5, 13, '1239' ] ], + # 'asks': [ [ 11.1, 27, '1244' ], [ 13.3, 13, '1243' ], [ 14.4, 12, '1242' ], [ 15.5, 11, '1241' ], [ 16.6, 10, '1240' ] ], + # 'timestamp': 1574827239000, + # 'datetime': '2019-11-27T04:00:39.000Z', + # 'nonce': 69, + # 'symbol': undefined, + # }; + overwrite1234 = { + 'bids': [[9.1, 11, '1235'], [9, 3, '1231'], [9, 1, '1232'], [8.2, 12, '1236'], [7.3, 13, '1237'], [6.4, 14, '1238'], [4.5, 13, '1239'], [4, 2, '12399']], + 'asks': [[11.1, 13, '1244'], [13.3, 13, '1243'], [14.4, 12, '1242'], [15.5, 11, '1241'], [16.6, 10, '1240']], + 'timestamp': 1574827239000, + 'datetime': '2019-11-27T04:00:39.000Z', + 'nonce': 69, + 'symbol': None, + } + overwrite1244 = { + 'bids': [[10, 10, '1234'], [9.1, 11, '1235'], [8.2, 12, '1236'], [7.3, 13, '1237'], [6.4, 14, '1238'], [4.5, 13, '1239']], + 'asks': [[13.3, 13, '1243'], [13.5, 13, '1244'], [14.4, 12, '1242'], [15.5, 11, '1241'], [16.6, 10, '1240']], + 'timestamp': 1574827239000, + 'datetime': '2019-11-27T04:00:39.000Z', + 'nonce': 69, + 'symbol': None, + } + counted_order_book_input = { + 'bids': [[10, 10, 1], [9.1, 11, 1], [8.2, 12, 1], [7.3, 13, 1], [7.3, 0, 1], [6.4, 14, 5], [4.5, 13, 5], [4.5, 13, 1], [4.5, 13, 0]], + 'asks': [[16.6, 10, 1], [15.5, 11, 1], [14.4, 12, 1], [13.3, 13, 3], [12.2, 14, 3], [11.1, 13, 3], [11.1, 13, 12]], + 'timestamp': 1574827239000, + 'nonce': 69, + 'symbol': None, + } + counted_order_book_target = { + 'bids': [[10, 10, 1], [9.1, 11, 1], [8.2, 12, 1], [6.4, 14, 5]], + 'asks': [[11.1, 13, 12], [12.2, 14, 3], [13.3, 13, 3], [14.4, 12, 1], [15.5, 11, 1], [16.6, 10, 1]], + 'timestamp': 1574827239000, + 'datetime': '2019-11-27T04:00:39.000Z', + 'nonce': 69, + 'symbol': None, + } + stored_counted_orderbook_target = { + 'bids': [[10, 10, 1], [9.1, 11, 1], [8.2, 12, 1], [6.4, 14, 5], [1, 1, 6]], + 'asks': [[11.1, 13, 12], [12.2, 14, 3], [13.3, 13, 3], [14.4, 12, 1], [15.5, 11, 1], [16.6, 10, 1]], + 'timestamp': 1574827239000, + 'datetime': '2019-11-27T04:00:39.000Z', + 'nonce': 69, + 'symbol': None, + } + limited_counted_order_book_target = { + 'bids': [[10, 10, 1], [9.1, 11, 1], [8.2, 12, 1], [6.4, 14, 5]], + 'asks': [[11.1, 13, 12], [12.2, 14, 3], [13.3, 13, 3], [14.4, 12, 1], [15.5, 11, 1]], + 'timestamp': 1574827239000, + 'datetime': '2019-11-27T04:00:39.000Z', + 'nonce': 69, + 'symbol': None, + } + # const incrementalOrderBookInput = { + # 'bids': [ [ 10.0, 1 ], [ 10.0, 2 ], [ 9.1, 0 ], [ 8.2, 1 ], [ 7.3, 1 ], [ 6.4, 1 ] ], + # 'asks': [ [ 11.1, 5 ], [ 11.1, -6 ], [ 11.1, 2 ], [ 12.2, 10 ], [ 12.2, -9.875 ], [ 12.2, 0 ], [ 13.3, 3 ], [ 14.4, 4 ], [ 15.5, 1 ], [ 16.6, 3 ] ], + # 'timestamp': 1574827239000, + # 'nonce': 69, + # 'symbol': undefined, + # }; + # const incremetalOrderBookTarget = { + # 'bids': [ [ 10.0, 3 ], [ 8.2, 1 ], [ 7.3, 1 ], [ 6.4, 1 ] ], + # 'asks': [ [ 11.1, 2 ], [ 12.2, 0.125 ], [ 13.3, 3 ], [ 14.4, 4 ], [ 15.5, 1 ], [ 16.6, 3 ] ], + # 'timestamp': 1574827239000, + # 'datetime': '2019-11-27T04:00:39.000Z', + # 'nonce': 69, + # 'symbol': undefined, + # }; + # const limitedIncremetalOrderBookTarget = { + # 'bids': [ [ 10.0, 3 ], [ 8.2, 1 ], [ 7.3, 1 ], [ 6.4, 1 ] ], + # 'asks': [ [ 11.1, 2 ], [ 12.2, 0.125 ], [ 13.3, 3 ], [ 14.4, 4 ], [ 15.5, 1 ] ], + # 'timestamp': 1574827239000, + # 'datetime': '2019-11-27T04:00:39.000Z', + # 'nonce': 69, + # 'symbol': undefined, + # }; + # const storedIncremetalOrderBookTarget = { + # 'bids': [ [ 10.0, 3 ], [ 8.2, 1 ], [ 7.3, 1 ], [ 6.4, 1 ], [ 3, 3 ] ], + # 'asks': [ [ 11.1, 2 ], [ 12.2, 0.125 ], [ 13.3, 3 ], [ 14.4, 4 ], [ 15.5, 1 ], [ 16.6, 3 ] ], + # 'timestamp': 1574827239000, + # 'datetime': '2019-11-27T04:00:39.000Z', + # 'nonce': 69, + # 'symbol': undefined, + # }; + # const doubleStoredIncremetalOrderBookTarget = { + # 'bids': [ [ 10.0, 3 ], [ 8.2, 1 ], [ 7.3, 1 ], [ 6.4, 1 ], [ 3, 10 ] ], + # 'asks': [ [ 11.1, 2 ], [ 12.2, 0.125 ], [ 13.3, 3 ], [ 14.4, 4 ], [ 15.5, 1 ], [ 16.6, 3 ] ], + # 'timestamp': 1574827239000, + # 'datetime': '2019-11-27T04:00:39.000Z', + # 'nonce': 69, + # 'symbol': undefined, + # }; + # const negativeStoredIncremetalOrderBookTarget = { + # 'bids': [ [ 10.0, 3 ], [ 8.2, 1 ], [ 7.3, 1 ], [ 6.4, 1 ] ], + # 'asks': [ [ 11.1, 2 ], [ 12.2, 0.125 ], [ 13.3, 3 ], [ 14.4, 4 ], [ 16.6, 3 ] ], + # 'timestamp': 1574827239000, + # 'datetime': '2019-11-27T04:00:39.000Z', + # 'nonce': 69, + # 'symbol': undefined, + # }; + # -------------------------------------------------------------------------------------------------------------------- + order_book = OrderBook(order_book_input) + limited = OrderBook(order_book_input, 5) + order_book.limit() + assert equals(order_book, order_book_target) + limited.limit() + assert equals(limited, limited_order_book_target) + order_book.limit() + assert equals(order_book, order_book_target) + bids = order_book['bids'] + bids.store(1000, 0) + order_book.limit() + assert equals(order_book, order_book_target) + bids.store(3, 4) + order_book.limit() + assert equals(order_book, store_bid) + bids.store(3, 0) + order_book.limit() + assert equals(order_book, order_book_target) + asks = limited['asks'] + asks.store(15.5, 0) + limited.limit() + assert equals(limited, limited_deleted_order_book_target) + # -------------------------------------------------------------------------------------------------------------------- + indexed_order_book = IndexedOrderBook(indexed_order_book_input) + limited_indexed_order_book = IndexedOrderBook(indexed_order_book_input, 5) + indexed_order_book.limit() + assert equals(indexed_order_book, indexed_order_book_target) + limited_indexed_order_book.limit() + assert equals(limited_indexed_order_book, limited_indexed_order_book_target) + indexed_order_book.limit() + assert equals(indexed_order_book, indexed_order_book_target) + indexed_bids = indexed_order_book['bids'] + indexed_bids.store_array([1000, 0, '12345']) + assert equals(indexed_order_book, indexed_order_book_target) + indexed_bids.store_array([10, 0, '1234']) + indexed_bids.store_array([10, 2, '1231']) + indexed_bids.store_array([10, 1, '1232']) + indexed_bids.store_array([4, 2, '12399']) + indexed_bids.store_array([9, 2, '1231']) + indexed_bids.store_array([9, 3, '1231']) + indexed_bids.store_array([9, 1, '1232']) + indexed_order_book.limit() + assert equals(indexed_order_book, overwrite1234) + indexed_order_book = IndexedOrderBook(indexed_order_book_input) + indexed_asks = indexed_order_book['asks'] + indexed_asks.store_array([13.5, 13, '1244']) + indexed_order_book.limit() + assert equals(indexed_order_book, overwrite1244) + # -------------------------------------------------------------------------------------------------------------------- + counted_order_book = CountedOrderBook(counted_order_book_input) + limited_counted_order_book = CountedOrderBook(counted_order_book_input, 5) + counted_order_book.limit() + assert equals(counted_order_book, counted_order_book_target) + limited_counted_order_book.limit() + assert equals(limited_counted_order_book, limited_counted_order_book_target) + counted_order_book.limit() + assert equals(counted_order_book, counted_order_book_target) + counted_bids = counted_order_book['bids'] + counted_bids.store_array([5, 0, 6]) + counted_order_book.limit() + assert equals(counted_order_book, counted_order_book_target) + counted_bids.store_array([1, 1, 6]) + counted_order_book.limit() + assert equals(counted_order_book, stored_counted_orderbook_target) + # -------------------------------------------------------------------------------------------------------------------- + # let incrementalOrderBook = new IncrementalOrderBook (incrementalOrderBookInput); + # const limitedIncrementalOrderBook = new IncrementalOrderBook (incrementalOrderBookInput, 5); + # incrementalOrderBook.limit (); + # assert (equals (incrementalOrderBook, incremetalOrderBookTarget)); + # incrementalOrderBook.limit (5); + # limitedIncrementalOrderBook.limit (); + # assert (equals (incrementalOrderBook, limitedIncremetalOrderBookTarget)); + # assert (equals (limitedIncrementalOrderBook, limitedIncremetalOrderBookTarget)); + # incrementalOrderBook.limit (); + # assert (equals (incrementalOrderBook, incremetalOrderBookTarget)); + # bids = incrementalOrderBook['bids']; + # bids.store (3, 3); + # incrementalOrderBook.limit (); + # assert (equals (incrementalOrderBook, storedIncremetalOrderBookTarget)); + # bids.store (3, 7); + # incrementalOrderBook.limit (); + # assert (equals (incrementalOrderBook, doubleStoredIncremetalOrderBookTarget)); + # bids.store (17, 0); + # assert (equals (incrementalOrderBook, doubleStoredIncremetalOrderBookTarget)); + # incrementalOrderBook = new IncrementalOrderBook (incrementalOrderBookInput); + # asks = incrementalOrderBook['asks']; + # asks.store (15.5, -10); + # incrementalOrderBook.limit (); + # assert (equals (incrementalOrderBook, negativeStoredIncremetalOrderBookTarget)); + # -------------------------------------------------------------------------------------------------------------------- + # let incrementalIndexedOrderBook = new IncrementalIndexedOrderBook (indexedOrderBookInput); + # const limitedIncrementalIndexedOrderBook = new IncrementalIndexedOrderBook (indexedOrderBookInput, 5); + # incrementalIndexedOrderBook.limit (); + # assert (equals (incrementalIndexedOrderBook, incrementalIndexedOrderBookTarget)); + # incrementalIndexedOrderBook.limit (5); + # limitedIncrementalIndexedOrderBook.limit (); + # assert (equals (incrementalIndexedOrderBook, limitedIncrementalIndexedOrderBookTarget)); + # assert (equals (limitedIncrementalIndexedOrderBook, limitedIncrementalIndexedOrderBookTarget)); + # incrementalIndexedOrderBook.limit (); + # assert (equals (incrementalIndexedOrderBook, incrementalIndexedOrderBookTarget)); + # bids = incrementalIndexedOrderBook['bids']; + # bids.store (5, 0, 'xxyy'); + # incrementalIndexedOrderBook.limit (); + # assert (equals (incrementalIndexedOrderBook, incrementalIndexedOrderBookTarget)); + # bids.store (10.0, 3, '1234'); # price does match merge size + # incrementalIndexedOrderBook.limit (); + # assert (equals (incrementalIndexedOrderBook, storedIncrementalIndexedOrderBookTarget)); + # bids.store (0, 0, '1234'); + # incrementalIndexedOrderBook.limit (); + # assert (equals (incrementalIndexedOrderBook, incrementalIndexedOrderBookDeletedTarget)); + # incrementalIndexedOrderBook = new IncrementalIndexedOrderBook (indexedOrderBookInput); + # bids = incrementalIndexedOrderBook['bids']; + # bids.store (10.2, 3, '1234'); # price does not match merge size + # incrementalIndexedOrderBook.limit (); + # assert (equals (incrementalIndexedOrderBook, anotherStoredIncrementalIndexedOrderBookTarget)); + # -------------------------------------------------------------------------------------------------------------------- + reset_book = OrderBook(store_bid) + reset_book.limit() + reset_book.reset(order_book_input) + reset_book.limit() + assert equals(reset_book, order_book_target) diff --git a/ccxt/pro/test/base/tests_init.py b/ccxt/pro/test/base/tests_init.py new file mode 100644 index 0000000..bd2d7fd --- /dev/null +++ b/ccxt/pro/test/base/tests_init.py @@ -0,0 +1,20 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +from asyncio import run + +from ccxt.pro.test.base.test_order_book import test_ws_order_book # noqa: F401 +from ccxt.pro.test.base.test_cache import test_ws_cache # noqa: F401 +# todo : from ccxt.pro.test.base.test_close import test_ws_close # noqa: F401 +from ccxt.pro.test.base.test_future import test_ws_future # noqa: F401 +from ccxt.pro.test.base.test_abnormal_close import test_abnormal_close # noqa: F401 + +def test_base_init_ws(): + test_ws_order_book() + test_ws_cache() + # todo : run(test_ws_close()) + run(test_ws_future()) + # run(test_abnormal_close()) stays in infinite loop in travis diff --git a/ccxt/pro/toobit.py b/ccxt/pro/toobit.py new file mode 100644 index 0000000..51d3661 --- /dev/null +++ b/ccxt/pro/toobit.py @@ -0,0 +1,1109 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolById, ArrayCacheBySymbolBySide, ArrayCacheByTimestamp +from ccxt.base.types import Any, Balances, Bool, Int, Market, Order, OrderBook, Position, Str, Strings, Ticker, Tickers, Trade +from ccxt.async_support.base.ws.client import Client +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import NotSupported + + +class toobit(ccxt.async_support.toobit): + + def describe(self) -> Any: + return self.deep_extend(super(toobit, self).describe(), { + 'has': { + 'ws': True, + 'watchBalance': True, + 'watchMyTrades': True, + 'watchOHLCV': True, + 'watchOHLCVForSymbols': True, + 'watchOrderBook': True, + 'watchOrderBookForSymbols': True, + 'watchOrders': True, + 'watchTicker': True, + 'watchTickers': True, + 'watchTrades': True, + 'watchTradesForSymbols': True, + # 'watchPosition': False, + }, + 'urls': { + 'api': { + 'ws': { + 'common': 'wss://stream.toobit.com', + }, + }, + }, + 'options': { + 'ws': { + 'timeframes': { + '1m': '1m', + '3m': '3m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1h', + '2h': '2h', + '4h': '4h', + '6h': '6h', + '8h': '8h', + '12h': '12h', + '1d': '1d', + '1w': '1w', + '1M': '1M', + }, + 'watchOrderBook': { + 'channel': 'depth', # depth, diffDepth + }, + 'listenKeyRefreshRate': 1200000, # 20 mins + }, + }, + 'streaming': { + 'keepAlive': (60 - 1) * 5 * 1000, # every 5 minutes + 'ping': self.ping, + }, + 'exceptions': { + 'ws': { + 'exact': { + }, + }, + }, + }) + + def ping(self, client: Client): + return { + 'ping': self.milliseconds(), + } + + def handle_message(self, client: Client, message): + # + # public + # + # { + # topic: "trade", + # symbol: "DOGEUSDT", + # symbolName: "DOGEUSDT", + # params: { + # realtimeInterval: "24h", + # binary: "false", + # }, + # data: [ + # { + # v: "4864732022868004630", + # t: 1757243788405, + # p: "0.21804", + # q: "80", + # m: True, + # }, + # ], + # f: True, # initial first snapshot or not + # sendTime: 1757244002117, + # shared: False, + # } + # + # private + # + # [ + # { + # e: 'outboundContractAccountInfo', + # E: '1758228398234', + # T: True, + # W: True, + # D: True, + # B: [[Object]] + # } + # ] + # + topic = self.safe_string(message, 'topic') + if self.handle_error_message(client, message): + return + # + # handle ping-pong: {ping: 1758540450000} + # + pongTimestamp = self.safe_integer(message, 'pong') + if pongTimestamp is not None: + self.handle_incoming_pong(client, pongTimestamp) + return + methods: dict = { + 'trade': self.handle_trades, + 'kline': self.handle_ohlcv, + 'realtimes': self.handle_tickers, + 'depth': self.handle_order_book_partial_snapshot, + 'diffDepth': self.handle_order_book, + 'outboundAccountInfo': self.handle_balance, + 'outboundContractAccountInfo': self.handle_balance, + 'executionReport': self.handle_order, + 'contractExecutionReport': self.handle_order, + 'ticketInfo': self.handle_my_trade, + 'outboundContractPositionInfo': self.handle_positions, + } + method = self.safe_value(methods, topic) + if method is not None: + method(client, message) + else: + # check private streams + for i in range(0, len(message)): + item = message[i] + event = self.safe_string(item, 'e') + method2 = self.safe_value(methods, event) + if method2 is not None: + method2(client, item) + + def handle_incoming_pong(self, client: Client, pongTimestamp: Int): + client.lastPong = pongTimestamp + + async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + watches information on multiple trades made in a market + + https://toobit-docs.github.io/apidocs/spot/v1/en/#trade-streams + + :param str symbol: unified market symbol of the market trades were made in + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trade structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + return await self.watch_trades_for_symbols([symbol], since, limit, params) + + async def watch_trades_for_symbols(self, symbols: List[str], since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a list of symbols + + https://toobit-docs.github.io/apidocs/spot/v1/en/#trade-streams + + :param str[] symbols: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.name]: the name of the method to call, 'trade' or 'aggTrade', default is 'trade' + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, False) + messageHashes = [] + subParams = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + market = self.market(symbol) + messageHashes.append('trade::' + symbol) + rawHash = market['id'] + subParams.append(rawHash) + marketIds = self.market_ids(symbols) + url = self.urls['api']['ws']['common'] + '/quote/ws/v1' + request: dict = { + 'symbol': ','.join(marketIds), + 'topic': 'trade', + 'event': 'sub', + } + trades = await self.watch_multiple(url, messageHashes, self.extend(request, params), messageHashes) + if self.newUpdates: + first = self.safe_value(trades, 0) + tradeSymbol = self.safe_string(first, 'symbol') + limit = trades.getLimit(tradeSymbol, limit) + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + def handle_trades(self, client: Client, message): + # + # { + # symbol: "DOGEUSDT", + # symbolName: "DOGEUSDT", + # topic: "trade", + # params: { + # realtimeInterval: "24h", + # binary: "false", + # }, + # data: [ + # { + # v: "4864732022868004630", + # t: 1757243788405, + # p: "0.21804", + # q: "80", + # m: True, + # }, + # ], + # f: True, # initial first snapshot or not + # sendTime: 1757244002117, + # shared: False, + # } + # + marketId = self.safe_string(message, 'symbol') + market = self.safe_market(marketId) + symbol = market['symbol'] + if not (symbol in self.trades): + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + self.trades[symbol] = ArrayCache(limit) + stored = self.trades[symbol] + data = self.safe_list(message, 'data', []) + parsed = self.parse_ws_trades(data, market) + for i in range(0, len(parsed)): + trade = parsed[i] + trade['symbol'] = symbol + stored.append(trade) + messageHash = 'trade::' + symbol + client.resolve(stored, messageHash) + + def parse_ws_trade(self, trade: dict, market: Market = None) -> Trade: + return self.parse_trade(trade, market) + + async def watch_ohlcv(self, symbol: str, timeframe='1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + watches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://toobit-docs.github.io/apidocs/spot/v1/en/#kline-candlestick-streams + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + params['callerMethodName'] = 'watchOHLCV' + result = await self.watch_ohlcv_for_symbols([[symbol, timeframe]], since, limit, params) + return result[symbol][timeframe] + + async def watch_ohlcv_for_symbols(self, symbolsAndTimeframes: List[List[str]], since: Int = None, limit: Int = None, params={}): + """ + watches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://toobit-docs.github.io/apidocs/spot/v1/en/#kline-candlestick-streams + + :param str[][] symbolsAndTimeframes: array of arrays containing unified symbols and timeframes to fetch OHLCV data for, example [['BTC/USDT', '1m'], ['LTC/USDT', '5m']] + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + url = self.urls['api']['ws']['common'] + '/quote/ws/v1' + messageHashes = [] + timeframes = self.safe_dict(self.options['ws'], 'timeframes', {}) + marketIds = [] + selectedTimeframe: Str = None + for i in range(0, len(symbolsAndTimeframes)): + data = symbolsAndTimeframes[i] + symbolStr = self.safe_string(data, 0) + market = self.market(symbolStr) + marketId = market['id'] + unfiedTimeframe = self.safe_string(data, 1, '1m') + rawTimeframe = self.safe_string(timeframes, unfiedTimeframe, unfiedTimeframe) + if selectedTimeframe is not None and selectedTimeframe != rawTimeframe: + raise NotSupported(self.id + ' watchOHLCVForSymbols() only supports a single timeframe for all symbols') + else: + selectedTimeframe = rawTimeframe + marketIds.append(marketId) + messageHashes.append('ohlcv::' + symbolStr + '::' + unfiedTimeframe) + request: dict = { + 'symbol': ','.join(marketIds), + 'topic': 'kline_' + selectedTimeframe, + 'event': 'sub', + } + symbol, timeframe, stored = await self.watch_multiple(url, messageHashes, self.extend(request, params), messageHashes) + if self.newUpdates: + limit = stored.getLimit(symbol, limit) + filtered = self.filter_by_since_limit(stored, since, limit, 0, True) + return self.create_ohlcv_object(symbol, timeframe, filtered) + + def handle_ohlcv(self, client: Client, message): + # + # { + # symbol: 'DOGEUSDT', + # symbolName: 'DOGEUSDT', + # klineType: '1m', + # topic: 'kline', + # params: {realtimeInterval: '24h', klineType: '1m', binary: 'false'}, + # data: [ + # { + # t: 1757251200000, + # s: 'DOGEUSDT', + # sn: 'DOGEUSDT', + # c: '0.21889', + # h: '0.21898', + # l: '0.21889', + # o: '0.21897', + # v: '5247', + # st: 0 + # } + # ], + # f: True, + # sendTime: 1757251217643, + # shared: False + # } + # + marketId = self.safe_string(message, 'symbol') + market = self.market(marketId) + symbol = market['symbol'] + params = self.safe_dict(message, 'params', {}) + timeframeId = self.safe_string(params, 'klineType') + timeframe = self.find_timeframe(timeframeId) + if not (symbol in self.ohlcvs): + self.ohlcvs[symbol] = {} + if not (timeframe in self.ohlcvs[symbol]): + limit = self.safe_integer(self.options['ws'], 'OHLCVLimit', 1000) + self.ohlcvs[symbol][timeframe] = ArrayCacheByTimestamp(limit) + stored = self.ohlcvs[symbol][timeframe] + data = self.safe_list(message, 'data', []) + for i in range(0, len(data)): + parsed = self.parse_ws_ohlcv(data[i], market) + stored.append(parsed) + messageHash = 'ohlcv::' + symbol + '::' + timeframe + resolveData = [symbol, timeframe, stored] + client.resolve(resolveData, messageHash) + + def parse_ws_ohlcv(self, ohlcv, market=None) -> list: + # + # { + # t: 1757251200000, + # o: '0.21897', + # h: '0.21898', + # l: '0.21889', + # c: '0.21889', + # v: '5247', + # s: 'DOGEUSDT', + # sn: 'DOGEUSDT', + # st: 0 + # } + # + parsed = self.parse_ohlcv(ohlcv, market) + return parsed + + async def watch_ticker(self, symbol: str, params={}) -> Ticker: + """ + + https://toobit-docs.github.io/apidocs/spot/v1/en/#individual-symbol-ticker-streams + + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbol = self.symbol(symbol) + tickers = await self.watch_tickers([symbol], params) + return tickers[symbol] + + async def watch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + + https://toobit-docs.github.io/apidocs/spot/v1/en/#individual-symbol-ticker-streams + + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list + :param str[] symbols: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, False) + messageHashes = [] + subParams = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + market = self.market(symbol) + messageHashes.append('ticker::' + symbol) + rawHash = market['id'] + subParams.append(rawHash) + marketIds = self.market_ids(symbols) + url = self.urls['api']['ws']['common'] + '/quote/ws/v1' + request: dict = { + 'symbol': ','.join(marketIds), + 'topic': 'realtimes', + 'event': 'sub', + } + ticker = await self.watch_multiple(url, messageHashes, self.extend(request, params), messageHashes) + if self.newUpdates: + result: dict = {} + result[ticker['symbol']] = ticker + return result + return self.filter_by_array(self.tickers, 'symbol', symbols) + + def handle_tickers(self, client: Client, message): + # + # { + # "symbol": "DOGEUSDT", + # "symbolName": "DOGEUSDT", + # "topic": "realtimes", + # "params": { + # "realtimeInterval": "24h" + # }, + # "data": [ + # { + # "t": 1757257643683, + # "s": "DOGEUSDT", + # "o": "0.21462", + # "h": "0.22518", + # "l": "0.21229", + # "c": "0.2232", + # "v": "283337017", + # "qv": "62063771.42702", + # "sn": "DOGEUSDT", + # "m": "0.04", + # "e": 301, + # "c24h": "0.2232", + # "h24h": "0.22518", + # "l24h": "0.21229", + # "o24h": "0.21462", + # "v24h": "283337017", + # "qv24h": "62063771.42702", + # "m24h": "0.04" + # } + # ], + # "f": False, + # "sendTime": 1757257643751, + # "shared": False + # } + # + data = self.safe_list(message, 'data') + newTickers = {} + for i in range(0, len(data)): + ticker = data[i] + parsed = self.parse_ws_ticker(ticker) + symbol = parsed['symbol'] + self.tickers[symbol] = parsed + newTickers[symbol] = parsed + messageHash = 'ticker::' + symbol + client.resolve(parsed, messageHash) + client.resolve(newTickers, 'tickers') + + def parse_ws_ticker(self, ticker, market=None): + return self.parse_ticker(ticker, market) + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://toobit-docs.github.io/apidocs/spot/v1/en/#partial-book-depth-streams + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return. + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + return await self.watch_order_book_for_symbols([symbol], limit, params) + + async def watch_order_book_for_symbols(self, symbols: List[str], limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://toobit-docs.github.io/apidocs/spot/v1/en/#partial-book-depth-streams + + :param str[] symbols: unified array of symbols + :param int [limit]: the maximum amount of order book entries to return. + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, False) + channel: Str = None + channel, params = self.handle_option_and_params(params, 'watchOrderBook', 'channel', 'depth') + messageHashes = [] + subParams = [] + for i in range(0, len(symbols)): + symbol = symbols[i] + market = self.market(symbol) + messageHashes.append('orderBook::' + symbol + '::' + channel) + rawHash = market['id'] + subParams.append(rawHash) + marketIds = self.market_ids(symbols) + url = self.urls['api']['ws']['common'] + '/quote/ws/v1' + request: dict = { + 'symbol': ','.join(marketIds), + 'topic': channel, + 'event': 'sub', + } + orderbook = await self.watch_multiple(url, messageHashes, self.extend(request, params), messageHashes) + return orderbook.limit() + + def handle_order_book(self, client: Client, message): + # + # { + # symbol: 'DOGEUSDT', + # symbolName: 'DOGEUSDT', + # topic: 'depth', + # params: {realtimeInterval: '24h'}, + # data: [ + # { + # e: 301, + # t: 1757304842860, + # v: '9814355_1E-18', + # b: [Array], + # a: [Array], + # o: 0 + # } + # ], + # f: False, + # sendTime: 1757304843047, + # shared: False + # } + # + isSnapshot = self.safe_bool(message, 'f', False) + if isSnapshot: + self.set_order_book_snapshot(client, message, 'diffDepth') + return + marketId = self.safe_string(message, 'symbol') + market = self.safe_market(marketId) + symbol = market['symbol'] + data = self.safe_list(message, 'data', []) + for i in range(0, len(data)): + entry = data[i] + messageHash = 'orderBook::' + symbol + '::' + 'diffDepth' + if not (symbol in self.orderbooks): + limit = self.safe_integer(self.options['ws'], 'orderBookLimit', 1000) + self.orderbooks[symbol] = self.order_book({}, limit) + orderBook = self.orderbooks[symbol] + timestamp = self.safe_integer(entry, 't') + bids = self.safe_list(entry, 'b', []) + asks = self.safe_list(entry, 'a', []) + self.handle_deltas(orderBook['asks'], asks) + self.handle_deltas(orderBook['bids'], bids) + orderBook['timestamp'] = timestamp + self.orderbooks[symbol] = orderBook + client.resolve(orderBook, messageHash) + + def handle_delta(self, bookside, delta): + bidAsk = self.parse_bid_ask(delta) + bookside.storeArray(bidAsk) + + def handle_order_book_partial_snapshot(self, client: Client, message): + # + # { + # symbol: 'DOGEUSDT', + # symbolName: 'DOGEUSDT', + # topic: 'depth', + # params: {realtimeInterval: '24h'}, + # data: [ + # { + # e: 301, + # s: 'DOGEUSDT', + # t: 1757304842860, + # v: '9814355_1E-18', + # b: [Array], + # a: [Array], + # o: 0 + # } + # ], + # f: False, + # sendTime: 1757304843047, + # shared: False + # } + # + self.set_order_book_snapshot(client, message, 'depth') + + def set_order_book_snapshot(self, client: Client, message, channel: str): + data = self.safe_list(message, 'data', []) + length = len(data) + if length == 0: + return + for i in range(0, length): + entry = data[i] + marketId = self.safe_string(entry, 's') + symbol = self.safe_symbol(marketId) + messageHash = 'orderBook::' + symbol + '::' + channel + if not (symbol in self.orderbooks): + limit = self.safe_integer(self.options['ws'], 'orderBookLimit', 1000) + self.orderbooks[symbol] = self.order_book({}, limit) + orderbook = self.orderbooks[symbol] + timestamp = self.safe_integer(entry, 't') + snapshot = self.parse_order_book(entry, symbol, timestamp, 'b', 'a') + orderbook.reset(snapshot) + client.resolve(orderbook, messageHash) + + async def watch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://toobit-docs.github.io/apidocs/spot/v1/en/#payload-account-update + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + await self.authenticate() + marketType = None + marketType, params = self.handle_market_type_and_params('watchBalance', None, params) + isSpot = (marketType == 'spot') + type = 'spot' if isSpot else 'contract' + spotSubHash = 'spot:balance' + swapSubHash = 'contract:private' + spotMessageHash = 'spot:balance' + swapMessageHash = 'contract:balance' + messageHash = spotMessageHash if isSpot else swapMessageHash + subscriptionHash = spotSubHash if isSpot else swapSubHash + url = self.get_user_stream_url() + client = self.client(url) + self.set_balance_cache(client, marketType, subscriptionHash, params) + client.future(type + ':fetchBalanceSnapshot') + return await self.watch(url, messageHash, params, subscriptionHash) + + def set_balance_cache(self, client: Client, marketType, subscriptionHash: Str = None, params={}): + if subscriptionHash in client.subscriptions: + return + type = 'spot' if (marketType == 'spot') else 'contract' + messageHash = type + ':fetchBalanceSnapshot' + if not (messageHash in client.futures): + client.future(messageHash) + self.spawn(self.load_balance_snapshot, client, messageHash, marketType) + + def handle_balance(self, client: Client, message): + # + # spot + # + # [ + # { + # e: 'outboundAccountInfo', + # E: '1758226989725', + # T: True, + # W: True, + # D: True, + # B: [ + # { + # a: "USDT", + # f: "6.37242839", + # l: "0", + # }, + # ] + # } + # ] + # + # contract + # + # [ + # { + # e: 'outboundContractAccountInfo', + # E: '1758226989742', + # T: True, + # W: True, + # D: True, + # B: [[Object]] + # } + # ] + # + channel = self.safe_string(message, 'e') + data = self.safe_list(message, 'B', []) + timestamp = self.safe_integer(message, 'E') + type = 'contract' if (channel == 'outboundContractAccountInfo') else 'spot' + if not (type in self.balance): + self.balance[type] = {} + self.balance[type]['info'] = data + self.balance[type]['timestamp'] = timestamp + self.balance[type]['datetime'] = self.iso8601(timestamp) + for i in range(0, len(data)): + balance = data[i] + currencyId = self.safe_string(balance, 'a') + code = self.safe_currency_code(currencyId) + account = self.account() + account['info'] = balance + account['used'] = self.safe_string(balance, 'l') + account['free'] = self.safe_string(balance, 'f') + self.balance[type][code] = account + self.balance[type] = self.safe_balance(self.balance[type]) + client.resolve(self.balance[type], type + ':balance') + + async def load_balance_snapshot(self, client, messageHash, marketType): + response = await self.fetch_balance({'type': marketType}) + type = 'spot' if (marketType == 'spot') else 'contract' + self.balance[type] = self.extend(response, self.safe_dict(self.balance, type, {})) + # don't remove the future from the .futures cache + future = client.futures[messageHash] + future.resolve() + client.resolve(self.balance[type], type + ':fetchBalanceSnapshot') + client.resolve(self.balance[type], type + ':balance') # we should also resolve right away after snapshot, so user doesn't double-fetch balance + + async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + watches information on multiple orders made by the user + + https://toobit-docs.github.io/apidocs/spot/v1/en/#payload-order-update + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + await self.authenticate() + market = self.market_or_None(symbol) + symbol = self.safe_string(market, 'symbol', symbol) + messageHash = 'orders' + if symbol is not None: + messageHash = messageHash + ':' + symbol + url = self.get_user_stream_url() + orders = await self.watch(url, messageHash, params, messageHash) + if self.newUpdates: + limit = orders.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(orders, symbol, since, limit, True) + + def handle_order(self, client: Client, message): + # + # { + # "e": "executionReport", + # "E": "1758311011844", + # "s": "DOGEUSDT", + # "c": "1758311011948", + # "S": "BUY", + # "o": "LIMIT", + # "f": "GTC", + # "q": "22", + # "p": "0.23", + # "pt": "INPUT", + # "X": "NEW", + # "i": "2043255292855185152", + # "l": "0", # Last executed quantity + # "z": "0", # Cumulative filled quantity + # "L": "0", # Last executed price + # "n": "0", + # "N": "", + # "u": True, + # "w": True, + # "m": False, + # "O": "1758311011833", + # "U": "1758311011841", + # "Z": "0", + # "C": False, + # "v": "0", + # "rp": "0", + # "td": "0" + # } + # + if self.orders is None: + limit = self.safe_integer(self.options, 'ordersLimit', 1000) + self.orders = ArrayCacheBySymbolById(limit) + orders = self.orders + order = self.parse_ws_order(message) + orders.append(order) + messageHash = 'orders' + client.resolve(orders, messageHash) + messageHash = 'orders:' + self.safe_string(order, 'symbol') + client.resolve(orders, messageHash) + + def parse_ws_order(self, order, market=None): + timestamp = self.safe_integer(order, 'O') + marketId = self.safe_string(order, 's') + symbol = self.safe_symbol(marketId, market) + priceType = self.safe_string_lower(order, 'pt') + rawOrderType = self.safe_string_lower(order, 'o') + orderType: Str = None + if priceType == 'market': + orderType = 'market' + else: + orderType = rawOrderType + feeCost = self.safe_number(order, 'n') + fee = None + if feeCost is not None: + fee = { + 'cost': feeCost, + 'currency': None, + } + return self.safe_order({ + 'info': order, + 'id': self.safe_string(order, 'i'), + 'clientOrderId': self.safe_string(order, 'c'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastUpdateTimestamp': self.safe_integer_2(order, 'U', 'E'), + 'symbol': symbol, + 'type': orderType, + 'timeInForce': self.safe_string_upper(order, 'f'), + 'postOnly': None, + 'side': self.safe_string_lower(order, 'S'), + 'price': self.safe_string(order, 'L'), + 'stopPrice': None, + 'triggerPrice': None, + 'amount': self.safe_string(order, 'q'), + 'cost': None, + 'average': self.safe_string(order, 'p'), + 'filled': self.safe_string(order, 'z'), + 'remaining': None, + 'status': self.parse_order_status(self.safe_string(order, 'X')), + 'fee': fee, + 'trades': None, + }, market) + + async def watch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + watches information on multiple trades made by the user + + https://toobit-docs.github.io/apidocs/spot/v1/en/#payload-ticket-push + + :param str symbol: unified market symbol of the market trades were made in + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trade structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.unifiedMargin]: use unified margin account + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + await self.authenticate() + market = self.market_or_None(symbol) + symbol = self.safe_string(market, 'symbol', symbol) + messageHash = 'myTrades' + if symbol is not None: + messageHash = messageHash + ':' + symbol + url = self.get_user_stream_url() + trades = await self.watch(url, messageHash, params, messageHash) + if self.newUpdates: + limit = trades.getLimit(symbol, limit) + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + def handle_my_trade(self, client: Client, message): + # + # { + # "e": "ticketInfo", + # "E": "1758314657847", + # "s": "DOGEUSDT", + # "q": "22.0", + # "t": "1758314657842", + # "p": "0.26667", + # "T": "4864732022877055421", + # "o": "2043285877770284800", + # "c": "1758314657002", + # "a": "1783404067076253952", + # "m": False, + # "S": "BUY" + # } + # + myTrades = self.myTrades + if myTrades is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + myTrades = ArrayCacheBySymbolById(limit) + trade = self.parse_my_trade(message) + myTrades.append(trade) + messageHash = 'myTrades:' + trade['symbol'] + client.resolve(myTrades, messageHash) + messageHash = 'myTrades' + client.resolve(myTrades, messageHash) + + def parse_my_trade(self, trade, market=None): + marketId = self.safe_string(trade, 's') + ts = self.safe_string(trade, 't') + return self.safe_trade({ + 'info': trade, + 'id': self.safe_string(trade, 'T'), + 'timestamp': ts, + 'datetime': self.iso8601(ts), + 'symbol': self.safe_symbol(marketId, market), + 'order': self.safe_string(trade, 'o'), + 'type': None, + 'side': self.safe_string_lower(trade, 'S'), + 'takerOrMaker': 'maker' if self.safe_bool(trade, 'm') else 'taker', + 'price': self.safe_string(trade, 'p'), + 'amount': self.safe_string(trade, 'q'), + 'cost': None, + 'fee': None, + }, market) + + async def watch_positions(self, symbols: Strings = None, since: Int = None, limit: Int = None, params={}) -> List[Position]: + """ + + https://toobit-docs.github.io/apidocs/usdt_swap/v1/en/#event-position-update + + watch all open positions + :param str[] [symbols]: list of unified market symbols + :param int [since]: the earliest time in ms to fetch positions for + :param int [limit]: the maximum number of positions to retrieve + :param dict params: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + await self.load_markets() + await self.authenticate() + messageHash = '' + if not self.is_empty(symbols): + symbols = self.market_symbols(symbols) + messageHash = '::' + ','.join(symbols) + url = self.get_user_stream_url() + client = self.client(url) + await self.authenticate(url) + self.set_positions_cache(client, symbols) + cache = self.positions + if cache is None: + snapshot = await client.future('fetchPositionsSnapshot') + return self.filter_by_symbols_since_limit(snapshot, symbols, since, limit, True) + newPositions = await self.watch(url, messageHash, None, messageHash) + if self.newUpdates: + return newPositions + return self.filter_by_symbols_since_limit(cache, symbols, since, limit, True) + + def set_positions_cache(self, client: Client, type, symbols: Strings = None, isPortfolioMargin=False): + if self.positions is None: + self.positions = {} + if type in self.positions: + return + fetchPositionsSnapshot = self.handle_option('watchPositions', 'fetchPositionsSnapshot', False) + if fetchPositionsSnapshot: + messageHash = type + ':fetchPositionsSnapshot' + if not (messageHash in client.futures): + client.future(messageHash) + self.spawn(self.load_positions_snapshot, client, messageHash, type, isPortfolioMargin) + else: + self.positions[type] = ArrayCacheBySymbolBySide() + + async def load_positions_snapshot(self, client, messageHash, type): + params: dict = { + 'type': type, + } + positions = await self.fetch_positions(None, params) + self.positions[type] = ArrayCacheBySymbolBySide() + cache = self.positions[type] + for i in range(0, len(positions)): + position = positions[i] + cache.append(position) + # don't remove the future from the .futures cache + future = client.futures[messageHash] + future.resolve(cache) + client.resolve(cache, type + ':positions') + + def handle_positions(self, client, message): + # + # [ + # { + # e: 'outboundContractPositionInfo', + # E: '1758316454554', + # A: '1783404067076253954', + # s: 'DOGE-SWAP-USDT', + # S: 'LONG', + # p: '0', + # P: '0', + # a: '0', + # f: '0.1228', + # m: '0', + # r: '0', + # up: '0', + # pr: '0', + # pv: '0', + # v: '3.0', + # mt: 'CROSS', + # mm: '0', + # mp: '0.265410000000000000' + # } + # ] + # + subscriptions = list(client.subscriptions.keys()) + accountType = subscriptions[0] + if self.positions is None: + self.positions = {} + if not (accountType in self.positions): + self.positions[accountType] = ArrayCacheBySymbolBySide() + cache = self.positions[accountType] + newPositions = [] + for i in range(0, len(message)): + rawPosition = message[i] + position = self.parse_ws_position(rawPosition) + timestamp = self.safe_integer(rawPosition, 'E') + position['timestamp'] = timestamp + position['datetime'] = self.iso8601(timestamp) + newPositions.append(position) + cache.append(position) + messageHashes = self.find_message_hashes(client, accountType + ':positions::') + for i in range(0, len(messageHashes)): + messageHash = messageHashes[i] + parts = messageHash.split('::') + symbolsString = parts[1] + symbols = symbolsString.split(',') + positions = self.filter_by_array(newPositions, 'symbol', symbols, False) + if not self.is_empty(positions): + client.resolve(positions, messageHash) + client.resolve(newPositions, accountType + ':positions') + + def parse_ws_position(self, position, market=None): + marketId = self.safe_string(position, 's') + return self.safe_position({ + 'info': position, + 'id': None, + 'symbol': self.safe_symbol(marketId, None), + 'notional': self.omit_zero(self.safe_string(position, 'pv')), + 'marginMode': self.safe_string_lower(position, 'mt'), + 'liquidationPrice': self.safe_string(position, 'f'), + 'entryPrice': self.safe_string(position, 'p'), + 'unrealizedPnl': self.safe_string(position, 'up'), + 'realizedPnl': self.safe_number(position, 'r'), + 'percentage': None, + 'contracts': None, + 'contractSize': None, + 'markPrice': self.safe_string(position, 'mp'), + 'side': self.safe_string_lower(position, 'S'), + 'hedged': None, + 'timestamp': None, + 'datetime': None, + 'maintenanceMargin': self.safe_string(position, 'mm'), + 'maintenanceMarginPercentage': None, + 'collateral': None, + 'initialMargin': self.omit_zero(self.safe_string(position, 'm')), + 'initialMarginPercentage': None, + 'leverage': self.safe_string(position, 'v'), + 'marginRatio': None, + }) + + async def authenticate(self, params={}): + client = self.client(self.get_user_stream_url()) + messageHash = 'authenticated' + future = client.reusableFuture(messageHash) + authenticated = self.safe_value(client.subscriptions, messageHash) + if authenticated is None: + self.check_required_credentials() + time = self.milliseconds() + lastAuthenticatedTime = self.safe_integer(self.options['ws'], 'lastAuthenticatedTime', 0) + listenKeyRefreshRate = self.safe_integer(self.options['ws'], 'listenKeyRefreshRate', 1200000) + delay = self.sum(listenKeyRefreshRate, 10000) + if time - lastAuthenticatedTime > delay: + try: + client.subscriptions[messageHash] = True + response = await self.privatePostApiV1UserDataStream(params) + self.options['ws']['listenKey'] = self.safe_string(response, 'listenKey') + self.options['ws']['lastAuthenticatedTime'] = time + future.resolve(True) + self.delay(listenKeyRefreshRate, self.keep_alive_listen_key, params) + except Exception as e: + err = AuthenticationError(self.id + ' ' + self.json(e)) + client.reject(err, messageHash) + if messageHash in client.subscriptions: + del client.subscriptions[messageHash] + return await future + + async def keep_alive_listen_key(self, params={}): + options = self.safe_value(self.options, 'ws', {}) + listenKey = self.safe_string(options, 'listenKey') + if listenKey is None: + # A network error happened: we can't renew a listen key that does not exist. + return + try: + response = await self.privatePostApiV1UserDataStream(params) + self.options['ws']['listenKey'] = self.safe_string(response, 'listenKey') + self.options['ws']['lastAuthenticatedTime'] = self.milliseconds() + except Exception as error: + url = self.get_user_stream_url() + client = self.client(url) + messageHashes = list(client.futures.keys()) + for i in range(0, len(messageHashes)): + messageHash = messageHashes[i] + client.reject(error, messageHash) + self.options['ws']['listenKey'] = None + self.options['ws']['lastAuthenticatedTime'] = 0 + return + # whether or not to schedule another listenKey keepAlive request + listenKeyRefreshRate = self.safe_integer(self.options, 'listenKeyRefreshRate', 1200000) + self.delay(listenKeyRefreshRate, self.keep_alive_listen_key, params) + + def get_user_stream_url(self): + return self.urls['api']['ws']['common'] + '/api/v1/ws/' + self.options['ws']['listenKey'] + + def handle_error_message(self, client: Client, message) -> Bool: + # + # { + # "code": '-100010', + # "desc": "Invalid Symbols!" + # } + # + code = self.safe_string(message, 'code') + if code is not None: + desc = self.safe_string(message, 'desc') + msg = self.id + ' code: ' + code + ' message: ' + desc + exception = ExchangeError(msg) # c# fix + client.reject(exception) + return True + return False diff --git a/ccxt/pro/upbit.py b/ccxt/pro/upbit.py new file mode 100644 index 0000000..f228c53 --- /dev/null +++ b/ccxt/pro/upbit.py @@ -0,0 +1,679 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolById +from ccxt.base.types import Any, Balances, Int, Order, OrderBook, Str, Strings, Ticker, Tickers, Trade +from ccxt.async_support.base.ws.client import Client +from typing import List +from ccxt.base.errors import NotSupported + + +class upbit(ccxt.async_support.upbit): + + def describe(self) -> Any: + return self.deep_extend(super(upbit, self).describe(), { + 'has': { + 'ws': True, + 'watchOrderBook': True, + 'watchTicker': True, + 'watchTickers': True, + 'watchTrades': True, + 'watchTradesForSymbols': True, + 'watchOHLCV': True, + 'watchOrders': True, + 'watchMyTrades': True, + 'watchBalance': True, + }, + 'urls': { + 'api': { + 'ws': 'wss://{hostname}/websocket/v1', + }, + }, + 'options': { + 'tradesLimit': 1000, + }, + }) + + async def watch_public(self, symbol: str, channel, params={}): + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + marketId = market['id'] + url = self.implode_params(self.urls['api']['ws'], { + 'hostname': self.hostname, + }) + self.options[channel] = self.safe_value(self.options, channel, {}) + self.options[channel][symbol] = True + symbols = list(self.options[channel].keys()) + marketIds = self.market_ids(symbols) + request = [ + { + 'ticket': self.uuid(), + }, + { + 'type': channel, + 'codes': marketIds, + # 'isOnlySnapshot': False, + # 'isOnlyRealtime': False, + }, + ] + messageHash = channel + ':' + marketId + return await self.watch(url, messageHash, request, messageHash) + + async def watch_public_multiple(self, symbols: Strings, channel, params={}): + await self.load_markets() + if symbols is None: + symbols = self.symbols + symbols = self.market_symbols(symbols) + marketIds = self.market_ids(symbols) + url = self.implode_params(self.urls['api']['ws'], { + 'hostname': self.hostname, + }) + messageHashes = [] + for i in range(0, len(marketIds)): + messageHashes.append(channel + ':' + marketIds[i]) + request = [ + { + 'ticket': self.uuid(), + }, + { + 'type': channel, + 'codes': marketIds, + # 'isOnlySnapshot': False, + # 'isOnlyRealtime': False, + }, + ] + return await self.watch_multiple(url, messageHashes, request, messageHashes) + + async def watch_ticker(self, symbol: str, params={}) -> Ticker: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://global-docs.upbit.com/reference/websocket-ticker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + return await self.watch_public(symbol, 'ticker') + + async def watch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list + + https://global-docs.upbit.com/reference/websocket-ticker + + :param str[] symbols: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + newTickers = await self.watch_public_multiple(symbols, 'ticker') + if self.newUpdates: + tickers: dict = {} + tickers[newTickers['symbol']] = newTickers + return tickers + return self.filter_by_array(self.tickers, 'symbol', symbols) + + async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://global-docs.upbit.com/reference/websocket-trade + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + return await self.watch_trades_for_symbols([symbol], since, limit, params) + + async def watch_trades_for_symbols(self, symbols: List[str], since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a list of symbols + + https://global-docs.upbit.com/reference/websocket-trade + + :param str[] symbols: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, False, True, True) + channel = 'trade' + messageHashes = [] + url = self.implode_params(self.urls['api']['ws'], { + 'hostname': self.hostname, + }) + if symbols is not None: + for i in range(0, len(symbols)): + market = self.market(symbols[i]) + marketId = market['id'] + symbol = market['symbol'] + self.options[channel] = self.safe_value(self.options, channel, {}) + self.options[channel][symbol] = True + messageHashes.append(channel + ':' + marketId) + optionSymbols = list(self.options[channel].keys()) + marketIds = self.market_ids(optionSymbols) + request = [ + { + 'ticket': self.uuid(), + }, + { + 'type': channel, + 'codes': marketIds, + # 'isOnlySnapshot': False, + # 'isOnlyRealtime': False, + }, + ] + trades = await self.watch_multiple(url, messageHashes, request, messageHashes) + if self.newUpdates: + first = self.safe_value(trades, 0) + tradeSymbol = self.safe_string(first, 'symbol') + limit = trades.getLimit(tradeSymbol, limit) + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://global-docs.upbit.com/reference/websocket-orderbook + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + orderbook = await self.watch_public(symbol, 'orderbook') + return orderbook.limit() + + async def watch_ohlcv(self, symbol: str, timeframe: str = '1s', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + watches information an OHLCV with timestamp, openingPrice, highPrice, lowPrice, tradePrice, baseVolume in 1s. + + https://docs.upbit.com/kr/reference/websocket-candle for Upbit KR + https://global-docs.upbit.com/reference/websocket-candle for Upbit Global + + :param str symbol: unified market symbol of the market orders were made in + :param str timeframe: specifies the OHLCV candle interval to watch. As of now, Upbit only supports 1s candles. + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns OHLCV[]: a list of `OHLCV structures ` + """ + if timeframe != '1s': + raise NotSupported(self.id + ' watchOHLCV does not support' + timeframe + ' candle.') + timeFrameOHLCV = 'candle.' + timeframe + return await self.watch_public(symbol, timeFrameOHLCV) + + def handle_ticker(self, client: Client, message): + # 2020-03-17T23:07:36.511Z "onMessage" + # {type: "ticker", + # "code": "BTC-ETH", + # "opening_price": 0.02295092, + # "high_price": 0.02295092, + # "low_price": 0.02161249, + # "trade_price": 0.02161249, + # "prev_closing_price": 0.02185802, + # "acc_trade_price": 2.32732482, + # "change": "FALL", + # "change_price": 0.00024553, + # "signed_change_price": -0.00024553, + # "change_rate": 0.0112329479, + # "signed_change_rate": -0.0112329479, + # "ask_bid": "ASK", + # "trade_volume": 2.12, + # "acc_trade_volume": 106.11798418, + # "trade_date": "20200317", + # "trade_time": "215843", + # "trade_timestamp": 1584482323000, + # "acc_ask_volume": 90.16935908, + # "acc_bid_volume": 15.9486251, + # "highest_52_week_price": 0.03537414, + # "highest_52_week_date": "2019-04-08", + # "lowest_52_week_price": 0.01614901, + # "lowest_52_week_date": "2019-09-06", + # "trade_status": null, + # "market_state": "ACTIVE", + # "market_state_for_ios": null, + # "is_trading_suspended": False, + # "delisting_date": null, + # "market_warning": "NONE", + # "timestamp": 1584482323378, + # "acc_trade_price_24h": 2.5955306323568927, + # "acc_trade_volume_24h": 118.38798416, + # "stream_type": "SNAPSHOT"} + marketId = self.safe_string(message, 'code') + messageHash = 'ticker:' + marketId + ticker = self.parse_ticker(message) + symbol = ticker['symbol'] + self.tickers[symbol] = ticker + client.resolve(ticker, messageHash) + + def handle_order_book(self, client: Client, message): + # {type: "orderbook", + # "code": "BTC-ETH", + # "timestamp": 1584486737444, + # "total_ask_size": 16.76384456, + # "total_bid_size": 168.9020623, + # "orderbook_units": + # [{ask_price: 0.02295077, + # "bid_price": 0.02161249, + # "ask_size": 3.57100696, + # "bid_size": 22.5303265}, + # {ask_price: 0.02295078, + # "bid_price": 0.02152658, + # "ask_size": 0.52451651, + # "bid_size": 2.30355128}, + # {ask_price: 0.02295086, + # "bid_price": 0.02150802, + # "ask_size": 1.585, + # "bid_size": 5}, ...], + # "stream_type": "SNAPSHOT"} + marketId = self.safe_string(message, 'code') + symbol = self.safe_symbol(marketId, None, '-') + type = self.safe_string(message, 'stream_type') + options = self.safe_value(self.options, 'watchOrderBook', {}) + limit = self.safe_integer(options, 'limit', 15) + if type == 'SNAPSHOT': + self.orderbooks[symbol] = self.order_book({}, limit) + orderbook = self.orderbooks[symbol] + # upbit always returns a snapshot of 15 topmost entries + # the "REALTIME" deltas are not incremental + # therefore we reset the orderbook on each update + # and reinitialize it again with new bidasks + orderbook.reset({}) + orderbook['symbol'] = symbol + bids = orderbook['bids'] + asks = orderbook['asks'] + data = self.safe_value(message, 'orderbook_units', []) + for i in range(0, len(data)): + entry = data[i] + ask_price = self.safe_float(entry, 'ask_price') + ask_size = self.safe_float(entry, 'ask_size') + bid_price = self.safe_float(entry, 'bid_price') + bid_size = self.safe_float(entry, 'bid_size') + asks.store(ask_price, ask_size) + bids.store(bid_price, bid_size) + timestamp = self.safe_integer(message, 'timestamp') + datetime = self.iso8601(timestamp) + orderbook['timestamp'] = timestamp + orderbook['datetime'] = datetime + messageHash = 'orderbook:' + marketId + client.resolve(orderbook, messageHash) + + def handle_trades(self, client: Client, message): + # {type: "trade", + # "code": "KRW-BTC", + # "timestamp": 1584508285812, + # "trade_date": "2020-03-18", + # "trade_time": "05:11:25", + # "trade_timestamp": 1584508285000, + # "trade_price": 6747000, + # "trade_volume": 0.06499468, + # "ask_bid": "ASK", + # "prev_closing_price": 6774000, + # "change": "FALL", + # "change_price": 27000, + # "sequential_id": 1584508285000002, + # "stream_type": "REALTIME"} + trade = self.parse_trade(message) + symbol = trade['symbol'] + stored = self.safe_value(self.trades, symbol) + if stored is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + stored = ArrayCache(limit) + self.trades[symbol] = stored + stored.append(trade) + marketId = self.safe_string(message, 'code') + messageHash = 'trade:' + marketId + client.resolve(stored, messageHash) + + def handle_ohlcv(self, client: Client, message): + # { + # type: 'candle.1s', + # code: 'KRW-USDT', + # candle_date_time_utc: '2025-04-22T09:50:34', + # candle_date_time_kst: '2025-04-22T18:50:34', + # opening_price: 1438, + # high_price: 1438, + # low_price: 1438, + # trade_price: 1438, + # candle_acc_trade_volume: 1145.8935, + # candle_acc_trade_price: 1647794.853, + # timestamp: 1745315434125, + # stream_type: 'REALTIME' + # } + marketId = self.safe_string(message, 'code') + messageHash = 'candle.1s:' + marketId + ohlcv = self.parse_ohlcv(message) + client.resolve(ohlcv, messageHash) + + async def authenticate(self, params={}): + self.check_required_credentials() + wsOptions: dict = self.safe_dict(self.options, 'ws', {}) + authenticated = self.safe_string(wsOptions, 'token') + if authenticated is None: + auth: dict = { + 'access_key': self.apiKey, + 'nonce': self.uuid(), + } + token = self.jwt(auth, self.encode(self.secret), 'sha256', False) + wsOptions['token'] = token + wsOptions['options'] = { + 'headers': { + 'authorization': 'Bearer ' + token, + }, + } + self.options['ws'] = wsOptions + url = self.urls['api']['ws'] + '/private' + client = self.client(url) + return client + + async def watch_private(self, symbol, channel, messageHash, params={}): + await self.authenticate() + request = { + 'type': channel, + } + if symbol is not None: + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + symbols = [symbol] + marketIds = self.market_ids(symbols) + request['codes'] = marketIds + messageHash = messageHash + ':' + symbol + url = self.implode_params(self.urls['api']['ws'], { + 'hostname': self.hostname, + }) + url += '/private' + client = self.client(url) + # Track private channel subscriptions to support multiple concurrent watches + subscriptionsKey = 'upbitPrivateSubscriptions' + if not (subscriptionsKey in client.subscriptions): + client.subscriptions[subscriptionsKey] = {} + channelKey = channel + if symbol is not None: + channelKey = channel + ':' + symbol + subscriptions = client.subscriptions[subscriptionsKey] + isNewChannel = not (channelKey in subscriptions) + if isNewChannel: + subscriptions[channelKey] = request + # Build subscription message with all requested private channels + # Format: [{'ticket': uuid}, {'type': 'myOrder'}, {'type': 'myAsset'}, ...] + requests = [] + channelKeys = list(subscriptions.keys()) + for i in range(0, len(channelKeys)): + requests.append(subscriptions[channelKeys[i]]) + message = [ + { + 'ticket': self.uuid(), + }, + ] + for i in range(0, len(requests)): + message.append(requests[i]) + return await self.watch(url, messageHash, message, messageHash) + + async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + watches information on multiple orders made by the user + + https://global-docs.upbit.com/reference/websocket-myorder + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + channel = 'myOrder' + messageHash = 'myOrder' + orders = await self.watch_private(symbol, channel, messageHash) + if self.newUpdates: + limit = orders.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(orders, symbol, since, limit, True) + + async def watch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + watches information on multiple trades made by the user + + https://global-docs.upbit.com/reference/websocket-myorder + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + channel = 'myOrder' + messageHash = 'myTrades' + trades = await self.watch_private(symbol, channel, messageHash) + if self.newUpdates: + limit = trades.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(trades, symbol, since, limit, True) + + def parse_ws_order_status(self, status: Str): + statuses: dict = { + 'wait': 'open', + 'done': 'closed', + 'cancel': 'canceled', + 'watch': 'open', # not sure what self status means + 'trade': 'open', + } + return self.safe_string(statuses, status, status) + + def parse_ws_order(self, order, market=None): + # + # { + # "type": "myOrder", + # "code": "SGD-XRP", + # "uuid": "ac2dc2a3-fce9-40a2-a4f6-5987c25c438f", + # "ask_bid": "BID", + # "order_type": "limit", + # "state": "trade", + # "price": 0.001453, + # "avg_price": 0.00145372, + # "volume": 30925891.29839369, + # "remaining_volume": 29968038.09235948, + # "executed_volume": 30925891.29839369, + # "trades_count": 1, + # "reserved_fee": 44.23943970238218, + # "remaining_fee": 21.77177967409916, + # "paid_fee": 22.467660028283017, + # "locked": 43565.33112787242, + # "executed_funds": 44935.32005656603, + # "order_timestamp": 1710751590000, + # "timestamp": 1710751597500, + # "stream_type": "REALTIME" + # } + # + id = self.safe_string(order, 'uuid') + side = self.safe_string_lower(order, 'ask_bid') + if side == 'bid': + side = 'buy' + else: + side = 'sell' + timestamp = self.parse8601(self.safe_string(order, 'order_timestamp')) + status = self.parse_ws_order_status(self.safe_string(order, 'state')) + marketId = self.safe_string(order, 'code') + market = self.safe_market(marketId, market) + fee = None + feeCost = self.safe_string(order, 'paid_fee') + if feeCost is not None: + fee = { + 'currency': market['quote'], + 'cost': feeCost, + } + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': self.safe_string(order, 'trade_timestamp'), + 'symbol': market['symbol'], + 'type': self.safe_string(order, 'order_type'), + 'timeInForce': self.safe_string(order, 'time_in_force'), + 'postOnly': None, + 'side': side, + 'price': self.safe_string(order, 'price'), + 'stopPrice': None, + 'triggerPrice': None, + 'cost': self.safe_string(order, 'executed_funds'), + 'average': self.safe_string(order, 'avg_price'), + 'amount': self.safe_string(order, 'volume'), + 'filled': self.safe_string(order, 'executed_volume'), + 'remaining': self.safe_string(order, 'remaining_volume'), + 'status': status, + 'fee': fee, + 'trades': None, + }) + + def parse_ws_trade(self, trade, market=None): + # see: parseWsOrder + side = self.safe_string_lower(trade, 'ask_bid') + if side == 'bid': + side = 'buy' + else: + side = 'sell' + timestamp = self.parse8601(self.safe_string(trade, 'trade_timestamp')) + marketId = self.safe_string(trade, 'code') + market = self.safe_market(marketId, market) + fee = None + feeCost = self.safe_string(trade, 'paid_fee') + if feeCost is not None: + fee = { + 'currency': market['quote'], + 'cost': feeCost, + } + return self.safe_trade({ + 'id': self.safe_string(trade, 'trade_uuid'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'side': side, + 'price': self.safe_string(trade, 'price'), + 'amount': self.safe_string(trade, 'volume'), + 'cost': self.safe_string(trade, 'executed_funds'), + 'order': self.safe_string(trade, 'uuid'), + 'takerOrMaker': None, + 'type': self.safe_string(trade, 'order_type'), + 'fee': fee, + 'info': trade, + }, market) + + def handle_my_order(self, client: Client, message): + # see: parseWsOrder + tradeId = self.safe_string(message, 'trade_uuid') + if tradeId is not None: + self.handle_my_trade(client, message) + self.handle_order(client, message) + + def handle_my_trade(self, client: Client, message): + # see: parseWsOrder + myTrades = self.myTrades + if myTrades is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + myTrades = ArrayCacheBySymbolById(limit) + trade = self.parse_ws_trade(message) + myTrades.append(trade) + messageHash = 'myTrades' + client.resolve(myTrades, messageHash) + messageHash = 'myTrades:' + trade['symbol'] + client.resolve(myTrades, messageHash) + + def handle_order(self, client: Client, message): + parsed = self.parse_ws_order(message) + symbol = self.safe_string(parsed, 'symbol') + orderId = self.safe_string(parsed, 'id') + if self.orders is None: + limit = self.safe_integer(self.options, 'ordersLimit', 1000) + self.orders = ArrayCacheBySymbolById(limit) + cachedOrders = self.orders + orders = self.safe_value(cachedOrders.hashmap, symbol, {}) + order = self.safe_value(orders, orderId) + if order is not None: + fee = self.safe_value(order, 'fee') + if fee is not None: + parsed['fee'] = fee + fees = self.safe_value(order, 'fees') + if fees is not None: + parsed['fees'] = fees + parsed['trades'] = self.safe_value(order, 'trades') + parsed['timestamp'] = self.safe_integer(order, 'timestamp') + parsed['datetime'] = self.safe_string(order, 'datetime') + cachedOrders.append(parsed) + messageHash = 'myOrder' + client.resolve(self.orders, messageHash) + messageHash = messageHash + ':' + symbol + client.resolve(self.orders, messageHash) + + async def watch_balance(self, params={}) -> Balances: + """ + + https://global-docs.upbit.com/reference/websocket-myasset + + query for balance and get the amount of funds available for trading or funds locked in orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + channel = 'myAsset' + messageHash = 'myAsset' + return await self.watch_private(None, channel, messageHash) + + def handle_balance(self, client: Client, message): + # + # { + # "type": "myAsset", + # "asset_uuid": "e635f223-1609-4969-8fb6-4376937baad6", + # "assets": [ + # { + # "currency": "SGD", + # "balance": 1386929.37231066771348207123, + # "locked": 10329.670127489597585685 + # } + # ], + # "asset_timestamp": 1710146517259, + # "timestamp": 1710146517267, + # "stream_type": "REALTIME" + # } + # + data = self.safe_list(message, 'assets', []) + timestamp = self.safe_integer(message, 'timestamp') + self.balance['timestamp'] = timestamp + self.balance['datetime'] = self.iso8601(timestamp) + for i in range(0, len(data)): + balance = data[i] + currencyId = self.safe_string(balance, 'currency') + code = self.safe_currency_code(currencyId) + available = self.safe_string(balance, 'balance') + frozen = self.safe_string(balance, 'locked') + account = self.account() + account['free'] = available + account['used'] = frozen + self.balance[code] = account + self.balance = self.safe_balance(self.balance) + messageHash = self.safe_string(message, 'type') + client.resolve(self.balance, messageHash) + + def handle_message(self, client: Client, message): + methods: dict = { + 'ticker': self.handle_ticker, + 'orderbook': self.handle_order_book, + 'trade': self.handle_trades, + 'myOrder': self.handle_my_order, + 'myAsset': self.handle_balance, + 'candle.1s': self.handle_ohlcv, + } + methodName = self.safe_string(message, 'type') + method = self.safe_value(methods, methodName) + if method is not None: + method(client, message) diff --git a/ccxt/pro/whitebit.py b/ccxt/pro/whitebit.py new file mode 100644 index 0000000..2348422 --- /dev/null +++ b/ccxt/pro/whitebit.py @@ -0,0 +1,917 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolById, ArrayCacheByTimestamp +from ccxt.base.types import Any, Balances, Bool, Int, Order, OrderBook, Str, Strings, Ticker, Tickers, Trade +from ccxt.async_support.base.ws.client import Client +from typing import List +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.precise import Precise + + +class whitebit(ccxt.async_support.whitebit): + + def describe(self) -> Any: + return self.deep_extend(super(whitebit, self).describe(), { + 'has': { + 'ws': True, + 'watchBalance': True, + 'watchMyTrades': True, + 'watchOHLCV': True, + 'watchOrderBook': True, + 'watchOrders': True, + 'watchTicker': True, + 'watchTickers': True, + 'watchTrades': True, + 'watchTradesForSymbols': False, + }, + 'urls': { + 'api': { + 'ws': 'wss://api.whitebit.com/ws', + }, + }, + 'options': { + 'timeframes': { + '1m': '60', + '5m': '300', + '15m': '900', + '30m': '1800', + '1h': '3600', + '4h': '14400', + '8h': '28800', + '1d': '86400', + '1w': '604800', + }, + 'watchOrderBook': { + 'priceInterval': 0, # "0" - no interval, available values - "0.00000001", "0.0000001", "0.000001", "0.00001", "0.0001", "0.001", "0.01", "0.1" + }, + }, + 'streaming': { + 'ping': self.ping, + }, + 'exceptions': { + 'ws': { + 'exact': { + '1': BadRequest, # {error: {code: 1, message: 'invalid argument'}, result: null, id: 1656404342} + '2': BadRequest, # {error: {code: 2, message: 'internal error'}, result: null, id: 1656404075} + '4': BadRequest, # {error: {code: 4, message: 'method not found'}, result: null, id: 1656404250} + '6': AuthenticationError, # {error: {code: 6, message: 'require authentication'}, result: null, id: 1656404076} + }, + }, + }, + }) + + async def watch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + watches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://docs.whitebit.com/public/websocket/#kline + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + timeframes = self.safe_value(self.options, 'timeframes', {}) + interval = self.safe_integer(timeframes, timeframe) + marketId = market['id'] + # currently there is no way of knowing + # the interval upon getting an update + # so that can't be part of the message hash, and the user can only subscribe + # to one timeframe per symbol + messageHash = 'candles:' + symbol + reqParams = [marketId, interval] + method = 'candles_subscribe' + ohlcv = await self.watch_public(messageHash, method, reqParams, params) + if self.newUpdates: + limit = ohlcv.getLimit(symbol, limit) + return self.filter_by_since_limit(ohlcv, since, limit, 0, True) + + def handle_ohlcv(self, client: Client, message): + # + # { + # "method": "candles_update", + # "params": [ + # [ + # 1655204760, + # "22374.15", + # "22351.34", + # "22374.27", + # "22342.52", + # "30.213426", + # "675499.29718947", + # "BTC_USDT" + # ] + # ], + # "id": null + # } + # + params = self.safe_value(message, 'params', []) + for i in range(0, len(params)): + data = params[i] + marketId = self.safe_string(data, 7) + market = self.safe_market(marketId) + symbol = market['symbol'] + messageHash = 'candles' + ':' + symbol + parsed = self.parse_ohlcv(data, market) + # self.ohlcvs[symbol] = self.safe_value(self.ohlcvs, symbol) + if not (symbol in self.ohlcvs): + self.ohlcvs[symbol] = {} + # stored = self.ohlcvs[symbol]['unknown'] # we don't know the timeframe but we need to respect the type + if not ('unknown' in self.ohlcvs[symbol]): + limit = self.safe_integer(self.options, 'OHLCVLimit', 1000) + stored = ArrayCacheByTimestamp(limit) + self.ohlcvs[symbol]['unknown'] = stored + ohlcv = self.ohlcvs[symbol]['unknown'] + ohlcv.append(parsed) + client.resolve(ohlcv, messageHash) + return message + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://docs.whitebit.com/public/websocket/#market-depth + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + if limit is None: + limit = 10 # max 100 + messageHash = 'orderbook' + ':' + market['symbol'] + method = 'depth_subscribe' + options = self.safe_value(self.options, 'watchOrderBook', {}) + defaultPriceInterval = self.safe_string(options, 'priceInterval', '0') + priceInterval = self.safe_string(params, 'priceInterval', defaultPriceInterval) + params = self.omit(params, 'priceInterval') + reqParams = [ + market['id'], + limit, + priceInterval, + True, # True for allowing multiple subscriptions + ] + orderbook = await self.watch_public(messageHash, method, reqParams, params) + return orderbook.limit() + + def handle_order_book(self, client: Client, message): + # + # { + # "method":"depth_update", + # "params":[ + # True, + # { + # "timestamp": 1708679568.940867, + # "asks":[ + # ["21252.45","0.01957"], + # ["21252.55","0.126205"], + # ["21252.66","0.222689"], + # ["21252.76","0.185358"], + # ["21252.87","0.210077"], + # ["21252.98","0.303991"], + # ["21253.08","0.327909"], + # ["21253.19","0.399007"], + # ["21253.3","0.427695"], + # ["21253.4","0.492901"] + # ], + # "bids":[ + # ["21248.82","0.22"], + # ["21248.73","0.000467"], + # ["21248.62","0.100864"], + # ["21248.51","0.061436"], + # ["21248.42","0.091"], + # ["21248.41","0.126839"], + # ["21248.3","0.063511"], + # ["21248.2","0.110547"], + # ["21248","0.25257"], + # ["21247.7","1.71813"] + # ] + # }, + # "BTC_USDT" + # ], + # "id":null + # } + # + params = self.safe_value(message, 'params', []) + isSnapshot = self.safe_value(params, 0) + marketId = self.safe_string(params, 2) + market = self.safe_market(marketId) + symbol = market['symbol'] + data = self.safe_value(params, 1) + timestamp = self.safe_timestamp(data, 'timestamp') + if not (symbol in self.orderbooks): + ob = self.order_book() + self.orderbooks[symbol] = ob + orderbook = self.orderbooks[symbol] + orderbook['timestamp'] = timestamp + orderbook['datetime'] = self.iso8601(timestamp) + if isSnapshot: + snapshot = self.parse_order_book(data, symbol) + orderbook.reset(snapshot) + else: + asks = self.safe_value(data, 'asks', []) + bids = self.safe_value(data, 'bids', []) + self.handle_deltas(orderbook['asks'], asks) + self.handle_deltas(orderbook['bids'], bids) + messageHash = 'orderbook' + ':' + symbol + client.resolve(orderbook, messageHash) + + def handle_delta(self, bookside, delta): + price = self.safe_float(delta, 0) + amount = self.safe_float(delta, 1) + bookside.store(price, amount) + + def handle_deltas(self, bookside, deltas): + for i in range(0, len(deltas)): + self.handle_delta(bookside, deltas[i]) + + async def watch_ticker(self, symbol: str, params={}) -> Ticker: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://docs.whitebit.com/public/websocket/#market-statistics + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + method = 'market_subscribe' + messageHash = 'ticker:' + symbol + # every time we want to subscribe to another market we have to "re-subscribe" sending it all again + return await self.watch_multiple_subscription(messageHash, method, symbol, False, params) + + async def watch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list + + https://docs.whitebit.com/public/websocket/#market-statistics + + :param str[] [symbols]: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols, None, False) + method = 'market_subscribe' + url = self.urls['api']['ws'] + id = self.nonce() + messageHashes = [] + args = [] + for i in range(0, len(symbols)): + market = self.market(symbols[i]) + messageHashes.append('ticker:' + market['symbol']) + args.append(market['id']) + request: dict = { + 'id': id, + 'method': method, + 'params': args, + } + await self.watch_multiple(url, messageHashes, self.extend(request, params), messageHashes) + return self.filter_by_array(self.tickers, 'symbol', symbols) + + def handle_ticker(self, client: Client, message): + # + # { + # "method": "market_update", + # "params": [ + # "BTC_USDT", + # { + # "close": "22293.86", + # "deal": "1986990019.96552952", + # "high": "24360.7", + # "last": "22293.86", + # "low": "20851.44", + # "open": "24076.12", + # "period": 86400, + # "volume": "87016.995668" + # } + # ], + # "id": null + # } + # + tickers = self.safe_value(message, 'params', []) + marketId = self.safe_string(tickers, 0) + market = self.safe_market(marketId, None) + symbol = market['symbol'] + rawTicker = self.safe_value(tickers, 1, {}) + messageHash = 'ticker' + ':' + symbol + ticker = self.parse_ticker(rawTicker, market) + self.tickers[symbol] = ticker + # watchTicker + client.resolve(ticker, messageHash) + # watchTickers + messageHashes = list(client.futures.keys()) + for i in range(0, len(messageHashes)): + currentMessageHash = messageHashes[i] + if currentMessageHash.find('tickers') >= 0 and currentMessageHash.find(symbol) >= 0: + # Example: user calls watchTickers with ['LTC/USDT', 'ETH/USDT'] + # the associated messagehash will be: 'tickers:LTC/USDT:ETH/USDT' + # since we only have access to a single symbol at a time + # we have to do a reverse lookup into the tickers hashes + # and check if the current symbol is a part of one or more + # tickers hashes and resolve them + # user might have multiple watchTickers promises + # watchTickers( ['LTC/USDT', 'ETH/USDT'] ), watchTickers( ['ETC/USDT', 'DOGE/USDT'] ) + # and we want to make sure we resolve only the correct ones + client.resolve(ticker, currentMessageHash) + return message + + async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://docs.whitebit.com/public/websocket/#market-trades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + messageHash = 'trades' + ':' + symbol + method = 'trades_subscribe' + # every time we want to subscribe to another market we have to 're-subscribe' sending it all again + trades = await self.watch_multiple_subscription(messageHash, method, symbol, False, params) + if self.newUpdates: + limit = trades.getLimit(symbol, limit) + return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) + + def handle_trades(self, client: Client, message): + # + # { + # "method":"trades_update", + # "params":[ + # "BTC_USDT", + # [ + # { + # "id":1900632398, + # "time":1656320231.404343, + # "price":"21443.04", + # "amount":"0.072844", + # "type":"buy" + # }, + # { + # "id":1900632397, + # "time":1656320231.400001, + # "price":"21443.15", + # "amount":"0.060757", + # "type":"buy" + # } + # ] + # ] + # } + # + params = self.safe_value(message, 'params', []) + marketId = self.safe_string(params, 0) + market = self.safe_market(marketId) + symbol = market['symbol'] + stored = self.safe_value(self.trades, symbol) + if stored is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + stored = ArrayCache(limit) + self.trades[symbol] = stored + data = self.safe_value(params, 1, []) + parsedTrades = self.parse_trades(data, market) + for j in range(0, len(parsedTrades)): + stored.append(parsedTrades[j]) + messageHash = 'trades:' + market['symbol'] + client.resolve(stored, messageHash) + + async def watch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + watches trades made by the user + + https://docs.whitebit.com/private/websocket/#deals + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' watchMyTrades() requires a symbol argument') + await self.load_markets() + await self.authenticate() + market = self.market(symbol) + symbol = market['symbol'] + messageHash = 'myTrades:' + symbol + method = 'deals_subscribe' + trades = await self.watch_multiple_subscription(messageHash, method, symbol, True, params) + if self.newUpdates: + limit = trades.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(trades, symbol, since, limit, True) + + def handle_my_trades(self, client: Client, message, subscription=None): + # + # { + # "method": "deals_update", + # "params": [ + # 1894994106, + # 1656151427.729706, + # "LTC_USDT", + # 96624037337, + # "56.78", + # "0.16717", + # "0.0094919126", + # '' + # ], + # "id": null + # } + # + trade = self.safe_value(message, 'params') + if self.myTrades is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + self.myTrades = ArrayCache(limit) + stored = self.myTrades + parsed = self.parse_ws_trade(trade) + stored.append(parsed) + symbol = parsed['symbol'] + messageHash = 'myTrades:' + symbol + client.resolve(stored, messageHash) + + def parse_ws_trade(self, trade, market=None): + # + # [ + # 1894994106, # id + # 1656151427.729706, # deal time + # "LTC_USDT", # symbol + # 96624037337, # order id + # "56.78", # price + # "0.16717", # amount + # "0.0094919126", # fee + # '' # client order id + # ] + # + orderId = self.safe_string(trade, 3) + timestamp = self.safe_timestamp(trade, 1) + id = self.safe_string(trade, 0) + price = self.safe_string(trade, 4) + amount = self.safe_string(trade, 5) + marketId = self.safe_string(trade, 2) + market = self.safe_market(marketId, market) + fee = None + feeCost = self.safe_string(trade, 6) + if feeCost is not None: + fee = { + 'cost': feeCost, + 'currency': market['quote'], + } + return self.safe_trade({ + 'id': id, + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'order': orderId, + 'type': None, + 'side': None, + 'takerOrMaker': None, + 'price': price, + 'amount': amount, + 'cost': None, + 'fee': fee, + }, market) + + async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + watches information on multiple orders made by the user + + https://docs.whitebit.com/private/websocket/#orders-pending + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' watchOrders() requires a symbol argument') + await self.load_markets() + await self.authenticate() + market = self.market(symbol) + symbol = market['symbol'] + messageHash = 'orders:' + symbol + method = 'ordersPending_subscribe' + trades = await self.watch_multiple_subscription(messageHash, method, symbol, False, params) + if self.newUpdates: + limit = trades.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(trades, symbol, since, limit, True) + + def handle_order(self, client: Client, message, subscription=None): + # + # { + # "method": "ordersPending_update", + # "params": [ + # 1, # 1 = new, 2 = update 3 = cancel or execute + # { + # "id": 96433622651, + # "market": "LTC_USDT", + # "type": 1, + # "side": 2, + # "ctime": 1656092215.39375, + # "mtime": 1656092215.39375, + # "price": "25", + # "amount": "0.202", + # "taker_fee": "0.001", + # "maker_fee": "0.001", + # "left": "0.202", + # "deal_stock": "0", + # "deal_money": "0", + # "deal_fee": "0", + # "client_order_id": '' + # } + # ] + # "id": null + # } + # + params = self.safe_value(message, 'params', []) + data = self.safe_value(params, 1) + if self.orders is None: + limit = self.safe_integer(self.options, 'ordersLimit', 1000) + self.orders = ArrayCacheBySymbolById(limit) + stored = self.orders + status = self.safe_integer(params, 0) + parsed = self.parse_ws_order(self.extend(data, {'status': status})) + stored.append(parsed) + symbol = parsed['symbol'] + messageHash = 'orders:' + symbol + client.resolve(self.orders, messageHash) + + def parse_ws_order(self, order, market=None): + # + # { + # "id": 96433622651, + # "market": "LTC_USDT", + # "type": 1, + # "side": 2, #1- sell 2-buy + # "ctime": 1656092215.39375, + # "mtime": 1656092215.39375, + # "price": "25", + # "amount": "0.202", + # "taker_fee": "0.001", + # "maker_fee": "0.001", + # "left": "0.202", + # "deal_stock": "0", + # "deal_money": "0", + # "deal_fee": "0", + # "activation_price": "40", + # "activation_condition": "lte", + # "client_order_id": '' + # "status": 1, # 1 = new, 2 = update 3 = cancel or execute + # } + # + status = self.safe_integer(order, 'status') + marketId = self.safe_string(order, 'market') + market = self.safe_market(marketId, market) + id = self.safe_string(order, 'id') + clientOrderId = self.omit_zero(self.safe_string(order, 'client_order_id')) + price = self.safe_string(order, 'price') + filled = self.safe_string(order, 'deal_stock') + cost = self.safe_string(order, 'deal_money') + stopPrice = self.safe_string(order, 'activation_price') + rawType = self.safe_string(order, 'type') + type = self.parse_ws_order_type(rawType) + amount = None + remaining = None + if type == 'market': + amount = self.safe_string(order, 'deal_stock') + remaining = '0' + else: + remaining = self.safe_string(order, 'left') + amount = self.safe_string(order, 'amount') + timestamp = self.safe_timestamp(order, 'ctime') + lastTradeTimestamp = self.safe_timestamp(order, 'mtime') + symbol = market['symbol'] + rawSide = self.safe_integer(order, 'side') + side = 'sell' if (rawSide == 1) else 'buy' + dealFee = self.safe_string(order, 'deal_fee') + fee = None + if dealFee is not None: + fee = { + 'cost': self.parse_number(dealFee), + 'currency': market['quote'], + } + unifiedStatus = None + if (status == 1) or (status == 2): + unifiedStatus = 'open' + else: + if Precise.string_equals(remaining, '0'): + unifiedStatus = 'closed' + else: + unifiedStatus = 'canceled' + return self.safe_order({ + 'info': order, + 'symbol': symbol, + 'id': id, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': lastTradeTimestamp, + 'type': type, + 'timeInForce': None, + 'postOnly': None, + 'side': side, + 'price': price, + 'stopPrice': stopPrice, + 'triggerPrice': stopPrice, + 'amount': amount, + 'cost': cost, + 'average': None, + 'filled': filled, + 'remaining': remaining, + 'status': unifiedStatus, + 'fee': fee, + 'trades': None, + }, market) + + def parse_ws_order_type(self, status): + statuses: dict = { + '1': 'limit', + '2': 'market', + '202': 'market', + '3': 'limit', + '4': 'market', + '5': 'limit', + '6': 'market', + '8': 'limit', + '10': 'market', + } + return self.safe_string(statuses, status, status) + + async def watch_balance(self, params={}) -> Balances: + """ + watch balance and get the amount of funds available for trading or funds locked in orders + + https://docs.whitebit.com/private/websocket/#balance-spot + https://docs.whitebit.com/private/websocket/#balance-margin + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: spot or contract if not provided self.options['defaultType'] is used + :returns dict: a `balance structure ` + """ + await self.load_markets() + type = None + type, params = self.handle_market_type_and_params('watchBalance', None, params) + messageHash = 'wallet:' + method = None + if type == 'spot': + method = 'balanceSpot_subscribe' + messageHash += 'spot' + else: + method = 'balanceMargin_subscribe' + messageHash += 'margin' + currencies = list(self.currencies.keys()) + return await self.watch_private(messageHash, method, currencies, params) + + def handle_balance(self, client: Client, message): + # + # { + # "method":"balanceSpot_update", + # "params":[ + # { + # "LTC":{ + # "available":"0.16587", + # "freeze":"0" + # } + # } + # ], + # "id":null + # } + # + method = self.safe_string(message, 'method') + data = self.safe_value(message, 'params') + balanceDict = self.safe_value(data, 0) + self.balance['info'] = balanceDict + keys = list(balanceDict.keys()) + currencyId = self.safe_value(keys, 0) + rawBalance = self.safe_value(balanceDict, currencyId) + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(rawBalance, 'available') + account['used'] = self.safe_string(rawBalance, 'freeze') + self.balance[code] = account + self.balance = self.safe_balance(self.balance) + messageHash = 'wallet:' + if method.find('Spot') >= 0: + messageHash += 'spot' + else: + messageHash += 'margin' + client.resolve(self.balance, messageHash) + + async def watch_public(self, messageHash, method, reqParams=[], params={}): + url = self.urls['api']['ws'] + id = self.nonce() + request: dict = { + 'id': id, + 'method': method, + 'params': reqParams, + } + message = self.extend(request, params) + return await self.watch(url, messageHash, message, messageHash) + + async def watch_multiple_subscription(self, messageHash, method, symbol, isNested=False, params={}): + await self.load_markets() + url = self.urls['api']['ws'] + id = self.nonce() + client = self.safe_value(self.clients, url) + request = None + marketIds = [] + if client is None: + subscription: dict = {} + market = self.market(symbol) + marketId = market['id'] + subscription[marketId] = True + marketIds = [marketId] + if isNested: + marketIds = [marketIds] + request = { + 'id': id, + 'method': method, + 'params': marketIds, + } + message = self.extend(request, params) + return await self.watch(url, messageHash, message, method, subscription) + else: + subscription = self.safe_value(client.subscriptions, method, {}) + hasSymbolSubscription = True + market = self.market(symbol) + marketId = market['id'] + isSubscribed = self.safe_bool(subscription, marketId, False) + if not isSubscribed: + subscription[marketId] = True + hasSymbolSubscription = False + if hasSymbolSubscription: + # already subscribed to self market(s) + return await self.watch(url, messageHash, request, method, subscription) + else: + # resubscribe + marketIdsNew = [] + marketIdsNew = list(subscription.keys()) + if isNested: + marketIdsNew = [marketIdsNew] + resubRequest: dict = { + 'id': id, + 'method': method, + 'params': marketIdsNew, + } + if method in client.subscriptions: + del client.subscriptions[method] + return await self.watch(url, messageHash, resubRequest, method, subscription) + + async def watch_private(self, messageHash, method, reqParams=[], params={}): + self.check_required_credentials() + await self.authenticate() + url = self.urls['api']['ws'] + id = self.nonce() + request: dict = { + 'id': id, + 'method': method, + 'params': reqParams, + } + message = self.extend(request, params) + return await self.watch(url, messageHash, message, messageHash) + + async def authenticate(self, params={}): + self.check_required_credentials() + url = self.urls['api']['ws'] + messageHash = 'authenticated' + client = self.client(url) + future = client.reusableFuture('authenticated') + authenticated = self.safe_value(client.subscriptions, messageHash) + if authenticated is None: + authToken = await self.v4PrivatePostProfileWebsocketToken() + # + # { + # "websocket_token": "$2y$10$lxCvTXig/XrcTBFY1bdFseCKQmFTDtCpEzHNVnXowGplExFxPJp9y" + # } + # + token = self.safe_string(authToken, 'websocket_token') + id = self.nonce() + request: dict = { + 'id': id, + 'method': 'authorize', + 'params': [ + token, + 'public', + ], + } + subscription: dict = { + 'id': id, + 'method': self.handle_authenticate, + } + try: + await self.watch(url, messageHash, request, messageHash, subscription) + except Exception as e: + del client.subscriptions[messageHash] + future.reject(e) + return await future + + def handle_authenticate(self, client: Client, message): + # + # {error: null, result: {status: "success"}, id: 1656084550} + # + future = client.futures['authenticated'] + future.resolve(1) + return message + + def handle_error_message(self, client: Client, message) -> Bool: + # + # { + # "error": {code: 1, message: "invalid argument"}, + # "result": null, + # "id": 1656090882 + # } + # + error = self.safe_value(message, 'error') + try: + if error is not None: + code = self.safe_string(message, 'code') + feedback = self.id + ' ' + self.json(message) + self.throw_exactly_matched_exception(self.exceptions['ws']['exact'], code, feedback) + except Exception as e: + if isinstance(e, AuthenticationError): + client.reject(e, 'authenticated') + if 'authenticated' in client.subscriptions: + del client.subscriptions['authenticated'] + return False + return message + + def handle_message(self, client: Client, message): + # + # auth + # {error: null, result: {status: "success"}, id: 1656084550} + # + # pong + # {error: null, result: "pong", id: 0} + # + if not self.handle_error_message(client, message): + return + result = self.safe_string(message, 'result') + if result == 'pong': + self.handle_pong(client, message) + return + id = self.safe_integer(message, 'id') + if id is not None: + self.handle_subscription_status(client, message, id) + return + methods: dict = { + 'market_update': self.handle_ticker, + 'trades_update': self.handle_trades, + 'depth_update': self.handle_order_book, + 'candles_update': self.handle_ohlcv, + 'ordersPending_update': self.handle_order, + 'ordersExecuted_update': self.handle_order, + 'balanceSpot_update': self.handle_balance, + 'balanceMargin_update': self.handle_balance, + 'deals_update': self.handle_my_trades, + } + topic = self.safe_value(message, 'method') + method = self.safe_value(methods, topic) + if method is not None: + method(client, message) + + def handle_subscription_status(self, client: Client, message, id): + # not every method stores its subscription + # object so we can't do indeById here + subs = client.subscriptions + values = list(subs.values()) + for i in range(0, len(values)): + subscription = values[i] + if subscription is not True: + subId = self.safe_integer(subscription, 'id') + if (subId is not None) and (subId == id): + method = self.safe_value(subscription, 'method') + if method is not None: + method(client, message) + return + + def handle_pong(self, client: Client, message): + client.lastPong = self.milliseconds() + return message + + def ping(self, client: Client): + return { + 'id': 0, + 'method': 'ping', + 'params': [], + } diff --git a/ccxt/pro/woo.py b/ccxt/pro/woo.py new file mode 100644 index 0000000..3030818 --- /dev/null +++ b/ccxt/pro/woo.py @@ -0,0 +1,1470 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolById, ArrayCacheBySymbolBySide, ArrayCacheByTimestamp +import hashlib +from ccxt.base.types import Any, Balances, Bool, Int, Order, OrderBook, Position, Str, Strings, Ticker, Tickers, Trade +from ccxt.async_support.base.ws.client import Client +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import NotSupported +from ccxt.base.precise import Precise + + +class woo(ccxt.async_support.woo): + + def describe(self) -> Any: + return self.deep_extend(super(woo, self).describe(), { + 'has': { + 'ws': True, + 'watchBalance': True, + 'watchMyTrades': True, + 'watchOHLCV': True, + 'watchOrderBook': True, + 'watchOrders': True, + 'watchTicker': True, + 'watchTickers': True, + 'watchBidsAsks': True, + 'watchTrades': True, + 'watchTradesForSymbols': False, + 'watchPositions': True, + 'unWatchTicker': True, + 'unWatchTickers': True, + 'unWatchOrderBook': True, + 'unWatchOHLCV': True, + 'unWatchTrades': True, + }, + 'urls': { + 'api': { + 'ws': { + 'public': 'wss://wss.woox.io/ws/stream', + 'private': 'wss://wss.woox.io/v2/ws/private/stream', + }, + }, + 'test': { + 'ws': { + 'public': 'wss://wss.staging.woox.io/ws/stream', + 'private': 'wss://wss.staging.woox.io/v2/ws/private/stream', + }, + }, + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + 'uid': True, + }, + 'options': { + 'tradesLimit': 1000, + 'ordersLimit': 1000, + 'requestId': {}, + 'watchPositions': { + 'fetchPositionsSnapshot': True, # or False + 'awaitPositionsSnapshot': True, # whether to wait for the positions snapshot before providing updates + }, + }, + 'streaming': { + 'ping': self.ping, + 'keepAlive': 9000, + }, + 'exceptions': { + 'ws': { + 'exact': { + 'Auth is needed.': AuthenticationError, + }, + }, + }, + }) + + def request_id(self, url): + options = self.safe_value(self.options, 'requestId', {}) + previousValue = self.safe_integer(options, url, 0) + newValue = self.sum(previousValue, 1) + self.options['requestId'][url] = newValue + return newValue + + async def watch_public(self, messageHash, message): + urlUid = '/' + self.uid if (self.uid) else '' + url = self.urls['api']['ws']['public'] + urlUid + requestId = self.request_id(url) + subscribe: dict = { + 'id': requestId, + } + request = self.extend(subscribe, message) + return await self.watch(url, messageHash, request, messageHash, subscribe) + + async def unwatch_public(self, subHash: str, symbol: str, topic: str, params={}) -> Any: + urlUid = '/' + self.uid if (self.uid) else '' + url = self.urls['api']['ws']['public'] + urlUid + requestId = self.request_id(url) + unsubHash = 'unsubscribe::' + subHash + message: dict = { + 'id': requestId, + 'event': 'unsubscribe', + 'topic': subHash, + } + subscription: dict = { + 'id': str(requestId), + 'unsubscribe': True, + 'symbols': [symbol], + 'topic': topic, + 'subMessageHashes': [subHash], + 'unsubMessageHashes': [unsubHash], + } + symbolsAndTimeframes = self.safe_list(params, 'symbolsAndTimeframes') + if symbolsAndTimeframes is not None: + subscription['symbolsAndTimeframes'] = symbolsAndTimeframes + params = self.omit(params, 'symbolsAndTimeframes') + return await self.watch(url, unsubHash, self.extend(message, params), unsubHash, subscription) + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + + https://docs.woox.io/#orderbookupdate + https://docs.woox.io/#orderbook + + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return. + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.method]: either(default) 'orderbook' or 'orderbookupdate', default is 'orderbook' + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + method = None + method, params = self.handle_option_and_params(params, 'watchOrderBook', 'method', 'orderbook') + market = self.market(symbol) + topic = market['id'] + '@' + method + urlUid = '/' + self.uid if (self.uid) else '' + url = self.urls['api']['ws']['public'] + urlUid + requestId = self.request_id(url) + request: dict = { + 'event': 'subscribe', + 'topic': topic, + 'id': requestId, + } + subscription: dict = { + 'id': str(requestId), + 'name': method, + 'symbol': market['symbol'], + 'limit': limit, + 'params': params, + } + if method == 'orderbookupdate': + subscription['method'] = self.handle_order_book_subscription + orderbook = await self.watch(url, topic, self.extend(request, params), topic, subscription) + return orderbook.limit() + + async def un_watch_order_book(self, symbol: str, params={}) -> Any: + """ + unWatches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://docs.woox.io/#orderbookupdate + https://docs.woox.io/#orderbook + + :param str symbol: unified symbol of the market + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + method = None + method, params = self.handle_option_and_params(params, 'watchOrderBook', 'method', 'orderbook') + market = self.market(symbol) + subHash = market['id'] + '@' + method + topic = 'orderbook' + return await self.unwatch_public(subHash, market['symbol'], topic, params) + + def handle_order_book(self, client: Client, message): + # + # { + # "topic": "PERP_BTC_USDT@orderbookupdate", + # "ts": 1722500373999, + # "data": { + # "symbol": "PERP_BTC_USDT", + # "prevTs": 1722500373799, + # "bids": [ + # [ + # 0.30891, + # 2469.98 + # ] + # ], + # "asks": [ + # [ + # 0.31075, + # 2379.63 + # ] + # ] + # } + # } + # + data = self.safe_dict(message, 'data') + marketId = self.safe_string(data, 'symbol') + market = self.safe_market(marketId) + symbol = market['symbol'] + topic = self.safe_string(message, 'topic') + method = self.safe_string(topic.split('@'), 1) + if method == 'orderbookupdate': + if not (symbol in self.orderbooks): + return + orderbook = self.orderbooks[symbol] + timestamp = self.safe_integer(orderbook, 'timestamp') + if timestamp is None: + orderbook.cache.append(message) + else: + try: + ts = self.safe_integer(message, 'ts') + if ts > timestamp: + self.handle_order_book_message(client, message, orderbook) + client.resolve(orderbook, topic) + except Exception as e: + del self.orderbooks[symbol] + del client.subscriptions[topic] + client.reject(e, topic) + else: + if not (symbol in self.orderbooks): + defaultLimit = self.safe_integer(self.options, 'watchOrderBookLimit', 1000) + subscription = client.subscriptions[topic] + limit = self.safe_integer(subscription, 'limit', defaultLimit) + self.orderbooks[symbol] = self.order_book({}, limit) + orderbook = self.orderbooks[symbol] + timestamp = self.safe_integer(message, 'ts') + snapshot = self.parse_order_book(data, symbol, timestamp, 'bids', 'asks') + orderbook.reset(snapshot) + client.resolve(orderbook, topic) + + def handle_order_book_subscription(self, client: Client, message, subscription): + defaultLimit = self.safe_integer(self.options, 'watchOrderBookLimit', 1000) + limit = self.safe_integer(subscription, 'limit', defaultLimit) + symbol = self.safe_string(subscription, 'symbol') # watchOrderBook + if symbol in self.orderbooks: + del self.orderbooks[symbol] + self.orderbooks[symbol] = self.order_book({}, limit) + self.spawn(self.fetch_order_book_snapshot, client, message, subscription) + + async def fetch_order_book_snapshot(self, client, message, subscription): + symbol = self.safe_string(subscription, 'symbol') + messageHash = self.safe_string(message, 'topic') + try: + defaultLimit = self.safe_integer(self.options, 'watchOrderBookLimit', 1000) + limit = self.safe_integer(subscription, 'limit', defaultLimit) + params = self.safe_value(subscription, 'params') + snapshot = await self.fetch_rest_order_book_safe(symbol, limit, params) + if self.safe_value(self.orderbooks, symbol) is None: + # if the orderbook is dropped before the snapshot is received + return + orderbook = self.orderbooks[symbol] + orderbook.reset(snapshot) + messages = orderbook.cache + for i in range(0, len(messages)): + messageItem = messages[i] + ts = self.safe_integer(messageItem, 'ts') + if ts < orderbook['timestamp']: + continue + else: + self.handle_order_book_message(client, messageItem, orderbook) + self.orderbooks[symbol] = orderbook + client.resolve(orderbook, messageHash) + except Exception as e: + del client.subscriptions[messageHash] + client.reject(e, messageHash) + + def handle_order_book_message(self, client: Client, message, orderbook): + data = self.safe_dict(message, 'data') + self.handle_deltas(orderbook['asks'], self.safe_value(data, 'asks', [])) + self.handle_deltas(orderbook['bids'], self.safe_value(data, 'bids', [])) + timestamp = self.safe_integer(message, 'ts') + orderbook['timestamp'] = timestamp + orderbook['datetime'] = self.iso8601(timestamp) + return orderbook + + def handle_delta(self, bookside, delta): + price = self.safe_float_2(delta, 'price', 0) + amount = self.safe_float_2(delta, 'quantity', 1) + bookside.store(price, amount) + + def handle_deltas(self, bookside, deltas): + for i in range(0, len(deltas)): + self.handle_delta(bookside, deltas[i]) + + async def watch_ticker(self, symbol: str, params={}) -> Ticker: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + name = 'ticker' + market = self.market(symbol) + symbol = market['symbol'] + topic = market['id'] + '@' + name + request: dict = { + 'event': 'subscribe', + 'topic': topic, + } + message = self.extend(request, params) + return await self.watch_public(topic, message) + + async def un_watch_ticker(self, symbol: str, params={}) -> Any: + """ + unWatches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + method = None + method, params = self.handle_option_and_params(params, 'watchTicker', 'method', 'ticker') + market = self.market(symbol) + subHash = market['id'] + '@' + method + topic = 'ticker' + return await self.unwatch_public(subHash, market['symbol'], topic, params) + + def parse_ws_ticker(self, ticker, market=None): + # + # { + # "symbol": "PERP_BTC_USDT", + # "open": 19441.5, + # "close": 20147.07, + # "high": 20761.87, + # "low": 19320.54, + # "volume": 2481.103, + # "amount": 50037935.0286, + # "count": 3689 + # } + # + return self.safe_ticker({ + 'symbol': self.safe_symbol(None, market), + 'timestamp': None, + 'datetime': None, + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': None, + 'bidVolume': None, + 'ask': None, + 'askVolume': None, + 'vwap': None, + 'open': self.safe_string(ticker, 'open'), + 'close': self.safe_string(ticker, 'close'), + 'last': None, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': self.safe_string(ticker, 'volume'), + 'quoteVolume': self.safe_string(ticker, 'amount'), + 'info': ticker, + }, market) + + def handle_ticker(self, client: Client, message): + # + # { + # "topic": "PERP_BTC_USDT@ticker", + # "ts": 1657120017000, + # "data": { + # "symbol": "PERP_BTC_USDT", + # "open": 19441.5, + # "close": 20147.07, + # "high": 20761.87, + # "low": 19320.54, + # "volume": 2481.103, + # "amount": 50037935.0286, + # "count": 3689 + # } + # } + # + data = self.safe_value(message, 'data') + topic = self.safe_value(message, 'topic') + marketId = self.safe_string(data, 'symbol') + market = self.safe_market(marketId) + timestamp = self.safe_integer(message, 'ts') + data['date'] = timestamp + ticker = self.parse_ws_ticker(data, market) + ticker['symbol'] = market['symbol'] + self.tickers[market['symbol']] = ticker + client.resolve(ticker, topic) + return message + + async def watch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + + https://docs.woox.io/#24h-tickers + + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list + :param str[] symbols: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + name = 'tickers' + topic = name + request: dict = { + 'event': 'subscribe', + 'topic': topic, + } + message = self.extend(request, params) + tickers = await self.watch_public(topic, message) + return self.filter_by_array(tickers, 'symbol', symbols) + + async def un_watch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + + https://docs.woox.io/#24h-tickers + + stops watching a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list + :param str[] symbols: unified symbol of the market to stop fetching the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + if symbols is not None: + raise NotSupported(self.id + ' unWatchTickers() does not support a symbols argument. Only unwatch all tickers at once') + topic = 'ticker' + subHash = 'tickers' + return await self.unwatch_public(subHash, None, topic, params) + + def handle_tickers(self, client: Client, message): + # + # { + # "topic":"tickers", + # "ts":1618820615000, + # "data":[ + # { + # "symbol":"SPOT_OKB_USDT", + # "open":16.297, + # "close":17.183, + # "high":24.707, + # "low":11.997, + # "volume":0, + # "amount":0, + # "count":0 + # }, + # { + # "symbol":"SPOT_XRP_USDT", + # "open":1.3515, + # "close":1.43794, + # "high":1.96674, + # "low":0.39264, + # "volume":750127.1, + # "amount":985440.5122, + # "count":396 + # }, + # ... + # ] + # } + # + topic = self.safe_value(message, 'topic') + data = self.safe_value(message, 'data') + timestamp = self.safe_integer(message, 'ts') + result = [] + for i in range(0, len(data)): + marketId = self.safe_string(data[i], 'symbol') + market = self.safe_market(marketId) + ticker = self.parse_ws_ticker(self.extend(data[i], {'date': timestamp}), market) + self.tickers[market['symbol']] = ticker + result.append(ticker) + client.resolve(result, topic) + + async def watch_bids_asks(self, symbols: Strings = None, params={}) -> Tickers: + """ + + https://docs.woox.io/#bbos + + watches best bid & ask for symbols + :param str[] [symbols]: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + name = 'bbos' + topic = name + request: dict = { + 'event': 'subscribe', + 'topic': topic, + } + message = self.extend(request, params) + bidsasks = await self.watch_public(topic, message) + if self.newUpdates: + return bidsasks + return self.filter_by_array(self.bidsasks, 'symbol', symbols) + + async def un_watch_bids_asks(self, symbols: Strings = None, params={}) -> Any: + """ + + https://docs.woox.io/#bbos + + unWatches best bid & ask for symbols + :param str[] [symbols]: unified symbol of the market to fetch the ticker for(not used by woo) + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + if symbols is not None: + raise NotSupported(self.id + ' unWatchBidsAsks() does not support a symbols argument. Only unwatch all bidsAsks at once') + subHash = 'bbos' + topic = 'bidsasks' + return await self.unwatch_public(subHash, None, topic, params) + + def handle_bid_ask(self, client: Client, message): + # + # { + # "topic": "bbos", + # "ts": 1618822376000, + # "data": [ + # { + # "symbol": "SPOT_FIL_USDT", + # "ask": 159.0318, + # "askSize": 370.43, + # "bid": 158.9158, + # "bidSize": 16 + # } + # ] + # } + # + topic = self.safe_string(message, 'topic') + data = self.safe_list(message, 'data', []) + timestamp = self.safe_integer(message, 'ts') + result: dict = {} + for i in range(0, len(data)): + ticker = self.safe_dict(data, i) + ticker['ts'] = timestamp + parsedTicker = self.parse_ws_bid_ask(ticker) + symbol = parsedTicker['symbol'] + self.bidsasks[symbol] = parsedTicker + result[symbol] = parsedTicker + client.resolve(result, topic) + + def parse_ws_bid_ask(self, ticker, market=None): + marketId = self.safe_string(ticker, 'symbol') + market = self.safe_market(marketId, market) + symbol = self.safe_string(market, 'symbol') + timestamp = self.safe_integer(ticker, 'ts') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'ask': self.safe_string(ticker, 'ask'), + 'askVolume': self.safe_string(ticker, 'askSize'), + 'bid': self.safe_string(ticker, 'bid'), + 'bidVolume': self.safe_string(ticker, 'bidSize'), + 'info': ticker, + }, market) + + async def watch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + watches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://docs.woox.io/#k-line + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + if (timeframe != '1m') and (timeframe != '5m') and (timeframe != '15m') and (timeframe != '30m') and (timeframe != '1h') and (timeframe != '1d') and (timeframe != '1w') and (timeframe != '1M'): + raise ExchangeError(self.id + ' watchOHLCV timeframe argument must be 1m, 5m, 15m, 30m, 1h, 1d, 1w, 1M') + market = self.market(symbol) + interval = self.safe_string(self.timeframes, timeframe, timeframe) + name = 'kline' + topic = market['id'] + '@' + name + '_' + interval + request: dict = { + 'event': 'subscribe', + 'topic': topic, + } + message = self.extend(request, params) + ohlcv = await self.watch_public(topic, message) + if self.newUpdates: + limit = ohlcv.getLimit(market['symbol'], limit) + return self.filter_by_since_limit(ohlcv, since, limit, 0, True) + + async def un_watch_ohlcv(self, symbol: str, timeframe: str = '1m', params={}) -> Any: + """ + unWatches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://docs.woox.io/#k-line + + :param str symbol: unified symbol of the market + :param str timeframe: the length of time each candle represents + :param dict [params]: extra parameters specific to the exchange API endpoint + :param dict [params.timezone]: if provided, kline intervals are interpreted in that timezone instead of UTC, example '+08:00' + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + interval = self.safe_string(self.timeframes, timeframe, timeframe) + topic = 'ohlcv' + name = 'kline' + subHash = market['id'] + '@' + name + '_' + interval + params['symbolsAndTimeframes'] = [[market['symbol'], timeframe]] + return await self.unwatch_public(subHash, market['symbol'], topic, params) + + def handle_ohlcv(self, client: Client, message): + # + # { + # "topic":"SPOT_BTC_USDT@kline_1m", + # "ts":1618822432146, + # "data":{ + # "symbol":"SPOT_BTC_USDT", + # "type":"1m", + # "open":56948.97, + # "close":56891.76, + # "high":56948.97, + # "low":56889.06, + # "volume":44.00947568, + # "amount":2504584.9, + # "startTime":1618822380000, + # "endTime":1618822440000 + # } + # } + # + data = self.safe_value(message, 'data') + topic = self.safe_value(message, 'topic') + marketId = self.safe_string(data, 'symbol') + market = self.safe_market(marketId) + symbol = market['symbol'] + interval = self.safe_string(data, 'type') + timeframe = self.find_timeframe(interval) + parsed = [ + self.safe_integer(data, 'startTime'), + self.safe_float(data, 'open'), + self.safe_float(data, 'high'), + self.safe_float(data, 'low'), + self.safe_float(data, 'close'), + self.safe_float(data, 'volume'), + ] + self.ohlcvs[symbol] = self.safe_value(self.ohlcvs, symbol, {}) + stored = self.safe_value(self.ohlcvs[symbol], timeframe) + if stored is None: + limit = self.safe_integer(self.options, 'OHLCVLimit', 1000) + stored = ArrayCacheByTimestamp(limit) + self.ohlcvs[symbol][timeframe] = stored + stored.append(parsed) + client.resolve(stored, topic) + + async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + watches information on multiple trades made in a market + + https://docs.woox.io/#trade + + :param str symbol: unified market symbol of the market trades were made in + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trade structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + topic = market['id'] + '@trade' + request: dict = { + 'event': 'subscribe', + 'topic': topic, + } + message = self.extend(request, params) + trades = await self.watch_public(topic, message) + if self.newUpdates: + limit = trades.getLimit(market['symbol'], limit) + return self.filter_by_symbol_since_limit(trades, symbol, since, limit, True) + + async def un_watch_trades(self, symbol: str, params={}) -> Any: + """ + unWatches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list + + https://docs.woox.io/#trade + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + topic = 'trades' + subHash = market['id'] + '@trade' + return await self.unwatch_public(subHash, market['symbol'], topic, params) + + def handle_trade(self, client: Client, message): + # + # { + # "topic":"SPOT_ADA_USDT@trade", + # "ts":1618820361552, + # "data":{ + # "symbol":"SPOT_ADA_USDT", + # "price":1.27988, + # "size":300, + # "side":"BUY", + # "source":0 + # } + # } + # + topic = self.safe_string(message, 'topic') + timestamp = self.safe_integer(message, 'ts') + data = self.safe_value(message, 'data') + marketId = self.safe_string(data, 'symbol') + market = self.safe_market(marketId) + symbol = market['symbol'] + trade = self.parse_ws_trade(self.extend(data, {'timestamp': timestamp}), market) + tradesArray = self.safe_value(self.trades, symbol) + if tradesArray is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + tradesArray = ArrayCache(limit) + tradesArray.append(trade) + self.trades[symbol] = tradesArray + client.resolve(tradesArray, topic) + + def parse_ws_trade(self, trade, market=None): + # + # { + # "symbol":"SPOT_ADA_USDT", + # "timestamp":1618820361552, + # "price":1.27988, + # "size":300, + # "side":"BUY", + # "source":0 + # } + # private trade + # { + # "msgType": 0, # execution report + # "symbol": "SPOT_BTC_USDT", + # "clientOrderId": 0, + # "orderId": 54774393, + # "type": "MARKET", + # "side": "BUY", + # "quantity": 0.0, + # "price": 0.0, + # "tradeId": 56201985, + # "executedPrice": 23534.06, + # "executedQuantity": 0.00040791, + # "fee": 2.1E-7, + # "feeAsset": "BTC", + # "totalExecutedQuantity": 0.00040791, + # "avgPrice": 23534.06, + # "status": "FILLED", + # "reason": "", + # "orderTag": "default", + # "totalFee": 2.1E-7, + # "feeCurrency": "BTC", + # "totalRebate": 0, + # "rebateCurrency": "USDT", + # "visible": 0.0, + # "timestamp": 1675406261689, + # "reduceOnly": False, + # "maker": False + # } + # + marketId = self.safe_string(trade, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + price = self.safe_string_2(trade, 'executedPrice', 'price') + amount = self.safe_string_2(trade, 'executedQuantity', 'size') + cost = Precise.string_mul(price, amount) + side = self.safe_string_lower(trade, 'side') + timestamp = self.safe_integer(trade, 'timestamp') + maker = self.safe_bool(trade, 'marker') + takerOrMaker = None + if maker is not None: + takerOrMaker = 'maker' if maker else 'taker' + type = self.safe_string_lower(trade, 'type') + fee = None + feeCost = self.safe_number(trade, 'fee') + if feeCost is not None: + fee = { + 'cost': feeCost, + 'currency': self.safe_currency_code(self.safe_string(trade, 'feeCurrency')), + } + return self.safe_trade({ + 'id': self.safe_string(trade, 'tradeId'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'side': side, + 'price': price, + 'amount': amount, + 'cost': cost, + 'order': self.safe_string(trade, 'orderId'), + 'takerOrMaker': takerOrMaker, + 'type': type, + 'fee': fee, + 'info': trade, + }, market) + + def check_required_uid(self, error=True): + if not self.uid: + if error: + raise AuthenticationError(self.id + ' requires `uid` credential(woox calls it `application_id`)') + else: + return False + return True + + async def authenticate(self, params={}): + self.check_required_credentials() + url = self.urls['api']['ws']['private'] + '/' + self.uid + client = self.client(url) + messageHash = 'authenticated' + event = 'auth' + future = client.reusableFuture(messageHash) + authenticated = self.safe_value(client.subscriptions, messageHash) + if authenticated is None: + ts = str(self.nonce()) + auth = '|' + ts + signature = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256) + request: dict = { + 'event': event, + 'params': { + 'apikey': self.apiKey, + 'sign': signature, + 'timestamp': ts, + }, + } + message = self.extend(request, params) + self.watch(url, messageHash, message, messageHash, message) + return await future + + async def watch_private(self, messageHash, message, params={}): + await self.authenticate(params) + url = self.urls['api']['ws']['private'] + '/' + self.uid + requestId = self.request_id(url) + subscribe: dict = { + 'id': requestId, + } + request = self.extend(subscribe, message) + return await self.watch(url, messageHash, request, messageHash, subscribe) + + async def watch_private_multiple(self, messageHashes, message, params={}): + await self.authenticate(params) + url = self.urls['api']['ws']['private'] + '/' + self.uid + requestId = self.request_id(url) + subscribe: dict = { + 'id': requestId, + } + request = self.extend(subscribe, message) + return await self.watch_multiple(url, messageHashes, request, messageHashes, subscribe) + + async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + + https://docs.woox.io/#executionreport + https://docs.woox.io/#algoexecutionreportv2 + + watches information on multiple orders made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.trigger]: True if trigger order + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + trigger = self.safe_bool_2(params, 'stop', 'trigger', False) + topic = 'algoexecutionreportv2' if (trigger) else 'executionreport' + params = self.omit(params, ['stop', 'trigger']) + messageHash = topic + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + messageHash += ':' + symbol + request: dict = { + 'event': 'subscribe', + 'topic': topic, + } + message = self.extend(request, params) + orders = await self.watch_private(messageHash, message) + if self.newUpdates: + limit = orders.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(orders, symbol, since, limit, True) + + async def watch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + + https://docs.woox.io/#executionreport + https://docs.woox.io/#algoexecutionreportv2 + + watches information on multiple trades made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.trigger]: True if trigger order + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + trigger = self.safe_bool_2(params, 'stop', 'trigger', False) + topic = 'algoexecutionreportv2' if (trigger) else 'executionreport' + params = self.omit(params, ['stop', 'trigger']) + messageHash = 'myTrades' + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + messageHash += ':' + symbol + request: dict = { + 'event': 'subscribe', + 'topic': topic, + } + message = self.extend(request, params) + trades = await self.watch_private(messageHash, message) + if self.newUpdates: + limit = trades.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(trades, symbol, since, limit, True) + + def parse_ws_order(self, order, market=None): + # + # { + # "symbol": "PERP_BTC_USDT", + # "clientOrderId": 0, + # "orderId": 52952826, + # "type": "LIMIT", + # "side": "SELL", + # "quantity": 0.01, + # "price": 22000, + # "tradeId": 0, + # "executedPrice": 0, + # "executedQuantity": 0, + # "fee": 0, + # "feeAsset": "USDT", + # "totalExecutedQuantity": 0, + # "status": "NEW", + # "reason": '', + # "orderTag": "default", + # "totalFee": 0, + # "visible": 0.01, + # "timestamp": 1657515556799, + # "reduceOnly": False, + # "maker": False + # } + # { + # "symbol": "SPOT_BTC_USDT", + # "rootAlgoOrderId": 2573778, + # "parentAlgoOrderId": 0, + # "algoOrderId": 2573778, + # "clientOrderId": 0, + # "orderTag": "default", + # "algoType": "STOP_LOSS", + # "side": "SELL", + # "quantity": 0.00011, + # "triggerPrice": 98566.67, + # "triggerStatus": "USELESS", + # "price": 0, + # "type": "MARKET", + # "triggerTradePrice": 0, + # "triggerTime": 0, + # "tradeId": 0, + # "executedPrice": 0, + # "executedQuantity": 0, + # "fee": 0, + # "reason": "", + # "feeAsset": "", + # "totalExecutedQuantity": 0, + # "averageExecutedPrice": 0, + # "totalFee": 0, + # "timestamp": 1761030467426, + # "visibleQuantity": 0, + # "reduceOnly": False, + # "triggerPriceType": "MARKET_PRICE", + # "positionSide": "BOTH", + # "feeCurrency": "", + # "totalRebate": 0.0, + # "rebateCurrency": "", + # "triggered": False, + # "maker": False, + # "activated": False, + # "isTriggered": False, + # "isMaker": False, + # "isActivated": False, + # "rootAlgoStatus": "NEW", + # "algoStatus": "NEW" + # } + # + orderId = self.safe_string_2(order, 'orderId', 'algoOrderId') + marketId = self.safe_string(order, 'symbol') + market = self.market(marketId) + symbol = market['symbol'] + timestamp = self.safe_integer(order, 'timestamp') + fee = { + 'cost': self.safe_string(order, 'totalFee'), + 'currency': self.safe_string(order, 'feeAsset'), + } + priceString = self.safe_string(order, 'price') + price = self.safe_number(order, 'price') + avgPrice = self.safe_number(order, 'avgPrice') + if Precise.string_eq(priceString, '0') and (avgPrice is not None): + price = avgPrice + amount = self.safe_float(order, 'quantity') + side = self.safe_string_lower(order, 'side') + type = self.safe_string_lower(order, 'type') + filled = self.safe_number(order, 'totalExecutedQuantity') + totalExecQuantity = self.safe_float(order, 'totalExecutedQuantity') + remaining = amount + if amount >= totalExecQuantity: + remaining -= totalExecQuantity + rawStatus = self.safe_string_2(order, 'status', 'algoStatus') + status = self.parse_order_status(rawStatus) + trades = None + clientOrderId = self.safe_string(order, 'clientOrderId') + triggerPrice = self.safe_string(order, 'triggerPrice') + return self.safe_order({ + 'info': order, + 'symbol': symbol, + 'id': orderId, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': timestamp, + 'type': type, + 'timeInForce': None, + 'postOnly': None, + 'side': side, + 'price': price, + 'stopPrice': triggerPrice, + 'triggerPrice': triggerPrice, + 'reduceOnly': self.safe_bool(order, 'reduceOnly'), + 'amount': amount, + 'cost': None, + 'average': avgPrice, + 'filled': filled, + 'remaining': remaining, + 'status': status, + 'fee': fee, + 'trades': trades, + }) + + def handle_order_update(self, client: Client, message): + # + # { + # "topic": "executionreport", + # "ts": 1657515556799, + # "data": { + # "symbol": "PERP_BTC_USDT", + # "clientOrderId": 0, + # "orderId": 52952826, + # "type": "LIMIT", + # "side": "SELL", + # "quantity": 0.01, + # "price": 22000, + # "tradeId": 0, + # "executedPrice": 0, + # "executedQuantity": 0, + # "fee": 0, + # "feeAsset": "USDT", + # "totalExecutedQuantity": 0, + # "status": "NEW", + # "reason": '', + # "orderTag": "default", + # "totalFee": 0, + # "visible": 0.01, + # "timestamp": 1657515556799, + # "reduceOnly": False, + # "maker": False + # } + # } + # + topic = self.safe_string(message, 'topic') + data = self.safe_value(message, 'data') + if isinstance(data, list): + # algoexecutionreportv2 + for i in range(0, len(data)): + order = data[i] + tradeId = self.omit_zero(self.safe_string(data, 'tradeId')) + if tradeId is not None: + self.handle_my_trade(client, order) + self.handle_order(client, order, topic) + else: + # executionreport + tradeId = self.omit_zero(self.safe_string(data, 'tradeId')) + if tradeId is not None: + self.handle_my_trade(client, data) + self.handle_order(client, data, topic) + + def handle_order(self, client: Client, message, topic): + parsed = self.parse_ws_order(message) + symbol = self.safe_string(parsed, 'symbol') + orderId = self.safe_string(parsed, 'id') + if symbol is not None: + if self.orders is None: + limit = self.safe_integer(self.options, 'ordersLimit', 1000) + self.orders = ArrayCacheBySymbolById(limit) + cachedOrders = self.orders + orders = self.safe_value(cachedOrders.hashmap, symbol, {}) + order = self.safe_value(orders, orderId) + if order is not None: + fee = self.safe_value(order, 'fee') + if fee is not None: + parsed['fee'] = fee + fees = self.safe_value(order, 'fees') + if fees is not None: + parsed['fees'] = fees + parsed['trades'] = self.safe_value(order, 'trades') + parsed['timestamp'] = self.safe_integer(order, 'timestamp') + parsed['datetime'] = self.safe_string(order, 'datetime') + cachedOrders.append(parsed) + client.resolve(self.orders, topic) + messageHashSymbol = topic + ':' + symbol + client.resolve(self.orders, messageHashSymbol) + + def handle_my_trade(self, client: Client, message): + # + # { + # "msgType": 0, # execution report + # "symbol": "SPOT_BTC_USDT", + # "clientOrderId": 0, + # "orderId": 54774393, + # "type": "MARKET", + # "side": "BUY", + # "quantity": 0.0, + # "price": 0.0, + # "tradeId": 56201985, + # "executedPrice": 23534.06, + # "executedQuantity": 0.00040791, + # "fee": 2.1E-7, + # "feeAsset": "BTC", + # "totalExecutedQuantity": 0.00040791, + # "avgPrice": 23534.06, + # "status": "FILLED", + # "reason": "", + # "orderTag": "default", + # "totalFee": 2.1E-7, + # "feeCurrency": "BTC", + # "totalRebate": 0, + # "rebateCurrency": "USDT", + # "visible": 0.0, + # "timestamp": 1675406261689, + # "reduceOnly": False, + # "maker": False + # } + # + myTrades = self.myTrades + if myTrades is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + myTrades = ArrayCacheBySymbolById(limit) + trade = self.parse_ws_trade(message) + myTrades.append(trade) + messageHash = 'myTrades:' + trade['symbol'] + client.resolve(myTrades, messageHash) + messageHash = 'myTrades' + client.resolve(myTrades, messageHash) + + async def watch_positions(self, symbols: Strings = None, since: Int = None, limit: Int = None, params={}) -> List[Position]: + """ + + https://docs.woox.io/#position-push + + watch all open positions + :param str[]|None symbols: list of unified market symbols + @param since + @param limit + :param dict params: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + await self.load_markets() + messageHashes = [] + symbols = self.market_symbols(symbols) + if not self.is_empty(symbols): + for i in range(0, len(symbols)): + symbol = symbols[i] + messageHashes.append('positions::' + symbol) + else: + messageHashes.append('positions') + url = self.urls['api']['ws']['private'] + '/' + self.uid + client = self.client(url) + self.set_positions_cache(client, symbols) + fetchPositionsSnapshot = self.handle_option('watchPositions', 'fetchPositionsSnapshot', True) + awaitPositionsSnapshot = self.handle_option('watchPositions', 'awaitPositionsSnapshot', True) + if fetchPositionsSnapshot and awaitPositionsSnapshot and self.positions is None: + snapshot = await client.future('fetchPositionsSnapshot') + return self.filter_by_symbols_since_limit(snapshot, symbols, since, limit, True) + request: dict = { + 'event': 'subscribe', + 'topic': 'position', + } + newPositions = await self.watch_private_multiple(messageHashes, request, params) + if self.newUpdates: + return newPositions + return self.filter_by_symbols_since_limit(self.positions, symbols, since, limit, True) + + def set_positions_cache(self, client: Client, type, symbols: Strings = None): + fetchPositionsSnapshot = self.handle_option('watchPositions', 'fetchPositionsSnapshot', False) + if fetchPositionsSnapshot: + messageHash = 'fetchPositionsSnapshot' + if not (messageHash in client.futures): + client.future(messageHash) + self.spawn(self.load_positions_snapshot, client, messageHash) + else: + self.positions = ArrayCacheBySymbolBySide() + + async def load_positions_snapshot(self, client, messageHash): + positions = await self.fetch_positions() + self.positions = ArrayCacheBySymbolBySide() + cache = self.positions + for i in range(0, len(positions)): + position = positions[i] + contracts = self.safe_number(position, 'contracts', 0) + if contracts > 0: + cache.append(position) + # don't remove the future from the .futures cache + future = client.futures[messageHash] + future.resolve(cache) + client.resolve(cache, 'positions') + + def handle_positions(self, client, message): + # + # { + # "topic":"position", + # "ts":1705292345255, + # "data":{ + # "positions":{ + # "PERP_LTC_USDT":{ + # "holding":1, + # "pendingLongQty":0, + # "pendingShortQty":0, + # "averageOpenPrice":71.53, + # "pnl24H":0, + # "fee24H":0.07153, + # "settlePrice":71.53, + # "markPrice":71.32098452065145, + # "version":7886, + # "openingTime":1705292304267, + # "pnl24HPercentage":0, + # "adlQuantile":1, + # "positionSide":"BOTH" + # } + # } + # } + # } + # + data = self.safe_value(message, 'data', {}) + rawPositions = self.safe_value(data, 'positions', {}) + postitionsIds = list(rawPositions.keys()) + if self.positions is None: + self.positions = ArrayCacheBySymbolBySide() + cache = self.positions + newPositions = [] + for i in range(0, len(postitionsIds)): + marketId = postitionsIds[i] + market = self.safe_market(marketId) + rawPosition = rawPositions[marketId] + position = self.parse_position(rawPosition, market) + newPositions.append(position) + cache.append(position) + messageHash = 'positions::' + market['symbol'] + client.resolve(position, messageHash) + client.resolve(newPositions, 'positions') + + async def watch_balance(self, params={}) -> Balances: + """ + + https://docs.woox.io/#balance + + watch balance and get the amount of funds available for trading or funds locked in orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + topic = 'balance' + messageHash = topic + request: dict = { + 'event': 'subscribe', + 'topic': topic, + } + message = self.extend(request, params) + return await self.watch_private(messageHash, message) + + def handle_balance(self, client, message): + # + # { + # "topic": "balance", + # "ts": 1695716888789, + # "data": { + # "balances": { + # "USDT": { + # "holding": 266.56059176, + # "frozen": 0, + # "interest": 0, + # "pendingShortQty": 0, + # "pendingExposure": 0, + # "pendingLongQty": 0, + # "pendingLongExposure": 0, + # "version": 37, + # "staked": 0, + # "unbonding": 0, + # "vault": 0, + # "averageOpenPrice": 0, + # "pnl24H": 0, + # "fee24H": 0, + # "markPrice": 1, + # "pnl24HPercentage": 0 + # } + # } + # + # } + # + data = self.safe_value(message, 'data') + balances = self.safe_value(data, 'balances') + keys = list(balances.keys()) + ts = self.safe_integer(message, 'ts') + self.balance['info'] = data + self.balance['timestamp'] = ts + self.balance['datetime'] = self.iso8601(ts) + for i in range(0, len(keys)): + key = keys[i] + value = balances[key] + code = self.safe_currency_code(key) + account = self.balance[code] if (code in self.balance) else self.account() + total = self.safe_string(value, 'holding') + used = self.safe_string(value, 'frozen') + account['total'] = total + account['used'] = used + account['free'] = Precise.string_sub(total, used) + self.balance[code] = account + self.balance = self.safe_balance(self.balance) + client.resolve(self.balance, 'balance') + + def handle_error_message(self, client: Client, message) -> Bool: + # + # {"id":"1","event":"subscribe","success":false,"ts":1710780997216,"errorMsg":"Auth is needed."} + # + if not ('success' in message): + return False + success = self.safe_bool(message, 'success') + if success: + return False + errorMessage = self.safe_string(message, 'errorMsg') + try: + if errorMessage is not None: + feedback = self.id + ' ' + self.json(message) + self.throw_exactly_matched_exception(self.exceptions['exact'], errorMessage, feedback) + return False + except Exception as error: + if isinstance(error, AuthenticationError): + messageHash = 'authenticated' + client.reject(error, messageHash) + if messageHash in client.subscriptions: + del client.subscriptions[messageHash] + else: + client.reject(error) + return True + + def handle_un_subscription(self, client: Client, message): + # + # { + # "id": "2", + # "event": "unsubscribe", + # "success": True, + # "ts": 1759568478343, + # "data": "SPOT_BTC_USDT@orderbook" + # } + # + subscribeHash = self.safe_string(message, 'data') + unsubscribeHash = 'unsubscribe::' + subscribeHash + subscription = self.safe_dict(client.subscriptions, unsubscribeHash, {}) + subMessageHashes = self.safe_list(subscription, 'subMessageHashes', []) + unsubMessageHashes = self.safe_list(subscription, 'unsubMessageHashes', []) + for i in range(0, len(subMessageHashes)): + subHash = subMessageHashes[i] + unsubHash = unsubMessageHashes[i] + self.clean_unsubscription(client, subHash, unsubHash) + self.clean_cache(subscription) + + def handle_message(self, client: Client, message): + if self.handle_error_message(client, message): + return + methods: dict = { + 'ping': self.handle_ping, + 'pong': self.handle_pong, + 'subscribe': self.handle_subscribe, + 'unsubscribe': self.handle_un_subscription, + 'orderbook': self.handle_order_book, + 'orderbookupdate': self.handle_order_book, + 'ticker': self.handle_ticker, + 'tickers': self.handle_tickers, + 'kline': self.handle_ohlcv, + 'auth': self.handle_auth, + 'executionreport': self.handle_order_update, + 'algoexecutionreportv2': self.handle_order_update, + 'trade': self.handle_trade, + 'balance': self.handle_balance, + 'position': self.handle_positions, + 'bbos': self.handle_bid_ask, + } + event = self.safe_string(message, 'event') + method = self.safe_value(methods, event) + if method is not None: + method(client, message) + return + topic = self.safe_string(message, 'topic') + if topic is not None: + method = self.safe_value(methods, topic) + if method is not None: + method(client, message) + return + splitTopic = topic.split('@') + splitLength = len(splitTopic) + if splitLength == 2: + name = self.safe_string(splitTopic, 1) + method = self.safe_value(methods, name) + if method is not None: + method(client, message) + return + splitName = name.split('_') + splitNameLength = len(splitTopic) + if splitNameLength == 2: + method = self.safe_value(methods, self.safe_string(splitName, 0)) + if method is not None: + method(client, message) + + def ping(self, client: Client): + return {'event': 'ping'} + + def handle_ping(self, client: Client, message): + return {'event': 'pong'} + + def handle_pong(self, client: Client, message): + # + # {event: "pong", ts: 1657117026090} + # + client.lastPong = self.milliseconds() + return message + + def handle_subscribe(self, client: Client, message): + # + # { + # "id": "666888", + # "event": "subscribe", + # "success": True, + # "ts": 1657117712212 + # } + # + id = self.safe_string(message, 'id') + subscriptionsById = self.index_by(client.subscriptions, 'id') + subscription = self.safe_value(subscriptionsById, id, {}) + method = self.safe_value(subscription, 'method') + if method is not None: + method(client, message, subscription) + return message + + def handle_auth(self, client: Client, message): + # + # { + # "event": "auth", + # "success": True, + # "ts": 1657463158812 + # } + # + messageHash = 'authenticated' + success = self.safe_value(message, 'success') + if success: + # client.resolve(message, messageHash) + future = self.safe_value(client.futures, 'authenticated') + future.resolve(True) + else: + error = AuthenticationError(self.json(message)) + client.reject(error, messageHash) + # allows further authentication attempts + if messageHash in client.subscriptions: + del client.subscriptions['authenticated'] diff --git a/ccxt/pro/woofipro.py b/ccxt/pro/woofipro.py new file mode 100644 index 0000000..581499b --- /dev/null +++ b/ccxt/pro/woofipro.py @@ -0,0 +1,1271 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolById, ArrayCacheBySymbolBySide, ArrayCacheByTimestamp +from ccxt.base.types import Any, Balances, Bool, Int, Order, OrderBook, Position, Str, Strings, Ticker, Tickers, Trade +from ccxt.async_support.base.ws.client import Client +from typing import List +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import NotSupported +from ccxt.base.precise import Precise + + +class woofipro(ccxt.async_support.woofipro): + + def describe(self) -> Any: + return self.deep_extend(super(woofipro, self).describe(), { + 'has': { + 'ws': True, + 'watchBalance': True, + 'watchMyTrades': True, + 'watchOHLCV': True, + 'watchOrderBook': True, + 'watchOrders': True, + 'watchTicker': True, + 'watchTickers': True, + 'watchBidsAsks': True, + 'watchTrades': True, + 'watchTradesForSymbols': False, + 'watchPositions': True, + }, + 'urls': { + 'api': { + 'ws': { + 'public': 'wss://ws-evm.orderly.org/ws/stream', + 'private': 'wss://ws-private-evm.orderly.org/v2/ws/private/stream', + }, + }, + 'test': { + 'ws': { + 'public': 'wss://testnet-ws-evm.orderly.org/ws/stream', + 'private': 'wss://testnet-ws-private-evm.orderly.org/v2/ws/private/stream', + }, + }, + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + 'accountId': True, + }, + 'options': { + 'tradesLimit': 1000, + 'ordersLimit': 1000, + 'requestId': {}, + 'watchPositions': { + 'fetchPositionsSnapshot': True, # or False + 'awaitPositionsSnapshot': True, # whether to wait for the positions snapshot before providing updates + }, + }, + 'streaming': { + 'ping': self.ping, + 'keepAlive': 10000, + }, + 'exceptions': { + 'ws': { + 'exact': { + 'Auth is needed.': AuthenticationError, + }, + }, + }, + }) + + def request_id(self, url): + options = self.safe_dict(self.options, 'requestId', {}) + previousValue = self.safe_integer(options, url, 0) + newValue = self.sum(previousValue, 1) + self.options['requestId'][url] = newValue + return newValue + + async def watch_public(self, messageHash, message): + # the default id + id = 'OqdphuyCtYWxwzhxyLLjOWNdFP7sQt8RPWzmb5xY' + if self.accountId is not None and self.accountId != '': + id = self.accountId + url = self.urls['api']['ws']['public'] + '/' + id + requestId = self.request_id(url) + subscribe: dict = { + 'id': requestId, + } + request = self.extend(subscribe, message) + return await self.watch(url, messageHash, request, messageHash, subscribe) + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + + https://orderly.network/docs/build-on-evm/evm-api/websocket-api/public/orderbook + + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return. + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + name = 'orderbook' + market = self.market(symbol) + topic = market['id'] + '@' + name + request: dict = { + 'event': 'subscribe', + 'topic': topic, + } + message = self.extend(request, params) + orderbook = await self.watch_public(topic, message) + return orderbook.limit() + + def handle_order_book(self, client: Client, message): + # + # { + # "topic": "PERP_BTC_USDC@orderbook", + # "ts": 1650121915308, + # "data": { + # "symbol": "PERP_BTC_USDC", + # "bids": [ + # [ + # 0.30891, + # 2469.98 + # ] + # ], + # "asks": [ + # [ + # 0.31075, + # 2379.63 + # ] + # ] + # } + # } + # + data = self.safe_dict(message, 'data', {}) + marketId = self.safe_string(data, 'symbol') + market = self.safe_market(marketId) + symbol = market['symbol'] + topic = self.safe_string(message, 'topic') + if not (symbol in self.orderbooks): + self.orderbooks[symbol] = self.order_book() + orderbook = self.orderbooks[symbol] + timestamp = self.safe_integer(message, 'ts') + snapshot = self.parse_order_book(data, symbol, timestamp, 'bids', 'asks') + orderbook.reset(snapshot) + client.resolve(orderbook, topic) + + async def watch_ticker(self, symbol: str, params={}) -> Ticker: + """ + + https://orderly.network/docs/build-on-evm/evm-api/websocket-api/public/24-hour-ticker + + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + name = 'ticker' + market = self.market(symbol) + symbol = market['symbol'] + topic = market['id'] + '@' + name + request: dict = { + 'event': 'subscribe', + 'topic': topic, + } + message = self.extend(request, params) + return await self.watch_public(topic, message) + + def parse_ws_ticker(self, ticker, market=None): + # + # { + # "symbol": "PERP_BTC_USDC", + # "open": 19441.5, + # "close": 20147.07, + # "high": 20761.87, + # "low": 19320.54, + # "volume": 2481.103, + # "amount": 50037935.0286, + # "count": 3689 + # } + # + return self.safe_ticker({ + 'symbol': self.safe_symbol(None, market), + 'timestamp': None, + 'datetime': None, + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': None, + 'bidVolume': None, + 'ask': None, + 'askVolume': None, + 'vwap': None, + 'open': self.safe_string(ticker, 'open'), + 'close': self.safe_string(ticker, 'close'), + 'last': None, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': self.safe_string(ticker, 'volume'), + 'quoteVolume': self.safe_string(ticker, 'amount'), + 'info': ticker, + }, market) + + def handle_ticker(self, client: Client, message): + # + # { + # "topic": "PERP_BTC_USDC@ticker", + # "ts": 1657120017000, + # "data": { + # "symbol": "PERP_BTC_USDC", + # "open": 19441.5, + # "close": 20147.07, + # "high": 20761.87, + # "low": 19320.54, + # "volume": 2481.103, + # "amount": 50037935.0286, + # "count": 3689 + # } + # } + # + data = self.safe_dict(message, 'data', {}) + topic = self.safe_string(message, 'topic') + marketId = self.safe_string(data, 'symbol') + market = self.safe_market(marketId) + timestamp = self.safe_integer(message, 'ts') + data['date'] = timestamp + ticker = self.parse_ws_ticker(data, market) + ticker['symbol'] = market['symbol'] + self.tickers[market['symbol']] = ticker + client.resolve(ticker, topic) + return message + + async def watch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + + https://orderly.network/docs/build-on-evm/evm-api/websocket-api/public/24-hour-tickers + + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list + :param str[] symbols: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + name = 'tickers' + topic = name + request: dict = { + 'event': 'subscribe', + 'topic': topic, + } + message = self.extend(request, params) + tickers = await self.watch_public(topic, message) + return self.filter_by_array(tickers, 'symbol', symbols) + + def handle_tickers(self, client: Client, message): + # + # { + # "topic":"tickers", + # "ts":1618820615000, + # "data":[ + # { + # "symbol":"PERP_NEAR_USDC", + # "open":16.297, + # "close":17.183, + # "high":24.707, + # "low":11.997, + # "volume":0, + # "amount":0, + # "count":0 + # }, + # ... + # ] + # } + # + topic = self.safe_string(message, 'topic') + data = self.safe_list(message, 'data', []) + timestamp = self.safe_integer(message, 'ts') + result = [] + for i in range(0, len(data)): + marketId = self.safe_string(data[i], 'symbol') + market = self.safe_market(marketId) + ticker = self.parse_ws_ticker(self.extend(data[i], {'date': timestamp}), market) + self.tickers[market['symbol']] = ticker + result.append(ticker) + client.resolve(result, topic) + + async def watch_bids_asks(self, symbols: Strings = None, params={}) -> Tickers: + """ + + https://orderly.network/docs/build-on-evm/evm-api/websocket-api/public/bbos + + watches best bid & ask for symbols + :param str[] symbols: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + name = 'bbos' + topic = name + request: dict = { + 'event': 'subscribe', + 'topic': topic, + } + message = self.extend(request, params) + tickers = await self.watch_public(topic, message) + return self.filter_by_array(tickers, 'symbol', symbols) + + def handle_bid_ask(self, client: Client, message): + # + # { + # "topic": "bbos", + # "ts": 1726212495000, + # "data": [ + # { + # "symbol": "PERP_WOO_USDC", + # "ask": 0.16570, + # "askSize": 4224, + # "bid": 0.16553, + # "bidSize": 6645 + # } + # ] + # } + # + topic = self.safe_string(message, 'topic') + data = self.safe_list(message, 'data', []) + timestamp = self.safe_integer(message, 'ts') + result = [] + for i in range(0, len(data)): + ticker = self.parse_ws_bid_ask(self.extend(data[i], {'ts': timestamp})) + self.tickers[ticker['symbol']] = ticker + result.append(ticker) + client.resolve(result, topic) + + def parse_ws_bid_ask(self, ticker, market=None): + marketId = self.safe_string(ticker, 'symbol') + market = self.safe_market(marketId, market) + symbol = self.safe_string(market, 'symbol') + timestamp = self.safe_integer(ticker, 'ts') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'ask': self.safe_string(ticker, 'ask'), + 'askVolume': self.safe_string(ticker, 'askSize'), + 'bid': self.safe_string(ticker, 'bid'), + 'bidVolume': self.safe_string(ticker, 'bidSize'), + 'info': ticker, + }, market) + + async def watch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + watches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://orderly.network/docs/build-on-evm/evm-api/websocket-api/public/k-line + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + if (timeframe != '1m') and (timeframe != '5m') and (timeframe != '15m') and (timeframe != '30m') and (timeframe != '1h') and (timeframe != '1d') and (timeframe != '1w') and (timeframe != '1M'): + raise NotSupported(self.id + ' watchOHLCV timeframe argument must be 1m, 5m, 15m, 30m, 1h, 1d, 1w, 1M') + market = self.market(symbol) + interval = self.safe_string(self.timeframes, timeframe, timeframe) + name = 'kline' + topic = market['id'] + '@' + name + '_' + interval + request: dict = { + 'event': 'subscribe', + 'topic': topic, + } + message = self.extend(request, params) + ohlcv = await self.watch_public(topic, message) + if self.newUpdates: + limit = ohlcv.getLimit(market['symbol'], limit) + return self.filter_by_since_limit(ohlcv, since, limit, 0, True) + + def handle_ohlcv(self, client: Client, message): + # + # { + # "topic":"PERP_BTC_USDC@kline_1m", + # "ts":1618822432146, + # "data":{ + # "symbol":"PERP_BTC_USDC", + # "type":"1m", + # "open":56948.97, + # "close":56891.76, + # "high":56948.97, + # "low":56889.06, + # "volume":44.00947568, + # "amount":2504584.9, + # "startTime":1618822380000, + # "endTime":1618822440000 + # } + # } + # + data = self.safe_dict(message, 'data', {}) + topic = self.safe_string(message, 'topic') + marketId = self.safe_string(data, 'symbol') + market = self.safe_market(marketId) + symbol = market['symbol'] + interval = self.safe_string(data, 'type') + timeframe = self.find_timeframe(interval) + parsed = [ + self.safe_integer(data, 'startTime'), + self.safe_number(data, 'open'), + self.safe_number(data, 'high'), + self.safe_number(data, 'low'), + self.safe_number(data, 'close'), + self.safe_number(data, 'volume'), + ] + self.ohlcvs[symbol] = self.safe_value(self.ohlcvs, symbol, {}) + stored = self.safe_value(self.ohlcvs[symbol], timeframe) + if stored is None: + limit = self.safe_integer(self.options, 'OHLCVLimit', 1000) + stored = ArrayCacheByTimestamp(limit) + self.ohlcvs[symbol][timeframe] = stored + ohlcvCache = self.ohlcvs[symbol][timeframe] + ohlcvCache.append(parsed) + client.resolve(ohlcvCache, topic) + + async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + watches information on multiple trades made in a market + + https://orderly.network/docs/build-on-evm/evm-api/websocket-api/public/trade + + :param str symbol: unified market symbol of the market trades were made in + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trade structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + topic = market['id'] + '@trade' + request: dict = { + 'event': 'subscribe', + 'topic': topic, + } + message = self.extend(request, params) + trades = await self.watch_public(topic, message) + if self.newUpdates: + limit = trades.getLimit(market['symbol'], limit) + return self.filter_by_symbol_since_limit(trades, symbol, since, limit, True) + + def handle_trade(self, client: Client, message): + # + # { + # "topic":"PERP_ADA_USDC@trade", + # "ts":1618820361552, + # "data":{ + # "symbol":"PERP_ADA_USDC", + # "price":1.27988, + # "size":300, + # "side":"BUY", + # } + # } + # + topic = self.safe_string(message, 'topic') + timestamp = self.safe_integer(message, 'ts') + data = self.safe_dict(message, 'data', {}) + marketId = self.safe_string(data, 'symbol') + market = self.safe_market(marketId) + symbol = market['symbol'] + trade = self.parse_ws_trade(self.extend(data, {'timestamp': timestamp}), market) + if not (symbol in self.trades): + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + stored = ArrayCache(limit) + self.trades[symbol] = stored + trades = self.trades[symbol] + trades.append(trade) + self.trades[symbol] = trades + client.resolve(trades, topic) + + def parse_ws_trade(self, trade, market=None): + # + # { + # "symbol":"PERP_ADA_USDC", + # "timestamp":1618820361552, + # "price":1.27988, + # "size":300, + # "side":"BUY", + # } + # private stream + # { + # symbol: 'PERP_XRP_USDC', + # clientOrderId: '', + # orderId: 1167632251, + # type: 'MARKET', + # side: 'BUY', + # quantity: 20, + # price: 0, + # tradeId: '1715179456664012', + # executedPrice: 0.5276, + # executedQuantity: 20, + # fee: 0.006332, + # feeAsset: 'USDC', + # totalExecutedQuantity: 20, + # avgPrice: 0.5276, + # averageExecutedPrice: 0.5276, + # status: 'FILLED', + # reason: '', + # totalFee: 0.006332, + # visible: 0, + # visibleQuantity: 0, + # timestamp: 1715179456660, + # orderTag: 'CCXT', + # createdTime: 1715179456656, + # maker: False + # } + # + marketId = self.safe_string(trade, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + price = self.safe_string_2(trade, 'executedPrice', 'price') + amount = self.safe_string_2(trade, 'executedQuantity', 'size') + cost = Precise.string_mul(price, amount) + side = self.safe_string_lower(trade, 'side') + timestamp = self.safe_integer(trade, 'timestamp') + takerOrMaker = None + maker = self.safe_bool(trade, 'maker') + if maker is not None: + takerOrMaker = 'maker' if maker else 'taker' + fee = None + feeValue = self.safe_string(trade, 'fee') + if feeValue is not None: + fee = { + 'cost': feeValue, + 'currency': self.safe_currency_code(self.safe_string(trade, 'feeAsset')), + } + return self.safe_trade({ + 'id': self.safe_string(trade, 'tradeId'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'side': side, + 'price': price, + 'amount': amount, + 'cost': cost, + 'order': self.safe_string(trade, 'orderId'), + 'takerOrMaker': takerOrMaker, + 'type': self.safe_string_lower(trade, 'type'), + 'fee': fee, + 'info': trade, + }, market) + + def handle_auth(self, client: Client, message): + # + # { + # "event": "auth", + # "success": True, + # "ts": 1657463158812 + # } + # + messageHash = 'authenticated' + success = self.safe_value(message, 'success') + if success: + # client.resolve(message, messageHash) + future = self.safe_value(client.futures, 'authenticated') + future.resolve(True) + else: + error = AuthenticationError(self.json(message)) + client.reject(error, messageHash) + # allows further authentication attempts + if messageHash in client.subscriptions: + del client.subscriptions['authenticated'] + + async def authenticate(self, params={}): + self.check_required_credentials() + url = self.urls['api']['ws']['private'] + '/' + self.accountId + client = self.client(url) + messageHash = 'authenticated' + event = 'auth' + future = client.reusableFuture(messageHash) + authenticated = self.safe_value(client.subscriptions, messageHash) + if authenticated is None: + ts = str(self.nonce()) + auth = ts + secret = self.secret + if secret.find('ed25519:') >= 0: + parts = secret.split('ed25519:') + secret = parts[1] + signature = self.eddsa(self.encode(auth), self.base58_to_binary(secret), 'ed25519') + request: dict = { + 'event': event, + 'params': { + 'orderly_key': self.apiKey, + 'sign': signature, + 'timestamp': ts, + }, + } + message = self.extend(request, params) + self.watch(url, messageHash, message, messageHash) + return await future + + async def watch_private(self, messageHash, message, params={}): + await self.authenticate(params) + url = self.urls['api']['ws']['private'] + '/' + self.accountId + requestId = self.request_id(url) + subscribe: dict = { + 'id': requestId, + } + request = self.extend(subscribe, message) + return await self.watch(url, messageHash, request, messageHash, subscribe) + + async def watch_private_multiple(self, messageHashes, message, params={}): + await self.authenticate(params) + url = self.urls['api']['ws']['private'] + '/' + self.accountId + requestId = self.request_id(url) + subscribe: dict = { + 'id': requestId, + } + request = self.extend(subscribe, message) + return await self.watch_multiple(url, messageHashes, request, messageHashes, subscribe) + + async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + watches information on multiple orders made by the user + + https://orderly.network/docs/build-on-evm/evm-api/websocket-api/private/execution-report + https://orderly.network/docs/build-on-evm/evm-api/websocket-api/private/algo-execution-report + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.trigger]: True if trigger order + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + trigger = self.safe_bool_2(params, 'stop', 'trigger', False) + topic = 'algoexecutionreport' if (trigger) else 'executionreport' + params = self.omit(params, ['stop', 'trigger']) + messageHash = topic + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + messageHash += ':' + symbol + request: dict = { + 'event': 'subscribe', + 'topic': topic, + } + message = self.extend(request, params) + orders = await self.watch_private(messageHash, message) + if self.newUpdates: + limit = orders.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(orders, symbol, since, limit, True) + + async def watch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + watches information on multiple trades made by the user + + https://orderly.network/docs/build-on-evm/evm-api/websocket-api/private/execution-report + https://orderly.network/docs/build-on-evm/evm-api/websocket-api/private/algo-execution-report + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.trigger]: True if trigger order + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + trigger = self.safe_bool_2(params, 'stop', 'trigger', False) + topic = 'algoexecutionreport' if (trigger) else 'executionreport' + params = self.omit(params, 'stop') + messageHash = 'myTrades' + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + messageHash += ':' + symbol + request: dict = { + 'event': 'subscribe', + 'topic': topic, + } + message = self.extend(request, params) + orders = await self.watch_private(messageHash, message) + if self.newUpdates: + limit = orders.getLimit(symbol, limit) + return self.filter_by_symbol_since_limit(orders, symbol, since, limit, True) + + def parse_ws_order(self, order, market=None): + # + # { + # "symbol": "PERP_BTC_USDT", + # "clientOrderId": 0, + # "orderId": 52952826, + # "type": "LIMIT", + # "side": "SELL", + # "quantity": 0.01, + # "price": 22000, + # "tradeId": 0, + # "executedPrice": 0, + # "executedQuantity": 0, + # "fee": 0, + # "feeAsset": "USDT", + # "totalExecutedQuantity": 0, + # "status": "NEW", + # "reason": '', + # "orderTag": "default", + # "totalFee": 0, + # "visible": 0.01, + # "timestamp": 1657515556799, + # "reduceOnly": False, + # "maker": False + # } + # algo order + # { + # "symbol":"PERP_MATIC_USDC", + # "rootAlgoOrderId":123, + # "parentAlgoOrderId":123, + # "algoOrderId":123, + # "orderTag":"some tags", + # "algoType": "STOP", + # "clientOrderId":"client_id", + # "type":"LIMIT", + # "side":"BUY", + # "quantity":7029.0, + # "price":0.7699, + # "tradeId":0, + # "triggerTradePrice":0, + # "triggerTime":1234567, + # "triggered": False, + # "activated": False, + # "executedPrice":0.0, + # "executedQuantity":0.0, + # "fee":0.0, + # "feeAsset":"USDC", + # "totalExecutedQuantity":0.0, + # "averageExecutedQuantity":0.0, + # "avgPrice":0, + # "triggerPrice":0.0, + # "triggerPriceType":"STOP", + # "isActivated": False, + # "status":"NEW", + # "rootAlgoStatus": "FILLED", + # "algoStatus": "FILLED", + # "reason":"", + # "totalFee":0.0, + # "visible": 7029.0, + # "visibleQuantity":7029.0, + # "timestamp":1704679472448, + # "maker":false, + # "isMaker":false, + # "createdTime":1704679472448 + # } + # + orderId = self.safe_string(order, 'orderId') + marketId = self.safe_string(order, 'symbol') + market = self.market(marketId) + symbol = market['symbol'] + timestamp = self.safe_integer(order, 'timestamp') + fee = { + 'cost': self.safe_string(order, 'totalFee'), + 'currency': self.safe_string(order, 'feeAsset'), + } + priceString = self.safe_string(order, 'price') + price = self.safe_number(order, 'price') + avgPrice = self.safe_number(order, 'avgPrice') + if Precise.string_eq(priceString, '0') and (avgPrice is not None): + price = avgPrice + amount = self.safe_string(order, 'quantity') + side = self.safe_string_lower(order, 'side') + type = self.safe_string_lower(order, 'type') + filled = self.safe_number(order, 'totalExecutedQuantity') + totalExecQuantity = self.safe_string(order, 'totalExecutedQuantity') + remaining = amount + if Precise.string_ge(amount, totalExecQuantity): + remaining = Precise.string_sub(remaining, totalExecQuantity) + rawStatus = self.safe_string(order, 'status') + status = self.parse_order_status(rawStatus) + trades = None + clientOrderId = self.safe_string(order, 'clientOrderId') + triggerPrice = self.safe_number(order, 'triggerPrice') + return self.safe_order({ + 'info': order, + 'symbol': symbol, + 'id': orderId, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': timestamp, + 'type': type, + 'timeInForce': None, + 'postOnly': None, + 'side': side, + 'price': price, + 'stopPrice': triggerPrice, + 'triggerPrice': triggerPrice, + 'amount': amount, + 'cost': None, + 'average': None, + 'filled': filled, + 'remaining': remaining, + 'status': status, + 'fee': fee, + 'trades': trades, + }) + + def handle_order_update(self, client: Client, message): + # + # { + # "topic": "executionreport", + # "ts": 1657515556799, + # "data": { + # "symbol": "PERP_BTC_USDT", + # "clientOrderId": 0, + # "orderId": 52952826, + # "type": "LIMIT", + # "side": "SELL", + # "quantity": 0.01, + # "price": 22000, + # "tradeId": 0, + # "executedPrice": 0, + # "executedQuantity": 0, + # "fee": 0, + # "feeAsset": "USDT", + # "totalExecutedQuantity": 0, + # "status": "NEW", + # "reason": '', + # "orderTag": "default", + # "totalFee": 0, + # "visible": 0.01, + # "timestamp": 1657515556799, + # "maker": False + # } + # } + # + topic = self.safe_string(message, 'topic') + data = self.safe_value(message, 'data') + if isinstance(data, list): + # algoexecutionreport + for i in range(0, len(data)): + order = data[i] + tradeId = self.omit_zero(self.safe_string(data, 'tradeId')) + if tradeId is not None: + self.handle_my_trade(client, order) + self.handle_order(client, order, topic) + else: + # executionreport + tradeId = self.omit_zero(self.safe_string(data, 'tradeId')) + if tradeId is not None: + self.handle_my_trade(client, data) + self.handle_order(client, data, topic) + + def handle_order(self, client: Client, message, topic): + parsed = self.parse_ws_order(message) + symbol = self.safe_string(parsed, 'symbol') + orderId = self.safe_string(parsed, 'id') + if symbol is not None: + if self.orders is None: + limit = self.safe_integer(self.options, 'ordersLimit', 1000) + self.orders = ArrayCacheBySymbolById(limit) + cachedOrders = self.orders + orders = self.safe_dict(cachedOrders.hashmap, symbol, {}) + order = self.safe_dict(orders, orderId) + if order is not None: + fee = self.safe_value(order, 'fee') + if fee is not None: + parsed['fee'] = fee + fees = self.safe_list(order, 'fees') + if fees is not None: + parsed['fees'] = fees + parsed['trades'] = self.safe_list(order, 'trades') + parsed['timestamp'] = self.safe_integer(order, 'timestamp') + parsed['datetime'] = self.safe_string(order, 'datetime') + cachedOrders.append(parsed) + client.resolve(self.orders, topic) + messageHashSymbol = topic + ':' + symbol + client.resolve(self.orders, messageHashSymbol) + + def handle_my_trade(self, client: Client, message): + # + # { + # symbol: 'PERP_XRP_USDC', + # clientOrderId: '', + # orderId: 1167632251, + # type: 'MARKET', + # side: 'BUY', + # quantity: 20, + # price: 0, + # tradeId: '1715179456664012', + # executedPrice: 0.5276, + # executedQuantity: 20, + # fee: 0.006332, + # feeAsset: 'USDC', + # totalExecutedQuantity: 20, + # avgPrice: 0.5276, + # averageExecutedPrice: 0.5276, + # status: 'FILLED', + # reason: '', + # totalFee: 0.006332, + # visible: 0, + # visibleQuantity: 0, + # timestamp: 1715179456660, + # orderTag: 'CCXT', + # createdTime: 1715179456656, + # maker: False + # } + # + messageHash = 'myTrades' + marketId = self.safe_string(message, 'symbol') + market = self.safe_market(marketId) + symbol = market['symbol'] + trade = self.parse_ws_trade(message, market) + trades = self.myTrades + if trades is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + trades = ArrayCacheBySymbolById(limit) + self.myTrades = trades + trades.append(trade) + client.resolve(trades, messageHash) + symbolSpecificMessageHash = messageHash + ':' + symbol + client.resolve(trades, symbolSpecificMessageHash) + + async def watch_positions(self, symbols: Strings = None, since: Int = None, limit: Int = None, params={}) -> List[Position]: + """ + + https://orderly.network/docs/build-on-evm/evm-api/websocket-api/private/position-push + + watch all open positions + :param str[] [symbols]: list of unified market symbols + @param since timestamp in ms of the earliest position to fetch + @param limit the maximum number of positions to fetch + :param dict params: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + await self.load_markets() + messageHashes = [] + symbols = self.market_symbols(symbols) + if not self.is_empty(symbols): + for i in range(0, len(symbols)): + symbol = symbols[i] + messageHashes.append('positions::' + symbol) + else: + messageHashes.append('positions') + url = self.urls['api']['ws']['private'] + '/' + self.accountId + client = self.client(url) + self.set_positions_cache(client, symbols) + fetchPositionsSnapshot = self.handle_option('watchPositions', 'fetchPositionsSnapshot', True) + awaitPositionsSnapshot = self.handle_option('watchPositions', 'awaitPositionsSnapshot', True) + if fetchPositionsSnapshot and awaitPositionsSnapshot and self.positions is None: + snapshot = await client.future('fetchPositionsSnapshot') + return self.filter_by_symbols_since_limit(snapshot, symbols, since, limit, True) + request: dict = { + 'event': 'subscribe', + 'topic': 'position', + } + newPositions = await self.watch_private_multiple(messageHashes, request, params) + if self.newUpdates: + return newPositions + return self.filter_by_symbols_since_limit(self.positions, symbols, since, limit, True) + + def set_positions_cache(self, client: Client, type, symbols: Strings = None): + fetchPositionsSnapshot = self.handle_option('watchPositions', 'fetchPositionsSnapshot', False) + if fetchPositionsSnapshot: + messageHash = 'fetchPositionsSnapshot' + if not (messageHash in client.futures): + client.future(messageHash) + self.spawn(self.load_positions_snapshot, client, messageHash) + else: + self.positions = ArrayCacheBySymbolBySide() + + async def load_positions_snapshot(self, client, messageHash): + positions = await self.fetch_positions() + self.positions = ArrayCacheBySymbolBySide() + cache = self.positions + for i in range(0, len(positions)): + position = positions[i] + contracts = self.safe_string(position, 'contracts', '0') + if Precise.string_gt(contracts, '0'): + cache.append(position) + # don't remove the future from the .futures cache + future = client.futures[messageHash] + future.resolve(cache) + client.resolve(cache, 'positions') + + def handle_positions(self, client, message): + # + # { + # "topic":"position", + # "ts":1705292345255, + # "data":{ + # "positions":[ + # { + # "symbol":"PERP_ETH_USDC", + # "positionQty":3.1408, + # "costPosition":5706.51952, + # "lastSumUnitaryFunding":0.804, + # "sumUnitaryFundingVersion":0, + # "pendingLongQty":0.0, + # "pendingShortQty":-1.0, + # "settlePrice":1816.9, + # "averageOpenPrice":1804.51490427, + # "unsettledPnl":-2.79856, + # "pnl24H":-338.90179488, + # "fee24H":4.242423, + # "markPrice":1816.2, + # "estLiqPrice":0.0, + # "version":179967, + # "imrwithOrders":0.1, + # "mmrwithOrders":0.05, + # "mmr":0.05, + # "imr":0.1, + # "timestamp":1685154032762 + # } + # ] + # } + # } + # + data = self.safe_dict(message, 'data', {}) + rawPositions = self.safe_list(data, 'positions', []) + if self.positions is None: + self.positions = ArrayCacheBySymbolBySide() + cache = self.positions + newPositions = [] + for i in range(0, len(rawPositions)): + rawPosition = rawPositions[i] + marketId = self.safe_string(rawPosition, 'symbol') + market = self.safe_market(marketId) + position = self.parse_ws_position(rawPosition, market) + newPositions.append(position) + cache.append(position) + messageHash = 'positions::' + market['symbol'] + client.resolve(position, messageHash) + client.resolve(newPositions, 'positions') + + def parse_ws_position(self, position, market=None): + # + # { + # "symbol":"PERP_ETH_USDC", + # "positionQty":3.1408, + # "costPosition":5706.51952, + # "lastSumUnitaryFunding":0.804, + # "sumUnitaryFundingVersion":0, + # "pendingLongQty":0.0, + # "pendingShortQty":-1.0, + # "settlePrice":1816.9, + # "averageOpenPrice":1804.51490427, + # "unsettledPnl":-2.79856, + # "pnl24H":-338.90179488, + # "fee24H":4.242423, + # "markPrice":1816.2, + # "estLiqPrice":0.0, + # "version":179967, + # "imrwithOrders":0.1, + # "mmrwithOrders":0.05, + # "mmr":0.05, + # "imr":0.1, + # "timestamp":1685154032762 + # } + # + contract = self.safe_string(position, 'symbol') + market = self.safe_market(contract, market) + size = self.safe_string(position, 'positionQty') + side: Str = None + if Precise.string_gt(size, '0'): + side = 'long' + else: + side = 'short' + contractSize = self.safe_string(market, 'contractSize') + markPrice = self.safe_string(position, 'markPrice') + timestamp = self.safe_integer(position, 'timestamp') + entryPrice = self.safe_string(position, 'averageOpenPrice') + unrealisedPnl = self.safe_string(position, 'unsettledPnl') + size = Precise.string_abs(size) + notional = Precise.string_mul(size, markPrice) + return self.safe_position({ + 'info': position, + 'id': None, + 'symbol': self.safe_string(market, 'symbol'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastUpdateTimestamp': None, + 'initialMargin': None, + 'initialMarginPercentage': None, + 'maintenanceMargin': None, + 'maintenanceMarginPercentage': None, + 'entryPrice': self.parse_number(entryPrice), + 'notional': self.parse_number(notional), + 'leverage': None, + 'unrealizedPnl': self.parse_number(unrealisedPnl), + 'contracts': self.parse_number(size), + 'contractSize': self.parse_number(contractSize), + 'marginRatio': None, + 'liquidationPrice': self.safe_number(position, 'estLiqPrice'), + 'markPrice': self.parse_number(markPrice), + 'lastPrice': None, + 'collateral': None, + 'marginMode': 'cross', + 'marginType': None, + 'side': side, + 'percentage': None, + 'hedged': None, + 'stopLossPrice': None, + 'takeProfitPrice': None, + }) + + async def watch_balance(self, params={}) -> Balances: + """ + watch balance and get the amount of funds available for trading or funds locked in orders + + https://orderly.network/docs/build-on-evm/evm-api/websocket-api/private/balance + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + await self.load_markets() + topic = 'balance' + messageHash = topic + request: dict = { + 'event': 'subscribe', + 'topic': topic, + } + message = self.extend(request, params) + return await self.watch_private(messageHash, message) + + def handle_balance(self, client, message): + # + # { + # "topic":"balance", + # "ts":1651836695254, + # "data":{ + # "balances":{ + # "USDC":{ + # "holding":5555815.47398272, + # "frozen":0, + # "interest":0, + # "pendingShortQty":0, + # "pendingExposure":0, + # "pendingLongQty":0, + # "pendingLongExposure":0, + # "version":894, + # "staked":51370692, + # "unbonding":0, + # "vault":0, + # "averageOpenPrice":0.00000574, + # "pnl24H":0, + # "fee24H":0.01914, + # "markPrice":0.31885 + # } + # } + # } + # } + # + data = self.safe_dict(message, 'data', {}) + balances = self.safe_dict(data, 'balances', {}) + keys = list(balances.keys()) + ts = self.safe_integer(message, 'ts') + self.balance['info'] = data + self.balance['timestamp'] = ts + self.balance['datetime'] = self.iso8601(ts) + for i in range(0, len(keys)): + key = keys[i] + value = balances[key] + code = self.safe_currency_code(key) + account = self.balance[code] if (code in self.balance) else self.account() + total = self.safe_string(value, 'holding') + used = self.safe_string(value, 'frozen') + account['total'] = total + account['used'] = used + account['free'] = Precise.string_sub(total, used) + self.balance[code] = account + self.balance = self.safe_balance(self.balance) + client.resolve(self.balance, 'balance') + + def handle_error_message(self, client: Client, message) -> Bool: + # + # {"id":"1","event":"subscribe","success":false,"ts":1710780997216,"errorMsg":"Auth is needed."} + # + if not ('success' in message): + return False + success = self.safe_bool(message, 'success') + if success: + return False + errorMessage = self.safe_string(message, 'errorMsg') + try: + if errorMessage is not None: + feedback = self.id + ' ' + self.json(message) + self.throw_exactly_matched_exception(self.exceptions['exact'], errorMessage, feedback) + return False + except Exception as error: + if isinstance(error, AuthenticationError): + messageHash = 'authenticated' + client.reject(error, messageHash) + if messageHash in client.subscriptions: + del client.subscriptions[messageHash] + else: + client.reject(error) + return True + + def handle_message(self, client: Client, message): + if self.handle_error_message(client, message): + return + methods: dict = { + 'ping': self.handle_ping, + 'pong': self.handle_pong, + 'subscribe': self.handle_subscribe, + 'orderbook': self.handle_order_book, + 'ticker': self.handle_ticker, + 'tickers': self.handle_tickers, + 'kline': self.handle_ohlcv, + 'trade': self.handle_trade, + 'auth': self.handle_auth, + 'executionreport': self.handle_order_update, + 'algoexecutionreport': self.handle_order_update, + 'position': self.handle_positions, + 'balance': self.handle_balance, + 'bbos': self.handle_bid_ask, + } + event = self.safe_string(message, 'event') + method = self.safe_value(methods, event) + if method is not None: + method(client, message) + return + topic = self.safe_string(message, 'topic') + if topic is not None: + method = self.safe_value(methods, topic) + if method is not None: + method(client, message) + return + splitTopic = topic.split('@') + splitLength = len(splitTopic) + if splitLength == 2: + name = self.safe_string(splitTopic, 1) + method = self.safe_value(methods, name) + if method is not None: + method(client, message) + return + splitName = name.split('_') + splitNameLength = len(splitTopic) + if splitNameLength == 2: + method = self.safe_value(methods, self.safe_string(splitName, 0)) + if method is not None: + method(client, message) + + def ping(self, client: Client): + return {'event': 'ping'} + + def handle_ping(self, client: Client, message): + return {'event': 'pong'} + + def handle_pong(self, client: Client, message): + # + # {event: "pong", ts: 1614667590000} + # + client.lastPong = self.milliseconds() + return message + + def handle_subscribe(self, client: Client, message): + # + # { + # "id": "666888", + # "event": "subscribe", + # "success": True, + # "ts": 1657117712212 + # } + # + return message diff --git a/ccxt/pro/xt.py b/ccxt/pro/xt.py new file mode 100644 index 0000000..36158bd --- /dev/null +++ b/ccxt/pro/xt.py @@ -0,0 +1,1173 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +import ccxt.async_support +from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolById, ArrayCacheBySymbolBySide, ArrayCacheByTimestamp +from ccxt.base.types import Any, Balances, Int, Market, Order, OrderBook, Position, Str, Strings, Ticker, Tickers, Trade +from ccxt.async_support.base.ws.client import Client +from typing import List + + +class xt(ccxt.async_support.xt): + + def describe(self) -> Any: + return self.deep_extend(super(xt, self).describe(), { + 'has': { + 'ws': True, + 'watchOHLCV': True, + 'watchOrderBook': True, + 'watchTicker': True, + 'watchTickers': True, + 'watchTrades': True, + 'watchTradesForSymbols': False, + 'watchBalance': True, + 'watchOrders': True, + 'watchMyTrades': True, + 'watchPositions': True, + }, + 'urls': { + 'api': { + 'ws': { + 'spot': 'wss://stream.xt.com', + 'contract': 'wss://fstream.xt.com/ws', + }, + }, + }, + 'options': { + 'tradesLimit': 1000, + 'ordersLimit': 1000, + 'OHLCVLimit': 1000, + 'watchTicker': { + 'method': 'ticker', # agg_ticker(contract only) + }, + 'watchTickers': { + 'method': 'tickers', # agg_tickers(contract only) + }, + 'watchPositions': { + 'type': 'swap', + 'fetchPositionsSnapshot': True, + 'awaitPositionsSnapshot': True, + }, + }, + 'streaming': { + 'keepAlive': 20000, + 'ping': self.ping, + }, + 'token': None, + }) + + async def get_listen_key(self, isContract: bool): + """ + @ignore + required for private endpoints + :param str isContract: True for contract trades + + https://doc.xt.com/#websocket_privategetToken + https://doc.xt.com/#futures_user_websocket_v2base + + :returns str: listen key / access token + """ + self.check_required_credentials() + tradeType = 'contract' if isContract else 'spot' + url = self.urls['api']['ws'][tradeType] + if not isContract: + url = url + '/private' + client = self.client(url) + token = self.safe_string(client.subscriptions, 'token') + if token is None: + if isContract: + response = await self.privateLinearGetFutureUserV1UserListenKey() + # + # { + # returnCode: '0', + # msgInfo: 'success', + # error: null, + # result: '3BC1D71D6CF96DA3458FC35B05B633351684511731128' + # } + # + client.subscriptions['token'] = self.safe_string(response, 'result') + else: + response = await self.privateSpotPostWsToken() + # + # { + # "rc": 0, + # "mc": "SUCCESS", + # "ma": [], + # "result": { + # "token": "eyJhbqGciOiJSUzI1NiJ9.eyJhY2NvdW50SWQiOiIyMTQ2Mjg1MzIyNTU5Iiwic3ViIjoibGh4dDRfMDAwMUBzbmFwbWFpbC5jYyIsInNjb3BlIjoiYXV0aCIsImlzcyI6Inh0LmNvbSIsImxhc3RBdXRoVGltZSI6MTY2MzgxMzY5MDk1NSwic2lnblR5cGUiOiJBSyIsInVzZXJOYW1lIjoibGh4dDRfMDAwMUBzbmFwbWFpbC5jYyIsImV4cCI6MTY2NjQwNTY5MCwiZGV2aWNlIjoidW5rbm93biIsInVzZXJJZCI6MjE0NjI4NTMyMjU1OX0.h3zJlJBQrK2x1HvUxsKivnn6PlSrSDXXXJ7WqHAYSrN2CG5XPTKc4zKnTVoYFbg6fTS0u1fT8wH7wXqcLWXX71vm0YuP8PCvdPAkUIq4-HyzltbPr5uDYd0UByx0FPQtq1exvsQGe7evXQuDXx3SEJXxEqUbq_DNlXPTq_JyScI", + # "refreshToken": "eyJhbGciOiqJSUzI1NiJ9.eyJhY2NvdW50SWQiOiIyMTQ2Mjg1MzIyNTU5Iiwic3ViIjoibGh4dDRfMDAwMUBzbmFwbWFpbC5jYyIsInNjb3BlIjoicmVmcmVzaCIsImlzcyI6Inh0LmNvbSIsImxhc3RBdXRoVGltZSI6MTY2MzgxMzY5MDk1NSwic2lnblR5cGUiOiJBSyIsInVzZXJOYW1lIjoibGh4dDRfMDAwMUBzbmFwbWFpbC5jYyIsImV4cCI6MTY2NjQwNTY5MCwiZGV2aWNlIjoidW5rbm93biIsInVzZXJJZCI6MjE0NjI4NTMyMjU1OX0.Fs3YVm5YrEOzzYOSQYETSmt9iwxUHBovh2u73liv1hLUec683WGfktA_s28gMk4NCpZKFeQWFii623FvdfNoteXR0v1yZ2519uNvNndtuZICDdv3BQ4wzW1wIHZa1skxFfqvsDnGdXpjqu9UFSbtHwxprxeYfnxChNk4ssei430" + # } + # } + # + result = self.safe_dict(response, 'result') + client.subscriptions['token'] = self.safe_string(result, 'accessToken') + return client.subscriptions['token'] + + def get_cache_index(self, orderbook, cache): + # return the first index of the cache that can be applied to the orderbook or -1 if not possible + nonce = self.safe_integer(orderbook, 'nonce') + firstDelta = self.safe_value(cache, 0) + firstDeltaNonce = self.safe_integer_2(firstDelta, 'i', 'u') + if nonce < firstDeltaNonce - 1: + return -1 + for i in range(0, len(cache)): + delta = cache[i] + deltaNonce = self.safe_integer_2(delta, 'i', 'u') + if deltaNonce >= nonce: + return i + return len(cache) + + def handle_delta(self, orderbook, delta): + orderbook['nonce'] = self.safe_integer_2(delta, 'i', 'u') + obAsks = self.safe_list(delta, 'a', []) + obBids = self.safe_list(delta, 'b', []) + bids = orderbook['bids'] + asks = orderbook['asks'] + for i in range(0, len(obBids)): + bid = obBids[i] + price = self.safe_number(bid, 0) + quantity = self.safe_number(bid, 1) + bids.store(price, quantity) + for i in range(0, len(obAsks)): + ask = obAsks[i] + price = self.safe_number(ask, 0) + quantity = self.safe_number(ask, 1) + asks.store(price, quantity) + # self.handleBidAsks(storedBids, bids) + # self.handleBidAsks(storedAsks, asks) + + async def subscribe(self, name: str, access: str, methodName: str, market: Market = None, symbols: List[str] = None, params={}): + """ + @ignore + Connects to a websocket channel + + https://doc.xt.com/#websocket_privaterequestFormat + https://doc.xt.com/#futures_market_websocket_v2base + + :param str name: name of the channel + :param str access: public or private + :param str methodName: the name of the CCXT class method + :param dict [market]: CCXT market + :param str[] [symbols]: unified market symbols + :param dict params: extra parameters specific to the xt api + :returns dict: data from the websocket stream + """ + privateAccess = access == 'private' + type = None + type, params = self.handle_market_type_and_params(methodName, market, params) + isContract = (type != 'spot') + subscribe = { + 'method': 'SUBSCRIBE' if isContract else 'subscribe', + 'id': self.number_to_string(self.milliseconds()) + name, # call back ID + } + if privateAccess: + if not isContract: + subscribe['params'] = [name] + subscribe['listenKey'] = await self.get_listen_key(isContract) + else: + listenKey = await self.get_listen_key(isContract) + param = name + '@' + listenKey + subscribe['params'] = [param] + else: + subscribe['params'] = [name] + tradeType = 'contract' if isContract else 'spot' + messageHash = name + '::' + tradeType + if symbols is not None: + messageHash = messageHash + '::' + ','.join(symbols) + request = self.extend(subscribe, params) + tail = access + if isContract: + tail = 'user' if privateAccess else 'market' + url = self.urls['api']['ws'][tradeType] + '/' + tail + return await self.watch(url, messageHash, request, messageHash) + + async def watch_ticker(self, symbol: str, params={}) -> Ticker: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://doc.xt.com/#websocket_publictickerRealTime + https://doc.xt.com/#futures_market_websocket_v2tickerRealTime + https://doc.xt.com/#futures_market_websocket_v2aggTickerRealTime + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict params: extra parameters specific to the xt api endpoint + :param str [params.method]: 'agg_ticker'(contract only) or 'ticker', default = 'ticker' - the endpoint that will be streamed + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + options = self.safe_dict(self.options, 'watchTicker') + defaultMethod = self.safe_string(options, 'method', 'ticker') + method = self.safe_string(params, 'method', defaultMethod) + name = method + '@' + market['id'] + return await self.subscribe(name, 'public', 'watchTicker', market, None, params) + + async def watch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://doc.xt.com/#websocket_publicallTicker + https://doc.xt.com/#futures_market_websocket_v2allTicker + https://doc.xt.com/#futures_market_websocket_v2allAggTicker + + :param str [symbols]: unified market symbols + :param dict params: extra parameters specific to the xt api endpoint + :param str [params.method]: 'agg_tickers'(contract only) or 'tickers', default = 'tickers' - the endpoint that will be streamed + :returns dict: a `ticker structure ` + """ + await self.load_markets() + options = self.safe_dict(self.options, 'watchTickers') + defaultMethod = self.safe_string(options, 'method', 'tickers') + name = self.safe_string(params, 'method', defaultMethod) + market = None + if symbols is not None: + market = self.market(symbols[0]) + tickers = await self.subscribe(name, 'public', 'watchTickers', market, symbols, params) + if self.newUpdates: + return tickers + return self.filter_by_array(self.tickers, 'symbol', symbols) + + async def watch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + watches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://doc.xt.com/#websocket_publicsymbolKline + https://doc.xt.com/#futures_market_websocket_v2symbolKline + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: 1m, 3m, 5m, 15m, 30m, 1h, 2h, 4h, 6h, 8h, 12h, 1d, 3d, 1w, or 1M + :param int [since]: not used by xt watchOHLCV + :param int [limit]: not used by xt watchOHLCV + :param dict params: extra parameters specific to the xt api endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + name = 'kline@' + market['id'] + ',' + timeframe + ohlcv = await self.subscribe(name, 'public', 'watchOHLCV', market, None, params) + if self.newUpdates: + limit = ohlcv.getLimit(symbol, limit) + return self.filter_by_since_limit(ohlcv, since, limit, 0, True) + + async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://doc.xt.com/#websocket_publicdealRecord + https://doc.xt.com/#futures_market_websocket_v2dealRecord + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict params: extra parameters specific to the xt api endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + market = self.market(symbol) + name = 'trade@' + market['id'] + trades = await self.subscribe(name, 'public', 'watchTrades', market, None, params) + if self.newUpdates: + limit = trades.getLimit(symbol, limit) + return self.filter_by_since_limit(trades, since, limit, 'timestamp') + + async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://doc.xt.com/#websocket_publiclimitDepth + https://doc.xt.com/#websocket_publicincreDepth + https://doc.xt.com/#futures_market_websocket_v2limitDepth + https://doc.xt.com/#futures_market_websocket_v2increDepth + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: not used by xt watchOrderBook + :param dict params: extra parameters specific to the xt api endpoint + :param int [params.levels]: 5, 10, 20, or 50 + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + levels = self.safe_string(params, 'levels') + params = self.omit(params, 'levels') + name = 'depth_update@' + market['id'] + if levels is not None: + name = 'depth@' + market['id'] + ',' + levels + orderbook = await self.subscribe(name, 'public', 'watchOrderBook', market, None, params) + return orderbook.limit() + + async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + watches information on multiple orders made by the user + + https://doc.xt.com/#websocket_privateorderChange + https://doc.xt.com/#futures_user_websocket_v2order + + :param str [symbol]: unified market symbol + :param int [since]: not used by xt watchOrders + :param int [limit]: the maximum number of orders to return + :param dict params: extra parameters specific to the xt api endpoint + :returns dict[]: a list of `order structures ` + """ + await self.load_markets() + name = 'order' + market = None + if symbol is not None: + market = self.market(symbol) + orders = await self.subscribe(name, 'private', 'watchOrders', market, None, params) + if self.newUpdates: + limit = orders.getLimit(symbol, limit) + return self.filter_by_since_limit(orders, since, limit, 'timestamp') + + async def watch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + watches information on multiple trades made by the user + + https://doc.xt.com/#websocket_privateorderDeal + https://doc.xt.com/#futures_user_websocket_v2trade + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of orde structures to retrieve + :param dict params: extra parameters specific to the kucoin api endpoint + :returns dict[]: a list of `trade structures ` + """ + await self.load_markets() + name = 'trade' + market = None + if symbol is not None: + market = self.market(symbol) + trades = await self.subscribe(name, 'private', 'watchMyTrades', market, None, params) + if self.newUpdates: + limit = trades.getLimit(symbol, limit) + return self.filter_by_since_limit(trades, since, limit, 'timestamp') + + async def watch_balance(self, params={}) -> Balances: + """ + watches information on multiple orders made by the user + + https://doc.xt.com/#websocket_privatebalanceChange + https://doc.xt.com/#futures_user_websocket_v2balance + + :param dict params: extra parameters specific to the xt api endpoint + :returns dict[]: a list of `balance structures ` + """ + await self.load_markets() + name = 'balance' + return await self.subscribe(name, 'private', 'watchBalance', None, None, params) + + async def watch_positions(self, symbols: Strings = None, since: Int = None, limit: Int = None, params={}) -> List[Position]: + """ + + https://doc.xt.com/#futures_user_websocket_v2position + + watch all open positions + :param str[]|None symbols: list of unified market symbols + :param number [since]: since timestamp + :param number [limit]: limit + :param dict params: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + await self.load_markets() + url = self.urls['api']['ws']['contract'] + '/' + 'user' + client = self.client(url) + self.set_positions_cache(client) + fetchPositionsSnapshot = self.handle_option('watchPositions', 'fetchPositionsSnapshot', True) + awaitPositionsSnapshot = self.handle_option('watchPositions', 'awaitPositionsSnapshot', True) + cache = self.positions + if fetchPositionsSnapshot and awaitPositionsSnapshot and self.is_empty(cache): + snapshot = await client.future('fetchPositionsSnapshot') + return self.filter_by_symbols_since_limit(snapshot, symbols, since, limit, True) + name = 'position' + newPositions = await self.subscribe(name, 'private', 'watchPositions', None, None, params) + if self.newUpdates: + return newPositions + return self.filter_by_symbols_since_limit(cache, symbols, since, limit, True) + + def set_positions_cache(self, client: Client): + if self.positions is None: + self.positions = ArrayCacheBySymbolBySide() + fetchPositionsSnapshot = self.handle_option('watchPositions', 'fetchPositionsSnapshot') + if fetchPositionsSnapshot: + messageHash = 'fetchPositionsSnapshot' + if not (messageHash in client.futures): + client.future(messageHash) + self.spawn(self.load_positions_snapshot, client, messageHash) + + async def load_positions_snapshot(self, client, messageHash): + positions = await self.fetch_positions(None) + self.positions = ArrayCacheBySymbolBySide() + cache = self.positions + for i in range(0, len(positions)): + position = positions[i] + contracts = self.safe_number(position, 'contracts', 0) + if contracts > 0: + cache.append(position) + # don't remove the future from the .futures cache + future = client.futures[messageHash] + future.resolve(cache) + client.resolve(cache, 'position::contract') + + def handle_position(self, client, message): + # + # { + # topic: 'position', + # event: 'position', + # data: { + # accountId: 245296, + # accountType: 0, + # symbol: 'eth_usdt', + # contractType: 'PERPETUAL', + # positionType: 'CROSSED', + # positionSide: 'LONG', + # positionSize: '1', + # closeOrderSize: '0', + # availableCloseSize: '1', + # realizedProfit: '-0.0121', + # entryPrice: '2637.87', + # openOrderSize: '1', + # isolatedMargin: '2.63787', + # openOrderMarginFrozen: '2.78832014', + # underlyingType: 'U_BASED', + # leverage: 10, + # welfareAccount: False, + # profitFixedLatest: {}, + # closeProfit: '0.0000', + # totalFee: '-0.0158', + # totalFundFee: '0.0037', + # markPrice: '2690.96' + # } + # } + # + if self.positions is None: + self.positions = ArrayCacheBySymbolBySide() + cache = self.positions + data = self.safe_dict(message, 'data', {}) + position = self.parse_position(data) + cache.append(position) + messageHashes = self.find_message_hashes(client, 'position::contract') + for i in range(0, len(messageHashes)): + messageHash = messageHashes[i] + parts = messageHash.split('::') + symbolsString = parts[1] + symbols = symbolsString.split(',') + positions = self.filter_by_array([position], 'symbol', symbols, False) + if not self.is_empty(positions): + client.resolve(positions, messageHash) + client.resolve([position], 'position::contract') + + def handle_ticker(self, client: Client, message: dict): + # + # spot + # + # { + # topic: 'ticker', + # event: 'ticker@btc_usdt', + # data: { + # s: 'btc_usdt', # symbol + # t: 1683501935877, # time(Last transaction time) + # cv: '-82.67', # priceChangeValue(24 hour price change) + # cr: '-0.0028', # priceChangeRate 24-hour price change(percentage) + # o: '28823.87', # open price + # c: '28741.20', # close price + # h: '29137.64', # highest price + # l: '28660.93', # lowest price + # q: '6372.601573', # quantity + # v: '184086075.2772391' # volume + # } + # } + # + # contract + # + # { + # "topic": "ticker", + # "event": "ticker@btc_usdt", + # "data": { + # "s": "btc_index", # trading pair + # "o": "49000", # opening price + # "c": "50000", # closing price + # "h": "0.1", # highest price + # "l": "0.1", # lowest price + # "a": "0.1", # volume + # "v": "0.1", # turnover + # "ch": "0.21", # quote change + # "t": 123124124 # timestamp + # } + # } + # + # agg_ticker(contract) + # + # { + # "topic": "agg_ticker", + # "event": "agg_ticker@btc_usdt", + # "data": { + # "s": "btc_index", # trading pair + # "o": "49000", # opening price + # "c": "50000", # closing price + # "h": "0.1", # highest price + # "l": "0.1", # lowest price + # "a": "0.1", # volume + # "v": "0.1", # turnover + # "ch": "0.21", # quote change + # "i": "0.21" , # index price + # "m": "0.21", # mark price + # "bp": "0.21", # bid price + # "ap": "0.21" , # ask price + # "t": 123124124 # timestamp + # } + # } + # + data = self.safe_dict(message, 'data') + marketId = self.safe_string(data, 's') + if marketId is not None: + cv = self.safe_string(data, 'cv') + isSpot = cv is not None + ticker = self.parse_ticker(data) + symbol = ticker['symbol'] + self.tickers[symbol] = ticker + event = self.safe_string(message, 'event') + messageHashTail = 'spot' if isSpot else 'contract' + messageHash = event + '::' + messageHashTail + client.resolve(ticker, messageHash) + return message + + def handle_tickers(self, client: Client, message: dict): + # + # spot + # + # { + # topic: 'tickers', + # event: 'tickers', + # data: [ + # { + # s: 'elon_usdt', + # t: 1683502958381, + # cv: '-0.0000000125', + # cr: '-0.0495', + # o: '0.0000002522', + # c: '0.0000002397', + # h: '0.0000002690', + # l: '0.0000002371', + # q: '3803783034.0000000000', + # v: '955.3260820022' + # }, + # ... + # ] + # } + # + # contract + # + # { + # "topic": "tickers", + # "event": "tickers", + # "data": [ + # { + # "s": "btc_index", # trading pair + # "o": "49000", # opening price + # "c": "50000", # closing price + # "h": "0.1", # highest price + # "l": "0.1", # lowest price + # "a": "0.1", # volume + # "v": "0.1", # turnover + # "ch": "0.21", # quote change + # "t": 123124124 # timestamp + # } + # ] + # } + # + # agg_ticker(contract) + # + # { + # "topic": "agg_tickers", + # "event": "agg_tickers", + # "data": [ + # { + # "s": "btc_index", # trading pair + # "o": "49000", # opening price + # "c": "50000", # closing price + # "h": "0.1", # highest price + # "l": "0.1", # lowest price + # "a": "0.1", # volume + # "v": "0.1", # turnover + # "ch": "0.21", # quote change + # "i": "0.21" , # index price + # "m": "0.21", # mark price + # "bp": "0.21", # bid price + # "ap": "0.21" , # ask price + # "t": 123124124 # timestamp + # } + # ] + # } + # + data = self.safe_list(message, 'data', []) + firstTicker = self.safe_dict(data, 0) + spotTest = self.safe_string_2(firstTicker, 'cv', 'aq') + tradeType = 'spot' if (spotTest is not None) else 'contract' + newTickers = [] + for i in range(0, len(data)): + tickerData = data[i] + ticker = self.parse_ticker(tickerData) + symbol = ticker['symbol'] + self.tickers[symbol] = ticker + newTickers.append(ticker) + messageHashStart = self.safe_string(message, 'topic') + '::' + tradeType + messageHashes = self.find_message_hashes(client, messageHashStart + '::') + for i in range(0, len(messageHashes)): + messageHash = messageHashes[i] + parts = messageHash.split('::') + symbolsString = parts[2] + symbols = symbolsString.split(',') + tickers = self.filter_by_array(newTickers, 'symbol', symbols) + tickersSymbols = list(tickers.keys()) + numTickers = len(tickersSymbols) + if numTickers > 0: + client.resolve(tickers, messageHash) + client.resolve(self.tickers, messageHashStart) + return message + + def handle_ohlcv(self, client: Client, message: dict): + # + # spot + # + # { + # "topic": "kline", + # "event": "kline@btc_usdt,5m", + # "data": { + # "s": "btc_usdt", # symbol + # "t": 1656043200000, # time + # "i": "5m", # interval + # "o": "44000", # open price + # "c": "50000", # close price + # "h": "52000", # highest price + # "l": "36000", # lowest price + # "q": "34.2", # qty(quantity) + # "v": "230000" # volume + # } + # } + # + # contract + # + # { + # "topic": "kline", + # "event": "kline@btc_usdt,5m", + # "data": { + # "s": "btc_index", # trading pair + # "o": "49000", # opening price + # "c": "50000", # closing price + # "h": "0.1", # highest price + # "l": "0.1", # lowest price + # "a": "0.1", # volume + # "v": "0.1", # turnover + # "ch": "0.21", # quote change + # "t": 123124124 # timestamp + # } + # } + # + data = self.safe_dict(message, 'data', {}) + marketId = self.safe_string(data, 's') + if marketId is not None: + timeframe = self.safe_string(data, 'i') + tradeType = 'spot' if ('q' in data) else 'contract' + market = self.safe_market(marketId, None, None, tradeType) + symbol = market['symbol'] + parsed = self.parse_ohlcv(data, market) + self.ohlcvs[symbol] = self.safe_dict(self.ohlcvs, symbol, {}) + stored = self.safe_value(self.ohlcvs[symbol], timeframe) + if stored is None: + limit = self.safe_integer(self.options, 'OHLCVLimit', 1000) + stored = ArrayCacheByTimestamp(limit) + self.ohlcvs[symbol][timeframe] = stored + stored.append(parsed) + event = self.safe_string(message, 'event') + messageHash = event + '::' + tradeType + client.resolve(stored, messageHash) + return message + + def handle_trade(self, client: Client, message: dict): + # + # spot + # + # { + # topic: 'trade', + # event: 'trade@btc_usdt', + # data: { + # s: 'btc_usdt', + # i: '228825383103928709', + # t: 1684258222702, + # p: '27003.65', + # q: '0.000796', + # b: True + # } + # } + # + # contract + # + # { + # "topic": "trade", + # "event": "trade@btc_usdt", + # "data": { + # "s": "btc_index", # trading pair + # "p": "50000", # price + # "a": "0.1" # Quantity + # "m": "BID" # Deal side BID:Buy ASK:Sell + # "t": 123124124 # timestamp + # } + # } + # + data = self.safe_dict(message, 'data') + marketId = self.safe_string_lower(data, 's') + if marketId is not None: + trade = self.parse_trade(data) + i = self.safe_string(data, 'i') + tradeType = 'spot' if (i is not None) else 'contract' + market = self.safe_market(marketId, None, None, tradeType) + symbol = market['symbol'] + event = self.safe_string(message, 'event') + tradesArray = self.safe_value(self.trades, symbol) + if tradesArray is None: + tradesLimit = self.safe_integer(self.options, 'tradesLimit', 1000) + tradesArray = ArrayCache(tradesLimit) + self.trades[symbol] = tradesArray + tradesArray.append(trade) + messageHash = event + '::' + tradeType + client.resolve(tradesArray, messageHash) + return message + + def handle_order_book(self, client: Client, message: dict): + # + # spot + # + # { + # "topic": "depth", + # "event": "depth@btc_usdt,20", + # "data": { + # "s": "btc_usdt", # symbol + # "fi": 1681433733351, # firstUpdateId = previous lastUpdateId + 1 + # "i": 1681433733371, # updateId + # "a": [ # asks(sell order) + # [ # [0]price, [1]quantity + # "34000", # price + # "1.2" # quantity + # ], + # [ + # "34001", + # "2.3" + # ] + # ], + # "b": [ # bids(buy order) + # [ + # "32000", + # "0.2" + # ], + # [ + # "31000", + # "0.5" + # ] + # ] + # } + # } + # + # contract + # + # { + # "topic": "depth", + # "event": "depth@btc_usdt,20", + # "data": { + # s: "btc_usdt", + # pu: "548111455664", + # fu: "548111455665", + # u: "548111455667", + # a: [ + # [ + # "26841.5", + # "50210", + # ], + # ], + # b: [ + # [ + # "26841", + # "67075", + # ], + # ], + # t: 1684530667083, + # } + # } + # + data = self.safe_dict(message, 'data') + marketId = self.safe_string(data, 's') + if marketId is not None: + event = self.safe_string(message, 'event') + splitEvent = event.split(',') + event = self.safe_string(splitEvent, 0) + tradeType = 'contract' if ('fu' in data) else 'spot' + market = self.safe_market(marketId, None, None, tradeType) + symbol = market['symbol'] + obAsks = self.safe_list(data, 'a') + obBids = self.safe_list(data, 'b') + messageHash = event + '::' + tradeType + if not (symbol in self.orderbooks): + subscription = self.safe_dict(client.subscriptions, messageHash, {}) + limit = self.safe_integer(subscription, 'limit') + self.orderbooks[symbol] = self.order_book({}, limit) + orderbook = self.orderbooks[symbol] + nonce = self.safe_integer(orderbook, 'nonce') + if nonce is None: + cacheLength = len(orderbook.cache) + snapshotDelay = self.handle_option('watchOrderBook', 'snapshotDelay', 25) + if cacheLength == snapshotDelay: + self.spawn(self.load_order_book, client, messageHash, symbol) + orderbook.cache.append(data) + return + if obAsks is not None: + asks = orderbook['asks'] + for i in range(0, len(obAsks)): + ask = obAsks[i] + price = self.safe_number(ask, 0) + quantity = self.safe_number(ask, 1) + asks.store(price, quantity) + if obBids is not None: + bids = orderbook['bids'] + for i in range(0, len(obBids)): + bid = obBids[i] + price = self.safe_number(bid, 0) + quantity = self.safe_number(bid, 1) + bids.store(price, quantity) + timestamp = self.safe_integer(data, 't') + orderbook['nonce'] = self.safe_integer_2(data, 'i', 'u') + orderbook['timestamp'] = timestamp + orderbook['datetime'] = self.iso8601(timestamp) + orderbook['symbol'] = symbol + client.resolve(orderbook, messageHash) + + def parse_ws_order_trade(self, trade: dict, market: Market = None): + # + # { + # "s": "btc_usdt", # symbol + # "t": 1656043204763, # time happened time + # "i": "6216559590087220004", # orderId, + # "ci": "test123", # clientOrderId + # "st": "PARTIALLY_FILLED", # state + # "sd": "BUY", # side BUY/SELL + # "eq": "2", # executedQty executed quantity + # "ap": "30000", # avg price + # "f": "0.002" # fee + # } + # + # contract + # + # { + # "symbol": "btc_usdt", # Trading pair + # "orderId": "1234", # Order Id + # "origQty": "34244", # Original Quantity + # "avgPrice": "123", # Quantity + # "price": "1111", # Average price + # "executedQty": "34244", # Volume(Cont) + # "orderSide": "BUY", # BUY, SELL + # "positionSide": "LONG", # LONG, SHORT + # "marginFrozen": "123", # Occupied margin + # "sourceType": "default", # DEFAULT:normal order,ENTRUST:plan commission,PROFIR:Take Profit and Stop Loss + # "sourceId" : "1231231", # Triggering conditions ID + # "state": "", # state:NEW:New order(unfilled);PARTIALLY_FILLED:Partial deal;PARTIALLY_CANCELED:Partial revocation;FILLED:Filled;CANCELED:Cancled;REJECTED:Order failed;EXPIRED:Expired + # "createTime": 1731231231, # CreateTime + # "clientOrderId": "204788317630342726" + # } + # + marketId = self.safe_string(trade, 's') + tradeType = 'contract' if ('symbol' in trade) else 'spot' + market = self.safe_market(marketId, market, None, tradeType) + timestamp = self.safe_string(trade, 't') + return self.safe_trade({ + 'info': trade, + 'id': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'order': self.safe_string(trade, 'i', 'orderId'), + 'type': self.parse_order_status(self.safe_string(trade, 'st', 'state')), + 'side': self.safe_string_lower(trade, 'sd', 'orderSide'), + 'takerOrMaker': None, + 'price': self.safe_number(trade, 'price'), + 'amount': self.safe_string(trade, 'origQty'), + 'cost': None, + 'fee': { + 'currency': None, + 'cost': self.safe_number(trade, 'f'), + 'rate': None, + }, + }, market) + + def parse_ws_order(self, order: dict, market: Market = None): + # + # spot + # + # { + # "s": "btc_usdt", # symbol + # "bc": "btc", # base currency + # "qc": "usdt", # quotation currency + # "t": 1656043204763, # happened time + # "ct": 1656043204663, # create time + # "i": "6216559590087220004", # order id, + # "ci": "test123", # client order id + # "st": "PARTIALLY_FILLED", # state NEW/PARTIALLY_FILLED/FILLED/CANCELED/REJECTED/EXPIRED + # "sd": "BUY", # side BUY/SELL + # "tp": "LIMIT", # type LIMIT/MARKET + # "oq": "4" # original quantity + # "oqq": 48000, # original quotation quantity + # "eq": "2", # executed quantity + # "lq": "2", # remaining quantity + # "p": "4000", # price + # "ap": "30000", # avg price + # "f":"0.002" # fee + # } + # + # contract + # + # { + # "symbol": "btc_usdt", # Trading pair + # "orderId": "1234", # Order Id + # "origQty": "34244", # Original Quantity + # "avgPrice": "123", # Quantity + # "price": "1111", # Average price + # "executedQty": "34244", # Volume(Cont) + # "orderSide": "BUY", # BUY, SELL + # "positionSide": "LONG", # LONG, SHORT + # "marginFrozen": "123", # Occupied margin + # "sourceType": "default", # DEFAULT:normal order,ENTRUST:plan commission,PROFIR:Take Profit and Stop Loss + # "sourceId" : "1231231", # Triggering conditions ID + # "state": "", # state:NEW:New order(unfilled);PARTIALLY_FILLED:Partial deal;PARTIALLY_CANCELED:Partial revocation;FILLED:Filled;CANCELED:Cancled;REJECTED:Order failed;EXPIRED:Expired + # "createTime": 1731231231, # CreateTime + # "clientOrderId": "204788317630342726" + # } + # + marketId = self.safe_string_2(order, 's', 'symbol') + tradeType = 'contract' if ('symbol' in order) else 'spot' + market = self.safe_market(marketId, market, None, tradeType) + timestamp = self.safe_integer_2(order, 'ct', 'createTime') + return self.safe_order({ + 'info': order, + 'id': self.safe_string_2(order, 'i', 'orderId'), + 'clientOrderId': self.safe_string_2(order, 'ci', 'clientOrderId'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'symbol': market['symbol'], + 'type': market['type'], + 'timeInForce': None, + 'postOnly': None, + 'side': self.safe_string_lower_2(order, 'sd', 'orderSide'), + 'price': self.safe_number_2(order, 'p', 'price'), + 'stopPrice': None, + 'stopLoss': None, + 'takeProfit': None, + 'amount': self.safe_string_2(order, 'oq', 'origQty'), + 'filled': self.safe_string_2(order, 'eq', 'executedQty'), + 'remaining': self.safe_string(order, 'lq'), + 'cost': None, + 'average': self.safe_string_2(order, 'ap', 'avgPrice'), + 'status': self.parse_order_status(self.safe_string(order, 'st', 'state')), + 'fee': { + 'currency': None, + 'cost': self.safe_number(order, 'f'), + }, + 'trades': None, + }, market) + + def handle_order(self, client: Client, message: dict): + # + # spot + # + # { + # "topic": "order", + # "event": "order", + # "data": { + # "s": "btc_usdt", # symbol + # "t": 1656043204763, # time happened time + # "i": "6216559590087220004", # orderId, + # "ci": "test123", # clientOrderId + # "st": "PARTIALLY_FILLED", # state + # "sd": "BUY", # side BUY/SELL + # "eq": "2", # executedQty executed quantity + # "ap": "30000", # avg price + # "f": "0.002" # fee + # } + # } + # + # contract + # + # { + # "topic": "order", + # "event": "order@123456", + # "data": { + # "symbol": "btc_usdt", # Trading pair + # "orderId": "1234", # Order Id + # "origQty": "34244", # Original Quantity + # "avgPrice": "123", # Quantity + # "price": "1111", # Average price + # "executedQty": "34244", # Volume(Cont) + # "orderSide": "BUY", # BUY, SELL + # "positionSide": "LONG", # LONG, SHORT + # "marginFrozen": "123", # Occupied margin + # "sourceType": "default", # DEFAULT:normal order,ENTRUST:plan commission,PROFIR:Take Profit and Stop Loss + # "sourceId" : "1231231", # Triggering conditions ID + # "state": "", # state:NEW:New order(unfilled);PARTIALLY_FILLED:Partial deal;PARTIALLY_CANCELED:Partial revocation;FILLED:Filled;CANCELED:Cancled;REJECTED:Order failed;EXPIRED:Expired + # "createTime": 1731231231, # CreateTime + # "clientOrderId": "204788317630342726" + # } + # } + # + orders = self.orders + if orders is None: + limit = self.safe_integer(self.options, 'ordersLimit') + orders = ArrayCacheBySymbolById(limit) + self.orders = orders + order = self.safe_dict(message, 'data', {}) + marketId = self.safe_string_2(order, 's', 'symbol') + if marketId is not None: + tradeType = 'contract' if ('symbol' in order) else 'spot' + market = self.safe_market(marketId, None, None, tradeType) + parsed = self.parse_ws_order(order, market) + orders.append(parsed) + client.resolve(orders, 'order::' + tradeType) + return message + + def handle_balance(self, client: Client, message: dict): + # + # spot + # + # { + # topic: 'balance', + # event: 'balance', + # data: { + # a: 3513677381884, + # t: 1684250056775, + # c: 'usdt', + # b: '7.71000000', + # f: '0.00000000', + # z: 'SPOT' + # } + # } + # + # contract + # + # { + # "topic": "balance", + # "event": "balance@123456", + # "data": { + # "coin": "usdt", + # "underlyingType": 1, # 1:Coin-M,2:USDT-M + # "walletBalance": "123", # Balance + # "openOrderMarginFrozen": "123", # Frozen order + # "isolatedMargin": "213", # Isolated Margin + # "crossedMargin": "0" # Crossed Margin + # "availableBalance": '2.256114450000000000', + # "coupon": '0', + # "bonus": '0' + # } + # } + # + data = self.safe_dict(message, 'data', {}) + currencyId = self.safe_string_2(data, 'c', 'coin') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(data, 'availableBalance') + account['used'] = self.safe_string(data, 'f') + account['total'] = self.safe_string_2(data, 'b', 'walletBalance') + self.balance[code] = account + self.balance = self.safe_balance(self.balance) + tradeType = 'contract' if ('coin' in data) else 'spot' + client.resolve(self.balance, 'balance::' + tradeType) + + def handle_my_trades(self, client: Client, message: dict): + # + # spot + # + # { + # "topic": "trade", + # "event": "trade", + # "data": { + # "s": "btc_usdt", # symbol + # "t": 1656043204763, # time + # "i": "6316559590087251233", # tradeId + # "oi": "6216559590087220004", # orderId + # "p": "30000", # trade price + # "q": "3", # qty quantity + # "v": "90000" # volume trade amount + # } + # } + # + # contract + # + # { + # "topic": "trade", + # "event": "trade@123456", + # "data": { + # "symbol": 'btc_usdt', + # "orderSide": 'SELL', + # "positionSide": 'LONG', + # "orderId": '231485367663419328', + # "price": '27152.7', + # "quantity": '33', + # "marginUnfrozen": '2.85318000', + # "timestamp": 1684892412565 + # } + # } + # + data = self.safe_dict(message, 'data', {}) + stored = self.myTrades + if stored is None: + limit = self.safe_integer(self.options, 'tradesLimit', 1000) + stored = ArrayCacheBySymbolById(limit) + self.myTrades = stored + parsedTrade = self.parse_trade(data) + market = self.market(parsedTrade['symbol']) + stored.append(parsedTrade) + tradeType = 'contract' if market['contract'] else 'spot' + client.resolve(stored, 'trade::' + tradeType) + + def handle_message(self, client: Client, message): + event = self.safe_string(message, 'event') + if event == 'pong': + client.onPong() + elif event is not None: + topic = self.safe_string(message, 'topic') + methods = { + 'kline': self.handle_ohlcv, + 'depth': self.handle_order_book, + 'depth_update': self.handle_order_book, + 'ticker': self.handle_ticker, + 'agg_ticker': self.handle_ticker, + 'tickers': self.handle_tickers, + 'agg_tickers': self.handle_tickers, + 'balance': self.handle_balance, + 'order': self.handle_order, + 'position': self.handle_position, + } + method = self.safe_value(methods, topic) + if topic == 'trade': + data = self.safe_dict(message, 'data') + if ('oi' in data) or ('orderId' in data): + method = self.handle_my_trades + else: + method = self.handle_trade + if method is not None: + method(client, message) + + def ping(self, client: Client): + client.lastPong = self.milliseconds() + return 'ping' + + def handle_error_message(self, client: Client, message: dict): + # + # { + # "id": "123", + # "code": 401, + # "msg": "token expire" + # } + # + msg = self.safe_string(message, 'msg') + if (msg == 'invalid_listen_key') or (msg == 'token expire'): + client.subscriptions['token'] = None + self.get_listen_key(True) + return + client.reject(message) diff --git a/ccxt/probit.py b/ccxt/probit.py new file mode 100644 index 0000000..d71036c --- /dev/null +++ b/ccxt/probit.py @@ -0,0 +1,1863 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.probit import ImplicitAPI +import math +from ccxt.base.types import Any, Balances, Currencies, Currency, DepositAddress, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import MarketClosed +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidAddress +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import DDoSProtection +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.errors import BadResponse +from ccxt.base.decimal_to_precision import TRUNCATE +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class probit(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(probit, self).describe(), { + 'id': 'probit', + 'name': 'ProBit', + 'countries': ['SC', 'KR'], # Seychelles, South Korea + 'rateLimit': 50, # ms + 'pro': True, + 'has': { + 'CORS': True, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelOrder': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createMarketBuyOrderWithCost': True, + 'createMarketOrder': True, + 'createMarketOrderWithCost': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createReduceOnlyOrder': False, + 'createStopLimitOrder': False, + 'createStopMarketOrder': False, + 'createStopOrder': False, + 'fetchAllGreeks': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': True, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchDepositsWithdrawals': True, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrice': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': False, + 'fetchTransactions': 'emulated', + 'fetchTransfer': False, + 'fetchTransfers': False, + 'fetchUnderlyingAssets': False, + 'fetchVolatilityHistory': False, + 'fetchWithdrawal': False, + 'fetchWithdrawals': True, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'sandbox': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'signIn': True, + 'transfer': False, + 'withdraw': True, + }, + 'timeframes': { + '1m': '1m', + '3m': '3m', + '5m': '5m', + '10m': '10m', + '15m': '15m', + '30m': '30m', + '1h': '1h', + '4h': '4h', + '6h': '6h', + '12h': '12h', + '1d': '1D', + '1w': '1W', + '1M': '1M', + }, + 'version': 'v1', + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/51840849/79268032-c4379480-7ea2-11ea-80b3-dd96bb29fd0d.jpg', + 'api': { + 'accounts': 'https://accounts.probit.com', + 'public': 'https://api.probit.com/api/exchange', + 'private': 'https://api.probit.com/api/exchange', + }, + 'www': 'https://www.probit.com', + 'doc': [ + 'https://docs-en.probit.com', + 'https://docs-ko.probit.com', + ], + 'fees': 'https://support.probit.com/hc/en-us/articles/360020968611-Trading-Fees', + 'referral': 'https://www.probit.com/r/34608773', + }, + 'api': { + 'public': { + 'get': { + 'market': 1, + 'currency': 1, + 'currency_with_platform': 1, + 'time': 1, + 'ticker': 1, + 'order_book': 1, + 'trade': 1, + 'candle': 1, + }, + }, + 'private': { + 'post': { + 'new_order': 2, + 'cancel_order': 1, + 'withdrawal': 2, + }, + 'get': { + 'balance': 1, + 'order': 1, + 'open_order': 1, + 'order_history': 1, + 'trade_history': 1, + 'deposit_address': 1, + 'transfer/payment': 1, + }, + }, + 'accounts': { + 'post': { + 'token': 1, + }, + }, + }, + 'fees': { + 'trading': { + 'tierBased': False, + 'percentage': True, + 'maker': self.parse_number('0.002'), + 'taker': self.parse_number('0.002'), + }, + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': False, + 'triggerDirection': False, + 'triggerPriceType': None, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + # todo + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': False, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 100000, # todo + 'untilDays': 100000, # todo + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 100000, # todo + 'daysBackCanceled': 1, # todo + 'untilDays': 90, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 4000, + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'exceptions': { + 'exact': { + 'UNAUTHORIZED': AuthenticationError, + 'INVALID_ARGUMENT': BadRequest, # Parameters are not a valid format, parameters are empty, or out of range, or a parameter was sent when not required. + 'TRADING_UNAVAILABLE': ExchangeNotAvailable, + 'NOT_ENOUGH_BALANCE': InsufficientFunds, + 'NOT_ALLOWED_COMBINATION': BadRequest, + 'INVALID_ORDER': InvalidOrder, # Requested order does not exist, or it is not your order + 'RATE_LIMIT_EXCEEDED': RateLimitExceeded, # You are sending requests too frequently. Please try it later. + 'MARKET_UNAVAILABLE': ExchangeNotAvailable, # Market is closed today + 'INVALID_MARKET': BadSymbol, # Requested market is not exist + 'MARKET_CLOSED': MarketClosed, # {"errorCode":"MARKET_CLOSED"} + 'MARKET_NOT_FOUND': BadSymbol, # {"errorCode":"MARKET_NOT_FOUND","message":"8e2b8496-0a1e-5beb-b990-a205b902eabe","details":{}} + 'INVALID_CURRENCY': BadRequest, # Requested currency is not exist on ProBit system + 'TOO_MANY_OPEN_ORDERS': DDoSProtection, # Too many open orders + 'DUPLICATE_ADDRESS': InvalidAddress, # Address already exists in withdrawal address list + 'invalid_grant': AuthenticationError, # {"error":"invalid_grant"} + }, + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + }, + 'precisionMode': TICK_SIZE, + 'options': { + 'createMarketBuyOrderRequiresPrice': True, + 'timeInForce': { + 'limit': 'gtc', + 'market': 'ioc', + }, + 'networks': { + 'BEP20': 'BSC', + 'ERC20': 'ETH', + 'TRC20': 'TRON', + }, + }, + 'commonCurrencies': { + 'BB': 'Baby Bali', + 'CBC': 'CryptoBharatCoin', + 'CTK': 'Cryptyk', + 'CTT': 'Castweet', + 'DKT': 'DAKOTA', + 'EGC': 'EcoG9coin', + 'EPS': 'Epanus', # conflict with EPS Ellipsis https://github.com/ccxt/ccxt/issues/8909 + 'FX': 'Fanzy', + 'GM': 'GM Holding', + 'GOGOL': 'GOL', + 'GOL': 'Goldofir', + 'HUSL': 'The Hustle App', + 'LAND': 'Landbox', + 'SST': 'SocialSwap', + 'TCT': 'Top Coin Token', + 'TOR': 'Torex', + 'UNI': 'UNICORN Token', + 'UNISWAP': 'UNI', + }, + }) + + def fetch_markets(self, params={}) -> List[Market]: + """ + + https://docs-en.probit.com/reference/market + + retrieves data on all markets for probit + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = self.publicGetMarket(params) + # + # { + # "data":[ + # { + # "id":"MONA-USDT", + # "base_currency_id":"MONA", + # "quote_currency_id":"USDT", + # "min_price":"0.001", + # "max_price":"9999999999999999", + # "price_increment":"0.001", + # "min_quantity":"0.0001", + # "max_quantity":"9999999999999999", + # "quantity_precision":4, + # "min_cost":"1", + # "max_cost":"9999999999999999", + # "cost_precision":8, + # "taker_fee_rate":"0.2", + # "maker_fee_rate":"0.2", + # "show_in_ui":true, + # "closed":false + # }, + # ] + # } + # + markets = self.safe_value(response, 'data', []) + return self.parse_markets(markets) + + def parse_market(self, market: dict) -> Market: + id = self.safe_string(market, 'id') + baseId = self.safe_string(market, 'base_currency_id') + quoteId = self.safe_string(market, 'quote_currency_id') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + closed = self.safe_bool(market, 'closed', False) + showInUI = self.safe_bool(market, 'show_in_ui', True) + active = not closed and showInUI + takerFeeRate = self.safe_string(market, 'taker_fee_rate') + taker = Precise.string_div(takerFeeRate, '100') + makerFeeRate = self.safe_string(market, 'maker_fee_rate') + maker = Precise.string_div(makerFeeRate, '100') + return { + 'id': id, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': active, + 'contract': False, + 'linear': None, + 'inverse': None, + 'taker': self.parse_number(taker), + 'maker': self.parse_number(maker), + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(self.parse_precision(self.safe_string(market, 'quantity_precision'))), + 'price': self.safe_number(market, 'price_increment'), + 'cost': self.parse_number(self.parse_precision(self.safe_string(market, 'cost_precision'))), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(market, 'min_quantity'), + 'max': self.safe_number(market, 'max_quantity'), + }, + 'price': { + 'min': self.safe_number(market, 'min_price'), + 'max': self.safe_number(market, 'max_price'), + }, + 'cost': { + 'min': self.safe_number(market, 'min_cost'), + 'max': self.safe_number(market, 'max_cost'), + }, + }, + 'created': None, + 'info': market, + } + + def fetch_currencies(self, params={}) -> Currencies: + """ + + https://docs-en.probit.com/reference/currency + + fetches all available currencies on an exchange + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = self.publicGetCurrencyWithPlatform(params) + # + # { + # "data":[ + # { + # "id":"USDT", + # "display_name":{"ko-kr":"테더","en-us":"Tether"}, + # "show_in_ui":true, + # "platform":[ + # { + # "id":"ETH", + # "priority":1, + # "deposit":true, + # "withdrawal":true, + # "currency_id":"USDT", + # "precision":6, + # "min_confirmation_count":15, + # "require_destination_tag":false, + # "display_name":{"name":{"ko-kr":"ERC-20","en-us":"ERC-20"}}, + # "min_deposit_amount":"0", + # "min_withdrawal_amount":"1", + # "withdrawal_fee":[ + # {"amount":"0.01","priority":2,"currency_id":"ETH"}, + # {"amount":"1.5","priority":1,"currency_id":"USDT"}, + # ], + # "deposit_fee":{}, + # "suspended_reason":"", + # "deposit_suspended":false, + # "withdrawal_suspended":false + # }, + # { + # "id":"OMNI", + # "priority":2, + # "deposit":true, + # "withdrawal":true, + # "currency_id":"USDT", + # "precision":6, + # "min_confirmation_count":3, + # "require_destination_tag":false, + # "display_name":{"name":{"ko-kr":"OMNI","en-us":"OMNI"}}, + # "min_deposit_amount":"0", + # "min_withdrawal_amount":"5", + # "withdrawal_fee":[{"amount":"5","priority":1,"currency_id":"USDT"}], + # "deposit_fee":{}, + # "suspended_reason":"wallet_maintenance", + # "deposit_suspended":false, + # "withdrawal_suspended":false + # } + # ], + # "stakeable":false, + # "unstakeable":false, + # "auto_stake":false, + # "auto_stake_amount":"0" + # } + # ] + # } + # + currencies = self.safe_list(response, 'data', []) + result: dict = {} + for i in range(0, len(currencies)): + currency = currencies[i] + id = self.safe_string(currency, 'id') + code = self.safe_currency_code(id) + displayName = self.safe_dict(currency, 'display_name') + name = self.safe_string(displayName, 'en-us') + platforms = self.safe_list(currency, 'platform', []) + platformsByPriority = self.sort_by(platforms, 'priority') + networkList: dict = {} + for j in range(0, len(platformsByPriority)): + network = platformsByPriority[j] + idInner = self.safe_string(network, 'id') + networkCode = self.network_id_to_code(idInner) + withdrawFee = self.safe_list(network, 'withdrawal_fee', []) + networkFee = self.safe_dict(withdrawFee, 0, {}) + for k in range(0, len(withdrawFee)): + withdrawPlatform = withdrawFee[k] + feeCurrencyId = self.safe_string(withdrawPlatform, 'currency_id') + if feeCurrencyId == id: + networkFee = withdrawPlatform + break + networkList[networkCode] = { + 'id': idInner, + 'network': networkCode, + 'active': None, + 'deposit': not self.safe_bool(network, 'deposit_suspended'), + 'withdraw': not self.safe_bool(network, 'withdrawal_suspended'), + 'fee': self.safe_number(networkFee, 'amount'), + 'precision': self.parse_number(self.parse_precision(self.safe_string(network, 'precision'))), + 'limits': { + 'withdraw': { + 'min': self.safe_number(network, 'min_withdrawal_amount'), + 'max': None, + }, + 'deposit': { + 'min': self.safe_number(network, 'min_deposit_amount'), + 'max': None, + }, + }, + 'info': network, + } + result[code] = self.safe_currency_structure({ + 'id': id, + 'code': code, + 'info': currency, + 'name': name, + 'active': None, + 'deposit': None, + 'withdraw': None, + 'type': 'crypto', + 'fee': None, + 'precision': None, + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'deposit': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + }, + 'networks': networkList, + }) + return result + + def parse_balance(self, response) -> Balances: + result: dict = { + 'info': response, + 'timestamp': None, + 'datetime': None, + } + data = self.safe_value(response, 'data', []) + for i in range(0, len(data)): + balance = data[i] + currencyId = self.safe_string(balance, 'currency_id') + code = self.safe_currency_code(currencyId) + account = self.account() + account['total'] = self.safe_string(balance, 'total') + account['free'] = self.safe_string(balance, 'available') + result[code] = account + return self.safe_balance(result) + + def fetch_balance(self, params={}) -> Balances: + """ + + https://docs-en.probit.com/reference/balance + + query for balance and get the amount of funds available for trading or funds locked in orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + response = self.privateGetBalance(params) + # + # { + # "data": [ + # { + # "currency_id":"XRP", + # "total":"100", + # "available":"0", + # } + # ] + # } + # + return self.parse_balance(response) + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + + https://docs-en.probit.com/reference/order_book + + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'market_id': market['id'], + } + response = self.publicGetOrderBook(self.extend(request, params)) + # + # { + # data: [ + # {side: 'buy', price: '0.000031', quantity: '10'}, + # {side: 'buy', price: '0.00356007', quantity: '4.92156877'}, + # {side: 'sell', price: '0.1857', quantity: '0.17'}, + # ] + # } + # + data = self.safe_value(response, 'data', []) + dataBySide = self.group_by(data, 'side') + return self.parse_order_book(dataBySide, market['symbol'], None, 'buy', 'sell', 'price', 'quantity') + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + + https://docs-en.probit.com/reference/ticker + + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + request: dict = {} + if symbols is not None: + marketIds = self.market_ids(symbols) + request['market_ids'] = ','.join(marketIds) + response = self.publicGetTicker(self.extend(request, params)) + # + # { + # "data":[ + # { + # "last":"0.022902", + # "low":"0.021693", + # "high":"0.024093", + # "change":"-0.000047", + # "base_volume":"15681.986", + # "quote_volume":"360.514403624", + # "market_id":"ETH-BTC", + # "time":"2020-04-12T18:43:38.000Z" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_tickers(data, symbols) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + + https://docs-en.probit.com/reference/ticker + + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'market_ids': market['id'], + } + response = self.publicGetTicker(self.extend(request, params)) + # + # { + # "data":[ + # { + # "last":"0.022902", + # "low":"0.021693", + # "high":"0.024093", + # "change":"-0.000047", + # "base_volume":"15681.986", + # "quote_volume":"360.514403624", + # "market_id":"ETH-BTC", + # "time":"2020-04-12T18:43:38.000Z" + # } + # ] + # } + # + data = self.safe_value(response, 'data', []) + ticker = self.safe_value(data, 0) + if ticker is None: + raise BadResponse(self.id + ' fetchTicker() returned an empty response') + return self.parse_ticker(ticker, market) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "last":"0.022902", + # "low":"0.021693", + # "high":"0.024093", + # "change":"-0.000047", + # "base_volume":"15681.986", + # "quote_volume":"360.514403624", + # "market_id":"ETH-BTC", + # "time":"2020-04-12T18:43:38.000Z" + # } + # + timestamp = self.parse8601(self.safe_string(ticker, 'time')) + marketId = self.safe_string(ticker, 'market_id') + symbol = self.safe_symbol(marketId, market, '-') + close = self.safe_string(ticker, 'last') + change = self.safe_string(ticker, 'change') + baseVolume = self.safe_string(ticker, 'base_volume') + quoteVolume = self.safe_string(ticker, 'quote_volume') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': None, + 'bidVolume': None, + 'ask': None, + 'askVolume': None, + 'vwap': None, + 'open': None, + 'close': close, + 'last': close, + 'previousClose': None, # previous day close + 'change': change, + 'percentage': None, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'info': ticker, + }, market) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + + https://docs-en.probit.com/reference/trade + + fetch all trades made by the user + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market: Market = None + now = self.milliseconds() + request: dict = { + 'limit': 100, + 'start_time': self.iso8601(now - 31536000000), # -365 days + 'end_time': self.iso8601(now), + } + if symbol is not None: + market = self.market(symbol) + request['market_id'] = market['id'] + if since is not None: + request['start_time'] = self.iso8601(since) + request['end_time'] = self.iso8601(min(now, since + 31536000000)) + if limit is not None: + request['limit'] = limit + response = self.privateGetTradeHistory(self.extend(request, params)) + # + # { + # "data": [ + # { + # "id":"BTC-USDT:183566", + # "order_id":"17209376", + # "side":"sell", + # "fee_amount":"0.657396569175", + # "fee_currency_id":"USDT", + # "status":"settled", + # "price":"6573.96569175", + # "quantity":"0.1", + # "cost":"657.396569175", + # "time":"2018-08-10T06:06:46.000Z", + # "market_id":"BTC-USDT" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_trades(data, market, since, limit) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + + https://docs-en.probit.com/reference/trade-1 + + get the list of most recent trades for a particular symbol + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'market_id': market['id'], + 'start_time': '1970-01-01T00:00:00.000Z', + 'end_time': self.iso8601(self.milliseconds()), + } + if since is not None: + request['start_time'] = self.iso8601(since) + if limit is not None: + request['limit'] = min(limit, 1000) + else: + request['limit'] = 1000 # required to set any value + response = self.publicGetTrade(self.extend(request, params)) + # + # { + # "data":[ + # { + # "id":"ETH-BTC:3331886", + # "price":"0.022982", + # "quantity":"12.337", + # "time":"2020-04-12T20:55:42.371Z", + # "side":"sell", + # "tick_direction":"down" + # }, + # { + # "id":"ETH-BTC:3331885", + # "price":"0.022982", + # "quantity":"6.472", + # "time":"2020-04-12T20:55:39.652Z", + # "side":"sell", + # "tick_direction":"down" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_trades(data, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(public) + # + # { + # "id":"ETH-BTC:3331886", + # "price":"0.022981", + # "quantity":"12.337", + # "time":"2020-04-12T20:55:42.371Z", + # "side":"sell", + # "tick_direction":"down" + # } + # + # fetchMyTrades(private) + # + # { + # "id":"BTC-USDT:183566", + # "order_id":"17209376", + # "side":"sell", + # "fee_amount":"0.657396569175", + # "fee_currency_id":"USDT", + # "status":"settled", + # "price":"6573.96569175", + # "quantity":"0.1", + # "cost":"657.396569175", + # "time":"2018-08-10T06:06:46.000Z", + # "market_id":"BTC-USDT" + # } + # + timestamp = self.parse8601(self.safe_string(trade, 'time')) + id = self.safe_string(trade, 'id') + marketId: Str = None + if id is not None: + parts = id.split(':') + marketId = self.safe_string(parts, 0) + marketId = self.safe_string(trade, 'market_id', marketId) + symbol = self.safe_symbol(marketId, market, '-') + side = self.safe_string(trade, 'side') + priceString = self.safe_string(trade, 'price') + amountString = self.safe_string(trade, 'quantity') + orderId = self.safe_string(trade, 'order_id') + feeCostString = self.safe_string(trade, 'fee_amount') + fee = None + if feeCostString is not None: + feeCurrencyId = self.safe_string(trade, 'fee_currency_id') + feeCurrencyCode = self.safe_currency_code(feeCurrencyId) + fee = { + 'cost': feeCostString, + 'currency': feeCurrencyCode, + } + return self.safe_trade({ + 'id': id, + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'order': orderId, + 'type': None, + 'side': side, + 'takerOrMaker': None, + 'price': priceString, + 'amount': amountString, + 'cost': None, + 'fee': fee, + }, market) + + def fetch_time(self, params={}) -> Int: + """ + + https://docs-en.probit.com/reference/time + + fetches the current integer timestamp in milliseconds from the exchange server + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = self.publicGetTime(params) + # + # {"data":"2020-04-12T18:54:25.390Z"} + # + timestamp = self.parse8601(self.safe_string(response, 'data')) + return timestamp + + def normalize_ohlcv_timestamp(self, timestamp, timeframe, after=False): + duration = self.parse_timeframe(timeframe) + if timeframe == '1M': + iso8601 = self.iso8601(timestamp) + parts = iso8601.split('-') + year = self.safe_string(parts, 0) + month = self.safe_integer(parts, 1) + monthString: Str = None + if after: + monthString = self.sum(month, str(1)) + if month < 10: + monthString = '0' + str(month) + return year + '-' + monthString + '-01T00:00:00.000Z' + elif timeframe == '1w': + timestamp = self.parse_to_int(timestamp / 1000) + firstSunday = 259200 # 1970-01-04T00:00:00.000Z + difference = timestamp - firstSunday + numWeeks = int(math.floor(difference / duration)) + previousSunday = self.sum(firstSunday, numWeeks * duration) + if after: + previousSunday = self.sum(previousSunday, duration) + return self.iso8601(previousSunday * 1000) + else: + timestamp = self.parse_to_int(timestamp / 1000) + timestamp = duration * self.parse_to_int(timestamp / duration) + if after: + timestamp = self.sum(timestamp, duration) + return self.iso8601(timestamp * 1000) + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + + https://docs-en.probit.com/reference/candle + + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.until]: timestamp in ms of the earliest candle to fetch + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + market = self.market(symbol) + interval = self.safe_string(self.timeframes, timeframe, timeframe) + limit = 100 if (limit is None) else limit + requestLimit = self.sum(limit, 1) + requestLimit = min(1000, requestLimit) # max 1000 + request: dict = { + 'market_ids': market['id'], + 'interval': interval, + 'sort': 'asc', # 'asc' will always include the start_time, 'desc' will always include end_time + 'limit': requestLimit, # max 1000 + } + now = self.milliseconds() + until = self.safe_integer(params, 'until') + durationMilliseconds = self.parse_timeframe(timeframe) * 1000 + startTime = since + endTime = until - durationMilliseconds if (until is not None) else now + if since is None: + if limit is None: + limit = requestLimit + startLimit = limit - 1 + startTime = endTime - startLimit * durationMilliseconds + else: + if limit is not None: + endByLimit = self.sum(since, limit * durationMilliseconds) + endTime = min(endTime, endByLimit) + startTimeNormalized = self.normalize_ohlcv_timestamp(startTime, timeframe) + endTimeNormalized = self.normalize_ohlcv_timestamp(endTime, timeframe, True) + request['start_time'] = startTimeNormalized + request['end_time'] = endTimeNormalized + response = self.publicGetCandle(self.extend(request, params)) + # + # { + # "data":[ + # { + # "market_id":"ETH-BTC", + # "open":"0.02811", + # "close":"0.02811", + # "low":"0.02811", + # "high":"0.02811", + # "base_volume":"0.0005", + # "quote_volume":"0.000014055", + # "start_time":"2018-11-30T18:19:00.000Z", + # "end_time":"2018-11-30T18:20:00.000Z" + # }, + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_ohlcvs(data, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # { + # "market_id":"ETH-BTC", + # "open":"0.02811", + # "close":"0.02811", + # "low":"0.02811", + # "high":"0.02811", + # "base_volume":"0.0005", + # "quote_volume":"0.000014055", + # "start_time":"2018-11-30T18:19:00.000Z", + # "end_time":"2018-11-30T18:20:00.000Z" + # } + # + return [ + self.parse8601(self.safe_string(ohlcv, 'start_time')), + self.safe_number(ohlcv, 'open'), + self.safe_number(ohlcv, 'high'), + self.safe_number(ohlcv, 'low'), + self.safe_number(ohlcv, 'close'), + self.safe_number(ohlcv, 'base_volume'), + ] + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + + https://docs-en.probit.com/reference/open_order-1 + + fetch all unfilled currently open orders + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + since = self.parse8601(since) + request: dict = {} + market: Market = None + if symbol is not None: + market = self.market(symbol) + request['market_id'] = market['id'] + response = self.privateGetOpenOrder(self.extend(request, params)) + data = self.safe_list(response, 'data') + return self.parse_orders(data, market, since, limit) + + def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + + https://docs-en.probit.com/reference/order + + fetches information on multiple closed orders made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + request: dict = { + 'start_time': self.iso8601(0), + 'end_time': self.iso8601(self.milliseconds()), + 'limit': 100, + } + market: Market = None + if symbol is not None: + market = self.market(symbol) + request['market_id'] = market['id'] + if since: + request['start_time'] = self.iso8601(since) + if limit: + request['limit'] = limit + response = self.privateGetOrderHistory(self.extend(request, params)) + data = self.safe_list(response, 'data') + return self.parse_orders(data, market, since, limit) + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + + https://docs-en.probit.com/reference/order-3 + + fetches information on an order made by the user + :param str id: the order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrder() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'market_id': market['id'], + } + clientOrderId = self.safe_string_2(params, 'clientOrderId', 'client_order_id') + if clientOrderId is not None: + request['client_order_id'] = clientOrderId + else: + request['order_id'] = id + query = self.omit(params, ['clientOrderId', 'client_order_id']) + response = self.privateGetOrder(self.extend(request, query)) + data = self.safe_value(response, 'data', []) + order = self.safe_dict(data, 0) + return self.parse_order(order, market) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'open': 'open', + 'cancelled': 'canceled', + 'filled': 'closed', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # { + # id, + # user_id, + # market_id, + # "type": "orderType", + # "side": "side", + # quantity, + # limit_price, + # "time_in_force": "timeInForce", + # filled_cost, + # filled_quantity, + # open_quantity, + # cancelled_quantity, + # "status": "orderStatus", + # "time": "date", + # client_order_id, + # } + # + status = self.parse_order_status(self.safe_string(order, 'status')) + id = self.safe_string(order, 'id') + type = self.safe_string(order, 'type') + side = self.safe_string(order, 'side') + marketId = self.safe_string(order, 'market_id') + symbol = self.safe_symbol(marketId, market, '-') + timestamp = self.parse8601(self.safe_string(order, 'time')) + price = self.safe_string(order, 'limit_price') + filled = self.safe_string(order, 'filled_quantity') + remaining = self.safe_string(order, 'open_quantity') + canceledAmount = self.safe_string(order, 'cancelled_quantity') + if canceledAmount is not None: + remaining = Precise.string_add(remaining, canceledAmount) + amount = self.safe_string(order, 'quantity', Precise.string_add(filled, remaining)) + cost = self.safe_string_2(order, 'filled_cost', 'cost') + if type == 'market': + price = None + clientOrderId = self.safe_string(order, 'client_order_id') + timeInForce = self.safe_string_upper(order, 'time_in_force') + return self.safe_order({ + 'id': id, + 'info': order, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'symbol': symbol, + 'type': type, + 'timeInForce': timeInForce, + 'side': side, + 'status': status, + 'price': price, + 'triggerPrice': None, + 'amount': amount, + 'filled': filled, + 'remaining': remaining, + 'average': None, + 'cost': cost, + 'fee': None, + 'trades': None, + }, market) + + def cost_to_precision(self, symbol, cost): + return self.decimal_to_precision(cost, TRUNCATE, self.markets[symbol]['precision']['cost'], self.precisionMode) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://docs-en.probit.com/reference/order-1 + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.cost]: the quote quantity that can be used alternative for the amount for market buy orders + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + options = self.safe_value(self.options, 'timeInForce') + defaultTimeInForce = self.safe_value(options, type) + timeInForce = self.safe_string_2(params, 'timeInForce', 'time_in_force', defaultTimeInForce) + request: dict = { + 'market_id': market['id'], + 'type': type, + 'side': side, + 'time_in_force': timeInForce, + } + clientOrderId = self.safe_string_2(params, 'clientOrderId', 'client_order_id') + if clientOrderId is not None: + request['client_order_id'] = clientOrderId + quoteAmount = None + if type == 'limit': + request['limit_price'] = self.price_to_precision(symbol, price) + request['quantity'] = self.amount_to_precision(symbol, amount) + elif type == 'market': + # for market buy it requires the amount of quote currency to spend + if side == 'buy': + createMarketBuyOrderRequiresPrice = True + createMarketBuyOrderRequiresPrice, params = self.handle_option_and_params(params, 'createOrder', 'createMarketBuyOrderRequiresPrice', True) + cost = self.safe_string(params, 'cost') + params = self.omit(params, 'cost') + if cost is not None: + quoteAmount = self.cost_to_precision(symbol, cost) + elif createMarketBuyOrderRequiresPrice: + if price is None: + raise InvalidOrder(self.id + ' createOrder() requires the price argument for market buy orders to calculate the total cost to spend(amount * price), alternatively set the createMarketBuyOrderRequiresPrice option or param to False and pass the cost to spend in the amount argument') + else: + amountString = self.number_to_string(amount) + priceString = self.number_to_string(price) + costRequest = Precise.string_mul(amountString, priceString) + quoteAmount = self.cost_to_precision(symbol, costRequest) + else: + quoteAmount = self.cost_to_precision(symbol, amount) + request['cost'] = quoteAmount + else: + request['quantity'] = self.amount_to_precision(symbol, amount) + query = self.omit(params, ['timeInForce', 'time_in_force', 'clientOrderId', 'client_order_id']) + response = self.privatePostNewOrder(self.extend(request, query)) + # + # { + # "data": { + # id, + # user_id, + # market_id, + # "type": "orderType", + # "side": "side", + # quantity, + # limit_price, + # "time_in_force": "timeInForce", + # filled_cost, + # filled_quantity, + # open_quantity, + # cancelled_quantity, + # "status": "orderStatus", + # "time": "date", + # client_order_id, + # } + # } + # + data = self.safe_value(response, 'data') + order = self.parse_order(data, market) + # a workaround for incorrect huge amounts + # returned by the exchange on market buys + if (type == 'market') and (side == 'buy'): + order['amount'] = None + order['cost'] = self.parse_number(quoteAmount) + order['remaining'] = None + return order + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + + https://docs-en.probit.com/reference/order-2 + + cancels an open order + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'market_id': market['id'], + 'order_id': id, + } + response = self.privatePostCancelOrder(self.extend(request, params)) + data = self.safe_dict(response, 'data') + return self.parse_order(data) + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + address = self.safe_string(depositAddress, 'address') + tag = self.safe_string(depositAddress, 'destination_tag') + currencyId = self.safe_string(depositAddress, 'currency_id') + currency = self.safe_currency(currencyId, currency) + code = currency['code'] + network = self.safe_string(depositAddress, 'platform_id') + self.check_address(address) + return { + 'info': depositAddress, + 'currency': code, + 'network': network, + 'address': address, + 'tag': tag, + } + + def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + + https://docs-en.probit.com/reference/deposit_address + + fetch the deposit address for a currency associated with self account + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency_id': currency['id'], + # 'platform_id': 'TRON',(undocumented) + } + networks = self.safe_value(self.options, 'networks', {}) + network = self.safe_string_upper(params, 'network') # self line allows the user to specify either ERC20 or ETH + network = self.safe_string(networks, network, network) # handle ERC20>ETH alias + if network is not None: + request['platform_id'] = network + params = self.omit(params, 'platform_id') + response = self.privateGetDepositAddress(self.extend(request, params)) + # + # without 'platform_id' + # { + # "data":[ + # { + # "currency_id":"ETH", + # "address":"0x12e2caf3c4051ba1146e612f532901a423a9898a", + # "destination_tag":null + # } + # ] + # } + # + # with 'platform_id' + # { + # "data":[ + # { + # "platform_id":"TRON", + # "address":"TDQLMxBTa6MzuoZ6deSGZkqET3Ek8v7uC6", + # "destination_tag":null + # } + # ] + # } + # + data = self.safe_value(response, 'data', []) + firstAddress = self.safe_value(data, 0) + if firstAddress is None: + raise InvalidAddress(self.id + ' fetchDepositAddress() returned an empty response') + return self.parse_deposit_address(firstAddress, currency) + + def fetch_deposit_addresses(self, codes: Strings = None, params={}) -> List[DepositAddress]: + """ + + https://docs-en.probit.com/reference/deposit_address + + fetch deposit addresses for multiple currencies and chain types + :param str[]|None codes: list of unified currency codes, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `address structures ` + """ + self.load_markets() + request: dict = {} + if codes: + currencyIds = [] + for i in range(0, len(codes)): + currency = self.currency(codes[i]) + currencyIds.append(currency['id']) + request['currency_id'] = ','.join(codes) + response = self.privateGetDepositAddress(self.extend(request, params)) + data = self.safe_list(response, 'data', []) + return self.parse_deposit_addresses(data, codes) + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + + https://docs-en.probit.com/reference/withdrawal + + make a withdrawal + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + # In order to use self method + # you need to allow API withdrawal from the API Settings Page, and + # and register the list of withdrawal addresses and destination tags on the API Settings page + # you can only withdraw to the registered addresses using the API + self.check_address(address) + self.load_markets() + currency = self.currency(code) + if tag is None: + tag = '' + request: dict = { + 'currency_id': currency['id'], + # 'platform_id': 'ETH', # if omitted it will use the default platform for the currency + 'address': address, + 'destination_tag': tag, + 'amount': self.number_to_string(amount), + # which currency to pay the withdrawal fees + # only applicable for currencies that accepts multiple withdrawal fee options + # 'fee_currency_id': 'ETH', # if omitted it will use the default fee policy for each currency + # whether the amount field includes fees + # 'include_fee': False, # makes sense only when fee_currency_id is equal to currency_id + } + networks = self.safe_value(self.options, 'networks', {}) + network = self.safe_string_upper(params, 'network') # self line allows the user to specify either ERC20 or ETH + network = self.safe_string(networks, network, network) # handle ERC20>ETH alias + if network is not None: + request['platform_id'] = network + params = self.omit(params, 'network') + response = self.privatePostWithdrawal(self.extend(request, params)) + data = self.safe_dict(response, 'data') + return self.parse_transaction(data, currency) + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of transaction structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + request: dict = { + 'type': 'deposit', + } + result = self.fetch_transactions(code, since, limit, self.extend(request, params)) + return result + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made to an account + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of transaction structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + request: dict = { + 'type': 'withdrawal', + } + result = self.fetch_transactions(code, since, limit, self.extend(request, params)) + return result + + def fetch_deposits_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch history of deposits and withdrawals + + https://docs-en.probit.com/reference/transferpayment + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch transactions for + :param int [limit]: the maximum number of transaction structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch transactions for + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + currency: Currency = None + request: dict = {} + if code is not None: + currency = self.currency(code) + request['currency_id'] = currency['id'] + if since is not None: + request['start_time'] = self.iso8601(since) + else: + request['start_time'] = self.iso8601(1) + until = self.safe_integer(params, 'until') + if until is not None: + request['end_time'] = self.iso8601(until) + params = self.omit(params, ['until']) + else: + request['end_time'] = self.iso8601(self.milliseconds()) + if limit is not None: + request['limit'] = limit + else: + request['limit'] = 100 + response = self.privateGetTransferPayment(self.extend(request, params)) + # + # { + # "data": [ + # { + # "id": "01211d4b-0e68-41d6-97cb-298bfe2cab67", + # "type": "deposit", + # "status": "done", + # "amount": "0.01", + # "address": "0x9e7430fc0bdd14745bd00a1b92ed25133a7c765f", + # "time": "2023-06-14T12:03:11.000Z", + # "hash": "0x0ff5bedc9e378f9529acc6b9840fa8c2ef00fd0275e0bac7fa0589a9b5d1712e", + # "currency_id": "ETH", + # "confirmations":0, + # "fee": "0", + # "destination_tag": null, + # "platform_id": "ETH", + # "fee_currency_id": "ETH", + # "payment_service_name":null, + # "payment_service_display_name":null, + # "crypto":null + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_transactions(data, currency, since, limit) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # { + # "id": "01211d4b-0e68-41d6-97cb-298bfe2cab67", + # "type": "deposit", + # "status": "done", + # "amount": "0.01", + # "address": "0x9e7430fc0bdd14745bd00a1b92ed25133a7c765f", + # "time": "2023-06-14T12:03:11.000Z", + # "hash": "0x0ff5bedc9e378f9529acc6b9840fa8c2ef00fd0275e0bac7fa0589a9b5d1712e", + # "currency_id": "ETH", + # "confirmations":0, + # "fee": "0", + # "destination_tag": null, + # "platform_id": "ETH", + # "fee_currency_id": "ETH", + # "payment_service_name":null, + # "payment_service_display_name":null, + # "crypto":null + # } + # + id = self.safe_string(transaction, 'id') + networkId = self.safe_string(transaction, 'platform_id') + networkCode = self.network_id_to_code(networkId) + amount = self.safe_number(transaction, 'amount') + address = self.safe_string(transaction, 'address') + tag = self.safe_string(transaction, 'destination_tag') + txid = self.safe_string(transaction, 'hash') + timestamp = self.parse8601(self.safe_string(transaction, 'time')) + type = self.safe_string(transaction, 'type') + currencyId = self.safe_string(transaction, 'currency_id') + code = self.safe_currency_code(currencyId) + status = self.parse_transaction_status(self.safe_string(transaction, 'status')) + feeCostString = self.safe_string(transaction, 'fee') + fee = None + if feeCostString is not None and feeCostString != '0': + fee = { + 'currency': code, + 'cost': self.parse_number(feeCostString), + } + return { + 'id': id, + 'currency': code, + 'amount': amount, + 'network': networkCode, + 'addressFrom': None, + 'address': address, + 'addressTo': address, + 'tagFrom': None, + 'tag': tag, + 'tagTo': tag, + 'status': status, + 'type': type, + 'txid': txid, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'updated': None, + 'internal': None, + 'comment': None, + 'fee': fee, + 'info': transaction, + } + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'requested': 'pending', + 'pending': 'pending', + 'confirming': 'pending', + 'confirmed': 'pending', + 'applying': 'pending', + 'done': 'ok', + 'cancelled': 'canceled', + 'cancelling': 'canceled', + } + return self.safe_string(statuses, status, status) + + def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}): + """ + + https://docs-en.probit.com/reference/currency + + fetch deposit and withdraw fees + :param str[]|None codes: list of unified currency codes + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `fees structures ` + """ + self.load_markets() + response = self.publicGetCurrencyWithPlatform(params) + # + # { + # "data": [ + # { + # "id": "AFX", + # "display_name": { + # "ko-kr": "아프릭스", + # "en-us": "Afrix" + # }, + # "show_in_ui": True, + # "platform": [ + # { + # "id": "ZYN", + # "priority": 1, + # "deposit": True, + # "withdrawal": True, + # "currency_id": "AFX", + # "precision": 18, + # "min_confirmation_count": 60, + # "require_destination_tag": False, + # "allow_withdrawal_destination_tag": False, + # "display_name": { + # "name": { + # "ko-kr": "지네코인", + # "en-us": "Wethio" + # } + # }, + # "min_deposit_amount": "0", + # "min_withdrawal_amount": "0", + # "withdrawal_fee": [ + # { + # "currency_id": "ZYN", + # "amount": "0.5", + # "priority": 1 + # } + # ], + # "deposit_fee": {}, + # "suspended_reason": "", + # "deposit_suspended": False, + # "withdrawal_suspended": False, + # "platform_currency_display_name": {} + # } + # ], + # "internal_transfer": { + # "suspended_reason": null, + # "suspended": False + # }, + # "stakeable": False, + # "unstakeable": False, + # "auto_stake": False, + # "auto_stake_amount": "0" + # }, + # ] + # } + # + data = self.safe_list(response, 'data') + return self.parse_deposit_withdraw_fees(data, codes, 'id') + + def parse_deposit_withdraw_fee(self, fee, currency: Currency = None): + # + # { + # "id": "USDT", + # "display_name": {"ko-kr": '테더', "en-us": "Tether"}, + # "show_in_ui": True, + # "platform": [ + # { + # "id": "ETH", + # "priority": "1", + # "deposit": True, + # "withdrawal": True, + # "currency_id": "USDT", + # "precision": "6", + # "min_confirmation_count": "15", + # "require_destination_tag": False, + # "allow_withdrawal_destination_tag": False, + # "display_name": [Object], + # "min_deposit_amount": "0", + # "min_withdrawal_amount": "1", + # "withdrawal_fee": [Array], + # "deposit_fee": {}, + # "suspended_reason": '', + # "deposit_suspended": False, + # "withdrawal_suspended": False, + # "platform_currency_display_name": [Object] + # }, + # ], + # "internal_transfer": {suspended_reason: null, suspended: False}, + # "stakeable": False, + # "unstakeable": False, + # "auto_stake": False, + # "auto_stake_amount": "0" + # } + # + depositWithdrawFee = self.deposit_withdraw_fee({}) + platforms = self.safe_value(fee, 'platform', []) + depositResult: dict = { + 'fee': None, + 'percentage': None, + } + for i in range(0, len(platforms)): + network = platforms[i] + networkId = self.safe_string(network, 'id') + networkCode = self.network_id_to_code(networkId, currency['code']) + withdrawalFees = self.safe_value(network, 'withdrawal_fee', {}) + withdrawFee = self.safe_number(withdrawalFees[0], 'amount') + if len(withdrawalFees): + withdrawResult: dict = { + 'fee': withdrawFee, + 'percentage': False if (withdrawFee is not None) else None, + } + if i == 0: + depositWithdrawFee['withdraw'] = withdrawResult + depositWithdrawFee['networks'][networkCode] = { + 'withdraw': withdrawResult, + 'deposit': depositResult, + } + depositWithdrawFee['info'] = fee + return depositWithdrawFee + + def nonce(self): + return self.milliseconds() + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = self.urls['api'][api] + '/' + query = self.omit(params, self.extract_params(path)) + if api == 'accounts': + self.check_required_credentials() + url += self.implode_params(path, params) + auth = self.apiKey + ':' + self.secret + auth64 = self.string_to_base64(auth) + headers = { + 'Authorization': 'Basic ' + auth64, + 'Content-Type': 'application/json', + } + if query: + body = self.json(query) + else: + url += self.version + '/' + if api == 'public': + url += self.implode_params(path, params) + if query: + url += '?' + self.urlencode(query) + elif api == 'private': + now = self.milliseconds() + self.check_required_credentials() + expires = self.safe_integer(self.options, 'expires') + if (expires is None) or (expires < now): + raise AuthenticationError(self.id + ' access token expired, call signIn() method') + accessToken = self.safe_string(self.options, 'accessToken') + headers = { + 'Authorization': 'Bearer ' + accessToken, + } + url += self.implode_params(path, params) + if method == 'GET': + if query: + url += '?' + self.urlencode(query) + elif query: + body = self.json(query) + headers['Content-Type'] = 'application/json' + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def sign_in(self, params={}): + """ + + https://docs-en.probit.com/reference/token + + sign in, must be called prior to using other authenticated methods + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns: response from exchange + """ + self.check_required_credentials() + request: dict = { + 'grant_type': 'client_credentials', # the only supported value + } + response = self.accountsPostToken(self.extend(request, params)) + # + # { + # "access_token": "0ttDv/2hTTn3bLi8GP1gKaneiEQ6+0hOBenPrxNQt2s=", + # "token_type": "bearer", + # "expires_in": 900 + # } + # + expiresIn = self.safe_integer(response, 'expires_in') + accessToken = self.safe_string(response, 'access_token') + self.options['accessToken'] = accessToken + self.options['expires'] = self.sum(self.milliseconds(), expiresIn * 1000) + return response + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None # fallback to default error handler + if 'errorCode' in response: + errorCode = self.safe_string(response, 'errorCode') + if errorCode is not None: + errMessage = self.safe_string(response, 'message', '') + details = self.safe_value(response, 'details') + feedback = self.id + ' ' + errorCode + ' ' + errMessage + ' ' + self.json(details) + if 'exact' in self.exceptions: + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + if 'broad' in self.exceptions: + self.throw_broadly_matched_exception(self.exceptions['broad'], errMessage, feedback) + raise ExchangeError(feedback) + return None diff --git a/ccxt/protobuf/__init__.py b/ccxt/protobuf/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ccxt/protobuf/__pycache__/__init__.cpython-311.pyc b/ccxt/protobuf/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..9f3747e Binary files /dev/null and b/ccxt/protobuf/__pycache__/__init__.cpython-311.pyc differ diff --git a/ccxt/protobuf/mexc/PrivateAccountV3Api_pb2.py b/ccxt/protobuf/mexc/PrivateAccountV3Api_pb2.py new file mode 100644 index 0000000..e37b0cf --- /dev/null +++ b/ccxt/protobuf/mexc/PrivateAccountV3Api_pb2.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: PrivateAccountV3Api.proto +# Protobuf Python Version: 5.29.3 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 5, + 29, + 3, + '', + 'PrivateAccountV3Api.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x19PrivateAccountV3Api.proto\"\xba\x01\n\x13PrivateAccountV3Api\x12\x11\n\tvcoinName\x18\x01 \x01(\t\x12\x0e\n\x06\x63oinId\x18\x02 \x01(\t\x12\x15\n\rbalanceAmount\x18\x03 \x01(\t\x12\x1b\n\x13\x62\x61lanceAmountChange\x18\x04 \x01(\t\x12\x14\n\x0c\x66rozenAmount\x18\x05 \x01(\t\x12\x1a\n\x12\x66rozenAmountChange\x18\x06 \x01(\t\x12\x0c\n\x04type\x18\x07 \x01(\t\x12\x0c\n\x04time\x18\x08 \x01(\x03\x42<\n\x1c\x63om.mxc.push.common.protobufB\x18PrivateAccountV3ApiProtoH\x01P\x01\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'PrivateAccountV3Api_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\034com.mxc.push.common.protobufB\030PrivateAccountV3ApiProtoH\001P\001' + _globals['_PRIVATEACCOUNTV3API']._serialized_start=30 + _globals['_PRIVATEACCOUNTV3API']._serialized_end=216 +# @@protoc_insertion_point(module_scope) diff --git a/ccxt/protobuf/mexc/PrivateDealsV3Api_pb2.py b/ccxt/protobuf/mexc/PrivateDealsV3Api_pb2.py new file mode 100644 index 0000000..0003104 --- /dev/null +++ b/ccxt/protobuf/mexc/PrivateDealsV3Api_pb2.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: PrivateDealsV3Api.proto +# Protobuf Python Version: 5.29.3 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 5, + 29, + 3, + '', + 'PrivateDealsV3Api.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x17PrivateDealsV3Api.proto\"\xec\x01\n\x11PrivateDealsV3Api\x12\r\n\x05price\x18\x01 \x01(\t\x12\x10\n\x08quantity\x18\x02 \x01(\t\x12\x0e\n\x06\x61mount\x18\x03 \x01(\t\x12\x11\n\ttradeType\x18\x04 \x01(\x05\x12\x0f\n\x07isMaker\x18\x05 \x01(\x08\x12\x13\n\x0bisSelfTrade\x18\x06 \x01(\x08\x12\x0f\n\x07tradeId\x18\x07 \x01(\t\x12\x15\n\rclientOrderId\x18\x08 \x01(\t\x12\x0f\n\x07orderId\x18\t \x01(\t\x12\x11\n\tfeeAmount\x18\n \x01(\t\x12\x13\n\x0b\x66\x65\x65\x43urrency\x18\x0b \x01(\t\x12\x0c\n\x04time\x18\x0c \x01(\x03\x42:\n\x1c\x63om.mxc.push.common.protobufB\x16PrivateDealsV3ApiProtoH\x01P\x01\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'PrivateDealsV3Api_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\034com.mxc.push.common.protobufB\026PrivateDealsV3ApiProtoH\001P\001' + _globals['_PRIVATEDEALSV3API']._serialized_start=28 + _globals['_PRIVATEDEALSV3API']._serialized_end=264 +# @@protoc_insertion_point(module_scope) diff --git a/ccxt/protobuf/mexc/PrivateOrdersV3Api_pb2.py b/ccxt/protobuf/mexc/PrivateOrdersV3Api_pb2.py new file mode 100644 index 0000000..3de188d --- /dev/null +++ b/ccxt/protobuf/mexc/PrivateOrdersV3Api_pb2.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: PrivateOrdersV3Api.proto +# Protobuf Python Version: 5.29.3 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 5, + 29, + 3, + '', + 'PrivateOrdersV3Api.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x18PrivateOrdersV3Api.proto\"\xe8\x05\n\x12PrivateOrdersV3Api\x12\n\n\x02id\x18\x01 \x01(\t\x12\x10\n\x08\x63lientId\x18\x02 \x01(\t\x12\r\n\x05price\x18\x03 \x01(\t\x12\x10\n\x08quantity\x18\x04 \x01(\t\x12\x0e\n\x06\x61mount\x18\x05 \x01(\t\x12\x10\n\x08\x61vgPrice\x18\x06 \x01(\t\x12\x11\n\torderType\x18\x07 \x01(\x05\x12\x11\n\ttradeType\x18\x08 \x01(\x05\x12\x0f\n\x07isMaker\x18\t \x01(\x08\x12\x14\n\x0cremainAmount\x18\n \x01(\t\x12\x16\n\x0eremainQuantity\x18\x0b \x01(\t\x12\x1d\n\x10lastDealQuantity\x18\x0c \x01(\tH\x00\x88\x01\x01\x12\x1a\n\x12\x63umulativeQuantity\x18\r \x01(\t\x12\x18\n\x10\x63umulativeAmount\x18\x0e \x01(\t\x12\x0e\n\x06status\x18\x0f \x01(\x05\x12\x12\n\ncreateTime\x18\x10 \x01(\x03\x12\x13\n\x06market\x18\x11 \x01(\tH\x01\x88\x01\x01\x12\x18\n\x0btriggerType\x18\x12 \x01(\x05H\x02\x88\x01\x01\x12\x19\n\x0ctriggerPrice\x18\x13 \x01(\tH\x03\x88\x01\x01\x12\x12\n\x05state\x18\x14 \x01(\x05H\x04\x88\x01\x01\x12\x12\n\x05ocoId\x18\x15 \x01(\tH\x05\x88\x01\x01\x12\x18\n\x0brouteFactor\x18\x16 \x01(\tH\x06\x88\x01\x01\x12\x15\n\x08symbolId\x18\x17 \x01(\tH\x07\x88\x01\x01\x12\x15\n\x08marketId\x18\x18 \x01(\tH\x08\x88\x01\x01\x12\x1d\n\x10marketCurrencyId\x18\x19 \x01(\tH\t\x88\x01\x01\x12\x17\n\ncurrencyId\x18\x1a \x01(\tH\n\x88\x01\x01\x42\x13\n\x11_lastDealQuantityB\t\n\x07_marketB\x0e\n\x0c_triggerTypeB\x0f\n\r_triggerPriceB\x08\n\x06_stateB\x08\n\x06_ocoIdB\x0e\n\x0c_routeFactorB\x0b\n\t_symbolIdB\x0b\n\t_marketIdB\x13\n\x11_marketCurrencyIdB\r\n\x0b_currencyIdB;\n\x1c\x63om.mxc.push.common.protobufB\x17PrivateOrdersV3ApiProtoH\x01P\x01\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'PrivateOrdersV3Api_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\034com.mxc.push.common.protobufB\027PrivateOrdersV3ApiProtoH\001P\001' + _globals['_PRIVATEORDERSV3API']._serialized_start=29 + _globals['_PRIVATEORDERSV3API']._serialized_end=773 +# @@protoc_insertion_point(module_scope) diff --git a/ccxt/protobuf/mexc/PublicAggreBookTickerV3Api_pb2.py b/ccxt/protobuf/mexc/PublicAggreBookTickerV3Api_pb2.py new file mode 100644 index 0000000..b1f4c9e --- /dev/null +++ b/ccxt/protobuf/mexc/PublicAggreBookTickerV3Api_pb2.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: PublicAggreBookTickerV3Api.proto +# Protobuf Python Version: 5.29.3 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 5, + 29, + 3, + '', + 'PublicAggreBookTickerV3Api.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n PublicAggreBookTickerV3Api.proto\"j\n\x1aPublicAggreBookTickerV3Api\x12\x10\n\x08\x62idPrice\x18\x01 \x01(\t\x12\x13\n\x0b\x62idQuantity\x18\x02 \x01(\t\x12\x10\n\x08\x61skPrice\x18\x03 \x01(\t\x12\x13\n\x0b\x61skQuantity\x18\x04 \x01(\tBC\n\x1c\x63om.mxc.push.common.protobufB\x1fPublicAggreBookTickerV3ApiProtoH\x01P\x01\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'PublicAggreBookTickerV3Api_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\034com.mxc.push.common.protobufB\037PublicAggreBookTickerV3ApiProtoH\001P\001' + _globals['_PUBLICAGGREBOOKTICKERV3API']._serialized_start=36 + _globals['_PUBLICAGGREBOOKTICKERV3API']._serialized_end=142 +# @@protoc_insertion_point(module_scope) diff --git a/ccxt/protobuf/mexc/PublicAggreDealsV3Api_pb2.py b/ccxt/protobuf/mexc/PublicAggreDealsV3Api_pb2.py new file mode 100644 index 0000000..7fcc2c1 --- /dev/null +++ b/ccxt/protobuf/mexc/PublicAggreDealsV3Api_pb2.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: PublicAggreDealsV3Api.proto +# Protobuf Python Version: 5.29.3 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 5, + 29, + 3, + '', + 'PublicAggreDealsV3Api.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1bPublicAggreDealsV3Api.proto\"U\n\x15PublicAggreDealsV3Api\x12)\n\x05\x64\x65\x61ls\x18\x01 \x03(\x0b\x32\x1a.PublicAggreDealsV3ApiItem\x12\x11\n\teventType\x18\x02 \x01(\t\"]\n\x19PublicAggreDealsV3ApiItem\x12\r\n\x05price\x18\x01 \x01(\t\x12\x10\n\x08quantity\x18\x02 \x01(\t\x12\x11\n\ttradeType\x18\x03 \x01(\x05\x12\x0c\n\x04time\x18\x04 \x01(\x03\x42>\n\x1c\x63om.mxc.push.common.protobufB\x1aPublicAggreDealsV3ApiProtoH\x01P\x01\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'PublicAggreDealsV3Api_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\034com.mxc.push.common.protobufB\032PublicAggreDealsV3ApiProtoH\001P\001' + _globals['_PUBLICAGGREDEALSV3API']._serialized_start=31 + _globals['_PUBLICAGGREDEALSV3API']._serialized_end=116 + _globals['_PUBLICAGGREDEALSV3APIITEM']._serialized_start=118 + _globals['_PUBLICAGGREDEALSV3APIITEM']._serialized_end=211 +# @@protoc_insertion_point(module_scope) diff --git a/ccxt/protobuf/mexc/PublicAggreDepthsV3Api_pb2.py b/ccxt/protobuf/mexc/PublicAggreDepthsV3Api_pb2.py new file mode 100644 index 0000000..306d593 --- /dev/null +++ b/ccxt/protobuf/mexc/PublicAggreDepthsV3Api_pb2.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: PublicAggreDepthsV3Api.proto +# Protobuf Python Version: 5.29.3 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 5, + 29, + 3, + '', + 'PublicAggreDepthsV3Api.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1cPublicAggreDepthsV3Api.proto\"\xa7\x01\n\x16PublicAggreDepthsV3Api\x12(\n\x04\x61sks\x18\x01 \x03(\x0b\x32\x1a.PublicAggreDepthV3ApiItem\x12(\n\x04\x62ids\x18\x02 \x03(\x0b\x32\x1a.PublicAggreDepthV3ApiItem\x12\x11\n\teventType\x18\x03 \x01(\t\x12\x13\n\x0b\x66romVersion\x18\x04 \x01(\t\x12\x11\n\ttoVersion\x18\x05 \x01(\t\"<\n\x19PublicAggreDepthV3ApiItem\x12\r\n\x05price\x18\x01 \x01(\t\x12\x10\n\x08quantity\x18\x02 \x01(\tB?\n\x1c\x63om.mxc.push.common.protobufB\x1bPublicAggreDepthsV3ApiProtoH\x01P\x01\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'PublicAggreDepthsV3Api_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\034com.mxc.push.common.protobufB\033PublicAggreDepthsV3ApiProtoH\001P\001' + _globals['_PUBLICAGGREDEPTHSV3API']._serialized_start=33 + _globals['_PUBLICAGGREDEPTHSV3API']._serialized_end=200 + _globals['_PUBLICAGGREDEPTHV3APIITEM']._serialized_start=202 + _globals['_PUBLICAGGREDEPTHV3APIITEM']._serialized_end=262 +# @@protoc_insertion_point(module_scope) diff --git a/ccxt/protobuf/mexc/PublicBookTickerBatchV3Api_pb2.py b/ccxt/protobuf/mexc/PublicBookTickerBatchV3Api_pb2.py new file mode 100644 index 0000000..affb8a9 --- /dev/null +++ b/ccxt/protobuf/mexc/PublicBookTickerBatchV3Api_pb2.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: PublicBookTickerBatchV3Api.proto +# Protobuf Python Version: 5.29.3 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 5, + 29, + 3, + '', + 'PublicBookTickerBatchV3Api.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from ccxt.protobuf.mexc import PublicBookTickerV3Api_pb2 as PublicBookTickerV3Api__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n PublicBookTickerBatchV3Api.proto\x1a\x1bPublicBookTickerV3Api.proto\"C\n\x1aPublicBookTickerBatchV3Api\x12%\n\x05items\x18\x01 \x03(\x0b\x32\x16.PublicBookTickerV3ApiBC\n\x1c\x63om.mxc.push.common.protobufB\x1fPublicBookTickerBatchV3ApiProtoH\x01P\x01\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'PublicBookTickerBatchV3Api_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\034com.mxc.push.common.protobufB\037PublicBookTickerBatchV3ApiProtoH\001P\001' + _globals['_PUBLICBOOKTICKERBATCHV3API']._serialized_start=65 + _globals['_PUBLICBOOKTICKERBATCHV3API']._serialized_end=132 +# @@protoc_insertion_point(module_scope) diff --git a/ccxt/protobuf/mexc/PublicBookTickerV3Api_pb2.py b/ccxt/protobuf/mexc/PublicBookTickerV3Api_pb2.py new file mode 100644 index 0000000..70e15a8 --- /dev/null +++ b/ccxt/protobuf/mexc/PublicBookTickerV3Api_pb2.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: PublicBookTickerV3Api.proto +# Protobuf Python Version: 5.29.3 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 5, + 29, + 3, + '', + 'PublicBookTickerV3Api.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1bPublicBookTickerV3Api.proto\"e\n\x15PublicBookTickerV3Api\x12\x10\n\x08\x62idPrice\x18\x01 \x01(\t\x12\x13\n\x0b\x62idQuantity\x18\x02 \x01(\t\x12\x10\n\x08\x61skPrice\x18\x03 \x01(\t\x12\x13\n\x0b\x61skQuantity\x18\x04 \x01(\tB>\n\x1c\x63om.mxc.push.common.protobufB\x1aPublicBookTickerV3ApiProtoH\x01P\x01\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'PublicBookTickerV3Api_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\034com.mxc.push.common.protobufB\032PublicBookTickerV3ApiProtoH\001P\001' + _globals['_PUBLICBOOKTICKERV3API']._serialized_start=31 + _globals['_PUBLICBOOKTICKERV3API']._serialized_end=132 +# @@protoc_insertion_point(module_scope) diff --git a/ccxt/protobuf/mexc/PublicDealsV3Api_pb2.py b/ccxt/protobuf/mexc/PublicDealsV3Api_pb2.py new file mode 100644 index 0000000..7c4178a --- /dev/null +++ b/ccxt/protobuf/mexc/PublicDealsV3Api_pb2.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: PublicDealsV3Api.proto +# Protobuf Python Version: 5.29.3 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 5, + 29, + 3, + '', + 'PublicDealsV3Api.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x16PublicDealsV3Api.proto\"K\n\x10PublicDealsV3Api\x12$\n\x05\x64\x65\x61ls\x18\x01 \x03(\x0b\x32\x15.PublicDealsV3ApiItem\x12\x11\n\teventType\x18\x02 \x01(\t\"X\n\x14PublicDealsV3ApiItem\x12\r\n\x05price\x18\x01 \x01(\t\x12\x10\n\x08quantity\x18\x02 \x01(\t\x12\x11\n\ttradeType\x18\x03 \x01(\x05\x12\x0c\n\x04time\x18\x04 \x01(\x03\x42\x39\n\x1c\x63om.mxc.push.common.protobufB\x15PublicDealsV3ApiProtoH\x01P\x01\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'PublicDealsV3Api_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\034com.mxc.push.common.protobufB\025PublicDealsV3ApiProtoH\001P\001' + _globals['_PUBLICDEALSV3API']._serialized_start=26 + _globals['_PUBLICDEALSV3API']._serialized_end=101 + _globals['_PUBLICDEALSV3APIITEM']._serialized_start=103 + _globals['_PUBLICDEALSV3APIITEM']._serialized_end=191 +# @@protoc_insertion_point(module_scope) diff --git a/ccxt/protobuf/mexc/PublicIncreaseDepthsBatchV3Api_pb2.py b/ccxt/protobuf/mexc/PublicIncreaseDepthsBatchV3Api_pb2.py new file mode 100644 index 0000000..e1a6749 --- /dev/null +++ b/ccxt/protobuf/mexc/PublicIncreaseDepthsBatchV3Api_pb2.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: PublicIncreaseDepthsBatchV3Api.proto +# Protobuf Python Version: 5.29.3 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 5, + 29, + 3, + '', + 'PublicIncreaseDepthsBatchV3Api.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from ccxt.protobuf.mexc import PublicIncreaseDepthsV3Api_pb2 as PublicIncreaseDepthsV3Api__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n$PublicIncreaseDepthsBatchV3Api.proto\x1a\x1fPublicIncreaseDepthsV3Api.proto\"^\n\x1ePublicIncreaseDepthsBatchV3Api\x12)\n\x05items\x18\x01 \x03(\x0b\x32\x1a.PublicIncreaseDepthsV3Api\x12\x11\n\teventType\x18\x02 \x01(\tBG\n\x1c\x63om.mxc.push.common.protobufB#PublicIncreaseDepthsBatchV3ApiProtoH\x01P\x01\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'PublicIncreaseDepthsBatchV3Api_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\034com.mxc.push.common.protobufB#PublicIncreaseDepthsBatchV3ApiProtoH\001P\001' + _globals['_PUBLICINCREASEDEPTHSBATCHV3API']._serialized_start=73 + _globals['_PUBLICINCREASEDEPTHSBATCHV3API']._serialized_end=167 +# @@protoc_insertion_point(module_scope) diff --git a/ccxt/protobuf/mexc/PublicIncreaseDepthsV3Api_pb2.py b/ccxt/protobuf/mexc/PublicIncreaseDepthsV3Api_pb2.py new file mode 100644 index 0000000..f4cf5ba --- /dev/null +++ b/ccxt/protobuf/mexc/PublicIncreaseDepthsV3Api_pb2.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: PublicIncreaseDepthsV3Api.proto +# Protobuf Python Version: 5.29.3 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 5, + 29, + 3, + '', + 'PublicIncreaseDepthsV3Api.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1fPublicIncreaseDepthsV3Api.proto\"\x99\x01\n\x19PublicIncreaseDepthsV3Api\x12+\n\x04\x61sks\x18\x01 \x03(\x0b\x32\x1d.PublicIncreaseDepthV3ApiItem\x12+\n\x04\x62ids\x18\x02 \x03(\x0b\x32\x1d.PublicIncreaseDepthV3ApiItem\x12\x11\n\teventType\x18\x03 \x01(\t\x12\x0f\n\x07version\x18\x04 \x01(\t\"?\n\x1cPublicIncreaseDepthV3ApiItem\x12\r\n\x05price\x18\x01 \x01(\t\x12\x10\n\x08quantity\x18\x02 \x01(\tBB\n\x1c\x63om.mxc.push.common.protobufB\x1ePublicIncreaseDepthsV3ApiProtoH\x01P\x01\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'PublicIncreaseDepthsV3Api_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\034com.mxc.push.common.protobufB\036PublicIncreaseDepthsV3ApiProtoH\001P\001' + _globals['_PUBLICINCREASEDEPTHSV3API']._serialized_start=36 + _globals['_PUBLICINCREASEDEPTHSV3API']._serialized_end=189 + _globals['_PUBLICINCREASEDEPTHV3APIITEM']._serialized_start=191 + _globals['_PUBLICINCREASEDEPTHV3APIITEM']._serialized_end=254 +# @@protoc_insertion_point(module_scope) diff --git a/ccxt/protobuf/mexc/PublicLimitDepthsV3Api_pb2.py b/ccxt/protobuf/mexc/PublicLimitDepthsV3Api_pb2.py new file mode 100644 index 0000000..0682007 --- /dev/null +++ b/ccxt/protobuf/mexc/PublicLimitDepthsV3Api_pb2.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: PublicLimitDepthsV3Api.proto +# Protobuf Python Version: 5.29.3 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 5, + 29, + 3, + '', + 'PublicLimitDepthsV3Api.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1cPublicLimitDepthsV3Api.proto\"\x90\x01\n\x16PublicLimitDepthsV3Api\x12(\n\x04\x61sks\x18\x01 \x03(\x0b\x32\x1a.PublicLimitDepthV3ApiItem\x12(\n\x04\x62ids\x18\x02 \x03(\x0b\x32\x1a.PublicLimitDepthV3ApiItem\x12\x11\n\teventType\x18\x03 \x01(\t\x12\x0f\n\x07version\x18\x04 \x01(\t\"<\n\x19PublicLimitDepthV3ApiItem\x12\r\n\x05price\x18\x01 \x01(\t\x12\x10\n\x08quantity\x18\x02 \x01(\tB?\n\x1c\x63om.mxc.push.common.protobufB\x1bPublicLimitDepthsV3ApiProtoH\x01P\x01\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'PublicLimitDepthsV3Api_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\034com.mxc.push.common.protobufB\033PublicLimitDepthsV3ApiProtoH\001P\001' + _globals['_PUBLICLIMITDEPTHSV3API']._serialized_start=33 + _globals['_PUBLICLIMITDEPTHSV3API']._serialized_end=177 + _globals['_PUBLICLIMITDEPTHV3APIITEM']._serialized_start=179 + _globals['_PUBLICLIMITDEPTHV3APIITEM']._serialized_end=239 +# @@protoc_insertion_point(module_scope) diff --git a/ccxt/protobuf/mexc/PublicMiniTickerV3Api_pb2.py b/ccxt/protobuf/mexc/PublicMiniTickerV3Api_pb2.py new file mode 100644 index 0000000..43b4077 --- /dev/null +++ b/ccxt/protobuf/mexc/PublicMiniTickerV3Api_pb2.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: PublicMiniTickerV3Api.proto +# Protobuf Python Version: 5.29.3 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 5, + 29, + 3, + '', + 'PublicMiniTickerV3Api.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1bPublicMiniTickerV3Api.proto\"\xf4\x01\n\x15PublicMiniTickerV3Api\x12\x0e\n\x06symbol\x18\x01 \x01(\t\x12\r\n\x05price\x18\x02 \x01(\t\x12\x0c\n\x04rate\x18\x03 \x01(\t\x12\x11\n\tzonedRate\x18\x04 \x01(\t\x12\x0c\n\x04high\x18\x05 \x01(\t\x12\x0b\n\x03low\x18\x06 \x01(\t\x12\x0e\n\x06volume\x18\x07 \x01(\t\x12\x10\n\x08quantity\x18\x08 \x01(\t\x12\x15\n\rlastCloseRate\x18\t \x01(\t\x12\x1a\n\x12lastCloseZonedRate\x18\n \x01(\t\x12\x15\n\rlastCloseHigh\x18\x0b \x01(\t\x12\x14\n\x0clastCloseLow\x18\x0c \x01(\tB>\n\x1c\x63om.mxc.push.common.protobufB\x1aPublicMiniTickerV3ApiProtoH\x01P\x01\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'PublicMiniTickerV3Api_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\034com.mxc.push.common.protobufB\032PublicMiniTickerV3ApiProtoH\001P\001' + _globals['_PUBLICMINITICKERV3API']._serialized_start=32 + _globals['_PUBLICMINITICKERV3API']._serialized_end=276 +# @@protoc_insertion_point(module_scope) diff --git a/ccxt/protobuf/mexc/PublicMiniTickersV3Api_pb2.py b/ccxt/protobuf/mexc/PublicMiniTickersV3Api_pb2.py new file mode 100644 index 0000000..d2eb7e5 --- /dev/null +++ b/ccxt/protobuf/mexc/PublicMiniTickersV3Api_pb2.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: PublicMiniTickersV3Api.proto +# Protobuf Python Version: 5.29.3 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 5, + 29, + 3, + '', + 'PublicMiniTickersV3Api.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from ccxt.protobuf.mexc import PublicMiniTickerV3Api_pb2 as PublicMiniTickerV3Api__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1cPublicMiniTickersV3Api.proto\x1a\x1bPublicMiniTickerV3Api.proto\"?\n\x16PublicMiniTickersV3Api\x12%\n\x05items\x18\x01 \x03(\x0b\x32\x16.PublicMiniTickerV3ApiB?\n\x1c\x63om.mxc.push.common.protobufB\x1bPublicMiniTickersV3ApiProtoH\x01P\x01\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'PublicMiniTickersV3Api_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\034com.mxc.push.common.protobufB\033PublicMiniTickersV3ApiProtoH\001P\001' + _globals['_PUBLICMINITICKERSV3API']._serialized_start=61 + _globals['_PUBLICMINITICKERSV3API']._serialized_end=124 +# @@protoc_insertion_point(module_scope) diff --git a/ccxt/protobuf/mexc/PublicSpotKlineV3Api_pb2.py b/ccxt/protobuf/mexc/PublicSpotKlineV3Api_pb2.py new file mode 100644 index 0000000..9458de1 --- /dev/null +++ b/ccxt/protobuf/mexc/PublicSpotKlineV3Api_pb2.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: PublicSpotKlineV3Api.proto +# Protobuf Python Version: 5.29.3 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 5, + 29, + 3, + '', + 'PublicSpotKlineV3Api.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1aPublicSpotKlineV3Api.proto\"\xc7\x01\n\x14PublicSpotKlineV3Api\x12\x10\n\x08interval\x18\x01 \x01(\t\x12\x13\n\x0bwindowStart\x18\x02 \x01(\x03\x12\x14\n\x0copeningPrice\x18\x03 \x01(\t\x12\x14\n\x0c\x63losingPrice\x18\x04 \x01(\t\x12\x14\n\x0chighestPrice\x18\x05 \x01(\t\x12\x13\n\x0blowestPrice\x18\x06 \x01(\t\x12\x0e\n\x06volume\x18\x07 \x01(\t\x12\x0e\n\x06\x61mount\x18\x08 \x01(\t\x12\x11\n\twindowEnd\x18\t \x01(\x03\x42=\n\x1c\x63om.mxc.push.common.protobufB\x19PublicSpotKlineV3ApiProtoH\x01P\x01\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'PublicSpotKlineV3Api_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\034com.mxc.push.common.protobufB\031PublicSpotKlineV3ApiProtoH\001P\001' + _globals['_PUBLICSPOTKLINEV3API']._serialized_start=31 + _globals['_PUBLICSPOTKLINEV3API']._serialized_end=230 +# @@protoc_insertion_point(module_scope) diff --git a/ccxt/protobuf/mexc/PushDataV3ApiWrapper_pb2.py b/ccxt/protobuf/mexc/PushDataV3ApiWrapper_pb2.py new file mode 100644 index 0000000..8383b1f --- /dev/null +++ b/ccxt/protobuf/mexc/PushDataV3ApiWrapper_pb2.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: PushDataV3ApiWrapper.proto +# Protobuf Python Version: 5.29.3 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 5, + 29, + 3, + '', + 'PushDataV3ApiWrapper.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from ccxt.protobuf.mexc import PublicDealsV3Api_pb2 as PublicDealsV3Api__pb2 +from ccxt.protobuf.mexc import PublicIncreaseDepthsV3Api_pb2 as PublicIncreaseDepthsV3Api__pb2 +from ccxt.protobuf.mexc import PublicLimitDepthsV3Api_pb2 as PublicLimitDepthsV3Api__pb2 +from ccxt.protobuf.mexc import PrivateOrdersV3Api_pb2 as PrivateOrdersV3Api__pb2 +from ccxt.protobuf.mexc import PublicBookTickerV3Api_pb2 as PublicBookTickerV3Api__pb2 +from ccxt.protobuf.mexc import PrivateDealsV3Api_pb2 as PrivateDealsV3Api__pb2 +from ccxt.protobuf.mexc import PrivateAccountV3Api_pb2 as PrivateAccountV3Api__pb2 +from ccxt.protobuf.mexc import PublicSpotKlineV3Api_pb2 as PublicSpotKlineV3Api__pb2 +from ccxt.protobuf.mexc import PublicMiniTickerV3Api_pb2 as PublicMiniTickerV3Api__pb2 +from ccxt.protobuf.mexc import PublicMiniTickersV3Api_pb2 as PublicMiniTickersV3Api__pb2 +from ccxt.protobuf.mexc import PublicBookTickerBatchV3Api_pb2 as PublicBookTickerBatchV3Api__pb2 +from ccxt.protobuf.mexc import PublicIncreaseDepthsBatchV3Api_pb2 as PublicIncreaseDepthsBatchV3Api__pb2 +from ccxt.protobuf.mexc import PublicAggreDepthsV3Api_pb2 as PublicAggreDepthsV3Api__pb2 +from ccxt.protobuf.mexc import PublicAggreDealsV3Api_pb2 as PublicAggreDealsV3Api__pb2 +from ccxt.protobuf.mexc import PublicAggreBookTickerV3Api_pb2 as PublicAggreBookTickerV3Api__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1aPushDataV3ApiWrapper.proto\x1a\x16PublicDealsV3Api.proto\x1a\x1fPublicIncreaseDepthsV3Api.proto\x1a\x1cPublicLimitDepthsV3Api.proto\x1a\x18PrivateOrdersV3Api.proto\x1a\x1bPublicBookTickerV3Api.proto\x1a\x17PrivateDealsV3Api.proto\x1a\x19PrivateAccountV3Api.proto\x1a\x1aPublicSpotKlineV3Api.proto\x1a\x1bPublicMiniTickerV3Api.proto\x1a\x1cPublicMiniTickersV3Api.proto\x1a PublicBookTickerBatchV3Api.proto\x1a$PublicIncreaseDepthsBatchV3Api.proto\x1a\x1cPublicAggreDepthsV3Api.proto\x1a\x1bPublicAggreDealsV3Api.proto\x1a PublicAggreBookTickerV3Api.proto\"\xf0\x07\n\x14PushDataV3ApiWrapper\x12\x0f\n\x07\x63hannel\x18\x01 \x01(\t\x12)\n\x0bpublicDeals\x18\xad\x02 \x01(\x0b\x32\x11.PublicDealsV3ApiH\x00\x12;\n\x14publicIncreaseDepths\x18\xae\x02 \x01(\x0b\x32\x1a.PublicIncreaseDepthsV3ApiH\x00\x12\x35\n\x11publicLimitDepths\x18\xaf\x02 \x01(\x0b\x32\x17.PublicLimitDepthsV3ApiH\x00\x12-\n\rprivateOrders\x18\xb0\x02 \x01(\x0b\x32\x13.PrivateOrdersV3ApiH\x00\x12\x33\n\x10publicBookTicker\x18\xb1\x02 \x01(\x0b\x32\x16.PublicBookTickerV3ApiH\x00\x12+\n\x0cprivateDeals\x18\xb2\x02 \x01(\x0b\x32\x12.PrivateDealsV3ApiH\x00\x12/\n\x0eprivateAccount\x18\xb3\x02 \x01(\x0b\x32\x14.PrivateAccountV3ApiH\x00\x12\x31\n\x0fpublicSpotKline\x18\xb4\x02 \x01(\x0b\x32\x15.PublicSpotKlineV3ApiH\x00\x12\x33\n\x10publicMiniTicker\x18\xb5\x02 \x01(\x0b\x32\x16.PublicMiniTickerV3ApiH\x00\x12\x35\n\x11publicMiniTickers\x18\xb6\x02 \x01(\x0b\x32\x17.PublicMiniTickersV3ApiH\x00\x12=\n\x15publicBookTickerBatch\x18\xb7\x02 \x01(\x0b\x32\x1b.PublicBookTickerBatchV3ApiH\x00\x12\x45\n\x19publicIncreaseDepthsBatch\x18\xb8\x02 \x01(\x0b\x32\x1f.PublicIncreaseDepthsBatchV3ApiH\x00\x12\x35\n\x11publicAggreDepths\x18\xb9\x02 \x01(\x0b\x32\x17.PublicAggreDepthsV3ApiH\x00\x12\x33\n\x10publicAggreDeals\x18\xba\x02 \x01(\x0b\x32\x16.PublicAggreDealsV3ApiH\x00\x12=\n\x15publicAggreBookTicker\x18\xbb\x02 \x01(\x0b\x32\x1b.PublicAggreBookTickerV3ApiH\x00\x12\x13\n\x06symbol\x18\x03 \x01(\tH\x01\x88\x01\x01\x12\x15\n\x08symbolId\x18\x04 \x01(\tH\x02\x88\x01\x01\x12\x17\n\ncreateTime\x18\x05 \x01(\x03H\x03\x88\x01\x01\x12\x15\n\x08sendTime\x18\x06 \x01(\x03H\x04\x88\x01\x01\x42\x06\n\x04\x62odyB\t\n\x07_symbolB\x0b\n\t_symbolIdB\r\n\x0b_createTimeB\x0b\n\t_sendTimeB=\n\x1c\x63om.mxc.push.common.protobufB\x19PushDataV3ApiWrapperProtoH\x01P\x01\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'PushDataV3ApiWrapper_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\034com.mxc.push.common.protobufB\031PushDataV3ApiWrapperProtoH\001P\001' + _globals['_PUSHDATAV3APIWRAPPER']._serialized_start=477 + _globals['_PUSHDATAV3APIWRAPPER']._serialized_end=1485 +# @@protoc_insertion_point(module_scope) diff --git a/ccxt/protobuf/mexc/__init__.py b/ccxt/protobuf/mexc/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ccxt/protobuf/mexc/__pycache__/PrivateAccountV3Api_pb2.cpython-311.pyc b/ccxt/protobuf/mexc/__pycache__/PrivateAccountV3Api_pb2.cpython-311.pyc new file mode 100644 index 0000000..cde3286 Binary files /dev/null and b/ccxt/protobuf/mexc/__pycache__/PrivateAccountV3Api_pb2.cpython-311.pyc differ diff --git a/ccxt/protobuf/mexc/__pycache__/PrivateDealsV3Api_pb2.cpython-311.pyc b/ccxt/protobuf/mexc/__pycache__/PrivateDealsV3Api_pb2.cpython-311.pyc new file mode 100644 index 0000000..2785223 Binary files /dev/null and b/ccxt/protobuf/mexc/__pycache__/PrivateDealsV3Api_pb2.cpython-311.pyc differ diff --git a/ccxt/protobuf/mexc/__pycache__/PrivateOrdersV3Api_pb2.cpython-311.pyc b/ccxt/protobuf/mexc/__pycache__/PrivateOrdersV3Api_pb2.cpython-311.pyc new file mode 100644 index 0000000..fefe8f9 Binary files /dev/null and b/ccxt/protobuf/mexc/__pycache__/PrivateOrdersV3Api_pb2.cpython-311.pyc differ diff --git a/ccxt/protobuf/mexc/__pycache__/PublicAggreBookTickerV3Api_pb2.cpython-311.pyc b/ccxt/protobuf/mexc/__pycache__/PublicAggreBookTickerV3Api_pb2.cpython-311.pyc new file mode 100644 index 0000000..ff18a5d Binary files /dev/null and b/ccxt/protobuf/mexc/__pycache__/PublicAggreBookTickerV3Api_pb2.cpython-311.pyc differ diff --git a/ccxt/protobuf/mexc/__pycache__/PublicAggreDealsV3Api_pb2.cpython-311.pyc b/ccxt/protobuf/mexc/__pycache__/PublicAggreDealsV3Api_pb2.cpython-311.pyc new file mode 100644 index 0000000..5ab6338 Binary files /dev/null and b/ccxt/protobuf/mexc/__pycache__/PublicAggreDealsV3Api_pb2.cpython-311.pyc differ diff --git a/ccxt/protobuf/mexc/__pycache__/PublicAggreDepthsV3Api_pb2.cpython-311.pyc b/ccxt/protobuf/mexc/__pycache__/PublicAggreDepthsV3Api_pb2.cpython-311.pyc new file mode 100644 index 0000000..fc5e9fb Binary files /dev/null and b/ccxt/protobuf/mexc/__pycache__/PublicAggreDepthsV3Api_pb2.cpython-311.pyc differ diff --git a/ccxt/protobuf/mexc/__pycache__/PublicBookTickerBatchV3Api_pb2.cpython-311.pyc b/ccxt/protobuf/mexc/__pycache__/PublicBookTickerBatchV3Api_pb2.cpython-311.pyc new file mode 100644 index 0000000..b8a6bc9 Binary files /dev/null and b/ccxt/protobuf/mexc/__pycache__/PublicBookTickerBatchV3Api_pb2.cpython-311.pyc differ diff --git a/ccxt/protobuf/mexc/__pycache__/PublicBookTickerV3Api_pb2.cpython-311.pyc b/ccxt/protobuf/mexc/__pycache__/PublicBookTickerV3Api_pb2.cpython-311.pyc new file mode 100644 index 0000000..8d6f7fd Binary files /dev/null and b/ccxt/protobuf/mexc/__pycache__/PublicBookTickerV3Api_pb2.cpython-311.pyc differ diff --git a/ccxt/protobuf/mexc/__pycache__/PublicDealsV3Api_pb2.cpython-311.pyc b/ccxt/protobuf/mexc/__pycache__/PublicDealsV3Api_pb2.cpython-311.pyc new file mode 100644 index 0000000..a9b0dd4 Binary files /dev/null and b/ccxt/protobuf/mexc/__pycache__/PublicDealsV3Api_pb2.cpython-311.pyc differ diff --git a/ccxt/protobuf/mexc/__pycache__/PublicIncreaseDepthsBatchV3Api_pb2.cpython-311.pyc b/ccxt/protobuf/mexc/__pycache__/PublicIncreaseDepthsBatchV3Api_pb2.cpython-311.pyc new file mode 100644 index 0000000..5e7c28d Binary files /dev/null and b/ccxt/protobuf/mexc/__pycache__/PublicIncreaseDepthsBatchV3Api_pb2.cpython-311.pyc differ diff --git a/ccxt/protobuf/mexc/__pycache__/PublicIncreaseDepthsV3Api_pb2.cpython-311.pyc b/ccxt/protobuf/mexc/__pycache__/PublicIncreaseDepthsV3Api_pb2.cpython-311.pyc new file mode 100644 index 0000000..a13daa7 Binary files /dev/null and b/ccxt/protobuf/mexc/__pycache__/PublicIncreaseDepthsV3Api_pb2.cpython-311.pyc differ diff --git a/ccxt/protobuf/mexc/__pycache__/PublicLimitDepthsV3Api_pb2.cpython-311.pyc b/ccxt/protobuf/mexc/__pycache__/PublicLimitDepthsV3Api_pb2.cpython-311.pyc new file mode 100644 index 0000000..7e29ead Binary files /dev/null and b/ccxt/protobuf/mexc/__pycache__/PublicLimitDepthsV3Api_pb2.cpython-311.pyc differ diff --git a/ccxt/protobuf/mexc/__pycache__/PublicMiniTickerV3Api_pb2.cpython-311.pyc b/ccxt/protobuf/mexc/__pycache__/PublicMiniTickerV3Api_pb2.cpython-311.pyc new file mode 100644 index 0000000..8ae2d59 Binary files /dev/null and b/ccxt/protobuf/mexc/__pycache__/PublicMiniTickerV3Api_pb2.cpython-311.pyc differ diff --git a/ccxt/protobuf/mexc/__pycache__/PublicMiniTickersV3Api_pb2.cpython-311.pyc b/ccxt/protobuf/mexc/__pycache__/PublicMiniTickersV3Api_pb2.cpython-311.pyc new file mode 100644 index 0000000..e7aacda Binary files /dev/null and b/ccxt/protobuf/mexc/__pycache__/PublicMiniTickersV3Api_pb2.cpython-311.pyc differ diff --git a/ccxt/protobuf/mexc/__pycache__/PublicSpotKlineV3Api_pb2.cpython-311.pyc b/ccxt/protobuf/mexc/__pycache__/PublicSpotKlineV3Api_pb2.cpython-311.pyc new file mode 100644 index 0000000..78554f2 Binary files /dev/null and b/ccxt/protobuf/mexc/__pycache__/PublicSpotKlineV3Api_pb2.cpython-311.pyc differ diff --git a/ccxt/protobuf/mexc/__pycache__/PushDataV3ApiWrapper_pb2.cpython-311.pyc b/ccxt/protobuf/mexc/__pycache__/PushDataV3ApiWrapper_pb2.cpython-311.pyc new file mode 100644 index 0000000..5586b49 Binary files /dev/null and b/ccxt/protobuf/mexc/__pycache__/PushDataV3ApiWrapper_pb2.cpython-311.pyc differ diff --git a/ccxt/protobuf/mexc/__pycache__/__init__.cpython-311.pyc b/ccxt/protobuf/mexc/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..108cfdc Binary files /dev/null and b/ccxt/protobuf/mexc/__pycache__/__init__.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/README.md b/ccxt/static_dependencies/README.md new file mode 100644 index 0000000..54c25e7 --- /dev/null +++ b/ccxt/static_dependencies/README.md @@ -0,0 +1 @@ +// TODO: add web3 diff --git a/ccxt/static_dependencies/__init__.py b/ccxt/static_dependencies/__init__.py new file mode 100644 index 0000000..f7f0899 --- /dev/null +++ b/ccxt/static_dependencies/__init__.py @@ -0,0 +1 @@ +__all__ = ['ecdsa', 'keccak', 'aiohttp_socks', 'ethereum', 'parsimonious', 'toolz', 'starknet', 'marshmallow', 'marshmallow_oneofschema', 'lark', 'starkware', 'sympy'] diff --git a/ccxt/static_dependencies/__pycache__/__init__.cpython-311.pyc b/ccxt/static_dependencies/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..6e131c5 Binary files /dev/null and b/ccxt/static_dependencies/__pycache__/__init__.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/ecdsa/__init__.py b/ccxt/static_dependencies/ecdsa/__init__.py new file mode 100644 index 0000000..e5def2f --- /dev/null +++ b/ccxt/static_dependencies/ecdsa/__init__.py @@ -0,0 +1,14 @@ +from .keys import SigningKey, VerifyingKey, BadSignatureError, BadDigestError +from .curves import NIST192p, NIST224p, NIST256p, NIST384p, NIST521p, SECP256k1 + +# This code comes from http://github.com/warner/python-ecdsa +#from ._version import get_versions +__version__ = 'ccxt' # custom ccxt version +#del get_versions + +__all__ = ["curves", "der", "ecdsa", "ellipticcurve", "keys", "numbertheory", + "util"] + +_hush_pyflakes = [SigningKey, VerifyingKey, BadSignatureError, BadDigestError, + NIST192p, NIST224p, NIST256p, NIST384p, NIST521p, SECP256k1] +del _hush_pyflakes diff --git a/ccxt/static_dependencies/ecdsa/__pycache__/__init__.cpython-311.pyc b/ccxt/static_dependencies/ecdsa/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..2a8fe80 Binary files /dev/null and b/ccxt/static_dependencies/ecdsa/__pycache__/__init__.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/ecdsa/__pycache__/curves.cpython-311.pyc b/ccxt/static_dependencies/ecdsa/__pycache__/curves.cpython-311.pyc new file mode 100644 index 0000000..ec96568 Binary files /dev/null and b/ccxt/static_dependencies/ecdsa/__pycache__/curves.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/ecdsa/__pycache__/der.cpython-311.pyc b/ccxt/static_dependencies/ecdsa/__pycache__/der.cpython-311.pyc new file mode 100644 index 0000000..48fbe9c Binary files /dev/null and b/ccxt/static_dependencies/ecdsa/__pycache__/der.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/ecdsa/__pycache__/ecdsa.cpython-311.pyc b/ccxt/static_dependencies/ecdsa/__pycache__/ecdsa.cpython-311.pyc new file mode 100644 index 0000000..0213f83 Binary files /dev/null and b/ccxt/static_dependencies/ecdsa/__pycache__/ecdsa.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/ecdsa/__pycache__/ellipticcurve.cpython-311.pyc b/ccxt/static_dependencies/ecdsa/__pycache__/ellipticcurve.cpython-311.pyc new file mode 100644 index 0000000..3a6b300 Binary files /dev/null and b/ccxt/static_dependencies/ecdsa/__pycache__/ellipticcurve.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/ecdsa/__pycache__/keys.cpython-311.pyc b/ccxt/static_dependencies/ecdsa/__pycache__/keys.cpython-311.pyc new file mode 100644 index 0000000..53f600b Binary files /dev/null and b/ccxt/static_dependencies/ecdsa/__pycache__/keys.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/ecdsa/__pycache__/numbertheory.cpython-311.pyc b/ccxt/static_dependencies/ecdsa/__pycache__/numbertheory.cpython-311.pyc new file mode 100644 index 0000000..3371739 Binary files /dev/null and b/ccxt/static_dependencies/ecdsa/__pycache__/numbertheory.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/ecdsa/__pycache__/rfc6979.cpython-311.pyc b/ccxt/static_dependencies/ecdsa/__pycache__/rfc6979.cpython-311.pyc new file mode 100644 index 0000000..119411b Binary files /dev/null and b/ccxt/static_dependencies/ecdsa/__pycache__/rfc6979.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/ecdsa/__pycache__/util.cpython-311.pyc b/ccxt/static_dependencies/ecdsa/__pycache__/util.cpython-311.pyc new file mode 100644 index 0000000..4bcdc7f Binary files /dev/null and b/ccxt/static_dependencies/ecdsa/__pycache__/util.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/ecdsa/_version.py b/ccxt/static_dependencies/ecdsa/_version.py new file mode 100644 index 0000000..70a9130 --- /dev/null +++ b/ccxt/static_dependencies/ecdsa/_version.py @@ -0,0 +1,520 @@ + +# This file helps to compute a version number in source trees obtained from +# git-archive tarball (such as those provided by githubs download-from-tag +# feature). Distribution tarballs (built by setup.py sdist) and build +# directories (produced by setup.py build) will contain a much shorter file +# that just contains the computed version number. + +# This file is released into the public domain. Generated by +# versioneer-0.17 (https://github.com/warner/python-versioneer) + +"""Git implementation of _version.py.""" + +import errno +import os +import re +import subprocess +import sys + + +def get_keywords(): + """Get the keywords needed to look up the version information.""" + # these strings will be replaced by git during git-archive. + # setup.py/versioneer.py will grep for the variable names, so they must + # each be defined on a line of their own. _version.py will just call + # get_keywords(). + git_refnames = "$Format:%d$" + git_full = "$Format:%H$" + git_date = "$Format:%ci$" + keywords = {"refnames": git_refnames, "full": git_full, "date": git_date} + return keywords + + +class VersioneerConfig: + """Container for Versioneer configuration parameters.""" + + +def get_config(): + """Create, populate and return the VersioneerConfig() object.""" + # these strings are filled in when 'setup.py versioneer' creates + # _version.py + cfg = VersioneerConfig() + cfg.VCS = "git" + cfg.style = "pep440" + cfg.tag_prefix = "python-ecdsa-" + cfg.parentdir_prefix = "ecdsa-" + cfg.versionfile_source = "ecdsa/_version.py" + cfg.verbose = False + return cfg + + +class NotThisMethod(Exception): + """Exception raised if a method is not valid for the current scenario.""" + + +LONG_VERSION_PY = {} +HANDLERS = {} + + +def register_vcs_handler(vcs, method): # decorator + """Decorator to mark a method as the handler for a particular VCS.""" + def decorate(f): + """Store f in HANDLERS[vcs][method].""" + if vcs not in HANDLERS: + HANDLERS[vcs] = {} + HANDLERS[vcs][method] = f + return f + return decorate + + +def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, + env=None): + """Call the given command(s).""" + assert isinstance(commands, list) + p = None + for c in commands: + try: + dispcmd = str([c] + args) + # remember shell=False, so use git.cmd on windows, not just git + p = subprocess.Popen([c] + args, cwd=cwd, env=env, + stdout=subprocess.PIPE, + stderr=(subprocess.PIPE if hide_stderr + else None)) + break + except EnvironmentError: + e = sys.exc_info()[1] + if e.errno == errno.ENOENT: + continue + if verbose: + print("unable to run %s" % dispcmd) + print(e) + return None, None + else: + if verbose: + print("unable to find command, tried %s" % (commands,)) + return None, None + stdout = p.communicate()[0].strip() + if sys.version_info[0] >= 3: + stdout = stdout.decode() + if p.returncode != 0: + if verbose: + print("unable to run %s (error)" % dispcmd) + print("stdout was %s" % stdout) + return None, p.returncode + return stdout, p.returncode + + +def versions_from_parentdir(parentdir_prefix, root, verbose): + """Try to determine the version from the parent directory name. + + Source tarballs conventionally unpack into a directory that includes both + the project name and a version string. We will also support searching up + two directory levels for an appropriately named parent directory + """ + rootdirs = [] + + for i in range(3): + dirname = os.path.basename(root) + if dirname.startswith(parentdir_prefix): + return {"version": dirname[len(parentdir_prefix):], + "full-revisionid": None, + "dirty": False, "error": None, "date": None} + else: + rootdirs.append(root) + root = os.path.dirname(root) # up a level + + if verbose: + print("Tried directories %s but none started with prefix %s" % + (str(rootdirs), parentdir_prefix)) + raise NotThisMethod("rootdir doesn't start with parentdir_prefix") + + +@register_vcs_handler("git", "get_keywords") +def git_get_keywords(versionfile_abs): + """Extract version information from the given file.""" + # the code embedded in _version.py can just fetch the value of these + # keywords. When used from setup.py, we don't want to import _version.py, + # so we do it with a regexp instead. This function is not used from + # _version.py. + keywords = {} + try: + f = open(versionfile_abs, "r") + for line in f.readlines(): + if line.strip().startswith("git_refnames ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["refnames"] = mo.group(1) + if line.strip().startswith("git_full ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["full"] = mo.group(1) + if line.strip().startswith("git_date ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["date"] = mo.group(1) + f.close() + except EnvironmentError: + pass + return keywords + + +@register_vcs_handler("git", "keywords") +def git_versions_from_keywords(keywords, tag_prefix, verbose): + """Get version information from git keywords.""" + if not keywords: + raise NotThisMethod("no keywords at all, weird") + date = keywords.get("date") + if date is not None: + # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant + # datestamp. However we prefer "%ci" (which expands to an "ISO-8601 + # -like" string, which we must then edit to make compliant), because + # it's been around since git-1.5.3, and it's too difficult to + # discover which version we're using, or to work around using an + # older one. + date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) + refnames = keywords["refnames"].strip() + if refnames.startswith("$Format"): + if verbose: + print("keywords are unexpanded, not using") + raise NotThisMethod("unexpanded keywords, not a git-archive tarball") + refs = set([r.strip() for r in refnames.strip("()").split(",")]) + # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of + # just "foo-1.0". If we see a "tag: " prefix, prefer those. + TAG = "tag: " + tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) + if not tags: + # Either we're using git < 1.8.3, or there really are no tags. We use + # a heuristic: assume all version tags have a digit. The old git %d + # expansion behaves like git log --decorate=short and strips out the + # refs/heads/ and refs/tags/ prefixes that would let us distinguish + # between branches and tags. By ignoring refnames without digits, we + # filter out many common branch names like "release" and + # "stabilization", as well as "HEAD" and "master". + tags = set([r for r in refs if re.search(r'\d', r)]) + if verbose: + print("discarding '%s', no digits" % ",".join(refs - tags)) + if verbose: + print("likely tags: %s" % ",".join(sorted(tags))) + for ref in sorted(tags): + # sorting will prefer e.g. "2.0" over "2.0rc1" + if ref.startswith(tag_prefix): + r = ref[len(tag_prefix):] + if verbose: + print("picking %s" % r) + return {"version": r, + "full-revisionid": keywords["full"].strip(), + "dirty": False, "error": None, + "date": date} + # no suitable tags, so version is "0+unknown", but full hex is still there + if verbose: + print("no suitable tags, using unknown + full revision id") + return {"version": "0+unknown", + "full-revisionid": keywords["full"].strip(), + "dirty": False, "error": "no suitable tags", "date": None} + + +@register_vcs_handler("git", "pieces_from_vcs") +def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): + """Get version from 'git describe' in the root of the source tree. + + This only gets called if the git-archive 'subst' keywords were *not* + expanded, and _version.py hasn't already been rewritten with a short + version string, meaning we're inside a checked out source tree. + """ + GITS = ["git"] + if sys.platform == "win32": + GITS = ["git.cmd", "git.exe"] + + out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, + hide_stderr=True) + if rc != 0: + if verbose: + print("Directory %s not under git control" % root) + raise NotThisMethod("'git rev-parse --git-dir' returned error") + + # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] + # if there isn't one, this yields HEX[-dirty] (no NUM) + describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty", + "--always", "--long", + "--match", "%s*" % tag_prefix], + cwd=root) + # --long was added in git-1.5.5 + if describe_out is None: + raise NotThisMethod("'git describe' failed") + describe_out = describe_out.strip() + full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) + if full_out is None: + raise NotThisMethod("'git rev-parse' failed") + full_out = full_out.strip() + + pieces = {} + pieces["long"] = full_out + pieces["short"] = full_out[:7] # maybe improved later + pieces["error"] = None + + # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] + # TAG might have hyphens. + git_describe = describe_out + + # look for -dirty suffix + dirty = git_describe.endswith("-dirty") + pieces["dirty"] = dirty + if dirty: + git_describe = git_describe[:git_describe.rindex("-dirty")] + + # now we have TAG-NUM-gHEX or HEX + + if "-" in git_describe: + # TAG-NUM-gHEX + mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) + if not mo: + # unparseable. Maybe git-describe is misbehaving? + pieces["error"] = ("unable to parse git-describe output: '%s'" + % describe_out) + return pieces + + # tag + full_tag = mo.group(1) + if not full_tag.startswith(tag_prefix): + if verbose: + fmt = "tag '%s' doesn't start with prefix '%s'" + print(fmt % (full_tag, tag_prefix)) + pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" + % (full_tag, tag_prefix)) + return pieces + pieces["closest-tag"] = full_tag[len(tag_prefix):] + + # distance: number of commits since tag + pieces["distance"] = int(mo.group(2)) + + # commit: short hex revision ID + pieces["short"] = mo.group(3) + + else: + # HEX: no tags + pieces["closest-tag"] = None + count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], + cwd=root) + pieces["distance"] = int(count_out) # total number of commits + + # commit date: see ISO-8601 comment in git_versions_from_keywords() + date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"], + cwd=root)[0].strip() + pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) + + return pieces + + +def plus_or_dot(pieces): + """Return a + if we don't already have one, else return a .""" + if "+" in pieces.get("closest-tag", ""): + return "." + return "+" + + +def render_pep440(pieces): + """Build up version string, with post-release "local version identifier". + + Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you + get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty + + Exceptions: + 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += plus_or_dot(pieces) + rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + else: + # exception #1 + rendered = "0+untagged.%d.g%s" % (pieces["distance"], + pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + return rendered + + +def render_pep440_pre(pieces): + """TAG[.post.devDISTANCE] -- No -dirty. + + Exceptions: + 1: no tags. 0.post.devDISTANCE + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"]: + rendered += ".post.dev%d" % pieces["distance"] + else: + # exception #1 + rendered = "0.post.dev%d" % pieces["distance"] + return rendered + + +def render_pep440_post(pieces): + """TAG[.postDISTANCE[.dev0]+gHEX] . + + The ".dev0" means dirty. Note that .dev0 sorts backwards + (a dirty tree will appear "older" than the corresponding clean one), + but you shouldn't be releasing software with -dirty anyways. + + Exceptions: + 1: no tags. 0.postDISTANCE[.dev0] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += ".post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + rendered += plus_or_dot(pieces) + rendered += "g%s" % pieces["short"] + else: + # exception #1 + rendered = "0.post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + rendered += "+g%s" % pieces["short"] + return rendered + + +def render_pep440_old(pieces): + """TAG[.postDISTANCE[.dev0]] . + + The ".dev0" means dirty. + + Eexceptions: + 1: no tags. 0.postDISTANCE[.dev0] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += ".post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + else: + # exception #1 + rendered = "0.post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + return rendered + + +def render_git_describe(pieces): + """TAG[-DISTANCE-gHEX][-dirty]. + + Like 'git describe --tags --dirty --always'. + + Exceptions: + 1: no tags. HEX[-dirty] (note: no 'g' prefix) + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"]: + rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) + else: + # exception #1 + rendered = pieces["short"] + if pieces["dirty"]: + rendered += "-dirty" + return rendered + + +def render_git_describe_long(pieces): + """TAG-DISTANCE-gHEX[-dirty]. + + Like 'git describe --tags --dirty --always -long'. + The distance/hash is unconditional. + + Exceptions: + 1: no tags. HEX[-dirty] (note: no 'g' prefix) + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) + else: + # exception #1 + rendered = pieces["short"] + if pieces["dirty"]: + rendered += "-dirty" + return rendered + + +def render(pieces, style): + """Render the given version pieces into the requested style.""" + if pieces["error"]: + return {"version": "unknown", + "full-revisionid": pieces.get("long"), + "dirty": None, + "error": pieces["error"], + "date": None} + + if not style or style == "default": + style = "pep440" # the default + + if style == "pep440": + rendered = render_pep440(pieces) + elif style == "pep440-pre": + rendered = render_pep440_pre(pieces) + elif style == "pep440-post": + rendered = render_pep440_post(pieces) + elif style == "pep440-old": + rendered = render_pep440_old(pieces) + elif style == "git-describe": + rendered = render_git_describe(pieces) + elif style == "git-describe-long": + rendered = render_git_describe_long(pieces) + else: + raise ValueError("unknown style '%s'" % style) + + return {"version": rendered, "full-revisionid": pieces["long"], + "dirty": pieces["dirty"], "error": None, + "date": pieces.get("date")} + + +def get_versions(): + """Get version information or return default if unable to do so.""" + # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have + # __file__, we can work backwards from there to the root. Some + # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which + # case we can only use expanded keywords. + + cfg = get_config() + verbose = cfg.verbose + + try: + return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, + verbose) + except NotThisMethod: + pass + + try: + root = os.path.realpath(__file__) + # versionfile_source is the relative path from the top of the source + # tree (where the .git directory might live) to this file. Invert + # this to find the root from __file__. + for i in cfg.versionfile_source.split('/'): + root = os.path.dirname(root) + except NameError: + return {"version": "0+unknown", "full-revisionid": None, + "dirty": None, + "error": "unable to find root of source tree", + "date": None} + + try: + pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) + return render(pieces, cfg.style) + except NotThisMethod: + pass + + try: + if cfg.parentdir_prefix: + return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) + except NotThisMethod: + pass + + return {"version": "0+unknown", "full-revisionid": None, + "dirty": None, + "error": "unable to compute version", "date": None} diff --git a/ccxt/static_dependencies/ecdsa/curves.py b/ccxt/static_dependencies/ecdsa/curves.py new file mode 100644 index 0000000..5ff53ad --- /dev/null +++ b/ccxt/static_dependencies/ecdsa/curves.py @@ -0,0 +1,56 @@ +from __future__ import division + +from . import der, ecdsa + + +class UnknownCurveError(Exception): + pass + + +def orderlen(order): + return (1+len("%x" % order))//2 # bytes + + +# the NIST curves +class Curve: + def __init__(self, name, curve, generator, oid, openssl_name=None): + self.name = name + self.openssl_name = openssl_name # maybe None + self.curve = curve + self.generator = generator + self.order = generator.order() + self.baselen = orderlen(self.order) + self.verifying_key_length = 2*self.baselen + self.signature_length = 2*self.baselen + self.oid = oid + self.encoded_oid = der.encode_oid(*oid) + +NIST192p = Curve("NIST192p", ecdsa.curve_192, + ecdsa.generator_192, + (1, 2, 840, 10045, 3, 1, 1), "prime192v1") +NIST224p = Curve("NIST224p", ecdsa.curve_224, + ecdsa.generator_224, + (1, 3, 132, 0, 33), "secp224r1") +NIST256p = Curve("NIST256p", ecdsa.curve_256, + ecdsa.generator_256, + (1, 2, 840, 10045, 3, 1, 7), "prime256v1") +NIST384p = Curve("NIST384p", ecdsa.curve_384, + ecdsa.generator_384, + (1, 3, 132, 0, 34), "secp384r1") +NIST521p = Curve("NIST521p", ecdsa.curve_521, + ecdsa.generator_521, + (1, 3, 132, 0, 35), "secp521r1") +SECP256k1 = Curve("SECP256k1", ecdsa.curve_secp256k1, + ecdsa.generator_secp256k1, + (1, 3, 132, 0, 10), "secp256k1") + +curves = [NIST192p, NIST224p, NIST256p, NIST384p, NIST521p, SECP256k1] + + +def find_curve(oid_curve): + for c in curves: + if c.oid == oid_curve: + return c + raise UnknownCurveError("I don't know about the curve with oid %s." + "I only know about these: %s" % + (oid_curve, [c.name for c in curves])) diff --git a/ccxt/static_dependencies/ecdsa/der.py b/ccxt/static_dependencies/ecdsa/der.py new file mode 100644 index 0000000..98a86e9 --- /dev/null +++ b/ccxt/static_dependencies/ecdsa/der.py @@ -0,0 +1,221 @@ +from __future__ import division + +import binascii +import base64 + + +class UnexpectedDER(Exception): + pass + + +def encode_constructed(tag, value): + return int.to_bytes(0xa0+tag, 1, 'big') + encode_length(len(value)) + value + + +def encode_integer(r): + assert r >= 0 # can't support negative numbers yet + h = ("%x" % r).encode() + if len(h) % 2: + h = b'0' + h + s = binascii.unhexlify(h) + num = s[0] if isinstance(s[0], int) else ord(s[0]) + if num <= 0x7f: + return b'\x02' + int.to_bytes(len(s), 1, 'big') + s + else: + # DER integers are two's complement, so if the first byte is + # 0x80-0xff then we need an extra 0x00 byte to prevent it from + # looking negative. + return b'\x02' + int.to_bytes(len(s)+1, 1, 'big') + b'\x00' + s + + +def encode_bitstring(s): + return b'\x03' + encode_length(len(s)) + s + + +def encode_octet_string(s): + return b'\x04' + encode_length(len(s)) + s + + +def encode_oid(first, second, *pieces): + assert first <= 2 + assert second <= 39 + encoded_pieces = [int.to_bytes(40*first+second, 1, 'big')] + [encode_number(p) + for p in pieces] + body = b''.join(encoded_pieces) + return b'\x06' + encode_length(len(body)) + body + + +def encode_sequence(*encoded_pieces): + total_len = sum([len(p) for p in encoded_pieces]) + return b'\x30' + encode_length(total_len) + b''.join(encoded_pieces) + + +def encode_number(n): + b128_digits = [] + while n: + b128_digits.insert(0, (n & 0x7f) | 0x80) + n = n >> 7 + if not b128_digits: + b128_digits.append(0) + b128_digits[-1] &= 0x7f + return b''.join([int.to_bytes(d, 1, 'big') for d in b128_digits]) + + +def remove_constructed(string): + s0 = string[0] if isinstance(string[0], int) else ord(string[0]) + if (s0 & 0xe0) != 0xa0: + raise UnexpectedDER("wanted constructed tag (0xa0-0xbf), got 0x%02x" + % s0) + tag = s0 & 0x1f + length, llen = read_length(string[1:]) + body = string[1+llen:1+llen+length] + rest = string[1+llen+length:] + return tag, body, rest + + +def remove_sequence(string): + if not string.startswith(b'\x30'): + n = string[0] if isinstance(string[0], int) else ord(string[0]) + raise UnexpectedDER("wanted sequence (0x30), got 0x%02x" % n) + length, lengthlength = read_length(string[1:]) + endseq = 1+lengthlength+length + return string[1+lengthlength:endseq], string[endseq:] + + +def remove_octet_string(string): + if not string.startswith(b'\x04'): + n = string[0] if isinstance(string[0], int) else ord(string[0]) + raise UnexpectedDER("wanted octetstring (0x04), got 0x%02x" % n) + length, llen = read_length(string[1:]) + body = string[1+llen:1+llen+length] + rest = string[1+llen+length:] + return body, rest + + +def remove_object(string): + if not string.startswith(b'\x06'): + n = string[0] if isinstance(string[0], int) else ord(string[0]) + raise UnexpectedDER("wanted object (0x06), got 0x%02x" % n) + length, lengthlength = read_length(string[1:]) + body = string[1+lengthlength:1+lengthlength+length] + rest = string[1+lengthlength+length:] + numbers = [] + while body: + n, ll = read_number(body) + numbers.append(n) + body = body[ll:] + n0 = numbers.pop(0) + first = n0//40 + second = n0-(40*first) + numbers.insert(0, first) + numbers.insert(1, second) + return tuple(numbers), rest + + +def remove_integer(string): + if not string.startswith(b'\x02'): + n = string[0] if isinstance(string[0], int) else ord(string[0]) + raise UnexpectedDER("wanted integer (0x02), got 0x%02x" % n) + length, llen = read_length(string[1:]) + numberbytes = string[1+llen:1+llen+length] + rest = string[1+llen+length:] + nbytes = numberbytes[0] if isinstance(numberbytes[0], int) else ord(numberbytes[0]) + assert nbytes < 0x80 # can't support negative numbers yet + return int(binascii.hexlify(numberbytes), 16), rest + + +def read_number(string): + number = 0 + llen = 0 + # base-128 big endian, with b7 set in all but the last byte + while True: + if llen > len(string): + raise UnexpectedDER("ran out of length bytes") + number = number << 7 + d = string[llen] if isinstance(string[llen], int) else ord(string[llen]) + number += (d & 0x7f) + llen += 1 + if not d & 0x80: + break + return number, llen + + +def encode_length(l): + assert l >= 0 + if l < 0x80: + return int.to_bytes(l, 1, 'big') + s = ("%x" % l).encode() + if len(s) % 2: + s = b'0' + s + s = binascii.unhexlify(s) + llen = len(s) + return int.to_bytes(0x80 | llen, 1, 'big') + s + + +def read_length(string): + num = string[0] if isinstance(string[0], int) else ord(string[0]) + if not (num & 0x80): + # short form + return (num & 0x7f), 1 + # else long-form: b0&0x7f is number of additional base256 length bytes, + # big-endian + llen = num & 0x7f + if llen > len(string)-1: + raise UnexpectedDER("ran out of length bytes") + return int(binascii.hexlify(string[1:1+llen]), 16), 1+llen + + +def remove_bitstring(string): + num = string[0] if isinstance(string[0], int) else ord(string[0]) + if not string.startswith(b'\x03'): + raise UnexpectedDER("wanted bitstring (0x03), got 0x%02x" % num) + length, llen = read_length(string[1:]) + body = string[1+llen:1+llen+length] + rest = string[1+llen+length:] + return body, rest + +# SEQUENCE([1, STRING(secexp), cont[0], OBJECT(curvename), cont[1], BINTSTRING) + + +# signatures: (from RFC3279) +# ansi-X9-62 OBJECT IDENTIFIER ::= { +# iso(1) member-body(2) us(840) 10045 } +# +# id-ecSigType OBJECT IDENTIFIER ::= { +# ansi-X9-62 signatures(4) } +# ecdsa-with-SHA1 OBJECT IDENTIFIER ::= { +# id-ecSigType 1 } +## so 1,2,840,10045,4,1 +## so 0x42, .. .. + +# Ecdsa-Sig-Value ::= SEQUENCE { +# r INTEGER, +# s INTEGER } + +# id-public-key-type OBJECT IDENTIFIER ::= { ansi-X9.62 2 } +# +# id-ecPublicKey OBJECT IDENTIFIER ::= { id-publicKeyType 1 } + +# I think the secp224r1 identifier is (t=06,l=05,v=2b81040021) +# secp224r1 OBJECT IDENTIFIER ::= { +# iso(1) identified-organization(3) certicom(132) curve(0) 33 } +# and the secp384r1 is (t=06,l=05,v=2b81040022) +# secp384r1 OBJECT IDENTIFIER ::= { +# iso(1) identified-organization(3) certicom(132) curve(0) 34 } + +def unpem(pem): + if isinstance(pem, str): + pem = pem.encode() + + d = b''.join([l.strip() for l in pem.split(b'\n') + if l and not l.startswith(b'-----')]) + return base64.b64decode(d) + + +def topem(der, name): + b64 = base64.b64encode(der) + lines = [("-----BEGIN %s-----\n" % name).encode()] + lines.extend([b64[start:start+64]+b'\n' + for start in range(0, len(b64), 64)]) + lines.append(("-----END %s-----\n" % name).encode()) + return b''.join(lines) diff --git a/ccxt/static_dependencies/ecdsa/ecdsa.py b/ccxt/static_dependencies/ecdsa/ecdsa.py new file mode 100644 index 0000000..ec67349 --- /dev/null +++ b/ccxt/static_dependencies/ecdsa/ecdsa.py @@ -0,0 +1,310 @@ +#! /usr/bin/env python + +""" +Implementation of Elliptic-Curve Digital Signatures. + +Classes and methods for elliptic-curve signatures: +private keys, public keys, signatures, +NIST prime-modulus curves with modulus lengths of +192, 224, 256, 384, and 521 bits. + +Example: + + # (In real-life applications, you would probably want to + # protect against defects in SystemRandom.) + from random import SystemRandom + randrange = SystemRandom().randrange + + # Generate a public/private key pair using the NIST Curve P-192: + + g = generator_192 + n = g.order() + secret = randrange( 1, n ) + pubkey = Public_key( g, g * secret ) + privkey = Private_key( pubkey, secret ) + + # Signing a hash value: + + hash = randrange( 1, n ) + signature = privkey.sign( hash, randrange( 1, n ) ) + + # Verifying a signature for a hash value: + + if pubkey.verifies( hash, signature ): + print_("Demo verification succeeded.") + else: + print_("*** Demo verification failed.") + + # Verification fails if the hash value is modified: + + if pubkey.verifies( hash-1, signature ): + print_("**** Demo verification failed to reject tampered hash.") + else: + print_("Demo verification correctly rejected tampered hash.") + +Version of 2009.05.16. + +Revision history: + 2005.12.31 - Initial version. + 2008.11.25 - Substantial revisions introducing new classes. + 2009.05.16 - Warn against using random.randrange in real applications. + 2009.05.17 - Use random.SystemRandom by default. + +Written in 2005 by Peter Pearson and placed in the public domain. +""" + +from . import ellipticcurve +from . import numbertheory + + +class RSZeroError(RuntimeError): + pass + + +class Signature(object): + """ECDSA signature. + """ + + def __init__(self, r, s, recovery_param): + self.r = r + self.s = s + self.recovery_param = recovery_param + + def recover_public_keys(self, hash, generator): + """Returns two public keys for which the signature is valid + hash is signed hash + generator is the used generator of the signature + """ + curve = generator.curve() + n = generator.order() + r = self.r + s = self.s + e = hash + x = r + + # Compute the curve point with x as x-coordinate + alpha = (pow(x, 3, curve.p()) + (curve.a() * x) + curve.b()) % curve.p() + beta = numbertheory.square_root_mod_prime(alpha, curve.p()) + y = beta if beta % 2 == 0 else curve.p() - beta + + # Compute the public key + R1 = ellipticcurve.Point(curve, x, y, n) + Q1 = numbertheory.inverse_mod(r, n) * (s * R1 + (-e % n) * generator) + Pk1 = Public_key(generator, Q1) + + # And the second solution + R2 = ellipticcurve.Point(curve, x, -y, n) + Q2 = numbertheory.inverse_mod(r, n) * (s * R2 + (-e % n) * generator) + Pk2 = Public_key(generator, Q2) + + return [Pk1, Pk2] + + +class Public_key(object): + """Public key for ECDSA. + """ + + def __init__(self, generator, point): + """generator is the Point that generates the group, + point is the Point that defines the public key. + """ + + self.curve = generator.curve() + self.generator = generator + self.point = point + n = generator.order() + if not n: + raise RuntimeError("Generator point must have order.") + if not n * point == ellipticcurve.INFINITY: + raise RuntimeError("Generator point order is bad.") + if point.x() < 0 or n <= point.x() or point.y() < 0 or n <= point.y(): + raise RuntimeError("Generator point has x or y out of range.") + + def verifies(self, hash, signature): + """Verify that signature is a valid signature of hash. + Return True if the signature is valid. + """ + + # From X9.62 J.3.1. + + G = self.generator + n = G.order() + r = signature.r + s = signature.s + if r < 1 or r > n - 1: + return False + if s < 1 or s > n - 1: + return False + c = numbertheory.inverse_mod(s, n) + u1 = (hash * c) % n + u2 = (r * c) % n + xy = u1 * G + u2 * self.point + v = xy.x() % n + return v == r + + +class Private_key(object): + """Private key for ECDSA. + """ + + def __init__(self, public_key, secret_multiplier): + """public_key is of class Public_key; + secret_multiplier is a large integer. + """ + + self.public_key = public_key + self.secret_multiplier = secret_multiplier + + def sign(self, hash, random_k): + """Return a signature for the provided hash, using the provided + random nonce. It is absolutely vital that random_k be an unpredictable + number in the range [1, self.public_key.point.order()-1]. If + an attacker can guess random_k, he can compute our private key from a + single signature. Also, if an attacker knows a few high-order + bits (or a few low-order bits) of random_k, he can compute our private + key from many signatures. The generation of nonces with adequate + cryptographic strength is very difficult and far beyond the scope + of this comment. + + May raise RuntimeError, in which case retrying with a new + random value k is in order. + """ + + G = self.public_key.generator + n = G.order() + k = random_k % n + p1 = k * G + r = p1.x() % n + if r == 0: + raise RSZeroError("amazingly unlucky random number r") + s = (numbertheory.inverse_mod(k, n) * + (hash + (self.secret_multiplier * r) % n)) % n + if s == 0: + raise RSZeroError("amazingly unlucky random number s") + recovery_param = p1.y() % 2 or (2 if p1.x() == k else 0) + return Signature(r, s, recovery_param) + + +def int_to_string(x): + """Convert integer x into a string of bytes, as per X9.62.""" + assert x >= 0 + if x == 0: + return b'\0' + result = [] + while x: + ordinal = x & 0xFF + result.append(int.to_bytes(ordinal, 1, 'big')) + x >>= 8 + + result.reverse() + return b''.join(result) + + +def string_to_int(s): + """Convert a string of bytes into an integer, as per X9.62.""" + result = 0 + for c in s: + if not isinstance(c, int): + c = ord(c) + result = 256 * result + c + return result + + +def digest_integer(m): + """Convert an integer into a string of bytes, compute + its SHA-1 hash, and convert the result to an integer.""" + # + # I don't expect this function to be used much. I wrote + # it in order to be able to duplicate the examples + # in ECDSAVS. + # + from hashlib import sha1 + return string_to_int(sha1(int_to_string(m)).digest()) + + +def point_is_valid(generator, x, y): + """Is (x,y) a valid public key based on the specified generator?""" + + # These are the tests specified in X9.62. + + n = generator.order() + curve = generator.curve() + if x < 0 or n <= x or y < 0 or n <= y: + return False + if not curve.contains_point(x, y): + return False + if not n * ellipticcurve.Point(curve, x, y) == ellipticcurve.INFINITY: + return False + return True + + +# NIST Curve P-192: +_p = 6277101735386680763835789423207666416083908700390324961279 +_r = 6277101735386680763835789423176059013767194773182842284081 +# s = 0x3045ae6fc8422f64ed579528d38120eae12196d5L +# c = 0x3099d2bbbfcb2538542dcd5fb078b6ef5f3d6fe2c745de65L +_b = 0x64210519e59c80e70fa7e9ab72243049feb8deecc146b9b1 +_Gx = 0x188da80eb03090f67cbf20eb43a18800f4ff0afd82ff1012 +_Gy = 0x07192b95ffc8da78631011ed6b24cdd573f977a11e794811 + +curve_192 = ellipticcurve.CurveFp(_p, -3, _b) +generator_192 = ellipticcurve.Point(curve_192, _Gx, _Gy, _r) + +# NIST Curve P-224: +_p = 26959946667150639794667015087019630673557916260026308143510066298881 +_r = 26959946667150639794667015087019625940457807714424391721682722368061 +# s = 0xbd71344799d5c7fcdc45b59fa3b9ab8f6a948bc5L +# c = 0x5b056c7e11dd68f40469ee7f3c7a7d74f7d121116506d031218291fbL +_b = 0xb4050a850c04b3abf54132565044b0b7d7bfd8ba270b39432355ffb4 +_Gx = 0xb70e0cbd6bb4bf7f321390b94a03c1d356c21122343280d6115c1d21 +_Gy = 0xbd376388b5f723fb4c22dfe6cd4375a05a07476444d5819985007e34 + +curve_224 = ellipticcurve.CurveFp(_p, -3, _b) +generator_224 = ellipticcurve.Point(curve_224, _Gx, _Gy, _r) + +# NIST Curve P-256: +_p = 115792089210356248762697446949407573530086143415290314195533631308867097853951 +_r = 115792089210356248762697446949407573529996955224135760342422259061068512044369 +# s = 0xc49d360886e704936a6678e1139d26b7819f7e90L +# c = 0x7efba1662985be9403cb055c75d4f7e0ce8d84a9c5114abcaf3177680104fa0dL +_b = 0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b +_Gx = 0x6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296 +_Gy = 0x4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5 + +curve_256 = ellipticcurve.CurveFp(_p, -3, _b) +generator_256 = ellipticcurve.Point(curve_256, _Gx, _Gy, _r) + +# NIST Curve P-384: +_p = 39402006196394479212279040100143613805079739270465446667948293404245721771496870329047266088258938001861606973112319 +_r = 39402006196394479212279040100143613805079739270465446667946905279627659399113263569398956308152294913554433653942643 +# s = 0xa335926aa319a27a1d00896a6773a4827acdac73L +# c = 0x79d1e655f868f02fff48dcdee14151ddb80643c1406d0ca10dfe6fc52009540a495e8042ea5f744f6e184667cc722483L +_b = 0xb3312fa7e23ee7e4988e056be3f82d19181d9c6efe8141120314088f5013875ac656398d8a2ed19d2a85c8edd3ec2aef +_Gx = 0xaa87ca22be8b05378eb1c71ef320ad746e1d3b628ba79b9859f741e082542a385502f25dbf55296c3a545e3872760ab7 +_Gy = 0x3617de4a96262c6f5d9e98bf9292dc29f8f41dbd289a147ce9da3113b5f0b8c00a60b1ce1d7e819d7a431d7c90ea0e5f + +curve_384 = ellipticcurve.CurveFp(_p, -3, _b) +generator_384 = ellipticcurve.Point(curve_384, _Gx, _Gy, _r) + +# NIST Curve P-521: +_p = 6864797660130609714981900799081393217269435300143305409394463459185543183397656052122559640661454554977296311391480858037121987999716643812574028291115057151 +_r = 6864797660130609714981900799081393217269435300143305409394463459185543183397655394245057746333217197532963996371363321113864768612440380340372808892707005449 +# s = 0xd09e8800291cb85396cc6717393284aaa0da64baL +# c = 0x0b48bfa5f420a34949539d2bdfc264eeeeb077688e44fbf0ad8f6d0edb37bd6b533281000518e19f1b9ffbe0fe9ed8a3c2200b8f875e523868c70c1e5bf55bad637L +_b = 0x051953eb9618e1c9a1f929a21a0b68540eea2da725b99b315f3b8b489918ef109e156193951ec7e937b1652c0bd3bb1bf073573df883d2c34f1ef451fd46b503f00 +_Gx = 0xc6858e06b70404e9cd9e3ecb662395b4429c648139053fb521f828af606b4d3dbaa14b5e77efe75928fe1dc127a2ffa8de3348b3c1856a429bf97e7e31c2e5bd66 +_Gy = 0x11839296a789a3bc0045c8a5fb42c7d1bd998f54449579b446817afbd17273e662c97ee72995ef42640c550b9013fad0761353c7086a272c24088be94769fd16650 + +curve_521 = ellipticcurve.CurveFp(_p, -3, _b) +generator_521 = ellipticcurve.Point(curve_521, _Gx, _Gy, _r) + +# Certicom secp256-k1 +_a = 0x0000000000000000000000000000000000000000000000000000000000000000 +_b = 0x0000000000000000000000000000000000000000000000000000000000000007 +_p = 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f +_Gx = 0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 +_Gy = 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8 +_r = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141 + +curve_secp256k1 = ellipticcurve.CurveFp(_p, _a, _b) +generator_secp256k1 = ellipticcurve.Point(curve_secp256k1, _Gx, _Gy, _r) diff --git a/ccxt/static_dependencies/ecdsa/ellipticcurve.py b/ccxt/static_dependencies/ecdsa/ellipticcurve.py new file mode 100644 index 0000000..57c44da --- /dev/null +++ b/ccxt/static_dependencies/ecdsa/ellipticcurve.py @@ -0,0 +1,197 @@ +#! /usr/bin/env python +# +# Implementation of elliptic curves, for cryptographic applications. +# +# This module doesn't provide any way to choose a random elliptic +# curve, nor to verify that an elliptic curve was chosen randomly, +# because one can simply use NIST's standard curves. +# +# Notes from X9.62-1998 (draft): +# Nomenclature: +# - Q is a public key. +# The "Elliptic Curve Domain Parameters" include: +# - q is the "field size", which in our case equals p. +# - p is a big prime. +# - G is a point of prime order (5.1.1.1). +# - n is the order of G (5.1.1.1). +# Public-key validation (5.2.2): +# - Verify that Q is not the point at infinity. +# - Verify that X_Q and Y_Q are in [0,p-1]. +# - Verify that Q is on the curve. +# - Verify that nQ is the point at infinity. +# Signature generation (5.3): +# - Pick random k from [1,n-1]. +# Signature checking (5.4.2): +# - Verify that r and s are in [1,n-1]. +# +# Version of 2008.11.25. +# +# Revision history: +# 2005.12.31 - Initial version. +# 2008.11.25 - Change CurveFp.is_on to contains_point. +# +# Written in 2005 by Peter Pearson and placed in the public domain. + +from __future__ import division + +from . import numbertheory + + +class CurveFp(object): + """Elliptic Curve over the field of integers modulo a prime.""" + + def __init__(self, p, a, b): + """The curve of points satisfying y^2 = x^3 + a*x + b (mod p).""" + self.__p = p + self.__a = a + self.__b = b + + def p(self): + return self.__p + + def a(self): + return self.__a + + def b(self): + return self.__b + + def contains_point(self, x, y): + """Is the point (x,y) on this curve?""" + return (y * y - (x * x * x + self.__a * x + self.__b)) % self.__p == 0 + + def __str__(self): + return "CurveFp(p=%d, a=%d, b=%d)" % (self.__p, self.__a, self.__b) + + +class Point(object): + """A point on an elliptic curve. Altering x and y is forbidding, + but they can be read by the x() and y() methods.""" + + def __init__(self, curve, x, y, order=None): + """curve, x, y, order; order (optional) is the order of this point.""" + self.__curve = curve + self.__x = x + self.__y = y + self.__order = order + # self.curve is allowed to be None only for INFINITY: + if self.__curve: + assert self.__curve.contains_point(x, y) + if order: + assert self * order == INFINITY + + def __eq__(self, other): + """Return True if the points are identical, False otherwise.""" + if self.__curve == other.__curve \ + and self.__x == other.__x \ + and self.__y == other.__y: + return True + else: + return False + + def __add__(self, other): + """Add one point to another point.""" + + # X9.62 B.3: + + if other == INFINITY: + return self + if self == INFINITY: + return other + assert self.__curve == other.__curve + if self.__x == other.__x: + if (self.__y + other.__y) % self.__curve.p() == 0: + return INFINITY + else: + return self.double() + + p = self.__curve.p() + + l = ((other.__y - self.__y) * \ + numbertheory.inverse_mod(other.__x - self.__x, p)) % p + + x3 = (l * l - self.__x - other.__x) % p + y3 = (l * (self.__x - x3) - self.__y) % p + + return Point(self.__curve, x3, y3) + + def __mul__(self, other): + """Multiply a point by an integer.""" + + def leftmost_bit(x): + assert x > 0 + result = 1 + while result <= x: + result = 2 * result + return result // 2 + + e = other + if self.__order: + e = e % self.__order + if e == 0: + return INFINITY + if self == INFINITY: + return INFINITY + assert e > 0 + + # From X9.62 D.3.2: + + e3 = 3 * e + negative_self = Point(self.__curve, self.__x, -self.__y, self.__order) + i = leftmost_bit(e3) // 2 + result = self + # print_("Multiplying %s by %d (e3 = %d):" % (self, other, e3)) + while i > 1: + result = result.double() + if (e3 & i) != 0 and (e & i) == 0: + result = result + self + if (e3 & i) == 0 and (e & i) != 0: + result = result + negative_self + # print_(". . . i = %d, result = %s" % ( i, result )) + i = i // 2 + + return result + + def __rmul__(self, other): + """Multiply a point by an integer.""" + + return self * other + + def __str__(self): + if self == INFINITY: + return "infinity" + return "(%d,%d)" % (self.__x, self.__y) + + def double(self): + """Return a new point that is twice the old.""" + + if self == INFINITY: + return INFINITY + + # X9.62 B.3: + + p = self.__curve.p() + a = self.__curve.a() + + l = ((3 * self.__x * self.__x + a) * \ + numbertheory.inverse_mod(2 * self.__y, p)) % p + + x3 = (l * l - 2 * self.__x) % p + y3 = (l * (self.__x - x3) - self.__y) % p + + return Point(self.__curve, x3, y3) + + def x(self): + return self.__x + + def y(self): + return self.__y + + def curve(self): + return self.__curve + + def order(self): + return self.__order + + +# This one point is the Point At Infinity for all purposes: +INFINITY = Point(None, None, None) diff --git a/ccxt/static_dependencies/ecdsa/keys.py b/ccxt/static_dependencies/ecdsa/keys.py new file mode 100644 index 0000000..ee59cfb --- /dev/null +++ b/ccxt/static_dependencies/ecdsa/keys.py @@ -0,0 +1,332 @@ +import binascii + +from . import ecdsa +from . import der +from . import rfc6979 +from .curves import NIST192p, find_curve +from .ecdsa import RSZeroError +from .util import string_to_number, number_to_string, randrange +from .util import sigencode_string, sigdecode_string +from .util import oid_ecPublicKey, encoded_oid_ecPublicKey +from hashlib import sha1 + + +class BadSignatureError(Exception): + pass + + +class BadDigestError(Exception): + pass + + +class VerifyingKey: + def __init__(self, _error__please_use_generate=None): + if not _error__please_use_generate: + raise TypeError("Please use VerifyingKey.generate() to " + "construct me") + + @classmethod + def from_public_point(klass, point, curve=NIST192p, hashfunc=sha1): + self = klass(_error__please_use_generate=True) + self.curve = curve + self.default_hashfunc = hashfunc + self.pubkey = ecdsa.Public_key(curve.generator, point) + self.pubkey.order = curve.order + return self + + @classmethod + def from_string(klass, string, curve=NIST192p, hashfunc=sha1, + validate_point=True): + order = curve.order + assert (len(string) == curve.verifying_key_length), \ + (len(string), curve.verifying_key_length) + xs = string[:curve.baselen] + ys = string[curve.baselen:] + assert len(xs) == curve.baselen, (len(xs), curve.baselen) + assert len(ys) == curve.baselen, (len(ys), curve.baselen) + x = string_to_number(xs) + y = string_to_number(ys) + if validate_point: + assert ecdsa.point_is_valid(curve.generator, x, y) + from . import ellipticcurve + point = ellipticcurve.Point(curve.curve, x, y, order) + return klass.from_public_point(point, curve, hashfunc) + + @classmethod + def from_pem(klass, string): + return klass.from_der(der.unpem(string)) + + @classmethod + def from_der(klass, string): + # [[oid_ecPublicKey,oid_curve], point_str_bitstring] + s1, empty = der.remove_sequence(string) + if empty != b'': + raise der.UnexpectedDER("trailing junk after DER pubkey: %s" % + binascii.hexlify(empty)) + s2, point_str_bitstring = der.remove_sequence(s1) + # s2 = oid_ecPublicKey,oid_curve + oid_pk, rest = der.remove_object(s2) + oid_curve, empty = der.remove_object(rest) + if empty != b'': + raise der.UnexpectedDER("trailing junk after DER pubkey objects: %s" % + binascii.hexlify(empty)) + assert oid_pk == oid_ecPublicKey, (oid_pk, oid_ecPublicKey) + curve = find_curve(oid_curve) + point_str, empty = der.remove_bitstring(point_str_bitstring) + if empty != b'': + raise der.UnexpectedDER("trailing junk after pubkey pointstring: %s" % + binascii.hexlify(empty)) + assert point_str.startswith(b'\x00\x04') + return klass.from_string(point_str[2:], curve) + + @classmethod + def from_public_key_recovery(klass, signature, data, curve, hashfunc=sha1, sigdecode=sigdecode_string): + # Given a signature and corresponding message this function + # returns a list of verifying keys for this signature and message + + digest = hashfunc(data).digest() + return klass.from_public_key_recovery_with_digest(signature, digest, curve, hashfunc=sha1, sigdecode=sigdecode) + + @classmethod + def from_public_key_recovery_with_digest(klass, signature, digest, curve, hashfunc=sha1, sigdecode=sigdecode_string): + # Given a signature and corresponding digest this function + # returns a list of verifying keys for this signature and message + + generator = curve.generator + r, s = sigdecode(signature, generator.order()) + sig = ecdsa.Signature(r, s) + + digest_as_number = string_to_number(digest) + pks = sig.recover_public_keys(digest_as_number, generator) + + # Transforms the ecdsa.Public_key object into a VerifyingKey + verifying_keys = [klass.from_public_point(pk.point, curve, hashfunc) for pk in pks] + return verifying_keys + + def to_string(self): + # VerifyingKey.from_string(vk.to_string()) == vk as long as the + # curves are the same: the curve itself is not included in the + # serialized form + order = self.pubkey.order + x_str = number_to_string(self.pubkey.point.x(), order) + y_str = number_to_string(self.pubkey.point.y(), order) + return x_str + y_str + + def to_pem(self): + return der.topem(self.to_der(), "PUBLIC KEY") + + def to_der(self): + order = self.pubkey.order + x_str = number_to_string(self.pubkey.point.x(), order) + y_str = number_to_string(self.pubkey.point.y(), order) + point_str = b'\x00\x04' + x_str + y_str + return der.encode_sequence(der.encode_sequence(encoded_oid_ecPublicKey, + self.curve.encoded_oid), + der.encode_bitstring(point_str)) + + def verify(self, signature, data, hashfunc=None, sigdecode=sigdecode_string): + hashfunc = hashfunc or self.default_hashfunc + digest = hashfunc(data).digest() + return self.verify_digest(signature, digest, sigdecode) + + def verify_digest(self, signature, digest, sigdecode=sigdecode_string): + if len(digest) > self.curve.baselen: + raise BadDigestError("this curve (%s) is too short " + "for your digest (%d)" % (self.curve.name, + 8 * len(digest))) + number = string_to_number(digest) + r, s = sigdecode(signature, self.pubkey.order) + sig = ecdsa.Signature(r, s) + if self.pubkey.verifies(number, sig): + return True + raise BadSignatureError + + +class SigningKey: + def __init__(self, _error__please_use_generate=None): + if not _error__please_use_generate: + raise TypeError("Please use SigningKey.generate() to construct me") + + @classmethod + def generate(klass, curve=NIST192p, entropy=None, hashfunc=sha1): + secexp = randrange(curve.order, entropy) + return klass.from_secret_exponent(secexp, curve, hashfunc) + + # to create a signing key from a short (arbitrary-length) seed, convert + # that seed into an integer with something like + # secexp=util.randrange_from_seed__X(seed, curve.order), and then pass + # that integer into SigningKey.from_secret_exponent(secexp, curve) + + @classmethod + def from_secret_exponent(klass, secexp, curve=NIST192p, hashfunc=sha1): + self = klass(_error__please_use_generate=True) + self.curve = curve + self.default_hashfunc = hashfunc + self.baselen = curve.baselen + n = curve.order + assert 1 <= secexp < n + pubkey_point = curve.generator * secexp + pubkey = ecdsa.Public_key(curve.generator, pubkey_point) + pubkey.order = n + self.verifying_key = VerifyingKey.from_public_point(pubkey_point, curve, + hashfunc) + self.privkey = ecdsa.Private_key(pubkey, secexp) + self.privkey.order = n + return self + + @classmethod + def from_string(klass, string, curve=NIST192p, hashfunc=sha1): + assert len(string) == curve.baselen, (len(string), curve.baselen) + secexp = string_to_number(string) + return klass.from_secret_exponent(secexp, curve, hashfunc) + + @classmethod + def from_pem(klass, string, hashfunc=sha1): + # the privkey pem file has two sections: "EC PARAMETERS" and "EC + # PRIVATE KEY". The first is redundant. + if isinstance(string, str): + string = string.encode() + privkey_pem = string[string.index(b'-----BEGIN EC PRIVATE KEY-----'):] + return klass.from_der(der.unpem(privkey_pem), hashfunc) + + @classmethod + def from_der(klass, string, hashfunc=sha1): + # SEQ([int(1), octetstring(privkey),cont[0], oid(secp224r1), + # cont[1],bitstring]) + s, empty = der.remove_sequence(string) + if empty != b'': + raise der.UnexpectedDER("trailing junk after DER privkey: %s" % + binascii.hexlify(empty)) + one, s = der.remove_integer(s) + if one != 1: + raise der.UnexpectedDER("expected '1' at start of DER privkey," + " got %d" % one) + privkey_str, s = der.remove_octet_string(s) + tag, curve_oid_str, s = der.remove_constructed(s) + if tag != 0: + raise der.UnexpectedDER("expected tag 0 in DER privkey," + " got %d" % tag) + curve_oid, empty = der.remove_object(curve_oid_str) + if empty != b'': + raise der.UnexpectedDER("trailing junk after DER privkey " + "curve_oid: %s" % binascii.hexlify(empty)) + curve = find_curve(curve_oid) + + # we don't actually care about the following fields + # + # tag, pubkey_bitstring, s = der.remove_constructed(s) + # if tag != 1: + # raise der.UnexpectedDER("expected tag 1 in DER privkey, got %d" + # % tag) + # pubkey_str = der.remove_bitstring(pubkey_bitstring) + # if empty != "": + # raise der.UnexpectedDER("trailing junk after DER privkey " + # "pubkeystr: %s" % binascii.hexlify(empty)) + + # our from_string method likes fixed-length privkey strings + if len(privkey_str) < curve.baselen: + privkey_str = b'\x00' * (curve.baselen - len(privkey_str)) + privkey_str + return klass.from_string(privkey_str, curve, hashfunc) + + def to_string(self): + secexp = self.privkey.secret_multiplier + s = number_to_string(secexp, self.privkey.order) + return s + + def to_pem(self): + # TODO: "BEGIN ECPARAMETERS" + return der.topem(self.to_der(), "EC PRIVATE KEY") + + def to_der(self): + # SEQ([int(1), octetstring(privkey),cont[0], oid(secp224r1), + # cont[1],bitstring]) + encoded_vk = b'\x00\x04' + self.get_verifying_key().to_string() + return der.encode_sequence(der.encode_integer(1), + der.encode_octet_string(self.to_string()), + der.encode_constructed(0, self.curve.encoded_oid), + der.encode_constructed(1, der.encode_bitstring(encoded_vk)), + ) + + def get_verifying_key(self): + return self.verifying_key + + def sign_deterministic(self, data, hashfunc=None, + sigencode=sigencode_string, + extra_entropy=b''): + hashfunc = hashfunc or self.default_hashfunc + digest = hashfunc(data).digest() + + return self.sign_digest_deterministic( + digest, hashfunc=hashfunc, sigencode=sigencode, + extra_entropy=extra_entropy) + + def sign_digest_deterministic(self, digest, hashfunc=None, + sigencode=sigencode_string, + extra_entropy=b''): + """ + Calculates 'k' from data itself, removing the need for strong + random generator and producing deterministic (reproducible) signatures. + See RFC 6979 for more details. + """ + secexp = self.privkey.secret_multiplier + + def simple_r_s(r, s, order, v): + return r, s, order, v + + retry_gen = 0 + while True: + k = rfc6979.generate_k( + self.curve.generator.order(), secexp, hashfunc, digest, + retry_gen=retry_gen, extra_entropy=extra_entropy) + try: + r, s, order, v = self.sign_digest(digest, sigencode=simple_r_s, k=k) + break + except RSZeroError: + retry_gen += 1 + + return sigencode(r, s, order, v) + + def sign(self, data, entropy=None, hashfunc=None, sigencode=sigencode_string, k=None): + """ + hashfunc= should behave like hashlib.sha1 . The output length of the + hash (in bytes) must not be longer than the length of the curve order + (rounded up to the nearest byte), so using SHA256 with nist256p is + ok, but SHA256 with nist192p is not. (In the 2**-96ish unlikely event + of a hash output larger than the curve order, the hash will + effectively be wrapped mod n). + + Use hashfunc=hashlib.sha1 to match openssl's -ecdsa-with-SHA1 mode, + or hashfunc=hashlib.sha256 for openssl-1.0.0's -ecdsa-with-SHA256. + """ + + hashfunc = hashfunc or self.default_hashfunc + h = hashfunc(data).digest() + return self.sign_digest(h, entropy, sigencode, k) + + def sign_digest(self, digest, entropy=None, sigencode=sigencode_string, k=None): + if len(digest) > self.curve.baselen: + raise BadDigestError("this curve (%s) is too short " + "for your digest (%d)" % (self.curve.name, + 8 * len(digest))) + number = string_to_number(digest) + r, s, v = self.sign_number(number, entropy, k) + return sigencode(r, s, self.privkey.order, v) + + def sign_number(self, number, entropy=None, k=None): + # returns a pair of numbers + order = self.privkey.order + # privkey.sign() may raise RuntimeError in the amazingly unlikely + # (2**-192) event that r=0 or s=0, because that would leak the key. + # We could re-try with a different 'k', but we couldn't test that + # code, so I choose to allow the signature to fail instead. + + # If k is set, it is used directly. In other cases + # it is generated using entropy function + if k is not None: + _k = k + else: + _k = randrange(order, entropy) + + assert 1 <= _k < order + sig = self.privkey.sign(number, _k) + return sig.r, sig.s, sig.recovery_param diff --git a/ccxt/static_dependencies/ecdsa/numbertheory.py b/ccxt/static_dependencies/ecdsa/numbertheory.py new file mode 100644 index 0000000..8264da0 --- /dev/null +++ b/ccxt/static_dependencies/ecdsa/numbertheory.py @@ -0,0 +1,531 @@ +#! /usr/bin/env python +# +# Provide some simple capabilities from number theory. +# +# Version of 2008.11.14. +# +# Written in 2005 and 2006 by Peter Pearson and placed in the public domain. +# Revision history: +# 2008.11.14: Use pow(base, exponent, modulus) for modular_exp. +# Make gcd and lcm accept arbitrarly many arguments. + +from __future__ import division + +from functools import reduce + +import math + + +class Error(Exception): + """Base class for exceptions in this module.""" + pass + + +class SquareRootError(Error): + pass + + +class NegativeExponentError(Error): + pass + + +def modular_exp(base, exponent, modulus): + "Raise base to exponent, reducing by modulus" + if exponent < 0: + raise NegativeExponentError("Negative exponents (%d) not allowed" \ + % exponent) + return pow(base, exponent, modulus) + + +# result = 1L +# x = exponent +# b = base + 0L +# while x > 0: +# if x % 2 > 0: result = (result * b) % modulus +# x = x // 2 +# b = (b * b) % modulus +# return result + + +def polynomial_reduce_mod(poly, polymod, p): + """Reduce poly by polymod, integer arithmetic modulo p. + + Polynomials are represented as lists of coefficients + of increasing powers of x.""" + + # This module has been tested only by extensive use + # in calculating modular square roots. + + # Just to make this easy, require a monic polynomial: + assert polymod[-1] == 1 + + assert len(polymod) > 1 + + while len(poly) >= len(polymod): + if poly[-1] != 0: + for i in range(2, len(polymod) + 1): + poly[-i] = (poly[-i] - poly[-1] * polymod[-i]) % p + poly = poly[0:-1] + + return poly + + +def polynomial_multiply_mod(m1, m2, polymod, p): + """Polynomial multiplication modulo a polynomial over ints mod p. + + Polynomials are represented as lists of coefficients + of increasing powers of x.""" + + # This is just a seat-of-the-pants implementation. + + # This module has been tested only by extensive use + # in calculating modular square roots. + + # Initialize the product to zero: + + prod = (len(m1) + len(m2) - 1) * [0] + + # Add together all the cross-terms: + + for i in range(len(m1)): + for j in range(len(m2)): + prod[i + j] = (prod[i + j] + m1[i] * m2[j]) % p + + return polynomial_reduce_mod(prod, polymod, p) + + +def polynomial_exp_mod(base, exponent, polymod, p): + """Polynomial exponentiation modulo a polynomial over ints mod p. + + Polynomials are represented as lists of coefficients + of increasing powers of x.""" + + # Based on the Handbook of Applied Cryptography, algorithm 2.227. + + # This module has been tested only by extensive use + # in calculating modular square roots. + + assert exponent < p + + if exponent == 0: + return [1] + + G = base + k = exponent + if k % 2 == 1: + s = G + else: + s = [1] + + while k > 1: + k = k // 2 + G = polynomial_multiply_mod(G, G, polymod, p) + if k % 2 == 1: + s = polynomial_multiply_mod(G, s, polymod, p) + + return s + + +def jacobi(a, n): + """Jacobi symbol""" + + # Based on the Handbook of Applied Cryptography (HAC), algorithm 2.149. + + # This function has been tested by comparison with a small + # table printed in HAC, and by extensive use in calculating + # modular square roots. + + assert n >= 3 + assert n % 2 == 1 + a = a % n + if a == 0: + return 0 + if a == 1: + return 1 + a1, e = a, 0 + while a1 % 2 == 0: + a1, e = a1 // 2, e + 1 + if e % 2 == 0 or n % 8 == 1 or n % 8 == 7: + s = 1 + else: + s = -1 + if a1 == 1: + return s + if n % 4 == 3 and a1 % 4 == 3: + s = -s + return s * jacobi(n % a1, a1) + + +def square_root_mod_prime(a, p): + """Modular square root of a, mod p, p prime.""" + + # Based on the Handbook of Applied Cryptography, algorithms 3.34 to 3.39. + + # This module has been tested for all values in [0,p-1] for + # every prime p from 3 to 1229. + + assert 0 <= a < p + assert 1 < p + + if a == 0: + return 0 + if p == 2: + return a + + jac = jacobi(a, p) + if jac == -1: + raise SquareRootError("%d has no square root modulo %d" \ + % (a, p)) + + if p % 4 == 3: + return modular_exp(a, (p + 1) // 4, p) + + if p % 8 == 5: + d = modular_exp(a, (p - 1) // 4, p) + if d == 1: + return modular_exp(a, (p + 3) // 8, p) + if d == p - 1: + return (2 * a * modular_exp(4 * a, (p - 5) // 8, p)) % p + raise RuntimeError("Shouldn't get here.") + + for b in range(2, p): + if jacobi(b * b - 4 * a, p) == -1: + f = (a, -b, 1) + ff = polynomial_exp_mod((0, 1), (p + 1) // 2, f, p) + assert ff[1] == 0 + return ff[0] + raise RuntimeError("No b found.") + + +def inverse_mod(a, m): + """Inverse of a mod m.""" + + if a < 0 or m <= a: + a = a % m + + # From Ferguson and Schneier, roughly: + + c, d = a, m + uc, vc, ud, vd = 1, 0, 0, 1 + while c != 0: + q, c, d = divmod(d, c) + (c,) + uc, vc, ud, vd = ud - q * uc, vd - q * vc, uc, vc + + # At this point, d is the GCD, and ud*a+vd*m = d. + # If d == 1, this means that ud is a inverse. + + assert d == 1 + if ud > 0: + return ud + else: + return ud + m + + +def gcd2(a, b): + """Greatest common divisor using Euclid's algorithm.""" + while a: + a, b = b % a, a + return b + + +def gcd(*a): + """Greatest common divisor. + + Usage: gcd([ 2, 4, 6 ]) + or: gcd(2, 4, 6) + """ + + if len(a) > 1: + return reduce(gcd2, a) + if hasattr(a[0], "__iter__"): + return reduce(gcd2, a[0]) + return a[0] + + +def lcm2(a, b): + """Least common multiple of two integers.""" + + return (a * b) // gcd(a, b) + + +def lcm(*a): + """Least common multiple. + + Usage: lcm([ 3, 4, 5 ]) + or: lcm(3, 4, 5) + """ + + if len(a) > 1: + return reduce(lcm2, a) + if hasattr(a[0], "__iter__"): + return reduce(lcm2, a[0]) + return a[0] + + +def factorization(n): + """Decompose n into a list of (prime,exponent) pairs.""" + + assert isinstance(n, int) + + if n < 2: + return [] + + result = [] + d = 2 + + # Test the small primes: + + for d in smallprimes: + if d > n: + break + q, r = divmod(n, d) + if r == 0: + count = 1 + while d <= n: + n = q + q, r = divmod(n, d) + if r != 0: + break + count = count + 1 + result.append((d, count)) + + # If n is still greater than the last of our small primes, + # it may require further work: + + if n > smallprimes[-1]: + if is_prime(n): # If what's left is prime, it's easy: + result.append((n, 1)) + else: # Ugh. Search stupidly for a divisor: + d = smallprimes[-1] + while 1: + d = d + 2 # Try the next divisor. + q, r = divmod(n, d) + if q < d: # n < d*d means we're done, n = 1 or prime. + break + if r == 0: # d divides n. How many times? + count = 1 + n = q + while d <= n: # As long as d might still divide n, + q, r = divmod(n, d) # see if it does. + if r != 0: + break + n = q # It does. Reduce n, increase count. + count = count + 1 + result.append((d, count)) + if n > 1: + result.append((n, 1)) + + return result + + +def phi(n): + """Return the Euler totient function of n.""" + + assert isinstance(n, int) + + if n < 3: + return 1 + + result = 1 + ff = factorization(n) + for f in ff: + e = f[1] + if e > 1: + result = result * f[0] ** (e - 1) * (f[0] - 1) + else: + result = result * (f[0] - 1) + return result + + +def carmichael(n): + """Return Carmichael function of n. + + Carmichael(n) is the smallest integer x such that + m**x = 1 mod n for all m relatively prime to n. + """ + + return carmichael_of_factorized(factorization(n)) + + +def carmichael_of_factorized(f_list): + """Return the Carmichael function of a number that is + represented as a list of (prime,exponent) pairs. + """ + + if len(f_list) < 1: + return 1 + + result = carmichael_of_ppower(f_list[0]) + for i in range(1, len(f_list)): + result = lcm(result, carmichael_of_ppower(f_list[i])) + + return result + + +def carmichael_of_ppower(pp): + """Carmichael function of the given power of the given prime. + """ + + p, a = pp + if p == 2 and a > 2: + return 2 ** (a - 2) + else: + return (p - 1) * p ** (a - 1) + + +def order_mod(x, m): + """Return the order of x in the multiplicative group mod m. + """ + + # Warning: this implementation is not very clever, and will + # take a long time if m is very large. + + if m <= 1: + return 0 + + assert gcd(x, m) == 1 + + z = x + result = 1 + while z != 1: + z = (z * x) % m + result = result + 1 + return result + + +def largest_factor_relatively_prime(a, b): + """Return the largest factor of a relatively prime to b. + """ + + while 1: + d = gcd(a, b) + if d <= 1: + break + b = d + while 1: + q, r = divmod(a, d) + if r > 0: + break + a = q + return a + + +def kinda_order_mod(x, m): + """Return the order of x in the multiplicative group mod m', + where m' is the largest factor of m relatively prime to x. + """ + + return order_mod(x, largest_factor_relatively_prime(m, x)) + + +def is_prime(n): + """Return True if x is prime, False otherwise. + + We use the Miller-Rabin test, as given in Menezes et al. p. 138. + This test is not exact: there are composite values n for which + it returns True. + + In testing the odd numbers from 10000001 to 19999999, + about 66 composites got past the first test, + 5 got past the second test, and none got past the third. + Since factors of 2, 3, 5, 7, and 11 were detected during + preliminary screening, the number of numbers tested by + Miller-Rabin was (19999999 - 10000001)*(2/3)*(4/5)*(6/7) + = 4.57 million. + """ + + # (This is used to study the risk of false positives:) + global miller_rabin_test_count + + miller_rabin_test_count = 0 + + if n <= smallprimes[-1]: + if n in smallprimes: + return True + else: + return False + + if gcd(n, 2 * 3 * 5 * 7 * 11) != 1: + return False + + # Choose a number of iterations sufficient to reduce the + # probability of accepting a composite below 2**-80 + # (from Menezes et al. Table 4.4): + + t = 40 + n_bits = 1 + int(math.log(n, 2)) + for k, tt in ((100, 27), + (150, 18), + (200, 15), + (250, 12), + (300, 9), + (350, 8), + (400, 7), + (450, 6), + (550, 5), + (650, 4), + (850, 3), + (1300, 2), + ): + if n_bits < k: + break + t = tt + + # Run the test t times: + + s = 0 + r = n - 1 + while (r % 2) == 0: + s = s + 1 + r = r // 2 + for i in range(t): + a = smallprimes[i] + y = modular_exp(a, r, n) + if y != 1 and y != n - 1: + j = 1 + while j <= s - 1 and y != n - 1: + y = modular_exp(y, 2, n) + if y == 1: + miller_rabin_test_count = i + 1 + return False + j = j + 1 + if y != n - 1: + miller_rabin_test_count = i + 1 + return False + return True + + +def next_prime(starting_value): + "Return the smallest prime larger than the starting value." + + if starting_value < 2: + return 2 + result = (starting_value + 1) | 1 + while not is_prime(result): + result = result + 2 + return result + + +smallprimes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, + 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, + 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, + 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, + 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, + 263, 269, 271, 277, 281, 283, 293, 307, 311, 313, + 317, 331, 337, 347, 349, 353, 359, 367, 373, 379, + 383, 389, 397, 401, 409, 419, 421, 431, 433, 439, + 443, 449, 457, 461, 463, 467, 479, 487, 491, 499, + 503, 509, 521, 523, 541, 547, 557, 563, 569, 571, + 577, 587, 593, 599, 601, 607, 613, 617, 619, 631, + 641, 643, 647, 653, 659, 661, 673, 677, 683, 691, + 701, 709, 719, 727, 733, 739, 743, 751, 757, 761, + 769, 773, 787, 797, 809, 811, 821, 823, 827, 829, + 839, 853, 857, 859, 863, 877, 881, 883, 887, 907, + 911, 919, 929, 937, 941, 947, 953, 967, 971, 977, + 983, 991, 997, 1009, 1013, 1019, 1021, 1031, 1033, + 1039, 1049, 1051, 1061, 1063, 1069, 1087, 1091, 1093, + 1097, 1103, 1109, 1117, 1123, 1129, 1151, 1153, 1163, + 1171, 1181, 1187, 1193, 1201, 1213, 1217, 1223, 1229] + +miller_rabin_test_count = 0 + diff --git a/ccxt/static_dependencies/ecdsa/rfc6979.py b/ccxt/static_dependencies/ecdsa/rfc6979.py new file mode 100644 index 0000000..7a66e97 --- /dev/null +++ b/ccxt/static_dependencies/ecdsa/rfc6979.py @@ -0,0 +1,100 @@ +''' +RFC 6979: + Deterministic Usage of the Digital Signature Algorithm (DSA) and + Elliptic Curve Digital Signature Algorithm (ECDSA) + + http://tools.ietf.org/html/rfc6979 + +Many thanks to Coda Hale for his implementation in Go language: + https://github.com/codahale/rfc6979 +''' + +import hmac +from binascii import hexlify +from .util import number_to_string, number_to_string_crop + + +def bit_length(num): + # http://docs.python.org/dev/library/stdtypes.html#int.bit_length + s = bin(num) # binary representation: bin(-37) --> '-0b100101' + s = s.lstrip('-0b') # remove leading zeros and minus sign + return len(s) # len('100101') --> 6 + + +def bits2int(data, qlen): + x = int(hexlify(data), 16) + l = len(data) * 8 + + if l > qlen: + return x >> (l - qlen) + return x + + +def bits2octets(data, order): + z1 = bits2int(data, bit_length(order)) + z2 = z1 - order + + if z2 < 0: + z2 = z1 + + return number_to_string_crop(z2, order) + + +# https://tools.ietf.org/html/rfc6979#section-3.2 +def generate_k(order, secexp, hash_func, data, retry_gen=0, extra_entropy=b''): + ''' + order - order of the DSA generator used in the signature + secexp - secure exponent (private key) in numeric form + hash_func - reference to the same hash function used for generating hash + data - hash in binary form of the signing data + retry_gen - int - how many good 'k' values to skip before returning + extra_entropy - extra added data in binary form as per section-3.6 of + rfc6979 + ''' + + qlen = bit_length(order) + holen = hash_func().digest_size + rolen = (qlen + 7) / 8 + bx = number_to_string(secexp, order) + bits2octets(data, order) + \ + extra_entropy + + # Step B + v = b'\x01' * holen + + # Step C + k = b'\x00' * holen + + # Step D + + k = hmac.new(k, v + b'\x00' + bx, hash_func).digest() + + # Step E + v = hmac.new(k, v, hash_func).digest() + + # Step F + k = hmac.new(k, v + b'\x01' + bx, hash_func).digest() + + # Step G + v = hmac.new(k, v, hash_func).digest() + + # Step H + while True: + # Step H1 + t = b'' + + # Step H2 + while len(t) < rolen: + v = hmac.new(k, v, hash_func).digest() + t += v + + # Step H3 + secret = bits2int(t, qlen) + + if secret >= 1 and secret < order: + if retry_gen <= 0: + return secret + else: + retry_gen -= 1 + + k = hmac.new(k, v + b'\x00', hash_func).digest() + v = hmac.new(k, v, hash_func).digest() diff --git a/ccxt/static_dependencies/ecdsa/util.py b/ccxt/static_dependencies/ecdsa/util.py new file mode 100644 index 0000000..5d27a13 --- /dev/null +++ b/ccxt/static_dependencies/ecdsa/util.py @@ -0,0 +1,266 @@ +from __future__ import division + +import os +import math +import binascii +from hashlib import sha256 +from . import der +from .curves import orderlen + +# RFC5480: +# The "unrestricted" algorithm identifier is: +# id-ecPublicKey OBJECT IDENTIFIER ::= { +# iso(1) member-body(2) us(840) ansi-X9-62(10045) keyType(2) 1 } + +oid_ecPublicKey = (1, 2, 840, 10045, 2, 1) +encoded_oid_ecPublicKey = der.encode_oid(*oid_ecPublicKey) + + +def randrange(order, entropy=None): + """Return a random integer k such that 1 <= k < order, uniformly + distributed across that range. For simplicity, this only behaves well if + 'order' is fairly close (but below) a power of 256. The try-try-again + algorithm we use takes longer and longer time (on average) to complete as + 'order' falls, rising to a maximum of avg=512 loops for the worst-case + (256**k)+1 . All of the standard curves behave well. There is a cutoff at + 10k loops (which raises RuntimeError) to prevent an infinite loop when + something is really broken like the entropy function not working. + + Note that this function is not declared to be forwards-compatible: we may + change the behavior in future releases. The entropy= argument (which + should get a callable that behaves like os.urandom) can be used to + achieve stability within a given release (for repeatable unit tests), but + should not be used as a long-term-compatible key generation algorithm. + """ + # we could handle arbitrary orders (even 256**k+1) better if we created + # candidates bit-wise instead of byte-wise, which would reduce the + # worst-case behavior to avg=2 loops, but that would be more complex. The + # change would be to round the order up to a power of 256, subtract one + # (to get 0xffff..), use that to get a byte-long mask for the top byte, + # generate the len-1 entropy bytes, generate one extra byte and mask off + # the top bits, then combine it with the rest. Requires jumping back and + # forth between strings and integers a lot. + + if entropy is None: + entropy = os.urandom + assert order > 1 + bytes = orderlen(order) + dont_try_forever = 10000 # gives about 2**-60 failures for worst case + while dont_try_forever > 0: + dont_try_forever -= 1 + candidate = string_to_number(entropy(bytes)) + 1 + if 1 <= candidate < order: + return candidate + continue + raise RuntimeError("randrange() tried hard but gave up, either something" + " is very wrong or you got realllly unlucky. Order was" + " %x" % order) + + +class PRNG: + # this returns a callable which, when invoked with an integer N, will + # return N pseudorandom bytes. Note: this is a short-term PRNG, meant + # primarily for the needs of randrange_from_seed__trytryagain(), which + # only needs to run it a few times per seed. It does not provide + # protection against state compromise (forward security). + def __init__(self, seed): + self.generator = self.block_generator(seed) + + def __call__(self, numbytes): + a = [next(self.generator) for i in range(numbytes)] + return bytes(a) + + def block_generator(self, seed): + counter = 0 + while True: + for byte in sha256(("prng-%d-%s" % (counter, seed)).encode()).digest(): + yield byte + counter += 1 + + +def randrange_from_seed__overshoot_modulo(seed, order): + # hash the data, then turn the digest into a number in [1,order). + # + # We use David-Sarah Hopwood's suggestion: turn it into a number that's + # sufficiently larger than the group order, then modulo it down to fit. + # This should give adequate (but not perfect) uniformity, and simple + # code. There are other choices: try-try-again is the main one. + base = PRNG(seed)(2 * orderlen(order)) + number = (int(binascii.hexlify(base), 16) % (order - 1)) + 1 + assert 1 <= number < order, (1, number, order) + return number + + +def lsb_of_ones(numbits): + return (1 << numbits) - 1 + + +def bits_and_bytes(order): + bits = int(math.log(order - 1, 2) + 1) + bytes = bits // 8 + extrabits = bits % 8 + return bits, bytes, extrabits + + +# the following randrange_from_seed__METHOD() functions take an +# arbitrarily-sized secret seed and turn it into a number that obeys the same +# range limits as randrange() above. They are meant for deriving consistent +# signing keys from a secret rather than generating them randomly, for +# example a protocol in which three signing keys are derived from a master +# secret. You should use a uniformly-distributed unguessable seed with about +# curve.baselen bytes of entropy. To use one, do this: +# seed = os.urandom(curve.baselen) # or other starting point +# secexp = ecdsa.util.randrange_from_seed__trytryagain(sed, curve.order) +# sk = SigningKey.from_secret_exponent(secexp, curve) + +def randrange_from_seed__truncate_bytes(seed, order, hashmod=sha256): + # hash the seed, then turn the digest into a number in [1,order), but + # don't worry about trying to uniformly fill the range. This will lose, + # on average, four bits of entropy. + bits, _bytes, extrabits = bits_and_bytes(order) + if extrabits: + _bytes += 1 + base = hashmod(seed).digest()[:_bytes] + base = "\x00" * (_bytes - len(base)) + base + number = 1 + int(binascii.hexlify(base), 16) + assert 1 <= number < order + return number + + +def randrange_from_seed__truncate_bits(seed, order, hashmod=sha256): + # like string_to_randrange_truncate_bytes, but only lose an average of + # half a bit + bits = int(math.log(order - 1, 2) + 1) + maxbytes = (bits + 7) // 8 + base = hashmod(seed).digest()[:maxbytes] + base = "\x00" * (maxbytes - len(base)) + base + topbits = 8 * maxbytes - bits + if topbits: + base = int.to_bytes(ord(base[0]) & lsb_of_ones(topbits), 1, 'big') + base[1:] + number = 1 + int(binascii.hexlify(base), 16) + assert 1 <= number < order + return number + + +def randrange_from_seed__trytryagain(seed, order): + # figure out exactly how many bits we need (rounded up to the nearest + # bit), so we can reduce the chance of looping to less than 0.5 . This is + # specified to feed from a byte-oriented PRNG, and discards the + # high-order bits of the first byte as necessary to get the right number + # of bits. The average number of loops will range from 1.0 (when + # order=2**k-1) to 2.0 (when order=2**k+1). + assert order > 1 + bits, bytes, extrabits = bits_and_bytes(order) + generate = PRNG(seed) + while True: + extrabyte = b'' + if extrabits: + extrabyte = int.to_bytes(ord(generate(1)) & lsb_of_ones(extrabits), 1, 'big') + guess = string_to_number(extrabyte + generate(bytes)) + 1 + if 1 <= guess < order: + return guess + + +def number_to_string(num, order): + l = orderlen(order) + fmt_str = "%0" + str(2 * l) + "x" + string = binascii.unhexlify((fmt_str % num).encode()) + assert len(string) == l, (len(string), l) + return string + + +def number_to_string_crop(num, order): + l = orderlen(order) + fmt_str = "%0" + str(2 * l) + "x" + string = binascii.unhexlify((fmt_str % num).encode()) + return string[:l] + + +def string_to_number(string): + return int(binascii.hexlify(string), 16) + + +def string_to_number_fixedlen(string, order): + l = orderlen(order) + assert len(string) == l, (len(string), l) + return int(binascii.hexlify(string), 16) + + +# these methods are useful for the sigencode= argument to SK.sign() and the +# sigdecode= argument to VK.verify(), and control how the signature is packed +# or unpacked. + +def sigencode_strings(r, s, order, v=None): + r_str = number_to_string(r, order) + s_str = number_to_string(s, order) + return r_str, s_str, v + + +def sigencode_string(r, s, order, v=None): + # for any given curve, the size of the signature numbers is + # fixed, so just use simple concatenation + r_str, s_str, v = sigencode_strings(r, s, order) + return r_str + s_str + + +def sigencode_der(r, s, order, v=None): + return der.encode_sequence(der.encode_integer(r), der.encode_integer(s)) + + +# canonical versions of sigencode methods +# these enforce low S values, by negating the value (modulo the order) if above order/2 +# see CECKey::Sign() https://github.com/bitcoin/bitcoin/blob/master/src/key.cpp#L214 +def sigencode_strings_canonize(r, s, order, v=None): + if s > order / 2: + s = order - s + if v is not None: + v ^= 1 + return sigencode_strings(r, s, order, v) + + +def sigencode_string_canonize(r, s, order, v=None): + if s > order / 2: + s = order - s + if v is not None: + v ^= 1 + return sigencode_string(r, s, order, v) + + +def sigencode_der_canonize(r, s, order, v=None): + if s > order / 2: + s = order - s + if v is not None: + v ^= 1 + return sigencode_der(r, s, order, v) + + +def sigdecode_string(signature, order): + l = orderlen(order) + assert len(signature) == 2 * l, (len(signature), 2 * l) + r = string_to_number_fixedlen(signature[:l], order) + s = string_to_number_fixedlen(signature[l:], order) + return r, s + + +def sigdecode_strings(rs_strings, order): + (r_str, s_str) = rs_strings + l = orderlen(order) + assert len(r_str) == l, (len(r_str), l) + assert len(s_str) == l, (len(s_str), l) + r = string_to_number_fixedlen(r_str, order) + s = string_to_number_fixedlen(s_str, order) + return r, s + + +def sigdecode_der(sig_der, order): + # return der.encode_sequence(der.encode_integer(r), der.encode_integer(s)) + rs_strings, empty = der.remove_sequence(sig_der) + if empty != b'': + raise der.UnexpectedDER("trailing junk after DER sig: %s" % + binascii.hexlify(empty)) + r, rest = der.remove_integer(rs_strings) + s, empty = der.remove_integer(rest) + if empty != b'': + raise der.UnexpectedDER("trailing junk after DER numbers: %s" % + binascii.hexlify(empty)) + return r, s diff --git a/ccxt/static_dependencies/ethereum/__init__.py b/ccxt/static_dependencies/ethereum/__init__.py new file mode 100644 index 0000000..daf2346 --- /dev/null +++ b/ccxt/static_dependencies/ethereum/__init__.py @@ -0,0 +1,7 @@ +from .abi import * +from .account import * +from .typing import * +from .utils import * +from .hexbytes import * + +__all__ = [ 'account', 'typing', 'utils', 'abi', 'hexbytes' ] \ No newline at end of file diff --git a/ccxt/static_dependencies/ethereum/__pycache__/__init__.cpython-311.pyc b/ccxt/static_dependencies/ethereum/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..b7e531b Binary files /dev/null and b/ccxt/static_dependencies/ethereum/__pycache__/__init__.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/ethereum/abi/__init__.py b/ccxt/static_dependencies/ethereum/abi/__init__.py new file mode 100644 index 0000000..5afc86d --- /dev/null +++ b/ccxt/static_dependencies/ethereum/abi/__init__.py @@ -0,0 +1,16 @@ + +from .abi import ( + decode, + decode_abi, + decode_single, + encode, + encode_abi, + encode_single, + is_encodable, + is_encodable_type, +) + +# This code from: https://github.com/ethereum/eth-abi/tree/v3.0.1 +__version__ = 'ccxt' + +__all__ = ['decode','encode'] diff --git a/ccxt/static_dependencies/ethereum/abi/__pycache__/__init__.cpython-311.pyc b/ccxt/static_dependencies/ethereum/abi/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..ffea8e9 Binary files /dev/null and b/ccxt/static_dependencies/ethereum/abi/__pycache__/__init__.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/ethereum/abi/__pycache__/abi.cpython-311.pyc b/ccxt/static_dependencies/ethereum/abi/__pycache__/abi.cpython-311.pyc new file mode 100644 index 0000000..ce10c04 Binary files /dev/null and b/ccxt/static_dependencies/ethereum/abi/__pycache__/abi.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/ethereum/abi/__pycache__/base.cpython-311.pyc b/ccxt/static_dependencies/ethereum/abi/__pycache__/base.cpython-311.pyc new file mode 100644 index 0000000..2ed9fe2 Binary files /dev/null and b/ccxt/static_dependencies/ethereum/abi/__pycache__/base.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/ethereum/abi/__pycache__/codec.cpython-311.pyc b/ccxt/static_dependencies/ethereum/abi/__pycache__/codec.cpython-311.pyc new file mode 100644 index 0000000..e55d0ac Binary files /dev/null and b/ccxt/static_dependencies/ethereum/abi/__pycache__/codec.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/ethereum/abi/__pycache__/decoding.cpython-311.pyc b/ccxt/static_dependencies/ethereum/abi/__pycache__/decoding.cpython-311.pyc new file mode 100644 index 0000000..8708b43 Binary files /dev/null and b/ccxt/static_dependencies/ethereum/abi/__pycache__/decoding.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/ethereum/abi/__pycache__/encoding.cpython-311.pyc b/ccxt/static_dependencies/ethereum/abi/__pycache__/encoding.cpython-311.pyc new file mode 100644 index 0000000..837fc0c Binary files /dev/null and b/ccxt/static_dependencies/ethereum/abi/__pycache__/encoding.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/ethereum/abi/__pycache__/exceptions.cpython-311.pyc b/ccxt/static_dependencies/ethereum/abi/__pycache__/exceptions.cpython-311.pyc new file mode 100644 index 0000000..b4ed4ad Binary files /dev/null and b/ccxt/static_dependencies/ethereum/abi/__pycache__/exceptions.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/ethereum/abi/__pycache__/grammar.cpython-311.pyc b/ccxt/static_dependencies/ethereum/abi/__pycache__/grammar.cpython-311.pyc new file mode 100644 index 0000000..0039bf3 Binary files /dev/null and b/ccxt/static_dependencies/ethereum/abi/__pycache__/grammar.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/ethereum/abi/__pycache__/registry.cpython-311.pyc b/ccxt/static_dependencies/ethereum/abi/__pycache__/registry.cpython-311.pyc new file mode 100644 index 0000000..e3c0c2a Binary files /dev/null and b/ccxt/static_dependencies/ethereum/abi/__pycache__/registry.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/ethereum/abi/abi.py b/ccxt/static_dependencies/ethereum/abi/abi.py new file mode 100644 index 0000000..5fbfd18 --- /dev/null +++ b/ccxt/static_dependencies/ethereum/abi/abi.py @@ -0,0 +1,19 @@ +from .codec import ( + ABICodec, +) +from .registry import ( + registry, +) + +default_codec = ABICodec(registry) + +encode = default_codec.encode +encode_abi = default_codec.encode_abi # deprecated +encode_single = default_codec.encode_single # deprecated + +decode = default_codec.decode +decode_abi = default_codec.decode_abi # deprecated +decode_single = default_codec.decode_single # deprecated + +is_encodable = default_codec.is_encodable +is_encodable_type = default_codec.is_encodable_type diff --git a/ccxt/static_dependencies/ethereum/abi/base.py b/ccxt/static_dependencies/ethereum/abi/base.py new file mode 100644 index 0000000..e0b7901 --- /dev/null +++ b/ccxt/static_dependencies/ethereum/abi/base.py @@ -0,0 +1,152 @@ +import functools + +from ..typing.abi import ( + TypeStr, +) + +from .grammar import ( + BasicType, + TupleType, + normalize, + parse, +) + + +def parse_type_str(expected_base=None, with_arrlist=False): + """ + Used by BaseCoder subclasses as a convenience for implementing the + ``from_type_str`` method required by ``ABIRegistry``. Useful if normalizing + then parsing a type string with an (optional) expected base is required in + that method. + """ + + def decorator(old_from_type_str): + @functools.wraps(old_from_type_str) + def new_from_type_str(cls, type_str, registry): + normalized_type_str = normalize(type_str) + abi_type = parse(normalized_type_str) + + type_str_repr = repr(type_str) + if type_str != normalized_type_str: + type_str_repr = "{} (normalized to {})".format( + type_str_repr, + repr(normalized_type_str), + ) + + if expected_base is not None: + if not isinstance(abi_type, BasicType): + raise ValueError( + "Cannot create {} for non-basic type {}".format( + cls.__name__, + type_str_repr, + ) + ) + if abi_type.base != expected_base: + raise ValueError( + "Cannot create {} for type {}: expected type with " + "base '{}'".format( + cls.__name__, + type_str_repr, + expected_base, + ) + ) + + if not with_arrlist and abi_type.arrlist is not None: + raise ValueError( + "Cannot create {} for type {}: expected type with " + "no array dimension list".format( + cls.__name__, + type_str_repr, + ) + ) + if with_arrlist and abi_type.arrlist is None: + raise ValueError( + "Cannot create {} for type {}: expected type with " + "array dimension list".format( + cls.__name__, + type_str_repr, + ) + ) + + # Perform general validation of default solidity types + abi_type.validate() + + return old_from_type_str(cls, abi_type, registry) + + return classmethod(new_from_type_str) + + return decorator + + +def parse_tuple_type_str(old_from_type_str): + """ + Used by BaseCoder subclasses as a convenience for implementing the + ``from_type_str`` method required by ``ABIRegistry``. Useful if normalizing + then parsing a tuple type string is required in that method. + """ + + @functools.wraps(old_from_type_str) + def new_from_type_str(cls, type_str, registry): + normalized_type_str = normalize(type_str) + abi_type = parse(normalized_type_str) + + type_str_repr = repr(type_str) + if type_str != normalized_type_str: + type_str_repr = "{} (normalized to {})".format( + type_str_repr, + repr(normalized_type_str), + ) + + if not isinstance(abi_type, TupleType): + raise ValueError( + "Cannot create {} for non-tuple type {}".format( + cls.__name__, + type_str_repr, + ) + ) + + abi_type.validate() + + return old_from_type_str(cls, abi_type, registry) + + return classmethod(new_from_type_str) + + +class BaseCoder: + """ + Base class for all encoder and decoder classes. + """ + + is_dynamic = False + + def __init__(self, **kwargs): + cls = type(self) + + # Ensure no unrecognized kwargs were given + for key, value in kwargs.items(): + if not hasattr(cls, key): + raise AttributeError( + "Property {key} not found on {cls_name} class. " + "`{cls_name}.__init__` only accepts keyword arguments which are " + "present on the {cls_name} class.".format( + key=key, + cls_name=cls.__name__, + ) + ) + setattr(self, key, value) + + # Validate given combination of kwargs + self.validate() + + def validate(self): + pass + + @classmethod + def from_type_str( + cls, type_str: TypeStr, registry + ) -> "BaseCoder": # pragma: no cover + """ + Used by :any:`ABIRegistry` to get an appropriate encoder or decoder + instance for the given type string and type registry. + """ + raise NotImplementedError("Must implement `from_type_str`") diff --git a/ccxt/static_dependencies/ethereum/abi/codec.py b/ccxt/static_dependencies/ethereum/abi/codec.py new file mode 100644 index 0000000..714cb59 --- /dev/null +++ b/ccxt/static_dependencies/ethereum/abi/codec.py @@ -0,0 +1,217 @@ +from typing import ( + Any, + Iterable, + Tuple, +) +import warnings + +from ..typing.abi import ( + Decodable, + TypeStr, +) +from ..utils import ( + is_bytes, +) + +from .decoding import ( + ContextFramesBytesIO, + TupleDecoder, +) +from .encoding import ( + TupleEncoder, +) +from .exceptions import ( + EncodingError, +) +from .registry import ( + ABIRegistry, +) + + +class BaseABICoder: + """ + Base class for porcelain coding APIs. These are classes which wrap + instances of :class:`~.registry.ABIRegistry` to provide last-mile + coding functionality. + """ + + def __init__(self, registry: ABIRegistry): + """ + Constructor. + + :param registry: The registry providing the encoders to be used when + encoding values. + """ + self._registry = registry + + +class ABIEncoder(BaseABICoder): + """ + Wraps a registry to provide last-mile encoding functionality. + """ + + def encode_single(self, typ: TypeStr, arg: Any) -> bytes: + """ + Encodes the python value ``arg`` as a binary value of the ABI type + ``typ``. + + :param typ: The string representation of the ABI type that will be used + for encoding e.g. ``'uint256'``, ``'bytes[]'``, ``'(int,int)'``, + etc. + :param arg: The python value to be encoded. + + :returns: The binary representation of the python value ``arg`` as a + value of the ABI type ``typ``. + """ + warnings.warn( + "abi.encode_single() and abi.encode_single_packed() are deprecated " + "and will be removed in version 4.0.0 in favor of abi.encode() and " + "abi.encode_packed(), respectively", + category=DeprecationWarning, + ) + + encoder = self._registry.get_encoder(typ) + + return encoder(arg) + + def encode_abi(self, types: Iterable[TypeStr], args: Iterable[Any]) -> bytes: + """ + Encodes the python values in ``args`` as a sequence of binary values of + the ABI types in ``types`` via the head-tail mechanism. + + :param types: An iterable of string representations of the ABI types + that will be used for encoding e.g. ``('uint256', 'bytes[]', + '(int,int)')`` + :param args: An iterable of python values to be encoded. + + :returns: The head-tail encoded binary representation of the python + values in ``args`` as values of the ABI types in ``types``. + """ + warnings.warn( + "abi.encode_abi() and abi.encode_abi_packed() are deprecated and will be " + "removed in version 4.0.0 in favor of abi.encode() and " + "abi.encode_packed(), respectively", + category=DeprecationWarning, + ) + return self.encode(types, args) + + def encode(self, types, args): + encoders = [self._registry.get_encoder(type_str) for type_str in types] + + encoder = TupleEncoder(encoders=encoders) + + return encoder(args) + + def is_encodable(self, typ: TypeStr, arg: Any) -> bool: + """ + Determines if the python value ``arg`` is encodable as a value of the + ABI type ``typ``. + + :param typ: A string representation for the ABI type against which the + python value ``arg`` will be checked e.g. ``'uint256'``, + ``'bytes[]'``, ``'(int,int)'``, etc. + :param arg: The python value whose encodability should be checked. + + :returns: ``True`` if ``arg`` is encodable as a value of the ABI type + ``typ``. Otherwise, ``False``. + """ + encoder = self._registry.get_encoder(typ) + + try: + encoder.validate_value(arg) + except EncodingError: + return False + except AttributeError: + try: + encoder(arg) + except EncodingError: + return False + + return True + + def is_encodable_type(self, typ: TypeStr) -> bool: + """ + Returns ``True`` if values for the ABI type ``typ`` can be encoded by + this codec. + + :param typ: A string representation for the ABI type that will be + checked for encodability e.g. ``'uint256'``, ``'bytes[]'``, + ``'(int,int)'``, etc. + + :returns: ``True`` if values for ``typ`` can be encoded by this codec. + Otherwise, ``False``. + """ + return self._registry.has_encoder(typ) + + +class ABIDecoder(BaseABICoder): + """ + Wraps a registry to provide last-mile decoding functionality. + """ + + stream_class = ContextFramesBytesIO + + def decode_single(self, typ: TypeStr, data: Decodable) -> Any: + """ + Decodes the binary value ``data`` of the ABI type ``typ`` into its + equivalent python value. + + :param typ: The string representation of the ABI type that will be used for + decoding e.g. ``'uint256'``, ``'bytes[]'``, ``'(int,int)'``, etc. + :param data: The binary value to be decoded. + + :returns: The equivalent python value of the ABI value represented in + ``data``. + """ + warnings.warn( + "abi.decode_single() is deprecated and will be removed in version 4.0.0 " + "in favor of abi.decode()", + category=DeprecationWarning, + ) + + if not is_bytes(data): + raise TypeError( + "The `data` value must be of bytes type. Got {0}".format(type(data)) + ) + + decoder = self._registry.get_decoder(typ) + stream = self.stream_class(data) + + return decoder(stream) + + def decode_abi(self, types: Iterable[TypeStr], data: Decodable) -> Tuple[Any, ...]: + """ + Decodes the binary value ``data`` as a sequence of values of the ABI types + in ``types`` via the head-tail mechanism into a tuple of equivalent python + values. + + :param types: An iterable of string representations of the ABI types that + will be used for decoding e.g. ``('uint256', 'bytes[]', '(int,int)')`` + :param data: The binary value to be decoded. + + :returns: A tuple of equivalent python values for the ABI values + represented in ``data``. + """ + warnings.warn( + "abi.decode_abi() is deprecated and will be removed in version 4.0.0 in " + "favor of abi.decode()", + category=DeprecationWarning, + ) + return self.decode(types, data) + + def decode(self, types, data): + if not is_bytes(data): + raise TypeError( + f"The `data` value must be of bytes type. Got {type(data)}" + ) + + decoders = [self._registry.get_decoder(type_str) for type_str in types] + + decoder = TupleDecoder(decoders=decoders) + stream = self.stream_class(data) + + return decoder(stream) + + +class ABICodec(ABIEncoder, ABIDecoder): + pass diff --git a/ccxt/static_dependencies/ethereum/abi/constants.py b/ccxt/static_dependencies/ethereum/abi/constants.py new file mode 100644 index 0000000..edb4fa3 --- /dev/null +++ b/ccxt/static_dependencies/ethereum/abi/constants.py @@ -0,0 +1,3 @@ +TT256 = 2**256 +TT256M1 = 2**256 - 1 +TT255 = 2**255 diff --git a/ccxt/static_dependencies/ethereum/abi/decoding.py b/ccxt/static_dependencies/ethereum/abi/decoding.py new file mode 100644 index 0000000..5391ae4 --- /dev/null +++ b/ccxt/static_dependencies/ethereum/abi/decoding.py @@ -0,0 +1,565 @@ +import abc +import decimal +import io +from typing import ( + Any, +) + +from ..utils import ( + big_endian_to_int, + to_normalized_address, + to_tuple, +) + +from .base import ( + BaseCoder, + parse_tuple_type_str, + parse_type_str, +) +from .exceptions import ( + DecodingError, + InsufficientDataBytes, + NonEmptyPaddingBytes, +) +from .utils.numeric import ( + TEN, + abi_decimal_context, + ceil32, +) + + +class ContextFramesBytesIO(io.BytesIO): + """ + A byte stream which can track a series of contextual frames in a stack. This + data structure is necessary to perform nested decodings using the + :py:class:``HeadTailDecoder`` since offsets present in head sections are + relative only to a particular encoded object. These offsets can only be + used to locate a position in a decoding stream if they are paired with a + contextual offset that establishes the position of the object in which they + are found. + + For example, consider the encoding of a value for the following type:: + + type: (int,(int,int[])) + value: (1,(2,[3,3])) + + There are two tuples in this type: one inner and one outer. The inner tuple + type contains a dynamic type ``int[]`` and, therefore, is itself dynamic. + This means that its value encoding will be placed in the tail section of the + outer tuple's encoding. Furthermore, the inner tuple's encoding will, + itself, contain a tail section with the encoding for ``[3,3]``. All + together, the encoded value of ``(1,(2,[3,3]))`` would look like this (the + data values are normally 32 bytes wide but have been truncated to remove the + redundant zeros at the beginnings of their encodings):: + + offset data + -------------------------- + ^ 0 0x01 + | 32 0x40 <-- Offset of object A in global frame (64) + -----|-------------------- + Global frame ^ 64 0x02 <-- Beginning of object A (64 w/offset 0 = 64) + | | 96 0x40 <-- Offset of object B in frame of object A (64) + -----|-Object A's frame--- + | | 128 0x02 <-- Beginning of object B (64 w/offset 64 = 128) + | | 160 0x03 + v v 192 0x03 + -------------------------- + + Note that the offset of object B is encoded as 64 which only specifies the + beginning of its encoded value relative to the beginning of object A's + encoding. Globally, object B is located at offset 128. In order to make + sense out of object B's offset, it needs to be positioned in the context of + its enclosing object's frame (object A). + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self._frames = [] + self._total_offset = 0 + + def seek_in_frame(self, pos, *args, **kwargs): + """ + Seeks relative to the total offset of the current contextual frames. + """ + self.seek(self._total_offset + pos, *args, **kwargs) + + def push_frame(self, offset): + """ + Pushes a new contextual frame onto the stack with the given offset and a + return position at the current cursor position then seeks to the new + total offset. + """ + self._frames.append((offset, self.tell())) + self._total_offset += offset + + self.seek_in_frame(0) + + def pop_frame(self): + """ + Pops the current contextual frame off of the stack and returns the + cursor to the frame's return position. + """ + try: + offset, return_pos = self._frames.pop() + except IndexError: + raise IndexError("no frames to pop") + self._total_offset -= offset + + self.seek(return_pos) + + +class BaseDecoder(BaseCoder, metaclass=abc.ABCMeta): + """ + Base class for all decoder classes. Subclass this if you want to define a + custom decoder class. Subclasses must also implement + :any:`BaseCoder.from_type_str`. + """ + + @abc.abstractmethod + def decode(self, stream: ContextFramesBytesIO) -> Any: # pragma: no cover + """ + Decodes the given stream of bytes into a python value. Should raise + :any:`exceptions.DecodingError` if a python value cannot be decoded + from the given byte stream. + """ + pass + + def __call__(self, stream: ContextFramesBytesIO) -> Any: + return self.decode(stream) + + +class HeadTailDecoder(BaseDecoder): + is_dynamic = True + + tail_decoder = None + + def validate(self): + super().validate() + + if self.tail_decoder is None: + raise ValueError("No `tail_decoder` set") + + def decode(self, stream): + start_pos = decode_uint_256(stream) + + stream.push_frame(start_pos) + value = self.tail_decoder(stream) + stream.pop_frame() + + return value + + +class TupleDecoder(BaseDecoder): + decoders = None + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + self.decoders = tuple( + HeadTailDecoder(tail_decoder=d) if getattr(d, "is_dynamic", False) else d + for d in self.decoders + ) + + self.is_dynamic = any(getattr(d, "is_dynamic", False) for d in self.decoders) + + def validate(self): + super().validate() + + if self.decoders is None: + raise ValueError("No `decoders` set") + + @to_tuple + def decode(self, stream): + for decoder in self.decoders: + yield decoder(stream) + + @parse_tuple_type_str + def from_type_str(cls, abi_type, registry): + decoders = tuple( + registry.get_decoder(c.to_type_str()) for c in abi_type.components + ) + + return cls(decoders=decoders) + + +class SingleDecoder(BaseDecoder): + decoder_fn = None + + def validate(self): + super().validate() + + if self.decoder_fn is None: + raise ValueError("No `decoder_fn` set") + + def validate_padding_bytes(self, value, padding_bytes): + raise NotImplementedError("Must be implemented by subclasses") + + def decode(self, stream): + raw_data = self.read_data_from_stream(stream) + data, padding_bytes = self.split_data_and_padding(raw_data) + value = self.decoder_fn(data) + self.validate_padding_bytes(value, padding_bytes) + + return value + + def read_data_from_stream(self, stream): + raise NotImplementedError("Must be implemented by subclasses") + + def split_data_and_padding(self, raw_data): + return raw_data, b"" + + +class BaseArrayDecoder(BaseDecoder): + item_decoder = None + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + # Use a head-tail decoder to decode dynamic elements + if self.item_decoder.is_dynamic: + self.item_decoder = HeadTailDecoder( + tail_decoder=self.item_decoder, + ) + + def validate(self): + super().validate() + + if self.item_decoder is None: + raise ValueError("No `item_decoder` set") + + @parse_type_str(with_arrlist=True) + def from_type_str(cls, abi_type, registry): + item_decoder = registry.get_decoder(abi_type.item_type.to_type_str()) + + array_spec = abi_type.arrlist[-1] + if len(array_spec) == 1: + # If array dimension is fixed + return SizedArrayDecoder( + array_size=array_spec[0], + item_decoder=item_decoder, + ) + else: + # If array dimension is dynamic + return DynamicArrayDecoder(item_decoder=item_decoder) + + +class SizedArrayDecoder(BaseArrayDecoder): + array_size = None + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + self.is_dynamic = self.item_decoder.is_dynamic + + @to_tuple + def decode(self, stream): + for _ in range(self.array_size): + yield self.item_decoder(stream) + + +class DynamicArrayDecoder(BaseArrayDecoder): + # Dynamic arrays are always dynamic, regardless of their elements + is_dynamic = True + + @to_tuple + def decode(self, stream): + array_size = decode_uint_256(stream) + stream.push_frame(32) + for _ in range(array_size): + yield self.item_decoder(stream) + stream.pop_frame() + + +class FixedByteSizeDecoder(SingleDecoder): + decoder_fn = None + value_bit_size = None + data_byte_size = None + is_big_endian = None + + def validate(self): + super().validate() + + if self.value_bit_size is None: + raise ValueError("`value_bit_size` may not be None") + if self.data_byte_size is None: + raise ValueError("`data_byte_size` may not be None") + if self.decoder_fn is None: + raise ValueError("`decoder_fn` may not be None") + if self.is_big_endian is None: + raise ValueError("`is_big_endian` may not be None") + + if self.value_bit_size % 8 != 0: + raise ValueError( + "Invalid value bit size: {0}. Must be a multiple of 8".format( + self.value_bit_size, + ) + ) + + if self.value_bit_size > self.data_byte_size * 8: + raise ValueError("Value byte size exceeds data size") + + def read_data_from_stream(self, stream): + data = stream.read(self.data_byte_size) + + if len(data) != self.data_byte_size: + raise InsufficientDataBytes( + "Tried to read {0} bytes. Only got {1} bytes".format( + self.data_byte_size, + len(data), + ) + ) + + return data + + def split_data_and_padding(self, raw_data): + value_byte_size = self._get_value_byte_size() + padding_size = self.data_byte_size - value_byte_size + + if self.is_big_endian: + padding_bytes = raw_data[:padding_size] + data = raw_data[padding_size:] + else: + data = raw_data[:value_byte_size] + padding_bytes = raw_data[value_byte_size:] + + return data, padding_bytes + + def validate_padding_bytes(self, value, padding_bytes): + value_byte_size = self._get_value_byte_size() + padding_size = self.data_byte_size - value_byte_size + + if padding_bytes != b"\x00" * padding_size: + raise NonEmptyPaddingBytes( + "Padding bytes were not empty: {0}".format(repr(padding_bytes)) + ) + + def _get_value_byte_size(self): + value_byte_size = self.value_bit_size // 8 + return value_byte_size + + +class Fixed32ByteSizeDecoder(FixedByteSizeDecoder): + data_byte_size = 32 + + +class BooleanDecoder(Fixed32ByteSizeDecoder): + value_bit_size = 8 + is_big_endian = True + + @staticmethod + def decoder_fn(data): + if data == b"\x00": + return False + elif data == b"\x01": + return True + else: + raise NonEmptyPaddingBytes( + "Boolean must be either 0x0 or 0x1. Got: {0}".format(repr(data)) + ) + + @parse_type_str("bool") + def from_type_str(cls, abi_type, registry): + return cls() + + +class AddressDecoder(Fixed32ByteSizeDecoder): + value_bit_size = 20 * 8 + is_big_endian = True + decoder_fn = staticmethod(to_normalized_address) + + @parse_type_str("address") + def from_type_str(cls, abi_type, registry): + return cls() + + +# +# Unsigned Integer Decoders +# +class UnsignedIntegerDecoder(Fixed32ByteSizeDecoder): + decoder_fn = staticmethod(big_endian_to_int) + is_big_endian = True + + @parse_type_str("uint") + def from_type_str(cls, abi_type, registry): + return cls(value_bit_size=abi_type.sub) + + +decode_uint_256 = UnsignedIntegerDecoder(value_bit_size=256) + + +# +# Signed Integer Decoders +# +class SignedIntegerDecoder(Fixed32ByteSizeDecoder): + is_big_endian = True + + def decoder_fn(self, data): + value = big_endian_to_int(data) + if value >= 2 ** (self.value_bit_size - 1): + return value - 2**self.value_bit_size + else: + return value + + def validate_padding_bytes(self, value, padding_bytes): + value_byte_size = self._get_value_byte_size() + padding_size = self.data_byte_size - value_byte_size + + if value >= 0: + expected_padding_bytes = b"\x00" * padding_size + else: + expected_padding_bytes = b"\xff" * padding_size + + if padding_bytes != expected_padding_bytes: + raise NonEmptyPaddingBytes( + "Padding bytes were not empty: {0}".format(repr(padding_bytes)) + ) + + @parse_type_str("int") + def from_type_str(cls, abi_type, registry): + return cls(value_bit_size=abi_type.sub) + + +# +# Bytes1..32 +# +class BytesDecoder(Fixed32ByteSizeDecoder): + is_big_endian = False + + @staticmethod + def decoder_fn(data): + return data + + @parse_type_str("bytes") + def from_type_str(cls, abi_type, registry): + return cls(value_bit_size=abi_type.sub * 8) + + +class BaseFixedDecoder(Fixed32ByteSizeDecoder): + frac_places = None + is_big_endian = True + + def validate(self): + super().validate() + + if self.frac_places is None: + raise ValueError("must specify `frac_places`") + + if self.frac_places <= 0 or self.frac_places > 80: + raise ValueError("`frac_places` must be in range (0, 80]") + + +class UnsignedFixedDecoder(BaseFixedDecoder): + def decoder_fn(self, data): + value = big_endian_to_int(data) + + with decimal.localcontext(abi_decimal_context): + decimal_value = decimal.Decimal(value) / TEN**self.frac_places + + return decimal_value + + @parse_type_str("ufixed") + def from_type_str(cls, abi_type, registry): + value_bit_size, frac_places = abi_type.sub + + return cls(value_bit_size=value_bit_size, frac_places=frac_places) + + +class SignedFixedDecoder(BaseFixedDecoder): + def decoder_fn(self, data): + value = big_endian_to_int(data) + if value >= 2 ** (self.value_bit_size - 1): + signed_value = value - 2**self.value_bit_size + else: + signed_value = value + + with decimal.localcontext(abi_decimal_context): + decimal_value = decimal.Decimal(signed_value) / TEN**self.frac_places + + return decimal_value + + def validate_padding_bytes(self, value, padding_bytes): + value_byte_size = self._get_value_byte_size() + padding_size = self.data_byte_size - value_byte_size + + if value >= 0: + expected_padding_bytes = b"\x00" * padding_size + else: + expected_padding_bytes = b"\xff" * padding_size + + if padding_bytes != expected_padding_bytes: + raise NonEmptyPaddingBytes( + "Padding bytes were not empty: {0}".format(repr(padding_bytes)) + ) + + @parse_type_str("fixed") + def from_type_str(cls, abi_type, registry): + value_bit_size, frac_places = abi_type.sub + + return cls(value_bit_size=value_bit_size, frac_places=frac_places) + + +# +# String and Bytes +# +class ByteStringDecoder(SingleDecoder): + is_dynamic = True + + @staticmethod + def decoder_fn(data): + return data + + @staticmethod + def read_data_from_stream(stream): + data_length = decode_uint_256(stream) + padded_length = ceil32(data_length) + + data = stream.read(padded_length) + + if len(data) < padded_length: + raise InsufficientDataBytes( + "Tried to read {0} bytes. Only got {1} bytes".format( + padded_length, + len(data), + ) + ) + + padding_bytes = data[data_length:] + + if padding_bytes != b"\x00" * (padded_length - data_length): + raise NonEmptyPaddingBytes( + "Padding bytes were not empty: {0}".format(repr(padding_bytes)) + ) + + return data[:data_length] + + def validate_padding_bytes(self, value, padding_bytes): + pass + + @parse_type_str("bytes") + def from_type_str(cls, abi_type, registry): + return cls() + + +class StringDecoder(ByteStringDecoder): + @parse_type_str("string") + def from_type_str(cls, abi_type, registry): + return cls() + + @staticmethod + def decoder_fn(data): + try: + value = data.decode("utf-8") + except UnicodeDecodeError as e: + raise DecodingError( + e.encoding, + e.object, + e.start, + e.end, + "The returned type for this function is string which is " + "expected to be a UTF8 encoded string of text. The returned " + "value could not be decoded as valid UTF8. This is indicative " + "of a broken application which is using incorrect return types for " + "binary data.", + ) from e + return value diff --git a/ccxt/static_dependencies/ethereum/abi/encoding.py b/ccxt/static_dependencies/ethereum/abi/encoding.py new file mode 100644 index 0000000..b34026a --- /dev/null +++ b/ccxt/static_dependencies/ethereum/abi/encoding.py @@ -0,0 +1,720 @@ +import abc +import codecs +import decimal +from itertools import ( + accumulate, +) +from typing import ( + Any, + Optional, + Type, +) + +from ..utils import ( + int_to_big_endian, + is_address, + is_boolean, + is_bytes, + is_integer, + is_list_like, + is_number, + is_text, + to_canonical_address, +) + +from .base import ( + BaseCoder, + parse_tuple_type_str, + parse_type_str, +) +from .exceptions import ( + EncodingTypeError, + IllegalValue, + ValueOutOfBounds, +) +from .utils.numeric import ( + TEN, + abi_decimal_context, + ceil32, + compute_signed_fixed_bounds, + compute_signed_integer_bounds, + compute_unsigned_fixed_bounds, + compute_unsigned_integer_bounds, +) +from .utils.padding import ( + fpad, + zpad, + zpad_right, +) +from .utils.string import ( + abbr, +) + + +class BaseEncoder(BaseCoder, metaclass=abc.ABCMeta): + """ + Base class for all encoder classes. Subclass this if you want to define a + custom encoder class. Subclasses must also implement + :any:`BaseCoder.from_type_str`. + """ + + @abc.abstractmethod + def encode(self, value: Any) -> bytes: # pragma: no cover + """ + Encodes the given value as a sequence of bytes. Should raise + :any:`exceptions.EncodingError` if ``value`` cannot be encoded. + """ + pass + + @abc.abstractmethod + def validate_value(self, value: Any) -> None: # pragma: no cover + """ + Checks whether or not the given value can be encoded by this encoder. + If the given value cannot be encoded, must raise + :any:`exceptions.EncodingError`. + """ + pass + + @classmethod + def invalidate_value( + cls, + value: Any, + exc: Type[Exception] = EncodingTypeError, + msg: Optional[str] = None, + ) -> None: + """ + Throws a standard exception for when a value is not encodable by an + encoder. + """ + raise exc( + "Value `{rep}` of type {typ} cannot be encoded by {cls}{msg}".format( + rep=abbr(value), + typ=type(value), + cls=cls.__name__, + msg="" if msg is None else (": " + msg), + ) + ) + + def __call__(self, value: Any) -> bytes: + return self.encode(value) + + +class TupleEncoder(BaseEncoder): + encoders = None + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + self.is_dynamic = any(getattr(e, "is_dynamic", False) for e in self.encoders) + + def validate(self): + super().validate() + + if self.encoders is None: + raise ValueError("`encoders` may not be none") + + def validate_value(self, value): + if not is_list_like(value): + self.invalidate_value( + value, + msg="must be list-like object such as array or tuple", + ) + + if len(value) != len(self.encoders): + self.invalidate_value( + value, + exc=ValueOutOfBounds, + msg="value has {} items when {} were expected".format( + len(value), + len(self.encoders), + ), + ) + + for item, encoder in zip(value, self.encoders): + try: + encoder.validate_value(item) + except AttributeError: + encoder(item) + + def encode(self, values): + self.validate_value(values) + + raw_head_chunks = [] + tail_chunks = [] + for value, encoder in zip(values, self.encoders): + if getattr(encoder, "is_dynamic", False): + raw_head_chunks.append(None) + tail_chunks.append(encoder(value)) + else: + raw_head_chunks.append(encoder(value)) + tail_chunks.append(b"") + + head_length = sum(32 if item is None else len(item) for item in raw_head_chunks) + tail_offsets = (0,) + tuple(accumulate(map(len, tail_chunks[:-1]))) + head_chunks = tuple( + encode_uint_256(head_length + offset) if chunk is None else chunk + for chunk, offset in zip(raw_head_chunks, tail_offsets) + ) + + encoded_value = b"".join(head_chunks + tuple(tail_chunks)) + + return encoded_value + + @parse_tuple_type_str + def from_type_str(cls, abi_type, registry): + encoders = tuple( + registry.get_encoder(c.to_type_str()) for c in abi_type.components + ) + + return cls(encoders=encoders) + + +class FixedSizeEncoder(BaseEncoder): + value_bit_size = None + data_byte_size = None + encode_fn = None + type_check_fn = None + is_big_endian = None + + def validate(self): + super().validate() + + if self.value_bit_size is None: + raise ValueError("`value_bit_size` may not be none") + if self.data_byte_size is None: + raise ValueError("`data_byte_size` may not be none") + if self.encode_fn is None: + raise ValueError("`encode_fn` may not be none") + if self.is_big_endian is None: + raise ValueError("`is_big_endian` may not be none") + + if self.value_bit_size % 8 != 0: + raise ValueError( + "Invalid value bit size: {0}. Must be a multiple of 8".format( + self.value_bit_size, + ) + ) + + if self.value_bit_size > self.data_byte_size * 8: + raise ValueError("Value byte size exceeds data size") + + def validate_value(self, value): + raise NotImplementedError("Must be implemented by subclasses") + + def encode(self, value): + self.validate_value(value) + base_encoded_value = self.encode_fn(value) + + if self.is_big_endian: + padded_encoded_value = zpad(base_encoded_value, self.data_byte_size) + else: + padded_encoded_value = zpad_right(base_encoded_value, self.data_byte_size) + + return padded_encoded_value + + +class Fixed32ByteSizeEncoder(FixedSizeEncoder): + data_byte_size = 32 + + +class BooleanEncoder(Fixed32ByteSizeEncoder): + value_bit_size = 8 + is_big_endian = True + + @classmethod + def validate_value(cls, value): + if not is_boolean(value): + cls.invalidate_value(value) + + @classmethod + def encode_fn(cls, value): + if value is True: + return b"\x01" + elif value is False: + return b"\x00" + else: + raise ValueError("Invariant") + + @parse_type_str("bool") + def from_type_str(cls, abi_type, registry): + return cls() + + +class PackedBooleanEncoder(BooleanEncoder): + data_byte_size = 1 + + +class NumberEncoder(Fixed32ByteSizeEncoder): + is_big_endian = True + bounds_fn = None + illegal_value_fn = None + type_check_fn = None + + def validate(self): + super().validate() + + if self.bounds_fn is None: + raise ValueError("`bounds_fn` cannot be null") + if self.type_check_fn is None: + raise ValueError("`type_check_fn` cannot be null") + + def validate_value(self, value): + if not self.type_check_fn(value): + self.invalidate_value(value) + + illegal_value = self.illegal_value_fn is not None and self.illegal_value_fn( + value + ) + if illegal_value: + self.invalidate_value(value, exc=IllegalValue) + + lower_bound, upper_bound = self.bounds_fn(self.value_bit_size) + if value < lower_bound or value > upper_bound: + self.invalidate_value( + value, + exc=ValueOutOfBounds, + msg=( + "Cannot be encoded in {} bits. Must be bounded " + "between [{}, {}].".format( + self.value_bit_size, + lower_bound, + upper_bound, + ) + ), + ) + + +class UnsignedIntegerEncoder(NumberEncoder): + encode_fn = staticmethod(int_to_big_endian) + bounds_fn = staticmethod(compute_unsigned_integer_bounds) + type_check_fn = staticmethod(is_integer) + + @parse_type_str("uint") + def from_type_str(cls, abi_type, registry): + return cls(value_bit_size=abi_type.sub) + + +encode_uint_256 = UnsignedIntegerEncoder(value_bit_size=256, data_byte_size=32) + + +class PackedUnsignedIntegerEncoder(UnsignedIntegerEncoder): + @parse_type_str("uint") + def from_type_str(cls, abi_type, registry): + return cls( + value_bit_size=abi_type.sub, + data_byte_size=abi_type.sub // 8, + ) + + +class SignedIntegerEncoder(NumberEncoder): + bounds_fn = staticmethod(compute_signed_integer_bounds) + type_check_fn = staticmethod(is_integer) + + def encode_fn(self, value): + return int_to_big_endian(value % (2**self.value_bit_size)) + + def encode(self, value): + self.validate_value(value) + base_encoded_value = self.encode_fn(value) + + if value >= 0: + padded_encoded_value = zpad(base_encoded_value, self.data_byte_size) + else: + padded_encoded_value = fpad(base_encoded_value, self.data_byte_size) + + return padded_encoded_value + + @parse_type_str("int") + def from_type_str(cls, abi_type, registry): + return cls(value_bit_size=abi_type.sub) + + +class PackedSignedIntegerEncoder(SignedIntegerEncoder): + @parse_type_str("int") + def from_type_str(cls, abi_type, registry): + return cls( + value_bit_size=abi_type.sub, + data_byte_size=abi_type.sub // 8, + ) + + +class BaseFixedEncoder(NumberEncoder): + frac_places = None + + @staticmethod + def type_check_fn(value): + return is_number(value) and not isinstance(value, float) + + @staticmethod + def illegal_value_fn(value): + if isinstance(value, decimal.Decimal): + return value.is_nan() or value.is_infinite() + + return False + + def validate_value(self, value): + super().validate_value(value) + + with decimal.localcontext(abi_decimal_context): + residue = value % (TEN**-self.frac_places) + + if residue > 0: + self.invalidate_value( + value, + exc=IllegalValue, + msg="residue {} outside allowed fractional precision of {}".format( + repr(residue), + self.frac_places, + ), + ) + + def validate(self): + super().validate() + + if self.frac_places is None: + raise ValueError("must specify `frac_places`") + + if self.frac_places <= 0 or self.frac_places > 80: + raise ValueError("`frac_places` must be in range (0, 80]") + + +class UnsignedFixedEncoder(BaseFixedEncoder): + def bounds_fn(self, value_bit_size): + return compute_unsigned_fixed_bounds(self.value_bit_size, self.frac_places) + + def encode_fn(self, value): + with decimal.localcontext(abi_decimal_context): + scaled_value = value * TEN**self.frac_places + integer_value = int(scaled_value) + + return int_to_big_endian(integer_value) + + @parse_type_str("ufixed") + def from_type_str(cls, abi_type, registry): + value_bit_size, frac_places = abi_type.sub + + return cls( + value_bit_size=value_bit_size, + frac_places=frac_places, + ) + + +class PackedUnsignedFixedEncoder(UnsignedFixedEncoder): + @parse_type_str("ufixed") + def from_type_str(cls, abi_type, registry): + value_bit_size, frac_places = abi_type.sub + + return cls( + value_bit_size=value_bit_size, + data_byte_size=value_bit_size // 8, + frac_places=frac_places, + ) + + +class SignedFixedEncoder(BaseFixedEncoder): + def bounds_fn(self, value_bit_size): + return compute_signed_fixed_bounds(self.value_bit_size, self.frac_places) + + def encode_fn(self, value): + with decimal.localcontext(abi_decimal_context): + scaled_value = value * TEN**self.frac_places + integer_value = int(scaled_value) + + unsigned_integer_value = integer_value % (2**self.value_bit_size) + + return int_to_big_endian(unsigned_integer_value) + + def encode(self, value): + self.validate_value(value) + base_encoded_value = self.encode_fn(value) + + if value >= 0: + padded_encoded_value = zpad(base_encoded_value, self.data_byte_size) + else: + padded_encoded_value = fpad(base_encoded_value, self.data_byte_size) + + return padded_encoded_value + + @parse_type_str("fixed") + def from_type_str(cls, abi_type, registry): + value_bit_size, frac_places = abi_type.sub + + return cls( + value_bit_size=value_bit_size, + frac_places=frac_places, + ) + + +class PackedSignedFixedEncoder(SignedFixedEncoder): + @parse_type_str("fixed") + def from_type_str(cls, abi_type, registry): + value_bit_size, frac_places = abi_type.sub + + return cls( + value_bit_size=value_bit_size, + data_byte_size=value_bit_size // 8, + frac_places=frac_places, + ) + + +class AddressEncoder(Fixed32ByteSizeEncoder): + value_bit_size = 20 * 8 + encode_fn = staticmethod(to_canonical_address) + is_big_endian = True + + @classmethod + def validate_value(cls, value): + if not is_address(value): + cls.invalidate_value(value) + + def validate(self): + super().validate() + + if self.value_bit_size != 20 * 8: + raise ValueError("Addresses must be 160 bits in length") + + @parse_type_str("address") + def from_type_str(cls, abi_type, registry): + return cls() + + +class PackedAddressEncoder(AddressEncoder): + data_byte_size = 20 + + +class BytesEncoder(Fixed32ByteSizeEncoder): + is_big_endian = False + + def validate_value(self, value): + if not is_bytes(value): + self.invalidate_value(value) + + byte_size = self.value_bit_size // 8 + if len(value) > byte_size: + self.invalidate_value( + value, + exc=ValueOutOfBounds, + msg="exceeds total byte size for bytes{} encoding".format(byte_size), + ) + + @staticmethod + def encode_fn(value): + return value + + @parse_type_str("bytes") + def from_type_str(cls, abi_type, registry): + return cls(value_bit_size=abi_type.sub * 8) + + +class PackedBytesEncoder(BytesEncoder): + @parse_type_str("bytes") + def from_type_str(cls, abi_type, registry): + return cls( + value_bit_size=abi_type.sub * 8, + data_byte_size=abi_type.sub, + ) + + +class ByteStringEncoder(BaseEncoder): + is_dynamic = True + + @classmethod + def validate_value(cls, value): + if not is_bytes(value): + cls.invalidate_value(value) + + @classmethod + def encode(cls, value): + cls.validate_value(value) + + if not value: + padded_value = b"\x00" * 32 + else: + padded_value = zpad_right(value, ceil32(len(value))) + + encoded_size = encode_uint_256(len(value)) + encoded_value = encoded_size + padded_value + + return encoded_value + + @parse_type_str("bytes") + def from_type_str(cls, abi_type, registry): + return cls() + + +class PackedByteStringEncoder(ByteStringEncoder): + is_dynamic = False + + @classmethod + def encode(cls, value): + cls.validate_value(value) + return value + + +class TextStringEncoder(BaseEncoder): + is_dynamic = True + + @classmethod + def validate_value(cls, value): + if not is_text(value): + cls.invalidate_value(value) + + @classmethod + def encode(cls, value): + cls.validate_value(value) + + value_as_bytes = codecs.encode(value, "utf8") + + if not value_as_bytes: + padded_value = b"\x00" * 32 + else: + padded_value = zpad_right(value_as_bytes, ceil32(len(value_as_bytes))) + + encoded_size = encode_uint_256(len(value_as_bytes)) + encoded_value = encoded_size + padded_value + + return encoded_value + + @parse_type_str("string") + def from_type_str(cls, abi_type, registry): + return cls() + + +class PackedTextStringEncoder(TextStringEncoder): + is_dynamic = False + + @classmethod + def encode(cls, value): + cls.validate_value(value) + return codecs.encode(value, "utf8") + + +class BaseArrayEncoder(BaseEncoder): + item_encoder = None + + def validate(self): + super().validate() + + if self.item_encoder is None: + raise ValueError("`item_encoder` may not be none") + + def validate_value(self, value): + if not is_list_like(value): + self.invalidate_value( + value, + msg="must be list-like such as array or tuple", + ) + + for item in value: + self.item_encoder.validate_value(item) + + def encode_elements(self, value): + self.validate_value(value) + + item_encoder = self.item_encoder + tail_chunks = tuple(item_encoder(i) for i in value) + + items_are_dynamic = getattr(item_encoder, "is_dynamic", False) + if not items_are_dynamic: + return b"".join(tail_chunks) + + head_length = 32 * len(value) + tail_offsets = (0,) + tuple(accumulate(map(len, tail_chunks[:-1]))) + head_chunks = tuple( + encode_uint_256(head_length + offset) for offset in tail_offsets + ) + return b"".join(head_chunks + tail_chunks) + + @parse_type_str(with_arrlist=True) + def from_type_str(cls, abi_type, registry): + item_encoder = registry.get_encoder(abi_type.item_type.to_type_str()) + + array_spec = abi_type.arrlist[-1] + if len(array_spec) == 1: + # If array dimension is fixed + return SizedArrayEncoder( + array_size=array_spec[0], + item_encoder=item_encoder, + ) + else: + # If array dimension is dynamic + return DynamicArrayEncoder(item_encoder=item_encoder) + + +class PackedArrayEncoder(BaseArrayEncoder): + array_size = None + + def validate_value(self, value): + super().validate_value(value) + + if self.array_size is not None and len(value) != self.array_size: + self.invalidate_value( + value, + exc=ValueOutOfBounds, + msg="value has {} items when {} were expected".format( + len(value), + self.array_size, + ), + ) + + def encode(self, value): + encoded_elements = self.encode_elements(value) + + return encoded_elements + + @parse_type_str(with_arrlist=True) + def from_type_str(cls, abi_type, registry): + item_encoder = registry.get_encoder(abi_type.item_type.to_type_str()) + + array_spec = abi_type.arrlist[-1] + if len(array_spec) == 1: + return cls( + array_size=array_spec[0], + item_encoder=item_encoder, + ) + else: + return cls(item_encoder=item_encoder) + + +class SizedArrayEncoder(BaseArrayEncoder): + array_size = None + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + self.is_dynamic = self.item_encoder.is_dynamic + + def validate(self): + super().validate() + + if self.array_size is None: + raise ValueError("`array_size` may not be none") + + def validate_value(self, value): + super().validate_value(value) + + if len(value) != self.array_size: + self.invalidate_value( + value, + exc=ValueOutOfBounds, + msg="value has {} items when {} were expected".format( + len(value), + self.array_size, + ), + ) + + def encode(self, value): + encoded_elements = self.encode_elements(value) + + return encoded_elements + + +class DynamicArrayEncoder(BaseArrayEncoder): + is_dynamic = True + + def encode(self, value): + encoded_size = encode_uint_256(len(value)) + encoded_elements = self.encode_elements(value) + encoded_value = encoded_size + encoded_elements + + return encoded_value diff --git a/ccxt/static_dependencies/ethereum/abi/exceptions.py b/ccxt/static_dependencies/ethereum/abi/exceptions.py new file mode 100644 index 0000000..becb444 --- /dev/null +++ b/ccxt/static_dependencies/ethereum/abi/exceptions.py @@ -0,0 +1,139 @@ +from ...parsimonious import ( + ParseError +) + + +class EncodingError(Exception): + """ + Base exception for any error that occurs during encoding. + """ + + pass + + +class EncodingTypeError(EncodingError): + """ + Raised when trying to encode a python value whose type is not supported for + the output ABI type. + """ + + pass + + +class IllegalValue(EncodingError): + """ + Raised when trying to encode a python value with the correct type but with + a value that is not considered legal for the output ABI type. + + Example: + + .. code-block:: python + + fixed128x19_encoder(Decimal('NaN')) # cannot encode NaN + + """ + + pass + + +class ValueOutOfBounds(IllegalValue): + """ + Raised when trying to encode a python value with the correct type but with + a value that appears outside the range of valid values for the output ABI + type. + + Example: + + .. code-block:: python + + ufixed8x1_encoder(Decimal('25.6')) # out of bounds + + """ + + pass + + +class DecodingError(Exception): + """ + Base exception for any error that occurs during decoding. + """ + + pass + + +class InsufficientDataBytes(DecodingError): + """ + Raised when there are insufficient data to decode a value for a given ABI + type. + """ + + pass + + +class NonEmptyPaddingBytes(DecodingError): + """ + Raised when the padding bytes of an ABI value are malformed. + """ + + pass + + +class ParseError(ParseError): + """ + Raised when an ABI type string cannot be parsed. + """ + + def __str__(self): + return "Parse error at '{}' (column {}) in type string '{}'".format( + self.text[self.pos : self.pos + 5], + self.column(), + self.text, + ) + + +class ABITypeError(ValueError): + """ + Raised when a parsed ABI type has inconsistent properties; for example, + when trying to parse the type string ``'uint7'`` (which has a bit-width + that is not congruent with zero modulo eight). + """ + + pass + + +class PredicateMappingError(Exception): + """ + Raised when an error occurs in a registry's internal mapping. + """ + + pass + + +class NoEntriesFound(ValueError, PredicateMappingError): + """ + Raised when no registration is found for a type string in a registry's + internal mapping. + + .. warning:: + + In a future version of ``eth-abi``, this error class will no longer + inherit from ``ValueError``. + """ + + pass + + +class MultipleEntriesFound(ValueError, PredicateMappingError): + """ + Raised when multiple registrations are found for a type string in a + registry's internal mapping. This error is non-recoverable and indicates + that a registry was configured incorrectly. Registrations are expected to + cover completely distinct ranges of type strings. + + .. warning:: + + In a future version of ``eth-abi``, this error class will no longer + inherit from ``ValueError``. + """ + + pass diff --git a/ccxt/static_dependencies/ethereum/abi/grammar.py b/ccxt/static_dependencies/ethereum/abi/grammar.py new file mode 100644 index 0000000..0596794 --- /dev/null +++ b/ccxt/static_dependencies/ethereum/abi/grammar.py @@ -0,0 +1,443 @@ +import functools +import re + +from ...parsimonious import ( + expressions, + ParseError, + NodeVisitor, + Grammar +) + +from .exceptions import ( + ABITypeError, + ParseError, +) + +grammar = Grammar( + r""" + type = tuple_type / basic_type + + tuple_type = components arrlist? + components = non_zero_tuple / zero_tuple + + non_zero_tuple = "(" type next_type* ")" + next_type = "," type + + zero_tuple = "()" + + basic_type = base sub? arrlist? + + base = alphas + + sub = two_size / digits + two_size = (digits "x" digits) + + arrlist = (const_arr / dynam_arr)+ + const_arr = "[" digits "]" + dynam_arr = "[]" + + alphas = ~"[A-Za-z]+" + digits = ~"[1-9][0-9]*" + """ +) + + +class NodeVisitor(NodeVisitor): + """ + Parsimonious node visitor which performs both parsing of type strings and + post-processing of parse trees. Parsing operations are cached. + """ + + grammar = grammar + + def visit_non_zero_tuple(self, node, visited_children): + # Ignore left and right parens + _, first, rest, _ = visited_children + + return (first,) + rest + + def visit_tuple_type(self, node, visited_children): + components, arrlist = visited_children + + return TupleType(components, arrlist, node=node) + + def visit_next_type(self, node, visited_children): + # Ignore comma + _, abi_type = visited_children + + return abi_type + + def visit_zero_tuple(self, node, visited_children): + return tuple() + + def visit_basic_type(self, node, visited_children): + base, sub, arrlist = visited_children + + return BasicType(base, sub, arrlist, node=node) + + def visit_two_size(self, node, visited_children): + # Ignore "x" + first, _, second = visited_children + + return first, second + + def visit_const_arr(self, node, visited_children): + # Ignore left and right brackets + _, int_value, _ = visited_children + + return (int_value,) + + def visit_dynam_arr(self, node, visited_children): + return tuple() + + def visit_alphas(self, node, visited_children): + return node.text + + def visit_digits(self, node, visited_children): + return int(node.text) + + def generic_visit(self, node, visited_children): + if isinstance(node.expr, expressions.OneOf): + # Unwrap value chosen from alternatives + return visited_children[0] + if isinstance(node.expr, expressions.Optional): + # Unwrap optional value or return `None` + if len(visited_children) != 0: + return visited_children[0] + + return None + + return tuple(visited_children) + + @functools.lru_cache(maxsize=None) + def parse(self, type_str): + """ + Parses a type string into an appropriate instance of + :class:`~eth_abi.grammar.ABIType`. If a type string cannot be parsed, + throws :class:`~eth_abi.exceptions.ParseError`. + + :param type_str: The type string to be parsed. + :returns: An instance of :class:`~eth_abi.grammar.ABIType` containing + information about the parsed type string. + """ + if not isinstance(type_str, str): + raise TypeError( + "Can only parse string values: got {}".format(type(type_str)) + ) + + try: + return super().parse(type_str) + except ParseError as e: + raise ParseError(e.text, e.pos, e.expr) + + +visitor = NodeVisitor() + + +class ABIType: + """ + Base class for results of type string parsing operations. + """ + + __slots__ = ("arrlist", "node") + + def __init__(self, arrlist=None, node=None): + self.arrlist = arrlist + """ + The list of array dimensions for a parsed type. Equal to ``None`` if + type string has no array dimensions. + """ + + self.node = node + """ + The parsimonious ``Node`` instance associated with this parsed type. + Used to generate error messages for invalid types. + """ + + def __repr__(self): # pragma: no cover + return "<{} {}>".format( + type(self).__qualname__, + repr(self.to_type_str()), + ) + + def __eq__(self, other): + # Two ABI types are equal if their string representations are equal + return type(self) is type(other) and self.to_type_str() == other.to_type_str() + + def to_type_str(self): # pragma: no cover + """ + Returns the string representation of an ABI type. This will be equal to + the type string from which it was created. + """ + raise NotImplementedError("Must implement `to_type_str`") + + @property + def item_type(self): + """ + If this type is an array type, equal to an appropriate + :class:`~eth_abi.grammar.ABIType` instance for the array's items. + """ + raise NotImplementedError("Must implement `item_type`") + + def validate(self): # pragma: no cover + """ + Validates the properties of an ABI type against the solidity ABI spec: + + https://solidity.readthedocs.io/en/develop/abi-spec.html + + Raises :class:`~eth_abi.exceptions.ABITypeError` if validation fails. + """ + raise NotImplementedError("Must implement `validate`") + + def invalidate(self, error_msg): + # Invalidates an ABI type with the given error message. Expects that a + # parsimonious node was provided from the original parsing operation + # that yielded this type. + node = self.node + + raise ABITypeError( + "For '{comp_str}' type at column {col} " + "in '{type_str}': {error_msg}".format( + comp_str=node.text, + col=node.start + 1, + type_str=node.full_text, + error_msg=error_msg, + ), + ) + + @property + def is_array(self): + """ + Equal to ``True`` if a type is an array type (i.e. if it has an array + dimension list). Otherwise, equal to ``False``. + """ + return self.arrlist is not None + + @property + def is_dynamic(self): + """ + Equal to ``True`` if a type has a dynamically sized encoding. + Otherwise, equal to ``False``. + """ + raise NotImplementedError("Must implement `is_dynamic`") + + @property + def _has_dynamic_arrlist(self): + return self.is_array and any(len(dim) == 0 for dim in self.arrlist) + + +class TupleType(ABIType): + """ + Represents the result of parsing a tuple type string e.g. "(int,bool)". + """ + + __slots__ = ("components",) + + def __init__(self, components, arrlist=None, *, node=None): + super().__init__(arrlist, node) + + self.components = components + """ + A tuple of :class:`~eth_abi.grammar.ABIType` instances for each of the + tuple type's components. + """ + + def to_type_str(self): + arrlist = self.arrlist + + if isinstance(arrlist, tuple): + arrlist = "".join(repr(list(a)) for a in arrlist) + else: + arrlist = "" + + return "({}){}".format( + ",".join(c.to_type_str() for c in self.components), + arrlist, + ) + + @property + def item_type(self): + if not self.is_array: + raise ValueError( + "Cannot determine item type for non-array type '{}'".format( + self.to_type_str(), + ) + ) + + return type(self)( + self.components, + self.arrlist[:-1] or None, + node=self.node, + ) + + def validate(self): + for c in self.components: + c.validate() + + @property + def is_dynamic(self): + if self._has_dynamic_arrlist: + return True + + return any(c.is_dynamic for c in self.components) + + +class BasicType(ABIType): + """ + Represents the result of parsing a basic type string e.g. "uint", "address", + "ufixed128x19[][2]". + """ + + __slots__ = ("base", "sub") + + def __init__(self, base, sub=None, arrlist=None, *, node=None): + super().__init__(arrlist, node) + + self.base = base + """The base of a basic type e.g. "uint" for "uint256" etc.""" + + self.sub = sub + """ + The sub type of a basic type e.g. ``256`` for "uint256" or ``(128, 18)`` + for "ufixed128x18" etc. Equal to ``None`` if type string has no sub + type. + """ + + def to_type_str(self): + sub, arrlist = self.sub, self.arrlist + + if isinstance(sub, int): + sub = str(sub) + elif isinstance(sub, tuple): + sub = "x".join(str(s) for s in sub) + else: + sub = "" + + if isinstance(arrlist, tuple): + arrlist = "".join(repr(list(a)) for a in arrlist) + else: + arrlist = "" + + return self.base + sub + arrlist + + @property + def item_type(self): + if not self.is_array: + raise ValueError( + "Cannot determine item type for non-array type '{}'".format( + self.to_type_str(), + ) + ) + + return type(self)( + self.base, + self.sub, + self.arrlist[:-1] or None, + node=self.node, + ) + + @property + def is_dynamic(self): + if self._has_dynamic_arrlist: + return True + + if self.base == "string": + return True + + if self.base == "bytes" and self.sub is None: + return True + + return False + + def validate(self): + base, sub = self.base, self.sub + + # Check validity of string type + if base == "string": + if sub is not None: + self.invalidate("string type cannot have suffix") + + # Check validity of bytes type + elif base == "bytes": + if not (sub is None or isinstance(sub, int)): + self.invalidate( + "bytes type must have either no suffix or a numerical suffix" + ) + + if isinstance(sub, int) and sub > 32: + self.invalidate("maximum 32 bytes for fixed-length bytes") + + # Check validity of integer type + elif base in ("int", "uint"): + if not isinstance(sub, int): + self.invalidate("integer type must have numerical suffix") + + if sub < 8 or 256 < sub: + self.invalidate("integer size out of bounds (max 256 bits)") + + if sub % 8 != 0: + self.invalidate("integer size must be multiple of 8") + + # Check validity of fixed type + elif base in ("fixed", "ufixed"): + if not isinstance(sub, tuple): + self.invalidate( + "fixed type must have suffix of form x, " + "e.g. 128x19", + ) + + bits, minus_e = sub + + if bits < 8 or 256 < bits: + self.invalidate("fixed size out of bounds (max 256 bits)") + + if bits % 8 != 0: + self.invalidate("fixed size must be multiple of 8") + + if minus_e < 1 or 80 < minus_e: + self.invalidate( + "fixed exponent size out of bounds, {} must be in 1-80".format( + minus_e, + ), + ) + + # Check validity of hash type + elif base == "hash": + if not isinstance(sub, int): + self.invalidate("hash type must have numerical suffix") + + # Check validity of address type + elif base == "address": + if sub is not None: + self.invalidate("address cannot have suffix") + + +TYPE_ALIASES = { + "int": "int256", + "uint": "uint256", + "fixed": "fixed128x18", + "ufixed": "ufixed128x18", + "function": "bytes24", + "byte": "bytes1", +} + +TYPE_ALIAS_RE = re.compile( + r"\b({})\b".format("|".join(re.escape(a) for a in TYPE_ALIASES.keys())) +) + + +def normalize(type_str): + """ + Normalizes a type string into its canonical version e.g. the type string + 'int' becomes 'int256', etc. + + :param type_str: The type string to be normalized. + :returns: The canonical version of the input type string. + """ + return TYPE_ALIAS_RE.sub( + lambda match: TYPE_ALIASES[match.group(0)], + type_str, + ) + + +parse = visitor.parse diff --git a/ccxt/static_dependencies/ethereum/abi/packed.py b/ccxt/static_dependencies/ethereum/abi/packed.py new file mode 100644 index 0000000..fcab1d4 --- /dev/null +++ b/ccxt/static_dependencies/ethereum/abi/packed.py @@ -0,0 +1,13 @@ +from .codec import ( + ABIEncoder, +) +from .registry import ( + registry_packed, +) + +default_encoder_packed = ABIEncoder(registry_packed) + +encode_packed = default_encoder_packed.encode +is_encodable_packed = default_encoder_packed.is_encodable +encode_single_packed = default_encoder_packed.encode_single # deprecated +encode_abi_packed = default_encoder_packed.encode_abi # deprecated diff --git a/ccxt/static_dependencies/ethereum/abi/py.typed b/ccxt/static_dependencies/ethereum/abi/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/ccxt/static_dependencies/ethereum/abi/registry.py b/ccxt/static_dependencies/ethereum/abi/registry.py new file mode 100644 index 0000000..632e57e --- /dev/null +++ b/ccxt/static_dependencies/ethereum/abi/registry.py @@ -0,0 +1,643 @@ +import abc +import copy +import functools +from typing import ( + Any, + Callable, + Type, + Union, +) + +from ..typing import ( + abi, +) + +from . import ( + decoding, + encoding, + exceptions, + grammar, +) +from .base import ( + BaseCoder, +) +from .exceptions import ( + ABITypeError, + MultipleEntriesFound, + NoEntriesFound, +) + +Lookup = Union[abi.TypeStr, Callable[[abi.TypeStr], bool]] + +EncoderCallable = Callable[[Any], bytes] +DecoderCallable = Callable[[decoding.ContextFramesBytesIO], Any] + +Encoder = Union[EncoderCallable, Type[encoding.BaseEncoder]] +Decoder = Union[DecoderCallable, Type[decoding.BaseDecoder]] + + +class Copyable(abc.ABC): + @abc.abstractmethod + def copy(self): + pass + + def __copy__(self): + return self.copy() + + def __deepcopy__(self, *args): + return self.copy() + + +class PredicateMapping(Copyable): + """ + Acts as a mapping from predicate functions to values. Values are retrieved + when their corresponding predicate matches a given input. Predicates can + also be labeled to facilitate removal from the mapping. + """ + + def __init__(self, name): + self._name = name + self._values = {} + self._labeled_predicates = {} + + def add(self, predicate, value, label=None): + if predicate in self._values: + raise ValueError( + "Matcher {} already exists in {}".format( + repr(predicate), + self._name, + ) + ) + + if label is not None: + if label in self._labeled_predicates: + raise ValueError( + "Matcher {} with label '{}' already exists in {}".format( + repr(predicate), + label, + self._name, + ), + ) + + self._labeled_predicates[label] = predicate + + self._values[predicate] = value + + def find(self, type_str): + results = tuple( + (predicate, value) + for predicate, value in self._values.items() + if predicate(type_str) + ) + + if len(results) == 0: + raise NoEntriesFound( + "No matching entries for '{}' in {}".format( + type_str, + self._name, + ) + ) + + predicates, values = tuple(zip(*results)) + + if len(results) > 1: + predicate_reprs = ", ".join(map(repr, predicates)) + raise MultipleEntriesFound( + f"Multiple matching entries for '{type_str}' in {self._name}: " + f"{predicate_reprs}. This occurs when two registrations match the " + "same type string. You may need to delete one of the " + "registrations or modify its matching behavior to ensure it " + 'doesn\'t collide with other registrations. See the "Registry" ' + "documentation for more information." + ) + + return values[0] + + def remove_by_equality(self, predicate): + # Delete the predicate mapping to the previously stored value + try: + del self._values[predicate] + except KeyError: + raise KeyError( + "Matcher {} not found in {}".format( + repr(predicate), + self._name, + ) + ) + + # Delete any label which refers to this predicate + try: + label = self._label_for_predicate(predicate) + except ValueError: + pass + else: + del self._labeled_predicates[label] + + def _label_for_predicate(self, predicate): + # Both keys and values in `_labeled_predicates` are unique since the + # `add` method enforces this + for key, value in self._labeled_predicates.items(): + if value is predicate: + return key + + raise ValueError( + "Matcher {} not referred to by any label in {}".format( + repr(predicate), + self._name, + ) + ) + + def remove_by_label(self, label): + try: + predicate = self._labeled_predicates[label] + except KeyError: + raise KeyError("Label '{}' not found in {}".format(label, self._name)) + + del self._labeled_predicates[label] + del self._values[predicate] + + def remove(self, predicate_or_label): + if callable(predicate_or_label): + self.remove_by_equality(predicate_or_label) + elif isinstance(predicate_or_label, str): + self.remove_by_label(predicate_or_label) + else: + raise TypeError( + "Key to be removed must be callable or string: got {}".format( + type(predicate_or_label), + ) + ) + + def copy(self): + cpy = type(self)(self._name) + + cpy._values = copy.copy(self._values) + cpy._labeled_predicates = copy.copy(self._labeled_predicates) + + return cpy + + +class Predicate: + """ + Represents a predicate function to be used for type matching in + ``ABIRegistry``. + """ + + __slots__ = tuple() + + def __call__(self, *args, **kwargs): # pragma: no cover + raise NotImplementedError("Must implement `__call__`") + + def __str__(self): # pragma: no cover + raise NotImplementedError("Must implement `__str__`") + + def __repr__(self): + return "<{} {}>".format(type(self).__name__, self) + + def __iter__(self): + for attr in self.__slots__: + yield getattr(self, attr) + + def __hash__(self): + return hash(tuple(self)) + + def __eq__(self, other): + return type(self) is type(other) and tuple(self) == tuple(other) + + +class Equals(Predicate): + """ + A predicate that matches any input equal to `value`. + """ + + __slots__ = ("value",) + + def __init__(self, value): + self.value = value + + def __call__(self, other): + return self.value == other + + def __str__(self): + return "(== {})".format(repr(self.value)) + + +class BaseEquals(Predicate): + """ + A predicate that matches a basic type string with a base component equal to + `value` and no array component. If `with_sub` is `True`, the type string + must have a sub component to match. If `with_sub` is `False`, the type + string must *not* have a sub component to match. If `with_sub` is None, + the type string's sub component is ignored. + """ + + __slots__ = ("base", "with_sub") + + def __init__(self, base, *, with_sub=None): + self.base = base + self.with_sub = with_sub + + def __call__(self, type_str): + try: + abi_type = grammar.parse(type_str) + except exceptions.ParseError: + return False + + if isinstance(abi_type, grammar.BasicType): + if abi_type.arrlist is not None: + return False + + if self.with_sub is not None: + if self.with_sub and abi_type.sub is None: + return False + if not self.with_sub and abi_type.sub is not None: + return False + + return abi_type.base == self.base + + # We'd reach this point if `type_str` did not contain a basic type + # e.g. if it contained a tuple type + return False + + def __str__(self): + return "(base == {}{})".format( + repr(self.base), + "" + if self.with_sub is None + else (" and sub is not None" if self.with_sub else " and sub is None"), + ) + + +def has_arrlist(type_str): + """ + A predicate that matches a type string with an array dimension list. + """ + try: + abi_type = grammar.parse(type_str) + except exceptions.ParseError: + return False + + return abi_type.arrlist is not None + + +def is_base_tuple(type_str): + """ + A predicate that matches a tuple type with no array dimension list. + """ + try: + abi_type = grammar.parse(type_str) + except exceptions.ParseError: + return False + + return isinstance(abi_type, grammar.TupleType) and abi_type.arrlist is None + + +def _clear_encoder_cache(old_method): + @functools.wraps(old_method) + def new_method(self, *args, **kwargs): + self.get_encoder.cache_clear() + return old_method(self, *args, **kwargs) + + return new_method + + +def _clear_decoder_cache(old_method): + @functools.wraps(old_method) + def new_method(self, *args, **kwargs): + self.get_decoder.cache_clear() + return old_method(self, *args, **kwargs) + + return new_method + + +class BaseRegistry: + @staticmethod + def _register(mapping, lookup, value, label=None): + if callable(lookup): + mapping.add(lookup, value, label) + return + + if isinstance(lookup, str): + mapping.add(Equals(lookup), value, lookup) + return + + raise TypeError( + "Lookup must be a callable or a value of type `str`: got {}".format( + repr(lookup), + ) + ) + + @staticmethod + def _unregister(mapping, lookup_or_label): + if callable(lookup_or_label): + mapping.remove_by_equality(lookup_or_label) + return + + if isinstance(lookup_or_label, str): + mapping.remove_by_label(lookup_or_label) + return + + raise TypeError( + "Lookup/label must be a callable or a value of type `str`: got {}".format( + repr(lookup_or_label), + ) + ) + + @staticmethod + def _get_registration(mapping, type_str): + try: + value = mapping.find(type_str) + except ValueError as e: + if "No matching" in e.args[0]: + # If no matches found, attempt to parse in case lack of matches + # was due to unparsability + grammar.parse(type_str) + + raise + + return value + + +class ABIRegistry(Copyable, BaseRegistry): + def __init__(self): + self._encoders = PredicateMapping("encoder registry") + self._decoders = PredicateMapping("decoder registry") + + def _get_registration(self, mapping, type_str): + coder = super()._get_registration(mapping, type_str) + + if isinstance(coder, type) and issubclass(coder, BaseCoder): + return coder.from_type_str(type_str, self) + + return coder + + @_clear_encoder_cache + def register_encoder( + self, lookup: Lookup, encoder: Encoder, label: str = None + ) -> None: + """ + Registers the given ``encoder`` under the given ``lookup``. A unique + string label may be optionally provided that can be used to refer to + the registration by name. For more information about arguments, refer + to :any:`register`. + """ + self._register(self._encoders, lookup, encoder, label=label) + + @_clear_encoder_cache + def unregister_encoder(self, lookup_or_label: Lookup) -> None: + """ + Unregisters an encoder in the registry with the given lookup or label. + If ``lookup_or_label`` is a string, the encoder with the label + ``lookup_or_label`` will be unregistered. If it is an function, the + encoder with the lookup function ``lookup_or_label`` will be + unregistered. + """ + self._unregister(self._encoders, lookup_or_label) + + @_clear_decoder_cache + def register_decoder( + self, lookup: Lookup, decoder: Decoder, label: str = None + ) -> None: + """ + Registers the given ``decoder`` under the given ``lookup``. A unique + string label may be optionally provided that can be used to refer to + the registration by name. For more information about arguments, refer + to :any:`register`. + """ + self._register(self._decoders, lookup, decoder, label=label) + + @_clear_decoder_cache + def unregister_decoder(self, lookup_or_label: Lookup) -> None: + """ + Unregisters a decoder in the registry with the given lookup or label. + If ``lookup_or_label`` is a string, the decoder with the label + ``lookup_or_label`` will be unregistered. If it is an function, the + decoder with the lookup function ``lookup_or_label`` will be + unregistered. + """ + self._unregister(self._decoders, lookup_or_label) + + def register( + self, lookup: Lookup, encoder: Encoder, decoder: Decoder, label: str = None + ) -> None: + """ + Registers the given ``encoder`` and ``decoder`` under the given + ``lookup``. A unique string label may be optionally provided that can + be used to refer to the registration by name. + + :param lookup: A type string or type string matcher function + (predicate). When the registry is queried with a type string + ``query`` to determine which encoder or decoder to use, ``query`` + will be checked against every registration in the registry. If a + registration was created with a type string for ``lookup``, it will + be considered a match if ``lookup == query``. If a registration + was created with a matcher function for ``lookup``, it will be + considered a match if ``lookup(query) is True``. If more than one + registration is found to be a match, then an exception is raised. + + :param encoder: An encoder callable or class to use if ``lookup`` + matches a query. If ``encoder`` is a callable, it must accept a + python value and return a ``bytes`` value. If ``encoder`` is a + class, it must be a valid subclass of :any:`encoding.BaseEncoder` + and must also implement the :any:`from_type_str` method on + :any:`base.BaseCoder`. + + :param decoder: A decoder callable or class to use if ``lookup`` + matches a query. If ``decoder`` is a callable, it must accept a + stream-like object of bytes and return a python value. If + ``decoder`` is a class, it must be a valid subclass of + :any:`decoding.BaseDecoder` and must also implement the + :any:`from_type_str` method on :any:`base.BaseCoder`. + + :param label: An optional label that can be used to refer to this + registration by name. This label can be used to unregister an + entry in the registry via the :any:`unregister` method and its + variants. + """ + self.register_encoder(lookup, encoder, label=label) + self.register_decoder(lookup, decoder, label=label) + + def unregister(self, label: str) -> None: + """ + Unregisters the entries in the encoder and decoder registries which + have the label ``label``. + """ + self.unregister_encoder(label) + self.unregister_decoder(label) + + @functools.lru_cache(maxsize=None) + def get_encoder(self, type_str): + return self._get_registration(self._encoders, type_str) + + def has_encoder(self, type_str: abi.TypeStr) -> bool: + """ + Returns ``True`` if an encoder is found for the given type string + ``type_str``. Otherwise, returns ``False``. Raises + :class:`~eth_abi.exceptions.MultipleEntriesFound` if multiple encoders + are found. + """ + try: + self.get_encoder(type_str) + except (ABITypeError, NoEntriesFound): + return False + else: + return True + + @functools.lru_cache(maxsize=None) + def get_decoder(self, type_str): + return self._get_registration(self._decoders, type_str) + + def copy(self): + """ + Copies a registry such that new registrations can be made or existing + registrations can be unregistered without affecting any instance from + which a copy was obtained. This is useful if an existing registry + fulfills most of a user's needs but requires one or two modifications. + In that case, a copy of that registry can be obtained and the necessary + changes made without affecting the original registry. + """ + cpy = type(self)() + + cpy._encoders = copy.copy(self._encoders) + cpy._decoders = copy.copy(self._decoders) + + return cpy + + +registry = ABIRegistry() + +registry.register( + BaseEquals("uint"), + encoding.UnsignedIntegerEncoder, + decoding.UnsignedIntegerDecoder, + label="uint", +) +registry.register( + BaseEquals("int"), + encoding.SignedIntegerEncoder, + decoding.SignedIntegerDecoder, + label="int", +) +registry.register( + BaseEquals("address"), + encoding.AddressEncoder, + decoding.AddressDecoder, + label="address", +) +registry.register( + BaseEquals("bool"), + encoding.BooleanEncoder, + decoding.BooleanDecoder, + label="bool", +) +registry.register( + BaseEquals("ufixed"), + encoding.UnsignedFixedEncoder, + decoding.UnsignedFixedDecoder, + label="ufixed", +) +registry.register( + BaseEquals("fixed"), + encoding.SignedFixedEncoder, + decoding.SignedFixedDecoder, + label="fixed", +) +registry.register( + BaseEquals("bytes", with_sub=True), + encoding.BytesEncoder, + decoding.BytesDecoder, + label="bytes", +) +registry.register( + BaseEquals("bytes", with_sub=False), + encoding.ByteStringEncoder, + decoding.ByteStringDecoder, + label="bytes", +) +registry.register( + BaseEquals("function"), + encoding.BytesEncoder, + decoding.BytesDecoder, + label="function", +) +registry.register( + BaseEquals("string"), + encoding.TextStringEncoder, + decoding.StringDecoder, + label="string", +) +registry.register( + has_arrlist, + encoding.BaseArrayEncoder, + decoding.BaseArrayDecoder, + label="has_arrlist", +) +registry.register( + is_base_tuple, + encoding.TupleEncoder, + decoding.TupleDecoder, + label="is_base_tuple", +) + +registry_packed = ABIRegistry() + +registry_packed.register_encoder( + BaseEquals("uint"), + encoding.PackedUnsignedIntegerEncoder, + label="uint", +) +registry_packed.register_encoder( + BaseEquals("int"), + encoding.PackedSignedIntegerEncoder, + label="int", +) +registry_packed.register_encoder( + BaseEquals("address"), + encoding.PackedAddressEncoder, + label="address", +) +registry_packed.register_encoder( + BaseEquals("bool"), + encoding.PackedBooleanEncoder, + label="bool", +) +registry_packed.register_encoder( + BaseEquals("ufixed"), + encoding.PackedUnsignedFixedEncoder, + label="ufixed", +) +registry_packed.register_encoder( + BaseEquals("fixed"), + encoding.PackedSignedFixedEncoder, + label="fixed", +) +registry_packed.register_encoder( + BaseEquals("bytes", with_sub=True), + encoding.PackedBytesEncoder, + label="bytes", +) +registry_packed.register_encoder( + BaseEquals("bytes", with_sub=False), + encoding.PackedByteStringEncoder, + label="bytes", +) +registry_packed.register_encoder( + BaseEquals("function"), + encoding.PackedBytesEncoder, + label="function", +) +registry_packed.register_encoder( + BaseEquals("string"), + encoding.PackedTextStringEncoder, + label="string", +) +registry_packed.register_encoder( + has_arrlist, + encoding.PackedArrayEncoder, + label="has_arrlist", +) +registry_packed.register_encoder( + is_base_tuple, + encoding.TupleEncoder, + label="is_base_tuple", +) diff --git a/ccxt/static_dependencies/ethereum/abi/tools/__init__.py b/ccxt/static_dependencies/ethereum/abi/tools/__init__.py new file mode 100644 index 0000000..5123dfa --- /dev/null +++ b/ccxt/static_dependencies/ethereum/abi/tools/__init__.py @@ -0,0 +1,3 @@ +from ._strategies import ( # noqa: F401 + get_abi_strategy, +) diff --git a/ccxt/static_dependencies/ethereum/abi/tools/_strategies.py b/ccxt/static_dependencies/ethereum/abi/tools/_strategies.py new file mode 100644 index 0000000..5d004e6 --- /dev/null +++ b/ccxt/static_dependencies/ethereum/abi/tools/_strategies.py @@ -0,0 +1,230 @@ +from typing import ( + Callable, + Union, +) + +from ...typing.abi import ( + TypeStr, +) +from ..utils import ( + to_checksum_address, +) +from hypothesis import ( + strategies as st, +) + +from ..grammar import ( + ABIType, + normalize, + parse, +) +from ..registry import ( + BaseEquals, + BaseRegistry, + Lookup, + PredicateMapping, + has_arrlist, + is_base_tuple, +) +from ..utils.numeric import ( + scale_places, +) + +StrategyFactory = Callable[[ABIType, "StrategyRegistry"], st.SearchStrategy] +StrategyRegistration = Union[st.SearchStrategy, StrategyFactory] + + +class StrategyRegistry(BaseRegistry): + def __init__(self): + self._strategies = PredicateMapping("strategy registry") + + def register_strategy( + self, lookup: Lookup, registration: StrategyRegistration, label: str = None + ) -> None: + self._register(self._strategies, lookup, registration, label=label) + + def unregister_strategy(self, lookup_or_label: Lookup) -> None: + self._unregister(self._strategies, lookup_or_label) + + def get_strategy(self, type_str: TypeStr) -> st.SearchStrategy: + """ + Returns a hypothesis strategy for the given ABI type. + + :param type_str: The canonical string representation of the ABI type + for which a hypothesis strategy should be returned. + + :returns: A hypothesis strategy for generating Python values that are + encodable as values of the given ABI type. + """ + registration = self._get_registration(self._strategies, type_str) + + if isinstance(registration, st.SearchStrategy): + # If a hypothesis strategy was registered, just return it + return registration + else: + # Otherwise, assume the factory is a callable. Call it with the abi + # type to get an appropriate hypothesis strategy. + normalized_type_str = normalize(type_str) + abi_type = parse(normalized_type_str) + strategy = registration(abi_type, self) + + return strategy + + +def get_uint_strategy( + abi_type: ABIType, registry: StrategyRegistry +) -> st.SearchStrategy: + bits = abi_type.sub + + return st.integers( + min_value=0, + max_value=2**bits - 1, + ) + + +def get_int_strategy( + abi_type: ABIType, registry: StrategyRegistry +) -> st.SearchStrategy: + bits = abi_type.sub + + return st.integers( + min_value=-(2 ** (bits - 1)), + max_value=2 ** (bits - 1) - 1, + ) + + +address_strategy = st.binary(min_size=20, max_size=20).map(to_checksum_address) +bool_strategy = st.booleans() + + +def get_ufixed_strategy( + abi_type: ABIType, registry: StrategyRegistry +) -> st.SearchStrategy: + bits, places = abi_type.sub + + return st.decimals( + min_value=0, + max_value=2**bits - 1, + places=0, + ).map(scale_places(places)) + + +def get_fixed_strategy( + abi_type: ABIType, registry: StrategyRegistry +) -> st.SearchStrategy: + bits, places = abi_type.sub + + return st.decimals( + min_value=-(2 ** (bits - 1)), + max_value=2 ** (bits - 1) - 1, + places=0, + ).map(scale_places(places)) + + +def get_bytes_strategy( + abi_type: ABIType, registry: StrategyRegistry +) -> st.SearchStrategy: + num_bytes = abi_type.sub + + return st.binary( + min_size=num_bytes, + max_size=num_bytes, + ) + + +bytes_strategy = st.binary(min_size=0, max_size=4096) +string_strategy = st.text() + + +def get_array_strategy( + abi_type: ABIType, registry: StrategyRegistry +) -> st.SearchStrategy: + item_type = abi_type.item_type + item_type_str = item_type.to_type_str() + item_strategy = registry.get_strategy(item_type_str) + + last_dim = abi_type.arrlist[-1] + if len(last_dim) == 0: + # Is dynamic list. Don't restrict length. + return st.lists(item_strategy) + else: + # Is static list. Restrict length. + dim_size = last_dim[0] + return st.lists(item_strategy, min_size=dim_size, max_size=dim_size) + + +def get_tuple_strategy( + abi_type: ABIType, registry: StrategyRegistry +) -> st.SearchStrategy: + component_strategies = [ + registry.get_strategy(comp_abi_type.to_type_str()) + for comp_abi_type in abi_type.components + ] + + return st.tuples(*component_strategies) + + +strategy_registry = StrategyRegistry() + +strategy_registry.register_strategy( + BaseEquals("uint"), + get_uint_strategy, + label="uint", +) +strategy_registry.register_strategy( + BaseEquals("int"), + get_int_strategy, + label="int", +) +strategy_registry.register_strategy( + BaseEquals("address", with_sub=False), + address_strategy, + label="address", +) +strategy_registry.register_strategy( + BaseEquals("bool", with_sub=False), + bool_strategy, + label="bool", +) +strategy_registry.register_strategy( + BaseEquals("ufixed"), + get_ufixed_strategy, + label="ufixed", +) +strategy_registry.register_strategy( + BaseEquals("fixed"), + get_fixed_strategy, + label="fixed", +) +strategy_registry.register_strategy( + BaseEquals("bytes", with_sub=True), + get_bytes_strategy, + label="bytes", +) +strategy_registry.register_strategy( + BaseEquals("bytes", with_sub=False), + bytes_strategy, + label="bytes", +) +strategy_registry.register_strategy( + BaseEquals("function", with_sub=False), + get_bytes_strategy, + label="function", +) +strategy_registry.register_strategy( + BaseEquals("string", with_sub=False), + string_strategy, + label="string", +) +strategy_registry.register_strategy( + has_arrlist, + get_array_strategy, + label="has_arrlist", +) +strategy_registry.register_strategy( + is_base_tuple, + get_tuple_strategy, + label="is_base_tuple", +) + +get_abi_strategy = strategy_registry.get_strategy diff --git a/ccxt/static_dependencies/ethereum/abi/utils/__init__.py b/ccxt/static_dependencies/ethereum/abi/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ccxt/static_dependencies/ethereum/abi/utils/__pycache__/__init__.cpython-311.pyc b/ccxt/static_dependencies/ethereum/abi/utils/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..33d7504 Binary files /dev/null and b/ccxt/static_dependencies/ethereum/abi/utils/__pycache__/__init__.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/ethereum/abi/utils/__pycache__/numeric.cpython-311.pyc b/ccxt/static_dependencies/ethereum/abi/utils/__pycache__/numeric.cpython-311.pyc new file mode 100644 index 0000000..d642e30 Binary files /dev/null and b/ccxt/static_dependencies/ethereum/abi/utils/__pycache__/numeric.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/ethereum/abi/utils/__pycache__/padding.cpython-311.pyc b/ccxt/static_dependencies/ethereum/abi/utils/__pycache__/padding.cpython-311.pyc new file mode 100644 index 0000000..6d68b51 Binary files /dev/null and b/ccxt/static_dependencies/ethereum/abi/utils/__pycache__/padding.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/ethereum/abi/utils/__pycache__/string.cpython-311.pyc b/ccxt/static_dependencies/ethereum/abi/utils/__pycache__/string.cpython-311.pyc new file mode 100644 index 0000000..f9d71e6 Binary files /dev/null and b/ccxt/static_dependencies/ethereum/abi/utils/__pycache__/string.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/ethereum/abi/utils/numeric.py b/ccxt/static_dependencies/ethereum/abi/utils/numeric.py new file mode 100644 index 0000000..acc030b --- /dev/null +++ b/ccxt/static_dependencies/ethereum/abi/utils/numeric.py @@ -0,0 +1,83 @@ +import decimal +from typing import ( + Callable, + Tuple, +) + +ABI_DECIMAL_PREC = 999 + +abi_decimal_context = decimal.Context(prec=ABI_DECIMAL_PREC) + +ZERO = decimal.Decimal(0) +TEN = decimal.Decimal(10) + + +def ceil32(x: int) -> int: + return x if x % 32 == 0 else x + 32 - (x % 32) + + +def compute_unsigned_integer_bounds(num_bits: int) -> Tuple[int, int]: + return ( + 0, + 2**num_bits - 1, + ) + + +def compute_signed_integer_bounds(num_bits: int) -> Tuple[int, int]: + return ( + -1 * 2 ** (num_bits - 1), + 2 ** (num_bits - 1) - 1, + ) + + +def compute_unsigned_fixed_bounds( + num_bits: int, + frac_places: int, +) -> Tuple[decimal.Decimal, decimal.Decimal]: + int_upper = compute_unsigned_integer_bounds(num_bits)[1] + + with decimal.localcontext(abi_decimal_context): + upper = decimal.Decimal(int_upper) * TEN**-frac_places + + return ZERO, upper + + +def compute_signed_fixed_bounds( + num_bits: int, + frac_places: int, +) -> Tuple[decimal.Decimal, decimal.Decimal]: + int_lower, int_upper = compute_signed_integer_bounds(num_bits) + + with decimal.localcontext(abi_decimal_context): + exp = TEN**-frac_places + lower = decimal.Decimal(int_lower) * exp + upper = decimal.Decimal(int_upper) * exp + + return lower, upper + + +def scale_places(places: int) -> Callable[[decimal.Decimal], decimal.Decimal]: + """ + Returns a function that shifts the decimal point of decimal values to the + right by ``places`` places. + """ + if not isinstance(places, int): + raise ValueError( + f"Argument `places` must be int. Got value {places} " + f"of type {type(places)}.", + ) + + with decimal.localcontext(abi_decimal_context): + scaling_factor = TEN**-places + + def f(x: decimal.Decimal) -> decimal.Decimal: + with decimal.localcontext(abi_decimal_context): + return x * scaling_factor + + places_repr = f"Eneg{places}" if places > 0 else f"Epos{-places}" + func_name = f"scale_by_{places_repr}" + + f.__name__ = func_name + f.__qualname__ = func_name + + return f diff --git a/ccxt/static_dependencies/ethereum/abi/utils/padding.py b/ccxt/static_dependencies/ethereum/abi/utils/padding.py new file mode 100644 index 0000000..8cecd13 --- /dev/null +++ b/ccxt/static_dependencies/ethereum/abi/utils/padding.py @@ -0,0 +1,27 @@ +from ...utils.toolz import ( + curry, +) + + +@curry +def zpad(value: bytes, length: int) -> bytes: + return value.rjust(length, b"\x00") + + +zpad32 = zpad(length=32) + + +@curry +def zpad_right(value: bytes, length: int) -> bytes: + return value.ljust(length, b"\x00") + + +zpad32_right = zpad_right(length=32) + + +@curry +def fpad(value: bytes, length: int) -> bytes: + return value.rjust(length, b"\xff") + + +fpad32 = fpad(length=32) diff --git a/ccxt/static_dependencies/ethereum/abi/utils/string.py b/ccxt/static_dependencies/ethereum/abi/utils/string.py new file mode 100644 index 0000000..abd76b8 --- /dev/null +++ b/ccxt/static_dependencies/ethereum/abi/utils/string.py @@ -0,0 +1,19 @@ +from typing import ( + Any, +) + + +def abbr(value: Any, limit: int = 79) -> str: + """ + Converts a value into its string representation and abbreviates that + representation based on the given length `limit` if necessary. + """ + rep = repr(value) + + if len(rep) > limit: + if limit < 3: + raise ValueError("Abbreviation limit may not be less than 3") + + rep = rep[: limit - 3] + "..." + + return rep diff --git a/ccxt/static_dependencies/ethereum/account/__init__.py b/ccxt/static_dependencies/ethereum/account/__init__.py new file mode 100644 index 0000000..44dae89 --- /dev/null +++ b/ccxt/static_dependencies/ethereum/account/__init__.py @@ -0,0 +1,3 @@ +from .messages import * + +__all__ = ["messages"] diff --git a/ccxt/static_dependencies/ethereum/account/__pycache__/__init__.cpython-311.pyc b/ccxt/static_dependencies/ethereum/account/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..e6ccedd Binary files /dev/null and b/ccxt/static_dependencies/ethereum/account/__pycache__/__init__.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/ethereum/account/__pycache__/messages.cpython-311.pyc b/ccxt/static_dependencies/ethereum/account/__pycache__/messages.cpython-311.pyc new file mode 100644 index 0000000..63d2eae Binary files /dev/null and b/ccxt/static_dependencies/ethereum/account/__pycache__/messages.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/ethereum/account/encode_typed_data/__init__.py b/ccxt/static_dependencies/ethereum/account/encode_typed_data/__init__.py new file mode 100644 index 0000000..0e1fbec --- /dev/null +++ b/ccxt/static_dependencies/ethereum/account/encode_typed_data/__init__.py @@ -0,0 +1,4 @@ +from .encoding_and_hashing import ( + hash_domain, + hash_eip712_message, +) diff --git a/ccxt/static_dependencies/ethereum/account/encode_typed_data/__pycache__/__init__.cpython-311.pyc b/ccxt/static_dependencies/ethereum/account/encode_typed_data/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..870a0b9 Binary files /dev/null and b/ccxt/static_dependencies/ethereum/account/encode_typed_data/__pycache__/__init__.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/ethereum/account/encode_typed_data/__pycache__/encoding_and_hashing.cpython-311.pyc b/ccxt/static_dependencies/ethereum/account/encode_typed_data/__pycache__/encoding_and_hashing.cpython-311.pyc new file mode 100644 index 0000000..8c1d6d0 Binary files /dev/null and b/ccxt/static_dependencies/ethereum/account/encode_typed_data/__pycache__/encoding_and_hashing.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/ethereum/account/encode_typed_data/__pycache__/helpers.cpython-311.pyc b/ccxt/static_dependencies/ethereum/account/encode_typed_data/__pycache__/helpers.cpython-311.pyc new file mode 100644 index 0000000..cb41432 Binary files /dev/null and b/ccxt/static_dependencies/ethereum/account/encode_typed_data/__pycache__/helpers.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/ethereum/account/encode_typed_data/encoding_and_hashing.py b/ccxt/static_dependencies/ethereum/account/encode_typed_data/encoding_and_hashing.py new file mode 100644 index 0000000..c7587b7 --- /dev/null +++ b/ccxt/static_dependencies/ethereum/account/encode_typed_data/encoding_and_hashing.py @@ -0,0 +1,239 @@ +from typing import ( + Any, + Dict, + List, + Tuple, + Union, +) + +from ...abi import ( + encode, +) +from ....keccak import ( + SHA3 as keccak +) +from ...utils import ( + to_bytes, + to_int, +) + +from .helpers import ( + EIP712_SOLIDITY_TYPES, + is_0x_prefixed_hexstr, + is_array_type, + parse_core_array_type, + parse_parent_array_type, +) + + +def get_primary_type(types: Dict[str, List[Dict[str, str]]]) -> str: + custom_types = set(types.keys()) + custom_types_that_are_deps = set() + + for type_ in custom_types: + type_fields = types[type_] + for field in type_fields: + parsed_type = parse_core_array_type(field["type"]) + if parsed_type in custom_types and parsed_type != type_: + custom_types_that_are_deps.add(parsed_type) + + primary_type = list(custom_types.difference(custom_types_that_are_deps)) + if len(primary_type) == 1: + return primary_type[0] + else: + raise ValueError("Unable to determine primary type") + + +def encode_field( + types: Dict[str, List[Dict[str, str]]], + name: str, + type_: str, + value: Any, +) -> Tuple[str, Union[int, bytes]]: + if type_ in types.keys(): + # type is a custom type + if value is None: + return ("bytes32", b"\x00" * 32) + else: + return ("bytes32", keccak(encode_data(type_, types, value))) + + elif type_ in ["string", "bytes"] and value is None: + return ("bytes32", b"") + + # None is allowed only for custom and dynamic types + elif value is None: + raise ValueError(f"Missing value for field `{name}` of type `{type_}`") + + elif is_array_type(type_): + # handle array type with non-array value + if not isinstance(value, list): + raise ValueError( + f"Invalid value for field `{name}` of type `{type_}`: " + f"expected array, got `{value}` of type `{type(value)}`" + ) + + parsed_type = parse_parent_array_type(type_) + type_value_pairs = [ + encode_field(types, name, parsed_type, item) for item in value + ] + if not type_value_pairs: + # the keccak hash of `encode((), ())` + return ( + "bytes32", + b"\xc5\xd2F\x01\x86\xf7#<\x92~}\xb2\xdc\xc7\x03\xc0\xe5\x00\xb6S\xca\x82';{\xfa\xd8\x04]\x85\xa4p", # noqa: E501 + ) + + data_types, data_hashes = zip(*type_value_pairs) + return ("bytes32", keccak(encode(data_types, data_hashes))) + + elif type_ == "bool": + return (type_, bool(value)) + + # all bytes types allow hexstr and str values + elif type_.startswith("bytes"): + if not isinstance(value, bytes): + if is_0x_prefixed_hexstr(value): + value = to_bytes(hexstr=value) + elif isinstance(value, str): + value = to_bytes(text=value) + else: + if isinstance(value, int) and value < 0: + value = 0 + + value = to_bytes(value) + + return ( + # keccak hash if dynamic `bytes` type + ("bytes32", keccak(value)) + if type_ == "bytes" + # if fixed bytesXX type, do not hash + else (type_, value) + ) + + elif type_ == "string": + if isinstance(value, int): + value = to_bytes(value) + else: + value = to_bytes(text=value) + return ("bytes32", keccak(value)) + + # allow string values for int and uint types + elif type(value) == str and type_.startswith(("int", "uint")): + if is_0x_prefixed_hexstr(value): + return (type_, to_int(hexstr=value)) + else: + return (type_, to_int(text=value)) + + return (type_, value) + + +def find_type_dependencies(type_, types, results=None): + if results is None: + results = set() + + # a type must be a string + if not isinstance(type_, str): + raise ValueError( + "Invalid find_type_dependencies input: expected string, got " + f"`{type_}` of type `{type(type_)}`" + ) + # get core type if it's an array type + type_ = parse_core_array_type(type_) + + if ( + # don't look for dependencies of solidity types + type_ in EIP712_SOLIDITY_TYPES + # found a type that's already been added + or type_ in results + ): + return results + + # found a type that isn't defined + elif type_ not in types: + raise ValueError(f"No definition of type `{type_}`") + + results.add(type_) + + for field in types[type_]: + find_type_dependencies(field["type"], types, results) + return results + + +def encode_type(type_: str, types: Dict[str, List[Dict[str, str]]]) -> str: + result = "" + unsorted_deps = find_type_dependencies(type_, types) + if type_ in unsorted_deps: + unsorted_deps.remove(type_) + + deps = [type_] + sorted(list(unsorted_deps)) + for type_ in deps: + children_list = [] + for child in types[type_]: + child_type = child["type"] + child_name = child["name"] + children_list.append(f"{child_type} {child_name}") + + result += f"{type_}({','.join(children_list)})" + return result + + +def hash_type(type_: str, types: Dict[str, List[Dict[str, str]]]) -> bytes: + return keccak(to_bytes(text=encode_type(type_, types))) + + +def encode_data( + type_: str, + types: Dict[str, List[Dict[str, str]]], + data: Dict[str, Any], +) -> bytes: + encoded_types: List[str] = ["bytes32"] + encoded_values: List[Union[bytes, int]] = [hash_type(type_, types)] + + for field in types[type_]: + type, value = encode_field( + types, field["name"], field["type"], data.get(field["name"]) + ) + encoded_types.append(type) + encoded_values.append(value) + + return encode(encoded_types, encoded_values) + + +def hash_struct( + type_: str, + types: Dict[str, List[Dict[str, str]]], + data: Dict[str, Any], +) -> bytes: + encoded = encode_data(type_, types, data) + return keccak(encoded) + + +def hash_eip712_message( + # returns the same hash as `hash_struct`, but automatically determines primary type + message_types: Dict[str, List[Dict[str, str]]], + message_data: Dict[str, Any], +) -> bytes: + primary_type = get_primary_type(message_types) + return keccak(encode_data(primary_type, message_types, message_data)) + + +def hash_domain(domain_data: Dict[str, Any]) -> bytes: + eip712_domain_map = { + "name": {"name": "name", "type": "string"}, + "version": {"name": "version", "type": "string"}, + "chainId": {"name": "chainId", "type": "uint256"}, + "verifyingContract": {"name": "verifyingContract", "type": "address"}, + "salt": {"name": "salt", "type": "bytes32"}, + } + + for k in domain_data.keys(): + if k not in eip712_domain_map.keys(): + raise ValueError(f"Invalid domain key: `{k}`") + + domain_types = { + "EIP712Domain": [ + eip712_domain_map[k] for k in eip712_domain_map.keys() if k in domain_data + ] + } + + return hash_struct("EIP712Domain", domain_types, domain_data) diff --git a/ccxt/static_dependencies/ethereum/account/encode_typed_data/helpers.py b/ccxt/static_dependencies/ethereum/account/encode_typed_data/helpers.py new file mode 100644 index 0000000..67ec495 --- /dev/null +++ b/ccxt/static_dependencies/ethereum/account/encode_typed_data/helpers.py @@ -0,0 +1,40 @@ +from typing import ( + Any, +) + +from ...utils import ( + is_hexstr, +) + + +def _get_eip712_solidity_types(): + types = ["bool", "address", "string", "bytes", "uint", "int"] + ints = [f"int{(x + 1) * 8}" for x in range(32)] + uints = [f"uint{(x + 1) * 8}" for x in range(32)] + bytes_ = [f"bytes{x + 1}" for x in range(32)] + return types + ints + uints + bytes_ + + +EIP712_SOLIDITY_TYPES = _get_eip712_solidity_types() + + +def is_array_type(type_: str) -> bool: + return type_.endswith("]") + + +def is_0x_prefixed_hexstr(value: Any) -> bool: + return is_hexstr(value) and value.startswith("0x") + + +# strip all brackets: Person[][] -> Person +def parse_core_array_type(type_: str) -> str: + if is_array_type(type_): + type_ = type_[: type_.index("[")] + return type_ + + +# strip only last set of brackets: Person[3][1] -> Person[3] +def parse_parent_array_type(type_: str) -> str: + if is_array_type(type_): + type_ = type_[: type_.rindex("[")] + return type_ diff --git a/ccxt/static_dependencies/ethereum/account/messages.py b/ccxt/static_dependencies/ethereum/account/messages.py new file mode 100644 index 0000000..28794e5 --- /dev/null +++ b/ccxt/static_dependencies/ethereum/account/messages.py @@ -0,0 +1,263 @@ +from collections.abc import ( + Mapping, +) +from typing import ( + Any, + Dict, + NamedTuple, +) +import warnings + +from ..typing import ( + Address, +) +from ..utils.curried import ( + ValidationError, +) +from ..hexbytes import ( + HexBytes, +) + +from .encode_typed_data.encoding_and_hashing import ( + hash_domain, + hash_eip712_message, +) + +# watch for updates to signature format +class SignableMessage(NamedTuple): + """ + A message compatible with EIP-191_ that is ready to be signed. + + The properties are components of an EIP-191_ signable message. Other message formats + can be encoded into this format for easy signing. This data structure doesn't need + to know about the original message format. For example, you can think of + EIP-712 as compiling down to an EIP-191 message. + + In typical usage, you should never need to create these by hand. Instead, use + one of the available encode_* methods in this module, like: + + - :meth:`encode_typed_data` + + .. _EIP-191: https://eips.ethereum.org/EIPS/eip-191 + """ + + version: bytes # must be length 1 + header: bytes # aka "version specific data" + body: bytes # aka "data to sign" + +def encode_typed_data( + domain_data: Dict[str, Any] = None, + message_types: Dict[str, Any] = None, + message_data: Dict[str, Any] = None, + full_message: Dict[str, Any] = None, +) -> SignableMessage: + r""" + Encode an EIP-712_ message in a manner compatible with other implementations + in use, such as the Metamask and Ethers ``signTypedData`` functions. + + See the `EIP-712 spec `_ for more information. + + You may supply the information to be encoded in one of two ways: + + As exactly three arguments: + + - ``domain_data``, a dict of the EIP-712 domain data + - ``message_types``, a dict of custom types (do not include a ``EIP712Domain`` + key) + - ``message_data``, a dict of the data to be signed + + Or as a single argument: + + - ``full_message``, a dict containing the following keys: + - ``types``, a dict of custom types (may include a ``EIP712Domain`` key) + - ``primaryType``, (optional) a string of the primary type of the message + - ``domain``, a dict of the EIP-712 domain data + - ``message``, a dict of the data to be signed + + .. WARNING:: Note that this code has not gone through an external audit, and + the test cases are incomplete. + + Type Coercion: + - For fixed-size bytes types, smaller values will be padded to fit in larger + types, but values larger than the type will raise ``ValueOutOfBounds``. + e.g., an 8-byte value will be padded to fit a ``bytes16`` type, but 16-byte + value provided for a ``bytes8`` type will raise an error. + - Fixed-size and dynamic ``bytes`` types will accept ``int``s. Any negative + values will be converted to ``0`` before being converted to ``bytes`` + - ``int`` and ``uint`` types will also accept strings. If prefixed with ``"0x"`` + , the string will be interpreted as hex. Otherwise, it will be interpreted as + decimal. + + Noteable differences from ``signTypedData``: + - Custom types that are not alphanumeric will encode differently. + - Custom types that are used but not defined in ``types`` will not encode. + + :param domain_data: EIP712 domain data + :param message_types: custom types used by the `value` data + :param message_data: data to be signed + :param full_message: a dict containing all data and types + :returns: a ``SignableMessage``, an encoded message ready to be signed + + + .. doctest:: python + + >>> # examples of basic usage + >>> from eth_account import Account + >>> from .messages import encode_typed_data + >>> # 3-argument usage + + >>> # all domain properties are optional + >>> domain_data = { + ... "name": "Ether Mail", + ... "version": "1", + ... "chainId": 1, + ... "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC", + ... "salt": b"decafbeef", + ... } + >>> # custom types + >>> message_types = { + ... "Person": [ + ... {"name": "name", "type": "string"}, + ... {"name": "wallet", "type": "address"}, + ... ], + ... "Mail": [ + ... {"name": "from", "type": "Person"}, + ... {"name": "to", "type": "Person"}, + ... {"name": "contents", "type": "string"}, + ... ], + ... } + >>> # the data to be signed + >>> message_data = { + ... "from": { + ... "name": "Cow", + ... "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", + ... }, + ... "to": { + ... "name": "Bob", + ... "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB", + ... }, + ... "contents": "Hello, Bob!", + ... } + >>> key = "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + >>> signable_message = encode_typed_data(domain_data, message_types, message_data) + >>> signed_message = Account.sign_message(signable_message, key) + >>> signed_message.messageHash + HexBytes('0xc5bb16ccc59ae9a3ad1cb8343d4e3351f057c994a97656e1aff8c134e56f7530') + >>> # the message can be signed in one step using Account.sign_typed_data + >>> signed_typed_data = Account.sign_typed_data(key, domain_data, message_types, message_data) + >>> signed_typed_data == signed_message + True + + >>> # 1-argument usage + + >>> # all domain properties are optional + >>> full_message = { + ... "types": { + ... "EIP712Domain": [ + ... {"name": "name", "type": "string"}, + ... {"name": "version", "type": "string"}, + ... {"name": "chainId", "type": "uint256"}, + ... {"name": "verifyingContract", "type": "address"}, + ... {"name": "salt", "type": "bytes32"}, + ... ], + ... "Person": [ + ... {"name": "name", "type": "string"}, + ... {"name": "wallet", "type": "address"}, + ... ], + ... "Mail": [ + ... {"name": "from", "type": "Person"}, + ... {"name": "to", "type": "Person"}, + ... {"name": "contents", "type": "string"}, + ... ], + ... }, + ... "primaryType": "Mail", + ... "domain": { + ... "name": "Ether Mail", + ... "version": "1", + ... "chainId": 1, + ... "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC", + ... "salt": b"decafbeef" + ... }, + ... "message": { + ... "from": { + ... "name": "Cow", + ... "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" + ... }, + ... "to": { + ... "name": "Bob", + ... "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" + ... }, + ... "contents": "Hello, Bob!", + ... }, + ... } + >>> signable_message_2 = encode_typed_data(full_message=full_message) + >>> signed_message_2 = Account.sign_message(signable_message_2, key) + >>> signed_message_2.messageHash + HexBytes('0xc5bb16ccc59ae9a3ad1cb8343d4e3351f057c994a97656e1aff8c134e56f7530') + >>> signed_message_2 == signed_message + True + >>> # the full_message can be signed in one step using Account.sign_typed_data + >>> signed_typed_data_2 = Account.sign_typed_data(key, domain_data, message_types, message_data) + >>> signed_typed_data_2 == signed_message_2 + True + + .. _EIP-712: https://eips.ethereum.org/EIPS/eip-712 + """ # noqa: E501 + if full_message is not None: + if ( + domain_data is not None + or message_types is not None + or message_data is not None + ): + raise ValueError( + "You may supply either `full_message` as a single argument or " + "`domain_data`, `message_types`, and `message_data` as three arguments," + " but not both." + ) + + full_message_types = full_message["types"].copy() + full_message_domain = full_message["domain"].copy() + + # If EIP712Domain types were provided, check that they match the domain data + if "EIP712Domain" in full_message_types: + domain_data_keys = list(full_message_domain.keys()) + domain_types_keys = [ + field["name"] for field in full_message_types["EIP712Domain"] + ] + + if set(domain_data_keys) != (set(domain_types_keys)): + raise ValidationError( + "The fields provided in `domain` do not match the fields provided" + " in `types.EIP712Domain`. The fields provided in `domain` were" + f" `{domain_data_keys}`, but the fields provided in " + f"`types.EIP712Domain` were `{domain_types_keys}`." + ) + + full_message_types.pop("EIP712Domain", None) + + # If primaryType was provided, check that it matches the derived primaryType + if "primaryType" in full_message: + derived_primary_type = get_primary_type(full_message_types) + provided_primary_type = full_message["primaryType"] + if derived_primary_type != provided_primary_type: + raise ValidationError( + "The provided `primaryType` does not match the derived " + "`primaryType`. The provided `primaryType` was " + f"`{provided_primary_type}`, but the derived `primaryType` was " + f"`{derived_primary_type}`." + ) + + parsed_domain_data = full_message_domain + parsed_message_types = full_message_types + parsed_message_data = full_message["message"] + + else: + parsed_domain_data = domain_data + parsed_message_types = message_types + parsed_message_data = message_data + + return SignableMessage( + HexBytes(b"\x01"), + hash_domain(parsed_domain_data), + hash_eip712_message(parsed_message_types, parsed_message_data), + ) diff --git a/ccxt/static_dependencies/ethereum/account/py.typed b/ccxt/static_dependencies/ethereum/account/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/ccxt/static_dependencies/ethereum/hexbytes/__init__.py b/ccxt/static_dependencies/ethereum/hexbytes/__init__.py new file mode 100644 index 0000000..a00261e --- /dev/null +++ b/ccxt/static_dependencies/ethereum/hexbytes/__init__.py @@ -0,0 +1,5 @@ +from .main import ( + HexBytes, +) + +__all__ = ["HexBytes"] diff --git a/ccxt/static_dependencies/ethereum/hexbytes/__pycache__/__init__.cpython-311.pyc b/ccxt/static_dependencies/ethereum/hexbytes/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..ace3e3f Binary files /dev/null and b/ccxt/static_dependencies/ethereum/hexbytes/__pycache__/__init__.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/ethereum/hexbytes/__pycache__/_utils.cpython-311.pyc b/ccxt/static_dependencies/ethereum/hexbytes/__pycache__/_utils.cpython-311.pyc new file mode 100644 index 0000000..cfce961 Binary files /dev/null and b/ccxt/static_dependencies/ethereum/hexbytes/__pycache__/_utils.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/ethereum/hexbytes/__pycache__/main.cpython-311.pyc b/ccxt/static_dependencies/ethereum/hexbytes/__pycache__/main.cpython-311.pyc new file mode 100644 index 0000000..177fb8f Binary files /dev/null and b/ccxt/static_dependencies/ethereum/hexbytes/__pycache__/main.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/ethereum/hexbytes/_utils.py b/ccxt/static_dependencies/ethereum/hexbytes/_utils.py new file mode 100644 index 0000000..5551728 --- /dev/null +++ b/ccxt/static_dependencies/ethereum/hexbytes/_utils.py @@ -0,0 +1,54 @@ +import binascii +from typing import ( + Union, +) + + +def to_bytes(val: Union[bool, bytearray, bytes, int, str, memoryview]) -> bytes: + """ + Equivalent to: `eth_utils.hexstr_if_str(eth_utils.to_bytes, val)` . + + Convert a hex string, integer, or bool, to a bytes representation. + Alternatively, pass through bytes or bytearray as a bytes value. + """ + if isinstance(val, bytes): + return val + elif isinstance(val, str): + return hexstr_to_bytes(val) + elif isinstance(val, bytearray): + return bytes(val) + elif isinstance(val, bool): + return b"\x01" if val else b"\x00" + elif isinstance(val, int): + # Note that this int check must come after the bool check, because + # isinstance(True, int) is True + if val < 0: + raise ValueError(f"Cannot convert negative integer {val} to bytes") + else: + return to_bytes(hex(val)) + elif isinstance(val, memoryview): + return bytes(val) + else: + raise TypeError(f"Cannot convert {val!r} of type {type(val)} to bytes") + + +def hexstr_to_bytes(hexstr: str) -> bytes: + if hexstr.startswith(("0x", "0X")): + non_prefixed_hex = hexstr[2:] + else: + non_prefixed_hex = hexstr + + # if the hex string is odd-length, then left-pad it to an even length + if len(hexstr) % 2: + padded_hex = "0" + non_prefixed_hex + else: + padded_hex = non_prefixed_hex + + try: + ascii_hex = padded_hex.encode("ascii") + except UnicodeDecodeError: + raise ValueError( + f"hex string {padded_hex} may only contain [0-9a-fA-F] characters" + ) + else: + return binascii.unhexlify(ascii_hex) diff --git a/ccxt/static_dependencies/ethereum/hexbytes/main.py b/ccxt/static_dependencies/ethereum/hexbytes/main.py new file mode 100644 index 0000000..1b20b16 --- /dev/null +++ b/ccxt/static_dependencies/ethereum/hexbytes/main.py @@ -0,0 +1,65 @@ +import sys +from typing import ( + TYPE_CHECKING, + Type, + Union, + cast, + overload, +) + +from ._utils import ( + to_bytes, +) + +if TYPE_CHECKING: + from typing import ( + SupportsIndex, + ) + +BytesLike = Union[bool, bytearray, bytes, int, str, memoryview] + + +class HexBytes(bytes): + """ + HexBytes is a *very* thin wrapper around the python built-in :class:`bytes` class. + + It has these three changes: + 1. Accepts more initializing values, like hex strings, non-negative integers, + and booleans + 2. Returns hex with prefix '0x' from :meth:`HexBytes.hex` + 3. The representation at console is in hex + """ + + def __new__(cls: Type[bytes], val: BytesLike) -> "HexBytes": + bytesval = to_bytes(val) + return cast(HexBytes, super().__new__(cls, bytesval)) # type: ignore # https://github.com/python/typeshed/issues/2630 # noqa: E501 + + def hex( + self, sep: Union[str, bytes] = None, bytes_per_sep: "SupportsIndex" = 1 + ) -> str: + """ + Output hex-encoded bytes, with an "0x" prefix. + + Everything following the "0x" is output exactly like :meth:`bytes.hex`. + """ + return "0x" + super().hex() + + @overload + def __getitem__(self, key: "SupportsIndex") -> int: # noqa: F811 + ... + + @overload # noqa: F811 + def __getitem__(self, key: slice) -> "HexBytes": # noqa: F811 + ... + + def __getitem__( # noqa: F811 + self, key: Union["SupportsIndex", slice] + ) -> Union[int, bytes, "HexBytes"]: + result = super().__getitem__(key) + if hasattr(result, "hex"): + return type(self)(result) + else: + return result + + def __repr__(self) -> str: + return f"HexBytes({self.hex()!r})" diff --git a/ccxt/static_dependencies/ethereum/hexbytes/py.typed b/ccxt/static_dependencies/ethereum/hexbytes/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/ccxt/static_dependencies/ethereum/typing/__init__.py b/ccxt/static_dependencies/ethereum/typing/__init__.py new file mode 100644 index 0000000..31d886b --- /dev/null +++ b/ccxt/static_dependencies/ethereum/typing/__init__.py @@ -0,0 +1,63 @@ +from importlib.metadata import ( + version as __version, +) + +from .abi import ( + Decodable, + TypeStr, +) +from .bls import ( + BLSPrivateKey, + BLSPubkey, + BLSSignature, +) +from .discovery import ( + NodeID, +) +from .encoding import ( + HexStr, + Primitives, +) +from .enums import ( + ForkName, +) +from .ethpm import ( + URI, + ContractName, + Manifest, +) +from .evm import ( + Address, + AnyAddress, + BlockIdentifier, + BlockNumber, + ChecksumAddress, + Hash32, + HexAddress, +) +from .networks import ( + ChainId, +) + +__all__ = ( + "Decodable", + "TypeStr", + "BLSPrivateKey", + "BLSPubkey", + "BLSSignature", + "NodeID", + "HexStr", + "Primitives", + "ForkName", + "ChainId", + "URI", + "ContractName", + "Manifest", + "Address", + "AnyAddress", + "BlockIdentifier", + "BlockNumber", + "ChecksumAddress", + "Hash32", + "HexAddress", +) diff --git a/ccxt/static_dependencies/ethereum/typing/__pycache__/__init__.cpython-311.pyc b/ccxt/static_dependencies/ethereum/typing/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..faf7bbc Binary files /dev/null and b/ccxt/static_dependencies/ethereum/typing/__pycache__/__init__.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/ethereum/typing/__pycache__/abi.cpython-311.pyc b/ccxt/static_dependencies/ethereum/typing/__pycache__/abi.cpython-311.pyc new file mode 100644 index 0000000..db359a2 Binary files /dev/null and b/ccxt/static_dependencies/ethereum/typing/__pycache__/abi.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/ethereum/typing/__pycache__/bls.cpython-311.pyc b/ccxt/static_dependencies/ethereum/typing/__pycache__/bls.cpython-311.pyc new file mode 100644 index 0000000..add3664 Binary files /dev/null and b/ccxt/static_dependencies/ethereum/typing/__pycache__/bls.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/ethereum/typing/__pycache__/discovery.cpython-311.pyc b/ccxt/static_dependencies/ethereum/typing/__pycache__/discovery.cpython-311.pyc new file mode 100644 index 0000000..060d50e Binary files /dev/null and b/ccxt/static_dependencies/ethereum/typing/__pycache__/discovery.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/ethereum/typing/__pycache__/encoding.cpython-311.pyc b/ccxt/static_dependencies/ethereum/typing/__pycache__/encoding.cpython-311.pyc new file mode 100644 index 0000000..fbf0390 Binary files /dev/null and b/ccxt/static_dependencies/ethereum/typing/__pycache__/encoding.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/ethereum/typing/__pycache__/enums.cpython-311.pyc b/ccxt/static_dependencies/ethereum/typing/__pycache__/enums.cpython-311.pyc new file mode 100644 index 0000000..65eff91 Binary files /dev/null and b/ccxt/static_dependencies/ethereum/typing/__pycache__/enums.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/ethereum/typing/__pycache__/ethpm.cpython-311.pyc b/ccxt/static_dependencies/ethereum/typing/__pycache__/ethpm.cpython-311.pyc new file mode 100644 index 0000000..6e6b5e3 Binary files /dev/null and b/ccxt/static_dependencies/ethereum/typing/__pycache__/ethpm.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/ethereum/typing/__pycache__/evm.cpython-311.pyc b/ccxt/static_dependencies/ethereum/typing/__pycache__/evm.cpython-311.pyc new file mode 100644 index 0000000..5a54966 Binary files /dev/null and b/ccxt/static_dependencies/ethereum/typing/__pycache__/evm.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/ethereum/typing/__pycache__/networks.cpython-311.pyc b/ccxt/static_dependencies/ethereum/typing/__pycache__/networks.cpython-311.pyc new file mode 100644 index 0000000..ccc4a1d Binary files /dev/null and b/ccxt/static_dependencies/ethereum/typing/__pycache__/networks.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/ethereum/typing/abi.py b/ccxt/static_dependencies/ethereum/typing/abi.py new file mode 100644 index 0000000..0ac4251 --- /dev/null +++ b/ccxt/static_dependencies/ethereum/typing/abi.py @@ -0,0 +1,6 @@ +from typing import ( + Union, +) + +TypeStr = str +Decodable = Union[bytes, bytearray] diff --git a/ccxt/static_dependencies/ethereum/typing/bls.py b/ccxt/static_dependencies/ethereum/typing/bls.py new file mode 100644 index 0000000..47b3bb8 --- /dev/null +++ b/ccxt/static_dependencies/ethereum/typing/bls.py @@ -0,0 +1,7 @@ +from typing import ( + NewType, +) + +BLSPubkey = NewType("BLSPubkey", bytes) # bytes48 +BLSPrivateKey = NewType("BLSPrivateKey", int) +BLSSignature = NewType("BLSSignature", bytes) # bytes96 diff --git a/ccxt/static_dependencies/ethereum/typing/discovery.py b/ccxt/static_dependencies/ethereum/typing/discovery.py new file mode 100644 index 0000000..80bfe21 --- /dev/null +++ b/ccxt/static_dependencies/ethereum/typing/discovery.py @@ -0,0 +1,5 @@ +from typing import ( + NewType, +) + +NodeID = NewType("NodeID", bytes) diff --git a/ccxt/static_dependencies/ethereum/typing/encoding.py b/ccxt/static_dependencies/ethereum/typing/encoding.py new file mode 100644 index 0000000..0058d91 --- /dev/null +++ b/ccxt/static_dependencies/ethereum/typing/encoding.py @@ -0,0 +1,7 @@ +from typing import ( + NewType, + Union, +) + +HexStr = NewType("HexStr", str) +Primitives = Union[bytes, int, bool] diff --git a/ccxt/static_dependencies/ethereum/typing/enums.py b/ccxt/static_dependencies/ethereum/typing/enums.py new file mode 100644 index 0000000..e01ea50 --- /dev/null +++ b/ccxt/static_dependencies/ethereum/typing/enums.py @@ -0,0 +1,17 @@ +class ForkName: + Frontier = "Frontier" + Homestead = "Homestead" + EIP150 = "EIP150" + EIP158 = "EIP158" + Byzantium = "Byzantium" + Constantinople = "Constantinople" + Metropolis = "Metropolis" + ConstantinopleFix = "ConstantinopleFix" + Istanbul = "Istanbul" + Berlin = "Berlin" + London = "London" + ArrowGlacier = "ArrowGlacier" + GrayGlacier = "GrayGlacier" + Paris = "Paris" + Shanghai = "Shanghai" + Cancun = "Cancun" diff --git a/ccxt/static_dependencies/ethereum/typing/ethpm.py b/ccxt/static_dependencies/ethereum/typing/ethpm.py new file mode 100644 index 0000000..bf31906 --- /dev/null +++ b/ccxt/static_dependencies/ethereum/typing/ethpm.py @@ -0,0 +1,9 @@ +from typing import ( + Any, + Dict, + NewType, +) + +ContractName = NewType("ContractName", str) +Manifest = NewType("Manifest", Dict[str, Any]) +URI = NewType("URI", str) diff --git a/ccxt/static_dependencies/ethereum/typing/evm.py b/ccxt/static_dependencies/ethereum/typing/evm.py new file mode 100644 index 0000000..1bf7040 --- /dev/null +++ b/ccxt/static_dependencies/ethereum/typing/evm.py @@ -0,0 +1,20 @@ +from typing import ( + Literal, + NewType, + TypeVar, + Union, +) + +from .encoding import ( + HexStr, +) + +Hash32 = NewType("Hash32", bytes) +BlockNumber = NewType("BlockNumber", int) +BlockParams = Literal["latest", "earliest", "pending", "safe", "finalized"] +BlockIdentifier = Union[BlockParams, BlockNumber, Hash32, HexStr, int] + +Address = NewType("Address", bytes) +HexAddress = NewType("HexAddress", HexStr) +ChecksumAddress = NewType("ChecksumAddress", HexAddress) +AnyAddress = TypeVar("AnyAddress", Address, HexAddress, ChecksumAddress) diff --git a/ccxt/static_dependencies/ethereum/typing/networks.py b/ccxt/static_dependencies/ethereum/typing/networks.py new file mode 100644 index 0000000..c1c9f05 --- /dev/null +++ b/ccxt/static_dependencies/ethereum/typing/networks.py @@ -0,0 +1,1122 @@ +from enum import ( + IntEnum, +) + + +class ChainId(IntEnum): + ETH = 1 + EXP = 2 + ROP = 3 + RIN = 4 + GOR = 5 + KOT = 6 + TCH = 7 + UBQ = 8 + TUBQ = 9 + OETH = 10 + META = 11 + KAL = 12 + DSTG = 13 + FLR = 14 + DIODE = 15 + CFLR = 16 + TFI = 17 + TST = 18 + SGB = 19 + ESC = 20 + ESCT = 21 + ELADID = 22 + ELADIDT = 23 + KARDIACHAIN = 24 + CRO = 25 + L1TEST = 26 + SHIB = 27 + BOBARINKEBY = 28 + L1 = 29 + RSK = 30 + TRSK = 31 + GOODT = 32 + GOOD = 33 + SCAI = 34 + TBWG = 35 + DX = 36 + XPLA = 37 + VAL = 38 + U2U = 39 + TELOSEVM = 40 + TELOSEVMTESTNET = 41 + LUKSO = 42 + PANGOLIN = 43 + CRAB = 44 + PANGORO = 45 + DARWINIA = 46 + AIC = 47 + ETMP = 48 + ETMPTEST = 49 + XDC = 50 + TXDC = 51 + CET = 52 + TCET = 53 + OP = 54 + ZYX = 55 + BNB = 56 + SYS = 57 + ONTOLOGYMAINNET = 58 + EOS_LEGACY = 59 + GO = 60 + ETC = 61 + TETC = 62 + METC = 63 + ELLAISM = 64 + TOKT = 65 + OKT = 66 + DBM = 67 + SO1 = 68 + OKOV = 69 + HSC = 70 + CFXTEST = 71 + DXC = 72 + FNCY = 73 + IDCHAIN = 74 + DSC = 75 + MIX = 76 + SPOA = 77 + PRIMUSCHAIN = 78 + ZENITH = 79 + GENECHAIN = 80 + JOC = 81 + METER = 82 + METERTEST = 83 + LINQTO_DEVNET = 84 + GTTEST = 85 + GT = 86 + NNW = 87 + TOMO = 88 + TOMOT = 89 + GAR_S0 = 90 + GAR_S1 = 91 + GAR_S2 = 92 + GAR_S3 = 93 + SDLT = 94 + CAMDL = 95 + BKC = 96 + BNBT = 97 + SIX = 98 + POA = 99 + GNO = 100 + ETI = 101 + TW3G = 102 + WLC = 103 + TKLC = 104 + DW3G = 105 + VLX = 106 + NTN = 107 + TT = 108 + SHIBARIUMECOSYSTEM = 109 + XPR = 110 + ETL = 111 + COINBIT = 112 + DEH = 113 + C2FLR = 114 + DEBANK_TESTNET = 115 + DEBANK_MAINNET = 116 + AUPTICK = 117 + ARCOLOGY = 118 + ENULS = 119 + ENULST = 120 + REAL = 121 + FUSE = 122 + SPARK = 123 + DWU = 124 + OYCHAINTESTNET = 125 + OYCHAINMAINNET = 126 + FETH = 127 + HECO = 128 + RLC = 134 + ALYXTESTNET = 135 + DEAM = 136 + MATIC = 137 + DFIO_META_MAIN = 138 + WOOP = 139 + OPTEST = 141 + DAX = 142 + PHI = 144 + SHIMMEREVM_MAINNET = 148 + SIXT = 150 + RBN = 151 + RBN_DEVNET = 152 + RBN_TESTNET = 153 + RBN_TGE = 154 + TENET_TESTNET = 155 + OBE = 156 + EVA = 160 + WALL_E = 161 + TPHT = 162 + PHT = 163 + OMNI_TESTNET = 165 + ATOSHI = 167 + AIOZ = 168 + MANTA = 169 + HOOSMARTCHAIN = 170 + RESIL = 172 + AME = 180 + SEELE = 186 + BMC = 188 + BMCT = 189 + CEM = 193 + TOKB = 195 + OKB = 196 + NEUTR = 197 + BIT = 198 + BTT = 199 + AOX = 200 + MOACTEST = 201 + OBNB = 204 + VCTEST = 206 + VC = 207 + UTX = 208 + BTN = 210 + EDI = 211 + MAKALU = 212 + SHINARIUM = 214 + SIN2 = 217 + SO1_OLD = 218 + ASK = 222 + LA = 225 + TLA = 226 + SDX = 230 + DEAMTEST = 236 + PLGCHAIN = 242 + EWT = 246 + OAS = 248 + FTM = 250 + KROMA = 255 + HECOT = 256 + SETM = 258 + NEON = 259 + SUR = 262 + HPB = 269 + EGONM = 271 + LACHAIN = 274 + ZKSYNC_GOERLI = 280 + BOBA = 288 + ORDERLY = 291 + HEDERA_MAINNET = 295 + HEDERA_TESTNET = 296 + HEDERA_PREVIEWNET = 297 + HEDERA_LOCALNET = 298 + OGC = 300 + BOBAOPERA = 301 + NCNT = 303 + WYZ = 309 + OMAX = 311 + NCN = 313 + FILECOIN = 314 + KCS = 321 + KCST = 322 + ZKSYNC = 324 + W3Q = 333 + DFKTEST = 335 + SDN = 336 + TCRO = 338 + YVM = 345 + THETA_MAINNET = 361 + THETA_SAPPHIRE = 363 + THETA_AMBER = 364 + THETA_TESTNET = 365 + PLS = 369 + TCNT = 371 + LISINSKI = 385 + N3 = 399 + HPN = 400 + OZO_TST = 401 + PEPE = 411 + SX = 416 + LATESTNET = 418 + OGOR = 420 + PGN = 424 + ZEETH = 427 + OBS_TESTNET = 443 + SYNAPSE_SEPOLIA = 444 + ARZIO = 456 + TAREA = 462 + RUPX = 499 + CAMINO = 500 + COLUMBUS = 501 + AAC = 512 + AACT = 513 + GZ_MAINNET = 516 + XT = 520 + FIRE = 529 + FXCORE = 530 + CNDL = 534 + PAW = 542 + CLASS = 555 + TAO = 558 + DCT = 568 + SYS_ROLLUX = 570 + METIS_STARDUST = 588 + ASTR = 592 + MACA = 595 + TKAR = 596 + TACA = 597 + METIS_GOERLI = 599 + MESH_CHAIN_TESTNET = 600 + PEER = 601 + GLQ = 614 + AVOCADO = 634 + SX_TESTNET = 647 + ACE = 648 + PIXIE_CHAIN_TESTNET = 666 + LAOS = 667 + JUNCA = 668 + JUNCAT = 669 + KAR = 686 + SNS = 700 + BCS = 707 + TBCS = 708 + FURY = 710 + SHIBARIUM = 719 + LYC = 721 + TCANTO = 740 + VSCT = 741 + SPAY = 742 + QOM = 766 + OPC = 776 + CTH = 777 + MAAL = 786 + ACA = 787 + TAERO = 788 + PETH = 789 + LUCID = 800 + HAIC = 803 + PFTEST = 808 + MEER = 813 + BOC = 818 + CLO = 820 + TCLO = 821 + TARA = 841 + TARATEST = 842 + ZEETHDEV = 859 + FSCMAINNET = 868 + BNKEN = 876 + DXT = 877 + AMBROS = 880 + WAN = 888 + GAR_TEST_S0 = 900 + GAR_TEST_S1 = 901 + GAR_TEST_S2 = 902 + GAR_TEST_S3 = 903 + PF = 909 + DBONE = 910 + TFIRE = 917 + MODESEP = 919 + YDK = 927 + TPLS = 940 + T2BPLS = 941 + T3PLS = 942 + T4PLS = 943 + MUNODE = 956 + BTC20 = 963 + CCN = 970 + HUYGENS = 971 + ASCRAEUS = 972 + YETI = 977 + TOP_EVM = 980 + MEMOCHAIN = 985 + TOP = 989 + ELM = 990 + _5IRE = 997 + LN = 998 + TWAN = 999 + GTON = 1000 + BAOBAB = 1001 + TET = 1003 + T_EKTA = 1004 + TNEW = 1007 + EUN = 1008 + EVC = 1010 + NEW = 1012 + SKU = 1022 + TCLV = 1023 + CLV = 1024 + TBTT = 1028 + CFX = 1030 + PRX = 1031 + BRONOS_TESTNET = 1038 + BRONOS_MAINNET = 1039 + SHIMMEREVM_TESTNET_DEPRECATED = 1071 + SHIMMEREVM_TESTNET = 1072 + MINTARA_TESTNET = 1079 + MINTARA = 1080 + METIS_ANDROMEDA = 1088 + HUMANS = 1089 + MOAC = 1099 + ZKEVM = 1101 + TBLXQ = 1107 + BLXQ = 1108 + WEMIX = 1111 + TWEMIX = 1112 + TCORE = 1115 + CORE = 1116 + DOGSM = 1117 + DFI = 1130 + DFI_T = 1131 + CHANGI = 1133 + ASART = 1138 + MATH = 1139 + TMATH = 1140 + PLEXCHAIN = 1149 + AUOC = 1170 + SHT = 1177 + IORA = 1197 + AVIS = 1201 + WTT = 1202 + POPCAT = 1213 + ENTER = 1214 + XZO = 1229 + ULTRONTESTNET = 1230 + UTRONMAINNET = 1231 + STEP = 1234 + ARC = 1243 + TARC = 1244 + OM = 1246 + CICT = 1252 + HO = 1280 + MBEAM = 1284 + MRIVER = 1285 + MROCK_OLD = 1286 + MBASE = 1287 + MROCK = 1288 + SWTR = 1291 + BOBABEAM = 1294 + BOBABASE = 1297 + TDOS = 1311 + ALYX = 1314 + AIA = 1319 + AIATESTNET = 1320 + GETH = 1337 + ELST = 1338 + ELSM = 1339 + CIC = 1353 + ZAFIC = 1369 + KLC = 1379 + ASAR = 1388 + MUN = 1392 + ZKEVMTEST = 1402 + TESTNET_ZKEVM_MANGO_PRE_AUDIT_UPGRADED = 1422 + RIK = 1433 + LAS = 1440 + TESTNET_ZKEVM_MANGO = 1442 + GIL = 1452 + CTEX = 1455 + CHAINX = 1501 + SHERPAX = 1506 + SHERPAXTESTNET = 1507 + BEAGLE = 1515 + TENET = 1559 + CATE = 1618 + ATH = 1620 + BTA = 1657 + YUMA = 1662 + GOBI = 1663 + LUDAN = 1688 + ANYTYPECHAIN = 1701 + TBSI = 1707 + TTBSI = 1708 + PCM = 1718 + TEAPARTY = 1773 + GAUSS = 1777 + KERLEANO = 1804 + RANA = 1807 + CUBE = 1818 + CUBET = 1819 + TSF = 1856 + WBT = 1875 + GITSHOCKCHAIN = 1881 + LIGHTLINK_PHOENIX = 1890 + LIGHTLINK_PEGASUS = 1891 + BOYA = 1898 + SCN = 1904 + BITCI = 1907 + TBITCI = 1908 + ONUS_TESTNET = 1945 + DCHAIN_MAINNET = 1951 + DEXILLA = 1954 + MTC = 1967 + TSCS = 1969 + SCS = 1970 + ATLR = 1971 + ONUS_MAINNET = 1975 + EUNTEST = 1984 + SATOSHIE = 1985 + SATOSHIE_TESTNET = 1986 + EGEM = 1987 + EKTA = 1994 + EDX = 1995 + DC = 2000 + MILKADA = 2001 + MILKALGO = 2002 + CLOUDWALK_TESTNET = 2008 + CLOUDWALK_MAINNET = 2009 + NETZM = 2016 + PMINT_DEV = 2018 + PMINT_TEST = 2019 + PMINT = 2020 + EDG = 2021 + EDGT = 2022 + TAYCAN_TESTNET = 2023 + RPG = 2025 + CFG = 2031 + NCFG = 2032 + KIWI = 2037 + SHRAPTEST = 2038 + OTP = 2043 + SHRAPNEL = 2044 + STOS_TESTNET = 2047 + STOS_MAINNET = 2048 + MOVO = 2049 + QKA = 2077 + AIR = 2088 + ALGL = 2089 + ECO = 2100 + ESP = 2101 + EXN = 2109 + METAD = 2122 + MEU = 2124 + BIGSB = 2137 + DFIO_META_TEST = 2138 + BOA = 2151 + FRA = 2152 + FINDORA_TESTNET = 2153 + FINDORA_FORGE = 2154 + MSN = 2199 + ABNM = 2202 + BTC = 2203 + EVANESCO = 2213 + TKAVA = 2221 + KAVA = 2222 + VCHAIN = 2223 + KRST = 2241 + BOMB = 2300 + AREVIA = 2309 + SMA = 2323 + ALT = 2330 + SMAM = 2332 + DEPRECATED_KROMA_SEPOLIA = 2357 + KROMA_SEPOLIA = 2358 + BOMBT = 2399 + TCGV = 2400 + XODEX = 2415 + U2U_NEBULAS = 2484 + KTOC = 2559 + TPC = 2569 + POCRNET = 2606 + REDLC = 2611 + EZCHAIN = 2612 + FUJI_EZCHAIN = 2613 + TWBT = 2625 + TMORPH = 2710 + BOBAGOERLI = 2888 + BTY = 2999 + CENNZ_R = 3000 + CENNZ_N = 3001 + CAU = 3003 + _3ULL = 3011 + ORL = 3031 + BFC = 3068 + IMMU3 = 3100 + VFI = 3102 + FILECOIN_HYPERSPACE = 3141 + DUBX = 3269 + TESTDUBX = 3270 + DEBOUNCE_DEVNET = 3306 + ZCRBEACH = 3331 + W3Q_T = 3333 + W3Q_G = 3334 + PRB = 3400 + SCAIT = 3434 + PRBTESTNET = 3500 + JFIN = 3501 + PANDO_MAINNET = 3601 + PANDO_TESTNET = 3602 + BTNX = 3636 + BTCM = 3637 + ISLAMI = 3639 + JOULEVERSE = 3666 + BTX = 3690 + EMPIRE = 3693 + SPCT = 3698 + SPCM = 3699 + XPLATEST = 3701 + CSB = 3737 + ALV = 3797 + KALYMAINNET = 3888 + KALYTESTNET = 3889 + DRAC = 3912 + DOST = 3939 + DYNO = 3966 + TDYNO = 3967 + YCC = 3999 + OZO = 4000 + PERIUM = 4001 + TFTM = 4002 + BOBAOPERATESTNET = 4051 + NAHMII3MAINNET = 4061 + NAHMII3TESTNET = 4062 + OASIS = 4090 + BNIT = 4096 + BNIM = 4099 + AIOZ_TESTNET = 4102 + HUMANS_TESTNET = 4139 + TPBXT = 4141 + PHIV1 = 4181 + LUKSO_TESTNET = 4201 + NEXI = 4242 + BOBAFUJITESTNET = 4328 + BEAM = 4337 + HTML = 4444 + ORDERLYL2 = 4460 + IOTEX_MAINNET = 4689 + IOTEX_TESTNET = 4690 + TESTMEV = 4759 + TBXN = 4777 + TXVM = 4918 + XVM = 4919 + BXN = 4999 + MANTLE = 5000 + MANTLE_TESTNET = 5001 + TREASURENET = 5002 + MNT_SEP = 5003 + TNTEST = 5005 + FTN = 5165 + TLC = 5177 + ES = 5197 + HMND = 5234 + _OLD_FIRE = 5290 + UZMI = 5315 + TTRN = 5353 + VEX = 5522 + NAHMII = 5551 + NAHMIITESTNET = 5553 + CVERSE = 5555 + OBNBT = 5611 + ARCT = 5616 + TANSSICC = 5678 + TSYS = 5700 + HIK = 5729 + SATST = 5758 + GGUI = 5777 + ONTOLOGYTESTNET = 5851 + RBD = 5869 + TRESTEST = 6065 + TRESMAIN = 6066 + CASCADIA = 6102 + UPTN_TEST = 6118 + UPTN = 6119 + PEERPAY = 6502 + SRC_TEST = 6552 + FOX = 6565 + PIXIE_CHAIN = 6626 + IRIS = 6688 + STANDM = 6789 + TOMBCHAIN = 6969 + PSC = 6999 + ZETACHAIN_MAINNET = 7000 + ZETACHAIN_ATHENS = 7001 + ELLA = 7027 + PLANQ = 7070 + BITROCK = 7171 + KLY = 7331 + EON = 7332 + SHYFT = 7341 + RABA = 7484 + MEV = 7518 + TADIL = 7575 + ADIL = 7576 + TRN_MAINNET = 7668 + TRN_PORCINI = 7672 + CANTO = 7700 + TESTNETCANTO = 7701 + TBITROCK = 7771 + RISEOFTHEWARBOTSTESTNET = 7777 + TSCAS = 7878 + ARD = 7895 + DOS = 7979 + TELEPORT = 8000 + TELEPORT_TESTNET = 8001 + MDGL = 8029 + LIBERTY10 = 8080 + LIBERTY20 = 8081 + SPHINX10 = 8082 + BITETH = 8086 + STREAMUX = 8098 + MEERTEST = 8131 + MEERMIX = 8132 + MEERPRIV = 8133 + AMANA = 8134 + FLANA = 8135 + MIZANA = 8136 + TBOC = 8181 + TTQF = 8194 + CYPRESS = 8217 + BTON = 8272 + KORTHO = 8285 + FUCK = 8387 + BASE = 8453 + TOKI = 8654 + TOKI_TESTNET = 8655 + HELA = 8668 + OLO = 8723 + TOLO = 8724 + ALPH = 8738 + TMY = 8768 + MARO = 8848 + UNQ = 8880 + QTZ = 8881 + OPL = 8882 + SPH = 8883 + XANACHAIN = 8888 + VSC = 8889 + MMT = 8898 + JBC = 8899 + GMMT = 8989 + BERG = 8995 + EVMOS_TESTNET = 9000 + EVMOS = 9001 + BRB = 9012 + GENEC = 9100 + _OLD_TFIRE = 9170 + COF = 9223 + DOGST = 9339 + TRPG = 9527 + QETTEST = 9528 + TESTNEON = 9559 + MAINNETDEV = 9700 + BOBABNBTESTNET = 9728 + NETZT = 9768 + PN = 9779 + CARBON = 9790 + CARBON_TESTNET = 9792 + TIMP = 9818 + IMP = 9819 + TMIND = 9977 + AGNG = 9990 + MIND = 9996 + ALT_TESTNET = 9997 + MYN = 9999 + SMARTBCH = 10000 + SMARTBCHTEST = 10001 + GON = 10024 + JOCT = 10081 + SJ = 10086 + GEN = 10101 + CHI = 10200 + PWR = 10201 + AA = 10243 + _0XT = 10248 + TWLC = 10395 + JADE = 10507 + SNOW = 10508 + CCP = 10823 + QUADRANS = 10946 + QUADRANSTESTNET = 10947 + ASTRA = 11110 + WAGMI = 11111 + ASTRA_TESTNET = 11115 + HBIT = 11119 + ISLM = 11235 + SHYFTT = 11437 + SRDXT = 11612 + SAN = 11888 + ARIANEE = 11891 + SATS = 12009 + TZERO = 12051 + ZERO = 12052 + BRC = 12123 + FIBO = 12306 + BLGCHAIN = 12321 + STEPTEST = 12345 + ASTRZK = 12611 + TRIK = 12715 + TQNET = 12890 + SPS = 13000 + CREDIT = 13308 + BEAM_TESTNET = 13337 + PHOENIX = 13381 + SUS = 13812 + SPS_TEST = 14000 + HMND_T5 = 14853 + LOOP = 15551 + TRUSTTESTNET = 15555 + EOS_TESTNET = 15557 + MTT = 16000 + MTTTEST = 16001 + GENESYS = 16507 + NYANCAT = 16688 + AIRDAO = 16718 + TIVAR = 16888 + HOLESKY = 17000 + G8CM = 17171 + PCT = 17180 + EOS = 17777 + ZKST = 18000 + STN = 18122 + POM = 18159 + G8CT = 18181 + MXCZKEVM = 18686 + HMV = 19011 + BTCIX = 19845 + CAMELARK = 20001 + CLOTESTNET = 20729 + P12 = 20736 + CENNZ_A = 21337 + OMC = 21816 + SFL = 22023 + AIRDAO_TEST = 22040 + NAUTCHAIN = 22222 + MAP = 22776 + ABNT = 23006 + OPSIDE = 23118 + SAPPHIRE = 23294 + SAPPHIRE_TESTNET = 23295 + WEB = 24484 + MINTME = 24734 + GOLDT = 25888 + BKCT = 25925 + FRM = 26026 + HTZ = 26600 + OAC = 26863 + OBGOR = 28528 + MCHV = 29548 + PIECE = 30067 + CERI = 30103 + ESN = 31102 + CLDTX = 31223 + CLD = 31224 + GOT = 31337 + FILECOIN_WALLABY = 31415 + BRISE = 32520 + FSN = 32659 + ZIL = 32769 + ZIL_ISOLATED_SERVER = 32990 + ZIL_TESTNET = 33101 + AVS = 33333 + ZIL_DEVNET = 33385 + ZQ2_DEVNET = 33469 + J2O = 35011 + Q = 35441 + Q_TESTNET = 35443 + CMRPG = 38400 + TTRPG = 38401 + NRG = 39797 + OHO = 39815 + OX_BETA = 41500 + PC = 42069 + ARB1 = 42161 + ARB_NOVA = 42170 + CELO = 42220 + EMERALD_TESTNET = 42261 + EMERALD = 42262 + GST = 42801 + KETH = 42888 + AVAETH = 43110 + FUJI = 43113 + AVAX = 43114 + BOBAAVAX = 43288 + FREN = 44444 + ALFA = 44787 + AUTOBAHNNETWORK = 45000 + TFSN = 46688 + REI = 47805 + FLORIPA = 49049 + TBFC = 49088 + TNRG = 49797 + LOE = 50001 + TGTON = 50021 + LUMOZ_TESTNET = 51178 + SRDXM = 51712 + ETN_MAINNET = 52014 + DFK = 53935 + ISLMT = 54211 + TORONETTESTNET = 54321 + TETH = 55004 + REICHAIN = 55555 + TREI = 55556 + BOBABNB = 56288 + VELO = 56789 + TSYS_ROLLUX = 57000 + SEPPGN = 58008 + LINEA_TESTNET = 59140 + LINEA = 59144 + TKM_TEST0 = 60000 + TKM_TEST1 = 60001 + TKM_TEST2 = 60002 + TKM_TEST103 = 60103 + AIUM_DEV = 61800 + ETICA = 61803 + DOKEN = 61916 + BKLV = 62320 + MTV = 62621 + ECS = 63000 + ECS_TESTNET = 63001 + SRC = 65450 + MCL = 67390 + COSMIC = 67588 + DM2 = 68770 + CNDR = 69420 + TKM0 = 70000 + TKM1 = 70001 + TKM2 = 70002 + TKM103 = 70103 + GUAPX = 71111 + CKB = 71393 + GW_TESTNET_V1 = 71401 + GW_MAINNET_V1 = 71402 + VT = 73799 + MVM = 73927 + RESIN = 75000 + FNC = 77238 + VSCM = 77612 + TORONET = 77777 + FIRENZE = 78110 + DFLY = 78281 + AMPLIFY = 78430 + BULLETIN = 78431 + CONDUIT = 78432 + STANDT = 79879 + MATICMUM = 80001 + AMANATEST = 81341 + AMANAMIX = 81342 + AMANAPRIV = 81343 + FLANATEST = 81351 + FLANAMIX = 81352 + FLANAPRIV = 81353 + MIZANATEST = 81361 + MIZANAMIX = 81362 + MIZANAPRIV = 81363 + QNET = 81720 + BASEGOR = 84531 + BASESEP = 84532 + AERIE = 84886 + CYBER = 85449 + NAUTTEST = 88002 + CHZ = 88880 + IVAR = 88888 + BVHL = 90210 + NAUT = 91002 + LAMBDA_TESTNET = 92001 + MANTIS = 96970 + BOBABNBOLD = 97288 + ELT = 99099 + USCTEST = 99998 + USC = 99999 + QKC_R = 100000 + QKC_S0 = 100001 + QKC_S1 = 100002 + QKC_S2 = 100003 + QKC_S3 = 100004 + QKC_S4 = 100005 + QKC_S5 = 100006 + QKC_S6 = 100007 + QKC_S7 = 100008 + VECHAIN = 100009 + VECHAIN_TESTNET = 100010 + CHI1 = 100100 + SVRNT = 101010 + CRFI = 103090 + BRO = 108801 + QKC_D_R = 110000 + QKC_D_S0 = 110001 + QKC_D_S1 = 110002 + QKC_D_S2 = 110003 + QKC_D_S3 = 110004 + QKC_D_S4 = 110005 + QKC_D_S5 = 110006 + QKC_D_S6 = 110007 + QKC_D_S7 = 110008 + TESTSBR = 111000 + SBR = 111111 + METAO = 112358 + DADIL = 123456 + ETND = 131419 + ICPLAZA = 142857 + TAIKO_A2 = 167004 + TAIKO_L2 = 167005 + TAIKO_L3 = 167006 + TKO_JOLNIR = 167007 + BDCC = 188710 + CONDOR = 188881 + MILKTADA = 200101 + MILKTALGO = 200202 + AKA = 200625 + ALAYA = 201018 + ALAYADEV = 201030 + MYTH = 201804 + TDSC = 202020 + TWL_JELLIE = 202624 + PLATON = 210425 + MAS = 220315 + REAP = 221230 + REAP_TESTNET = 221231 + TAFECO = 224168 + HSKTEST = 230315 + HYM = 234666 + ATS = 246529 + ATSTAU = 246785 + SAAKURU_TESTNET = 247253 + CMP_MAINNET = 256256 + GZ_TESTNET = 266256 + EGONT = 271271 + SOCHAIN = 281121 + FILECOIN_CALIBRATION = 314159 + TC = 330844 + AVST = 333331 + N3_TEST = 333333 + OONETEST = 333666 + OONEDEV = 333777 + SPARTA = 333888 + OLYMPUS = 333999 + BITFINITY = 355113 + HAP_TESTNET = 373737 + METAL = 381931 + TAHOE = 381932 + TPBXM = 404040 + KEK = 420420 + TKEK = 420666 + ALTERIUM = 420692 + ARB_RINKEBY = 421611 + ARB_GOERLI = 421613 + ARB_SEP = 421614 + FASTEXTESTNET = 424242 + MARKR_GO = 431140 + DEXALOT_TESTNET = 432201 + DEXALOT = 432204 + WLKT = 444900 + PSEP = 471100 + OC = 474142 + CMP = 512512 + ETHF = 513100 + SCR_SEPOLIA = 534351 + SCR = 534352 + SCR_ALPHA = 534353 + SCR_PREALPHA = 534354 + SHI = 534849 + BESC = 535037 + RTH = 622277 + BRNKC = 641230 + ALL = 651940 + VPIONEER = 666666 + HELA_TESTNET = 666888 + BRNKCTEST = 751230 + MIEXS = 761412 + MDLRM = 776877 + OCTA = 800001 + CURVEM = 827431 + BLOQS4GOOD = 846000 + DODAO = 855456 + VISION = 888888 + PSC_S0 = 900000 + PSC_T_S0 = 910000 + PSC_D_S0 = 920000 + PSC_D_S1 = 920001 + TFNCY = 923018 + ELV = 955305 + ECROX = 988207 + AZKTN = 1261120 + ETHO = 1313114 + XERO = 1313500 + KINTSUGI = 1337702 + KILN = 1337802 + ZHEJIANG = 1337803 + DBK = 2021398 + PLIAN_MAINNET = 2099156 + PLATONDEV = 2203181 + PLATONDEV2 = 2206132 + DPU = 2611555 + FILECOIN_BUTTERFLY = 3141592 + MANTATESTNET = 3441005 + ALT_ZEROGAS = 4000003 + WORLDSCAL = 4281033 + MXC = 5167003 + ETN_TESTNET = 5201420 + IMVERSED = 5555555 + IMVERSED_TESTNET = 5555558 + SAAKURU = 7225878 + VSL = 7355310 + TQOM = 7668378 + MUSIC = 7762959 + ZORA = 7777777 + PLIAN_MAINNET_L2 = 8007736 + HAP = 8794598 + QUARIX_TESTNET = 8888881 + QUARIX = 8888888 + PLIAN_TESTNET_L2 = 10067275 + SVRNM = 10101010 + SEP = 11155111 + TPEP = 13371337 + ANDUSCHAIN_MAINNET = 14288640 + PLIAN_TESTNET = 16658437 + ILT = 18289463 + SPECTRUM = 20180430 + QKI = 20181205 + PG = 20201022 + XLON = 22052002 + EXLVOLTA = 27082017 + EXL = 27082022 + AUXI = 28945486 + FLA = 29032022 + FILECOIN_LOCAL = 31415926 + JOYS = 35855456 + MAIS = 43214913 + AQUA = 61717561 + BAKERLOO_0 = 65010000 + PICCADILLY_0 = 65100000 + FRAMETEST = 68840142 + TEAM = 88888888 + TOYS = 99415706 + GTH = 192837465 + KANAZAWA = 222000222 + NEONEVM_DEVNET = 245022926 + NEONEVM_MAINNET = 245022934 + NEONEVM_TESTNET = 245022940 + RAZOR = 278611351 + ONELEDGER = 311752642 + MELD = 333000333 + CALYPSO_TESTNET = 344106930 + TGTH = 356256156 + DGTH = 486217935 + NEBULA_STAGING = 503129905 + ZSEP = 999999999 + IPOS = 1122334455 + CYB = 1146703430 + HUMAN_MAINNET = 1273227453 + AURORA = 1313161554 + AURORA_TESTNET = 1313161555 + AURORA_BETANET = 1313161556 + CHAOS_TENET = 1351057110 + RPTR = 1380996178 + NEBULA_MAINNET = 1482601649 + CALYPSO_MAINNET = 1564830818 + HMY_S0 = 1666600000 + HMY_S1 = 1666600001 + HMY_S2 = 1666600002 + HMY_S3 = 1666600003 + HMY_B_S0 = 1666700000 + HMY_B_S1 = 1666700001 + HMY_PS_S0 = 1666900000 + HMY_PS_S1 = 1666900001 + HOP = 2021121117 + EUROPA = 2046399126 + A8 = 2863311531 + PIRL = 3125659152 + FRANKENSTEIN = 4216137055 + TPALM = 11297108099 + PALM = 11297108109 + ALPHABET = 111222333444 + NTT = 197710212030 + NTT_HARADEV = 197710212031 + ZENIQ = 383414847825 + IPDC = 666301171999 + MOLE = 6022140761023 + GW_TESTNET_V1_DEPRECATED = 868455272153094 diff --git a/ccxt/static_dependencies/ethereum/typing/py.typed b/ccxt/static_dependencies/ethereum/typing/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/ccxt/static_dependencies/ethereum/utils/__init__.py b/ccxt/static_dependencies/ethereum/utils/__init__.py new file mode 100644 index 0000000..647141c --- /dev/null +++ b/ccxt/static_dependencies/ethereum/utils/__init__.py @@ -0,0 +1,115 @@ +from importlib.metadata import ( + version as __version, +) + +# from .abi import ( +# event_abi_to_log_topic, +# event_signature_to_log_topic, +# function_abi_to_4byte_selector, +# function_signature_to_4byte_selector, +# ) +from .address import ( + is_address, + is_binary_address, + is_canonical_address, + is_checksum_address, + is_checksum_formatted_address, + is_hex_address, + is_normalized_address, + is_same_address, + to_canonical_address, + to_checksum_address, + to_normalized_address, +) +from .applicators import ( + apply_formatter_at_index, + apply_formatter_if, + apply_formatter_to_array, + apply_formatters_to_dict, + apply_formatters_to_sequence, + apply_key_map, + apply_one_of_formatters, + combine_argument_formatters, +) +from .conversions import ( + hexstr_if_str, + text_if_str, + to_bytes, + to_hex, + to_int, + to_text, +) +from .currency import ( + denoms, + from_wei, + to_wei, +) +from .decorators import ( + combomethod, + replace_exceptions, +) +from .encoding import ( + big_endian_to_int, + int_to_big_endian, +) +from .exceptions import ( + ValidationError, +) +from .functional import ( + apply_to_return_value, + flatten_return, + reversed_return, + sort_return, + to_dict, + to_list, + to_ordered_dict, + to_set, + to_tuple, +) +from .hexadecimal import ( + add_0x_prefix, + decode_hex, + encode_hex, + is_0x_prefixed, + is_hex, + is_hexstr, + remove_0x_prefix, +) +from .humanize import ( + humanize_bytes, + humanize_hash, + humanize_integer_sequence, + humanize_ipfs_uri, + humanize_seconds, + humanize_wei, +) +from .logging import ( + DEBUG2_LEVEL_NUM, + ExtendedDebugLogger, + HasExtendedDebugLogger, + HasExtendedDebugLoggerMeta, + HasLogger, + HasLoggerMeta, + get_extended_debug_logger, + get_logger, + setup_DEBUG2_logging, +) +from .module_loading import ( + import_string, +) +from .numeric import ( + clamp, +) +from .types import ( + is_boolean, + is_bytes, + is_dict, + is_integer, + is_list, + is_list_like, + is_null, + is_number, + is_string, + is_text, + is_tuple, +) diff --git a/ccxt/static_dependencies/ethereum/utils/__pycache__/__init__.cpython-311.pyc b/ccxt/static_dependencies/ethereum/utils/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..3c5c7df Binary files /dev/null and b/ccxt/static_dependencies/ethereum/utils/__pycache__/__init__.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/ethereum/utils/__pycache__/address.cpython-311.pyc b/ccxt/static_dependencies/ethereum/utils/__pycache__/address.cpython-311.pyc new file mode 100644 index 0000000..f2e3ba6 Binary files /dev/null and b/ccxt/static_dependencies/ethereum/utils/__pycache__/address.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/ethereum/utils/__pycache__/applicators.cpython-311.pyc b/ccxt/static_dependencies/ethereum/utils/__pycache__/applicators.cpython-311.pyc new file mode 100644 index 0000000..6f079c5 Binary files /dev/null and b/ccxt/static_dependencies/ethereum/utils/__pycache__/applicators.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/ethereum/utils/__pycache__/conversions.cpython-311.pyc b/ccxt/static_dependencies/ethereum/utils/__pycache__/conversions.cpython-311.pyc new file mode 100644 index 0000000..a78bd74 Binary files /dev/null and b/ccxt/static_dependencies/ethereum/utils/__pycache__/conversions.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/ethereum/utils/__pycache__/currency.cpython-311.pyc b/ccxt/static_dependencies/ethereum/utils/__pycache__/currency.cpython-311.pyc new file mode 100644 index 0000000..42effdc Binary files /dev/null and b/ccxt/static_dependencies/ethereum/utils/__pycache__/currency.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/ethereum/utils/__pycache__/decorators.cpython-311.pyc b/ccxt/static_dependencies/ethereum/utils/__pycache__/decorators.cpython-311.pyc new file mode 100644 index 0000000..8783d27 Binary files /dev/null and b/ccxt/static_dependencies/ethereum/utils/__pycache__/decorators.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/ethereum/utils/__pycache__/encoding.cpython-311.pyc b/ccxt/static_dependencies/ethereum/utils/__pycache__/encoding.cpython-311.pyc new file mode 100644 index 0000000..c31980d Binary files /dev/null and b/ccxt/static_dependencies/ethereum/utils/__pycache__/encoding.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/ethereum/utils/__pycache__/exceptions.cpython-311.pyc b/ccxt/static_dependencies/ethereum/utils/__pycache__/exceptions.cpython-311.pyc new file mode 100644 index 0000000..e592f99 Binary files /dev/null and b/ccxt/static_dependencies/ethereum/utils/__pycache__/exceptions.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/ethereum/utils/__pycache__/functional.cpython-311.pyc b/ccxt/static_dependencies/ethereum/utils/__pycache__/functional.cpython-311.pyc new file mode 100644 index 0000000..638696f Binary files /dev/null and b/ccxt/static_dependencies/ethereum/utils/__pycache__/functional.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/ethereum/utils/__pycache__/hexadecimal.cpython-311.pyc b/ccxt/static_dependencies/ethereum/utils/__pycache__/hexadecimal.cpython-311.pyc new file mode 100644 index 0000000..b51fcb9 Binary files /dev/null and b/ccxt/static_dependencies/ethereum/utils/__pycache__/hexadecimal.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/ethereum/utils/__pycache__/humanize.cpython-311.pyc b/ccxt/static_dependencies/ethereum/utils/__pycache__/humanize.cpython-311.pyc new file mode 100644 index 0000000..d8467b6 Binary files /dev/null and b/ccxt/static_dependencies/ethereum/utils/__pycache__/humanize.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/ethereum/utils/__pycache__/logging.cpython-311.pyc b/ccxt/static_dependencies/ethereum/utils/__pycache__/logging.cpython-311.pyc new file mode 100644 index 0000000..3cf1d12 Binary files /dev/null and b/ccxt/static_dependencies/ethereum/utils/__pycache__/logging.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/ethereum/utils/__pycache__/module_loading.cpython-311.pyc b/ccxt/static_dependencies/ethereum/utils/__pycache__/module_loading.cpython-311.pyc new file mode 100644 index 0000000..f1c096f Binary files /dev/null and b/ccxt/static_dependencies/ethereum/utils/__pycache__/module_loading.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/ethereum/utils/__pycache__/numeric.cpython-311.pyc b/ccxt/static_dependencies/ethereum/utils/__pycache__/numeric.cpython-311.pyc new file mode 100644 index 0000000..58b3934 Binary files /dev/null and b/ccxt/static_dependencies/ethereum/utils/__pycache__/numeric.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/ethereum/utils/__pycache__/toolz.cpython-311.pyc b/ccxt/static_dependencies/ethereum/utils/__pycache__/toolz.cpython-311.pyc new file mode 100644 index 0000000..de05521 Binary files /dev/null and b/ccxt/static_dependencies/ethereum/utils/__pycache__/toolz.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/ethereum/utils/__pycache__/types.cpython-311.pyc b/ccxt/static_dependencies/ethereum/utils/__pycache__/types.cpython-311.pyc new file mode 100644 index 0000000..4466c25 Binary files /dev/null and b/ccxt/static_dependencies/ethereum/utils/__pycache__/types.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/ethereum/utils/__pycache__/units.cpython-311.pyc b/ccxt/static_dependencies/ethereum/utils/__pycache__/units.cpython-311.pyc new file mode 100644 index 0000000..c8749de Binary files /dev/null and b/ccxt/static_dependencies/ethereum/utils/__pycache__/units.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/ethereum/utils/abi.py b/ccxt/static_dependencies/ethereum/utils/abi.py new file mode 100644 index 0000000..5753d17 --- /dev/null +++ b/ccxt/static_dependencies/ethereum/utils/abi.py @@ -0,0 +1,72 @@ +from typing import ( + Any, + Dict, +) + +from .conversions import ( + to_bytes +) + +from ...keccak import ( + SHA3 as keccak, +) + + +def collapse_if_tuple(abi: Dict[str, Any]) -> str: + """ + Converts a tuple from a dict to a parenthesized list of its types. + + >>> from eth_utils.abi import collapse_if_tuple + >>> collapse_if_tuple( + ... { + ... 'components': [ + ... {'name': 'anAddress', 'type': 'address'}, + ... {'name': 'anInt', 'type': 'uint256'}, + ... {'name': 'someBytes', 'type': 'bytes'}, + ... ], + ... 'type': 'tuple', + ... } + ... ) + '(address,uint256,bytes)' + """ + typ = abi["type"] + if not isinstance(typ, str): + raise TypeError( + f"The 'type' must be a string, but got {repr(typ)} of type {type(typ)}" + ) + elif not typ.startswith("tuple"): + return typ + + delimited = ",".join(collapse_if_tuple(c) for c in abi["components"]) + # Whatever comes after "tuple" is the array dims. The ABI spec states that + # this will have the form "", "[]", or "[k]". + array_dim = typ[5:] + collapsed = f"({delimited}){array_dim}" + + return collapsed + + +def _abi_to_signature(abi: Dict[str, Any]) -> str: + fn_input_types = ",".join( + [collapse_if_tuple(abi_input) for abi_input in abi.get("inputs", [])] + ) + function_signature = f"{abi['name']}({fn_input_types})" + return function_signature + + +def function_signature_to_4byte_selector(event_signature: str) -> bytes: + return keccak(to_bytes(text=event_signature.replace(" ", "")))[:4] + + +def function_abi_to_4byte_selector(function_abi: Dict[str, Any]) -> bytes: + function_signature = _abi_to_signature(function_abi) + return function_signature_to_4byte_selector(function_signature) + + +def event_signature_to_log_topic(event_signature: str) -> bytes: + return keccak(to_bytes(text=event_signature.replace(" ", ""))) + + +def event_abi_to_log_topic(event_abi: Dict[str, Any]) -> bytes: + event_signature = _abi_to_signature(event_abi) + return event_signature_to_log_topic(event_signature) diff --git a/ccxt/static_dependencies/ethereum/utils/address.py b/ccxt/static_dependencies/ethereum/utils/address.py new file mode 100644 index 0000000..e9b852e --- /dev/null +++ b/ccxt/static_dependencies/ethereum/utils/address.py @@ -0,0 +1,171 @@ +import re +from typing import ( + Any, + Union, + cast, +) + +from ..typing import ( + Address, + AnyAddress, + ChecksumAddress, + HexAddress, + HexStr, +) + +from .conversions import ( + hexstr_if_str, + to_hex, + to_bytes, +) +from ...keccak import ( + SHA3 as keccak, +) +from .hexadecimal import ( + add_0x_prefix, + decode_hex, + encode_hex, + remove_0x_prefix, +) +from .types import ( + is_bytes, + is_text, +) + +_HEX_ADDRESS_REGEXP = re.compile("(0x)?[0-9a-f]{40}", re.IGNORECASE | re.ASCII) + + +def is_hex_address(value: Any) -> bool: + """ + Checks if the given string of text type is an address in hexadecimal encoded form. + """ + if not is_text(value): + return False + return _HEX_ADDRESS_REGEXP.fullmatch(value) is not None + + +def is_binary_address(value: Any) -> bool: + """ + Checks if the given string is an address in raw bytes form. + """ + if not is_bytes(value): + return False + elif len(value) != 20: + return False + else: + return True + + +def is_address(value: Any) -> bool: + """ + Is the given string an address in any of the known formats? + """ + if is_hex_address(value): + if _is_checksum_formatted(value): + return is_checksum_address(value) + return True + + if is_binary_address(value): + return True + + return False + + +def to_normalized_address(value: Union[AnyAddress, str, bytes]) -> HexAddress: + """ + Converts an address to its normalized hexadecimal representation. + """ + try: + hex_address = hexstr_if_str(to_hex, value).lower() + except AttributeError: + raise TypeError(f"Value must be any string, instead got type {type(value)}") + if is_address(hex_address): + return HexAddress(HexStr(hex_address)) + else: + raise ValueError( + f"Unknown format {repr(value)}, attempted to normalize to " + f"{repr(hex_address)}" + ) + + +def is_normalized_address(value: Any) -> bool: + """ + Returns whether the provided value is an address in its normalized form. + """ + if not is_address(value): + return False + else: + is_equal = value == to_normalized_address(value) + return cast(bool, is_equal) + + +def to_canonical_address(address: Union[AnyAddress, str, bytes]) -> Address: + """ + Convert a valid address to its canonical form (20-length bytes). + """ + return Address(decode_hex(to_normalized_address(address))) + + +def is_canonical_address(address: Any) -> bool: + """ + Returns `True` if the `value` is an address in its canonical form. + """ + if not is_bytes(address) or len(address) != 20: + return False + is_equal = address == to_canonical_address(address) + return cast(bool, is_equal) + + +def is_same_address(left: AnyAddress, right: AnyAddress) -> bool: + """ + Checks if both addresses are same or not. + """ + if not is_address(left) or not is_address(right): + raise ValueError("Both values must be valid addresses") + else: + return bool(to_normalized_address(left) == to_normalized_address(right)) + + +def to_checksum_address(value: Union[AnyAddress, str, bytes]) -> ChecksumAddress: + """ + Makes a checksum address given a supported format. + """ + norm_address = to_normalized_address(value) + address_hash = encode_hex(keccak(to_bytes(text=remove_0x_prefix(HexStr(norm_address))))) + + checksum_address = add_0x_prefix( + HexStr( + "".join( + ( + norm_address[i].upper() + if int(address_hash[i], 16) > 7 + else norm_address[i] + ) + for i in range(2, 42) + ) + ) + ) + return ChecksumAddress(HexAddress(checksum_address)) + + +def is_checksum_address(value: Any) -> bool: + if not is_text(value): + return False + + if not is_hex_address(value): + return False + is_equal = value == to_checksum_address(value) + return cast(bool, is_equal) + + +def _is_checksum_formatted(value: Any) -> bool: + unprefixed_value = remove_0x_prefix(value) + return ( + not unprefixed_value.islower() + and not unprefixed_value.isupper() + and not unprefixed_value.isnumeric() + ) + + +def is_checksum_formatted_address(value: Any) -> bool: + return is_hex_address(value) and _is_checksum_formatted(value) diff --git a/ccxt/static_dependencies/ethereum/utils/applicators.py b/ccxt/static_dependencies/ethereum/utils/applicators.py new file mode 100644 index 0000000..282ebd0 --- /dev/null +++ b/ccxt/static_dependencies/ethereum/utils/applicators.py @@ -0,0 +1,151 @@ +from typing import ( + Any, + Callable, + Dict, + Generator, + List, + Tuple, +) +import warnings + +from .decorators import ( + return_arg_type, +) +from .functional import ( + to_dict, +) +from .toolz import ( + compose, + curry, +) + +Formatters = Callable[[List[Any]], List[Any]] + + +@return_arg_type(2) +def apply_formatter_at_index( + formatter: Callable[..., Any], at_index: int, value: List[Any] +) -> Generator[List[Any], None, None]: + if at_index + 1 > len(value): + raise IndexError( + f"Not enough values in iterable to apply formatter. Got: {len(value)}. " + f"Need: {at_index + 1}" + ) + for index, item in enumerate(value): + if index == at_index: + yield formatter(item) + else: + yield item + + +def combine_argument_formatters(*formatters: List[Callable[..., Any]]) -> Formatters: + warnings.warn( + DeprecationWarning( + "combine_argument_formatters(formatter1, formatter2)([item1, item2])" + "has been deprecated and will be removed in a subsequent major version " + "release of the eth-utils library. Update your calls to use " + "apply_formatters_to_sequence([formatter1, formatter2], [item1, item2]) " + "instead." + ), + stacklevel=2, + ) + + _formatter_at_index = curry(apply_formatter_at_index) + return compose( # type: ignore + *( + _formatter_at_index(formatter, index) + for index, formatter in enumerate(formatters) + ) + ) + + +@return_arg_type(1) +def apply_formatters_to_sequence( + formatters: List[Any], sequence: List[Any] +) -> Generator[List[Any], None, None]: + if len(formatters) > len(sequence): + raise IndexError( + f"Too many formatters for sequence: {len(formatters)} formatters for " + f"{repr(sequence)}" + ) + elif len(formatters) < len(sequence): + raise IndexError( + f"Too few formatters for sequence: {len(formatters)} formatters for " + f"{repr(sequence)}" + ) + else: + for formatter, item in zip(formatters, sequence): + yield formatter(item) + + +def apply_formatter_if( + condition: Callable[..., bool], formatter: Callable[..., Any], value: Any +) -> Any: + if condition(value): + return formatter(value) + else: + return value + + +@to_dict +def apply_formatters_to_dict( + formatters: Dict[Any, Any], value: Dict[Any, Any] +) -> Generator[Tuple[Any, Any], None, None]: + for key, item in value.items(): + if key in formatters: + try: + yield key, formatters[key](item) + except ValueError as exc: + new_error_message = ( + f"Could not format invalid value {repr(item)} as field {repr(key)}" + ) + raise ValueError(new_error_message) from exc + except TypeError as exc: + new_error_message = ( + f"Could not format invalid type {repr(item)} as field {repr(key)}" + ) + raise TypeError(new_error_message) from exc + else: + yield key, item + + +@return_arg_type(1) +def apply_formatter_to_array( + formatter: Callable[..., Any], value: List[Any] +) -> Generator[List[Any], None, None]: + for item in value: + yield formatter(item) + + +def apply_one_of_formatters( + formatter_condition_pairs: Tuple[Tuple[Callable[..., Any], Callable[..., Any]]], + value: Any, +) -> Any: + for condition, formatter in formatter_condition_pairs: + if condition(value): + return formatter(value) + else: + raise ValueError( + "The provided value did not satisfy any of the formatter conditions" + ) + + +@to_dict +def apply_key_map( + key_mappings: Dict[Any, Any], value: Dict[Any, Any] +) -> Generator[Tuple[Any, Any], None, None]: + key_conflicts = ( + set(value.keys()) + .difference(key_mappings.keys()) + .intersection(v for k, v in key_mappings.items() if v in value) + ) + if key_conflicts: + raise KeyError( + f"Could not apply key map due to conflicting key(s): {key_conflicts}" + ) + + for key, item in value.items(): + if key in key_mappings: + yield key_mappings[key], item + else: + yield key, item diff --git a/ccxt/static_dependencies/ethereum/utils/conversions.py b/ccxt/static_dependencies/ethereum/utils/conversions.py new file mode 100644 index 0000000..1ee6170 --- /dev/null +++ b/ccxt/static_dependencies/ethereum/utils/conversions.py @@ -0,0 +1,190 @@ +from typing import ( + Callable, + Optional, + TypeVar, + Union, + cast, +) + +from ..typing import ( + HexStr, + Primitives, +) + +from .decorators import ( + validate_conversion_arguments, +) +from .encoding import ( + big_endian_to_int, + int_to_big_endian, +) +from .hexadecimal import ( + add_0x_prefix, + decode_hex, + encode_hex, + is_hexstr, + remove_0x_prefix, +) +from .types import ( + is_boolean, + is_integer, + is_string, +) + +T = TypeVar("T") + + +@validate_conversion_arguments +def to_hex( + primitive: Optional[Primitives] = None, + hexstr: Optional[HexStr] = None, + text: Optional[str] = None, +) -> HexStr: + """ + Auto converts any supported value into its hex representation. + Trims leading zeros, as defined in: + https://github.com/ethereum/wiki/wiki/JSON-RPC#hex-value-encoding + """ + if hexstr is not None: + return add_0x_prefix(HexStr(hexstr.lower())) + + if text is not None: + return encode_hex(text.encode("utf-8")) + + if is_boolean(primitive): + return HexStr("0x1") if primitive else HexStr("0x0") + + if isinstance(primitive, (bytes, bytearray)): + return encode_hex(primitive) + elif is_string(primitive): + raise TypeError( + "Unsupported type: The primitive argument must be one of: bytes," + "bytearray, int or bool and not str" + ) + + if is_integer(primitive): + return HexStr(hex(cast(int, primitive))) + + raise TypeError( + f"Unsupported type: '{repr(type(primitive))}'. Must be one of: bool, str, " + "bytes, bytearray or int." + ) + + +@validate_conversion_arguments +def to_int( + primitive: Optional[Primitives] = None, + hexstr: Optional[HexStr] = None, + text: Optional[str] = None, +) -> int: + """ + Converts value to its integer representation. + Values are converted this way: + + * primitive: + + * bytes, bytearrays: big-endian integer + * bool: True => 1, False => 0 + * hexstr: interpret hex as integer + * text: interpret as string of digits, like '12' => 12 + """ + if hexstr is not None: + return int(hexstr, 16) + elif text is not None: + return int(text) + elif isinstance(primitive, (bytes, bytearray)): + return big_endian_to_int(primitive) + elif isinstance(primitive, str): + raise TypeError("Pass in strings with keyword hexstr or text") + elif isinstance(primitive, (int, bool)): + return int(primitive) + else: + raise TypeError( + "Invalid type. Expected one of int/bool/str/bytes/bytearray. Got " + f"{type(primitive)}" + ) + + +@validate_conversion_arguments +def to_bytes( + primitive: Optional[Primitives] = None, + hexstr: Optional[HexStr] = None, + text: Optional[str] = None, +) -> bytes: + if is_boolean(primitive): + return b"\x01" if primitive else b"\x00" + elif isinstance(primitive, bytearray): + return bytes(primitive) + elif isinstance(primitive, bytes): + return primitive + elif is_integer(primitive): + return to_bytes(hexstr=to_hex(primitive)) + elif hexstr is not None: + if len(hexstr) % 2: + hexstr = cast(HexStr, "0x0" + remove_0x_prefix(hexstr)) + return decode_hex(hexstr) + elif text is not None: + return text.encode("utf-8") + raise TypeError( + "expected a bool, int, byte or bytearray in first arg, " + "or keyword of hexstr or text" + ) + + +@validate_conversion_arguments +def to_text( + primitive: Optional[Primitives] = None, + hexstr: Optional[HexStr] = None, + text: Optional[str] = None, +) -> str: + if hexstr is not None: + return to_bytes(hexstr=hexstr).decode("utf-8") + elif text is not None: + return text + elif isinstance(primitive, str): + return to_text(hexstr=primitive) + elif isinstance(primitive, (bytes, bytearray)): + return primitive.decode("utf-8") + elif is_integer(primitive): + byte_encoding = int_to_big_endian(cast(int, primitive)) + return to_text(byte_encoding) + raise TypeError("Expected an int, bytes, bytearray or hexstr.") + + +def text_if_str( + to_type: Callable[..., T], text_or_primitive: Union[bytes, int, str] +) -> T: + """ + Convert to a type, assuming that strings can be only unicode text (not a hexstr). + + :param to_type function: takes the arguments (primitive, hexstr=hexstr, text=text), + eg~ to_bytes, to_text, to_hex, to_int, etc + :param text_or_primitive bytes, str, int: value to convert + """ + if isinstance(text_or_primitive, str): + return to_type(text=text_or_primitive) + else: + return to_type(text_or_primitive) + + +def hexstr_if_str( + to_type: Callable[..., T], hexstr_or_primitive: Union[bytes, int, str] +) -> T: + """ + Convert to a type, assuming that strings can be only hexstr (not unicode text). + + :param to_type function: takes the arguments (primitive, hexstr=hexstr, text=text), + eg~ to_bytes, to_text, to_hex, to_int, etc + :param hexstr_or_primitive bytes, str, int: value to convert + """ + if isinstance(hexstr_or_primitive, str): + if remove_0x_prefix(HexStr(hexstr_or_primitive)) and not is_hexstr( + hexstr_or_primitive + ): + raise ValueError( + "when sending a str, it must be a hex string. " + f"Got: {repr(hexstr_or_primitive)}" + ) + return to_type(hexstr=hexstr_or_primitive) + else: + return to_type(hexstr_or_primitive) diff --git a/ccxt/static_dependencies/ethereum/utils/currency.py b/ccxt/static_dependencies/ethereum/utils/currency.py new file mode 100644 index 0000000..e25c0f6 --- /dev/null +++ b/ccxt/static_dependencies/ethereum/utils/currency.py @@ -0,0 +1,107 @@ +import decimal +from decimal import ( + localcontext, +) +from typing import ( + Union, +) + +from .types import ( + is_integer, + is_string, +) +from .units import ( + units, +) + + +class denoms: + wei = int(units["wei"]) + kwei = int(units["kwei"]) + babbage = int(units["babbage"]) + femtoether = int(units["femtoether"]) + mwei = int(units["mwei"]) + lovelace = int(units["lovelace"]) + picoether = int(units["picoether"]) + gwei = int(units["gwei"]) + shannon = int(units["shannon"]) + nanoether = int(units["nanoether"]) + nano = int(units["nano"]) + szabo = int(units["szabo"]) + microether = int(units["microether"]) + micro = int(units["micro"]) + finney = int(units["finney"]) + milliether = int(units["milliether"]) + milli = int(units["milli"]) + ether = int(units["ether"]) + kether = int(units["kether"]) + grand = int(units["grand"]) + mether = int(units["mether"]) + gether = int(units["gether"]) + tether = int(units["tether"]) + + +MIN_WEI = 0 +MAX_WEI = 2**256 - 1 + + +def from_wei(number: int, unit: str) -> Union[int, decimal.Decimal]: + """ + Takes a number of wei and converts it to any other ether unit. + """ + if unit.lower() not in units: + raise ValueError(f"Unknown unit. Must be one of {'/'.join(units.keys())}") + + if number == 0: + return 0 + + if number < MIN_WEI or number > MAX_WEI: + raise ValueError("value must be between 1 and 2**256 - 1") + + unit_value = units[unit.lower()] + + with localcontext() as ctx: + ctx.prec = 999 + d_number = decimal.Decimal(value=number, context=ctx) + result_value = d_number / unit_value + + return result_value + + +def to_wei(number: Union[int, float, str, decimal.Decimal], unit: str) -> int: + """ + Takes a number of a unit and converts it to wei. + """ + if unit.lower() not in units: + raise ValueError(f"Unknown unit. Must be one of {'/'.join(units.keys())}") + + if is_integer(number) or is_string(number): + d_number = decimal.Decimal(value=number) + elif isinstance(number, float): + d_number = decimal.Decimal(value=str(number)) + elif isinstance(number, decimal.Decimal): + d_number = number + else: + raise TypeError("Unsupported type. Must be one of integer, float, or string") + + s_number = str(number) + unit_value = units[unit.lower()] + + if d_number == decimal.Decimal(0): + return 0 + + if d_number < 1 and "." in s_number: + with localcontext() as ctx: + multiplier = len(s_number) - s_number.index(".") - 1 + ctx.prec = multiplier + d_number = decimal.Decimal(value=number, context=ctx) * 10**multiplier + unit_value /= 10**multiplier + + with localcontext() as ctx: + ctx.prec = 999 + result_value = decimal.Decimal(value=d_number, context=ctx) * unit_value + + if result_value < MIN_WEI or result_value > MAX_WEI: + raise ValueError("Resulting wei value must be between 1 and 2**256 - 1") + + return int(result_value) diff --git a/ccxt/static_dependencies/ethereum/utils/curried/__init__.py b/ccxt/static_dependencies/ethereum/utils/curried/__init__.py new file mode 100644 index 0000000..e3e1ae0 --- /dev/null +++ b/ccxt/static_dependencies/ethereum/utils/curried/__init__.py @@ -0,0 +1,269 @@ +from typing import ( + Any, + Callable, + Dict, + Generator, + Optional, + Sequence, + Tuple, + TypeVar, + Union, + overload, +) + +from .. import ( + ExtendedDebugLogger, + HasExtendedDebugLogger, + HasExtendedDebugLoggerMeta, + HasLogger, + HasLoggerMeta, + ValidationError, + add_0x_prefix, + apply_formatter_at_index, + apply_formatter_if as non_curried_apply_formatter_if, + apply_formatter_to_array, + apply_formatters_to_dict as non_curried_apply_formatters_to_dict, + apply_formatters_to_sequence, + apply_key_map, + apply_one_of_formatters as non_curried_apply_one_of_formatters, + apply_to_return_value, + big_endian_to_int, + clamp, + combine_argument_formatters, + combomethod, + decode_hex, + denoms, + encode_hex, + # event_abi_to_log_topic, + # event_signature_to_log_topic, + flatten_return, + from_wei, + # function_abi_to_4byte_selector, + # function_signature_to_4byte_selector, + get_extended_debug_logger, + get_logger, + hexstr_if_str as non_curried_hexstr_if_str, + humanize_bytes, + humanize_hash, + humanize_integer_sequence, + humanize_ipfs_uri, + humanize_seconds, + humanize_wei, + import_string, + int_to_big_endian, + is_0x_prefixed, + is_address, + is_binary_address, + is_boolean, + is_bytes, + is_canonical_address, + is_checksum_address, + is_checksum_formatted_address, + is_dict, + is_hex, + is_hex_address, + is_hexstr, + is_integer, + is_list, + is_list_like, + is_normalized_address, + is_null, + is_number, + is_same_address, + is_string, + is_text, + is_tuple, + # keccak, + remove_0x_prefix, + replace_exceptions, + reversed_return, + setup_DEBUG2_logging, + sort_return, + text_if_str as non_curried_text_if_str, + to_bytes, + to_canonical_address, + to_checksum_address, + to_dict, + to_hex, + to_int, + to_list, + to_normalized_address, + to_ordered_dict, + to_set, + to_text, + to_tuple, + to_wei, +) +from ..toolz import ( + curry, +) + +TReturn = TypeVar("TReturn") +TValue = TypeVar("TValue") + + +@overload +def apply_formatter_if( + condition: Callable[..., bool] +) -> Callable[[Callable[..., TReturn]], Callable[[TValue], Union[TReturn, TValue]]]: + pass + + +@overload +def apply_formatter_if( + condition: Callable[..., bool], formatter: Callable[..., TReturn] +) -> Callable[[TValue], Union[TReturn, TValue]]: + pass + + +@overload +def apply_formatter_if( + condition: Callable[..., bool], formatter: Callable[..., TReturn], value: TValue +) -> Union[TReturn, TValue]: + pass + + +# This is just a stub to appease mypy, it gets overwritten later +def apply_formatter_if( # type: ignore + condition: Callable[..., bool], + formatter: Optional[Callable[..., TReturn]] = None, + value: Optional[TValue] = None, +) -> Union[ + Callable[[Callable[..., TReturn]], Callable[[TValue], Union[TReturn, TValue]]], + Callable[[TValue], Union[TReturn, TValue]], + TReturn, + TValue, +]: + pass + + +@overload +def apply_one_of_formatters( + formatter_condition_pairs: Sequence[ + Tuple[Callable[..., bool], Callable[..., TReturn]] + ] +) -> Callable[[TValue], TReturn]: + ... + + +@overload +def apply_one_of_formatters( + formatter_condition_pairs: Sequence[ + Tuple[Callable[..., bool], Callable[..., TReturn]] + ], + value: TValue, +) -> TReturn: + ... + + +# This is just a stub to appease mypy, it gets overwritten later +def apply_one_of_formatters( # type: ignore + formatter_condition_pairs: Sequence[ + Tuple[Callable[..., bool], Callable[..., TReturn]] + ], + value: Optional[TValue] = None, +) -> TReturn: + ... + + +@overload +def hexstr_if_str( + to_type: Callable[..., TReturn] +) -> Callable[[Union[bytes, int, str]], TReturn]: + ... + + +@overload +def hexstr_if_str( + to_type: Callable[..., TReturn], to_format: Union[bytes, int, str] +) -> TReturn: + ... + + +# This is just a stub to appease mypy, it gets overwritten later +def hexstr_if_str( # type: ignore + to_type: Callable[..., TReturn], to_format: Optional[Union[bytes, int, str]] = None +) -> TReturn: + ... + + +@overload +def text_if_str( + to_type: Callable[..., TReturn] +) -> Callable[[Union[bytes, int, str]], TReturn]: + ... + + +@overload +def text_if_str( + to_type: Callable[..., TReturn], text_or_primitive: Union[bytes, int, str] +) -> TReturn: + ... + + +# This is just a stub to appease mypy, it gets overwritten later +def text_if_str( # type: ignore + to_type: Callable[..., TReturn], + text_or_primitive: Optional[Union[bytes, int, str]] = None, +) -> TReturn: + ... + + +@overload +def apply_formatters_to_dict( + formatters: Dict[Any, Any] +) -> Callable[[Dict[Any, Any]], TReturn]: + ... + + +@overload +def apply_formatters_to_dict( + formatters: Dict[Any, Any], value: Dict[Any, Any] +) -> Dict[Any, Any]: + ... + + +# This is just a stub to appease mypy, it gets overwritten later +def apply_formatters_to_dict( # type: ignore + formatters: Dict[Any, Any], value: Optional[Dict[Any, Any]] = None +) -> Dict[Any, Any]: + ... + + +apply_formatter_at_index = curry(apply_formatter_at_index) +apply_formatter_if = curry(non_curried_apply_formatter_if) # noqa: F811 +apply_formatter_to_array = curry(apply_formatter_to_array) +apply_formatters_to_dict = curry(non_curried_apply_formatters_to_dict) # noqa: F811 +apply_formatters_to_sequence = curry(apply_formatters_to_sequence) +apply_key_map = curry(apply_key_map) +apply_one_of_formatters = curry(non_curried_apply_one_of_formatters) # noqa: F811 +from_wei = curry(from_wei) +get_logger = curry(get_logger) +hexstr_if_str = curry(non_curried_hexstr_if_str) # noqa: F811 +is_same_address = curry(is_same_address) +text_if_str = curry(non_curried_text_if_str) # noqa: F811 +to_wei = curry(to_wei) +clamp = curry(clamp) + +# Delete any methods and classes that are not intended to be importable from +# `eth_utils.curried`. We do this approach instead of __all__ because this approach +# actually prevents importing the wrong thing, while __all__ only affects +# `from eth_utils.curried import *` +del Any +del Callable +del Dict +del Generator +del Optional +del Sequence +del TReturn +del TValue +del Tuple +del TypeVar +del Union +del curry +del non_curried_apply_formatter_if +del non_curried_apply_one_of_formatters +del non_curried_apply_formatters_to_dict +del non_curried_hexstr_if_str +del non_curried_text_if_str +del overload diff --git a/ccxt/static_dependencies/ethereum/utils/curried/__pycache__/__init__.cpython-311.pyc b/ccxt/static_dependencies/ethereum/utils/curried/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..61e7351 Binary files /dev/null and b/ccxt/static_dependencies/ethereum/utils/curried/__pycache__/__init__.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/ethereum/utils/debug.py b/ccxt/static_dependencies/ethereum/utils/debug.py new file mode 100644 index 0000000..7f6c7f4 --- /dev/null +++ b/ccxt/static_dependencies/ethereum/utils/debug.py @@ -0,0 +1,20 @@ +import platform +import subprocess +import sys + + +def pip_freeze() -> str: + result = subprocess.run("python -m pip freeze".split(), stdout=subprocess.PIPE) + return f"python -m pip freeze result:\n{result.stdout.decode()}" + + +def python_version() -> str: + return f"Python version:\n{sys.version}" + + +def platform_info() -> str: + return f"Operating System: {platform.platform()}" + + +def get_environment_summary() -> str: + return "\n\n".join([python_version(), platform_info(), pip_freeze()]) diff --git a/ccxt/static_dependencies/ethereum/utils/decorators.py b/ccxt/static_dependencies/ethereum/utils/decorators.py new file mode 100644 index 0000000..b2cc862 --- /dev/null +++ b/ccxt/static_dependencies/ethereum/utils/decorators.py @@ -0,0 +1,132 @@ +import functools +import itertools +from typing import ( + Any, + Callable, + Dict, + Optional, + Type, + TypeVar, +) + +from .types import ( + is_text, +) + +T = TypeVar("T") + + +class combomethod: + def __init__(self, method: Callable[..., Any]) -> None: + self.method = method + + def __get__( + self, obj: Optional[T] = None, objtype: Optional[Type[T]] = None + ) -> Callable[..., Any]: + @functools.wraps(self.method) + def _wrapper(*args: Any, **kwargs: Any) -> Any: + if obj is not None: + return self.method(obj, *args, **kwargs) + else: + return self.method(objtype, *args, **kwargs) + + return _wrapper + + +def _has_one_val(*args: T, **kwargs: T) -> bool: + vals = itertools.chain(args, kwargs.values()) + not_nones = list(filter(lambda val: val is not None, vals)) + return len(not_nones) == 1 + + +def _assert_one_val(*args: T, **kwargs: T) -> None: + if not _has_one_val(*args, **kwargs): + raise TypeError( + "Exactly one of the passed values can be specified. " + f"Instead, values were: {repr(args)}, {repr(kwargs)}" + ) + + +def _hexstr_or_text_kwarg_is_text_type(**kwargs: T) -> bool: + value = kwargs["hexstr"] if "hexstr" in kwargs else kwargs["text"] + return is_text(value) + + +def _assert_hexstr_or_text_kwarg_is_text_type(**kwargs: T) -> None: + if not _hexstr_or_text_kwarg_is_text_type(**kwargs): + raise TypeError( + "Arguments passed as hexstr or text must be of text type. " + f"Instead, value was: {(repr(next(iter(list(kwargs.values())))))}" + ) + + +def _validate_supported_kwarg(kwargs: Any) -> None: + if next(iter(kwargs)) not in ["primitive", "hexstr", "text"]: + raise TypeError( + "Kwarg must be 'primitive', 'hexstr', or 'text'. " + f"Instead, kwarg was: {repr((next(iter(kwargs))))}" + ) + + +def validate_conversion_arguments(to_wrap: Callable[..., T]) -> Callable[..., T]: + """ + Validates arguments for conversion functions. + - Only a single argument is present + - Kwarg must be 'primitive' 'hexstr' or 'text' + - If it is 'hexstr' or 'text' that it is a text type + """ + + @functools.wraps(to_wrap) + def wrapper(*args: Any, **kwargs: Any) -> T: + _assert_one_val(*args, **kwargs) + if kwargs: + _validate_supported_kwarg(kwargs) + + if len(args) == 0 and "primitive" not in kwargs: + _assert_hexstr_or_text_kwarg_is_text_type(**kwargs) + return to_wrap(*args, **kwargs) + + return wrapper + + +def return_arg_type(at_position: int) -> Callable[..., Callable[..., T]]: + """ + Wrap the return value with the result of `type(args[at_position])`. + """ + + def decorator(to_wrap: Callable[..., Any]) -> Callable[..., T]: + @functools.wraps(to_wrap) + def wrapper(*args: Any, **kwargs: Any) -> T: # type: ignore + result = to_wrap(*args, **kwargs) + ReturnType = type(args[at_position]) + return ReturnType(result) # type: ignore + + return wrapper + + return decorator + + +def replace_exceptions( + old_to_new_exceptions: Dict[Type[BaseException], Type[BaseException]] +) -> Callable[[Callable[..., T]], Callable[..., T]]: + """ + Replaces old exceptions with new exceptions to be raised in their place. + """ + old_exceptions = tuple(old_to_new_exceptions.keys()) + + def decorator(to_wrap: Callable[..., T]) -> Callable[..., T]: + @functools.wraps(to_wrap) + def wrapped(*args: Any, **kwargs: Any) -> T: + try: + return to_wrap(*args, **kwargs) + except old_exceptions as err: + try: + raise old_to_new_exceptions[type(err)](err) from err + except KeyError: + raise TypeError( + f"could not look up new exception to use for {repr(err)}" + ) from err + + return wrapped + + return decorator diff --git a/ccxt/static_dependencies/ethereum/utils/encoding.py b/ccxt/static_dependencies/ethereum/utils/encoding.py new file mode 100644 index 0000000..44a9fe7 --- /dev/null +++ b/ccxt/static_dependencies/ethereum/utils/encoding.py @@ -0,0 +1,6 @@ +def int_to_big_endian(value: int) -> bytes: + return value.to_bytes((value.bit_length() + 7) // 8 or 1, "big") + + +def big_endian_to_int(value: bytes) -> int: + return int.from_bytes(value, "big") diff --git a/ccxt/static_dependencies/ethereum/utils/exceptions.py b/ccxt/static_dependencies/ethereum/utils/exceptions.py new file mode 100644 index 0000000..8313684 --- /dev/null +++ b/ccxt/static_dependencies/ethereum/utils/exceptions.py @@ -0,0 +1,4 @@ +class ValidationError(Exception): + """ + Raised when something does not pass a validation check. + """ diff --git a/ccxt/static_dependencies/ethereum/utils/functional.py b/ccxt/static_dependencies/ethereum/utils/functional.py new file mode 100644 index 0000000..07ebd0e --- /dev/null +++ b/ccxt/static_dependencies/ethereum/utils/functional.py @@ -0,0 +1,75 @@ +import collections +import functools +import itertools +from typing import ( # noqa: F401 + Any, + Callable, + Dict, + Iterable, + List, + Mapping, + Set, + Tuple, + TypeVar, + Union, +) + +from .toolz import ( + compose as _compose, +) + +T = TypeVar("T") + + +def identity(value: T) -> T: + return value + + +TGIn = TypeVar("TGIn") +TGOut = TypeVar("TGOut") +TFOut = TypeVar("TFOut") + + +def combine( + f: Callable[[TGOut], TFOut], g: Callable[[TGIn], TGOut] +) -> Callable[[TGIn], TFOut]: + return lambda x: f(g(x)) + + +def apply_to_return_value( + callback: Callable[..., T] +) -> Callable[..., Callable[..., T]]: + def outer(fn: Callable[..., T]) -> Callable[..., T]: + # We would need to type annotate *args and **kwargs but doing so segfaults + # the PyPy builds. We ignore instead. + @functools.wraps(fn) + def inner(*args, **kwargs) -> T: # type: ignore + return callback(fn(*args, **kwargs)) + + return inner + + return outer + + +TVal = TypeVar("TVal") +TKey = TypeVar("TKey") +to_tuple = apply_to_return_value( + tuple +) # type: Callable[[Callable[..., Iterable[TVal]]], Callable[..., Tuple[TVal, ...]]] # noqa: E501 +to_list = apply_to_return_value( + list +) # type: Callable[[Callable[..., Iterable[TVal]]], Callable[..., List[TVal]]] # noqa: E501 +to_set = apply_to_return_value( + set +) # type: Callable[[Callable[..., Iterable[TVal]]], Callable[..., Set[TVal]]] # noqa: E501 +to_dict = apply_to_return_value( + dict +) # type: Callable[[Callable[..., Iterable[Union[Mapping[TKey, TVal], Tuple[TKey, TVal]]]]], Callable[..., Dict[TKey, TVal]]] # noqa: E501 +to_ordered_dict = apply_to_return_value( + collections.OrderedDict +) # type: Callable[[Callable[..., Iterable[Union[Mapping[TKey, TVal], Tuple[TKey, TVal]]]]], Callable[..., collections.OrderedDict[TKey, TVal]]] # noqa: E501 +sort_return = _compose(to_tuple, apply_to_return_value(sorted)) +flatten_return = _compose( + to_tuple, apply_to_return_value(itertools.chain.from_iterable) +) +reversed_return = _compose(to_tuple, apply_to_return_value(reversed), to_tuple) diff --git a/ccxt/static_dependencies/ethereum/utils/hexadecimal.py b/ccxt/static_dependencies/ethereum/utils/hexadecimal.py new file mode 100644 index 0000000..a442361 --- /dev/null +++ b/ccxt/static_dependencies/ethereum/utils/hexadecimal.py @@ -0,0 +1,74 @@ +# String encodings and numeric representations + +import binascii +import re +from typing import ( + Any, + AnyStr, +) + +from ..typing import ( + HexStr, +) + +from .types import ( + is_string, + is_text, +) + +_HEX_REGEXP = re.compile("(0[xX])?[0-9a-fA-F]*") + + +def decode_hex(value: str) -> bytes: + if not is_text(value): + raise TypeError("Value must be an instance of str") + non_prefixed = remove_0x_prefix(HexStr(value)) + # unhexlify will only accept bytes type someday + ascii_hex = non_prefixed.encode("ascii") + return binascii.unhexlify(ascii_hex) + + +def encode_hex(value: AnyStr) -> HexStr: + if not is_string(value): + raise TypeError("Value must be an instance of str or unicode") + elif isinstance(value, (bytes, bytearray)): + ascii_bytes = value + else: + ascii_bytes = value.encode("ascii") + + binary_hex = binascii.hexlify(ascii_bytes) + return add_0x_prefix(HexStr(binary_hex.decode("ascii"))) + + +def is_0x_prefixed(value: str) -> bool: + if not is_text(value): + raise TypeError( + f"is_0x_prefixed requires text typed arguments. Got: {repr(value)}" + ) + return value.startswith(("0x", "0X")) + + +def remove_0x_prefix(value: HexStr) -> HexStr: + if is_0x_prefixed(value): + return HexStr(value[2:]) + return value + + +def add_0x_prefix(value: HexStr) -> HexStr: + if is_0x_prefixed(value): + return value + return HexStr("0x" + value) + + +def is_hexstr(value: Any) -> bool: + if not is_text(value) or not value: + return False + return _HEX_REGEXP.fullmatch(value) is not None + + +def is_hex(value: Any) -> bool: + if not is_text(value): + raise TypeError(f"is_hex requires text typed arguments. Got: {repr(value)}") + if not value: + return False + return _HEX_REGEXP.fullmatch(value) is not None diff --git a/ccxt/static_dependencies/ethereum/utils/humanize.py b/ccxt/static_dependencies/ethereum/utils/humanize.py new file mode 100644 index 0000000..35f93d2 --- /dev/null +++ b/ccxt/static_dependencies/ethereum/utils/humanize.py @@ -0,0 +1,188 @@ +from typing import ( + Any, + Iterable, + Iterator, + Tuple, + Union, +) +from urllib import ( + parse, +) + +from ..typing import ( + URI, + Hash32, +) + +from .currency import ( + denoms, + from_wei, +) + +from .toolz import ( + sliding_window, + take, +) + + +def humanize_seconds(seconds: Union[float, int]) -> str: + if int(seconds) == 0: + return "0s" + + unit_values = _consume_leading_zero_units(_humanize_seconds(int(seconds))) + + return "".join((f"{amount}{unit}" for amount, unit in take(3, unit_values))) + + +SECOND = 1 +MINUTE = 60 +HOUR = 60 * 60 +DAY = 24 * HOUR +YEAR = 365 * DAY +MONTH = YEAR // 12 +WEEK = 7 * DAY + + +UNITS = ( + (YEAR, "y"), + (MONTH, "m"), + (WEEK, "w"), + (DAY, "d"), + (HOUR, "h"), + (MINUTE, "m"), + (SECOND, "s"), +) + + +def _consume_leading_zero_units( + units_iter: Iterator[Tuple[int, str]] +) -> Iterator[Tuple[int, str]]: + for amount, unit in units_iter: + if amount == 0: + continue + else: + yield (amount, unit) + break + + yield from units_iter + + +def _humanize_seconds(seconds: int) -> Iterator[Tuple[int, str]]: + remainder = seconds + + for duration, unit in UNITS: + if not remainder: + break + + num = remainder // duration + yield num, unit + + remainder %= duration + + +DISPLAY_HASH_CHARS = 4 + + +def humanize_bytes(value: bytes) -> str: + if len(value) <= DISPLAY_HASH_CHARS + 1: + return value.hex() + value_as_hex = value.hex() + head = value_as_hex[:DISPLAY_HASH_CHARS] + tail = value_as_hex[-1 * DISPLAY_HASH_CHARS :] + return f"{head}..{tail}" + + +def humanize_hash(value: Hash32) -> str: + return humanize_bytes(value) + + +def humanize_ipfs_uri(uri: URI) -> str: + if not is_ipfs_uri(uri): + raise TypeError( + f"{uri} does not look like a valid IPFS uri. Currently, " + "only CIDv0 hash schemes are supported." + ) + + parsed = parse.urlparse(uri) + ipfs_hash = parsed.netloc + head = ipfs_hash[:DISPLAY_HASH_CHARS] + tail = ipfs_hash[-1 * DISPLAY_HASH_CHARS :] + return f"ipfs://{head}..{tail}" + + +def is_ipfs_uri(value: Any) -> bool: + if not isinstance(value, str): + return False + + parsed = parse.urlparse(value) + if parsed.scheme != "ipfs" or not parsed.netloc: + return False + + return _is_CIDv0_ipfs_hash(parsed.netloc) + + +def _is_CIDv0_ipfs_hash(ipfs_hash: str) -> bool: + if ipfs_hash.startswith("Qm") and len(ipfs_hash) == 46: + return True + return False + + +def _find_breakpoints(*values: int) -> Iterator[int]: + yield 0 + for index, (left, right) in enumerate(sliding_window(2, values), 1): + if left + 1 == right: + continue + else: + yield index + yield len(values) + + +def _extract_integer_ranges(*values: int) -> Iterator[Tuple[int, int]]: + """ + Return a tuple of consecutive ranges of integers. + + :param values: a sequence of ordered integers + + - fn(1, 2, 3) -> ((1, 3),) + - fn(1, 2, 3, 7, 8, 9) -> ((1, 3), (7, 9)) + - fn(1, 7, 8, 9) -> ((1, 1), (7, 9)) + """ + for left, right in sliding_window(2, _find_breakpoints(*values)): + chunk = values[left:right] + yield chunk[0], chunk[-1] + + +def _humanize_range(bounds: Tuple[int, int]) -> str: + left, right = bounds + if left == right: + return str(left) + else: + return f"{left}-{right}" + + +def humanize_integer_sequence(values_iter: Iterable[int]) -> str: + """ + Return a concise, human-readable string representing a sequence of integers. + + - fn((1, 2, 3)) -> '1-3' + - fn((1, 2, 3, 7, 8, 9)) -> '1-3|7-9' + - fn((1, 2, 3, 5, 7, 8, 9)) -> '1-3|5|7-9' + - fn((1, 7, 8, 9)) -> '1|7-9' + """ + values = tuple(values_iter) + if not values: + return "(empty)" + else: + return "|".join(map(_humanize_range, _extract_integer_ranges(*values))) + + +def humanize_wei(number: int) -> str: + if number >= denoms.finney: + unit = "ether" + elif number >= denoms.mwei: + unit = "gwei" + else: + unit = "wei" + amount = from_wei(number, unit) + x = f"{str(amount)} {unit}" + return x diff --git a/ccxt/static_dependencies/ethereum/utils/logging.py b/ccxt/static_dependencies/ethereum/utils/logging.py new file mode 100644 index 0000000..fd9c940 --- /dev/null +++ b/ccxt/static_dependencies/ethereum/utils/logging.py @@ -0,0 +1,159 @@ +import contextlib +from functools import ( + cached_property, +) +import logging +from typing import ( + Any, + Dict, + Iterator, + Tuple, + Type, + TypeVar, + Union, + cast, +) + +from .toolz import ( + assoc, +) + +DEBUG2_LEVEL_NUM = 8 + +TLogger = TypeVar("TLogger", bound=logging.Logger) + + +class ExtendedDebugLogger(logging.Logger): + """ + Logging class that can be used for lower level debug logging. + """ + + @cached_property + def show_debug2(self) -> bool: + return self.isEnabledFor(DEBUG2_LEVEL_NUM) + + def debug2(self, message: str, *args: Any, **kwargs: Any) -> None: + if self.show_debug2: + self.log(DEBUG2_LEVEL_NUM, message, *args, **kwargs) + else: + # When we find that `DEBUG2` isn't enabled we completely replace + # the `debug2` function in this instance of the logger with a noop + # lambda to further speed up + self.__dict__["debug2"] = lambda message, *args, **kwargs: None + + def __reduce__(self) -> Tuple[Any, ...]: + # This is needed because our parent's implementation could + # cause us to become a regular Logger on unpickling. + return get_extended_debug_logger, (self.name,) + + +def setup_DEBUG2_logging() -> None: + """ + Installs the `DEBUG2` level logging levels to the main logging module. + """ + if not hasattr(logging, "DEBUG2"): + logging.addLevelName(DEBUG2_LEVEL_NUM, "DEBUG2") + logging.DEBUG2 = DEBUG2_LEVEL_NUM # type: ignore + + +@contextlib.contextmanager +def _use_logger_class(logger_class: Type[logging.Logger]) -> Iterator[None]: + original_logger_class = logging.getLoggerClass() + logging.setLoggerClass(logger_class) + try: + yield + finally: + logging.setLoggerClass(original_logger_class) + + +def get_logger(name: str, logger_class: Union[Type[TLogger], None] = None) -> TLogger: + if logger_class is None: + return cast(TLogger, logging.getLogger(name)) + else: + with _use_logger_class(logger_class): + # The logging module caches logger instances. The following code + # ensures that if there is a cached instance that we don't + # accidentally return the incorrect logger type because the logging + # module does not *update* the cached instance in the event that + # the global logging class changes. + # + # types ignored b/c mypy doesn't identify presence of + # manager on logging.Logger + manager = logging.Logger.manager + if name in manager.loggerDict: + if type(manager.loggerDict[name]) is not logger_class: + del manager.loggerDict[name] + return cast(TLogger, logging.getLogger(name)) + + +def get_extended_debug_logger(name: str) -> ExtendedDebugLogger: + return get_logger(name, ExtendedDebugLogger) + + +THasLoggerMeta = TypeVar("THasLoggerMeta", bound="HasLoggerMeta") + + +class HasLoggerMeta(type): + """ + Assigns a logger instance to a class, derived from the import path and name. + + This metaclass uses `__qualname__` to identify a unique and meaningful name + to use when creating the associated logger for a given class. + """ + + logger_class = logging.Logger + + def __new__( + mcls: Type[THasLoggerMeta], + name: str, + bases: Tuple[Type[Any]], + namespace: Dict[str, Any], + ) -> THasLoggerMeta: + if "logger" in namespace: + # If a logger was explicitly declared we shouldn't do anything to + # replace it. + return super().__new__(mcls, name, bases, namespace) + if "__qualname__" not in namespace: + raise AttributeError("Missing __qualname__") + + with _use_logger_class(mcls.logger_class): + logger = logging.getLogger(namespace["__qualname__"]) + + return super().__new__(mcls, name, bases, assoc(namespace, "logger", logger)) + + @classmethod + def replace_logger_class( + mcls: Type[THasLoggerMeta], value: Type[logging.Logger] + ) -> Type[THasLoggerMeta]: + return type(mcls.__name__, (mcls,), {"logger_class": value}) + + @classmethod + def meta_compat( + mcls: Type[THasLoggerMeta], other: Type[type] + ) -> Type[THasLoggerMeta]: + return type(mcls.__name__, (mcls, other), {}) + + +class _BaseHasLogger(metaclass=HasLoggerMeta): + # This class exists to a allow us to define the type of the logger. Once + # python3.5 is deprecated this can be removed in favor of a simple type + # annotation on the main class. + logger = logging.Logger("") # type: logging.Logger + + +class HasLogger(_BaseHasLogger): + pass + + +HasExtendedDebugLoggerMeta = HasLoggerMeta.replace_logger_class(ExtendedDebugLogger) + + +class _BaseHasExtendedDebugLogger(metaclass=HasExtendedDebugLoggerMeta): # type: ignore + # This class exists to a allow us to define the type of the logger. Once + # python3.5 is deprecated this can be removed in favor of a simple type + # annotation on the main class. + logger = ExtendedDebugLogger("") # type: ExtendedDebugLogger + + +class HasExtendedDebugLogger(_BaseHasExtendedDebugLogger): + pass diff --git a/ccxt/static_dependencies/ethereum/utils/module_loading.py b/ccxt/static_dependencies/ethereum/utils/module_loading.py new file mode 100644 index 0000000..118addd --- /dev/null +++ b/ccxt/static_dependencies/ethereum/utils/module_loading.py @@ -0,0 +1,31 @@ +from importlib import ( + import_module, +) +from typing import ( + Any, +) + + +def import_string(dotted_path: str) -> Any: + """ + Import a variable using its path and name. + + :param dotted_path: dotted module path and variable/class name + :return: the attribute/class designated by the last name in the path + :raise: ImportError, if the import failed + + Source: django.utils.module_loading + """ + try: + module_path, class_name = dotted_path.rsplit(".", 1) + except ValueError: + msg = f"{dotted_path} doesn't look like a module path" + raise ImportError(msg) + + module = import_module(module_path) + + try: + return getattr(module, class_name) + except AttributeError: + msg = f'Module "{module_path}" does not define a "{class_name}" attribute/class' + raise ImportError(msg) diff --git a/ccxt/static_dependencies/ethereum/utils/numeric.py b/ccxt/static_dependencies/ethereum/utils/numeric.py new file mode 100644 index 0000000..7fcef98 --- /dev/null +++ b/ccxt/static_dependencies/ethereum/utils/numeric.py @@ -0,0 +1,43 @@ +from abc import ( + ABC, + abstractmethod, +) +import decimal +import numbers +from typing import ( + Any, + TypeVar, + Union, +) + + +class Comparable(ABC): + @abstractmethod + def __lt__(self, other: Any) -> bool: + ... + + @abstractmethod + def __gt__(self, other: Any) -> bool: + ... + + +TComparable = Union[Comparable, numbers.Real, int, float, decimal.Decimal] + + +TValue = TypeVar("TValue", bound=TComparable) + + +def clamp(lower_bound: TValue, upper_bound: TValue, value: TValue) -> TValue: + # The `mypy` ignore statements here are due to doing a comparison of + # `Union` types which isn't allowed. (per cburgdorf). This approach was + # chosen over using `typing.overload` to define multiple signatures for + # each comparison type here since the added value of "proper" typing + # doesn't seem to justify the complexity of having a bunch of different + # signatures defined. The external library perspective on this function + # should still be adequate under this approach + if value < lower_bound: # type: ignore + return lower_bound + elif value > upper_bound: # type: ignore + return upper_bound + else: + return value diff --git a/ccxt/static_dependencies/ethereum/utils/py.typed b/ccxt/static_dependencies/ethereum/utils/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/ccxt/static_dependencies/ethereum/utils/toolz.py b/ccxt/static_dependencies/ethereum/utils/toolz.py new file mode 100644 index 0000000..e42a2dd --- /dev/null +++ b/ccxt/static_dependencies/ethereum/utils/toolz.py @@ -0,0 +1,76 @@ +from ...toolz import ( # noqa: F401 + accumulate, + assoc, + assoc_in, + comp, + complement, + compose, + concat, + concatv, + cons, + count, + countby, + curried, + curry, + dicttoolz, + diff, + dissoc, + do, + drop, + excepts, + filter, + first, + flip, + frequencies, + functoolz, + get, + get_in, + groupby, + identity, + interleave, + interpose, + isdistinct, + isiterable, + itemfilter, + itemmap, + iterate, + itertoolz, + join, + juxt, + keyfilter, + keymap, + last, + map, + mapcat, + memoize, + merge, + merge_sorted, + merge_with, + nth, + partial, + partition, + partition_all, + partitionby, + peek, + pipe, + pluck, + random_sample, + recipes, + reduce, + reduceby, + remove, + second, + sliding_window, + sorted, + tail, + take, + take_nth, + thread_first, + thread_last, + topk, + unique, + update_in, + utils, + valfilter, + valmap, +) diff --git a/ccxt/static_dependencies/ethereum/utils/types.py b/ccxt/static_dependencies/ethereum/utils/types.py new file mode 100644 index 0000000..69bc844 --- /dev/null +++ b/ccxt/static_dependencies/ethereum/utils/types.py @@ -0,0 +1,54 @@ +import collections.abc +import numbers +from typing import ( + Any, +) + +bytes_types = (bytes, bytearray) +integer_types = (int,) +text_types = (str,) +string_types = (bytes, str, bytearray) + + +def is_integer(value: Any) -> bool: + return isinstance(value, integer_types) and not isinstance(value, bool) + + +def is_bytes(value: Any) -> bool: + return isinstance(value, bytes_types) + + +def is_text(value: Any) -> bool: + return isinstance(value, text_types) + + +def is_string(value: Any) -> bool: + return isinstance(value, string_types) + + +def is_boolean(value: Any) -> bool: + return isinstance(value, bool) + + +def is_dict(obj: Any) -> bool: + return isinstance(obj, collections.abc.Mapping) + + +def is_list_like(obj: Any) -> bool: + return not is_string(obj) and isinstance(obj, collections.abc.Sequence) + + +def is_list(obj: Any) -> bool: + return isinstance(obj, list) + + +def is_tuple(obj: Any) -> bool: + return isinstance(obj, tuple) + + +def is_null(obj: Any) -> bool: + return obj is None + + +def is_number(obj: Any) -> bool: + return isinstance(obj, numbers.Number) diff --git a/ccxt/static_dependencies/ethereum/utils/typing/__init__.py b/ccxt/static_dependencies/ethereum/utils/typing/__init__.py new file mode 100644 index 0000000..bc8c488 --- /dev/null +++ b/ccxt/static_dependencies/ethereum/utils/typing/__init__.py @@ -0,0 +1,18 @@ +import warnings + +from .misc import ( + Address, + AnyAddress, + ChecksumAddress, + HexAddress, + HexStr, + Primitives, + T, +) + +warnings.warn( + "The eth_utils.typing module will be deprecated in favor " + "of eth-typing in the next major version bump.", + category=DeprecationWarning, + stacklevel=2, +) diff --git a/ccxt/static_dependencies/ethereum/utils/typing/misc.py b/ccxt/static_dependencies/ethereum/utils/typing/misc.py new file mode 100644 index 0000000..6ab3f89 --- /dev/null +++ b/ccxt/static_dependencies/ethereum/utils/typing/misc.py @@ -0,0 +1,14 @@ +from typing import ( + TypeVar, +) + +from ...typing import ( # noqa: F401 + Address, + AnyAddress, + ChecksumAddress, + HexAddress, + HexStr, + Primitives, +) + +T = TypeVar("T") diff --git a/ccxt/static_dependencies/ethereum/utils/units.py b/ccxt/static_dependencies/ethereum/utils/units.py new file mode 100644 index 0000000..8848b89 --- /dev/null +++ b/ccxt/static_dependencies/ethereum/utils/units.py @@ -0,0 +1,31 @@ +import decimal + +# Units are in their own module here, so that they can keep this +# formatting, as this module is excluded from black in pyproject.toml +# fmt: off +units = { + 'wei': decimal.Decimal('1'), # noqa: E241 + 'kwei': decimal.Decimal('1000'), # noqa: E241 + 'babbage': decimal.Decimal('1000'), # noqa: E241 + 'femtoether': decimal.Decimal('1000'), # noqa: E241 + 'mwei': decimal.Decimal('1000000'), # noqa: E241 + 'lovelace': decimal.Decimal('1000000'), # noqa: E241 + 'picoether': decimal.Decimal('1000000'), # noqa: E241 + 'gwei': decimal.Decimal('1000000000'), # noqa: E241 + 'shannon': decimal.Decimal('1000000000'), # noqa: E241 + 'nanoether': decimal.Decimal('1000000000'), # noqa: E241 + 'nano': decimal.Decimal('1000000000'), # noqa: E241 + 'szabo': decimal.Decimal('1000000000000'), # noqa: E241 + 'microether': decimal.Decimal('1000000000000'), # noqa: E241 + 'micro': decimal.Decimal('1000000000000'), # noqa: E241 + 'finney': decimal.Decimal('1000000000000000'), # noqa: E241 + 'milliether': decimal.Decimal('1000000000000000'), # noqa: E241 + 'milli': decimal.Decimal('1000000000000000'), # noqa: E241 + 'ether': decimal.Decimal('1000000000000000000'), # noqa: E241 + 'kether': decimal.Decimal('1000000000000000000000'), # noqa: E241 + 'grand': decimal.Decimal('1000000000000000000000'), # noqa: E241 + 'mether': decimal.Decimal('1000000000000000000000000'), # noqa: E241 + 'gether': decimal.Decimal('1000000000000000000000000000'), # noqa: E241 + 'tether': decimal.Decimal('1000000000000000000000000000000'), # noqa: E241 +} +# fmt: on diff --git a/ccxt/static_dependencies/keccak/__init__.py b/ccxt/static_dependencies/keccak/__init__.py new file mode 100644 index 0000000..78143ac --- /dev/null +++ b/ccxt/static_dependencies/keccak/__init__.py @@ -0,0 +1,3 @@ +from .keccak import SHA3 + +__all__ = ['SHA3'] diff --git a/ccxt/static_dependencies/keccak/__pycache__/__init__.cpython-311.pyc b/ccxt/static_dependencies/keccak/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..924662c Binary files /dev/null and b/ccxt/static_dependencies/keccak/__pycache__/__init__.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/keccak/__pycache__/keccak.cpython-311.pyc b/ccxt/static_dependencies/keccak/__pycache__/keccak.cpython-311.pyc new file mode 100644 index 0000000..24c7159 Binary files /dev/null and b/ccxt/static_dependencies/keccak/__pycache__/keccak.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/keccak/keccak.py b/ccxt/static_dependencies/keccak/keccak.py new file mode 100644 index 0000000..2621eac --- /dev/null +++ b/ccxt/static_dependencies/keccak/keccak.py @@ -0,0 +1,197 @@ +# code modified from https://github.com/zambonin/alice-and-bob +# thank you @zambonin for your contributions to the open source code that is used by ccxt! + +"""keccakf1600.py + +Keccak is a family of hash functions based on the sponge construction. It was +chosen by NIST to become the SHA-3 standard. This code implements the +Keccak-f[1600] permutation. Detailed information about this function can be +found on the official site [1]. Original implementation [2] by the Keccak Team. + +Some caveats about the implementation: + * width `b` of permutation is fixed as 1600 (5 * 5 * 64), which means + the number of rounds is 24 (12 + 2ℓ, ℓ = log_2(b / 25)) + * ρ step could have its offsets pre-computed as the array `r`. + * ι step could have its round constants pre-computed as the array `RC`. + +[1] http://keccak.noekeon.org/ +[2] https://git.io/vKfkb +""" + + +def keccak_f_1600(state): + """The inner permutation for the Keccak sponge function. + + The Keccak-f permutation is an iterated construction consisting of a + sequence of almost identical rounds. It operates on a state array with + each of the twenty-four rounds performing five steps, described below + with detail. + + The loops above and below the core of the permutation are used to save and + restore the state array to a stream of bytes, used outside the permutation. + The original state array has three dimensions, whereas this characteristic + can be cleverly optimized to a 5x5 matrix with 64-bit words. As such, this + implementation makes use of this trick and stores an entire lane (a z-axis + set of bits within the state) as a single word. + + The θ step diffuses the bits alongside the state array by calculating the + parity of nearby columns relative to a lane. + + The ρ and π steps are merged; together, they move more bits around + according to two alike recurrence relations. + + The χ step is similar to an S-box permutation; it makes the whole round + non-linear with a few logic operations on bits inside a line. + + The ι step is a simple LFSR that breaks the symmetry of the rounds. It + generates constants by doing computations according to the round number + and its previous output, modulo polynomials over GF(2)[x]. + + Args: + state: square matrix of order 5 that holds the input bytes. + + Returns: + state: bytes permuted by Keccak-f[1600]. + """ + + def load64(b): + """ + Saves each byte on its respective position within a 64-bit word. + + Args: + b: partial list of bytes from input. + + Returns: + Sum of list with numbers shifted. + """ + return sum((b[i] << (8 * i)) for i in range(8)) + + def store64(a): + """ + Transforms a 64-bit word into a list of bytes. + + Args: + a: 64-bit word. + + Returns: + List of bytes separated by position on the word. + """ + return list((a >> (8 * i)) % 256 for i in range(8)) + + def rotate(a, n): + """ + Denotes the bitwise cyclic shift operation, moving bit at position + `i` into position `i + n` (modulo the lane size). + + Args: + a: lane with a 64-bit word, or elements from the state array. + n: offset for rotation. + + Returns: + The rotated lane. + """ + return ((a >> (64 - (n % 64))) + (a << (n % 64))) % (1 << 64) + + A = [[0 for _ in range(5)] for _ in range(5)] + for x in range(5): + for y in range(5): + i = 8 * (x + 5 * y) + A[x][y] = load64(state[i : i + 8]) + + R = 1 + for _ in range(24): + C = [A[x][0] ^ A[x][1] ^ A[x][2] ^ A[x][3] ^ A[x][4] for x in range(5)] + D = [C[(x - 1) % 5] ^ rotate(C[(x + 1) % 5], 1) for x in range(5)] + A = [[A[x][y] ^ D[x] for y in range(5)] for x in range(5)] + + x, y, current = 1, 0, A[1][0] + for t in range(24): + x, y = y, (2 * x + 3 * y) % 5 + offset = ((t + 1) * (t + 2)) // 2 + current, A[x][y] = A[x][y], rotate(current, offset) + + for y in range(5): + T = [A[x][y] for x in range(5)] + for x in range(5): + A[x][y] = T[x] ^ ((~T[(x + 1) % 5]) & T[(x + 2) % 5]) + + for j in range(7): + R = ((R << 1) ^ ((R >> 7) * 0x71)) % 256 + if R & 2: + A[0][0] ^= 1 << ((1 << j) - 1) + + for x in range(5): + for y in range(5): + i = 8 * (x + 5 * y) + state[i : i + 8] = store64(A[x][y]) + + return state + + +def Keccak(r, c, _input, suffix, output_len): + """ + The general sponge function, consisting of the inner permutation and a + padding rule (`pad10*1`). It consists of three main parts. + * absorbing, where the input will be permuted repeatedly every time + it is divided into a block of size `r + c`. + * padding, where an oddly sized last block will be filled with bits + until it can be fed into the permutation. + * squeezing, where the output's blocks will be permuted more times + until they are concatenated to the desired size. + + Args: + r: rate, or the number of input bits processed or output bits + generated per invocation of the underlying function + c: capacity, or the width of the underlying function minus + the rate + _input: list of bytes containing the desired object to be hashed + suffix: distinguishes the inputs arising from SHA-3/SHAKE functions + output_len: length of hash output. + + Returns: + Hash of the input bytes. + """ + state = bytearray((r + c) // 8) + rate_bytes, block, offset = r // 8, 0, 0 + + while offset < len(_input): + block = min(len(_input) - offset, rate_bytes) + for i in range(block): + state[i] ^= _input[i + offset] + offset += block + if block == rate_bytes: + state = keccak_f_1600(state) + block = 0 + + state[block] ^= suffix + if (suffix & 0x80) and (block == (rate_bytes - 1)): + state = keccak_f_1600(state) + state[rate_bytes - 1] ^= 0x80 + state = keccak_f_1600(state) + + output = bytearray() + while output_len: + block = min(output_len, rate_bytes) + output += state[:block] + output_len -= block + if output_len: + state = keccak_f_1600(state) + + return output + + +def SHA3(_input): + """ + FIPS 202 generalized instance of the SHA-3 hash function. + + Args: + size: instance of desired SHA3 algorithm. + _input: list of bytes to compute a hash from. + + Returns: + Instance of the Keccak permutation that calculates the hash. + """ + size = 256 + # https://www.cybertest.com/blog/keccak-vs-sha3 + padding = 0x01 # change this to 0x06 for NIST sha3 + return Keccak(1600 - size * 2, size * 2, _input, padding, size // 8) diff --git a/ccxt/static_dependencies/lark/__init__.py b/ccxt/static_dependencies/lark/__init__.py new file mode 100644 index 0000000..4e4b551 --- /dev/null +++ b/ccxt/static_dependencies/lark/__init__.py @@ -0,0 +1,38 @@ +from .exceptions import ( + GrammarError, + LarkError, + LexError, + ParseError, + UnexpectedCharacters, + UnexpectedEOF, + UnexpectedInput, + UnexpectedToken, +) +from .lark import Lark +from .lexer import Token +from .tree import ParseTree, Tree +from .utils import logger +from .visitors import Discard, Transformer, Transformer_NonRecursive, Visitor, v_args + +__version__: str = "1.2.0" + +__all__ = ( + "GrammarError", + "LarkError", + "LexError", + "ParseError", + "UnexpectedCharacters", + "UnexpectedEOF", + "UnexpectedInput", + "UnexpectedToken", + "Lark", + "Token", + "ParseTree", + "Tree", + "logger", + "Discard", + "Transformer", + "Transformer_NonRecursive", + "Visitor", + "v_args", +) diff --git a/ccxt/static_dependencies/lark/__pyinstaller/__init__.py b/ccxt/static_dependencies/lark/__pyinstaller/__init__.py new file mode 100644 index 0000000..9da62a3 --- /dev/null +++ b/ccxt/static_dependencies/lark/__pyinstaller/__init__.py @@ -0,0 +1,6 @@ +# For usage of lark with PyInstaller. See https://pyinstaller-sample-hook.readthedocs.io/en/latest/index.html + +import os + +def get_hook_dirs(): + return [os.path.dirname(__file__)] diff --git a/ccxt/static_dependencies/lark/__pyinstaller/hook-lark.py b/ccxt/static_dependencies/lark/__pyinstaller/hook-lark.py new file mode 100644 index 0000000..cf3d8e3 --- /dev/null +++ b/ccxt/static_dependencies/lark/__pyinstaller/hook-lark.py @@ -0,0 +1,14 @@ +#----------------------------------------------------------------------------- +# Copyright (c) 2017-2020, PyInstaller Development Team. +# +# Distributed under the terms of the GNU General Public License (version 2 +# or later) with exception for distributing the bootloader. +# +# The full license is in the file COPYING.txt, distributed with this software. +# +# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception) +#----------------------------------------------------------------------------- + +from PyInstaller.utils.hooks import collect_data_files + +datas = collect_data_files('lark') diff --git a/ccxt/static_dependencies/lark/ast_utils.py b/ccxt/static_dependencies/lark/ast_utils.py new file mode 100644 index 0000000..a5460f3 --- /dev/null +++ b/ccxt/static_dependencies/lark/ast_utils.py @@ -0,0 +1,59 @@ +""" + Module of utilities for transforming a lark.Tree into a custom Abstract Syntax Tree (AST defined in classes) +""" + +import inspect, re +import types +from typing import Optional, Callable + +from lark import Transformer, v_args + +class Ast: + """Abstract class + + Subclasses will be collected by `create_transformer()` + """ + pass + +class AsList: + """Abstract class + + Subclasses will be instantiated with the parse results as a single list, instead of as arguments. + """ + +class WithMeta: + """Abstract class + + Subclasses will be instantiated with the Meta instance of the tree. (see ``v_args`` for more detail) + """ + pass + +def camel_to_snake(name): + return re.sub(r'(? Transformer: + """Collects `Ast` subclasses from the given module, and creates a Lark transformer that builds the AST. + + For each class, we create a corresponding rule in the transformer, with a matching name. + CamelCase names will be converted into snake_case. Example: "CodeBlock" -> "code_block". + + Classes starting with an underscore (`_`) will be skipped. + + Parameters: + ast_module: A Python module containing all the subclasses of ``ast_utils.Ast`` + transformer (Optional[Transformer]): An initial transformer. Its attributes may be overwritten. + decorator_factory (Callable): An optional callable accepting two booleans, inline, and meta, + and returning a decorator for the methods of ``transformer``. (default: ``v_args``). + """ + t = transformer or Transformer() + + for name, obj in inspect.getmembers(ast_module): + if not name.startswith('_') and inspect.isclass(obj): + if issubclass(obj, Ast): + wrapper = decorator_factory(inline=not issubclass(obj, AsList), meta=issubclass(obj, WithMeta)) + obj = wrapper(obj).__get__(t) + setattr(t, camel_to_snake(name), obj) + + return t diff --git a/ccxt/static_dependencies/lark/common.py b/ccxt/static_dependencies/lark/common.py new file mode 100644 index 0000000..71b6a4c --- /dev/null +++ b/ccxt/static_dependencies/lark/common.py @@ -0,0 +1,86 @@ +from copy import deepcopy +import sys +from types import ModuleType +from typing import Callable, Collection, Dict, Optional, TYPE_CHECKING, List + +if TYPE_CHECKING: + from .lark import PostLex + from .lexer import Lexer + from .grammar import Rule + from typing import Union, Type + from typing import Literal + if sys.version_info >= (3, 10): + from typing import TypeAlias + else: + from typing_extensions import TypeAlias + +from .utils import Serialize +from .lexer import TerminalDef, Token + +###{standalone + +_ParserArgType: 'TypeAlias' = 'Literal["earley", "lalr", "cyk", "auto"]' +_LexerArgType: 'TypeAlias' = 'Union[Literal["auto", "basic", "contextual", "dynamic", "dynamic_complete"], Type[Lexer]]' +_LexerCallback = Callable[[Token], Token] +ParserCallbacks = Dict[str, Callable] + +class LexerConf(Serialize): + __serialize_fields__ = 'terminals', 'ignore', 'g_regex_flags', 'use_bytes', 'lexer_type' + __serialize_namespace__ = TerminalDef, + + terminals: Collection[TerminalDef] + re_module: ModuleType + ignore: Collection[str] + postlex: 'Optional[PostLex]' + callbacks: Dict[str, _LexerCallback] + g_regex_flags: int + skip_validation: bool + use_bytes: bool + lexer_type: Optional[_LexerArgType] + strict: bool + + def __init__(self, terminals: Collection[TerminalDef], re_module: ModuleType, ignore: Collection[str]=(), postlex: 'Optional[PostLex]'=None, + callbacks: Optional[Dict[str, _LexerCallback]]=None, g_regex_flags: int=0, skip_validation: bool=False, use_bytes: bool=False, strict: bool=False): + self.terminals = terminals + self.terminals_by_name = {t.name: t for t in self.terminals} + assert len(self.terminals) == len(self.terminals_by_name) + self.ignore = ignore + self.postlex = postlex + self.callbacks = callbacks or {} + self.g_regex_flags = g_regex_flags + self.re_module = re_module + self.skip_validation = skip_validation + self.use_bytes = use_bytes + self.strict = strict + self.lexer_type = None + + def _deserialize(self): + self.terminals_by_name = {t.name: t for t in self.terminals} + + def __deepcopy__(self, memo=None): + return type(self)( + deepcopy(self.terminals, memo), + self.re_module, + deepcopy(self.ignore, memo), + deepcopy(self.postlex, memo), + deepcopy(self.callbacks, memo), + deepcopy(self.g_regex_flags, memo), + deepcopy(self.skip_validation, memo), + deepcopy(self.use_bytes, memo), + ) + +class ParserConf(Serialize): + __serialize_fields__ = 'rules', 'start', 'parser_type' + + rules: List['Rule'] + callbacks: ParserCallbacks + start: List[str] + parser_type: _ParserArgType + + def __init__(self, rules: List['Rule'], callbacks: ParserCallbacks, start: List[str]): + assert isinstance(start, list) + self.rules = rules + self.callbacks = callbacks + self.start = start + +###} diff --git a/ccxt/static_dependencies/lark/exceptions.py b/ccxt/static_dependencies/lark/exceptions.py new file mode 100644 index 0000000..e099d59 --- /dev/null +++ b/ccxt/static_dependencies/lark/exceptions.py @@ -0,0 +1,292 @@ +from .utils import logger, NO_VALUE +from typing import Mapping, Iterable, Callable, Union, TypeVar, Tuple, Any, List, Set, Optional, Collection, TYPE_CHECKING + +if TYPE_CHECKING: + from .lexer import Token + from .parsers.lalr_interactive_parser import InteractiveParser + from .tree import Tree + +###{standalone + +class LarkError(Exception): + pass + + +class ConfigurationError(LarkError, ValueError): + pass + + +def assert_config(value, options: Collection, msg='Got %r, expected one of %s'): + if value not in options: + raise ConfigurationError(msg % (value, options)) + + +class GrammarError(LarkError): + pass + + +class ParseError(LarkError): + pass + + +class LexError(LarkError): + pass + +T = TypeVar('T') + +class UnexpectedInput(LarkError): + """UnexpectedInput Error. + + Used as a base class for the following exceptions: + + - ``UnexpectedCharacters``: The lexer encountered an unexpected string + - ``UnexpectedToken``: The parser received an unexpected token + - ``UnexpectedEOF``: The parser expected a token, but the input ended + + After catching one of these exceptions, you may call the following helper methods to create a nicer error message. + """ + line: int + column: int + pos_in_stream = None + state: Any + _terminals_by_name = None + interactive_parser: 'InteractiveParser' + + def get_context(self, text: str, span: int=40) -> str: + """Returns a pretty string pinpointing the error in the text, + with span amount of context characters around it. + + Note: + The parser doesn't hold a copy of the text it has to parse, + so you have to provide it again + """ + assert self.pos_in_stream is not None, self + pos = self.pos_in_stream + start = max(pos - span, 0) + end = pos + span + if not isinstance(text, bytes): + before = text[start:pos].rsplit('\n', 1)[-1] + after = text[pos:end].split('\n', 1)[0] + return before + after + '\n' + ' ' * len(before.expandtabs()) + '^\n' + else: + before = text[start:pos].rsplit(b'\n', 1)[-1] + after = text[pos:end].split(b'\n', 1)[0] + return (before + after + b'\n' + b' ' * len(before.expandtabs()) + b'^\n').decode("ascii", "backslashreplace") + + def match_examples(self, parse_fn: 'Callable[[str], Tree]', + examples: Union[Mapping[T, Iterable[str]], Iterable[Tuple[T, Iterable[str]]]], + token_type_match_fallback: bool=False, + use_accepts: bool=True + ) -> Optional[T]: + """Allows you to detect what's wrong in the input text by matching + against example errors. + + Given a parser instance and a dictionary mapping some label with + some malformed syntax examples, it'll return the label for the + example that bests matches the current error. The function will + iterate the dictionary until it finds a matching error, and + return the corresponding value. + + For an example usage, see `examples/error_reporting_lalr.py` + + Parameters: + parse_fn: parse function (usually ``lark_instance.parse``) + examples: dictionary of ``{'example_string': value}``. + use_accepts: Recommended to keep this as ``use_accepts=True``. + """ + assert self.state is not None, "Not supported for this exception" + + if isinstance(examples, Mapping): + examples = examples.items() + + candidate = (None, False) + for i, (label, example) in enumerate(examples): + assert not isinstance(example, str), "Expecting a list" + + for j, malformed in enumerate(example): + try: + parse_fn(malformed) + except UnexpectedInput as ut: + if ut.state == self.state: + if ( + use_accepts + and isinstance(self, UnexpectedToken) + and isinstance(ut, UnexpectedToken) + and ut.accepts != self.accepts + ): + logger.debug("Different accepts with same state[%d]: %s != %s at example [%s][%s]" % + (self.state, self.accepts, ut.accepts, i, j)) + continue + if ( + isinstance(self, (UnexpectedToken, UnexpectedEOF)) + and isinstance(ut, (UnexpectedToken, UnexpectedEOF)) + ): + if ut.token == self.token: # Try exact match first + logger.debug("Exact Match at example [%s][%s]" % (i, j)) + return label + + if token_type_match_fallback: + # Fallback to token types match + if (ut.token.type == self.token.type) and not candidate[-1]: + logger.debug("Token Type Fallback at example [%s][%s]" % (i, j)) + candidate = label, True + + if candidate[0] is None: + logger.debug("Same State match at example [%s][%s]" % (i, j)) + candidate = label, False + + return candidate[0] + + def _format_expected(self, expected): + if self._terminals_by_name: + d = self._terminals_by_name + expected = [d[t_name].user_repr() if t_name in d else t_name for t_name in expected] + return "Expected one of: \n\t* %s\n" % '\n\t* '.join(expected) + + +class UnexpectedEOF(ParseError, UnexpectedInput): + """An exception that is raised by the parser, when the input ends while it still expects a token. + """ + expected: 'List[Token]' + + def __init__(self, expected, state=None, terminals_by_name=None): + super(UnexpectedEOF, self).__init__() + + self.expected = expected + self.state = state + from .lexer import Token + self.token = Token("", "") # , line=-1, column=-1, pos_in_stream=-1) + self.pos_in_stream = -1 + self.line = -1 + self.column = -1 + self._terminals_by_name = terminals_by_name + + + def __str__(self): + message = "Unexpected end-of-input. " + message += self._format_expected(self.expected) + return message + + +class UnexpectedCharacters(LexError, UnexpectedInput): + """An exception that is raised by the lexer, when it cannot match the next + string of characters to any of its terminals. + """ + + allowed: Set[str] + considered_tokens: Set[Any] + + def __init__(self, seq, lex_pos, line, column, allowed=None, considered_tokens=None, state=None, token_history=None, + terminals_by_name=None, considered_rules=None): + super(UnexpectedCharacters, self).__init__() + + # TODO considered_tokens and allowed can be figured out using state + self.line = line + self.column = column + self.pos_in_stream = lex_pos + self.state = state + self._terminals_by_name = terminals_by_name + + self.allowed = allowed + self.considered_tokens = considered_tokens + self.considered_rules = considered_rules + self.token_history = token_history + + if isinstance(seq, bytes): + self.char = seq[lex_pos:lex_pos + 1].decode("ascii", "backslashreplace") + else: + self.char = seq[lex_pos] + self._context = self.get_context(seq) + + + def __str__(self): + message = "No terminal matches '%s' in the current parser context, at line %d col %d" % (self.char, self.line, self.column) + message += '\n\n' + self._context + if self.allowed: + message += self._format_expected(self.allowed) + if self.token_history: + message += '\nPrevious tokens: %s\n' % ', '.join(repr(t) for t in self.token_history) + return message + + +class UnexpectedToken(ParseError, UnexpectedInput): + """An exception that is raised by the parser, when the token it received + doesn't match any valid step forward. + + Parameters: + token: The mismatched token + expected: The set of expected tokens + considered_rules: Which rules were considered, to deduce the expected tokens + state: A value representing the parser state. Do not rely on its value or type. + interactive_parser: An instance of ``InteractiveParser``, that is initialized to the point of failure, + and can be used for debugging and error handling. + + Note: These parameters are available as attributes of the instance. + """ + + expected: Set[str] + considered_rules: Set[str] + + def __init__(self, token, expected, considered_rules=None, state=None, interactive_parser=None, terminals_by_name=None, token_history=None): + super(UnexpectedToken, self).__init__() + + # TODO considered_rules and expected can be figured out using state + self.line = getattr(token, 'line', '?') + self.column = getattr(token, 'column', '?') + self.pos_in_stream = getattr(token, 'start_pos', None) + self.state = state + + self.token = token + self.expected = expected # XXX deprecate? `accepts` is better + self._accepts = NO_VALUE + self.considered_rules = considered_rules + self.interactive_parser = interactive_parser + self._terminals_by_name = terminals_by_name + self.token_history = token_history + + + @property + def accepts(self) -> Set[str]: + if self._accepts is NO_VALUE: + self._accepts = self.interactive_parser and self.interactive_parser.accepts() + return self._accepts + + def __str__(self): + message = ("Unexpected token %r at line %s, column %s.\n%s" + % (self.token, self.line, self.column, self._format_expected(self.accepts or self.expected))) + if self.token_history: + message += "Previous tokens: %r\n" % self.token_history + + return message + + + +class VisitError(LarkError): + """VisitError is raised when visitors are interrupted by an exception + + It provides the following attributes for inspection: + + Parameters: + rule: the name of the visit rule that failed + obj: the tree-node or token that was being processed + orig_exc: the exception that cause it to fail + + Note: These parameters are available as attributes + """ + + obj: 'Union[Tree, Token]' + orig_exc: Exception + + def __init__(self, rule, obj, orig_exc): + message = 'Error trying to process rule "%s":\n\n%s' % (rule, orig_exc) + super(VisitError, self).__init__(message) + + self.rule = rule + self.obj = obj + self.orig_exc = orig_exc + + +class MissingVariableError(LarkError): + pass + +###} diff --git a/ccxt/static_dependencies/lark/grammar.py b/ccxt/static_dependencies/lark/grammar.py new file mode 100644 index 0000000..1d226d9 --- /dev/null +++ b/ccxt/static_dependencies/lark/grammar.py @@ -0,0 +1,130 @@ +from typing import Optional, Tuple, ClassVar, Sequence + +from .utils import Serialize + +###{standalone +TOKEN_DEFAULT_PRIORITY = 0 + + +class Symbol(Serialize): + __slots__ = ('name',) + + name: str + is_term: ClassVar[bool] = NotImplemented + + def __init__(self, name: str) -> None: + self.name = name + + def __eq__(self, other): + assert isinstance(other, Symbol), other + return self.is_term == other.is_term and self.name == other.name + + def __ne__(self, other): + return not (self == other) + + def __hash__(self): + return hash(self.name) + + def __repr__(self): + return '%s(%r)' % (type(self).__name__, self.name) + + fullrepr = property(__repr__) + + def renamed(self, f): + return type(self)(f(self.name)) + + +class Terminal(Symbol): + __serialize_fields__ = 'name', 'filter_out' + + is_term: ClassVar[bool] = True + + def __init__(self, name, filter_out=False): + self.name = name + self.filter_out = filter_out + + @property + def fullrepr(self): + return '%s(%r, %r)' % (type(self).__name__, self.name, self.filter_out) + + def renamed(self, f): + return type(self)(f(self.name), self.filter_out) + + +class NonTerminal(Symbol): + __serialize_fields__ = 'name', + + is_term: ClassVar[bool] = False + + +class RuleOptions(Serialize): + __serialize_fields__ = 'keep_all_tokens', 'expand1', 'priority', 'template_source', 'empty_indices' + + keep_all_tokens: bool + expand1: bool + priority: Optional[int] + template_source: Optional[str] + empty_indices: Tuple[bool, ...] + + def __init__(self, keep_all_tokens: bool=False, expand1: bool=False, priority: Optional[int]=None, template_source: Optional[str]=None, empty_indices: Tuple[bool, ...]=()) -> None: + self.keep_all_tokens = keep_all_tokens + self.expand1 = expand1 + self.priority = priority + self.template_source = template_source + self.empty_indices = empty_indices + + def __repr__(self): + return 'RuleOptions(%r, %r, %r, %r)' % ( + self.keep_all_tokens, + self.expand1, + self.priority, + self.template_source + ) + + +class Rule(Serialize): + """ + origin : a symbol + expansion : a list of symbols + order : index of this expansion amongst all rules of the same name + """ + __slots__ = ('origin', 'expansion', 'alias', 'options', 'order', '_hash') + + __serialize_fields__ = 'origin', 'expansion', 'order', 'alias', 'options' + __serialize_namespace__ = Terminal, NonTerminal, RuleOptions + + origin: NonTerminal + expansion: Sequence[Symbol] + order: int + alias: Optional[str] + options: RuleOptions + _hash: int + + def __init__(self, origin: NonTerminal, expansion: Sequence[Symbol], + order: int=0, alias: Optional[str]=None, options: Optional[RuleOptions]=None): + self.origin = origin + self.expansion = expansion + self.alias = alias + self.order = order + self.options = options or RuleOptions() + self._hash = hash((self.origin, tuple(self.expansion))) + + def _deserialize(self): + self._hash = hash((self.origin, tuple(self.expansion))) + + def __str__(self): + return '<%s : %s>' % (self.origin.name, ' '.join(x.name for x in self.expansion)) + + def __repr__(self): + return 'Rule(%r, %r, %r, %r)' % (self.origin, self.expansion, self.alias, self.options) + + def __hash__(self): + return self._hash + + def __eq__(self, other): + if not isinstance(other, Rule): + return False + return self.origin == other.origin and self.expansion == other.expansion + + +###} diff --git a/ccxt/static_dependencies/lark/grammars/__init__.py b/ccxt/static_dependencies/lark/grammars/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ccxt/static_dependencies/lark/grammars/common.lark b/ccxt/static_dependencies/lark/grammars/common.lark new file mode 100644 index 0000000..d2e86d1 --- /dev/null +++ b/ccxt/static_dependencies/lark/grammars/common.lark @@ -0,0 +1,59 @@ +// Basic terminals for common use + + +// +// Numbers +// + +DIGIT: "0".."9" +HEXDIGIT: "a".."f"|"A".."F"|DIGIT + +INT: DIGIT+ +SIGNED_INT: ["+"|"-"] INT +DECIMAL: INT "." INT? | "." INT + +// float = /-?\d+(\.\d+)?([eE][+-]?\d+)?/ +_EXP: ("e"|"E") SIGNED_INT +FLOAT: INT _EXP | DECIMAL _EXP? +SIGNED_FLOAT: ["+"|"-"] FLOAT + +NUMBER: FLOAT | INT +SIGNED_NUMBER: ["+"|"-"] NUMBER + +// +// Strings +// +_STRING_INNER: /.*?/ +_STRING_ESC_INNER: _STRING_INNER /(? ignore + | "%import" import_path ["->" name] -> import + | "%import" import_path name_list -> multi_import + | "%override" rule -> override_rule + | "%declare" name+ -> declare + +!import_path: "."? name ("." name)* +name_list: "(" name ("," name)* ")" + +?expansions: alias (_VBAR alias)* + +?alias: expansion ["->" RULE] + +?expansion: expr* + +?expr: atom [OP | "~" NUMBER [".." NUMBER]] + +?atom: "(" expansions ")" + | "[" expansions "]" -> maybe + | value + +?value: STRING ".." STRING -> literal_range + | name + | (REGEXP | STRING) -> literal + | name "{" value ("," value)* "}" -> template_usage + +name: RULE + | TOKEN + +_VBAR: _NL? "|" +OP: /[+*]|[?](?![a-z])/ +RULE: /!?[_?]?[a-z][_a-z0-9]*/ +TOKEN: /_?[A-Z][_A-Z0-9]*/ +STRING: _STRING "i"? +REGEXP: /\/(?!\/)(\\\/|\\\\|[^\/])*?\/[imslux]*/ +_NL: /(\r?\n)+\s*/ + +%import common.ESCAPED_STRING -> _STRING +%import common.SIGNED_INT -> NUMBER +%import common.WS_INLINE + +COMMENT: /\s*/ "//" /[^\n]/* | /\s*/ "#" /[^\n]/* + +%ignore WS_INLINE +%ignore COMMENT diff --git a/ccxt/static_dependencies/lark/grammars/python.lark b/ccxt/static_dependencies/lark/grammars/python.lark new file mode 100644 index 0000000..8a75966 --- /dev/null +++ b/ccxt/static_dependencies/lark/grammars/python.lark @@ -0,0 +1,302 @@ +// Python 3 grammar for Lark + +// This grammar should parse all python 3.x code successfully. + +// Adapted from: https://docs.python.org/3/reference/grammar.html + +// Start symbols for the grammar: +// single_input is a single interactive statement; +// file_input is a module or sequence of commands read from an input file; +// eval_input is the input for the eval() functions. +// NB: compound_stmt in single_input is followed by extra NEWLINE! +// + +single_input: _NEWLINE | simple_stmt | compound_stmt _NEWLINE +file_input: (_NEWLINE | stmt)* +eval_input: testlist _NEWLINE* + +decorator: "@" dotted_name [ "(" [arguments] ")" ] _NEWLINE +decorators: decorator+ +decorated: decorators (classdef | funcdef | async_funcdef) + +async_funcdef: "async" funcdef +funcdef: "def" name "(" [parameters] ")" ["->" test] ":" suite + +parameters: paramvalue ("," paramvalue)* ["," SLASH ("," paramvalue)*] ["," [starparams | kwparams]] + | starparams + | kwparams + +SLASH: "/" // Otherwise the it will completely disappear and it will be undisguisable in the result +starparams: (starparam | starguard) poststarparams +starparam: "*" typedparam +starguard: "*" +poststarparams: ("," paramvalue)* ["," kwparams] +kwparams: "**" typedparam ","? + +?paramvalue: typedparam ("=" test)? +?typedparam: name (":" test)? + + +lambdef: "lambda" [lambda_params] ":" test +lambdef_nocond: "lambda" [lambda_params] ":" test_nocond +lambda_params: lambda_paramvalue ("," lambda_paramvalue)* ["," [lambda_starparams | lambda_kwparams]] + | lambda_starparams + | lambda_kwparams +?lambda_paramvalue: name ("=" test)? +lambda_starparams: "*" [name] ("," lambda_paramvalue)* ["," [lambda_kwparams]] +lambda_kwparams: "**" name ","? + + +?stmt: simple_stmt | compound_stmt +?simple_stmt: small_stmt (";" small_stmt)* [";"] _NEWLINE +?small_stmt: (expr_stmt | assign_stmt | del_stmt | pass_stmt | flow_stmt | import_stmt | global_stmt | nonlocal_stmt | assert_stmt) +expr_stmt: testlist_star_expr +assign_stmt: annassign | augassign | assign + +annassign: testlist_star_expr ":" test ["=" test] +assign: testlist_star_expr ("=" (yield_expr|testlist_star_expr))+ +augassign: testlist_star_expr augassign_op (yield_expr|testlist) +!augassign_op: "+=" | "-=" | "*=" | "@=" | "/=" | "%=" | "&=" | "|=" | "^=" | "<<=" | ">>=" | "**=" | "//=" +?testlist_star_expr: test_or_star_expr + | test_or_star_expr ("," test_or_star_expr)+ ","? -> tuple + | test_or_star_expr "," -> tuple + +// For normal and annotated assignments, additional restrictions enforced by the interpreter +del_stmt: "del" exprlist +pass_stmt: "pass" +?flow_stmt: break_stmt | continue_stmt | return_stmt | raise_stmt | yield_stmt +break_stmt: "break" +continue_stmt: "continue" +return_stmt: "return" [testlist] +yield_stmt: yield_expr +raise_stmt: "raise" [test ["from" test]] +import_stmt: import_name | import_from +import_name: "import" dotted_as_names +// note below: the ("." | "...") is necessary because "..." is tokenized as ELLIPSIS +import_from: "from" (dots? dotted_name | dots) "import" ("*" | "(" import_as_names ")" | import_as_names) +!dots: "."+ +import_as_name: name ["as" name] +dotted_as_name: dotted_name ["as" name] +import_as_names: import_as_name ("," import_as_name)* [","] +dotted_as_names: dotted_as_name ("," dotted_as_name)* +dotted_name: name ("." name)* +global_stmt: "global" name ("," name)* +nonlocal_stmt: "nonlocal" name ("," name)* +assert_stmt: "assert" test ["," test] + +?compound_stmt: if_stmt | while_stmt | for_stmt | try_stmt | match_stmt + | with_stmt | funcdef | classdef | decorated | async_stmt +async_stmt: "async" (funcdef | with_stmt | for_stmt) +if_stmt: "if" test ":" suite elifs ["else" ":" suite] +elifs: elif_* +elif_: "elif" test ":" suite +while_stmt: "while" test ":" suite ["else" ":" suite] +for_stmt: "for" exprlist "in" testlist ":" suite ["else" ":" suite] +try_stmt: "try" ":" suite except_clauses ["else" ":" suite] [finally] + | "try" ":" suite finally -> try_finally +finally: "finally" ":" suite +except_clauses: except_clause+ +except_clause: "except" [test ["as" name]] ":" suite +// NB compile.c makes sure that the default except clause is last + + +with_stmt: "with" with_items ":" suite +with_items: with_item ("," with_item)* +with_item: test ["as" name] + +match_stmt: "match" test ":" _NEWLINE _INDENT case+ _DEDENT + +case: "case" pattern ["if" test] ":" suite + +?pattern: sequence_item_pattern "," _sequence_pattern -> sequence_pattern + | as_pattern +?as_pattern: or_pattern ("as" NAME)? +?or_pattern: closed_pattern ("|" closed_pattern)* +?closed_pattern: literal_pattern + | NAME -> capture_pattern + | "_" -> any_pattern + | attr_pattern + | "(" as_pattern ")" + | "[" _sequence_pattern "]" -> sequence_pattern + | "(" (sequence_item_pattern "," _sequence_pattern)? ")" -> sequence_pattern + | "{" (mapping_item_pattern ("," mapping_item_pattern)* ","?)?"}" -> mapping_pattern + | "{" (mapping_item_pattern ("," mapping_item_pattern)* ",")? "**" NAME ","? "}" -> mapping_star_pattern + | class_pattern + +literal_pattern: inner_literal_pattern + +?inner_literal_pattern: "None" -> const_none + | "True" -> const_true + | "False" -> const_false + | STRING -> string + | number + +attr_pattern: NAME ("." NAME)+ -> value + +name_or_attr_pattern: NAME ("." NAME)* -> value + +mapping_item_pattern: (literal_pattern|attr_pattern) ":" as_pattern + +_sequence_pattern: (sequence_item_pattern ("," sequence_item_pattern)* ","?)? +?sequence_item_pattern: as_pattern + | "*" NAME -> star_pattern + +class_pattern: name_or_attr_pattern "(" [arguments_pattern ","?] ")" +arguments_pattern: pos_arg_pattern ["," keyws_arg_pattern] + | keyws_arg_pattern -> no_pos_arguments + +pos_arg_pattern: as_pattern ("," as_pattern)* +keyws_arg_pattern: keyw_arg_pattern ("," keyw_arg_pattern)* +keyw_arg_pattern: NAME "=" as_pattern + + + +suite: simple_stmt | _NEWLINE _INDENT stmt+ _DEDENT + +?test: or_test ("if" or_test "else" test)? + | lambdef + | assign_expr + +assign_expr: name ":=" test + +?test_nocond: or_test | lambdef_nocond + +?or_test: and_test ("or" and_test)* +?and_test: not_test_ ("and" not_test_)* +?not_test_: "not" not_test_ -> not_test + | comparison +?comparison: expr (comp_op expr)* +star_expr: "*" expr + +?expr: or_expr +?or_expr: xor_expr ("|" xor_expr)* +?xor_expr: and_expr ("^" and_expr)* +?and_expr: shift_expr ("&" shift_expr)* +?shift_expr: arith_expr (_shift_op arith_expr)* +?arith_expr: term (_add_op term)* +?term: factor (_mul_op factor)* +?factor: _unary_op factor | power + +!_unary_op: "+"|"-"|"~" +!_add_op: "+"|"-" +!_shift_op: "<<"|">>" +!_mul_op: "*"|"@"|"/"|"%"|"//" +// <> isn't actually a valid comparison operator in Python. It's here for the +// sake of a __future__ import described in PEP 401 (which really works :-) +!comp_op: "<"|">"|"=="|">="|"<="|"<>"|"!="|"in"|"not" "in"|"is"|"is" "not" + +?power: await_expr ("**" factor)? +?await_expr: AWAIT? atom_expr +AWAIT: "await" + +?atom_expr: atom_expr "(" [arguments] ")" -> funccall + | atom_expr "[" subscriptlist "]" -> getitem + | atom_expr "." name -> getattr + | atom + +?atom: "(" yield_expr ")" + | "(" _tuple_inner? ")" -> tuple + | "(" comprehension{test_or_star_expr} ")" -> tuple_comprehension + | "[" _exprlist? "]" -> list + | "[" comprehension{test_or_star_expr} "]" -> list_comprehension + | "{" _dict_exprlist? "}" -> dict + | "{" comprehension{key_value} "}" -> dict_comprehension + | "{" _exprlist "}" -> set + | "{" comprehension{test} "}" -> set_comprehension + | name -> var + | number + | string_concat + | "(" test ")" + | "..." -> ellipsis + | "None" -> const_none + | "True" -> const_true + | "False" -> const_false + + +?string_concat: string+ + +_tuple_inner: test_or_star_expr (("," test_or_star_expr)+ [","] | ",") + +?test_or_star_expr: test + | star_expr + +?subscriptlist: subscript + | subscript (("," subscript)+ [","] | ",") -> subscript_tuple +?subscript: test | ([test] ":" [test] [sliceop]) -> slice +sliceop: ":" [test] +?exprlist: (expr|star_expr) + | (expr|star_expr) (("," (expr|star_expr))+ [","]|",") +?testlist: test | testlist_tuple +testlist_tuple: test (("," test)+ [","] | ",") +_dict_exprlist: (key_value | "**" expr) ("," (key_value | "**" expr))* [","] + +key_value: test ":" test + +_exprlist: test_or_star_expr ("," test_or_star_expr)* [","] + +classdef: "class" name ["(" [arguments] ")"] ":" suite + + + +arguments: argvalue ("," argvalue)* ("," [ starargs | kwargs])? + | starargs + | kwargs + | comprehension{test} + +starargs: stararg ("," stararg)* ("," argvalue)* ["," kwargs] +stararg: "*" test +kwargs: "**" test ("," argvalue)* + +?argvalue: test ("=" test)? + + +comprehension{comp_result}: comp_result comp_fors [comp_if] +comp_fors: comp_for+ +comp_for: [ASYNC] "for" exprlist "in" or_test +ASYNC: "async" +?comp_if: "if" test_nocond + +// not used in grammar, but may appear in "node" passed from Parser to Compiler +encoding_decl: name + +yield_expr: "yield" [testlist] + | "yield" "from" test -> yield_from + +number: DEC_NUMBER | HEX_NUMBER | BIN_NUMBER | OCT_NUMBER | FLOAT_NUMBER | IMAG_NUMBER +string: STRING | LONG_STRING + +// Other terminals + +_NEWLINE: ( /\r?\n[\t ]*/ | COMMENT )+ + +%ignore /[\t \f]+/ // WS +%ignore /\\[\t \f]*\r?\n/ // LINE_CONT +%ignore COMMENT +%declare _INDENT _DEDENT + + +// Python terminals + +!name: NAME | "match" | "case" +NAME: /[^\W\d]\w*/ +COMMENT: /#[^\n]*/ + +STRING: /([ubf]?r?|r[ubf])("(?!"").*?(? None: + self.paren_level = 0 + self.indent_level = [0] + assert self.tab_len > 0 + + def handle_NL(self, token: Token) -> Iterator[Token]: + if self.paren_level > 0: + return + + yield token + + indent_str = token.rsplit('\n', 1)[1] # Tabs and spaces + indent = indent_str.count(' ') + indent_str.count('\t') * self.tab_len + + if indent > self.indent_level[-1]: + self.indent_level.append(indent) + yield Token.new_borrow_pos(self.INDENT_type, indent_str, token) + else: + while indent < self.indent_level[-1]: + self.indent_level.pop() + yield Token.new_borrow_pos(self.DEDENT_type, indent_str, token) + + if indent != self.indent_level[-1]: + raise DedentError('Unexpected dedent to column %s. Expected dedent to %s' % (indent, self.indent_level[-1])) + + def _process(self, stream): + for token in stream: + if token.type == self.NL_type: + yield from self.handle_NL(token) + else: + yield token + + if token.type in self.OPEN_PAREN_types: + self.paren_level += 1 + elif token.type in self.CLOSE_PAREN_types: + self.paren_level -= 1 + assert self.paren_level >= 0 + + while len(self.indent_level) > 1: + self.indent_level.pop() + yield Token(self.DEDENT_type, '') + + assert self.indent_level == [0], self.indent_level + + def process(self, stream): + self.paren_level = 0 + self.indent_level = [0] + return self._process(stream) + + # XXX Hack for ContextualLexer. Maybe there's a more elegant solution? + @property + def always_accept(self): + return (self.NL_type,) + + @property + @abstractmethod + def NL_type(self) -> str: + "The name of the newline token" + raise NotImplementedError() + + @property + @abstractmethod + def OPEN_PAREN_types(self) -> List[str]: + "The names of the tokens that open a parenthesis" + raise NotImplementedError() + + @property + @abstractmethod + def CLOSE_PAREN_types(self) -> List[str]: + """The names of the tokens that close a parenthesis + """ + raise NotImplementedError() + + @property + @abstractmethod + def INDENT_type(self) -> str: + """The name of the token that starts an indentation in the grammar. + + See also: %declare + """ + raise NotImplementedError() + + @property + @abstractmethod + def DEDENT_type(self) -> str: + """The name of the token that end an indentation in the grammar. + + See also: %declare + """ + raise NotImplementedError() + + @property + @abstractmethod + def tab_len(self) -> int: + """How many spaces does a tab equal""" + raise NotImplementedError() + + +class PythonIndenter(Indenter): + """A postlexer that "injects" _INDENT/_DEDENT tokens based on indentation, according to the Python syntax. + + See also: the ``postlex`` option in `Lark`. + """ + + NL_type = '_NEWLINE' + OPEN_PAREN_types = ['LPAR', 'LSQB', 'LBRACE'] + CLOSE_PAREN_types = ['RPAR', 'RSQB', 'RBRACE'] + INDENT_type = '_INDENT' + DEDENT_type = '_DEDENT' + tab_len = 8 + +###} diff --git a/ccxt/static_dependencies/lark/lark.py b/ccxt/static_dependencies/lark/lark.py new file mode 100644 index 0000000..7ae1f24 --- /dev/null +++ b/ccxt/static_dependencies/lark/lark.py @@ -0,0 +1,658 @@ +from abc import ABC, abstractmethod +import getpass +import sys, os, pickle +import tempfile +import types +import re +from typing import ( + TypeVar, Type, List, Dict, Iterator, Callable, Union, Optional, Sequence, + Tuple, Iterable, IO, Any, TYPE_CHECKING, Collection +) +if TYPE_CHECKING: + from .parsers.lalr_interactive_parser import InteractiveParser + from .tree import ParseTree + from .visitors import Transformer + from typing import Literal + from .parser_frontends import ParsingFrontend + +from .exceptions import ConfigurationError, assert_config, UnexpectedInput +from .utils import Serialize, SerializeMemoizer, FS, logger +from .load_grammar import load_grammar, FromPackageLoader, Grammar, verify_used_files, PackageResource, sha256_digest +from .tree import Tree +from .common import LexerConf, ParserConf, _ParserArgType, _LexerArgType + +from .lexer import Lexer, BasicLexer, TerminalDef, LexerThread, Token +from .parse_tree_builder import ParseTreeBuilder +from .parser_frontends import _validate_frontend_args, _get_lexer_callbacks, _deserialize_parsing_frontend, _construct_parsing_frontend +from .grammar import Rule + + +try: + import regex + _has_regex = True +except ImportError: + _has_regex = False + + +###{standalone + + +class PostLex(ABC): + @abstractmethod + def process(self, stream: Iterator[Token]) -> Iterator[Token]: + return stream + + always_accept: Iterable[str] = () + +class LarkOptions(Serialize): + """Specifies the options for Lark + + """ + + start: List[str] + debug: bool + strict: bool + transformer: 'Optional[Transformer]' + propagate_positions: Union[bool, str] + maybe_placeholders: bool + cache: Union[bool, str] + regex: bool + g_regex_flags: int + keep_all_tokens: bool + tree_class: Optional[Callable[[str, List], Any]] + parser: _ParserArgType + lexer: _LexerArgType + ambiguity: 'Literal["auto", "resolve", "explicit", "forest"]' + postlex: Optional[PostLex] + priority: 'Optional[Literal["auto", "normal", "invert"]]' + lexer_callbacks: Dict[str, Callable[[Token], Token]] + use_bytes: bool + ordered_sets: bool + edit_terminals: Optional[Callable[[TerminalDef], TerminalDef]] + import_paths: 'List[Union[str, Callable[[Union[None, str, PackageResource], str], Tuple[str, str]]]]' + source_path: Optional[str] + + OPTIONS_DOC = r""" + **=== General Options ===** + + start + The start symbol. Either a string, or a list of strings for multiple possible starts (Default: "start") + debug + Display debug information and extra warnings. Use only when debugging (Default: ``False``) + When used with Earley, it generates a forest graph as "sppf.png", if 'dot' is installed. + strict + Throw an exception on any potential ambiguity, including shift/reduce conflicts, and regex collisions. + transformer + Applies the transformer to every parse tree (equivalent to applying it after the parse, but faster) + propagate_positions + Propagates positional attributes into the 'meta' attribute of all tree branches. + Sets attributes: (line, column, end_line, end_column, start_pos, end_pos, + container_line, container_column, container_end_line, container_end_column) + Accepts ``False``, ``True``, or a callable, which will filter which nodes to ignore when propagating. + maybe_placeholders + When ``True``, the ``[]`` operator returns ``None`` when not matched. + When ``False``, ``[]`` behaves like the ``?`` operator, and returns no value at all. + (default= ``True``) + cache + Cache the results of the Lark grammar analysis, for x2 to x3 faster loading. LALR only for now. + + - When ``False``, does nothing (default) + - When ``True``, caches to a temporary file in the local directory + - When given a string, caches to the path pointed by the string + regex + When True, uses the ``regex`` module instead of the stdlib ``re``. + g_regex_flags + Flags that are applied to all terminals (both regex and strings) + keep_all_tokens + Prevent the tree builder from automagically removing "punctuation" tokens (Default: ``False``) + tree_class + Lark will produce trees comprised of instances of this class instead of the default ``lark.Tree``. + + **=== Algorithm Options ===** + + parser + Decides which parser engine to use. Accepts "earley" or "lalr". (Default: "earley"). + (there is also a "cyk" option for legacy) + lexer + Decides whether or not to use a lexer stage + + - "auto" (default): Choose for me based on the parser + - "basic": Use a basic lexer + - "contextual": Stronger lexer (only works with parser="lalr") + - "dynamic": Flexible and powerful (only with parser="earley") + - "dynamic_complete": Same as dynamic, but tries *every* variation of tokenizing possible. + ambiguity + Decides how to handle ambiguity in the parse. Only relevant if parser="earley" + + - "resolve": The parser will automatically choose the simplest derivation + (it chooses consistently: greedy for tokens, non-greedy for rules) + - "explicit": The parser will return all derivations wrapped in "_ambig" tree nodes (i.e. a forest). + - "forest": The parser will return the root of the shared packed parse forest. + + **=== Misc. / Domain Specific Options ===** + + postlex + Lexer post-processing (Default: ``None``) Only works with the basic and contextual lexers. + priority + How priorities should be evaluated - "auto", ``None``, "normal", "invert" (Default: "auto") + lexer_callbacks + Dictionary of callbacks for the lexer. May alter tokens during lexing. Use with caution. + use_bytes + Accept an input of type ``bytes`` instead of ``str``. + ordered_sets + Should Earley use ordered-sets to achieve stable output (~10% slower than regular sets. Default: True) + edit_terminals + A callback for editing the terminals before parse. + import_paths + A List of either paths or loader functions to specify from where grammars are imported + source_path + Override the source of from where the grammar was loaded. Useful for relative imports and unconventional grammar loading + **=== End of Options ===** + """ + if __doc__: + __doc__ += OPTIONS_DOC + + + # Adding a new option needs to be done in multiple places: + # - In the dictionary below. This is the primary truth of which options `Lark.__init__` accepts + # - In the docstring above. It is used both for the docstring of `LarkOptions` and `Lark`, and in readthedocs + # - As an attribute of `LarkOptions` above + # - Potentially in `_LOAD_ALLOWED_OPTIONS` below this class, when the option doesn't change how the grammar is loaded + # - Potentially in `lark.tools.__init__`, if it makes sense, and it can easily be passed as a cmd argument + _defaults: Dict[str, Any] = { + 'debug': False, + 'strict': False, + 'keep_all_tokens': False, + 'tree_class': None, + 'cache': False, + 'postlex': None, + 'parser': 'earley', + 'lexer': 'auto', + 'transformer': None, + 'start': 'start', + 'priority': 'auto', + 'ambiguity': 'auto', + 'regex': False, + 'propagate_positions': False, + 'lexer_callbacks': {}, + 'maybe_placeholders': True, + 'edit_terminals': None, + 'g_regex_flags': 0, + 'use_bytes': False, + 'ordered_sets': True, + 'import_paths': [], + 'source_path': None, + '_plugins': {}, + } + + def __init__(self, options_dict: Dict[str, Any]) -> None: + o = dict(options_dict) + + options = {} + for name, default in self._defaults.items(): + if name in o: + value = o.pop(name) + if isinstance(default, bool) and name not in ('cache', 'use_bytes', 'propagate_positions'): + value = bool(value) + else: + value = default + + options[name] = value + + if isinstance(options['start'], str): + options['start'] = [options['start']] + + self.__dict__['options'] = options + + + assert_config(self.parser, ('earley', 'lalr', 'cyk', None)) + + if self.parser == 'earley' and self.transformer: + raise ConfigurationError('Cannot specify an embedded transformer when using the Earley algorithm. ' + 'Please use your transformer on the resulting parse tree, or use a different algorithm (i.e. LALR)') + + if o: + raise ConfigurationError("Unknown options: %s" % o.keys()) + + def __getattr__(self, name: str) -> Any: + try: + return self.__dict__['options'][name] + except KeyError as e: + raise AttributeError(e) + + def __setattr__(self, name: str, value: str) -> None: + assert_config(name, self.options.keys(), "%r isn't a valid option. Expected one of: %s") + self.options[name] = value + + def serialize(self, memo = None) -> Dict[str, Any]: + return self.options + + @classmethod + def deserialize(cls, data: Dict[str, Any], memo: Dict[int, Union[TerminalDef, Rule]]) -> "LarkOptions": + return cls(data) + + +# Options that can be passed to the Lark parser, even when it was loaded from cache/standalone. +# These options are only used outside of `load_grammar`. +_LOAD_ALLOWED_OPTIONS = {'postlex', 'transformer', 'lexer_callbacks', 'use_bytes', 'debug', 'g_regex_flags', 'regex', 'propagate_positions', 'tree_class', '_plugins'} + +_VALID_PRIORITY_OPTIONS = ('auto', 'normal', 'invert', None) +_VALID_AMBIGUITY_OPTIONS = ('auto', 'resolve', 'explicit', 'forest') + + +_T = TypeVar('_T', bound="Lark") + +class Lark(Serialize): + """Main interface for the library. + + It's mostly a thin wrapper for the many different parsers, and for the tree constructor. + + Parameters: + grammar: a string or file-object containing the grammar spec (using Lark's ebnf syntax) + options: a dictionary controlling various aspects of Lark. + + Example: + >>> Lark(r'''start: "foo" ''') + Lark(...) + """ + + source_path: str + source_grammar: str + grammar: 'Grammar' + options: LarkOptions + lexer: Lexer + parser: 'ParsingFrontend' + terminals: Collection[TerminalDef] + + def __init__(self, grammar: 'Union[Grammar, str, IO[str]]', **options) -> None: + self.options = LarkOptions(options) + re_module: types.ModuleType + + # Set regex or re module + use_regex = self.options.regex + if use_regex: + if _has_regex: + re_module = regex + else: + raise ImportError('`regex` module must be installed if calling `Lark(regex=True)`.') + else: + re_module = re + + # Some, but not all file-like objects have a 'name' attribute + if self.options.source_path is None: + try: + self.source_path = grammar.name # type: ignore[union-attr] + except AttributeError: + self.source_path = '' + else: + self.source_path = self.options.source_path + + # Drain file-like objects to get their contents + try: + read = grammar.read # type: ignore[union-attr] + except AttributeError: + pass + else: + grammar = read() + + cache_fn = None + cache_sha256 = None + if isinstance(grammar, str): + self.source_grammar = grammar + if self.options.use_bytes: + if not grammar.isascii(): + raise ConfigurationError("Grammar must be ascii only, when use_bytes=True") + + if self.options.cache: + if self.options.parser != 'lalr': + raise ConfigurationError("cache only works with parser='lalr' for now") + + unhashable = ('transformer', 'postlex', 'lexer_callbacks', 'edit_terminals', '_plugins') + options_str = ''.join(k+str(v) for k, v in options.items() if k not in unhashable) + from . import __version__ + s = grammar + options_str + __version__ + str(sys.version_info[:2]) + cache_sha256 = sha256_digest(s) + + if isinstance(self.options.cache, str): + cache_fn = self.options.cache + else: + if self.options.cache is not True: + raise ConfigurationError("cache argument must be bool or str") + + try: + username = getpass.getuser() + except Exception: + # The exception raised may be ImportError or OSError in + # the future. For the cache, we don't care about the + # specific reason - we just want a username. + username = "unknown" + + cache_fn = tempfile.gettempdir() + "/.lark_cache_%s_%s_%s_%s.tmp" % (username, cache_sha256, *sys.version_info[:2]) + + old_options = self.options + try: + with FS.open(cache_fn, 'rb') as f: + logger.debug('Loading grammar from cache: %s', cache_fn) + # Remove options that aren't relevant for loading from cache + for name in (set(options) - _LOAD_ALLOWED_OPTIONS): + del options[name] + file_sha256 = f.readline().rstrip(b'\n') + cached_used_files = pickle.load(f) + if file_sha256 == cache_sha256.encode('utf8') and verify_used_files(cached_used_files): + cached_parser_data = pickle.load(f) + self._load(cached_parser_data, **options) + return + except FileNotFoundError: + # The cache file doesn't exist; parse and compose the grammar as normal + pass + except Exception: # We should probably narrow done which errors we catch here. + logger.exception("Failed to load Lark from cache: %r. We will try to carry on.", cache_fn) + + # In theory, the Lark instance might have been messed up by the call to `_load`. + # In practice the only relevant thing that might have been overwritten should be `options` + self.options = old_options + + + # Parse the grammar file and compose the grammars + self.grammar, used_files = load_grammar(grammar, self.source_path, self.options.import_paths, self.options.keep_all_tokens) + else: + assert isinstance(grammar, Grammar) + self.grammar = grammar + + + if self.options.lexer == 'auto': + if self.options.parser == 'lalr': + self.options.lexer = 'contextual' + elif self.options.parser == 'earley': + if self.options.postlex is not None: + logger.info("postlex can't be used with the dynamic lexer, so we use 'basic' instead. " + "Consider using lalr with contextual instead of earley") + self.options.lexer = 'basic' + else: + self.options.lexer = 'dynamic' + elif self.options.parser == 'cyk': + self.options.lexer = 'basic' + else: + assert False, self.options.parser + lexer = self.options.lexer + if isinstance(lexer, type): + assert issubclass(lexer, Lexer) # XXX Is this really important? Maybe just ensure interface compliance + else: + assert_config(lexer, ('basic', 'contextual', 'dynamic', 'dynamic_complete')) + if self.options.postlex is not None and 'dynamic' in lexer: + raise ConfigurationError("Can't use postlex with a dynamic lexer. Use basic or contextual instead") + + if self.options.ambiguity == 'auto': + if self.options.parser == 'earley': + self.options.ambiguity = 'resolve' + else: + assert_config(self.options.parser, ('earley', 'cyk'), "%r doesn't support disambiguation. Use one of these parsers instead: %s") + + if self.options.priority == 'auto': + self.options.priority = 'normal' + + if self.options.priority not in _VALID_PRIORITY_OPTIONS: + raise ConfigurationError("invalid priority option: %r. Must be one of %r" % (self.options.priority, _VALID_PRIORITY_OPTIONS)) + if self.options.ambiguity not in _VALID_AMBIGUITY_OPTIONS: + raise ConfigurationError("invalid ambiguity option: %r. Must be one of %r" % (self.options.ambiguity, _VALID_AMBIGUITY_OPTIONS)) + + if self.options.parser is None: + terminals_to_keep = '*' + elif self.options.postlex is not None: + terminals_to_keep = set(self.options.postlex.always_accept) + else: + terminals_to_keep = set() + + # Compile the EBNF grammar into BNF + self.terminals, self.rules, self.ignore_tokens = self.grammar.compile(self.options.start, terminals_to_keep) + + if self.options.edit_terminals: + for t in self.terminals: + self.options.edit_terminals(t) + + self._terminals_dict = {t.name: t for t in self.terminals} + + # If the user asked to invert the priorities, negate them all here. + if self.options.priority == 'invert': + for rule in self.rules: + if rule.options.priority is not None: + rule.options.priority = -rule.options.priority + for term in self.terminals: + term.priority = -term.priority + # Else, if the user asked to disable priorities, strip them from the + # rules and terminals. This allows the Earley parsers to skip an extra forest walk + # for improved performance, if you don't need them (or didn't specify any). + elif self.options.priority is None: + for rule in self.rules: + if rule.options.priority is not None: + rule.options.priority = None + for term in self.terminals: + term.priority = 0 + + # TODO Deprecate lexer_callbacks? + self.lexer_conf = LexerConf( + self.terminals, re_module, self.ignore_tokens, self.options.postlex, + self.options.lexer_callbacks, self.options.g_regex_flags, use_bytes=self.options.use_bytes, strict=self.options.strict + ) + + if self.options.parser: + self.parser = self._build_parser() + elif lexer: + self.lexer = self._build_lexer() + + if cache_fn: + logger.debug('Saving grammar to cache: %s', cache_fn) + try: + with FS.open(cache_fn, 'wb') as f: + assert cache_sha256 is not None + f.write(cache_sha256.encode('utf8') + b'\n') + pickle.dump(used_files, f) + self.save(f, _LOAD_ALLOWED_OPTIONS) + except IOError as e: + logger.exception("Failed to save Lark to cache: %r.", cache_fn, e) + + if __doc__: + __doc__ += "\n\n" + LarkOptions.OPTIONS_DOC + + __serialize_fields__ = 'parser', 'rules', 'options' + + def _build_lexer(self, dont_ignore: bool=False) -> BasicLexer: + lexer_conf = self.lexer_conf + if dont_ignore: + from copy import copy + lexer_conf = copy(lexer_conf) + lexer_conf.ignore = () + return BasicLexer(lexer_conf) + + def _prepare_callbacks(self) -> None: + self._callbacks = {} + # we don't need these callbacks if we aren't building a tree + if self.options.ambiguity != 'forest': + self._parse_tree_builder = ParseTreeBuilder( + self.rules, + self.options.tree_class or Tree, + self.options.propagate_positions, + self.options.parser != 'lalr' and self.options.ambiguity == 'explicit', + self.options.maybe_placeholders + ) + self._callbacks = self._parse_tree_builder.create_callback(self.options.transformer) + self._callbacks.update(_get_lexer_callbacks(self.options.transformer, self.terminals)) + + def _build_parser(self) -> "ParsingFrontend": + self._prepare_callbacks() + _validate_frontend_args(self.options.parser, self.options.lexer) + parser_conf = ParserConf(self.rules, self._callbacks, self.options.start) + return _construct_parsing_frontend( + self.options.parser, + self.options.lexer, + self.lexer_conf, + parser_conf, + options=self.options + ) + + def save(self, f, exclude_options: Collection[str] = ()) -> None: + """Saves the instance into the given file object + + Useful for caching and multiprocessing. + """ + if self.options.parser != 'lalr': + raise NotImplementedError("Lark.save() is only implemented for the LALR(1) parser.") + data, m = self.memo_serialize([TerminalDef, Rule]) + if exclude_options: + data["options"] = {n: v for n, v in data["options"].items() if n not in exclude_options} + pickle.dump({'data': data, 'memo': m}, f, protocol=pickle.HIGHEST_PROTOCOL) + + @classmethod + def load(cls: Type[_T], f) -> _T: + """Loads an instance from the given file object + + Useful for caching and multiprocessing. + """ + inst = cls.__new__(cls) + return inst._load(f) + + def _deserialize_lexer_conf(self, data: Dict[str, Any], memo: Dict[int, Union[TerminalDef, Rule]], options: LarkOptions) -> LexerConf: + lexer_conf = LexerConf.deserialize(data['lexer_conf'], memo) + lexer_conf.callbacks = options.lexer_callbacks or {} + lexer_conf.re_module = regex if options.regex else re + lexer_conf.use_bytes = options.use_bytes + lexer_conf.g_regex_flags = options.g_regex_flags + lexer_conf.skip_validation = True + lexer_conf.postlex = options.postlex + return lexer_conf + + def _load(self: _T, f: Any, **kwargs) -> _T: + if isinstance(f, dict): + d = f + else: + d = pickle.load(f) + memo_json = d['memo'] + data = d['data'] + + assert memo_json + memo = SerializeMemoizer.deserialize(memo_json, {'Rule': Rule, 'TerminalDef': TerminalDef}, {}) + options = dict(data['options']) + if (set(kwargs) - _LOAD_ALLOWED_OPTIONS) & set(LarkOptions._defaults): + raise ConfigurationError("Some options are not allowed when loading a Parser: {}" + .format(set(kwargs) - _LOAD_ALLOWED_OPTIONS)) + options.update(kwargs) + self.options = LarkOptions.deserialize(options, memo) + self.rules = [Rule.deserialize(r, memo) for r in data['rules']] + self.source_path = '' + _validate_frontend_args(self.options.parser, self.options.lexer) + self.lexer_conf = self._deserialize_lexer_conf(data['parser'], memo, self.options) + self.terminals = self.lexer_conf.terminals + self._prepare_callbacks() + self._terminals_dict = {t.name: t for t in self.terminals} + self.parser = _deserialize_parsing_frontend( + data['parser'], + memo, + self.lexer_conf, + self._callbacks, + self.options, # Not all, but multiple attributes are used + ) + return self + + @classmethod + def _load_from_dict(cls, data, memo, **kwargs): + inst = cls.__new__(cls) + return inst._load({'data': data, 'memo': memo}, **kwargs) + + @classmethod + def open(cls: Type[_T], grammar_filename: str, rel_to: Optional[str]=None, **options) -> _T: + """Create an instance of Lark with the grammar given by its filename + + If ``rel_to`` is provided, the function will find the grammar filename in relation to it. + + Example: + + >>> Lark.open("grammar_file.lark", rel_to=__file__, parser="lalr") + Lark(...) + + """ + if rel_to: + basepath = os.path.dirname(rel_to) + grammar_filename = os.path.join(basepath, grammar_filename) + with open(grammar_filename, encoding='utf8') as f: + return cls(f, **options) + + @classmethod + def open_from_package(cls: Type[_T], package: str, grammar_path: str, search_paths: 'Sequence[str]'=[""], **options) -> _T: + """Create an instance of Lark with the grammar loaded from within the package `package`. + This allows grammar loading from zipapps. + + Imports in the grammar will use the `package` and `search_paths` provided, through `FromPackageLoader` + + Example: + + Lark.open_from_package(__name__, "example.lark", ("grammars",), parser=...) + """ + package_loader = FromPackageLoader(package, search_paths) + full_path, text = package_loader(None, grammar_path) + options.setdefault('source_path', full_path) + options.setdefault('import_paths', []) + options['import_paths'].append(package_loader) + return cls(text, **options) + + def __repr__(self): + return 'Lark(open(%r), parser=%r, lexer=%r, ...)' % (self.source_path, self.options.parser, self.options.lexer) + + + def lex(self, text: str, dont_ignore: bool=False) -> Iterator[Token]: + """Only lex (and postlex) the text, without parsing it. Only relevant when lexer='basic' + + When dont_ignore=True, the lexer will return all tokens, even those marked for %ignore. + + :raises UnexpectedCharacters: In case the lexer cannot find a suitable match. + """ + lexer: Lexer + if not hasattr(self, 'lexer') or dont_ignore: + lexer = self._build_lexer(dont_ignore) + else: + lexer = self.lexer + lexer_thread = LexerThread.from_text(lexer, text) + stream = lexer_thread.lex(None) + if self.options.postlex: + return self.options.postlex.process(stream) + return stream + + def get_terminal(self, name: str) -> TerminalDef: + """Get information about a terminal""" + return self._terminals_dict[name] + + def parse_interactive(self, text: Optional[str]=None, start: Optional[str]=None) -> 'InteractiveParser': + """Start an interactive parsing session. + + Parameters: + text (str, optional): Text to be parsed. Required for ``resume_parse()``. + start (str, optional): Start symbol + + Returns: + A new InteractiveParser instance. + + See Also: ``Lark.parse()`` + """ + return self.parser.parse_interactive(text, start=start) + + def parse(self, text: str, start: Optional[str]=None, on_error: 'Optional[Callable[[UnexpectedInput], bool]]'=None) -> 'ParseTree': + """Parse the given text, according to the options provided. + + Parameters: + text (str): Text to be parsed. + start (str, optional): Required if Lark was given multiple possible start symbols (using the start option). + on_error (function, optional): if provided, will be called on UnexpectedToken error. Return true to resume parsing. + LALR only. See examples/advanced/error_handling.py for an example of how to use on_error. + + Returns: + If a transformer is supplied to ``__init__``, returns whatever is the + result of the transformation. Otherwise, returns a Tree instance. + + :raises UnexpectedInput: On a parse error, one of these sub-exceptions will rise: + ``UnexpectedCharacters``, ``UnexpectedToken``, or ``UnexpectedEOF``. + For convenience, these sub-exceptions also inherit from ``ParserError`` and ``LexerError``. + + """ + return self.parser.parse(text, start=start, on_error=on_error) + + +###} diff --git a/ccxt/static_dependencies/lark/lexer.py b/ccxt/static_dependencies/lark/lexer.py new file mode 100644 index 0000000..9061d60 --- /dev/null +++ b/ccxt/static_dependencies/lark/lexer.py @@ -0,0 +1,678 @@ +# Lexer Implementation + +from abc import abstractmethod, ABC +import re +from contextlib import suppress +from typing import ( + TypeVar, Type, Dict, Iterator, Collection, Callable, Optional, FrozenSet, Any, + ClassVar, TYPE_CHECKING, overload +) +from types import ModuleType +import warnings +try: + import interegular +except ImportError: + pass +if TYPE_CHECKING: + from .common import LexerConf + from .parsers.lalr_parser_state import ParserState + +from .utils import classify, get_regexp_width, Serialize, logger +from .exceptions import UnexpectedCharacters, LexError, UnexpectedToken +from .grammar import TOKEN_DEFAULT_PRIORITY + + +###{standalone +from copy import copy + +try: # For the standalone parser, we need to make sure that has_interegular is False to avoid NameErrors later on + has_interegular = bool(interegular) +except NameError: + has_interegular = False + +class Pattern(Serialize, ABC): + "An abstraction over regular expressions." + + value: str + flags: Collection[str] + raw: Optional[str] + type: ClassVar[str] + + def __init__(self, value: str, flags: Collection[str] = (), raw: Optional[str] = None) -> None: + self.value = value + self.flags = frozenset(flags) + self.raw = raw + + def __repr__(self): + return repr(self.to_regexp()) + + # Pattern Hashing assumes all subclasses have a different priority! + def __hash__(self): + return hash((type(self), self.value, self.flags)) + + def __eq__(self, other): + return type(self) == type(other) and self.value == other.value and self.flags == other.flags + + @abstractmethod + def to_regexp(self) -> str: + raise NotImplementedError() + + @property + @abstractmethod + def min_width(self) -> int: + raise NotImplementedError() + + @property + @abstractmethod + def max_width(self) -> int: + raise NotImplementedError() + + def _get_flags(self, value): + for f in self.flags: + value = ('(?%s:%s)' % (f, value)) + return value + + +class PatternStr(Pattern): + __serialize_fields__ = 'value', 'flags', 'raw' + + type: ClassVar[str] = "str" + + def to_regexp(self) -> str: + return self._get_flags(re.escape(self.value)) + + @property + def min_width(self) -> int: + return len(self.value) + + @property + def max_width(self) -> int: + return len(self.value) + + +class PatternRE(Pattern): + __serialize_fields__ = 'value', 'flags', 'raw', '_width' + + type: ClassVar[str] = "re" + + def to_regexp(self) -> str: + return self._get_flags(self.value) + + _width = None + def _get_width(self): + if self._width is None: + self._width = get_regexp_width(self.to_regexp()) + return self._width + + @property + def min_width(self) -> int: + return self._get_width()[0] + + @property + def max_width(self) -> int: + return self._get_width()[1] + + +class TerminalDef(Serialize): + "A definition of a terminal" + __serialize_fields__ = 'name', 'pattern', 'priority' + __serialize_namespace__ = PatternStr, PatternRE + + name: str + pattern: Pattern + priority: int + + def __init__(self, name: str, pattern: Pattern, priority: int = TOKEN_DEFAULT_PRIORITY) -> None: + assert isinstance(pattern, Pattern), pattern + self.name = name + self.pattern = pattern + self.priority = priority + + def __repr__(self): + return '%s(%r, %r)' % (type(self).__name__, self.name, self.pattern) + + def user_repr(self) -> str: + if self.name.startswith('__'): # We represent a generated terminal + return self.pattern.raw or self.name + else: + return self.name + +_T = TypeVar('_T', bound="Token") + +class Token(str): + """A string with meta-information, that is produced by the lexer. + + When parsing text, the resulting chunks of the input that haven't been discarded, + will end up in the tree as Token instances. The Token class inherits from Python's ``str``, + so normal string comparisons and operations will work as expected. + + Attributes: + type: Name of the token (as specified in grammar) + value: Value of the token (redundant, as ``token.value == token`` will always be true) + start_pos: The index of the token in the text + line: The line of the token in the text (starting with 1) + column: The column of the token in the text (starting with 1) + end_line: The line where the token ends + end_column: The next column after the end of the token. For example, + if the token is a single character with a column value of 4, + end_column will be 5. + end_pos: the index where the token ends (basically ``start_pos + len(token)``) + """ + __slots__ = ('type', 'start_pos', 'value', 'line', 'column', 'end_line', 'end_column', 'end_pos') + + __match_args__ = ('type', 'value') + + type: str + start_pos: Optional[int] + value: Any + line: Optional[int] + column: Optional[int] + end_line: Optional[int] + end_column: Optional[int] + end_pos: Optional[int] + + + @overload + def __new__( + cls, + type: str, + value: Any, + start_pos: Optional[int] = None, + line: Optional[int] = None, + column: Optional[int] = None, + end_line: Optional[int] = None, + end_column: Optional[int] = None, + end_pos: Optional[int] = None + ) -> 'Token': + ... + + @overload + def __new__( + cls, + type_: str, + value: Any, + start_pos: Optional[int] = None, + line: Optional[int] = None, + column: Optional[int] = None, + end_line: Optional[int] = None, + end_column: Optional[int] = None, + end_pos: Optional[int] = None + ) -> 'Token': ... + + def __new__(cls, *args, **kwargs): + if "type_" in kwargs: + warnings.warn("`type_` is deprecated use `type` instead", DeprecationWarning) + + if "type" in kwargs: + raise TypeError("Error: using both 'type' and the deprecated 'type_' as arguments.") + kwargs["type"] = kwargs.pop("type_") + + return cls._future_new(*args, **kwargs) + + + @classmethod + def _future_new(cls, type, value, start_pos=None, line=None, column=None, end_line=None, end_column=None, end_pos=None): + inst = super(Token, cls).__new__(cls, value) + + inst.type = type + inst.start_pos = start_pos + inst.value = value + inst.line = line + inst.column = column + inst.end_line = end_line + inst.end_column = end_column + inst.end_pos = end_pos + return inst + + @overload + def update(self, type: Optional[str] = None, value: Optional[Any] = None) -> 'Token': + ... + + @overload + def update(self, type_: Optional[str] = None, value: Optional[Any] = None) -> 'Token': + ... + + def update(self, *args, **kwargs): + if "type_" in kwargs: + warnings.warn("`type_` is deprecated use `type` instead", DeprecationWarning) + + if "type" in kwargs: + raise TypeError("Error: using both 'type' and the deprecated 'type_' as arguments.") + kwargs["type"] = kwargs.pop("type_") + + return self._future_update(*args, **kwargs) + + def _future_update(self, type: Optional[str] = None, value: Optional[Any] = None) -> 'Token': + return Token.new_borrow_pos( + type if type is not None else self.type, + value if value is not None else self.value, + self + ) + + @classmethod + def new_borrow_pos(cls: Type[_T], type_: str, value: Any, borrow_t: 'Token') -> _T: + return cls(type_, value, borrow_t.start_pos, borrow_t.line, borrow_t.column, borrow_t.end_line, borrow_t.end_column, borrow_t.end_pos) + + def __reduce__(self): + return (self.__class__, (self.type, self.value, self.start_pos, self.line, self.column)) + + def __repr__(self): + return 'Token(%r, %r)' % (self.type, self.value) + + def __deepcopy__(self, memo): + return Token(self.type, self.value, self.start_pos, self.line, self.column) + + def __eq__(self, other): + if isinstance(other, Token) and self.type != other.type: + return False + + return str.__eq__(self, other) + + __hash__ = str.__hash__ + + +class LineCounter: + "A utility class for keeping track of line & column information" + + __slots__ = 'char_pos', 'line', 'column', 'line_start_pos', 'newline_char' + + def __init__(self, newline_char): + self.newline_char = newline_char + self.char_pos = 0 + self.line = 1 + self.column = 1 + self.line_start_pos = 0 + + def __eq__(self, other): + if not isinstance(other, LineCounter): + return NotImplemented + + return self.char_pos == other.char_pos and self.newline_char == other.newline_char + + def feed(self, token: Token, test_newline=True): + """Consume a token and calculate the new line & column. + + As an optional optimization, set test_newline=False if token doesn't contain a newline. + """ + if test_newline: + newlines = token.count(self.newline_char) + if newlines: + self.line += newlines + self.line_start_pos = self.char_pos + token.rindex(self.newline_char) + 1 + + self.char_pos += len(token) + self.column = self.char_pos - self.line_start_pos + 1 + + +class UnlessCallback: + def __init__(self, scanner): + self.scanner = scanner + + def __call__(self, t): + res = self.scanner.match(t.value, 0) + if res: + _value, t.type = res + return t + + +class CallChain: + def __init__(self, callback1, callback2, cond): + self.callback1 = callback1 + self.callback2 = callback2 + self.cond = cond + + def __call__(self, t): + t2 = self.callback1(t) + return self.callback2(t) if self.cond(t2) else t2 + + +def _get_match(re_, regexp, s, flags): + m = re_.match(regexp, s, flags) + if m: + return m.group(0) + +def _create_unless(terminals, g_regex_flags, re_, use_bytes): + tokens_by_type = classify(terminals, lambda t: type(t.pattern)) + assert len(tokens_by_type) <= 2, tokens_by_type.keys() + embedded_strs = set() + callback = {} + for retok in tokens_by_type.get(PatternRE, []): + unless = [] + for strtok in tokens_by_type.get(PatternStr, []): + if strtok.priority != retok.priority: + continue + s = strtok.pattern.value + if s == _get_match(re_, retok.pattern.to_regexp(), s, g_regex_flags): + unless.append(strtok) + if strtok.pattern.flags <= retok.pattern.flags: + embedded_strs.add(strtok) + if unless: + callback[retok.name] = UnlessCallback(Scanner(unless, g_regex_flags, re_, match_whole=True, use_bytes=use_bytes)) + + new_terminals = [t for t in terminals if t not in embedded_strs] + return new_terminals, callback + + +class Scanner: + def __init__(self, terminals, g_regex_flags, re_, use_bytes, match_whole=False): + self.terminals = terminals + self.g_regex_flags = g_regex_flags + self.re_ = re_ + self.use_bytes = use_bytes + self.match_whole = match_whole + + self.allowed_types = {t.name for t in self.terminals} + + self._mres = self._build_mres(terminals, len(terminals)) + + def _build_mres(self, terminals, max_size): + # Python sets an unreasonable group limit (currently 100) in its re module + # Worse, the only way to know we reached it is by catching an AssertionError! + # This function recursively tries less and less groups until it's successful. + postfix = '$' if self.match_whole else '' + mres = [] + while terminals: + pattern = u'|'.join(u'(?P<%s>%s)' % (t.name, t.pattern.to_regexp() + postfix) for t in terminals[:max_size]) + if self.use_bytes: + pattern = pattern.encode('latin-1') + try: + mre = self.re_.compile(pattern, self.g_regex_flags) + except AssertionError: # Yes, this is what Python provides us.. :/ + return self._build_mres(terminals, max_size // 2) + + mres.append(mre) + terminals = terminals[max_size:] + return mres + + def match(self, text, pos): + for mre in self._mres: + m = mre.match(text, pos) + if m: + return m.group(0), m.lastgroup + + +def _regexp_has_newline(r: str): + r"""Expressions that may indicate newlines in a regexp: + - newlines (\n) + - escaped newline (\\n) + - anything but ([^...]) + - any-char (.) when the flag (?s) exists + - spaces (\s) + """ + return '\n' in r or '\\n' in r or '\\s' in r or '[^' in r or ('(?s' in r and '.' in r) + + +class LexerState: + """Represents the current state of the lexer as it scans the text + (Lexer objects are only instantiated per grammar, not per text) + """ + + __slots__ = 'text', 'line_ctr', 'last_token' + + text: str + line_ctr: LineCounter + last_token: Optional[Token] + + def __init__(self, text: str, line_ctr: Optional[LineCounter]=None, last_token: Optional[Token]=None): + self.text = text + self.line_ctr = line_ctr or LineCounter(b'\n' if isinstance(text, bytes) else '\n') + self.last_token = last_token + + def __eq__(self, other): + if not isinstance(other, LexerState): + return NotImplemented + + return self.text is other.text and self.line_ctr == other.line_ctr and self.last_token == other.last_token + + def __copy__(self): + return type(self)(self.text, copy(self.line_ctr), self.last_token) + + +class LexerThread: + """A thread that ties a lexer instance and a lexer state, to be used by the parser + """ + + def __init__(self, lexer: 'Lexer', lexer_state: LexerState): + self.lexer = lexer + self.state = lexer_state + + @classmethod + def from_text(cls, lexer: 'Lexer', text: str) -> 'LexerThread': + return cls(lexer, LexerState(text)) + + def lex(self, parser_state): + return self.lexer.lex(self.state, parser_state) + + def __copy__(self): + return type(self)(self.lexer, copy(self.state)) + + _Token = Token + + +_Callback = Callable[[Token], Token] + +class Lexer(ABC): + """Lexer interface + + Method Signatures: + lex(self, lexer_state, parser_state) -> Iterator[Token] + """ + @abstractmethod + def lex(self, lexer_state: LexerState, parser_state: Any) -> Iterator[Token]: + return NotImplemented + + def make_lexer_state(self, text): + "Deprecated" + return LexerState(text) + + +def _check_regex_collisions(terminal_to_regexp: Dict[TerminalDef, str], comparator, strict_mode, max_collisions_to_show=8): + if not comparator: + comparator = interegular.Comparator.from_regexes(terminal_to_regexp) + + # When in strict mode, we only ever try to provide one example, so taking + # a long time for that should be fine + max_time = 2 if strict_mode else 0.2 + + # We don't want to show too many collisions. + if comparator.count_marked_pairs() >= max_collisions_to_show: + return + for group in classify(terminal_to_regexp, lambda t: t.priority).values(): + for a, b in comparator.check(group, skip_marked=True): + assert a.priority == b.priority + # Mark this pair to not repeat warnings when multiple different BasicLexers see the same collision + comparator.mark(a, b) + + # Notify the user + message = f"Collision between Terminals {a.name} and {b.name}. " + try: + example = comparator.get_example_overlap(a, b, max_time).format_multiline() + except ValueError: + # Couldn't find an example within max_time steps. + example = "No example could be found fast enough. However, the collision does still exists" + if strict_mode: + raise LexError(f"{message}\n{example}") + logger.warning("%s The lexer will choose between them arbitrarily.\n%s", message, example) + if comparator.count_marked_pairs() >= max_collisions_to_show: + logger.warning("Found 8 regex collisions, will not check for more.") + return + + +class AbstractBasicLexer(Lexer): + terminals_by_name: Dict[str, TerminalDef] + + @abstractmethod + def __init__(self, conf: 'LexerConf', comparator=None) -> None: + ... + + @abstractmethod + def next_token(self, lex_state: LexerState, parser_state: Any = None) -> Token: + ... + + def lex(self, state: LexerState, parser_state: Any) -> Iterator[Token]: + with suppress(EOFError): + while True: + yield self.next_token(state, parser_state) + + +class BasicLexer(AbstractBasicLexer): + terminals: Collection[TerminalDef] + ignore_types: FrozenSet[str] + newline_types: FrozenSet[str] + user_callbacks: Dict[str, _Callback] + callback: Dict[str, _Callback] + re: ModuleType + + def __init__(self, conf: 'LexerConf', comparator=None) -> None: + terminals = list(conf.terminals) + assert all(isinstance(t, TerminalDef) for t in terminals), terminals + + self.re = conf.re_module + + if not conf.skip_validation: + # Sanitization + terminal_to_regexp = {} + for t in terminals: + regexp = t.pattern.to_regexp() + try: + self.re.compile(regexp, conf.g_regex_flags) + except self.re.error: + raise LexError("Cannot compile token %s: %s" % (t.name, t.pattern)) + + if t.pattern.min_width == 0: + raise LexError("Lexer does not allow zero-width terminals. (%s: %s)" % (t.name, t.pattern)) + if t.pattern.type == "re": + terminal_to_regexp[t] = regexp + + if not (set(conf.ignore) <= {t.name for t in terminals}): + raise LexError("Ignore terminals are not defined: %s" % (set(conf.ignore) - {t.name for t in terminals})) + + if has_interegular: + _check_regex_collisions(terminal_to_regexp, comparator, conf.strict) + elif conf.strict: + raise LexError("interegular must be installed for strict mode. Use `pip install 'lark[interegular]'`.") + + # Init + self.newline_types = frozenset(t.name for t in terminals if _regexp_has_newline(t.pattern.to_regexp())) + self.ignore_types = frozenset(conf.ignore) + + terminals.sort(key=lambda x: (-x.priority, -x.pattern.max_width, -len(x.pattern.value), x.name)) + self.terminals = terminals + self.user_callbacks = conf.callbacks + self.g_regex_flags = conf.g_regex_flags + self.use_bytes = conf.use_bytes + self.terminals_by_name = conf.terminals_by_name + + self._scanner = None + + def _build_scanner(self): + terminals, self.callback = _create_unless(self.terminals, self.g_regex_flags, self.re, self.use_bytes) + assert all(self.callback.values()) + + for type_, f in self.user_callbacks.items(): + if type_ in self.callback: + # Already a callback there, probably UnlessCallback + self.callback[type_] = CallChain(self.callback[type_], f, lambda t: t.type == type_) + else: + self.callback[type_] = f + + self._scanner = Scanner(terminals, self.g_regex_flags, self.re, self.use_bytes) + + @property + def scanner(self): + if self._scanner is None: + self._build_scanner() + return self._scanner + + def match(self, text, pos): + return self.scanner.match(text, pos) + + def next_token(self, lex_state: LexerState, parser_state: Any = None) -> Token: + line_ctr = lex_state.line_ctr + while line_ctr.char_pos < len(lex_state.text): + res = self.match(lex_state.text, line_ctr.char_pos) + if not res: + allowed = self.scanner.allowed_types - self.ignore_types + if not allowed: + allowed = {""} + raise UnexpectedCharacters(lex_state.text, line_ctr.char_pos, line_ctr.line, line_ctr.column, + allowed=allowed, token_history=lex_state.last_token and [lex_state.last_token], + state=parser_state, terminals_by_name=self.terminals_by_name) + + value, type_ = res + + ignored = type_ in self.ignore_types + t = None + if not ignored or type_ in self.callback: + t = Token(type_, value, line_ctr.char_pos, line_ctr.line, line_ctr.column) + line_ctr.feed(value, type_ in self.newline_types) + if t is not None: + t.end_line = line_ctr.line + t.end_column = line_ctr.column + t.end_pos = line_ctr.char_pos + if t.type in self.callback: + t = self.callback[t.type](t) + if not ignored: + if not isinstance(t, Token): + raise LexError("Callbacks must return a token (returned %r)" % t) + lex_state.last_token = t + return t + + # EOF + raise EOFError(self) + + +class ContextualLexer(Lexer): + lexers: Dict[int, AbstractBasicLexer] + root_lexer: AbstractBasicLexer + + BasicLexer: Type[AbstractBasicLexer] = BasicLexer + + def __init__(self, conf: 'LexerConf', states: Dict[int, Collection[str]], always_accept: Collection[str]=()) -> None: + terminals = list(conf.terminals) + terminals_by_name = conf.terminals_by_name + + trad_conf = copy(conf) + trad_conf.terminals = terminals + + if has_interegular and not conf.skip_validation: + comparator = interegular.Comparator.from_regexes({t: t.pattern.to_regexp() for t in terminals}) + else: + comparator = None + lexer_by_tokens: Dict[FrozenSet[str], AbstractBasicLexer] = {} + self.lexers = {} + for state, accepts in states.items(): + key = frozenset(accepts) + try: + lexer = lexer_by_tokens[key] + except KeyError: + accepts = set(accepts) | set(conf.ignore) | set(always_accept) + lexer_conf = copy(trad_conf) + lexer_conf.terminals = [terminals_by_name[n] for n in accepts if n in terminals_by_name] + lexer = self.BasicLexer(lexer_conf, comparator) + lexer_by_tokens[key] = lexer + + self.lexers[state] = lexer + + assert trad_conf.terminals is terminals + trad_conf.skip_validation = True # We don't need to verify all terminals again + self.root_lexer = self.BasicLexer(trad_conf, comparator) + + def lex(self, lexer_state: LexerState, parser_state: 'ParserState') -> Iterator[Token]: + try: + while True: + lexer = self.lexers[parser_state.position] + yield lexer.next_token(lexer_state, parser_state) + except EOFError: + pass + except UnexpectedCharacters as e: + # In the contextual lexer, UnexpectedCharacters can mean that the terminal is defined, but not in the current context. + # This tests the input against the global context, to provide a nicer error. + try: + last_token = lexer_state.last_token # Save last_token. Calling root_lexer.next_token will change this to the wrong token + token = self.root_lexer.next_token(lexer_state, parser_state) + raise UnexpectedToken(token, e.allowed, state=parser_state, token_history=[last_token], terminals_by_name=self.root_lexer.terminals_by_name) + except UnexpectedCharacters: + raise e # Raise the original UnexpectedCharacters. The root lexer raises it with the wrong expected set. + +###} diff --git a/ccxt/static_dependencies/lark/load_grammar.py b/ccxt/static_dependencies/lark/load_grammar.py new file mode 100644 index 0000000..362a845 --- /dev/null +++ b/ccxt/static_dependencies/lark/load_grammar.py @@ -0,0 +1,1428 @@ +"""Parses and compiles Lark grammars into an internal representation. +""" + +import hashlib +import os.path +import sys +from collections import namedtuple +from copy import copy, deepcopy +import pkgutil +from ast import literal_eval +from contextlib import suppress +from typing import List, Tuple, Union, Callable, Dict, Optional, Sequence, Generator + +from .utils import bfs, logger, classify_bool, is_id_continue, is_id_start, bfs_all_unique, small_factors, OrderedSet +from .lexer import Token, TerminalDef, PatternStr, PatternRE, Pattern + +from .parse_tree_builder import ParseTreeBuilder +from .parser_frontends import ParsingFrontend +from .common import LexerConf, ParserConf +from .grammar import RuleOptions, Rule, Terminal, NonTerminal, Symbol, TOKEN_DEFAULT_PRIORITY +from .utils import classify, dedup_list +from .exceptions import GrammarError, UnexpectedCharacters, UnexpectedToken, ParseError, UnexpectedInput + +from .tree import Tree, SlottedTree as ST +from .visitors import Transformer, Visitor, v_args, Transformer_InPlace, Transformer_NonRecursive +inline_args = v_args(inline=True) + +IMPORT_PATHS = ['grammars'] + +EXT = '.lark' + +_RE_FLAGS = 'imslux' + +_EMPTY = Symbol('__empty__') + +_TERMINAL_NAMES = { + '.' : 'DOT', + ',' : 'COMMA', + ':' : 'COLON', + ';' : 'SEMICOLON', + '+' : 'PLUS', + '-' : 'MINUS', + '*' : 'STAR', + '/' : 'SLASH', + '\\' : 'BACKSLASH', + '|' : 'VBAR', + '?' : 'QMARK', + '!' : 'BANG', + '@' : 'AT', + '#' : 'HASH', + '$' : 'DOLLAR', + '%' : 'PERCENT', + '^' : 'CIRCUMFLEX', + '&' : 'AMPERSAND', + '_' : 'UNDERSCORE', + '<' : 'LESSTHAN', + '>' : 'MORETHAN', + '=' : 'EQUAL', + '"' : 'DBLQUOTE', + '\'' : 'QUOTE', + '`' : 'BACKQUOTE', + '~' : 'TILDE', + '(' : 'LPAR', + ')' : 'RPAR', + '{' : 'LBRACE', + '}' : 'RBRACE', + '[' : 'LSQB', + ']' : 'RSQB', + '\n' : 'NEWLINE', + '\r\n' : 'CRLF', + '\t' : 'TAB', + ' ' : 'SPACE', +} + +# Grammar Parser +TERMINALS = { + '_LPAR': r'\(', + '_RPAR': r'\)', + '_LBRA': r'\[', + '_RBRA': r'\]', + '_LBRACE': r'\{', + '_RBRACE': r'\}', + 'OP': '[+*]|[?](?![a-z_])', + '_COLON': ':', + '_COMMA': ',', + '_OR': r'\|', + '_DOT': r'\.(?!\.)', + '_DOTDOT': r'\.\.', + 'TILDE': '~', + 'RULE_MODIFIERS': '(!|![?]?|[?]!?)(?=[_a-z])', + 'RULE': '_?[a-z][_a-z0-9]*', + 'TERMINAL': '_?[A-Z][_A-Z0-9]*', + 'STRING': r'"(\\"|\\\\|[^"\n])*?"i?', + 'REGEXP': r'/(?!/)(\\/|\\\\|[^/])*?/[%s]*' % _RE_FLAGS, + '_NL': r'(\r?\n)+\s*', + '_NL_OR': r'(\r?\n)+\s*\|', + 'WS': r'[ \t]+', + 'COMMENT': r'\s*//[^\n]*|\s*#[^\n]*', + 'BACKSLASH': r'\\[ ]*\n', + '_TO': '->', + '_IGNORE': r'%ignore', + '_OVERRIDE': r'%override', + '_DECLARE': r'%declare', + '_EXTEND': r'%extend', + '_IMPORT': r'%import', + 'NUMBER': r'[+-]?\d+', +} + +RULES = { + 'start': ['_list'], + '_list': ['_item', '_list _item'], + '_item': ['rule', 'term', 'ignore', 'import', 'declare', 'override', 'extend', '_NL'], + + 'rule': ['rule_modifiers RULE template_params priority _COLON expansions _NL'], + 'rule_modifiers': ['RULE_MODIFIERS', + ''], + 'priority': ['_DOT NUMBER', + ''], + 'template_params': ['_LBRACE _template_params _RBRACE', + ''], + '_template_params': ['RULE', + '_template_params _COMMA RULE'], + 'expansions': ['_expansions'], + '_expansions': ['alias', + '_expansions _OR alias', + '_expansions _NL_OR alias'], + + '?alias': ['expansion _TO nonterminal', 'expansion'], + 'expansion': ['_expansion'], + + '_expansion': ['', '_expansion expr'], + + '?expr': ['atom', + 'atom OP', + 'atom TILDE NUMBER', + 'atom TILDE NUMBER _DOTDOT NUMBER', + ], + + '?atom': ['_LPAR expansions _RPAR', + 'maybe', + 'value'], + + 'value': ['terminal', + 'nonterminal', + 'literal', + 'range', + 'template_usage'], + + 'terminal': ['TERMINAL'], + 'nonterminal': ['RULE'], + + '?name': ['RULE', 'TERMINAL'], + '?symbol': ['terminal', 'nonterminal'], + + 'maybe': ['_LBRA expansions _RBRA'], + 'range': ['STRING _DOTDOT STRING'], + + 'template_usage': ['nonterminal _LBRACE _template_args _RBRACE'], + '_template_args': ['value', + '_template_args _COMMA value'], + + 'term': ['TERMINAL _COLON expansions _NL', + 'TERMINAL _DOT NUMBER _COLON expansions _NL'], + 'override': ['_OVERRIDE rule', + '_OVERRIDE term'], + 'extend': ['_EXTEND rule', + '_EXTEND term'], + 'ignore': ['_IGNORE expansions _NL'], + 'declare': ['_DECLARE _declare_args _NL'], + 'import': ['_IMPORT _import_path _NL', + '_IMPORT _import_path _LPAR name_list _RPAR _NL', + '_IMPORT _import_path _TO name _NL'], + + '_import_path': ['import_lib', 'import_rel'], + 'import_lib': ['_import_args'], + 'import_rel': ['_DOT _import_args'], + '_import_args': ['name', '_import_args _DOT name'], + + 'name_list': ['_name_list'], + '_name_list': ['name', '_name_list _COMMA name'], + + '_declare_args': ['symbol', '_declare_args symbol'], + 'literal': ['REGEXP', 'STRING'], +} + + +# Value 5 keeps the number of states in the lalr parser somewhat minimal +# It isn't optimal, but close to it. See PR #949 +SMALL_FACTOR_THRESHOLD = 5 +# The Threshold whether repeat via ~ are split up into different rules +# 50 is chosen since it keeps the number of states low and therefore lalr analysis time low, +# while not being to overaggressive and unnecessarily creating rules that might create shift/reduce conflicts. +# (See PR #949) +REPEAT_BREAK_THRESHOLD = 50 + + +class FindRuleSize(Transformer): + def __init__(self, keep_all_tokens: bool): + self.keep_all_tokens = keep_all_tokens + + def _will_not_get_removed(self, sym: Symbol) -> bool: + if isinstance(sym, NonTerminal): + return not sym.name.startswith('_') + if isinstance(sym, Terminal): + return self.keep_all_tokens or not sym.filter_out + if sym is _EMPTY: + return False + assert False, sym + + def _args_as_int(self, args: List[Union[int, Symbol]]) -> Generator[int, None, None]: + for a in args: + if isinstance(a, int): + yield a + elif isinstance(a, Symbol): + yield 1 if self._will_not_get_removed(a) else 0 + else: + assert False + + def expansion(self, args) -> int: + return sum(self._args_as_int(args)) + + def expansions(self, args) -> int: + return max(self._args_as_int(args)) + + +@inline_args +class EBNF_to_BNF(Transformer_InPlace): + def __init__(self): + self.new_rules = [] + self.rules_cache = {} + self.prefix = 'anon' + self.i = 0 + self.rule_options = None + + def _name_rule(self, inner: str): + new_name = '__%s_%s_%d' % (self.prefix, inner, self.i) + self.i += 1 + return new_name + + def _add_rule(self, key, name, expansions): + t = NonTerminal(name) + self.new_rules.append((name, expansions, self.rule_options)) + self.rules_cache[key] = t + return t + + def _add_recurse_rule(self, type_: str, expr: Tree): + try: + return self.rules_cache[expr] + except KeyError: + new_name = self._name_rule(type_) + t = NonTerminal(new_name) + tree = ST('expansions', [ + ST('expansion', [expr]), + ST('expansion', [t, expr]) + ]) + return self._add_rule(expr, new_name, tree) + + def _add_repeat_rule(self, a, b, target, atom): + """Generate a rule that repeats target ``a`` times, and repeats atom ``b`` times. + + When called recursively (into target), it repeats atom for x(n) times, where: + x(0) = 1 + x(n) = a(n) * x(n-1) + b + + Example rule when a=3, b=4: + + new_rule: target target target atom atom atom atom + + """ + key = (a, b, target, atom) + try: + return self.rules_cache[key] + except KeyError: + new_name = self._name_rule('repeat_a%d_b%d' % (a, b)) + tree = ST('expansions', [ST('expansion', [target] * a + [atom] * b)]) + return self._add_rule(key, new_name, tree) + + def _add_repeat_opt_rule(self, a, b, target, target_opt, atom): + """Creates a rule that matches atom 0 to (a*n+b)-1 times. + + When target matches n times atom, and target_opt 0 to n-1 times target_opt, + + First we generate target * i followed by target_opt, for i from 0 to a-1 + These match 0 to n*a - 1 times atom + + Then we generate target * a followed by atom * i, for i from 0 to b-1 + These match n*a to n*a + b-1 times atom + + The created rule will not have any shift/reduce conflicts so that it can be used with lalr + + Example rule when a=3, b=4: + + new_rule: target_opt + | target target_opt + | target target target_opt + + | target target target + | target target target atom + | target target target atom atom + | target target target atom atom atom + + """ + key = (a, b, target, atom, "opt") + try: + return self.rules_cache[key] + except KeyError: + new_name = self._name_rule('repeat_a%d_b%d_opt' % (a, b)) + tree = ST('expansions', [ + ST('expansion', [target]*i + [target_opt]) for i in range(a) + ] + [ + ST('expansion', [target]*a + [atom]*i) for i in range(b) + ]) + return self._add_rule(key, new_name, tree) + + def _generate_repeats(self, rule: Tree, mn: int, mx: int): + """Generates a rule tree that repeats ``rule`` exactly between ``mn`` to ``mx`` times. + """ + # For a small number of repeats, we can take the naive approach + if mx < REPEAT_BREAK_THRESHOLD: + return ST('expansions', [ST('expansion', [rule] * n) for n in range(mn, mx + 1)]) + + # For large repeat values, we break the repetition into sub-rules. + # We treat ``rule~mn..mx`` as ``rule~mn rule~0..(diff=mx-mn)``. + # We then use small_factors to split up mn and diff up into values [(a, b), ...] + # This values are used with the help of _add_repeat_rule and _add_repeat_rule_opt + # to generate a complete rule/expression that matches the corresponding number of repeats + mn_target = rule + for a, b in small_factors(mn, SMALL_FACTOR_THRESHOLD): + mn_target = self._add_repeat_rule(a, b, mn_target, rule) + if mx == mn: + return mn_target + + diff = mx - mn + 1 # We add one because _add_repeat_opt_rule generates rules that match one less + diff_factors = small_factors(diff, SMALL_FACTOR_THRESHOLD) + diff_target = rule # Match rule 1 times + diff_opt_target = ST('expansion', []) # match rule 0 times (e.g. up to 1 -1 times) + for a, b in diff_factors[:-1]: + diff_opt_target = self._add_repeat_opt_rule(a, b, diff_target, diff_opt_target, rule) + diff_target = self._add_repeat_rule(a, b, diff_target, rule) + + a, b = diff_factors[-1] + diff_opt_target = self._add_repeat_opt_rule(a, b, diff_target, diff_opt_target, rule) + + return ST('expansions', [ST('expansion', [mn_target] + [diff_opt_target])]) + + def expr(self, rule: Tree, op: Token, *args): + if op.value == '?': + empty = ST('expansion', []) + return ST('expansions', [rule, empty]) + elif op.value == '+': + # a : b c+ d + # --> + # a : b _c d + # _c : _c c | c; + return self._add_recurse_rule('plus', rule) + elif op.value == '*': + # a : b c* d + # --> + # a : b _c? d + # _c : _c c | c; + new_name = self._add_recurse_rule('star', rule) + return ST('expansions', [new_name, ST('expansion', [])]) + elif op.value == '~': + if len(args) == 1: + mn = mx = int(args[0]) + else: + mn, mx = map(int, args) + if mx < mn or mn < 0: + raise GrammarError("Bad Range for %s (%d..%d isn't allowed)" % (rule, mn, mx)) + + return self._generate_repeats(rule, mn, mx) + + assert False, op + + def maybe(self, rule: Tree): + keep_all_tokens = self.rule_options and self.rule_options.keep_all_tokens + rule_size = FindRuleSize(keep_all_tokens).transform(rule) + empty = ST('expansion', [_EMPTY] * rule_size) + return ST('expansions', [rule, empty]) + + +class SimplifyRule_Visitor(Visitor): + + @staticmethod + def _flatten(tree: Tree): + while tree.expand_kids_by_data(tree.data): + pass + + def expansion(self, tree: Tree): + # rules_list unpacking + # a : b (c|d) e + # --> + # a : b c e | b d e + # + # In AST terms: + # expansion(b, expansions(c, d), e) + # --> + # expansions( expansion(b, c, e), expansion(b, d, e) ) + + self._flatten(tree) + + for i, child in enumerate(tree.children): + if isinstance(child, Tree) and child.data == 'expansions': + tree.data = 'expansions' + tree.children = [self.visit(ST('expansion', [option if i == j else other + for j, other in enumerate(tree.children)])) + for option in dedup_list(child.children)] + self._flatten(tree) + break + + def alias(self, tree): + rule, alias_name = tree.children + if rule.data == 'expansions': + aliases = [] + for child in tree.children[0].children: + aliases.append(ST('alias', [child, alias_name])) + tree.data = 'expansions' + tree.children = aliases + + def expansions(self, tree: Tree): + self._flatten(tree) + # Ensure all children are unique + if len(set(tree.children)) != len(tree.children): + tree.children = dedup_list(tree.children) # dedup is expensive, so try to minimize its use + + +class RuleTreeToText(Transformer): + def expansions(self, x): + return x + + def expansion(self, symbols): + return symbols, None + + def alias(self, x): + (expansion, _alias), alias = x + assert _alias is None, (alias, expansion, '-', _alias) # Double alias not allowed + return expansion, alias.name + + +class PrepareAnonTerminals(Transformer_InPlace): + """Create a unique list of anonymous terminals. Attempt to give meaningful names to them when we add them""" + + def __init__(self, terminals): + self.terminals = terminals + self.term_set = {td.name for td in self.terminals} + self.term_reverse = {td.pattern: td for td in terminals} + self.i = 0 + self.rule_options = None + + @inline_args + def pattern(self, p): + value = p.value + if p in self.term_reverse and p.flags != self.term_reverse[p].pattern.flags: + raise GrammarError(u'Conflicting flags for the same terminal: %s' % p) + + term_name = None + + if isinstance(p, PatternStr): + try: + # If already defined, use the user-defined terminal name + term_name = self.term_reverse[p].name + except KeyError: + # Try to assign an indicative anon-terminal name + try: + term_name = _TERMINAL_NAMES[value] + except KeyError: + if value and is_id_continue(value) and is_id_start(value[0]) and value.upper() not in self.term_set: + term_name = value.upper() + + if term_name in self.term_set: + term_name = None + + elif isinstance(p, PatternRE): + if p in self.term_reverse: # Kind of a weird placement.name + term_name = self.term_reverse[p].name + else: + assert False, p + + if term_name is None: + term_name = '__ANON_%d' % self.i + self.i += 1 + + if term_name not in self.term_set: + assert p not in self.term_reverse + self.term_set.add(term_name) + termdef = TerminalDef(term_name, p) + self.term_reverse[p] = termdef + self.terminals.append(termdef) + + filter_out = False if self.rule_options and self.rule_options.keep_all_tokens else isinstance(p, PatternStr) + + return Terminal(term_name, filter_out=filter_out) + + +class _ReplaceSymbols(Transformer_InPlace): + """Helper for ApplyTemplates""" + + def __init__(self): + self.names = {} + + def value(self, c): + if len(c) == 1 and isinstance(c[0], Symbol) and c[0].name in self.names: + return self.names[c[0].name] + return self.__default__('value', c, None) + + def template_usage(self, c): + name = c[0].name + if name in self.names: + return self.__default__('template_usage', [self.names[name]] + c[1:], None) + return self.__default__('template_usage', c, None) + + +class ApplyTemplates(Transformer_InPlace): + """Apply the templates, creating new rules that represent the used templates""" + + def __init__(self, rule_defs): + self.rule_defs = rule_defs + self.replacer = _ReplaceSymbols() + self.created_templates = set() + + def template_usage(self, c): + name = c[0].name + args = c[1:] + result_name = "%s{%s}" % (name, ",".join(a.name for a in args)) + if result_name not in self.created_templates: + self.created_templates.add(result_name) + (_n, params, tree, options) ,= (t for t in self.rule_defs if t[0] == name) + assert len(params) == len(args), args + result_tree = deepcopy(tree) + self.replacer.names = dict(zip(params, args)) + self.replacer.transform(result_tree) + self.rule_defs.append((result_name, [], result_tree, deepcopy(options))) + return NonTerminal(result_name) + + +def _rfind(s, choices): + return max(s.rfind(c) for c in choices) + + +def eval_escaping(s): + w = '' + i = iter(s) + for n in i: + w += n + if n == '\\': + try: + n2 = next(i) + except StopIteration: + raise GrammarError("Literal ended unexpectedly (bad escaping): `%r`" % s) + if n2 == '\\': + w += '\\\\' + elif n2 not in 'Uuxnftr': + w += '\\' + w += n2 + w = w.replace('\\"', '"').replace("'", "\\'") + + to_eval = "u'''%s'''" % w + try: + s = literal_eval(to_eval) + except SyntaxError as e: + raise GrammarError(s, e) + + return s + + +def _literal_to_pattern(literal): + assert isinstance(literal, Token) + v = literal.value + flag_start = _rfind(v, '/"')+1 + assert flag_start > 0 + flags = v[flag_start:] + assert all(f in _RE_FLAGS for f in flags), flags + + if literal.type == 'STRING' and '\n' in v: + raise GrammarError('You cannot put newlines in string literals') + + if literal.type == 'REGEXP' and '\n' in v and 'x' not in flags: + raise GrammarError('You can only use newlines in regular expressions ' + 'with the `x` (verbose) flag') + + v = v[:flag_start] + assert v[0] == v[-1] and v[0] in '"/' + x = v[1:-1] + + s = eval_escaping(x) + + if s == "": + raise GrammarError("Empty terminals are not allowed (%s)" % literal) + + if literal.type == 'STRING': + s = s.replace('\\\\', '\\') + return PatternStr(s, flags, raw=literal.value) + elif literal.type == 'REGEXP': + return PatternRE(s, flags, raw=literal.value) + else: + assert False, 'Invariant failed: literal.type not in ["STRING", "REGEXP"]' + + +@inline_args +class PrepareLiterals(Transformer_InPlace): + def literal(self, literal): + return ST('pattern', [_literal_to_pattern(literal)]) + + def range(self, start, end): + assert start.type == end.type == 'STRING' + start = start.value[1:-1] + end = end.value[1:-1] + assert len(eval_escaping(start)) == len(eval_escaping(end)) == 1 + regexp = '[%s-%s]' % (start, end) + return ST('pattern', [PatternRE(regexp)]) + + +def _make_joined_pattern(regexp, flags_set) -> PatternRE: + return PatternRE(regexp, ()) + +class TerminalTreeToPattern(Transformer_NonRecursive): + def pattern(self, ps): + p ,= ps + return p + + def expansion(self, items: List[Pattern]) -> Pattern: + if not items: + return PatternStr('') + + if len(items) == 1: + return items[0] + + pattern = ''.join(i.to_regexp() for i in items) + return _make_joined_pattern(pattern, {i.flags for i in items}) + + def expansions(self, exps: List[Pattern]) -> Pattern: + if len(exps) == 1: + return exps[0] + + # Do a bit of sorting to make sure that the longest option is returned + # (Python's re module otherwise prefers just 'l' when given (l|ll) and both could match) + exps.sort(key=lambda x: (-x.max_width, -x.min_width, -len(x.value))) + + pattern = '(?:%s)' % ('|'.join(i.to_regexp() for i in exps)) + return _make_joined_pattern(pattern, {i.flags for i in exps}) + + def expr(self, args) -> Pattern: + inner: Pattern + inner, op = args[:2] + if op == '~': + if len(args) == 3: + op = "{%d}" % int(args[2]) + else: + mn, mx = map(int, args[2:]) + if mx < mn: + raise GrammarError("Bad Range for %s (%d..%d isn't allowed)" % (inner, mn, mx)) + op = "{%d,%d}" % (mn, mx) + else: + assert len(args) == 2 + return PatternRE('(?:%s)%s' % (inner.to_regexp(), op), inner.flags) + + def maybe(self, expr): + return self.expr(expr + ['?']) + + def alias(self, t): + raise GrammarError("Aliasing not allowed in terminals (You used -> in the wrong place)") + + def value(self, v): + return v[0] + + +class ValidateSymbols(Transformer_InPlace): + def value(self, v): + v ,= v + assert isinstance(v, (Tree, Symbol)) + return v + + +def nr_deepcopy_tree(t): + """Deepcopy tree `t` without recursion""" + return Transformer_NonRecursive(False).transform(t) + + +class Grammar: + + term_defs: List[Tuple[str, Tuple[Tree, int]]] + rule_defs: List[Tuple[str, Tuple[str, ...], Tree, RuleOptions]] + ignore: List[str] + + def __init__(self, rule_defs: List[Tuple[str, Tuple[str, ...], Tree, RuleOptions]], term_defs: List[Tuple[str, Tuple[Tree, int]]], ignore: List[str]) -> None: + self.term_defs = term_defs + self.rule_defs = rule_defs + self.ignore = ignore + + def compile(self, start, terminals_to_keep) -> Tuple[List[TerminalDef], List[Rule], List[str]]: + # We change the trees in-place (to support huge grammars) + # So deepcopy allows calling compile more than once. + term_defs = [(n, (nr_deepcopy_tree(t), p)) for n, (t, p) in self.term_defs] + rule_defs = [(n, p, nr_deepcopy_tree(t), o) for n, p, t, o in self.rule_defs] + + # =================== + # Compile Terminals + # =================== + + # Convert terminal-trees to strings/regexps + + for name, (term_tree, priority) in term_defs: + if term_tree is None: # Terminal added through %declare + continue + expansions = list(term_tree.find_data('expansion')) + if len(expansions) == 1 and not expansions[0].children: + raise GrammarError("Terminals cannot be empty (%s)" % name) + + transformer = PrepareLiterals() * TerminalTreeToPattern() + terminals = [TerminalDef(name, transformer.transform(term_tree), priority) + for name, (term_tree, priority) in term_defs if term_tree] + + # ================= + # Compile Rules + # ================= + + # 1. Pre-process terminals + anon_tokens_transf = PrepareAnonTerminals(terminals) + transformer = PrepareLiterals() * ValidateSymbols() * anon_tokens_transf # Adds to terminals + + # 2. Inline Templates + + transformer *= ApplyTemplates(rule_defs) + + # 3. Convert EBNF to BNF (and apply step 1 & 2) + ebnf_to_bnf = EBNF_to_BNF() + rules = [] + i = 0 + while i < len(rule_defs): # We have to do it like this because rule_defs might grow due to templates + name, params, rule_tree, options = rule_defs[i] + i += 1 + if len(params) != 0: # Dont transform templates + continue + rule_options = RuleOptions(keep_all_tokens=True) if options and options.keep_all_tokens else None + ebnf_to_bnf.rule_options = rule_options + ebnf_to_bnf.prefix = name + anon_tokens_transf.rule_options = rule_options + tree = transformer.transform(rule_tree) + res: Tree = ebnf_to_bnf.transform(tree) + rules.append((name, res, options)) + rules += ebnf_to_bnf.new_rules + + assert len(rules) == len({name for name, _t, _o in rules}), "Whoops, name collision" + + # 4. Compile tree to Rule objects + rule_tree_to_text = RuleTreeToText() + + simplify_rule = SimplifyRule_Visitor() + compiled_rules: List[Rule] = [] + for rule_content in rules: + name, tree, options = rule_content + simplify_rule.visit(tree) + expansions = rule_tree_to_text.transform(tree) + + for i, (expansion, alias) in enumerate(expansions): + if alias and name.startswith('_'): + raise GrammarError("Rule %s is marked for expansion (it starts with an underscore) and isn't allowed to have aliases (alias=%s)"% (name, alias)) + + empty_indices = tuple(x==_EMPTY for x in expansion) + if any(empty_indices): + exp_options = copy(options) or RuleOptions() + exp_options.empty_indices = empty_indices + expansion = [x for x in expansion if x!=_EMPTY] + else: + exp_options = options + + for sym in expansion: + assert isinstance(sym, Symbol) + if sym.is_term and exp_options and exp_options.keep_all_tokens: + assert isinstance(sym, Terminal) + sym.filter_out = False + rule = Rule(NonTerminal(name), expansion, i, alias, exp_options) + compiled_rules.append(rule) + + # Remove duplicates of empty rules, throw error for non-empty duplicates + if len(set(compiled_rules)) != len(compiled_rules): + duplicates = classify(compiled_rules, lambda x: x) + for dups in duplicates.values(): + if len(dups) > 1: + if dups[0].expansion: + raise GrammarError("Rules defined twice: %s\n\n(Might happen due to colliding expansion of optionals: [] or ?)" + % ''.join('\n * %s' % i for i in dups)) + + # Empty rule; assert all other attributes are equal + assert len({(r.alias, r.order, r.options) for r in dups}) == len(dups) + + # Remove duplicates + compiled_rules = list(OrderedSet(compiled_rules)) + + # Filter out unused rules + while True: + c = len(compiled_rules) + used_rules = {s for r in compiled_rules + for s in r.expansion + if isinstance(s, NonTerminal) + and s != r.origin} + used_rules |= {NonTerminal(s) for s in start} + compiled_rules, unused = classify_bool(compiled_rules, lambda r: r.origin in used_rules) + for r in unused: + logger.debug("Unused rule: %s", r) + if len(compiled_rules) == c: + break + + # Filter out unused terminals + if terminals_to_keep != '*': + used_terms = {t.name for r in compiled_rules + for t in r.expansion + if isinstance(t, Terminal)} + terminals, unused = classify_bool(terminals, lambda t: t.name in used_terms or t.name in self.ignore or t.name in terminals_to_keep) + if unused: + logger.debug("Unused terminals: %s", [t.name for t in unused]) + + return terminals, compiled_rules, self.ignore + + +PackageResource = namedtuple('PackageResource', 'pkg_name path') + + +class FromPackageLoader: + """ + Provides a simple way of creating custom import loaders that load from packages via ``pkgutil.get_data`` instead of using `open`. + This allows them to be compatible even from within zip files. + + Relative imports are handled, so you can just freely use them. + + pkg_name: The name of the package. You can probably provide `__name__` most of the time + search_paths: All the path that will be search on absolute imports. + """ + + pkg_name: str + search_paths: Sequence[str] + + def __init__(self, pkg_name: str, search_paths: Sequence[str]=("", )) -> None: + self.pkg_name = pkg_name + self.search_paths = search_paths + + def __repr__(self): + return "%s(%r, %r)" % (type(self).__name__, self.pkg_name, self.search_paths) + + def __call__(self, base_path: Union[None, str, PackageResource], grammar_path: str) -> Tuple[PackageResource, str]: + if base_path is None: + to_try = self.search_paths + else: + # Check whether or not the importing grammar was loaded by this module. + if not isinstance(base_path, PackageResource) or base_path.pkg_name != self.pkg_name: + # Technically false, but FileNotFound doesn't exist in python2.7, and this message should never reach the end user anyway + raise IOError() + to_try = [base_path.path] + + err = None + for path in to_try: + full_path = os.path.join(path, grammar_path) + try: + text: Optional[bytes] = pkgutil.get_data(self.pkg_name, full_path) + except IOError as e: + err = e + continue + else: + return PackageResource(self.pkg_name, full_path), (text.decode() if text else '') + + raise IOError('Cannot find grammar in given paths') from err + + +stdlib_loader = FromPackageLoader('lark', IMPORT_PATHS) + + + +def resolve_term_references(term_dict): + # TODO Solve with transitive closure (maybe) + + while True: + changed = False + for name, token_tree in term_dict.items(): + if token_tree is None: # Terminal added through %declare + continue + for exp in token_tree.find_data('value'): + item ,= exp.children + if isinstance(item, NonTerminal): + raise GrammarError("Rules aren't allowed inside terminals (%s in %s)" % (item, name)) + elif isinstance(item, Terminal): + try: + term_value = term_dict[item.name] + except KeyError: + raise GrammarError("Terminal used but not defined: %s" % item.name) + assert term_value is not None + exp.children[0] = term_value + changed = True + else: + assert isinstance(item, Tree) + if not changed: + break + + for name, term in term_dict.items(): + if term: # Not just declared + for child in term.children: + ids = [id(x) for x in child.iter_subtrees()] + if id(term) in ids: + raise GrammarError("Recursion in terminal '%s' (recursion is only allowed in rules, not terminals)" % name) + + + +def symbol_from_strcase(s): + assert isinstance(s, str) + return Terminal(s, filter_out=s.startswith('_')) if s.isupper() else NonTerminal(s) + +@inline_args +class PrepareGrammar(Transformer_InPlace): + def terminal(self, name): + return Terminal(str(name), filter_out=name.startswith('_')) + + def nonterminal(self, name): + return NonTerminal(name.value) + + +def _find_used_symbols(tree): + assert tree.data == 'expansions' + return {t.name for x in tree.find_data('expansion') + for t in x.scan_values(lambda t: isinstance(t, Symbol))} + + +def _get_parser(): + try: + return _get_parser.cache + except AttributeError: + terminals = [TerminalDef(name, PatternRE(value)) for name, value in TERMINALS.items()] + + rules = [(name.lstrip('?'), x, RuleOptions(expand1=name.startswith('?'))) + for name, x in RULES.items()] + rules = [Rule(NonTerminal(r), [symbol_from_strcase(s) for s in x.split()], i, None, o) + for r, xs, o in rules for i, x in enumerate(xs)] + + callback = ParseTreeBuilder(rules, ST).create_callback() + import re + lexer_conf = LexerConf(terminals, re, ['WS', 'COMMENT', 'BACKSLASH']) + parser_conf = ParserConf(rules, callback, ['start']) + lexer_conf.lexer_type = 'basic' + parser_conf.parser_type = 'lalr' + _get_parser.cache = ParsingFrontend(lexer_conf, parser_conf, None) + return _get_parser.cache + +GRAMMAR_ERRORS = [ + ('Incorrect type of value', ['a: 1\n']), + ('Unclosed parenthesis', ['a: (\n']), + ('Unmatched closing parenthesis', ['a: )\n', 'a: [)\n', 'a: (]\n']), + ('Expecting rule or terminal definition (missing colon)', ['a\n', 'A\n', 'a->\n', 'A->\n', 'a A\n']), + ('Illegal name for rules or terminals', ['Aa:\n']), + ('Alias expects lowercase name', ['a: -> "a"\n']), + ('Unexpected colon', ['a::\n', 'a: b:\n', 'a: B:\n', 'a: "a":\n']), + ('Misplaced operator', ['a: b??', 'a: b(?)', 'a:+\n', 'a:?\n', 'a:*\n', 'a:|*\n']), + ('Expecting option ("|") or a new rule or terminal definition', ['a:a\n()\n']), + ('Terminal names cannot contain dots', ['A.B\n']), + ('Expecting rule or terminal definition', ['"a"\n']), + ('%import expects a name', ['%import "a"\n']), + ('%ignore expects a value', ['%ignore %import\n']), + ] + +def _translate_parser_exception(parse, e): + error = e.match_examples(parse, GRAMMAR_ERRORS, use_accepts=True) + if error: + return error + elif 'STRING' in e.expected: + return "Expecting a value" + +def _parse_grammar(text, name, start='start'): + try: + tree = _get_parser().parse(text + '\n', start) + except UnexpectedCharacters as e: + context = e.get_context(text) + raise GrammarError("Unexpected input at line %d column %d in %s: \n\n%s" % + (e.line, e.column, name, context)) + except UnexpectedToken as e: + context = e.get_context(text) + error = _translate_parser_exception(_get_parser().parse, e) + if error: + raise GrammarError("%s, at line %s column %s\n\n%s" % (error, e.line, e.column, context)) + raise + + return PrepareGrammar().transform(tree) + + +def _error_repr(error): + if isinstance(error, UnexpectedToken): + error2 = _translate_parser_exception(_get_parser().parse, error) + if error2: + return error2 + expected = ', '.join(error.accepts or error.expected) + return "Unexpected token %r. Expected one of: {%s}" % (str(error.token), expected) + else: + return str(error) + +def _search_interactive_parser(interactive_parser, predicate): + def expand(node): + path, p = node + for choice in p.choices(): + t = Token(choice, '') + try: + new_p = p.feed_token(t) + except ParseError: # Illegal + pass + else: + yield path + (choice,), new_p + + for path, p in bfs_all_unique([((), interactive_parser)], expand): + if predicate(p): + return path, p + +def find_grammar_errors(text: str, start: str='start') -> List[Tuple[UnexpectedInput, str]]: + errors = [] + def on_error(e): + errors.append((e, _error_repr(e))) + + # recover to a new line + token_path, _ = _search_interactive_parser(e.interactive_parser.as_immutable(), lambda p: '_NL' in p.choices()) + for token_type in token_path: + e.interactive_parser.feed_token(Token(token_type, '')) + e.interactive_parser.feed_token(Token('_NL', '\n')) + return True + + _tree = _get_parser().parse(text + '\n', start, on_error=on_error) + + errors_by_line = classify(errors, lambda e: e[0].line) + errors = [el[0] for el in errors_by_line.values()] # already sorted + + for e in errors: + e[0].interactive_parser = None + return errors + + +def _get_mangle(prefix, aliases, base_mangle=None): + def mangle(s): + if s in aliases: + s = aliases[s] + else: + if s[0] == '_': + s = '_%s__%s' % (prefix, s[1:]) + else: + s = '%s__%s' % (prefix, s) + if base_mangle is not None: + s = base_mangle(s) + return s + return mangle + +def _mangle_definition_tree(exp, mangle): + if mangle is None: + return exp + exp = deepcopy(exp) # TODO: is this needed? + for t in exp.iter_subtrees(): + for i, c in enumerate(t.children): + if isinstance(c, Symbol): + t.children[i] = c.renamed(mangle) + + return exp + +def _make_rule_tuple(modifiers_tree, name, params, priority_tree, expansions): + if modifiers_tree.children: + m ,= modifiers_tree.children + expand1 = '?' in m + if expand1 and name.startswith('_'): + raise GrammarError("Inlined rules (_rule) cannot use the ?rule modifier.") + keep_all_tokens = '!' in m + else: + keep_all_tokens = False + expand1 = False + + if priority_tree.children: + p ,= priority_tree.children + priority = int(p) + else: + priority = None + + if params is not None: + params = [t.value for t in params.children] # For the grammar parser + + return name, params, expansions, RuleOptions(keep_all_tokens, expand1, priority=priority, + template_source=(name if params else None)) + + +class Definition: + def __init__(self, is_term, tree, params=(), options=None): + self.is_term = is_term + self.tree = tree + self.params = tuple(params) + self.options = options + +class GrammarBuilder: + + global_keep_all_tokens: bool + import_paths: List[Union[str, Callable]] + used_files: Dict[str, str] + + _definitions: Dict[str, Definition] + _ignore_names: List[str] + + def __init__(self, global_keep_all_tokens: bool=False, import_paths: Optional[List[Union[str, Callable]]]=None, used_files: Optional[Dict[str, str]]=None) -> None: + self.global_keep_all_tokens = global_keep_all_tokens + self.import_paths = import_paths or [] + self.used_files = used_files or {} + + self._definitions: Dict[str, Definition] = {} + self._ignore_names: List[str] = [] + + def _grammar_error(self, is_term, msg, *names): + args = {} + for i, name in enumerate(names, start=1): + postfix = '' if i == 1 else str(i) + args['name' + postfix] = name + args['type' + postfix] = lowercase_type = ("rule", "terminal")[is_term] + args['Type' + postfix] = lowercase_type.title() + raise GrammarError(msg.format(**args)) + + def _check_options(self, is_term, options): + if is_term: + if options is None: + options = 1 + elif not isinstance(options, int): + raise GrammarError("Terminal require a single int as 'options' (e.g. priority), got %s" % (type(options),)) + else: + if options is None: + options = RuleOptions() + elif not isinstance(options, RuleOptions): + raise GrammarError("Rules require a RuleOptions instance as 'options'") + if self.global_keep_all_tokens: + options.keep_all_tokens = True + return options + + + def _define(self, name, is_term, exp, params=(), options=None, *, override=False): + if name in self._definitions: + if not override: + self._grammar_error(is_term, "{Type} '{name}' defined more than once", name) + elif override: + self._grammar_error(is_term, "Cannot override a nonexisting {type} {name}", name) + + if name.startswith('__'): + self._grammar_error(is_term, 'Names starting with double-underscore are reserved (Error at {name})', name) + + self._definitions[name] = Definition(is_term, exp, params, self._check_options(is_term, options)) + + def _extend(self, name, is_term, exp, params=(), options=None): + if name not in self._definitions: + self._grammar_error(is_term, "Can't extend {type} {name} as it wasn't defined before", name) + + d = self._definitions[name] + + if is_term != d.is_term: + self._grammar_error(is_term, "Cannot extend {type} {name} - one is a terminal, while the other is not.", name) + if tuple(params) != d.params: + self._grammar_error(is_term, "Cannot extend {type} with different parameters: {name}", name) + + if d.tree is None: + self._grammar_error(is_term, "Can't extend {type} {name} - it is abstract.", name) + + # TODO: think about what to do with 'options' + base = d.tree + + assert isinstance(base, Tree) and base.data == 'expansions' + base.children.insert(0, exp) + + def _ignore(self, exp_or_name): + if isinstance(exp_or_name, str): + self._ignore_names.append(exp_or_name) + else: + assert isinstance(exp_or_name, Tree) + t = exp_or_name + if t.data == 'expansions' and len(t.children) == 1: + t2 ,= t.children + if t2.data=='expansion' and len(t2.children) == 1: + item ,= t2.children + if item.data == 'value': + item ,= item.children + if isinstance(item, Terminal): + # Keep terminal name, no need to create a new definition + self._ignore_names.append(item.name) + return + + name = '__IGNORE_%d'% len(self._ignore_names) + self._ignore_names.append(name) + self._definitions[name] = Definition(True, t, options=TOKEN_DEFAULT_PRIORITY) + + def _unpack_import(self, stmt, grammar_name): + if len(stmt.children) > 1: + path_node, arg1 = stmt.children + else: + path_node, = stmt.children + arg1 = None + + if isinstance(arg1, Tree): # Multi import + dotted_path = tuple(path_node.children) + names = arg1.children + aliases = dict(zip(names, names)) # Can't have aliased multi import, so all aliases will be the same as names + else: # Single import + dotted_path = tuple(path_node.children[:-1]) + if not dotted_path: + name ,= path_node.children + raise GrammarError("Nothing was imported from grammar `%s`" % name) + name = path_node.children[-1] # Get name from dotted path + aliases = {name.value: (arg1 or name).value} # Aliases if exist + + if path_node.data == 'import_lib': # Import from library + base_path = None + else: # Relative import + if grammar_name == '': # Import relative to script file path if grammar is coded in script + try: + base_file = os.path.abspath(sys.modules['__main__'].__file__) + except AttributeError: + base_file = None + else: + base_file = grammar_name # Import relative to grammar file path if external grammar file + if base_file: + if isinstance(base_file, PackageResource): + base_path = PackageResource(base_file.pkg_name, os.path.split(base_file.path)[0]) + else: + base_path = os.path.split(base_file)[0] + else: + base_path = os.path.abspath(os.path.curdir) + + return dotted_path, base_path, aliases + + def _unpack_definition(self, tree, mangle): + + if tree.data == 'rule': + name, params, exp, opts = _make_rule_tuple(*tree.children) + is_term = False + else: + name = tree.children[0].value + params = () # TODO terminal templates + opts = int(tree.children[1]) if len(tree.children) == 3 else TOKEN_DEFAULT_PRIORITY # priority + exp = tree.children[-1] + is_term = True + + if mangle is not None: + params = tuple(mangle(p) for p in params) + name = mangle(name) + + exp = _mangle_definition_tree(exp, mangle) + return name, is_term, exp, params, opts + + + def load_grammar(self, grammar_text: str, grammar_name: str="", mangle: Optional[Callable[[str], str]]=None) -> None: + tree = _parse_grammar(grammar_text, grammar_name) + + imports: Dict[Tuple[str, ...], Tuple[Optional[str], Dict[str, str]]] = {} + + for stmt in tree.children: + if stmt.data == 'import': + dotted_path, base_path, aliases = self._unpack_import(stmt, grammar_name) + try: + import_base_path, import_aliases = imports[dotted_path] + assert base_path == import_base_path, 'Inconsistent base_path for %s.' % '.'.join(dotted_path) + import_aliases.update(aliases) + except KeyError: + imports[dotted_path] = base_path, aliases + + for dotted_path, (base_path, aliases) in imports.items(): + self.do_import(dotted_path, base_path, aliases, mangle) + + for stmt in tree.children: + if stmt.data in ('term', 'rule'): + self._define(*self._unpack_definition(stmt, mangle)) + elif stmt.data == 'override': + r ,= stmt.children + self._define(*self._unpack_definition(r, mangle), override=True) + elif stmt.data == 'extend': + r ,= stmt.children + self._extend(*self._unpack_definition(r, mangle)) + elif stmt.data == 'ignore': + # if mangle is not None, we shouldn't apply ignore, since we aren't in a toplevel grammar + if mangle is None: + self._ignore(*stmt.children) + elif stmt.data == 'declare': + for symbol in stmt.children: + assert isinstance(symbol, Symbol), symbol + is_term = isinstance(symbol, Terminal) + if mangle is None: + name = symbol.name + else: + name = mangle(symbol.name) + self._define(name, is_term, None) + elif stmt.data == 'import': + pass + else: + assert False, stmt + + + term_defs = { name: d.tree + for name, d in self._definitions.items() + if d.is_term + } + resolve_term_references(term_defs) + + + def _remove_unused(self, used): + def rule_dependencies(symbol): + try: + d = self._definitions[symbol] + except KeyError: + return [] + if d.is_term: + return [] + return _find_used_symbols(d.tree) - set(d.params) + + _used = set(bfs(used, rule_dependencies)) + self._definitions = {k: v for k, v in self._definitions.items() if k in _used} + + + def do_import(self, dotted_path: Tuple[str, ...], base_path: Optional[str], aliases: Dict[str, str], base_mangle: Optional[Callable[[str], str]]=None) -> None: + assert dotted_path + mangle = _get_mangle('__'.join(dotted_path), aliases, base_mangle) + grammar_path = os.path.join(*dotted_path) + EXT + to_try = self.import_paths + ([base_path] if base_path is not None else []) + [stdlib_loader] + for source in to_try: + try: + if callable(source): + joined_path, text = source(base_path, grammar_path) + else: + joined_path = os.path.join(source, grammar_path) + with open(joined_path, encoding='utf8') as f: + text = f.read() + except IOError: + continue + else: + h = sha256_digest(text) + if self.used_files.get(joined_path, h) != h: + raise RuntimeError("Grammar file was changed during importing") + self.used_files[joined_path] = h + + gb = GrammarBuilder(self.global_keep_all_tokens, self.import_paths, self.used_files) + gb.load_grammar(text, joined_path, mangle) + gb._remove_unused(map(mangle, aliases)) + for name in gb._definitions: + if name in self._definitions: + raise GrammarError("Cannot import '%s' from '%s': Symbol already defined." % (name, grammar_path)) + + self._definitions.update(**gb._definitions) + break + else: + # Search failed. Make Python throw a nice error. + open(grammar_path, encoding='utf8') + assert False, "Couldn't import grammar %s, but a corresponding file was found at a place where lark doesn't search for it" % (dotted_path,) + + + def validate(self) -> None: + for name, d in self._definitions.items(): + params = d.params + exp = d.tree + + for i, p in enumerate(params): + if p in self._definitions: + raise GrammarError("Template Parameter conflicts with rule %s (in template %s)" % (p, name)) + if p in params[:i]: + raise GrammarError("Duplicate Template Parameter %s (in template %s)" % (p, name)) + + if exp is None: # Remaining checks don't apply to abstract rules/terminals (created with %declare) + continue + + for temp in exp.find_data('template_usage'): + sym = temp.children[0].name + args = temp.children[1:] + if sym not in params: + if sym not in self._definitions: + self._grammar_error(d.is_term, "Template '%s' used but not defined (in {type} {name})" % sym, name) + if len(args) != len(self._definitions[sym].params): + expected, actual = len(self._definitions[sym].params), len(args) + self._grammar_error(d.is_term, "Wrong number of template arguments used for {name} " + "(expected %s, got %s) (in {type2} {name2})" % (expected, actual), sym, name) + + for sym in _find_used_symbols(exp): + if sym not in self._definitions and sym not in params: + self._grammar_error(d.is_term, "{Type} '{name}' used but not defined (in {type2} {name2})", sym, name) + + if not set(self._definitions).issuperset(self._ignore_names): + raise GrammarError("Terminals %s were marked to ignore but were not defined!" % (set(self._ignore_names) - set(self._definitions))) + + def build(self) -> Grammar: + self.validate() + rule_defs = [] + term_defs = [] + for name, d in self._definitions.items(): + (params, exp, options) = d.params, d.tree, d.options + if d.is_term: + assert len(params) == 0 + term_defs.append((name, (exp, options))) + else: + rule_defs.append((name, params, exp, options)) + # resolve_term_references(term_defs) + return Grammar(rule_defs, term_defs, self._ignore_names) + + +def verify_used_files(file_hashes): + for path, old in file_hashes.items(): + text = None + if isinstance(path, str) and os.path.exists(path): + with open(path, encoding='utf8') as f: + text = f.read() + elif isinstance(path, PackageResource): + with suppress(IOError): + text = pkgutil.get_data(*path).decode('utf-8') + if text is None: # We don't know how to load the path. ignore it. + continue + + current = sha256_digest(text) + if old != current: + logger.info("File %r changed, rebuilding Parser" % path) + return False + return True + +def list_grammar_imports(grammar, import_paths=[]): + "Returns a list of paths to the lark grammars imported by the given grammar (recursively)" + builder = GrammarBuilder(False, import_paths) + builder.load_grammar(grammar, '') + return list(builder.used_files.keys()) + +def load_grammar(grammar, source, import_paths, global_keep_all_tokens): + builder = GrammarBuilder(global_keep_all_tokens, import_paths) + builder.load_grammar(grammar, source) + return builder.build(), builder.used_files + + +def sha256_digest(s: str) -> str: + """Get the sha256 digest of a string + + Supports the `usedforsecurity` argument for Python 3.9+ to allow running on + a FIPS-enabled system. + """ + if sys.version_info >= (3, 9): + return hashlib.sha256(s.encode('utf8'), usedforsecurity=False).hexdigest() + else: + return hashlib.sha256(s.encode('utf8')).hexdigest() diff --git a/ccxt/static_dependencies/lark/parse_tree_builder.py b/ccxt/static_dependencies/lark/parse_tree_builder.py new file mode 100644 index 0000000..e3a4171 --- /dev/null +++ b/ccxt/static_dependencies/lark/parse_tree_builder.py @@ -0,0 +1,391 @@ +"""Provides functions for the automatic building and shaping of the parse-tree.""" + +from typing import List + +from .exceptions import GrammarError, ConfigurationError +from .lexer import Token +from .tree import Tree +from .visitors import Transformer_InPlace +from .visitors import _vargs_meta, _vargs_meta_inline + +###{standalone +from functools import partial, wraps +from itertools import product + + +class ExpandSingleChild: + def __init__(self, node_builder): + self.node_builder = node_builder + + def __call__(self, children): + if len(children) == 1: + return children[0] + else: + return self.node_builder(children) + + + +class PropagatePositions: + def __init__(self, node_builder, node_filter=None): + self.node_builder = node_builder + self.node_filter = node_filter + + def __call__(self, children): + res = self.node_builder(children) + + if isinstance(res, Tree): + # Calculate positions while the tree is streaming, according to the rule: + # - nodes start at the start of their first child's container, + # and end at the end of their last child's container. + # Containers are nodes that take up space in text, but have been inlined in the tree. + + res_meta = res.meta + + first_meta = self._pp_get_meta(children) + if first_meta is not None: + if not hasattr(res_meta, 'line'): + # meta was already set, probably because the rule has been inlined (e.g. `?rule`) + res_meta.line = getattr(first_meta, 'container_line', first_meta.line) + res_meta.column = getattr(first_meta, 'container_column', first_meta.column) + res_meta.start_pos = getattr(first_meta, 'container_start_pos', first_meta.start_pos) + res_meta.empty = False + + res_meta.container_line = getattr(first_meta, 'container_line', first_meta.line) + res_meta.container_column = getattr(first_meta, 'container_column', first_meta.column) + res_meta.container_start_pos = getattr(first_meta, 'container_start_pos', first_meta.start_pos) + + last_meta = self._pp_get_meta(reversed(children)) + if last_meta is not None: + if not hasattr(res_meta, 'end_line'): + res_meta.end_line = getattr(last_meta, 'container_end_line', last_meta.end_line) + res_meta.end_column = getattr(last_meta, 'container_end_column', last_meta.end_column) + res_meta.end_pos = getattr(last_meta, 'container_end_pos', last_meta.end_pos) + res_meta.empty = False + + res_meta.container_end_line = getattr(last_meta, 'container_end_line', last_meta.end_line) + res_meta.container_end_column = getattr(last_meta, 'container_end_column', last_meta.end_column) + res_meta.container_end_pos = getattr(last_meta, 'container_end_pos', last_meta.end_pos) + + return res + + def _pp_get_meta(self, children): + for c in children: + if self.node_filter is not None and not self.node_filter(c): + continue + if isinstance(c, Tree): + if not c.meta.empty: + return c.meta + elif isinstance(c, Token): + return c + elif hasattr(c, '__lark_meta__'): + return c.__lark_meta__() + +def make_propagate_positions(option): + if callable(option): + return partial(PropagatePositions, node_filter=option) + elif option is True: + return PropagatePositions + elif option is False: + return None + + raise ConfigurationError('Invalid option for propagate_positions: %r' % option) + + +class ChildFilter: + def __init__(self, to_include, append_none, node_builder): + self.node_builder = node_builder + self.to_include = to_include + self.append_none = append_none + + def __call__(self, children): + filtered = [] + + for i, to_expand, add_none in self.to_include: + if add_none: + filtered += [None] * add_none + if to_expand: + filtered += children[i].children + else: + filtered.append(children[i]) + + if self.append_none: + filtered += [None] * self.append_none + + return self.node_builder(filtered) + + +class ChildFilterLALR(ChildFilter): + """Optimized childfilter for LALR (assumes no duplication in parse tree, so it's safe to change it)""" + + def __call__(self, children): + filtered = [] + for i, to_expand, add_none in self.to_include: + if add_none: + filtered += [None] * add_none + if to_expand: + if filtered: + filtered += children[i].children + else: # Optimize for left-recursion + filtered = children[i].children + else: + filtered.append(children[i]) + + if self.append_none: + filtered += [None] * self.append_none + + return self.node_builder(filtered) + + +class ChildFilterLALR_NoPlaceholders(ChildFilter): + "Optimized childfilter for LALR (assumes no duplication in parse tree, so it's safe to change it)" + def __init__(self, to_include, node_builder): + self.node_builder = node_builder + self.to_include = to_include + + def __call__(self, children): + filtered = [] + for i, to_expand in self.to_include: + if to_expand: + if filtered: + filtered += children[i].children + else: # Optimize for left-recursion + filtered = children[i].children + else: + filtered.append(children[i]) + return self.node_builder(filtered) + + +def _should_expand(sym): + return not sym.is_term and sym.name.startswith('_') + + +def maybe_create_child_filter(expansion, keep_all_tokens, ambiguous, _empty_indices: List[bool]): + # Prepare empty_indices as: How many Nones to insert at each index? + if _empty_indices: + assert _empty_indices.count(False) == len(expansion) + s = ''.join(str(int(b)) for b in _empty_indices) + empty_indices = [len(ones) for ones in s.split('0')] + assert len(empty_indices) == len(expansion)+1, (empty_indices, len(expansion)) + else: + empty_indices = [0] * (len(expansion)+1) + + to_include = [] + nones_to_add = 0 + for i, sym in enumerate(expansion): + nones_to_add += empty_indices[i] + if keep_all_tokens or not (sym.is_term and sym.filter_out): + to_include.append((i, _should_expand(sym), nones_to_add)) + nones_to_add = 0 + + nones_to_add += empty_indices[len(expansion)] + + if _empty_indices or len(to_include) < len(expansion) or any(to_expand for i, to_expand,_ in to_include): + if _empty_indices or ambiguous: + return partial(ChildFilter if ambiguous else ChildFilterLALR, to_include, nones_to_add) + else: + # LALR without placeholders + return partial(ChildFilterLALR_NoPlaceholders, [(i, x) for i,x,_ in to_include]) + + +class AmbiguousExpander: + """Deal with the case where we're expanding children ('_rule') into a parent but the children + are ambiguous. i.e. (parent->_ambig->_expand_this_rule). In this case, make the parent itself + ambiguous with as many copies as there are ambiguous children, and then copy the ambiguous children + into the right parents in the right places, essentially shifting the ambiguity up the tree.""" + def __init__(self, to_expand, tree_class, node_builder): + self.node_builder = node_builder + self.tree_class = tree_class + self.to_expand = to_expand + + def __call__(self, children): + def _is_ambig_tree(t): + return hasattr(t, 'data') and t.data == '_ambig' + + # -- When we're repeatedly expanding ambiguities we can end up with nested ambiguities. + # All children of an _ambig node should be a derivation of that ambig node, hence + # it is safe to assume that if we see an _ambig node nested within an ambig node + # it is safe to simply expand it into the parent _ambig node as an alternative derivation. + ambiguous = [] + for i, child in enumerate(children): + if _is_ambig_tree(child): + if i in self.to_expand: + ambiguous.append(i) + + child.expand_kids_by_data('_ambig') + + if not ambiguous: + return self.node_builder(children) + + expand = [child.children if i in ambiguous else (child,) for i, child in enumerate(children)] + return self.tree_class('_ambig', [self.node_builder(list(f)) for f in product(*expand)]) + + +def maybe_create_ambiguous_expander(tree_class, expansion, keep_all_tokens): + to_expand = [i for i, sym in enumerate(expansion) + if keep_all_tokens or ((not (sym.is_term and sym.filter_out)) and _should_expand(sym))] + if to_expand: + return partial(AmbiguousExpander, to_expand, tree_class) + + +class AmbiguousIntermediateExpander: + """ + Propagate ambiguous intermediate nodes and their derivations up to the + current rule. + + In general, converts + + rule + _iambig + _inter + someChildren1 + ... + _inter + someChildren2 + ... + someChildren3 + ... + + to + + _ambig + rule + someChildren1 + ... + someChildren3 + ... + rule + someChildren2 + ... + someChildren3 + ... + rule + childrenFromNestedIambigs + ... + someChildren3 + ... + ... + + propagating up any nested '_iambig' nodes along the way. + """ + + def __init__(self, tree_class, node_builder): + self.node_builder = node_builder + self.tree_class = tree_class + + def __call__(self, children): + def _is_iambig_tree(child): + return hasattr(child, 'data') and child.data == '_iambig' + + def _collapse_iambig(children): + """ + Recursively flatten the derivations of the parent of an '_iambig' + node. Returns a list of '_inter' nodes guaranteed not + to contain any nested '_iambig' nodes, or None if children does + not contain an '_iambig' node. + """ + + # Due to the structure of the SPPF, + # an '_iambig' node can only appear as the first child + if children and _is_iambig_tree(children[0]): + iambig_node = children[0] + result = [] + for grandchild in iambig_node.children: + collapsed = _collapse_iambig(grandchild.children) + if collapsed: + for child in collapsed: + child.children += children[1:] + result += collapsed + else: + new_tree = self.tree_class('_inter', grandchild.children + children[1:]) + result.append(new_tree) + return result + + collapsed = _collapse_iambig(children) + if collapsed: + processed_nodes = [self.node_builder(c.children) for c in collapsed] + return self.tree_class('_ambig', processed_nodes) + + return self.node_builder(children) + + + +def inplace_transformer(func): + @wraps(func) + def f(children): + # function name in a Transformer is a rule name. + tree = Tree(func.__name__, children) + return func(tree) + return f + + +def apply_visit_wrapper(func, name, wrapper): + if wrapper is _vargs_meta or wrapper is _vargs_meta_inline: + raise NotImplementedError("Meta args not supported for internal transformer") + + @wraps(func) + def f(children): + return wrapper(func, name, children, None) + return f + + +class ParseTreeBuilder: + def __init__(self, rules, tree_class, propagate_positions=False, ambiguous=False, maybe_placeholders=False): + self.tree_class = tree_class + self.propagate_positions = propagate_positions + self.ambiguous = ambiguous + self.maybe_placeholders = maybe_placeholders + + self.rule_builders = list(self._init_builders(rules)) + + def _init_builders(self, rules): + propagate_positions = make_propagate_positions(self.propagate_positions) + + for rule in rules: + options = rule.options + keep_all_tokens = options.keep_all_tokens + expand_single_child = options.expand1 + + wrapper_chain = list(filter(None, [ + (expand_single_child and not rule.alias) and ExpandSingleChild, + maybe_create_child_filter(rule.expansion, keep_all_tokens, self.ambiguous, options.empty_indices if self.maybe_placeholders else None), + propagate_positions, + self.ambiguous and maybe_create_ambiguous_expander(self.tree_class, rule.expansion, keep_all_tokens), + self.ambiguous and partial(AmbiguousIntermediateExpander, self.tree_class) + ])) + + yield rule, wrapper_chain + + def create_callback(self, transformer=None): + callbacks = {} + + default_handler = getattr(transformer, '__default__', None) + if default_handler: + def default_callback(data, children): + return default_handler(data, children, None) + else: + default_callback = self.tree_class + + for rule, wrapper_chain in self.rule_builders: + + user_callback_name = rule.alias or rule.options.template_source or rule.origin.name + try: + f = getattr(transformer, user_callback_name) + wrapper = getattr(f, 'visit_wrapper', None) + if wrapper is not None: + f = apply_visit_wrapper(f, user_callback_name, wrapper) + elif isinstance(transformer, Transformer_InPlace): + f = inplace_transformer(f) + except AttributeError: + f = partial(default_callback, user_callback_name) + + for w in wrapper_chain: + f = w(f) + + if rule in callbacks: + raise GrammarError("Rule '%s' already exists" % (rule,)) + + callbacks[rule] = f + + return callbacks + +###} diff --git a/ccxt/static_dependencies/lark/parser_frontends.py b/ccxt/static_dependencies/lark/parser_frontends.py new file mode 100644 index 0000000..186058a --- /dev/null +++ b/ccxt/static_dependencies/lark/parser_frontends.py @@ -0,0 +1,257 @@ +from typing import Any, Callable, Dict, Optional, Collection, Union, TYPE_CHECKING + +from .exceptions import ConfigurationError, GrammarError, assert_config +from .utils import get_regexp_width, Serialize +from .lexer import LexerThread, BasicLexer, ContextualLexer, Lexer +from .parsers import earley, xearley, cyk +from .parsers.lalr_parser import LALR_Parser +from .tree import Tree +from .common import LexerConf, ParserConf, _ParserArgType, _LexerArgType + +if TYPE_CHECKING: + from .parsers.lalr_analysis import ParseTableBase + + +###{standalone + +def _wrap_lexer(lexer_class): + future_interface = getattr(lexer_class, '__future_interface__', False) + if future_interface: + return lexer_class + else: + class CustomLexerWrapper(Lexer): + def __init__(self, lexer_conf): + self.lexer = lexer_class(lexer_conf) + def lex(self, lexer_state, parser_state): + return self.lexer.lex(lexer_state.text) + return CustomLexerWrapper + + +def _deserialize_parsing_frontend(data, memo, lexer_conf, callbacks, options): + parser_conf = ParserConf.deserialize(data['parser_conf'], memo) + cls = (options and options._plugins.get('LALR_Parser')) or LALR_Parser + parser = cls.deserialize(data['parser'], memo, callbacks, options.debug) + parser_conf.callbacks = callbacks + return ParsingFrontend(lexer_conf, parser_conf, options, parser=parser) + + +_parser_creators: 'Dict[str, Callable[[LexerConf, Any, Any], Any]]' = {} + + +class ParsingFrontend(Serialize): + __serialize_fields__ = 'lexer_conf', 'parser_conf', 'parser' + + lexer_conf: LexerConf + parser_conf: ParserConf + options: Any + + def __init__(self, lexer_conf: LexerConf, parser_conf: ParserConf, options, parser=None): + self.parser_conf = parser_conf + self.lexer_conf = lexer_conf + self.options = options + + # Set-up parser + if parser: # From cache + self.parser = parser + else: + create_parser = _parser_creators.get(parser_conf.parser_type) + assert create_parser is not None, "{} is not supported in standalone mode".format( + parser_conf.parser_type + ) + self.parser = create_parser(lexer_conf, parser_conf, options) + + # Set-up lexer + lexer_type = lexer_conf.lexer_type + self.skip_lexer = False + if lexer_type in ('dynamic', 'dynamic_complete'): + assert lexer_conf.postlex is None + self.skip_lexer = True + return + + if isinstance(lexer_type, type): + assert issubclass(lexer_type, Lexer) + self.lexer = _wrap_lexer(lexer_type)(lexer_conf) + elif isinstance(lexer_type, str): + create_lexer = { + 'basic': create_basic_lexer, + 'contextual': create_contextual_lexer, + }[lexer_type] + self.lexer = create_lexer(lexer_conf, self.parser, lexer_conf.postlex, options) + else: + raise TypeError("Bad value for lexer_type: {lexer_type}") + + if lexer_conf.postlex: + self.lexer = PostLexConnector(self.lexer, lexer_conf.postlex) + + def _verify_start(self, start=None): + if start is None: + start_decls = self.parser_conf.start + if len(start_decls) > 1: + raise ConfigurationError("Lark initialized with more than 1 possible start rule. Must specify which start rule to parse", start_decls) + start ,= start_decls + elif start not in self.parser_conf.start: + raise ConfigurationError("Unknown start rule %s. Must be one of %r" % (start, self.parser_conf.start)) + return start + + def _make_lexer_thread(self, text: str) -> Union[str, LexerThread]: + cls = (self.options and self.options._plugins.get('LexerThread')) or LexerThread + return text if self.skip_lexer else cls.from_text(self.lexer, text) + + def parse(self, text: str, start=None, on_error=None): + chosen_start = self._verify_start(start) + kw = {} if on_error is None else {'on_error': on_error} + stream = self._make_lexer_thread(text) + return self.parser.parse(stream, chosen_start, **kw) + + def parse_interactive(self, text: Optional[str]=None, start=None): + # TODO BREAK - Change text from Optional[str] to text: str = ''. + # Would break behavior of exhaust_lexer(), which currently raises TypeError, and after the change would just return [] + chosen_start = self._verify_start(start) + if self.parser_conf.parser_type != 'lalr': + raise ConfigurationError("parse_interactive() currently only works with parser='lalr' ") + stream = self._make_lexer_thread(text) # type: ignore[arg-type] + return self.parser.parse_interactive(stream, chosen_start) + + +def _validate_frontend_args(parser, lexer) -> None: + assert_config(parser, ('lalr', 'earley', 'cyk')) + if not isinstance(lexer, type): # not custom lexer? + expected = { + 'lalr': ('basic', 'contextual'), + 'earley': ('basic', 'dynamic', 'dynamic_complete'), + 'cyk': ('basic', ), + }[parser] + assert_config(lexer, expected, 'Parser %r does not support lexer %%r, expected one of %%s' % parser) + + +def _get_lexer_callbacks(transformer, terminals): + result = {} + for terminal in terminals: + callback = getattr(transformer, terminal.name, None) + if callback is not None: + result[terminal.name] = callback + return result + +class PostLexConnector: + def __init__(self, lexer, postlexer): + self.lexer = lexer + self.postlexer = postlexer + + def lex(self, lexer_state, parser_state): + i = self.lexer.lex(lexer_state, parser_state) + return self.postlexer.process(i) + + + +def create_basic_lexer(lexer_conf, parser, postlex, options) -> BasicLexer: + cls = (options and options._plugins.get('BasicLexer')) or BasicLexer + return cls(lexer_conf) + +def create_contextual_lexer(lexer_conf: LexerConf, parser, postlex, options) -> ContextualLexer: + cls = (options and options._plugins.get('ContextualLexer')) or ContextualLexer + parse_table: ParseTableBase[int] = parser._parse_table + states: Dict[int, Collection[str]] = {idx:list(t.keys()) for idx, t in parse_table.states.items()} + always_accept: Collection[str] = postlex.always_accept if postlex else () + return cls(lexer_conf, states, always_accept=always_accept) + +def create_lalr_parser(lexer_conf: LexerConf, parser_conf: ParserConf, options=None) -> LALR_Parser: + debug = options.debug if options else False + strict = options.strict if options else False + cls = (options and options._plugins.get('LALR_Parser')) or LALR_Parser + return cls(parser_conf, debug=debug, strict=strict) + +_parser_creators['lalr'] = create_lalr_parser + +###} + +class EarleyRegexpMatcher: + def __init__(self, lexer_conf): + self.regexps = {} + for t in lexer_conf.terminals: + regexp = t.pattern.to_regexp() + try: + width = get_regexp_width(regexp)[0] + except ValueError: + raise GrammarError("Bad regexp in token %s: %s" % (t.name, regexp)) + else: + if width == 0: + raise GrammarError("Dynamic Earley doesn't allow zero-width regexps", t) + if lexer_conf.use_bytes: + regexp = regexp.encode('utf-8') + + self.regexps[t.name] = lexer_conf.re_module.compile(regexp, lexer_conf.g_regex_flags) + + def match(self, term, text, index=0): + return self.regexps[term.name].match(text, index) + + +def create_earley_parser__dynamic(lexer_conf: LexerConf, parser_conf: ParserConf, **kw): + if lexer_conf.callbacks: + raise GrammarError("Earley's dynamic lexer doesn't support lexer_callbacks.") + + earley_matcher = EarleyRegexpMatcher(lexer_conf) + return xearley.Parser(lexer_conf, parser_conf, earley_matcher.match, **kw) + +def _match_earley_basic(term, token): + return term.name == token.type + +def create_earley_parser__basic(lexer_conf: LexerConf, parser_conf: ParserConf, **kw): + return earley.Parser(lexer_conf, parser_conf, _match_earley_basic, **kw) + +def create_earley_parser(lexer_conf: LexerConf, parser_conf: ParserConf, options) -> earley.Parser: + resolve_ambiguity = options.ambiguity == 'resolve' + debug = options.debug if options else False + tree_class = options.tree_class or Tree if options.ambiguity != 'forest' else None + + extra = {} + if lexer_conf.lexer_type == 'dynamic': + f = create_earley_parser__dynamic + elif lexer_conf.lexer_type == 'dynamic_complete': + extra['complete_lex'] = True + f = create_earley_parser__dynamic + else: + f = create_earley_parser__basic + + return f(lexer_conf, parser_conf, resolve_ambiguity=resolve_ambiguity, + debug=debug, tree_class=tree_class, ordered_sets=options.ordered_sets, **extra) + + + +class CYK_FrontEnd: + def __init__(self, lexer_conf, parser_conf, options=None): + self.parser = cyk.Parser(parser_conf.rules) + + self.callbacks = parser_conf.callbacks + + def parse(self, lexer_thread, start): + tokens = list(lexer_thread.lex(None)) + tree = self.parser.parse(tokens, start) + return self._transform(tree) + + def _transform(self, tree): + subtrees = list(tree.iter_subtrees()) + for subtree in subtrees: + subtree.children = [self._apply_callback(c) if isinstance(c, Tree) else c for c in subtree.children] + + return self._apply_callback(tree) + + def _apply_callback(self, tree): + return self.callbacks[tree.rule](tree.children) + + +_parser_creators['earley'] = create_earley_parser +_parser_creators['cyk'] = CYK_FrontEnd + + +def _construct_parsing_frontend( + parser_type: _ParserArgType, + lexer_type: _LexerArgType, + lexer_conf, + parser_conf, + options +): + assert isinstance(lexer_conf, LexerConf) + assert isinstance(parser_conf, ParserConf) + parser_conf.parser_type = parser_type + lexer_conf.lexer_type = lexer_type + return ParsingFrontend(lexer_conf, parser_conf, options) diff --git a/ccxt/static_dependencies/lark/parsers/__init__.py b/ccxt/static_dependencies/lark/parsers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ccxt/static_dependencies/lark/parsers/cyk.py b/ccxt/static_dependencies/lark/parsers/cyk.py new file mode 100644 index 0000000..b5334f9 --- /dev/null +++ b/ccxt/static_dependencies/lark/parsers/cyk.py @@ -0,0 +1,340 @@ +"""This module implements a CYK parser.""" + +# Author: https://github.com/ehudt (2018) +# +# Adapted by Erez + + +from collections import defaultdict +import itertools + +from ..exceptions import ParseError +from ..lexer import Token +from ..tree import Tree +from ..grammar import Terminal as T, NonTerminal as NT, Symbol + +def match(t, s): + assert isinstance(t, T) + return t.name == s.type + + +class Rule: + """Context-free grammar rule.""" + + def __init__(self, lhs, rhs, weight, alias): + super(Rule, self).__init__() + assert isinstance(lhs, NT), lhs + assert all(isinstance(x, NT) or isinstance(x, T) for x in rhs), rhs + self.lhs = lhs + self.rhs = rhs + self.weight = weight + self.alias = alias + + def __str__(self): + return '%s -> %s' % (str(self.lhs), ' '.join(str(x) for x in self.rhs)) + + def __repr__(self): + return str(self) + + def __hash__(self): + return hash((self.lhs, tuple(self.rhs))) + + def __eq__(self, other): + return self.lhs == other.lhs and self.rhs == other.rhs + + def __ne__(self, other): + return not (self == other) + + +class Grammar: + """Context-free grammar.""" + + def __init__(self, rules): + self.rules = frozenset(rules) + + def __eq__(self, other): + return self.rules == other.rules + + def __str__(self): + return '\n' + '\n'.join(sorted(repr(x) for x in self.rules)) + '\n' + + def __repr__(self): + return str(self) + + +# Parse tree data structures +class RuleNode: + """A node in the parse tree, which also contains the full rhs rule.""" + + def __init__(self, rule, children, weight=0): + self.rule = rule + self.children = children + self.weight = weight + + def __repr__(self): + return 'RuleNode(%s, [%s])' % (repr(self.rule.lhs), ', '.join(str(x) for x in self.children)) + + + +class Parser: + """Parser wrapper.""" + + def __init__(self, rules): + super(Parser, self).__init__() + self.orig_rules = {rule: rule for rule in rules} + rules = [self._to_rule(rule) for rule in rules] + self.grammar = to_cnf(Grammar(rules)) + + def _to_rule(self, lark_rule): + """Converts a lark rule, (lhs, rhs, callback, options), to a Rule.""" + assert isinstance(lark_rule.origin, NT) + assert all(isinstance(x, Symbol) for x in lark_rule.expansion) + return Rule( + lark_rule.origin, lark_rule.expansion, + weight=lark_rule.options.priority if lark_rule.options.priority else 0, + alias=lark_rule) + + def parse(self, tokenized, start): # pylint: disable=invalid-name + """Parses input, which is a list of tokens.""" + assert start + start = NT(start) + + table, trees = _parse(tokenized, self.grammar) + # Check if the parse succeeded. + if all(r.lhs != start for r in table[(0, len(tokenized) - 1)]): + raise ParseError('Parsing failed.') + parse = trees[(0, len(tokenized) - 1)][start] + return self._to_tree(revert_cnf(parse)) + + def _to_tree(self, rule_node): + """Converts a RuleNode parse tree to a lark Tree.""" + orig_rule = self.orig_rules[rule_node.rule.alias] + children = [] + for child in rule_node.children: + if isinstance(child, RuleNode): + children.append(self._to_tree(child)) + else: + assert isinstance(child.name, Token) + children.append(child.name) + t = Tree(orig_rule.origin, children) + t.rule=orig_rule + return t + + +def print_parse(node, indent=0): + if isinstance(node, RuleNode): + print(' ' * (indent * 2) + str(node.rule.lhs)) + for child in node.children: + print_parse(child, indent + 1) + else: + print(' ' * (indent * 2) + str(node.s)) + + +def _parse(s, g): + """Parses sentence 's' using CNF grammar 'g'.""" + # The CYK table. Indexed with a 2-tuple: (start pos, end pos) + table = defaultdict(set) + # Top-level structure is similar to the CYK table. Each cell is a dict from + # rule name to the best (lightest) tree for that rule. + trees = defaultdict(dict) + # Populate base case with existing terminal production rules + for i, w in enumerate(s): + for terminal, rules in g.terminal_rules.items(): + if match(terminal, w): + for rule in rules: + table[(i, i)].add(rule) + if (rule.lhs not in trees[(i, i)] or + rule.weight < trees[(i, i)][rule.lhs].weight): + trees[(i, i)][rule.lhs] = RuleNode(rule, [T(w)], weight=rule.weight) + + # Iterate over lengths of sub-sentences + for l in range(2, len(s) + 1): + # Iterate over sub-sentences with the given length + for i in range(len(s) - l + 1): + # Choose partition of the sub-sentence in [1, l) + for p in range(i + 1, i + l): + span1 = (i, p - 1) + span2 = (p, i + l - 1) + for r1, r2 in itertools.product(table[span1], table[span2]): + for rule in g.nonterminal_rules.get((r1.lhs, r2.lhs), []): + table[(i, i + l - 1)].add(rule) + r1_tree = trees[span1][r1.lhs] + r2_tree = trees[span2][r2.lhs] + rule_total_weight = rule.weight + r1_tree.weight + r2_tree.weight + if (rule.lhs not in trees[(i, i + l - 1)] + or rule_total_weight < trees[(i, i + l - 1)][rule.lhs].weight): + trees[(i, i + l - 1)][rule.lhs] = RuleNode(rule, [r1_tree, r2_tree], weight=rule_total_weight) + return table, trees + + +# This section implements context-free grammar converter to Chomsky normal form. +# It also implements a conversion of parse trees from its CNF to the original +# grammar. +# Overview: +# Applies the following operations in this order: +# * TERM: Eliminates non-solitary terminals from all rules +# * BIN: Eliminates rules with more than 2 symbols on their right-hand-side. +# * UNIT: Eliminates non-terminal unit rules +# +# The following grammar characteristics aren't featured: +# * Start symbol appears on RHS +# * Empty rules (epsilon rules) + + +class CnfWrapper: + """CNF wrapper for grammar. + + Validates that the input grammar is CNF and provides helper data structures. + """ + + def __init__(self, grammar): + super(CnfWrapper, self).__init__() + self.grammar = grammar + self.rules = grammar.rules + self.terminal_rules = defaultdict(list) + self.nonterminal_rules = defaultdict(list) + for r in self.rules: + # Validate that the grammar is CNF and populate auxiliary data structures. + assert isinstance(r.lhs, NT), r + if len(r.rhs) not in [1, 2]: + raise ParseError("CYK doesn't support empty rules") + if len(r.rhs) == 1 and isinstance(r.rhs[0], T): + self.terminal_rules[r.rhs[0]].append(r) + elif len(r.rhs) == 2 and all(isinstance(x, NT) for x in r.rhs): + self.nonterminal_rules[tuple(r.rhs)].append(r) + else: + assert False, r + + def __eq__(self, other): + return self.grammar == other.grammar + + def __repr__(self): + return repr(self.grammar) + + +class UnitSkipRule(Rule): + """A rule that records NTs that were skipped during transformation.""" + + def __init__(self, lhs, rhs, skipped_rules, weight, alias): + super(UnitSkipRule, self).__init__(lhs, rhs, weight, alias) + self.skipped_rules = skipped_rules + + def __eq__(self, other): + return isinstance(other, type(self)) and self.skipped_rules == other.skipped_rules + + __hash__ = Rule.__hash__ + + +def build_unit_skiprule(unit_rule, target_rule): + skipped_rules = [] + if isinstance(unit_rule, UnitSkipRule): + skipped_rules += unit_rule.skipped_rules + skipped_rules.append(target_rule) + if isinstance(target_rule, UnitSkipRule): + skipped_rules += target_rule.skipped_rules + return UnitSkipRule(unit_rule.lhs, target_rule.rhs, skipped_rules, + weight=unit_rule.weight + target_rule.weight, alias=unit_rule.alias) + + +def get_any_nt_unit_rule(g): + """Returns a non-terminal unit rule from 'g', or None if there is none.""" + for rule in g.rules: + if len(rule.rhs) == 1 and isinstance(rule.rhs[0], NT): + return rule + return None + + +def _remove_unit_rule(g, rule): + """Removes 'rule' from 'g' without changing the language produced by 'g'.""" + new_rules = [x for x in g.rules if x != rule] + refs = [x for x in g.rules if x.lhs == rule.rhs[0]] + new_rules += [build_unit_skiprule(rule, ref) for ref in refs] + return Grammar(new_rules) + + +def _split(rule): + """Splits a rule whose len(rhs) > 2 into shorter rules.""" + rule_str = str(rule.lhs) + '__' + '_'.join(str(x) for x in rule.rhs) + rule_name = '__SP_%s' % (rule_str) + '_%d' + yield Rule(rule.lhs, [rule.rhs[0], NT(rule_name % 1)], weight=rule.weight, alias=rule.alias) + for i in range(1, len(rule.rhs) - 2): + yield Rule(NT(rule_name % i), [rule.rhs[i], NT(rule_name % (i + 1))], weight=0, alias='Split') + yield Rule(NT(rule_name % (len(rule.rhs) - 2)), rule.rhs[-2:], weight=0, alias='Split') + + +def _term(g): + """Applies the TERM rule on 'g' (see top comment).""" + all_t = {x for rule in g.rules for x in rule.rhs if isinstance(x, T)} + t_rules = {t: Rule(NT('__T_%s' % str(t)), [t], weight=0, alias='Term') for t in all_t} + new_rules = [] + for rule in g.rules: + if len(rule.rhs) > 1 and any(isinstance(x, T) for x in rule.rhs): + new_rhs = [t_rules[x].lhs if isinstance(x, T) else x for x in rule.rhs] + new_rules.append(Rule(rule.lhs, new_rhs, weight=rule.weight, alias=rule.alias)) + new_rules.extend(v for k, v in t_rules.items() if k in rule.rhs) + else: + new_rules.append(rule) + return Grammar(new_rules) + + +def _bin(g): + """Applies the BIN rule to 'g' (see top comment).""" + new_rules = [] + for rule in g.rules: + if len(rule.rhs) > 2: + new_rules += _split(rule) + else: + new_rules.append(rule) + return Grammar(new_rules) + + +def _unit(g): + """Applies the UNIT rule to 'g' (see top comment).""" + nt_unit_rule = get_any_nt_unit_rule(g) + while nt_unit_rule: + g = _remove_unit_rule(g, nt_unit_rule) + nt_unit_rule = get_any_nt_unit_rule(g) + return g + + +def to_cnf(g): + """Creates a CNF grammar from a general context-free grammar 'g'.""" + g = _unit(_bin(_term(g))) + return CnfWrapper(g) + + +def unroll_unit_skiprule(lhs, orig_rhs, skipped_rules, children, weight, alias): + if not skipped_rules: + return RuleNode(Rule(lhs, orig_rhs, weight=weight, alias=alias), children, weight=weight) + else: + weight = weight - skipped_rules[0].weight + return RuleNode( + Rule(lhs, [skipped_rules[0].lhs], weight=weight, alias=alias), [ + unroll_unit_skiprule(skipped_rules[0].lhs, orig_rhs, + skipped_rules[1:], children, + skipped_rules[0].weight, skipped_rules[0].alias) + ], weight=weight) + + +def revert_cnf(node): + """Reverts a parse tree (RuleNode) to its original non-CNF form (Node).""" + if isinstance(node, T): + return node + # Reverts TERM rule. + if node.rule.lhs.name.startswith('__T_'): + return node.children[0] + else: + children = [] + for child in map(revert_cnf, node.children): + # Reverts BIN rule. + if isinstance(child, RuleNode) and child.rule.lhs.name.startswith('__SP_'): + children += child.children + else: + children.append(child) + # Reverts UNIT rule. + if isinstance(node.rule, UnitSkipRule): + return unroll_unit_skiprule(node.rule.lhs, node.rule.rhs, + node.rule.skipped_rules, children, + node.rule.weight, node.rule.alias) + else: + return RuleNode(node.rule, children) diff --git a/ccxt/static_dependencies/lark/parsers/earley.py b/ccxt/static_dependencies/lark/parsers/earley.py new file mode 100644 index 0000000..2153a0c --- /dev/null +++ b/ccxt/static_dependencies/lark/parsers/earley.py @@ -0,0 +1,314 @@ +"""This module implements an Earley parser. + +The core Earley algorithm used here is based on Elizabeth Scott's implementation, here: + https://www.sciencedirect.com/science/article/pii/S1571066108001497 + +That is probably the best reference for understanding the algorithm here. + +The Earley parser outputs an SPPF-tree as per that document. The SPPF tree format +is explained here: https://lark-parser.readthedocs.io/en/latest/_static/sppf/sppf.html +""" + +from typing import TYPE_CHECKING, Callable, Optional, List, Any +from collections import deque + +from ..lexer import Token +from ..tree import Tree +from ..exceptions import UnexpectedEOF, UnexpectedToken +from ..utils import logger, OrderedSet, dedup_list +from .grammar_analysis import GrammarAnalyzer +from ..grammar import NonTerminal +from .earley_common import Item +from .earley_forest import ForestSumVisitor, SymbolNode, StableSymbolNode, TokenNode, ForestToParseTree + +if TYPE_CHECKING: + from ..common import LexerConf, ParserConf + +class Parser: + lexer_conf: 'LexerConf' + parser_conf: 'ParserConf' + debug: bool + + def __init__(self, lexer_conf: 'LexerConf', parser_conf: 'ParserConf', term_matcher: Callable, + resolve_ambiguity: bool=True, debug: bool=False, + tree_class: Optional[Callable[[str, List], Any]]=Tree, ordered_sets: bool=True): + analysis = GrammarAnalyzer(parser_conf) + self.lexer_conf = lexer_conf + self.parser_conf = parser_conf + self.resolve_ambiguity = resolve_ambiguity + self.debug = debug + self.Tree = tree_class + self.Set = OrderedSet if ordered_sets else set + self.SymbolNode = StableSymbolNode if ordered_sets else SymbolNode + + self.FIRST = analysis.FIRST + self.NULLABLE = analysis.NULLABLE + self.callbacks = parser_conf.callbacks + # TODO add typing info + self.predictions = {} # type: ignore[var-annotated] + + ## These could be moved to the grammar analyzer. Pre-computing these is *much* faster than + # the slow 'isupper' in is_terminal. + self.TERMINALS = { sym for r in parser_conf.rules for sym in r.expansion if sym.is_term } + self.NON_TERMINALS = { sym for r in parser_conf.rules for sym in r.expansion if not sym.is_term } + + self.forest_sum_visitor = None + for rule in parser_conf.rules: + if rule.origin not in self.predictions: + self.predictions[rule.origin] = [x.rule for x in analysis.expand_rule(rule.origin)] + + ## Detect if any rules/terminals have priorities set. If the user specified priority = None, then + # the priorities will be stripped from all rules/terminals before they reach us, allowing us to + # skip the extra tree walk. We'll also skip this if the user just didn't specify priorities + # on any rules/terminals. + if self.forest_sum_visitor is None and rule.options.priority is not None: + self.forest_sum_visitor = ForestSumVisitor + + # Check terminals for priorities + # Ignore terminal priorities if the basic lexer is used + if self.lexer_conf.lexer_type != 'basic' and self.forest_sum_visitor is None: + for term in self.lexer_conf.terminals: + if term.priority: + self.forest_sum_visitor = ForestSumVisitor + break + + self.term_matcher = term_matcher + + + def predict_and_complete(self, i, to_scan, columns, transitives): + """The core Earley Predictor and Completer. + + At each stage of the input, we handling any completed items (things + that matched on the last cycle) and use those to predict what should + come next in the input stream. The completions and any predicted + non-terminals are recursively processed until we reach a set of, + which can be added to the scan list for the next scanner cycle.""" + # Held Completions (H in E.Scotts paper). + node_cache = {} + held_completions = {} + + column = columns[i] + # R (items) = Ei (column.items) + items = deque(column) + while items: + item = items.pop() # remove an element, A say, from R + + ### The Earley completer + if item.is_complete: ### (item.s == string) + if item.node is None: + label = (item.s, item.start, i) + item.node = node_cache[label] if label in node_cache else node_cache.setdefault(label, self.SymbolNode(*label)) + item.node.add_family(item.s, item.rule, item.start, None, None) + + # create_leo_transitives(item.rule.origin, item.start) + + ###R Joop Leo right recursion Completer + if item.rule.origin in transitives[item.start]: + transitive = transitives[item.start][item.s] + if transitive.previous in transitives[transitive.column]: + root_transitive = transitives[transitive.column][transitive.previous] + else: + root_transitive = transitive + + new_item = Item(transitive.rule, transitive.ptr, transitive.start) + label = (root_transitive.s, root_transitive.start, i) + new_item.node = node_cache[label] if label in node_cache else node_cache.setdefault(label, self.SymbolNode(*label)) + new_item.node.add_path(root_transitive, item.node) + if new_item.expect in self.TERMINALS: + # Add (B :: aC.B, h, y) to Q + to_scan.add(new_item) + elif new_item not in column: + # Add (B :: aC.B, h, y) to Ei and R + column.add(new_item) + items.append(new_item) + ###R Regular Earley completer + else: + # Empty has 0 length. If we complete an empty symbol in a particular + # parse step, we need to be able to use that same empty symbol to complete + # any predictions that result, that themselves require empty. Avoids + # infinite recursion on empty symbols. + # held_completions is 'H' in E.Scott's paper. + is_empty_item = item.start == i + if is_empty_item: + held_completions[item.rule.origin] = item.node + + originators = [originator for originator in columns[item.start] if originator.expect is not None and originator.expect == item.s] + for originator in originators: + new_item = originator.advance() + label = (new_item.s, originator.start, i) + new_item.node = node_cache[label] if label in node_cache else node_cache.setdefault(label, self.SymbolNode(*label)) + new_item.node.add_family(new_item.s, new_item.rule, i, originator.node, item.node) + if new_item.expect in self.TERMINALS: + # Add (B :: aC.B, h, y) to Q + to_scan.add(new_item) + elif new_item not in column: + # Add (B :: aC.B, h, y) to Ei and R + column.add(new_item) + items.append(new_item) + + ### The Earley predictor + elif item.expect in self.NON_TERMINALS: ### (item.s == lr0) + new_items = [] + for rule in self.predictions[item.expect]: + new_item = Item(rule, 0, i) + new_items.append(new_item) + + # Process any held completions (H). + if item.expect in held_completions: + new_item = item.advance() + label = (new_item.s, item.start, i) + new_item.node = node_cache[label] if label in node_cache else node_cache.setdefault(label, self.SymbolNode(*label)) + new_item.node.add_family(new_item.s, new_item.rule, new_item.start, item.node, held_completions[item.expect]) + new_items.append(new_item) + + for new_item in new_items: + if new_item.expect in self.TERMINALS: + to_scan.add(new_item) + elif new_item not in column: + column.add(new_item) + items.append(new_item) + + def _parse(self, lexer, columns, to_scan, start_symbol=None): + + def is_quasi_complete(item): + if item.is_complete: + return True + + quasi = item.advance() + while not quasi.is_complete: + if quasi.expect not in self.NULLABLE: + return False + if quasi.rule.origin == start_symbol and quasi.expect == start_symbol: + return False + quasi = quasi.advance() + return True + + # def create_leo_transitives(origin, start): + # ... # removed at commit 4c1cfb2faf24e8f8bff7112627a00b94d261b420 + + def scan(i, token, to_scan): + """The core Earley Scanner. + + This is a custom implementation of the scanner that uses the + Lark lexer to match tokens. The scan list is built by the + Earley predictor, based on the previously completed tokens. + This ensures that at each phase of the parse we have a custom + lexer context, allowing for more complex ambiguities.""" + next_to_scan = self.Set() + next_set = self.Set() + columns.append(next_set) + transitives.append({}) + node_cache = {} + + for item in self.Set(to_scan): + if match(item.expect, token): + new_item = item.advance() + label = (new_item.s, new_item.start, i) + # 'terminals' may not contain token.type when using %declare + # Additionally, token is not always a Token + # For example, it can be a Tree when using TreeMatcher + term = terminals.get(token.type) if isinstance(token, Token) else None + # Set the priority of the token node to 0 so that the + # terminal priorities do not affect the Tree chosen by + # ForestSumVisitor after the basic lexer has already + # "used up" the terminal priorities + token_node = TokenNode(token, term, priority=0) + new_item.node = node_cache[label] if label in node_cache else node_cache.setdefault(label, self.SymbolNode(*label)) + new_item.node.add_family(new_item.s, item.rule, new_item.start, item.node, token_node) + + if new_item.expect in self.TERMINALS: + # add (B ::= Aai+1.B, h, y) to Q' + next_to_scan.add(new_item) + else: + # add (B ::= Aa+1.B, h, y) to Ei+1 + next_set.add(new_item) + + if not next_set and not next_to_scan: + expect = {i.expect.name for i in to_scan} + raise UnexpectedToken(token, expect, considered_rules=set(to_scan), state=frozenset(i.s for i in to_scan)) + + return next_to_scan + + + # Define parser functions + match = self.term_matcher + + terminals = self.lexer_conf.terminals_by_name + + # Cache for nodes & tokens created in a particular parse step. + transitives = [{}] + + ## The main Earley loop. + # Run the Prediction/Completion cycle for any Items in the current Earley set. + # Completions will be added to the SPPF tree, and predictions will be recursively + # processed down to terminals/empty nodes to be added to the scanner for the next + # step. + expects = {i.expect for i in to_scan} + i = 0 + for token in lexer.lex(expects): + self.predict_and_complete(i, to_scan, columns, transitives) + + to_scan = scan(i, token, to_scan) + i += 1 + + expects.clear() + expects |= {i.expect for i in to_scan} + + self.predict_and_complete(i, to_scan, columns, transitives) + + ## Column is now the final column in the parse. + assert i == len(columns)-1 + return to_scan + + def parse(self, lexer, start): + assert start, start + start_symbol = NonTerminal(start) + + columns = [self.Set()] + to_scan = self.Set() # The scan buffer. 'Q' in E.Scott's paper. + + ## Predict for the start_symbol. + # Add predicted items to the first Earley set (for the predictor) if they + # result in a non-terminal, or the scanner if they result in a terminal. + for rule in self.predictions[start_symbol]: + item = Item(rule, 0, 0) + if item.expect in self.TERMINALS: + to_scan.add(item) + else: + columns[0].add(item) + + to_scan = self._parse(lexer, columns, to_scan, start_symbol) + + # If the parse was successful, the start + # symbol should have been completed in the last step of the Earley cycle, and will be in + # this column. Find the item for the start_symbol, which is the root of the SPPF tree. + solutions = dedup_list(n.node for n in columns[-1] if n.is_complete and n.node is not None and n.s == start_symbol and n.start == 0) + if not solutions: + expected_terminals = [t.expect.name for t in to_scan] + raise UnexpectedEOF(expected_terminals, state=frozenset(i.s for i in to_scan)) + + if self.debug: + from .earley_forest import ForestToPyDotVisitor + try: + debug_walker = ForestToPyDotVisitor() + except ImportError: + logger.warning("Cannot find dependency 'pydot', will not generate sppf debug image") + else: + for i, s in enumerate(solutions): + debug_walker.visit(s, f"sppf{i}.png") + + + if self.Tree is not None: + # Perform our SPPF -> AST conversion + transformer = ForestToParseTree(self.Tree, self.callbacks, self.forest_sum_visitor and self.forest_sum_visitor(), self.resolve_ambiguity) + solutions = [transformer.transform(s) for s in solutions] + + if len(solutions) > 1: + t: Tree = self.Tree('_ambig', solutions) + t.expand_kids_by_data('_ambig') # solutions may themselves be _ambig nodes + return t + return solutions[0] + + # return the root of the SPPF + # TODO return a list of solutions, or join them together somehow + return solutions[0] diff --git a/ccxt/static_dependencies/lark/parsers/earley_common.py b/ccxt/static_dependencies/lark/parsers/earley_common.py new file mode 100644 index 0000000..0ea2d4f --- /dev/null +++ b/ccxt/static_dependencies/lark/parsers/earley_common.py @@ -0,0 +1,42 @@ +"""This module implements useful building blocks for the Earley parser +""" + + +class Item: + "An Earley Item, the atom of the algorithm." + + __slots__ = ('s', 'rule', 'ptr', 'start', 'is_complete', 'expect', 'previous', 'node', '_hash') + def __init__(self, rule, ptr, start): + self.is_complete = len(rule.expansion) == ptr + self.rule = rule # rule + self.ptr = ptr # ptr + self.start = start # j + self.node = None # w + if self.is_complete: + self.s = rule.origin + self.expect = None + self.previous = rule.expansion[ptr - 1] if ptr > 0 and len(rule.expansion) else None + else: + self.s = (rule, ptr) + self.expect = rule.expansion[ptr] + self.previous = rule.expansion[ptr - 1] if ptr > 0 and len(rule.expansion) else None + self._hash = hash((self.s, self.start, self.rule)) + + def advance(self): + return Item(self.rule, self.ptr + 1, self.start) + + def __eq__(self, other): + return self is other or (self.s == other.s and self.start == other.start and self.rule == other.rule) + + def __hash__(self): + return self._hash + + def __repr__(self): + before = ( expansion.name for expansion in self.rule.expansion[:self.ptr] ) + after = ( expansion.name for expansion in self.rule.expansion[self.ptr:] ) + symbol = "{} ::= {}* {}".format(self.rule.origin.name, ' '.join(before), ' '.join(after)) + return '%s (%d)' % (symbol, self.start) + + +# class TransitiveItem(Item): +# ... # removed at commit 4c1cfb2faf24e8f8bff7112627a00b94d261b420 diff --git a/ccxt/static_dependencies/lark/parsers/earley_forest.py b/ccxt/static_dependencies/lark/parsers/earley_forest.py new file mode 100644 index 0000000..cdc613a --- /dev/null +++ b/ccxt/static_dependencies/lark/parsers/earley_forest.py @@ -0,0 +1,801 @@ +""""This module implements an SPPF implementation + +This is used as the primary output mechanism for the Earley parser +in order to store complex ambiguities. + +Full reference and more details is here: +https://web.archive.org/web/20190616123959/http://www.bramvandersanden.com/post/2014/06/shared-packed-parse-forest/ +""" + +from typing import Type, AbstractSet +from random import randint +from collections import deque +from operator import attrgetter +from importlib import import_module +from functools import partial + +from ..parse_tree_builder import AmbiguousIntermediateExpander +from ..visitors import Discard +from ..utils import logger, OrderedSet +from ..tree import Tree + +class ForestNode: + pass + +class SymbolNode(ForestNode): + """ + A Symbol Node represents a symbol (or Intermediate LR0). + + Symbol nodes are keyed by the symbol (s). For intermediate nodes + s will be an LR0, stored as a tuple of (rule, ptr). For completed symbol + nodes, s will be a string representing the non-terminal origin (i.e. + the left hand side of the rule). + + The children of a Symbol or Intermediate Node will always be Packed Nodes; + with each Packed Node child representing a single derivation of a production. + + Hence a Symbol Node with a single child is unambiguous. + + Parameters: + s: A Symbol, or a tuple of (rule, ptr) for an intermediate node. + start: For dynamic lexers, the index of the start of the substring matched by this symbol (inclusive). + end: For dynamic lexers, the index of the end of the substring matched by this symbol (exclusive). + + Properties: + is_intermediate: True if this node is an intermediate node. + priority: The priority of the node's symbol. + """ + Set: Type[AbstractSet] = set # Overridden by StableSymbolNode + __slots__ = ('s', 'start', 'end', '_children', 'paths', 'paths_loaded', 'priority', 'is_intermediate') + def __init__(self, s, start, end): + self.s = s + self.start = start + self.end = end + self._children = self.Set() + self.paths = self.Set() + self.paths_loaded = False + + ### We use inf here as it can be safely negated without resorting to conditionals, + # unlike None or float('NaN'), and sorts appropriately. + self.priority = float('-inf') + self.is_intermediate = isinstance(s, tuple) + + def add_family(self, lr0, rule, start, left, right): + self._children.add(PackedNode(self, lr0, rule, start, left, right)) + + def add_path(self, transitive, node): + self.paths.add((transitive, node)) + + def load_paths(self): + for transitive, node in self.paths: + if transitive.next_titem is not None: + vn = type(self)(transitive.next_titem.s, transitive.next_titem.start, self.end) + vn.add_path(transitive.next_titem, node) + self.add_family(transitive.reduction.rule.origin, transitive.reduction.rule, transitive.reduction.start, transitive.reduction.node, vn) + else: + self.add_family(transitive.reduction.rule.origin, transitive.reduction.rule, transitive.reduction.start, transitive.reduction.node, node) + self.paths_loaded = True + + @property + def is_ambiguous(self): + """Returns True if this node is ambiguous.""" + return len(self.children) > 1 + + @property + def children(self): + """Returns a list of this node's children sorted from greatest to + least priority.""" + if not self.paths_loaded: + self.load_paths() + return sorted(self._children, key=attrgetter('sort_key')) + + def __iter__(self): + return iter(self._children) + + def __repr__(self): + if self.is_intermediate: + rule = self.s[0] + ptr = self.s[1] + before = ( expansion.name for expansion in rule.expansion[:ptr] ) + after = ( expansion.name for expansion in rule.expansion[ptr:] ) + symbol = "{} ::= {}* {}".format(rule.origin.name, ' '.join(before), ' '.join(after)) + else: + symbol = self.s.name + return "({}, {}, {}, {})".format(symbol, self.start, self.end, self.priority) + +class StableSymbolNode(SymbolNode): + "A version of SymbolNode that uses OrderedSet for output stability" + Set = OrderedSet + +class PackedNode(ForestNode): + """ + A Packed Node represents a single derivation in a symbol node. + + Parameters: + rule: The rule associated with this node. + parent: The parent of this node. + left: The left child of this node. ``None`` if one does not exist. + right: The right child of this node. ``None`` if one does not exist. + priority: The priority of this node. + """ + __slots__ = ('parent', 's', 'rule', 'start', 'left', 'right', 'priority', '_hash') + def __init__(self, parent, s, rule, start, left, right): + self.parent = parent + self.s = s + self.start = start + self.rule = rule + self.left = left + self.right = right + self.priority = float('-inf') + self._hash = hash((self.left, self.right)) + + @property + def is_empty(self): + return self.left is None and self.right is None + + @property + def sort_key(self): + """ + Used to sort PackedNode children of SymbolNodes. + A SymbolNode has multiple PackedNodes if it matched + ambiguously. Hence, we use the sort order to identify + the order in which ambiguous children should be considered. + """ + return self.is_empty, -self.priority, self.rule.order + + @property + def children(self): + """Returns a list of this node's children.""" + return [x for x in [self.left, self.right] if x is not None] + + def __iter__(self): + yield self.left + yield self.right + + def __eq__(self, other): + if not isinstance(other, PackedNode): + return False + return self is other or (self.left == other.left and self.right == other.right) + + def __hash__(self): + return self._hash + + def __repr__(self): + if isinstance(self.s, tuple): + rule = self.s[0] + ptr = self.s[1] + before = ( expansion.name for expansion in rule.expansion[:ptr] ) + after = ( expansion.name for expansion in rule.expansion[ptr:] ) + symbol = "{} ::= {}* {}".format(rule.origin.name, ' '.join(before), ' '.join(after)) + else: + symbol = self.s.name + return "({}, {}, {}, {})".format(symbol, self.start, self.priority, self.rule.order) + +class TokenNode(ForestNode): + """ + A Token Node represents a matched terminal and is always a leaf node. + + Parameters: + token: The Token associated with this node. + term: The TerminalDef matched by the token. + priority: The priority of this node. + """ + __slots__ = ('token', 'term', 'priority', '_hash') + def __init__(self, token, term, priority=None): + self.token = token + self.term = term + if priority is not None: + self.priority = priority + else: + self.priority = term.priority if term is not None else 0 + self._hash = hash(token) + + def __eq__(self, other): + if not isinstance(other, TokenNode): + return False + return self is other or (self.token == other.token) + + def __hash__(self): + return self._hash + + def __repr__(self): + return repr(self.token) + +class ForestVisitor: + """ + An abstract base class for building forest visitors. + + This class performs a controllable depth-first walk of an SPPF. + The visitor will not enter cycles and will backtrack if one is encountered. + Subclasses are notified of cycles through the ``on_cycle`` method. + + Behavior for visit events is defined by overriding the + ``visit*node*`` functions. + + The walk is controlled by the return values of the ``visit*node_in`` + methods. Returning a node(s) will schedule them to be visited. The visitor + will begin to backtrack if no nodes are returned. + + Parameters: + single_visit: If ``True``, non-Token nodes will only be visited once. + """ + + def __init__(self, single_visit=False): + self.single_visit = single_visit + + def visit_token_node(self, node): + """Called when a ``Token`` is visited. ``Token`` nodes are always leaves.""" + pass + + def visit_symbol_node_in(self, node): + """Called when a symbol node is visited. Nodes that are returned + will be scheduled to be visited. If ``visit_intermediate_node_in`` + is not implemented, this function will be called for intermediate + nodes as well.""" + pass + + def visit_symbol_node_out(self, node): + """Called after all nodes returned from a corresponding ``visit_symbol_node_in`` + call have been visited. If ``visit_intermediate_node_out`` + is not implemented, this function will be called for intermediate + nodes as well.""" + pass + + def visit_packed_node_in(self, node): + """Called when a packed node is visited. Nodes that are returned + will be scheduled to be visited. """ + pass + + def visit_packed_node_out(self, node): + """Called after all nodes returned from a corresponding ``visit_packed_node_in`` + call have been visited.""" + pass + + def on_cycle(self, node, path): + """Called when a cycle is encountered. + + Parameters: + node: The node that causes a cycle. + path: The list of nodes being visited: nodes that have been + entered but not exited. The first element is the root in a forest + visit, and the last element is the node visited most recently. + ``path`` should be treated as read-only. + """ + pass + + def get_cycle_in_path(self, node, path): + """A utility function for use in ``on_cycle`` to obtain a slice of + ``path`` that only contains the nodes that make up the cycle.""" + index = len(path) - 1 + while id(path[index]) != id(node): + index -= 1 + return path[index:] + + def visit(self, root): + # Visiting is a list of IDs of all symbol/intermediate nodes currently in + # the stack. It serves two purposes: to detect when we 'recurse' in and out + # of a symbol/intermediate so that we can process both up and down. Also, + # since the SPPF can have cycles it allows us to detect if we're trying + # to recurse into a node that's already on the stack (infinite recursion). + visiting = set() + + # set of all nodes that have been visited + visited = set() + + # a list of nodes that are currently being visited + # used for the `on_cycle` callback + path = [] + + # We do not use recursion here to walk the Forest due to the limited + # stack size in python. Therefore input_stack is essentially our stack. + input_stack = deque([root]) + + # It is much faster to cache these as locals since they are called + # many times in large parses. + vpno = getattr(self, 'visit_packed_node_out') + vpni = getattr(self, 'visit_packed_node_in') + vsno = getattr(self, 'visit_symbol_node_out') + vsni = getattr(self, 'visit_symbol_node_in') + vino = getattr(self, 'visit_intermediate_node_out', vsno) + vini = getattr(self, 'visit_intermediate_node_in', vsni) + vtn = getattr(self, 'visit_token_node') + oc = getattr(self, 'on_cycle') + + while input_stack: + current = next(reversed(input_stack)) + try: + next_node = next(current) + except StopIteration: + input_stack.pop() + continue + except TypeError: + ### If the current object is not an iterator, pass through to Token/SymbolNode + pass + else: + if next_node is None: + continue + + if id(next_node) in visiting: + oc(next_node, path) + continue + + input_stack.append(next_node) + continue + + if isinstance(current, TokenNode): + vtn(current.token) + input_stack.pop() + continue + + current_id = id(current) + if current_id in visiting: + if isinstance(current, PackedNode): + vpno(current) + elif current.is_intermediate: + vino(current) + else: + vsno(current) + input_stack.pop() + path.pop() + visiting.remove(current_id) + visited.add(current_id) + elif self.single_visit and current_id in visited: + input_stack.pop() + else: + visiting.add(current_id) + path.append(current) + if isinstance(current, PackedNode): + next_node = vpni(current) + elif current.is_intermediate: + next_node = vini(current) + else: + next_node = vsni(current) + if next_node is None: + continue + + if not isinstance(next_node, ForestNode): + next_node = iter(next_node) + elif id(next_node) in visiting: + oc(next_node, path) + continue + + input_stack.append(next_node) + +class ForestTransformer(ForestVisitor): + """The base class for a bottom-up forest transformation. Most users will + want to use ``TreeForestTransformer`` instead as it has a friendlier + interface and covers most use cases. + + Transformations are applied via inheritance and overriding of the + ``transform*node`` methods. + + ``transform_token_node`` receives a ``Token`` as an argument. + All other methods receive the node that is being transformed and + a list of the results of the transformations of that node's children. + The return value of these methods are the resulting transformations. + + If ``Discard`` is raised in a node's transformation, no data from that node + will be passed to its parent's transformation. + """ + + def __init__(self): + super(ForestTransformer, self).__init__() + # results of transformations + self.data = dict() + # used to track parent nodes + self.node_stack = deque() + + def transform(self, root): + """Perform a transformation on an SPPF.""" + self.node_stack.append('result') + self.data['result'] = [] + self.visit(root) + assert len(self.data['result']) <= 1 + if self.data['result']: + return self.data['result'][0] + + def transform_symbol_node(self, node, data): + """Transform a symbol node.""" + return node + + def transform_intermediate_node(self, node, data): + """Transform an intermediate node.""" + return node + + def transform_packed_node(self, node, data): + """Transform a packed node.""" + return node + + def transform_token_node(self, node): + """Transform a ``Token``.""" + return node + + def visit_symbol_node_in(self, node): + self.node_stack.append(id(node)) + self.data[id(node)] = [] + return node.children + + def visit_packed_node_in(self, node): + self.node_stack.append(id(node)) + self.data[id(node)] = [] + return node.children + + def visit_token_node(self, node): + transformed = self.transform_token_node(node) + if transformed is not Discard: + self.data[self.node_stack[-1]].append(transformed) + + def _visit_node_out_helper(self, node, method): + self.node_stack.pop() + transformed = method(node, self.data[id(node)]) + if transformed is not Discard: + self.data[self.node_stack[-1]].append(transformed) + del self.data[id(node)] + + def visit_symbol_node_out(self, node): + self._visit_node_out_helper(node, self.transform_symbol_node) + + def visit_intermediate_node_out(self, node): + self._visit_node_out_helper(node, self.transform_intermediate_node) + + def visit_packed_node_out(self, node): + self._visit_node_out_helper(node, self.transform_packed_node) + + +class ForestSumVisitor(ForestVisitor): + """ + A visitor for prioritizing ambiguous parts of the Forest. + + This visitor is used when support for explicit priorities on + rules is requested (whether normal, or invert). It walks the + forest (or subsets thereof) and cascades properties upwards + from the leaves. + + It would be ideal to do this during parsing, however this would + require processing each Earley item multiple times. That's + a big performance drawback; so running a forest walk is the + lesser of two evils: there can be significantly more Earley + items created during parsing than there are SPPF nodes in the + final tree. + """ + def __init__(self): + super(ForestSumVisitor, self).__init__(single_visit=True) + + def visit_packed_node_in(self, node): + yield node.left + yield node.right + + def visit_symbol_node_in(self, node): + return iter(node.children) + + def visit_packed_node_out(self, node): + priority = node.rule.options.priority if not node.parent.is_intermediate and node.rule.options.priority else 0 + priority += getattr(node.right, 'priority', 0) + priority += getattr(node.left, 'priority', 0) + node.priority = priority + + def visit_symbol_node_out(self, node): + node.priority = max(child.priority for child in node.children) + +class PackedData(): + """Used in transformationss of packed nodes to distinguish the data + that comes from the left child and the right child. + """ + + class _NoData(): + pass + + NO_DATA = _NoData() + + def __init__(self, node, data): + self.left = self.NO_DATA + self.right = self.NO_DATA + if data: + if node.left is not None: + self.left = data[0] + if len(data) > 1: + self.right = data[1] + else: + self.right = data[0] + +class ForestToParseTree(ForestTransformer): + """Used by the earley parser when ambiguity equals 'resolve' or + 'explicit'. Transforms an SPPF into an (ambiguous) parse tree. + + Parameters: + tree_class: The tree class to use for construction + callbacks: A dictionary of rules to functions that output a tree + prioritizer: A ``ForestVisitor`` that manipulates the priorities of ForestNodes + resolve_ambiguity: If True, ambiguities will be resolved based on + priorities. Otherwise, `_ambig` nodes will be in the resulting tree. + use_cache: If True, the results of packed node transformations will be cached. + """ + + def __init__(self, tree_class=Tree, callbacks=dict(), prioritizer=ForestSumVisitor(), resolve_ambiguity=True, use_cache=True): + super(ForestToParseTree, self).__init__() + self.tree_class = tree_class + self.callbacks = callbacks + self.prioritizer = prioritizer + self.resolve_ambiguity = resolve_ambiguity + self._use_cache = use_cache + self._cache = {} + self._on_cycle_retreat = False + self._cycle_node = None + self._successful_visits = set() + + def visit(self, root): + if self.prioritizer: + self.prioritizer.visit(root) + super(ForestToParseTree, self).visit(root) + self._cache = {} + + def on_cycle(self, node, path): + logger.debug("Cycle encountered in the SPPF at node: %s. " + "As infinite ambiguities cannot be represented in a tree, " + "this family of derivations will be discarded.", node) + self._cycle_node = node + self._on_cycle_retreat = True + + def _check_cycle(self, node): + if self._on_cycle_retreat: + if id(node) == id(self._cycle_node) or id(node) in self._successful_visits: + self._cycle_node = None + self._on_cycle_retreat = False + else: + return Discard + + def _collapse_ambig(self, children): + new_children = [] + for child in children: + if hasattr(child, 'data') and child.data == '_ambig': + new_children += child.children + else: + new_children.append(child) + return new_children + + def _call_rule_func(self, node, data): + # called when transforming children of symbol nodes + # data is a list of trees or tokens that correspond to the + # symbol's rule expansion + return self.callbacks[node.rule](data) + + def _call_ambig_func(self, node, data): + # called when transforming a symbol node + # data is a list of trees where each tree's data is + # equal to the name of the symbol or one of its aliases. + if len(data) > 1: + return self.tree_class('_ambig', data) + elif data: + return data[0] + return Discard + + def transform_symbol_node(self, node, data): + if id(node) not in self._successful_visits: + return Discard + r = self._check_cycle(node) + if r is Discard: + return r + self._successful_visits.remove(id(node)) + data = self._collapse_ambig(data) + return self._call_ambig_func(node, data) + + def transform_intermediate_node(self, node, data): + if id(node) not in self._successful_visits: + return Discard + r = self._check_cycle(node) + if r is Discard: + return r + self._successful_visits.remove(id(node)) + if len(data) > 1: + children = [self.tree_class('_inter', c) for c in data] + return self.tree_class('_iambig', children) + return data[0] + + def transform_packed_node(self, node, data): + r = self._check_cycle(node) + if r is Discard: + return r + if self.resolve_ambiguity and id(node.parent) in self._successful_visits: + return Discard + if self._use_cache and id(node) in self._cache: + return self._cache[id(node)] + children = [] + assert len(data) <= 2 + data = PackedData(node, data) + if data.left is not PackedData.NO_DATA: + if node.left.is_intermediate and isinstance(data.left, list): + children += data.left + else: + children.append(data.left) + if data.right is not PackedData.NO_DATA: + children.append(data.right) + if node.parent.is_intermediate: + return self._cache.setdefault(id(node), children) + return self._cache.setdefault(id(node), self._call_rule_func(node, children)) + + def visit_symbol_node_in(self, node): + super(ForestToParseTree, self).visit_symbol_node_in(node) + if self._on_cycle_retreat: + return + return node.children + + def visit_packed_node_in(self, node): + self._on_cycle_retreat = False + to_visit = super(ForestToParseTree, self).visit_packed_node_in(node) + if not self.resolve_ambiguity or id(node.parent) not in self._successful_visits: + if not self._use_cache or id(node) not in self._cache: + return to_visit + + def visit_packed_node_out(self, node): + super(ForestToParseTree, self).visit_packed_node_out(node) + if not self._on_cycle_retreat: + self._successful_visits.add(id(node.parent)) + +def handles_ambiguity(func): + """Decorator for methods of subclasses of ``TreeForestTransformer``. + Denotes that the method should receive a list of transformed derivations.""" + func.handles_ambiguity = True + return func + +class TreeForestTransformer(ForestToParseTree): + """A ``ForestTransformer`` with a tree ``Transformer``-like interface. + By default, it will construct a tree. + + Methods provided via inheritance are called based on the rule/symbol + names of nodes in the forest. + + Methods that act on rules will receive a list of the results of the + transformations of the rule's children. By default, trees and tokens. + + Methods that act on tokens will receive a token. + + Alternatively, methods that act on rules may be annotated with + ``handles_ambiguity``. In this case, the function will receive a list + of all the transformations of all the derivations of the rule. + By default, a list of trees where each tree.data is equal to the + rule name or one of its aliases. + + Non-tree transformations are made possible by override of + ``__default__``, ``__default_token__``, and ``__default_ambig__``. + + Note: + Tree shaping features such as inlined rules and token filtering are + not built into the transformation. Positions are also not propagated. + + Parameters: + tree_class: The tree class to use for construction + prioritizer: A ``ForestVisitor`` that manipulates the priorities of nodes in the SPPF. + resolve_ambiguity: If True, ambiguities will be resolved based on priorities. + use_cache (bool): If True, caches the results of some transformations, + potentially improving performance when ``resolve_ambiguity==False``. + Only use if you know what you are doing: i.e. All transformation + functions are pure and referentially transparent. + """ + + def __init__(self, tree_class=Tree, prioritizer=ForestSumVisitor(), resolve_ambiguity=True, use_cache=False): + super(TreeForestTransformer, self).__init__(tree_class, dict(), prioritizer, resolve_ambiguity, use_cache) + + def __default__(self, name, data): + """Default operation on tree (for override). + + Returns a tree with name with data as children. + """ + return self.tree_class(name, data) + + def __default_ambig__(self, name, data): + """Default operation on ambiguous rule (for override). + + Wraps data in an '_ambig_' node if it contains more than + one element. + """ + if len(data) > 1: + return self.tree_class('_ambig', data) + elif data: + return data[0] + return Discard + + def __default_token__(self, node): + """Default operation on ``Token`` (for override). + + Returns ``node``. + """ + return node + + def transform_token_node(self, node): + return getattr(self, node.type, self.__default_token__)(node) + + def _call_rule_func(self, node, data): + name = node.rule.alias or node.rule.options.template_source or node.rule.origin.name + user_func = getattr(self, name, self.__default__) + if user_func == self.__default__ or hasattr(user_func, 'handles_ambiguity'): + user_func = partial(self.__default__, name) + if not self.resolve_ambiguity: + wrapper = partial(AmbiguousIntermediateExpander, self.tree_class) + user_func = wrapper(user_func) + return user_func(data) + + def _call_ambig_func(self, node, data): + name = node.s.name + user_func = getattr(self, name, self.__default_ambig__) + if user_func == self.__default_ambig__ or not hasattr(user_func, 'handles_ambiguity'): + user_func = partial(self.__default_ambig__, name) + return user_func(data) + +class ForestToPyDotVisitor(ForestVisitor): + """ + A Forest visitor which writes the SPPF to a PNG. + + The SPPF can get really large, really quickly because + of the amount of meta-data it stores, so this is probably + only useful for trivial trees and learning how the SPPF + is structured. + """ + def __init__(self, rankdir="TB"): + super(ForestToPyDotVisitor, self).__init__(single_visit=True) + self.pydot = import_module('pydot') + self.graph = self.pydot.Dot(graph_type='digraph', rankdir=rankdir) + + def visit(self, root, filename): + super(ForestToPyDotVisitor, self).visit(root) + try: + self.graph.write_png(filename) + except FileNotFoundError as e: + logger.error("Could not write png: ", e) + + def visit_token_node(self, node): + graph_node_id = str(id(node)) + graph_node_label = "\"{}\"".format(node.value.replace('"', '\\"')) + graph_node_color = 0x808080 + graph_node_style = "\"filled,rounded\"" + graph_node_shape = "diamond" + graph_node = self.pydot.Node(graph_node_id, style=graph_node_style, fillcolor="#{:06x}".format(graph_node_color), shape=graph_node_shape, label=graph_node_label) + self.graph.add_node(graph_node) + + def visit_packed_node_in(self, node): + graph_node_id = str(id(node)) + graph_node_label = repr(node) + graph_node_color = 0x808080 + graph_node_style = "filled" + graph_node_shape = "diamond" + graph_node = self.pydot.Node(graph_node_id, style=graph_node_style, fillcolor="#{:06x}".format(graph_node_color), shape=graph_node_shape, label=graph_node_label) + self.graph.add_node(graph_node) + yield node.left + yield node.right + + def visit_packed_node_out(self, node): + graph_node_id = str(id(node)) + graph_node = self.graph.get_node(graph_node_id)[0] + for child in [node.left, node.right]: + if child is not None: + child_graph_node_id = str(id(child.token if isinstance(child, TokenNode) else child)) + child_graph_node = self.graph.get_node(child_graph_node_id)[0] + self.graph.add_edge(self.pydot.Edge(graph_node, child_graph_node)) + else: + #### Try and be above the Python object ID range; probably impl. specific, but maybe this is okay. + child_graph_node_id = str(randint(100000000000000000000000000000,123456789012345678901234567890)) + child_graph_node_style = "invis" + child_graph_node = self.pydot.Node(child_graph_node_id, style=child_graph_node_style, label="None") + child_edge_style = "invis" + self.graph.add_node(child_graph_node) + self.graph.add_edge(self.pydot.Edge(graph_node, child_graph_node, style=child_edge_style)) + + def visit_symbol_node_in(self, node): + graph_node_id = str(id(node)) + graph_node_label = repr(node) + graph_node_color = 0x808080 + graph_node_style = "\"filled\"" + if node.is_intermediate: + graph_node_shape = "ellipse" + else: + graph_node_shape = "rectangle" + graph_node = self.pydot.Node(graph_node_id, style=graph_node_style, fillcolor="#{:06x}".format(graph_node_color), shape=graph_node_shape, label=graph_node_label) + self.graph.add_node(graph_node) + return iter(node.children) + + def visit_symbol_node_out(self, node): + graph_node_id = str(id(node)) + graph_node = self.graph.get_node(graph_node_id)[0] + for child in node.children: + child_graph_node_id = str(id(child)) + child_graph_node = self.graph.get_node(child_graph_node_id)[0] + self.graph.add_edge(self.pydot.Edge(graph_node, child_graph_node)) diff --git a/ccxt/static_dependencies/lark/parsers/grammar_analysis.py b/ccxt/static_dependencies/lark/parsers/grammar_analysis.py new file mode 100644 index 0000000..b52e50d --- /dev/null +++ b/ccxt/static_dependencies/lark/parsers/grammar_analysis.py @@ -0,0 +1,203 @@ +"Provides for superficial grammar analysis." + +from collections import Counter, defaultdict +from typing import List, Dict, Iterator, FrozenSet, Set + +from ..utils import bfs, fzset, classify +from ..exceptions import GrammarError +from ..grammar import Rule, Terminal, NonTerminal, Symbol +from ..common import ParserConf + + +class RulePtr: + __slots__ = ('rule', 'index') + rule: Rule + index: int + + def __init__(self, rule: Rule, index: int): + assert isinstance(rule, Rule) + assert index <= len(rule.expansion) + self.rule = rule + self.index = index + + def __repr__(self): + before = [x.name for x in self.rule.expansion[:self.index]] + after = [x.name for x in self.rule.expansion[self.index:]] + return '<%s : %s * %s>' % (self.rule.origin.name, ' '.join(before), ' '.join(after)) + + @property + def next(self) -> Symbol: + return self.rule.expansion[self.index] + + def advance(self, sym: Symbol) -> 'RulePtr': + assert self.next == sym + return RulePtr(self.rule, self.index+1) + + @property + def is_satisfied(self) -> bool: + return self.index == len(self.rule.expansion) + + def __eq__(self, other) -> bool: + if not isinstance(other, RulePtr): + return NotImplemented + return self.rule == other.rule and self.index == other.index + + def __hash__(self) -> int: + return hash((self.rule, self.index)) + + +State = FrozenSet[RulePtr] + +# state generation ensures no duplicate LR0ItemSets +class LR0ItemSet: + __slots__ = ('kernel', 'closure', 'transitions', 'lookaheads') + + kernel: State + closure: State + transitions: Dict[Symbol, 'LR0ItemSet'] + lookaheads: Dict[Symbol, Set[Rule]] + + def __init__(self, kernel, closure): + self.kernel = fzset(kernel) + self.closure = fzset(closure) + self.transitions = {} + self.lookaheads = defaultdict(set) + + def __repr__(self): + return '{%s | %s}' % (', '.join([repr(r) for r in self.kernel]), ', '.join([repr(r) for r in self.closure])) + + +def update_set(set1, set2): + if not set2 or set1 > set2: + return False + + copy = set(set1) + set1 |= set2 + return set1 != copy + +def calculate_sets(rules): + """Calculate FOLLOW sets. + + Adapted from: http://lara.epfl.ch/w/cc09:algorithm_for_first_and_follow_sets""" + symbols = {sym for rule in rules for sym in rule.expansion} | {rule.origin for rule in rules} + + # foreach grammar rule X ::= Y(1) ... Y(k) + # if k=0 or {Y(1),...,Y(k)} subset of NULLABLE then + # NULLABLE = NULLABLE union {X} + # for i = 1 to k + # if i=1 or {Y(1),...,Y(i-1)} subset of NULLABLE then + # FIRST(X) = FIRST(X) union FIRST(Y(i)) + # for j = i+1 to k + # if i=k or {Y(i+1),...Y(k)} subset of NULLABLE then + # FOLLOW(Y(i)) = FOLLOW(Y(i)) union FOLLOW(X) + # if i+1=j or {Y(i+1),...,Y(j-1)} subset of NULLABLE then + # FOLLOW(Y(i)) = FOLLOW(Y(i)) union FIRST(Y(j)) + # until none of NULLABLE,FIRST,FOLLOW changed in last iteration + + NULLABLE = set() + FIRST = {} + FOLLOW = {} + for sym in symbols: + FIRST[sym]={sym} if sym.is_term else set() + FOLLOW[sym]=set() + + # Calculate NULLABLE and FIRST + changed = True + while changed: + changed = False + + for rule in rules: + if set(rule.expansion) <= NULLABLE: + if update_set(NULLABLE, {rule.origin}): + changed = True + + for i, sym in enumerate(rule.expansion): + if set(rule.expansion[:i]) <= NULLABLE: + if update_set(FIRST[rule.origin], FIRST[sym]): + changed = True + else: + break + + # Calculate FOLLOW + changed = True + while changed: + changed = False + + for rule in rules: + for i, sym in enumerate(rule.expansion): + if i==len(rule.expansion)-1 or set(rule.expansion[i+1:]) <= NULLABLE: + if update_set(FOLLOW[sym], FOLLOW[rule.origin]): + changed = True + + for j in range(i+1, len(rule.expansion)): + if set(rule.expansion[i+1:j]) <= NULLABLE: + if update_set(FOLLOW[sym], FIRST[rule.expansion[j]]): + changed = True + + return FIRST, FOLLOW, NULLABLE + + +class GrammarAnalyzer: + def __init__(self, parser_conf: ParserConf, debug: bool=False, strict: bool=False): + self.debug = debug + self.strict = strict + + root_rules = {start: Rule(NonTerminal('$root_' + start), [NonTerminal(start), Terminal('$END')]) + for start in parser_conf.start} + + rules = parser_conf.rules + list(root_rules.values()) + self.rules_by_origin: Dict[NonTerminal, List[Rule]] = classify(rules, lambda r: r.origin) + + if len(rules) != len(set(rules)): + duplicates = [item for item, count in Counter(rules).items() if count > 1] + raise GrammarError("Rules defined twice: %s" % ', '.join(str(i) for i in duplicates)) + + for r in rules: + for sym in r.expansion: + if not (sym.is_term or sym in self.rules_by_origin): + raise GrammarError("Using an undefined rule: %s" % sym) + + self.start_states = {start: self.expand_rule(root_rule.origin) + for start, root_rule in root_rules.items()} + + self.end_states = {start: fzset({RulePtr(root_rule, len(root_rule.expansion))}) + for start, root_rule in root_rules.items()} + + lr0_root_rules = {start: Rule(NonTerminal('$root_' + start), [NonTerminal(start)]) + for start in parser_conf.start} + + lr0_rules = parser_conf.rules + list(lr0_root_rules.values()) + assert(len(lr0_rules) == len(set(lr0_rules))) + + self.lr0_rules_by_origin = classify(lr0_rules, lambda r: r.origin) + + # cache RulePtr(r, 0) in r (no duplicate RulePtr objects) + self.lr0_start_states = {start: LR0ItemSet([RulePtr(root_rule, 0)], self.expand_rule(root_rule.origin, self.lr0_rules_by_origin)) + for start, root_rule in lr0_root_rules.items()} + + self.FIRST, self.FOLLOW, self.NULLABLE = calculate_sets(rules) + + def expand_rule(self, source_rule: NonTerminal, rules_by_origin=None) -> State: + "Returns all init_ptrs accessible by rule (recursive)" + + if rules_by_origin is None: + rules_by_origin = self.rules_by_origin + + init_ptrs = set() + def _expand_rule(rule: NonTerminal) -> Iterator[NonTerminal]: + assert not rule.is_term, rule + + for r in rules_by_origin[rule]: + init_ptr = RulePtr(r, 0) + init_ptrs.add(init_ptr) + + if r.expansion: # if not empty rule + new_r = init_ptr.next + if not new_r.is_term: + assert isinstance(new_r, NonTerminal) + yield new_r + + for _ in bfs([source_rule], _expand_rule): + pass + + return fzset(init_ptrs) diff --git a/ccxt/static_dependencies/lark/parsers/lalr_analysis.py b/ccxt/static_dependencies/lark/parsers/lalr_analysis.py new file mode 100644 index 0000000..b7b3fdf --- /dev/null +++ b/ccxt/static_dependencies/lark/parsers/lalr_analysis.py @@ -0,0 +1,332 @@ +"""This module builds a LALR(1) transition-table for lalr_parser.py + +For now, shift/reduce conflicts are automatically resolved as shifts. +""" + +# Author: Erez Shinan (2017) +# Email : erezshin@gmail.com + +from typing import Dict, Set, Iterator, Tuple, List, TypeVar, Generic +from collections import defaultdict + +from ..utils import classify, classify_bool, bfs, fzset, Enumerator, logger +from ..exceptions import GrammarError + +from .grammar_analysis import GrammarAnalyzer, Terminal, LR0ItemSet, RulePtr, State +from ..grammar import Rule, Symbol +from ..common import ParserConf + +###{standalone + +class Action: + def __init__(self, name): + self.name = name + def __str__(self): + return self.name + def __repr__(self): + return str(self) + +Shift = Action('Shift') +Reduce = Action('Reduce') + +StateT = TypeVar("StateT") + +class ParseTableBase(Generic[StateT]): + states: Dict[StateT, Dict[str, Tuple]] + start_states: Dict[str, StateT] + end_states: Dict[str, StateT] + + def __init__(self, states, start_states, end_states): + self.states = states + self.start_states = start_states + self.end_states = end_states + + def serialize(self, memo): + tokens = Enumerator() + + states = { + state: {tokens.get(token): ((1, arg.serialize(memo)) if action is Reduce else (0, arg)) + for token, (action, arg) in actions.items()} + for state, actions in self.states.items() + } + + return { + 'tokens': tokens.reversed(), + 'states': states, + 'start_states': self.start_states, + 'end_states': self.end_states, + } + + @classmethod + def deserialize(cls, data, memo): + tokens = data['tokens'] + states = { + state: {tokens[token]: ((Reduce, Rule.deserialize(arg, memo)) if action==1 else (Shift, arg)) + for token, (action, arg) in actions.items()} + for state, actions in data['states'].items() + } + return cls(states, data['start_states'], data['end_states']) + +class ParseTable(ParseTableBase['State']): + """Parse-table whose key is State, i.e. set[RulePtr] + + Slower than IntParseTable, but useful for debugging + """ + pass + + +class IntParseTable(ParseTableBase[int]): + """Parse-table whose key is int. Best for performance.""" + + @classmethod + def from_ParseTable(cls, parse_table: ParseTable): + enum = list(parse_table.states) + state_to_idx: Dict['State', int] = {s:i for i,s in enumerate(enum)} + int_states = {} + + for s, la in parse_table.states.items(): + la = {k:(v[0], state_to_idx[v[1]]) if v[0] is Shift else v + for k,v in la.items()} + int_states[ state_to_idx[s] ] = la + + + start_states = {start:state_to_idx[s] for start, s in parse_table.start_states.items()} + end_states = {start:state_to_idx[s] for start, s in parse_table.end_states.items()} + return cls(int_states, start_states, end_states) + +###} + + +# digraph and traverse, see The Theory and Practice of Compiler Writing + +# computes F(x) = G(x) union (union { G(y) | x R y }) +# X: nodes +# R: relation (function mapping node -> list of nodes that satisfy the relation) +# G: set valued function +def digraph(X, R, G): + F = {} + S = [] + N = dict.fromkeys(X, 0) + for x in X: + # this is always true for the first iteration, but N[x] may be updated in traverse below + if N[x] == 0: + traverse(x, S, N, X, R, G, F) + return F + +# x: single node +# S: stack +# N: weights +# X: nodes +# R: relation (see above) +# G: set valued function +# F: set valued function we are computing (map of input -> output) +def traverse(x, S, N, X, R, G, F): + S.append(x) + d = len(S) + N[x] = d + F[x] = G[x] + for y in R[x]: + if N[y] == 0: + traverse(y, S, N, X, R, G, F) + n_x = N[x] + assert(n_x > 0) + n_y = N[y] + assert(n_y != 0) + if (n_y > 0) and (n_y < n_x): + N[x] = n_y + F[x].update(F[y]) + if N[x] == d: + f_x = F[x] + while True: + z = S.pop() + N[z] = -1 + F[z] = f_x + if z == x: + break + + +class LALR_Analyzer(GrammarAnalyzer): + lr0_itemsets: Set[LR0ItemSet] + nonterminal_transitions: List[Tuple[LR0ItemSet, Symbol]] + lookback: Dict[Tuple[LR0ItemSet, Symbol], Set[Tuple[LR0ItemSet, Rule]]] + includes: Dict[Tuple[LR0ItemSet, Symbol], Set[Tuple[LR0ItemSet, Symbol]]] + reads: Dict[Tuple[LR0ItemSet, Symbol], Set[Tuple[LR0ItemSet, Symbol]]] + directly_reads: Dict[Tuple[LR0ItemSet, Symbol], Set[Symbol]] + + + def __init__(self, parser_conf: ParserConf, debug: bool=False, strict: bool=False): + GrammarAnalyzer.__init__(self, parser_conf, debug, strict) + self.nonterminal_transitions = [] + self.directly_reads = defaultdict(set) + self.reads = defaultdict(set) + self.includes = defaultdict(set) + self.lookback = defaultdict(set) + + + def compute_lr0_states(self) -> None: + self.lr0_itemsets = set() + # map of kernels to LR0ItemSets + cache: Dict['State', LR0ItemSet] = {} + + def step(state: LR0ItemSet) -> Iterator[LR0ItemSet]: + _, unsat = classify_bool(state.closure, lambda rp: rp.is_satisfied) + + d = classify(unsat, lambda rp: rp.next) + for sym, rps in d.items(): + kernel = fzset({rp.advance(sym) for rp in rps}) + new_state = cache.get(kernel, None) + if new_state is None: + closure = set(kernel) + for rp in kernel: + if not rp.is_satisfied and not rp.next.is_term: + closure |= self.expand_rule(rp.next, self.lr0_rules_by_origin) + new_state = LR0ItemSet(kernel, closure) + cache[kernel] = new_state + + state.transitions[sym] = new_state + yield new_state + + self.lr0_itemsets.add(state) + + for _ in bfs(self.lr0_start_states.values(), step): + pass + + def compute_reads_relations(self): + # handle start state + for root in self.lr0_start_states.values(): + assert(len(root.kernel) == 1) + for rp in root.kernel: + assert(rp.index == 0) + self.directly_reads[(root, rp.next)] = set([ Terminal('$END') ]) + + for state in self.lr0_itemsets: + seen = set() + for rp in state.closure: + if rp.is_satisfied: + continue + s = rp.next + # if s is a not a nonterminal + if s not in self.lr0_rules_by_origin: + continue + if s in seen: + continue + seen.add(s) + nt = (state, s) + self.nonterminal_transitions.append(nt) + dr = self.directly_reads[nt] + r = self.reads[nt] + next_state = state.transitions[s] + for rp2 in next_state.closure: + if rp2.is_satisfied: + continue + s2 = rp2.next + # if s2 is a terminal + if s2 not in self.lr0_rules_by_origin: + dr.add(s2) + if s2 in self.NULLABLE: + r.add((next_state, s2)) + + def compute_includes_lookback(self): + for nt in self.nonterminal_transitions: + state, nonterminal = nt + includes = [] + lookback = self.lookback[nt] + for rp in state.closure: + if rp.rule.origin != nonterminal: + continue + # traverse the states for rp(.rule) + state2 = state + for i in range(rp.index, len(rp.rule.expansion)): + s = rp.rule.expansion[i] + nt2 = (state2, s) + state2 = state2.transitions[s] + if nt2 not in self.reads: + continue + for j in range(i + 1, len(rp.rule.expansion)): + if rp.rule.expansion[j] not in self.NULLABLE: + break + else: + includes.append(nt2) + # state2 is at the final state for rp.rule + if rp.index == 0: + for rp2 in state2.closure: + if (rp2.rule == rp.rule) and rp2.is_satisfied: + lookback.add((state2, rp2.rule)) + for nt2 in includes: + self.includes[nt2].add(nt) + + def compute_lookaheads(self): + read_sets = digraph(self.nonterminal_transitions, self.reads, self.directly_reads) + follow_sets = digraph(self.nonterminal_transitions, self.includes, read_sets) + + for nt, lookbacks in self.lookback.items(): + for state, rule in lookbacks: + for s in follow_sets[nt]: + state.lookaheads[s].add(rule) + + def compute_lalr1_states(self) -> None: + m: Dict[LR0ItemSet, Dict[str, Tuple]] = {} + reduce_reduce = [] + for itemset in self.lr0_itemsets: + actions: Dict[Symbol, Tuple] = {la: (Shift, next_state.closure) + for la, next_state in itemset.transitions.items()} + for la, rules in itemset.lookaheads.items(): + if len(rules) > 1: + # Try to resolve conflict based on priority + p = [(r.options.priority or 0, r) for r in rules] + p.sort(key=lambda r: r[0], reverse=True) + best, second_best = p[:2] + if best[0] > second_best[0]: + rules = {best[1]} + else: + reduce_reduce.append((itemset, la, rules)) + continue + + rule ,= rules + if la in actions: + if self.strict: + raise GrammarError(f"Shift/Reduce conflict for terminal {la.name}. [strict-mode]\n ") + elif self.debug: + logger.warning('Shift/Reduce conflict for terminal %s: (resolving as shift)', la.name) + logger.warning(' * %s', rule) + else: + logger.debug('Shift/Reduce conflict for terminal %s: (resolving as shift)', la.name) + logger.debug(' * %s', rule) + else: + actions[la] = (Reduce, rule) + m[itemset] = { k.name: v for k, v in actions.items() } + + if reduce_reduce: + msgs = [] + for itemset, la, rules in reduce_reduce: + msg = 'Reduce/Reduce collision in %s between the following rules: %s' % (la, ''.join([ '\n\t- ' + str(r) for r in rules ])) + if self.debug: + msg += '\n collision occurred in state: {%s\n }' % ''.join(['\n\t' + str(x) for x in itemset.closure]) + msgs.append(msg) + raise GrammarError('\n\n'.join(msgs)) + + states = { k.closure: v for k, v in m.items() } + + # compute end states + end_states: Dict[str, 'State'] = {} + for state in states: + for rp in state: + for start in self.lr0_start_states: + if rp.rule.origin.name == ('$root_' + start) and rp.is_satisfied: + assert start not in end_states + end_states[start] = state + + start_states = { start: state.closure for start, state in self.lr0_start_states.items() } + _parse_table = ParseTable(states, start_states, end_states) + + if self.debug: + self.parse_table = _parse_table + else: + self.parse_table = IntParseTable.from_ParseTable(_parse_table) + + def compute_lalr(self): + self.compute_lr0_states() + self.compute_reads_relations() + self.compute_includes_lookback() + self.compute_lookaheads() + self.compute_lalr1_states() diff --git a/ccxt/static_dependencies/lark/parsers/lalr_interactive_parser.py b/ccxt/static_dependencies/lark/parsers/lalr_interactive_parser.py new file mode 100644 index 0000000..c3060f4 --- /dev/null +++ b/ccxt/static_dependencies/lark/parsers/lalr_interactive_parser.py @@ -0,0 +1,158 @@ +# This module provides a LALR interactive parser, which is used for debugging and error handling + +from typing import Iterator, List +from copy import copy +import warnings + +from ..exceptions import UnexpectedToken +from ..lexer import Token, LexerThread +from .lalr_parser_state import ParserState + +###{standalone + +class InteractiveParser: + """InteractiveParser gives you advanced control over parsing and error handling when parsing with LALR. + + For a simpler interface, see the ``on_error`` argument to ``Lark.parse()``. + """ + def __init__(self, parser, parser_state: ParserState, lexer_thread: LexerThread): + self.parser = parser + self.parser_state = parser_state + self.lexer_thread = lexer_thread + self.result = None + + @property + def lexer_state(self) -> LexerThread: + warnings.warn("lexer_state will be removed in subsequent releases. Use lexer_thread instead.", DeprecationWarning) + return self.lexer_thread + + def feed_token(self, token: Token): + """Feed the parser with a token, and advance it to the next state, as if it received it from the lexer. + + Note that ``token`` has to be an instance of ``Token``. + """ + return self.parser_state.feed_token(token, token.type == '$END') + + def iter_parse(self) -> Iterator[Token]: + """Step through the different stages of the parse, by reading tokens from the lexer + and feeding them to the parser, one per iteration. + + Returns an iterator of the tokens it encounters. + + When the parse is over, the resulting tree can be found in ``InteractiveParser.result``. + """ + for token in self.lexer_thread.lex(self.parser_state): + yield token + self.result = self.feed_token(token) + + def exhaust_lexer(self) -> List[Token]: + """Try to feed the rest of the lexer state into the interactive parser. + + Note that this modifies the instance in place and does not feed an '$END' Token + """ + return list(self.iter_parse()) + + + def feed_eof(self, last_token=None): + """Feed a '$END' Token. Borrows from 'last_token' if given.""" + eof = Token.new_borrow_pos('$END', '', last_token) if last_token is not None else self.lexer_thread._Token('$END', '', 0, 1, 1) + return self.feed_token(eof) + + + def __copy__(self): + """Create a new interactive parser with a separate state. + + Calls to feed_token() won't affect the old instance, and vice-versa. + """ + return self.copy() + + def copy(self, deepcopy_values=True): + return type(self)( + self.parser, + self.parser_state.copy(deepcopy_values=deepcopy_values), + copy(self.lexer_thread), + ) + + def __eq__(self, other): + if not isinstance(other, InteractiveParser): + return False + + return self.parser_state == other.parser_state and self.lexer_thread == other.lexer_thread + + def as_immutable(self): + """Convert to an ``ImmutableInteractiveParser``.""" + p = copy(self) + return ImmutableInteractiveParser(p.parser, p.parser_state, p.lexer_thread) + + def pretty(self): + """Print the output of ``choices()`` in a way that's easier to read.""" + out = ["Parser choices:"] + for k, v in self.choices().items(): + out.append('\t- %s -> %r' % (k, v)) + out.append('stack size: %s' % len(self.parser_state.state_stack)) + return '\n'.join(out) + + def choices(self): + """Returns a dictionary of token types, matched to their action in the parser. + + Only returns token types that are accepted by the current state. + + Updated by ``feed_token()``. + """ + return self.parser_state.parse_conf.parse_table.states[self.parser_state.position] + + def accepts(self): + """Returns the set of possible tokens that will advance the parser into a new valid state.""" + accepts = set() + conf_no_callbacks = copy(self.parser_state.parse_conf) + # We don't want to call callbacks here since those might have arbitrary side effects + # and are unnecessarily slow. + conf_no_callbacks.callbacks = {} + for t in self.choices(): + if t.isupper(): # is terminal? + new_cursor = self.copy(deepcopy_values=False) + new_cursor.parser_state.parse_conf = conf_no_callbacks + try: + new_cursor.feed_token(self.lexer_thread._Token(t, '')) + except UnexpectedToken: + pass + else: + accepts.add(t) + return accepts + + def resume_parse(self): + """Resume automated parsing from the current state. + """ + return self.parser.parse_from_state(self.parser_state, last_token=self.lexer_thread.state.last_token) + + + +class ImmutableInteractiveParser(InteractiveParser): + """Same as ``InteractiveParser``, but operations create a new instance instead + of changing it in-place. + """ + + result = None + + def __hash__(self): + return hash((self.parser_state, self.lexer_thread)) + + def feed_token(self, token): + c = copy(self) + c.result = InteractiveParser.feed_token(c, token) + return c + + def exhaust_lexer(self): + """Try to feed the rest of the lexer state into the parser. + + Note that this returns a new ImmutableInteractiveParser and does not feed an '$END' Token""" + cursor = self.as_mutable() + cursor.exhaust_lexer() + return cursor.as_immutable() + + def as_mutable(self): + """Convert to an ``InteractiveParser``.""" + p = copy(self) + return InteractiveParser(p.parser, p.parser_state, p.lexer_thread) + +###} diff --git a/ccxt/static_dependencies/lark/parsers/lalr_parser.py b/ccxt/static_dependencies/lark/parsers/lalr_parser.py new file mode 100644 index 0000000..5b2d390 --- /dev/null +++ b/ccxt/static_dependencies/lark/parsers/lalr_parser.py @@ -0,0 +1,122 @@ +"""This module implements a LALR(1) Parser +""" +# Author: Erez Shinan (2017) +# Email : erezshin@gmail.com +from typing import Dict, Any, Optional +from ..lexer import Token, LexerThread +from ..utils import Serialize +from ..common import ParserConf, ParserCallbacks + +from .lalr_analysis import LALR_Analyzer, IntParseTable, ParseTableBase +from .lalr_interactive_parser import InteractiveParser +from ..exceptions import UnexpectedCharacters, UnexpectedInput, UnexpectedToken +from .lalr_parser_state import ParserState, ParseConf + +###{standalone + +class LALR_Parser(Serialize): + def __init__(self, parser_conf: ParserConf, debug: bool=False, strict: bool=False): + analysis = LALR_Analyzer(parser_conf, debug=debug, strict=strict) + analysis.compute_lalr() + callbacks = parser_conf.callbacks + + self._parse_table = analysis.parse_table + self.parser_conf = parser_conf + self.parser = _Parser(analysis.parse_table, callbacks, debug) + + @classmethod + def deserialize(cls, data, memo, callbacks, debug=False): + inst = cls.__new__(cls) + inst._parse_table = IntParseTable.deserialize(data, memo) + inst.parser = _Parser(inst._parse_table, callbacks, debug) + return inst + + def serialize(self, memo: Any = None) -> Dict[str, Any]: + return self._parse_table.serialize(memo) + + def parse_interactive(self, lexer: LexerThread, start: str): + return self.parser.parse(lexer, start, start_interactive=True) + + def parse(self, lexer, start, on_error=None): + try: + return self.parser.parse(lexer, start) + except UnexpectedInput as e: + if on_error is None: + raise + + while True: + if isinstance(e, UnexpectedCharacters): + s = e.interactive_parser.lexer_thread.state + p = s.line_ctr.char_pos + + if not on_error(e): + raise e + + if isinstance(e, UnexpectedCharacters): + # If user didn't change the character position, then we should + if p == s.line_ctr.char_pos: + s.line_ctr.feed(s.text[p:p+1]) + + try: + return e.interactive_parser.resume_parse() + except UnexpectedToken as e2: + if (isinstance(e, UnexpectedToken) + and e.token.type == e2.token.type == '$END' + and e.interactive_parser == e2.interactive_parser): + # Prevent infinite loop + raise e2 + e = e2 + except UnexpectedCharacters as e2: + e = e2 + + +class _Parser: + parse_table: ParseTableBase + callbacks: ParserCallbacks + debug: bool + + def __init__(self, parse_table: ParseTableBase, callbacks: ParserCallbacks, debug: bool=False): + self.parse_table = parse_table + self.callbacks = callbacks + self.debug = debug + + def parse(self, lexer: LexerThread, start: str, value_stack=None, state_stack=None, start_interactive=False): + parse_conf = ParseConf(self.parse_table, self.callbacks, start) + parser_state = ParserState(parse_conf, lexer, state_stack, value_stack) + if start_interactive: + return InteractiveParser(self, parser_state, parser_state.lexer) + return self.parse_from_state(parser_state) + + + def parse_from_state(self, state: ParserState, last_token: Optional[Token]=None): + """Run the main LALR parser loop + + Parameters: + state - the initial state. Changed in-place. + last_token - Used only for line information in case of an empty lexer. + """ + try: + token = last_token + for token in state.lexer.lex(state): + assert token is not None + state.feed_token(token) + + end_token = Token.new_borrow_pos('$END', '', token) if token else Token('$END', '', 0, 1, 1) + return state.feed_token(end_token, True) + except UnexpectedInput as e: + try: + e.interactive_parser = InteractiveParser(self, state, state.lexer) + except NameError: + pass + raise e + except Exception as e: + if self.debug: + print("") + print("STATE STACK DUMP") + print("----------------") + for i, s in enumerate(state.state_stack): + print('%d)' % i , s) + print("") + + raise +###} diff --git a/ccxt/static_dependencies/lark/parsers/lalr_parser_state.py b/ccxt/static_dependencies/lark/parsers/lalr_parser_state.py new file mode 100644 index 0000000..d2b8e36 --- /dev/null +++ b/ccxt/static_dependencies/lark/parsers/lalr_parser_state.py @@ -0,0 +1,110 @@ +from copy import deepcopy, copy +from typing import Dict, Any, Generic, List +from ..lexer import Token, LexerThread +from ..common import ParserCallbacks + +from .lalr_analysis import Shift, ParseTableBase, StateT +from ..exceptions import UnexpectedToken + +###{standalone + +class ParseConf(Generic[StateT]): + __slots__ = 'parse_table', 'callbacks', 'start', 'start_state', 'end_state', 'states' + + parse_table: ParseTableBase[StateT] + callbacks: ParserCallbacks + start: str + + start_state: StateT + end_state: StateT + states: Dict[StateT, Dict[str, tuple]] + + def __init__(self, parse_table: ParseTableBase[StateT], callbacks: ParserCallbacks, start: str): + self.parse_table = parse_table + + self.start_state = self.parse_table.start_states[start] + self.end_state = self.parse_table.end_states[start] + self.states = self.parse_table.states + + self.callbacks = callbacks + self.start = start + +class ParserState(Generic[StateT]): + __slots__ = 'parse_conf', 'lexer', 'state_stack', 'value_stack' + + parse_conf: ParseConf[StateT] + lexer: LexerThread + state_stack: List[StateT] + value_stack: list + + def __init__(self, parse_conf: ParseConf[StateT], lexer: LexerThread, state_stack=None, value_stack=None): + self.parse_conf = parse_conf + self.lexer = lexer + self.state_stack = state_stack or [self.parse_conf.start_state] + self.value_stack = value_stack or [] + + @property + def position(self) -> StateT: + return self.state_stack[-1] + + # Necessary for match_examples() to work + def __eq__(self, other) -> bool: + if not isinstance(other, ParserState): + return NotImplemented + return len(self.state_stack) == len(other.state_stack) and self.position == other.position + + def __copy__(self): + return self.copy() + + def copy(self, deepcopy_values=True) -> 'ParserState[StateT]': + return type(self)( + self.parse_conf, + self.lexer, # XXX copy + copy(self.state_stack), + deepcopy(self.value_stack) if deepcopy_values else copy(self.value_stack), + ) + + def feed_token(self, token: Token, is_end=False) -> Any: + state_stack = self.state_stack + value_stack = self.value_stack + states = self.parse_conf.states + end_state = self.parse_conf.end_state + callbacks = self.parse_conf.callbacks + + while True: + state = state_stack[-1] + try: + action, arg = states[state][token.type] + except KeyError: + expected = {s for s in states[state].keys() if s.isupper()} + raise UnexpectedToken(token, expected, state=self, interactive_parser=None) + + assert arg != end_state + + if action is Shift: + # shift once and return + assert not is_end + state_stack.append(arg) + value_stack.append(token if token.type not in callbacks else callbacks[token.type](token)) + return + else: + # reduce+shift as many times as necessary + rule = arg + size = len(rule.expansion) + if size: + s = value_stack[-size:] + del state_stack[-size:] + del value_stack[-size:] + else: + s = [] + + value = callbacks[rule](s) if callbacks else s + + _action, new_state = states[state_stack[-1]][rule.origin.name] + assert _action is Shift + state_stack.append(new_state) + value_stack.append(value) + + if is_end and state_stack[-1] == end_state: + return value_stack[-1] +###} diff --git a/ccxt/static_dependencies/lark/parsers/xearley.py b/ccxt/static_dependencies/lark/parsers/xearley.py new file mode 100644 index 0000000..a0f43ac --- /dev/null +++ b/ccxt/static_dependencies/lark/parsers/xearley.py @@ -0,0 +1,165 @@ +"""This module implements an Earley parser with a dynamic lexer + +The core Earley algorithm used here is based on Elizabeth Scott's implementation, here: + https://www.sciencedirect.com/science/article/pii/S1571066108001497 + +That is probably the best reference for understanding the algorithm here. + +The Earley parser outputs an SPPF-tree as per that document. The SPPF tree format +is better documented here: + http://www.bramvandersanden.com/post/2014/06/shared-packed-parse-forest/ + +Instead of running a lexer beforehand, or using a costy char-by-char method, this parser +uses regular expressions by necessity, achieving high-performance while maintaining all of +Earley's power in parsing any CFG. +""" + +from typing import TYPE_CHECKING, Callable, Optional, List, Any +from collections import defaultdict + +from ..tree import Tree +from ..exceptions import UnexpectedCharacters +from ..lexer import Token +from ..grammar import Terminal +from .earley import Parser as BaseParser +from .earley_forest import TokenNode + +if TYPE_CHECKING: + from ..common import LexerConf, ParserConf + +class Parser(BaseParser): + def __init__(self, lexer_conf: 'LexerConf', parser_conf: 'ParserConf', term_matcher: Callable, + resolve_ambiguity: bool=True, complete_lex: bool=False, debug: bool=False, + tree_class: Optional[Callable[[str, List], Any]]=Tree, ordered_sets: bool=True): + BaseParser.__init__(self, lexer_conf, parser_conf, term_matcher, resolve_ambiguity, + debug, tree_class, ordered_sets) + self.ignore = [Terminal(t) for t in lexer_conf.ignore] + self.complete_lex = complete_lex + + def _parse(self, stream, columns, to_scan, start_symbol=None): + + def scan(i, to_scan): + """The core Earley Scanner. + + This is a custom implementation of the scanner that uses the + Lark lexer to match tokens. The scan list is built by the + Earley predictor, based on the previously completed tokens. + This ensures that at each phase of the parse we have a custom + lexer context, allowing for more complex ambiguities.""" + + node_cache = {} + + # 1) Loop the expectations and ask the lexer to match. + # Since regexp is forward looking on the input stream, and we only + # want to process tokens when we hit the point in the stream at which + # they complete, we push all tokens into a buffer (delayed_matches), to + # be held possibly for a later parse step when we reach the point in the + # input stream at which they complete. + for item in self.Set(to_scan): + m = match(item.expect, stream, i) + if m: + t = Token(item.expect.name, m.group(0), i, text_line, text_column) + delayed_matches[m.end()].append( (item, i, t) ) + + if self.complete_lex: + s = m.group(0) + for j in range(1, len(s)): + m = match(item.expect, s[:-j]) + if m: + t = Token(item.expect.name, m.group(0), i, text_line, text_column) + delayed_matches[i+m.end()].append( (item, i, t) ) + + # XXX The following 3 lines were commented out for causing a bug. See issue #768 + # # Remove any items that successfully matched in this pass from the to_scan buffer. + # # This ensures we don't carry over tokens that already matched, if we're ignoring below. + # to_scan.remove(item) + + # 3) Process any ignores. This is typically used for e.g. whitespace. + # We carry over any unmatched items from the to_scan buffer to be matched again after + # the ignore. This should allow us to use ignored symbols in non-terminals to implement + # e.g. mandatory spacing. + for x in self.ignore: + m = match(x, stream, i) + if m: + # Carry over any items still in the scan buffer, to past the end of the ignored items. + delayed_matches[m.end()].extend([(item, i, None) for item in to_scan ]) + + # If we're ignoring up to the end of the file, # carry over the start symbol if it already completed. + delayed_matches[m.end()].extend([(item, i, None) for item in columns[i] if item.is_complete and item.s == start_symbol]) + + next_to_scan = self.Set() + next_set = self.Set() + columns.append(next_set) + transitives.append({}) + + ## 4) Process Tokens from delayed_matches. + # This is the core of the Earley scanner. Create an SPPF node for each Token, + # and create the symbol node in the SPPF tree. Advance the item that completed, + # and add the resulting new item to either the Earley set (for processing by the + # completer/predictor) or the to_scan buffer for the next parse step. + for item, start, token in delayed_matches[i+1]: + if token is not None: + token.end_line = text_line + token.end_column = text_column + 1 + token.end_pos = i + 1 + + new_item = item.advance() + label = (new_item.s, new_item.start, i + 1) + token_node = TokenNode(token, terminals[token.type]) + new_item.node = node_cache[label] if label in node_cache else node_cache.setdefault(label, self.SymbolNode(*label)) + new_item.node.add_family(new_item.s, item.rule, new_item.start, item.node, token_node) + else: + new_item = item + + if new_item.expect in self.TERMINALS: + # add (B ::= Aai+1.B, h, y) to Q' + next_to_scan.add(new_item) + else: + # add (B ::= Aa+1.B, h, y) to Ei+1 + next_set.add(new_item) + + del delayed_matches[i+1] # No longer needed, so unburden memory + + if not next_set and not delayed_matches and not next_to_scan: + considered_rules = list(sorted(to_scan, key=lambda key: key.rule.origin.name)) + raise UnexpectedCharacters(stream, i, text_line, text_column, {item.expect.name for item in to_scan}, + set(to_scan), state=frozenset(i.s for i in to_scan), + considered_rules=considered_rules + ) + + return next_to_scan + + + delayed_matches = defaultdict(list) + match = self.term_matcher + terminals = self.lexer_conf.terminals_by_name + + # Cache for nodes & tokens created in a particular parse step. + transitives = [{}] + + text_line = 1 + text_column = 1 + + ## The main Earley loop. + # Run the Prediction/Completion cycle for any Items in the current Earley set. + # Completions will be added to the SPPF tree, and predictions will be recursively + # processed down to terminals/empty nodes to be added to the scanner for the next + # step. + i = 0 + for token in stream: + self.predict_and_complete(i, to_scan, columns, transitives) + + to_scan = scan(i, to_scan) + + if token == '\n': + text_line += 1 + text_column = 1 + else: + text_column += 1 + i += 1 + + self.predict_and_complete(i, to_scan, columns, transitives) + + ## Column is now the final column in the parse. + assert i == len(columns)-1 + return to_scan diff --git a/ccxt/static_dependencies/lark/py.typed b/ccxt/static_dependencies/lark/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/ccxt/static_dependencies/lark/reconstruct.py b/ccxt/static_dependencies/lark/reconstruct.py new file mode 100644 index 0000000..2d8423a --- /dev/null +++ b/ccxt/static_dependencies/lark/reconstruct.py @@ -0,0 +1,107 @@ +"""This is an experimental tool for reconstructing text from a shaped tree, based on a Lark grammar. +""" + +from typing import Dict, Callable, Iterable, Optional + +from .lark import Lark +from .tree import Tree, ParseTree +from .visitors import Transformer_InPlace +from .lexer import Token, PatternStr, TerminalDef +from .grammar import Terminal, NonTerminal, Symbol + +from .tree_matcher import TreeMatcher, is_discarded_terminal +from .utils import is_id_continue + +def is_iter_empty(i): + try: + _ = next(i) + return False + except StopIteration: + return True + + +class WriteTokensTransformer(Transformer_InPlace): + "Inserts discarded tokens into their correct place, according to the rules of grammar" + + tokens: Dict[str, TerminalDef] + term_subs: Dict[str, Callable[[Symbol], str]] + + def __init__(self, tokens: Dict[str, TerminalDef], term_subs: Dict[str, Callable[[Symbol], str]]) -> None: + self.tokens = tokens + self.term_subs = term_subs + + def __default__(self, data, children, meta): + if not getattr(meta, 'match_tree', False): + return Tree(data, children) + + iter_args = iter(children) + to_write = [] + for sym in meta.orig_expansion: + if is_discarded_terminal(sym): + try: + v = self.term_subs[sym.name](sym) + except KeyError: + t = self.tokens[sym.name] + if not isinstance(t.pattern, PatternStr): + raise NotImplementedError("Reconstructing regexps not supported yet: %s" % t) + + v = t.pattern.value + to_write.append(v) + else: + x = next(iter_args) + if isinstance(x, list): + to_write += x + else: + if isinstance(x, Token): + assert Terminal(x.type) == sym, x + else: + assert NonTerminal(x.data) == sym, (sym, x) + to_write.append(x) + + assert is_iter_empty(iter_args) + return to_write + + +class Reconstructor(TreeMatcher): + """ + A Reconstructor that will, given a full parse Tree, generate source code. + + Note: + The reconstructor cannot generate values from regexps. If you need to produce discarded + regexes, such as newlines, use `term_subs` and provide default values for them. + + Parameters: + parser: a Lark instance + term_subs: a dictionary of [Terminal name as str] to [output text as str] + """ + + write_tokens: WriteTokensTransformer + + def __init__(self, parser: Lark, term_subs: Optional[Dict[str, Callable[[Symbol], str]]]=None) -> None: + TreeMatcher.__init__(self, parser) + + self.write_tokens = WriteTokensTransformer({t.name:t for t in self.tokens}, term_subs or {}) + + def _reconstruct(self, tree): + unreduced_tree = self.match_tree(tree, tree.data) + + res = self.write_tokens.transform(unreduced_tree) + for item in res: + if isinstance(item, Tree): + # TODO use orig_expansion.rulename to support templates + yield from self._reconstruct(item) + else: + yield item + + def reconstruct(self, tree: ParseTree, postproc: Optional[Callable[[Iterable[str]], Iterable[str]]]=None, insert_spaces: bool=True) -> str: + x = self._reconstruct(tree) + if postproc: + x = postproc(x) + y = [] + prev_item = '' + for item in x: + if insert_spaces and prev_item and item and is_id_continue(prev_item[-1]) and is_id_continue(item[0]): + y.append(' ') + y.append(item) + prev_item = item + return ''.join(y) diff --git a/ccxt/static_dependencies/lark/tools/__init__.py b/ccxt/static_dependencies/lark/tools/__init__.py new file mode 100644 index 0000000..eeb40e1 --- /dev/null +++ b/ccxt/static_dependencies/lark/tools/__init__.py @@ -0,0 +1,70 @@ +import sys +from argparse import ArgumentParser, FileType +from textwrap import indent +from logging import DEBUG, INFO, WARN, ERROR +from typing import Optional +import warnings + +from lark import Lark, logger +try: + from interegular import logger as interegular_logger + has_interegular = True +except ImportError: + has_interegular = False + +lalr_argparser = ArgumentParser(add_help=False, epilog='Look at the Lark documentation for more info on the options') + +flags = [ + ('d', 'debug'), + 'keep_all_tokens', + 'regex', + 'propagate_positions', + 'maybe_placeholders', + 'use_bytes' +] + +options = ['start', 'lexer'] + +lalr_argparser.add_argument('-v', '--verbose', action='count', default=0, help="Increase Logger output level, up to three times") +lalr_argparser.add_argument('-s', '--start', action='append', default=[]) +lalr_argparser.add_argument('-l', '--lexer', default='contextual', choices=('basic', 'contextual')) +lalr_argparser.add_argument('-o', '--out', type=FileType('w', encoding='utf-8'), default=sys.stdout, help='the output file (default=stdout)') +lalr_argparser.add_argument('grammar_file', type=FileType('r', encoding='utf-8'), help='A valid .lark file') + +for flag in flags: + if isinstance(flag, tuple): + options.append(flag[1]) + lalr_argparser.add_argument('-' + flag[0], '--' + flag[1], action='store_true') + elif isinstance(flag, str): + options.append(flag) + lalr_argparser.add_argument('--' + flag, action='store_true') + else: + raise NotImplementedError("flags must only contain strings or tuples of strings") + + +def build_lalr(namespace): + logger.setLevel((ERROR, WARN, INFO, DEBUG)[min(namespace.verbose, 3)]) + if has_interegular: + interegular_logger.setLevel(logger.getEffectiveLevel()) + if len(namespace.start) == 0: + namespace.start.append('start') + kwargs = {n: getattr(namespace, n) for n in options} + return Lark(namespace.grammar_file, parser='lalr', **kwargs), namespace.out + + +def showwarning_as_comment(message, category, filename, lineno, file=None, line=None): + # Based on warnings._showwarnmsg_impl + text = warnings.formatwarning(message, category, filename, lineno, line) + text = indent(text, '# ') + if file is None: + file = sys.stderr + if file is None: + return + try: + file.write(text) + except OSError: + pass + + +def make_warnings_comments(): + warnings.showwarning = showwarning_as_comment diff --git a/ccxt/static_dependencies/lark/tools/nearley.py b/ccxt/static_dependencies/lark/tools/nearley.py new file mode 100644 index 0000000..1fc27d5 --- /dev/null +++ b/ccxt/static_dependencies/lark/tools/nearley.py @@ -0,0 +1,202 @@ +"Converts Nearley grammars to Lark" + +import os.path +import sys +import codecs +import argparse + + +from lark import Lark, Transformer, v_args + +nearley_grammar = r""" + start: (ruledef|directive)+ + + directive: "@" NAME (STRING|NAME) + | "@" JS -> js_code + ruledef: NAME "->" expansions + | NAME REGEXP "->" expansions -> macro + expansions: expansion ("|" expansion)* + + expansion: expr+ js + + ?expr: item (":" /[+*?]/)? + + ?item: rule|string|regexp|null + | "(" expansions ")" + + rule: NAME + string: STRING + regexp: REGEXP + null: "null" + JS: /{%.*?%}/s + js: JS? + + NAME: /[a-zA-Z_$]\w*/ + COMMENT: /#[^\n]*/ + REGEXP: /\[.*?\]/ + + STRING: _STRING "i"? + + %import common.ESCAPED_STRING -> _STRING + %import common.WS + %ignore WS + %ignore COMMENT + + """ + +nearley_grammar_parser = Lark(nearley_grammar, parser='earley', lexer='basic') + +def _get_rulename(name): + name = {'_': '_ws_maybe', '__': '_ws'}.get(name, name) + return 'n_' + name.replace('$', '__DOLLAR__').lower() + +@v_args(inline=True) +class NearleyToLark(Transformer): + def __init__(self): + self._count = 0 + self.extra_rules = {} + self.extra_rules_rev = {} + self.alias_js_code = {} + + def _new_function(self, code): + name = 'alias_%d' % self._count + self._count += 1 + + self.alias_js_code[name] = code + return name + + def _extra_rule(self, rule): + if rule in self.extra_rules_rev: + return self.extra_rules_rev[rule] + + name = 'xrule_%d' % len(self.extra_rules) + assert name not in self.extra_rules + self.extra_rules[name] = rule + self.extra_rules_rev[rule] = name + return name + + def rule(self, name): + return _get_rulename(name) + + def ruledef(self, name, exps): + return '!%s: %s' % (_get_rulename(name), exps) + + def expr(self, item, op): + rule = '(%s)%s' % (item, op) + return self._extra_rule(rule) + + def regexp(self, r): + return '/%s/' % r + + def null(self): + return '' + + def string(self, s): + return self._extra_rule(s) + + def expansion(self, *x): + x, js = x[:-1], x[-1] + if js.children: + js_code ,= js.children + js_code = js_code[2:-2] + alias = '-> ' + self._new_function(js_code) + else: + alias = '' + return ' '.join(x) + alias + + def expansions(self, *x): + return '%s' % ('\n |'.join(x)) + + def start(self, *rules): + return '\n'.join(filter(None, rules)) + +def _nearley_to_lark(g, builtin_path, n2l, js_code, folder_path, includes): + rule_defs = [] + + tree = nearley_grammar_parser.parse(g) + for statement in tree.children: + if statement.data == 'directive': + directive, arg = statement.children + if directive in ('builtin', 'include'): + folder = builtin_path if directive == 'builtin' else folder_path + path = os.path.join(folder, arg[1:-1]) + if path not in includes: + includes.add(path) + with codecs.open(path, encoding='utf8') as f: + text = f.read() + rule_defs += _nearley_to_lark(text, builtin_path, n2l, js_code, os.path.abspath(os.path.dirname(path)), includes) + else: + assert False, directive + elif statement.data == 'js_code': + code ,= statement.children + code = code[2:-2] + js_code.append(code) + elif statement.data == 'macro': + pass # TODO Add support for macros! + elif statement.data == 'ruledef': + rule_defs.append(n2l.transform(statement)) + else: + raise Exception("Unknown statement: %s" % statement) + + return rule_defs + + +def create_code_for_nearley_grammar(g, start, builtin_path, folder_path, es6=False): + import js2py + + emit_code = [] + def emit(x=None): + if x: + emit_code.append(x) + emit_code.append('\n') + + js_code = ['function id(x) {return x[0];}'] + n2l = NearleyToLark() + rule_defs = _nearley_to_lark(g, builtin_path, n2l, js_code, folder_path, set()) + lark_g = '\n'.join(rule_defs) + lark_g += '\n'+'\n'.join('!%s: %s' % item for item in n2l.extra_rules.items()) + + emit('from lark import Lark, Transformer') + emit() + emit('grammar = ' + repr(lark_g)) + emit() + + for alias, code in n2l.alias_js_code.items(): + js_code.append('%s = (%s);' % (alias, code)) + + if es6: + emit(js2py.translate_js6('\n'.join(js_code))) + else: + emit(js2py.translate_js('\n'.join(js_code))) + emit('class TransformNearley(Transformer):') + for alias in n2l.alias_js_code: + emit(" %s = var.get('%s').to_python()" % (alias, alias)) + emit(" __default__ = lambda self, n, c, m: c if c else None") + + emit() + emit('parser = Lark(grammar, start="n_%s", maybe_placeholders=False)' % start) + emit('def parse(text):') + emit(' return TransformNearley().transform(parser.parse(text))') + + return ''.join(emit_code) + +def main(fn, start, nearley_lib, es6=False): + with codecs.open(fn, encoding='utf8') as f: + grammar = f.read() + return create_code_for_nearley_grammar(grammar, start, os.path.join(nearley_lib, 'builtin'), os.path.abspath(os.path.dirname(fn)), es6=es6) + +def get_arg_parser(): + parser = argparse.ArgumentParser(description='Reads a Nearley grammar (with js functions), and outputs an equivalent lark parser.') + parser.add_argument('nearley_grammar', help='Path to the file containing the nearley grammar') + parser.add_argument('start_rule', help='Rule within the nearley grammar to make the base rule') + parser.add_argument('nearley_lib', help='Path to root directory of nearley codebase (used for including builtins)') + parser.add_argument('--es6', help='Enable experimental ES6 support', action='store_true') + return parser + +if __name__ == '__main__': + parser = get_arg_parser() + if len(sys.argv) == 1: + parser.print_help(sys.stderr) + sys.exit(1) + args = parser.parse_args() + print(main(fn=args.nearley_grammar, start=args.start_rule, nearley_lib=args.nearley_lib, es6=args.es6)) diff --git a/ccxt/static_dependencies/lark/tools/serialize.py b/ccxt/static_dependencies/lark/tools/serialize.py new file mode 100644 index 0000000..eb28824 --- /dev/null +++ b/ccxt/static_dependencies/lark/tools/serialize.py @@ -0,0 +1,32 @@ +import sys +import json + +from lark.grammar import Rule +from lark.lexer import TerminalDef +from lark.tools import lalr_argparser, build_lalr + +import argparse + +argparser = argparse.ArgumentParser(prog='python -m lark.tools.serialize', parents=[lalr_argparser], + description="Lark Serialization Tool - Stores Lark's internal state & LALR analysis as a JSON file", + epilog='Look at the Lark documentation for more info on the options') + + +def serialize(lark_inst, outfile): + data, memo = lark_inst.memo_serialize([TerminalDef, Rule]) + outfile.write('{\n') + outfile.write(' "data": %s,\n' % json.dumps(data)) + outfile.write(' "memo": %s\n' % json.dumps(memo)) + outfile.write('}\n') + + +def main(): + if len(sys.argv)==1: + argparser.print_help(sys.stderr) + sys.exit(1) + ns = argparser.parse_args() + serialize(*build_lalr(ns)) + + +if __name__ == '__main__': + main() diff --git a/ccxt/static_dependencies/lark/tools/standalone.py b/ccxt/static_dependencies/lark/tools/standalone.py new file mode 100644 index 0000000..9940ccb --- /dev/null +++ b/ccxt/static_dependencies/lark/tools/standalone.py @@ -0,0 +1,196 @@ +###{standalone +# +# +# Lark Stand-alone Generator Tool +# ---------------------------------- +# Generates a stand-alone LALR(1) parser +# +# Git: https://github.com/erezsh/lark +# Author: Erez Shinan (erezshin@gmail.com) +# +# +# >>> LICENSE +# +# This tool and its generated code use a separate license from Lark, +# and are subject to the terms of the Mozilla Public License, v. 2.0. +# If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +# +# If you wish to purchase a commercial license for this tool and its +# generated code, you may contact me via email or otherwise. +# +# If MPL2 is incompatible with your free or open-source project, +# contact me and we'll work it out. +# +# + +from copy import deepcopy +from abc import ABC, abstractmethod +from types import ModuleType +from typing import ( + TypeVar, Generic, Type, Tuple, List, Dict, Iterator, Collection, Callable, Optional, FrozenSet, Any, + Union, Iterable, IO, TYPE_CHECKING, overload, Sequence, + Pattern as REPattern, ClassVar, Set, Mapping +) +###} + +import sys +import token, tokenize +import os +from os import path +from collections import defaultdict +from functools import partial +from argparse import ArgumentParser + +import lark +from lark.tools import lalr_argparser, build_lalr, make_warnings_comments + + +from lark.grammar import Rule +from lark.lexer import TerminalDef + +_dir = path.dirname(__file__) +_larkdir = path.join(_dir, path.pardir) + + +EXTRACT_STANDALONE_FILES = [ + 'tools/standalone.py', + 'exceptions.py', + 'utils.py', + 'tree.py', + 'visitors.py', + 'grammar.py', + 'lexer.py', + 'common.py', + 'parse_tree_builder.py', + 'parsers/lalr_analysis.py', + 'parsers/lalr_parser_state.py', + 'parsers/lalr_parser.py', + 'parsers/lalr_interactive_parser.py', + 'parser_frontends.py', + 'lark.py', + 'indenter.py', +] + +def extract_sections(lines): + section = None + text = [] + sections = defaultdict(list) + for line in lines: + if line.startswith('###'): + if line[3] == '{': + section = line[4:].strip() + elif line[3] == '}': + sections[section] += text + section = None + text = [] + else: + raise ValueError(line) + elif section: + text.append(line) + + return {name: ''.join(text) for name, text in sections.items()} + + +def strip_docstrings(line_gen): + """ Strip comments and docstrings from a file. + Based on code from: https://stackoverflow.com/questions/1769332/script-to-remove-python-comments-docstrings + """ + res = [] + + prev_toktype = token.INDENT + last_lineno = -1 + last_col = 0 + + tokgen = tokenize.generate_tokens(line_gen) + for toktype, ttext, (slineno, scol), (elineno, ecol), ltext in tokgen: + if slineno > last_lineno: + last_col = 0 + if scol > last_col: + res.append(" " * (scol - last_col)) + if toktype == token.STRING and prev_toktype == token.INDENT: + # Docstring + res.append("#--") + elif toktype == tokenize.COMMENT: + # Comment + res.append("##\n") + else: + res.append(ttext) + prev_toktype = toktype + last_col = ecol + last_lineno = elineno + + return ''.join(res) + + +def gen_standalone(lark_inst, output=None, out=sys.stdout, compress=False): + if output is None: + output = partial(print, file=out) + + import pickle, zlib, base64 + def compressed_output(obj): + s = pickle.dumps(obj, pickle.HIGHEST_PROTOCOL) + c = zlib.compress(s) + output(repr(base64.b64encode(c))) + + def output_decompress(name): + output('%(name)s = pickle.loads(zlib.decompress(base64.b64decode(%(name)s)))' % locals()) + + output('# The file was automatically generated by Lark v%s' % lark.__version__) + output('__version__ = "%s"' % lark.__version__) + output() + + for i, pyfile in enumerate(EXTRACT_STANDALONE_FILES): + with open(os.path.join(_larkdir, pyfile)) as f: + code = extract_sections(f)['standalone'] + if i: # if not this file + code = strip_docstrings(partial(next, iter(code.splitlines(True)))) + output(code) + + data, m = lark_inst.memo_serialize([TerminalDef, Rule]) + output('import pickle, zlib, base64') + if compress: + output('DATA = (') + compressed_output(data) + output(')') + output_decompress('DATA') + output('MEMO = (') + compressed_output(m) + output(')') + output_decompress('MEMO') + else: + output('DATA = (') + output(data) + output(')') + output('MEMO = (') + output(m) + output(')') + + + output('Shift = 0') + output('Reduce = 1') + output("def Lark_StandAlone(**kwargs):") + output(" return Lark._load_from_dict(DATA, MEMO, **kwargs)") + + + + +def main(): + make_warnings_comments() + parser = ArgumentParser(prog="prog='python -m lark.tools.standalone'", description="Lark Stand-alone Generator Tool", + parents=[lalr_argparser], epilog='Look at the Lark documentation for more info on the options') + parser.add_argument('-c', '--compress', action='store_true', default=0, help="Enable compression") + if len(sys.argv) == 1: + parser.print_help(sys.stderr) + sys.exit(1) + ns = parser.parse_args() + + lark_inst, out = build_lalr(ns) + gen_standalone(lark_inst, out=out, compress=ns.compress) + + ns.out.close() + ns.grammar_file.close() + + +if __name__ == '__main__': + main() diff --git a/ccxt/static_dependencies/lark/tree.py b/ccxt/static_dependencies/lark/tree.py new file mode 100644 index 0000000..76f8738 --- /dev/null +++ b/ccxt/static_dependencies/lark/tree.py @@ -0,0 +1,267 @@ +import sys +from copy import deepcopy + +from typing import List, Callable, Iterator, Union, Optional, Generic, TypeVar, TYPE_CHECKING + +if TYPE_CHECKING: + from .lexer import TerminalDef, Token + try: + import rich + except ImportError: + pass + from typing import Literal + +###{standalone + +class Meta: + + empty: bool + line: int + column: int + start_pos: int + end_line: int + end_column: int + end_pos: int + orig_expansion: 'List[TerminalDef]' + match_tree: bool + + def __init__(self): + self.empty = True + + +_Leaf_T = TypeVar("_Leaf_T") +Branch = Union[_Leaf_T, 'Tree[_Leaf_T]'] + + +class Tree(Generic[_Leaf_T]): + """The main tree class. + + Creates a new tree, and stores "data" and "children" in attributes of the same name. + Trees can be hashed and compared. + + Parameters: + data: The name of the rule or alias + children: List of matched sub-rules and terminals + meta: Line & Column numbers (if ``propagate_positions`` is enabled). + meta attributes: (line, column, end_line, end_column, start_pos, end_pos, + container_line, container_column, container_end_line, container_end_column) + container_* attributes consider all symbols, including those that have been inlined in the tree. + For example, in the rule 'a: _A B _C', the regular attributes will mark the start and end of B, + but the container_* attributes will also include _A and _C in the range. However, rules that + contain 'a' will consider it in full, including _A and _C for all attributes. + """ + + data: str + children: 'List[Branch[_Leaf_T]]' + + def __init__(self, data: str, children: 'List[Branch[_Leaf_T]]', meta: Optional[Meta]=None) -> None: + self.data = data + self.children = children + self._meta = meta + + @property + def meta(self) -> Meta: + if self._meta is None: + self._meta = Meta() + return self._meta + + def __repr__(self): + return 'Tree(%r, %r)' % (self.data, self.children) + + def _pretty_label(self): + return self.data + + def _pretty(self, level, indent_str): + yield f'{indent_str*level}{self._pretty_label()}' + if len(self.children) == 1 and not isinstance(self.children[0], Tree): + yield f'\t{self.children[0]}\n' + else: + yield '\n' + for n in self.children: + if isinstance(n, Tree): + yield from n._pretty(level+1, indent_str) + else: + yield f'{indent_str*(level+1)}{n}\n' + + def pretty(self, indent_str: str=' ') -> str: + """Returns an indented string representation of the tree. + + Great for debugging. + """ + return ''.join(self._pretty(0, indent_str)) + + def __rich__(self, parent:Optional['rich.tree.Tree']=None) -> 'rich.tree.Tree': + """Returns a tree widget for the 'rich' library. + + Example: + :: + from rich import print + from lark import Tree + + tree = Tree('root', ['node1', 'node2']) + print(tree) + """ + return self._rich(parent) + + def _rich(self, parent): + if parent: + tree = parent.add(f'[bold]{self.data}[/bold]') + else: + import rich.tree + tree = rich.tree.Tree(self.data) + + for c in self.children: + if isinstance(c, Tree): + c._rich(tree) + else: + tree.add(f'[green]{c}[/green]') + + return tree + + def __eq__(self, other): + try: + return self.data == other.data and self.children == other.children + except AttributeError: + return False + + def __ne__(self, other): + return not (self == other) + + def __hash__(self) -> int: + return hash((self.data, tuple(self.children))) + + def iter_subtrees(self) -> 'Iterator[Tree[_Leaf_T]]': + """Depth-first iteration. + + Iterates over all the subtrees, never returning to the same node twice (Lark's parse-tree is actually a DAG). + """ + queue = [self] + subtrees = dict() + for subtree in queue: + subtrees[id(subtree)] = subtree + queue += [c for c in reversed(subtree.children) + if isinstance(c, Tree) and id(c) not in subtrees] + + del queue + return reversed(list(subtrees.values())) + + def iter_subtrees_topdown(self): + """Breadth-first iteration. + + Iterates over all the subtrees, return nodes in order like pretty() does. + """ + stack = [self] + stack_append = stack.append + stack_pop = stack.pop + while stack: + node = stack_pop() + if not isinstance(node, Tree): + continue + yield node + for child in reversed(node.children): + stack_append(child) + + def find_pred(self, pred: 'Callable[[Tree[_Leaf_T]], bool]') -> 'Iterator[Tree[_Leaf_T]]': + """Returns all nodes of the tree that evaluate pred(node) as true.""" + return filter(pred, self.iter_subtrees()) + + def find_data(self, data: str) -> 'Iterator[Tree[_Leaf_T]]': + """Returns all nodes of the tree whose data equals the given data.""" + return self.find_pred(lambda t: t.data == data) + +###} + + def expand_kids_by_data(self, *data_values): + """Expand (inline) children with any of the given data values. Returns True if anything changed""" + changed = False + for i in range(len(self.children)-1, -1, -1): + child = self.children[i] + if isinstance(child, Tree) and child.data in data_values: + self.children[i:i+1] = child.children + changed = True + return changed + + + def scan_values(self, pred: 'Callable[[Branch[_Leaf_T]], bool]') -> Iterator[_Leaf_T]: + """Return all values in the tree that evaluate pred(value) as true. + + This can be used to find all the tokens in the tree. + + Example: + >>> all_tokens = tree.scan_values(lambda v: isinstance(v, Token)) + """ + for c in self.children: + if isinstance(c, Tree): + for t in c.scan_values(pred): + yield t + else: + if pred(c): + yield c + + def __deepcopy__(self, memo): + return type(self)(self.data, deepcopy(self.children, memo), meta=self._meta) + + def copy(self) -> 'Tree[_Leaf_T]': + return type(self)(self.data, self.children) + + def set(self, data: str, children: 'List[Branch[_Leaf_T]]') -> None: + self.data = data + self.children = children + + +ParseTree = Tree['Token'] + + +class SlottedTree(Tree): + __slots__ = 'data', 'children', 'rule', '_meta' + + +def pydot__tree_to_png(tree: Tree, filename: str, rankdir: 'Literal["TB", "LR", "BT", "RL"]'="LR", **kwargs) -> None: + graph = pydot__tree_to_graph(tree, rankdir, **kwargs) + graph.write_png(filename) + + +def pydot__tree_to_dot(tree: Tree, filename, rankdir="LR", **kwargs): + graph = pydot__tree_to_graph(tree, rankdir, **kwargs) + graph.write(filename) + + +def pydot__tree_to_graph(tree: Tree, rankdir="LR", **kwargs): + """Creates a colorful image that represents the tree (data+children, without meta) + + Possible values for `rankdir` are "TB", "LR", "BT", "RL", corresponding to + directed graphs drawn from top to bottom, from left to right, from bottom to + top, and from right to left, respectively. + + `kwargs` can be any graph attribute (e. g. `dpi=200`). For a list of + possible attributes, see https://www.graphviz.org/doc/info/attrs.html. + """ + + import pydot # type: ignore[import-not-found] + graph = pydot.Dot(graph_type='digraph', rankdir=rankdir, **kwargs) + + i = [0] + + def new_leaf(leaf): + node = pydot.Node(i[0], label=repr(leaf)) + i[0] += 1 + graph.add_node(node) + return node + + def _to_pydot(subtree): + color = hash(subtree.data) & 0xffffff + color |= 0x808080 + + subnodes = [_to_pydot(child) if isinstance(child, Tree) else new_leaf(child) + for child in subtree.children] + node = pydot.Node(i[0], style="filled", fillcolor="#%x" % color, label=subtree.data) + i[0] += 1 + graph.add_node(node) + + for subnode in subnodes: + graph.add_edge(pydot.Edge(node, subnode)) + + return node + + _to_pydot(tree) + return graph diff --git a/ccxt/static_dependencies/lark/tree_matcher.py b/ccxt/static_dependencies/lark/tree_matcher.py new file mode 100644 index 0000000..0f42652 --- /dev/null +++ b/ccxt/static_dependencies/lark/tree_matcher.py @@ -0,0 +1,186 @@ +"""Tree matcher based on Lark grammar""" + +import re +from collections import defaultdict + +from . import Tree, Token +from .common import ParserConf +from .parsers import earley +from .grammar import Rule, Terminal, NonTerminal + + +def is_discarded_terminal(t): + return t.is_term and t.filter_out + + +class _MakeTreeMatch: + def __init__(self, name, expansion): + self.name = name + self.expansion = expansion + + def __call__(self, args): + t = Tree(self.name, args) + t.meta.match_tree = True + t.meta.orig_expansion = self.expansion + return t + + +def _best_from_group(seq, group_key, cmp_key): + d = {} + for item in seq: + key = group_key(item) + if key in d: + v1 = cmp_key(item) + v2 = cmp_key(d[key]) + if v2 > v1: + d[key] = item + else: + d[key] = item + return list(d.values()) + + +def _best_rules_from_group(rules): + rules = _best_from_group(rules, lambda r: r, lambda r: -len(r.expansion)) + rules.sort(key=lambda r: len(r.expansion)) + return rules + + +def _match(term, token): + if isinstance(token, Tree): + name, _args = parse_rulename(term.name) + return token.data == name + elif isinstance(token, Token): + return term == Terminal(token.type) + assert False, (term, token) + + +def make_recons_rule(origin, expansion, old_expansion): + return Rule(origin, expansion, alias=_MakeTreeMatch(origin.name, old_expansion)) + + +def make_recons_rule_to_term(origin, term): + return make_recons_rule(origin, [Terminal(term.name)], [term]) + + +def parse_rulename(s): + "Parse rule names that may contain a template syntax (like rule{a, b, ...})" + name, args_str = re.match(r'(\w+)(?:{(.+)})?', s).groups() + args = args_str and [a.strip() for a in args_str.split(',')] + return name, args + + + +class ChildrenLexer: + def __init__(self, children): + self.children = children + + def lex(self, parser_state): + return self.children + +class TreeMatcher: + """Match the elements of a tree node, based on an ontology + provided by a Lark grammar. + + Supports templates and inlined rules (`rule{a, b,..}` and `_rule`) + + Initialize with an instance of Lark. + """ + + def __init__(self, parser): + # XXX TODO calling compile twice returns different results! + assert not parser.options.maybe_placeholders + # XXX TODO: we just ignore the potential existence of a postlexer + self.tokens, rules, _extra = parser.grammar.compile(parser.options.start, set()) + + self.rules_for_root = defaultdict(list) + + self.rules = list(self._build_recons_rules(rules)) + self.rules.reverse() + + # Choose the best rule from each group of {rule => [rule.alias]}, since we only really need one derivation. + self.rules = _best_rules_from_group(self.rules) + + self.parser = parser + self._parser_cache = {} + + def _build_recons_rules(self, rules): + "Convert tree-parsing/construction rules to tree-matching rules" + expand1s = {r.origin for r in rules if r.options.expand1} + + aliases = defaultdict(list) + for r in rules: + if r.alias: + aliases[r.origin].append(r.alias) + + rule_names = {r.origin for r in rules} + nonterminals = {sym for sym in rule_names + if sym.name.startswith('_') or sym in expand1s or sym in aliases} + + seen = set() + for r in rules: + recons_exp = [sym if sym in nonterminals else Terminal(sym.name) + for sym in r.expansion if not is_discarded_terminal(sym)] + + # Skip self-recursive constructs + if recons_exp == [r.origin] and r.alias is None: + continue + + sym = NonTerminal(r.alias) if r.alias else r.origin + rule = make_recons_rule(sym, recons_exp, r.expansion) + + if sym in expand1s and len(recons_exp) != 1: + self.rules_for_root[sym.name].append(rule) + + if sym.name not in seen: + yield make_recons_rule_to_term(sym, sym) + seen.add(sym.name) + else: + if sym.name.startswith('_') or sym in expand1s: + yield rule + else: + self.rules_for_root[sym.name].append(rule) + + for origin, rule_aliases in aliases.items(): + for alias in rule_aliases: + yield make_recons_rule_to_term(origin, NonTerminal(alias)) + yield make_recons_rule_to_term(origin, origin) + + def match_tree(self, tree, rulename): + """Match the elements of `tree` to the symbols of rule `rulename`. + + Parameters: + tree (Tree): the tree node to match + rulename (str): The expected full rule name (including template args) + + Returns: + Tree: an unreduced tree that matches `rulename` + + Raises: + UnexpectedToken: If no match was found. + + Note: + It's the callers' responsibility match the tree recursively. + """ + if rulename: + # validate + name, _args = parse_rulename(rulename) + assert tree.data == name + else: + rulename = tree.data + + # TODO: ambiguity? + try: + parser = self._parser_cache[rulename] + except KeyError: + rules = self.rules + _best_rules_from_group(self.rules_for_root[rulename]) + + # TODO pass callbacks through dict, instead of alias? + callbacks = {rule: rule.alias for rule in rules} + conf = ParserConf(rules, callbacks, [rulename]) + parser = earley.Parser(self.parser.lexer_conf, conf, _match, resolve_ambiguity=True) + self._parser_cache[rulename] = parser + + # find a full derivation + unreduced_tree = parser.parse(ChildrenLexer(tree.children), rulename) + assert unreduced_tree.data == rulename + return unreduced_tree diff --git a/ccxt/static_dependencies/lark/tree_templates.py b/ccxt/static_dependencies/lark/tree_templates.py new file mode 100644 index 0000000..6ec7323 --- /dev/null +++ b/ccxt/static_dependencies/lark/tree_templates.py @@ -0,0 +1,180 @@ +"""This module defines utilities for matching and translation tree templates. + +A tree templates is a tree that contains nodes that are template variables. + +""" + +from typing import Union, Optional, Mapping, Dict, Tuple, Iterator + +from lark import Tree, Transformer +from lark.exceptions import MissingVariableError + +Branch = Union[Tree[str], str] +TreeOrCode = Union[Tree[str], str] +MatchResult = Dict[str, Tree] +_TEMPLATE_MARKER = '$' + + +class TemplateConf: + """Template Configuration + + Allows customization for different uses of Template + + parse() must return a Tree instance. + """ + + def __init__(self, parse=None): + self._parse = parse + + def test_var(self, var: Union[Tree[str], str]) -> Optional[str]: + """Given a tree node, if it is a template variable return its name. Otherwise, return None. + + This method may be overridden for customization + + Parameters: + var: Tree | str - The tree node to test + + """ + if isinstance(var, str): + return _get_template_name(var) + + if ( + isinstance(var, Tree) + and var.data == "var" + and len(var.children) > 0 + and isinstance(var.children[0], str) + ): + return _get_template_name(var.children[0]) + + return None + + def _get_tree(self, template: TreeOrCode) -> Tree[str]: + if isinstance(template, str): + assert self._parse + template = self._parse(template) + + if not isinstance(template, Tree): + raise TypeError("template parser must return a Tree instance") + + return template + + def __call__(self, template: Tree[str]) -> 'Template': + return Template(template, conf=self) + + def _match_tree_template(self, template: TreeOrCode, tree: Branch) -> Optional[MatchResult]: + """Returns dict of {var: match} if found a match, else None + """ + template_var = self.test_var(template) + if template_var: + if not isinstance(tree, Tree): + raise TypeError(f"Template variables can only match Tree instances. Not {tree!r}") + return {template_var: tree} + + if isinstance(template, str): + if template == tree: + return {} + return None + + assert isinstance(template, Tree) and isinstance(tree, Tree), f"template={template} tree={tree}" + + if template.data == tree.data and len(template.children) == len(tree.children): + res = {} + for t1, t2 in zip(template.children, tree.children): + matches = self._match_tree_template(t1, t2) + if matches is None: + return None + + res.update(matches) + + return res + + return None + + +class _ReplaceVars(Transformer[str, Tree[str]]): + def __init__(self, conf: TemplateConf, vars: Mapping[str, Tree[str]]) -> None: + super().__init__() + self._conf = conf + self._vars = vars + + def __default__(self, data, children, meta) -> Tree[str]: + tree = super().__default__(data, children, meta) + + var = self._conf.test_var(tree) + if var: + try: + return self._vars[var] + except KeyError: + raise MissingVariableError(f"No mapping for template variable ({var})") + return tree + + +class Template: + """Represents a tree template, tied to a specific configuration + + A tree template is a tree that contains nodes that are template variables. + Those variables will match any tree. + (future versions may support annotations on the variables, to allow more complex templates) + """ + + def __init__(self, tree: Tree[str], conf: TemplateConf = TemplateConf()): + self.conf = conf + self.tree = conf._get_tree(tree) + + def match(self, tree: TreeOrCode) -> Optional[MatchResult]: + """Match a tree template to a tree. + + A tree template without variables will only match ``tree`` if it is equal to the template. + + Parameters: + tree (Tree): The tree to match to the template + + Returns: + Optional[Dict[str, Tree]]: If match is found, returns a dictionary mapping + template variable names to their matching tree nodes. + If no match was found, returns None. + """ + tree = self.conf._get_tree(tree) + return self.conf._match_tree_template(self.tree, tree) + + def search(self, tree: TreeOrCode) -> Iterator[Tuple[Tree[str], MatchResult]]: + """Search for all occurrences of the tree template inside ``tree``. + """ + tree = self.conf._get_tree(tree) + for subtree in tree.iter_subtrees(): + res = self.match(subtree) + if res: + yield subtree, res + + def apply_vars(self, vars: Mapping[str, Tree[str]]) -> Tree[str]: + """Apply vars to the template tree + """ + return _ReplaceVars(self.conf, vars).transform(self.tree) + + +def translate(t1: Template, t2: Template, tree: TreeOrCode): + """Search tree and translate each occurrence of t1 into t2. + """ + tree = t1.conf._get_tree(tree) # ensure it's a tree, parse if necessary and possible + for subtree, vars in t1.search(tree): + res = t2.apply_vars(vars) + subtree.set(res.data, res.children) + return tree + + +class TemplateTranslator: + """Utility class for translating a collection of patterns + """ + + def __init__(self, translations: Mapping[Template, Template]): + assert all(isinstance(k, Template) and isinstance(v, Template) for k, v in translations.items()) + self.translations = translations + + def translate(self, tree: Tree[str]): + for k, v in self.translations.items(): + tree = translate(k, v, tree) + return tree + + +def _get_template_name(value: str) -> Optional[str]: + return value.lstrip(_TEMPLATE_MARKER) if value.startswith(_TEMPLATE_MARKER) else None diff --git a/ccxt/static_dependencies/lark/utils.py b/ccxt/static_dependencies/lark/utils.py new file mode 100644 index 0000000..04d6eae --- /dev/null +++ b/ccxt/static_dependencies/lark/utils.py @@ -0,0 +1,343 @@ +import unicodedata +import os +from itertools import product +from collections import deque +from typing import Callable, Iterator, List, Optional, Tuple, Type, TypeVar, Union, Dict, Any, Sequence, Iterable, AbstractSet + +###{standalone +import sys, re +import logging + +logger: logging.Logger = logging.getLogger("lark") +logger.addHandler(logging.StreamHandler()) +# Set to highest level, since we have some warnings amongst the code +# By default, we should not output any log messages +logger.setLevel(logging.CRITICAL) + + +NO_VALUE = object() + +T = TypeVar("T") + + +def classify(seq: Iterable, key: Optional[Callable] = None, value: Optional[Callable] = None) -> Dict: + d: Dict[Any, Any] = {} + for item in seq: + k = key(item) if (key is not None) else item + v = value(item) if (value is not None) else item + try: + d[k].append(v) + except KeyError: + d[k] = [v] + return d + + +def _deserialize(data: Any, namespace: Dict[str, Any], memo: Dict) -> Any: + if isinstance(data, dict): + if '__type__' in data: # Object + class_ = namespace[data['__type__']] + return class_.deserialize(data, memo) + elif '@' in data: + return memo[data['@']] + return {key:_deserialize(value, namespace, memo) for key, value in data.items()} + elif isinstance(data, list): + return [_deserialize(value, namespace, memo) for value in data] + return data + + +_T = TypeVar("_T", bound="Serialize") + +class Serialize: + """Safe-ish serialization interface that doesn't rely on Pickle + + Attributes: + __serialize_fields__ (List[str]): Fields (aka attributes) to serialize. + __serialize_namespace__ (list): List of classes that deserialization is allowed to instantiate. + Should include all field types that aren't builtin types. + """ + + def memo_serialize(self, types_to_memoize: List) -> Any: + memo = SerializeMemoizer(types_to_memoize) + return self.serialize(memo), memo.serialize() + + def serialize(self, memo = None) -> Dict[str, Any]: + if memo and memo.in_types(self): + return {'@': memo.memoized.get(self)} + + fields = getattr(self, '__serialize_fields__') + res = {f: _serialize(getattr(self, f), memo) for f in fields} + res['__type__'] = type(self).__name__ + if hasattr(self, '_serialize'): + self._serialize(res, memo) + return res + + @classmethod + def deserialize(cls: Type[_T], data: Dict[str, Any], memo: Dict[int, Any]) -> _T: + namespace = getattr(cls, '__serialize_namespace__', []) + namespace = {c.__name__:c for c in namespace} + + fields = getattr(cls, '__serialize_fields__') + + if '@' in data: + return memo[data['@']] + + inst = cls.__new__(cls) + for f in fields: + try: + setattr(inst, f, _deserialize(data[f], namespace, memo)) + except KeyError as e: + raise KeyError("Cannot find key for class", cls, e) + + if hasattr(inst, '_deserialize'): + inst._deserialize() + + return inst + + +class SerializeMemoizer(Serialize): + "A version of serialize that memoizes objects to reduce space" + + __serialize_fields__ = 'memoized', + + def __init__(self, types_to_memoize: List) -> None: + self.types_to_memoize = tuple(types_to_memoize) + self.memoized = Enumerator() + + def in_types(self, value: Serialize) -> bool: + return isinstance(value, self.types_to_memoize) + + def serialize(self) -> Dict[int, Any]: # type: ignore[override] + return _serialize(self.memoized.reversed(), None) + + @classmethod + def deserialize(cls, data: Dict[int, Any], namespace: Dict[str, Any], memo: Dict[Any, Any]) -> Dict[int, Any]: # type: ignore[override] + return _deserialize(data, namespace, memo) + + +try: + import regex + _has_regex = True +except ImportError: + _has_regex = False + +if sys.version_info >= (3, 11): + import re._parser as sre_parse + import re._constants as sre_constants +else: + import sre_parse + import sre_constants + +categ_pattern = re.compile(r'\\p{[A-Za-z_]+}') + +def get_regexp_width(expr: str) -> Union[Tuple[int, int], List[int]]: + if _has_regex: + # Since `sre_parse` cannot deal with Unicode categories of the form `\p{Mn}`, we replace these with + # a simple letter, which makes no difference as we are only trying to get the possible lengths of the regex + # match here below. + regexp_final = re.sub(categ_pattern, 'A', expr) + else: + if re.search(categ_pattern, expr): + raise ImportError('`regex` module must be installed in order to use Unicode categories.', expr) + regexp_final = expr + try: + # Fixed in next version (past 0.960) of typeshed + return [int(x) for x in sre_parse.parse(regexp_final).getwidth()] + except sre_constants.error: + if not _has_regex: + raise ValueError(expr) + else: + # sre_parse does not support the new features in regex. To not completely fail in that case, + # we manually test for the most important info (whether the empty string is matched) + c = regex.compile(regexp_final) + # Python 3.11.7 introducded sre_parse.MAXWIDTH that is used instead of MAXREPEAT + # See lark-parser/lark#1376 and python/cpython#109859 + MAXWIDTH = getattr(sre_parse, "MAXWIDTH", sre_constants.MAXREPEAT) + if c.match('') is None: + # MAXREPEAT is a none pickable subclass of int, therefore needs to be converted to enable caching + return 1, int(MAXWIDTH) + else: + return 0, int(MAXWIDTH) + +###} + + +_ID_START = 'Lu', 'Ll', 'Lt', 'Lm', 'Lo', 'Mn', 'Mc', 'Pc' +_ID_CONTINUE = _ID_START + ('Nd', 'Nl',) + +def _test_unicode_category(s: str, categories: Sequence[str]) -> bool: + if len(s) != 1: + return all(_test_unicode_category(char, categories) for char in s) + return s == '_' or unicodedata.category(s) in categories + +def is_id_continue(s: str) -> bool: + """ + Checks if all characters in `s` are alphanumeric characters (Unicode standard, so diacritics, indian vowels, non-latin + numbers, etc. all pass). Synonymous with a Python `ID_CONTINUE` identifier. See PEP 3131 for details. + """ + return _test_unicode_category(s, _ID_CONTINUE) + +def is_id_start(s: str) -> bool: + """ + Checks if all characters in `s` are alphabetic characters (Unicode standard, so diacritics, indian vowels, non-latin + numbers, etc. all pass). Synonymous with a Python `ID_START` identifier. See PEP 3131 for details. + """ + return _test_unicode_category(s, _ID_START) + + +def dedup_list(l: Sequence[T]) -> List[T]: + """Given a list (l) will removing duplicates from the list, + preserving the original order of the list. Assumes that + the list entries are hashable.""" + return list(dict.fromkeys(l)) + + +class Enumerator(Serialize): + def __init__(self) -> None: + self.enums: Dict[Any, int] = {} + + def get(self, item) -> int: + if item not in self.enums: + self.enums[item] = len(self.enums) + return self.enums[item] + + def __len__(self): + return len(self.enums) + + def reversed(self) -> Dict[int, Any]: + r = {v: k for k, v in self.enums.items()} + assert len(r) == len(self.enums) + return r + + + +def combine_alternatives(lists): + """ + Accepts a list of alternatives, and enumerates all their possible concatenations. + + Examples: + >>> combine_alternatives([range(2), [4,5]]) + [[0, 4], [0, 5], [1, 4], [1, 5]] + + >>> combine_alternatives(["abc", "xy", '$']) + [['a', 'x', '$'], ['a', 'y', '$'], ['b', 'x', '$'], ['b', 'y', '$'], ['c', 'x', '$'], ['c', 'y', '$']] + + >>> combine_alternatives([]) + [[]] + """ + if not lists: + return [[]] + assert all(l for l in lists), lists + return list(product(*lists)) + +try: + import atomicwrites + _has_atomicwrites = True +except ImportError: + _has_atomicwrites = False + +class FS: + exists = staticmethod(os.path.exists) + + @staticmethod + def open(name, mode="r", **kwargs): + if _has_atomicwrites and "w" in mode: + return atomicwrites.atomic_write(name, mode=mode, overwrite=True, **kwargs) + else: + return open(name, mode, **kwargs) + + +class fzset(frozenset): + def __repr__(self): + return '{%s}' % ', '.join(map(repr, self)) + + +def classify_bool(seq: Iterable, pred: Callable) -> Any: + false_elems = [] + true_elems = [elem for elem in seq if pred(elem) or false_elems.append(elem)] # type: ignore[func-returns-value] + return true_elems, false_elems + + +def bfs(initial: Iterable, expand: Callable) -> Iterator: + open_q = deque(list(initial)) + visited = set(open_q) + while open_q: + node = open_q.popleft() + yield node + for next_node in expand(node): + if next_node not in visited: + visited.add(next_node) + open_q.append(next_node) + +def bfs_all_unique(initial, expand): + "bfs, but doesn't keep track of visited (aka seen), because there can be no repetitions" + open_q = deque(list(initial)) + while open_q: + node = open_q.popleft() + yield node + open_q += expand(node) + + +def _serialize(value: Any, memo: Optional[SerializeMemoizer]) -> Any: + if isinstance(value, Serialize): + return value.serialize(memo) + elif isinstance(value, list): + return [_serialize(elem, memo) for elem in value] + elif isinstance(value, frozenset): + return list(value) # TODO reversible? + elif isinstance(value, dict): + return {key:_serialize(elem, memo) for key, elem in value.items()} + # assert value is None or isinstance(value, (int, float, str, tuple)), value + return value + + + + +def small_factors(n: int, max_factor: int) -> List[Tuple[int, int]]: + """ + Splits n up into smaller factors and summands <= max_factor. + Returns a list of [(a, b), ...] + so that the following code returns n: + + n = 1 + for a, b in values: + n = n * a + b + + Currently, we also keep a + b <= max_factor, but that might change + """ + assert n >= 0 + assert max_factor > 2 + if n <= max_factor: + return [(n, 0)] + + for a in range(max_factor, 1, -1): + r, b = divmod(n, a) + if a + b <= max_factor: + return small_factors(r, max_factor) + [(a, b)] + assert False, "Failed to factorize %s" % n + + +class OrderedSet(AbstractSet[T]): + """A minimal OrderedSet implementation, using a dictionary. + + (relies on the dictionary being ordered) + """ + def __init__(self, items: Iterable[T] =()): + self.d = dict.fromkeys(items) + + def __contains__(self, item: Any) -> bool: + return item in self.d + + def add(self, item: T): + self.d[item] = None + + def __iter__(self) -> Iterator[T]: + return iter(self.d) + + def remove(self, item: T): + del self.d[item] + + def __bool__(self): + return bool(self.d) + + def __len__(self) -> int: + return len(self.d) diff --git a/ccxt/static_dependencies/lark/visitors.py b/ccxt/static_dependencies/lark/visitors.py new file mode 100644 index 0000000..18455d9 --- /dev/null +++ b/ccxt/static_dependencies/lark/visitors.py @@ -0,0 +1,596 @@ +from typing import TypeVar, Tuple, List, Callable, Generic, Type, Union, Optional, Any, cast +from abc import ABC + +from .utils import combine_alternatives +from .tree import Tree, Branch +from .exceptions import VisitError, GrammarError +from .lexer import Token + +###{standalone +from functools import wraps, update_wrapper +from inspect import getmembers, getmro + +_Return_T = TypeVar('_Return_T') +_Return_V = TypeVar('_Return_V') +_Leaf_T = TypeVar('_Leaf_T') +_Leaf_U = TypeVar('_Leaf_U') +_R = TypeVar('_R') +_FUNC = Callable[..., _Return_T] +_DECORATED = Union[_FUNC, type] + +class _DiscardType: + """When the Discard value is returned from a transformer callback, + that node is discarded and won't appear in the parent. + + Note: + This feature is disabled when the transformer is provided to Lark + using the ``transformer`` keyword (aka Tree-less LALR mode). + + Example: + :: + + class T(Transformer): + def ignore_tree(self, children): + return Discard + + def IGNORE_TOKEN(self, token): + return Discard + """ + + def __repr__(self): + return "lark.visitors.Discard" + +Discard = _DiscardType() + +# Transformers + +class _Decoratable: + "Provides support for decorating methods with @v_args" + + @classmethod + def _apply_v_args(cls, visit_wrapper): + mro = getmro(cls) + assert mro[0] is cls + libmembers = {name for _cls in mro[1:] for name, _ in getmembers(_cls)} + for name, value in getmembers(cls): + + # Make sure the function isn't inherited (unless it's overwritten) + if name.startswith('_') or (name in libmembers and name not in cls.__dict__): + continue + if not callable(value): + continue + + # Skip if v_args already applied (at the function level) + if isinstance(cls.__dict__[name], _VArgsWrapper): + continue + + setattr(cls, name, _VArgsWrapper(cls.__dict__[name], visit_wrapper)) + return cls + + def __class_getitem__(cls, _): + return cls + + +class Transformer(_Decoratable, ABC, Generic[_Leaf_T, _Return_T]): + """Transformers work bottom-up (or depth-first), starting with visiting the leaves and working + their way up until ending at the root of the tree. + + For each node visited, the transformer will call the appropriate method (callbacks), according to the + node's ``data``, and use the returned value to replace the node, thereby creating a new tree structure. + + Transformers can be used to implement map & reduce patterns. Because nodes are reduced from leaf to root, + at any point the callbacks may assume the children have already been transformed (if applicable). + + If the transformer cannot find a method with the right name, it will instead call ``__default__``, which by + default creates a copy of the node. + + To discard a node, return Discard (``lark.visitors.Discard``). + + ``Transformer`` can do anything ``Visitor`` can do, but because it reconstructs the tree, + it is slightly less efficient. + + A transformer without methods essentially performs a non-memoized partial deepcopy. + + All these classes implement the transformer interface: + + - ``Transformer`` - Recursively transforms the tree. This is the one you probably want. + - ``Transformer_InPlace`` - Non-recursive. Changes the tree in-place instead of returning new instances + - ``Transformer_InPlaceRecursive`` - Recursive. Changes the tree in-place instead of returning new instances + + Parameters: + visit_tokens (bool, optional): Should the transformer visit tokens in addition to rules. + Setting this to ``False`` is slightly faster. Defaults to ``True``. + (For processing ignored tokens, use the ``lexer_callbacks`` options) + + """ + __visit_tokens__ = True # For backwards compatibility + + def __init__(self, visit_tokens: bool=True) -> None: + self.__visit_tokens__ = visit_tokens + + def _call_userfunc(self, tree, new_children=None): + # Assumes tree is already transformed + children = new_children if new_children is not None else tree.children + try: + f = getattr(self, tree.data) + except AttributeError: + return self.__default__(tree.data, children, tree.meta) + else: + try: + wrapper = getattr(f, 'visit_wrapper', None) + if wrapper is not None: + return f.visit_wrapper(f, tree.data, children, tree.meta) + else: + return f(children) + except GrammarError: + raise + except Exception as e: + raise VisitError(tree.data, tree, e) + + def _call_userfunc_token(self, token): + try: + f = getattr(self, token.type) + except AttributeError: + return self.__default_token__(token) + else: + try: + return f(token) + except GrammarError: + raise + except Exception as e: + raise VisitError(token.type, token, e) + + def _transform_children(self, children): + for c in children: + if isinstance(c, Tree): + res = self._transform_tree(c) + elif self.__visit_tokens__ and isinstance(c, Token): + res = self._call_userfunc_token(c) + else: + res = c + + if res is not Discard: + yield res + + def _transform_tree(self, tree): + children = list(self._transform_children(tree.children)) + return self._call_userfunc(tree, children) + + def transform(self, tree: Tree[_Leaf_T]) -> _Return_T: + "Transform the given tree, and return the final result" + res = list(self._transform_children([tree])) + if not res: + return None # type: ignore[return-value] + assert len(res) == 1 + return res[0] + + def __mul__( + self: 'Transformer[_Leaf_T, Tree[_Leaf_U]]', + other: 'Union[Transformer[_Leaf_U, _Return_V], TransformerChain[_Leaf_U, _Return_V,]]' + ) -> 'TransformerChain[_Leaf_T, _Return_V]': + """Chain two transformers together, returning a new transformer. + """ + return TransformerChain(self, other) + + def __default__(self, data, children, meta): + """Default function that is called if there is no attribute matching ``data`` + + Can be overridden. Defaults to creating a new copy of the tree node (i.e. ``return Tree(data, children, meta)``) + """ + return Tree(data, children, meta) + + def __default_token__(self, token): + """Default function that is called if there is no attribute matching ``token.type`` + + Can be overridden. Defaults to returning the token as-is. + """ + return token + + +def merge_transformers(base_transformer=None, **transformers_to_merge): + """Merge a collection of transformers into the base_transformer, each into its own 'namespace'. + + When called, it will collect the methods from each transformer, and assign them to base_transformer, + with their name prefixed with the given keyword, as ``prefix__methodname``. + + This function is especially useful for processing grammars that import other grammars, + thereby creating some of their rules in a 'namespace'. (i.e with a consistent name prefix). + In this case, the key for the transformer should match the name of the imported grammar. + + Parameters: + base_transformer (Transformer, optional): The transformer that all other transformers will be added to. + **transformers_to_merge: Keyword arguments, in the form of ``name_prefix = transformer``. + + Raises: + AttributeError: In case of a name collision in the merged methods + + Example: + :: + + class TBase(Transformer): + def start(self, children): + return children[0] + 'bar' + + class TImportedGrammar(Transformer): + def foo(self, children): + return "foo" + + composed_transformer = merge_transformers(TBase(), imported=TImportedGrammar()) + + t = Tree('start', [ Tree('imported__foo', []) ]) + + assert composed_transformer.transform(t) == 'foobar' + + """ + if base_transformer is None: + base_transformer = Transformer() + for prefix, transformer in transformers_to_merge.items(): + for method_name in dir(transformer): + method = getattr(transformer, method_name) + if not callable(method): + continue + if method_name.startswith("_") or method_name == "transform": + continue + prefixed_method = prefix + "__" + method_name + if hasattr(base_transformer, prefixed_method): + raise AttributeError("Cannot merge: method '%s' appears more than once" % prefixed_method) + + setattr(base_transformer, prefixed_method, method) + + return base_transformer + + +class InlineTransformer(Transformer): # XXX Deprecated + def _call_userfunc(self, tree, new_children=None): + # Assumes tree is already transformed + children = new_children if new_children is not None else tree.children + try: + f = getattr(self, tree.data) + except AttributeError: + return self.__default__(tree.data, children, tree.meta) + else: + return f(*children) + + +class TransformerChain(Generic[_Leaf_T, _Return_T]): + + transformers: 'Tuple[Union[Transformer, TransformerChain], ...]' + + def __init__(self, *transformers: 'Union[Transformer, TransformerChain]') -> None: + self.transformers = transformers + + def transform(self, tree: Tree[_Leaf_T]) -> _Return_T: + for t in self.transformers: + tree = t.transform(tree) + return cast(_Return_T, tree) + + def __mul__( + self: 'TransformerChain[_Leaf_T, Tree[_Leaf_U]]', + other: 'Union[Transformer[_Leaf_U, _Return_V], TransformerChain[_Leaf_U, _Return_V]]' + ) -> 'TransformerChain[_Leaf_T, _Return_V]': + return TransformerChain(*self.transformers + (other,)) + + +class Transformer_InPlace(Transformer[_Leaf_T, _Return_T]): + """Same as Transformer, but non-recursive, and changes the tree in-place instead of returning new instances + + Useful for huge trees. Conservative in memory. + """ + def _transform_tree(self, tree): # Cancel recursion + return self._call_userfunc(tree) + + def transform(self, tree: Tree[_Leaf_T]) -> _Return_T: + for subtree in tree.iter_subtrees(): + subtree.children = list(self._transform_children(subtree.children)) + + return self._transform_tree(tree) + + +class Transformer_NonRecursive(Transformer[_Leaf_T, _Return_T]): + """Same as Transformer but non-recursive. + + Like Transformer, it doesn't change the original tree. + + Useful for huge trees. + """ + + def transform(self, tree: Tree[_Leaf_T]) -> _Return_T: + # Tree to postfix + rev_postfix = [] + q: List[Branch[_Leaf_T]] = [tree] + while q: + t = q.pop() + rev_postfix.append(t) + if isinstance(t, Tree): + q += t.children + + # Postfix to tree + stack: List = [] + for x in reversed(rev_postfix): + if isinstance(x, Tree): + size = len(x.children) + if size: + args = stack[-size:] + del stack[-size:] + else: + args = [] + + res = self._call_userfunc(x, args) + if res is not Discard: + stack.append(res) + + elif self.__visit_tokens__ and isinstance(x, Token): + res = self._call_userfunc_token(x) + if res is not Discard: + stack.append(res) + else: + stack.append(x) + + result, = stack # We should have only one tree remaining + # There are no guarantees on the type of the value produced by calling a user func for a + # child will produce. This means type system can't statically know that the final result is + # _Return_T. As a result a cast is required. + return cast(_Return_T, result) + + +class Transformer_InPlaceRecursive(Transformer): + "Same as Transformer, recursive, but changes the tree in-place instead of returning new instances" + def _transform_tree(self, tree): + tree.children = list(self._transform_children(tree.children)) + return self._call_userfunc(tree) + + +# Visitors + +class VisitorBase: + def _call_userfunc(self, tree): + return getattr(self, tree.data, self.__default__)(tree) + + def __default__(self, tree): + """Default function that is called if there is no attribute matching ``tree.data`` + + Can be overridden. Defaults to doing nothing. + """ + return tree + + def __class_getitem__(cls, _): + return cls + + +class Visitor(VisitorBase, ABC, Generic[_Leaf_T]): + """Tree visitor, non-recursive (can handle huge trees). + + Visiting a node calls its methods (provided by the user via inheritance) according to ``tree.data`` + """ + + def visit(self, tree: Tree[_Leaf_T]) -> Tree[_Leaf_T]: + "Visits the tree, starting with the leaves and finally the root (bottom-up)" + for subtree in tree.iter_subtrees(): + self._call_userfunc(subtree) + return tree + + def visit_topdown(self, tree: Tree[_Leaf_T]) -> Tree[_Leaf_T]: + "Visit the tree, starting at the root, and ending at the leaves (top-down)" + for subtree in tree.iter_subtrees_topdown(): + self._call_userfunc(subtree) + return tree + + +class Visitor_Recursive(VisitorBase, Generic[_Leaf_T]): + """Bottom-up visitor, recursive. + + Visiting a node calls its methods (provided by the user via inheritance) according to ``tree.data`` + + Slightly faster than the non-recursive version. + """ + + def visit(self, tree: Tree[_Leaf_T]) -> Tree[_Leaf_T]: + "Visits the tree, starting with the leaves and finally the root (bottom-up)" + for child in tree.children: + if isinstance(child, Tree): + self.visit(child) + + self._call_userfunc(tree) + return tree + + def visit_topdown(self,tree: Tree[_Leaf_T]) -> Tree[_Leaf_T]: + "Visit the tree, starting at the root, and ending at the leaves (top-down)" + self._call_userfunc(tree) + + for child in tree.children: + if isinstance(child, Tree): + self.visit_topdown(child) + + return tree + + +class Interpreter(_Decoratable, ABC, Generic[_Leaf_T, _Return_T]): + """Interpreter walks the tree starting at the root. + + Visits the tree, starting with the root and finally the leaves (top-down) + + For each tree node, it calls its methods (provided by user via inheritance) according to ``tree.data``. + + Unlike ``Transformer`` and ``Visitor``, the Interpreter doesn't automatically visit its sub-branches. + The user has to explicitly call ``visit``, ``visit_children``, or use the ``@visit_children_decor``. + This allows the user to implement branching and loops. + """ + + def visit(self, tree: Tree[_Leaf_T]) -> _Return_T: + # There are no guarantees on the type of the value produced by calling a user func for a + # child will produce. So only annotate the public method and use an internal method when + # visiting child trees. + return self._visit_tree(tree) + + def _visit_tree(self, tree: Tree[_Leaf_T]): + f = getattr(self, tree.data) + wrapper = getattr(f, 'visit_wrapper', None) + if wrapper is not None: + return f.visit_wrapper(f, tree.data, tree.children, tree.meta) + else: + return f(tree) + + def visit_children(self, tree: Tree[_Leaf_T]) -> List: + return [self._visit_tree(child) if isinstance(child, Tree) else child + for child in tree.children] + + def __getattr__(self, name): + return self.__default__ + + def __default__(self, tree): + return self.visit_children(tree) + + +_InterMethod = Callable[[Type[Interpreter], _Return_T], _R] + +def visit_children_decor(func: _InterMethod) -> _InterMethod: + "See Interpreter" + @wraps(func) + def inner(cls, tree): + values = cls.visit_children(tree) + return func(cls, values) + return inner + +# Decorators + +def _apply_v_args(obj, visit_wrapper): + try: + _apply = obj._apply_v_args + except AttributeError: + return _VArgsWrapper(obj, visit_wrapper) + else: + return _apply(visit_wrapper) + + +class _VArgsWrapper: + """ + A wrapper around a Callable. It delegates `__call__` to the Callable. + If the Callable has a `__get__`, that is also delegate and the resulting function is wrapped. + Otherwise, we use the original function mirroring the behaviour without a __get__. + We also have the visit_wrapper attribute to be used by Transformers. + """ + base_func: Callable + + def __init__(self, func: Callable, visit_wrapper: Callable[[Callable, str, list, Any], Any]): + if isinstance(func, _VArgsWrapper): + func = func.base_func + self.base_func = func + self.visit_wrapper = visit_wrapper + update_wrapper(self, func) + + def __call__(self, *args, **kwargs): + return self.base_func(*args, **kwargs) + + def __get__(self, instance, owner=None): + try: + # Use the __get__ attribute of the type instead of the instance + # to fully mirror the behavior of getattr + g = type(self.base_func).__get__ + except AttributeError: + return self + else: + return _VArgsWrapper(g(self.base_func, instance, owner), self.visit_wrapper) + + def __set_name__(self, owner, name): + try: + f = type(self.base_func).__set_name__ + except AttributeError: + return + else: + f(self.base_func, owner, name) + + +def _vargs_inline(f, _data, children, _meta): + return f(*children) +def _vargs_meta_inline(f, _data, children, meta): + return f(meta, *children) +def _vargs_meta(f, _data, children, meta): + return f(meta, children) +def _vargs_tree(f, data, children, meta): + return f(Tree(data, children, meta)) + + +def v_args(inline: bool = False, meta: bool = False, tree: bool = False, wrapper: Optional[Callable] = None) -> Callable[[_DECORATED], _DECORATED]: + """A convenience decorator factory for modifying the behavior of user-supplied visitor methods. + + By default, callback methods of transformers/visitors accept one argument - a list of the node's children. + + ``v_args`` can modify this behavior. When used on a transformer/visitor class definition, + it applies to all the callback methods inside it. + + ``v_args`` can be applied to a single method, or to an entire class. When applied to both, + the options given to the method take precedence. + + Parameters: + inline (bool, optional): Children are provided as ``*args`` instead of a list argument (not recommended for very long lists). + meta (bool, optional): Provides two arguments: ``meta`` and ``children`` (instead of just the latter) + tree (bool, optional): Provides the entire tree as the argument, instead of the children. + wrapper (function, optional): Provide a function to decorate all methods. + + Example: + :: + + @v_args(inline=True) + class SolveArith(Transformer): + def add(self, left, right): + return left + right + + @v_args(meta=True) + def mul(self, meta, children): + logger.info(f'mul at line {meta.line}') + left, right = children + return left * right + + + class ReverseNotation(Transformer_InPlace): + @v_args(tree=True) + def tree_node(self, tree): + tree.children = tree.children[::-1] + """ + if tree and (meta or inline): + raise ValueError("Visitor functions cannot combine 'tree' with 'meta' or 'inline'.") + + func = None + if meta: + if inline: + func = _vargs_meta_inline + else: + func = _vargs_meta + elif inline: + func = _vargs_inline + elif tree: + func = _vargs_tree + + if wrapper is not None: + if func is not None: + raise ValueError("Cannot use 'wrapper' along with 'tree', 'meta' or 'inline'.") + func = wrapper + + def _visitor_args_dec(obj): + return _apply_v_args(obj, func) + return _visitor_args_dec + + +###} + + +# --- Visitor Utilities --- + +class CollapseAmbiguities(Transformer): + """ + Transforms a tree that contains any number of _ambig nodes into a list of trees, + each one containing an unambiguous tree. + + The length of the resulting list is the product of the length of all _ambig nodes. + + Warning: This may quickly explode for highly ambiguous trees. + + """ + def _ambig(self, options): + return sum(options, []) + + def __default__(self, data, children_lists, meta): + return [Tree(data, children, meta) for children in combine_alternatives(children_lists)] + + def __default_token__(self, t): + return [t] diff --git a/ccxt/static_dependencies/marshmallow/__init__.py b/ccxt/static_dependencies/marshmallow/__init__.py new file mode 100644 index 0000000..7f47476 --- /dev/null +++ b/ccxt/static_dependencies/marshmallow/__init__.py @@ -0,0 +1,81 @@ +from __future__ import annotations + +import importlib.metadata +import typing + +# from packaging.version import Version + +from .decorators import ( + post_dump, + post_load, + pre_dump, + pre_load, + validates, + validates_schema, +) +from .exceptions import ValidationError +from .schema import Schema, SchemaOpts +from .utils import EXCLUDE, INCLUDE, RAISE, missing, pprint + +from . import fields + + +def __getattr__(name: str) -> typing.Any: + import warnings + + if name == "__version__": + warnings.warn( + "The '__version__' attribute is deprecated and will be removed in" + " in a future version. Use feature detection or" + " 'importlib.metadata.version(\"marshmallow\")' instead.", + DeprecationWarning, + stacklevel=2, + ) + return importlib.metadata.version("marshmallow") + + # if name == "__parsed_version__": + # warnings.warn( + # "The '__parsed_version__' attribute is deprecated and will be removed in" + # " in a future version. Use feature detection or" + # " 'packaging.Version(importlib.metadata.version(\"marshmallow\"))' instead.", + # DeprecationWarning, + # stacklevel=2, + # ) + # return Version(importlib.metadata.version("marshmallow")) + + if name == "__version_info__": + warnings.warn( + "The '__version_info__' attribute is deprecated and will be removed in" + " in a future version. Use feature detection or" + " 'packaging.Version(importlib.metadata.version(\"marshmallow\")).release' instead.", + DeprecationWarning, + stacklevel=2, + ) + # __parsed_version__ = Version(importlib.metadata.version("marshmallow")) + __version_info__: tuple[int, int, int] | tuple[int, int, int, str, int] = ( + __parsed_version__.release # type: ignore[assignment] + ) + if __parsed_version__.pre: + __version_info__ += __parsed_version__.pre # type: ignore[assignment] + return __version_info__ + + raise AttributeError(name) + + +__all__ = [ + "EXCLUDE", + "INCLUDE", + "RAISE", + "Schema", + "SchemaOpts", + "fields", + "validates", + "validates_schema", + "pre_dump", + "post_dump", + "pre_load", + "post_load", + "pprint", + "ValidationError", + "missing", +] diff --git a/ccxt/static_dependencies/marshmallow/__pycache__/__init__.cpython-311.pyc b/ccxt/static_dependencies/marshmallow/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..3ab3eec Binary files /dev/null and b/ccxt/static_dependencies/marshmallow/__pycache__/__init__.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/marshmallow/__pycache__/base.cpython-311.pyc b/ccxt/static_dependencies/marshmallow/__pycache__/base.cpython-311.pyc new file mode 100644 index 0000000..5728d0d Binary files /dev/null and b/ccxt/static_dependencies/marshmallow/__pycache__/base.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/marshmallow/__pycache__/class_registry.cpython-311.pyc b/ccxt/static_dependencies/marshmallow/__pycache__/class_registry.cpython-311.pyc new file mode 100644 index 0000000..18b2c4d Binary files /dev/null and b/ccxt/static_dependencies/marshmallow/__pycache__/class_registry.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/marshmallow/__pycache__/decorators.cpython-311.pyc b/ccxt/static_dependencies/marshmallow/__pycache__/decorators.cpython-311.pyc new file mode 100644 index 0000000..113dcc3 Binary files /dev/null and b/ccxt/static_dependencies/marshmallow/__pycache__/decorators.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/marshmallow/__pycache__/error_store.cpython-311.pyc b/ccxt/static_dependencies/marshmallow/__pycache__/error_store.cpython-311.pyc new file mode 100644 index 0000000..57cf968 Binary files /dev/null and b/ccxt/static_dependencies/marshmallow/__pycache__/error_store.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/marshmallow/__pycache__/exceptions.cpython-311.pyc b/ccxt/static_dependencies/marshmallow/__pycache__/exceptions.cpython-311.pyc new file mode 100644 index 0000000..78b8a24 Binary files /dev/null and b/ccxt/static_dependencies/marshmallow/__pycache__/exceptions.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/marshmallow/__pycache__/fields.cpython-311.pyc b/ccxt/static_dependencies/marshmallow/__pycache__/fields.cpython-311.pyc new file mode 100644 index 0000000..7695d79 Binary files /dev/null and b/ccxt/static_dependencies/marshmallow/__pycache__/fields.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/marshmallow/__pycache__/orderedset.cpython-311.pyc b/ccxt/static_dependencies/marshmallow/__pycache__/orderedset.cpython-311.pyc new file mode 100644 index 0000000..92aa5c8 Binary files /dev/null and b/ccxt/static_dependencies/marshmallow/__pycache__/orderedset.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/marshmallow/__pycache__/schema.cpython-311.pyc b/ccxt/static_dependencies/marshmallow/__pycache__/schema.cpython-311.pyc new file mode 100644 index 0000000..125819d Binary files /dev/null and b/ccxt/static_dependencies/marshmallow/__pycache__/schema.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/marshmallow/__pycache__/types.cpython-311.pyc b/ccxt/static_dependencies/marshmallow/__pycache__/types.cpython-311.pyc new file mode 100644 index 0000000..3cf208b Binary files /dev/null and b/ccxt/static_dependencies/marshmallow/__pycache__/types.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/marshmallow/__pycache__/utils.cpython-311.pyc b/ccxt/static_dependencies/marshmallow/__pycache__/utils.cpython-311.pyc new file mode 100644 index 0000000..c8e8c4e Binary files /dev/null and b/ccxt/static_dependencies/marshmallow/__pycache__/utils.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/marshmallow/__pycache__/validate.cpython-311.pyc b/ccxt/static_dependencies/marshmallow/__pycache__/validate.cpython-311.pyc new file mode 100644 index 0000000..ecc85b9 Binary files /dev/null and b/ccxt/static_dependencies/marshmallow/__pycache__/validate.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/marshmallow/__pycache__/warnings.cpython-311.pyc b/ccxt/static_dependencies/marshmallow/__pycache__/warnings.cpython-311.pyc new file mode 100644 index 0000000..f19f693 Binary files /dev/null and b/ccxt/static_dependencies/marshmallow/__pycache__/warnings.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/marshmallow/base.py b/ccxt/static_dependencies/marshmallow/base.py new file mode 100644 index 0000000..e82848d --- /dev/null +++ b/ccxt/static_dependencies/marshmallow/base.py @@ -0,0 +1,65 @@ +"""Abstract base classes. + +These are necessary to avoid circular imports between schema.py and fields.py. + +.. warning:: + + This module is treated as private API. + Users should not need to use this module directly. +""" + +from __future__ import annotations + +from abc import ABC, abstractmethod + + +class FieldABC(ABC): + """Abstract base class from which all Field classes inherit.""" + + parent = None + name = None + root = None + + @abstractmethod + def serialize(self, attr, obj, accessor=None): + pass + + @abstractmethod + def deserialize(self, value): + pass + + @abstractmethod + def _serialize(self, value, attr, obj, **kwargs): + pass + + @abstractmethod + def _deserialize(self, value, attr, data, **kwargs): + pass + + +class SchemaABC(ABC): + """Abstract base class from which all Schemas inherit.""" + + @abstractmethod + def dump(self, obj, *, many: bool | None = None): + pass + + @abstractmethod + def dumps(self, obj, *, many: bool | None = None): + pass + + @abstractmethod + def load(self, data, *, many: bool | None = None, partial=None, unknown=None): + pass + + @abstractmethod + def loads( + self, + json_data, + *, + many: bool | None = None, + partial=None, + unknown=None, + **kwargs, + ): + pass diff --git a/ccxt/static_dependencies/marshmallow/class_registry.py b/ccxt/static_dependencies/marshmallow/class_registry.py new file mode 100644 index 0000000..ea9aca1 --- /dev/null +++ b/ccxt/static_dependencies/marshmallow/class_registry.py @@ -0,0 +1,94 @@ +"""A registry of :class:`Schema ` classes. This allows for string +lookup of schemas, which may be used with +class:`fields.Nested `. + +.. warning:: + + This module is treated as private API. + Users should not need to use this module directly. +""" + +from __future__ import annotations + +import typing + +from .exceptions import RegistryError + +if typing.TYPE_CHECKING: + from . import Schema + + SchemaType = typing.Type[Schema] + +# { +# : +# : +# } +_registry = {} # type: dict[str, list[SchemaType]] + + +def register(classname: str, cls: SchemaType) -> None: + """Add a class to the registry of serializer classes. When a class is + registered, an entry for both its classname and its full, module-qualified + path are added to the registry. + + Example: :: + + class MyClass: + pass + + + register("MyClass", MyClass) + # Registry: + # { + # 'MyClass': [path.to.MyClass], + # 'path.to.MyClass': [path.to.MyClass], + # } + + """ + # Module where the class is located + module = cls.__module__ + # Full module path to the class + # e.g. user.schemas.UserSchema + fullpath = ".".join([module, classname]) + # If the class is already registered; need to check if the entries are + # in the same module as cls to avoid having multiple instances of the same + # class in the registry + if classname in _registry and not any( + each.__module__ == module for each in _registry[classname] + ): + _registry[classname].append(cls) + elif classname not in _registry: + _registry[classname] = [cls] + + # Also register the full path + if fullpath not in _registry: + _registry.setdefault(fullpath, []).append(cls) + else: + # If fullpath does exist, replace existing entry + _registry[fullpath] = [cls] + return None + + +def get_class(classname: str, all: bool = False) -> list[SchemaType] | SchemaType: + """Retrieve a class from the registry. + + :raises: marshmallow.exceptions.RegistryError if the class cannot be found + or if there are multiple entries for the given class name. + """ + try: + classes = _registry[classname] + except KeyError as error: + raise RegistryError( + f"Class with name {classname!r} was not found. You may need " + "to import the class." + ) from error + if len(classes) > 1: + if all: + return _registry[classname] + raise RegistryError( + f"Multiple classes with name {classname!r} " + "were found. Please use the full, " + "module-qualified path." + ) + else: + return _registry[classname][0] diff --git a/ccxt/static_dependencies/marshmallow/decorators.py b/ccxt/static_dependencies/marshmallow/decorators.py new file mode 100644 index 0000000..06e8400 --- /dev/null +++ b/ccxt/static_dependencies/marshmallow/decorators.py @@ -0,0 +1,231 @@ +"""Decorators for registering schema pre-processing and post-processing methods. +These should be imported from the top-level `marshmallow` module. + +Methods decorated with +`pre_load `, `post_load `, +`pre_dump `, `post_dump `, +and `validates_schema ` receive +``many`` as a keyword argument. In addition, `pre_load `, +`post_load `, +and `validates_schema ` receive +``partial``. If you don't need these arguments, add ``**kwargs`` to your method +signature. + + +Example: :: + + from . import ( + Schema, + pre_load, + pre_dump, + post_load, + validates_schema, + validates, + fields, + ValidationError, + ) + + + class UserSchema(Schema): + email = fields.Str(required=True) + age = fields.Integer(required=True) + + @post_load + def lowerstrip_email(self, item, many, **kwargs): + item["email"] = item["email"].lower().strip() + return item + + @pre_load(pass_many=True) + def remove_envelope(self, data, many, **kwargs): + namespace = "results" if many else "result" + return data[namespace] + + @post_dump(pass_many=True) + def add_envelope(self, data, many, **kwargs): + namespace = "results" if many else "result" + return {namespace: data} + + @validates_schema + def validate_email(self, data, **kwargs): + if len(data["email"]) < 3: + raise ValidationError("Email must be more than 3 characters", "email") + + @validates("age") + def validate_age(self, data, **kwargs): + if data < 14: + raise ValidationError("Too young!") + +.. note:: + These decorators only work with instance methods. Class and static + methods are not supported. + +.. warning:: + The invocation order of decorated methods of the same type is not guaranteed. + If you need to guarantee order of different processing steps, you should put + them in the same processing method. +""" + +from __future__ import annotations + +import functools +from typing import Any, Callable, cast + +PRE_DUMP = "pre_dump" +POST_DUMP = "post_dump" +PRE_LOAD = "pre_load" +POST_LOAD = "post_load" +VALIDATES = "validates" +VALIDATES_SCHEMA = "validates_schema" + + +class MarshmallowHook: + __marshmallow_hook__: dict[tuple[str, bool] | str, Any] | None = None + + +def validates(field_name: str) -> Callable[..., Any]: + """Register a field validator. + + :param str field_name: Name of the field that the method validates. + """ + return set_hook(None, VALIDATES, field_name=field_name) + + +def validates_schema( + fn: Callable[..., Any] | None = None, + pass_many: bool = False, + pass_original: bool = False, + skip_on_field_errors: bool = True, +) -> Callable[..., Any]: + """Register a schema-level validator. + + By default it receives a single object at a time, transparently handling the ``many`` + argument passed to the `Schema`'s :func:`~marshmallow.Schema.validate` call. + If ``pass_many=True``, the raw data (which may be a collection) is passed. + + If ``pass_original=True``, the original data (before unmarshalling) will be passed as + an additional argument to the method. + + If ``skip_on_field_errors=True``, this validation method will be skipped whenever + validation errors have been detected when validating fields. + + .. versionchanged:: 3.0.0b1 + ``skip_on_field_errors`` defaults to `True`. + + .. versionchanged:: 3.0.0 + ``partial`` and ``many`` are always passed as keyword arguments to + the decorated method. + """ + return set_hook( + fn, + (VALIDATES_SCHEMA, pass_many), + pass_original=pass_original, + skip_on_field_errors=skip_on_field_errors, + ) + + +def pre_dump( + fn: Callable[..., Any] | None = None, pass_many: bool = False +) -> Callable[..., Any]: + """Register a method to invoke before serializing an object. The method + receives the object to be serialized and returns the processed object. + + By default it receives a single object at a time, transparently handling the ``many`` + argument passed to the `Schema`'s :func:`~marshmallow.Schema.dump` call. + If ``pass_many=True``, the raw data (which may be a collection) is passed. + + .. versionchanged:: 3.0.0 + ``many`` is always passed as a keyword arguments to the decorated method. + """ + return set_hook(fn, (PRE_DUMP, pass_many)) + + +def post_dump( + fn: Callable[..., Any] | None = None, + pass_many: bool = False, + pass_original: bool = False, +) -> Callable[..., Any]: + """Register a method to invoke after serializing an object. The method + receives the serialized object and returns the processed object. + + By default it receives a single object at a time, transparently handling the ``many`` + argument passed to the `Schema`'s :func:`~marshmallow.Schema.dump` call. + If ``pass_many=True``, the raw data (which may be a collection) is passed. + + If ``pass_original=True``, the original data (before serializing) will be passed as + an additional argument to the method. + + .. versionchanged:: 3.0.0 + ``many`` is always passed as a keyword arguments to the decorated method. + """ + return set_hook(fn, (POST_DUMP, pass_many), pass_original=pass_original) + + +def pre_load( + fn: Callable[..., Any] | None = None, pass_many: bool = False +) -> Callable[..., Any]: + """Register a method to invoke before deserializing an object. The method + receives the data to be deserialized and returns the processed data. + + By default it receives a single object at a time, transparently handling the ``many`` + argument passed to the `Schema`'s :func:`~marshmallow.Schema.load` call. + If ``pass_many=True``, the raw data (which may be a collection) is passed. + + .. versionchanged:: 3.0.0 + ``partial`` and ``many`` are always passed as keyword arguments to + the decorated method. + """ + return set_hook(fn, (PRE_LOAD, pass_many)) + + +def post_load( + fn: Callable[..., Any] | None = None, + pass_many: bool = False, + pass_original: bool = False, +) -> Callable[..., Any]: + """Register a method to invoke after deserializing an object. The method + receives the deserialized data and returns the processed data. + + By default it receives a single object at a time, transparently handling the ``many`` + argument passed to the `Schema`'s :func:`~marshmallow.Schema.load` call. + If ``pass_many=True``, the raw data (which may be a collection) is passed. + + If ``pass_original=True``, the original data (before deserializing) will be passed as + an additional argument to the method. + + .. versionchanged:: 3.0.0 + ``partial`` and ``many`` are always passed as keyword arguments to + the decorated method. + """ + return set_hook(fn, (POST_LOAD, pass_many), pass_original=pass_original) + + +def set_hook( + fn: Callable[..., Any] | None, key: tuple[str, bool] | str, **kwargs: Any +) -> Callable[..., Any]: + """Mark decorated function as a hook to be picked up later. + You should not need to use this method directly. + + .. note:: + Currently only works with functions and instance methods. Class and + static methods are not supported. + + :return: Decorated function if supplied, else this decorator with its args + bound. + """ + # Allow using this as either a decorator or a decorator factory. + if fn is None: + return functools.partial(set_hook, key=key, **kwargs) + + # Set a __marshmallow_hook__ attribute instead of wrapping in some class, + # because I still want this to end up as a normal (unbound) method. + function = cast(MarshmallowHook, fn) + try: + hook_config = function.__marshmallow_hook__ + except AttributeError: + function.__marshmallow_hook__ = hook_config = {} + # Also save the kwargs for the tagged function on + # __marshmallow_hook__, keyed by (, ) + if hook_config is not None: + hook_config[key] = kwargs + + return fn diff --git a/ccxt/static_dependencies/marshmallow/error_store.py b/ccxt/static_dependencies/marshmallow/error_store.py new file mode 100644 index 0000000..b3d01f2 --- /dev/null +++ b/ccxt/static_dependencies/marshmallow/error_store.py @@ -0,0 +1,60 @@ +"""Utilities for storing collections of error messages. + +.. warning:: + + This module is treated as private API. + Users should not need to use this module directly. +""" + +from .exceptions import SCHEMA + + +class ErrorStore: + def __init__(self): + #: Dictionary of errors stored during serialization + self.errors = {} + + def store_error(self, messages, field_name=SCHEMA, index=None): + # field error -> store/merge error messages under field name key + # schema error -> if string or list, store/merge under _schema key + # -> if dict, store/merge with other top-level keys + if field_name != SCHEMA or not isinstance(messages, dict): + messages = {field_name: messages} + if index is not None: + messages = {index: messages} + self.errors = merge_errors(self.errors, messages) + + +def merge_errors(errors1, errors2): + """Deeply merge two error messages. + + The format of ``errors1`` and ``errors2`` matches the ``message`` + parameter of :exc:`marshmallow.exceptions.ValidationError`. + """ + if not errors1: + return errors2 + if not errors2: + return errors1 + if isinstance(errors1, list): + if isinstance(errors2, list): + return errors1 + errors2 + if isinstance(errors2, dict): + return dict(errors2, **{SCHEMA: merge_errors(errors1, errors2.get(SCHEMA))}) + return errors1 + [errors2] + if isinstance(errors1, dict): + if isinstance(errors2, list): + return dict(errors1, **{SCHEMA: merge_errors(errors1.get(SCHEMA), errors2)}) + if isinstance(errors2, dict): + errors = dict(errors1) + for key, val in errors2.items(): + if key in errors: + errors[key] = merge_errors(errors[key], val) + else: + errors[key] = val + return errors + return dict(errors1, **{SCHEMA: merge_errors(errors1.get(SCHEMA), errors2)}) + if isinstance(errors2, list): + return [errors1] + errors2 + if isinstance(errors2, dict): + return dict(errors2, **{SCHEMA: merge_errors(errors1, errors2.get(SCHEMA))}) + return [errors1, errors2] diff --git a/ccxt/static_dependencies/marshmallow/exceptions.py b/ccxt/static_dependencies/marshmallow/exceptions.py new file mode 100644 index 0000000..096b6bd --- /dev/null +++ b/ccxt/static_dependencies/marshmallow/exceptions.py @@ -0,0 +1,71 @@ +"""Exception classes for marshmallow-related errors.""" + +from __future__ import annotations + +import typing + +# Key used for schema-level validation errors +SCHEMA = "_schema" + + +class MarshmallowError(Exception): + """Base class for all marshmallow-related errors.""" + + +class ValidationError(MarshmallowError): + """Raised when validation fails on a field or schema. + + Validators and custom fields should raise this exception. + + :param message: An error message, list of error messages, or dict of + error messages. If a dict, the keys are subitems and the values are error messages. + :param field_name: Field name to store the error on. + If `None`, the error is stored as schema-level error. + :param data: Raw input data. + :param valid_data: Valid (de)serialized data. + """ + + def __init__( + self, + message: str | list | dict, + field_name: str = SCHEMA, + data: typing.Mapping[str, typing.Any] + | typing.Iterable[typing.Mapping[str, typing.Any]] + | None = None, + valid_data: list[dict[str, typing.Any]] | dict[str, typing.Any] | None = None, + **kwargs, + ): + self.messages = [message] if isinstance(message, (str, bytes)) else message + self.field_name = field_name + self.data = data + self.valid_data = valid_data + self.kwargs = kwargs + super().__init__(message) + + def normalized_messages(self): + if self.field_name == SCHEMA and isinstance(self.messages, dict): + return self.messages + return {self.field_name: self.messages} + + @property + def messages_dict(self) -> dict[str, typing.Any]: + if not isinstance(self.messages, dict): + raise TypeError( + "cannot access 'messages_dict' when 'messages' is of type " + + type(self.messages).__name__ + ) + return self.messages + + +class RegistryError(NameError): + """Raised when an invalid operation is performed on the serializer + class registry. + """ + + +class StringNotCollectionError(MarshmallowError, TypeError): + """Raised when a string is passed when a list of strings is expected.""" + + +class FieldInstanceResolutionError(MarshmallowError, TypeError): + """Raised when schema to instantiate is neither a Schema class nor an instance.""" diff --git a/ccxt/static_dependencies/marshmallow/fields.py b/ccxt/static_dependencies/marshmallow/fields.py new file mode 100644 index 0000000..6ec50fd --- /dev/null +++ b/ccxt/static_dependencies/marshmallow/fields.py @@ -0,0 +1,2114 @@ +"""Field classes for various types of data.""" + +from __future__ import annotations + +import collections +import copy +import datetime as dt +import decimal +import ipaddress +import math +import numbers +import typing +import uuid +import warnings +from collections.abc import Mapping as _Mapping +from enum import Enum as EnumType + +from . import class_registry, types, utils, validate +from .base import FieldABC, SchemaABC +from .exceptions import ( + FieldInstanceResolutionError, + StringNotCollectionError, + ValidationError, +) +from .utils import ( + is_aware, + is_collection, + resolve_field_instance, +) +from .utils import ( + missing as missing_, +) +from .validate import And, Length +from .warnings import RemovedInMarshmallow4Warning + +__all__ = [ + "Field", + "Raw", + "Nested", + "Mapping", + "Dict", + "List", + "Tuple", + "String", + "UUID", + "Number", + "Integer", + "Decimal", + "Boolean", + "Float", + "DateTime", + "NaiveDateTime", + "AwareDateTime", + "Time", + "Date", + "TimeDelta", + "Url", + "URL", + "Email", + "IP", + "IPv4", + "IPv6", + "IPInterface", + "IPv4Interface", + "IPv6Interface", + "Enum", + "Method", + "Function", + "Str", + "Bool", + "Int", + "Constant", + "Pluck", +] + +_T = typing.TypeVar("_T") + + +class Field(FieldABC): + """Basic field from which other fields should extend. It applies no + formatting by default, and should only be used in cases where + data does not need to be formatted before being serialized or deserialized. + On error, the name of the field will be returned. + + :param dump_default: If set, this value will be used during serialization if the + input value is missing. If not set, the field will be excluded from the + serialized output if the input value is missing. May be a value or a callable. + :param load_default: Default deserialization value for the field if the field is not + found in the input data. May be a value or a callable. + :param data_key: The name of the dict key in the external representation, i.e. + the input of `load` and the output of `dump`. + If `None`, the key will match the name of the field. + :param attribute: The name of the key/attribute in the internal representation, i.e. + the output of `load` and the input of `dump`. + If `None`, the key/attribute will match the name of the field. + Note: This should only be used for very specific use cases such as + outputting multiple fields for a single attribute, or using keys/attributes + that are invalid variable names, unsuitable for field names. In most cases, + you should use ``data_key`` instead. + :param validate: Validator or collection of validators that are called + during deserialization. Validator takes a field's input value as + its only parameter and returns a boolean. + If it returns `False`, an :exc:`ValidationError` is raised. + :param required: Raise a :exc:`ValidationError` if the field value + is not supplied during deserialization. + :param allow_none: Set this to `True` if `None` should be considered a valid value during + validation/deserialization. If ``load_default=None`` and ``allow_none`` is unset, + will default to ``True``. Otherwise, the default is ``False``. + :param load_only: If `True` skip this field during serialization, otherwise + its value will be present in the serialized data. + :param dump_only: If `True` skip this field during deserialization, otherwise + its value will be present in the deserialized object. In the context of an + HTTP API, this effectively marks the field as "read-only". + :param dict error_messages: Overrides for `Field.default_error_messages`. + :param metadata: Extra information to be stored as field metadata. + + .. versionchanged:: 2.0.0 + Removed `error` parameter. Use ``error_messages`` instead. + + .. versionchanged:: 2.0.0 + Added `allow_none` parameter, which makes validation/deserialization of `None` + consistent across fields. + + .. versionchanged:: 2.0.0 + Added `load_only` and `dump_only` parameters, which allow field skipping + during the (de)serialization process. + + .. versionchanged:: 2.0.0 + Added `missing` parameter, which indicates the value for a field if the field + is not found during deserialization. + + .. versionchanged:: 2.0.0 + ``default`` value is only used if explicitly set. Otherwise, missing values + inputs are excluded from serialized output. + + .. versionchanged:: 3.0.0b8 + Add ``data_key`` parameter for the specifying the key in the input and + output data. This parameter replaced both ``load_from`` and ``dump_to``. + """ + + # Some fields, such as Method fields and Function fields, are not expected + # to exist as attributes on the objects to serialize. Set this to False + # for those fields + _CHECK_ATTRIBUTE = True + + #: Default error messages for various kinds of errors. The keys in this dictionary + #: are passed to `Field.make_error`. The values are error messages passed to + #: :exc:`marshmallow.exceptions.ValidationError`. + default_error_messages = { + "required": "Missing data for required field.", + "null": "Field may not be null.", + "validator_failed": "Invalid value.", + } + + def __init__( + self, + *, + load_default: typing.Any = missing_, + missing: typing.Any = missing_, + dump_default: typing.Any = missing_, + default: typing.Any = missing_, + data_key: str | None = None, + attribute: str | None = None, + validate: ( + None + | typing.Callable[[typing.Any], typing.Any] + | typing.Iterable[typing.Callable[[typing.Any], typing.Any]] + ) = None, + required: bool = False, + allow_none: bool | None = None, + load_only: bool = False, + dump_only: bool = False, + error_messages: dict[str, str] | None = None, + metadata: typing.Mapping[str, typing.Any] | None = None, + **additional_metadata, + ) -> None: + # handle deprecated `default` and `missing` parameters + if default is not missing_: + warnings.warn( + "The 'default' argument to fields is deprecated. " + "Use 'dump_default' instead.", + RemovedInMarshmallow4Warning, + stacklevel=2, + ) + if dump_default is missing_: + dump_default = default + if missing is not missing_: + warnings.warn( + "The 'missing' argument to fields is deprecated. " + "Use 'load_default' instead.", + RemovedInMarshmallow4Warning, + stacklevel=2, + ) + if load_default is missing_: + load_default = missing + self.dump_default = dump_default + self.load_default = load_default + + self.attribute = attribute + self.data_key = data_key + self.validate = validate + if validate is None: + self.validators = [] + elif callable(validate): + self.validators = [validate] + elif utils.is_iterable_but_not_string(validate): + self.validators = list(validate) + else: + raise ValueError( + "The 'validate' parameter must be a callable " + "or a collection of callables." + ) + + # If allow_none is None and load_default is None + # None should be considered valid by default + self.allow_none = load_default is None if allow_none is None else allow_none + self.load_only = load_only + self.dump_only = dump_only + if required is True and load_default is not missing_: + raise ValueError("'load_default' must not be set for required fields.") + self.required = required + + metadata = metadata or {} + self.metadata = {**metadata, **additional_metadata} + if additional_metadata: + warnings.warn( + "Passing field metadata as keyword arguments is deprecated. Use the " + "explicit `metadata=...` argument instead. " + f"Additional metadata: {additional_metadata}", + RemovedInMarshmallow4Warning, + stacklevel=2, + ) + + # Collect default error message from self and parent classes + messages = {} # type: dict[str, str] + for cls in reversed(self.__class__.__mro__): + messages.update(getattr(cls, "default_error_messages", {})) + messages.update(error_messages or {}) + self.error_messages = messages + + def __repr__(self) -> str: + return ( + f"" + ) + + def __deepcopy__(self, memo): + return copy.copy(self) + + def get_value(self, obj, attr, accessor=None, default=missing_): + """Return the value for a given key from an object. + + :param object obj: The object to get the value from. + :param str attr: The attribute/key in `obj` to get the value from. + :param callable accessor: A callable used to retrieve the value of `attr` from + the object `obj`. Defaults to `marshmallow.utils.get_value`. + """ + accessor_func = accessor or utils.get_value + check_key = attr if self.attribute is None else self.attribute + return accessor_func(obj, check_key, default) + + def _validate(self, value): + """Perform validation on ``value``. Raise a :exc:`ValidationError` if validation + does not succeed. + """ + self._validate_all(value) + + @property + def _validate_all(self): + return And(*self.validators, error=self.error_messages["validator_failed"]) + + def make_error(self, key: str, **kwargs) -> ValidationError: + """Helper method to make a `ValidationError` with an error message + from ``self.error_messages``. + """ + try: + msg = self.error_messages[key] + except KeyError as error: + class_name = self.__class__.__name__ + message = ( + f"ValidationError raised by `{class_name}`, but error key `{key}` does " + "not exist in the `error_messages` dictionary." + ) + raise AssertionError(message) from error + if isinstance(msg, (str, bytes)): + msg = msg.format(**kwargs) + return ValidationError(msg) + + def fail(self, key: str, **kwargs): + """Helper method that raises a `ValidationError` with an error message + from ``self.error_messages``. + + .. deprecated:: 3.0.0 + Use `make_error ` instead. + """ + warnings.warn( + f'`Field.fail` is deprecated. Use `raise self.make_error("{key}", ...)` instead.', + RemovedInMarshmallow4Warning, + stacklevel=2, + ) + raise self.make_error(key=key, **kwargs) + + def _validate_missing(self, value): + """Validate missing values. Raise a :exc:`ValidationError` if + `value` should be considered missing. + """ + if value is missing_ and self.required: + raise self.make_error("required") + if value is None and not self.allow_none: + raise self.make_error("null") + + def serialize( + self, + attr: str, + obj: typing.Any, + accessor: typing.Callable[[typing.Any, str, typing.Any], typing.Any] + | None = None, + **kwargs, + ): + """Pulls the value for the given key from the object, applies the + field's formatting and returns the result. + + :param attr: The attribute/key to get from the object. + :param obj: The object to access the attribute/key from. + :param accessor: Function used to access values from ``obj``. + :param kwargs: Field-specific keyword arguments. + """ + if self._CHECK_ATTRIBUTE: + value = self.get_value(obj, attr, accessor=accessor) + if value is missing_: + default = self.dump_default + value = default() if callable(default) else default + if value is missing_: + return value + else: + value = None + return self._serialize(value, attr, obj, **kwargs) + + def deserialize( + self, + value: typing.Any, + attr: str | None = None, + data: typing.Mapping[str, typing.Any] | None = None, + **kwargs, + ): + """Deserialize ``value``. + + :param value: The value to deserialize. + :param attr: The attribute/key in `data` to deserialize. + :param data: The raw input data passed to `Schema.load`. + :param kwargs: Field-specific keyword arguments. + :raise ValidationError: If an invalid value is passed or if a required value + is missing. + """ + # Validate required fields, deserialize, then validate + # deserialized value + self._validate_missing(value) + if value is missing_: + _miss = self.load_default + return _miss() if callable(_miss) else _miss + if self.allow_none and value is None: + return None + output = self._deserialize(value, attr, data, **kwargs) + self._validate(output) + return output + + # Methods for concrete classes to override. + + def _bind_to_schema(self, field_name, schema): + """Update field with values from its parent schema. Called by + :meth:`Schema._bind_field `. + + :param str field_name: Field name set in schema. + :param Schema|Field schema: Parent object. + """ + self.parent = self.parent or schema + self.name = self.name or field_name + self.root = self.root or ( + self.parent.root if isinstance(self.parent, FieldABC) else self.parent + ) + + def _serialize( + self, value: typing.Any, attr: str | None, obj: typing.Any, **kwargs + ): + """Serializes ``value`` to a basic Python datatype. Noop by default. + Concrete :class:`Field` classes should implement this method. + + Example: :: + + class TitleCase(Field): + def _serialize(self, value, attr, obj, **kwargs): + if not value: + return "" + return str(value).title() + + :param value: The value to be serialized. + :param str attr: The attribute or key on the object to be serialized. + :param object obj: The object the value was pulled from. + :param dict kwargs: Field-specific keyword arguments. + :return: The serialized value + """ + return value + + def _deserialize( + self, + value: typing.Any, + attr: str | None, + data: typing.Mapping[str, typing.Any] | None, + **kwargs, + ): + """Deserialize value. Concrete :class:`Field` classes should implement this method. + + :param value: The value to be deserialized. + :param attr: The attribute/key in `data` to be deserialized. + :param data: The raw input data passed to the `Schema.load`. + :param kwargs: Field-specific keyword arguments. + :raise ValidationError: In case of formatting or validation failure. + :return: The deserialized value. + + .. versionchanged:: 2.0.0 + Added ``attr`` and ``data`` parameters. + + .. versionchanged:: 3.0.0 + Added ``**kwargs`` to signature. + """ + return value + + # Properties + + @property + def context(self): + """The context dictionary for the parent :class:`Schema`.""" + return self.parent.context + + # the default and missing properties are provided for compatibility and + # emit warnings when they are accessed and set + @property + def default(self): + warnings.warn( + "The 'default' attribute of fields is deprecated. " + "Use 'dump_default' instead.", + RemovedInMarshmallow4Warning, + stacklevel=2, + ) + return self.dump_default + + @default.setter + def default(self, value): + warnings.warn( + "The 'default' attribute of fields is deprecated. " + "Use 'dump_default' instead.", + RemovedInMarshmallow4Warning, + stacklevel=2, + ) + self.dump_default = value + + @property + def missing(self): + warnings.warn( + "The 'missing' attribute of fields is deprecated. " + "Use 'load_default' instead.", + RemovedInMarshmallow4Warning, + stacklevel=2, + ) + return self.load_default + + @missing.setter + def missing(self, value): + warnings.warn( + "The 'missing' attribute of fields is deprecated. " + "Use 'load_default' instead.", + RemovedInMarshmallow4Warning, + stacklevel=2, + ) + self.load_default = value + + +class Raw(Field): + """Field that applies no formatting.""" + + +class Nested(Field): + """Allows you to nest a :class:`Schema ` + inside a field. + + Examples: :: + + class ChildSchema(Schema): + id = fields.Str() + name = fields.Str() + # Use lambda functions when you need two-way nesting or self-nesting + parent = fields.Nested(lambda: ParentSchema(only=("id",)), dump_only=True) + siblings = fields.List(fields.Nested(lambda: ChildSchema(only=("id", "name")))) + + + class ParentSchema(Schema): + id = fields.Str() + children = fields.List( + fields.Nested(ChildSchema(only=("id", "parent", "siblings"))) + ) + spouse = fields.Nested(lambda: ParentSchema(only=("id",))) + + When passing a `Schema ` instance as the first argument, + the instance's ``exclude``, ``only``, and ``many`` attributes will be respected. + + Therefore, when passing the ``exclude``, ``only``, or ``many`` arguments to `fields.Nested`, + you should pass a `Schema ` class (not an instance) as the first argument. + + :: + + # Yes + author = fields.Nested(UserSchema, only=("id", "name")) + + # No + author = fields.Nested(UserSchema(), only=("id", "name")) + + :param nested: `Schema` instance, class, class name (string), dictionary, or callable that + returns a `Schema` or dictionary. Dictionaries are converted with `Schema.from_dict`. + :param exclude: A list or tuple of fields to exclude. + :param only: A list or tuple of fields to marshal. If `None`, all fields are marshalled. + This parameter takes precedence over ``exclude``. + :param many: Whether the field is a collection of objects. + :param unknown: Whether to exclude, include, or raise an error for unknown + fields in the data. Use `EXCLUDE`, `INCLUDE` or `RAISE`. + :param kwargs: The same keyword arguments that :class:`Field` receives. + """ + + #: Default error messages. + default_error_messages = {"type": "Invalid type."} + + def __init__( + self, + nested: SchemaABC + | type + | str + | dict[str, Field | type] + | typing.Callable[[], SchemaABC | type | dict[str, Field | type]], + *, + dump_default: typing.Any = missing_, + default: typing.Any = missing_, + only: types.StrSequenceOrSet | None = None, + exclude: types.StrSequenceOrSet = (), + many: bool = False, + unknown: str | None = None, + **kwargs, + ): + # Raise error if only or exclude is passed as string, not list of strings + if only is not None and not is_collection(only): + raise StringNotCollectionError('"only" should be a collection of strings.') + if not is_collection(exclude): + raise StringNotCollectionError( + '"exclude" should be a collection of strings.' + ) + if nested == "self": + warnings.warn( + "Passing 'self' to `Nested` is deprecated. " + "Use `Nested(lambda: MySchema(...))` instead.", + RemovedInMarshmallow4Warning, + stacklevel=2, + ) + self.nested = nested + self.only = only + self.exclude = exclude + self.many = many + self.unknown = unknown + self._schema = None # Cached Schema instance + super().__init__(default=default, dump_default=dump_default, **kwargs) + + @property + def schema(self): + """The nested Schema object. + + .. versionchanged:: 1.0.0 + Renamed from `serializer` to `schema`. + """ + if not self._schema: + # Inherit context from parent. + context = getattr(self.parent, "context", {}) + if callable(self.nested) and not isinstance(self.nested, type): + nested = self.nested() + else: + nested = self.nested + if isinstance(nested, dict): + # defer the import of `marshmallow.schema` to avoid circular imports + from .schema import Schema + + nested = Schema.from_dict(nested) + + if isinstance(nested, SchemaABC): + self._schema = copy.copy(nested) + self._schema.context.update(context) + # Respect only and exclude passed from parent and re-initialize fields + set_class = self._schema.set_class + if self.only is not None: + if self._schema.only is not None: + original = self._schema.only + else: # only=None -> all fields + original = self._schema.fields.keys() + self._schema.only = set_class(self.only) & set_class(original) + if self.exclude: + original = self._schema.exclude + self._schema.exclude = set_class(self.exclude) | set_class(original) + self._schema._init_fields() + else: + if isinstance(nested, type) and issubclass(nested, SchemaABC): + schema_class = nested + elif not isinstance(nested, (str, bytes)): + raise ValueError( + "`Nested` fields must be passed a " + f"`Schema`, not {nested.__class__}." + ) + elif nested == "self": + schema_class = self.root.__class__ + else: + schema_class = class_registry.get_class(nested) + self._schema = schema_class( + many=self.many, + only=self.only, + exclude=self.exclude, + context=context, + load_only=self._nested_normalized_option("load_only"), + dump_only=self._nested_normalized_option("dump_only"), + ) + return self._schema + + def _nested_normalized_option(self, option_name: str) -> list[str]: + nested_field = f"{self.name}." + return [ + field.split(nested_field, 1)[1] + for field in getattr(self.root, option_name, set()) + if field.startswith(nested_field) + ] + + def _serialize(self, nested_obj, attr, obj, **kwargs): + # Load up the schema first. This allows a RegistryError to be raised + # if an invalid schema name was passed + schema = self.schema + if nested_obj is None: + return None + many = schema.many or self.many + return schema.dump(nested_obj, many=many) + + def _test_collection(self, value): + many = self.schema.many or self.many + if many and not utils.is_collection(value): + raise self.make_error("type", input=value, type=value.__class__.__name__) + + def _load(self, value, data, partial=None): + try: + valid_data = self.schema.load(value, unknown=self.unknown, partial=partial) + except ValidationError as error: + raise ValidationError( + error.messages, valid_data=error.valid_data + ) from error + return valid_data + + def _deserialize(self, value, attr, data, partial=None, **kwargs): + """Same as :meth:`Field._deserialize` with additional ``partial`` argument. + + :param bool|tuple partial: For nested schemas, the ``partial`` + parameter passed to `Schema.load`. + + .. versionchanged:: 3.0.0 + Add ``partial`` parameter. + """ + self._test_collection(value) + return self._load(value, data, partial=partial) + + +class Pluck(Nested): + """Allows you to replace nested data with one of the data's fields. + + Example: :: + + from . import Schema, fields + + + class ArtistSchema(Schema): + id = fields.Int() + name = fields.Str() + + + class AlbumSchema(Schema): + artist = fields.Pluck(ArtistSchema, "id") + + + in_data = {"artist": 42} + loaded = AlbumSchema().load(in_data) # => {'artist': {'id': 42}} + dumped = AlbumSchema().dump(loaded) # => {'artist': 42} + + :param Schema nested: The Schema class or class name (string) + to nest, or ``"self"`` to nest the :class:`Schema` within itself. + :param str field_name: The key to pluck a value from. + :param kwargs: The same keyword arguments that :class:`Nested` receives. + """ + + def __init__( + self, + nested: SchemaABC | type | str | typing.Callable[[], SchemaABC], + field_name: str, + **kwargs, + ): + super().__init__(nested, only=(field_name,), **kwargs) + self.field_name = field_name + + @property + def _field_data_key(self): + only_field = self.schema.fields[self.field_name] + return only_field.data_key or self.field_name + + def _serialize(self, nested_obj, attr, obj, **kwargs): + ret = super()._serialize(nested_obj, attr, obj, **kwargs) + if ret is None: + return None + if self.many: + return utils.pluck(ret, key=self._field_data_key) + return ret[self._field_data_key] + + def _deserialize(self, value, attr, data, partial=None, **kwargs): + self._test_collection(value) + if self.many: + value = [{self._field_data_key: v} for v in value] + else: + value = {self._field_data_key: value} + return self._load(value, data, partial=partial) + + +class List(Field): + """A list field, composed with another `Field` class or + instance. + + Example: :: + + numbers = fields.List(fields.Float()) + + :param cls_or_instance: A field class or instance. + :param kwargs: The same keyword arguments that :class:`Field` receives. + + .. versionchanged:: 2.0.0 + The ``allow_none`` parameter now applies to deserialization and + has the same semantics as the other fields. + + .. versionchanged:: 3.0.0rc9 + Does not serialize scalar values to single-item lists. + """ + + #: Default error messages. + default_error_messages = {"invalid": "Not a valid list."} + + def __init__(self, cls_or_instance: Field | type, **kwargs): + super().__init__(**kwargs) + try: + self.inner = resolve_field_instance(cls_or_instance) + except FieldInstanceResolutionError as error: + raise ValueError( + "The list elements must be a subclass or instance of " + "marshmallow.base.FieldABC." + ) from error + if isinstance(self.inner, Nested): + self.only = self.inner.only + self.exclude = self.inner.exclude + + def _bind_to_schema(self, field_name, schema): + super()._bind_to_schema(field_name, schema) + self.inner = copy.deepcopy(self.inner) + self.inner._bind_to_schema(field_name, self) + if isinstance(self.inner, Nested): + self.inner.only = self.only + self.inner.exclude = self.exclude + + def _serialize(self, value, attr, obj, **kwargs) -> list[typing.Any] | None: + if value is None: + return None + return [self.inner._serialize(each, attr, obj, **kwargs) for each in value] + + def _deserialize(self, value, attr, data, **kwargs) -> list[typing.Any]: + if not utils.is_collection(value): + raise self.make_error("invalid") + + result = [] + errors = {} + for idx, each in enumerate(value): + try: + result.append(self.inner.deserialize(each, **kwargs)) + except ValidationError as error: + if error.valid_data is not None: + result.append(error.valid_data) + errors.update({idx: error.messages}) + if errors: + raise ValidationError(errors, valid_data=result) + return result + + +class Tuple(Field): + """A tuple field, composed of a fixed number of other `Field` classes or + instances + + Example: :: + + row = Tuple((fields.String(), fields.Integer(), fields.Float())) + + .. note:: + Because of the structured nature of `collections.namedtuple` and + `typing.NamedTuple`, using a Schema within a Nested field for them is + more appropriate than using a `Tuple` field. + + :param Iterable[Field] tuple_fields: An iterable of field classes or + instances. + :param kwargs: The same keyword arguments that :class:`Field` receives. + + .. versionadded:: 3.0.0rc4 + """ + + #: Default error messages. + default_error_messages = {"invalid": "Not a valid tuple."} + + def __init__(self, tuple_fields, *args, **kwargs): + super().__init__(*args, **kwargs) + if not utils.is_collection(tuple_fields): + raise ValueError( + "tuple_fields must be an iterable of Field classes or " "instances." + ) + + try: + self.tuple_fields = [ + resolve_field_instance(cls_or_instance) + for cls_or_instance in tuple_fields + ] + except FieldInstanceResolutionError as error: + raise ValueError( + 'Elements of "tuple_fields" must be subclasses or ' + "instances of marshmallow.base.FieldABC." + ) from error + + self.validate_length = Length(equal=len(self.tuple_fields)) + + def _bind_to_schema(self, field_name, schema): + super()._bind_to_schema(field_name, schema) + new_tuple_fields = [] + for field in self.tuple_fields: + field = copy.deepcopy(field) + field._bind_to_schema(field_name, self) + new_tuple_fields.append(field) + + self.tuple_fields = new_tuple_fields + + def _serialize(self, value, attr, obj, **kwargs) -> tuple | None: + if value is None: + return None + + return tuple( + field._serialize(each, attr, obj, **kwargs) + for field, each in zip(self.tuple_fields, value) + ) + + def _deserialize(self, value, attr, data, **kwargs) -> tuple: + if not utils.is_collection(value): + raise self.make_error("invalid") + + self.validate_length(value) + + result = [] + errors = {} + + for idx, (field, each) in enumerate(zip(self.tuple_fields, value)): + try: + result.append(field.deserialize(each, **kwargs)) + except ValidationError as error: + if error.valid_data is not None: + result.append(error.valid_data) + errors.update({idx: error.messages}) + if errors: + raise ValidationError(errors, valid_data=result) + + return tuple(result) + + +class String(Field): + """A string field. + + :param kwargs: The same keyword arguments that :class:`Field` receives. + """ + + #: Default error messages. + default_error_messages = { + "invalid": "Not a valid string.", + "invalid_utf8": "Not a valid utf-8 string.", + } + + def _serialize(self, value, attr, obj, **kwargs) -> str | None: + if value is None: + return None + return utils.ensure_text_type(value) + + def _deserialize(self, value, attr, data, **kwargs) -> typing.Any: + if not isinstance(value, (str, bytes)): + raise self.make_error("invalid") + try: + return utils.ensure_text_type(value) + except UnicodeDecodeError as error: + raise self.make_error("invalid_utf8") from error + + +class UUID(String): + """A UUID field.""" + + #: Default error messages. + default_error_messages = {"invalid_uuid": "Not a valid UUID."} + + def _validated(self, value) -> uuid.UUID | None: + """Format the value or raise a :exc:`ValidationError` if an error occurs.""" + if value is None: + return None + if isinstance(value, uuid.UUID): + return value + try: + if isinstance(value, bytes) and len(value) == 16: + return uuid.UUID(bytes=value) + return uuid.UUID(value) + except (ValueError, AttributeError, TypeError) as error: + raise self.make_error("invalid_uuid") from error + + def _deserialize(self, value, attr, data, **kwargs) -> uuid.UUID | None: + return self._validated(value) + + +class Number(Field): + """Base class for number fields. + + :param bool as_string: If `True`, format the serialized value as a string. + :param kwargs: The same keyword arguments that :class:`Field` receives. + """ + + num_type = float # type: typing.Type + + #: Default error messages. + default_error_messages = { + "invalid": "Not a valid number.", + "too_large": "Number too large.", + } + + def __init__(self, *, as_string: bool = False, **kwargs): + self.as_string = as_string + super().__init__(**kwargs) + + def _format_num(self, value) -> typing.Any: + """Return the number value for value, given this field's `num_type`.""" + return self.num_type(value) + + def _validated(self, value) -> _T | None: + """Format the value or raise a :exc:`ValidationError` if an error occurs.""" + if value is None: + return None + # (value is True or value is False) is ~5x faster than isinstance(value, bool) + if value is True or value is False: + raise self.make_error("invalid", input=value) + try: + return self._format_num(value) + except (TypeError, ValueError) as error: + raise self.make_error("invalid", input=value) from error + except OverflowError as error: + raise self.make_error("too_large", input=value) from error + + def _to_string(self, value) -> str: + return str(value) + + def _serialize(self, value, attr, obj, **kwargs) -> str | _T | None: + """Return a string if `self.as_string=True`, otherwise return this field's `num_type`.""" + if value is None: + return None + ret = self._format_num(value) # type: _T + return self._to_string(ret) if self.as_string else ret + + def _deserialize(self, value, attr, data, **kwargs) -> _T | None: + return self._validated(value) + + +class Integer(Number): + """An integer field. + + :param strict: If `True`, only integer types are valid. + Otherwise, any value castable to `int` is valid. + :param kwargs: The same keyword arguments that :class:`Number` receives. + """ + + num_type = int + + #: Default error messages. + default_error_messages = {"invalid": "Not a valid integer."} + + def __init__(self, *, strict: bool = False, **kwargs): + self.strict = strict + super().__init__(**kwargs) + + # override Number + def _validated(self, value): + if self.strict and not isinstance(value, numbers.Integral): + raise self.make_error("invalid", input=value) + return super()._validated(value) + + +class Float(Number): + """A double as an IEEE-754 double precision string. + + :param bool allow_nan: If `True`, `NaN`, `Infinity` and `-Infinity` are allowed, + even though they are illegal according to the JSON specification. + :param bool as_string: If `True`, format the value as a string. + :param kwargs: The same keyword arguments that :class:`Number` receives. + """ + + num_type = float + + #: Default error messages. + default_error_messages = { + "special": "Special numeric values (nan or infinity) are not permitted." + } + + def __init__(self, *, allow_nan: bool = False, as_string: bool = False, **kwargs): + self.allow_nan = allow_nan + super().__init__(as_string=as_string, **kwargs) + + def _validated(self, value): + num = super()._validated(value) + if self.allow_nan is False: + if math.isnan(num) or num == float("inf") or num == float("-inf"): + raise self.make_error("special") + return num + + +class Decimal(Number): + """A field that (de)serializes to the Python ``decimal.Decimal`` type. + It's safe to use when dealing with money values, percentages, ratios + or other numbers where precision is critical. + + .. warning:: + + This field serializes to a `decimal.Decimal` object by default. If you need + to render your data as JSON, keep in mind that the `json` module from the + standard library does not encode `decimal.Decimal`. Therefore, you must use + a JSON library that can handle decimals, such as `simplejson`, or serialize + to a string by passing ``as_string=True``. + + .. warning:: + + If a JSON `float` value is passed to this field for deserialization it will + first be cast to its corresponding `string` value before being deserialized + to a `decimal.Decimal` object. The default `__str__` implementation of the + built-in Python `float` type may apply a destructive transformation upon + its input data and therefore cannot be relied upon to preserve precision. + To avoid this, you can instead pass a JSON `string` to be deserialized + directly. + + :param places: How many decimal places to quantize the value. If `None`, does + not quantize the value. + :param rounding: How to round the value during quantize, for example + `decimal.ROUND_UP`. If `None`, uses the rounding value from + the current thread's context. + :param allow_nan: If `True`, `NaN`, `Infinity` and `-Infinity` are allowed, + even though they are illegal according to the JSON specification. + :param as_string: If `True`, serialize to a string instead of a Python + `decimal.Decimal` type. + :param kwargs: The same keyword arguments that :class:`Number` receives. + + .. versionadded:: 1.2.0 + """ + + num_type = decimal.Decimal + + #: Default error messages. + default_error_messages = { + "special": "Special numeric values (nan or infinity) are not permitted." + } + + def __init__( + self, + places: int | None = None, + rounding: str | None = None, + *, + allow_nan: bool = False, + as_string: bool = False, + **kwargs, + ): + self.places = ( + decimal.Decimal((0, (1,), -places)) if places is not None else None + ) + self.rounding = rounding + self.allow_nan = allow_nan + super().__init__(as_string=as_string, **kwargs) + + # override Number + def _format_num(self, value): + num = decimal.Decimal(str(value)) + if self.allow_nan: + if num.is_nan(): + return decimal.Decimal("NaN") # avoid sNaN, -sNaN and -NaN + if self.places is not None and num.is_finite(): + num = num.quantize(self.places, rounding=self.rounding) + return num + + # override Number + def _validated(self, value): + try: + num = super()._validated(value) + except decimal.InvalidOperation as error: + raise self.make_error("invalid") from error + if not self.allow_nan and (num.is_nan() or num.is_infinite()): + raise self.make_error("special") + return num + + # override Number + def _to_string(self, value): + return format(value, "f") + + +class Boolean(Field): + """A boolean field. + + :param truthy: Values that will (de)serialize to `True`. If an empty + set, any non-falsy value will deserialize to `True`. If `None`, + `marshmallow.fields.Boolean.truthy` will be used. + :param falsy: Values that will (de)serialize to `False`. If `None`, + `marshmallow.fields.Boolean.falsy` will be used. + :param kwargs: The same keyword arguments that :class:`Field` receives. + """ + + #: Default truthy values. + truthy = { + "t", + "T", + "true", + "True", + "TRUE", + "on", + "On", + "ON", + "y", + "Y", + "yes", + "Yes", + "YES", + "1", + 1, + # Equal to 1 + # True, + } + #: Default falsy values. + falsy = { + "f", + "F", + "false", + "False", + "FALSE", + "off", + "Off", + "OFF", + "n", + "N", + "no", + "No", + "NO", + "0", + 0, + # Equal to 0 + # 0.0, + # False, + } + + #: Default error messages. + default_error_messages = {"invalid": "Not a valid boolean."} + + def __init__( + self, + *, + truthy: set | None = None, + falsy: set | None = None, + **kwargs, + ): + super().__init__(**kwargs) + + if truthy is not None: + self.truthy = set(truthy) + if falsy is not None: + self.falsy = set(falsy) + + def _serialize(self, value, attr, obj, **kwargs): + if value is None: + return None + + try: + if value in self.truthy: + return True + if value in self.falsy: + return False + except TypeError: + pass + + return bool(value) + + def _deserialize(self, value, attr, data, **kwargs): + if not self.truthy: + return bool(value) + try: + if value in self.truthy: + return True + if value in self.falsy: + return False + except TypeError as error: + raise self.make_error("invalid", input=value) from error + raise self.make_error("invalid", input=value) + + +class DateTime(Field): + """A formatted datetime string. + + Example: ``'2014-12-22T03:12:58.019077+00:00'`` + + :param format: Either ``"rfc"`` (for RFC822), ``"iso"`` (for ISO8601), + ``"timestamp"``, ``"timestamp_ms"`` (for a POSIX timestamp) or a date format string. + If `None`, defaults to "iso". + :param kwargs: The same keyword arguments that :class:`Field` receives. + + .. versionchanged:: 3.0.0rc9 + Does not modify timezone information on (de)serialization. + .. versionchanged:: 3.19 + Add timestamp as a format. + """ + + SERIALIZATION_FUNCS = { + "iso": utils.isoformat, + "iso8601": utils.isoformat, + "rfc": utils.rfcformat, + "rfc822": utils.rfcformat, + "timestamp": utils.timestamp, + "timestamp_ms": utils.timestamp_ms, + } # type: typing.Dict[str, typing.Callable[[typing.Any], str | float]] + + DESERIALIZATION_FUNCS = { + "iso": utils.from_iso_datetime, + "iso8601": utils.from_iso_datetime, + "rfc": utils.from_rfc, + "rfc822": utils.from_rfc, + "timestamp": utils.from_timestamp, + "timestamp_ms": utils.from_timestamp_ms, + } # type: typing.Dict[str, typing.Callable[[str], typing.Any]] + + DEFAULT_FORMAT = "iso" + + OBJ_TYPE = "datetime" + + SCHEMA_OPTS_VAR_NAME = "datetimeformat" + + #: Default error messages. + default_error_messages = { + "invalid": "Not a valid {obj_type}.", + "invalid_awareness": "Not a valid {awareness} {obj_type}.", + "format": '"{input}" cannot be formatted as a {obj_type}.', + } + + def __init__(self, format: str | None = None, **kwargs) -> None: + super().__init__(**kwargs) + # Allow this to be None. It may be set later in the ``_serialize`` + # or ``_deserialize`` methods. This allows a Schema to dynamically set the + # format, e.g. from a Meta option + self.format = format + + def _bind_to_schema(self, field_name, schema): + super()._bind_to_schema(field_name, schema) + self.format = ( + self.format + or getattr(self.root.opts, self.SCHEMA_OPTS_VAR_NAME) + or self.DEFAULT_FORMAT + ) + + def _serialize(self, value, attr, obj, **kwargs) -> str | float | None: + if value is None: + return None + data_format = self.format or self.DEFAULT_FORMAT + format_func = self.SERIALIZATION_FUNCS.get(data_format) + if format_func: + return format_func(value) + return value.strftime(data_format) + + def _deserialize(self, value, attr, data, **kwargs) -> dt.datetime: + data_format = self.format or self.DEFAULT_FORMAT + func = self.DESERIALIZATION_FUNCS.get(data_format) + try: + if func: + return func(value) + return self._make_object_from_format(value, data_format) + except (TypeError, AttributeError, ValueError) as error: + raise self.make_error( + "invalid", input=value, obj_type=self.OBJ_TYPE + ) from error + + @staticmethod + def _make_object_from_format(value, data_format) -> dt.datetime: + return dt.datetime.strptime(value, data_format) + + +class NaiveDateTime(DateTime): + """A formatted naive datetime string. + + :param format: See :class:`DateTime`. + :param timezone: Used on deserialization. If `None`, + aware datetimes are rejected. If not `None`, aware datetimes are + converted to this timezone before their timezone information is + removed. + :param kwargs: The same keyword arguments that :class:`Field` receives. + + .. versionadded:: 3.0.0rc9 + """ + + AWARENESS = "naive" + + def __init__( + self, + format: str | None = None, + *, + timezone: dt.timezone | None = None, + **kwargs, + ) -> None: + super().__init__(format=format, **kwargs) + self.timezone = timezone + + def _deserialize(self, value, attr, data, **kwargs) -> dt.datetime: + ret = super()._deserialize(value, attr, data, **kwargs) + if is_aware(ret): + if self.timezone is None: + raise self.make_error( + "invalid_awareness", + awareness=self.AWARENESS, + obj_type=self.OBJ_TYPE, + ) + ret = ret.astimezone(self.timezone).replace(tzinfo=None) + return ret + + +class AwareDateTime(DateTime): + """A formatted aware datetime string. + + :param format: See :class:`DateTime`. + :param default_timezone: Used on deserialization. If `None`, naive + datetimes are rejected. If not `None`, naive datetimes are set this + timezone. + :param kwargs: The same keyword arguments that :class:`Field` receives. + + .. versionadded:: 3.0.0rc9 + """ + + AWARENESS = "aware" + + def __init__( + self, + format: str | None = None, + *, + default_timezone: dt.tzinfo | None = None, + **kwargs, + ) -> None: + super().__init__(format=format, **kwargs) + self.default_timezone = default_timezone + + def _deserialize(self, value, attr, data, **kwargs) -> dt.datetime: + ret = super()._deserialize(value, attr, data, **kwargs) + if not is_aware(ret): + if self.default_timezone is None: + raise self.make_error( + "invalid_awareness", + awareness=self.AWARENESS, + obj_type=self.OBJ_TYPE, + ) + ret = ret.replace(tzinfo=self.default_timezone) + return ret + + +class Time(DateTime): + """A formatted time string. + + Example: ``'03:12:58.019077'`` + + :param format: Either ``"iso"`` (for ISO8601) or a date format string. + If `None`, defaults to "iso". + :param kwargs: The same keyword arguments that :class:`Field` receives. + """ + + SERIALIZATION_FUNCS = {"iso": utils.to_iso_time, "iso8601": utils.to_iso_time} + + DESERIALIZATION_FUNCS = {"iso": utils.from_iso_time, "iso8601": utils.from_iso_time} + + DEFAULT_FORMAT = "iso" + + OBJ_TYPE = "time" + + SCHEMA_OPTS_VAR_NAME = "timeformat" + + @staticmethod + def _make_object_from_format(value, data_format): + return dt.datetime.strptime(value, data_format).time() + + +class Date(DateTime): + """ISO8601-formatted date string. + + :param format: Either ``"iso"`` (for ISO8601) or a date format string. + If `None`, defaults to "iso". + :param kwargs: The same keyword arguments that :class:`Field` receives. + """ + + #: Default error messages. + default_error_messages = { + "invalid": "Not a valid date.", + "format": '"{input}" cannot be formatted as a date.', + } + + SERIALIZATION_FUNCS = {"iso": utils.to_iso_date, "iso8601": utils.to_iso_date} + + DESERIALIZATION_FUNCS = {"iso": utils.from_iso_date, "iso8601": utils.from_iso_date} + + DEFAULT_FORMAT = "iso" + + OBJ_TYPE = "date" + + SCHEMA_OPTS_VAR_NAME = "dateformat" + + @staticmethod + def _make_object_from_format(value, data_format): + return dt.datetime.strptime(value, data_format).date() + + +class TimeDelta(Field): + """A field that (de)serializes a :class:`datetime.timedelta` object to an + integer or float and vice versa. The integer or float can represent the + number of days, seconds or microseconds. + + :param precision: Influences how the integer or float is interpreted during + (de)serialization. Must be 'days', 'seconds', 'microseconds', + 'milliseconds', 'minutes', 'hours' or 'weeks'. + :param serialization_type: Whether to (de)serialize to a `int` or `float`. + :param kwargs: The same keyword arguments that :class:`Field` receives. + + Integer Caveats + --------------- + Any fractional parts (which depends on the precision used) will be truncated + when serializing using `int`. + + Float Caveats + ------------- + Use of `float` when (de)serializing may result in data precision loss due + to the way machines handle floating point values. + + Regardless of the precision chosen, the fractional part when using `float` + will always be truncated to microseconds. + For example, `1.12345` interpreted as microseconds will result in `timedelta(microseconds=1)`. + + .. versionchanged:: 2.0.0 + Always serializes to an integer value to avoid rounding errors. + Add `precision` parameter. + .. versionchanged:: 3.17.0 + Allow (de)serialization to `float` through use of a new `serialization_type` parameter. + `int` is the default to retain previous behaviour. + """ + + DAYS = "days" + SECONDS = "seconds" + MICROSECONDS = "microseconds" + MILLISECONDS = "milliseconds" + MINUTES = "minutes" + HOURS = "hours" + WEEKS = "weeks" + + #: Default error messages. + default_error_messages = { + "invalid": "Not a valid period of time.", + "format": "{input!r} cannot be formatted as a timedelta.", + } + + def __init__( + self, + precision: str = SECONDS, + serialization_type: type[int | float] = int, + **kwargs, + ): + precision = precision.lower() + units = ( + self.DAYS, + self.SECONDS, + self.MICROSECONDS, + self.MILLISECONDS, + self.MINUTES, + self.HOURS, + self.WEEKS, + ) + + if precision not in units: + msg = 'The precision must be {} or "{}".'.format( + ", ".join([f'"{each}"' for each in units[:-1]]), units[-1] + ) + raise ValueError(msg) + + if serialization_type not in (int, float): + raise ValueError("The serialization type must be one of int or float") + + self.precision = precision + self.serialization_type = serialization_type + super().__init__(**kwargs) + + def _serialize(self, value, attr, obj, **kwargs): + if value is None: + return None + + base_unit = dt.timedelta(**{self.precision: 1}) + + if self.serialization_type is int: + delta = utils.timedelta_to_microseconds(value) + unit = utils.timedelta_to_microseconds(base_unit) + return delta // unit + assert self.serialization_type is float + return value.total_seconds() / base_unit.total_seconds() + + def _deserialize(self, value, attr, data, **kwargs): + try: + value = self.serialization_type(value) + except (TypeError, ValueError) as error: + raise self.make_error("invalid") from error + + kwargs = {self.precision: value} + + try: + return dt.timedelta(**kwargs) + except OverflowError as error: + raise self.make_error("invalid") from error + + +class Mapping(Field): + """An abstract class for objects with key-value pairs. + + :param keys: A field class or instance for dict keys. + :param values: A field class or instance for dict values. + :param kwargs: The same keyword arguments that :class:`Field` receives. + + .. note:: + When the structure of nested data is not known, you may omit the + `keys` and `values` arguments to prevent content validation. + + .. versionadded:: 3.0.0rc4 + """ + + mapping_type = dict + + #: Default error messages. + default_error_messages = {"invalid": "Not a valid mapping type."} + + def __init__( + self, + keys: Field | type | None = None, + values: Field | type | None = None, + **kwargs, + ): + super().__init__(**kwargs) + if keys is None: + self.key_field = None + else: + try: + self.key_field = resolve_field_instance(keys) + except FieldInstanceResolutionError as error: + raise ValueError( + '"keys" must be a subclass or instance of ' + "marshmallow.base.FieldABC." + ) from error + + if values is None: + self.value_field = None + else: + try: + self.value_field = resolve_field_instance(values) + except FieldInstanceResolutionError as error: + raise ValueError( + '"values" must be a subclass or instance of ' + "marshmallow.base.FieldABC." + ) from error + if isinstance(self.value_field, Nested): + self.only = self.value_field.only + self.exclude = self.value_field.exclude + + def _bind_to_schema(self, field_name, schema): + super()._bind_to_schema(field_name, schema) + if self.value_field: + self.value_field = copy.deepcopy(self.value_field) + self.value_field._bind_to_schema(field_name, self) + if isinstance(self.value_field, Nested): + self.value_field.only = self.only + self.value_field.exclude = self.exclude + if self.key_field: + self.key_field = copy.deepcopy(self.key_field) + self.key_field._bind_to_schema(field_name, self) + + def _serialize(self, value, attr, obj, **kwargs): + if value is None: + return None + if not self.value_field and not self.key_field: + return self.mapping_type(value) + + #  Serialize keys + if self.key_field is None: + keys = {k: k for k in value.keys()} + else: + keys = { + k: self.key_field._serialize(k, None, None, **kwargs) + for k in value.keys() + } + + #  Serialize values + result = self.mapping_type() + if self.value_field is None: + for k, v in value.items(): + if k in keys: + result[keys[k]] = v + else: + for k, v in value.items(): + result[keys[k]] = self.value_field._serialize(v, None, None, **kwargs) + + return result + + def _deserialize(self, value, attr, data, **kwargs): + if not isinstance(value, _Mapping): + raise self.make_error("invalid") + if not self.value_field and not self.key_field: + return self.mapping_type(value) + + errors = collections.defaultdict(dict) + + #  Deserialize keys + if self.key_field is None: + keys = {k: k for k in value.keys()} + else: + keys = {} + for key in value.keys(): + try: + keys[key] = self.key_field.deserialize(key, **kwargs) + except ValidationError as error: + errors[key]["key"] = error.messages + + #  Deserialize values + result = self.mapping_type() + if self.value_field is None: + for k, v in value.items(): + if k in keys: + result[keys[k]] = v + else: + for key, val in value.items(): + try: + deser_val = self.value_field.deserialize(val, **kwargs) + except ValidationError as error: + errors[key]["value"] = error.messages + if error.valid_data is not None and key in keys: + result[keys[key]] = error.valid_data + else: + if key in keys: + result[keys[key]] = deser_val + + if errors: + raise ValidationError(errors, valid_data=result) + + return result + + +class Dict(Mapping): + """A dict field. Supports dicts and dict-like objects. Extends + Mapping with dict as the mapping_type. + + Example: :: + + numbers = fields.Dict(keys=fields.Str(), values=fields.Float()) + + :param kwargs: The same keyword arguments that :class:`Mapping` receives. + + .. versionadded:: 2.1.0 + """ + + mapping_type = dict + + +class Url(String): + """An URL field. + + :param default: Default value for the field if the attribute is not set. + :param relative: Whether to allow relative URLs. + :param require_tld: Whether to reject non-FQDN hostnames. + :param schemes: Valid schemes. By default, ``http``, ``https``, + ``ftp``, and ``ftps`` are allowed. + :param kwargs: The same keyword arguments that :class:`String` receives. + """ + + #: Default error messages. + default_error_messages = {"invalid": "Not a valid URL."} + + def __init__( + self, + *, + relative: bool = False, + absolute: bool = True, + schemes: types.StrSequenceOrSet | None = None, + require_tld: bool = True, + **kwargs, + ): + super().__init__(**kwargs) + + self.relative = relative + self.absolute = absolute + self.require_tld = require_tld + # Insert validation into self.validators so that multiple errors can be stored. + validator = validate.URL( + relative=self.relative, + absolute=self.absolute, + schemes=schemes, + require_tld=self.require_tld, + error=self.error_messages["invalid"], + ) + self.validators.insert(0, validator) + + +class Email(String): + """An email field. + + :param args: The same positional arguments that :class:`String` receives. + :param kwargs: The same keyword arguments that :class:`String` receives. + """ + + #: Default error messages. + default_error_messages = {"invalid": "Not a valid email address."} + + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + # Insert validation into self.validators so that multiple errors can be stored. + validator = validate.Email(error=self.error_messages["invalid"]) + self.validators.insert(0, validator) + + +class IP(Field): + """A IP address field. + + :param bool exploded: If `True`, serialize ipv6 address in long form, ie. with groups + consisting entirely of zeros included. + + .. versionadded:: 3.8.0 + """ + + default_error_messages = {"invalid_ip": "Not a valid IP address."} + + DESERIALIZATION_CLASS = None # type: typing.Optional[typing.Type] + + def __init__(self, *args, exploded=False, **kwargs): + super().__init__(*args, **kwargs) + self.exploded = exploded + + def _serialize(self, value, attr, obj, **kwargs) -> str | None: + if value is None: + return None + if self.exploded: + return value.exploded + return value.compressed + + def _deserialize( + self, value, attr, data, **kwargs + ) -> ipaddress.IPv4Address | ipaddress.IPv6Address | None: + if value is None: + return None + try: + return (self.DESERIALIZATION_CLASS or ipaddress.ip_address)( + utils.ensure_text_type(value) + ) + except (ValueError, TypeError) as error: + raise self.make_error("invalid_ip") from error + + +class IPv4(IP): + """A IPv4 address field. + + .. versionadded:: 3.8.0 + """ + + default_error_messages = {"invalid_ip": "Not a valid IPv4 address."} + + DESERIALIZATION_CLASS = ipaddress.IPv4Address + + +class IPv6(IP): + """A IPv6 address field. + + .. versionadded:: 3.8.0 + """ + + default_error_messages = {"invalid_ip": "Not a valid IPv6 address."} + + DESERIALIZATION_CLASS = ipaddress.IPv6Address + + +class IPInterface(Field): + """A IPInterface field. + + IP interface is the non-strict form of the IPNetwork type where arbitrary host + addresses are always accepted. + + IPAddress and mask e.g. '192.168.0.2/24' or '192.168.0.2/255.255.255.0' + + see https://python.readthedocs.io/en/latest/library/ipaddress.html#interface-objects + + :param bool exploded: If `True`, serialize ipv6 interface in long form, ie. with groups + consisting entirely of zeros included. + """ + + default_error_messages = {"invalid_ip_interface": "Not a valid IP interface."} + + DESERIALIZATION_CLASS = None # type: typing.Optional[typing.Type] + + def __init__(self, *args, exploded: bool = False, **kwargs): + super().__init__(*args, **kwargs) + self.exploded = exploded + + def _serialize(self, value, attr, obj, **kwargs) -> str | None: + if value is None: + return None + if self.exploded: + return value.exploded + return value.compressed + + def _deserialize(self, value, attr, data, **kwargs) -> None | ( + ipaddress.IPv4Interface | ipaddress.IPv6Interface + ): + if value is None: + return None + try: + return (self.DESERIALIZATION_CLASS or ipaddress.ip_interface)( + utils.ensure_text_type(value) + ) + except (ValueError, TypeError) as error: + raise self.make_error("invalid_ip_interface") from error + + +class IPv4Interface(IPInterface): + """A IPv4 Network Interface field.""" + + default_error_messages = {"invalid_ip_interface": "Not a valid IPv4 interface."} + + DESERIALIZATION_CLASS = ipaddress.IPv4Interface + + +class IPv6Interface(IPInterface): + """A IPv6 Network Interface field.""" + + default_error_messages = {"invalid_ip_interface": "Not a valid IPv6 interface."} + + DESERIALIZATION_CLASS = ipaddress.IPv6Interface + + +class Enum(Field): + """An Enum field (de)serializing enum members by symbol (name) or by value. + + :param enum Enum: Enum class + :param boolean|Schema|Field by_value: Whether to (de)serialize by value or by name, + or Field class or instance to use to (de)serialize by value. Defaults to False. + + If `by_value` is `False` (default), enum members are (de)serialized by symbol (name). + If it is `True`, they are (de)serialized by value using :class:`Field`. + If it is a field instance or class, they are (de)serialized by value using this field. + + .. versionadded:: 3.18.0 + """ + + default_error_messages = { + "unknown": "Must be one of: {choices}.", + } + + def __init__( + self, + enum: type[EnumType], + *, + by_value: bool | Field | type = False, + **kwargs, + ): + super().__init__(**kwargs) + self.enum = enum + self.by_value = by_value + + # Serialization by name + if by_value is False: + self.field: Field = String() + self.choices_text = ", ".join( + str(self.field._serialize(m, None, None)) for m in enum.__members__ + ) + # Serialization by value + else: + if by_value is True: + self.field = Field() + else: + try: + self.field = resolve_field_instance(by_value) + except FieldInstanceResolutionError as error: + raise ValueError( + '"by_value" must be either a bool or a subclass or instance of ' + "marshmallow.base.FieldABC." + ) from error + self.choices_text = ", ".join( + str(self.field._serialize(m.value, None, None)) for m in enum + ) + + def _serialize(self, value, attr, obj, **kwargs): + if value is None: + return None + if self.by_value: + val = value.value + else: + val = value.name + return self.field._serialize(val, attr, obj, **kwargs) + + def _deserialize(self, value, attr, data, **kwargs): + val = self.field._deserialize(value, attr, data, **kwargs) + if self.by_value: + try: + return self.enum(val) + except ValueError as error: + raise self.make_error("unknown", choices=self.choices_text) from error + try: + return getattr(self.enum, val) + except AttributeError as error: + raise self.make_error("unknown", choices=self.choices_text) from error + + +class Method(Field): + """A field that takes the value returned by a `Schema` method. + + :param str serialize: The name of the Schema method from which + to retrieve the value. The method must take an argument ``obj`` + (in addition to self) that is the object to be serialized. + :param str deserialize: Optional name of the Schema method for deserializing + a value The method must take a single argument ``value``, which is the + value to deserialize. + + .. versionchanged:: 2.0.0 + Removed optional ``context`` parameter on methods. Use ``self.context`` instead. + + .. versionchanged:: 2.3.0 + Deprecated ``method_name`` parameter in favor of ``serialize`` and allow + ``serialize`` to not be passed at all. + + .. versionchanged:: 3.0.0 + Removed ``method_name`` parameter. + """ + + _CHECK_ATTRIBUTE = False + + def __init__( + self, + serialize: str | None = None, + deserialize: str | None = None, + **kwargs, + ): + # Set dump_only and load_only based on arguments + kwargs["dump_only"] = bool(serialize) and not bool(deserialize) + kwargs["load_only"] = bool(deserialize) and not bool(serialize) + super().__init__(**kwargs) + self.serialize_method_name = serialize + self.deserialize_method_name = deserialize + self._serialize_method = None + self._deserialize_method = None + + def _bind_to_schema(self, field_name, schema): + if self.serialize_method_name: + self._serialize_method = utils.callable_or_raise( + getattr(schema, self.serialize_method_name) + ) + + if self.deserialize_method_name: + self._deserialize_method = utils.callable_or_raise( + getattr(schema, self.deserialize_method_name) + ) + + super()._bind_to_schema(field_name, schema) + + def _serialize(self, value, attr, obj, **kwargs): + if self._serialize_method is not None: + return self._serialize_method(obj) + return missing_ + + def _deserialize(self, value, attr, data, **kwargs): + if self._deserialize_method is not None: + return self._deserialize_method(value) + return value + + +class Function(Field): + """A field that takes the value returned by a function. + + :param serialize: A callable from which to retrieve the value. + The function must take a single argument ``obj`` which is the object + to be serialized. It can also optionally take a ``context`` argument, + which is a dictionary of context variables passed to the serializer. + If no callable is provided then the ```load_only``` flag will be set + to True. + :param deserialize: A callable from which to retrieve the value. + The function must take a single argument ``value`` which is the value + to be deserialized. It can also optionally take a ``context`` argument, + which is a dictionary of context variables passed to the deserializer. + If no callable is provided then ```value``` will be passed through + unchanged. + + .. versionchanged:: 2.3.0 + Deprecated ``func`` parameter in favor of ``serialize``. + + .. versionchanged:: 3.0.0a1 + Removed ``func`` parameter. + """ + + _CHECK_ATTRIBUTE = False + + def __init__( + self, + serialize: ( + None + | typing.Callable[[typing.Any], typing.Any] + | typing.Callable[[typing.Any, dict], typing.Any] + ) = None, + deserialize: ( + None + | typing.Callable[[typing.Any], typing.Any] + | typing.Callable[[typing.Any, dict], typing.Any] + ) = None, + **kwargs, + ): + # Set dump_only and load_only based on arguments + kwargs["dump_only"] = bool(serialize) and not bool(deserialize) + kwargs["load_only"] = bool(deserialize) and not bool(serialize) + super().__init__(**kwargs) + self.serialize_func = serialize and utils.callable_or_raise(serialize) + self.deserialize_func = deserialize and utils.callable_or_raise(deserialize) + + def _serialize(self, value, attr, obj, **kwargs): + return self._call_or_raise(self.serialize_func, obj, attr) + + def _deserialize(self, value, attr, data, **kwargs): + if self.deserialize_func: + return self._call_or_raise(self.deserialize_func, value, attr) + return value + + def _call_or_raise(self, func, value, attr): + if len(utils.get_func_args(func)) > 1: + if self.parent.context is None: + msg = f"No context available for Function field {attr!r}" + raise ValidationError(msg) + return func(value, self.parent.context) + return func(value) + + +class Constant(Field): + """A field that (de)serializes to a preset constant. If you only want the + constant added for serialization or deserialization, you should use + ``dump_only=True`` or ``load_only=True`` respectively. + + :param constant: The constant to return for the field attribute. + + .. versionadded:: 2.0.0 + """ + + _CHECK_ATTRIBUTE = False + + def __init__(self, constant: typing.Any, **kwargs): + super().__init__(**kwargs) + self.constant = constant + self.load_default = constant + self.dump_default = constant + + def _serialize(self, value, *args, **kwargs): + return self.constant + + def _deserialize(self, value, *args, **kwargs): + return self.constant + + +class Inferred(Field): + """A field that infers how to serialize, based on the value type. + + .. warning:: + + This class is treated as private API. + Users should not need to use this class directly. + """ + + def __init__(self): + super().__init__() + # We memoize the fields to avoid creating and binding new fields + # every time on serialization. + self._field_cache = {} + + def _serialize(self, value, attr, obj, **kwargs): + field_cls = self.root.TYPE_MAPPING.get(type(value)) + if field_cls is None: + field = super() + else: + field = self._field_cache.get(field_cls) + if field is None: + field = field_cls() + field._bind_to_schema(self.name, self.parent) + self._field_cache[field_cls] = field + return field._serialize(value, attr, obj, **kwargs) + + +# Aliases +URL = Url +Str = String +Bool = Boolean +Int = Integer diff --git a/ccxt/static_dependencies/marshmallow/orderedset.py b/ccxt/static_dependencies/marshmallow/orderedset.py new file mode 100644 index 0000000..7ce0723 --- /dev/null +++ b/ccxt/static_dependencies/marshmallow/orderedset.py @@ -0,0 +1,89 @@ +# OrderedSet +# Copyright (c) 2009 Raymond Hettinger +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +from collections.abc import MutableSet + + +class OrderedSet(MutableSet): + def __init__(self, iterable=None): + self.end = end = [] + end += [None, end, end] # sentinel node for doubly linked list + self.map = {} # key --> [key, prev, next] + if iterable is not None: + self |= iterable + + def __len__(self): + return len(self.map) + + def __contains__(self, key): + return key in self.map + + def add(self, key): + if key not in self.map: + end = self.end + curr = end[1] + curr[2] = end[1] = self.map[key] = [key, curr, end] + + def discard(self, key): + if key in self.map: + key, prev, next = self.map.pop(key) + prev[2] = next + next[1] = prev + + def __iter__(self): + end = self.end + curr = end[2] + while curr is not end: + yield curr[0] + curr = curr[2] + + def __reversed__(self): + end = self.end + curr = end[1] + while curr is not end: + yield curr[0] + curr = curr[1] + + def pop(self, last=True): + if not self: + raise KeyError("set is empty") + key = self.end[1][0] if last else self.end[2][0] + self.discard(key) + return key + + def __repr__(self): + if not self: + return f"{self.__class__.__name__}()" + return f"{self.__class__.__name__}({list(self)!r})" + + def __eq__(self, other): + if isinstance(other, OrderedSet): + return len(self) == len(other) and list(self) == list(other) + return set(self) == set(other) + + +if __name__ == "__main__": + s = OrderedSet("abracadaba") + t = OrderedSet("simsalabim") + print(s | t) + print(s & t) + print(s - t) diff --git a/ccxt/static_dependencies/marshmallow/py.typed b/ccxt/static_dependencies/marshmallow/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/ccxt/static_dependencies/marshmallow/schema.py b/ccxt/static_dependencies/marshmallow/schema.py new file mode 100644 index 0000000..b229f08 --- /dev/null +++ b/ccxt/static_dependencies/marshmallow/schema.py @@ -0,0 +1,1228 @@ +"""The :class:`Schema` class, including its metaclass and options (class Meta).""" + +from __future__ import annotations + +import copy +import datetime as dt +import decimal +import inspect +import json +import typing +import uuid +import warnings +from abc import ABCMeta +from collections import OrderedDict, defaultdict +from collections.abc import Mapping + +from . import base, class_registry, types +from . import fields as ma_fields +from .decorators import ( + POST_DUMP, + POST_LOAD, + PRE_DUMP, + PRE_LOAD, + VALIDATES, + VALIDATES_SCHEMA, +) +from .error_store import ErrorStore +from .exceptions import StringNotCollectionError, ValidationError +from .orderedset import OrderedSet +from .utils import ( + EXCLUDE, + INCLUDE, + RAISE, + get_value, + is_collection, + is_instance_or_subclass, + missing, + set_value, + validate_unknown_parameter_value, +) +from .warnings import RemovedInMarshmallow4Warning + +_T = typing.TypeVar("_T") + + +def _get_fields(attrs): + """Get fields from a class + + :param attrs: Mapping of class attributes + """ + return [ + (field_name, field_value) + for field_name, field_value in attrs.items() + if is_instance_or_subclass(field_value, base.FieldABC) + ] + + +# This function allows Schemas to inherit from non-Schema classes and ensures +# inheritance according to the MRO +def _get_fields_by_mro(klass): + """Collect fields from a class, following its method resolution order. The + class itself is excluded from the search; only its parents are checked. Get + fields from ``_declared_fields`` if available, else use ``__dict__``. + + :param type klass: Class whose fields to retrieve + """ + mro = inspect.getmro(klass) + # Loop over mro in reverse to maintain correct order of fields + return sum( + ( + _get_fields( + getattr(base, "_declared_fields", base.__dict__), + ) + for base in mro[:0:-1] + ), + [], + ) + + +class SchemaMeta(ABCMeta): + """Metaclass for the Schema class. Binds the declared fields to + a ``_declared_fields`` attribute, which is a dictionary mapping attribute + names to field objects. Also sets the ``opts`` class attribute, which is + the Schema class's ``class Meta`` options. + """ + + def __new__(mcs, name, bases, attrs): + meta = attrs.get("Meta") + ordered = getattr(meta, "ordered", False) + if not ordered: + # Inherit 'ordered' option + # Warning: We loop through bases instead of MRO because we don't + # yet have access to the class object + # (i.e. can't call super before we have fields) + for base_ in bases: + if hasattr(base_, "Meta") and hasattr(base_.Meta, "ordered"): + ordered = base_.Meta.ordered + break + else: + ordered = False + cls_fields = _get_fields(attrs) + # Remove fields from list of class attributes to avoid shadowing + # Schema attributes/methods in case of name conflict + for field_name, _ in cls_fields: + del attrs[field_name] + klass = super().__new__(mcs, name, bases, attrs) + inherited_fields = _get_fields_by_mro(klass) + + meta = klass.Meta + # Set klass.opts in __new__ rather than __init__ so that it is accessible in + # get_declared_fields + klass.opts = klass.OPTIONS_CLASS(meta, ordered=ordered) + # Add fields specified in the `include` class Meta option + cls_fields += list(klass.opts.include.items()) + + # Assign _declared_fields on class + klass._declared_fields = mcs.get_declared_fields( + klass=klass, + cls_fields=cls_fields, + inherited_fields=inherited_fields, + dict_cls=dict, + ) + return klass + + @classmethod + def get_declared_fields( + mcs, + klass: type, + cls_fields: list, + inherited_fields: list, + dict_cls: type = dict, + ): + """Returns a dictionary of field_name => `Field` pairs declared on the class. + This is exposed mainly so that plugins can add additional fields, e.g. fields + computed from class Meta options. + + :param klass: The class object. + :param cls_fields: The fields declared on the class, including those added + by the ``include`` class Meta option. + :param inherited_fields: Inherited fields. + :param dict_cls: dict-like class to use for dict output Default to ``dict``. + """ + return dict_cls(inherited_fields + cls_fields) + + def __init__(cls, name, bases, attrs): + super().__init__(name, bases, attrs) + if name and cls.opts.register: + class_registry.register(name, cls) + cls._hooks = cls.resolve_hooks() + + def resolve_hooks(cls) -> dict[types.Tag, list[str]]: + """Add in the decorated processors + + By doing this after constructing the class, we let standard inheritance + do all the hard work. + """ + mro = inspect.getmro(cls) + + hooks = defaultdict(list) # type: typing.Dict[types.Tag, typing.List[str]] + + for attr_name in dir(cls): + # Need to look up the actual descriptor, not whatever might be + # bound to the class. This needs to come from the __dict__ of the + # declaring class. + for parent in mro: + try: + attr = parent.__dict__[attr_name] + except KeyError: + continue + else: + break + else: + # In case we didn't find the attribute and didn't break above. + # We should never hit this - it's just here for completeness + # to exclude the possibility of attr being undefined. + continue + + try: + hook_config = attr.__marshmallow_hook__ + except AttributeError: + pass + else: + for key in hook_config.keys(): + # Use name here so we can get the bound method later, in + # case the processor was a descriptor or something. + hooks[key].append(attr_name) + + return hooks + + +class SchemaOpts: + """class Meta options for the :class:`Schema`. Defines defaults.""" + + def __init__(self, meta, ordered: bool = False): + self.fields = getattr(meta, "fields", ()) + if not isinstance(self.fields, (list, tuple)): + raise ValueError("`fields` option must be a list or tuple.") + self.additional = getattr(meta, "additional", ()) + if not isinstance(self.additional, (list, tuple)): + raise ValueError("`additional` option must be a list or tuple.") + if self.fields and self.additional: + raise ValueError( + "Cannot set both `fields` and `additional` options" + " for the same Schema." + ) + self.exclude = getattr(meta, "exclude", ()) + if not isinstance(self.exclude, (list, tuple)): + raise ValueError("`exclude` must be a list or tuple.") + self.dateformat = getattr(meta, "dateformat", None) + self.datetimeformat = getattr(meta, "datetimeformat", None) + self.timeformat = getattr(meta, "timeformat", None) + if hasattr(meta, "json_module"): + warnings.warn( + "The json_module class Meta option is deprecated. Use render_module instead.", + RemovedInMarshmallow4Warning, + stacklevel=2, + ) + render_module = getattr(meta, "json_module", json) + else: + render_module = json + self.render_module = getattr(meta, "render_module", render_module) + self.ordered = getattr(meta, "ordered", ordered) + self.index_errors = getattr(meta, "index_errors", True) + self.include = getattr(meta, "include", {}) + self.load_only = getattr(meta, "load_only", ()) + self.dump_only = getattr(meta, "dump_only", ()) + self.unknown = validate_unknown_parameter_value(getattr(meta, "unknown", RAISE)) + self.register = getattr(meta, "register", True) + + +class Schema(base.SchemaABC, metaclass=SchemaMeta): + """Base schema class with which to define custom schemas. + + Example usage: + + .. code-block:: python + + import datetime as dt + from dataclasses import dataclass + + from . import Schema, fields + + + @dataclass + class Album: + title: str + release_date: dt.date + + + class AlbumSchema(Schema): + title = fields.Str() + release_date = fields.Date() + + + album = Album("Beggars Banquet", dt.date(1968, 12, 6)) + schema = AlbumSchema() + data = schema.dump(album) + data # {'release_date': '1968-12-06', 'title': 'Beggars Banquet'} + + :param only: Whitelist of the declared fields to select when + instantiating the Schema. If None, all fields are used. Nested fields + can be represented with dot delimiters. + :param exclude: Blacklist of the declared fields to exclude + when instantiating the Schema. If a field appears in both `only` and + `exclude`, it is not used. Nested fields can be represented with dot + delimiters. + :param many: Should be set to `True` if ``obj`` is a collection + so that the object will be serialized to a list. + :param context: Optional context passed to :class:`fields.Method` and + :class:`fields.Function` fields. + :param load_only: Fields to skip during serialization (write-only fields) + :param dump_only: Fields to skip during deserialization (read-only fields) + :param partial: Whether to ignore missing fields and not require + any fields declared. Propagates down to ``Nested`` fields as well. If + its value is an iterable, only missing fields listed in that iterable + will be ignored. Use dot delimiters to specify nested fields. + :param unknown: Whether to exclude, include, or raise an error for unknown + fields in the data. Use `EXCLUDE`, `INCLUDE` or `RAISE`. + + .. versionchanged:: 3.0.0 + `prefix` parameter removed. + + .. versionchanged:: 2.0.0 + `__validators__`, `__preprocessors__`, and `__data_handlers__` are removed in favor of + `marshmallow.decorators.validates_schema`, + `marshmallow.decorators.pre_load` and `marshmallow.decorators.post_dump`. + `__accessor__` and `__error_handler__` are deprecated. Implement the + `handle_error` and `get_attribute` methods instead. + """ + + TYPE_MAPPING = { + str: ma_fields.String, + bytes: ma_fields.String, + dt.datetime: ma_fields.DateTime, + float: ma_fields.Float, + bool: ma_fields.Boolean, + tuple: ma_fields.Raw, + list: ma_fields.Raw, + set: ma_fields.Raw, + int: ma_fields.Integer, + uuid.UUID: ma_fields.UUID, + dt.time: ma_fields.Time, + dt.date: ma_fields.Date, + dt.timedelta: ma_fields.TimeDelta, + decimal.Decimal: ma_fields.Decimal, + } # type: typing.Dict[type, typing.Type[ma_fields.Field]] + #: Overrides for default schema-level error messages + error_messages = {} # type: typing.Dict[str, str] + + _default_error_messages = { + "type": "Invalid input type.", + "unknown": "Unknown field.", + } # type: typing.Dict[str, str] + + OPTIONS_CLASS = SchemaOpts # type: type + + set_class = OrderedSet + + # These get set by SchemaMeta + opts = None # type: SchemaOpts + _declared_fields = {} # type: typing.Dict[str, ma_fields.Field] + _hooks = {} # type: typing.Dict[types.Tag, typing.List[str]] + + class Meta: + """Options object for a Schema. + + Example usage: :: + + class Meta: + fields = ("id", "email", "date_created") + exclude = ("password", "secret_attribute") + + Available options: + + - ``fields``: Tuple or list of fields to include in the serialized result. + - ``additional``: Tuple or list of fields to include *in addition* to the + explicitly declared fields. ``additional`` and ``fields`` are + mutually-exclusive options. + - ``include``: Dictionary of additional fields to include in the schema. It is + usually better to define fields as class variables, but you may need to + use this option, e.g., if your fields are Python keywords. May be an + `OrderedDict`. + - ``exclude``: Tuple or list of fields to exclude in the serialized result. + Nested fields can be represented with dot delimiters. + - ``dateformat``: Default format for `Date ` fields. + - ``datetimeformat``: Default format for `DateTime ` fields. + - ``timeformat``: Default format for `Time ` fields. + - ``render_module``: Module to use for `loads ` and `dumps `. + Defaults to `json` from the standard library. + - ``ordered``: If `True`, output of `Schema.dump` will be a `collections.OrderedDict`. + - ``index_errors``: If `True`, errors dictionaries will include the index + of invalid items in a collection. + - ``load_only``: Tuple or list of fields to exclude from serialized results. + - ``dump_only``: Tuple or list of fields to exclude from deserialization + - ``unknown``: Whether to exclude, include, or raise an error for unknown + fields in the data. Use `EXCLUDE`, `INCLUDE` or `RAISE`. + - ``register``: Whether to register the `Schema` with marshmallow's internal + class registry. Must be `True` if you intend to refer to this `Schema` + by class name in `Nested` fields. Only set this to `False` when memory + usage is critical. Defaults to `True`. + """ + + def __init__( + self, + *, + only: types.StrSequenceOrSet | None = None, + exclude: types.StrSequenceOrSet = (), + many: bool = False, + context: dict | None = None, + load_only: types.StrSequenceOrSet = (), + dump_only: types.StrSequenceOrSet = (), + partial: bool | types.StrSequenceOrSet | None = None, + unknown: str | None = None, + ): + # Raise error if only or exclude is passed as string, not list of strings + if only is not None and not is_collection(only): + raise StringNotCollectionError('"only" should be a list of strings') + if not is_collection(exclude): + raise StringNotCollectionError('"exclude" should be a list of strings') + # copy declared fields from metaclass + self.declared_fields = copy.deepcopy(self._declared_fields) + self.many = many + self.only = only + self.exclude: set[typing.Any] | typing.MutableSet[typing.Any] = set( + self.opts.exclude + ) | set(exclude) + self.ordered = self.opts.ordered + self.load_only = set(load_only) or set(self.opts.load_only) + self.dump_only = set(dump_only) or set(self.opts.dump_only) + self.partial = partial + self.unknown = ( + self.opts.unknown + if unknown is None + else validate_unknown_parameter_value(unknown) + ) + self.context = context or {} + self._normalize_nested_options() + #: Dictionary mapping field_names -> :class:`Field` objects + self.fields = {} # type: typing.Dict[str, ma_fields.Field] + self.load_fields = {} # type: typing.Dict[str, ma_fields.Field] + self.dump_fields = {} # type: typing.Dict[str, ma_fields.Field] + self._init_fields() + messages = {} + messages.update(self._default_error_messages) + for cls in reversed(self.__class__.__mro__): + messages.update(getattr(cls, "error_messages", {})) + messages.update(self.error_messages or {}) + self.error_messages = messages + + def __repr__(self) -> str: + return f"<{self.__class__.__name__}(many={self.many})>" + + @property + def dict_class(self) -> type: + return OrderedDict if self.ordered else dict + + @classmethod + def from_dict( + cls, + fields: dict[str, ma_fields.Field | type], + *, + name: str = "GeneratedSchema", + ) -> type: + """Generate a `Schema` class given a dictionary of fields. + + .. code-block:: python + + from . import Schema, fields + + PersonSchema = Schema.from_dict({"name": fields.Str()}) + print(PersonSchema().load({"name": "David"})) # => {'name': 'David'} + + Generated schemas are not added to the class registry and therefore cannot + be referred to by name in `Nested` fields. + + :param dict fields: Dictionary mapping field names to field instances. + :param str name: Optional name for the class, which will appear in + the ``repr`` for the class. + + .. versionadded:: 3.0.0 + """ + attrs = fields.copy() + attrs["Meta"] = type( + "GeneratedMeta", (getattr(cls, "Meta", object),), {"register": False} + ) + schema_cls = type(name, (cls,), attrs) + return schema_cls + + ##### Override-able methods ##### + + def handle_error( + self, error: ValidationError, data: typing.Any, *, many: bool, **kwargs + ): + """Custom error handler function for the schema. + + :param error: The `ValidationError` raised during (de)serialization. + :param data: The original input data. + :param many: Value of ``many`` on dump or load. + :param partial: Value of ``partial`` on load. + + .. versionadded:: 2.0.0 + + .. versionchanged:: 3.0.0rc9 + Receives `many` and `partial` (on deserialization) as keyword arguments. + """ + pass + + def get_attribute(self, obj: typing.Any, attr: str, default: typing.Any): + """Defines how to pull values from an object to serialize. + + .. versionadded:: 2.0.0 + + .. versionchanged:: 3.0.0a1 + Changed position of ``obj`` and ``attr``. + """ + return get_value(obj, attr, default) + + ##### Serialization/Deserialization API ##### + + @staticmethod + def _call_and_store(getter_func, data, *, field_name, error_store, index=None): + """Call ``getter_func`` with ``data`` as its argument, and store any `ValidationErrors`. + + :param callable getter_func: Function for getting the serialized/deserialized + value from ``data``. + :param data: The data passed to ``getter_func``. + :param str field_name: Field name. + :param int index: Index of the item being validated, if validating a collection, + otherwise `None`. + """ + try: + value = getter_func(data) + except ValidationError as error: + error_store.store_error(error.messages, field_name, index=index) + # When a Nested field fails validation, the marshalled data is stored + # on the ValidationError's valid_data attribute + return error.valid_data or missing + return value + + def _serialize(self, obj: _T | typing.Iterable[_T], *, many: bool = False): + """Serialize ``obj``. + + :param obj: The object(s) to serialize. + :param bool many: `True` if ``data`` should be serialized as a collection. + :return: A dictionary of the serialized data + + .. versionchanged:: 1.0.0 + Renamed from ``marshal``. + """ + if many and obj is not None: + return [ + self._serialize(d, many=False) + for d in typing.cast(typing.Iterable[_T], obj) + ] + ret = self.dict_class() + for attr_name, field_obj in self.dump_fields.items(): + value = field_obj.serialize(attr_name, obj, accessor=self.get_attribute) + if value is missing: + continue + key = field_obj.data_key if field_obj.data_key is not None else attr_name + ret[key] = value + return ret + + def dump(self, obj: typing.Any, *, many: bool | None = None): + """Serialize an object to native Python data types according to this + Schema's fields. + + :param obj: The object to serialize. + :param many: Whether to serialize `obj` as a collection. If `None`, the value + for `self.many` is used. + :return: Serialized data + + .. versionadded:: 1.0.0 + .. versionchanged:: 3.0.0b7 + This method returns the serialized data rather than a ``(data, errors)`` duple. + A :exc:`ValidationError ` is raised + if ``obj`` is invalid. + .. versionchanged:: 3.0.0rc9 + Validation no longer occurs upon serialization. + """ + many = self.many if many is None else bool(many) + if self._has_processors(PRE_DUMP): + processed_obj = self._invoke_dump_processors( + PRE_DUMP, obj, many=many, original_data=obj + ) + else: + processed_obj = obj + + result = self._serialize(processed_obj, many=many) + + if self._has_processors(POST_DUMP): + result = self._invoke_dump_processors( + POST_DUMP, result, many=many, original_data=obj + ) + + return result + + def dumps(self, obj: typing.Any, *args, many: bool | None = None, **kwargs): + """Same as :meth:`dump`, except return a JSON-encoded string. + + :param obj: The object to serialize. + :param many: Whether to serialize `obj` as a collection. If `None`, the value + for `self.many` is used. + :return: A ``json`` string + + .. versionadded:: 1.0.0 + .. versionchanged:: 3.0.0b7 + This method returns the serialized data rather than a ``(data, errors)`` duple. + A :exc:`ValidationError ` is raised + if ``obj`` is invalid. + """ + serialized = self.dump(obj, many=many) + return self.opts.render_module.dumps(serialized, *args, **kwargs) + + def _deserialize( + self, + data: ( + typing.Mapping[str, typing.Any] + | typing.Iterable[typing.Mapping[str, typing.Any]] + ), + *, + error_store: ErrorStore, + many: bool = False, + partial=None, + unknown=RAISE, + index=None, + ) -> _T | list[_T]: + """Deserialize ``data``. + + :param dict data: The data to deserialize. + :param ErrorStore error_store: Structure to store errors. + :param bool many: `True` if ``data`` should be deserialized as a collection. + :param bool|tuple partial: Whether to ignore missing fields and not require + any fields declared. Propagates down to ``Nested`` fields as well. If + its value is an iterable, only missing fields listed in that iterable + will be ignored. Use dot delimiters to specify nested fields. + :param unknown: Whether to exclude, include, or raise an error for unknown + fields in the data. Use `EXCLUDE`, `INCLUDE` or `RAISE`. + :param int index: Index of the item being serialized (for storing errors) if + serializing a collection, otherwise `None`. + :return: A dictionary of the deserialized data. + """ + index_errors = self.opts.index_errors + index = index if index_errors else None + if many: + if not is_collection(data): + error_store.store_error([self.error_messages["type"]], index=index) + ret_l = [] # type: typing.List[_T] + else: + ret_l = [ + typing.cast( + _T, + self._deserialize( + typing.cast(typing.Mapping[str, typing.Any], d), + error_store=error_store, + many=False, + partial=partial, + unknown=unknown, + index=idx, + ), + ) + for idx, d in enumerate(data) + ] + return ret_l + ret_d = self.dict_class() + # Check data is a dict + if not isinstance(data, Mapping): + error_store.store_error([self.error_messages["type"]], index=index) + else: + partial_is_collection = is_collection(partial) + for attr_name, field_obj in self.load_fields.items(): + field_name = ( + field_obj.data_key if field_obj.data_key is not None else attr_name + ) + raw_value = data.get(field_name, missing) + if raw_value is missing: + # Ignore missing field if we're allowed to. + if partial is True or ( + partial_is_collection and attr_name in partial + ): + continue + d_kwargs = {} + # Allow partial loading of nested schemas. + if partial_is_collection: + prefix = field_name + "." + len_prefix = len(prefix) + sub_partial = [ + f[len_prefix:] for f in partial if f.startswith(prefix) + ] + d_kwargs["partial"] = sub_partial + elif partial is not None: + d_kwargs["partial"] = partial + + def getter( + val, field_obj=field_obj, field_name=field_name, d_kwargs=d_kwargs + ): + return field_obj.deserialize( + val, + field_name, + data, + **d_kwargs, + ) + + value = self._call_and_store( + getter_func=getter, + data=raw_value, + field_name=field_name, + error_store=error_store, + index=index, + ) + if value is not missing: + key = field_obj.attribute or attr_name + set_value(ret_d, key, value) + if unknown != EXCLUDE: + fields = { + field_obj.data_key if field_obj.data_key is not None else field_name + for field_name, field_obj in self.load_fields.items() + } + for key in set(data) - fields: + value = data[key] + if unknown == INCLUDE: + ret_d[key] = value + elif unknown == RAISE: + error_store.store_error( + [self.error_messages["unknown"]], + key, + (index if index_errors else None), + ) + return ret_d + + def load( + self, + data: ( + typing.Mapping[str, typing.Any] + | typing.Iterable[typing.Mapping[str, typing.Any]] + ), + *, + many: bool | None = None, + partial: bool | types.StrSequenceOrSet | None = None, + unknown: str | None = None, + ): + """Deserialize a data structure to an object defined by this Schema's fields. + + :param data: The data to deserialize. + :param many: Whether to deserialize `data` as a collection. If `None`, the + value for `self.many` is used. + :param partial: Whether to ignore missing fields and not require + any fields declared. Propagates down to ``Nested`` fields as well. If + its value is an iterable, only missing fields listed in that iterable + will be ignored. Use dot delimiters to specify nested fields. + :param unknown: Whether to exclude, include, or raise an error for unknown + fields in the data. Use `EXCLUDE`, `INCLUDE` or `RAISE`. + If `None`, the value for `self.unknown` is used. + :return: Deserialized data + + .. versionadded:: 1.0.0 + .. versionchanged:: 3.0.0b7 + This method returns the deserialized data rather than a ``(data, errors)`` duple. + A :exc:`ValidationError ` is raised + if invalid data are passed. + """ + return self._do_load( + data, many=many, partial=partial, unknown=unknown, postprocess=True + ) + + def loads( + self, + json_data: str, + *, + many: bool | None = None, + partial: bool | types.StrSequenceOrSet | None = None, + unknown: str | None = None, + **kwargs, + ): + """Same as :meth:`load`, except it takes a JSON string as input. + + :param json_data: A JSON string of the data to deserialize. + :param many: Whether to deserialize `obj` as a collection. If `None`, the + value for `self.many` is used. + :param partial: Whether to ignore missing fields and not require + any fields declared. Propagates down to ``Nested`` fields as well. If + its value is an iterable, only missing fields listed in that iterable + will be ignored. Use dot delimiters to specify nested fields. + :param unknown: Whether to exclude, include, or raise an error for unknown + fields in the data. Use `EXCLUDE`, `INCLUDE` or `RAISE`. + If `None`, the value for `self.unknown` is used. + :return: Deserialized data + + .. versionadded:: 1.0.0 + .. versionchanged:: 3.0.0b7 + This method returns the deserialized data rather than a ``(data, errors)`` duple. + A :exc:`ValidationError ` is raised + if invalid data are passed. + """ + data = self.opts.render_module.loads(json_data, **kwargs) + return self.load(data, many=many, partial=partial, unknown=unknown) + + def _run_validator( + self, + validator_func, + output, + *, + original_data, + error_store, + many, + partial, + pass_original, + index=None, + ): + try: + if pass_original: # Pass original, raw data (before unmarshalling) + validator_func(output, original_data, partial=partial, many=many) + else: + validator_func(output, partial=partial, many=many) + except ValidationError as err: + error_store.store_error(err.messages, err.field_name, index=index) + + def validate( + self, + data: ( + typing.Mapping[str, typing.Any] + | typing.Iterable[typing.Mapping[str, typing.Any]] + ), + *, + many: bool | None = None, + partial: bool | types.StrSequenceOrSet | None = None, + ) -> dict[str, list[str]]: + """Validate `data` against the schema, returning a dictionary of + validation errors. + + :param data: The data to validate. + :param many: Whether to validate `data` as a collection. If `None`, the + value for `self.many` is used. + :param partial: Whether to ignore missing fields and not require + any fields declared. Propagates down to ``Nested`` fields as well. If + its value is an iterable, only missing fields listed in that iterable + will be ignored. Use dot delimiters to specify nested fields. + :return: A dictionary of validation errors. + + .. versionadded:: 1.1.0 + """ + try: + self._do_load(data, many=many, partial=partial, postprocess=False) + except ValidationError as exc: + return typing.cast(typing.Dict[str, typing.List[str]], exc.messages) + return {} + + ##### Private Helpers ##### + + def _do_load( + self, + data: ( + typing.Mapping[str, typing.Any] + | typing.Iterable[typing.Mapping[str, typing.Any]] + ), + *, + many: bool | None = None, + partial: bool | types.StrSequenceOrSet | None = None, + unknown: str | None = None, + postprocess: bool = True, + ): + """Deserialize `data`, returning the deserialized result. + This method is private API. + + :param data: The data to deserialize. + :param many: Whether to deserialize `data` as a collection. If `None`, the + value for `self.many` is used. + :param partial: Whether to validate required fields. If its + value is an iterable, only fields listed in that iterable will be + ignored will be allowed missing. If `True`, all fields will be allowed missing. + If `None`, the value for `self.partial` is used. + :param unknown: Whether to exclude, include, or raise an error for unknown + fields in the data. Use `EXCLUDE`, `INCLUDE` or `RAISE`. + If `None`, the value for `self.unknown` is used. + :param postprocess: Whether to run post_load methods.. + :return: Deserialized data + """ + error_store = ErrorStore() + errors = {} # type: dict[str, list[str]] + many = self.many if many is None else bool(many) + unknown = ( + self.unknown + if unknown is None + else validate_unknown_parameter_value(unknown) + ) + if partial is None: + partial = self.partial + # Run preprocessors + if self._has_processors(PRE_LOAD): + try: + processed_data = self._invoke_load_processors( + PRE_LOAD, data, many=many, original_data=data, partial=partial + ) + except ValidationError as err: + errors = err.normalized_messages() + result = None # type: list | dict | None + else: + processed_data = data + if not errors: + # Deserialize data + result = self._deserialize( + processed_data, + error_store=error_store, + many=many, + partial=partial, + unknown=unknown, + ) + # Run field-level validation + self._invoke_field_validators( + error_store=error_store, data=result, many=many + ) + # Run schema-level validation + if self._has_processors(VALIDATES_SCHEMA): + field_errors = bool(error_store.errors) + self._invoke_schema_validators( + error_store=error_store, + pass_many=True, + data=result, + original_data=data, + many=many, + partial=partial, + field_errors=field_errors, + ) + self._invoke_schema_validators( + error_store=error_store, + pass_many=False, + data=result, + original_data=data, + many=many, + partial=partial, + field_errors=field_errors, + ) + errors = error_store.errors + # Run post processors + if not errors and postprocess and self._has_processors(POST_LOAD): + try: + result = self._invoke_load_processors( + POST_LOAD, + result, + many=many, + original_data=data, + partial=partial, + ) + except ValidationError as err: + errors = err.normalized_messages() + if errors: + exc = ValidationError(errors, data=data, valid_data=result) + self.handle_error(exc, data, many=many, partial=partial) + raise exc + + return result + + def _normalize_nested_options(self) -> None: + """Apply then flatten nested schema options. + This method is private API. + """ + if self.only is not None: + # Apply the only option to nested fields. + self.__apply_nested_option("only", self.only, "intersection") + # Remove the child field names from the only option. + self.only = self.set_class([field.split(".", 1)[0] for field in self.only]) + if self.exclude: + # Apply the exclude option to nested fields. + self.__apply_nested_option("exclude", self.exclude, "union") + # Remove the parent field names from the exclude option. + self.exclude = self.set_class( + [field for field in self.exclude if "." not in field] + ) + + def __apply_nested_option(self, option_name, field_names, set_operation) -> None: + """Apply nested options to nested fields""" + # Split nested field names on the first dot. + nested_fields = [name.split(".", 1) for name in field_names if "." in name] + # Partition the nested field names by parent field. + nested_options = defaultdict(list) # type: defaultdict + for parent, nested_names in nested_fields: + nested_options[parent].append(nested_names) + # Apply the nested field options. + for key, options in iter(nested_options.items()): + new_options = self.set_class(options) + original_options = getattr(self.declared_fields[key], option_name, ()) + if original_options: + if set_operation == "union": + new_options |= self.set_class(original_options) + if set_operation == "intersection": + new_options &= self.set_class(original_options) + setattr(self.declared_fields[key], option_name, new_options) + + def _init_fields(self) -> None: + """Update self.fields, self.load_fields, and self.dump_fields based on schema options. + This method is private API. + """ + if self.opts.fields: + available_field_names = self.set_class(self.opts.fields) + else: + available_field_names = self.set_class(self.declared_fields.keys()) + if self.opts.additional: + available_field_names |= self.set_class(self.opts.additional) + + invalid_fields = self.set_class() + + if self.only is not None: + # Return only fields specified in only option + field_names: typing.AbstractSet[typing.Any] = self.set_class(self.only) + + invalid_fields |= field_names - available_field_names + else: + field_names = available_field_names + + # If "exclude" option or param is specified, remove those fields. + if self.exclude: + # Note that this isn't available_field_names, since we want to + # apply "only" for the actual calculation. + field_names = field_names - self.exclude + invalid_fields |= self.exclude - available_field_names + + if invalid_fields: + message = f"Invalid fields for {self}: {invalid_fields}." + raise ValueError(message) + + fields_dict = self.dict_class() + for field_name in field_names: + field_obj = self.declared_fields.get(field_name, ma_fields.Inferred()) + self._bind_field(field_name, field_obj) + fields_dict[field_name] = field_obj + + load_fields, dump_fields = self.dict_class(), self.dict_class() + for field_name, field_obj in fields_dict.items(): + if not field_obj.dump_only: + load_fields[field_name] = field_obj + if not field_obj.load_only: + dump_fields[field_name] = field_obj + + dump_data_keys = [ + field_obj.data_key if field_obj.data_key is not None else name + for name, field_obj in dump_fields.items() + ] + if len(dump_data_keys) != len(set(dump_data_keys)): + data_keys_duplicates = { + x for x in dump_data_keys if dump_data_keys.count(x) > 1 + } + raise ValueError( + "The data_key argument for one or more fields collides " + "with another field's name or data_key argument. " + "Check the following field names and " + f"data_key arguments: {list(data_keys_duplicates)}" + ) + load_attributes = [obj.attribute or name for name, obj in load_fields.items()] + if len(load_attributes) != len(set(load_attributes)): + attributes_duplicates = { + x for x in load_attributes if load_attributes.count(x) > 1 + } + raise ValueError( + "The attribute argument for one or more fields collides " + "with another field's name or attribute argument. " + "Check the following field names and " + f"attribute arguments: {list(attributes_duplicates)}" + ) + + self.fields = fields_dict + self.dump_fields = dump_fields + self.load_fields = load_fields + + def on_bind_field(self, field_name: str, field_obj: ma_fields.Field) -> None: + """Hook to modify a field when it is bound to the `Schema`. + + No-op by default. + """ + return None + + def _bind_field(self, field_name: str, field_obj: ma_fields.Field) -> None: + """Bind field to the schema, setting any necessary attributes on the + field (e.g. parent and name). + + Also set field load_only and dump_only values if field_name was + specified in ``class Meta``. + """ + if field_name in self.load_only: + field_obj.load_only = True + if field_name in self.dump_only: + field_obj.dump_only = True + try: + field_obj._bind_to_schema(field_name, self) + except TypeError as error: + # Field declared as a class, not an instance. Ignore type checking because + # we handle unsupported arg types, i.e. this is dead code from + # the type checker's perspective. + if isinstance(field_obj, type) and issubclass(field_obj, base.FieldABC): + msg = ( + f'Field for "{field_name}" must be declared as a ' + "Field instance, not a class. " + f'Did you mean "fields.{field_obj.__name__}()"?' # type: ignore + ) + raise TypeError(msg) from error + raise error + self.on_bind_field(field_name, field_obj) + + def _has_processors(self, tag) -> bool: + return bool(self._hooks[(tag, True)] or self._hooks[(tag, False)]) + + def _invoke_dump_processors( + self, tag: str, data, *, many: bool, original_data=None + ): + # The pass_many post-dump processors may do things like add an envelope, so + # invoke those after invoking the non-pass_many processors which will expect + # to get a list of items. + data = self._invoke_processors( + tag, pass_many=False, data=data, many=many, original_data=original_data + ) + data = self._invoke_processors( + tag, pass_many=True, data=data, many=many, original_data=original_data + ) + return data + + def _invoke_load_processors( + self, + tag: str, + data, + *, + many: bool, + original_data, + partial: bool | types.StrSequenceOrSet | None, + ): + # This has to invert the order of the dump processors, so run the pass_many + # processors first. + data = self._invoke_processors( + tag, + pass_many=True, + data=data, + many=many, + original_data=original_data, + partial=partial, + ) + data = self._invoke_processors( + tag, + pass_many=False, + data=data, + many=many, + original_data=original_data, + partial=partial, + ) + return data + + def _invoke_field_validators(self, *, error_store: ErrorStore, data, many: bool): + for attr_name in self._hooks[VALIDATES]: + validator = getattr(self, attr_name) + validator_kwargs = validator.__marshmallow_hook__[VALIDATES] + field_name = validator_kwargs["field_name"] + + try: + field_obj = self.fields[field_name] + except KeyError as error: + if field_name in self.declared_fields: + continue + raise ValueError(f'"{field_name}" field does not exist.') from error + + data_key = ( + field_obj.data_key if field_obj.data_key is not None else field_name + ) + if many: + for idx, item in enumerate(data): + try: + value = item[field_obj.attribute or field_name] + except KeyError: + pass + else: + validated_value = self._call_and_store( + getter_func=validator, + data=value, + field_name=data_key, + error_store=error_store, + index=(idx if self.opts.index_errors else None), + ) + if validated_value is missing: + data[idx].pop(field_name, None) + else: + try: + value = data[field_obj.attribute or field_name] + except KeyError: + pass + else: + validated_value = self._call_and_store( + getter_func=validator, + data=value, + field_name=data_key, + error_store=error_store, + ) + if validated_value is missing: + data.pop(field_name, None) + + def _invoke_schema_validators( + self, + *, + error_store: ErrorStore, + pass_many: bool, + data, + original_data, + many: bool, + partial: bool | types.StrSequenceOrSet | None, + field_errors: bool = False, + ): + for attr_name in self._hooks[(VALIDATES_SCHEMA, pass_many)]: + validator = getattr(self, attr_name) + validator_kwargs = validator.__marshmallow_hook__[ + (VALIDATES_SCHEMA, pass_many) + ] + if field_errors and validator_kwargs["skip_on_field_errors"]: + continue + pass_original = validator_kwargs.get("pass_original", False) + + if many and not pass_many: + for idx, (item, orig) in enumerate(zip(data, original_data)): + self._run_validator( + validator, + item, + original_data=orig, + error_store=error_store, + many=many, + partial=partial, + index=idx, + pass_original=pass_original, + ) + else: + self._run_validator( + validator, + data, + original_data=original_data, + error_store=error_store, + many=many, + pass_original=pass_original, + partial=partial, + ) + + def _invoke_processors( + self, + tag: str, + *, + pass_many: bool, + data, + many: bool, + original_data=None, + **kwargs, + ): + key = (tag, pass_many) + for attr_name in self._hooks[key]: + # This will be a bound method. + processor = getattr(self, attr_name) + + processor_kwargs = processor.__marshmallow_hook__[key] + pass_original = processor_kwargs.get("pass_original", False) + + if many and not pass_many: + if pass_original: + data = [ + processor(item, original, many=many, **kwargs) + for item, original in zip(data, original_data) + ] + else: + data = [processor(item, many=many, **kwargs) for item in data] + else: + if pass_original: + data = processor(data, original_data, many=many, **kwargs) + else: + data = processor(data, many=many, **kwargs) + return data + + +BaseSchema = Schema # for backwards compatibility diff --git a/ccxt/static_dependencies/marshmallow/types.py b/ccxt/static_dependencies/marshmallow/types.py new file mode 100644 index 0000000..ce31c05 --- /dev/null +++ b/ccxt/static_dependencies/marshmallow/types.py @@ -0,0 +1,12 @@ +"""Type aliases. + +.. warning:: + + This module is provisional. Types may be modified, added, and removed between minor releases. +""" + +import typing + +StrSequenceOrSet = typing.Union[typing.Sequence[str], typing.AbstractSet[str]] +Tag = typing.Union[str, typing.Tuple[str, bool]] +Validator = typing.Callable[[typing.Any], typing.Any] diff --git a/ccxt/static_dependencies/marshmallow/utils.py b/ccxt/static_dependencies/marshmallow/utils.py new file mode 100644 index 0000000..591e96c --- /dev/null +++ b/ccxt/static_dependencies/marshmallow/utils.py @@ -0,0 +1,378 @@ +"""Utility methods for marshmallow.""" + +from __future__ import annotations + +import collections +import datetime as dt +import functools +import inspect +import json +import re +import typing +import warnings +from collections.abc import Mapping +from email.utils import format_datetime, parsedate_to_datetime +from pprint import pprint as py_pprint + +from .base import FieldABC +from .exceptions import FieldInstanceResolutionError +from .warnings import RemovedInMarshmallow4Warning + +EXCLUDE = "exclude" +INCLUDE = "include" +RAISE = "raise" +_UNKNOWN_VALUES = {EXCLUDE, INCLUDE, RAISE} + + +class _Missing: + def __bool__(self): + return False + + def __copy__(self): + return self + + def __deepcopy__(self, _): + return self + + def __repr__(self): + return "" + + +# Singleton value that indicates that a field's value is missing from input +# dict passed to :meth:`Schema.load`. If the field's value is not required, +# it's ``default`` value is used. +missing = _Missing() + + +def is_generator(obj) -> bool: + """Return True if ``obj`` is a generator""" + return inspect.isgeneratorfunction(obj) or inspect.isgenerator(obj) + + +def is_iterable_but_not_string(obj) -> bool: + """Return True if ``obj`` is an iterable object that isn't a string.""" + return (hasattr(obj, "__iter__") and not hasattr(obj, "strip")) or is_generator(obj) + + +def is_collection(obj) -> bool: + """Return True if ``obj`` is a collection type, e.g list, tuple, queryset.""" + return is_iterable_but_not_string(obj) and not isinstance(obj, Mapping) + + +def is_instance_or_subclass(val, class_) -> bool: + """Return True if ``val`` is either a subclass or instance of ``class_``.""" + try: + return issubclass(val, class_) + except TypeError: + return isinstance(val, class_) + + +def is_keyed_tuple(obj) -> bool: + """Return True if ``obj`` has keyed tuple behavior, such as + namedtuples or SQLAlchemy's KeyedTuples. + """ + return isinstance(obj, tuple) and hasattr(obj, "_fields") + + +def pprint(obj, *args, **kwargs) -> None: + """Pretty-printing function that can pretty-print OrderedDicts + like regular dictionaries. Useful for printing the output of + :meth:`marshmallow.Schema.dump`. + + .. deprecated:: 3.7.0 + marshmallow.pprint will be removed in marshmallow 4. + """ + warnings.warn( + "marshmallow's pprint function is deprecated and will be removed in marshmallow 4.", + RemovedInMarshmallow4Warning, + stacklevel=2, + ) + if isinstance(obj, collections.OrderedDict): + print(json.dumps(obj, *args, **kwargs)) + else: + py_pprint(obj, *args, **kwargs) + + +# https://stackoverflow.com/a/27596917 +def is_aware(datetime: dt.datetime) -> bool: + return ( + datetime.tzinfo is not None and datetime.tzinfo.utcoffset(datetime) is not None + ) + + +def from_rfc(datestring: str) -> dt.datetime: + """Parse a RFC822-formatted datetime string and return a datetime object. + + https://stackoverflow.com/questions/885015/how-to-parse-a-rfc-2822-date-time-into-a-python-datetime # noqa: B950 + """ + return parsedate_to_datetime(datestring) + + +def rfcformat(datetime: dt.datetime) -> str: + """Return the RFC822-formatted representation of a datetime object. + + :param datetime datetime: The datetime. + """ + return format_datetime(datetime) + + +# Hat tip to Django for ISO8601 deserialization functions + +_iso8601_datetime_re = re.compile( + r"(?P\d{4})-(?P\d{1,2})-(?P\d{1,2})" + r"[T ](?P\d{1,2}):(?P\d{1,2})" + r"(?::(?P\d{1,2})(?:\.(?P\d{1,6})\d{0,6})?)?" + r"(?PZ|[+-]\d{2}(?::?\d{2})?)?$" +) + +_iso8601_date_re = re.compile(r"(?P\d{4})-(?P\d{1,2})-(?P\d{1,2})$") + +_iso8601_time_re = re.compile( + r"(?P\d{1,2}):(?P\d{1,2})" + r"(?::(?P\d{1,2})(?:\.(?P\d{1,6})\d{0,6})?)?" +) + + +def get_fixed_timezone(offset: int | float | dt.timedelta) -> dt.timezone: + """Return a tzinfo instance with a fixed offset from UTC.""" + if isinstance(offset, dt.timedelta): + offset = offset.total_seconds() // 60 + sign = "-" if offset < 0 else "+" + hhmm = "%02d%02d" % divmod(abs(offset), 60) + name = sign + hhmm + return dt.timezone(dt.timedelta(minutes=offset), name) + + +def from_iso_datetime(value): + """Parse a string and return a datetime.datetime. + + This function supports time zone offsets. When the input contains one, + the output uses a timezone with a fixed offset from UTC. + """ + match = _iso8601_datetime_re.match(value) + if not match: + raise ValueError("Not a valid ISO8601-formatted datetime string") + kw = match.groupdict() + kw["microsecond"] = kw["microsecond"] and kw["microsecond"].ljust(6, "0") + tzinfo = kw.pop("tzinfo") + if tzinfo == "Z": + tzinfo = dt.timezone.utc + elif tzinfo is not None: + offset_mins = int(tzinfo[-2:]) if len(tzinfo) > 3 else 0 + offset = 60 * int(tzinfo[1:3]) + offset_mins + if tzinfo[0] == "-": + offset = -offset + tzinfo = get_fixed_timezone(offset) + kw = {k: int(v) for k, v in kw.items() if v is not None} + kw["tzinfo"] = tzinfo + return dt.datetime(**kw) + + +def from_iso_time(value): + """Parse a string and return a datetime.time. + + This function doesn't support time zone offsets. + """ + match = _iso8601_time_re.match(value) + if not match: + raise ValueError("Not a valid ISO8601-formatted time string") + kw = match.groupdict() + kw["microsecond"] = kw["microsecond"] and kw["microsecond"].ljust(6, "0") + kw = {k: int(v) for k, v in kw.items() if v is not None} + return dt.time(**kw) + + +def from_iso_date(value): + """Parse a string and return a datetime.date.""" + match = _iso8601_date_re.match(value) + if not match: + raise ValueError("Not a valid ISO8601-formatted date string") + kw = {k: int(v) for k, v in match.groupdict().items()} + return dt.date(**kw) + + +def from_timestamp(value: typing.Any) -> dt.datetime: + if value is True or value is False: + raise ValueError("Not a valid POSIX timestamp") + value = float(value) + if value < 0: + raise ValueError("Not a valid POSIX timestamp") + + # Load a timestamp with utc as timezone to prevent using system timezone. + # Then set timezone to None, to let the Field handle adding timezone info. + try: + return dt.datetime.fromtimestamp(value, tz=dt.timezone.utc).replace(tzinfo=None) + except OverflowError as exc: + raise ValueError("Timestamp is too large") from exc + except OSError as exc: + raise ValueError("Error converting value to datetime") from exc + + +def from_timestamp_ms(value: typing.Any) -> dt.datetime: + value = float(value) + return from_timestamp(value / 1000) + + +def timestamp( + value: dt.datetime, +) -> float: + if not is_aware(value): + # When a date is naive, use UTC as zone info to prevent using system timezone. + value = value.replace(tzinfo=dt.timezone.utc) + return value.timestamp() + + +def timestamp_ms(value: dt.datetime) -> float: + return timestamp(value) * 1000 + + +def isoformat(datetime: dt.datetime) -> str: + """Return the ISO8601-formatted representation of a datetime object. + + :param datetime datetime: The datetime. + """ + return datetime.isoformat() + + +def to_iso_time(time: dt.time) -> str: + return dt.time.isoformat(time) + + +def to_iso_date(date: dt.date) -> str: + return dt.date.isoformat(date) + + +def ensure_text_type(val: str | bytes) -> str: + if isinstance(val, bytes): + val = val.decode("utf-8") + return str(val) + + +def pluck(dictlist: list[dict[str, typing.Any]], key: str): + """Extracts a list of dictionary values from a list of dictionaries. + :: + + >>> dlist = [{'id': 1, 'name': 'foo'}, {'id': 2, 'name': 'bar'}] + >>> pluck(dlist, 'id') + [1, 2] + """ + return [d[key] for d in dictlist] + + +# Various utilities for pulling keyed values from objects + + +def get_value(obj, key: int | str, default=missing): + """Helper for pulling a keyed value off various types of objects. Fields use + this method by default to access attributes of the source object. For object `x` + and attribute `i`, this method first tries to access `x[i]`, and then falls back to + `x.i` if an exception is raised. + + .. warning:: + If an object `x` does not raise an exception when `x[i]` does not exist, + `get_value` will never check the value `x.i`. Consider overriding + `marshmallow.fields.Field.get_value` in this case. + """ + if not isinstance(key, int) and "." in key: + return _get_value_for_keys(obj, key.split("."), default) + else: + return _get_value_for_key(obj, key, default) + + +def _get_value_for_keys(obj, keys, default): + if len(keys) == 1: + return _get_value_for_key(obj, keys[0], default) + else: + return _get_value_for_keys( + _get_value_for_key(obj, keys[0], default), keys[1:], default + ) + + +def _get_value_for_key(obj, key, default): + if not hasattr(obj, "__getitem__"): + return getattr(obj, key, default) + + try: + return obj[key] + except (KeyError, IndexError, TypeError, AttributeError): + return getattr(obj, key, default) + + +def set_value(dct: dict[str, typing.Any], key: str, value: typing.Any): + """Set a value in a dict. If `key` contains a '.', it is assumed + be a path (i.e. dot-delimited string) to the value's location. + + :: + + >>> d = {} + >>> set_value(d, 'foo.bar', 42) + >>> d + {'foo': {'bar': 42}} + """ + if "." in key: + head, rest = key.split(".", 1) + target = dct.setdefault(head, {}) + if not isinstance(target, dict): + raise ValueError( + f"Cannot set {key} in {head} " f"due to existing value: {target}" + ) + set_value(target, rest, value) + else: + dct[key] = value + + +def callable_or_raise(obj): + """Check that an object is callable, else raise a :exc:`TypeError`.""" + if not callable(obj): + raise TypeError(f"Object {obj!r} is not callable.") + return obj + + +def _signature(func: typing.Callable) -> list[str]: + return list(inspect.signature(func).parameters.keys()) + + +def get_func_args(func: typing.Callable) -> list[str]: + """Given a callable, return a list of argument names. Handles + `functools.partial` objects and class-based callables. + + .. versionchanged:: 3.0.0a1 + Do not return bound arguments, eg. ``self``. + """ + if inspect.isfunction(func) or inspect.ismethod(func): + return _signature(func) + if isinstance(func, functools.partial): + return _signature(func.func) + # Callable class + return _signature(func) + + +def resolve_field_instance(cls_or_instance): + """Return a Schema instance from a Schema class or instance. + + :param type|Schema cls_or_instance: Marshmallow Schema class or instance. + """ + if isinstance(cls_or_instance, type): + if not issubclass(cls_or_instance, FieldABC): + raise FieldInstanceResolutionError + return cls_or_instance() + else: + if not isinstance(cls_or_instance, FieldABC): + raise FieldInstanceResolutionError + return cls_or_instance + + +def timedelta_to_microseconds(value: dt.timedelta) -> int: + """Compute the total microseconds of a timedelta + + https://github.com/python/cpython/blob/bb3e0c240bc60fe08d332ff5955d54197f79751c/Lib/datetime.py#L665-L667 # noqa: B950 + """ + return (value.days * (24 * 3600) + value.seconds) * 1000000 + value.microseconds + + +def validate_unknown_parameter_value(obj: typing.Any) -> str: + if obj not in _UNKNOWN_VALUES: + raise ValueError( + f"Object {obj!r} is not a valid value for the 'unknown' parameter" + ) + return obj diff --git a/ccxt/static_dependencies/marshmallow/validate.py b/ccxt/static_dependencies/marshmallow/validate.py new file mode 100644 index 0000000..1805bed --- /dev/null +++ b/ccxt/static_dependencies/marshmallow/validate.py @@ -0,0 +1,678 @@ +"""Validation classes for various types of data.""" + +from __future__ import annotations + +import re +import typing +from abc import ABC, abstractmethod +from itertools import zip_longest +from operator import attrgetter + +from . import types +from .exceptions import ValidationError + +_T = typing.TypeVar("_T") + + +class Validator(ABC): + """Abstract base class for validators. + + .. note:: + This class does not provide any validation behavior. It is only used to + add a useful `__repr__` implementation for validators. + """ + + error = None # type: str | None + + def __repr__(self) -> str: + args = self._repr_args() + args = f"{args}, " if args else "" + + return f"<{self.__class__.__name__}({args}error={self.error!r})>" + + def _repr_args(self) -> str: + """A string representation of the args passed to this validator. Used by + `__repr__`. + """ + return "" + + @abstractmethod + def __call__(self, value: typing.Any) -> typing.Any: ... + + +class And(Validator): + """Compose multiple validators and combine their error messages. + + Example: :: + + from . import validate, ValidationError + + + def is_even(value): + if value % 2 != 0: + raise ValidationError("Not an even value.") + + + validator = validate.And(validate.Range(min=0), is_even) + validator(-1) + # ValidationError: ['Must be greater than or equal to 0.', 'Not an even value.'] + + :param validators: Validators to combine. + :param error: Error message to use when a validator returns ``False``. + """ + + default_error_message = "Invalid value." + + def __init__(self, *validators: types.Validator, error: str | None = None): + self.validators = tuple(validators) + self.error = error or self.default_error_message # type: str + + def _repr_args(self) -> str: + return f"validators={self.validators!r}" + + def __call__(self, value: typing.Any) -> typing.Any: + errors = [] + kwargs = {} + for validator in self.validators: + try: + r = validator(value) + if not isinstance(validator, Validator) and r is False: + raise ValidationError(self.error) + except ValidationError as err: + kwargs.update(err.kwargs) + if isinstance(err.messages, dict): + errors.append(err.messages) + else: + # FIXME : Get rid of cast + errors.extend(typing.cast(list, err.messages)) + if errors: + raise ValidationError(errors, **kwargs) + return value + + +class URL(Validator): + """Validate a URL. + + :param relative: Whether to allow relative URLs. + :param absolute: Whether to allow absolute URLs. + :param error: Error message to raise in case of a validation error. + Can be interpolated with `{input}`. + :param schemes: Valid schemes. By default, ``http``, ``https``, + ``ftp``, and ``ftps`` are allowed. + :param require_tld: Whether to reject non-FQDN hostnames. + """ + + class RegexMemoizer: + def __init__(self): + self._memoized = {} + + def _regex_generator( + self, relative: bool, absolute: bool, require_tld: bool + ) -> typing.Pattern: + hostname_variants = [ + # a normal domain name, expressed in [A-Z0-9] chars with hyphens allowed only in the middle + # note that the regex will be compiled with IGNORECASE, so these are upper and lowercase chars + ( + r"(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+" + r"(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)" + ), + # or the special string 'localhost' + r"localhost", + # or IPv4 + r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}", + # or IPv6 + r"\[[A-F0-9]*:[A-F0-9:]+\]", + ] + if not require_tld: + # allow dotless hostnames + hostname_variants.append(r"(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.?)") + + absolute_part = "".join( + ( + # scheme (e.g. 'https://', 'ftp://', etc) + # this is validated separately against allowed schemes, so in the regex + # we simply want to capture its existence + r"(?:[a-z0-9\.\-\+]*)://", + # userinfo, for URLs encoding authentication + # e.g. 'ftp://foo:bar@ftp.example.org/' + r"(?:(?:[a-z0-9\-._~!$&'()*+,;=:]|%[0-9a-f]{2})*@)?", + # netloc, the hostname/domain part of the URL plus the optional port + r"(?:", + "|".join(hostname_variants), + r")", + r"(?::\d+)?", + ) + ) + relative_part = r"(?:/?|[/?]\S+)\Z" + + if relative: + if absolute: + parts: tuple[str, ...] = ( + r"^(", + absolute_part, + r")?", + relative_part, + ) + else: + parts = (r"^", relative_part) + else: + parts = (r"^", absolute_part, relative_part) + + return re.compile("".join(parts), re.IGNORECASE) + + def __call__( + self, relative: bool, absolute: bool, require_tld: bool + ) -> typing.Pattern: + key = (relative, absolute, require_tld) + if key not in self._memoized: + self._memoized[key] = self._regex_generator( + relative, absolute, require_tld + ) + + return self._memoized[key] + + _regex = RegexMemoizer() + + default_message = "Not a valid URL." + default_schemes = {"http", "https", "ftp", "ftps"} + + def __init__( + self, + *, + relative: bool = False, + absolute: bool = True, + schemes: types.StrSequenceOrSet | None = None, + require_tld: bool = True, + error: str | None = None, + ): + if not relative and not absolute: + raise ValueError( + "URL validation cannot set both relative and absolute to False." + ) + self.relative = relative + self.absolute = absolute + self.error = error or self.default_message # type: str + self.schemes = schemes or self.default_schemes + self.require_tld = require_tld + + def _repr_args(self) -> str: + return f"relative={self.relative!r}, absolute={self.absolute!r}" + + def _format_error(self, value) -> str: + return self.error.format(input=value) + + def __call__(self, value: str) -> str: + message = self._format_error(value) + if not value: + raise ValidationError(message) + + # Check first if the scheme is valid + if "://" in value: + scheme = value.split("://")[0].lower() + if scheme not in self.schemes: + raise ValidationError(message) + + regex = self._regex(self.relative, self.absolute, self.require_tld) + + if not regex.search(value): + raise ValidationError(message) + + return value + + +class Email(Validator): + """Validate an email address. + + :param error: Error message to raise in case of a validation error. Can be + interpolated with `{input}`. + """ + + USER_REGEX = re.compile( + r"(^[-!#$%&'*+/=?^`{}|~\w]+(\.[-!#$%&'*+/=?^`{}|~\w]+)*\Z" # dot-atom + # quoted-string + r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]' + r'|\\[\001-\011\013\014\016-\177])*"\Z)', + re.IGNORECASE | re.UNICODE, + ) + + DOMAIN_REGEX = re.compile( + # domain + r"(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+" + r"(?:[A-Z]{2,6}|[A-Z0-9-]{2,})\Z" + # literal form, ipv4 address (SMTP 4.1.3) + r"|^\[(25[0-5]|2[0-4]\d|[0-1]?\d?\d)" + r"(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}\]\Z", + re.IGNORECASE | re.UNICODE, + ) + + DOMAIN_WHITELIST = ("localhost",) + + default_message = "Not a valid email address." + + def __init__(self, *, error: str | None = None): + self.error = error or self.default_message # type: str + + def _format_error(self, value: str) -> str: + return self.error.format(input=value) + + def __call__(self, value: str) -> str: + message = self._format_error(value) + + if not value or "@" not in value: + raise ValidationError(message) + + user_part, domain_part = value.rsplit("@", 1) + + if not self.USER_REGEX.match(user_part): + raise ValidationError(message) + + if domain_part not in self.DOMAIN_WHITELIST: + if not self.DOMAIN_REGEX.match(domain_part): + try: + domain_part = domain_part.encode("idna").decode("ascii") + except UnicodeError: + pass + else: + if self.DOMAIN_REGEX.match(domain_part): + return value + raise ValidationError(message) + + return value + + +class Range(Validator): + """Validator which succeeds if the value passed to it is within the specified + range. If ``min`` is not specified, or is specified as `None`, + no lower bound exists. If ``max`` is not specified, or is specified as `None`, + no upper bound exists. The inclusivity of the bounds (if they exist) is configurable. + If ``min_inclusive`` is not specified, or is specified as `True`, then + the ``min`` bound is included in the range. If ``max_inclusive`` is not specified, + or is specified as `True`, then the ``max`` bound is included in the range. + + :param min: The minimum value (lower bound). If not provided, minimum + value will not be checked. + :param max: The maximum value (upper bound). If not provided, maximum + value will not be checked. + :param min_inclusive: Whether the `min` bound is included in the range. + :param max_inclusive: Whether the `max` bound is included in the range. + :param error: Error message to raise in case of a validation error. + Can be interpolated with `{input}`, `{min}` and `{max}`. + """ + + message_min = "Must be {min_op} {{min}}." + message_max = "Must be {max_op} {{max}}." + message_all = "Must be {min_op} {{min}} and {max_op} {{max}}." + + message_gte = "greater than or equal to" + message_gt = "greater than" + message_lte = "less than or equal to" + message_lt = "less than" + + def __init__( + self, + min=None, + max=None, + *, + min_inclusive: bool = True, + max_inclusive: bool = True, + error: str | None = None, + ): + self.min = min + self.max = max + self.error = error + self.min_inclusive = min_inclusive + self.max_inclusive = max_inclusive + + # interpolate messages based on bound inclusivity + self.message_min = self.message_min.format( + min_op=self.message_gte if self.min_inclusive else self.message_gt + ) + self.message_max = self.message_max.format( + max_op=self.message_lte if self.max_inclusive else self.message_lt + ) + self.message_all = self.message_all.format( + min_op=self.message_gte if self.min_inclusive else self.message_gt, + max_op=self.message_lte if self.max_inclusive else self.message_lt, + ) + + def _repr_args(self) -> str: + return f"min={self.min!r}, max={self.max!r}, min_inclusive={self.min_inclusive!r}, max_inclusive={self.max_inclusive!r}" + + def _format_error(self, value: _T, message: str) -> str: + return (self.error or message).format(input=value, min=self.min, max=self.max) + + def __call__(self, value: _T) -> _T: + if self.min is not None and ( + value < self.min if self.min_inclusive else value <= self.min + ): + message = self.message_min if self.max is None else self.message_all + raise ValidationError(self._format_error(value, message)) + + if self.max is not None and ( + value > self.max if self.max_inclusive else value >= self.max + ): + message = self.message_max if self.min is None else self.message_all + raise ValidationError(self._format_error(value, message)) + + return value + + +class Length(Validator): + """Validator which succeeds if the value passed to it has a + length between a minimum and maximum. Uses len(), so it + can work for strings, lists, or anything with length. + + :param min: The minimum length. If not provided, minimum length + will not be checked. + :param max: The maximum length. If not provided, maximum length + will not be checked. + :param equal: The exact length. If provided, maximum and minimum + length will not be checked. + :param error: Error message to raise in case of a validation error. + Can be interpolated with `{input}`, `{min}` and `{max}`. + """ + + message_min = "Shorter than minimum length {min}." + message_max = "Longer than maximum length {max}." + message_all = "Length must be between {min} and {max}." + message_equal = "Length must be {equal}." + + def __init__( + self, + min: int | None = None, + max: int | None = None, + *, + equal: int | None = None, + error: str | None = None, + ): + if equal is not None and any([min, max]): + raise ValueError( + "The `equal` parameter was provided, maximum or " + "minimum parameter must not be provided." + ) + + self.min = min + self.max = max + self.error = error + self.equal = equal + + def _repr_args(self) -> str: + return f"min={self.min!r}, max={self.max!r}, equal={self.equal!r}" + + def _format_error(self, value: typing.Sized, message: str) -> str: + return (self.error or message).format( + input=value, min=self.min, max=self.max, equal=self.equal + ) + + def __call__(self, value: typing.Sized) -> typing.Sized: + length = len(value) + + if self.equal is not None: + if length != self.equal: + raise ValidationError(self._format_error(value, self.message_equal)) + return value + + if self.min is not None and length < self.min: + message = self.message_min if self.max is None else self.message_all + raise ValidationError(self._format_error(value, message)) + + if self.max is not None and length > self.max: + message = self.message_max if self.min is None else self.message_all + raise ValidationError(self._format_error(value, message)) + + return value + + +class Equal(Validator): + """Validator which succeeds if the ``value`` passed to it is + equal to ``comparable``. + + :param comparable: The object to compare to. + :param error: Error message to raise in case of a validation error. + Can be interpolated with `{input}` and `{other}`. + """ + + default_message = "Must be equal to {other}." + + def __init__(self, comparable, *, error: str | None = None): + self.comparable = comparable + self.error = error or self.default_message # type: str + + def _repr_args(self) -> str: + return f"comparable={self.comparable!r}" + + def _format_error(self, value: _T) -> str: + return self.error.format(input=value, other=self.comparable) + + def __call__(self, value: _T) -> _T: + if value != self.comparable: + raise ValidationError(self._format_error(value)) + return value + + +class Regexp(Validator): + """Validator which succeeds if the ``value`` matches ``regex``. + + .. note:: + + Uses `re.match`, which searches for a match at the beginning of a string. + + :param regex: The regular expression string to use. Can also be a compiled + regular expression pattern. + :param flags: The regexp flags to use, for example re.IGNORECASE. Ignored + if ``regex`` is not a string. + :param error: Error message to raise in case of a validation error. + Can be interpolated with `{input}` and `{regex}`. + """ + + default_message = "String does not match expected pattern." + + def __init__( + self, + regex: str | bytes | typing.Pattern, + flags: int = 0, + *, + error: str | None = None, + ): + self.regex = ( + re.compile(regex, flags) if isinstance(regex, (str, bytes)) else regex + ) + self.error = error or self.default_message # type: str + + def _repr_args(self) -> str: + return f"regex={self.regex!r}" + + def _format_error(self, value: str | bytes) -> str: + return self.error.format(input=value, regex=self.regex.pattern) + + @typing.overload + def __call__(self, value: str) -> str: ... + + @typing.overload + def __call__(self, value: bytes) -> bytes: ... + + def __call__(self, value): + if self.regex.match(value) is None: + raise ValidationError(self._format_error(value)) + + return value + + +class Predicate(Validator): + """Call the specified ``method`` of the ``value`` object. The + validator succeeds if the invoked method returns an object that + evaluates to True in a Boolean context. Any additional keyword + argument will be passed to the method. + + :param method: The name of the method to invoke. + :param error: Error message to raise in case of a validation error. + Can be interpolated with `{input}` and `{method}`. + :param kwargs: Additional keyword arguments to pass to the method. + """ + + default_message = "Invalid input." + + def __init__(self, method: str, *, error: str | None = None, **kwargs): + self.method = method + self.error = error or self.default_message # type: str + self.kwargs = kwargs + + def _repr_args(self) -> str: + return f"method={self.method!r}, kwargs={self.kwargs!r}" + + def _format_error(self, value: typing.Any) -> str: + return self.error.format(input=value, method=self.method) + + def __call__(self, value: typing.Any) -> typing.Any: + method = getattr(value, self.method) + + if not method(**self.kwargs): + raise ValidationError(self._format_error(value)) + + return value + + +class NoneOf(Validator): + """Validator which fails if ``value`` is a member of ``iterable``. + + :param iterable: A sequence of invalid values. + :param error: Error message to raise in case of a validation error. Can be + interpolated using `{input}` and `{values}`. + """ + + default_message = "Invalid input." + + def __init__(self, iterable: typing.Iterable, *, error: str | None = None): + self.iterable = iterable + self.values_text = ", ".join(str(each) for each in self.iterable) + self.error = error or self.default_message # type: str + + def _repr_args(self) -> str: + return f"iterable={self.iterable!r}" + + def _format_error(self, value) -> str: + return self.error.format(input=value, values=self.values_text) + + def __call__(self, value: typing.Any) -> typing.Any: + try: + if value in self.iterable: + raise ValidationError(self._format_error(value)) + except TypeError: + pass + + return value + + +class OneOf(Validator): + """Validator which succeeds if ``value`` is a member of ``choices``. + + :param choices: A sequence of valid values. + :param labels: Optional sequence of labels to pair with the choices. + :param error: Error message to raise in case of a validation error. Can be + interpolated with `{input}`, `{choices}` and `{labels}`. + """ + + default_message = "Must be one of: {choices}." + + def __init__( + self, + choices: typing.Iterable, + labels: typing.Iterable[str] | None = None, + *, + error: str | None = None, + ): + self.choices = choices + self.choices_text = ", ".join(str(choice) for choice in self.choices) + self.labels = labels if labels is not None else [] + self.labels_text = ", ".join(str(label) for label in self.labels) + self.error = error or self.default_message # type: str + + def _repr_args(self) -> str: + return f"choices={self.choices!r}, labels={self.labels!r}" + + def _format_error(self, value) -> str: + return self.error.format( + input=value, choices=self.choices_text, labels=self.labels_text + ) + + def __call__(self, value: typing.Any) -> typing.Any: + try: + if value not in self.choices: + raise ValidationError(self._format_error(value)) + except TypeError as error: + raise ValidationError(self._format_error(value)) from error + + return value + + def options( + self, + valuegetter: str | typing.Callable[[typing.Any], typing.Any] = str, + ) -> typing.Iterable[tuple[typing.Any, str]]: + """Return a generator over the (value, label) pairs, where value + is a string associated with each choice. This convenience method + is useful to populate, for instance, a form select field. + + :param valuegetter: Can be a callable or a string. In the former case, it must + be a one-argument callable which returns the value of a + choice. In the latter case, the string specifies the name + of an attribute of the choice objects. Defaults to `str()` + or `str()`. + """ + valuegetter = valuegetter if callable(valuegetter) else attrgetter(valuegetter) + pairs = zip_longest(self.choices, self.labels, fillvalue="") + + return ((valuegetter(choice), label) for choice, label in pairs) + + +class ContainsOnly(OneOf): + """Validator which succeeds if ``value`` is a sequence and each element + in the sequence is also in the sequence passed as ``choices``. Empty input + is considered valid. + + :param iterable choices: Same as :class:`OneOf`. + :param iterable labels: Same as :class:`OneOf`. + :param str error: Same as :class:`OneOf`. + + .. versionchanged:: 3.0.0b2 + Duplicate values are considered valid. + .. versionchanged:: 3.0.0b2 + Empty input is considered valid. Use `validate.Length(min=1) ` + to validate against empty inputs. + """ + + default_message = "One or more of the choices you made was not in: {choices}." + + def _format_error(self, value) -> str: + value_text = ", ".join(str(val) for val in value) + return super()._format_error(value_text) + + def __call__(self, value: typing.Sequence[_T]) -> typing.Sequence[_T]: + # We can't use set.issubset because does not handle unhashable types + for val in value: + if val not in self.choices: + raise ValidationError(self._format_error(value)) + return value + + +class ContainsNoneOf(NoneOf): + """Validator which fails if ``value`` is a sequence and any element + in the sequence is a member of the sequence passed as ``iterable``. Empty input + is considered valid. + + :param iterable iterable: Same as :class:`NoneOf`. + :param str error: Same as :class:`NoneOf`. + + .. versionadded:: 3.6.0 + """ + + default_message = "One or more of the choices you made was in: {values}." + + def _format_error(self, value) -> str: + value_text = ", ".join(str(val) for val in value) + return super()._format_error(value_text) + + def __call__(self, value: typing.Sequence[_T]) -> typing.Sequence[_T]: + for val in value: + if val in self.iterable: + raise ValidationError(self._format_error(value)) + return value diff --git a/ccxt/static_dependencies/marshmallow/warnings.py b/ccxt/static_dependencies/marshmallow/warnings.py new file mode 100644 index 0000000..0da3c50 --- /dev/null +++ b/ccxt/static_dependencies/marshmallow/warnings.py @@ -0,0 +1,2 @@ +class RemovedInMarshmallow4Warning(DeprecationWarning): + pass diff --git a/ccxt/static_dependencies/marshmallow_dataclass/__init__.py b/ccxt/static_dependencies/marshmallow_dataclass/__init__.py new file mode 100644 index 0000000..1bd7fbd --- /dev/null +++ b/ccxt/static_dependencies/marshmallow_dataclass/__init__.py @@ -0,0 +1,1047 @@ +""" +This library allows the conversion of python 3.7's :mod:`dataclasses` +to :mod:`marshmallow` schemas. + +It takes a python class, and generates a marshmallow schema for it. + +Simple example:: + + from marshmallow import Schema + from marshmallow_dataclass import dataclass + + @dataclass + class Point: + x:float + y:float + + point = Point(x=0, y=0) + point_json = Point.Schema().dumps(point) + +Full example:: + + from marshmallow import Schema + from dataclasses import field + from marshmallow_dataclass import dataclass + import datetime + + @dataclass + class User: + birth: datetime.date = field(metadata= { + "required": True # A parameter to pass to marshmallow's field + }) + website:str = field(metadata = { + "marshmallow_field": marshmallow.fields.Url() # Custom marshmallow field + }) + Schema: ClassVar[Type[Schema]] = Schema # For the type checker +""" + +import collections.abc +import dataclasses +import inspect +import sys +import threading +import types +import warnings +from enum import Enum +from functools import lru_cache, partial +from typing import ( + Any, + Callable, + Dict, + FrozenSet, + Generic, + List, + Mapping, + NewType as typing_NewType, + Optional, + Sequence, + Set, + Tuple, + Type, + TypeVar, + Union, + cast, + get_type_hints, + overload, +) + +from .. import marshmallow +from ..typing_extensions import * +from ..typing_inspect import * + +from .lazy_class_attribute import lazy_class_attribute + +if sys.version_info >= (3, 9): + from typing import Annotated +else: + from ..typing_extensions import Annotated + +if sys.version_info >= (3, 11): + from typing import dataclass_transform +else: + from ..typing_extensions import dataclass_transform + + +__all__ = ["dataclass", "add_schema", "class_schema", "field_for_schema", "NewType"] + +NoneType = type(None) +_U = TypeVar("_U") + +# Whitelist of dataclass members that will be copied to generated schema. +MEMBERS_WHITELIST: Set[str] = {"Meta"} + +# Max number of generated schemas that class_schema keeps of generated schemas. Removes duplicates. +MAX_CLASS_SCHEMA_CACHE_SIZE = 1024 + + +def _maybe_get_callers_frame( + cls: type, stacklevel: int = 1 +) -> Optional[types.FrameType]: + """Return the caller's frame, but only if it will help resolve forward type references. + + We sometimes need the caller's frame to get access to the caller's + local namespace in order to be able to resolve forward type + references in dataclasses. + + Notes + ----- + + If the caller's locals are the same as the dataclass' module + globals — this is the case for the common case of dataclasses + defined at the module top-level — we don't need the locals. + (Typing.get_type_hints() knows how to check the class module + globals on its own.) + + In that case, we don't need the caller's frame. Not holding a + reference to the frame in our our lazy ``.Scheme`` class attribute + is a significant win, memory-wise. + + """ + try: + frame = inspect.currentframe() + for _ in range(stacklevel + 1): + if frame is None: + return None + frame = frame.f_back + + if frame is None: + return None + + globalns = getattr(sys.modules.get(cls.__module__), "__dict__", None) + if frame.f_locals is globalns: + # Locals are the globals + return None + + return frame + + finally: + # Paranoia, per https://docs.python.org/3/library/inspect.html#the-interpreter-stack + del frame + + +@overload +def dataclass( + _cls: Type[_U], + *, + repr: bool = True, + eq: bool = True, + order: bool = False, + unsafe_hash: bool = False, + frozen: bool = False, + base_schema: Optional[Type[marshmallow.Schema]] = None, + cls_frame: Optional[types.FrameType] = None, +) -> Type[_U]: + ... + + +@overload +def dataclass( + *, + repr: bool = True, + eq: bool = True, + order: bool = False, + unsafe_hash: bool = False, + frozen: bool = False, + base_schema: Optional[Type[marshmallow.Schema]] = None, + cls_frame: Optional[types.FrameType] = None, +) -> Callable[[Type[_U]], Type[_U]]: + ... + + +# _cls should never be specified by keyword, so start it with an +# underscore. The presence of _cls is used to detect if this +# decorator is being called with parameters or not. +@dataclass_transform(field_specifiers=(dataclasses.Field, dataclasses.field)) +def dataclass( + _cls: Optional[Type[_U]] = None, + *, + repr: bool = True, + eq: bool = True, + order: bool = False, + unsafe_hash: bool = False, + frozen: bool = False, + base_schema: Optional[Type[marshmallow.Schema]] = None, + cls_frame: Optional[types.FrameType] = None, + stacklevel: int = 1, +) -> Union[Type[_U], Callable[[Type[_U]], Type[_U]]]: + """ + This decorator does the same as dataclasses.dataclass, but also applies :func:`add_schema`. + It adds a `.Schema` attribute to the class object + + :param base_schema: marshmallow schema used as a base class when deriving dataclass schema + :param cls_frame: frame of cls definition, used to obtain locals with other classes definitions. + If None is passed the caller frame will be treated as cls_frame + + >>> @dataclass + ... class Artist: + ... name: str + >>> Artist.Schema + + + >>> from typing import ClassVar + >>> from marshmallow import Schema + >>> @dataclass(order=True) # preserve field order + ... class Point: + ... x:float + ... y:float + ... Schema: ClassVar[Type[Schema]] = Schema # For the type checker + ... + >>> Point.Schema().load({'x':0, 'y':0}) # This line can be statically type checked + Point(x=0.0, y=0.0) + """ + dc = dataclasses.dataclass( + repr=repr, eq=eq, order=order, unsafe_hash=unsafe_hash, frozen=frozen + ) + + def decorator(cls: Type[_U], stacklevel: int = 1) -> Type[_U]: + return add_schema( + dc(cls), base_schema, cls_frame=cls_frame, stacklevel=stacklevel + 1 + ) + + if _cls is None: + return decorator + return decorator(_cls, stacklevel=stacklevel + 1) + + +@overload +def add_schema(_cls: Type[_U]) -> Type[_U]: + ... + + +@overload +def add_schema( + base_schema: Optional[Type[marshmallow.Schema]] = None, +) -> Callable[[Type[_U]], Type[_U]]: + ... + + +@overload +def add_schema( + _cls: Type[_U], + base_schema: Optional[Type[marshmallow.Schema]] = None, + cls_frame: Optional[types.FrameType] = None, + stacklevel: int = 1, +) -> Type[_U]: + ... + + +def add_schema(_cls=None, base_schema=None, cls_frame=None, stacklevel=1): + """ + This decorator adds a marshmallow schema as the 'Schema' attribute in a dataclass. + It uses :func:`class_schema` internally. + + :param type _cls: The dataclass to which a Schema should be added + :param base_schema: marshmallow schema used as a base class when deriving dataclass schema + :param cls_frame: frame of cls definition + + >>> class BaseSchema(marshmallow.Schema): + ... def on_bind_field(self, field_name, field_obj): + ... field_obj.data_key = (field_obj.data_key or field_name).upper() + + >>> @add_schema(base_schema=BaseSchema) + ... @dataclasses.dataclass + ... class Artist: + ... names: Tuple[str, str] + >>> artist = Artist.Schema().loads('{"NAMES": ["Martin", "Ramirez"]}') + >>> artist + Artist(names=('Martin', 'Ramirez')) + """ + + def decorator(clazz: Type[_U], stacklevel: int = stacklevel) -> Type[_U]: + if cls_frame is not None: + frame = cls_frame + else: + frame = _maybe_get_callers_frame(clazz, stacklevel=stacklevel) + + # noinspection PyTypeHints + clazz.Schema = lazy_class_attribute( # type: ignore + partial(class_schema, clazz, base_schema, frame), + "Schema", + clazz.__name__, + ) + return clazz + + if _cls is None: + return decorator + return decorator(_cls, stacklevel=stacklevel + 1) + + +@overload +def class_schema( + clazz: type, + base_schema: Optional[Type[marshmallow.Schema]] = None, + *, + globalns: Optional[Dict[str, Any]] = None, + localns: Optional[Dict[str, Any]] = None, +) -> Type[marshmallow.Schema]: + ... + + +@overload +def class_schema( + clazz: type, + base_schema: Optional[Type[marshmallow.Schema]] = None, + clazz_frame: Optional[types.FrameType] = None, + *, + globalns: Optional[Dict[str, Any]] = None, +) -> Type[marshmallow.Schema]: + ... + + +def class_schema( + clazz: type, + base_schema: Optional[Type[marshmallow.Schema]] = None, + # FIXME: delete clazz_frame from API? + clazz_frame: Optional[types.FrameType] = None, + *, + globalns: Optional[Dict[str, Any]] = None, + localns: Optional[Dict[str, Any]] = None, +) -> Type[marshmallow.Schema]: + """ + Convert a class to a marshmallow schema + + :param clazz: A python class (may be a dataclass) + :param base_schema: marshmallow schema used as a base class when deriving dataclass schema + :param clazz_frame: frame of cls definition + :return: A marshmallow Schema corresponding to the dataclass + + .. note:: + All the arguments supported by marshmallow field classes can + be passed in the `metadata` dictionary of a field. + + + If you want to use a custom marshmallow field + (one that has no equivalent python type), you can pass it as the + ``marshmallow_field`` key in the metadata dictionary. + + >>> import typing + >>> Meters = typing.NewType('Meters', float) + >>> @dataclasses.dataclass() + ... class Building: + ... height: Optional[Meters] + ... name: str = dataclasses.field(default="anonymous") + ... class Meta: + ... ordered = True + ... + >>> class_schema(Building) # Returns a marshmallow schema class (not an instance) + + >>> @dataclasses.dataclass() + ... class City: + ... name: str = dataclasses.field(metadata={'required':True}) + ... best_building: Building # Reference to another dataclass. A schema will be created for it too. + ... other_buildings: List[Building] = dataclasses.field(default_factory=lambda: []) + ... + >>> citySchema = class_schema(City)() + >>> city = citySchema.load({"name":"Paris", "best_building": {"name": "Eiffel Tower"}}) + >>> city + City(name='Paris', best_building=Building(height=None, name='Eiffel Tower'), other_buildings=[]) + + >>> citySchema.load({"name":"Paris"}) + Traceback (most recent call last): + ... + marshmallow.exceptions.ValidationError: {'best_building': ['Missing data for required field.']} + + >>> city_json = citySchema.dump(city) + >>> city_json['best_building'] # We get an OrderedDict because we specified order = True in the Meta class + OrderedDict([('height', None), ('name', 'Eiffel Tower')]) + + >>> @dataclasses.dataclass() + ... class Person: + ... name: str = dataclasses.field(default="Anonymous") + ... friends: List['Person'] = dataclasses.field(default_factory=lambda:[]) # Recursive field + ... + >>> person = class_schema(Person)().load({ + ... "friends": [{"name": "Roger Boucher"}] + ... }) + >>> person + Person(name='Anonymous', friends=[Person(name='Roger Boucher', friends=[])]) + + Marking dataclass fields as non-initialized (``init=False``), by default, will result in those + fields from being exluded in the schema. To override this behaviour, set the ``Meta`` option + ``include_non_init=True``. + + >>> @dataclasses.dataclass() + ... class C: + ... important: int = dataclasses.field(init=True, default=0) + ... # Only fields that are in the __init__ method will be added: + ... unimportant: int = dataclasses.field(init=False, default=0) + ... + >>> c = class_schema(C)().load({ + ... "important": 9, # This field will be imported + ... "unimportant": 9 # This field will NOT be imported + ... }, unknown=marshmallow.EXCLUDE) + >>> c + C(important=9, unimportant=0) + + >>> @dataclasses.dataclass() + ... class C: + ... class Meta: + ... include_non_init = True + ... important: int = dataclasses.field(init=True, default=0) + ... unimportant: int = dataclasses.field(init=False, default=0) + ... + >>> c = class_schema(C)().load({ + ... "important": 9, # This field will be imported + ... "unimportant": 9 # This field will be imported + ... }, unknown=marshmallow.EXCLUDE) + >>> c + C(important=9, unimportant=9) + + >>> @dataclasses.dataclass + ... class Website: + ... url:str = dataclasses.field(metadata = { + ... "marshmallow_field": marshmallow.fields.Url() # Custom marshmallow field + ... }) + ... + >>> class_schema(Website)().load({"url": "I am not a good URL !"}) + Traceback (most recent call last): + ... + marshmallow.exceptions.ValidationError: {'url': ['Not a valid URL.']} + + >>> @dataclasses.dataclass + ... class NeverValid: + ... @marshmallow.validates_schema + ... def validate(self, data, **_): + ... raise marshmallow.ValidationError('never valid') + ... + >>> class_schema(NeverValid)().load({}) + Traceback (most recent call last): + ... + marshmallow.exceptions.ValidationError: {'_schema': ['never valid']} + + >>> @dataclasses.dataclass + ... class Anything: + ... name: str + ... @marshmallow.validates('name') + ... def validates(self, value): + ... if len(value) > 5: raise marshmallow.ValidationError("Name too long") + >>> class_schema(Anything)().load({"name": "aaaaaargh"}) + Traceback (most recent call last): + ... + marshmallow.exceptions.ValidationError: {'name': ['Name too long']} + + You can use the ``metadata`` argument to override default field behaviour, e.g. the fact that + ``Optional`` fields allow ``None`` values: + + >>> @dataclasses.dataclass + ... class Custom: + ... name: Optional[str] = dataclasses.field(metadata={"allow_none": False}) + >>> class_schema(Custom)().load({"name": None}) + Traceback (most recent call last): + ... + marshmallow.exceptions.ValidationError: {'name': ['Field may not be null.']} + >>> class_schema(Custom)().load({}) + Custom(name=None) + """ + if not dataclasses.is_dataclass(clazz): + clazz = dataclasses.dataclass(clazz) + if localns is None: + if clazz_frame is None: + clazz_frame = _maybe_get_callers_frame(clazz) + if clazz_frame is not None: + localns = clazz_frame.f_locals + with _SchemaContext(globalns, localns): + return _internal_class_schema(clazz, base_schema) + + +class _SchemaContext: + """Global context for an invocation of class_schema.""" + + def __init__( + self, + globalns: Optional[Dict[str, Any]] = None, + localns: Optional[Dict[str, Any]] = None, + ): + self.seen_classes: Dict[type, str] = {} + self.globalns = globalns + self.localns = localns + + def __enter__(self) -> "_SchemaContext": + _schema_ctx_stack.push(self) + return self + + def __exit__( + self, + _typ: Optional[Type[BaseException]], + _value: Optional[BaseException], + _tb: Optional[types.TracebackType], + ) -> None: + _schema_ctx_stack.pop() + + +class _LocalStack(threading.local, Generic[_U]): + def __init__(self) -> None: + self.stack: List[_U] = [] + + def push(self, value: _U) -> None: + self.stack.append(value) + + def pop(self) -> None: + self.stack.pop() + + @property + def top(self) -> _U: + return self.stack[-1] + + +_schema_ctx_stack = _LocalStack[_SchemaContext]() + + +@lru_cache(maxsize=MAX_CLASS_SCHEMA_CACHE_SIZE) +def _internal_class_schema( + clazz: type, + base_schema: Optional[Type[marshmallow.Schema]] = None, +) -> Type[marshmallow.Schema]: + schema_ctx = _schema_ctx_stack.top + + if typing_extensions.get_origin(clazz) is Annotated and sys.version_info < (3, 10): + # https://github.com/python/cpython/blob/3.10/Lib/typing.py#L977 + class_name = clazz._name or clazz.__origin__.__name__ # type: ignore[attr-defined] + else: + class_name = clazz.__name__ + + schema_ctx.seen_classes[clazz] = class_name + + try: + # noinspection PyDataclass + fields: Tuple[dataclasses.Field, ...] = dataclasses.fields(clazz) + except TypeError: # Not a dataclass + try: + warnings.warn( + "****** WARNING ****** " + f"marshmallow_dataclass was called on the class {clazz}, which is not a dataclass. " + "It is going to try and convert the class into a dataclass, which may have " + "undesirable side effects. To avoid this message, make sure all your classes and " + "all the classes of their fields are either explicitly supported by " + "marshmallow_dataclass, or define the schema explicitly using " + "field(metadata=dict(marshmallow_field=...)). For more information, see " + "https://github.com/lovasoa/marshmallow_dataclass/issues/51 " + "****** WARNING ******" + ) + created_dataclass: type = dataclasses.dataclass(clazz) + return _internal_class_schema(created_dataclass, base_schema) + except Exception as exc: + raise TypeError( + f"{getattr(clazz, '__name__', repr(clazz))} is not a dataclass and cannot be turned into one." + ) from exc + + # Copy all marshmallow hooks and whitelisted members of the dataclass to the schema. + attributes = { + k: v + for k, v in inspect.getmembers(clazz) + if hasattr(v, "__marshmallow_hook__") or k in MEMBERS_WHITELIST + } + + # Determine whether we should include non-init fields + include_non_init = getattr(getattr(clazz, "Meta", None), "include_non_init", False) + + # Update the schema members to contain marshmallow fields instead of dataclass fields + + if sys.version_info >= (3, 9): + type_hints = get_type_hints( + clazz, + globalns=schema_ctx.globalns, + localns=schema_ctx.localns, + include_extras=True, + ) + else: + type_hints = get_type_hints( + clazz, globalns=schema_ctx.globalns, localns=schema_ctx.localns + ) + attributes.update( + ( + field.name, + _field_for_schema( + type_hints[field.name], + _get_field_default(field), + field.metadata, + base_schema, + ), + ) + for field in fields + if field.init or include_non_init + ) + + schema_class = type(clazz.__name__, (_base_schema(clazz, base_schema),), attributes) + return cast(Type[marshmallow.Schema], schema_class) + + +def _field_by_type( + typ: Union[type, Any], base_schema: Optional[Type[marshmallow.Schema]] +) -> Optional[Type[marshmallow.fields.Field]]: + return ( + base_schema and base_schema.TYPE_MAPPING.get(typ) + ) or marshmallow.Schema.TYPE_MAPPING.get(typ) + + +def _field_by_supertype( + typ: Type, + default: Any, + newtype_supertype: Type, + metadata: dict, + base_schema: Optional[Type[marshmallow.Schema]], +) -> marshmallow.fields.Field: + """ + Return a new field for fields based on a super field. (Usually spawned from NewType) + """ + # Add the information coming our custom NewType implementation + + typ_args = getattr(typ, "_marshmallow_args", {}) + + # Handle multiple validators from both `typ` and `metadata`. + # See https://github.com/lovasoa/marshmallow_dataclass/issues/91 + new_validators: List[Callable] = [] + for meta_dict in (typ_args, metadata): + if "validate" in meta_dict: + if marshmallow.utils.is_iterable_but_not_string(meta_dict["validate"]): + new_validators.extend(meta_dict["validate"]) + elif callable(meta_dict["validate"]): + new_validators.append(meta_dict["validate"]) + metadata["validate"] = new_validators if new_validators else None + + metadata = {**typ_args, **metadata} + metadata.setdefault("metadata", {}).setdefault("description", typ.__name__) + field = getattr(typ, "_marshmallow_field", None) + if field: + return field(**metadata) + else: + return _field_for_schema( + newtype_supertype, + metadata=metadata, + default=default, + base_schema=base_schema, + ) + + +def _generic_type_add_any(typ: type) -> type: + """if typ is generic type without arguments, replace them by Any.""" + if typ is list or typ is List: + typ = List[Any] + elif typ is dict or typ is Dict: + typ = Dict[Any, Any] + elif typ is Mapping: + typ = Mapping[Any, Any] + elif typ is Sequence: + typ = Sequence[Any] + elif typ is set or typ is Set: + typ = Set[Any] + elif typ is frozenset or typ is FrozenSet: + typ = FrozenSet[Any] + return typ + + +def _field_for_generic_type( + typ: type, + base_schema: Optional[Type[marshmallow.Schema]], + **metadata: Any, +) -> Optional[marshmallow.fields.Field]: + """ + If the type is a generic interface, resolve the arguments and construct the appropriate Field. + """ + origin = typing_extensions.get_origin(typ) + arguments = typing_extensions.get_args(typ) + if origin: + # Override base_schema.TYPE_MAPPING to change the class used for generic types below + type_mapping = base_schema.TYPE_MAPPING if base_schema else {} + + if origin in (list, List): + child_type = _field_for_schema(arguments[0], base_schema=base_schema) + list_type = cast( + Type[marshmallow.fields.List], + type_mapping.get(List, marshmallow.fields.List), + ) + return list_type(child_type, **metadata) + if origin in (collections.abc.Sequence, Sequence) or ( + origin in (tuple, Tuple) + and len(arguments) == 2 + and arguments[1] is Ellipsis + ): + from . import collection_field + + child_type = _field_for_schema(arguments[0], base_schema=base_schema) + return collection_field.Sequence(cls_or_instance=child_type, **metadata) + if origin in (set, Set): + from . import collection_field + + child_type = _field_for_schema(arguments[0], base_schema=base_schema) + return collection_field.Set( + cls_or_instance=child_type, frozen=False, **metadata + ) + if origin in (frozenset, FrozenSet): + from . import collection_field + + child_type = _field_for_schema(arguments[0], base_schema=base_schema) + return collection_field.Set( + cls_or_instance=child_type, frozen=True, **metadata + ) + if origin in (tuple, Tuple): + children = tuple( + _field_for_schema(arg, base_schema=base_schema) for arg in arguments + ) + tuple_type = cast( + Type[marshmallow.fields.Tuple], + type_mapping.get( # type:ignore[call-overload] + Tuple, marshmallow.fields.Tuple + ), + ) + return tuple_type(children, **metadata) + elif origin in (dict, Dict, collections.abc.Mapping, Mapping): + dict_type = type_mapping.get(Dict, marshmallow.fields.Dict) + return dict_type( + keys=_field_for_schema(arguments[0], base_schema=base_schema), + values=_field_for_schema(arguments[1], base_schema=base_schema), + **metadata, + ) + + return None + + +def _field_for_annotated_type( + typ: type, + **metadata: Any, +) -> Optional[marshmallow.fields.Field]: + """ + If the type is an Annotated interface, resolve the arguments and construct the appropriate Field. + """ + origin = typing_extensions.get_origin(typ) + arguments = typing_extensions.get_args(typ) + if origin and origin is Annotated: + marshmallow_annotations = [ + arg + for arg in arguments[1:] + if (inspect.isclass(arg) and issubclass(arg, marshmallow.fields.Field)) + or isinstance(arg, marshmallow.fields.Field) + ] + if marshmallow_annotations: + if len(marshmallow_annotations) > 1: + warnings.warn( + "Multiple marshmallow Field annotations found. Using the last one." + ) + + field = marshmallow_annotations[-1] + # Got a field instance, return as is. User must know what they're doing + if isinstance(field, marshmallow.fields.Field): + return field + + return field(**metadata) + return None + + +def _field_for_union_type( + typ: type, + base_schema: Optional[Type[marshmallow.Schema]], + **metadata: Any, +) -> Optional[marshmallow.fields.Field]: + arguments = typing_extensions.get_args(typ) + if typing_inspect.is_union_type(typ): + if typing_inspect.is_optional_type(typ): + metadata["allow_none"] = metadata.get("allow_none", True) + metadata["dump_default"] = metadata.get("dump_default", None) + if not metadata.get("required"): + metadata["load_default"] = metadata.get("load_default", None) + metadata.setdefault("required", False) + subtypes = [t for t in arguments if t is not NoneType] # type: ignore + if len(subtypes) == 1: + return _field_for_schema( + subtypes[0], + metadata=metadata, + base_schema=base_schema, + ) + from . import union_field + + return union_field.Union( + [ + ( + subtyp, + _field_for_schema( + subtyp, + metadata={"required": True}, + base_schema=base_schema, + ), + ) + for subtyp in subtypes + ], + **metadata, + ) + return None + + +def field_for_schema( + typ: type, + default: Any = marshmallow.missing, + metadata: Optional[Mapping[str, Any]] = None, + base_schema: Optional[Type[marshmallow.Schema]] = None, + # FIXME: delete typ_frame from API? + typ_frame: Optional[types.FrameType] = None, +) -> marshmallow.fields.Field: + """ + Get a marshmallow Field corresponding to the given python type. + The metadata of the dataclass field is used as arguments to the marshmallow Field. + + :param typ: The type for which a field should be generated + :param default: value to use for (de)serialization when the field is missing + :param metadata: Additional parameters to pass to the marshmallow field constructor + :param base_schema: marshmallow schema used as a base class when deriving dataclass schema + :param typ_frame: frame of type definition + + >>> int_field = field_for_schema(int, default=9, metadata=dict(required=True)) + >>> int_field.__class__ + + + >>> int_field.dump_default + 9 + + >>> field_for_schema(str, metadata={"marshmallow_field": marshmallow.fields.Url()}).__class__ + + """ + with _SchemaContext(localns=typ_frame.f_locals if typ_frame is not None else None): + return _field_for_schema(typ, default, metadata, base_schema) + + +def _field_for_schema( + typ: type, + default: Any = marshmallow.missing, + metadata: Optional[Mapping[str, Any]] = None, + base_schema: Optional[Type[marshmallow.Schema]] = None, +) -> marshmallow.fields.Field: + """ + Get a marshmallow Field corresponding to the given python type. + The metadata of the dataclass field is used as arguments to the marshmallow Field. + + This is an internal version of field_for_schema. It assumes a _SchemaContext + has been pushed onto the local stack. + + :param typ: The type for which a field should be generated + :param default: value to use for (de)serialization when the field is missing + :param metadata: Additional parameters to pass to the marshmallow field constructor + :param base_schema: marshmallow schema used as a base class when deriving dataclass schema + + """ + + metadata = {} if metadata is None else dict(metadata) + + if default is not marshmallow.missing: + metadata.setdefault("dump_default", default) + # 'missing' must not be set for required fields. + if not metadata.get("required"): + metadata.setdefault("load_default", default) + else: + metadata.setdefault("required", not typing_inspect.is_optional_type(typ)) + + # If the field was already defined by the user + predefined_field = metadata.get("marshmallow_field") + if predefined_field: + return predefined_field + + # Generic types specified without type arguments + typ = _generic_type_add_any(typ) + + # Base types + field = _field_by_type(typ, base_schema) + if field: + return field(**metadata) + + if typ is Any: + metadata.setdefault("allow_none", True) + return marshmallow.fields.Raw(**metadata) + + # i.e.: Literal['abc'] + if typing_inspect.is_literal_type(typ): + arguments = typing_inspect.get_args(typ) + return marshmallow.fields.Raw( + validate=( + marshmallow.validate.Equal(arguments[0]) + if len(arguments) == 1 + else marshmallow.validate.OneOf(arguments) + ), + **metadata, + ) + + # i.e.: Final[str] = 'abc' + if typing_inspect.is_final_type(typ): + arguments = typing_inspect.get_args(typ) + if arguments: + subtyp = arguments[0] + elif default is not marshmallow.missing: + if callable(default): + subtyp = Any + warnings.warn( + "****** WARNING ****** " + "marshmallow_dataclass was called on a dataclass with an " + 'attribute that is type-annotated with "Final" and uses ' + "dataclasses.field for specifying a default value using a " + "factory. The Marshmallow field type cannot be inferred from the " + "factory and will fall back to a raw field which is equivalent to " + 'the type annotation "Any" and will result in no validation. ' + "Provide a type to Final[...] to ensure accurate validation. " + "****** WARNING ******" + ) + else: + subtyp = type(default) + warnings.warn( + "****** WARNING ****** " + "marshmallow_dataclass was called on a dataclass with an " + 'attribute that is type-annotated with "Final" with a default ' + "value from which the Marshmallow field type is inferred. " + "Support for type inference from a default value is limited and " + "may result in inaccurate validation. Provide a type to " + "Final[...] to ensure accurate validation. " + "****** WARNING ******" + ) + else: + subtyp = Any + return _field_for_schema(subtyp, default, metadata, base_schema) + + annotated_field = _field_for_annotated_type(typ, **metadata) + if annotated_field: + return annotated_field + + union_field = _field_for_union_type(typ, base_schema, **metadata) + if union_field: + return union_field + + # Generic types + generic_field = _field_for_generic_type(typ, base_schema, **metadata) + if generic_field: + return generic_field + + # typing.NewType returns a function (in python <= 3.9) or a class (python >= 3.10) with a + # __supertype__ attribute + newtype_supertype = getattr(typ, "__supertype__", None) + if typing_inspect.is_new_type(typ) and newtype_supertype is not None: + return _field_by_supertype( + typ=typ, + default=default, + newtype_supertype=newtype_supertype, + metadata=metadata, + base_schema=base_schema, + ) + + # enumerations + if inspect.isclass(typ) and issubclass(typ, Enum): + return marshmallow.fields.Enum(typ, **metadata) + + # Nested marshmallow dataclass + # it would be just a class name instead of actual schema util the schema is not ready yet + nested_schema = getattr(typ, "Schema", None) + + # Nested dataclasses + forward_reference = getattr(typ, "__forward_arg__", None) + + nested = ( + nested_schema + or forward_reference + or _schema_ctx_stack.top.seen_classes.get(typ) + or _internal_class_schema(typ, base_schema) # type: ignore[arg-type] # FIXME + ) + + return marshmallow.fields.Nested(nested, **metadata) + + +def _base_schema( + clazz: type, base_schema: Optional[Type[marshmallow.Schema]] = None +) -> Type[marshmallow.Schema]: + """ + Base schema factory that creates a schema for `clazz` derived either from `base_schema` + or `BaseSchema` + """ + + # Remove `type: ignore` when mypy handles dynamic base classes + # https://github.com/python/mypy/issues/2813 + class BaseSchema(base_schema or marshmallow.Schema): # type: ignore + def load(self, data: Mapping, *, many: Optional[bool] = None, **kwargs): + all_loaded = super().load(data, many=many, **kwargs) + many = self.many if many is None else bool(many) + if many: + return [clazz(**loaded) for loaded in all_loaded] + else: + return clazz(**all_loaded) + + return BaseSchema + + +def _get_field_default(field: dataclasses.Field): + """ + Return a marshmallow default value given a dataclass default value + + >>> _get_field_default(dataclasses.field()) + + """ + # Remove `type: ignore` when https://github.com/python/mypy/issues/6910 is fixed + default_factory = field.default_factory # type: ignore + if default_factory is not dataclasses.MISSING: + return default_factory + elif field.default is dataclasses.MISSING: + return marshmallow.missing + return field.default + + +def NewType( + name: str, + typ: Type[_U], + field: Optional[Type[marshmallow.fields.Field]] = None, + **kwargs, +) -> Callable[[_U], _U]: + """DEPRECATED: Use typing.Annotated instead. + NewType creates simple unique types + to which you can attach custom marshmallow attributes. + All the keyword arguments passed to this function will be transmitted + to the marshmallow field constructor. + + >>> import marshmallow.validate + >>> IPv4 = NewType('IPv4', str, validate=marshmallow.validate.Regexp(r'^([0-9]{1,3}\\.){3}[0-9]{1,3}$')) + >>> @dataclass + ... class MyIps: + ... ips: List[IPv4] + >>> MyIps.Schema().load({"ips": ["0.0.0.0", "grumble grumble"]}) + Traceback (most recent call last): + ... + marshmallow.exceptions.ValidationError: {'ips': {1: ['String does not match expected pattern.']}} + >>> MyIps.Schema().load({"ips": ["127.0.0.1"]}) + MyIps(ips=['127.0.0.1']) + + >>> Email = NewType('Email', str, field=marshmallow.fields.Email) + >>> @dataclass + ... class ContactInfo: + ... mail: Email = dataclasses.field(default="anonymous@example.org") + >>> ContactInfo.Schema().load({}) + ContactInfo(mail='anonymous@example.org') + >>> ContactInfo.Schema().load({"mail": "grumble grumble"}) + Traceback (most recent call last): + ... + marshmallow.exceptions.ValidationError: {'mail': ['Not a valid email address.']} + """ + + # noinspection PyTypeHints + new_type = typing_NewType(name, typ) # type: ignore + # noinspection PyTypeHints + new_type._marshmallow_field = field # type: ignore + # noinspection PyTypeHints + new_type._marshmallow_args = kwargs # type: ignore + return new_type + + +if __name__ == "__main__": + import doctest + + doctest.testmod(verbose=True) diff --git a/ccxt/static_dependencies/marshmallow_dataclass/collection_field.py b/ccxt/static_dependencies/marshmallow_dataclass/collection_field.py new file mode 100644 index 0000000..6823b72 --- /dev/null +++ b/ccxt/static_dependencies/marshmallow_dataclass/collection_field.py @@ -0,0 +1,51 @@ +import typing + +import marshmallow + + +class Sequence(marshmallow.fields.List): + """ + A sequence field, basically an immutable version of the list field. + """ + + def _deserialize( # type: ignore[override] + self, + value: typing.Any, + attr: typing.Any, + data: typing.Any, + **kwargs: typing.Any, + ) -> typing.Optional[typing.Sequence[typing.Any]]: + optional_list = super()._deserialize(value, attr, data, **kwargs) + return None if optional_list is None else tuple(optional_list) + + +class Set(marshmallow.fields.List): + """ + A set field. A set is an unordered/mutable collection of unique elements, same for frozenset + except it's immutable. + + Notes: + Beware the a Set guarantees uniqueness in the resulting list but in return the item's order + will be random. So if the order matters, use a List or Sequence ! + """ + + def __init__( + self, + cls_or_instance: typing.Union[marshmallow.fields.Field, type], + frozen: bool = False, + **kwargs, + ): + super().__init__(cls_or_instance, **kwargs) + self.set_type: typing.Type[typing.Union[frozenset, set]] = ( + frozenset if frozen else set + ) + + def _deserialize( # type: ignore[override] + self, + value: typing.Any, + attr: typing.Any, + data: typing.Any, + **kwargs: typing.Any, + ) -> typing.Union[typing.Set[typing.Any], typing.FrozenSet[typing.Any], None]: + optional_list = super()._deserialize(value, attr, data, **kwargs) + return None if optional_list is None else self.set_type(optional_list) diff --git a/ccxt/static_dependencies/marshmallow_dataclass/lazy_class_attribute.py b/ccxt/static_dependencies/marshmallow_dataclass/lazy_class_attribute.py new file mode 100644 index 0000000..2dbe4a4 --- /dev/null +++ b/ccxt/static_dependencies/marshmallow_dataclass/lazy_class_attribute.py @@ -0,0 +1,45 @@ +from typing import Any, Callable, Optional + + +__all__ = ("lazy_class_attribute",) + + +class LazyClassAttribute: + """Descriptor decorator implementing a class-level, read-only + property, which caches its results on the class(es) on which it + operates. + """ + + __slots__ = ("func", "name", "called", "forward_value") + + def __init__( + self, + func: Callable[..., Any], + name: Optional[str] = None, + forward_value: Any = None, + ): + self.func = func + self.name = name + self.called = False + self.forward_value = forward_value + + def __get__(self, instance, cls=None): + if not cls: + cls = type(instance) + + # avoid recursion + if self.called: + return self.forward_value + + self.called = True + + setattr(cls, self.name, self.func()) + + # "getattr" is used to handle bounded methods + return getattr(cls, self.name) + + def __set_name__(self, owner, name): + self.name = self.name or name + + +lazy_class_attribute = LazyClassAttribute diff --git a/ccxt/static_dependencies/marshmallow_dataclass/mypy.py b/ccxt/static_dependencies/marshmallow_dataclass/mypy.py new file mode 100644 index 0000000..d33a5ad --- /dev/null +++ b/ccxt/static_dependencies/marshmallow_dataclass/mypy.py @@ -0,0 +1,71 @@ +import inspect +from typing import Callable, Optional, Type + +from mypy import nodes +from mypy.plugin import DynamicClassDefContext, Plugin +from mypy.plugins import dataclasses + +import marshmallow_dataclass + +_NEW_TYPE_SIG = inspect.signature(marshmallow_dataclass.NewType) + + +def plugin(version: str) -> Type[Plugin]: + return MarshmallowDataclassPlugin + + +class MarshmallowDataclassPlugin(Plugin): + def get_dynamic_class_hook( + self, fullname: str + ) -> Optional[Callable[[DynamicClassDefContext], None]]: + if fullname == "marshmallow_dataclass.NewType": + return new_type_hook + return None + + def get_class_decorator_hook(self, fullname: str): + if fullname == "marshmallow_dataclass.dataclass": + return dataclasses.dataclass_class_maker_callback + return None + + +def new_type_hook(ctx: DynamicClassDefContext) -> None: + """ + Dynamic class hook for :func:`marshmallow_dataclass.NewType`. + + Uses the type of the ``typ`` argument. + """ + typ = _get_arg_by_name(ctx.call, "typ", _NEW_TYPE_SIG) + if not isinstance(typ, nodes.RefExpr): + return + info = typ.node + if not isinstance(info, nodes.TypeInfo): + return + ctx.api.add_symbol_table_node(ctx.name, nodes.SymbolTableNode(nodes.GDEF, info)) + + +def _get_arg_by_name( + call: nodes.CallExpr, name: str, sig: inspect.Signature +) -> Optional[nodes.Expression]: + """ + Get value of argument from a call. + + :return: The argument value, or ``None`` if it cannot be found. + + .. warning:: + This probably doesn't yet work for calls with ``*args`` and/or ``*kwargs``. + """ + args = [] + kwargs = {} + for arg_name, arg_value in zip(call.arg_names, call.args): + if arg_name is None: + args.append(arg_value) + else: + kwargs[arg_name] = arg_value + try: + bound_args = sig.bind(*args, **kwargs) + except TypeError: + return None + try: + return bound_args.arguments[name] + except KeyError: + return None diff --git a/ccxt/static_dependencies/marshmallow_dataclass/py.typed b/ccxt/static_dependencies/marshmallow_dataclass/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/ccxt/static_dependencies/marshmallow_dataclass/typing.py b/ccxt/static_dependencies/marshmallow_dataclass/typing.py new file mode 100644 index 0000000..4db2f15 --- /dev/null +++ b/ccxt/static_dependencies/marshmallow_dataclass/typing.py @@ -0,0 +1,14 @@ +import sys + +import marshmallow.fields + +if sys.version_info >= (3, 9): + from typing import Annotated +else: + from typing_extensions import Annotated + +Url = Annotated[str, marshmallow.fields.Url] +Email = Annotated[str, marshmallow.fields.Email] + +# Aliases +URL = Url diff --git a/ccxt/static_dependencies/marshmallow_dataclass/union_field.py b/ccxt/static_dependencies/marshmallow_dataclass/union_field.py new file mode 100644 index 0000000..ffe998d --- /dev/null +++ b/ccxt/static_dependencies/marshmallow_dataclass/union_field.py @@ -0,0 +1,82 @@ +import copy +import inspect +from typing import List, Tuple, Any, Optional + +import typeguard +from marshmallow import fields, Schema, ValidationError + +try: + from typeguard import TypeCheckError # type: ignore[attr-defined] +except ImportError: + # typeguard < 3 + TypeCheckError = TypeError # type: ignore[misc, assignment] + +if "argname" not in inspect.signature(typeguard.check_type).parameters: + + def _check_type(value, expected_type, argname: str): + return typeguard.check_type(value=value, expected_type=expected_type) + +else: + # typeguard < 3.0.0rc2 + def _check_type(value, expected_type, argname: str): + return typeguard.check_type( # type: ignore[call-overload] + value=value, expected_type=expected_type, argname=argname + ) + + +class Union(fields.Field): + """A union field, composed other `Field` classes or instances. + This field serializes elements based on their type, with one of its child fields. + + Example: :: + + number_or_string = UnionField([ + (float, fields.Float()), + (str, fields.Str()) + ]) + + :param union_fields: A list of types and their associated field instance. + :param kwargs: The same keyword arguments that :class:`Field` receives. + """ + + def __init__(self, union_fields: List[Tuple[type, fields.Field]], **kwargs): + super().__init__(**kwargs) + self.union_fields = union_fields + + def _bind_to_schema(self, field_name: str, schema: Schema) -> None: + super()._bind_to_schema(field_name, schema) + new_union_fields = [] + for typ, field in self.union_fields: + field = copy.deepcopy(field) + field._bind_to_schema(field_name, self) + new_union_fields.append((typ, field)) + + self.union_fields = new_union_fields + + def _serialize(self, value: Any, attr: Optional[str], obj, **kwargs) -> Any: + errors = [] + if value is None: + return value + for typ, field in self.union_fields: + try: + _check_type(value=value, expected_type=typ, argname=attr or "anonymous") + return field._serialize(value, attr, obj, **kwargs) + except TypeCheckError as e: + errors.append(e) + raise TypeError( + f"Unable to serialize value with any of the fields in the union: {errors}" + ) + + def _deserialize(self, value: Any, attr: Optional[str], data, **kwargs) -> Any: + errors = [] + for typ, field in self.union_fields: + try: + result = field.deserialize(value, **kwargs) + _check_type( + value=result, expected_type=typ, argname=attr or "anonymous" + ) + return result + except (TypeCheckError, ValidationError) as e: + errors.append(e) + + raise ValidationError(errors) diff --git a/ccxt/static_dependencies/marshmallow_oneofschema/__init__.py b/ccxt/static_dependencies/marshmallow_oneofschema/__init__.py new file mode 100644 index 0000000..62413dd --- /dev/null +++ b/ccxt/static_dependencies/marshmallow_oneofschema/__init__.py @@ -0,0 +1 @@ +from .one_of_schema import OneOfSchema # noqa diff --git a/ccxt/static_dependencies/marshmallow_oneofschema/one_of_schema.py b/ccxt/static_dependencies/marshmallow_oneofschema/one_of_schema.py new file mode 100644 index 0000000..e28760f --- /dev/null +++ b/ccxt/static_dependencies/marshmallow_oneofschema/one_of_schema.py @@ -0,0 +1,193 @@ +import typing + +from ..marshmallow import Schema, ValidationError + + +class OneOfSchema(Schema): + """ + This is a special kind of schema that actually multiplexes other schemas + based on object type. When serializing values, it uses get_obj_type() method + to get object type name. Then it uses `type_schemas` name-to-Schema mapping + to get schema for that particular object type, serializes object using that + schema and adds an extra "type" field with name of object type. + Deserialization is reverse. + + Example: + + class Foo(object): + def __init__(self, foo): + self.foo = foo + + class Bar(object): + def __init__(self, bar): + self.bar = bar + + class FooSchema(marshmallow.Schema): + foo = marshmallow.fields.String(required=True) + + @marshmallow.post_load + def make_foo(self, data, **kwargs): + return Foo(**data) + + class BarSchema(marshmallow.Schema): + bar = marshmallow.fields.Integer(required=True) + + @marshmallow.post_load + def make_bar(self, data, **kwargs): + return Bar(**data) + + class MyUberSchema(marshmallow.OneOfSchema): + type_schemas = { + 'foo': FooSchema, + 'bar': BarSchema, + } + + def get_obj_type(self, obj): + if isinstance(obj, Foo): + return 'foo' + elif isinstance(obj, Bar): + return 'bar' + else: + raise Exception('Unknown object type: %s' % repr(obj)) + + MyUberSchema().dump([Foo(foo='hello'), Bar(bar=123)], many=True) + # => [{'type': 'foo', 'foo': 'hello'}, {'type': 'bar', 'bar': 123}] + + You can control type field name added to serialized object representation by + setting `type_field` class property. + """ + + type_field = "type" + type_field_remove = True + type_schemas: typing.Mapping[str, typing.Union[typing.Type[Schema], Schema]] = {} + + def get_obj_type(self, obj): + """Returns name of the schema during dump() calls, given the object + being dumped.""" + return obj.__class__.__name__ + + def get_data_type(self, data): + """Returns name of the schema during load() calls, given the data being + loaded. Defaults to looking up `type_field` in the data.""" + data_type = data.get(self.type_field) + if self.type_field in data and self.type_field_remove: + data.pop(self.type_field) + return data_type + + def dump(self, obj, *, many=None, **kwargs): + errors = {} + result_data = [] + result_errors = {} + many = self.many if many is None else bool(many) + if not many: + result = result_data = self._dump(obj, **kwargs) + else: + for idx, o in enumerate(obj): + try: + result = self._dump(o, **kwargs) + result_data.append(result) + except ValidationError as error: + result_errors[idx] = error.normalized_messages() + result_data.append(error.valid_data) + + result = result_data + errors = result_errors + + if not errors: + return result + else: + exc = ValidationError(errors, data=obj, valid_data=result) + raise exc + + def _dump(self, obj, *, update_fields=True, **kwargs): + obj_type = self.get_obj_type(obj) + if obj_type is None: + return ( + None, + {"_schema": f"Unknown object class: {obj.__class__.__name__}"}, + ) + + type_schema = self.type_schemas.get(obj_type) + if not type_schema: + return None, {"_schema": f"Unsupported object type: {obj_type}"} + + schema = type_schema if isinstance(type_schema, Schema) else type_schema() + + schema.context.update(getattr(self, "context", {})) + + result = schema.dump(obj, many=False, **kwargs) + if result is not None: + result[self.type_field] = obj_type + return result + + def load(self, data, *, many=None, partial=None, unknown=None, **kwargs): + errors = {} + result_data = [] + result_errors = {} + many = self.many if many is None else bool(many) + if partial is None: + partial = self.partial + if not many: + try: + result = result_data = self._load( + data, partial=partial, unknown=unknown, **kwargs + ) + # result_data.append(result) + except ValidationError as error: + result_errors = error.normalized_messages() + result_data.append(error.valid_data) + else: + for idx, item in enumerate(data): + try: + result = self._load(item, partial=partial, **kwargs) + result_data.append(result) + except ValidationError as error: + result_errors[idx] = error.normalized_messages() + result_data.append(error.valid_data) + + result = result_data + errors = result_errors + + if not errors: + return result + else: + exc = ValidationError(errors, data=data, valid_data=result) + raise exc + + def _load(self, data, *, partial=None, unknown=None, **kwargs): + if not isinstance(data, dict): + raise ValidationError({"_schema": f"Invalid data type: {data}"}) + + data = dict(data) + unknown = unknown or self.unknown + data_type = self.get_data_type(data) + + if data_type is None: + raise ValidationError( + {self.type_field: ["Missing data for required field."]} + ) + + try: + type_schema = self.type_schemas.get(data_type) + except TypeError as error: + # data_type could be unhashable + raise ValidationError( + {self.type_field: [f"Invalid value: {data_type}"]} + ) from error + if not type_schema: + raise ValidationError( + {self.type_field: [f"Unsupported value: {data_type}"]} + ) + + schema = type_schema if isinstance(type_schema, Schema) else type_schema() + + schema.context.update(getattr(self, "context", {})) + + return schema.load(data, many=False, partial=partial, unknown=unknown, **kwargs) + + def validate(self, data, *, many=None, partial=None): + try: + self.load(data, many=many, partial=partial) + except ValidationError as ve: + return ve.messages + return {} diff --git a/ccxt/static_dependencies/marshmallow_oneofschema/py.typed b/ccxt/static_dependencies/marshmallow_oneofschema/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/ccxt/static_dependencies/msgpack/__init__.py b/ccxt/static_dependencies/msgpack/__init__.py new file mode 100644 index 0000000..60a088c --- /dev/null +++ b/ccxt/static_dependencies/msgpack/__init__.py @@ -0,0 +1,55 @@ +from .exceptions import * +from .ext import ExtType, Timestamp + +import os + + +version = (1, 0, 7) +__version__ = "1.0.7" + + +if os.environ.get("MSGPACK_PUREPYTHON"): + from .fallback import Packer, unpackb, Unpacker +else: + try: + from ._cmsgpack import Packer, unpackb, Unpacker + except ImportError: + from .fallback import Packer, unpackb, Unpacker + + +def pack(o, stream, **kwargs): + """ + Pack object `o` and write it to `stream` + + See :class:`Packer` for options. + """ + packer = Packer(**kwargs) + stream.write(packer.pack(o)) + + +def packb(o, **kwargs): + """ + Pack object `o` and return packed bytes + + See :class:`Packer` for options. + """ + return Packer(**kwargs).pack(o) + + +def unpack(stream, **kwargs): + """ + Unpack an object from `stream`. + + Raises `ExtraData` when `stream` contains extra bytes. + See :class:`Unpacker` for options. + """ + data = stream.read() + return unpackb(data, **kwargs) + + +# alias for compatibility to simplejson/marshal/pickle. +load = unpack +loads = unpackb + +dump = pack +dumps = packb diff --git a/ccxt/static_dependencies/msgpack/__pycache__/__init__.cpython-311.pyc b/ccxt/static_dependencies/msgpack/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..75feb91 Binary files /dev/null and b/ccxt/static_dependencies/msgpack/__pycache__/__init__.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/msgpack/__pycache__/exceptions.cpython-311.pyc b/ccxt/static_dependencies/msgpack/__pycache__/exceptions.cpython-311.pyc new file mode 100644 index 0000000..7b93578 Binary files /dev/null and b/ccxt/static_dependencies/msgpack/__pycache__/exceptions.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/msgpack/__pycache__/ext.cpython-311.pyc b/ccxt/static_dependencies/msgpack/__pycache__/ext.cpython-311.pyc new file mode 100644 index 0000000..3300523 Binary files /dev/null and b/ccxt/static_dependencies/msgpack/__pycache__/ext.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/msgpack/__pycache__/fallback.cpython-311.pyc b/ccxt/static_dependencies/msgpack/__pycache__/fallback.cpython-311.pyc new file mode 100644 index 0000000..54b9086 Binary files /dev/null and b/ccxt/static_dependencies/msgpack/__pycache__/fallback.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/msgpack/_cmsgpack.pyx b/ccxt/static_dependencies/msgpack/_cmsgpack.pyx new file mode 100644 index 0000000..1faaac3 --- /dev/null +++ b/ccxt/static_dependencies/msgpack/_cmsgpack.pyx @@ -0,0 +1,11 @@ +# coding: utf-8 +#cython: embedsignature=True, c_string_encoding=ascii, language_level=3 +from cpython.datetime cimport import_datetime, datetime_new +import_datetime() + +import datetime +cdef object utc = datetime.timezone.utc +cdef object epoch = datetime_new(1970, 1, 1, 0, 0, 0, 0, tz=utc) + +include "_packer.pyx" +include "_unpacker.pyx" diff --git a/ccxt/static_dependencies/msgpack/_packer.pyx b/ccxt/static_dependencies/msgpack/_packer.pyx new file mode 100644 index 0000000..3c39867 --- /dev/null +++ b/ccxt/static_dependencies/msgpack/_packer.pyx @@ -0,0 +1,374 @@ +# coding: utf-8 + +from cpython cimport * +from cpython.bytearray cimport PyByteArray_Check, PyByteArray_CheckExact +from cpython.datetime cimport ( + PyDateTime_CheckExact, PyDelta_CheckExact, + datetime_tzinfo, timedelta_days, timedelta_seconds, timedelta_microseconds, +) + +cdef ExtType +cdef Timestamp + +from .ext import ExtType, Timestamp + + +cdef extern from "Python.h": + + int PyMemoryView_Check(object obj) + char* PyUnicode_AsUTF8AndSize(object obj, Py_ssize_t *l) except NULL + + +cdef extern from "pack.h": + struct msgpack_packer: + char* buf + size_t length + size_t buf_size + bint use_bin_type + + int msgpack_pack_int(msgpack_packer* pk, int d) + int msgpack_pack_nil(msgpack_packer* pk) + int msgpack_pack_true(msgpack_packer* pk) + int msgpack_pack_false(msgpack_packer* pk) + int msgpack_pack_long(msgpack_packer* pk, long d) + int msgpack_pack_long_long(msgpack_packer* pk, long long d) + int msgpack_pack_unsigned_long_long(msgpack_packer* pk, unsigned long long d) + int msgpack_pack_float(msgpack_packer* pk, float d) + int msgpack_pack_double(msgpack_packer* pk, double d) + int msgpack_pack_array(msgpack_packer* pk, size_t l) + int msgpack_pack_map(msgpack_packer* pk, size_t l) + int msgpack_pack_raw(msgpack_packer* pk, size_t l) + int msgpack_pack_bin(msgpack_packer* pk, size_t l) + int msgpack_pack_raw_body(msgpack_packer* pk, char* body, size_t l) + int msgpack_pack_ext(msgpack_packer* pk, char typecode, size_t l) + int msgpack_pack_timestamp(msgpack_packer* x, long long seconds, unsigned long nanoseconds); + int msgpack_pack_unicode(msgpack_packer* pk, object o, long long limit) + +cdef extern from "buff_converter.h": + object buff_to_buff(char *, Py_ssize_t) + +cdef int DEFAULT_RECURSE_LIMIT=511 +cdef long long ITEM_LIMIT = (2**32)-1 + + +cdef inline int PyBytesLike_Check(object o): + return PyBytes_Check(o) or PyByteArray_Check(o) + + +cdef inline int PyBytesLike_CheckExact(object o): + return PyBytes_CheckExact(o) or PyByteArray_CheckExact(o) + + +cdef class Packer(object): + """ + MessagePack Packer + + Usage:: + + packer = Packer() + astream.write(packer.pack(a)) + astream.write(packer.pack(b)) + + Packer's constructor has some keyword arguments: + + :param default: + When specified, it should be callable. + Convert user type to builtin type that Packer supports. + See also simplejson's document. + + :param bool use_single_float: + Use single precision float type for float. (default: False) + + :param bool autoreset: + Reset buffer after each pack and return its content as `bytes`. (default: True). + If set this to false, use `bytes()` to get content and `.reset()` to clear buffer. + + :param bool use_bin_type: + Use bin type introduced in msgpack spec 2.0 for bytes. + It also enables str8 type for unicode. (default: True) + + :param bool strict_types: + If set to true, types will be checked to be exact. Derived classes + from serializeable types will not be serialized and will be + treated as unsupported type and forwarded to default. + Additionally tuples will not be serialized as lists. + This is useful when trying to implement accurate serialization + for python types. + + :param bool datetime: + If set to true, datetime with tzinfo is packed into Timestamp type. + Note that the tzinfo is stripped in the timestamp. + You can get UTC datetime with `timestamp=3` option of the Unpacker. + + :param str unicode_errors: + The error handler for encoding unicode. (default: 'strict') + DO NOT USE THIS!! This option is kept for very specific usage. + """ + cdef msgpack_packer pk + cdef object _default + cdef object _berrors + cdef const char *unicode_errors + cdef bint strict_types + cdef bint use_float + cdef bint autoreset + cdef bint datetime + + def __cinit__(self): + cdef int buf_size = 1024*1024 + self.pk.buf = PyMem_Malloc(buf_size) + if self.pk.buf == NULL: + raise MemoryError("Unable to allocate internal buffer.") + self.pk.buf_size = buf_size + self.pk.length = 0 + + def __init__(self, *, default=None, + bint use_single_float=False, bint autoreset=True, bint use_bin_type=True, + bint strict_types=False, bint datetime=False, unicode_errors=None): + self.use_float = use_single_float + self.strict_types = strict_types + self.autoreset = autoreset + self.datetime = datetime + self.pk.use_bin_type = use_bin_type + if default is not None: + if not PyCallable_Check(default): + raise TypeError("default must be a callable.") + self._default = default + + self._berrors = unicode_errors + if unicode_errors is None: + self.unicode_errors = NULL + else: + self.unicode_errors = self._berrors + + def __dealloc__(self): + PyMem_Free(self.pk.buf) + self.pk.buf = NULL + + cdef int _pack(self, object o, int nest_limit=DEFAULT_RECURSE_LIMIT) except -1: + cdef long long llval + cdef unsigned long long ullval + cdef unsigned long ulval + cdef long longval + cdef float fval + cdef double dval + cdef char* rawval + cdef int ret + cdef dict d + cdef Py_ssize_t L + cdef int default_used = 0 + cdef bint strict_types = self.strict_types + cdef Py_buffer view + + if nest_limit < 0: + raise ValueError("recursion limit exceeded.") + + while True: + if o is None: + ret = msgpack_pack_nil(&self.pk) + elif o is True: + ret = msgpack_pack_true(&self.pk) + elif o is False: + ret = msgpack_pack_false(&self.pk) + elif PyLong_CheckExact(o) if strict_types else PyLong_Check(o): + # PyInt_Check(long) is True for Python 3. + # So we should test long before int. + try: + if o > 0: + ullval = o + ret = msgpack_pack_unsigned_long_long(&self.pk, ullval) + else: + llval = o + ret = msgpack_pack_long_long(&self.pk, llval) + except OverflowError as oe: + if not default_used and self._default is not None: + o = self._default(o) + default_used = True + continue + else: + raise OverflowError("Integer value out of range") + elif PyInt_CheckExact(o) if strict_types else PyInt_Check(o): + longval = o + ret = msgpack_pack_long(&self.pk, longval) + elif PyFloat_CheckExact(o) if strict_types else PyFloat_Check(o): + if self.use_float: + fval = o + ret = msgpack_pack_float(&self.pk, fval) + else: + dval = o + ret = msgpack_pack_double(&self.pk, dval) + elif PyBytesLike_CheckExact(o) if strict_types else PyBytesLike_Check(o): + L = Py_SIZE(o) + if L > ITEM_LIMIT: + PyErr_Format(ValueError, b"%.200s object is too large", Py_TYPE(o).tp_name) + rawval = o + ret = msgpack_pack_bin(&self.pk, L) + if ret == 0: + ret = msgpack_pack_raw_body(&self.pk, rawval, L) + elif PyUnicode_CheckExact(o) if strict_types else PyUnicode_Check(o): + if self.unicode_errors == NULL: + ret = msgpack_pack_unicode(&self.pk, o, ITEM_LIMIT); + if ret == -2: + raise ValueError("unicode string is too large") + else: + o = PyUnicode_AsEncodedString(o, NULL, self.unicode_errors) + L = Py_SIZE(o) + if L > ITEM_LIMIT: + raise ValueError("unicode string is too large") + ret = msgpack_pack_raw(&self.pk, L) + if ret == 0: + rawval = o + ret = msgpack_pack_raw_body(&self.pk, rawval, L) + elif PyDict_CheckExact(o): + d = o + L = len(d) + if L > ITEM_LIMIT: + raise ValueError("dict is too large") + ret = msgpack_pack_map(&self.pk, L) + if ret == 0: + for k, v in d.items(): + ret = self._pack(k, nest_limit-1) + if ret != 0: break + ret = self._pack(v, nest_limit-1) + if ret != 0: break + elif not strict_types and PyDict_Check(o): + L = len(o) + if L > ITEM_LIMIT: + raise ValueError("dict is too large") + ret = msgpack_pack_map(&self.pk, L) + if ret == 0: + for k, v in o.items(): + ret = self._pack(k, nest_limit-1) + if ret != 0: break + ret = self._pack(v, nest_limit-1) + if ret != 0: break + elif type(o) is ExtType if strict_types else isinstance(o, ExtType): + # This should be before Tuple because ExtType is namedtuple. + longval = o.code + rawval = o.data + L = len(o.data) + if L > ITEM_LIMIT: + raise ValueError("EXT data is too large") + ret = msgpack_pack_ext(&self.pk, longval, L) + ret = msgpack_pack_raw_body(&self.pk, rawval, L) + elif type(o) is Timestamp: + llval = o.seconds + ulval = o.nanoseconds + ret = msgpack_pack_timestamp(&self.pk, llval, ulval) + elif PyList_CheckExact(o) if strict_types else (PyTuple_Check(o) or PyList_Check(o)): + L = Py_SIZE(o) + if L > ITEM_LIMIT: + raise ValueError("list is too large") + ret = msgpack_pack_array(&self.pk, L) + if ret == 0: + for v in o: + ret = self._pack(v, nest_limit-1) + if ret != 0: break + elif PyMemoryView_Check(o): + if PyObject_GetBuffer(o, &view, PyBUF_SIMPLE) != 0: + raise ValueError("could not get buffer for memoryview") + L = view.len + if L > ITEM_LIMIT: + PyBuffer_Release(&view); + raise ValueError("memoryview is too large") + ret = msgpack_pack_bin(&self.pk, L) + if ret == 0: + ret = msgpack_pack_raw_body(&self.pk, view.buf, L) + PyBuffer_Release(&view); + elif self.datetime and PyDateTime_CheckExact(o) and datetime_tzinfo(o) is not None: + delta = o - epoch + if not PyDelta_CheckExact(delta): + raise ValueError("failed to calculate delta") + llval = timedelta_days(delta) * (24*60*60) + timedelta_seconds(delta) + ulval = timedelta_microseconds(delta) * 1000 + ret = msgpack_pack_timestamp(&self.pk, llval, ulval) + elif not default_used and self._default: + o = self._default(o) + default_used = 1 + continue + elif self.datetime and PyDateTime_CheckExact(o): + PyErr_Format(ValueError, b"can not serialize '%.200s' object where tzinfo=None", Py_TYPE(o).tp_name) + else: + PyErr_Format(TypeError, b"can not serialize '%.200s' object", Py_TYPE(o).tp_name) + return ret + + cpdef pack(self, object obj): + cdef int ret + try: + ret = self._pack(obj, DEFAULT_RECURSE_LIMIT) + except: + self.pk.length = 0 + raise + if ret: # should not happen. + raise RuntimeError("internal error") + if self.autoreset: + buf = PyBytes_FromStringAndSize(self.pk.buf, self.pk.length) + self.pk.length = 0 + return buf + + def pack_ext_type(self, typecode, data): + msgpack_pack_ext(&self.pk, typecode, len(data)) + msgpack_pack_raw_body(&self.pk, data, len(data)) + + def pack_array_header(self, long long size): + if size > ITEM_LIMIT: + raise ValueError + cdef int ret = msgpack_pack_array(&self.pk, size) + if ret == -1: + raise MemoryError + elif ret: # should not happen + raise TypeError + if self.autoreset: + buf = PyBytes_FromStringAndSize(self.pk.buf, self.pk.length) + self.pk.length = 0 + return buf + + def pack_map_header(self, long long size): + if size > ITEM_LIMIT: + raise ValueError + cdef int ret = msgpack_pack_map(&self.pk, size) + if ret == -1: + raise MemoryError + elif ret: # should not happen + raise TypeError + if self.autoreset: + buf = PyBytes_FromStringAndSize(self.pk.buf, self.pk.length) + self.pk.length = 0 + return buf + + def pack_map_pairs(self, object pairs): + """ + Pack *pairs* as msgpack map type. + + *pairs* should be a sequence of pairs. + (`len(pairs)` and `for k, v in pairs:` should be supported.) + """ + cdef int ret = msgpack_pack_map(&self.pk, len(pairs)) + if ret == 0: + for k, v in pairs: + ret = self._pack(k) + if ret != 0: break + ret = self._pack(v) + if ret != 0: break + if ret == -1: + raise MemoryError + elif ret: # should not happen + raise TypeError + if self.autoreset: + buf = PyBytes_FromStringAndSize(self.pk.buf, self.pk.length) + self.pk.length = 0 + return buf + + def reset(self): + """Reset internal buffer. + + This method is useful only when autoreset=False. + """ + self.pk.length = 0 + + def bytes(self): + """Return internal buffer contents as bytes object""" + return PyBytes_FromStringAndSize(self.pk.buf, self.pk.length) + + def getbuffer(self): + """Return view of internal buffer.""" + return buff_to_buff(self.pk.buf, self.pk.length) diff --git a/ccxt/static_dependencies/msgpack/_unpacker.pyx b/ccxt/static_dependencies/msgpack/_unpacker.pyx new file mode 100644 index 0000000..56126f4 --- /dev/null +++ b/ccxt/static_dependencies/msgpack/_unpacker.pyx @@ -0,0 +1,547 @@ +# coding: utf-8 + +from cpython cimport * +cdef extern from "Python.h": + ctypedef struct PyObject + object PyMemoryView_GetContiguous(object obj, int buffertype, char order) + +from libc.stdlib cimport * +from libc.string cimport * +from libc.limits cimport * +from libc.stdint cimport uint64_t + +from .exceptions import ( + BufferFull, + OutOfData, + ExtraData, + FormatError, + StackError, +) +from .ext import ExtType, Timestamp + +cdef object giga = 1_000_000_000 + + +cdef extern from "unpack.h": + ctypedef struct msgpack_user: + bint use_list + bint raw + bint has_pairs_hook # call object_hook with k-v pairs + bint strict_map_key + int timestamp + PyObject* object_hook + PyObject* list_hook + PyObject* ext_hook + PyObject* timestamp_t + PyObject *giga; + PyObject *utc; + char *unicode_errors + Py_ssize_t max_str_len + Py_ssize_t max_bin_len + Py_ssize_t max_array_len + Py_ssize_t max_map_len + Py_ssize_t max_ext_len + + ctypedef struct unpack_context: + msgpack_user user + PyObject* obj + Py_ssize_t count + + ctypedef int (*execute_fn)(unpack_context* ctx, const char* data, + Py_ssize_t len, Py_ssize_t* off) except? -1 + execute_fn unpack_construct + execute_fn unpack_skip + execute_fn read_array_header + execute_fn read_map_header + void unpack_init(unpack_context* ctx) + object unpack_data(unpack_context* ctx) + void unpack_clear(unpack_context* ctx) + +cdef inline init_ctx(unpack_context *ctx, + object object_hook, object object_pairs_hook, + object list_hook, object ext_hook, + bint use_list, bint raw, int timestamp, + bint strict_map_key, + const char* unicode_errors, + Py_ssize_t max_str_len, Py_ssize_t max_bin_len, + Py_ssize_t max_array_len, Py_ssize_t max_map_len, + Py_ssize_t max_ext_len): + unpack_init(ctx) + ctx.user.use_list = use_list + ctx.user.raw = raw + ctx.user.strict_map_key = strict_map_key + ctx.user.object_hook = ctx.user.list_hook = NULL + ctx.user.max_str_len = max_str_len + ctx.user.max_bin_len = max_bin_len + ctx.user.max_array_len = max_array_len + ctx.user.max_map_len = max_map_len + ctx.user.max_ext_len = max_ext_len + + if object_hook is not None and object_pairs_hook is not None: + raise TypeError("object_pairs_hook and object_hook are mutually exclusive.") + + if object_hook is not None: + if not PyCallable_Check(object_hook): + raise TypeError("object_hook must be a callable.") + ctx.user.object_hook = object_hook + + if object_pairs_hook is None: + ctx.user.has_pairs_hook = False + else: + if not PyCallable_Check(object_pairs_hook): + raise TypeError("object_pairs_hook must be a callable.") + ctx.user.object_hook = object_pairs_hook + ctx.user.has_pairs_hook = True + + if list_hook is not None: + if not PyCallable_Check(list_hook): + raise TypeError("list_hook must be a callable.") + ctx.user.list_hook = list_hook + + if ext_hook is not None: + if not PyCallable_Check(ext_hook): + raise TypeError("ext_hook must be a callable.") + ctx.user.ext_hook = ext_hook + + if timestamp < 0 or 3 < timestamp: + raise ValueError("timestamp must be 0..3") + + # Add Timestamp type to the user object so it may be used in unpack.h + ctx.user.timestamp = timestamp + ctx.user.timestamp_t = Timestamp + ctx.user.giga = giga + ctx.user.utc = utc + ctx.user.unicode_errors = unicode_errors + +def default_read_extended_type(typecode, data): + raise NotImplementedError("Cannot decode extended type with typecode=%d" % typecode) + +cdef inline int get_data_from_buffer(object obj, + Py_buffer *view, + char **buf, + Py_ssize_t *buffer_len) except 0: + cdef object contiguous + cdef Py_buffer tmp + if PyObject_GetBuffer(obj, view, PyBUF_FULL_RO) == -1: + raise + if view.itemsize != 1: + PyBuffer_Release(view) + raise BufferError("cannot unpack from multi-byte object") + if PyBuffer_IsContiguous(view, b'A') == 0: + PyBuffer_Release(view) + # create a contiguous copy and get buffer + contiguous = PyMemoryView_GetContiguous(obj, PyBUF_READ, b'C') + PyObject_GetBuffer(contiguous, view, PyBUF_SIMPLE) + # view must hold the only reference to contiguous, + # so memory is freed when view is released + Py_DECREF(contiguous) + buffer_len[0] = view.len + buf[0] = view.buf + return 1 + + +def unpackb(object packed, *, object object_hook=None, object list_hook=None, + bint use_list=True, bint raw=False, int timestamp=0, bint strict_map_key=True, + unicode_errors=None, + object_pairs_hook=None, ext_hook=ExtType, + Py_ssize_t max_str_len=-1, + Py_ssize_t max_bin_len=-1, + Py_ssize_t max_array_len=-1, + Py_ssize_t max_map_len=-1, + Py_ssize_t max_ext_len=-1): + """ + Unpack packed_bytes to object. Returns an unpacked object. + + Raises ``ExtraData`` when *packed* contains extra bytes. + Raises ``ValueError`` when *packed* is incomplete. + Raises ``FormatError`` when *packed* is not valid msgpack. + Raises ``StackError`` when *packed* contains too nested. + Other exceptions can be raised during unpacking. + + See :class:`Unpacker` for options. + + *max_xxx_len* options are configured automatically from ``len(packed)``. + """ + cdef unpack_context ctx + cdef Py_ssize_t off = 0 + cdef int ret + + cdef Py_buffer view + cdef char* buf = NULL + cdef Py_ssize_t buf_len + cdef const char* cerr = NULL + + if unicode_errors is not None: + cerr = unicode_errors + + get_data_from_buffer(packed, &view, &buf, &buf_len) + + if max_str_len == -1: + max_str_len = buf_len + if max_bin_len == -1: + max_bin_len = buf_len + if max_array_len == -1: + max_array_len = buf_len + if max_map_len == -1: + max_map_len = buf_len//2 + if max_ext_len == -1: + max_ext_len = buf_len + + try: + init_ctx(&ctx, object_hook, object_pairs_hook, list_hook, ext_hook, + use_list, raw, timestamp, strict_map_key, cerr, + max_str_len, max_bin_len, max_array_len, max_map_len, max_ext_len) + ret = unpack_construct(&ctx, buf, buf_len, &off) + finally: + PyBuffer_Release(&view); + + if ret == 1: + obj = unpack_data(&ctx) + if off < buf_len: + raise ExtraData(obj, PyBytes_FromStringAndSize(buf+off, buf_len-off)) + return obj + unpack_clear(&ctx) + if ret == 0: + raise ValueError("Unpack failed: incomplete input") + elif ret == -2: + raise FormatError + elif ret == -3: + raise StackError + raise ValueError("Unpack failed: error = %d" % (ret,)) + + +cdef class Unpacker(object): + """Streaming unpacker. + + Arguments: + + :param file_like: + File-like object having `.read(n)` method. + If specified, unpacker reads serialized data from it and `.feed()` is not usable. + + :param int read_size: + Used as `file_like.read(read_size)`. (default: `min(16*1024, max_buffer_size)`) + + :param bool use_list: + If true, unpack msgpack array to Python list. + Otherwise, unpack to Python tuple. (default: True) + + :param bool raw: + If true, unpack msgpack raw to Python bytes. + Otherwise, unpack to Python str by decoding with UTF-8 encoding (default). + + :param int timestamp: + Control how timestamp type is unpacked: + + 0 - Timestamp + 1 - float (Seconds from the EPOCH) + 2 - int (Nanoseconds from the EPOCH) + 3 - datetime.datetime (UTC). + + :param bool strict_map_key: + If true (default), only str or bytes are accepted for map (dict) keys. + + :param object_hook: + When specified, it should be callable. + Unpacker calls it with a dict argument after unpacking msgpack map. + (See also simplejson) + + :param object_pairs_hook: + When specified, it should be callable. + Unpacker calls it with a list of key-value pairs after unpacking msgpack map. + (See also simplejson) + + :param str unicode_errors: + The error handler for decoding unicode. (default: 'strict') + This option should be used only when you have msgpack data which + contains invalid UTF-8 string. + + :param int max_buffer_size: + Limits size of data waiting unpacked. 0 means 2**32-1. + The default value is 100*1024*1024 (100MiB). + Raises `BufferFull` exception when it is insufficient. + You should set this parameter when unpacking data from untrusted source. + + :param int max_str_len: + Deprecated, use *max_buffer_size* instead. + Limits max length of str. (default: max_buffer_size) + + :param int max_bin_len: + Deprecated, use *max_buffer_size* instead. + Limits max length of bin. (default: max_buffer_size) + + :param int max_array_len: + Limits max length of array. + (default: max_buffer_size) + + :param int max_map_len: + Limits max length of map. + (default: max_buffer_size//2) + + :param int max_ext_len: + Deprecated, use *max_buffer_size* instead. + Limits max size of ext type. (default: max_buffer_size) + + Example of streaming deserialize from file-like object:: + + unpacker = Unpacker(file_like) + for o in unpacker: + process(o) + + Example of streaming deserialize from socket:: + + unpacker = Unpacker() + while True: + buf = sock.recv(1024**2) + if not buf: + break + unpacker.feed(buf) + for o in unpacker: + process(o) + + Raises ``ExtraData`` when *packed* contains extra bytes. + Raises ``OutOfData`` when *packed* is incomplete. + Raises ``FormatError`` when *packed* is not valid msgpack. + Raises ``StackError`` when *packed* contains too nested. + Other exceptions can be raised during unpacking. + """ + cdef unpack_context ctx + cdef char* buf + cdef Py_ssize_t buf_size, buf_head, buf_tail + cdef object file_like + cdef object file_like_read + cdef Py_ssize_t read_size + # To maintain refcnt. + cdef object object_hook, object_pairs_hook, list_hook, ext_hook + cdef object unicode_errors + cdef Py_ssize_t max_buffer_size + cdef uint64_t stream_offset + + def __cinit__(self): + self.buf = NULL + + def __dealloc__(self): + PyMem_Free(self.buf) + self.buf = NULL + + def __init__(self, file_like=None, *, Py_ssize_t read_size=0, + bint use_list=True, bint raw=False, int timestamp=0, bint strict_map_key=True, + object object_hook=None, object object_pairs_hook=None, object list_hook=None, + unicode_errors=None, Py_ssize_t max_buffer_size=100*1024*1024, + object ext_hook=ExtType, + Py_ssize_t max_str_len=-1, + Py_ssize_t max_bin_len=-1, + Py_ssize_t max_array_len=-1, + Py_ssize_t max_map_len=-1, + Py_ssize_t max_ext_len=-1): + cdef const char *cerr=NULL + + self.object_hook = object_hook + self.object_pairs_hook = object_pairs_hook + self.list_hook = list_hook + self.ext_hook = ext_hook + + self.file_like = file_like + if file_like: + self.file_like_read = file_like.read + if not PyCallable_Check(self.file_like_read): + raise TypeError("`file_like.read` must be a callable.") + + if not max_buffer_size: + max_buffer_size = INT_MAX + if max_str_len == -1: + max_str_len = max_buffer_size + if max_bin_len == -1: + max_bin_len = max_buffer_size + if max_array_len == -1: + max_array_len = max_buffer_size + if max_map_len == -1: + max_map_len = max_buffer_size//2 + if max_ext_len == -1: + max_ext_len = max_buffer_size + + if read_size > max_buffer_size: + raise ValueError("read_size should be less or equal to max_buffer_size") + if not read_size: + read_size = min(max_buffer_size, 1024**2) + + self.max_buffer_size = max_buffer_size + self.read_size = read_size + self.buf = PyMem_Malloc(read_size) + if self.buf == NULL: + raise MemoryError("Unable to allocate internal buffer.") + self.buf_size = read_size + self.buf_head = 0 + self.buf_tail = 0 + self.stream_offset = 0 + + if unicode_errors is not None: + self.unicode_errors = unicode_errors + cerr = unicode_errors + + init_ctx(&self.ctx, object_hook, object_pairs_hook, list_hook, + ext_hook, use_list, raw, timestamp, strict_map_key, cerr, + max_str_len, max_bin_len, max_array_len, + max_map_len, max_ext_len) + + def feed(self, object next_bytes): + """Append `next_bytes` to internal buffer.""" + cdef Py_buffer pybuff + cdef char* buf + cdef Py_ssize_t buf_len + + if self.file_like is not None: + raise AssertionError( + "unpacker.feed() is not be able to use with `file_like`.") + + get_data_from_buffer(next_bytes, &pybuff, &buf, &buf_len) + try: + self.append_buffer(buf, buf_len) + finally: + PyBuffer_Release(&pybuff) + + cdef append_buffer(self, void* _buf, Py_ssize_t _buf_len): + cdef: + char* buf = self.buf + char* new_buf + Py_ssize_t head = self.buf_head + Py_ssize_t tail = self.buf_tail + Py_ssize_t buf_size = self.buf_size + Py_ssize_t new_size + + if tail + _buf_len > buf_size: + if ((tail - head) + _buf_len) <= buf_size: + # move to front. + memmove(buf, buf + head, tail - head) + tail -= head + head = 0 + else: + # expand buffer. + new_size = (tail-head) + _buf_len + if new_size > self.max_buffer_size: + raise BufferFull + new_size = min(new_size*2, self.max_buffer_size) + new_buf = PyMem_Malloc(new_size) + if new_buf == NULL: + # self.buf still holds old buffer and will be freed during + # obj destruction + raise MemoryError("Unable to enlarge internal buffer.") + memcpy(new_buf, buf + head, tail - head) + PyMem_Free(buf) + + buf = new_buf + buf_size = new_size + tail -= head + head = 0 + + memcpy(buf + tail, (_buf), _buf_len) + self.buf = buf + self.buf_head = head + self.buf_size = buf_size + self.buf_tail = tail + _buf_len + + cdef int read_from_file(self) except -1: + cdef Py_ssize_t remains = self.max_buffer_size - (self.buf_tail - self.buf_head) + if remains <= 0: + raise BufferFull + + next_bytes = self.file_like_read(min(self.read_size, remains)) + if next_bytes: + self.append_buffer(PyBytes_AsString(next_bytes), PyBytes_Size(next_bytes)) + else: + self.file_like = None + return 0 + + cdef object _unpack(self, execute_fn execute, bint iter=0): + cdef int ret + cdef object obj + cdef Py_ssize_t prev_head + + while 1: + prev_head = self.buf_head + if prev_head < self.buf_tail: + ret = execute(&self.ctx, self.buf, self.buf_tail, &self.buf_head) + self.stream_offset += self.buf_head - prev_head + else: + ret = 0 + + if ret == 1: + obj = unpack_data(&self.ctx) + unpack_init(&self.ctx) + return obj + elif ret == 0: + if self.file_like is not None: + self.read_from_file() + continue + if iter: + raise StopIteration("No more data to unpack.") + else: + raise OutOfData("No more data to unpack.") + elif ret == -2: + raise FormatError + elif ret == -3: + raise StackError + else: + raise ValueError("Unpack failed: error = %d" % (ret,)) + + def read_bytes(self, Py_ssize_t nbytes): + """Read a specified number of raw bytes from the stream""" + cdef Py_ssize_t nread + nread = min(self.buf_tail - self.buf_head, nbytes) + ret = PyBytes_FromStringAndSize(self.buf + self.buf_head, nread) + self.buf_head += nread + if nread < nbytes and self.file_like is not None: + ret += self.file_like.read(nbytes - nread) + nread = len(ret) + self.stream_offset += nread + return ret + + def unpack(self): + """Unpack one object + + Raises `OutOfData` when there are no more bytes to unpack. + """ + return self._unpack(unpack_construct) + + def skip(self): + """Read and ignore one object, returning None + + Raises `OutOfData` when there are no more bytes to unpack. + """ + return self._unpack(unpack_skip) + + def read_array_header(self): + """assuming the next object is an array, return its size n, such that + the next n unpack() calls will iterate over its contents. + + Raises `OutOfData` when there are no more bytes to unpack. + """ + return self._unpack(read_array_header) + + def read_map_header(self): + """assuming the next object is a map, return its size n, such that the + next n * 2 unpack() calls will iterate over its key-value pairs. + + Raises `OutOfData` when there are no more bytes to unpack. + """ + return self._unpack(read_map_header) + + def tell(self): + """Returns the current position of the Unpacker in bytes, i.e., the + number of bytes that were read from the input, also the starting + position of the next object. + """ + return self.stream_offset + + def __iter__(self): + return self + + def __next__(self): + return self._unpack(unpack_construct, 1) + + # for debug. + #def _buf(self): + # return PyString_FromStringAndSize(self.buf, self.buf_tail) + + #def _off(self): + # return self.buf_head diff --git a/ccxt/static_dependencies/msgpack/buff_converter.h b/ccxt/static_dependencies/msgpack/buff_converter.h new file mode 100644 index 0000000..86b4196 --- /dev/null +++ b/ccxt/static_dependencies/msgpack/buff_converter.h @@ -0,0 +1,8 @@ +#include "Python.h" + +/* cython does not support this preprocessor check => write it in raw C */ +static PyObject * +buff_to_buff(char *buff, Py_ssize_t size) +{ + return PyMemoryView_FromMemory(buff, size, PyBUF_READ); +} diff --git a/ccxt/static_dependencies/msgpack/exceptions.py b/ccxt/static_dependencies/msgpack/exceptions.py new file mode 100644 index 0000000..d6d2615 --- /dev/null +++ b/ccxt/static_dependencies/msgpack/exceptions.py @@ -0,0 +1,48 @@ +class UnpackException(Exception): + """Base class for some exceptions raised while unpacking. + + NOTE: unpack may raise exception other than subclass of + UnpackException. If you want to catch all error, catch + Exception instead. + """ + + +class BufferFull(UnpackException): + pass + + +class OutOfData(UnpackException): + pass + + +class FormatError(ValueError, UnpackException): + """Invalid msgpack format""" + + +class StackError(ValueError, UnpackException): + """Too nested""" + + +# Deprecated. Use ValueError instead +UnpackValueError = ValueError + + +class ExtraData(UnpackValueError): + """ExtraData is raised when there is trailing data. + + This exception is raised while only one-shot (not streaming) + unpack. + """ + + def __init__(self, unpacked, extra): + self.unpacked = unpacked + self.extra = extra + + def __str__(self): + return "unpack(b) received extra data." + + +# Deprecated. Use Exception instead to catch all exception during packing. +PackException = Exception +PackValueError = ValueError +PackOverflowError = OverflowError diff --git a/ccxt/static_dependencies/msgpack/ext.py b/ccxt/static_dependencies/msgpack/ext.py new file mode 100644 index 0000000..02c2c43 --- /dev/null +++ b/ccxt/static_dependencies/msgpack/ext.py @@ -0,0 +1,168 @@ +from collections import namedtuple +import datetime +import struct + + +class ExtType(namedtuple("ExtType", "code data")): + """ExtType represents ext type in msgpack.""" + + def __new__(cls, code, data): + if not isinstance(code, int): + raise TypeError("code must be int") + if not isinstance(data, bytes): + raise TypeError("data must be bytes") + if not 0 <= code <= 127: + raise ValueError("code must be 0~127") + return super().__new__(cls, code, data) + + +class Timestamp: + """Timestamp represents the Timestamp extension type in msgpack. + + When built with Cython, msgpack uses C methods to pack and unpack `Timestamp`. + When using pure-Python msgpack, :func:`to_bytes` and :func:`from_bytes` are used to pack and + unpack `Timestamp`. + + This class is immutable: Do not override seconds and nanoseconds. + """ + + __slots__ = ["seconds", "nanoseconds"] + + def __init__(self, seconds, nanoseconds=0): + """Initialize a Timestamp object. + + :param int seconds: + Number of seconds since the UNIX epoch (00:00:00 UTC Jan 1 1970, minus leap seconds). + May be negative. + + :param int nanoseconds: + Number of nanoseconds to add to `seconds` to get fractional time. + Maximum is 999_999_999. Default is 0. + + Note: Negative times (before the UNIX epoch) are represented as neg. seconds + pos. ns. + """ + if not isinstance(seconds, int): + raise TypeError("seconds must be an integer") + if not isinstance(nanoseconds, int): + raise TypeError("nanoseconds must be an integer") + if not (0 <= nanoseconds < 10**9): + raise ValueError("nanoseconds must be a non-negative integer less than 999999999.") + self.seconds = seconds + self.nanoseconds = nanoseconds + + def __repr__(self): + """String representation of Timestamp.""" + return f"Timestamp(seconds={self.seconds}, nanoseconds={self.nanoseconds})" + + def __eq__(self, other): + """Check for equality with another Timestamp object""" + if type(other) is self.__class__: + return self.seconds == other.seconds and self.nanoseconds == other.nanoseconds + return False + + def __ne__(self, other): + """not-equals method (see :func:`__eq__()`)""" + return not self.__eq__(other) + + def __hash__(self): + return hash((self.seconds, self.nanoseconds)) + + @staticmethod + def from_bytes(b): + """Unpack bytes into a `Timestamp` object. + + Used for pure-Python msgpack unpacking. + + :param b: Payload from msgpack ext message with code -1 + :type b: bytes + + :returns: Timestamp object unpacked from msgpack ext payload + :rtype: Timestamp + """ + if len(b) == 4: + seconds = struct.unpack("!L", b)[0] + nanoseconds = 0 + elif len(b) == 8: + data64 = struct.unpack("!Q", b)[0] + seconds = data64 & 0x00000003FFFFFFFF + nanoseconds = data64 >> 34 + elif len(b) == 12: + nanoseconds, seconds = struct.unpack("!Iq", b) + else: + raise ValueError( + "Timestamp type can only be created from 32, 64, or 96-bit byte objects" + ) + return Timestamp(seconds, nanoseconds) + + def to_bytes(self): + """Pack this Timestamp object into bytes. + + Used for pure-Python msgpack packing. + + :returns data: Payload for EXT message with code -1 (timestamp type) + :rtype: bytes + """ + if (self.seconds >> 34) == 0: # seconds is non-negative and fits in 34 bits + data64 = self.nanoseconds << 34 | self.seconds + if data64 & 0xFFFFFFFF00000000 == 0: + # nanoseconds is zero and seconds < 2**32, so timestamp 32 + data = struct.pack("!L", data64) + else: + # timestamp 64 + data = struct.pack("!Q", data64) + else: + # timestamp 96 + data = struct.pack("!Iq", self.nanoseconds, self.seconds) + return data + + @staticmethod + def from_unix(unix_sec): + """Create a Timestamp from posix timestamp in seconds. + + :param unix_float: Posix timestamp in seconds. + :type unix_float: int or float + """ + seconds = int(unix_sec // 1) + nanoseconds = int((unix_sec % 1) * 10**9) + return Timestamp(seconds, nanoseconds) + + def to_unix(self): + """Get the timestamp as a floating-point value. + + :returns: posix timestamp + :rtype: float + """ + return self.seconds + self.nanoseconds / 1e9 + + @staticmethod + def from_unix_nano(unix_ns): + """Create a Timestamp from posix timestamp in nanoseconds. + + :param int unix_ns: Posix timestamp in nanoseconds. + :rtype: Timestamp + """ + return Timestamp(*divmod(unix_ns, 10**9)) + + def to_unix_nano(self): + """Get the timestamp as a unixtime in nanoseconds. + + :returns: posix timestamp in nanoseconds + :rtype: int + """ + return self.seconds * 10**9 + self.nanoseconds + + def to_datetime(self): + """Get the timestamp as a UTC datetime. + + :rtype: `datetime.datetime` + """ + utc = datetime.timezone.utc + return datetime.datetime.fromtimestamp(0, utc) + datetime.timedelta(seconds=self.to_unix()) + + @staticmethod + def from_datetime(dt): + """Create a Timestamp from datetime with tzinfo. + + :rtype: Timestamp + """ + return Timestamp.from_unix(dt.timestamp()) diff --git a/ccxt/static_dependencies/msgpack/fallback.py b/ccxt/static_dependencies/msgpack/fallback.py new file mode 100644 index 0000000..a174162 --- /dev/null +++ b/ccxt/static_dependencies/msgpack/fallback.py @@ -0,0 +1,951 @@ +"""Fallback pure Python implementation of msgpack""" +from datetime import datetime as _DateTime +import sys +import struct + + +if hasattr(sys, "pypy_version_info"): + # StringIO is slow on PyPy, StringIO is faster. However: PyPy's own + # StringBuilder is fastest. + from __pypy__ import newlist_hint + + try: + from __pypy__.builders import BytesBuilder as StringBuilder + except ImportError: + from __pypy__.builders import StringBuilder + USING_STRINGBUILDER = True + + class StringIO: + def __init__(self, s=b""): + if s: + self.builder = StringBuilder(len(s)) + self.builder.append(s) + else: + self.builder = StringBuilder() + + def write(self, s): + if isinstance(s, memoryview): + s = s.tobytes() + elif isinstance(s, bytearray): + s = bytes(s) + self.builder.append(s) + + def getvalue(self): + return self.builder.build() + +else: + USING_STRINGBUILDER = False + from io import BytesIO as StringIO + + newlist_hint = lambda size: [] + + +from .exceptions import BufferFull, OutOfData, ExtraData, FormatError, StackError + +from .ext import ExtType, Timestamp + + +EX_SKIP = 0 +EX_CONSTRUCT = 1 +EX_READ_ARRAY_HEADER = 2 +EX_READ_MAP_HEADER = 3 + +TYPE_IMMEDIATE = 0 +TYPE_ARRAY = 1 +TYPE_MAP = 2 +TYPE_RAW = 3 +TYPE_BIN = 4 +TYPE_EXT = 5 + +DEFAULT_RECURSE_LIMIT = 511 + + +def _check_type_strict(obj, t, type=type, tuple=tuple): + if type(t) is tuple: + return type(obj) in t + else: + return type(obj) is t + + +def _get_data_from_buffer(obj): + view = memoryview(obj) + if view.itemsize != 1: + raise ValueError("cannot unpack from multi-byte object") + return view + + +def unpackb(packed, **kwargs): + """ + Unpack an object from `packed`. + + Raises ``ExtraData`` when *packed* contains extra bytes. + Raises ``ValueError`` when *packed* is incomplete. + Raises ``FormatError`` when *packed* is not valid msgpack. + Raises ``StackError`` when *packed* contains too nested. + Other exceptions can be raised during unpacking. + + See :class:`Unpacker` for options. + """ + unpacker = Unpacker(None, max_buffer_size=len(packed), **kwargs) + unpacker.feed(packed) + try: + ret = unpacker._unpack() + except OutOfData: + raise ValueError("Unpack failed: incomplete input") + except RecursionError: + raise StackError + if unpacker._got_extradata(): + raise ExtraData(ret, unpacker._get_extradata()) + return ret + + +_NO_FORMAT_USED = "" +_MSGPACK_HEADERS = { + 0xC4: (1, _NO_FORMAT_USED, TYPE_BIN), + 0xC5: (2, ">H", TYPE_BIN), + 0xC6: (4, ">I", TYPE_BIN), + 0xC7: (2, "Bb", TYPE_EXT), + 0xC8: (3, ">Hb", TYPE_EXT), + 0xC9: (5, ">Ib", TYPE_EXT), + 0xCA: (4, ">f"), + 0xCB: (8, ">d"), + 0xCC: (1, _NO_FORMAT_USED), + 0xCD: (2, ">H"), + 0xCE: (4, ">I"), + 0xCF: (8, ">Q"), + 0xD0: (1, "b"), + 0xD1: (2, ">h"), + 0xD2: (4, ">i"), + 0xD3: (8, ">q"), + 0xD4: (1, "b1s", TYPE_EXT), + 0xD5: (2, "b2s", TYPE_EXT), + 0xD6: (4, "b4s", TYPE_EXT), + 0xD7: (8, "b8s", TYPE_EXT), + 0xD8: (16, "b16s", TYPE_EXT), + 0xD9: (1, _NO_FORMAT_USED, TYPE_RAW), + 0xDA: (2, ">H", TYPE_RAW), + 0xDB: (4, ">I", TYPE_RAW), + 0xDC: (2, ">H", TYPE_ARRAY), + 0xDD: (4, ">I", TYPE_ARRAY), + 0xDE: (2, ">H", TYPE_MAP), + 0xDF: (4, ">I", TYPE_MAP), +} + + +class Unpacker: + """Streaming unpacker. + + Arguments: + + :param file_like: + File-like object having `.read(n)` method. + If specified, unpacker reads serialized data from it and `.feed()` is not usable. + + :param int read_size: + Used as `file_like.read(read_size)`. (default: `min(16*1024, max_buffer_size)`) + + :param bool use_list: + If true, unpack msgpack array to Python list. + Otherwise, unpack to Python tuple. (default: True) + + :param bool raw: + If true, unpack msgpack raw to Python bytes. + Otherwise, unpack to Python str by decoding with UTF-8 encoding (default). + + :param int timestamp: + Control how timestamp type is unpacked: + + 0 - Timestamp + 1 - float (Seconds from the EPOCH) + 2 - int (Nanoseconds from the EPOCH) + 3 - datetime.datetime (UTC). + + :param bool strict_map_key: + If true (default), only str or bytes are accepted for map (dict) keys. + + :param object_hook: + When specified, it should be callable. + Unpacker calls it with a dict argument after unpacking msgpack map. + (See also simplejson) + + :param object_pairs_hook: + When specified, it should be callable. + Unpacker calls it with a list of key-value pairs after unpacking msgpack map. + (See also simplejson) + + :param str unicode_errors: + The error handler for decoding unicode. (default: 'strict') + This option should be used only when you have msgpack data which + contains invalid UTF-8 string. + + :param int max_buffer_size: + Limits size of data waiting unpacked. 0 means 2**32-1. + The default value is 100*1024*1024 (100MiB). + Raises `BufferFull` exception when it is insufficient. + You should set this parameter when unpacking data from untrusted source. + + :param int max_str_len: + Deprecated, use *max_buffer_size* instead. + Limits max length of str. (default: max_buffer_size) + + :param int max_bin_len: + Deprecated, use *max_buffer_size* instead. + Limits max length of bin. (default: max_buffer_size) + + :param int max_array_len: + Limits max length of array. + (default: max_buffer_size) + + :param int max_map_len: + Limits max length of map. + (default: max_buffer_size//2) + + :param int max_ext_len: + Deprecated, use *max_buffer_size* instead. + Limits max size of ext type. (default: max_buffer_size) + + Example of streaming deserialize from file-like object:: + + unpacker = Unpacker(file_like) + for o in unpacker: + process(o) + + Example of streaming deserialize from socket:: + + unpacker = Unpacker() + while True: + buf = sock.recv(1024**2) + if not buf: + break + unpacker.feed(buf) + for o in unpacker: + process(o) + + Raises ``ExtraData`` when *packed* contains extra bytes. + Raises ``OutOfData`` when *packed* is incomplete. + Raises ``FormatError`` when *packed* is not valid msgpack. + Raises ``StackError`` when *packed* contains too nested. + Other exceptions can be raised during unpacking. + """ + + def __init__( + self, + file_like=None, + read_size=0, + use_list=True, + raw=False, + timestamp=0, + strict_map_key=True, + object_hook=None, + object_pairs_hook=None, + list_hook=None, + unicode_errors=None, + max_buffer_size=100 * 1024 * 1024, + ext_hook=ExtType, + max_str_len=-1, + max_bin_len=-1, + max_array_len=-1, + max_map_len=-1, + max_ext_len=-1, + ): + if unicode_errors is None: + unicode_errors = "strict" + + if file_like is None: + self._feeding = True + else: + if not callable(file_like.read): + raise TypeError("`file_like.read` must be callable") + self.file_like = file_like + self._feeding = False + + #: array of bytes fed. + self._buffer = bytearray() + #: Which position we currently reads + self._buff_i = 0 + + # When Unpacker is used as an iterable, between the calls to next(), + # the buffer is not "consumed" completely, for efficiency sake. + # Instead, it is done sloppily. To make sure we raise BufferFull at + # the correct moments, we have to keep track of how sloppy we were. + # Furthermore, when the buffer is incomplete (that is: in the case + # we raise an OutOfData) we need to rollback the buffer to the correct + # state, which _buf_checkpoint records. + self._buf_checkpoint = 0 + + if not max_buffer_size: + max_buffer_size = 2**31 - 1 + if max_str_len == -1: + max_str_len = max_buffer_size + if max_bin_len == -1: + max_bin_len = max_buffer_size + if max_array_len == -1: + max_array_len = max_buffer_size + if max_map_len == -1: + max_map_len = max_buffer_size // 2 + if max_ext_len == -1: + max_ext_len = max_buffer_size + + self._max_buffer_size = max_buffer_size + if read_size > self._max_buffer_size: + raise ValueError("read_size must be smaller than max_buffer_size") + self._read_size = read_size or min(self._max_buffer_size, 16 * 1024) + self._raw = bool(raw) + self._strict_map_key = bool(strict_map_key) + self._unicode_errors = unicode_errors + self._use_list = use_list + if not (0 <= timestamp <= 3): + raise ValueError("timestamp must be 0..3") + self._timestamp = timestamp + self._list_hook = list_hook + self._object_hook = object_hook + self._object_pairs_hook = object_pairs_hook + self._ext_hook = ext_hook + self._max_str_len = max_str_len + self._max_bin_len = max_bin_len + self._max_array_len = max_array_len + self._max_map_len = max_map_len + self._max_ext_len = max_ext_len + self._stream_offset = 0 + + if list_hook is not None and not callable(list_hook): + raise TypeError("`list_hook` is not callable") + if object_hook is not None and not callable(object_hook): + raise TypeError("`object_hook` is not callable") + if object_pairs_hook is not None and not callable(object_pairs_hook): + raise TypeError("`object_pairs_hook` is not callable") + if object_hook is not None and object_pairs_hook is not None: + raise TypeError("object_pairs_hook and object_hook are mutually exclusive") + if not callable(ext_hook): + raise TypeError("`ext_hook` is not callable") + + def feed(self, next_bytes): + assert self._feeding + view = _get_data_from_buffer(next_bytes) + if len(self._buffer) - self._buff_i + len(view) > self._max_buffer_size: + raise BufferFull + + # Strip buffer before checkpoint before reading file. + if self._buf_checkpoint > 0: + del self._buffer[: self._buf_checkpoint] + self._buff_i -= self._buf_checkpoint + self._buf_checkpoint = 0 + + # Use extend here: INPLACE_ADD += doesn't reliably typecast memoryview in jython + self._buffer.extend(view) + + def _consume(self): + """Gets rid of the used parts of the buffer.""" + self._stream_offset += self._buff_i - self._buf_checkpoint + self._buf_checkpoint = self._buff_i + + def _got_extradata(self): + return self._buff_i < len(self._buffer) + + def _get_extradata(self): + return self._buffer[self._buff_i :] + + def read_bytes(self, n): + ret = self._read(n, raise_outofdata=False) + self._consume() + return ret + + def _read(self, n, raise_outofdata=True): + # (int) -> bytearray + self._reserve(n, raise_outofdata=raise_outofdata) + i = self._buff_i + ret = self._buffer[i : i + n] + self._buff_i = i + len(ret) + return ret + + def _reserve(self, n, raise_outofdata=True): + remain_bytes = len(self._buffer) - self._buff_i - n + + # Fast path: buffer has n bytes already + if remain_bytes >= 0: + return + + if self._feeding: + self._buff_i = self._buf_checkpoint + raise OutOfData + + # Strip buffer before checkpoint before reading file. + if self._buf_checkpoint > 0: + del self._buffer[: self._buf_checkpoint] + self._buff_i -= self._buf_checkpoint + self._buf_checkpoint = 0 + + # Read from file + remain_bytes = -remain_bytes + if remain_bytes + len(self._buffer) > self._max_buffer_size: + raise BufferFull + while remain_bytes > 0: + to_read_bytes = max(self._read_size, remain_bytes) + read_data = self.file_like.read(to_read_bytes) + if not read_data: + break + assert isinstance(read_data, bytes) + self._buffer += read_data + remain_bytes -= len(read_data) + + if len(self._buffer) < n + self._buff_i and raise_outofdata: + self._buff_i = 0 # rollback + raise OutOfData + + def _read_header(self): + typ = TYPE_IMMEDIATE + n = 0 + obj = None + self._reserve(1) + b = self._buffer[self._buff_i] + self._buff_i += 1 + if b & 0b10000000 == 0: + obj = b + elif b & 0b11100000 == 0b11100000: + obj = -1 - (b ^ 0xFF) + elif b & 0b11100000 == 0b10100000: + n = b & 0b00011111 + typ = TYPE_RAW + if n > self._max_str_len: + raise ValueError(f"{n} exceeds max_str_len({self._max_str_len})") + obj = self._read(n) + elif b & 0b11110000 == 0b10010000: + n = b & 0b00001111 + typ = TYPE_ARRAY + if n > self._max_array_len: + raise ValueError(f"{n} exceeds max_array_len({self._max_array_len})") + elif b & 0b11110000 == 0b10000000: + n = b & 0b00001111 + typ = TYPE_MAP + if n > self._max_map_len: + raise ValueError(f"{n} exceeds max_map_len({self._max_map_len})") + elif b == 0xC0: + obj = None + elif b == 0xC2: + obj = False + elif b == 0xC3: + obj = True + elif 0xC4 <= b <= 0xC6: + size, fmt, typ = _MSGPACK_HEADERS[b] + self._reserve(size) + if len(fmt) > 0: + n = struct.unpack_from(fmt, self._buffer, self._buff_i)[0] + else: + n = self._buffer[self._buff_i] + self._buff_i += size + if n > self._max_bin_len: + raise ValueError(f"{n} exceeds max_bin_len({self._max_bin_len})") + obj = self._read(n) + elif 0xC7 <= b <= 0xC9: + size, fmt, typ = _MSGPACK_HEADERS[b] + self._reserve(size) + L, n = struct.unpack_from(fmt, self._buffer, self._buff_i) + self._buff_i += size + if L > self._max_ext_len: + raise ValueError(f"{L} exceeds max_ext_len({self._max_ext_len})") + obj = self._read(L) + elif 0xCA <= b <= 0xD3: + size, fmt = _MSGPACK_HEADERS[b] + self._reserve(size) + if len(fmt) > 0: + obj = struct.unpack_from(fmt, self._buffer, self._buff_i)[0] + else: + obj = self._buffer[self._buff_i] + self._buff_i += size + elif 0xD4 <= b <= 0xD8: + size, fmt, typ = _MSGPACK_HEADERS[b] + if self._max_ext_len < size: + raise ValueError(f"{size} exceeds max_ext_len({self._max_ext_len})") + self._reserve(size + 1) + n, obj = struct.unpack_from(fmt, self._buffer, self._buff_i) + self._buff_i += size + 1 + elif 0xD9 <= b <= 0xDB: + size, fmt, typ = _MSGPACK_HEADERS[b] + self._reserve(size) + if len(fmt) > 0: + (n,) = struct.unpack_from(fmt, self._buffer, self._buff_i) + else: + n = self._buffer[self._buff_i] + self._buff_i += size + if n > self._max_str_len: + raise ValueError(f"{n} exceeds max_str_len({self._max_str_len})") + obj = self._read(n) + elif 0xDC <= b <= 0xDD: + size, fmt, typ = _MSGPACK_HEADERS[b] + self._reserve(size) + (n,) = struct.unpack_from(fmt, self._buffer, self._buff_i) + self._buff_i += size + if n > self._max_array_len: + raise ValueError(f"{n} exceeds max_array_len({self._max_array_len})") + elif 0xDE <= b <= 0xDF: + size, fmt, typ = _MSGPACK_HEADERS[b] + self._reserve(size) + (n,) = struct.unpack_from(fmt, self._buffer, self._buff_i) + self._buff_i += size + if n > self._max_map_len: + raise ValueError(f"{n} exceeds max_map_len({self._max_map_len})") + else: + raise FormatError("Unknown header: 0x%x" % b) + return typ, n, obj + + def _unpack(self, execute=EX_CONSTRUCT): + typ, n, obj = self._read_header() + + if execute == EX_READ_ARRAY_HEADER: + if typ != TYPE_ARRAY: + raise ValueError("Expected array") + return n + if execute == EX_READ_MAP_HEADER: + if typ != TYPE_MAP: + raise ValueError("Expected map") + return n + # TODO should we eliminate the recursion? + if typ == TYPE_ARRAY: + if execute == EX_SKIP: + for i in range(n): + # TODO check whether we need to call `list_hook` + self._unpack(EX_SKIP) + return + ret = newlist_hint(n) + for i in range(n): + ret.append(self._unpack(EX_CONSTRUCT)) + if self._list_hook is not None: + ret = self._list_hook(ret) + # TODO is the interaction between `list_hook` and `use_list` ok? + return ret if self._use_list else tuple(ret) + if typ == TYPE_MAP: + if execute == EX_SKIP: + for i in range(n): + # TODO check whether we need to call hooks + self._unpack(EX_SKIP) + self._unpack(EX_SKIP) + return + if self._object_pairs_hook is not None: + ret = self._object_pairs_hook( + (self._unpack(EX_CONSTRUCT), self._unpack(EX_CONSTRUCT)) for _ in range(n) + ) + else: + ret = {} + for _ in range(n): + key = self._unpack(EX_CONSTRUCT) + if self._strict_map_key and type(key) not in (str, bytes): + raise ValueError("%s is not allowed for map key" % str(type(key))) + if isinstance(key, str): + key = sys.intern(key) + ret[key] = self._unpack(EX_CONSTRUCT) + if self._object_hook is not None: + ret = self._object_hook(ret) + return ret + if execute == EX_SKIP: + return + if typ == TYPE_RAW: + if self._raw: + obj = bytes(obj) + else: + obj = obj.decode("utf_8", self._unicode_errors) + return obj + if typ == TYPE_BIN: + return bytes(obj) + if typ == TYPE_EXT: + if n == -1: # timestamp + ts = Timestamp.from_bytes(bytes(obj)) + if self._timestamp == 1: + return ts.to_unix() + elif self._timestamp == 2: + return ts.to_unix_nano() + elif self._timestamp == 3: + return ts.to_datetime() + else: + return ts + else: + return self._ext_hook(n, bytes(obj)) + assert typ == TYPE_IMMEDIATE + return obj + + def __iter__(self): + return self + + def __next__(self): + try: + ret = self._unpack(EX_CONSTRUCT) + self._consume() + return ret + except OutOfData: + self._consume() + raise StopIteration + except RecursionError: + raise StackError + + next = __next__ + + def skip(self): + self._unpack(EX_SKIP) + self._consume() + + def unpack(self): + try: + ret = self._unpack(EX_CONSTRUCT) + except RecursionError: + raise StackError + self._consume() + return ret + + def read_array_header(self): + ret = self._unpack(EX_READ_ARRAY_HEADER) + self._consume() + return ret + + def read_map_header(self): + ret = self._unpack(EX_READ_MAP_HEADER) + self._consume() + return ret + + def tell(self): + return self._stream_offset + + +class Packer: + """ + MessagePack Packer + + Usage:: + + packer = Packer() + astream.write(packer.pack(a)) + astream.write(packer.pack(b)) + + Packer's constructor has some keyword arguments: + + :param default: + When specified, it should be callable. + Convert user type to builtin type that Packer supports. + See also simplejson's document. + + :param bool use_single_float: + Use single precision float type for float. (default: False) + + :param bool autoreset: + Reset buffer after each pack and return its content as `bytes`. (default: True). + If set this to false, use `bytes()` to get content and `.reset()` to clear buffer. + + :param bool use_bin_type: + Use bin type introduced in msgpack spec 2.0 for bytes. + It also enables str8 type for unicode. (default: True) + + :param bool strict_types: + If set to true, types will be checked to be exact. Derived classes + from serializable types will not be serialized and will be + treated as unsupported type and forwarded to default. + Additionally tuples will not be serialized as lists. + This is useful when trying to implement accurate serialization + for python types. + + :param bool datetime: + If set to true, datetime with tzinfo is packed into Timestamp type. + Note that the tzinfo is stripped in the timestamp. + You can get UTC datetime with `timestamp=3` option of the Unpacker. + + :param str unicode_errors: + The error handler for encoding unicode. (default: 'strict') + DO NOT USE THIS!! This option is kept for very specific usage. + + Example of streaming deserialize from file-like object:: + + unpacker = Unpacker(file_like) + for o in unpacker: + process(o) + + Example of streaming deserialize from socket:: + + unpacker = Unpacker() + while True: + buf = sock.recv(1024**2) + if not buf: + break + unpacker.feed(buf) + for o in unpacker: + process(o) + + Raises ``ExtraData`` when *packed* contains extra bytes. + Raises ``OutOfData`` when *packed* is incomplete. + Raises ``FormatError`` when *packed* is not valid msgpack. + Raises ``StackError`` when *packed* contains too nested. + Other exceptions can be raised during unpacking. + """ + + def __init__( + self, + default=None, + use_single_float=False, + autoreset=True, + use_bin_type=True, + strict_types=False, + datetime=False, + unicode_errors=None, + ): + self._strict_types = strict_types + self._use_float = use_single_float + self._autoreset = autoreset + self._use_bin_type = use_bin_type + self._buffer = StringIO() + self._datetime = bool(datetime) + self._unicode_errors = unicode_errors or "strict" + if default is not None: + if not callable(default): + raise TypeError("default must be callable") + self._default = default + + def _pack( + self, + obj, + nest_limit=DEFAULT_RECURSE_LIMIT, + check=isinstance, + check_type_strict=_check_type_strict, + ): + default_used = False + if self._strict_types: + check = check_type_strict + list_types = list + else: + list_types = (list, tuple) + while True: + if nest_limit < 0: + raise ValueError("recursion limit exceeded") + if obj is None: + return self._buffer.write(b"\xc0") + if check(obj, bool): + if obj: + return self._buffer.write(b"\xc3") + return self._buffer.write(b"\xc2") + if check(obj, int): + if 0 <= obj < 0x80: + return self._buffer.write(struct.pack("B", obj)) + if -0x20 <= obj < 0: + return self._buffer.write(struct.pack("b", obj)) + if 0x80 <= obj <= 0xFF: + return self._buffer.write(struct.pack("BB", 0xCC, obj)) + if -0x80 <= obj < 0: + return self._buffer.write(struct.pack(">Bb", 0xD0, obj)) + if 0xFF < obj <= 0xFFFF: + return self._buffer.write(struct.pack(">BH", 0xCD, obj)) + if -0x8000 <= obj < -0x80: + return self._buffer.write(struct.pack(">Bh", 0xD1, obj)) + if 0xFFFF < obj <= 0xFFFFFFFF: + return self._buffer.write(struct.pack(">BI", 0xCE, obj)) + if -0x80000000 <= obj < -0x8000: + return self._buffer.write(struct.pack(">Bi", 0xD2, obj)) + if 0xFFFFFFFF < obj <= 0xFFFFFFFFFFFFFFFF: + return self._buffer.write(struct.pack(">BQ", 0xCF, obj)) + if -0x8000000000000000 <= obj < -0x80000000: + return self._buffer.write(struct.pack(">Bq", 0xD3, obj)) + if not default_used and self._default is not None: + obj = self._default(obj) + default_used = True + continue + raise OverflowError("Integer value out of range") + if check(obj, (bytes, bytearray)): + n = len(obj) + if n >= 2**32: + raise ValueError("%s is too large" % type(obj).__name__) + self._pack_bin_header(n) + return self._buffer.write(obj) + if check(obj, str): + obj = obj.encode("utf-8", self._unicode_errors) + n = len(obj) + if n >= 2**32: + raise ValueError("String is too large") + self._pack_raw_header(n) + return self._buffer.write(obj) + if check(obj, memoryview): + n = obj.nbytes + if n >= 2**32: + raise ValueError("Memoryview is too large") + self._pack_bin_header(n) + return self._buffer.write(obj) + if check(obj, float): + if self._use_float: + return self._buffer.write(struct.pack(">Bf", 0xCA, obj)) + return self._buffer.write(struct.pack(">Bd", 0xCB, obj)) + if check(obj, (ExtType, Timestamp)): + if check(obj, Timestamp): + code = -1 + data = obj.to_bytes() + else: + code = obj.code + data = obj.data + assert isinstance(code, int) + assert isinstance(data, bytes) + L = len(data) + if L == 1: + self._buffer.write(b"\xd4") + elif L == 2: + self._buffer.write(b"\xd5") + elif L == 4: + self._buffer.write(b"\xd6") + elif L == 8: + self._buffer.write(b"\xd7") + elif L == 16: + self._buffer.write(b"\xd8") + elif L <= 0xFF: + self._buffer.write(struct.pack(">BB", 0xC7, L)) + elif L <= 0xFFFF: + self._buffer.write(struct.pack(">BH", 0xC8, L)) + else: + self._buffer.write(struct.pack(">BI", 0xC9, L)) + self._buffer.write(struct.pack("b", code)) + self._buffer.write(data) + return + if check(obj, list_types): + n = len(obj) + self._pack_array_header(n) + for i in range(n): + self._pack(obj[i], nest_limit - 1) + return + if check(obj, dict): + return self._pack_map_pairs(len(obj), obj.items(), nest_limit - 1) + + if self._datetime and check(obj, _DateTime) and obj.tzinfo is not None: + obj = Timestamp.from_datetime(obj) + default_used = 1 + continue + + if not default_used and self._default is not None: + obj = self._default(obj) + default_used = 1 + continue + + if self._datetime and check(obj, _DateTime): + raise ValueError(f"Cannot serialize {obj!r} where tzinfo=None") + + raise TypeError(f"Cannot serialize {obj!r}") + + def pack(self, obj): + try: + self._pack(obj) + except: + self._buffer = StringIO() # force reset + raise + if self._autoreset: + ret = self._buffer.getvalue() + self._buffer = StringIO() + return ret + + def pack_map_pairs(self, pairs): + self._pack_map_pairs(len(pairs), pairs) + if self._autoreset: + ret = self._buffer.getvalue() + self._buffer = StringIO() + return ret + + def pack_array_header(self, n): + if n >= 2**32: + raise ValueError + self._pack_array_header(n) + if self._autoreset: + ret = self._buffer.getvalue() + self._buffer = StringIO() + return ret + + def pack_map_header(self, n): + if n >= 2**32: + raise ValueError + self._pack_map_header(n) + if self._autoreset: + ret = self._buffer.getvalue() + self._buffer = StringIO() + return ret + + def pack_ext_type(self, typecode, data): + if not isinstance(typecode, int): + raise TypeError("typecode must have int type.") + if not 0 <= typecode <= 127: + raise ValueError("typecode should be 0-127") + if not isinstance(data, bytes): + raise TypeError("data must have bytes type") + L = len(data) + if L > 0xFFFFFFFF: + raise ValueError("Too large data") + if L == 1: + self._buffer.write(b"\xd4") + elif L == 2: + self._buffer.write(b"\xd5") + elif L == 4: + self._buffer.write(b"\xd6") + elif L == 8: + self._buffer.write(b"\xd7") + elif L == 16: + self._buffer.write(b"\xd8") + elif L <= 0xFF: + self._buffer.write(b"\xc7" + struct.pack("B", L)) + elif L <= 0xFFFF: + self._buffer.write(b"\xc8" + struct.pack(">H", L)) + else: + self._buffer.write(b"\xc9" + struct.pack(">I", L)) + self._buffer.write(struct.pack("B", typecode)) + self._buffer.write(data) + + def _pack_array_header(self, n): + if n <= 0x0F: + return self._buffer.write(struct.pack("B", 0x90 + n)) + if n <= 0xFFFF: + return self._buffer.write(struct.pack(">BH", 0xDC, n)) + if n <= 0xFFFFFFFF: + return self._buffer.write(struct.pack(">BI", 0xDD, n)) + raise ValueError("Array is too large") + + def _pack_map_header(self, n): + if n <= 0x0F: + return self._buffer.write(struct.pack("B", 0x80 + n)) + if n <= 0xFFFF: + return self._buffer.write(struct.pack(">BH", 0xDE, n)) + if n <= 0xFFFFFFFF: + return self._buffer.write(struct.pack(">BI", 0xDF, n)) + raise ValueError("Dict is too large") + + def _pack_map_pairs(self, n, pairs, nest_limit=DEFAULT_RECURSE_LIMIT): + self._pack_map_header(n) + for k, v in pairs: + self._pack(k, nest_limit - 1) + self._pack(v, nest_limit - 1) + + def _pack_raw_header(self, n): + if n <= 0x1F: + self._buffer.write(struct.pack("B", 0xA0 + n)) + elif self._use_bin_type and n <= 0xFF: + self._buffer.write(struct.pack(">BB", 0xD9, n)) + elif n <= 0xFFFF: + self._buffer.write(struct.pack(">BH", 0xDA, n)) + elif n <= 0xFFFFFFFF: + self._buffer.write(struct.pack(">BI", 0xDB, n)) + else: + raise ValueError("Raw is too large") + + def _pack_bin_header(self, n): + if not self._use_bin_type: + return self._pack_raw_header(n) + elif n <= 0xFF: + return self._buffer.write(struct.pack(">BB", 0xC4, n)) + elif n <= 0xFFFF: + return self._buffer.write(struct.pack(">BH", 0xC5, n)) + elif n <= 0xFFFFFFFF: + return self._buffer.write(struct.pack(">BI", 0xC6, n)) + else: + raise ValueError("Bin is too large") + + def bytes(self): + """Return internal buffer contents as bytes object""" + return self._buffer.getvalue() + + def reset(self): + """Reset internal buffer. + + This method is useful only when autoreset=False. + """ + self._buffer = StringIO() + + def getbuffer(self): + """Return view of internal buffer.""" + if USING_STRINGBUILDER: + return memoryview(self.bytes()) + else: + return self._buffer.getbuffer() diff --git a/ccxt/static_dependencies/msgpack/pack.h b/ccxt/static_dependencies/msgpack/pack.h new file mode 100644 index 0000000..2453428 --- /dev/null +++ b/ccxt/static_dependencies/msgpack/pack.h @@ -0,0 +1,89 @@ +/* + * MessagePack for Python packing routine + * + * Copyright (C) 2009 Naoki INADA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include "sysdep.h" +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct msgpack_packer { + char *buf; + size_t length; + size_t buf_size; + bool use_bin_type; +} msgpack_packer; + +typedef struct Packer Packer; + +static inline int msgpack_pack_write(msgpack_packer* pk, const char *data, size_t l) +{ + char* buf = pk->buf; + size_t bs = pk->buf_size; + size_t len = pk->length; + + if (len + l > bs) { + bs = (len + l) * 2; + buf = (char*)PyMem_Realloc(buf, bs); + if (!buf) { + PyErr_NoMemory(); + return -1; + } + } + memcpy(buf + len, data, l); + len += l; + + pk->buf = buf; + pk->buf_size = bs; + pk->length = len; + return 0; +} + +#define msgpack_pack_append_buffer(user, buf, len) \ + return msgpack_pack_write(user, (const char*)buf, len) + +#include "pack_template.h" + +// return -2 when o is too long +static inline int +msgpack_pack_unicode(msgpack_packer *pk, PyObject *o, long long limit) +{ + assert(PyUnicode_Check(o)); + + Py_ssize_t len; + const char* buf = PyUnicode_AsUTF8AndSize(o, &len); + if (buf == NULL) + return -1; + + if (len > limit) { + return -2; + } + + int ret = msgpack_pack_raw(pk, len); + if (ret) return ret; + + return msgpack_pack_raw_body(pk, buf, len); +} + +#ifdef __cplusplus +} +#endif diff --git a/ccxt/static_dependencies/msgpack/pack_template.h b/ccxt/static_dependencies/msgpack/pack_template.h new file mode 100644 index 0000000..7d479b6 --- /dev/null +++ b/ccxt/static_dependencies/msgpack/pack_template.h @@ -0,0 +1,820 @@ +/* + * MessagePack packing routine template + * + * Copyright (C) 2008-2010 FURUHASHI Sadayuki + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#if defined(__LITTLE_ENDIAN__) +#define TAKE8_8(d) ((uint8_t*)&d)[0] +#define TAKE8_16(d) ((uint8_t*)&d)[0] +#define TAKE8_32(d) ((uint8_t*)&d)[0] +#define TAKE8_64(d) ((uint8_t*)&d)[0] +#elif defined(__BIG_ENDIAN__) +#define TAKE8_8(d) ((uint8_t*)&d)[0] +#define TAKE8_16(d) ((uint8_t*)&d)[1] +#define TAKE8_32(d) ((uint8_t*)&d)[3] +#define TAKE8_64(d) ((uint8_t*)&d)[7] +#endif + +#ifndef msgpack_pack_append_buffer +#error msgpack_pack_append_buffer callback is not defined +#endif + + +/* + * Integer + */ + +#define msgpack_pack_real_uint8(x, d) \ +do { \ + if(d < (1<<7)) { \ + /* fixnum */ \ + msgpack_pack_append_buffer(x, &TAKE8_8(d), 1); \ + } else { \ + /* unsigned 8 */ \ + unsigned char buf[2] = {0xcc, TAKE8_8(d)}; \ + msgpack_pack_append_buffer(x, buf, 2); \ + } \ +} while(0) + +#define msgpack_pack_real_uint16(x, d) \ +do { \ + if(d < (1<<7)) { \ + /* fixnum */ \ + msgpack_pack_append_buffer(x, &TAKE8_16(d), 1); \ + } else if(d < (1<<8)) { \ + /* unsigned 8 */ \ + unsigned char buf[2] = {0xcc, TAKE8_16(d)}; \ + msgpack_pack_append_buffer(x, buf, 2); \ + } else { \ + /* unsigned 16 */ \ + unsigned char buf[3]; \ + buf[0] = 0xcd; _msgpack_store16(&buf[1], (uint16_t)d); \ + msgpack_pack_append_buffer(x, buf, 3); \ + } \ +} while(0) + +#define msgpack_pack_real_uint32(x, d) \ +do { \ + if(d < (1<<8)) { \ + if(d < (1<<7)) { \ + /* fixnum */ \ + msgpack_pack_append_buffer(x, &TAKE8_32(d), 1); \ + } else { \ + /* unsigned 8 */ \ + unsigned char buf[2] = {0xcc, TAKE8_32(d)}; \ + msgpack_pack_append_buffer(x, buf, 2); \ + } \ + } else { \ + if(d < (1<<16)) { \ + /* unsigned 16 */ \ + unsigned char buf[3]; \ + buf[0] = 0xcd; _msgpack_store16(&buf[1], (uint16_t)d); \ + msgpack_pack_append_buffer(x, buf, 3); \ + } else { \ + /* unsigned 32 */ \ + unsigned char buf[5]; \ + buf[0] = 0xce; _msgpack_store32(&buf[1], (uint32_t)d); \ + msgpack_pack_append_buffer(x, buf, 5); \ + } \ + } \ +} while(0) + +#define msgpack_pack_real_uint64(x, d) \ +do { \ + if(d < (1ULL<<8)) { \ + if(d < (1ULL<<7)) { \ + /* fixnum */ \ + msgpack_pack_append_buffer(x, &TAKE8_64(d), 1); \ + } else { \ + /* unsigned 8 */ \ + unsigned char buf[2] = {0xcc, TAKE8_64(d)}; \ + msgpack_pack_append_buffer(x, buf, 2); \ + } \ + } else { \ + if(d < (1ULL<<16)) { \ + /* unsigned 16 */ \ + unsigned char buf[3]; \ + buf[0] = 0xcd; _msgpack_store16(&buf[1], (uint16_t)d); \ + msgpack_pack_append_buffer(x, buf, 3); \ + } else if(d < (1ULL<<32)) { \ + /* unsigned 32 */ \ + unsigned char buf[5]; \ + buf[0] = 0xce; _msgpack_store32(&buf[1], (uint32_t)d); \ + msgpack_pack_append_buffer(x, buf, 5); \ + } else { \ + /* unsigned 64 */ \ + unsigned char buf[9]; \ + buf[0] = 0xcf; _msgpack_store64(&buf[1], d); \ + msgpack_pack_append_buffer(x, buf, 9); \ + } \ + } \ +} while(0) + +#define msgpack_pack_real_int8(x, d) \ +do { \ + if(d < -(1<<5)) { \ + /* signed 8 */ \ + unsigned char buf[2] = {0xd0, TAKE8_8(d)}; \ + msgpack_pack_append_buffer(x, buf, 2); \ + } else { \ + /* fixnum */ \ + msgpack_pack_append_buffer(x, &TAKE8_8(d), 1); \ + } \ +} while(0) + +#define msgpack_pack_real_int16(x, d) \ +do { \ + if(d < -(1<<5)) { \ + if(d < -(1<<7)) { \ + /* signed 16 */ \ + unsigned char buf[3]; \ + buf[0] = 0xd1; _msgpack_store16(&buf[1], (int16_t)d); \ + msgpack_pack_append_buffer(x, buf, 3); \ + } else { \ + /* signed 8 */ \ + unsigned char buf[2] = {0xd0, TAKE8_16(d)}; \ + msgpack_pack_append_buffer(x, buf, 2); \ + } \ + } else if(d < (1<<7)) { \ + /* fixnum */ \ + msgpack_pack_append_buffer(x, &TAKE8_16(d), 1); \ + } else { \ + if(d < (1<<8)) { \ + /* unsigned 8 */ \ + unsigned char buf[2] = {0xcc, TAKE8_16(d)}; \ + msgpack_pack_append_buffer(x, buf, 2); \ + } else { \ + /* unsigned 16 */ \ + unsigned char buf[3]; \ + buf[0] = 0xcd; _msgpack_store16(&buf[1], (uint16_t)d); \ + msgpack_pack_append_buffer(x, buf, 3); \ + } \ + } \ +} while(0) + +#define msgpack_pack_real_int32(x, d) \ +do { \ + if(d < -(1<<5)) { \ + if(d < -(1<<15)) { \ + /* signed 32 */ \ + unsigned char buf[5]; \ + buf[0] = 0xd2; _msgpack_store32(&buf[1], (int32_t)d); \ + msgpack_pack_append_buffer(x, buf, 5); \ + } else if(d < -(1<<7)) { \ + /* signed 16 */ \ + unsigned char buf[3]; \ + buf[0] = 0xd1; _msgpack_store16(&buf[1], (int16_t)d); \ + msgpack_pack_append_buffer(x, buf, 3); \ + } else { \ + /* signed 8 */ \ + unsigned char buf[2] = {0xd0, TAKE8_32(d)}; \ + msgpack_pack_append_buffer(x, buf, 2); \ + } \ + } else if(d < (1<<7)) { \ + /* fixnum */ \ + msgpack_pack_append_buffer(x, &TAKE8_32(d), 1); \ + } else { \ + if(d < (1<<8)) { \ + /* unsigned 8 */ \ + unsigned char buf[2] = {0xcc, TAKE8_32(d)}; \ + msgpack_pack_append_buffer(x, buf, 2); \ + } else if(d < (1<<16)) { \ + /* unsigned 16 */ \ + unsigned char buf[3]; \ + buf[0] = 0xcd; _msgpack_store16(&buf[1], (uint16_t)d); \ + msgpack_pack_append_buffer(x, buf, 3); \ + } else { \ + /* unsigned 32 */ \ + unsigned char buf[5]; \ + buf[0] = 0xce; _msgpack_store32(&buf[1], (uint32_t)d); \ + msgpack_pack_append_buffer(x, buf, 5); \ + } \ + } \ +} while(0) + +#define msgpack_pack_real_int64(x, d) \ +do { \ + if(d < -(1LL<<5)) { \ + if(d < -(1LL<<15)) { \ + if(d < -(1LL<<31)) { \ + /* signed 64 */ \ + unsigned char buf[9]; \ + buf[0] = 0xd3; _msgpack_store64(&buf[1], d); \ + msgpack_pack_append_buffer(x, buf, 9); \ + } else { \ + /* signed 32 */ \ + unsigned char buf[5]; \ + buf[0] = 0xd2; _msgpack_store32(&buf[1], (int32_t)d); \ + msgpack_pack_append_buffer(x, buf, 5); \ + } \ + } else { \ + if(d < -(1<<7)) { \ + /* signed 16 */ \ + unsigned char buf[3]; \ + buf[0] = 0xd1; _msgpack_store16(&buf[1], (int16_t)d); \ + msgpack_pack_append_buffer(x, buf, 3); \ + } else { \ + /* signed 8 */ \ + unsigned char buf[2] = {0xd0, TAKE8_64(d)}; \ + msgpack_pack_append_buffer(x, buf, 2); \ + } \ + } \ + } else if(d < (1<<7)) { \ + /* fixnum */ \ + msgpack_pack_append_buffer(x, &TAKE8_64(d), 1); \ + } else { \ + if(d < (1LL<<16)) { \ + if(d < (1<<8)) { \ + /* unsigned 8 */ \ + unsigned char buf[2] = {0xcc, TAKE8_64(d)}; \ + msgpack_pack_append_buffer(x, buf, 2); \ + } else { \ + /* unsigned 16 */ \ + unsigned char buf[3]; \ + buf[0] = 0xcd; _msgpack_store16(&buf[1], (uint16_t)d); \ + msgpack_pack_append_buffer(x, buf, 3); \ + } \ + } else { \ + if(d < (1LL<<32)) { \ + /* unsigned 32 */ \ + unsigned char buf[5]; \ + buf[0] = 0xce; _msgpack_store32(&buf[1], (uint32_t)d); \ + msgpack_pack_append_buffer(x, buf, 5); \ + } else { \ + /* unsigned 64 */ \ + unsigned char buf[9]; \ + buf[0] = 0xcf; _msgpack_store64(&buf[1], d); \ + msgpack_pack_append_buffer(x, buf, 9); \ + } \ + } \ + } \ +} while(0) + + +static inline int msgpack_pack_uint8(msgpack_packer* x, uint8_t d) +{ + msgpack_pack_real_uint8(x, d); +} + +static inline int msgpack_pack_uint16(msgpack_packer* x, uint16_t d) +{ + msgpack_pack_real_uint16(x, d); +} + +static inline int msgpack_pack_uint32(msgpack_packer* x, uint32_t d) +{ + msgpack_pack_real_uint32(x, d); +} + +static inline int msgpack_pack_uint64(msgpack_packer* x, uint64_t d) +{ + msgpack_pack_real_uint64(x, d); +} + +static inline int msgpack_pack_int8(msgpack_packer* x, int8_t d) +{ + msgpack_pack_real_int8(x, d); +} + +static inline int msgpack_pack_int16(msgpack_packer* x, int16_t d) +{ + msgpack_pack_real_int16(x, d); +} + +static inline int msgpack_pack_int32(msgpack_packer* x, int32_t d) +{ + msgpack_pack_real_int32(x, d); +} + +static inline int msgpack_pack_int64(msgpack_packer* x, int64_t d) +{ + msgpack_pack_real_int64(x, d); +} + + +//#ifdef msgpack_pack_inline_func_cint + +static inline int msgpack_pack_short(msgpack_packer* x, short d) +{ +#if defined(SIZEOF_SHORT) +#if SIZEOF_SHORT == 2 + msgpack_pack_real_int16(x, d); +#elif SIZEOF_SHORT == 4 + msgpack_pack_real_int32(x, d); +#else + msgpack_pack_real_int64(x, d); +#endif + +#elif defined(SHRT_MAX) +#if SHRT_MAX == 0x7fff + msgpack_pack_real_int16(x, d); +#elif SHRT_MAX == 0x7fffffff + msgpack_pack_real_int32(x, d); +#else + msgpack_pack_real_int64(x, d); +#endif + +#else +if(sizeof(short) == 2) { + msgpack_pack_real_int16(x, d); +} else if(sizeof(short) == 4) { + msgpack_pack_real_int32(x, d); +} else { + msgpack_pack_real_int64(x, d); +} +#endif +} + +static inline int msgpack_pack_int(msgpack_packer* x, int d) +{ +#if defined(SIZEOF_INT) +#if SIZEOF_INT == 2 + msgpack_pack_real_int16(x, d); +#elif SIZEOF_INT == 4 + msgpack_pack_real_int32(x, d); +#else + msgpack_pack_real_int64(x, d); +#endif + +#elif defined(INT_MAX) +#if INT_MAX == 0x7fff + msgpack_pack_real_int16(x, d); +#elif INT_MAX == 0x7fffffff + msgpack_pack_real_int32(x, d); +#else + msgpack_pack_real_int64(x, d); +#endif + +#else +if(sizeof(int) == 2) { + msgpack_pack_real_int16(x, d); +} else if(sizeof(int) == 4) { + msgpack_pack_real_int32(x, d); +} else { + msgpack_pack_real_int64(x, d); +} +#endif +} + +static inline int msgpack_pack_long(msgpack_packer* x, long d) +{ +#if defined(SIZEOF_LONG) +#if SIZEOF_LONG == 2 + msgpack_pack_real_int16(x, d); +#elif SIZEOF_LONG == 4 + msgpack_pack_real_int32(x, d); +#else + msgpack_pack_real_int64(x, d); +#endif + +#elif defined(LONG_MAX) +#if LONG_MAX == 0x7fffL + msgpack_pack_real_int16(x, d); +#elif LONG_MAX == 0x7fffffffL + msgpack_pack_real_int32(x, d); +#else + msgpack_pack_real_int64(x, d); +#endif + +#else +if(sizeof(long) == 2) { + msgpack_pack_real_int16(x, d); +} else if(sizeof(long) == 4) { + msgpack_pack_real_int32(x, d); +} else { + msgpack_pack_real_int64(x, d); +} +#endif +} + +static inline int msgpack_pack_long_long(msgpack_packer* x, long long d) +{ +#if defined(SIZEOF_LONG_LONG) +#if SIZEOF_LONG_LONG == 2 + msgpack_pack_real_int16(x, d); +#elif SIZEOF_LONG_LONG == 4 + msgpack_pack_real_int32(x, d); +#else + msgpack_pack_real_int64(x, d); +#endif + +#elif defined(LLONG_MAX) +#if LLONG_MAX == 0x7fffL + msgpack_pack_real_int16(x, d); +#elif LLONG_MAX == 0x7fffffffL + msgpack_pack_real_int32(x, d); +#else + msgpack_pack_real_int64(x, d); +#endif + +#else +if(sizeof(long long) == 2) { + msgpack_pack_real_int16(x, d); +} else if(sizeof(long long) == 4) { + msgpack_pack_real_int32(x, d); +} else { + msgpack_pack_real_int64(x, d); +} +#endif +} + +static inline int msgpack_pack_unsigned_short(msgpack_packer* x, unsigned short d) +{ +#if defined(SIZEOF_SHORT) +#if SIZEOF_SHORT == 2 + msgpack_pack_real_uint16(x, d); +#elif SIZEOF_SHORT == 4 + msgpack_pack_real_uint32(x, d); +#else + msgpack_pack_real_uint64(x, d); +#endif + +#elif defined(USHRT_MAX) +#if USHRT_MAX == 0xffffU + msgpack_pack_real_uint16(x, d); +#elif USHRT_MAX == 0xffffffffU + msgpack_pack_real_uint32(x, d); +#else + msgpack_pack_real_uint64(x, d); +#endif + +#else +if(sizeof(unsigned short) == 2) { + msgpack_pack_real_uint16(x, d); +} else if(sizeof(unsigned short) == 4) { + msgpack_pack_real_uint32(x, d); +} else { + msgpack_pack_real_uint64(x, d); +} +#endif +} + +static inline int msgpack_pack_unsigned_int(msgpack_packer* x, unsigned int d) +{ +#if defined(SIZEOF_INT) +#if SIZEOF_INT == 2 + msgpack_pack_real_uint16(x, d); +#elif SIZEOF_INT == 4 + msgpack_pack_real_uint32(x, d); +#else + msgpack_pack_real_uint64(x, d); +#endif + +#elif defined(UINT_MAX) +#if UINT_MAX == 0xffffU + msgpack_pack_real_uint16(x, d); +#elif UINT_MAX == 0xffffffffU + msgpack_pack_real_uint32(x, d); +#else + msgpack_pack_real_uint64(x, d); +#endif + +#else +if(sizeof(unsigned int) == 2) { + msgpack_pack_real_uint16(x, d); +} else if(sizeof(unsigned int) == 4) { + msgpack_pack_real_uint32(x, d); +} else { + msgpack_pack_real_uint64(x, d); +} +#endif +} + +static inline int msgpack_pack_unsigned_long(msgpack_packer* x, unsigned long d) +{ +#if defined(SIZEOF_LONG) +#if SIZEOF_LONG == 2 + msgpack_pack_real_uint16(x, d); +#elif SIZEOF_LONG == 4 + msgpack_pack_real_uint32(x, d); +#else + msgpack_pack_real_uint64(x, d); +#endif + +#elif defined(ULONG_MAX) +#if ULONG_MAX == 0xffffUL + msgpack_pack_real_uint16(x, d); +#elif ULONG_MAX == 0xffffffffUL + msgpack_pack_real_uint32(x, d); +#else + msgpack_pack_real_uint64(x, d); +#endif + +#else +if(sizeof(unsigned long) == 2) { + msgpack_pack_real_uint16(x, d); +} else if(sizeof(unsigned long) == 4) { + msgpack_pack_real_uint32(x, d); +} else { + msgpack_pack_real_uint64(x, d); +} +#endif +} + +static inline int msgpack_pack_unsigned_long_long(msgpack_packer* x, unsigned long long d) +{ +#if defined(SIZEOF_LONG_LONG) +#if SIZEOF_LONG_LONG == 2 + msgpack_pack_real_uint16(x, d); +#elif SIZEOF_LONG_LONG == 4 + msgpack_pack_real_uint32(x, d); +#else + msgpack_pack_real_uint64(x, d); +#endif + +#elif defined(ULLONG_MAX) +#if ULLONG_MAX == 0xffffUL + msgpack_pack_real_uint16(x, d); +#elif ULLONG_MAX == 0xffffffffUL + msgpack_pack_real_uint32(x, d); +#else + msgpack_pack_real_uint64(x, d); +#endif + +#else +if(sizeof(unsigned long long) == 2) { + msgpack_pack_real_uint16(x, d); +} else if(sizeof(unsigned long long) == 4) { + msgpack_pack_real_uint32(x, d); +} else { + msgpack_pack_real_uint64(x, d); +} +#endif +} + +//#undef msgpack_pack_inline_func_cint +//#endif + + + +/* + * Float + */ + +static inline int msgpack_pack_float(msgpack_packer* x, float d) +{ + unsigned char buf[5]; + buf[0] = 0xca; + +#if PY_VERSION_HEX >= 0x030B00A7 + PyFloat_Pack4(d, (char *)&buf[1], 0); +#else + _PyFloat_Pack4(d, &buf[1], 0); +#endif + msgpack_pack_append_buffer(x, buf, 5); +} + +static inline int msgpack_pack_double(msgpack_packer* x, double d) +{ + unsigned char buf[9]; + buf[0] = 0xcb; +#if PY_VERSION_HEX >= 0x030B00A7 + PyFloat_Pack8(d, (char *)&buf[1], 0); +#else + _PyFloat_Pack8(d, &buf[1], 0); +#endif + msgpack_pack_append_buffer(x, buf, 9); +} + + +/* + * Nil + */ + +static inline int msgpack_pack_nil(msgpack_packer* x) +{ + static const unsigned char d = 0xc0; + msgpack_pack_append_buffer(x, &d, 1); +} + + +/* + * Boolean + */ + +static inline int msgpack_pack_true(msgpack_packer* x) +{ + static const unsigned char d = 0xc3; + msgpack_pack_append_buffer(x, &d, 1); +} + +static inline int msgpack_pack_false(msgpack_packer* x) +{ + static const unsigned char d = 0xc2; + msgpack_pack_append_buffer(x, &d, 1); +} + + +/* + * Array + */ + +static inline int msgpack_pack_array(msgpack_packer* x, unsigned int n) +{ + if(n < 16) { + unsigned char d = 0x90 | n; + msgpack_pack_append_buffer(x, &d, 1); + } else if(n < 65536) { + unsigned char buf[3]; + buf[0] = 0xdc; _msgpack_store16(&buf[1], (uint16_t)n); + msgpack_pack_append_buffer(x, buf, 3); + } else { + unsigned char buf[5]; + buf[0] = 0xdd; _msgpack_store32(&buf[1], (uint32_t)n); + msgpack_pack_append_buffer(x, buf, 5); + } +} + + +/* + * Map + */ + +static inline int msgpack_pack_map(msgpack_packer* x, unsigned int n) +{ + if(n < 16) { + unsigned char d = 0x80 | n; + msgpack_pack_append_buffer(x, &TAKE8_8(d), 1); + } else if(n < 65536) { + unsigned char buf[3]; + buf[0] = 0xde; _msgpack_store16(&buf[1], (uint16_t)n); + msgpack_pack_append_buffer(x, buf, 3); + } else { + unsigned char buf[5]; + buf[0] = 0xdf; _msgpack_store32(&buf[1], (uint32_t)n); + msgpack_pack_append_buffer(x, buf, 5); + } +} + + +/* + * Raw + */ + +static inline int msgpack_pack_raw(msgpack_packer* x, size_t l) +{ + if (l < 32) { + unsigned char d = 0xa0 | (uint8_t)l; + msgpack_pack_append_buffer(x, &TAKE8_8(d), 1); + } else if (x->use_bin_type && l < 256) { // str8 is new format introduced with bin. + unsigned char buf[2] = {0xd9, (uint8_t)l}; + msgpack_pack_append_buffer(x, buf, 2); + } else if (l < 65536) { + unsigned char buf[3]; + buf[0] = 0xda; _msgpack_store16(&buf[1], (uint16_t)l); + msgpack_pack_append_buffer(x, buf, 3); + } else { + unsigned char buf[5]; + buf[0] = 0xdb; _msgpack_store32(&buf[1], (uint32_t)l); + msgpack_pack_append_buffer(x, buf, 5); + } +} + +/* + * bin + */ +static inline int msgpack_pack_bin(msgpack_packer *x, size_t l) +{ + if (!x->use_bin_type) { + return msgpack_pack_raw(x, l); + } + if (l < 256) { + unsigned char buf[2] = {0xc4, (unsigned char)l}; + msgpack_pack_append_buffer(x, buf, 2); + } else if (l < 65536) { + unsigned char buf[3] = {0xc5}; + _msgpack_store16(&buf[1], (uint16_t)l); + msgpack_pack_append_buffer(x, buf, 3); + } else { + unsigned char buf[5] = {0xc6}; + _msgpack_store32(&buf[1], (uint32_t)l); + msgpack_pack_append_buffer(x, buf, 5); + } +} + +static inline int msgpack_pack_raw_body(msgpack_packer* x, const void* b, size_t l) +{ + if (l > 0) msgpack_pack_append_buffer(x, (const unsigned char*)b, l); + return 0; +} + +/* + * Ext + */ +static inline int msgpack_pack_ext(msgpack_packer* x, char typecode, size_t l) +{ + if (l == 1) { + unsigned char buf[2]; + buf[0] = 0xd4; + buf[1] = (unsigned char)typecode; + msgpack_pack_append_buffer(x, buf, 2); + } + else if(l == 2) { + unsigned char buf[2]; + buf[0] = 0xd5; + buf[1] = (unsigned char)typecode; + msgpack_pack_append_buffer(x, buf, 2); + } + else if(l == 4) { + unsigned char buf[2]; + buf[0] = 0xd6; + buf[1] = (unsigned char)typecode; + msgpack_pack_append_buffer(x, buf, 2); + } + else if(l == 8) { + unsigned char buf[2]; + buf[0] = 0xd7; + buf[1] = (unsigned char)typecode; + msgpack_pack_append_buffer(x, buf, 2); + } + else if(l == 16) { + unsigned char buf[2]; + buf[0] = 0xd8; + buf[1] = (unsigned char)typecode; + msgpack_pack_append_buffer(x, buf, 2); + } + else if(l < 256) { + unsigned char buf[3]; + buf[0] = 0xc7; + buf[1] = l; + buf[2] = (unsigned char)typecode; + msgpack_pack_append_buffer(x, buf, 3); + } else if(l < 65536) { + unsigned char buf[4]; + buf[0] = 0xc8; + _msgpack_store16(&buf[1], (uint16_t)l); + buf[3] = (unsigned char)typecode; + msgpack_pack_append_buffer(x, buf, 4); + } else { + unsigned char buf[6]; + buf[0] = 0xc9; + _msgpack_store32(&buf[1], (uint32_t)l); + buf[5] = (unsigned char)typecode; + msgpack_pack_append_buffer(x, buf, 6); + } + +} + +/* + * Pack Timestamp extension type. Follows msgpack-c pack_template.h. + */ +static inline int msgpack_pack_timestamp(msgpack_packer* x, int64_t seconds, uint32_t nanoseconds) +{ + if ((seconds >> 34) == 0) { + /* seconds is unsigned and fits in 34 bits */ + uint64_t data64 = ((uint64_t)nanoseconds << 34) | (uint64_t)seconds; + if ((data64 & 0xffffffff00000000L) == 0) { + /* no nanoseconds and seconds is 32bits or smaller. timestamp32. */ + unsigned char buf[4]; + uint32_t data32 = (uint32_t)data64; + msgpack_pack_ext(x, -1, 4); + _msgpack_store32(buf, data32); + msgpack_pack_raw_body(x, buf, 4); + } else { + /* timestamp64 */ + unsigned char buf[8]; + msgpack_pack_ext(x, -1, 8); + _msgpack_store64(buf, data64); + msgpack_pack_raw_body(x, buf, 8); + + } + } else { + /* seconds is signed or >34bits */ + unsigned char buf[12]; + _msgpack_store32(&buf[0], nanoseconds); + _msgpack_store64(&buf[4], seconds); + msgpack_pack_ext(x, -1, 12); + msgpack_pack_raw_body(x, buf, 12); + } + return 0; +} + + +#undef msgpack_pack_append_buffer + +#undef TAKE8_8 +#undef TAKE8_16 +#undef TAKE8_32 +#undef TAKE8_64 + +#undef msgpack_pack_real_uint8 +#undef msgpack_pack_real_uint16 +#undef msgpack_pack_real_uint32 +#undef msgpack_pack_real_uint64 +#undef msgpack_pack_real_int8 +#undef msgpack_pack_real_int16 +#undef msgpack_pack_real_int32 +#undef msgpack_pack_real_int64 diff --git a/ccxt/static_dependencies/msgpack/sysdep.h b/ccxt/static_dependencies/msgpack/sysdep.h new file mode 100644 index 0000000..7067300 --- /dev/null +++ b/ccxt/static_dependencies/msgpack/sysdep.h @@ -0,0 +1,194 @@ +/* + * MessagePack system dependencies + * + * Copyright (C) 2008-2010 FURUHASHI Sadayuki + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#ifndef MSGPACK_SYSDEP_H__ +#define MSGPACK_SYSDEP_H__ + +#include +#include +#if defined(_MSC_VER) && _MSC_VER < 1600 +typedef __int8 int8_t; +typedef unsigned __int8 uint8_t; +typedef __int16 int16_t; +typedef unsigned __int16 uint16_t; +typedef __int32 int32_t; +typedef unsigned __int32 uint32_t; +typedef __int64 int64_t; +typedef unsigned __int64 uint64_t; +#elif defined(_MSC_VER) // && _MSC_VER >= 1600 +#include +#else +#include +#include +#endif + +#ifdef _WIN32 +#define _msgpack_atomic_counter_header +typedef long _msgpack_atomic_counter_t; +#define _msgpack_sync_decr_and_fetch(ptr) InterlockedDecrement(ptr) +#define _msgpack_sync_incr_and_fetch(ptr) InterlockedIncrement(ptr) +#elif defined(__GNUC__) && ((__GNUC__*10 + __GNUC_MINOR__) < 41) +#define _msgpack_atomic_counter_header "gcc_atomic.h" +#else +typedef unsigned int _msgpack_atomic_counter_t; +#define _msgpack_sync_decr_and_fetch(ptr) __sync_sub_and_fetch(ptr, 1) +#define _msgpack_sync_incr_and_fetch(ptr) __sync_add_and_fetch(ptr, 1) +#endif + +#ifdef _WIN32 + +#ifdef __cplusplus +/* numeric_limits::min,max */ +#ifdef max +#undef max +#endif +#ifdef min +#undef min +#endif +#endif + +#else /* _WIN32 */ +#include /* ntohs, ntohl */ +#endif + +#if !defined(__LITTLE_ENDIAN__) && !defined(__BIG_ENDIAN__) +#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ +#define __LITTLE_ENDIAN__ +#elif __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ +#define __BIG_ENDIAN__ +#elif _WIN32 +#define __LITTLE_ENDIAN__ +#endif +#endif + + +#ifdef __LITTLE_ENDIAN__ + +#ifdef _WIN32 +# if defined(ntohs) +# define _msgpack_be16(x) ntohs(x) +# elif defined(_byteswap_ushort) || (defined(_MSC_VER) && _MSC_VER >= 1400) +# define _msgpack_be16(x) ((uint16_t)_byteswap_ushort((unsigned short)x)) +# else +# define _msgpack_be16(x) ( \ + ((((uint16_t)x) << 8) ) | \ + ((((uint16_t)x) >> 8) ) ) +# endif +#else +# define _msgpack_be16(x) ntohs(x) +#endif + +#ifdef _WIN32 +# if defined(ntohl) +# define _msgpack_be32(x) ntohl(x) +# elif defined(_byteswap_ulong) || defined(_MSC_VER) +# define _msgpack_be32(x) ((uint32_t)_byteswap_ulong((unsigned long)x)) +# else +# define _msgpack_be32(x) \ + ( ((((uint32_t)x) << 24) ) | \ + ((((uint32_t)x) << 8) & 0x00ff0000U ) | \ + ((((uint32_t)x) >> 8) & 0x0000ff00U ) | \ + ((((uint32_t)x) >> 24) ) ) +# endif +#else +# define _msgpack_be32(x) ntohl(x) +#endif + +#if defined(_byteswap_uint64) || defined(_MSC_VER) +# define _msgpack_be64(x) (_byteswap_uint64(x)) +#elif defined(bswap_64) +# define _msgpack_be64(x) bswap_64(x) +#elif defined(__DARWIN_OSSwapInt64) +# define _msgpack_be64(x) __DARWIN_OSSwapInt64(x) +#else +#define _msgpack_be64(x) \ + ( ((((uint64_t)x) << 56) ) | \ + ((((uint64_t)x) << 40) & 0x00ff000000000000ULL ) | \ + ((((uint64_t)x) << 24) & 0x0000ff0000000000ULL ) | \ + ((((uint64_t)x) << 8) & 0x000000ff00000000ULL ) | \ + ((((uint64_t)x) >> 8) & 0x00000000ff000000ULL ) | \ + ((((uint64_t)x) >> 24) & 0x0000000000ff0000ULL ) | \ + ((((uint64_t)x) >> 40) & 0x000000000000ff00ULL ) | \ + ((((uint64_t)x) >> 56) ) ) +#endif + +#define _msgpack_load16(cast, from) ((cast)( \ + (((uint16_t)((uint8_t*)(from))[0]) << 8) | \ + (((uint16_t)((uint8_t*)(from))[1]) ) )) + +#define _msgpack_load32(cast, from) ((cast)( \ + (((uint32_t)((uint8_t*)(from))[0]) << 24) | \ + (((uint32_t)((uint8_t*)(from))[1]) << 16) | \ + (((uint32_t)((uint8_t*)(from))[2]) << 8) | \ + (((uint32_t)((uint8_t*)(from))[3]) ) )) + +#define _msgpack_load64(cast, from) ((cast)( \ + (((uint64_t)((uint8_t*)(from))[0]) << 56) | \ + (((uint64_t)((uint8_t*)(from))[1]) << 48) | \ + (((uint64_t)((uint8_t*)(from))[2]) << 40) | \ + (((uint64_t)((uint8_t*)(from))[3]) << 32) | \ + (((uint64_t)((uint8_t*)(from))[4]) << 24) | \ + (((uint64_t)((uint8_t*)(from))[5]) << 16) | \ + (((uint64_t)((uint8_t*)(from))[6]) << 8) | \ + (((uint64_t)((uint8_t*)(from))[7]) ) )) + +#else + +#define _msgpack_be16(x) (x) +#define _msgpack_be32(x) (x) +#define _msgpack_be64(x) (x) + +#define _msgpack_load16(cast, from) ((cast)( \ + (((uint16_t)((uint8_t*)from)[0]) << 8) | \ + (((uint16_t)((uint8_t*)from)[1]) ) )) + +#define _msgpack_load32(cast, from) ((cast)( \ + (((uint32_t)((uint8_t*)from)[0]) << 24) | \ + (((uint32_t)((uint8_t*)from)[1]) << 16) | \ + (((uint32_t)((uint8_t*)from)[2]) << 8) | \ + (((uint32_t)((uint8_t*)from)[3]) ) )) + +#define _msgpack_load64(cast, from) ((cast)( \ + (((uint64_t)((uint8_t*)from)[0]) << 56) | \ + (((uint64_t)((uint8_t*)from)[1]) << 48) | \ + (((uint64_t)((uint8_t*)from)[2]) << 40) | \ + (((uint64_t)((uint8_t*)from)[3]) << 32) | \ + (((uint64_t)((uint8_t*)from)[4]) << 24) | \ + (((uint64_t)((uint8_t*)from)[5]) << 16) | \ + (((uint64_t)((uint8_t*)from)[6]) << 8) | \ + (((uint64_t)((uint8_t*)from)[7]) ) )) +#endif + + +#define _msgpack_store16(to, num) \ + do { uint16_t val = _msgpack_be16(num); memcpy(to, &val, 2); } while(0) +#define _msgpack_store32(to, num) \ + do { uint32_t val = _msgpack_be32(num); memcpy(to, &val, 4); } while(0) +#define _msgpack_store64(to, num) \ + do { uint64_t val = _msgpack_be64(num); memcpy(to, &val, 8); } while(0) + +/* +#define _msgpack_load16(cast, from) \ + ({ cast val; memcpy(&val, (char*)from, 2); _msgpack_be16(val); }) +#define _msgpack_load32(cast, from) \ + ({ cast val; memcpy(&val, (char*)from, 4); _msgpack_be32(val); }) +#define _msgpack_load64(cast, from) \ + ({ cast val; memcpy(&val, (char*)from, 8); _msgpack_be64(val); }) +*/ + + +#endif /* msgpack/sysdep.h */ diff --git a/ccxt/static_dependencies/msgpack/unpack.h b/ccxt/static_dependencies/msgpack/unpack.h new file mode 100644 index 0000000..23aa622 --- /dev/null +++ b/ccxt/static_dependencies/msgpack/unpack.h @@ -0,0 +1,391 @@ +/* + * MessagePack for Python unpacking routine + * + * Copyright (C) 2009 Naoki INADA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define MSGPACK_EMBED_STACK_SIZE (1024) +#include "unpack_define.h" + +typedef struct unpack_user { + bool use_list; + bool raw; + bool has_pairs_hook; + bool strict_map_key; + int timestamp; + PyObject *object_hook; + PyObject *list_hook; + PyObject *ext_hook; + PyObject *timestamp_t; + PyObject *giga; + PyObject *utc; + const char *unicode_errors; + Py_ssize_t max_str_len, max_bin_len, max_array_len, max_map_len, max_ext_len; +} unpack_user; + +typedef PyObject* msgpack_unpack_object; +struct unpack_context; +typedef struct unpack_context unpack_context; +typedef int (*execute_fn)(unpack_context *ctx, const char* data, Py_ssize_t len, Py_ssize_t* off); + +static inline msgpack_unpack_object unpack_callback_root(unpack_user* u) +{ + return NULL; +} + +static inline int unpack_callback_uint16(unpack_user* u, uint16_t d, msgpack_unpack_object* o) +{ + PyObject *p = PyInt_FromLong((long)d); + if (!p) + return -1; + *o = p; + return 0; +} +static inline int unpack_callback_uint8(unpack_user* u, uint8_t d, msgpack_unpack_object* o) +{ + return unpack_callback_uint16(u, d, o); +} + + +static inline int unpack_callback_uint32(unpack_user* u, uint32_t d, msgpack_unpack_object* o) +{ + PyObject *p = PyInt_FromSize_t((size_t)d); + if (!p) + return -1; + *o = p; + return 0; +} + +static inline int unpack_callback_uint64(unpack_user* u, uint64_t d, msgpack_unpack_object* o) +{ + PyObject *p; + if (d > LONG_MAX) { + p = PyLong_FromUnsignedLongLong((unsigned PY_LONG_LONG)d); + } else { + p = PyInt_FromLong((long)d); + } + if (!p) + return -1; + *o = p; + return 0; +} + +static inline int unpack_callback_int32(unpack_user* u, int32_t d, msgpack_unpack_object* o) +{ + PyObject *p = PyInt_FromLong(d); + if (!p) + return -1; + *o = p; + return 0; +} + +static inline int unpack_callback_int16(unpack_user* u, int16_t d, msgpack_unpack_object* o) +{ + return unpack_callback_int32(u, d, o); +} + +static inline int unpack_callback_int8(unpack_user* u, int8_t d, msgpack_unpack_object* o) +{ + return unpack_callback_int32(u, d, o); +} + +static inline int unpack_callback_int64(unpack_user* u, int64_t d, msgpack_unpack_object* o) +{ + PyObject *p; + if (d > LONG_MAX || d < LONG_MIN) { + p = PyLong_FromLongLong((PY_LONG_LONG)d); + } else { + p = PyInt_FromLong((long)d); + } + *o = p; + return 0; +} + +static inline int unpack_callback_double(unpack_user* u, double d, msgpack_unpack_object* o) +{ + PyObject *p = PyFloat_FromDouble(d); + if (!p) + return -1; + *o = p; + return 0; +} + +static inline int unpack_callback_float(unpack_user* u, float d, msgpack_unpack_object* o) +{ + return unpack_callback_double(u, d, o); +} + +static inline int unpack_callback_nil(unpack_user* u, msgpack_unpack_object* o) +{ Py_INCREF(Py_None); *o = Py_None; return 0; } + +static inline int unpack_callback_true(unpack_user* u, msgpack_unpack_object* o) +{ Py_INCREF(Py_True); *o = Py_True; return 0; } + +static inline int unpack_callback_false(unpack_user* u, msgpack_unpack_object* o) +{ Py_INCREF(Py_False); *o = Py_False; return 0; } + +static inline int unpack_callback_array(unpack_user* u, unsigned int n, msgpack_unpack_object* o) +{ + if (n > u->max_array_len) { + PyErr_Format(PyExc_ValueError, "%u exceeds max_array_len(%zd)", n, u->max_array_len); + return -1; + } + PyObject *p = u->use_list ? PyList_New(n) : PyTuple_New(n); + + if (!p) + return -1; + *o = p; + return 0; +} + +static inline int unpack_callback_array_item(unpack_user* u, unsigned int current, msgpack_unpack_object* c, msgpack_unpack_object o) +{ + if (u->use_list) + PyList_SET_ITEM(*c, current, o); + else + PyTuple_SET_ITEM(*c, current, o); + return 0; +} + +static inline int unpack_callback_array_end(unpack_user* u, msgpack_unpack_object* c) +{ + if (u->list_hook) { + PyObject *new_c = PyObject_CallFunctionObjArgs(u->list_hook, *c, NULL); + if (!new_c) + return -1; + Py_DECREF(*c); + *c = new_c; + } + return 0; +} + +static inline int unpack_callback_map(unpack_user* u, unsigned int n, msgpack_unpack_object* o) +{ + if (n > u->max_map_len) { + PyErr_Format(PyExc_ValueError, "%u exceeds max_map_len(%zd)", n, u->max_map_len); + return -1; + } + PyObject *p; + if (u->has_pairs_hook) { + p = PyList_New(n); // Or use tuple? + } + else { + p = PyDict_New(); + } + if (!p) + return -1; + *o = p; + return 0; +} + +static inline int unpack_callback_map_item(unpack_user* u, unsigned int current, msgpack_unpack_object* c, msgpack_unpack_object k, msgpack_unpack_object v) +{ + if (u->strict_map_key && !PyUnicode_CheckExact(k) && !PyBytes_CheckExact(k)) { + PyErr_Format(PyExc_ValueError, "%.100s is not allowed for map key when strict_map_key=True", Py_TYPE(k)->tp_name); + return -1; + } + if (PyUnicode_CheckExact(k)) { + PyUnicode_InternInPlace(&k); + } + if (u->has_pairs_hook) { + msgpack_unpack_object item = PyTuple_Pack(2, k, v); + if (!item) + return -1; + Py_DECREF(k); + Py_DECREF(v); + PyList_SET_ITEM(*c, current, item); + return 0; + } + else if (PyDict_SetItem(*c, k, v) == 0) { + Py_DECREF(k); + Py_DECREF(v); + return 0; + } + return -1; +} + +static inline int unpack_callback_map_end(unpack_user* u, msgpack_unpack_object* c) +{ + if (u->object_hook) { + PyObject *new_c = PyObject_CallFunctionObjArgs(u->object_hook, *c, NULL); + if (!new_c) + return -1; + + Py_DECREF(*c); + *c = new_c; + } + return 0; +} + +static inline int unpack_callback_raw(unpack_user* u, const char* b, const char* p, unsigned int l, msgpack_unpack_object* o) +{ + if (l > u->max_str_len) { + PyErr_Format(PyExc_ValueError, "%u exceeds max_str_len(%zd)", l, u->max_str_len); + return -1; + } + + PyObject *py; + + if (u->raw) { + py = PyBytes_FromStringAndSize(p, l); + } else { + py = PyUnicode_DecodeUTF8(p, l, u->unicode_errors); + } + if (!py) + return -1; + *o = py; + return 0; +} + +static inline int unpack_callback_bin(unpack_user* u, const char* b, const char* p, unsigned int l, msgpack_unpack_object* o) +{ + if (l > u->max_bin_len) { + PyErr_Format(PyExc_ValueError, "%u exceeds max_bin_len(%zd)", l, u->max_bin_len); + return -1; + } + + PyObject *py = PyBytes_FromStringAndSize(p, l); + if (!py) + return -1; + *o = py; + return 0; +} + +typedef struct msgpack_timestamp { + int64_t tv_sec; + uint32_t tv_nsec; +} msgpack_timestamp; + +/* + * Unpack ext buffer to a timestamp. Pulled from msgpack-c timestamp.h. + */ +static int unpack_timestamp(const char* buf, unsigned int buflen, msgpack_timestamp* ts) { + switch (buflen) { + case 4: + ts->tv_nsec = 0; + { + uint32_t v = _msgpack_load32(uint32_t, buf); + ts->tv_sec = (int64_t)v; + } + return 0; + case 8: { + uint64_t value =_msgpack_load64(uint64_t, buf); + ts->tv_nsec = (uint32_t)(value >> 34); + ts->tv_sec = value & 0x00000003ffffffffLL; + return 0; + } + case 12: + ts->tv_nsec = _msgpack_load32(uint32_t, buf); + ts->tv_sec = _msgpack_load64(int64_t, buf + 4); + return 0; + default: + return -1; + } +} + +#include "datetime.h" + +static int unpack_callback_ext(unpack_user* u, const char* base, const char* pos, + unsigned int length, msgpack_unpack_object* o) +{ + int8_t typecode = (int8_t)*pos++; + if (!u->ext_hook) { + PyErr_SetString(PyExc_AssertionError, "u->ext_hook cannot be NULL"); + return -1; + } + if (length-1 > u->max_ext_len) { + PyErr_Format(PyExc_ValueError, "%u exceeds max_ext_len(%zd)", length, u->max_ext_len); + return -1; + } + + PyObject *py = NULL; + // length also includes the typecode, so the actual data is length-1 + if (typecode == -1) { + msgpack_timestamp ts; + if (unpack_timestamp(pos, length-1, &ts) < 0) { + return -1; + } + + if (u->timestamp == 2) { // int + PyObject *a = PyLong_FromLongLong(ts.tv_sec); + if (a == NULL) return -1; + + PyObject *c = PyNumber_Multiply(a, u->giga); + Py_DECREF(a); + if (c == NULL) { + return -1; + } + + PyObject *b = PyLong_FromUnsignedLong(ts.tv_nsec); + if (b == NULL) { + Py_DECREF(c); + return -1; + } + + py = PyNumber_Add(c, b); + Py_DECREF(c); + Py_DECREF(b); + } + else if (u->timestamp == 0) { // Timestamp + py = PyObject_CallFunction(u->timestamp_t, "(Lk)", ts.tv_sec, ts.tv_nsec); + } + else if (u->timestamp == 3) { // datetime + // Calculate datetime using epoch + delta + // due to limitations PyDateTime_FromTimestamp on Windows with negative timestamps + PyObject *epoch = PyDateTimeAPI->DateTime_FromDateAndTime(1970, 1, 1, 0, 0, 0, 0, u->utc, PyDateTimeAPI->DateTimeType); + if (epoch == NULL) { + return -1; + } + + PyObject* d = PyDelta_FromDSU(ts.tv_sec/(24*3600), ts.tv_sec%(24*3600), ts.tv_nsec / 1000); + if (d == NULL) { + Py_DECREF(epoch); + return -1; + } + + py = PyNumber_Add(epoch, d); + + Py_DECREF(epoch); + Py_DECREF(d); + } + else { // float + PyObject *a = PyFloat_FromDouble((double)ts.tv_nsec); + if (a == NULL) return -1; + + PyObject *b = PyNumber_TrueDivide(a, u->giga); + Py_DECREF(a); + if (b == NULL) return -1; + + PyObject *c = PyLong_FromLongLong(ts.tv_sec); + if (c == NULL) { + Py_DECREF(b); + return -1; + } + + a = PyNumber_Add(b, c); + Py_DECREF(b); + Py_DECREF(c); + py = a; + } + } else { + py = PyObject_CallFunction(u->ext_hook, "(iy#)", (int)typecode, pos, (Py_ssize_t)length-1); + } + if (!py) + return -1; + *o = py; + return 0; +} + +#include "unpack_template.h" diff --git a/ccxt/static_dependencies/msgpack/unpack_define.h b/ccxt/static_dependencies/msgpack/unpack_define.h new file mode 100644 index 0000000..0dd708d --- /dev/null +++ b/ccxt/static_dependencies/msgpack/unpack_define.h @@ -0,0 +1,95 @@ +/* + * MessagePack unpacking routine template + * + * Copyright (C) 2008-2010 FURUHASHI Sadayuki + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#ifndef MSGPACK_UNPACK_DEFINE_H__ +#define MSGPACK_UNPACK_DEFINE_H__ + +#include "msgpack/sysdep.h" +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + + +#ifndef MSGPACK_EMBED_STACK_SIZE +#define MSGPACK_EMBED_STACK_SIZE 32 +#endif + + +// CS is first byte & 0x1f +typedef enum { + CS_HEADER = 0x00, // nil + + //CS_ = 0x01, + //CS_ = 0x02, // false + //CS_ = 0x03, // true + + CS_BIN_8 = 0x04, + CS_BIN_16 = 0x05, + CS_BIN_32 = 0x06, + + CS_EXT_8 = 0x07, + CS_EXT_16 = 0x08, + CS_EXT_32 = 0x09, + + CS_FLOAT = 0x0a, + CS_DOUBLE = 0x0b, + CS_UINT_8 = 0x0c, + CS_UINT_16 = 0x0d, + CS_UINT_32 = 0x0e, + CS_UINT_64 = 0x0f, + CS_INT_8 = 0x10, + CS_INT_16 = 0x11, + CS_INT_32 = 0x12, + CS_INT_64 = 0x13, + + //CS_FIXEXT1 = 0x14, + //CS_FIXEXT2 = 0x15, + //CS_FIXEXT4 = 0x16, + //CS_FIXEXT8 = 0x17, + //CS_FIXEXT16 = 0x18, + + CS_RAW_8 = 0x19, + CS_RAW_16 = 0x1a, + CS_RAW_32 = 0x1b, + CS_ARRAY_16 = 0x1c, + CS_ARRAY_32 = 0x1d, + CS_MAP_16 = 0x1e, + CS_MAP_32 = 0x1f, + + ACS_RAW_VALUE, + ACS_BIN_VALUE, + ACS_EXT_VALUE, +} msgpack_unpack_state; + + +typedef enum { + CT_ARRAY_ITEM, + CT_MAP_KEY, + CT_MAP_VALUE, +} msgpack_container_type; + + +#ifdef __cplusplus +} +#endif + +#endif /* msgpack/unpack_define.h */ diff --git a/ccxt/static_dependencies/msgpack/unpack_template.h b/ccxt/static_dependencies/msgpack/unpack_template.h new file mode 100644 index 0000000..8b9fcc1 --- /dev/null +++ b/ccxt/static_dependencies/msgpack/unpack_template.h @@ -0,0 +1,464 @@ +/* + * MessagePack unpacking routine template + * + * Copyright (C) 2008-2010 FURUHASHI Sadayuki + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef USE_CASE_RANGE +#if !defined(_MSC_VER) +#define USE_CASE_RANGE +#endif +#endif + +typedef struct unpack_stack { + PyObject* obj; + Py_ssize_t size; + Py_ssize_t count; + unsigned int ct; + PyObject* map_key; +} unpack_stack; + +struct unpack_context { + unpack_user user; + unsigned int cs; + unsigned int trail; + unsigned int top; + /* + unpack_stack* stack; + unsigned int stack_size; + unpack_stack embed_stack[MSGPACK_EMBED_STACK_SIZE]; + */ + unpack_stack stack[MSGPACK_EMBED_STACK_SIZE]; +}; + + +static inline void unpack_init(unpack_context* ctx) +{ + ctx->cs = CS_HEADER; + ctx->trail = 0; + ctx->top = 0; + /* + ctx->stack = ctx->embed_stack; + ctx->stack_size = MSGPACK_EMBED_STACK_SIZE; + */ + ctx->stack[0].obj = unpack_callback_root(&ctx->user); +} + +/* +static inline void unpack_destroy(unpack_context* ctx) +{ + if(ctx->stack_size != MSGPACK_EMBED_STACK_SIZE) { + free(ctx->stack); + } +} +*/ + +static inline PyObject* unpack_data(unpack_context* ctx) +{ + return (ctx)->stack[0].obj; +} + +static inline void unpack_clear(unpack_context *ctx) +{ + Py_CLEAR(ctx->stack[0].obj); +} + +template +static inline int unpack_execute(unpack_context* ctx, const char* data, Py_ssize_t len, Py_ssize_t* off) +{ + assert(len >= *off); + + const unsigned char* p = (unsigned char*)data + *off; + const unsigned char* const pe = (unsigned char*)data + len; + const void* n = p; + + unsigned int trail = ctx->trail; + unsigned int cs = ctx->cs; + unsigned int top = ctx->top; + unpack_stack* stack = ctx->stack; + /* + unsigned int stack_size = ctx->stack_size; + */ + unpack_user* user = &ctx->user; + + PyObject* obj = NULL; + unpack_stack* c = NULL; + + int ret; + +#define construct_cb(name) \ + construct && unpack_callback ## name + +#define push_simple_value(func) \ + if(construct_cb(func)(user, &obj) < 0) { goto _failed; } \ + goto _push +#define push_fixed_value(func, arg) \ + if(construct_cb(func)(user, arg, &obj) < 0) { goto _failed; } \ + goto _push +#define push_variable_value(func, base, pos, len) \ + if(construct_cb(func)(user, \ + (const char*)base, (const char*)pos, len, &obj) < 0) { goto _failed; } \ + goto _push + +#define again_fixed_trail(_cs, trail_len) \ + trail = trail_len; \ + cs = _cs; \ + goto _fixed_trail_again +#define again_fixed_trail_if_zero(_cs, trail_len, ifzero) \ + trail = trail_len; \ + if(trail == 0) { goto ifzero; } \ + cs = _cs; \ + goto _fixed_trail_again + +#define start_container(func, count_, ct_) \ + if(top >= MSGPACK_EMBED_STACK_SIZE) { ret = -3; goto _end; } \ + if(construct_cb(func)(user, count_, &stack[top].obj) < 0) { goto _failed; } \ + if((count_) == 0) { obj = stack[top].obj; \ + if (construct_cb(func##_end)(user, &obj) < 0) { goto _failed; } \ + goto _push; } \ + stack[top].ct = ct_; \ + stack[top].size = count_; \ + stack[top].count = 0; \ + ++top; \ + goto _header_again + +#define NEXT_CS(p) ((unsigned int)*p & 0x1f) + +#ifdef USE_CASE_RANGE +#define SWITCH_RANGE_BEGIN switch(*p) { +#define SWITCH_RANGE(FROM, TO) case FROM ... TO: +#define SWITCH_RANGE_DEFAULT default: +#define SWITCH_RANGE_END } +#else +#define SWITCH_RANGE_BEGIN { if(0) { +#define SWITCH_RANGE(FROM, TO) } else if(FROM <= *p && *p <= TO) { +#define SWITCH_RANGE_DEFAULT } else { +#define SWITCH_RANGE_END } } +#endif + + if(p == pe) { goto _out; } + do { + switch(cs) { + case CS_HEADER: + SWITCH_RANGE_BEGIN + SWITCH_RANGE(0x00, 0x7f) // Positive Fixnum + push_fixed_value(_uint8, *(uint8_t*)p); + SWITCH_RANGE(0xe0, 0xff) // Negative Fixnum + push_fixed_value(_int8, *(int8_t*)p); + SWITCH_RANGE(0xc0, 0xdf) // Variable + switch(*p) { + case 0xc0: // nil + push_simple_value(_nil); + //case 0xc1: // never used + case 0xc2: // false + push_simple_value(_false); + case 0xc3: // true + push_simple_value(_true); + case 0xc4: // bin 8 + again_fixed_trail(NEXT_CS(p), 1); + case 0xc5: // bin 16 + again_fixed_trail(NEXT_CS(p), 2); + case 0xc6: // bin 32 + again_fixed_trail(NEXT_CS(p), 4); + case 0xc7: // ext 8 + again_fixed_trail(NEXT_CS(p), 1); + case 0xc8: // ext 16 + again_fixed_trail(NEXT_CS(p), 2); + case 0xc9: // ext 32 + again_fixed_trail(NEXT_CS(p), 4); + case 0xca: // float + case 0xcb: // double + case 0xcc: // unsigned int 8 + case 0xcd: // unsigned int 16 + case 0xce: // unsigned int 32 + case 0xcf: // unsigned int 64 + case 0xd0: // signed int 8 + case 0xd1: // signed int 16 + case 0xd2: // signed int 32 + case 0xd3: // signed int 64 + again_fixed_trail(NEXT_CS(p), 1 << (((unsigned int)*p) & 0x03)); + case 0xd4: // fixext 1 + case 0xd5: // fixext 2 + case 0xd6: // fixext 4 + case 0xd7: // fixext 8 + again_fixed_trail_if_zero(ACS_EXT_VALUE, + (1 << (((unsigned int)*p) & 0x03))+1, + _ext_zero); + case 0xd8: // fixext 16 + again_fixed_trail_if_zero(ACS_EXT_VALUE, 16+1, _ext_zero); + case 0xd9: // str 8 + again_fixed_trail(NEXT_CS(p), 1); + case 0xda: // raw 16 + case 0xdb: // raw 32 + case 0xdc: // array 16 + case 0xdd: // array 32 + case 0xde: // map 16 + case 0xdf: // map 32 + again_fixed_trail(NEXT_CS(p), 2 << (((unsigned int)*p) & 0x01)); + default: + ret = -2; + goto _end; + } + SWITCH_RANGE(0xa0, 0xbf) // FixRaw + again_fixed_trail_if_zero(ACS_RAW_VALUE, ((unsigned int)*p & 0x1f), _raw_zero); + SWITCH_RANGE(0x90, 0x9f) // FixArray + start_container(_array, ((unsigned int)*p) & 0x0f, CT_ARRAY_ITEM); + SWITCH_RANGE(0x80, 0x8f) // FixMap + start_container(_map, ((unsigned int)*p) & 0x0f, CT_MAP_KEY); + + SWITCH_RANGE_DEFAULT + ret = -2; + goto _end; + SWITCH_RANGE_END + // end CS_HEADER + + + _fixed_trail_again: + ++p; + + default: + if((size_t)(pe - p) < trail) { goto _out; } + n = p; p += trail - 1; + switch(cs) { + case CS_EXT_8: + again_fixed_trail_if_zero(ACS_EXT_VALUE, *(uint8_t*)n+1, _ext_zero); + case CS_EXT_16: + again_fixed_trail_if_zero(ACS_EXT_VALUE, + _msgpack_load16(uint16_t,n)+1, + _ext_zero); + case CS_EXT_32: + again_fixed_trail_if_zero(ACS_EXT_VALUE, + _msgpack_load32(uint32_t,n)+1, + _ext_zero); + case CS_FLOAT: { + double f; +#if PY_VERSION_HEX >= 0x030B00A7 + f = PyFloat_Unpack4((const char*)n, 0); +#else + f = _PyFloat_Unpack4((unsigned char*)n, 0); +#endif + push_fixed_value(_float, f); } + case CS_DOUBLE: { + double f; +#if PY_VERSION_HEX >= 0x030B00A7 + f = PyFloat_Unpack8((const char*)n, 0); +#else + f = _PyFloat_Unpack8((unsigned char*)n, 0); +#endif + push_fixed_value(_double, f); } + case CS_UINT_8: + push_fixed_value(_uint8, *(uint8_t*)n); + case CS_UINT_16: + push_fixed_value(_uint16, _msgpack_load16(uint16_t,n)); + case CS_UINT_32: + push_fixed_value(_uint32, _msgpack_load32(uint32_t,n)); + case CS_UINT_64: + push_fixed_value(_uint64, _msgpack_load64(uint64_t,n)); + + case CS_INT_8: + push_fixed_value(_int8, *(int8_t*)n); + case CS_INT_16: + push_fixed_value(_int16, _msgpack_load16(int16_t,n)); + case CS_INT_32: + push_fixed_value(_int32, _msgpack_load32(int32_t,n)); + case CS_INT_64: + push_fixed_value(_int64, _msgpack_load64(int64_t,n)); + + case CS_BIN_8: + again_fixed_trail_if_zero(ACS_BIN_VALUE, *(uint8_t*)n, _bin_zero); + case CS_BIN_16: + again_fixed_trail_if_zero(ACS_BIN_VALUE, _msgpack_load16(uint16_t,n), _bin_zero); + case CS_BIN_32: + again_fixed_trail_if_zero(ACS_BIN_VALUE, _msgpack_load32(uint32_t,n), _bin_zero); + case ACS_BIN_VALUE: + _bin_zero: + push_variable_value(_bin, data, n, trail); + + case CS_RAW_8: + again_fixed_trail_if_zero(ACS_RAW_VALUE, *(uint8_t*)n, _raw_zero); + case CS_RAW_16: + again_fixed_trail_if_zero(ACS_RAW_VALUE, _msgpack_load16(uint16_t,n), _raw_zero); + case CS_RAW_32: + again_fixed_trail_if_zero(ACS_RAW_VALUE, _msgpack_load32(uint32_t,n), _raw_zero); + case ACS_RAW_VALUE: + _raw_zero: + push_variable_value(_raw, data, n, trail); + + case ACS_EXT_VALUE: + _ext_zero: + push_variable_value(_ext, data, n, trail); + + case CS_ARRAY_16: + start_container(_array, _msgpack_load16(uint16_t,n), CT_ARRAY_ITEM); + case CS_ARRAY_32: + /* FIXME security guard */ + start_container(_array, _msgpack_load32(uint32_t,n), CT_ARRAY_ITEM); + + case CS_MAP_16: + start_container(_map, _msgpack_load16(uint16_t,n), CT_MAP_KEY); + case CS_MAP_32: + /* FIXME security guard */ + start_container(_map, _msgpack_load32(uint32_t,n), CT_MAP_KEY); + + default: + goto _failed; + } + } + +_push: + if(top == 0) { goto _finish; } + c = &stack[top-1]; + switch(c->ct) { + case CT_ARRAY_ITEM: + if(construct_cb(_array_item)(user, c->count, &c->obj, obj) < 0) { goto _failed; } + if(++c->count == c->size) { + obj = c->obj; + if (construct_cb(_array_end)(user, &obj) < 0) { goto _failed; } + --top; + /*printf("stack pop %d\n", top);*/ + goto _push; + } + goto _header_again; + case CT_MAP_KEY: + c->map_key = obj; + c->ct = CT_MAP_VALUE; + goto _header_again; + case CT_MAP_VALUE: + if(construct_cb(_map_item)(user, c->count, &c->obj, c->map_key, obj) < 0) { goto _failed; } + if(++c->count == c->size) { + obj = c->obj; + if (construct_cb(_map_end)(user, &obj) < 0) { goto _failed; } + --top; + /*printf("stack pop %d\n", top);*/ + goto _push; + } + c->ct = CT_MAP_KEY; + goto _header_again; + + default: + goto _failed; + } + +_header_again: + cs = CS_HEADER; + ++p; + } while(p != pe); + goto _out; + + +_finish: + if (!construct) + unpack_callback_nil(user, &obj); + stack[0].obj = obj; + ++p; + ret = 1; + /*printf("-- finish --\n"); */ + goto _end; + +_failed: + /*printf("** FAILED **\n"); */ + ret = -1; + goto _end; + +_out: + ret = 0; + goto _end; + +_end: + ctx->cs = cs; + ctx->trail = trail; + ctx->top = top; + *off = p - (const unsigned char*)data; + + return ret; +#undef construct_cb +} + +#undef SWITCH_RANGE_BEGIN +#undef SWITCH_RANGE +#undef SWITCH_RANGE_DEFAULT +#undef SWITCH_RANGE_END +#undef push_simple_value +#undef push_fixed_value +#undef push_variable_value +#undef again_fixed_trail +#undef again_fixed_trail_if_zero +#undef start_container + +template +static inline int unpack_container_header(unpack_context* ctx, const char* data, Py_ssize_t len, Py_ssize_t* off) +{ + assert(len >= *off); + uint32_t size; + const unsigned char *const p = (unsigned char*)data + *off; + +#define inc_offset(inc) \ + if (len - *off < inc) \ + return 0; \ + *off += inc; + + switch (*p) { + case var_offset: + inc_offset(3); + size = _msgpack_load16(uint16_t, p + 1); + break; + case var_offset + 1: + inc_offset(5); + size = _msgpack_load32(uint32_t, p + 1); + break; +#ifdef USE_CASE_RANGE + case fixed_offset + 0x0 ... fixed_offset + 0xf: +#else + case fixed_offset + 0x0: + case fixed_offset + 0x1: + case fixed_offset + 0x2: + case fixed_offset + 0x3: + case fixed_offset + 0x4: + case fixed_offset + 0x5: + case fixed_offset + 0x6: + case fixed_offset + 0x7: + case fixed_offset + 0x8: + case fixed_offset + 0x9: + case fixed_offset + 0xa: + case fixed_offset + 0xb: + case fixed_offset + 0xc: + case fixed_offset + 0xd: + case fixed_offset + 0xe: + case fixed_offset + 0xf: +#endif + ++*off; + size = ((unsigned int)*p) & 0x0f; + break; + default: + PyErr_SetString(PyExc_ValueError, "Unexpected type header on stream"); + return -1; + } + unpack_callback_uint32(&ctx->user, size, &ctx->stack[0].obj); + return 1; +} + +#undef SWITCH_RANGE_BEGIN +#undef SWITCH_RANGE +#undef SWITCH_RANGE_DEFAULT +#undef SWITCH_RANGE_END + +static const execute_fn unpack_construct = &unpack_execute; +static const execute_fn unpack_skip = &unpack_execute; +static const execute_fn read_array_header = &unpack_container_header<0x90, 0xdc>; +static const execute_fn read_map_header = &unpack_container_header<0x80, 0xde>; + +#undef NEXT_CS + +/* vim: set ts=4 sw=4 sts=4 expandtab */ diff --git a/ccxt/static_dependencies/parsimonious/__init__.py b/ccxt/static_dependencies/parsimonious/__init__.py new file mode 100644 index 0000000..62de46b --- /dev/null +++ b/ccxt/static_dependencies/parsimonious/__init__.py @@ -0,0 +1,10 @@ +"""Parsimonious's public API. Import from here. + +Things may move around in modules deeper than this one. + +""" +from .exceptions import (ParseError, IncompleteParseError, + VisitationError, UndefinedLabel, + BadGrammar) +from .grammar import Grammar, TokenGrammar +from .nodes import NodeVisitor, VisitationError, rule diff --git a/ccxt/static_dependencies/parsimonious/__pycache__/__init__.cpython-311.pyc b/ccxt/static_dependencies/parsimonious/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..484f1d7 Binary files /dev/null and b/ccxt/static_dependencies/parsimonious/__pycache__/__init__.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/parsimonious/__pycache__/exceptions.cpython-311.pyc b/ccxt/static_dependencies/parsimonious/__pycache__/exceptions.cpython-311.pyc new file mode 100644 index 0000000..3bfd6ae Binary files /dev/null and b/ccxt/static_dependencies/parsimonious/__pycache__/exceptions.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/parsimonious/__pycache__/expressions.cpython-311.pyc b/ccxt/static_dependencies/parsimonious/__pycache__/expressions.cpython-311.pyc new file mode 100644 index 0000000..02cfb9c Binary files /dev/null and b/ccxt/static_dependencies/parsimonious/__pycache__/expressions.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/parsimonious/__pycache__/grammar.cpython-311.pyc b/ccxt/static_dependencies/parsimonious/__pycache__/grammar.cpython-311.pyc new file mode 100644 index 0000000..29af03b Binary files /dev/null and b/ccxt/static_dependencies/parsimonious/__pycache__/grammar.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/parsimonious/__pycache__/nodes.cpython-311.pyc b/ccxt/static_dependencies/parsimonious/__pycache__/nodes.cpython-311.pyc new file mode 100644 index 0000000..c2645f7 Binary files /dev/null and b/ccxt/static_dependencies/parsimonious/__pycache__/nodes.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/parsimonious/__pycache__/utils.cpython-311.pyc b/ccxt/static_dependencies/parsimonious/__pycache__/utils.cpython-311.pyc new file mode 100644 index 0000000..19f5e91 Binary files /dev/null and b/ccxt/static_dependencies/parsimonious/__pycache__/utils.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/parsimonious/exceptions.py b/ccxt/static_dependencies/parsimonious/exceptions.py new file mode 100644 index 0000000..69e1ad9 --- /dev/null +++ b/ccxt/static_dependencies/parsimonious/exceptions.py @@ -0,0 +1,105 @@ + +from .utils import StrAndRepr + + +class ParseError(StrAndRepr, Exception): + """A call to ``Expression.parse()`` or ``match()`` didn't match.""" + + def __init__(self, text, pos=-1, expr=None): + # It would be nice to use self.args, but I don't want to pay a penalty + # to call descriptors or have the confusion of numerical indices in + # Expression.match_core(). + self.text = text + self.pos = pos + self.expr = expr + + def __str__(self): + rule_name = ((u"'%s'" % self.expr.name) if self.expr.name else + str(self.expr)) + return u"Rule %s didn't match at '%s' (line %s, column %s)." % ( + rule_name, + self.text[self.pos:self.pos + 20], + self.line(), + self.column()) + + # TODO: Add line, col, and separated-out error message so callers can build + # their own presentation. + + def line(self): + """Return the 1-based line number where the expression ceased to + match.""" + # This is a method rather than a property in case we ever wanted to + # pass in which line endings we want to use. + return self.text.count('\n', 0, self.pos) + 1 + + def column(self): + """Return the 1-based column where the expression ceased to match.""" + # We choose 1-based because that's what Python does with SyntaxErrors. + try: + return self.pos - self.text.rindex('\n', 0, self.pos) + except ValueError: + return self.pos + 1 + + +class IncompleteParseError(ParseError): + """A call to ``parse()`` matched a whole Expression but did not consume the + entire text.""" + + def __str__(self): + return u"Rule '%s' matched in its entirety, but it didn't consume all the text. The non-matching portion of the text begins with '%s' (line %s, column %s)." % ( + self.expr.name, + self.text[self.pos:self.pos + 20], + self.line(), + self.column()) + + +class VisitationError(Exception): + """Something went wrong while traversing a parse tree. + + This exception exists to augment an underlying exception with information + about where in the parse tree the error occurred. Otherwise, it could be + tiresome to figure out what went wrong; you'd have to play back the whole + tree traversal in your head. + + """ + # TODO: Make sure this is pickleable. Probably use @property pattern. Make + # the original exc and node available on it if they don't cause a whole + # raft of stack frames to be retained. + def __init__(self, exc, exc_class, node): + """Construct. + + :arg exc: What went wrong. We wrap this and add more info. + :arg node: The node at which the error occurred + + """ + self.original_class = exc_class + super(VisitationError, self).__init__( + '%s: %s\n\n' + 'Parse tree:\n' + '%s' % + (exc_class.__name__, + exc, + node.prettily(error=node))) + + +class BadGrammar(StrAndRepr, Exception): + """Something was wrong with the definition of a grammar. + + Note that a ParseError might be raised instead if the error is in the + grammar definition syntax. + + """ + + +class UndefinedLabel(BadGrammar): + """A rule referenced in a grammar was never defined. + + Circular references and forward references are okay, but you have to define + stuff at some point. + + """ + def __init__(self, label): + self.label = label + + def __str__(self): + return u'The label "%s" was never defined.' % self.label diff --git a/ccxt/static_dependencies/parsimonious/expressions.py b/ccxt/static_dependencies/parsimonious/expressions.py new file mode 100644 index 0000000..790dbd8 --- /dev/null +++ b/ccxt/static_dependencies/parsimonious/expressions.py @@ -0,0 +1,479 @@ +"""Subexpressions that make up a parsed grammar + +These do the parsing. + +""" +# TODO: Make sure all symbol refs are local--not class lookups or +# anything--for speed. And kill all the dots. + +from inspect import getfullargspec, isfunction, ismethod, ismethoddescriptor +import re + +from .exceptions import ParseError, IncompleteParseError +from .nodes import Node, RegexNode +from .utils import StrAndRepr + +MARKER = object() + + +def is_callable(value): + criteria = [isfunction, ismethod, ismethoddescriptor] + return any([criterion(value) for criterion in criteria]) + + +def expression(callable, rule_name, grammar): + """Turn a plain callable into an Expression. + + The callable can be of this simple form:: + + def foo(text, pos): + '''If this custom expression matches starting at text[pos], return + the index where it stops matching. Otherwise, return None.''' + if the expression matched: + return end_pos + + If there child nodes to return, return a tuple:: + + return end_pos, children + + If the expression doesn't match at the given ``pos`` at all... :: + + return None + + If your callable needs to make sub-calls to other rules in the grammar or + do error reporting, it can take this form, gaining additional arguments:: + + def foo(text, pos, cache, error, grammar): + # Call out to other rules: + node = grammar['another_rule'].match_core(text, pos, cache, error) + ... + # Return values as above. + + The return value of the callable, if an int or a tuple, will be + automatically transmuted into a :class:`~.Node`. If it returns + a Node-like class directly, it will be passed through unchanged. + + :arg rule_name: The rule name to attach to the resulting + :class:`~.Expression` + :arg grammar: The :class:`~.Grammar` this expression will be a + part of, to make delegating to other rules possible + + """ + + # Resolve unbound methods; allows grammars to use @staticmethod custom rules + # https://stackoverflow.com/questions/41921255/staticmethod-object-is-not-callable + if ismethoddescriptor(callable) and hasattr(callable, '__func__'): + callable = callable.__func__ + + num_args = len(getfullargspec(callable).args) + if ismethod(callable): + # do not count the first argument (typically 'self') for methods + num_args -= 1 + if num_args == 2: + is_simple = True + elif num_args == 5: + is_simple = False + else: + raise RuntimeError("Custom rule functions must take either 2 or 5 " + "arguments, not %s." % num_args) + + class AdHocExpression(Expression): + def _uncached_match(self, text, pos, cache, error): + result = (callable(text, pos) if is_simple else + callable(text, pos, cache, error, grammar)) + + if isinstance(result, int): + end, children = result, None + elif isinstance(result, tuple): + end, children = result + else: + # Node or None + return result + return Node(self, text, pos, end, children=children) + + def _as_rhs(self): + return '{custom function "%s"}' % callable.__name__ + + return AdHocExpression(name=rule_name) + + +class Expression(StrAndRepr): + """A thing that can be matched against a piece of text""" + + # Slots are about twice as fast as __dict__-based attributes: + # http://stackoverflow.com/questions/1336791/dictionary-vs-object-which-is-more-efficient-and-why + + # Top-level expressions--rules--have names. Subexpressions are named ''. + __slots__ = ['name', 'identity_tuple'] + + def __init__(self, name=''): + self.name = name + self.identity_tuple = (self.name, ) + + def __hash__(self): + return hash(self.identity_tuple) + + def __eq__(self, other): + return isinstance(other, self.__class__) and self.identity_tuple == other.identity_tuple + + def __ne__(self, other): + return not (self == other) + + def parse(self, text, pos=0): + """Return a parse tree of ``text``. + + Raise ``ParseError`` if the expression wasn't satisfied. Raise + ``IncompleteParseError`` if the expression was satisfied but didn't + consume the full string. + + """ + node = self.match(text, pos=pos) + if node.end < len(text): + raise IncompleteParseError(text, node.end, self) + return node + + def match(self, text, pos=0): + """Return the parse tree matching this expression at the given + position, not necessarily extending all the way to the end of ``text``. + + Raise ``ParseError`` if there is no match there. + + :arg pos: The index at which to start matching + + """ + error = ParseError(text) + node = self.match_core(text, pos, {}, error) + if node is None: + raise error + return node + + def match_core(self, text, pos, cache, error): + """Internal guts of ``match()`` + + This is appropriate to call only from custom rules or Expression + subclasses. + + :arg cache: The packrat cache:: + + {(oid, pos): Node tree matched by object `oid` at index `pos` ...} + + :arg error: A ParseError instance with ``text`` already filled in but + otherwise blank. We update the error reporting info on this object + as we go. (Sticking references on an existing instance is faster + than allocating a new one for each expression that fails.) We + return None rather than raising and catching ParseErrors because + catching is slow. + + """ + # TODO: Optimize. Probably a hot spot. + # + # Is there a way of looking up cached stuff that's faster than hashing + # this id-pos pair? + # + # If this is slow, think about the array module. It might (or might + # not!) use more RAM, but it'll likely be faster than hashing things + # all the time. Also, can we move all the allocs up front? + # + # To save space, we have lots of choices: (0) Quit caching whole Node + # objects. Cache just what you need to reconstitute them. (1) Cache + # only the results of entire rules, not subexpressions (probably a + # horrible idea for rules that need to backtrack internally a lot). (2) + # Age stuff out of the cache somehow. LRU? (3) Cuts. + expr_id = id(self) + node = cache.get((expr_id, pos), MARKER) # TODO: Change to setdefault to prevent infinite recursion in left-recursive rules. + if node is MARKER: + node = cache[(expr_id, pos)] = self._uncached_match(text, + pos, + cache, + error) + + # Record progress for error reporting: + if node is None and pos >= error.pos and ( + self.name or getattr(error.expr, 'name', None) is None): + # Don't bother reporting on unnamed expressions (unless that's all + # we've seen so far), as they're hard to track down for a human. + # Perhaps we could include the unnamed subexpressions later as + # auxiliary info. + error.expr = self + error.pos = pos + + return node + + def __str__(self): + return u'<%s %s>' % ( + self.__class__.__name__, + self.as_rule()) + + def as_rule(self): + """Return the left- and right-hand sides of a rule that represents me. + + Return unicode. If I have no ``name``, omit the left-hand side. + + """ + rhs = self._as_rhs().strip() + if rhs.startswith('(') and rhs.endswith(')'): + rhs = rhs[1:-1] + + return (u'%s = %s' % (self.name, rhs)) if self.name else rhs + + def _unicode_members(self): + """Return an iterable of my unicode-represented children, stopping + descent when we hit a named node so the returned value resembles the + input rule.""" + return [(m.name or m._as_rhs()) for m in self.members] + + def _as_rhs(self): + """Return the right-hand side of a rule that represents me. + + Implemented by subclasses. + + """ + raise NotImplementedError + + +class Literal(Expression): + """A string literal + + Use these if you can; they're the fastest. + + """ + __slots__ = ['literal'] + + def __init__(self, literal, name=''): + super(Literal, self).__init__(name) + self.literal = literal + self.identity_tuple = (name, literal) + + def _uncached_match(self, text, pos, cache, error): + if text.startswith(self.literal, pos): + return Node(self, text, pos, pos + len(self.literal)) + + def _as_rhs(self): + return repr(self.literal) + + +class TokenMatcher(Literal): + """An expression matching a single token of a given type + + This is for use only with TokenGrammars. + + """ + def _uncached_match(self, token_list, pos, cache, error): + if token_list[pos].type == self.literal: + return Node(self, token_list, pos, pos + 1) + + +class Regex(Expression): + """An expression that matches what a regex does. + + Use these as much as you can and jam as much into each one as you can; + they're fast. + + """ + __slots__ = ['re'] + + def __init__(self, pattern, name='', ignore_case=False, locale=False, + multiline=False, dot_all=False, unicode=False, verbose=False, ascii=False): + super(Regex, self).__init__(name) + self.re = re.compile(pattern, (ignore_case and re.I) | + (locale and re.L) | + (multiline and re.M) | + (dot_all and re.S) | + (unicode and re.U) | + (verbose and re.X) | + (ascii and re.A)) + self.identity_tuple = (self.name, self.re) + + def _uncached_match(self, text, pos, cache, error): + """Return length of match, ``None`` if no match.""" + m = self.re.match(text, pos) + if m is not None: + span = m.span() + node = RegexNode(self, text, pos, pos + span[1] - span[0]) + node.match = m # TODO: A terrible idea for cache size? + return node + + def _regex_flags_from_bits(self, bits): + """Return the textual equivalent of numerically encoded regex flags.""" + flags = 'ilmsuxa' + return ''.join(flags[i - 1] if (1 << i) & bits else '' for i in range(1, len(flags) + 1)) + + def _as_rhs(self): + return '~{!r}{}'.format(self.re.pattern, + self._regex_flags_from_bits(self.re.flags)) + + +class Compound(Expression): + """An abstract expression which contains other expressions""" + + __slots__ = ['members'] + + def __init__(self, *members, **kwargs): + """``members`` is a sequence of expressions.""" + super(Compound, self).__init__(kwargs.get('name', '')) + self.members = members + + def __hash__(self): + # Note we leave members out of the hash computation, since compounds can get added to + # sets, then have their members mutated. See RuleVisitor._resolve_refs. + # Equality should still work, but we want the rules to go into the correct hash bucket. + return hash((self.__class__, self.name)) + + def __eq__(self, other): + return ( + isinstance(other, self.__class__) and + self.name == other.name and + self.members == other.members) + + +class Sequence(Compound): + """A series of expressions that must match contiguous, ordered pieces of + the text + + In other words, it's a concatenation operator: each piece has to match, one + after another. + + """ + def _uncached_match(self, text, pos, cache, error): + new_pos = pos + length_of_sequence = 0 + children = [] + for m in self.members: + node = m.match_core(text, new_pos, cache, error) + if node is None: + return None + children.append(node) + length = node.end - node.start + new_pos += length + length_of_sequence += length + # Hooray! We got through all the members! + return Node(self, text, pos, pos + length_of_sequence, children) + + def _as_rhs(self): + return u'({0})'.format(u' '.join(self._unicode_members())) + + +class OneOf(Compound): + """A series of expressions, one of which must match + + Expressions are tested in order from first to last. The first to succeed + wins. + + """ + def _uncached_match(self, text, pos, cache, error): + for m in self.members: + node = m.match_core(text, pos, cache, error) + if node is not None: + # Wrap the succeeding child in a node representing the OneOf: + return Node(self, text, pos, node.end, children=[node]) + + def _as_rhs(self): + return u'({0})'.format(u' / '.join(self._unicode_members())) + + +class Lookahead(Compound): + """An expression which consumes nothing, even if its contained expression + succeeds""" + + # TODO: Merge this and Not for better cache hit ratios and less code. + # Downside: pretty-printed grammars might be spelled differently than what + # went in. That doesn't bother me. + + def _uncached_match(self, text, pos, cache, error): + node = self.members[0].match_core(text, pos, cache, error) + if node is not None: + return Node(self, text, pos, pos) + + def _as_rhs(self): + return u'&%s' % self._unicode_members()[0] + + +class Not(Compound): + """An expression that succeeds only if the expression within it doesn't + + In any case, it never consumes any characters; it's a negative lookahead. + + """ + def _uncached_match(self, text, pos, cache, error): + # FWIW, the implementation in Parsing Techniques in Figure 15.29 does + # not bother to cache NOTs directly. + node = self.members[0].match_core(text, pos, cache, error) + if node is None: + return Node(self, text, pos, pos) + + def _as_rhs(self): + # TODO: Make sure this parenthesizes the member properly if it's an OR + # or AND. + return u'!%s' % self._unicode_members()[0] + + +# Quantifiers. None of these is strictly necessary, but they're darn handy. + +class Optional(Compound): + """An expression that succeeds whether or not the contained one does + + If the contained expression succeeds, it goes ahead and consumes what it + consumes. Otherwise, it consumes nothing. + + """ + def _uncached_match(self, text, pos, cache, error): + node = self.members[0].match_core(text, pos, cache, error) + return (Node(self, text, pos, pos) if node is None else + Node(self, text, pos, node.end, children=[node])) + + def _as_rhs(self): + return u'%s?' % self._unicode_members()[0] + + +# TODO: Merge with OneOrMore. +class ZeroOrMore(Compound): + """An expression wrapper like the * quantifier in regexes.""" + + def _uncached_match(self, text, pos, cache, error): + new_pos = pos + children = [] + while True: + node = self.members[0].match_core(text, new_pos, cache, error) + if node is None or not (node.end - node.start): + # Node was None or 0 length. 0 would otherwise loop infinitely. + return Node(self, text, pos, new_pos, children) + children.append(node) + new_pos += node.end - node.start + + def _as_rhs(self): + return u'%s*' % self._unicode_members()[0] + + +class OneOrMore(Compound): + """An expression wrapper like the + quantifier in regexes. + + You can also pass in an alternate minimum to make this behave like "2 or + more", "3 or more", etc. + + """ + __slots__ = ['min'] + + # TODO: Add max. It should probably succeed if there are more than the max + # --just not consume them. + + def __init__(self, member, name='', min=1): + super(OneOrMore, self).__init__(member, name=name) + self.min = min + + def _uncached_match(self, text, pos, cache, error): + new_pos = pos + children = [] + while True: + node = self.members[0].match_core(text, new_pos, cache, error) + if node is None: + break + children.append(node) + length = node.end - node.start + if length == 0: # Don't loop infinitely. + break + new_pos += length + if len(children) >= self.min: + return Node(self, text, pos, new_pos, children) + + def _as_rhs(self): + return u'%s+' % self._unicode_members()[0] diff --git a/ccxt/static_dependencies/parsimonious/grammar.py b/ccxt/static_dependencies/parsimonious/grammar.py new file mode 100644 index 0000000..bd89ebf --- /dev/null +++ b/ccxt/static_dependencies/parsimonious/grammar.py @@ -0,0 +1,487 @@ +"""A convenience which constructs expression trees from an easy-to-read syntax + +Use this unless you have a compelling reason not to; it performs some +optimizations that would be tedious to do when constructing an expression tree +by hand. + +""" +from collections import OrderedDict + +from .exceptions import BadGrammar, UndefinedLabel +from .expressions import (Literal, Regex, Sequence, OneOf, + Lookahead, Optional, ZeroOrMore, OneOrMore, Not, TokenMatcher, + expression, is_callable) +from .nodes import NodeVisitor +from .utils import evaluate_string + +class Grammar(OrderedDict): + """A collection of rules that describe a language + + You can start parsing from the default rule by calling ``parse()`` + directly on the ``Grammar`` object:: + + g = Grammar(''' + polite_greeting = greeting ", my good " title + greeting = "Hi" / "Hello" + title = "madam" / "sir" + ''') + g.parse('Hello, my good sir') + + Or start parsing from any of the other rules; you can pull them out of the + grammar as if it were a dictionary:: + + g['title'].parse('sir') + + You could also just construct a bunch of ``Expression`` objects yourself + and stitch them together into a language, but using a ``Grammar`` has some + important advantages: + + * Languages are much easier to define in the nice syntax it provides. + * Circular references aren't a pain. + * It does all kinds of whizzy space- and time-saving optimizations, like + factoring up repeated subexpressions into a single object, which should + increase cache hit ratio. [Is this implemented yet?] + + """ + def __init__(self, rules='', **more_rules): + """Construct a grammar. + + :arg rules: A string of production rules, one per line. + :arg default_rule: The name of the rule invoked when you call + :meth:`parse()` or :meth:`match()` on the grammar. Defaults to the + first rule. Falls back to None if there are no string-based rules + in this grammar. + :arg more_rules: Additional kwargs whose names are rule names and + values are Expressions or custom-coded callables which accomplish + things the built-in rule syntax cannot. These take precedence over + ``rules`` in case of naming conflicts. + + """ + + decorated_custom_rules = { + k: (expression(v, k, self) if is_callable(v) else v) + for k, v in more_rules.items()} + + exprs, first = self._expressions_from_rules(rules, decorated_custom_rules) + super(Grammar, self).__init__(exprs.items()) + self.default_rule = first # may be None + + def default(self, rule_name): + """Return a new Grammar whose :term:`default rule` is ``rule_name``.""" + new = self._copy() + new.default_rule = new[rule_name] + return new + + def _copy(self): + """Return a shallow copy of myself. + + Deep is unnecessary, since Expression trees are immutable. Subgrammars + recreate all the Expressions from scratch, and AbstractGrammars have + no Expressions. + + """ + new = Grammar.__new__(Grammar) + super(Grammar, new).__init__(self.items()) + new.default_rule = self.default_rule + return new + + def _expressions_from_rules(self, rules, custom_rules): + """Return a 2-tuple: a dict of rule names pointing to their + expressions, and then the first rule. + + It's a web of expressions, all referencing each other. Typically, + there's a single root to the web of references, and that root is the + starting symbol for parsing, but there's nothing saying you can't have + multiple roots. + + :arg custom_rules: A map of rule names to custom-coded rules: + Expressions + + """ + tree = rule_grammar.parse(rules) + return RuleVisitor(custom_rules).visit(tree) + + def parse(self, text, pos=0): + """Parse some text with the :term:`default rule`. + + :arg pos: The index at which to start parsing + + """ + self._check_default_rule() + return self.default_rule.parse(text, pos=pos) + + def match(self, text, pos=0): + """Parse some text with the :term:`default rule` but not necessarily + all the way to the end. + + :arg pos: The index at which to start parsing + + """ + self._check_default_rule() + return self.default_rule.match(text, pos=pos) + + def _check_default_rule(self): + """Raise RuntimeError if there is no default rule defined.""" + if not self.default_rule: + raise RuntimeError("Can't call parse() on a Grammar that has no " + "default rule. Choose a specific rule instead, " + "like some_grammar['some_rule'].parse(...).") + + def __str__(self): + """Return a rule string that, when passed to the constructor, would + reconstitute the grammar.""" + exprs = [self.default_rule] if self.default_rule else [] + exprs.extend(expr for expr in self.values() if + expr is not self.default_rule) + return '\n'.join(expr.as_rule() for expr in exprs) + + def __repr__(self): + """Return an expression that will reconstitute the grammar.""" + return "Grammar({!r})".format(str(self)) + + +class TokenGrammar(Grammar): + """A Grammar which takes a list of pre-lexed tokens instead of text + + This is useful if you want to do the lexing yourself, as a separate pass: + for example, to implement indentation-based languages. + + """ + def _expressions_from_rules(self, rules, custom_rules): + tree = rule_grammar.parse(rules) + return TokenRuleVisitor(custom_rules).visit(tree) + + +class BootstrappingGrammar(Grammar): + """The grammar used to recognize the textual rules that describe other + grammars + + This grammar gets its start from some hard-coded Expressions and claws its + way from there to an expression tree that describes how to parse the + grammar description syntax. + + """ + def _expressions_from_rules(self, rule_syntax, custom_rules): + """Return the rules for parsing the grammar definition syntax. + + Return a 2-tuple: a dict of rule names pointing to their expressions, + and then the top-level expression for the first rule. + + """ + # Hard-code enough of the rules to parse the grammar that describes the + # grammar description language, to bootstrap: + comment = Regex(r'#[^\r\n]*', name='comment') + meaninglessness = OneOf(Regex(r'\s+'), comment, name='meaninglessness') + _ = ZeroOrMore(meaninglessness, name='_') + equals = Sequence(Literal('='), _, name='equals') + label = Sequence(Regex(r'[a-zA-Z_][a-zA-Z_0-9]*'), _, name='label') + reference = Sequence(label, Not(equals), name='reference') + quantifier = Sequence(Regex(r'[*+?]'), _, name='quantifier') + # This pattern supports empty literals. TODO: A problem? + spaceless_literal = Regex(r'u?r?"[^"\\]*(?:\\.[^"\\]*)*"', + ignore_case=True, + dot_all=True, + name='spaceless_literal') + literal = Sequence(spaceless_literal, _, name='literal') + regex = Sequence(Literal('~'), + literal, + Regex('[ilmsuxa]*', ignore_case=True), + _, + name='regex') + atom = OneOf(reference, literal, regex, name='atom') + quantified = Sequence(atom, quantifier, name='quantified') + + term = OneOf(quantified, atom, name='term') + not_term = Sequence(Literal('!'), term, _, name='not_term') + term.members = (not_term,) + term.members + + sequence = Sequence(term, OneOrMore(term), name='sequence') + or_term = Sequence(Literal('/'), _, term, name='or_term') + ored = Sequence(term, OneOrMore(or_term), name='ored') + expression = OneOf(ored, sequence, term, name='expression') + rule = Sequence(label, equals, expression, name='rule') + rules = Sequence(_, OneOrMore(rule), name='rules') + + # Use those hard-coded rules to parse the (more extensive) rule syntax. + # (For example, unless I start using parentheses in the rule language + # definition itself, I should never have to hard-code expressions for + # those above.) + + rule_tree = rules.parse(rule_syntax) + + # Turn the parse tree into a map of expressions: + return RuleVisitor().visit(rule_tree) + + +# The grammar for parsing PEG grammar definitions: +# This is a nice, simple grammar. We may someday add to it, but it's a safe bet +# that the future will always be a superset of this. +rule_syntax = (r''' + # Ignored things (represented by _) are typically hung off the end of the + # leafmost kinds of nodes. Literals like "/" count as leaves. + + rules = _ rule* + rule = label equals expression + equals = "=" _ + literal = spaceless_literal _ + + # So you can't spell a regex like `~"..." ilm`: + spaceless_literal = ~"u?r?\"[^\"\\\\]*(?:\\\\.[^\"\\\\]*)*\""is / + ~"u?r?'[^'\\\\]*(?:\\\\.[^'\\\\]*)*'"is + + expression = ored / sequence / term + or_term = "/" _ term + ored = term or_term+ + sequence = term term+ + not_term = "!" term _ + lookahead_term = "&" term _ + term = not_term / lookahead_term / quantified / atom + quantified = atom quantifier + atom = reference / literal / regex / parenthesized + regex = "~" spaceless_literal ~"[ilmsuxa]*"i _ + parenthesized = "(" _ expression ")" _ + quantifier = ~"[*+?]" _ + reference = label !equals + + # A subsequent equal sign is the only thing that distinguishes a label + # (which begins a new rule) from a reference (which is just a pointer to a + # rule defined somewhere else): + label = ~"[a-zA-Z_][a-zA-Z_0-9]*" _ + + # _ = ~r"\s*(?:#[^\r\n]*)?\s*" + _ = meaninglessness* + meaninglessness = ~r"\s+" / comment + comment = ~r"#[^\r\n]*" + ''') + + +class LazyReference(str): + """A lazy reference to a rule, which we resolve after grokking all the + rules""" + + name = u'' + + # Just for debugging: + def _as_rhs(self): + return u'' % self + + +class RuleVisitor(NodeVisitor): + """Turns a parse tree of a grammar definition into a map of ``Expression`` + objects + + This is the magic piece that breathes life into a parsed bunch of parse + rules, allowing them to go forth and parse other things. + + """ + quantifier_classes = {'?': Optional, '*': ZeroOrMore, '+': OneOrMore} + + visit_expression = visit_term = visit_atom = NodeVisitor.lift_child + + def __init__(self, custom_rules=None): + """Construct. + + :arg custom_rules: A dict of {rule name: expression} holding custom + rules which will take precedence over the others + + """ + self.custom_rules = custom_rules or {} + + def visit_parenthesized(self, node, parenthesized): + """Treat a parenthesized subexpression as just its contents. + + Its position in the tree suffices to maintain its grouping semantics. + + """ + left_paren, _, expression, right_paren, _ = parenthesized + return expression + + def visit_quantifier(self, node, quantifier): + """Turn a quantifier into just its symbol-matching node.""" + symbol, _ = quantifier + return symbol + + def visit_quantified(self, node, quantified): + atom, quantifier = quantified + return self.quantifier_classes[quantifier.text](atom) + + def visit_lookahead_term(self, node, lookahead_term): + ampersand, term, _ = lookahead_term + return Lookahead(term) + + def visit_not_term(self, node, not_term): + exclamation, term, _ = not_term + return Not(term) + + def visit_rule(self, node, rule): + """Assign a name to the Expression and return it.""" + label, equals, expression = rule + expression.name = label # Assign a name to the expr. + return expression + + def visit_sequence(self, node, sequence): + """A parsed Sequence looks like [term node, OneOrMore node of + ``another_term``s]. Flatten it out.""" + term, other_terms = sequence + return Sequence(term, *other_terms) + + def visit_ored(self, node, ored): + first_term, other_terms = ored + return OneOf(first_term, *other_terms) + + def visit_or_term(self, node, or_term): + """Return just the term from an ``or_term``. + + We already know it's going to be ored, from the containing ``ored``. + + """ + slash, _, term = or_term + return term + + def visit_label(self, node, label): + """Turn a label into a unicode string.""" + name, _ = label + return name.text + + def visit_reference(self, node, reference): + """Stick a :class:`LazyReference` in the tree as a placeholder. + + We resolve them all later. + + """ + label, not_equals = reference + return LazyReference(label) + + def visit_regex(self, node, regex): + """Return a ``Regex`` expression.""" + tilde, literal, flags, _ = regex + flags = flags.text.upper() + pattern = literal.literal # Pull the string back out of the Literal + # object. + return Regex(pattern, ignore_case='I' in flags, + locale='L' in flags, + multiline='M' in flags, + dot_all='S' in flags, + unicode='U' in flags, + verbose='X' in flags, + ascii='A' in flags) + + def visit_spaceless_literal(self, spaceless_literal, visited_children): + """Turn a string literal into a ``Literal`` that recognizes it.""" + return Literal(evaluate_string(spaceless_literal.text)) + + def visit_literal(self, node, literal): + """Pick just the literal out of a literal-and-junk combo.""" + spaceless_literal, _ = literal + return spaceless_literal + + def generic_visit(self, node, visited_children): + """Replace childbearing nodes with a list of their children; keep + others untouched. + + For our case, if a node has children, only the children are important. + Otherwise, keep the node around for (for example) the flags of the + regex rule. Most of these kept-around nodes are subsequently thrown + away by the other visitor methods. + + We can't simply hang the visited children off the original node; that + would be disastrous if the node occurred in more than one place in the + tree. + + """ + return visited_children or node # should semantically be a tuple + + def _resolve_refs(self, rule_map, expr, done): + """Return an expression with all its lazy references recursively + resolved. + + Resolve any lazy references in the expression ``expr``, recursing into + all subexpressions. + + :arg done: The set of Expressions that have already been or are + currently being resolved, to ward off redundant work and prevent + infinite recursion for circular refs + + """ + if isinstance(expr, LazyReference): + label = str(expr) + try: + reffed_expr = rule_map[label] + except KeyError: + raise UndefinedLabel(expr) + return self._resolve_refs(rule_map, reffed_expr, done) + else: + if getattr(expr, 'members', ()) and expr not in done: + # Prevents infinite recursion for circular refs. At worst, one + # of `expr.members` can refer back to `expr`, but it can't go + # any farther. + done.add(expr) + expr.members = tuple(self._resolve_refs(rule_map, member, done) + for member in expr.members) + return expr + + def visit_rules(self, node, rules_list): + """Collate all the rules into a map. Return (map, default rule). + + The default rule is the first one. Or, if you have more than one rule + of that name, it's the last-occurring rule of that name. (This lets you + override the default rule when you extend a grammar.) If there are no + string-based rules, the default rule is None, because the custom rules, + due to being kwarg-based, are unordered. + + """ + _, rules = rules_list + + # Map each rule's name to its Expression. Later rules of the same name + # override earlier ones. This lets us define rules multiple times and + # have the last declaration win, so you can extend grammars by + # concatenation. + rule_map = OrderedDict((expr.name, expr) for expr in rules) + + # And custom rules override string-based rules. This is the least + # surprising choice when you compare the dict constructor: + # dict({'x': 5}, x=6). + rule_map.update(self.custom_rules) + + # Resolve references. This tolerates forward references. + done = set() + rule_map = OrderedDict((expr.name, self._resolve_refs(rule_map, expr, done)) + for expr in rule_map.values()) + + # isinstance() is a temporary hack around the fact that * rules don't + # always get transformed into lists by NodeVisitor. We should fix that; + # it's surprising and requires writing lame branches like this. + return rule_map, (rule_map[rules[0].name] + if isinstance(rules, list) and rules else None) + + +class TokenRuleVisitor(RuleVisitor): + """A visitor which builds expression trees meant to work on sequences of + pre-lexed tokens rather than strings""" + + def visit_spaceless_literal(self, spaceless_literal, visited_children): + """Turn a string literal into a ``TokenMatcher`` that matches + ``Token`` objects by their ``type`` attributes.""" + return TokenMatcher(evaluate_string(spaceless_literal.text)) + + def visit_regex(self, node, regex): + tilde, literal, flags, _ = regex + raise BadGrammar('Regexes do not make sense in TokenGrammars, since ' + 'TokenGrammars operate on pre-lexed tokens rather ' + 'than characters.') + + +# Bootstrap to level 1... +rule_grammar = BootstrappingGrammar(rule_syntax) +# ...and then to level 2. This establishes that the node tree of our rule +# syntax is built by the same machinery that will build trees of our users' +# grammars. And the correctness of that tree is tested, indirectly, in +# test_grammar. +rule_grammar = Grammar(rule_syntax) + + +# TODO: Teach Expression trees how to spit out Python representations of +# themselves. Then we can just paste that in above, and we won't have to +# bootstrap on import. Though it'll be a little less DRY. [Ah, but this is not +# so clean, because it would have to output multiple statements to get multiple +# refs to a single expression hooked up.] diff --git a/ccxt/static_dependencies/parsimonious/nodes.py b/ccxt/static_dependencies/parsimonious/nodes.py new file mode 100644 index 0000000..b3e9a29 --- /dev/null +++ b/ccxt/static_dependencies/parsimonious/nodes.py @@ -0,0 +1,325 @@ +"""Nodes that make up parse trees + +Parsing spits out a tree of these, which you can then tell to walk itself and +spit out a useful value. Or you can walk it yourself; the structural attributes +are public. + +""" +# TODO: If this is slow, think about using cElementTree or something. +from inspect import isfunction +from sys import version_info, exc_info + +from .exceptions import VisitationError, UndefinedLabel + + +class Node(object): + """A parse tree node + + Consider these immutable once constructed. As a side effect of a + memory-saving strategy in the cache, multiple references to a single + ``Node`` might be returned in a single parse tree. So, if you start + messing with one, you'll see surprising parallel changes pop up elsewhere. + + My philosophy is that parse trees (and their nodes) should be + representation-agnostic. That is, they shouldn't get all mixed up with what + the final rendered form of a wiki page (or the intermediate representation + of a programming language, or whatever) is going to be: you should be able + to parse once and render several representations from the tree, one after + another. + + """ + # I tried making this subclass list, but it got ugly. I had to construct + # invalid ones and patch them up later, and there were other problems. + __slots__ = ['expr', # The expression that generated me + 'full_text', # The full text fed to the parser + 'start', # The position in the text where that expr started matching + 'end', # The position after start where the expr first didn't + # match. [start:end] follow Python slice conventions. + 'children'] # List of child parse tree nodes + + def __init__(self, expr, full_text, start, end, children=None): + self.expr = expr + self.full_text = full_text + self.start = start + self.end = end + self.children = children or [] + + @property + def expr_name(self): + # backwards compatibility + return self.expr.name + + def __iter__(self): + """Support looping over my children and doing tuple unpacks on me. + + It can be very handy to unpack nodes in arg lists; see + :class:`PegVisitor` for an example. + + """ + return iter(self.children) + + @property + def text(self): + """Return the text this node matched.""" + return self.full_text[self.start:self.end] + + # From here down is just stuff for testing and debugging. + + def prettily(self, error=None): + """Return a unicode, pretty-printed representation of me. + + :arg error: The node to highlight because an error occurred there + + """ + # TODO: If a Node appears multiple times in the tree, we'll point to + # them all. Whoops. + def indent(text): + return '\n'.join((' ' + line) for line in text.splitlines()) + ret = [u'<%s%s matching "%s">%s' % ( + self.__class__.__name__, + (' called "%s"' % self.expr_name) if self.expr_name else '', + self.text, + ' <-- *** We were here. ***' if error is self else '')] + for n in self: + ret.append(indent(n.prettily(error=error))) + return '\n'.join(ret) + + def __str__(self): + """Return a compact, human-readable representation of me.""" + return self.prettily() + + def __eq__(self, other): + """Support by-value deep comparison with other nodes for testing.""" + if not isinstance(other, Node): + return NotImplemented + + return (self.expr == other.expr and + self.full_text == other.full_text and + self.start == other.start and + self.end == other.end and + self.children == other.children) + + def __ne__(self, other): + return not self == other + + def __repr__(self, top_level=True): + """Return a bit of code (though not an expression) that will recreate + me.""" + # repr() of unicode flattens everything out to ASCII, so we don't need + # to explicitly encode things afterward. + ret = ["s = %r" % self.full_text] if top_level else [] + ret.append("%s(%r, s, %s, %s%s)" % ( + self.__class__.__name__, + self.expr, + self.start, + self.end, + (', children=[%s]' % + ', '.join([c.__repr__(top_level=False) for c in self.children])) + if self.children else '')) + return '\n'.join(ret) + + +class RegexNode(Node): + """Node returned from a ``Regex`` expression + + Grants access to the ``re.Match`` object, in case you want to access + capturing groups, etc. + + """ + __slots__ = ['match'] + + +class RuleDecoratorMeta(type): + def __new__(metaclass, name, bases, namespace): + def unvisit(name): + """Remove any leading "visit_" from a method name.""" + return name[6:] if name.startswith('visit_') else name + + methods = [v for k, v in namespace.items() if + hasattr(v, '_rule') and isfunction(v)] + if methods: + from .grammar import Grammar # circular import dodge + + methods.sort(key=(lambda x: x.func_code.co_firstlineno) + if version_info[0] < 3 else + (lambda x: x.__code__.co_firstlineno)) + # Possible enhancement: once we get the Grammar extensibility story + # solidified, we can have @rules *add* to the default grammar + # rather than pave over it. + namespace['grammar'] = Grammar( + '\n'.join('{name} = {expr}'.format(name=unvisit(m.__name__), + expr=m._rule) + for m in methods)) + return super(RuleDecoratorMeta, + metaclass).__new__(metaclass, name, bases, namespace) + + +class NodeVisitor(object, metaclass=RuleDecoratorMeta): + """A shell for writing things that turn parse trees into something useful + + Performs a depth-first traversal of an AST. Subclass this, add methods for + each expr you care about, instantiate, and call + ``visit(top_node_of_parse_tree)``. It'll return the useful stuff. This API + is very similar to that of ``ast.NodeVisitor``. + + These could easily all be static methods, but that would add at least as + much weirdness at the call site as the ``()`` for instantiation. And this + way, we support subclasses that require state: options, for example, or a + symbol table constructed from a programming language's AST. + + We never transform the parse tree in place, because... + + * There are likely multiple references to the same ``Node`` object in a + parse tree, and changes to one reference would surprise you elsewhere. + * It makes it impossible to report errors: you'd end up with the "error" + arrow pointing someplace in a half-transformed mishmash of nodes--and + that's assuming you're even transforming the tree into another tree. + Heaven forbid you're making it into a string or something else. + + """ + + #: The :term:`default grammar`: the one recommended for use with this + #: visitor. If you populate this, you will be able to call + #: :meth:`NodeVisitor.parse()` as a shortcut. + grammar = None + + #: Classes of exceptions you actually intend to raise during visitation + #: and which should propagate out of the visitor. These will not be + #: wrapped in a VisitationError when they arise. + unwrapped_exceptions = () + + # TODO: If we need to optimize this, we can go back to putting subclasses + # in charge of visiting children; they know when not to bother. Or we can + # mark nodes as not descent-worthy in the grammar. + def visit(self, node): + """Walk a parse tree, transforming it into another representation. + + Recursively descend a parse tree, dispatching to the method named after + the rule in the :class:`~.grammar.Grammar` that produced + each node. If, for example, a rule was... :: + + bold = '' + + ...the ``visit_bold()`` method would be called. It is your + responsibility to subclass :class:`NodeVisitor` and implement those + methods. + + """ + method = getattr(self, 'visit_' + node.expr_name, self.generic_visit) + + # Call that method, and show where in the tree it failed if it blows + # up. + try: + return method(node, [self.visit(n) for n in node]) + except (VisitationError, UndefinedLabel): + # Don't catch and re-wrap already-wrapped exceptions. + raise + except Exception as exc: + # implentors may define exception classes that should not be + # wrapped. + if isinstance(exc, self.unwrapped_exceptions): + raise + # Catch any exception, and tack on a parse tree so it's easier to + # see where it went wrong. + exc_class = type(exc) + raise VisitationError(exc, exc_class, node) + + def generic_visit(self, node, visited_children): + """Default visitor method + + :arg node: The node we're visiting + :arg visited_children: The results of visiting the children of that + node, in a list + + I'm not sure there's an implementation of this that makes sense across + all (or even most) use cases, so we leave it to subclasses to implement + for now. + + """ + raise NotImplementedError('No visitor method was defined for this expression: %s' % + node.expr.as_rule()) + + # Convenience methods: + + def parse(self, text, pos=0): + """Parse some text with this Visitor's default grammar and return the + result of visiting it. + + ``SomeVisitor().parse('some_string')`` is a shortcut for + ``SomeVisitor().visit(some_grammar.parse('some_string'))``. + + """ + return self._parse_or_match(text, pos, 'parse') + + def match(self, text, pos=0): + """Parse and visit some text with this Visitor's default grammar, but + don't insist on parsing all the way to the end. + + ``SomeVisitor().match('some_string')`` is a shortcut for + ``SomeVisitor().visit(some_grammar.match('some_string'))``. + + """ + return self._parse_or_match(text, pos, 'match') + + # Internal convenience methods to help you write your own visitors: + + def lift_child(self, node, children): + """Lift the sole child of ``node`` up to replace the node.""" + first_child, = children + return first_child + + # Private methods: + + def _parse_or_match(self, text, pos, method_name): + """Execute a parse or match on the default grammar, followed by a + visitation. + + Raise RuntimeError if there is no default grammar specified. + + """ + if not self.grammar: + raise RuntimeError( + "The {cls}.{method}() shortcut won't work because {cls} was " + "never associated with a specific " "grammar. Fill out its " + "`grammar` attribute, and try again.".format( + cls=self.__class__.__name__, + method=method_name)) + return self.visit(getattr(self.grammar, method_name)(text, pos=pos)) + + +def rule(rule_string): + """Decorate a NodeVisitor ``visit_*`` method to tie a grammar rule to it. + + The following will arrange for the ``visit_digit`` method to receive the + results of the ``~"[0-9]"`` parse rule:: + + @rule('~"[0-9]"') + def visit_digit(self, node, visited_children): + ... + + Notice that there is no "digit = " as part of the rule; that gets inferred + from the method name. + + In cases where there is only one kind of visitor interested in a grammar, + using ``@rule`` saves you having to look back and forth between the visitor + and the grammar definition. + + On an implementation level, all ``@rule`` rules get stitched together into + a :class:`~.Grammar` that becomes the NodeVisitor's + :term:`default grammar`. + + Typically, the choice of a default rule for this grammar is simple: whatever + ``@rule`` comes first in the class is the default. But the choice may become + surprising if you divide the ``@rule`` calls among subclasses. At the + moment, which method "comes first" is decided simply by comparing line + numbers, so whatever method is on the smallest-numbered line will be the + default. In a future release, this will change to pick the + first ``@rule`` call on the basemost class that has one. That way, a + subclass which does not override the default rule's ``visit_*`` method + won't unintentionally change which rule is the default. + + """ + def decorator(method): + method._rule = rule_string # XXX: Maybe register them on a class var instead so we can just override a @rule'd visitor method on a subclass without blowing away the rule string that comes with it. + return method + return decorator diff --git a/ccxt/static_dependencies/parsimonious/utils.py b/ccxt/static_dependencies/parsimonious/utils.py new file mode 100644 index 0000000..3dc27cc --- /dev/null +++ b/ccxt/static_dependencies/parsimonious/utils.py @@ -0,0 +1,40 @@ +"""General tools which don't depend on other parts of Parsimonious""" + +import ast + + +class StrAndRepr(object): + """Mix-in which gives the class the same __repr__ and __str__.""" + + def __repr__(self): + return self.__str__() + + +def evaluate_string(string): + """Piggyback on Python's string support so we can have backslash escaping + and niceties like \n, \t, etc. string.decode('string_escape') would have + been a lower-level possibility. + + """ + return ast.literal_eval(string) + + +class Token(StrAndRepr): + """A class to represent tokens, for use with TokenGrammars + + You will likely want to subclass this to hold additional information, like + the characters that you lexed to create this token. Alternately, feel free + to create your own class from scratch. The only contract is that tokens + must have a ``type`` attr. + + """ + __slots__ = ['type'] + + def __init__(self, type): + self.type = type + + def __str__(self): + return u'' % (self.type,) + + def __eq__(self, other): + return self.type == other.type diff --git a/ccxt/static_dependencies/starknet/__init__.py b/ccxt/static_dependencies/starknet/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ccxt/static_dependencies/starknet/__pycache__/__init__.cpython-311.pyc b/ccxt/static_dependencies/starknet/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..94ab828 Binary files /dev/null and b/ccxt/static_dependencies/starknet/__pycache__/__init__.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/starknet/__pycache__/ccxt_utils.cpython-311.pyc b/ccxt/static_dependencies/starknet/__pycache__/ccxt_utils.cpython-311.pyc new file mode 100644 index 0000000..5b2662b Binary files /dev/null and b/ccxt/static_dependencies/starknet/__pycache__/ccxt_utils.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/starknet/__pycache__/common.cpython-311.pyc b/ccxt/static_dependencies/starknet/__pycache__/common.cpython-311.pyc new file mode 100644 index 0000000..825c5f9 Binary files /dev/null and b/ccxt/static_dependencies/starknet/__pycache__/common.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/starknet/__pycache__/constants.cpython-311.pyc b/ccxt/static_dependencies/starknet/__pycache__/constants.cpython-311.pyc new file mode 100644 index 0000000..8ed702e Binary files /dev/null and b/ccxt/static_dependencies/starknet/__pycache__/constants.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/starknet/abi/v0/__init__.py b/ccxt/static_dependencies/starknet/abi/v0/__init__.py new file mode 100644 index 0000000..16b9092 --- /dev/null +++ b/ccxt/static_dependencies/starknet/abi/v0/__init__.py @@ -0,0 +1,2 @@ +from .model import Abi +from .parser import AbiParser, AbiParsingError diff --git a/ccxt/static_dependencies/starknet/abi/v0/model.py b/ccxt/static_dependencies/starknet/abi/v0/model.py new file mode 100644 index 0000000..657adb4 --- /dev/null +++ b/ccxt/static_dependencies/starknet/abi/v0/model.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Dict, Optional, OrderedDict + +from ...cairo.data_types import CairoType, StructType + + +@dataclass +class Abi: + """ + Dataclass representing class abi. Contains parsed functions, events and structures. + """ + + @dataclass + class Function: + """ + Dataclass representing function's abi. + """ + + name: str + inputs: OrderedDict[str, CairoType] + outputs: OrderedDict[str, CairoType] + + @dataclass + class Event: + """ + Dataclass representing event's abi. + """ + + name: str + data: OrderedDict[str, CairoType] + + defined_structures: Dict[ + str, StructType + ] #: Abi of structures defined by the class. + functions: Dict[str, Function] #: Functions defined by the class. + constructor: Optional[ + Function + ] #: Contract's constructor. It is None if class doesn't define one. + l1_handler: Optional[ + Function + ] #: Handler of L1 messages. It is None if class doesn't define one. + events: Dict[str, Event] #: Events defined by the class diff --git a/ccxt/static_dependencies/starknet/abi/v0/parser.py b/ccxt/static_dependencies/starknet/abi/v0/parser.py new file mode 100644 index 0000000..b8cd762 --- /dev/null +++ b/ccxt/static_dependencies/starknet/abi/v0/parser.py @@ -0,0 +1,216 @@ +from __future__ import annotations + +import dataclasses +import json +from collections import OrderedDict, defaultdict +from typing import DefaultDict, Dict, List, Optional, cast + +from ....marshmallow import EXCLUDE + +from .model import Abi +from .schemas import ContractAbiEntrySchema +from .shape import ( + CONSTRUCTOR_ENTRY, + EVENT_ENTRY, + FUNCTION_ENTRY, + L1_HANDLER_ENTRY, + STRUCT_ENTRY, + EventDict, + FunctionDict, + StructMemberDict, + TypedMemberDict, +) +from ...cairo.data_types import CairoType, StructType +from ...cairo.type_parser import TypeParser + + +class AbiParsingError(ValueError): + """ + Error raised when something wrong goes during abi parsing. + """ + + +class AbiParser: + """ + Utility class for parsing abi into a dataclass. + """ + + # Entries from ABI grouped by entry type + _grouped: DefaultDict[str, List[Dict]] + # lazy init property + _type_parser: Optional[TypeParser] = None + + def __init__(self, abi_list: List[Dict]): + """ + Abi parser constructor. Ensures that abi satisfies the abi schema. + + :param abi_list: Contract's ABI as a list of dictionaries. + """ + abi = [ + ContractAbiEntrySchema().load(entry, unknown=EXCLUDE) for entry in abi_list + ] + grouped = defaultdict(list) + for entry in abi: + assert isinstance(entry, dict) + grouped[entry["type"]].append(entry) + + self._grouped = grouped + + def parse(self) -> Abi: + """ + Parse abi provided to constructor and return it as a dataclass. Ensures that there are no cycles in the abi. + + :raises: AbiParsingError: on any parsing error. + :return: Abi dataclass. + """ + structures = self._parse_structures() + functions_dict = cast( + Dict[str, FunctionDict], + AbiParser._group_by_entry_name( + self._grouped[FUNCTION_ENTRY], "defined functions" + ), + ) + events_dict = cast( + Dict[str, EventDict], + AbiParser._group_by_entry_name( + self._grouped[EVENT_ENTRY], "defined events" + ), + ) + constructors = cast(List[FunctionDict], self._grouped[CONSTRUCTOR_ENTRY]) + l1_handlers = cast(List[FunctionDict], self._grouped[L1_HANDLER_ENTRY]) + + if len(l1_handlers) > 1: + raise AbiParsingError("L1 handler in ABI must be defined at most once.") + + if len(constructors) > 1: + raise AbiParsingError("Constructor in ABI must be defined at most once.") + + return Abi( + defined_structures=structures, + constructor=( + self._parse_function(constructors[0]) if constructors else None + ), + l1_handler=(self._parse_function(l1_handlers[0]) if l1_handlers else None), + functions={ + name: self._parse_function(entry) + for name, entry in functions_dict.items() + }, + events={ + name: self._parse_event(entry) for name, entry in events_dict.items() + }, + ) + + @property + def type_parser(self) -> TypeParser: + if self._type_parser: + return self._type_parser + + raise RuntimeError("Tried to get type_parser before it was set.") + + def _parse_structures(self) -> Dict[str, StructType]: + structs_dict = AbiParser._group_by_entry_name( + self._grouped[STRUCT_ENTRY], "defined structures" + ) + + # Contains sorted members of the struct + struct_members: Dict[str, List[StructMemberDict]] = {} + structs: Dict[str, StructType] = {} + + # Example problem (with a simplified json structure): + # [{name: User, fields: {id: Uint256}}, {name: "Uint256", ...}] + # User refers to Uint256 even though it is not known yet (will be parsed next). + # This is why it is important to create the structure types first. This way other types can already refer to + # them when parsing types, even thought their fields are not filled yet. + # At the end we will mutate those structures to contain the right fields. An alternative would be to use + # topological sorting with an additional "unresolved type", so this flow is much easier. + for name, struct in structs_dict.items(): + structs[name] = StructType(name, OrderedDict()) + without_offset = [ + member for member in struct["members"] if member.get("offset") is None + ] + with_offset = [ + member for member in struct["members"] if member not in without_offset + ] + struct_members[name] = sorted( + with_offset, key=lambda member: member["offset"] # pyright: ignore + ) + for member in without_offset: + member["offset"] = ( + struct_members[name][-1].get("offset", 0) + 1 + if struct_members[name] + else 0 + ) + struct_members[name].append(member) + + # Now parse the types of members and save them. + self._type_parser = TypeParser(structs) + for name, struct in structs.items(): + members = self._parse_members( + cast(List[TypedMemberDict], struct_members[name]), + f"members of structure '{name}'", + ) + struct.types.update(members) + + # All types have their members assigned now + + self._check_for_cycles(structs) + + return structs + + @staticmethod + def _check_for_cycles(structs: Dict[str, StructType]): + # We want to avoid creating our own cycle checker as it would make it more complex. json module has a built-in + # checker for cycles. + try: + _to_json(structs) + except ValueError as err: + raise AbiParsingError(err) from ValueError + + def _parse_function(self, function: FunctionDict) -> Abi.Function: + return Abi.Function( + name=function["name"], + inputs=self._parse_members(function["inputs"], function["name"]), + outputs=self._parse_members(function["outputs"], function["name"]), + ) + + def _parse_event(self, event: EventDict) -> Abi.Event: + return Abi.Event( + name=event["name"], + data=self._parse_members(event["data"], event["name"]), + ) + + def _parse_members( + self, params: List[TypedMemberDict], entity_name: str + ) -> OrderedDict[str, CairoType]: + # Without cast, it complains that 'Type "TypedMemberDict" cannot be assigned to type "T@_group_by_name"' + members = AbiParser._group_by_entry_name(cast(List[Dict], params), entity_name) + return OrderedDict( + (name, self.type_parser.parse_inline_type(param["type"])) + for name, param in members.items() + ) + + @staticmethod + def _group_by_entry_name( + dicts: List[Dict], entity_name: str + ) -> OrderedDict[str, Dict]: + grouped = OrderedDict() + for entry in dicts: + name = entry["name"] + if name in grouped: + raise AbiParsingError( + f"Name '{name}' was used more than once in {entity_name}." + ) + grouped[name] = entry + return grouped + + +def _to_json(value): + class DataclassSupportingEncoder(json.JSONEncoder): + def default(self, o): + # Dataclasses are not supported by json. Additionally, dataclasses.asdict() works recursively and doesn't + # check for cycles, so we need to flatten dataclasses (by ONE LEVEL) ourselves. + if dataclasses.is_dataclass(o): + return tuple(getattr(o, field.name) for field in dataclasses.fields(o)) + return super().default(o) + + return json.dumps(value, cls=DataclassSupportingEncoder) diff --git a/ccxt/static_dependencies/starknet/abi/v0/schemas.py b/ccxt/static_dependencies/starknet/abi/v0/schemas.py new file mode 100644 index 0000000..acf1d70 --- /dev/null +++ b/ccxt/static_dependencies/starknet/abi/v0/schemas.py @@ -0,0 +1,72 @@ +from ....marshmallow import Schema, fields +from ....marshmallow_oneofschema import OneOfSchema + +from .shape import ( + CONSTRUCTOR_ENTRY, + EVENT_ENTRY, + FUNCTION_ENTRY, + L1_HANDLER_ENTRY, + STRUCT_ENTRY, +) + + +class TypedParameterSchema(Schema): + name = fields.String(data_key="name", required=True) + type = fields.String(data_key="type", required=True) + + +class StructMemberSchema(TypedParameterSchema): + offset = fields.Integer(data_key="offset", required=False) + + +class FunctionBaseSchema(Schema): + name = fields.String(data_key="name", required=True) + inputs = fields.List( + fields.Nested(TypedParameterSchema()), data_key="inputs", required=True + ) + outputs = fields.List( + fields.Nested(TypedParameterSchema()), data_key="outputs", required=True + ) + + +class FunctionAbiEntrySchema(FunctionBaseSchema): + type = fields.Constant(FUNCTION_ENTRY, data_key="type", required=True) + + +class ConstructorAbiEntrySchema(FunctionBaseSchema): + type = fields.Constant(CONSTRUCTOR_ENTRY, data_key="type", required=True) + + +class L1HandlerAbiEntrySchema(FunctionBaseSchema): + type = fields.Constant(L1_HANDLER_ENTRY, data_key="type", required=True) + + +class EventAbiEntrySchema(Schema): + type = fields.Constant(EVENT_ENTRY, data_key="type", required=True) + name = fields.String(data_key="name", required=True) + keys = fields.List( + fields.Nested(TypedParameterSchema()), data_key="keys", required=True + ) + data = fields.List( + fields.Nested(TypedParameterSchema()), data_key="data", required=True + ) + + +class StructAbiEntrySchema(Schema): + type = fields.Constant(STRUCT_ENTRY, data_key="type", required=True) + name = fields.String(data_key="name", required=True) + size = fields.Integer(data_key="size", required=True) + members = fields.List( + fields.Nested(StructMemberSchema()), data_key="members", required=True + ) + + +class ContractAbiEntrySchema(OneOfSchema): + type_field_remove = False + type_schemas = { + FUNCTION_ENTRY: FunctionAbiEntrySchema, + L1_HANDLER_ENTRY: L1HandlerAbiEntrySchema, + CONSTRUCTOR_ENTRY: ConstructorAbiEntrySchema, + EVENT_ENTRY: EventAbiEntrySchema, + STRUCT_ENTRY: StructAbiEntrySchema, + } diff --git a/ccxt/static_dependencies/starknet/abi/v0/shape.py b/ccxt/static_dependencies/starknet/abi/v0/shape.py new file mode 100644 index 0000000..deb5e7b --- /dev/null +++ b/ccxt/static_dependencies/starknet/abi/v0/shape.py @@ -0,0 +1,63 @@ +# TODO (#1260): update pylint to 3.1.0 and remove pylint disable +# pylint: disable=too-many-ancestors +import sys +from typing import List, Literal, Union + +if sys.version_info < (3, 11): + from typing_extensions import NotRequired, TypedDict +else: + from typing import NotRequired, TypedDict + +STRUCT_ENTRY = "struct" +FUNCTION_ENTRY = "function" +CONSTRUCTOR_ENTRY = "constructor" +L1_HANDLER_ENTRY = "l1_handler" +EVENT_ENTRY = "event" + + +class TypedMemberDict(TypedDict): + name: str + type: str + + +class StructMemberDict(TypedMemberDict): + offset: NotRequired[int] + + +class StructDict(TypedDict): + type: Literal["struct"] + name: str + size: int + members: List[StructMemberDict] + + +class FunctionBaseDict(TypedDict): + name: str + inputs: List[TypedMemberDict] + outputs: List[TypedMemberDict] + stateMutability: NotRequired[Literal["view"]] + + +class FunctionDict(FunctionBaseDict): + type: Literal["function"] + + +class ConstructorDict(FunctionBaseDict): + type: Literal["constructor"] + + +class L1HandlerDict(FunctionBaseDict): + type: Literal["l1_handler"] + + +class EventDict(TypedDict): + name: str + type: Literal["event"] + data: List[TypedMemberDict] + keys: List[TypedMemberDict] + + +AbiDictEntry = Union[ + StructDict, FunctionDict, ConstructorDict, L1HandlerDict, EventDict +] +AbiDictList = List[AbiDictEntry] diff --git a/ccxt/static_dependencies/starknet/abi/v1/__init__.py b/ccxt/static_dependencies/starknet/abi/v1/__init__.py new file mode 100644 index 0000000..16b9092 --- /dev/null +++ b/ccxt/static_dependencies/starknet/abi/v1/__init__.py @@ -0,0 +1,2 @@ +from .model import Abi +from .parser import AbiParser, AbiParsingError diff --git a/ccxt/static_dependencies/starknet/abi/v1/core_structures.json b/ccxt/static_dependencies/starknet/abi/v1/core_structures.json new file mode 100644 index 0000000..0448bc2 --- /dev/null +++ b/ccxt/static_dependencies/starknet/abi/v1/core_structures.json @@ -0,0 +1,14 @@ +{ + "abi": [ + { + "type": "struct", + "name": "core::starknet::eth_address::EthAddress", + "members": [ + { + "name": "address", + "type": "core::felt252" + } + ] + } + ] +} diff --git a/ccxt/static_dependencies/starknet/abi/v1/model.py b/ccxt/static_dependencies/starknet/abi/v1/model.py new file mode 100644 index 0000000..e900ede --- /dev/null +++ b/ccxt/static_dependencies/starknet/abi/v1/model.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Dict, List, OrderedDict + +from ...cairo.data_types import CairoType, EnumType, StructType + + +@dataclass +class Abi: + """ + Dataclass representing class abi. Contains parsed functions, enums, events and structures. + """ + + @dataclass + class Function: + """ + Dataclass representing function's abi. + """ + + name: str + inputs: OrderedDict[str, CairoType] + outputs: List[CairoType] + + @dataclass + class Event: + """ + Dataclass representing event's abi. + """ + + name: str + inputs: OrderedDict[str, CairoType] + + defined_structures: Dict[ + str, StructType + ] #: Abi of structures defined by the class. + defined_enums: Dict[str, EnumType] #: Abi of enums defined by the class. + functions: Dict[str, Function] #: Functions defined by the class. + events: Dict[str, Event] #: Events defined by the class diff --git a/ccxt/static_dependencies/starknet/abi/v1/parser.py b/ccxt/static_dependencies/starknet/abi/v1/parser.py new file mode 100644 index 0000000..da6945a --- /dev/null +++ b/ccxt/static_dependencies/starknet/abi/v1/parser.py @@ -0,0 +1,220 @@ +from __future__ import annotations + +import dataclasses +import json +import os +from collections import OrderedDict, defaultdict +from pathlib import Path +from typing import DefaultDict, Dict, List, Optional, Tuple, Union, cast + +from ....marshmallow import EXCLUDE + +from .model import Abi +from .schemas import ContractAbiEntrySchema +from .shape import ( + ENUM_ENTRY, + EVENT_ENTRY, + FUNCTION_ENTRY, + STRUCT_ENTRY, + EventDict, + FunctionDict, + TypedParameterDict, +) +from ...cairo.data_types import CairoType, EnumType, StructType +from ...cairo.v1.type_parser import TypeParser + + +class AbiParsingError(ValueError): + """ + Error raised when something wrong goes during abi parsing. + """ + + +class AbiParser: + """ + Utility class for parsing abi into a dataclass. + """ + + # Entries from ABI grouped by entry type + _grouped: DefaultDict[str, List[Dict]] + # lazy init property + _type_parser: Optional[TypeParser] = None + + def __init__(self, abi_list: List[Dict]): + """ + Abi parser constructor. Ensures that abi satisfies the abi schema. + + :param abi_list: Contract's ABI as a list of dictionaries. + """ + # prepend abi with core structures + core_structures = ( + Path(os.path.dirname(__file__)) / "core_structures.json" + ).read_text("utf-8") + abi_list = json.loads(core_structures)["abi"] + abi_list + abi = [ + ContractAbiEntrySchema().load(entry, unknown=EXCLUDE) for entry in abi_list + ] + grouped = defaultdict(list) + for entry in abi: + assert isinstance(entry, dict) + grouped[entry["type"]].append(entry) + + self._grouped = grouped + + def parse(self) -> Abi: + """ + Parse abi provided to constructor and return it as a dataclass. Ensures that there are no cycles in the abi. + + :raises: AbiParsingError: on any parsing error. + :return: Abi dataclass. + """ + structures, enums = self._parse_structures_and_enums() + functions_dict = cast( + Dict[str, FunctionDict], + AbiParser._group_by_entry_name( + self._grouped[FUNCTION_ENTRY], "defined functions" + ), + ) + events_dict = cast( + Dict[str, EventDict], + AbiParser._group_by_entry_name( + self._grouped[EVENT_ENTRY], "defined events" + ), + ) + + return Abi( + defined_structures=structures, + defined_enums=enums, + functions={ + name: self._parse_function(entry) + for name, entry in functions_dict.items() + }, + events={ + name: self._parse_event(entry) for name, entry in events_dict.items() + }, + ) + + @property + def type_parser(self) -> TypeParser: + if self._type_parser: + return self._type_parser + + raise RuntimeError("Tried to get type_parser before it was set.") + + def _parse_structures_and_enums( + self, + ) -> Tuple[Dict[str, StructType], Dict[str, EnumType]]: + structs_dict = AbiParser._group_by_entry_name( + self._grouped[STRUCT_ENTRY], "defined structures" + ) + enums_dict = AbiParser._group_by_entry_name( + self._grouped[ENUM_ENTRY], "defined enums" + ) + + # Contains sorted members of the struct + struct_members: Dict[str, List[TypedParameterDict]] = {} + structs: Dict[str, StructType] = {} + + # Contains sorted members of the enum + enum_members: Dict[str, List[TypedParameterDict]] = {} + enums: Dict[str, EnumType] = {} + + # Example problem (with a simplified json structure): + # [{name: User, fields: {id: Uint256}}, {name: "Uint256", ...}] + # User refers to Uint256 even though it is not known yet (will be parsed next). + # This is why it is important to create the structure types first. This way other types can already refer to + # them when parsing types, even thought their fields are not filled yet. + # At the end we will mutate those structures to contain the right fields. An alternative would be to use + # topological sorting with an additional "unresolved type", so this flow is much easier. + for name, struct in structs_dict.items(): + structs[name] = StructType(name, OrderedDict()) + struct_members[name] = struct["members"] + + for name, enum in enums_dict.items(): + enums[name] = EnumType(name, OrderedDict()) + enum_members[name] = enum["variants"] + + # Now parse the types of members and save them. + defined_structs_enums: Dict[str, Union[StructType, EnumType]] = dict(structs) + defined_structs_enums.update(enums) + + self._type_parser = TypeParser(defined_structs_enums) + for name, struct in structs.items(): + members = self._parse_members( + cast(List[TypedParameterDict], struct_members[name]), + f"members of structure '{name}'", + ) + struct.types.update(members) + for name, enum in enums.items(): + members = self._parse_members( + cast(List[TypedParameterDict], enum_members[name]), + f"members of enum '{name}'", + ) + enum.variants.update(members) + + # All types have their members assigned now + + self._check_for_cycles(defined_structs_enums) + + return structs, enums + + @staticmethod + def _check_for_cycles(structs: Dict[str, Union[StructType, EnumType]]): + # We want to avoid creating our own cycle checker as it would make it more complex. json module has a built-in + # checker for cycles. + try: + _to_json(structs) + except ValueError as err: + raise AbiParsingError(err) from ValueError + + def _parse_function(self, function: FunctionDict) -> Abi.Function: + return Abi.Function( + name=function["name"], + inputs=self._parse_members(function["inputs"], function["name"]), + outputs=list( + self.type_parser.parse_inline_type(param["type"]) + for param in function["outputs"] + ), + ) + + def _parse_event(self, event: EventDict) -> Abi.Event: + return Abi.Event( + name=event["name"], + inputs=self._parse_members(event["inputs"], event["name"]), + ) + + def _parse_members( + self, params: List[TypedParameterDict], entity_name: str + ) -> OrderedDict[str, CairoType]: + # Without cast, it complains that 'Type "TypedParameterDict" cannot be assigned to type "T@_group_by_name"' + members = AbiParser._group_by_entry_name(cast(List[Dict], params), entity_name) + return OrderedDict( + (name, self.type_parser.parse_inline_type(param["type"])) + for name, param in members.items() + ) + + @staticmethod + def _group_by_entry_name( + dicts: List[Dict], entity_name: str + ) -> OrderedDict[str, Dict]: + grouped = OrderedDict() + for entry in dicts: + name = entry["name"] + if name in grouped: + raise AbiParsingError( + f"Name '{name}' was used more than once in {entity_name}." + ) + grouped[name] = entry + return grouped + + +def _to_json(value): + class DataclassSupportingEncoder(json.JSONEncoder): + def default(self, o): + # Dataclasses are not supported by json. Additionally, dataclasses.asdict() works recursively and doesn't + # check for cycles, so we need to flatten dataclasses (by ONE LEVEL) ourselves. + if dataclasses.is_dataclass(o): + return tuple(getattr(o, field.name) for field in dataclasses.fields(o)) + return super().default(o) + + return json.dumps(value, cls=DataclassSupportingEncoder) diff --git a/ccxt/static_dependencies/starknet/abi/v1/parser_transformer.py b/ccxt/static_dependencies/starknet/abi/v1/parser_transformer.py new file mode 100644 index 0000000..89d453d --- /dev/null +++ b/ccxt/static_dependencies/starknet/abi/v1/parser_transformer.py @@ -0,0 +1,179 @@ +from typing import Any, List, Optional + +from ....lark import * +from ....lark import Token, Transformer + +from ...cairo.data_types import ( + ArrayType, + BoolType, + CairoType, + FeltType, + OptionType, + TupleType, + TypeIdentifier, + UintType, + UnitType, +) + +ABI_EBNF = """ + IDENTIFIER: /[a-zA-Z_][a-zA-Z_0-9]*/ + + type: type_unit + | type_bool + | type_felt + | type_uint + | type_contract_address + | type_class_hash + | type_storage_address + | type_option + | type_array + | type_span + | tuple + | type_identifier + + + type_unit: "()" + type_felt: "core::felt252" + type_bool: "core::bool" + type_uint: "core::integer::u" INT + type_contract_address: "core::starknet::contract_address::ContractAddress" + type_class_hash: "core::starknet::class_hash::ClassHash" + type_storage_address: "core::starknet::storage_access::StorageAddress" + type_option: "core::option::Option::<" (type | type_identifier) ">" + type_array: "core::array::Array::<" (type | type_identifier) ">" + type_span: "core::array::Span::<" (type | type_identifier) ">" + + tuple: "(" type? ("," type?)* ")" + + type_identifier: (IDENTIFIER | "::")+ ("<" (type | ",")+ ">")? + + + %import common.INT + %import common.WS + %ignore WS +""" + + +class ParserTransformer(Transformer): + """ + Transforms the lark tree into CairoTypes. + """ + + def __init__(self, type_identifiers: Optional[dict] = None) -> None: + if type_identifiers is None: + type_identifiers = {} + self.type_identifiers = type_identifiers + super(Transformer, self).__init__() + + # pylint: disable=no-self-use + + def __default__(self, data: str, children, meta): + raise TypeError(f"Unable to parse tree node of type {data}.") + + def type(self, value: List[Optional[CairoType]]) -> Optional[CairoType]: + """ + Tokens are read bottom-up, so here all of them are parsed and should be just returned. + `Optional` is added in case of the unit type. + """ + assert len(value) == 1 + return value[0] + + def type_felt(self, _value: List[Any]) -> FeltType: + """ + Felt does not contain any additional arguments, so `_value` is just an empty list. + """ + return FeltType() + + def type_bool(self, _value: List[Any]) -> BoolType: + """ + Bool does not contain any additional arguments, so `_value` is just an empty list. + """ + return BoolType() + + def type_uint(self, value: List[Token]) -> UintType: + """ + Uint type contains information about its size. It is present in the value[0]. + """ + return UintType(int(value[0])) + + def type_unit(self, _value: List[Any]) -> UnitType: + """ + `()` type. + """ + return UnitType() + + def type_option(self, value: List[CairoType]) -> OptionType: + """ + Option includes an information about which type it eventually represents. + `Optional` is added in case of the unit type. + """ + return OptionType(value[0]) + + def type_array(self, value: List[CairoType]) -> ArrayType: + """ + Array contains values of type under `value[0]`. + """ + return ArrayType(value[0]) + + def type_span(self, value: List[CairoType]) -> ArrayType: + """ + Span contains values of type under `value[0]`. + """ + return ArrayType(value[0]) + + def type_identifier(self, tokens: List[Token]) -> TypeIdentifier: + """ + Structs and enums are defined as follows: (IDENTIFIER | "::")+ [some not important info] + where IDENTIFIER is a string. + + Tokens would contain strings and types (if it is present). + We are interested only in the strings because a structure (or enum) name can be built from them. + """ + name = "::".join(token for token in tokens if isinstance(token, str)) + if name in self.type_identifiers: + return self.type_identifiers[name] + return TypeIdentifier(name) + + def type_contract_address(self, _value: List[Any]) -> FeltType: + """ + ContractAddress is represented by the felt252. + """ + return FeltType() + + def type_class_hash(self, _value: List[Any]) -> FeltType: + """ + ClassHash is represented by the felt252. + """ + return FeltType() + + def type_storage_address(self, _value: List[Any]) -> FeltType: + """ + StorageAddress is represented by the felt252. + """ + return FeltType() + + def tuple(self, types: List[CairoType]) -> TupleType: + """ + Tuple contains values defined in the `types` argument. + """ + return TupleType(types) + + +def parse( + code: str, + type_identifiers, +) -> CairoType: + """ + Parse the given string and return a CairoType. + """ + grammar_parser = lark.Lark( + grammar=ABI_EBNF, + start="type", + parser="earley", + ) + parsed = grammar_parser.parse(code) + + parser_transformer = ParserTransformer(type_identifiers) + cairo_type = parser_transformer.transform(parsed) + + return cairo_type diff --git a/ccxt/static_dependencies/starknet/abi/v1/schemas.py b/ccxt/static_dependencies/starknet/abi/v1/schemas.py new file mode 100644 index 0000000..9b0ff2f --- /dev/null +++ b/ccxt/static_dependencies/starknet/abi/v1/schemas.py @@ -0,0 +1,66 @@ +from ....marshmallow import Schema, fields +from ....marshmallow_oneofschema import OneOfSchema + +from .shape import ( + ENUM_ENTRY, + EVENT_ENTRY, + FUNCTION_ENTRY, + STRUCT_ENTRY, +) + + +class TypeSchema(Schema): + type = fields.String(data_key="type", required=True) + + +class TypedParameterSchema(TypeSchema): + name = fields.String(data_key="name", required=True) + + +class FunctionBaseSchema(Schema): + name = fields.String(data_key="name", required=True) + inputs = fields.List( + fields.Nested(TypedParameterSchema()), data_key="inputs", required=True + ) + outputs = fields.List( + fields.Nested(TypeSchema()), data_key="outputs", required=True + ) + state_mutability = fields.String(data_key="state_mutability", default=None) + + +class FunctionAbiEntrySchema(FunctionBaseSchema): + type = fields.Constant(FUNCTION_ENTRY, data_key="type", required=True) + + +class EventAbiEntrySchema(Schema): + type = fields.Constant(EVENT_ENTRY, data_key="type", required=True) + name = fields.String(data_key="name", required=True) + inputs = fields.List( + fields.Nested(TypedParameterSchema()), data_key="inputs", required=True + ) + + +class StructAbiEntrySchema(Schema): + type = fields.Constant(STRUCT_ENTRY, data_key="type", required=True) + name = fields.String(data_key="name", required=True) + members = fields.List( + fields.Nested(TypedParameterSchema()), data_key="members", required=True + ) + + +class EnumAbiEntrySchema(Schema): + type = fields.Constant(ENUM_ENTRY, data_key="type", required=True) + name = fields.String(data_key="name", required=True) + variants = fields.List( + fields.Nested(TypedParameterSchema(), data_key="variants", required=True) + ) + + +class ContractAbiEntrySchema(OneOfSchema): + type_field_remove = False + type_schemas = { + FUNCTION_ENTRY: FunctionAbiEntrySchema, + EVENT_ENTRY: EventAbiEntrySchema, + STRUCT_ENTRY: StructAbiEntrySchema, + ENUM_ENTRY: EnumAbiEntrySchema, + } diff --git a/ccxt/static_dependencies/starknet/abi/v1/shape.py b/ccxt/static_dependencies/starknet/abi/v1/shape.py new file mode 100644 index 0000000..0a4b94f --- /dev/null +++ b/ccxt/static_dependencies/starknet/abi/v1/shape.py @@ -0,0 +1,47 @@ +from typing import List, Literal, Optional, TypedDict, Union + +ENUM_ENTRY = "enum" +STRUCT_ENTRY = "struct" +FUNCTION_ENTRY = "function" +EVENT_ENTRY = "event" + + +class TypeDict(TypedDict): + type: str + + +class TypedParameterDict(TypeDict): + name: str + + +class StructDict(TypedDict): + type: Literal["struct"] + name: str + members: List[TypedParameterDict] + + +class FunctionBaseDict(TypedDict): + name: str + inputs: List[TypedParameterDict] + outputs: List[TypeDict] + state_mutability: Optional[Literal["external", "view"]] + + +class FunctionDict(FunctionBaseDict): + type: Literal["function"] + + +class EventDict(TypedDict): + name: str + type: Literal["event"] + inputs: List[TypedParameterDict] + + +class EnumDict(TypedDict): + type: Literal["enum"] + name: str + variants: List[TypedParameterDict] + + +AbiDictEntry = Union[StructDict, FunctionDict, EventDict, EnumDict] +AbiDictList = List[AbiDictEntry] diff --git a/ccxt/static_dependencies/starknet/abi/v2/__init__.py b/ccxt/static_dependencies/starknet/abi/v2/__init__.py new file mode 100644 index 0000000..16b9092 --- /dev/null +++ b/ccxt/static_dependencies/starknet/abi/v2/__init__.py @@ -0,0 +1,2 @@ +from .model import Abi +from .parser import AbiParser, AbiParsingError diff --git a/ccxt/static_dependencies/starknet/abi/v2/model.py b/ccxt/static_dependencies/starknet/abi/v2/model.py new file mode 100644 index 0000000..d290659 --- /dev/null +++ b/ccxt/static_dependencies/starknet/abi/v2/model.py @@ -0,0 +1,89 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Dict, List, Optional, OrderedDict, Union + +from ...cairo.data_types import CairoType, EnumType, EventType, StructType + + +@dataclass +class Abi: + """ + Dataclass representing class abi. Contains parsed functions, enums, events and structures. + """ + + # pylint: disable=too-many-instance-attributes + + @dataclass + class Function: + """ + Dataclass representing function's abi. + """ + + name: str + inputs: OrderedDict[str, CairoType] + outputs: List[CairoType] + + @dataclass + class Constructor: + """ + Dataclass representing constructor's abi. + """ + + name: str + inputs: OrderedDict[str, CairoType] + + @dataclass + class EventStruct: + """ + Dataclass representing struct event's abi. + """ + + name: str + members: OrderedDict[str, CairoType] + + @dataclass + class EventEnum: + """ + Dataclass representing enum event's abi. + """ + + name: str + variants: OrderedDict[str, CairoType] + + Event = Union[EventStruct, EventEnum] + + @dataclass + class Interface: + """ + Dataclass representing an interface. + """ + + name: str + items: OrderedDict[ + str, Abi.Function + ] # Only functions can be defined in the interface + + @dataclass + class Impl: + """ + Dataclass representing an impl. + """ + + name: str + interface_name: str + + defined_structures: Dict[ + str, StructType + ] #: Abi of structures defined by the class. + defined_enums: Dict[str, EnumType] #: Abi of enums defined by the class. + functions: Dict[str, Function] #: Functions defined by the class. + events: Dict[str, EventType] #: Events defined by the class + constructor: Optional[ + Constructor + ] #: Contract's constructor. It is None if class doesn't define one. + l1_handler: Optional[ + Dict[str, Function] + ] #: Handlers of L1 messages. It is None if class doesn't define one. + interfaces: Dict[str, Interface] + implementations: Dict[str, Impl] diff --git a/ccxt/static_dependencies/starknet/abi/v2/parser.py b/ccxt/static_dependencies/starknet/abi/v2/parser.py new file mode 100644 index 0000000..5fe485c --- /dev/null +++ b/ccxt/static_dependencies/starknet/abi/v2/parser.py @@ -0,0 +1,293 @@ +from __future__ import annotations + +import dataclasses +import json +from collections import OrderedDict, defaultdict +from typing import DefaultDict, Dict, List, Optional, Tuple, TypeVar, Union, cast + +from ....marshmallow import EXCLUDE + +from .model import Abi +from .schemas import ContractAbiEntrySchema +from .shape import ( + CONSTRUCTOR_ENTRY, + ENUM_ENTRY, + EVENT_ENTRY, + FUNCTION_ENTRY, + IMPL_ENTRY, + INTERFACE_ENTRY, + L1_HANDLER_ENTRY, + STRUCT_ENTRY, + ConstructorDict, + EventDict, + EventEnumVariantDict, + EventStructMemberDict, + FunctionDict, + ImplDict, + InterfaceDict, + TypedParameterDict, +) +from ...cairo.data_types import CairoType, EnumType, EventType, StructType +from ...cairo.v2.type_parser import TypeParser + + +class AbiParsingError(ValueError): + """ + Error raised when something wrong goes during abi parsing. + """ + + +class AbiParser: + """ + Utility class for parsing abi into a dataclass. + """ + + # Entries from ABI grouped by entry type + _grouped: DefaultDict[str, List[Dict]] + # lazy init property + _type_parser: Optional[TypeParser] = None + + def __init__(self, abi_list: List[Dict]): + """ + Abi parser constructor. Ensures that abi satisfies the abi schema. + + :param abi_list: Contract's ABI as a list of dictionaries. + """ + abi = [ + ContractAbiEntrySchema().load(entry, unknown=EXCLUDE) for entry in abi_list + ] + grouped = defaultdict(list) + for entry in abi: + assert isinstance(entry, dict) + grouped[entry["type"]].append(entry) + + self._grouped = grouped + + def parse(self) -> Abi: + """ + Parse abi provided to constructor and return it as a dataclass. Ensures that there are no cycles in the abi. + + :raises: AbiParsingError: on any parsing error. + :return: Abi dataclass. + """ + structures, enums = self._parse_structures_and_enums() + events_dict = cast( + Dict[str, EventDict], + AbiParser._group_by_entry_name( + self._grouped[EVENT_ENTRY], "defined events" + ), + ) + + events: Dict[str, EventType] = {} + for name, event in events_dict.items(): + events[name] = self._parse_event(event) + assert self._type_parser is not None + self._type_parser.add_defined_type(events[name]) + + functions_dict = cast( + Dict[str, FunctionDict], + AbiParser._group_by_entry_name( + self._grouped[FUNCTION_ENTRY], "defined functions" + ), + ) + interfaces_dict = cast( + Dict[str, InterfaceDict], + AbiParser._group_by_entry_name( + self._grouped[INTERFACE_ENTRY], "defined interfaces" + ), + ) + impls_dict = cast( + Dict[str, ImplDict], + AbiParser._group_by_entry_name(self._grouped[IMPL_ENTRY], "defined impls"), + ) + l1_handlers_dict = cast( + Dict[str, FunctionDict], + AbiParser._group_by_entry_name( + self._grouped[L1_HANDLER_ENTRY], "defined L1 handlers" + ), + ) + constructors = self._grouped[CONSTRUCTOR_ENTRY] + + if len(constructors) > 1: + raise AbiParsingError("Constructor in ABI must be defined at most once.") + + return Abi( + defined_structures=structures, + defined_enums=enums, + constructor=( + self._parse_constructor(cast(ConstructorDict, constructors[0])) + if constructors + else None + ), + l1_handler={ + name: self._parse_function(entry) + for name, entry in l1_handlers_dict.items() + }, + functions={ + name: self._parse_function(entry) + for name, entry in functions_dict.items() + }, + events=events, + interfaces={ + name: self._parse_interface(entry) + for name, entry in interfaces_dict.items() + }, + implementations={ + name: self._parse_impl(entry) for name, entry in impls_dict.items() + }, + ) + + @property + def type_parser(self) -> TypeParser: + if self._type_parser: + return self._type_parser + + raise RuntimeError("Tried to get type_parser before it was set.") + + def _parse_structures_and_enums( + self, + ) -> Tuple[Dict[str, StructType], Dict[str, EnumType]]: + structs_dict = AbiParser._group_by_entry_name( + self._grouped[STRUCT_ENTRY], "defined structures" + ) + enums_dict = AbiParser._group_by_entry_name( + self._grouped[ENUM_ENTRY], "defined enums" + ) + + # Contains sorted members of the struct + struct_members: Dict[str, List[TypedParameterDict]] = {} + structs: Dict[str, StructType] = {} + + # Contains sorted members of the enum + enum_members: Dict[str, List[TypedParameterDict]] = {} + enums: Dict[str, EnumType] = {} + + # Example problem (with a simplified json structure): + # [{name: User, fields: {id: Uint256}}, {name: "Uint256", ...}] + # User refers to Uint256 even though it is not known yet (will be parsed next). + # This is why it is important to create the structure types first. This way other types can already refer to + # them when parsing types, even thought their fields are not filled yet. + # At the end we will mutate those structures to contain the right fields. An alternative would be to use + # topological sorting with an additional "unresolved type", so this flow is much easier. + for name, struct in structs_dict.items(): + structs[name] = StructType(name, OrderedDict()) + struct_members[name] = struct["members"] + + for name, enum in enums_dict.items(): + enums[name] = EnumType(name, OrderedDict()) + enum_members[name] = enum["variants"] + + # Now parse the types of members and save them. + defined_structs_enums: Dict[str, Union[StructType, EnumType]] = dict(structs) + defined_structs_enums.update(enums) + + self._type_parser = TypeParser(defined_structs_enums) # pyright: ignore + for name, struct in structs.items(): + members = self._parse_members( + cast(List[TypedParameterDict], struct_members[name]), + f"members of structure '{name}'", + ) + struct.types.update(members) + for name, enum in enums.items(): + members = self._parse_members( + cast(List[TypedParameterDict], enum_members[name]), + f"members of enum '{name}'", + ) + enum.variants.update(members) + + # All types have their members assigned now + + self._check_for_cycles(defined_structs_enums) + + return structs, enums + + @staticmethod + def _check_for_cycles(structs: Dict[str, Union[StructType, EnumType]]): + # We want to avoid creating our own cycle checker as it would make it more complex. json module has a built-in + # checker for cycles. + try: + _to_json(structs) + except ValueError as err: + raise AbiParsingError(err) from ValueError + + def _parse_function(self, function: FunctionDict) -> Abi.Function: + return Abi.Function( + name=function["name"], + inputs=self._parse_members(function["inputs"], function["name"]), + outputs=list( + self.type_parser.parse_inline_type(param["type"]) + for param in function["outputs"] + ), + ) + + def _parse_constructor(self, constructor: ConstructorDict) -> Abi.Constructor: + return Abi.Constructor( + name=constructor["name"], + inputs=self._parse_members(constructor["inputs"], constructor["name"]), + ) + + def _parse_event(self, event: EventDict) -> EventType: + members_ = event.get("members", event.get("variants")) + assert isinstance(members_, list) + return EventType( + name=event["name"], + types=self._parse_members( + cast(List[TypedParameterDict], members_), event["name"] + ), + ) + + TypedParam = TypeVar( + "TypedParam", TypedParameterDict, EventStructMemberDict, EventEnumVariantDict + ) + + def _parse_members( + self, params: List[TypedParam], entity_name: str + ) -> OrderedDict[str, CairoType]: + # Without cast, it complains that 'Type "TypedParameterDict" cannot be assigned to type "T@_group_by_name"' + members = AbiParser._group_by_entry_name(cast(List[Dict], params), entity_name) + return OrderedDict( + (name, self.type_parser.parse_inline_type(param["type"])) + for name, param in members.items() + ) + + def _parse_interface(self, interface: InterfaceDict) -> Abi.Interface: + return Abi.Interface( + name=interface["name"], + items=OrderedDict( + (entry["name"], self._parse_function(entry)) + for entry in interface["items"] + ), + ) + + @staticmethod + def _parse_impl(impl: ImplDict) -> Abi.Impl: + return Abi.Impl( + name=impl["name"], + interface_name=impl["interface_name"], + ) + + @staticmethod + def _group_by_entry_name( + dicts: List[Dict], entity_name: str + ) -> OrderedDict[str, Dict]: + grouped = OrderedDict() + for entry in dicts: + name = entry["name"] + if name in grouped: + raise AbiParsingError( + f"Name '{name}' was used more than once in {entity_name}." + ) + grouped[name] = entry + return grouped + + +def _to_json(value): + class DataclassSupportingEncoder(json.JSONEncoder): + def default(self, o): + # Dataclasses are not supported by json. Additionally, dataclasses.asdict() works recursively and doesn't + # check for cycles, so we need to flatten dataclasses (by ONE LEVEL) ourselves. + if dataclasses.is_dataclass(o): + return tuple(getattr(o, field.name) for field in dataclasses.fields(o)) + return super().default(o) + + return json.dumps(value, cls=DataclassSupportingEncoder) diff --git a/ccxt/static_dependencies/starknet/abi/v2/parser_transformer.py b/ccxt/static_dependencies/starknet/abi/v2/parser_transformer.py new file mode 100644 index 0000000..1c81885 --- /dev/null +++ b/ccxt/static_dependencies/starknet/abi/v2/parser_transformer.py @@ -0,0 +1,192 @@ +from typing import Any, List, Optional + +from ....lark import * +from ....lark import Token, Transformer + +from ...cairo.data_types import ( + ArrayType, + BoolType, + CairoType, + FeltType, + OptionType, + TupleType, + TypeIdentifier, + UintType, + UnitType, +) + +ABI_EBNF = """ + IDENTIFIER: /[a-zA-Z_][a-zA-Z_0-9]*/ + + type: "@"? actual_type + + actual_type: type_unit + | type_bool + | type_felt + | type_bytes + | type_uint + | type_contract_address + | type_class_hash + | type_storage_address + | type_option + | type_array + | type_span + | tuple + | type_identifier + + + type_unit: "()" + type_felt: "core::felt252" + type_bytes: "core::bytes_31::bytes31" + type_bool: "core::bool" + type_uint: "core::integer::u" INT + type_contract_address: "core::starknet::contract_address::ContractAddress" + type_class_hash: "core::starknet::class_hash::ClassHash" + type_storage_address: "core::starknet::storage_access::StorageAddress" + type_option: "core::option::Option::<" (type | type_identifier) ">" + type_array: "core::array::Array::<" (type | type_identifier) ">" + type_span: "core::array::Span::<" (type | type_identifier) ">" + + tuple: "(" type? ("," type?)* ")" + + type_identifier: (IDENTIFIER | "::")+ ("<" (type | ",")+ ">")? + + + %import common.INT + %import common.WS + %ignore WS +""" + + +class ParserTransformer(Transformer): + """ + Transforms the lark tree into CairoTypes. + """ + + def __init__(self, type_identifiers: Optional[dict] = None) -> None: + if type_identifiers is None: + type_identifiers = {} + self.type_identifiers = type_identifiers + super(Transformer, self).__init__() + + # pylint: disable=no-self-use + + def __default__(self, data: str, children, meta): + raise TypeError(f"Unable to parse tree node of type {data}.") + + def type(self, value: List[Optional[CairoType]]) -> Optional[CairoType]: + """ + Tokens are read bottom-up, so here all of them are parsed and should be just returned. + `Optional` is added in case of the unit type. + """ + assert len(value) == 1 + return value[0] + + def actual_type(self, value) -> Optional[CairoType]: + return value[0] + + def type_felt(self, _value: List[Any]) -> FeltType: + """ + Felt does not contain any additional arguments, so `_value` is just an empty list. + """ + return FeltType() + + def type_bytes(self, _value: List[Any]) -> FeltType: + """ + Felt does not contain any additional arguments, so `_value` is just an empty list. + """ + return FeltType() + + def type_bool(self, _value: List[Any]) -> BoolType: + """ + Bool does not contain any additional arguments, so `_value` is just an empty list. + """ + return BoolType() + + def type_uint(self, value: List[Token]) -> UintType: + """ + Uint type contains information about its size. It is present in the value[0]. + """ + return UintType(int(value[0])) + + def type_unit(self, _value: List[Any]) -> UnitType: + """ + `()` type. + """ + return UnitType() + + def type_option(self, value: List[CairoType]) -> OptionType: + """ + Option includes an information about which type it eventually represents. + `Optional` is added in case of the unit type. + """ + return OptionType(value[0]) + + def type_array(self, value: List[CairoType]) -> ArrayType: + """ + Array contains values of type under `value[0]`. + """ + return ArrayType(value[0]) + + def type_span(self, value: List[CairoType]) -> ArrayType: + """ + Span contains values of type under `value[0]`. + """ + return ArrayType(value[0]) + + def type_identifier(self, tokens: List[Token]) -> TypeIdentifier: + """ + Structs and enums are defined as follows: (IDENTIFIER | "::")+ [some not important info] + where IDENTIFIER is a string. + + Tokens would contain strings and types (if it is present). + We are interested only in the strings because a structure (or enum) name can be built from them. + """ + name = "::".join(token for token in tokens if isinstance(token, str)) + if name in self.type_identifiers: + return self.type_identifiers[name] + return TypeIdentifier(name) + + def type_contract_address(self, _value: List[Any]) -> FeltType: + """ + ContractAddress is represented by the felt252. + """ + return FeltType() + + def type_class_hash(self, _value: List[Any]) -> FeltType: + """ + ClassHash is represented by the felt252. + """ + return FeltType() + + def type_storage_address(self, _value: List[Any]) -> FeltType: + """ + StorageAddress is represented by the felt252. + """ + return FeltType() + + def tuple(self, types: List[CairoType]) -> TupleType: + """ + Tuple contains values defined in the `types` argument. + """ + return TupleType(types) + + +def parse( + code: str, + type_identifiers, +) -> CairoType: + """ + Parse the given string and return a CairoType. + """ + grammar_parser = lark.Lark( + grammar=ABI_EBNF, + start="type", + parser="earley", + ) + parsed_lark_tree = grammar_parser.parse(code) + + parser_transformer = ParserTransformer(type_identifiers) + cairo_type = parser_transformer.transform(parsed_lark_tree) + + return cairo_type diff --git a/ccxt/static_dependencies/starknet/abi/v2/schemas.py b/ccxt/static_dependencies/starknet/abi/v2/schemas.py new file mode 100644 index 0000000..aef2426 --- /dev/null +++ b/ccxt/static_dependencies/starknet/abi/v2/schemas.py @@ -0,0 +1,132 @@ +from ....marshmallow import Schema, fields +from ....marshmallow_oneofschema import OneOfSchema + +from .shape import ( + CONSTRUCTOR_ENTRY, + DATA_KIND, + ENUM_ENTRY, + EVENT_ENTRY, + FUNCTION_ENTRY, + IMPL_ENTRY, + INTERFACE_ENTRY, + L1_HANDLER_ENTRY, + NESTED_KIND, + STRUCT_ENTRY, +) + + +class TypeSchema(Schema): + type = fields.String(data_key="type", required=True) + + +class TypedParameterSchema(TypeSchema): + name = fields.String(data_key="name", required=True) + + +class FunctionBaseSchema(Schema): + name = fields.String(data_key="name", required=True) + inputs = fields.List( + fields.Nested(TypedParameterSchema()), data_key="inputs", required=True + ) + outputs = fields.List( + fields.Nested(TypeSchema()), data_key="outputs", required=True + ) + state_mutability = fields.String(data_key="state_mutability", default=None) + + +class FunctionAbiEntrySchema(FunctionBaseSchema): + type = fields.Constant(FUNCTION_ENTRY, data_key="type", required=True) + + +class ConstructorAbiEntrySchema(Schema): + type = fields.Constant(CONSTRUCTOR_ENTRY, data_key="type", required=True) + name = fields.String(data_key="name", required=True) + inputs = fields.List( + fields.Nested(TypedParameterSchema()), data_key="inputs", required=True + ) + + +class L1HandlerAbiEntrySchema(FunctionBaseSchema): + type = fields.Constant(L1_HANDLER_ENTRY, data_key="type", required=True) + + +class EventStructMemberSchema(TypedParameterSchema): + kind = fields.Constant(DATA_KIND, data_key="kind", required=True) + + +class EventStructAbiEntrySchema(Schema): + type = fields.Constant(EVENT_ENTRY, data_key="type", required=True) + name = fields.String(data_key="name", required=True) + kind = fields.Constant(STRUCT_ENTRY, data_key="kind", required=True) + members = fields.List( + fields.Nested(EventStructMemberSchema()), data_key="members", required=True + ) + + +class EventEnumVariantSchema(TypedParameterSchema): + kind = fields.Constant(NESTED_KIND, data_key="kind", required=True) + + +class EventEnumAbiEntrySchema(Schema): + type = fields.Constant(EVENT_ENTRY, data_key="type", required=True) + name = fields.String(data_key="name", required=True) + kind = fields.Constant(ENUM_ENTRY, data_key="kind", required=True) + variants = fields.List( + fields.Nested(EventEnumVariantSchema()), data_key="variants", required=True + ) + + +class EventAbiEntrySchema(OneOfSchema): + type_field = "kind" + type_field_remove = False + type_schemas = { + STRUCT_ENTRY: EventStructAbiEntrySchema, + ENUM_ENTRY: EventEnumAbiEntrySchema, + } + + +class StructAbiEntrySchema(Schema): + type = fields.Constant(STRUCT_ENTRY, data_key="type", required=True) + name = fields.String(data_key="name", required=True) + members = fields.List( + fields.Nested(TypedParameterSchema()), data_key="members", required=True + ) + + +class EnumAbiEntrySchema(Schema): + type = fields.Constant(ENUM_ENTRY, data_key="type", required=True) + name = fields.String(data_key="name", required=True) + variants = fields.List( + fields.Nested(TypedParameterSchema(), data_key="variants", required=True) + ) + + +class ImplAbiEntrySchema(Schema): + type = fields.Constant(IMPL_ENTRY, data_key="type", required=True) + name = fields.String(data_key="name", required=True) + interface_name = fields.String(data_key="interface_name", required=True) + + +class InterfaceAbiEntrySchema(Schema): + type = fields.Constant(INTERFACE_ENTRY, data_key="type", required=True) + name = fields.String(data_key="name", required=True) + + items = fields.List( + fields.Nested( + FunctionAbiEntrySchema(), data_key="items", required=True + ) # for now only functions can be defined here + ) + + +class ContractAbiEntrySchema(OneOfSchema): + type_field_remove = False + type_schemas = { + FUNCTION_ENTRY: FunctionAbiEntrySchema, + EVENT_ENTRY: EventAbiEntrySchema, + STRUCT_ENTRY: StructAbiEntrySchema, + ENUM_ENTRY: EnumAbiEntrySchema, + CONSTRUCTOR_ENTRY: ConstructorAbiEntrySchema, + L1_HANDLER_ENTRY: L1HandlerAbiEntrySchema, + IMPL_ENTRY: ImplAbiEntrySchema, + INTERFACE_ENTRY: InterfaceAbiEntrySchema, + } diff --git a/ccxt/static_dependencies/starknet/abi/v2/shape.py b/ccxt/static_dependencies/starknet/abi/v2/shape.py new file mode 100644 index 0000000..f26ebe2 --- /dev/null +++ b/ccxt/static_dependencies/starknet/abi/v2/shape.py @@ -0,0 +1,107 @@ +from __future__ import annotations + +from typing import List, Literal, Optional, TypedDict, Union + +STRUCT_ENTRY = "struct" +EVENT_ENTRY = "event" +FUNCTION_ENTRY = "function" +ENUM_ENTRY = "enum" +CONSTRUCTOR_ENTRY = "constructor" +L1_HANDLER_ENTRY = "l1_handler" +IMPL_ENTRY = "impl" +INTERFACE_ENTRY = "interface" + +DATA_KIND = "data" +NESTED_KIND = "nested" + + +class TypeDict(TypedDict): + type: str + + +class TypedParameterDict(TypeDict): + name: str + + +class StructDict(TypedDict): + type: Literal["struct"] + name: str + members: List[TypedParameterDict] + + +class FunctionBaseDict(TypedDict): + name: str + inputs: List[TypedParameterDict] + outputs: List[TypeDict] + state_mutability: Optional[Literal["external", "view"]] + + +class FunctionDict(FunctionBaseDict): + type: Literal["function"] + + +class ConstructorDict(TypedDict): + type: Literal["constructor"] + name: str + inputs: List[TypedParameterDict] + + +class L1HandlerDict(FunctionBaseDict): + type: Literal["l1_handler"] + + +class EventBaseDict(TypedDict): + type: Literal["event"] + name: str + + +class EventStructMemberDict(TypedParameterDict): + kind: Literal["data"] + + +class EventStructDict(EventBaseDict): + kind: Literal["struct"] + members: List[EventStructMemberDict] + + +class EventEnumVariantDict(TypedParameterDict): + kind: Literal["nested"] + + +class EventEnumDict(EventBaseDict): + kind: Literal["enum"] + variants: List[EventEnumVariantDict] + + +EventDict = Union[EventStructDict, EventEnumDict] + + +class EnumDict(TypedDict): + type: Literal["enum"] + name: str + variants: List[TypedParameterDict] + + +class ImplDict(TypedDict): + type: Literal["impl"] + name: str + interface_name: str + + +class InterfaceDict(TypedDict): + type: Literal["interface"] + name: str + items: List[FunctionDict] # for now only functions can be defined here + + +AbiDictEntry = Union[ + StructDict, + FunctionDict, + EventDict, + EnumDict, + ConstructorDict, + L1HandlerDict, + ImplDict, + InterfaceDict, +] +AbiDictList = List[AbiDictEntry] diff --git a/ccxt/static_dependencies/starknet/cairo/__init__.py b/ccxt/static_dependencies/starknet/cairo/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ccxt/static_dependencies/starknet/cairo/__pycache__/__init__.cpython-311.pyc b/ccxt/static_dependencies/starknet/cairo/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..06fa9a1 Binary files /dev/null and b/ccxt/static_dependencies/starknet/cairo/__pycache__/__init__.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/starknet/cairo/__pycache__/felt.cpython-311.pyc b/ccxt/static_dependencies/starknet/cairo/__pycache__/felt.cpython-311.pyc new file mode 100644 index 0000000..7b4f447 Binary files /dev/null and b/ccxt/static_dependencies/starknet/cairo/__pycache__/felt.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/starknet/cairo/data_types.py b/ccxt/static_dependencies/starknet/cairo/data_types.py new file mode 100644 index 0000000..872a136 --- /dev/null +++ b/ccxt/static_dependencies/starknet/cairo/data_types.py @@ -0,0 +1,123 @@ +from __future__ import annotations + +from abc import ABC +from collections import OrderedDict +from dataclasses import dataclass +from typing import List + + +class CairoType(ABC): + """ + Base type for all Cairo type representations. All types extend it. + """ + + +@dataclass +class FeltType(CairoType): + """ + Type representation of Cairo field element. + """ + + +@dataclass +class BoolType(CairoType): + """ + Type representation of Cairo boolean. + """ + + +@dataclass +class TupleType(CairoType): + """ + Type representation of Cairo tuples without named fields. + """ + + types: List[CairoType] #: types of every tuple element. + + +@dataclass +class NamedTupleType(CairoType): + """ + Type representation of Cairo tuples with named fields. + """ + + types: OrderedDict[str, CairoType] #: types of every tuple member. + + +@dataclass +class ArrayType(CairoType): + """ + Type representation of Cairo arrays. + """ + + inner_type: CairoType #: type of element inside array. + + +@dataclass +class StructType(CairoType): + """ + Type representation of Cairo structures. + """ + + name: str #: Structure name + # We need ordered dict, because it is important in serialization + types: OrderedDict[str, CairoType] #: types of every structure member. + + +@dataclass +class EnumType(CairoType): + """ + Type representation of Cairo enums. + """ + + name: str + variants: OrderedDict[str, CairoType] + + +@dataclass +class OptionType(CairoType): + """ + Type representation of Cairo options. + """ + + type: CairoType + + +@dataclass +class UintType(CairoType): + """ + Type representation of Cairo unsigned integers. + """ + + bits: int + + def check_range(self, value: int): + """ + Utility method checking if the `value` is in range. + """ + + +@dataclass +class TypeIdentifier(CairoType): + """ + Type representation of Cairo identifiers. + """ + + name: str + + +@dataclass +class UnitType(CairoType): + """ + Type representation of Cairo unit `()`. + """ + + +@dataclass +class EventType(CairoType): + """ + Type representation of Cairo Event. + """ + + name: str + types: OrderedDict[str, CairoType] diff --git a/ccxt/static_dependencies/starknet/cairo/deprecated_parse/__init__.py b/ccxt/static_dependencies/starknet/cairo/deprecated_parse/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ccxt/static_dependencies/starknet/cairo/deprecated_parse/cairo_types.py b/ccxt/static_dependencies/starknet/cairo/deprecated_parse/cairo_types.py new file mode 100644 index 0000000..eda4f85 --- /dev/null +++ b/ccxt/static_dependencies/starknet/cairo/deprecated_parse/cairo_types.py @@ -0,0 +1,77 @@ +import dataclasses +from typing import List, Optional + + +class CairoType: + """ + Base class for cairo types. + """ + + +@dataclasses.dataclass +class TypeFelt(CairoType): + pass + + +@dataclasses.dataclass +class TypeCodeoffset(CairoType): + pass + + +@dataclasses.dataclass +class TypePointer(CairoType): + pointee: CairoType + + +@dataclasses.dataclass +class TypeIdentifier(CairoType): + """ + Represents a name of an unresolved type. + This type can be resolved to TypeStruct or TypeDefinition. + """ + + name: str + + +@dataclasses.dataclass +class TypeStruct(CairoType): + scope: str + + +@dataclasses.dataclass +class TypeFunction(CairoType): + """ + Represents a type of a function. + """ + + scope: str + + +@dataclasses.dataclass +class TypeTuple(CairoType): + """ + Represents a type of a named or unnamed tuple. + For example, "(felt, felt*)" or "(a: felt, b: felt*)". + """ + + @dataclasses.dataclass + class Item(CairoType): + """ + Represents a possibly named type item of a TypeTuple. + For example: "felt" or "a: felt". + """ + + name: Optional[str] + typ: CairoType + + members: List["TypeTuple.Item"] + has_trailing_comma: bool = dataclasses.field(hash=False, compare=False) + + @property + def is_named(self) -> bool: + return all(member.name is not None for member in self.members) + + +@dataclasses.dataclass +class ExprIdentifier(CairoType): + name: str diff --git a/ccxt/static_dependencies/starknet/cairo/deprecated_parse/parser.py b/ccxt/static_dependencies/starknet/cairo/deprecated_parse/parser.py new file mode 100644 index 0000000..ecb4c3c --- /dev/null +++ b/ccxt/static_dependencies/starknet/cairo/deprecated_parse/parser.py @@ -0,0 +1,46 @@ +from ....lark import Lark + +from .cairo_types import CairoType +from .parser_transformer import ParserTransformer + +CAIRO_EBNF = """ + %import common.WS_INLINE + %ignore WS_INLINE + + IDENTIFIER: /[a-zA-Z_][a-zA-Z_0-9]*/ + _DBL_STAR: "**" + COMMA: "," + + ?type: non_identifier_type + | identifier -> type_struct + + comma_separated{item}: item? (COMMA item)* COMMA? + + named_type: identifier (":" type)? | non_identifier_type + non_identifier_type: "felt" -> type_felt + | "codeoffset" -> type_codeoffset + | type "*" -> type_pointer + | type _DBL_STAR -> type_pointer2 + | "(" comma_separated{named_type} ")" -> type_tuple + + identifier: IDENTIFIER ("." IDENTIFIER)* +""" + + +def parse(code: str) -> CairoType: + """ + Parses the given string and returns a CairoType. + """ + + grammar = CAIRO_EBNF + + grammar_parser = Lark( + grammar=grammar, + start=["type"], + parser="lalr", + ) + + parsed = grammar_parser.parse(code) + transformed = ParserTransformer().transform(parsed) + + return transformed diff --git a/ccxt/static_dependencies/starknet/cairo/deprecated_parse/parser_transformer.py b/ccxt/static_dependencies/starknet/cairo/deprecated_parse/parser_transformer.py new file mode 100644 index 0000000..7d4d9e7 --- /dev/null +++ b/ccxt/static_dependencies/starknet/cairo/deprecated_parse/parser_transformer.py @@ -0,0 +1,138 @@ +import dataclasses +from typing import Optional, Tuple + +from ....lark import Token, Transformer, v_args + +from .cairo_types import ( + CairoType, + ExprIdentifier, + TypeCodeoffset, + TypeFelt, + TypeIdentifier, + TypePointer, + TypeStruct, + TypeTuple, +) + + +@dataclasses.dataclass +class ParserContext: + """ + Represents information that affects the parsing process. + """ + + # If True, treat type identifiers as resolved. + resolved_types: bool = False + + +class ParserError(Exception): + """ + Base exception for parsing process. + """ + + +@dataclasses.dataclass +class CommaSeparated: + """ + Represents a list of comma separated values, such as expressions or types. + """ + + args: list + has_trailing_comma: bool + + +class ParserTransformer(Transformer): + """ + Transforms the lark tree into an AST based on the classes defined in cairo_types.py. + """ + + # pylint: disable=unused-argument, no-self-use + + def __init__(self): + super().__init__() + self.parser_context = ParserContext() + + def __default__(self, data: str, children, meta): + raise TypeError(f"Unable to parse tree node of type {data}") + + def comma_separated(self, value) -> CommaSeparated: + saw_comma = None + args: list = [] + for v in value: + if isinstance(v, Token) and v.type == "COMMA": + if saw_comma is not False: + raise ParserError("Unexpected comma.") + saw_comma = True + else: + if saw_comma is False: + raise ParserError("Expected a comma before this expression.") + args.append(v) + + # Reset state. + saw_comma = False + + if saw_comma is None: + saw_comma = False + + return CommaSeparated(args=args, has_trailing_comma=saw_comma) + + # Types. + + @v_args(meta=True) + def named_type(self, meta, value) -> TypeTuple.Item: + name: Optional[str] + if len(value) == 1: + # Unnamed type. + (typ,) = value + name = None + if isinstance(typ, ExprIdentifier): + typ = self.type_struct([typ]) + elif len(value) == 2: + # Named type. + identifier, typ = value + assert isinstance(identifier, ExprIdentifier) + assert isinstance(typ, CairoType) + if "." in identifier.name: + raise ParserError("Unexpected . in name.") + name = identifier.name + else: + raise NotImplementedError(f"Unexpected number of values. {value}") + + return TypeTuple.Item(name=name, typ=typ) + + @v_args(meta=True) + def type_felt(self, meta, value): + return TypeFelt() + + @v_args(meta=True) + def type_codeoffset(self, meta, value): + return TypeCodeoffset() + + def type_struct(self, value): + assert len(value) == 1 and isinstance(value[0], ExprIdentifier) + if self.parser_context.resolved_types: + # If parser_context.resolved_types is True, assume that the type is a struct. + return TypeStruct(scope=value[0].name) + + return TypeIdentifier(name=value[0].name) + + @v_args(meta=True) + def type_pointer(self, meta, value): + return TypePointer(pointee=value[0]) + + @v_args(meta=True) + def type_pointer2(self, meta, value): + return TypePointer(pointee=TypePointer(pointee=value[0])) + + @v_args(meta=True) + def type_tuple(self, meta, value: Tuple[CommaSeparated]): + (lst,) = value + return TypeTuple(members=lst.args, has_trailing_comma=lst.has_trailing_comma) + + @v_args(meta=True) + def identifier(self, meta, value): + return ExprIdentifier(name=".".join(x.value for x in value)) + + @v_args(meta=True) + def identifier_def(self, meta, value): + return ExprIdentifier(name=value[0].value) diff --git a/ccxt/static_dependencies/starknet/cairo/felt.py b/ccxt/static_dependencies/starknet/cairo/felt.py new file mode 100644 index 0000000..4a7ada7 --- /dev/null +++ b/ccxt/static_dependencies/starknet/cairo/felt.py @@ -0,0 +1,64 @@ +from typing import List + +from ..constants import FIELD_PRIME + +CairoData = List[int] + + +MAX_UINT256 = (1 << 256) - 1 +MIN_UINT256 = 0 + + +def uint256_range_check(value: int): + if not MIN_UINT256 <= value <= MAX_UINT256: + raise ValueError( + f"Uint256 is expected to be in range [0;2**256), got: {value}." + ) + + +MIN_FELT = -FIELD_PRIME // 2 +MAX_FELT = FIELD_PRIME // 2 + + +def is_in_felt_range(value: int) -> bool: + return 0 <= value < FIELD_PRIME + + +def cairo_vm_range_check(value: int): + if not is_in_felt_range(value): + raise ValueError( + f"Felt is expected to be in range [0; {FIELD_PRIME}), got: {value}." + ) + + +def encode_shortstring(text: str) -> int: + """ + A function which encodes short string value (at most 31 characters) into cairo felt (MSB as first character) + + :param text: A short string value in python + :return: Short string value encoded into felt + """ + if len(text) > 31: + raise ValueError( + f"Shortstring cannot be longer than 31 characters, got: {len(text)}." + ) + + try: + text_bytes = text.encode("ascii") + except UnicodeEncodeError as u_err: + raise ValueError(f"Expected an ascii string. Found: {repr(text)}.") from u_err + value = int.from_bytes(text_bytes, "big") + + cairo_vm_range_check(value) + return value + + +def decode_shortstring(value: int) -> str: + """ + A function which decodes a felt value to short string (at most 31 characters) + + :param value: A felt value + :return: Decoded string which is corresponds to that felt + """ + cairo_vm_range_check(value) + return "".join([chr(i) for i in value.to_bytes(31, byteorder="big")]).lstrip("\x00") diff --git a/ccxt/static_dependencies/starknet/cairo/type_parser.py b/ccxt/static_dependencies/starknet/cairo/type_parser.py new file mode 100644 index 0000000..2b074da --- /dev/null +++ b/ccxt/static_dependencies/starknet/cairo/type_parser.py @@ -0,0 +1,121 @@ +from __future__ import annotations + +from collections import OrderedDict +from typing import Dict, cast + +from .deprecated_parse import cairo_types as cairo_lang_types +from .data_types import ( + ArrayType, + CairoType, + FeltType, + NamedTupleType, + StructType, + TupleType, +) +from .deprecated_parse.parser import parse + + +class UnknownCairoTypeError(ValueError): + """ + Error thrown when TypeParser finds type that was not declared prior to parsing. + """ + + type_name: str + + def __init__(self, type_name: str): + super().__init__(f"Type '{type_name}' is not defined") + self.type_name = type_name + + +class TypeParser: + """ + Low level utility class for parsing Cairo types that can be used in external methods. + """ + + defined_types: Dict[str, StructType] + + def __init__(self, defined_types: Dict[str, StructType]): + """ + TypeParser constructor. + + :param defined_types: dictionary containing all defined types. For now, they can only be structures. + """ + self.defined_types = defined_types + for name, struct in defined_types.items(): + if name != struct.name: + raise ValueError( + f"Keys must match name of type, '{name}' != '{struct.name}'." + ) + + def parse_inline_type(self, type_string: str) -> CairoType: + """ + Inline type is one that can be used inline, for instance as return type. For instance + (a: Uint256, b: felt*, c: (felt, felt)). Structure can only be referenced in inline type, can't be defined + this way. + + :param type_string: type to parse. + """ + parsed = parse(type_string) + return self._transform_cairo_lang_type(parsed) + + def _transform_cairo_lang_type( + self, cairo_type: cairo_lang_types.CairoType + ) -> CairoType: + """ + For now, we use parse function from cairo-lang package. It will be replaced in the future, but we need to hide + it from the users. + This function takes types returned by cairo-lang package and maps them to our type classes. + + :param cairo_type: type returned from parse_type function. + :return: CairoType defined by our package. + """ + if isinstance(cairo_type, cairo_lang_types.TypeFelt): + return FeltType() + + if isinstance(cairo_type, cairo_lang_types.TypePointer): + return ArrayType(self._transform_cairo_lang_type(cairo_type.pointee)) + + if isinstance(cairo_type, cairo_lang_types.TypeIdentifier): + return self._get_struct(str(cairo_type.name)) + + if isinstance(cairo_type, cairo_lang_types.TypeTuple): + # Cairo returns is_named when there are no members + if cairo_type.is_named and len(cairo_type.members) != 0: + assert all(member.name is not None for member in cairo_type.members) + + return NamedTupleType( + OrderedDict( + ( + cast( + str, member.name + ), # without that pyright is complaining + self._transform_cairo_lang_type(member.typ), + ) + for member in cairo_type.members + ) + ) + + return TupleType( + [ + self._transform_cairo_lang_type(member.typ) + for member in cairo_type.members + ] + ) + + # Contracts don't support codeoffset as input/output type, user can only use it if it was defined in types + if isinstance(cairo_type, cairo_lang_types.TypeCodeoffset): + return self._get_struct("codeoffset") + + # Other options are: TypeFunction, TypeStruct + # Neither of them are possible. In particular TypeStruct is not possible because we parse structs without + # info about other structs, so they will be just TypeIdentifier (structure that was not parsed). + + # This is an error of our logic, so we throw a RuntimeError. + raise RuntimeError( + f"Received unknown type '{cairo_type}' from parser." + ) # pragma: no cover + + def _get_struct(self, name: str): + if name not in self.defined_types: + raise UnknownCairoTypeError(name) + return self.defined_types[name] diff --git a/ccxt/static_dependencies/starknet/cairo/v1/__init__.py b/ccxt/static_dependencies/starknet/cairo/v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ccxt/static_dependencies/starknet/cairo/v1/type_parser.py b/ccxt/static_dependencies/starknet/cairo/v1/type_parser.py new file mode 100644 index 0000000..9e83bb4 --- /dev/null +++ b/ccxt/static_dependencies/starknet/cairo/v1/type_parser.py @@ -0,0 +1,59 @@ +from __future__ import annotations + +from typing import Dict, Union + +from ...abi.v1.parser_transformer import parse +from ..data_types import CairoType, EnumType, StructType, TypeIdentifier + + +class UnknownCairoTypeError(ValueError): + """ + Error thrown when TypeParser finds type that was not declared prior to parsing. + """ + + type_name: str + + def __init__(self, type_name: str): + super().__init__( + # pylint: disable=line-too-long + f"Type '{type_name}' is not defined. Please report this issue at https://github.com/software-mansion/starknet.py/issues" + ) + self.type_name = type_name + + +class TypeParser: + """ + Low level utility class for parsing Cairo types that can be used in external methods. + """ + + defined_types: Dict[str, Union[StructType, EnumType]] + + def __init__(self, defined_types: Dict[str, Union[StructType, EnumType]]): + """ + TypeParser constructor. + + :param defined_types: dictionary containing all defined types. For now, they can only be structures. + """ + self.defined_types = defined_types + for name, defined_type in defined_types.items(): + if name != defined_type.name: + raise ValueError( + f"Keys must match name of type, '{name}' != '{defined_type.name}'." + ) + + def parse_inline_type(self, type_string: str) -> CairoType: + """ + Inline type is one that can be used inline, for instance as return type. For instance + (core::felt252, (), (core::felt252,)). Structure can only be referenced in inline type, can't be defined + this way. + + :param type_string: type to parse. + """ + parsed = parse(type_string, self.defined_types) + if isinstance(parsed, TypeIdentifier): + for defined_name in self.defined_types.keys(): + if parsed.name == defined_name.split("<")[0].strip(":"): + return self.defined_types[defined_name] + raise UnknownCairoTypeError(parsed.name) + + return parsed diff --git a/ccxt/static_dependencies/starknet/cairo/v2/__init__.py b/ccxt/static_dependencies/starknet/cairo/v2/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ccxt/static_dependencies/starknet/cairo/v2/type_parser.py b/ccxt/static_dependencies/starknet/cairo/v2/type_parser.py new file mode 100644 index 0000000..e944050 --- /dev/null +++ b/ccxt/static_dependencies/starknet/cairo/v2/type_parser.py @@ -0,0 +1,77 @@ +from __future__ import annotations + +from typing import Dict, Union + +from ...abi.v2.parser_transformer import parse +from ..data_types import ( + CairoType, + EnumType, + EventType, + StructType, + TypeIdentifier, +) + + +class UnknownCairoTypeError(ValueError): + """ + Error thrown when TypeParser finds type that was not declared prior to parsing. + """ + + type_name: str + + def __init__(self, type_name: str): + super().__init__( + # pylint: disable=line-too-long + f"Type '{type_name}' is not defined. Please report this issue at https://github.com/software-mansion/starknet.py/issues" + ) + self.type_name = type_name + + +class TypeParser: + """ + Low level utility class for parsing Cairo types that can be used in external methods. + """ + + defined_types: Dict[str, Union[StructType, EnumType, EventType]] + + def __init__( + self, defined_types: Dict[str, Union[StructType, EnumType, EventType]] + ): + """ + TypeParser constructor. + + :param defined_types: dictionary containing all defined types. For now, they can only be structures. + """ + self.defined_types = defined_types + for name, defined_type in defined_types.items(): + if name != defined_type.name: + raise ValueError( + f"Keys must match name of type, '{name}' != '{defined_type.name}'." + ) + + def update_defined_types( + self, defined_types: Dict[str, Union[StructType, EnumType, EventType]] + ) -> None: + self.defined_types.update(defined_types) + + def add_defined_type( + self, defined_type: Union[StructType, EnumType, EventType] + ) -> None: + self.defined_types.update({defined_type.name: defined_type}) + + def parse_inline_type(self, type_string: str) -> CairoType: + """ + Inline type is one that can be used inline, for instance as return type. For instance + (core::felt252, (), (core::felt252,)). Structure can only be referenced in inline type, can't be defined + this way. + + :param type_string: type to parse. + """ + parsed = parse(type_string, self.defined_types) + if isinstance(parsed, TypeIdentifier): + for defined_name in self.defined_types.keys(): + if parsed.name == defined_name.split("<")[0].strip(":"): + return self.defined_types[defined_name] + raise UnknownCairoTypeError(parsed.name) + + return parsed diff --git a/ccxt/static_dependencies/starknet/ccxt_utils.py b/ccxt/static_dependencies/starknet/ccxt_utils.py new file mode 100644 index 0000000..2ed72ea --- /dev/null +++ b/ccxt/static_dependencies/starknet/ccxt_utils.py @@ -0,0 +1,7 @@ +# utils to use starknet library in ccxt +from .constants import EC_ORDER +from ..starkware.crypto.signature import grind_key + +def get_private_key_from_eth_signature(eth_signature_hex: str) -> int: + r = eth_signature_hex[2 : 64 + 2] if eth_signature_hex[0:2] == '0x' else eth_signature_hex[0 : 64] + return grind_key(int(r, 16), EC_ORDER) \ No newline at end of file diff --git a/ccxt/static_dependencies/starknet/common.py b/ccxt/static_dependencies/starknet/common.py new file mode 100644 index 0000000..5ea0275 --- /dev/null +++ b/ccxt/static_dependencies/starknet/common.py @@ -0,0 +1,15 @@ +from typing import Literal, Union + +def int_from_hex(number: Union[str, int]) -> int: + return number if isinstance(number, int) else int(number, 16) + + +def int_from_bytes( + value: bytes, + byte_order: Literal["big", "little"] = "big", + signed: bool = False, +) -> int: + """ + Converts the given bytes object (parsed according to the given byte order) to an integer. + """ + return int.from_bytes(value, byteorder=byte_order, signed=signed) diff --git a/ccxt/static_dependencies/starknet/constants.py b/ccxt/static_dependencies/starknet/constants.py new file mode 100644 index 0000000..1e746a9 --- /dev/null +++ b/ccxt/static_dependencies/starknet/constants.py @@ -0,0 +1,39 @@ +from pathlib import Path + +# Address came from starkware-libs/starknet-addresses repository: https://github.com/starkware-libs/starknet-addresses +FEE_CONTRACT_ADDRESS = ( + "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7" +) + +DEFAULT_DEPLOYER_ADDRESS = ( + "0x041a78e741e5aF2fEc34B695679bC6891742439f7AFB8484Ecd7766661aD02BF" +) + +API_VERSION = 0 + +RPC_CONTRACT_NOT_FOUND_ERROR = 20 +RPC_INVALID_MESSAGE_SELECTOR_ERROR = 21 +RPC_CLASS_HASH_NOT_FOUND_ERROR = 28 +RPC_CONTRACT_ERROR = 40 + +DEFAULT_ENTRY_POINT_NAME = "__default__" +DEFAULT_L1_ENTRY_POINT_NAME = "__l1_default__" +DEFAULT_ENTRY_POINT_SELECTOR = 0 +DEFAULT_DECLARE_SENDER_ADDRESS = 1 + +# MAX_STORAGE_ITEM_SIZE and ADDR_BOUND must be consistent with the corresponding constant in +# starkware/starknet/common/storage.cairo. +MAX_STORAGE_ITEM_SIZE = 256 +ADDR_BOUND = 2**251 - MAX_STORAGE_ITEM_SIZE + +FIELD_PRIME = 0x800000000000011000000000000000000000000000000000000000000000001 +EC_ORDER = 0x800000000000010FFFFFFFFFFFFFFFFB781126DCAE7B2321E66A241ADC64D2F + +# From cairo-lang +# int_from_bytes(b"STARKNET_CONTRACT_ADDRESS") +CONTRACT_ADDRESS_PREFIX = 523065374597054866729014270389667305596563390979550329787219 +L2_ADDRESS_UPPER_BOUND = 2**251 - 256 + +QUERY_VERSION_BASE = 2**128 + +ROOT_PATH = Path(__file__).parent diff --git a/ccxt/static_dependencies/starknet/hash/__init__.py b/ccxt/static_dependencies/starknet/hash/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ccxt/static_dependencies/starknet/hash/__pycache__/__init__.cpython-311.pyc b/ccxt/static_dependencies/starknet/hash/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..5b61cbb Binary files /dev/null and b/ccxt/static_dependencies/starknet/hash/__pycache__/__init__.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/starknet/hash/__pycache__/address.cpython-311.pyc b/ccxt/static_dependencies/starknet/hash/__pycache__/address.cpython-311.pyc new file mode 100644 index 0000000..1179312 Binary files /dev/null and b/ccxt/static_dependencies/starknet/hash/__pycache__/address.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/starknet/hash/__pycache__/selector.cpython-311.pyc b/ccxt/static_dependencies/starknet/hash/__pycache__/selector.cpython-311.pyc new file mode 100644 index 0000000..258db1f Binary files /dev/null and b/ccxt/static_dependencies/starknet/hash/__pycache__/selector.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/starknet/hash/__pycache__/utils.cpython-311.pyc b/ccxt/static_dependencies/starknet/hash/__pycache__/utils.cpython-311.pyc new file mode 100644 index 0000000..a2b0917 Binary files /dev/null and b/ccxt/static_dependencies/starknet/hash/__pycache__/utils.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/starknet/hash/address.py b/ccxt/static_dependencies/starknet/hash/address.py new file mode 100644 index 0000000..b7387f2 --- /dev/null +++ b/ccxt/static_dependencies/starknet/hash/address.py @@ -0,0 +1,79 @@ +from typing import Sequence + +from ..constants import CONTRACT_ADDRESS_PREFIX, L2_ADDRESS_UPPER_BOUND +from .utils import ( + HEX_PREFIX, + _starknet_keccak, + compute_hash_on_elements, + encode_uint, + get_bytes_length, +) + + +def compute_address( + *, + class_hash: int, + constructor_calldata: Sequence[int], + salt: int, + deployer_address: int = 0, +) -> int: + """ + Computes the contract address in the Starknet network - a unique identifier of the contract. + + :param class_hash: class hash of the contract + :param constructor_calldata: calldata for the contract constructor + :param salt: salt used to calculate contract address + :param deployer_address: address of the deployer (if not provided default 0 is used) + :return: Contract's address + """ + + constructor_calldata_hash = compute_hash_on_elements(data=constructor_calldata) + raw_address = compute_hash_on_elements( + data=[ + CONTRACT_ADDRESS_PREFIX, + deployer_address, + salt, + class_hash, + constructor_calldata_hash, + ], + ) + + return raw_address % L2_ADDRESS_UPPER_BOUND + + +def get_checksum_address(address: str) -> str: + """ + Outputs formatted checksum address. + + Follows implementation of starknet.js. It is not compatible with EIP55 as it treats hex string as encoded number, + instead of encoding it as ASCII string. + + :param address: Address to encode + :return: Checksum address + """ + if not address.lower().startswith(HEX_PREFIX): + raise ValueError(f"{address} is not a valid hexadecimal address.") + + int_address = int(address, 16) + string_address = address[2:].zfill(64) + + address_in_bytes = encode_uint(int_address, get_bytes_length(int_address)) + address_hash = _starknet_keccak(address_in_bytes) + + result = "".join( + ( + char.upper() + if char.isalpha() and (address_hash >> 256 - 4 * i - 1) & 1 + else char + ) + for i, char in enumerate(string_address) + ) + + return f"{HEX_PREFIX}{result}" + + +def is_checksum_address(address: str) -> bool: + """ + Checks if provided string is in a checksum address format. + """ + return get_checksum_address(address) == address diff --git a/ccxt/static_dependencies/starknet/hash/compiled_class_hash_objects.py b/ccxt/static_dependencies/starknet/hash/compiled_class_hash_objects.py new file mode 100644 index 0000000..9472744 --- /dev/null +++ b/ccxt/static_dependencies/starknet/hash/compiled_class_hash_objects.py @@ -0,0 +1,111 @@ +# File is copied from +# https://github.com/starkware-libs/cairo-lang/blob/v0.13.1/src/starkware/starknet/core/os/contract_class/compiled_class_hash_objects.py + +import dataclasses +import itertools +from abc import ABC, abstractmethod +from typing import Any, List, Union + +from poseidon_py.poseidon_hash import poseidon_hash_many + + +class BytecodeSegmentStructure(ABC): + """ + Represents the structure of the bytecode to allow loading it partially into the OS memory. + See the documentation of the OS function `bytecode_hash_node` in `compiled_class.cairo` + for more details. + """ + + @abstractmethod + def hash(self) -> int: + """ + Computes the hash of the node. + """ + + def bytecode_with_skipped_segments(self): + """ + Returns the bytecode of the node. + Skipped segments are replaced with [-1, -2, -2, -2, ...]. + """ + res: List[int] = [] + self.add_bytecode_with_skipped_segments(res) + return res + + @abstractmethod + def add_bytecode_with_skipped_segments(self, data: List[int]): + """ + Same as bytecode_with_skipped_segments, but appends the result to the given list. + """ + + +@dataclasses.dataclass +class BytecodeLeaf(BytecodeSegmentStructure): + """ + Represents a leaf in the bytecode segment tree. + """ + + data: List[int] + + def hash(self) -> int: + return poseidon_hash_many(self.data) + + def add_bytecode_with_skipped_segments(self, data: List[int]): + data.extend(self.data) + + +@dataclasses.dataclass +class BytecodeSegmentedNode(BytecodeSegmentStructure): + """ + Represents an internal node in the bytecode segment tree. + Each child can be loaded into memory or skipped. + """ + + segments: List["BytecodeSegment"] + + def hash(self) -> int: + return ( + poseidon_hash_many( + itertools.chain( # pyright: ignore + *[ + (node.segment_length, node.inner_structure.hash()) + for node in self.segments + ] + ) + ) + + 1 + ) + + def add_bytecode_with_skipped_segments(self, data: List[int]): + for segment in self.segments: + if segment.is_used: + segment.inner_structure.add_bytecode_with_skipped_segments(data) + else: + data.append(-1) + data.extend(-2 for _ in range(segment.segment_length - 1)) + + +@dataclasses.dataclass +class BytecodeSegment: + """ + Represents a child of BytecodeSegmentedNode. + """ + + # The length of the segment. + segment_length: int + # Should the segment (or part of it) be loaded to memory. + # In other words, is the segment used during the execution. + # Note that if is_used is False, the entire segment is not loaded to memory. + # If is_used is True, it is possible that part of the segment will be skipped (according + # to the "is_used" field of the child segments). + is_used: bool + # The inner structure of the segment. + inner_structure: BytecodeSegmentStructure + + def __post_init__(self): + assert ( + self.segment_length > 0 + ), f"Invalid segment length: {self.segment_length}." + + +# Represents a nested list of integers. E.g., [1, [2, [3], 4], 5, 6]. +NestedIntList = Union[int, List[Any]] diff --git a/ccxt/static_dependencies/starknet/hash/selector.py b/ccxt/static_dependencies/starknet/hash/selector.py new file mode 100644 index 0000000..43d8aac --- /dev/null +++ b/ccxt/static_dependencies/starknet/hash/selector.py @@ -0,0 +1,16 @@ +from ..constants import ( + DEFAULT_ENTRY_POINT_NAME, + DEFAULT_ENTRY_POINT_SELECTOR, + DEFAULT_L1_ENTRY_POINT_NAME, +) +from ..hash.utils import _starknet_keccak + + +def get_selector_from_name(func_name: str) -> int: + """ + Returns the selector of a contract's function name. + """ + if func_name in [DEFAULT_ENTRY_POINT_NAME, DEFAULT_L1_ENTRY_POINT_NAME]: + return DEFAULT_ENTRY_POINT_SELECTOR + + return _starknet_keccak(data=func_name.encode("ascii")) diff --git a/ccxt/static_dependencies/starknet/hash/storage.py b/ccxt/static_dependencies/starknet/hash/storage.py new file mode 100644 index 0000000..f1dba09 --- /dev/null +++ b/ccxt/static_dependencies/starknet/hash/storage.py @@ -0,0 +1,12 @@ +from functools import reduce + +from constants import ADDR_BOUND +from hash.utils import _starknet_keccak, pedersen_hash + + +def get_storage_var_address(var_name: str, *args: int) -> int: + """ + Returns the storage address of a Starknet storage variable given its name and arguments. + """ + res = _starknet_keccak(var_name.encode("ascii")) + return reduce(pedersen_hash, args, res) % ADDR_BOUND diff --git a/ccxt/static_dependencies/starknet/hash/utils.py b/ccxt/static_dependencies/starknet/hash/utils.py new file mode 100644 index 0000000..2aa4d53 --- /dev/null +++ b/ccxt/static_dependencies/starknet/hash/utils.py @@ -0,0 +1,78 @@ +import functools +from typing import List, Optional, Sequence + +from ... import keccak + +from ..common import int_from_bytes +from ..constants import EC_ORDER +from ...starkware.crypto.signature import ( + ECSignature, + private_to_stark_key, + sign + # verify +) +from ...starkware.crypto.fast_pedersen_hash import ( + pedersen_hash +) + +MASK_250 = 2**250 - 1 +HEX_PREFIX = "0x" + + +def _starknet_keccak(data: bytes) -> int: + """ + A variant of eth-keccak that computes a value that fits in a Starknet field element. + """ + return int_from_bytes(keccak.SHA3(data)) & MASK_250 + + +# def pedersen_hash(left: int, right: int) -> int: +# """ +# One of two hash functions (along with _starknet_keccak) used throughout Starknet. +# """ +# return cpp_hash(left, right) + + +def compute_hash_on_elements(data: Sequence) -> int: + """ + Computes a hash chain over the data, in the following order: + h(h(h(h(0, data[0]), data[1]), ...), data[n-1]), n). + + The hash is initialized with 0 and ends with the data length appended. + The length is appended in order to avoid collisions of the following kind: + H([x,y,z]) = h(h(x,y),z) = H([w, z]) where w = h(x,y). + """ + return functools.reduce(pedersen_hash, [*data, len(data)], 0) + + +def message_signature( + msg_hash: int, priv_key: int, seed: Optional[int] = 32 +) -> ECSignature: + """ + Signs the message with private key. + """ + return sign(msg_hash, priv_key, seed) + + +# def verify_message_signature( +# msg_hash: int, signature: List[int], public_key: int +# ) -> bool: +# """ +# Verifies ECDSA signature of a given message hash with a given public key. +# Returns true if public_key signs the message. +# """ +# sig_r, sig_s = signature +# # sig_w = pow(sig_s, -1, EC_ORDER) +# return verify(msg_hash=msg_hash, r=sig_r, s=sig_s, public_key=public_key) + + +def encode_uint(value: int, bytes_length: int = 32) -> bytes: + return value.to_bytes(bytes_length, byteorder="big") + + +def encode_uint_list(data: List[int]) -> bytes: + return b"".join(encode_uint(x) for x in data) + + +def get_bytes_length(value: int) -> int: + return (value.bit_length() + 7) // 8 diff --git a/ccxt/static_dependencies/starknet/models/__init__.py b/ccxt/static_dependencies/starknet/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ccxt/static_dependencies/starknet/models/__pycache__/__init__.cpython-311.pyc b/ccxt/static_dependencies/starknet/models/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..469002f Binary files /dev/null and b/ccxt/static_dependencies/starknet/models/__pycache__/__init__.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/starknet/models/__pycache__/typed_data.cpython-311.pyc b/ccxt/static_dependencies/starknet/models/__pycache__/typed_data.cpython-311.pyc new file mode 100644 index 0000000..587d414 Binary files /dev/null and b/ccxt/static_dependencies/starknet/models/__pycache__/typed_data.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/starknet/models/typed_data.py b/ccxt/static_dependencies/starknet/models/typed_data.py new file mode 100644 index 0000000..8836e66 --- /dev/null +++ b/ccxt/static_dependencies/starknet/models/typed_data.py @@ -0,0 +1,45 @@ +""" +TypedDict structures for TypedData +""" + +from enum import Enum +from typing import Any, Dict, List, Optional, TypedDict + +class Revision(Enum): + """ + Enum representing the revision of the specification to be used. + """ + + V0 = 0 + V1 = 1 + + +class ParameterDict(TypedDict): + """ + TypedDict representing a Parameter object + """ + + name: str + type: str + + +class StarkNetDomainDict(TypedDict): + """ + TypedDict representing a domain object (both StarkNetDomain, StarknetDomain). + """ + + name: str + version: str + chainId: str + revision: Optional[Revision] + + +class TypedDataDict(TypedDict): + """ + TypedDict representing a TypedData object + """ + + types: Dict[str, List[ParameterDict]] + primaryType: str + domain: StarkNetDomainDict + message: Dict[str, Any] diff --git a/ccxt/static_dependencies/starknet/serialization/__init__.py b/ccxt/static_dependencies/starknet/serialization/__init__.py new file mode 100644 index 0000000..35dc440 --- /dev/null +++ b/ccxt/static_dependencies/starknet/serialization/__init__.py @@ -0,0 +1,24 @@ +# PayloadSerializer and FunctionSerializationAdapter would mostly be used by users +from .data_serializers import ( + ArraySerializer, + CairoDataSerializer, + FeltSerializer, + NamedTupleSerializer, + PayloadSerializer, + StructSerializer, + TupleSerializer, + Uint256Serializer, +) +from .errors import ( + CairoSerializerException, + InvalidTypeException, + InvalidValueException, +) +from .factory import ( + serializer_for_event, + serializer_for_function, + serializer_for_payload, + serializer_for_type, +) +from .function_serialization_adapter import FunctionSerializationAdapter +from .tuple_dataclass import TupleDataclass diff --git a/ccxt/static_dependencies/starknet/serialization/_calldata_reader.py b/ccxt/static_dependencies/starknet/serialization/_calldata_reader.py new file mode 100644 index 0000000..d6419de --- /dev/null +++ b/ccxt/static_dependencies/starknet/serialization/_calldata_reader.py @@ -0,0 +1,40 @@ +from typing import List + +from ..cairo.felt import CairoData + + +class OutOfBoundsError(Exception): + def __init__(self, position: int, requested_size: int, remaining_size: int): + super().__init__( + f"Requested {requested_size} elements, {remaining_size} available." + ) + self.position = position + self.requested_size = requested_size + self.remaining_len = remaining_size + + +class CalldataReader: + _data: List[int] + _position: int + + def __init__(self, data: List[int]): + self._data = data + self._position = 0 + + @property + def remaining_len(self) -> int: + return len(self._data) - self._position + + def read(self, size: int) -> CairoData: + if size < 1: + raise ValueError("size must be greater than 0") + + if size > self.remaining_len: + raise OutOfBoundsError( + position=self._position, + requested_size=size, + remaining_size=self.remaining_len, + ) + data = self._data[self._position : self._position + size] + self._position += size + return data diff --git a/ccxt/static_dependencies/starknet/serialization/_context.py b/ccxt/static_dependencies/starknet/serialization/_context.py new file mode 100644 index 0000000..b74c07c --- /dev/null +++ b/ccxt/static_dependencies/starknet/serialization/_context.py @@ -0,0 +1,142 @@ +from __future__ import annotations + +from abc import ABC +from contextlib import contextmanager +from typing import Any, Generator, Iterator, List + +from ._calldata_reader import ( + CairoData, + CalldataReader, + OutOfBoundsError, +) +from .errors import InvalidTypeException, InvalidValueException + + +class Context(ABC): + """ + Holds information about context when (de)serializing data. This is needed to inform what and where went + wrong during processing. Every separate (de)serialization should have its own context. + """ + + _namespace_stack: List[str] + + def __init__(self): + self._namespace_stack = [] + + @property + def current_entity(self): + """ + Name of currently processed entity. + + :return: transformed path. + """ + return ".".join(self._namespace_stack) + + @contextmanager + def push_entity(self, name: str) -> Generator: + """ + Manager used for maintaining information about names of (de)serialized types. Wraps some errors with + custom errors, adding information about the context. + + :param name: name of (de)serialized entity. + """ + # This ensures the name will be popped if everything is ok. In case an exception is raised we want the stack to + # be filled to wrap the error at the end. + self._namespace_stack.append(name) + yield + self._namespace_stack.pop() + + def ensure_valid_value(self, valid: bool, text: str): + if not valid: + raise InvalidValueException(f"{self._error_prefix}: {text}.") + + def ensure_valid_type(self, value: Any, valid: bool, expected_type: str): + if not valid: + raise InvalidTypeException( + f"{self._error_prefix}: expected {expected_type}, " + f"received '{value}' of type '{type(value)}'." + ) + + @contextmanager + def _wrap_errors(self): + try: + yield + except OutOfBoundsError as err: + action_name = ( + f"deserialize '{self.current_entity}'" + if self._namespace_stack + else "deserialize" + ) + # This way we can precisely inform user what's wrong when reading calldata. + raise InvalidValueException( + f"Not enough data to {action_name}. " + f"Can't read {err.requested_size} values at position {err.position}, {err.remaining_len} available." + ) from err + + # Those two are based on ValueError and TypeError, we have to catch them early + except (InvalidValueException, InvalidTypeException) as err: + raise err + + except ValueError as err: + raise InvalidValueException(f"{self._error_prefix}: {err}") from err + except TypeError as err: + raise InvalidTypeException(f"{self._error_prefix}: {err}") from err + + @property + def _error_prefix(self): + if not self._namespace_stack: + return "Error" + return f"Error at path '{self.current_entity}'" + + +class SerializationContext(Context): + """ + Context used during serialization. + """ + + # Type is iterator, because ContextManager doesn't work with pyright :| + # https://github.com/microsoft/pyright/issues/476 + @classmethod + @contextmanager + def create(cls) -> Iterator[SerializationContext]: + context = cls() + with context._wrap_errors(): + yield context + + +class DeserializationContext(Context): + """ + Context used during deserialization. + """ + + reader: CalldataReader + + def __init__(self, calldata: CairoData): + """ + Don't use default constructor. Use DeserializationContext.create context manager. + """ + super().__init__() + self._namespace_stack = [] + self.reader = CalldataReader(calldata) + + @classmethod + @contextmanager + def create(cls, data: CairoData) -> Iterator[DeserializationContext]: + context = cls(data) + with context._wrap_errors(): + yield context + context._ensure_all_values_read(len(data)) + + def _ensure_all_values_read(self, total_len: int): + values_not_used = self.reader.remaining_len + if values_not_used != 0: + # We want to output up to 3 values. It there is more they will be truncated like "0x1,0x1,0x1..." + max_values_to_show = 3 + values_to_show = min(values_not_used, max_values_to_show) + example = ",".join(hex(v) for v in self.reader.read(values_to_show)) + suffix = "..." if values_not_used > max_values_to_show else "" + + raise InvalidValueException( + f"Last {values_not_used} values '{example}{suffix}' out of total {total_len} " + "values were not used during deserialization." + ) diff --git a/ccxt/static_dependencies/starknet/serialization/data_serializers/__init__.py b/ccxt/static_dependencies/starknet/serialization/data_serializers/__init__.py new file mode 100644 index 0000000..f8a7111 --- /dev/null +++ b/ccxt/static_dependencies/starknet/serialization/data_serializers/__init__.py @@ -0,0 +1,10 @@ +from .array_serializer import ArraySerializer +from .bool_serializer import BoolSerializer +from .byte_array_serializer import ByteArraySerializer +from .cairo_data_serializer import CairoDataSerializer +from .felt_serializer import FeltSerializer +from .named_tuple_serializer import NamedTupleSerializer +from .payload_serializer import PayloadSerializer +from .struct_serializer import StructSerializer +from .tuple_serializer import TupleSerializer +from .uint256_serializer import Uint256Serializer diff --git a/ccxt/static_dependencies/starknet/serialization/data_serializers/_common.py b/ccxt/static_dependencies/starknet/serialization/data_serializers/_common.py new file mode 100644 index 0000000..2e94786 --- /dev/null +++ b/ccxt/static_dependencies/starknet/serialization/data_serializers/_common.py @@ -0,0 +1,82 @@ +# We have to use parametrised type from typing +from collections import OrderedDict as _OrderedDict +from typing import Dict, Generator, List, OrderedDict + +from .._context import ( + DeserializationContext, + SerializationContext, +) +from .cairo_data_serializer import ( + CairoDataSerializer, +) + +# The actual serialization logic is very similar among all serializers: they either serialize data based on +# position or their name. Having this logic reused adds indirection, but makes sure proper logic is used everywhere. + + +def deserialize_to_list( + deserializers: List[CairoDataSerializer], context: DeserializationContext +) -> List: + """ + Deserializes data from context to list. This logic is used in every sequential type (arrays and tuples). + """ + result = [] + + for index, serializer in enumerate(deserializers): + with context.push_entity(f"[{index}]"): + result.append(serializer.deserialize_with_context(context)) + + return result + + +def deserialize_to_dict( + deserializers: OrderedDict[str, CairoDataSerializer], + context: DeserializationContext, +) -> OrderedDict: + """ + Deserializes data from context to dictionary. This logic is used in every type with named fields (structs, + named tuples and payloads). + """ + result = _OrderedDict() + + for key, serializer in deserializers.items(): + with context.push_entity(key): + result[key] = serializer.deserialize_with_context(context) + + return result + + +def serialize_from_list( + serializers: List[CairoDataSerializer], context: SerializationContext, values: List +) -> Generator[int, None, None]: + """ + Serializes data from list. This logic is used in every sequential type (arrays and tuples). + """ + context.ensure_valid_value( + len(serializers) == len(values), + f"expected {len(serializers)} elements, {len(values)} provided", + ) + + for index, (serializer, value) in enumerate(zip(serializers, values)): + with context.push_entity(f"[{index}]"): + yield from serializer.serialize_with_context(context, value) + + +def serialize_from_dict( + serializers: OrderedDict[str, CairoDataSerializer], + context: SerializationContext, + values: Dict, +) -> Generator[int, None, None]: + """ + Serializes data from dict. This logic is used in every type with named fields (structs, named tuples and payloads). + """ + excessive_keys = set(values.keys()).difference(serializers.keys()) + context.ensure_valid_value( + not excessive_keys, + f"unexpected keys '{','.join(excessive_keys)}' were provided", + ) + + for name, serializer in serializers.items(): + with context.push_entity(name): + context.ensure_valid_value(name in values, f"key '{name}' is missing") + yield from serializer.serialize_with_context(context, values[name]) diff --git a/ccxt/static_dependencies/starknet/serialization/data_serializers/array_serializer.py b/ccxt/static_dependencies/starknet/serialization/data_serializers/array_serializer.py new file mode 100644 index 0000000..7fadb6d --- /dev/null +++ b/ccxt/static_dependencies/starknet/serialization/data_serializers/array_serializer.py @@ -0,0 +1,43 @@ +from dataclasses import dataclass +from typing import Generator, Iterable, List + +from .._context import ( + DeserializationContext, + SerializationContext, +) +from ..data_serializers._common import ( + deserialize_to_list, + serialize_from_list, +) +from ..data_serializers.cairo_data_serializer import ( + CairoDataSerializer, +) + + +@dataclass +class ArraySerializer(CairoDataSerializer[Iterable, List]): + """ + Serializer for arrays. In abi they are represented as a pointer to a type. + Can serialize any iterable and prepends its length to resulting list. + Deserializes data to a list. + + Examples: + [1,2,3] => [3,1,2,3] + [] => [0] + """ + + inner_serializer: CairoDataSerializer + + def deserialize_with_context(self, context: DeserializationContext) -> List: + with context.push_entity("len"): + [size] = context.reader.read(1) + + return deserialize_to_list([self.inner_serializer] * size, context) + + def serialize_with_context( + self, context: SerializationContext, value: List + ) -> Generator[int, None, None]: + yield len(value) + yield from serialize_from_list( + [self.inner_serializer] * len(value), context, value + ) diff --git a/ccxt/static_dependencies/starknet/serialization/data_serializers/bool_serializer.py b/ccxt/static_dependencies/starknet/serialization/data_serializers/bool_serializer.py new file mode 100644 index 0000000..2cdad03 --- /dev/null +++ b/ccxt/static_dependencies/starknet/serialization/data_serializers/bool_serializer.py @@ -0,0 +1,37 @@ +from dataclasses import dataclass +from typing import Generator + +from .._context import ( + Context, + DeserializationContext, + SerializationContext, +) +from .cairo_data_serializer import ( + CairoDataSerializer, +) + + +@dataclass +class BoolSerializer(CairoDataSerializer[bool, int]): + """ + Serializer for boolean. + """ + + def deserialize_with_context(self, context: DeserializationContext) -> bool: + [val] = context.reader.read(1) + self._ensure_bool(context, val) + return bool(val) + + def serialize_with_context( + self, context: SerializationContext, value: bool + ) -> Generator[int, None, None]: + context.ensure_valid_type(value, isinstance(value, bool), "bool") + self._ensure_bool(context, value) + yield int(value) + + @staticmethod + def _ensure_bool(context: Context, value: int): + context.ensure_valid_value( + value in [0, 1], + f"invalid value '{value}' - must be in [0, 2) range", + ) diff --git a/ccxt/static_dependencies/starknet/serialization/data_serializers/byte_array_serializer.py b/ccxt/static_dependencies/starknet/serialization/data_serializers/byte_array_serializer.py new file mode 100644 index 0000000..ff95d58 --- /dev/null +++ b/ccxt/static_dependencies/starknet/serialization/data_serializers/byte_array_serializer.py @@ -0,0 +1,66 @@ +from dataclasses import dataclass +from typing import Generator + +from ...cairo.felt import decode_shortstring, encode_shortstring +from .._context import ( + DeserializationContext, + SerializationContext, +) +from ._common import ( + deserialize_to_list, + serialize_from_list, +) +from .cairo_data_serializer import ( + CairoDataSerializer, +) +from .felt_serializer import FeltSerializer + +BYTES_31_SIZE = 31 + + +@dataclass +class ByteArraySerializer(CairoDataSerializer[str, str]): + """ + Serializer for ByteArrays. Serializes to and deserializes from str values. + + Examples: + "" => [0,0,0] + "hello" => [0,448378203247,5] + """ + + def deserialize_with_context(self, context: DeserializationContext) -> str: + with context.push_entity("data_array_len"): + [size] = context.reader.read(1) + + data = deserialize_to_list([FeltSerializer()] * size, context) + + with context.push_entity("pending_word"): + [pending_word] = context.reader.read(1) + + with context.push_entity("pending_word_len"): + [pending_word_len] = context.reader.read(1) + + pending_word = decode_shortstring(pending_word) + context.ensure_valid_value( + len(pending_word) == pending_word_len, + f"Invalid length {pending_word_len} for pending word {pending_word}", + ) + + data_joined = "".join(map(decode_shortstring, data)) + return data_joined + pending_word + + def serialize_with_context( + self, context: SerializationContext, value: str + ) -> Generator[int, None, None]: + context.ensure_valid_type(value, isinstance(value, str), "str") + data = [ + value[i : i + BYTES_31_SIZE] for i in range(0, len(value), BYTES_31_SIZE) + ] + pending_word = ( + "" if len(data) == 0 or len(data[-1]) == BYTES_31_SIZE else data.pop(-1) + ) + + yield len(data) + yield from serialize_from_list([FeltSerializer()] * len(data), context, data) + yield encode_shortstring(pending_word) + yield len(pending_word) diff --git a/ccxt/static_dependencies/starknet/serialization/data_serializers/cairo_data_serializer.py b/ccxt/static_dependencies/starknet/serialization/data_serializers/cairo_data_serializer.py new file mode 100644 index 0000000..b5af751 --- /dev/null +++ b/ccxt/static_dependencies/starknet/serialization/data_serializers/cairo_data_serializer.py @@ -0,0 +1,71 @@ +from abc import ABC, abstractmethod +from typing import Generator, Generic, List, TypeVar + +from .._calldata_reader import CairoData +from .._context import ( + DeserializationContext, + SerializationContext, +) + +# Python type that is accepted by a serializer +# pylint: disable=invalid-name +SerializationType = TypeVar("SerializationType") + +# Python type that will be returned from a serializer. Often same as SerializationType. +# pylint: disable=invalid-name +DeserializationType = TypeVar("DeserializationType") + + +class CairoDataSerializer(ABC, Generic[SerializationType, DeserializationType]): + """ + Base class for serializing/deserializing data to/from calldata. + """ + + def deserialize(self, data: List[int]) -> DeserializationType: + """ + Transform calldata into python value. + + :param data: calldata to deserialize. + :return: defined DeserializationType. + """ + with DeserializationContext.create(data) as context: + return self.deserialize_with_context(context) + + def serialize(self, data: SerializationType) -> CairoData: + """ + Transform python data into calldata. + + :param data: data to serialize. + :return: calldata. + """ + with SerializationContext.create() as context: + serialized_data = list(self.serialize_with_context(context, data)) + + return self.remove_units_from_serialized_data(serialized_data) + + @abstractmethod + def deserialize_with_context( + self, context: DeserializationContext + ) -> DeserializationType: + """ + Transform calldata into python value. + + :param context: context of this deserialization. + :return: defined DeserializationType. + """ + + @abstractmethod + def serialize_with_context( + self, context: SerializationContext, value: SerializationType + ) -> Generator[int, None, None]: + """ + Transform python value into calldata. + + :param context: context of this serialization. + :param value: python value to serialize. + :return: defined SerializationType. + """ + + @staticmethod + def remove_units_from_serialized_data(serialized_data: List) -> List: + return [x for x in serialized_data if x is not None] diff --git a/ccxt/static_dependencies/starknet/serialization/data_serializers/enum_serializer.py b/ccxt/static_dependencies/starknet/serialization/data_serializers/enum_serializer.py new file mode 100644 index 0000000..5db1840 --- /dev/null +++ b/ccxt/static_dependencies/starknet/serialization/data_serializers/enum_serializer.py @@ -0,0 +1,71 @@ +from dataclasses import dataclass +from typing import Dict, Generator, OrderedDict, Tuple, Union + +from .._context import ( + DeserializationContext, + SerializationContext, +) +from .cairo_data_serializer import ( + CairoDataSerializer, +) +from ..tuple_dataclass import TupleDataclass + + +@dataclass +class EnumSerializer(CairoDataSerializer[Union[Dict, TupleDataclass], TupleDataclass]): + """ + Serializer of enums. + Can serialize a dictionary and TupleDataclass. + Deserializes data to a TupleDataclass. + + Example: + enum MyEnum { + a: u128, + b: u128 + } + + {"a": 1} => [0, 1] + {"b": 100} => [1, 100] + TupleDataclass(variant='a', value=100) => [0, 100] + """ + + serializers: OrderedDict[str, CairoDataSerializer] + + def deserialize_with_context( + self, context: DeserializationContext + ) -> TupleDataclass: + [variant_index] = context.reader.read(1) + variant_name, serializer = self._get_variant(variant_index) + + with context.push_entity("enum.variant: " + variant_name): + result_dict = { + "variant": variant_name, + "value": serializer.deserialize_with_context(context), + } + + return TupleDataclass.from_dict(result_dict) + + def serialize_with_context( + self, context: SerializationContext, value: Union[Dict, TupleDataclass] + ) -> Generator[int, None, None]: + if isinstance(value, Dict): + items = list(value.items()) + if len(items) != 1: + raise ValueError( + "Can serialize only one enum variant, got: " + str(len(items)) + ) + + variant_name, variant_value = items[0] + else: + variant_name, variant_value = value + + yield self._get_variant_index(variant_name) + yield from self.serializers[variant_name].serialize_with_context( + context, variant_value + ) + + def _get_variant(self, variant_index: int) -> Tuple[str, CairoDataSerializer]: + return list(self.serializers.items())[variant_index] + + def _get_variant_index(self, variant_name: str) -> int: + return list(self.serializers.keys()).index(variant_name) diff --git a/ccxt/static_dependencies/starknet/serialization/data_serializers/felt_serializer.py b/ccxt/static_dependencies/starknet/serialization/data_serializers/felt_serializer.py new file mode 100644 index 0000000..23653b8 --- /dev/null +++ b/ccxt/static_dependencies/starknet/serialization/data_serializers/felt_serializer.py @@ -0,0 +1,50 @@ +import warnings +from dataclasses import dataclass +from typing import Generator + +from ...cairo.felt import encode_shortstring, is_in_felt_range +from ...constants import FIELD_PRIME +from .._context import ( + Context, + DeserializationContext, + SerializationContext, +) +from .cairo_data_serializer import ( + CairoDataSerializer, +) + + +@dataclass +class FeltSerializer(CairoDataSerializer[int, int]): + """ + Serializer for field element. At the time of writing it is the only existing numeric type. + """ + + def deserialize_with_context(self, context: DeserializationContext) -> int: + [val] = context.reader.read(1) + self._ensure_felt(context, val) + return val + + def serialize_with_context( + self, context: SerializationContext, value: int + ) -> Generator[int, None, None]: + if isinstance(value, str): + warnings.warn( + "Serializing shortstrings in FeltSerializer is deprecated. " + "Use starknet_py.cairo.felt.encode_shortstring instead.", + category=DeprecationWarning, + ) + value = encode_shortstring(value) + yield value + return + + context.ensure_valid_type(value, isinstance(value, int), "int") + self._ensure_felt(context, value) + yield value + + @staticmethod + def _ensure_felt(context: Context, value: int): + context.ensure_valid_value( + is_in_felt_range(value), + f"invalid value '{value}' - must be in [0, {FIELD_PRIME}) range", + ) diff --git a/ccxt/static_dependencies/starknet/serialization/data_serializers/named_tuple_serializer.py b/ccxt/static_dependencies/starknet/serialization/data_serializers/named_tuple_serializer.py new file mode 100644 index 0000000..586a612 --- /dev/null +++ b/ccxt/static_dependencies/starknet/serialization/data_serializers/named_tuple_serializer.py @@ -0,0 +1,58 @@ +from dataclasses import dataclass +from typing import Dict, Generator, NamedTuple, OrderedDict, Union + +from .._context import ( + DeserializationContext, + SerializationContext, +) +from ._common import ( + deserialize_to_dict, + serialize_from_dict, +) +from .cairo_data_serializer import ( + CairoDataSerializer, +) +from ..tuple_dataclass import TupleDataclass + + +@dataclass +class NamedTupleSerializer( + CairoDataSerializer[Union[Dict, NamedTuple, TupleDataclass], TupleDataclass] +): + """ + Serializer for tuples with named fields. + Can serialize a dictionary, a named tuple and TupleDataclass. + Deserializes data to a TupleDataclass. + + Example: + {"a": 1, "b": 2} => [1,2] + """ + + serializers: OrderedDict[str, CairoDataSerializer] + + def deserialize_with_context( + self, context: DeserializationContext + ) -> TupleDataclass: + as_dictionary = deserialize_to_dict(self.serializers, context) + return TupleDataclass.from_dict(as_dictionary) + + def serialize_with_context( + self, + context: SerializationContext, + value: Union[Dict, NamedTuple, TupleDataclass], + ) -> Generator[int, None, None]: + # We can't use isinstance(value, NamedTuple), because there is no NamedTuple type. + context.ensure_valid_type( + value, + isinstance(value, (dict, TupleDataclass)) or self._is_namedtuple(value), + "dict, NamedTuple or TupleDataclass", + ) + + # noinspection PyUnresolvedReferences, PyProtectedMember + values: Dict = value if isinstance(value, dict) else value._asdict() + + yield from serialize_from_dict(self.serializers, context, values) + + @staticmethod + def _is_namedtuple(value) -> bool: + return isinstance(value, tuple) and hasattr(value, "_fields") diff --git a/ccxt/static_dependencies/starknet/serialization/data_serializers/option_serializer.py b/ccxt/static_dependencies/starknet/serialization/data_serializers/option_serializer.py new file mode 100644 index 0000000..d95e38a --- /dev/null +++ b/ccxt/static_dependencies/starknet/serialization/data_serializers/option_serializer.py @@ -0,0 +1,43 @@ +from dataclasses import dataclass +from typing import Any, Generator, Optional + +from .._context import ( + DeserializationContext, + SerializationContext, +) +from .cairo_data_serializer import ( + CairoDataSerializer, +) + + +@dataclass +class OptionSerializer(CairoDataSerializer[Optional[Any], Optional[Any]]): + """ + Serializer for Option type. + Can serialize None and common CairoTypes. + Deserializes data to None or CairoType. + + Example: + None => [1] + {"option1": 123, "option2": None} => [0, 123, 1] + """ + + serializer: CairoDataSerializer + + def deserialize_with_context( + self, context: DeserializationContext + ) -> Optional[Any]: + (is_none,) = context.reader.read(1) + if is_none == 1: + return None + + return self.serializer.deserialize_with_context(context) + + def serialize_with_context( + self, context: SerializationContext, value: Optional[Any] + ) -> Generator[int, None, None]: + if value is None: + yield 1 + else: + yield 0 + yield from self.serializer.serialize_with_context(context, value) diff --git a/ccxt/static_dependencies/starknet/serialization/data_serializers/output_serializer.py b/ccxt/static_dependencies/starknet/serialization/data_serializers/output_serializer.py new file mode 100644 index 0000000..a397986 --- /dev/null +++ b/ccxt/static_dependencies/starknet/serialization/data_serializers/output_serializer.py @@ -0,0 +1,40 @@ +from dataclasses import dataclass, field +from typing import Dict, Generator, List, Tuple + +from .._context import ( + DeserializationContext, + SerializationContext, +) +from .cairo_data_serializer import ( + CairoDataSerializer, +) + + +@dataclass +class OutputSerializer(CairoDataSerializer[List, Tuple]): + """ + Serializer for function output. + Can't serialize anything. + Deserializes data to a Tuple. + + Example: + [1, 1, 1] => (340282366920938463463374607431768211457) + """ + + serializers: List[CairoDataSerializer] = field(init=True) + + def deserialize_with_context(self, context: DeserializationContext) -> Tuple: + result = [] + + for index, serializer in enumerate(self.serializers): + with context.push_entity("output[" + str(index) + "]"): + result.append(serializer.deserialize_with_context(context)) + + return tuple(result) + + def serialize_with_context( + self, context: SerializationContext, value: Dict + ) -> Generator[int, None, None]: + raise ValueError( + "Output serializer can't be used to transform python data into calldata." + ) diff --git a/ccxt/static_dependencies/starknet/serialization/data_serializers/payload_serializer.py b/ccxt/static_dependencies/starknet/serialization/data_serializers/payload_serializer.py new file mode 100644 index 0000000..3ca3957 --- /dev/null +++ b/ccxt/static_dependencies/starknet/serialization/data_serializers/payload_serializer.py @@ -0,0 +1,72 @@ +from collections import OrderedDict as _OrderedDict +from dataclasses import InitVar, dataclass, field +from typing import Dict, Generator, OrderedDict + +from .._context import ( + DeserializationContext, + SerializationContext, +) +from ._common import ( + deserialize_to_dict, + serialize_from_dict, +) +from .array_serializer import ArraySerializer +from .cairo_data_serializer import ( + CairoDataSerializer, +) +from .felt_serializer import FeltSerializer +from ..tuple_dataclass import TupleDataclass + +SIZE_SUFFIX = "_len" +SIZE_SUFFIX_LEN = len(SIZE_SUFFIX) + + +@dataclass +class PayloadSerializer(CairoDataSerializer[Dict, TupleDataclass]): + """ + Serializer for payloads like function arguments/function outputs/events. + Can serialize a dictionary. + Deserializes data to a TupleDataclass. + + Example: + {"a": 1, "b": 2} => [1,2] + """ + + # Value present only in constructor. + # We don't want to mutate the serializers received in constructor. + input_serializers: InitVar[OrderedDict[str, CairoDataSerializer]] + + serializers: OrderedDict[str, CairoDataSerializer] = field(init=False) + + def __post_init__(self, input_serializers): + """ + ABI adds ARG_len for every argument ARG that is an array. We parse length as a part of ArraySerializer, so we + need to remove those lengths from args. + """ + self.serializers = _OrderedDict( + (key, serializer) + for key, serializer in input_serializers.items() + if not self._is_len_arg(key, input_serializers) + ) + + def deserialize_with_context( + self, context: DeserializationContext + ) -> TupleDataclass: + as_dictionary = deserialize_to_dict(self.serializers, context) + return TupleDataclass.from_dict(as_dictionary) + + def serialize_with_context( + self, context: SerializationContext, value: Dict + ) -> Generator[int, None, None]: + yield from serialize_from_dict(self.serializers, context, value) + + @staticmethod + def _is_len_arg(arg_name: str, serializers: Dict[str, CairoDataSerializer]) -> bool: + return ( + arg_name.endswith(SIZE_SUFFIX) + and isinstance(serializers[arg_name], FeltSerializer) + # There is an ArraySerializer under key that is arg_name without the size suffix + and isinstance( + serializers.get(arg_name[:-SIZE_SUFFIX_LEN]), ArraySerializer + ) + ) diff --git a/ccxt/static_dependencies/starknet/serialization/data_serializers/struct_serializer.py b/ccxt/static_dependencies/starknet/serialization/data_serializers/struct_serializer.py new file mode 100644 index 0000000..7a38812 --- /dev/null +++ b/ccxt/static_dependencies/starknet/serialization/data_serializers/struct_serializer.py @@ -0,0 +1,36 @@ +from dataclasses import dataclass +from typing import Dict, Generator, OrderedDict + +from .._context import ( + DeserializationContext, + SerializationContext, +) +from ._common import ( + deserialize_to_dict, + serialize_from_dict, +) +from .cairo_data_serializer import ( + CairoDataSerializer, +) + + +@dataclass +class StructSerializer(CairoDataSerializer[Dict, Dict]): + """ + Serializer of custom structures. + Can serialize a dictionary. + Deserializes data to a dictionary. + + Example: + {"a": 1, "b": 2} => [1,2] + """ + + serializers: OrderedDict[str, CairoDataSerializer] + + def deserialize_with_context(self, context: DeserializationContext) -> Dict: + return deserialize_to_dict(self.serializers, context) + + def serialize_with_context( + self, context: SerializationContext, value: Dict + ) -> Generator[int, None, None]: + yield from serialize_from_dict(self.serializers, context, value) diff --git a/ccxt/static_dependencies/starknet/serialization/data_serializers/tuple_serializer.py b/ccxt/static_dependencies/starknet/serialization/data_serializers/tuple_serializer.py new file mode 100644 index 0000000..b27745b --- /dev/null +++ b/ccxt/static_dependencies/starknet/serialization/data_serializers/tuple_serializer.py @@ -0,0 +1,36 @@ +from dataclasses import dataclass +from typing import Generator, Iterable, List, Tuple + +from .._context import ( + DeserializationContext, + SerializationContext, +) +from ._common import ( + deserialize_to_list, + serialize_from_list, +) +from .cairo_data_serializer import ( + CairoDataSerializer, +) + + +@dataclass +class TupleSerializer(CairoDataSerializer[Iterable, Tuple]): + """ + Serializer for tuples without named fields. + Can serialize any iterable. + Deserializes data to a python tuple. + + Example: + (1,2,(3,4)) => [1,2,3,4] + """ + + serializers: List[CairoDataSerializer] + + def deserialize_with_context(self, context: DeserializationContext) -> Tuple: + return tuple(deserialize_to_list(self.serializers, context)) + + def serialize_with_context( + self, context: SerializationContext, value: Iterable + ) -> Generator[int, None, None]: + yield from serialize_from_list(self.serializers, context, [*value]) diff --git a/ccxt/static_dependencies/starknet/serialization/data_serializers/uint256_serializer.py b/ccxt/static_dependencies/starknet/serialization/data_serializers/uint256_serializer.py new file mode 100644 index 0000000..7962521 --- /dev/null +++ b/ccxt/static_dependencies/starknet/serialization/data_serializers/uint256_serializer.py @@ -0,0 +1,76 @@ +from dataclasses import dataclass +from typing import Generator, TypedDict, Union + +from ...cairo.felt import uint256_range_check +from .._context import ( + Context, + DeserializationContext, + SerializationContext, +) +from .cairo_data_serializer import ( + CairoDataSerializer, +) + +U128_UPPER_BOUND = 2**128 + + +class Uint256Dict(TypedDict): + low: int + high: int + + +@dataclass +class Uint256Serializer(CairoDataSerializer[Union[int, Uint256Dict], int]): + """ + Serializer of Uint256. In Cairo it is represented by structure {low: Uint128, high: Uint128}. + Can serialize an int. + Deserializes data to an int. + + Examples: + 0 => [0,0] + 1 => [1,0] + 2**128 => [0,1] + 3 + 2**128 => [3,1] + """ + + def deserialize_with_context(self, context: DeserializationContext) -> int: + [low, high] = context.reader.read(2) + + # Checking if resulting value is in [0, 2**256) range is not enough. Uint256 should be made of two uint128. + with context.push_entity("low"): + self._ensure_valid_uint128(low, context) + with context.push_entity("high"): + self._ensure_valid_uint128(high, context) + + return (high << 128) + low + + def serialize_with_context( + self, context: SerializationContext, value: Union[int, Uint256Dict] + ) -> Generator[int, None, None]: + context.ensure_valid_type(value, isinstance(value, (int, dict)), "int or dict") + if isinstance(value, int): + yield from self._serialize_from_int(value) + else: + yield from self._serialize_from_dict(context, value) + + @staticmethod + def _serialize_from_int(value: int) -> Generator[int, None, None]: + uint256_range_check(value) + result = (value % 2**128, value // 2**128) + yield from result + + def _serialize_from_dict( + self, context: SerializationContext, value: Uint256Dict + ) -> Generator[int, None, None]: + with context.push_entity("low"): + self._ensure_valid_uint128(value["low"], context) + yield value["low"] + with context.push_entity("high"): + self._ensure_valid_uint128(value["high"], context) + yield value["high"] + + @staticmethod + def _ensure_valid_uint128(value: int, context: Context): + context.ensure_valid_value( + 0 <= value < U128_UPPER_BOUND, "expected value in range [0;2**128)" + ) diff --git a/ccxt/static_dependencies/starknet/serialization/data_serializers/uint_serializer.py b/ccxt/static_dependencies/starknet/serialization/data_serializers/uint_serializer.py new file mode 100644 index 0000000..2d9429f --- /dev/null +++ b/ccxt/static_dependencies/starknet/serialization/data_serializers/uint_serializer.py @@ -0,0 +1,100 @@ +from dataclasses import dataclass +from typing import Generator, TypedDict, Union + +from ...cairo.felt import uint256_range_check +from .._context import ( + Context, + DeserializationContext, + SerializationContext, +) +from .cairo_data_serializer import ( + CairoDataSerializer, +) + + +class Uint256Dict(TypedDict): + low: int + high: int + + +@dataclass +class UintSerializer(CairoDataSerializer[Union[int, Uint256Dict], int]): + """ + Serializer of uint. In Cairo there are few uints (u8, ..., u128 and u256). + u256 is represented by structure {low: u128, high: u128}. + Can serialize an int and dict. + Deserializes data to an int. + + Examples: + if bits < 256: + 0 => [0] + 1 => [1] + 2**128-1 => [2**128-1] + else: + 0 => [0,0] + 1 => [1,0] + 2**128 => [0,1] + 3 + 2**128 => [3,1] + """ + + bits: int + + def deserialize_with_context(self, context: DeserializationContext) -> int: + if self.bits < 256: + (uint,) = context.reader.read(1) + with context.push_entity("uint" + str(self.bits)): + self._ensure_valid_uint(uint, context, self.bits) + + return uint + + [low, high] = context.reader.read(2) + + # Checking if resulting value is in [0, 2**256) range is not enough. Uint256 should be made of two uint128. + with context.push_entity("low"): + self._ensure_valid_uint(low, context, bits=128) + with context.push_entity("high"): + self._ensure_valid_uint(high, context, bits=128) + + return (high << 128) + low + + def serialize_with_context( + self, context: SerializationContext, value: Union[int, Uint256Dict] + ) -> Generator[int, None, None]: + context.ensure_valid_type(value, isinstance(value, (int, dict)), "int or dict") + if isinstance(value, int): + yield from self._serialize_from_int(value, context, self.bits) + else: + yield from self._serialize_from_dict(context, value) + + @staticmethod + def _serialize_from_int( + value: int, context: SerializationContext, bits: int + ) -> Generator[int, None, None]: + if bits < 256: + UintSerializer._ensure_valid_uint(value, context, bits) + + yield value + else: + uint256_range_check(value) + + result = (value % 2**128, value >> 128) + yield from result + + def _serialize_from_dict( + self, context: SerializationContext, value: Uint256Dict + ) -> Generator[int, None, None]: + with context.push_entity("low"): + self._ensure_valid_uint(value["low"], context, bits=128) + yield value["low"] + with context.push_entity("high"): + self._ensure_valid_uint(value["high"], context, bits=128) + yield value["high"] + + @staticmethod + def _ensure_valid_uint(value: int, context: Context, bits: int): + """ + Ensures that value is a valid uint on `bits` bits. + """ + context.ensure_valid_value( + 0 <= value < 2**bits, "expected value in range [0;2**" + str(bits) + ")" + ) diff --git a/ccxt/static_dependencies/starknet/serialization/data_serializers/unit_serializer.py b/ccxt/static_dependencies/starknet/serialization/data_serializers/unit_serializer.py new file mode 100644 index 0000000..248e192 --- /dev/null +++ b/ccxt/static_dependencies/starknet/serialization/data_serializers/unit_serializer.py @@ -0,0 +1,32 @@ +from dataclasses import dataclass +from typing import Any, Generator, Optional + +from .._context import ( + DeserializationContext, + SerializationContext, +) +from .cairo_data_serializer import ( + CairoDataSerializer, +) + + +@dataclass +class UnitSerializer(CairoDataSerializer[None, None]): + """ + Serializer for unit type. + Can only serialize None. + Deserializes data to None. + + Example: + [] => None + """ + + def deserialize_with_context(self, context: DeserializationContext) -> None: + return None + + def serialize_with_context( + self, context: SerializationContext, value: Optional[Any] + ) -> Generator[None, None, None]: + if value is not None: + raise ValueError("Can only serialize `None`.") + yield None diff --git a/ccxt/static_dependencies/starknet/serialization/errors.py b/ccxt/static_dependencies/starknet/serialization/errors.py new file mode 100644 index 0000000..8eeabe8 --- /dev/null +++ b/ccxt/static_dependencies/starknet/serialization/errors.py @@ -0,0 +1,10 @@ +class CairoSerializerException(Exception): + """Exception thrown by CairoSerializer.""" + + +class InvalidTypeException(CairoSerializerException, TypeError): + """Exception thrown when invalid type was provided.""" + + +class InvalidValueException(CairoSerializerException, ValueError): + """Exception thrown when invalid value was provided.""" diff --git a/ccxt/static_dependencies/starknet/serialization/factory.py b/ccxt/static_dependencies/starknet/serialization/factory.py new file mode 100644 index 0000000..a086d4d --- /dev/null +++ b/ccxt/static_dependencies/starknet/serialization/factory.py @@ -0,0 +1,229 @@ +from __future__ import annotations + +from collections import OrderedDict +from typing import Dict, List, Union + +from ..abi.v0 import Abi as AbiV0 +from ..abi.v1 import Abi as AbiV1 +from ..abi.v2 import Abi as AbiV2 +from ..cairo.data_types import ( + ArrayType, + BoolType, + CairoType, + EnumType, + EventType, + FeltType, + NamedTupleType, + OptionType, + StructType, + TupleType, + UintType, + UnitType, +) +from .data_serializers import ( + BoolSerializer, + ByteArraySerializer, +) +from .data_serializers.array_serializer import ArraySerializer +from .data_serializers.cairo_data_serializer import ( + CairoDataSerializer, +) +from .data_serializers.enum_serializer import EnumSerializer +from .data_serializers.felt_serializer import FeltSerializer +from .data_serializers.named_tuple_serializer import ( + NamedTupleSerializer, +) +from .data_serializers.option_serializer import ( + OptionSerializer, +) +from .data_serializers.output_serializer import ( + OutputSerializer, +) +from .data_serializers.payload_serializer import ( + PayloadSerializer, +) +from .data_serializers.struct_serializer import ( + StructSerializer, +) +from .data_serializers.tuple_serializer import TupleSerializer +from .data_serializers.uint256_serializer import ( + Uint256Serializer, +) +from .data_serializers.uint_serializer import UintSerializer +from .data_serializers.unit_serializer import UnitSerializer +from .errors import InvalidTypeException +from .function_serialization_adapter import ( + FunctionSerializationAdapter, + FunctionSerializationAdapterV1, +) + +_uint256_type = StructType("Uint256", OrderedDict(low=FeltType(), high=FeltType())) +_byte_array_type = StructType( + "core::byte_array::ByteArray", + OrderedDict( + data=ArrayType(FeltType()), + pending_word=FeltType(), + pending_word_len=UintType(bits=32), + ), +) + + +def serializer_for_type(cairo_type: CairoType) -> CairoDataSerializer: + """ + Create a serializer for cairo type. + + :param cairo_type: CairoType. + :return: CairoDataSerializer. + """ + # pylint: disable=too-many-return-statements, too-many-branches + if isinstance(cairo_type, FeltType): + return FeltSerializer() + + if isinstance(cairo_type, BoolType): + return BoolSerializer() + + if isinstance(cairo_type, StructType): + # Special case: Uint256 is represented as struct + if cairo_type == _uint256_type: + return Uint256Serializer() + + if cairo_type == _byte_array_type: + return ByteArraySerializer() + + return StructSerializer( + OrderedDict( + (name, serializer_for_type(member_type)) + for name, member_type in cairo_type.types.items() + ) + ) + + if isinstance(cairo_type, ArrayType): + return ArraySerializer(serializer_for_type(cairo_type.inner_type)) + + if isinstance(cairo_type, TupleType): + return TupleSerializer( + [serializer_for_type(member) for member in cairo_type.types] + ) + + if isinstance(cairo_type, NamedTupleType): + return NamedTupleSerializer( + OrderedDict( + (name, serializer_for_type(member_type)) + for name, member_type in cairo_type.types.items() + ) + ) + + if isinstance(cairo_type, UintType): + return UintSerializer(bits=cairo_type.bits) + + if isinstance(cairo_type, OptionType): + return OptionSerializer(serializer_for_type(cairo_type.type)) + + if isinstance(cairo_type, UnitType): + return UnitSerializer() + + if isinstance(cairo_type, EnumType): + return EnumSerializer( + OrderedDict( + (name, serializer_for_type(variant_type)) + for name, variant_type in cairo_type.variants.items() + ) + ) + if isinstance(cairo_type, EventType): + return serializer_for_payload(cairo_type.types) + + raise InvalidTypeException(f"Received unknown Cairo type '{cairo_type}'.") + + +# We don't want to require users to use OrderedDict. Regular python requires order since python 3.7. +def serializer_for_payload(payload: Dict[str, CairoType]) -> PayloadSerializer: + """ + Create PayloadSerializer for types listed in a dictionary. Please note that the order of fields in the dict is + very important. Make sure the keys are provided in the right order. + + :param payload: dictionary with cairo types. + :return: PayloadSerializer that can be used to (de)serialize events/function calls. + """ + return PayloadSerializer( + OrderedDict( + (name, serializer_for_type(cairo_type)) + for name, cairo_type in payload.items() + ) + ) + + +def serializer_for_outputs(payload: List[CairoType]) -> OutputSerializer: + """ + Create OutputSerializer for types in list. Please note that the order of fields in the list is + very important. Make sure the types are provided in the right order. + + :param payload: list with cairo types. + :return: OutputSerializer that can be used to deserialize function outputs. + """ + return OutputSerializer( + serializers=[serializer_for_type(cairo_type) for cairo_type in payload] + ) + + +EventV0 = AbiV0.Event +EventV1 = AbiV1.Event +EventV2 = EventType + + +def serializer_for_event(event: EventV0 | EventV1 | EventV2) -> PayloadSerializer: + """ + Create serializer for an event. + + :param event: parsed event. + :return: PayloadSerializer that can be used to (de)serialize events. + """ + if isinstance(event, EventV0): + return serializer_for_payload(event.data) + if isinstance(event, EventV1): + return serializer_for_payload(event.inputs) + return serializer_for_payload(event.types) + + +def serializer_for_function( + abi_function: AbiV0.Function, +) -> FunctionSerializationAdapter: + """ + Create FunctionSerializationAdapter for serializing function inputs and deserializing function outputs. + + :param abi_function: parsed function's abi. + :return: FunctionSerializationAdapter. + """ + return FunctionSerializationAdapter( + inputs_serializer=serializer_for_payload(abi_function.inputs), + outputs_deserializer=serializer_for_payload(abi_function.outputs), + ) + + +def serializer_for_function_v1( + abi_function: Union[AbiV1.Function, AbiV2.Function], +) -> FunctionSerializationAdapter: + """ + Create FunctionSerializationAdapter for serializing function inputs and deserializing function outputs. + + :param abi_function: parsed function's abi. + :return: FunctionSerializationAdapter. + """ + return FunctionSerializationAdapterV1( + inputs_serializer=serializer_for_payload(abi_function.inputs), + outputs_deserializer=serializer_for_outputs(abi_function.outputs), + ) + + +def serializer_for_constructor_v2( + abi_function: AbiV2.Constructor, +) -> FunctionSerializationAdapter: + """ + Create FunctionSerializationAdapter for serializing constructor inputs. + + :param abi_function: parsed constructor's abi. + :return: FunctionSerializationAdapter. + """ + return FunctionSerializationAdapterV1( + inputs_serializer=serializer_for_payload(abi_function.inputs), + outputs_deserializer=serializer_for_outputs([]), + ) diff --git a/ccxt/static_dependencies/starknet/serialization/function_serialization_adapter.py b/ccxt/static_dependencies/starknet/serialization/function_serialization_adapter.py new file mode 100644 index 0000000..4cc8ee9 --- /dev/null +++ b/ccxt/static_dependencies/starknet/serialization/function_serialization_adapter.py @@ -0,0 +1,110 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Dict, List, Set, Tuple + +from ..cairo.felt import CairoData +from .data_serializers.output_serializer import ( + OutputSerializer, +) +from .data_serializers.payload_serializer import ( + PayloadSerializer, +) +from .errors import InvalidTypeException +from .tuple_dataclass import TupleDataclass + + +@dataclass +class FunctionSerializationAdapter: + """ + Class serializing ``*args`` and ``**kwargs`` by adapting them to function inputs. + """ + + inputs_serializer: PayloadSerializer + outputs_deserializer: PayloadSerializer + + expected_args: Tuple[str] = field(init=False) + + def __post_init__(self): + self.expected_args = tuple( + self.inputs_serializer.serializers.keys() + ) # pyright: ignore + + def serialize(self, *args, **kwargs) -> CairoData: + """ + Method using args and kwargs to match members and serialize them separately. + + :return: Members serialized separately in SerializedPayload. + """ + named_arguments = self._merge_arguments(args, kwargs) + return self.inputs_serializer.serialize(named_arguments) + + def deserialize(self, data: List[int]) -> TupleDataclass: + """ + Deserializes data into TupleDataclass containing python representations. + + :return: cairo data. + """ + return self.outputs_deserializer.deserialize(data) + + def _merge_arguments(self, args: Tuple, kwargs: Dict) -> Dict: + """ + Merges positional and keyed arguments. + """ + # After this line we know that len(args) <= len(self.expected_args) + self._ensure_no_unnecessary_positional_args(args) + + named_arguments = dict(kwargs) + for arg, input_name in zip(args, self.expected_args): + if input_name in kwargs: + raise InvalidTypeException( + f"Both positional and named argument provided for '{input_name}'." + ) + named_arguments[input_name] = arg + + expected_args = set(self.expected_args) + provided_args = set(named_arguments.keys()) + + # named_arguments might have unnecessary arguments coming from kwargs (we ensure that + # len(args) <= len(self.expected_args) above) + self._ensure_no_unnecessary_args(expected_args, provided_args) + + # there might be some argument missing (not provided) + self._ensure_no_missing_args(expected_args, provided_args) + + return named_arguments + + def _ensure_no_unnecessary_positional_args(self, args: Tuple): + if len(args) > len(self.expected_args): + raise InvalidTypeException( + f"Provided {len(args)} positional arguments, {len(self.expected_args)} possible." + ) + + @staticmethod + def _ensure_no_unnecessary_args(expected_args: Set[str], provided_args: Set[str]): + excessive_arguments = provided_args - expected_args + if excessive_arguments: + raise InvalidTypeException( + f"Unnecessary named arguments provided: '{', '.join(excessive_arguments)}'." + ) + + @staticmethod + def _ensure_no_missing_args(expected_args: Set[str], provided_args: Set[str]): + missing_arguments = expected_args - provided_args + if missing_arguments: + raise InvalidTypeException( + f"Missing arguments: '{', '.join(missing_arguments)}'." + ) + + +@dataclass +class FunctionSerializationAdapterV1(FunctionSerializationAdapter): + outputs_deserializer: OutputSerializer + + def deserialize(self, data: List[int]) -> Tuple: + """ + Deserializes data into TupleDataclass containing python representations. + + :return: cairo data. + """ + return self.outputs_deserializer.deserialize(data) diff --git a/ccxt/static_dependencies/starknet/serialization/tuple_dataclass.py b/ccxt/static_dependencies/starknet/serialization/tuple_dataclass.py new file mode 100644 index 0000000..271ac3a --- /dev/null +++ b/ccxt/static_dependencies/starknet/serialization/tuple_dataclass.py @@ -0,0 +1,59 @@ +from __future__ import annotations + +from dataclasses import dataclass, fields, make_dataclass +from typing import Dict, Optional, Tuple + + +@dataclass(frozen=True, eq=False) +class TupleDataclass: + """ + Dataclass that behaves like a tuple at the same time. Used when data has defined order and names. + For instance in case of named tuples or function responses. + """ + + # getattr is called when attribute is not found in object. For instance when using object.unknown_attribute. + # This way pyright will know that there might be some arguments it doesn't know about and will stop complaining + # about some fields that don't exist statically. + def __getattr__(self, item): + # This should always fail - only attributes that don't exist end up in here. + # We use __getattribute__ to get the native error. + return super().__getattribute__(item) + + def __getitem__(self, item: int): + field = fields(self)[item] + return getattr(self, field.name) + + def __iter__(self): + return (getattr(self, field.name) for field in fields(self)) + + def as_tuple(self) -> Tuple: + """ + Creates a regular tuple from TupleDataclass. + """ + return tuple(self) + + def as_dict(self) -> Dict: + """ + Creates a regular dict from TupleDataclass. + """ + return {field.name: getattr(self, field.name) for field in fields(self)} + + # Added for backward compatibility with previous implementation based on NamedTuple + def _asdict(self): + return self.as_dict() + + def __eq__(self, other): + if isinstance(other, TupleDataclass): + return self.as_tuple() == other.as_tuple() + return self.as_tuple() == other + + @staticmethod + def from_dict(data: Dict, *, name: Optional[str] = None) -> TupleDataclass: + result_class = make_dataclass( + name or "TupleDataclass", + fields=[(key, type(value)) for key, value in data.items()], + bases=(TupleDataclass,), + frozen=True, + eq=False, + ) + return result_class(**data) diff --git a/ccxt/static_dependencies/starknet/utils/__init__.py b/ccxt/static_dependencies/starknet/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ccxt/static_dependencies/starknet/utils/__pycache__/__init__.cpython-311.pyc b/ccxt/static_dependencies/starknet/utils/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..ff55412 Binary files /dev/null and b/ccxt/static_dependencies/starknet/utils/__pycache__/__init__.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/starknet/utils/__pycache__/typed_data.cpython-311.pyc b/ccxt/static_dependencies/starknet/utils/__pycache__/typed_data.cpython-311.pyc new file mode 100644 index 0000000..80c56b2 Binary files /dev/null and b/ccxt/static_dependencies/starknet/utils/__pycache__/typed_data.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/starknet/utils/constructor_args_translator.py b/ccxt/static_dependencies/starknet/utils/constructor_args_translator.py new file mode 100644 index 0000000..dde3600 --- /dev/null +++ b/ccxt/static_dependencies/starknet/utils/constructor_args_translator.py @@ -0,0 +1,86 @@ +from typing import List, Optional, Union + +from ..abi.v2 import shape as ShapeV2 +from ..abi.v0 import AbiParser as AbiParserV0 +from ..abi.v1 import AbiParser as AbiParserV1 +from ..abi.v2 import AbiParser as AbiParserV2 +from ..serialization import ( + FunctionSerializationAdapter, + serializer_for_function, +) +from ..serialization.factory import ( + serializer_for_constructor_v2, + serializer_for_function_v1, +) + + +def translate_constructor_args( + abi: List, constructor_args: Optional[Union[List, dict]], *, cairo_version: int = 1 +) -> List[int]: + serializer = ( + _get_constructor_serializer_v1(abi) + if cairo_version == 1 + else _get_constructor_serializer_v0(abi) + ) + + if serializer is None or len(serializer.inputs_serializer.serializers) == 0: + return [] + + if not constructor_args: + raise ValueError( + "Provided contract has a constructor and no arguments were provided." + ) + + args, kwargs = ( + ([], constructor_args) + if isinstance(constructor_args, dict) + else (constructor_args, {}) + ) + return serializer.serialize(*args, **kwargs) + + +def _get_constructor_serializer_v1(abi: List) -> Optional[FunctionSerializationAdapter]: + if _is_abi_v2(abi): + parsed = AbiParserV2(abi).parse() + constructor = parsed.constructor + + if constructor is None or not constructor.inputs: + return None + + return serializer_for_constructor_v2(constructor) + + parsed = AbiParserV1(abi).parse() + constructor = parsed.functions.get("constructor", None) + + # Constructor might not accept any arguments + if constructor is None or not constructor.inputs: + return None + + return serializer_for_function_v1(constructor) + + +def _is_abi_v2(abi: List) -> bool: + for entry in abi: + if entry["type"] in [ + ShapeV2.CONSTRUCTOR_ENTRY, + ShapeV2.L1_HANDLER_ENTRY, + ShapeV2.INTERFACE_ENTRY, + ShapeV2.IMPL_ENTRY, + ]: + return True + if entry["type"] == ShapeV2.EVENT_ENTRY: + if "inputs" in entry: + return False + if "kind" in entry: + return True + return False + + +def _get_constructor_serializer_v0(abi: List) -> Optional[FunctionSerializationAdapter]: + parsed = AbiParserV0(abi).parse() + + # Constructor might not accept any arguments + if not parsed.constructor or not parsed.constructor.inputs: + return None + + return serializer_for_function(parsed.constructor) diff --git a/ccxt/static_dependencies/starknet/utils/iterable.py b/ccxt/static_dependencies/starknet/utils/iterable.py new file mode 100644 index 0000000..a6478eb --- /dev/null +++ b/ccxt/static_dependencies/starknet/utils/iterable.py @@ -0,0 +1,13 @@ +from typing import Iterable, TypeVar, Union + +T = TypeVar("T") + + +# pyright: reportGeneralTypeIssues=false +def ensure_iterable(value: Union[T, Iterable[T]]) -> Iterable[T]: + try: + iter(value) + # Now we now it is iterable + return value + except TypeError: + return [value] diff --git a/ccxt/static_dependencies/starknet/utils/schema.py b/ccxt/static_dependencies/starknet/utils/schema.py new file mode 100644 index 0000000..f3ec784 --- /dev/null +++ b/ccxt/static_dependencies/starknet/utils/schema.py @@ -0,0 +1,13 @@ +import os + +from ..marshmallow import EXCLUDE, RAISE +from ..marshmallow import Schema as MarshmallowSchema + +MARSHMALLOW_UKNOWN_EXCLUDE = os.environ.get("STARKNET_PY_MARSHMALLOW_UKNOWN_EXCLUDE") + + +class Schema(MarshmallowSchema): + class Meta: + unknown = ( + EXCLUDE if (MARSHMALLOW_UKNOWN_EXCLUDE or "").lower() == "true" else RAISE + ) diff --git a/ccxt/static_dependencies/starknet/utils/typed_data.py b/ccxt/static_dependencies/starknet/utils/typed_data.py new file mode 100644 index 0000000..1fa921a --- /dev/null +++ b/ccxt/static_dependencies/starknet/utils/typed_data.py @@ -0,0 +1,182 @@ +from dataclasses import dataclass +from typing import Dict, List, Union, cast + +from ...marshmallow import Schema, fields, post_load + +from ..cairo.felt import encode_shortstring +from ..hash.selector import get_selector_from_name +from ..hash.utils import compute_hash_on_elements +from ..models.typed_data import StarkNetDomainDict, TypedDataDict + + +@dataclass(frozen=True) +class Parameter: + """ + Dataclass representing a Parameter object + """ + + name: str + type: str + + +@dataclass(frozen=True) +class TypedData: + """ + Dataclass representing a TypedData object + """ + + types: Dict[str, List[Parameter]] + primary_type: str + domain: StarkNetDomainDict + message: dict + + @staticmethod + def from_dict(data: TypedDataDict) -> "TypedData": + """ + Create TypedData dataclass from dictionary. + + :param data: TypedData dictionary. + :return: TypedData dataclass instance. + """ + return cast(TypedData, TypedDataSchema().load(data)) + + def _is_struct(self, type_name: str) -> bool: + return type_name in self.types + + def _encode_value(self, type_name: str, value: Union[int, str, dict, list]) -> int: + if is_pointer(type_name) and isinstance(value, list): + type_name = strip_pointer(type_name) + + if self._is_struct(type_name): + return compute_hash_on_elements( + [self.struct_hash(type_name, data) for data in value] + ) + return compute_hash_on_elements([int(get_hex(val), 16) for val in value]) + + if self._is_struct(type_name) and isinstance(value, dict): + return self.struct_hash(type_name, value) + + value = cast(Union[int, str], value) + return int(get_hex(value), 16) + + def _encode_data(self, type_name: str, data: dict) -> List[int]: + values = [] + for param in self.types[type_name]: + encoded_value = self._encode_value(param.type, data[param.name]) + values.append(encoded_value) + + return values + + def _get_dependencies(self, type_name: str) -> List[str]: + if type_name not in self.types: + # type_name is a primitive type, has no dependencies + return [] + + dependencies = set() + + def collect_deps(type_name: str) -> None: + for param in self.types[type_name]: + fixed_type = strip_pointer(param.type) + if fixed_type in self.types and fixed_type not in dependencies: + dependencies.add(fixed_type) + # recursive call + collect_deps(fixed_type) + + # collect dependencies into a set + collect_deps(type_name) + return [type_name, *list(dependencies)] + + def _encode_type(self, type_name: str) -> str: + primary, *dependencies = self._get_dependencies(type_name) + types = [primary, *sorted(dependencies)] + + def make_dependency_str(dependency): + lst = [f"{t.name}:{t.type}" for t in self.types[dependency]] + return f"{dependency}({','.join(lst)})" + + return "".join([make_dependency_str(x) for x in types]) + + def type_hash(self, type_name: str) -> int: + """ + Calculate the hash of a type name. + + :param type_name: Name of the type. + :return: Hash of the type name. + """ + return get_selector_from_name(self._encode_type(type_name)) + + def struct_hash(self, type_name: str, data: dict) -> int: + """ + Calculate the hash of a struct. + + :param type_name: Name of the type. + :param data: Data defining the struct. + :return: Hash of the struct. + """ + return compute_hash_on_elements( + [self.type_hash(type_name), *self._encode_data(type_name, data)] + ) + + def message_hash(self, account_address: int) -> int: + """ + Calculate the hash of the message. + + :param account_address: Address of an account. + :return: Hash of the message. + """ + message = [ + encode_shortstring("StarkNet Message"), + self.struct_hash("StarkNetDomain", cast(dict, self.domain)), + account_address, + self.struct_hash(self.primary_type, self.message), + ] + + return compute_hash_on_elements(message) + + +def get_hex(value: Union[int, str]) -> str: + if isinstance(value, int): + return hex(value) + if value[:2] == "0x": + return value + if value.isnumeric(): + return hex(int(value)) + return hex(encode_shortstring(value)) + + +def is_pointer(value: str) -> bool: + return len(value) > 0 and value[-1] == "*" + + +def strip_pointer(value: str) -> str: + if is_pointer(value): + return value[:-1] + return value + + +# pylint: disable=unused-argument +# pylint: disable=no-self-use + + +class ParameterSchema(Schema): + name = fields.String(data_key="name", required=True) + type = fields.String(data_key="type", required=True) + + @post_load + def make_dataclass(self, data, **kwargs) -> Parameter: + return Parameter(**data) + + +class TypedDataSchema(Schema): + types = fields.Dict( + data_key="types", + keys=fields.Str(), + values=fields.List(fields.Nested(ParameterSchema())), + ) + primary_type = fields.String(data_key="primaryType", required=True) + domain = fields.Dict(data_key="domain", required=True) + message = fields.Dict(data_key="message", required=True) + + @post_load + def make_dataclass(self, data, **kwargs) -> TypedData: + return TypedData(**data) diff --git a/ccxt/static_dependencies/starkware/__init__.py b/ccxt/static_dependencies/starkware/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ccxt/static_dependencies/starkware/__pycache__/__init__.cpython-311.pyc b/ccxt/static_dependencies/starkware/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..fdbc466 Binary files /dev/null and b/ccxt/static_dependencies/starkware/__pycache__/__init__.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/starkware/crypto/__init__.py b/ccxt/static_dependencies/starkware/crypto/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ccxt/static_dependencies/starkware/crypto/__pycache__/__init__.cpython-311.pyc b/ccxt/static_dependencies/starkware/crypto/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..bbbc181 Binary files /dev/null and b/ccxt/static_dependencies/starkware/crypto/__pycache__/__init__.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/starkware/crypto/__pycache__/fast_pedersen_hash.cpython-311.pyc b/ccxt/static_dependencies/starkware/crypto/__pycache__/fast_pedersen_hash.cpython-311.pyc new file mode 100644 index 0000000..bbba38e Binary files /dev/null and b/ccxt/static_dependencies/starkware/crypto/__pycache__/fast_pedersen_hash.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/starkware/crypto/__pycache__/math_utils.cpython-311.pyc b/ccxt/static_dependencies/starkware/crypto/__pycache__/math_utils.cpython-311.pyc new file mode 100644 index 0000000..5e8953c Binary files /dev/null and b/ccxt/static_dependencies/starkware/crypto/__pycache__/math_utils.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/starkware/crypto/__pycache__/signature.cpython-311.pyc b/ccxt/static_dependencies/starkware/crypto/__pycache__/signature.cpython-311.pyc new file mode 100644 index 0000000..201987c Binary files /dev/null and b/ccxt/static_dependencies/starkware/crypto/__pycache__/signature.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/starkware/crypto/__pycache__/utils.cpython-311.pyc b/ccxt/static_dependencies/starkware/crypto/__pycache__/utils.cpython-311.pyc new file mode 100644 index 0000000..dd41954 Binary files /dev/null and b/ccxt/static_dependencies/starkware/crypto/__pycache__/utils.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/starkware/crypto/fast_pedersen_hash.py b/ccxt/static_dependencies/starkware/crypto/fast_pedersen_hash.py new file mode 100644 index 0000000..3fb34c2 --- /dev/null +++ b/ccxt/static_dependencies/starkware/crypto/fast_pedersen_hash.py @@ -0,0 +1,50 @@ +from ...ecdsa.curves import Curve +from ...ecdsa.ellipticcurve import CurveFp, Point + +from .signature import ( + ALPHA, + BETA, + CONSTANT_POINTS, + EC_ORDER, + FIELD_PRIME, + N_ELEMENT_BITS_HASH, + SHIFT_POINT, +) +from .utils import from_bytes, to_bytes + +curve_stark = CurveFp(FIELD_PRIME, ALPHA, BETA) +LOW_PART_BITS = 248 +LOW_PART_MASK = 2**248 - 1 +HASH_SHIFT_POINT = Point(curve_stark, SHIFT_POINT[0], SHIFT_POINT[1], EC_ORDER) +P_0 = Point(curve_stark, CONSTANT_POINTS[2][0], CONSTANT_POINTS[2][1], EC_ORDER) +P_1 = Point(curve_stark, CONSTANT_POINTS[2 + LOW_PART_BITS][0], CONSTANT_POINTS[2 + LOW_PART_BITS][1], EC_ORDER) +P_2 = Point(curve_stark, CONSTANT_POINTS[2 + N_ELEMENT_BITS_HASH][0], CONSTANT_POINTS[2 + N_ELEMENT_BITS_HASH][1], EC_ORDER) +P_3 = Point(curve_stark, CONSTANT_POINTS[2 + N_ELEMENT_BITS_HASH + LOW_PART_BITS][0], CONSTANT_POINTS[2 + N_ELEMENT_BITS_HASH + LOW_PART_BITS][1], EC_ORDER) + +def process_single_element(element: int, p1, p2) -> Point: + assert 0 <= element < FIELD_PRIME, "Element integer value is out of range" + + high_nibble = element >> LOW_PART_BITS + low_part = element & LOW_PART_MASK + return low_part * p1 + high_nibble * p2 + + +def pedersen_hash(x: int, y: int) -> int: + """ + Computes the Starkware version of the Pedersen hash of x and y. + The hash is defined by: + shift_point + x_low * P_0 + x_high * P1 + y_low * P2 + y_high * P3 + where x_low is the 248 low bits of x, x_high is the 4 high bits of x and similarly for y. + shift_point, P_0, P_1, P_2, P_3 are constant points generated from the digits of pi. + """ + return ( + HASH_SHIFT_POINT + process_single_element(x, P_0, P_1) + process_single_element(y, P_2, P_3) + ).x() + + +def pedersen_hash_func(x: bytes, y: bytes) -> bytes: + """ + A variant of 'pedersen_hash', where the elements and their resulting hash are in bytes. + """ + assert len(x) == len(y) == 32, "Unexpected element length." + return to_bytes(pedersen_hash(*(from_bytes(element) for element in (x, y)))) diff --git a/ccxt/static_dependencies/starkware/crypto/math_utils.py b/ccxt/static_dependencies/starkware/crypto/math_utils.py new file mode 100644 index 0000000..534a492 --- /dev/null +++ b/ccxt/static_dependencies/starkware/crypto/math_utils.py @@ -0,0 +1,78 @@ +############################################################################### +# Copyright 2019 StarkWare Industries Ltd. # +# # +# Licensed under the Apache License, Version 2.0 (the "License"). # +# You may not use this file except in compliance with the License. # +# You may obtain a copy of the License at # +# # +# https://www.starkware.co/open-source-license/ # +# # +# Unless required by applicable law or agreed to in writing, # +# software distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions # +# and limitations under the License. # +############################################################################### + + +from typing import Tuple + +from ...sympy.core.intfunc import igcdex + +# A type that represents a point (x,y) on an elliptic curve. +ECPoint = Tuple[int, int] + +def div_mod(n: int, m: int, p: int) -> int: + """ + Finds a nonnegative integer 0 <= x < p such that (m * x) % p == n + """ + a, b, c = igcdex(m, p) + assert c == 1 + return (n * a) % p + +def div_ceil(x, y): + assert isinstance(x, int) and isinstance(y, int) + return -((-x) // y) + +def ec_add(point1: ECPoint, point2: ECPoint, p: int) -> ECPoint: + """ + Gets two points on an elliptic curve mod p and returns their sum. + Assumes the points are given in affine form (x, y) and have different x coordinates. + """ + assert (point1[0] - point2[0]) % p != 0 + m = div_mod(point1[1] - point2[1], point1[0] - point2[0], p) + x = (m * m - point1[0] - point2[0]) % p + y = (m * (point1[0] - x) - point1[1]) % p + return x, y + + +def ec_neg(point: ECPoint, p: int) -> ECPoint: + """ + Given a point (x,y) return (x, -y) + """ + x, y = point + return (x, (-y) % p) + + +def ec_double(point: ECPoint, alpha: int, p: int) -> ECPoint: + """ + Doubles a point on an elliptic curve with the equation y^2 = x^3 + alpha*x + beta mod p. + Assumes the point is given in affine form (x, y) and has y != 0. + """ + assert point[1] % p != 0 + m = div_mod(3 * point[0] * point[0] + alpha, 2 * point[1], p) + x = (m * m - 2 * point[0]) % p + y = (m * (point[0] - x) - point[1]) % p + return x, y + + +def ec_mult(m: int, point: ECPoint, alpha: int, p: int) -> ECPoint: + """ + Multiplies by m a point on the elliptic curve with equation y^2 = x^3 + alpha*x + beta mod p. + Assumes the point is given in affine form (x, y) and that 0 < m < order(point). + """ + if m == 1: + return point + if m % 2 == 0: + return ec_mult(m // 2, ec_double(point, alpha, p), alpha, p) + return ec_add(ec_mult(m - 1, point, alpha, p), point, p) diff --git a/ccxt/static_dependencies/starkware/crypto/signature.py b/ccxt/static_dependencies/starkware/crypto/signature.py new file mode 100644 index 0000000..cca75bc --- /dev/null +++ b/ccxt/static_dependencies/starkware/crypto/signature.py @@ -0,0 +1,2344 @@ +############################################################################### +# Copyright 2019 StarkWare Industries Ltd. # +# # +# Licensed under the Apache License, Version 2.0 (the "License"). # +# You may not use this file except in compliance with the License. # +# You may obtain a copy of the License at # +# # +# https://www.starkware.co/open-source-license/ # +# # +# Unless required by applicable law or agreed to in writing, # +# software distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions # +# and limitations under the License. # +############################################################################### + +import hashlib +import itertools +import json +import math +import os +import secrets +from typing import Optional, Tuple, Union + +from ...ecdsa.rfc6979 import generate_k + +from .math_utils import ( + ECPoint, + div_mod, + div_ceil, + ec_add, + ec_double, + ec_mult, +) + +# TODO: require more module from sympy +# from ...sympy.ntheory.residue_ntheory import ( +# is_quad_residue, +# sqrt_mod, +# ) + +FIELD_PRIME = 3618502788666131213697322783095070105623107215331596699973092056135872020481 +FIELD_GEN = 3 +ALPHA = 1 +BETA = 3141592653589793238462643383279502884197169399375105820974944592307816406665 +EC_ORDER = 3618502788666131213697322783095070105526743751716087489154079457884512865583 +CONSTANT_POINTS = [ + [ + 2089986280348253421170679821480865132823066470938446095505822317253594081284, + 1713931329540660377023406109199410414810705867260802078187082345529207694986 + ], + [ + 874739451078007766457464989774322083649278607533249481151382481072868806602, + 152666792071518830868575557812948353041420400780739481342941381225525861407 + ], + [ + 996781205833008774514500082376783249102396023663454813447423147977397232763, + 1668503676786377725805489344771023921079126552019160156920634619255970485781 + ], + [ + 100775230685312048816501234355008830851785728808228209380195522984287974518, + 3198314560325546891798262260233968848553481119985289977998522774043088964633 + ], + [ + 1837189741429329983886833789246131275985545035599091291966623919967841244204, + 469920083884440505232139273974987899994000885911056071194573294589259802432 + ], + [ + 1337726844298689299569036965005062374791732295462158862097564380968412485659, + 3094702644796621069343809899235459280874613277076424986270525032931210979878 + ], + [ + 2997390320399291502365701712271136720873363591256030629621859546399086933620, + 2725742381037070528763700586156979930560374472266472382691451570287013862562 + ], + [ + 3608386523905995279224196894194758246854376991737956048428718275550441491554, + 299638830690759628369563708877422667364443387620215168054000198378323554222 + ], + [ + 1733017745745290190841058775834438078769759612359153596488000160651631909868, + 1973340172374381850851160588687352250788736199336041450103281811142396650489 + ], + [ + 855657745844414012325398643860801166203065495756352613799675558543302817038, + 1379036914678019505188657918379814767819231204146554192918997656166330268474 + ], + [ + 2860710426779608457334569506319606721823380279653117262373857444958848532006, + 1390846552016301495855136360351297463700036202880431397235275981413499580322 + ], + [ + 2395624363109833935111082867579092089638282063493755655374369403894420657396, + 351237427147755677344067136337758392262982966921757802462075586166198965221 + ], + [ + 1518817631841006315871038165514435660668372956425694825094659891110110998470, + 2435234811597428668734076452595083530950234466401981561306652881621340269965 + ], + [ + 2173245854114081430013864960244839145346281378834121479101410821419573677603, + 2546798213917003006819050845641858786968858658397560158974242382076827691040 + ], + [ + 2842565516483040219247288049386440051275065340592157409909273207335045943247, + 3243970369543480657564144388570283526584293743815525434693286186817417955980 + ], + [ + 334001339911595275369567085510917903426590364565508070786916614629507192987, + 3111246400312591389547128607242178414610449977696648758380570718520342084022 + ], + [ + 1524160182224703084171959692156493185929433834577885561910744542328224256855, + 1537801596806048756579645687819844574476915843680990392821821911338917834516 + ], + [ + 1534228784878613855372285213913393282004680247144707529194564051083323737667, + 3521706376781514787959257460794337508069645724875214092054188903006114926236 + ], + [ + 2578937995141029655393232141271255572790413762563128577126763729975116228193, + 17390356333795810120168422581001175036590566546824644641783194730252048211 + ], + [ + 947940612979492942947148169286573131514814097313999984923945564630579515590, + 2308193393705297792974084886503909156992936885451139308263357445074155842124 + ], + [ + 732404465937527082089939128149870791505934917542321234949662968808570781433, + 143709480454569956048931032102611838633822436488408778496842771196869318906 + ], + [ + 241248627215637165874725355816367843299343644290443713521922700286140902436, + 3252553440660691138666231381716834106176440363202963142721270080741642531818 + ], + [ + 3333115552336678637619322993761507811794447605372046548664704236825849321847, + 2074314011440265695926966409849756773065015399410882685131987099183343980472 + ], + [ + 2828708362623152676836369441327395494506045083356924287447843608221054063061, + 107382801318187992328203770492115828936772008265759480771447426051158848300 + ], + [ + 3093728769381682918281543022553646237541394965209383769732554106568421526166, + 3204173745255459543321323207111205642245664180117592291733272407863239345733 + ], + [ + 2408410160653222627937499570601090771762354825212795227033567284727088044150, + 2304538566806563442614047090440785060491938762209719835685218901694719627776 + ], + [ + 2758360715188072223623539313334284404194065029791792170224299872004682172868, + 1002646182229402950578888347706450598176482335256655665515308125378628073404 + ], + [ + 2379910339855741683480364155463331175570260120162494489033888506779165916952, + 2649708833736663077287705299849077665696945338155198794587505679066706972556 + ], + [ + 1176714920396664309204390318351093831295503091238549055894748705033779114462, + 715774545317274898026140714630411642171682270543528864055131586173491427672 + ], + [ + 2785974441098456234843127330799770200846625265290625972155616950088804499059, + 307863489533861377687037248795744305150392367370243564208692826588510059533 + ], + [ + 3127903794657845782054923624413460963746108626701051648823597412593664219443, + 2832400994360149010034695923237223654142501296305205824531678157494587069403 + ], + [ + 1131830029003838132931634271160654858275272609594100638978880153740390535738, + 3607754722674459909791405256520586221653709952825470711876211792388292839610 + ], + [ + 2759794674261780431984200708995704387783325908768350345798229435903528807938, + 1260417916396710926345525006943606967340884567049582926597956766543273788168 + ], + [ + 2830057895043497782751868691208958763779500933641414034760294364554584648598, + 3148801330152002136119343944143478481505330324328753740340717562089558415415 + ], + [ + 2506640265270419609137616465635205683276867684162736282412466285973014171890, + 517183264945713035190384665697926865674306942691511144684578407765174829225 + ], + [ + 297135274309227547571122074141892368978567606416603548099453251059259457396, + 738308515934554491948011858951484620721581230541917718419817808748771885016 + ], + [ + 3287710003144516108009450594509223314440628263909148329742349774812346409307, + 225177091586755328705836619671963709040245082813366263262829376695813391167 + ], + [ + 1902667075164809149654789463437998238417922554649696913795190312096632954124, + 3609476325943007214468624874971854834826753291984136726633316509571578121273 + ], + [ + 3452217073856686233854377494033704667278961088549888774623466171791636016755, + 2444544408047898094236889539040891081629098003972134793849574953018755818545 + ], + [ + 2069167537346986671273731269107346759773110422380837126332493778223975152855, + 357269144817598369811221449775153952909196646906055962631273486250245080334 + ], + [ + 1047243972090526803876529530618926456830431728514494419504365869386003201726, + 493385597033162791196594107722557650629404615534399757351633988473756315396 + ], + [ + 965109286411904242713728581817485738428793211202426744037474223240067211186, + 2413690664561921424572393647761853376475215279589433231249258838213909974115 + ], + [ + 1034828486658124322341241159997750207973283714015564270100039839265723642437, + 3320659525509256706388336697822491014651786050898707765488292630053240833630 + ], + [ + 2924967592602073254569141793533465663326310402870978597207810641466195156731, + 2828397747600941786969312189630745459745605646639868710157419955197341205208 + ], + [ + 1208329771806436797417016120088886878299172415075658275611114767604464163273, + 2810710693404583496584233908768327740199404051365142000873009254465681789118 + ], + [ + 2187000042175732773516370724251479381096587891539947053664138571731476871402, + 746498189666538551392041580103908969319041062419800073057943031798594621183 + ], + [ + 2000452964676707688903182602940959322690781577603915068119089450310903786954, + 866954387631286490641992457358203082174659959202834537990114265745821626191 + ], + [ + 1296834309098498653963459206815713058813876510889656567958698555814791917906, + 900321515532234476515871433334993494171305935536543684417444011164731278279 + ], + [ + 1595376832537627540806114085753076669172519984983967639366317789074759898235, + 1219266627855965397760533984052253611682860681989985974389624113620845749733 + ], + [ + 1823240537897691300512000714094702014772232075125035605123065504505635249040, + 1906261986240090609038909222466712928329872469704972427112770233118685440655 + ], + [ + 778303663772980866291056760213466667611301230393329301216572062540133184143, + 2984789228888160339109292850517099811453943454548552440328876677370962441196 + ], + [ + 3543009119282959041814671650391719969699313481882590413207543576117841443934, + 1490213523215199378557197585333711645365263188010733339965078460230935145833 + ], + [ + 1176236937487751405961855617527764992282992896230256112304717984169523263763, + 327501310716241925530534584357204203311238806558120970999031300336125027957 + ], + [ + 2406374227501733859839714271591391036982988438954690468147627905682319529429, + 2498960794066678523664440543302013058525262611284856216226688049821146904878 + ], + [ + 3452133497919418476271423809649290075304287340106989073706651714039300732642, + 1721794031770397703814538822528137647140579794352083932468384744314312603894 + ], + [ + 1149850245936233973982137051189893468427998957468612707869248329754912343300, + 3458926667343838493950348154788263034977717528749131548957463567618227215963 + ], + [ + 889697158819326131781010711389595245311511671705340221964679483759691059211, + 2019807322878676390755723975464224869137141739292295209065143377221547630036 + ], + [ + 1115329342882491971826579323754611049286425400118842701871616630850493708107, + 279298782170669703031554266329450929495785208527313704344065678320374720785 + ], + [ + 3365519876326833923487050935023315949049714901059255282163633136201965868269, + 1868260280532817409831058300719431634773697469228981331390874154636702517757 + ], + [ + 2669519052792032403625707785046224942570603898100356640293648365762798392505, + 1740654081939116207510779753054238062700744649382792523869583125748991653229 + ], + [ + 3121331648294614359396440970780137002689430823241383158647529355695088932901, + 873685066624425351999444458200994077868639460913455806504495956245857350007 + ], + [ + 1816660828193076969492079690868149793376961857938776531133929200951688539889, + 1617163330180274180907112309026652344859788476549850480902966972316617122251 + ], + [ + 928998762106806361096934313135623561544110643422813945289044484271228303836, + 464078854784700975668790446344005332658112186605529977536203111266829995975 + ], + [ + 496484433760448456019075524089809339174015679144397089226817311073355526314, + 656343647912825200827812764729017634457244310809059156122299990679423100787 + ], + [ + 1973676598953671410920434538501081964242302598763799105681937944844095305545, + 307489436917501023536717840176704363109773690206006513056594461780393179493 + ], + [ + 518536787018692743767191710241721928752533989539898170372615978836875877432, + 2521626354257053998255710814293449754273786720271901759585107157901808273967 + ], + [ + 690595468184470683559850269431813912957862648277508592008590933748732173747, + 2414429566032394919031053748274838227119236756361308391916397575422431579532 + ], + [ + 2501095101472669025652293419986706422252484721018908950115277955062729551801, + 2787294359824056854441860940419185812333861607391901444904784313692077324784 + ], + [ + 565118044580500186326118761527011487144705745596478022906701885062524158603, + 1799231527053210762358771329838631545632111862410291843983616507220396846052 + ], + [ + 2037340835455495949556975891561839169602876578929015568690589435716642289270, + 2204387525434065888311157590483645040393870259860783204107223503007512510657 + ], + [ + 2114937389277866993631127029230629118622631637448671765297876516930885448024, + 2772043872341063054220063825377798998299638741683281348998984204361894484463 + ], + [ + 2097763236780897995115236415009286780722416534493059040644518024665003224715, + 2246556465712181592290422124919768779861225704408240007042932086214236197576 + ], + [ + 3612119898822167069923931382556535386023574823466693257258182923730749602297, + 1269484610898538742657592460359658026176515519847024771198089933933954376158 + ], + [ + 2881403268965266082547256964340568575654154178897275699709021163679539118655, + 1692819601432103623771042884269589390667189502599274435132350636867812310182 + ], + [ + 750328371578183028452365126719312491229205795053566378454173411358094968605, + 953119186030327873981560224892058145045695472203493861081607133113834466378 + ], + [ + 1656613311827787565035393466226110392419398195273172946657205368665450125099, + 2599745208280264384426758693760932474903320975443399225749144566172064401743 + ], + [ + 2103692960985198293421966097241235578982164940176248129325331384077838061492, + 2570917734334329081039835855157625956461983506342767455507599344910133039387 + ], + [ + 2956028884218562660581192985661469944972780357808337397013249554146206921192, + 408046758724714924414144046633476879802492779243354079741111917699605101737 + ], + [ + 3295092543885183250245242131201657515391353210344574951962581116320755471573, + 1642807297265426717090166824916187234878363764999511552230372565887780637331 + ], + [ + 3254747955211938731592559910914052760563649051803026767692046312174361324755, + 2597825541939769956115552787003343561035903289305000309435219987085455364052 + ], + [ + 3061661576103456171574781131851984681727274357576590194413976242212020548895, + 1440946704104779823819051619396174008908467766221900978049401983321224347705 + ], + [ + 838962129372725157269649943170983294581294061955271030939039912615696392969, + 2450855656904900531871678154573620082208900269187857841648756800522172293828 + ], + [ + 623738578803750870859101112646088321855359403665383400994605481921028955581, + 1896987617779463068695767102228909909503881877404532152457554106592081120284 + ], + [ + 3005336317328034100451063789097066979058288244531776915568829475740707033442, + 589659290793751995469380634546964377934517395448274738900772252627065284531 + ], + [ + 12756189276497641079437863756137709474617047416617345367587373008165663758, + 1456142055978131072821045695636502275738045623685201320945241755292775664199 + ], + [ + 1074674018821399880635425421583189259497571069746227839865670725436462244312, + 258858007368753107323242423300483485125794748975129950895267988132082308316 + ], + [ + 681655034855073789847960596245322342285304909895796912572444021565696961022, + 2638585878119722820634387321875125243576892120356441191562743859796708349183 + ], + [ + 484275861089119330366419813397313284916585101030604880621077715936286046362, + 2124340832207188633301820498272014794490866326891190193431359618065899307375 + ], + [ + 405073150800337564971044639647327667948449004749665061152270060911349262451, + 1489771173602690638325396124433593050299556632624187628464627170731054511183 + ], + [ + 2210274362697653963013276763896406028937726941229800687802476566182252928069, + 107281585620564596853732679859399446219714819235699352412476509704530753455 + ], + [ + 1574843453768695165378209623495036727403155857878058624836922271817659867629, + 1555661183633642518402202300513733784743599394430116656383692711334768895734 + ], + [ + 1123478630620218654285047429952065588412730975530361457071991777237712271756, + 2829567770920061439901039508116248775591132699709271161323340183772358690173 + ], + [ + 1297650053639848975729187455845252204377426797153455899043246280172874099679, + 1368877225005672215217218872501867910626338344551878492763876596080463242259 + ], + [ + 1370690034945541589206390781179695998551445194688862467131056389583582009970, + 1470539777067742221259042319802797839837448440044303811384116671245122402709 + ], + [ + 3447187626984579154758815357460309989835728867823525757738362796350869385148, + 1816603979655684111669599224323721980301208636016613170580056711560881185458 + ], + [ + 1813653163931885994109909806517422543930671118466754170167858925831571853970, + 3512341620174737184060311796746868634824078847254277831432645720920236439319 + ], + [ + 2837307695083915103816975903131537789416753154015497312646069123780420491714, + 616619065833574133353826959462642204697394233876988991826103321230494864299 + ], + [ + 594079125176886234216350989657852431152571525526195525372802362637040396383, + 2494644159641327263522333201651180948727022160625648361761185979144016156268 + ], + [ + 2908396604150117877413275805593843594741970566352971841958566678954509739722, + 2270651079544400929734484036612745055500450526594372099549710701035274374884 + ], + [ + 2678766929120334020289731872716504689707050034449508844312788171212235149009, + 335161156418784138664067328871308180390826302690183885244799298812600140579 + ], + [ + 292866251242750359983852405638782648443335655395113648998608114038199112084, + 1929874131411396072089047462553509072816290408149385625319723159940078151305 + ], + [ + 882230982554387611436849430391722624915254936568405968528170702860016504332, + 212032938028459898975134792582247420939126124794435382098720034240512634047 + ], + [ + 128726401362994700814735301317860445928097536803417431852613367962589785439, + 3394124118935926464118252583967031473364357246179309645829616988941527947171 + ], + [ + 677401161940040406778133733501716798305847724216576059351439033320244262845, + 2925999450717968641674678177839015248195288227966544952091044204990727381539 + ], + [ + 2609307202957921769201454597723184920663265694086171832237331231605229071753, + 801185762689670517839772705471851009753066843170718046248101910206876036095 + ], + [ + 3164184719276807654208802895852587431370308788690119771658913844464285523544, + 1905779810840212631464048113896329901522020622508861085333684930860131307278 + ], + [ + 3611134164349757211212839193742321198628214417127680099335976437205997336534, + 2043780132560451089266915016427409559115509465273768103961201197432849551860 + ], + [ + 674219880578998785256251799630500124321523321851449394348048261225376968813, + 3482911053866603432328977995361936638192865211341332440041477929097847986703 + ], + [ + 3592898179794583311559546383565181628347625484079136119992096748458485223135, + 1465170880570843353446345695255388397672185811127330686304727767842553737080 + ], + [ + 3479945591612641094508473900775458017345694655844890750906112888442669167437, + 3101936119331166017640859304880796239643688879875971179134677606262220157899 + ], + [ + 1109415449398025759620102870831886622665877707210305893342279346428768457000, + 2860907454930708330476655483302818728978205359193672814876717458325223426655 + ], + [ + 1357688207483205144399854674655145367528198873070486160807161516476844171794, + 2883341787687489538845947867713953817206085374003322509387259371280461173550 + ], + [ + 2673153466150226365693822405439428986034577351717746967078667507408831331822, + 1711608722888314089357865502727953201489434753448475578858737527005192231458 + ], + [ + 3399600989769963316575978481506459307955666332477901574428783938231920315833, + 3215591135790328619011904119580811201910745729655431204459208397816583809934 + ], + [ + 1836471948074401070182493369353230908796463402812051721703561360825727986274, + 2080945706738209357213401773997996954868066166149050730738271792162977075010 + ], + [ + 3078673344420931157936045314816499193943345153816528362806332351627801597029, + 1525425884468796152026606689861526369646610285280224319617687192867257228599 + ], + [ + 2722661126018430265331502523339121563282558062510457654064467493725828237046, + 2961950709147303512642570304801930511214999580227551995337732709133755475031 + ], + [ + 3274644392893639165667150492068461730297126947445956777112794513023991125188, + 2045331969496016255571307687320013551853048491733214352646441341466617604859 + ], + [ + 112765822376634836986557327486952401905342511371567932656784212360129920193, + 2925784973032974919740053039088226660025084255241123579355659877189527261408 + ], + [ + 1615139200617101600744409619536119151548135245881789111743749077087127202607, + 928952548463900597431429074097494026341638502402888819595959975139532597586 + ], + [ + 89110835703618626006281975484745142479630710003689233432269611283359724860, + 215577133560624823939271084084157878843706221683942956847919314607432159969 + ], + [ + 2365204845648678418296386461366871427710672197784591168077970521383279482418, + 1305956795685348559654727794862561316677393382417655218949059817923691390961 + ], + [ + 3079404798552502156907780297645909071656015399012681371402409463283987518254, + 21056707131743755971547625076568417377953234492785298586731786122687871587 + ], + [ + 1774615721844036510130640283396354163953067525827829514432813052928446640946, + 2335149358230730291686085189834918003837685389648150105141235796252419309479 + ], + [ + 1735808336641971303276438949279976616427622212845904655791080520595464040270, + 1731419497051910986191800745158520013128509429166504524452212426369487243072 + ], + [ + 225628732048332665517140883504098234968194122687532130198116739739190659914, + 2967548981364742052624681942465301778302189310173531702492567780724033172480 + ], + [ + 799750724987433962024178782345988625264638811940430837962580781131993819643, + 2706844911451851414869588131946375233343117270234616506423955430971490523817 + ], + [ + 1369724618979165159567545620227678034464433354639639473366228046556595024708, + 2336720505293481339154253277307615910867992206313138581663759107186258146704 + ], + [ + 1930319710428527822612172469423758994346239585108798378474140896733181974186, + 3075715260783582884087648822116793974103741019719484227525273088977328650560 + ], + [ + 3338564282673470966018775402725903223528182579868287010100527892104759349598, + 3420593956586273983490728381161913171223107153319939485895365605089451302873 + ], + [ + 3555800392979470758767025620764223025989211483193054579480906006330657301837, + 2149899957029896266930433713982567969321434161670385808908337955713550175074 + ], + [ + 3608545457332454869249792272060591549043786202385519868035962077469064609868, + 2809440208316315053241747840271233428261159310708153434099156262944856645508 + ], + [ + 3322687396347671973732888070910250198991075315814763746941655084877070735058, + 832722278416792540687431048542953362405174329235971097088928697230453072453 + ], + [ + 3256622670111196871124104758094097761744096984947383503745643093253708235118, + 978374695622574912744133217293321822318821008760942454268417456141195625584 + ], + [ + 3410173130373258134073085888536026114751965010082957684030563699696697480724, + 1029778749451237919869173468746897632364581346265567722171293156697958069877 + ], + [ + 3112433780585229726581053697334217985356658602483885980524816220344484038215, + 2532342168888613388980558686859546650509221859779222781594186523997684356788 + ], + [ + 2105373184300453833566715269278756869308617438842998862447898598873739219726, + 877369785681960743494801702556244962845285506808792672979162666273085193389 + ], + [ + 1393232346062500558436496118495653473949473378263920202909352539047530421805, + 2756127133762923853208501814856325859087297216739373809908612188013181865996 + ], + [ + 2602735208672053198262611186569615211175565049926056251480233572878547833479, + 629329025417775185892796948300727047077553416972749774530835029538350502253 + ], + [ + 1793674060552460299974233062088263069538015472516735538751555695772239138820, + 3267910293235826685471049883722469455862230494501330393412138559038075359794 + ], + [ + 86863311610097694600007716574788775452468848027297474155593683107676444091, + 2437707467955141280580805831659061124536906723244797886865002980932492722630 + ], + [ + 647853547298757687427688341299451732092383134491816678870980494283372695378, + 3617636854438826076469516140770697015113683007543372219324291693845766252558 + ], + [ + 3334781490544017130581236435947958883590529475501049503331616858692678672100, + 1524104441388820711776621309003922630015731846383839927647934980171536936944 + ], + [ + 1706920492017425179811478489658749041569247255320747028220786202351882167016, + 3136062931779844207701097197470225751864670973511824570767144629934893764829 + ], + [ + 149961882640494348338043440935347934018050992098250101342463562074704074105, + 3512185327682078074777552971488882847736861539286017805546782388407073599152 + ], + [ + 370860151698809780775659878430714854391039795566897134502189484720213964480, + 1238632390866429469777130064298502868565198081327737370254478336416980048965 + ], + [ + 1282856472050108295483684218758047396436543346709509434400652555424465185837, + 3192157841056717224043634359360155098877589892941349993757962372268962797115 + ], + [ + 3312829654231446830533616425871987858809512713259532177921343043407846981843, + 1806976906510042164961209222220236480094335196359899499152673663644589005295 + ], + [ + 2826214476236947463134060188026701900955812519597178470627224598830931756638, + 1580208692732193723402036455390144043645806993218896764366725410799716087870 + ], + [ + 379282466405240907198946408283110293887947304133389197401888303636683398267, + 2678326696284237155462064013317839501356719176588712513572353629324338176024 + ], + [ + 523694191683203858486629087773330056719704186407500237062037916699142935895, + 2926557820959194747499923406515961933437184756076777735860240549085691053587 + ], + [ + 124833202191020085496726895079225667003902078587119506095688146305802394652, + 3363531045637590226091127058344713074476460386293564329053575414302767346399 + ], + [ + 2143651374532047414518845091756289685497664604612574318922876963772765730070, + 2673827034448835877302891179434422590334329367577351180190722501245625474345 + ], + [ + 3221138751067425577747344145841964056907436939401545122534796814786108536883, + 235336175612617006468583569743134666841147774340073069013500151038053147462 + ], + [ + 2555268812438549803835155012590418157639496654385486677760162898546595196513, + 1689750620936872465271905664884810894356966245587199342495604460756806777168 + ], + [ + 625234954128220194452646182586699463591457452260757480259168774898052708673, + 213161676086201716193087398992571428821719377385651036018131821081133441051 + ], + [ + 791204612818253360042670315855828265280744396446868065071639490833059389857, + 2903319472504514375570923883911935291656049846866663158923319947614657595965 + ], + [ + 2454646497469354145786043440836113873410753626627793200029667058037294177092, + 2032912288570051024788875629005185779516597292453270591316074924761292028478 + ], + [ + 2123337906329005118437052559658178011321855724007242277348144379266185921681, + 1894503378078529242131889602767919022671882244815906977803914704575270961364 + ], + [ + 3602877859331983834496856242375420741994478431198393391854598373337944528390, + 1390248307806555980791681315101366407220742869690031661994109003230157894293 + ], + [ + 639878381660983183381027433798087557736798886846049125326586828176785833470, + 3512419367912974372084649665826686824136068377404083433804922599142564888100 + ], + [ + 1419134474578125103135604337269063811149389272963545586390539000069337598859, + 2235806112698740444007125277529836633381483234992038997663439878202987160165 + ], + [ + 2778935883893248105666429190432349575416381537333107318387561120528861164061, + 1077278765722002922040368598422352116268480578772186030121792709039469085744 + ], + [ + 428120812125473710465911539814876147732404851270862105382572167865288414733, + 311433631492674653227552598028197969484982737235311408621797869297871619494 + ], + [ + 2952060790673513881810338872881675262252364545105524564508325071871367391468, + 3582199809541312286609332154241682748678754599041344486017973240874423950806 + ], + [ + 1594461313084220822848406979098581830091616974334911374972620995310916825614, + 2807125397722833979284621487977999302186252215199452982876117209095859541679 + ], + [ + 2043845413859332839989544551581949027414436310630598302726839258465898602603, + 1493460761394315567854883351837738680253927509880345067100858649883130457048 + ], + [ + 910520306205610055407037560562219297599712042011958124271729124108713600122, + 1227332044591912072232756174484900317585931037443282067667157281800895006658 + ], + [ + 1356322905904034407802797063629117404975010366322135572201704229186917875805, + 3062811278605169224008088325523606193685397687377873754697372903028059871958 + ], + [ + 2651699680375521157731474794703697878196552894539932179312542308190248741615, + 1374999585439430684113300998459982464172009456194373596393318069525504982141 + ], + [ + 2478956707907870604028063666882128463565422187569850243079631251653021026113, + 3484420957530723200820826205205389208856700316444106810018542093549855044781 + ], + [ + 2745165789654622430617796031383732860380537410258722201878555322390065895409, + 1389786264162289064932319984048404041828181018706839586582409596900469763316 + ], + [ + 792291068607630837524967871082697288161825884871154205347723150298096233265, + 1595885520235983259847467303703354388143203603602941324268342974672349807911 + ], + [ + 1112766696158267198974840537128095637678660084973877593826070735478819877428, + 76854025577946938851193238773961903814332442336300641590764677722879845665 + ], + [ + 3075088147214758449280318679771364592694228314680731955170139149431172226453, + 2631464825354322427832325664266556982858783834009349057603625173618876560686 + ], + [ + 1946952991061874087036397224151139450490763292883750208451596293164980017180, + 1671486844817988227846350440864329091397847162425205354534975696550529649006 + ], + [ + 447922002514491195056340638904274303849743543434622916451489369322819570022, + 481989665314939062190810517257104327416911189393567287995442516666694890745 + ], + [ + 278768424991728679375985793004023299002069942702077846697446381167229872879, + 2117939832550233574127330090850877444733498561141023361942969078992294709841 + ], + [ + 3216273780587845885344648641198104357960391646838456530374473185745790254775, + 3606237386675957259713749411947060068172141797816584221362457562749070134457 + ], + [ + 888490252135075821787230358754699355629649708262317489940736076372880196421, + 2243899191640238208431440104126936619623401575061085948377697882800556601532 + ], + [ + 1748762228708691375443278014066210232528543730943854135660867476022163374357, + 299810809101280072373711346937892828549469075660577914238588790523849116964 + ], + [ + 21800463980443581727936921323539002523916110909487141095813330024439604509, + 3239991592162948599965263758537867210199504881270279333333530879723254864285 + ], + [ + 1945649262091085266248324326387300230209483523911852369537103537534602188669, + 937547850790372496342721899405171497997439341142225383636485893081045476840 + ], + [ + 95187198852190497553925369717104578448538072022660253488035756790745062479, + 3284691659350683572361562338651440251050266476442725062173141352883841793890 + ], + [ + 396966715395293979183022035167233963473677021753099328784772352776439798275, + 1613322234499535439350563907086596802838800251144492804438492852751836971863 + ], + [ + 1705843197146248589209974041619858441399461896194255757072890762703413482026, + 2709234803093296173390529287307769438552461179797518815269401286400797214943 + ], + [ + 965896257227883986820160011629505229958129089095279852981573428580834882124, + 3222878455111086494470662906288505024631219103956585024274050126506278071618 + ], + [ + 1711062768895650467512736187912681238407905324392544748841566987114959619940, + 2037166002293356114375846587992164902616900087753130705770148512236761109532 + ], + [ + 306929171824313514409556697011428394218011837518848517597157431560368406919, + 2733907887575749650664983471979960087709811489243842872308868788028064963544 + ], + [ + 1589132075979748817708848065945386431745143428233530475552120730272187989177, + 3479574566456289934918449243936076329515058364276769923171744108995406563734 + ], + [ + 535088375724656896741944948092293887713409890408387277644439471611811707698, + 3084839991780046496389632054225148955101489706660124944353740712683200220129 + ], + [ + 1108714469400502294909191174424037717245383586375174474928489701713698780203, + 344696096736515166863459054453204973598183407079213029580530012456358476055 + ], + [ + 1896541196944951993777265586048099398652846183460110390523151621587553084390, + 1356944636529832014079353150870395726873584547194854259003641654476043751831 + ], + [ + 2129740212445659709473295068445927295211143686808997136843204790705829600051, + 2784093412242244048858451658511978494505560368170364505963748965317959330652 + ], + [ + 1164682312446525913981569213875150029132760939832308651498251601400403129032, + 860011516352836343688401715896426496333552023448545234232950198907527747215 + ], + [ + 403381603306414312665420204496598046512017134030800187206579847445734886844, + 156710329648174386978898329138661964114318418114565494873287202679709752521 + ], + [ + 3612709524322508414252878964415432233546157461869758422797096779080105910197, + 3298597797713539922720249912653326762917459398002362406667649739024691730641 + ], + [ + 2788472028266807168790808585452088947945831098822807970786023663768554965883, + 780984533356516602828788864429847085423068302018929746266555371946539918101 + ], + [ + 765015221711443098273066494715116075624315673921356068577773696969663768888, + 121864134634742875401471744901077698759842590065938894637311780483786803739 + ], + [ + 3420458059929261250134488209708428351089317850483628650712157545902459535082, + 2753073224523413466263788479220930266258909876418753081552150110011967761473 + ], + [ + 2435788601301668445647253094872163056944118800775256416712610409880768679053, + 600038247016283378097972299409314284965737590535869850167688589980357128171 + ], + [ + 2791234127851217827076638673185921990434565258934782489657649399234080946948, + 3019532625505810270756207292319968517034464214472035802893765165728566407791 + ], + [ + 2175512767988966931157181387193147869682253002700484615343260272758293076239, + 16356925783563920024655061104076219675967060212474163352137306664035714196 + ], + [ + 2346289056582359849501003989438672266264448907419687867675159268542148069281, + 269429309748271385175467663457270641975914217681440684037010041835530573698 + ], + [ + 481679002404926691230990214906065876613510532429501667509718578731645569407, + 1609976879746739928846288345159176310170130379856291476257183396230103110555 + ], + [ + 2127782061956380596600761098774414382531840909564578638354721422890231607858, + 3522660228464874209807741153414518456209790498657511195587886434205303653276 + ], + [ + 2990749966959304578563050621982841125682975201749885210174805380941485460083, + 295197603441351883186074553477194964532333406546643184464952640061744781463 + ], + [ + 322299859114885008311771586431338392030172898629995798709098486233393241698, + 1023285551085384033485966778701460844235475326250602612915962425374866217237 + ], + [ + 2679886206461898569320731641611496876582297592825799115233896892980598546123, + 3099565537546363902378775008459983997262715254411954165424270988571981464521 + ], + [ + 853528159824941445672647399048964399831359191507493106790462832690982526176, + 1009640753029626140011297690792696405115462988587033910421277684043461588633 + ], + [ + 1303674320335969124745880365084447847319437106063642511200542633107159295205, + 862053741658449155711084470286087651524720201941852653377417774320602998882 + ], + [ + 371784976647836509603140476112794524387140033126839183825485397106022337443, + 1481126947243599849498136670060476522501192641446886664638431717153471858730 + ], + [ + 256524228763970643322523560381608580385109852183329255389135850583181280611, + 406062519770964392170499110443173965524033961539587233636744514155485982986 + ], + [ + 889909761640899390466461648537512393214660407373609451150870536404743826631, + 3579579432590016439918468051553122806010260839218529712227712227008716479453 + ], + [ + 2181687776535382366933204874050926043644394510983340241902960182125931413775, + 125591469249575554746765789850870005842434420699124766244689384267560349454 + ], + [ + 1698011648746714871368937594549801676987990963546029514109897028714017239145, + 2032238188811755521211420251330988300583305301151292725000783151183349336764 + ], + [ + 207686199415566734810376310722982179068617367123996792470313948717972567639, + 1457942055660330932304294699096717861622662715626699977446009835743129689987 + ], + [ + 732258411798183046441278957122759122697038828507788948928154704288224686840, + 2612299176220286719934749375400756907183274870137646747294920834312868644617 + ], + [ + 1131610047798062548492567521656272602549897300746434874306725974344582031445, + 592276015836506844762700749171355258254203250155822561654862572344939966418 + ], + [ + 1683203479448902722167828838268483614879733285176640956474769817134838920079, + 2990934852576563000604264577641206566403815930150479384314858100664762791309 + ], + [ + 3379046363093840121427219819819164110884408212445336400953102193400631404907, + 3581767823598790218171818955173138326266037759069958949236204540729167542495 + ], + [ + 267147147550523725868561003543093307186644052136871409944613118661005584614, + 1306549083362239169609006162611383775071361663461903546405449470601023271459 + ], + [ + 296975912955049993838036596802334313092629826214334467025699377929310754375, + 1563707217733470474074970856533625421700727823760061087588764083345926207730 + ], + [ + 409930698864338128894354352383270205190831536703476012076004429065058122460, + 810813209917474447569624498205572650260997215620628649252215243057189717098 + ], + [ + 1244080118151222628554430592856097691345343883216811606005263269357031069918, + 3112961342564635769384393489844295061472524432493301559668801711930497029521 + ], + [ + 3287905561524106163037657312076054800525947413789669708058413492038554328847, + 2950266460046239577741220775173597014955783375111361479081038783371923101261 + ], + [ + 1834667074444753359896291977244884324984125181165619220523638440982002878973, + 1028938841439025536907121844571940895060940730436245288628559554616030756654 + ], + [ + 2696579144808608214357559333220835033277874753649087677547926288655864862396, + 1205667596625326964488748126939548850792621845142605787353435428502428194722 + ], + [ + 2681502303973249854884858344399536104286034851010362500805472440066210957701, + 2090624297500029615299510863468795665118802449870439334493020091696427901315 + ], + [ + 1353314499186573664458206377062562808249080943135426913783009658343962961481, + 2765083962822990885404819840064727313062737767044627137615530427213920746301 + ], + [ + 418493206714369123577433462079076010029158131200185602134250755432510923649, + 707998386543299716720343491204719022813222859618895609544390001004782644320 + ], + [ + 994749436490026363483216765171719719813756635373513071896945407046466009916, + 3542023359278349542922434792313919575183783573969934645527389879728870323439 + ], + [ + 1890008117079085184019219906457544800392678580280084811260498224586609070286, + 530233764155506684122011374141421351751148778529665626269052967779624308325 + ], + [ + 1380484666773413147984807138259355829485624671596401959499503325097178446404, + 2703143567161792821055257608672826415980743659971254233408855036509811875962 + ], + [ + 2143881856351418759124748482049358565778910428733836748398777699149555891034, + 2186504045350479968631256614333785165240148508469904761831572298072325147795 + ], + [ + 2794147613012298673104841054632587797837325721650333247952170883643570500710, + 2613093490820907283519815366146280477230890436771830509953858178043843459008 + ], + [ + 2625979094712716243539648580331695634041650060241725094563615224537054056479, + 145143322058807873791926380354826174760141531075716146087195666113341406100 + ], + [ + 2912042742810958222559628370002186243459717464446177149380721928196350211880, + 1095311779741249761721276459609161253222688862762095992035499286908387724976 + ], + [ + 1779688139950750136680613010527778524901137919784933323807835083178662865397, + 2443673470483539351187560062972650745287069240410986822268053157528350814229 + ], + [ + 2131257578483960220347030216154712350782825143774667810440672905625819556803, + 868491622632592338574614600558827126185061061280839427086037749491272636942 + ], + [ + 2219005280109778946483442325473674751018277532855518690831693876689847358912, + 268228073082292114390894552479318098546823436290210690954657472261887508233 + ], + [ + 2467777871565605588821089206361725030593121231139954405293743820019316429337, + 2069991336503010889436721322920702620510647146193316568814112316083040831176 + ], + [ + 3577501695097063979000880929536422727582550178446643147830010270693854635348, + 2035294884291881522017348441414523234930446764971044277218808172917534058938 + ], + [ + 2916007638012715951040881085223392656327753091743451984926427831539302440694, + 101999113634064710169079402836107323839592545174054596535848670931124794072 + ], + [ + 2824482828379328312126109430912691348039395134484392469690037535472985287979, + 170123149569960330410734287368175015043768931496132108203420592659991682297 + ], + [ + 1722717317273017311710033441508078253294103515222910371014974460548425614659, + 2455794982320624552496521720819953146480298894982220570859668985011878438440 + ], + [ + 3323332844428319232630841298737492488606773387534937061908049192786445054249, + 2775452603532873679877923100539353685218185462328621498382613902225794012998 + ], + [ + 1608012153975591201510431671483571431120513181059080848126129285407754084748, + 575592304166370707508545110070168578909474113291472127326388774685581172878 + ], + [ + 916809345825028426832118427347317839178426151786124666474024597549724310957, + 3069555112506328035675763181932227894553434381085323151827207060709357462248 + ], + [ + 153560760075250381766471501226441998688409013753252062800770032940893580349, + 2329038724186047986576327612748573076822768420981449905843361105210433443270 + ], + [ + 2251563274489750535117886426533222435294046428347329203627021249169616184184, + 1798716007562728905295480679789526322175868328062420237419143593021674992973 + ], + [ + 1952032427782133959985228051054870623876234309599006856796795466237953231448, + 744413679445899225088843138289996934867918835619653321950444531036817070023 + ], + [ + 3162883296795762041497700357918312017339509549159370543230518526626437112653, + 1148816493574468887215135074549621724232310091624566542087195747872792868322 + ], + [ + 786950607934610388520508637842877731308927224203614303759337398330090790623, + 444134697710380413360573180457740543414390667171294106626775858829645857259 + ], + [ + 2138414695194151160943305727036575959195309218611738193261179310511854807447, + 113410276730064486255102093846540133784865286929052426931474106396135072156 + ], + [ + 955835104121335308947276399583596848959851821452355864667907221325383590383, + 1935683370537209806184243406809646867168041420482463925534831987536519170687 + ], + [ + 1499611007946205543199170798196098786940176368383871138867689500928607330374, + 2662008410012406974490205380527854666032675717377005780162761284021647432331 + ], + [ + 2335197826477933983066242217922654955176345813321200025173943441365679314150, + 86799141726517067858463816876244730494086420214168736349361077938664862865 + ], + [ + 2426305297748003258516448191982872419574201776532744568583614742711471397997, + 611146900843862661160935598912412637915904741887617221933114742269852367033 + ], + [ + 1819742747902080962279305391799448622733402573848283520339621821028260796959, + 1894241144684539746894260685967399165405614901992249187163143950195275701901 + ], + [ + 1566900754533300858179807012858131246327928750222566819288909586499451206334, + 2323811598271524355017219639060977623543930469383254150460465078663134678714 + ], + [ + 1338097767250711433700785616700918851742529208514529951106274243527927831096, + 3601290097662913510540208850467494388944371519879253105483940917906091319433 + ], + [ + 1683076217460316609061101548674917172577600596732354862732552514637488814573, + 2586296790559369551539502725860235780473467857462493373020086667442440193461 + ], + [ + 3470075416984304553645258515396988848402044843556043814143694549291509194436, + 1666299363887708992239894153878840011200424023864139235605837090808931067390 + ], + [ + 129884650079435408471813606095297277059523238954518094473826756223429238429, + 913031838387203882010246435451325905809656720680281880669305438372695953289 + ], + [ + 3445652480962486836667659474432628384578750077745289335336644514805586651762, + 1741653654983215120166402497338863916929176017304303902363120840611844416235 + ], + [ + 1173074699967858342716736787366933102436976281324119440358211746283473098092, + 3075964035581410146694612776964161590283242001470153540218734715257792166291 + ], + [ + 1948864707183825487572530934859230839519236819821721157111541446530257224135, + 798780108424505404488913172014962400148988458061505603829909122085722968572 + ], + [ + 3165333849798411161138142302194551326502959074734249886012073955960267320358, + 2854077232678939262337800847761960471531706329482426572081757005793264065021 + ], + [ + 584219684922011437746073096716853840728059302768805720402293458731947997988, + 2691605392627141392945611586058726186445966055416403501891746673585009447073 + ], + [ + 264865088823397344079537851791331047852344700250947641609789815815437187715, + 856557814156118883640651140266045634237123586226962432863642966399740605873 + ], + [ + 1212105529543873848411369707567547187480526619270372944387195134251016552437, + 2552579868998151139055870387891260255591250119487805856658511804923635744103 + ], + [ + 2759929251220121311757096521396321905172783069804137943187241377410890848589, + 1013388642776766405334075600441854256934688539631486641630323046104523794325 + ], + [ + 3435662243060467637677279408549422366191108299439077218684237876763341810879, + 1206100200764686560370450625743787352415337337047308087134628700539206972086 + ], + [ + 1977901254287550894956728572957134897985203087357872251941076511047285452123, + 1346842642356348816775497637530312996348462152604627197529080983616576235485 + ], + [ + 1581907638985290987975047412112008678916597048367684526184099730550194065415, + 2288246533671252346247687962179397967163408912494860510981460961024159618572 + ], + [ + 3215377452673680185835021004830688268079390648010676635220128902519168416923, + 1674957009593589739638616939189211577318542927235942161494800623173476894918 + ], + [ + 3396057617195027713159897957858092093258826926757827467732086865786377444102, + 2403568170323924513104032431512050355627094479417158474914772545814875796054 + ], + [ + 325310856310207888841271598900176645709547071516271373679357299878055669525, + 2861677118910432905996905108378731982262901938718390134034663016688312343427 + ], + [ + 1925427404573041560549638633971822727278056640694873177265388259055141273564, + 2149185931265263003264629477521934555446245963141224065368283592107464297049 + ], + [ + 1057655004272797062633705695597766087036831552982842977913880603549380151466, + 2130130885627581719544847915036196574607634081790527579681856058518350163936 + ], + [ + 1786242059890666469307365196518174814453782118901787482186686744245818012463, + 2172680482993759140271167348144713503721696709986572085951066664839388293484 + ], + [ + 2592525455921742135918400168106149306210970009440868747852006912674191605906, + 2550305630907505809534995587257079662509793820536464892848197484572098781014 + ], + [ + 111752568734708072011137681957171091740064310210630952635788677462490949489, + 1966538689680059826741582906363349945575114015419692615822155104740977191321 + ], + [ + 2324264192758669920944928844116594735168711655659168025211571634463212989215, + 2453209181874565651016960427707879723586882840596638134526741863234420038217 + ], + [ + 818792252748214652222203568101444114842946877275244081137505104552993989249, + 2471135473251293858404864644674805198948427457640609601515835463680395027066 + ], + [ + 776167224210253175836285301928858826524230272765386107941590743597167238720, + 3044593275856778999572277625341930135050598088363464944610781758478552167560 + ], + [ + 2767418361596469194389897577063980781767359707916121685125519618739970163355, + 1124183356341334924855702050560569148670124301541230516023323823985404910253 + ], + [ + 3513158900447819618700445228863547036694626890925843610953740962149603482734, + 2038286698090839235054970774714871346106378670221757061557885786099548421055 + ], + [ + 517681831514430710817158481088000571456636089484182444914961386383717794582, + 1459737506190708125961565377404508114648445797956698177580320945537417189767 + ], + [ + 2764450063359771480330846034975350106048674448417446553801535244886068866892, + 2500955915434901122019038304617790171097079017113487693837548584032628559083 + ], + [ + 3184601066099193053823632069416872853499121524829816523443698520118638972221, + 111399286855641690694757616558822271075634785548525857820633539322287438076 + ], + [ + 1861764219607632476451968430680705159045765477029736548085454909942815526303, + 3542081125583459227861063385092240523576730790188055012978461087647652048846 + ], + [ + 10925555246854055743289295413923076326390541878672074284471241830391529976, + 810379063403665716239985697021625834252465819699617633253170927366131373937 + ], + [ + 1925469472110966024666451107813057200428670133173573112269655451746350245386, + 1187189363484828103108277567024110279560050728332792104397849042655830458502 + ], + [ + 1257234032480175630185580604439948675879173824493927714083831311549585046766, + 482591378023150470327639933521764621888328430410377343971291074047537101431 + ], + [ + 2334319993908953455700110484178707117214967972232088939662977870360647156970, + 2818056499399790981578195943345246458116384474394556164088380917912187593212 + ], + [ + 1453145677148250112321822321443172335864030607730961070308740138762391252935, + 3617981792798631412887889951712686522258702128207981768326584469873932484754 + ], + [ + 264643236147417159992044802699391039945632102423375233223220671619422214434, + 610904689309149986363589451581676922381464434446910426922817060056083567097 + ], + [ + 2773555644105234303846021450759411618679442176305095821653566380427481880668, + 2878405667973852006171581845359598408350517329366546879570244783921286317021 + ], + [ + 2380074631138752101437386328647668302433161481157298955599519485146473998414, + 2940162033501039253058405622329360778854888527422589401316007987697096499987 + ], + [ + 59923324335814857005652966889729706976632643533439489239403033851211490829, + 3181196819190011787131540430610930324408745638588775047528706552833472475080 + ], + [ + 1571134397970145634572604253962346713191790104259693653437624133884723196915, + 511309416166810662427969443722895543878733742083314900009440861143766415361 + ], + [ + 21090530989878836908367190554625378531500191692603997969723554269923877778, + 2771500794785272828011587832172996564585508124740321638858260733779070073032 + ], + [ + 621303564182234060808181503818404204870407855544326969616228975404966772646, + 1381758119149546152274589114806432955337739448745667222869629159555498152415 + ], + [ + 1603367875109750844401808777605019545782079142993759227560886400939494569928, + 3404233473008527201696274593235213333767802823633271526617525914604458121747 + ], + [ + 3064432839007969661251145008088305693584410819966794512614368597856219962482, + 783256745930834086288610539747908078655069214006352981421868494072213571354 + ], + [ + 2573509260611834641929601495624663326040450806493971688424856342734838037808, + 489632329414366712473800098158402333281555035744201997032862455100685111530 + ], + [ + 1373013110025567380060459077575726370083805340382038749245013725221282327457, + 2271658983436031571764113689216829854166985975189230813662757962593273370705 + ], + [ + 243466876262712870310499646879826213360676861126171299065326838396697300684, + 891923608292171984769797750319491387015995834783589731015608767716778489152 + ], + [ + 2035465927594189567864787276274121052811830838707542644166137070067107322500, + 2293243214098049976425980695256140756537564052456935637846524530699635529072 + ], + [ + 1715507122267435124880907385699722499007841543359383748906967432836714601915, + 2065683622001523424317888964929493250974883654939917054448293842930286201120 + ], + [ + 2906653642813167415863836383642871880050990532175816005542735996509246708445, + 2101103582375172516655999605681405804159432128657161521495199065707274548843 + ], + [ + 1560478280364678701345899621740888632653158026800096320697331299727453323955, + 2502904681091262932533150924633878024793617322541538974642118205130709004124 + ], + [ + 1088825136235742041556075285669369753699521218552343596173215811771470309575, + 607071558939135607706029375168245965172230512282049740495005044883485254652 + ], + [ + 416480930348830698091647060484670913532953382502409400899099946640850762655, + 2303434329028515556374106529351629273202094072128674875449835104642893549223 + ], + [ + 1059226326329269227798760274851746346121838934144866840722833084379215695718, + 455998255867283640545200132173274853754233138086189946906020858549998434504 + ], + [ + 1024559718945017260657672873923464530437289768440141950557818820996778411842, + 525500033723589597309036453730301911712422803406229661029628208639589866511 + ], + [ + 2056028306188984024699016201885443563164872865972667060670091877117616276834, + 1419412493706512914027360568510428136300835377049697326470468114560769950308 + ], + [ + 2521038744059190227863027780309672155134248925803510182471085885871826328010, + 2055664921579551707407017847075753140537141245184993747243302270696485943484 + ], + [ + 1408309654797092197568389828883985051397496347829594535681640019995307652445, + 295921513421790144469734685069951228414338437813405505294106756210992032229 + ], + [ + 1987857595984681386153299845716030466354725651174836127615155461210837869564, + 2293923060372667346124288305276256569940238390418855305479173407955750665287 + ], + [ + 1074973828105956295697455820943040985586724095278707830076756602555601108444, + 3326765437245957223406988059616777659852354773867298474672160569379624598430 + ], + [ + 1401862061794147305806657568952279890802547970381675217693442582127912005310, + 910058274349373118447954573574441457656632333180366570053619401133150262813 + ], + [ + 2040966475187373851921668684777109791068677257197750053913982531497273133975, + 1167958332666304570814202030094477634345363022473150543742806601986529818461 + ], + [ + 2881780782119017791457583552000797082603415839437706575588805401515980505801, + 2436105237174666086889653702042028393653105027534224748338553775980791489395 + ], + [ + 1138781660141971412207137170011812332951155719283266572057616562955205995829, + 2657768812194147024915418195050409322486415583242017023943729410483676474626 + ], + [ + 88581112279530612334542943009705636136002388463681106693549082166685838725, + 3496134215297708121790719225608456452574651558469594407547800088961881026765 + ], + [ + 866718519953191029204822844477480603275933535155329865978768093814160529674, + 2022882774094084071548678749116103222489874860976240925473298660635212244802 + ], + [ + 1103918207045639319456460136379598726437052653753960546349780312287131158223, + 1656811459960430943611479537236804458740132078453755963357596576700576629867 + ], + [ + 2975764785119134105842544511285384981490365867343400021422469912399500877645, + 2687980107817591862475555961986751017099827078208385723645417765552791682099 + ], + [ + 2054447365318177209738241671190147649701574355450360479017715224292009927393, + 1227206940403531159772964107232373497220530020193348767638035040017379885984 + ], + [ + 402275007431361887464920597581616397711484740464854159502779178788056390885, + 1547405786479003871142635515616266099578579586990668594451288428801472362903 + ], + [ + 2669324117696876754084336282080783960314916540857113798726442554388023656184, + 3074286759765067223595900394644532377637168990646921851127828180794850850636 + ], + [ + 611932396022950536967338459506782836623876614969030243077175645873783092390, + 2986433254570022449958119232739190557250941610456904564848292348906028792067 + ], + [ + 1110995377995699045828083953623434494450657345505392977032896710538879544144, + 3071470793346350302357608763590610007478960895381257765814531991563057030742 + ], + [ + 2191168535074842056167853224940102773478321048031820308130136731344529117475, + 492617323352024208418412531798559543092874332708570994021422823607454559418 + ], + [ + 1818759115593575585308068448791508192789866090941451793170759417890828490762, + 1305353903124550364838683686338031038285043137449345518795766390174337151903 + ], + [ + 2631013586413722723014078756848572110540232554858227410413816548047325168051, + 2105417412111265728359541491742019675861543959418319743611117424131871073867 + ], + [ + 494722738667200879981866755759016615873180216451714847099458992922362500227, + 1539404447578224883291548288433633968332223396361092124453992010526489574088 + ], + [ + 1894316435470914382555165707375744683572341270069865836107052559173764639667, + 401287370121559774698743821124544382770349508247396498174321596110198582372 + ], + [ + 1339247525522496394127023115420380868431447741930632224270989291600289579235, + 2330930519705316142879600410372647788413471043363313385713997026124191817517 + ], + [ + 2320587088454453474461645476206103420492539243459981382841070259500342411471, + 707303545980793210971675132996162637283200017126206538680628878370110521153 + ], + [ + 2377659897024221375470644518744478788982138263672700674443630590978849683809, + 3419989952269036538781036113174387172326270380530270982346793780460729200034 + ], + [ + 2687585515362755421896341880578567868284030156693703993329134509207013359293, + 550674138339341111775701426337380778844351792663711912206724163064378058172 + ], + [ + 619785501193479957534515714310017569776352132000912157061487205381157886132, + 1961366910419378396302827972401933597848792446727660497590498798601343853897 + ], + [ + 2960597928226130798805429646261618976240340007412692237798738046399566677924, + 3508695180344311906202946672427154710274134142520774461776498819979015518079 + ], + [ + 2926849849449158589418504157622781316761588577631946629896933307400237548255, + 3579904677936943713540587614497052174139389714845927197988064028772422409729 + ], + [ + 2528266877173878010055911786705258188545546534139599446181068037003246641972, + 2090593124885859667054468890302718556366617309852711480847196828672489780384 + ], + [ + 2444375104485440204606531945078810145038148666393834678853843328800784491413, + 2715725794432857257355582195062001572910526811270194737205485553709223325149 + ], + [ + 22830464908174189828089916657865484119153417213263588787115884234394820551, + 2144656372197073668726998695697011727390449826738494763726208492934675687351 + ], + [ + 2160911736065509245304608365930348966695110267270046309451656634373627649798, + 1163682116033125825657519424156769471044453229066709940373641637423931934054 + ], + [ + 3337375727076925923347860860990507186997043901018925752183704995009780982617, + 789849303430659746633537234664825369690245855962498730835768457501816445262 + ], + [ + 2716386435720211080341482697862373973972358535390181213762877306777737012402, + 1694833211852186945995412154030088403536183890500755705683031155572812198349 + ], + [ + 2068303401887779893147375033717840031634024130559056428854775490478246923099, + 1302558735633136858997628501160702103360976241643583615145118788605800501823 + ], + [ + 2528669114399089536204487719847492540160633221112006566613245017377953815975, + 3355482321728480378876070603715444548564989647597270339584107783929482403385 + ], + [ + 1261599618177618962793920186597687295419265824130061936332619302494768478919, + 2783617516834130711477034905360493329011706287809097673216506817763076782638 + ], + [ + 2296010137652334615076906848600702206420819604705598933314103330038064507904, + 593137988508667983800537181778974626700843711375991890922024569808180330689 + ], + [ + 3550068663997426488921976653382588625011435366616319009448120503817895378028, + 333410037907206817306890788780367628702748092247180262748072669817226553712 + ], + [ + 1578244233650262393441190533726865755228496176869169339972883019206417143875, + 2916675240291132751272523072177820169167325640678555998767122677068069504203 + ], + [ + 2578708307946151727285356080366654771256622596667386016883098096024764936468, + 3547149991717605187248264974055140267141465574101933313814496584854066409070 + ], + [ + 1067242594064079501648168388172061910333397472745655832282853323030820063843, + 108409220808480307369448490727067533487994293376444700684037781053298403259 + ], + [ + 1438879968873284678993516485008080871992105419269026450500135928583384168688, + 333856453999414929392413408589358932084193824353874579187651515313020359769 + ], + [ + 2085247683945044480852971266898030909967072967111091473808832254928660354607, + 2485835628881711191910958556160457454778996828420985267633930125611726115768 + ], + [ + 477210647966136763919276861800763093178095337893692383194424592690231940061, + 249607430987277999234526517540164575126654109544108544480054797746736570800 + ], + [ + 3439303457066047146897968415062046665605853275226669283725228224973410585910, + 576124419653395914968187737099874519814103127209134795696921745172483058236 + ], + [ + 3028687819239678122848199946542573863395992135514552484651611678970005763129, + 2961643823025691121513520662201781259179479037567198808346912150041025760642 + ], + [ + 3217248534103588798939334232631337032410025788470581973981396764231465681047, + 2382487873308676654385098962584563887276844064402347832716704744043722784812 + ], + [ + 70223145378785170243702578121178644986245368450401326703486231450287863627, + 566197969536520322901287947074318556204558945230303949390810423446802406323 + ], + [ + 3028144136397350722039435806065376945342224221363467830775872476071042099596, + 167098690867154485139002287996988541908068352838119188548912894969546326088 + ], + [ + 1424858530411014835161893358951895950005600021438253129735780882682252689593, + 2305727793424806539572292293130051301594968780927626275319782923070998204084 + ], + [ + 2137513716516327603808109201538743935930903413061745351331773762548511760158, + 1642530299556920094344308735995008094003081457462443723760327041776469019076 + ], + [ + 2170444911584390774802338541806841054793712556211735475250435550223474552592, + 925789261675057834371037875523035088704965828465840628994713637520457333016 + ], + [ + 239299595833165228919005931278081722748571104921165506568868902567605074538, + 485001543376876278371961680415603784960370504830892419674368813289405424875 + ], + [ + 3080380062043844759018736411849858262358110468668995639984779021951968636078, + 2402265611218391177266008066876605247210502592712781467199097469773462678225 + ], + [ + 726523175548388386261502324171774291430220813407320118077006064537560392330, + 786330320941681238670628195289980168691286367143496676979339036480301999376 + ], + [ + 527181411328890981971052946489613295601720332326053123212209359114463976205, + 2153641849366054831187204654204168306183349514724183599886856681068048360647 + ], + [ + 548403912567710249138395852931702442214070304260935779684833978128636533986, + 1820119432165084967589110130985988930607430366654346658826186881340393135345 + ], + [ + 1055707400443116102787479640936967686027177253722371245083270825233659393085, + 1529404944427212407459549740446357844894249706911152782301314430602619804108 + ], + [ + 3604699563774018613013478662139866299897364136688436784910471089709564771121, + 1877420639173665439817870412707771026257151154196414247016132896387470215862 + ], + [ + 1334227623025169197188407571635794051283637310231760303667085410581915700021, + 1949877078010470778680860969498171171122335730952599024624803418631307495103 + ], + [ + 3308762084022080981421381628996649710517264452719086435874478919169368804715, + 2402101985509711398701847117755018203425777675996940000965112623957330145868 + ], + [ + 1665881871958289138802712383541734953129067055326286239727041865486531283570, + 3616247644667366899175075134655397038478794588048970408476914992988017251161 + ], + [ + 2112164335316897143532032199025725867588905166012009239598709182355280065515, + 2825402900866689367007140167090188180425426910788355395313762834688260248117 + ], + [ + 634951525865404702868066023611980901349684561467873427882470721637495099919, + 2178647893306834053326446190201725650048611097288227403773599186740980976285 + ], + [ + 1341178436815612054719172387245100541421209962514061177862693819245929984135, + 1326643103813011222249037873591741605099080649257821474786188634904101413693 + ], + [ + 1928710057077228449920040314271488497108943704097705502064531376997670270931, + 2266181469128837281287656381761672050739367882500404480638892977272852366881 + ], + [ + 969218608841843132346322324841729971529593954176078767277133720598489668357, + 606900183168454040777559974758602149603502324650244544303079399500690042393 + ], + [ + 2013350832011494097843192023013584600183073420263235866273101164869312611708, + 90712660236563155479754789398598402903732977477056735001011271430022436372 + ], + [ + 3396976507268771940572242446745957441443927281190120339106378150765794683243, + 2412741837818252081470894623640222254809923360460175760983381510548136273698 + ], + [ + 3606409001635563285156761927226165887070229114178147857036216526058311335249, + 1092396833411428890667907446500502751825053988217814009934347833612745642070 + ], + [ + 1544923824473254019116239003129558626125362616742425500116394033599505254529, + 1465349443371708061238467795142261954743713344644799596216285990321977000373 + ], + [ + 113491804090467857612125750322844575213787440679203395630526874638303077885, + 2028057101479207102461777655354995420997496563459354054546268475793811643845 + ], + [ + 1652495545845371778467883968680026022222422089600601509520876233591221298667, + 1257512680478734992352959448953843834930888651298716349936596227713642044718 + ], + [ + 1735132339493184243340725542423753987254833174769560101474361526571946583351, + 27330540439335548628321946448059137005887875330678118281933391074303528644 + ], + [ + 999589601024651596708840623565818878499651672244526787051482333844246243941, + 1556707544224102520772713942483754928174646286011090264336584778980967757013 + ], + [ + 1681312560856804229526683984753830811943700510863269888887897515341932766251, + 2065757756192665992865056781757244443742379869237378116242523960469865631998 + ], + [ + 2058951870735992982026605733702307208101813514180467857318656495202383185176, + 3268996299695136959341188422640393244547564085474313632107392993963324140014 + ], + [ + 2056960676848885737798371134589584163780497279931334671919866051948581811168, + 1741359147160041371302807510240561900995865771279421888187249838945794238723 + ], + [ + 1167002070266749840825005896490956016236165528302729567545471999112944906391, + 724716405138963198014997547695908810889048549156514080027596147160291370305 + ], + [ + 717433242268160049163784137690479364638414455354853503549047874912074602102, + 1548748206365927633434472413810887260565959748266730133832127499425371516599 + ], + [ + 1113770395880110291540688660582111108167550152891676896388447618267044770699, + 225614903467949057887693353344008462684737622787852941160491287904124662539 + ], + [ + 1485058593675291436770632813229171213081600725926085342541850536046844108938, + 1793621954142096162709529585852653649155672217087744074142012644930653464937 + ], + [ + 3476670695984120275482827092638881388770060638684248848714588194147006700081, + 1886524232365250879956678425943479251575532912879489640410209691903853633501 + ], + [ + 78942502761918568770028495237440346726648682399909104284710887733399086106, + 2352329060006211734866796061368085624251562468004096360671974444953215839619 + ], + [ + 813411306209308549231686755344288448950826770557611647490856063401538266082, + 388260357229176651922921451505223699462456291090208287248038895703757789197 + ], + [ + 2400377653395741560762585517571389386824872672242294297839787377212513042206, + 625963574857112775116550276725479329998557438550085741821320481021022314633 + ], + [ + 2328311306030212189043981540771838515808167713931548944984202663139515716155, + 2492267419179473373149177460543961766435995439950143787911026640253198630634 + ], + [ + 2387278462118275663019682655223568758817746496473668610784380666316276847549, + 2366392912056517351075178537570019722508034279024404671859228940369425653110 + ], + [ + 2114043901163989266960550512790503188352140805965143750858598214845925650606, + 1263535257267454609784459072919175411484521178579707297022413088083025242903 + ], + [ + 59742145227906406483850036502479681434689473008676866553687016399244912392, + 2542708853923609004067985135730679808561712875491472740521746862583772210812 + ], + [ + 636010637932045133320800908704730609314370804715286131693312911610589568614, + 961184011917051478754418596971670071602153673068923576248721877819369343802 + ], + [ + 1385321288061225556756217710115128107797163633073592537851457891527164069955, + 884133458066727093516473175829774361048238638693699012568518768170315343692 + ], + [ + 758367055389438625364170431009355773885665422613428814074753204465381568635, + 1297727568042288931569138891121976026502156814623013130863114577102812764955 + ], + [ + 347250288556045883174524792293804631653970133899259158293492482097831569584, + 1961316292966031207016509998947408765986081794211886403901120755246344327975 + ], + [ + 1618690143706255801807773941554549190817935595982010405820190876061657720006, + 869166062594354289846899813362309895350435099932527145815122092397979607037 + ], + [ + 1838634315648320098377933813041132543577219555069642930639820023291216325415, + 1529983605494771676066515349178506396294249260663468133203781588859819007226 + ], + [ + 1605761809110472849967286703249646269449262088256390507097425667304759998260, + 2778705695237273113564800057362189513234607857754067598956643514325456052945 + ], + [ + 3411908887615707635377666445857046055873541270252857286357865762696431003144, + 731871578063282896385513070338640294885089868946599519285295305156675019160 + ], + [ + 3087826179342245618253320870694339144383060104280990174148199503867526625383, + 3491370412646223145431588455284304413010853471815192082926914611232193577659 + ], + [ + 1436482751270429389743178806038120026461471807922309148229447715029903414786, + 1666868445318653269944338633661851246179662160526363040276638175667354338167 + ], + [ + 2083331818739824497741363579518842947887353398223338451239015909684569222581, + 803732308089347618300580814078570019393110433387113056533917045601819866173 + ], + [ + 1333165738767294811324643473740459024513360282013848003638568328453569807861, + 3324872774354633383424936425649606578793830970593411555017483797025902771350 + ], + [ + 1701353517399938600269579188937389421636718828509341624534004474699861544304, + 982045252370486712463601150815912589800926352240648829942255288306435594694 + ], + [ + 154403491180633257693040438747387470378033364863415878175486169393433469355, + 3204938488959824048546277939996603027140231747699901842459271581175521669368 + ], + [ + 723459534533890816489433777735842913898349668429456824296910758117461179034, + 242799587564906089500242560200906533162881507520621156475321851862194708981 + ], + [ + 2753553332546729968386491500304817162737325726065761497175967890302687642812, + 1607942131855693595003166153590488091960419143993462584570217492975562129656 + ], + [ + 1318380449900940858865061333131606747009833602974109493687016869645480407519, + 824831465625011385685225132275899244334146133266851046834317903702436443339 + ], + [ + 3406605858133553361333265720938744225870879664771897953784469453809670221140, + 951771439755520751413184037659273661906475899204291367776225253586492726939 + ], + [ + 322881185844437667428941222631269048437916337535492466690110884912942662893, + 2408973683372380251710642884281850709418801321112976394586138388159314573007 + ], + [ + 405651299272338889177305609117326577035390334272323319632791541534976007116, + 2403189350929802616202215631814070933678092854541380504903809068323291560762 + ], + [ + 224362689432001608162440684107608338550077679080690583663897253625637368804, + 1089475930411433192334001657654788489125032803526758217361824123802939165263 + ], + [ + 3427005033874860450481529297496326414099998098069008018777339291027217607772, + 1838523767315535107424920135038015356690704952046462913411177863052889918094 + ], + [ + 971265369167429416740822747897568727646706124584943344118217646401474769333, + 1841017974237665428880957941884047733042022771551566493876831131552515616862 + ], + [ + 593870383382776876660279010014844606756390978498220975404532129561707141345, + 1686799834245885653972636115594234495471835994524713453126028613189622517928 + ], + [ + 158526420068961916859457596090914564342752702816639997380569526445580395317, + 829117473118004491391038359292216575457137041347974965494549565557957856696 + ], + [ + 1145779849055685339419364615770769333393499278335670275163049680913140509683, + 2788919154400397863209918313477978879516215345298341199550275309881538159925 + ], + [ + 3116362794833038183976492329109547313095843900691440633169686540582464014059, + 3493161563205582068593545591027301392887053692389997071622408416554079460959 + ], + [ + 1484626752109997960148427439402147978447920625565926552692192513400801032840, + 106197164952655019784492783946283241349642545386756801239606388396528838557 + ], + [ + 1905886704700624859591161108842694006068401527337545253689518506514368311453, + 2930319791340393503878111037261975689788636509301758506631633748789105811524 + ], + [ + 2520975745491353699235764487116637116999082965127347369976279028452140192996, + 1121629571053704096076618751099836481664435087676492791791464786738401934277 + ], + [ + 243197297174950489047429059272755382647269925832357483684904121385442353161, + 2417007034799146494310932386003928951978739444041601103686030150489540099099 + ], + [ + 808704503940529633042533527196170354739879165235854099335954018048090166094, + 3250069164566476474875427019235306310423414676070245668865855537758860899226 + ], + [ + 154229390082782874040656704404635500870072484057583506571360633170672994819, + 2702580522127958238752043600053843204107536082922189692357192526470020183763 + ], + [ + 2296876062395150663198655775945704456626070946553449162794630784785525562017, + 2954047352783324167659477602480890942097095078541501369002104009282407983605 + ], + [ + 2369843936932443670822535910120393374869045918271657278773939763527759009977, + 1504523220772650882417075434754212591932774780940943124208852030753505106366 + ], + [ + 939818528450006271533633387575035651098636084551955010694000346778607054148, + 3288036962429189904257974342922419507417520404552917887258043999949243251337 + ], + [ + 3261482392020733556192001297773623607067547108643955544388526649674482826812, + 3591646574338256506691865028073929692592246225874807859597509821099836701353 + ], + [ + 1787025500314509707209621873912066561653490978360118110526034359358411964417, + 3421109838181987883898563834025345980326698037134918112926105203592921164968 + ], + [ + 2311067462891595099991833582046690970468764702035366824733716384542858538356, + 470591103372156288650571624843786296549423881481598013229606695835762897581 + ], + [ + 342559482224571965025189023541087083168706464251790471061097523992515769052, + 2368202754453331817392255123704696636258753331639084720511390832405023708522 + ], + [ + 2572444430230869831563822697183525106374829659556462575156759807443152314062, + 1629527166738200666038465328035159152469458766421654684492365855846947518815 + ], + [ + 1737389803319738463977618408470915902963778725702701713925657991380950954649, + 607082767712049126327196794168950822631607164722155751481706035333917604213 + ], + [ + 2871556558000552242893836092551691122481664492915647218631883507493547887264, + 500384672074954803514661117946887054859762315512424468765642494189718439748 + ], + [ + 3461109898461294404913712148331642005392308171104928758316008253448940997221, + 1749611977268851605988230236285965166361721258988901681470546570063157256891 + ], + [ + 78615564253467413638171585726207276986620474184921384429093571227531830518, + 1981471018460691817560093727855711117339515783816308036046814473313844294189 + ], + [ + 1474473102515930508594064469182212180992913337870765723235685741540431041359, + 1469840572053747949720853289796099707562318378077626046833147818685776895044 + ], + [ + 2774277348282328170085742674659971495571500392161601753293416467112865068097, + 3137849475565620656681313987560185334413793189698703604034985791122356336275 + ], + [ + 672166412479844650342747861081472852496472428762577522054587528815692740558, + 1663866879616164274903483340205017481052054060286752355521506181082184021495 + ], + [ + 3180945697595716737370693680466484191429900897946060952610697010699886591271, + 370261256417300103212360232751326303498788243730491970857436044670395786744 + ], + [ + 1201682462650513594149029305106730482154241597912555054934133306913124052927, + 2702519627582077982048055809065014719130205668704469328908061241215382284753 + ], + [ + 2573098232031718801548737595348908190100964683270928802802545310591952021697, + 421880381247280215923854338509978900130125935079274502459764130391391089799 + ], + [ + 3080354619128885268149548971617703089581818473398812557354479708280835955809, + 256390985131798172675848658065159234227830929883285189158702397702435740036 + ], + [ + 3305006433068254197629607860194494003641107593674801290918966761186348868243, + 3339026583930460592461023889277320373514761589020084302062859793412730118034 + ], + [ + 1923241940978623629561560707497295652725232345651095835780507541645042579369, + 1034987404476549078390013964157937064176009291357135198452850234267991254512 + ], + [ + 1242878056067701165715334664693705019638823755244701687487484225344981621708, + 2778020811088591142544537894197696376781458923989081525073574174237563505877 + ], + [ + 103159350300766901891674139940778235823347527941771255048984655353665811131, + 2998408586900586528158477448701326671048806329090729531148457644075161991659 + ], + [ + 71218614431436475107665191107448218056096758969742620338129750710339397239, + 2301542035924343190515686028267024414404475677263930511758245534549493426119 + ], + [ + 1192223060475270158246514565467403478627193592812213292121865811070708026418, + 3208674842884877565377729626392486507374130037365033772947426862394273679084 + ], + [ + 2701994874964587709938210915198043688526355413043491003913483399075336026535, + 2886333482965974313702782452985415254520267819631653595494065556318924996302 + ], + [ + 3306009714468475346677294651301864623371911173285781201209240934162798592783, + 605466198613255313875059832114128899690381126611676641778478133738267702577 + ], + [ + 1450098270166428194278914193816868101013739489893607935042066987079292018245, + 60254834310937851613452000117427371272748499709961477924319944539270878930 + ], + [ + 3472634693037182832142729498155218303319113785483118130849719300023909673520, + 1641508292869191604146734378916767617013327049910565729753281855691648138118 + ], + [ + 1894817968472977732589643163725633598408826498334595898528746877517402007256, + 193046947184625502016096786396456839948351998258967537298275688147378157385 + ], + [ + 798518742331453195093601874576575285888369661705098199469223675278431898599, + 107377575629504498627957848614396996685294283100119805971247351789316564355 + ], + [ + 72958015465538907944252844948497831886498059248823205873862483987609118945, + 382019162223291772579736282379325284805025953290359275955015292364404489133 + ], + [ + 3314147563388677609583640640385513143694481753397403848688825124581323140921, + 3133015892701145825892878874198767217439365812782859844688129544070997319541 + ], + [ + 2444095071507078967491725470181706633826193926858202958437137361403432629390, + 3611326330343873710939978620686319096878621369582121415988028359537867630792 + ], + [ + 2107740367200683219354058003519333570583941271011338026730935614636775950367, + 953104025671531038979165726896993106119997272237926795749220750824852831326 + ], + [ + 3234656093348684616411954956071945712517868853923240328793520762092030352104, + 593797219275153872348854593022027262920747808515266478952508298984248786194 + ], + [ + 1602095369325252473599793473477096021341779224145909730627015015569257565602, + 3272207934512491788245765497830881358513949839139669547034252308088069134971 + ], + [ + 3149982146511124605253586861983261220008132001764223357617600582721741955729, + 2095033709871635786785037791315376486221829475125386227641672208905379085173 + ], + [ + 3001928273494873166012582714776651165587585607934603548408913923445707516447, + 1090266449665582127890982346068170500195143341692394417827957481874850148101 + ], + [ + 1200188694803418163698740652082064619102559508865389996854547617715365179733, + 2701249247866363010207334776256734312390753341409567681725083482804664533572 + ], + [ + 2181535855195652401374211290838795366238299923929423873085735410894622521047, + 2376412857416063533601832417302250911062085318826783757605801470091451365057 + ], + [ + 482216227882593950837484293882738923170227555670484160841935409522897080275, + 3568625956076637648484022304136932386854238480511347282187445648692365982275 + ], + [ + 2597799669063340267995599892195854202917996255210920411104497198234262573206, + 3599688873496047961188179975715306136394737017927100659789000949688648352738 + ], + [ + 1074803073878259665128704812644508804681887514658526880458531694790041080733, + 2797007314237358398853761880558407828488654212157591536243300688627270327611 + ], + [ + 468516314257643341712703808920275922572701541039900608201362821198817231181, + 1374527914917866113032496762177078616084751647116110264806150108724834406388 + ], + [ + 1208295021405021769918747031548141820393186404250802478605705170489054454157, + 1808650369660257850053512198148641736021245568921649705336796845945032764079 + ], + [ + 1565766387634045687737221757815822397332528241094852872444435795933964809539, + 3012257951756551767601579252037773433674622118381097513581891561867035592007 + ], + [ + 296854483963576847362550837105567519963678928320906132062353506063381160038, + 2997621846549637009998526882695309289938245125374301351844169021609166074993 + ], + [ + 967192065183772403576792198824105189328662921952353748318585728895552782317, + 343259257621685032456419166732348373845783852374756662051672936363788935338 + ], + [ + 456199991075423266290193634515181612721738873753897142916892886319728411618, + 3009804764464949268795409945347301690744692042792259709767667423717478448229 + ], + [ + 242210936510534966601707881924518098930470912600217515366037129037143259602, + 2698396953115175496998177800235267214917400761768300598355994305287141321271 + ], + [ + 943070145114606420402378547694273832277173459266557776382587828687889744387, + 463274201648439487604179878678549669204059390508994779482933722127905403494 + ], + [ + 958740504301613688526614902955971923282735882762427544770668882582144756278, + 1648715733425449195104866742590789175535499260400395501324113794510596751843 + ], + [ + 2738467504442851843676882768027062309179245908086784030957627999641833602734, + 403058158250173068725971433188527745485878446592356816888847490041658726521 + ], + [ + 851584175891221844656684173543344216725376446573878021753176795750916050165, + 2989949954698892115645230865442980084822288702406021287339635187958555345139 + ], + [ + 2648599427528401936572004779373684054684563511138695864967500218937169746782, + 545540526832939914359571382575431287745621956136188859527645452849687661804 + ], + [ + 2781909713387830233242081629088160756893816968845366829353355409163126437112, + 3474066345655056798683792894555452083305620276965763551620554269294421970656 + ], + [ + 62007937137219837991330947293062511223089197148302827535260096716770644685, + 3537197938414984401398955238628360208832764753222288521326061893140886724271 + ], + [ + 2379962749567351885752724891227938183011949129833673362440656643086021394946, + 776496453633298175483985398648758586525933812536653089401905292063708816422 + ], + [ + 553697491755753712548822408932664734674730150084063981046477343718694621804, + 2797798649021537247229237999331435556632872779265479409612091247299955463913 + ], + [ + 2026114267613810970244390071397350467776533880677809710454617259260017487512, + 3330593270696197494966018967263043594632970418364498628573044882141635806155 + ], + [ + 1254733481274108825174693797237617285863727098996450904398879255272288617861, + 2644890941682394074696857415419096381561354281743803087373802494123523779468 + ] + ] + +N_ELEMENT_BITS_ECDSA = math.floor(math.log(FIELD_PRIME, 2)) +assert N_ELEMENT_BITS_ECDSA == 251 + +N_ELEMENT_BITS_HASH = FIELD_PRIME.bit_length() +assert N_ELEMENT_BITS_HASH == 252 + +# Elliptic curve parameters. +assert 2**N_ELEMENT_BITS_ECDSA < EC_ORDER < FIELD_PRIME + +SHIFT_POINT = CONSTANT_POINTS[0] +MINUS_SHIFT_POINT = (SHIFT_POINT[0], FIELD_PRIME - SHIFT_POINT[1]) +EC_GEN = CONSTANT_POINTS[1] + +assert SHIFT_POINT == [ + 0x49EE3EBA8C1600700EE1B87EB599F16716B0B1022947733551FDE4050CA6804, + 0x3CA0CFE4B3BC6DDF346D49D06EA0ED34E621062C0E056C1D0405D266E10268A, +] +assert EC_GEN == [ + 0x1EF15C18599971B7BECED415A40F0C7DEACFD9B0D1819E03D723D8BC943CFCA, + 0x5668060AA49730B7BE4801DF46EC62DE53ECD11ABE43A32873000C36E8DC1F, +] + + +######### +# ECDSA # +######### + +# A type for the digital signature. +ECSignature = Tuple[int, int] + + +class InvalidPublicKeyError(Exception): + def __init__(self): + super().__init__("Given x coordinate does not represent any point on the elliptic curve.") + + +def get_y_coordinate(stark_key_x_coordinate: int) -> int: + """ + Given the x coordinate of a stark_key, returns a possible y coordinate such that together the + point (x,y) is on the curve. + Note that the real y coordinate is either y or -y. + If x is invalid stark_key it throws an error. + """ + + x = stark_key_x_coordinate + y_squared = (x * x * x + ALPHA * x + BETA) % FIELD_PRIME + if not is_quad_residue(y_squared, FIELD_PRIME): + raise InvalidPublicKeyError() + return sqrt_mod(y_squared, FIELD_PRIME, all_roots=True) + + +def get_random_private_key() -> int: + # Returns a private key in the range [1, EC_ORDER). + return secrets.randbelow(EC_ORDER - 1) + 1 + + +def private_key_to_ec_point_on_stark_curve(priv_key: int) -> ECPoint: + assert 0 < priv_key < EC_ORDER + return ec_mult(priv_key, EC_GEN, ALPHA, FIELD_PRIME) + + +def private_to_stark_key(priv_key: int) -> int: + return private_key_to_ec_point_on_stark_curve(priv_key)[0] + + +def inv_mod_curve_size(x: int) -> int: + return div_mod(1, x, EC_ORDER) + + +def generate_k_rfc6979(msg_hash: int, priv_key: int, seed: Optional[int] = None) -> int: + # Pad the message hash, for consistency with the elliptic.js library. + if 1 <= msg_hash.bit_length() % 8 <= 4 and msg_hash.bit_length() >= 248: + # Only if we are one-nibble short: + msg_hash *= 16 + + if seed is None: + extra_entropy = b"" + else: + extra_entropy = seed.to_bytes(math.ceil(seed.bit_length() / 8), "big") + + return generate_k( + EC_ORDER, + priv_key, + hashlib.sha256, + msg_hash.to_bytes(math.ceil(msg_hash.bit_length() / 8), "big"), + extra_entropy=extra_entropy, + ) + + +def sign(msg_hash: int, priv_key: int, seed: Optional[int] = None) -> ECSignature: + # Note: msg_hash must be smaller than 2**N_ELEMENT_BITS_ECDSA. + # Message whose hash is >= 2**N_ELEMENT_BITS_ECDSA cannot be signed. + # This happens with a very small probability. + assert 0 <= msg_hash < 2**N_ELEMENT_BITS_ECDSA, "Message not signable." + + # Choose a valid k. In our version of ECDSA not every k value is valid, + # and there is a negligible probability a drawn k cannot be used for signing. + # This is why we have this loop. + while True: + k = generate_k_rfc6979(msg_hash, priv_key, seed) + # Update seed for next iteration in case the value of k is bad. + if seed is None: + seed = 1 + else: + seed += 1 + + # Cannot fail because 0 < k < EC_ORDER and EC_ORDER is prime. + x = ec_mult(k, EC_GEN, ALPHA, FIELD_PRIME)[0] + + # DIFF: in classic ECDSA, we take int(x) % n. + r = int(x) + if not (1 <= r < 2**N_ELEMENT_BITS_ECDSA): + # Bad value. This fails with negligible probability. + continue + + if (msg_hash + r * priv_key) % EC_ORDER == 0: + # Bad value. This fails with negligible probability. + continue + + w = div_mod(k, msg_hash + r * priv_key, EC_ORDER) + if not (1 <= w < 2**N_ELEMENT_BITS_ECDSA): + # Bad value. This fails with negligible probability. + continue + + s = inv_mod_curve_size(w) + return r, s + + +def mimic_ec_mult_air(m: int, point: ECPoint, shift_point: ECPoint) -> ECPoint: + """ + Computes m * point + shift_point using the same steps like the AIR and throws an exception if + and only if the AIR errors. + """ + assert 0 < m < 2**N_ELEMENT_BITS_ECDSA + partial_sum = shift_point + for _ in range(N_ELEMENT_BITS_ECDSA): + assert partial_sum[0] != point[0] + if m & 1: + partial_sum = ec_add(partial_sum, point, FIELD_PRIME) + point = ec_double(point, ALPHA, FIELD_PRIME) + m >>= 1 + assert m == 0 + return partial_sum + + +def is_point_on_curve(x: int, y: int) -> bool: + return pow(y, 2, FIELD_PRIME) == (pow(x, 3, FIELD_PRIME) + ALPHA * x + BETA) % FIELD_PRIME + + +def is_valid_stark_private_key(private_key: int) -> bool: + """ + Returns whether the given input is a valid STARK private key. + """ + return 0 < private_key < EC_ORDER + + +# def is_valid_stark_key(stark_key: int) -> bool: +# """ +# Returns whether the given input is a valid STARK key. +# """ +# # Only the x coordinate of the point is given, get the y coordinate and make sure that the +# # point is on the curve. +# try: +# get_y_coordinate(stark_key_x_coordinate=stark_key) +# except InvalidPublicKeyError: +# return False +# return True + + +# def verify(msg_hash: int, r: int, s: int, public_key: Union[int, ECPoint]) -> bool: +# # Compute w = s^-1 (mod EC_ORDER). +# assert 1 <= s < EC_ORDER, "s = %s" % s +# w = inv_mod_curve_size(s) + +# # Preassumptions: +# # DIFF: in classic ECDSA, we assert 1 <= r, w <= EC_ORDER-1. +# # Since r, w < 2**N_ELEMENT_BITS_ECDSA < EC_ORDER, we only need to verify r, w != 0. +# assert 1 <= r < 2**N_ELEMENT_BITS_ECDSA, "r = %s" % r +# assert 1 <= w < 2**N_ELEMENT_BITS_ECDSA, "w = %s" % w +# assert 0 <= msg_hash < 2**N_ELEMENT_BITS_ECDSA, "msg_hash = %s" % msg_hash + +# if isinstance(public_key, int): +# # Only the x coordinate of the point is given, check the two possibilities for the y +# # coordinate. +# try: +# y = get_y_coordinate(public_key) +# except InvalidPublicKeyError: +# return False +# return verify(msg_hash, r, s, (public_key, y)) or verify( +# msg_hash, r, s, (public_key, (-y) % FIELD_PRIME) +# ) + +# # The public key is provided as a point. +# assert is_point_on_curve(x=public_key[0], y=public_key[1]) + +# # Signature validation. +# # DIFF: original formula is: +# # x = (w*msg_hash)*EC_GEN + (w*r)*public_key +# # While what we implement is: +# # x = w*(msg_hash*EC_GEN + r*public_key). +# # While both mathematically equivalent, one might error while the other doesn't, +# # given the current implementation. +# # This formula ensures that if the verification errors in our AIR, it errors here as well. +# try: +# zG = mimic_ec_mult_air(msg_hash, EC_GEN, MINUS_SHIFT_POINT) +# rQ = mimic_ec_mult_air(r, public_key, SHIFT_POINT) +# wB = mimic_ec_mult_air(w, ec_add(zG, rQ, FIELD_PRIME), SHIFT_POINT) +# x = ec_add(wB, MINUS_SHIFT_POINT, FIELD_PRIME)[0] +# except AssertionError: +# return False + +# # DIFF: Here we drop the mod n from classic ECDSA. +# return r == x + + +def grind_key(key_seed: int, key_value_limit: int) -> int: # type: ignore[return] + """ + Given a cryptographically-secure seed and a limit, deterministically generates a pseudorandom + key in the range [0, limit). + This is a reference implementation, and cryptographic security is not guaranteed (for example, + it may be vulnerable to side-channel attacks); this function is not recommended for use with key + generation on mainnet. + """ + # Simply taking a uniform value in [0, 2**256) and returning the result modulo key_value_limit + # is not necessarily uniform on [0, key_value_limit). We define max_allowed_value to be a + # multiple of the limit, so that a uniform sample of [0, max_allowed_value) mod key_value_limit + # is uniform on [0, key_value_limit). + max_allowed_value = 2**256 - (2**256 % key_value_limit) + + def to_bytes_no_pad(x: int) -> bytes: + # To conform with the JS implementation, convert integer to bytes using minimal amount of + # bytes possible. We would like 0.to_bytes() to be b'\x00', so a minimal length of 1 is + # enforced. + return x.to_bytes(length=max(1, div_ceil(x.bit_length(), 8)), byteorder="big", signed=False) + + # Increment the index (salt) until the hash value falls in the range [0, max_allowed_value). + for index in itertools.count(): + hash_input = to_bytes_no_pad(key_seed) + to_bytes_no_pad(index) + key = int(hashlib.sha256(hash_input).hexdigest(), 16) + if key < max_allowed_value: + return key % key_value_limit + + +################# +# Pedersen hash # +################# + + +def pedersen_hash(*elements: int) -> int: + return pedersen_hash_as_point(*elements)[0] + + +def pedersen_hash_as_point(*elements: int) -> ECPoint: + """ + Similar to pedersen_hash but also returns the y coordinate of the resulting EC point. + This function is used for testing. + """ + point = SHIFT_POINT + for i, x in enumerate(elements): + assert 0 <= x < FIELD_PRIME + point_list = CONSTANT_POINTS[ + 2 + i * N_ELEMENT_BITS_HASH : 2 + (i + 1) * N_ELEMENT_BITS_HASH + ] + assert len(point_list) == N_ELEMENT_BITS_HASH + for pt in point_list: + assert point[0] != pt[0], "Unhashable input." + if x & 1: + point = ec_add(point, pt, FIELD_PRIME) + x >>= 1 + assert x == 0 + return point diff --git a/ccxt/static_dependencies/starkware/crypto/utils.py b/ccxt/static_dependencies/starkware/crypto/utils.py new file mode 100644 index 0000000..7a16679 --- /dev/null +++ b/ccxt/static_dependencies/starkware/crypto/utils.py @@ -0,0 +1,63 @@ +from typing import ( + AsyncGenerator, + Optional, + TypeVar, +) + +import sys + +if sys.version_info.minor >= 11: + from typing import Literal, ParamSpec +else: + from typing_extensions import Literal, ParamSpec + +T = TypeVar("T") +P = ParamSpec("P") +K = TypeVar("K") +V = TypeVar("V") +TAsyncGenerator = TypeVar("TAsyncGenerator", bound=AsyncGenerator) +NumType = TypeVar("NumType", int, float) +HASH_BYTES = 32 + +# If more shared types start popping up here extract to types.py. +Endianness = Literal["big", "little"] +TComparable = TypeVar("TComparable", bound="Comparable") + +def to_bytes( + value: int, + length: Optional[int] = None, + byte_order: Optional[Endianness] = None, + signed: Optional[bool] = None, +) -> bytes: + """ + Converts the given integer to a bytes object of given length and byte order. + The default values are 32B width (which is the hash result width) and 'big', respectively. + """ + if length is None: + length = HASH_BYTES + + if byte_order is None: + byte_order = "big" + + if signed is None: + signed = False + + return int.to_bytes(value, length=length, byteorder=byte_order, signed=signed) + + +def from_bytes( + value: bytes, + byte_order: Optional[Endianness] = None, + signed: Optional[bool] = None, +) -> int: + """ + Converts the given bytes object (parsed according to the given byte order) to an integer. + Default byte order is 'big'. + """ + if byte_order is None: + byte_order = "big" + + if signed is None: + signed = False + + return int.from_bytes(value, byteorder=byte_order, signed=signed) \ No newline at end of file diff --git a/ccxt/static_dependencies/sympy/__init__.py b/ccxt/static_dependencies/sympy/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ccxt/static_dependencies/sympy/__pycache__/__init__.cpython-311.pyc b/ccxt/static_dependencies/sympy/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..7a1fef2 Binary files /dev/null and b/ccxt/static_dependencies/sympy/__pycache__/__init__.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/sympy/core/__init__.py b/ccxt/static_dependencies/sympy/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ccxt/static_dependencies/sympy/core/__pycache__/__init__.cpython-311.pyc b/ccxt/static_dependencies/sympy/core/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..8fd80fc Binary files /dev/null and b/ccxt/static_dependencies/sympy/core/__pycache__/__init__.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/sympy/core/__pycache__/intfunc.cpython-311.pyc b/ccxt/static_dependencies/sympy/core/__pycache__/intfunc.cpython-311.pyc new file mode 100644 index 0000000..0c36cd7 Binary files /dev/null and b/ccxt/static_dependencies/sympy/core/__pycache__/intfunc.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/sympy/core/intfunc.py b/ccxt/static_dependencies/sympy/core/intfunc.py new file mode 100644 index 0000000..93bb5d3 --- /dev/null +++ b/ccxt/static_dependencies/sympy/core/intfunc.py @@ -0,0 +1,35 @@ +""" +The routines here were removed from numbers.py, power.py, +digits.py and factor_.py so they could be imported into core +without raising circular import errors. + +Although the name 'intfunc' was chosen to represent functions that +work with integers, it can also be thought of as containing +internal/core functions that are needed by the classes of the core. +""" + +from ..external.gmpy import (gcdext) + +def igcdex(a, b): + """Returns x, y, g such that g = x*a + y*b = gcd(a, b). + + Examples + ======== + + >>> from sympy.core.intfunc import igcdex + >>> igcdex(2, 3) + (-1, 1, 1) + >>> igcdex(10, 12) + (-1, 1, 2) + + >>> x, y, g = igcdex(100, 2004) + >>> x, y, g + (-20, 1, 4) + >>> x*100 + y*2004 + 4 + + """ + if (not a) and (not b): + return (0, 1, 0) + g, x, y = gcdext(int(a), int(b)) + return x, y, g diff --git a/ccxt/static_dependencies/sympy/external/__init__.py b/ccxt/static_dependencies/sympy/external/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ccxt/static_dependencies/sympy/external/__pycache__/__init__.cpython-311.pyc b/ccxt/static_dependencies/sympy/external/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..4e3f54c Binary files /dev/null and b/ccxt/static_dependencies/sympy/external/__pycache__/__init__.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/sympy/external/__pycache__/gmpy.cpython-311.pyc b/ccxt/static_dependencies/sympy/external/__pycache__/gmpy.cpython-311.pyc new file mode 100644 index 0000000..beabd35 Binary files /dev/null and b/ccxt/static_dependencies/sympy/external/__pycache__/gmpy.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/sympy/external/__pycache__/importtools.cpython-311.pyc b/ccxt/static_dependencies/sympy/external/__pycache__/importtools.cpython-311.pyc new file mode 100644 index 0000000..ba950a5 Binary files /dev/null and b/ccxt/static_dependencies/sympy/external/__pycache__/importtools.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/sympy/external/__pycache__/ntheory.cpython-311.pyc b/ccxt/static_dependencies/sympy/external/__pycache__/ntheory.cpython-311.pyc new file mode 100644 index 0000000..5168637 Binary files /dev/null and b/ccxt/static_dependencies/sympy/external/__pycache__/ntheory.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/sympy/external/__pycache__/pythonmpq.cpython-311.pyc b/ccxt/static_dependencies/sympy/external/__pycache__/pythonmpq.cpython-311.pyc new file mode 100644 index 0000000..8aceeb8 Binary files /dev/null and b/ccxt/static_dependencies/sympy/external/__pycache__/pythonmpq.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/sympy/external/gmpy.py b/ccxt/static_dependencies/sympy/external/gmpy.py new file mode 100644 index 0000000..15d2a38 --- /dev/null +++ b/ccxt/static_dependencies/sympy/external/gmpy.py @@ -0,0 +1,345 @@ +import os +from ctypes import c_long, sizeof +from functools import reduce +from typing import Tuple as tTuple, Type +from warnings import warn + +from .importtools import import_module + +from .pythonmpq import PythonMPQ + +from .ntheory import ( + # bit_scan1 as python_bit_scan1, + # bit_scan0 as python_bit_scan0, + # remove as python_remove, + # factorial as python_factorial, + # sqrt as python_sqrt, + # sqrtrem as python_sqrtrem, + # gcd as python_gcd, + # lcm as python_lcm, + gcdext as python_gcdext, + # is_square as python_is_square, + # invert as python_invert, + # legendre as python_legendre, + # jacobi as python_jacobi, + # kronecker as python_kronecker, + # iroot as python_iroot, + # is_fermat_prp as python_is_fermat_prp, + # is_euler_prp as python_is_euler_prp, + # is_strong_prp as python_is_strong_prp, + # is_fibonacci_prp as python_is_fibonacci_prp, + # is_lucas_prp as python_is_lucas_prp, + # is_selfridge_prp as python_is_selfridge_prp, + # is_strong_lucas_prp as python_is_strong_lucas_prp, + # is_strong_selfridge_prp as python_is_strong_selfridge_prp, + # is_bpsw_prp as python_is_bpsw_prp, + # is_strong_bpsw_prp as python_is_strong_bpsw_prp, +) + + +__all__ = [ + # GROUND_TYPES is either 'gmpy' or 'python' depending on which is used. If + # gmpy is installed then it will be used unless the environment variable + # SYMPY_GROUND_TYPES is set to something other than 'auto', 'gmpy', or + # 'gmpy2'. + 'GROUND_TYPES', + + # If HAS_GMPY is 0, no supported version of gmpy is available. Otherwise, + # HAS_GMPY will be 2 for gmpy2 if GROUND_TYPES is 'gmpy'. It used to be + # possible for HAS_GMPY to be 1 for gmpy but gmpy is no longer supported. + 'HAS_GMPY', + + # SYMPY_INTS is a tuple containing the base types for valid integer types. + # This is either (int,) or (int, type(mpz(0))) depending on GROUND_TYPES. + 'SYMPY_INTS', + + # MPQ is either gmpy.mpq or the Python equivalent from + # sympy.external.pythonmpq + 'MPQ', + + # MPZ is either gmpy.mpz or int. + 'MPZ', + + # 'bit_scan1', + # 'bit_scan0', + # 'remove', + # 'factorial', + # 'sqrt', + # 'is_square', + # 'sqrtrem', + # 'gcd', + # 'lcm', + 'gcdext', + # 'invert', + # 'legendre', + # 'jacobi', + # 'kronecker', + # 'iroot', + # 'is_fermat_prp', + # 'is_euler_prp', + # 'is_strong_prp', + # 'is_fibonacci_prp', + # 'is_lucas_prp', + # 'is_selfridge_prp', + # 'is_strong_lucas_prp', + # 'is_strong_selfridge_prp', + # 'is_bpsw_prp', + # 'is_strong_bpsw_prp', +] + + +# +# Tested python-flint version. Future versions might work but we will only use +# them if explicitly requested by SYMPY_GROUND_TYPES=flint. +# +_PYTHON_FLINT_VERSION_NEEDED = "0.6.*" + + +def _flint_version_okay(flint_version): + flint_ver = flint_version.split('.')[:2] + needed_ver = _PYTHON_FLINT_VERSION_NEEDED.split('.')[:2] + return flint_ver == needed_ver + +# +# We will only use gmpy2 >= 2.0.0 +# +_GMPY2_MIN_VERSION = '2.0.0' + + +def _get_flint(sympy_ground_types): + if sympy_ground_types not in ('auto', 'flint'): + return None + + try: + import flint + # Earlier versions of python-flint may not have __version__. + from flint import __version__ as _flint_version + except ImportError: + if sympy_ground_types == 'flint': + warn("SYMPY_GROUND_TYPES was set to flint but python-flint is not " + "installed. Falling back to other ground types.") + return None + + if _flint_version_okay(_flint_version): + return flint + elif sympy_ground_types == 'auto': + warn(f"python-flint {_flint_version} is installed but only version " + f"{_PYTHON_FLINT_VERSION_NEEDED} will be used by default. " + f"Falling back to other ground types. Use " + f"SYMPY_GROUND_TYPES=flint to force the use of python-flint.") + return None + else: + warn(f"Using python-flint {_flint_version} because SYMPY_GROUND_TYPES " + f"is set to flint but this version of SymPy has only been tested " + f"with python-flint {_PYTHON_FLINT_VERSION_NEEDED}.") + return flint + + +def _get_gmpy2(sympy_ground_types): + if sympy_ground_types not in ('auto', 'gmpy', 'gmpy2'): + return None + + gmpy = import_module('gmpy2', min_module_version=_GMPY2_MIN_VERSION, + module_version_attr='version', module_version_attr_call_args=()) + + if sympy_ground_types != 'auto' and gmpy is None: + warn("gmpy2 library is not installed, switching to 'python' ground types") + + return gmpy + + +# +# SYMPY_GROUND_TYPES can be flint, gmpy, gmpy2, python or auto (default) +# +_SYMPY_GROUND_TYPES = os.environ.get('SYMPY_GROUND_TYPES', 'auto').lower() +_flint = None +_gmpy = None + +# +# First handle auto-detection of flint/gmpy2. We will prefer flint if available +# or otherwise gmpy2 if available and then lastly the python types. +# +if _SYMPY_GROUND_TYPES in ('auto', 'flint'): + _flint = _get_flint(_SYMPY_GROUND_TYPES) + if _flint is not None: + _SYMPY_GROUND_TYPES = 'flint' + else: + _SYMPY_GROUND_TYPES = 'auto' + +if _SYMPY_GROUND_TYPES in ('auto', 'gmpy', 'gmpy2'): + _gmpy = _get_gmpy2(_SYMPY_GROUND_TYPES) + if _gmpy is not None: + _SYMPY_GROUND_TYPES = 'gmpy' + else: + _SYMPY_GROUND_TYPES = 'python' + +if _SYMPY_GROUND_TYPES not in ('flint', 'gmpy', 'python'): + warn("SYMPY_GROUND_TYPES environment variable unrecognised. " + "Should be 'auto', 'flint', 'gmpy', 'gmpy2' or 'python'.") + _SYMPY_GROUND_TYPES = 'python' + +# +# At this point _SYMPY_GROUND_TYPES is either flint, gmpy or python. The blocks +# below define the values exported by this module in each case. +# + +# +# In gmpy2 and flint, there are functions that take a long (or unsigned long) +# argument. That is, it is not possible to input a value larger than that. +# +LONG_MAX = (1 << (8*sizeof(c_long) - 1)) - 1 + +# +# Type checkers are confused by what SYMPY_INTS is. There may be a better type +# hint for this like Type[Integral] or something. +# +SYMPY_INTS: tTuple[Type, ...] + +if _SYMPY_GROUND_TYPES == 'gmpy': + + assert _gmpy is not None + + flint = None + gmpy = _gmpy + + HAS_GMPY = 2 + GROUND_TYPES = 'gmpy' + SYMPY_INTS = (int, type(gmpy.mpz(0))) + MPZ = gmpy.mpz + MPQ = gmpy.mpq + + # bit_scan1 = gmpy.bit_scan1 + # bit_scan0 = gmpy.bit_scan0 + # remove = gmpy.remove + # factorial = gmpy.fac + # sqrt = gmpy.isqrt + # is_square = gmpy.is_square + # sqrtrem = gmpy.isqrt_rem + # gcd = gmpy.gcd + # lcm = gmpy.lcm + gcdext = gmpy.gcdext + # invert = gmpy.invert + # legendre = gmpy.legendre + # jacobi = gmpy.jacobi + # kronecker = gmpy.kronecker + + # def iroot(x, n): + # # In the latest gmpy2, the threshold for n is ULONG_MAX, + # # but adjust to the older one. + # if n <= LONG_MAX: + # return gmpy.iroot(x, n) + # return python_iroot(x, n) + + # is_fermat_prp = gmpy.is_fermat_prp + # is_euler_prp = gmpy.is_euler_prp + # is_strong_prp = gmpy.is_strong_prp + # is_fibonacci_prp = gmpy.is_fibonacci_prp + # is_lucas_prp = gmpy.is_lucas_prp + # is_selfridge_prp = gmpy.is_selfridge_prp + # is_strong_lucas_prp = gmpy.is_strong_lucas_prp + # is_strong_selfridge_prp = gmpy.is_strong_selfridge_prp + # is_bpsw_prp = gmpy.is_bpsw_prp + # is_strong_bpsw_prp = gmpy.is_strong_bpsw_prp + +elif _SYMPY_GROUND_TYPES == 'flint': + + assert _flint is not None + + flint = _flint + gmpy = None + + HAS_GMPY = 0 + GROUND_TYPES = 'flint' + SYMPY_INTS = (int, flint.fmpz) # type: ignore + MPZ = flint.fmpz # type: ignore + MPQ = flint.fmpq # type: ignore + + # bit_scan1 = python_bit_scan1 + # bit_scan0 = python_bit_scan0 + # remove = python_remove + # factorial = python_factorial + + # def sqrt(x): + # return flint.fmpz(x).isqrt() + + # def is_square(x): + # if x < 0: + # return False + # return flint.fmpz(x).sqrtrem()[1] == 0 + + # def sqrtrem(x): + # return flint.fmpz(x).sqrtrem() + + # def gcd(*args): + # return reduce(flint.fmpz.gcd, args, flint.fmpz(0)) + + # def lcm(*args): + # return reduce(flint.fmpz.lcm, args, flint.fmpz(1)) + + gcdext = python_gcdext + # invert = python_invert + # legendre = python_legendre + + # def jacobi(x, y): + # if y <= 0 or not y % 2: + # raise ValueError("y should be an odd positive integer") + # return flint.fmpz(x).jacobi(y) + + # kronecker = python_kronecker + + # def iroot(x, n): + # if n <= LONG_MAX: + # y = flint.fmpz(x).root(n) + # return y, y**n == x + # return python_iroot(x, n) + + # is_fermat_prp = python_is_fermat_prp + # is_euler_prp = python_is_euler_prp + # is_strong_prp = python_is_strong_prp + # is_fibonacci_prp = python_is_fibonacci_prp + # is_lucas_prp = python_is_lucas_prp + # is_selfridge_prp = python_is_selfridge_prp + # is_strong_lucas_prp = python_is_strong_lucas_prp + # is_strong_selfridge_prp = python_is_strong_selfridge_prp + # is_bpsw_prp = python_is_bpsw_prp + # is_strong_bpsw_prp = python_is_strong_bpsw_prp + +elif _SYMPY_GROUND_TYPES == 'python': + + flint = None + gmpy = None + + HAS_GMPY = 0 + GROUND_TYPES = 'python' + SYMPY_INTS = (int,) + MPZ = int + MPQ = PythonMPQ + + # bit_scan1 = python_bit_scan1 + # bit_scan0 = python_bit_scan0 + # remove = python_remove + # factorial = python_factorial + # sqrt = python_sqrt + # is_square = python_is_square + # sqrtrem = python_sqrtrem + # gcd = python_gcd + # lcm = python_lcm + gcdext = python_gcdext + # invert = python_invert + # legendre = python_legendre + # jacobi = python_jacobi + # kronecker = python_kronecker + # iroot = python_iroot + # is_fermat_prp = python_is_fermat_prp + # is_euler_prp = python_is_euler_prp + # is_strong_prp = python_is_strong_prp + # is_fibonacci_prp = python_is_fibonacci_prp + # is_lucas_prp = python_is_lucas_prp + # is_selfridge_prp = python_is_selfridge_prp + # is_strong_lucas_prp = python_is_strong_lucas_prp + # is_strong_selfridge_prp = python_is_strong_selfridge_prp + # is_bpsw_prp = python_is_bpsw_prp + # is_strong_bpsw_prp = python_is_strong_bpsw_prp + +else: + assert False diff --git a/ccxt/static_dependencies/sympy/external/importtools.py b/ccxt/static_dependencies/sympy/external/importtools.py new file mode 100644 index 0000000..5008b3d --- /dev/null +++ b/ccxt/static_dependencies/sympy/external/importtools.py @@ -0,0 +1,187 @@ +"""Tools to assist importing optional external modules.""" + +import sys +import re + +# Override these in the module to change the default warning behavior. +# For example, you might set both to False before running the tests so that +# warnings are not printed to the console, or set both to True for debugging. + +WARN_NOT_INSTALLED = None # Default is False +WARN_OLD_VERSION = None # Default is True + + +def __sympy_debug(): + # helper function from sympy/__init__.py + # We don't just import SYMPY_DEBUG from that file because we don't want to + # import all of SymPy just to use this module. + import os + debug_str = os.getenv('SYMPY_DEBUG', 'False') + if debug_str in ('True', 'False'): + return eval(debug_str) + else: + raise RuntimeError("unrecognized value for SYMPY_DEBUG: %s" % + debug_str) + +if __sympy_debug(): + WARN_OLD_VERSION = True + WARN_NOT_INSTALLED = True + + +_component_re = re.compile(r'(\d+ | [a-z]+ | \.)', re.VERBOSE) + +def version_tuple(vstring): + # Parse a version string to a tuple e.g. '1.2' -> (1, 2) + # Simplified from distutils.version.LooseVersion which was deprecated in + # Python 3.10. + components = [] + for x in _component_re.split(vstring): + if x and x != '.': + try: + x = int(x) + except ValueError: + pass + components.append(x) + return tuple(components) + + +def import_module(module, min_module_version=None, min_python_version=None, + warn_not_installed=None, warn_old_version=None, + module_version_attr='__version__', module_version_attr_call_args=None, + import_kwargs={}, catch=()): + """ + Import and return a module if it is installed. + + If the module is not installed, it returns None. + + A minimum version for the module can be given as the keyword argument + min_module_version. This should be comparable against the module version. + By default, module.__version__ is used to get the module version. To + override this, set the module_version_attr keyword argument. If the + attribute of the module to get the version should be called (e.g., + module.version()), then set module_version_attr_call_args to the args such + that module.module_version_attr(*module_version_attr_call_args) returns the + module's version. + + If the module version is less than min_module_version using the Python < + comparison, None will be returned, even if the module is installed. You can + use this to keep from importing an incompatible older version of a module. + + You can also specify a minimum Python version by using the + min_python_version keyword argument. This should be comparable against + sys.version_info. + + If the keyword argument warn_not_installed is set to True, the function will + emit a UserWarning when the module is not installed. + + If the keyword argument warn_old_version is set to True, the function will + emit a UserWarning when the library is installed, but cannot be imported + because of the min_module_version or min_python_version options. + + Note that because of the way warnings are handled, a warning will be + emitted for each module only once. You can change the default warning + behavior by overriding the values of WARN_NOT_INSTALLED and WARN_OLD_VERSION + in sympy.external.importtools. By default, WARN_NOT_INSTALLED is False and + WARN_OLD_VERSION is True. + + This function uses __import__() to import the module. To pass additional + options to __import__(), use the import_kwargs keyword argument. For + example, to import a submodule A.B, you must pass a nonempty fromlist option + to __import__. See the docstring of __import__(). + + This catches ImportError to determine if the module is not installed. To + catch additional errors, pass them as a tuple to the catch keyword + argument. + + Examples + ======== + + >>> from sympy.external import import_module + + >>> numpy = import_module('numpy') + + >>> numpy = import_module('numpy', min_python_version=(2, 7), + ... warn_old_version=False) + + >>> numpy = import_module('numpy', min_module_version='1.5', + ... warn_old_version=False) # numpy.__version__ is a string + + >>> # gmpy does not have __version__, but it does have gmpy.version() + + >>> gmpy = import_module('gmpy', min_module_version='1.14', + ... module_version_attr='version', module_version_attr_call_args=(), + ... warn_old_version=False) + + >>> # To import a submodule, you must pass a nonempty fromlist to + >>> # __import__(). The values do not matter. + >>> p3 = import_module('mpl_toolkits.mplot3d', + ... import_kwargs={'fromlist':['something']}) + + >>> # matplotlib.pyplot can raise RuntimeError when the display cannot be opened + >>> matplotlib = import_module('matplotlib', + ... import_kwargs={'fromlist':['pyplot']}, catch=(RuntimeError,)) + + """ + # keyword argument overrides default, and global variable overrides + # keyword argument. + warn_old_version = (WARN_OLD_VERSION if WARN_OLD_VERSION is not None + else warn_old_version or True) + warn_not_installed = (WARN_NOT_INSTALLED if WARN_NOT_INSTALLED is not None + else warn_not_installed or False) + + import warnings + + # Check Python first so we don't waste time importing a module we can't use + if min_python_version: + if sys.version_info < min_python_version: + if warn_old_version: + warnings.warn("Python version is too old to use %s " + "(%s or newer required)" % ( + module, '.'.join(map(str, min_python_version))), + UserWarning, stacklevel=2) + return + + try: + mod = __import__(module, **import_kwargs) + + ## there's something funny about imports with matplotlib and py3k. doing + ## from matplotlib import collections + ## gives python's stdlib collections module. explicitly re-importing + ## the module fixes this. + from_list = import_kwargs.get('fromlist', ()) + for submod in from_list: + if submod == 'collections' and mod.__name__ == 'matplotlib': + __import__(module + '.' + submod) + except ImportError: + if warn_not_installed: + warnings.warn("%s module is not installed" % module, UserWarning, + stacklevel=2) + return + except catch as e: + if warn_not_installed: + warnings.warn( + "%s module could not be used (%s)" % (module, repr(e)), + stacklevel=2) + return + + if min_module_version: + modversion = getattr(mod, module_version_attr) + if module_version_attr_call_args is not None: + modversion = modversion(*module_version_attr_call_args) + if version_tuple(modversion) < version_tuple(min_module_version): + if warn_old_version: + # Attempt to create a pretty string version of the version + if isinstance(min_module_version, str): + verstr = min_module_version + elif isinstance(min_module_version, (tuple, list)): + verstr = '.'.join(map(str, min_module_version)) + else: + # Either don't know what this is. Hopefully + # it's something that has a nice str version, like an int. + verstr = str(min_module_version) + warnings.warn("%s version is too old to use " + "(%s or newer required)" % (module, verstr), + UserWarning, stacklevel=2) + return + + return mod diff --git a/ccxt/static_dependencies/sympy/external/ntheory.py b/ccxt/static_dependencies/sympy/external/ntheory.py new file mode 100644 index 0000000..a5d5f5f --- /dev/null +++ b/ccxt/static_dependencies/sympy/external/ntheory.py @@ -0,0 +1,637 @@ +# sympy.external.ntheory +# +# This module provides pure Python implementations of some number theory +# functions that are alternately used from gmpy2 if it is installed. + +import sys +import math + +# import mpmath.libmp as mlib + + +_small_trailing = [0] * 256 +for j in range(1, 8): + _small_trailing[1 << j :: 1 << (j + 1)] = [j] * (1 << (7 - j)) + + +# def bit_scan1(x, n=0): +# if not x: +# return +# x = abs(x >> n) +# low_byte = x & 0xFF +# if low_byte: +# return _small_trailing[low_byte] + n + +# t = 8 + n +# x >>= 8 +# # 2**m is quick for z up through 2**30 +# z = x.bit_length() - 1 +# if x == 1 << z: +# return z + t + +# if z < 300: +# # fixed 8-byte reduction +# while not x & 0xFF: +# x >>= 8 +# t += 8 +# else: +# # binary reduction important when there might be a large +# # number of trailing 0s +# p = z >> 1 +# while not x & 0xFF: +# while x & ((1 << p) - 1): +# p >>= 1 +# x >>= p +# t += p +# return t + _small_trailing[x & 0xFF] + + +# def bit_scan0(x, n=0): +# return bit_scan1(x + (1 << n), n) + + +# def remove(x, f): +# if f < 2: +# raise ValueError("factor must be > 1") +# if x == 0: +# return 0, 0 +# if f == 2: +# b = bit_scan1(x) +# return x >> b, b +# m = 0 +# y, rem = divmod(x, f) +# while not rem: +# x = y +# m += 1 +# if m > 5: +# pow_list = [f**2] +# while pow_list: +# _f = pow_list[-1] +# y, rem = divmod(x, _f) +# if not rem: +# m += 1 << len(pow_list) +# x = y +# pow_list.append(_f**2) +# else: +# pow_list.pop() +# y, rem = divmod(x, f) +# return x, m + + +# def factorial(x): +# """Return x!.""" +# return int(mlib.ifac(int(x))) + + +# def sqrt(x): +# """Integer square root of x.""" +# return int(mlib.isqrt(int(x))) + + +# def sqrtrem(x): +# """Integer square root of x and remainder.""" +# s, r = mlib.sqrtrem(int(x)) +# return (int(s), int(r)) + + +# if sys.version_info[:2] >= (3, 9): +# # As of Python 3.9 these can take multiple arguments +# gcd = math.gcd +# lcm = math.lcm + +# else: +# # Until python 3.8 is no longer supported +# from functools import reduce + + +# def gcd(*args): +# """gcd of multiple integers.""" +# return reduce(math.gcd, args, 0) + + +# def lcm(*args): +# """lcm of multiple integers.""" +# if 0 in args: +# return 0 +# return reduce(lambda x, y: x*y//math.gcd(x, y), args, 1) + + +def _sign(n): + if n < 0: + return -1, -n + return 1, n + + +def gcdext(a, b): + if not a or not b: + g = abs(a) or abs(b) + if not g: + return (0, 0, 0) + return (g, a // g, b // g) + + x_sign, a = _sign(a) + y_sign, b = _sign(b) + x, r = 1, 0 + y, s = 0, 1 + + while b: + q, c = divmod(a, b) + a, b = b, c + x, r = r, x - q*r + y, s = s, y - q*s + + return (a, x * x_sign, y * y_sign) + + +# def is_square(x): +# """Return True if x is a square number.""" +# if x < 0: +# return False + +# # Note that the possible values of y**2 % n for a given n are limited. +# # For example, when n=4, y**2 % n can only take 0 or 1. +# # In other words, if x % 4 is 2 or 3, then x is not a square number. +# # Mathematically, it determines if it belongs to the set {y**2 % n}, +# # but implementationally, it can be realized as a logical conjunction +# # with an n-bit integer. +# # see https://mersenneforum.org/showpost.php?p=110896 +# # def magic(n): +# # s = {y**2 % n for y in range(n)} +# # s = set(range(n)) - s +# # return sum(1 << bit for bit in s) +# # >>> print(hex(magic(128))) +# # 0xfdfdfdedfdfdfdecfdfdfdedfdfcfdec +# # >>> print(hex(magic(99))) +# # 0x5f6f9ffb6fb7ddfcb75befdec +# # >>> print(hex(magic(91))) +# # 0x6fd1bfcfed5f3679d3ebdec +# # >>> print(hex(magic(85))) +# # 0xdef9ae771ffe3b9d67dec +# if 0xfdfdfdedfdfdfdecfdfdfdedfdfcfdec & (1 << (x & 127)): +# return False # e.g. 2, 3 +# m = x % 765765 # 765765 = 99 * 91 * 85 +# if 0x5f6f9ffb6fb7ddfcb75befdec & (1 << (m % 99)): +# return False # e.g. 17, 68 +# if 0x6fd1bfcfed5f3679d3ebdec & (1 << (m % 91)): +# return False # e.g. 97, 388 +# if 0xdef9ae771ffe3b9d67dec & (1 << (m % 85)): +# return False # e.g. 793, 1408 +# return mlib.sqrtrem(int(x))[1] == 0 + + +# def invert(x, m): +# """Modular inverse of x modulo m. + +# Returns y such that x*y == 1 mod m. + +# Uses ``math.pow`` but reproduces the behaviour of ``gmpy2.invert`` +# which raises ZeroDivisionError if no inverse exists. +# """ +# try: +# return pow(x, -1, m) +# except ValueError: +# raise ZeroDivisionError("invert() no inverse exists") + + +# def legendre(x, y): +# """Legendre symbol (x / y). + +# Following the implementation of gmpy2, +# the error is raised only when y is an even number. +# """ +# if y <= 0 or not y % 2: +# raise ValueError("y should be an odd prime") +# x %= y +# if not x: +# return 0 +# if pow(x, (y - 1) // 2, y) == 1: +# return 1 +# return -1 + + +# def jacobi(x, y): +# """Jacobi symbol (x / y).""" +# if y <= 0 or not y % 2: +# raise ValueError("y should be an odd positive integer") +# x %= y +# if not x: +# return int(y == 1) +# if y == 1 or x == 1: +# return 1 +# if gcd(x, y) != 1: +# return 0 +# j = 1 +# while x != 0: +# while x % 2 == 0 and x > 0: +# x >>= 1 +# if y % 8 in [3, 5]: +# j = -j +# x, y = y, x +# if x % 4 == y % 4 == 3: +# j = -j +# x %= y +# return j + + +# def kronecker(x, y): +# """Kronecker symbol (x / y).""" +# if gcd(x, y) != 1: +# return 0 +# if y == 0: +# return 1 +# sign = -1 if y < 0 and x < 0 else 1 +# y = abs(y) +# s = bit_scan1(y) +# y >>= s +# if s % 2 and x % 8 in [3, 5]: +# sign = -sign +# return sign * jacobi(x, y) + + +# def iroot(y, n): +# if y < 0: +# raise ValueError("y must be nonnegative") +# if n < 1: +# raise ValueError("n must be positive") +# if y in (0, 1): +# return y, True +# if n == 1: +# return y, True +# if n == 2: +# x, rem = mlib.sqrtrem(y) +# return int(x), not rem +# if n >= y.bit_length(): +# return 1, False +# # Get initial estimate for Newton's method. Care must be taken to +# # avoid overflow +# try: +# guess = int(y**(1./n) + 0.5) +# except OverflowError: +# exp = math.log2(y)/n +# if exp > 53: +# shift = int(exp - 53) +# guess = int(2.0**(exp - shift) + 1) << shift +# else: +# guess = int(2.0**exp) +# if guess > 2**50: +# # Newton iteration +# xprev, x = -1, guess +# while 1: +# t = x**(n - 1) +# xprev, x = x, ((n - 1)*x + y//t)//n +# if abs(x - xprev) < 2: +# break +# else: +# x = guess +# # Compensate +# t = x**n +# while t < y: +# x += 1 +# t = x**n +# while t > y: +# x -= 1 +# t = x**n +# return x, t == y + + +# def is_fermat_prp(n, a): +# if a < 2: +# raise ValueError("is_fermat_prp() requires 'a' greater than or equal to 2") +# if n < 1: +# raise ValueError("is_fermat_prp() requires 'n' be greater than 0") +# if n == 1: +# return False +# if n % 2 == 0: +# return n == 2 +# a %= n +# if gcd(n, a) != 1: +# raise ValueError("is_fermat_prp() requires gcd(n,a) == 1") +# return pow(a, n - 1, n) == 1 + + +# def is_euler_prp(n, a): +# if a < 2: +# raise ValueError("is_euler_prp() requires 'a' greater than or equal to 2") +# if n < 1: +# raise ValueError("is_euler_prp() requires 'n' be greater than 0") +# if n == 1: +# return False +# if n % 2 == 0: +# return n == 2 +# a %= n +# if gcd(n, a) != 1: +# raise ValueError("is_euler_prp() requires gcd(n,a) == 1") +# return pow(a, n >> 1, n) == jacobi(a, n) % n + + +# def _is_strong_prp(n, a): +# s = bit_scan1(n - 1) +# a = pow(a, n >> s, n) +# if a == 1 or a == n - 1: +# return True +# for _ in range(s - 1): +# a = pow(a, 2, n) +# if a == n - 1: +# return True +# if a == 1: +# return False +# return False + + +# def is_strong_prp(n, a): +# if a < 2: +# raise ValueError("is_strong_prp() requires 'a' greater than or equal to 2") +# if n < 1: +# raise ValueError("is_strong_prp() requires 'n' be greater than 0") +# if n == 1: +# return False +# if n % 2 == 0: +# return n == 2 +# a %= n +# if gcd(n, a) != 1: +# raise ValueError("is_strong_prp() requires gcd(n,a) == 1") +# return _is_strong_prp(n, a) + + +# def _lucas_sequence(n, P, Q, k): +# r"""Return the modular Lucas sequence (U_k, V_k, Q_k). + +# Explanation +# =========== + +# Given a Lucas sequence defined by P, Q, returns the kth values for +# U and V, along with Q^k, all modulo n. This is intended for use with +# possibly very large values of n and k, where the combinatorial functions +# would be completely unusable. + +# .. math :: +# U_k = \begin{cases} +# 0 & \text{if } k = 0\\ +# 1 & \text{if } k = 1\\ +# PU_{k-1} - QU_{k-2} & \text{if } k > 1 +# \end{cases}\\ +# V_k = \begin{cases} +# 2 & \text{if } k = 0\\ +# P & \text{if } k = 1\\ +# PV_{k-1} - QV_{k-2} & \text{if } k > 1 +# \end{cases} + +# The modular Lucas sequences are used in numerous places in number theory, +# especially in the Lucas compositeness tests and the various n + 1 proofs. + +# Parameters +# ========== + +# n : int +# n is an odd number greater than or equal to 3 +# P : int +# Q : int +# D determined by D = P**2 - 4*Q is non-zero +# k : int +# k is a nonnegative integer + +# Returns +# ======= + +# U, V, Qk : (int, int, int) +# `(U_k \bmod{n}, V_k \bmod{n}, Q^k \bmod{n})` + +# Examples +# ======== + +# >>> from sympy.external.ntheory import _lucas_sequence +# >>> N = 10**2000 + 4561 +# >>> sol = U, V, Qk = _lucas_sequence(N, 3, 1, N//2); sol +# (0, 2, 1) + +# References +# ========== + +# .. [1] https://en.wikipedia.org/wiki/Lucas_sequence + +# """ +# if k == 0: +# return (0, 2, 1) +# D = P**2 - 4*Q +# U = 1 +# V = P +# Qk = Q % n +# if Q == 1: +# # Optimization for extra strong tests. +# for b in bin(k)[3:]: +# U = (U*V) % n +# V = (V*V - 2) % n +# if b == "1": +# U, V = U*P + V, V*P + U*D +# if U & 1: +# U += n +# if V & 1: +# V += n +# U, V = U >> 1, V >> 1 +# elif P == 1 and Q == -1: +# # Small optimization for 50% of Selfridge parameters. +# for b in bin(k)[3:]: +# U = (U*V) % n +# if Qk == 1: +# V = (V*V - 2) % n +# else: +# V = (V*V + 2) % n +# Qk = 1 +# if b == "1": +# # new_U = (U + V) // 2 +# # new_V = (5*U + V) // 2 = 2*U + new_U +# U, V = U + V, U << 1 +# if U & 1: +# U += n +# U >>= 1 +# V += U +# Qk = -1 +# Qk %= n +# elif P == 1: +# for b in bin(k)[3:]: +# U = (U*V) % n +# V = (V*V - 2*Qk) % n +# Qk *= Qk +# if b == "1": +# # new_U = (U + V) // 2 +# # new_V = new_U - 2*Q*U +# U, V = U + V, (Q*U) << 1 +# if U & 1: +# U += n +# U >>= 1 +# V = U - V +# Qk *= Q +# Qk %= n +# else: +# # The general case with any P and Q. +# for b in bin(k)[3:]: +# U = (U*V) % n +# V = (V*V - 2*Qk) % n +# Qk *= Qk +# if b == "1": +# U, V = U*P + V, V*P + U*D +# if U & 1: +# U += n +# if V & 1: +# V += n +# U, V = U >> 1, V >> 1 +# Qk *= Q +# Qk %= n +# return (U % n, V % n, Qk) + + +# def is_fibonacci_prp(n, p, q): +# d = p**2 - 4*q +# if d == 0 or p <= 0 or q not in [1, -1]: +# raise ValueError("invalid values for p,q in is_fibonacci_prp()") +# if n < 1: +# raise ValueError("is_fibonacci_prp() requires 'n' be greater than 0") +# if n == 1: +# return False +# if n % 2 == 0: +# return n == 2 +# return _lucas_sequence(n, p, q, n)[1] == p % n + + +# def is_lucas_prp(n, p, q): +# d = p**2 - 4*q +# if d == 0: +# raise ValueError("invalid values for p,q in is_lucas_prp()") +# if n < 1: +# raise ValueError("is_lucas_prp() requires 'n' be greater than 0") +# if n == 1: +# return False +# if n % 2 == 0: +# return n == 2 +# if gcd(n, q*d) not in [1, n]: +# raise ValueError("is_lucas_prp() requires gcd(n,2*q*D) == 1") +# return _lucas_sequence(n, p, q, n - jacobi(d, n))[0] == 0 + + +# def _is_selfridge_prp(n): +# """Lucas compositeness test with the Selfridge parameters for n. + +# Explanation +# =========== + +# The Lucas compositeness test checks whether n is a prime number. +# The test can be run with arbitrary parameters ``P`` and ``Q``, which also change the performance of the test. +# So, which parameters are most effective for running the Lucas compositeness test? +# As an algorithm for determining ``P`` and ``Q``, Selfridge proposed method A [1]_ page 1401 +# (Since two methods were proposed, referred to simply as A and B in the paper, +# we will refer to one of them as "method A"). + +# method A fixes ``P = 1``. Then, ``D`` defined by ``D = P**2 - 4Q`` is varied from 5, -7, 9, -11, 13, and so on, +# with the first ``D`` being ``jacobi(D, n) == -1``. Once ``D`` is determined, +# ``Q`` is determined to be ``(P**2 - D)//4``. + +# References +# ========== + +# .. [1] Robert Baillie, Samuel S. Wagstaff, Lucas Pseudoprimes, +# Math. Comp. Vol 35, Number 152 (1980), pp. 1391-1417, +# https://doi.org/10.1090%2FS0025-5718-1980-0583518-6 +# http://mpqs.free.fr/LucasPseudoprimes.pdf + +# """ +# for D in range(5, 1_000_000, 2): +# if D & 2: # if D % 4 == 3 +# D = -D +# j = jacobi(D, n) +# if j == -1: +# return _lucas_sequence(n, 1, (1-D) // 4, n + 1)[0] == 0 +# if j == 0 and D % n: +# return False +# # When j == -1 is hard to find, suspect a square number +# if D == 13 and is_square(n): +# return False +# raise ValueError("appropriate value for D cannot be found in is_selfridge_prp()") + + +# def is_selfridge_prp(n): +# if n < 1: +# raise ValueError("is_selfridge_prp() requires 'n' be greater than 0") +# if n == 1: +# return False +# if n % 2 == 0: +# return n == 2 +# return _is_selfridge_prp(n) + + +# def is_strong_lucas_prp(n, p, q): +# D = p**2 - 4*q +# if D == 0: +# raise ValueError("invalid values for p,q in is_strong_lucas_prp()") +# if n < 1: +# raise ValueError("is_selfridge_prp() requires 'n' be greater than 0") +# if n == 1: +# return False +# if n % 2 == 0: +# return n == 2 +# if gcd(n, q*D) not in [1, n]: +# raise ValueError("is_strong_lucas_prp() requires gcd(n,2*q*D) == 1") +# j = jacobi(D, n) +# s = bit_scan1(n - j) +# U, V, Qk = _lucas_sequence(n, p, q, (n - j) >> s) +# if U == 0 or V == 0: +# return True +# for _ in range(s - 1): +# V = (V*V - 2*Qk) % n +# if V == 0: +# return True +# Qk = pow(Qk, 2, n) +# return False + + +# def _is_strong_selfridge_prp(n): +# for D in range(5, 1_000_000, 2): +# if D & 2: # if D % 4 == 3 +# D = -D +# j = jacobi(D, n) +# if j == -1: +# s = bit_scan1(n + 1) +# U, V, Qk = _lucas_sequence(n, 1, (1-D) // 4, (n + 1) >> s) +# if U == 0 or V == 0: +# return True +# for _ in range(s - 1): +# V = (V*V - 2*Qk) % n +# if V == 0: +# return True +# Qk = pow(Qk, 2, n) +# return False +# if j == 0 and D % n: +# return False +# # When j == -1 is hard to find, suspect a square number +# if D == 13 and is_square(n): +# return False +# raise ValueError("appropriate value for D cannot be found in is_strong_selfridge_prp()") + + +# def is_strong_selfridge_prp(n): +# if n < 1: +# raise ValueError("is_strong_selfridge_prp() requires 'n' be greater than 0") +# if n == 1: +# return False +# if n % 2 == 0: +# return n == 2 +# return _is_strong_selfridge_prp(n) + + +# def is_bpsw_prp(n): +# if n < 1: +# raise ValueError("is_bpsw_prp() requires 'n' be greater than 0") +# if n == 1: +# return False +# if n % 2 == 0: +# return n == 2 +# return _is_strong_prp(n, 2) and _is_selfridge_prp(n) + + +# def is_strong_bpsw_prp(n): +# if n < 1: +# raise ValueError("is_strong_bpsw_prp() requires 'n' be greater than 0") +# if n == 1: +# return False +# if n % 2 == 0: +# return n == 2 +# return _is_strong_prp(n, 2) and _is_strong_selfridge_prp(n) diff --git a/ccxt/static_dependencies/sympy/external/pythonmpq.py b/ccxt/static_dependencies/sympy/external/pythonmpq.py new file mode 100644 index 0000000..b8efd18 --- /dev/null +++ b/ccxt/static_dependencies/sympy/external/pythonmpq.py @@ -0,0 +1,341 @@ +""" +PythonMPQ: Rational number type based on Python integers. + +This class is intended as a pure Python fallback for when gmpy2 is not +installed. If gmpy2 is installed then its mpq type will be used instead. The +mpq type is around 20x faster. We could just use the stdlib Fraction class +here but that is slower: + + from fractions import Fraction + from sympy.external.pythonmpq import PythonMPQ + nums = range(1000) + dens = range(5, 1005) + rats = [Fraction(n, d) for n, d in zip(nums, dens)] + sum(rats) # <--- 24 milliseconds + rats = [PythonMPQ(n, d) for n, d in zip(nums, dens)] + sum(rats) # <--- 7 milliseconds + +Both mpq and Fraction have some awkward features like the behaviour of +division with // and %: + + >>> from fractions import Fraction + >>> Fraction(2, 3) % Fraction(1, 4) + 1/6 + +For the QQ domain we do not want this behaviour because there should be no +remainder when dividing rational numbers. SymPy does not make use of this +aspect of mpq when gmpy2 is installed. Since this class is a fallback for that +case we do not bother implementing e.g. __mod__ so that we can be sure we +are not using it when gmpy2 is installed either. +""" + + +import operator +from math import gcd +from decimal import Decimal +from fractions import Fraction +import sys +from typing import Tuple as tTuple, Type + + +# Used for __hash__ +_PyHASH_MODULUS = sys.hash_info.modulus +_PyHASH_INF = sys.hash_info.inf + + +class PythonMPQ: + """Rational number implementation that is intended to be compatible with + gmpy2's mpq. + + Also slightly faster than fractions.Fraction. + + PythonMPQ should be treated as immutable although no effort is made to + prevent mutation (since that might slow down calculations). + """ + __slots__ = ('numerator', 'denominator') + + def __new__(cls, numerator, denominator=None): + """Construct PythonMPQ with gcd computation and checks""" + if denominator is not None: + # + # PythonMPQ(n, d): require n and d to be int and d != 0 + # + if isinstance(numerator, int) and isinstance(denominator, int): + # This is the slow part: + divisor = gcd(numerator, denominator) + numerator //= divisor + denominator //= divisor + return cls._new_check(numerator, denominator) + else: + # + # PythonMPQ(q) + # + # Here q can be PythonMPQ, int, Decimal, float, Fraction or str + # + if isinstance(numerator, int): + return cls._new(numerator, 1) + elif isinstance(numerator, PythonMPQ): + return cls._new(numerator.numerator, numerator.denominator) + + # Let Fraction handle Decimal/float conversion and str parsing + if isinstance(numerator, (Decimal, float, str)): + numerator = Fraction(numerator) + if isinstance(numerator, Fraction): + return cls._new(numerator.numerator, numerator.denominator) + # + # Reject everything else. This is more strict than mpq which allows + # things like mpq(Fraction, Fraction) or mpq(Decimal, any). The mpq + # behaviour is somewhat inconsistent so we choose to accept only a + # more strict subset of what mpq allows. + # + raise TypeError("PythonMPQ() requires numeric or string argument") + + @classmethod + def _new_check(cls, numerator, denominator): + """Construct PythonMPQ, check divide by zero and canonicalize signs""" + if not denominator: + raise ZeroDivisionError(f'Zero divisor {numerator}/{denominator}') + elif denominator < 0: + numerator = -numerator + denominator = -denominator + return cls._new(numerator, denominator) + + @classmethod + def _new(cls, numerator, denominator): + """Construct PythonMPQ efficiently (no checks)""" + obj = super().__new__(cls) + obj.numerator = numerator + obj.denominator = denominator + return obj + + def __int__(self): + """Convert to int (truncates towards zero)""" + p, q = self.numerator, self.denominator + if p < 0: + return -(-p//q) + return p//q + + def __float__(self): + """Convert to float (approximately)""" + return self.numerator / self.denominator + + def __bool__(self): + """True/False if nonzero/zero""" + return bool(self.numerator) + + def __eq__(self, other): + """Compare equal with PythonMPQ, int, float, Decimal or Fraction""" + if isinstance(other, PythonMPQ): + return (self.numerator == other.numerator + and self.denominator == other.denominator) + elif isinstance(other, self._compatible_types): + return self.__eq__(PythonMPQ(other)) + else: + return NotImplemented + + def __hash__(self): + """hash - same as mpq/Fraction""" + try: + dinv = pow(self.denominator, -1, _PyHASH_MODULUS) + except ValueError: + hash_ = _PyHASH_INF + else: + hash_ = hash(hash(abs(self.numerator)) * dinv) + result = hash_ if self.numerator >= 0 else -hash_ + return -2 if result == -1 else result + + def __reduce__(self): + """Deconstruct for pickling""" + return type(self), (self.numerator, self.denominator) + + def __str__(self): + """Convert to string""" + if self.denominator != 1: + return f"{self.numerator}/{self.denominator}" + else: + return f"{self.numerator}" + + def __repr__(self): + """Convert to string""" + return f"MPQ({self.numerator},{self.denominator})" + + def _cmp(self, other, op): + """Helper for lt/le/gt/ge""" + if not isinstance(other, self._compatible_types): + return NotImplemented + lhs = self.numerator * other.denominator + rhs = other.numerator * self.denominator + return op(lhs, rhs) + + def __lt__(self, other): + """self < other""" + return self._cmp(other, operator.lt) + + def __le__(self, other): + """self <= other""" + return self._cmp(other, operator.le) + + def __gt__(self, other): + """self > other""" + return self._cmp(other, operator.gt) + + def __ge__(self, other): + """self >= other""" + return self._cmp(other, operator.ge) + + def __abs__(self): + """abs(q)""" + return self._new(abs(self.numerator), self.denominator) + + def __pos__(self): + """+q""" + return self + + def __neg__(self): + """-q""" + return self._new(-self.numerator, self.denominator) + + def __add__(self, other): + """q1 + q2""" + if isinstance(other, PythonMPQ): + # + # This is much faster than the naive method used in the stdlib + # fractions module. Not sure where this method comes from + # though... + # + # Compare timings for something like: + # nums = range(1000) + # rats = [PythonMPQ(n, d) for n, d in zip(nums[:-5], nums[5:])] + # sum(rats) # <-- time this + # + ap, aq = self.numerator, self.denominator + bp, bq = other.numerator, other.denominator + g = gcd(aq, bq) + if g == 1: + p = ap*bq + aq*bp + q = bq*aq + else: + q1, q2 = aq//g, bq//g + p, q = ap*q2 + bp*q1, q1*q2 + g2 = gcd(p, g) + p, q = (p // g2), q * (g // g2) + + elif isinstance(other, int): + p = self.numerator + self.denominator * other + q = self.denominator + else: + return NotImplemented + + return self._new(p, q) + + def __radd__(self, other): + """z1 + q2""" + if isinstance(other, int): + p = self.numerator + self.denominator * other + q = self.denominator + return self._new(p, q) + else: + return NotImplemented + + def __sub__(self ,other): + """q1 - q2""" + if isinstance(other, PythonMPQ): + ap, aq = self.numerator, self.denominator + bp, bq = other.numerator, other.denominator + g = gcd(aq, bq) + if g == 1: + p = ap*bq - aq*bp + q = bq*aq + else: + q1, q2 = aq//g, bq//g + p, q = ap*q2 - bp*q1, q1*q2 + g2 = gcd(p, g) + p, q = (p // g2), q * (g // g2) + elif isinstance(other, int): + p = self.numerator - self.denominator*other + q = self.denominator + else: + return NotImplemented + + return self._new(p, q) + + def __rsub__(self, other): + """z1 - q2""" + if isinstance(other, int): + p = self.denominator * other - self.numerator + q = self.denominator + return self._new(p, q) + else: + return NotImplemented + + def __mul__(self, other): + """q1 * q2""" + if isinstance(other, PythonMPQ): + ap, aq = self.numerator, self.denominator + bp, bq = other.numerator, other.denominator + x1 = gcd(ap, bq) + x2 = gcd(bp, aq) + p, q = ((ap//x1)*(bp//x2), (aq//x2)*(bq//x1)) + elif isinstance(other, int): + x = gcd(other, self.denominator) + p = self.numerator*(other//x) + q = self.denominator//x + else: + return NotImplemented + + return self._new(p, q) + + def __rmul__(self, other): + """z1 * q2""" + if isinstance(other, int): + x = gcd(self.denominator, other) + p = self.numerator*(other//x) + q = self.denominator//x + return self._new(p, q) + else: + return NotImplemented + + def __pow__(self, exp): + """q ** z""" + p, q = self.numerator, self.denominator + + if exp < 0: + p, q, exp = q, p, -exp + + return self._new_check(p**exp, q**exp) + + def __truediv__(self, other): + """q1 / q2""" + if isinstance(other, PythonMPQ): + ap, aq = self.numerator, self.denominator + bp, bq = other.numerator, other.denominator + x1 = gcd(ap, bp) + x2 = gcd(bq, aq) + p, q = ((ap//x1)*(bq//x2), (aq//x2)*(bp//x1)) + elif isinstance(other, int): + x = gcd(other, self.numerator) + p = self.numerator//x + q = self.denominator*(other//x) + else: + return NotImplemented + + return self._new_check(p, q) + + def __rtruediv__(self, other): + """z / q""" + if isinstance(other, int): + x = gcd(self.numerator, other) + p = self.denominator*(other//x) + q = self.numerator//x + return self._new_check(p, q) + else: + return NotImplemented + + _compatible_types: tTuple[Type, ...] = () + +# +# These are the types that PythonMPQ will interoperate with for operations +# and comparisons such as ==, + etc. We define this down here so that we can +# include PythonMPQ in the list as well. +# +PythonMPQ._compatible_types = (PythonMPQ, int, Decimal, Fraction) diff --git a/ccxt/static_dependencies/toolz/__init__.py b/ccxt/static_dependencies/toolz/__init__.py new file mode 100644 index 0000000..d4037af --- /dev/null +++ b/ccxt/static_dependencies/toolz/__init__.py @@ -0,0 +1,26 @@ +from .itertoolz import * + +from .functoolz import * + +from .dicttoolz import * + +from .recipes import * + +from functools import partial, reduce + +sorted = sorted + +map = map + +filter = filter + +# Aliases +comp = compose + +from . import curried + +# functoolz._sigs.create_signature_registry() + +from ._version import get_versions +__version__ = get_versions()['version'] +del get_versions diff --git a/ccxt/static_dependencies/toolz/__pycache__/__init__.cpython-311.pyc b/ccxt/static_dependencies/toolz/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..27302c9 Binary files /dev/null and b/ccxt/static_dependencies/toolz/__pycache__/__init__.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/toolz/__pycache__/_signatures.cpython-311.pyc b/ccxt/static_dependencies/toolz/__pycache__/_signatures.cpython-311.pyc new file mode 100644 index 0000000..6b93198 Binary files /dev/null and b/ccxt/static_dependencies/toolz/__pycache__/_signatures.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/toolz/__pycache__/_version.cpython-311.pyc b/ccxt/static_dependencies/toolz/__pycache__/_version.cpython-311.pyc new file mode 100644 index 0000000..15e71fc Binary files /dev/null and b/ccxt/static_dependencies/toolz/__pycache__/_version.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/toolz/__pycache__/dicttoolz.cpython-311.pyc b/ccxt/static_dependencies/toolz/__pycache__/dicttoolz.cpython-311.pyc new file mode 100644 index 0000000..660f119 Binary files /dev/null and b/ccxt/static_dependencies/toolz/__pycache__/dicttoolz.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/toolz/__pycache__/functoolz.cpython-311.pyc b/ccxt/static_dependencies/toolz/__pycache__/functoolz.cpython-311.pyc new file mode 100644 index 0000000..f12fed5 Binary files /dev/null and b/ccxt/static_dependencies/toolz/__pycache__/functoolz.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/toolz/__pycache__/itertoolz.cpython-311.pyc b/ccxt/static_dependencies/toolz/__pycache__/itertoolz.cpython-311.pyc new file mode 100644 index 0000000..d90486f Binary files /dev/null and b/ccxt/static_dependencies/toolz/__pycache__/itertoolz.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/toolz/__pycache__/recipes.cpython-311.pyc b/ccxt/static_dependencies/toolz/__pycache__/recipes.cpython-311.pyc new file mode 100644 index 0000000..d698ba6 Binary files /dev/null and b/ccxt/static_dependencies/toolz/__pycache__/recipes.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/toolz/__pycache__/utils.cpython-311.pyc b/ccxt/static_dependencies/toolz/__pycache__/utils.cpython-311.pyc new file mode 100644 index 0000000..f6ff426 Binary files /dev/null and b/ccxt/static_dependencies/toolz/__pycache__/utils.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/toolz/_signatures.py b/ccxt/static_dependencies/toolz/_signatures.py new file mode 100644 index 0000000..27229ef --- /dev/null +++ b/ccxt/static_dependencies/toolz/_signatures.py @@ -0,0 +1,784 @@ +"""Internal module for better introspection of builtins. + +The main functions are ``is_builtin_valid_args``, ``is_builtin_partial_args``, +and ``has_unknown_args``. Other functions in this module support these three. + +Notably, we create a ``signatures`` registry to enable introspection of +builtin functions in any Python version. This includes builtins that +have more than one valid signature. Currently, the registry includes +builtins from ``builtins``, ``functools``, ``itertools``, and ``operator`` +modules. More can be added as requested. We don't guarantee full coverage. + +Everything in this module should be regarded as implementation details. +Users should try to not use this module directly. +""" +import functools +import inspect +import itertools +import operator +from importlib import import_module + +from .functoolz import (is_partial_args, is_arity, has_varargs, + has_keywords, num_required_args) + +import builtins + +# We mock builtin callables using lists of tuples with lambda functions. +# +# The tuple spec is (num_position_args, lambda_func, keyword_only_args). +# +# num_position_args: +# - The number of positional-only arguments. If not specified, +# all positional arguments are considered positional-only. +# +# lambda_func: +# - lambda function that matches a signature of a builtin, but does +# not include keyword-only arguments. +# +# keyword_only_args: (optional) +# - Tuple of keyword-only arguments. + +module_info = {} + +module_info[builtins] = dict( + abs=[ + lambda x: None], + all=[ + lambda iterable: None], + anext=[ + lambda aiterator: None, + lambda aiterator, default: None], + any=[ + lambda iterable: None], + apply=[ + lambda object: None, + lambda object, args: None, + lambda object, args, kwargs: None], + ascii=[ + lambda obj: None], + bin=[ + lambda number: None], + bool=[ + lambda x=False: None], + buffer=[ + lambda object: None, + lambda object, offset: None, + lambda object, offset, size: None], + bytearray=[ + lambda: None, + lambda int: None, + lambda string, encoding='utf8', errors='strict': None], + callable=[ + lambda obj: None], + chr=[ + lambda i: None], + classmethod=[ + lambda function: None], + cmp=[ + lambda x, y: None], + coerce=[ + lambda x, y: None], + complex=[ + lambda real=0, imag=0: None], + delattr=[ + lambda obj, name: None], + dict=[ + lambda **kwargs: None, + lambda mapping, **kwargs: None], + dir=[ + lambda: None, + lambda object: None], + divmod=[ + lambda x, y: None], + enumerate=[ + (0, lambda iterable, start=0: None)], + eval=[ + lambda source: None, + lambda source, globals: None, + lambda source, globals, locals: None], + execfile=[ + lambda filename: None, + lambda filename, globals: None, + lambda filename, globals, locals: None], + file=[ + (0, lambda name, mode='r', buffering=-1: None)], + filter=[ + lambda function, iterable: None], + float=[ + lambda x=0.0: None], + format=[ + lambda value: None, + lambda value, format_spec: None], + frozenset=[ + lambda: None, + lambda iterable: None], + getattr=[ + lambda object, name: None, + lambda object, name, default: None], + globals=[ + lambda: None], + hasattr=[ + lambda obj, name: None], + hash=[ + lambda obj: None], + hex=[ + lambda number: None], + id=[ + lambda obj: None], + input=[ + lambda: None, + lambda prompt: None], + int=[ + lambda x=0: None, + (0, lambda x, base=10: None)], + intern=[ + lambda string: None], + isinstance=[ + lambda obj, class_or_tuple: None], + issubclass=[ + lambda cls, class_or_tuple: None], + iter=[ + lambda iterable: None, + lambda callable, sentinel: None], + len=[ + lambda obj: None], + list=[ + lambda: None, + lambda iterable: None], + locals=[ + lambda: None], + long=[ + lambda x=0: None, + (0, lambda x, base=10: None)], + map=[ + lambda func, sequence, *iterables: None], + memoryview=[ + (0, lambda object: None)], + next=[ + lambda iterator: None, + lambda iterator, default: None], + object=[ + lambda: None], + oct=[ + lambda number: None], + ord=[ + lambda c: None], + pow=[ + lambda x, y: None, + lambda x, y, z: None], + property=[ + lambda fget=None, fset=None, fdel=None, doc=None: None], + range=[ + lambda stop: None, + lambda start, stop: None, + lambda start, stop, step: None], + raw_input=[ + lambda: None, + lambda prompt: None], + reduce=[ + lambda function, sequence: None, + lambda function, sequence, initial: None], + reload=[ + lambda module: None], + repr=[ + lambda obj: None], + reversed=[ + lambda sequence: None], + round=[ + (0, lambda number, ndigits=0: None)], + set=[ + lambda: None, + lambda iterable: None], + setattr=[ + lambda obj, name, value: None], + slice=[ + lambda stop: None, + lambda start, stop: None, + lambda start, stop, step: None], + staticmethod=[ + lambda function: None], + sum=[ + lambda iterable: None, + lambda iterable, start: None], + super=[ + lambda type: None, + lambda type, obj: None], + tuple=[ + lambda: None, + lambda iterable: None], + type=[ + lambda object: None, + lambda name, bases, dict: None], + unichr=[ + lambda i: None], + unicode=[ + lambda object: None, + lambda string='', encoding='utf8', errors='strict': None], + vars=[ + lambda: None, + lambda object: None], + xrange=[ + lambda stop: None, + lambda start, stop: None, + lambda start, stop, step: None], + zip=[ + lambda *iterables: None], + __build_class__=[ + (2, lambda func, name, *bases, **kwds: None, ('metaclass',))], + __import__=[ + (0, lambda name, globals=None, locals=None, fromlist=None, + level=None: None)], +) +module_info[builtins]['exec'] = [ + lambda source: None, + lambda source, globals: None, + lambda source, globals, locals: None] + +module_info[builtins].update( + breakpoint=[ + lambda *args, **kws: None], + bytes=[ + lambda: None, + lambda int: None, + lambda string, encoding='utf8', errors='strict': None], + compile=[ + (0, lambda source, filename, mode, flags=0, + dont_inherit=False, optimize=-1: None)], + max=[ + (1, lambda iterable: None, ('default', 'key',)), + (1, lambda arg1, arg2, *args: None, ('key',))], + min=[ + (1, lambda iterable: None, ('default', 'key',)), + (1, lambda arg1, arg2, *args: None, ('key',))], + open=[ + (0, lambda file, mode='r', buffering=-1, encoding=None, + errors=None, newline=None, closefd=True, opener=None: None)], + sorted=[ + (1, lambda iterable: None, ('key', 'reverse'))], + str=[ + lambda object='', encoding='utf', errors='strict': None], +) +module_info[builtins]['print'] = [ + (0, lambda *args: None, ('sep', 'end', 'file', 'flush',))] + + +module_info[functools] = dict( + cmp_to_key=[ + (0, lambda mycmp: None)], + partial=[ + lambda func, *args, **kwargs: None], + partialmethod=[ + lambda func, *args, **kwargs: None], + reduce=[ + lambda function, sequence: None, + lambda function, sequence, initial: None], +) + +module_info[itertools] = dict( + accumulate=[ + (0, lambda iterable, func=None: None)], + chain=[ + lambda *iterables: None], + combinations=[ + (0, lambda iterable, r: None)], + combinations_with_replacement=[ + (0, lambda iterable, r: None)], + compress=[ + (0, lambda data, selectors: None)], + count=[ + lambda start=0, step=1: None], + cycle=[ + lambda iterable: None], + dropwhile=[ + lambda predicate, iterable: None], + filterfalse=[ + lambda function, sequence: None], + groupby=[ + (0, lambda iterable, key=None: None)], + ifilter=[ + lambda function, sequence: None], + ifilterfalse=[ + lambda function, sequence: None], + imap=[ + lambda func, sequence, *iterables: None], + islice=[ + lambda iterable, stop: None, + lambda iterable, start, stop: None, + lambda iterable, start, stop, step: None], + izip=[ + lambda *iterables: None], + izip_longest=[ + (0, lambda *iterables: None, ('fillvalue',))], + pairwise=[ + lambda iterable: None], + permutations=[ + (0, lambda iterable, r=0: None)], + product=[ + (0, lambda *iterables: None, ('repeat',))], + repeat=[ + (0, lambda object, times=0: None)], + starmap=[ + lambda function, sequence: None], + takewhile=[ + lambda predicate, iterable: None], + tee=[ + lambda iterable: None, + lambda iterable, n: None], + zip_longest=[ + (0, lambda *iterables: None, ('fillvalue',))], +) + + +module_info[operator] = dict( + __abs__=[ + lambda a: None], + __add__=[ + lambda a, b: None], + __and__=[ + lambda a, b: None], + __concat__=[ + lambda a, b: None], + __contains__=[ + lambda a, b: None], + __delitem__=[ + lambda a, b: None], + __delslice__=[ + lambda a, b, c: None], + __div__=[ + lambda a, b: None], + __eq__=[ + lambda a, b: None], + __floordiv__=[ + lambda a, b: None], + __ge__=[ + lambda a, b: None], + __getitem__=[ + lambda a, b: None], + __getslice__=[ + lambda a, b, c: None], + __gt__=[ + lambda a, b: None], + __iadd__=[ + lambda a, b: None], + __iand__=[ + lambda a, b: None], + __iconcat__=[ + lambda a, b: None], + __idiv__=[ + lambda a, b: None], + __ifloordiv__=[ + lambda a, b: None], + __ilshift__=[ + lambda a, b: None], + __imatmul__=[ + lambda a, b: None], + __imod__=[ + lambda a, b: None], + __imul__=[ + lambda a, b: None], + __index__=[ + lambda a: None], + __inv__=[ + lambda a: None], + __invert__=[ + lambda a: None], + __ior__=[ + lambda a, b: None], + __ipow__=[ + lambda a, b: None], + __irepeat__=[ + lambda a, b: None], + __irshift__=[ + lambda a, b: None], + __isub__=[ + lambda a, b: None], + __itruediv__=[ + lambda a, b: None], + __ixor__=[ + lambda a, b: None], + __le__=[ + lambda a, b: None], + __lshift__=[ + lambda a, b: None], + __lt__=[ + lambda a, b: None], + __matmul__=[ + lambda a, b: None], + __mod__=[ + lambda a, b: None], + __mul__=[ + lambda a, b: None], + __ne__=[ + lambda a, b: None], + __neg__=[ + lambda a: None], + __not__=[ + lambda a: None], + __or__=[ + lambda a, b: None], + __pos__=[ + lambda a: None], + __pow__=[ + lambda a, b: None], + __repeat__=[ + lambda a, b: None], + __rshift__=[ + lambda a, b: None], + __setitem__=[ + lambda a, b, c: None], + __setslice__=[ + lambda a, b, c, d: None], + __sub__=[ + lambda a, b: None], + __truediv__=[ + lambda a, b: None], + __xor__=[ + lambda a, b: None], + _abs=[ + lambda x: None], + _compare_digest=[ + lambda a, b: None], + abs=[ + lambda a: None], + add=[ + lambda a, b: None], + and_=[ + lambda a, b: None], + attrgetter=[ + lambda attr, *args: None], + concat=[ + lambda a, b: None], + contains=[ + lambda a, b: None], + countOf=[ + lambda a, b: None], + delitem=[ + lambda a, b: None], + delslice=[ + lambda a, b, c: None], + div=[ + lambda a, b: None], + eq=[ + lambda a, b: None], + floordiv=[ + lambda a, b: None], + ge=[ + lambda a, b: None], + getitem=[ + lambda a, b: None], + getslice=[ + lambda a, b, c: None], + gt=[ + lambda a, b: None], + iadd=[ + lambda a, b: None], + iand=[ + lambda a, b: None], + iconcat=[ + lambda a, b: None], + idiv=[ + lambda a, b: None], + ifloordiv=[ + lambda a, b: None], + ilshift=[ + lambda a, b: None], + imatmul=[ + lambda a, b: None], + imod=[ + lambda a, b: None], + imul=[ + lambda a, b: None], + index=[ + lambda a: None], + indexOf=[ + lambda a, b: None], + inv=[ + lambda a: None], + invert=[ + lambda a: None], + ior=[ + lambda a, b: None], + ipow=[ + lambda a, b: None], + irepeat=[ + lambda a, b: None], + irshift=[ + lambda a, b: None], + is_=[ + lambda a, b: None], + is_not=[ + lambda a, b: None], + isCallable=[ + lambda a: None], + isMappingType=[ + lambda a: None], + isNumberType=[ + lambda a: None], + isSequenceType=[ + lambda a: None], + isub=[ + lambda a, b: None], + itemgetter=[ + lambda item, *args: None], + itruediv=[ + lambda a, b: None], + ixor=[ + lambda a, b: None], + le=[ + lambda a, b: None], + length_hint=[ + lambda obj: None, + lambda obj, default: None], + lshift=[ + lambda a, b: None], + lt=[ + lambda a, b: None], + matmul=[ + lambda a, b: None], + methodcaller=[ + lambda name, *args, **kwargs: None], + mod=[ + lambda a, b: None], + mul=[ + lambda a, b: None], + ne=[ + lambda a, b: None], + neg=[ + lambda a: None], + not_=[ + lambda a: None], + or_=[ + lambda a, b: None], + pos=[ + lambda a: None], + pow=[ + lambda a, b: None], + repeat=[ + lambda a, b: None], + rshift=[ + lambda a, b: None], + sequenceIncludes=[ + lambda a, b: None], + setitem=[ + lambda a, b, c: None], + setslice=[ + lambda a, b, c, d: None], + sub=[ + lambda a, b: None], + truediv=[ + lambda a, b: None], + truth=[ + lambda a: None], + xor=[ + lambda a, b: None], +) + +module_info['toolz'] = dict( + curry=[ + (0, lambda *args, **kwargs: None)], + excepts=[ + (0, lambda exc, func, handler=None: None)], + flip=[ + (0, lambda func=None, a=None, b=None: None)], + juxt=[ + (0, lambda *funcs: None)], + memoize=[ + (0, lambda func=None, cache=None, key=None: None)], +) + +module_info['toolz.functoolz'] = dict( + Compose=[ + (0, lambda funcs: None)], + InstanceProperty=[ + (0, lambda fget=None, fset=None, fdel=None, doc=None, + classval=None: None)], +) + + +def num_pos_args(sigspec): + """ Return the number of positional arguments. ``f(x, y=1)`` has 1""" + return sum(1 for x in sigspec.parameters.values() + if x.kind == x.POSITIONAL_OR_KEYWORD + and x.default is x.empty) + + +def get_exclude_keywords(num_pos_only, sigspec): + """ Return the names of position-only arguments if func has **kwargs""" + if num_pos_only == 0: + return () + has_kwargs = any(x.kind == x.VAR_KEYWORD + for x in sigspec.parameters.values()) + if not has_kwargs: + return () + pos_args = list(sigspec.parameters.values())[:num_pos_only] + return tuple(x.name for x in pos_args) + + +def signature_or_spec(func): + try: + return inspect.signature(func) + except (ValueError, TypeError): + return None + + +def expand_sig(sig): + """ Convert the signature spec in ``module_info`` to add to ``signatures`` + + The input signature spec is one of: + - ``lambda_func`` + - ``(num_position_args, lambda_func)`` + - ``(num_position_args, lambda_func, keyword_only_args)`` + + The output signature spec is: + ``(num_position_args, lambda_func, keyword_exclude, sigspec)`` + + where ``keyword_exclude`` includes keyword only arguments and, if variadic + keywords is present, the names of position-only argument. The latter is + included to support builtins such as ``partial(func, *args, **kwargs)``, + which allows ``func=`` to be used as a keyword even though it's the name + of a positional argument. + """ + if isinstance(sig, tuple): + if len(sig) == 3: + num_pos_only, func, keyword_only = sig + assert isinstance(sig[-1], tuple) + else: + num_pos_only, func = sig + keyword_only = () + sigspec = signature_or_spec(func) + else: + func = sig + sigspec = signature_or_spec(func) + num_pos_only = num_pos_args(sigspec) + keyword_only = () + keyword_exclude = get_exclude_keywords(num_pos_only, sigspec) + return num_pos_only, func, keyword_only + keyword_exclude, sigspec + + +signatures = {} + + +def create_signature_registry(module_info=module_info, signatures=signatures): + for module, info in module_info.items(): + if isinstance(module, str): + module = import_module(module) + for name, sigs in info.items(): + if hasattr(module, name): + new_sigs = tuple(expand_sig(sig) for sig in sigs) + signatures[getattr(module, name)] = new_sigs + + +def check_valid(sig, args, kwargs): + """ Like ``is_valid_args`` for the given signature spec""" + num_pos_only, func, keyword_exclude, sigspec = sig + if len(args) < num_pos_only: + return False + if keyword_exclude: + kwargs = dict(kwargs) + for item in keyword_exclude: + kwargs.pop(item, None) + try: + func(*args, **kwargs) + return True + except TypeError: + return False + + +def _is_valid_args(func, args, kwargs): + """ Like ``is_valid_args`` for builtins in our ``signatures`` registry""" + if func not in signatures: + return None + sigs = signatures[func] + return any(check_valid(sig, args, kwargs) for sig in sigs) + + +def check_partial(sig, args, kwargs): + """ Like ``is_partial_args`` for the given signature spec""" + num_pos_only, func, keyword_exclude, sigspec = sig + if len(args) < num_pos_only: + pad = (None,) * (num_pos_only - len(args)) + args = args + pad + if keyword_exclude: + kwargs = dict(kwargs) + for item in keyword_exclude: + kwargs.pop(item, None) + return is_partial_args(func, args, kwargs, sigspec) + + +def _is_partial_args(func, args, kwargs): + """ Like ``is_partial_args`` for builtins in our ``signatures`` registry""" + if func not in signatures: + return None + sigs = signatures[func] + return any(check_partial(sig, args, kwargs) for sig in sigs) + + +def check_arity(n, sig): + num_pos_only, func, keyword_exclude, sigspec = sig + if keyword_exclude or num_pos_only > n: + return False + return is_arity(n, func, sigspec) + + +def _is_arity(n, func): + if func not in signatures: + return None + sigs = signatures[func] + checks = [check_arity(n, sig) for sig in sigs] + if all(checks): + return True + elif any(checks): + return None + return False + + +def check_varargs(sig): + num_pos_only, func, keyword_exclude, sigspec = sig + return has_varargs(func, sigspec) + + +def _has_varargs(func): + if func not in signatures: + return None + sigs = signatures[func] + checks = [check_varargs(sig) for sig in sigs] + if all(checks): + return True + elif any(checks): + return None + return False + + +def check_keywords(sig): + num_pos_only, func, keyword_exclude, sigspec = sig + if keyword_exclude: + return True + return has_keywords(func, sigspec) + + +def _has_keywords(func): + if func not in signatures: + return None + sigs = signatures[func] + checks = [check_keywords(sig) for sig in sigs] + if all(checks): + return True + elif any(checks): + return None + return False + + +def check_required_args(sig): + num_pos_only, func, keyword_exclude, sigspec = sig + return num_required_args(func, sigspec) + + +def _num_required_args(func): + if func not in signatures: + return None + sigs = signatures[func] + vals = [check_required_args(sig) for sig in sigs] + val = vals[0] + if all(x == val for x in vals): + return val + return None diff --git a/ccxt/static_dependencies/toolz/_version.py b/ccxt/static_dependencies/toolz/_version.py new file mode 100644 index 0000000..6e0bd8f --- /dev/null +++ b/ccxt/static_dependencies/toolz/_version.py @@ -0,0 +1,520 @@ + +# This file helps to compute a version number in source trees obtained from +# git-archive tarball (such as those provided by githubs download-from-tag +# feature). Distribution tarballs (built by setup.py sdist) and build +# directories (produced by setup.py build) will contain a much shorter file +# that just contains the computed version number. + +# This file is released into the public domain. Generated by +# versioneer-0.18 (https://github.com/warner/python-versioneer) + +"""Git implementation of _version.py.""" + +import errno +import os +import re +import subprocess +import sys + + +def get_keywords(): + """Get the keywords needed to look up the version information.""" + # these strings will be replaced by git during git-archive. + # setup.py/versioneer.py will grep for the variable names, so they must + # each be defined on a line of their own. _version.py will just call + # get_keywords(). + git_refnames = "$Format:%d$" + git_full = "$Format:%H$" + git_date = "$Format:%ci$" + keywords = {"refnames": git_refnames, "full": git_full, "date": git_date} + return keywords + + +class VersioneerConfig: + """Container for Versioneer configuration parameters.""" + + +def get_config(): + """Create, populate and return the VersioneerConfig() object.""" + # these strings are filled in when 'setup.py versioneer' creates + # _version.py + cfg = VersioneerConfig() + cfg.VCS = "git" + cfg.style = "pep440" + cfg.tag_prefix = "" + cfg.parentdir_prefix = "toolz-" + cfg.versionfile_source = "toolz/_version.py" + cfg.verbose = False + return cfg + + +class NotThisMethod(Exception): + """Exception raised if a method is not valid for the current scenario.""" + + +LONG_VERSION_PY = {} +HANDLERS = {} + + +def register_vcs_handler(vcs, method): # decorator + """Decorator to mark a method as the handler for a particular VCS.""" + def decorate(f): + """Store f in HANDLERS[vcs][method].""" + if vcs not in HANDLERS: + HANDLERS[vcs] = {} + HANDLERS[vcs][method] = f + return f + return decorate + + +def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, + env=None): + """Call the given command(s).""" + assert isinstance(commands, list) + p = None + for c in commands: + try: + dispcmd = str([c] + args) + # remember shell=False, so use git.cmd on windows, not just git + p = subprocess.Popen([c] + args, cwd=cwd, env=env, + stdout=subprocess.PIPE, + stderr=(subprocess.PIPE if hide_stderr + else None)) + break + except EnvironmentError: + e = sys.exc_info()[1] + if e.errno == errno.ENOENT: + continue + if verbose: + print("unable to run %s" % dispcmd) + print(e) + return None, None + else: + if verbose: + print("unable to find command, tried %s" % (commands,)) + return None, None + stdout = p.communicate()[0].strip() + if sys.version_info[0] >= 3: + stdout = stdout.decode() + if p.returncode != 0: + if verbose: + print("unable to run %s (error)" % dispcmd) + print("stdout was %s" % stdout) + return None, p.returncode + return stdout, p.returncode + + +def versions_from_parentdir(parentdir_prefix, root, verbose): + """Try to determine the version from the parent directory name. + + Source tarballs conventionally unpack into a directory that includes both + the project name and a version string. We will also support searching up + two directory levels for an appropriately named parent directory + """ + rootdirs = [] + + for i in range(3): + dirname = os.path.basename(root) + if dirname.startswith(parentdir_prefix): + return {"version": dirname[len(parentdir_prefix):], + "full-revisionid": None, + "dirty": False, "error": None, "date": None} + else: + rootdirs.append(root) + root = os.path.dirname(root) # up a level + + if verbose: + print("Tried directories %s but none started with prefix %s" % + (str(rootdirs), parentdir_prefix)) + raise NotThisMethod("rootdir doesn't start with parentdir_prefix") + + +@register_vcs_handler("git", "get_keywords") +def git_get_keywords(versionfile_abs): + """Extract version information from the given file.""" + # the code embedded in _version.py can just fetch the value of these + # keywords. When used from setup.py, we don't want to import _version.py, + # so we do it with a regexp instead. This function is not used from + # _version.py. + keywords = {} + try: + f = open(versionfile_abs, "r") + for line in f.readlines(): + if line.strip().startswith("git_refnames ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["refnames"] = mo.group(1) + if line.strip().startswith("git_full ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["full"] = mo.group(1) + if line.strip().startswith("git_date ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["date"] = mo.group(1) + f.close() + except EnvironmentError: + pass + return keywords + + +@register_vcs_handler("git", "keywords") +def git_versions_from_keywords(keywords, tag_prefix, verbose): + """Get version information from git keywords.""" + if not keywords: + raise NotThisMethod("no keywords at all, weird") + date = keywords.get("date") + if date is not None: + # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant + # datestamp. However we prefer "%ci" (which expands to an "ISO-8601 + # -like" string, which we must then edit to make compliant), because + # it's been around since git-1.5.3, and it's too difficult to + # discover which version we're using, or to work around using an + # older one. + date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) + refnames = keywords["refnames"].strip() + if refnames.startswith("$Format"): + if verbose: + print("keywords are unexpanded, not using") + raise NotThisMethod("unexpanded keywords, not a git-archive tarball") + refs = set([r.strip() for r in refnames.strip("()").split(",")]) + # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of + # just "foo-1.0". If we see a "tag: " prefix, prefer those. + TAG = "tag: " + tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) + if not tags: + # Either we're using git < 1.8.3, or there really are no tags. We use + # a heuristic: assume all version tags have a digit. The old git %d + # expansion behaves like git log --decorate=short and strips out the + # refs/heads/ and refs/tags/ prefixes that would let us distinguish + # between branches and tags. By ignoring refnames without digits, we + # filter out many common branch names like "release" and + # "stabilization", as well as "HEAD" and "master". + tags = set([r for r in refs if re.search(r'\d', r)]) + if verbose: + print("discarding '%s', no digits" % ",".join(refs - tags)) + if verbose: + print("likely tags: %s" % ",".join(sorted(tags))) + for ref in sorted(tags): + # sorting will prefer e.g. "2.0" over "2.0rc1" + if ref.startswith(tag_prefix): + r = ref[len(tag_prefix):] + if verbose: + print("picking %s" % r) + return {"version": r, + "full-revisionid": keywords["full"].strip(), + "dirty": False, "error": None, + "date": date} + # no suitable tags, so version is "0+unknown", but full hex is still there + if verbose: + print("no suitable tags, using unknown + full revision id") + return {"version": "0+unknown", + "full-revisionid": keywords["full"].strip(), + "dirty": False, "error": "no suitable tags", "date": None} + + +@register_vcs_handler("git", "pieces_from_vcs") +def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): + """Get version from 'git describe' in the root of the source tree. + + This only gets called if the git-archive 'subst' keywords were *not* + expanded, and _version.py hasn't already been rewritten with a short + version string, meaning we're inside a checked out source tree. + """ + GITS = ["git"] + if sys.platform == "win32": + GITS = ["git.cmd", "git.exe"] + + out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, + hide_stderr=True) + if rc != 0: + if verbose: + print("Directory %s not under git control" % root) + raise NotThisMethod("'git rev-parse --git-dir' returned error") + + # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] + # if there isn't one, this yields HEX[-dirty] (no NUM) + describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty", + "--always", "--long", + "--match", "%s*" % tag_prefix], + cwd=root) + # --long was added in git-1.5.5 + if describe_out is None: + raise NotThisMethod("'git describe' failed") + describe_out = describe_out.strip() + full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) + if full_out is None: + raise NotThisMethod("'git rev-parse' failed") + full_out = full_out.strip() + + pieces = {} + pieces["long"] = full_out + pieces["short"] = full_out[:7] # maybe improved later + pieces["error"] = None + + # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] + # TAG might have hyphens. + git_describe = describe_out + + # look for -dirty suffix + dirty = git_describe.endswith("-dirty") + pieces["dirty"] = dirty + if dirty: + git_describe = git_describe[:git_describe.rindex("-dirty")] + + # now we have TAG-NUM-gHEX or HEX + + if "-" in git_describe: + # TAG-NUM-gHEX + mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) + if not mo: + # unparsable. Maybe git-describe is misbehaving? + pieces["error"] = ("unable to parse git-describe output: '%s'" + % describe_out) + return pieces + + # tag + full_tag = mo.group(1) + if not full_tag.startswith(tag_prefix): + if verbose: + fmt = "tag '%s' doesn't start with prefix '%s'" + print(fmt % (full_tag, tag_prefix)) + pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" + % (full_tag, tag_prefix)) + return pieces + pieces["closest-tag"] = full_tag[len(tag_prefix):] + + # distance: number of commits since tag + pieces["distance"] = int(mo.group(2)) + + # commit: short hex revision ID + pieces["short"] = mo.group(3) + + else: + # HEX: no tags + pieces["closest-tag"] = None + count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], + cwd=root) + pieces["distance"] = int(count_out) # total number of commits + + # commit date: see ISO-8601 comment in git_versions_from_keywords() + date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"], + cwd=root)[0].strip() + pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) + + return pieces + + +def plus_or_dot(pieces): + """Return a + if we don't already have one, else return a .""" + if "+" in pieces.get("closest-tag", ""): + return "." + return "+" + + +def render_pep440(pieces): + """Build up version string, with post-release "local version identifier". + + Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you + get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty + + Exceptions: + 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += plus_or_dot(pieces) + rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + else: + # exception #1 + rendered = "0+untagged.%d.g%s" % (pieces["distance"], + pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + return rendered + + +def render_pep440_pre(pieces): + """TAG[.post.devDISTANCE] -- No -dirty. + + Exceptions: + 1: no tags. 0.post.devDISTANCE + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"]: + rendered += ".post.dev%d" % pieces["distance"] + else: + # exception #1 + rendered = "0.post.dev%d" % pieces["distance"] + return rendered + + +def render_pep440_post(pieces): + """TAG[.postDISTANCE[.dev0]+gHEX] . + + The ".dev0" means dirty. Note that .dev0 sorts backwards + (a dirty tree will appear "older" than the corresponding clean one), + but you shouldn't be releasing software with -dirty anyways. + + Exceptions: + 1: no tags. 0.postDISTANCE[.dev0] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += ".post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + rendered += plus_or_dot(pieces) + rendered += "g%s" % pieces["short"] + else: + # exception #1 + rendered = "0.post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + rendered += "+g%s" % pieces["short"] + return rendered + + +def render_pep440_old(pieces): + """TAG[.postDISTANCE[.dev0]] . + + The ".dev0" means dirty. + + Eexceptions: + 1: no tags. 0.postDISTANCE[.dev0] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += ".post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + else: + # exception #1 + rendered = "0.post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + return rendered + + +def render_git_describe(pieces): + """TAG[-DISTANCE-gHEX][-dirty]. + + Like 'git describe --tags --dirty --always'. + + Exceptions: + 1: no tags. HEX[-dirty] (note: no 'g' prefix) + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"]: + rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) + else: + # exception #1 + rendered = pieces["short"] + if pieces["dirty"]: + rendered += "-dirty" + return rendered + + +def render_git_describe_long(pieces): + """TAG-DISTANCE-gHEX[-dirty]. + + Like 'git describe --tags --dirty --always -long'. + The distance/hash is unconditional. + + Exceptions: + 1: no tags. HEX[-dirty] (note: no 'g' prefix) + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) + else: + # exception #1 + rendered = pieces["short"] + if pieces["dirty"]: + rendered += "-dirty" + return rendered + + +def render(pieces, style): + """Render the given version pieces into the requested style.""" + if pieces["error"]: + return {"version": "unknown", + "full-revisionid": pieces.get("long"), + "dirty": None, + "error": pieces["error"], + "date": None} + + if not style or style == "default": + style = "pep440" # the default + + if style == "pep440": + rendered = render_pep440(pieces) + elif style == "pep440-pre": + rendered = render_pep440_pre(pieces) + elif style == "pep440-post": + rendered = render_pep440_post(pieces) + elif style == "pep440-old": + rendered = render_pep440_old(pieces) + elif style == "git-describe": + rendered = render_git_describe(pieces) + elif style == "git-describe-long": + rendered = render_git_describe_long(pieces) + else: + raise ValueError("unknown style '%s'" % style) + + return {"version": rendered, "full-revisionid": pieces["long"], + "dirty": pieces["dirty"], "error": None, + "date": pieces.get("date")} + + +def get_versions(): + """Get version information or return default if unable to do so.""" + # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have + # __file__, we can work backwards from there to the root. Some + # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which + # case we can only use expanded keywords. + + cfg = get_config() + verbose = cfg.verbose + + try: + return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, + verbose) + except NotThisMethod: + pass + + try: + root = os.path.realpath(__file__) + # versionfile_source is the relative path from the top of the source + # tree (where the .git directory might live) to this file. Invert + # this to find the root from __file__. + for i in cfg.versionfile_source.split('/'): + root = os.path.dirname(root) + except NameError: + return {"version": "0+unknown", "full-revisionid": None, + "dirty": None, + "error": "unable to find root of source tree", + "date": None} + + try: + pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) + return render(pieces, cfg.style) + except NotThisMethod: + pass + + try: + if cfg.parentdir_prefix: + return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) + except NotThisMethod: + pass + + return {"version": "0+unknown", "full-revisionid": None, + "dirty": None, + "error": "unable to compute version", "date": None} diff --git a/ccxt/static_dependencies/toolz/compatibility.py b/ccxt/static_dependencies/toolz/compatibility.py new file mode 100644 index 0000000..28bef91 --- /dev/null +++ b/ccxt/static_dependencies/toolz/compatibility.py @@ -0,0 +1,30 @@ +import warnings +warnings.warn("The toolz.compatibility module is no longer " + "needed in Python 3 and has been deprecated. Please " + "import these utilities directly from the standard library. " + "This module will be removed in a future release.", + category=DeprecationWarning, stacklevel=2) + +import operator +import sys + +PY3 = sys.version_info[0] > 2 +PY34 = sys.version_info[0] == 3 and sys.version_info[1] == 4 +PYPY = hasattr(sys, 'pypy_version_info') and PY3 + +__all__ = ('map', 'filter', 'range', 'zip', 'reduce', 'zip_longest', + 'iteritems', 'iterkeys', 'itervalues', 'filterfalse', + 'PY3', 'PY34', 'PYPY') + + +map = map +filter = filter +range = range +zip = zip +from functools import reduce +from itertools import zip_longest +from itertools import filterfalse +iteritems = operator.methodcaller('items') +iterkeys = operator.methodcaller('keys') +itervalues = operator.methodcaller('values') +from collections.abc import Sequence diff --git a/ccxt/static_dependencies/toolz/curried/__init__.py b/ccxt/static_dependencies/toolz/curried/__init__.py new file mode 100644 index 0000000..b92e96f --- /dev/null +++ b/ccxt/static_dependencies/toolz/curried/__init__.py @@ -0,0 +1,101 @@ +""" +Alternate namespace for toolz such that all functions are curried + +Currying provides implicit partial evaluation of all functions + +Example: + + Get usually requires two arguments, an index and a collection + >>> from curried import get + >>> get(0, ('a', 'b')) + 'a' + + When we use it in higher order functions we often want to pass a partially + evaluated form + >>> data = [(1, 2), (11, 22), (111, 222)] + >>> list(map(lambda seq: get(0, seq), data)) + [1, 11, 111] + + The curried version allows simple expression of partial evaluation + >>> list(map(get(0), data)) + [1, 11, 111] + +See Also: + funccurry +""" +from . import operator +from .. import ( + apply, + comp, + complement, + compose, + compose_left, + concat, + concatv, + count, + curry, + diff, + first, + flip, + frequencies, + identity, + interleave, + isdistinct, + isiterable, + juxt, + last, + memoize, + merge_sorted, + peek, + pipe, + second, + thread_first, + thread_last, +) +from .exceptions import merge, merge_with + +# accumulate = curry(accumulate) +# assoc = curry(assoc) +# assoc_in = curry(assoc_in) +# cons = curry(cons) +# countby = curry(countby) +# dissoc = curry(dissoc) +# do = curry(do) +# drop = curry(drop) +# excepts = curry(excepts) +# filter = curry(filter) +# get = curry(get) +# get_in = curry(get_in) +# groupby = curry(groupby) +# interpose = curry(interpose) +# itemfilter = curry(itemfilter) +# itemmap = curry(itemmap) +# iterate = curry(iterate) +# join = curry(join) +# keyfilter = curry(keyfilter) +# keymap = curry(keymap) +# map = curry(map) +# mapcat = curry(mapcat) +# nth = curry(nth) +# partial = curry(partial) +# partition = curry(partition) +# partition_all = curry(partition_all) +# partitionby = curry(partitionby) +# peekn = curry(peekn) +# pluck = curry(pluck) +# random_sample = curry(random_sample) +# reduce = curry(reduce) +# reduceby = curry(reduceby) +# remove = curry(remove) +# sliding_window = curry(sliding_window) +# sorted = curry(sorted) +# tail = curry(tail) +# take = curry(take) +# take_nth = curry(take_nth) +# topk = curry(topk) +# unique = curry(unique) +# update_in = curry(update_in) +# valfilter = curry(valfilter) +# valmap = curry(valmap) + +del exceptions diff --git a/ccxt/static_dependencies/toolz/curried/__pycache__/__init__.cpython-311.pyc b/ccxt/static_dependencies/toolz/curried/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..778e124 Binary files /dev/null and b/ccxt/static_dependencies/toolz/curried/__pycache__/__init__.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/toolz/curried/__pycache__/exceptions.cpython-311.pyc b/ccxt/static_dependencies/toolz/curried/__pycache__/exceptions.cpython-311.pyc new file mode 100644 index 0000000..eb1bf1d Binary files /dev/null and b/ccxt/static_dependencies/toolz/curried/__pycache__/exceptions.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/toolz/curried/__pycache__/operator.cpython-311.pyc b/ccxt/static_dependencies/toolz/curried/__pycache__/operator.cpython-311.pyc new file mode 100644 index 0000000..5cdbc4c Binary files /dev/null and b/ccxt/static_dependencies/toolz/curried/__pycache__/operator.cpython-311.pyc differ diff --git a/ccxt/static_dependencies/toolz/curried/exceptions.py b/ccxt/static_dependencies/toolz/curried/exceptions.py new file mode 100644 index 0000000..e05c7b8 --- /dev/null +++ b/ccxt/static_dependencies/toolz/curried/exceptions.py @@ -0,0 +1,22 @@ +from .. import ( + curry, + merge_with, + merge +) + + +__all__ = ['merge_with', 'merge'] + + +@curry +def merge_with(func, d, *dicts, **kwargs): + return merge_with(func, d, *dicts, **kwargs) + + +@curry +def merge(d, *dicts, **kwargs): + return merge(d, *dicts, **kwargs) + + +merge_with.__doc__ = merge_with.__doc__ +merge.__doc__ = merge.__doc__ diff --git a/ccxt/static_dependencies/toolz/curried/operator.py b/ccxt/static_dependencies/toolz/curried/operator.py new file mode 100644 index 0000000..ca73b92 --- /dev/null +++ b/ccxt/static_dependencies/toolz/curried/operator.py @@ -0,0 +1,22 @@ +from __future__ import absolute_import + +import operator + +from ..functoolz import curry + + +# Tests will catch if/when this needs updated +IGNORE = { + "__abs__", "__index__", "__inv__", "__invert__", "__neg__", "__not__", + "__pos__", "_abs", "abs", "attrgetter", "index", "inv", "invert", + "itemgetter", "neg", "not_", "pos", "truth" +} +locals().update( + {name: f if name in IGNORE else curry(f) + for name, f in vars(operator).items() if callable(f)} +) + +# Clean up the namespace. +del IGNORE +del curry +del operator diff --git a/ccxt/static_dependencies/toolz/dicttoolz.py b/ccxt/static_dependencies/toolz/dicttoolz.py new file mode 100644 index 0000000..457bc26 --- /dev/null +++ b/ccxt/static_dependencies/toolz/dicttoolz.py @@ -0,0 +1,339 @@ +import operator +import collections +from functools import reduce +from collections.abc import Mapping + +__all__ = ('merge', 'merge_with', 'valmap', 'keymap', 'itemmap', + 'valfilter', 'keyfilter', 'itemfilter', + 'assoc', 'dissoc', 'assoc_in', 'update_in', 'get_in') + + +def _get_factory(f, kwargs): + factory = kwargs.pop('factory', dict) + if kwargs: + raise TypeError("{}() got an unexpected keyword argument " + "'{}'".format(f.__name__, kwargs.popitem()[0])) + return factory + + +def merge(*dicts, **kwargs): + """ Merge a collection of dictionaries + + >>> merge({1: 'one'}, {2: 'two'}) + {1: 'one', 2: 'two'} + + Later dictionaries have precedence + + >>> merge({1: 2, 3: 4}, {3: 3, 4: 4}) + {1: 2, 3: 3, 4: 4} + + See Also: + merge_with + """ + if len(dicts) == 1 and not isinstance(dicts[0], Mapping): + dicts = dicts[0] + factory = _get_factory(merge, kwargs) + + rv = factory() + for d in dicts: + rv.update(d) + return rv + + +def merge_with(func, *dicts, **kwargs): + """ Merge dictionaries and apply function to combined values + + A key may occur in more than one dict, and all values mapped from the key + will be passed to the function as a list, such as func([val1, val2, ...]). + + >>> merge_with(sum, {1: 1, 2: 2}, {1: 10, 2: 20}) + {1: 11, 2: 22} + + >>> merge_with(first, {1: 1, 2: 2}, {2: 20, 3: 30}) # doctest: +SKIP + {1: 1, 2: 2, 3: 30} + + See Also: + merge + """ + if len(dicts) == 1 and not isinstance(dicts[0], Mapping): + dicts = dicts[0] + factory = _get_factory(merge_with, kwargs) + + values = collections.defaultdict(lambda: [].append) + for d in dicts: + for k, v in d.items(): + values[k](v) + + result = factory() + for k, v in values.items(): + result[k] = func(v.__self__) + return result + + +def valmap(func, d, factory=dict): + """ Apply function to values of dictionary + + >>> bills = {"Alice": [20, 15, 30], "Bob": [10, 35]} + >>> valmap(sum, bills) # doctest: +SKIP + {'Alice': 65, 'Bob': 45} + + See Also: + keymap + itemmap + """ + rv = factory() + rv.update(zip(d.keys(), map(func, d.values()))) + return rv + + +def keymap(func, d, factory=dict): + """ Apply function to keys of dictionary + + >>> bills = {"Alice": [20, 15, 30], "Bob": [10, 35]} + >>> keymap(str.lower, bills) # doctest: +SKIP + {'alice': [20, 15, 30], 'bob': [10, 35]} + + See Also: + valmap + itemmap + """ + rv = factory() + rv.update(zip(map(func, d.keys()), d.values())) + return rv + + +def itemmap(func, d, factory=dict): + """ Apply function to items of dictionary + + >>> accountids = {"Alice": 10, "Bob": 20} + >>> itemmap(reversed, accountids) # doctest: +SKIP + {10: "Alice", 20: "Bob"} + + See Also: + keymap + valmap + """ + rv = factory() + rv.update(map(func, d.items())) + return rv + + +def valfilter(predicate, d, factory=dict): + """ Filter items in dictionary by value + + >>> iseven = lambda x: x % 2 == 0 + >>> d = {1: 2, 2: 3, 3: 4, 4: 5} + >>> valfilter(iseven, d) + {1: 2, 3: 4} + + See Also: + keyfilter + itemfilter + valmap + """ + rv = factory() + for k, v in d.items(): + if predicate(v): + rv[k] = v + return rv + + +def keyfilter(predicate, d, factory=dict): + """ Filter items in dictionary by key + + >>> iseven = lambda x: x % 2 == 0 + >>> d = {1: 2, 2: 3, 3: 4, 4: 5} + >>> keyfilter(iseven, d) + {2: 3, 4: 5} + + See Also: + valfilter + itemfilter + keymap + """ + rv = factory() + for k, v in d.items(): + if predicate(k): + rv[k] = v + return rv + + +def itemfilter(predicate, d, factory=dict): + """ Filter items in dictionary by item + + >>> def isvalid(item): + ... k, v = item + ... return k % 2 == 0 and v < 4 + + >>> d = {1: 2, 2: 3, 3: 4, 4: 5} + >>> itemfilter(isvalid, d) + {2: 3} + + See Also: + keyfilter + valfilter + itemmap + """ + rv = factory() + for item in d.items(): + if predicate(item): + k, v = item + rv[k] = v + return rv + + +def assoc(d, key, value, factory=dict): + """ Return a new dict with new key value pair + + New dict has d[key] set to value. Does not modify the initial dictionary. + + >>> assoc({'x': 1}, 'x', 2) + {'x': 2} + >>> assoc({'x': 1}, 'y', 3) # doctest: +SKIP + {'x': 1, 'y': 3} + """ + d2 = factory() + d2.update(d) + d2[key] = value + return d2 + + +def dissoc(d, *keys, **kwargs): + """ Return a new dict with the given key(s) removed. + + New dict has d[key] deleted for each supplied key. + Does not modify the initial dictionary. + + >>> dissoc({'x': 1, 'y': 2}, 'y') + {'x': 1} + >>> dissoc({'x': 1, 'y': 2}, 'y', 'x') + {} + >>> dissoc({'x': 1}, 'y') # Ignores missing keys + {'x': 1} + """ + factory = _get_factory(dissoc, kwargs) + d2 = factory() + + if len(keys) < len(d) * .6: + d2.update(d) + for key in keys: + if key in d2: + del d2[key] + else: + remaining = set(d) + remaining.difference_update(keys) + for k in remaining: + d2[k] = d[k] + return d2 + + +def assoc_in(d, keys, value, factory=dict): + """ Return a new dict with new, potentially nested, key value pair + + >>> purchase = {'name': 'Alice', + ... 'order': {'items': ['Apple', 'Orange'], + ... 'costs': [0.50, 1.25]}, + ... 'credit card': '5555-1234-1234-1234'} + >>> assoc_in(purchase, ['order', 'costs'], [0.25, 1.00]) # doctest: +SKIP + {'credit card': '5555-1234-1234-1234', + 'name': 'Alice', + 'order': {'costs': [0.25, 1.00], 'items': ['Apple', 'Orange']}} + """ + return update_in(d, keys, lambda x: value, value, factory) + + +def update_in(d, keys, func, default=None, factory=dict): + """ Update value in a (potentially) nested dictionary + + inputs: + d - dictionary on which to operate + keys - list or tuple giving the location of the value to be changed in d + func - function to operate on that value + + If keys == [k0,..,kX] and d[k0]..[kX] == v, update_in returns a copy of the + original dictionary with v replaced by func(v), but does not mutate the + original dictionary. + + If k0 is not a key in d, update_in creates nested dictionaries to the depth + specified by the keys, with the innermost value set to func(default). + + >>> inc = lambda x: x + 1 + >>> update_in({'a': 0}, ['a'], inc) + {'a': 1} + + >>> transaction = {'name': 'Alice', + ... 'purchase': {'items': ['Apple', 'Orange'], + ... 'costs': [0.50, 1.25]}, + ... 'credit card': '5555-1234-1234-1234'} + >>> update_in(transaction, ['purchase', 'costs'], sum) # doctest: +SKIP + {'credit card': '5555-1234-1234-1234', + 'name': 'Alice', + 'purchase': {'costs': 1.75, 'items': ['Apple', 'Orange']}} + + >>> # updating a value when k0 is not in d + >>> update_in({}, [1, 2, 3], str, default="bar") + {1: {2: {3: 'bar'}}} + >>> update_in({1: 'foo'}, [2, 3, 4], inc, 0) + {1: 'foo', 2: {3: {4: 1}}} + """ + ks = iter(keys) + k = next(ks) + + rv = inner = factory() + rv.update(d) + + for key in ks: + if k in d: + d = d[k] + dtemp = factory() + dtemp.update(d) + else: + d = dtemp = factory() + + inner[k] = inner = dtemp + k = key + + if k in d: + inner[k] = func(d[k]) + else: + inner[k] = func(default) + return rv + + +def get_in(keys, coll, default=None, no_default=False): + """ Returns coll[i0][i1]...[iX] where [i0, i1, ..., iX]==keys. + + If coll[i0][i1]...[iX] cannot be found, returns ``default``, unless + ``no_default`` is specified, then it raises KeyError or IndexError. + + ``get_in`` is a generalization of ``operator.getitem`` for nested data + structures such as dictionaries and lists. + + >>> transaction = {'name': 'Alice', + ... 'purchase': {'items': ['Apple', 'Orange'], + ... 'costs': [0.50, 1.25]}, + ... 'credit card': '5555-1234-1234-1234'} + >>> get_in(['purchase', 'items', 0], transaction) + 'Apple' + >>> get_in(['name'], transaction) + 'Alice' + >>> get_in(['purchase', 'total'], transaction) + >>> get_in(['purchase', 'items', 'apple'], transaction) + >>> get_in(['purchase', 'items', 10], transaction) + >>> get_in(['purchase', 'total'], transaction, 0) + 0 + >>> get_in(['y'], {}, no_default=True) + Traceback (most recent call last): + ... + KeyError: 'y' + + See Also: + itertoolz.get + operator.getitem + """ + try: + return reduce(operator.getitem, keys, coll) + except (KeyError, IndexError, TypeError): + if no_default: + raise + return default diff --git a/ccxt/static_dependencies/toolz/functoolz.py b/ccxt/static_dependencies/toolz/functoolz.py new file mode 100644 index 0000000..7709f15 --- /dev/null +++ b/ccxt/static_dependencies/toolz/functoolz.py @@ -0,0 +1,1049 @@ +from functools import reduce, partial +import inspect +import sys +from operator import attrgetter, not_ +from importlib import import_module +from types import MethodType + +from .utils import no_default + +PYPY = hasattr(sys, 'pypy_version_info') and sys.version_info[0] > 2 + + +__all__ = ('identity', 'apply', 'thread_first', 'thread_last', 'memoize', + 'compose', 'compose_left', 'pipe', 'complement', 'juxt', 'do', + 'curry', 'flip', 'excepts') + +PYPY = hasattr(sys, 'pypy_version_info') + + +def identity(x): + """ Identity function. Return x + + >>> identity(3) + 3 + """ + return x + + +def apply(*func_and_args, **kwargs): + """ Applies a function and returns the results + + >>> def double(x): return 2*x + >>> def inc(x): return x + 1 + >>> apply(double, 5) + 10 + + >>> tuple(map(apply, [double, inc, double], [10, 500, 8000])) + (20, 501, 16000) + """ + if not func_and_args: + raise TypeError('func argument is required') + func, args = func_and_args[0], func_and_args[1:] + return func(*args, **kwargs) + + +def thread_first(val, *forms): + """ Thread value through a sequence of functions/forms + + >>> def double(x): return 2*x + >>> def inc(x): return x + 1 + >>> thread_first(1, inc, double) + 4 + + If the function expects more than one input you can specify those inputs + in a tuple. The value is used as the first input. + + >>> def add(x, y): return x + y + >>> def pow(x, y): return x**y + >>> thread_first(1, (add, 4), (pow, 2)) # pow(add(1, 4), 2) + 25 + + So in general + thread_first(x, f, (g, y, z)) + expands to + g(f(x), y, z) + + See Also: + thread_last + """ + def evalform_front(val, form): + if callable(form): + return form(val) + if isinstance(form, tuple): + func, args = form[0], form[1:] + args = (val,) + args + return func(*args) + return reduce(evalform_front, forms, val) + + +def thread_last(val, *forms): + """ Thread value through a sequence of functions/forms + + >>> def double(x): return 2*x + >>> def inc(x): return x + 1 + >>> thread_last(1, inc, double) + 4 + + If the function expects more than one input you can specify those inputs + in a tuple. The value is used as the last input. + + >>> def add(x, y): return x + y + >>> def pow(x, y): return x**y + >>> thread_last(1, (add, 4), (pow, 2)) # pow(2, add(4, 1)) + 32 + + So in general + thread_last(x, f, (g, y, z)) + expands to + g(y, z, f(x)) + + >>> def iseven(x): + ... return x % 2 == 0 + >>> list(thread_last([1, 2, 3], (map, inc), (filter, iseven))) + [2, 4] + + See Also: + thread_first + """ + def evalform_back(val, form): + if callable(form): + return form(val) + if isinstance(form, tuple): + func, args = form[0], form[1:] + args = args + (val,) + return func(*args) + return reduce(evalform_back, forms, val) + + +def instanceproperty(fget=None, fset=None, fdel=None, doc=None, classval=None): + """ Like @property, but returns ``classval`` when used as a class attribute + + >>> class MyClass(object): + ... '''The class docstring''' + ... @instanceproperty(classval=__doc__) + ... def __doc__(self): + ... return 'An object docstring' + ... @instanceproperty + ... def val(self): + ... return 42 + ... + >>> MyClass.__doc__ + 'The class docstring' + >>> MyClass.val is None + True + >>> obj = MyClass() + >>> obj.__doc__ + 'An object docstring' + >>> obj.val + 42 + """ + if fget is None: + return partial(instanceproperty, fset=fset, fdel=fdel, doc=doc, + classval=classval) + return InstanceProperty(fget=fget, fset=fset, fdel=fdel, doc=doc, + classval=classval) + + +class InstanceProperty(property): + """ Like @property, but returns ``classval`` when used as a class attribute + + Should not be used directly. Use ``instanceproperty`` instead. + """ + def __init__(self, fget=None, fset=None, fdel=None, doc=None, + classval=None): + self.classval = classval + property.__init__(self, fget=fget, fset=fset, fdel=fdel, doc=doc) + + def __get__(self, obj, type=None): + if obj is None: + return self.classval + return property.__get__(self, obj, type) + + def __reduce__(self): + state = (self.fget, self.fset, self.fdel, self.__doc__, self.classval) + return InstanceProperty, state + + +class curry(object): + """ Curry a callable function + + Enables partial application of arguments through calling a function with an + incomplete set of arguments. + + >>> def mul(x, y): + ... return x * y + >>> mul = curry(mul) + + >>> double = mul(2) + >>> double(10) + 20 + + Also supports keyword arguments + + >>> @curry # Can use curry as a decorator + ... def f(x, y, a=10): + ... return a * (x + y) + + >>> add = f(a=1) + >>> add(2, 3) + 5 + + See Also: + toolz.curried - namespace of curried functions + https://toolz.readthedocs.io/en/latest/curry.html + """ + def __init__(self, *args, **kwargs): + if not args: + raise TypeError('__init__() takes at least 2 arguments (1 given)') + func, args = args[0], args[1:] + if not callable(func): + raise TypeError("Input must be callable") + + # curry- or functools.partial-like object? Unpack and merge arguments + if ( + hasattr(func, 'func') + and hasattr(func, 'args') + and hasattr(func, 'keywords') + and isinstance(func.args, tuple) + ): + _kwargs = {} + if func.keywords: + _kwargs.update(func.keywords) + _kwargs.update(kwargs) + kwargs = _kwargs + args = func.args + args + func = func.func + + if kwargs: + self._partial = partial(func, *args, **kwargs) + else: + self._partial = partial(func, *args) + + self.__doc__ = getattr(func, '__doc__', None) + self.__name__ = getattr(func, '__name__', '') + self.__module__ = getattr(func, '__module__', None) + self.__qualname__ = getattr(func, '__qualname__', None) + self._sigspec = None + self._has_unknown_args = None + + @instanceproperty + def func(self): + return self._partial.func + + @instanceproperty + def __signature__(self): + sig = inspect.signature(self.func) + args = self.args or () + keywords = self.keywords or {} + if is_partial_args(self.func, args, keywords, sig) is False: + raise TypeError('curry object has incorrect arguments') + + params = list(sig.parameters.values()) + skip = 0 + for param in params[:len(args)]: + if param.kind == param.VAR_POSITIONAL: + break + skip += 1 + + kwonly = False + newparams = [] + for param in params[skip:]: + kind = param.kind + default = param.default + if kind == param.VAR_KEYWORD: + pass + elif kind == param.VAR_POSITIONAL: + if kwonly: + continue + elif param.name in keywords: + default = keywords[param.name] + kind = param.KEYWORD_ONLY + kwonly = True + else: + if kwonly: + kind = param.KEYWORD_ONLY + if default is param.empty: + default = no_default + newparams.append(param.replace(default=default, kind=kind)) + + return sig.replace(parameters=newparams) + + @instanceproperty + def args(self): + return self._partial.args + + @instanceproperty + def keywords(self): + return self._partial.keywords + + @instanceproperty + def func_name(self): + return self.__name__ + + def __str__(self): + return str(self.func) + + def __repr__(self): + return repr(self.func) + + def __hash__(self): + return hash((self.func, self.args, + frozenset(self.keywords.items()) if self.keywords + else None)) + + def __eq__(self, other): + return (isinstance(other, curry) and self.func == other.func and + self.args == other.args and self.keywords == other.keywords) + + def __ne__(self, other): + return not self.__eq__(other) + + def __call__(self, *args, **kwargs): + try: + return self._partial(*args, **kwargs) + except TypeError as exc: + if self._should_curry(args, kwargs, exc): + return self.bind(*args, **kwargs) + raise + + def _should_curry(self, args, kwargs, exc=None): + func = self.func + args = self.args + args + if self.keywords: + kwargs = dict(self.keywords, **kwargs) + if self._sigspec is None: + sigspec = self._sigspec = _sigs.signature_or_spec(func) + self._has_unknown_args = has_varargs(func, sigspec) is not False + else: + sigspec = self._sigspec + + if is_partial_args(func, args, kwargs, sigspec) is False: + # Nothing can make the call valid + return False + elif self._has_unknown_args: + # The call may be valid and raised a TypeError, but we curry + # anyway because the function may have `*args`. This is useful + # for decorators with signature `func(*args, **kwargs)`. + return True + elif not is_valid_args(func, args, kwargs, sigspec): + # Adding more arguments may make the call valid + return True + else: + # There was a genuine TypeError + return False + + def bind(self, *args, **kwargs): + return type(self)(self, *args, **kwargs) + + def call(self, *args, **kwargs): + return self._partial(*args, **kwargs) + + def __get__(self, instance, owner): + if instance is None: + return self + return curry(self, instance) + + def __reduce__(self): + func = self.func + modname = getattr(func, '__module__', None) + qualname = getattr(func, '__qualname__', None) + if qualname is None: # pragma: no cover + qualname = getattr(func, '__name__', None) + is_decorated = None + if modname and qualname: + attrs = [] + obj = import_module(modname) + for attr in qualname.split('.'): + if isinstance(obj, curry): + attrs.append('func') + obj = obj.func + obj = getattr(obj, attr, None) + if obj is None: + break + attrs.append(attr) + if isinstance(obj, curry) and obj.func is func: + is_decorated = obj is self + qualname = '.'.join(attrs) + func = '%s:%s' % (modname, qualname) + + # functools.partial objects can't be pickled + userdict = tuple((k, v) for k, v in self.__dict__.items() + if k not in ('_partial', '_sigspec')) + state = (type(self), func, self.args, self.keywords, userdict, + is_decorated) + return _restore_curry, state + + +def _restore_curry(cls, func, args, kwargs, userdict, is_decorated): + if isinstance(func, str): + modname, qualname = func.rsplit(':', 1) + obj = import_module(modname) + for attr in qualname.split('.'): + obj = getattr(obj, attr) + if is_decorated: + return obj + func = obj.func + obj = cls(func, *args, **(kwargs or {})) + obj.__dict__.update(userdict) + return obj + + +@curry +def memoize(func, cache=None, key=None): + """ Cache a function's result for speedy future evaluation + + Considerations: + Trades memory for speed. + Only use on pure functions. + + >>> def add(x, y): return x + y + >>> add = memoize(add) + + Or use as a decorator + + >>> @memoize + ... def add(x, y): + ... return x + y + + Use the ``cache`` keyword to provide a dict-like object as an initial cache + + >>> @memoize(cache={(1, 2): 3}) + ... def add(x, y): + ... return x + y + + Note that the above works as a decorator because ``memoize`` is curried. + + It is also possible to provide a ``key(args, kwargs)`` function that + calculates keys used for the cache, which receives an ``args`` tuple and + ``kwargs`` dict as input, and must return a hashable value. However, + the default key function should be sufficient most of the time. + + >>> # Use key function that ignores extraneous keyword arguments + >>> @memoize(key=lambda args, kwargs: args) + ... def add(x, y, verbose=False): + ... if verbose: + ... print('Calculating %s + %s' % (x, y)) + ... return x + y + """ + if cache is None: + cache = {} + + try: + may_have_kwargs = has_keywords(func) is not False + # Is unary function (single arg, no variadic argument or keywords)? + is_unary = is_arity(1, func) + except TypeError: # pragma: no cover + may_have_kwargs = True + is_unary = False + + if key is None: + if is_unary: + def key(args, kwargs): + return args[0] + elif may_have_kwargs: + def key(args, kwargs): + return ( + args or None, + frozenset(kwargs.items()) if kwargs else None, + ) + else: + def key(args, kwargs): + return args + + def memof(*args, **kwargs): + k = key(args, kwargs) + try: + return cache[k] + except TypeError: + raise TypeError("Arguments to memoized function must be hashable") + except KeyError: + cache[k] = result = func(*args, **kwargs) + return result + + try: + memof.__name__ = func.__name__ + except AttributeError: + pass + memof.__doc__ = func.__doc__ + memof.__wrapped__ = func + return memof + + +class Compose(object): + """ A composition of functions + + See Also: + compose + """ + __slots__ = 'first', 'funcs' + + def __init__(self, funcs): + funcs = tuple(reversed(funcs)) + self.first = funcs[0] + self.funcs = funcs[1:] + + def __call__(self, *args, **kwargs): + ret = self.first(*args, **kwargs) + for f in self.funcs: + ret = f(ret) + return ret + + def __getstate__(self): + return self.first, self.funcs + + def __setstate__(self, state): + self.first, self.funcs = state + + @instanceproperty(classval=__doc__) + def __doc__(self): + def composed_doc(*fs): + """Generate a docstring for the composition of fs. + """ + if not fs: + # Argument name for the docstring. + return '*args, **kwargs' + + return '{f}({g})'.format(f=fs[0].__name__, g=composed_doc(*fs[1:])) + + try: + return ( + 'lambda *args, **kwargs: ' + + composed_doc(*reversed((self.first,) + self.funcs)) + ) + except AttributeError: + # One of our callables does not have a `__name__`, whatever. + return 'A composition of functions' + + @property + def __name__(self): + try: + return '_of_'.join( + (f.__name__ for f in reversed((self.first,) + self.funcs)) + ) + except AttributeError: + return type(self).__name__ + + def __repr__(self): + return '{.__class__.__name__}{!r}'.format( + self, tuple(reversed((self.first, ) + self.funcs))) + + def __eq__(self, other): + if isinstance(other, Compose): + return other.first == self.first and other.funcs == self.funcs + return NotImplemented + + def __ne__(self, other): + equality = self.__eq__(other) + return NotImplemented if equality is NotImplemented else not equality + + def __hash__(self): + return hash(self.first) ^ hash(self.funcs) + + # Mimic the descriptor behavior of python functions. + # i.e. let Compose be called as a method when bound to a class. + # adapted from + # docs.python.org/3/howto/descriptor.html#functions-and-methods + def __get__(self, obj, objtype=None): + return self if obj is None else MethodType(self, obj) + + # introspection with Signature is only possible from py3.3+ + @instanceproperty + def __signature__(self): + base = inspect.signature(self.first) + last = inspect.signature(self.funcs[-1]) + return base.replace(return_annotation=last.return_annotation) + + __wrapped__ = instanceproperty(attrgetter('first')) + + +def compose(*funcs): + """ Compose functions to operate in series. + + Returns a function that applies other functions in sequence. + + Functions are applied from right to left so that + ``compose(f, g, h)(x, y)`` is the same as ``f(g(h(x, y)))``. + + If no arguments are provided, the identity function (f(x) = x) is returned. + + >>> inc = lambda i: i + 1 + >>> compose(str, inc)(3) + '4' + + See Also: + compose_left + pipe + """ + if not funcs: + return identity + if len(funcs) == 1: + return funcs[0] + else: + return Compose(funcs) + + +def compose_left(*funcs): + """ Compose functions to operate in series. + + Returns a function that applies other functions in sequence. + + Functions are applied from left to right so that + ``compose_left(f, g, h)(x, y)`` is the same as ``h(g(f(x, y)))``. + + If no arguments are provided, the identity function (f(x) = x) is returned. + + >>> inc = lambda i: i + 1 + >>> compose_left(inc, str)(3) + '4' + + See Also: + compose + pipe + """ + return compose(*reversed(funcs)) + + +def pipe(data, *funcs): + """ Pipe a value through a sequence of functions + + I.e. ``pipe(data, f, g, h)`` is equivalent to ``h(g(f(data)))`` + + We think of the value as progressing through a pipe of several + transformations, much like pipes in UNIX + + ``$ cat data | f | g | h`` + + >>> double = lambda i: 2 * i + >>> pipe(3, double, str) + '6' + + See Also: + compose + compose_left + thread_first + thread_last + """ + for func in funcs: + data = func(data) + return data + + +def complement(func): + """ Convert a predicate function to its logical complement. + + In other words, return a function that, for inputs that normally + yield True, yields False, and vice-versa. + + >>> def iseven(n): return n % 2 == 0 + >>> isodd = complement(iseven) + >>> iseven(2) + True + >>> isodd(2) + False + """ + return compose(not_, func) + + +class juxt(object): + """ Creates a function that calls several functions with the same arguments + + Takes several functions and returns a function that applies its arguments + to each of those functions then returns a tuple of the results. + + Name comes from juxtaposition: the fact of two things being seen or placed + close together with contrasting effect. + + >>> inc = lambda x: x + 1 + >>> double = lambda x: x * 2 + >>> juxt(inc, double)(10) + (11, 20) + >>> juxt([inc, double])(10) + (11, 20) + """ + __slots__ = ['funcs'] + + def __init__(self, *funcs): + if len(funcs) == 1 and not callable(funcs[0]): + funcs = funcs[0] + self.funcs = tuple(funcs) + + def __call__(self, *args, **kwargs): + return tuple(func(*args, **kwargs) for func in self.funcs) + + def __getstate__(self): + return self.funcs + + def __setstate__(self, state): + self.funcs = state + + +def do(func, x): + """ Runs ``func`` on ``x``, returns ``x`` + + Because the results of ``func`` are not returned, only the side + effects of ``func`` are relevant. + + Logging functions can be made by composing ``do`` with a storage function + like ``list.append`` or ``file.write`` + + >>> from toolz import compose + >>> from toolz.curried import do + + >>> log = [] + >>> inc = lambda x: x + 1 + >>> inc = compose(inc, do(log.append)) + >>> inc(1) + 2 + >>> inc(11) + 12 + >>> log + [1, 11] + """ + func(x) + return x + + +@curry +def flip(func, a, b): + """ Call the function call with the arguments flipped + + This function is curried. + + >>> def div(a, b): + ... return a // b + ... + >>> flip(div, 2, 6) + 3 + >>> div_by_two = flip(div, 2) + >>> div_by_two(4) + 2 + + This is particularly useful for built in functions and functions defined + in C extensions that accept positional only arguments. For example: + isinstance, issubclass. + + >>> data = [1, 'a', 'b', 2, 1.5, object(), 3] + >>> only_ints = list(filter(flip(isinstance, int), data)) + >>> only_ints + [1, 2, 3] + """ + return func(b, a) + + +def return_none(exc): + """ Returns None. + """ + return None + + +class excepts(object): + """A wrapper around a function to catch exceptions and + dispatch to a handler. + + This is like a functional try/except block, in the same way that + ifexprs are functional if/else blocks. + + Examples + -------- + >>> excepting = excepts( + ... ValueError, + ... lambda a: [1, 2].index(a), + ... lambda _: -1, + ... ) + >>> excepting(1) + 0 + >>> excepting(3) + -1 + + Multiple exceptions and default except clause. + + >>> excepting = excepts((IndexError, KeyError), lambda a: a[0]) + >>> excepting([]) + >>> excepting([1]) + 1 + >>> excepting({}) + >>> excepting({0: 1}) + 1 + """ + def __init__(self, exc, func, handler=return_none): + self.exc = exc + self.func = func + self.handler = handler + + def __call__(self, *args, **kwargs): + try: + return self.func(*args, **kwargs) + except self.exc as e: + return self.handler(e) + + @instanceproperty(classval=__doc__) + def __doc__(self): + from textwrap import dedent + + exc = self.exc + try: + if isinstance(exc, tuple): + exc_name = '(%s)' % ', '.join( + map(attrgetter('__name__'), exc), + ) + else: + exc_name = exc.__name__ + + return dedent( + """\ + A wrapper around {inst.func.__name__!r} that will except: + {exc} + and handle any exceptions with {inst.handler.__name__!r}. + + Docs for {inst.func.__name__!r}: + {inst.func.__doc__} + + Docs for {inst.handler.__name__!r}: + {inst.handler.__doc__} + """ + ).format( + inst=self, + exc=exc_name, + ) + except AttributeError: + return type(self).__doc__ + + @property + def __name__(self): + exc = self.exc + try: + if isinstance(exc, tuple): + exc_name = '_or_'.join(map(attrgetter('__name__'), exc)) + else: + exc_name = exc.__name__ + return '%s_excepting_%s' % (self.func.__name__, exc_name) + except AttributeError: + return 'excepting' + + +def _check_sigspec(sigspec, func, builtin_func, *builtin_args): + if sigspec is None: + try: + sigspec = inspect.signature(func) + except (ValueError, TypeError) as e: + sigspec = e + if isinstance(sigspec, ValueError): + return None, builtin_func(*builtin_args) + elif not isinstance(sigspec, inspect.Signature): + if ( + func in _sigs.signatures + and (( + hasattr(func, '__signature__') + and hasattr(func.__signature__, '__get__') + )) + ): + val = builtin_func(*builtin_args) + return None, val + return None, False + return sigspec, None + + +if PYPY: # pragma: no cover + _check_sigspec_orig = _check_sigspec + + def _check_sigspec(sigspec, func, builtin_func, *builtin_args): + # PyPy may lie, so use our registry for builtins instead + if func in _sigs.signatures: + val = builtin_func(*builtin_args) + return None, val + return _check_sigspec_orig(sigspec, func, builtin_func, *builtin_args) + + +_check_sigspec.__doc__ = """ \ +Private function to aid in introspection compatibly across Python versions. + +If a callable doesn't have a signature (Python 3) or an argspec (Python 2), +the signature registry in toolz._signatures is used. +""" + + +def num_required_args(func, sigspec=None): + sigspec, rv = _check_sigspec(sigspec, func, _sigs._num_required_args, + func) + if sigspec is None: + return rv + return sum(1 for p in sigspec.parameters.values() + if p.default is p.empty + and p.kind in (p.POSITIONAL_OR_KEYWORD, p.POSITIONAL_ONLY)) + + +def has_varargs(func, sigspec=None): + sigspec, rv = _check_sigspec(sigspec, func, _sigs._has_varargs, func) + if sigspec is None: + return rv + return any(p.kind == p.VAR_POSITIONAL + for p in sigspec.parameters.values()) + + +def has_keywords(func, sigspec=None): + sigspec, rv = _check_sigspec(sigspec, func, _sigs._has_keywords, func) + if sigspec is None: + return rv + return any(p.default is not p.empty + or p.kind in (p.KEYWORD_ONLY, p.VAR_KEYWORD) + for p in sigspec.parameters.values()) + + +def is_valid_args(func, args, kwargs, sigspec=None): + sigspec, rv = _check_sigspec(sigspec, func, _sigs._is_valid_args, + func, args, kwargs) + if sigspec is None: + return rv + try: + sigspec.bind(*args, **kwargs) + except TypeError: + return False + return True + + +def is_partial_args(func, args, kwargs, sigspec=None): + sigspec, rv = _check_sigspec(sigspec, func, _sigs._is_partial_args, + func, args, kwargs) + if sigspec is None: + return rv + try: + sigspec.bind_partial(*args, **kwargs) + except TypeError: + return False + return True + + +def is_arity(n, func, sigspec=None): + """ Does a function have only n positional arguments? + + This function relies on introspection and does not call the function. + Returns None if validity can't be determined. + + >>> def f(x): + ... return x + >>> is_arity(1, f) + True + >>> def g(x, y=1): + ... return x + y + >>> is_arity(1, g) + False + """ + sigspec, rv = _check_sigspec(sigspec, func, _sigs._is_arity, n, func) + if sigspec is None: + return rv + num = num_required_args(func, sigspec) + if num is not None: + num = num == n + if not num: + return False + varargs = has_varargs(func, sigspec) + if varargs: + return False + keywords = has_keywords(func, sigspec) + if keywords: + return False + if num is None or varargs is None or keywords is None: # pragma: no cover + return None + return True + + +num_required_args.__doc__ = """ \ +Number of required positional arguments + + This function relies on introspection and does not call the function. + Returns None if validity can't be determined. + + >>> def f(x, y, z=3): + ... return x + y + z + >>> num_required_args(f) + 2 + >>> def g(*args, **kwargs): + ... pass + >>> num_required_args(g) + 0 + """ + +has_varargs.__doc__ = """ \ +Does a function have variadic positional arguments? + + This function relies on introspection and does not call the function. + Returns None if validity can't be determined. + + >>> def f(*args): + ... return args + >>> has_varargs(f) + True + >>> def g(**kwargs): + ... return kwargs + >>> has_varargs(g) + False + """ + +has_keywords.__doc__ = """ \ +Does a function have keyword arguments? + + This function relies on introspection and does not call the function. + Returns None if validity can't be determined. + + >>> def f(x, y=0): + ... return x + y + + >>> has_keywords(f) + True + """ + +is_valid_args.__doc__ = """ \ +Is ``func(*args, **kwargs)`` a valid function call? + + This function relies on introspection and does not call the function. + Returns None if validity can't be determined. + + >>> def add(x, y): + ... return x + y + + >>> is_valid_args(add, (1,), {}) + False + >>> is_valid_args(add, (1, 2), {}) + True + >>> is_valid_args(map, (), {}) + False + + **Implementation notes** + Python 2 relies on ``inspect.getargspec``, which only works for + user-defined functions. Python 3 uses ``inspect.signature``, which + works for many more types of callables. + + Many builtins in the standard library are also supported. + """ + +is_partial_args.__doc__ = """ \ +Can partial(func, *args, **kwargs)(*args2, **kwargs2) be a valid call? + + Returns True *only* if the call is valid or if it is possible for the + call to become valid by adding more positional or keyword arguments. + + This function relies on introspection and does not call the function. + Returns None if validity can't be determined. + + >>> def add(x, y): + ... return x + y + + >>> is_partial_args(add, (1,), {}) + True + >>> is_partial_args(add, (1, 2), {}) + True + >>> is_partial_args(add, (1, 2, 3), {}) + False + >>> is_partial_args(map, (), {}) + True + + **Implementation notes** + Python 2 relies on ``inspect.getargspec``, which only works for + user-defined functions. Python 3 uses ``inspect.signature``, which + works for many more types of callables. + + Many builtins in the standard library are also supported. + """ + +from . import _signatures as _sigs diff --git a/ccxt/static_dependencies/toolz/itertoolz.py b/ccxt/static_dependencies/toolz/itertoolz.py new file mode 100644 index 0000000..179bdcd --- /dev/null +++ b/ccxt/static_dependencies/toolz/itertoolz.py @@ -0,0 +1,1057 @@ +import itertools +import heapq +import collections +import operator +from functools import partial +from itertools import filterfalse, zip_longest +from collections.abc import Sequence +from .utils import no_default + + +__all__ = ('remove', 'accumulate', 'groupby', 'merge_sorted', 'interleave', + 'unique', 'isiterable', 'isdistinct', 'take', 'drop', 'take_nth', + 'first', 'second', 'nth', 'last', 'get', 'concat', 'concatv', + 'mapcat', 'cons', 'interpose', 'frequencies', 'reduceby', 'iterate', + 'sliding_window', 'partition', 'partition_all', 'count', 'pluck', + 'join', 'tail', 'diff', 'topk', 'peek', 'peekn', 'random_sample') + + +def remove(predicate, seq): + """ Return those items of sequence for which predicate(item) is False + + >>> def iseven(x): + ... return x % 2 == 0 + >>> list(remove(iseven, [1, 2, 3, 4])) + [1, 3] + """ + return filterfalse(predicate, seq) + + +def accumulate(binop, seq, initial=no_default): + """ Repeatedly apply binary function to a sequence, accumulating results + + >>> from operator import add, mul + >>> list(accumulate(add, [1, 2, 3, 4, 5])) + [1, 3, 6, 10, 15] + >>> list(accumulate(mul, [1, 2, 3, 4, 5])) + [1, 2, 6, 24, 120] + + Accumulate is similar to ``reduce`` and is good for making functions like + cumulative sum: + + >>> from functools import partial, reduce + >>> sum = partial(reduce, add) + >>> cumsum = partial(accumulate, add) + + Accumulate also takes an optional argument that will be used as the first + value. This is similar to reduce. + + >>> list(accumulate(add, [1, 2, 3], -1)) + [-1, 0, 2, 5] + >>> list(accumulate(add, [], 1)) + [1] + + See Also: + itertools.accumulate : In standard itertools for Python 3.2+ + """ + seq = iter(seq) + if initial == no_default: + try: + result = next(seq) + except StopIteration: + return + else: + result = initial + yield result + for elem in seq: + result = binop(result, elem) + yield result + + +def groupby(key, seq): + """ Group a collection by a key function + + >>> names = ['Alice', 'Bob', 'Charlie', 'Dan', 'Edith', 'Frank'] + >>> groupby(len, names) # doctest: +SKIP + {3: ['Bob', 'Dan'], 5: ['Alice', 'Edith', 'Frank'], 7: ['Charlie']} + + >>> iseven = lambda x: x % 2 == 0 + >>> groupby(iseven, [1, 2, 3, 4, 5, 6, 7, 8]) # doctest: +SKIP + {False: [1, 3, 5, 7], True: [2, 4, 6, 8]} + + Non-callable keys imply grouping on a member. + + >>> groupby('gender', [{'name': 'Alice', 'gender': 'F'}, + ... {'name': 'Bob', 'gender': 'M'}, + ... {'name': 'Charlie', 'gender': 'M'}]) # doctest:+SKIP + {'F': [{'gender': 'F', 'name': 'Alice'}], + 'M': [{'gender': 'M', 'name': 'Bob'}, + {'gender': 'M', 'name': 'Charlie'}]} + + Not to be confused with ``itertools.groupby`` + + See Also: + countby + """ + if not callable(key): + key = getter(key) + d = collections.defaultdict(lambda: [].append) + for item in seq: + d[key(item)](item) + rv = {} + for k, v in d.items(): + rv[k] = v.__self__ + return rv + + +def merge_sorted(*seqs, **kwargs): + """ Merge and sort a collection of sorted collections + + This works lazily and only keeps one value from each iterable in memory. + + >>> list(merge_sorted([1, 3, 5], [2, 4, 6])) + [1, 2, 3, 4, 5, 6] + + >>> ''.join(merge_sorted('abc', 'abc', 'abc')) + 'aaabbbccc' + + The "key" function used to sort the input may be passed as a keyword. + + >>> list(merge_sorted([2, 3], [1, 3], key=lambda x: x // 3)) + [2, 1, 3, 3] + """ + if len(seqs) == 0: + return iter([]) + elif len(seqs) == 1: + return iter(seqs[0]) + + key = kwargs.get('key', None) + if key is None: + return _merge_sorted_binary(seqs) + else: + return _merge_sorted_binary_key(seqs, key) + + +def _merge_sorted_binary(seqs): + mid = len(seqs) // 2 + L1 = seqs[:mid] + if len(L1) == 1: + seq1 = iter(L1[0]) + else: + seq1 = _merge_sorted_binary(L1) + L2 = seqs[mid:] + if len(L2) == 1: + seq2 = iter(L2[0]) + else: + seq2 = _merge_sorted_binary(L2) + + try: + val2 = next(seq2) + except StopIteration: + for val1 in seq1: + yield val1 + return + + for val1 in seq1: + if val2 < val1: + yield val2 + for val2 in seq2: + if val2 < val1: + yield val2 + else: + yield val1 + break + else: + break + else: + yield val1 + else: + yield val2 + for val2 in seq2: + yield val2 + return + yield val1 + for val1 in seq1: + yield val1 + + +def _merge_sorted_binary_key(seqs, key): + mid = len(seqs) // 2 + L1 = seqs[:mid] + if len(L1) == 1: + seq1 = iter(L1[0]) + else: + seq1 = _merge_sorted_binary_key(L1, key) + L2 = seqs[mid:] + if len(L2) == 1: + seq2 = iter(L2[0]) + else: + seq2 = _merge_sorted_binary_key(L2, key) + + try: + val2 = next(seq2) + except StopIteration: + for val1 in seq1: + yield val1 + return + key2 = key(val2) + + for val1 in seq1: + key1 = key(val1) + if key2 < key1: + yield val2 + for val2 in seq2: + key2 = key(val2) + if key2 < key1: + yield val2 + else: + yield val1 + break + else: + break + else: + yield val1 + else: + yield val2 + for val2 in seq2: + yield val2 + return + yield val1 + for val1 in seq1: + yield val1 + + +def interleave(seqs): + """ Interleave a sequence of sequences + + >>> list(interleave([[1, 2], [3, 4]])) + [1, 3, 2, 4] + + >>> ''.join(interleave(('ABC', 'XY'))) + 'AXBYC' + + Both the individual sequences and the sequence of sequences may be infinite + + Returns a lazy iterator + """ + iters = itertools.cycle(map(iter, seqs)) + while True: + try: + for itr in iters: + yield next(itr) + return + except StopIteration: + predicate = partial(operator.is_not, itr) + iters = itertools.cycle(itertools.takewhile(predicate, iters)) + + +def unique(seq, key=None): + """ Return only unique elements of a sequence + + >>> tuple(unique((1, 2, 3))) + (1, 2, 3) + >>> tuple(unique((1, 2, 1, 3))) + (1, 2, 3) + + Uniqueness can be defined by key keyword + + >>> tuple(unique(['cat', 'mouse', 'dog', 'hen'], key=len)) + ('cat', 'mouse') + """ + seen = set() + seen_add = seen.add + if key is None: + for item in seq: + if item not in seen: + seen_add(item) + yield item + else: # calculate key + for item in seq: + val = key(item) + if val not in seen: + seen_add(val) + yield item + + +def isiterable(x): + """ Is x iterable? + + >>> isiterable([1, 2, 3]) + True + >>> isiterable('abc') + True + >>> isiterable(5) + False + """ + try: + iter(x) + return True + except TypeError: + return False + + +def isdistinct(seq): + """ All values in sequence are distinct + + >>> isdistinct([1, 2, 3]) + True + >>> isdistinct([1, 2, 1]) + False + + >>> isdistinct("Hello") + False + >>> isdistinct("World") + True + """ + if iter(seq) is seq: + seen = set() + seen_add = seen.add + for item in seq: + if item in seen: + return False + seen_add(item) + return True + else: + return len(seq) == len(set(seq)) + + +def take(n, seq): + """ The first n elements of a sequence + + >>> list(take(2, [10, 20, 30, 40, 50])) + [10, 20] + + See Also: + drop + tail + """ + return itertools.islice(seq, n) + + +def tail(n, seq): + """ The last n elements of a sequence + + >>> tail(2, [10, 20, 30, 40, 50]) + [40, 50] + + See Also: + drop + take + """ + try: + return seq[-n:] + except (TypeError, KeyError): + return tuple(collections.deque(seq, n)) + + +def drop(n, seq): + """ The sequence following the first n elements + + >>> list(drop(2, [10, 20, 30, 40, 50])) + [30, 40, 50] + + See Also: + take + tail + """ + return itertools.islice(seq, n, None) + + +def take_nth(n, seq): + """ Every nth item in seq + + >>> list(take_nth(2, [10, 20, 30, 40, 50])) + [10, 30, 50] + """ + return itertools.islice(seq, 0, None, n) + + +def first(seq): + """ The first element in a sequence + + >>> first('ABC') + 'A' + """ + return next(iter(seq)) + + +def second(seq): + """ The second element in a sequence + + >>> second('ABC') + 'B' + """ + seq = iter(seq) + next(seq) + return next(seq) + + +def nth(n, seq): + """ The nth element in a sequence + + >>> nth(1, 'ABC') + 'B' + """ + if isinstance(seq, (tuple, list, Sequence)): + return seq[n] + else: + return next(itertools.islice(seq, n, None)) + + +def last(seq): + """ The last element in a sequence + + >>> last('ABC') + 'C' + """ + return tail(1, seq)[0] + + +rest = partial(drop, 1) + + +def _get(ind, seq, default): + try: + return seq[ind] + except (KeyError, IndexError): + return default + + +def get(ind, seq, default=no_default): + """ Get element in a sequence or dict + + Provides standard indexing + + >>> get(1, 'ABC') # Same as 'ABC'[1] + 'B' + + Pass a list to get multiple values + + >>> get([1, 2], 'ABC') # ('ABC'[1], 'ABC'[2]) + ('B', 'C') + + Works on any value that supports indexing/getitem + For example here we see that it works with dictionaries + + >>> phonebook = {'Alice': '555-1234', + ... 'Bob': '555-5678', + ... 'Charlie':'555-9999'} + >>> get('Alice', phonebook) + '555-1234' + + >>> get(['Alice', 'Bob'], phonebook) + ('555-1234', '555-5678') + + Provide a default for missing values + + >>> get(['Alice', 'Dennis'], phonebook, None) + ('555-1234', None) + + See Also: + pluck + """ + try: + return seq[ind] + except TypeError: # `ind` may be a list + if isinstance(ind, list): + if default == no_default: + if len(ind) > 1: + return operator.itemgetter(*ind)(seq) + elif ind: + return seq[ind[0]], + else: + return () + else: + return tuple(_get(i, seq, default) for i in ind) + elif default != no_default: + return default + else: + raise + except (KeyError, IndexError): # we know `ind` is not a list + if default == no_default: + raise + else: + return default + + +def concat(seqs): + """ Concatenate zero or more iterables, any of which may be infinite. + + An infinite sequence will prevent the rest of the arguments from + being included. + + We use chain.from_iterable rather than ``chain(*seqs)`` so that seqs + can be a generator. + + >>> list(concat([[], [1], [2, 3]])) + [1, 2, 3] + + See also: + itertools.chain.from_iterable equivalent + """ + return itertools.chain.from_iterable(seqs) + + +def concatv(*seqs): + """ Variadic version of concat + + >>> list(concatv([], ["a"], ["b", "c"])) + ['a', 'b', 'c'] + + See also: + itertools.chain + """ + return concat(seqs) + + +def mapcat(func, seqs): + """ Apply func to each sequence in seqs, concatenating results. + + >>> list(mapcat(lambda s: [c.upper() for c in s], + ... [["a", "b"], ["c", "d", "e"]])) + ['A', 'B', 'C', 'D', 'E'] + """ + return concat(map(func, seqs)) + + +def cons(el, seq): + """ Add el to beginning of (possibly infinite) sequence seq. + + >>> list(cons(1, [2, 3])) + [1, 2, 3] + """ + return itertools.chain([el], seq) + + +def interpose(el, seq): + """ Introduce element between each pair of elements in seq + + >>> list(interpose("a", [1, 2, 3])) + [1, 'a', 2, 'a', 3] + """ + inposed = concat(zip(itertools.repeat(el), seq)) + next(inposed) + return inposed + + +def frequencies(seq): + """ Find number of occurrences of each value in seq + + >>> frequencies(['cat', 'cat', 'ox', 'pig', 'pig', 'cat']) #doctest: +SKIP + {'cat': 3, 'ox': 1, 'pig': 2} + + See Also: + countby + groupby + """ + d = collections.defaultdict(int) + for item in seq: + d[item] += 1 + return dict(d) + + +def reduceby(key, binop, seq, init=no_default): + """ Perform a simultaneous groupby and reduction + + The computation: + + >>> result = reduceby(key, binop, seq, init) # doctest: +SKIP + + is equivalent to the following: + + >>> def reduction(group): # doctest: +SKIP + ... return reduce(binop, group, init) # doctest: +SKIP + + >>> groups = groupby(key, seq) # doctest: +SKIP + >>> result = valmap(reduction, groups) # doctest: +SKIP + + But the former does not build the intermediate groups, allowing it to + operate in much less space. This makes it suitable for larger datasets + that do not fit comfortably in memory + + The ``init`` keyword argument is the default initialization of the + reduction. This can be either a constant value like ``0`` or a callable + like ``lambda : 0`` as might be used in ``defaultdict``. + + Simple Examples + --------------- + + >>> from operator import add, mul + >>> iseven = lambda x: x % 2 == 0 + + >>> data = [1, 2, 3, 4, 5] + + >>> reduceby(iseven, add, data) # doctest: +SKIP + {False: 9, True: 6} + + >>> reduceby(iseven, mul, data) # doctest: +SKIP + {False: 15, True: 8} + + Complex Example + --------------- + + >>> projects = [{'name': 'build roads', 'state': 'CA', 'cost': 1000000}, + ... {'name': 'fight crime', 'state': 'IL', 'cost': 100000}, + ... {'name': 'help farmers', 'state': 'IL', 'cost': 2000000}, + ... {'name': 'help farmers', 'state': 'CA', 'cost': 200000}] + + >>> reduceby('state', # doctest: +SKIP + ... lambda acc, x: acc + x['cost'], + ... projects, 0) + {'CA': 1200000, 'IL': 2100000} + + Example Using ``init`` + ---------------------- + + >>> def set_add(s, i): + ... s.add(i) + ... return s + + >>> reduceby(iseven, set_add, [1, 2, 3, 4, 1, 2, 3], set) # doctest: +SKIP + {True: set([2, 4]), + False: set([1, 3])} + """ + is_no_default = init == no_default + if not is_no_default and not callable(init): + _init = init + init = lambda: _init + if not callable(key): + key = getter(key) + d = {} + for item in seq: + k = key(item) + if k not in d: + if is_no_default: + d[k] = item + continue + else: + d[k] = init() + d[k] = binop(d[k], item) + return d + + +def iterate(func, x): + """ Repeatedly apply a function func onto an original input + + Yields x, then func(x), then func(func(x)), then func(func(func(x))), etc.. + + >>> def inc(x): return x + 1 + >>> counter = iterate(inc, 0) + >>> next(counter) + 0 + >>> next(counter) + 1 + >>> next(counter) + 2 + + >>> double = lambda x: x * 2 + >>> powers_of_two = iterate(double, 1) + >>> next(powers_of_two) + 1 + >>> next(powers_of_two) + 2 + >>> next(powers_of_two) + 4 + >>> next(powers_of_two) + 8 + """ + while True: + yield x + x = func(x) + + +def sliding_window(n, seq): + """ A sequence of overlapping subsequences + + >>> list(sliding_window(2, [1, 2, 3, 4])) + [(1, 2), (2, 3), (3, 4)] + + This function creates a sliding window suitable for transformations like + sliding means / smoothing + + >>> mean = lambda seq: float(sum(seq)) / len(seq) + >>> list(map(mean, sliding_window(2, [1, 2, 3, 4]))) + [1.5, 2.5, 3.5] + """ + return zip(*(collections.deque(itertools.islice(it, i), 0) or it + for i, it in enumerate(itertools.tee(seq, n)))) + + +no_pad = '__no__pad__' + + +def partition(n, seq, pad=no_pad): + """ Partition sequence into tuples of length n + + >>> list(partition(2, [1, 2, 3, 4])) + [(1, 2), (3, 4)] + + If the length of ``seq`` is not evenly divisible by ``n``, the final tuple + is dropped if ``pad`` is not specified, or filled to length ``n`` by pad: + + >>> list(partition(2, [1, 2, 3, 4, 5])) + [(1, 2), (3, 4)] + + >>> list(partition(2, [1, 2, 3, 4, 5], pad=None)) + [(1, 2), (3, 4), (5, None)] + + See Also: + partition_all + """ + args = [iter(seq)] * n + if pad is no_pad: + return zip(*args) + else: + return zip_longest(*args, fillvalue=pad) + + +def partition_all(n, seq): + """ Partition all elements of sequence into tuples of length at most n + + The final tuple may be shorter to accommodate extra elements. + + >>> list(partition_all(2, [1, 2, 3, 4])) + [(1, 2), (3, 4)] + + >>> list(partition_all(2, [1, 2, 3, 4, 5])) + [(1, 2), (3, 4), (5,)] + + See Also: + partition + """ + args = [iter(seq)] * n + it = zip_longest(*args, fillvalue=no_pad) + try: + prev = next(it) + except StopIteration: + return + for item in it: + yield prev + prev = item + if prev[-1] is no_pad: + try: + # If seq defines __len__, then + # we can quickly calculate where no_pad starts + yield prev[:len(seq) % n] + except TypeError: + # Get first index of no_pad without using .index() + # https://github.com/pytoolz/toolz/issues/387 + # Binary search from CPython's bisect module, + # modified for identity testing. + lo, hi = 0, n + while lo < hi: + mid = (lo + hi) // 2 + if prev[mid] is no_pad: + hi = mid + else: + lo = mid + 1 + yield prev[:lo] + else: + yield prev + + +def count(seq): + """ Count the number of items in seq + + Like the builtin ``len`` but works on lazy sequences. + + Not to be confused with ``itertools.count`` + + See also: + len + """ + if hasattr(seq, '__len__'): + return len(seq) + return sum(1 for i in seq) + + +def pluck(ind, seqs, default=no_default): + """ plucks an element or several elements from each item in a sequence. + + ``pluck`` maps ``itertoolz.get`` over a sequence and returns one or more + elements of each item in the sequence. + + This is equivalent to running `map(curried.get(ind), seqs)` + + ``ind`` can be either a single string/index or a list of strings/indices. + ``seqs`` should be sequence containing sequences or dicts. + + e.g. + + >>> data = [{'id': 1, 'name': 'Cheese'}, {'id': 2, 'name': 'Pies'}] + >>> list(pluck('name', data)) + ['Cheese', 'Pies'] + >>> list(pluck([0, 1], [[1, 2, 3], [4, 5, 7]])) + [(1, 2), (4, 5)] + + See Also: + get + map + """ + if default == no_default: + get = getter(ind) + return map(get, seqs) + elif isinstance(ind, list): + return (tuple(_get(item, seq, default) for item in ind) + for seq in seqs) + return (_get(ind, seq, default) for seq in seqs) + + +def getter(index): + if isinstance(index, list): + if len(index) == 1: + index = index[0] + return lambda x: (x[index],) + elif index: + return operator.itemgetter(*index) + else: + return lambda x: () + else: + return operator.itemgetter(index) + + +def join(leftkey, leftseq, rightkey, rightseq, + left_default=no_default, right_default=no_default): + """ Join two sequences on common attributes + + This is a semi-streaming operation. The LEFT sequence is fully evaluated + and placed into memory. The RIGHT sequence is evaluated lazily and so can + be arbitrarily large. + (Note: If right_default is defined, then unique keys of rightseq + will also be stored in memory.) + + >>> friends = [('Alice', 'Edith'), + ... ('Alice', 'Zhao'), + ... ('Edith', 'Alice'), + ... ('Zhao', 'Alice'), + ... ('Zhao', 'Edith')] + + >>> cities = [('Alice', 'NYC'), + ... ('Alice', 'Chicago'), + ... ('Dan', 'Sydney'), + ... ('Edith', 'Paris'), + ... ('Edith', 'Berlin'), + ... ('Zhao', 'Shanghai')] + + >>> # Vacation opportunities + >>> # In what cities do people have friends? + >>> result = join(second, friends, + ... first, cities) + >>> for ((a, b), (c, d)) in sorted(unique(result)): + ... print((a, d)) + ('Alice', 'Berlin') + ('Alice', 'Paris') + ('Alice', 'Shanghai') + ('Edith', 'Chicago') + ('Edith', 'NYC') + ('Zhao', 'Chicago') + ('Zhao', 'NYC') + ('Zhao', 'Berlin') + ('Zhao', 'Paris') + + Specify outer joins with keyword arguments ``left_default`` and/or + ``right_default``. Here is a full outer join in which unmatched elements + are paired with None. + + >>> identity = lambda x: x + >>> list(join(identity, [1, 2, 3], + ... identity, [2, 3, 4], + ... left_default=None, right_default=None)) + [(2, 2), (3, 3), (None, 4), (1, None)] + + Usually the key arguments are callables to be applied to the sequences. If + the keys are not obviously callable then it is assumed that indexing was + intended, e.g. the following is a legal change. + The join is implemented as a hash join and the keys of leftseq must be + hashable. Additionally, if right_default is defined, then keys of rightseq + must also be hashable. + + >>> # result = join(second, friends, first, cities) + >>> result = join(1, friends, 0, cities) # doctest: +SKIP + """ + if not callable(leftkey): + leftkey = getter(leftkey) + if not callable(rightkey): + rightkey = getter(rightkey) + + d = groupby(leftkey, leftseq) + + if left_default == no_default and right_default == no_default: + # Inner Join + for item in rightseq: + key = rightkey(item) + if key in d: + for left_match in d[key]: + yield (left_match, item) + elif left_default != no_default and right_default == no_default: + # Right Join + for item in rightseq: + key = rightkey(item) + if key in d: + for left_match in d[key]: + yield (left_match, item) + else: + yield (left_default, item) + elif right_default != no_default: + seen_keys = set() + seen = seen_keys.add + + if left_default == no_default: + # Left Join + for item in rightseq: + key = rightkey(item) + seen(key) + if key in d: + for left_match in d[key]: + yield (left_match, item) + else: + # Full Join + for item in rightseq: + key = rightkey(item) + seen(key) + if key in d: + for left_match in d[key]: + yield (left_match, item) + else: + yield (left_default, item) + + for key, matches in d.items(): + if key not in seen_keys: + for match in matches: + yield (match, right_default) + + +def diff(*seqs, **kwargs): + """ Return those items that differ between sequences + + >>> list(diff([1, 2, 3], [1, 2, 10, 100])) + [(3, 10)] + + Shorter sequences may be padded with a ``default`` value: + + >>> list(diff([1, 2, 3], [1, 2, 10, 100], default=None)) + [(3, 10), (None, 100)] + + A ``key`` function may also be applied to each item to use during + comparisons: + + >>> list(diff(['apples', 'bananas'], ['Apples', 'Oranges'], key=str.lower)) + [('bananas', 'Oranges')] + """ + N = len(seqs) + if N == 1 and isinstance(seqs[0], list): + seqs = seqs[0] + N = len(seqs) + if N < 2: + raise TypeError('Too few sequences given (min 2 required)') + default = kwargs.get('default', no_default) + if default == no_default: + iters = zip(*seqs) + else: + iters = zip_longest(*seqs, fillvalue=default) + key = kwargs.get('key', None) + if key is None: + for items in iters: + if items.count(items[0]) != N: + yield items + else: + for items in iters: + vals = tuple(map(key, items)) + if vals.count(vals[0]) != N: + yield items + + +def topk(k, seq, key=None): + """ Find the k largest elements of a sequence + + Operates lazily in ``n*log(k)`` time + + >>> topk(2, [1, 100, 10, 1000]) + (1000, 100) + + Use a key function to change sorted order + + >>> topk(2, ['Alice', 'Bob', 'Charlie', 'Dan'], key=len) + ('Charlie', 'Alice') + + See also: + heapq.nlargest + """ + if key is not None and not callable(key): + key = getter(key) + return tuple(heapq.nlargest(k, seq, key=key)) + + +def peek(seq): + """ Retrieve the next element of a sequence + + Returns the first element and an iterable equivalent to the original + sequence, still having the element retrieved. + + >>> seq = [0, 1, 2, 3, 4] + >>> first, seq = peek(seq) + >>> first + 0 + >>> list(seq) + [0, 1, 2, 3, 4] + """ + iterator = iter(seq) + item = next(iterator) + return item, itertools.chain((item,), iterator) + + +def peekn(n, seq): + """ Retrieve the next n elements of a sequence + + Returns a tuple of the first n elements and an iterable equivalent + to the original, still having the elements retrieved. + + >>> seq = [0, 1, 2, 3, 4] + >>> first_two, seq = peekn(2, seq) + >>> first_two + (0, 1) + >>> list(seq) + [0, 1, 2, 3, 4] + """ + iterator = iter(seq) + peeked = tuple(take(n, iterator)) + return peeked, itertools.chain(iter(peeked), iterator) + + +def random_sample(prob, seq, random_state=None): + """ Return elements from a sequence with probability of prob + + Returns a lazy iterator of random items from seq. + + ``random_sample`` considers each item independently and without + replacement. See below how the first time it returned 13 items and the + next time it returned 6 items. + + >>> seq = list(range(100)) + >>> list(random_sample(0.1, seq)) # doctest: +SKIP + [6, 9, 19, 35, 45, 50, 58, 62, 68, 72, 78, 86, 95] + >>> list(random_sample(0.1, seq)) # doctest: +SKIP + [6, 44, 54, 61, 69, 94] + + Providing an integer seed for ``random_state`` will result in + deterministic sampling. Given the same seed it will return the same sample + every time. + + >>> list(random_sample(0.1, seq, random_state=2016)) + [7, 9, 19, 25, 30, 32, 34, 48, 59, 60, 81, 98] + >>> list(random_sample(0.1, seq, random_state=2016)) + [7, 9, 19, 25, 30, 32, 34, 48, 59, 60, 81, 98] + + ``random_state`` can also be any object with a method ``random`` that + returns floats between 0.0 and 1.0 (exclusive). + + >>> from random import Random + >>> randobj = Random(2016) + >>> list(random_sample(0.1, seq, random_state=randobj)) + [7, 9, 19, 25, 30, 32, 34, 48, 59, 60, 81, 98] + """ + if not hasattr(random_state, 'random'): + from random import Random + + random_state = Random(random_state) + return filter(lambda _: random_state.random() < prob, seq) diff --git a/ccxt/static_dependencies/toolz/recipes.py b/ccxt/static_dependencies/toolz/recipes.py new file mode 100644 index 0000000..89de88d --- /dev/null +++ b/ccxt/static_dependencies/toolz/recipes.py @@ -0,0 +1,46 @@ +import itertools +from .itertoolz import frequencies, pluck, getter + + +__all__ = ('countby', 'partitionby') + + +def countby(key, seq): + """ Count elements of a collection by a key function + + >>> countby(len, ['cat', 'mouse', 'dog']) + {3: 2, 5: 1} + + >>> def iseven(x): return x % 2 == 0 + >>> countby(iseven, [1, 2, 3]) # doctest:+SKIP + {True: 1, False: 2} + + See Also: + groupby + """ + if not callable(key): + key = getter(key) + return frequencies(map(key, seq)) + + +def partitionby(func, seq): + """ Partition a sequence according to a function + + Partition `s` into a sequence of lists such that, when traversing + `s`, every time the output of `func` changes a new list is started + and that and subsequent items are collected into that list. + + >>> is_space = lambda c: c == " " + >>> list(partitionby(is_space, "I have space")) + [('I',), (' ',), ('h', 'a', 'v', 'e'), (' ',), ('s', 'p', 'a', 'c', 'e')] + + >>> is_large = lambda x: x > 10 + >>> list(partitionby(is_large, [1, 2, 1, 99, 88, 33, 99, -1, 5])) + [(1, 2, 1), (99, 88, 33, 99), (-1, 5)] + + See also: + partition + groupby + itertools.groupby + """ + return map(tuple, pluck(1, itertools.groupby(seq, key=func))) diff --git a/ccxt/static_dependencies/toolz/utils.py b/ccxt/static_dependencies/toolz/utils.py new file mode 100644 index 0000000..1002c46 --- /dev/null +++ b/ccxt/static_dependencies/toolz/utils.py @@ -0,0 +1,9 @@ +def raises(err, lamda): + try: + lamda() + return False + except err: + return True + + +no_default = '__no__default__' diff --git a/ccxt/static_dependencies/typing_inspect/__init__.py b/ccxt/static_dependencies/typing_inspect/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ccxt/static_dependencies/typing_inspect/typing_inspect.py b/ccxt/static_dependencies/typing_inspect/typing_inspect.py new file mode 100644 index 0000000..0345066 --- /dev/null +++ b/ccxt/static_dependencies/typing_inspect/typing_inspect.py @@ -0,0 +1,851 @@ +"""Defines experimental API for runtime inspection of types defined +in the standard "typing" module. + +Example usage:: + from typing_inspect import is_generic_type +""" + +# NOTE: This module must support Python 2.7 in addition to Python 3.x + +import sys +import types +import typing +import typing_extensions + +from mypy_extensions import _TypedDictMeta as _TypedDictMeta_Mypy + +# See comments in typing_extensions source on why the switch is at 3.9.2 +if (3, 4, 0) <= sys.version_info[:3] < (3, 9, 2): + from typing_extensions import _TypedDictMeta as _TypedDictMeta_TE +elif sys.version_info[:3] >= (3, 9, 2): + # Situation with typing_extensions.TypedDict is complicated. + # Use the one defined in typing_extentions, and if there is none, + # fall back to typing. + try: + from typing_extensions import _TypedDictMeta as _TypedDictMeta_TE + except ImportError: + from typing import _TypedDictMeta as _TypedDictMeta_TE +else: + # typing_extensions.TypedDict is a re-export from typing. + from typing import TypedDict + _TypedDictMeta_TE = type(TypedDict) + +NEW_TYPING = sys.version_info[:3] >= (3, 7, 0) # PEP 560 +if NEW_TYPING: + import collections.abc + +WITH_FINAL = True +WITH_LITERAL = True +WITH_CLASSVAR = True +WITH_NEWTYPE = True +LEGACY_TYPING = False + +if NEW_TYPING: + from typing import ( + Generic, Callable, Union, TypeVar, ClassVar, Tuple, _GenericAlias, + ForwardRef, NewType, + ) + from typing_extensions import Final, Literal + if sys.version_info[:3] >= (3, 9, 0): + from typing import _SpecialGenericAlias + typingGenericAlias = (_GenericAlias, _SpecialGenericAlias, types.GenericAlias) + else: + typingGenericAlias = (_GenericAlias,) +else: + from typing import ( + Callable, CallableMeta, Union, Tuple, TupleMeta, TypeVar, GenericMeta, + _ForwardRef, + ) + try: + from typing import _Union, _ClassVar + except ImportError: + # support for very old typing module <=3.5.3 + _Union = type(Union) + WITH_CLASSVAR = False + LEGACY_TYPING = True + + try: # python 3.6 + from typing_extensions import _Final + except ImportError: # python 2.7 + try: + from typing import _Final + except ImportError: + WITH_FINAL = False + + try: # python 3.6 + from typing_extensions import Literal + except ImportError: # python 2.7 + try: + from typing import Literal + except ImportError: + WITH_LITERAL = False + + try: # python < 3.5.2 + from typing_extensions import NewType + except ImportError: + try: + from typing import NewType + except ImportError: + WITH_NEWTYPE = False + + +def _gorg(cls): + """This function exists for compatibility with old typing versions.""" + assert isinstance(cls, GenericMeta) + if hasattr(cls, '_gorg'): + return cls._gorg + while cls.__origin__ is not None: + cls = cls.__origin__ + return cls + + +def is_generic_type(tp): + """Test if the given type is a generic type. This includes Generic itself, but + excludes special typing constructs such as Union, Tuple, Callable, ClassVar. + Examples:: + + is_generic_type(int) == False + is_generic_type(Union[int, str]) == False + is_generic_type(Union[int, T]) == False + is_generic_type(ClassVar[List[int]]) == False + is_generic_type(Callable[..., T]) == False + + is_generic_type(Generic) == True + is_generic_type(Generic[T]) == True + is_generic_type(Iterable[int]) == True + is_generic_type(Mapping) == True + is_generic_type(MutableMapping[T, List[int]]) == True + is_generic_type(Sequence[Union[str, bytes]]) == True + """ + if NEW_TYPING: + return (isinstance(tp, type) and issubclass(tp, Generic) or + isinstance(tp, typingGenericAlias) and + tp.__origin__ not in (Union, tuple, ClassVar, collections.abc.Callable)) + return (isinstance(tp, GenericMeta) and not + isinstance(tp, (CallableMeta, TupleMeta))) + + +def is_callable_type(tp): + """Test if the type is a generic callable type, including subclasses + excluding non-generic types and callables. + Examples:: + + is_callable_type(int) == False + is_callable_type(type) == False + is_callable_type(Callable) == True + is_callable_type(Callable[..., int]) == True + is_callable_type(Callable[[int, int], Iterable[str]]) == True + class MyClass(Callable[[int], int]): + ... + is_callable_type(MyClass) == True + + For more general tests use callable(), for more precise test + (excluding subclasses) use:: + + get_origin(tp) is collections.abc.Callable # Callable prior to Python 3.7 + """ + if NEW_TYPING: + return (tp is Callable or isinstance(tp, typingGenericAlias) and + tp.__origin__ is collections.abc.Callable or + isinstance(tp, type) and issubclass(tp, Generic) and + issubclass(tp, collections.abc.Callable)) + return type(tp) is CallableMeta + + +def is_tuple_type(tp): + """Test if the type is a generic tuple type, including subclasses excluding + non-generic classes. + Examples:: + + is_tuple_type(int) == False + is_tuple_type(tuple) == False + is_tuple_type(Tuple) == True + is_tuple_type(Tuple[str, int]) == True + class MyClass(Tuple[str, int]): + ... + is_tuple_type(MyClass) == True + + For more general tests use issubclass(..., tuple), for more precise test + (excluding subclasses) use:: + + get_origin(tp) is tuple # Tuple prior to Python 3.7 + """ + if NEW_TYPING: + return (tp is Tuple or isinstance(tp, typingGenericAlias) and + tp.__origin__ is tuple or + isinstance(tp, type) and issubclass(tp, Generic) and + issubclass(tp, tuple)) + return type(tp) is TupleMeta + + +def is_optional_type(tp): + """Test if the type is type(None), or is a direct union with it, such as Optional[T]. + + NOTE: this method inspects nested `Union` arguments but not `TypeVar` definition + bounds and constraints. So it will return `False` if + - `tp` is a `TypeVar` bound, or constrained to, an optional type + - `tp` is a `Union` to a `TypeVar` bound or constrained to an optional type, + - `tp` refers to a *nested* `Union` containing an optional type or one of the above. + + Users wishing to check for optionality in types relying on type variables might wish + to use this method in combination with `get_constraints` and `get_bound` + """ + + if tp is type(None): # noqa + return True + elif is_union_type(tp): + return any(is_optional_type(tt) for tt in get_args(tp, evaluate=True)) + else: + return False + + +def is_final_type(tp): + """Test if the type is a final type. Examples:: + + is_final_type(int) == False + is_final_type(Final) == True + is_final_type(Final[int]) == True + """ + if NEW_TYPING: + return (tp is Final or + isinstance(tp, typingGenericAlias) and tp.__origin__ is Final) + return WITH_FINAL and type(tp) is _Final + + +try: + MaybeUnionType = types.UnionType +except AttributeError: + MaybeUnionType = None + + +def is_union_type(tp): + """Test if the type is a union type. Examples:: + + is_union_type(int) == False + is_union_type(Union) == True + is_union_type(Union[int, int]) == False + is_union_type(Union[T, int]) == True + is_union_type(int | int) == False + is_union_type(T | int) == True + """ + if NEW_TYPING: + return (tp is Union or + (isinstance(tp, typingGenericAlias) and tp.__origin__ is Union) or + (MaybeUnionType and isinstance(tp, MaybeUnionType))) + return type(tp) is _Union + + +LITERALS = {Literal} +if hasattr(typing, "Literal"): + LITERALS.add(typing.Literal) + + +def is_literal_type(tp): + if NEW_TYPING: + return (tp in LITERALS or + isinstance(tp, typingGenericAlias) and tp.__origin__ in LITERALS) + return WITH_LITERAL and type(tp) is type(Literal) + + +def is_typevar(tp): + """Test if the type represents a type variable. Examples:: + + is_typevar(int) == False + is_typevar(T) == True + is_typevar(Union[T, int]) == False + """ + + return type(tp) is TypeVar + + +def is_classvar(tp): + """Test if the type represents a class variable. Examples:: + + is_classvar(int) == False + is_classvar(ClassVar) == True + is_classvar(ClassVar[int]) == True + is_classvar(ClassVar[List[T]]) == True + """ + if NEW_TYPING: + return (tp is ClassVar or + isinstance(tp, typingGenericAlias) and tp.__origin__ is ClassVar) + elif WITH_CLASSVAR: + return type(tp) is _ClassVar + else: + return False + + +def is_new_type(tp): + """Tests if the type represents a distinct type. Examples:: + + is_new_type(int) == False + is_new_type(NewType) == True + is_new_type(NewType('Age', int)) == True + is_new_type(NewType('Scores', List[Dict[str, float]])) == True + """ + if not WITH_NEWTYPE: + return False + elif sys.version_info[:3] >= (3, 10, 0) and sys.version_info.releaselevel != 'beta': + return (tp in (NewType, typing_extensions.NewType) or + isinstance(tp, (NewType, typing_extensions.NewType))) + elif sys.version_info[:3] >= (3, 0, 0): + try: + res = isinstance(tp, typing_extensions.NewType) + except TypeError: + pass + else: + if res: + return res + return (tp in (NewType, typing_extensions.NewType) or + (getattr(tp, '__supertype__', None) is not None and + getattr(tp, '__qualname__', '') == 'NewType..new_type' and + tp.__module__ in ('typing', 'typing_extensions'))) + else: # python 2 + # __qualname__ is not available in python 2, so we simplify the test here + return (tp is NewType or + (getattr(tp, '__supertype__', None) is not None and + tp.__module__ in ('typing', 'typing_extensions'))) + + +def is_forward_ref(tp): + """Tests if the type is a :class:`typing.ForwardRef`. Examples:: + + u = Union["Milk", Way] + args = get_args(u) + is_forward_ref(args[0]) == True + is_forward_ref(args[1]) == False + """ + if not NEW_TYPING: + return isinstance(tp, _ForwardRef) + return isinstance(tp, ForwardRef) + + +def get_last_origin(tp): + """Get the last base of (multiply) subscripted type. Supports generic types, + Union, Callable, and Tuple. Returns None for unsupported types. + Examples:: + + get_last_origin(int) == None + get_last_origin(ClassVar[int]) == None + get_last_origin(Generic[T]) == Generic + get_last_origin(Union[T, int][str]) == Union[T, int] + get_last_origin(List[Tuple[T, T]][int]) == List[Tuple[T, T]] + get_last_origin(List) == List + """ + if NEW_TYPING: + raise ValueError('This function is only supported in Python 3.6,' + ' use get_origin instead') + sentinel = object() + origin = getattr(tp, '__origin__', sentinel) + if origin is sentinel: + return None + if origin is None: + return tp + return origin + + +def get_origin(tp): + """Get the unsubscripted version of a type. Supports generic types, Union, + Callable, and Tuple. Returns None for unsupported types. Examples:: + + get_origin(int) == None + get_origin(ClassVar[int]) == None + get_origin(Generic) == Generic + get_origin(Generic[T]) == Generic + get_origin(Union[T, int]) == Union + get_origin(List[Tuple[T, T]][int]) == list # List prior to Python 3.7 + """ + if NEW_TYPING: + if isinstance(tp, typingGenericAlias): + return tp.__origin__ if tp.__origin__ is not ClassVar else None + if tp is Generic: + return Generic + return None + if isinstance(tp, GenericMeta): + return _gorg(tp) + if is_union_type(tp): + return Union + if is_tuple_type(tp): + return Tuple + if is_literal_type(tp): + if NEW_TYPING: + return tp.__origin__ or tp + return Literal + + return None + + +def get_parameters(tp): + """Return type parameters of a parameterizable type as a tuple + in lexicographic order. Parameterizable types are generic types, + unions, tuple types and callable types. Examples:: + + get_parameters(int) == () + get_parameters(Generic) == () + get_parameters(Union) == () + get_parameters(List[int]) == () + + get_parameters(Generic[T]) == (T,) + get_parameters(Tuple[List[T], List[S_co]]) == (T, S_co) + get_parameters(Union[S_co, Tuple[T, T]][int, U]) == (U,) + get_parameters(Mapping[T, Tuple[S_co, T]]) == (T, S_co) + """ + if LEGACY_TYPING: + # python <= 3.5.2 + if is_union_type(tp): + params = [] + for arg in (tp.__union_params__ if tp.__union_params__ is not None else ()): + params += get_parameters(arg) + return tuple(params) + elif is_tuple_type(tp): + params = [] + for arg in (tp.__tuple_params__ if tp.__tuple_params__ is not None else ()): + params += get_parameters(arg) + return tuple(params) + elif is_generic_type(tp): + params = [] + base_params = tp.__parameters__ + if base_params is None: + return () + for bp_ in base_params: + for bp in (get_args(bp_) if is_tuple_type(bp_) else (bp_,)): + if _has_type_var(bp) and not isinstance(bp, TypeVar): + raise TypeError( + "Cannot inherit from a generic class " + "parameterized with " + "non-type-variable %s" % bp) + if params is None: + params = [] + if bp not in params: + params.append(bp) + if params is not None: + return tuple(params) + else: + return () + else: + return () + elif NEW_TYPING: + if ( + ( + isinstance(tp, typingGenericAlias) and + hasattr(tp, '__parameters__') + ) or + isinstance(tp, type) and issubclass(tp, Generic) and + tp is not Generic): + return tp.__parameters__ + else: + return () + elif ( + is_generic_type(tp) or is_union_type(tp) or + is_callable_type(tp) or is_tuple_type(tp) + ): + return tp.__parameters__ if tp.__parameters__ is not None else () + else: + return () + + +def get_last_args(tp): + """Get last arguments of (multiply) subscripted type. + Parameters for Callable are flattened. Examples:: + + get_last_args(int) == () + get_last_args(Union) == () + get_last_args(ClassVar[int]) == (int,) + get_last_args(Union[T, int]) == (T, int) + get_last_args(Iterable[Tuple[T, S]][int, T]) == (int, T) + get_last_args(Callable[[T], int]) == (T, int) + get_last_args(Callable[[], int]) == (int,) + """ + if NEW_TYPING: + raise ValueError('This function is only supported in Python 3.6,' + ' use get_args instead') + elif is_classvar(tp): + return (tp.__type__,) if tp.__type__ is not None else () + elif is_generic_type(tp): + try: + if tp.__args__ is not None and len(tp.__args__) > 0: + return tp.__args__ + except AttributeError: + # python 3.5.1 + pass + return tp.__parameters__ if tp.__parameters__ is not None else () + elif is_union_type(tp): + try: + return tp.__args__ if tp.__args__ is not None else () + except AttributeError: + # python 3.5.2 + return tp.__union_params__ if tp.__union_params__ is not None else () + elif is_callable_type(tp): + return tp.__args__ if tp.__args__ is not None else () + elif is_tuple_type(tp): + try: + return tp.__args__ if tp.__args__ is not None else () + except AttributeError: + # python 3.5.2 + return tp.__tuple_params__ if tp.__tuple_params__ is not None else () + else: + return () + + +def _eval_args(args): + """Internal helper for get_args.""" + res = [] + for arg in args: + if not isinstance(arg, tuple): + res.append(arg) + elif is_callable_type(arg[0]): + callable_args = _eval_args(arg[1:]) + if len(arg) == 2: + res.append(Callable[[], callable_args[0]]) + elif arg[1] is Ellipsis: + res.append(Callable[..., callable_args[1]]) + else: + res.append(Callable[list(callable_args[:-1]), callable_args[-1]]) + else: + res.append(type(arg[0]).__getitem__(arg[0], _eval_args(arg[1:]))) + return tuple(res) + + +def get_args(tp, evaluate=None): + """Get type arguments with all substitutions performed. For unions, + basic simplifications used by Union constructor are performed. + On versions prior to 3.7 if `evaluate` is False (default), + report result as nested tuple, this matches + the internal representation of types. If `evaluate` is True + (or if Python version is 3.7 or greater), then all + type parameters are applied (this could be time and memory expensive). + Examples:: + + get_args(int) == () + get_args(Union[int, Union[T, int], str][int]) == (int, str) + get_args(Union[int, Tuple[T, int]][str]) == (int, (Tuple, str, int)) + + get_args(Union[int, Tuple[T, int]][str], evaluate=True) == \ + (int, Tuple[str, int]) + get_args(Dict[int, Tuple[T, T]][Optional[int]], evaluate=True) == \ + (int, Tuple[Optional[int], Optional[int]]) + get_args(Callable[[], T][int], evaluate=True) == ([], int,) + """ + if NEW_TYPING: + if evaluate is not None and not evaluate: + raise ValueError('evaluate can only be True in Python >= 3.7') + # Note special aliases on Python 3.9 don't have __args__. + if isinstance(tp, typingGenericAlias) and hasattr(tp, '__args__'): + res = tp.__args__ + if get_origin(tp) is collections.abc.Callable and res[0] is not Ellipsis: + res = (list(res[:-1]), res[-1]) + return res + if MaybeUnionType and isinstance(tp, MaybeUnionType): + return tp.__args__ + return () + if is_classvar(tp) or is_final_type(tp): + return (tp.__type__,) if tp.__type__ is not None else () + if is_literal_type(tp): + return tp.__values__ or () + if ( + is_generic_type(tp) or is_union_type(tp) or + is_callable_type(tp) or is_tuple_type(tp) + ): + try: + tree = tp._subs_tree() + except AttributeError: + # Old python typing module <= 3.5.3 + if is_union_type(tp): + # backport of union's subs_tree + tree = _union_subs_tree(tp) + elif is_generic_type(tp): + # backport of GenericMeta's subs_tree + tree = _generic_subs_tree(tp) + elif is_tuple_type(tp): + # ad-hoc (inspired by union) + tree = _tuple_subs_tree(tp) + else: + # tree = _subs_tree(tp) + return () + + if isinstance(tree, tuple) and len(tree) > 1: + if not evaluate: + return tree[1:] + res = _eval_args(tree[1:]) + if get_origin(tp) is Callable and res[0] is not Ellipsis: + res = (list(res[:-1]), res[-1]) + return res + + return () + + +def get_bound(tp): + """Return the type bound to a `TypeVar` if any. + + It the type is not a `TypeVar`, a `TypeError` is raised. + Examples:: + + get_bound(TypeVar('T')) == None + get_bound(TypeVar('T', bound=int)) == int + """ + + if is_typevar(tp): + return getattr(tp, '__bound__', None) + else: + raise TypeError("type is not a `TypeVar`: " + str(tp)) + + +def get_constraints(tp): + """Returns the constraints of a `TypeVar` if any. + + It the type is not a `TypeVar`, a `TypeError` is raised + Examples:: + + get_constraints(TypeVar('T')) == () + get_constraints(TypeVar('T', int, str)) == (int, str) + """ + + if is_typevar(tp): + return getattr(tp, '__constraints__', ()) + else: + raise TypeError("type is not a `TypeVar`: " + str(tp)) + + +def get_generic_type(obj): + """Get the generic type of an object if possible, or runtime class otherwise. + Examples:: + + class Node(Generic[T]): + ... + type(Node[int]()) == Node + get_generic_type(Node[int]()) == Node[int] + get_generic_type(Node[T]()) == Node[T] + get_generic_type(1) == int + """ + + gen_type = getattr(obj, '__orig_class__', None) + return gen_type if gen_type is not None else type(obj) + + +def get_generic_bases(tp): + """Get generic base types of a type or empty tuple if not possible. + Example:: + + class MyClass(List[int], Mapping[str, List[int]]): + ... + MyClass.__bases__ == (List, Mapping) + get_generic_bases(MyClass) == (List[int], Mapping[str, List[int]]) + """ + if LEGACY_TYPING: + return tuple(t for t in tp.__bases__ if isinstance(t, GenericMeta)) + else: + return getattr(tp, '__orig_bases__', ()) + + +def typed_dict_keys(td): + """If td is a TypedDict class, return a dictionary mapping the typed keys to types. + Otherwise, return None. Examples:: + + class TD(TypedDict): + x: int + y: int + class Other(dict): + x: int + y: int + + typed_dict_keys(TD) == {'x': int, 'y': int} + typed_dict_keys(dict) == None + typed_dict_keys(Other) == None + """ + if isinstance(td, (_TypedDictMeta_Mypy, _TypedDictMeta_TE)): + return td.__annotations__.copy() + return None + + +def get_forward_arg(fr): + """ + If fr is a ForwardRef, return the string representation of the forward reference. + Otherwise return None. Examples:: + + tp = List["FRef"] + fr = get_args(tp)[0] + get_forward_arg(fr) == "FRef" + get_forward_arg(tp) == None + """ + return fr.__forward_arg__ if is_forward_ref(fr) else None + + +# A few functions backported and adapted for the LEGACY_TYPING context, and used above + +def _replace_arg(arg, tvars, args): + """backport of _replace_arg""" + if tvars is None: + tvars = [] + # if hasattr(arg, '_subs_tree') and isinstance(arg, (GenericMeta, _TypingBase)): + # return arg._subs_tree(tvars, args) + if is_union_type(arg): + return _union_subs_tree(arg, tvars, args) + if is_tuple_type(arg): + return _tuple_subs_tree(arg, tvars, args) + if is_generic_type(arg): + return _generic_subs_tree(arg, tvars, args) + if isinstance(arg, TypeVar): + for i, tvar in enumerate(tvars): + if arg == tvar: + return args[i] + return arg + + +def _remove_dups_flatten(parameters): + """backport of _remove_dups_flatten""" + + # Flatten out Union[Union[...], ...]. + params = [] + for p in parameters: + if isinstance(p, _Union): # and p.__origin__ is Union: + params.extend(p.__union_params__) # p.__args__) + elif isinstance(p, tuple) and len(p) > 0 and p[0] is Union: + params.extend(p[1:]) + else: + params.append(p) + # Weed out strict duplicates, preserving the first of each occurrence. + all_params = set(params) + if len(all_params) < len(params): + new_params = [] + for t in params: + if t in all_params: + new_params.append(t) + all_params.remove(t) + params = new_params + assert not all_params, all_params + # Weed out subclasses. + # E.g. Union[int, Employee, Manager] == Union[int, Employee]. + # If object is present it will be sole survivor among proper classes. + # Never discard type variables. + # (In particular, Union[str, AnyStr] != AnyStr.) + all_params = set(params) + for t1 in params: + if not isinstance(t1, type): + continue + if any(isinstance(t2, type) and issubclass(t1, t2) + for t2 in all_params - {t1} + if (not (isinstance(t2, GenericMeta) and + get_origin(t2) is not None) and + not isinstance(t2, TypeVar))): + all_params.remove(t1) + return tuple(t for t in params if t in all_params) + + +def _subs_tree(cls, tvars=None, args=None): + """backport of typing._subs_tree, adapted for legacy versions """ + def _get_origin(cls): + try: + return cls.__origin__ + except AttributeError: + return None + + current = _get_origin(cls) + if current is None: + if not is_union_type(cls) and not is_tuple_type(cls): + return cls + + # Make of chain of origins (i.e. cls -> cls.__origin__) + orig_chain = [] + while _get_origin(current) is not None: + orig_chain.append(current) + current = _get_origin(current) + + # Replace type variables in __args__ if asked ... + tree_args = [] + + def _get_args(cls): + if is_union_type(cls): + cls_args = cls.__union_params__ + elif is_tuple_type(cls): + cls_args = cls.__tuple_params__ + else: + try: + cls_args = cls.__args__ + except AttributeError: + cls_args = () + return cls_args if cls_args is not None else () + + for arg in _get_args(cls): + tree_args.append(_replace_arg(arg, tvars, args)) + # ... then continue replacing down the origin chain. + for ocls in orig_chain: + new_tree_args = [] + for arg in _get_args(ocls): + new_tree_args.append(_replace_arg(arg, get_parameters(ocls), tree_args)) + tree_args = new_tree_args + return tree_args + + +def _union_subs_tree(tp, tvars=None, args=None): + """ backport of Union._subs_tree """ + if tp is Union: + return Union # Nothing to substitute + tree_args = _subs_tree(tp, tvars, args) + # tree_args = tp.__union_params__ if tp.__union_params__ is not None else () + tree_args = _remove_dups_flatten(tree_args) + if len(tree_args) == 1: + return tree_args[0] # Union of a single type is that type + return (Union,) + tree_args + + +def _generic_subs_tree(tp, tvars=None, args=None): + """ backport of GenericMeta._subs_tree """ + if tp.__origin__ is None: + return tp + tree_args = _subs_tree(tp, tvars, args) + return (_gorg(tp),) + tuple(tree_args) + + +def _tuple_subs_tree(tp, tvars=None, args=None): + """ ad-hoc function (inspired by union) for legacy typing """ + if tp is Tuple: + return Tuple # Nothing to substitute + tree_args = _subs_tree(tp, tvars, args) + return (Tuple,) + tuple(tree_args) + + +def _has_type_var(t): + if t is None: + return False + elif is_union_type(t): + return _union_has_type_var(t) + elif is_tuple_type(t): + return _tuple_has_type_var(t) + elif is_generic_type(t): + return _generic_has_type_var(t) + elif is_callable_type(t): + return _callable_has_type_var(t) + else: + return False + + +def _union_has_type_var(tp): + if tp.__union_params__: + for t in tp.__union_params__: + if _has_type_var(t): + return True + return False + + +def _tuple_has_type_var(tp): + if tp.__tuple_params__: + for t in tp.__tuple_params__: + if _has_type_var(t): + return True + return False + + +def _callable_has_type_var(tp): + if tp.__args__: + for t in tp.__args__: + if _has_type_var(t): + return True + return _has_type_var(tp.__result__) + + +def _generic_has_type_var(tp): + if tp.__parameters__: + for t in tp.__parameters__: + if _has_type_var(t): + return True + return False diff --git a/ccxt/test/__init__.py b/ccxt/test/__init__.py new file mode 100644 index 0000000..c2a36b8 --- /dev/null +++ b/ccxt/test/__init__.py @@ -0,0 +1,3 @@ +# from ccxt.test.test_ohlcv import test_ohlcv # noqa: F401 +# from . import ccxt +# __all__ = [ ccxt.test.test_shared_methods ] diff --git a/ccxt/test/base/language_specific/test_language_specific.py b/ccxt/test/base/language_specific/test_language_specific.py new file mode 100644 index 0000000..5b29619 --- /dev/null +++ b/ccxt/test/base/language_specific/test_language_specific.py @@ -0,0 +1,19 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + + +from ccxt.test.base.test_deep_extend import test_deep_extend # noqa E402 + +def test_language_specific(): + pass diff --git a/ccxt/test/base/test_after_constructor.py b/ccxt/test/base/test_after_constructor.py new file mode 100644 index 0000000..22a03f9 --- /dev/null +++ b/ccxt/test/base/test_after_constructor.py @@ -0,0 +1,114 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +import ccxt # noqa: F402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +def helper_test_init_throttler(): + exchange = ccxt.Exchange({ + 'id': 'sampleexchange', + 'rateLimit': 10.8, + }) + # todo: assert (exchange.MAX_VALUE !== undefined); + token_bucket = exchange.get_property(exchange, 'tokenBucket') # trick for uncamelcase transpilation + if token_bucket is None: + token_bucket = exchange.get_property(exchange, 'TokenBucket') + assert token_bucket is not None + assert 'GO_SKIP_START' + rate_limit = exchange.get_property(exchange, 'rateLimit') + assert rate_limit == 10.8 + assert token_bucket['delay'] == 0.001 + assert token_bucket['refillRate'] == 1 / rate_limit + assert 'GO_SKIP_END' + # fix decimal/integer issues across langs + assert exchange.in_array(token_bucket['capacity'], [1, 1]) + cost = exchange.parse_to_numeric(exchange.safe_string_2(token_bucket, 'cost', 'defaultCost')) # python sync, todo fix + assert exchange.in_array(cost, [1, 1]) + assert not ('maxCapacity' in token_bucket) or exchange.in_array(token_bucket['maxCapacity'], [1000, 1000]) + + +def helper_test_sandbox_state(exchange, should_be_enabled=True): + assert exchange.urls is not None + assert 'test' in exchange.urls + assert 'GO_SKIP_START' + is_sandbox_mode_enabled = exchange.get_property(exchange, 'isSandboxModeEnabled') + if should_be_enabled: + assert is_sandbox_mode_enabled + assert exchange.urls['api']['public'] == 'https://example.org' + assert exchange.urls['apiBackup']['public'] == 'https://example.com' + else: + assert not is_sandbox_mode_enabled + assert exchange.urls['api']['public'] == 'https://example.com' + assert exchange.urls['test']['public'] == 'https://example.org' + assert 'GO_SKIP_END' + + +def helper_test_init_sandbox(): + # todo: sandbox for real exchanges + opts = { + 'id': 'sampleexchange', + 'options': { + 'sandbox': False, + }, + 'urls': { + 'api': { + 'public': 'https://example.com', + }, + 'test': { + 'public': 'https://example.org', + }, + }, + } + # + # CASE A: when sandbox is not enabled + # + exchange3 = ccxt.Exchange(opts) + helper_test_sandbox_state(exchange3, False) + exchange3.set_sandbox_mode(True) + helper_test_sandbox_state(exchange3, True) + # + # CASE B: when sandbox is enabled + # + opts['options']['sandbox'] = True + exchange4 = ccxt.Exchange(opts) + helper_test_sandbox_state(exchange4, True) + exchange4.set_sandbox_mode(False) + helper_test_sandbox_state(exchange4, False) + + +def helper_test_init_market(): + # ############# markets ############# # + sample_market = { + 'id': 'BtcUsd', + 'symbol': 'BTC/USD', + 'base': 'BTC', + 'quote': 'USD', + 'baseId': 'Btc', + 'quoteId': 'Usd', + 'type': 'spot', + 'spot': True, + } + exchange2 = ccxt.Exchange({ + 'id': 'sampleexchange', + 'markets': { + 'BTC/USD': sample_market, + }, + }) + assert exchange2.markets['BTC/USD'] is not None + + +def test_after_constructor(): + helper_test_init_throttler() + helper_test_init_sandbox() + helper_test_init_market() diff --git a/ccxt/test/base/test_arrays_concat.py b/ccxt/test/base/test_arrays_concat.py new file mode 100644 index 0000000..bbccbd4 --- /dev/null +++ b/ccxt/test/base/test_arrays_concat.py @@ -0,0 +1,22 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +import ccxt # noqa: F402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +def test_arrays_concat(): + exchange = ccxt.Exchange({ + 'id': 'sampleexchange', + }) + test_shared_methods.assert_deep_equal(exchange, None, 'testArraysConcat', exchange.arrays_concat([['b'], ['a', 'c']]), ['b', 'a', 'c']) diff --git a/ccxt/test/base/test_cryptography.py b/ccxt/test/base/test_cryptography.py new file mode 100644 index 0000000..92622b2 --- /dev/null +++ b/ccxt/test/base/test_cryptography.py @@ -0,0 +1,136 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- + +import ccxt # noqa: F402 +import hashlib # noqa: F402 + +Exchange = ccxt.Exchange +hash = Exchange.hash +hmac = Exchange.hmac +ecdsa = Exchange.ecdsa +eddsa = Exchange.eddsa +safe_string = Exchange.safe_string +safe_integer = Exchange.safe_integer +in_array = Exchange.in_array +jwt = Exchange.jwt +crc32 = Exchange.crc32 +rsa = Exchange.rsa +encode = Exchange.encode + + +def equals(a, b): + return a == b + + +# even though no AUTO_TRANSP flag here, self file is manually transpiled + + +def test_cryptography(): + + # exchange = Exchange() + + # --------------------------------------------------------------------------------------------------------------------- + + assert hash(encode(''), 'sha256', 'hex') == 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' + assert hash(encode('cheese'), 'sha256', 'hex') == '873ac9ffea4dd04fa719e8920cd6938f0c23cd678af330939cff53c3d2855f34' + + assert hash(encode(''), 'md5', 'hex') == 'd41d8cd98f00b204e9800998ecf8427e' + assert hash(encode('sexyfish'), 'md5', 'hex') == 'c8a35464aa9d5683585786f44d5889f8' + + assert hash(encode(''), 'sha1', 'hex') == 'da39a3ee5e6b4b0d3255bfef95601890afd80709' + assert hash(encode('nutella'), 'sha1', 'hex') == 'b3d60a34b744159793c483b067c56d8affc5111a' + assert hmac(encode('hello'), encode('there'), hashlib.sha256, 'hex') == '551e1c1ecbce0fe9b643745a376584a6289f5f43a46861b315fac9edc8d52a26' + assert hmac(encode('a message'), encode('a secret'), hashlib.md5, 'hex') == '0bfa503bdbc7358185fcd49b4869e23d' + + # --------------------------------------------------------------------------------------------------------------------- + + + privateKey = '1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a' + + + assert(equals(ecdsa('1a', privateKey, 'secp256k1', 'sha256'), { + 'r': '23dcb2a2a3728a35eb1a35cc01743c4609550d9cceaf2083550f13a9eb135f9f', + 's': '317963fcac18e4ec9f7921b97d7ea0c82a873dd6299cbfb6af016e08ef5ed667', + 'v': 0, + })) + + + assert(equals(ecdsa(privateKey, privateKey, 'secp256k1', None), { + 'r': 'b84a36a6fbabd5277ede578448b93d48e70b38efb5b15b1d4e2a298accf938b1', + 's': '66ebfb8221cda925526e699a59cd221bb4cc84bdc563024b1802c4d9e1d8bbe9', + 'v': 1, + })) + + # --------------------------------------------------------------------------------------------------------------------- + + # + # assert exchange.hashMessage(privateKey) == '0x59ea5d98c3500c3729f95cf98aa91663f498518cc401360df2912742c232207f' + # + # assert(equals(exchange.signHash('0x59ea5d98c3500c3729f95cf98aa91663f498518cc401360df2912742c232207f', privateKey), { + # 'r': '0x6f684aa41c02da83dac3039d8805ddbe79a03b1297e247c7742cab8dfc19d341', + # 's': '0x62473881674550563cb028ff40a7846fd53620ddf40a20cc1003b8484a109a4a', + # 'v': 27 + # })) + # + # assert(equals(exchange.signMessage(privateKey, privateKey), { + # 'r': '0x6f684aa41c02da83dac3039d8805ddbe79a03b1297e247c7742cab8dfc19d341', + # 's': '0x62473881674550563cb028ff40a7846fd53620ddf40a20cc1003b8484a109a4a', + # 'v': 27 + # })) + # + # --------------------------------------------------------------------------------------------------------------------- + + pemKeyArray = [ + '-----BEGIN RSA PRIVATE KEY-----', + 'MIIEpAIBAAKCAQEAqUtXQXv2uSm9zPvdJTRpu5/65rBjAoHmIiowAs+u7fY0QP9O', + '+T8CZRQjvZrfPomBiccjCxDo8mm7GkmL67rs9s7VOMQFvpTTtMBeglNSEbXJ3wTt', + '7WM5gD3ZAbhfGtwWdGpcOMX0hV5/d/fiw1zQk0+AooayTryZ7HV+DQcD1sGTYYir', + 'LePUGkKV2NCvKtGOQfRAtuo0ccigMxMvEhWuExCc9Fhd077OpXMc0n7OiFqA2JWN', + 'jlocM9S71Srvw/lDos8a7lGaFUV8gP1CsQY/4qTju6hhsRd4lFujVr5J9FqmDbnv', + 'wQ79SSu6+lT0+ToFdDHiYOorBK2ESFR7EzlyLwIDAQABAoIBAEobQMbZjNbg/sSM', + 'O/HdT6tiDGKPM8gVNLgf34RbhSeFbrpFCDzy6Al3F24YLUEi0CGPmjdt34q93blU', + 'GHvIB5LCV3PR2vHiFAo7ayOBdZtrCEMn1T7lAHaynBu0qW0IiovLQzNW9AKtqv7I', + '8+qw5lyVoKmEbOkqhfaMN/Fb8MJAo3yEVyhyp4EjtFhqDQ9DuHEjp1jGthnMgm+4', + 'qFyELf0DXZSAN+R9IGDqWPyo78lOcPW4pgeLuoLQ6Nn/2JAEGHFt1a7zMEw5yYbR', + 'XB89Bh0dheSmopBN6Heay4YuhnNKua37OlL1/nhKpLGUNE3z/UOXeiUiMeE1C2i+', + 'GjK+xgECgYEA4XH63rJ3VYDHHWUGlWffFWkEpgR4BDEkZ8MyVYQoUjQA2TpCvQ2+', + 'bR7aAIGXhBnRTrIBNu9m4+am0aUfi1hVmVHqt1b2vFgEb9uKuO4U62tCFpeji+Rz', + '8C8QJyu5v5OUWdQZ4EA0d7ljeoTl50g8tpcGsDhnImLt+/jvJuFt8dECgYEAwD0n', + 'rhlkEjKVHxu1xwKeczxYCqqcUV9Quq8HnPh4VJQ77ljfZ57V7sAKrFDi5XfVD9jS', + 'oJPwxjw6za134VobhdvCM6WNs6AJmbR8E/b5kDMbGaZn27vt+/8JfnGaWBqpDi6W', + 'a5xoJOqmcMBQBK1YleZDoY4PLbk+onVKLMfGI/8CgYEAkyUIv8euGdGWpHnm5SdJ', + 'tLi5vv4Vs267uzntJWG/y3+DukTLgIdy7dgAI+pxkVgkg/+syUVSW5eU9CqZPyLl', + 'o8+Sqh2Jp36vTq71iSRj5RA5r3ND3K+8eFzPZzGj6AWUA1lrljFxzV7kLfiF8gH1', + 'FpvWUrhNoGT/vcFJno/uabECgYAecRC5hxfLserfVDoC261PvjyK492BHUDhbxob', + 'h1U2v4qGAdjOxd5Gwm5uPxjPEZzRt5oTB5pXKe5953xWWTiGh/hGyW6ZBTy/9E65', + 'sqBub0lZVHqZ1zamcwqD1WWFkiM3NbVoMQpk3iuhKzMAqpqekiofiSlqKi16+GvY', + 'j4IW7QKBgQCrIUckPZ6IY3ERIN1IL4TIcK2gOJcznp7fLWpC+sv35ya3OhtDXKFt', + 'GjIHmwLuiUNc0iPzA9Rw89W0zIkKWmWcxM88/ithdxh3MEeNDUGSnd4hQ9SECwxB', + 'Wem3eBT7I4VtFYoaTE3/bX1SKfgBdTzIRqWKSDpgBNZg/P2Tc+s11g==', + '-----END RSA PRIVATE KEY-----', + ] + + + pemKey = "\n".join(pemKeyArray) + assert rsa('hello', pemKey, 'sha256') == 'PqHotvSEBvM/AejnMOWBXUcOf3uHtcGu2zAYdlYdnlNSSQ80Uq4lcyAEstnZ2AnQJ9l5TCC53uoRZ26GQ47zlACgqtYglmPhQKLvQ5fldRzeBauYhGgM2C0mUuUGxh074fNGbK+bgmwEmDMIrnSPtXwiCqTAHh+8VEnC7us3t09D61y298dPBJYEBNN3dFZT0w0pCIQg3j3DSiFJOCfywmOKyXqS1pvmk6A38DVclQZORQ5WZXp2yvSKRLjxpzjxDl76h1GfbBl7sMLEFMyzk0wyIhIz8ZELMibYn036G4X1IcSlDcimthEkIbn2QjM0ntyYDZIS4QnsMBjvkV2UHw==' + assert rsa('poopy', pemKey, 'sha256') == 'or069qHwRDyl162T1s5G5+LfLnvDxlgk9kEsJvwI3vM02KB1LHW4+8gWqsV4TENZpeqed2Tb4na6ex+L/UR8JxJnnZtpVp33nUBgcp3miqp/YhcGN4++qolP4YN/21/AyfLFZW+VYggc+Mhh6PJSm+0dSEWMVsP35uH+35abXVxgB8GVOn7YTOSPaL8aw7hn4wlZWf2ieikKAi7AwAjkxnd7/Bu5+cW8D+ZdPHQfSKj+XHuPXlJNuQX0MDIqhdD2yuYJOQL56eKYs6nHPlClATkndSaAemQSGKet3X3Iz1awG4MGgz2Ei6bOanlNugc0f6Rng6rdwmqiMU3G4it6cw==' + + assert jwt({'chicken': 'salad'}, encode(pemKey), 'sha256', True) == 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJjaGlja2VuIjoic2FsYWQifQ.FSKD5Y6RNzkHTuHdvG3753U7QNZ-u-GUSPfP1FMjEaK0Rr_iyQTSSmHhkdYSFFnmBvrrN_l-UwKwir52WlsgmQm9HYm0kidxbj7fWwrK2E1oe0P7OjupFjv1BZxc5W69WeaHtOPWe28tiHiON1LCnax6HgfI5lcIBsESGIIBZMVeaioQn9gDVwea7JxJvAlrhDIWZowIHTIdCQocXip7g5jREWHeEIuJNug67mwnfAFxCjvTRiTd0Bw6oBwjM3FLya-RyEyWrejQOWSuC8CNWVUHISaSmEyZ7uM6wTi2m_58TaE9mQwlef32OPErPvvBpgL5pZIyQ4ymwrCIFQLBQQ' + assert jwt({'lil': 'xan'}, encode('betrayed'), 'sha256', False) == 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsaWwiOiJ4YW4ifQ.md-oFvZagA-NXmZoRNyJOQ7zwK-PWUMmMQ_LI9ZOKaM' + + assert crc32('hello', True) == 907060870 + assert crc32('tasty chicken breast :)', True) == 825820175 + assert crc32('21101:0.00123125:21102:-0.001:21100:0.710705:21103:-0.001:21096:0.71076:21104:-0.001:21094:1.0746:21105:-0.001:21093:0.710854:21106:-0.419:21092:0.01368102:21107:-0.001:21090:0.710975:21109:-0.001:21089:0.63586344:21110:-1.186213:21087:0.299:21111:-0.48751202:21086:0.9493:21112:-0.03702409:21082:0.03537667:21113:-0.712385:21081:0.00101366:21114:-0.2903:21079:0.710713:21115:-0.001:21078:0.997048:21116:-0.60089827:21077:0.23770225:21117:-0.83201:21076:0.03619135:21118:-0.09996142:21075:0.1272433:21119:-1.09681107:21074:0.7447885:21120:-0.04771792:21073:0.0011:21121:-0.91495684:21072:0.73311632:21122:-0.07940416:21071:0.09817:21123:-0.39376843:21070:0.19101052:21124:-1.51692599:21069:0.2757:21125:-0.11107322:21068:0.12480303:21126:-0.12704666:21067:0.4201:21128:-0.12804666', True) == -51055998 + + # assert eddsa('1b1b', privateKey, 'ed25519') == '3DBaaz8z4Pq9n6ncNCjB4pFLWaWTXbjaCUqKQmBgS3w7AP6opeDqANBhPssbV3jyfJB4LfK8kGR6pu6GU8fbjMuy' + diff --git a/ccxt/test/base/test_datetime.py b/ccxt/test/base/test_datetime.py new file mode 100644 index 0000000..1395a69 --- /dev/null +++ b/ccxt/test/base/test_datetime.py @@ -0,0 +1,69 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.base.decimal_to_precision import ROUND # noqa E402 +from ccxt.base.decimal_to_precision import ROUND_UP # noqa E402 +from ccxt.base.decimal_to_precision import ROUND_DOWN # noqa E402 +import ccxt # noqa: F402 + +def test_datetime(): + exchange = ccxt.Exchange({ + 'id': 'regirock', + }) + assert exchange.iso8601(514862627000) == '1986-04-26T01:23:47.000Z' + assert exchange.iso8601(514862627559) == '1986-04-26T01:23:47.559Z' + assert exchange.iso8601(514862627062) == '1986-04-26T01:23:47.062Z' + assert exchange.iso8601(1) == '1970-01-01T00:00:00.001Z' + assert exchange.iso8601(-1) is None + # assert (exchange.iso8601 () === undefined); + # todo: assert (exchange.iso8601 () === undefined); + assert exchange.iso8601(None) is None + assert exchange.iso8601('') is None + assert exchange.iso8601('a') is None + assert exchange.iso8601({}) is None + # ---------------------------------------------------------------------------- + assert exchange.parse8601('1986-04-26T01:23:47.000Z') == 514862627000 + assert exchange.parse8601('1986-04-26T01:23:47.559Z') == 514862627559 + assert exchange.parse8601('1986-04-26T01:23:47.062Z') == 514862627062 + assert exchange.parse8601('1986-04-26T01:23:47.06Z') == 514862627060 + assert exchange.parse8601('1986-04-26T01:23:47.6Z') == 514862627600 + assert exchange.parse8601('1977-13-13T00:00:00.000Z') is None + assert exchange.parse8601('1986-04-26T25:71:47.000Z') is None + assert exchange.parse8601('3333') is None + assert exchange.parse8601('Sr90') is None + assert exchange.parse8601('') is None + # assert (exchange.parse8601 () === undefined); + # todo: assert (exchange.parse8601 () === undefined); + assert exchange.parse8601(None) is None + assert exchange.parse8601({}) is None + assert exchange.parse8601(33) is None + # ---------------------------------------------------------------------------- + assert exchange.parse_date('1986-04-26 00:00:00') == 514857600000 + assert exchange.parse_date('1986-04-26T01:23:47.000Z') == 514862627000 + assert exchange.parse_date('1986-13-13 00:00:00') is None + # GMT formats (todo: bugs in php) + # assert (exchange.parseDate ('Mon, 29 Apr 2024 14:00:17 GMT') === 1714399217000); + # assert (exchange.parseDate ('Mon, 29 Apr 2024 14:09:17 GMT') === 1714399757000); + # assert (exchange.parseDate ('Sun, 29 Dec 2024 01:01:10 GMT') === 1735434070000); + # assert (exchange.parseDate ('Sun, 29 Dec 2024 02:11:10 GMT') === 1735438270000); + # assert (exchange.parseDate ('Sun, 08 Dec 2024 02:03:04 GMT') === 1733623384000); + assert exchange.round_timeframe('5m', exchange.parse8601('2019-08-12 13:22:08'), ROUND_DOWN) == exchange.parse8601('2019-08-12 13:20:00') + assert exchange.round_timeframe('10m', exchange.parse8601('2019-08-12 13:22:08'), ROUND_DOWN) == exchange.parse8601('2019-08-12 13:20:00') + assert exchange.round_timeframe('30m', exchange.parse8601('2019-08-12 13:22:08'), ROUND_DOWN) == exchange.parse8601('2019-08-12 13:00:00') + assert exchange.round_timeframe('1d', exchange.parse8601('2019-08-12 13:22:08'), ROUND_DOWN) == exchange.parse8601('2019-08-12 00:00:00') + assert exchange.round_timeframe('5m', exchange.parse8601('2019-08-12 13:22:08'), ROUND_UP) == exchange.parse8601('2019-08-12 13:25:00') + assert exchange.round_timeframe('10m', exchange.parse8601('2019-08-12 13:22:08'), ROUND_UP) == exchange.parse8601('2019-08-12 13:30:00') + assert exchange.round_timeframe('30m', exchange.parse8601('2019-08-12 13:22:08'), ROUND_UP) == exchange.parse8601('2019-08-12 13:30:00') + assert exchange.round_timeframe('1h', exchange.parse8601('2019-08-12 13:22:08'), ROUND_UP) == exchange.parse8601('2019-08-12 14:00:00') + assert exchange.round_timeframe('1d', exchange.parse8601('2019-08-12 13:22:08'), ROUND_UP) == exchange.parse8601('2019-08-13 00:00:00') diff --git a/ccxt/test/base/test_decimal_to_precision.py b/ccxt/test/base/test_decimal_to_precision.py new file mode 100644 index 0000000..8cb1346 --- /dev/null +++ b/ccxt/test/base/test_decimal_to_precision.py @@ -0,0 +1,325 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.base.decimal_to_precision import DECIMAL_PLACES # noqa E402 +from ccxt.base.decimal_to_precision import TICK_SIZE # noqa E402 +from ccxt.base.decimal_to_precision import NO_PADDING # noqa E402 +from ccxt.base.decimal_to_precision import TRUNCATE # noqa E402 +from ccxt.base.decimal_to_precision import ROUND # noqa E402 +from ccxt.base.decimal_to_precision import SIGNIFICANT_DIGITS # noqa E402 +from ccxt.base.decimal_to_precision import PAD_WITH_ZERO # noqa E402 +from ccxt.base.decimal_to_precision import decimal_to_precision # noqa E402 +import ccxt # noqa: F402 + +def test_decimal_to_precision(): + exchange = ccxt.Exchange({ + 'id': 'regirock', + }) + # ---------------------------------------------------------------------------- + # Truncate To N Digits After Dot + assert exchange.decimal_to_precision('12.3456000', TRUNCATE, 100, DECIMAL_PLACES) == '12.3456' + assert exchange.decimal_to_precision('12.3456', TRUNCATE, 100, DECIMAL_PLACES) == '12.3456' + assert exchange.decimal_to_precision('12.3456', TRUNCATE, 4, DECIMAL_PLACES) == '12.3456' + assert exchange.decimal_to_precision('12.3456', TRUNCATE, 3, DECIMAL_PLACES) == '12.345' + assert exchange.decimal_to_precision('12.3456', TRUNCATE, 2, DECIMAL_PLACES) == '12.34' + assert exchange.decimal_to_precision('12.3456', TRUNCATE, 1, DECIMAL_PLACES) == '12.3' + assert exchange.decimal_to_precision('12.3456', TRUNCATE, 0, DECIMAL_PLACES) == '12' + # ['12.3456', TRUNCATE, -1, DECIMAL_PLACES, '10'], # not yet supported + # ['123.456', TRUNCATE, -2, DECIMAL_PLACES, '120'], # not yet supported + # ['123.456', TRUNCATE, -3, DECIMAL_PLACES, '100'], # not yet supported + assert exchange.decimal_to_precision('0.0000001', TRUNCATE, 8, DECIMAL_PLACES) == '0.0000001' + assert exchange.decimal_to_precision('0.00000001', TRUNCATE, 8, DECIMAL_PLACES) == '0.00000001' + assert exchange.decimal_to_precision('0.000000000', TRUNCATE, 9, DECIMAL_PLACES, PAD_WITH_ZERO) == '0.000000000' + assert exchange.decimal_to_precision('0.000000001', TRUNCATE, 9, DECIMAL_PLACES, PAD_WITH_ZERO) == '0.000000001' + assert exchange.decimal_to_precision('12.3456', TRUNCATE, -1, DECIMAL_PLACES) == '10' + assert exchange.decimal_to_precision('123.456', TRUNCATE, -1, DECIMAL_PLACES) == '120' + assert exchange.decimal_to_precision('123.456', TRUNCATE, -2, DECIMAL_PLACES) == '100' + assert exchange.decimal_to_precision('9.99999', TRUNCATE, -1, DECIMAL_PLACES) == '0' + assert exchange.decimal_to_precision('99.9999', TRUNCATE, -1, DECIMAL_PLACES) == '90' + assert exchange.decimal_to_precision('99.9999', TRUNCATE, -2, DECIMAL_PLACES) == '0' + assert exchange.decimal_to_precision('0', TRUNCATE, 0, DECIMAL_PLACES) == '0' + assert exchange.decimal_to_precision('-0.9', TRUNCATE, 0, DECIMAL_PLACES) == '0' + # ---------------------------------------------------------------------------- + # Truncate To N Significant Digits + assert exchange.decimal_to_precision('0.000123456700', TRUNCATE, 100, SIGNIFICANT_DIGITS) == '0.0001234567' + assert exchange.decimal_to_precision('0.0001234567', TRUNCATE, 100, SIGNIFICANT_DIGITS) == '0.0001234567' + assert exchange.decimal_to_precision('0.0001234567', TRUNCATE, 7, SIGNIFICANT_DIGITS) == '0.0001234567' + assert exchange.decimal_to_precision('0.000123456', TRUNCATE, 6, SIGNIFICANT_DIGITS) == '0.000123456' + assert exchange.decimal_to_precision('0.000123456', TRUNCATE, 5, SIGNIFICANT_DIGITS) == '0.00012345' + assert exchange.decimal_to_precision('0.000123456', TRUNCATE, 2, SIGNIFICANT_DIGITS) == '0.00012' + assert exchange.decimal_to_precision('0.000123456', TRUNCATE, 1, SIGNIFICANT_DIGITS) == '0.0001' + assert exchange.decimal_to_precision('123.0000987654', TRUNCATE, 10, SIGNIFICANT_DIGITS, PAD_WITH_ZERO) == '123.0000987' + assert exchange.decimal_to_precision('123.0000987654', TRUNCATE, 8, SIGNIFICANT_DIGITS) == '123.00009' + assert exchange.decimal_to_precision('123.0000987654', TRUNCATE, 7, SIGNIFICANT_DIGITS, PAD_WITH_ZERO) == '123.0000' + assert exchange.decimal_to_precision('123.0000987654', TRUNCATE, 6, SIGNIFICANT_DIGITS) == '123' + assert exchange.decimal_to_precision('123.0000987654', TRUNCATE, 5, SIGNIFICANT_DIGITS, PAD_WITH_ZERO) == '123.00' + assert exchange.decimal_to_precision('123.0000987654', TRUNCATE, 4, SIGNIFICANT_DIGITS) == '123' + assert exchange.decimal_to_precision('123.0000987654', TRUNCATE, 4, SIGNIFICANT_DIGITS, PAD_WITH_ZERO) == '123.0' + assert exchange.decimal_to_precision('123.0000987654', TRUNCATE, 3, SIGNIFICANT_DIGITS, PAD_WITH_ZERO) == '123' + assert exchange.decimal_to_precision('123.0000987654', TRUNCATE, 2, SIGNIFICANT_DIGITS) == '120' + assert exchange.decimal_to_precision('123.0000987654', TRUNCATE, 1, SIGNIFICANT_DIGITS) == '100' + assert exchange.decimal_to_precision('123.0000987654', TRUNCATE, 1, SIGNIFICANT_DIGITS, PAD_WITH_ZERO) == '100' + assert exchange.decimal_to_precision('1234', TRUNCATE, 5, SIGNIFICANT_DIGITS) == '1234' + assert exchange.decimal_to_precision('1234', TRUNCATE, 5, SIGNIFICANT_DIGITS, PAD_WITH_ZERO) == '1234.0' + assert exchange.decimal_to_precision('1234', TRUNCATE, 4, SIGNIFICANT_DIGITS) == '1234' + assert exchange.decimal_to_precision('1234', TRUNCATE, 4, SIGNIFICANT_DIGITS, PAD_WITH_ZERO) == '1234' + assert exchange.decimal_to_precision('1234.69', TRUNCATE, 0, SIGNIFICANT_DIGITS) == '0' + assert exchange.decimal_to_precision('1234.69', TRUNCATE, 0, SIGNIFICANT_DIGITS, PAD_WITH_ZERO) == '0' + # ---------------------------------------------------------------------------- + # Round To N Digits After Dot + assert exchange.decimal_to_precision('12.3456000', ROUND, 100, DECIMAL_PLACES) == '12.3456' + assert exchange.decimal_to_precision('12.3456', ROUND, 100, DECIMAL_PLACES) == '12.3456' + assert exchange.decimal_to_precision('12.3456', ROUND, 4, DECIMAL_PLACES) == '12.3456' + assert exchange.decimal_to_precision('12.3456', ROUND, 3, DECIMAL_PLACES) == '12.346' + assert exchange.decimal_to_precision('12.3456', ROUND, 2, DECIMAL_PLACES) == '12.35' + assert exchange.decimal_to_precision('12.3456', ROUND, 1, DECIMAL_PLACES) == '12.3' + assert exchange.decimal_to_precision('12.3456', ROUND, 0, DECIMAL_PLACES) == '12' + # todo: + # ['9.999', ROUND, 3, DECIMAL_PLACES, NO_PADDING, '9.999'], + # ['9.999', ROUND, 2, DECIMAL_PLACES, NO_PADDING, '10'], + # ['9.999', ROUND, 2, DECIMAL_PLACES, PAD_WITH_ZERO, '10.00'], + # ['99.999', ROUND, 2, DECIMAL_PLACES, PAD_WITH_ZERO, '100.00'], + # ['-99.999', ROUND, 2, DECIMAL_PLACES, PAD_WITH_ZERO, '-100.00'], + # ['12.3456', ROUND, -1, DECIMAL_PLACES, NO_PADDING, '10'], # not yet supported + # ['123.456', ROUND, -1, DECIMAL_PLACES, NO_PADDING, '120'], # not yet supported + # ['123.456', ROUND, -2, DECIMAL_PLACES, NO_PADDING, '100'], # not yet supported + # a problematic case in PHP + assert exchange.decimal_to_precision('10000', ROUND, 6, DECIMAL_PLACES) == '10000' + assert exchange.decimal_to_precision('0.00003186', ROUND, 8, DECIMAL_PLACES) == '0.00003186' + assert exchange.decimal_to_precision('12.3456', ROUND, -1, DECIMAL_PLACES) == '10' + assert exchange.decimal_to_precision('123.456', ROUND, -1, DECIMAL_PLACES) == '120' + assert exchange.decimal_to_precision('123.456', ROUND, -2, DECIMAL_PLACES) == '100' + assert exchange.decimal_to_precision('9.99999', ROUND, -1, DECIMAL_PLACES) == '10' + assert exchange.decimal_to_precision('99.9999', ROUND, -1, DECIMAL_PLACES) == '100' + assert exchange.decimal_to_precision('99.9999', ROUND, -2, DECIMAL_PLACES) == '100' + assert exchange.decimal_to_precision('9.999', ROUND, 3, DECIMAL_PLACES) == '9.999' + assert exchange.decimal_to_precision('9.999', ROUND, 2, DECIMAL_PLACES) == '10' + assert exchange.decimal_to_precision('9.999', ROUND, 2, DECIMAL_PLACES, PAD_WITH_ZERO) == '10.00' + assert exchange.decimal_to_precision('99.999', ROUND, 2, DECIMAL_PLACES, PAD_WITH_ZERO) == '100.00' + assert exchange.decimal_to_precision('-99.999', ROUND, 2, DECIMAL_PLACES, PAD_WITH_ZERO) == '-100.00' + # ---------------------------------------------------------------------------- + # Round To N Significant Digits + assert exchange.decimal_to_precision('0.000123456700', ROUND, 100, SIGNIFICANT_DIGITS) == '0.0001234567' + assert exchange.decimal_to_precision('0.0001234567', ROUND, 100, SIGNIFICANT_DIGITS) == '0.0001234567' + assert exchange.decimal_to_precision('0.0001234567', ROUND, 7, SIGNIFICANT_DIGITS) == '0.0001234567' + assert exchange.decimal_to_precision('0.000123456', ROUND, 6, SIGNIFICANT_DIGITS) == '0.000123456' + assert exchange.decimal_to_precision('0.000123456', ROUND, 5, SIGNIFICANT_DIGITS) == '0.00012346' + assert exchange.decimal_to_precision('0.000123456', ROUND, 4, SIGNIFICANT_DIGITS) == '0.0001235' + assert exchange.decimal_to_precision('0.00012', ROUND, 2, SIGNIFICANT_DIGITS) == '0.00012' + assert exchange.decimal_to_precision('0.0001', ROUND, 1, SIGNIFICANT_DIGITS) == '0.0001' + assert exchange.decimal_to_precision('123.0000987654', ROUND, 7, SIGNIFICANT_DIGITS) == '123.0001' + assert exchange.decimal_to_precision('123.0000987654', ROUND, 6, SIGNIFICANT_DIGITS) == '123' + assert exchange.decimal_to_precision('0.00098765', ROUND, 2, SIGNIFICANT_DIGITS) == '0.00099' + assert exchange.decimal_to_precision('0.00098765', ROUND, 2, SIGNIFICANT_DIGITS, PAD_WITH_ZERO) == '0.00099' + assert exchange.decimal_to_precision('0.00098765', ROUND, 1, SIGNIFICANT_DIGITS) == '0.001' + assert exchange.decimal_to_precision('0.00098765', ROUND, 10, SIGNIFICANT_DIGITS, PAD_WITH_ZERO) == '0.0009876500000' + assert exchange.decimal_to_precision('0.098765', ROUND, 1, SIGNIFICANT_DIGITS, PAD_WITH_ZERO) == '0.1' + assert exchange.decimal_to_precision('0', ROUND, 0, SIGNIFICANT_DIGITS) == '0' + assert exchange.decimal_to_precision('-0.123', ROUND, 0, SIGNIFICANT_DIGITS) == '0' + assert exchange.decimal_to_precision('0.00000044', ROUND, 5, SIGNIFICANT_DIGITS) == '0.00000044' + assert exchange.decimal_to_precision('0.123456', ROUND, 5, SIGNIFICANT_DIGITS) == '0.12346' + assert exchange.decimal_to_precision('0.123456', ROUND, 6, SIGNIFICANT_DIGITS) == '0.123456' + assert exchange.decimal_to_precision('0.123456', ROUND, 7, SIGNIFICANT_DIGITS) == '0.123456' + assert exchange.decimal_to_precision('1.234567', ROUND, 5, SIGNIFICANT_DIGITS) == '1.2346' + assert exchange.decimal_to_precision('1.234567', ROUND, 6, SIGNIFICANT_DIGITS) == '1.23457' + assert exchange.decimal_to_precision('1.234567', ROUND, 7, SIGNIFICANT_DIGITS) == '1.234567' + assert exchange.decimal_to_precision('12.34567', ROUND, 5, SIGNIFICANT_DIGITS) == '12.346' + assert exchange.decimal_to_precision('12.34567', ROUND, 6, SIGNIFICANT_DIGITS) == '12.3457' + assert exchange.decimal_to_precision('12.34567', ROUND, 7, SIGNIFICANT_DIGITS) == '12.34567' + # above 1.0 + assert exchange.decimal_to_precision('1114.5', ROUND, 3, SIGNIFICANT_DIGITS) == '1110' + assert exchange.decimal_to_precision('1115.5', ROUND, 3, SIGNIFICANT_DIGITS) == '1120' + assert exchange.decimal_to_precision('1114.5', ROUND, 4, SIGNIFICANT_DIGITS) == '1115' + assert exchange.decimal_to_precision('1114.5', ROUND, 5, SIGNIFICANT_DIGITS) == '1114.5' + assert exchange.decimal_to_precision('1115.5', ROUND, 5, SIGNIFICANT_DIGITS) == '1115.5' + # ---------------------------------------------------------------------------- + # Round To Tick Size + assert exchange.decimal_to_precision('0.000123456700', ROUND, 0.00012, TICK_SIZE) == '0.00012' + assert exchange.decimal_to_precision('0.0001234567', ROUND, 0.00013, TICK_SIZE) == '0.00013' + assert exchange.decimal_to_precision('0.0001234567', TRUNCATE, 0.00013, TICK_SIZE) == '0' + assert exchange.decimal_to_precision('101.000123456700', ROUND, 100, TICK_SIZE) == '100' + assert exchange.decimal_to_precision('0.000123456700', ROUND, 100, TICK_SIZE) == '0' + assert exchange.decimal_to_precision('165', TRUNCATE, 110, TICK_SIZE) == '110' + assert exchange.decimal_to_precision('3210', TRUNCATE, 1110, TICK_SIZE) == '2220' + assert exchange.decimal_to_precision('165', ROUND, 110, TICK_SIZE) == '220' + assert exchange.decimal_to_precision('0.000123456789', ROUND, 1.2e-7, TICK_SIZE) == '0.00012348' + assert exchange.decimal_to_precision('0.000123456789', TRUNCATE, 1.2e-7, TICK_SIZE) == '0.00012336' + assert exchange.decimal_to_precision('0.000273398', ROUND, 1e-7, TICK_SIZE) == '0.0002734' + assert exchange.decimal_to_precision('0.00005714', TRUNCATE, 1e-8, TICK_SIZE) == '0.00005714' + # this line causes problems in JS, fix with Precise + # assert (exchange.decimalToPrecision ('0.0000571495257361', TRUNCATE, 0.00000001, TICK_SIZE) === '0.00005714'); + assert exchange.decimal_to_precision('0.01', ROUND, 0.0001, TICK_SIZE, PAD_WITH_ZERO) == '0.0100' + assert exchange.decimal_to_precision('0.01', TRUNCATE, 0.0001, TICK_SIZE, PAD_WITH_ZERO) == '0.0100' + assert exchange.decimal_to_precision('-0.000123456789', ROUND, 1.2e-7, TICK_SIZE) == '-0.00012348' + assert exchange.decimal_to_precision('-0.000123456789', TRUNCATE, 1.2e-7, TICK_SIZE) == '-0.00012336' + assert exchange.decimal_to_precision('-165', TRUNCATE, 110, TICK_SIZE) == '-110' + assert exchange.decimal_to_precision('-165', ROUND, 110, TICK_SIZE) == '-220' + assert exchange.decimal_to_precision('-1650', TRUNCATE, 1100, TICK_SIZE) == '-1100' + assert exchange.decimal_to_precision('-1650', ROUND, 1100, TICK_SIZE) == '-2200' + assert exchange.decimal_to_precision('0.0006', TRUNCATE, 0.0001, TICK_SIZE) == '0.0006' + assert exchange.decimal_to_precision('-0.0006', TRUNCATE, 0.0001, TICK_SIZE) == '-0.0006' + assert exchange.decimal_to_precision('0.6', TRUNCATE, 0.2, TICK_SIZE) == '0.6' + assert exchange.decimal_to_precision('-0.6', TRUNCATE, 0.2, TICK_SIZE) == '-0.6' + assert exchange.decimal_to_precision('1.2', ROUND, 0.4, TICK_SIZE) == '1.2' + assert exchange.decimal_to_precision('-1.2', ROUND, 0.4, TICK_SIZE) == '-1.2' + assert exchange.decimal_to_precision('1.2', ROUND, 0.02, TICK_SIZE) == '1.2' + assert exchange.decimal_to_precision('-1.2', ROUND, 0.02, TICK_SIZE) == '-1.2' + assert exchange.decimal_to_precision('44', ROUND, 4.4, TICK_SIZE) == '44' + assert exchange.decimal_to_precision('-44', ROUND, 4.4, TICK_SIZE) == '-44' + assert exchange.decimal_to_precision('44.00000001', ROUND, 4.4, TICK_SIZE) == '44' + assert exchange.decimal_to_precision('-44.00000001', ROUND, 4.4, TICK_SIZE) == '-44' + # https://github.com/ccxt/ccxt/issues/6731 + assert exchange.decimal_to_precision('20', TRUNCATE, 1e-8, TICK_SIZE) == '20' + assert exchange.decimal_to_precision('0.000123456789', TRUNCATE, 1e-8, TICK_SIZE) == '0.00012345' + # ---------------------------------------------------------------------------- + # Negative Numbers + assert exchange.decimal_to_precision('-0.123456', TRUNCATE, 5, DECIMAL_PLACES) == '-0.12345' + assert exchange.decimal_to_precision('-0.123456', ROUND, 5, DECIMAL_PLACES) == '-0.12346' + # ---------------------------------------------------------------------------- + # without dot / trailing dot + assert exchange.decimal_to_precision('123', TRUNCATE, 0) == '123' + assert exchange.decimal_to_precision('123', TRUNCATE, 5, DECIMAL_PLACES) == '123' + assert exchange.decimal_to_precision('123', TRUNCATE, 5, DECIMAL_PLACES, PAD_WITH_ZERO) == '123.00000' + assert exchange.decimal_to_precision('123.', TRUNCATE, 0, DECIMAL_PLACES) == '123' + assert exchange.decimal_to_precision('123.', TRUNCATE, 5, DECIMAL_PLACES, PAD_WITH_ZERO) == '123.00000' + assert exchange.decimal_to_precision('0.', TRUNCATE, 0) == '0' + assert exchange.decimal_to_precision('0.', TRUNCATE, 5, DECIMAL_PLACES, PAD_WITH_ZERO) == '0.00000' + # ---------------------------------------------------------------------------- + # rounding for equidistant digits + assert exchange.decimal_to_precision('1.44', ROUND, 1, DECIMAL_PLACES) == '1.4' + assert exchange.decimal_to_precision('1.45', ROUND, 1, DECIMAL_PLACES) == '1.5' + assert exchange.decimal_to_precision('1.45', ROUND, 0, DECIMAL_PLACES) == '1' # not 2 + # ---------------------------------------------------------------------------- + # negative precision only implemented so far in python + # pretty useless for decimal applications as anything |x| < 5 === 0 + # NO_PADDING and PAD_WITH_ZERO are ignored + assert exchange.decimal_to_precision('5', ROUND, -1, DECIMAL_PLACES) == '10' + assert exchange.decimal_to_precision('4.999', ROUND, -1, DECIMAL_PLACES) == '0' + assert exchange.decimal_to_precision('0.0431531423', ROUND, -1, DECIMAL_PLACES) == '0' + assert exchange.decimal_to_precision('-69.3', ROUND, -1, DECIMAL_PLACES) == '-70' + assert exchange.decimal_to_precision('5001', ROUND, -4, DECIMAL_PLACES) == '10000' + assert exchange.decimal_to_precision('4999.999', ROUND, -4, DECIMAL_PLACES) == '0' + assert exchange.decimal_to_precision('69.3', TRUNCATE, -2, DECIMAL_PLACES) == '0' + assert exchange.decimal_to_precision('-69.3', TRUNCATE, -2, DECIMAL_PLACES) == '0' + assert exchange.decimal_to_precision('69.3', TRUNCATE, -1, SIGNIFICANT_DIGITS) == '60' + assert exchange.decimal_to_precision('-69.3', TRUNCATE, -1, SIGNIFICANT_DIGITS) == '-60' + assert exchange.decimal_to_precision('69.3', TRUNCATE, -2, SIGNIFICANT_DIGITS) == '0' + assert exchange.decimal_to_precision('1602000000000000000000', TRUNCATE, 3, SIGNIFICANT_DIGITS) == '1600000000000000000000' + # ---------------------------------------------------------------------------- + # stringified precision + assert exchange.decimal_to_precision('-0.000123456789', ROUND, '0.00000012', TICK_SIZE) == '-0.00012348' + assert exchange.decimal_to_precision('-0.000123456789', TRUNCATE, '0.00000012', TICK_SIZE) == '-0.00012336' + assert exchange.decimal_to_precision('-165', TRUNCATE, '110', TICK_SIZE) == '-110' + assert exchange.decimal_to_precision('-165', ROUND, '110', TICK_SIZE) == '-220' + # ---------------------------------------------------------------------------- + # testDecimalToPrecisionErrorHandling (todo) + # + # throws (() => + # decimalToPrecision ('123456.789', TRUNCATE, -2, DECIMAL_PLACES), + # 'negative precision is not yet supported') + # + # throws (() => + # decimalToPrecision ('foo'), + # "invalid number (contains an illegal character 'f')") + # + # throws (() => + # decimalToPrecision ('0.01', TRUNCATE, -1, TICK_SIZE), + # "TICK_SIZE cant be used with negative numPrecisionDigits") + # ---------------------------------------------------------------------------- + # Additional Edge Cases + # Zero handling variations + assert exchange.decimal_to_precision('0.0', TRUNCATE, 2, DECIMAL_PLACES) == '0' + assert exchange.decimal_to_precision('0.00', ROUND, 3, DECIMAL_PLACES, PAD_WITH_ZERO) == '0.000' + assert exchange.decimal_to_precision('-0.0', TRUNCATE, 2, DECIMAL_PLACES) == '0' + assert exchange.decimal_to_precision('-0.00', ROUND, 1, DECIMAL_PLACES) == '0' + # Very small numbers close to zero + assert exchange.decimal_to_precision('0.0000000001', TRUNCATE, 8, DECIMAL_PLACES) == '0' + assert exchange.decimal_to_precision('0.0000000001', ROUND, 8, DECIMAL_PLACES) == '0' + assert exchange.decimal_to_precision('0.0000000001', TRUNCATE, 10, DECIMAL_PLACES) == '0.0000000001' + assert exchange.decimal_to_precision('0.00000000009', ROUND, 10, DECIMAL_PLACES) == '0.0000000001' + assert exchange.decimal_to_precision('0.00000000015', ROUND, 10, DECIMAL_PLACES) == '0.0000000002' + # Very large numbers + assert exchange.decimal_to_precision('99999999999999.99999', TRUNCATE, 2, DECIMAL_PLACES) == '99999999999999.99' + assert exchange.decimal_to_precision('99999999999999.99999', ROUND, 2, DECIMAL_PLACES) == '100000000000000' + assert exchange.decimal_to_precision('123456789012345', TRUNCATE, 3, SIGNIFICANT_DIGITS) == '123000000000000' + assert exchange.decimal_to_precision('123456789012345', ROUND, 3, SIGNIFICANT_DIGITS) == '123000000000000' + # Numbers with leading zeros + assert exchange.decimal_to_precision('000123.456', TRUNCATE, 2, DECIMAL_PLACES) == '123.45' + assert exchange.decimal_to_precision('000123.456', ROUND, 2, DECIMAL_PLACES) == '123.46' + assert exchange.decimal_to_precision('0000.123', TRUNCATE, 2, DECIMAL_PLACES) == '0.12' + # Boundary rounding cases (exactly at 0.5) + assert exchange.decimal_to_precision('1.5', ROUND, 0, DECIMAL_PLACES) == '2' + assert exchange.decimal_to_precision('2.5', ROUND, 0, DECIMAL_PLACES) == '3' + assert exchange.decimal_to_precision('-1.5', ROUND, 0, DECIMAL_PLACES) == '-2' + assert exchange.decimal_to_precision('-2.5', ROUND, 0, DECIMAL_PLACES) == '-3' + assert exchange.decimal_to_precision('1.25', ROUND, 1, DECIMAL_PLACES) == '1.3' + assert exchange.decimal_to_precision('1.35', ROUND, 1, DECIMAL_PLACES) == '1.4' + # Carry-over in rounding (cascading effects) + assert exchange.decimal_to_precision('9.999999', ROUND, 0, DECIMAL_PLACES) == '10' + assert exchange.decimal_to_precision('99.999999', ROUND, 0, DECIMAL_PLACES) == '100' + assert exchange.decimal_to_precision('999.999999', ROUND, 0, DECIMAL_PLACES) == '1000' + assert exchange.decimal_to_precision('9.999999', ROUND, 1, DECIMAL_PLACES) == '10' + assert exchange.decimal_to_precision('9.999999', ROUND, 2, DECIMAL_PLACES) == '10' + assert exchange.decimal_to_precision('-9.999999', ROUND, 0, DECIMAL_PLACES) == '-10' + assert exchange.decimal_to_precision('-99.999999', ROUND, 0, DECIMAL_PLACES) == '-100' + # Edge cases for TICK_SIZE with very small ticks + assert exchange.decimal_to_precision('1.2345', ROUND, 0.0001, TICK_SIZE) == '1.2345' + assert exchange.decimal_to_precision('1.23456', ROUND, 0.0001, TICK_SIZE) == '1.2346' + assert exchange.decimal_to_precision('1.23454', ROUND, 0.0001, TICK_SIZE) == '1.2345' + assert exchange.decimal_to_precision('1.23444', TRUNCATE, 0.0001, TICK_SIZE) == '1.2344' + # TICK_SIZE with numbers smaller than tick + assert exchange.decimal_to_precision('0.05', ROUND, 0.1, TICK_SIZE) == '0.1' + assert exchange.decimal_to_precision('0.04', ROUND, 0.1, TICK_SIZE) == '0' + assert exchange.decimal_to_precision('0.04', TRUNCATE, 0.1, TICK_SIZE) == '0' + assert exchange.decimal_to_precision('0.049999', ROUND, 0.1, TICK_SIZE) == '0' + # SIGNIFICANT_DIGITS edge cases + assert exchange.decimal_to_precision('10000000', TRUNCATE, 1, SIGNIFICANT_DIGITS) == '10000000' + assert exchange.decimal_to_precision('10000001', TRUNCATE, 1, SIGNIFICANT_DIGITS) == '10000000' + assert exchange.decimal_to_precision('19999999', TRUNCATE, 1, SIGNIFICANT_DIGITS) == '10000000' + assert exchange.decimal_to_precision('19999999', ROUND, 1, SIGNIFICANT_DIGITS) == '20000000' + # Precision with PAD_WITH_ZERO edge cases + assert exchange.decimal_to_precision('1', TRUNCATE, 0, DECIMAL_PLACES, PAD_WITH_ZERO) == '1' + assert exchange.decimal_to_precision('1.0', TRUNCATE, 0, DECIMAL_PLACES, PAD_WITH_ZERO) == '1' + assert exchange.decimal_to_precision('1', TRUNCATE, 3, DECIMAL_PLACES, PAD_WITH_ZERO) == '1.000' + assert exchange.decimal_to_precision('1.1', TRUNCATE, 5, DECIMAL_PLACES, PAD_WITH_ZERO) == '1.10000' + # Numbers that are exactly multiples of precision + assert exchange.decimal_to_precision('1.2', TRUNCATE, 0.1, TICK_SIZE) == '1.2' + assert exchange.decimal_to_precision('1.2', ROUND, 0.1, TICK_SIZE) == '1.2' + assert exchange.decimal_to_precision('12', TRUNCATE, 4, TICK_SIZE) == '12' + assert exchange.decimal_to_precision('12', ROUND, 4, TICK_SIZE) == '12' + # Very high precision values + assert exchange.decimal_to_precision('1.123456789012345', TRUNCATE, 15, DECIMAL_PLACES) == '1.123456789012345' + assert exchange.decimal_to_precision('1.123456789012345', TRUNCATE, 10, DECIMAL_PLACES) == '1.123456789' + assert exchange.decimal_to_precision('1.123456789012345', TRUNCATE, 10, DECIMAL_PLACES, PAD_WITH_ZERO) == '1.1234567890' + assert exchange.decimal_to_precision('0.123456789012345', TRUNCATE, 15, SIGNIFICANT_DIGITS) == '0.123456789012345' + # Mixed large and small components + assert exchange.decimal_to_precision('1000000.000001', TRUNCATE, 6, DECIMAL_PLACES) == '1000000.000001' + assert exchange.decimal_to_precision('1000000.000001', TRUNCATE, 5, DECIMAL_PLACES, PAD_WITH_ZERO) == '1000000.00000' + assert exchange.decimal_to_precision('1000000.000001', ROUND, 5, DECIMAL_PLACES, PAD_WITH_ZERO) == '1000000.00000' + # Edge cases around 1.0 boundary for significant digits + assert exchange.decimal_to_precision('0.999999', ROUND, 1, SIGNIFICANT_DIGITS) == '1' + assert exchange.decimal_to_precision('0.999999', ROUND, 2, SIGNIFICANT_DIGITS, PAD_WITH_ZERO) == '1.0' + assert exchange.decimal_to_precision('0.999999', ROUND, 3, SIGNIFICANT_DIGITS, PAD_WITH_ZERO) == '1.00' + assert exchange.decimal_to_precision('0.999949', ROUND, 4, SIGNIFICANT_DIGITS) == '0.9999' + assert exchange.decimal_to_precision('0.999951', ROUND, 4, SIGNIFICANT_DIGITS, PAD_WITH_ZERO) == '1.000' + # ---------------------------------------------------------------------------- + # https://github.com/ccxt/ccxt/issues/11765 + assert exchange.decimal_to_precision('123456.12345678912', TRUNCATE, 1e-8, TICK_SIZE) == '123456.12345678' + # todo: not sure about below + assert exchange.decimal_to_precision('123456.12345674999', TRUNCATE, 5e-8, TICK_SIZE) == '123456.1234567' + assert exchange.decimal_to_precision('123456.12345674999', TRUNCATE, 5e-8, TICK_SIZE, PAD_WITH_ZERO) == '123456.12345670' + assert exchange.decimal_to_precision('123456.12345675001', TRUNCATE, 5e-8, TICK_SIZE) == '123456.12345675' + assert exchange.decimal_to_precision('123456.50000000001', TRUNCATE, 0.5, TICK_SIZE, PAD_WITH_ZERO) == '123456.5' + assert exchange.decimal_to_precision('123456.49999999999', TRUNCATE, 0.5, TICK_SIZE, PAD_WITH_ZERO) == '123456.0' + assert exchange.decimal_to_precision('123456.12345678912', TRUNCATE, '0.00000001', TICK_SIZE) == '123456.12345678' diff --git a/ccxt/test/base/test_deep_extend.py b/ccxt/test/base/test_deep_extend.py new file mode 100644 index 0000000..6c06641 --- /dev/null +++ b/ccxt/test/base/test_deep_extend.py @@ -0,0 +1,23 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +import ccxt # noqa: F402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +def test_deep_extend(): + exchange = ccxt.Exchange({ + 'id': 'sampleexchange', + }) + assert exchange.parse_to_numeric('1') == 1 + return True # dummy for now diff --git a/ccxt/test/base/test_extend.py b/ccxt/test/base/test_extend.py new file mode 100644 index 0000000..1b8c775 --- /dev/null +++ b/ccxt/test/base/test_extend.py @@ -0,0 +1,82 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +import ccxt # noqa: F402 + +def test_extend(): + exchange = ccxt.Exchange({ + 'id': 'regirock', + }) + obj1 = { + 'a': 1, + 'b': [1, 2], + 'c': [{ + 'test1': 1, + 'test2': 1, +}], + 'd': None, + 'e': 'not_undefined', + 'sub': { + 'a': 1, + 'b': [1, 2], + 'c': [{ + 'test1': 1, + 'test2': 2, +}], + 'd': None, + 'e': 'not_undefined', + 'other1': 'x', + }, + 'other1': 'x', + } + obj2 = { + 'a': 2, + 'b': [3, 4], + 'c': [{ + 'test1': 2, + 'test3': 3, +}], + 'd': 'not_undefined', + 'e': None, + 'sub': { + 'a': 2, + 'b': [3, 4], + 'c': [{ + 'test1': 2, + 'test3': 3, +}], + 'd': 'not_undefined', + 'e': None, + 'other2': 'y', + }, + 'other2': 'y', + } + # extend + extended = exchange.extend(obj1, obj2) + tbfe_check_extended(extended, True) + + +def tbfe_check_extended(extended, has_sub): + assert extended['a'] == 2 + assert extended['b'][0] == 3 + assert extended['b'][1] == 4 + assert extended['c'][0]['test1'] == 2 + assert not ('test2' in extended['c'][0]) + assert extended['c'][0]['test3'] == 3 + assert extended['d'] == 'not_undefined' + assert extended['e'] is None + assert extended['other1'] == 'x' + assert extended['other2'] == 'y' + if has_sub: + assert 'sub' in extended diff --git a/ccxt/test/base/test_filter_by.py b/ccxt/test/base/test_filter_by.py new file mode 100644 index 0000000..1badd28 --- /dev/null +++ b/ccxt/test/base/test_filter_by.py @@ -0,0 +1,49 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +import ccxt # noqa: F402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +def test_filter_by(): + exchange = ccxt.Exchange({ + 'id': 'sampleexchange', + }) + sample_array = [{ + 'foo': 'a', +}, { + 'foo': None, +}, { + 'foo': 'b', +}, { + 'foo': 'a', + 'bar': 'b', +}, { + 'foo': 'c', +}, { + 'foo': 'd', +}, { + 'foo': 'b', +}, { + 'foo': 'c', +}, { + 'foo': 'c', +}] + current_value = exchange.filter_by(sample_array, 'foo', 'a') + stored_value = [{ + 'foo': 'a', +}, { + 'foo': 'a', + 'bar': 'b', +}] + test_shared_methods.assert_deep_equal(exchange, None, 'testFilterBy', current_value, stored_value) diff --git a/ccxt/test/base/test_group_by.py b/ccxt/test/base/test_group_by.py new file mode 100644 index 0000000..373cf8c --- /dev/null +++ b/ccxt/test/base/test_group_by.py @@ -0,0 +1,53 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +import ccxt # noqa: F402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +def test_group_by(): + exchange = ccxt.Exchange({ + 'id': 'sampleexchange', + }) + sample_array = [{ + 'foo': 'a', +}, { + 'foo': 'b', +}, { + 'foo': 'c', +}, { + 'foo': 'b', +}, { + 'foo': 'c', +}, { + 'foo': 'c', +}] + current_value = exchange.group_by(sample_array, 'foo') + stored_value = { + 'a': [{ + 'foo': 'a', +}], + 'b': [{ + 'foo': 'b', +}, { + 'foo': 'b', +}], + 'c': [{ + 'foo': 'c', +}, { + 'foo': 'c', +}, { + 'foo': 'c', +}], + } + test_shared_methods.assert_deep_equal(exchange, None, 'testGroupBy', current_value, stored_value) diff --git a/ccxt/test/base/test_handle_methods.py b/ccxt/test/base/test_handle_methods.py new file mode 100644 index 0000000..26d7a32 --- /dev/null +++ b/ccxt/test/base/test_handle_methods.py @@ -0,0 +1,93 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +import ccxt # noqa: F402 + +def helper_test_handle_market_type_and_params(): + exchange = ccxt.Exchange({ + 'id': 'sampleexchange', + 'options': { + 'defaultType': 'valueFromOptions', + 'fetchX': { + 'defaultType': 'valueFromMethodOptions', + }, + }, + }) + initial_params = { + 'defaultType': 'valueFromParam', + } + market = exchange.safe_market('TEST1/TEST2') + market['type'] = 'spot' + # + # ########### test different variations ########### + # + # case #1, should prevail: param + # + [market_type_1, params1] = exchange.handle_market_type_and_params('fetchX', market, initial_params, 'valueDefault') + assert 'defaultType' in initial_params + assert not ('defaultType' in params1) + assert market_type_1 == 'valueFromParam' + # + # case #2, should prevail: market.type + # + [market_type_2, params2] = exchange.handle_market_type_and_params('fetchX', market, {}, 'valueDefault') + assert market_type_2 == 'spot' + # + # case #3, should prevail: valueDefault + # + [market_type_3, params3] = exchange.handle_market_type_and_params('fetchX', None, {}, 'valueDefault') + assert market_type_3 == 'valueDefault' + # + # case #4, should prevail: method options + # + [market_type_4, params4] = exchange.handle_market_type_and_params('fetchX', None, {}) + assert market_type_4 == 'valueFromMethodOptions' + # + # case #5, should prevail: options + # + [market_type_5, params5] = exchange.handle_market_type_and_params('fetchY', None, {}, None) + assert market_type_5 == 'valueFromOptions' + # + # case #6, should prevail: spot (because hardcoded in base) + # + exchange.options['defaultType'] = None + [market_type_6, params6] = exchange.handle_market_type_and_params('fetchY', None, {}, None) + assert market_type_6 == 'spot' + # fake assertion to avoid unused vars + assert params1 is not None or params2 is not None or params3 is not None or params4 is not None or params5 is not None or params6 is not None + + +def helper_test_handle_network_request(): + exchange = ccxt.Exchange({ + 'id': 'sampleexchange', + 'options': { + 'networks': { + 'XYZ': 'Xyz', + }, + }, + }) + exchange.currencies = exchange.create_safe_dictionary() # todo: initialize in C# base files + currency_code = 'ETH' # todo: in future with complex cases + # no-case + [request1, params1] = exchange.handle_request_network({ + 'network': 'XYZ', + }, {}, 'chain_id', currency_code, False) + assert not ('network' in params1) + assert 'chain_id' in request1 + assert request1['chain_id'] == 'Xyz' + + +def test_handle_methods(): + helper_test_handle_market_type_and_params() + helper_test_handle_network_request() diff --git a/ccxt/test/base/test_json.py b/ccxt/test/base/test_json.py new file mode 100644 index 0000000..dbc8c4b --- /dev/null +++ b/ccxt/test/base/test_json.py @@ -0,0 +1,42 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +import ccxt # noqa: F402 +from ccxt.base.errors import BadRequest # noqa E402 + +def test_json(): + exchange = ccxt.Exchange({ + 'id': 'regirock', + }) + # Test: object + obj = { + 'k': 'v', + } + obj_json = exchange.json(obj) + assert obj_json == '{"k":"v"}' + # Test: list + list = [1, 2] + list_json = exchange.json(list) + assert list_json == '[1,2]' + assert 'GO_SKIP_START' + try: + raise BadRequest('some error') + except Exception as e: + err_string = exchange.json(e) + assert err_string == '{"name":"BadRequest"}' + assert 'GO_SKIP_END' + # Test: json a string + str = 'ccxt, rocks!' + serialized_string = exchange.json(str) + assert serialized_string == '"ccxt, rocks!"' diff --git a/ccxt/test/base/test_number.py b/ccxt/test/base/test_number.py new file mode 100644 index 0000000..d2d2ed4 --- /dev/null +++ b/ccxt/test/base/test_number.py @@ -0,0 +1,342 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.base.decimal_to_precision import DECIMAL_PLACES # noqa E402 +from ccxt.base.decimal_to_precision import TICK_SIZE # noqa E402 +from ccxt.base.decimal_to_precision import NO_PADDING # noqa E402 +from ccxt.base.decimal_to_precision import TRUNCATE # noqa E402 +from ccxt.base.decimal_to_precision import ROUND # noqa E402 +from ccxt.base.decimal_to_precision import SIGNIFICANT_DIGITS # noqa E402 +from ccxt.base.decimal_to_precision import PAD_WITH_ZERO # noqa E402 +from ccxt.base.decimal_to_precision import decimal_to_precision # noqa E402 +from ccxt.base.decimal_to_precision import number_to_string # noqa E402 +import ccxt # noqa: F402 +from ccxt.base.precise import Precise # noqa E402 + +def test_number(): + exchange = ccxt.Exchange({ + 'id': 'regirock', + }) + # ---------------------------------------------------------------------------- + # numberToString + assert exchange.number_to_string(-7.8e-7) == '-0.00000078' + assert exchange.number_to_string(7.8e-7) == '0.00000078' + assert exchange.number_to_string(-0.0000017805) == '-0.0000017805' + assert exchange.number_to_string(0.0000017805) == '0.0000017805' + assert exchange.number_to_string(-7.0005e+27) == '-7000500000000000000000000000' + assert exchange.number_to_string(7.0005e+27) == '7000500000000000000000000000' + assert exchange.number_to_string(-7.9e+27) == '-7900000000000000000000000000' + assert exchange.number_to_string(7e+27) == '7000000000000000000000000000' + assert exchange.number_to_string(7.9e+27) == '7900000000000000000000000000' + assert exchange.number_to_string(-12.345) == '-12.345' + assert exchange.number_to_string(12.345) == '12.345' + assert exchange.number_to_string(0) == '0' + assert exchange.number_to_string(7.35946e+21) == '7359460000000000000000' + assert exchange.number_to_string(1e-8) == '0.00000001' + assert exchange.number_to_string(1e-7) == '0.0000001' + assert exchange.number_to_string(-1e-7) == '-0.0000001' + # ---------------------------------------------------------------------------- + # testDecimalToPrecisionTruncationToNDigitsAfterDot + assert exchange.decimal_to_precision('12.3456000', TRUNCATE, 100, DECIMAL_PLACES) == '12.3456' + assert exchange.decimal_to_precision('12.3456', TRUNCATE, 100, DECIMAL_PLACES) == '12.3456' + assert exchange.decimal_to_precision('12.3456', TRUNCATE, 4, DECIMAL_PLACES) == '12.3456' + assert exchange.decimal_to_precision('12.3456', TRUNCATE, 3, DECIMAL_PLACES) == '12.345' + assert exchange.decimal_to_precision('12.3456', TRUNCATE, 2, DECIMAL_PLACES) == '12.34' + assert exchange.decimal_to_precision('12.3456', TRUNCATE, 1, DECIMAL_PLACES) == '12.3' + assert exchange.decimal_to_precision('12.3456', TRUNCATE, 0, DECIMAL_PLACES) == '12' + # ['12.3456', TRUNCATE, -1, DECIMAL_PLACES, '10'], # not yet supported + # ['123.456', TRUNCATE, -2, DECIMAL_PLACES, '120'], # not yet supported + # ['123.456', TRUNCATE, -3, DECIMAL_PLACES, '100'], # not yet supported + assert exchange.decimal_to_precision('0.0000001', TRUNCATE, 8, DECIMAL_PLACES) == '0.0000001' + assert exchange.decimal_to_precision('0.00000001', TRUNCATE, 8, DECIMAL_PLACES) == '0.00000001' + assert exchange.decimal_to_precision('0.000000000', TRUNCATE, 9, DECIMAL_PLACES, PAD_WITH_ZERO) == '0.000000000' + assert exchange.decimal_to_precision('0.000000001', TRUNCATE, 9, DECIMAL_PLACES, PAD_WITH_ZERO) == '0.000000001' + assert exchange.decimal_to_precision('12.3456', TRUNCATE, -1, DECIMAL_PLACES) == '10' + assert exchange.decimal_to_precision('123.456', TRUNCATE, -1, DECIMAL_PLACES) == '120' + assert exchange.decimal_to_precision('123.456', TRUNCATE, -2, DECIMAL_PLACES) == '100' + assert exchange.decimal_to_precision('9.99999', TRUNCATE, -1, DECIMAL_PLACES) == '0' + assert exchange.decimal_to_precision('99.9999', TRUNCATE, -1, DECIMAL_PLACES) == '90' + assert exchange.decimal_to_precision('99.9999', TRUNCATE, -2, DECIMAL_PLACES) == '0' + assert exchange.decimal_to_precision('0', TRUNCATE, 0, DECIMAL_PLACES) == '0' + assert exchange.decimal_to_precision('-0.9', TRUNCATE, 0, DECIMAL_PLACES) == '0' + # ---------------------------------------------------------------------------- + # testDecimalToPrecisionTruncationToNSignificantDigits + assert exchange.decimal_to_precision('0.000123456700', TRUNCATE, 100, SIGNIFICANT_DIGITS) == '0.0001234567' + assert exchange.decimal_to_precision('0.0001234567', TRUNCATE, 100, SIGNIFICANT_DIGITS) == '0.0001234567' + assert exchange.decimal_to_precision('0.0001234567', TRUNCATE, 7, SIGNIFICANT_DIGITS) == '0.0001234567' + assert exchange.decimal_to_precision('0.000123456', TRUNCATE, 6, SIGNIFICANT_DIGITS) == '0.000123456' + assert exchange.decimal_to_precision('0.000123456', TRUNCATE, 5, SIGNIFICANT_DIGITS) == '0.00012345' + assert exchange.decimal_to_precision('0.000123456', TRUNCATE, 2, SIGNIFICANT_DIGITS) == '0.00012' + assert exchange.decimal_to_precision('0.000123456', TRUNCATE, 1, SIGNIFICANT_DIGITS) == '0.0001' + assert exchange.decimal_to_precision('123.0000987654', TRUNCATE, 10, SIGNIFICANT_DIGITS, PAD_WITH_ZERO) == '123.0000987' + assert exchange.decimal_to_precision('123.0000987654', TRUNCATE, 8, SIGNIFICANT_DIGITS) == '123.00009' + assert exchange.decimal_to_precision('123.0000987654', TRUNCATE, 7, SIGNIFICANT_DIGITS, PAD_WITH_ZERO) == '123.0000' + assert exchange.decimal_to_precision('123.0000987654', TRUNCATE, 6, SIGNIFICANT_DIGITS) == '123' + assert exchange.decimal_to_precision('123.0000987654', TRUNCATE, 5, SIGNIFICANT_DIGITS, PAD_WITH_ZERO) == '123.00' + assert exchange.decimal_to_precision('123.0000987654', TRUNCATE, 4, SIGNIFICANT_DIGITS) == '123' + assert exchange.decimal_to_precision('123.0000987654', TRUNCATE, 4, SIGNIFICANT_DIGITS, PAD_WITH_ZERO) == '123.0' + assert exchange.decimal_to_precision('123.0000987654', TRUNCATE, 3, SIGNIFICANT_DIGITS, PAD_WITH_ZERO) == '123' + assert exchange.decimal_to_precision('123.0000987654', TRUNCATE, 2, SIGNIFICANT_DIGITS) == '120' + assert exchange.decimal_to_precision('123.0000987654', TRUNCATE, 1, SIGNIFICANT_DIGITS) == '100' + assert exchange.decimal_to_precision('123.0000987654', TRUNCATE, 1, SIGNIFICANT_DIGITS, PAD_WITH_ZERO) == '100' + assert exchange.decimal_to_precision('1234', TRUNCATE, 5, SIGNIFICANT_DIGITS) == '1234' + assert exchange.decimal_to_precision('1234', TRUNCATE, 5, SIGNIFICANT_DIGITS, PAD_WITH_ZERO) == '1234.0' + assert exchange.decimal_to_precision('1234', TRUNCATE, 4, SIGNIFICANT_DIGITS) == '1234' + assert exchange.decimal_to_precision('1234', TRUNCATE, 4, SIGNIFICANT_DIGITS, PAD_WITH_ZERO) == '1234' + assert exchange.decimal_to_precision('1234.69', TRUNCATE, 0, SIGNIFICANT_DIGITS) == '0' + assert exchange.decimal_to_precision('1234.69', TRUNCATE, 0, SIGNIFICANT_DIGITS, PAD_WITH_ZERO) == '0' + # ---------------------------------------------------------------------------- + # testDecimalToPrecisionRoundingToNDigitsAfterDot + assert exchange.decimal_to_precision('12.3456000', ROUND, 100, DECIMAL_PLACES) == '12.3456' + assert exchange.decimal_to_precision('12.3456', ROUND, 100, DECIMAL_PLACES) == '12.3456' + assert exchange.decimal_to_precision('12.3456', ROUND, 4, DECIMAL_PLACES) == '12.3456' + assert exchange.decimal_to_precision('12.3456', ROUND, 3, DECIMAL_PLACES) == '12.346' + assert exchange.decimal_to_precision('12.3456', ROUND, 2, DECIMAL_PLACES) == '12.35' + assert exchange.decimal_to_precision('12.3456', ROUND, 1, DECIMAL_PLACES) == '12.3' + assert exchange.decimal_to_precision('12.3456', ROUND, 0, DECIMAL_PLACES) == '12' + # todo: + # ['9.999', ROUND, 3, DECIMAL_PLACES, NO_PADDING, '9.999'], + # ['9.999', ROUND, 2, DECIMAL_PLACES, NO_PADDING, '10'], + # ['9.999', ROUND, 2, DECIMAL_PLACES, PAD_WITH_ZERO, '10.00'], + # ['99.999', ROUND, 2, DECIMAL_PLACES, PAD_WITH_ZERO, '100.00'], + # ['-99.999', ROUND, 2, DECIMAL_PLACES, PAD_WITH_ZERO, '-100.00'], + # ['12.3456', ROUND, -1, DECIMAL_PLACES, NO_PADDING, '10'], # not yet supported + # ['123.456', ROUND, -1, DECIMAL_PLACES, NO_PADDING, '120'], # not yet supported + # ['123.456', ROUND, -2, DECIMAL_PLACES, NO_PADDING, '100'], # not yet supported + # a problematic case in PHP + assert exchange.decimal_to_precision('10000', ROUND, 6, DECIMAL_PLACES) == '10000' + assert exchange.decimal_to_precision('0.00003186', ROUND, 8, DECIMAL_PLACES) == '0.00003186' + assert exchange.decimal_to_precision('12.3456', ROUND, -1, DECIMAL_PLACES) == '10' + assert exchange.decimal_to_precision('123.456', ROUND, -1, DECIMAL_PLACES) == '120' + assert exchange.decimal_to_precision('123.456', ROUND, -2, DECIMAL_PLACES) == '100' + assert exchange.decimal_to_precision('9.99999', ROUND, -1, DECIMAL_PLACES) == '10' + assert exchange.decimal_to_precision('99.9999', ROUND, -1, DECIMAL_PLACES) == '100' + assert exchange.decimal_to_precision('99.9999', ROUND, -2, DECIMAL_PLACES) == '100' + assert exchange.decimal_to_precision('9.999', ROUND, 3, DECIMAL_PLACES) == '9.999' + assert exchange.decimal_to_precision('9.999', ROUND, 2, DECIMAL_PLACES) == '10' + assert exchange.decimal_to_precision('9.999', ROUND, 2, DECIMAL_PLACES, PAD_WITH_ZERO) == '10.00' + assert exchange.decimal_to_precision('99.999', ROUND, 2, DECIMAL_PLACES, PAD_WITH_ZERO) == '100.00' + assert exchange.decimal_to_precision('-99.999', ROUND, 2, DECIMAL_PLACES, PAD_WITH_ZERO) == '-100.00' + # ---------------------------------------------------------------------------- + # testDecimalToPrecisionRoundingToNSignificantDigits + assert exchange.decimal_to_precision('0.000123456700', ROUND, 100, SIGNIFICANT_DIGITS) == '0.0001234567' + assert exchange.decimal_to_precision('0.0001234567', ROUND, 100, SIGNIFICANT_DIGITS) == '0.0001234567' + assert exchange.decimal_to_precision('0.0001234567', ROUND, 7, SIGNIFICANT_DIGITS) == '0.0001234567' + assert exchange.decimal_to_precision('0.000123456', ROUND, 6, SIGNIFICANT_DIGITS) == '0.000123456' + assert exchange.decimal_to_precision('0.000123456', ROUND, 5, SIGNIFICANT_DIGITS) == '0.00012346' + assert exchange.decimal_to_precision('0.000123456', ROUND, 4, SIGNIFICANT_DIGITS) == '0.0001235' + assert exchange.decimal_to_precision('0.00012', ROUND, 2, SIGNIFICANT_DIGITS) == '0.00012' + assert exchange.decimal_to_precision('0.0001', ROUND, 1, SIGNIFICANT_DIGITS) == '0.0001' + assert exchange.decimal_to_precision('123.0000987654', ROUND, 7, SIGNIFICANT_DIGITS) == '123.0001' + assert exchange.decimal_to_precision('123.0000987654', ROUND, 6, SIGNIFICANT_DIGITS) == '123' + assert exchange.decimal_to_precision('0.00098765', ROUND, 2, SIGNIFICANT_DIGITS) == '0.00099' + assert exchange.decimal_to_precision('0.00098765', ROUND, 2, SIGNIFICANT_DIGITS, PAD_WITH_ZERO) == '0.00099' + assert exchange.decimal_to_precision('0.00098765', ROUND, 1, SIGNIFICANT_DIGITS) == '0.001' + assert exchange.decimal_to_precision('0.00098765', ROUND, 10, SIGNIFICANT_DIGITS, PAD_WITH_ZERO) == '0.0009876500000' + assert exchange.decimal_to_precision('0.098765', ROUND, 1, SIGNIFICANT_DIGITS, PAD_WITH_ZERO) == '0.1' + assert exchange.decimal_to_precision('0', ROUND, 0, SIGNIFICANT_DIGITS) == '0' + assert exchange.decimal_to_precision('-0.123', ROUND, 0, SIGNIFICANT_DIGITS) == '0' + assert exchange.decimal_to_precision('0.00000044', ROUND, 5, SIGNIFICANT_DIGITS) == '0.00000044' + # ---------------------------------------------------------------------------- + # testDecimalToPrecisionRoundingToTickSize + assert exchange.decimal_to_precision('0.000123456700', ROUND, 0.00012, TICK_SIZE) == '0.00012' + assert exchange.decimal_to_precision('0.0001234567', ROUND, 0.00013, TICK_SIZE) == '0.00013' + assert exchange.decimal_to_precision('0.0001234567', TRUNCATE, 0.00013, TICK_SIZE) == '0' + assert exchange.decimal_to_precision('101.000123456700', ROUND, 100, TICK_SIZE) == '100' + assert exchange.decimal_to_precision('0.000123456700', ROUND, 100, TICK_SIZE) == '0' + assert exchange.decimal_to_precision('165', TRUNCATE, 110, TICK_SIZE) == '110' + assert exchange.decimal_to_precision('3210', TRUNCATE, 1110, TICK_SIZE) == '2220' + assert exchange.decimal_to_precision('165', ROUND, 110, TICK_SIZE) == '220' + assert exchange.decimal_to_precision('0.000123456789', ROUND, 1.2e-7, TICK_SIZE) == '0.00012348' + assert exchange.decimal_to_precision('0.000123456789', TRUNCATE, 1.2e-7, TICK_SIZE) == '0.00012336' + assert exchange.decimal_to_precision('0.000273398', ROUND, 1e-7, TICK_SIZE) == '0.0002734' + assert exchange.decimal_to_precision('0.00005714', TRUNCATE, 1e-8, TICK_SIZE) == '0.00005714' + # this line causes problems in JS, fix with Precise + # assert (exchange.decimalToPrecision ('0.0000571495257361', TRUNCATE, 0.00000001, TICK_SIZE) === '0.00005714'); + assert exchange.decimal_to_precision('0.01', ROUND, 0.0001, TICK_SIZE, PAD_WITH_ZERO) == '0.0100' + assert exchange.decimal_to_precision('0.01', TRUNCATE, 0.0001, TICK_SIZE, PAD_WITH_ZERO) == '0.0100' + assert exchange.decimal_to_precision('-0.000123456789', ROUND, 1.2e-7, TICK_SIZE) == '-0.00012348' + assert exchange.decimal_to_precision('-0.000123456789', TRUNCATE, 1.2e-7, TICK_SIZE) == '-0.00012336' + assert exchange.decimal_to_precision('-165', TRUNCATE, 110, TICK_SIZE) == '-110' + assert exchange.decimal_to_precision('-165', ROUND, 110, TICK_SIZE) == '-220' + assert exchange.decimal_to_precision('-1650', TRUNCATE, 1100, TICK_SIZE) == '-1100' + assert exchange.decimal_to_precision('-1650', ROUND, 1100, TICK_SIZE) == '-2200' + assert exchange.decimal_to_precision('0.0006', TRUNCATE, 0.0001, TICK_SIZE) == '0.0006' + assert exchange.decimal_to_precision('-0.0006', TRUNCATE, 0.0001, TICK_SIZE) == '-0.0006' + assert exchange.decimal_to_precision('0.6', TRUNCATE, 0.2, TICK_SIZE) == '0.6' + assert exchange.decimal_to_precision('-0.6', TRUNCATE, 0.2, TICK_SIZE) == '-0.6' + assert exchange.decimal_to_precision('1.2', ROUND, 0.4, TICK_SIZE) == '1.2' + assert exchange.decimal_to_precision('-1.2', ROUND, 0.4, TICK_SIZE) == '-1.2' + assert exchange.decimal_to_precision('1.2', ROUND, 0.02, TICK_SIZE) == '1.2' + assert exchange.decimal_to_precision('-1.2', ROUND, 0.02, TICK_SIZE) == '-1.2' + assert exchange.decimal_to_precision('44', ROUND, 4.4, TICK_SIZE) == '44' + assert exchange.decimal_to_precision('-44', ROUND, 4.4, TICK_SIZE) == '-44' + assert exchange.decimal_to_precision('44.00000001', ROUND, 4.4, TICK_SIZE) == '44' + assert exchange.decimal_to_precision('-44.00000001', ROUND, 4.4, TICK_SIZE) == '-44' + # https://github.com/ccxt/ccxt/issues/6731 + assert exchange.decimal_to_precision('20', TRUNCATE, 1e-8, TICK_SIZE) == '20' + # ---------------------------------------------------------------------------- + # testDecimalToPrecisionNegativeNumbers + assert exchange.decimal_to_precision('-0.123456', TRUNCATE, 5, DECIMAL_PLACES) == '-0.12345' + assert exchange.decimal_to_precision('-0.123456', ROUND, 5, DECIMAL_PLACES) == '-0.12346' + # ---------------------------------------------------------------------------- + # decimalToPrecision: without dot / trailing dot + assert exchange.decimal_to_precision('123', TRUNCATE, 0) == '123' + assert exchange.decimal_to_precision('123', TRUNCATE, 5, DECIMAL_PLACES) == '123' + assert exchange.decimal_to_precision('123', TRUNCATE, 5, DECIMAL_PLACES, PAD_WITH_ZERO) == '123.00000' + assert exchange.decimal_to_precision('123.', TRUNCATE, 0, DECIMAL_PLACES) == '123' + assert exchange.decimal_to_precision('123.', TRUNCATE, 5, DECIMAL_PLACES, PAD_WITH_ZERO) == '123.00000' + assert exchange.decimal_to_precision('0.', TRUNCATE, 0) == '0' + assert exchange.decimal_to_precision('0.', TRUNCATE, 5, DECIMAL_PLACES, PAD_WITH_ZERO) == '0.00000' + # ---------------------------------------------------------------------------- + # decimalToPrecision: rounding for equidistant digits + assert exchange.decimal_to_precision('1.44', ROUND, 1, DECIMAL_PLACES) == '1.4' + assert exchange.decimal_to_precision('1.45', ROUND, 1, DECIMAL_PLACES) == '1.5' + assert exchange.decimal_to_precision('1.45', ROUND, 0, DECIMAL_PLACES) == '1' # not 2 + # ---------------------------------------------------------------------------- + # negative precision only implemented so far in python + # pretty useless for decimal applications as anything |x| < 5 === 0 + # NO_PADDING and PAD_WITH_ZERO are ignored + assert exchange.decimal_to_precision('5', ROUND, -1, DECIMAL_PLACES) == '10' + assert exchange.decimal_to_precision('4.999', ROUND, -1, DECIMAL_PLACES) == '0' + assert exchange.decimal_to_precision('0.0431531423', ROUND, -1, DECIMAL_PLACES) == '0' + assert exchange.decimal_to_precision('-69.3', ROUND, -1, DECIMAL_PLACES) == '-70' + assert exchange.decimal_to_precision('5001', ROUND, -4, DECIMAL_PLACES) == '10000' + assert exchange.decimal_to_precision('4999.999', ROUND, -4, DECIMAL_PLACES) == '0' + assert exchange.decimal_to_precision('69.3', TRUNCATE, -2, DECIMAL_PLACES) == '0' + assert exchange.decimal_to_precision('-69.3', TRUNCATE, -2, DECIMAL_PLACES) == '0' + assert exchange.decimal_to_precision('69.3', TRUNCATE, -1, SIGNIFICANT_DIGITS) == '60' + assert exchange.decimal_to_precision('-69.3', TRUNCATE, -1, SIGNIFICANT_DIGITS) == '-60' + assert exchange.decimal_to_precision('69.3', TRUNCATE, -2, SIGNIFICANT_DIGITS) == '0' + assert exchange.decimal_to_precision('1602000000000000000000', TRUNCATE, 3, SIGNIFICANT_DIGITS) == '1600000000000000000000' + # ---------------------------------------------------------------------------- + # decimal_to_precision: stringified precision + assert exchange.decimal_to_precision('-0.000123456789', ROUND, '0.00000012', TICK_SIZE) == '-0.00012348' + assert exchange.decimal_to_precision('-0.000123456789', TRUNCATE, '0.00000012', TICK_SIZE) == '-0.00012336' + assert exchange.decimal_to_precision('-165', TRUNCATE, '110', TICK_SIZE) == '-110' + assert exchange.decimal_to_precision('-165', ROUND, '110', TICK_SIZE) == '-220' + # ---------------------------------------------------------------------------- + # testDecimalToPrecisionErrorHandling (todo) + # + # throws (() => + # decimalToPrecision ('123456.789', TRUNCATE, -2, DECIMAL_PLACES), + # 'negative precision is not yet supported') + # + # throws (() => + # decimalToPrecision ('foo'), + # "invalid number (contains an illegal character 'f')") + # + # throws (() => + # decimalToPrecision ('0.01', TRUNCATE, -1, TICK_SIZE), + # "TICK_SIZE cant be used with negative numPrecisionDigits") + # ---------------------------------------------------------------------------- + w = '-1.123e-6' + x = '0.00000002' + y = '69696900000' + z = '0' + a = '1e8' + assert Precise.string_mul(x, y) == '1393.938' + assert Precise.string_mul(y, x) == '1393.938' + assert Precise.string_add(x, y) == '69696900000.00000002' + assert Precise.string_add(y, x) == '69696900000.00000002' + assert Precise.string_sub(x, y) == '-69696899999.99999998' + assert Precise.string_sub(y, x) == '69696899999.99999998' + assert Precise.string_div(x, y, 1) == '0' + assert Precise.string_div(x, y) == '0' + assert Precise.string_div(x, y, 19) == '0.0000000000000000002' + assert Precise.string_div(x, y, 20) == '0.00000000000000000028' + assert Precise.string_div(x, y, 21) == '0.000000000000000000286' + assert Precise.string_div(x, y, 22) == '0.0000000000000000002869' + assert Precise.string_div(y, x) == '3484845000000000000' + assert Precise.string_mul(x, w) == '-0.00000000000002246' + assert Precise.string_mul(w, x) == '-0.00000000000002246' + assert Precise.string_add(x, w) == '-0.000001103' + assert Precise.string_add(w, x) == '-0.000001103' + assert Precise.string_sub(x, w) == '0.000001143' + assert Precise.string_sub(w, x) == '-0.000001143' + assert Precise.string_div(x, w) == '-0.017809439002671415' + assert Precise.string_div(w, x) == '-56.15' + assert Precise.string_mul(z, w) == '0' + assert Precise.string_mul(z, x) == '0' + assert Precise.string_mul(z, y) == '0' + assert Precise.string_mul(w, z) == '0' + assert Precise.string_mul(x, z) == '0' + assert Precise.string_mul(y, z) == '0' + assert Precise.string_add(z, w) == '-0.000001123' + assert Precise.string_add(z, x) == '0.00000002' + assert Precise.string_add(z, y) == '69696900000' + assert Precise.string_add(w, z) == '-0.000001123' + assert Precise.string_add(x, z) == '0.00000002' + assert Precise.string_add(y, z) == '69696900000' + assert Precise.string_mul(x, a) == '2' + assert Precise.string_mul(a, x) == '2' + assert Precise.string_mul(y, a) == '6969690000000000000' + assert Precise.string_mul(a, y) == '6969690000000000000' + assert Precise.string_div(y, a) == '696.969' + assert Precise.string_div(y, a, -1) == '690' + assert Precise.string_div(y, a, 0) == '696' + assert Precise.string_div(y, a, 1) == '696.9' + assert Precise.string_div(y, a, 2) == '696.96' + assert Precise.string_div(a, y) == '0.001434784043479695' + assert Precise.string_abs('0') == '0' + assert Precise.string_abs('-0') == '0' + assert Precise.string_abs('-500.1') == '500.1' + assert Precise.string_abs('213') == '213' + assert Precise.string_neg('0') == '0' + assert Precise.string_neg('-0') == '0' + assert Precise.string_neg('-500.1') == '500.1' + assert Precise.string_neg('213') == '-213' + assert Precise.string_mod('57.123', '10') == '7.123' + assert Precise.string_mod('18', '6') == '0' + assert Precise.string_mod('10.1', '0.5') == '0.1' + assert Precise.string_mod('10000000', '5555') == '1000' + assert Precise.string_mod('5550', '120') == '30' + assert Precise.string_equals('1.0000', '1') + assert Precise.string_equals('-0.0', '0') + assert Precise.string_equals('-0.0', '0.0') + assert Precise.string_equals('5.534000', '5.5340') + assert Precise.string_min('1.0000', '2') == '1' + assert Precise.string_min('2', '1.2345') == '1.2345' + assert Precise.string_min('3.1415', '-2') == '-2' + assert Precise.string_min('-3.1415', '-2') == '-3.1415' + assert Precise.string_min('0.000', '-0.0') == '0' + assert Precise.string_max('1.0000', '2') == '2' + assert Precise.string_max('2', '1.2345') == '2' + assert Precise.string_max('3.1415', '-2') == '3.1415' + assert Precise.string_max('-3.1415', '-2') == '-2' + assert Precise.string_max('0.000', '-0.0') == '0' + assert not Precise.string_gt('1.0000', '2') + assert Precise.string_gt('2', '1.2345') + assert Precise.string_gt('3.1415', '-2') + assert not Precise.string_gt('-3.1415', '-2') + assert not Precise.string_gt('3.1415', '3.1415') + assert Precise.string_gt('3.14150000000000000000001', '3.1415') + assert not Precise.string_ge('1.0000', '2') + assert Precise.string_ge('2', '1.2345') + assert Precise.string_ge('3.1415', '-2') + assert not Precise.string_ge('-3.1415', '-2') + assert Precise.string_ge('3.1415', '3.1415') + assert Precise.string_ge('3.14150000000000000000001', '3.1415') + assert Precise.string_lt('1.0000', '2') + assert not Precise.string_lt('2', '1.2345') + assert not Precise.string_lt('3.1415', '-2') + assert Precise.string_lt('-3.1415', '-2') + assert not Precise.string_lt('3.1415', '3.1415') + assert Precise.string_lt('3.1415', '3.14150000000000000000001') + assert Precise.string_le('1.0000', '2') + assert not Precise.string_le('2', '1.2345') + assert not Precise.string_le('3.1415', '-2') + assert Precise.string_le('-3.1415', '-2') + assert Precise.string_le('3.1415', '3.1415') + assert Precise.string_le('3.1415', '3.14150000000000000000001') diff --git a/ccxt/test/base/test_number_to_string.py b/ccxt/test/base/test_number_to_string.py new file mode 100644 index 0000000..c65622a --- /dev/null +++ b/ccxt/test/base/test_number_to_string.py @@ -0,0 +1,39 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.base.decimal_to_precision import number_to_string # noqa E402 +import ccxt # noqa: F402 + +def test_number_to_string(): + exchange = ccxt.Exchange({ + 'id': 'regirock', + }) + # ---------------------------------------------------------------------------- + # numberToString + assert exchange.number_to_string(-7.8e-7) == '-0.00000078' + assert exchange.number_to_string(7.8e-7) == '0.00000078' + assert exchange.number_to_string(-0.0000017805) == '-0.0000017805' + assert exchange.number_to_string(0.0000017805) == '0.0000017805' + assert exchange.number_to_string(-7.0005e+27) == '-7000500000000000000000000000' + assert exchange.number_to_string(7.0005e+27) == '7000500000000000000000000000' + assert exchange.number_to_string(-7.9e+27) == '-7900000000000000000000000000' + assert exchange.number_to_string(7e+27) == '7000000000000000000000000000' + assert exchange.number_to_string(7.9e+27) == '7900000000000000000000000000' + assert exchange.number_to_string(-12.345) == '-12.345' + assert exchange.number_to_string(12.345) == '12.345' + assert exchange.number_to_string(0) == '0' + assert exchange.number_to_string(7.35946e+21) == '7359460000000000000000' + assert exchange.number_to_string(1e-8) == '0.00000001' + assert exchange.number_to_string(1e-7) == '0.0000001' + assert exchange.number_to_string(-1e-7) == '-0.0000001' diff --git a/ccxt/test/base/test_omit.py b/ccxt/test/base/test_omit.py new file mode 100644 index 0000000..0f8880d --- /dev/null +++ b/ccxt/test/base/test_omit.py @@ -0,0 +1,37 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +import ccxt # noqa: F402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +def test_omit(): + exchange = ccxt.Exchange({ + 'id': 'sampleexchange', + }) + test_shared_methods.assert_deep_equal(exchange, None, 'testOmit', exchange.omit({}, 'foo'), {}) + test_shared_methods.assert_deep_equal(exchange, None, 'testOmit', exchange.omit({ + 'foo': 2, + }, 'foo'), {}) + test_shared_methods.assert_deep_equal(exchange, None, 'testOmit', exchange.omit({ + 'foo': 2, + 'bar': 3, + }, 'foo'), { + 'bar': 3, + }) + test_shared_methods.assert_deep_equal(exchange, None, 'testOmit', exchange.omit({ + 'foo': 2, + 'bar': 3, + }, ['foo']), { + 'bar': 3, + }) diff --git a/ccxt/test/base/test_parse_precision.py b/ccxt/test/base/test_parse_precision.py new file mode 100644 index 0000000..632fb5c --- /dev/null +++ b/ccxt/test/base/test_parse_precision.py @@ -0,0 +1,24 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +import ccxt # noqa: F402 + +def test_parse_precision(): + exchange = ccxt.Exchange({ + 'id': 'sampleexchange', + }) + assert exchange.parse_precision('15') == '0.000000000000001' + assert exchange.parse_precision('1') == '0.1' + assert exchange.parse_precision('0') == '1' + assert exchange.parse_precision('-5') == '100000' diff --git a/ccxt/test/base/test_precise.py b/ccxt/test/base/test_precise.py new file mode 100644 index 0000000..12ed6ff --- /dev/null +++ b/ccxt/test/base/test_precise.py @@ -0,0 +1,116 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.base.precise import Precise # noqa E402 + +def test_precise(): + w = '-1.123e-6' + x = '0.00000002' + y = '69696900000' + z = '0' + a = '1e8' + assert Precise.string_mul(x, y) == '1393.938' + assert Precise.string_mul(y, x) == '1393.938' + assert Precise.string_add(x, y) == '69696900000.00000002' + assert Precise.string_add(y, x) == '69696900000.00000002' + assert Precise.string_sub(x, y) == '-69696899999.99999998' + assert Precise.string_sub(y, x) == '69696899999.99999998' + assert Precise.string_div(x, y, 1) == '0' + assert Precise.string_div(x, y) == '0' + assert Precise.string_div(x, y, 19) == '0.0000000000000000002' + assert Precise.string_div(x, y, 20) == '0.00000000000000000028' + assert Precise.string_div(x, y, 21) == '0.000000000000000000286' + assert Precise.string_div(x, y, 22) == '0.0000000000000000002869' + assert Precise.string_div(y, x) == '3484845000000000000' + assert Precise.string_mul(x, w) == '-0.00000000000002246' + assert Precise.string_mul(w, x) == '-0.00000000000002246' + assert Precise.string_add(x, w) == '-0.000001103' + assert Precise.string_add(w, x) == '-0.000001103' + assert Precise.string_sub(x, w) == '0.000001143' + assert Precise.string_sub(w, x) == '-0.000001143' + assert Precise.string_div(x, w) == '-0.017809439002671415' + assert Precise.string_div(w, x) == '-56.15' + assert Precise.string_mul(z, w) == '0' + assert Precise.string_mul(z, x) == '0' + assert Precise.string_mul(z, y) == '0' + assert Precise.string_mul(w, z) == '0' + assert Precise.string_mul(x, z) == '0' + assert Precise.string_mul(y, z) == '0' + assert Precise.string_add(z, w) == '-0.000001123' + assert Precise.string_add(z, x) == '0.00000002' + assert Precise.string_add(z, y) == '69696900000' + assert Precise.string_add(w, z) == '-0.000001123' + assert Precise.string_add(x, z) == '0.00000002' + assert Precise.string_add(y, z) == '69696900000' + assert Precise.string_mul(x, a) == '2' + assert Precise.string_mul(a, x) == '2' + assert Precise.string_mul(y, a) == '6969690000000000000' + assert Precise.string_mul(a, y) == '6969690000000000000' + assert Precise.string_div(y, a) == '696.969' + assert Precise.string_div(y, a, -1) == '690' + assert Precise.string_div(y, a, 0) == '696' + assert Precise.string_div(y, a, 1) == '696.9' + assert Precise.string_div(y, a, 2) == '696.96' + assert Precise.string_div(a, y) == '0.001434784043479695' + assert Precise.string_abs('0') == '0' + assert Precise.string_abs('-0') == '0' + assert Precise.string_abs('-500.1') == '500.1' + assert Precise.string_abs('213') == '213' + assert Precise.string_neg('0') == '0' + assert Precise.string_neg('-0') == '0' + assert Precise.string_neg('-500.1') == '500.1' + assert Precise.string_neg('213') == '-213' + assert Precise.string_mod('57.123', '10') == '7.123' + assert Precise.string_mod('18', '6') == '0' + assert Precise.string_mod('10.1', '0.5') == '0.1' + assert Precise.string_mod('10000000', '5555') == '1000' + assert Precise.string_mod('5550', '120') == '30' + assert Precise.string_equals('1.0000', '1') + assert Precise.string_equals('-0.0', '0') + assert Precise.string_equals('-0.0', '0.0') + assert Precise.string_equals('5.534000', '5.5340') + assert Precise.string_min('1.0000', '2') == '1' + assert Precise.string_min('2', '1.2345') == '1.2345' + assert Precise.string_min('3.1415', '-2') == '-2' + assert Precise.string_min('-3.1415', '-2') == '-3.1415' + assert Precise.string_min('0.000', '-0.0') == '0' + assert Precise.string_max('1.0000', '2') == '2' + assert Precise.string_max('2', '1.2345') == '2' + assert Precise.string_max('3.1415', '-2') == '3.1415' + assert Precise.string_max('-3.1415', '-2') == '-2' + assert Precise.string_max('0.000', '-0.0') == '0' + assert not Precise.string_gt('1.0000', '2') + assert Precise.string_gt('2', '1.2345') + assert Precise.string_gt('3.1415', '-2') + assert not Precise.string_gt('-3.1415', '-2') + assert not Precise.string_gt('3.1415', '3.1415') + assert Precise.string_gt('3.14150000000000000000001', '3.1415') + assert not Precise.string_ge('1.0000', '2') + assert Precise.string_ge('2', '1.2345') + assert Precise.string_ge('3.1415', '-2') + assert not Precise.string_ge('-3.1415', '-2') + assert Precise.string_ge('3.1415', '3.1415') + assert Precise.string_ge('3.14150000000000000000001', '3.1415') + assert Precise.string_lt('1.0000', '2') + assert not Precise.string_lt('2', '1.2345') + assert not Precise.string_lt('3.1415', '-2') + assert Precise.string_lt('-3.1415', '-2') + assert not Precise.string_lt('3.1415', '3.1415') + assert Precise.string_lt('3.1415', '3.14150000000000000000001') + assert Precise.string_le('1.0000', '2') + assert not Precise.string_le('2', '1.2345') + assert not Precise.string_le('3.1415', '-2') + assert Precise.string_le('-3.1415', '-2') + assert Precise.string_le('3.1415', '3.1415') + assert Precise.string_le('3.1415', '3.14150000000000000000001') diff --git a/ccxt/test/base/test_remove_repeated_elements_from_array.py b/ccxt/test/base/test_remove_repeated_elements_from_array.py new file mode 100644 index 0000000..4a09c0a --- /dev/null +++ b/ccxt/test/base/test_remove_repeated_elements_from_array.py @@ -0,0 +1,74 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +import ccxt # noqa: F402 + +def test_remove_repeated_elements_from_array(): + exchange = ccxt.Exchange({ + 'id': 'sampleexchange', + }) + # CASE 1: by id + array1 = [{ + 'id': 'a', + 'timestamp': 1, + 'uniq': 'x1', +}, { + 'id': 'b', + 'timestamp': 2, + 'uniq': 'x2', +}, { + 'id': 'a', + 'timestamp': 3, + 'uniq': 'x3', +}, { + 'id': 'c', + 'timestamp': 1, + 'uniq': 'x4', +}] + res1 = exchange.remove_repeated_elements_from_array(array1, False) + res1_length = len(res1) + assert res1_length == 3 + assert res1[0]['uniq'] == 'x1' + assert res1[1]['uniq'] == 'x2' + assert res1[2]['uniq'] == 'x4' + # CASE 2: by timestamp + array2 = [{ + 'id': None, + 'timestamp': 1, + 'uniq': 'x1', +}, { + 'id': None, + 'timestamp': 2, + 'uniq': 'x2', +}, { + 'id': None, + 'timestamp': 1, + 'uniq': 'x3', +}, { + 'id': None, + 'timestamp': 3, + 'uniq': 'x4', +}] + res2 = exchange.remove_repeated_elements_from_array(array2, True) + res2_length = len(res2) + assert res2_length == 3 + assert res2[0]['uniq'] == 'x1' + assert res2[1]['uniq'] == 'x2' + assert res2[2]['uniq'] == 'x4' + # CASE 3: by timestamp index (used in ohlcv) + array3 = [[555, 1, 1, 'x1'], [666, 1, 1, 'x2'], [555, 1, 1, 'x3']] + res3 = exchange.remove_repeated_elements_from_array(array3, True) + assert len(res3) == 2 + assert res3[0][3] == 'x1' + assert res3[1][3] == 'x2' diff --git a/ccxt/test/base/test_safe_methods.py b/ccxt/test/base/test_safe_methods.py new file mode 100644 index 0000000..41d52b6 --- /dev/null +++ b/ccxt/test/base/test_safe_methods.py @@ -0,0 +1,275 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +import ccxt # noqa: F402 + +def equals(a, b): + return a == b + +def test_safe_methods(): + exchange = ccxt.Exchange({ + 'id': 'regirock', + }) + input_dict = { + 'i': 1, + 'f': 0.123, + 'bool': True, + 'list': [1, 2, 3], + 'dict': { + 'a': 1, + }, + 'str': 'heLlo', + 'strNumber': '3', + 'zeroNumeric': 0, + 'zeroString': '0', + 'undefined': None, + 'emptyString': '', + 'floatNumeric': 0.123, + 'floatString': '0.123', + } + input_list = ['Hi', 2] + compare_dict = { + 'a': 1, + } + compare_list = [1, 2, 3] + factor = 10 + # safeValue + assert exchange.safe_value(input_dict, 'i') == 1 + assert exchange.safe_value(input_dict, 'f') == 0.123 + assert exchange.safe_value(input_dict, 'bool') + assert equals(exchange.safe_value(input_dict, 'list'), compare_list) + dict_object = exchange.safe_value(input_dict, 'dict') + assert equals(dict_object, compare_dict) + assert exchange.safe_value(input_dict, 'str') == 'heLlo' + assert exchange.safe_value(input_dict, 'strNumber') == '3' + assert exchange.safe_value(input_list, 0) == 'Hi' + # safeValue2 + assert exchange.safe_value_2(input_dict, 'a', 'i') == 1 + assert exchange.safe_value_2(input_dict, 'a', 'f') == 0.123 + assert exchange.safe_value_2(input_dict, 'a', 'bool') + assert equals(exchange.safe_value_2(input_dict, 'a', 'list'), compare_list) + dict_object = exchange.safe_value_2(input_dict, 'a', 'dict') + assert equals(dict_object, compare_dict) + assert exchange.safe_value_2(input_dict, 'a', 'str') == 'heLlo' + assert exchange.safe_value_2(input_dict, 'a', 'strNumber') == '3' + assert exchange.safe_value_2(input_list, 2, 0) == 'Hi' + # safeValueN + assert exchange.safe_value_n(input_dict, ['a', 'b', 'i']) == 1 + assert exchange.safe_value_n(input_dict, ['a', 'b', 'f']) == 0.123 + assert exchange.safe_value_n(input_dict, ['a', 'b', 'bool']) + assert equals(exchange.safe_value_n(input_dict, ['a', 'b', 'list']), compare_list) + dict_object = exchange.safe_value_n(input_dict, ['a', 'b', 'dict']) + assert equals(dict_object, compare_dict) + assert exchange.safe_value_n(input_dict, ['a', 'b', 'str']) == 'heLlo' + assert exchange.safe_value_n(input_dict, ['a', 'b', 'strNumber']) == '3' + assert exchange.safe_value_n(input_list, [3, 2, 0]) == 'Hi' + # safeDict + dict_object = exchange.safe_dict(input_dict, 'dict') + assert equals(dict_object, compare_dict) + list_object = exchange.safe_dict(input_dict, 'list') + assert list_object is None + assert exchange.safe_dict(input_list, 1) is None + # safeDict2 + dict_object = exchange.safe_dict_2(input_dict, 'a', 'dict') + assert equals(dict_object, compare_dict) + list_object = exchange.safe_dict_2(input_dict, 'a', 'list') + assert list_object is None + # @ts-expect-error + assert exchange.safe_dict_2(input_list, 2, 1) is None + # safeDictN + dict_object = exchange.safe_dict_n(input_dict, ['a', 'b', 'dict']) + assert equals(dict_object, compare_dict) + list_object = exchange.safe_dict_n(input_dict, ['a', 'b', 'list']) + assert list_object is None + assert exchange.safe_dict_n(input_list, [3, 2, 1]) is None + # safeList + list_object = exchange.safe_list(input_dict, 'list') + assert equals(dict_object, compare_dict) + assert exchange.safe_list(input_dict, 'dict') is None + assert exchange.safe_list(input_list, 1) is None + # safeList2 + list_object = exchange.safe_list_2(input_dict, 'a', 'list') + assert equals(dict_object, compare_dict) + assert exchange.safe_list_2(input_dict, 'a', 'dict') is None + # @ts-expect-error + assert exchange.safe_list_2(input_list, 2, 1) is None + # safeListN + list_object = exchange.safe_list_n(input_dict, ['a', 'b', 'list']) + assert equals(dict_object, compare_dict) + assert exchange.safe_list_n(input_dict, ['a', 'b', 'dict']) is None + assert exchange.safe_list_n(input_list, [3, 2, 1]) is None + # safeString + assert exchange.safe_string(input_dict, 'i') == '1' + assert exchange.safe_string(input_dict, 'f') == '0.123' + # assert (exchange.safeString (inputDict, 'bool') === 'true'); returns True in python and 'true' in js + assert exchange.safe_string(input_dict, 'str') == 'heLlo' + assert exchange.safe_string(input_dict, 'strNumber') == '3' + assert exchange.safe_string(input_list, 0) == 'Hi' + # safeString2 + assert exchange.safe_string_2(input_dict, 'a', 'i') == '1' + assert exchange.safe_string_2(input_dict, 'a', 'f') == '0.123' + assert exchange.safe_string_2(input_dict, 'a', 'str') == 'heLlo' + assert exchange.safe_string_2(input_dict, 'a', 'strNumber') == '3' + assert exchange.safe_string_2(input_list, 2, 0) == 'Hi' + # safeStringN + assert exchange.safe_string_n(input_dict, ['a', 'b', 'i']) == '1' + assert exchange.safe_string_n(input_dict, ['a', 'b', 'f']) == '0.123' + assert exchange.safe_string_n(input_dict, ['a', 'b', 'str']) == 'heLlo' + assert exchange.safe_string_n(input_dict, ['a', 'b', 'strNumber']) == '3' + assert exchange.safe_string_n(input_list, [3, 2, 0]) == 'Hi' + # safeStringLower + assert exchange.safe_string_lower(input_dict, 'i') == '1' + assert exchange.safe_string_lower(input_dict, 'f') == '0.123' + assert exchange.safe_string_lower(input_dict, 'str') == 'hello' + assert exchange.safe_string_lower(input_dict, 'strNumber') == '3' + assert exchange.safe_string_lower(input_list, 0) == 'hi' + # safeStringLower2 + assert exchange.safe_string_lower_2(input_dict, 'a', 'i') == '1' + assert exchange.safe_string_lower_2(input_dict, 'a', 'f') == '0.123' + assert exchange.safe_string_lower_2(input_dict, 'a', 'str') == 'hello' + assert exchange.safe_string_lower_2(input_dict, 'a', 'strNumber') == '3' + assert exchange.safe_string_lower_2(input_list, 2, 0) == 'hi' + # safeStringLowerN + assert exchange.safe_string_lower_n(input_dict, ['a', 'b', 'i']) == '1' + assert exchange.safe_string_lower_n(input_dict, ['a', 'b', 'f']) == '0.123' + assert exchange.safe_string_lower_n(input_dict, ['a', 'b', 'str']) == 'hello' + assert exchange.safe_string_lower_n(input_dict, ['a', 'b', 'strNumber']) == '3' + assert exchange.safe_string_lower_n(input_list, [3, 2, 0]) == 'hi' + # safeStringUpper + assert exchange.safe_string_upper(input_dict, 'i') == '1' + assert exchange.safe_string_upper(input_dict, 'f') == '0.123' + assert exchange.safe_string_upper(input_dict, 'str') == 'HELLO' + assert exchange.safe_string_upper(input_dict, 'strNumber') == '3' + assert exchange.safe_string_upper(input_list, 0) == 'HI' + # safeStringUpper2 + assert exchange.safe_string_upper_2(input_dict, 'a', 'i') == '1' + assert exchange.safe_string_upper_2(input_dict, 'a', 'f') == '0.123' + assert exchange.safe_string_upper_2(input_dict, 'a', 'str') == 'HELLO' + assert exchange.safe_string_upper_2(input_dict, 'a', 'strNumber') == '3' + assert exchange.safe_string_upper_2(input_list, 2, 0) == 'HI' + # safeStringUpperN + assert exchange.safe_string_upper_n(input_dict, ['a', 'b', 'i']) == '1' + assert exchange.safe_string_upper_n(input_dict, ['a', 'b', 'f']) == '0.123' + assert exchange.safe_string_upper_n(input_dict, ['a', 'b', 'str']) == 'HELLO' + assert exchange.safe_string_upper_n(input_dict, ['a', 'b', 'strNumber']) == '3' + assert exchange.safe_string_upper_n(input_list, [3, 2, 0]) == 'HI' + # safeInteger + assert exchange.safe_integer(input_dict, 'i') == 1 + assert exchange.safe_integer(input_dict, 'f') == 0 + assert exchange.safe_integer(input_dict, 'strNumber') == 3 + assert exchange.safe_integer(input_list, 1) == 2 + # safeInteger2 + assert exchange.safe_integer_2(input_dict, 'a', 'i') == 1 + assert exchange.safe_integer_2(input_dict, 'a', 'f') == 0 + assert exchange.safe_integer_2(input_dict, 'a', 'strNumber') == 3 + assert exchange.safe_integer_2(input_list, 2, 1) == 2 + # safeIntegerN + assert exchange.safe_integer_n(input_dict, ['a', 'b', 'i']) == 1 + assert exchange.safe_integer_n(input_dict, ['a', 'b', 'f']) == 0 + assert exchange.safe_integer_n(input_dict, ['a', 'b', 'strNumber']) == 3 + assert exchange.safe_integer_n(input_list, [3, 2, 1]) == 2 + # safeIntegerOmitZero + assert exchange.safe_integer_omit_zero(input_dict, 'i') == 1 + assert exchange.safe_integer_omit_zero(input_dict, 'f') is None + assert exchange.safe_integer_omit_zero(input_dict, 'strNumber') == 3 + assert exchange.safe_integer_omit_zero(input_list, 1) == 2 + # safeIntegerProduct + assert exchange.safe_integer_product(input_dict, 'i', factor) == 10 + assert exchange.safe_integer_product(input_dict, 'f', factor) == 1 # NB the result is 1 + assert exchange.safe_integer_product(input_dict, 'strNumber', factor) == 30 + assert exchange.safe_integer_product(input_list, 1, factor) == 20 + # safeIntegerProduct2 + assert exchange.safe_integer_product_2(input_dict, 'a', 'i', factor) == 10 + assert exchange.safe_integer_product_2(input_dict, 'a', 'f', factor) == 1 # NB the result is 1 + assert exchange.safe_integer_product_2(input_dict, 'a', 'strNumber', factor) == 30 + assert exchange.safe_integer_product_2(input_list, 2, 1, factor) == 20 + # safeIntegerProductN + assert exchange.safe_integer_product_n(input_dict, ['a', 'b', 'i'], factor) == 10 + assert exchange.safe_integer_product_n(input_dict, ['a', 'b', 'f'], factor) == 1 # NB the result is 1 + assert exchange.safe_integer_product_n(input_dict, ['a', 'b', 'strNumber'], factor) == 30 + assert exchange.safe_integer_product_n(input_list, [3, 2, 1], factor) == 20 + # safeTimestamp + assert exchange.safe_timestamp(input_dict, 'i') == 1000 + assert exchange.safe_timestamp(input_dict, 'f') == 123 + assert exchange.safe_timestamp(input_dict, 'strNumber') == 3000 + assert exchange.safe_timestamp(input_list, 1) == 2000 + # safeTimestamp2 + assert exchange.safe_timestamp_2(input_dict, 'a', 'i') == 1000 + assert exchange.safe_timestamp_2(input_dict, 'a', 'f') == 123 + assert exchange.safe_timestamp_2(input_dict, 'a', 'strNumber') == 3000 + assert exchange.safe_timestamp_2(input_list, 2, 1) == 2000 + # safeTimestampN + assert exchange.safe_timestamp_n(input_dict, ['a', 'b', 'i']) == 1000 + assert exchange.safe_timestamp_n(input_dict, ['a', 'b', 'f']) == 123 + assert exchange.safe_timestamp_n(input_dict, ['a', 'b', 'strNumber']) == 3000 + assert exchange.safe_timestamp_n(input_list, [3, 2, 1]) == 2000 + # safeFloat + # @ts-expect-error + assert exchange.safe_float(input_dict, 'i') == float(1) + assert exchange.safe_float(input_dict, 'f') == 0.123 + # @ts-expect-error + assert exchange.safe_float(input_dict, 'strNumber') == float(3) + # @ts-expect-error + assert exchange.safe_float(input_list, 1) == float(2) + # safeFloat2 + # @ts-expect-error + assert exchange.safe_float_2(input_dict, 'a', 'i') == float(1) + assert exchange.safe_float_2(input_dict, 'a', 'f') == 0.123 + # @ts-expect-error + assert exchange.safe_float_2(input_dict, 'a', 'strNumber') == float(3) + # @ts-expect-error + assert exchange.safe_float_2(input_list, 2, 1) == float(2) + # safeFloatN + # @ts-expect-error + assert exchange.safe_float_n(input_dict, ['a', 'b', 'i']) == float(1) + assert exchange.safe_float_n(input_dict, ['a', 'b', 'f']) == 0.123 + # @ts-expect-error + assert exchange.safe_float_n(input_dict, ['a', 'b', 'strNumber']) == float(3) + # @ts-expect-error + assert exchange.safe_float_n(input_list, [3, 2, 1]) == float(2) + # safeNumber + assert exchange.safe_number(input_dict, 'i') == exchange.parse_number(1) + assert exchange.safe_number(input_dict, 'f') == exchange.parse_number(0.123) + assert exchange.safe_number(input_dict, 'strNumber') == exchange.parse_number(3) + assert exchange.safe_number(input_list, 1) == exchange.parse_number(2) + assert exchange.safe_number(input_list, 'bool') is None + assert exchange.safe_number(input_list, 'list') is None + assert exchange.safe_number(input_list, 'dict') is None + assert exchange.safe_number(input_list, 'str') is None + # safeNumber2 + assert exchange.safe_number_2(input_dict, 'a', 'i') == exchange.parse_number(1) + assert exchange.safe_number_2(input_dict, 'a', 'f') == exchange.parse_number(0.123) + assert exchange.safe_number_2(input_dict, 'a', 'strNumber') == exchange.parse_number(3) + assert exchange.safe_number_2(input_list, 2, 1) == exchange.parse_number(2) + # safeNumberN + assert exchange.safe_number_n(input_dict, ['a', 'b', 'i']) == exchange.parse_number(1) + assert exchange.safe_number_n(input_dict, ['a', 'b', 'f']) == exchange.parse_number(0.123) + assert exchange.safe_number_n(input_dict, ['a', 'b', 'strNumber']) == exchange.parse_number(3) + assert exchange.safe_number_n(input_list, [3, 2, 1]) == exchange.parse_number(2) + # safeBool + assert exchange.safe_bool(input_dict, 'bool') + assert exchange.safe_bool(input_list, 1) is None + # safeBool2 + assert exchange.safe_bool_2(input_dict, 'a', 'bool') + assert exchange.safe_bool_2(input_list, 2, 1) is None + # safeBoolN + assert exchange.safe_bool_n(input_dict, ['a', 'b', 'bool']) + assert exchange.safe_bool_n(input_list, [3, 2, 1]) is None + # safeNumberOmitZero + assert exchange.safe_number_omit_zero(input_dict, 'zeroNumeric') is None + assert exchange.safe_number_omit_zero(input_dict, 'zeroString') is None + assert exchange.safe_number_omit_zero(input_dict, 'undefined') is None + assert exchange.safe_number_omit_zero(input_dict, 'emptyString') is None + assert exchange.safe_number_omit_zero(input_dict, 'floatNumeric') is not None + assert exchange.safe_number_omit_zero(input_dict, 'floatString') is not None diff --git a/ccxt/test/base/test_safe_ticker.py b/ccxt/test/base/test_safe_ticker.py new file mode 100644 index 0000000..97cceda --- /dev/null +++ b/ccxt/test/base/test_safe_ticker.py @@ -0,0 +1,137 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +import ccxt # noqa: F402 +from ccxt.base.precise import Precise # noqa E402 + +def precise_equal_str(exchange, result, key, expected): + return Precise.string_eq(exchange.safe_string(result, key), expected) + + +def test_safe_ticker(): + exchange = ccxt.Exchange({ + 'id': 'sampleexchange', + }) + # CASE 1 - by open + ticker1 = { + 'open': 5, + 'change': 1, + } + result1 = exchange.safe_ticker(ticker1) + assert precise_equal_str(exchange, result1, 'percentage', '20.0') + assert precise_equal_str(exchange, result1, 'average', '5.5') + assert precise_equal_str(exchange, result1, 'close', '6.0') + assert precise_equal_str(exchange, result1, 'last', '6.0') + # CASE 2 - by open + ticker2 = { + 'open': 5, + 'percentage': 20, + } + result2 = exchange.safe_ticker(ticker2) + assert precise_equal_str(exchange, result2, 'change', '1.0') + assert precise_equal_str(exchange, result2, 'average', '5.5') + assert precise_equal_str(exchange, result2, 'close', '6.0') + assert precise_equal_str(exchange, result2, 'last', '6.0') + # CASE 3 - by close + ticker3 = { + 'close': 6, + 'change': 1, + } + result3 = exchange.safe_ticker(ticker3) + assert precise_equal_str(exchange, result3, 'open', '5.0') + assert precise_equal_str(exchange, result3, 'percentage', '20.0') + assert precise_equal_str(exchange, result3, 'average', '5.5') + assert precise_equal_str(exchange, result3, 'last', '6.0') + # CASE 4 - by close + ticker4 = { + 'close': 6, + 'percentage': 20, + } + result4 = exchange.safe_ticker(ticker4) + assert precise_equal_str(exchange, result4, 'open', '5.0') + assert precise_equal_str(exchange, result4, 'change', '1.0') + assert precise_equal_str(exchange, result4, 'average', '5.5') + assert precise_equal_str(exchange, result4, 'last', '6.0') + # CASE 5 - by average + ticker5 = { + 'average': 5.5, + 'percentage': 20, + } + result5 = exchange.safe_ticker(ticker5) + assert precise_equal_str(exchange, result5, 'open', '5.0') + assert precise_equal_str(exchange, result5, 'change', '1.0') + assert precise_equal_str(exchange, result5, 'close', '6.0') + assert precise_equal_str(exchange, result5, 'last', '6.0') + # CASE 6 + ticker6 = { + 'average': 5.5, + 'change': 1, + } + result6 = exchange.safe_ticker(ticker6) + assert precise_equal_str(exchange, result6, 'open', '5.0') + assert precise_equal_str(exchange, result6, 'percentage', '20.0') + assert precise_equal_str(exchange, result6, 'close', '6.0') + assert precise_equal_str(exchange, result6, 'last', '6.0') + # CASE 7 - by open and close + ticker7 = { + 'open': 5, + 'close': 6, + } + result7 = exchange.safe_ticker(ticker7) + assert precise_equal_str(exchange, result7, 'change', '1.0') + assert precise_equal_str(exchange, result7, 'percentage', '20.0') + assert precise_equal_str(exchange, result7, 'average', '5.5') + assert precise_equal_str(exchange, result7, 'last', '6.0') + # CASE 8 - full ticker + ticker8 = { + 'open': 5, + 'close': 6, + 'last': 6, + 'high': 6.5, + 'low': 4.5, + 'average': 5.5, + 'bid': 5.9, + 'bidVolume': 100, + 'ask': 6.1, + 'askVolume': 200, + 'change': 1, + 'percentage': 20, + 'vwap': 5.75, + 'baseVolume': 1000, + 'quoteVolume': 5750, + 'previousClose': 4.9, + 'indexPrice': 5.8, + 'markPrice': 5.9, + 'info': {}, + } + result8 = exchange.safe_ticker(ticker8) + assert precise_equal_str(exchange, result8, 'open', '5.0') + assert precise_equal_str(exchange, result8, 'high', '6.5') + assert precise_equal_str(exchange, result8, 'low', '4.5') + assert precise_equal_str(exchange, result8, 'close', '6.0') + assert precise_equal_str(exchange, result8, 'last', '6.0') + assert precise_equal_str(exchange, result8, 'change', '1.0') + assert precise_equal_str(exchange, result8, 'percentage', '20.0') + assert precise_equal_str(exchange, result8, 'average', '5.5') + assert precise_equal_str(exchange, result8, 'bid', '5.9') + assert precise_equal_str(exchange, result8, 'bidVolume', '100.0') + assert precise_equal_str(exchange, result8, 'ask', '6.1') + assert precise_equal_str(exchange, result8, 'askVolume', '200.0') + assert precise_equal_str(exchange, result8, 'vwap', '5.75') + assert precise_equal_str(exchange, result8, 'baseVolume', '1000.0') + assert precise_equal_str(exchange, result8, 'quoteVolume', '5750.0') + assert precise_equal_str(exchange, result8, 'previousClose', '4.9') + assert precise_equal_str(exchange, result8, 'indexPrice', '5.8') + assert precise_equal_str(exchange, result8, 'markPrice', '5.9') + assert result8['info'] is not None diff --git a/ccxt/test/base/test_sort.py b/ccxt/test/base/test_sort.py new file mode 100644 index 0000000..34dd319 --- /dev/null +++ b/ccxt/test/base/test_sort.py @@ -0,0 +1,25 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +import ccxt # noqa: F402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +def test_sort(): + # todo: other argument checks + exchange = ccxt.Exchange({ + 'id': 'sampleexchange', + }) + arr = ['b', 'a', 'c', 'd'] + sorted_arr = exchange.sort(arr) + test_shared_methods.assert_deep_equal(exchange, None, 'sort', sorted_arr, ['a', 'b', 'c', 'd']) diff --git a/ccxt/test/base/test_sort_by.py b/ccxt/test/base/test_sort_by.py new file mode 100644 index 0000000..ebd68fe --- /dev/null +++ b/ccxt/test/base/test_sort_by.py @@ -0,0 +1,65 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +import ccxt # noqa: F402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +def test_sort_by(): + # todo: other argument checks + exchange = ccxt.Exchange({ + 'id': 'sampleexchange', + }) + arr = [{ + 'x': 5, +}, { + 'x': 2, +}, { + 'x': 4, +}, { + 'x': 0, +}, { + 'x': 1, +}, { + 'x': 3, +}] + new_array = exchange.sort_by(arr, 'x') + test_shared_methods.assert_deep_equal(exchange, None, 'sortBy', new_array, [{ + 'x': 0, +}, { + 'x': 1, +}, { + 'x': 2, +}, { + 'x': 3, +}, { + 'x': 4, +}, { + 'x': 5, +}]) + new_array_descending = exchange.sort_by(arr, 'x', True) + test_shared_methods.assert_deep_equal(exchange, None, 'sortBy', new_array_descending, [{ + 'x': 5, +}, { + 'x': 4, +}, { + 'x': 3, +}, { + 'x': 2, +}, { + 'x': 1, +}, { + 'x': 0, +}]) + empty_array = exchange.sort_by([], 'x') + test_shared_methods.assert_deep_equal(exchange, None, 'sortBy', empty_array, []) diff --git a/ccxt/test/base/test_sum.py b/ccxt/test/base/test_sum.py new file mode 100644 index 0000000..6f892b2 --- /dev/null +++ b/ccxt/test/base/test_sum.py @@ -0,0 +1,24 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +import ccxt # noqa: F402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +def test_sum(): + exchange = ccxt.Exchange({ + 'id': 'sampleexchange', + }) + # testSharedMethods.assertDeepEqual (exchange, undefined, 'testSum', exchange.sum (), undefined); # todo: bugs in py + test_shared_methods.assert_deep_equal(exchange, None, 'testSum', exchange.sum(2), 2) + test_shared_methods.assert_deep_equal(exchange, None, 'testSum', exchange.sum(2, 30, 400), 432) diff --git a/ccxt/test/base/tests_init.py b/ccxt/test/base/tests_init.py new file mode 100644 index 0000000..dbba510 --- /dev/null +++ b/ccxt/test/base/tests_init.py @@ -0,0 +1,60 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.base.decimal_to_precision import decimal_to_precision # noqa E402 +from ccxt.base.decimal_to_precision import number_to_string # noqa E402 +from ccxt.base.precise import Precise # noqa E402 +from ccxt.test.base.test_decimal_to_precision import test_decimal_to_precision # noqa E402 +from ccxt.test.base.test_number_to_string import test_number_to_string # noqa E402 +from ccxt.test.base.test_precise import test_precise # noqa E402 +from ccxt.test.base.test_datetime import test_datetime # noqa E402 +from ccxt.test.base.test_cryptography import test_cryptography # noqa E402 +from ccxt.test.base.test_extend import test_extend # noqa E402 +from ccxt.test.base.test_deep_extend import test_deep_extend # noqa E402 +from ccxt.test.base.language_specific.test_language_specific import test_language_specific # noqa E402 +from ccxt.test.base.test_safe_methods import test_safe_methods # noqa E402 +from ccxt.test.base.test_safe_ticker import test_safe_ticker # noqa E402 +from ccxt.test.base.test_sort_by import test_sort_by # noqa E402 +from ccxt.test.base.test_sum import test_sum # noqa E402 +from ccxt.test.base.test_omit import test_omit # noqa E402 +from ccxt.test.base.test_group_by import test_group_by # noqa E402 +from ccxt.test.base.test_filter_by import test_filter_by # noqa E402 +from ccxt.test.base.test_after_constructor import test_after_constructor # noqa E402 +from ccxt.test.base.test_handle_methods import test_handle_methods # noqa E402 +from ccxt.test.base.test_remove_repeated_elements_from_array import test_remove_repeated_elements_from_array # noqa E402 +from ccxt.test.base.test_parse_precision import test_parse_precision # noqa E402 +from ccxt.test.base.test_arrays_concat import test_arrays_concat # noqa E402 + +def base_tests_init(): + test_language_specific() + test_after_constructor() + test_extend() + test_deep_extend() + test_cryptography() + test_datetime() + test_decimal_to_precision() + test_number_to_string() + test_precise() + test_safe_methods() + test_safe_ticker() + # testJson (); + test_sort_by() + test_sum() + test_omit() + test_group_by() + test_filter_by() + test_handle_methods() + test_remove_repeated_elements_from_array() + test_parse_precision() + test_arrays_concat() diff --git a/ccxt/test/custom/.gitignore b/ccxt/test/custom/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/ccxt/test/exchange/async/test_create_order.py b/ccxt/test/exchange/async/test_create_order.py new file mode 100644 index 0000000..70d3fdd --- /dev/null +++ b/ccxt/test/exchange/async/test_create_order.py @@ -0,0 +1,253 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.base.decimal_to_precision import TICK_SIZE # noqa E402 +from ccxt.base.decimal_to_precision import TRUNCATE # noqa E402 +from ccxt.base.decimal_to_precision import ROUND # noqa E402 +from ccxt.base.decimal_to_precision import ROUND_UP # noqa E402 +from ccxt.base.decimal_to_precision import decimal_to_precision # noqa E402 +from ccxt.base.decimal_to_precision import number_to_string # noqa E402 +from ccxt.base.precise import Precise # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 +from ccxt.test.exchange.base import test_order # noqa E402 + +# ---------------------------------------------------------------------------- +def tco_debug(exchange, symbol, message): + # just for debugging purposes + debug_create_order = True + if debug_create_order: + # for c# fix, extra step to convert them to string + print(' >>>>> testCreateOrder [', str((exchange['id'])), ' : ', symbol, '] ', message) + return True + + +# ---------------------------------------------------------------------------- +async def test_create_order(exchange, skipped_properties, symbol): + log_prefix = test_shared_methods.log_template(exchange, 'createOrder', [symbol]) + assert exchange.has['cancelOrder'] or exchange.has['cancelOrders'] or exchange.has['cancelAllOrders'], log_prefix + ' does not have cancelOrder|cancelOrders|canelAllOrders method, which is needed to make tests for `createOrder` method. Skipping the test...' + # pre-define some coefficients, which will be used down below + limit_price_safety_multiplier_from_median = 1.045 # todo: when this https://github.com/ccxt/ccxt/issues/22442 is implemented, we'll remove hardcoded value. atm 5% is enough + market = exchange.market(symbol) + is_swap_future = market['swap'] or market['future'] + assert exchange.has['fetchBalance'], log_prefix + ' does not have fetchBalance() method, which is needed to make tests for `createOrder` method. Skipping the test...' + balance = await exchange.fetch_balance() + initial_base_balance = balance[market['base']]['free'] + initial_quote_balance = balance[market['quote']]['free'] + assert initial_quote_balance is not None, log_prefix + ' - testing account not have balance of' + market['quote'] + ' in fetchBalance() which is required to test' + tco_debug(exchange, symbol, 'fetched balance for ' + symbol + ' : ' + str(initial_base_balance) + ' ' + market['base'] + '/' + initial_quote_balance + ' ' + market['quote']) + [best_bid, best_ask] = await test_shared_methods.fetch_best_bid_ask(exchange, 'createOrder', symbol) + # **************** [Scenario 1 - START] **************** # + tco_debug(exchange, symbol, '### SCENARIO 1 ###') + # create a "limit order" which IS GUARANTEED not to have a fill (i.e. being far from the real price) + await tco_create_unfillable_order(exchange, market, log_prefix, skipped_properties, best_bid, best_ask, limit_price_safety_multiplier_from_median, 'buy', None) + if is_swap_future: + # for swap markets, we test sell orders too + await tco_create_unfillable_order(exchange, market, log_prefix, skipped_properties, best_bid, best_ask, limit_price_safety_multiplier_from_median, 'sell', None) + tco_debug(exchange, symbol, '### SCENARIO 1 PASSED ###') + # **************** [Scenario 2 - START] **************** # + tco_debug(exchange, symbol, '### SCENARIO 2 ###') + # create an order which IS GUARANTEED to have a fill (full or partial) + await tco_create_fillable_order(exchange, market, log_prefix, skipped_properties, best_bid, best_ask, limit_price_safety_multiplier_from_median, 'buy', None) + if is_swap_future: + # for swap markets, we test sell orders too + await tco_create_fillable_order(exchange, market, log_prefix, skipped_properties, best_bid, best_ask, limit_price_safety_multiplier_from_median, 'sell', None) + tco_debug(exchange, symbol, '### SCENARIO 2 PASSED ###') + # **************** [Scenario 3 - START] **************** # + return True + + +# ---------------------------------------------------------------------------- +async def tco_create_unfillable_order(exchange, market, log_prefix, skipped_properties, best_bid, best_ask, limit_price_safety_multiplier_from_median, buy_or_sell, predefined_amount=None): + try: + symbol = market['symbol'] + minimun_prices = exchange.safe_dict(market['limits'], 'price', {}) + minimum_price = minimun_prices['min'] + maximum_price = minimun_prices['max'] + # below we set limit price, where the order will not be completed. + # We do not use the extreme "limits" values for that market, because, even though min purchase amount for BTC/USDT can be 0.01 BTC, it means with 10$ you can buy 1000 BTC, which leads to unrealistic outcome. So, we just use around 5%-10% far price from the current price. + limit_buy_price_non_fillable = best_bid / limit_price_safety_multiplier_from_median + if minimum_price is not None and limit_buy_price_non_fillable < minimum_price: + limit_buy_price_non_fillable = minimum_price + limit_sell_price_non_fillable = best_ask * limit_price_safety_multiplier_from_median + if maximum_price is not None and limit_sell_price_non_fillable > maximum_price: + limit_sell_price_non_fillable = maximum_price + created_order = None + if buy_or_sell == 'buy': + order_amount = tco_get_minimum_amount_for_limit_price(exchange, market, limit_buy_price_non_fillable, predefined_amount) + created_order = await tco_create_order_safe(exchange, symbol, 'limit', 'buy', order_amount, limit_buy_price_non_fillable, {}, skipped_properties) + else: + order_amount = tco_get_minimum_amount_for_limit_price(exchange, market, limit_sell_price_non_fillable, predefined_amount) + created_order = await tco_create_order_safe(exchange, symbol, 'limit', 'sell', order_amount, limit_sell_price_non_fillable, {}, skipped_properties) + fetched_order = await test_shared_methods.fetch_order(exchange, symbol, created_order['id'], skipped_properties) + # test fetched order object + if fetched_order is not None: + test_order(exchange, skipped_properties, 'createOrder', fetched_order, symbol, exchange.milliseconds()) + # ensure that order is not filled + test_shared_methods.assert_order_state(exchange, skipped_properties, 'createdOrder', created_order, 'open', False) + test_shared_methods.assert_order_state(exchange, skipped_properties, 'fetchedOrder', fetched_order, 'open', True) + # ensure that order side matches + test_shared_methods.assert_in_array(exchange, skipped_properties, 'createdOrder', created_order, 'side', [None, buy_or_sell]) + test_shared_methods.assert_in_array(exchange, skipped_properties, 'fetchedOrder', fetched_order, 'side', [None, buy_or_sell]) + await tco_cancel_order(exchange, symbol, created_order['id']) + except Exception as e: + raise Error(log_prefix + ' failed for Scenario 1: ' + str(e)) + return True + + +async def tco_create_fillable_order(exchange, market, log_prefix, skipped_properties, best_bid, best_ask, limit_price_safety_multiplier_from_median, buy_or_sell_string, predefined_amount=None): + try: + is_swap_future = market['swap'] or market['future'] + is_buy = (buy_or_sell_string == 'buy') + entry_side = 'buy' if is_buy else 'sell' + exit_side = 'sell' if is_buy else 'buy' + entryorder_price = best_ask * limit_price_safety_multiplier_from_median if is_buy else best_bid / limit_price_safety_multiplier_from_median + exitorder_price = best_bid / limit_price_safety_multiplier_from_median if is_buy else best_ask * limit_price_safety_multiplier_from_median # todo revise: (tcoMininumCost (exchange, market) / amountToClose) / limitPriceSafetyMultiplierFromMedian; + # + # + symbol = market['symbol'] + entry_amount = tco_get_minimum_amount_for_limit_price(exchange, market, entryorder_price) + entryorder_filled = await tco_create_order_safe(exchange, symbol, 'limit', entry_side, entry_amount, entryorder_price, {}, skipped_properties) + # just for case, cancel any possible unfilled amount (though it is not be expected because the order was fillable) + await tco_try_cancel_order(exchange, symbol, entryorder_filled, skipped_properties) + # now, as order is closed/canceled, we can reliably fetch the order information + entryorder_fetched = await test_shared_methods.fetch_order(exchange, symbol, entryorder_filled['id'], skipped_properties) + tco_assert_filled_order(exchange, market, log_prefix, skipped_properties, entryorder_filled, entryorder_fetched, entry_side, entry_amount) + # + # ### close the traded position ### + # + amount_to_close = exchange.parse_to_numeric(exchange.safe_string(entryorder_fetched, 'filled')) + params = {} + # as we want to close position, we should use 'reduceOnly' to ensure we don't open a margined position accidentally, because some exchanges might have automatically enabled margin-mode (on spot) or hedge-mode (on contracts) + if is_swap_future: + params['reduceOnly'] = True + exitorder_filled = await tco_create_order_safe(exchange, symbol, 'market', exit_side, amount_to_close, (None if market['spot'] else exitorder_price), params, skipped_properties) + exitorder_fetched = await test_shared_methods.fetch_order(exchange, symbol, exitorder_filled['id'], skipped_properties) + tco_assert_filled_order(exchange, market, log_prefix, skipped_properties, exitorder_filled, exitorder_fetched, exit_side, amount_to_close) + except Exception as e: + raise Error('failed for Scenario 2: ' + str(e)) + return True + + +def tco_assert_filled_order(exchange, market, log_prefix, skipped_properties, created_order, fetched_order, requested_side, requested_amount): + # test filled amount + precision_amount = exchange.safe_string(market['precision'], 'amount') + entryorder_amount_string = exchange.number_to_string(requested_amount) + filled_string = exchange.safe_string(fetched_order, 'filled') + assert filled_string is not None, log_prefix + ' order should be filled, but it is not. ' + exchange.json(fetched_order) + # filled amount should be whithin the expected range i.e. if you buy 100 DOGECOIN and amount-precision is 1, + # and also considering possible roundings in implementation, then filled amount should be between 99 and 101 + max_expected_filled_amount = Precise.string_add(entryorder_amount_string, precision_amount) + min_expected_filled_amount = Precise.string_sub(entryorder_amount_string, precision_amount) + assert Precise.string_le(filled_string, max_expected_filled_amount), log_prefix + ' filled amount is more than expected, possibly some implementation issue. ' + exchange.json(fetched_order) + assert Precise.string_ge(filled_string, min_expected_filled_amount), log_prefix + ' filled amount is less than expected, possibly some implementation issue. ' + exchange.json(fetched_order) + # order state should be "closed" + test_shared_methods.assert_order_state(exchange, skipped_properties, 'createdOrder', created_order, 'closed', False) + test_shared_methods.assert_order_state(exchange, skipped_properties, 'fetchedOrder', fetched_order, 'closed', True) + # ensure that order side matches + test_shared_methods.assert_in_array(exchange, skipped_properties, 'createdOrder', created_order, 'side', [None, requested_side]) + test_shared_methods.assert_in_array(exchange, skipped_properties, 'fetchedOrder', fetched_order, 'side', [None, requested_side]) + return True + + +# ---------------------------------------------------------------------------- +async def tco_cancel_order(exchange, symbol, order_id=None): + log_prefix = test_shared_methods.log_template(exchange, 'createOrder', [symbol]) + used_method = '' + cancel_result = None + if exchange.has['cancelOrder'] and order_id is not None: + used_method = 'cancelOrder' + cancel_result = await exchange.cancel_order(order_id, symbol) + elif exchange.has['cancelAllOrders']: + used_method = 'cancelAllOrders' + cancel_result = await exchange.cancel_all_orders(symbol) + elif exchange.has['cancelOrders']: + raise Error(log_prefix + ' cancelOrders method is not unified yet, coming soon...') + tco_debug(exchange, symbol, 'canceled order using ' + used_method + ':' + cancel_result['id']) + # todo: + # testSharedMethods.assertOrderState (exchange, skippedProperties, 'cancelOrder', cancelResult, 'canceled', false); + # testSharedMethods.assertOrderState (exchange, skippedProperties, 'cancelOrder', cancelResult, 'closed', true); + return True + + +# ---------------------------------------------------------------------------- +# ---------------------------------------------------------------------------- +async def tco_create_order_safe(exchange, symbol, order_type, side, amount, price=None, params={}, skipped_properties={}): + tco_debug(exchange, symbol, 'Executing createOrder ' + order_type + ' ' + side + ' ' + amount + ' ' + price + ' ' + exchange.json(params)) + order = await exchange.create_order(symbol, order_type, side, amount, price, params) + try: + test_order(exchange, skipped_properties, 'createOrder', order, symbol, int(time.time() * 1000)) + except Exception as e: + if order_type != 'market': + # if it was limit order, try to cancel it before exiting the script + await tco_try_cancel_order(exchange, symbol, order, skipped_properties) + raise e + return order + + +def tco_mininum_amount(exchange, market): + amount_values = exchange.safe_dict(market['limits'], 'amount', {}) + amount_min = exchange.safe_number(amount_values, 'min') + assert amount_min is not None, exchange.id + ' ' + market['symbol'] + ' can not determine minimum amount for order' + return amount_min + + +def tco_mininum_cost(exchange, market): + cost_values = exchange.safe_dict(market['limits'], 'cost', {}) + cost_min = exchange.safe_number(cost_values, 'min') + assert cost_min is not None, exchange.id + ' ' + market['symbol'] + ' can not determine minimum cost for order' + return cost_min + + +def tco_get_minimum_amount_for_limit_price(exchange, market, price, predefined_amount=None): + # this method calculates the minimum realistic order amount: + # at first it checks the "minimum hardcap limit" (i.e. 7 DOGE), however, if exchange also has "minimum cost" limits, + # then we need to calculate the amount using cost, because of price is volatile, today's 7 DOGE cost could be 1$ + # but "minimum cost" requirement could be 5$ (which translates to 35 DOGE amount) + minimum_amount = tco_mininum_amount(exchange, market) + minimum_cost = tco_mininum_cost(exchange, market) + final_amount = minimum_amount + if minimum_cost is not None: + if final_amount * price < minimum_cost: + final_amount = minimum_cost / price + if predefined_amount is not None: + final_amount = max(final_amount, predefined_amount) + # because it's possible that calculated value might get truncated down in "createOrder" (i.e. 0.129 -> 0.12), we should ensure that final amount * price would bypass minimum cost requirements, by adding the "minimum precision" + amount_precision = exchange.safe_number(market['precision'], 'amount') + is_tick_size_precision = exchange.precisionMode == 4 + if amount_precision is None: + amount_precision = 1e-15 # todo: revise this for better way in future + else: + # todo: remove after TICK_SIZE unification + if not is_tick_size_precision: + amount_precision = 1 / math.pow(10, amount_precision) # this converts DECIMAL_PRECISION into TICK_SIZE + final_amount = final_amount + amount_precision + final_amount = final_amount * 1.1 # add around 10% to ensure "cost" is enough + final_amount = float(exchange.decimal_to_precision(final_amount, 2, market['precision']['amount'], exchange.precisionMode)) # 2 stands for ROUND_UP constant, 0 stands for TRUNCATE + return final_amount + + +async def tco_try_cancel_order(exchange, symbol, order, skipped_properties): + order_fetched = await test_shared_methods.fetch_order(exchange, symbol, order['id'], skipped_properties) + needs_cancel = exchange.in_array(order_fetched['status'], ['open', 'pending', None]) + # if it was not reported as closed/filled, then try to cancel it + if needs_cancel: + tco_debug(exchange, symbol, 'trying to cancel the remaining amount of partially filled order...') + try: + await tco_cancel_order(exchange, symbol, order['id']) + except Exception as e: + # order might have been closed/filled already, before 'cancelOrder' call reaches server, so it is tolerable, we don't throw exception + tco_debug(exchange, symbol, ' a moment ago order was reported as pending, but could not be cancelled at this moment. Exception message: ' + str(e)) + else: + tco_debug(exchange, symbol, 'order is already closed/filled, no need to cancel it') + return True diff --git a/ccxt/test/exchange/async/test_features.py b/ccxt/test/exchange/async/test_features.py new file mode 100644 index 0000000..3644392 --- /dev/null +++ b/ccxt/test/exchange/async/test_features.py @@ -0,0 +1,124 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +async def test_features(exchange, skipped_properties): + market_types = ['spot', 'swap', 'future', 'option'] + sub_types = ['linear', 'inverse'] + features = exchange.features + keys = list(features.keys()) + for i in range(0, len(keys)): + test_shared_methods.assert_in_array(exchange, skipped_properties, 'features', keys, i, market_types) + market_type = keys[i] + value = features[market_type] + # assert (value !== undefined, 'exchange.features["' + marketType + '"] is undefined, that key should be either absent or have a value'); + if value is None: + continue + if market_type == 'spot': + test_features_inner(exchange, skipped_properties, value) + else: + sub_keys = list(value.keys()) + for j in range(0, len(sub_keys)): + sub_key = sub_keys[j] + test_shared_methods.assert_in_array(exchange, skipped_properties, 'features', sub_keys, j, sub_types) + sub_value = value[sub_key] + # sometimes it might not be available for exchange, eg. future>inverse) + if sub_value is not None: + test_features_inner(exchange, skipped_properties, sub_value) + return True + + +def test_features_inner(exchange, skipped_properties, feature_obj): + format = { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': False, + 'triggerPriceType': { + 'mark': False, + 'last': False, + 'index': False, + }, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': { + 'triggerPriceType': { + 'last': False, + 'mark': False, + 'index': False, + }, + 'price': False, + }, + 'timeInForce': { + 'GTC': False, + 'IOC': False, + 'FOK': False, + 'PO': False, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + }, + 'createOrders': { + 'max': 5, + }, + 'fetchMyTrades': { + 'marginMode': False, + 'daysBack': 0, + 'limit': 0, + 'untilDays': 0, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 0, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': 0, + 'daysBack': 0, + 'untilDays': 0, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 0, + 'daysBack': 0, + 'daysBackCanceled': 0, + 'untilDays': 0, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 0, + }, + } + feature_keys = list(feature_obj.keys()) + all_methods = list(exchange.has.keys()) + for i in range(0, len(feature_keys)): + test_shared_methods.assert_in_array(exchange, skipped_properties, 'features', feature_keys, i, all_methods) + test_shared_methods.assert_structure(exchange, skipped_properties, 'features', feature_obj, format, None, True) # deep structure check diff --git a/ccxt/test/exchange/async/test_fetch_accounts.py b/ccxt/test/exchange/async/test_fetch_accounts.py new file mode 100644 index 0000000..59d69e9 --- /dev/null +++ b/ccxt/test/exchange/async/test_fetch_accounts.py @@ -0,0 +1,24 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_account # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +async def test_fetch_accounts(exchange, skipped_properties): + method = 'fetchAccounts' + accounts = await exchange.fetch_accounts() + test_shared_methods.assert_non_emtpy_array(exchange, skipped_properties, method, accounts) + for i in range(0, len(accounts)): + test_account(exchange, skipped_properties, method, accounts[i]) + return True diff --git a/ccxt/test/exchange/async/test_fetch_balance.py b/ccxt/test/exchange/async/test_fetch_balance.py new file mode 100644 index 0000000..ada9407 --- /dev/null +++ b/ccxt/test/exchange/async/test_fetch_balance.py @@ -0,0 +1,21 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_balance # noqa E402 + +async def test_fetch_balance(exchange, skipped_properties): + method = 'fetchBalance' + response = await exchange.fetch_balance() + test_balance(exchange, skipped_properties, method, response) + return True diff --git a/ccxt/test/exchange/async/test_fetch_borrow_interest.py b/ccxt/test/exchange/async/test_fetch_borrow_interest.py new file mode 100644 index 0000000..8a0647c --- /dev/null +++ b/ccxt/test/exchange/async/test_fetch_borrow_interest.py @@ -0,0 +1,24 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_borrow_interest # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +async def test_fetch_borrow_interest(exchange, skipped_properties, code, symbol): + method = 'fetchBorrowInterest' + borrow_interest = await exchange.fetch_borrow_interest(code, symbol) + test_shared_methods.assert_non_emtpy_array(exchange, skipped_properties, method, borrow_interest, code) + for i in range(0, len(borrow_interest)): + test_borrow_interest(exchange, skipped_properties, method, borrow_interest[i], code, symbol) + return True diff --git a/ccxt/test/exchange/async/test_fetch_closed_orders.py b/ccxt/test/exchange/async/test_fetch_closed_orders.py new file mode 100644 index 0000000..7871d33 --- /dev/null +++ b/ccxt/test/exchange/async/test_fetch_closed_orders.py @@ -0,0 +1,28 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_order # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +async def test_fetch_closed_orders(exchange, skipped_properties, symbol): + method = 'fetchClosedOrders' + orders = await exchange.fetch_closed_orders(symbol) + test_shared_methods.assert_non_emtpy_array(exchange, skipped_properties, method, orders, symbol) + now = exchange.milliseconds() + for i in range(0, len(orders)): + order = orders[i] + test_order(exchange, skipped_properties, method, order, symbol, now) + test_shared_methods.assert_in_array(exchange, skipped_properties, method, order, 'status', ['closed', 'canceled']) + test_shared_methods.assert_timestamp_order(exchange, method, symbol, orders) + return True diff --git a/ccxt/test/exchange/async/test_fetch_currencies.py b/ccxt/test/exchange/async/test_fetch_currencies.py new file mode 100644 index 0000000..ff2bba5 --- /dev/null +++ b/ccxt/test/exchange/async/test_fetch_currencies.py @@ -0,0 +1,73 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_currency # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +async def test_fetch_currencies(exchange, skipped_properties): + method = 'fetchCurrencies' + currencies = await exchange.fetch_currencies() + # todo: try to invent something to avoid undefined undefined, i.e. maybe move into private and force it to have a value + num_inactive_currencies = 0 + max_inactive_currencies_percentage = exchange.safe_integer(skipped_properties, 'maxInactiveCurrenciesPercentage', 50) # no more than X% currencies should be inactive + required_active_currencies = ['BTC', 'ETH', 'USDT', 'USDC'] + features = exchange.features + features_spot = exchange.safe_dict(features, 'spot', {}) + fetch_currencies = exchange.safe_dict(features_spot, 'fetchCurrencies', {}) + is_fetch_currencies_private = exchange.safe_value(fetch_currencies, 'private', False) + if not is_fetch_currencies_private: + values = list(currencies.values()) + test_shared_methods.assert_non_emtpy_array(exchange, skipped_properties, method, values) + currencies_length = len(values) + # ensure exchange returns enough length of currencies + skip_amount = ('amountOfCurrencies' in skipped_properties) + assert skip_amount or currencies_length > 5, exchange.id + ' ' + method + ' must return at least several currencies, but it returned ' + str(currencies_length) + # allow skipped exchanges + skip_active = ('activeCurrenciesQuota' in skipped_properties) + skip_major_currency_check = ('activeMajorCurrencies' in skipped_properties) + # loop + for i in range(0, currencies_length): + currency = values[i] + test_currency(exchange, skipped_properties, method, currency) + # detailed check for deposit/withdraw + active = exchange.safe_bool(currency, 'active') + if active is False: + num_inactive_currencies = num_inactive_currencies + 1 + # ensure that major currencies are active and enabled for deposit and withdrawal + code = exchange.safe_string(currency, 'code', None) + withdraw = exchange.safe_bool(currency, 'withdraw') + deposit = exchange.safe_bool(currency, 'deposit') + if exchange.in_array(code, required_active_currencies): + assert skip_major_currency_check or (withdraw and deposit), 'Major currency ' + code + ' should have withdraw and deposit flags enabled' + # check at least X% of currencies are active + inactive_currencies_percentage = (num_inactive_currencies / currencies_length) * 100 + assert skip_active or (inactive_currencies_percentage < max_inactive_currencies_percentage), 'Percentage of inactive currencies is too high at ' + str(inactive_currencies_percentage) + '% that is more than the allowed maximum of ' + str(max_inactive_currencies_percentage) + '%' + detect_currency_conflicts(exchange, currencies) + return True + + +def detect_currency_conflicts(exchange, currency_values): + # detect if there are currencies with different ids for the same code + ids = {} + keys = list(currency_values.keys()) + for i in range(0, len(keys)): + key = keys[i] + currency = currency_values[key] + code = currency['code'] + if not (code in ids): + ids[code] = currency['id'] + else: + is_different = ids[code] != currency['id'] + assert not is_different, exchange.id + ' fetchCurrencies() has different ids for the same code: ' + code + ' ' + ids[code] + ' ' + currency['id'] + return True diff --git a/ccxt/test/exchange/async/test_fetch_deposit_withdrawals.py b/ccxt/test/exchange/async/test_fetch_deposit_withdrawals.py new file mode 100644 index 0000000..94addb5 --- /dev/null +++ b/ccxt/test/exchange/async/test_fetch_deposit_withdrawals.py @@ -0,0 +1,26 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_deposit_withdrawal # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +async def test_fetch_deposit_withdrawals(exchange, skipped_properties, code): + method = 'fetchTransactions' + transactions = await exchange.fetch_transactions(code) + test_shared_methods.assert_non_emtpy_array(exchange, skipped_properties, method, transactions, code) + now = exchange.milliseconds() + for i in range(0, len(transactions)): + test_deposit_withdrawal(exchange, skipped_properties, method, transactions[i], code, now) + test_shared_methods.assert_timestamp_order(exchange, method, code, transactions) + return True diff --git a/ccxt/test/exchange/async/test_fetch_deposits.py b/ccxt/test/exchange/async/test_fetch_deposits.py new file mode 100644 index 0000000..f277321 --- /dev/null +++ b/ccxt/test/exchange/async/test_fetch_deposits.py @@ -0,0 +1,26 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_deposit_withdrawal # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +async def test_fetch_deposits(exchange, skipped_properties, code): + method = 'fetchDeposits' + transactions = await exchange.fetch_deposits(code) + test_shared_methods.assert_non_emtpy_array(exchange, skipped_properties, method, transactions, code) + now = exchange.milliseconds() + for i in range(0, len(transactions)): + test_deposit_withdrawal(exchange, skipped_properties, method, transactions[i], code, now) + test_shared_methods.assert_timestamp_order(exchange, method, code, transactions) + return True diff --git a/ccxt/test/exchange/async/test_fetch_funding_rate_history.py b/ccxt/test/exchange/async/test_fetch_funding_rate_history.py new file mode 100644 index 0000000..6b07317 --- /dev/null +++ b/ccxt/test/exchange/async/test_fetch_funding_rate_history.py @@ -0,0 +1,25 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_funding_rate_history # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +async def test_fetch_funding_rate_history(exchange, skipped_properties, symbol): + method = 'fetchFundingRateHistory' + funding_rates_history = await exchange.fetch_funding_rate_history(symbol) + test_shared_methods.assert_non_emtpy_array(exchange, skipped_properties, method, funding_rates_history, symbol) + for i in range(0, len(funding_rates_history)): + test_funding_rate_history(exchange, skipped_properties, method, funding_rates_history[i], symbol) + test_shared_methods.assert_timestamp_order(exchange, method, symbol, funding_rates_history) + return True diff --git a/ccxt/test/exchange/async/test_fetch_l2_order_book.py b/ccxt/test/exchange/async/test_fetch_l2_order_book.py new file mode 100644 index 0000000..99890b9 --- /dev/null +++ b/ccxt/test/exchange/async/test_fetch_l2_order_book.py @@ -0,0 +1,21 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_order_book # noqa E402 + +async def test_fetch_l2_order_book(exchange, skipped_properties, symbol): + method = 'fetchL2OrderBook' + order_book = await exchange.fetch_l2_order_book(symbol) + test_order_book(exchange, skipped_properties, method, order_book, symbol) + return True diff --git a/ccxt/test/exchange/async/test_fetch_last_prices.py b/ccxt/test/exchange/async/test_fetch_last_prices.py new file mode 100644 index 0000000..61500d7 --- /dev/null +++ b/ccxt/test/exchange/async/test_fetch_last_prices.py @@ -0,0 +1,37 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_last_price # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +async def test_fetch_last_prices(exchange, skipped_properties, symbol): + method = 'fetchLastprices' + # log ('fetching all tickers at once...') + response = None + checked_symbol = None + try: + response = await exchange.fetch_last_prices() + except Exception as e: + response = await exchange.fetch_last_prices([symbol]) + checked_symbol = symbol + assert isinstance(response, dict), exchange.id + ' ' + method + ' ' + checked_symbol + ' must return an object. ' + exchange.json(response) + values = list(response.values()) + test_shared_methods.assert_non_emtpy_array(exchange, skipped_properties, method, values, checked_symbol) + at_least_one_passed = False + for i in range(0, len(values)): + # todo: symbol check here + test_last_price(exchange, skipped_properties, method, values[i], checked_symbol) + at_least_one_passed = at_least_one_passed or (exchange.safe_number(values[i], 'price') > 0) + assert at_least_one_passed, exchange.id + ' ' + method + ' ' + checked_symbol + ' at least one symbol should pass the test' + return True diff --git a/ccxt/test/exchange/async/test_fetch_ledger.py b/ccxt/test/exchange/async/test_fetch_ledger.py new file mode 100644 index 0000000..2bafa2c --- /dev/null +++ b/ccxt/test/exchange/async/test_fetch_ledger.py @@ -0,0 +1,26 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_ledger_entry # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +async def test_fetch_ledger(exchange, skipped_properties, code): + method = 'fetchLedger' + items = await exchange.fetch_ledger(code) + test_shared_methods.assert_non_emtpy_array(exchange, skipped_properties, method, items, code) + now = exchange.milliseconds() + for i in range(0, len(items)): + test_ledger_entry(exchange, skipped_properties, method, items[i], code, now) + test_shared_methods.assert_timestamp_order(exchange, method, code, items) + return True diff --git a/ccxt/test/exchange/async/test_fetch_ledger_entry.py b/ccxt/test/exchange/async/test_fetch_ledger_entry.py new file mode 100644 index 0000000..ce35bb4 --- /dev/null +++ b/ccxt/test/exchange/async/test_fetch_ledger_entry.py @@ -0,0 +1,29 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_ledger_entry # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +async def test_fetch_ledger_entry(exchange, skipped_properties, code): + method = 'fetchLedgerEntry' + items = await exchange.fetch_ledger(code) + length = len(items) + test_shared_methods.assert_non_emtpy_array(exchange, skipped_properties, method, items, code) + if length > 0: + first_item = items[0] + id = first_item['id'] + item = await exchange.fetch_ledger_entry(id) + now = exchange.milliseconds() + test_ledger_entry(exchange, skipped_properties, method, item, code, now) + return True diff --git a/ccxt/test/exchange/async/test_fetch_leverage_tiers.py b/ccxt/test/exchange/async/test_fetch_leverage_tiers.py new file mode 100644 index 0000000..378690c --- /dev/null +++ b/ccxt/test/exchange/async/test_fetch_leverage_tiers.py @@ -0,0 +1,34 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_leverage_tier # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +async def test_fetch_leverage_tiers(exchange, skipped_properties, symbol): + method = 'fetchLeverageTiers' + tiers = await exchange.fetch_leverage_tiers(['symbol']) + # const format = { + # 'RAY/USDT': [ + # {}, + # ], + # }; + assert isinstance(tiers, dict), exchange.id + ' ' + method + ' ' + symbol + ' must return an object. ' + exchange.json(tiers) + tier_keys = list(tiers.keys()) + test_shared_methods.assert_non_emtpy_array(exchange, skipped_properties, method, tier_keys, symbol) + for i in range(0, len(tier_keys)): + tiers_for_symbol = tiers[tier_keys[i]] + test_shared_methods.assert_non_emtpy_array(exchange, skipped_properties, method, tiers_for_symbol, symbol) + for j in range(0, len(tiers_for_symbol)): + test_leverage_tier(exchange, skipped_properties, method, tiers_for_symbol[j]) + return True diff --git a/ccxt/test/exchange/async/test_fetch_liquidations.py b/ccxt/test/exchange/async/test_fetch_liquidations.py new file mode 100644 index 0000000..c788c21 --- /dev/null +++ b/ccxt/test/exchange/async/test_fetch_liquidations.py @@ -0,0 +1,28 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_shared_methods # noqa E402 +from ccxt.test.exchange.base import test_liquidation # noqa E402 + +async def test_fetch_liquidations(exchange, skipped_properties, code): + method = 'fetchLiquidations' + if not exchange.has['fetchLiquidations']: + return True + items = await exchange.fetch_liquidations(code) + assert isinstance(items, list), exchange.id + ' ' + method + ' ' + code + ' must return an array. ' + exchange.json(items) + # const now = exchange.milliseconds (); + for i in range(0, len(items)): + test_liquidation(exchange, skipped_properties, method, items[i], code) + test_shared_methods.assert_timestamp_order(exchange, method, code, items) + return True diff --git a/ccxt/test/exchange/async/test_fetch_margin_mode.py b/ccxt/test/exchange/async/test_fetch_margin_mode.py new file mode 100644 index 0000000..2fc29f1 --- /dev/null +++ b/ccxt/test/exchange/async/test_fetch_margin_mode.py @@ -0,0 +1,21 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_margin_mode # noqa E402 + +async def test_fetch_margin_mode(exchange, skipped_properties, symbol): + method = 'fetchMarginMode' + margin_mode = await exchange.fetch_margin_mode(symbol) + test_margin_mode(exchange, skipped_properties, method, margin_mode) + return True diff --git a/ccxt/test/exchange/async/test_fetch_margin_modes.py b/ccxt/test/exchange/async/test_fetch_margin_modes.py new file mode 100644 index 0000000..226dfe8 --- /dev/null +++ b/ccxt/test/exchange/async/test_fetch_margin_modes.py @@ -0,0 +1,28 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_margin_mode # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +async def test_fetch_margin_modes(exchange, skipped_properties, symbol): + method = 'fetchMarginModes' + margin_modes = await exchange.fetch_margin_modes(['symbol']) + assert isinstance(margin_modes, dict), exchange.id + ' ' + method + ' ' + symbol + ' must return an object. ' + exchange.json(margin_modes) + margin_mode_keys = list(margin_modes.keys()) + test_shared_methods.assert_non_emtpy_array(exchange, skipped_properties, method, margin_modes, symbol) + for i in range(0, len(margin_mode_keys)): + margin_mode = margin_modes[margin_mode_keys[i]] + test_shared_methods.assert_non_emtpy_array(exchange, skipped_properties, method, margin_mode, symbol) + test_margin_mode(exchange, skipped_properties, method, margin_mode) + return True diff --git a/ccxt/test/exchange/async/test_fetch_market_leverage_tiers.py b/ccxt/test/exchange/async/test_fetch_market_leverage_tiers.py new file mode 100644 index 0000000..ef5cd56 --- /dev/null +++ b/ccxt/test/exchange/async/test_fetch_market_leverage_tiers.py @@ -0,0 +1,24 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_leverage_tier # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +async def test_fetch_market_leverage_tiers(exchange, skipped_properties, symbol): + method = 'fetchMarketLeverageTiers' + tiers = await exchange.fetch_market_leverage_tiers(symbol) + test_shared_methods.assert_non_emtpy_array(exchange, skipped_properties, method, tiers, symbol) + for j in range(0, len(tiers)): + test_leverage_tier(exchange, skipped_properties, method, tiers[j]) + return True diff --git a/ccxt/test/exchange/async/test_fetch_markets.py b/ccxt/test/exchange/async/test_fetch_markets.py new file mode 100644 index 0000000..ed8ffb4 --- /dev/null +++ b/ccxt/test/exchange/async/test_fetch_markets.py @@ -0,0 +1,41 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_market # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +async def test_fetch_markets(exchange, skipped_properties): + method = 'fetchMarkets' + markets = await exchange.fetch_markets() + assert isinstance(markets, dict), exchange.id + ' ' + method + ' must return an object. ' + exchange.json(markets) + market_values = list(markets.values()) + test_shared_methods.assert_non_emtpy_array(exchange, skipped_properties, method, market_values) + for i in range(0, len(market_values)): + test_market(exchange, skipped_properties, method, market_values[i]) + detect_market_conflicts(exchange, markets) + return True + + +def detect_market_conflicts(exchange, market_values): + # detect if there are markets with different ids for the same symbol + ids = {} + for i in range(0, len(market_values)): + market = market_values[i] + symbol = market['symbol'] + if not (symbol in ids): + ids[symbol] = market['id'] + else: + is_different = ids[symbol] != market['id'] + assert not is_different, exchange.id + ' fetchMarkets() has different ids for the same symbol: ' + symbol + ' ' + ids[symbol] + ' ' + market['id'] + return True diff --git a/ccxt/test/exchange/async/test_fetch_my_liquidations.py b/ccxt/test/exchange/async/test_fetch_my_liquidations.py new file mode 100644 index 0000000..b76f936 --- /dev/null +++ b/ccxt/test/exchange/async/test_fetch_my_liquidations.py @@ -0,0 +1,28 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_shared_methods # noqa E402 +from ccxt.test.exchange.base import test_liquidation # noqa E402 + +async def test_fetch_my_liquidations(exchange, skipped_properties, code): + method = 'fetchMyLiquidations' + if not exchange.has['fetchMyLiquidations']: + return True + items = await exchange.fetch_my_liquidations(code) + assert isinstance(items, list), exchange.id + ' ' + method + ' ' + code + ' must return an array. ' + exchange.json(items) + # const now = exchange.milliseconds (); + for i in range(0, len(items)): + test_liquidation(exchange, skipped_properties, method, items[i], code) + test_shared_methods.assert_timestamp_order(exchange, method, code, items) + return True diff --git a/ccxt/test/exchange/async/test_fetch_my_trades.py b/ccxt/test/exchange/async/test_fetch_my_trades.py new file mode 100644 index 0000000..fdc0441 --- /dev/null +++ b/ccxt/test/exchange/async/test_fetch_my_trades.py @@ -0,0 +1,26 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_shared_methods # noqa E402 +from ccxt.test.exchange.base import test_trade # noqa E402 + +async def test_fetch_my_trades(exchange, skipped_properties, symbol): + method = 'fetchMyTrades' + trades = await exchange.fetch_my_trades(symbol) + test_shared_methods.assert_non_emtpy_array(exchange, skipped_properties, method, trades, symbol) + now = exchange.milliseconds() + for i in range(0, len(trades)): + test_trade(exchange, skipped_properties, method, trades[i], symbol, now) + test_shared_methods.assert_timestamp_order(exchange, method, symbol, trades) + return True diff --git a/ccxt/test/exchange/async/test_fetch_ohlcv.py b/ccxt/test/exchange/async/test_fetch_ohlcv.py new file mode 100644 index 0000000..ed34b3d --- /dev/null +++ b/ccxt/test/exchange/async/test_fetch_ohlcv.py @@ -0,0 +1,35 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_ohlcv # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +async def test_fetch_ohlcv(exchange, skipped_properties, symbol): + method = 'fetchOHLCV' + timeframe_keys = list(exchange.timeframes.keys()) + assert len(timeframe_keys), exchange.id + ' ' + method + ' - no timeframes found' + # prefer 1m timeframe if available, otherwise return the first one + chosen_timeframe_key = '1m' + if not exchange.in_array(chosen_timeframe_key, timeframe_keys): + chosen_timeframe_key = timeframe_keys[0] + limit = 10 + duration = exchange.parse_timeframe(chosen_timeframe_key) + since = exchange.milliseconds() - duration * limit * 1000 - 1000 + ohlcvs = await exchange.fetch_ohlcv(symbol, chosen_timeframe_key, since, limit) + test_shared_methods.assert_non_emtpy_array(exchange, skipped_properties, method, ohlcvs, symbol) + now = exchange.milliseconds() + for i in range(0, len(ohlcvs)): + test_ohlcv(exchange, skipped_properties, method, ohlcvs[i], symbol, now) + # todo: sorted timestamps check + return True diff --git a/ccxt/test/exchange/async/test_fetch_open_interest_history.py b/ccxt/test/exchange/async/test_fetch_open_interest_history.py new file mode 100644 index 0000000..9c2044e --- /dev/null +++ b/ccxt/test/exchange/async/test_fetch_open_interest_history.py @@ -0,0 +1,24 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_open_interest # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +async def test_fetch_open_interest_history(exchange, skipped_properties, symbol): + method = 'fetchOpenInterestHistory' + open_interest_history = await exchange.fetch_open_interest_history(symbol) + test_shared_methods.assert_non_emtpy_array(exchange, skipped_properties, method, open_interest_history, symbol) + for i in range(0, len(open_interest_history)): + test_open_interest(exchange, skipped_properties, method, open_interest_history[i]) + return True diff --git a/ccxt/test/exchange/async/test_fetch_open_orders.py b/ccxt/test/exchange/async/test_fetch_open_orders.py new file mode 100644 index 0000000..692af08 --- /dev/null +++ b/ccxt/test/exchange/async/test_fetch_open_orders.py @@ -0,0 +1,28 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_order # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +async def test_fetch_open_orders(exchange, skipped_properties, symbol): + method = 'fetchOpenOrders' + orders = await exchange.fetch_open_orders(symbol) + test_shared_methods.assert_non_emtpy_array(exchange, skipped_properties, method, orders, symbol) + now = exchange.milliseconds() + for i in range(0, len(orders)): + order = orders[i] + test_order(exchange, skipped_properties, method, order, symbol, now) + test_shared_methods.assert_in_array(exchange, skipped_properties, method, order, 'status', ['open']) + test_shared_methods.assert_timestamp_order(exchange, method, symbol, orders) + return True diff --git a/ccxt/test/exchange/async/test_fetch_order_book.py b/ccxt/test/exchange/async/test_fetch_order_book.py new file mode 100644 index 0000000..16ef74d --- /dev/null +++ b/ccxt/test/exchange/async/test_fetch_order_book.py @@ -0,0 +1,21 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_order_book # noqa E402 + +async def test_fetch_order_book(exchange, skipped_properties, symbol): + method = 'fetchOrderBook' + orderbook = await exchange.fetch_order_book(symbol) + test_order_book(exchange, skipped_properties, method, orderbook, symbol) + return True diff --git a/ccxt/test/exchange/async/test_fetch_order_books.py b/ccxt/test/exchange/async/test_fetch_order_books.py new file mode 100644 index 0000000..557232d --- /dev/null +++ b/ccxt/test/exchange/async/test_fetch_order_books.py @@ -0,0 +1,27 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_order_book # noqa E402 + +async def test_fetch_order_books(exchange, skipped_properties): + method = 'fetchOrderBooks' + symbol = exchange.symbols[0] + order_books = await exchange.fetch_order_books([symbol]) + assert isinstance(order_books, dict), exchange.id + ' ' + method + ' must return an object. ' + exchange.json(order_books) + order_book_keys = list(order_books.keys()) + assert len(order_book_keys), exchange.id + ' ' + method + ' returned 0 length data' + for i in range(0, len(order_book_keys)): + symbol_inner = order_book_keys[i] + test_order_book(exchange, skipped_properties, method, order_books[symbol_inner], symbol_inner) + return True diff --git a/ccxt/test/exchange/async/test_fetch_orders.py b/ccxt/test/exchange/async/test_fetch_orders.py new file mode 100644 index 0000000..54d0c8f --- /dev/null +++ b/ccxt/test/exchange/async/test_fetch_orders.py @@ -0,0 +1,27 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_order # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +async def test_fetch_orders(exchange, skipped_properties, symbol): + method = 'fetchOrders' + orders = await exchange.fetch_orders(symbol) + assert isinstance(orders, list), exchange.id + ' ' + method + ' must return an array, returned ' + exchange.json(orders) + test_shared_methods.assert_non_emtpy_array(exchange, skipped_properties, method, orders, symbol) + now = exchange.milliseconds() + for i in range(0, len(orders)): + test_order(exchange, skipped_properties, method, orders[i], symbol, now) + test_shared_methods.assert_timestamp_order(exchange, method, symbol, orders) + return True diff --git a/ccxt/test/exchange/async/test_fetch_positions.py b/ccxt/test/exchange/async/test_fetch_positions.py new file mode 100644 index 0000000..5460597 --- /dev/null +++ b/ccxt/test/exchange/async/test_fetch_positions.py @@ -0,0 +1,35 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_position # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +async def test_fetch_positions(exchange, skipped_properties, symbol): + method = 'fetchPositions' + now = exchange.milliseconds() + # without symbol + positions = await exchange.fetch_positions() + test_shared_methods.assert_non_emtpy_array(exchange, skipped_properties, method, positions, symbol) + for i in range(0, len(positions)): + test_position(exchange, skipped_properties, method, positions[i], None, now) + # testSharedMethods.assertTimestampOrder (exchange, method, undefined, positions); # currently order of positions does not make sense + # with symbol + positions_for_symbol = await exchange.fetch_positions([symbol]) + assert isinstance(positions_for_symbol, list), exchange.id + ' ' + method + ' must return an array, returned ' + exchange.json(positions_for_symbol) + positions_for_symbol_length = len(positions_for_symbol) + assert positions_for_symbol_length <= 4, exchange.id + ' ' + method + ' positions length for particular symbol should be less than 4, returned ' + exchange.json(positions_for_symbol) + for i in range(0, len(positions_for_symbol)): + test_position(exchange, skipped_properties, method, positions_for_symbol[i], symbol, now) + # testSharedMethods.assertTimestampOrder (exchange, method, symbol, positionsForSymbol); + return True diff --git a/ccxt/test/exchange/async/test_fetch_status.py b/ccxt/test/exchange/async/test_fetch_status.py new file mode 100644 index 0000000..2a14d98 --- /dev/null +++ b/ccxt/test/exchange/async/test_fetch_status.py @@ -0,0 +1,21 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_status # noqa E402 + +async def test_fetch_status(exchange, skipped_properties): + method = 'fetchStatus' + status = await exchange.fetch_status() + test_status(exchange, skipped_properties, method, status, exchange.milliseconds()) + return True diff --git a/ccxt/test/exchange/async/test_fetch_ticker.py b/ccxt/test/exchange/async/test_fetch_ticker.py new file mode 100644 index 0000000..8e29d03 --- /dev/null +++ b/ccxt/test/exchange/async/test_fetch_ticker.py @@ -0,0 +1,21 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_ticker # noqa E402 + +async def test_fetch_ticker(exchange, skipped_properties, symbol): + method = 'fetchTicker' + ticker = await exchange.fetch_ticker(symbol) + test_ticker(exchange, skipped_properties, method, ticker, symbol) + return True diff --git a/ccxt/test/exchange/async/test_fetch_tickers.py b/ccxt/test/exchange/async/test_fetch_tickers.py new file mode 100644 index 0000000..c4cdecf --- /dev/null +++ b/ccxt/test/exchange/async/test_fetch_tickers.py @@ -0,0 +1,59 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +import asyncio +from ccxt.test.exchange.base import test_ticker # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +async def test_fetch_tickers(exchange, skipped_properties, symbol): + without_symbol = test_fetch_tickers_helper(exchange, skipped_properties, None) + with_symbol = test_fetch_tickers_helper(exchange, skipped_properties, [symbol]) + results = await asyncio.gather(*[without_symbol, with_symbol]) + test_fetch_tickers_amounts(exchange, skipped_properties, results[0]) + return results + + +async def test_fetch_tickers_helper(exchange, skipped_properties, arg_symbols, arg_params={}): + method = 'fetchTickers' + response = await exchange.fetch_tickers(arg_symbols, arg_params) + assert isinstance(response, dict), exchange.id + ' ' + method + ' ' + exchange.json(arg_symbols) + ' must return an object. ' + exchange.json(response) + values = list(response.values()) + checked_symbol = None + if arg_symbols is not None and len(arg_symbols) == 1: + checked_symbol = arg_symbols[0] + test_shared_methods.assert_non_emtpy_array(exchange, skipped_properties, method, values, checked_symbol) + for i in range(0, len(values)): + # todo: symbol check here + ticker = values[i] + test_ticker(exchange, skipped_properties, method, ticker, checked_symbol) + return response + + +def test_fetch_tickers_amounts(exchange, skipped_properties, tickers): + tickers_values = list(tickers.values()) + if not ('checkActiveSymbols' in skipped_properties): + # + # ensure all "active" symbols have tickers + # + non_inactive_markets = test_shared_methods.get_active_markets(exchange) + not_inactive_symbols_length = len(non_inactive_markets) + obtained_tickers_length = len(tickers_values) + min_ratio = 0.99 # 1.0 - 0.01 = 0.99, hardcoded to avoid C# transpiler type casting issues + assert obtained_tickers_length >= not_inactive_symbols_length * min_ratio, exchange.id + ' ' + 'fetchTickers' + ' must return tickers for all active markets. but returned: ' + str(obtained_tickers_length) + ' tickers, ' + str(not_inactive_symbols_length) + ' active markets' + # + # ensure tickers length is less than markets length + # + all_markets = exchange.markets + all_markets_length = len(list(all_markets.keys())) + assert obtained_tickers_length <= all_markets_length, exchange.id + ' ' + 'fetchTickers' + ' must return <= than all markets, but returned: ' + str(obtained_tickers_length) + ' tickers, ' + str(all_markets_length) + ' markets' diff --git a/ccxt/test/exchange/async/test_fetch_trades.py b/ccxt/test/exchange/async/test_fetch_trades.py new file mode 100644 index 0000000..b71fd5f --- /dev/null +++ b/ccxt/test/exchange/async/test_fetch_trades.py @@ -0,0 +1,28 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_shared_methods # noqa E402 +from ccxt.test.exchange.base import test_trade # noqa E402 + +async def test_fetch_trades(exchange, skipped_properties, symbol): + method = 'fetchTrades' + trades = await exchange.fetch_trades(symbol) + test_shared_methods.assert_non_emtpy_array(exchange, skipped_properties, method, trades) + now = exchange.milliseconds() + for i in range(0, len(trades)): + test_trade(exchange, skipped_properties, method, trades[i], symbol, now) + test_shared_methods.assert_in_array(exchange, skipped_properties, method, trades[i], 'takerOrMaker', ['taker', None]) + if not ('timestampSort' in skipped_properties): + test_shared_methods.assert_timestamp_order(exchange, method, symbol, trades) + return True diff --git a/ccxt/test/exchange/async/test_fetch_trading_fee.py b/ccxt/test/exchange/async/test_fetch_trading_fee.py new file mode 100644 index 0000000..6a24020 --- /dev/null +++ b/ccxt/test/exchange/async/test_fetch_trading_fee.py @@ -0,0 +1,22 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_trading_fee # noqa E402 + +async def test_fetch_trading_fee(exchange, skipped_properties, symbol): + method = 'fetchTradingFee' + fee = await exchange.fetch_trading_fee(symbol) + assert isinstance(fee, dict), exchange.id + ' ' + method + ' ' + symbol + ' must return an object. ' + exchange.json(fee) + test_trading_fee(exchange, skipped_properties, method, symbol, fee) + return True diff --git a/ccxt/test/exchange/async/test_fetch_trading_fees.py b/ccxt/test/exchange/async/test_fetch_trading_fees.py new file mode 100644 index 0000000..4ebaf49 --- /dev/null +++ b/ccxt/test/exchange/async/test_fetch_trading_fees.py @@ -0,0 +1,26 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_trading_fee # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +async def test_fetch_trading_fees(exchange, skipped_properties): + method = 'fetchTradingFees' + fees = await exchange.fetch_trading_fees() + symbols = list(fees.keys()) + test_shared_methods.assert_non_emtpy_array(exchange, skipped_properties, method, symbols) + for i in range(0, len(symbols)): + symbol = symbols[i] + test_trading_fee(exchange, skipped_properties, method, symbol, fees[symbol]) + return True diff --git a/ccxt/test/exchange/async/test_fetch_transaction_fees.py b/ccxt/test/exchange/async/test_fetch_transaction_fees.py new file mode 100644 index 0000000..afeb78e --- /dev/null +++ b/ccxt/test/exchange/async/test_fetch_transaction_fees.py @@ -0,0 +1,22 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + + + +async def test_fetch_transaction_fees(exchange, skipped_properties): + # const method = 'fetchTransactionFees'; + # const fees = await exchange.fetchTransactionFees (); + # const withdrawKeys = Object.keys (fees['withdraw']); + # todo : assert each entry + return None diff --git a/ccxt/test/exchange/async/test_fetch_withdrawals.py b/ccxt/test/exchange/async/test_fetch_withdrawals.py new file mode 100644 index 0000000..ec03b2f --- /dev/null +++ b/ccxt/test/exchange/async/test_fetch_withdrawals.py @@ -0,0 +1,26 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_deposit_withdrawal # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +async def test_fetch_withdrawals(exchange, skipped_properties, code): + method = 'fetchWithdrawals' + transactions = await exchange.fetch_withdrawals(code) + test_shared_methods.assert_non_emtpy_array(exchange, skipped_properties, method, transactions, code) + now = exchange.milliseconds() + for i in range(0, len(transactions)): + test_deposit_withdrawal(exchange, skipped_properties, method, transactions[i], code, now) + test_shared_methods.assert_timestamp_order(exchange, method, code, transactions) + return True diff --git a/ccxt/test/exchange/async/test_load_markets.py b/ccxt/test/exchange/async/test_load_markets.py new file mode 100644 index 0000000..8966db6 --- /dev/null +++ b/ccxt/test/exchange/async/test_load_markets.py @@ -0,0 +1,31 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_market # noqa E402 + +async def test_load_markets(exchange, skipped_properties): + method = 'loadMarkets' + markets = await exchange.load_markets() + assert isinstance(exchange.markets, dict), '.markets is not an object' + assert isinstance(exchange.symbols, list), '.symbols is not an array' + symbols_length = len(exchange.symbols) + market_keys = list(exchange.markets.keys()) + market_keys_length = len(market_keys) + assert symbols_length > 0, '.symbols count <= 0 (less than or equal to zero)' + assert market_keys_length > 0, '.markets objects keys length <= 0 (less than or equal to zero)' + assert symbols_length == market_keys_length, 'number of .symbols is not equal to the number of .markets' + market_values = list(markets.values()) + for i in range(0, len(market_values)): + test_market(exchange, skipped_properties, method, market_values[i]) + return True diff --git a/ccxt/test/exchange/async/test_proxies.py b/ccxt/test/exchange/async/test_proxies.py new file mode 100644 index 0000000..57e1480 --- /dev/null +++ b/ccxt/test/exchange/async/test_proxies.py @@ -0,0 +1,73 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +async def test_proxies(exchange, skipped_properties): + await test_proxy_url(exchange, skipped_properties) + await test_http_proxy(exchange, skipped_properties) + # 'httpsProxy', 'socksProxy' + await test_proxy_for_exceptions(exchange, skipped_properties) + + +async def test_proxy_url(exchange, skipped_properties): + method = 'proxyUrl' + proxy_server_ip = '5.75.153.75' + [proxy_url, http_proxy, https_proxy, socks_proxy] = test_shared_methods.remove_proxy_options(exchange, skipped_properties) + exchange.proxy_url = 'http://' + proxy_server_ip + ':8090/proxy_url.php?caller=https://ccxt.com&url=' + encoded_colon = '%3A' + encoded_slash = '%2F' + ip_check_url = 'https' + encoded_colon + encoded_slash + encoded_slash + 'api.ipify.org' + response = await exchange.fetch(ip_check_url) + assert response == proxy_server_ip, exchange.id + ' ' + method + ' test failed. Returned response is ' + response + ' while it should be "' + proxy_server_ip + '"' + # reset the instance property + test_shared_methods.set_proxy_options(exchange, skipped_properties, proxy_url, http_proxy, https_proxy, socks_proxy) + return True + + +async def test_http_proxy(exchange, skipped_properties): + method = 'httpProxy' + proxy_server_ip = '5.75.153.75' + [proxy_url, http_proxy, https_proxy, socks_proxy] = test_shared_methods.remove_proxy_options(exchange, skipped_properties) + exchange.http_proxy = 'http://' + proxy_server_ip + ':8911' + ip_check_url = 'https://api.ipify.org/' + response = await exchange.fetch(ip_check_url) + assert response == proxy_server_ip, exchange.id + ' ' + method + ' test failed. Returned response is ' + response + ' while it should be "' + proxy_server_ip + '"' + # reset the instance property + test_shared_methods.set_proxy_options(exchange, skipped_properties, proxy_url, http_proxy, https_proxy, socks_proxy) + + +# with the below method we test out all variations of possible proxy options, so at least 2 of them should be set together, and such cases must throw exception +async def test_proxy_for_exceptions(exchange, skipped_properties): + method = 'testProxyForExceptions' + [proxy_url, http_proxy, https_proxy, socks_proxy] = test_shared_methods.remove_proxy_options(exchange, skipped_properties) + possible_options_array = ['proxyUrl', 'proxyUrlCallback', 'proxy_url', 'proxy_url_callback', 'httpProxy', 'httpProxyCallback', 'http_proxy', 'http_proxy_callback', 'httpsProxy', 'httpsProxyCallback', 'https_proxy', 'https_proxy_callback', 'socksProxy', 'socksProxyCallback', 'socks_proxy', 'socks_proxy_callback'] + for i in range(0, len(possible_options_array)): + for j in range(0, len(possible_options_array)): + if j != i: + proxy_first = possible_options_array[i] + proxy_second = possible_options_array[j] + exchange.set_property(exchange, proxy_first, '0.0.0.0') # actual value does not matter + exchange.set_property(exchange, proxy_second, '0.0.0.0') # actual value does not matter + exception_caught = False + try: + await exchange.fetch('http://example.com') # url does not matter, it will not be called + except Exception as e: + exception_caught = True + assert exception_caught, exchange.id + ' ' + method + ' test failed. No exception was thrown, while ' + proxy_first + ' and ' + proxy_second + ' were set together' + # reset to undefined + exchange.set_property(exchange, proxy_first, None) + exchange.set_property(exchange, proxy_second, None) + # reset the instance property + test_shared_methods.set_proxy_options(exchange, skipped_properties, proxy_url, http_proxy, https_proxy, socks_proxy) diff --git a/ccxt/test/exchange/async/test_sign_in.py b/ccxt/test/exchange/async/test_sign_in.py new file mode 100644 index 0000000..754a3d6 --- /dev/null +++ b/ccxt/test/exchange/async/test_sign_in.py @@ -0,0 +1,21 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + + + +async def test_sign_in(exchange, skipped_properties): + method = 'signIn' + if exchange.has[method]: + await exchange.sign_in() + return True diff --git a/ccxt/test/exchange/base/__init__.py b/ccxt/test/exchange/base/__init__.py new file mode 100644 index 0000000..ca72c6f --- /dev/null +++ b/ccxt/test/exchange/base/__init__.py @@ -0,0 +1,29 @@ + +# ---------------------------------------------------------------------------- +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code +# ----------------------------------------------------------------------------- + +from ccxt.test.exchange.base.test_account import test_account # noqa E402 +from ccxt.test.exchange.base.test_balance import test_balance # noqa E402 +from ccxt.test.exchange.base.test_borrow_interest import test_borrow_interest # noqa E402 +from ccxt.test.exchange.base.test_borrow_rate import test_borrow_rate # noqa E402 +from ccxt.test.exchange.base.test_currency import test_currency # noqa E402 +from ccxt.test.exchange.base.test_deposit_withdrawal import test_deposit_withdrawal # noqa E402 +from ccxt.test.exchange.base.test_funding_rate_history import test_funding_rate_history # noqa E402 +from ccxt.test.exchange.base.test_last_price import test_last_price # noqa E402 +from ccxt.test.exchange.base.test_ledger_entry import test_ledger_entry # noqa E402 +from ccxt.test.exchange.base.test_leverage_tier import test_leverage_tier # noqa E402 +from ccxt.test.exchange.base.test_liquidation import test_liquidation # noqa E402 +from ccxt.test.exchange.base.test_margin_mode import test_margin_mode # noqa E402 +from ccxt.test.exchange.base.test_margin_modification import test_margin_modification # noqa E402 +from ccxt.test.exchange.base.test_market import test_market # noqa E402 +from ccxt.test.exchange.base.test_ohlcv import test_ohlcv # noqa E402 +from ccxt.test.exchange.base.test_open_interest import test_open_interest # noqa E402 +from ccxt.test.exchange.base.test_order import test_order # noqa E402 +from ccxt.test.exchange.base.test_order_book import test_order_book # noqa E402 +from ccxt.test.exchange.base.test_position import test_position # noqa E402 +from ccxt.test.exchange.base.test_status import test_status # noqa E402 +from ccxt.test.exchange.base.test_ticker import test_ticker # noqa E402 +from ccxt.test.exchange.base.test_trade import test_trade # noqa E402 +from ccxt.test.exchange.base.test_trading_fee import test_trading_fee # noqa E402 \ No newline at end of file diff --git a/ccxt/test/exchange/base/test_account.py b/ccxt/test/exchange/base/test_account.py new file mode 100644 index 0000000..eca569c --- /dev/null +++ b/ccxt/test/exchange/base/test_account.py @@ -0,0 +1,26 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +def test_account(exchange, skipped_properties, method, entry): + format = { + 'info': {}, + 'code': 'BTC', + 'type': 'spot', + 'id': '12345', + } + empty_allowed_for = ['code', 'id'] + test_shared_methods.assert_structure(exchange, skipped_properties, method, entry, format, empty_allowed_for) + test_shared_methods.assert_currency_code(exchange, skipped_properties, method, entry, entry['code']) diff --git a/ccxt/test/exchange/base/test_balance.py b/ccxt/test/exchange/base/test_balance.py new file mode 100644 index 0000000..07ac24c --- /dev/null +++ b/ccxt/test/exchange/base/test_balance.py @@ -0,0 +1,56 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.base.precise import Precise # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +def test_balance(exchange, skipped_properties, method, entry): + format = { + 'free': {}, + 'used': {}, + 'total': {}, + 'info': {}, + } + test_shared_methods.assert_structure(exchange, skipped_properties, method, entry, format) + log_text = test_shared_methods.log_template(exchange, method, entry) + # + codes_total = list(entry['total'].keys()) + codes_free = list(entry['free'].keys()) + codes_used = list(entry['used'].keys()) + test_shared_methods.assert_non_emtpy_array(exchange, skipped_properties, method, codes_total, 'total') + test_shared_methods.assert_non_emtpy_array(exchange, skipped_properties, method, codes_free, 'free') + test_shared_methods.assert_non_emtpy_array(exchange, skipped_properties, method, codes_used, 'used') + all_codes = exchange.array_concat(codes_total, codes_free) + all_codes = exchange.array_concat(all_codes, codes_used) + codes_length = len(codes_total) + free_length = len(codes_free) + used_length = len(codes_used) + assert (codes_length == free_length) or (codes_length == used_length), 'free and total and used codes have different lengths' + log_text + for i in range(0, len(all_codes)): + code = all_codes[i] + # testSharedMethods.assertCurrencyCode (exchange, skippedProperties, method, entry, code); + assert code in entry['total'], 'code ' + code + ' not in total' + log_text + assert code in entry['free'], 'code ' + code + ' not in free' + log_text + assert code in entry['used'], 'code ' + code + ' not in used' + log_text + total = exchange.safe_string(entry['total'], code) + free = exchange.safe_string(entry['free'], code) + used = exchange.safe_string(entry['used'], code) + assert total is not None, 'total is undefined' + log_text + assert free is not None, 'free is undefined' + log_text + assert used is not None, 'used is undefined' + log_text + assert Precise.string_ge(total, '0'), 'total is not positive' + log_text + assert Precise.string_ge(free, '0'), 'free is not positive' + log_text + assert Precise.string_ge(used, '0'), 'used is not positive' + log_text + sum_free_used = Precise.string_add(free, used) + assert Precise.string_eq(total, sum_free_used), 'free and used do not sum to total' + log_text diff --git a/ccxt/test/exchange/base/test_borrow_interest.py b/ccxt/test/exchange/base/test_borrow_interest.py new file mode 100644 index 0000000..5eb760f --- /dev/null +++ b/ccxt/test/exchange/base/test_borrow_interest.py @@ -0,0 +1,35 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +def test_borrow_interest(exchange, skipped_properties, method, entry, requested_code, requested_symbol): + format = { + 'info': {}, + 'account': 'BTC/USDT', + 'currency': 'USDT', + 'interest': exchange.parse_number('0.1444'), + 'interestRate': exchange.parse_number('0.0006'), + 'amountBorrowed': exchange.parse_number('30.0'), + 'timestamp': 1638230400000, + 'datetime': '2021-11-30T00:00:00.000Z', + } + empty_allowed_for = ['account'] + test_shared_methods.assert_structure(exchange, skipped_properties, method, entry, format, empty_allowed_for) + test_shared_methods.assert_timestamp_and_datetime(exchange, skipped_properties, method, entry) + test_shared_methods.assert_currency_code(exchange, skipped_properties, method, entry, entry['currency'], requested_code) + test_shared_methods.assert_symbol(exchange, skipped_properties, method, entry, entry['account'], requested_symbol) + test_shared_methods.assert_greater(exchange, skipped_properties, method, entry, 'interest', '0') + test_shared_methods.assert_greater(exchange, skipped_properties, method, entry, 'interestRate', '0') + test_shared_methods.assert_greater(exchange, skipped_properties, method, entry, 'amountBorrowed', '0') diff --git a/ccxt/test/exchange/base/test_borrow_rate.py b/ccxt/test/exchange/base/test_borrow_rate.py new file mode 100644 index 0000000..e921886 --- /dev/null +++ b/ccxt/test/exchange/base/test_borrow_rate.py @@ -0,0 +1,32 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +def test_borrow_rate(exchange, skipped_properties, method, entry, requested_code): + format = { + 'info': {}, + 'currency': 'USDT', + 'timestamp': 1638230400000, + 'datetime': '2021-11-30T00:00:00.000Z', + 'rate': exchange.parse_number('0.0006'), + 'period': 86400000, + } + test_shared_methods.assert_structure(exchange, skipped_properties, method, entry, format) + test_shared_methods.assert_timestamp_and_datetime(exchange, skipped_properties, method, entry) + test_shared_methods.assert_currency_code(exchange, skipped_properties, method, entry, entry['currency'], requested_code) + # + # assert (borrowRate['period'] === 86400000 || borrowRate['period'] === 3600000) # Milliseconds in an hour or a day + test_shared_methods.assert_greater(exchange, skipped_properties, method, entry, 'period', '0') + test_shared_methods.assert_greater(exchange, skipped_properties, method, entry, 'rate', '0') diff --git a/ccxt/test/exchange/base/test_currency.py b/ccxt/test/exchange/base/test_currency.py new file mode 100644 index 0000000..c334519 --- /dev/null +++ b/ccxt/test/exchange/base/test_currency.py @@ -0,0 +1,85 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.base.decimal_to_precision import SIGNIFICANT_DIGITS # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +def test_currency(exchange, skipped_properties, method, entry): + format = { + 'id': 'btc', + 'code': 'BTC', + } + # todo: remove fee from empty + empty_allowed_for = ['name', 'fee'] + # todo: info key needs to be added in base, when exchange does not have fetchCurrencies + is_native = exchange.has['fetchCurrencies'] and exchange.has['fetchCurrencies'] != 'emulated' + currency_type = exchange.safe_string(entry, 'type') + if is_native: + format['info'] = {} + # todo: 'name': 'Bitcoin', # uppercase string, base currency, 2 or more letters + format['withdraw'] = True # withdraw enabled + format['deposit'] = True # deposit enabled + format['precision'] = exchange.parse_number('0.0001') # in case of SIGNIFICANT_DIGITS it will be 4 - number of digits "after the dot" + format['fee'] = exchange.parse_number('0.001') + format['networks'] = {} + format['limits'] = { + 'withdraw': { + 'min': exchange.parse_number('0.01'), + 'max': exchange.parse_number('1000'), + }, + 'deposit': { + 'min': exchange.parse_number('0.01'), + 'max': exchange.parse_number('1000'), + }, + } + format['type'] = 'crypto' # crypto, fiat, leverage, other + test_shared_methods.assert_in_array(exchange, skipped_properties, method, entry, 'type', ['fiat', 'crypto', 'leveraged', 'other', None]) # todo: remove undefined + # only require "deposit" & "withdraw" values, when currency is not fiat, or when it's fiat, but not skipped + if currency_type != 'crypto' and ('depositForNonCrypto' in skipped_properties): + empty_allowed_for.append('deposit') + if currency_type != 'crypto' and ('withdrawForNonCrypto' in skipped_properties): + empty_allowed_for.append('withdraw') + if currency_type == 'leveraged' or currency_type == 'other': + empty_allowed_for.append('precision') + # + test_shared_methods.assert_currency_code(exchange, skipped_properties, method, entry, entry['code']) + # check if empty networks should be skipped + networks = exchange.safe_dict(entry, 'networks', {}) + network_keys = list(networks.keys()) + network_keys_length = len(network_keys) + if network_keys_length == 0 and ('skipCurrenciesWithoutNetworks' in skipped_properties): + return + # + test_shared_methods.assert_structure(exchange, skipped_properties, method, entry, format, empty_allowed_for) + # + test_shared_methods.check_precision_accuracy(exchange, skipped_properties, method, entry, 'precision') + test_shared_methods.assert_greater_or_equal(exchange, skipped_properties, method, entry, 'fee', '0') + if not ('limits' in skipped_properties): + limits = exchange.safe_value(entry, 'limits', {}) + withdraw_limits = exchange.safe_value(limits, 'withdraw', {}) + deposit_limits = exchange.safe_value(limits, 'deposit', {}) + test_shared_methods.assert_greater_or_equal(exchange, skipped_properties, method, withdraw_limits, 'min', '0') + test_shared_methods.assert_greater_or_equal(exchange, skipped_properties, method, withdraw_limits, 'max', '0') + test_shared_methods.assert_greater_or_equal(exchange, skipped_properties, method, deposit_limits, 'min', '0') + test_shared_methods.assert_greater_or_equal(exchange, skipped_properties, method, deposit_limits, 'max', '0') + # max should be more than min (withdrawal limits) + min_string_withdrawal = exchange.safe_string(withdraw_limits, 'min') + if min_string_withdrawal is not None: + test_shared_methods.assert_greater_or_equal(exchange, skipped_properties, method, withdraw_limits, 'max', min_string_withdrawal) + # max should be more than min (deposit limits) + min_string_deposit = exchange.safe_string(deposit_limits, 'min') + if min_string_deposit is not None: + test_shared_methods.assert_greater_or_equal(exchange, skipped_properties, method, deposit_limits, 'max', min_string_deposit) + # check valid ID & CODE + test_shared_methods.assert_valid_currency_id_and_code(exchange, skipped_properties, method, entry, entry['id'], entry['code']) diff --git a/ccxt/test/exchange/base/test_deposit_withdrawal.py b/ccxt/test/exchange/base/test_deposit_withdrawal.py new file mode 100644 index 0000000..3437f25 --- /dev/null +++ b/ccxt/test/exchange/base/test_deposit_withdrawal.py @@ -0,0 +1,50 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +def test_deposit_withdrawal(exchange, skipped_properties, method, entry, requested_code, now): + format = { + 'info': {}, + 'id': '1234', + 'txid': '0x1345FEG45EAEF7', + 'timestamp': 1502962946216, + 'datetime': '2017-08-17 12:42:48.000', + 'network': 'ETH', + 'address': '0xEFE3487358AEF352345345', + 'addressTo': '0xEFE3487358AEF352345123', + 'addressFrom': '0xEFE3487358AEF352345456', + 'tag': 'smth', + 'tagTo': 'smth', + 'tagFrom': 'smth', + 'type': 'deposit', + 'amount': exchange.parse_number('1.234'), + 'currency': 'USDT', + 'status': 'ok', + 'updated': 1502962946233, + 'fee': {}, + } + empty_allowed_for = ['address', 'addressTo', 'addressFrom', 'tag', 'tagTo', 'tagFrom'] # below we still do assertion for to/from + test_shared_methods.assert_structure(exchange, skipped_properties, method, entry, format, empty_allowed_for) + test_shared_methods.assert_timestamp_and_datetime(exchange, skipped_properties, method, entry, now) + test_shared_methods.assert_currency_code(exchange, skipped_properties, method, entry, entry['currency'], requested_code) + # + test_shared_methods.assert_in_array(exchange, skipped_properties, method, entry, 'status', ['ok', 'pending', 'failed', 'rejected', 'canceled']) + test_shared_methods.assert_in_array(exchange, skipped_properties, method, entry, 'type', ['deposit', 'withdrawal']) + test_shared_methods.assert_greater_or_equal(exchange, skipped_properties, method, entry, 'amount', '0') + test_shared_methods.assert_fee_structure(exchange, skipped_properties, method, entry, 'fee') + if entry['type'] == 'deposit': + test_shared_methods.assert_type(exchange, skipped_properties, entry, 'addressFrom', format) + else: + test_shared_methods.assert_type(exchange, skipped_properties, entry, 'addressTo', format) diff --git a/ccxt/test/exchange/base/test_funding_rate_history.py b/ccxt/test/exchange/base/test_funding_rate_history.py new file mode 100644 index 0000000..345d280 --- /dev/null +++ b/ccxt/test/exchange/base/test_funding_rate_history.py @@ -0,0 +1,29 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +def test_funding_rate_history(exchange, skipped_properties, method, entry, symbol): + format = { + 'info': {}, + 'symbol': 'BTC/USDT:USDT', + 'timestamp': 1638230400000, + 'datetime': '2021-11-30T00:00:00.000Z', + 'fundingRate': exchange.parse_number('0.0006'), + } + test_shared_methods.assert_structure(exchange, skipped_properties, method, entry, format) + test_shared_methods.assert_symbol(exchange, skipped_properties, method, entry, 'symbol', symbol) + test_shared_methods.assert_timestamp_and_datetime(exchange, skipped_properties, method, entry) + test_shared_methods.assert_greater(exchange, skipped_properties, method, entry, 'fundingRate', '-100') + test_shared_methods.assert_less(exchange, skipped_properties, method, entry, 'fundingRate', '100') diff --git a/ccxt/test/exchange/base/test_last_price.py b/ccxt/test/exchange/base/test_last_price.py new file mode 100644 index 0000000..4639090 --- /dev/null +++ b/ccxt/test/exchange/base/test_last_price.py @@ -0,0 +1,31 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +def test_last_price(exchange, skipped_properties, method, entry, symbol): + format = { + 'info': {}, + 'symbol': 'ETH/BTC', + 'timestamp': 1502962946216, + 'datetime': '2017-09-01T00:00:00', + 'price': exchange.parse_number('1.234'), + 'side': 'buy', + } + empty_allowed_for = ['timestamp', 'datetime', 'side', 'price'] # binance sometimes provides empty prices for old pairs + test_shared_methods.assert_structure(exchange, skipped_properties, method, entry, format, empty_allowed_for) + test_shared_methods.assert_timestamp_and_datetime(exchange, skipped_properties, method, entry) + # + test_shared_methods.assert_greater(exchange, skipped_properties, method, entry, 'price', '0') + test_shared_methods.assert_in_array(exchange, skipped_properties, method, entry, 'side', ['buy', 'sell', None]) diff --git a/ccxt/test/exchange/base/test_ledger_entry.py b/ccxt/test/exchange/base/test_ledger_entry.py new file mode 100644 index 0000000..130306f --- /dev/null +++ b/ccxt/test/exchange/base/test_ledger_entry.py @@ -0,0 +1,45 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +def test_ledger_entry(exchange, skipped_properties, method, entry, requested_code, now): + format = { + 'info': {}, + 'id': 'x1234', + 'currency': 'BTC', + 'account': 'spot', + 'referenceId': 'foo', + 'referenceAccount': 'bar', + 'status': 'ok', + 'amount': exchange.parse_number('22'), + 'before': exchange.parse_number('111'), + 'after': exchange.parse_number('133'), + 'fee': {}, + 'direction': 'in', + 'timestamp': 1638230400000, + 'datetime': '2021-11-30T00:00:00.000Z', + 'type': 'deposit', + } + empty_allowed_for = ['referenceId', 'referenceAccount', 'id'] + test_shared_methods.assert_structure(exchange, skipped_properties, method, entry, format, empty_allowed_for) + test_shared_methods.assert_timestamp_and_datetime(exchange, skipped_properties, method, entry, now) + test_shared_methods.assert_currency_code(exchange, skipped_properties, method, entry, entry['currency'], requested_code) + # + test_shared_methods.assert_in_array(exchange, skipped_properties, method, entry, 'direction', ['in', 'out']) + test_shared_methods.assert_in_array(exchange, skipped_properties, method, entry, 'type', ['trade', 'transaction', 'margin', 'cashback', 'referral', 'transfer', 'fee']) + # testSharedMethods.assertInArray (exchange, skippedProperties, method, entry, 'account', ['spot', 'swap', .. ]); # todo + test_shared_methods.assert_greater_or_equal(exchange, skipped_properties, method, entry, 'amount', '0') + test_shared_methods.assert_greater_or_equal(exchange, skipped_properties, method, entry, 'before', '0') + test_shared_methods.assert_greater_or_equal(exchange, skipped_properties, method, entry, 'after', '0') diff --git a/ccxt/test/exchange/base/test_leverage_tier.py b/ccxt/test/exchange/base/test_leverage_tier.py new file mode 100644 index 0000000..c272fe8 --- /dev/null +++ b/ccxt/test/exchange/base/test_leverage_tier.py @@ -0,0 +1,33 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +def test_leverage_tier(exchange, skipped_properties, method, entry): + format = { + 'tier': exchange.parse_number('1'), + 'minNotional': exchange.parse_number('0'), + 'maxNotional': exchange.parse_number('5000'), + 'maintenanceMarginRate': exchange.parse_number('0.01'), + 'maxLeverage': exchange.parse_number('25'), + 'info': {}, + } + empty_allowed_for = ['maintenanceMarginRate'] + test_shared_methods.assert_structure(exchange, skipped_properties, method, entry, format, empty_allowed_for) + # + test_shared_methods.assert_greater_or_equal(exchange, skipped_properties, method, entry, 'tier', '0') + test_shared_methods.assert_greater_or_equal(exchange, skipped_properties, method, entry, 'minNotional', '0') + test_shared_methods.assert_greater_or_equal(exchange, skipped_properties, method, entry, 'maxNotional', '0') + test_shared_methods.assert_greater_or_equal(exchange, skipped_properties, method, entry, 'maxLeverage', '1') + test_shared_methods.assert_less_or_equal(exchange, skipped_properties, method, entry, 'maintenanceMarginRate', '1') diff --git a/ccxt/test/exchange/base/test_liquidation.py b/ccxt/test/exchange/base/test_liquidation.py new file mode 100644 index 0000000..7b336bc --- /dev/null +++ b/ccxt/test/exchange/base/test_liquidation.py @@ -0,0 +1,50 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.base.precise import Precise # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +def test_liquidation(exchange, skipped_properties, method, entry, symbol): + format = { + 'info': {}, + 'symbol': 'ETH/BTC', + 'contracts': exchange.parse_number('1.234'), + 'contractSize': exchange.parse_number('1.234'), + 'price': exchange.parse_number('1.234'), + 'baseValue': exchange.parse_number('1.234'), + 'quoteValue': exchange.parse_number('1.234'), + 'timestamp': 1502962946216, + 'datetime': '2017-09-01T00:00:00', + } + # todo: atm, many exchanges fail, so temporarily decrease stict mode + empty_allowed_for = ['timestamp', 'datetime', 'quoteValue', 'baseValue', 'previousClose', 'price', 'contractSize', 'contracts'] + test_shared_methods.assert_structure(exchange, skipped_properties, method, entry, format, empty_allowed_for) + test_shared_methods.assert_timestamp_and_datetime(exchange, skipped_properties, method, entry) + log_text = test_shared_methods.log_template(exchange, method, entry) + test_shared_methods.assert_greater(exchange, skipped_properties, method, entry, 'contracts', '0') + test_shared_methods.assert_greater(exchange, skipped_properties, method, entry, 'contractSize', '0') + test_shared_methods.assert_greater(exchange, skipped_properties, method, entry, 'price', '0') + test_shared_methods.assert_greater(exchange, skipped_properties, method, entry, 'baseValue', '0') + test_shared_methods.assert_greater(exchange, skipped_properties, method, entry, 'quoteValue', '0') + contracts = exchange.safe_string(entry, 'contracts') + contract_size = exchange.safe_string(entry, 'contractSize') + price = exchange.safe_string(entry, 'price') + base_value = exchange.safe_string(entry, 'baseValue') + if contracts and contract_size: + assert Precise.string_eq(base_value, Precise.string_mul(contracts, contract_size)), 'baseValue == contracts * contractSize' + log_text + if price: + assert Precise.string_eq(base_value, Precise.string_mul(Precise.string_mul(contracts, contract_size), price)), 'quoteValue == contracts * contractSize * price' + log_text + # if singular was called, then symbol needs to be asserted + if method == 'watchLiquidations' or method == 'fetchLiquidations': + test_shared_methods.assert_symbol(exchange, skipped_properties, method, entry, 'symbol', symbol) diff --git a/ccxt/test/exchange/base/test_margin_mode.py b/ccxt/test/exchange/base/test_margin_mode.py new file mode 100644 index 0000000..5c3f813 --- /dev/null +++ b/ccxt/test/exchange/base/test_margin_mode.py @@ -0,0 +1,24 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +def test_margin_mode(exchange, skipped_properties, method, entry): + format = { + 'info': {}, + 'symbol': 'BTC/USDT:USDT', + 'marginMode': 'cross', + } + empty_allowed_for = ['symbol'] + test_shared_methods.assert_structure(exchange, skipped_properties, method, entry, format, empty_allowed_for) diff --git a/ccxt/test/exchange/base/test_margin_modification.py b/ccxt/test/exchange/base/test_margin_modification.py new file mode 100644 index 0000000..660d1f5 --- /dev/null +++ b/ccxt/test/exchange/base/test_margin_modification.py @@ -0,0 +1,35 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +def test_margin_modification(exchange, skipped_properties, method, entry): + format = { + 'info': {}, + 'type': 'add', + 'amount': exchange.parse_number('0.1'), + 'total': exchange.parse_number('0.29934828'), + 'code': 'USDT', + 'symbol': 'ADA/USDT:USDT', + 'status': 'ok', + } + empty_allowed_for = ['status', 'symbol', 'code', 'total', 'amount'] + test_shared_methods.assert_structure(exchange, skipped_properties, method, entry, format, empty_allowed_for) + test_shared_methods.assert_currency_code(exchange, skipped_properties, method, entry, entry['code']) + # + test_shared_methods.assert_greater_or_equal(exchange, skipped_properties, method, entry, 'amount', '0') + test_shared_methods.assert_greater_or_equal(exchange, skipped_properties, method, entry, 'total', '0') + test_shared_methods.assert_in_array(exchange, skipped_properties, method, entry, 'type', ['add', 'reduce', 'set']) + test_shared_methods.assert_in_array(exchange, skipped_properties, method, entry, 'status', ['ok', 'pending', 'canceled', 'failed']) + test_shared_methods.assert_symbol(exchange, skipped_properties, method, entry, 'symbol') diff --git a/ccxt/test/exchange/base/test_market.py b/ccxt/test/exchange/base/test_market.py new file mode 100644 index 0000000..21bf9a0 --- /dev/null +++ b/ccxt/test/exchange/base/test_market.py @@ -0,0 +1,241 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.base.precise import Precise # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +def test_market(exchange, skipped_properties, method, market): + format = { + 'id': 'btcusd', + 'symbol': 'BTC/USD', + 'base': 'BTC', + 'quote': 'USD', + 'taker': exchange.parse_number('0.0011'), + 'maker': exchange.parse_number('0.0009'), + 'baseId': 'btc', + 'quoteId': 'usd', + 'active': False, + 'type': 'spot', + 'linear': False, + 'inverse': False, + 'spot': False, + 'swap': False, + 'future': False, + 'option': False, + 'margin': False, + 'contract': False, + 'contractSize': exchange.parse_number('0.001'), + 'expiry': 1656057600000, + 'expiryDatetime': '2022-06-24T08:00:00.000Z', + 'optionType': 'put', + 'strike': exchange.parse_number('56000'), + 'settle': 'XYZ', + 'settleId': 'Xyz', + 'precision': { + 'price': exchange.parse_number('0.001'), + 'amount': exchange.parse_number('0.001'), + 'cost': exchange.parse_number('0.001'), + }, + 'limits': { + 'amount': { + 'min': exchange.parse_number('0.01'), + 'max': exchange.parse_number('1000'), + }, + 'price': { + 'min': exchange.parse_number('0.01'), + 'max': exchange.parse_number('1000'), + }, + 'cost': { + 'min': exchange.parse_number('0.01'), + 'max': exchange.parse_number('1000'), + }, + }, + 'marginModes': { + 'cross': True, + 'isolated': False, + }, + 'info': {}, + } + # temporary: only test QUANTO markets where that prop exists (todo: add in type later) + if 'quanto' in market: + format['quanto'] = False # whether the market is QUANTO or not + # define locals + spot = market['spot'] + contract = market['contract'] + swap = market['swap'] + future = market['future'] + option = market['option'] + index = exchange.safe_bool(market, 'index') # todo: unify + is_index = (index is not None) and index + linear = market['linear'] + inverse = market['inverse'] + quanto = exchange.safe_bool(market, 'quanto') # todo: unify + is_quanto = (quanto is not None) and quanto + # + empty_allowed_for = ['margin'] + if not contract: + empty_allowed_for.append('contractSize') + empty_allowed_for.append('linear') + empty_allowed_for.append('inverse') + empty_allowed_for.append('quanto') + empty_allowed_for.append('settle') + empty_allowed_for.append('settleId') + if not future and not option: + empty_allowed_for.append('expiry') + empty_allowed_for.append('expiryDatetime') + if not option: + empty_allowed_for.append('optionType') + empty_allowed_for.append('strike') + test_shared_methods.assert_structure(exchange, skipped_properties, method, market, format, empty_allowed_for) + test_shared_methods.assert_symbol(exchange, skipped_properties, method, market, 'symbol') + log_text = test_shared_methods.log_template(exchange, method, market) + # check taker/maker + # todo: check not all to be within 0-1.0 + test_shared_methods.assert_greater(exchange, skipped_properties, method, market, 'taker', '-100') + test_shared_methods.assert_less(exchange, skipped_properties, method, market, 'taker', '100') + test_shared_methods.assert_greater(exchange, skipped_properties, method, market, 'maker', '-100') + test_shared_methods.assert_less(exchange, skipped_properties, method, market, 'maker', '100') + # validate type + valid_types = ['spot', 'margin', 'swap', 'future', 'option', 'index', 'other'] + test_shared_methods.assert_in_array(exchange, skipped_properties, method, market, 'type', valid_types) + # validate subTypes + valid_sub_types = ['linear', 'inverse', 'quanto', None] + test_shared_methods.assert_in_array(exchange, skipped_properties, method, market, 'subType', valid_sub_types) + # check if 'type' is consistent + checked_types = ['spot', 'swap', 'future', 'option'] + for i in range(0, len(checked_types)): + type = checked_types[i] + if market[type]: + assert type == market['type'], 'market.type (' + market['type'] + ') not equal to "' + type + '"' + log_text + # check if 'subType' is consistent + if swap or future: + checked_sub_types = ['linear', 'inverse'] + for i in range(0, len(checked_sub_types)): + sub_type = checked_sub_types[i] + if market[sub_type]: + assert sub_type == market['subType'], 'market.subType (' + market['subType'] + ') not equal to "' + sub_type + '"' + log_text + # margin check (todo: add margin as mandatory, instead of undefined) + if spot: + # for spot market, 'margin' can be either true/false or undefined + test_shared_methods.assert_in_array(exchange, skipped_properties, method, market, 'margin', [True, False, None]) + else: + # otherwise, it must be false or undefined + test_shared_methods.assert_in_array(exchange, skipped_properties, method, market, 'margin', [False, None]) + # check mutually exclusive fields + if spot: + assert not contract and linear is None and inverse is None and not option and not swap and not future, 'for spot market, none of contract/linear/inverse/option/swap/future should be set' + log_text + else: + # if not spot, any of the below should be true + assert contract and (future or swap or option or is_index), 'for non-spot markets, any of (future/swap/option/index) should be set' + log_text + contract_size = exchange.safe_string(market, 'contractSize') + # contract fields + if contract: + if is_quanto: + assert linear is False, 'linear must be false when "quanto" is true' + log_text + assert inverse is False, 'inverse must be false when "quanto" is true' + log_text + else: + # if false or undefined + assert inverse is not None, 'inverse must be defined when "contract" is true' + log_text + assert linear is not None, 'linear must be defined when "contract" is true' + log_text + assert linear != inverse, 'linear and inverse must not be the same' + log_text + # contract size should be defined + assert (('contractSize' in skipped_properties) or contract_size is not None), '"contractSize" must be defined when "contract" is true' + log_text + # contract size should be above zero + assert ('contractSize' in skipped_properties) or Precise.string_gt(contract_size, '0'), '"contractSize" must be > 0 when "contract" is true' + log_text + # settle should be defined + assert ('settle' in skipped_properties) or (market['settle'] is not None and market['settleId'] is not None), '"settle" & "settleId" must be defined when "contract" is true' + log_text + else: + # linear & inverse needs to be undefined + assert linear is None and inverse is None and quanto is None, 'market linear and inverse (and quanto) must be undefined when "contract" is false' + log_text + # contract size should be undefined + assert contract_size is None, '"contractSize" must be undefined when "contract" is false' + log_text + # settle should be undefined + assert (market['settle'] is None) and (market['settleId'] is None), '"settle" must be undefined when "contract" is false' + log_text + # future, swap and option should be mutually exclusive + if market['future']: + assert not market['swap'] and not market['option'] and not is_index, 'market swap and option must be false when "future" is true' + log_text + elif market['swap']: + assert not market['future'] and not market['option'], 'market future and option must be false when "swap" is true' + log_text + elif market['option']: + assert not market['future'] and not market['swap'], 'market future and swap must be false when "option" is true' + log_text + # check specific fields for options & futures + if option or future: + # future or option markets need 'expiry' and 'expiryDatetime' + assert market['expiry'] is not None, '"expiry" must be defined when "future" is true' + log_text + assert market['expiryDatetime'] is not None, '"expiryDatetime" must be defined when "future" is true' + log_text + # expiry datetime should be correct + iso_string = exchange.iso8601(market['expiry']) + assert market['expiryDatetime'] == iso_string, 'expiryDatetime ("' + market['expiryDatetime'] + '") must be equal to expiry in iso8601 format "' + iso_string + '"' + log_text + test_shared_methods.assert_greater(exchange, skipped_properties, method, market, 'expiry', '0') + if option: + # strike should be defined + assert (('strike' in skipped_properties) or market['strike'] is not None), '"strike" must be defined when "option" is true' + log_text + test_shared_methods.assert_greater(exchange, skipped_properties, method, market, 'strike', '0') + # optionType should be defined + assert (('optionType' in skipped_properties) or market['optionType'] is not None), '"optionType" must be defined when "option" is true' + log_text + test_shared_methods.assert_in_array(exchange, skipped_properties, method, market, 'optionType', ['put', 'call']) + else: + # if not option, then strike and optionType should be undefined + assert market['strike'] is None, '"strike" must be undefined when "option" is false' + log_text + assert market['optionType'] is None, '"optionType" must be undefined when "option" is false' + log_text + elif spot: + # otherwise, expiry needs to be undefined + assert (market['expiry'] is None) and (market['expiryDatetime'] is None), '"expiry" and "expiryDatetime" must be undefined when it is not future|option market' + log_text + # check precisions + precision_keys = list(market['precision'].keys()) + precision_keys_len = len(precision_keys) + assert precision_keys_len >= 2, 'precision should have "amount" and "price" keys at least' + log_text + for i in range(0, len(precision_keys)): + price_or_amount_key = precision_keys[i] + # only allow very high priced markets (wher coin costs around 100k) to have a 5$ price tickSize + is_exclusive_pair = market['baseId'] == 'BTC' + is_non_spot = not spot # such high precision is only allowed in contract markets + is_price = price_or_amount_key == 'price' + is_tick_size_5 = Precise.string_eq('5', exchange.safe_string(market['precision'], price_or_amount_key)) + if is_non_spot and is_price and is_exclusive_pair and is_tick_size_5: + continue + if not ('precision' in skipped_properties): + test_shared_methods.check_precision_accuracy(exchange, skipped_properties, method, market['precision'], price_or_amount_key) + is_inactive_market = market['active'] is False + # check limits + limits_keys = list(market['limits'].keys()) + limits_keys_length = len(limits_keys) + assert limits_keys_length >= 3, 'limits should have "amount", "price" and "cost" keys at least' + log_text + for i in range(0, len(limits_keys)): + key = limits_keys[i] + limit_entry = market['limits'][key] + if is_inactive_market: + continue # check limits + if not ('limits' in skipped_properties): + # min >= 0 + test_shared_methods.assert_greater_or_equal(exchange, skipped_properties, method, limit_entry, 'min', '0') + # max >= 0 + test_shared_methods.assert_greater(exchange, skipped_properties, method, limit_entry, 'max', '0') + # max >= min + min_string = exchange.safe_string(limit_entry, 'min') + if min_string is not None: + test_shared_methods.assert_greater_or_equal(exchange, skipped_properties, method, limit_entry, 'max', min_string) + # check currencies + test_shared_methods.assert_valid_currency_id_and_code(exchange, skipped_properties, method, market, market['baseId'], market['base']) + test_shared_methods.assert_valid_currency_id_and_code(exchange, skipped_properties, method, market, market['quoteId'], market['quote']) + test_shared_methods.assert_valid_currency_id_and_code(exchange, skipped_properties, method, market, market['settleId'], market['settle']) + # check ts + test_shared_methods.assert_timestamp(exchange, skipped_properties, method, market, None, 'created') + # margin modes + if not ('marginModes' in skipped_properties): + margin_modes = exchange.safe_dict(market, 'marginModes') # in future, remove safeDict + assert 'cross' in margin_modes, 'marginModes should have "cross" key' + log_text + assert 'isolated' in margin_modes, 'marginModes should have "isolated" key' + log_text + test_shared_methods.assert_in_array(exchange, skipped_properties, method, margin_modes, 'cross', [True, False, None]) + test_shared_methods.assert_in_array(exchange, skipped_properties, method, margin_modes, 'isolated', [True, False, None]) diff --git a/ccxt/test/exchange/base/test_ohlcv.py b/ccxt/test/exchange/base/test_ohlcv.py new file mode 100644 index 0000000..5369cf5 --- /dev/null +++ b/ccxt/test/exchange/base/test_ohlcv.py @@ -0,0 +1,33 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +def test_ohlcv(exchange, skipped_properties, method, entry, symbol, now): + format = [1638230400000, exchange.parse_number('0.123'), exchange.parse_number('0.125'), exchange.parse_number('0.121'), exchange.parse_number('0.122'), exchange.parse_number('123.456')] + empty_not_allowed_for = [0, 1, 2, 3, 4, 5] + test_shared_methods.assert_structure(exchange, skipped_properties, method, entry, format, empty_not_allowed_for) + test_shared_methods.assert_timestamp_and_datetime(exchange, skipped_properties, method, entry, now, 0) + log_text = test_shared_methods.log_template(exchange, method, entry) + # + assert len(entry) >= 6, 'ohlcv array length should be >= 6;' + log_text + if not ('roundTimestamp' in skipped_properties): + test_shared_methods.assert_round_minute_timestamp(exchange, skipped_properties, method, entry, 0) + high = exchange.safe_string(entry, 2) + low = exchange.safe_string(entry, 3) + test_shared_methods.assert_less_or_equal(exchange, skipped_properties, method, entry, '1', high) + test_shared_methods.assert_greater_or_equal(exchange, skipped_properties, method, entry, '1', low) + test_shared_methods.assert_less_or_equal(exchange, skipped_properties, method, entry, '4', high) + test_shared_methods.assert_greater_or_equal(exchange, skipped_properties, method, entry, '4', low) + assert (symbol is None) or (isinstance(symbol, str)), 'symbol ' + symbol + ' is incorrect' + log_text # todo: check with standard symbol check diff --git a/ccxt/test/exchange/base/test_open_interest.py b/ccxt/test/exchange/base/test_open_interest.py new file mode 100644 index 0000000..38aa4af --- /dev/null +++ b/ccxt/test/exchange/base/test_open_interest.py @@ -0,0 +1,32 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +def test_open_interest(exchange, skipped_properties, method, entry): + format = { + 'symbol': 'BTC/USDT', + 'openInterestAmount': exchange.parse_number('3544581864.598'), + 'openInterestValue': exchange.parse_number('3544581864.598'), + 'timestamp': 1649373600000, + 'datetime': '2022-04-07T23:20:00.000Z', + 'info': {}, + } + empty_allowed_for = ['symbol', 'timestamp', 'openInterestAmount', 'openInterestValue', 'datetime'] + test_shared_methods.assert_structure(exchange, skipped_properties, method, entry, format, empty_allowed_for) + test_shared_methods.assert_symbol(exchange, skipped_properties, method, entry, 'symbol') + test_shared_methods.assert_timestamp_and_datetime(exchange, skipped_properties, method, entry) + # + test_shared_methods.assert_greater(exchange, skipped_properties, method, entry, 'openInterestAmount', '0') + test_shared_methods.assert_greater(exchange, skipped_properties, method, entry, 'openInterestValue', '0') diff --git a/ccxt/test/exchange/base/test_order.py b/ccxt/test/exchange/base/test_order.py new file mode 100644 index 0000000..901e8de --- /dev/null +++ b/ccxt/test/exchange/base/test_order.py @@ -0,0 +1,69 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_shared_methods # noqa E402 +from ccxt.test.exchange.base.test_trade import test_trade # noqa E402 + +def test_order(exchange, skipped_properties, method, entry, symbol, now): + format = { + 'info': {}, + 'id': '123', + 'clientOrderId': '1234', + 'timestamp': 1649373600000, + 'datetime': '2022-04-07T23:20:00.000Z', + 'lastTradeTimestamp': 1649373610000, + 'symbol': 'XYZ/USDT', + 'type': 'limit', + 'timeInForce': 'GTC', + 'postOnly': True, + 'side': 'sell', + 'price': exchange.parse_number('1.23456'), + 'stopPrice': exchange.parse_number('1.1111'), + 'amount': exchange.parse_number('1.23'), + 'cost': exchange.parse_number('2.34'), + 'average': exchange.parse_number('1.234'), + 'filled': exchange.parse_number('1.23'), + 'remaining': exchange.parse_number('0.123'), + 'status': 'ok', + 'fee': {}, + 'trades': [], + } + empty_allowed_for = ['clientOrderId', 'stopPrice', 'trades', 'timestamp', 'datetime', 'lastTradeTimestamp', 'average', 'type', 'timeInForce', 'postOnly', 'side', 'price', 'amount', 'cost', 'filled', 'remaining', 'status', 'fee'] # there are exchanges that return only order id, so we don't need to strictly requite all props to be set. + test_shared_methods.assert_structure(exchange, skipped_properties, method, entry, format, empty_allowed_for) + test_shared_methods.assert_timestamp_and_datetime(exchange, skipped_properties, method, entry, now) + # + test_shared_methods.assert_in_array(exchange, skipped_properties, method, entry, 'timeInForce', ['GTC', 'GTK', 'IOC', 'FOK', 'PO']) + test_shared_methods.assert_in_array(exchange, skipped_properties, method, entry, 'status', ['open', 'closed', 'canceled']) + test_shared_methods.assert_in_array(exchange, skipped_properties, method, entry, 'side', ['buy', 'sell']) + test_shared_methods.assert_in_array(exchange, skipped_properties, method, entry, 'postOnly', [True, False]) + test_shared_methods.assert_symbol(exchange, skipped_properties, method, entry, 'symbol', symbol) + test_shared_methods.assert_greater(exchange, skipped_properties, method, entry, 'price', '0') + test_shared_methods.assert_greater(exchange, skipped_properties, method, entry, 'stopPrice', '0') + test_shared_methods.assert_greater_or_equal(exchange, skipped_properties, method, entry, 'cost', '0') + test_shared_methods.assert_greater(exchange, skipped_properties, method, entry, 'average', '0') + test_shared_methods.assert_greater_or_equal(exchange, skipped_properties, method, entry, 'filled', '0') + test_shared_methods.assert_greater_or_equal(exchange, skipped_properties, method, entry, 'remaining', '0') + test_shared_methods.assert_greater_or_equal(exchange, skipped_properties, method, entry, 'amount', '0') + test_shared_methods.assert_greater_or_equal(exchange, skipped_properties, method, entry, 'amount', exchange.safe_string(entry, 'remaining')) + test_shared_methods.assert_greater_or_equal(exchange, skipped_properties, method, entry, 'amount', exchange.safe_string(entry, 'filled')) + if not ('trades' in skipped_properties): + skipped_new = exchange.deep_extend(skipped_properties, { + 'timestamp': True, + 'datetime': True, + 'side': True, + }) + if entry['trades'] is not None: + for i in range(0, len(entry['trades'])): + test_trade(exchange, skipped_new, method, entry['trades'][i], symbol, now) + test_shared_methods.assert_fee_structure(exchange, skipped_properties, method, entry, 'fee') diff --git a/ccxt/test/exchange/base/test_order_book.py b/ccxt/test/exchange/base/test_order_book.py new file mode 100644 index 0000000..e64a2eb --- /dev/null +++ b/ccxt/test/exchange/base/test_order_book.py @@ -0,0 +1,66 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.base.precise import Precise # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +def test_order_book(exchange, skipped_properties, method, orderbook, symbol): + format = { + 'symbol': 'ETH/BTC', + 'asks': [[exchange.parse_number('1.24'), exchange.parse_number('0.453')], [exchange.parse_number('1.25'), exchange.parse_number('0.157')]], + 'bids': [[exchange.parse_number('1.23'), exchange.parse_number('0.123')], [exchange.parse_number('1.22'), exchange.parse_number('0.543')]], + 'timestamp': 1504224000000, + 'datetime': '2017-09-01T00:00:00', + 'nonce': 134234234, + } + empty_allowed_for = ['nonce'] + # turn into copy: https://discord.com/channels/690203284119617602/921046068555313202/1220626834887282728 + orderbook = exchange.deep_extend({}, orderbook) + test_shared_methods.assert_structure(exchange, skipped_properties, method, orderbook, format, empty_allowed_for) + test_shared_methods.assert_timestamp_and_datetime(exchange, skipped_properties, method, orderbook) + test_shared_methods.assert_symbol(exchange, skipped_properties, method, orderbook, 'symbol', symbol) + log_text = test_shared_methods.log_template(exchange, method, orderbook) + # todo: check non-emtpy arrays for bids/asks for toptier exchanges + bids = orderbook['bids'] + bids_length = len(bids) + for i in range(0, bids_length): + current_bid_string = exchange.safe_string(bids[i], 0) + if not ('compareToNextItem' in skipped_properties): + next_i = i + 1 + if bids_length > next_i: + next_bid_string = exchange.safe_string(bids[next_i], 0) + assert Precise.string_gt(current_bid_string, next_bid_string), 'current bid should be > than the next one: ' + current_bid_string + '>' + next_bid_string + log_text + if not ('compareToZero' in skipped_properties): + # compare price & volume to zero + test_shared_methods.assert_greater(exchange, skipped_properties, method, bids[i], 0, '0') + test_shared_methods.assert_greater(exchange, skipped_properties, method, bids[i], 1, '0') + asks = orderbook['asks'] + asks_length = len(asks) + for i in range(0, asks_length): + current_ask_string = exchange.safe_string(asks[i], 0) + if not ('compareToNextItem' in skipped_properties): + next_i = i + 1 + if asks_length > next_i: + next_ask_string = exchange.safe_string(asks[next_i], 0) + assert Precise.string_lt(current_ask_string, next_ask_string), 'current ask should be < than the next one: ' + current_ask_string + '<' + next_ask_string + log_text + if not ('compareToZero' in skipped_properties): + # compare price & volume to zero + test_shared_methods.assert_greater(exchange, skipped_properties, method, asks[i], 0, '0') + test_shared_methods.assert_greater(exchange, skipped_properties, method, asks[i], 1, '0') + if not ('spread' in skipped_properties): + if bids_length and asks_length: + first_bid = exchange.safe_string(bids[0], 0) + first_ask = exchange.safe_string(asks[0], 0) + # check bid-ask spread + assert Precise.string_lt(first_bid, first_ask), 'bids[0][0] (' + first_bid + ') should be < than asks[0][0] (' + first_ask + ')' + log_text diff --git a/ccxt/test/exchange/base/test_position.py b/ccxt/test/exchange/base/test_position.py new file mode 100644 index 0000000..f609fae --- /dev/null +++ b/ccxt/test/exchange/base/test_position.py @@ -0,0 +1,60 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +def test_position(exchange, skipped_properties, method, entry, symbol, now): + format = { + 'info': {}, + 'symbol': 'XYZ/USDT', + 'timestamp': 1504224000000, + 'datetime': '2017-09-01T00:00:00', + 'initialMargin': exchange.parse_number('1.234'), + 'initialMarginPercentage': exchange.parse_number('0.123'), + 'maintenanceMargin': exchange.parse_number('1.234'), + 'maintenanceMarginPercentage': exchange.parse_number('0.123'), + 'entryPrice': exchange.parse_number('1.234'), + 'notional': exchange.parse_number('1.234'), + 'leverage': exchange.parse_number('1.234'), + 'unrealizedPnl': exchange.parse_number('1.234'), + 'contracts': exchange.parse_number('1'), + 'contractSize': exchange.parse_number('1.234'), + 'marginRatio': exchange.parse_number('1.234'), + 'liquidationPrice': exchange.parse_number('1.234'), + 'markPrice': exchange.parse_number('1.234'), + 'collateral': exchange.parse_number('1.234'), + 'marginMode': 'cross', + 'side': 'long', + 'percentage': exchange.parse_number('1.234'), + } + emptyot_allowed_for = ['liquidationPrice', 'initialMargin', 'initialMarginPercentage', 'maintenanceMargin', 'maintenanceMarginPercentage', 'marginRatio'] + test_shared_methods.assert_structure(exchange, skipped_properties, method, entry, format, emptyot_allowed_for) + test_shared_methods.assert_timestamp_and_datetime(exchange, skipped_properties, method, entry, now) + test_shared_methods.assert_symbol(exchange, skipped_properties, method, entry, 'symbol', symbol) + test_shared_methods.assert_in_array(exchange, skipped_properties, method, entry, 'side', ['long', 'short']) + test_shared_methods.assert_in_array(exchange, skipped_properties, method, entry, 'marginMode', ['cross', 'isolated']) + test_shared_methods.assert_greater(exchange, skipped_properties, method, entry, 'leverage', '0') + test_shared_methods.assert_less_or_equal(exchange, skipped_properties, method, entry, 'leverage', '200') + test_shared_methods.assert_greater(exchange, skipped_properties, method, entry, 'initialMargin', '0') + test_shared_methods.assert_greater(exchange, skipped_properties, method, entry, 'initialMarginPercentage', '0') + test_shared_methods.assert_greater(exchange, skipped_properties, method, entry, 'maintenanceMargin', '0') + test_shared_methods.assert_greater(exchange, skipped_properties, method, entry, 'maintenanceMarginPercentage', '0') + test_shared_methods.assert_greater(exchange, skipped_properties, method, entry, 'entryPrice', '0') + test_shared_methods.assert_greater(exchange, skipped_properties, method, entry, 'notional', '0') + test_shared_methods.assert_greater(exchange, skipped_properties, method, entry, 'contracts', '0') + test_shared_methods.assert_greater(exchange, skipped_properties, method, entry, 'contractSize', '0') + test_shared_methods.assert_greater(exchange, skipped_properties, method, entry, 'marginRatio', '0') + test_shared_methods.assert_greater(exchange, skipped_properties, method, entry, 'liquidationPrice', '0') + test_shared_methods.assert_greater(exchange, skipped_properties, method, entry, 'markPrice', '0') + test_shared_methods.assert_greater(exchange, skipped_properties, method, entry, 'collateral', '0') diff --git a/ccxt/test/exchange/base/test_shared_methods.py b/ccxt/test/exchange/base/test_shared_methods.py new file mode 100644 index 0000000..a08c1ce --- /dev/null +++ b/ccxt/test/exchange/base/test_shared_methods.py @@ -0,0 +1,551 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.base.decimal_to_precision import DECIMAL_PLACES # noqa E402 +from ccxt.base.decimal_to_precision import TICK_SIZE # noqa E402 +import numbers # noqa E402 +import json # noqa E402 +from ccxt.base.precise import Precise # noqa E402 +from ccxt.base.errors import OnMaintenance # noqa E402 +from ccxt.base.errors import OperationFailed # noqa E402 + +def log_template(exchange, method, entry): + # there are cases when exchange is undefined (eg. base tests) + id = exchange.id if (exchange is not None) else 'undefined' + method_string = method if (method is not None) else 'undefined' + entry_string = exchange.json(entry) if (exchange is not None) else '' + return ' <<< ' + id + ' ' + method_string + ' ::: ' + entry_string + ' >>> ' + + +def is_temporary_failure(e): + return (isinstance(e, OperationFailed)) and (not (isinstance(e, OnMaintenance))) + + +def string_value(value): + string_val = None + if isinstance(value, str): + string_val = value + elif value is None: + string_val = 'undefined' + else: + string_val = str(value) + return string_val + + +def assert_type(exchange, skipped_properties, entry, key, format): + if key in skipped_properties: + return None + # because "typeof" string is not transpilable without === 'name', we list them manually at this moment + entry_key_val = exchange.safe_value(entry, key) + format_key_val = exchange.safe_value(format, key) + same_string = (isinstance(entry_key_val, str)) and (isinstance(format_key_val, str)) + same_numeric = (isinstance(entry_key_val, numbers.Real)) and (isinstance(format_key_val, numbers.Real)) + same_boolean = ((entry_key_val) or (entry_key_val is False)) and ((format_key_val) or (format_key_val is False)) + same_array = isinstance(entry_key_val, list) and isinstance(format_key_val, list) + same_object = (isinstance(entry_key_val, dict)) and (isinstance(format_key_val, dict)) + result = (entry_key_val is None) or same_string or same_numeric or same_boolean or same_array or same_object + return result + + +def assert_structure(exchange, skipped_properties, method, entry, format, empty_allowed_for=None, deep=False): + log_text = log_template(exchange, method, entry) + assert entry is not None, 'item is null/undefined' + log_text + # get all expected & predefined keys for this specific item and ensure thos ekeys exist in parsed structure + allow_empty_skips = exchange.safe_list(skipped_properties, 'allowNull', []) + if empty_allowed_for is not None: + empty_allowed_for = concat(empty_allowed_for, allow_empty_skips) + if isinstance(format, list): + assert isinstance(entry, list), 'entry is not an array' + log_text + real_length = len(entry) + expected_length = len(format) + assert real_length == expected_length, 'entry length is not equal to expected length of ' + str(expected_length) + log_text + for i in range(0, len(format)): + empty_allowed_for_this_key = (empty_allowed_for is None) or exchange.in_array(i, empty_allowed_for) + value = entry[i] + if i in skipped_properties: + continue + # check when: + # - it's not inside "allowe empty values" list + # - it's not undefined + if empty_allowed_for_this_key and (value is None): + continue + assert value is not None, str(i) + ' index is expected to have a value' + log_text + # because of other langs, this is needed for arrays + type_assertion = assert_type(exchange, skipped_properties, entry, i, format) + assert type_assertion, str(i) + ' index does not have an expected type ' + log_text + else: + assert isinstance(entry, dict), 'entry is not an object' + log_text + keys = list(format.keys()) + for i in range(0, len(keys)): + key = keys[i] + if key in skipped_properties: + continue + assert key in entry, '"' + string_value(key) + '" key is missing from structure' + log_text + if key in skipped_properties: + continue + empty_allowed_for_this_key = (empty_allowed_for is None) or exchange.in_array(key, empty_allowed_for) + value = entry[key] + # check when: + # - it's not inside "allowe empty values" list + # - it's not undefined + if empty_allowed_for_this_key and (value is None): + continue + # if it was in needed keys, then it should have value. + assert value is not None, '"' + string_value(key) + '" key is expected to have a value' + log_text + # add exclusion for info key, as it can be any type + if key != 'info': + type_assertion = assert_type(exchange, skipped_properties, entry, key, format) + assert type_assertion, '"' + string_value(key) + '" key is neither undefined, neither of expected type' + log_text + if deep: + if isinstance(value, dict): + assert_structure(exchange, skipped_properties, method, value, format[key], empty_allowed_for, deep) + + +def assert_timestamp(exchange, skipped_properties, method, entry, now_to_check=None, key_name_or_index='timestamp', allow_null=True): + log_text = log_template(exchange, method, entry) + skip_value = exchange.safe_value(skipped_properties, key_name_or_index) + if skip_value is not None: + return # skipped + is_date_time_object = isinstance(key_name_or_index, str) + if is_date_time_object: + assert (key_name_or_index in entry), 'timestamp key "' + key_name_or_index + '" is missing from structure' + log_text + else: + # if index was provided (mostly from fetchOHLCV) then we check if it exists, as mandatory + assert not (entry[key_name_or_index] is None), 'timestamp index ' + string_value(key_name_or_index) + ' is undefined' + log_text + ts = entry[key_name_or_index] + assert ts is not None or allow_null, 'timestamp is null' + log_text + if ts is not None: + assert isinstance(ts, numbers.Real), 'timestamp is not numeric' + log_text + assert isinstance(ts, int), 'timestamp should be an integer' + log_text + min_ts = 1230940800000 # 03 Jan 2009 - first block + max_ts = 2147483648000 # 19 Jan 2038 - max int + assert ts > min_ts, 'timestamp is impossible to be before ' + str(min_ts) + ' (03.01.2009)' + log_text # 03 Jan 2009 - first block + assert ts < max_ts, 'timestamp more than ' + str(max_ts) + ' (19.01.2038)' + log_text # 19 Jan 2038 - int32 overflows # 7258118400000 -> Jan 1 2200 + if now_to_check is not None: + max_ms_offset = 60000 # 1 min + assert ts < now_to_check + max_ms_offset, 'returned item timestamp (' + exchange.iso8601(ts) + ') is ahead of the current time (' + exchange.iso8601(now_to_check) + ')' + log_text + + +def assert_timestamp_and_datetime(exchange, skipped_properties, method, entry, now_to_check=None, key_name_or_index='timestamp', allow_null=True): + log_text = log_template(exchange, method, entry) + skip_value = exchange.safe_value(skipped_properties, key_name_or_index) + if skip_value is not None: + return + assert_timestamp(exchange, skipped_properties, method, entry, now_to_check, key_name_or_index) + is_date_time_object = isinstance(key_name_or_index, str) + # only in case if the entry is a dictionary, thus it must have 'timestamp' & 'datetime' string keys + if is_date_time_object: + # we also test 'datetime' here because it's certain sibling of 'timestamp' + assert ('datetime' in entry), '"datetime" key is missing from structure' + log_text + dt = entry['datetime'] + assert dt is not None or allow_null, 'timestamp is null' + log_text + if dt is not None: + assert isinstance(dt, str), '"datetime" key does not have a string value' + log_text + # there are exceptional cases, like getting microsecond-targeted string '2022-08-08T22:03:19.014680Z', so parsed unified timestamp, which carries only 13 digits (millisecond precision) can not be stringified back to microsecond accuracy, causing the bellow assertion to fail + # assert (dt === exchange.iso8601 (entry['timestamp'])) + # so, we have to compare with millisecond accururacy + dt_parsed = exchange.parse8601(dt) + ts_ms = entry['timestamp'] + diff = abs(dt_parsed - ts_ms) + if diff >= 500: + dt_parsed_string = exchange.iso8601(dt_parsed) + dt_entry_string = exchange.iso8601(ts_ms) + assert False, 'datetime is not iso8601 of timestamp:' + dt_parsed_string + '(string) != ' + dt_entry_string + '(from ts)' + log_text + + +def assert_currency_code(exchange, skipped_properties, method, entry, actual_code, expected_code=None, allow_null=True): + if ('currency' in skipped_properties) or ('currencyIdAndCode' in skipped_properties): + return + log_text = log_template(exchange, method, entry) + assert actual_code is not None or allow_null, 'currency code is null' + log_text + if actual_code is not None: + assert isinstance(actual_code, str), 'currency code should be either undefined or a string' + log_text + assert (actual_code in exchange.currencies), 'currency code ("' + actual_code + '") should be present in exchange.currencies' + log_text + if expected_code is not None: + assert actual_code == expected_code, 'currency code in response ("' + string_value(actual_code) + '") should be equal to expected code ("' + string_value(expected_code) + '")' + log_text + + +def assert_valid_currency_id_and_code(exchange, skipped_properties, method, entry, currency_id, currency_code, allow_null=True): + # this is exclusive exceptional key name to be used in `skip-tests.json`, to skip check for currency id and code + if ('currency' in skipped_properties) or ('currencyIdAndCode' in skipped_properties): + return + log_text = log_template(exchange, method, entry) + undefined_values = currency_id is None and currency_code is None + defined_values = currency_id is not None and currency_code is not None + assert undefined_values or defined_values, 'currencyId and currencyCode should be either both defined or both undefined' + log_text + assert defined_values or allow_null, 'currency code and id is not defined' + log_text + if defined_values: + # check by code + currency_by_code = exchange.currency(currency_code) + assert currency_by_code['id'] == currency_id, 'currencyId "' + string_value(currency_id) + '" does not match currency id from instance: "' + string_value(currency_by_code['id']) + '"' + log_text + # check by id + currency_by_id = exchange.safe_currency(currency_id) + assert currency_by_id['code'] == currency_code, 'currencyCode ' + string_value(currency_code) + ' does not match currency of id: ' + string_value(currency_id) + log_text + + +def assert_symbol(exchange, skipped_properties, method, entry, key, expected_symbol=None, allow_null=True): + if key in skipped_properties: + return + log_text = log_template(exchange, method, entry) + actual_symbol = exchange.safe_string(entry, key) + if actual_symbol is not None: + assert isinstance(actual_symbol, str), 'symbol should be either undefined or a string' + log_text + if expected_symbol is not None: + assert actual_symbol == expected_symbol, 'symbol in response ("' + string_value(actual_symbol) + '") should be equal to expected symbol ("' + string_value(expected_symbol) + '")' + log_text + defined_values = actual_symbol is not None and expected_symbol is not None + assert defined_values or allow_null, 'symbols are not defined' + log_text + + +def assert_symbol_in_markets(exchange, skipped_properties, method, symbol): + log_text = log_template(exchange, method, {}) + assert (symbol in exchange.markets), 'symbol should be present in exchange.symbols' + log_text + + +def assert_greater(exchange, skipped_properties, method, entry, key, compare_to, allow_null=True): + if key in skipped_properties: + return + log_text = log_template(exchange, method, entry) + value = exchange.safe_string(entry, key) + assert value is not None or allow_null, 'value is null' + log_text + if value is not None: + assert Precise.string_gt(value, compare_to), string_value(key) + ' key (with a value of ' + string_value(value) + ') was expected to be > ' + string_value(compare_to) + log_text + + +def assert_greater_or_equal(exchange, skipped_properties, method, entry, key, compare_to, allow_null=True): + if key in skipped_properties: + return + log_text = log_template(exchange, method, entry) + value = exchange.safe_string(entry, key) + assert value is not None or allow_null, 'value is null' + log_text + if value is not None and compare_to is not None: + assert Precise.string_ge(value, compare_to), string_value(key) + ' key (with a value of ' + string_value(value) + ') was expected to be >= ' + string_value(compare_to) + log_text + + +def assert_less(exchange, skipped_properties, method, entry, key, compare_to, allow_null=True): + if key in skipped_properties: + return + log_text = log_template(exchange, method, entry) + value = exchange.safe_string(entry, key) + assert value is not None or allow_null, 'value is null' + log_text + if value is not None and compare_to is not None: + assert Precise.string_lt(value, compare_to), string_value(key) + ' key (with a value of ' + string_value(value) + ') was expected to be < ' + string_value(compare_to) + log_text + + +def assert_less_or_equal(exchange, skipped_properties, method, entry, key, compare_to, allow_null=True): + if key in skipped_properties: + return + log_text = log_template(exchange, method, entry) + value = exchange.safe_string(entry, key) + assert value is not None or allow_null, 'value is null' + log_text + if value is not None and compare_to is not None: + assert Precise.string_le(value, compare_to), string_value(key) + ' key (with a value of ' + string_value(value) + ') was expected to be <= ' + string_value(compare_to) + log_text + + +def assert_equal(exchange, skipped_properties, method, entry, key, compare_to, allow_null=True): + if key in skipped_properties: + return + log_text = log_template(exchange, method, entry) + value = exchange.safe_string(entry, key) + assert value is not None or allow_null, 'value is null' + log_text + if value is not None and compare_to is not None: + assert Precise.string_eq(value, compare_to), string_value(key) + ' key (with a value of ' + string_value(value) + ') was expected to be equal to ' + string_value(compare_to) + log_text + + +def assert_non_equal(exchange, skipped_properties, method, entry, key, compare_to, allow_null=True): + if key in skipped_properties: + return + log_text = log_template(exchange, method, entry) + value = exchange.safe_string(entry, key) + assert value is not None or allow_null, 'value is null' + log_text + if value is not None: + assert not Precise.string_eq(value, compare_to), string_value(key) + ' key (with a value of ' + string_value(value) + ') was expected not to be equal to ' + string_value(compare_to) + log_text + + +def assert_in_array(exchange, skipped_properties, method, entry, key, expected_array, allow_null=True): + if key in skipped_properties: + return + log_text = log_template(exchange, method, entry) + value = exchange.safe_value(entry, key) + assert value is not None or allow_null, 'value is null' + log_text + # todo: remove undefined check + if value is not None: + stingified_array_value = exchange.json(expected_array) # don't use expectedArray.join (','), as it bugs in other languages, if values are bool, undefined or etc.. + assert exchange.in_array(value, expected_array), '"' + string_value(key) + '" key (value "' + string_value(value) + '") is not from the expected list : [' + stingified_array_value + ']' + log_text + + +def assert_fee_structure(exchange, skipped_properties, method, entry, key, allow_null=True): + log_text = log_template(exchange, method, entry) + key_string = string_value(key) + if isinstance(key, int): + assert isinstance(entry, list), 'fee container is expected to be an array' + log_text + assert key < len(entry), 'fee key ' + key_string + ' was expected to be present in entry' + log_text + else: + assert isinstance(entry, dict), 'fee container is expected to be an object' + log_text + assert key in entry, 'fee key "' + key + '" was expected to be present in entry' + log_text + fee_object = exchange.safe_value(entry, key) + assert fee_object is not None or allow_null, 'fee object is null' + log_text + # todo: remove undefined check to make stricter + if fee_object is not None: + assert 'cost' in fee_object, key_string + ' fee object should contain "cost" key' + log_text + if fee_object['cost'] is None: + return # todo: remove undefined check to make stricter + assert isinstance(fee_object['cost'], numbers.Real), key_string + ' "cost" must be numeric type' + log_text + # assertGreaterOrEqual (exchange, skippedProperties, method, feeObject, 'cost', '0'); # fee might be negative in the case of a rebate or reward + assert 'currency' in fee_object, '"' + key_string + '" fee object should contain "currency" key' + log_text + assert_currency_code(exchange, skipped_properties, method, entry, fee_object['currency']) + + +def assert_timestamp_order(exchange, method, code_or_symbol, items, ascending=True): + for i in range(0, len(items)): + if i > 0: + current_ts = items[i - 1]['timestamp'] + next_ts = items[i]['timestamp'] + if current_ts is not None and next_ts is not None: + ascending_or_descending = 'ascending' if ascending else 'descending' + comparison = (current_ts <= next_ts) if ascending else (current_ts >= next_ts) + assert comparison, exchange.id + ' ' + method + ' ' + string_value(code_or_symbol) + ' must return a ' + ascending_or_descending + ' sorted array of items by timestamp, but ' + str(current_ts) + ' is opposite with its next ' + str(next_ts) + ' ' + exchange.json(items) + + +def assert_integer(exchange, skipped_properties, method, entry, key, allow_null=True): + if key in skipped_properties: + return + log_text = log_template(exchange, method, entry) + if entry is not None: + value = exchange.safe_value(entry, key) + assert value is not None or allow_null, 'value is null' + log_text + if value is not None: + is_integer = isinstance(value, int) + assert is_integer, '"' + string_value(key) + '" key (value "' + string_value(value) + '") is not an integer' + log_text + + +def check_precision_accuracy(exchange, skipped_properties, method, entry, key): + if key in skipped_properties: + return + if exchange.is_tick_precision(): + # TICK_SIZE should be above zero + assert_greater(exchange, skipped_properties, method, entry, key, '0') + # the below array of integers are inexistent tick-sizes (theoretically technically possible, but not in real-world cases), so their existence in our case indicates to incorrectly implemented tick-sizes, which might mistakenly be implemented with DECIMAL_PLACES, so we throw error + decimal_numbers = ['2', '3', '4', '5', '6', '7', '8', '9', '11', '12', '13', '14', '15', '16'] + for i in range(0, len(decimal_numbers)): + num = decimal_numbers[i] + num_str = num + assert_non_equal(exchange, skipped_properties, method, entry, key, num_str) + else: + # todo: significant-digits return doubles from `this.parseNumber`, so for now can't assert against integer atm + # assertInteger (exchange, skippedProperties, method, entry, key); # should be integer + assert_less_or_equal(exchange, skipped_properties, method, entry, key, '18') # should be under 18 decimals + assert_greater_or_equal(exchange, skipped_properties, method, entry, key, '-8') # in real-world cases, there would not be less than that + + +def fetch_best_bid_ask(exchange, method, symbol): + log_text = log_template(exchange, method, {}) + # find out best bid/ask price + best_bid = None + best_ask = None + used_method = None + if exchange.has['fetchOrderBook']: + used_method = 'fetchOrderBook' + orderbook = exchange.fetch_order_book(symbol) + bids = exchange.safe_list(orderbook, 'bids') + asks = exchange.safe_list(orderbook, 'asks') + best_bid_array = exchange.safe_list(bids, 0) + best_ask_array = exchange.safe_list(asks, 0) + best_bid = exchange.safe_number(best_bid_array, 0) + best_ask = exchange.safe_number(best_ask_array, 0) + elif exchange.has['fetchBidsAsks']: + used_method = 'fetchBidsAsks' + tickers = exchange.fetch_bids_asks([symbol]) + ticker = exchange.safe_dict(tickers, symbol) + best_bid = exchange.safe_number(ticker, 'bid') + best_ask = exchange.safe_number(ticker, 'ask') + elif exchange.has['fetchTicker']: + used_method = 'fetchTicker' + ticker = exchange.fetch_ticker(symbol) + best_bid = exchange.safe_number(ticker, 'bid') + best_ask = exchange.safe_number(ticker, 'ask') + elif exchange.has['fetchTickers']: + used_method = 'fetchTickers' + tickers = exchange.fetch_tickers([symbol]) + ticker = exchange.safe_dict(tickers, symbol) + best_bid = exchange.safe_number(ticker, 'bid') + best_ask = exchange.safe_number(ticker, 'ask') + # + assert best_bid is not None and best_ask is not None, log_text + ' ' + exchange.id + ' could not get best bid/ask for ' + symbol + ' using ' + used_method + ' while testing ' + method + return [best_bid, best_ask] + + +def fetch_order(exchange, symbol, order_id, skipped_properties): + fetched_order = None + original_id = order_id + # set 'since' to 5 minute ago for optimal results + since_time = exchange.milliseconds() - 1000 * 60 * 5 + # iterate + methods_singular = ['fetchOrder', 'fetchOpenOrder', 'fetchClosedOrder', 'fetchCanceledOrder'] + for i in range(0, len(methods_singular)): + singular_fetch_name = methods_singular[i] + if exchange.has[singular_fetch_name]: + current_order = exchange[singular_fetch_name](original_id, symbol) + # if there is an id inside the order, it means the order was fetched successfully + if current_order['id'] == original_id: + fetched_order = current_order + break + # + # search through plural methods + if fetched_order is None: + methods_plural = ['fetchOrders', 'fetchOpenOrders', 'fetchClosedOrders', 'fetchCanceledOrders'] + for i in range(0, len(methods_plural)): + plural_fetch_name = methods_plural[i] + if exchange.has[plural_fetch_name]: + orders = exchange[plural_fetch_name](symbol, since_time) + found = False + for j in range(0, len(orders)): + current_order = orders[j] + if current_order['id'] == original_id: + fetched_order = current_order + found = True + break + if found: + break + return fetched_order + + +def assert_order_state(exchange, skipped_properties, method, order, asserted_status, strict_check): + # note, `strictCheck` is `true` only from "fetchOrder" cases + log_text = log_template(exchange, method, order) + msg = 'order should be ' + asserted_status + ', but it was not asserted' + log_text + filled = exchange.safe_string(order, 'filled') + amount = exchange.safe_string(order, 'amount') + # shorthand variables + status_undefined = (order['status'] is None) + status_open = (order['status'] == 'open') + status_closed = (order['status'] == 'closed') + status_clanceled = (order['status'] == 'canceled') + filled_defined = (filled is not None) + amount_defined = (amount is not None) + condition = None + # + # ### OPEN STATUS + # + # if strict check, then 'status' must be 'open' and filled amount should be less then whole order amount + strict_open = status_open and (filled_defined and amount_defined and filled < amount) + # if non-strict check, then accept & ignore undefined values + nonstrict_open = (status_open or status_undefined) and ((not filled_defined or not amount_defined) or Precise.string_lt(filled, amount)) + # check + if asserted_status == 'open': + condition = strict_open if strict_check else nonstrict_open + assert condition, msg + return + # + # ### CLOSED STATUS + # + # if strict check, then 'status' must be 'closed' and filled amount should be equal to the whole order amount + closed_strict = status_closed and (filled_defined and amount_defined and Precise.string_eq(filled, amount)) + # if non-strict check, then accept & ignore undefined values + closed_non_strict = (status_closed or status_undefined) and ((not filled_defined or not amount_defined) or Precise.string_eq(filled, amount)) + # check + if asserted_status == 'closed': + condition = closed_strict if strict_check else closed_non_strict + assert condition, msg + return + # + # ### CANCELED STATUS + # + # if strict check, then 'status' must be 'canceled' and filled amount should be less then whole order amount + canceled_strict = status_clanceled and (filled_defined and amount_defined and Precise.string_lt(filled, amount)) + # if non-strict check, then accept & ignore undefined values + canceled_non_strict = (status_clanceled or status_undefined) and ((not filled_defined or not amount_defined) or Precise.string_lt(filled, amount)) + # check + if asserted_status == 'canceled': + condition = canceled_strict if strict_check else canceled_non_strict + assert condition, msg + return + # + # ### CLOSED_or_CANCELED STATUS + # + if asserted_status == 'closed_or_canceled': + condition = (closed_strict or canceled_strict) if strict_check else (closed_non_strict or canceled_non_strict) + assert condition, msg + return + + +def get_active_markets(exchange, include_unknown=True): + filtered_active = exchange.filter_by(exchange.markets, 'active', True) + if include_unknown: + filtered_undefined = exchange.filter_by(exchange.markets, 'active', None) + return exchange.array_concat(filtered_active, filtered_undefined) + return filtered_active + + +def remove_proxy_options(exchange, skipped_properties): + proxy_url = exchange.check_proxy_url_settings() + [http_proxy, https_proxy, socks_proxy] = exchange.check_proxy_settings() + # because of bug in transpiled, about `.proxyUrl` being transpiled into `.proxy_url`, we have to use this workaround + exchange.set_property(exchange, 'proxyUrl', None) + exchange.set_property(exchange, 'proxy_url', None) + exchange.set_property(exchange, 'httpProxy', None) + exchange.set_property(exchange, 'http_proxy', None) + exchange.set_property(exchange, 'httpsProxy', None) + exchange.set_property(exchange, 'https_proxy', None) + exchange.set_property(exchange, 'socksProxy', None) + exchange.set_property(exchange, 'socks_proxy', None) + return [proxy_url, http_proxy, https_proxy, socks_proxy] + + +def set_proxy_options(exchange, skipped_properties, proxy_url, http_proxy, https_proxy, socks_proxy): + exchange.proxy_url = proxy_url + exchange.http_proxy = http_proxy + exchange.https_proxy = https_proxy + exchange.socks_proxy = socks_proxy + + +def concat(a=None, b=None): + # we use this method temporarily, because of ast-transpiler issue across langs + if a is None: + return b + elif b is None: + return a + else: + result = [] + for i in range(0, len(a)): + result.append(a[i]) + for j in range(0, len(b)): + result.append(b[j]) + return result + + +def assert_non_emtpy_array(exchange, skipped_properties, method, entry, hint=None): + log_text = log_template(exchange, method, entry) + if hint is not None: + log_text = log_text + ' ' + hint + assert isinstance(entry, list), 'response is expected to be an array' + log_text + if not ('emptyResponse' in skipped_properties): + return + assert len(entry) > 0, 'response is expected to be a non-empty array' + log_text + ' (add "emptyResponse" in skip-tests.json to skip this check)' + + +def assert_round_minute_timestamp(exchange, skipped_properties, method, entry, key): + if key in skipped_properties: + return + log_text = log_template(exchange, method, entry) + ts = exchange.safe_string(entry, key) + assert Precise.string_mod(ts, '60000') == '0', 'timestamp should be a multiple of 60 seconds (1 minute)' + log_text + + +def deep_equal(a, b): + return json.dumps(a) == json.dumps(b) + + +def assert_deep_equal(exchange, skipped_properties, method, a, b): + log_text = log_template(exchange, method, {}) + assert deep_equal(a, b), 'two dicts do not match: ' + json.dumps(a) + ' != ' + json.dumps(b) + log_text diff --git a/ccxt/test/exchange/base/test_status.py b/ccxt/test/exchange/base/test_status.py new file mode 100644 index 0000000..2b99333 --- /dev/null +++ b/ccxt/test/exchange/base/test_status.py @@ -0,0 +1,18 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + + + +def test_status(exchange, skipped_properties, method, entry, now): + assert True, 'testStatus' diff --git a/ccxt/test/exchange/base/test_ticker.py b/ccxt/test/exchange/base/test_ticker.py new file mode 100644 index 0000000..bc3d4a5 --- /dev/null +++ b/ccxt/test/exchange/base/test_ticker.py @@ -0,0 +1,177 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.base.precise import Precise # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +def test_ticker(exchange, skipped_properties, method, entry, symbol): + format = { + 'info': {}, + 'symbol': 'ETH/BTC', + 'timestamp': 1502962946216, + 'datetime': '2017-09-01T00:00:00', + 'high': exchange.parse_number('1.234'), + 'low': exchange.parse_number('1.234'), + 'bid': exchange.parse_number('1.234'), + 'bidVolume': exchange.parse_number('1.234'), + 'ask': exchange.parse_number('1.234'), + 'askVolume': exchange.parse_number('1.234'), + 'vwap': exchange.parse_number('1.234'), + 'open': exchange.parse_number('1.234'), + 'close': exchange.parse_number('1.234'), + 'last': exchange.parse_number('1.234'), + 'previousClose': exchange.parse_number('1.234'), + 'change': exchange.parse_number('1.234'), + 'percentage': exchange.parse_number('1.234'), + 'average': exchange.parse_number('1.234'), + 'baseVolume': exchange.parse_number('1.234'), + 'quoteVolume': exchange.parse_number('1.234'), + } + # todo: atm, many exchanges fail, so temporarily decrease stict mode + empty_allowed_for = ['timestamp', 'datetime', 'open', 'high', 'low', 'close', 'last', 'baseVolume', 'quoteVolume', 'previousClose', 'bidVolume', 'askVolume', 'vwap', 'change', 'percentage', 'average'] + # trick csharp-transpiler for string + if not ('BidsAsks' in str(method)): + empty_allowed_for.append('bid') + empty_allowed_for.append('ask') + test_shared_methods.assert_structure(exchange, skipped_properties, method, entry, format, empty_allowed_for) + test_shared_methods.assert_timestamp_and_datetime(exchange, skipped_properties, method, entry) + log_text = test_shared_methods.log_template(exchange, method, entry) + # check market + market = None + symbol_for_market = symbol if (symbol is not None) else exchange.safe_string(entry, 'symbol') + if symbol_for_market is not None and (symbol_for_market in exchange.markets): + market = exchange.market(symbol_for_market) + # only check "above zero" values if exchange is not supposed to have exotic index markets + is_standard_market = (market is not None and exchange.in_array(market['type'], ['spot', 'swap', 'future', 'option'])) + values_should_be_positive = is_standard_market # || (market === undefined) atm, no check for index markets + if values_should_be_positive and not ('positiveValues' in skipped_properties): + test_shared_methods.assert_greater(exchange, skipped_properties, method, entry, 'open', '0') + test_shared_methods.assert_greater(exchange, skipped_properties, method, entry, 'high', '0') + test_shared_methods.assert_greater(exchange, skipped_properties, method, entry, 'low', '0') + test_shared_methods.assert_greater(exchange, skipped_properties, method, entry, 'close', '0') + test_shared_methods.assert_greater(exchange, skipped_properties, method, entry, 'ask', '0') + test_shared_methods.assert_greater(exchange, skipped_properties, method, entry, 'bid', '0') + test_shared_methods.assert_greater(exchange, skipped_properties, method, entry, 'average', '0') + test_shared_methods.assert_greater_or_equal(exchange, skipped_properties, method, entry, 'vwap', '0') + # volume can not be negative + test_shared_methods.assert_greater_or_equal(exchange, skipped_properties, method, entry, 'askVolume', '0') + test_shared_methods.assert_greater_or_equal(exchange, skipped_properties, method, entry, 'bidVolume', '0') + test_shared_methods.assert_greater_or_equal(exchange, skipped_properties, method, entry, 'baseVolume', '0') + test_shared_methods.assert_greater_or_equal(exchange, skipped_properties, method, entry, 'quoteVolume', '0') + # + # close price + # + last_string = exchange.safe_string(entry, 'last') + close_string = exchange.safe_string(entry, 'close') + assert ((close_string is None) and (last_string is None)) or Precise.string_eq(last_string, close_string), '`last` != `close`' + log_text + open_price = exchange.safe_string(entry, 'open') + # + # base & quote volumes + # + base_volume = exchange.omit_zero(exchange.safe_string(entry, 'baseVolume')) + quote_volume = exchange.omit_zero(exchange.safe_string(entry, 'quoteVolume')) + high = exchange.omit_zero(exchange.safe_string(entry, 'high')) + low = exchange.omit_zero(exchange.safe_string(entry, 'low')) + open = exchange.omit_zero(exchange.safe_string(entry, 'open')) + close = exchange.omit_zero(exchange.safe_string(entry, 'close')) + if not ('compareQuoteVolumeBaseVolume' in skipped_properties): + # assert (baseVolumeDefined === quoteVolumeDefined, 'baseVolume or quoteVolume should be either both defined or both undefined' + logText); # No, exchanges might not report both values + if (base_volume is not None) and (quote_volume is not None) and (high is not None) and (low is not None): + base_low = Precise.string_mul(base_volume, low) + base_high = Precise.string_mul(base_volume, high) + # to avoid abnormal long precision issues (like https://discord.com/channels/690203284119617602/1338828283902689280/1338846071278927912 ) + m_precision = exchange.safe_dict(market, 'precision') + amount_precision = exchange.safe_string(m_precision, 'amount') + tolerance = '1.0001' + if amount_precision is not None: + base_low = Precise.string_mul(Precise.string_sub(base_volume, amount_precision), low) + base_high = Precise.string_mul(Precise.string_add(base_volume, amount_precision), high) + else: + # if nothing found, as an exclusion, just add 0.001% + base_low = Precise.string_mul(Precise.string_div(base_volume, tolerance), low) + base_high = Precise.string_mul(Precise.string_mul(base_volume, tolerance), high) + # because of exchange engines might not rounding numbers propertly, we add some tolerance of calculated 24hr high/low + base_low = Precise.string_div(base_low, tolerance) + base_high = Precise.string_mul(base_high, tolerance) + assert Precise.string_ge(quote_volume, base_low), 'quoteVolume should be => baseVolume * low' + log_text + assert Precise.string_le(quote_volume, base_high), 'quoteVolume should be <= baseVolume * high' + log_text + # open and close should be between High & Low + if high is not None and low is not None and not ('compareOHLC' in skipped_properties): + if open is not None: + assert Precise.string_ge(open, low), 'open should be >= low' + log_text + assert Precise.string_le(open, high), 'open should be <= high' + log_text + if close is not None: + assert Precise.string_ge(close, low), 'close should be >= low' + log_text + assert Precise.string_le(close, high), 'close should be <= high' + log_text + # + # vwap + # + vwap = exchange.safe_string(entry, 'vwap') + if vwap is not None: + # todo + # assert (high !== undefined, 'vwap is defined, but high is not' + logText); + # assert (low !== undefined, 'vwap is defined, but low is not' + logText); + # assert (vwap >= low && vwap <= high) + # todo: calc compare + assert not values_should_be_positive or Precise.string_ge(vwap, '0'), 'vwap is not greater than zero' + log_text + if base_volume is not None: + assert quote_volume is not None, 'baseVolume & vwap is defined, but quoteVolume is not' + log_text + if quote_volume is not None: + assert base_volume is not None, 'quoteVolume & vwap is defined, but baseVolume is not' + log_text + ask_string = exchange.safe_string(entry, 'ask') + bid_string = exchange.safe_string(entry, 'bid') + if (ask_string is not None) and (bid_string is not None) and not ('spread' in skipped_properties): + test_shared_methods.assert_greater(exchange, skipped_properties, method, entry, 'ask', exchange.safe_string(entry, 'bid')) + percentage = exchange.safe_string(entry, 'percentage') + change = exchange.safe_string(entry, 'change') + if not ('maxIncrease' in skipped_properties): + # + # percentage + # + max_increase = '100' # for testing purposes, if "increased" value is more than 100x, tests should break as implementation might be wrong. however, if something rarest event happens and some coin really had that huge increase, the tests will shortly recover in few hours, as new 24-hour cycle would stabilize tests) + if percentage is not None: + # - should be above -100 and below MAX + assert Precise.string_ge(percentage, '-100'), 'percentage should be above -100% ' + log_text + assert Precise.string_le(percentage, Precise.string_mul('+100', max_increase)), 'percentage should be below ' + max_increase + '00% ' + log_text + # + # change + # + approx_value = exchange.safe_string_n(entry, ['open', 'close', 'average', 'bid', 'ask', 'vwap', 'previousClose']) + if change is not None: + # - should be between -price & +price*100 + assert Precise.string_ge(change, Precise.string_neg(approx_value)), 'change should be above -price ' + log_text + assert Precise.string_le(change, Precise.string_mul(approx_value, max_increase)), 'change should be below ' + max_increase + 'x price ' + log_text + # + # ensure all expected values are defined + # + if last_string is not None: + if percentage is not None: + # if one knows 'last' and 'percentage' values, then 'change', 'open' and 'average' values should be determinable. + assert open_price is not None and change is not None, 'open & change should be defined if last & percentage are defined' + log_text # todo : add average price too + elif change is not None: + # if one knows 'last' and 'change' values, then 'percentage', 'open' and 'average' values should be determinable. + assert open_price is not None and percentage is not None, 'open & percentage should be defined if last & change are defined' + log_text # todo : add average price too + elif open_price is not None: + if percentage is not None: + # if one knows 'open' and 'percentage' values, then 'last', 'change' and 'average' values should be determinable. + assert last_string is not None and change is not None, 'last & change should be defined if open & percentage are defined' + log_text # todo : add average price too + elif change is not None: + # if one knows 'open' and 'change' values, then 'last', 'percentage' and 'average' values should be determinable. + assert last_string is not None and percentage is not None, 'last & percentage should be defined if open & change are defined' + log_text # todo : add average price too + # + # todo: rethink about this + # else { + # assert ((askString === undefined) && (bidString === undefined), 'ask & bid should be both defined or both undefined' + logText); + # } + test_shared_methods.assert_symbol(exchange, skipped_properties, method, entry, 'symbol', symbol) diff --git a/ccxt/test/exchange/base/test_trade.py b/ccxt/test/exchange/base/test_trade.py new file mode 100644 index 0000000..e1c9d8b --- /dev/null +++ b/ccxt/test/exchange/base/test_trade.py @@ -0,0 +1,47 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +def test_trade(exchange, skipped_properties, method, entry, symbol, now): + format = { + 'info': {}, + 'id': '12345-67890:09876/54321', + 'timestamp': 1502962946216, + 'datetime': '2017-08-17 12:42:48.000', + 'symbol': 'ETH/BTC', + 'order': '12345-67890:09876/54321', + 'side': 'buy', + 'takerOrMaker': 'taker', + 'price': exchange.parse_number('0.06917684'), + 'amount': exchange.parse_number('1.5'), + 'cost': exchange.parse_number('0.10376526'), + 'fees': [], + 'fee': {}, + } + # todo: add takeOrMaker as mandatory (atm, many exchanges fail) + # removed side because some public endpoints return trades without side + empty_allowed_for = ['fees', 'fee', 'symbol', 'order', 'id', 'takerOrMaker'] + test_shared_methods.assert_structure(exchange, skipped_properties, method, entry, format, empty_allowed_for) + test_shared_methods.assert_timestamp_and_datetime(exchange, skipped_properties, method, entry, now) + test_shared_methods.assert_symbol(exchange, skipped_properties, method, entry, 'symbol', symbol) + # + test_shared_methods.assert_in_array(exchange, skipped_properties, method, entry, 'side', ['buy', 'sell']) + test_shared_methods.assert_in_array(exchange, skipped_properties, method, entry, 'takerOrMaker', ['taker', 'maker']) + test_shared_methods.assert_fee_structure(exchange, skipped_properties, method, entry, 'fee') + if not ('fees' in skipped_properties): + # todo: remove undefined check and probably non-empty array check later + if entry['fees'] is not None: + for i in range(0, len(entry['fees'])): + test_shared_methods.assert_fee_structure(exchange, skipped_properties, method, entry['fees'], i) diff --git a/ccxt/test/exchange/base/test_trading_fee.py b/ccxt/test/exchange/base/test_trading_fee.py new file mode 100644 index 0000000..9b0c255 --- /dev/null +++ b/ccxt/test/exchange/base/test_trading_fee.py @@ -0,0 +1,26 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +def test_trading_fee(exchange, skipped_properties, method, symbol, entry): + format = { + 'info': {}, + 'symbol': 'ETH/BTC', + 'maker': exchange.parse_number('0.002'), + 'taker': exchange.parse_number('0.003'), + } + empty_allowed_for = ['tierBased', 'percentage', 'symbol'] + test_shared_methods.assert_structure(exchange, skipped_properties, method, entry, format, empty_allowed_for) + test_shared_methods.assert_symbol(exchange, skipped_properties, method, entry, 'symbol', symbol) diff --git a/ccxt/test/exchange/sync/test_create_order.py b/ccxt/test/exchange/sync/test_create_order.py new file mode 100644 index 0000000..ffd17b9 --- /dev/null +++ b/ccxt/test/exchange/sync/test_create_order.py @@ -0,0 +1,253 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.base.decimal_to_precision import TICK_SIZE # noqa E402 +from ccxt.base.decimal_to_precision import TRUNCATE # noqa E402 +from ccxt.base.decimal_to_precision import ROUND # noqa E402 +from ccxt.base.decimal_to_precision import ROUND_UP # noqa E402 +from ccxt.base.decimal_to_precision import decimal_to_precision # noqa E402 +from ccxt.base.decimal_to_precision import number_to_string # noqa E402 +from ccxt.base.precise import Precise # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 +from ccxt.test.exchange.base import test_order # noqa E402 + +# ---------------------------------------------------------------------------- +def tco_debug(exchange, symbol, message): + # just for debugging purposes + debug_create_order = True + if debug_create_order: + # for c# fix, extra step to convert them to string + print(' >>>>> testCreateOrder [', str((exchange['id'])), ' : ', symbol, '] ', message) + return True + + +# ---------------------------------------------------------------------------- +def test_create_order(exchange, skipped_properties, symbol): + log_prefix = test_shared_methods.log_template(exchange, 'createOrder', [symbol]) + assert exchange.has['cancelOrder'] or exchange.has['cancelOrders'] or exchange.has['cancelAllOrders'], log_prefix + ' does not have cancelOrder|cancelOrders|canelAllOrders method, which is needed to make tests for `createOrder` method. Skipping the test...' + # pre-define some coefficients, which will be used down below + limit_price_safety_multiplier_from_median = 1.045 # todo: when this https://github.com/ccxt/ccxt/issues/22442 is implemented, we'll remove hardcoded value. atm 5% is enough + market = exchange.market(symbol) + is_swap_future = market['swap'] or market['future'] + assert exchange.has['fetchBalance'], log_prefix + ' does not have fetchBalance() method, which is needed to make tests for `createOrder` method. Skipping the test...' + balance = exchange.fetch_balance() + initial_base_balance = balance[market['base']]['free'] + initial_quote_balance = balance[market['quote']]['free'] + assert initial_quote_balance is not None, log_prefix + ' - testing account not have balance of' + market['quote'] + ' in fetchBalance() which is required to test' + tco_debug(exchange, symbol, 'fetched balance for ' + symbol + ' : ' + str(initial_base_balance) + ' ' + market['base'] + '/' + initial_quote_balance + ' ' + market['quote']) + [best_bid, best_ask] = test_shared_methods.fetch_best_bid_ask(exchange, 'createOrder', symbol) + # **************** [Scenario 1 - START] **************** # + tco_debug(exchange, symbol, '### SCENARIO 1 ###') + # create a "limit order" which IS GUARANTEED not to have a fill (i.e. being far from the real price) + tco_create_unfillable_order(exchange, market, log_prefix, skipped_properties, best_bid, best_ask, limit_price_safety_multiplier_from_median, 'buy', None) + if is_swap_future: + # for swap markets, we test sell orders too + tco_create_unfillable_order(exchange, market, log_prefix, skipped_properties, best_bid, best_ask, limit_price_safety_multiplier_from_median, 'sell', None) + tco_debug(exchange, symbol, '### SCENARIO 1 PASSED ###') + # **************** [Scenario 2 - START] **************** # + tco_debug(exchange, symbol, '### SCENARIO 2 ###') + # create an order which IS GUARANTEED to have a fill (full or partial) + tco_create_fillable_order(exchange, market, log_prefix, skipped_properties, best_bid, best_ask, limit_price_safety_multiplier_from_median, 'buy', None) + if is_swap_future: + # for swap markets, we test sell orders too + tco_create_fillable_order(exchange, market, log_prefix, skipped_properties, best_bid, best_ask, limit_price_safety_multiplier_from_median, 'sell', None) + tco_debug(exchange, symbol, '### SCENARIO 2 PASSED ###') + # **************** [Scenario 3 - START] **************** # + return True + + +# ---------------------------------------------------------------------------- +def tco_create_unfillable_order(exchange, market, log_prefix, skipped_properties, best_bid, best_ask, limit_price_safety_multiplier_from_median, buy_or_sell, predefined_amount=None): + try: + symbol = market['symbol'] + minimun_prices = exchange.safe_dict(market['limits'], 'price', {}) + minimum_price = minimun_prices['min'] + maximum_price = minimun_prices['max'] + # below we set limit price, where the order will not be completed. + # We do not use the extreme "limits" values for that market, because, even though min purchase amount for BTC/USDT can be 0.01 BTC, it means with 10$ you can buy 1000 BTC, which leads to unrealistic outcome. So, we just use around 5%-10% far price from the current price. + limit_buy_price_non_fillable = best_bid / limit_price_safety_multiplier_from_median + if minimum_price is not None and limit_buy_price_non_fillable < minimum_price: + limit_buy_price_non_fillable = minimum_price + limit_sell_price_non_fillable = best_ask * limit_price_safety_multiplier_from_median + if maximum_price is not None and limit_sell_price_non_fillable > maximum_price: + limit_sell_price_non_fillable = maximum_price + created_order = None + if buy_or_sell == 'buy': + order_amount = tco_get_minimum_amount_for_limit_price(exchange, market, limit_buy_price_non_fillable, predefined_amount) + created_order = tco_create_order_safe(exchange, symbol, 'limit', 'buy', order_amount, limit_buy_price_non_fillable, {}, skipped_properties) + else: + order_amount = tco_get_minimum_amount_for_limit_price(exchange, market, limit_sell_price_non_fillable, predefined_amount) + created_order = tco_create_order_safe(exchange, symbol, 'limit', 'sell', order_amount, limit_sell_price_non_fillable, {}, skipped_properties) + fetched_order = test_shared_methods.fetch_order(exchange, symbol, created_order['id'], skipped_properties) + # test fetched order object + if fetched_order is not None: + test_order(exchange, skipped_properties, 'createOrder', fetched_order, symbol, exchange.milliseconds()) + # ensure that order is not filled + test_shared_methods.assert_order_state(exchange, skipped_properties, 'createdOrder', created_order, 'open', False) + test_shared_methods.assert_order_state(exchange, skipped_properties, 'fetchedOrder', fetched_order, 'open', True) + # ensure that order side matches + test_shared_methods.assert_in_array(exchange, skipped_properties, 'createdOrder', created_order, 'side', [None, buy_or_sell]) + test_shared_methods.assert_in_array(exchange, skipped_properties, 'fetchedOrder', fetched_order, 'side', [None, buy_or_sell]) + tco_cancel_order(exchange, symbol, created_order['id']) + except Exception as e: + raise Error(log_prefix + ' failed for Scenario 1: ' + str(e)) + return True + + +def tco_create_fillable_order(exchange, market, log_prefix, skipped_properties, best_bid, best_ask, limit_price_safety_multiplier_from_median, buy_or_sell_string, predefined_amount=None): + try: + is_swap_future = market['swap'] or market['future'] + is_buy = (buy_or_sell_string == 'buy') + entry_side = 'buy' if is_buy else 'sell' + exit_side = 'sell' if is_buy else 'buy' + entryorder_price = best_ask * limit_price_safety_multiplier_from_median if is_buy else best_bid / limit_price_safety_multiplier_from_median + exitorder_price = best_bid / limit_price_safety_multiplier_from_median if is_buy else best_ask * limit_price_safety_multiplier_from_median # todo revise: (tcoMininumCost (exchange, market) / amountToClose) / limitPriceSafetyMultiplierFromMedian; + # + # + symbol = market['symbol'] + entry_amount = tco_get_minimum_amount_for_limit_price(exchange, market, entryorder_price) + entryorder_filled = tco_create_order_safe(exchange, symbol, 'limit', entry_side, entry_amount, entryorder_price, {}, skipped_properties) + # just for case, cancel any possible unfilled amount (though it is not be expected because the order was fillable) + tco_try_cancel_order(exchange, symbol, entryorder_filled, skipped_properties) + # now, as order is closed/canceled, we can reliably fetch the order information + entryorder_fetched = test_shared_methods.fetch_order(exchange, symbol, entryorder_filled['id'], skipped_properties) + tco_assert_filled_order(exchange, market, log_prefix, skipped_properties, entryorder_filled, entryorder_fetched, entry_side, entry_amount) + # + # ### close the traded position ### + # + amount_to_close = exchange.parse_to_numeric(exchange.safe_string(entryorder_fetched, 'filled')) + params = {} + # as we want to close position, we should use 'reduceOnly' to ensure we don't open a margined position accidentally, because some exchanges might have automatically enabled margin-mode (on spot) or hedge-mode (on contracts) + if is_swap_future: + params['reduceOnly'] = True + exitorder_filled = tco_create_order_safe(exchange, symbol, 'market', exit_side, amount_to_close, (None if market['spot'] else exitorder_price), params, skipped_properties) + exitorder_fetched = test_shared_methods.fetch_order(exchange, symbol, exitorder_filled['id'], skipped_properties) + tco_assert_filled_order(exchange, market, log_prefix, skipped_properties, exitorder_filled, exitorder_fetched, exit_side, amount_to_close) + except Exception as e: + raise Error('failed for Scenario 2: ' + str(e)) + return True + + +def tco_assert_filled_order(exchange, market, log_prefix, skipped_properties, created_order, fetched_order, requested_side, requested_amount): + # test filled amount + precision_amount = exchange.safe_string(market['precision'], 'amount') + entryorder_amount_string = exchange.number_to_string(requested_amount) + filled_string = exchange.safe_string(fetched_order, 'filled') + assert filled_string is not None, log_prefix + ' order should be filled, but it is not. ' + exchange.json(fetched_order) + # filled amount should be whithin the expected range i.e. if you buy 100 DOGECOIN and amount-precision is 1, + # and also considering possible roundings in implementation, then filled amount should be between 99 and 101 + max_expected_filled_amount = Precise.string_add(entryorder_amount_string, precision_amount) + min_expected_filled_amount = Precise.string_sub(entryorder_amount_string, precision_amount) + assert Precise.string_le(filled_string, max_expected_filled_amount), log_prefix + ' filled amount is more than expected, possibly some implementation issue. ' + exchange.json(fetched_order) + assert Precise.string_ge(filled_string, min_expected_filled_amount), log_prefix + ' filled amount is less than expected, possibly some implementation issue. ' + exchange.json(fetched_order) + # order state should be "closed" + test_shared_methods.assert_order_state(exchange, skipped_properties, 'createdOrder', created_order, 'closed', False) + test_shared_methods.assert_order_state(exchange, skipped_properties, 'fetchedOrder', fetched_order, 'closed', True) + # ensure that order side matches + test_shared_methods.assert_in_array(exchange, skipped_properties, 'createdOrder', created_order, 'side', [None, requested_side]) + test_shared_methods.assert_in_array(exchange, skipped_properties, 'fetchedOrder', fetched_order, 'side', [None, requested_side]) + return True + + +# ---------------------------------------------------------------------------- +def tco_cancel_order(exchange, symbol, order_id=None): + log_prefix = test_shared_methods.log_template(exchange, 'createOrder', [symbol]) + used_method = '' + cancel_result = None + if exchange.has['cancelOrder'] and order_id is not None: + used_method = 'cancelOrder' + cancel_result = exchange.cancel_order(order_id, symbol) + elif exchange.has['cancelAllOrders']: + used_method = 'cancelAllOrders' + cancel_result = exchange.cancel_all_orders(symbol) + elif exchange.has['cancelOrders']: + raise Error(log_prefix + ' cancelOrders method is not unified yet, coming soon...') + tco_debug(exchange, symbol, 'canceled order using ' + used_method + ':' + cancel_result['id']) + # todo: + # testSharedMethods.assertOrderState (exchange, skippedProperties, 'cancelOrder', cancelResult, 'canceled', false); + # testSharedMethods.assertOrderState (exchange, skippedProperties, 'cancelOrder', cancelResult, 'closed', true); + return True + + +# ---------------------------------------------------------------------------- +# ---------------------------------------------------------------------------- +def tco_create_order_safe(exchange, symbol, order_type, side, amount, price=None, params={}, skipped_properties={}): + tco_debug(exchange, symbol, 'Executing createOrder ' + order_type + ' ' + side + ' ' + amount + ' ' + price + ' ' + exchange.json(params)) + order = exchange.create_order(symbol, order_type, side, amount, price, params) + try: + test_order(exchange, skipped_properties, 'createOrder', order, symbol, int(time.time() * 1000)) + except Exception as e: + if order_type != 'market': + # if it was limit order, try to cancel it before exiting the script + tco_try_cancel_order(exchange, symbol, order, skipped_properties) + raise e + return order + + +def tco_mininum_amount(exchange, market): + amount_values = exchange.safe_dict(market['limits'], 'amount', {}) + amount_min = exchange.safe_number(amount_values, 'min') + assert amount_min is not None, exchange.id + ' ' + market['symbol'] + ' can not determine minimum amount for order' + return amount_min + + +def tco_mininum_cost(exchange, market): + cost_values = exchange.safe_dict(market['limits'], 'cost', {}) + cost_min = exchange.safe_number(cost_values, 'min') + assert cost_min is not None, exchange.id + ' ' + market['symbol'] + ' can not determine minimum cost for order' + return cost_min + + +def tco_get_minimum_amount_for_limit_price(exchange, market, price, predefined_amount=None): + # this method calculates the minimum realistic order amount: + # at first it checks the "minimum hardcap limit" (i.e. 7 DOGE), however, if exchange also has "minimum cost" limits, + # then we need to calculate the amount using cost, because of price is volatile, today's 7 DOGE cost could be 1$ + # but "minimum cost" requirement could be 5$ (which translates to 35 DOGE amount) + minimum_amount = tco_mininum_amount(exchange, market) + minimum_cost = tco_mininum_cost(exchange, market) + final_amount = minimum_amount + if minimum_cost is not None: + if final_amount * price < minimum_cost: + final_amount = minimum_cost / price + if predefined_amount is not None: + final_amount = max(final_amount, predefined_amount) + # because it's possible that calculated value might get truncated down in "createOrder" (i.e. 0.129 -> 0.12), we should ensure that final amount * price would bypass minimum cost requirements, by adding the "minimum precision" + amount_precision = exchange.safe_number(market['precision'], 'amount') + is_tick_size_precision = exchange.precisionMode == 4 + if amount_precision is None: + amount_precision = 1e-15 # todo: revise this for better way in future + else: + # todo: remove after TICK_SIZE unification + if not is_tick_size_precision: + amount_precision = 1 / math.pow(10, amount_precision) # this converts DECIMAL_PRECISION into TICK_SIZE + final_amount = final_amount + amount_precision + final_amount = final_amount * 1.1 # add around 10% to ensure "cost" is enough + final_amount = float(exchange.decimal_to_precision(final_amount, 2, market['precision']['amount'], exchange.precisionMode)) # 2 stands for ROUND_UP constant, 0 stands for TRUNCATE + return final_amount + + +def tco_try_cancel_order(exchange, symbol, order, skipped_properties): + order_fetched = test_shared_methods.fetch_order(exchange, symbol, order['id'], skipped_properties) + needs_cancel = exchange.in_array(order_fetched['status'], ['open', 'pending', None]) + # if it was not reported as closed/filled, then try to cancel it + if needs_cancel: + tco_debug(exchange, symbol, 'trying to cancel the remaining amount of partially filled order...') + try: + tco_cancel_order(exchange, symbol, order['id']) + except Exception as e: + # order might have been closed/filled already, before 'cancelOrder' call reaches server, so it is tolerable, we don't throw exception + tco_debug(exchange, symbol, ' a moment ago order was reported as pending, but could not be cancelled at this moment. Exception message: ' + str(e)) + else: + tco_debug(exchange, symbol, 'order is already closed/filled, no need to cancel it') + return True diff --git a/ccxt/test/exchange/sync/test_features.py b/ccxt/test/exchange/sync/test_features.py new file mode 100644 index 0000000..1435365 --- /dev/null +++ b/ccxt/test/exchange/sync/test_features.py @@ -0,0 +1,124 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +def test_features(exchange, skipped_properties): + market_types = ['spot', 'swap', 'future', 'option'] + sub_types = ['linear', 'inverse'] + features = exchange.features + keys = list(features.keys()) + for i in range(0, len(keys)): + test_shared_methods.assert_in_array(exchange, skipped_properties, 'features', keys, i, market_types) + market_type = keys[i] + value = features[market_type] + # assert (value !== undefined, 'exchange.features["' + marketType + '"] is undefined, that key should be either absent or have a value'); + if value is None: + continue + if market_type == 'spot': + test_features_inner(exchange, skipped_properties, value) + else: + sub_keys = list(value.keys()) + for j in range(0, len(sub_keys)): + sub_key = sub_keys[j] + test_shared_methods.assert_in_array(exchange, skipped_properties, 'features', sub_keys, j, sub_types) + sub_value = value[sub_key] + # sometimes it might not be available for exchange, eg. future>inverse) + if sub_value is not None: + test_features_inner(exchange, skipped_properties, sub_value) + return True + + +def test_features_inner(exchange, skipped_properties, feature_obj): + format = { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': False, + 'triggerPriceType': { + 'mark': False, + 'last': False, + 'index': False, + }, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': { + 'triggerPriceType': { + 'last': False, + 'mark': False, + 'index': False, + }, + 'price': False, + }, + 'timeInForce': { + 'GTC': False, + 'IOC': False, + 'FOK': False, + 'PO': False, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + }, + 'createOrders': { + 'max': 5, + }, + 'fetchMyTrades': { + 'marginMode': False, + 'daysBack': 0, + 'limit': 0, + 'untilDays': 0, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 0, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': 0, + 'daysBack': 0, + 'untilDays': 0, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 0, + 'daysBack': 0, + 'daysBackCanceled': 0, + 'untilDays': 0, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 0, + }, + } + feature_keys = list(feature_obj.keys()) + all_methods = list(exchange.has.keys()) + for i in range(0, len(feature_keys)): + test_shared_methods.assert_in_array(exchange, skipped_properties, 'features', feature_keys, i, all_methods) + test_shared_methods.assert_structure(exchange, skipped_properties, 'features', feature_obj, format, None, True) # deep structure check diff --git a/ccxt/test/exchange/sync/test_fetch_accounts.py b/ccxt/test/exchange/sync/test_fetch_accounts.py new file mode 100644 index 0000000..9529af8 --- /dev/null +++ b/ccxt/test/exchange/sync/test_fetch_accounts.py @@ -0,0 +1,24 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_account # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +def test_fetch_accounts(exchange, skipped_properties): + method = 'fetchAccounts' + accounts = exchange.fetch_accounts() + test_shared_methods.assert_non_emtpy_array(exchange, skipped_properties, method, accounts) + for i in range(0, len(accounts)): + test_account(exchange, skipped_properties, method, accounts[i]) + return True diff --git a/ccxt/test/exchange/sync/test_fetch_balance.py b/ccxt/test/exchange/sync/test_fetch_balance.py new file mode 100644 index 0000000..41c3277 --- /dev/null +++ b/ccxt/test/exchange/sync/test_fetch_balance.py @@ -0,0 +1,21 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_balance # noqa E402 + +def test_fetch_balance(exchange, skipped_properties): + method = 'fetchBalance' + response = exchange.fetch_balance() + test_balance(exchange, skipped_properties, method, response) + return True diff --git a/ccxt/test/exchange/sync/test_fetch_borrow_interest.py b/ccxt/test/exchange/sync/test_fetch_borrow_interest.py new file mode 100644 index 0000000..580a457 --- /dev/null +++ b/ccxt/test/exchange/sync/test_fetch_borrow_interest.py @@ -0,0 +1,24 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_borrow_interest # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +def test_fetch_borrow_interest(exchange, skipped_properties, code, symbol): + method = 'fetchBorrowInterest' + borrow_interest = exchange.fetch_borrow_interest(code, symbol) + test_shared_methods.assert_non_emtpy_array(exchange, skipped_properties, method, borrow_interest, code) + for i in range(0, len(borrow_interest)): + test_borrow_interest(exchange, skipped_properties, method, borrow_interest[i], code, symbol) + return True diff --git a/ccxt/test/exchange/sync/test_fetch_closed_orders.py b/ccxt/test/exchange/sync/test_fetch_closed_orders.py new file mode 100644 index 0000000..6f90b59 --- /dev/null +++ b/ccxt/test/exchange/sync/test_fetch_closed_orders.py @@ -0,0 +1,28 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_order # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +def test_fetch_closed_orders(exchange, skipped_properties, symbol): + method = 'fetchClosedOrders' + orders = exchange.fetch_closed_orders(symbol) + test_shared_methods.assert_non_emtpy_array(exchange, skipped_properties, method, orders, symbol) + now = exchange.milliseconds() + for i in range(0, len(orders)): + order = orders[i] + test_order(exchange, skipped_properties, method, order, symbol, now) + test_shared_methods.assert_in_array(exchange, skipped_properties, method, order, 'status', ['closed', 'canceled']) + test_shared_methods.assert_timestamp_order(exchange, method, symbol, orders) + return True diff --git a/ccxt/test/exchange/sync/test_fetch_currencies.py b/ccxt/test/exchange/sync/test_fetch_currencies.py new file mode 100644 index 0000000..2c0d31a --- /dev/null +++ b/ccxt/test/exchange/sync/test_fetch_currencies.py @@ -0,0 +1,73 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_currency # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +def test_fetch_currencies(exchange, skipped_properties): + method = 'fetchCurrencies' + currencies = exchange.fetch_currencies() + # todo: try to invent something to avoid undefined undefined, i.e. maybe move into private and force it to have a value + num_inactive_currencies = 0 + max_inactive_currencies_percentage = exchange.safe_integer(skipped_properties, 'maxInactiveCurrenciesPercentage', 50) # no more than X% currencies should be inactive + required_active_currencies = ['BTC', 'ETH', 'USDT', 'USDC'] + features = exchange.features + features_spot = exchange.safe_dict(features, 'spot', {}) + fetch_currencies = exchange.safe_dict(features_spot, 'fetchCurrencies', {}) + is_fetch_currencies_private = exchange.safe_value(fetch_currencies, 'private', False) + if not is_fetch_currencies_private: + values = list(currencies.values()) + test_shared_methods.assert_non_emtpy_array(exchange, skipped_properties, method, values) + currencies_length = len(values) + # ensure exchange returns enough length of currencies + skip_amount = ('amountOfCurrencies' in skipped_properties) + assert skip_amount or currencies_length > 5, exchange.id + ' ' + method + ' must return at least several currencies, but it returned ' + str(currencies_length) + # allow skipped exchanges + skip_active = ('activeCurrenciesQuota' in skipped_properties) + skip_major_currency_check = ('activeMajorCurrencies' in skipped_properties) + # loop + for i in range(0, currencies_length): + currency = values[i] + test_currency(exchange, skipped_properties, method, currency) + # detailed check for deposit/withdraw + active = exchange.safe_bool(currency, 'active') + if active is False: + num_inactive_currencies = num_inactive_currencies + 1 + # ensure that major currencies are active and enabled for deposit and withdrawal + code = exchange.safe_string(currency, 'code', None) + withdraw = exchange.safe_bool(currency, 'withdraw') + deposit = exchange.safe_bool(currency, 'deposit') + if exchange.in_array(code, required_active_currencies): + assert skip_major_currency_check or (withdraw and deposit), 'Major currency ' + code + ' should have withdraw and deposit flags enabled' + # check at least X% of currencies are active + inactive_currencies_percentage = (num_inactive_currencies / currencies_length) * 100 + assert skip_active or (inactive_currencies_percentage < max_inactive_currencies_percentage), 'Percentage of inactive currencies is too high at ' + str(inactive_currencies_percentage) + '% that is more than the allowed maximum of ' + str(max_inactive_currencies_percentage) + '%' + detect_currency_conflicts(exchange, currencies) + return True + + +def detect_currency_conflicts(exchange, currency_values): + # detect if there are currencies with different ids for the same code + ids = {} + keys = list(currency_values.keys()) + for i in range(0, len(keys)): + key = keys[i] + currency = currency_values[key] + code = currency['code'] + if not (code in ids): + ids[code] = currency['id'] + else: + is_different = ids[code] != currency['id'] + assert not is_different, exchange.id + ' fetchCurrencies() has different ids for the same code: ' + code + ' ' + ids[code] + ' ' + currency['id'] + return True diff --git a/ccxt/test/exchange/sync/test_fetch_deposit_withdrawals.py b/ccxt/test/exchange/sync/test_fetch_deposit_withdrawals.py new file mode 100644 index 0000000..ef44c76 --- /dev/null +++ b/ccxt/test/exchange/sync/test_fetch_deposit_withdrawals.py @@ -0,0 +1,26 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_deposit_withdrawal # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +def test_fetch_deposit_withdrawals(exchange, skipped_properties, code): + method = 'fetchTransactions' + transactions = exchange.fetch_transactions(code) + test_shared_methods.assert_non_emtpy_array(exchange, skipped_properties, method, transactions, code) + now = exchange.milliseconds() + for i in range(0, len(transactions)): + test_deposit_withdrawal(exchange, skipped_properties, method, transactions[i], code, now) + test_shared_methods.assert_timestamp_order(exchange, method, code, transactions) + return True diff --git a/ccxt/test/exchange/sync/test_fetch_deposits.py b/ccxt/test/exchange/sync/test_fetch_deposits.py new file mode 100644 index 0000000..98fba34 --- /dev/null +++ b/ccxt/test/exchange/sync/test_fetch_deposits.py @@ -0,0 +1,26 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_deposit_withdrawal # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +def test_fetch_deposits(exchange, skipped_properties, code): + method = 'fetchDeposits' + transactions = exchange.fetch_deposits(code) + test_shared_methods.assert_non_emtpy_array(exchange, skipped_properties, method, transactions, code) + now = exchange.milliseconds() + for i in range(0, len(transactions)): + test_deposit_withdrawal(exchange, skipped_properties, method, transactions[i], code, now) + test_shared_methods.assert_timestamp_order(exchange, method, code, transactions) + return True diff --git a/ccxt/test/exchange/sync/test_fetch_funding_rate_history.py b/ccxt/test/exchange/sync/test_fetch_funding_rate_history.py new file mode 100644 index 0000000..91e2b8d --- /dev/null +++ b/ccxt/test/exchange/sync/test_fetch_funding_rate_history.py @@ -0,0 +1,25 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_funding_rate_history # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +def test_fetch_funding_rate_history(exchange, skipped_properties, symbol): + method = 'fetchFundingRateHistory' + funding_rates_history = exchange.fetch_funding_rate_history(symbol) + test_shared_methods.assert_non_emtpy_array(exchange, skipped_properties, method, funding_rates_history, symbol) + for i in range(0, len(funding_rates_history)): + test_funding_rate_history(exchange, skipped_properties, method, funding_rates_history[i], symbol) + test_shared_methods.assert_timestamp_order(exchange, method, symbol, funding_rates_history) + return True diff --git a/ccxt/test/exchange/sync/test_fetch_l2_order_book.py b/ccxt/test/exchange/sync/test_fetch_l2_order_book.py new file mode 100644 index 0000000..441a3c3 --- /dev/null +++ b/ccxt/test/exchange/sync/test_fetch_l2_order_book.py @@ -0,0 +1,21 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_order_book # noqa E402 + +def test_fetch_l2_order_book(exchange, skipped_properties, symbol): + method = 'fetchL2OrderBook' + order_book = exchange.fetch_l2_order_book(symbol) + test_order_book(exchange, skipped_properties, method, order_book, symbol) + return True diff --git a/ccxt/test/exchange/sync/test_fetch_last_prices.py b/ccxt/test/exchange/sync/test_fetch_last_prices.py new file mode 100644 index 0000000..b624cbf --- /dev/null +++ b/ccxt/test/exchange/sync/test_fetch_last_prices.py @@ -0,0 +1,37 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_last_price # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +def test_fetch_last_prices(exchange, skipped_properties, symbol): + method = 'fetchLastprices' + # log ('fetching all tickers at once...') + response = None + checked_symbol = None + try: + response = exchange.fetch_last_prices() + except Exception as e: + response = exchange.fetch_last_prices([symbol]) + checked_symbol = symbol + assert isinstance(response, dict), exchange.id + ' ' + method + ' ' + checked_symbol + ' must return an object. ' + exchange.json(response) + values = list(response.values()) + test_shared_methods.assert_non_emtpy_array(exchange, skipped_properties, method, values, checked_symbol) + at_least_one_passed = False + for i in range(0, len(values)): + # todo: symbol check here + test_last_price(exchange, skipped_properties, method, values[i], checked_symbol) + at_least_one_passed = at_least_one_passed or (exchange.safe_number(values[i], 'price') > 0) + assert at_least_one_passed, exchange.id + ' ' + method + ' ' + checked_symbol + ' at least one symbol should pass the test' + return True diff --git a/ccxt/test/exchange/sync/test_fetch_ledger.py b/ccxt/test/exchange/sync/test_fetch_ledger.py new file mode 100644 index 0000000..1639094 --- /dev/null +++ b/ccxt/test/exchange/sync/test_fetch_ledger.py @@ -0,0 +1,26 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_ledger_entry # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +def test_fetch_ledger(exchange, skipped_properties, code): + method = 'fetchLedger' + items = exchange.fetch_ledger(code) + test_shared_methods.assert_non_emtpy_array(exchange, skipped_properties, method, items, code) + now = exchange.milliseconds() + for i in range(0, len(items)): + test_ledger_entry(exchange, skipped_properties, method, items[i], code, now) + test_shared_methods.assert_timestamp_order(exchange, method, code, items) + return True diff --git a/ccxt/test/exchange/sync/test_fetch_ledger_entry.py b/ccxt/test/exchange/sync/test_fetch_ledger_entry.py new file mode 100644 index 0000000..2f183b6 --- /dev/null +++ b/ccxt/test/exchange/sync/test_fetch_ledger_entry.py @@ -0,0 +1,29 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_ledger_entry # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +def test_fetch_ledger_entry(exchange, skipped_properties, code): + method = 'fetchLedgerEntry' + items = exchange.fetch_ledger(code) + length = len(items) + test_shared_methods.assert_non_emtpy_array(exchange, skipped_properties, method, items, code) + if length > 0: + first_item = items[0] + id = first_item['id'] + item = exchange.fetch_ledger_entry(id) + now = exchange.milliseconds() + test_ledger_entry(exchange, skipped_properties, method, item, code, now) + return True diff --git a/ccxt/test/exchange/sync/test_fetch_leverage_tiers.py b/ccxt/test/exchange/sync/test_fetch_leverage_tiers.py new file mode 100644 index 0000000..c2d9f03 --- /dev/null +++ b/ccxt/test/exchange/sync/test_fetch_leverage_tiers.py @@ -0,0 +1,34 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_leverage_tier # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +def test_fetch_leverage_tiers(exchange, skipped_properties, symbol): + method = 'fetchLeverageTiers' + tiers = exchange.fetch_leverage_tiers(['symbol']) + # const format = { + # 'RAY/USDT': [ + # {}, + # ], + # }; + assert isinstance(tiers, dict), exchange.id + ' ' + method + ' ' + symbol + ' must return an object. ' + exchange.json(tiers) + tier_keys = list(tiers.keys()) + test_shared_methods.assert_non_emtpy_array(exchange, skipped_properties, method, tier_keys, symbol) + for i in range(0, len(tier_keys)): + tiers_for_symbol = tiers[tier_keys[i]] + test_shared_methods.assert_non_emtpy_array(exchange, skipped_properties, method, tiers_for_symbol, symbol) + for j in range(0, len(tiers_for_symbol)): + test_leverage_tier(exchange, skipped_properties, method, tiers_for_symbol[j]) + return True diff --git a/ccxt/test/exchange/sync/test_fetch_liquidations.py b/ccxt/test/exchange/sync/test_fetch_liquidations.py new file mode 100644 index 0000000..3856c1b --- /dev/null +++ b/ccxt/test/exchange/sync/test_fetch_liquidations.py @@ -0,0 +1,28 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_shared_methods # noqa E402 +from ccxt.test.exchange.base import test_liquidation # noqa E402 + +def test_fetch_liquidations(exchange, skipped_properties, code): + method = 'fetchLiquidations' + if not exchange.has['fetchLiquidations']: + return True + items = exchange.fetch_liquidations(code) + assert isinstance(items, list), exchange.id + ' ' + method + ' ' + code + ' must return an array. ' + exchange.json(items) + # const now = exchange.milliseconds (); + for i in range(0, len(items)): + test_liquidation(exchange, skipped_properties, method, items[i], code) + test_shared_methods.assert_timestamp_order(exchange, method, code, items) + return True diff --git a/ccxt/test/exchange/sync/test_fetch_margin_mode.py b/ccxt/test/exchange/sync/test_fetch_margin_mode.py new file mode 100644 index 0000000..e2e9fde --- /dev/null +++ b/ccxt/test/exchange/sync/test_fetch_margin_mode.py @@ -0,0 +1,21 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_margin_mode # noqa E402 + +def test_fetch_margin_mode(exchange, skipped_properties, symbol): + method = 'fetchMarginMode' + margin_mode = exchange.fetch_margin_mode(symbol) + test_margin_mode(exchange, skipped_properties, method, margin_mode) + return True diff --git a/ccxt/test/exchange/sync/test_fetch_margin_modes.py b/ccxt/test/exchange/sync/test_fetch_margin_modes.py new file mode 100644 index 0000000..03f5a9a --- /dev/null +++ b/ccxt/test/exchange/sync/test_fetch_margin_modes.py @@ -0,0 +1,28 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_margin_mode # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +def test_fetch_margin_modes(exchange, skipped_properties, symbol): + method = 'fetchMarginModes' + margin_modes = exchange.fetch_margin_modes(['symbol']) + assert isinstance(margin_modes, dict), exchange.id + ' ' + method + ' ' + symbol + ' must return an object. ' + exchange.json(margin_modes) + margin_mode_keys = list(margin_modes.keys()) + test_shared_methods.assert_non_emtpy_array(exchange, skipped_properties, method, margin_modes, symbol) + for i in range(0, len(margin_mode_keys)): + margin_mode = margin_modes[margin_mode_keys[i]] + test_shared_methods.assert_non_emtpy_array(exchange, skipped_properties, method, margin_mode, symbol) + test_margin_mode(exchange, skipped_properties, method, margin_mode) + return True diff --git a/ccxt/test/exchange/sync/test_fetch_market_leverage_tiers.py b/ccxt/test/exchange/sync/test_fetch_market_leverage_tiers.py new file mode 100644 index 0000000..2339cdd --- /dev/null +++ b/ccxt/test/exchange/sync/test_fetch_market_leverage_tiers.py @@ -0,0 +1,24 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_leverage_tier # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +def test_fetch_market_leverage_tiers(exchange, skipped_properties, symbol): + method = 'fetchMarketLeverageTiers' + tiers = exchange.fetch_market_leverage_tiers(symbol) + test_shared_methods.assert_non_emtpy_array(exchange, skipped_properties, method, tiers, symbol) + for j in range(0, len(tiers)): + test_leverage_tier(exchange, skipped_properties, method, tiers[j]) + return True diff --git a/ccxt/test/exchange/sync/test_fetch_markets.py b/ccxt/test/exchange/sync/test_fetch_markets.py new file mode 100644 index 0000000..181d5c1 --- /dev/null +++ b/ccxt/test/exchange/sync/test_fetch_markets.py @@ -0,0 +1,41 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_market # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +def test_fetch_markets(exchange, skipped_properties): + method = 'fetchMarkets' + markets = exchange.fetch_markets() + assert isinstance(markets, dict), exchange.id + ' ' + method + ' must return an object. ' + exchange.json(markets) + market_values = list(markets.values()) + test_shared_methods.assert_non_emtpy_array(exchange, skipped_properties, method, market_values) + for i in range(0, len(market_values)): + test_market(exchange, skipped_properties, method, market_values[i]) + detect_market_conflicts(exchange, markets) + return True + + +def detect_market_conflicts(exchange, market_values): + # detect if there are markets with different ids for the same symbol + ids = {} + for i in range(0, len(market_values)): + market = market_values[i] + symbol = market['symbol'] + if not (symbol in ids): + ids[symbol] = market['id'] + else: + is_different = ids[symbol] != market['id'] + assert not is_different, exchange.id + ' fetchMarkets() has different ids for the same symbol: ' + symbol + ' ' + ids[symbol] + ' ' + market['id'] + return True diff --git a/ccxt/test/exchange/sync/test_fetch_my_liquidations.py b/ccxt/test/exchange/sync/test_fetch_my_liquidations.py new file mode 100644 index 0000000..9988f25 --- /dev/null +++ b/ccxt/test/exchange/sync/test_fetch_my_liquidations.py @@ -0,0 +1,28 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_shared_methods # noqa E402 +from ccxt.test.exchange.base import test_liquidation # noqa E402 + +def test_fetch_my_liquidations(exchange, skipped_properties, code): + method = 'fetchMyLiquidations' + if not exchange.has['fetchMyLiquidations']: + return True + items = exchange.fetch_my_liquidations(code) + assert isinstance(items, list), exchange.id + ' ' + method + ' ' + code + ' must return an array. ' + exchange.json(items) + # const now = exchange.milliseconds (); + for i in range(0, len(items)): + test_liquidation(exchange, skipped_properties, method, items[i], code) + test_shared_methods.assert_timestamp_order(exchange, method, code, items) + return True diff --git a/ccxt/test/exchange/sync/test_fetch_my_trades.py b/ccxt/test/exchange/sync/test_fetch_my_trades.py new file mode 100644 index 0000000..a2b55f4 --- /dev/null +++ b/ccxt/test/exchange/sync/test_fetch_my_trades.py @@ -0,0 +1,26 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_shared_methods # noqa E402 +from ccxt.test.exchange.base import test_trade # noqa E402 + +def test_fetch_my_trades(exchange, skipped_properties, symbol): + method = 'fetchMyTrades' + trades = exchange.fetch_my_trades(symbol) + test_shared_methods.assert_non_emtpy_array(exchange, skipped_properties, method, trades, symbol) + now = exchange.milliseconds() + for i in range(0, len(trades)): + test_trade(exchange, skipped_properties, method, trades[i], symbol, now) + test_shared_methods.assert_timestamp_order(exchange, method, symbol, trades) + return True diff --git a/ccxt/test/exchange/sync/test_fetch_ohlcv.py b/ccxt/test/exchange/sync/test_fetch_ohlcv.py new file mode 100644 index 0000000..d933df4 --- /dev/null +++ b/ccxt/test/exchange/sync/test_fetch_ohlcv.py @@ -0,0 +1,35 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_ohlcv # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +def test_fetch_ohlcv(exchange, skipped_properties, symbol): + method = 'fetchOHLCV' + timeframe_keys = list(exchange.timeframes.keys()) + assert len(timeframe_keys), exchange.id + ' ' + method + ' - no timeframes found' + # prefer 1m timeframe if available, otherwise return the first one + chosen_timeframe_key = '1m' + if not exchange.in_array(chosen_timeframe_key, timeframe_keys): + chosen_timeframe_key = timeframe_keys[0] + limit = 10 + duration = exchange.parse_timeframe(chosen_timeframe_key) + since = exchange.milliseconds() - duration * limit * 1000 - 1000 + ohlcvs = exchange.fetch_ohlcv(symbol, chosen_timeframe_key, since, limit) + test_shared_methods.assert_non_emtpy_array(exchange, skipped_properties, method, ohlcvs, symbol) + now = exchange.milliseconds() + for i in range(0, len(ohlcvs)): + test_ohlcv(exchange, skipped_properties, method, ohlcvs[i], symbol, now) + # todo: sorted timestamps check + return True diff --git a/ccxt/test/exchange/sync/test_fetch_open_interest_history.py b/ccxt/test/exchange/sync/test_fetch_open_interest_history.py new file mode 100644 index 0000000..4b75f02 --- /dev/null +++ b/ccxt/test/exchange/sync/test_fetch_open_interest_history.py @@ -0,0 +1,24 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_open_interest # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +def test_fetch_open_interest_history(exchange, skipped_properties, symbol): + method = 'fetchOpenInterestHistory' + open_interest_history = exchange.fetch_open_interest_history(symbol) + test_shared_methods.assert_non_emtpy_array(exchange, skipped_properties, method, open_interest_history, symbol) + for i in range(0, len(open_interest_history)): + test_open_interest(exchange, skipped_properties, method, open_interest_history[i]) + return True diff --git a/ccxt/test/exchange/sync/test_fetch_open_orders.py b/ccxt/test/exchange/sync/test_fetch_open_orders.py new file mode 100644 index 0000000..5052af9 --- /dev/null +++ b/ccxt/test/exchange/sync/test_fetch_open_orders.py @@ -0,0 +1,28 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_order # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +def test_fetch_open_orders(exchange, skipped_properties, symbol): + method = 'fetchOpenOrders' + orders = exchange.fetch_open_orders(symbol) + test_shared_methods.assert_non_emtpy_array(exchange, skipped_properties, method, orders, symbol) + now = exchange.milliseconds() + for i in range(0, len(orders)): + order = orders[i] + test_order(exchange, skipped_properties, method, order, symbol, now) + test_shared_methods.assert_in_array(exchange, skipped_properties, method, order, 'status', ['open']) + test_shared_methods.assert_timestamp_order(exchange, method, symbol, orders) + return True diff --git a/ccxt/test/exchange/sync/test_fetch_order_book.py b/ccxt/test/exchange/sync/test_fetch_order_book.py new file mode 100644 index 0000000..e62187d --- /dev/null +++ b/ccxt/test/exchange/sync/test_fetch_order_book.py @@ -0,0 +1,21 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_order_book # noqa E402 + +def test_fetch_order_book(exchange, skipped_properties, symbol): + method = 'fetchOrderBook' + orderbook = exchange.fetch_order_book(symbol) + test_order_book(exchange, skipped_properties, method, orderbook, symbol) + return True diff --git a/ccxt/test/exchange/sync/test_fetch_order_books.py b/ccxt/test/exchange/sync/test_fetch_order_books.py new file mode 100644 index 0000000..41054aa --- /dev/null +++ b/ccxt/test/exchange/sync/test_fetch_order_books.py @@ -0,0 +1,27 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_order_book # noqa E402 + +def test_fetch_order_books(exchange, skipped_properties): + method = 'fetchOrderBooks' + symbol = exchange.symbols[0] + order_books = exchange.fetch_order_books([symbol]) + assert isinstance(order_books, dict), exchange.id + ' ' + method + ' must return an object. ' + exchange.json(order_books) + order_book_keys = list(order_books.keys()) + assert len(order_book_keys), exchange.id + ' ' + method + ' returned 0 length data' + for i in range(0, len(order_book_keys)): + symbol_inner = order_book_keys[i] + test_order_book(exchange, skipped_properties, method, order_books[symbol_inner], symbol_inner) + return True diff --git a/ccxt/test/exchange/sync/test_fetch_orders.py b/ccxt/test/exchange/sync/test_fetch_orders.py new file mode 100644 index 0000000..d4829d7 --- /dev/null +++ b/ccxt/test/exchange/sync/test_fetch_orders.py @@ -0,0 +1,27 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_order # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +def test_fetch_orders(exchange, skipped_properties, symbol): + method = 'fetchOrders' + orders = exchange.fetch_orders(symbol) + assert isinstance(orders, list), exchange.id + ' ' + method + ' must return an array, returned ' + exchange.json(orders) + test_shared_methods.assert_non_emtpy_array(exchange, skipped_properties, method, orders, symbol) + now = exchange.milliseconds() + for i in range(0, len(orders)): + test_order(exchange, skipped_properties, method, orders[i], symbol, now) + test_shared_methods.assert_timestamp_order(exchange, method, symbol, orders) + return True diff --git a/ccxt/test/exchange/sync/test_fetch_positions.py b/ccxt/test/exchange/sync/test_fetch_positions.py new file mode 100644 index 0000000..ae797e2 --- /dev/null +++ b/ccxt/test/exchange/sync/test_fetch_positions.py @@ -0,0 +1,35 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_position # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +def test_fetch_positions(exchange, skipped_properties, symbol): + method = 'fetchPositions' + now = exchange.milliseconds() + # without symbol + positions = exchange.fetch_positions() + test_shared_methods.assert_non_emtpy_array(exchange, skipped_properties, method, positions, symbol) + for i in range(0, len(positions)): + test_position(exchange, skipped_properties, method, positions[i], None, now) + # testSharedMethods.assertTimestampOrder (exchange, method, undefined, positions); # currently order of positions does not make sense + # with symbol + positions_for_symbol = exchange.fetch_positions([symbol]) + assert isinstance(positions_for_symbol, list), exchange.id + ' ' + method + ' must return an array, returned ' + exchange.json(positions_for_symbol) + positions_for_symbol_length = len(positions_for_symbol) + assert positions_for_symbol_length <= 4, exchange.id + ' ' + method + ' positions length for particular symbol should be less than 4, returned ' + exchange.json(positions_for_symbol) + for i in range(0, len(positions_for_symbol)): + test_position(exchange, skipped_properties, method, positions_for_symbol[i], symbol, now) + # testSharedMethods.assertTimestampOrder (exchange, method, symbol, positionsForSymbol); + return True diff --git a/ccxt/test/exchange/sync/test_fetch_status.py b/ccxt/test/exchange/sync/test_fetch_status.py new file mode 100644 index 0000000..0e7efe0 --- /dev/null +++ b/ccxt/test/exchange/sync/test_fetch_status.py @@ -0,0 +1,21 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_status # noqa E402 + +def test_fetch_status(exchange, skipped_properties): + method = 'fetchStatus' + status = exchange.fetch_status() + test_status(exchange, skipped_properties, method, status, exchange.milliseconds()) + return True diff --git a/ccxt/test/exchange/sync/test_fetch_ticker.py b/ccxt/test/exchange/sync/test_fetch_ticker.py new file mode 100644 index 0000000..ffd354c --- /dev/null +++ b/ccxt/test/exchange/sync/test_fetch_ticker.py @@ -0,0 +1,21 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_ticker # noqa E402 + +def test_fetch_ticker(exchange, skipped_properties, symbol): + method = 'fetchTicker' + ticker = exchange.fetch_ticker(symbol) + test_ticker(exchange, skipped_properties, method, ticker, symbol) + return True diff --git a/ccxt/test/exchange/sync/test_fetch_tickers.py b/ccxt/test/exchange/sync/test_fetch_tickers.py new file mode 100644 index 0000000..c445cfe --- /dev/null +++ b/ccxt/test/exchange/sync/test_fetch_tickers.py @@ -0,0 +1,58 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_ticker # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +def test_fetch_tickers(exchange, skipped_properties, symbol): + without_symbol = test_fetch_tickers_helper(exchange, skipped_properties, None) + with_symbol = test_fetch_tickers_helper(exchange, skipped_properties, [symbol]) + results = asyncio.gather(*[without_symbol, with_symbol]) + test_fetch_tickers_amounts(exchange, skipped_properties, results[0]) + return results + + +def test_fetch_tickers_helper(exchange, skipped_properties, arg_symbols, arg_params={}): + method = 'fetchTickers' + response = exchange.fetch_tickers(arg_symbols, arg_params) + assert isinstance(response, dict), exchange.id + ' ' + method + ' ' + exchange.json(arg_symbols) + ' must return an object. ' + exchange.json(response) + values = list(response.values()) + checked_symbol = None + if arg_symbols is not None and len(arg_symbols) == 1: + checked_symbol = arg_symbols[0] + test_shared_methods.assert_non_emtpy_array(exchange, skipped_properties, method, values, checked_symbol) + for i in range(0, len(values)): + # todo: symbol check here + ticker = values[i] + test_ticker(exchange, skipped_properties, method, ticker, checked_symbol) + return response + + +def test_fetch_tickers_amounts(exchange, skipped_properties, tickers): + tickers_values = list(tickers.values()) + if not ('checkActiveSymbols' in skipped_properties): + # + # ensure all "active" symbols have tickers + # + non_inactive_markets = test_shared_methods.get_active_markets(exchange) + not_inactive_symbols_length = len(non_inactive_markets) + obtained_tickers_length = len(tickers_values) + min_ratio = 0.99 # 1.0 - 0.01 = 0.99, hardcoded to avoid C# transpiler type casting issues + assert obtained_tickers_length >= not_inactive_symbols_length * min_ratio, exchange.id + ' ' + 'fetchTickers' + ' must return tickers for all active markets. but returned: ' + str(obtained_tickers_length) + ' tickers, ' + str(not_inactive_symbols_length) + ' active markets' + # + # ensure tickers length is less than markets length + # + all_markets = exchange.markets + all_markets_length = len(list(all_markets.keys())) + assert obtained_tickers_length <= all_markets_length, exchange.id + ' ' + 'fetchTickers' + ' must return <= than all markets, but returned: ' + str(obtained_tickers_length) + ' tickers, ' + str(all_markets_length) + ' markets' diff --git a/ccxt/test/exchange/sync/test_fetch_trades.py b/ccxt/test/exchange/sync/test_fetch_trades.py new file mode 100644 index 0000000..34718ee --- /dev/null +++ b/ccxt/test/exchange/sync/test_fetch_trades.py @@ -0,0 +1,28 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_shared_methods # noqa E402 +from ccxt.test.exchange.base import test_trade # noqa E402 + +def test_fetch_trades(exchange, skipped_properties, symbol): + method = 'fetchTrades' + trades = exchange.fetch_trades(symbol) + test_shared_methods.assert_non_emtpy_array(exchange, skipped_properties, method, trades) + now = exchange.milliseconds() + for i in range(0, len(trades)): + test_trade(exchange, skipped_properties, method, trades[i], symbol, now) + test_shared_methods.assert_in_array(exchange, skipped_properties, method, trades[i], 'takerOrMaker', ['taker', None]) + if not ('timestampSort' in skipped_properties): + test_shared_methods.assert_timestamp_order(exchange, method, symbol, trades) + return True diff --git a/ccxt/test/exchange/sync/test_fetch_trading_fee.py b/ccxt/test/exchange/sync/test_fetch_trading_fee.py new file mode 100644 index 0000000..5ada779 --- /dev/null +++ b/ccxt/test/exchange/sync/test_fetch_trading_fee.py @@ -0,0 +1,22 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_trading_fee # noqa E402 + +def test_fetch_trading_fee(exchange, skipped_properties, symbol): + method = 'fetchTradingFee' + fee = exchange.fetch_trading_fee(symbol) + assert isinstance(fee, dict), exchange.id + ' ' + method + ' ' + symbol + ' must return an object. ' + exchange.json(fee) + test_trading_fee(exchange, skipped_properties, method, symbol, fee) + return True diff --git a/ccxt/test/exchange/sync/test_fetch_trading_fees.py b/ccxt/test/exchange/sync/test_fetch_trading_fees.py new file mode 100644 index 0000000..92a8e49 --- /dev/null +++ b/ccxt/test/exchange/sync/test_fetch_trading_fees.py @@ -0,0 +1,26 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_trading_fee # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +def test_fetch_trading_fees(exchange, skipped_properties): + method = 'fetchTradingFees' + fees = exchange.fetch_trading_fees() + symbols = list(fees.keys()) + test_shared_methods.assert_non_emtpy_array(exchange, skipped_properties, method, symbols) + for i in range(0, len(symbols)): + symbol = symbols[i] + test_trading_fee(exchange, skipped_properties, method, symbol, fees[symbol]) + return True diff --git a/ccxt/test/exchange/sync/test_fetch_transaction_fees.py b/ccxt/test/exchange/sync/test_fetch_transaction_fees.py new file mode 100644 index 0000000..89609f6 --- /dev/null +++ b/ccxt/test/exchange/sync/test_fetch_transaction_fees.py @@ -0,0 +1,22 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + + + +def test_fetch_transaction_fees(exchange, skipped_properties): + # const method = 'fetchTransactionFees'; + # const fees = await exchange.fetchTransactionFees (); + # const withdrawKeys = Object.keys (fees['withdraw']); + # todo : assert each entry + return None diff --git a/ccxt/test/exchange/sync/test_fetch_withdrawals.py b/ccxt/test/exchange/sync/test_fetch_withdrawals.py new file mode 100644 index 0000000..2c6bf30 --- /dev/null +++ b/ccxt/test/exchange/sync/test_fetch_withdrawals.py @@ -0,0 +1,26 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_deposit_withdrawal # noqa E402 +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +def test_fetch_withdrawals(exchange, skipped_properties, code): + method = 'fetchWithdrawals' + transactions = exchange.fetch_withdrawals(code) + test_shared_methods.assert_non_emtpy_array(exchange, skipped_properties, method, transactions, code) + now = exchange.milliseconds() + for i in range(0, len(transactions)): + test_deposit_withdrawal(exchange, skipped_properties, method, transactions[i], code, now) + test_shared_methods.assert_timestamp_order(exchange, method, code, transactions) + return True diff --git a/ccxt/test/exchange/sync/test_load_markets.py b/ccxt/test/exchange/sync/test_load_markets.py new file mode 100644 index 0000000..0b7a654 --- /dev/null +++ b/ccxt/test/exchange/sync/test_load_markets.py @@ -0,0 +1,31 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_market # noqa E402 + +def test_load_markets(exchange, skipped_properties): + method = 'loadMarkets' + markets = exchange.load_markets() + assert isinstance(exchange.markets, dict), '.markets is not an object' + assert isinstance(exchange.symbols, list), '.symbols is not an array' + symbols_length = len(exchange.symbols) + market_keys = list(exchange.markets.keys()) + market_keys_length = len(market_keys) + assert symbols_length > 0, '.symbols count <= 0 (less than or equal to zero)' + assert market_keys_length > 0, '.markets objects keys length <= 0 (less than or equal to zero)' + assert symbols_length == market_keys_length, 'number of .symbols is not equal to the number of .markets' + market_values = list(markets.values()) + for i in range(0, len(market_values)): + test_market(exchange, skipped_properties, method, market_values[i]) + return True diff --git a/ccxt/test/exchange/sync/test_proxies.py b/ccxt/test/exchange/sync/test_proxies.py new file mode 100644 index 0000000..2dd6495 --- /dev/null +++ b/ccxt/test/exchange/sync/test_proxies.py @@ -0,0 +1,73 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + +from ccxt.test.exchange.base import test_shared_methods # noqa E402 + +def test_proxies(exchange, skipped_properties): + test_proxy_url(exchange, skipped_properties) + test_http_proxy(exchange, skipped_properties) + # 'httpsProxy', 'socksProxy' + test_proxy_for_exceptions(exchange, skipped_properties) + + +def test_proxy_url(exchange, skipped_properties): + method = 'proxyUrl' + proxy_server_ip = '5.75.153.75' + [proxy_url, http_proxy, https_proxy, socks_proxy] = test_shared_methods.remove_proxy_options(exchange, skipped_properties) + exchange.proxy_url = 'http://' + proxy_server_ip + ':8090/proxy_url.php?caller=https://ccxt.com&url=' + encoded_colon = '%3A' + encoded_slash = '%2F' + ip_check_url = 'https' + encoded_colon + encoded_slash + encoded_slash + 'api.ipify.org' + response = exchange.fetch(ip_check_url) + assert response == proxy_server_ip, exchange.id + ' ' + method + ' test failed. Returned response is ' + response + ' while it should be "' + proxy_server_ip + '"' + # reset the instance property + test_shared_methods.set_proxy_options(exchange, skipped_properties, proxy_url, http_proxy, https_proxy, socks_proxy) + return True + + +def test_http_proxy(exchange, skipped_properties): + method = 'httpProxy' + proxy_server_ip = '5.75.153.75' + [proxy_url, http_proxy, https_proxy, socks_proxy] = test_shared_methods.remove_proxy_options(exchange, skipped_properties) + exchange.http_proxy = 'http://' + proxy_server_ip + ':8911' + ip_check_url = 'https://api.ipify.org/' + response = exchange.fetch(ip_check_url) + assert response == proxy_server_ip, exchange.id + ' ' + method + ' test failed. Returned response is ' + response + ' while it should be "' + proxy_server_ip + '"' + # reset the instance property + test_shared_methods.set_proxy_options(exchange, skipped_properties, proxy_url, http_proxy, https_proxy, socks_proxy) + + +# with the below method we test out all variations of possible proxy options, so at least 2 of them should be set together, and such cases must throw exception +def test_proxy_for_exceptions(exchange, skipped_properties): + method = 'testProxyForExceptions' + [proxy_url, http_proxy, https_proxy, socks_proxy] = test_shared_methods.remove_proxy_options(exchange, skipped_properties) + possible_options_array = ['proxyUrl', 'proxyUrlCallback', 'proxy_url', 'proxy_url_callback', 'httpProxy', 'httpProxyCallback', 'http_proxy', 'http_proxy_callback', 'httpsProxy', 'httpsProxyCallback', 'https_proxy', 'https_proxy_callback', 'socksProxy', 'socksProxyCallback', 'socks_proxy', 'socks_proxy_callback'] + for i in range(0, len(possible_options_array)): + for j in range(0, len(possible_options_array)): + if j != i: + proxy_first = possible_options_array[i] + proxy_second = possible_options_array[j] + exchange.set_property(exchange, proxy_first, '0.0.0.0') # actual value does not matter + exchange.set_property(exchange, proxy_second, '0.0.0.0') # actual value does not matter + exception_caught = False + try: + exchange.fetch('http://example.com') # url does not matter, it will not be called + except Exception as e: + exception_caught = True + assert exception_caught, exchange.id + ' ' + method + ' test failed. No exception was thrown, while ' + proxy_first + ' and ' + proxy_second + ' were set together' + # reset to undefined + exchange.set_property(exchange, proxy_first, None) + exchange.set_property(exchange, proxy_second, None) + # reset the instance property + test_shared_methods.set_proxy_options(exchange, skipped_properties, proxy_url, http_proxy, https_proxy, socks_proxy) diff --git a/ccxt/test/exchange/sync/test_sign_in.py b/ccxt/test/exchange/sync/test_sign_in.py new file mode 100644 index 0000000..f44b9c1 --- /dev/null +++ b/ccxt/test/exchange/sync/test_sign_in.py @@ -0,0 +1,21 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +sys.path.append(root) + +# ---------------------------------------------------------------------------- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +# ---------------------------------------------------------------------------- +# -*- coding: utf-8 -*- + + + +def test_sign_in(exchange, skipped_properties): + method = 'signIn' + if exchange.has[method]: + exchange.sign_in() + return True diff --git a/ccxt/test/tests_async.py b/ccxt/test/tests_async.py new file mode 100644 index 0000000..ead9dbe --- /dev/null +++ b/ccxt/test/tests_async.py @@ -0,0 +1,1764 @@ +# -*- coding: utf-8 -*- + +import asyncio + + +from tests_helpers import AuthenticationError, NotSupported, InvalidProxySettings, ExchangeNotAvailable, OperationFailed, OnMaintenance, get_cli_arg_value, get_root_dir, is_sync, dump, json_parse, json_stringify, convert_ascii, io_file_exists, io_file_read, io_dir_read, call_method, call_method_sync, call_exchange_method_dynamically, call_exchange_method_dynamically_sync, get_root_exception, exception_message, exit_script, get_exchange_prop, set_exchange_prop, init_exchange, get_test_files_sync, get_test_files, set_fetch_response, is_null_value, close, get_env_vars, get_lang, get_ext # noqa: F401 + +class testMainClass: + id_tests = False + request_tests_failed = False + response_tests_failed = False + request_tests = False + ws_tests = False + response_tests = False + info = False + verbose = False + debug = False + private_test = False + private_test_only = False + load_keys = False + sandbox = False + only_specific_tests = [] + skipped_settings_for_exchange = {} + skipped_methods = {} + checked_public_tests = {} + test_files = {} + public_tests = {} + ext = '' + lang = '' + proxy_test_file_name = 'proxies' + + def parse_cli_args_and_props(self): + self.response_tests = get_cli_arg_value('--responseTests') or get_cli_arg_value('--response') + self.id_tests = get_cli_arg_value('--idTests') + self.request_tests = get_cli_arg_value('--requestTests') or get_cli_arg_value('--request') + self.info = get_cli_arg_value('--info') + self.verbose = get_cli_arg_value('--verbose') + self.debug = get_cli_arg_value('--debug') + self.private_test = get_cli_arg_value('--private') + self.private_test_only = get_cli_arg_value('--privateOnly') + self.sandbox = get_cli_arg_value('--sandbox') + self.load_keys = get_cli_arg_value('--loadKeys') + self.ws_tests = get_cli_arg_value('--ws') + self.lang = get_lang() + self.ext = get_ext() + + async def init(self, exchange_id, symbol_argv, method_argv): + try: + await self.init_inner(exchange_id, symbol_argv, method_argv) + except Exception as e: + dump('[TEST_FAILURE]') # tell run-tests.js this is failure + raise e + + async def init_inner(self, exchange_id, symbol_argv, method_argv): + self.parse_cli_args_and_props() + if self.request_tests and self.response_tests: + await self.run_static_request_tests(exchange_id, symbol_argv) + await self.run_static_response_tests(exchange_id, symbol_argv) + return True + if self.response_tests: + await self.run_static_response_tests(exchange_id, symbol_argv) + return True + if self.request_tests: + await self.run_static_request_tests(exchange_id, symbol_argv) # symbol here is the testname + return True + if self.id_tests: + await self.run_broker_id_tests() + return True + new_line = '\n' + dump(new_line + '' + new_line + '' + '[INFO] TESTING ', self.ext, { + 'exchange': exchange_id, + 'symbol': symbol_argv, + 'method': method_argv, + 'isWs': self.ws_tests, + 'useProxy': get_cli_arg_value('--useProxy'), + }, new_line) + exchange_args = { + 'verbose': self.verbose, + 'debug': self.debug, + 'enableRateLimit': True, + 'timeout': 30000, + } + exchange = init_exchange(exchange_id, exchange_args, self.ws_tests) + if exchange.alias: + dump(self.add_padding('[INFO] skipping alias', 25)) + exit_script(0) + await self.import_files(exchange) + assert len(list(self.test_files.keys())) > 0, 'Test files were not loaded' # ensure test files are found & filled + self.expand_settings(exchange) + self.check_if_specific_test_is_chosen(method_argv) + await self.start_test(exchange, symbol_argv) + exit_script(0) # needed to be explicitly finished for WS tests + + def check_if_specific_test_is_chosen(self, method_argv): + if method_argv is not None: + test_file_names = list(self.test_files.keys()) + possible_method_names = method_argv.split(',') # i.e. `test.ts binance fetchBalance,fetchDeposits` + if len(possible_method_names) >= 1: + for i in range(0, len(test_file_names)): + test_file_name = test_file_names[i] + for j in range(0, len(possible_method_names)): + method_name = possible_method_names[j] + method_name = method_name.replace('()', '') + if test_file_name == method_name: + self.only_specific_tests.append(test_file_name) + + async def import_files(self, exchange): + properties = list(exchange.has.keys()) + properties.append('loadMarkets') + if is_sync(): + self.test_files = get_test_files_sync(properties, self.ws_tests) + else: + self.test_files = await get_test_files(properties, self.ws_tests) + return True + + def load_credentials_from_env(self, exchange): + exchange_id = exchange.id + req_creds = get_exchange_prop(exchange, 're' + 'quiredCredentials') # dont glue the r-e-q-u-i-r-e phrase, because leads to messed up transpilation + objkeys = list(req_creds.keys()) + for i in range(0, len(objkeys)): + credential = objkeys[i] + is_required = req_creds[credential] + if is_required and get_exchange_prop(exchange, credential) is None: + full_key = exchange_id + '_' + credential + credential_env_name = full_key.upper() # example: KRAKEN_APIKEY + env_vars = get_env_vars() + credential_value = env_vars[credential_env_name] if (credential_env_name in env_vars) else None + if credential_value: + set_exchange_prop(exchange, credential, credential_value) + + def expand_settings(self, exchange): + exchange_id = exchange.id + keys_global = get_root_dir() + 'keys.json' + keys_local = get_root_dir() + 'keys.local.json' + keys_global_exists = io_file_exists(keys_global) + keys_local_exists = io_file_exists(keys_local) + global_settings = {} + if keys_global_exists: + global_settings = io_file_read(keys_global) + local_settings = {} + if keys_local_exists: + local_settings = io_file_read(keys_local) + all_settings = exchange.deep_extend(global_settings, local_settings) + exchange_settings = exchange.safe_value(all_settings, exchange_id, {}) + if exchange_settings: + setting_keys = list(exchange_settings.keys()) + for i in range(0, len(setting_keys)): + key = setting_keys[i] + if exchange_settings[key]: + final_value = None + if isinstance(exchange_settings[key], dict): + existing = get_exchange_prop(exchange, key, {}) + final_value = exchange.deep_extend(existing, exchange_settings[key]) + else: + final_value = exchange_settings[key] + set_exchange_prop(exchange, key, final_value) + # credentials + if self.load_keys: + self.load_credentials_from_env(exchange) + # skipped tests + skipped_file = get_root_dir() + 'skip-tests.json' + skipped_settings = io_file_read(skipped_file) + self.skipped_settings_for_exchange = exchange.safe_value(skipped_settings, exchange_id, {}) + skipped_settings_for_exchange = self.skipped_settings_for_exchange + # others + timeout = exchange.safe_value(skipped_settings_for_exchange, 'timeout') + if timeout is not None: + exchange.timeout = exchange.parse_to_int(timeout) + if get_cli_arg_value('--useProxy'): + exchange.http_proxy = exchange.safe_string(skipped_settings_for_exchange, 'httpProxy') + exchange.https_proxy = exchange.safe_string(skipped_settings_for_exchange, 'httpsProxy') + exchange.ws_proxy = exchange.safe_string(skipped_settings_for_exchange, 'wsProxy') + exchange.wss_proxy = exchange.safe_string(skipped_settings_for_exchange, 'wssProxy') + self.skipped_methods = exchange.safe_value(skipped_settings_for_exchange, 'skipMethods', {}) + self.checked_public_tests = {} + + def add_padding(self, message, size): + # has to be transpilable + res = '' + message_length = len(message) # avoid php transpilation issue + missing_space = size - message_length - 0 # - 0 is added just to trick transpile to treat the .length as a string for php + if missing_space > 0: + for i in range(0, missing_space): + res += ' ' + return message + res + + async def test_method(self, method_name, exchange, args, is_public): + # todo: temporary skip for c# + if 'OrderBook' in method_name and self.ext == 'cs': + exchange.options['checksum'] = False + # todo: temporary skip for php + if 'OrderBook' in method_name and self.ext == 'php': + return True + skipped_properties_for_method = self.get_skips(exchange, method_name) + is_load_markets = (method_name == 'loadMarkets') + is_fetch_currencies = (method_name == 'fetchCurrencies') + is_proxy_test = (method_name == self.proxy_test_file_name) + is_feature_test = (method_name == 'features') + # if this is a private test, and the implementation was already tested in public, then no need to re-test it in private test (exception is fetchCurrencies, because our approach in base exchange) + if not is_public and (method_name in self.checked_public_tests) and not is_fetch_currencies: + return True + skip_message = None + supported_by_exchange = (method_name in exchange.has) and exchange.has[method_name] + if not is_load_markets and (len(self.only_specific_tests) > 0 and not exchange.in_array(method_name, self.only_specific_tests)): + skip_message = '[INFO] IGNORED_TEST' + elif not is_load_markets and not supported_by_exchange and not is_proxy_test and not is_feature_test: + skip_message = '[INFO] UNSUPPORTED_TEST' # keep it aligned with the longest message + elif isinstance(skipped_properties_for_method, str): + skip_message = '[INFO] SKIPPED_TEST' + elif not (method_name in self.test_files): + skip_message = '[INFO] UNIMPLEMENTED_TEST' + # exceptionally for `loadMarkets` call, we call it before it's even checked for "skip" as we need it to be called anyway (but can skip "test.loadMarket" for it) + if is_load_markets: + await exchange.load_markets(True) + name = exchange.id + if skip_message: + if self.info: + dump(self.add_padding(skip_message, 25), name, method_name) + return True + if self.info: + args_stringified = '(' + exchange.json(args) + ')' # args.join() breaks when we provide a list of symbols or multidimensional array; "args.toString()" breaks bcz of "array to string conversion" + dump(self.add_padding('[INFO] TESTING', 25), name, method_name, args_stringified) + if is_sync(): + call_method_sync(self.test_files, method_name, exchange, skipped_properties_for_method, args) + else: + await call_method(self.test_files, method_name, exchange, skipped_properties_for_method, args) + if self.info: + dump(self.add_padding('[INFO] TESTING DONE', 25), name, method_name) + # add to the list of successed tests + if is_public: + self.checked_public_tests[method_name] = True + return True + + def get_skips(self, exchange, method_name): + final_skips = {} + # check the exact method (i.e. `fetchTrades`) and language-specific (i.e. `fetchTrades.php`) + method_names = [method_name, method_name + '.' + self.ext] + for i in range(0, len(method_names)): + m_name = method_names[i] + if m_name in self.skipped_methods: + # if whole method is skipped, by assigning a string to it, i.e. "fetchOrders":"blabla" + if isinstance(self.skipped_methods[m_name], str): + return self.skipped_methods[m_name] + else: + final_skips = exchange.deep_extend(final_skips, self.skipped_methods[m_name]) + # get "object-specific" skips + object_skips = { + 'orderBook': ['fetchOrderBook', 'fetchOrderBooks', 'fetchL2OrderBook', 'watchOrderBook', 'watchOrderBookForSymbols'], + 'ticker': ['fetchTicker', 'fetchTickers', 'watchTicker', 'watchTickers'], + 'trade': ['fetchTrades', 'watchTrades', 'watchTradesForSymbols'], + 'ohlcv': ['fetchOHLCV', 'watchOHLCV', 'watchOHLCVForSymbols'], + 'ledger': ['fetchLedger', 'fetchLedgerEntry'], + 'depositWithdraw': ['fetchDepositsWithdrawals', 'fetchDeposits', 'fetchWithdrawals'], + 'depositWithdrawFee': ['fetchDepositWithdrawFee', 'fetchDepositWithdrawFees'], + } + object_names = list(object_skips.keys()) + for i in range(0, len(object_names)): + object_name = object_names[i] + object_methods = object_skips[object_name] + if exchange.in_array(method_name, object_methods): + # if whole object is skipped, by assigning a string to it, i.e. "orderBook":"blabla" + if (object_name in self.skipped_methods) and (isinstance(self.skipped_methods[object_name], str)): + return self.skipped_methods[object_name] + extra_skips = exchange.safe_dict(self.skipped_methods, object_name, {}) + final_skips = exchange.deep_extend(final_skips, extra_skips) + # extend related skips + # - if 'timestamp' is skipped, we should do so for 'datetime' too + # - if 'bid' is skipped, skip 'ask' too + if ('timestamp' in final_skips) and not ('datetime' in final_skips): + final_skips['datetime'] = final_skips['timestamp'] + if ('bid' in final_skips) and not ('ask' in final_skips): + final_skips['ask'] = final_skips['bid'] + if ('baseVolume' in final_skips) and not ('quoteVolume' in final_skips): + final_skips['quoteVolume'] = final_skips['baseVolume'] + return final_skips + + async def test_safe(self, method_name, exchange, args=[], is_public=False): + # `testSafe` method does not throw an exception, instead mutes it. The reason we + # mute the thrown exceptions here is because we don't want to stop the whole + # tests queue if any single test-method fails. Instead, they are echoed with + # formatted message "[TEST_FAILURE] ..." and that output is then regex-matched by + # run-tests.js, so the exceptions are still printed out to console from there. + max_retries = 3 + args_stringified = exchange.json(args) # args.join() breaks when we provide a list of symbols or multidimensional array; "args.toString()" breaks bcz of "array to string conversion" + for i in range(0, max_retries): + try: + await self.test_method(method_name, exchange, args, is_public) + return True + except Exception as ex: + e = get_root_exception(ex) + is_load_markets = (method_name == 'loadMarkets') + is_auth_error = (isinstance(e, AuthenticationError)) + is_not_supported = (isinstance(e, NotSupported)) + is_operation_failed = (isinstance(e, OperationFailed)) # includes "DDoSProtection", "RateLimitExceeded", "RequestTimeout", "ExchangeNotAvailable", "OperationFailed", "InvalidNonce", ... + if is_operation_failed: + # if last retry was gone with same `tempFailure` error, then let's eventually return false + if i == max_retries - 1: + is_on_maintenance = (isinstance(e, OnMaintenance)) + is_exchange_not_available = (isinstance(e, ExchangeNotAvailable)) + should_fail = None + ret_success = None + if is_load_markets: + # if "loadMarkets" does not succeed, we must return "false" to caller method, to stop tests continual + ret_success = False + # we might not break exchange tests, if exchange is on maintenance at this moment + if is_on_maintenance: + should_fail = False + else: + should_fail = True + else: + # for any other method tests: + if is_exchange_not_available and not is_on_maintenance: + # break exchange tests if "ExchangeNotAvailable" exception is thrown, but it's not maintenance + should_fail = True + ret_success = False + else: + # in all other cases of OperationFailed, show Warning, but don't mark test as failed + should_fail = False + ret_success = True + # output the message + fail_type = '[TEST_FAILURE]' if should_fail else '[TEST_WARNING]' + dump(fail_type, 'Method could not be tested due to a repeated Network/Availability issues', ' | ', exchange.id, method_name, args_stringified, exception_message(e)) + return ret_success + else: + # wait and retry again + # (increase wait time on every retry) + await exchange.sleep((i + 1) * 1000) + else: + # if it's loadMarkets, then fail test, because it's mandatory for tests + if is_load_markets: + dump('[TEST_FAILURE]', 'Exchange can not load markets', exception_message(e), exchange.id, method_name, args_stringified) + return False + # if the specific arguments to the test method throws "NotSupported" exception + # then let's don't fail the test + if is_not_supported: + if self.info: + dump('[INFO] NOT_SUPPORTED', exception_message(e), exchange.id, method_name, args_stringified) + return True + # If public test faces authentication error, we don't break (see comments under `testSafe` method) + if is_public and is_auth_error: + if self.info: + # todo - turn into warning + dump('[INFO]', 'Authentication problem for public method', exception_message(e), exchange.id, method_name, args_stringified) + return True + else: + dump('[TEST_FAILURE]', exception_message(e), exchange.id, method_name, args_stringified) + return False + return True + + async def run_public_tests(self, exchange, symbol): + tests = { + 'features': [], + 'fetchCurrencies': [], + 'fetchTicker': [symbol], + 'fetchTickers': [symbol], + 'fetchLastPrices': [symbol], + 'fetchOHLCV': [symbol], + 'fetchTrades': [symbol], + 'fetchOrderBook': [symbol], + 'fetchOrderBooks': [], + 'fetchBidsAsks': [], + 'fetchStatus': [], + 'fetchTime': [], + } + if self.ws_tests: + tests = { + 'watchOHLCV': [symbol], + 'watchOHLCVForSymbols': [symbol], + 'watchTicker': [symbol], + 'watchTickers': [symbol], + 'watchBidsAsks': [symbol], + 'watchOrderBook': [symbol], + 'watchOrderBookForSymbols': [[symbol]], + 'watchTrades': [symbol], + 'watchTradesForSymbols': [[symbol]], + } + market = exchange.market(symbol) + is_spot = market['spot'] + if not self.ws_tests: + if is_spot: + tests['fetchCurrencies'] = [] + else: + tests['fetchFundingRates'] = [symbol] + tests['fetchFundingRate'] = [symbol] + tests['fetchFundingRateHistory'] = [symbol] + tests['fetchIndexOHLCV'] = [symbol] + tests['fetchMarkOHLCV'] = [symbol] + tests['fetchPremiumIndexOHLCV'] = [symbol] + self.public_tests = tests + await self.run_tests(exchange, tests, True) + return True + + async def run_tests(self, exchange, tests, is_public_test): + test_names = list(tests.keys()) + promises = [] + for i in range(0, len(test_names)): + test_name = test_names[i] + test_args = tests[test_name] + promises.append(self.test_safe(test_name, exchange, test_args, is_public_test)) + # todo - not yet ready in other langs too + # promises.push (testThrottle ()); + results = await asyncio.gather(*promises) + # now count which test-methods retuned `false` from "testSafe" and dump that info below + failed_methods = [] + for i in range(0, len(test_names)): + test_name = test_names[i] + test_returned_value = results[i] + if not test_returned_value: + failed_methods.append(test_name) + test_prefix_string = 'PUBLIC_TESTS' if is_public_test else 'PRIVATE_TESTS' + if len(failed_methods): + errors_string = ', '.join(failed_methods) + dump('[TEST_FAILURE]', exchange.id, test_prefix_string, 'Failed methods : ' + errors_string) + if self.info: + dump(self.add_padding('[INFO] END ' + test_prefix_string + ' ' + exchange.id, 25)) + return True + + async def load_exchange(self, exchange): + result = await self.test_safe('loadMarkets', exchange, [], True) + if not result: + return False + exchange_symbols_length = len(exchange.symbols) + dump('[INFO:MAIN] Exchange loaded', exchange_symbols_length, 'symbols') + return True + + def get_test_symbol(self, exchange, is_spot, symbols): + symbol = None + preferred_spot_symbol = exchange.safe_string(self.skipped_settings_for_exchange, 'preferredSpotSymbol') + preferred_swap_symbol = exchange.safe_string(self.skipped_settings_for_exchange, 'preferredSwapSymbol') + if is_spot and preferred_spot_symbol: + return preferred_spot_symbol + elif not is_spot and preferred_swap_symbol: + return preferred_swap_symbol + for i in range(0, len(symbols)): + s = symbols[i] + market = exchange.safe_value(exchange.markets, s) + if market is not None: + active = exchange.safe_value(market, 'active') + if active or (active is None): + symbol = s + break + return symbol + + def get_exchange_code(self, exchange, codes=None): + if codes is None: + codes = ['BTC', 'ETH', 'XRP', 'LTC', 'BCH', 'EOS', 'BNB', 'BSV', 'USDT'] + code = codes[0] + for i in range(0, len(codes)): + if codes[i] in exchange.currencies: + return codes[i] + return code + + def get_markets_from_exchange(self, exchange, spot=True): + res = {} + markets = exchange.markets + keys = list(markets.keys()) + for i in range(0, len(keys)): + key = keys[i] + market = markets[key] + if spot and market['spot']: + res[market['symbol']] = market + elif not spot and not market['spot']: + res[market['symbol']] = market + return res + + def get_valid_symbol(self, exchange, spot=True): + current_type_markets = self.get_markets_from_exchange(exchange, spot) + codes = ['BTC', 'ETH', 'XRP', 'LTC', 'BNB', 'DASH', 'DOGE', 'ETC', 'TRX', 'USDT', 'USDC', 'USD', 'GUSD', 'EUR', 'TUSD', 'CNY', 'JPY', 'BRL'] + spot_symbols = ['BTC/USDT', 'BTC/USDC', 'BTC/USD', 'BTC/CNY', 'BTC/EUR', 'BTC/AUD', 'BTC/BRL', 'BTC/JPY', 'ETH/USDT', 'ETH/USDC', 'ETH/USD', 'ETH/CNY', 'ETH/EUR', 'ETH/AUD', 'ETH/BRL', 'ETH/JPY', 'EUR/USDT', 'EUR/USD', 'EUR/USDC', 'USDT/EUR', 'USD/EUR', 'USDC/EUR', 'BTC/ETH', 'ETH/BTC'] + swap_symbols = ['BTC/USDT:USDT', 'BTC/USDC:USDC', 'BTC/USD:USD', 'ETH/USDT:USDT', 'ETH/USDC:USDC', 'ETH/USD:USD', 'BTC/USD:BTC', 'ETH/USD:ETH'] + target_symbols = spot_symbols if spot else swap_symbols + symbol = self.get_test_symbol(exchange, spot, target_symbols) + # if symbols wasn't found from above hardcoded list, then try to locate any symbol which has our target hardcoded 'base' code + if symbol is None: + for i in range(0, len(codes)): + current_code = codes[i] + markets_array_for_current_code = exchange.filter_by(current_type_markets, 'base', current_code) + indexed_mkts = exchange.index_by(markets_array_for_current_code, 'symbol') + symbols_array_for_current_code = list(indexed_mkts.keys()) + symbols_length = len(symbols_array_for_current_code) + if symbols_length: + symbol = self.get_test_symbol(exchange, spot, symbols_array_for_current_code) + break + # if there wasn't found any symbol with our hardcoded 'base' code, then just try to find symbols that are 'active' + if symbol is None: + active_markets = exchange.filter_by(current_type_markets, 'active', True) + active_symbols = [] + for i in range(0, len(active_markets)): + active_symbols.append(active_markets[i]['symbol']) + symbol = self.get_test_symbol(exchange, spot, active_symbols) + if symbol is None: + values = list(current_type_markets.values()) + values_length = len(values) + if values_length > 0: + first = values[0] + if first is not None: + symbol = first['symbol'] + return symbol + + async def test_exchange(self, exchange, provided_symbol=None): + spot_symbol = None + swap_symbol = None + if provided_symbol is not None: + market = exchange.market(provided_symbol) + if market['spot']: + spot_symbol = provided_symbol + else: + swap_symbol = provided_symbol + else: + if exchange.has['spot']: + spot_symbol = self.get_valid_symbol(exchange, True) + if exchange.has['swap']: + swap_symbol = self.get_valid_symbol(exchange, False) + if spot_symbol is not None: + dump('[INFO:MAIN] Selected SPOT SYMBOL:', spot_symbol) + if swap_symbol is not None: + dump('[INFO:MAIN] Selected SWAP SYMBOL:', swap_symbol) + if not self.private_test_only: + # note, spot & swap tests should run sequentially, because of conflicting `exchange.options['defaultType']` setting + if exchange.has['spot'] and spot_symbol is not None: + if self.info: + dump('[INFO] ### SPOT TESTS ###') + exchange.options['defaultType'] = 'spot' + await self.run_public_tests(exchange, spot_symbol) + if exchange.has['swap'] and swap_symbol is not None: + if self.info: + dump('[INFO] ### SWAP TESTS ###') + exchange.options['defaultType'] = 'swap' + await self.run_public_tests(exchange, swap_symbol) + if self.private_test or self.private_test_only: + if exchange.has['spot'] and spot_symbol is not None: + exchange.options['defaultType'] = 'spot' + await self.run_private_tests(exchange, spot_symbol) + if exchange.has['swap'] and swap_symbol is not None: + exchange.options['defaultType'] = 'swap' + await self.run_private_tests(exchange, swap_symbol) + return True + + async def run_private_tests(self, exchange, symbol): + if not exchange.check_required_credentials(False): + dump('[INFO] Skipping private tests', 'Keys not found') + return True + code = self.get_exchange_code(exchange) + # if (exchange.deepExtendedTest) { + # await test ('InvalidNonce', exchange, symbol); + # await test ('OrderNotFound', exchange, symbol); + # await test ('InvalidOrder', exchange, symbol); + # await test ('InsufficientFunds', exchange, symbol, balance); # danger zone - won't execute with non-empty balance + # } + tests = { + 'signIn': [], + 'fetchBalance': [], + 'fetchAccounts': [], + 'fetchTransactionFees': [], + 'fetchTradingFees': [], + 'fetchStatus': [], + 'fetchOrders': [symbol], + 'fetchOpenOrders': [symbol], + 'fetchClosedOrders': [symbol], + 'fetchMyTrades': [symbol], + 'fetchLeverageTiers': [[symbol]], + 'fetchLedger': [code], + 'fetchTransactions': [code], + 'fetchDeposits': [code], + 'fetchWithdrawals': [code], + 'fetchBorrowInterest': [code, symbol], + 'cancelAllOrders': [symbol], + 'fetchCanceledOrders': [symbol], + 'fetchMarginModes': [symbol], + 'fetchPosition': [symbol], + 'fetchDeposit': [code], + 'createDepositAddress': [code], + 'fetchDepositAddress': [code], + 'fetchDepositAddresses': [code], + 'fetchDepositAddressesByNetwork': [code], + 'fetchBorrowRateHistory': [code], + 'fetchLedgerEntry': [code], + } + if get_cli_arg_value('--fundedTests'): + tests['createOrder'] = [symbol] + if self.ws_tests: + tests = { + 'watchBalance': [code], + 'watchMyTrades': [symbol], + 'watchOrders': [symbol], + 'watchPosition': [symbol], + 'watchPositions': [symbol], + } + market = exchange.market(symbol) + is_spot = market['spot'] + if not self.ws_tests: + if is_spot: + tests['fetchCurrencies'] = [] + else: + # derivatives only + tests['fetchPositions'] = [symbol] # this test fetches all positions for 1 symbol + tests['fetchPosition'] = [symbol] + tests['fetchPositionRisk'] = [symbol] + tests['setPositionMode'] = [symbol] + tests['setMarginMode'] = [symbol] + tests['fetchOpenInterestHistory'] = [symbol] + tests['fetchFundingRateHistory'] = [symbol] + tests['fetchFundingHistory'] = [symbol] + # const combinedTests = exchange.deepExtend (this.publicTests, privateTests); + await self.run_tests(exchange, tests, False) + + async def test_proxies(self, exchange): + # these tests should be synchronously executed, because of conflicting nature of proxy settings + proxy_test_name = self.proxy_test_file_name + # todo: temporary skip for sync py + if self.ext == 'py' and is_sync(): + return True + # try proxy several times + max_retries = 3 + exception = None + for j in range(0, max_retries): + try: + await self.test_method(proxy_test_name, exchange, [], True) + return True # if successfull, then end the test + except Exception as e: + exception = e + await exchange.sleep(j * 1000) + # if exception was set, then throw it + if exception is not None: + error_message = '[TEST_FAILURE] Failed ' + proxy_test_name + ' : ' + exception_message(exception) + # temporary comment the below, because c# transpilation failure + # throw new Exchange Error (errorMessage.toString ()); + dump('[TEST_WARNING]' + error_message) + return True + + def check_constructor(self, exchange): + # todo: this might be moved in base tests later + if exchange.id == 'binance': + assert exchange.hostname is None or exchange.hostname == '', 'binance.com hostname should be empty' + assert exchange.urls['api']['public'] == 'https://api.binance.com/api/v3', 'https://api.binance.com/api/v3 does not match: ' + exchange.urls['api']['public'] + assert ('lending/union/account' in exchange.api['sapi']['get']), 'SAPI should contain the endpoint lending/union/account, ' + json_stringify(exchange.api['sapi']['get']) + elif exchange.id == 'binanceus': + assert exchange.hostname == 'binance.us', 'binance.us hostname does not match ' + exchange.hostname + assert exchange.urls['api']['public'] == 'https://api.binance.us/api/v3', 'https://api.binance.us/api/v3 does not match: ' + exchange.urls['api']['public'] + + async def test_return_response_headers(self, exchange): + if exchange.id != 'binance': + return False # this test is only for binance exchange for now + exchange.return_response_headers = True + ticker = await exchange.fetch_ticker('BTC/USDT') + info = ticker['info'] + headers = info['responseHeaders'] + headers_keys = list(headers.keys()) + assert len(headers_keys) > 0, 'Response headers should not be empty' + header_values = list(headers.values()) + assert len(header_values) > 0, 'Response headers values should not be empty' + exchange.return_response_headers = False + return True + + async def start_test(self, exchange, symbol): + # we do not need to test aliases + if exchange.alias: + return True + self.check_constructor(exchange) + # await this.testReturnResponseHeaders (exchange); + if self.sandbox or get_exchange_prop(exchange, 'sandbox'): + exchange.set_sandbox_mode(True) + try: + result = await self.load_exchange(exchange) + if not result: + if not is_sync(): + await close(exchange) + return True + # if (exchange.id === 'binance') { + # # we test proxies functionality just for one random exchange on each build, because proxy functionality is not exchange-specific, instead it's all done from base methods, so just one working sample would mean it works for all ccxt exchanges + # # await this.testProxies (exchange); + # } + await self.test_exchange(exchange, symbol) + if not is_sync(): + await close(exchange) + except Exception as e: + if not is_sync(): + await close(exchange) + raise e + + def assert_static_error(self, cond, message, calculated_output, stored_output, key=None): + # ----------------------------------------------------------------------------- + # --- Init of static tests functions------------------------------------------ + # ----------------------------------------------------------------------------- + calculated_string = json_stringify(calculated_output) + stored_string = json_stringify(stored_output) + error_message = message + if key is not None: + error_message = '[' + key + ']' + error_message += ' computed: ' + stored_string + ' stored: ' + calculated_string + assert cond, error_message + + def load_markets_from_file(self, id): + # load markets from file + # to make this test as fast as possible + # and basically independent from the exchange + # so we can run it offline + filename = get_root_dir() + './ts/src/test/static/markets/' + id + '.json' + content = io_file_read(filename) + return content + + def load_currencies_from_file(self, id): + filename = get_root_dir() + './ts/src/test/static/currencies/' + id + '.json' + content = io_file_read(filename) + return content + + def load_static_data(self, folder, target_exchange=None): + result = {} + if target_exchange: + # read a single exchange + path = folder + target_exchange + '.json' + if not io_file_exists(path): + dump('[WARN] tests not found: ' + path) + return None + result[target_exchange] = io_file_read(path) + return result + files = io_dir_read(folder) + for i in range(0, len(files)): + file = files[i] + exchange_name = file.replace('.json', '') + content = io_file_read(folder + file) + result[exchange_name] = content + return result + + def remove_hostnamefrom_url(self, url): + if url is None: + return None + url_parts = url.split('/') + res = '' + for i in range(0, len(url_parts)): + if i > 2: + current = url_parts[i] + if current.find('?') > -1: + # handle urls like this: /v1/account/accounts?AccessK + current_parts = current.split('?') + res += '/' + res += current_parts[0] + break + res += '/' + res += current + return res + + def urlencoded_to_dict(self, url): + result = {} + parts = url.split('&') + for i in range(0, len(parts)): + part = parts[i] + key_value = part.split('=') + keys_length = len(key_value) + if keys_length != 2: + continue + key = key_value[0] + value = key_value[1] + if (value is not None) and ((value.startswith('[')) or (value.startswith('{'))): + # some exchanges might return something like this: timestamp=1699382693405&batchOrders=[{\"symbol\":\"LTCUSDT\",\"side\":\"BUY\",\"newClientOrderI + value = json_parse(value) + result[key] = value + return result + + def assert_new_and_stored_output_inner(self, exchange, skip_keys, new_output, stored_output, strict_type_check=True, asserting_key=None): + if is_null_value(new_output) and is_null_value(stored_output): + return True + if not new_output and not stored_output: + return True + if (isinstance(stored_output, dict)) and (isinstance(new_output, dict)): + stored_output_keys = list(stored_output.keys()) + new_output_keys = list(new_output.keys()) + stored_keys_length = len(stored_output_keys) + new_keys_length = len(new_output_keys) + self.assert_static_error(stored_keys_length == new_keys_length, 'output length mismatch', stored_output, new_output) + # iterate over the keys + for i in range(0, len(stored_output_keys)): + key = stored_output_keys[i] + if exchange.in_array(key, skip_keys): + continue + if not (exchange.in_array(key, new_output_keys)): + self.assert_static_error(False, 'output key missing: ' + key, stored_output, new_output) + stored_value = stored_output[key] + new_value = new_output[key] + self.assert_new_and_stored_output(exchange, skip_keys, new_value, stored_value, strict_type_check, key) + elif isinstance(stored_output, list) and (isinstance(new_output, list)): + stored_array_length = len(stored_output) + new_array_length = len(new_output) + self.assert_static_error(stored_array_length == new_array_length, 'output length mismatch', stored_output, new_output) + for i in range(0, len(stored_output)): + stored_item = stored_output[i] + new_item = new_output[i] + self.assert_new_and_stored_output(exchange, skip_keys, new_item, stored_item, strict_type_check) + else: + # built-in types like strings, numbers, booleans + sanitized_new_output = None if (is_null_value(new_output)) else new_output # we store undefined as nulls in the json file so we need to convert it back + sanitized_stored_output = None if (is_null_value(stored_output)) else stored_output + new_output_string = str(sanitized_new_output) if sanitized_new_output else 'undefined' + stored_output_string = str(sanitized_stored_output) if sanitized_stored_output else 'undefined' + message_error = 'output value mismatch:' + new_output_string + ' != ' + stored_output_string + if strict_type_check and (self.lang != 'C#'): + # upon building the request we want strict type check to make sure all the types are correct + # when comparing the response we want to allow some flexibility, because a 50.0 can be equal to 50 after saving it to the json file + self.assert_static_error(sanitized_new_output == sanitized_stored_output, message_error, stored_output, new_output, asserting_key) + else: + is_computed_bool = (isinstance(sanitized_new_output, bool)) + is_stored_bool = (isinstance(sanitized_stored_output, bool)) + is_computed_string = (isinstance(sanitized_new_output, str)) + is_stored_string = (isinstance(sanitized_stored_output, str)) + is_computed_undefined = (sanitized_new_output is None) + is_stored_undefined = (sanitized_stored_output is None) + should_be_same = (is_computed_bool == is_stored_bool) and (is_computed_string == is_stored_string) and (is_computed_undefined == is_stored_undefined) + self.assert_static_error(should_be_same, 'output type mismatch', stored_output, new_output, asserting_key) + is_boolean = is_computed_bool or is_stored_bool + is_string = is_computed_string or is_stored_string + is_undefined = is_computed_undefined or is_stored_undefined # undefined is a perfetly valid value + if is_boolean or is_string or is_undefined: + if (self.lang == 'C#') or (self.lang == 'GO'): + # tmp c# number comparsion + is_number = False + try: + exchange.parse_to_numeric(sanitized_new_output) + is_number = True + except Exception as e: + # if we can't parse it to number, then it's not a number + is_number = False + if is_number: + self.assert_static_error(exchange.parse_to_numeric(sanitized_new_output) == exchange.parse_to_numeric(sanitized_stored_output), message_error, stored_output, new_output, asserting_key) + return True + else: + self.assert_static_error(convert_ascii(new_output_string) == convert_ascii(stored_output_string), message_error, stored_output, new_output, asserting_key) + return True + else: + self.assert_static_error(convert_ascii(new_output_string) == convert_ascii(stored_output_string), message_error, stored_output, new_output, asserting_key) + return True + else: + if self.lang == 'C#': + stringified_new_output = exchange.number_to_string(sanitized_new_output) + stringified_stored_output = exchange.number_to_string(sanitized_stored_output) + self.assert_static_error(str(stringified_new_output) == str(stringified_stored_output), message_error, stored_output, new_output, asserting_key) + else: + numeric_new_output = exchange.parse_to_numeric(new_output_string) + numeric_stored_output = exchange.parse_to_numeric(stored_output_string) + self.assert_static_error(numeric_new_output == numeric_stored_output, message_error, stored_output, new_output, asserting_key) + return True # c# requ + + def assert_new_and_stored_output(self, exchange, skip_keys, new_output, stored_output, strict_type_check=True, asserting_key=None): + res = True + try: + res = self.assert_new_and_stored_output_inner(exchange, skip_keys, new_output, stored_output, strict_type_check, asserting_key) + except Exception as e: + if self.info: + error_message = self.var_to_string(new_output) + '(calculated)' + ' != ' + self.var_to_string(stored_output) + '(stored)' + dump('[TEST_FAILURE_DETAIL]' + error_message) + raise e + return res + + def var_to_string(self, obj=None): + new_string = None + if obj is None: + new_string = 'undefined' + elif is_null_value(obj): + new_string = 'null' + else: + new_string = json_stringify(obj) + return new_string + + def assert_static_request_output(self, exchange, type, skip_keys, stored_url, request_url, stored_output, new_output): + if stored_url != request_url: + # remove the host part from the url + first_path = self.remove_hostnamefrom_url(stored_url) + second_path = self.remove_hostnamefrom_url(request_url) + self.assert_static_error(first_path == second_path, 'url mismatch', first_path, second_path) + # body (aka storedOutput and newOutput) is not defined and information is in the url + # example: "https://open-api.bingx.com/openApi/spot/v1/trade/order?quoteOrderQty=5&side=BUY&symbol=LTC-USDT×tamp=1698777135343&type=MARKET&signature=d55a7e4f7f9dbe56c4004c9f3ab340869d3cb004e2f0b5b861e5fbd1762fd9a0 + if (stored_output is None) and (new_output is None): + if (stored_url is not None) and (request_url is not None): + stored_url_parts = stored_url.split('?') + new_url_parts = request_url.split('?') + stored_url_query = exchange.safe_value(stored_url_parts, 1) + new_url_query = exchange.safe_value(new_url_parts, 1) + if (stored_url_query is None) and (new_url_query is None): + # might be a get request without any query parameters + # example: https://api.gateio.ws/api/v4/delivery/usdt/positions + return True + stored_url_params = self.urlencoded_to_dict(stored_url_query) + new_url_params = self.urlencoded_to_dict(new_url_query) + self.assert_new_and_stored_output(exchange, skip_keys, new_url_params, stored_url_params) + return True + if type == 'json' and (stored_output is not None) and (new_output is not None): + if isinstance(stored_output, str): + stored_output = json_parse(stored_output) + if isinstance(new_output, str): + new_output = json_parse(new_output) + elif type == 'urlencoded' and (stored_output is not None) and (new_output is not None): + stored_output = self.urlencoded_to_dict(stored_output) + new_output = self.urlencoded_to_dict(new_output) + elif type == 'both': + if stored_output.startswith('{') or stored_output.startswith('['): + stored_output = json_parse(stored_output) + new_output = json_parse(new_output) + else: + stored_output = self.urlencoded_to_dict(stored_output) + new_output = self.urlencoded_to_dict(new_output) + self.assert_new_and_stored_output(exchange, skip_keys, new_output, stored_output) + return True + + def assert_static_response_output(self, exchange, skip_keys, computed_result, stored_result): + self.assert_new_and_stored_output(exchange, skip_keys, computed_result, stored_result, False) + + def sanitize_data_input(self, input): + # remove nulls and replace with unefined instead + if input is None: + return None + new_input = [] + for i in range(0, len(input)): + current = input[i] + if is_null_value(current): + new_input.append(None) + else: + new_input.append(current) + return new_input + + async def test_request_statically(self, exchange, method, data, type, skip_keys): + output = None + request_url = None + if self.info: + dump('[INFO] STATIC REQUEST TEST:', method, ':', data['description']) + try: + if not is_sync(): + await call_exchange_method_dynamically(exchange, method, self.sanitize_data_input(data['input'])) + else: + call_exchange_method_dynamically_sync(exchange, method, self.sanitize_data_input(data['input'])) + except Exception as e: + if not (isinstance(e, InvalidProxySettings)): + raise e + output = exchange.last_request_body + request_url = exchange.last_request_url + try: + call_output = exchange.safe_value(data, 'output') + self.assert_static_request_output(exchange, type, skip_keys, data['url'], request_url, call_output, output) + except Exception as e: + self.request_tests_failed = True + error_message = '[' + self.lang + '][STATIC_REQUEST]' + '[' + exchange.id + ']' + '[' + method + ']' + '[' + data['description'] + ']' + exception_message(e) + dump('[TEST_FAILURE]' + error_message) + return True + + async def test_response_statically(self, exchange, method, skip_keys, data): + expected_result = exchange.safe_value(data, 'parsedResponse') + mocked_exchange = set_fetch_response(exchange, data['httpResponse']) + if self.info: + dump('[INFO] STATIC RESPONSE TEST:', method, ':', data['description']) + try: + if not is_sync(): + unified_result = await call_exchange_method_dynamically(exchange, method, self.sanitize_data_input(data['input'])) + self.assert_static_response_output(mocked_exchange, skip_keys, unified_result, expected_result) + else: + unified_result_sync = call_exchange_method_dynamically_sync(exchange, method, self.sanitize_data_input(data['input'])) + self.assert_static_response_output(mocked_exchange, skip_keys, unified_result_sync, expected_result) + except Exception as e: + self.response_tests_failed = True + error_message = '[' + self.lang + '][STATIC_RESPONSE]' + '[' + exchange.id + ']' + '[' + method + ']' + '[' + data['description'] + ']' + exception_message(e) + dump('[TEST_FAILURE]' + error_message) + set_fetch_response(exchange, None) # reset state + return True + + def init_offline_exchange(self, exchange_name): + markets = self.load_markets_from_file(exchange_name) + currencies = self.load_currencies_from_file(exchange_name) + # we add "proxy" 2 times to intentionally trigger InvalidProxySettings + exchange = init_exchange(exchange_name, { + 'markets': markets, + 'currencies': currencies, + 'enableRateLimit': False, + 'rateLimit': 1, + 'httpProxy': 'http://fake:8080', + 'httpsProxy': 'http://fake:8080', + 'apiKey': 'key', + 'secret': 'secretsecret', + 'password': 'password', + 'walletAddress': 'wallet', + 'privateKey': '0xff3bdd43534543d421f05aec535965b5050ad6ac15345435345435453495e771', + 'uid': 'uid', + 'token': 'token', + 'login': 'login', + 'accountId': '12345', + 'accounts': [{ + 'id': 'myAccount', + 'code': 'USDT', +}, { + 'id': 'myAccount', + 'code': 'USDC', +}], + 'options': { + 'enableUnifiedAccount': True, + 'enableUnifiedMargin': False, + 'accessToken': 'token', + 'expires': 999999999999999, + 'leverageBrackets': {}, + }, + }) + exchange.currencies = currencies + # not working in python if assigned in the config dict + return exchange + + async def test_exchange_request_statically(self, exchange_name, exchange_data, test_name=None): + # instantiate the exchange and make sure that we sink the requests to avoid an actual request + exchange = self.init_offline_exchange(exchange_name) + global_options = exchange.safe_dict(exchange_data, 'options', {}) + # read apiKey/secret from the test file + api_key = exchange.safe_string(exchange_data, 'apiKey') + if api_key: + exchange.apiKey = str(api_key) + secret = exchange.safe_string(exchange_data, 'secret') + if secret: + exchange.secret = str(secret) + private_key = exchange.safe_string(exchange_data, 'privateKey') + if private_key: + exchange.privateKey = str(private_key) + wallet_address = exchange.safe_string(exchange_data, 'walletAddress') + if wallet_address: + exchange.walletAddress = str(wallet_address) + accounts = exchange.safe_list(exchange_data, 'accounts') + if accounts: + exchange.accounts = accounts + # exchange.options = exchange.deepExtend (exchange.options, globalOptions); # custom options to be used in the tests + exchange.extend_exchange_options(global_options) + methods = exchange.safe_value(exchange_data, 'methods', {}) + methods_names = list(methods.keys()) + for i in range(0, len(methods_names)): + method = methods_names[i] + results = methods[method] + for j in range(0, len(results)): + result = results[j] + old_exchange_options = exchange.options # snapshot options; + test_exchange_options = exchange.safe_value(result, 'options', {}) + # exchange.options = exchange.deepExtend (oldExchangeOptions, testExchangeOptions); # custom options to be used in the tests + exchange.extend_exchange_options(exchange.deep_extend(old_exchange_options, test_exchange_options)) + description = exchange.safe_value(result, 'description') + if (test_name is not None) and (test_name != description): + continue + is_disabled = exchange.safe_bool(result, 'disabled', False) + if is_disabled: + continue + disabled_string = exchange.safe_string(result, 'disabled', '') + if disabled_string != '': + continue + is_disabled_c_sharp = exchange.safe_bool(result, 'disabledCS', False) + if is_disabled_c_sharp and (self.lang == 'C#'): + continue + is_disabled_go = exchange.safe_bool(result, 'disabledGO', False) + if is_disabled_go and (self.lang == 'GO'): + continue + type = exchange.safe_string(exchange_data, 'outputType') + skip_keys = exchange.safe_value(exchange_data, 'skipKeys', []) + await self.test_request_statically(exchange, method, result, type, skip_keys) + # reset options + exchange.options = exchange.convert_to_safe_dictionary(exchange.deep_extend(old_exchange_options, {})) + if not is_sync(): + await close(exchange) + return True # in c# methods that will be used with promiseAll need to return something + + async def test_exchange_response_statically(self, exchange_name, exchange_data, test_name=None): + exchange = self.init_offline_exchange(exchange_name) + # read apiKey/secret from the test file + api_key = exchange.safe_string(exchange_data, 'apiKey') + if api_key: + exchange.apiKey = str(api_key) + secret = exchange.safe_string(exchange_data, 'secret') + if secret: + exchange.secret = str(secret) + private_key = exchange.safe_string(exchange_data, 'privateKey') + if private_key: + exchange.privateKey = str(private_key) + wallet_address = exchange.safe_string(exchange_data, 'walletAddress') + if wallet_address: + exchange.walletAddress = str(wallet_address) + methods = exchange.safe_value(exchange_data, 'methods', {}) + options = exchange.safe_value(exchange_data, 'options', {}) + # exchange.options = exchange.deepExtend (exchange.options, options); # custom options to be used in the tests + exchange.extend_exchange_options(options) + methods_names = list(methods.keys()) + for i in range(0, len(methods_names)): + method = methods_names[i] + results = methods[method] + for j in range(0, len(results)): + result = results[j] + description = exchange.safe_value(result, 'description') + old_exchange_options = exchange.options # snapshot options; + test_exchange_options = exchange.safe_value(result, 'options', {}) + # exchange.options = exchange.deepExtend (oldExchangeOptions, testExchangeOptions); # custom options to be used in the tests + exchange.extend_exchange_options(exchange.deep_extend(old_exchange_options, test_exchange_options)) + is_disabled = exchange.safe_bool(result, 'disabled', False) + if is_disabled: + continue + is_disabled_c_sharp = exchange.safe_bool(result, 'disabledCS', False) + if is_disabled_c_sharp and (self.lang == 'C#'): + continue + is_disabled_php = exchange.safe_bool(result, 'disabledPHP', False) + if is_disabled_php and (self.lang == 'PHP'): + continue + if (test_name is not None) and (test_name != description): + continue + is_disabled_go = exchange.safe_bool(result, 'disabledGO', False) + if is_disabled_go and (self.lang == 'GO'): + continue + skip_keys = exchange.safe_value(exchange_data, 'skipKeys', []) + await self.test_response_statically(exchange, method, skip_keys, result) + # reset options + # exchange.options = exchange.deepExtend (oldExchangeOptions, {}); + exchange.extend_exchange_options(exchange.deep_extend(old_exchange_options, {})) + if not is_sync(): + await close(exchange) + return True # in c# methods that will be used with promiseAll need to return something + + def get_number_of_tests_from_exchange(self, exchange, exchange_data, test_name=None): + if test_name is not None: + return 1 + sum = 0 + methods = exchange_data['methods'] + methods_names = list(methods.keys()) + for i in range(0, len(methods_names)): + method = methods_names[i] + results = methods[method] + results_length = len(results) + sum = exchange.sum(sum, results_length) + return sum + + def check_if_exchange_is_disabled(self, exchange_name, exchange_data): + exchange = init_exchange('Exchange', {}) + is_disabled_py = exchange.safe_bool(exchange_data, 'disabledPy', False) + if is_disabled_py and (self.lang == 'PY'): + dump('[TEST_WARNING] Exchange ' + exchange_name + ' is disabled in python') + return True + is_disabled_php = exchange.safe_bool(exchange_data, 'disabledPHP', False) + if is_disabled_php and (self.lang == 'PHP'): + dump('[TEST_WARNING] Exchange ' + exchange_name + ' is disabled in php') + return True + is_disabled_c_sharp = exchange.safe_bool(exchange_data, 'disabledCS', False) + if is_disabled_c_sharp and (self.lang == 'C#'): + dump('[TEST_WARNING] Exchange ' + exchange_name + ' is disabled in c#') + return True + is_disabled_go = exchange.safe_bool(exchange_data, 'disabledGO', False) + if is_disabled_go and (self.lang == 'GO'): + dump('[TEST_WARNING] Exchange ' + exchange_name + ' is disabled in go') + return True + return False + + async def run_static_request_tests(self, target_exchange=None, test_name=None): + await self.run_static_tests('request', target_exchange, test_name) + return True + + async def run_static_tests(self, type, target_exchange=None, test_name=None): + folder = get_root_dir() + './ts/src/test/static/' + type + '/' + static_data = self.load_static_data(folder, target_exchange) + if static_data is None: + return True + exchanges = list(static_data.keys()) + exchange = init_exchange('Exchange', {}) # tmp to do the calculations until we have the ast-transpiler transpiling this code + promises = [] + sum = 0 + if target_exchange: + dump('[INFO:MAIN] Exchange to test: ' + target_exchange) + if test_name: + dump('[INFO:MAIN] Testing only: ' + test_name) + for i in range(0, len(exchanges)): + exchange_name = exchanges[i] + exchange_data = static_data[exchange_name] + disabled = self.check_if_exchange_is_disabled(exchange_name, exchange_data) + if disabled: + continue + number_of_tests = self.get_number_of_tests_from_exchange(exchange, exchange_data, test_name) + sum = exchange.sum(sum, number_of_tests) + if type == 'request': + promises.append(self.test_exchange_request_statically(exchange_name, exchange_data, test_name)) + else: + promises.append(self.test_exchange_response_statically(exchange_name, exchange_data, test_name)) + try: + await asyncio.gather(*promises) + except Exception as e: + if type == 'request': + self.request_tests_failed = True + else: + self.response_tests_failed = True + error_message = '[' + self.lang + '][STATIC_REQUEST]' + exception_message(e) + dump('[TEST_FAILURE]' + error_message) + if self.request_tests_failed or self.response_tests_failed: + exit_script(1) + else: + prefix = '[SYNC]' if (is_sync()) else '' + success_message = '[' + self.lang + ']' + prefix + '[TEST_SUCCESS] ' + str(sum) + ' static ' + type + ' tests passed.' + dump('[INFO]' + success_message) + + async def run_static_response_tests(self, exchange_name=None, test=None): + # ----------------------------------------------------------------------------- + # --- Init of mockResponses tests functions------------------------------------ + # ----------------------------------------------------------------------------- + await self.run_static_tests('response', exchange_name, test) + return True + + async def run_broker_id_tests(self): + # ----------------------------------------------------------------------------- + # --- Init of brokerId tests functions----------------------------------------- + # ----------------------------------------------------------------------------- + promises = [self.test_binance(), self.test_okx(), self.test_cryptocom(), self.test_bybit(), self.test_kucoin(), self.test_kucoinfutures(), self.test_bitget(), self.test_mexc(), self.test_htx(), self.test_woo(), self.test_bitmart(), self.test_coinex(), self.test_bingx(), self.test_phemex(), self.test_blofin(), self.test_coinbaseinternational(), self.test_coinbase_advanced(), self.test_woofi_pro(), self.test_oxfun(), self.test_xt(), self.test_paradex(), self.test_hashkey(), self.test_coincatch(), self.test_defx(), self.test_cryptomus(), self.test_derive(), self.test_mode_trade(), self.test_backpack()] + await asyncio.gather(*promises) + success_message = '[' + self.lang + '][TEST_SUCCESS] brokerId tests passed.' + dump('[INFO]' + success_message) + exit_script(0) + return True + + async def test_binance(self): + exchange = self.init_offline_exchange('binance') + spot_id = 'x-TKT5PX2F' + swap_id = 'x-cvBPrNm9' + inverse_swap_id = 'x-xcKtGhcu' + spot_order_request = None + try: + await exchange.create_order('BTC/USDT', 'limit', 'buy', 1, 20000) + except Exception as e: + spot_order_request = self.urlencoded_to_dict(exchange.last_request_body) + client_order_id = spot_order_request['newClientOrderId'] + spot_id_string = str(spot_id) + assert client_order_id.startswith(spot_id_string), 'binance - spot clientOrderId: ' + client_order_id + ' does not start with spotId' + spot_id_string + swap_order_request = None + try: + await exchange.create_order('BTC/USDT:USDT', 'limit', 'buy', 1, 20000) + except Exception as e: + swap_order_request = self.urlencoded_to_dict(exchange.last_request_body) + swap_inverse_order_request = None + try: + await exchange.create_order('BTC/USD:BTC', 'limit', 'buy', 1, 20000) + except Exception as e: + swap_inverse_order_request = self.urlencoded_to_dict(exchange.last_request_body) + # linear swap + client_order_id_swap = swap_order_request['newClientOrderId'] + swap_id_string = str(swap_id) + assert client_order_id_swap.startswith(swap_id_string), 'binance - swap clientOrderId: ' + client_order_id_swap + ' does not start with swapId' + swap_id_string + # inverse swap + client_order_id_inverse = swap_inverse_order_request['newClientOrderId'] + assert client_order_id_inverse.startswith(inverse_swap_id), 'binance - swap clientOrderIdInverse: ' + client_order_id_inverse + ' does not start with swapId' + inverse_swap_id + create_orders_request = None + try: + orders = [{ + 'symbol': 'BTC/USDT:USDT', + 'type': 'limit', + 'side': 'sell', + 'amount': 1, + 'price': 100000, +}, { + 'symbol': 'BTC/USDT:USDT', + 'type': 'market', + 'side': 'buy', + 'amount': 1, +}] + await exchange.create_orders(orders) + except Exception as e: + create_orders_request = self.urlencoded_to_dict(exchange.last_request_body) + batch_orders = create_orders_request['batchOrders'] + for i in range(0, len(batch_orders)): + current = batch_orders[i] + current_client_order_id = current['newClientOrderId'] + assert current_client_order_id.startswith(swap_id_string), 'binance createOrders - clientOrderId: ' + current_client_order_id + ' does not start with swapId' + swap_id_string + if not is_sync(): + await close(exchange) + return True + + async def test_okx(self): + exchange = self.init_offline_exchange('okx') + id = '6b9ad766b55dBCDE' + spot_order_request = None + try: + await exchange.create_order('BTC/USDT', 'limit', 'buy', 1, 20000) + except Exception as e: + spot_order_request = json_parse(exchange.last_request_body) + client_order_id = spot_order_request[0]['clOrdId'] # returns order inside array + id_string = str(id) + assert client_order_id.startswith(id_string), 'okx - spot clientOrderId: ' + client_order_id + ' does not start with id: ' + id_string + spot_tag = spot_order_request[0]['tag'] + assert spot_tag == id, 'okx - id: ' + id + ' different from spot tag: ' + spot_tag + swap_order_request = None + try: + await exchange.create_order('BTC/USDT:USDT', 'limit', 'buy', 1, 20000) + except Exception as e: + swap_order_request = json_parse(exchange.last_request_body) + client_order_id_swap = swap_order_request[0]['clOrdId'] + assert client_order_id_swap.startswith(id_string), 'okx - swap clientOrderId: ' + client_order_id_swap + ' does not start with id: ' + id_string + swap_tag = swap_order_request[0]['tag'] + assert swap_tag == id, 'okx - id: ' + id + ' different from swap tag: ' + swap_tag + if not is_sync(): + await close(exchange) + return True + + async def test_cryptocom(self): + exchange = self.init_offline_exchange('cryptocom') + id = 'CCXT' + await exchange.load_markets() + request = None + try: + await exchange.create_order('BTC/USDT', 'limit', 'buy', 1, 20000) + except Exception as e: + request = json_parse(exchange.last_request_body) + broker_id = request['params']['broker_id'] + assert broker_id == id, 'cryptocom - id: ' + id + ' different from broker_id: ' + broker_id + if not is_sync(): + await close(exchange) + return True + + async def test_bybit(self): + exchange = self.init_offline_exchange('bybit') + req_headers = None + id = 'CCXT' + assert exchange.options['brokerId'] == id, 'id not in options' + try: + await exchange.create_order('BTC/USDT', 'limit', 'buy', 1, 20000) + except Exception as e: + # we expect an error here, we're only interested in the headers + req_headers = exchange.last_request_headers + assert req_headers['Referer'] == id, 'bybit - id: ' + id + ' not in headers.' + if not is_sync(): + await close(exchange) + return True + + async def test_kucoin(self): + exchange = self.init_offline_exchange('kucoin') + req_headers = None + spot_id = exchange.options['partner']['spot']['id'] + spot_key = exchange.options['partner']['spot']['key'] + assert spot_id == 'ccxt', 'kucoin - id: ' + spot_id + ' not in options' + assert spot_key == '9e58cc35-5b5e-4133-92ec-166e3f077cb8', 'kucoin - key: ' + spot_key + ' not in options.' + try: + await exchange.create_order('BTC/USDT', 'limit', 'buy', 1, 20000) + except Exception as e: + # we expect an error here, we're only interested in the headers + req_headers = exchange.last_request_headers + id = 'ccxt' + assert req_headers['KC-API-PARTNER'] == id, 'kucoin - id: ' + id + ' not in headers.' + if not is_sync(): + await close(exchange) + return True + + async def test_kucoinfutures(self): + exchange = self.init_offline_exchange('kucoinfutures') + req_headers = None + id = 'ccxtfutures' + future_id = exchange.options['partner']['future']['id'] + future_key = exchange.options['partner']['future']['key'] + assert future_id == id, 'kucoinfutures - id: ' + future_id + ' not in options.' + assert future_key == '1b327198-f30c-4f14-a0ac-918871282f15', 'kucoinfutures - key: ' + future_key + ' not in options.' + try: + await exchange.create_order('BTC/USDT:USDT', 'limit', 'buy', 1, 20000) + except Exception as e: + req_headers = exchange.last_request_headers + assert req_headers['KC-API-PARTNER'] == id, 'kucoinfutures - id: ' + id + ' not in headers.' + if not is_sync(): + await close(exchange) + return True + + async def test_bitget(self): + exchange = self.init_offline_exchange('bitget') + req_headers = None + id = 'p4sve' + assert exchange.options['broker'] == id, 'bitget - id: ' + id + ' not in options' + try: + await exchange.create_order('BTC/USDT', 'limit', 'buy', 1, 20000) + except Exception as e: + req_headers = exchange.last_request_headers + assert req_headers['X-CHANNEL-API-CODE'] == id, 'bitget - id: ' + id + ' not in headers.' + if not is_sync(): + await close(exchange) + return True + + async def test_mexc(self): + exchange = self.init_offline_exchange('mexc') + req_headers = None + id = 'CCXT' + assert exchange.options['broker'] == id, 'mexc - id: ' + id + ' not in options' + await exchange.load_markets() + try: + await exchange.create_order('BTC/USDT', 'limit', 'buy', 1, 20000) + except Exception as e: + req_headers = exchange.last_request_headers + assert req_headers['source'] == id, 'mexc - id: ' + id + ' not in headers.' + if not is_sync(): + await close(exchange) + return True + + async def test_htx(self): + exchange = self.init_offline_exchange('htx') + # spot test + id = 'AA03022abc' + spot_order_request = None + try: + await exchange.create_order('BTC/USDT', 'limit', 'buy', 1, 20000) + except Exception as e: + spot_order_request = json_parse(exchange.last_request_body) + client_order_id = spot_order_request['client-order-id'] + id_string = str(id) + assert client_order_id.startswith(id_string), 'htx - spot clientOrderId ' + client_order_id + ' does not start with id: ' + id_string + # swap test + swap_order_request = None + try: + await exchange.create_order('BTC/USDT:USDT', 'limit', 'buy', 1, 20000) + except Exception as e: + swap_order_request = json_parse(exchange.last_request_body) + swap_inverse_order_request = None + try: + await exchange.create_order('BTC/USD:BTC', 'limit', 'buy', 1, 20000) + except Exception as e: + swap_inverse_order_request = json_parse(exchange.last_request_body) + client_order_id_swap = swap_order_request['channel_code'] + assert client_order_id_swap.startswith(id_string), 'htx - swap channel_code ' + client_order_id_swap + ' does not start with id: ' + id_string + client_order_id_inverse = swap_inverse_order_request['channel_code'] + assert client_order_id_inverse.startswith(id_string), 'htx - swap inverse channel_code ' + client_order_id_inverse + ' does not start with id: ' + id_string + if not is_sync(): + await close(exchange) + return True + + async def test_woo(self): + exchange = self.init_offline_exchange('woo') + # spot test + id = 'bc830de7-50f3-460b-9ee0-f430f83f9dad' + spot_order_request = None + try: + await exchange.create_order('BTC/USDT', 'limit', 'buy', 1, 20000) + except Exception as e: + spot_order_request = json_parse(exchange.last_request_body) + broker_id = spot_order_request['broker_id'] + id_string = str(id) + assert broker_id.startswith(id_string), 'woo - broker_id: ' + broker_id + ' does not start with id: ' + id_string + # swap test + stop_order_request = None + try: + await exchange.create_order('BTC/USDT:USDT', 'limit', 'buy', 1, 20000, { + 'stopPrice': 30000, + }) + except Exception as e: + stop_order_request = json_parse(exchange.last_request_body) + client_order_id_stop = stop_order_request['brokerId'] + assert client_order_id_stop.startswith(id_string), 'woo - brokerId: ' + client_order_id_stop + ' does not start with id: ' + id_string + if not is_sync(): + await close(exchange) + return True + + async def test_bitmart(self): + exchange = self.init_offline_exchange('bitmart') + req_headers = None + id = 'CCXTxBitmart000' + assert exchange.options['brokerId'] == id, 'bitmart - id: ' + id + ' not in options' + await exchange.load_markets() + try: + await exchange.create_order('BTC/USDT', 'limit', 'buy', 1, 20000) + except Exception as e: + req_headers = exchange.last_request_headers + assert req_headers['X-BM-BROKER-ID'] == id, 'bitmart - id: ' + id + ' not in headers' + if not is_sync(): + await close(exchange) + return True + + async def test_coinex(self): + exchange = self.init_offline_exchange('coinex') + id = 'x-167673045' + assert exchange.options['brokerId'] == id, 'coinex - id: ' + id + ' not in options' + spot_order_request = None + try: + await exchange.create_order('BTC/USDT', 'limit', 'buy', 1, 20000) + except Exception as e: + spot_order_request = json_parse(exchange.last_request_body) + client_order_id = spot_order_request['client_id'] + id_string = str(id) + assert client_order_id.startswith(id_string), 'coinex - clientOrderId: ' + client_order_id + ' does not start with id: ' + id_string + if not is_sync(): + await close(exchange) + return True + + async def test_bingx(self): + exchange = self.init_offline_exchange('bingx') + req_headers = None + id = 'CCXT' + assert exchange.options['broker'] == id, 'bingx - id: ' + id + ' not in options' + try: + await exchange.create_order('BTC/USDT', 'limit', 'buy', 1, 20000) + except Exception as e: + # we expect an error here, we're only interested in the headers + req_headers = exchange.last_request_headers + assert req_headers['X-SOURCE-KEY'] == id, 'bingx - id: ' + id + ' not in headers.' + if not is_sync(): + await close(exchange) + return True + + async def test_phemex(self): + exchange = self.init_offline_exchange('phemex') + id = 'CCXT123456' + request = None + try: + await exchange.create_order('BTC/USDT', 'limit', 'buy', 1, 20000) + except Exception as e: + request = json_parse(exchange.last_request_body) + client_order_id = request['clOrdID'] + id_string = str(id) + assert client_order_id.startswith(id_string), 'phemex - clOrdID: ' + client_order_id + ' does not start with id: ' + id_string + if not is_sync(): + await close(exchange) + return True + + async def test_blofin(self): + exchange = self.init_offline_exchange('blofin') + id = 'ec6dd3a7dd982d0b' + request = None + try: + await exchange.create_order('LTC/USDT:USDT', 'market', 'buy', 1) + except Exception as e: + request = json_parse(exchange.last_request_body) + broker_id = request['brokerId'] + id_string = str(id) + assert broker_id.startswith(id_string), 'blofin - brokerId: ' + broker_id + ' does not start with id: ' + id_string + if not is_sync(): + await close(exchange) + return True + + # async testHyperliquid () { + # const exchange = this.initOfflineExchange ('hyperliquid'); + # const id = '1'; + # let request = undefined; + # try { + # await exchange.createOrder ('SOL/USDC:USDC', 'limit', 'buy', 1, 100); + # } catch (e) { + # request = jsonParse (exchange.last_request_body); + # } + # const brokerId = (request['action']['brokerCode']).toString (); + # assert (brokerId === id, 'hyperliquid - brokerId: ' + brokerId + ' does not start with id: ' + id); + # if (!isSync ()) { + # await close (exchange); + # } + # return true; + # } + async def test_coinbaseinternational(self): + exchange = self.init_offline_exchange('coinbaseinternational') + exchange.options['portfolio'] = 'random' + id = 'nfqkvdjp' + assert exchange.options['brokerId'] == id, 'id not in options' + request = None + try: + await exchange.create_order('BTC/USDC:USDC', 'limit', 'buy', 1, 20000) + except Exception as e: + request = json_parse(exchange.last_request_body) + client_order_id = request['client_order_id'] + assert client_order_id.startswith(str(id)), 'clientOrderId does not start with id' + if not is_sync(): + await close(exchange) + return True + + async def test_coinbase_advanced(self): + exchange = self.init_offline_exchange('coinbase') + id = 'ccxt' + assert exchange.options['brokerId'] == id, 'id not in options' + request = None + try: + await exchange.create_order('BTC/USDC', 'limit', 'buy', 1, 20000) + except Exception as e: + request = json_parse(exchange.last_request_body) + client_order_id = request['client_order_id'] + assert client_order_id.startswith(str(id)), 'clientOrderId does not start with id' + if not is_sync(): + await close(exchange) + return True + + async def test_woofi_pro(self): + exchange = self.init_offline_exchange('woofipro') + exchange.secret = 'secretsecretsecretsecretsecretsecretsecrets' + id = 'CCXT' + await exchange.load_markets() + request = None + try: + await exchange.create_order('BTC/USDC:USDC', 'limit', 'buy', 1, 20000) + except Exception as e: + request = json_parse(exchange.last_request_body) + broker_id = request['order_tag'] + assert broker_id == id, 'woofipro - id: ' + id + ' different from broker_id: ' + broker_id + if not is_sync(): + await close(exchange) + return True + + async def test_oxfun(self): + exchange = self.init_offline_exchange('oxfun') + exchange.secret = 'secretsecretsecretsecretsecretsecretsecrets' + id = 1000 + await exchange.load_markets() + request = None + try: + await exchange.create_order('BTC/USD:OX', 'limit', 'buy', 1, 20000) + except Exception as e: + request = json_parse(exchange.last_request_body) + orders = request['orders'] + first = orders[0] + broker_id = first['source'] + assert broker_id == id, 'oxfun - id: ' + str(id) + ' different from broker_id: ' + str(broker_id) + return True + + async def test_xt(self): + exchange = self.init_offline_exchange('xt') + id = 'CCXT' + spot_order_request = None + try: + await exchange.create_order('BTC/USDT', 'limit', 'buy', 1, 20000) + except Exception as e: + spot_order_request = json_parse(exchange.last_request_body) + spot_media = spot_order_request['media'] + assert spot_media == id, 'xt - id: ' + id + ' different from swap tag: ' + spot_media + swap_order_request = None + try: + await exchange.create_order('BTC/USDT:USDT', 'limit', 'buy', 1, 20000) + except Exception as e: + swap_order_request = json_parse(exchange.last_request_body) + swap_media = swap_order_request['clientMedia'] + assert swap_media == id, 'xt - id: ' + id + ' different from swap tag: ' + swap_media + if not is_sync(): + await close(exchange) + return True + + async def test_paradex(self): + exchange = self.init_offline_exchange('paradex') + exchange.walletAddress = '0xc751489d24a33172541ea451bc253d7a9e98c781' + exchange.privateKey = 'c33b1eb4b53108bf52e10f636d8c1236c04c33a712357ba3543ab45f48a5cb0b' + exchange.options['authToken'] = 'token' + exchange.options['systemConfig'] = { + 'starknet_gateway_url': 'https://potc-testnet-sepolia.starknet.io', + 'starknet_fullnode_rpc_url': 'https://pathfinder.api.testnet.paradex.trade/rpc/v0_7', + 'starknet_chain_id': 'PRIVATE_SN_POTC_SEPOLIA', + 'block_explorer_url': 'https://voyager.testnet.paradex.trade/', + 'paraclear_address': '0x286003f7c7bfc3f94e8f0af48b48302e7aee2fb13c23b141479ba00832ef2c6', + 'paraclear_decimals': 8, + 'paraclear_account_proxy_hash': '0x3530cc4759d78042f1b543bf797f5f3d647cde0388c33734cf91b7f7b9314a9', + 'paraclear_account_hash': '0x41cb0280ebadaa75f996d8d92c6f265f6d040bb3ba442e5f86a554f1765244e', + 'oracle_address': '0x2c6a867917ef858d6b193a0ff9e62b46d0dc760366920d631715d58baeaca1f', + 'bridged_tokens': [{ + 'name': 'TEST USDC', + 'symbol': 'USDC', + 'decimals': 6, + 'l1_token_address': '0x29A873159D5e14AcBd63913D4A7E2df04570c666', + 'l1_bridge_address': '0x8586e05adc0C35aa11609023d4Ae6075Cb813b4C', + 'l2_token_address': '0x6f373b346561036d98ea10fb3e60d2f459c872b1933b50b21fe6ef4fda3b75e', + 'l2_bridge_address': '0x46e9237f5408b5f899e72125dd69bd55485a287aaf24663d3ebe00d237fc7ef', +}], + 'l1_core_contract_address': '0x582CC5d9b509391232cd544cDF9da036e55833Af', + 'l1_operator_address': '0x11bACdFbBcd3Febe5e8CEAa75E0Ef6444d9B45FB', + 'l1_chain_id': '11155111', + 'liquidation_fee': '0.2', + } + req_headers = None + id = 'CCXT' + assert exchange.options['broker'] == id, 'paradex - id: ' + id + ' not in options' + await exchange.load_markets() + try: + await exchange.create_order('BTC/USD:USDC', 'limit', 'buy', 1, 20000) + except Exception as e: + req_headers = exchange.last_request_headers + assert req_headers['PARADEX-PARTNER'] == id, 'paradex - id: ' + id + ' not in headers' + if not is_sync(): + await close(exchange) + return True + + async def test_hashkey(self): + exchange = self.init_offline_exchange('hashkey') + req_headers = None + id = '10000700011' + try: + await exchange.create_order('BTC/USDT', 'limit', 'buy', 1, 20000) + except Exception as e: + # we expect an error here, we're only interested in the headers + req_headers = exchange.last_request_headers + assert req_headers['INPUT-SOURCE'] == id, 'hashkey - id: ' + id + ' not in headers.' + if not is_sync(): + await close(exchange) + return True + + async def test_coincatch(self): + exchange = self.init_offline_exchange('coincatch') + req_headers = None + id = '47cfy' + try: + await exchange.create_order('BTC/USDT', 'limit', 'buy', 1, 20000) + except Exception as e: + # we expect an error here, we're only interested in the headers + req_headers = exchange.last_request_headers + assert req_headers['X-CHANNEL-API-CODE'] == id, 'coincatch - id: ' + id + ' not in headers.' + if not is_sync(): + await close(exchange) + return True + + async def test_defx(self): + exchange = self.init_offline_exchange('defx') + req_headers = None + try: + await exchange.create_order('DOGE/USDC:USDC', 'limit', 'buy', 100, 1) + except Exception as e: + # we expect an error here, we're only interested in the headers + req_headers = exchange.last_request_headers + id = 'ccxt' + assert req_headers['X-DEFX-SOURCE'] == id, 'defx - id: ' + id + ' not in headers.' + if not is_sync(): + await close(exchange) + return True + + async def test_cryptomus(self): + exchange = self.init_offline_exchange('cryptomus') + request = None + try: + await exchange.create_order('BTC/USDT', 'limit', 'sell', 1, 20000) + except Exception as e: + request = json_parse(exchange.last_request_body) + tag = 'ccxt' + assert request['tag'] == tag, 'cryptomus - tag: ' + tag + ' not in request.' + if not is_sync(): + await close(exchange) + return True + + async def test_derive(self): + exchange = self.init_offline_exchange('derive') + id = '0x0ad42b8e602c2d3d475ae52d678cf63d84ab2749' + assert exchange.options['id'] == id, 'derive - id: ' + id + ' not in options' + request = None + try: + params = { + 'subaccount_id': 1234, + 'max_fee': 10, + 'deriveWalletAddress': '0x0ad42b8e602c2d3d475ae52d678cf63d84ab2749', + } + exchange.walletAddress = '0x0ad42b8e602c2d3d475ae52d678cf63d84ab2749' + exchange.privateKey = '0x7b77bb7b20e92bbb85f2a22b330b896959229a5790e35f2f290922de3fb22ad5' + await exchange.create_order('LBTC/USDC', 'limit', 'sell', 0.01, 3000, params) + except Exception as e: + request = json_parse(exchange.last_request_body) + assert request['referral_code'] == id, 'derive - referral_code: ' + id + ' not in request.' + if not is_sync(): + await close(exchange) + return True + + async def test_mode_trade(self): + exchange = self.init_offline_exchange('modetrade') + exchange.secret = 'secretsecretsecretsecretsecretsecretsecrets' + id = 'CCXTMODE' + await exchange.load_markets() + request = None + try: + await exchange.create_order('BTC/USDC:USDC', 'limit', 'buy', 1, 20000) + except Exception as e: + request = json_parse(exchange.last_request_body) + broker_id = request['order_tag'] + assert broker_id == id, 'modetrade - id: ' + id + ' different from broker_id: ' + broker_id + if not is_sync(): + await close(exchange) + return True + + async def test_backpack(self): + exchange = self.init_offline_exchange('backpack') + exchange.apiKey = 'Jcj3vxDMAIrx0G5YYfydzS/le/owoQ+VSS164zC1RXo=' + exchange.secret = 'sRkC124Iazob0QYvaFj9dm63MXEVY48lDNt+/GVDVAU=' + req_headers = None + id = '1400' + try: + await exchange.create_order('ETH/USDC', 'limit', 'buy', 1, 5000) + except Exception as e: + # we expect an error here, we're only interested in the headers + req_headers = exchange.last_request_headers + assert req_headers['X-Broker-Id'] == id, 'backpack - id: ' + id + ' not in headers.' + if not is_sync(): + await close(exchange) + return True diff --git a/ccxt/test/tests_helpers.py b/ccxt/test/tests_helpers.py new file mode 100644 index 0000000..586b371 --- /dev/null +++ b/ccxt/test/tests_helpers.py @@ -0,0 +1,285 @@ +# -*- coding: utf-8 -*- + +import argparse +import json +# import logging +import os +import sys +from traceback import format_tb, format_exception + +import importlib # noqa: E402 +import re + +# ------------------------------------------------------------------------------ +# logging.basicConfig(level=logging.INFO) +# ------------------------------------------------------------------------------ +DIR_NAME = os.path.dirname(os.path.abspath(__file__)) +root = os.path.dirname(os.path.dirname(DIR_NAME)) +sys.path.append(root) + +import ccxt.async_support as ccxt # noqa: E402 +import ccxt as ccxt_sync # noqa: E402 +import ccxt.pro as ccxtpro # noqa: E402 + +# ------------------------------------------------------------------------------ +# from typing import Optional +# from typing import List +from ccxt.base.errors import NotSupported # noqa: F401 +from ccxt.base.errors import InvalidProxySettings # noqa: F401 +from ccxt.base.errors import OperationFailed # noqa: F401 +# from ccxt.base.errors import ExchangeError +from ccxt.base.errors import ExchangeNotAvailable # noqa: F401 +from ccxt.base.errors import OnMaintenance # noqa: F401 +from ccxt.base.errors import AuthenticationError # noqa: F401 + +# ------------------------------------------------------------------------------ + +class Argv(object): + id_tests = False + static_tests = False + ws_tests = False + request_tests = False + response_tests = False + sandbox = False + privateOnly = False + private = False + ws = False + verbose = False + nonce = None + exchange = None + symbol = None + info = False + sync = False + baseTests = False + exchangeTests = False + pass + + +argv = Argv() +parser = argparse.ArgumentParser() +parser.add_argument('--sandbox', action='store_true', help='enable sandbox mode') +parser.add_argument('--privateOnly', action='store_true', help='run private tests only') +parser.add_argument('--private', action='store_true', help='run private tests') +parser.add_argument('--verbose', action='store_true', help='enable verbose output') +parser.add_argument('--ws', action='store_true', help='websockets version') +parser.add_argument('--info', action='store_true', help='enable info output') +parser.add_argument('--static', action='store_true', help='run static tests') +parser.add_argument('--useProxy', action='store_true', help='run static tests') +parser.add_argument('--idTests', action='store_true', help='run brokerId tests') +parser.add_argument('--responseTests', action='store_true', help='run response tests') +parser.add_argument('--response', action='store_true', help='run response tests') +parser.add_argument('--requestTests', action='store_true', help='run request tests') +parser.add_argument('--request', action='store_true', help='run request tests') +parser.add_argument('--sync', action='store_true', help='is sync') +parser.add_argument('--baseTests', action='store_true', help='is base tests') +parser.add_argument('--exchangeTests', action='store_true', help='is exchange tests') +parser.add_argument('exchange', type=str, help='exchange id in lowercase', nargs='?') +parser.add_argument('symbol', type=str, help='symbol in uppercase', nargs='?') +parser.parse_args(namespace=argv) + +# ------------------------------------------------------------------------------ + +path = os.path.dirname(ccxt.__file__) +if 'site-packages' in os.path.dirname(ccxt.__file__): + raise Exception("You are running tests_async.py/test.py against a globally-installed version of the library! It was previously installed into your site-packages folder by pip or pip3. To ensure testing against the local folder uninstall it first with pip uninstall ccxt or pip3 uninstall ccxt") + +# ------------------------------------------------------------------------------ + +Error = Exception + +# # print an error string +# def dump_error(*args): +# string = ' '.join([str(arg) for arg in args]) +# print(string) +# sys.stderr.write(string + "\n") +# sys.stderr.flush() + + +def handle_all_unhandled_exceptions(type, value, traceback): + dump('[TEST_FAILURE]', (type), (value), '\n\n' + ('\n'.join(format_tb(traceback)))) + exit(1) # unrecoverable crash + + +sys.excepthook = handle_all_unhandled_exceptions +# ------------------------------------------------------------------------------ + +# non-transpiled part, but shared names among langs + +EXT = 'py' +LANG = 'PY' +IS_SYNCHRONOUS = argv.sync # 'async' not in os.path.basename(__file__) +PROXY_TEST_FILE_NAME = 'proxies' +ROOT_DIR = DIR_NAME + '/../../../' +ENV_VARS = os.environ +NEW_LINE = '\n' +LOG_CHARS_LENGTH = 10000 + + + +def get_cli_arg_value(arg): + arg_exists = getattr(argv, arg) if hasattr(argv, arg) else False + with_hyphen = '--' + arg + arg_exists_with_hyphen = getattr(argv, with_hyphen) if hasattr(argv, with_hyphen) else False + without_hyphen = arg.replace('--', '') + arg_exists_wo_hyphen = getattr(argv, without_hyphen) if hasattr(argv, without_hyphen) else False + return arg_exists or arg_exists_with_hyphen or arg_exists_wo_hyphen + +isWsTests = get_cli_arg_value('--ws') + +def dump(*args): + print(' '.join([str(arg) for arg in args])) + + +def convert_ascii(str): + return str # stub + +def json_parse(elem): + return json.loads(elem) + + +def json_stringify(elem): + return json.dumps(elem) + + +def convert_to_snake_case(content): + res = re.sub(r'(? LOG_CHARS_LENGTH: + # Accessing out of range element causes error + message = message[0:LOG_CHARS_LENGTH] + return message + +# stub for c# +def get_root_exception(exc): + return exc + +def exit_script(code=0): + exit(code) + + +def get_exchange_prop(exchange, prop, defaultValue=None): + if hasattr(exchange, prop): + res = getattr(exchange, prop) + if res is not None and res != '': + return res + return defaultValue + + +def set_exchange_prop(exchange, prop, value): + setattr(exchange, prop, value) + # set snake case too + setattr(exchange, convert_to_snake_case(prop), value) + + +def init_exchange(exchangeId, args, is_ws=False): + if IS_SYNCHRONOUS: + return getattr(ccxt_sync, exchangeId)(args) + if (is_ws): + return getattr(ccxtpro, exchangeId)(args) + return getattr(ccxt, exchangeId)(args) + + +def get_test_files_sync(properties, ws=False): + tests = {} + finalPropList = properties + [PROXY_TEST_FILE_NAME, 'features'] + for i in range(0, len(finalPropList)): + methodName = finalPropList[i] + name_snake_case = convert_to_snake_case(methodName) + prefix = 'async' if not IS_SYNCHRONOUS else 'sync' + dir_to_test = DIR_NAME + '/exchange/' + prefix + '/' + module_string = 'ccxt.test.exchange.' + prefix + '.test_' + name_snake_case + if (ws): + prefix = 'pro' + dir_to_test = DIR_NAME + '/../' + prefix + '/test/Exchange/' + module_string = 'ccxt.pro.test.Exchange.test_' + name_snake_case + filePathWithExt = dir_to_test + 'test_' + name_snake_case + '.py' + if (io_file_exists (filePathWithExt)): + imp = importlib.import_module(module_string) + tests[methodName] = imp # getattr(imp, finalName) + return tests + +async def get_test_files(properties, ws=False): + return get_test_files_sync(properties, ws) + +async def close(exchange): + if (not IS_SYNCHRONOUS and hasattr(exchange, 'close')): + await exchange.close() + +def is_null_value(value): + return value is None + +def set_fetch_response(exchange: ccxt.Exchange, data): + if (IS_SYNCHRONOUS): + def fetch(url, method='GET', headers=None, body=None): + return data + exchange.fetch = fetch + return exchange + async def fetch(url, method='GET', headers=None, body=None): + return data + exchange.fetch = fetch + return exchange + +def get_lang(): + return LANG + +def get_ext(): + return EXT + +def get_root_dir(): + return ROOT_DIR + +def get_env_vars(): + return ENV_VARS + +def is_sync(): + return IS_SYNCHRONOUS + +argvExchange = argv.exchange +argvSymbol = argv.symbol if argv.symbol and '/' in argv.symbol else None +# in python, we check it through "symbol" arg (as opposed to JS/PHP) because argvs were already built above +argvMethod = argv.symbol if argv.symbol and '()' in argv.symbol else None diff --git a/ccxt/test/tests_init.py b/ccxt/test/tests_init.py new file mode 100644 index 0000000..2c13e72 --- /dev/null +++ b/ccxt/test/tests_init.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- + +from tests_helpers import get_cli_arg_value, IS_SYNCHRONOUS, argvExchange, argvSymbol, argvMethod + +try: + import asyncio +except ImportError: + asyncio = None + +from base.tests_init import base_tests_init # noqa: F401 +from ccxt.pro.test.base.tests_init import test_base_init_ws # noqa: F401 + +# fix : https://github.com/aio-libs/aiodns/issues/86 +import sys +if sys.platform == 'win32': + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) + +# ########### args ########### +isWs = get_cli_arg_value('--ws') +isBaseTests = get_cli_arg_value('--baseTests') +runAll = get_cli_arg_value('--all') + +# ###### base tests ####### +if (isBaseTests): + if (isWs): + test_base_init_ws() + print('base WS tests passed!') + else: + base_tests_init() + print('base REST tests passed!') + if not runAll: + exit(0) + +# ###### exchange tests ####### +if (IS_SYNCHRONOUS): + from tests_sync import testMainClass as testMainClassSync + testMainClassSync().init(argvExchange, argvSymbol, argvMethod) +else: + from tests_async import testMainClass as testMainClassAsync + asyncio.run(testMainClassAsync().init(argvExchange, argvSymbol, argvMethod)) diff --git a/ccxt/test/tests_sync.py b/ccxt/test/tests_sync.py new file mode 100644 index 0000000..3cb6c8c --- /dev/null +++ b/ccxt/test/tests_sync.py @@ -0,0 +1,1761 @@ +# -*- coding: utf-8 -*- + +from tests_helpers import AuthenticationError, NotSupported, InvalidProxySettings, ExchangeNotAvailable, OperationFailed, OnMaintenance, get_cli_arg_value, get_root_dir, is_sync, dump, json_parse, json_stringify, convert_ascii, io_file_exists, io_file_read, io_dir_read, call_method, call_method_sync, call_exchange_method_dynamically, call_exchange_method_dynamically_sync, get_root_exception, exception_message, exit_script, get_exchange_prop, set_exchange_prop, init_exchange, get_test_files_sync, get_test_files, set_fetch_response, is_null_value, close, get_env_vars, get_lang, get_ext # noqa: F401 + +class testMainClass: + id_tests = False + request_tests_failed = False + response_tests_failed = False + request_tests = False + ws_tests = False + response_tests = False + info = False + verbose = False + debug = False + private_test = False + private_test_only = False + load_keys = False + sandbox = False + only_specific_tests = [] + skipped_settings_for_exchange = {} + skipped_methods = {} + checked_public_tests = {} + test_files = {} + public_tests = {} + ext = '' + lang = '' + proxy_test_file_name = 'proxies' + + def parse_cli_args_and_props(self): + self.response_tests = get_cli_arg_value('--responseTests') or get_cli_arg_value('--response') + self.id_tests = get_cli_arg_value('--idTests') + self.request_tests = get_cli_arg_value('--requestTests') or get_cli_arg_value('--request') + self.info = get_cli_arg_value('--info') + self.verbose = get_cli_arg_value('--verbose') + self.debug = get_cli_arg_value('--debug') + self.private_test = get_cli_arg_value('--private') + self.private_test_only = get_cli_arg_value('--privateOnly') + self.sandbox = get_cli_arg_value('--sandbox') + self.load_keys = get_cli_arg_value('--loadKeys') + self.ws_tests = get_cli_arg_value('--ws') + self.lang = get_lang() + self.ext = get_ext() + + def init(self, exchange_id, symbol_argv, method_argv): + try: + self.init_inner(exchange_id, symbol_argv, method_argv) + except Exception as e: + dump('[TEST_FAILURE]') # tell run-tests.js this is failure + raise e + + def init_inner(self, exchange_id, symbol_argv, method_argv): + self.parse_cli_args_and_props() + if self.request_tests and self.response_tests: + self.run_static_request_tests(exchange_id, symbol_argv) + self.run_static_response_tests(exchange_id, symbol_argv) + return True + if self.response_tests: + self.run_static_response_tests(exchange_id, symbol_argv) + return True + if self.request_tests: + self.run_static_request_tests(exchange_id, symbol_argv) # symbol here is the testname + return True + if self.id_tests: + self.run_broker_id_tests() + return True + new_line = '\n' + dump(new_line + '' + new_line + '' + '[INFO] TESTING ', self.ext, { + 'exchange': exchange_id, + 'symbol': symbol_argv, + 'method': method_argv, + 'isWs': self.ws_tests, + 'useProxy': get_cli_arg_value('--useProxy'), + }, new_line) + exchange_args = { + 'verbose': self.verbose, + 'debug': self.debug, + 'enableRateLimit': True, + 'timeout': 30000, + } + exchange = init_exchange(exchange_id, exchange_args, self.ws_tests) + if exchange.alias: + dump(self.add_padding('[INFO] skipping alias', 25)) + exit_script(0) + self.import_files(exchange) + assert len(list(self.test_files.keys())) > 0, 'Test files were not loaded' # ensure test files are found & filled + self.expand_settings(exchange) + self.check_if_specific_test_is_chosen(method_argv) + self.start_test(exchange, symbol_argv) + exit_script(0) # needed to be explicitly finished for WS tests + + def check_if_specific_test_is_chosen(self, method_argv): + if method_argv is not None: + test_file_names = list(self.test_files.keys()) + possible_method_names = method_argv.split(',') # i.e. `test.ts binance fetchBalance,fetchDeposits` + if len(possible_method_names) >= 1: + for i in range(0, len(test_file_names)): + test_file_name = test_file_names[i] + for j in range(0, len(possible_method_names)): + method_name = possible_method_names[j] + method_name = method_name.replace('()', '') + if test_file_name == method_name: + self.only_specific_tests.append(test_file_name) + + def import_files(self, exchange): + properties = list(exchange.has.keys()) + properties.append('loadMarkets') + if is_sync(): + self.test_files = get_test_files_sync(properties, self.ws_tests) + else: + self.test_files = get_test_files(properties, self.ws_tests) + return True + + def load_credentials_from_env(self, exchange): + exchange_id = exchange.id + req_creds = get_exchange_prop(exchange, 're' + 'quiredCredentials') # dont glue the r-e-q-u-i-r-e phrase, because leads to messed up transpilation + objkeys = list(req_creds.keys()) + for i in range(0, len(objkeys)): + credential = objkeys[i] + is_required = req_creds[credential] + if is_required and get_exchange_prop(exchange, credential) is None: + full_key = exchange_id + '_' + credential + credential_env_name = full_key.upper() # example: KRAKEN_APIKEY + env_vars = get_env_vars() + credential_value = env_vars[credential_env_name] if (credential_env_name in env_vars) else None + if credential_value: + set_exchange_prop(exchange, credential, credential_value) + + def expand_settings(self, exchange): + exchange_id = exchange.id + keys_global = get_root_dir() + 'keys.json' + keys_local = get_root_dir() + 'keys.local.json' + keys_global_exists = io_file_exists(keys_global) + keys_local_exists = io_file_exists(keys_local) + global_settings = {} + if keys_global_exists: + global_settings = io_file_read(keys_global) + local_settings = {} + if keys_local_exists: + local_settings = io_file_read(keys_local) + all_settings = exchange.deep_extend(global_settings, local_settings) + exchange_settings = exchange.safe_value(all_settings, exchange_id, {}) + if exchange_settings: + setting_keys = list(exchange_settings.keys()) + for i in range(0, len(setting_keys)): + key = setting_keys[i] + if exchange_settings[key]: + final_value = None + if isinstance(exchange_settings[key], dict): + existing = get_exchange_prop(exchange, key, {}) + final_value = exchange.deep_extend(existing, exchange_settings[key]) + else: + final_value = exchange_settings[key] + set_exchange_prop(exchange, key, final_value) + # credentials + if self.load_keys: + self.load_credentials_from_env(exchange) + # skipped tests + skipped_file = get_root_dir() + 'skip-tests.json' + skipped_settings = io_file_read(skipped_file) + self.skipped_settings_for_exchange = exchange.safe_value(skipped_settings, exchange_id, {}) + skipped_settings_for_exchange = self.skipped_settings_for_exchange + # others + timeout = exchange.safe_value(skipped_settings_for_exchange, 'timeout') + if timeout is not None: + exchange.timeout = exchange.parse_to_int(timeout) + if get_cli_arg_value('--useProxy'): + exchange.http_proxy = exchange.safe_string(skipped_settings_for_exchange, 'httpProxy') + exchange.https_proxy = exchange.safe_string(skipped_settings_for_exchange, 'httpsProxy') + exchange.ws_proxy = exchange.safe_string(skipped_settings_for_exchange, 'wsProxy') + exchange.wss_proxy = exchange.safe_string(skipped_settings_for_exchange, 'wssProxy') + self.skipped_methods = exchange.safe_value(skipped_settings_for_exchange, 'skipMethods', {}) + self.checked_public_tests = {} + + def add_padding(self, message, size): + # has to be transpilable + res = '' + message_length = len(message) # avoid php transpilation issue + missing_space = size - message_length - 0 # - 0 is added just to trick transpile to treat the .length as a string for php + if missing_space > 0: + for i in range(0, missing_space): + res += ' ' + return message + res + + def test_method(self, method_name, exchange, args, is_public): + # todo: temporary skip for c# + if 'OrderBook' in method_name and self.ext == 'cs': + exchange.options['checksum'] = False + # todo: temporary skip for php + if 'OrderBook' in method_name and self.ext == 'php': + return True + skipped_properties_for_method = self.get_skips(exchange, method_name) + is_load_markets = (method_name == 'loadMarkets') + is_fetch_currencies = (method_name == 'fetchCurrencies') + is_proxy_test = (method_name == self.proxy_test_file_name) + is_feature_test = (method_name == 'features') + # if this is a private test, and the implementation was already tested in public, then no need to re-test it in private test (exception is fetchCurrencies, because our approach in base exchange) + if not is_public and (method_name in self.checked_public_tests) and not is_fetch_currencies: + return True + skip_message = None + supported_by_exchange = (method_name in exchange.has) and exchange.has[method_name] + if not is_load_markets and (len(self.only_specific_tests) > 0 and not exchange.in_array(method_name, self.only_specific_tests)): + skip_message = '[INFO] IGNORED_TEST' + elif not is_load_markets and not supported_by_exchange and not is_proxy_test and not is_feature_test: + skip_message = '[INFO] UNSUPPORTED_TEST' # keep it aligned with the longest message + elif isinstance(skipped_properties_for_method, str): + skip_message = '[INFO] SKIPPED_TEST' + elif not (method_name in self.test_files): + skip_message = '[INFO] UNIMPLEMENTED_TEST' + # exceptionally for `loadMarkets` call, we call it before it's even checked for "skip" as we need it to be called anyway (but can skip "test.loadMarket" for it) + if is_load_markets: + exchange.load_markets(True) + name = exchange.id + if skip_message: + if self.info: + dump(self.add_padding(skip_message, 25), name, method_name) + return True + if self.info: + args_stringified = '(' + exchange.json(args) + ')' # args.join() breaks when we provide a list of symbols or multidimensional array; "args.toString()" breaks bcz of "array to string conversion" + dump(self.add_padding('[INFO] TESTING', 25), name, method_name, args_stringified) + if is_sync(): + call_method_sync(self.test_files, method_name, exchange, skipped_properties_for_method, args) + else: + call_method(self.test_files, method_name, exchange, skipped_properties_for_method, args) + if self.info: + dump(self.add_padding('[INFO] TESTING DONE', 25), name, method_name) + # add to the list of successed tests + if is_public: + self.checked_public_tests[method_name] = True + return True + + def get_skips(self, exchange, method_name): + final_skips = {} + # check the exact method (i.e. `fetchTrades`) and language-specific (i.e. `fetchTrades.php`) + method_names = [method_name, method_name + '.' + self.ext] + for i in range(0, len(method_names)): + m_name = method_names[i] + if m_name in self.skipped_methods: + # if whole method is skipped, by assigning a string to it, i.e. "fetchOrders":"blabla" + if isinstance(self.skipped_methods[m_name], str): + return self.skipped_methods[m_name] + else: + final_skips = exchange.deep_extend(final_skips, self.skipped_methods[m_name]) + # get "object-specific" skips + object_skips = { + 'orderBook': ['fetchOrderBook', 'fetchOrderBooks', 'fetchL2OrderBook', 'watchOrderBook', 'watchOrderBookForSymbols'], + 'ticker': ['fetchTicker', 'fetchTickers', 'watchTicker', 'watchTickers'], + 'trade': ['fetchTrades', 'watchTrades', 'watchTradesForSymbols'], + 'ohlcv': ['fetchOHLCV', 'watchOHLCV', 'watchOHLCVForSymbols'], + 'ledger': ['fetchLedger', 'fetchLedgerEntry'], + 'depositWithdraw': ['fetchDepositsWithdrawals', 'fetchDeposits', 'fetchWithdrawals'], + 'depositWithdrawFee': ['fetchDepositWithdrawFee', 'fetchDepositWithdrawFees'], + } + object_names = list(object_skips.keys()) + for i in range(0, len(object_names)): + object_name = object_names[i] + object_methods = object_skips[object_name] + if exchange.in_array(method_name, object_methods): + # if whole object is skipped, by assigning a string to it, i.e. "orderBook":"blabla" + if (object_name in self.skipped_methods) and (isinstance(self.skipped_methods[object_name], str)): + return self.skipped_methods[object_name] + extra_skips = exchange.safe_dict(self.skipped_methods, object_name, {}) + final_skips = exchange.deep_extend(final_skips, extra_skips) + # extend related skips + # - if 'timestamp' is skipped, we should do so for 'datetime' too + # - if 'bid' is skipped, skip 'ask' too + if ('timestamp' in final_skips) and not ('datetime' in final_skips): + final_skips['datetime'] = final_skips['timestamp'] + if ('bid' in final_skips) and not ('ask' in final_skips): + final_skips['ask'] = final_skips['bid'] + if ('baseVolume' in final_skips) and not ('quoteVolume' in final_skips): + final_skips['quoteVolume'] = final_skips['baseVolume'] + return final_skips + + def test_safe(self, method_name, exchange, args=[], is_public=False): + # `testSafe` method does not throw an exception, instead mutes it. The reason we + # mute the thrown exceptions here is because we don't want to stop the whole + # tests queue if any single test-method fails. Instead, they are echoed with + # formatted message "[TEST_FAILURE] ..." and that output is then regex-matched by + # run-tests.js, so the exceptions are still printed out to console from there. + max_retries = 3 + args_stringified = exchange.json(args) # args.join() breaks when we provide a list of symbols or multidimensional array; "args.toString()" breaks bcz of "array to string conversion" + for i in range(0, max_retries): + try: + self.test_method(method_name, exchange, args, is_public) + return True + except Exception as ex: + e = get_root_exception(ex) + is_load_markets = (method_name == 'loadMarkets') + is_auth_error = (isinstance(e, AuthenticationError)) + is_not_supported = (isinstance(e, NotSupported)) + is_operation_failed = (isinstance(e, OperationFailed)) # includes "DDoSProtection", "RateLimitExceeded", "RequestTimeout", "ExchangeNotAvailable", "OperationFailed", "InvalidNonce", ... + if is_operation_failed: + # if last retry was gone with same `tempFailure` error, then let's eventually return false + if i == max_retries - 1: + is_on_maintenance = (isinstance(e, OnMaintenance)) + is_exchange_not_available = (isinstance(e, ExchangeNotAvailable)) + should_fail = None + ret_success = None + if is_load_markets: + # if "loadMarkets" does not succeed, we must return "false" to caller method, to stop tests continual + ret_success = False + # we might not break exchange tests, if exchange is on maintenance at this moment + if is_on_maintenance: + should_fail = False + else: + should_fail = True + else: + # for any other method tests: + if is_exchange_not_available and not is_on_maintenance: + # break exchange tests if "ExchangeNotAvailable" exception is thrown, but it's not maintenance + should_fail = True + ret_success = False + else: + # in all other cases of OperationFailed, show Warning, but don't mark test as failed + should_fail = False + ret_success = True + # output the message + fail_type = '[TEST_FAILURE]' if should_fail else '[TEST_WARNING]' + dump(fail_type, 'Method could not be tested due to a repeated Network/Availability issues', ' | ', exchange.id, method_name, args_stringified, exception_message(e)) + return ret_success + else: + # wait and retry again + # (increase wait time on every retry) + exchange.sleep((i + 1) * 1000) + else: + # if it's loadMarkets, then fail test, because it's mandatory for tests + if is_load_markets: + dump('[TEST_FAILURE]', 'Exchange can not load markets', exception_message(e), exchange.id, method_name, args_stringified) + return False + # if the specific arguments to the test method throws "NotSupported" exception + # then let's don't fail the test + if is_not_supported: + if self.info: + dump('[INFO] NOT_SUPPORTED', exception_message(e), exchange.id, method_name, args_stringified) + return True + # If public test faces authentication error, we don't break (see comments under `testSafe` method) + if is_public and is_auth_error: + if self.info: + # todo - turn into warning + dump('[INFO]', 'Authentication problem for public method', exception_message(e), exchange.id, method_name, args_stringified) + return True + else: + dump('[TEST_FAILURE]', exception_message(e), exchange.id, method_name, args_stringified) + return False + return True + + def run_public_tests(self, exchange, symbol): + tests = { + 'features': [], + 'fetchCurrencies': [], + 'fetchTicker': [symbol], + 'fetchTickers': [symbol], + 'fetchLastPrices': [symbol], + 'fetchOHLCV': [symbol], + 'fetchTrades': [symbol], + 'fetchOrderBook': [symbol], + 'fetchOrderBooks': [], + 'fetchBidsAsks': [], + 'fetchStatus': [], + 'fetchTime': [], + } + if self.ws_tests: + tests = { + 'watchOHLCV': [symbol], + 'watchOHLCVForSymbols': [symbol], + 'watchTicker': [symbol], + 'watchTickers': [symbol], + 'watchBidsAsks': [symbol], + 'watchOrderBook': [symbol], + 'watchOrderBookForSymbols': [[symbol]], + 'watchTrades': [symbol], + 'watchTradesForSymbols': [[symbol]], + } + market = exchange.market(symbol) + is_spot = market['spot'] + if not self.ws_tests: + if is_spot: + tests['fetchCurrencies'] = [] + else: + tests['fetchFundingRates'] = [symbol] + tests['fetchFundingRate'] = [symbol] + tests['fetchFundingRateHistory'] = [symbol] + tests['fetchIndexOHLCV'] = [symbol] + tests['fetchMarkOHLCV'] = [symbol] + tests['fetchPremiumIndexOHLCV'] = [symbol] + self.public_tests = tests + self.run_tests(exchange, tests, True) + return True + + def run_tests(self, exchange, tests, is_public_test): + test_names = list(tests.keys()) + promises = [] + for i in range(0, len(test_names)): + test_name = test_names[i] + test_args = tests[test_name] + promises.append(self.test_safe(test_name, exchange, test_args, is_public_test)) + # todo - not yet ready in other langs too + # promises.push (testThrottle ()); + results = (promises) + # now count which test-methods retuned `false` from "testSafe" and dump that info below + failed_methods = [] + for i in range(0, len(test_names)): + test_name = test_names[i] + test_returned_value = results[i] + if not test_returned_value: + failed_methods.append(test_name) + test_prefix_string = 'PUBLIC_TESTS' if is_public_test else 'PRIVATE_TESTS' + if len(failed_methods): + errors_string = ', '.join(failed_methods) + dump('[TEST_FAILURE]', exchange.id, test_prefix_string, 'Failed methods : ' + errors_string) + if self.info: + dump(self.add_padding('[INFO] END ' + test_prefix_string + ' ' + exchange.id, 25)) + return True + + def load_exchange(self, exchange): + result = self.test_safe('loadMarkets', exchange, [], True) + if not result: + return False + exchange_symbols_length = len(exchange.symbols) + dump('[INFO:MAIN] Exchange loaded', exchange_symbols_length, 'symbols') + return True + + def get_test_symbol(self, exchange, is_spot, symbols): + symbol = None + preferred_spot_symbol = exchange.safe_string(self.skipped_settings_for_exchange, 'preferredSpotSymbol') + preferred_swap_symbol = exchange.safe_string(self.skipped_settings_for_exchange, 'preferredSwapSymbol') + if is_spot and preferred_spot_symbol: + return preferred_spot_symbol + elif not is_spot and preferred_swap_symbol: + return preferred_swap_symbol + for i in range(0, len(symbols)): + s = symbols[i] + market = exchange.safe_value(exchange.markets, s) + if market is not None: + active = exchange.safe_value(market, 'active') + if active or (active is None): + symbol = s + break + return symbol + + def get_exchange_code(self, exchange, codes=None): + if codes is None: + codes = ['BTC', 'ETH', 'XRP', 'LTC', 'BCH', 'EOS', 'BNB', 'BSV', 'USDT'] + code = codes[0] + for i in range(0, len(codes)): + if codes[i] in exchange.currencies: + return codes[i] + return code + + def get_markets_from_exchange(self, exchange, spot=True): + res = {} + markets = exchange.markets + keys = list(markets.keys()) + for i in range(0, len(keys)): + key = keys[i] + market = markets[key] + if spot and market['spot']: + res[market['symbol']] = market + elif not spot and not market['spot']: + res[market['symbol']] = market + return res + + def get_valid_symbol(self, exchange, spot=True): + current_type_markets = self.get_markets_from_exchange(exchange, spot) + codes = ['BTC', 'ETH', 'XRP', 'LTC', 'BNB', 'DASH', 'DOGE', 'ETC', 'TRX', 'USDT', 'USDC', 'USD', 'GUSD', 'EUR', 'TUSD', 'CNY', 'JPY', 'BRL'] + spot_symbols = ['BTC/USDT', 'BTC/USDC', 'BTC/USD', 'BTC/CNY', 'BTC/EUR', 'BTC/AUD', 'BTC/BRL', 'BTC/JPY', 'ETH/USDT', 'ETH/USDC', 'ETH/USD', 'ETH/CNY', 'ETH/EUR', 'ETH/AUD', 'ETH/BRL', 'ETH/JPY', 'EUR/USDT', 'EUR/USD', 'EUR/USDC', 'USDT/EUR', 'USD/EUR', 'USDC/EUR', 'BTC/ETH', 'ETH/BTC'] + swap_symbols = ['BTC/USDT:USDT', 'BTC/USDC:USDC', 'BTC/USD:USD', 'ETH/USDT:USDT', 'ETH/USDC:USDC', 'ETH/USD:USD', 'BTC/USD:BTC', 'ETH/USD:ETH'] + target_symbols = spot_symbols if spot else swap_symbols + symbol = self.get_test_symbol(exchange, spot, target_symbols) + # if symbols wasn't found from above hardcoded list, then try to locate any symbol which has our target hardcoded 'base' code + if symbol is None: + for i in range(0, len(codes)): + current_code = codes[i] + markets_array_for_current_code = exchange.filter_by(current_type_markets, 'base', current_code) + indexed_mkts = exchange.index_by(markets_array_for_current_code, 'symbol') + symbols_array_for_current_code = list(indexed_mkts.keys()) + symbols_length = len(symbols_array_for_current_code) + if symbols_length: + symbol = self.get_test_symbol(exchange, spot, symbols_array_for_current_code) + break + # if there wasn't found any symbol with our hardcoded 'base' code, then just try to find symbols that are 'active' + if symbol is None: + active_markets = exchange.filter_by(current_type_markets, 'active', True) + active_symbols = [] + for i in range(0, len(active_markets)): + active_symbols.append(active_markets[i]['symbol']) + symbol = self.get_test_symbol(exchange, spot, active_symbols) + if symbol is None: + values = list(current_type_markets.values()) + values_length = len(values) + if values_length > 0: + first = values[0] + if first is not None: + symbol = first['symbol'] + return symbol + + def test_exchange(self, exchange, provided_symbol=None): + spot_symbol = None + swap_symbol = None + if provided_symbol is not None: + market = exchange.market(provided_symbol) + if market['spot']: + spot_symbol = provided_symbol + else: + swap_symbol = provided_symbol + else: + if exchange.has['spot']: + spot_symbol = self.get_valid_symbol(exchange, True) + if exchange.has['swap']: + swap_symbol = self.get_valid_symbol(exchange, False) + if spot_symbol is not None: + dump('[INFO:MAIN] Selected SPOT SYMBOL:', spot_symbol) + if swap_symbol is not None: + dump('[INFO:MAIN] Selected SWAP SYMBOL:', swap_symbol) + if not self.private_test_only: + # note, spot & swap tests should run sequentially, because of conflicting `exchange.options['defaultType']` setting + if exchange.has['spot'] and spot_symbol is not None: + if self.info: + dump('[INFO] ### SPOT TESTS ###') + exchange.options['defaultType'] = 'spot' + self.run_public_tests(exchange, spot_symbol) + if exchange.has['swap'] and swap_symbol is not None: + if self.info: + dump('[INFO] ### SWAP TESTS ###') + exchange.options['defaultType'] = 'swap' + self.run_public_tests(exchange, swap_symbol) + if self.private_test or self.private_test_only: + if exchange.has['spot'] and spot_symbol is not None: + exchange.options['defaultType'] = 'spot' + self.run_private_tests(exchange, spot_symbol) + if exchange.has['swap'] and swap_symbol is not None: + exchange.options['defaultType'] = 'swap' + self.run_private_tests(exchange, swap_symbol) + return True + + def run_private_tests(self, exchange, symbol): + if not exchange.check_required_credentials(False): + dump('[INFO] Skipping private tests', 'Keys not found') + return True + code = self.get_exchange_code(exchange) + # if (exchange.deepExtendedTest) { + # test ('InvalidNonce', exchange, symbol); + # test ('OrderNotFound', exchange, symbol); + # test ('InvalidOrder', exchange, symbol); + # test ('InsufficientFunds', exchange, symbol, balance); # danger zone - won't execute with non-empty balance + # } + tests = { + 'signIn': [], + 'fetchBalance': [], + 'fetchAccounts': [], + 'fetchTransactionFees': [], + 'fetchTradingFees': [], + 'fetchStatus': [], + 'fetchOrders': [symbol], + 'fetchOpenOrders': [symbol], + 'fetchClosedOrders': [symbol], + 'fetchMyTrades': [symbol], + 'fetchLeverageTiers': [[symbol]], + 'fetchLedger': [code], + 'fetchTransactions': [code], + 'fetchDeposits': [code], + 'fetchWithdrawals': [code], + 'fetchBorrowInterest': [code, symbol], + 'cancelAllOrders': [symbol], + 'fetchCanceledOrders': [symbol], + 'fetchMarginModes': [symbol], + 'fetchPosition': [symbol], + 'fetchDeposit': [code], + 'createDepositAddress': [code], + 'fetchDepositAddress': [code], + 'fetchDepositAddresses': [code], + 'fetchDepositAddressesByNetwork': [code], + 'fetchBorrowRateHistory': [code], + 'fetchLedgerEntry': [code], + } + if get_cli_arg_value('--fundedTests'): + tests['createOrder'] = [symbol] + if self.ws_tests: + tests = { + 'watchBalance': [code], + 'watchMyTrades': [symbol], + 'watchOrders': [symbol], + 'watchPosition': [symbol], + 'watchPositions': [symbol], + } + market = exchange.market(symbol) + is_spot = market['spot'] + if not self.ws_tests: + if is_spot: + tests['fetchCurrencies'] = [] + else: + # derivatives only + tests['fetchPositions'] = [symbol] # this test fetches all positions for 1 symbol + tests['fetchPosition'] = [symbol] + tests['fetchPositionRisk'] = [symbol] + tests['setPositionMode'] = [symbol] + tests['setMarginMode'] = [symbol] + tests['fetchOpenInterestHistory'] = [symbol] + tests['fetchFundingRateHistory'] = [symbol] + tests['fetchFundingHistory'] = [symbol] + # const combinedTests = exchange.deepExtend (this.publicTests, privateTests); + self.run_tests(exchange, tests, False) + + def test_proxies(self, exchange): + # these tests should be synchronously executed, because of conflicting nature of proxy settings + proxy_test_name = self.proxy_test_file_name + # todo: temporary skip for sync py + if self.ext == 'py' and is_sync(): + return True + # try proxy several times + max_retries = 3 + exception = None + for j in range(0, max_retries): + try: + self.test_method(proxy_test_name, exchange, [], True) + return True # if successfull, then end the test + except Exception as e: + exception = e + exchange.sleep(j * 1000) + # if exception was set, then throw it + if exception is not None: + error_message = '[TEST_FAILURE] Failed ' + proxy_test_name + ' : ' + exception_message(exception) + # temporary comment the below, because c# transpilation failure + # throw new Exchange Error (errorMessage.toString ()); + dump('[TEST_WARNING]' + error_message) + return True + + def check_constructor(self, exchange): + # todo: this might be moved in base tests later + if exchange.id == 'binance': + assert exchange.hostname is None or exchange.hostname == '', 'binance.com hostname should be empty' + assert exchange.urls['api']['public'] == 'https://api.binance.com/api/v3', 'https://api.binance.com/api/v3 does not match: ' + exchange.urls['api']['public'] + assert ('lending/union/account' in exchange.api['sapi']['get']), 'SAPI should contain the endpoint lending/union/account, ' + json_stringify(exchange.api['sapi']['get']) + elif exchange.id == 'binanceus': + assert exchange.hostname == 'binance.us', 'binance.us hostname does not match ' + exchange.hostname + assert exchange.urls['api']['public'] == 'https://api.binance.us/api/v3', 'https://api.binance.us/api/v3 does not match: ' + exchange.urls['api']['public'] + + def test_return_response_headers(self, exchange): + if exchange.id != 'binance': + return False # this test is only for binance exchange for now + exchange.return_response_headers = True + ticker = exchange.fetch_ticker('BTC/USDT') + info = ticker['info'] + headers = info['responseHeaders'] + headers_keys = list(headers.keys()) + assert len(headers_keys) > 0, 'Response headers should not be empty' + header_values = list(headers.values()) + assert len(header_values) > 0, 'Response headers values should not be empty' + exchange.return_response_headers = False + return True + + def start_test(self, exchange, symbol): + # we do not need to test aliases + if exchange.alias: + return True + self.check_constructor(exchange) + # this.testReturnResponseHeaders (exchange); + if self.sandbox or get_exchange_prop(exchange, 'sandbox'): + exchange.set_sandbox_mode(True) + try: + result = self.load_exchange(exchange) + if not result: + if not is_sync(): + close(exchange) + return True + # if (exchange.id === 'binance') { + # # we test proxies functionality just for one random exchange on each build, because proxy functionality is not exchange-specific, instead it's all done from base methods, so just one working sample would mean it works for all ccxt exchanges + # # this.testProxies (exchange); + # } + self.test_exchange(exchange, symbol) + if not is_sync(): + close(exchange) + except Exception as e: + if not is_sync(): + close(exchange) + raise e + + def assert_static_error(self, cond, message, calculated_output, stored_output, key=None): + # ----------------------------------------------------------------------------- + # --- Init of static tests functions------------------------------------------ + # ----------------------------------------------------------------------------- + calculated_string = json_stringify(calculated_output) + stored_string = json_stringify(stored_output) + error_message = message + if key is not None: + error_message = '[' + key + ']' + error_message += ' computed: ' + stored_string + ' stored: ' + calculated_string + assert cond, error_message + + def load_markets_from_file(self, id): + # load markets from file + # to make this test as fast as possible + # and basically independent from the exchange + # so we can run it offline + filename = get_root_dir() + './ts/src/test/static/markets/' + id + '.json' + content = io_file_read(filename) + return content + + def load_currencies_from_file(self, id): + filename = get_root_dir() + './ts/src/test/static/currencies/' + id + '.json' + content = io_file_read(filename) + return content + + def load_static_data(self, folder, target_exchange=None): + result = {} + if target_exchange: + # read a single exchange + path = folder + target_exchange + '.json' + if not io_file_exists(path): + dump('[WARN] tests not found: ' + path) + return None + result[target_exchange] = io_file_read(path) + return result + files = io_dir_read(folder) + for i in range(0, len(files)): + file = files[i] + exchange_name = file.replace('.json', '') + content = io_file_read(folder + file) + result[exchange_name] = content + return result + + def remove_hostnamefrom_url(self, url): + if url is None: + return None + url_parts = url.split('/') + res = '' + for i in range(0, len(url_parts)): + if i > 2: + current = url_parts[i] + if current.find('?') > -1: + # handle urls like this: /v1/account/accounts?AccessK + current_parts = current.split('?') + res += '/' + res += current_parts[0] + break + res += '/' + res += current + return res + + def urlencoded_to_dict(self, url): + result = {} + parts = url.split('&') + for i in range(0, len(parts)): + part = parts[i] + key_value = part.split('=') + keys_length = len(key_value) + if keys_length != 2: + continue + key = key_value[0] + value = key_value[1] + if (value is not None) and ((value.startswith('[')) or (value.startswith('{'))): + # some exchanges might return something like this: timestamp=1699382693405&batchOrders=[{\"symbol\":\"LTCUSDT\",\"side\":\"BUY\",\"newClientOrderI + value = json_parse(value) + result[key] = value + return result + + def assert_new_and_stored_output_inner(self, exchange, skip_keys, new_output, stored_output, strict_type_check=True, asserting_key=None): + if is_null_value(new_output) and is_null_value(stored_output): + return True + if not new_output and not stored_output: + return True + if (isinstance(stored_output, dict)) and (isinstance(new_output, dict)): + stored_output_keys = list(stored_output.keys()) + new_output_keys = list(new_output.keys()) + stored_keys_length = len(stored_output_keys) + new_keys_length = len(new_output_keys) + self.assert_static_error(stored_keys_length == new_keys_length, 'output length mismatch', stored_output, new_output) + # iterate over the keys + for i in range(0, len(stored_output_keys)): + key = stored_output_keys[i] + if exchange.in_array(key, skip_keys): + continue + if not (exchange.in_array(key, new_output_keys)): + self.assert_static_error(False, 'output key missing: ' + key, stored_output, new_output) + stored_value = stored_output[key] + new_value = new_output[key] + self.assert_new_and_stored_output(exchange, skip_keys, new_value, stored_value, strict_type_check, key) + elif isinstance(stored_output, list) and (isinstance(new_output, list)): + stored_array_length = len(stored_output) + new_array_length = len(new_output) + self.assert_static_error(stored_array_length == new_array_length, 'output length mismatch', stored_output, new_output) + for i in range(0, len(stored_output)): + stored_item = stored_output[i] + new_item = new_output[i] + self.assert_new_and_stored_output(exchange, skip_keys, new_item, stored_item, strict_type_check) + else: + # built-in types like strings, numbers, booleans + sanitized_new_output = None if (is_null_value(new_output)) else new_output # we store undefined as nulls in the json file so we need to convert it back + sanitized_stored_output = None if (is_null_value(stored_output)) else stored_output + new_output_string = str(sanitized_new_output) if sanitized_new_output else 'undefined' + stored_output_string = str(sanitized_stored_output) if sanitized_stored_output else 'undefined' + message_error = 'output value mismatch:' + new_output_string + ' != ' + stored_output_string + if strict_type_check and (self.lang != 'C#'): + # upon building the request we want strict type check to make sure all the types are correct + # when comparing the response we want to allow some flexibility, because a 50.0 can be equal to 50 after saving it to the json file + self.assert_static_error(sanitized_new_output == sanitized_stored_output, message_error, stored_output, new_output, asserting_key) + else: + is_computed_bool = (isinstance(sanitized_new_output, bool)) + is_stored_bool = (isinstance(sanitized_stored_output, bool)) + is_computed_string = (isinstance(sanitized_new_output, str)) + is_stored_string = (isinstance(sanitized_stored_output, str)) + is_computed_undefined = (sanitized_new_output is None) + is_stored_undefined = (sanitized_stored_output is None) + should_be_same = (is_computed_bool == is_stored_bool) and (is_computed_string == is_stored_string) and (is_computed_undefined == is_stored_undefined) + self.assert_static_error(should_be_same, 'output type mismatch', stored_output, new_output, asserting_key) + is_boolean = is_computed_bool or is_stored_bool + is_string = is_computed_string or is_stored_string + is_undefined = is_computed_undefined or is_stored_undefined # undefined is a perfetly valid value + if is_boolean or is_string or is_undefined: + if (self.lang == 'C#') or (self.lang == 'GO'): + # tmp c# number comparsion + is_number = False + try: + exchange.parse_to_numeric(sanitized_new_output) + is_number = True + except Exception as e: + # if we can't parse it to number, then it's not a number + is_number = False + if is_number: + self.assert_static_error(exchange.parse_to_numeric(sanitized_new_output) == exchange.parse_to_numeric(sanitized_stored_output), message_error, stored_output, new_output, asserting_key) + return True + else: + self.assert_static_error(convert_ascii(new_output_string) == convert_ascii(stored_output_string), message_error, stored_output, new_output, asserting_key) + return True + else: + self.assert_static_error(convert_ascii(new_output_string) == convert_ascii(stored_output_string), message_error, stored_output, new_output, asserting_key) + return True + else: + if self.lang == 'C#': + stringified_new_output = exchange.number_to_string(sanitized_new_output) + stringified_stored_output = exchange.number_to_string(sanitized_stored_output) + self.assert_static_error(str(stringified_new_output) == str(stringified_stored_output), message_error, stored_output, new_output, asserting_key) + else: + numeric_new_output = exchange.parse_to_numeric(new_output_string) + numeric_stored_output = exchange.parse_to_numeric(stored_output_string) + self.assert_static_error(numeric_new_output == numeric_stored_output, message_error, stored_output, new_output, asserting_key) + return True # c# requ + + def assert_new_and_stored_output(self, exchange, skip_keys, new_output, stored_output, strict_type_check=True, asserting_key=None): + res = True + try: + res = self.assert_new_and_stored_output_inner(exchange, skip_keys, new_output, stored_output, strict_type_check, asserting_key) + except Exception as e: + if self.info: + error_message = self.var_to_string(new_output) + '(calculated)' + ' != ' + self.var_to_string(stored_output) + '(stored)' + dump('[TEST_FAILURE_DETAIL]' + error_message) + raise e + return res + + def var_to_string(self, obj=None): + new_string = None + if obj is None: + new_string = 'undefined' + elif is_null_value(obj): + new_string = 'null' + else: + new_string = json_stringify(obj) + return new_string + + def assert_static_request_output(self, exchange, type, skip_keys, stored_url, request_url, stored_output, new_output): + if stored_url != request_url: + # remove the host part from the url + first_path = self.remove_hostnamefrom_url(stored_url) + second_path = self.remove_hostnamefrom_url(request_url) + self.assert_static_error(first_path == second_path, 'url mismatch', first_path, second_path) + # body (aka storedOutput and newOutput) is not defined and information is in the url + # example: "https://open-api.bingx.com/openApi/spot/v1/trade/order?quoteOrderQty=5&side=BUY&symbol=LTC-USDT×tamp=1698777135343&type=MARKET&signature=d55a7e4f7f9dbe56c4004c9f3ab340869d3cb004e2f0b5b861e5fbd1762fd9a0 + if (stored_output is None) and (new_output is None): + if (stored_url is not None) and (request_url is not None): + stored_url_parts = stored_url.split('?') + new_url_parts = request_url.split('?') + stored_url_query = exchange.safe_value(stored_url_parts, 1) + new_url_query = exchange.safe_value(new_url_parts, 1) + if (stored_url_query is None) and (new_url_query is None): + # might be a get request without any query parameters + # example: https://api.gateio.ws/api/v4/delivery/usdt/positions + return True + stored_url_params = self.urlencoded_to_dict(stored_url_query) + new_url_params = self.urlencoded_to_dict(new_url_query) + self.assert_new_and_stored_output(exchange, skip_keys, new_url_params, stored_url_params) + return True + if type == 'json' and (stored_output is not None) and (new_output is not None): + if isinstance(stored_output, str): + stored_output = json_parse(stored_output) + if isinstance(new_output, str): + new_output = json_parse(new_output) + elif type == 'urlencoded' and (stored_output is not None) and (new_output is not None): + stored_output = self.urlencoded_to_dict(stored_output) + new_output = self.urlencoded_to_dict(new_output) + elif type == 'both': + if stored_output.startswith('{') or stored_output.startswith('['): + stored_output = json_parse(stored_output) + new_output = json_parse(new_output) + else: + stored_output = self.urlencoded_to_dict(stored_output) + new_output = self.urlencoded_to_dict(new_output) + self.assert_new_and_stored_output(exchange, skip_keys, new_output, stored_output) + return True + + def assert_static_response_output(self, exchange, skip_keys, computed_result, stored_result): + self.assert_new_and_stored_output(exchange, skip_keys, computed_result, stored_result, False) + + def sanitize_data_input(self, input): + # remove nulls and replace with unefined instead + if input is None: + return None + new_input = [] + for i in range(0, len(input)): + current = input[i] + if is_null_value(current): + new_input.append(None) + else: + new_input.append(current) + return new_input + + def test_request_statically(self, exchange, method, data, type, skip_keys): + output = None + request_url = None + if self.info: + dump('[INFO] STATIC REQUEST TEST:', method, ':', data['description']) + try: + if not is_sync(): + call_exchange_method_dynamically(exchange, method, self.sanitize_data_input(data['input'])) + else: + call_exchange_method_dynamically_sync(exchange, method, self.sanitize_data_input(data['input'])) + except Exception as e: + if not (isinstance(e, InvalidProxySettings)): + raise e + output = exchange.last_request_body + request_url = exchange.last_request_url + try: + call_output = exchange.safe_value(data, 'output') + self.assert_static_request_output(exchange, type, skip_keys, data['url'], request_url, call_output, output) + except Exception as e: + self.request_tests_failed = True + error_message = '[' + self.lang + '][STATIC_REQUEST]' + '[' + exchange.id + ']' + '[' + method + ']' + '[' + data['description'] + ']' + exception_message(e) + dump('[TEST_FAILURE]' + error_message) + return True + + def test_response_statically(self, exchange, method, skip_keys, data): + expected_result = exchange.safe_value(data, 'parsedResponse') + mocked_exchange = set_fetch_response(exchange, data['httpResponse']) + if self.info: + dump('[INFO] STATIC RESPONSE TEST:', method, ':', data['description']) + try: + if not is_sync(): + unified_result = call_exchange_method_dynamically(exchange, method, self.sanitize_data_input(data['input'])) + self.assert_static_response_output(mocked_exchange, skip_keys, unified_result, expected_result) + else: + unified_result_sync = call_exchange_method_dynamically_sync(exchange, method, self.sanitize_data_input(data['input'])) + self.assert_static_response_output(mocked_exchange, skip_keys, unified_result_sync, expected_result) + except Exception as e: + self.response_tests_failed = True + error_message = '[' + self.lang + '][STATIC_RESPONSE]' + '[' + exchange.id + ']' + '[' + method + ']' + '[' + data['description'] + ']' + exception_message(e) + dump('[TEST_FAILURE]' + error_message) + set_fetch_response(exchange, None) # reset state + return True + + def init_offline_exchange(self, exchange_name): + markets = self.load_markets_from_file(exchange_name) + currencies = self.load_currencies_from_file(exchange_name) + # we add "proxy" 2 times to intentionally trigger InvalidProxySettings + exchange = init_exchange(exchange_name, { + 'markets': markets, + 'currencies': currencies, + 'enableRateLimit': False, + 'rateLimit': 1, + 'httpProxy': 'http://fake:8080', + 'httpsProxy': 'http://fake:8080', + 'apiKey': 'key', + 'secret': 'secretsecret', + 'password': 'password', + 'walletAddress': 'wallet', + 'privateKey': '0xff3bdd43534543d421f05aec535965b5050ad6ac15345435345435453495e771', + 'uid': 'uid', + 'token': 'token', + 'login': 'login', + 'accountId': '12345', + 'accounts': [{ + 'id': 'myAccount', + 'code': 'USDT', +}, { + 'id': 'myAccount', + 'code': 'USDC', +}], + 'options': { + 'enableUnifiedAccount': True, + 'enableUnifiedMargin': False, + 'accessToken': 'token', + 'expires': 999999999999999, + 'leverageBrackets': {}, + }, + }) + exchange.currencies = currencies + # not working in python if assigned in the config dict + return exchange + + def test_exchange_request_statically(self, exchange_name, exchange_data, test_name=None): + # instantiate the exchange and make sure that we sink the requests to avoid an actual request + exchange = self.init_offline_exchange(exchange_name) + global_options = exchange.safe_dict(exchange_data, 'options', {}) + # read apiKey/secret from the test file + api_key = exchange.safe_string(exchange_data, 'apiKey') + if api_key: + exchange.apiKey = str(api_key) + secret = exchange.safe_string(exchange_data, 'secret') + if secret: + exchange.secret = str(secret) + private_key = exchange.safe_string(exchange_data, 'privateKey') + if private_key: + exchange.privateKey = str(private_key) + wallet_address = exchange.safe_string(exchange_data, 'walletAddress') + if wallet_address: + exchange.walletAddress = str(wallet_address) + accounts = exchange.safe_list(exchange_data, 'accounts') + if accounts: + exchange.accounts = accounts + # exchange.options = exchange.deepExtend (exchange.options, globalOptions); # custom options to be used in the tests + exchange.extend_exchange_options(global_options) + methods = exchange.safe_value(exchange_data, 'methods', {}) + methods_names = list(methods.keys()) + for i in range(0, len(methods_names)): + method = methods_names[i] + results = methods[method] + for j in range(0, len(results)): + result = results[j] + old_exchange_options = exchange.options # snapshot options; + test_exchange_options = exchange.safe_value(result, 'options', {}) + # exchange.options = exchange.deepExtend (oldExchangeOptions, testExchangeOptions); # custom options to be used in the tests + exchange.extend_exchange_options(exchange.deep_extend(old_exchange_options, test_exchange_options)) + description = exchange.safe_value(result, 'description') + if (test_name is not None) and (test_name != description): + continue + is_disabled = exchange.safe_bool(result, 'disabled', False) + if is_disabled: + continue + disabled_string = exchange.safe_string(result, 'disabled', '') + if disabled_string != '': + continue + is_disabled_c_sharp = exchange.safe_bool(result, 'disabledCS', False) + if is_disabled_c_sharp and (self.lang == 'C#'): + continue + is_disabled_go = exchange.safe_bool(result, 'disabledGO', False) + if is_disabled_go and (self.lang == 'GO'): + continue + type = exchange.safe_string(exchange_data, 'outputType') + skip_keys = exchange.safe_value(exchange_data, 'skipKeys', []) + self.test_request_statically(exchange, method, result, type, skip_keys) + # reset options + exchange.options = exchange.convert_to_safe_dictionary(exchange.deep_extend(old_exchange_options, {})) + if not is_sync(): + close(exchange) + return True # in c# methods that will be used with promiseAll need to return something + + def test_exchange_response_statically(self, exchange_name, exchange_data, test_name=None): + exchange = self.init_offline_exchange(exchange_name) + # read apiKey/secret from the test file + api_key = exchange.safe_string(exchange_data, 'apiKey') + if api_key: + exchange.apiKey = str(api_key) + secret = exchange.safe_string(exchange_data, 'secret') + if secret: + exchange.secret = str(secret) + private_key = exchange.safe_string(exchange_data, 'privateKey') + if private_key: + exchange.privateKey = str(private_key) + wallet_address = exchange.safe_string(exchange_data, 'walletAddress') + if wallet_address: + exchange.walletAddress = str(wallet_address) + methods = exchange.safe_value(exchange_data, 'methods', {}) + options = exchange.safe_value(exchange_data, 'options', {}) + # exchange.options = exchange.deepExtend (exchange.options, options); # custom options to be used in the tests + exchange.extend_exchange_options(options) + methods_names = list(methods.keys()) + for i in range(0, len(methods_names)): + method = methods_names[i] + results = methods[method] + for j in range(0, len(results)): + result = results[j] + description = exchange.safe_value(result, 'description') + old_exchange_options = exchange.options # snapshot options; + test_exchange_options = exchange.safe_value(result, 'options', {}) + # exchange.options = exchange.deepExtend (oldExchangeOptions, testExchangeOptions); # custom options to be used in the tests + exchange.extend_exchange_options(exchange.deep_extend(old_exchange_options, test_exchange_options)) + is_disabled = exchange.safe_bool(result, 'disabled', False) + if is_disabled: + continue + is_disabled_c_sharp = exchange.safe_bool(result, 'disabledCS', False) + if is_disabled_c_sharp and (self.lang == 'C#'): + continue + is_disabled_php = exchange.safe_bool(result, 'disabledPHP', False) + if is_disabled_php and (self.lang == 'PHP'): + continue + if (test_name is not None) and (test_name != description): + continue + is_disabled_go = exchange.safe_bool(result, 'disabledGO', False) + if is_disabled_go and (self.lang == 'GO'): + continue + skip_keys = exchange.safe_value(exchange_data, 'skipKeys', []) + self.test_response_statically(exchange, method, skip_keys, result) + # reset options + # exchange.options = exchange.deepExtend (oldExchangeOptions, {}); + exchange.extend_exchange_options(exchange.deep_extend(old_exchange_options, {})) + if not is_sync(): + close(exchange) + return True # in c# methods that will be used with promiseAll need to return something + + def get_number_of_tests_from_exchange(self, exchange, exchange_data, test_name=None): + if test_name is not None: + return 1 + sum = 0 + methods = exchange_data['methods'] + methods_names = list(methods.keys()) + for i in range(0, len(methods_names)): + method = methods_names[i] + results = methods[method] + results_length = len(results) + sum = exchange.sum(sum, results_length) + return sum + + def check_if_exchange_is_disabled(self, exchange_name, exchange_data): + exchange = init_exchange('Exchange', {}) + is_disabled_py = exchange.safe_bool(exchange_data, 'disabledPy', False) + if is_disabled_py and (self.lang == 'PY'): + dump('[TEST_WARNING] Exchange ' + exchange_name + ' is disabled in python') + return True + is_disabled_php = exchange.safe_bool(exchange_data, 'disabledPHP', False) + if is_disabled_php and (self.lang == 'PHP'): + dump('[TEST_WARNING] Exchange ' + exchange_name + ' is disabled in php') + return True + is_disabled_c_sharp = exchange.safe_bool(exchange_data, 'disabledCS', False) + if is_disabled_c_sharp and (self.lang == 'C#'): + dump('[TEST_WARNING] Exchange ' + exchange_name + ' is disabled in c#') + return True + is_disabled_go = exchange.safe_bool(exchange_data, 'disabledGO', False) + if is_disabled_go and (self.lang == 'GO'): + dump('[TEST_WARNING] Exchange ' + exchange_name + ' is disabled in go') + return True + return False + + def run_static_request_tests(self, target_exchange=None, test_name=None): + self.run_static_tests('request', target_exchange, test_name) + return True + + def run_static_tests(self, type, target_exchange=None, test_name=None): + folder = get_root_dir() + './ts/src/test/static/' + type + '/' + static_data = self.load_static_data(folder, target_exchange) + if static_data is None: + return True + exchanges = list(static_data.keys()) + exchange = init_exchange('Exchange', {}) # tmp to do the calculations until we have the ast-transpiler transpiling this code + promises = [] + sum = 0 + if target_exchange: + dump('[INFO:MAIN] Exchange to test: ' + target_exchange) + if test_name: + dump('[INFO:MAIN] Testing only: ' + test_name) + for i in range(0, len(exchanges)): + exchange_name = exchanges[i] + exchange_data = static_data[exchange_name] + disabled = self.check_if_exchange_is_disabled(exchange_name, exchange_data) + if disabled: + continue + number_of_tests = self.get_number_of_tests_from_exchange(exchange, exchange_data, test_name) + sum = exchange.sum(sum, number_of_tests) + if type == 'request': + promises.append(self.test_exchange_request_statically(exchange_name, exchange_data, test_name)) + else: + promises.append(self.test_exchange_response_statically(exchange_name, exchange_data, test_name)) + try: + (promises) + except Exception as e: + if type == 'request': + self.request_tests_failed = True + else: + self.response_tests_failed = True + error_message = '[' + self.lang + '][STATIC_REQUEST]' + exception_message(e) + dump('[TEST_FAILURE]' + error_message) + if self.request_tests_failed or self.response_tests_failed: + exit_script(1) + else: + prefix = '[SYNC]' if (is_sync()) else '' + success_message = '[' + self.lang + ']' + prefix + '[TEST_SUCCESS] ' + str(sum) + ' static ' + type + ' tests passed.' + dump('[INFO]' + success_message) + + def run_static_response_tests(self, exchange_name=None, test=None): + # ----------------------------------------------------------------------------- + # --- Init of mockResponses tests functions------------------------------------ + # ----------------------------------------------------------------------------- + self.run_static_tests('response', exchange_name, test) + return True + + def run_broker_id_tests(self): + # ----------------------------------------------------------------------------- + # --- Init of brokerId tests functions----------------------------------------- + # ----------------------------------------------------------------------------- + promises = [self.test_binance(), self.test_okx(), self.test_cryptocom(), self.test_bybit(), self.test_kucoin(), self.test_kucoinfutures(), self.test_bitget(), self.test_mexc(), self.test_htx(), self.test_woo(), self.test_bitmart(), self.test_coinex(), self.test_bingx(), self.test_phemex(), self.test_blofin(), self.test_coinbaseinternational(), self.test_coinbase_advanced(), self.test_woofi_pro(), self.test_oxfun(), self.test_xt(), self.test_paradex(), self.test_hashkey(), self.test_coincatch(), self.test_defx(), self.test_cryptomus(), self.test_derive(), self.test_mode_trade(), self.test_backpack()] + (promises) + success_message = '[' + self.lang + '][TEST_SUCCESS] brokerId tests passed.' + dump('[INFO]' + success_message) + exit_script(0) + return True + + def test_binance(self): + exchange = self.init_offline_exchange('binance') + spot_id = 'x-TKT5PX2F' + swap_id = 'x-cvBPrNm9' + inverse_swap_id = 'x-xcKtGhcu' + spot_order_request = None + try: + exchange.create_order('BTC/USDT', 'limit', 'buy', 1, 20000) + except Exception as e: + spot_order_request = self.urlencoded_to_dict(exchange.last_request_body) + client_order_id = spot_order_request['newClientOrderId'] + spot_id_string = str(spot_id) + assert client_order_id.startswith(spot_id_string), 'binance - spot clientOrderId: ' + client_order_id + ' does not start with spotId' + spot_id_string + swap_order_request = None + try: + exchange.create_order('BTC/USDT:USDT', 'limit', 'buy', 1, 20000) + except Exception as e: + swap_order_request = self.urlencoded_to_dict(exchange.last_request_body) + swap_inverse_order_request = None + try: + exchange.create_order('BTC/USD:BTC', 'limit', 'buy', 1, 20000) + except Exception as e: + swap_inverse_order_request = self.urlencoded_to_dict(exchange.last_request_body) + # linear swap + client_order_id_swap = swap_order_request['newClientOrderId'] + swap_id_string = str(swap_id) + assert client_order_id_swap.startswith(swap_id_string), 'binance - swap clientOrderId: ' + client_order_id_swap + ' does not start with swapId' + swap_id_string + # inverse swap + client_order_id_inverse = swap_inverse_order_request['newClientOrderId'] + assert client_order_id_inverse.startswith(inverse_swap_id), 'binance - swap clientOrderIdInverse: ' + client_order_id_inverse + ' does not start with swapId' + inverse_swap_id + create_orders_request = None + try: + orders = [{ + 'symbol': 'BTC/USDT:USDT', + 'type': 'limit', + 'side': 'sell', + 'amount': 1, + 'price': 100000, +}, { + 'symbol': 'BTC/USDT:USDT', + 'type': 'market', + 'side': 'buy', + 'amount': 1, +}] + exchange.create_orders(orders) + except Exception as e: + create_orders_request = self.urlencoded_to_dict(exchange.last_request_body) + batch_orders = create_orders_request['batchOrders'] + for i in range(0, len(batch_orders)): + current = batch_orders[i] + current_client_order_id = current['newClientOrderId'] + assert current_client_order_id.startswith(swap_id_string), 'binance createOrders - clientOrderId: ' + current_client_order_id + ' does not start with swapId' + swap_id_string + if not is_sync(): + close(exchange) + return True + + def test_okx(self): + exchange = self.init_offline_exchange('okx') + id = '6b9ad766b55dBCDE' + spot_order_request = None + try: + exchange.create_order('BTC/USDT', 'limit', 'buy', 1, 20000) + except Exception as e: + spot_order_request = json_parse(exchange.last_request_body) + client_order_id = spot_order_request[0]['clOrdId'] # returns order inside array + id_string = str(id) + assert client_order_id.startswith(id_string), 'okx - spot clientOrderId: ' + client_order_id + ' does not start with id: ' + id_string + spot_tag = spot_order_request[0]['tag'] + assert spot_tag == id, 'okx - id: ' + id + ' different from spot tag: ' + spot_tag + swap_order_request = None + try: + exchange.create_order('BTC/USDT:USDT', 'limit', 'buy', 1, 20000) + except Exception as e: + swap_order_request = json_parse(exchange.last_request_body) + client_order_id_swap = swap_order_request[0]['clOrdId'] + assert client_order_id_swap.startswith(id_string), 'okx - swap clientOrderId: ' + client_order_id_swap + ' does not start with id: ' + id_string + swap_tag = swap_order_request[0]['tag'] + assert swap_tag == id, 'okx - id: ' + id + ' different from swap tag: ' + swap_tag + if not is_sync(): + close(exchange) + return True + + def test_cryptocom(self): + exchange = self.init_offline_exchange('cryptocom') + id = 'CCXT' + exchange.load_markets() + request = None + try: + exchange.create_order('BTC/USDT', 'limit', 'buy', 1, 20000) + except Exception as e: + request = json_parse(exchange.last_request_body) + broker_id = request['params']['broker_id'] + assert broker_id == id, 'cryptocom - id: ' + id + ' different from broker_id: ' + broker_id + if not is_sync(): + close(exchange) + return True + + def test_bybit(self): + exchange = self.init_offline_exchange('bybit') + req_headers = None + id = 'CCXT' + assert exchange.options['brokerId'] == id, 'id not in options' + try: + exchange.create_order('BTC/USDT', 'limit', 'buy', 1, 20000) + except Exception as e: + # we expect an error here, we're only interested in the headers + req_headers = exchange.last_request_headers + assert req_headers['Referer'] == id, 'bybit - id: ' + id + ' not in headers.' + if not is_sync(): + close(exchange) + return True + + def test_kucoin(self): + exchange = self.init_offline_exchange('kucoin') + req_headers = None + spot_id = exchange.options['partner']['spot']['id'] + spot_key = exchange.options['partner']['spot']['key'] + assert spot_id == 'ccxt', 'kucoin - id: ' + spot_id + ' not in options' + assert spot_key == '9e58cc35-5b5e-4133-92ec-166e3f077cb8', 'kucoin - key: ' + spot_key + ' not in options.' + try: + exchange.create_order('BTC/USDT', 'limit', 'buy', 1, 20000) + except Exception as e: + # we expect an error here, we're only interested in the headers + req_headers = exchange.last_request_headers + id = 'ccxt' + assert req_headers['KC-API-PARTNER'] == id, 'kucoin - id: ' + id + ' not in headers.' + if not is_sync(): + close(exchange) + return True + + def test_kucoinfutures(self): + exchange = self.init_offline_exchange('kucoinfutures') + req_headers = None + id = 'ccxtfutures' + future_id = exchange.options['partner']['future']['id'] + future_key = exchange.options['partner']['future']['key'] + assert future_id == id, 'kucoinfutures - id: ' + future_id + ' not in options.' + assert future_key == '1b327198-f30c-4f14-a0ac-918871282f15', 'kucoinfutures - key: ' + future_key + ' not in options.' + try: + exchange.create_order('BTC/USDT:USDT', 'limit', 'buy', 1, 20000) + except Exception as e: + req_headers = exchange.last_request_headers + assert req_headers['KC-API-PARTNER'] == id, 'kucoinfutures - id: ' + id + ' not in headers.' + if not is_sync(): + close(exchange) + return True + + def test_bitget(self): + exchange = self.init_offline_exchange('bitget') + req_headers = None + id = 'p4sve' + assert exchange.options['broker'] == id, 'bitget - id: ' + id + ' not in options' + try: + exchange.create_order('BTC/USDT', 'limit', 'buy', 1, 20000) + except Exception as e: + req_headers = exchange.last_request_headers + assert req_headers['X-CHANNEL-API-CODE'] == id, 'bitget - id: ' + id + ' not in headers.' + if not is_sync(): + close(exchange) + return True + + def test_mexc(self): + exchange = self.init_offline_exchange('mexc') + req_headers = None + id = 'CCXT' + assert exchange.options['broker'] == id, 'mexc - id: ' + id + ' not in options' + exchange.load_markets() + try: + exchange.create_order('BTC/USDT', 'limit', 'buy', 1, 20000) + except Exception as e: + req_headers = exchange.last_request_headers + assert req_headers['source'] == id, 'mexc - id: ' + id + ' not in headers.' + if not is_sync(): + close(exchange) + return True + + def test_htx(self): + exchange = self.init_offline_exchange('htx') + # spot test + id = 'AA03022abc' + spot_order_request = None + try: + exchange.create_order('BTC/USDT', 'limit', 'buy', 1, 20000) + except Exception as e: + spot_order_request = json_parse(exchange.last_request_body) + client_order_id = spot_order_request['client-order-id'] + id_string = str(id) + assert client_order_id.startswith(id_string), 'htx - spot clientOrderId ' + client_order_id + ' does not start with id: ' + id_string + # swap test + swap_order_request = None + try: + exchange.create_order('BTC/USDT:USDT', 'limit', 'buy', 1, 20000) + except Exception as e: + swap_order_request = json_parse(exchange.last_request_body) + swap_inverse_order_request = None + try: + exchange.create_order('BTC/USD:BTC', 'limit', 'buy', 1, 20000) + except Exception as e: + swap_inverse_order_request = json_parse(exchange.last_request_body) + client_order_id_swap = swap_order_request['channel_code'] + assert client_order_id_swap.startswith(id_string), 'htx - swap channel_code ' + client_order_id_swap + ' does not start with id: ' + id_string + client_order_id_inverse = swap_inverse_order_request['channel_code'] + assert client_order_id_inverse.startswith(id_string), 'htx - swap inverse channel_code ' + client_order_id_inverse + ' does not start with id: ' + id_string + if not is_sync(): + close(exchange) + return True + + def test_woo(self): + exchange = self.init_offline_exchange('woo') + # spot test + id = 'bc830de7-50f3-460b-9ee0-f430f83f9dad' + spot_order_request = None + try: + exchange.create_order('BTC/USDT', 'limit', 'buy', 1, 20000) + except Exception as e: + spot_order_request = json_parse(exchange.last_request_body) + broker_id = spot_order_request['broker_id'] + id_string = str(id) + assert broker_id.startswith(id_string), 'woo - broker_id: ' + broker_id + ' does not start with id: ' + id_string + # swap test + stop_order_request = None + try: + exchange.create_order('BTC/USDT:USDT', 'limit', 'buy', 1, 20000, { + 'stopPrice': 30000, + }) + except Exception as e: + stop_order_request = json_parse(exchange.last_request_body) + client_order_id_stop = stop_order_request['brokerId'] + assert client_order_id_stop.startswith(id_string), 'woo - brokerId: ' + client_order_id_stop + ' does not start with id: ' + id_string + if not is_sync(): + close(exchange) + return True + + def test_bitmart(self): + exchange = self.init_offline_exchange('bitmart') + req_headers = None + id = 'CCXTxBitmart000' + assert exchange.options['brokerId'] == id, 'bitmart - id: ' + id + ' not in options' + exchange.load_markets() + try: + exchange.create_order('BTC/USDT', 'limit', 'buy', 1, 20000) + except Exception as e: + req_headers = exchange.last_request_headers + assert req_headers['X-BM-BROKER-ID'] == id, 'bitmart - id: ' + id + ' not in headers' + if not is_sync(): + close(exchange) + return True + + def test_coinex(self): + exchange = self.init_offline_exchange('coinex') + id = 'x-167673045' + assert exchange.options['brokerId'] == id, 'coinex - id: ' + id + ' not in options' + spot_order_request = None + try: + exchange.create_order('BTC/USDT', 'limit', 'buy', 1, 20000) + except Exception as e: + spot_order_request = json_parse(exchange.last_request_body) + client_order_id = spot_order_request['client_id'] + id_string = str(id) + assert client_order_id.startswith(id_string), 'coinex - clientOrderId: ' + client_order_id + ' does not start with id: ' + id_string + if not is_sync(): + close(exchange) + return True + + def test_bingx(self): + exchange = self.init_offline_exchange('bingx') + req_headers = None + id = 'CCXT' + assert exchange.options['broker'] == id, 'bingx - id: ' + id + ' not in options' + try: + exchange.create_order('BTC/USDT', 'limit', 'buy', 1, 20000) + except Exception as e: + # we expect an error here, we're only interested in the headers + req_headers = exchange.last_request_headers + assert req_headers['X-SOURCE-KEY'] == id, 'bingx - id: ' + id + ' not in headers.' + if not is_sync(): + close(exchange) + return True + + def test_phemex(self): + exchange = self.init_offline_exchange('phemex') + id = 'CCXT123456' + request = None + try: + exchange.create_order('BTC/USDT', 'limit', 'buy', 1, 20000) + except Exception as e: + request = json_parse(exchange.last_request_body) + client_order_id = request['clOrdID'] + id_string = str(id) + assert client_order_id.startswith(id_string), 'phemex - clOrdID: ' + client_order_id + ' does not start with id: ' + id_string + if not is_sync(): + close(exchange) + return True + + def test_blofin(self): + exchange = self.init_offline_exchange('blofin') + id = 'ec6dd3a7dd982d0b' + request = None + try: + exchange.create_order('LTC/USDT:USDT', 'market', 'buy', 1) + except Exception as e: + request = json_parse(exchange.last_request_body) + broker_id = request['brokerId'] + id_string = str(id) + assert broker_id.startswith(id_string), 'blofin - brokerId: ' + broker_id + ' does not start with id: ' + id_string + if not is_sync(): + close(exchange) + return True + + # testHyperliquid () { + # const exchange = this.initOfflineExchange ('hyperliquid'); + # const id = '1'; + # let request = undefined; + # try { + # exchange.createOrder ('SOL/USDC:USDC', 'limit', 'buy', 1, 100); + # } catch (e) { + # request = jsonParse (exchange.last_request_body); + # } + # const brokerId = (request['action']['brokerCode']).toString (); + # assert (brokerId === id, 'hyperliquid - brokerId: ' + brokerId + ' does not start with id: ' + id); + # if (!isSync ()) { + # close (exchange); + # } + # return true; + # } + def test_coinbaseinternational(self): + exchange = self.init_offline_exchange('coinbaseinternational') + exchange.options['portfolio'] = 'random' + id = 'nfqkvdjp' + assert exchange.options['brokerId'] == id, 'id not in options' + request = None + try: + exchange.create_order('BTC/USDC:USDC', 'limit', 'buy', 1, 20000) + except Exception as e: + request = json_parse(exchange.last_request_body) + client_order_id = request['client_order_id'] + assert client_order_id.startswith(str(id)), 'clientOrderId does not start with id' + if not is_sync(): + close(exchange) + return True + + def test_coinbase_advanced(self): + exchange = self.init_offline_exchange('coinbase') + id = 'ccxt' + assert exchange.options['brokerId'] == id, 'id not in options' + request = None + try: + exchange.create_order('BTC/USDC', 'limit', 'buy', 1, 20000) + except Exception as e: + request = json_parse(exchange.last_request_body) + client_order_id = request['client_order_id'] + assert client_order_id.startswith(str(id)), 'clientOrderId does not start with id' + if not is_sync(): + close(exchange) + return True + + def test_woofi_pro(self): + exchange = self.init_offline_exchange('woofipro') + exchange.secret = 'secretsecretsecretsecretsecretsecretsecrets' + id = 'CCXT' + exchange.load_markets() + request = None + try: + exchange.create_order('BTC/USDC:USDC', 'limit', 'buy', 1, 20000) + except Exception as e: + request = json_parse(exchange.last_request_body) + broker_id = request['order_tag'] + assert broker_id == id, 'woofipro - id: ' + id + ' different from broker_id: ' + broker_id + if not is_sync(): + close(exchange) + return True + + def test_oxfun(self): + exchange = self.init_offline_exchange('oxfun') + exchange.secret = 'secretsecretsecretsecretsecretsecretsecrets' + id = 1000 + exchange.load_markets() + request = None + try: + exchange.create_order('BTC/USD:OX', 'limit', 'buy', 1, 20000) + except Exception as e: + request = json_parse(exchange.last_request_body) + orders = request['orders'] + first = orders[0] + broker_id = first['source'] + assert broker_id == id, 'oxfun - id: ' + str(id) + ' different from broker_id: ' + str(broker_id) + return True + + def test_xt(self): + exchange = self.init_offline_exchange('xt') + id = 'CCXT' + spot_order_request = None + try: + exchange.create_order('BTC/USDT', 'limit', 'buy', 1, 20000) + except Exception as e: + spot_order_request = json_parse(exchange.last_request_body) + spot_media = spot_order_request['media'] + assert spot_media == id, 'xt - id: ' + id + ' different from swap tag: ' + spot_media + swap_order_request = None + try: + exchange.create_order('BTC/USDT:USDT', 'limit', 'buy', 1, 20000) + except Exception as e: + swap_order_request = json_parse(exchange.last_request_body) + swap_media = swap_order_request['clientMedia'] + assert swap_media == id, 'xt - id: ' + id + ' different from swap tag: ' + swap_media + if not is_sync(): + close(exchange) + return True + + def test_paradex(self): + exchange = self.init_offline_exchange('paradex') + exchange.walletAddress = '0xc751489d24a33172541ea451bc253d7a9e98c781' + exchange.privateKey = 'c33b1eb4b53108bf52e10f636d8c1236c04c33a712357ba3543ab45f48a5cb0b' + exchange.options['authToken'] = 'token' + exchange.options['systemConfig'] = { + 'starknet_gateway_url': 'https://potc-testnet-sepolia.starknet.io', + 'starknet_fullnode_rpc_url': 'https://pathfinder.api.testnet.paradex.trade/rpc/v0_7', + 'starknet_chain_id': 'PRIVATE_SN_POTC_SEPOLIA', + 'block_explorer_url': 'https://voyager.testnet.paradex.trade/', + 'paraclear_address': '0x286003f7c7bfc3f94e8f0af48b48302e7aee2fb13c23b141479ba00832ef2c6', + 'paraclear_decimals': 8, + 'paraclear_account_proxy_hash': '0x3530cc4759d78042f1b543bf797f5f3d647cde0388c33734cf91b7f7b9314a9', + 'paraclear_account_hash': '0x41cb0280ebadaa75f996d8d92c6f265f6d040bb3ba442e5f86a554f1765244e', + 'oracle_address': '0x2c6a867917ef858d6b193a0ff9e62b46d0dc760366920d631715d58baeaca1f', + 'bridged_tokens': [{ + 'name': 'TEST USDC', + 'symbol': 'USDC', + 'decimals': 6, + 'l1_token_address': '0x29A873159D5e14AcBd63913D4A7E2df04570c666', + 'l1_bridge_address': '0x8586e05adc0C35aa11609023d4Ae6075Cb813b4C', + 'l2_token_address': '0x6f373b346561036d98ea10fb3e60d2f459c872b1933b50b21fe6ef4fda3b75e', + 'l2_bridge_address': '0x46e9237f5408b5f899e72125dd69bd55485a287aaf24663d3ebe00d237fc7ef', +}], + 'l1_core_contract_address': '0x582CC5d9b509391232cd544cDF9da036e55833Af', + 'l1_operator_address': '0x11bACdFbBcd3Febe5e8CEAa75E0Ef6444d9B45FB', + 'l1_chain_id': '11155111', + 'liquidation_fee': '0.2', + } + req_headers = None + id = 'CCXT' + assert exchange.options['broker'] == id, 'paradex - id: ' + id + ' not in options' + exchange.load_markets() + try: + exchange.create_order('BTC/USD:USDC', 'limit', 'buy', 1, 20000) + except Exception as e: + req_headers = exchange.last_request_headers + assert req_headers['PARADEX-PARTNER'] == id, 'paradex - id: ' + id + ' not in headers' + if not is_sync(): + close(exchange) + return True + + def test_hashkey(self): + exchange = self.init_offline_exchange('hashkey') + req_headers = None + id = '10000700011' + try: + exchange.create_order('BTC/USDT', 'limit', 'buy', 1, 20000) + except Exception as e: + # we expect an error here, we're only interested in the headers + req_headers = exchange.last_request_headers + assert req_headers['INPUT-SOURCE'] == id, 'hashkey - id: ' + id + ' not in headers.' + if not is_sync(): + close(exchange) + return True + + def test_coincatch(self): + exchange = self.init_offline_exchange('coincatch') + req_headers = None + id = '47cfy' + try: + exchange.create_order('BTC/USDT', 'limit', 'buy', 1, 20000) + except Exception as e: + # we expect an error here, we're only interested in the headers + req_headers = exchange.last_request_headers + assert req_headers['X-CHANNEL-API-CODE'] == id, 'coincatch - id: ' + id + ' not in headers.' + if not is_sync(): + close(exchange) + return True + + def test_defx(self): + exchange = self.init_offline_exchange('defx') + req_headers = None + try: + exchange.create_order('DOGE/USDC:USDC', 'limit', 'buy', 100, 1) + except Exception as e: + # we expect an error here, we're only interested in the headers + req_headers = exchange.last_request_headers + id = 'ccxt' + assert req_headers['X-DEFX-SOURCE'] == id, 'defx - id: ' + id + ' not in headers.' + if not is_sync(): + close(exchange) + return True + + def test_cryptomus(self): + exchange = self.init_offline_exchange('cryptomus') + request = None + try: + exchange.create_order('BTC/USDT', 'limit', 'sell', 1, 20000) + except Exception as e: + request = json_parse(exchange.last_request_body) + tag = 'ccxt' + assert request['tag'] == tag, 'cryptomus - tag: ' + tag + ' not in request.' + if not is_sync(): + close(exchange) + return True + + def test_derive(self): + exchange = self.init_offline_exchange('derive') + id = '0x0ad42b8e602c2d3d475ae52d678cf63d84ab2749' + assert exchange.options['id'] == id, 'derive - id: ' + id + ' not in options' + request = None + try: + params = { + 'subaccount_id': 1234, + 'max_fee': 10, + 'deriveWalletAddress': '0x0ad42b8e602c2d3d475ae52d678cf63d84ab2749', + } + exchange.walletAddress = '0x0ad42b8e602c2d3d475ae52d678cf63d84ab2749' + exchange.privateKey = '0x7b77bb7b20e92bbb85f2a22b330b896959229a5790e35f2f290922de3fb22ad5' + exchange.create_order('LBTC/USDC', 'limit', 'sell', 0.01, 3000, params) + except Exception as e: + request = json_parse(exchange.last_request_body) + assert request['referral_code'] == id, 'derive - referral_code: ' + id + ' not in request.' + if not is_sync(): + close(exchange) + return True + + def test_mode_trade(self): + exchange = self.init_offline_exchange('modetrade') + exchange.secret = 'secretsecretsecretsecretsecretsecretsecrets' + id = 'CCXTMODE' + exchange.load_markets() + request = None + try: + exchange.create_order('BTC/USDC:USDC', 'limit', 'buy', 1, 20000) + except Exception as e: + request = json_parse(exchange.last_request_body) + broker_id = request['order_tag'] + assert broker_id == id, 'modetrade - id: ' + id + ' different from broker_id: ' + broker_id + if not is_sync(): + close(exchange) + return True + + def test_backpack(self): + exchange = self.init_offline_exchange('backpack') + exchange.apiKey = 'Jcj3vxDMAIrx0G5YYfydzS/le/owoQ+VSS164zC1RXo=' + exchange.secret = 'sRkC124Iazob0QYvaFj9dm63MXEVY48lDNt+/GVDVAU=' + req_headers = None + id = '1400' + try: + exchange.create_order('ETH/USDC', 'limit', 'buy', 1, 5000) + except Exception as e: + # we expect an error here, we're only interested in the headers + req_headers = exchange.last_request_headers + assert req_headers['X-Broker-Id'] == id, 'backpack - id: ' + id + ' not in headers.' + if not is_sync(): + close(exchange) + return True diff --git a/ccxt/timex.py b/ccxt/timex.py new file mode 100644 index 0000000..1943db8 --- /dev/null +++ b/ccxt/timex.py @@ -0,0 +1,1752 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.timex import ImplicitAPI +from ccxt.base.types import Any, Balances, Currencies, Currency, DepositAddress, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, TradingFeeInterface, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import NotSupported +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class timex(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(timex, self).describe(), { + 'id': 'timex', + 'name': 'TimeX', + 'countries': ['AU'], + 'version': 'v1', + 'rateLimit': 1500, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelOrder': True, + 'cancelOrders': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createOrder': True, + 'createReduceOnlyOrder': False, + 'createStopLimitOrder': False, + 'createStopMarketOrder': False, + 'createStopOrder': False, + 'editOrder': True, + 'fetchAllGreeks': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDeposit': False, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrice': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': True, # maker fee only + 'fetchUnderlyingAssets': False, + 'fetchVolatilityHistory': False, + 'fetchWithdrawal': False, + 'fetchWithdrawals': True, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + }, + 'timeframes': { + '1m': 'I1', + '5m': 'I5', + '15m': 'I15', + '30m': 'I30', + '1h': 'H1', + '2h': 'H2', + '4h': 'H4', + '6h': 'H6', + '12h': 'H12', + '1d': 'D1', + '1w': 'W1', + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/70423869-6839ab00-1a7f-11ea-8f94-13ae72c31115.jpg', + 'api': { + 'rest': 'https://plasma-relay-backend.timex.io', + }, + 'www': 'https://timex.io', + 'doc': 'https://plasma-relay-backend.timex.io/swagger-ui/index.html', + 'referral': 'https://timex.io/?refcode=1x27vNkTbP1uwkCck', + }, + 'api': { + 'addressbook': { + 'get': [ + 'me', + ], + 'post': [ + '', + 'id/{id}', + 'id/{id}/remove', + ], + }, + 'custody': { + 'get': [ + 'credentials', # Get api key for address + 'credentials/h/{hash}', # Get api key by hash + 'credentials/k/{key}', # Get api key by key + 'credentials/me', + 'credentials/me/address', # Get api key by hash + 'deposit-addresses', # Get deposit addresses list + 'deposit-addresses/h/{hash}', # Get deposit address by hash + ], + }, + 'history': { + 'get': [ + 'orders', # Gets historical orders + 'orders/details', # Gets order details + 'orders/export/csv', # Export orders to csv + 'trades', # Gets historical trades + 'trades/export/csv', # Export trades to csv + ], + }, + 'currencies': { + 'get': [ + 'a/{address}', # Gets currency by address + 'i/{id}', # Gets currency by id + 's/{symbol}', # Gets currency by symbol + ], + 'post': [ + 'perform', # Creates new currency + 'prepare', # Prepare creates new currency + 'remove/perform', # Removes currency by symbol + 's/{symbol}/remove/prepare', # Prepare remove currency by symbol + 's/{symbol}/update/perform', # Prepare update currency by symbol + 's/{symbol}/update/prepare', # Prepare update currency by symbol + ], + }, + 'manager': { + 'get': [ + 'deposits', + 'transfers', + 'withdrawals', + ], + }, + 'markets': { + 'get': [ + 'i/{id}', # Gets market by id + 's/{symbol}', # Gets market by symbol + ], + 'post': [ + 'perform', # Creates new market + 'prepare', # Prepare creates new market + 'remove/perform', # Removes market by symbol + 's/{symbol}/remove/prepare', # Prepare remove market by symbol + 's/{symbol}/update/perform', # Prepare update market by symbol + 's/{symbol}/update/prepare', # Prepare update market by symbol + ], + }, + 'public': { + 'get': [ + 'candles', # Gets candles + 'currencies', # Gets all the currencies + 'markets', # Gets all the markets + 'orderbook', # Gets orderbook + 'orderbook/raw', # Gets raw orderbook + 'orderbook/v2', # Gets orderbook v2 + 'tickers', # Gets all the tickers + 'trades', # Gets trades + ], + }, + 'statistics': { + 'get': [ + 'address', # calculateAddressStatistics + ], + }, + 'trading': { + 'get': [ + 'balances', # Get trading balances for all(or selected) currencies + 'fees', # Get trading fee rates for all(or selected) markets + 'orders', # Gets open orders + ], + 'post': [ + 'orders', # Create new order + 'orders/json', # Create orders + ], + 'put': [ + 'orders', # Cancel or update orders + 'orders/json', # Update orders + ], + 'delete': [ + 'orders', # Delete orders + 'orders/json', # Delete orders + ], + }, + 'tradingview': { + 'get': [ + 'config', # Gets config + 'history', # Gets history + 'symbol_info', # Gets symbol info + 'time', # Gets time + ], + }, + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'exact': { + '0': ExchangeError, + '1': NotSupported, + '4000': BadRequest, + '4001': BadRequest, + '4002': InsufficientFunds, + '4003': AuthenticationError, + '4004': AuthenticationError, + '4005': BadRequest, + '4006': BadRequest, + '4007': BadRequest, + '4300': PermissionDenied, + '4100': AuthenticationError, + '4400': OrderNotFound, + '5001': InvalidOrder, + '5002': ExchangeError, + '400': BadRequest, + '401': AuthenticationError, + '403': PermissionDenied, + '404': OrderNotFound, + '429': RateLimitExceeded, + '500': ExchangeError, + '503': ExchangeNotAvailable, + }, + 'broad': { + 'Insufficient': InsufficientFunds, + }, + }, + 'options': { + 'expireIn': 31536000, # 365 × 24 × 60 × 60 + 'fetchTickers': { + 'period': '1d', + }, + 'fetchTrades': { + 'sort': 'timestamp,asc', + }, + 'fetchMyTrades': { + 'sort': 'timestamp,asc', + }, + 'fetchOpenOrders': { + 'sort': 'createdAt,asc', + }, + 'fetchClosedOrders': { + 'sort': 'createdAt,asc', + }, + 'defaultSort': 'timestamp,asc', + 'defaultSortOrders': 'createdAt,asc', + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': False, + 'triggerDirection': False, + 'triggerPriceType': None, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + # todo + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': False, + 'GTD': True, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': False, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, # todo + 'daysBack': 100000, # todo + 'untilDays': 100000, # todo + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 100, # todo + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, # todo + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 100, # todo + 'daysBack': 100000, # todo + 'daysBackCanceled': 1, # todo + 'untilDays': 100000, # todo + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': None, + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + }) + + def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = self.tradingviewGetTime(params) + # + # 1708682617 + # + return self.parse_to_int(response) * 1000 + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for timex + + https://plasma-relay-backend.timex.io/swagger-ui/index.html?urls.primaryName=Relay#/Public/listMarkets + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = self.publicGetMarkets(params) + # + # [ + # { + # "symbol": "ETHBTC", + # "name": "ETH/BTC", + # "baseCurrency": "ETH", + # "baseTokenAddress": "0x45932db54b38af1f5a57136302eeba66a5975c15", + # "quoteCurrency": "BTC", + # "quoteTokenAddress": "0x8370fbc6ddec1e18b4e41e72ed943e238458487c", + # "feeCurrency": "BTC", + # "feeTokenAddress": "0x8370fbc6ddec1e18b4e41e72ed943e238458487c", + # "quantityIncrement": "0.0000001", + # "takerFee": "0.005", + # "makerFee": "0.0025", + # "tickSize": "0.00000001", + # "baseMinSize": "0.0001", + # "quoteMinSize": "0.00001", + # "locked": False + # } + # ] + # + return self.parse_markets(response) + + def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://plasma-relay-backend.timex.io/swagger-ui/index.html?urls.primaryName=Relay#/Public/listCurrencies + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = self.publicGetCurrencies(params) + # + # [ + # { + # "symbol": "BTC", + # "name": "Bitcoin", + # "address": "0x8370fbc6ddec1e18b4e41e72ed943e238458487c", + # "icon": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHZpZXdCb3g9IjAgMCA2MCA2MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggb3BhY2l0eT0iMC41IiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTMwIDUzQzQyLjcwMjUgNTMgNTMgNDIuNzAyNSA1MyAzMEM1MyAxNy4yOTc1IDQyLjcwMjUgNyAzMCA3QzE3LjI5NzUgNyA3IDE3LjI5NzUgNyAzMEM3IDQyLjcwMjUgMTcuMjk3NSA1MyAzMCA1M1pNMzAgNTVDNDMuODA3MSA1NSA1NSA0My44MDcxIDU1IDMwQzU1IDE2LjE5MjkgNDMuODA3MSA1IDMwIDVDMTYuMTkyOSA1IDUgMTYuMTkyOSA1IDMwQzUgNDMuODA3MSAxNi4xOTI5IDU1IDMwIDU1WiIvPgo8cGF0aCBkPSJNNDAuOTQyNSAyNi42NTg1QzQxLjQwMDMgMjMuNjExMyAzOS4wNzA1IDIxLjk3MzIgMzUuODg0OCAyMC44ODA0TDM2LjkxODIgMTYuNzUyNkwzNC4zOTUxIDE2LjEyNjRMMzMuMzg5IDIwLjE0NTVDMzIuNzI1OCAxOS45ODA5IDMyLjA0NDUgMTkuODI1NiAzMS4zNjc1IDE5LjY3MTdMMzIuMzgwOCAxNS42MjYyTDI5Ljg1OTEgMTVMMjguODI1IDE5LjEyNjRDMjguMjc2IDE5LjAwMTkgMjcuNzM3IDE4Ljg3ODggMjcuMjEzOSAxOC43NDkzTDI3LjIxNjggMTguNzM2NEwyMy43MzcyIDE3Ljg3MTJMMjMuMDY2IDIwLjU1NDhDMjMuMDY2IDIwLjU1NDggMjQuOTM4IDIwLjk4MjEgMjQuODk4NSAyMS4wMDg1QzI1LjkyMDQgMjEuMjYyNiAyNi4xMDUgMjEuOTM2IDI2LjA3NDEgMjIuNDY5OUwyNC44OTcgMjcuMTcyNEMyNC45Njc1IDI3LjE5MDMgMjUuMDU4NyAyNy4yMTYgMjUuMTU5MyAyNy4yNTYxQzI1LjA3NTMgMjcuMjM1NCAyNC45ODU0IDI3LjIxMjQgMjQuODkyNyAyNy4xOTAzTDIzLjI0MjggMzMuNzc3OEMyMy4xMTc3IDM0LjA4NjkgMjIuODAwOCAzNC41NTA2IDIyLjA4NjUgMzQuMzc0NkMyMi4xMTE3IDM0LjQxMTEgMjAuMjUyNiAzMy45MTg3IDIwLjI1MjYgMzMuOTE4N0wxOSAzNi43OTQ5TDIyLjI4MzQgMzcuNjFDMjIuODk0MiAzNy43NjI0IDIzLjQ5MjggMzcuOTIyIDI0LjA4MjEgMzguMDcyM0wyMy4wMzggNDIuMjQ3NEwyNS41NTgyIDQyLjg3MzZMMjYuNTkyMyAzOC43NDI5QzI3LjI4MDcgMzguOTI5IDI3Ljk0OSAzOS4xMDA3IDI4LjYwMyAzOS4yNjI0TDI3LjU3MjUgNDMuMzczOEwzMC4wOTU2IDQ0TDMxLjEzOTcgMzkuODMyOEMzNS40NDIyIDQwLjY0MzYgMzguNjc3NCA0MC4zMTY2IDQwLjAzOTIgMzYuNDQxNEM0MS4xMzY1IDMzLjMyMTIgMzkuOTg0NiAzMS41MjEzIDM3LjcyMDkgMzAuMzQ3N0MzOS4zNjk0IDI5Ljk2OTEgNDAuNjExMiAyOC44ODkyIDQwLjk0MjUgMjYuNjU4NVYyNi42NTg1Wk0zNS4xNzc3IDM0LjcwODhDMzQuMzk4IDM3LjgyOSAyOS4xMjI2IDM2LjE0MjIgMjcuNDEyMiAzNS43MTkzTDI4Ljc5NzcgMzAuMTg4MUMzMC41MDgxIDMwLjYxMzIgMzUuOTkyNiAzMS40NTQ4IDM1LjE3NzcgMzQuNzA4OFpNMzUuOTU4MSAyNi42MTM0QzM1LjI0NjcgMjkuNDUxNyAzMC44NTU5IDI4LjAwOTcgMjkuNDMxNiAyNy42NTYxTDMwLjY4NzcgMjIuNjM5NUMzMi4xMTIgMjIuOTkzIDM2LjY5OSAyMy42NTI4IDM1Ljk1ODEgMjYuNjEzNFoiLz4KPC9zdmc+Cg==", + # "background": "transparent", + # "fiatSymbol": "BTC", + # "decimals": 8, + # "tradeDecimals": 20, + # "displayDecimals": 4, + # "crypto": True, + # "depositEnabled": True, + # "withdrawalEnabled": True, + # "transferEnabled": True, + # "buyEnabled": False, + # "purchaseEnabled": False, + # "redeemEnabled": False, + # "active": True, + # "withdrawalFee": "50000000000000000", + # "purchaseCommissions": [] + # }, + # ] + # + return self.parse_currencies(response) + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://plasma-relay-backend.timex.io/swagger-ui/index.html?urls.primaryName=Relay#/Manager/getDeposits + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + address = self.safe_string(params, 'address') + params = self.omit(params, 'address') + if address is None: + raise ArgumentsRequired(self.id + ' fetchDeposits() requires an address parameter') + request: dict = { + 'address': address, + } + response = self.managerGetDeposits(self.extend(request, params)) + # + # [ + # { + # "from": "0x1134cc86b45039cc211c6d1d2e4b3c77f60207ed", + # "timestamp": "2022-01-01T00:00:00Z", + # "to": "0x1134cc86b45039cc211c6d1d2e4b3c77f60207ed", + # "token": "0x6baad3fe5d0fd4be604420e728adbd68d67e119e", + # "transferHash": "0x5464cdff35448314e178b8677ea41e670ea0f2533f4e52bfbd4e4a6cfcdef4c2", + # "value": "100" + # } + # ] + # + currency = self.safe_currency(code) + return self.parse_transactions(response, currency, since, limit) + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made to an account + + https://plasma-relay-backend.timex.io/swagger-ui/index.html?urls.primaryName=Relay#/Manager/getWithdraws + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of transaction structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + address = self.safe_string(params, 'address') + params = self.omit(params, 'address') + if address is None: + raise ArgumentsRequired(self.id + ' fetchDeposits() requires an address parameter') + request: dict = { + 'address': address, + } + response = self.managerGetWithdrawals(self.extend(request, params)) + # + # [ + # { + # "from": "0x1134cc86b45039cc211c6d1d2e4b3c77f60207ed", + # "timestamp": "2022-01-01T00:00:00Z", + # "to": "0x1134cc86b45039cc211c6d1d2e4b3c77f60207ed", + # "token": "0x6baad3fe5d0fd4be604420e728adbd68d67e119e", + # "transferHash": "0x5464cdff35448314e178b8677ea41e670ea0f2533f4e52bfbd4e4a6cfcdef4c2", + # "value": "100" + # } + # ] + # + currency = self.safe_currency(code) + return self.parse_transactions(response, currency, since, limit) + + def get_currency_by_address(self, address): + currencies = self.currencies + for i in range(0, len(currencies)): + currency = currencies[i] + info = self.safe_value(currency, 'info', {}) + a = self.safe_string(info, 'address') + if a == address: + return currency + return None + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # { + # "from": "0x1134cc86b45039cc211c6d1d2e4b3c77f60207ed", + # "timestamp": "2022-01-01T00:00:00Z", + # "to": "0x1134cc86b45039cc211c6d1d2e4b3c77f60207ed", + # "token": "0x6baad3fe5d0fd4be604420e728adbd68d67e119e", + # "transferHash": "0x5464cdff35448314e178b8677ea41e670ea0f2533f4e52bfbd4e4a6cfcdef4c2", + # "value": "100" + # } + # + datetime = self.safe_string(transaction, 'timestamp') + currencyAddresss = self.safe_string(transaction, 'token', '') + currency = self.get_currency_by_address(currencyAddresss) + return { + 'info': transaction, + 'id': self.safe_string(transaction, 'transferHash'), + 'txid': self.safe_string(transaction, 'txid'), + 'timestamp': self.parse8601(datetime), + 'datetime': datetime, + 'network': None, + 'address': None, + 'addressTo': self.safe_string(transaction, 'to'), + 'addressFrom': self.safe_string(transaction, 'from'), + 'tag': None, + 'tagTo': None, + 'tagFrom': None, + 'type': None, + 'amount': self.safe_number(transaction, 'value'), + 'currency': self.safe_currency_code(None, currency), + 'status': 'ok', + 'updated': None, + 'internal': None, + 'comment': None, + 'fee': None, + } + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://plasma-relay-backend.timex.io/swagger-ui/index.html?urls.primaryName=Relay#/Public/listTickers + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + period = self.safe_string(self.options['fetchTickers'], 'period', '1d') + request: dict = { + 'period': self.timeframes[period], # I1, I5, I15, I30, H1, H2, H4, H6, H12, D1, W1 + } + response = self.publicGetTickers(self.extend(request, params)) + # + # [ + # { + # "ask": 0.017, + # "bid": 0.016, + # "high": 0.019, + # "last": 0.017, + # "low": 0.015, + # "market": "TIME/ETH", + # "open": 0.016, + # "period": "H1", + # "timestamp": "2018-12-14T20:50:36.134Z", + # "volume": 4.57, + # "volumeQuote": 0.07312 + # } + # ] + # + return self.parse_tickers(response, symbols) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://plasma-relay-backend.timex.io/swagger-ui/index.html?urls.primaryName=Relay#/Public/listTickers + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + period = self.safe_string(self.options['fetchTickers'], 'period', '1d') + request: dict = { + 'market': market['id'], + 'period': self.timeframes[period], # I1, I5, I15, I30, H1, H2, H4, H6, H12, D1, W1 + } + response = self.publicGetTickers(self.extend(request, params)) + # + # [ + # { + # "ask": 0.017, + # "bid": 0.016, + # "high": 0.019, + # "last": 0.017, + # "low": 0.015, + # "market": "TIME/ETH", + # "open": 0.016, + # "period": "H1", + # "timestamp": "2018-12-14T20:50:36.134Z", + # "volume": 4.57, + # "volumeQuote": 0.07312 + # } + # ] + # + ticker = self.safe_dict(response, 0) + return self.parse_ticker(ticker, market) + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://plasma-relay-backend.timex.io/swagger-ui/index.html?urls.primaryName=Relay#/Public/orderbookV2 + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + if limit is not None: + request['limit'] = limit + response = self.publicGetOrderbookV2(self.extend(request, params)) + # + # { + # "timestamp":"2019-12-05T00:21:09.538", + # "bid":[ + # { + # "index":"2", + # "price":"0.02024007", + # "baseTokenAmount":"0.0096894", + # "baseTokenCumulativeAmount":"0.0096894", + # "quoteTokenAmount":"0.000196114134258", + # "quoteTokenCumulativeAmount":"0.000196114134258" + # }, + # "ask":[ + # { + # "index":"-3", + # "price":"0.02024012", + # "baseTokenAmount":"0.005", + # "baseTokenCumulativeAmount":"0.005", + # "quoteTokenAmount":"0.0001012006", + # "quoteTokenCumulativeAmount":"0.0001012006" + # }, + # ] + # } + # + timestamp = self.parse8601(self.safe_string(response, 'timestamp')) + return self.parse_order_book(response, symbol, timestamp, 'bid', 'ask', 'price', 'baseTokenAmount') + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://plasma-relay-backend.timex.io/swagger-ui/index.html?urls.primaryName=Relay#/Public/listTrades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + options = self.safe_value(self.options, 'fetchTrades', {}) + defaultSort = self.safe_value(options, 'sort', 'timestamp,asc') + sort = self.safe_string(params, 'sort', defaultSort) + query = self.omit(params, 'sort') + request: dict = { + # 'address': 'string', # trade’s member account(?) + # 'cursor': 1234, # int64(?) + # 'from': self.iso8601(since), + 'market': market['id'], + # 'page': 0, # results page you want to retrieve 0 .. N + # 'size': limit, # number of records per page, 100 by default + 'sort': sort, # array[string], sorting criteria in the format "property,asc" or "property,desc", default is ascending + # 'till': self.iso8601(self.milliseconds()), + } + if since is not None: + request['from'] = self.iso8601(since) + if limit is not None: + request['size'] = limit # default is 100 + response = self.publicGetTrades(self.extend(request, query)) + # + # [ + # { + # "id":1, + # "timestamp":"2019-06-25T17:01:50.309", + # "direction":"BUY", + # "price":"0.027", + # "quantity":"0.001" + # } + # ] + # + return self.parse_trades(response, market, since, limit) + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://plasma-relay-backend.timex.io/swagger-ui/index.html?urls.primaryName=Relay#/Public/listCandles + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest candle to fetch + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + 'period': self.safe_string(self.timeframes, timeframe, timeframe), + } + # if since and limit are not specified + duration = self.parse_timeframe(timeframe) + until = self.safe_integer(params, 'until') + if limit is None: + limit = 1000 # exchange provides tens of thousands of data, but we set generous default value + if since is not None: + request['from'] = self.iso8601(since) + if until is None: + request['till'] = self.iso8601(self.sum(since, self.sum(limit, 1) * duration * 1000)) + else: + request['till'] = self.iso8601(until) + elif until is not None: + request['till'] = self.iso8601(until) + fromTimestamp = until - self.sum(limit, 1) * duration * 1000 + request['from'] = self.iso8601(fromTimestamp) + else: + now = self.milliseconds() + request['till'] = self.iso8601(now) + request['from'] = self.iso8601(now - self.sum(limit, 1) * duration * 1000 - 1) + params = self.omit(params, 'until') + response = self.publicGetCandles(self.extend(request, params)) + # + # [ + # { + # "timestamp":"2019-12-04T23:00:00", + # "open":"0.02024009", + # "high":"0.02024009", + # "low":"0.02024009", + # "close":"0.02024009", + # "volume":"0.00008096036", + # "volumeQuote":"0.004", + # }, + # ] + # + return self.parse_ohlcvs(response, market, timeframe, since, limit) + + def parse_balance(self, response) -> Balances: + result: dict = { + 'info': response, + 'timestamp': None, + 'datetime': None, + } + for i in range(0, len(response)): + balance = response[i] + currencyId = self.safe_string(balance, 'currency') + code = self.safe_currency_code(currencyId) + account = self.account() + account['total'] = self.safe_string(balance, 'totalBalance') + account['used'] = self.safe_string(balance, 'lockedBalance') + result[code] = account + return self.safe_balance(result) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://plasma-relay-backend.timex.io/swagger-ui/index.html?urls.primaryName=Relay#/Trading/getBalances + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + response = self.tradingGetBalances(params) + # + # [ + # {"currency":"BTC","totalBalance":"0","lockedBalance":"0"}, + # {"currency":"AUDT","totalBalance":"0","lockedBalance":"0"}, + # {"currency":"ETH","totalBalance":"0","lockedBalance":"0"}, + # {"currency":"TIME","totalBalance":"0","lockedBalance":"0"}, + # {"currency":"USDT","totalBalance":"0","lockedBalance":"0"} + # ] + # + return self.parse_balance(response) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://plasma-relay-backend.timex.io/swagger-ui/index.html?urls.primaryName=Relay#/Trading/createOrder + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + uppercaseSide = side.upper() + uppercaseType = type.upper() + postOnly = self.safe_bool(params, 'postOnly', False) + if postOnly: + uppercaseType = 'POST_ONLY' + params = self.omit(params, ['postOnly']) + request: dict = { + 'symbol': market['id'], + 'quantity': self.amount_to_precision(symbol, amount), + 'side': uppercaseSide, + 'orderTypes': uppercaseType, + # 'clientOrderId': '123', + # 'expireIn': 1575523308, # in seconds + # 'expireTime': 1575523308, # unix timestamp + } + query = params + if (uppercaseType == 'LIMIT') or (uppercaseType == 'POST_ONLY'): + request['price'] = self.price_to_precision(symbol, price) + defaultExpireIn = self.safe_integer(self.options, 'expireIn') + expireTime = self.safe_value(params, 'expireTime') + expireIn = self.safe_value(params, 'expireIn', defaultExpireIn) + if expireTime is not None: + request['expireTime'] = expireTime + elif expireIn is not None: + request['expireIn'] = expireIn + else: + raise InvalidOrder(self.id + ' createOrder() method requires a expireTime or expireIn param for a ' + type + ' order, you can also set the expireIn exchange-wide option') + query = self.omit(params, ['expireTime', 'expireIn']) + else: + request['price'] = 0 + response = self.tradingPostOrders(self.extend(request, query)) + # + # { + # "orders": [ + # { + # "cancelledQuantity": "0.3", + # "clientOrderId": "my-order-1", + # "createdAt": "1970-01-01T00:00:00", + # "cursorId": 50, + # "expireTime": "1970-01-01T00:00:00", + # "filledQuantity": "0.3", + # "id": "string", + # "price": "0.017", + # "quantity": "0.3", + # "side": "BUY", + # "symbol": "TIMEETH", + # "type": "LIMIT", + # "updatedAt": "1970-01-01T00:00:00" + # } + # ] + # } + # + orders = self.safe_value(response, 'orders', []) + order = self.safe_dict(orders, 0, {}) + return self.parse_order(order, market) + + def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + self.load_markets() + market = self.market(symbol) + request: dict = { + 'id': id, + } + if amount is not None: + request['quantity'] = self.amount_to_precision(symbol, amount) + if price is not None: + request['price'] = self.price_to_precision(symbol, price) + response = self.tradingPutOrders(self.extend(request, params)) + # + # { + # "changedOrders": [ + # { + # "newOrder": { + # "cancelledQuantity": "0.3", + # "clientOrderId": "my-order-1", + # "createdAt": "1970-01-01T00:00:00", + # "cursorId": 50, + # "expireTime": "1970-01-01T00:00:00", + # "filledQuantity": "0.3", + # "id": "string", + # "price": "0.017", + # "quantity": "0.3", + # "side": "BUY", + # "symbol": "TIMEETH", + # "type": "LIMIT", + # "updatedAt": "1970-01-01T00:00:00" + # }, + # "oldId": "string", + # }, + # ], + # "unchangedOrders": ["string"], + # } + # + if 'unchangedOrders' in response: + orderIds = self.safe_value(response, 'unchangedOrders', []) + orderId = self.safe_string(orderIds, 0) + return self.safe_order({ + 'id': orderId, + 'info': response, + }) + orders = self.safe_value(response, 'changedOrders', []) + firstOrder = self.safe_value(orders, 0, {}) + order = self.safe_dict(firstOrder, 'newOrder', {}) + return self.parse_order(order, market) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://plasma-relay-backend.timex.io/swagger-ui/index.html?urls.primaryName=Relay#/Trading/deleteOrders + + :param str id: order id + :param str symbol: not used by timex cancelOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + orders = self.cancel_orders([id], symbol, params) + return self.safe_dict(orders, 0) + + def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): + """ + cancel multiple orders + + https://plasma-relay-backend.timex.io/swagger-ui/index.html?urls.primaryName=Relay#/Trading/deleteOrders + + :param str[] ids: order ids + :param str symbol: unified market symbol, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an list of `order structures ` + """ + self.load_markets() + request: dict = { + 'id': ids, + } + response = self.tradingDeleteOrders(self.extend(request, params)) + # + # { + # "changedOrders": [ + # { + # "newOrder": { + # "cancelledQuantity": "0.3", + # "clientOrderId": "my-order-1", + # "createdAt": "1970-01-01T00:00:00", + # "cursorId": 50, + # "expireTime": "1970-01-01T00:00:00", + # "filledQuantity": "0.3", + # "id": "string", + # "price": "0.017", + # "quantity": "0.3", + # "side": "BUY", + # "symbol": "TIMEETH", + # "type": "LIMIT", + # "updatedAt": "1970-01-01T00:00:00" + # }, + # "oldId": "string", + # }, + # ], + # "unchangedOrders": ["string"], + # } + # + changedOrders = self.safe_list(response, 'changedOrders', []) + unchangedOrders = self.safe_list(response, 'unchangedOrders', []) + orders = [] + for i in range(0, len(changedOrders)): + newOrder = self.safe_dict(changedOrders[i], 'newOrder') + orders.append(self.parse_order(newOrder)) + for i in range(0, len(unchangedOrders)): + orders.append(self.safe_order({ + 'info': unchangedOrders[i], + 'id': unchangedOrders[i], + 'status': 'unchanged', + })) + return orders + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://plasma-relay-backend.timex.io/swagger-ui/index.html?urls.primaryName=Relay#/History/getOrderDetails + + :param str id: order id + :param str symbol: not used by timex fetchOrder + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + request: dict = { + 'orderHash': id, + } + response = self.historyGetOrdersDetails(request) + # + # { + # "order": { + # "cancelledQuantity": "0.3", + # "clientOrderId": "my-order-1", + # "createdAt": "1970-01-01T00:00:00", + # "cursorId": 50, + # "expireTime": "1970-01-01T00:00:00", + # "filledQuantity": "0.3", + # "id": "string", + # "price": "0.017", + # "quantity": "0.3", + # "side": "BUY", + # "symbol": "TIMEETH", + # "type": "LIMIT", + # "updatedAt": "1970-01-01T00:00:00" + # }, + # "trades": [ + # { + # "fee": "0.3", + # "id": 100, + # "makerOrTaker": "MAKER", + # "makerOrderId": "string", + # "price": "0.017", + # "quantity": "0.3", + # "side": "BUY", + # "symbol": "TIMEETH", + # "takerOrderId": "string", + # "timestamp": "2019-12-05T07:48:26.310Z" + # } + # ] + # } + # + order = self.safe_value(response, 'order', {}) + trades = self.safe_list(response, 'trades', []) + return self.parse_order(self.extend(order, {'trades': trades})) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://plasma-relay-backend.timex.io/swagger-ui/index.html?urls.primaryName=Relay#/Trading/getOpenOrders + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + options = self.safe_value(self.options, 'fetchOpenOrders', {}) + defaultSort = self.safe_value(options, 'sort', 'createdAt,asc') + sort = self.safe_string(params, 'sort', defaultSort) + query = self.omit(params, 'sort') + request: dict = { + # 'clientOrderId': '123', # order’s client id list for filter + # page: 0, # results page you want to retrieve(0 .. N) + 'sort': sort, # sorting criteria in the format "property,asc" or "property,desc", default order is ascending, multiple sort criteria are supported + } + market: Market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if limit is not None: + request['size'] = limit + response = self.tradingGetOrders(self.extend(request, query)) + # + # { + # "orders": [ + # { + # "cancelledQuantity": "0.3", + # "clientOrderId": "my-order-1", + # "createdAt": "1970-01-01T00:00:00", + # "cursorId": 50, + # "expireTime": "1970-01-01T00:00:00", + # "filledQuantity": "0.3", + # "id": "string", + # "price": "0.017", + # "quantity": "0.3", + # "side": "BUY", + # "symbol": "TIMEETH", + # "type": "LIMIT", + # "updatedAt": "1970-01-01T00:00:00" + # } + # ] + # } + # + orders = self.safe_list(response, 'orders', []) + return self.parse_orders(orders, market, since, limit) + + def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://plasma-relay-backend.timex.io/swagger-ui/index.html?urls.primaryName=Relay#/History/getOrders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + options = self.safe_value(self.options, 'fetchClosedOrders', {}) + defaultSort = self.safe_value(options, 'sort', 'createdAt,asc') + sort = self.safe_string(params, 'sort', defaultSort) + query = self.omit(params, 'sort') + request: dict = { + # 'clientOrderId': '123', # order’s client id list for filter + # page: 0, # results page you want to retrieve(0 .. N) + 'sort': sort, # sorting criteria in the format "property,asc" or "property,desc", default order is ascending, multiple sort criteria are supported + 'side': 'BUY', # or 'SELL' + # 'till': self.iso8601(self.milliseconds()), + } + market: Market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if since is not None: + request['from'] = self.iso8601(since) + if limit is not None: + request['size'] = limit + response = self.historyGetOrders(self.extend(request, query)) + # + # { + # "orders": [ + # { + # "cancelledQuantity": "0.3", + # "clientOrderId": "my-order-1", + # "createdAt": "1970-01-01T00:00:00", + # "cursorId": 50, + # "expireTime": "1970-01-01T00:00:00", + # "filledQuantity": "0.3", + # "id": "string", + # "price": "0.017", + # "quantity": "0.3", + # "side": "BUY", + # "symbol": "TIMEETH", + # "type": "LIMIT", + # "updatedAt": "1970-01-01T00:00:00" + # } + # ] + # } + # + orders = self.safe_list(response, 'orders', []) + return self.parse_orders(orders, market, since, limit) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://plasma-relay-backend.timex.io/swagger-ui/index.html?urls.primaryName=Relay#/History/getTrades_1 + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + options = self.safe_value(self.options, 'fetchMyTrades', {}) + defaultSort = self.safe_value(options, 'sort', 'timestamp,asc') + sort = self.safe_string(params, 'sort', defaultSort) + query = self.omit(params, 'sort') + request: dict = { + # 'cursorId': 123, # int64(?) + # 'from': self.iso8601(since), + # 'makerOrderId': '1234', # maker order hash + # 'owner': '...', # owner address(?) + # 'page': 0, # results page you want to retrieve(0 .. N) + # 'side': 'BUY', # or 'SELL' + # 'size': limit, + 'sort': sort, # sorting criteria in the format "property,asc" or "property,desc", default order is ascending, multiple sort criteria are supported + # 'symbol': market['id'], + # 'takerOrderId': '1234', + # 'till': self.iso8601(self.milliseconds()), + } + market: Market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if since is not None: + request['from'] = self.iso8601(since) + if limit is not None: + request['size'] = limit + response = self.historyGetTrades(self.extend(request, query)) + # + # { + # "trades": [ + # { + # "fee": "0.3", + # "id": 100, + # "makerOrTaker": "MAKER", + # "makerOrderId": "string", + # "price": "0.017", + # "quantity": "0.3", + # "side": "BUY", + # "symbol": "TIMEETH", + # "takerOrderId": "string", + # "timestamp": "2019-12-08T04:54:11.171Z" + # } + # ] + # } + # + trades = self.safe_list(response, 'trades', []) + return self.parse_trades(trades, market, since, limit) + + def parse_trading_fee(self, fee: dict, market: Market = None) -> TradingFeeInterface: + # + # { + # "fee": 0.0075, + # "market": "ETHBTC" + # } + # + marketId = self.safe_string(fee, 'market') + rate = self.safe_number(fee, 'fee') + return { + 'info': fee, + 'symbol': self.safe_symbol(marketId, market), + 'maker': rate, + 'taker': rate, + 'percentage': None, + 'tierBased': None, + } + + def fetch_trading_fee(self, symbol: str, params={}) -> TradingFeeInterface: + """ + fetch the trading fees for a market + + https://plasma-relay-backend.timex.io/swagger-ui/index.html?urls.primaryName=Relay#/Trading/getFees + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `fee structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'markets': market['id'], + } + response = self.tradingGetFees(self.extend(request, params)) + # + # [ + # { + # "fee": 0.0075, + # "market": "ETHBTC" + # } + # ] + # + result = self.safe_value(response, 0, {}) + return self.parse_trading_fee(result, market) + + def parse_market(self, market: dict) -> Market: + # + # { + # "symbol": "ETHBTC", + # "name": "ETH/BTC", + # "baseCurrency": "ETH", + # "baseTokenAddress": "0x45932db54b38af1f5a57136302eeba66a5975c15", + # "quoteCurrency": "BTC", + # "quoteTokenAddress": "0x8370fbc6ddec1e18b4e41e72ed943e238458487c", + # "feeCurrency": "BTC", + # "feeTokenAddress": "0x8370fbc6ddec1e18b4e41e72ed943e238458487c", + # "quantityIncrement": "0.0000001", + # "takerFee": "0.005", + # "makerFee": "0.0025", + # "tickSize": "0.00000001", + # "baseMinSize": "0.0001", + # "quoteMinSize": "0.00001", + # "locked": False + # } + # + locked = self.safe_value(market, 'locked') + id = self.safe_string(market, 'symbol') + baseId = self.safe_string(market, 'baseCurrency') + quoteId = self.safe_string(market, 'quoteCurrency') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + amountIncrement = self.safe_string(market, 'quantityIncrement') + minBase = self.safe_string(market, 'baseMinSize') + minAmount = Precise.string_max(amountIncrement, minBase) + priceIncrement = self.safe_string(market, 'tickSize') + minCost = self.safe_number(market, 'quoteMinSize') + return { + 'id': id, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': not locked, + 'contract': False, + 'linear': None, + 'inverse': None, + 'taker': self.safe_number(market, 'takerFee'), + 'maker': self.safe_number(market, 'makerFee'), + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.safe_number(market, 'quantityIncrement'), + 'price': self.safe_number(market, 'tickSize'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.parse_number(minAmount), + 'max': None, + }, + 'price': { + 'min': self.parse_number(priceIncrement), + 'max': None, + }, + 'cost': { + 'min': minCost, + 'max': None, + }, + }, + 'created': None, + 'info': market, + } + + def parse_currency(self, currency: dict) -> Currency: + # + # { + # "symbol": "BTC", + # "name": "Bitcoin", + # "address": "0x8370fbc6ddec1e18b4e41e72ed943e238458487c", + # "icon": "data:image/svg+xml;base64,PHN2ZyB3aWR...mc+Cg==", + # "background": "transparent", + # "fiatSymbol": "BTC", + # "decimals": 8, + # "tradeDecimals": 20, + # "displayDecimals": 4, + # "crypto": True, + # "depositEnabled": True, + # "withdrawalEnabled": True, + # "transferEnabled": True, + # "buyEnabled": False, + # "purchaseEnabled": False, + # "redeemEnabled": False, + # "active": True, + # "withdrawalFee": "50000000000000000", + # "purchaseCommissions": [] + # } + # + # https://github.com/ccxt/ccxt/issues/6878 + # + # { + # "symbol":"XRP", + # "name":"Ripple", + # "address":"0x0dc8882914f3ddeebf4cec6dc20edb99df3def6c", + # "decimals":6, + # "tradeDecimals":16, + # "depositEnabled":true, + # "withdrawalEnabled":true, + # "transferEnabled":true, + # "active":true + # } + # + id = self.safe_string(currency, 'symbol') + code = self.safe_currency_code(id) + # fee = self.safe_number(currency, 'withdrawalFee') + feeString = self.safe_string(currency, 'withdrawalFee') + tradeDecimals = self.safe_integer(currency, 'tradeDecimals') + fee = None + if (feeString is not None) and (tradeDecimals is not None): + feeStringLen = len(feeString) + dotIndex = feeStringLen - tradeDecimals + if dotIndex > 0: + whole = feeString[0:dotIndex] + fraction = feeString[-dotIndex:] + fee = self.parse_number(whole + '.' + fraction) + else: + fraction = '.' + for i in range(0, -dotIndex): + fraction += '0' + fee = self.parse_number(fraction + feeString) + return self.safe_currency_structure({ + 'id': code, + 'code': code, + 'info': currency, + 'type': None, + 'name': self.safe_string(currency, 'name'), + 'active': self.safe_bool(currency, 'active'), + 'deposit': self.safe_bool(currency, 'depositEnabled'), + 'withdraw': self.safe_bool(currency, 'withdrawalEnabled'), + 'fee': fee, + 'precision': self.parse_number(self.parse_precision(self.safe_string(currency, 'decimals'))), + 'limits': { + 'withdraw': {'min': None, 'max': None}, + 'amount': {'min': None, 'max': None}, + }, + 'networks': {}, + }) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "ask": 0.017, + # "bid": 0.016, + # "high": 0.019, + # "last": 0.017, + # "low": 0.015, + # "market": "TIME/ETH", + # "open": 0.016, + # "period": "H1", + # "timestamp": "2018-12-14T20:50:36.134Z", + # "volume": 4.57, + # "volumeQuote": 0.07312 + # } + # + marketId = self.safe_string(ticker, 'market') + symbol = self.safe_symbol(marketId, market, '/') + timestamp = self.parse8601(self.safe_string(ticker, 'timestamp')) + last = self.safe_string(ticker, 'last') + open = self.safe_string(ticker, 'open') + return self.safe_ticker({ + 'symbol': symbol, + 'info': ticker, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': self.safe_string(ticker, 'bid'), + 'bidVolume': None, + 'ask': self.safe_string(ticker, 'ask'), + 'askVolume': None, + 'vwap': None, + 'open': open, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': self.safe_string(ticker, 'volume'), + 'quoteVolume': self.safe_string(ticker, 'volumeQuote'), + }, market) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(public) + # + # { + # "id":1, + # "timestamp":"2019-06-25T17:01:50.309", + # "direction":"BUY", + # "price":"0.027", + # "quantity":"0.001" + # } + # + # fetchMyTrades, fetchOrder(private) + # + # { + # "id": "7613414", + # "makerOrderId": "0x8420af060722f560098f786a2894d4358079b6ea5d14b395969ed77bc87a623a", + # "takerOrderId": "0x1235ef158a361815b54c9988b6241c85aedcbc1fe81caf8df8587d5ab0373d1a", + # "symbol": "LTCUSDT", + # "side": "BUY", + # "quantity": "0.2", + # "fee": "0.22685", + # "feeToken": "USDT", + # "price": "226.85", + # "makerOrTaker": "TAKER", + # "timestamp": "2021-04-09T15:39:45.608" + # } + # + marketId = self.safe_string(trade, 'symbol') + symbol = self.safe_symbol(marketId, market) + timestamp = self.parse8601(self.safe_string(trade, 'timestamp')) + priceString = self.safe_string(trade, 'price') + amountString = self.safe_string(trade, 'quantity') + price = self.parse_number(priceString) + amount = self.parse_number(amountString) + cost = self.parse_number(Precise.string_mul(priceString, amountString)) + id = self.safe_string(trade, 'id') + side = self.safe_string_lower_2(trade, 'direction', 'side') + takerOrMaker = self.safe_string_lower(trade, 'makerOrTaker') + orderId: Str = None + if takerOrMaker is not None: + orderId = self.safe_string(trade, takerOrMaker + 'OrderId') + fee = None + feeCost = self.safe_number(trade, 'fee') + feeCurrency = self.safe_currency_code(self.safe_string(trade, 'feeToken')) + if feeCost is not None: + fee = { + 'cost': feeCost, + 'currency': feeCurrency, + } + return self.safe_trade({ + 'info': trade, + 'id': id, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'order': orderId, + 'type': None, + 'side': side, + 'price': price, + 'amount': amount, + 'cost': cost, + 'takerOrMaker': takerOrMaker, + 'fee': fee, + }) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # { + # "timestamp":"2019-12-04T23:00:00", + # "open":"0.02024009", + # "high":"0.02024009", + # "low":"0.02024009", + # "close":"0.02024009", + # "volume":"0.00008096036", + # "volumeQuote":"0.004", + # } + # + return [ + self.parse8601(self.safe_string(ohlcv, 'timestamp')), + self.safe_number(ohlcv, 'open'), + self.safe_number(ohlcv, 'high'), + self.safe_number(ohlcv, 'low'), + self.safe_number(ohlcv, 'close'), + self.safe_number(ohlcv, 'volume'), + ] + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # fetchOrder, createOrder, cancelOrder, cancelOrders, fetchOpenOrders, fetchClosedOrders + # + # { + # "cancelledQuantity": "0.3", + # "clientOrderId": "my-order-1", + # "createdAt": "1970-01-01T00:00:00", + # "cursorId": 50, + # "expireTime": "1970-01-01T00:00:00", + # "filledQuantity": "0.3", + # "id": "string", + # "price": "0.017", + # "quantity": "0.3", + # "side": "BUY", + # "symbol": "TIMEETH", + # "type": "LIMIT", + # "updatedAt": "1970-01-01T00:00:00" + # "trades": [], # injected from the outside + # } + # + id = self.safe_string(order, 'id') + type = self.safe_string_lower(order, 'type') + side = self.safe_string_lower(order, 'side') + marketId = self.safe_string(order, 'symbol') + symbol = self.safe_symbol(marketId, market) + timestamp = self.parse8601(self.safe_string(order, 'createdAt')) + price = self.safe_string(order, 'price') + amount = self.safe_string(order, 'quantity') + filled = self.safe_string(order, 'filledQuantity') + canceledQuantity = self.omit_zero(self.safe_string(order, 'cancelledQuantity')) + status: str + if Precise.string_equals(filled, amount): + status = 'closed' + elif canceledQuantity is not None: + status = 'canceled' + else: + status = 'open' + rawTrades = self.safe_value(order, 'trades', []) + clientOrderId = self.safe_string(order, 'clientOrderId') + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'symbol': symbol, + 'type': type, + 'timeInForce': None, + 'postOnly': None, + 'side': side, + 'price': price, + 'triggerPrice': None, + 'amount': amount, + 'cost': None, + 'average': None, + 'filled': filled, + 'remaining': None, + 'status': status, + 'fee': None, + 'trades': rawTrades, + }, market) + + def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account, does not accept params["network"] + + https://plasma-relay-backend.timex.io/swagger-ui/index.html?urls.primaryName=Relay#/Currency/selectCurrencyBySymbol + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'symbol': currency['code'], + } + response = self.currenciesGetSSymbol(self.extend(request, params)) + # + # { + # id: '1', + # currency: { + # symbol: 'BTC', + # name: 'Bitcoin', + # address: '0x8370fbc6ddec1e18b4e41e72ed943e238458487c', + # decimals: '8', + # tradeDecimals: '20', + # fiatSymbol: 'BTC', + # depositEnabled: True, + # withdrawalEnabled: True, + # transferEnabled: True, + # active: True + # } + # } + # + data = self.safe_dict(response, 'currency', {}) + return self.parse_deposit_address(data, currency) + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + # + # { + # symbol: 'BTC', + # name: 'Bitcoin', + # address: '0x8370fbc6ddec1e18b4e41e72ed943e238458487c', + # decimals: '8', + # tradeDecimals: '20', + # fiatSymbol: 'BTC', + # depositEnabled: True, + # withdrawalEnabled: True, + # transferEnabled: True, + # active: True + # } + # + currencyId = self.safe_string(depositAddress, 'symbol') + return { + 'info': depositAddress, + 'currency': self.safe_currency_code(currencyId, currency), + 'network': None, + 'address': self.safe_string(depositAddress, 'address'), + 'tag': None, + } + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + paramsToExtract = self.extract_params(path) + path = self.implode_params(path, params) + params = self.omit(params, paramsToExtract) + url = self.urls['api']['rest'] + '/' + api + '/' + path + if params: + url += '?' + self.urlencode_with_array_repeat(params) + if api != 'public' and api != 'tradingview': + self.check_required_credentials() + auth = self.string_to_base64(self.apiKey + ':' + self.secret) + secret = 'Basic ' + auth + headers = {'authorization': secret} + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, statusCode: int, statusText: str, url: str, method: str, responseHeaders: dict, responseBody, response, requestHeaders, requestBody): + if response is None: + return None + if statusCode >= 400: + # + # {"error":{"timestamp":"05.12.2019T05:25:43.584+0000","status":"BAD_REQUEST","message":"Insufficient ETH balance. Required: 1, actual: 0.","code":4001}} + # {"error":{"timestamp":"05.12.2019T04:03:25.419+0000","status":"FORBIDDEN","message":"Access denied","code":4300}} + # + feedback = self.id + ' ' + responseBody + error = self.safe_value(response, 'error') + if error is None: + error = response + code = self.safe_string_2(error, 'code', 'status') + message = self.safe_string_2(error, 'message', 'debugMessage') + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], code, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) + raise ExchangeError(feedback) + return None diff --git a/ccxt/tokocrypto.py b/ccxt/tokocrypto.py new file mode 100644 index 0000000..53f0404 --- /dev/null +++ b/ccxt/tokocrypto.py @@ -0,0 +1,2525 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.tokocrypto import ImplicitAPI +import hashlib +import json +from ccxt.base.types import Any, Balances, Currency, DepositAddress, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import AccountSuspended +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import MarginModeAlreadySet +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import OrderImmediatelyFillable +from ccxt.base.errors import OrderNotFillable +from ccxt.base.errors import NotSupported +from ccxt.base.errors import DDoSProtection +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.errors import OnMaintenance +from ccxt.base.errors import InvalidNonce +from ccxt.base.errors import RequestTimeout +from ccxt.base.errors import BadResponse +from ccxt.base.decimal_to_precision import TRUNCATE +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class tokocrypto(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(tokocrypto, self).describe(), { + 'id': 'tokocrypto', + 'name': 'Tokocrypto', + 'countries': ['ID'], # Indonesia + 'certified': False, + 'pro': False, + 'version': 'v1', + # new metainfo interface + 'has': { + 'CORS': None, + 'spot': True, + 'margin': True, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': None, + 'borrowMargin': None, + 'cancelAllOrders': False, + 'cancelOrder': True, + 'cancelOrders': None, + 'createDepositAddress': False, + 'createMarketBuyOrderWithCost': True, + 'createMarketOrderWithCost': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createReduceOnlyOrder': None, + 'createStopLimitOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'fetchAccounts': False, + 'fetchBalance': True, + 'fetchBidsAsks': True, + 'fetchBorrowInterest': None, + 'fetchBorrowRateHistories': None, + 'fetchBorrowRateHistory': None, + 'fetchCanceledOrders': False, + 'fetchClosedOrder': False, + 'fetchClosedOrders': 'emulated', + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': False, + 'fetchDeposit': False, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchDepositsWithdrawals': False, + 'fetchFundingHistory': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchL3OrderBook': False, + 'fetchLedger': None, + 'fetchLeverage': False, + 'fetchLeverageTiers': False, + 'fetchMarketLeverageTiers': 'emulated', + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterestHistory': False, + 'fetchOpenOrder': False, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrderBooks': False, + 'fetchOrders': True, + 'fetchOrderTrades': False, + 'fetchPosition': False, + 'fetchPositions': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchStatus': False, + 'fetchTicker': False, + 'fetchTickers': False, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': False, + 'fetchTradingLimits': False, + 'fetchTransactionFee': False, + 'fetchTransactionFees': False, + 'fetchTransactions': False, + 'fetchTransfers': False, + 'fetchWithdrawal': False, + 'fetchWithdrawals': True, + 'fetchWithdrawalWhitelist': False, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'signIn': False, + 'transfer': False, + 'withdraw': True, + }, + 'timeframes': { + '1m': '1m', + '3m': '3m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1h', + '2h': '2h', + '4h': '4h', + '6h': '6h', + '8h': '8h', + '12h': '12h', + '1d': '1d', + '3d': '3d', + '1w': '1w', + '1M': '1M', + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/183870484-d3398d0c-f6a1-4cce-91b8-d58792308716.jpg', + 'api': { + 'rest': { + 'public': 'https://www.tokocrypto.com', + 'binance': 'https://api.binance.com/api/v3', + 'private': 'https://www.tokocrypto.com', + }, + }, + 'www': 'https://tokocrypto.com', + # 'referral': 'https://www.binance.us/?ref=35005074', + 'doc': 'https://www.tokocrypto.com/apidocs/', + 'fees': 'https://www.tokocrypto.com/fees/newschedule', + }, + 'api': { + 'binance': { + 'get': { + 'ping': 1, + 'time': 1, + 'depth': {'cost': 1, 'byLimit': [[100, 1], [500, 5], [1000, 10], [5000, 50]]}, + 'trades': 1, + 'aggTrades': 1, + 'historicalTrades': 5, + 'klines': 1, + 'ticker/24hr': {'cost': 1, 'noSymbol': 40}, + 'ticker/price': {'cost': 1, 'noSymbol': 2}, + 'ticker/bookTicker': {'cost': 1, 'noSymbol': 2}, + 'exchangeInfo': 10, + }, + 'put': { + 'userDataStream': 1, + }, + 'post': { + 'userDataStream': 1, + }, + 'delete': { + 'userDataStream': 1, + }, + }, + 'public': { + 'get': { + 'open/v1/common/time': 1, + 'open/v1/common/symbols': 1, + # all the actual symbols are type 1 + 'open/v1/market/depth': 1, # when symbol type is not 1 + 'open/v1/market/trades': 1, # when symbol type is not 1 + 'open/v1/market/agg-trades': 1, # when symbol type is not 1 + 'open/v1/market/klines': 1, # when symbol type is not 1 + }, + }, + 'private': { + 'get': { + 'open/v1/orders/detail': 1, + 'open/v1/orders': 1, + 'open/v1/account/spot': 1, + 'open/v1/account/spot/asset': 1, + 'open/v1/orders/trades': 1, + 'open/v1/withdraws': 1, + 'open/v1/deposits': 1, + 'open/v1/deposits/address': 1, + }, + 'post': { + 'open/v1/orders': 1, + 'open/v1/orders/cancel': 1, + 'open/v1/orders/oco': 1, + 'open/v1/withdraws': 1, + 'open/v1/user-data-stream': 1, + }, + }, + }, + 'fees': { + 'trading': { + 'tierBased': True, + 'percentage': True, + 'taker': self.parse_number('0.0075'), # 0.1% trading fee, zero fees for all trading pairs before November 1 + 'maker': self.parse_number('0.0075'), # 0.1% trading fee, zero fees for all trading pairs before November 1 + }, + }, + 'precisionMode': TICK_SIZE, + 'options': { + # 'fetchTradesMethod': 'binanceGetTrades', # binanceGetTrades, binanceGetAggTrades + 'createMarketBuyOrderRequiresPrice': True, + 'defaultTimeInForce': 'GTC', # 'GTC' = Good To Cancel(default), 'IOC' = Immediate Or Cancel + # 'defaultType': 'spot', # 'spot', 'future', 'margin', 'delivery' + 'hasAlreadyAuthenticatedSuccessfully': False, + 'warnOnFetchOpenOrdersWithoutSymbol': True, + # 'fetchPositions': 'positionRisk', # or 'account' + 'recvWindow': 5 * 1000, # 5 sec, binance default + 'timeDifference': 0, # the difference between system clock and Binance clock + 'adjustForTimeDifference': False, # controls the adjustment logic upon instantiation + 'newOrderRespType': { + 'market': 'FULL', # 'ACK' for order id, 'RESULT' for full order or 'FULL' for order with fills + 'limit': 'FULL', # we change it from 'ACK' by default to 'FULL'(returns immediately if limit is not hit) + }, + 'quoteOrderQty': False, # whether market orders support amounts in quote currency + 'networks': { + 'ERC20': 'ETH', + 'TRC20': 'TRX', + 'BEP2': 'BNB', + 'BEP20': 'BSC', + 'OMNI': 'OMNI', + 'EOS': 'EOS', + 'SPL': 'SOL', + }, + 'reverseNetworks': { + 'tronscan.org': 'TRC20', + 'etherscan.io': 'ERC20', + 'bscscan.com': 'BSC', + 'explorer.binance.org': 'BEP2', + 'bithomp.com': 'XRP', + 'bloks.io': 'EOS', + 'stellar.expert': 'XLM', + 'blockchair.com/bitcoin': 'BTC', + 'blockchair.com/bitcoin-cash': 'BCH', + 'blockchair.com/ecash': 'XEC', + 'explorer.litecoin.net': 'LTC', + 'explorer.avax.network': 'AVAX', + 'solscan.io': 'SOL', + 'polkadot.subscan.io': 'DOT', + 'dashboard.internetcomputer.org': 'ICP', + 'explorer.chiliz.com': 'CHZ', + 'cardanoscan.io': 'ADA', + 'mainnet.theoan.com': 'AION', + 'algoexplorer.io': 'ALGO', + 'explorer.ambrosus.com': 'AMB', + 'viewblock.io/zilliqa': 'ZIL', + 'viewblock.io/arweave': 'AR', + 'explorer.ark.io': 'ARK', + 'atomscan.com': 'ATOM', + 'www.mintscan.io': 'CTK', + 'explorer.bitcoindiamond.org': 'BCD', + 'btgexplorer.com': 'BTG', + 'bts.ai': 'BTS', + 'explorer.celo.org': 'CELO', + 'explorer.nervos.org': 'CKB', + 'cerebro.cortexlabs.ai': 'CTXC', + 'chainz.cryptoid.info': 'VIA', + 'explorer.dcrdata.org': 'DCR', + 'digiexplorer.info': 'DGB', + 'dock.subscan.io': 'DOCK', + 'dogechain.info': 'DOGE', + 'explorer.elrond.com': 'EGLD', + 'blockscout.com': 'ETC', + 'explore-fetchhub.fetch.ai': 'FET', + 'filfox.info': 'FIL', + 'fio.bloks.io': 'FIO', + 'explorer.firo.org': 'FIRO', + 'neoscan.io': 'NEO', + 'ftmscan.com': 'FTM', + 'explorer.gochain.io': 'GO', + 'block.gxb.io': 'GXS', + 'hash-hash.info': 'HBAR', + 'www.hiveblockexplorer.com': 'HIVE', + 'explorer.helium.com': 'HNT', + 'tracker.icon.foundation': 'ICX', + 'www.iostabc.com': 'IOST', + 'explorer.iota.org': 'IOTA', + 'iotexscan.io': 'IOTX', + 'irishub.iobscan.io': 'IRIS', + 'kava.mintscan.io': 'KAVA', + 'scope.klaytn.com': 'KLAY', + 'kmdexplorer.io': 'KMD', + 'kusama.subscan.io': 'KSM', + 'explorer.lto.network': 'LTO', + 'polygonscan.com': 'POLYGON', + 'explorer.ont.io': 'ONT', + 'minaexplorer.com': 'MINA', + 'nanolooker.com': 'NANO', + 'explorer.nebulas.io': 'NAS', + 'explorer.nbs.plus': 'NBS', + 'explorer.nebl.io': 'NEBL', + 'nulscan.io': 'NULS', + 'nxscan.com': 'NXS', + 'explorer.harmony.one': 'ONE', + 'explorer.poa.network': 'POA', + 'qtum.info': 'QTUM', + 'explorer.rsk.co': 'RSK', + 'www.oasisscan.com': 'ROSE', + 'ravencoin.network': 'RVN', + 'sc.tokenview.com': 'SC', + 'secretnodes.com': 'SCRT', + 'explorer.skycoin.com': 'SKY', + 'steemscan.com': 'STEEM', + 'explorer.stacks.co': 'STX', + 'www.thetascan.io': 'THETA', + 'scan.tomochain.com': 'TOMO', + 'explore.vechain.org': 'VET', + 'explorer.vite.net': 'VITE', + 'www.wanscan.org': 'WAN', + 'wavesexplorer.com': 'WAVES', + 'wax.eosx.io': 'WAXP', + 'waltonchain.pro': 'WTC', + 'chain.nem.ninja': 'XEM', + 'verge-blockchain.info': 'XVG', + 'explorer.yoyow.org': 'YOYOW', + 'explorer.zcha.in': 'ZEC', + 'explorer.zensystem.io': 'ZEN', + }, + 'impliedNetworks': { + 'ETH': {'ERC20': 'ETH'}, + 'TRX': {'TRC20': 'TRX'}, + }, + 'legalMoney': { + 'MXN': True, + 'UGX': True, + 'SEK': True, + 'CHF': True, + 'VND': True, + 'AED': True, + 'DKK': True, + 'KZT': True, + 'HUF': True, + 'PEN': True, + 'PHP': True, + 'USD': True, + 'TRY': True, + 'EUR': True, + 'NGN': True, + 'PLN': True, + 'BRL': True, + 'ZAR': True, + 'KES': True, + 'ARS': True, + 'RUB': True, + 'AUD': True, + 'NOK': True, + 'CZK': True, + 'GBP': True, + 'UAH': True, + 'GHS': True, + 'HKD': True, + 'CAD': True, + 'INR': True, + 'JPY': True, + 'NZD': True, + }, + }, + # https://binance-docs.github.io/apidocs/spot/en/#error-codes-2 + 'exceptions': { + 'exact': { + 'System is under maintenance.': OnMaintenance, # {"code":1,"msg":"System is under maintenance."} + 'System abnormality': ExchangeError, # {"code":-1000,"msg":"System abnormality"} + 'You are not authorized to execute self request.': PermissionDenied, # {"msg":"You are not authorized to execute self request."} + 'API key does not exist': AuthenticationError, + 'Order would trigger immediately.': OrderImmediatelyFillable, + 'Stop price would trigger immediately.': OrderImmediatelyFillable, # {"code":-2010,"msg":"Stop price would trigger immediately."} + 'Order would immediately match and take.': OrderImmediatelyFillable, # {"code":-2010,"msg":"Order would immediately match and take."} + 'Account has insufficient balance for requested action.': InsufficientFunds, + 'Rest API trading is not enabled.': ExchangeNotAvailable, + "You don't have permission.": PermissionDenied, # {"msg":"You don't have permission.","success":false} + 'Market is closed.': ExchangeNotAvailable, # {"code":-1013,"msg":"Market is closed."} + 'Too many requests. Please try again later.': DDoSProtection, # {"msg":"Too many requests. Please try again later.","success":false} + 'This action disabled is on self account.': AccountSuspended, # {"code":-2010,"msg":"This action disabled is on self account."} + '-1000': ExchangeNotAvailable, # {"code":-1000,"msg":"An unknown error occured while processing the request."} + '-1001': ExchangeNotAvailable, # {"code":-1001,"msg":"'Internal error; unable to process your request. Please try again.'"} + '-1002': AuthenticationError, # {"code":-1002,"msg":"'You are not authorized to execute self request.'"} + '-1003': RateLimitExceeded, # {"code":-1003,"msg":"Too much request weight used, current limit is 1200 request weight per 1 MINUTE. Please use the websocket for live updates to avoid polling the API."} + '-1004': DDoSProtection, # {"code":-1004,"msg":"Server is busy, please wait and try again"} + '-1005': PermissionDenied, # {"code":-1005,"msg":"No such IP has been white listed"} + '-1006': BadResponse, # {"code":-1006,"msg":"An unexpected response was received from the message bus. Execution status unknown."} + '-1007': RequestTimeout, # {"code":-1007,"msg":"Timeout waiting for response from backend server. Send status unknown; execution status unknown."} + '-1010': BadResponse, # {"code":-1010,"msg":"ERROR_MSG_RECEIVED."} + '-1011': PermissionDenied, # {"code":-1011,"msg":"This IP cannot access self route."} + '-1013': InvalidOrder, # {"code":-1013,"msg":"createOrder -> 'invalid quantity'/'invalid price'/MIN_NOTIONAL"} + '-1014': InvalidOrder, # {"code":-1014,"msg":"Unsupported order combination."} + '-1015': RateLimitExceeded, # {"code":-1015,"msg":"'Too many new orders; current limit is %s orders per %s.'"} + '-1016': ExchangeNotAvailable, # {"code":-1016,"msg":"'This service is no longer available.',"} + '-1020': BadRequest, # {"code":-1020,"msg":"'This operation is not supported.'"} + '-1021': InvalidNonce, # {"code":-1021,"msg":"'your time is ahead of server'"} + '-1022': AuthenticationError, # {"code":-1022,"msg":"Signature for self request is not valid."} + '-1023': BadRequest, # {"code":-1023,"msg":"Start time is greater than end time."} + '-1099': AuthenticationError, # {"code":-1099,"msg":"Not found, authenticated, or authorized"} + '-1100': BadRequest, # {"code":-1100,"msg":"createOrder(symbol, 1, asdf) -> 'Illegal characters found in parameter 'price'"} + '-1101': BadRequest, # {"code":-1101,"msg":"Too many parameters; expected %s and received %s."} + '-1102': BadRequest, # {"code":-1102,"msg":"Param %s or %s must be sent, but both were empty"} + '-1103': BadRequest, # {"code":-1103,"msg":"An unknown parameter was sent."} + '-1104': BadRequest, # {"code":-1104,"msg":"Not all sent parameters were read, read 8 parameters but was sent 9"} + '-1105': BadRequest, # {"code":-1105,"msg":"Parameter %s was empty."} + '-1106': BadRequest, # {"code":-1106,"msg":"Parameter %s sent when not required."} + '-1108': BadRequest, # {"code":-1108,"msg":"Invalid asset."} + '-1109': AuthenticationError, # {"code":-1109,"msg":"Invalid account."} + '-1110': BadRequest, # {"code":-1110,"msg":"Invalid symbolType."} + '-1111': BadRequest, # {"code":-1111,"msg":"Precision is over the maximum defined for self asset."} + '-1112': InvalidOrder, # {"code":-1112,"msg":"No orders on book for symbol."} + '-1113': BadRequest, # {"code":-1113,"msg":"Withdrawal amount must be negative."} + '-1114': BadRequest, # {"code":-1114,"msg":"TimeInForce parameter sent when not required."} + '-1115': BadRequest, # {"code":-1115,"msg":"Invalid timeInForce."} + '-1116': BadRequest, # {"code":-1116,"msg":"Invalid orderType."} + '-1117': BadRequest, # {"code":-1117,"msg":"Invalid side."} + '-1118': BadRequest, # {"code":-1118,"msg":"New client order ID was empty."} + '-1119': BadRequest, # {"code":-1119,"msg":"Original client order ID was empty."} + '-1120': BadRequest, # {"code":-1120,"msg":"Invalid interval."} + '-1121': BadSymbol, # {"code":-1121,"msg":"Invalid symbol."} + '-1125': AuthenticationError, # {"code":-1125,"msg":"This listenKey does not exist."} + '-1127': BadRequest, # {"code":-1127,"msg":"More than %s hours between startTime and endTime."} + '-1128': BadRequest, # {"code":-1128,"msg":"{"code":-1128,"msg":"Combination of optional parameters invalid."}"} + '-1130': BadRequest, # {"code":-1130,"msg":"Data sent for paramter %s is not valid."} + '-1131': BadRequest, # {"code":-1131,"msg":"recvWindow must be less than 60000"} + '-1136': BadRequest, # {"code":-1136,"msg":"Invalid newOrderRespType"} + '-2008': AuthenticationError, # {"code":-2008,"msg":"Invalid Api-Key ID."} + '-2010': ExchangeError, # {"code":-2010,"msg":"generic error code for createOrder -> 'Account has insufficient balance for requested action.', {"code":-2010,"msg":"Rest API trading is not enabled."}, etc..."} + '-2011': OrderNotFound, # {"code":-2011,"msg":"cancelOrder(1, 'BTC/USDT') -> 'UNKNOWN_ORDER'"} + '-2013': OrderNotFound, # {"code":-2013,"msg":"fetchOrder(1, 'BTC/USDT') -> 'Order does not exist'"} + '-2014': AuthenticationError, # {"code":-2014,"msg":"API-key format invalid."} + '-2015': AuthenticationError, # {"code":-2015,"msg":"Invalid API-key, IP, or permissions for action."} + '-2016': BadRequest, # {"code":-2016,"msg":"No trading window could be found for the symbol. Try ticker/24hrs instead."} + '-2018': InsufficientFunds, # {"code":-2018,"msg":"Balance is insufficient"} + '-2019': InsufficientFunds, # {"code":-2019,"msg":"Margin is insufficient."} + '-2020': OrderNotFillable, # {"code":-2020,"msg":"Unable to fill."} + '-2021': OrderImmediatelyFillable, # {"code":-2021,"msg":"Order would immediately trigger."} + '-2022': InvalidOrder, # {"code":-2022,"msg":"ReduceOnly Order is rejected."} + '-2023': InsufficientFunds, # {"code":-2023,"msg":"User in liquidation mode now."} + '-2024': InsufficientFunds, # {"code":-2024,"msg":"Position is not sufficient."} + '-2025': InvalidOrder, # {"code":-2025,"msg":"Reach max open order limit."} + '-2026': InvalidOrder, # {"code":-2026,"msg":"This OrderType is not supported when reduceOnly."} + '-2027': InvalidOrder, # {"code":-2027,"msg":"Exceeded the maximum allowable position at current leverage."} + '-2028': InsufficientFunds, # {"code":-2028,"msg":"Leverage is smaller than permitted: insufficient margin balance"} + '-3000': ExchangeError, # {"code":-3000,"msg":"Internal server error."} + '-3001': AuthenticationError, # {"code":-3001,"msg":"Please enable 2FA first."} + '-3002': BadSymbol, # {"code":-3002,"msg":"We don't have self asset."} + '-3003': BadRequest, # {"code":-3003,"msg":"Margin account does not exist."} + '-3004': ExchangeError, # {"code":-3004,"msg":"Trade not allowed."} + '-3005': InsufficientFunds, # {"code":-3005,"msg":"Transferring out not allowed. Transfer out amount exceeds max amount."} + '-3006': InsufficientFunds, # {"code":-3006,"msg":"Your borrow amount has exceed maximum borrow amount."} + '-3007': ExchangeError, # {"code":-3007,"msg":"You have pending transaction, please try again later.."} + '-3008': InsufficientFunds, # {"code":-3008,"msg":"Borrow not allowed. Your borrow amount has exceed maximum borrow amount."} + '-3009': BadRequest, # {"code":-3009,"msg":"This asset are not allowed to transfer into margin account currently."} + '-3010': ExchangeError, # {"code":-3010,"msg":"Repay not allowed. Repay amount exceeds borrow amount."} + '-3011': BadRequest, # {"code":-3011,"msg":"Your input date is invalid."} + '-3012': ExchangeError, # {"code":-3012,"msg":"Borrow is banned for self asset."} + '-3013': BadRequest, # {"code":-3013,"msg":"Borrow amount less than minimum borrow amount."} + '-3014': AccountSuspended, # {"code":-3014,"msg":"Borrow is banned for self account."} + '-3015': ExchangeError, # {"code":-3015,"msg":"Repay amount exceeds borrow amount."} + '-3016': BadRequest, # {"code":-3016,"msg":"Repay amount less than minimum repay amount."} + '-3017': ExchangeError, # {"code":-3017,"msg":"This asset are not allowed to transfer into margin account currently."} + '-3018': AccountSuspended, # {"code":-3018,"msg":"Transferring in has been banned for self account."} + '-3019': AccountSuspended, # {"code":-3019,"msg":"Transferring out has been banned for self account."} + '-3020': InsufficientFunds, # {"code":-3020,"msg":"Transfer out amount exceeds max amount."} + '-3021': BadRequest, # {"code":-3021,"msg":"Margin account are not allowed to trade self trading pair."} + '-3022': AccountSuspended, # {"code":-3022,"msg":"You account's trading is banned."} + '-3023': BadRequest, # {"code":-3023,"msg":"You can't transfer out/place order under current margin level."} + '-3024': ExchangeError, # {"code":-3024,"msg":"The unpaid debt is too small after self repayment."} + '-3025': BadRequest, # {"code":-3025,"msg":"Your input date is invalid."} + '-3026': BadRequest, # {"code":-3026,"msg":"Your input param is invalid."} + '-3027': BadSymbol, # {"code":-3027,"msg":"Not a valid margin asset."} + '-3028': BadSymbol, # {"code":-3028,"msg":"Not a valid margin pair."} + '-3029': ExchangeError, # {"code":-3029,"msg":"Transfer failed."} + '-3036': AccountSuspended, # {"code":-3036,"msg":"This account is not allowed to repay."} + '-3037': ExchangeError, # {"code":-3037,"msg":"PNL is clearing. Wait a second."} + '-3038': BadRequest, # {"code":-3038,"msg":"Listen key not found."} + '-3041': InsufficientFunds, # {"code":-3041,"msg":"Balance is not enough"} + '-3042': BadRequest, # {"code":-3042,"msg":"PriceIndex not available for self margin pair."} + '-3043': BadRequest, # {"code":-3043,"msg":"Transferring in not allowed."} + '-3044': DDoSProtection, # {"code":-3044,"msg":"System busy."} + '-3045': ExchangeError, # {"code":-3045,"msg":"The system doesn't have enough asset now."} + '-3999': ExchangeError, # {"code":-3999,"msg":"This function is only available for invited users."} + '-4001': BadRequest, # {"code":-4001 ,"msg":"Invalid operation."} + '-4002': BadRequest, # {"code":-4002 ,"msg":"Invalid get."} + '-4003': BadRequest, # {"code":-4003 ,"msg":"Your input email is invalid."} + '-4004': AuthenticationError, # {"code":-4004,"msg":"You don't login or auth."} + '-4005': RateLimitExceeded, # {"code":-4005 ,"msg":"Too many new requests."} + '-4006': BadRequest, # {"code":-4006 ,"msg":"Support main account only."} + '-4007': BadRequest, # {"code":-4007 ,"msg":"Address validation is not passed."} + '-4008': BadRequest, # {"code":-4008 ,"msg":"Address tag validation is not passed."} + '-4010': BadRequest, # {"code":-4010 ,"msg":"White list mail has been confirmed."} # [TODO] possible bug: it should probably be "has not been confirmed" + '-4011': BadRequest, # {"code":-4011 ,"msg":"White list mail is invalid."} + '-4012': BadRequest, # {"code":-4012 ,"msg":"White list is not opened."} + '-4013': AuthenticationError, # {"code":-4013 ,"msg":"2FA is not opened."} + '-4014': PermissionDenied, # {"code":-4014 ,"msg":"Withdraw is not allowed within 2 min login."} + '-4015': ExchangeError, # {"code":-4015 ,"msg":"Withdraw is limited."} + '-4016': PermissionDenied, # {"code":-4016 ,"msg":"Within 24 hours after password modification, withdrawal is prohibited."} + '-4017': PermissionDenied, # {"code":-4017 ,"msg":"Within 24 hours after the release of 2FA, withdrawal is prohibited."} + '-4018': BadSymbol, # {"code":-4018,"msg":"We don't have self asset."} + '-4019': BadSymbol, # {"code":-4019,"msg":"Current asset is not open for withdrawal."} + '-4021': BadRequest, # {"code":-4021,"msg":"Asset withdrawal must be an %s multiple of %s."} + '-4022': BadRequest, # {"code":-4022,"msg":"Not less than the minimum pick-up quantity %s."} + '-4023': ExchangeError, # {"code":-4023,"msg":"Within 24 hours, the withdrawal exceeds the maximum amount."} + '-4024': InsufficientFunds, # {"code":-4024,"msg":"You don't have self asset."} + '-4025': InsufficientFunds, # {"code":-4025,"msg":"The number of hold asset is less than zero."} + '-4026': InsufficientFunds, # {"code":-4026,"msg":"You have insufficient balance."} + '-4027': ExchangeError, # {"code":-4027,"msg":"Failed to obtain tranId."} + '-4028': BadRequest, # {"code":-4028,"msg":"The amount of withdrawal must be greater than the Commission."} + '-4029': BadRequest, # {"code":-4029,"msg":"The withdrawal record does not exist."} + '-4030': ExchangeError, # {"code":-4030,"msg":"Confirmation of successful asset withdrawal. [TODO] possible bug in docs"} + '-4031': ExchangeError, # {"code":-4031,"msg":"Cancellation failed."} + '-4032': ExchangeError, # {"code":-4032,"msg":"Withdraw verification exception."} + '-4033': BadRequest, # {"code":-4033,"msg":"Illegal address."} + '-4034': ExchangeError, # {"code":-4034,"msg":"The address is suspected of fake."} + '-4035': PermissionDenied, # {"code":-4035,"msg":"This address is not on the whitelist. Please join and try again."} + '-4036': BadRequest, # {"code":-4036,"msg":"The new address needs to be withdrawn in {0} hours."} + '-4037': ExchangeError, # {"code":-4037,"msg":"Re-sending Mail failed."} + '-4038': ExchangeError, # {"code":-4038,"msg":"Please try again in 5 minutes."} + '-4039': BadRequest, # {"code":-4039,"msg":"The user does not exist."} + '-4040': BadRequest, # {"code":-4040,"msg":"This address not charged."} + '-4041': ExchangeError, # {"code":-4041,"msg":"Please try again in one minute."} + '-4042': ExchangeError, # {"code":-4042,"msg":"This asset cannot get deposit address again."} + '-4043': BadRequest, # {"code":-4043,"msg":"More than 100 recharge addresses were used in 24 hours."} + '-4044': BadRequest, # {"code":-4044,"msg":"This is a blacklist country."} + '-4045': ExchangeError, # {"code":-4045,"msg":"Failure to acquire assets."} + '-4046': AuthenticationError, # {"code":-4046,"msg":"Agreement not confirmed."} + '-4047': BadRequest, # {"code":-4047,"msg":"Time interval must be within 0-90 days"} + '-5001': BadRequest, # {"code":-5001,"msg":"Don't allow transfer to micro assets."} + '-5002': InsufficientFunds, # {"code":-5002,"msg":"You have insufficient balance."} + '-5003': InsufficientFunds, # {"code":-5003,"msg":"You don't have self asset."} + '-5004': BadRequest, # {"code":-5004,"msg":"The residual balances of %s have exceeded 0.001BTC, Please re-choose."} + '-5005': InsufficientFunds, # {"code":-5005,"msg":"The residual balances of %s is too low, Please re-choose."} + '-5006': BadRequest, # {"code":-5006,"msg":"Only transfer once in 24 hours."} + '-5007': BadRequest, # {"code":-5007,"msg":"Quantity must be greater than zero."} + '-5008': InsufficientFunds, # {"code":-5008,"msg":"Insufficient amount of returnable assets."} + '-5009': BadRequest, # {"code":-5009,"msg":"Product does not exist."} + '-5010': ExchangeError, # {"code":-5010,"msg":"Asset transfer fail."} + '-5011': BadRequest, # {"code":-5011,"msg":"future account not exists."} + '-5012': ExchangeError, # {"code":-5012,"msg":"Asset transfer is in pending."} + '-5013': InsufficientFunds, # {"code":-5013,"msg":"Asset transfer failed: insufficient balance""} # undocumented + '-5021': BadRequest, # {"code":-5021,"msg":"This parent sub have no relation"} + '-6001': BadRequest, # {"code":-6001,"msg":"Daily product not exists."} + '-6003': BadRequest, # {"code":-6003,"msg":"Product not exist or you don't have permission"} + '-6004': ExchangeError, # {"code":-6004,"msg":"Product not in purchase status"} + '-6005': InvalidOrder, # {"code":-6005,"msg":"Smaller than min purchase limit"} + '-6006': BadRequest, # {"code":-6006,"msg":"Redeem amount error"} + '-6007': BadRequest, # {"code":-6007,"msg":"Not in redeem time"} + '-6008': BadRequest, # {"code":-6008,"msg":"Product not in redeem status"} + '-6009': RateLimitExceeded, # {"code":-6009,"msg":"Request frequency too high"} + '-6011': BadRequest, # {"code":-6011,"msg":"Exceeding the maximum num allowed to purchase per user"} + '-6012': InsufficientFunds, # {"code":-6012,"msg":"Balance not enough"} + '-6013': ExchangeError, # {"code":-6013,"msg":"Purchasing failed"} + '-6014': BadRequest, # {"code":-6014,"msg":"Exceed up-limit allowed to purchased"} + '-6015': BadRequest, # {"code":-6015,"msg":"Empty request body"} + '-6016': BadRequest, # {"code":-6016,"msg":"Parameter err"} + '-6017': BadRequest, # {"code":-6017,"msg":"Not in whitelist"} + '-6018': BadRequest, # {"code":-6018,"msg":"Asset not enough"} + '-6019': AuthenticationError, # {"code":-6019,"msg":"Need confirm"} + '-6020': BadRequest, # {"code":-6020,"msg":"Project not exists"} + '-7001': BadRequest, # {"code":-7001,"msg":"Date range is not supported."} + '-7002': BadRequest, # {"code":-7002,"msg":"Data request type is not supported."} + '-9000': InsufficientFunds, # {"code":-9000,"msg":"user have no avaliable amount"}" + '-10017': BadRequest, # {"code":-10017,"msg":"Repay amount should not be larger than liability."} + '-11008': InsufficientFunds, # {"code":-11008,"msg":"Exceeding the account's maximum borrowable limit."} # undocumented + '-12014': RateLimitExceeded, # {"code":-12014,"msg":"More than 1 request in 3 seconds"} + '-13000': BadRequest, # {"code":-13000,"msg":"Redeption of the token is forbiden now"} + '-13001': BadRequest, # {"code":-13001,"msg":"Exceeds individual 24h redemption limit of the token"} + '-13002': BadRequest, # {"code":-13002,"msg":"Exceeds total 24h redemption limit of the token"} + '-13003': BadRequest, # {"code":-13003,"msg":"Subscription of the token is forbiden now"} + '-13004': BadRequest, # {"code":-13004,"msg":"Exceeds individual 24h subscription limit of the token"} + '-13005': BadRequest, # {"code":-13005,"msg":"Exceeds total 24h subscription limit of the token"} + '-13006': InvalidOrder, # {"code":-13006,"msg":"Subscription amount is too small"} + '-13007': AuthenticationError, # {"code":-13007,"msg":"The Agreement is not signed"} + '-21001': BadRequest, # {"code":-21001,"msg":"USER_IS_NOT_UNIACCOUNT"} + '-21002': BadRequest, # {"code":-21002,"msg":"UNI_ACCOUNT_CANT_TRANSFER_FUTURE"} + '-21003': BadRequest, # {"code":-21003,"msg":"NET_ASSET_MUST_LTE_RATIO"} + '100001003': BadRequest, # {"code":100001003,"msg":"Verification failed"} # undocumented + '2202': InsufficientFunds, # {"code":2202,"msg":"Insufficient balance","data":{"code":-2010,"msg":"Account has insufficient balance for requested action."},"timestamp":1662733681161} + '3210': InvalidOrder, # {"code":3210,"msg":"The total volume is too low","data":{"code":-1013,"msg":"Filter failure: MIN_NOTIONAL"},"timestamp":1662734704462} + '3203': InvalidOrder, # {"code":3203,"msg":"Incorrect Order Quantity","timestamp":1662734809758} + '3211': InvalidOrder, # {"code":3211,"msg":"The total volume must be greater than 10","timestamp":1662739358179} + '3207': InvalidOrder, # {"code":3207,"msg":"The price cannot be lower than 12.18","timestamp":1662739502856} + '3218': OrderNotFound, # {"code":3218,"msg":"Order does not exist","timestamp":1662739749275} + }, + 'broad': { + 'has no operation privilege': PermissionDenied, + 'MAX_POSITION': InvalidOrder, # {"code":-2010,"msg":"Filter failure: MAX_POSITION"} + }, + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerDirection': False, + 'triggerPriceType': None, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': True, + 'selfTradePrevention': True, # todo + 'iceberg': True, # todo + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 100000, # todo + 'untilDays': 100000, # todo + 'symbolRequired': True, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 1000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 100000, + 'untilDays': 100000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 100000, # todo + 'daysBackCanceled': 1, # todo + 'untilDays': 100000, # todo + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOHLCV': { + 'limit': 1000, + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + }) + + def nonce(self): + return self.milliseconds() - self.options['timeDifference'] + + def fetch_time(self, params={}) -> Int: + """ + + https://www.tokocrypto.com/apidocs/#check-server-time + + fetches the current integer timestamp in milliseconds from the exchange server + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = self.publicGetOpenV1CommonTime(params) + # + # { + # "code": 0, + # "msg": "Success", + # "data": null, + # "timestamp": 1737378074159 + # } + # + return self.safe_integer(response, 'timestamp') + + def fetch_markets(self, params={}) -> List[Market]: + """ + + https://www.tokocrypto.com/apidocs/#get-all-supported-trading-symbol + + retrieves data on all markets for tokocrypto + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = self.publicGetOpenV1CommonSymbols(params) + # + # { + # "code":0, + # "msg":"Success", + # "data":{ + # "list":[ + # { + # "type":1, + # "symbol":"1INCH_BTC", + # "baseAsset":"1INCH", + # "basePrecision":8, + # "quoteAsset":"BTC", + # "quotePrecision":8, + # "filters":[ + # {"filterType":"PRICE_FILTER","minPrice":"0.00000001","maxPrice":"1000.00000000","tickSize":"0.00000001","applyToMarket":false}, + # {"filterType":"PERCENT_PRICE","multiplierUp":5,"multiplierDown":0.2,"avgPriceMins":"5","applyToMarket":false}, + # {"filterType":"LOT_SIZE","minQty":"0.10000000","maxQty":"90000000.00000000","stepSize":"0.10000000","applyToMarket":false}, + # {"filterType":"MIN_NOTIONAL","avgPriceMins":"5","minNotional":"0.00010000","applyToMarket":true}, + # {"filterType":"ICEBERG_PARTS","applyToMarket":false,"limit":"10"}, + # {"filterType":"MARKET_LOT_SIZE","minQty":"0.00000000","maxQty":"79460.14117231","stepSize":"0.00000000","applyToMarket":false}, + # {"filterType":"TRAILING_DELTA","applyToMarket":false}, + # {"filterType":"MAX_NUM_ORDERS","applyToMarket":false}, + # {"filterType":"MAX_NUM_ALGO_ORDERS","applyToMarket":false,"maxNumAlgoOrders":"5"} + # ], + # "orderTypes":["LIMIT","LIMIT_MAKER","MARKET","STOP_LOSS_LIMIT","TAKE_PROFIT_LIMIT"], + # "icebergEnable":1, + # "ocoEnable":1, + # "spotTradingEnable":1, + # "marginTradingEnable":1, + # "permissions":["SPOT","MARGIN"] + # }, + # ] + # }, + # "timestamp":1659492212507 + # } + # + if self.options['adjustForTimeDifference']: + self.load_time_difference() + data = self.safe_value(response, 'data', {}) + list = self.safe_value(data, 'list', []) + result = [] + for i in range(0, len(list)): + market = list[i] + baseId = self.safe_string(market, 'baseAsset') + quoteId = self.safe_string(market, 'quoteAsset') + id = self.safe_string(market, 'symbol') + lowercaseId = self.safe_string_lower(market, 'symbol') + settleId = self.safe_string(market, 'marginAsset') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + settle = self.safe_currency_code(settleId) + symbol = base + '/' + quote + filters = self.safe_value(market, 'filters', []) + filtersByType = self.index_by(filters, 'filterType') + status = self.safe_string(market, 'spotTradingEnable') + active = (status == '1') + permissions = self.safe_value(market, 'permissions', []) + for j in range(0, len(permissions)): + if permissions[j] == 'TRD_GRP_003': + active = False + break + isMarginTradingAllowed = self.safe_bool(market, 'isMarginTradingAllowed', False) + entry: dict = { + 'id': id, + 'lowercaseId': lowercaseId, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': 'spot', + 'spot': True, + 'margin': isMarginTradingAllowed, + 'swap': False, + 'future': False, + 'delivery': False, + 'option': False, + 'active': active, + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(self.parse_precision(self.safe_string(market, 'quantityPrecision'))), + 'price': self.parse_number(self.parse_precision(self.safe_string(market, 'pricePrecision'))), + 'base': self.parse_number(self.parse_precision(self.safe_string(market, 'baseAssetPrecision'))), + 'quote': self.parse_number(self.parse_precision(self.safe_string(market, 'quotePrecision'))), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': None, + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + 'info': market, + } + if 'PRICE_FILTER' in filtersByType: + filter = self.safe_value(filtersByType, 'PRICE_FILTER', {}) + entry['precision']['price'] = self.safe_number(filter, 'tickSize') + # PRICE_FILTER reports zero values for maxPrice + # since they updated filter types in November 2018 + # https://github.com/ccxt/ccxt/issues/4286 + # therefore limits['price']['max'] doesn't have any meaningful value except None + entry['limits']['price'] = { + 'min': self.safe_number(filter, 'minPrice'), + 'max': self.safe_number(filter, 'maxPrice'), + } + entry['precision']['price'] = filter['tickSize'] + if 'LOT_SIZE' in filtersByType: + filter = self.safe_value(filtersByType, 'LOT_SIZE', {}) + entry['precision']['amount'] = self.safe_number(filter, 'stepSize') + entry['limits']['amount'] = { + 'min': self.safe_number(filter, 'minQty'), + 'max': self.safe_number(filter, 'maxQty'), + } + if 'MARKET_LOT_SIZE' in filtersByType: + filter = self.safe_value(filtersByType, 'MARKET_LOT_SIZE', {}) + entry['limits']['market'] = { + 'min': self.safe_number(filter, 'minQty'), + 'max': self.safe_number(filter, 'maxQty'), + } + if 'MIN_NOTIONAL' in filtersByType: + filter = self.safe_value(filtersByType, 'MIN_NOTIONAL', {}) + entry['limits']['cost']['min'] = self.safe_number_2(filter, 'minNotional', 'notional') + result.append(entry) + return result + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + + https://www.tokocrypto.com/apidocs/#order-book + + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = {} + if limit is not None: + request['limit'] = limit # default 100, max 5000, see https://github.com/binance/binance-spot-api-docs/blob/master/rest-api.md#order-book + response = None + if market['quote'] == 'USDT': + request['symbol'] = market['baseId'] + market['quoteId'] + response = self.binanceGetDepth(self.extend(request, params)) + else: + request['symbol'] = market['id'] + response = self.publicGetOpenV1MarketDepth(self.extend(request, params)) + # + # future + # + # { + # "lastUpdateId":333598053905, + # "E":1618631511986, + # "T":1618631511964, + # "bids":[ + # ["2493.56","20.189"], + # ["2493.54","1.000"], + # ["2493.51","0.005"] + # ], + # "asks":[ + # ["2493.57","0.877"], + # ["2493.62","0.063"], + # ["2493.71","12.054"], + # ] + # } + # type not 1 + # { + # "code":0, + # "msg":"Success", + # "data":{ + # "lastUpdateId":3204783, + # "bids":[], + # "asks": [] + # }, + # "timestamp":1692262634599 + # } + data = self.safe_value(response, 'data', response) + timestamp = self.safe_integer_2(response, 'T', 'timestamp') + orderbook = self.parse_order_book(data, symbol, timestamp) + orderbook['nonce'] = self.safe_integer(data, 'lastUpdateId') + return orderbook + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # aggregate trades + # https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md#compressedaggregate-trades-list + # + # { + # "a": 26129, # Aggregate tradeId + # "p": "0.01633102", # Price + # "q": "4.70443515", # Quantity + # "f": 27781, # First tradeId + # "l": 27781, # Last tradeId + # "T": 1498793709153, # Timestamp + # "m": True, # Was the buyer the maker? + # "M": True # Was the trade the best price match? + # } + # + # recent public trades and old public trades + # https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md#recent-trades-list + # https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md#old-trade-lookup-market_data + # + # { + # "id": 28457, + # "price": "4.00000100", + # "qty": "12.00000000", + # "time": 1499865549590, + # "isBuyerMaker": True, + # "isBestMatch": True + # } + # + # private trades + # https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md#account-trade-list-user_data + # + # { + # "symbol": "BNBBTC", + # "id": 28457, + # "orderId": 100234, + # "price": "4.00000100", + # "qty": "12.00000000", + # "commission": "10.10000000", + # "commissionAsset": "BNB", + # "time": 1499865549590, + # "isBuyer": True, + # "isMaker": False, + # "isBestMatch": True + # } + # + # futures trades + # https://binance-docs.github.io/apidocs/futures/en/#account-trade-list-user_data + # + # { + # "accountId": 20, + # "buyer": False, + # "commission": "-0.07819010", + # "commissionAsset": "USDT", + # "counterPartyId": 653, + # "id": 698759, + # "maker": False, + # "orderId": 25851813, + # "price": "7819.01", + # "qty": "0.002", + # "quoteQty": "0.01563", + # "realizedPnl": "-0.91539999", + # "side": "SELL", + # "symbol": "BTCUSDT", + # "time": 1569514978020 + # } + # { + # "symbol": "BTCUSDT", + # "id": 477128891, + # "orderId": 13809777875, + # "side": "SELL", + # "price": "38479.55", + # "qty": "0.001", + # "realizedPnl": "-0.00009534", + # "marginAsset": "USDT", + # "quoteQty": "38.47955", + # "commission": "-0.00076959", + # "commissionAsset": "USDT", + # "time": 1612733566708, + # "positionSide": "BOTH", + # "maker": True, + # "buyer": False + # } + # + # {respType: FULL} + # + # { + # "price": "4000.00000000", + # "qty": "1.00000000", + # "commission": "4.00000000", + # "commissionAsset": "USDT", + # "tradeId": "1234", + # } + # + timestamp = self.safe_integer_2(trade, 'T', 'time') + price = self.safe_string_2(trade, 'p', 'price') + amount = self.safe_string_2(trade, 'q', 'qty') + cost = self.safe_string_2(trade, 'quoteQty', 'baseQty') # inverse futures + marketId = self.safe_string(trade, 'symbol') + symbol = self.safe_symbol(marketId, market) + id = self.safe_string_2(trade, 't', 'a') + id = self.safe_string_2(trade, 'id', 'tradeId', id) + side: Str = None + orderId = self.safe_string(trade, 'orderId') + buyerMaker = self.safe_value_2(trade, 'm', 'isBuyerMaker') + takerOrMaker: Str = None + if buyerMaker is not None: + side = 'sell' if buyerMaker else 'buy' # self is reversed intentionally + takerOrMaker = 'taker' + elif 'side' in trade: + side = self.safe_string_lower(trade, 'side') + else: + if 'isBuyer' in trade: + side = 'buy' if trade['isBuyer'] else 'sell' # self is a True side + fee = None + if 'commission' in trade: + fee = { + 'cost': self.safe_string(trade, 'commission'), + 'currency': self.safe_currency_code(self.safe_string(trade, 'commissionAsset')), + } + if 'isMaker' in trade: + takerOrMaker = 'maker' if trade['isMaker'] else 'taker' + if 'maker' in trade: + takerOrMaker = 'maker' if trade['maker'] else 'taker' + return self.safe_trade({ + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'id': id, + 'order': orderId, + 'type': None, + 'side': side, + 'takerOrMaker': takerOrMaker, + 'price': price, + 'amount': amount, + 'cost': cost, + 'fee': fee, + }, market) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + + https://www.tokocrypto.com/apidocs/#recent-trades-list + https://www.tokocrypto.com/apidocs/#compressedaggregate-trades-list + + get the list of most recent trades for a particular symbol + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': self.get_market_id_by_type(market), + # 'fromId': 123, # ID to get aggregate trades from INCLUSIVE. + # 'startTime': 456, # Timestamp in ms to get aggregate trades from INCLUSIVE. + # 'endTime': 789, # Timestamp in ms to get aggregate trades until INCLUSIVE. + # 'limit': 500, # default = 500, maximum = 1000 + } + if market['quote'] != 'USDT': + if limit is not None: + request['limit'] = limit + responseInner = self.publicGetOpenV1MarketTrades(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "success", + # "data": { + # "list": [ + # { + # "id": 28457, + # "price": "4.00000100", + # "qty": "12.00000000", + # "time": 1499865549590, + # "isBuyerMaker": True, + # "isBestMatch": True + # } + # ] + # }, + # "timestamp": 1571921637091 + # } + # + data = self.safe_dict(responseInner, 'data', {}) + list = self.safe_list(data, 'list', []) + return self.parse_trades(list, market, since, limit) + if limit is not None: + request['limit'] = limit # default = 500, maximum = 1000 + defaultMethod = 'binanceGetTrades' + method = self.safe_string(self.options, 'fetchTradesMethod', defaultMethod) + response = None + if (method == 'binanceGetAggTrades') and (since is not None): + request['startTime'] = since + # https://github.com/ccxt/ccxt/issues/6400 + # https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md#compressedaggregate-trades-list + request['endTime'] = self.sum(since, 3600000) + response = self.binanceGetAggTrades(self.extend(request, params)) + else: + response = self.binanceGetTrades(self.extend(request, params)) + # + # Caveats: + # - default limit(500) applies only if no other parameters set, trades up + # to the maximum limit may be returned to satisfy other parameters + # - if both limit and time window is set and time window contains more + # trades than the limit then the last trades from the window are returned + # - 'tradeId' accepted and returned by self method is "aggregate" trade id + # which is different from actual trade id + # - setting both fromId and time window results in error + # + # aggregate trades + # + # [ + # { + # "a": 26129, # Aggregate tradeId + # "p": "0.01633102", # Price + # "q": "4.70443515", # Quantity + # "f": 27781, # First tradeId + # "l": 27781, # Last tradeId + # "T": 1498793709153, # Timestamp + # "m": True, # Was the buyer the maker? + # "M": True # Was the trade the best price match? + # } + # ] + # + # recent public trades and historical public trades + # + # [ + # { + # "id": 28457, + # "price": "4.00000100", + # "qty": "12.00000000", + # "time": 1499865549590, + # "isBuyerMaker": True, + # "isBestMatch": True + # } + # ] + # + return self.parse_trades(response, market, since, limit) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "symbol": "ETHBTC", + # "priceChange": "0.00068700", + # "priceChangePercent": "2.075", + # "weightedAvgPrice": "0.03342681", + # "prevClosePrice": "0.03310300", + # "lastPrice": "0.03378900", + # "lastQty": "0.07700000", + # "bidPrice": "0.03378900", + # "bidQty": "7.16800000", + # "askPrice": "0.03379000", + # "askQty": "24.00000000", + # "openPrice": "0.03310200", + # "highPrice": "0.03388900", + # "lowPrice": "0.03306900", + # "volume": "205478.41000000", + # "quoteVolume": "6868.48826294", + # "openTime": 1601469986932, + # "closeTime": 1601556386932, + # "firstId": 196098772, + # "lastId": 196186315, + # "count": 87544 + # } + # + # coinm + # { + # "baseVolume": "214549.95171161", + # "closeTime": "1621965286847", + # "count": "1283779", + # "firstId": "152560106", + # "highPrice": "39938.3", + # "lastId": "153843955", + # "lastPrice": "37993.4", + # "lastQty": "1", + # "lowPrice": "36457.2", + # "openPrice": "37783.4", + # "openTime": "1621878840000", + # "pair": "BTCUSD", + # "priceChange": "210.0", + # "priceChangePercent": "0.556", + # "symbol": "BTCUSD_PERP", + # "volume": "81990451", + # "weightedAvgPrice": "38215.08713747" + # } + # + timestamp = self.safe_integer(ticker, 'closeTime') + marketId = self.safe_string(ticker, 'symbol') + symbol = self.safe_symbol(marketId, market) + last = self.safe_string(ticker, 'lastPrice') + isCoinm = ('baseVolume' in ticker) + baseVolume = None + quoteVolume = None + if isCoinm: + baseVolume = self.safe_string(ticker, 'baseVolume') + quoteVolume = self.safe_string(ticker, 'volume') + else: + baseVolume = self.safe_string(ticker, 'volume') + quoteVolume = self.safe_string(ticker, 'quoteVolume') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'highPrice'), + 'low': self.safe_string(ticker, 'lowPrice'), + 'bid': self.safe_string(ticker, 'bidPrice'), + 'bidVolume': self.safe_string(ticker, 'bidQty'), + 'ask': self.safe_string(ticker, 'askPrice'), + 'askVolume': self.safe_string(ticker, 'askQty'), + 'vwap': self.safe_string(ticker, 'weightedAvgPrice'), + 'open': self.safe_string(ticker, 'openPrice'), + 'close': last, + 'last': last, + 'previousClose': self.safe_string(ticker, 'prevClosePrice'), # previous day close + 'change': self.safe_string(ticker, 'priceChange'), + 'percentage': self.safe_string(ticker, 'priceChangePercent'), + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'info': ticker, + }, market) + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + + https://binance-docs.github.io/apidocs/spot/en/#24hr-ticker-price-change-statistics + + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + response = self.binanceGetTicker24hr(params) + return self.parse_tickers(response, symbols) + + def get_market_id_by_type(self, market): + if market['quote'] == 'USDT': + return market['baseId'] + market['quoteId'] + return market['id'] + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + + https://binance-docs.github.io/apidocs/spot/en/#24hr-ticker-price-change-statistics + + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['baseId'] + market['quoteId'], + } + response = self.binanceGetTicker24hr(self.extend(request, params)) + if isinstance(response, list): + firstTicker = self.safe_dict(response, 0, {}) + return self.parse_ticker(firstTicker, market) + return self.parse_ticker(response, market) + + def fetch_bids_asks(self, symbols: Strings = None, params={}): + """ + + https://binance-docs.github.io/apidocs/spot/en/#symbol-order-book-ticker + + fetches the bid and ask price and volume for multiple markets + :param str[]|None symbols: unified symbols of the markets to fetch the bids and asks for, all markets are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + response = self.binanceGetTickerBookTicker(params) + return self.parse_tickers(response, symbols) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # when api method = publicGetKlines or fapiPublicGetKlines or dapiPublicGetKlines + # [ + # 1591478520000, # open time + # "0.02501300", # open + # "0.02501800", # high + # "0.02500000", # low + # "0.02500000", # close + # "22.19000000", # volume + # 1591478579999, # close time + # "0.55490906", # quote asset volume + # 40, # number of trades + # "10.92900000", # taker buy base asset volume + # "0.27336462", # taker buy quote asset volume + # "0" # ignore + # ] + # + # when api method = fapiPublicGetMarkPriceKlines or fapiPublicGetIndexPriceKlines + # [ + # [ + # 1591256460000, # Open time + # "9653.29201333", # Open + # "9654.56401333", # High + # "9653.07367333", # Low + # "9653.07367333", # Close(or latest price) + # "0", # Ignore + # 1591256519999, # Close time + # "0", # Ignore + # 60, # Number of bisic data + # "0", # Ignore + # "0", # Ignore + # "0" # Ignore + # ] + # ] + # + return [ + self.safe_integer(ohlcv, 0), + self.safe_number(ohlcv, 1), + self.safe_number(ohlcv, 2), + self.safe_number(ohlcv, 3), + self.safe_number(ohlcv, 4), + self.safe_number(ohlcv, 5), + ] + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + + https://binance-docs.github.io/apidocs/spot/en/#kline-candlestick-data + + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.price]: "mark" or "index" for mark price and index price candles + :param int [params.until]: timestamp in ms of the latest candle to fetch + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + market = self.market(symbol) + # binance docs say that the default limit 500, max 1500 for futures, max 1000 for spot markets + # the reality is that the time range wider than 500 candles won't work right + defaultLimit = 500 + maxLimit = 1500 + price = self.safe_string(params, 'price') + until = self.safe_integer(params, 'until') + params = self.omit(params, ['price', 'until']) + limit = defaultLimit if (limit is None) else min(limit, maxLimit) + request: dict = { + 'interval': self.safe_string(self.timeframes, timeframe, timeframe), + 'limit': limit, + } + if price == 'index': + request['pair'] = market['id'] # Index price takes self argument instead of symbol + else: + request['symbol'] = self.get_market_id_by_type(market) + # duration = self.parse_timeframe(timeframe) + if since is not None: + request['startTime'] = since + if until is not None: + request['endTime'] = until + response = None + if market['quote'] == 'USDT': + response = self.binanceGetKlines(self.extend(request, params)) + else: + response = self.publicGetOpenV1MarketKlines(self.extend(request, params)) + # + # [ + # [1591478520000,"0.02501300","0.02501800","0.02500000","0.02500000","22.19000000",1591478579999,"0.55490906",40,"10.92900000","0.27336462","0"], + # [1591478580000,"0.02499600","0.02500900","0.02499400","0.02500300","21.34700000",1591478639999,"0.53370468",24,"7.53800000","0.18850725","0"], + # [1591478640000,"0.02500800","0.02501100","0.02500300","0.02500800","154.14200000",1591478699999,"3.85405839",97,"5.32300000","0.13312641","0"], + # ] + # + data = self.safe_list(response, 'data', response) + return self.parse_ohlcvs(data, market, timeframe, since, limit) + + def fetch_balance(self, params={}) -> Balances: + """ + + https://www.tokocrypto.com/apidocs/#account-information-signed + + query for balance and get the amount of funds available for trading or funds locked in orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: 'future', 'delivery', 'savings', 'funding', or 'spot' + :param str [params.marginMode]: 'cross' or 'isolated', for margin trading, uses self.options.defaultMarginMode if not passed, defaults to None/None/None + :param str[]|None [params.symbols]: unified market symbols, only used in isolated margin mode + :returns dict: a `balance structure ` + """ + self.load_markets() + defaultType = self.safe_string_2(self.options, 'fetchBalance', 'defaultType', 'spot') + type = self.safe_string(params, 'type', defaultType) + defaultMarginMode = self.safe_string_2(self.options, 'marginMode', 'defaultMarginMode') + marginMode = self.safe_string_lower(params, 'marginMode', defaultMarginMode) + request: dict = {} + response = self.privateGetOpenV1AccountSpot(self.extend(request, params)) + # + # spot + # + # { + # "code":0, + # "msg":"Success", + # "data":{ + # "makerCommission":"0.00100000", + # "takerCommission":"0.00100000", + # "buyerCommission":"0.00000000", + # "sellerCommission":"0.00000000", + # "canTrade":1, + # "canWithdraw":1, + # "canDeposit":1, + # "status":1, + # "accountAssets":[ + # {"asset":"1INCH","free":"0","locked":"0"}, + # {"asset":"AAVE","free":"0","locked":"0"}, + # {"asset":"ACA","free":"0","locked":"0"} + # ], + # }, + # "timestamp":1659666786943 + # } + # + return self.parse_balance_custom(response, type, marginMode) + + def parse_balance_custom(self, response, type=None, marginMode=None): + timestamp = self.safe_integer(response, 'updateTime') + result: dict = { + 'info': response, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + data = self.safe_value(response, 'data', {}) + balances = self.safe_value(data, 'accountAssets', []) + for i in range(0, len(balances)): + balance = balances[i] + currencyId = self.safe_string(balance, 'asset') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(balance, 'free') + account['used'] = self.safe_string(balance, 'locked') + result[code] = account + return self.safe_balance(result) + + def parse_order_status(self, status: Str): + statuses: dict = { + '-2': 'open', + '0': 'open', # NEW + '1': 'open', # PARTIALLY_FILLED + '2': 'closed', # FILLED + '3': 'canceled', # CANCELED + '4': 'canceling', # PENDING_CANCEL(currently unused) + '5': 'rejected', # REJECTED + '6': 'expired', # EXPIRED + 'NEW': 'open', + 'PARTIALLY_FILLED': 'open', + 'FILLED': 'closed', + 'CANCELED': 'canceled', + 'PENDING_CANCEL': 'canceling', # currently unused + 'REJECTED': 'rejected', + 'EXPIRED': 'expired', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # spot + # + # { + # "symbol": "LTCBTC", + # "orderId": 1, + # "clientOrderId": "myOrder1", + # "price": "0.1", + # "origQty": "1.0", + # "executedQty": "0.0", + # "cummulativeQuoteQty": "0.0", + # "status": "NEW", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "side": "BUY", + # "stopPrice": "0.0", + # "icebergQty": "0.0", + # "time": 1499827319559, + # "updateTime": 1499827319559, + # "isWorking": True + # } + # createOrder + # { + # "orderId": "145265071", + # "bOrderListId": "0", + # "clientId": "49c09c3c2cd54419a59c05441f517b3c", + # "bOrderId": "35247529", + # "symbol": "USDT_BIDR", + # "symbolType": "1", + # "side": "0", + # "type": "1", + # "price": "11915", + # "origQty": "2", + # "origQuoteQty": "23830.00", + # "executedQty": "0.00000000", + # "executedPrice": "0", + # "executedQuoteQty": "0.00", + # "timeInForce": "1", + # "stopPrice": "0", + # "icebergQty": "0", + # "status": "0", + # "createTime": "1662711074372" + # } + # + # createOrder with {"newOrderRespType": "FULL"} + # + # { + # "symbol": "BTCUSDT", + # "orderId": 5403233939, + # "orderListId": -1, + # "clientOrderId": "x-R4BD3S825e669e75b6c14f69a2c43e", + # "transactTime": 1617151923742, + # "price": "0.00000000", + # "origQty": "0.00050000", + # "executedQty": "0.00050000", + # "cummulativeQuoteQty": "29.47081500", + # "status": "FILLED", + # "timeInForce": "GTC", + # "type": "MARKET", + # "side": "BUY", + # "fills": [ + # { + # "price": "58941.63000000", + # "qty": "0.00050000", + # "commission": "0.00007050", + # "commissionAsset": "BNB", + # "tradeId": 737466631 + # } + # ] + # } + # + # delivery + # + # { + # "orderId": "18742727411", + # "symbol": "ETHUSD_PERP", + # "pair": "ETHUSD", + # "status": "FILLED", + # "clientOrderId": "x-xcKtGhcu3e2d1503fdd543b3b02419", + # "price": "0", + # "avgPrice": "4522.14", + # "origQty": "1", + # "executedQty": "1", + # "cumBase": "0.00221134", + # "timeInForce": "GTC", + # "type": "MARKET", + # "reduceOnly": False, + # "closePosition": False, + # "side": "SELL", + # "positionSide": "BOTH", + # "stopPrice": "0", + # "workingType": "CONTRACT_PRICE", + # "priceProtect": False, + # "origType": "MARKET", + # "time": "1636061952660", + # "updateTime": "1636061952660" + # } + # + status = self.parse_order_status(self.safe_string(order, 'status')) + marketId = self.safe_string(order, 'symbol') + symbol = self.safe_symbol(marketId, market) + filled = self.safe_string(order, 'executedQty', '0') + timestamp = self.safe_integer(order, 'createTime') + average = self.safe_string(order, 'avgPrice') + price = self.safe_string_2(order, 'price', 'executedPrice') + amount = self.safe_string(order, 'origQty') + # - Spot/Margin market: cummulativeQuoteQty + # Note self is not the actual cost, since Binance futures uses leverage to calculate margins. + cost = self.safe_string_n(order, ['cummulativeQuoteQty', 'cumQuote', 'executedQuoteQty', 'cumBase']) + id = self.safe_string(order, 'orderId') + type = self.parse_order_type(self.safe_string_lower(order, 'type')) + side = self.safe_string_lower(order, 'side') + if side == '0': + side = 'buy' + elif side == '1': + side = 'sell' + fills = self.safe_value(order, 'fills', []) + clientOrderId = self.safe_string_2(order, 'clientOrderId', 'clientId') + timeInForce = self.safe_string(order, 'timeInForce') + if timeInForce == 'GTX': + # GTX means "Good Till Crossing" and is an equivalent way of saying Post Only + timeInForce = 'PO' + postOnly = (type == 'limit_maker') or (timeInForce == 'PO') + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'symbol': symbol, + 'type': type, + 'timeInForce': timeInForce, + 'postOnly': postOnly, + 'reduceOnly': self.safe_value(order, 'reduceOnly'), + 'side': side, + 'price': price, + 'triggerPrice': self.parse_number(self.omit_zero(self.safe_string(order, 'stopPrice'))), + 'amount': amount, + 'cost': cost, + 'average': average, + 'filled': filled, + 'remaining': None, + 'status': status, + 'fee': None, + 'trades': fills, + }, market) + + def parse_order_type(self, status): + statuses: dict = { + '2': 'market', + '1': 'limit', + '4': 'limit', + '7': 'limit', + } + return self.safe_string(statuses, status, status) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://www.tokocrypto.com/apidocs/#new-order--signed + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: the price at which a trigger order would be triggered + :param float [params.cost]: for spot market buy orders, the quote quantity that can be used alternative for the amount + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + clientOrderId = self.safe_string_2(params, 'clientOrderId', 'clientId') + postOnly = self.safe_bool(params, 'postOnly', False) + # only supported for spot/margin api + if postOnly: + type = 'LIMIT_MAKER' + params = self.omit(params, ['clientId', 'clientOrderId']) + initialUppercaseType = type.upper() + uppercaseType = initialUppercaseType + triggerPrice = self.safe_value_2(params, 'triggerPrice', 'stopPrice') + if triggerPrice is not None: + params = self.omit(params, ['triggerPrice', 'stopPrice']) + if uppercaseType == 'MARKET': + uppercaseType = 'STOP_LOSS' + elif uppercaseType == 'LIMIT': + uppercaseType = 'STOP_LOSS_LIMIT' + validOrderTypes = self.safe_value(market['info'], 'orderTypes') + if not self.in_array(uppercaseType, validOrderTypes): + if initialUppercaseType != uppercaseType: + raise InvalidOrder(self.id + ' triggerPrice parameter is not allowed for ' + symbol + ' ' + type + ' orders') + else: + raise InvalidOrder(self.id + ' ' + type + ' is not a valid order type for the ' + symbol + ' market') + reverseOrderTypeMapping: dict = { + 'LIMIT': 1, + 'MARKET': 2, + 'STOP_LOSS': 3, + 'STOP_LOSS_LIMIT': 4, + 'TAKE_PROFIT': 5, + 'TAKE_PROFIT_LIMIT': 6, + 'LIMIT_MAKER': 7, + } + request: dict = { + 'symbol': market['baseId'] + '_' + market['quoteId'], + 'type': self.safe_string(reverseOrderTypeMapping, uppercaseType), + } + if side == 'buy': + request['side'] = 0 + elif side == 'sell': + request['side'] = 1 + if clientOrderId is None: + broker = self.safe_value(self.options, 'broker') + if broker is not None: + brokerId = self.safe_string(broker, 'marketType') + if brokerId is not None: + request['clientId'] = brokerId + self.uuid22() + else: + request['clientId'] = clientOrderId + # additional required fields depending on the order type + priceIsRequired = False + triggerPriceIsRequired = False + quantityIsRequired = False + # + # spot/margin + # + # LIMIT timeInForce, quantity, price + # MARKET quantity or quoteOrderQty + # STOP_LOSS quantity, stopPrice + # STOP_LOSS_LIMIT timeInForce, quantity, price, stopPrice + # TAKE_PROFIT quantity, stopPrice + # TAKE_PROFIT_LIMIT timeInForce, quantity, price, stopPrice + # LIMIT_MAKER quantity, price + # + if uppercaseType == 'MARKET': + if side == 'buy': + precision = market['precision']['price'] + quoteAmount = None + createMarketBuyOrderRequiresPrice = True + createMarketBuyOrderRequiresPrice, params = self.handle_option_and_params(params, 'createOrder', 'createMarketBuyOrderRequiresPrice', True) + cost = self.safe_number_2(params, 'cost', 'quoteOrderQty') + params = self.omit(params, ['cost', 'quoteOrderQty']) + if cost is not None: + quoteAmount = cost + elif createMarketBuyOrderRequiresPrice: + if price is None: + raise InvalidOrder(self.id + ' createOrder() requires the price argument for market buy orders to calculate the total cost to spend(amount * price), alternatively set the createMarketBuyOrderRequiresPrice option or param to False and pass the cost to spend(quote quantity) in the amount argument') + else: + amountString = self.number_to_string(amount) + priceString = self.number_to_string(price) + quoteAmount = Precise.string_mul(amountString, priceString) + else: + quoteAmount = amount + request['quoteOrderQty'] = self.decimal_to_precision(quoteAmount, TRUNCATE, precision, self.precisionMode) + else: + quantityIsRequired = True + elif uppercaseType == 'LIMIT': + priceIsRequired = True + quantityIsRequired = True + elif (uppercaseType == 'STOP_LOSS') or (uppercaseType == 'TAKE_PROFIT'): + triggerPriceIsRequired = True + quantityIsRequired = True + if market['linear'] or market['inverse']: + priceIsRequired = True + elif (uppercaseType == 'STOP_LOSS_LIMIT') or (uppercaseType == 'TAKE_PROFIT_LIMIT'): + quantityIsRequired = True + triggerPriceIsRequired = True + priceIsRequired = True + elif uppercaseType == 'LIMIT_MAKER': + priceIsRequired = True + quantityIsRequired = True + if quantityIsRequired: + request['quantity'] = self.amount_to_precision(symbol, amount) + if priceIsRequired: + if price is None: + raise InvalidOrder(self.id + ' createOrder() requires a price argument for a ' + type + ' order') + request['price'] = self.price_to_precision(symbol, price) + if triggerPriceIsRequired: + if triggerPrice is None: + raise InvalidOrder(self.id + ' createOrder() requires a triggerPrice extra param for a ' + type + ' order') + else: + request['stopPrice'] = self.price_to_precision(symbol, triggerPrice) + response = self.privatePostOpenV1Orders(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "Success", + # "data": { + # "orderId": 145264846, + # "bOrderListId": 0, + # "clientId": "4ee2ab5e55e74b358eaf98079c670d17", + # "bOrderId": 35247499, + # "symbol": "USDT_BIDR", + # "symbolType": 1, + # "side": 0, + # "type": 1, + # "price": "11915", + # "origQty": "2", + # "origQuoteQty": "23830.00", + # "executedQty": "0.00000000", + # "executedPrice": "0", + # "executedQuoteQty": "0.00", + # "timeInForce": 1, + # "stopPrice": 0, + # "icebergQty": "0", + # "status": 0, + # "createTime": 1662710994848 + # }, + # "timestamp": 1662710994975 + # } + # + rawOrder = self.safe_dict(response, 'data', {}) + return self.parse_order(rawOrder, market) + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + + https://www.tokocrypto.com/apidocs/#query-order-signed + + fetches information on an order made by the user + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + request: dict = { + 'orderId': id, + } + response = self.privateGetOpenV1Orders(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "Success", + # "data": { + # "list": [{ + # "orderId": "145221985", + # "clientId": "201515331fd64d03aedbe687a38152e3", + # "bOrderId": "35239632", + # "bOrderListId": "0", + # "symbol": "USDT_BIDR", + # "symbolType": 1, + # "side": 0, + # "type": 1, + # "price": "11907", + # "origQty": "2", + # "origQuoteQty": "23814", + # "executedQty": "0", + # "executedPrice": "0", + # "executedQuoteQty": "0", + # "timeInForce": 1, + # "stopPrice": "0", + # "icebergQty": "0", + # "status": 0, + # "createTime": 1662699360000 + # }] + # }, + # "timestamp": 1662710056523 + # } + # + data = self.safe_value(response, 'data', {}) + list = self.safe_value(data, 'list', []) + rawOrder = self.safe_dict(list, 0, {}) + return self.parse_order(rawOrder) + + def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + + https://www.tokocrypto.com/apidocs/#all-orders-signed + + fetches information on multiple orders made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrders() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + # 'type': -1, # -1 = all, 1 = open, 2 = closed + # 'side': 1, # or 2 + # 'startTime': since, + # 'endTime': self.milliseconds(), + # 'fromId': 'starting order ID', # if defined, the "direct" field becomes mandatory + # 'direct': 'prev', # prev, next + # 'limit': 500, # default 500, max 1000 + } + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + response = self.privateGetOpenV1Orders(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "success", + # "data": { + # "list": [ + # { + # "orderId": "4", # order id + # "bOrderId": "100001", # binance order id + # "bOrderListId": -1, # Unless part of an OCO, the value will always be -1. + # "clientId": "1aa4f99ad7bc4fab903395afd25d0597", # client custom order id + # "symbol": "ADA_USDT", + # "symbolType": 1, + # "side": 1, + # "type": 1, + # "price": "0.1", + # "origQty": "10", + # "origQuoteQty": "1", + # "executedQty": "0", + # "executedPrice": "0", + # "executedQuoteQty": "0", + # "timeInForce": 1, + # "stopPrice": "0.0000000000000000", + # "icebergQty": "0.0000000000000000", + # "status": 0, + # "isWorking": 0, + # "createTime": 1572692016811 + # } + # ] + # }, + # "timestamp": 1572860756458 + # } + # + data = self.safe_value(response, 'data', {}) + orders = self.safe_list(data, 'list', []) + return self.parse_orders(orders, market, since, limit) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + + https://www.tokocrypto.com/apidocs/#all-orders-signed + + fetch all unfilled currently open orders + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + request: dict = {'type': 1} # -1 = all, 1 = open, 2 = closed + return self.fetch_orders(symbol, since, limit, self.extend(request, params)) + + def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + + https://www.tokocrypto.com/apidocs/#all-orders-signed + + fetches information on multiple closed orders made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + request: dict = {'type': 2} # -1 = all, 1 = open, 2 = closed + return self.fetch_orders(symbol, since, limit, self.extend(request, params)) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + + https://www.tokocrypto.com/apidocs/#cancel-order-signed + + cancels an open order + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + request: dict = { + 'orderId': id, + } + response = self.privatePostOpenV1OrdersCancel(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "Success", + # "data": { + # "orderId": "145221985", + # "bOrderListId": "0", + # "clientId": "201515331fd64d03aedbe687a38152e3", + # "bOrderId": "35239632", + # "symbol": "USDT_BIDR", + # "symbolType": 1, + # "type": 1, + # "side": 0, + # "price": "11907.0000000000000000", + # "origQty": "2.0000000000000000", + # "origQuoteQty": "23814.0000000000000000", + # "executedPrice": "0.0000000000000000", + # "executedQty": "0.00000000", + # "executedQuoteQty": "0.00", + # "timeInForce": 1, + # "stopPrice": "0.0000000000000000", + # "icebergQty": "0.0000000000000000", + # "status": 3 + # }, + # "timestamp": 1662710683634 + # } + # + rawOrder = self.safe_dict(response, 'data', {}) + return self.parse_order(rawOrder) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + + https://www.tokocrypto.com/apidocs/#account-trade-list-signed + + fetch all trades made by the user + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMyTrades() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + endTime = self.safe_integer_2(params, 'until', 'endTime') + if since is not None: + request['startTime'] = since + if endTime is not None: + request['endTime'] = endTime + params = self.omit(params, ['endTime', 'until']) + if limit is not None: + request['limit'] = limit + response = self.privateGetOpenV1OrdersTrades(self.extend(request, params)) + # + # { + # "code": 0, + # "msg": "success", + # "data": { + # "list": [ + # { + # "tradeId": "3", + # "orderId": "2", + # "symbol": "ADA_USDT", + # "price": "0.04398", + # "qty": "250", + # "quoteQty": "10.995", + # "commission": "0.25", + # "commissionAsset": "ADA", + # "isBuyer": 1, + # "isMaker": 0, + # "isBestMatch": 1, + # "time": "1572920872276" + # } + # ] + # }, + # "timestamp": 1573723498893 + # } + # + data = self.safe_value(response, 'data', {}) + trades = self.safe_list(data, 'list', []) + return self.parse_trades(trades, market, since, limit) + + def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://www.tokocrypto.com/apidocs/#deposit-address-signed + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'asset': currency['id'], + # 'network': 'ETH', # 'BSC', 'XMR', you can get network and isDefault in networkList in the response of sapiGetCapitalConfigDetail + } + networks = self.safe_value(self.options, 'networks', {}) + network = self.safe_string_upper(params, 'network') # self line allows the user to specify either ERC20 or ETH + network = self.safe_string(networks, network, network) # handle ERC20>ETH alias + if network is not None: + request['network'] = network + params = self.omit(params, 'network') + # has support for the 'network' parameter + # https://binance-docs.github.io/apidocs/spot/en/#deposit-address-supporting-network-user_data + response = self.privateGetOpenV1DepositsAddress(self.extend(request, params)) + # + # { + # "code":0, + # "msg":"Success", + # "data":{ + # "uid":"182395", + # "asset":"USDT", + # "network":"ETH", + # "address":"0x101a925704f6ff13295ab8dd7a60988d116aaedf", + # "addressTag":"", + # "status":1 + # }, + # "timestamp":1660685915746 + # } + # + data = self.safe_value(response, 'data', {}) + address = self.safe_string(data, 'address') + tag = self.safe_string(data, 'addressTag', '') + if len(tag) == 0: + tag = None + self.check_address(address) + return { + 'info': response, + 'currency': code, + 'network': self.safe_string(data, 'network'), + 'address': address, + 'tag': tag, + } + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + + https://www.tokocrypto.com/apidocs/#deposit-history-signed + + fetch all deposits made to an account + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch deposits for + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + currency = None + request: dict = {} + until = self.safe_integer(params, 'until') + if code is not None: + currency = self.currency(code) + request['coin'] = currency['id'] + if since is not None: + request['startTime'] = since + # max 3 months range https://github.com/ccxt/ccxt/issues/6495 + endTime = self.sum(since, 7776000000) + if until is not None: + endTime = min(endTime, until) + request['endTime'] = endTime + if limit is not None: + request['limit'] = limit + response = self.privateGetOpenV1Deposits(self.extend(request, params)) + # + # { + # "code":0, + # "msg":"Success", + # "data":{ + # "list":[ + # { + # "id":5167969, + # "asset":"BIDR", + # "network":"BSC", + # "address":"0x101a925704f6ff13295ab8dd7a60988d116aaedf", + # "addressTag":"", + # "txId":"113409337867", + # "amount":"15000", + # "transferType":1, + # "status":1, + # "insertTime":"1659429390000" + # }, + # ] + # }, + # "timestamp":1659758865998 + # } + # + data = self.safe_value(response, 'data', {}) + deposits = self.safe_list(data, 'list', []) + return self.parse_transactions(deposits, currency, since, limit) + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + + https://www.tokocrypto.com/apidocs/#withdraw-signed + + fetch all withdrawals made from an account + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + request: dict = {} + currency = None + if code is not None: + currency = self.currency(code) + request['coin'] = currency['id'] + if since is not None: + request['startTime'] = since + # max 3 months range https://github.com/ccxt/ccxt/issues/6495 + request['endTime'] = self.sum(since, 7776000000) + if limit is not None: + request['limit'] = limit + response = self.privateGetOpenV1Withdraws(self.extend(request, params)) + # + # { + # "code":0, + # "msg":"Success", + # "data":{ + # "list":[ + # { + # "id":4245859, + # "clientId":"198", + # "asset":"BIDR", + # "network":"BSC", + # "address":"0xff1c75149cc492e7d5566145b859fcafc900b6e9", + # "addressTag":"", + # "amount":"10000", + # "fee":"0", + # "txId":"113501794501", + # "transferType":1, + # "status":10, + # "createTime":1659521314413 + # } + # ] + # }, + # "timestamp":1659759062187 + # } + # + data = self.safe_value(response, 'data', {}) + withdrawals = self.safe_list(data, 'list', []) + return self.parse_transactions(withdrawals, currency, since, limit) + + def parse_transaction_status_by_type(self, status, type=None): + statusesByType: dict = { + 'deposit': { + '0': 'pending', + '1': 'ok', + }, + 'withdrawal': { + '0': 'pending', # Email Sent + '1': 'canceled', # Cancelled(different from 1 = ok in deposits) + '2': 'pending', # Awaiting Approval + '3': 'failed', # Rejected + '4': 'pending', # Processing + '5': 'failed', # Failure + '10': 'ok', # Completed + }, + } + statuses = self.safe_value(statusesByType, type, {}) + return self.safe_string(statuses, status, status) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fetchDeposits + # + # { + # "id": 5167969, + # "asset": "BIDR", + # "network": "BSC", + # "address": "0x101a925704f6ff13295ab8dd7a60988d116aaedf", + # "addressTag": "", + # "txId": "113409337867", + # "amount": "15000", + # "transferType": 1, + # "status": 1, + # "insertTime": "1659429390000" + # } + # + # fetchWithdrawals + # + # { + # "id": 4245859, + # "clientId": "198", + # "asset": "BIDR", + # "network": "BSC", + # "address": "0xff1c75149cc492e7d5566145b859fcafc900b6e9", + # "addressTag": "", + # "amount": "10000", + # "fee": "0", + # "txId": "113501794501", + # "transferType": 1, + # "status": 10, + # "createTime": 1659521314413 + # } + # + # withdraw + # + # { + # "code": 0, + # "msg": "成功", + # "data": { + # "withdrawId":"12" + # }, + # "timestamp": 1571745049095 + # } + # + address = self.safe_string(transaction, 'address') + tag = self.safe_string(transaction, 'addressTag') # set but unused + if tag is not None: + if len(tag) < 1: + tag = None + txid = self.safe_string(transaction, 'txId') + if (txid is not None) and (txid.find('Internal transfer ') >= 0): + txid = txid[18:] + currencyId = self.safe_string_2(transaction, 'coin', 'fiatCurrency') + code = self.safe_currency_code(currencyId, currency) + timestamp = None + insertTime = self.safe_integer(transaction, 'insertTime') + createTime = self.safe_integer_2(transaction, 'createTime', 'timestamp') + type = self.safe_string(transaction, 'type') + if type is None: + if (insertTime is not None) and (createTime is None): + type = 'deposit' + timestamp = insertTime + elif (insertTime is None) and (createTime is not None): + type = 'withdrawal' + timestamp = createTime + feeCost = self.safe_number_2(transaction, 'transactionFee', 'totalFee') + fee = { + 'currency': None, + 'cost': None, + 'rate': None, + } + if feeCost is not None: + fee['currency'] = code + fee['cost'] = feeCost + internalRaw = self.safe_integer(transaction, 'transferType') + internal = False + if internalRaw is not None: + internal = True + id = self.safe_string(transaction, 'id') + if id is None: + data = self.safe_value(transaction, 'data', {}) + id = self.safe_string(data, 'withdrawId') + type = 'withdrawal' + return { + 'info': transaction, + 'id': id, + 'txid': txid, + 'type': type, + 'currency': code, + 'network': self.safe_string(transaction, 'network'), + 'amount': self.safe_number(transaction, 'amount'), + 'status': self.parse_transaction_status_by_type(self.safe_string(transaction, 'status'), type), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'address': address, + 'addressFrom': None, + 'addressTo': address, + 'tag': tag, + 'tagFrom': None, + 'tagTo': tag, + 'updated': self.safe_integer_2(transaction, 'successTime', 'updateTime'), + 'comment': None, + 'internal': internal, + 'fee': fee, + } + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + + https://www.tokocrypto.com/apidocs/#withdraw-signed + + make a withdrawal + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.load_markets() + self.check_address(address) + currency = self.currency(code) + request: dict = { + 'asset': currency['id'], + # 'clientId': 'string', # # client's custom id for withdraw order, server does not check it's uniqueness, automatically generated if not sent + # 'network': 'string', + 'address': address, + # 'addressTag': 'string', # for coins like XRP, XMR, etc + 'amount': self.number_to_string(amount), + } + if tag is not None: + request['addressTag'] = tag + networkCode, query = self.handle_network_code_and_params(params) + networkId = self.network_code_to_id(networkCode) + if networkId is not None: + request['network'] = networkId.upper() + response = self.privatePostOpenV1Withdraws(self.extend(request, query)) + # + # { + # "code": 0, + # "msg": "成功", + # "data": { + # "withdrawId":"12" + # }, + # "timestamp": 1571745049095 + # } + # + return self.parse_transaction(response, currency) + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + if not (api in self.urls['api']['rest']): + raise NotSupported(self.id + ' does not have a testnet/sandbox URL for ' + api + ' endpoints') + url = self.urls['api']['rest'][api] + url += '/' + path + if api == 'wapi': + url += '.html' + userDataStream = (path == 'userDataStream') or (path == 'listenKey') + if userDataStream: + if self.apiKey: + # v1 special case for userDataStream + headers = { + 'X-MBX-APIKEY': self.apiKey, + 'Content-Type': 'application/x-www-form-urlencoded', + } + if method != 'GET': + body = self.urlencode(params) + else: + raise AuthenticationError(self.id + ' userDataStream endpoint requires `apiKey` credential') + elif (api == 'private') or (api == 'sapi' and path != 'system/status') or (api == 'sapiV3') or (api == 'wapi' and path != 'systemStatus') or (api == 'dapiPrivate') or (api == 'dapiPrivateV2') or (api == 'fapiPrivate') or (api == 'fapiPrivateV2'): + self.check_required_credentials() + query = None + defaultRecvWindow = self.safe_integer(self.options, 'recvWindow') + extendedParams = self.extend({ + 'timestamp': self.nonce(), + }, params) + if defaultRecvWindow is not None: + extendedParams['recvWindow'] = defaultRecvWindow + recvWindow = self.safe_integer(params, 'recvWindow') + if recvWindow is not None: + extendedParams['recvWindow'] = recvWindow + if (api == 'sapi') and (path == 'asset/dust'): + query = self.urlencode_with_array_repeat(extendedParams) + elif (path == 'batchOrders') or (path.find('sub-account') >= 0) or (path == 'capital/withdraw/apply') or (path.find('staking') >= 0): + query = self.rawencode(extendedParams) + else: + query = self.urlencode(extendedParams) + signature = self.hmac(self.encode(query), self.encode(self.secret), hashlib.sha256) + query += '&' + 'signature=' + signature + headers = { + 'X-MBX-APIKEY': self.apiKey, + } + if (method == 'GET') or (method == 'DELETE') or (api == 'wapi'): + url += '?' + query + else: + body = query + headers['Content-Type'] = 'application/x-www-form-urlencoded' + else: + if params: + url += '?' + self.urlencode(params) + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if (code == 418) or (code == 429): + raise DDoSProtection(self.id + ' ' + str(code) + ' ' + reason + ' ' + body) + # error response in a form: {"code": -1013, "msg": "Invalid quantity."} + # following block cointains legacy checks against message patterns in "msg" property + # will switch "code" checks eventually, when we know all of them + if code >= 400: + if body.find('Price * QTY is zero or less') >= 0: + raise InvalidOrder(self.id + ' order cost = amount * price is zero or less ' + body) + if body.find('LOT_SIZE') >= 0: + raise InvalidOrder(self.id + ' order amount should be evenly divisible by lot size ' + body) + if body.find('PRICE_FILTER') >= 0: + raise InvalidOrder(self.id + ' order price is invalid, i.e. exceeds allowed price precision, exceeds min price or max price limits or is invalid value in general, use self.price_to_precision(symbol, amount) ' + body) + if response is None: + return None # fallback to default error handler + # check success value for wapi endpoints + # response in format {'msg': 'The coin does not exist.', 'success': True/false} + success = self.safe_bool(response, 'success', True) + if not success: + messageInner = self.safe_string(response, 'msg') + parsedMessage = None + if messageInner is not None: + try: + parsedMessage = json.loads(messageInner) + except Exception as e: + # do nothing + parsedMessage = None + if parsedMessage is not None: + response = parsedMessage + message = self.safe_string(response, 'msg') + if message is not None: + self.throw_exactly_matched_exception(self.exceptions['exact'], message, self.id + ' ' + message) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, self.id + ' ' + message) + # checks against error codes + error = self.safe_string(response, 'code') + if error is not None: + # https://github.com/ccxt/ccxt/issues/6501 + # https://github.com/ccxt/ccxt/issues/7742 + if (error == '200') or Precise.string_equals(error, '0'): + return None + # a workaround for {"code":-2015,"msg":"Invalid API-key, IP, or permissions for action."} + # despite that their message is very confusing, it is raised by Binance + # on a temporary ban, the API key is valid, but disabled for a while + if (error == '-2015') and self.options['hasAlreadyAuthenticatedSuccessfully']: + raise DDoSProtection(self.id + ' ' + body) + feedback = self.id + ' ' + body + if message == 'No need to change margin type.': + # not an error + # https://github.com/ccxt/ccxt/issues/11268 + # https://github.com/ccxt/ccxt/pull/11624 + # POST https://fapi.binance.com/fapi/v1/marginType 400 Bad Request + # binanceusdm {"code":-4046,"msg":"No need to change margin type."} + raise MarginModeAlreadySet(feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], error, feedback) + raise ExchangeError(feedback) + if not success: + raise ExchangeError(self.id + ' ' + body) + return None + + def calculate_rate_limiter_cost(self, api, method, path, params, config={}): + if ('noCoin' in config) and not ('coin' in params): + return config['noCoin'] + elif ('noSymbol' in config) and not ('symbol' in params): + return config['noSymbol'] + elif ('noPoolId' in config) and not ('poolId' in params): + return config['noPoolId'] + elif ('byLimit' in config) and ('limit' in params): + limit = params['limit'] + byLimit = config['byLimit'] + for i in range(0, len(byLimit)): + entry = byLimit[i] + if limit <= entry[0]: + return entry[1] + return self.safe_integer(config, 'cost', 1) diff --git a/ccxt/toobit.py b/ccxt/toobit.py new file mode 100644 index 0000000..b13966b --- /dev/null +++ b/ccxt/toobit.py @@ -0,0 +1,2868 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.toobit import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currencies, Currency, DepositAddress, Int, LedgerEntry, Leverage, Market, Num, Order, OrderBook, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, FundingRates, Trade, TradingFees, Transaction, MarketInterface, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import OperationRejected +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import NotSupported +from ccxt.base.errors import OperationFailed +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class toobit(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(toobit, self).describe(), { + 'id': 'toobit', + 'name': 'Toobit', + 'countries': ['KY'], # Cayman Islands + 'version': 'v1', + 'rateLimit': 20, # 50 requests per second + 'certified': False, + 'pro': True, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': True, + 'future': False, + 'option': False, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'cancelOrders': True, + 'createOrder': True, + 'fetchBalance': True, + 'fetchBidsAsks': True, + 'fetchCurrencies': True, + 'fetchDepositAddress': True, + 'fetchDeposits': True, + 'fetchFundingRateHistory': True, + 'fetchFundingRates': True, + 'fetchIndexOHLCV': True, + 'fetchLastPrices': True, + 'fetchLedger': True, + 'fetchMarkets': True, + 'fetchMarkOHLCV': True, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchStatus': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchWithdrawals': True, + 'setMarginMode': True, + 'transfer': True, + 'withdraw': True, + }, + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/0c7a97d5-182c-492e-b921-23540c868e0e', + 'api': { + 'common': 'https://api.toobit.com', + 'private': 'https://api.toobit.com', + }, + 'www': 'https://www.toobit.com/', + 'doc': [ + 'https://toobit-docs.github.io/apidocs/spot/v1/en/', + 'https://toobit-docs.github.io/apidocs/usdt_swap/v1/en/', + ], + 'referral': { + 'url': 'https://www.toobit.com/en-US/r?i=IFFPy0', + 'discount': 0.1, + }, + 'fees': 'https://www.toobit.com/fee', + }, + 'api': { + 'common': { + 'get': { + 'api/v1/time': 1, + 'api/v1/ping': 1, + 'api/v1/exchangeInfo': 1, + 'quote/v1/depth': 1, # todo: by limit 1-10 + 'quote/v1/depth/merged': 1, + 'quote/v1/trades': 1, + 'quote/v1/klines': 1, + 'quote/v1/index/klines': 1, + 'quote/v1/markPrice/klines': 1, + 'quote/v1/markPrice': 1, + 'quote/v1/index': 1, + 'quote/v1/ticker/24hr': 40, # todo: 1-40 depenidng noSymbol + 'quote/v1/contract/ticker/24hr': 40, # todo: 1-40 depenidng noSymbol + 'quote/v1/ticker/price': 1, + 'quote/v1/ticker/bookTicker': 1, + 'api/v1/futures/fundingRate': 1, + 'api/v1/futures/historyFundingRate': 1, + }, + }, + 'private': { + 'get': { + 'api/v1/account': 5, + 'api/v1/account/checkApiKey': 1, + 'api/v1/spot/order': 1 * 1.67, + 'api/v1/spot/openOrders': 1 * 1.67, + 'api/v1/futures/openOrders': 1 * 1.67, + 'api/v1/spot/tradeOrders': 5 * 1.67, + 'api/v1/futures/historyOrders': 5 * 1.67, + 'api/v1/account/trades': 5 * 1.67, + 'api/v1/account/balanceFlow': 5, + 'api/v1/account/depositOrders': 5, + 'api/v1/account/withdrawOrders': 5, + 'api/v1/account/deposit/address': 1, + # contracts + 'api/v1/subAccount': 5, + 'api/v1/futures/accountLeverage': 1, + 'api/v1/futures/order': 1 * 1.67, + 'api/v1/futures/positions': 5 * 1.67, + 'api/v1/futures/balance': 5, + 'api/v1/futures/userTrades': 5 * 1.67, + 'api/v1/futures/balanceFlow': 5, + 'api/v1/futures/commissionRate': 5, + 'api/v1/futures/todayPnl': 5, + }, + 'post': { + 'api/v1/spot/orderTest': 1 * 1.67, + 'api/v1/spot/order': 1 * 1.67, + 'api/v1/futures/order': 1 * 1.67, + 'api/v1/spot/batchOrders': 2 * 1.67, + 'api/v1/subAccount/transfer': 1, + 'api/v1/account/withdraw': 1, + # contracts + 'api/v1/futures/marginType': 1, + 'api/v1/futures/leverage': 1, + 'api/v1/futures/batchOrders': 2 * 1.67, + 'api/v1/futures/position/trading-stop': 3 * 1.67, + 'api/v1/futures/positionMargin': 1, + 'api/v1/userDataStream': 1, + 'api/v1/listenKey': 1, + }, + 'delete': { + 'api/v1/spot/order': 1 * 1.67, + 'api/v1/futures/order': 1 * 1.67, + 'api/v1/spot/openOrders': 5 * 1.67, + 'api/v1/futures/batchOrders': 5 * 1.67, + 'api/v1/spot/cancelOrderByIds': 5 * 1.67, + 'api/v1/futures/cancelOrderByIds': 5 * 1.67, + 'api/v1/listenKey': 1, + }, + 'put': { + 'api/v1/listenKey': 1, + }, + }, + }, + 'timeframes': { + '1m': '1m', + '3m': '3m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1h', + '2h': '2h', + '4h': '4h', + '6h': '6h', + '8h': '8h', + '12h': '12h', + '1d': '1d', + '1w': '1w', + '1M': '1M', + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'exact': { + '-1000': OperationFailed, # An unknown error occurred while processing the request. + '-1001': OperationFailed, # Internal error; unable to process your request. Please try again. + '-1002': PermissionDenied, # You are not authorized to execute self request. + '-1003': RateLimitExceeded, # TOO_MANY_REQUESTS + '-1004': BadRequest, # {"code":-1004,"msg":"Missing required parameter \u0027xyz\u0027"} | {"code":-1004,"msg":"Bad request"} + '-1006': OperationFailed, # An unexpected response was received from the message bus. Execution status unknown + '-1007': OperationFailed, # Timeout waiting for response from backend server. Send status unknown; execution status unknown. + '-1014': OperationFailed, # Unsupported order combination. + '-1015': RateLimitExceeded, # Too many new orders + '-1016': OperationRejected, # This service is no longer available. + '-1020': OperationRejected, # This operation is not supported. + '-1021': OperationRejected, # Timestamp for self request is outside of the recvWindow. + '-1022': OperationRejected, # Signature for self request is not valid. + '-1100': BadRequest, # Illegal characters found in a parameter. + '-1101': BadRequest, # Too many parameters sent for self endpoint. + '-1102': BadRequest, # A mandatory parameter was not sent, was empty/null, or malformed + '-1103': BadRequest, # An unknown parameter was sent + '-1104': BadRequest, # Not all sent parameters were read + '-1105': BadRequest, # A parameter was empty + '-1106': BadRequest, # A parameter was sent when not required + '-1111': BadRequest, # Precision is over the maximum defined for self asset. + '-1112': OperationRejected, # No orders on book for symbol. + '-1114': BadRequest, # TimeInForce parameter sent when not required. + '-1115': BadRequest, # Invalid timeInForce + '-1116': BadRequest, # Invalid orderType + '-1117': BadRequest, # Invalid side + '-1118': InvalidOrder, # New client order ID was empty. + '-1119': InvalidOrder, # Original client order ID was empty + '-1120': BadRequest, # Invalid interval + '-1121': BadRequest, # Invalid symbol + '-1125': OperationRejected, # This listenKey does not exist. + '-1127': OperationRejected, # Lookup interval is too big + '-1128': BadRequest, # Combination of optional parameters invalid + '-1130': BadRequest, # Invalid data sent for a parameter + '-1132': OperationRejected, # Order price too high + '-1133': OperationRejected, # Order price lower than the minimum,please check general broker info + '-1134': OperationRejected, # Order price decimal too long,please check general broker info + '-1135': OperationRejected, # Order quantity too large + '-1136': OperationRejected, # Order quantity lower than the minimum + '-1137': OperationRejected, # Order quantity decimal too long + '-1138': OperationRejected, # Order price exceeds permissible range + '-1139': OperationRejected, # Order has been filled + '-1140': OperationRejected, # Transaction amount lower than the minimum + '-1141': InvalidOrder, # Duplicate clientOrderId + '-1142': InvalidOrder, # Order has been canceled + '-1143': InvalidOrder, # Cannot be found on order book + '-1144': OperationRejected, # Order has been locked + '-1145': OperationRejected, # This order type does not support cancellation + '-1146': OperationFailed, # Order creation timeout + '-1147': OperationFailed, # Order cancellation timeout + '-1193': OperationRejected, # Create order count limit + '-1194': OperationRejected, # Create market order forbidden + '-1195': OperationRejected, # Create limit order price too small + '-1196': OperationRejected, # Create limit order price too big + '-1197': OperationRejected, # Create limit order buy price too big + '-1198': OperationRejected, # Create limit order sell price too small + '-1199': OperationRejected, # Create order buy quantity too small + '-1200': OperationRejected, # Create order buy quantity too big + '-1201': OperationRejected, # Create limit order sell price too big + '-1202': OperationRejected, # Create order sell quantity too small + '-1203': OperationRejected, # Create order sell quantity too big + '-1206': OperationRejected, # Orders over the maximum transaction amount + '-2010': OperationFailed, # NEW_ORDER_REJECTED + '-2011': OperationFailed, # CANCEL_REJECTED + '-2013': InvalidOrder, # Order does not exist. + '-2014': PermissionDenied, # API-key format invalid. + '-2015': PermissionDenied, # Invalid API-key, IP, or permissions for action. + '-2016': BadRequest, # No trading window could be found for the symbol. Try ticker/24hrs instead. + # errors above 3xxx are from swap API + '-3050': ExchangeError, # CREATE_API_KEY_EXCEED_LIMIT + '-3101': OperationRejected, # open margin account error + '-3102': OperationRejected, # get margin safety error + '-3103': BadRequest, # risk config is not exit + '-3105': OperationRejected, # token can not borrow + '-3107': OperationRejected, # token can not withdraw + '-3108': OperationRejected, # get token avail withdraw error + '-3109': OperationRejected, # margin withdraw failed + '-3110': InsufficientFunds, # margin avail withdraw not enough failed + '-3116': OperationRejected, # repay fail + '-3117': OperationRejected, # get margin all position fail + '-3120': OperationRejected, # get repay order fail + '-3124': OperationRejected, # Position and order data error + '-3125': OperationRejected, # Position size cannot meet target leverage + '-3126': OperationRejected, # Adjust leverage fail + '-3127': OperationFailed, # Adjust leverage timeout + '-3128': OperationRejected, # The margin mode cannot be changed while you have an open order/position + '-3129': BadRequest, # cone futures change position type error + '-3130': OperationRejected, # order margin insufficient + '-3131': NotSupported, # Leverage reduction is not supported in Isolated Margin Mode with open positions. + }, + 'broad': { + 'Unknown order sent': OrderNotFound, + 'Duplicate order sent': InvalidOrder, + 'Market is closed': OperationRejected, + 'Account has insufficient balance for requested action': InsufficientFunds, + 'Market orders are not supported for self symbol': OperationRejected, + 'Iceberg orders are not supported for self symbol': OperationRejected, + 'Stop loss orders are not supported for self symbol': OperationRejected, + 'Stop loss limit orders are not supported for self symbol': OperationRejected, + 'Take profit orders are not supported for self symbol': OperationRejected, + 'Take profit limit orders are not supported for self symbol': OperationRejected, + 'QTY is zero or less': BadRequest, + 'IcebergQty exceeds QTY': OperationRejected, + 'This action disabled is on self account': PermissionDenied, + 'Unsupported order combination': BadRequest, + 'Order would trigger immediately': OperationRejected, + 'Cancel order is invalid. Check origClOrdId and orderId': OperationRejected, + 'Order would immediately match and take': OperationRejected, + }, + }, + 'commonCurrencies': {}, + 'options': { + 'defaultType': 'spot', + 'accountsByType': { + 'spot': 'MAIN', + 'swap': 'FUTURES', + }, + 'networks': { + 'BTC': 'BTC', + 'ERC20': 'ETH', + 'ETH': 'ETH', + 'BEP20': 'BSC', + 'TRC20': 'TRX', + 'SOL': 'SOL', + 'MATIC': 'MATIC', + 'ARBONE': 'ARBITRUM', + 'BASE': 'BASE', + 'TON': 'TON', + 'AVAXC': 'AVAXC', + 'DOGE': 'DOGE', + 'XRP': 'XRP', + 'DOT': 'DOT', + 'ADA': 'ADA', + 'LTC': 'LTC', + 'APT': 'APT', + 'ATOM': 'ATOM', + 'ALGO': 'ALGO', + 'NEAR': 'NEAR', + 'XLM': 'XLM', + 'SUI': 'SUI', + 'ETC': 'ETC', + 'EOS': 'EOS', + 'WAVES': 'WAVES', + 'ICP': 'ICP', + 'ONE': 'ONE', + # 'CHZ2': 'CHZ2', + }, + 'networksById': { + 'ETH': 'ERC20', + 'ERC20': 'ERC20', + }, + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyRequiresPrice': False, + 'marketBuyByCost': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchOHLCV': { + 'limit': 1000, + }, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 100000, + 'untilDays': 100000, + 'symbolRequired': True, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 1000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': 500, + 'daysBack': 100000, + 'untilDays': 100000, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchClosedOrders': None, + }, + 'forDerivatives': { + 'createOrders': None, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + }) + + def fetch_status(self, params={}): + """ + the latest known information on the availability of the exchange API + + https://toobit-docs.github.io/apidocs/spot/v1/en/#test-connectivity + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `status structure ` + """ + response = self.commonGetApiV1Ping(params) + return { + 'status': 'ok', + 'updated': None, + 'eta': None, + 'url': None, + 'info': response, + } + + def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://toobit-docs.github.io/apidocs/spot/v1/en/#check-server-time + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = self.commonGetApiV1Time(params) + # + # { + # "serverTime": 1699827319559 + # } + # + return self.safe_integer(response, 'serverTime') + + def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = self.commonGetApiV1ExchangeInfo(params) + self.options['exchangeInfo'] = response # we store it in options for later use in fetchMarkets + # + # { + # "timezone": "UTC", + # "serverTime": "1755583099926", + # "brokerFilters": [], + # "symbols": [ + # { + # "filters": [ + # { + # "minPrice": "0.01", + # "maxPrice": "10000000.00000000", + # "tickSize": "0.01", + # "filterType": "PRICE_FILTER" + # }, + # { + # "minQty": "0.0001", + # "maxQty": "4000", + # "stepSize": "0.0001", + # "filterType": "LOT_SIZE" + # }, + # { + # "minNotional": "5", + # "filterType": "MIN_NOTIONAL" + # }, + # { + # "minAmount": "5", + # "maxAmount": "6600000", + # "minBuyPrice": "0.01", + # "filterType": "TRADE_AMOUNT" + # }, + # { + # "maxSellPrice": "99999999", + # "buyPriceUpRate": "0.1", + # "sellPriceDownRate": "0.1", + # "filterType": "LIMIT_TRADING" + # }, + # { + # "buyPriceUpRate": "0.1", + # "sellPriceDownRate": "0.1", + # "filterType": "MARKET_TRADING" + # }, + # { + # "noAllowMarketStartTime": "0", + # "noAllowMarketEndTime": "0", + # "limitOrderStartTime": "0", + # "limitOrderEndTime": "0", + # "limitMinPrice": "0", + # "limitMaxPrice": "0", + # "filterType": "OPEN_QUOTE" + # } + # ], + # "exchangeId": "301", + # "symbol": "ETHUSDT", + # "symbolName": "ETHUSDT", + # "status": "TRADING", + # "baseAsset": "ETH", + # "baseAssetName": "ETH", + # "baseAssetPrecision": "0.0001", + # "quoteAsset": "USDT", + # "quoteAssetName": "USDT", + # "quotePrecision": "0.01", + # "icebergAllowed": False, + # "isAggregate": False, + # "allowMargin": True, + # } + # ], + # "options": [], + # "contracts": [ + # { + # "filters": [...], + # "exchangeId": "301", + # "symbol": "BTC-SWAP-USDT", + # "symbolName": "BTC-SWAP-USDTUSDT", + # "status": "TRADING", + # "baseAsset": "BTC-SWAP-USDT", + # "baseAssetPrecision": "0.001", + # "quoteAsset": "USDT", + # "quoteAssetPrecision": "0.1", + # "icebergAllowed": False, + # "inverse": False, + # "index": "BTC", + # "indexToken": "BTCUSDT", + # "marginToken": "USDT", + # "marginPrecision": "0.0001", + # "contractMultiplier": "0.001", + # "underlying": "BTC", + # "riskLimits": [ + # { + # "riskLimitId": "200020911", + # "quantity": "42000.0", + # "initialMargin": "0.02", + # "maintMargin": "0.01", + # "isWhite": False + # }, + # { + # "riskLimitId": "200020912", + # "quantity": "84000.0", + # "initialMargin": "0.04", + # "maintMargin": "0.02", + # "isWhite": False + # }, + # ... + # ] + # }, + # ], + # "coins": [ + # { + # "orgId": "9001", + # "coinId": "TCOM", + # "coinName": "TCOM", + # "coinFullName": "TCOM", + # "allowWithdraw": True, + # "allowDeposit": True, + # "chainTypes": [ + # { + # "chainType": "BSC", + # "withdrawFee": "49.55478", + # "minWithdrawQuantity": "77", + # "maxWithdrawQuantity": "0", + # "minDepositQuantity": "48", + # "allowDeposit": True, + # "allowWithdraw": False + # } + # ], + # "isVirtual": False + # }, + # ... + # + coins = self.safe_list(response, 'coins', []) + result = {} + for i in range(0, len(coins)): + coin = coins[i] + parsed = self.parse_currency(coin) + code = parsed['code'] + result[code] = parsed + return result + + def parse_currency(self, rawCurrency: dict) -> Currency: + id = self.safe_string(rawCurrency, 'coinId') + code = self.safe_currency_code(id) + networks: dict = {} + rawNetworks = self.safe_list(rawCurrency, 'chainTypes') + for j in range(0, len(rawNetworks)): + rawNetwork = rawNetworks[j] + networkId = self.safe_string(rawNetwork, 'chainType') + networkCode = self.network_id_to_code(networkId) + networks[networkCode] = { + 'id': networkId, + 'network': networkCode, + 'margin': None, + 'deposit': self.safe_bool(rawNetwork, 'allowDeposit'), + 'withdraw': self.safe_bool(rawNetwork, 'allowWithdraw'), + 'active': None, + 'fee': self.safe_number(rawNetwork, 'withdrawFee'), + 'precision': None, + 'limits': { + 'deposit': { + 'min': self.safe_number(rawNetwork, 'minDepositQuantity'), + 'max': None, + }, + 'withdraw': { + 'min': self.safe_number(rawNetwork, 'minWithdrawQuantity'), + 'max': self.safe_number(rawNetwork, 'maxWithdrawQuantity'), + }, + }, + 'info': rawNetwork, + } + return self.safe_currency_structure({ + 'id': id, + 'code': code, + 'name': self.safe_string(rawCurrency, 'coinFullName'), + 'type': None, + 'active': None, + 'deposit': self.safe_bool(rawCurrency, 'allowDeposit'), + 'withdraw': self.safe_bool(rawCurrency, 'allowWithdraw'), + 'fee': None, + 'precision': None, + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + }, + 'networks': networks, + 'info': rawCurrency, + }) + + def fetch_markets(self, params={}) -> List[MarketInterface]: + """ + retrieves data on all markets for toobit + + https://toobit-docs.github.io/apidocs/spot/v1/en/#exchange-information + https://toobit-docs.github.io/apidocs/usdt_swap/v1/en/#exchange-information + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = self.safe_dict(self.options, 'exchangeInfo') + if response is not None: + self.options['exchangeInfo'] = None # reset it to avoid using old cached data + else: + response = self.commonGetApiV1ExchangeInfo(params) + # + # { + # "timezone": "UTC", + # "serverTime": "1755583099926", + # "brokerFilters": [], + # "symbols": [ + # { + # "filters": [ + # { + # "minPrice": "0.01", + # "maxPrice": "10000000.00000000", + # "tickSize": "0.01", + # "filterType": "PRICE_FILTER" + # }, + # { + # "minQty": "0.0001", + # "maxQty": "4000", + # "stepSize": "0.0001", + # "filterType": "LOT_SIZE" + # }, + # { + # "minNotional": "5", + # "filterType": "MIN_NOTIONAL" + # }, + # { + # "minAmount": "5", + # "maxAmount": "6600000", + # "minBuyPrice": "0.01", + # "filterType": "TRADE_AMOUNT" + # }, + # { + # "maxSellPrice": "99999999", + # "buyPriceUpRate": "0.1", + # "sellPriceDownRate": "0.1", + # "filterType": "LIMIT_TRADING" + # }, + # { + # "buyPriceUpRate": "0.1", + # "sellPriceDownRate": "0.1", + # "filterType": "MARKET_TRADING" + # }, + # { + # "noAllowMarketStartTime": "0", + # "noAllowMarketEndTime": "0", + # "limitOrderStartTime": "0", + # "limitOrderEndTime": "0", + # "limitMinPrice": "0", + # "limitMaxPrice": "0", + # "filterType": "OPEN_QUOTE" + # } + # ], + # "exchangeId": "301", + # "symbol": "ETHUSDT", + # "symbolName": "ETHUSDT", + # "status": "TRADING", + # "baseAsset": "ETH", + # "baseAssetName": "ETH", + # "baseAssetPrecision": "0.0001", + # "quoteAsset": "USDT", + # "quoteAssetName": "USDT", + # "quotePrecision": "0.01", + # "icebergAllowed": False, + # "isAggregate": False, + # "allowMargin": True, + # } + # ], + # "options": [], + # "contracts": [ + # { + # "filters": [...], + # "exchangeId": "301", + # "symbol": "BTC-SWAP-USDT", + # "symbolName": "BTC-SWAP-USDTUSDT", + # "status": "TRADING", + # "baseAsset": "BTC-SWAP-USDT", + # "baseAssetPrecision": "0.001", + # "quoteAsset": "USDT", + # "quoteAssetPrecision": "0.1", + # "icebergAllowed": False, + # "inverse": False, + # "index": "BTC", + # "indexToken": "BTCUSDT", + # "marginToken": "USDT", + # "marginPrecision": "0.0001", + # "contractMultiplier": "0.001", + # "underlying": "BTC", + # "riskLimits": [ + # { + # "riskLimitId": "200020911", + # "quantity": "42000.0", + # "initialMargin": "0.02", + # "maintMargin": "0.01", + # "isWhite": False + # }, + # { + # "riskLimitId": "200020912", + # "quantity": "84000.0", + # "initialMargin": "0.04", + # "maintMargin": "0.02", + # "isWhite": False + # }, + # ... + # ] + # }, + # ], + # "coins": [ + # { + # "orgId": "9001", + # "coinId": "TCOM", + # "coinName": "TCOM", + # "coinFullName": "TCOM", + # "allowWithdraw": True, + # "allowDeposit": True, + # "chainTypes": [ + # { + # "chainType": "BSC", + # "withdrawFee": "49.55478", + # "minWithdrawQuantity": "77", + # "maxWithdrawQuantity": "0", + # "minDepositQuantity": "48", + # "allowDeposit": True, + # "allowWithdraw": False + # } + # ], + # "isVirtual": False + # }, + # ... + # + symbols = self.safe_list(response, 'symbols', []) + contracts = self.safe_list(response, 'contracts', []) + all = self.array_concat(symbols, contracts) + result = [] + for i in range(0, len(all)): + market = all[i] + parsed = self.parse_market(market) + result.append(parsed) + return result + + def parse_market(self, market: dict) -> Market: + id = self.safe_string(market, 'symbol') + baseId = self.safe_string(market, 'baseAsset') + quoteId = self.safe_string(market, 'quoteAsset') + baseParts = baseId.split('-') + baseIdClean = baseParts[0] + base = self.safe_currency_code(baseIdClean) + quote = self.safe_currency_code(quoteId) + settleId = self.safe_string(market, 'marginToken') + settle = self.safe_currency_code(settleId) + status = self.safe_string(market, 'status') + active = (status == 'TRADING') + filters = self.safe_list(market, 'filters', []) + filtersByType = self.index_by(filters, 'filterType') + priceFilter = self.safe_dict(filtersByType, 'PRICE_FILTER', {}) + lotSizeFilter = self.safe_dict(filtersByType, 'LOT_SIZE', {}) + minNotionalFilter = self.safe_dict(filtersByType, 'MIN_NOTIONAL', {}) + symbol = base + '/' + quote + isContract = ('contractMultiplier' in market) + inverse = self.safe_bool_2(market, 'isInverse', 'inverse') + if isContract: + symbol += ':' + settle + return self.safe_market_structure({ + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': 'swap' if isContract else 'spot', + 'spot': not isContract, + 'margin': False, + 'swap': isContract, + 'future': False, + 'option': False, + 'active': active, + 'contract': isContract, + 'linear': not inverse if isContract else None, + 'inverse': inverse if isContract else None, + 'contractSize': self.safe_number(market, 'contractMultiplier'), + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.safe_number(lotSizeFilter, 'stepSize'), + 'price': self.safe_number(priceFilter, 'tickSize'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(lotSizeFilter, 'minQty'), + 'max': self.safe_number(lotSizeFilter, 'maxQty'), + }, + 'price': { + 'min': self.safe_number(priceFilter, 'minPrice'), + 'max': self.safe_number(priceFilter, 'maxPrice'), + }, + 'cost': { + 'min': self.safe_number(minNotionalFilter, 'minNotional'), + 'max': None, + }, + }, + 'created': None, + 'info': market, + }) + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://toobit-docs.github.io/apidocs/spot/v1/en/#order-book + https://toobit-docs.github.io/apidocs/usdt_swap/v1/en/#order-book + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['limit'] = limit + response = self.commonGetQuoteV1Depth(self.extend(request, params)) + # + # { + # "t": "1755593995237", + # "b": [ + # [ + # "115186.47", + # "4.184864" + # ], + # [ + # "115186.46", + # "0.002756" + # ], + # ... + # ], + # "a": [ + # [ + # "115186.48", + # "6.137369" + # ], + # [ + # "115186.49", + # "0.002914" + # ], + # ... + # ] + # } + # + timestamp = self.safe_integer(response, 't') + return self.parse_order_book(response, market['symbol'], timestamp, 'b', 'a') + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get a list of the most recent trades for a particular symbol + + https://toobit-docs.github.io/apidocs/spot/v1/en/#recent-trades-list + https://toobit-docs.github.io/apidocs/usdt_swap/v1/en/#recent-trades-list + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum number of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['limit'] = limit + response = self.commonGetQuoteV1Trades(self.extend(request, params)) + # + # [ + # { + # "t": "1755594277287", + # "p": "115276.99", + # "q": "0.001508", + # "ibm": True + # }, + # ] + # + return self.parse_trades(response, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades + # + # { + # "t": "1755594277287", + # "p": "115276.99", + # "q": "0.001508", + # "ibm": True + # }, + # # watchTrades have also an additional fields: + # "v": "4864732022868004630", # trade id + # "m": True, # is the buyer taker + # + # fetchMyTrades + # + # { + # "id": "2024934575206059008", + # "symbol": "ETHUSDT", + # "orderId": "2024934575097029888", + # "ticketId": "4864450547563401875", + # "price": "4641.21", + # "qty": "0.001", + # "time": "1756127012094", + # "isMaker": False, + # "commission": "0.00464121", + # "commissionAsset": "USDT", + # "makerRebate": "0", + # "symbolName": "ETHUSDT", # only in SPOT + # "isBuyer": False, # only in SPOT + # "feeAmount": "0.00464121", # only in SPOT + # "feeCoinId": "USDT", # only in SPOT + # "fee": { # only in SPOT + # "feeCoinId": "USDT", + # "feeCoinName": "USDT", + # "fee": "0.00464121" + # }, + # "type": "LIMIT", # only in CONTRACT + # "side": "BUY_OPEN", # only in CONTRACT + # "realizedPnl": "0", # only in CONTRACT + # }, + # + timestamp = self.safe_integer_2(trade, 't', 'time') + priceString = self.safe_string_2(trade, 'p', 'price') + amountString = self.safe_string_2(trade, 'q', 'qty') + isBuyer = self.safe_bool(trade, 'isBuyer') + side = None + isBuyerMaker = self.safe_bool(trade, 'ibm') + if isBuyerMaker is None: + isBuyerTaker = self.safe_bool(trade, 'm') + if isBuyerTaker is not None: + isBuyerMaker = not isBuyerTaker + if isBuyerMaker is not None: + if isBuyerMaker: + side = 'sell' + else: + side = 'buy' + else: + if isBuyer: + side = 'buy' + else: + side = 'sell' + feeCurrencyId = self.safe_string(trade, 'feeCoinId') + feeAmount = self.safe_string(trade, 'feeAmount') + fee = None + if feeAmount is not None: + fee = { + 'currency': self.safe_currency_code(feeCurrencyId), + 'cost': feeAmount, + } + isMaker = self.safe_bool(trade, 'isMaker') + takerOrMaker = None + if isMaker is not None: + takerOrMaker = 'maker' if isMaker else 'taker' + market = self.safe_market(None, market) + symbol = market['symbol'] + return self.safe_trade({ + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'id': self.safe_string_2(trade, 'id', 'v'), + 'order': self.safe_string(trade, 'orderId'), + 'type': None, + 'side': side, + 'amount': amountString, + 'price': priceString, + 'cost': None, + 'takerOrMaker': takerOrMaker, + 'fee': fee, + }, market) + + def fetch_ohlcv(self, symbol: str, timeframe='1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://toobit-docs.github.io/apidocs/spot/v1/en/#kline-candlestick-data + https://toobit-docs.github.io/apidocs/usdt_swap/v1/en/#kline-candlestick-data + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'interval': self.safe_string(self.timeframes, timeframe, timeframe), + } + if since is not None: + request['startTime'] = since + until = self.safe_integer(params, 'until') + if until is not None: + params = self.omit(params, 'until') + request['endTime'] = until + if limit is not None: + request['limit'] = limit + response = None + endpoint = None + endpoint, params = self.handle_option_and_params(params, 'fetchOHLCV', 'price') + if endpoint == 'index': + response = self.commonGetQuoteV1IndexKlines(self.extend(request, params)) + # + # { + # "code": 200, + # "data": [ + # { + # "t": 1669155300000,//time + # "s": "ETHUSDT",// symbol + # "sn": "ETHUSDT",//symbol name + # "c": "1127.1",//Close price + # "h": "1130.81",//High price + # "l": "1126.17",//Low price + # "o": "1130.8",//Open price + # "v": "0"//Volume + # }, + # { + # "t": 1669156200000, + # "s": "ETHUSDT", + # "sn": "ETHUSDT", + # "c": "1129.44", + # "h": "1129.54", + # "l": "1127.1", + # "o": "1127.1", + # "v": "0" + # } + # ] + # } + # + elif endpoint == 'mark': + response = self.commonGetQuoteV1MarkPriceKlines(self.extend(request, params)) + # + # { + # "code": 200, + # "data": [ + # { + # "symbol": "BTCUSDT",// Symbol + # "time": 1670157900000,// time + # "low": "16991.14096",//Low price + # "open": "16991.78288",//Open price + # "high": "16996.30641",// High prce + # "close": "16996.30641",// Close price + # "volume": "0",// Volume + # "curId": 1670157900000 + # } + # ] + # } + # + else: + response = self.commonGetQuoteV1Klines(self.extend(request, params)) + # + # [ + # [ + # 1755540660000, + # "116399.99", + # "116399.99", + # "116360.09", + # "116360.1", + # "2.236869", + # 0, + # "260303.79722607", + # 22, + # "2.221061", + # "258464.10338267" + # ], + # ... + # + return self.parse_ohlcvs(response, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + return [ + self.safe_integer_n(ohlcv, [0, 'time', 't']), + self.safe_number_n(ohlcv, [1, 'open', 'o']), + self.safe_number_n(ohlcv, [2, 'high', 'h']), + self.safe_number_n(ohlcv, [3, 'low', 'l']), + self.safe_number_n(ohlcv, [4, 'close', 'c']), + self.safe_number_n(ohlcv, [5, 'volume', 'v']), + ] + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://toobit-docs.github.io/apidocs/spot/v1/en/#24hr-ticker-price-change-statistics + https://toobit-docs.github.io/apidocs/usdt_swap/v1/en/#24hr-ticker-price-change-statistics + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + symbols = self.market_symbols(symbols) + type = None + market = None + request: dict = {} + if symbols is not None: + symbol = self.safe_string(symbols, 0) + market = self.market(symbol) + length = len(symbols) + if length == 1: + request['symbol'] = market['id'] + type, params = self.handle_market_type_and_params('fetchTickers', market, params) + response = None + if type == 'spot': + response = self.commonGetQuoteV1Ticker24hr(self.extend(request, params)) + else: + response = self.commonGetQuoteV1ContractTicker24hr(self.extend(request, params)) + # + # [ + # { + # "t": "1755601440162", + # "s": "GRDRUSDT", + # "o": "0.38", + # "h": "0.38", + # "l": "0.38", + # "c": "0.38", + # "v": "0", + # "qv": "0", + # "pc": "0", + # "pcp": "0" + # }, + # ... + # + return self.parse_tickers(response, symbols, params) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + marketId = self.safe_string(ticker, 's') + market = self.safe_market(marketId, market) + timestamp = self.safe_integer(ticker, 't') + last = self.safe_string(ticker, 'c') + return self.safe_ticker({ + 'symbol': market['symbol'], + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'h'), + 'low': self.safe_string(ticker, 'l'), + 'bid': None, + 'bidVolume': None, + 'ask': None, + 'askVolume': None, + 'vwap': None, + 'open': self.safe_string(ticker, 'o'), + 'close': last, + 'last': last, + 'previousClose': None, + 'change': self.safe_string(ticker, 'pc'), + 'percentage': self.safe_string(ticker, 'pcp'), + 'average': None, + 'baseVolume': self.safe_string(ticker, 'v'), + 'quoteVolume': self.safe_string(ticker, 'qv'), + 'info': ticker, + }, market) + + def fetch_last_prices(self, symbols: Strings = None, params={}): + """ + fetches the last price for multiple markets + + https://toobit-docs.github.io/apidocs/spot/v1/en/#symbol-price-ticker + https://toobit-docs.github.io/apidocs/usdt_swap/v1/en/#symbol-price-ticker + + :param str[]|None symbols: unified symbols of the markets to fetch the last prices + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of lastprices structures + """ + self.load_markets() + symbols = self.market_symbols(symbols) + request: dict = {} + if symbols is not None: + length = len(symbols) + if length == 1: + market = self.market(symbols[0]) + request['symbol'] = market['id'] + response = self.commonGetQuoteV1TickerPrice(self.extend(request, params)) + # + # [ + # { + # "s": "BNTUSDT", + # "si": "BNTUSDT", + # "p": "0.823" + # }, + # + return self.parse_last_prices(response, symbols) + + def parse_last_price(self, entry, market: Market = None): + marketId = self.safe_string(entry, 's') + market = self.safe_market(marketId, market) + return { + 'symbol': market['symbol'], + 'timestamp': None, + 'datetime': None, + 'price': self.safe_number_omit_zero(entry, 'price'), + 'side': None, + 'info': entry, + } + + def fetch_bids_asks(self, symbols: Strings = None, params={}): + """ + fetches the bid and ask price and volume for multiple markets + + https://toobit-docs.github.io/apidocs/spot/v1/en/#symbol-order-book-ticker + https://toobit-docs.github.io/apidocs/usdt_swap/v1/en/#symbol-order-book-ticker + + :param str[] [symbols]: unified symbols of the markets to fetch the bids and asks for, all markets are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + symbols = self.market_symbols(symbols) + request: dict = {} + if symbols is not None: + length = len(symbols) + if length == 1: + market = self.market(symbols[0]) + request['symbol'] = market['id'] + response = self.commonGetQuoteV1TickerBookTicker(self.extend(request, params)) + # + # [ + # { + # "s": "GRDRUSDT", + # "b": "0", + # "bq": "0", + # "a": "0", + # "aq": "0", + # "t": "1755936610506" + # }, ... + # + return self.parse_bids_asks_custom(response, symbols) + + def parse_bids_asks_custom(self, tickers, symbols: Strings = None, params={}) -> Tickers: + results = [] + for i in range(0, len(tickers)): + parsedTicker = self.parse_bid_ask_custom(tickers[i]) + ticker = self.extend(parsedTicker, params) + results.append(ticker) + symbols = self.market_symbols(symbols) + return self.filter_by_array(results, 'symbol', symbols) + + def parse_bid_ask_custom(self, ticker): + return { + 'timestamp': self.safe_string(ticker, 't'), + 'symbol': self.safe_string(ticker, 's'), + 'bid': self.safe_number(ticker, 'b'), + 'bidVolume': self.safe_number(ticker, 'bq'), + 'ask': self.safe_number(ticker, 'a'), + 'askVolume': self.safe_number(ticker, 'aq'), + 'info': ticker, + } + + def fetch_funding_rates(self, symbols: Strings = None, params={}) -> FundingRates: + """ + fetch the funding rate for multiple markets + + https://toobit-docs.github.io/apidocs/usdt_swap/v1/en/#funding-rate + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `funding rates structures `, indexe by market symbols + """ + self.load_markets() + symbols = self.market_symbols(symbols) + request: dict = {} + if symbols is not None: + length = len(symbols) + if length == 1: + market = self.market(symbols[0]) + request['symbol'] = market['id'] + response = self.commonGetApiV1FuturesFundingRate(self.extend(request, params)) + # + # [ + # { + # "symbol": "BTC-SWAP-USDT", + # "rate": "0.0001071148112848", + # "nextFundingTime": "1755964800000" + # },... + # + return self.parse_funding_rates(response, symbols) + + def parse_funding_rate(self, contract, market: Market = None) -> FundingRate: + marketId = self.safe_string(contract, 'symbol') + symbol = self.safe_symbol(marketId, market) + nextFundingRate = self.safe_number(contract, 'rate') + nextFundingRateTimestamp = self.safe_integer(contract, 'nextFundingTime') + return { + 'info': contract, + 'symbol': symbol, + 'markPrice': None, + 'indexPrice': None, + 'interestRate': None, + 'estimatedSettlePrice': None, + 'timestamp': None, + 'datetime': None, + 'previousFundingRate': None, + 'nextFundingRate': None, + 'previousFundingTimestamp': None, + 'nextFundingTimestamp': None, + 'previousFundingDatetime': None, + 'nextFundingDatetime': None, + 'fundingRate': nextFundingRate, + 'fundingTimestamp': nextFundingRateTimestamp, + 'fundingDatetime': self.iso8601(nextFundingRateTimestamp), + 'interval': None, + } + + def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical funding rate prices + + https://toobit-docs.github.io/apidocs/usdt_swap/v1/en/#get-funding-rate-history + + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: timestamp in ms of the earliest funding rate to fetch + :param int [limit]: the maximum amount of `funding rate structures ` to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest funding rate to fetch + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `funding rate structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchFundingRateHistory', 'paginate') + if paginate: + return self.fetch_paginated_call_deterministic('fetchFundingRateHistory', symbol, since, limit, '8h', params) + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['limit'] = limit + response = self.commonGetApiV1FuturesHistoryFundingRate(self.extend(request, params)) + # + # [ + # { + # "id": "869931", + # "symbol": "BTC-SWAP-USDT", + # "settleTime": "1755936000000", + # "settleRate": "0.0001" + # }, ... + # + return self.parse_funding_rate_histories(response, market, since, limit) + + def parse_funding_rate_history(self, contract, market: Market = None): + timestamp = self.safe_integer(contract, 'settleTime') + marketId = self.safe_string(contract, 'symbol') + return { + 'info': contract, + 'symbol': self.safe_symbol(marketId, market), + 'fundingRate': self.safe_number(contract, 'settleRate'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://toobit-docs.github.io/apidocs/spot/v1/en/#account-information-user_data + https://toobit-docs.github.io/apidocs/usdt_swap/v1/en/#futures-account-balance-user_data + + :param dict [params]: extra parameters specific to the exchange API endpointinvalid + :returns dict: a `balance structure ` + """ + self.load_markets() + response = None + marketType: Str = None + marketType, params = self.handle_market_type_and_params('fetchBalance', None, params) + if self.in_array(marketType, ['swap', 'future']): + response = self.privateGetApiV1FuturesBalance() + # + # [ + # { + # "asset": "USDT", # asset + # "balance": "999999999999.982", # total + # "availableBalance": "1899999999978.4995", # available balance Include unrealized pnl + # "positionMargin": "11.9825", #position Margin + # "orderMargin": "9.5", #order Margin + # "crossUnRealizedPnl": "10.01" #The unrealized profit and loss of cross position + # } + # ] + # + else: + response = self.privateGetApiV1Account() + # + # { + # "userId": "912902020", + # "balances": [ + # { + # "asset": "ETH", + # "assetId": "ETH", + # "assetName": "ETH", + # "total": "0.025", + # "free": "0.025", + # "locked": "0" + # } + # ] + # } + # + return self.parse_balance(response) + + def parse_balance(self, response) -> Balances: + result: dict = { + 'info': response, + 'timestamp': None, + 'datetime': None, + } + balances = self.safe_list(response, 'balances', response) + for i in range(0, len(balances)): + balance = balances[i] + code = self.safe_currency_code(self.safe_string(balance, 'asset')) + account = self.account() + account['free'] = self.safe_string_2(balance, 'free', 'availableBalance') + account['total'] = self.safe_string_2(balance, 'total', 'balance') + account['used'] = self.safe_string(balance, 'locked') + result[code] = account + return self.safe_balance(result) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://toobit-docs.github.io/apidocs/spot/v1/en/#new-order-trade + https://toobit-docs.github.io/apidocs/usdt_swap/v1/en/#new-order-trade + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market', 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + request = {} + response = None + if market['spot']: + request, params = self.create_order_request(symbol, type, side, amount, price, params) + response = self.privatePostApiV1SpotOrder(self.extend(request, params)) + else: + request, params = self.create_contract_order_request(symbol, type, side, amount, price, params) + response = self.privatePostApiV1FuturesOrder(self.extend(request, params)) + # + # { + # "symbol": "ETHUSDT", + # "price": "0", + # "origQty": "0.001", + # "orderId": "2024837825254460160", + # "clientOrderId": "1756115478113679", + # "executedQty": "0", + # "status": "PENDING_NEW", + # "timeInForce": "GTC", + # "type": "MARKET", + # "side": "SELL" + # "accountId": "1783404067076253952", # only in spot + # "symbolName": "ETHUSDT", # only in spot + # "transactTime": "1756115478604", # only in spot + # "time": "1668418485058", # only in contract + # "updateTime": "1668418485058", # only in contract + # "leverage": "2", # only in contract + # "avgPrice": "0", # only in contract + # "marginLocked": "9.5", # only in contract + # "priceType": "INPUT" # only in contract + # } + # + return self.parse_order(response, market) + + def create_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + market = self.market(symbol) + id = market['id'] + request: dict = { + 'symbol': id, + 'side': side.upper(), + } + if price is not None: + request['price'] = self.price_to_precision(symbol, price) + cost: Str = None + cost, params = self.handle_param_string(params, 'cost') + if type == 'market': + if cost is None and side == 'buy': + raise ArgumentsRequired(self.id + ' createOrder() requires params["cost"] for market buy order') + else: + request['quantity'] = self.cost_to_precision(symbol, cost) + else: + request['quantity'] = self.amount_to_precision(symbol, amount) + isPostOnly = None + isPostOnly, params = self.handle_post_only(type == 'market', False, params) + if isPostOnly: + request['type'] = 'LIMIT_MAKER' + else: + request['type'] = type.upper() + return [request, params] + + def create_contract_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'quantity': self.amount_to_precision(symbol, amount), + } + reduceOnly = None + reduceOnly, params = self.handle_param_bool(params, 'reduceOnly') + if side == 'buy': + side = 'SELL_CLOSE' if reduceOnly else 'BUY_OPEN' + elif side == 'sell': + side = 'BUY_CLOSE' if reduceOnly else 'SELL_OPEN' + request['side'] = side + if price is not None: + request['price'] = self.price_to_precision(symbol, price) + if self.in_array(type, ['limit', 'LIMIT']): + request['type'] = type.upper() + request['price'] = self.price_to_precision(symbol, price) + elif type == 'market': + request['type'] = 'LIMIT' # weird, but exchange works self way + request['priceType'] = 'MARKET' + isPostOnly = None + isPostOnly, params = self.handle_post_only(type == 'market', False, params) + if isPostOnly: + request['timeInForce'] = 'LIMIT_MAKER' + values = self.handle_trigger_prices_and_params(symbol, params) + triggerPrice = values[0] + params = values[3] + if triggerPrice is not None: + request['stopPrice'] = triggerPrice + stopLoss = self.safe_dict(params, 'stopLoss') + takeProfit = self.safe_dict(params, 'takeProfit') + triggerPriceTypes = { + 'mark': 'MARK_PRICE', + 'last': 'CONTRACT_PRICE', + } + if stopLoss is not None: + request['stopLoss'] = self.safe_value(stopLoss, 'triggerPrice') + limitPrice = self.safe_value(stopLoss, 'price') + if limitPrice is not None: + request['slOrderType'] = 'LIMIT' + request['slLimitPrice'] = self.price_to_precision(symbol, limitPrice) + triggerPriceType = self.safe_string(stopLoss, 'triggerPriceType') + if triggerPriceType is not None: + request['slTriggerBy'] = self.safe_string(triggerPriceTypes, triggerPriceType, triggerPriceType) + params = self.omit(params, 'stopLoss') + if takeProfit is not None: + request['takeProfit'] = self.safe_value(takeProfit, 'triggerPrice') + limitPrice = self.safe_value(takeProfit, 'price') + if limitPrice is not None: + request['tpOrderType'] = 'LIMIT' + request['tpLimitPrice'] = self.price_to_precision(symbol, limitPrice) + triggerPriceType = self.safe_string(takeProfit, 'triggerPriceType') + if triggerPriceType is not None: + request['tpTriggerBy'] = self.safe_string(triggerPriceTypes, triggerPriceType, triggerPriceType) + params = self.omit(params, 'takeProfit') + if not ('newClientOrderId' in params): + request['newClientOrderId'] = self.uuid() + return [request, params] + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder, cancelOrder + # + # { + # "symbol": "ETHUSDT", + # "price": "0", + # "origQty": "0.001", + # "orderId": "2024837825254460160", + # "clientOrderId": "1756115478113679", + # "executedQty": "0", + # "status": "PENDING_NEW", + # "timeInForce": "GTC", + # "type": "MARKET", + # "side": "SELL" + # "accountId": "1783404067076253952", # only in spot + # "symbolName": "ETHUSDT", # only in spot + # "transactTime": "1756115478604", # only in spot + # "time": "1668418485058", # only in contract + # "updateTime": "1668418485058", # only in contract + # "leverage": "2", # only in contract + # "avgPrice": "0", # only in contract + # "marginLocked": "9.5", # only in contract + # "priceType": "INPUT" # only in contract + # } + # + # + # fetchOrder, fetchOrders, fetchOpenOrders + # + # { + # "time": "1756140208069", + # "updateTime": "1756140208078", + # "orderId": "2025045271033977089", + # "clientOrderId": "17561402075722006", + # "symbol": "ETHUSDT", + # "price": "3000", + # "origQty": "0.002", + # "executedQty": "0", + # "avgPrice": "0", + # "type": "LIMIT", + # "side": "BUY", + # "timeInForce": "GTC", + # "status": "NEW", + # "accountId": "1783404067076253952", # only in SPOT + # "exchangeId": "301", # only in SPOT + # "symbolName": "ETHUSDT", # only in SPOT + # "cummulativeQuoteQty": "0", # only in SPOT + # "cumulativeQuoteQty": "0", # only in SPOT + # "stopPrice": "0.0", # only in SPOT + # "icebergQty": "0.0", # only in SPOT + # "isWorking": True # only in SPOT + # "leverage": "2", # only in CONTRACT + # "marginLocked": "9.5", # only in CONTRACT + # "priceType": "INPUT" # only in CONTRACT + # "triggerType": "0", # only in CONTRACT fetchClosedOrders + # "fallType": "0", # only in CONTRACT fetchClosedOrders + # "activeStatus": "0" # only in CONTRACT fetchClosedOrders + # } + # + timestamp = self.safe_integer_2(order, 'transactTime', 'time') + marketId = self.safe_string(order, 'symbol') + market = self.safe_market(marketId, market) + rawType = self.safe_string(order, 'type') + rawSideLower = self.safe_string_lower(order, 'side') + triggerPrice = self.omit_zero(self.safe_string(order, 'stopPrice')) + if triggerPrice == '0.0': + triggerPrice = None + return self.safe_order({ + 'info': order, + 'id': self.safe_string(order, 'orderId'), + 'clientOrderId': self.safe_string(order, 'clientOrderId'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'lastUpdateTimestamp': self.safe_integer(order, 'updateTime'), + 'status': self.parse_order_status(self.safe_string(order, 'status')), + 'symbol': market['symbol'], + 'type': self.parse_order_type(rawType), + 'timeInForce': self.safe_string(order, 'timeInForce'), + 'postOnly': (rawType == 'LIMIT_MAKER'), + 'side': rawSideLower, + 'price': self.omit_zero(self.safe_string(order, 'price')), + 'triggerPrice': triggerPrice, + 'cost': self.omit_zero(self.safe_string(order, 'cumulativeQuoteQty')), + 'average': self.safe_string(order, 'avgPrice'), + 'amount': self.safe_string(order, 'origQty'), + 'filled': self.safe_string(order, 'executedQty'), + 'remaining': None, + 'trades': None, + 'fee': None, + 'marginMode': None, + 'reduceOnly': None, + 'leverage': None, + 'hedged': None, + }, market) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'PENDING_NEW': 'open', + 'NEW': 'open', + 'PARTIALLY_FILLED': 'open', + 'FILLED': 'closed', + 'PENDING_CANCEL': 'canceled', + 'CANCELED': 'canceled', + 'REJECTED': 'canceled', + } + return self.safe_string(statuses, status, status) + + def parse_order_type(self, status): + statuses: dict = { + 'MARKET': 'market', + 'LIMIT': 'limit', + 'LIMIT_MAKER': 'limit', + } + return self.safe_string(statuses, status, status) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://toobit-docs.github.io/apidocs/spot/v1/en/#cancel-order-trade + https://toobit-docs.github.io/apidocs/usdt_swap/v1/en/#cancel-order-trade + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + request = {} + if self.safe_string(params, 'clientOrderId') is None: + request['orderId'] = id + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + marketType = None + marketType, params = self.handle_market_type_and_params('cancelOrder', market, params, 'none') + if marketType == 'none': + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument or the "defaultType" parameter to be set to "spot" or "swap"') + response = None + if marketType == 'spot': + response = self.privateDeleteApiV1SpotOrder(self.extend(request, params)) + else: + response = self.privateDeleteApiV1FuturesOrder(self.extend(request, params)) + # response same `createOrder` + status = self.parse_order_status(self.safe_string(response, 'status')) + if status != 'open': + raise OrderNotFound(self.id + ' order ' + id + ' can not be canceled, ' + self.json(response)) + return self.parse_order(response, market) + + def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders in a market + + https://toobit-docs.github.io/apidocs/spot/v1/en/#cancel-all-open-orders-trade + https://toobit-docs.github.io/apidocs/usdt_swap/v1/en/#cancel-orders-trade + + :param str symbol: unified symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `order structures ` + """ + self.load_markets() + request = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + marketType = None + marketType, params = self.handle_market_type_and_params('cancelAllOrders', market, params, 'none') + if marketType == 'none': + raise ArgumentsRequired(self.id + ' cancelAllOrders() requires a symbol argument or the "defaultType" parameter to be set to "spot" or "swap"') + response = None + if marketType == 'spot': + response = self.privateDeleteApiV1SpotOpenOrders(self.extend(request, params)) + # + # {"success":true} # always same response + # + else: + response = self.privateDeleteApiV1FuturesBatchOrders(self.extend(request, params)) + # + # {"code": 200, "message":"success", "timestamp":1541161088303} + # + return [ + self.safe_order({ + 'info': response, + }), + ] + + def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): + """ + cancel multiple orders + + https://toobit-docs.github.io/apidocs/spot/v1/en/#cancel-multiple-orders-trade + https://toobit-docs.github.io/apidocs/usdt_swap/v1/en/#cancel-multiple-orders-trade + + :param str[] ids: order ids + :param str [symbol]: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an list of `order structures ` + """ + self.load_markets() + idsString = ','.join(ids) + request: dict = { + 'ids': idsString, + } + market = None + if symbol is not None: + market = self.market(symbol) + marketType = None + marketType, params = self.handle_market_type_and_params('cancelOrders', market, params, 'none') + if marketType == 'none': + raise ArgumentsRequired(self.id + ' cancelOrders() requires a symbol argument or the "defaultType" parameter to be set to "spot" or "swap"') + response = None + if marketType == 'spot': + response = self.privateDeleteApiV1SpotCancelOrderByIds(self.extend(request, params)) + # + # {"success":true} # always same response + # + else: + response = self.privateDeleteApiV1FuturesCancelOrderByIds(self.extend(request, params)) + # + # { + # "code":200, + # "result":[ + # { + # "orderId":"1327047813809448704", + # "code":-2013 + # }, + # { + # "orderId":"1327047814212101888", + # "code":-2013 + # } + # ] + # } + # + # or empty array if no orders were canceled + result = self.safe_list(response, 'result', []) + return self.parse_orders(result, market) + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://toobit-docs.github.io/apidocs/spot/v1/en/#query-order-user_data + https://toobit-docs.github.io/apidocs/usdt_swap/v1/en/#query-order-user_data + + :param str id: the order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrder() requires a symbol argument') + self.load_markets() + request: dict = { + 'orderId': id, + } + market = self.market(symbol) + response = None + if market['spot']: + response = self.privateGetApiV1SpotOrder(self.extend(request, params)) + else: + response = self.privateGetApiV1FuturesOrder(self.extend(request, params)) + # + # { + # "time": "1756140208069", + # "updateTime": "1756140208078", + # "orderId": "2025045271033977089", + # "clientOrderId": "17561402075722006", + # "symbol": "ETHUSDT", + # "price": "3000", + # "origQty": "0.002", + # "executedQty": "0", + # "avgPrice": "0", + # "type": "LIMIT", + # "side": "BUY", + # "timeInForce": "GTC", + # "status": "NEW", + # "accountId": "1783404067076253952", # only in SPOT + # "exchangeId": "301", # only in SPOT + # "symbolName": "ETHUSDT", # only in SPOT + # "cummulativeQuoteQty": "0", # only in SPOT + # "cumulativeQuoteQty": "0", # only in SPOT + # "stopPrice": "0.0", # only in SPOT + # "icebergQty": "0.0", # only in SPOT + # "isWorking": True # only in SPOT + # "leverage": "2", # only in CONTRACT + # "marginLocked": "9.5", # only in CONTRACT + # "priceType": "INPUT" # only in CONTRACT + # } + # + return self.parse_order(response, market) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://toobit-docs.github.io/apidocs/spot/v1/en/#current-open-orders-user_data + https://toobit-docs.github.io/apidocs/usdt_swap/v1/en/#query-current-open-order-user_data + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + request = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if limit is not None: + request['limit'] = limit + marketType = None + marketType, params = self.handle_market_type_and_params('fetchOrders', market, params) + response = None + if marketType == 'spot': + response = self.privateGetApiV1SpotOpenOrders(self.extend(request, params)) + # + # [ + # { + # "accountId": "1783404067076253952", + # "exchangeId": "301", + # "symbol": "ETHUSDT", + # "symbolName": "ETHUSDT", + # "clientOrderId": "17561415157172008", + # "orderId": "2025056244339984384", + # "price": "3000", + # "origQty": "0.002", + # "executedQty": "0", + # "cummulativeQuoteQty": "0", + # "cumulativeQuoteQty": "0", + # "avgPrice": "0", + # "status": "NEW", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "side": "BUY", + # "stopPrice": "0.0", + # "icebergQty": "0.0", + # "time": "1756141516189", + # "updateTime": "1756141516198", + # "isWorking": True + # }, ... + # ] + # + else: + response = self.privateGetApiV1FuturesOpenOrders(self.extend(request, params)) + return self.parse_orders(response, market, since, limit) + + def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://toobit-docs.github.io/apidocs/spot/v1/en/#all-orders-user_data + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + request = {} + if limit is not None: + request['limit'] = limit + if since is not None: + request['startTime'] = since + request, params = self.handle_until_option('endTime', request, params) + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + marketType = None + marketType, params = self.handle_market_type_and_params('fetchOrders', market, params) + response = None + if marketType == 'spot': + response = self.privateGetApiV1SpotTradeOrders(request) + # + # [ + # { + # "accountId": "1783404067076253952", + # "exchangeId": "301", + # "symbol": "ETHUSDT", + # "symbolName": "ETHUSDT", + # "clientOrderId": "17561415157172008", + # "orderId": "2025056244339984384", + # "price": "3000", + # "origQty": "0.002", + # "executedQty": "0", + # "cummulativeQuoteQty": "0", + # "cumulativeQuoteQty": "0", + # "avgPrice": "0", + # "status": "NEW", + # "timeInForce": "GTC", + # "type": "LIMIT", + # "side": "BUY", + # "stopPrice": "0.0", + # "icebergQty": "0.0", + # "time": "1756141516189", + # "updateTime": "1756141516198", + # "isWorking": True + # }, ... + # ] + # + else: + raise NotSupported(self.id + ' fetchOrders() is not supported for ' + marketType + ' markets') + return self.parse_orders(response, market, since, limit) + + def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://toobit-docs.github.io/apidocs/usdt_swap/v1/en/#query-history-orders-user_data + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + # returns the most recent closed or canceled orders up to circa two weeks ago + self.load_markets() + request = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if since is not None: + request['startTime'] = since + request, params = self.handle_until_option('endTime', request, params) + marketType = None + marketType, params = self.handle_market_type_and_params('fetchClosedOrders', market, params) + response = None + if marketType == 'spot': + raise NotSupported(self.id + ' fetchOrders() is not supported for ' + marketType + ' markets') + else: + response = self.privateGetApiV1FuturesHistoryOrders(request) + # + # [ + # { + # "time": "1756756879360", + # "updateTime": "1756757165956", + # "orderId": "2030218284767504128", + # "clientOrderId": "1756756876002", + # "symbol": "SOL-SWAP-USDT", + # "price": "144", + # "leverage": "50", + # "origQty": "1", + # "executedQty": "0", + # "executeQty": "0", + # "avgPrice": "0", + # "marginLocked": "0", + # "type": "LIMIT", + # "side": "BUY_OPEN", + # "timeInForce": "GTC", + # "status": "CANCELED", + # "priceType": "INPUT", + # "triggerType": "0", + # "fallType": "0", + # "activeStatus": "0" + # } + # ] + # + ordersList = [] + for i in range(0, len(response)): + ordersList.append({'result': response[i]}) + return self.parse_orders(ordersList, market, since, limit) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://toobit-docs.github.io/apidocs/spot/v1/en/#account-trade-list-user_data + https://toobit-docs.github.io/apidocs/usdt_swap/v1/en/#account-trade-list-user_data + + :param str [symbol]: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trade structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch trades for + :returns Trade[]: a list of `trade structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMyTrades() requires a symbol argument') + self.load_markets() + request: dict = {} + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + market = self.market(symbol) + request['symbol'] = market['id'] + marketType = None + marketType, params = self.handle_market_type_and_params('fetchMyTrades', market, params) + request, params = self.handle_until_option('endTime', request, params) + response = None + if marketType == 'spot': + response = self.privateGetApiV1AccountTrades(self.extend(request, params)) + # + # [ + # { + # "id": "2024934575206059008", + # "symbol": "ETHUSDT", + # "symbolName": "ETHUSDT", + # "orderId": "2024934575097029888", + # "price": "4641.21", + # "qty": "0.001", + # "commission": "0.00464121", + # "commissionAsset": "USDT", + # "time": "1756127012094", + # "isBuyer": False, + # "isMaker": False, + # "fee": { + # "feeCoinId": "USDT", + # "feeCoinName": "USDT", + # "fee": "0.00464121" + # }, + # "feeCoinId": "USDT", + # "feeAmount": "0.00464121", + # "makerRebate": "0", + # "ticketId": "4864450547563401875" + # }, ... + # + else: + response = self.privateGetApiV1FuturesUserTrades(request) + # + # [ + # { + # "time": "1756758426899", + # "id": "2030231266499116032", + # "orderId": "2030231266373265152", + # "symbol": "DOGE-SWAP-USDT", + # "price": "0.21191", + # "qty": "63", + # "commissionAsset": "USDT", + # "commission": "0.00801019", + # "makerRebate": "0", + # "type": "LIMIT", + # "side": "BUY_OPEN", + # "realizedPnl": "0", + # "ticketId": "4900760819871364854", + # "isMaker": False + # } + # ] + # + return self.parse_trades(response, market, since, limit) + + def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://open.big.one/docs/spot_transfer.html#transfer-of-user + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: 'spot', 'swap' + :param str toAccount: 'spot', 'swap' + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transfer structure ` + """ + self.load_markets() + currency = self.currency(code) + accountsByType = self.safe_dict(self.options, 'accountsByType', {}) + fromId = self.safe_string(accountsByType, fromAccount, fromAccount) + toId = self.safe_string(accountsByType, toAccount, toAccount) + request: dict = { + 'asset': currency['id'], + 'quantity': self.currency_to_precision(code, amount), + 'fromAccountType': fromId, + 'toAccountType': toId, + } + response = self.privatePostApiV1SubAccountTransfer(self.extend(request, params)) + # + # { + # "code": 200, # 200 = success + # "msg": "success" # response message + # } + # + return self.parse_transfer(response, currency) + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + # + # { + # "code": 200, # 200 = success + # "msg": "success" # response message + # } + # + return { + 'info': transfer, + 'id': None, + 'timestamp': None, + 'datetime': None, + 'currency': None, + 'amount': None, + 'fromAccount': None, + 'toAccount': None, + 'status': None, + } + + def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + + https://toobit-docs.github.io/apidocs/spot/v1/en/#get-account-transaction-history-list-user_data + https://toobit-docs.github.io/apidocs/usdt_swap/v1/en/#get-future-account-transaction-history-list-user_data + + :param str [code]: unified currency code, default is None + :param int [since]: timestamp in ms of the earliest ledger entry, default is None + :param int [limit]: max number of ledger entries to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: end time in ms + :returns dict: a `ledger structure ` + """ + self.load_markets() + currency = None + request: dict = {} + if code is not None: + currency = self.currency(code) + request['coin'] = currency['id'] + if since is not None: + request['startTime'] = since + request, params = self.handle_until_option('endTime', request, params) + if limit is not None: + request['limit'] = limit + marketType = None + marketType, params = self.handle_market_type_and_params('cancelAllOrders', None, params) + response = None + if marketType == 'spot': + response = self.privateGetApiV1AccountBalanceFlow(self.extend(request, params)) + else: + response = self.privateGetApiV1FuturesBalanceFlow(self.extend(request, params)) + # + # both answers are same format + # + # [ + # { + # "id": "539870570957903104", + # "accountId": "122216245228131", + # "coin": "BTC", + # "coinId": "BTC", + # "coinName": "BTC", + # "flowTypeValue": 51, + # "flowType": "USER_ACCOUNT_TRANSFER", + # "flowName": "Transfer", + # "change": "-12.5", + # "total": "379.624059937852365", + # "created": "1579093587214" + # }, + # + return self.parse_ledger(response, currency, since, limit) + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + currencyId = self.safe_string(item, 'coinId') + currency = self.safe_currency(currencyId, currency) + timestamp = self.safe_integer(item, 'created') + after = self.safe_number(item, 'total') + amountRaw = self.safe_string(item, 'change') + amount = self.parse_number(Precise.string_abs(amountRaw)) + direction = 'in' + if amountRaw.startswith('-'): + direction = 'out' + return self.safe_ledger_entry({ + 'info': item, + 'id': self.safe_string(item, 'id'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'direction': direction, + 'account': None, + 'referenceId': None, + 'referenceAccount': None, + 'type': self.parse_ledger_type(self.safe_string(item, 'flowType')), + 'currency': currency['code'], + 'amount': amount, + 'before': None, + 'after': after, + 'status': None, + 'fee': None, + }, currency) + + def parse_ledger_type(self, type): + types: dict = { + 'USER_ACCOUNT_TRANSFER': 'transfer', + 'AIRDROP': 'rebate', + } + return self.safe_string(types, type, type) + + def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://toobit-docs.github.io/apidocs/usdt_swap/v1/en/#user-trade-fee-rate-user_data + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + self.load_markets() + response = None + marketType = None + market = None + marketType, params = self.handle_market_type_and_params('fetchTradingFees', None, params) + if marketType == 'spot': + raise NotSupported(self.id + ' fetchTradingFees(): does not support ' + marketType + ' markets') + elif self.in_array(marketType, ['swap', 'future']): + symbol: Str = None + symbol, params = self.handle_param_string(params, 'symbol') + if symbol is None: + raise BadRequest(self.id + ' fetchTradingFees requires a params["symbol"]') + market = self.market(symbol) + request = { + 'symbol': market['id'], + } + response = self.privateGetApiV1FuturesCommissionRate(self.extend(request, params)) + # + # { + # "openMakerFee": "0.000006", # The trade fee rate for opening pending orders + # "openTakerFee": "0.0001", # The trade fee rate for open position taker + # "closeMakerFee": "0.0002", # The trade fee rate for closing pending orders + # "closeTakerFee": "0.0004" # The trade fee rate for closing a taker order + # } + # + result: dict = {} + entry = response + marketId = self.safe_string(entry, 'symbol') + market = self.safe_market(marketId, market) + fee = self.parse_trading_fee(entry, market) + result[market['symbol']] = fee + return result + + def parse_trading_fee(self, data, market: Market = None): + marketId = self.safe_string(data, 'symbol') + return { + 'info': data, + 'symbol': self.safe_symbol(marketId, market), + 'maker': self.safe_number(data, 'closeMakerFee'), + 'taker': self.safe_number(data, 'closeTakerFee'), + 'percentage': None, + 'tierBased': None, + } + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://toobit-docs.github.io/apidocs/spot/v1/en/#deposit-history-user_data + + :param str [code]: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposit structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + return self.fetch_deposits_or_withdrawals_helper('deposits', code, since, limit, params) + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://toobit-docs.github.io/apidocs/spot/v1/en/#withdrawal-records-user_data + + :param str [code]: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawal structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + return self.fetch_deposits_or_withdrawals_helper('withdrawals', code, since, limit, params) + + def fetch_deposits_or_withdrawals_helper(self, type, code, since, limit, params): + self.load_markets() + currency = None + request: dict = {} + if code is not None: + currency = self.currency(code) + request['coin'] = currency['id'] + if since is not None: + request['startTime'] = since + request, params = self.handle_until_option('endTime', request, params) + if limit is not None: + request['limit'] = limit + response = None + if type == 'deposits': + response = self.privateGetApiV1AccountDepositOrders(self.extend(request, params)) + # + # [ + # { + # "time": 1499865549590, + # "id": 100234, + # "coinName": "EOS", + # "statusCode": "DEPOSIT_CAN_WITHDRAW", + # "status": "2", # 2=SUCCESS, 11=REJECT, 12=AUDIT + # "address": "deposit2bb", + # "txId": "98A3EA560C6B3336D348B6C83F0F95ECE4F1F5919E94BD006E5BF3BF264FACFC", + # "txIdUrl": "", + # "requiredConfirmTimes": "5", + # "confirmTimes": "5", + # "quantity": "1.01", + # "coin": "EOS", + # "fromAddress": "clarkkent", + # "fromAddressTag": "19029901" + # "addressTag": "19012584", + # } + # ] + # + elif type == 'withdrawals': + response = self.privateGetApiV1AccountWithdrawOrders(self.extend(request, params)) + # + # [ + # { + # "time":"1536232111669", + # "id ":"90161227158286336", + # "accountId":"517256161325920", + # "coinName":"BHC", + # "statusCode":"PROCESSING_STATUS", + # "status":3, + # "address":"0x815bF1c3cc0f49b8FC66B21A7e48fCb476051209", + # "txId ":"", + # "txIdUrl ":"", + # "requiredConfirmTimes ":0, # Number of confirmation requests + # "confirmTimes ":0, # number of confirmations + # "quantity":"14", # Withdrawal amount + # "coinId ":"BHC", + # "addressExt":"address tag", + # "arriveQuantity":"14", + # "walletHandleTime":"1536232111669", + # "feeCoinId ":"BHC", + # "feeCoinName ":"BHC", + # "fee":"0.1", + # "kernelId":"", # Exclusive to BEAM and GRIN + # "isInternalTransfer": False # Whether internal transfer + # } + # ] + # + return self.parse_transactions(response, currency, since, limit, params) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fetchDeposits & fetchWithdrawals + # + # { + # "time": 1499865549590, + # "id": 100234, + # "coinName": "EOS", + # "statusCode": "DEPOSIT_CAN_WITHDRAW", + # "status": "2", # 2=SUCCESS, 11=REJECT, 12=AUDIT + # "address": "deposit2bb", + # "txId": "98A3EA560C6B3336D348B6C83F0F95ECE4F1F5919E94BD006E5BF3BF264FACFC", + # "txIdUrl": "", + # "requiredConfirmTimes": "5", + # "confirmTimes": "5", + # "quantity": "1.01", + # "coin": "EOS", # present in "fetchDeposits" + # "coinId ":"BHC", # present in "fetchWithdrawals" + # "addressTag": "19012584", # present in "fetchDeposits" + # "addressExt":"address tag", # present in "fetchWithdrawals" + # "fromAddress": "clarkkent", # present in "fetchDeposits" + # "fromAddressTag": "19029901" # present in "fetchDeposits" + # "arriveQuantity":"14", # present in "fetchWithdrawals" + # "walletHandleTime":"1536232111669",// present in "fetchWithdrawals" + # "feeCoinId ":"BHC", # present in "fetchWithdrawals" + # "feeCoinName ":"BHC", # present in "fetchWithdrawals" + # "fee":"0.1", # present in "fetchWithdrawals" + # "kernelId":"", # present in "fetchWithdrawals" + # "isInternalTransfer": False # present in "fetchWithdrawals" + # } + # + # withdraw + # + # { + # "status": 0, + # "success": True, + # "needBrokerAudit": False, # Do you need a brokerage review? + # "id": "423885103582776064", + # "refuseReason":"" # failure rejection reason + # } + # + timestamp = self.safe_integer(transaction, 'time') + currencyId = self.safe_string_2(transaction, 'coin', 'coinId') + code = self.safe_currency_code(currencyId, currency) + feeString = self.safe_string(transaction, 'fee') + feeCoin = self.safe_string(transaction, 'feeCoinName') + fee = None + if feeString is not None: + fee = { + 'cost': self.parse_number(feeString), + 'currency': self.safe_currency_code(feeCoin), + } + tagTo = self.safe_string_2(transaction, 'addressTag', 'addressExt') + tagFrom = self.safe_string(transaction, 'fromAddressTag') + addressTo = self.safe_string(transaction, 'address') + addressFrom = self.safe_string(transaction, 'fromAddress') + isWithdraw = ('arriveQuantity' in transaction) + type = 'withdrawal' if isWithdraw else 'deposit' + return { + 'info': transaction, + 'id': self.safe_string(transaction, 'id'), + 'txid': self.safe_string(transaction, 'txId'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': None, + 'address': None, + 'addressTo': addressTo, + 'addressFrom': addressFrom, + 'tag': None, + 'tagTo': tagTo, + 'tagFrom': tagFrom, + 'type': type, + 'amount': self.safe_number(transaction, 'quantity'), + 'currency': code, + 'status': self.parse_transaction_status(self.safe_string(transaction, 'status')), + 'updated': None, + 'fee': fee, + 'comment': None, + 'internal': None, + } + + def parse_transaction_status(self, status: Str): + statuses: dict = { + '2': 'pending', + '12': 'pending', + '11': 'failed', + '3': 'ok', + } + return self.safe_string(statuses, status, status) + + def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://toobit-docs.github.io/apidocs/spot/v1/en/#deposit-address-user_data + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'coin': currency['id'], + } + networkCode, paramsOmitted = self.handle_network_code_and_params(self.extend(request, params)) + if networkCode is None: + raise ArgumentsRequired(self.id + ' fetchDepositAddress() : param["network"] is required') + request['chainType'] = self.network_code_to_id(networkCode) + response = self.privateGetApiV1AccountDepositAddress(self.extend(request, paramsOmitted)) + # + # { + # "canDeposit":false,//Is it possible to recharge + # "address":"0x815bF1c3cc0f49b8FC66B21A7e48fCb476051209", + # "addressExt":"address tag", + # "minQuantity":"100",//minimum amount + # "requiredConfirmTimes ":1,//Arrival confirmation number + # "canWithdrawConfirmNum ":12,//Withdrawal confirmation number + # "coinType":"ERC20_TOKEN" + # } + # + return self.parse_deposit_address(response, currency) + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + address = self.safe_string(depositAddress, 'address') + self.check_address(address) + return { + 'info': depositAddress, + 'currency': self.safe_string(currency, 'code'), + 'network': None, + 'address': address, + 'tag': self.safe_string(depositAddress, 'addressExt'), + } + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://toobit-docs.github.io/apidocs/spot/v1/en/#withdraw-user_data + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: a memo for the transaction + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + self.check_address(address) + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + if networkCode is None: + raise ArgumentsRequired(self.id + ' withdraw() : param["network"] is required') + self.load_markets() + currency = self.currency(code) + request: dict = { + 'coin': currency['id'], + 'address': address, + 'quantity': self.currency_to_precision(currency['code'], amount), + 'network': networkCode, + } + if tag is not None: + request['addressExt'] = tag + response = self.privatePostApiV1AccountWithdraw(self.extend(request, params)) + # + # { + # "status": 0, + # "success": True, + # "needBrokerAudit": False, # Do you need a brokerage review? + # "id": "423885103582776064", # Withdrawal successful order id + # "refuseReason":"" # failure rejection reason + # } + # + return self.parse_transaction(response, currency) + + def set_margin_mode(self, marginMode: str, symbol: Str = None, params={}): + """ + set margin mode to 'cross' or 'isolated' + + https://toobit-docs.github.io/apidocs/usdt_swap/v1/en/#change-margin-type-trade + + :param str marginMode: 'cross' or 'isolated' + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setMarginMode() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + if market['type'] != 'swap': + raise BadSymbol(self.id + ' setMarginMode() supports swap contracts only') + marginMode = marginMode.upper() + request: dict = { + 'symbol': market['id'], + 'marginType': marginMode, + } + response = self.privatePostApiV1FuturesMarginType(self.extend(request, params)) + # + # {"code":200,"symbolId":"BTC-SWAP-USDT","marginType":"ISOLATED"} + # + return response + + def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://toobit-docs.github.io/apidocs/usdt_swap/v1/en/#change-initial-leverage-trade + + :param float leverage: the rate of leverage + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setLeverage() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'leverage': leverage, + } + response = self.privatePostApiV1FuturesLeverage(self.extend(request, params)) + # + # {"code":200,"symbolId":"BTC-SWAP-USDT","leverage":"19"} + # + return response + + def fetch_leverage(self, symbol: str, params={}) -> Leverage: + """ + fetch the set leverage for a market + + https://toobit-docs.github.io/apidocs/usdt_swap/v1/en/#get-the-leverage-multiple-and-position-mode-user_data + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `leverage structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = self.privateGetApiV1FuturesAccountLeverage(self.extend(request, params)) + # + # [ + # { + # "symbol":"BTC-SWAP-USDT", #symbol + # "leverage":"20", # leverage + # "marginType":"CROSS" # CROSS;ISOLATED + # } + # ] + # + data = self.safe_dict(response, 'data', {}) + return self.parse_leverage(data, market) + + def parse_leverage(self, leverage: dict, market: Market = None) -> Leverage: + marketId = self.safe_string(leverage, 'symbol') + leverageValue = self.safe_integer(leverage, 'leverage') + marginType = self.safe_string(leverage, 'marginType') + marginMode = 'cross' if (marginType == 'crossed') else 'isolated' + return { + 'info': leverage, + 'symbol': self.safe_symbol(marketId, market), + 'marginMode': marginMode, + 'longLeverage': leverageValue, + 'shortLeverage': leverageValue, + } + + def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://toobit-docs.github.io/apidocs/usdt_swap/v1/en/#query-position-user_data + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structures ` + """ + self.load_markets() + request = {} + market = None + if symbols is not None: + length = len(symbols) + if length > 1: + raise BadRequest(self.id + ' fetchPositions() only accepts an array with a single symbol or without symbols argument') + firstSymbol = self.safe_string(symbols, 0) + if firstSymbol is not None: + market = self.market(firstSymbol) + request['symbol'] = market['id'] + response = self.privateGetApiV1FuturesPositions(self.extend(request, params)) + # + # [ + # { + # "symbol": "DOGE-SWAP-USDT", + # "side": "LONG", + # "avgPrice": "0.21191", + # "position": "63", + # "available": "63", + # "leverage": "25", + # "lastPrice": "0.20932", + # "positionValue": "13.3503", + # "flp": "0.05471", + # "margin": "0.5262", + # "marginRate": "", + # "unrealizedPnL": "-0.1701", + # "profitRate": "-0.3185", + # "realizedPnL": "-0.008", + # "minMargin": "0", + # "maxNotionalValue": "10000000", + # "markPrice": "0.20921" + # } + # ] + # + return self.parse_positions(response, symbols) + + def parse_position(self, position: dict, market: Market = None): + marketId = self.safe_string(position, 'symbol') + market = self.safe_market(marketId, market) + side = self.safe_string_lower(position, 'side') + quantity = self.safe_string(position, 'position') + leverage = self.safe_integer(position, 'leverage') + return self.safe_position({ + 'info': position, + 'id': self.safe_string(position, 'id'), + 'symbol': market['symbol'], + 'entryPrice': self.safe_string(position, 'avgPrice'), + 'markPrice': self.safe_string(position, 'markPrice'), + 'lastPrice': self.safe_string(position, 'lastPrice'), + 'notional': self.safe_string(position, 'positionValue'), + 'collateral': None, + 'unrealizedPnl': self.safe_string(position, 'unrealizedPnL'), + 'side': side, + 'contracts': self.parse_number(quantity), + 'contractSize': None, + 'timestamp': None, + 'datetime': None, + 'hedged': None, + 'maintenanceMargin': None, + 'maintenanceMarginPercentage': None, + 'initialMargin': self.safe_string(position, 'margin'), + 'initialMarginPercentage': None, + 'leverage': leverage, + 'liquidationPrice': None, + 'marginRatio': None, + 'marginMode': None, + 'percentage': None, + }) + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = self.urls['api'][api] + '/' + self.implode_params(path, params) + isPost = method == 'POST' + isDelete = method == 'DELETE' + extraQuery = {} + query = self.omit(params, self.extract_params(path)) + if api != 'private': + # Public endpoints + if not isPost: + if query: + url += '?' + self.urlencode(query) + else: + self.check_required_credentials() + timestamp = self.milliseconds() + # Add timestamp to parameters for signed endpoints + extraQuery['recvWindow'] = self.safe_string(self.options, 'recvWindow', '5000') + extraQuery['timestamp'] = str(timestamp) + queryExtended = self.extend(query, extraQuery) + queryString = '' + if isPost or isDelete: + # everything else except Batch-Orders + if not isinstance(params, list): + body = self.urlencode(queryExtended) + else: + queryString = self.urlencode(extraQuery) + body = self.json(query) + else: + queryString = self.urlencode(queryExtended) + payload = queryString + if body is not None: + payload = body + payload + signature = self.hmac(self.encode(payload), self.encode(self.secret), hashlib.sha256, 'hex') + if queryString != '': + queryString += '&signature=' + signature + url += '?' + queryString + else: + body += '&signature=' + signature + headers = { + 'Referrer': 'CCXT', + 'X-BB-APIKEY': self.apiKey, + 'Content-Type': 'application/x-www-form-urlencoded', + } + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None + errorCode = self.safe_string(response, 'code') + message = self.safe_string(response, 'msg') + if errorCode and errorCode != '200' and errorCode != '0': + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + raise ExchangeError(feedback) + return None diff --git a/ccxt/upbit.py b/ccxt/upbit.py new file mode 100644 index 0000000..39804fb --- /dev/null +++ b/ccxt/upbit.py @@ -0,0 +1,2246 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.upbit import ImplicitAPI +from ccxt.base.types import Any, Balances, Currency, DepositAddress, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, OrderBooks, Trade, TradingFeeInterface, TradingFees, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import AddressPending +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class upbit(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(upbit, self).describe(), { + 'id': 'upbit', + 'name': 'Upbit', + 'countries': ['KR'], + 'version': 'v1', + 'rateLimit': 50, + 'pro': True, + # new metainfo interface + 'has': { + 'CORS': True, + 'spot': True, + 'margin': None, + 'swap': False, + 'future': False, + 'option': False, + 'cancelOrder': True, + 'createDepositAddress': True, + 'createMarketBuyOrderWithCost': True, + 'createMarketOrder': True, + 'createMarketOrderWithCost': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'editOrder': True, + 'fetchBalance': True, + 'fetchCanceledOrders': True, + 'fetchClosedOrders': True, + 'fetchCurrencies': False, + 'fetchDeposit': True, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': True, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchFundingHistory': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchIndexOHLCV': False, + 'fetchMarginMode': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMyTrades': False, + 'fetchOHLCV': True, + 'fetchOpenInterestHistory': False, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrderBooks': True, + 'fetchOrders': False, + 'fetchPositionMode': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTrades': True, + 'fetchTradingFee': True, + 'fetchTradingFees': True, + 'fetchTransactions': False, + 'fetchWithdrawal': True, + 'fetchWithdrawals': True, + 'transfer': False, + 'withdraw': True, + }, + 'timeframes': { + '1s': 'seconds', + '1m': 'minutes', + '3m': 'minutes', + '5m': 'minutes', + '10m': 'minutes', + '15m': 'minutes', + '30m': 'minutes', + '1h': 'minutes', + '4h': 'minutes', + '1d': 'days', + '1w': 'weeks', + '1M': 'months', + '1y': 'years', + }, + 'hostname': 'api.upbit.com', + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/49245610-eeaabe00-f423-11e8-9cba-4b0aed794799.jpg', + 'api': { + 'public': 'https://{hostname}', + 'private': 'https://{hostname}', + }, + 'www': 'https://upbit.com', + 'doc': 'https://docs.upbit.com/docs/%EC%9A%94%EC%B2%AD-%EC%88%98-%EC%A0%9C%ED%95%9C', + 'fees': 'https://upbit.com/service_center/guide', + }, + 'api': { + # 'endpoint','API Cost' + # cost = 1000 / (rateLimit * RPS) + 'public': { + 'get': { + 'market/all': 2, # RPS: 10 + 'candles/{timeframe}': 2, + 'candles/{timeframe}/{unit}': 2, + 'candles/seconds': 2, + 'candles/minutes/{unit}': 2, + 'candles/minutes/1': 2, + 'candles/minutes/3': 2, + 'candles/minutes/5': 2, + 'candles/minutes/10': 2, + 'candles/minutes/15': 2, + 'candles/minutes/30': 2, + 'candles/minutes/60': 2, + 'candles/minutes/240': 2, + 'candles/days': 2, + 'candles/weeks': 2, + 'candles/months': 2, + 'candles/years': 2, + 'trades/ticks': 2, + 'ticker': 2, + 'ticker/all': 2, + 'orderbook': 2, + 'orderbook/instruments': 2, + 'orderbook/supported_levels': 2, # Upbit KR only, deprecatd + }, + }, + 'private': { + 'get': { + 'accounts': 0.67, # RPS: 30 + 'orders/chance': 0.67, + 'order': 0.67, + 'orders/closed': 0.67, + 'orders/open': 0.67, + 'orders/uuids': 0.67, + 'withdraws': 0.67, + 'withdraw': 0.67, + 'withdraws/chance': 0.67, + 'withdraws/coin_addresses': 0.67, + 'deposits': 0.67, + 'deposits/chance/coin': 0.67, + 'deposit': 0.67, + 'deposits/coin_addresses': 0.67, + 'deposits/coin_address': 0.67, + 'travel_rule/vasps': 0.67, + 'status/wallet': 0.67, # Upbit KR only + 'api_keys': 0.67, # Upbit KR only + }, + 'post': { + 'orders': 2.5, # RPS: 8 + 'orders/cancel_and_new': 2.5, # RPS: 8 + 'withdraws/coin': 0.67, + 'withdraws/krw': 0.67, # Upbit KR only. + 'deposits/krw': 0.67, # Upbit KR only. + 'deposits/generate_coin_address': 0.67, + 'travel_rule/deposit/uuid': 0.67, # RPS: 30, but each deposit can only be queried once every 10 minutes + 'travel_rule/deposit/txid': 0.67, # RPS: 30, but each deposit can only be queried once every 10 minutes + }, + 'delete': { + 'order': 0.67, + 'orders/open': 40, # RPS: 0.5 + 'orders/uuids': 0.67, + }, + }, + }, + 'fees': { + 'trading': { + 'tierBased': False, + 'percentage': True, + 'maker': self.parse_number('0.0025'), + 'taker': self.parse_number('0.0025'), + }, + 'funding': { + 'tierBased': False, + 'percentage': False, + 'withdraw': {}, + 'deposit': {}, + }, + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': False, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'leverage': False, + 'marketBuyByCost': False, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': True, + 'trailing': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': None, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': True, + 'limit': 100, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, # todo + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 100000, + 'daysBackCanceled': 1, + 'untilDays': 7, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 200, + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'exact': { + 'This key has expired.': AuthenticationError, + 'Missing request parameter error. Check the required parameters!': BadRequest, + 'side is missing, side does not have a valid value': InvalidOrder, + }, + 'broad': { + 'thirdparty_agreement_required': PermissionDenied, + 'out_of_scope': PermissionDenied, + 'order_not_found': OrderNotFound, + 'insufficient_funds': InsufficientFunds, + 'invalid_access_key': AuthenticationError, + 'jwt_verification': AuthenticationError, + 'create_ask_error': ExchangeError, + 'create_bid_error': ExchangeError, + 'volume_too_large': InvalidOrder, + 'invalid_funds': InvalidOrder, + }, + }, + 'options': { + 'createMarketBuyOrderRequiresPrice': True, + 'tradingFeesByQuoteCurrency': { + 'KRW': 0.0005, + }, + }, + 'commonCurrencies': { + 'TON': 'Tokamak Network', + }, + }) + + def fetch_currency(self, code: str, params={}): + # self method is for retrieving funding fees and limits per currency + # it requires private access and API keys properly set up + self.load_markets() + currency = self.currency(code) + return self.fetch_currency_by_id(currency['id'], params) + + def fetch_currency_by_id(self, id: str, params={}): + # self method is for retrieving funding fees and limits per currency + # it requires private access and API keys properly set up + request: dict = { + 'currency': id, + } + response = self.privateGetWithdrawsChance(self.extend(request, params)) + # + # { + # "member_level": { + # "security_level": 3, + # "fee_level": 0, + # "email_verified": True, + # "identity_auth_verified": True, + # "bank_account_verified": True, + # "kakao_pay_auth_verified": False, + # "locked": False, + # "wallet_locked": False + # }, + # "currency": { + # "code": "BTC", + # "withdraw_fee": "0.0005", + # "is_coin": True, + # "wallet_state": "working", + # "wallet_support": ["deposit", "withdraw"] + # }, + # "account": { + # "currency": "BTC", + # "balance": "10.0", + # "locked": "0.0", + # "avg_krw_buy_price": "8042000", + # "modified": False + # }, + # "withdraw_limit": { + # "currency": "BTC", + # "minimum": null, + # "onetime": null, + # "daily": "10.0", + # "remaining_daily": "10.0", + # "remaining_daily_krw": "0.0", + # "fixed": null, + # "can_withdraw": True + # } + # } + # + memberInfo = self.safe_value(response, 'member_level', {}) + currencyInfo = self.safe_value(response, 'currency', {}) + withdrawLimits = self.safe_value(response, 'withdraw_limit', {}) + canWithdraw = self.safe_value(withdrawLimits, 'can_withdraw') + walletState = self.safe_string(currencyInfo, 'wallet_state') + walletLocked = self.safe_value(memberInfo, 'wallet_locked') + locked = self.safe_value(memberInfo, 'locked') + active = True + if (canWithdraw is not None) and not canWithdraw: + active = False + elif walletState != 'working': + active = False + elif (walletLocked is not None) and walletLocked: + active = False + elif (locked is not None) and locked: + active = False + maxOnetimeWithdrawal = self.safe_string(withdrawLimits, 'onetime') + maxDailyWithdrawal = self.safe_string(withdrawLimits, 'daily', maxOnetimeWithdrawal) + remainingDailyWithdrawal = self.safe_string(withdrawLimits, 'remaining_daily', maxDailyWithdrawal) + maxWithdrawLimit = None + if Precise.string_gt(remainingDailyWithdrawal, '0'): + maxWithdrawLimit = remainingDailyWithdrawal + else: + maxWithdrawLimit = maxDailyWithdrawal + currencyId = self.safe_string(currencyInfo, 'code') + code = self.safe_currency_code(currencyId) + return { + 'info': response, + 'id': currencyId, + 'code': code, + 'name': code, + 'active': active, + 'fee': self.safe_number(currencyInfo, 'withdraw_fee'), + 'precision': None, + 'limits': { + 'withdraw': { + 'min': self.safe_number(withdrawLimits, 'minimum'), + 'max': self.parse_number(maxWithdrawLimit), + }, + }, + } + + def fetch_market(self, symbol: str, params={}): + # self method is for retrieving trading fees and limits per market + # it requires private access and API keys properly set up + self.load_markets() + market = self.market(symbol) + return self.fetch_market_by_id(market['id'], params) + + def fetch_market_by_id(self, id: str, params={}): + # self method is for retrieving trading fees and limits per market + # it requires private access and API keys properly set up + request: dict = { + 'market': id, + } + response = self.privateGetOrdersChance(self.extend(request, params)) + # + # { + # "bid_fee": "0.0015", + # "ask_fee": "0.0015", + # "market": { + # "id": "KRW-BTC", + # "name": "BTC/KRW", + # "order_types": ["limit"], + # "order_sides": ["ask", "bid"], + # "bid": {"currency": "KRW", "price_unit": null, "min_total": 1000}, + # "ask": {"currency": "BTC", "price_unit": null, "min_total": 1000}, + # "max_total": "100000000.0", + # "state": "active", + # }, + # "bid_account": { + # "currency": "KRW", + # "balance": "0.0", + # "locked": "0.0", + # "avg_buy_price": "0", + # "avg_buy_price_modified": False, + # "unit_currency": "KRW", + # }, + # "ask_account": { + # "currency": "BTC", + # "balance": "10.0", + # "locked": "0.0", + # "avg_buy_price": "8042000", + # "avg_buy_price_modified": False, + # "unit_currency": "KRW", + # } + # } + # + marketInfo = self.safe_value(response, 'market') + bid = self.safe_value(marketInfo, 'bid') + ask = self.safe_value(marketInfo, 'ask') + marketId = self.safe_string(marketInfo, 'id') + baseId = self.safe_string(ask, 'currency') + quoteId = self.safe_string(bid, 'currency') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + state = self.safe_string(marketInfo, 'state') + bidFee = self.safe_string(response, 'bid_fee') + askFee = self.safe_string(response, 'ask_fee') + fee = self.parse_number(Precise.string_max(bidFee, askFee)) + return self.safe_market_structure({ + 'id': marketId, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': (state == 'active'), + 'contract': False, + 'linear': None, + 'inverse': None, + 'taker': fee, + 'maker': fee, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number('1e-8'), + 'price': self.parse_number('1e-8'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(ask, 'min_total'), + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': self.safe_number(bid, 'min_total'), + 'max': self.safe_number(marketInfo, 'max_total'), + }, + 'info': response, + }, + }) + + def fetch_markets(self, params={}) -> List[Market]: + """ + + https://docs.upbit.com/kr/reference/마켓-코드-조회 + https://global-docs.upbit.com/reference/listing-market-list + + retrieves data on all markets for upbit + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = self.publicGetMarketAll(params) + # + # [ + # { + # "market": "KRW-BTC", + # "korean_name": "비트코인", + # "english_name": "Bitcoin" + # }, + # ..., + # ] + # + return self.parse_markets(response) + + def parse_market(self, market: dict) -> Market: + id = self.safe_string(market, 'market') + quoteId, baseId = id.split('-') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + return self.safe_market_structure({ + 'id': id, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': True, + 'contract': False, + 'linear': None, + 'inverse': None, + 'taker': self.safe_number(self.options['tradingFeesByQuoteCurrency'], quote, self.fees['trading']['taker']), + 'maker': self.safe_number(self.options['tradingFeesByQuoteCurrency'], quote, self.fees['trading']['maker']), + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'price': self.parse_number('1e-8'), + 'amount': self.parse_number('1e-8'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': None, + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + 'info': market, + }) + + def parse_balance(self, response) -> Balances: + result: dict = { + 'info': response, + 'timestamp': None, + 'datetime': None, + } + for i in range(0, len(response)): + balance = response[i] + currencyId = self.safe_string(balance, 'currency') + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(balance, 'balance') + account['used'] = self.safe_string(balance, 'locked') + result[code] = account + return self.safe_balance(result) + + def fetch_balance(self, params={}) -> Balances: + """ + + https://docs.upbit.com/kr/reference/전체-계좌-조회 + https://global-docs.upbit.com/reference/overall-account-inquiry + + query for balance and get the amount of funds available for trading or funds locked in orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + response = self.privateGetAccounts(params) + # + # [{ currency: "BTC", + # "balance": "0.005", + # "locked": "0.0", + # "avg_krw_buy_price": "7446000", + # "modified": False }, + # { currency: "ETH", + # "balance": "0.1", + # "locked": "0.0", + # "avg_krw_buy_price": "250000", + # "modified": False } ] + # + return self.parse_balance(response) + + def fetch_order_books(self, symbols: Strings = None, limit: Int = None, params={}) -> OrderBooks: + """ + + https://docs.upbit.com/kr/reference/호가-정보-조회 + https://global-docs.upbit.com/reference/order-book-list + + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data for multiple markets + :param str[]|None symbols: list of unified market symbols, all symbols fetched if None, default is None + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `order book structures ` indexed by market symbol + """ + self.load_markets() + ids = None + if symbols is None: + ids = ','.join(self.ids) + else: + ids = self.market_ids(symbols) + ids = ','.join(ids) + request: dict = { + 'markets': ids, + # 'count': limit, + } + if limit is not None: + request['count'] = limit + response = self.publicGetOrderbook(self.extend(request, params)) + # + # [{ market: "BTC-ETH", + # "timestamp": 1542899030043, + # "total_ask_size": 109.57065201, + # "total_bid_size": 125.74430631, + # "orderbook_units": [{ask_price: 0.02926679, + # "bid_price": 0.02919904, + # "ask_size": 4.20293961, + # "bid_size": 11.65043576}, + # ..., + # {ask_price: 0.02938209, + # "bid_price": 0.0291231, + # "ask_size": 0.05135782, + # "bid_size": 13.5595 } ]}, + # { market: "KRW-BTC", + # "timestamp": 1542899034662, + # "total_ask_size": 12.89790974, + # "total_bid_size": 4.88395783, + # "orderbook_units": [{ask_price: 5164000, + # "bid_price": 5162000, + # "ask_size": 2.57606495, + # "bid_size": 0.214 }, + # ..., + # {ask_price: 5176000, + # "bid_price": 5152000, + # "ask_size": 2.752, + # "bid_size": 0.4650305} ]} ] + # + result: dict = {} + for i in range(0, len(response)): + orderbook = response[i] + marketId = self.safe_string(orderbook, 'market') + symbol = self.safe_symbol(marketId, None, '-') + timestamp = self.safe_integer(orderbook, 'timestamp') + result[symbol] = { + 'symbol': symbol, + 'bids': self.sort_by(self.parse_bids_asks(orderbook['orderbook_units'], 'bid_price', 'bid_size'), 0, True), + 'asks': self.sort_by(self.parse_bids_asks(orderbook['orderbook_units'], 'ask_price', 'ask_size'), 0), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'nonce': None, + } + return result + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + + https://docs.upbit.com/kr/reference/호가-정보-조회 + https://global-docs.upbit.com/reference/order-book-list + + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + orderbooks = self.fetch_order_books([symbol], limit, params) + return self.safe_value(orderbooks, symbol) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { market: "BTC-ETH", + # "trade_date": "20181122", + # "trade_time": "104543", + # "trade_date_kst": "20181122", + # "trade_time_kst": "194543", + # "trade_timestamp": 1542883543096, + # "opening_price": 0.02976455, + # "high_price": 0.02992577, + # "low_price": 0.02934283, + # "trade_price": 0.02947773, + # "prev_closing_price": 0.02966, + # "change": "FALL", + # "change_price": 0.00018227, + # "change_rate": 0.0061453136, + # "signed_change_price": -0.00018227, + # "signed_change_rate": -0.0061453136, + # "trade_volume": 1.00000005, + # "acc_trade_price": 100.95825586, + # "acc_trade_price_24h": 289.58650166, + # "acc_trade_volume": 3409.85311036, + # "acc_trade_volume_24h": 9754.40510513, + # "highest_52_week_price": 0.12345678, + # "highest_52_week_date": "2018-02-01", + # "lowest_52_week_price": 0.023936, + # "lowest_52_week_date": "2017-12-08", + # "timestamp": 1542883543813 } + # + timestamp = self.safe_integer(ticker, 'trade_timestamp') + marketId = self.safe_string_2(ticker, 'market', 'code') + market = self.safe_market(marketId, market, '-') + last = self.safe_string(ticker, 'trade_price') + return self.safe_ticker({ + 'symbol': market['symbol'], + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high_price'), + 'low': self.safe_string(ticker, 'low_price'), + 'bid': None, + 'bidVolume': None, + 'ask': None, + 'askVolume': None, + 'vwap': None, + 'open': self.safe_string(ticker, 'opening_price'), + 'close': last, + 'last': last, + 'previousClose': self.safe_string(ticker, 'prev_closing_price'), + 'change': self.safe_string(ticker, 'signed_change_price'), + 'percentage': self.safe_string(ticker, 'signed_change_rate'), + 'average': None, + 'baseVolume': self.safe_string(ticker, 'acc_trade_volume_24h'), + 'quoteVolume': self.safe_string(ticker, 'acc_trade_price_24h'), + 'info': ticker, + }, market) + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + + https://docs.upbit.com/kr/reference/ticker현재가-정보 + https://global-docs.upbit.com/reference/tickers + + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + symbols = self.market_symbols(symbols) + ids = None + if symbols is None: + ids = ','.join(self.ids) + else: + ids = self.market_ids(symbols) + ids = ','.join(ids) + request: dict = { + 'markets': ids, + } + response = self.publicGetTicker(self.extend(request, params)) + # + # [{ market: "BTC-ETH", + # "trade_date": "20181122", + # "trade_time": "104543", + # "trade_date_kst": "20181122", + # "trade_time_kst": "194543", + # "trade_timestamp": 1542883543097, + # "opening_price": 0.02976455, + # "high_price": 0.02992577, + # "low_price": 0.02934283, + # "trade_price": 0.02947773, + # "prev_closing_price": 0.02966, + # "change": "FALL", + # "change_price": 0.00018227, + # "change_rate": 0.0061453136, + # "signed_change_price": -0.00018227, + # "signed_change_rate": -0.0061453136, + # "trade_volume": 1.00000005, + # "acc_trade_price": 100.95825586, + # "acc_trade_price_24h": 289.58650166, + # "acc_trade_volume": 3409.85311036, + # "acc_trade_volume_24h": 9754.40510513, + # "highest_52_week_price": 0.12345678, + # "highest_52_week_date": "2018-02-01", + # "lowest_52_week_price": 0.023936, + # "lowest_52_week_date": "2017-12-08", + # "timestamp": 1542883543813 }] + # + result: dict = {} + for t in range(0, len(response)): + ticker = self.parse_ticker(response[t]) + symbol = ticker['symbol'] + result[symbol] = ticker + return self.filter_by_array_tickers(result, 'symbol', symbols) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + + https://docs.upbit.com/kr/reference/ticker현재가-정보 + https://global-docs.upbit.com/reference/tickers + + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + tickers = self.fetch_tickers([symbol], params) + return self.safe_value(tickers, symbol) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades + # + # { market: "BTC-ETH", + # "trade_date_utc": "2018-11-22", + # "trade_time_utc": "13:55:24", + # "timestamp": 1542894924397, + # "trade_price": 0.02914289, + # "trade_volume": 0.20074397, + # "prev_closing_price": 0.02966, + # "change_price": -0.00051711, + # "ask_bid": "ASK", + # "sequential_id": 15428949259430000} + # + # fetchOrder trades + # + # { + # "market": "KRW-BTC", + # "uuid": "78162304-1a4d-4524-b9e6-c9a9e14d76c3", + # "price": "101000.0", + # "volume": "0.77368323", + # "funds": "78142.00623", + # "ask_fee": "117.213009345", + # "bid_fee": "117.213009345", + # "created_at": "2018-04-05T14:09:15+09:00", + # "side": "bid", + # } + # + id = self.safe_string_2(trade, 'sequential_id', 'uuid') + orderId = None + timestamp = self.safe_integer(trade, 'timestamp') + if timestamp is None: + timestamp = self.parse8601(self.safe_string(trade, 'created_at')) + side = None + askOrBid = self.safe_string_lower_2(trade, 'ask_bid', 'side') + if askOrBid == 'ask': + side = 'sell' + elif askOrBid == 'bid': + side = 'buy' + cost = self.safe_string(trade, 'funds') + price = self.safe_string_2(trade, 'trade_price', 'price') + amount = self.safe_string_2(trade, 'trade_volume', 'volume') + marketId = self.safe_string_2(trade, 'market', 'code') + market = self.safe_market(marketId, market, '-') + fee = None + feeCost = self.safe_string(trade, askOrBid + '_fee') + if feeCost is not None: + fee = { + 'currency': market['quote'], + 'cost': feeCost, + } + return self.safe_trade({ + 'id': id, + 'info': trade, + 'order': orderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'type': None, + 'side': side, + 'takerOrMaker': None, + 'price': price, + 'amount': amount, + 'cost': cost, + 'fee': fee, + }, market) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + + https://docs.upbit.com/kr/reference/최근-체결-내역 + https://global-docs.upbit.com/reference/today-trades-history + + get the list of most recent trades for a particular symbol + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + if limit is None: + limit = 200 + request: dict = { + 'market': market['id'], + 'count': limit, + } + response = self.publicGetTradesTicks(self.extend(request, params)) + # + # [{ market: "BTC-ETH", + # "trade_date_utc": "2018-11-22", + # "trade_time_utc": "13:55:24", + # "timestamp": 1542894924397, + # "trade_price": 0.02914289, + # "trade_volume": 0.20074397, + # "prev_closing_price": 0.02966, + # "change_price": -0.00051711, + # "ask_bid": "ASK", + # "sequential_id": 15428949259430000}, + # { market: "BTC-ETH", + # "trade_date_utc": "2018-11-22", + # "trade_time_utc": "13:03:10", + # "timestamp": 1542891790123, + # "trade_price": 0.02917, + # "trade_volume": 7.392, + # "prev_closing_price": 0.02966, + # "change_price": -0.00049, + # "ask_bid": "ASK", + # "sequential_id": 15428917910540000} ] + # + return self.parse_trades(response, market, since, limit) + + def fetch_trading_fee(self, symbol: str, params={}) -> TradingFeeInterface: + """ + + https://docs.upbit.com/kr/reference/주문-가능-정보 + https://global-docs.upbit.com/reference/available-order-information + + fetch the trading fees for a market + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `fee structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + response = self.privateGetOrdersChance(self.extend(request, params)) + # + # { + # "bid_fee": "0.0005", + # "ask_fee": "0.0005", + # "maker_bid_fee": "0.0005", + # "maker_ask_fee": "0.0005", + # "market": { + # "id": "KRW-BTC", + # "name": "BTC/KRW", + # "order_types": ["limit"], + # "order_sides": ["ask", "bid"], + # "bid": {"currency": "KRW", "price_unit": null, "min_total": 5000}, + # "ask": {"currency": "BTC", "price_unit": null, "min_total": 5000}, + # "max_total": "1000000000.0", + # "state": "active" + # }, + # "bid_account": { + # "currency": "KRW", + # "balance": "0.34202415", + # "locked": "4999.99999922", + # "avg_buy_price": "0", + # "avg_buy_price_modified": True, + # "unit_currency": "KRW" + # }, + # "ask_account": { + # "currency": "BTC", + # "balance": "0.00048", + # "locked": "0.0", + # "avg_buy_price": "20870000", + # "avg_buy_price_modified": False, + # "unit_currency": "KRW" + # } + # } + # + askFee = self.safe_string(response, 'ask_fee') + bidFee = self.safe_string(response, 'bid_fee') + taker = Precise.string_max(askFee, bidFee) + makerAskFee = self.safe_string(response, 'maker_ask_fee') + makerBidFee = self.safe_string(response, 'maker_bid_fee') + maker = Precise.string_max(makerAskFee, makerBidFee) + return { + 'info': response, + 'symbol': symbol, + 'maker': self.parse_number(maker), + 'taker': self.parse_number(taker), + 'percentage': True, + 'tierBased': False, + } + + def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for markets + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `trading fee structure ` + """ + self.load_markets() + fetchMarketResponse = self.fetch_markets(params) + response: dict = {} + for i in range(0, len(fetchMarketResponse)): + element: dict = {} + element['maker'] = self.safe_number(fetchMarketResponse[i], 'maker') + element['taker'] = self.safe_number(fetchMarketResponse[i], 'taker') + element['symbol'] = self.safe_string(fetchMarketResponse[i], 'symbol') + element['percentage'] = True + element['tierBased'] = False + element['info'] = fetchMarketResponse[i] + response[self.safe_string(fetchMarketResponse[i], 'symbol')] = element + return response + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # { + # "market": "BTC-ETH", + # "candle_date_time_utc": "2018-11-22T13:47:00", + # "candle_date_time_kst": "2018-11-22T22:47:00", + # "opening_price": 0.02915963, + # "high_price": 0.02915963, + # "low_price": 0.02915448, + # "trade_price": 0.02915448, + # "timestamp": 1542894473674, + # "candle_acc_trade_price": 0.0981629437535248, + # "candle_acc_trade_volume": 3.36693173, + # "unit": 1 + # } + # + return [ + self.parse8601(self.safe_string(ohlcv, 'candle_date_time_utc')), + self.safe_number(ohlcv, 'opening_price'), + self.safe_number(ohlcv, 'high_price'), + self.safe_number(ohlcv, 'low_price'), + self.safe_number(ohlcv, 'trade_price'), + self.safe_number(ohlcv, 'candle_acc_trade_volume'), # base volume + ] + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + + https://docs.upbit.com/kr/reference/분minute-캔들-1 + https://global-docs.upbit.com/reference/minutes + + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + market = self.market(symbol) + timeframePeriod = self.parse_timeframe(timeframe) + timeframeValue = self.safe_string(self.timeframes, timeframe, timeframe) + if limit is None: + limit = 200 + request: dict = { + 'market': market['id'], + 'timeframe': timeframeValue, + 'count': limit, + } + response = None + if since is not None: + # convert `since` to `to` value + request['to'] = self.iso8601(self.sum(since, timeframePeriod * limit * 1000)) + if timeframeValue == 'minutes': + numMinutes = int(round(timeframePeriod / 60)) + request['unit'] = numMinutes + response = self.publicGetCandlesTimeframeUnit(self.extend(request, params)) + else: + response = self.publicGetCandlesTimeframe(self.extend(request, params)) + # + # [ + # { + # "market": "BTC-ETH", + # "candle_date_time_utc": "2018-11-22T13:47:00", + # "candle_date_time_kst": "2018-11-22T22:47:00", + # "opening_price": 0.02915963, + # "high_price": 0.02915963, + # "low_price": 0.02915448, + # "trade_price": 0.02915448, + # "timestamp": 1542894473674, + # "candle_acc_trade_price": 0.0981629437535248, + # "candle_acc_trade_volume": 3.36693173, + # "unit": 1 + # }, + # { + # "market": "BTC-ETH", + # "candle_date_time_utc": "2018-11-22T10:06:00", + # "candle_date_time_kst": "2018-11-22T19:06:00", + # "opening_price": 0.0294, + # "high_price": 0.02940882, + # "low_price": 0.02934283, + # "trade_price": 0.02937354, + # "timestamp": 1542881219276, + # "candle_acc_trade_price": 0.0762597110943884, + # "candle_acc_trade_volume": 2.5949617, + # "unit": 1 + # } + # ] + # + return self.parse_ohlcvs(response, market, timeframe, since, limit) + + def calc_order_price(self, symbol: str, amount: float, price: Num = None, params={}) -> str: + quoteAmount = None + createMarketBuyOrderRequiresPrice = self.safe_value(self.options, 'createMarketBuyOrderRequiresPrice') + cost = self.safe_string(params, 'cost') + if cost is not None: + quoteAmount = self.cost_to_precision(symbol, cost) + elif createMarketBuyOrderRequiresPrice: + if price is None or amount is None: + raise InvalidOrder(self.id + ' createOrder() requires the price and amount argument for market buy orders to calculate the total cost to spend(amount * price), alternatively set the createMarketBuyOrderRequiresPrice option or param to False and pass the cost to spend(quote quantity) in the amount argument') + amountString = self.number_to_string(amount) + priceString = self.number_to_string(price) + costRequest = Precise.string_mul(amountString, priceString) + quoteAmount = self.cost_to_precision(symbol, costRequest) + else: + if amount is None: + raise ArgumentsRequired(self.id + ' When createMarketBuyOrderRequiresPrice is False, "amount" is required and should be the total quote amount to spend.') + quoteAmount = self.cost_to_precision(symbol, amount) + return quoteAmount + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://docs.upbit.com/kr/reference/주문하기 + https://global-docs.upbit.com/reference/order + + :param str symbol: unified symbol of the market to create an order in + :param str type: supports 'market' and 'limit'. if params.ordType is set to best, a best-type order will be created regardless of the value of type. + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.cost]: for market buy and best buy orders, the quote quantity that can be used alternative for the amount + :param str [params.ordType]: self field can be used to place a ‘best’ type order + :param str [params.timeInForce]: 'IOC' or 'FOK' for limit or best type orders, 'PO' for limit orders. self field is required when the order type is 'best'. + :param str [params.selfTradePrevention]: 'reduce', 'cancel_maker', 'cancel_taker' {@link https://global-docs.upbit.com/docs/smp} + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + clientOrderId = self.safe_string(params, 'clientOrderId') + customType = self.safe_string_2(params, 'ordType', 'ord_type') + postOnly = self.is_post_only(type == 'market', False, params) + timeInForce = self.safe_string_lower_2(params, 'timeInForce', 'time_in_force') + selfTradePrevention = self.safe_string_2(params, 'selfTradePrevention', 'smp_type') + if postOnly and (selfTradePrevention is not None): + raise ExchangeError(self.id + ' createOrder() does not support post_only and selfTradePrevention simultaneously.') + orderSide = None + if side == 'buy': + orderSide = 'bid' + elif side == 'sell': + orderSide = 'ask' + else: + raise InvalidOrder(self.id + ' createOrder() supports only buy or sell in the side argument.') + request: dict = { + 'market': market['id'], + 'side': orderSide, + # 'smp_type': selfTradePrevention, + } + if type == 'limit': + if price is None or amount is None: + raise ArgumentsRequired(self.id + ' the limit type order in createOrder() is required price and amount.') + request['ord_type'] = 'limit' + request['price'] = self.price_to_precision(symbol, price) + request['volume'] = self.amount_to_precision(symbol, amount) + elif type == 'market': + if side == 'buy': + request['ord_type'] = 'price' + orderPrice = self.calc_order_price(symbol, amount, price, params) + request['price'] = orderPrice + else: + if amount is None: + raise ArgumentsRequired(self.id + ' the market sell type order in createOrder() is required amount.') + request['ord_type'] = 'market' + request['volume'] = self.amount_to_precision(symbol, amount) + else: + raise InvalidOrder(self.id + ' createOrder() supports only limit or market types in the type argument.') + if customType == 'best': + params = self.omit(params, ['ordType', 'ord_type']) + request['ord_type'] = 'best' + if side == 'buy': + orderPrice = self.calc_order_price(symbol, amount, price, params) + request['price'] = orderPrice + else: + if amount is None: + raise ArgumentsRequired(self.id + ' the best sell type order in createOrder() is required amount.') + request['volume'] = self.amount_to_precision(symbol, amount) + if clientOrderId is not None: + request['identifier'] = clientOrderId + if postOnly: + if request['ord_type'] != 'limit': + raise InvalidOrder(self.id + ' postOnly orders are only supported for limit orders') + request['time_in_force'] = 'post_only' + if timeInForce is not None: + if timeInForce == 'ioc' or timeInForce == 'fok': + request['time_in_force'] = timeInForce + if request['ord_type'] == 'best' and timeInForce is None: + raise ArgumentsRequired(self.id + ' createOrder() requires a timeInForce parameter for best type orders') + params = self.omit(params, ['timeInForce', 'time_in_force', 'postOnly', 'clientOrderId', 'cost', 'selfTradePrevention', 'smp_type']) + response = self.privatePostOrders(self.extend(request, params)) + # + # { + # "uuid": "cdd92199-2897-4e14-9448-f923320408ad", + # "side": "bid", + # "ord_type": "limit", + # "price": "100.0", + # "avg_price": "0.0", + # "state": "wait", + # "market": "KRW-BTC", + # "created_at": "2018-04-10T15:42:23+09:00", + # "volume": "0.01", + # "remaining_volume": "0.01", + # "reserved_fee": "0.0015", + # "remaining_fee": "0.0015", + # "paid_fee": "0.0", + # "locked": "1.0015", + # "executed_volume": "0.0", + # "trades_count": 0 + # } + # + return self.parse_order(response) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + + https://docs.upbit.com/kr/reference/주문-취소 + https://global-docs.upbit.com/reference/order-cancel + + cancels an open order + :param str id: order id + :param str symbol: not used by upbit cancelOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + request: dict = { + 'uuid': id, + } + response = self.privateDeleteOrder(self.extend(request, params)) + # + # { + # "uuid": "cdd92199-2897-4e14-9448-f923320408ad", + # "side": "bid", + # "ord_type": "limit", + # "price": "100.0", + # "state": "wait", + # "market": "KRW-BTC", + # "created_at": "2018-04-10T15:42:23+09:00", + # "volume": "0.01", + # "remaining_volume": "0.01", + # "reserved_fee": "0.0015", + # "remaining_fee": "0.0015", + # "paid_fee": "0.0", + # "locked": "1.0015", + # "executed_volume": "0.0", + # "trades_count": 0 + # } + # + return self.parse_order(response) + + def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}) -> Order: + """ + + https://docs.upbit.com/kr/reference/취소-후-재주문 + https://global-docs.upbit.com/reference/cancel-and-new + + canceled existing order and create new order. It's only generated same side and symbol canceled order. it returns the data of the canceled order, except for `new_order_uuid` and `new_identifier`. to get the details of the new order, use `fetchOrder(new_order_uuid)`. + :param str id: the uuid of the previous order you want to edit. + :param str symbol: the symbol of the new order. it must be the same symbol of the previous order. + :param str type: the type of the new order. only limit or market is accepted. if params.newOrdType is set to best, a best-type order will be created regardless of the value of type. + :param str side: the side of the new order. it must be the same side of the previous order. + :param number amount: the amount of the asset you want to buy or sell. It could be overridden by specifying the new_volume parameter in params. + :param number price: the price of the asset you want to buy or sell. It could be overridden by specifying the new_price parameter in params. + :param dict [params]: extra parameters specific to the exchange API endpoint. + :param str [params.clientOrderId]: to identify the previous order, either the id or self field is hasattr(self, required) method. + :param float [params.cost]: for market buy and best buy orders, the quote quantity that can be used alternative for the amount. + :param str [params.newTimeInForce]: 'IOC' or 'FOK' for limit or best type orders, 'PO' for limit orders. self field is required when the order type is 'best'. + :param str [params.newClientOrderId]: the order ID that the user can define. + :param str [params.newOrdType]: self field only accepts limit, price, market, or best. You can refer to the Upbit developer documentation for details on how to use self field. + :param str [params.selfTradePrevention]: 'reduce', 'cancel_maker', 'cancel_taker' {@link https://global-docs.upbit.com/docs/smp} + :returns dict: An `order structure ` + """ + self.load_markets() + request: dict = {} + prevClientOrderId = self.safe_string(params, 'clientOrderId') + customType = self.safe_string_2(params, 'newOrdType', 'new_ord_type') + clientOrderId = self.safe_string(params, 'newClientOrderId') + postOnly = self.is_post_only(type == 'market', False, params) + timeInForce = self.safe_string_lower_2(params, 'newTimeInForce', 'new_time_in_force') + selfTradePrevention = self.safe_string_2(params, 'selfTradePrevention', 'new_smp_type') + if postOnly and (selfTradePrevention is not None): + raise ExchangeError(self.id + ' editOrder() does not support post_only and selfTradePrevention simultaneously.') + params = self.omit(params, 'clientOrderId') + if id is not None: + request['prev_order_uuid'] = id + elif prevClientOrderId is not None: + request['prev_order_identifier'] = prevClientOrderId + else: + raise ArgumentsRequired(self.id + ' editOrder() is required id or clientOrderId.') + if type == 'limit': + if price is None or amount is None: + raise ArgumentsRequired(self.id + ' editOrder() is required price and amount to create limit type order.') + request['new_ord_type'] = 'limit' + request['new_price'] = self.price_to_precision(symbol, price) + request['new_volume'] = self.amount_to_precision(symbol, amount) + elif type == 'market': + if side == 'buy': + request['new_ord_type'] = 'price' + orderPrice = self.calc_order_price(symbol, amount, price, params) + request['new_price'] = orderPrice + else: + if amount is None: + raise ArgumentsRequired(self.id + ' editOrder() is required amount to create market sell type order.') + request['new_ord_type'] = 'market' + request['new_volume'] = self.amount_to_precision(symbol, amount) + else: + raise InvalidOrder(self.id + ' editOrder() supports only limit or market types in the type argument.') + if customType == 'best': + params = self.omit(params, ['newOrdType', 'new_ord_type']) + request['new_ord_type'] = 'best' + if side == 'buy': + orderPrice = self.calc_order_price(symbol, amount, price, params) + request['new_price'] = orderPrice + else: + if amount is None: + raise ArgumentsRequired(self.id + ' editOrder() is required amount to create best sell order.') + request['new_volume'] = self.amount_to_precision(symbol, amount) + if clientOrderId is not None: + request['new_identifier'] = clientOrderId + if selfTradePrevention is not None: + request['new_smp_type'] = selfTradePrevention + if postOnly: + if request['new_ord_type'] != 'limit': + raise InvalidOrder(self.id + ' postOnly orders are only supported for limit orders') + request['new_time_in_force'] = 'post_only' + if timeInForce is not None: + if timeInForce == 'ioc' or timeInForce == 'fok': + request['new_time_in_force'] = timeInForce + if request['new_ord_type'] == 'best' and timeInForce is None: + raise ArgumentsRequired(self.id + ' editOrder() requires a timeInForce parameter for best type orders') + params = self.omit(params, ['newTimeInForce', 'new_time_in_force', 'postOnly', 'newClientOrderId', 'cost', 'selfTradePrevention', 'new_smp_type']) + # print('check the each request params: ', request) + response = self.privatePostOrdersCancelAndNew(self.extend(request, params)) + # { + # uuid: '63b38774-27db-4439-ac20-1be16a24d18e', #previous order data + # side: 'bid', #previous order data + # ord_type: 'limit', #previous order data + # price: '100000000', #previous order data + # state: 'wait', #previous order data + # market: 'KRW-BTC', #previous order data + # created_at: '2025-04-01T15:30:47+09:00', #previous order data + # volume: '0.00008', #previous order data + # remaining_volume: '0.00008', #previous order data + # reserved_fee: '4', #previous order data + # remaining_fee: '4', #previous order data + # paid_fee: '0', #previous order data + # locked: '8004', #previous order data + # executed_volume: '0', #previous order data + # trades_count: '0', #previous order data + # identifier: '21', #previous order data + # new_order_uuid: 'cb1cce56-6237-4a78-bc11-4cfffc1bb4c2', # new order data + # new_order_identifier: '22' # new order data + # } + result: dict = {} + result['uuid'] = self.safe_string(response, 'new_order_uuid') + result['identifier'] = self.safe_string(response, 'new_order_identifier') + result['side'] = self.safe_string(response, 'side') + result['market'] = self.safe_string(response, 'market') + return self.parse_order(result) + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + + https://docs.upbit.com/kr/reference/입금-리스트-조회 + https://global-docs.upbit.com/reference/deposit-list-inquiry + + fetch all deposits made to an account + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + request: dict = { + # 'page': 1, + # 'order_by': 'asc', # 'desc' + } + currency = None + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + if limit is not None: + request['limit'] = limit # default is 100 + response = self.privateGetDeposits(self.extend(request, params)) + # + # [ + # { + # "type": "deposit", + # "uuid": "94332e99-3a87-4a35-ad98-28b0c969f830", + # "currency": "KRW", + # "txid": "9e37c537-6849-4c8b-a134-57313f5dfc5a", + # "state": "ACCEPTED", + # "created_at": "2017-12-08T15:38:02+09:00", + # "done_at": "2017-12-08T15:38:02+09:00", + # "amount": "100000.0", + # "fee": "0.0" + # }, + # ..., + # ] + # + return self.parse_transactions(response, currency, since, limit) + + def fetch_deposit(self, id: str, code: Str = None, params={}): + """ + fetch information on a deposit + + https://docs.upbit.com/kr/reference/개별-입금-조회 + https://global-docs.upbit.com/reference/individual-deposit-inquiry + + :param str id: the unique id for the deposit + :param str [code]: unified currency code of the currency deposited + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.txid]: withdrawal transaction id, the id argument is reserved for uuid + :returns dict: a `transaction structure ` + """ + self.load_markets() + request: dict = { + 'uuid': id, + } + currency = None + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + response = self.privateGetDeposit(self.extend(request, params)) + # + # { + # "type": "deposit", + # "uuid": "7f54527e-2eee-4268-860e-fd8b9d7fe3c7", + # "currency": "ADA", + # "net_type": "ADA", + # "txid": "99795bbfeca91eaa071068bb659b33eeb65d8aaff2551fdf7c78f345d188952b", + # "state": "ACCEPTED", + # "created_at": "2023-12-12T04:58:41Z", + # "done_at": "2023-12-12T05:31:50Z", + # "amount": "35.72344", + # "fee": "0.0", + # "transaction_type": "default" + # } + # + return self.parse_transaction(response, currency) + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + + https://docs.upbit.com/kr/reference/전체-출금-조회 + https://global-docs.upbit.com/reference/withdrawal-list-inquiry + + fetch all withdrawals made from an account + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + request: dict = { + # 'state': 'submitting', # 'submitted', 'almost_accepted', 'rejected', 'accepted', 'processing', 'done', 'canceled' + } + currency = None + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + if limit is not None: + request['limit'] = limit # default is 100 + response = self.privateGetWithdraws(self.extend(request, params)) + # + # [ + # { + # "type": "withdraw", + # "uuid": "9f432943-54e0-40b7-825f-b6fec8b42b79", + # "currency": "BTC", + # "txid": null, + # "state": "processing", + # "created_at": "2018-04-13T11:24:01+09:00", + # "done_at": null, + # "amount": "0.01", + # "fee": "0.0", + # "krw_amount": "80420.0" + # }, + # ..., + # ] + # + return self.parse_transactions(response, currency, since, limit) + + def fetch_withdrawal(self, id: str, code: Str = None, params={}): + """ + fetch data on a currency withdrawal via the withdrawal id + + https://docs.upbit.com/kr/reference/개별-출금-조회 + https://global-docs.upbit.com/reference/individual-withdrawal-inquiry + + :param str id: the unique id for the withdrawal + :param str [code]: unified currency code of the currency withdrawn + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.txid]: withdrawal transaction id, the id argument is reserved for uuid + :returns dict: a `transaction structure ` + """ + self.load_markets() + request: dict = { + 'uuid': id, + } + currency = None + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + response = self.privateGetWithdraw(self.extend(request, params)) + # + # { + # "type": "withdraw", + # "uuid": "95ef274b-23a6-4de4-95b0-5cbef4ca658f", + # "currency": "ADA", + # "net_type": "ADA", + # "txid": "b1528f149297a71671b86636f731f8fdb0ff53da0f1d8c19093d59df96f34583", + # "state": "DONE", + # "created_at": "2023-12-14T02:46:52Z", + # "done_at": "2023-12-14T03:10:11Z", + # "amount": "35.22344", + # "fee": "0.5", + # "transaction_type": "default" + # } + # + return self.parse_transaction(response, currency) + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'submitting': 'pending', # 처리 중 + 'submitted': 'pending', # 처리 완료 + 'almost_accepted': 'pending', # 출금대기중 + 'rejected': 'failed', # 거부 + 'accepted': 'ok', # 승인됨 + 'processing': 'pending', # 처리 중 + 'done': 'ok', # 완료 + 'canceled': 'canceled', # 취소됨 + } + return self.safe_string(statuses, status, status) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fetchDeposits, fetchDeposit + # + # { + # "type": "deposit", + # "uuid": "94332e99-3a87-4a35-ad98-28b0c969f830", + # "currency": "KRW", + # "txid": "9e37c537-6849-4c8b-a134-57313f5dfc5a", + # "state": "ACCEPTED", + # "created_at": "2017-12-08T15:38:02+09:00", + # "done_at": "2017-12-08T15:38:02+09:00", + # "amount": "100000.0", + # "fee": "0.0" + # } + # + # fetchWithdrawals, fetchWithdrawal + # + # { + # "type": "withdraw", + # "uuid": "9f432943-54e0-40b7-825f-b6fec8b42b79", + # "currency": "BTC", + # "txid": "cd81e9b45df8da29f936836e58c907a106057e454a45767a7b06fcb19b966bba", + # "state": "processing", + # "created_at": "2018-04-13T11:24:01+09:00", + # "done_at": null, + # "amount": "0.01", + # "fee": "0.0", + # "krw_amount": "80420.0" + # } + # + address = None # not present in the data structure received from the exchange + tag = None # not present in the data structure received from the exchange + updatedRaw = self.safe_string(transaction, 'done_at') + timestamp = self.parse8601(self.safe_string(transaction, 'created_at', updatedRaw)) + type = self.safe_string(transaction, 'type') + if type == 'withdraw': + type = 'withdrawal' + currencyId = self.safe_string(transaction, 'currency') + code = self.safe_currency_code(currencyId, currency) + return { + 'info': transaction, + 'id': self.safe_string(transaction, 'uuid'), + 'currency': code, + 'amount': self.safe_number(transaction, 'amount'), + 'network': None, + 'address': address, + 'addressTo': None, + 'addressFrom': None, + 'tag': tag, + 'tagTo': None, + 'tagFrom': None, + 'status': self.parse_transaction_status(self.safe_string_lower(transaction, 'state')), + 'type': type, + 'updated': self.parse8601(updatedRaw), + 'txid': self.safe_string(transaction, 'txid'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'internal': None, + 'comment': None, + 'fee': { + 'currency': code, + 'cost': self.safe_number(transaction, 'fee'), + }, + } + + def parse_order_status(self, status: Str): + statuses: dict = { + 'wait': 'open', + 'done': 'closed', + 'cancel': 'canceled', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # { + # "market": "KRW-USDT", + # "uuid": "3b67e543-8ad3-48d0-8451-0dad315cae73", + # "side": "ask", + # "ord_type": "market", + # "state": "done", + # "created_at": "2025-08-09T16:44:00+09:00", + # "volume": "5.377594", + # "remaining_volume": "0", + # "executed_volume": "5.377594", + # "reserved_fee": "0", + # "remaining_fee": "0", + # "paid_fee": "3.697095875", + # "locked": "0", + # "prevented_volume": "0", + # "prevented_locked": "0", + # "trades_count": 1, + # "trades": [ + # { + # "market": "KRW-USDT", + # "uuid": "795dff29-bba6-49b2-baab-63473ab7931c", + # "price": "1375", + # "volume": "5.377594", + # "funds": "7394.19175", + # "trend": "down", + # "created_at": "2025-08-09T16:44:00.597751+09:00", + # "side": "ask" + # } + # ] + # } + # + # fetchOpenOrders, fetchClosedOrders, fetchCanceledOrders + # + # { + # "uuid": "637fd66-d019-4d77-bee6-8e0cff28edd9", + # "side": "ask", + # "ord_type": "limit", + # "price": "1.5", + # "state": "wait", + # "market": "SGD-XRP", + # "created_at": "2024-06-05T09:37:10Z", + # "volume": "10", + # "remaining_volume": "10", + # "reserved_fee": "0", + # "remaining_fee": "0", + # "paid_fee": "0", + # "locked": "10", + # "executed_volume": "0", + # "executed_funds": "0", + # "trades_count": 0, + # "time_in_force": "ioc" + # } + # + # { + # uuid: '63b38774-27db-4439-ac20-1be16a24d18e', + # side: 'bid', + # ord_type: 'limit', + # price: '100000000', + # state: 'wait', + # market: 'KRW-BTC', + # created_at: '2025-04-01T15:30:47+09:00', + # volume: '0.00008', + # remaining_volume: '0.00008', + # reserved_fee: '4', + # remaining_fee: '4', + # paid_fee: '0', + # locked: '8004', + # executed_volume: '0', + # trades_count: '0', + # identifier: '21', + # new_order_uuid: 'cb1cce56-6237-4a78-bc11-4cfffc1bb4c2', + # new_order_identifier: '22' + # } + id = self.safe_string(order, 'uuid') + side = self.safe_string(order, 'side') + if side == 'bid': + side = 'buy' + else: + side = 'sell' + identifier = self.safe_string(order, 'identifier') + type = self.safe_string(order, 'ord_type') + timestamp = self.parse8601(self.safe_string(order, 'created_at')) + status = self.parse_order_status(self.safe_string(order, 'state')) + lastTradeTimestamp = None + price = self.safe_string(order, 'price') + amount = self.safe_string(order, 'volume') + remaining = self.safe_string(order, 'remaining_volume') + filled = self.safe_string(order, 'executed_volume') + cost = None + if type == 'price': + type = 'market' + cost = price + price = None + average = None + fee = None + feeCost = self.safe_string(order, 'paid_fee') + marketId = self.safe_string(order, 'market') + market = self.safe_market(marketId, market) + trades = self.safe_value(order, 'trades', []) + trades = self.parse_trades(trades, market, None, None, { + 'order': id, + 'type': type, + }) + numTrades = len(trades) + if numTrades > 0: + # the timestamp in fetchOrder trades is missing + lastTradeTimestamp = trades[numTrades - 1]['timestamp'] + getFeesFromTrades = False + if feeCost is None: + getFeesFromTrades = True + feeCost = '0' + cost = '0' + for i in range(0, numTrades): + trade = trades[i] + cost = Precise.string_add(cost, self.safe_string(trade, 'cost')) + if getFeesFromTrades: + tradeFee = self.safe_value(trades[i], 'fee', {}) + tradeFeeCost = self.safe_string(tradeFee, 'cost') + if tradeFeeCost is not None: + feeCost = Precise.string_add(feeCost, tradeFeeCost) + average = Precise.string_div(cost, filled) + if feeCost is not None: + fee = { + 'currency': market['quote'], + 'cost': feeCost, + } + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': identifier, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': lastTradeTimestamp, + 'symbol': market['symbol'], + 'type': type, + 'timeInForce': self.safe_string_upper(order, 'time_in_force'), + 'postOnly': None, + 'side': side, + 'price': price, + 'triggerPrice': None, + 'cost': self.parse_number(cost), + 'average': self.parse_number(average), + 'amount': amount, + 'filled': filled, + 'remaining': remaining, + 'status': status, + 'fee': fee, + 'trades': trades, + }) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://docs.upbit.com/kr/reference/대기-주문-조회 + https://global-docs.upbit.com/reference/open-order + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.state]: default is 'wait', set to 'watch' for stop limit orders + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['market'] = market['id'] + if limit is not None: + request['limit'] = limit + response = self.privateGetOrdersOpen(self.extend(request, params)) + # + # [ + # { + # "uuid": "637fd66-d019-4d77-bee6-8e0cff28edd9", + # "side": "ask", + # "ord_type": "limit", + # "price": "1.5", + # "state": "wait", + # "market": "SGD-XRP", + # "created_at": "2024-06-05T09:37:10Z", + # "volume": "10", + # "remaining_volume": "10", + # "reserved_fee": "0", + # "remaining_fee": "0", + # "paid_fee": "0", + # "locked": "10", + # "executed_volume": "0", + # "executed_funds": "0", + # "trades_count": 0 + # } + # ] + # + return self.parse_orders(response, market, since, limit) + + def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://docs.upbit.com/kr/reference/종료-주문-조회 + https://global-docs.upbit.com/reference/closed-order + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest order + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + request: dict = { + 'state': 'done', + } + market = None + if symbol is not None: + market = self.market(symbol) + request['market'] = market['id'] + if since is not None: + request['start_time'] = since + if limit is not None: + request['limit'] = limit + request, params = self.handle_until_option('end_time', request, params) + response = self.privateGetOrdersClosed(self.extend(request, params)) + # + # [ + # { + # "uuid": "637fd66-d019-4d77-bee6-8e0cff28edd9", + # "side": "ask", + # "ord_type": "limit", + # "price": "1.5", + # "state": "done", + # "market": "SGD-XRP", + # "created_at": "2024-06-05T09:37:10Z", + # "volume": "10", + # "remaining_volume": "10", + # "reserved_fee": "0", + # "remaining_fee": "0", + # "paid_fee": "0", + # "locked": "10", + # "executed_volume": "0", + # "executed_funds": "0", + # "trades_count": 0, + # "time_in_force": "ioc" + # } + # ] + # + return self.parse_orders(response, market, since, limit) + + def fetch_canceled_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches information on multiple canceled orders made by the user + + https://docs.upbit.com/kr/reference/종료-주문-조회 + https://global-docs.upbit.com/reference/closed-order + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: timestamp in ms of the earliest order, default is None + :param int [limit]: max number of orders to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest order + :returns dict: a list of `order structures ` + """ + self.load_markets() + request: dict = { + 'state': 'cancel', + } + market = None + if symbol is not None: + market = self.market(symbol) + request['market'] = market['id'] + if since is not None: + request['start_time'] = since + if limit is not None: + request['limit'] = limit + request, params = self.handle_until_option('end_time', request, params) + response = self.privateGetOrdersClosed(self.extend(request, params)) + # + # [ + # { + # "uuid": "637fd66-d019-4d77-bee6-8e0cff28edd9", + # "side": "ask", + # "ord_type": "limit", + # "price": "1.5", + # "state": "cancel", + # "market": "SGD-XRP", + # "created_at": "2024-06-05T09:37:10Z", + # "volume": "10", + # "remaining_volume": "10", + # "reserved_fee": "0", + # "remaining_fee": "0", + # "paid_fee": "0", + # "locked": "10", + # "executed_volume": "0", + # "executed_funds": "0", + # "trades_count": 0, + # "time_in_force": "ioc" + # } + # ] + # + return self.parse_orders(response, market, since, limit) + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + + https://docs.upbit.com/kr/reference/개별-주문-조회 + https://global-docs.upbit.com/reference/individual-order-inquiry + + fetches information on an order made by the user + :param str id: order id + :param str symbol: not used by upbit fetchOrder + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + request: dict = { + 'uuid': id, + } + response = self.privateGetOrder(self.extend(request, params)) + # + # { + # "uuid": "a08f09b1-1718-42e2-9358-f0e5e083d3ee", + # "side": "bid", + # "ord_type": "limit", + # "price": "17417000.0", + # "state": "done", + # "market": "KRW-BTC", + # "created_at": "2018-04-05T14:09:14+09:00", + # "volume": "1.0", + # "remaining_volume": "0.0", + # "reserved_fee": "26125.5", + # "remaining_fee": "25974.0", + # "paid_fee": "151.5", + # "locked": "17341974.0", + # "executed_volume": "1.0", + # "trades_count": 2, + # "trades": [ + # { + # "market": "KRW-BTC", + # "uuid": "78162304-1a4d-4524-b9e6-c9a9e14d76c3", + # "price": "101000.0", + # "volume": "0.77368323", + # "funds": "78142.00623", + # "ask_fee": "117.213009345", + # "bid_fee": "117.213009345", + # "created_at": "2018-04-05T14:09:15+09:00", + # "side": "bid" + # }, + # { + # "market": "KRW-BTC", + # "uuid": "f73da467-c42f-407d-92fa-e10d86450a20", + # "price": "101000.0", + # "volume": "0.22631677", + # "funds": "22857.99377", + # "ask_fee": "34.286990655", + # "bid_fee": "34.286990655", + # "created_at": "2018-04-05T14:09:15+09:00", + # "side": "bid" + # } + # ] + # } + # + return self.parse_order(response) + + def fetch_deposit_addresses(self, codes: Strings = None, params={}) -> List[DepositAddress]: + """ + + https://docs.upbit.com/kr/reference/전체-입금-주소-조회 + https://global-docs.upbit.com/reference/general-deposit-address-inquiry + + fetch deposit addresses for multiple currencies and chain types + :param str[]|None codes: list of unified currency codes, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `address structures ` + """ + self.load_markets() + response = self.privateGetDepositsCoinAddresses(params) + # + # [ + # { + # "currency": "BTC", + # "deposit_address": "3EusRwybuZUhVDeHL7gh3HSLmbhLcy7NqD", + # "secondary_address": null + # }, + # { + # "currency": "ETH", + # "deposit_address": "0x0d73e0a482b8cf568976d2e8688f4a899d29301c", + # "secondary_address": null + # }, + # { + # "currency": "XRP", + # "deposit_address": "rN9qNpgnBaZwqCg8CvUZRPqCcPPY7wfWep", + # "secondary_address": "3057887915" + # } + # ] + # + return self.parse_deposit_addresses(response, codes) + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + # + # { + # currency: 'XRP', + # net_type: 'XRP', + # deposit_address: 'raQwCVAJVqjrVm1Nj5SFRcX8i22BhdC9WA', + # secondary_address: '167029435' + # } + # + address = self.safe_string(depositAddress, 'deposit_address') + tag = self.safe_string(depositAddress, 'secondary_address') + currencyId = self.safe_string(depositAddress, 'currency') + code = self.safe_currency_code(currencyId) + networkId = self.safe_string(depositAddress, 'net_type') + self.check_address(address) + return { + 'info': depositAddress, + 'currency': code, + 'network': self.network_id_to_code(networkId), + 'address': address, + 'tag': tag, + } + + def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + + https://docs.upbit.com/kr/reference/개별-입금-주소-조회 + https://global-docs.upbit.com/reference/individual-deposit-address-inquiry + + fetch the deposit address for a currency associated with self account + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str params['network']: deposit chain, can view all chains via self.publicGetWalletAssets, default is eth, unless the currency has a default chain within self.options['networks'] + :returns dict: an `address structure ` + """ + self.load_markets() + currency = self.currency(code) + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + if networkCode is None: + raise ArgumentsRequired(self.id + ' fetchDepositAddress requires params["network"]') + response = self.privateGetDepositsCoinAddress(self.extend({ + 'currency': currency['id'], + 'net_type': self.network_code_to_id(networkCode, currency['code']), + }, params)) + # + # { + # currency: 'XRP', + # net_type: 'XRP', + # deposit_address: 'raQwCVAJVqjrVm1Nj5SFRcX8i22BhdC9WA', + # secondary_address: '167029435' + # } + # + return self.parse_deposit_address(response) + + def create_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + + https://docs.upbit.com/kr/reference/입금-주소-생성-요청 + https://global-docs.upbit.com/reference/deposit-address-generation + + create a currency deposit address + :param str code: unified currency code of the currency for the deposit address + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + } + # https://github.com/ccxt/ccxt/issues/6452 + response = self.privatePostDepositsGenerateCoinAddress(self.extend(request, params)) + # + # https://docs.upbit.com/v1.0/reference#%EC%9E%85%EA%B8%88-%EC%A3%BC%EC%86%8C-%EC%83%9D%EC%84%B1-%EC%9A%94%EC%B2%AD + # can be any of the two responses: + # + # { + # "success" : True, + # "message" : "Creating BTC deposit address." + # } + # + # { + # "currency": "BTC", + # "deposit_address": "3EusRwybuZUhVDeHL7gh3HSLmbhLcy7NqD", + # "secondary_address": null + # } + # + message = self.safe_string(response, 'message') + if message is not None: + raise AddressPending(self.id + ' is generating ' + code + ' deposit address, call fetchDepositAddress or createDepositAddress one more time later to retrieve the generated address') + return self.parse_deposit_address(response) + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + + https://docs.upbit.com/kr/reference/디지털자산-출금하기 + https://global-docs.upbit.com/reference/withdrawal-digital-assets + + make a withdrawal + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.load_markets() + currency = self.currency(code) + request: dict = { + 'amount': amount, + } + response = None + if code != 'KRW': + self.check_address(address) + # 2023-05-23 Change to required parameters for digital assets + network = self.safe_string_upper_2(params, 'network', 'net_type') + if network is None: + raise ArgumentsRequired(self.id + ' withdraw() requires a network argument') + params = self.omit(params, ['network']) + request['net_type'] = network + request['currency'] = currency['id'] + request['address'] = address + if tag is not None: + request['secondary_address'] = tag + params = self.omit(params, 'network') + response = self.privatePostWithdrawsCoin(self.extend(request, params)) + else: + response = self.privatePostWithdrawsKrw(self.extend(request, params)) + # + # { + # "type": "withdraw", + # "uuid": "9f432943-54e0-40b7-825f-b6fec8b42b79", + # "currency": "BTC", + # "txid": "ebe6937b-130e-4066-8ac6-4b0e67f28adc", + # "state": "processing", + # "created_at": "2018-04-13T11:24:01+09:00", + # "done_at": null, + # "amount": "0.01", + # "fee": "0.0", + # "krw_amount": "80420.0" + # } + # + return self.parse_transaction(response) + + def nonce(self): + return self.milliseconds() + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = self.implode_params(self.urls['api'][api], { + 'hostname': self.hostname, + }) + url += '/' + self.version + '/' + self.implode_params(path, params) + query = self.omit(params, self.extract_params(path)) + if method != 'POST': + if query: + url += '?' + self.urlencode(query) + if api == 'private': + self.check_required_credentials() + headers = {} + nonce = self.uuid() + request: dict = { + 'access_key': self.apiKey, + 'nonce': nonce, + } + hasQuery = query + auth = None + if (method != 'GET') and (method != 'DELETE'): + body = self.json(params) + headers['Content-Type'] = 'application/json' + if hasQuery: + auth = self.rawencode(query) + if auth is not None: + hash = self.hash(self.encode(auth), 'sha512') + request['query_hash'] = hash + request['query_hash_alg'] = 'SHA512' + token = self.jwt(request, self.encode(self.secret), 'sha256') + headers['Authorization'] = 'Bearer ' + token + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None # fallback to default error handler + # + # {'error': {'message': "Missing request parameter error. Check the required parameters!", 'name': 400}}, + # {'error': {'message': "side is missing, side does not have a valid value", 'name': "validation_error"}}, + # {'error': {'message': "개인정보 제 3자 제공 동의가 필요합니다.", 'name': "thirdparty_agreement_required"}}, + # {'error': {'message': "권한이 부족합니다.", 'name': "out_of_scope"}}, + # {'error': {'message': "주문을 찾지 못했습니다.", 'name': "order_not_found"}}, + # {'error': {'message': "주문가능한 금액(ETH)이 부족합니다.", 'name': "insufficient_funds_ask"}}, + # {'error': {'message': "주문가능한 금액(BTC)이 부족합니다.", 'name': "insufficient_funds_bid"}}, + # {'error': {'message': "잘못된 엑세스 키입니다.", 'name': "invalid_access_key"}}, + # {'error': {'message': "Jwt 토큰 검증에 실패했습니다.", 'name': "jwt_verification"}} + # + error = self.safe_value(response, 'error') + if error is not None: + message = self.safe_string(error, 'message') + name = self.safe_string(error, 'name') + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], name, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], name, feedback) + raise ExchangeError(feedback) # unknown message + return None diff --git a/ccxt/wavesexchange.py b/ccxt/wavesexchange.py new file mode 100644 index 0000000..7917188 --- /dev/null +++ b/ccxt/wavesexchange.py @@ -0,0 +1,2641 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.wavesexchange import ImplicitAPI +import json +from ccxt.base.types import Any, Balances, Currency, DepositAddress, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import AccountSuspended +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import DuplicateOrderId +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class wavesexchange(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(wavesexchange, self).describe(), { + 'id': 'wavesexchange', + 'name': 'Waves.Exchange', + 'countries': ['CH'], # Switzerland + 'certified': False, + 'pro': False, + 'dex': True, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelOrder': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createMarketOrder': True, + 'createOrder': True, + 'createReduceOnlyOrder': False, + 'createStopLimitOrder': False, + 'createStopMarketOrder': False, + 'createStopOrder': False, + 'fetchAllGreeks': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchClosedOrders': True, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': False, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': None, + 'fetchDepositAddressesByNetwork': None, + 'fetchDepositWithdrawFee': 'emulated', + 'fetchDepositWithdrawFees': True, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrice': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTrades': True, + 'fetchTransfer': False, + 'fetchTransfers': False, + 'fetchUnderlyingAssets': False, + 'fetchVolatilityHistory': False, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'sandbox': True, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'signIn': True, + 'transfer': False, + 'withdraw': True, + 'ws': False, + }, + 'timeframes': { + '1m': '1m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1h', + '2h': '2h', + '3h': '3h', + '4h': '4h', + '6h': '6h', + '12h': '12h', + '1d': '1d', + '1w': '1w', + '1M': '1M', + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/84547058-5fb27d80-ad0b-11ea-8711-78ac8b3c7f31.jpg', + 'test': { + 'matcher': 'https://matcher-testnet.wx.network', + 'node': 'https://nodes-testnet.wavesnodes.com', + 'public': 'https://api-testnet.wavesplatform.com/v0', + 'private': 'https://api-testnet.wx.network/v1', + 'forward': 'https://testnet.wx.network/api/v1/forward/matcher', + 'market': 'https://testnet.wx.network/api/v1/forward/marketdata/api/v1', + }, + 'api': { + 'matcher': 'https://matcher.wx.network', + 'node': 'https://nodes.wx.network', + 'public': 'https://api.wavesplatform.com/v0', + 'private': 'https://api.wx.network/v1', + 'forward': 'https://wx.network/api/v1/forward/matcher', + 'market': 'https://wx.network/api/v1/forward/marketdata/api/v1', + }, + 'doc': [ + 'https://docs.wx.network', + 'https://docs.waves.tech', + 'https://api.wavesplatform.com/v0/docs/', + 'https://nodes.wavesnodes.com/api-docs/index.html', + 'https://matcher.waves.exchange/api-docs/index.html', + ], + 'www': 'https://wx.network', + }, + 'api': { + 'matcher': { + 'get': [ + 'matcher', + 'matcher/settings', + 'matcher/settings/rates', + 'matcher/balance/reserved/{publicKey}', + 'matcher/debug/allSnashotOffsets', + 'matcher/debug/currentOffset', + 'matcher/debug/lastOffset', + 'matcher/debug/oldestSnapshotOffset', + 'matcher/debug/config', + 'matcher/debug/address/{address}', + 'matcher/debug/status', + 'matcher/debug/address/{address}/check', + 'matcher/orderbook', + 'matcher/orderbook/{baseId}/{quoteId}', + 'matcher/orderbook/{baseId}/{quoteId}/publicKey/{publicKey}', + 'matcher/orderbook/{baseId}/{quoteId}/{orderId}', + 'matcher/orderbook/{baseId}/{quoteId}/info', + 'matcher/orderbook/{baseId}/{quoteId}/status', + 'matcher/orderbook/{baseId}/{quoteId}/tradableBalance/{address}', + 'matcher/orderbook/{publicKey}', + 'matcher/orderbook/{publicKey}/{orderId}', + 'matcher/orders/{address}', + 'matcher/orders/{address}/{orderId}', + 'matcher/transactions/{orderId}', + 'api/v1/orderbook/{baseId}/{quoteId}', + ], + 'post': [ + 'matcher/orderbook', + 'matcher/orderbook/market', + 'matcher/orderbook/cancel', + 'matcher/orderbook/{baseId}/{quoteId}/cancel', + 'matcher/orderbook/{baseId}/{quoteId}/calculateFee', + 'matcher/orderbook/{baseId}/{quoteId}/delete', + 'matcher/orderbook/{baseId}/{quoteId}/cancelAll', + 'matcher/debug/saveSnapshots', + 'matcher/orders/{address}/cancel', + 'matcher/orders/cancel/{orderId}', + 'matcher/orders/serialize', + ], + 'delete': [ + 'matcher/orderbook/{baseId}/{quoteId}', + 'matcher/settings/rates/{assetId}', + ], + 'put': [ + 'matcher/settings/rates/{assetId}', + ], + }, + 'node': { + 'get': [ + 'addresses', + 'addresses/balance/{address}', + 'addresses/balance/{address}/{confirmations}', + 'addresses/balance/details/{address}', + 'addresses/data/{address}', + 'addresses/data/{address}/{key}', + 'addresses/effectiveBalance/{address}', + 'addresses/effectiveBalance/{address}/{confirmations}', + 'addresses/publicKey/{publicKey}', + 'addresses/scriptInfo/{address}', + 'addresses/scriptInfo/{address}/meta', + 'addresses/seed/{address}', + 'addresses/seq/{from}/{to}', + 'addresses/validate/{address}', + 'alias/by-address/{address}', + 'alias/by-alias/{alias}', + 'assets/{assetId}/distribution/{height}/{limit}', + 'assets/balance/{address}', + 'assets/balance/{address}/{assetId}', + 'assets/details/{assetId}', + 'assets/nft/{address}/limit/{limit}', + 'blockchain/rewards', + 'blockchain/rewards/height', + 'blocks/address/{address}/{from}/{to}/', + 'blocks/at/{height}', + 'blocks/delay/{signature}/{blockNum}', + 'blocks/first', + 'blocks/headers/last', + 'blocks/headers/seq/{from}/{to}', + 'blocks/height', + 'blocks/height/{signature}', + 'blocks/last', + 'blocks/seq/{from}/{to}', + 'blocks/signature/{signature}', + 'consensus/algo', + 'consensus/basetarget', + 'consensus/basetarget/{blockId}', + 'consensus/{generatingbalance}/address', + 'consensus/generationsignature', + 'consensus/generationsignature/{blockId}', + 'debug/balances/history/{address}', + 'debug/blocks/{howMany}', + 'debug/configInfo', + 'debug/historyInfo', + 'debug/info', + 'debug/minerInfo', + 'debug/portfolios/{address}', + 'debug/state', + 'debug/stateChanges/address/{address}', + 'debug/stateChanges/info/{id}', + 'debug/stateWaves/{height}', + 'leasing/active/{address}', + 'node/state', + 'node/version', + 'peers/all', + 'peers/blacklisted', + 'peers/connected', + 'peers/suspended', + 'transactions/address/{address}/limit/{limit}', + 'transactions/info/{id}', + 'transactions/status', + 'transactions/unconfirmed', + 'transactions/unconfirmed/info/{id}', + 'transactions/unconfirmed/size', + 'utils/seed', + 'utils/seed/{length}', + 'utils/time', + 'wallet/seed', + ], + 'post': [ + 'addresses', + 'addresses/data/{address}', + 'addresses/sign/{address}', + 'addresses/signText/{address}', + 'addresses/verify/{address}', + 'addresses/verifyText/{address}', + 'debug/blacklist', + 'debug/print', + 'debug/rollback', + 'debug/validate', + 'node/stop', + 'peers/clearblacklist', + 'peers/connect', + 'transactions/broadcast', + 'transactions/calculateFee', + 'tranasctions/sign', + 'transactions/sign/{signerAddress}', + 'tranasctions/status', + 'utils/hash/fast', + 'utils/hash/secure', + 'utils/script/compileCode', + 'utils/script/compileWithImports', + 'utils/script/decompile', + 'utils/script/estimate', + 'utils/sign/{privateKey}', + 'utils/transactionsSerialize', + ], + 'delete': [ + 'addresses/{address}', + 'debug/rollback-to/{signature}', + ], + }, + 'public': { + 'get': [ + 'assets', + 'pairs', + 'candles/{baseId}/{quoteId}', + 'transactions/exchange', + ], + }, + 'private': { + 'get': [ + 'deposit/addresses/{currency}', + 'deposit/addresses/{currency}/{platform}', + 'platforms', + 'deposit/currencies', + 'withdraw/currencies', + 'withdraw/addresses/{currency}/{address}', + ], + 'post': [ + 'oauth2/token', + ], + }, + 'forward': { + 'get': [ + 'matcher/orders/{address}', # can't get the orders endpoint to work with the matcher api + 'matcher/orders/{address}/{orderId}', + ], + 'post': [ + 'matcher/orders/{wavesAddress}/cancel', + ], + }, + 'market': { + 'get': [ + 'tickers', + ], + }, + }, + 'currencies': { + 'WX': self.safe_currency_structure({'id': 'EMAMLxDnv3xiz8RXg8Btj33jcEw3wLczL3JKYYmuubpc', 'numericId': None, 'code': 'WX', 'precision': self.parse_number('1e-8')}), + }, + 'precisionMode': TICK_SIZE, + 'options': { + 'allowedCandles': 1440, + 'accessToken': None, + 'createMarketBuyOrderRequiresPrice': True, + 'matcherPublicKey': None, + 'quotes': None, + 'createOrderDefaultExpiry': 2419200000, # 60 * 60 * 24 * 28 * 1000 + 'wavesAddress': None, + 'withdrawFeeUSDN': 7420, + 'withdrawFeeWAVES': 100000, + 'wavesPrecision': 1e-8, + 'messagePrefix': 'W', # W for production, T for testnet + 'networks': { + 'ERC20': 'ETH', + 'BEP20': 'BSC', + }, + }, + 'features': { + 'spot': { + 'sandbox': True, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, # todo + 'triggerDirection': False, + 'triggerPriceType': None, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': False, + 'FOK': False, + 'PO': False, + 'GTD': True, # todo + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': False, # todo + 'marketBuyRequiresPrice': True, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, # todo + 'daysBack': 100000, # todo + 'untilDays': 100000, # todo + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 100, # todo + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': 100, # todo + 'daysBack': None, + 'untilDays': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, # todo + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 100000, # todo + 'daysBackCanceled': 1, # todo + 'untilDays': 100000, # todo + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': None, # todo + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'commonCurrencies': { + 'EGG': 'Waves Ducks', + }, + 'requiresEddsa': True, + 'exceptions': { + '3147270': InsufficientFunds, # https://github.com/wavesplatform/matcher/wiki/List-of-all-errors + '112': InsufficientFunds, + '4': ExchangeError, + '13': ExchangeNotAvailable, + '14': ExchangeNotAvailable, + '3145733': AccountSuspended, + '3148040': DuplicateOrderId, + '3148801': AuthenticationError, + '9440512': AuthenticationError, + '9440771': BadSymbol, + '9441026': InvalidOrder, + '9441282': InvalidOrder, + '9441286': InvalidOrder, + '9441295': InvalidOrder, + '9441540': InvalidOrder, + '9441542': InvalidOrder, + '106954752': AuthenticationError, + '106954769': AuthenticationError, + '106957828': AuthenticationError, + '106960131': AuthenticationError, + '106981137': AuthenticationError, + '9437184': BadRequest, # {"error":9437184,"message":"The order is invalid: SpendAmount should be > 0","template":"The order is invalid: {{details}}","params":{"details":"SpendAmount should be > 0"},"status":"OrderRejected","success":false} + '9437193': OrderNotFound, + '1048577': BadRequest, + '1051904': AuthenticationError, + }, + }) + + def set_sandbox_mode(self, enabled): + self.options['messagePrefix'] = 'T' if enabled else 'W' + self.options['sandboxMode'] = enabled + super(wavesexchange, self).set_sandbox_mode(enabled) + + def get_fees_for_asset(self, symbol: str, side, amount, price, params={}): + self.load_markets() + market = self.market(symbol) + amount = self.to_real_symbol_amount(symbol, amount) + price = self.to_real_symbol_price(symbol, price) + request = self.extend({ + 'baseId': market['baseId'], + 'quoteId': market['quoteId'], + 'orderType': side, + 'amount': amount, + 'price': price, + }, params) + return self.matcherPostMatcherOrderbookBaseIdQuoteIdCalculateFee(request) + + def custom_calculate_fee(self, symbol: str, type, side, amount, price, takerOrMaker='taker', params={}): + response = self.get_fees_for_asset(symbol, side, amount, price) + # { + # "base":{ + # "feeAssetId":"WAVES", + # "matcherFee":"1000000" + # }, + # "discount":{ + # "feeAssetId":"EMAMLxDnv3xiz8RXg8Btj33jcEw3wLczL3JKYYmuubpc", + # "matcherFee":"4077612" + # } + # } + isDiscountFee = self.safe_bool(params, 'isDiscountFee', False) + mode = None + if isDiscountFee: + mode = self.safe_value(response, 'discount') + else: + mode = self.safe_value(response, 'base') + matcherFee = self.safe_string(mode, 'matcherFee') + feeAssetId = self.safe_string(mode, 'feeAssetId') + feeAsset = self.safe_currency_code(feeAssetId) + adjustedMatcherFee = self.from_real_currency_amount(feeAsset, matcherFee) + amountAsString = self.number_to_string(amount) + priceAsString = self.number_to_string(price) + feeCost = self.fee_to_precision(symbol, self.parse_number(adjustedMatcherFee)) + feeRate = Precise.string_div(adjustedMatcherFee, Precise.string_mul(amountAsString, priceAsString)) + return { + 'type': takerOrMaker, + 'currency': feeAsset, + 'rate': self.parse_number(feeRate), + 'cost': self.parse_number(feeCost), + } + + def get_quotes(self): + quotes = self.safe_value(self.options, 'quotes') + if quotes: + return quotes + else: + # currencies can have any name because you can create you own token + # result someone can create a fake token called BTC + # we use self mapping to determine the real tokens + # https://docs.wx.network/en/waves-matcher/matcher-api#asset-pair + response = self.matcherGetMatcherSettings() + # { + # "orderVersions": [ + # 1, + # 2, + # 3 + # ], + # "success": True, + # "matcherPublicKey": "9cpfKN9suPNvfeUNphzxXMjcnn974eme8ZhWUjaktzU5", + # "orderFee": { + # "dynamic": { + # "baseFee": 300000, + # "rates": { + # "34N9YcEETLWn93qYQ64EsP1x89tSruJU44RrEMSXXEPJ": 1.22639597, + # "62LyMjcr2DtiyF5yVXFhoQ2q414VPPJXjsNYp72SuDCH": 0.00989643, + # "HZk1mbfuJpmxU1Fs4AX5MWLVYtctsNcg6e2C6VKqK8zk": 0.0395674, + # "8LQW8f7P5d5PZM7GtZEBgaqRPGSzS3DfPuiXrURJ4AJS": 0.00018814, + # "4LHHvYGNKJUg5hj65aGD5vgScvCBmLpdRFtjokvCjSL8": 26.19721262, + # "474jTeYx2r2Va35794tCScAXWJG9hU2HcgxzMowaZUnu": 0.00752978, + # "DG2xFkPdDwKUoBkzGAhQtLpSGzfXLiCYPEzeKH2Ad24p": 1.84575, + # "B3uGHFRpSUuGEDWjqB9LWWxafQj8VTvpMucEyoxzws5H": 0.02330273, + # "zMFqXuoyrn5w17PFurTqxB7GsS71fp9dfk6XFwxbPCy": 0.00721412, + # "5WvPKSJXzVE2orvbkJ8wsQmmQKqTv9sGBPksV4adViw3": 0.02659103, + # "WAVES": 1, + # "BrjUWjndUanm5VsJkbUip8VRYy6LWJePtxya3FNv4TQa": 0.03433583 + # } + # } + # }, + # "networkByte": 87, + # "matcherVersion": "2.1.3.5", + # "status": "SimpleResponse", + # "priceAssets": [ + # "Ft8X1v1LTa1ABafufpaCWyVj8KkaxUWE6xBhW6sNFJck", + # "DG2xFkPdDwKUoBkzGAhQtLpSGzfXLiCYPEzeKH2Ad24p", + # "34N9YcEETLWn93qYQ64EsP1x89tSruJU44RrEMSXXEPJ", + # "Gtb1WRznfchDnTh37ezoDTJ4wcoKaRsKqKjJjy7nm2zU", + # "2mX5DzVKWrAJw8iwdJnV2qtoeVG9h5nTDpTqC1wb1WEN", + # "8LQW8f7P5d5PZM7GtZEBgaqRPGSzS3DfPuiXrURJ4AJS", + # "WAVES", + # "474jTeYx2r2Va35794tCScAXWJG9hU2HcgxzMowaZUnu", + # "zMFqXuoyrn5w17PFurTqxB7GsS71fp9dfk6XFwxbPCy", + # "62LyMjcr2DtiyF5yVXFhoQ2q414VPPJXjsNYp72SuDCH", + # "HZk1mbfuJpmxU1Fs4AX5MWLVYtctsNcg6e2C6VKqK8zk", + # "B3uGHFRpSUuGEDWjqB9LWWxafQj8VTvpMucEyoxzws5H", + # "5WvPKSJXzVE2orvbkJ8wsQmmQKqTv9sGBPksV4adViw3", + # "BrjUWjndUanm5VsJkbUip8VRYy6LWJePtxya3FNv4TQa", + # "4LHHvYGNKJUg5hj65aGD5vgScvCBmLpdRFtjokvCjSL8" + # ] + # } + quotes = {} + priceAssets = self.safe_value(response, 'priceAssets') + for i in range(0, len(priceAssets)): + quotes[priceAssets[i]] = True + self.options['quotes'] = quotes + return quotes + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for wavesexchange + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = self.marketGetTickers() + # + # [ + # { + # "symbol": "WAVES/BTC", + # "amountAssetID": "WAVES", + # "amountAssetName": "Waves", + # "amountAssetDecimals": 8, + # "amountAssetTotalSupply": "106908766.00000000", + # "amountAssetMaxSupply": "106908766.00000000", + # "amountAssetCirculatingSupply": "106908766.00000000", + # "priceAssetID": "8LQW8f7P5d5PZM7GtZEBgaqRPGSzS3DfPuiXrURJ4AJS", + # "priceAssetName": "WBTC", + # "priceAssetDecimals": 8, + # "priceAssetTotalSupply": "20999999.96007507", + # "priceAssetMaxSupply": "20999999.96007507", + # "priceAssetCirculatingSupply": "20999999.66019601", + # "24h_open": "0.00032688", + # "24h_high": "0.00033508", + # "24h_low": "0.00032443", + # "24h_close": "0.00032806", + # "24h_vwap": "0.00032988", + # "24h_volume": "42349.69440104", + # "24h_priceVolume": "13.97037207", + # "timestamp":1640232379124 + # } + # ... + # ] + # + result = [] + for i in range(0, len(response)): + entry = response[i] + baseId = self.safe_string(entry, 'amountAssetID') + quoteId = self.safe_string(entry, 'priceAssetID') + id = baseId + '/' + quoteId + marketId = self.safe_string(entry, 'symbol') + base, quote = marketId.split('/') + base = self.safe_currency_code(base) + quote = self.safe_currency_code(quote) + symbol = base + '/' + quote + result.append({ + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': None, + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(self.parse_precision(self.safe_string(entry, 'amountAssetDecimals'))), + 'price': self.parse_number(self.parse_precision(self.safe_string(entry, 'priceAssetDecimals'))), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': None, + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + 'info': entry, + }) + return result + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://matcher.waves.exchange/api-docs/index.html#/markets/getOrderBook + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request = self.extend({ + 'baseId': market['baseId'], + 'quoteId': market['quoteId'], + }, params) + response = self.matcherGetMatcherOrderbookBaseIdQuoteId(request) + timestamp = self.safe_integer(response, 'timestamp') + bids = self.parse_order_book_side(self.safe_value(response, 'bids'), market, limit) + asks = self.parse_order_book_side(self.safe_value(response, 'asks'), market, limit) + return { + 'symbol': symbol, + 'bids': bids, + 'asks': asks, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'nonce': None, + } + + def parse_order_book_side(self, bookSide, market=None, limit: Int = None): + precision = market['precision'] + wavesPrecision = self.safe_string(self.options, 'wavesPrecision', '1e-8') + amountPrecisionString = self.safe_string(precision, 'amount') + pricePrecisionString = self.safe_string(precision, 'price') + difference = Precise.string_div(amountPrecisionString, pricePrecisionString) + pricePrecision = Precise.string_div(wavesPrecision, difference) + result = [] + for i in range(0, len(bookSide)): + entry = bookSide[i] + entryPrice = self.safe_string(entry, 'price', '0') + entryAmount = self.safe_string(entry, 'amount', '0') + price = None + amount = None + if (pricePrecision is not None) and (entryPrice is not None): + price = Precise.string_mul(entryPrice, pricePrecision) + if (amountPrecisionString is not None) and (entryAmount is not None): + amount = Precise.string_mul(entryAmount, amountPrecisionString) + if (limit is not None) and (i > limit): + break + result.append([ + self.parse_number(price), + self.parse_number(amount), + ]) + return result + + def check_required_keys(self): + if self.apiKey is None: + raise AuthenticationError(self.id + ' requires apiKey credential') + if self.secret is None: + raise AuthenticationError(self.id + ' requires secret credential') + apiKeyBytes = None + secretKeyBytes = None + try: + apiKeyBytes = self.base58_to_binary(self.apiKey) + except Exception as e: + raise AuthenticationError(self.id + ' apiKey must be a base58 encoded public key') + try: + secretKeyBytes = self.base58_to_binary(self.secret) + except Exception as e: + raise AuthenticationError(self.id + ' secret must be a base58 encoded private key') + hexApiKeyBytes = self.binary_to_base16(apiKeyBytes) + hexSecretKeyBytes = self.binary_to_base16(secretKeyBytes) + if len(hexApiKeyBytes) != 64: + raise AuthenticationError(self.id + ' apiKey must be a base58 encoded public key') + if len(hexSecretKeyBytes) != 64: + raise AuthenticationError(self.id + ' secret must be a base58 encoded private key') + return True + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + query = self.omit(params, self.extract_params(path)) + isCancelOrder = path == 'matcher/orders/{wavesAddress}/cancel' + path = self.implode_params(path, params) + url = self.urls['api'][api] + '/' + path + queryString = self.urlencode_with_array_repeat(query) + if (api == 'private') or (api == 'forward'): + headers = { + 'Accept': 'application/json', + } + accessToken = self.safe_string(self.options, 'accessToken') + if accessToken: + headers['Authorization'] = 'Bearer ' + accessToken + if method == 'POST': + headers['content-type'] = 'application/json' + else: + headers['content-type'] = 'application/x-www-form-urlencoded' + if isCancelOrder: + body = self.json([query['orderId']]) + queryString = '' + if len(queryString) > 0: + url += '?' + queryString + elif api == 'matcher': + if method == 'POST': + headers = { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + } + body = self.json(query) + else: + headers = query + else: + if method == 'POST': + headers = { + 'content-type': 'application/json', + } + body = self.json(query) + else: + headers = { + 'content-type': 'application/x-www-form-urlencoded', + } + if len(queryString) > 0: + url += '?' + queryString + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def sign_in(self, params={}): + """ + sign in, must be called prior to using other authenticated methods + + https://docs.wx.network/en/api/auth/oauth2-token + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns: response from exchange + """ + if not self.safe_string(self.options, 'accessToken'): + prefix = 'ffffff01' + expiresDelta = 60 * 60 * 24 * 7 + seconds = self.sum(self.seconds(), expiresDelta) + seconds = str(seconds) + clientId = 'wx.network' + # W for production, T for testnet + defaultMessagePrefix = self.safe_string(self.options, 'messagePrefix', 'W') + message = defaultMessagePrefix + ':' + clientId + ':' + seconds + messageHex = self.binary_to_base16(self.encode(message)) + payload = prefix + messageHex + hexKey = self.binary_to_base16(self.base58_to_binary(self.secret)) + signature = self.axolotl(payload, hexKey, 'ed25519') + request: dict = { + 'grant_type': 'password', + 'scope': 'general', + 'username': self.apiKey, + 'password': seconds + ':' + signature, + 'client_id': clientId, + } + response = self.privatePostOauth2Token(request) + # {access_token: "eyJhbGciOXJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzaWciOiJiaTZiMVhMQlo0M1Q4QmRTSlVSejJBZGlQdVlpaFZQYVhhVjc4ZGVIOEpTM3M3NUdSeEU1VkZVOE5LRUI0UXViNkFHaUhpVFpuZ3pzcnhXdExUclRvZTgiLCJhIjoiM1A4VnpMU2EyM0VXNUNWY2tIYlY3ZDVCb043NWZGMWhoRkgiLCJuYiI6IlciLCJ1c2VyX25hbWUiOiJBSFhuOG5CQTRTZkxRRjdoTFFpU24xNmt4eWVoaml6QkdXMVRkcm1TWjFnRiIsInNjb3BlIjpbImdlbmVyYWwiXSwibHQiOjYwNDc5OSwicGsiOiJBSFhuOG5CQTRTZkxRRjdoTFFpU24xNmt4eWVoaml6QkdXMVRkcm1TWjFnRiIsImV4cCI6MTU5MTk3NTA1NywiZXhwMCI6MTU5MTk3NTA1NywianRpIjoiN2JhOTUxMTMtOGI2MS00NjEzLTlkZmYtNTEwYTc0NjlkOWI5IiwiY2lkIjoid2F2ZXMuZXhjaGFuZ2UifQ.B-XwexBnUAzbWknVN68RKT0ZP5w6Qk1SKJ8usL3OIwDEzCUUX9PjW-5TQHmiCRcA4oft8lqXEiCwEoNfsblCo_jTpRo518a1vZkIbHQk0-13Dm1K5ewGxfxAwBk0g49odcbKdjl64TN1yM_PO1VtLVuiTeZP-XF-S42Uj-7fcO-r7AulyQLuTE0uo-Qdep8HDCk47rduZwtJOmhFbCCnSgnLYvKWy3CVTeldsR77qxUY-vy8q9McqeP7Id-_MWnsob8vWXpkeJxaEsw1Fke1dxApJaJam09VU8EB3ZJWpkT7V8PdafIrQGeexx3jhKKxo7rRb4hDV8kfpVoCgkvFan", + # "token_type": "bearer", + # "refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzaWciOiJiaTZiMVhMQlo0M1Q4QmRTSlVSejJBZGlQdVlpaFZQYVhhVjc4ZGVIOEpTM3M3NUdSeEU1VkZVOE5LRUI0UXViNkFHaUhpVFpuZ3pzcnhXdExUclRvZTgiLCJhIjoiM1A4VnpMU2EyM0VXNUNWY2tIYlY3ZDVCb043NWZGMWhoRkgiLCJuYiI6IlciLCJ1c2VyX25hbWUiOiJBSFhuOG5CQTRTZkxRRjdoTFFpU24xNmt4eWVoaml6QkdXMVRkcm1TWjFnRiIsInNjb3BlIjpbImdlbmVyYWwiXSwiYXRpIjoiN2JhOTUxMTMtOGI2MS00NjEzLTlkZmYtNTEwYTc0NjlkXWI5IiwibHQiOjYwNDc5OSwicGsiOiJBSFhuOG5CQTRTZkxRRjdoTFFpU24xNmt4eWVoaml6QkdXMVRkcm1TWjFnRiIsImV4cCI6MTU5Mzk2MjI1OCwiZXhwMCI6MTU5MTk3NTA1NywianRpIjoiM2MzZWRlMTktNjI5My00MTNlLWJmMWUtZTRlZDZlYzUzZTgzIiwiY2lkIjoid2F2ZXMuZXhjaGFuZ2UifQ.gD1Qj0jfqayfZpBvNY0t3ccMyK5hdbT7dY-_5L6LxwV0Knan4ndEtvygxlTOczmJUKtnA4T1r5GBFgNMZTvtViKZIbqZNysEg2OY8UxwDaF4VPeGJLg_QXEnn8wBeBQdyMafh9UQdwD2ci7x-saM4tOAGmncAygfTDxy80201gwDhfAkAGerb9kL00oWzSJScldxu--pNLDBUEHZt52MSEel10HGrzvZkkvvSh67vcQo5TOGb5KG6nh65UdJCwr41AVz4fbQPP-N2Nkxqy0TE_bqVzZxExXgvcS8TS0Z82T3ijJa_ct7B9wblpylBnvmyj3VycUzufD6uy8MUGq32D", + # "expires_in": 604798, + # "scope": "general"} + self.options['accessToken'] = self.safe_string(response, 'access_token') + return self.options['accessToken'] + return None + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "symbol": "WAVES/BTC", + # "amountAssetID": "WAVES", + # "amountAssetName": "Waves", + # "amountAssetDecimals": 8, + # "amountAssetTotalSupply": "106908766.00000000", + # "amountAssetMaxSupply": "106908766.00000000", + # "amountAssetCirculatingSupply": "106908766.00000000", + # "priceAssetID": "8LQW8f7P5d5PZM7GtZEBgaqRPGSzS3DfPuiXrURJ4AJS", + # "priceAssetName": "WBTC", + # "priceAssetDecimals": 8, + # "priceAssetTotalSupply": "20999999.96007507", + # "priceAssetMaxSupply": "20999999.96007507", + # "priceAssetCirculatingSupply": "20999999.66019601", + # "24h_open": "0.00032688", + # "24h_high": "0.00033508", + # "24h_low": "0.00032443", + # "24h_close": "0.00032806", + # "24h_vwap": "0.00032988", + # "24h_volume": "42349.69440104", + # "24h_priceVolume": "13.97037207", + # "timestamp":1640232379124 + # } + # + # fetch ticker + # + # { + # "firstPrice": "21749", + # "lastPrice": "22000", + # "volume": "0.73747149", + # "quoteVolume": "16409.44564928645471", + # "high": "23589.999941", + # "low": "21010.000845", + # "weightedAveragePrice": "22250.955964", + # "txsCount": "148", + # "volumeWaves": "0.0000000000680511203072" + # } + # + timestamp = self.safe_integer(ticker, 'timestamp') + marketId = self.safe_string(ticker, 'symbol') + market = self.safe_market(marketId, market, '/') + symbol = market['symbol'] + last = self.safe_string_2(ticker, '24h_close', 'lastPrice') + low = self.safe_string_2(ticker, '24h_low', 'low') + high = self.safe_string_2(ticker, '24h_high', 'high') + vwap = self.safe_string_2(ticker, '24h_vwap', 'weightedAveragePrice') + baseVolume = self.safe_string_2(ticker, '24h_volume', 'volume') + quoteVolume = self.safe_string_2(ticker, '24h_priceVolume', 'quoteVolume') + open = self.safe_string_2(ticker, '24h_open', 'firstPrice') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': high, + 'low': low, + 'bid': None, + 'bidVolume': None, + 'ask': None, + 'askVolume': None, + 'vwap': vwap, + 'open': open, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'info': ticker, + }, market) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://api.wavesplatform.com/v0/docs/#/pairs/getPairsListAll + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pairs': market['id'], + } + response = self.publicGetPairs(self.extend(request, params)) + # + # { + # "__type":"list", + # "data":[ + # { + # "__type":"pair", + # "data":{ + # "firstPrice":0.00012512, + # "lastPrice":0.00012441, + # "low":0.00012167, + # "high":0.00012768, + # "weightedAveragePrice":0.000124710697407246, + # "volume":209554.26356614, + # "quoteVolume":26.1336583539951, + # "volumeWaves":209554.26356614, + # "txsCount":6655 + # }, + # "amountAsset":"WAVES", + # "priceAsset":"8LQW8f7P5d5PZM7GtZEBgaqRPGSzS3DfPuiXrURJ4AJS" + # } + # ] + # } + # + data = self.safe_value(response, 'data', []) + ticker = self.safe_value(data, 0, {}) + dataTicker = self.safe_dict(ticker, 'data', {}) + return self.parse_ticker(dataTicker, market) + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + :param str[] [symbols]: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + response = self.marketGetTickers(params) + # + # [ + # { + # "symbol": "WAVES/BTC", + # "amountAssetID": "WAVES", + # "amountAssetName": "Waves", + # "amountAssetDecimals": 8, + # "amountAssetTotalSupply": "106908766.00000000", + # "amountAssetMaxSupply": "106908766.00000000", + # "amountAssetCirculatingSupply": "106908766.00000000", + # "priceAssetID": "8LQW8f7P5d5PZM7GtZEBgaqRPGSzS3DfPuiXrURJ4AJS", + # "priceAssetName": "WBTC", + # "priceAssetDecimals": 8, + # "priceAssetTotalSupply": "20999999.96007507", + # "priceAssetMaxSupply": "20999999.96007507", + # "priceAssetCirculatingSupply": "20999999.66019601", + # "24h_open": "0.00032688", + # "24h_high": "0.00033508", + # "24h_low": "0.00032443", + # "24h_close": "0.00032806", + # "24h_vwap": "0.00032988", + # "24h_volume": "42349.69440104", + # "24h_priceVolume": "13.97037207", + # "timestamp":1640232379124 + # } + # ... + # ] + # + return self.parse_tickers(response, symbols) + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://api.wavesplatform.com/v0/docs/#/candles/getCandles + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest candle to fetch + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'baseId': market['baseId'], + 'quoteId': market['quoteId'], + 'interval': self.safe_string(self.timeframes, timeframe, timeframe), + } + allowedCandles = self.safe_integer(self.options, 'allowedCandles', 1440) + until = self.safe_integer(params, 'until') + untilIsDefined = until is not None + if limit is None: + limit = allowedCandles + limit = min(allowedCandles, limit) + duration = self.parse_timeframe(timeframe) * 1000 + if since is None: + now = self.milliseconds() + timeEnd = until if untilIsDefined else now + durationRoundedTimestamp = self.parse_to_int(timeEnd / duration) * duration + delta = (limit - 1) * duration + timeStart = durationRoundedTimestamp - delta + request['timeStart'] = str(timeStart) + if untilIsDefined: + request['timeEnd'] = str(until) + else: + request['timeStart'] = str(since) + if untilIsDefined: + request['timeEnd'] = str(until) + else: + timeEnd = self.sum(since, duration * limit) + request['timeEnd'] = str(timeEnd) + params = self.omit(params, 'until') + response = self.publicGetCandlesBaseIdQuoteId(self.extend(request, params)) + # + # { + # "__type": "list", + # "data": [ + # { + # "__type": "candle", + # "data": { + # "time": "2020-06-09T14:47:00.000Z", + # "open": 0.0250385, + # "close": 0.0250385, + # "high": 0.0250385, + # "low": 0.0250385, + # "volume": 0.01033012, + # "quoteVolume": 0.00025865, + # "weightedAveragePrice": 0.0250385, + # "maxHeight": 2099399, + # "txsCount": 5, + # "timeClose": "2020-06-09T14:47:59.999Z" + # } + # } + # ] + # } + # + data = self.safe_value(response, 'data', []) + result = self.parse_ohlcvs(data, market, timeframe, since, limit) + result = self.filter_future_candles(result) + lastClose = None + length = len(result) + for i in range(0, len(result)): + j = length - i - 1 + entry = result[j] + open = entry[1] + if open is None: + entry[1] = lastClose + entry[2] = lastClose + entry[3] = lastClose + entry[4] = lastClose + result[j] = entry + lastClose = entry[4] + return result + + def filter_future_candles(self, ohlcvs): + result = [] + timestamp = self.milliseconds() + for i in range(0, len(ohlcvs)): + if ohlcvs[i][0] > timestamp: + # stop when getting data from the future + break + result.append(ohlcvs[i]) + return result + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # { + # "__type": "candle", + # "data": { + # "time": "2020-06-05T20:46:00.000Z", + # "open": 240.573975, + # "close": 240.573975, + # "high": 240.573975, + # "low": 240.573975, + # "volume": 0.01278413, + # "quoteVolume": 3.075528, + # "weightedAveragePrice": 240.573975, + # "maxHeight": 2093895, + # "txsCount": 5, + # "timeClose": "2020-06-05T20:46:59.999Z" + # } + # } + # + data = self.safe_value(ohlcv, 'data', {}) + return [ + self.parse8601(self.safe_string(data, 'time')), + self.safe_number(data, 'open'), + self.safe_number(data, 'high'), + self.safe_number(data, 'low'), + self.safe_number(data, 'close'), + self.safe_number(data, 'volume', 0), + ] + + def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + self.sign_in() + networks = self.safe_value(self.options, 'networks', {}) + rawNetwork = self.safe_string_upper(params, 'network') + network = self.safe_string(networks, rawNetwork, rawNetwork) + params = self.omit(params, ['network']) + supportedCurrencies = self.privateGetPlatforms() + # + # { + # "type": "list", + # "page_info": { + # "has_next_page": False, + # "last_cursor": null + # }, + # "items": [ + # { + # "type": "platform", + # "id": "ETH", + # "name": "Ethereum", + # "currencies": [ + # "BAG", + # "BNT", + # "CRV", + # "EGG", + # "ETH", + # "EURN", + # "FL", + # "NSBT", + # "USDAP", + # "USDC", + # "USDFL", + # "USDN", + # "USDT", + # "WAVES" + # ] + # } + # ] + # } + # + currencies: dict = {} + networksByCurrency: dict = {} + items = self.safe_value(supportedCurrencies, 'items', []) + for i in range(0, len(items)): + entry = items[i] + currencyId = self.safe_string(entry, 'id') + innerCurrencies = self.safe_value(entry, 'currencies', []) + for j in range(0, len(innerCurrencies)): + currencyCode = self.safe_string(innerCurrencies, j) + currencies[currencyCode] = True + if not (currencyCode in networksByCurrency): + networksByCurrency[currencyCode] = {} + networksByCurrency[currencyCode][currencyId] = True + if not (code in currencies): + codes = list(currencies.keys()) + raise ExchangeError(self.id + ' fetchDepositAddress() ' + code + ' not supported. Currency code must be one of ' + ', '.join(codes)) + response = None + if network is None: + request: dict = { + 'currency': code, + } + response = self.privateGetDepositAddressesCurrency(self.extend(request, params)) + else: + supportedNetworks = networksByCurrency[code] + if not (network in supportedNetworks): + supportedNetworkKeys = list(supportedNetworks.keys()) + raise ExchangeError(self.id + ' ' + network + ' network ' + code + ' deposit address not supported. Network must be one of ' + ', '.join(supportedNetworkKeys)) + if network == 'WAVES': + request: dict = { + 'publicKey': self.apiKey, + } + responseInner = self.nodeGetAddressesPublicKeyPublicKey(self.extend(request, request)) + addressInner = self.safe_string(response, 'address') + return { + 'info': responseInner, + 'currency': code, + 'network': network, + 'address': addressInner, + 'tag': None, + } + else: + request: dict = { + 'currency': code, + 'platform': network, + } + response = self.privateGetDepositAddressesCurrencyPlatform(self.extend(request, params)) + # + # { + # "type": "deposit_addresses", + # "currency": { + # "type": "deposit_currency", + # "id": "ERGO", + # "waves_asset_id": "5dJj4Hn9t2Ve3tRpNGirUHy4yBK6qdJRAJYV21yPPuGz", + # "platform_id": "BSC", + # "decimals": 9, + # "status": "active", + # "allowed_amount": { + # "min": 0.001, + # "max": 100000 + # }, + # "fees": { + # "flat": 0, + # "rate": 0 + # } + # }, + # "deposit_addresses": [ + # "9fRAAQjF8Yqg7qicQCL884zjimsRnuwsSavsM1rUdDaoG8mThku" + # ] + # } + currency = self.safe_value(response, 'currency') + networkId = self.safe_string(currency, 'platform_id') + networkByIds = self.safe_value(self.options, 'networkByIds', {}) + unifiedNetwork = self.safe_string(networkByIds, networkId, networkId) + addresses = self.safe_value(response, 'deposit_addresses') + address = self.safe_string(addresses, 0) + return { + 'info': response, + 'currency': code, + 'network': unifiedNetwork, + 'address': address, + 'tag': None, + } + + def get_matcher_public_key(self): + # self method returns a single string + matcherPublicKey = self.safe_string(self.options, 'matcherPublicKey') + if matcherPublicKey: + return matcherPublicKey + else: + response = self.matcherGetMatcher() + # remove trailing quotes from string response + self.options['matcherPublicKey'] = response[1:len(response) - 1] + return self.options['matcherPublicKey'] + + def get_asset_bytes(self, currencyId): + if currencyId == 'WAVES': + return self.number_to_be(0, 1) + else: + return self.binary_concat(self.number_to_be(1, 1), self.base58_to_binary(currencyId)) + + def get_asset_id(self, currencyId): + if currencyId == 'WAVES': + return '' + return currencyId + + def to_real_currency_amount(self, code: str, amount: float, networkCode=None): + currency = self.currency(code) + stringValue = Precise.string_div(self.number_to_string(amount), self.safe_string(currency, 'precision')) + return int(stringValue) + + def from_real_currency_amount(self, code: str, amountString: str): + if not (code in self.currencies): + return amountString + currency = self.currency(code) + precisionAmount = self.safe_string(currency, 'precision') + return Precise.string_mul(amountString, precisionAmount) + + def to_real_symbol_price(self, symbol: str, price: float): + market = self.market(symbol) + stringValue = Precise.string_div(self.number_to_string(price), self.safe_string(market['precision'], 'price')) + return int(stringValue) + + def from_real_symbol_price(self, symbol: str, priceString: str): + market = self.markets[symbol] + return Precise.string_mul(priceString, self.safe_string(market['precision'], 'price')) + + def to_real_symbol_amount(self, symbol: str, amount: float): + market = self.market(symbol) + stringValue = Precise.string_div(self.number_to_string(amount), self.safe_string(market['precision'], 'amount')) + return int(stringValue) + + def from_real_symbol_amount(self, symbol: str, amountString: str): + market = self.markets[symbol] + return Precise.string_mul(amountString, market['precision']['amount']) + + def safe_get_dynamic(self, settings): + orderFee = self.safe_value(settings, 'orderFee') + if 'dynamic' in orderFee: + return self.safe_value(orderFee, 'dynamic') + else: + return self.safe_value(orderFee['composite']['default'], 'dynamic') + + def safe_get_rates(self, dynamic): + rates = self.safe_value(dynamic, 'rates') + if rates is None: + return {'WAVES': 1} + return rates + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://matcher.waves.exchange/api-docs/index.html#/serialize/serializeOrder + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: The price at which a stop order is triggered at + :returns dict: an `order structure ` + """ + self.check_required_dependencies() + self.check_required_keys() + self.load_markets() + market = self.market(symbol) + matcherPublicKey = self.get_matcher_public_key() + amountAsset = self.get_asset_id(market['baseId']) + priceAsset = self.get_asset_id(market['quoteId']) + isMarketOrder = (type == 'market') + triggerPrice = self.safe_float_2(params, 'triggerPrice', 'stopPrice') + isStopOrder = (triggerPrice is not None) + if (isMarketOrder) and (price is None): + raise InvalidOrder(self.id + ' createOrder() requires a price argument for ' + type + ' orders to determine the max price for buy and the min price for sell') + timestamp = self.milliseconds() + defaultExpiryDelta = None + defaultExpiryDelta, params = self.handle_option_and_params(params, 'createOrder', 'defaultExpiry', self.safe_integer(self.options, 'createOrderDefaultExpiry', 2419200000)) + expiration = self.sum(timestamp, defaultExpiryDelta) + matcherFees = self.get_fees_for_asset(symbol, side, amount, price) + # { + # "base":{ + # "feeAssetId":"WAVES", # varies depending on the trading pair + # "matcherFee":"1000000" + # }, + # "discount":{ + # "feeAssetId":"EMAMLxDnv3xiz8RXg8Btj33jcEw3wLczL3JKYYmuubpc", + # "matcherFee":"4077612" + # } + # } + base = self.safe_value_2(matcherFees, 'base', 'discount') + baseFeeAssetId = self.safe_string(base, 'feeAssetId') + baseFeeAsset = self.safe_currency_code(baseFeeAssetId) + baseMatcherFee = self.safe_string(base, 'matcherFee') + discount = self.safe_value(matcherFees, 'discount') + discountFeeAssetId = self.safe_string(discount, 'feeAssetId') + discountFeeAsset = self.safe_currency_code(discountFeeAssetId) + discountMatcherFee = self.safe_string(discount, 'matcherFee') + matcherFeeAssetId = None + matcherFee = None + # check first if user supplied asset fee is valid + if ('feeAsset' in params) or ('feeAsset' in self.options): + feeAsset = self.safe_string(params, 'feeAsset', self.safe_string(self.options, 'feeAsset')) + feeCurrency = self.currency(feeAsset) + matcherFeeAssetId = self.safe_string(feeCurrency, 'id') + balances = self.fetch_balance() + if matcherFeeAssetId is not None: + if baseFeeAssetId != matcherFeeAssetId and discountFeeAssetId != matcherFeeAssetId: + raise InvalidOrder(self.id + ' asset fee must be ' + baseFeeAsset + ' or ' + discountFeeAsset) + matcherFeeAsset = self.safe_currency_code(matcherFeeAssetId) + rawMatcherFee = baseMatcherFee if (matcherFeeAssetId == baseFeeAssetId) else discountMatcherFee + floatMatcherFee = float(self.from_real_currency_amount(matcherFeeAsset, rawMatcherFee)) + if (matcherFeeAsset in balances) and (balances[matcherFeeAsset]['free'] >= floatMatcherFee): + matcherFee = int(rawMatcherFee) + else: + raise InsufficientFunds(self.id + ' not enough funds of the selected asset fee') + floatBaseMatcherFee = self.from_real_currency_amount(baseFeeAsset, baseMatcherFee) + floatDiscountMatcherFee = self.from_real_currency_amount(discountFeeAsset, discountMatcherFee) + if matcherFeeAssetId is None: + # try to the pay the fee using the base first then discount asset + if (baseFeeAsset in balances) and (balances[baseFeeAsset]['free'] >= float(floatBaseMatcherFee)): + matcherFeeAssetId = baseFeeAssetId + matcherFee = int(baseMatcherFee) + else: + if (discountFeeAsset in balances) and (balances[discountFeeAsset]['free'] >= float(floatDiscountMatcherFee)): + matcherFeeAssetId = discountFeeAssetId + matcherFee = int(discountMatcherFee) + if matcherFeeAssetId is None: + raise InsufficientFunds(self.id + ' not enough funds on none of the eligible asset fees: ' + baseFeeAsset + ' ' + floatBaseMatcherFee + ' or ' + discountFeeAsset + ' ' + floatDiscountMatcherFee) + amount = self.to_real_symbol_amount(symbol, amount) + price = self.to_real_symbol_price(symbol, price) + assetPair: dict = { + 'amountAsset': amountAsset, + 'priceAsset': priceAsset, + } + sandboxMode = self.safe_bool(self.options, 'sandboxMode', False) + chainId = 84 if (sandboxMode) else 87 + body: dict = { + 'senderPublicKey': self.apiKey, + 'matcherPublicKey': matcherPublicKey, + 'assetPair': assetPair, + 'orderType': side, + 'price': price, + 'amount': amount, + 'timestamp': timestamp, + 'expiration': expiration, + 'matcherFee': int(matcherFee), + 'priceMode': 'assetDecimals', + 'version': 4, + 'chainId': chainId, + } + if isStopOrder: + # + # { + # "v": 1, # version(int) + # "c": { # condition(object) + # "t": "sp", # condition type. for now only "stop-price"(string) + # "v": { # value(object) + # "p": "123", # price(long) + # }, + # }, + # } + # + attachment: dict = { + 'v': 1, + 'c': { + 't': 'sp', + 'v': { + 'p': self.to_real_symbol_price(symbol, triggerPrice), + }, + }, + } + body['attachment'] = self.binary_to_base58(self.encode(json.dumps(attachment))) + if matcherFeeAssetId != 'WAVES': + body['matcherFeeAssetId'] = matcherFeeAssetId + serializedOrder = self.matcherPostMatcherOrdersSerialize(body) + if (serializedOrder[0] == '"') and (serializedOrder[(len(serializedOrder) - 1)] == '"'): + serializedOrder = serializedOrder[1:len(serializedOrder) - 1] + signature = self.axolotl(self.binary_to_base16(self.base58_to_binary(serializedOrder)), self.binary_to_base16(self.base58_to_binary(self.secret)), 'ed25519') + body['signature'] = signature + # + # { + # "success": True, + # "message": { + # "version": 4, + # "id": "8VR49dLZFaYcVwzx9TqVMTAZCSUoyB74kLUHrEPCSJgN", + # "sender": "3MpEdBXtsRHRj2TvZURSb8uLDxzneVbYczW", + # "senderPublicKey": "8aUTNqHGCBiubySBRhcS1N6NC5jLczhVcndRfMAuwtkY", + # "matcherPublicKey": "8QUAqtTckM5B8gvcuP7mMswat9SjKUuafJMusEoSn1Gy", + # "assetPair": { + # "amountAsset": "EMAMLxDnv3xiz8RXg8Btj33jcEw3wLczL3JKYYmuubpc", + # "priceAsset": "25FEqEjRkqK6yCkiT7Lz6SAYz7gUFCtxfCChnrVFD5AT" + # }, + # "orderType": "sell", + # "amount": 100000, + # "price": 480000, + # "timestamp": 1690852043772, + # "expiration": 1693271243772, + # "matcherFee": 83327570, + # "signature": "3QYDWQVSP4kdqpTLodCuboh8bpWd6GW5s1pQyKdce1JBDwX6t4kH5Xtuq35pqo94gxjo3cfG6k6Xuic2JaYLubkK", + # "proofs": [ + # "3QYDWQVSP4kdqpTLodCuboh8bpWd6GW5s1pQyKdce1JBDwX6t4kH5Xtuq35pqo94gxjo3cfG6k6Xuic2JaYLubkK" + # ], + # "matcherFeeAssetId": "EMAMLxDnv3xiz8RXg8Btj33jcEw3wLczL3JKYYmuubpc", + # "eip712Signature": null, + # "priceMode": "assetDecimals", + # "attachment": "2PQ4akZHnMSZrQissuu5uudoXbgsipeDnFcRtXtjVgkdm1gUWEgGzp" + # }, + # "status": "OrderAccepted" + # } + # + if isMarketOrder: + response = self.matcherPostMatcherOrderbookMarket(self.extend(body, params)) + value = self.safe_dict(response, 'message') + return self.parse_order(value, market) + else: + response = self.matcherPostMatcherOrderbook(self.extend(body, params)) + value = self.safe_dict(response, 'message') + return self.parse_order(value, market) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://matcher.waves.exchange/api-docs/index.html#/cancel/cancelOrdersByIdsWithKeyOrSignature + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.check_required_dependencies() + self.check_required_keys() + self.sign_in() + wavesAddress = self.get_waves_address() + response = self.forwardPostMatcherOrdersWavesAddressCancel({ + 'wavesAddress': wavesAddress, + 'orderId': id, + }) + # { + # "success":true, + # "message":[[{"orderId":"EBpJeGM36KKFz5gTJAUKDBm89V8wqxKipSFBdU35AN3c","success":true,"status":"OrderCanceled"}]], + # "status":"BatchCancelCompleted" + # } + message = self.safe_value(response, 'message') + firstMessage = self.safe_value(message, 0) + firstOrder = self.safe_value(firstMessage, 0) + returnedId = self.safe_string(firstOrder, 'orderId') + return self.safe_order({ + 'info': response, + 'id': returnedId, + 'clientOrderId': None, + 'timestamp': None, + 'datetime': None, + 'lastTradeTimestamp': None, + 'symbol': symbol, + 'type': None, + 'side': None, + 'price': None, + 'amount': None, + 'cost': None, + 'average': None, + 'filled': None, + 'remaining': None, + 'status': None, + 'fee': None, + 'trades': None, + }) + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetches information on an order made by the user + + https://matcher.waves.exchange/api-docs/index.html#/status/getOrderStatusByPKAndIdWithSig + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.check_required_dependencies() + self.check_required_keys() + self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + timestamp = self.milliseconds() + byteArray = [ + self.base58_to_binary(self.apiKey), + self.number_to_be(timestamp, 8), + ] + binary = self.binary_concat_array(byteArray) + hexSecret = self.binary_to_base16(self.base58_to_binary(self.secret)) + signature = self.axolotl(self.binary_to_base16(binary), hexSecret, 'ed25519') + request: dict = { + 'Timestamp': str(timestamp), + 'Signature': signature, + 'publicKey': self.apiKey, + 'orderId': id, + } + response = self.matcherGetMatcherOrderbookPublicKeyOrderId(self.extend(request, params)) + return self.parse_order(response, market) + + def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + self.check_required_dependencies() + self.check_required_keys() + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrders() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + timestamp = self.milliseconds() + byteArray = [ + self.base58_to_binary(self.apiKey), + self.number_to_be(timestamp, 8), + ] + binary = self.binary_concat_array(byteArray) + hexSecret = self.binary_to_base16(self.base58_to_binary(self.secret)) + signature = self.axolotl(self.binary_to_base16(binary), hexSecret, 'ed25519') + request: dict = { + 'Accept': 'application/json', + 'Timestamp': str(timestamp), + 'Signature': signature, + 'publicKey': self.apiKey, + 'baseId': market['baseId'], + 'quoteId': market['quoteId'], + } + response = self.matcherGetMatcherOrderbookBaseIdQuoteIdPublicKeyPublicKey(self.extend(request, params)) + # [{id: "3KicDeWayY2mdrRoYdCkP3gUAoUZUNT1AA6GAtWuPLfa", + # "type": "sell", + # "orderType": "limit", + # "amount": 1, + # "fee": 300000, + # "price": 100000000, + # "timestamp": 1591651254076, + # "filled": 0, + # "filledFee": 0, + # "feeAsset": "WAVES", + # "status": "Accepted", + # "assetPair": + # {amountAsset: null, + # "priceAsset": "8LQW8f7P5d5PZM7GtZEBgaqRPGSzS3DfPuiXrURJ4AJS"}, + # "avgWeighedPrice": 0}, ...] + return self.parse_orders(response, market, since, limit) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + self.sign_in() + market = None + if symbol is not None: + market = self.market(symbol) + address = self.get_waves_address() + request: dict = { + 'address': address, + 'activeOnly': True, + } + response = self.forwardGetMatcherOrdersAddress(request) + return self.parse_orders(response, market, since, limit) + + def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + self.sign_in() + market = None + if symbol is not None: + market = self.market(symbol) + address = self.get_waves_address() + request: dict = { + 'address': address, + 'closedOnly': True, + } + response = self.forwardGetMatcherOrdersAddress(request) + # [ + # { + # "id": "9aXcxvXai73jbAm7tQNnqaQ2PwUjdmWuyjvRTKAHsw4f", + # "type": "buy", + # "orderType": "limit", + # "amount": 23738330, + # "fee": 300000, + # "price": 3828348334, + # "timestamp": 1591926905636, + # "filled": 23738330, + # "filledFee": 300000, + # "feeAsset": "WAVES", + # "status": "Filled", + # "assetPair": { + # "amountAsset": "HZk1mbfuJpmxU1Fs4AX5MWLVYtctsNcg6e2C6VKqK8zk", + # "priceAsset": null + # }, + # "avgWeighedPrice": 3828348334 + # }, ... + # ] + return self.parse_orders(response, market, since, limit) + + def parse_order_status(self, status: Str): + statuses: dict = { + 'Cancelled': 'canceled', + 'Accepted': 'open', + 'Filled': 'closed', + 'PartiallyFilled': 'open', + } + return self.safe_string(statuses, status, status) + + def get_symbol_from_asset_pair(self, assetPair): + # a blank string or null can indicate WAVES + baseId = self.safe_string(assetPair, 'amountAsset', 'WAVES') + quoteId = self.safe_string(assetPair, 'priceAsset', 'WAVES') + return self.safe_currency_code(baseId) + '/' + self.safe_currency_code(quoteId) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder + # + # { + # "version": 4, + # "id": "BshyeHXDfJmTnjTdBYt371jD4yWaT3JTP6KpjpsiZepS", + # "sender": "3P8VzLSa23EW5CVckHbV7d5BoN75fF1hhFH", + # "senderPublicKey": "AHXn8nBA4SfLQF7hLQiSn16kxyehjizBGW1TdrmSZ1gF", + # "matcherPublicKey": "9cpfKN9suPNvfeUNphzxXMjcnn974eme8ZhWUjaktzU5", + # "assetPair": { + # "amountAsset": "474jTeYx2r2Va35794tCScAXWJG9hU2HcgxzMowaZUnu", + # "priceAsset": "DG2xFkPdDwKUoBkzGAhQtLpSGzfXLiCYPEzeKH2Ad24p", + # }, + # "orderType": "buy", + # "amount": 10000, + # "price": 400000000, + # "timestamp": 1599848586891, + # "expiration": 1602267786891, + # "matcherFee": 3008, + # "matcherFeeAssetId": "474jTeYx2r2Va35794tCScAXWJG9hU2HcgxzMowaZUnu", + # "signature": "3D2h8ubrhuWkXbVn4qJ3dvjmZQxLoRNfjTqb9uNpnLxUuwm4fGW2qGH6yKFe2SQPrcbgkS3bDVe7SNtMuatEJ7qy", + # "proofs": [ + # "3D2h8ubrhuWkXbVn4qJ3dvjmZQxLoRNfjTqb9uNpnLxUuwm4fGW2qGH6yKFe2SQPrcbgkS3bDVe7SNtMuatEJ7qy", + # ], + # "attachment":"77rnoyFX5BDr15hqZiUtgXKSN46zsbHHQjVNrTMLZcLz62mmFKr39FJ" + # } + # + # + # fetchOrder, fetchOrders, fetchOpenOrders, fetchClosedOrders + # + # { + # "id": "81D9uKk2NfmZzfG7uaJsDtxqWFbJXZmjYvrL88h15fk8", + # "type": "buy", + # "orderType": "limit", + # "amount": 30000000000, + # "filled": 0, + # "price": 1000000, + # "fee": 300000, + # "filledFee": 0, + # "feeAsset": "WAVES", + # "timestamp": 1594303779322, + # "status": "Cancelled", + # "assetPair": { + # "amountAsset": "474jTeYx2r2Va35794tCScAXWJG9hU2HcgxzMowaZUnu", + # "priceAsset": "WAVES" + # }, + # "avgWeighedPrice": 0, + # "version": 4, + # "totalExecutedPriceAssets": 0, # in fetchOpenOrder/s + # "attachment":"77rnoyFX5BDr15hqZiUtgXKSN46zsbHHQjVNrTMLZcLz62mmFKr39FJ" + # } + # + timestamp = self.safe_integer(order, 'timestamp') + side = self.safe_string_2(order, 'type', 'orderType') + type = 'limit' + if 'type' in order: + # fetchOrders + type = self.safe_string(order, 'orderType', type) + id = self.safe_string(order, 'id') + filledString = self.safe_string(order, 'filled') + priceString = self.safe_string(order, 'price') + amountString = self.safe_string(order, 'amount') + assetPair = self.safe_value(order, 'assetPair') + symbol = None + if assetPair is not None: + symbol = self.get_symbol_from_asset_pair(assetPair) + elif market is not None: + symbol = market['symbol'] + amountCurrency = self.safe_currency_code(self.safe_string(assetPair, 'amountAsset', 'WAVES')) + price = self.from_real_symbol_price(symbol, priceString) + amount = self.from_real_currency_amount(amountCurrency, amountString) + filled = self.from_real_currency_amount(amountCurrency, filledString) + average = self.from_real_symbol_price(symbol, self.safe_string(order, 'avgWeighedPrice')) + status = self.parse_order_status(self.safe_string(order, 'status')) + fee = None + if 'type' in order: + code = self.safe_currency_code(self.safe_string(order, 'feeAsset')) + fee = { + 'currency': code, + 'fee': self.parse_number(self.from_real_currency_amount(code, self.safe_string(order, 'filledFee'))), + } + else: + code = self.safe_currency_code(self.safe_string(order, 'matcherFeeAssetId', 'WAVES')) + fee = { + 'currency': code, + 'fee': self.parse_number(self.from_real_currency_amount(code, self.safe_string(order, 'matcherFee'))), + } + triggerPrice = None + attachment = self.safe_string(order, 'attachment') + if attachment is not None: + decodedAttachment = self.parse_json(self.decode(self.base58_to_binary(attachment))) + if decodedAttachment is not None: + c = self.safe_value(decodedAttachment, 'c') + if c is not None: + v = self.safe_value(c, 'v') + if v is not None: + triggerPrice = self.safe_string(v, 'p') + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'symbol': symbol, + 'type': type, + 'timeInForce': None, + 'postOnly': None, + 'side': side, + 'price': price, + 'triggerPrice': triggerPrice, + 'amount': amount, + 'cost': None, + 'average': average, + 'filled': filled, + 'remaining': None, + 'status': status, + 'fee': fee, + 'trades': None, + }, market) + + def get_waves_address(self): + cachedAddreess = self.safe_string(self.options, 'wavesAddress') + if cachedAddreess is None: + request: dict = { + 'publicKey': self.apiKey, + } + response = self.nodeGetAddressesPublicKeyPublicKey(request) + self.options['wavesAddress'] = self.safe_string(response, 'address') + return self.options['wavesAddress'] + else: + return cachedAddreess + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + # makes a lot of different requests to get all the data + # in particular: + # fetchMarkets, getWavesAddress, + # getTotalBalance(doesn't include waves), getReservedBalance(doesn't include waves) + # getReservedBalance(includes WAVES) + # I couldn't find another way to get all the data + self.check_required_dependencies() + self.check_required_keys() + self.load_markets() + wavesAddress = self.get_waves_address() + request: dict = { + 'address': wavesAddress, + } + totalBalance = self.nodeGetAssetsBalanceAddress(request) + # { + # "address": "3P8VzLSa23EW5CVckHbV7d5BoN75fF1hhFH", + # "balances": [ + # { + # "assetId": "DG2xFkPdDwKUoBkzGAhQtLpSGzfXLiCYPEzeKH2Ad24p", + # "balance": 1177200, + # "reissuable": False, + # "minSponsoredAssetFee": 7420, + # "sponsorBalance": 47492147189709, + # "quantity": 999999999775381400, + # "issueTransaction": { + # "senderPublicKey": "BRnVwSVctnV8pge5vRpsJdWnkjWEJspFb6QvrmZvu3Ht", + # "quantity": 1000000000000000000, + # "fee": 100400000, + # "description": "Neutrino USD", + # "type": 3, + # "version": 2, + # "reissuable": False, + # "script": null, + # "sender": "3PC9BfRwJWWiw9AREE2B3eWzCks3CYtg4yo", + # "feeAssetId": null, + # "chainId": 87, + # "proofs": [ + # "3HNpbVkgP69NWSeb9hGYauiQDaXrRXh3tXFzNsGwsAAXnFrA29SYGbLtziW9JLpXEq7qW1uytv5Fnm5XTUMB2BxU" + # ], + # "assetId": "DG2xFkPdDwKUoBkzGAhQtLpSGzfXLiCYPEzeKH2Ad24p", + # "decimals": 6, + # "name": "USD-N", + # "id": "DG2xFkPdDwKUoBkzGAhQtLpSGzfXLiCYPEzeKH2Ad24p", + # "timestamp": 1574429393962 + # } + # } + # ] + # } + balances = self.safe_value(totalBalance, 'balances', []) + result: dict = {} + timestamp = None + assetIds = [] + nonStandardBalances = [] + for i in range(0, len(balances)): + entry = balances[i] + entryTimestamp = self.safe_integer(entry, 'timestamp') + timestamp = entryTimestamp if (timestamp is None) else max(timestamp, entryTimestamp) + issueTransaction = self.safe_value(entry, 'issueTransaction') + currencyId = self.safe_string(entry, 'assetId') + balance = self.safe_string(entry, 'balance') + currencyExists = (currencyId in self.currencies_by_id) + if currencyExists: + code = self.safe_currency_code(currencyId) + result[code] = self.account() + result[code]['total'] = self.from_real_currency_amount(code, balance) + elif issueTransaction is None: + assetIds.append(currencyId) + nonStandardBalances.append(balance) + nonStandardAssets = len(assetIds) + if nonStandardAssets: + requestInner: dict = { + 'ids': assetIds, + } + response = self.publicGetAssets(requestInner) + data = self.safe_value(response, 'data', []) + for i in range(0, len(data)): + entry = data[i] + balance = nonStandardBalances[i] + inner = self.safe_value(entry, 'data') + precision = self.parse_precision(self.safe_string(inner, 'precision')) + ticker = self.safe_string(inner, 'ticker') + code = self.safe_currency_code(ticker) + result[code] = self.account() + result[code]['total'] = Precise.string_mul(balance, precision) + currentTimestamp = self.milliseconds() + byteArray = [ + self.base58_to_binary(self.apiKey), + self.number_to_be(currentTimestamp, 8), + ] + binary = self.binary_concat_array(byteArray) + hexSecret = self.binary_to_base16(self.base58_to_binary(self.secret)) + signature = self.axolotl(self.binary_to_base16(binary), hexSecret, 'ed25519') + matcherRequest: dict = { + 'publicKey': self.apiKey, + 'signature': signature, + 'timestamp': str(currentTimestamp), + } + reservedBalance = self.matcherGetMatcherBalanceReservedPublicKey(matcherRequest) + # {WAVES: 200300000} + reservedKeys = list(reservedBalance.keys()) + for i in range(0, len(reservedKeys)): + currencyId = reservedKeys[i] + code = self.safe_currency_code(currencyId) + if not (code in result): + result[code] = self.account() + amount = self.safe_string(reservedBalance, currencyId) + result[code]['used'] = self.from_real_currency_amount(code, amount) + wavesRequest: dict = { + 'address': wavesAddress, + } + wavesTotal = self.nodeGetAddressesBalanceAddress(wavesRequest) + # { + # "address": "3P8VzLSa23EW5CVckHbV7d5BoN75fF1hhFH", + # "confirmations": 0, + # "balance": 909085978 + # } + result['WAVES'] = self.safe_value(result, 'WAVES', self.account()) + result['WAVES']['total'] = self.from_real_currency_amount('WAVES', self.safe_string(wavesTotal, 'balance')) + result = self.set_undefined_balances_to_zero(result) + result['timestamp'] = timestamp + result['datetime'] = self.iso8601(timestamp) + return self.safe_balance(result) + + def set_undefined_balances_to_zero(self, balances, key='used'): + codes = list(balances.keys()) + for i in range(0, len(codes)): + code = codes[i] + if self.safe_value(balances[code], 'used') is None: + balances[code][key] = '0' + return balances + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://api.wavesplatform.com/v0/docs/#/transactions/searchTxsExchange + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + address = self.get_waves_address() + request: dict = { + 'sender': address, + } + market = None + if symbol is not None: + market = self.market(symbol) + request['amountAsset'] = market['baseId'] + request['priceAsset'] = market['quoteId'] + response = self.publicGetTransactionsExchange(request) + data = self.safe_value(response, 'data') + # + # { + # "__type":"list", + # "isLastPage":true, + # "lastCursor":"MzA2MjQ0MzAwMDI5OjpkZXNj", + # "data": [ + # { + # "__type":"transaction", + # "data": { + # "id":"GbjPqco2wRP5QSrY5LimFrUyJaM535K9nhK5zaQ7J7Tx", + # "timestamp":"2022-04-06T19:56:31.479Z", + # "height":3062443, + # "type":7, + # "version":2, + # "proofs":[ + # "57mYrANw61eiArCTv2eYwzXm71jYC2KpZ5AeM9zHEstuRaYSAWSuSE7njAJYJu8zap6DMCm3nzqc6es3wQFDpRCN" + # ], + # "fee":0.003, + # "applicationStatus":"succeeded", + # "sender":"3PEjHv3JGjcWNpYEEkif2w8NXV4kbhnoGgu", + # "senderPublicKey":"9cpfKN9suPNvfeUNphzxXMjcnn974eme8ZhWUjaktzU5", + # "buyMatcherFee":0, + # "sellMatcherFee":0.00141728, + # "price":215.7431, + # "amount":0.09, + # "order1": { + # "id":"49qiuQj5frdZ6zpTCEpMuKPMAh1EimwXpXWB4BeCw33h", + # "senderPublicKey":"CjUfoH3dsDZsf5UuAjqqzpWHXgvKzBZpVG9YixF7L48K", + # "matcherPublicKey":"9cpfKN9suPNvfeUNphzxXMjcnn974eme8ZhWUjaktzU5", + # "assetPair": { + # "amountAsset":"7TMu26hAs7B2oW6c5sfx45KSZT7GQA3TZNYuCav8Dcqt", + # "priceAsset":"DG2xFkPdDwKUoBkzGAhQtLpSGzfXLiCYPEzeKH2Ad24p" + # }, + # "orderType":"buy", + # "price":215.7431, + # "sender":"3PR9WmaHV5ueVw2Wr9xsiCG3t4ySXzkkGLy", + # "amount":0.36265477, + # "timestamp":"2022-04-06T19:55:06.832Z", + # "expiration":"2022-05-05T19:55:06.832Z", + # "matcherFee":3.000334, + # "signature":"2rBWhdeuRJNpQfXfTFtcR8x8Lpic8FUHPdLML9uxABRUuxe48YRJcZxbncwWAh9LWFCEUZiztv7RZBZfGMWfFxTs", + # "matcherFeeAssetId":"DG2xFkPdDwKUoBkzGAhQtLpSGzfXLiCYPEzeKH2Ad24p" + # }, + # "order2": { + # "id":"AkxiJqCuv6wm8K41TUSgFNwShZMnCbMDT78MqrcWpQ53", + # "senderPublicKey":"72o7qNKyne5hthB1Ww6famE7uHrk5vTVB2ZfUMBEqL3Y", + # "matcherPublicKey":"9cpfKN9suPNvfeUNphzxXMjcnn974eme8ZhWUjaktzU5", + # "assetPair": { + # "amountAsset":"7TMu26hAs7B2oW6c5sfx45KSZT7GQA3TZNYuCav8Dcqt", + # "priceAsset":"DG2xFkPdDwKUoBkzGAhQtLpSGzfXLiCYPEzeKH2Ad24p" + # }, + # "orderType":"sell", + # "price":210, + # "sender":"3P3CzbjGgiqEyUBeKZYfgZtyaZfMG8fjoUD", + # "amount":0.09, + # "timestamp":"2022-04-06T19:56:18.535Z", + # "expiration":"2022-05-04T19:56:18.535Z", + # "matcherFee":0.00141728, + # "signature":"5BZCjYn6QzVkMXBFDBnzcAUBdCZqhq9hQfRXFHfLUQCsbis4zeriw4sUqLa1BZRT2isC6iY4Z4HtekikPqZ461PT", + # "matcherFeeAssetId":"7TMu26hAs7B2oW6c5sfx45KSZT7GQA3TZNYuCav8Dcqt" + # } + # } + # },... + # ] + # } + # + return self.parse_trades(data, market, since, limit) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://api.wavesplatform.com/v0/docs/#/transactions/searchTxsExchange + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'amountAsset': market['baseId'], + 'priceAsset': market['quoteId'], + } + if limit is not None: + request['limit'] = min(limit, 100) + if since is not None: + request['timeStart'] = since + response = self.publicGetTransactionsExchange(request) + data = self.safe_value(response, 'data') + # + # { + # "__type":"list", + # "isLastPage":false, + # "lastCursor":"MzA2MjM2MTAwMDU0OjpkZXNj", + # "data": [ + # { + # "__type":"transaction", + # "data": { + # "id":"F42WsvSsyEzvpPLFjVhQKkSNuopooP4zMkjSUs47NeML", + # "timestamp":"2022-04-06T18:39:49.145Z", + # "height":3062361, + # "type":7, + # "version":2, + # "proofs": [ + # "39iJv82kFi4pyuBxYeZpP45NXXjbrCXdVsHPAAvj32UMLmTXLjMTfV43PcmZDSAuS93HKSDo1aKJrin8UvkeE9Bs" + # ], + # "fee":0.003, + # "applicationStatus":"succeeded", + # "sender":"3PEjHv3JGjcWNpYEEkif2w8NXV4kbhnoGgu", + # "senderPublicKey":"9cpfKN9suPNvfeUNphzxXMjcnn974eme8ZhWUjaktzU5", + # "buyMatcherFee":0.02314421, + # "sellMatcherFee":0, + # "price":217.3893, + # "amount":0.34523025, + # "order1": { + # "id":"HkM36PHGaeeZdDKT1mYgZXhaU9PRZ54RZiJc2K4YMT3Q", + # "senderPublicKey":"7wYCaDcc6GX1Jx2uS7QgLHBypBKvrezTS1HfiW6Xe4Bk", + # "matcherPublicKey":"9cpfKN9suPNvfeUNphzxXMjcnn974eme8ZhWUjaktzU5", + # "assetPair": { + # "amountAsset":"7TMu26hAs7B2oW6c5sfx45KSZT7GQA3TZNYuCav8Dcqt", + # "priceAsset":"DG2xFkPdDwKUoBkzGAhQtLpSGzfXLiCYPEzeKH2Ad24p" + # }, + # "orderType":"buy", + # "price":225.2693, + # "sender":"3PLPc8f4DGYaF9C9bwJ2uVmHqRv3NCjg5VQ", + # "amount":2.529, + # "timestamp":"2022-04-06T18:39:48.796Z", + # "expiration":"2022-05-05T18:39:48.796Z", + # "matcherFee":0.17584444, + # "signature":"2yQfJoomv86evQDw36fg1uiRkHvPDZtRp3qvxqTBWPvz4JLTHGQtEHJF5NGTvym6U93CtgNprngzmD9ecHBjxf6U", + # "matcherFeeAssetId":"Atqv59EYzjFGuitKVnMRk6H8FukjoV3ktPorbEys25on" + # }, + # "order2": { + # "id":"F7HKmeuzwWdk3wKitHLnVx5MuD4wBWPpphQ8kUGx4tT9", + # "senderPublicKey":"CjUfoH3dsDZsf5UuAjqqzpWHXgvKzBZpVG9YixF7L48K", + # "matcherPublicKey":"9cpfKN9suPNvfeUNphzxXMjcnn974eme8ZhWUjaktzU5", + # "assetPair": { + # "amountAsset":"7TMu26hAs7B2oW6c5sfx45KSZT7GQA3TZNYuCav8Dcqt", + # "priceAsset":"DG2xFkPdDwKUoBkzGAhQtLpSGzfXLiCYPEzeKH2Ad24p" + # }, + # "orderType":"sell", + # "price":217.3893, + # "sender":"3PR9WmaHV5ueVw2Wr9xsiCG3t4ySXzkkGLy", + # "amount":0.35767793, + # "timestamp":"2022-04-06T18:32:01.390Z", + # "expiration":"2022-05-05T18:32:01.390Z", + # "matcherFee":0.0139168, + # "signature":"34HgWVLPgeYWkiSvAc5ChVepGTYDQDug2dMTSincs6idEyoM7AtaZuH3mqQ5RJG2fcxxH2QSB723Qq3dgLQwQmKf", + # "matcherFeeAssetId":"7TMu26hAs7B2oW6c5sfx45KSZT7GQA3TZNYuCav8Dcqt" + # } + # } + # }, ... + # ] + # } + # + return self.parse_trades(data, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # {__type: "transaction", + # "data": + # {id: "HSdruioHqvYHeyn9hhyoHdRWPB2bFA8ujeCPZMK6992c", + # "timestamp": "2020-06-09T19:34:51.897Z", + # "height": 2099684, + # "type": 7, + # "version": 2, + # "proofs": + # ["26teDHERQgwjjHqEn4REcDotNG8M21xjou3X42XuDuCvrRkQo6aPyrswByH3UrkWG8v27ZAaVNzoxDg4teNcLtde"], + # "fee": 0.003, + # "sender": "3PEjHv3JGjcWNpYEEkif2w8NXV4kbhnoGgu", + # "senderPublicKey": "9cpfKN9suPNvfeUNphzxXMjcnn974eme8ZhWUjaktzU5", + # "buyMatcherFee": 0.00299999, + # "sellMatcherFee": 0.00299999, + # "price": 0.00012003, + # "amount": 60.80421562, + # "order1": + # {id: "CBRwP3ar4oMvvpUiGyfxc1syh41488SDi2GkrjuBDegv", + # "senderPublicKey": "DBXSHBz96NFsMu7xh4fi2eT9ZnyxefAHXsMxUayzgC6a", + # "matcherPublicKey": "9cpfKN9suPNvfeUNphzxXMjcnn974eme8ZhWUjaktzU5", + # "assetPair": [Object], + # "orderType": "buy", + # "price": 0.00012003, + # "sender": "3PJfFRgVuJ47UY4ckb74EGzEBzkHXtmG1LA", + # "amount": 60.80424773, + # "timestamp": "2020-06-09T19:34:51.885Z", + # "expiration": "2020-06-10T12:31:31.885Z", + # "matcherFee": 0.003, + # "signature": "4cA3ZAb3XAEEXaFG7caqpto5TRbpR5PkhZpxoNQZ9ZReNvjuJQs5a3THnumv7rcqmVUiVtuHAgk2f67ANcqtKyJ8", + # "matcherFeeAssetId": null}, + # "order2": + # {id: "CHJSLQ6dfSPs6gu2mAegrMUcRiDEDqaj2GKfvptMjS3M", + # "senderPublicKey": "3RUC4NGFZm9H8VJhSSjJyFLdiE42qNiUagDcZPwjgDf8", + # "matcherPublicKey": "9cpfKN9suPNvfeUNphzxXMjcnn974eme8ZhWUjaktzU5", + # "assetPair": [Object], + # "orderType": "sell", + # "price": 0.00012003, + # "sender": "3P9vKoQpMZtaSkHKpNh977YY9ZPzTuntLAq", + # "amount": 60.80424773, + # "timestamp": "2020-06-09T19:34:51.887Z", + # "expiration": "2020-06-10T12:31:31.887Z", + # "matcherFee": 0.003, + # "signature": "3SFyrcqzou2ddZyNisnLYaGhLt5qRjKxH8Nw3s4T5U7CEKGX9DDo8dS27RgThPVGbYF1rYET1FwrWoQ2UFZ6SMTR", + # "matcherFeeAssetId": null}}} + # + data = self.safe_value(trade, 'data') + datetime = self.safe_string(data, 'timestamp') + timestamp = self.parse8601(datetime) + id = self.safe_string(data, 'id') + priceString = self.safe_string(data, 'price') + amountString = self.safe_string(data, 'amount') + order1 = self.safe_value(data, 'order1') + order2 = self.safe_value(data, 'order2') + order = None + # at first, detect if response is from `fetch_my_trades` + if self.safe_string(order1, 'senderPublicKey') == self.apiKey: + order = order1 + elif self.safe_string(order2, 'senderPublicKey') == self.apiKey: + order = order2 + else: + # response is from `fetch_trades`, so find only taker order + date1 = self.safe_string(order1, 'timestamp') + date2 = self.safe_string(order2, 'timestamp') + ts1 = self.parse8601(date1) + ts2 = self.parse8601(date2) + if ts1 > ts2: + order = order1 + else: + order = order2 + symbol = None + assetPair = self.safe_value(order, 'assetPair') + if assetPair is not None: + symbol = self.get_symbol_from_asset_pair(assetPair) + elif market is not None: + symbol = market['symbol'] + side = self.safe_string(order, 'orderType') + orderId = self.safe_string(order, 'id') + fee = { + 'cost': self.safe_string(order, 'matcherFee'), + 'currency': self.safe_currency_code(self.safe_string(order, 'matcherFeeAssetId', 'WAVES')), + } + return self.safe_trade({ + 'info': trade, + 'timestamp': timestamp, + 'datetime': datetime, + 'symbol': symbol, + 'id': id, + 'order': orderId, + 'type': None, + 'side': side, + 'takerOrMaker': None, + 'price': priceString, + 'amount': amountString, + 'cost': None, + 'fee': fee, + }, market) + + def parse_deposit_withdraw_fees(self, response, codes: Strings = None, currencyIdKey=None) -> Any: + depositWithdrawFees: dict = {} + codes = self.market_codes(codes) + for i in range(0, len(response)): + entry = response[i] + dictionary = entry + currencyId = self.safe_string(dictionary, currencyIdKey) + currency = self.safe_value(self.currencies_by_id, currencyId) + code = self.safe_string(currency, 'code', currencyId) + if (codes is None) or (self.in_array(code, codes)): + depositWithdrawFee = self.safe_value(depositWithdrawFees, code) + if depositWithdrawFee is None: + depositWithdrawFee = { + 'info': [dictionary], + 'withdraw': { + 'fee': None, + 'percentage': None, + }, + 'deposit': { + 'fee': None, + 'percentage': None, + }, + 'networks': {}, + } + else: + depositWithdrawFee = depositWithdrawFees[code] + depositWithdrawFee['info'] = self.array_concat(depositWithdrawFee['info'], [dictionary]) + networkId = self.safe_string(dictionary, 'platform_id') + currencyCode = self.safe_string(currency, 'code') + networkCode = self.network_id_to_code(networkId, currencyCode) + network = self.safe_value(depositWithdrawFee['networks'], networkCode) + if network is None: + network = { + 'withdraw': { + 'fee': None, + 'percentage': None, + }, + 'deposit': { + 'fee': None, + 'percentage': None, + }, + } + feeType = self.safe_string(dictionary, 'type') + fees = self.safe_value(dictionary, 'fees') + networkKey = 'deposit' + if feeType == 'withdrawal_currency': + networkKey = 'withdraw' + network[networkKey] = {'fee': self.safe_number(fees, 'flat'), 'percentage': False} + depositWithdrawFee['networks'][networkCode] = network + depositWithdrawFees[code] = depositWithdrawFee + depositWithdrawFeesKeys = list(depositWithdrawFees.keys()) + for i in range(0, len(depositWithdrawFeesKeys)): + code = depositWithdrawFeesKeys[i] + entry = depositWithdrawFees[code] + networks = self.safe_value(entry, 'networks') + networkKeys = list(networks.keys()) + networkKeysLength = len(networkKeys) + if networkKeysLength == 1: + network = self.safe_value(networks, networkKeys[0]) + depositWithdrawFees[code]['withdraw'] = self.safe_value(network, 'withdraw') + depositWithdrawFees[code]['deposit'] = self.safe_value(network, 'deposit') + return depositWithdrawFees + + def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}): + """ + fetch deposit and withdraw fees + + https://docs.wx.network/en/api/gateways/deposit/currencies + https://docs.wx.network/en/api/gateways/withdraw/currencies + + :param str[]|None codes: list of unified currency codes + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `fee structures ` + """ + self.load_markets() + data = [] + promises = [] + promises.append(self.privateGetDepositCurrencies(params)) + promises.append(self.privateGetWithdrawCurrencies(params)) + promises = promises + # + # { + # "type": "list", + # "page_info": { + # "has_next_page": False, + # "last_cursor": null + # }, + # "items": [ + # { + # "type": "deposit_currency", + # "id": "WEST", + # "platform_id": "WEST", + # "waves_asset_id": "4LHHvYGNKJUg5hj65aGD5vgScvCBmLpdRFtjokvCjSL8", + # "platform_asset_id": "WEST", + # "decimals": 8, + # "status": "active", + # "allowed_amount": { + # "min": 0.1, + # "max": 2000000 + # }, + # "fees": { + # "flat": 0, + # "rate": 0 + # } + # }, + # ] + # } + # + # + # { + # "type": "list", + # "page_info": { + # "has_next_page": False, + # "last_cursor": null + # }, + # "items": [ + # { + # "type": "withdrawal_currency", + # "id": "BTC", + # "platform_id": "BTC", + # "waves_asset_id": "8LQW8f7P5d5PZM7GtZEBgaqRPGSzS3DfPuiXrURJ4AJS", + # "platform_asset_id": "BTC", + # "decimals": 8, + # "status": "inactive", + # "allowed_amount": { + # "min": 0.001, + # "max": 10 + # }, + # "fees": { + # "flat": 0.001, + # "rate": 0 + # } + # }, + # ] + # } + # + for i in range(0, len(promises)): + items = self.safe_value(promises[i], 'items') + data = self.array_concat(data, items) + return self.parse_deposit_withdraw_fees(data, codes, 'id') + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + errorCode = self.safe_string(response, 'error') + success = self.safe_bool(response, 'success', True) + Exception = self.safe_value(self.exceptions, errorCode) + if Exception is not None: + messageInner = self.safe_string(response, 'message') + raise Exception(self.id + ' ' + messageInner) + message = self.safe_string(response, 'message') + if message == 'Validation Error': + raise BadRequest(self.id + ' ' + body) + if not success: + raise ExchangeError(self.id + ' ' + body) + return None + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + # currently only works for BTC and WAVES + if code != 'WAVES': + supportedCurrencies = self.privateGetWithdrawCurrencies() + currencies: dict = {} + items = self.safe_value(supportedCurrencies, 'items', []) + for i in range(0, len(items)): + entry = items[i] + currencyCode = self.safe_string(entry, 'id') + currencies[currencyCode] = True + if not (code in currencies): + codes = list(currencies.keys()) + raise ExchangeError(self.id + ' withdraw() ' + code + ' not supported. Currency code must be one of ' + str(codes)) + self.load_markets() + hexChars = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'] + set: dict = {} + for i in range(0, len(hexChars)): + key = hexChars[i] + set[key] = True + isErc20 = True + noPrefix = self.remove0x_prefix(address) + lower = noPrefix.lower() + stringLength = len(lower) * 1 + for i in range(0, stringLength): + character = lower[i] + if not (character in set): + isErc20 = False + break + self.sign_in() + proxyAddress = None + if code == 'WAVES' and not isErc20: + proxyAddress = address + else: + withdrawAddressRequest: dict = { + 'address': address, + 'currency': code, + } + withdrawAddress = self.privateGetWithdrawAddressesCurrencyAddress(withdrawAddressRequest) + currencyInner = self.safe_value(withdrawAddress, 'currency') + allowedAmount = self.safe_value(currencyInner, 'allowed_amount') + minimum = self.safe_number(allowedAmount, 'min') + if amount <= minimum: + raise BadRequest(self.id + ' ' + code + ' withdraw failed, amount ' + str(amount) + ' must be greater than the minimum allowed amount of ' + str(minimum)) + # { + # "type": "withdrawal_addresses", + # "currency": { + # "type": "withdrawal_currency", + # "id": "BTC", + # "waves_asset_id": "8LQW8f7P5d5PZM7GtZEBgaqRPGSzS3DfPuiXrURJ4AJS", + # "decimals": 8, + # "status": "active", + # "allowed_amount": { + # "min": 0.001, + # "max": 20 + # }, + # "fees": { + # "flat": 0.001, + # "rate": 0 + # } + # }, + # "proxy_addresses": [ + # "3P3qqmkiLwNHB7x1FeoE8bvkRtULwGpo9ga" + # ] + # } + proxyAddresses = self.safe_value(withdrawAddress, 'proxy_addresses', []) + proxyAddress = self.safe_string(proxyAddresses, 0) + fee = self.safe_integer(self.options, 'withdrawFeeWAVES', 100000) # 0.001 WAVES + feeAssetId = 'WAVES' + type = 4 # transfer + version = 2 + amountInteger = self.to_real_currency_amount(code, amount) + currency = self.currency(code) + timestamp = self.milliseconds() + byteArray = [ + self.number_to_be(4, 1), + self.number_to_be(2, 1), + self.base58_to_binary(self.apiKey), + self.get_asset_bytes(currency['id']), + self.get_asset_bytes(feeAssetId), + self.number_to_be(timestamp, 8), + self.number_to_be(amountInteger, 8), + self.number_to_be(fee, 8), + self.base58_to_binary(proxyAddress), + self.number_to_be(0, 2), + ] + binary = self.binary_concat_array(byteArray) + hexSecret = self.binary_to_base16(self.base58_to_binary(self.secret)) + signature = self.axolotl(self.binary_to_base16(binary), hexSecret, 'ed25519') + request: dict = { + 'senderPublicKey': self.apiKey, + 'amount': amountInteger, + 'fee': fee, + 'type': type, + 'version': version, + 'attachment': '', + 'feeAssetId': self.get_asset_id(feeAssetId), + 'proofs': [ + signature, + ], + 'assetId': self.get_asset_id(currency['id']), + 'recipient': proxyAddress, + 'timestamp': timestamp, + 'signature': signature, + } + result = self.nodePostTransactionsBroadcast(request) + # + # { + # "id": "string", + # "signature": "string", + # "fee": 0, + # "timestamp": 1460678400000, + # "recipient": "3P274YB5qseSE9DTTL3bpSjosZrYBPDpJ8k", + # "amount": 0 + # } + # + return self.parse_transaction(result, currency) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # withdraw + # + # { + # "id": "string", + # "signature": "string", + # "fee": 0, + # "timestamp": 1460678400000, + # "recipient": "3P274YB5qseSE9DTTL3bpSjosZrYBPDpJ8k", + # "amount": 0 + # } + # + # withdraw new: + # { + # type: "4", + # id: "2xnWTqG9ar7jEDrLxfbVyyspPZ6XZNrrw9ai9sQ81Eya", + # fee: "100000", + # feeAssetId: null, + # timestamp: "1715786263807", + # version: "2", + # sender: "3P81LLX1kk2CSJC9L8C2enxdHB7XvnSGAEE", + # senderPublicKey: "DdmzmXf9mty1FBE8AdVGnrncVLEAzP4gR4nWoTFAJoXz", + # proofs: ["RyoKwdSYv3EqotJCYftfFM9JE2j1ZpDRxKwYfiRhLAFeyNp6VfJUXNDS884XfeCeHeNypNmTCZt5NYR1ekyjCX3",], + # recipient: "3P9tXxu38a8tgewNEKFzourVxeqHd11ppOc", + # assetId: null, + # feeAsset: null, + # amount: "2000000", + # attachment: "", + # } + # + currency = self.safe_currency(None, currency) + code = currency['code'] + typeRaw = self.safe_string(transaction, 'type') + type = 'withdraw' if (typeRaw == '4') else 'deposit' + amount = self.parse_number(self.from_real_currency_amount(code, self.safe_string(transaction, 'amount'))) + feeString = self.safe_string(transaction, 'fee') + feeAssetId = self.safe_string(transaction, 'feeAssetId', 'WAVES') + feeCode = self.safe_currency_code(feeAssetId) + feeAmount = self.parse_number(self.from_real_currency_amount(feeCode, feeString)) + timestamp = self.safe_integer(transaction, 'timestamp') + return { + 'id': self.safe_string(transaction, 'id'), + 'txid': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': None, + 'addressFrom': self.safe_string(transaction, 'sender'), + 'address': None, + 'addressTo': self.safe_string(transaction, 'recipient'), + 'amount': amount, + 'type': type, + 'currency': currency['code'], + 'status': None, + 'updated': None, + 'tagFrom': None, + 'tag': None, + 'tagTo': None, + 'comment': None, + 'internal': None, + 'fee': { + 'currency': feeCode, + 'cost': feeAmount, + }, + 'info': transaction, + } diff --git a/ccxt/whitebit.py b/ccxt/whitebit.py new file mode 100644 index 0000000..1019ea7 --- /dev/null +++ b/ccxt/whitebit.py @@ -0,0 +1,3816 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.whitebit import ImplicitAPI +import asyncio +import hashlib +from ccxt.base.types import Account, Any, Balances, BorrowInterest, Bool, Conversion, CrossBorrowRate, Currencies, Currency, DepositAddress, FundingHistory, Int, Market, MarketType, Num, Order, OrderBook, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, FundingRates, Trade, TradingFees, Transaction, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import NotSupported +from ccxt.base.errors import DDoSProtection +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class whitebit(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(whitebit, self).describe(), { + 'id': 'whitebit', + 'name': 'WhiteBit', + 'version': 'v4', + 'countries': ['EE'], + 'rateLimit': 50, + 'pro': True, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': True, + 'swap': True, + 'future': False, + 'option': False, + 'cancelAllOrders': True, + 'cancelAllOrdersAfter': True, + 'cancelOrder': True, + 'cancelOrders': False, + 'createConvertTrade': True, + 'createDepositAddress': True, + 'createMarketBuyOrderWithCost': True, + 'createMarketOrderWithCost': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createPostOnlyOrder': True, + 'createStopLimitOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'createTriggerOrder': True, + 'editOrder': True, + 'fetchAccounts': True, + 'fetchBalance': True, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchClosedOrders': True, + 'fetchConvertQuote': True, + 'fetchConvertTrade': False, + 'fetchConvertTradeHistory': True, + 'fetchCrossBorrowRate': True, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': True, + 'fetchDeposit': True, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchDepositsWithdrawals': True, + 'fetchDepositWithdrawFee': 'emulated', + 'fetchDepositWithdrawFees': True, + 'fetchFundingHistory': True, + 'fetchFundingLimits': True, + 'fetchFundingRate': True, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': True, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchLedger': False, + 'fetchMarginMode': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterestHistory': False, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchOrderTrades': True, + 'fetchPosition': True, + 'fetchPositionHistory': True, + 'fetchPositionMode': False, + 'fetchPositions': True, + 'fetchPremiumIndexOHLCV': False, + 'fetchStatus': True, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': True, + 'fetchTradingLimits': True, + 'fetchTransactionFees': True, + 'fetchTransactions': True, + 'fetchWithdrawals': True, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'setLeverage': True, + 'transfer': True, + 'withdraw': True, + }, + 'timeframes': { + '1m': '1m', + '3m': '3m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1h', + '2h': '2h', + '4h': '4h', + '6h': '6h', + '8h': '8h', + '12h': '12h', + '1d': '1d', + '3d': '3d', + '1w': '1w', + '1M': '1M', + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/66732963-8eb7dd00-ee66-11e9-849b-10d9282bb9e0.jpg', + 'api': { + 'v1': { + 'public': 'https://whitebit.com/api/v1/public', + 'private': 'https://whitebit.com/api/v1', + }, + 'v2': { + 'public': 'https://whitebit.com/api/v2/public', + }, + 'v4': { + 'public': 'https://whitebit.com/api/v4/public', + 'private': 'https://whitebit.com/api/v4', + }, + }, + 'www': 'https://www.whitebit.com', + 'doc': 'https://github.com/whitebit-exchange/api-docs', + 'fees': 'https://whitebit.com/fee-schedule', + 'referral': 'https://whitebit.com/referral/d9bdf40e-28f2-4b52-b2f9-cd1415d82963', + }, + 'api': { + 'web': { + 'get': [ + 'v1/healthcheck', + ], + }, + 'v1': { + 'public': { + 'get': [ + 'markets', + 'tickers', + 'ticker', + 'symbols', + 'depth/result', + 'history', + 'kline', + ], + }, + 'private': { + 'post': [ + 'account/balance', + 'order/new', + 'order/cancel', + 'orders', + 'account/order_history', + 'account/executed_history', + 'account/executed_history/all', + 'account/order', + ], + }, + }, + 'v2': { + 'public': { + 'get': [ + 'markets', + 'ticker', + 'assets', + 'fee', + 'depth/{market}', + 'trades/{market}', + ], + }, + }, + 'v4': { + 'public': { + 'get': [ + 'assets', + 'collateral/markets', + 'fee', + 'orderbook/depth/{market}', + 'orderbook/{market}', + 'ticker', + 'trades/{market}', + 'time', + 'ping', + 'markets', + 'futures', + 'platform/status', + 'mining-pool', + ], + }, + 'private': { + 'post': [ + 'collateral-account/balance', + 'collateral-account/balance-summary', + 'collateral-account/positions/history', + 'collateral-account/leverage', + 'collateral-account/positions/open', + 'collateral-account/summary', + 'collateral-account/funding-history', + 'main-account/address', + 'main-account/balance', + 'main-account/create-new-address', + 'main-account/codes', + 'main-account/codes/apply', + 'main-account/codes/my', + 'main-account/codes/history', + 'main-account/fiat-deposit-url', + 'main-account/history', + 'main-account/withdraw', + 'main-account/withdraw-pay', + 'main-account/transfer', + 'main-account/smart/plans', + 'main-account/smart/investment', + 'main-account/smart/investment/close', + 'main-account/smart/investments', + 'main-account/fee', + 'main-account/smart/interest-payment-history', + 'trade-account/balance', + 'trade-account/executed-history', + 'trade-account/order/history', + 'trade-account/order', + 'order/collateral/limit', + 'order/collateral/market', + 'order/collateral/stop-limit', + 'order/collateral/trigger-market', + 'order/collateral/bulk', + 'order/new', + 'order/market', + 'order/stock_market', + 'order/stop_limit', + 'order/stop_market', + 'order/cancel', + 'order/cancel/all', + 'order/kill-switch', + 'order/kill-switch/status', + 'order/bulk', + 'order/modify', + 'order/conditional-cancel', + 'orders', + 'oco-orders', + 'order/collateral/oco', + 'order/oco-cancel', + 'order/oto-cancel', + 'profile/websocket_token', + 'convert/estimate', + 'convert/confirm', + 'convert/history', + 'sub-account/create', + 'sub-account/delete', + 'sub-account/edit', + 'sub-account/list', + 'sub-account/transfer', + 'sub-account/block', + 'sub-account/unblock', + 'sub-account/balances', + 'sub-account/transfer/history', + 'sub-account/api-key/create', + 'sub-account/api-key/edit', + 'sub-account/api-key/delete', + 'sub-account/api-key/list', + 'sub-account/api-key/reset', + 'sub-account/api-key/ip-address/list', + 'sub-account/api-key/ip-address/create', + 'sub-account/api-key/ip-address/delete', + 'mining/rewards', + 'market/fee', + 'conditional-orders', + ], + }, + }, + }, + 'fees': { + 'trading': { + 'tierBased': False, + 'percentage': True, + 'taker': self.parse_number('0.001'), + 'maker': self.parse_number('0.001'), + }, + }, + 'options': { + 'timeDifference': 0, # the difference between system clock and exchange clock + 'adjustForTimeDifference': False, # controls the adjustment logic upon instantiation + 'fiatCurrencies': ['EUR', 'USD', 'RUB', 'UAH'], + 'fetchBalance': { + 'account': 'spot', + }, + 'accountsByType': { + 'funding': 'main', + 'main': 'main', + 'spot': 'spot', + 'margin': 'collateral', + 'trade': 'spot', + }, + 'networksById': { + 'BEP20': 'BSC', + }, + 'defaultType': 'spot', + 'brokerId': 'ccxt', + }, + 'features': { + 'default': { + 'sandbox': False, + 'createOrder': { + 'marginMode': True, + 'triggerPrice': True, + 'triggerDirection': False, + 'triggerPriceType': None, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, # todo + 'FOK': False, + 'PO': True, # todo + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 100, + 'daysBack': None, + 'untilDays': None, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'checkActive': True, + 'checkExecuted': True, + 'symbolRequired': False, + 'marginMode': False, + 'trigger': False, + 'trailing': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 100, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': None, + 'untilDays': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': None, + 'daysBackCanceled': None, + 'untilDays': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 1440, + }, + 'fetchWithdrawals': { + 'marginMode': False, + 'limit': 100, + 'daysBack': None, + 'untilDays': None, + 'symbolRequired': False, + }, + }, + 'spot': { + 'extends': 'default', + }, + 'swap': { + 'linear': { + 'extends': 'default', + }, + 'inverse': { + 'extends': 'default', + }, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'exact': { + 'Unauthorized request.': AuthenticationError, # {"code":10,"message":"Unauthorized request."} + 'The market format is invalid.': BadSymbol, # {"code":0,"message":"Validation failed","errors":{"market":["The market format is invalid."]}} + 'Market is not available': BadSymbol, # {"success":false,"message":{"market":["Market is not available"]},"result":[]} + 'Invalid payload.': BadRequest, # {"code":9,"message":"Invalid payload."} + 'Amount must be greater than 0': InvalidOrder, # {"code":0,"message":"Validation failed","errors":{"amount":["Amount must be greater than 0"]}} + 'Not enough balance.': InsufficientFunds, # {"code":10,"message":"Inner validation failed","errors":{"amount":["Not enough balance."]}} + 'The order id field is required.': InvalidOrder, # {"code":0,"message":"Validation failed","errors":{"orderId":["The order id field is required."]}} + 'Not enough balance': InsufficientFunds, # {"code":0,"message":"Validation failed","errors":{"amount":["Not enough balance"]}} + 'This action is unauthorized.': PermissionDenied, # {"code":0,"message":"This action is unauthorized."} + 'This API Key is not authorized to perform self action.': PermissionDenied, # {"code":4,"message":"This API Key is not authorized to perform self action."} + 'Unexecuted order was not found.': OrderNotFound, # {"code":2,"message":"Inner validation failed","errors":{"order_id":["Unexecuted order was not found."]}} + 'The selected from is invalid.': BadRequest, # {"code":0,"message":"Validation failed","errors":{"from":["The selected from is invalid."]}} + '503': ExchangeNotAvailable, # {"response":null,"status":503,"errors":{"message":[""]},"notification":null,"warning":null,"_token":null}, + '422': OrderNotFound, # {"response":null,"status":422,"errors":{"orderId":["Finished order id 1295772653 not found on your account"]},"notification":null,"warning":"Finished order id 1295772653 not found on your account","_token":null} + }, + 'broad': { + 'This action is unauthorized': PermissionDenied, # {"code":2,"message":"This action is unauthorized. Enable your key in API settings"} + 'Given amount is less than min amount': InvalidOrder, # {"code":0,"message":"Validation failed","errors":{"amount":["Given amount is less than min amount 200000"],"total":["Total is less than 5.05"]}} + 'Min amount step': InvalidOrder, # {"code":32,"errors":{"amount":["Min amount step = 0.01"]},"message":"Validation failed"} + 'Total is less than': InvalidOrder, # {"code":0,"message":"Validation failed","errors":{"amount":["Given amount is less than min amount 200000"],"total":["Total is less than 5.05"]}} + 'fee must be no less than': InvalidOrder, # {"code":0,"message":"Validation failed","errors":{"amount":["Total amount + fee must be no less than 5.05505"]}} + 'Enable your key in API settings': PermissionDenied, # {"code":2,"message":"This action is unauthorized. Enable your key in API settings"} + 'You don\'t have such amount for transfer': InsufficientFunds, # {"code":3,"message":"Inner validation failed","errors":{"amount":["You don't have such amount for transfer(available 0.44523433, in amount: 2)"]}} + }, + }, + }) + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for whitebit + + https://docs.whitebit.com/public/http-v4/#market-info + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + if self.options['adjustForTimeDifference']: + self.load_time_difference() + markets = self.v4PublicGetMarkets() + # + # [ + # { + # "name": "SON_USD", # Market pair name + # "stock": "SON", # Ticker of stock currency + # "money": "USD", # Ticker of money currency + # "stockPrec": "3", # Stock currency precision + # "moneyPrec": "2", # Precision of money currency + # "feePrec": "4", # Fee precision + # "makerFee": "0.1", # Default maker fee ratio + # "takerFee": "0.1", # Default taker fee ratio + # "minAmount": "0.001", # Minimal amount of stock to trade + # "minTotal": "0.001", # Minimal amount of money to trade + # "tradesEnabled": True, # Is trading enabled + # "isCollateral": True, # Is margin trading enabled + # "type": "spot", # Market type. Possible values: "spot", "futures" + # "maxTotal": "1000000000" # Maximum total(amount * price) of money to trade + # }, + # { + # ... + # } + # ] + # + return self.parse_markets(markets) + + def parse_market(self, market: dict) -> Market: + id = self.safe_string(market, 'name') + baseId = self.safe_string(market, 'stock') + quoteId = self.safe_string(market, 'money') + quoteId = 'USDT' if (quoteId == 'PERP') else quoteId + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + active = self.safe_value(market, 'tradesEnabled') + isCollateral = self.safe_value(market, 'isCollateral') + typeId = self.safe_string(market, 'type') + type: MarketType + settle: Str = None + settleId: Str = None + symbol = base + '/' + quote + swap = typeId == 'futures' + margin = isCollateral and not swap + contract = False + amountPrecision = self.parse_number(self.parse_precision(self.safe_string(market, 'stockPrec'))) + contractSize = amountPrecision + linear: Bool = None + inverse: Bool = None + if swap: + settleId = quoteId + settle = self.safe_currency_code(settleId) + symbol = symbol + ':' + settle + type = 'swap' + contract = True + linear = True + inverse = False + else: + type = 'spot' + takerFeeRate = self.safe_string(market, 'takerFee') + taker = Precise.string_div(takerFeeRate, '100') + makerFeeRate = self.safe_string(market, 'makerFee') + maker = Precise.string_div(makerFeeRate, '100') + return { + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': type, + 'spot': not swap, + 'margin': margin, + 'swap': swap, + 'future': False, + 'option': False, + 'active': active, + 'contract': contract, + 'linear': linear, + 'inverse': inverse, + 'taker': self.parse_number(taker), + 'maker': self.parse_number(maker), + 'contractSize': contractSize, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': amountPrecision, + 'price': self.parse_number(self.parse_precision(self.safe_string(market, 'moneyPrec'))), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(market, 'minAmount'), + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': self.safe_number(market, 'minTotal'), + 'max': self.safe_number(market, 'maxTotal'), + }, + }, + 'created': None, + 'info': market, + } + + def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://docs.whitebit.com/public/http-v4/#asset-status-list + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + response = self.v4PublicGetAssets(params) + # + # { + # BTC: { + # name: "Bitcoin", + # unified_cryptoasset_id: "1", + # can_withdraw: True, + # can_deposit: True, + # min_withdraw: "0.0003", + # max_withdraw: "0", + # maker_fee: "0.1", + # taker_fee: "0.1", + # min_deposit: "0.0001", + # max_deposit: "0", + # networks: { + # deposits: ["BTC",], + # withdraws: ["BTC",], + # default: "BTC", + # }, + # confirmations: { + # BTC: "2", + # }, + # limits: { + # deposit: { + # BTC: {min: "0.0001",}, + # }, + # withdraw: { + # BTC: {min: "0.0003",}, + # }, + # }, + # currency_precision: "8", + # is_memo: False, + # }, + # USD: { + # name: "United States Dollar", + # unified_cryptoasset_id: "6955", + # can_withdraw: True, + # can_deposit: True, + # min_withdraw: "10", + # max_withdraw: "10000", + # maker_fee: "0.1", + # taker_fee: "0.1", + # min_deposit: "10", + # max_deposit: "10000", + # networks: { + # deposits: ["USD",], + # withdraws: ["USD",], + # default: "USD", + # }, + # providers: { + # deposits: ["ADVCASH",], + # withdraws: ["ADVCASH",], + # }, + # limits: { + # deposit: { + # USD: { max: "10000", min: "10",}, + # }, + # withdraw: { + # USD: {max: "10000", min: "10",}, + # }, + # }, + # currency_precision: "2", + # is_memo: False, + # } + # } + # + ids = list(response.keys()) + result: dict = {} + for i in range(0, len(ids)): + id = ids[i] + currency = response[id] + # name = self.safe_string(currency, 'name') # breaks down in Python due to utf8 encoding issues on the exchange side + code = self.safe_currency_code(id) + hasProvider = ('providers' in currency) + networks = {} + rawNetworks = self.safe_dict(currency, 'networks', {}) + depositsNetworks = self.safe_list(rawNetworks, 'deposits', []) + withdrawsNetworks = self.safe_list(rawNetworks, 'withdraws', []) + networkLimits = self.safe_dict(currency, 'limits', {}) + depositLimits = self.safe_dict(networkLimits, 'deposit', {}) + withdrawLimits = self.safe_dict(networkLimits, 'withdraw', {}) + allNetworks = self.array_concat(depositsNetworks, withdrawsNetworks) + for j in range(0, len(allNetworks)): + networkId = allNetworks[j] + networkCode = self.network_id_to_code(networkId) + networks[networkCode] = { + 'id': networkId, + 'network': networkCode, + 'active': None, + 'deposit': self.in_array(networkId, depositsNetworks), + 'withdraw': self.in_array(networkId, withdrawsNetworks), + 'fee': None, + 'precision': None, + 'limits': { + 'deposit': { + 'min': self.safe_number(depositLimits, 'min', None), + 'max': self.safe_number(depositLimits, 'max', None), + }, + 'withdraw': { + 'min': self.safe_number(withdrawLimits, 'min', None), + 'max': self.safe_number(withdrawLimits, 'max', None), + }, + }, + } + result[code] = self.safe_currency_structure({ + 'id': id, + 'code': code, + 'info': currency, # the original payload + 'name': None, # see the comment above + 'active': None, + 'deposit': self.safe_bool(currency, 'can_deposit'), + 'withdraw': self.safe_bool(currency, 'can_withdraw'), + 'fee': None, + 'networks': None, # todo + 'type': 'fiat' if hasProvider else 'crypto', + 'precision': self.parse_number(self.parse_precision(self.safe_string(currency, 'currency_precision'))), + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': self.safe_number(currency, 'min_withdraw'), + 'max': self.safe_number(currency, 'max_withdraw'), + }, + 'deposit': { + 'min': self.safe_number(currency, 'min_deposit'), + 'max': self.safe_number(currency, 'max_deposit'), + }, + }, + }) + return result + + def fetch_transaction_fees(self, codes: Strings = None, params={}): + """ + @deprecated + please use fetchDepositWithdrawFees instead + + https://docs.whitebit.com/public/http-v4/#fee + + :param str[]|None codes: not used by fetchTransactionFees() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `fee structures ` + """ + self.load_markets() + response = self.v4PublicGetFee(params) + # + # { + # "1INCH":{ + # "is_depositable":true, + # "is_withdrawal":true, + # "ticker":"1INCH", + # "name":"1inch", + # "providers":[ + # ], + # "withdraw":{ + # "max_amount":"0", + # "min_amount":"21.5", + # "fixed":"17.5", + # "flex":null + # }, + # "deposit":{ + # "max_amount":"0", + # "min_amount":"19.5", + # "fixed":null, + # "flex":null + # } + # }, + # {...} + # } + # + currenciesIds = list(response.keys()) + withdrawFees: dict = {} + depositFees: dict = {} + for i in range(0, len(currenciesIds)): + currency = currenciesIds[i] + data = response[currency] + code = self.safe_currency_code(currency) + withdraw = self.safe_value(data, 'withdraw', {}) + withdrawFees[code] = self.safe_string(withdraw, 'fixed') + deposit = self.safe_value(data, 'deposit', {}) + depositFees[code] = self.safe_string(deposit, 'fixed') + return { + 'withdraw': withdrawFees, + 'deposit': depositFees, + 'info': response, + } + + def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}): + """ + fetch deposit and withdraw fees + + https://docs.whitebit.com/public/http-v4/#fee + + :param str[]|None codes: not used by fetchDepositWithdrawFees() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `fee structures ` + """ + self.load_markets() + response = self.v4PublicGetFee(params) + # + # { + # "1INCH": { + # "is_depositable": True, + # "is_withdrawal": True, + # "ticker": "1INCH", + # "name": "1inch", + # "providers": [], + # "withdraw": { + # "max_amount": "0", + # "min_amount": "21.5", + # "fixed": "17.5", + # "flex": null + # }, + # "deposit": { + # "max_amount": "0", + # "min_amount": "19.5", + # "fixed": null, + # "flex": null + # } + # }, + # "WBT(ERC20)": { + # "is_depositable": True, + # "is_withdrawal": True, + # "ticker": "WBT", + # "name": "WhiteBIT Token", + # "providers": [], + # "withdraw": {max_amount: "0", min_amount: '0.7', fixed: "0.253", flex: null}, + # "deposit": {max_amount: "0", min_amount: "0.35", fixed: null, flex: null} + # }, + # "WBT(TRC20)": { + # "is_depositable": True, + # "is_withdrawal": True, + # "ticker": "WBT", + # "name": "WhiteBIT Token", + # "providers": [], + # "withdraw": {max_amount: "0", min_amount: "1.5", fixed: "0.075", flex: null}, + # "deposit": {max_amount: "0", min_amount: "0.75", fixed: null, flex: null} + # }, + # ... + # } + # + return self.parse_deposit_withdraw_fees(response, codes) + + def parse_deposit_withdraw_fees(self, response, codes=None, currencyIdKey=None): + # + # { + # "1INCH": { + # "is_depositable": True, + # "is_withdrawal": True, + # "ticker": "1INCH", + # "name": "1inch", + # "providers": [], + # "withdraw": { + # "max_amount": "0", + # "min_amount": "21.5", + # "fixed": "17.5", + # "flex": null + # }, + # "deposit": { + # "max_amount": "0", + # "min_amount": "19.5", + # "fixed": null, + # "flex": null + # } + # }, + # "WBT(ERC20)": { + # "is_depositable": True, + # "is_withdrawal": True, + # "ticker": "WBT", + # "name": "WhiteBIT Token", + # "providers": [], + # "withdraw": {max_amount: "0", min_amount: "0.7", fixed: "0.253", flex: null}, + # "deposit": {max_amount: "0", min_amount: "0.35", fixed: null, flex: null} + # }, + # "WBT(TRC20)": { + # "is_depositable": True, + # "is_withdrawal": True, + # "ticker": "WBT", + # "name": "WhiteBIT Token", + # "providers": [], + # "withdraw": {max_amount: "0", min_amount: "1.5", fixed: "0.075", flex: null}, + # "deposit": {max_amount: "0", min_amount: "0.75", fixed: null, flex: null} + # }, + # ... + # } + # + depositWithdrawFees: dict = {} + codes = self.market_codes(codes) + currencyIds = list(response.keys()) + for i in range(0, len(currencyIds)): + entry = currencyIds[i] + splitEntry = entry.split(' ') + currencyId = splitEntry[0] + feeInfo = response[entry] + code = self.safe_currency_code(currencyId) + if (codes is None) or (self.in_array(code, codes)): + depositWithdrawFee = self.safe_value(depositWithdrawFees, code) + if depositWithdrawFee is None: + depositWithdrawFees[code] = self.deposit_withdraw_fee({}) + depositWithdrawFees[code]['info'][entry] = feeInfo + networkId = self.safe_string(splitEntry, 1) + withdraw = self.safe_value(feeInfo, 'withdraw') + deposit = self.safe_value(feeInfo, 'deposit') + withdrawFee = self.safe_number(withdraw, 'fixed') + depositFee = self.safe_number(deposit, 'fixed') + withdrawResult: dict = { + 'fee': withdrawFee, + 'percentage': False if (withdrawFee is not None) else None, + } + depositResult: dict = { + 'fee': depositFee, + 'percentage': False if (depositFee is not None) else None, + } + if networkId is not None: + networkLength = len(networkId) + networkId = networkId[1:networkLength - 1] + networkCode = self.network_id_to_code(networkId) + depositWithdrawFees[code]['networks'][networkCode] = { + 'withdraw': withdrawResult, + 'deposit': depositResult, + } + else: + depositWithdrawFees[code]['withdraw'] = withdrawResult + depositWithdrawFees[code]['deposit'] = depositResult + depositWithdrawCodes = list(depositWithdrawFees.keys()) + for i in range(0, len(depositWithdrawCodes)): + code = depositWithdrawCodes[i] + currency = self.currency(code) + depositWithdrawFees[code] = self.assign_default_deposit_withdraw_fees(depositWithdrawFees[code], currency) + return depositWithdrawFees + + def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://docs.whitebit.com/public/http-v4/#asset-status-list + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + self.load_markets() + response = self.v4PublicGetAssets(params) + # + # { + # "1INCH": { + # "name": "1inch", + # "unified_cryptoasset_id": "8104", + # "can_withdraw": True, + # "can_deposit": True, + # "min_withdraw": "33", + # "max_withdraw": "0", + # "maker_fee": "0.1", + # "taker_fee": "0.1", + # "min_deposit": "30", + # "max_deposit": "0" + # }, + # ... + # } + # + result: dict = {} + for i in range(0, len(self.symbols)): + symbol = self.symbols[i] + market = self.market(symbol) + fee = self.safe_value(response, market['baseId'], {}) + makerFee = self.safe_string(fee, 'maker_fee') + takerFee = self.safe_string(fee, 'taker_fee') + makerFee = Precise.string_div(makerFee, '100') + takerFee = Precise.string_div(takerFee, '100') + result[symbol] = { + 'info': fee, + 'symbol': market['symbol'], + 'percentage': True, + 'tierBased': False, + 'maker': self.parse_number(makerFee), + 'taker': self.parse_number(takerFee), + } + return result + + def fetch_trading_limits(self, symbols: Strings = None, params={}) -> Any: + """ + fetch the trading limits for a market + + https://docs.whitebit.com/public/http-v4/#market-info + + :param str[]|None symbols: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `trading limits structure ` + """ + self.load_markets() + # + # Trading limits are derived from market information already loaded by loadMarkets() + # Market structure includes: + # { + # "id": "BTC_USDT", # Market ID + # "symbol": "BTC/USDT", # Unified symbol + # "base": "BTC", # Base currency + # "quote": "USDT", # Quote currency + # "active": True, # Market active status + # "type": "spot", # Market type + # "spot": True, # Spot trading enabled + # "margin": False, # Margin trading enabled + # "future": False, # Futures trading enabled + # "option": False, # Options trading enabled + # "contract": False, # Contract trading enabled + # "settle": None, # Settlement currency + # "settleId": None, # Settlement currency ID + # "contractSize": None, # Contract size + # "linear": None, # Linear contract + # "inverse": None, # Inverse contract + # "limits": { # Trading limits + # "amount": { # Amount limits + # "min": 0.00001, # Minimum amount + # "max": 1000000 # Maximum amount + # }, + # "price": { # Price limits + # "min": 0.01, # Minimum price + # "max": 1000000 # Maximum price + # }, + # "cost": { # Cost limits + # "min": 5.0, # Minimum cost + # "max": 10000000 # Maximum cost + # } + # }, + # "precision": { # Precision settings + # "amount": 5, # Amount precision + # "price": 2 # Price precision + # }, + # "taker": 0.001, # Taker fee + # "maker": 0.001, # Maker fee + # "percentage": True, # Fee percentage + # "tierBased": False # Tier-based fees + # } + # + result: dict = {} + # Process all markets from the loaded markets cache + marketIds = list(self.markets.keys()) + for i in range(0, len(marketIds)): + marketId = marketIds[i] + market = self.markets[marketId] + if not market or not market['symbol']: + continue # Skip invalid markets silently + symbol = market['symbol'] + # Filter by symbols if specified + if symbols: + symbolFound = False + for j in range(0, len(symbols)): + if symbols[j] == symbol: + symbolFound = True + break + if not symbolFound: + continue # Skip symbols not in requested list + # Extract trading limits + limits = self.safe_dict(market, 'limits') + amountLimits = self.safe_dict(limits, 'amount') + priceLimits = self.safe_dict(limits, 'price') + costLimits = self.safe_dict(limits, 'cost') + # Validate that all required limits exist and are valid numbers + hasAmountLimits = amountLimits and self.safe_number(amountLimits, 'min') is not None and self.safe_number(amountLimits, 'max') is not None + hasPriceLimits = priceLimits and self.safe_number(priceLimits, 'min') is not None and self.safe_number(priceLimits, 'max') is not None + hasCostLimits = costLimits and self.safe_number(costLimits, 'min') is not None and self.safe_number(costLimits, 'max') is not None + if hasAmountLimits and hasPriceLimits and hasCostLimits: + result[symbol] = { + 'info': market, + 'limits': { + 'amount': { + 'min': self.safe_number(amountLimits, 'min'), + 'max': self.safe_number(amountLimits, 'max'), + }, + 'price': { + 'min': self.safe_number(priceLimits, 'min'), + 'max': self.safe_number(priceLimits, 'max'), + }, + 'cost': { + 'min': self.safe_number(costLimits, 'min'), + 'max': self.safe_number(costLimits, 'max'), + }, + }, + } + return result + + def fetch_funding_limits(self, codes: Strings = None, params={}): + """ + fetch the deposit and withdrawal limits for a currency + + https://docs.whitebit.com/public/http-v4/#asset-status-list + https://docs.whitebit.com/public/http-v4/#fee + + :param str[]|None codes: unified currency codes + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding limits structure ` + """ + self.load_markets() + # Fetch both currencies and fees data for comprehensive funding limits + currenciesData, feesData = asyncio.gather(*[ + self.fetch_currencies(), + self.v4PublicGetFee(params), + ]) + # + # Currencies response structure(from fetchCurrencies): + # { + # "BTC": { + # "id": "BTC", # Currency ID + # "code": "BTC", # Currency code + # "name": "Bitcoin", # Currency name + # "active": True, # Currency active status + # "type": "crypto", # Currency type + # "precision": 8, # Currency precision + # "limits": { # Currency limits + # "deposit": { # Deposit limits + # "min": 0.00001, # Minimum deposit + # "max": 1000000 # Maximum deposit + # }, + # "withdraw": { # Withdrawal limits + # "min": 0.00001, # Minimum withdrawal + # "max": 1000000 # Maximum withdrawal + # } + # }, + # "networks": { # Network-specific limits + # "BTC": { + # "limits": { + # "deposit": {"min": "0.001"}, + # "withdraw": {"min": "0.002"} + # } + # } + # }, + # "info": {...} # Original API response + # } + # } + # + # Fees response structure(from /api/v4/public/fee): + # { + # "USDT(ERC20)": { + # "ticker": "USDT", + # "name": "Tether US", + # "deposit": { + # "min_amount": "0.0005", + # "max_amount": "0.1", + # "fixed": "0.0005", + # "flex": { + # "min_fee": "100", + # "max_fee": "1000", + # "percent": "10" + # } + # }, + # "withdraw": { + # "min_amount": "0.001", + # "max_amount": "0", + # "fixed": null, + # "flex": null + # } + # } + # } + # + result: dict = {} + currencyKeys = list(currenciesData.keys()) + for i in range(0, len(currencyKeys)): + code = currencyKeys[i] + currency = currenciesData[code] + if not currency: + # Skip invalid currency silently + continue + if codes is not None and not self.in_array(code, codes): + # Skip currency not in requested list silently + continue + # Find corresponding fee data for self currency + feeData = None + feeKeys = list(feesData.keys()) + for j in range(0, len(feeKeys)): + feeKey = feeKeys[j] + fee = feesData[feeKey] + if fee and fee['ticker'] == code: + feeData = fee + break + # Build comprehensive funding limits + limits: dict = { + 'deposit': { + 'min': currency['limits']['deposit']['min'], + 'max': currency['limits']['deposit']['max'], + }, + 'withdraw': { + 'min': currency['limits']['withdraw']['min'], + 'max': currency['limits']['withdraw']['max'], + }, + } + # Add fee information if available + if feeData: + depositFee = feeData['deposit'] + withdrawFee = feeData['withdraw'] + if depositFee: + depositFeeData = { + 'fixed': self.safe_number(depositFee, 'fixed'), + } + if depositFee['flex']: + depositFeeData['flex'] = { + 'min': self.safe_number(depositFee['flex'], 'min_fee'), + 'max': self.safe_number(depositFee['flex'], 'max_fee'), + 'percent': self.safe_number(depositFee['flex'], 'percent'), + } + limits['deposit']['fee'] = depositFeeData + if withdrawFee: + withdrawFeeData = { + 'fixed': self.safe_number(withdrawFee, 'fixed'), + } + if withdrawFee['flex']: + withdrawFeeData['flex'] = { + 'min': self.safe_number(withdrawFee['flex'], 'min_fee'), + 'max': self.safe_number(withdrawFee['flex'], 'max_fee'), + 'percent': self.safe_number(withdrawFee['flex'], 'percent'), + } + limits['withdraw']['fee'] = withdrawFeeData + # Add network-specific limits if available + if currency['networks']: + limits['networks'] = currency['networks'] + result[code] = { + 'info': currency, + 'limits': limits, + } + return result + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://docs.whitebit.com/public/http-v4/#market-activity + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + response = self.v1PublicGetTicker(self.extend(request, params)) + # + # { + # "success":true, + # "message":"", + # "result": { + # "bid":"0.021979", + # "ask":"0.021996", + # "open":"0.02182", + # "high":"0.022039", + # "low":"0.02161", + # "last":"0.021987", + # "volume":"2810.267", + # "deal":"61.383565474", + # "change":"0.76", + # }, + # } + # + ticker = self.safe_dict(response, 'result', {}) + return self.parse_ticker(ticker, market) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # FetchTicker(v1) + # + # { + # "bid": "0.021979", + # "ask": "0.021996", + # "open": "0.02182", + # "high": "0.022039", + # "low": "0.02161", + # "last": "0.021987", + # "volume": "2810.267", + # "deal": "61.383565474", + # "change": "0.76", + # } + # + # FetchTickers(v4) + # + # "BCH_RUB": { + # "base_id": 1831, + # "quote_id": 0, + # "last_price": "32830.21", + # "quote_volume": "1494659.8024096", + # "base_volume": "46.1083", + # "isFrozen": False, + # "change": "2.12" # in percent + # } + # + # WS market_update + # + # { + # "open": "52853.04", + # "close": "55913.88", + # "high": "56272", + # "low": "49549.67", + # "volume": "57331.067185", + # "deal": "3063860382.42985338", + # "last": "55913.88", + # "period": 86400 + # } + # v2 + # { + # lastUpdateTimestamp: '2025-01-02T09:16:36.000Z', + # tradingPairs: 'ARB_USDC', + # lastPrice: '0.7727', + # lowestAsk: '0.7735', + # highestBid: '0.7732', + # baseVolume24h: '1555793.74', + # quoteVolume24h: '1157602.622406', + # tradesEnabled: True + # } + # + marketId = self.safe_string(ticker, 'tradingPairs') + market = self.safe_market(marketId, market) + # last price is provided as "last" or "last_price" + last = self.safe_string_n(ticker, ['last', 'last_price', 'lastPrice']) + # if "close" is provided, use it, otherwise use + close = self.safe_string(ticker, 'close', last) + return self.safe_ticker({ + 'symbol': market['symbol'], + 'timestamp': None, + 'datetime': None, + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': self.safe_string_2(ticker, 'bid', 'highestBid'), + 'bidVolume': None, + 'ask': self.safe_string_2(ticker, 'ask', 'lowestAsk'), + 'askVolume': None, + 'vwap': None, + 'open': self.safe_string(ticker, 'open'), + 'close': close, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': self.safe_string(ticker, 'change'), + 'average': None, + 'baseVolume': self.safe_string_n(ticker, ['base_volume', 'volume', 'baseVolume24h']), + 'quoteVolume': self.safe_string_n(ticker, ['quote_volume', 'deal', 'quoteVolume24h']), + 'info': ticker, + }, market) + + def fetch_order(self, id: str, symbol: Str = None, params={}) -> Order: + """ + fetches information on an order by the id + + https://docs.whitebit.com/private/http-trade-v4/#query-unexecutedactive-orders + https://docs.whitebit.com/private/http-trade-v4/#query-executed-orders + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.checkActive]: whether to check active orders(default: True) + :param boolean [params.checkExecuted]: whether to check executed orders(default: True) + :returns dict: an `order structure ` + """ + self.load_markets() + # Extract control parameters from params + checkActive = self.safe_bool(params, 'checkActive', True) + checkExecuted = self.safe_bool(params, 'checkExecuted', True) + request: dict = { + 'orderId': id, + } + market = None + if symbol is not None: + market = self.market(symbol) + request['market'] = market['id'] + # Try active orders first(if enabled) + if checkActive: + try: + response = self.v4PrivatePostOrders(self.extend(request, params)) + # Search for order in active orders response(array format) + for i in range(0, len(response)): + order = response[i] + orderId = self.safe_string(order, 'orderId') + if orderId == id: + marketId = self.safe_string(order, 'market') + marketNew = self.safe_market(marketId, None, '_') + return self.parse_order(order, marketNew) + except Exception as error: + if not (isinstance(error, OrderNotFound)): + raise error + # Try executed orders(if enabled) + if checkExecuted: + try: + response = self.v4PrivatePostTradeAccountOrderHistory(self.extend(request, params)) + # Search for order in executed orders response(object format) + marketIds = list(response.keys()) + for i in range(0, len(marketIds)): + marketId = marketIds[i] + marketNew = self.safe_market(marketId, None, '_') + orders = response[marketId] + for j in range(0, len(orders)): + order = orders[j] + orderId = self.safe_string(order, 'id') + if orderId == id: + return self.parse_order(order, marketNew) + except Exception as error: + if not (isinstance(error, OrderNotFound)): + raise error + # If both checks failed or were disabled, raise OrderNotFound + raise OrderNotFound(self.id + ' fetchOrder() order not found: ' + id) + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://docs.whitebit.com/public/http-v4/#market-activity + + :param str[] [symbols]: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.method]: either v2PublicGetTicker or v4PublicGetTicker default is v4PublicGetTicker + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + symbols = self.market_symbols(symbols) + method = 'v4PublicGetTicker' + method, params = self.handle_option_and_params(params, 'fetchTickers', 'method', method) + response = None + if method == 'v4PublicGetTicker': + response = self.v4PublicGetTicker(params) + else: + response = self.v2PublicGetTicker(params) + # + # "BCH_RUB": { + # "base_id":1831, + # "quote_id":0, + # "last_price":"32830.21", + # "quote_volume":"1494659.8024096", + # "base_volume":"46.1083", + # "isFrozen":false, + # "change":"2.12" + # }, + # + resultList = self.safe_list(response, 'result') + if resultList is not None: + return self.parse_tickers(resultList, symbols) + marketIds = list(response.keys()) + result: dict = {} + for i in range(0, len(marketIds)): + marketId = marketIds[i] + market = self.safe_market(marketId) + ticker = self.parse_ticker(response[marketId], market) + symbol = ticker['symbol'] + result[symbol] = ticker + return self.filter_by_array_tickers(result, 'symbol', symbols) + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://docs.whitebit.com/public/http-v4/#orderbook + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + if limit is not None: + request['limit'] = limit # default = 100, maximum = 100 + response = self.v4PublicGetOrderbookMarket(self.extend(request, params)) + # + # { + # "timestamp": 1594391413, + # "asks": [ + # [ + # "9184.41", + # "0.773162" + # ], + # [...] + # ], + # "bids": [ + # [ + # "9181.19", + # "0.010873" + # ], + # [...] + # ] + # } + # + timestamp = self.safe_timestamp(response, 'timestamp') + return self.parse_order_book(response, symbol, timestamp) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://docs.whitebit.com/public/http-v4/#recent-trades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + response = self.v4PublicGetTradesMarket(self.extend(request, params)) + # + # [ + # { + # "tradeID": 158056419, + # "price": "9186.13", + # "quote_volume": "0.0021", + # "base_volume": "9186.13", + # "trade_timestamp": 1594391747, + # "type": "sell" + # }, + # ], + # + return self.parse_trades(response, market, since, limit) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://docs.whitebit.com/private/http-trade-v4/#query-executed-order-history + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market: Market = None + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['market'] = market['id'] + response = self.v4PrivatePostTradeAccountExecutedHistory(self.extend(request, params)) + # + # when no symbol is provided + # + # { + # "USDC_USDT":[ + # { + # "id":"1343815269", + # "clientOrderId":"", + # "time":"1641051917.532965", + # "side":"sell", + # "role":"2", + # "amount":"9.986", + # "price":"0.9995", + # "deal":"9.981007", + # "fee":"0.009981007", + # "orderId":"58166729555" + # }, + # ] + # } + # + # when a symbol is provided + # + # [ + # { + # "id": 1343815269, + # "clientOrderId": '', + # "time": 1641051917.532965, + # "side": "sell", + # "role": 2, + # "amount": "9.986", + # "price": "0.9995", + # "deal": "9.981007", + # "fee": "0.009981007", + # "orderId": 58166729555, + # }, + # ] + # + if isinstance(response, list): + return self.parse_trades(response, market, since, limit) + else: + results = [] + keys = list(response.keys()) + for i in range(0, len(keys)): + marketId = keys[i] + marketNew = self.safe_market(marketId, None, '_') + rawTrades = self.safe_value(response, marketId, []) + parsed = self.parse_trades(rawTrades, marketNew, since, limit) + results = self.array_concat(results, parsed) + results = self.sort_by_2(results, 'timestamp', 'id') + return self.filter_by_since_limit(results, since, limit, 'timestamp') + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTradesV4 + # + # { + # "tradeID": 158056419, + # "price": "9186.13", + # "quote_volume": "0.0021", + # "base_volume": "9186.13", + # "trade_timestamp": 1594391747, + # "type": "sell" + # } + # + # orderTrades(v4Private) + # + # { + # "time": 1593342324.613711, + # "fee": "0.00000419198", + # "price": "0.00000701", + # "amount": "598", + # "id": 149156519, # trade id + # "dealOrderId": 3134995325, # orderId + # "clientOrderId": "customId11", + # "role": 2, # 1 = maker, 2 = taker + # "deal": "0.00419198" # amount in money + # "feeAsset": "USDT" + # } + # + # fetchMyTrades + # + # { + # "id": 1343815269, + # "clientOrderId": '', + # "time": 1641051917.532965, + # "side": "sell", + # "role": 2, + # "amount": "9.986", + # "price": "0.9995", + # "deal": "9.981007", + # "fee": "0.009981007", + # "orderId": 58166729555, + # "feeAsset": "USDT" + # } + # + market = self.safe_market(None, market) + timestamp = self.safe_timestamp_2(trade, 'time', 'trade_timestamp') + orderId = self.safe_string_2(trade, 'dealOrderId', 'orderId') + cost = self.safe_string(trade, 'deal') + price = self.safe_string(trade, 'price') + amount = self.safe_string_2(trade, 'amount', 'quote_volume') + id = self.safe_string_2(trade, 'id', 'tradeID') + side = self.safe_string_2(trade, 'type', 'side') + symbol = market['symbol'] + role = self.safe_integer(trade, 'role') + takerOrMaker: Str = None + if role is not None: + takerOrMaker = 'maker' if (role == 1) else 'taker' + fee = None + feeCost = self.safe_string(trade, 'fee') + if feeCost is not None: + fee = { + 'cost': feeCost, + 'currency': self.safe_currency_code(self.safe_string(trade, 'feeAsset')), + } + return self.safe_trade({ + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'id': id, + 'order': orderId, + 'type': None, + 'takerOrMaker': takerOrMaker, + 'side': side, + 'price': price, + 'amount': amount, + 'cost': cost, + 'fee': fee, + }, market) + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://docs.whitebit.com/public/http-v1/#kline + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + 'interval': self.safe_string(self.timeframes, timeframe, timeframe), + } + if since is not None: + maxLimit = 1440 + if limit is None: + limit = maxLimit + limit = min(limit, maxLimit) + start = self.parse_to_int(since / 1000) + request['start'] = start + if limit is not None: + request['limit'] = min(limit, 1440) + response = self.v1PublicGetKline(self.extend(request, params)) + # + # { + # "success":true, + # "message":"", + # "result":[ + # [1591488000,"0.025025","0.025025","0.025029","0.025023","6.181","0.154686629"], + # [1591488060,"0.025028","0.025033","0.025035","0.025026","8.067","0.201921167"], + # [1591488120,"0.025034","0.02505","0.02505","0.025034","20.089","0.503114696"], + # ] + # } + # + result = self.safe_list(response, 'result', []) + return self.parse_ohlcvs(result, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # [ + # 1591488000, + # "0.025025", + # "0.025025", + # "0.025029", + # "0.025023", + # "6.181", + # "0.154686629" + # ] + # + return [ + self.safe_timestamp(ohlcv, 0), # timestamp + self.safe_number(ohlcv, 1), # open + self.safe_number(ohlcv, 3), # high + self.safe_number(ohlcv, 4), # low + self.safe_number(ohlcv, 2), # close + self.safe_number(ohlcv, 5), # volume + ] + + def fetch_status(self, params={}): + """ + the latest known information on the availability of the exchange API + + https://docs.whitebit.com/public/http-v4/#server-status + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `status structure ` + """ + response = self.v4PublicGetPing(params) + # + # [ + # "pong" + # ] + # + status = self.safe_string(response, 0) + return { + 'status': 'ok' if (status == 'pong') else status, + 'updated': None, + 'eta': None, + 'url': None, + 'info': response, + } + + def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://docs.whitebit.com/public/http-v4/#server-time + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = self.v4PublicGetTime(params) + # + # { + # "time":1737380046 + # } + # + return self.safe_integer(response, 'time') + + def create_market_order_with_cost(self, symbol: str, side: OrderSide, cost: float, params={}): + """ + create a market order by providing the symbol, side and cost + :param str symbol: unified symbol of the market to create an order in + :param str side: 'buy' or 'sell' + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + req = { + 'cost': cost, + } + # only buy side is supported + return self.create_order(symbol, 'market', side, 0, None, self.extend(req, params)) + + def create_market_buy_order_with_cost(self, symbol: str, cost: float, params={}) -> Order: + """ + create a market buy order by providing the symbol and cost + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + return self.create_market_order_with_cost(symbol, 'buy', cost, params) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://docs.whitebit.com/private/http-trade-v4/#create-limit-order + https://docs.whitebit.com/private/http-trade-v4/#create-market-order + https://docs.whitebit.com/private/http-trade-v4/#create-buy-stock-market-order + https://docs.whitebit.com/private/http-trade-v4/#create-stop-limit-order + https://docs.whitebit.com/private/http-trade-v4/#create-stop-market-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.cost]: *market orders only* the cost of the order in units of the base currency + :param float [params.triggerPrice]: The price at which a trigger order is triggered at + :param bool [params.postOnly]: If True, the order will only be posted to the order book and not executed immediately + :param str [params.clientOrderId]: a unique id for the order + :param str [params.marginMode]: 'cross' or 'isolated', for margin trading, uses self.options.defaultMarginMode if not passed, defaults to None/None/None + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + 'side': side, + } + cost = None + cost, params = self.handle_param_string(params, 'cost') + if cost is not None: + if (side != 'buy') or (type != 'market'): + raise InvalidOrder(self.id + ' createOrder() cost is only supported for market buy orders') + request['amount'] = self.cost_to_precision(symbol, cost) + else: + request['amount'] = self.amount_to_precision(symbol, amount) + clientOrderId = self.safe_string_2(params, 'clOrdId', 'clientOrderId') + if clientOrderId is None: + brokerId = self.safe_string(self.options, 'brokerId') + if brokerId is not None: + request['clientOrderId'] = brokerId + self.uuid16() + else: + request['clientOrderId'] = clientOrderId + params = self.omit(params, ['clientOrderId']) + marketType = self.safe_string(market, 'type') + isLimitOrder = type == 'limit' + isMarketOrder = type == 'market' + triggerPrice = self.safe_number_n(params, ['triggerPrice', 'stopPrice', 'activation_price']) + isStopOrder = (triggerPrice is not None) + postOnly = self.is_post_only(isMarketOrder, False, params) + marginMode, query = self.handle_margin_mode_and_params('createOrder', params) + if postOnly: + request['postOnly'] = True + if marginMode is not None and marginMode != 'cross': + raise NotSupported(self.id + ' createOrder() is only available for cross margin') + params = self.omit(query, ['postOnly', 'triggerPrice', 'stopPrice']) + useCollateralEndpoint = marginMode is not None or marketType == 'swap' + response = None + if isStopOrder: + request['activation_price'] = self.price_to_precision(symbol, triggerPrice) + if isLimitOrder: + # stop limit order + request['price'] = self.price_to_precision(symbol, price) + response = self.v4PrivatePostOrderStopLimit(self.extend(request, params)) + else: + # stop market order + if useCollateralEndpoint: + response = self.v4PrivatePostOrderCollateralTriggerMarket(self.extend(request, params)) + else: + response = self.v4PrivatePostOrderStopMarket(self.extend(request, params)) + else: + if isLimitOrder: + # limit order + request['price'] = self.price_to_precision(symbol, price) + if useCollateralEndpoint: + response = self.v4PrivatePostOrderCollateralLimit(self.extend(request, params)) + else: + response = self.v4PrivatePostOrderNew(self.extend(request, params)) + else: + # market order + if useCollateralEndpoint: + response = self.v4PrivatePostOrderCollateralMarket(self.extend(request, params)) + else: + if cost is not None: + response = self.v4PrivatePostOrderMarket(self.extend(request, params)) + else: + response = self.v4PrivatePostOrderStockMarket(self.extend(request, params)) + return self.parse_order(response) + + def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + """ + edit a trade order + + https://docs.whitebit.com/private/http-trade-v4/#modify-order + + :param str id: cancel order id + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float price: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + # Handle clientOrderId vs orderId(clientOrderId takes priority) + clientOrderId = self.safe_string(params, 'clientOrderId') + if clientOrderId is not None: + request['clientOrderId'] = clientOrderId + else: + request['orderId'] = id + # Handle amount vs total parameter based on order type and side + triggerPrice = self.safe_number_n(params, ['triggerPrice', 'stopPrice', 'activationPrice']) + isStopOrder = (triggerPrice is not None) + # Handle activation price for stop orders + if isStopOrder: + request['activation_price'] = self.price_to_precision(symbol, triggerPrice) + isLimitOrder = type == 'limit' + total = self.safe_number(params, 'total') + if total is not None: + request['total'] = self.amount_to_precision(symbol, total) + elif amount is not None: + if isLimitOrder: + # Limit orders always use amount parameter + request['amount'] = self.amount_to_precision(symbol, amount) + elif type == 'market' and side == 'buy': + # Market buy orders use total parameter + request['total'] = self.amount_to_precision(symbol, amount) + else: + # Market sell orders use amount parameter + request['amount'] = self.amount_to_precision(symbol, amount) + # Handle price parameter for limit orders + if price is not None: + request['price'] = self.price_to_precision(symbol, price) + # Ensure at least one modifiable parameter is provided + hasModifiableParam = (amount is not None) or (price is not None) or (triggerPrice is not None) or (total is not None) + if not hasModifiableParam: + raise ArgumentsRequired(self.id + ' editOrder() requires at least one of: amount, price, activationPrice, or total parameters') + params = self.omit(params, ['clientOrderId', 'triggerPrice', 'stopPrice', 'activationPrice', 'total']) + response = self.v4PrivatePostOrderModify(self.extend(request, params)) + return self.parse_order(response) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://docs.whitebit.com/private/http-trade-v4/#cancel-order + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + 'orderId': int(id), + } + response = self.v4PrivatePostOrderCancel(self.extend(request, params)) + # + # { + # "orderId": 4180284841, # order id + # "clientOrderId": "customId11", # custom order identifier; "clientOrderId": "" - if not specified. + # "market": "BTC_USDT", # deal market + # "side": "buy", # order side + # "type": "stop market", # order type + # "timestamp": 1595792396.165973, # current timestamp + # "dealMoney": "0", # if order finished - amount in money currency that is finished + # "dealStock": "0", # if order finished - amount in stock currency that is finished + # "amount": "0.001", # amount + # "takerFee": "0.001", # maker fee ratio. If the number less than 0.0001 - it will be rounded to zero + # "makerFee": "0.001", # maker fee ratio. If the number less than 0.0001 - it will be rounded to zero + # "left": "0.001", # if order not finished - rest of the amount that must be finished + # "dealFee": "0", # fee in money that you pay if order is finished + # "price": "40000", # price if price isset + # "activation_price": "40000" # activation price if activation price is set + # } + # + return self.parse_order(response) + + def cancel_all_orders(self, symbol: Str = None, params={}): + """ + cancel all open orders + + https://docs.whitebit.com/private/http-trade-v4/#cancel-all-orders + + :param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: market type, ['swap', 'spot'] + :param boolean [params.isMargin]: cancel all margin orders + :returns dict[]: a list of `order structures ` + """ + self.load_markets() + market = None + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['market'] = market['id'] + type = None + type, params = self.handle_market_type_and_params('cancelAllOrders', market, params) + requestType = [] + if type == 'spot': + isMargin = None + isMargin, params = self.handle_option_and_params(params, 'cancelAllOrders', 'isMargin', False) + if isMargin: + requestType.append('margin') + else: + requestType.append('spot') + elif type == 'swap': + requestType.append('futures') + else: + raise NotSupported(self.id + ' cancelAllOrders() does not support ' + type + ' type') + request['type'] = requestType + response = self.v4PrivatePostOrderCancelAll(self.extend(request, params)) + # + # [] + # + return self.parse_orders(response, market) + + def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user(combines open and closed orders) + + https://docs.whitebit.com/private/http-trade-v4/#query-unexecutedactive-orders + https://docs.whitebit.com/private/http-trade-v4/#query-executed-orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + # Fetch both open and closed orders in parallel + openOrders, closedOrders = asyncio.gather(*[ + self.fetch_open_orders(symbol, since, limit, params), + self.fetch_closed_orders(symbol, since, limit, params), + ]) + allOrders = self.array_concat(openOrders, closedOrders) + # Sort by timestamp(most recent first) + sortedOrders = self.sort_by(allOrders, 'timestamp', True) + # Apply limit if specified(since and symbol filtering already handled by individual methods) + if limit is not None and len(sortedOrders) > limit: + return sortedOrders[0:limit] + return sortedOrders + + def cancel_all_orders_after(self, timeout: Int, params={}): + """ + dead man's switch, cancel all orders after the given timeout + + https://docs.whitebit.com/private/http-trade-v4/#sync-kill-switch-timer + + :param number timeout: time in milliseconds, 0 represents cancel the timer + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.types]: Order types value. Example: "spot", "margin", "futures" or None + :param str [params.symbol]: symbol unified symbol of the market the order was made in + :returns dict: the api result + """ + self.load_markets() + symbol = self.safe_string(params, 'symbol') + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelAllOrdersAfter() requires a symbol argument in params') + market = self.market(symbol) + params = self.omit(params, 'symbol') + isBiggerThanZero = (timeout > 0) + request: dict = { + 'market': market['id'], + # 'timeout': self.number_to_string(timeout / 1000) if (timeout > 0) else null, + } + if isBiggerThanZero: + request['timeout'] = self.number_to_string(timeout / 1000) + else: + request['timeout'] = 'null' + response = self.v4PrivatePostOrderKillSwitch(self.extend(request, params)) + # + # { + # "market": "BTC_USDT", # currency market, + # "startTime": 1662478154, # now timestamp, + # "cancellationTime": 1662478154, # now + timer_value, + # "types": ["spot", "margin"] + # } + # + return response + + def parse_balance(self, response) -> Balances: + balanceKeys = list(response.keys()) + result: dict = {} + for i in range(0, len(balanceKeys)): + id = balanceKeys[i] + code = self.safe_currency_code(id) + balance = response[id] + if isinstance(balance, dict) and balance is not None: + account = self.account() + account['free'] = self.safe_string_2(balance, 'available', 'main_balance') + account['used'] = self.safe_string(balance, 'freeze') + account['total'] = self.safe_string(balance, 'main_balance') + result[code] = account + else: + account = self.account() + account['total'] = balance + result[code] = account + return self.safe_balance(result) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://docs.whitebit.com/private/http-main-v4/#main-balance + https://docs.whitebit.com/private/http-trade-v4/#trading-balance + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + marketType = None + marketType, params = self.handle_market_type_and_params('fetchBalance', None, params) + response = None + if marketType == 'swap': + response = self.v4PrivatePostCollateralAccountBalance(params) + else: + options = self.safe_value(self.options, 'fetchBalance', {}) + defaultAccount = self.safe_string(options, 'account') + account = self.safe_string_2(params, 'account', 'type', defaultAccount) + params = self.omit(params, ['account', 'type']) + if account == 'main' or account == 'funding': + response = self.v4PrivatePostMainAccountBalance(params) + else: + response = self.v4PrivatePostTradeAccountBalance(params) + # + # main account + # + # { + # "BTC":{"main_balance":"0.0013929494020316"}, + # "ETH":{"main_balance":"0.001398289308"}, + # } + # + # spot trade account + # + # { + # "BTC": {"available": "0.123", "freeze": "1"}, + # "XMR": {"available": "3013", "freeze": "100"}, + # } + # + # swap + # + # { + # "BTC": 1, + # "USDT": 1000 + # } + # + return self.parse_balance(response) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://docs.whitebit.com/private/http-trade-v4/#query-unexecutedactive-orders + + :param str [symbol]: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + market = None + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['market'] = market['id'] + if limit is not None: + request['limit'] = min(limit, 100) + response = self.v4PrivatePostOrders(self.extend(request, params)) + # + # [ + # { + # "orderId": 3686033640, + # "clientOrderId": "customId11", + # "market": "BTC_USDT", + # "side": "buy", + # "type": "limit", + # "timestamp": 1594605801.49815, # current timestamp of unexecuted order + # "dealMoney": "0", # executed amount in money + # "dealStock": "0", # executed amount in stock + # "amount": "2.241379", # active order amount + # "takerFee": "0.001", + # "makerFee": "0.001", + # "left": "2.241379", # unexecuted amount in stock + # "dealFee": "0", # executed fee by deal + # "price": "40000" + # }, + # ] + # + return self.parse_orders(response, market, since, limit, {'status': 'open'}) + + def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://docs.whitebit.com/private/http-trade-v4/#query-executed-orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + request['market'] = market['id'] + if limit is not None: + request['limit'] = min(limit, 100) # default 50 max 100 + response = self.v4PrivatePostTradeAccountOrderHistory(self.extend(request, params)) + # + # { + # "BTC_USDT": [ + # { + # "id": 160305483, + # "clientOrderId": "customId11", + # "time": 1594667731.724403, + # "side": "sell", + # "role": 2, # 1 = maker, 2 = taker + # "amount": "0.000076", + # "price": "9264.21", + # "deal": "0.70407996", + # "fee": "0.00070407996" + # }, + # ], + # } + # + marketIds = list(response.keys()) + results = [] + for i in range(0, len(marketIds)): + marketId = marketIds[i] + marketNew = self.safe_market(marketId, None, '_') + orders = response[marketId] + for j in range(0, len(orders)): + order = self.parse_order(orders[j], marketNew) + results.append(self.extend(order, {'status': 'closed'})) + results = self.sort_by(results, 'timestamp') + results = self.filter_by_symbol_since_limit(results, symbol, since, limit) + return results + + def parse_order_type(self, type: Str): + types: dict = { + 'limit': 'limit', + 'market': 'market', + 'stop market': 'market', + 'stop limit': 'limit', + 'stock market': 'market', + 'margin limit': 'limit', + 'margin market': 'market', + } + return self.safe_string(types, type, type) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder, fetchOpenOrders, cancelOrder + # + # { + # "orderId":105687928629, + # "clientOrderId":"", + # "market":"DOGE_USDT", + # "side":"sell", + # "type":"stop market", + # "timestamp":1659091079.729576, + # "dealMoney":"0", # executed amount in quote + # "dealStock":"0", # base filled amount + # "amount":"100", + # "takerFee":"0.001", + # "makerFee":"0", + # "left":"100", + # "price": "40000", # price if price isset + # "dealFee":"0", + # "activation_price":"0.065" # stop price(if stop limit or stop market) + # } + # + # fetchClosedOrders + # + # { + # "id":105531094719, + # "clientOrderId":"", + # "ctime":1659045334.550127, + # "ftime":1659045334.550127, + # "side":"buy", + # "amount":"5.9940059", # cost in terms of quote for regular market orders, amount in terms or base for all other order types + # "price":"0", + # "type":"market", + # "takerFee":"0.001", + # "makerFee":"0", + # "dealFee":"0.0059375815", + # "dealStock":"85", # base filled amount + # "dealMoney":"5.9375815", # executed amount in quote + # } + # + marketId = self.safe_string(order, 'market') + market = self.safe_market(marketId, market, '_') + symbol = market['symbol'] + side = self.safe_string(order, 'side') + filled = self.safe_string(order, 'dealStock') + remaining = self.safe_string(order, 'left') + clientOrderId = self.safe_string(order, 'clientOrderId') + if clientOrderId == '': + clientOrderId = None + price = self.safe_string(order, 'price') + triggerPrice = self.safe_number(order, 'activation_price') + orderId = self.safe_string_2(order, 'orderId', 'id') + type = self.safe_string(order, 'type') + orderType = self.parse_order_type(type) + if orderType == 'market': + remaining = None + amount = self.safe_string(order, 'amount') + cost = self.safe_string(order, 'dealMoney') + if (side == 'buy') and ((type == 'market') or (type == 'stop market')): + amount = filled + dealFee = self.safe_string(order, 'dealFee') + fee = None + if dealFee is not None: + fee = { + 'cost': self.parse_number(dealFee), + 'currency': market['quote'], + } + timestamp = self.safe_timestamp_2(order, 'ctime', 'timestamp') + lastTradeTimestamp = self.safe_timestamp(order, 'ftime') + return self.safe_order({ + 'info': order, + 'id': orderId, + 'symbol': symbol, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': lastTradeTimestamp, + 'timeInForce': None, + 'postOnly': None, + 'status': None, + 'side': side, + 'price': price, + 'type': orderType, + 'triggerPrice': triggerPrice, + 'amount': amount, + 'filled': filled, + 'remaining': remaining, + 'average': None, + 'cost': cost, + 'fee': fee, + 'trades': None, + }, market) + + def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all the trades made from a single order + + https://docs.whitebit.com/private/http-trade-v4/#query-executed-order-deals + + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + self.load_markets() + request: dict = { + 'orderId': int(id), + } + market = None + if symbol is not None: + market = self.market(symbol) + request['market'] = market['id'] + if limit is not None: + request['limit'] = min(limit, 100) + response = self.v4PrivatePostTradeAccountOrder(self.extend(request, params)) + # + # { + # "records": [ + # { + # "time": 1593342324.613711, + # "fee": "0.00000419198", + # "price": "0.00000701", + # "amount": "598", + # "id": 149156519, # trade id + # "dealOrderId": 3134995325, # orderId + # "clientOrderId": "customId11", # empty string if not specified + # "role": 2, # 1 = maker, 2 = taker + # "deal": "0.00419198" + # } + # ], + # "offset": 0, + # "limit": 100 + # } + # + data = self.safe_list(response, 'records', []) + return self.parse_trades(data, market) + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://docs.whitebit.com/private/http-main-v4/#get-depositwithdraw-history + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.transactionMethod]: transaction method(1=deposit, 2=withdrawal) - automatically set to '2' for withdrawals + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + currency = None + request: dict = {} + if code is not None: + currency = self.currency(code) + request['ticker'] = currency['id'] + if since is not None: + request['startDate'] = self.parse_to_int(since / 1000) + if limit is None or limit > 100: + limit = 100 + if limit is not None: + request['limit'] = limit + # Use transactionMethod parameter to filter withdrawals server-side(method = 2) + request['transactionMethod'] = '2' + response = self.v4PrivatePostMainAccountHistory(self.extend(request, params)) + # + # [ + # { + # "id": 123456789, # Transaction ID + # "method": "2", # Method: 1=deposit, 2=withdrawal(filtered server-side) + # "ticker": "BTC", # Currency ticker + # "amount": "0.001", # Transaction amount + # "address": "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", # Withdrawal address + # "memo": "", # Memo/tag(if required) + # "network": "BTC", # Network name + # "fee": "0.0005", # Transaction fee + # "status": "1", # Status: 0=pending, 1=completed, 2=failed + # "timestamp": 1641051917, # Transaction timestamp + # "txid": "abc123def456..." # Transaction hash + # }, + # {...} # More withdrawal transactions + # ] + # + return self.parse_transactions(response, currency, since, limit) + + def fetch_transactions(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch history of deposits and withdrawals + + https://docs.whitebit.com/private/http-main-v4/#get-depositwithdraw-history + + :param str [code]: unified currency code + :param int [since]: the earliest time in ms to fetch transactions for + :param int [limit]: the maximum number of transactions structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.transactionMethod]: transaction method(1=deposit, 2=withdrawal) - automatically set to '1' for deposits + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + currency = None + request: dict = {} + if code is not None: + currency = self.currency(code) + request['ticker'] = currency['id'] + if since is not None: + request['startDate'] = self.parse_to_int(since / 1000) + if limit is None or limit > 100: + limit = 100 + if limit is not None: + request['limit'] = limit + # Do not filter by transactionMethod to get all transactions(deposits and withdrawals) + response = self.v4PrivatePostMainAccountHistory(self.extend(request, params)) + # + # [ + # { + # "id": 123456789, # Transaction ID + # "method": "1", # Method: 1=deposit, 2=withdrawal + # "ticker": "BTC", # Currency ticker + # "amount": "0.001", # Transaction amount + # "address": "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", # Transaction address + # "memo": "", # Memo/tag(if required) + # "network": "BTC", # Network name + # "fee": "0.0005", # Transaction fee + # "status": "1", # Status: 0=pending, 1=completed, 2=failed + # "timestamp": 1641051917, # Transaction timestamp + # "txid": "abc123def456..." # Transaction hash + # }, + # {...} # More transactions(deposits and withdrawals) + # ] + # + return self.parse_transactions(response, currency, since, limit) + + def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://docs.whitebit.com/private/http-main-v4/#get-fiat-deposit-address + https://docs.whitebit.com/private/http-main-v4/#get-cryptocurrency-deposit-address + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'ticker': currency['id'], + } + response = None + if self.is_fiat(code): + provider = self.safe_string(params, 'provider') + if provider is None: + raise ArgumentsRequired(self.id + ' fetchDepositAddress() requires a provider when the ticker is fiat') + request['provider'] = provider + amount = self.safe_number(params, 'amount') + if amount is None: + raise ArgumentsRequired(self.id + ' fetchDepositAddress() requires an amount when the ticker is fiat') + request['amount'] = amount + uniqueId = self.safe_value(params, 'uniqueId') + if uniqueId is None: + raise ArgumentsRequired(self.id + ' fetchDepositAddress() requires an uniqueId when the ticker is fiat') + response = self.v4PrivatePostMainAccountFiatDepositUrl(self.extend(request, params)) + else: + response = self.v4PrivatePostMainAccountAddress(self.extend(request, params)) + # + # fiat + # + # { + # "url": "https://someaddress.com" + # } + # + # crypto + # + # { + # "account": { + # "address": "GDTSOI56XNVAKJNJBLJGRNZIVOCIZJRBIDKTWSCYEYNFAZEMBLN75RMN", + # "memo": "48565488244493" + # }, + # "required": { + # "fixedFee": "0", + # "flexFee": { + # "maxFee": "0", + # "minFee": "0", + # "percent": "0" + # }, + # "maxAmount": "0", + # "minAmount": "1" + # } + # } + # + url = self.safe_string(response, 'url') + account = self.safe_value(response, 'account', {}) + address = self.safe_string(account, 'address', url) + tag = self.safe_string(account, 'memo') + self.check_address(address) + return { + 'info': response, + 'currency': code, + 'network': None, + 'address': address, + 'tag': tag, + } + + def create_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + create a currency deposit address + + https://docs.whitebit.com/private/http-main-v4/#create-new-address-for-deposit + + :param str code: unified currency code of the currency for the deposit address + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.network]: the blockchain network to create a deposit address on + :param str [params.type]: address type, available for specific currencies + :returns dict: an `address structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'ticker': currency['id'], + } + response = self.v4PrivatePostMainAccountCreateNewAddress(self.extend(request, params)) + # + # { + # "account": { + # "address": "GDTSOI56XNVAKJNJBLJGRNZIVOCIZJRBIDKTWSCYEYNFAZEMBLN75RMN", + # "memo": "48565488244493" + # }, + # "required": { + # "maxAmount": "0", + # "minAmount": "1", + # "fixedFee": "0", + # "flexFee": { + # "maxFee": "0", + # "minFee": "0", + # "percent": "0" + # } + # } + # } + # + data = self.safe_dict(response, 'account', {}) + return self.parse_deposit_address(data, currency) + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + # + # { + # "address": "GDTSOI56XNVAKJNJBLJGRNZIVOCIZJRBIDKTWSCYEYNFAZEMBLN75RMN", + # "memo": "48565488244493" + # }, + # + return { + 'info': depositAddress, + 'currency': self.safe_currency_code(None, currency), + 'network': None, + 'address': self.safe_string(depositAddress, 'address'), + 'tag': self.safe_string(depositAddress, 'memo'), + } + + def fetch_accounts(self, params={}) -> List[Account]: + """ + fetch all the accounts associated with a profile + + https://docs.whitebit.com/private/http-main-v4/#sub-account-list + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `account structures ` + """ + self.load_markets() + accounts = [] + # Fetch sub-accounts + # + # [ + # { + # "id": "12345", + # "name": "SubAccount1", + # "status": "active", + # "permissions": ["trade", "withdraw"] + # } + # ] + # + subAccounts = self.v4PrivatePostSubAccountList(params) + if subAccounts and isinstance(subAccounts, list): + for i in range(0, len(subAccounts)): + subAccount = self.safe_value(subAccounts, i) + accountId = self.safe_string(subAccount, 'id') + accountName = self.safe_string(subAccount, 'name') + if accountId: + accounts.append({ + 'id': accountId, + 'type': 'subaccount', + 'name': accountName or 'SubAccount ' + accountId, + 'code': None, + 'info': subAccount, + }) + return accounts + + def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://docs.whitebit.com/private/http-trade-v4/#change-collateral-account-leverage + + :param float leverage: the rate of leverage + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + self.load_markets() + if symbol is not None: + raise NotSupported(self.id + ' setLeverage() does not allow to set per symbol') + if (leverage < 1) or (leverage > 20): + raise BadRequest(self.id + ' setLeverage() leverage should be between 1 and 20') + request: dict = { + 'leverage': leverage, + } + return self.v4PrivatePostCollateralAccountLeverage(self.extend(request, params)) + # { + # "leverage": 5 + # } + + def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://docs.whitebit.com/private/http-main-v4/#transfer-between-main-and-trade-balances + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account to transfer from - main, spot, collateral + :param str toAccount: account to transfer to - main, spot, collateral + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transfer structure ` + """ + self.load_markets() + currency = self.currency(code) + accountsByType = self.safe_value(self.options, 'accountsByType') + fromAccountId = self.safe_string(accountsByType, fromAccount, fromAccount) + toAccountId = self.safe_string(accountsByType, toAccount, toAccount) + amountString = self.currency_to_precision(code, amount) + request: dict = { + 'ticker': currency['id'], + 'amount': amountString, + 'from': fromAccountId, + 'to': toAccountId, + } + response = self.v4PrivatePostMainAccountTransfer(self.extend(request, params)) + # + # [] + # + return self.parse_transfer(response, currency) + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + # + # [] + # + return { + 'info': transfer, + 'id': None, + 'timestamp': None, + 'datetime': None, + 'currency': self.safe_currency_code(None, currency), + 'amount': None, + 'fromAccount': None, + 'toAccount': None, + 'status': None, + } + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://docs.whitebit.com/private/http-main-v4/#create-withdraw-request + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + self.load_markets() + currency = self.currency(code) # check if it has canDeposit + request: dict = { + 'ticker': currency['id'], + 'amount': self.currency_to_precision(code, amount), + 'address': address, + } + uniqueId = self.safe_value(params, 'uniqueId') + if uniqueId is None: + uniqueId = self.uuid22() + request['uniqueId'] = uniqueId + if tag is not None: + request['memo'] = tag + if self.is_fiat(code): + provider = self.safe_value(params, 'provider') + if provider is None: + raise ArgumentsRequired(self.id + ' withdraw() requires a provider when the ticker is fiat') + request['provider'] = provider + response = self.v4PrivatePostMainAccountWithdraw(self.extend(request, params)) + # + # empty array with a success status + # go to deposit/withdraw history and check you request status by uniqueId + # + # [] + # + return self.extend({'id': uniqueId}, self.parse_transaction(response, currency)) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # { + # "address": "3ApEASLcrQtZpg1TsssFgYF5V5YQJAKvuE", # deposit address + # "uniqueId": null, # unique Id of deposit + # "transactionId": "a6d71d69-2b17-4ad8-8b15-2d686c54a1a5", + # "createdAt": 1593437922, # timestamp of deposit + # "currency": "Bitcoin", # deposit currency + # "ticker": "BTC", # deposit currency ticker + # "method": 1, # called method 1 - deposit, 2 - withdraw + # "amount": "0.0006", # amount of deposit + # "description": "", # deposit description + # "memo": "", # deposit memo + # "fee": "0", # deposit fee + # "status": 15, # transactions status + # "network": null, # if currency is multinetwork + # "transactionHash": "a275a514013e4e0f927fd0d1bed215e7f6f2c4c6ce762836fe135ec22529d886", # deposit transaction hash + # "details": { + # "partial": { # details about partially successful withdrawals + # "requestAmount": "50000", # requested withdrawal amount + # "processedAmount": "39000", # processed withdrawal amount + # "processedFee": "273", # fee for processed withdrawal amount + # "normalizeTransaction": "" # deposit id + # } + # }, + # "confirmations": { # if transaction status == 15 you can see self object + # "actual": 1, # current block confirmations + # "required": 2 # required block confirmation for successful deposit + # } + # "centralized": False, + # } + # + currency = self.safe_currency(None, currency) + address = self.safe_string(transaction, 'address') + timestamp = self.safe_timestamp(transaction, 'createdAt') + currencyId = self.safe_string(transaction, 'ticker') + status = self.safe_string(transaction, 'status') + method = self.safe_string(transaction, 'method') + return { + 'id': self.safe_string(transaction, 'uniqueId'), + 'txid': self.safe_string(transaction, 'transactionId'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'network': self.safe_string(transaction, 'network'), + 'addressFrom': address if (method == '1') else None, + 'address': address, + 'addressTo': address if (method == '2') else None, + 'amount': self.safe_number(transaction, 'amount'), + 'type': 'deposit' if (method == '1') else 'withdrawal', + 'currency': self.safe_currency_code(currencyId, currency), + 'status': self.parse_transaction_status(status), + 'updated': None, + 'tagFrom': None, + 'tag': None, + 'tagTo': None, + 'comment': self.safe_string(transaction, 'description'), + 'internal': None, + 'fee': { + 'cost': self.safe_number(transaction, 'fee'), + 'currency': self.safe_currency_code(currencyId, currency), + }, + 'info': transaction, + } + + def parse_transaction_status(self, status: Str): + statuses: dict = { + '1': 'pending', + '2': 'pending', + '3': 'ok', + '4': 'canceled', + '5': 'pending', + '6': 'pending', + '7': 'ok', + '9': 'canceled', + '10': 'pending', + '11': 'pending', + '12': 'pending', + '13': 'pending', + '14': 'pending', + '15': 'pending', + '16': 'pending', + '17': 'pending', + } + return self.safe_string(statuses, status, status) + + def fetch_deposit(self, id: str, code: Str = None, params={}): + """ + fetch information on a deposit + + https://docs.whitebit.com/private/http-main-v4/#get-depositwithdraw-history + + :param str id: deposit id + :param str code: not used by whitebit fetchDeposit() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + self.load_markets() + currency = None + request: dict = { + 'transactionMethod': 1, + 'uniqueId': id, + 'limit': 1, + 'offset': 0, + } + if code is not None: + currency = self.currency(code) + request['ticker'] = currency['id'] + response = self.v4PrivatePostMainAccountHistory(self.extend(request, params)) + # + # { + # "limit": 100, + # "offset": 0, + # "records": [ + # { + # "address": "3ApEASLcrQtZpg1TsssFgYF5V5YQJAKvuE", # deposit address + # "uniqueId": null, # unique Id of deposit + # "createdAt": 1593437922, # timestamp of deposit + # "currency": "Bitcoin", # deposit currency + # "ticker": "BTC", # deposit currency ticker + # "method": 1, # called method 1 - deposit, 2 - withdraw + # "amount": "0.0006", # amount of deposit + # "description": "", # deposit description + # "memo": "", # deposit memo + # "fee": "0", # deposit fee + # "status": 15, # transactions status + # "network": null, # if currency is multinetwork + # "transactionHash": "a275a514013e4e0f927fd0d1bed215e7f6f2c4c6ce762836fe135ec22529d886", # deposit transaction hash + # "details": { + # "partial": { # details about partially successful withdrawals + # "requestAmount": "50000", # requested withdrawal amount + # "processedAmount": "39000", # processed withdrawal amount + # "processedFee": "273", # fee for processed withdrawal amount + # "normalizeTransaction": "" # deposit id + # } + # }, + # "confirmations": { # if transaction status == 15 you can see self object + # "actual": 1, # current block confirmations + # "required": 2 # required block confirmation for successful deposit + # } + # }, + # {...}, + # ], + # "total": 300 # total number of transactions, use self for calculating ‘limit’ and ‘offset' + # } + # + records = self.safe_value(response, 'records', []) + first = self.safe_dict(records, 0, {}) + return self.parse_transaction(first, currency) + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://docs.whitebit.com/private/http-main-v4/#get-depositwithdraw-history + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + currency = None + request: dict = { + 'transactionMethod': 1, + 'limit': 100, + 'offset': 0, + } + if code is not None: + currency = self.currency(code) + request['ticker'] = currency['id'] + if limit is not None: + request['limit'] = min(limit, 100) + response = self.v4PrivatePostMainAccountHistory(self.extend(request, params)) + # + # { + # "limit": 100, + # "offset": 0, + # "records": [ + # { + # "address": "3ApEASLcrQtZpg1TsssFgYF5V5YQJAKvuE", # deposit address + # "uniqueId": null, # unique Id of deposit + # "createdAt": 1593437922, # timestamp of deposit + # "currency": "Bitcoin", # deposit currency + # "ticker": "BTC", # deposit currency ticker + # "method": 1, # called method 1 - deposit, 2 - withdraw + # "amount": "0.0006", # amount of deposit + # "description": "", # deposit description + # "memo": "", # deposit memo + # "fee": "0", # deposit fee + # "status": 15, # transactions status + # "network": null, # if currency is multinetwork + # "transactionHash": "a275a514013e4e0f927fd0d1bed215e7f6f2c4c6ce762836fe135ec22529d886", # deposit transaction hash + # "details": { + # "partial": { # details about partially successful withdrawals + # "requestAmount": "50000", # requested withdrawal amount + # "processedAmount": "39000", # processed withdrawal amount + # "processedFee": "273", # fee for processed withdrawal amount + # "normalizeTransaction": "" # deposit id + # } + # }, + # "confirmations": { # if transaction status == 15 you can see self object + # "actual": 1, # current block confirmations + # "required": 2 # required block confirmation for successful deposit + # } + # }, + # {...}, + # ], + # "total": 300 # total number of transactions, use self for calculating ‘limit’ and ‘offset' + # } + # + records = self.safe_list(response, 'records', []) + return self.parse_transactions(records, currency, since, limit) + + def fetch_borrow_interest(self, code: Str = None, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[BorrowInterest]: + """ + fetch the interest owed by the user for borrowing currency for margin trading + + https://docs.whitebit.com/private/http-trade-v4/#open-positions + + :param str code: unified currency code + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch borrrow interest for + :param int [limit]: the maximum number of structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `borrow interest structures ` + """ + self.load_markets() + request: dict = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['market'] = market['id'] + response = self.v4PrivatePostCollateralAccountPositionsOpen(self.extend(request, params)) + # + # [ + # { + # "positionId": 191823, + # "market": "BTC_USDT", + # "openDate": 1660340344.027163, + # "modifyDate": 1660340344.027163, + # "amount": "0.003075", + # "basePrice": "24149.24512", + # "liquidationPrice": "7059.02", + # "leverage": "5", + # "pnl": "-0.15", + # "pnlPercent": "-0.20", + # "margin": "14.86", + # "freeMargin": "44.99", + # "funding": "0", + # "unrealizedFunding": "0.0000307828284903", + # "liquidationState": null + # } + # ] + # + interest = self.parse_borrow_interests(response, market) + return self.filter_by_currency_since_limit(interest, code, since, limit) + + def parse_borrow_interest(self, info: dict, market: Market = None) -> BorrowInterest: + # + # { + # "positionId": 191823, + # "market": "BTC_USDT", + # "openDate": 1660340344.027163, + # "modifyDate": 1660340344.027163, + # "amount": "0.003075", + # "basePrice": "24149.24512", + # "liquidationPrice": "7059.02", + # "leverage": "5", + # "pnl": "-0.15", + # "pnlPercent": "-0.20", + # "margin": "14.86", + # "freeMargin": "44.99", + # "funding": "0", + # "unrealizedFunding": "0.0000307828284903", + # "liquidationState": null + # } + # + marketId = self.safe_string(info, 'market') + symbol = self.safe_symbol(marketId, market, '_') + timestamp = self.safe_timestamp(info, 'modifyDate') + return { + 'info': info, + 'symbol': symbol, + 'currency': 'USDT', + 'interest': self.safe_number(info, 'unrealizedFunding'), + 'interestRate': 0.00098, # https://whitebit.com/fees + 'amountBorrowed': self.safe_number(info, 'amount'), + 'marginMode': 'cross', + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + + def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate + + https://docs.whitebit.com/public/http-v4/#available-futures-markets-list + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + self.load_markets() + symbol = self.symbol(symbol) + response = self.fetch_funding_rates([symbol], params) + return self.safe_value(response, symbol) + + def fetch_funding_rates(self, symbols: Strings = None, params={}) -> FundingRates: + """ + fetch the funding rate for multiple markets + + https://docs.whitebit.com/public/http-v4/#available-futures-markets-list + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `funding rate structures `, indexed by market symbols + """ + self.load_markets() + symbols = self.market_symbols(symbols) + response = self.v4PublicGetFutures(params) + # + # [ + # { + # "name": "BTC_USDT", + # "type": "direct", + # "quanto_multiplier": "0.0001", + # "ref_discount_rate": "0", + # "order_price_deviate": "0.5", + # "maintenance_rate": "0.005", + # "mark_type": "index", + # "last_price": "38026", + # "mark_price": "37985.6", + # "index_price": "37954.92", + # "funding_rate_indicative": "0.000219", + # "mark_price_round": "0.01", + # "funding_offset": 0, + # "in_delisting": False, + # "risk_limit_base": "1000000", + # "interest_rate": "0.0003", + # "order_price_round": "0.1", + # "order_size_min": 1, + # "ref_rebate_rate": "0.2", + # "funding_interval": 28800, + # "risk_limit_step": "1000000", + # "leverage_min": "1", + # "leverage_max": "100", + # "risk_limit_max": "8000000", + # "maker_fee_rate": "-0.00025", + # "taker_fee_rate": "0.00075", + # "funding_rate": "0.002053", + # "order_size_max": 1000000, + # "funding_next_apply": 1610035200, + # "short_users": 977, + # "config_change_time": 1609899548, + # "trade_size": 28530850594, + # "position_size": 5223816, + # "long_users": 455, + # "funding_impact_value": "60000", + # "orders_limit": 50, + # "trade_id": 10851092, + # "orderbook_id": 2129638396 + # } + # ] + # + data = self.safe_list(response, 'result', []) + return self.parse_funding_rates(data, symbols) + + def parse_funding_rate(self, contract, market: Market = None) -> FundingRate: + # + # { + # "ticker_id":"ADA_PERP", + # "stock_currency":"ADA", + # "money_currency":"USDT", + # "last_price":"0.296708", + # "stock_volume":"7982130", + # "money_volume":"2345758.29189", + # "bid":"0.296608", + # "ask":"0.296758", + # "high":"0.298338", + # "low":"0.290171", + # "product_type":"Perpetual", + # "open_interest":"46533000", + # "index_price":"0.29659", + # "index_name":"Cardano", + # "index_currency":"ADA", + # "funding_rate":"0.0001", + # "next_funding_rate_timestamp":"1691193600000", + # "brackets":{ + # "1":"0", + # "2":"0", + # "3":"0", + # "5":"0", + # "10":"0", + # "20":"0", + # "50":"-10000", + # "100":"-5000" + # }, + # "max_leverage":"100" + # } + # + marketId = self.safe_string(contract, 'ticker_id') + symbol = self.safe_symbol(marketId, market) + markPrice = self.safe_number(contract, 'markPrice') + indexPrice = self.safe_number(contract, 'indexPrice') + interestRate = self.safe_number(contract, 'interestRate') + fundingRate = self.safe_number(contract, 'funding_rate') + fundingTime = self.safe_integer(contract, 'next_funding_rate_timestamp') + return { + 'info': contract, + 'symbol': symbol, + 'markPrice': markPrice, + 'indexPrice': indexPrice, + 'interestRate': interestRate, + 'timestamp': None, + 'datetime': None, + 'fundingRate': fundingRate, + 'fundingTimestamp': fundingTime, + 'fundingDatetime': self.iso8601(fundingTime), + 'nextFundingRate': None, + 'nextFundingTimestamp': None, + 'nextFundingDatetime': None, + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': None, + } + + def fetch_funding_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[FundingHistory]: + """ + fetch the history of funding payments paid and received on self account + + https://docs.whitebit.com/private/http-trade-v4/#funding-history + + :param str [symbol]: unified market symbol + :param int [since]: the starting timestamp in milliseconds + :param int [limit]: the number of entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch funding history for + :returns dict[]: a list of `funding history structures ` + """ + self.load_markets() + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchFundingHistory() requires a symbol argument') + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + if since is not None: + request['startDate'] = since + if limit is not None: + request['limit'] = since + request, params = self.handle_until_option('endDate', request, params) + response = self.v4PrivatePostCollateralAccountFundingHistory(request) + # + # { + # "records": [ + # { + # "market": "BTC_PERP", + # "fundingTime": "1708704000000", + # "fundingRate": "0.00017674", + # "fundingAmount": "-0.171053531892", + # "positionAmount": "0.019", + # "settlementPrice": "50938.2", + # "rateCalculatedTime": "1708675200000" + # }, + # ], + # "limit": 100, + # "offset": 0 + # } + # + data = self.safe_list(response, 'records', []) + return self.parse_funding_histories(data, market, since, limit) + + def parse_funding_history(self, contract, market: Market = None): + # + # { + # "market": "BTC_PERP", + # "fundingTime": "1708704000000", + # "fundingRate": "0.00017674", + # "fundingAmount": "-0.171053531892", + # "positionAmount": "0.019", + # "settlementPrice": "50938.2", + # "rateCalculatedTime": "1708675200000" + # } + # + marketId = self.safe_string(contract, 'market') + timestamp = self.safe_integer(contract, 'fundingTime') + return { + 'info': contract, + 'symbol': self.safe_symbol(marketId, market, None, 'swap'), + 'code': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': None, + 'amount': self.safe_number(contract, 'fundingAmount'), + } + + def parse_funding_histories(self, contracts, market=None, since: Int = None, limit: Int = None) -> List[FundingHistory]: + result = [] + for i in range(0, len(contracts)): + contract = contracts[i] + result.append(self.parse_funding_history(contract, market)) + sorted = self.sort_by(result, 'timestamp') + return self.filter_by_since_limit(sorted, since, limit) + + def fetch_deposits_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch history of deposits and withdrawals + + https://github.com/whitebit-exchange/api-docs/blob/main/pages/private/http-main-v4.md#get-depositwithdraw-history + + :param str [code]: unified currency code for the currency of the deposit/withdrawals, default is None + :param int [since]: timestamp in ms of the earliest deposit/withdrawal, default is None + :param int [limit]: max number of deposit/withdrawals to return, default = 50, Min: 1, Max: 100 + :param dict [params]: extra parameters specific to the exchange API endpoint + + EXCHANGE SPECIFIC PARAMETERS + :param number [params.transactionMethod]: Method. Example: 1 to display deposits / 2 to display withdraws. Do not send self parameter in order to receive both deposits and withdraws. + :param str [params.address]: Can be used for filtering transactions by specific address or memo. + :param str[] [params.addresses]: Can be used for filtering transactions by specific addresses or memos(max: 20). + :param str [params.uniqueId]: Can be used for filtering transactions by specific unique id + :param int [params.offset]: If you want the request to return entries starting from a particular line, you can use OFFSET clause to tell it where it should start. Default: 0, Min: 0, Max: 10000 + :param str[] [params.status]: Can be used for filtering transactions by status codes. Caution: You must use self parameter with appropriate transactionMethod and use valid status codes for self method. You can find them below. Example: "status": [3,7] + :returns dict: a list of `transaction structure ` + """ + self.load_markets() + request: dict = {} + currency = None + if code is not None: + currency = self.currency(code) + request['ticker'] = currency['id'] + if limit is not None: + request['limit'] = limit # default 1000 + response = self.v4PrivatePostMainAccountHistory(self.extend(request, params)) + # + # { + # "limit": 100, + # "offset": 0, + # "records": [ + # { + # "address": "3ApEASLcrQtZpg1TsssFgYF5V5YQJAKvuE", # deposit address + # "uniqueId": null, # unique Id of deposit + # "createdAt": 1593437922, # timestamp of deposit + # "currency": "Bitcoin", # deposit currency + # "ticker": "BTC", # deposit currency ticker + # "method": 1, # called method 1 - deposit, 2 - withdraw + # "amount": "0.0006", # amount of deposit + # "description": "", # deposit description + # "memo": "", # deposit memo + # "fee": "0", # deposit fee + # "status": 15, # transactions status + # "network": null, # if currency is multinetwork + # "transactionHash": "a275a514013e4e0f927fd0d1bed215e7f6f2c4c6ce762836fe135ec22529d886", # deposit transaction hash + # "transactionId": "5e112b38-9652-11ed-a1eb-0242ac120002", # transaction id + # "details": { + # "partial": { # details about partially successful withdrawals + # "requestAmount": "50000", # requested withdrawal amount + # "processedAmount": "39000", # processed withdrawal amount + # "processedFee": "273", # fee for processed withdrawal amount + # "normalizeTransaction": "" # deposit id + # } + # }, + # "confirmations": { # if transaction status == 15(Pending) you can see self object + # "actual": 1, # current block confirmations + # "required": 2 # required block confirmation for successful deposit + # } + # }, + # {...}, + # ], + # "total": 300 # total number of transactions, use self for calculating ‘limit’ and ‘offset' + # } + # + records = self.safe_list(response, 'records') + return self.parse_transactions(records, currency, since, limit) + + def fetch_convert_quote(self, fromCode: str, toCode: str, amount: Num = None, params={}) -> Conversion: + """ + fetch a quote for converting from one currency to another + + https://docs.whitebit.com/private/http-trade-v4/#convert-estimate + + :param str fromCode: the currency that you want to sell and convert from + :param str toCode: the currency that you want to buy and convert into + :param float amount: how much you want to trade in units of the from currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `conversion structure ` + """ + self.load_markets() + fromCurrency = self.currency(fromCode) + toCurrency = self.currency(toCode) + request: dict = { + 'from': fromCode, + 'to': toCode, + 'amount': self.number_to_string(amount), + 'direction': 'from', + } + response = self.v4PrivatePostConvertEstimate(self.extend(request, params)) + # + # { + # "give": "4", + # "receive": "0.00004762", + # "rate": "0.0000119", + # "id": "1740889", + # "expireAt": 1741090147, + # "from": "USDT", + # "to": "BTC" + # } + # + return self.parse_conversion(response, fromCurrency, toCurrency) + + def create_convert_trade(self, id: str, fromCode: str, toCode: str, amount: Num = None, params={}) -> Conversion: + """ + convert from one currency to another + + https://docs.whitebit.com/private/http-trade-v4/#convert-confirm + + :param str id: the id of the trade that you want to make + :param str fromCode: the currency that you want to sell and convert from + :param str toCode: the currency that you want to buy and convert into + :param float [amount]: how much you want to trade in units of the from currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `conversion structure ` + """ + self.load_markets() + fromCurrency = self.currency(fromCode) + toCurrency = self.currency(toCode) + request: dict = { + 'quoteId': id, + } + response = self.v4PrivatePostConvertConfirm(self.extend(request, params)) + # + # { + # "finalGive": "4", + # "finalReceive": "0.00004772" + # } + # + return self.parse_conversion(response, fromCurrency, toCurrency) + + def fetch_convert_trade_history(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Conversion]: + """ + fetch the users history of conversion trades + + https://docs.whitebit.com/private/http-trade-v4/#convert-history + + :param str [code]: the unified currency code + :param int [since]: the earliest time in ms to fetch conversions for + :param int [limit]: the maximum number of conversion structures to retrieve, default 20, max 200 + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.until]: the end time in ms + :param str [params.fromTicker]: the currency that you sold and converted from + :param str [params.toTicker]: the currency that you bought and converted into + :param str [params.quoteId]: the quote id of the conversion + :returns dict[]: a list of `conversion structures ` + """ + self.load_markets() + request: dict = {} + if code is not None: + request['fromTicker'] = code + if since is not None: + start = self.parse_to_int(since / 1000) + request['from'] = self.number_to_string(start) + if limit is not None: + request['limit'] = limit + request, params = self.handle_until_option('to', request, params, 0.001) + response = self.v4PrivatePostConvertHistory(self.extend(request, params)) + # + # { + # "records": [ + # { + # "id": "1741105", + # "path": [ + # { + # "from": "USDT", + # "to": "BTC", + # "rate": "0.00001193" + # } + # ], + # "date": 1741090757, + # "give": "4", + # "receive": "0.00004772", + # "rate": "0.00001193" + # } + # ], + # "total": 1, + # "limit": 100, + # "offset": 0 + # } + # + rows = self.safe_list(response, 'records', []) + return self.parse_conversions(rows, code, 'fromCurrency', 'toCurrency', since, limit) + + def parse_conversion(self, conversion: dict, fromCurrency: Currency = None, toCurrency: Currency = None) -> Conversion: + # + # fetchConvertQuote + # + # { + # "give": "4", + # "receive": "0.00004762", + # "rate": "0.0000119", + # "id": "1740889", + # "expireAt": 1741090147, + # "from": "USDT", + # "to": "BTC" + # } + # + # createConvertTrade + # + # { + # "finalGive": "4", + # "finalReceive": "0.00004772" + # } + # + # fetchConvertTradeHistory + # + # { + # "id": "1741105", + # "path": [ + # { + # "from": "USDT", + # "to": "BTC", + # "rate": "0.00001193" + # } + # ], + # "date": 1741090757, + # "give": "4", + # "receive": "0.00004772", + # "rate": "0.00001193" + # } + # + path = self.safe_list(conversion, 'path', []) + first = self.safe_dict(path, 0, {}) + fromPath = self.safe_string(first, 'from') + toPath = self.safe_string(first, 'to') + timestamp = self.safe_timestamp_2(conversion, 'date', 'expireAt') + fromCoin = self.safe_string(conversion, 'from', fromPath) + fromCode = self.safe_currency_code(fromCoin, fromCurrency) + toCoin = self.safe_string(conversion, 'to', toPath) + toCode = self.safe_currency_code(toCoin, toCurrency) + return { + 'info': conversion, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': self.safe_string(conversion, 'id'), + 'fromCurrency': fromCode, + 'fromAmount': self.safe_number_2(conversion, 'give', 'finalGive'), + 'toCurrency': toCode, + 'toAmount': self.safe_number_2(conversion, 'receive', 'finalReceive'), + 'price': self.safe_number(conversion, 'rate'), + 'fee': None, + } + + def fetch_position_history(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Position]: + """ + fetches historical positions + + https://docs.whitebit.com/private/http-trade-v4/#positions-history + + :param str symbol: unified contract symbol + :param int [since]: the earliest time in ms to fetch positions for + :param int [limit]: the maximum amount of records to fetch + :param dict [params]: extra parameters specific to the exchange api endpoint + :param int [params.positionId]: the id of the requested position + :returns dict[]: a list of `position structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'market': market['id'], + } + if since is not None: + request['startDate'] = since + if limit is not None: + request['limit'] = since + request, params = self.handle_until_option('endDate', request, params) + response = self.v4PrivatePostCollateralAccountPositionsHistory(self.extend(request, params)) + # + # [ + # { + # "positionId": 479975679, + # "market": "BTC_PERP", + # "openDate": 1741941025.309887, + # "modifyDate": 1741941025.309887, + # "amount": "0.001", + # "basePrice": "82498.7", + # "realizedFunding": "0", + # "liquidationPrice": "0", + # "liquidationState": null, + # "orderDetail": { + # "id": 1224727949521, + # "tradeAmount": "0.001", + # "price": "82498.7", + # "tradeFee": "0.028874545", + # "fundingFee": "0", + # "realizedPnl": "-0.028874545" + # } + # } + # ] + # + positions = self.parse_positions(response) + return self.filter_by_symbol_since_limit(positions, symbol, since, limit) + + def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://docs.whitebit.com/private/http-trade-v4/#open-positions + + :param str[] [symbols]: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structures ` + """ + self.load_markets() + symbols = self.market_symbols(symbols) + response = self.v4PrivatePostCollateralAccountPositionsOpen(params) + # + # [ + # { + # "positionId": 479975679, + # "market": "BTC_PERP", + # "openDate": 1741941025.3098869, + # "modifyDate": 1741941025.3098869, + # "amount": "0.001", + # "basePrice": "82498.7", + # "liquidationPrice": "70177.2", + # "pnl": "0", + # "pnlPercent": "0.00", + # "margin": "4.2", + # "freeMargin": "9.9", + # "funding": "0", + # "unrealizedFunding": "0", + # "liquidationState": null, + # "tpsl": null + # } + # ] + # + return self.parse_positions(response, symbols) + + def fetch_position(self, symbol: str, params={}) -> Position: + """ + fetch data on a single open contract trade position + + https://docs.whitebit.com/private/http-trade-v4/#open-positions + + :param str symbol: unified market symbol of the market the position is held in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `position structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = self.v4PrivatePostCollateralAccountPositionsOpen(self.extend(request, params)) + # + # [ + # { + # "positionId": 479975679, + # "market": "BTC_PERP", + # "openDate": 1741941025.3098869, + # "modifyDate": 1741941025.3098869, + # "amount": "0.001", + # "basePrice": "82498.7", + # "liquidationPrice": "70177.2", + # "pnl": "0", + # "pnlPercent": "0.00", + # "margin": "4.2", + # "freeMargin": "9.9", + # "funding": "0", + # "unrealizedFunding": "0", + # "liquidationState": null, + # "tpsl": null + # } + # ] + # + data = self.safe_dict(response, 0, {}) + return self.parse_position(data, market) + + def parse_position(self, position: dict, market: Market = None) -> Position: + # + # fetchPosition, fetchPositions + # + # { + # "positionId": 479975679, + # "market": "BTC_PERP", + # "openDate": 1741941025.3098869, + # "modifyDate": 1741941025.3098869, + # "amount": "0.001", + # "basePrice": "82498.7", + # "liquidationPrice": "70177.2", + # "pnl": "0", + # "pnlPercent": "0.00", + # "margin": "4.2", + # "freeMargin": "9.9", + # "funding": "0", + # "unrealizedFunding": "0", + # "liquidationState": null, + # "tpsl": null + # } + # + # fetchPositionHistory + # + # { + # "positionId": 479975679, + # "market": "BTC_PERP", + # "openDate": 1741941025.309887, + # "modifyDate": 1741941025.309887, + # "amount": "0.001", + # "basePrice": "82498.7", + # "realizedFunding": "0", + # "liquidationPrice": "0", + # "liquidationState": null, + # "orderDetail": { + # "id": 1224727949521, + # "tradeAmount": "0.001", + # "price": "82498.7", + # "tradeFee": "0.028874545", + # "fundingFee": "0", + # "realizedPnl": "-0.028874545" + # } + # } + # + marketId = self.safe_string(position, 'market') + timestamp = self.safe_timestamp(position, 'openDate') + tpsl = self.safe_dict(position, 'tpsl', {}) + orderDetail = self.safe_dict(position, 'orderDetail', {}) + return self.safe_position({ + 'info': position, + 'id': self.safe_string(position, 'positionId'), + 'symbol': self.safe_symbol(marketId, market), + 'notional': None, + 'marginMode': None, + 'liquidationPrice': self.safe_number(position, 'liquidationPrice'), + 'entryPrice': self.safe_number(position, 'basePrice'), + 'unrealizedPnl': self.safe_number(position, 'pnl'), + 'realizedPnl': self.safe_number(orderDetail, 'realizedPnl'), + 'percentage': self.safe_number(position, 'pnlPercent'), + 'contracts': None, + 'contractSize': None, + 'markPrice': None, + 'lastPrice': None, + 'side': None, + 'hedged': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastUpdateTimestamp': self.safe_timestamp(position, 'modifyDate'), + 'maintenanceMargin': None, + 'maintenanceMarginPercentage': None, + 'collateral': self.safe_number(position, 'margin'), + 'initialMargin': None, + 'initialMarginPercentage': None, + 'leverage': None, + 'marginRatio': None, + 'stopLossPrice': self.safe_number(tpsl, 'stopLoss'), + 'takeProfitPrice': self.safe_number(tpsl, 'takeProfit'), + }) + + def fetch_cross_borrow_rate(self, code: str, params={}) -> CrossBorrowRate: + """ + fetch the rate of interest to borrow a currency for margin trading + + https://docs.whitebit.com/private/http-main-v4/#get-plans + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `borrow rate structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'ticker': currency['id'], + } + response = self.v4PrivatePostMainAccountSmartPlans(self.extend(request, params)) + # + # + data = self.safe_list(response, 0, []) + return self.parse_borrow_rate(data, currency) + + def parse_borrow_rate(self, info, currency: Currency = None): + # + # + currencyId = self.safe_string(info, 'ticker') + percent = self.safe_string(info, 'percent') + return { + 'currency': self.safe_currency_code(currencyId, currency), + 'rate': self.parse_number(Precise.string_div(percent, '100')), + 'period': self.safe_integer(info, 'duration'), + 'timestamp': None, + 'datetime': None, + 'info': info, + } + + def is_fiat(self, currency: str) -> bool: + fiatCurrencies = self.safe_value(self.options, 'fiatCurrencies', []) + return self.in_array(currency, fiatCurrencies) + + def nonce(self): + return self.milliseconds() - self.options['timeDifference'] + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + query = self.omit(params, self.extract_params(path)) + version = self.safe_value(api, 0) + accessibility = self.safe_value(api, 1) + if headers is None: + headers = {} + headers['User-Agent'] = 'ccxt/' + self.id + '-' + self.version + pathWithParams = '/' + self.implode_params(path, params) + url = self.urls['api'][version][accessibility] + pathWithParams + if accessibility == 'public': + if query: + url += '?' + self.urlencode(query) + if accessibility == 'private': + self.check_required_credentials() + nonce = str(self.nonce()) + secret = self.encode(self.secret) + request = '/' + 'api' + '/' + version + pathWithParams + body = self.json(self.extend({'request': request, 'nonce': nonce}, params)) + payload = self.string_to_base64(body) + signature = self.hmac(self.encode(payload), secret, hashlib.sha512) + headers = { + 'Content-Type': 'application/json', + 'X-TXC-APIKEY': self.apiKey, + 'X-TXC-PAYLOAD': payload, + 'X-TXC-SIGNATURE': signature, + } + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if (code == 418) or (code == 429): + raise DDoSProtection(self.id + ' ' + str(code) + ' ' + reason + ' ' + body) + if code == 404: + raise ExchangeError(self.id + ' ' + str(code) + ' endpoint not found') + if response is not None: + # For cases where we have a meaningful status + # {"response":null,"status":422,"errors":{"orderId":["Finished order id 435453454535 not found on your account"]},"notification":null,"warning":"Finished order id 435453454535 not found on your account","_token":null} + status = self.safe_string(response, 'status') + errors = self.safe_value(response, 'errors') + # {"code":10,"message":"Unauthorized request."} + message = self.safe_string(response, 'message') + # For these cases where we have a generic code variable error key + # {"code":0,"message":"Validation failed","errors":{"amount":["Amount must be greater than 0"]}} + codeNew = self.safe_integer(response, 'code') + hasErrorStatus = status is not None and status != '200' and errors is not None + if hasErrorStatus or codeNew is not None: + feedback = self.id + ' ' + body + errorInfo = message + if hasErrorStatus: + errorInfo = status + else: + errorObject = self.safe_dict(response, 'errors', {}) + errorKeys = list(errorObject.keys()) + errorsLength = len(errorKeys) + if errorsLength > 0: + errorKey = errorKeys[0] + errorMessageArray = self.safe_value(errorObject, errorKey, []) + errorMessageLength = len(errorMessageArray) + errorInfo = errorMessageArray[0] if (errorMessageLength > 0) else body + self.throw_exactly_matched_exception(self.exceptions['exact'], errorInfo, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], body, feedback) + raise ExchangeError(feedback) + return None diff --git a/ccxt/woo.py b/ccxt/woo.py new file mode 100644 index 0000000..fb10a39 --- /dev/null +++ b/ccxt/woo.py @@ -0,0 +1,4047 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.woo import ImplicitAPI +import hashlib +from ccxt.base.types import Account, Any, Balances, Bool, Conversion, Currencies, Currency, DepositAddress, Int, LedgerEntry, Leverage, MarginModification, Market, MarketType, Num, Order, OrderBook, OrderSide, OrderType, Position, Str, Strings, FundingRate, FundingRates, Trade, TradingFeeInterface, TradingFees, Transaction, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import NotSupported +from ccxt.base.errors import OperationFailed +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import OnMaintenance +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class woo(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(woo, self).describe(), { + 'id': 'woo', + 'name': 'WOO X', + 'countries': ['KY'], # Cayman Islands + 'rateLimit': 100, + 'version': 'v1', + 'certified': True, + 'pro': True, + 'hostname': 'woox.io', + 'has': { + 'CORS': None, + 'spot': True, + 'margin': True, + 'swap': True, + 'future': False, + 'option': False, + 'addMargin': True, + 'cancelAllOrders': True, + 'cancelAllOrdersAfter': True, + 'cancelOrder': True, + 'cancelWithdraw': False, # exchange have that endpoint disabled atm, but was once implemented in ccxt per old docs: https://docx.woo.io/wootrade-documents/#cancel-withdraw-request + 'closeAllPositions': False, + 'closePosition': False, + 'createConvertTrade': True, + 'createDepositAddress': False, + 'createMarketBuyOrderWithCost': True, + 'createMarketOrder': False, + 'createMarketOrderWithCost': False, + 'createMarketSellOrderWithCost': True, + 'createOrder': True, + 'createOrderWithTakeProfitAndStopLoss': True, + 'createReduceOnlyOrder': True, + 'createStopLimitOrder': False, + 'createStopLossOrder': True, + 'createStopMarketOrder': False, + 'createStopOrder': False, + 'createTakeProfitOrder': True, + 'createTrailingAmountOrder': True, + 'createTrailingPercentOrder': True, + 'createTriggerOrder': True, + 'fetchAccounts': True, + 'fetchBalance': True, + 'fetchCanceledOrders': False, + 'fetchClosedOrder': False, + 'fetchClosedOrders': True, + 'fetchConvertCurrencies': True, + 'fetchConvertQuote': True, + 'fetchConvertTrade': True, + 'fetchConvertTradeHistory': True, + 'fetchCurrencies': True, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchDepositsWithdrawals': True, + 'fetchFundingHistory': True, + 'fetchFundingInterval': True, + 'fetchFundingIntervals': False, + 'fetchFundingRate': True, + 'fetchFundingRateHistory': True, + 'fetchFundingRates': True, + 'fetchIndexOHLCV': False, + 'fetchLedger': True, + 'fetchLeverage': True, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterestHistory': False, + 'fetchOpenOrder': False, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchOrderTrades': True, + 'fetchPosition': True, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': True, + 'fetchPositionsHistory': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchStatus': True, + 'fetchTicker': False, + 'fetchTickers': False, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': True, + 'fetchTradingFees': True, + 'fetchTransactions': 'emulated', + 'fetchTransfers': True, + 'fetchWithdrawals': True, + 'reduceMargin': False, + 'sandbox': True, + 'setLeverage': True, + 'setMargin': False, + 'setPositionMode': True, + 'transfer': True, + 'withdraw': True, # exchange have that endpoint disabled atm, but was once implemented in ccxt per old docs: https://docx.woo.io/wootrade-documents/#token-withdraw + }, + 'timeframes': { + '1m': '1m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1h', + '4h': '4h', + '12h': '12h', + '1d': '1d', + '1w': '1w', + '1M': '1mon', + '1y': '1y', + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/150730761-1a00e5e0-d28c-480f-9e65-089ce3e6ef3b.jpg', + 'api': { + 'pub': 'https://api-pub.woox.io', + 'public': 'https://api.{hostname}', + 'private': 'https://api.{hostname}', + }, + 'test': { + 'pub': 'https://api-pub.staging.woox.io', + 'public': 'https://api.staging.woox.io', + 'private': 'https://api.staging.woox.io', + }, + 'www': 'https://woox.io/', + 'doc': [ + 'https://docs.woox.io/', + ], + 'fees': [ + 'https://support.woox.io/hc/en-001/articles/4404611795353--Trading-Fees', + ], + 'referral': { + 'url': 'https://woox.io/register?ref=DIJT0CNL', + 'discount': 0.35, + }, + }, + 'api': { + 'v1': { + 'pub': { + 'get': { + 'hist/kline': 10, + 'hist/trades': 10, + }, + }, + 'public': { + 'get': { + 'info': 1, + 'info/{symbol}': 1, + 'system_info': 1, + 'market_trades': 1, + 'token': 1, + 'token_network': 1, + 'funding_rates': 1, + 'funding_rate/{symbol}': 1, + 'funding_rate_history': 1, + 'futures': 1, + 'futures/{symbol}': 1, + 'orderbook/{symbol}': 1, + 'kline': 1, + }, + }, + 'private': { + 'get': { + 'client/token': 1, + 'order/{oid}': 1, + 'client/order/{client_order_id}': 1, + 'orders': 1, + 'client/trade/{tid}': 1, + 'order/{oid}/trades': 1, + 'client/trades': 1, + 'client/hist_trades': 1, + 'staking/yield_history': 1, + 'client/holding': 1, + 'asset/deposit': 10, + 'asset/history': 60, + 'sub_account/all': 60, + 'sub_account/assets': 60, + 'sub_account/asset_detail': 60, + 'sub_account/ip_restriction': 10, + 'asset/main_sub_transfer_history': 30, + 'token_interest': 60, + 'token_interest/{token}': 60, + 'interest/history': 60, + 'interest/repay': 60, + 'funding_fee/history': 30, + 'positions': 3.33, # 30 requests per 10 seconds + 'position/{symbol}': 3.33, + 'client/transaction_history': 60, + 'client/futures_leverage': 60, + }, + 'post': { + 'order': 1, # 10 requests per 1 second per symbol + 'order/cancel_all_after': 1, + 'asset/ltv': 30, + 'asset/internal_withdraw': 30, + 'interest/repay': 60, + 'client/account_mode': 120, + 'client/position_mode': 5, + 'client/leverage': 120, + 'client/futures_leverage': 30, + 'client/isolated_margin': 30, + }, + 'delete': { + 'order': 1, + 'client/order': 1, + 'orders': 1, + 'asset/withdraw': 120, # implemented in ccxt, disabled on the exchange side https://docx.woo.io/wootrade-documents/#cancel-withdraw-request + }, + }, + }, + 'v2': { + 'private': { + 'get': { + 'client/holding': 1, + }, + }, + }, + 'v3': { + 'public': { + 'get': { + 'systemInfo': 1, # 10/1s + 'instruments': 1, # 10/1s + 'token': 1, # 10/1s + 'tokenNetwork': 1, # 10/1s + 'tokenInfo': 1, # 10/1s + 'marketTrades': 1, # 10/1s + 'marketTradesHistory': 1, # 10/1s + 'orderbook': 1, # 10/1s + 'kline': 1, # 10/1s + 'klineHistory': 1, # 10/1s + 'futures': 1, # 10/1s + 'fundingRate': 1, # 10/1s + 'fundingRateHistory': 1, # 10/1s + 'insuranceFund': 1, # 10/1s + }, + }, + 'private': { + 'get': { + 'trade/order': 2, # 5/1s + 'trade/orders': 1, # 10/1s + 'trade/algoOrder': 1, # 10/1s + 'trade/algoOrders': 1, # 10/1s + 'trade/transaction': 1, # 10/1s + 'trade/transactionHistory': 5, # 2/1s + 'trade/tradingFee': 5, # 2/1s + 'account/info': 60, # 10/60s + 'account/tokenConfig': 1, # 10/1s + 'account/symbolConfig': 1, # 10/1s + 'account/subAccounts/all': 60, # 10/60s + 'account/referral/summary': 60, # 10/60s + 'account/referral/rewardHistory': 60, # 10/60s + 'account/credentials': 60, # 10/60s + 'asset/balances': 1, # 10/1s + 'asset/token/history': 60, # 10/60s + 'asset/transfer/history': 30, # 20/60s + 'asset/wallet/history': 60, # 10/60s + 'asset/wallet/deposit': 60, # 10/60s + 'asset/staking/yieldHistory': 60, # 10/60s + 'futures/positions': 3.33, # 30/10s + 'futures/leverage': 60, # 10/60s + 'futures/defaultMarginMode': 60, # 10/60s + 'futures/fundingFee/history': 30, # 20/60s + 'spotMargin/interestRate': 60, # 10/60s + 'spotMargin/interestHistory': 60, # 10/60s + 'spotMargin/maxMargin': 60, # 10/60s + 'algo/order/{oid}': 1, + 'algo/orders': 1, + 'positions': 3.33, + 'buypower': 1, + 'convert/exchangeInfo': 1, + 'convert/assetInfo': 1, + 'convert/rfq': 60, + 'convert/trade': 1, + 'convert/trades': 1, + }, + 'post': { + 'trade/order': 2, # 5/1s + 'trade/algoOrder': 5, # 2/1s + 'trade/cancelAllAfter': 1, # 10/1s + 'account/tradingMode': 120, # 5/60s + 'account/listenKey': 20, # 5/10s + 'asset/transfer': 30, # 20/60s + 'asset/wallet/withdraw': 60, # 10/60s + 'spotMargin/leverage': 120, # 5/60s + 'spotMargin/interestRepay': 60, # 10/60s + 'algo/order': 5, + 'convert/rft': 60, + }, + 'put': { + 'trade/order': 2, # 5/1s + 'trade/algoOrder': 2, # 5/1s + 'futures/leverage': 60, # 10/60s + 'futures/positionMode': 120, # 5/60s + 'order/{oid}': 2, + 'order/client/{client_order_id}': 2, + 'algo/order/{oid}': 2, + 'algo/order/client/{client_order_id}': 2, + }, + 'delete': { + 'trade/order': 1, # 10/1s + 'trade/orders': 1, # 10/1s + 'trade/algoOrder': 1, # 10/1s + 'trade/algoOrders': 1, # 10/1s + 'trade/allOrders': 1, # 10/1s + 'algo/order/{order_id}': 1, + 'algo/orders/pending': 1, + 'algo/orders/pending/{symbol}': 1, + 'orders/pending': 1, + }, + }, + }, + }, + 'fees': { + 'trading': { + 'tierBased': True, + 'percentage': True, + 'maker': self.parse_number('0.0002'), + 'taker': self.parse_number('0.0005'), + }, + }, + 'options': { + 'timeDifference': 0, # the difference between system clock and exchange clock + 'adjustForTimeDifference': False, # controls the adjustment logic upon instantiation + 'sandboxMode': False, + 'createMarketBuyOrderRequiresPrice': True, + # these network aliases require manual mapping here + 'network-aliases-for-tokens': { + 'HT': 'ERC20', + 'OMG': 'ERC20', + 'UATOM': 'ATOM', + 'ZRX': 'ZRX', + }, + 'networks': { + 'TRX': 'TRON', + 'TRC20': 'TRON', + 'ERC20': 'ETH', + 'BEP20': 'BSC', + 'ARB': 'Arbitrum', + }, + 'networksById': { + 'TRX': 'TRC20', + 'TRON': 'TRC20', + }, + # override defaultNetworkCodePriorities for a specific currency + 'defaultNetworkCodeForCurrencies': { + # 'USDT': 'TRC20', + # 'BTC': 'BTC', + }, + 'transfer': { + 'fillResponseFromRequest': True, + }, + 'brokerId': 'bc830de7-50f3-460b-9ee0-f430f83f9dad', + }, + 'features': { + 'default': { + 'sandbox': True, + 'createOrder': { + 'marginMode': True, + 'triggerPrice': True, + 'triggerPriceType': { + 'last': True, + 'mark': True, + 'index': False, + }, + 'triggerDirection': False, + 'stopLossPrice': False, # todo by triggerPrice + 'takeProfitPrice': False, # todo by triggerPrice + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': True, + }, + 'hedged': False, + 'trailing': True, + 'leverage': False, + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': False, + 'iceberg': True, # todo implement + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 500, + 'daysBack': 90, + 'untilDays': 10000, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': True, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 500, + 'trigger': True, + 'trailing': True, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': False, + 'limit': 500, + 'daysBack': None, + 'untilDays': 100000, + 'trigger': True, + 'trailing': True, + 'symbolRequired': False, + }, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 500, + 'daysBack': None, + 'daysBackCanceled': None, + 'untilDays': 100000, + 'trigger': True, + 'trailing': True, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 1000, + }, + }, + 'spot': { + 'extends': 'default', + }, + 'forSwap': { + 'extends': 'default', + 'createOrder': { + 'hedged': True, + }, + }, + 'swap': { + 'linear': { + 'extends': 'forSwap', + }, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'commonCurrencies': {}, + 'exceptions': { + 'exact': { + '-1000': OperationFailed, # {"code": -1000, "message": "An unknown error occurred while processing the request"} or {"success":false,"code":"-1000","message":"An internal error has occurred. We are unable to process your request. Please try again later."} + '-1001': AuthenticationError, # {"code": -1001, "message": "The api key or secret is in wrong format"} + '-1002': AuthenticationError, # {"code": -1002, "message": "API key or secret is invalid, it may because key have insufficient permission or the key is expired/revoked."} + '-1003': RateLimitExceeded, # {"code": -1003, "message": "Rate limit exceed."} + '-1004': BadRequest, # {"code": -1004, "message": "An unknown parameter was sent."} + '-1005': BadRequest, # {"code": -1005, "message": "Some parameters are in wrong format for api."} + '-1006': BadRequest, # {"code": -1006, "message": "The data is not found in server."} + '-1007': BadRequest, # {"code": -1007, "message": "The data is already exists or your request is duplicated."} + '-1008': InvalidOrder, # {"code": -1008, "message": "The quantity of settlement is too high than you can request."} + '-1009': BadRequest, # {"code": -1009, "message": "Can not request withdrawal settlement, you need to deposit other arrears first."} + '-1012': BadRequest, # {"code": -1012, "message": "Amount is required for buy market orders when margin disabled."} The place/cancel order request is rejected by internal module, it may because the account is in liquidation or other internal errors. Please try again in a few seconds."} + '-1101': InvalidOrder, # {"code": -1101, "message": "The risk exposure for client is too high, it may cause by sending too big order or the leverage is too low. please refer to client info to check the current exposure."} + '-1102': InvalidOrder, # {"code": -1102, "message": "The order value(price * size) is too small."} + '-1103': InvalidOrder, # {"code": -1103, "message": "The order price is not following the tick size rule for the symbol."} + '-1104': InvalidOrder, # {"code": -1104, "message": "The order quantity is not following the step size rule for the symbol."} + '-1105': InvalidOrder, # {"code": -1105, "message": "Price is X% too high or X% too low from the mid price."} + }, + 'broad': { + 'Can not place': ExchangeError, # {"code": -1011, "message": "Can not place/cancel orders, it may because internal network error. Please try again in a few seconds."} + 'maintenance': OnMaintenance, # {"code":"-1011","message":"The system is under maintenance.","success":false} + 'symbol must not be blank': BadRequest, # when sending 'cancelOrder' without symbol [-1005] + 'The token is not supported': BadRequest, # when getting incorrect token's deposit address [-1005] + 'Your order and symbol are not valid or already canceled': BadRequest, # actual response whensending 'cancelOrder' for already canceled id [-1006] + 'Insufficient WOO. Please enable margin trading for leverage trading': BadRequest, # when selling insufficent token [-1012] + }, + }, + 'precisionMode': TICK_SIZE, + }) + + def fetch_status(self, params={}): + """ + the latest known information on the availability of the exchange API + + https://developer.woox.io/api-reference/endpoint/public_data/systemInfo + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `status structure ` + """ + response = self.v3PublicGetSystemInfo(params) + # + # { + # "success": True, + # "data": { + # "status": 0, + # "msg": "System is functioning properly.", + # "estimatedEndTime": 1749963600362 + # }, + # "timestamp": 1751442989564 + # } + # + data = self.safe_dict(response, 'data', {}) + status = self.safe_string(data, 'status') + if status is None: + status = 'error' + elif status == '0': + status = 'ok' + else: + status = 'maintenance' + return { + 'status': status, + 'updated': None, + 'eta': None, + 'url': None, + 'info': response, + } + + def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://developer.woox.io/api-reference/endpoint/public_data/systemInfo + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = self.v3PublicGetSystemInfo(params) + # + # { + # "success": True, + # "data": { + # "status": 0, + # "msg": "System is functioning properly.", + # "estimatedEndTime": 1749963600362 + # }, + # "timestamp": 1751442989564 + # } + # + return self.safe_integer(response, 'timestamp') + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for woo + + https://developer.woox.io/api-reference/endpoint/public_data/instruments + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + if self.options['adjustForTimeDifference']: + self.load_time_difference() + response = self.v3PublicGetInstruments(params) + # + # { + # "success": True, + # "data": { + # "rows": [ + # { + # "symbol": "SPOT_AAVE_USDT", + # "status": "TRADING", + # "baseAsset": "AAVE", + # "baseAssetMultiplier": 1, + # "quoteAsset": "USDT", + # "quoteMin": "0", + # "quoteMax": "100000", + # "quoteTick": "0.01", + # "baseMin": "0.005", + # "baseMax": "5000", + # "baseTick": "0.0001", + # "minNotional": "1", + # "bidCapRatio": "1.1", + # "bidFloorRatio": null, + # "askCapRatio": null, + # "askFloorRatio": "0.9", + # "orderMode": "NORMAL", + # "impactNotional": null, + # "isAllowedRpi": False, + # "tickGranularity": null + # } + # ] + # }, + # "timestamp": 1751512951338 + # } + # + data = self.safe_dict(response, 'data', {}) + rows = self.safe_list(data, 'rows', []) + return self.parse_markets(rows) + + def parse_market(self, market: dict) -> Market: + marketId = self.safe_string(market, 'symbol') + parts = marketId.split('_') + first = self.safe_string(parts, 0) + marketType: MarketType + spot = False + swap = False + if first == 'SPOT': + spot = True + marketType = 'spot' + elif first == 'PERP': + swap = True + marketType = 'swap' + baseId = self.safe_string(parts, 1) + quoteId = self.safe_string(parts, 2) + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + settleId: Str = None + settle: Str = None + symbol = base + '/' + quote + contractSize: Num = None + linear: Bool = None + inverse: Bool = None + margin = True + contract = swap + if contract: + margin = False + settleId = self.safe_string(parts, 2) + settle = self.safe_currency_code(settleId) + symbol = base + '/' + quote + ':' + settle + contractSize = self.parse_number('1') + linear = True + inverse = False + active = self.safe_string(market, 'status') == 'TRADING' + return { + 'id': marketId, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': marketType, + 'spot': spot, + 'margin': margin, + 'swap': swap, + 'future': False, + 'option': False, + 'active': active, + 'contract': contract, + 'linear': linear, + 'inverse': inverse, + 'contractSize': contractSize, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.safe_number(market, 'baseTick'), + 'price': self.safe_number(market, 'quoteTick'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(market, 'baseMin'), + 'max': self.safe_number(market, 'baseMax'), + }, + 'price': { + 'min': self.safe_number(market, 'quoteMin'), + 'max': self.safe_number(market, 'quoteMax'), + }, + 'cost': { + 'min': self.safe_number(market, 'minNotional'), + 'max': None, + }, + }, + 'created': None, + 'info': market, + } + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://developer.woox.io/api-reference/endpoint/public_data/marketTrades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['limit'] = limit + response = self.v3PublicGetMarketTrades(self.extend(request, params)) + # + # { + # "success": True, + # "data": { + # "rows": [ + # { + # "symbol": "SPOT_BTC_USDT", + # "side": "SELL", + # "source": 0, + # "executedPrice": "108741.01", + # "executedQuantity": "0.02477", + # "executedTimestamp": 1751513940144 + # } + # ] + # }, + # "timestamp": 1751513988543 + # } + # + data = self.safe_dict(response, 'data', {}) + rows = self.safe_list(data, 'rows', []) + return self.parse_trades(rows, market, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # public/market_trades + # + # { + # "symbol": "SPOT_BTC_USDT", + # "side": "SELL", + # "source": 0, + # "executedPrice": "108741.01", + # "executedQuantity": "0.02477", + # "executedTimestamp": 1751513940144 + # } + # + # fetchOrderTrades, fetchOrder + # + # { + # "id": 1734947821, + # "symbol": "SPOT_LTC_USDT", + # "orderId": 60780383217, + # "executedPrice": 87.86, + # "executedQuantity": 0.1, + # "fee": 0.0001, + # "realizedPnl": null, + # "feeAsset": "LTC", + # "orderTag": "default", + # "side": "BUY", + # "executedTimestamp": "1752055173.630", + # "isMaker": 0 + # } + # + isFromFetchOrder = ('id' in trade) + timestampString = self.safe_string_2(trade, 'executed_timestamp', 'executedTimestamp') + timestamp = None + if timestampString is not None: + if timestampString.find('.') > -1: + timestamp = self.safe_timestamp_2(trade, 'executed_timestamp', 'executedTimestamp') + else: + timestamp = self.safe_integer(trade, 'executedTimestamp') + marketId = self.safe_string(trade, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + price = self.safe_string_2(trade, 'executed_price', 'executedPrice') + amount = self.safe_string_2(trade, 'executed_quantity', 'executedQuantity') + order_id = self.safe_string_2(trade, 'order_id', 'orderId') + fee = self.parse_token_and_fee_temp(trade, ['fee_asset', 'feeAsset'], ['fee']) + feeCost = self.safe_string(fee, 'cost') + if feeCost is not None: + fee['cost'] = feeCost + cost = Precise.string_mul(price, amount) + side = self.safe_string_lower(trade, 'side') + id = self.safe_string(trade, 'id') + takerOrMaker: Str = None + if isFromFetchOrder: + isMaker = self.safe_string_2(trade, 'is_maker', 'isMaker') == '1' + takerOrMaker = 'maker' if isMaker else 'taker' + return self.safe_trade({ + 'id': id, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'side': side, + 'price': price, + 'amount': amount, + 'cost': cost, + 'order': order_id, + 'takerOrMaker': takerOrMaker, + 'type': None, + 'fee': fee, + 'info': trade, + }, market) + + def parse_token_and_fee_temp(self, item, feeTokenKeys, feeAmountKeys): + feeCost = self.safe_string_n(item, feeAmountKeys) + fee = None + if feeCost is not None: + feeCurrencyId = self.safe_string_n(item, feeTokenKeys) + feeCurrencyCode = self.safe_currency_code(feeCurrencyId) + fee = { + 'cost': feeCost, + 'currency': feeCurrencyCode, + } + return fee + + def parse_trading_fee(self, fee: dict, market: Market = None) -> TradingFeeInterface: + marketId = self.safe_string(fee, 'symbol') + symbol = self.safe_symbol(marketId, market) + return { + 'info': fee, + 'symbol': symbol, + 'maker': self.parse_number(Precise.string_div(self.safe_string(fee, 'makerFee'), '100')), + 'taker': self.parse_number(Precise.string_div(self.safe_string(fee, 'takerFee'), '100')), + 'percentage': None, + 'tierBased': None, + } + + def fetch_trading_fee(self, symbol: str, params={}) -> TradingFeeInterface: + """ + fetch the trading fees for a market + + https://developer.woox.io/api-reference/endpoint/trading/get_tradingFee + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.portfolioMargin]: set to True if you would like to fetch trading fees in a portfolio margin account + :param str [params.subType]: "linear" or "inverse" + :returns dict: a `fee structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = self.v3PrivateGetTradeTradingFee(self.extend(request, params)) + # + # { + # "success": True, + # "data": { + # "symbol": "SPOT_BTC_USDT", + # "takerFee": "10", + # "makerFee": "8" + # }, + # "timestamp": 1751858977368 + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_trading_fee(data, market) + + def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://developer.woox.io/api-reference/endpoint/account/get_account_info + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + self.load_markets() + response = self.v3PrivateGetAccountInfo(params) + # + # { + # "success": True, + # "data": { + # "applicationId": "251bf5c4-f3c8-4544-bb8b-80001007c3c0", + # "account": "carlos_jose_lima@yahoo.com", + # "alias": "carlos_jose_lima@yahoo.com", + # "otpauth": True, + # "accountMode": "FUTURES", + # "positionMode": "ONE_WAY", + # "leverage": 0, + # "makerFeeRate": 0, + # "takerFeeRate": 0, + # "marginRatio": "10", + # "openMarginRatio": "10", + # "initialMarginRatio": "10", + # "maintenanceMarginRatio": "0.03", + # "totalCollateral": "165.55629469", + # "freeCollateral": "165.55629469", + # "totalAccountValue": "167.32418611", + # "totalTradingValue": "167.32418611", + # "totalVaultValue": "0", + # "totalStakingValue": "0", + # "totalLaunchpadValue": "0", + # "totalEarnValue": "0", + # "referrerID": null, + # "accountType": "Main" + # }, + # "timestamp": 1752062807915 + # } + # + data = self.safe_dict(response, 'data', {}) + maker = self.safe_string(data, 'makerFeeRate') + taker = self.safe_string(data, 'takerFeeRate') + result: dict = {} + for i in range(0, len(self.symbols)): + symbol = self.symbols[i] + result[symbol] = { + 'info': response, + 'symbol': symbol, + 'maker': self.parse_number(Precise.string_div(maker, '10000')), + 'taker': self.parse_number(Precise.string_div(taker, '10000')), + 'percentage': True, + 'tierBased': True, + } + return result + + def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://docs.woox.io/#available-token-public + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + result: dict = {} + tokenResponsePromise = self.v1PublicGetToken(params) + # + # { + # "rows": [ + # { + # "token": "ETH_USDT", + # "fullname": "Tether", + # "network": "ETH", + # "decimals": "6", + # "delisted": False, + # "balance_token": "USDT", + # "created_time": "1710123398", + # "updated_time": "1746528481", + # "can_collateral": True, + # "can_short": True + # }, + # { + # "token": "BSC_USDT", + # "fullname": "Tether", + # "network": "BSC", + # "decimals": "18", + # "delisted": False, + # "balance_token": "USDT", + # "created_time": "1710123395", + # "updated_time": "1746528601", + # "can_collateral": True, + # "can_short": True + # }, + # { + # "token": "ALGO", + # "fullname": "Algorand", + # "network": "ALGO", + # "decimals": "6", + # "delisted": False, + # "balance_token": "ALGO", + # "created_time": "1710123394", + # "updated_time": "1723087518", + # "can_collateral": True, + # "can_short": True + # }, + # ... + # ], + # "success": True + # } + # + # only make one request for currrencies... + tokenNetworkResponsePromise = self.v1PublicGetTokenNetwork(params) + # + # { + # "rows": [ + # { + # "protocol": "ERC20", + # "network": "ETH", + # "token": "USDT", + # "name": "Ethereum(ERC20)", + # "minimum_withdrawal": "10.00000000", + # "withdrawal_fee": "2.00000000", + # "allow_deposit": "1", + # "allow_withdraw": "1" + # }, + # { + # "protocol": "TRC20", + # "network": "TRX", + # "token": "USDT", + # "name": "Tron(TRC20)", + # "minimum_withdrawal": "10.00000000", + # "withdrawal_fee": "4.50000000", + # "allow_deposit": "1", + # "allow_withdraw": "1" + # }, + # ... + # ], + # "success": True + # } + # + tokenResponse, tokenNetworkResponse = [tokenResponsePromise, tokenNetworkResponsePromise] + tokenRows = self.safe_list(tokenResponse, 'rows', []) + tokenNetworkRows = self.safe_list(tokenNetworkResponse, 'rows', []) + networksById = self.group_by(tokenNetworkRows, 'token') + tokensById = self.group_by(tokenRows, 'balance_token') + currencyIds = list(tokensById.keys()) + for i in range(0, len(currencyIds)): + currencyId = currencyIds[i] + code = self.safe_currency_code(currencyId) + tokensByNetworkId = self.index_by(tokensById[currencyId], 'network') + chainsByNetworkId = self.index_by(networksById[currencyId], 'network') + keys = list(chainsByNetworkId.keys()) + resultingNetworks: dict = {} + for j in range(0, len(keys)): + networkId = keys[j] + tokenEntry = self.safe_dict(tokensByNetworkId, networkId, {}) + networkEntry = self.safe_dict(chainsByNetworkId, networkId, {}) + networkCode = self.network_id_to_code(networkId, code) + specialNetworkId = self.safe_string(tokenEntry, 'token') + resultingNetworks[networkCode] = { + 'id': networkId, + 'currencyNetworkId': specialNetworkId, # exchange uses special crrency-ids(coin + network junction) + 'network': networkCode, + 'active': None, + 'deposit': self.safe_string(networkEntry, 'allow_deposit') == '1', + 'withdraw': self.safe_string(networkEntry, 'allow_withdraw') == '1', + 'fee': self.safe_number(networkEntry, 'withdrawal_fee'), + 'precision': self.parse_number(self.parse_precision(self.safe_string(tokenEntry, 'decimals'))), + 'limits': { + 'withdraw': { + 'min': self.safe_number(networkEntry, 'minimum_withdrawal'), + 'max': None, + }, + 'deposit': { + 'min': None, + 'max': None, + }, + }, + 'info': [networkEntry, tokenEntry], + } + result[code] = self.safe_currency_structure({ + 'id': currencyId, + 'name': None, + 'code': code, + 'precision': None, + 'active': None, + 'fee': None, + 'networks': resultingNetworks, + 'deposit': None, + 'withdraw': None, + 'type': 'crypto', + 'limits': { + 'deposit': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + }, + 'info': [tokensByNetworkId, chainsByNetworkId], + }) + return result + + def create_market_buy_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market buy order by providing the symbol and cost + + https://docs.woox.io/#send-order + + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' createMarketBuyOrderWithCost() supports spot orders only') + return self.create_order(symbol, 'market', 'buy', cost, 1, params) + + def create_market_sell_order_with_cost(self, symbol: str, cost: float, params={}): + """ + create a market sell order by providing the symbol and cost + + https://docs.woox.io/#send-order + + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' createMarketSellOrderWithCost() supports spot orders only') + return self.create_order(symbol, 'market', 'sell', cost, 1, params) + + def create_trailing_amount_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, trailingAmount: Num = None, trailingTriggerPrice: Num = None, params={}) -> Order: + """ + create a trailing order by providing the symbol, type, side, amount, price and trailingAmount + + https://docs.woox.io/#send-algo-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency, or number of contracts + :param float [price]: the price for the order to be filled at, in units of the quote currency, ignored in market orders + :param float trailingAmount: the quote amount to trail away from the current market price + :param float trailingTriggerPrice: the price to activate a trailing order, default uses the price argument + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + if trailingAmount is None: + raise ArgumentsRequired(self.id + ' createTrailingAmountOrder() requires a trailingAmount argument') + if trailingTriggerPrice is None: + raise ArgumentsRequired(self.id + ' createTrailingAmountOrder() requires a trailingTriggerPrice argument') + params['trailingAmount'] = trailingAmount + params['trailingTriggerPrice'] = trailingTriggerPrice + return self.create_order(symbol, type, side, amount, price, params) + + def create_trailing_percent_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, trailingPercent: Num = None, trailingTriggerPrice: Num = None, params={}) -> Order: + """ + create a trailing order by providing the symbol, type, side, amount, price and trailingPercent + + https://docs.woox.io/#send-algo-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency, or number of contracts + :param float [price]: the price for the order to be filled at, in units of the quote currency, ignored in market orders + :param float trailingPercent: the percent to trail away from the current market price + :param float trailingTriggerPrice: the price to activate a trailing order, default uses the price argument + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + if trailingPercent is None: + raise ArgumentsRequired(self.id + ' createTrailingPercentOrder() requires a trailingPercent argument') + if trailingTriggerPrice is None: + raise ArgumentsRequired(self.id + ' createTrailingPercentOrder() requires a trailingTriggerPrice argument') + params['trailingPercent'] = trailingPercent + params['trailingTriggerPrice'] = trailingTriggerPrice + return self.create_order(symbol, type, side, amount, price, params) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://developer.woox.io/api-reference/endpoint/trading/post_order + https://developer.woox.io/api-reference/endpoint/trading/post_algo_order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: *for swap markets only* 'cross' or 'isolated', default 'cross' + :param float [params.triggerPrice]: The price a trigger order is triggered at + :param dict [params.takeProfit]: *takeProfit object in params* containing the triggerPrice at which the attached take profit order will be triggered(perpetual swap markets only) + :param float [params.takeProfit.triggerPrice]: take profit trigger price + :param dict [params.stopLoss]: *stopLoss object in params* containing the triggerPrice at which the attached stop loss order will be triggered(perpetual swap markets only) + :param float [params.stopLoss.triggerPrice]: stop loss trigger price + :param float [params.algoType]: 'STOP' or 'TRAILING_STOP' or 'OCO' or 'CLOSE_POSITION' + :param float [params.cost]: *spot market buy only* the quote quantity that can be used alternative for the amount + :param str [params.trailingAmount]: the quote amount to trail away from the current market price + :param str [params.trailingPercent]: the percent to trail away from the current market price + :param str [params.trailingTriggerPrice]: the price to trigger a trailing order, default uses the price argument + :param str [params.position_side]: 'SHORT' or 'LONG' - if position mode is HEDGE_MODE and the trading involves futures, then is required, otherwise self parameter is not required + :returns dict: an `order structure ` + """ + reduceOnly = self.safe_bool_2(params, 'reduceOnly', 'reduce_only') + params = self.omit(params, ['reduceOnly', 'reduce_only']) + orderType = type.upper() + self.load_markets() + market = self.market(symbol) + orderSide = side.upper() + request: dict = { + 'symbol': market['id'], + 'side': orderSide, + } + marginMode: Str = None + marginMode, params = self.handle_margin_mode_and_params('createOrder', params) + if marginMode is not None: + request['marginMode'] = self.encode_margin_mode(marginMode) + triggerPrice = self.safe_string_2(params, 'triggerPrice', 'stopPrice') + stopLoss = self.safe_value(params, 'stopLoss') + takeProfit = self.safe_value(params, 'takeProfit') + algoType = self.safe_string(params, 'algoType') + trailingTriggerPrice = self.safe_string_2(params, 'trailingTriggerPrice', 'activatedPrice', self.number_to_string(price)) + trailingAmount = self.safe_string_2(params, 'trailingAmount', 'callbackValue') + trailingPercent = self.safe_string_2(params, 'trailingPercent', 'callbackRate') + isTrailingAmountOrder = trailingAmount is not None + isTrailingPercentOrder = trailingPercent is not None + isTrailing = isTrailingAmountOrder or isTrailingPercentOrder + isConditional = isTrailing or triggerPrice is not None or stopLoss is not None or takeProfit is not None or (self.safe_value(params, 'childOrders') is not None) + isMarket = orderType == 'MARKET' + timeInForce = self.safe_string_lower(params, 'timeInForce') + postOnly = self.is_post_only(isMarket, None, params) + clientOrderIdKey = 'clientAlgoOrderId' if isConditional else 'clientOrderId' + request['type'] = orderType # LIMIT/MARKET/IOC/FOK/POST_ONLY/ASK/BID + if not isConditional: + if postOnly: + request['type'] = 'POST_ONLY' + elif timeInForce == 'fok': + request['type'] = 'FOK' + elif timeInForce == 'ioc': + request['type'] = 'IOC' + if reduceOnly: + request['reduceOnly'] = reduceOnly + if not isMarket and price is not None: + request['price'] = self.price_to_precision(symbol, price) + if isMarket and not isConditional: + # for market buy it requires the amount of quote currency to spend + cost = self.safe_string_n(params, ['cost', 'order_amount', 'orderAmount']) + params = self.omit(params, ['cost', 'order_amount', 'orderAmount']) + isPriceProvided = price is not None + if market['spot'] and (isPriceProvided or (cost is not None)): + quoteAmount = None + if cost is not None: + quoteAmount = self.cost_to_precision(symbol, cost) + else: + amountString = self.number_to_string(amount) + priceString = self.number_to_string(price) + costRequest = Precise.string_mul(amountString, priceString) + quoteAmount = self.cost_to_precision(symbol, costRequest) + request['amount'] = quoteAmount + else: + request['quantity'] = self.amount_to_precision(symbol, amount) + elif algoType != 'POSITIONAL_TP_SL': + request['quantity'] = self.amount_to_precision(symbol, amount) + clientOrderId = self.safe_string_n(params, ['clOrdID', 'clientOrderId', 'client_order_id']) + if clientOrderId is not None: + request[clientOrderIdKey] = clientOrderId + if isTrailing: + if trailingTriggerPrice is None: + raise ArgumentsRequired(self.id + ' createOrder() requires a trailingTriggerPrice parameter for trailing orders') + request['activatedPrice'] = self.price_to_precision(symbol, trailingTriggerPrice) + request['algoType'] = 'TRAILING_STOP' + if isTrailingAmountOrder: + request['callbackValue'] = trailingAmount + elif isTrailingPercentOrder: + convertedTrailingPercent = Precise.string_div(trailingPercent, '100') + request['callbackRate'] = convertedTrailingPercent + elif triggerPrice is not None: + if algoType != 'TRAILING_STOP': + request['triggerPrice'] = self.price_to_precision(symbol, triggerPrice) + request['algoType'] = 'STOP' + elif (stopLoss is not None) or (takeProfit is not None): + request['algoType'] = 'BRACKET' + outterOrder: dict = { + 'symbol': market['id'], + 'reduceOnly': False, + 'algoType': 'POSITIONAL_TP_SL', + 'childOrders': [], + } + childOrders = outterOrder['childOrders'] + closeSide = 'SELL' if (orderSide == 'BUY') else 'BUY' + if stopLoss is not None: + stopLossPrice = self.safe_string(stopLoss, 'triggerPrice', stopLoss) + stopLossOrder: dict = { + 'side': closeSide, + 'algoType': 'STOP_LOSS', + 'triggerPrice': self.price_to_precision(symbol, stopLossPrice), + 'type': 'CLOSE_POSITION', + 'reduceOnly': True, + } + childOrders.append(stopLossOrder) + if takeProfit is not None: + takeProfitPrice = self.safe_string(takeProfit, 'triggerPrice', takeProfit) + takeProfitOrder: dict = { + 'side': closeSide, + 'algoType': 'TAKE_PROFIT', + 'triggerPrice': self.price_to_precision(symbol, takeProfitPrice), + 'type': 'CLOSE_POSITION', + 'reduceOnly': True, + } + childOrders.append(takeProfitOrder) + request['childOrders'] = [outterOrder] + params = self.omit(params, ['clOrdID', 'clientOrderId', 'client_order_id', 'postOnly', 'timeInForce', 'stopPrice', 'triggerPrice', 'stopLoss', 'takeProfit', 'trailingPercent', 'trailingAmount', 'trailingTriggerPrice']) + response = None + if isConditional: + response = self.v3PrivatePostTradeAlgoOrder(self.extend(request, params)) + # + # { + # "success": True, + # "data": { + # "rows": [ + # { + # "orderId": "1578938", + # "clientOrderId": "0", + # "algoType": "STOP_LOSS", + # "quantity": "0.1" + # } + # ] + # }, + # "timestamp": "1686149372216" + # } + # + else: + response = self.v3PrivatePostTradeOrder(self.extend(request, params)) + # + # { + # "success": True, + # "data": { + # "orderId": 60667653330, + # "clientOrderId": 0, + # "type": "LIMIT", + # "price": 60, + # "quantity": 0.1, + # "amount": null, + # "bidAskLevel": null + # }, + # "timestamp": 1751871779855 + # } + # + data = self.safe_dict(response, 'data', {}) + data = self.safe_dict(self.safe_list(data, 'rows'), 0, data) + data['timestamp'] = self.safe_string(response, 'timestamp') + return self.parse_order(data, market) + + def encode_margin_mode(self, mode): + modes = { + 'cross': 'CROSS', + 'isolated': 'ISOLATED', + } + return self.safe_string(modes, mode, mode) + + def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + """ + edit a trade order + + https://docs.woox.io/#edit-order + https://docs.woox.io/#edit-order-by-client_order_id + https://docs.woox.io/#edit-algo-order + https://docs.woox.io/#edit-algo-order-by-client_order_id + + :param str id: order id + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: The price a trigger order is triggered at + :param float [params.stopLossPrice]: price to trigger stop-loss orders + :param float [params.takeProfitPrice]: price to trigger take-profit orders + :param str [params.trailingAmount]: the quote amount to trail away from the current market price + :param str [params.trailingPercent]: the percent to trail away from the current market price + :param str [params.trailingTriggerPrice]: the price to trigger a trailing order, default uses the price argument + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + # 'quantity': self.amount_to_precision(symbol, amount), + # 'price': self.price_to_precision(symbol, price), + } + if price is not None: + request['price'] = self.price_to_precision(symbol, price) + if amount is not None: + request['quantity'] = self.amount_to_precision(symbol, amount) + clientOrderIdUnified = self.safe_string_2(params, 'clOrdID', 'clientOrderId') + clientOrderIdExchangeSpecific = self.safe_string(params, 'client_order_id', clientOrderIdUnified) + isByClientOrder = clientOrderIdExchangeSpecific is not None + triggerPrice = self.safe_number_n(params, ['triggerPrice', 'stopPrice', 'takeProfitPrice', 'stopLossPrice']) + if triggerPrice is not None: + request['triggerPrice'] = self.price_to_precision(symbol, triggerPrice) + trailingTriggerPrice = self.safe_string_2(params, 'trailingTriggerPrice', 'activatedPrice', self.number_to_string(price)) + trailingAmount = self.safe_string_2(params, 'trailingAmount', 'callbackValue') + trailingPercent = self.safe_string_2(params, 'trailingPercent', 'callbackRate') + isTrailingAmountOrder = trailingAmount is not None + isTrailingPercentOrder = trailingPercent is not None + isTrailing = isTrailingAmountOrder or isTrailingPercentOrder + if isTrailing: + if trailingTriggerPrice is not None: + request['activatedPrice'] = self.price_to_precision(symbol, trailingTriggerPrice) + if isTrailingAmountOrder: + request['callbackValue'] = trailingAmount + elif isTrailingPercentOrder: + convertedTrailingPercent = Precise.string_div(trailingPercent, '100') + request['callbackRate'] = convertedTrailingPercent + params = self.omit(params, ['clOrdID', 'clientOrderId', 'client_order_id', 'stopPrice', 'triggerPrice', 'takeProfitPrice', 'stopLossPrice', 'trailingTriggerPrice', 'trailingAmount', 'trailingPercent']) + isConditional = isTrailing or (triggerPrice is not None) or (self.safe_value(params, 'childOrders') is not None) + response = None + if isByClientOrder: + request['client_order_id'] = clientOrderIdExchangeSpecific + if isConditional: + response = self.v3PrivatePutAlgoOrderClientClientOrderId(self.extend(request, params)) + else: + response = self.v3PrivatePutOrderClientClientOrderId(self.extend(request, params)) + else: + request['oid'] = id + if isConditional: + response = self.v3PrivatePutAlgoOrderOid(self.extend(request, params)) + else: + response = self.v3PrivatePutOrderOid(self.extend(request, params)) + # + # { + # "code": 0, + # "data": { + # "status": "string", + # "success": True + # }, + # "message": "string", + # "success": True, + # "timestamp": 0 + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data, market) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + + https://developer.woox.io/api-reference/endpoint/trading/cancel_order + https://developer.woox.io/api-reference/endpoint/trading/cancel_algo_order + + cancels an open order + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: whether the order is a trigger/algo order + :returns dict: An `order structure ` + """ + isTrigger = self.safe_bool_2(params, 'trigger', 'stop', False) + params = self.omit(params, ['trigger', 'stop']) + if not isTrigger and (symbol is None): + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + self.load_markets() + market: Market = None + if symbol is not None: + market = self.market(symbol) + request: dict = {} + clientOrderIdUnified = self.safe_string_2(params, 'clOrdID', 'clientOrderId') + clientOrderIdExchangeSpecific = self.safe_string(params, 'client_order_id', clientOrderIdUnified) + params = self.omit(params, ['clOrdID', 'clientOrderId', 'client_order_id']) + isByClientOrder = clientOrderIdExchangeSpecific is not None + response = None + if isTrigger: + if isByClientOrder: + request['clientAlgoOrderId'] = clientOrderIdExchangeSpecific + else: + request['algoOrderId'] = id + response = self.v3PrivateDeleteTradeAlgoOrder(self.extend(request, params)) + else: + request['symbol'] = market['id'] + if isByClientOrder: + request['clientOrderId'] = clientOrderIdExchangeSpecific + else: + request['orderId'] = id + response = self.v3PrivateDeleteTradeOrder(self.extend(request, params)) + # + # { + # "success": True, + # "data": { + # "status": "CANCEL_SENT" + # }, + # "timestamp": 1751940315838 + # } + # + data = self.safe_dict(response, 'data', {}) + data['timestamp'] = self.safe_string(response, 'timestamp') + if isByClientOrder: + data['clientOrderId'] = clientOrderIdExchangeSpecific + else: + data['orderId'] = id + return self.parse_order(data, market) + + def cancel_all_orders(self, symbol: Str = None, params={}): + """ + + https://developer.woox.io/api-reference/endpoint/trading/cancel_all_order + https://developer.woox.io/api-reference/endpoint/trading/cancel_algo_orders + + cancel all open orders in a market + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: whether the order is a trigger/algo order + :returns dict: an list of `order structures ` + """ + self.load_markets() + trigger = self.safe_bool_2(params, 'stop', 'trigger') + params = self.omit(params, ['stop', 'trigger']) + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + response = None + if trigger: + response = self.v3PrivateDeleteTradeAlgoOrders(params) + else: + response = self.v3PrivateDeleteTradeOrders(self.extend(request, params)) + # + # { + # "success": True, + # "data": { + # "status": "CANCEL_ALL_SENT" + # }, + # "timestamp": 1751941988134 + # } + # + data = self.safe_dict(response, 'data', {}) + return [self.safe_order({'info': data})] + + def cancel_all_orders_after(self, timeout: Int, params={}): + """ + dead man's switch, cancel all orders after the given timeout + + https://developer.woox.io/api-reference/endpoint/trading/cancel_all_after + + :param number timeout: time in milliseconds, 0 represents cancel the timer + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: the api result + """ + self.load_markets() + request: dict = { + 'triggerAfter': min(timeout, 900000) if (timeout > 0) else 0, + } + response = self.v3PrivatePostTradeCancelAllAfter(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 123, + # "data": { + # "expectedTriggerTime": 123 + # } + # } + # + return response + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + + https://developer.woox.io/api-reference/endpoint/trading/get_order + https://developer.woox.io/api-reference/endpoint/trading/get_algo_order + + fetches information on an order made by the user + :param str id: the order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: whether the order is a trigger/algo order + :returns dict: An `order structure ` + """ + self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + trigger = self.safe_bool_2(params, 'stop', 'trigger') + params = self.omit(params, ['stop', 'trigger']) + request: dict = {} + clientOrderId = self.safe_string_2(params, 'clOrdID', 'clientOrderId') + response = None + if trigger: + if clientOrderId is not None: + request['clientAlgoOrderId'] = id + else: + request['algoOrderId'] = id + response = self.v3PrivateGetTradeAlgoOrder(self.extend(request, params)) + # + # { + # "success": True, + # "data": { + # "algoOrderId": 10399260, + # "clientAlgoOrderId": 0, + # "rootAlgoOrderId": 10399260, + # "parentAlgoOrderId": 0, + # "symbol": "SPOT_LTC_USDT", + # "algoOrderTag": "default", + # "algoType": "TAKE_PROFIT", + # "side": "BUY", + # "quantity": 0.1, + # "isTriggered": False, + # "triggerPrice": 65, + # "triggerStatus": "USELESS", + # "type": "LIMIT", + # "rootAlgoStatus": "NEW", + # "algoStatus": "NEW", + # "triggerPriceType": "MARKET_PRICE", + # "price": 60, + # "triggerTime": "0", + # "totalExecutedQuantity": 0, + # "visibleQuantity": 0.1, + # "averageExecutedPrice": 0, + # "totalFee": 0, + # "feeAsset": "", + # "totalRebate": 0, + # "rebateAsset": "", + # "reduceOnly": False, + # "createdTime": "1752049747.732", + # "updatedTime": "1752049747.732", + # "positionSide": "BOTH" + # }, + # "timestamp": 1752049767550 + # } + # + else: + if clientOrderId is not None: + request['clientOrderId'] = clientOrderId + else: + request['orderId'] = id + response = self.v3PrivateGetTradeOrder(self.extend(request, params)) + # + # { + # "success": True, + # "data": { + # "orderId": 60780315704, + # "clientOrderId": 0, + # "symbol": "SPOT_LTC_USDT", + # "orderTag": "default", + # "side": "BUY", + # "quantity": 0.1, + # "amount": null, + # "type": "LIMIT", + # "status": "NEW", + # "price": 60, + # "executed": 0, + # "visible": 0.1, + # "averageExecutedPrice": 0, + # "totalFee": 0, + # "feeAsset": "LTC", + # "totalRebate": 0, + # "rebateAsset": "USDT", + # "reduceOnly": False, + # "createdTime": "1752049062.496", + # "realizedPnl": null, + # "positionSide": "BOTH", + # "bidAskLevel": null + # }, + # "timestamp": 1752049393466 + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_order(data, market) + + def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://developer.woox.io/api-reference/endpoint/trading/get_orders + https://developer.woox.io/api-reference/endpoint/trading/get_algo_orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: whether the order is a trigger/algo order + :param boolean [params.isTriggered]: whether the order has been triggered(False by default) + :param str [params.side]: 'buy' or 'sell' + :param boolean [params.paginate]: set to True if you want to fetch orders with pagination + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOrders', 'paginate') + if paginate: + return self.fetch_paginated_call_incremental('fetchOrders', symbol, since, limit, params, 'page', 500) + request: dict = {} + market: Market = None + trigger = self.safe_bool_2(params, 'stop', 'trigger') + params = self.omit(params, ['stop', 'trigger']) + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if since is not None: + request['startTime'] = since + until = self.safe_integer(params, 'until') # unified in milliseconds + params = self.omit(params, ['until']) + if until is not None: + request['endTime'] = until + if limit is not None: + request['size'] = min(limit, 500) + response = None + if trigger: + response = self.v3PrivateGetTradeAlgoOrders(self.extend(request, params)) + # + # { + # "success": True, + # "data": { + # "rows": [ + # { + # "algoOrderId": 10399260, + # "clientAlgoOrderId": 0, + # "rootAlgoOrderId": 10399260, + # "parentAlgoOrderId": 0, + # "symbol": "SPOT_LTC_USDT", + # "algoOrderTag": "default", + # "algoType": "TAKE_PROFIT", + # "side": "BUY", + # "quantity": 0.1, + # "isTriggered": False, + # "triggerPrice": 65, + # "triggerStatus": "USELESS", + # "type": "LIMIT", + # "rootAlgoStatus": "NEW", + # "algoStatus": "NEW", + # "triggerPriceType": "MARKET_PRICE", + # "price": 60, + # "triggerTime": "0", + # "totalExecutedQuantity": 0, + # "visibleQuantity": 0.1, + # "averageExecutedPrice": 0, + # "totalFee": 0, + # "feeAsset": "", + # "totalRebate": 0, + # "rebateAsset": "", + # "reduceOnly": False, + # "createdTime": "1752049747.730", + # "updatedTime": "1752049747.730", + # "positionSide": "BOTH" + # } + # ], + # "meta": { + # "total": 7, + # "recordsPerPage": 1, + # "currentPage": 1 + # } + # }, + # "timestamp": 1752053127448 + # } + # + else: + response = self.v3PrivateGetTradeOrders(self.extend(request, params)) + # + # { + # "success": True, + # "data": { + # "rows": [ + # { + # "orderId": 60780315704, + # "clientOrderId": 0, + # "symbol": "SPOT_LTC_USDT", + # "orderTag": "default", + # "side": "BUY", + # "quantity": 0.1, + # "amount": null, + # "type": "LIMIT", + # "status": "NEW", + # "price": 60, + # "executed": 0, + # "visible": 0.1, + # "averageExecutedPrice": 0, + # "totalFee": 0, + # "feeAsset": "LTC", + # "totalRebate": 0, + # "rebateAsset": "USDT", + # "reduceOnly": False, + # "createdTime": "1752049062.496", + # "realizedPnl": null, + # "positionSide": "BOTH", + # "bidAskLevel": null + # } + # ], + # "meta": { + # "total": 11, + # "recordsPerPage": 1, + # "currentPage": 1 + # } + # }, + # "timestamp": 1752053061236 + # } + # + data = self.safe_value(response, 'data', {}) + orders = self.safe_list(data, 'rows', []) + return self.parse_orders(orders, market, since, limit) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://developer.woox.io/api-reference/endpoint/trading/get_orders + https://developer.woox.io/api-reference/endpoint/trading/get_algo_orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: whether the order is a trigger/algo order + :param boolean [params.isTriggered]: whether the order has been triggered(False by default) + :param str [params.side]: 'buy' or 'sell' + :param boolean [params.trailing]: set to True if you want to fetch trailing orders + :param boolean [params.paginate]: set to True if you want to fetch orders with pagination + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + extendedParams = self.extend(params, {'status': 'INCOMPLETE'}) + return self.fetch_orders(symbol, since, limit, extendedParams) + + def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://developer.woox.io/api-reference/endpoint/trading/get_orders + https://developer.woox.io/api-reference/endpoint/trading/get_algo_orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: whether the order is a trigger/algo order + :param boolean [params.isTriggered]: whether the order has been triggered(False by default) + :param str [params.side]: 'buy' or 'sell' + :param boolean [params.trailing]: set to True if you want to fetch trailing orders + :param boolean [params.paginate]: set to True if you want to fetch orders with pagination + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + extendedParams = self.extend(params, {'status': 'COMPLETED'}) + return self.fetch_orders(symbol, since, limit, extendedParams) + + def parse_time_in_force(self, timeInForce: Str): + timeInForces: dict = { + 'ioc': 'IOC', + 'fok': 'FOK', + 'post_only': 'PO', + } + return self.safe_string(timeInForces, timeInForce, None) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder + # { + # "orderId": 60667653330, + # "clientOrderId": 0, + # "type": "LIMIT", + # "price": 60, + # "quantity": 0.1, + # "amount": null, + # "bidAskLevel": null, + # "timestamp": 1751871779855 + # } + # + # createOrder - algo + # { + # "orderId": "1578938", + # "clientOrderId": "0", + # "algoType": "STOP_LOSS", + # "quantity": "0.1", + # "timestamp": "1686149372216" + # } + # + # fetchOrder + # { + # "orderId": 60780315704, + # "clientOrderId": 0, + # "symbol": "SPOT_LTC_USDT", + # "orderTag": "default", + # "side": "BUY", + # "quantity": 0.1, + # "amount": null, + # "type": "LIMIT", + # "status": "NEW", + # "price": 60, + # "executed": 0, + # "visible": 0.1, + # "averageExecutedPrice": 0, + # "totalFee": 0, + # "feeAsset": "LTC", + # "totalRebate": 0, + # "rebateAsset": "USDT", + # "reduceOnly": False, + # "createdTime": "1752049062.496", + # "realizedPnl": null, + # "positionSide": "BOTH", + # "bidAskLevel": null + # } + # + # fetchOrder - algo + # { + # "algoOrderId": 10399260, + # "clientAlgoOrderId": 0, + # "rootAlgoOrderId": 10399260, + # "parentAlgoOrderId": 0, + # "symbol": "SPOT_LTC_USDT", + # "algoOrderTag": "default", + # "algoType": "TAKE_PROFIT", + # "side": "BUY", + # "quantity": 0.1, + # "isTriggered": False, + # "triggerPrice": 65, + # "triggerStatus": "USELESS", + # "type": "LIMIT", + # "rootAlgoStatus": "NEW", + # "algoStatus": "NEW", + # "triggerPriceType": "MARKET_PRICE", + # "price": 60, + # "triggerTime": "0", + # "totalExecutedQuantity": 0, + # "visibleQuantity": 0.1, + # "averageExecutedPrice": 0, + # "totalFee": 0, + # "feeAsset": "", + # "totalRebate": 0, + # "rebateAsset": "", + # "reduceOnly": False, + # "createdTime": "1752049747.732", + # "updatedTime": "1752049747.732", + # "positionSide": "BOTH" + # } + # + timestamp = None + timestrampString = self.safe_string(order, 'createdTime') + if timestrampString is not None: + if timestrampString.find('.') >= 0: + timestamp = self.safe_timestamp(order, 'createdTime') # algo orders + else: + timestamp = self.safe_integer(order, 'createdTime') # regular orders + if timestamp is None: + timestamp = self.safe_integer(order, 'timestamp') + orderId = self.safe_string_2(order, 'orderId', 'algoOrderId') + clientOrderId = self.omit_zero(self.safe_string_2(order, 'clientOrderId', 'clientAlgoOrderId')) # Somehow, self always returns 0 for limit order + marketId = self.safe_string(order, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + price = self.safe_string(order, 'price') + amount = self.safe_string(order, 'quantity') # This is base amount + cost = self.safe_string(order, 'amount') # This is quote amount + orderType = self.safe_string_lower(order, 'type') + status = self.safe_value_2(order, 'status', 'algoStatus') + side = self.safe_string_lower(order, 'side') + filled = self.omit_zero(self.safe_value_2(order, 'executed', 'totalExecutedQuantity')) + average = self.omit_zero(self.safe_string(order, 'averageExecutedPrice')) + # remaining = Precise.string_sub(cost, filled) + fee = self.safe_number(order, 'totalFee') + feeCurrency = self.safe_string(order, 'feeAsset') + triggerPrice = self.safe_number(order, 'triggerPrice') + lastUpdateTimestampString = self.safe_string(order, 'updatedTime') + lastUpdateTimestamp = None + if lastUpdateTimestampString is not None: + if lastUpdateTimestampString.find('.') >= 0: + lastUpdateTimestamp = self.safe_timestamp(order, 'updatedTime') # algo orders + else: + lastUpdateTimestamp = self.safe_integer(order, 'updatedTime') # regular orders + return self.safe_order({ + 'id': orderId, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'lastUpdateTimestamp': lastUpdateTimestamp, + 'status': self.parse_order_status(status), + 'symbol': symbol, + 'type': orderType, + 'timeInForce': self.parse_time_in_force(orderType), + 'postOnly': None, # TO_DO + 'reduceOnly': self.safe_bool(order, 'reduceOnly'), + 'side': side, + 'price': price, + 'triggerPrice': triggerPrice, + 'takeProfitPrice': None, + 'stopLossPrice': None, + 'average': average, + 'amount': amount, + 'filled': filled, + 'remaining': None, # TO_DO + 'cost': cost, + 'trades': None, + 'fee': { + 'cost': fee, + 'currency': feeCurrency, + }, + 'info': order, + }, market) + + def parse_order_status(self, status: Str): + if status is not None: + statuses: dict = { + 'NEW': 'open', + 'FILLED': 'closed', + 'CANCEL_SENT': 'canceled', + 'CANCEL_ALL_SENT': 'canceled', + 'CANCELLED': 'canceled', + 'PARTIAL_FILLED': 'open', + 'REJECTED': 'rejected', + 'INCOMPLETE': 'open', + 'COMPLETED': 'closed', + } + return self.safe_string(statuses, status, status) + return status + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://developer.woox.io/api-reference/endpoint/public_data/orderbook + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['maxLevel'] = limit + response = self.v3PublicGetOrderbook(self.extend(request, params)) + # + # } + # { + # "success": True, + # "timestamp": 1751620923344, + # "data": { + # "asks": [ + # { + # "price": "108924.86", + # "quantity": "0.032126" + # } + # ], + # "bids": [ + # { + # "price": "108924.85", + # "quantity": "1.714147" + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + timestamp = self.safe_integer(response, 'timestamp') + return self.parse_order_book(data, symbol, timestamp, 'bids', 'asks', 'price', 'quantity') + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + + https://developer.woox.io/api-reference/endpoint/public_data/klineHistory + + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: max=1000, max=100 when since is defined and is less than(now - (999 * (timeframe in ms))) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch entries for + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'type': self.safe_string(self.timeframes, timeframe, timeframe), + } + if limit is not None: + request['limit'] = min(limit, 1000) + if since is not None: + request['after'] = since + until = self.safe_integer(params, 'until') + params = self.omit(params, 'until') + if until is not None: + request['before'] = until + response = self.v3PublicGetKlineHistory(self.extend(request, params)) + # + # { + # "success": True, + # "data": { + # "rows": [ + # { + # "symbol": "SPOT_BTC_USDT", + # "open": "108994.16", + # "close": "108994.16", + # "high": "108994.16", + # "low": "108994.16", + # "volume": "0", + # "amount": "0", + # "type": "1m", + # "startTimestamp": 1751622120000, + # "endTimestamp": 1751622180000 + # } + # ] + # }, + # "timestamp": 1751622205410 + # } + # + data = self.safe_dict(response, 'data', {}) + rows = self.safe_list(data, 'rows', []) + return self.parse_ohlcvs(rows, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + return [ + self.safe_integer(ohlcv, 'startTimestamp'), + self.safe_number(ohlcv, 'open'), + self.safe_number(ohlcv, 'high'), + self.safe_number(ohlcv, 'low'), + self.safe_number(ohlcv, 'close'), + self.safe_number(ohlcv, 'volume'), + ] + + def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all the trades made from a single order + + https://docs.woox.io/#get-trades + + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + self.load_markets() + market: Market = None + if symbol is not None: + market = self.market(symbol) + request: dict = { + 'oid': id, + } + response = self.v1PrivateGetOrderOidTrades(self.extend(request, params)) + # { + # "success": True, + # "rows": [ + # { + # "id": "99111647", + # "symbol": "SPOT_WOO_USDT", + # "fee": "0.0024", + # "side": "BUY", + # "executed_timestamp": "1641482113.084", + # "order_id": "87541111", + # "order_tag": "default", + # "executed_price": "1", + # "executed_quantity": "12", + # "fee_asset": "WOO", + # "is_maker": "1" + # } + # ] + # } + trades = self.safe_list(response, 'rows', []) + return self.parse_trades(trades, market, since, limit, params) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://developer.woox.io/api-reference/endpoint/trading/get_transactions + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: set to True if you want to fetch trades with pagination + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchMyTrades', 'paginate') + if paginate: + return self.fetch_paginated_call_incremental('fetchMyTrades', symbol, since, limit, params, 'page', 500) + request: dict = {} + market: Market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if since is not None: + request['startTime'] = since + until = self.safe_integer(params, 'until') # unified in milliseconds + params = self.omit(params, ['until']) + if until is not None: + request['endTime'] = until + if limit is not None: + request['limit'] = limit + response = self.v3PrivateGetTradeTransactionHistory(self.extend(request, params)) + # + # { + # "success": True, + # "data": { + # "rows": [ + # { + # "id": 1734947821, + # "symbol": "SPOT_LTC_USDT", + # "orderId": 60780383217, + # "executedPrice": 87.86, + # "executedQuantity": 0.1, + # "fee": 0.0001, + # "realizedPnl": null, + # "feeAsset": "LTC", + # "orderTag": "default", + # "side": "BUY", + # "executedTimestamp": "1752055173.630", + # "isMaker": 0 + # } + # ], + # "meta": { + # "total": 1, + # "recordsPerPage": 100, + # "currentPage": 1 + # } + # }, + # "timestamp": 1752055545121 + # } + # + data = self.safe_dict(response, 'data', {}) + trades = self.safe_list(data, 'rows', []) + return self.parse_trades(trades, market, since, limit, params) + + def fetch_accounts(self, params={}) -> List[Account]: + """ + fetch all the accounts associated with a profile + + https://developer.woox.io/api-reference/endpoint/account/get_account_info + https://developer.woox.io/api-reference/endpoint/account/sub_accounts + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `account structures ` indexed by the account type + """ + mainAccountPromise = self.v3PrivateGetAccountInfo(params) + # + # { + # "success": True, + # "data": { + # "applicationId": "251bf5c4-f3c8-4544-bb8b-80001007c3c0", + # "account": "carlos_jose_lima@yahoo.com", + # "alias": "carlos_jose_lima@yahoo.com", + # "otpauth": True, + # "accountMode": "FUTURES", + # "positionMode": "ONE_WAY", + # "leverage": 0, + # "marginRatio": "10", + # "openMarginRatio": "10", + # "initialMarginRatio": "10", + # "maintenanceMarginRatio": "0.03", + # "totalCollateral": "165.55629469", + # "freeCollateral": "165.55629469", + # "totalAccountValue": "167.32418611", + # "totalTradingValue": "167.32418611", + # "totalVaultValue": "0", + # "totalStakingValue": "0", + # "totalLaunchpadValue": "0", + # "totalEarnValue": "0", + # "referrerID": null, + # "accountType": "Main" + # }, + # "timestamp": 1752062807915 + # } + # + subAccountPromise = self.v3PrivateGetAccountSubAccountsAll(params) + # + # { + # "success": True, + # "data": { + # "rows": [ + # { + # "applicationId": "6b43de5c-0955-4887-9862-d84e4689f9fe", + # "name": "sub_account_2", + # "createdTime": "1606897264.994" + # }, + # ] + # }, + # "timestamp": 1721295317627 + # } + # + mainAccountResponse, subAccountResponse = [mainAccountPromise, subAccountPromise] + mainData = self.safe_dict(mainAccountResponse, 'data', {}) + mainRows = [mainData] + subData = self.safe_dict(subAccountResponse, 'data', {}) + subRows = self.safe_list(subData, 'rows', []) + rows = self.array_concat(mainRows, subRows) + return self.parse_accounts(rows, params) + + def parse_account(self, account): + # + # { + # "applicationId": "251bf5c4-f3c8-4544-bb8b-80001007c3c0", + # "account": "carlos_jose_lima@yahoo.com", + # "alias": "carlos_jose_lima@yahoo.com", + # "otpauth": True, + # "accountMode": "FUTURES", + # "positionMode": "ONE_WAY", + # "leverage": 0, + # "marginRatio": "10", + # "openMarginRatio": "10", + # "initialMarginRatio": "10", + # "maintenanceMarginRatio": "0.03", + # "totalCollateral": "165.55629469", + # "freeCollateral": "165.55629469", + # "totalAccountValue": "167.32418611", + # "totalTradingValue": "167.32418611", + # "totalVaultValue": "0", + # "totalStakingValue": "0", + # "totalLaunchpadValue": "0", + # "totalEarnValue": "0", + # "referrerID": null, + # "accountType": "Main" + # } + # + # { + # "applicationId": "6b43de5c-0955-4887-9862-d84e4689f9fe", + # "name": "sub_account_2", + # "createdTime": "1606897264.994" + # } + # + return { + 'info': account, + 'id': self.safe_string(account, 'applicationId'), + 'name': self.safe_string_n(account, ['name', 'account', 'alias']), + 'code': None, + 'type': self.safe_string_lower(account, 'accountType', 'subaccount'), + } + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://docs.woox.io/#get-current-holding-get-balance-new + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + response = self.v3PrivateGetAssetBalances(params) + # + # { + # "success": True, + # "data": { + # "holding": [ + # { + # "token": "0_token", + # "holding": 1, + # "frozen": 0, + # "staked": 0, + # "unbonding": 0, + # "vault": 0, + # "interest": 0, + # "pendingShortQty": 0, + # "pendingLongQty": 0, + # "availableBalance": 0, + # "updatedTime": 312321.121 + # } + # ] + # }, + # "timestamp": 1673323746259 + # } + # + data = self.safe_dict(response, 'data') + return self.parse_balance(data) + + def parse_balance(self, response) -> Balances: + result: dict = { + 'info': response, + } + balances = self.safe_list(response, 'holding', []) + for i in range(0, len(balances)): + balance = balances[i] + code = self.safe_currency_code(self.safe_string(balance, 'token')) + account = self.account() + account['total'] = self.safe_string(balance, 'holding') + account['free'] = self.safe_string(balance, 'availableBalance') + result[code] = account + return self.safe_balance(result) + + def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://developer.woox.io/api-reference/endpoint/assets/get_wallet_deposit + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + # self method is TODO because of networks unification + self.load_markets() + currency = self.currency(code) + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + request: dict = { + 'token': currency['id'], + 'network': self.network_code_to_id(networkCode), + } + response = self.v3PrivateGetAssetWalletDeposit(self.extend(request, params)) + # + # { + # "success": True, + # "data": { + # "address": "0x31d64B3230f8baDD91dE1710A65DF536aF8f7cDa", + # "extra": "" + # }, + # "timestamp": 1721300689532 + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_deposit_address(data, currency) + + def get_dedicated_network_id(self, currency, params: dict) -> Any: + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + networkCode = self.network_id_to_code(networkCode, currency['code']) + networkEntry = self.safe_dict(currency['networks'], networkCode) + if networkEntry is None: + supportedNetworks = list(currency['networks'].keys()) + raise BadRequest(self.id + ' can not determine a network code, please provide unified "network" param, one from the following: ' + self.json(supportedNetworks)) + currentyNetworkId = self.safe_string(networkEntry, 'currencyNetworkId') + return [currentyNetworkId, params] + + def parse_deposit_address(self, depositEntry, currency: Currency = None) -> DepositAddress: + address = self.safe_string(depositEntry, 'address') + self.check_address(address) + return { + 'info': depositEntry, + 'currency': self.safe_string(currency, 'code'), + 'network': None, + 'address': address, + 'tag': self.safe_string(depositEntry, 'extra'), + } + + def get_asset_history_rows(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> Any: + self.load_markets() + request: dict = {} + currency: Currency = None + if code is not None: + currency = self.currency(code) + request['token'] = currency['id'] + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + if networkCode is not None: + request['network'] = self.network_code_to_id(networkCode) + if since is not None: + request['startTime'] = since + if limit is not None: + request['size'] = min(limit, 1000) + transactionType = self.safe_string(params, 'type') + params = self.omit(params, 'type') + if transactionType is not None: + request['type'] = transactionType + response = self.v3PrivateGetAssetWalletHistory(self.extend(request, params)) + # + # { + # "success": True, + # "data": { + # "rows": [ + # { + # "createdTime": "1734964440.523", + # "updatedTime": "1734964614.081", + # "id": "24122314340000585", + # "externalId": "241223143600621", + # "applicationId": "251bf5c4-f3c8-4544-bb8b-80001007c3c0", + # "token": "ARB_USDCNATIVE", + # "targetAddress": "0x4d6802d2736daa85e6242ef0dc0f00aa0e68f635", + # "sourceAddress": "0x63DFE4e34A3bFC00eB0220786238a7C6cEF8Ffc4", + # "extra": "", + # "type": "BALANCE", + # "tokenSide": "WITHDRAW", + # "amount": "10.00000000", + # "txId": "0x891ade0a47fd55466bb9d06702bea4edcb75ed9367d9afbc47b93a84f496d2e6", + # "feeToken": "USDC", + # "feeAmount": "2", + # "status": "COMPLETED", + # "confirmingThreshold": null, + # "confirmedNumber": null + # } + # ], + # "meta": { + # "total": 1, + # "records_per_page": 25, + # "current_page": 1 + # } + # }, + # "timestamp": 1752485344719 + # } + # + data = self.safe_dict(response, 'data', {}) + return [currency, self.safe_list(data, 'rows', [])] + + def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered balance of the user + + https://developer.woox.io/api-reference/endpoint/assets/get_wallet_history + + :param str [code]: unified currency code, default is None + :param int [since]: timestamp in ms of the earliest ledger entry, default is None + :param int [limit]: max number of ledger entries to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ledger structure ` + """ + currencyRows = self.get_asset_history_rows(code, since, limit, params) + currency = self.safe_value(currencyRows, 0) + rows = self.safe_list(currencyRows, 1) + return self.parse_ledger(rows, currency, since, limit, params) + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + # + # { + # "createdTime": "1734964440.523", + # "updatedTime": "1734964614.081", + # "id": "24122314340000585", + # "externalId": "241223143600621", + # "applicationId": "251bf5c4-f3c8-4544-bb8b-80001007c3c0", + # "token": "ARB_USDCNATIVE", + # "targetAddress": "0x4d6802d2736daa85e6242ef0dc0f00aa0e68f635", + # "sourceAddress": "0x63DFE4e34A3bFC00eB0220786238a7C6cEF8Ffc4", + # "extra": "", + # "type": "BALANCE", + # "tokenSide": "WITHDRAW", + # "amount": "10.00000000", + # "txId": "0x891ade0a47fd55466bb9d06702bea4edcb75ed9367d9afbc47b93a84f496d2e6", + # "feeToken": "USDC", + # "feeAmount": "2", + # "status": "COMPLETED", + # "confirmingThreshold": null, + # "confirmedNumber": null + # } + # + networkizedCode = self.safe_string(item, 'token') + code = self.safe_currency_code(networkizedCode, currency) + currency = self.safe_currency(code, currency) + amount = self.safe_number(item, 'amount') + side = self.safe_string(item, 'tokenSide') + direction = 'in' if (side == 'DEPOSIT') else 'out' + timestamp = self.safe_timestamp(item, 'createdTime') + fee = self.parse_token_and_fee_temp(item, ['feeToken'], ['feeAmount']) + return self.safe_ledger_entry({ + 'info': item, + 'id': self.safe_string(item, 'id'), + 'currency': code, + 'account': self.safe_string(item, 'account'), + 'referenceAccount': None, + 'referenceId': self.safe_string(item, 'txId'), + 'status': self.parse_transaction_status(self.safe_string(item, 'status')), + 'amount': amount, + 'before': None, + 'after': None, + 'direction': direction, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'type': self.parse_ledger_entry_type(self.safe_string(item, 'type')), + 'fee': fee, + }, currency) + + def parse_ledger_entry_type(self, type): + types: dict = { + 'BALANCE': 'transaction', # Funds moved in/out wallet + 'COLLATERAL': 'transfer', # Funds moved between portfolios + } + return self.safe_string(types, type, type) + + def get_currency_from_chaincode(self, networkizedCode, currency): + if currency is not None: + return currency + else: + parts = networkizedCode.split('_') + partsLength = len(parts) + firstPart = self.safe_string(parts, 0) + currencyId = self.safe_string(parts, 1, firstPart) + if partsLength > 2: + currencyId += '_' + self.safe_string(parts, 2) + currency = self.safe_currency(currencyId) + return currency + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://developer.woox.io/api-reference/endpoint/assets/get_wallet_history + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + request: dict = { + 'tokenSide': 'DEPOSIT', + } + return self.fetch_deposits_withdrawals(code, since, limit, self.extend(request, params)) + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://developer.woox.io/api-reference/endpoint/assets/get_wallet_history + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + request: dict = { + 'tokenSide': 'WITHDRAW', + } + return self.fetch_deposits_withdrawals(code, since, limit, self.extend(request, params)) + + def fetch_deposits_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch history of deposits and withdrawals + + https://developer.woox.io/api-reference/endpoint/assets/get_wallet_history + + :param str [code]: unified currency code for the currency of the deposit/withdrawals, default is None + :param int [since]: timestamp in ms of the earliest deposit/withdrawal, default is None + :param int [limit]: max number of deposit/withdrawals to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `transaction structure ` + """ + request: dict = { + 'type': 'BALANCE', + } + currencyRows = self.get_asset_history_rows(code, since, limit, self.extend(request, params)) + currency = self.safe_value(currencyRows, 0) + rows = self.safe_list(currencyRows, 1) + return self.parse_transactions(rows, currency, since, limit, params) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # { + # "createdTime": "1734964440.523", + # "updatedTime": "1734964614.081", + # "id": "24122314340000585", + # "externalId": "241223143600621", + # "applicationId": "251bf5c4-f3c8-4544-bb8b-80001007c3c0", + # "token": "ARB_USDCNATIVE", + # "targetAddress": "0x4d6802d2736daa85e6242ef0dc0f00aa0e68f635", + # "sourceAddress": "0x63DFE4e34A3bFC00eB0220786238a7C6cEF8Ffc4", + # "extra": "", + # "type": "BALANCE", + # "tokenSide": "WITHDRAW", + # "amount": "10.00000000", + # "txId": "0x891ade0a47fd55466bb9d06702bea4edcb75ed9367d9afbc47b93a84f496d2e6", + # "feeToken": "USDC", + # "feeAmount": "2", + # "status": "COMPLETED", + # "confirmingThreshold": null, + # "confirmedNumber": null + # } + # + networkizedCode = self.safe_string(transaction, 'token') + currencyDefined = self.get_currency_from_chaincode(networkizedCode, currency) + code = currencyDefined['code'] + movementDirection = self.safe_string_lower_n(transaction, ['token_side', 'tokenSide', 'type']) + if movementDirection == 'withdraw': + movementDirection = 'withdrawal' + fee = self.parse_token_and_fee_temp(transaction, ['fee_token', 'feeToken'], ['fee_amount', 'feeAmount']) + addressTo = self.safe_string_n(transaction, ['target_address', 'targetAddress', 'addressTo']) + addressFrom = self.safe_string_2(transaction, 'source_address', 'sourceAddress') + timestamp = self.safe_timestamp_n(transaction, ['created_time', 'createdTime'], self.safe_integer(transaction, 'timestamp')) + return { + 'info': transaction, + 'id': self.safe_string_n(transaction, ['id', 'withdraw_id', 'withdrawId']), + 'txid': self.safe_string_2(transaction, 'tx_id', 'txId'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'address': None, + 'addressFrom': addressFrom, + 'addressTo': addressTo, + 'tag': self.safe_string_2(transaction, 'extra', 'tag'), + 'tagFrom': None, + 'tagTo': None, + 'type': movementDirection, + 'amount': self.safe_number(transaction, 'amount'), + 'currency': code, + 'status': self.parse_transaction_status(self.safe_string(transaction, 'status')), + 'updated': self.safe_timestamp_2(transaction, 'updated_time', 'updatedTime'), + 'comment': None, + 'internal': None, + 'fee': fee, + 'network': self.network_id_to_code(self.safe_string(transaction, 'network')), + } + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'NEW': 'pending', + 'CONFIRMING': 'pending', + 'PROCESSING': 'pending', + 'COMPLETED': 'ok', + 'CANCELED': 'canceled', + } + return self.safe_string(statuses, status, status) + + def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://docs.woox.io/#get-transfer-history + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account to transfer from + :param str toAccount: account to transfer to + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transfer structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'token': currency['id'], + 'amount': self.parse_to_numeric(amount), + 'from': { + 'applicationId': fromAccount, + }, + 'to': { + 'applicationId': toAccount, + }, + } + response = self.v3PrivatePostAssetTransfer(self.extend(request, params)) + # + # { + # "success": True, + # "id": 200 + # } + # + data = self.safe_dict(response, 'data', {}) + data['timestamp'] = self.safe_integer(response, 'timestamp') + data['token'] = currency['id'] + data['status'] = 'ok' + transfer = self.parse_transfer(data, currency) + transferOptions = self.safe_dict(self.options, 'transfer', {}) + fillResponseFromRequest = self.safe_bool(transferOptions, 'fillResponseFromRequest', True) + if fillResponseFromRequest: + transfer['amount'] = amount + transfer['fromAccount'] = fromAccount + transfer['toAccount'] = toAccount + return transfer + + def fetch_transfers(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[TransferEntry]: + """ + fetch a history of internal transfers made on an account + + https://developer.woox.io/api-reference/endpoint/assets/get_transfer_history + + :param str code: unified currency code of the currency transferred + :param int [since]: the earliest time in ms to fetch transfers for + :param int [limit]: the maximum number of transfers structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch entries for + :returns dict[]: a list of `transfer structures ` + """ + request: dict = {} + currency = None + if code is not None: + currency = self.currency(code) + if limit is not None: + request['size'] = limit + if since is not None: + request['startTime'] = since + until = self.safe_integer(params, 'until') # unified in milliseconds + params = self.omit(params, ['until']) + if until is not None: + request['endTime'] = until + response = self.v3PrivateGetAssetTransferHistory(self.extend(request, params)) + # + # { + # "success": True, + # "data": { + # "rows": [ + # { + # "id": 225, + # "token": "USDT", + # "amount": "1000000", + # "status": "COMPLETED", + # "from": { + # "applicationId": "046b5c5c-5b44-4d27-9593-ddc32c0a08ae", + # "accountName": "Main" + # }, + # "to": { + # "applicationId": "082ae5ae-e26a-4fb1-be5b-03e5b4867663", + # "accountName": "sub001" + # }, + # "createdTime": "1642660941.534", + # "updatedTime": "1642660941.950" + # } + # ], + # "meta": { + # "total": 46, + # "recordsPerPage": 1, + # "currentPage": 1 + # } + # }, + # "timestamp": 1721295317627 + # } + # + data = self.safe_dict(response, 'data', {}) + rows = self.safe_list(data, 'rows', []) + return self.parse_transfers(rows, currency, since, limit, params) + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + # + # fetchTransfers + # { + # "id": 46704, + # "token": "USDT", + # "amount": 30000.00000000, + # "status": "COMPLETED", + # "from_application_id": "0f1bd3cd-dba2-4563-b8bb-0adb1bfb83a3", + # "to_application_id": "c01e6940-a735-4022-9b6c-9d3971cdfdfa", + # "from_user": "LeverageLow", + # "to_user": "dev", + # "created_time": "1709022325.427", + # "updated_time": "1709022325.542" + # } + # { + # "id": 225, + # "token": "USDT", + # "amount": "1000000", + # "status": "COMPLETED", + # "from": { + # "applicationId": "046b5c5c-5b44-4d27-9593-ddc32c0a08ae", + # "accountName": "Main" + # }, + # "to": { + # "applicationId": "082ae5ae-e26a-4fb1-be5b-03e5b4867663", + # "accountName": "sub001" + # }, + # "createdTime": "1642660941.534", + # "updatedTime": "1642660941.950" + # } + # + # transfer + # { + # "success": True, + # "id": 200 + # } + # + code = self.safe_currency_code(self.safe_string(transfer, 'token'), currency) + timestamp = self.safe_timestamp_2(transfer, 'createdTime', 'timestamp') + success = self.safe_bool(transfer, 'success') + status: Str = None + if success is not None: + status = 'ok' if success else 'failed' + fromAccount = self.safe_dict(transfer, 'from', {}) + toAccount = self.safe_dict(transfer, 'to', {}) + return { + 'id': self.safe_string(transfer, 'id'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'currency': code, + 'amount': self.safe_number(transfer, 'amount'), + 'fromAccount': self.safe_string(fromAccount, 'applicationId'), + 'toAccount': self.safe_string(toAccount, 'applicationId'), + 'status': self.parse_transfer_status(self.safe_string(transfer, 'status', status)), + 'info': transfer, + } + + def parse_transfer_status(self, status: Str) -> Str: + statuses: dict = { + 'NEW': 'pending', + 'CONFIRMING': 'pending', + 'PROCESSING': 'pending', + 'COMPLETED': 'ok', + 'CANCELED': 'canceled', + } + return self.safe_string(statuses, status, status) + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://docs.woox.io/#token-withdraw-v3 + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.load_markets() + self.check_address(address) + currency = self.currency(code) + request: dict = { + 'amount': amount, + 'address': address, + } + if tag is not None: + request['extra'] = tag + network = self.safe_string(params, 'network') + if network is None: + raise ArgumentsRequired(self.id + ' withdraw() requires a network parameter for ' + code) + params = self.omit(params, 'network') + request['token'] = currency['id'] + request['network'] = self.network_code_to_id(network) + response = self.v3PrivatePostAssetWalletWithdraw(self.extend(request, params)) + # + # { + # "success": True, + # "withdraw_id": "20200119145703654" + # } + # + data = self.safe_dict(response, 'data', {}) + transactionData = self.extend(data, { + 'id': self.safe_string(data, 'withdrawId'), + 'timestamp': self.safe_integer(response, 'timestamp'), + 'currency': code, + 'amount': amount, + 'addressTo': address, + 'tag': tag, + 'network': network, + 'type': 'withdrawal', + 'status': 'pending', + }) + return self.parse_transaction(transactionData, currency) + + def repay_margin(self, code: str, amount: float, symbol: Str = None, params={}): + """ + repay borrowed margin and interest + + https://docs.woox.io/#repay-interest + + :param str code: unified currency code of the currency to repay + :param float amount: the amount to repay + :param str symbol: not used by woo.repayMargin() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin loan structure ` + """ + self.load_markets() + market: Market = None + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + currency = self.currency(code) + request: dict = { + 'token': currency['id'], # interest token that you want to repay + 'amount': self.currency_to_precision(code, amount), + } + response = self.v1PrivatePostInterestRepay(self.extend(request, params)) + # + # { + # "success": True, + # } + # + transaction = self.parse_margin_loan(response, currency) + return self.extend(transaction, { + 'amount': amount, + 'symbol': symbol, + }) + + def parse_margin_loan(self, info, currency: Currency = None): + # + # { + # "success": True, + # } + # + return { + 'id': None, + 'currency': self.safe_currency_code(None, currency), + 'amount': None, + 'symbol': None, + 'timestamp': None, + 'datetime': None, + 'info': info, + } + + def nonce(self): + return self.milliseconds() - self.options['timeDifference'] + + def sign(self, path, section='public', method='GET', params={}, headers=None, body=None): + version = section[0] + access = section[1] + pathWithParams = self.implode_params(path, params) + url = self.implode_hostname(self.urls['api'][access]) + url += '/' + version + '/' + params = self.omit(params, self.extract_params(path)) + params = self.keysort(params) + if access == 'public': + url += access + '/' + pathWithParams + if params: + url += '?' + self.urlencode(params) + elif access == 'pub': + url += pathWithParams + if params: + url += '?' + self.urlencode(params) + else: + self.check_required_credentials() + if method == 'POST' and (path == 'trade/algoOrder' or path == 'trade/order'): + isSandboxMode = self.safe_bool(self.options, 'sandboxMode', False) + if not isSandboxMode: + applicationId = 'bc830de7-50f3-460b-9ee0-f430f83f9dad' + brokerId = self.safe_string(self.options, 'brokerId', applicationId) + isTrigger = path.find('algo') > -1 + if isTrigger: + params['brokerId'] = brokerId + else: + params['broker_id'] = brokerId + params = self.keysort(params) + auth = '' + ts = str(self.nonce()) + url += pathWithParams + headers = { + 'x-api-key': self.apiKey, + 'x-api-timestamp': ts, + } + if version == 'v3': + auth = ts + method + '/' + version + '/' + pathWithParams + if method == 'POST' or method == 'PUT': + body = self.json(params) + auth += body + headers['content-type'] = 'application/json' + else: + if params: + query = self.urlencode(params) + url += '?' + query + auth += '?' + query + else: + auth = self.urlencode(params) + if method == 'POST' or method == 'PUT' or method == 'DELETE': + body = auth + else: + if params: + url += '?' + auth + auth += '|' + ts + headers['content-type'] = 'application/x-www-form-urlencoded' + headers['x-api-signature'] = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256) + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if not response: + return None # fallback to default error handler + # + # 400 Bad Request {"success":false,"code":-1012,"message":"Amount is required for buy market orders when margin disabled."} + # {"code":"-1011","message":"The system is under maintenance.","success":false} + # + success = self.safe_bool(response, 'success') + errorCode = self.safe_string(response, 'code') + if not success: + feedback = self.id + ' ' + self.json(response) + self.throw_broadly_matched_exception(self.exceptions['broad'], body, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + return None + + def parse_income(self, income, market: Market = None): + # + # { + # "id": 1286360, + # "symbol": "PERP_BTC_USDT", + # "fundingRate": -0.00001445, + # "markPrice": "26930.60000000", + # "fundingFee": "9.56021744", + # "fundingIntervalHours": 8, + # "paymentType": "Pay", + # "status": "COMPLETED", + # "createdTime": 1696060873259, + # "updatedTime": 1696060873286 + # } + # + marketId = self.safe_string(income, 'symbol') + symbol = self.safe_symbol(marketId, market) + amount = self.safe_string(income, 'fundingFee') + code = self.safe_currency_code('USD') + id = self.safe_string(income, 'id') + timestamp = self.safe_integer(income, 'updatedTime') + rate = self.safe_number(income, 'fundingRate') + paymentType = self.safe_string(income, 'paymentType') + amount = Precise.string_neg(amount) if (paymentType == 'Pay') else amount + return { + 'info': income, + 'symbol': symbol, + 'code': code, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': id, + 'amount': self.parse_number(amount), + 'rate': rate, + } + + def fetch_funding_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch the history of funding payments paid and received on self account + + https://developer.woox.io/api-reference/endpoint/futures/get_fundingFee_history + + :param str [symbol]: unified market symbol + :param int [since]: the earliest time in ms to fetch funding history for + :param int [limit]: the maximum number of funding history structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict: a `funding history structure ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchFundingHistory', 'paginate') + if paginate: + return self.fetch_paginated_call_incremental('fetchFundingHistory', symbol, since, limit, params, 'page', 500) + request: dict = {} + market: Market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if since is not None: + request['startTime'] = since + until = self.safe_integer(params, 'until') # unified in milliseconds + params = self.omit(params, ['until']) + if until is not None: + request['endTime'] = until + if limit is not None: + request['size'] = min(limit, 500) + response = self.v3PrivateGetFuturesFundingFeeHistory(self.extend(request, params)) + # + # { + # "success": True, + # "data": { + # "meta": { + # "total": 670, + # "recordsPerPage": 25, + # "currentPage": 1 + # }, + # "rows": [ + # { + # "id": 1286360, + # "symbol": "PERP_BTC_USDT", + # "fundingRate": -0.00001445, + # "markPrice": "26930.60000000", + # "fundingFee": "9.56021744", + # "fundingIntervalHours": 8, + # "paymentType": "Pay", + # "status": "COMPLETED", + # "createdTime": 1696060873259, + # "updatedTime": 1696060873286 + # } + # ] + # }, + # "timestamp": 1721351502594 + # } + # + data = self.safe_dict(response, 'data', {}) + rows = self.safe_list(data, 'rows', []) + return self.parse_incomes(rows, market, since, limit) + + def parse_funding_rate(self, fundingRate, market: Market = None) -> FundingRate: + # + # { + # "symbol": "PERP_BTC_USDT", + # "estFundingRate": "-0.00000441", + # "estFundingRateTimestamp": 1751623979022, + # "lastFundingRate": "-0.00004953", + # "lastFundingRateTimestamp": 1751616000000, + # "nextFundingTime": 1751644800000, + # "lastFundingIntervalHours": 8, + # "estFundingIntervalHours": 8 + # } + # + symbol = self.safe_string(fundingRate, 'symbol') + market = self.market(symbol) + nextFundingTimestamp = self.safe_integer(fundingRate, 'nextFundingTime') + estFundingRateTimestamp = self.safe_integer(fundingRate, 'estFundingRateTimestamp') + lastFundingRateTimestamp = self.safe_integer(fundingRate, 'lastFundingRateTimestamp') + intervalString = self.safe_string(fundingRate, 'estFundingIntervalHours') + return { + 'info': fundingRate, + 'symbol': market['symbol'], + 'markPrice': None, + 'indexPrice': None, + 'interestRate': self.parse_number('0'), + 'estimatedSettlePrice': None, + 'timestamp': estFundingRateTimestamp, + 'datetime': self.iso8601(estFundingRateTimestamp), + 'fundingRate': self.safe_number(fundingRate, 'estFundingRate'), + 'fundingTimestamp': nextFundingTimestamp, + 'fundingDatetime': self.iso8601(nextFundingTimestamp), + 'nextFundingRate': None, + 'nextFundingTimestamp': None, + 'nextFundingDatetime': None, + 'previousFundingRate': self.safe_number(fundingRate, 'lastFundingRate'), + 'previousFundingTimestamp': lastFundingRateTimestamp, + 'previousFundingDatetime': self.iso8601(lastFundingRateTimestamp), + 'interval': intervalString + 'h', + } + + def fetch_funding_interval(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate interval + + https://developer.woox.io/api-reference/endpoint/public_data/fundingRate + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + return self.fetch_funding_rate(symbol, params) + + def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate + + https://developer.woox.io/api-reference/endpoint/public_data/fundingRate + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = self.v3PublicGetFundingRate(self.extend(request, params)) + # + # { + # "success": True, + # "data": { + # "rows": [ + # { + # "symbol": "PERP_BTC_USDT", + # "estFundingRate": "-0.00000441", + # "estFundingRateTimestamp": 1751623979022, + # "lastFundingRate": "-0.00004953", + # "lastFundingRateTimestamp": 1751616000000, + # "nextFundingTime": 1751644800000, + # "lastFundingIntervalHours": 8, + # "estFundingIntervalHours": 8 + # } + # ] + # }, + # "timestamp": 1751624037798 + # } + # + data = self.safe_dict(response, 'data', {}) + rows = self.safe_list(data, 'rows', []) + first = self.safe_dict(rows, 0, {}) + return self.parse_funding_rate(first, market) + + def fetch_funding_rates(self, symbols: Strings = None, params={}) -> FundingRates: + """ + fetch the funding rate for multiple markets + + https://developer.woox.io/api-reference/endpoint/public_data/fundingRate + + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `funding rate structures `, indexed by market symbols + """ + self.load_markets() + symbols = self.market_symbols(symbols) + response = self.v3PublicGetFundingRate(params) + # + # { + # "success": True, + # "data": { + # "rows": [ + # { + # "symbol": "PERP_BTC_USDT", + # "estFundingRate": "-0.00000441", + # "estFundingRateTimestamp": 1751623979022, + # "lastFundingRate": "-0.00004953", + # "lastFundingRateTimestamp": 1751616000000, + # "nextFundingTime": 1751644800000, + # "lastFundingIntervalHours": 8, + # "estFundingIntervalHours": 8 + # } + # ] + # }, + # "timestamp": 1751624037798 + # } + # + data = self.safe_dict(response, 'data', {}) + rows = self.safe_list(data, 'rows', []) + return self.parse_funding_rates(rows, symbols) + + def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical funding rate prices + + https://developer.woox.io/api-reference/endpoint/public_data/fundingRateHistory + + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: timestamp in ms of the earliest funding rate to fetch + :param int [limit]: the maximum amount of `funding rate structures ` to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest funding rate + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `funding rate structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchFundingRateHistory', 'paginate') + if paginate: + return self.fetch_paginated_call_incremental('fetchFundingRateHistory', symbol, since, limit, params, 'page', 25) + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchFundingRateHistory() requires a symbol argument') + market = self.market(symbol) + symbol = market['symbol'] + request: dict = { + 'symbol': market['id'], + } + if since is not None: + request['startTime'] = since + request, params = self.handle_until_option('endTime', request, params) + response = self.v3PublicGetFundingRateHistory(self.extend(request, params)) + # + # { + # "success": True, + # "data": { + # "rows": [ + # { + # "symbol": "PERP_BTC_USDT", + # "fundingRate": "-0.00004953", + # "fundingRateTimestamp": 1751616000000, + # "nextFundingTime": 1751644800000, + # "markPrice": "108708" + # } + # ], + # "meta": { + # "total": 11690, + # "recordsPerPage": 25, + # "currentPage": 1 + # } + # }, + # "timestamp": 1751632390031 + # } + # + data = self.safe_dict(response, 'data', {}) + rows = self.safe_list(data, 'rows', []) + rates = [] + for i in range(0, len(rows)): + entry = rows[i] + marketId = self.safe_string(entry, 'symbol') + timestamp = self.safe_integer(entry, 'fundingRateTimestamp') + rates.append({ + 'info': entry, + 'symbol': self.safe_symbol(marketId), + 'fundingRate': self.safe_number(entry, 'fundingRate'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + sorted = self.sort_by(rates, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, symbol, since, limit) + + def set_position_mode(self, hedged: bool, symbol: Str = None, params={}): + """ + set hedged to True or False for a market + + https://developer.woox.io/api-reference/endpoint/futures/position_mode + + :param bool hedged: set to True to use HEDGE_MODE, False for ONE_WAY + :param str symbol: not used by woo setPositionMode + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + hedgeMode = None + if hedged: + hedgeMode = 'HEDGE_MODE' + else: + hedgeMode = 'ONE_WAY' + request: dict = { + 'positionMode': hedgeMode, + } + response = self.v3PrivatePutFuturesPositionMode(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1752550492845 + # } + # + return response + + def fetch_leverage(self, symbol: str, params={}) -> Leverage: + """ + fetch the set leverage for a market + + https://developer.woox.io/api-reference/endpoint/account/get_account_info + https://developer.woox.io/api-reference/endpoint/futures/get_leverage + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: *for swap markets only* 'cross' or 'isolated' + :param str [params.positionMode]: *for swap markets only* 'ONE_WAY' or 'HEDGE_MODE' + :returns dict: a `leverage structure ` + """ + self.load_markets() + market = self.market(symbol) + response: dict = None + if market['spot']: + response = self.v3PrivateGetAccountInfo(params) + # + # { + # "success": True, + # "data": { + # "applicationId": "dsa", + # "account": "dsa", + # "alias": "haha", + # "otpauth": True, + # "accountMode": "FUTURES", + # "positionMode": "ONE_WAY", + # "leverage": 0, + # "marginRatio": "10", + # "openMarginRatio": "10", + # "initialMarginRatio": "10", + # "maintenanceMarginRatio": "0.03", + # "totalCollateral": "165.6115334", + # "freeCollateral": "165.6115334", + # "totalAccountValue": "167.52723093", + # "totalTradingValue": "167.52723093", + # "totalVaultValue": "0", + # "totalStakingValue": "0", + # "totalLaunchpadValue": "0", + # "totalEarnValue": "0", + # "referrerID": null, + # "accountType": "Main" + # }, + # "timestamp": 1752645129054 + # } + # + elif market['swap']: + request: dict = { + 'symbol': market['id'], + } + marginMode: Str = None + marginMode, params = self.handle_margin_mode_and_params('fetchLeverage', params, 'cross') + request['marginMode'] = self.encode_margin_mode(marginMode) + response = self.v3PrivateGetFuturesLeverage(self.extend(request, params)) + # + # HEDGE_MODE + # { + # "success": True, + # "data": + # { + # "symbol": "PERP_ETH_USDT", + # "marginMode": "CROSS", + # "positionMode": "HEDGE_MODE", + # "details": [ + # { + # "positionSide": "LONG", + # "leverage": 10 + # }, + # { + # "positionSide": "SHORT", + # "leverage": 10 + # } + # ] + # }, + # "timestamp": 1720886470482 + # } + # + # ONE_WAY + # { + # "success": True, + # "data": { + # "symbol": "PERP_ETH_USDT", + # "marginMode": "ISOLATED", + # "positionMode": "ONE_WAY", + # "details": [ + # { + # "positionSide": "BOTH", + # "leverage": 10 + # } + # ] + # }, + # "timestamp": 1720886810317 + # } + # + else: + raise NotSupported(self.id + ' fetchLeverage() is not supported for ' + market['type'] + ' markets') + data = self.safe_dict(response, 'data', {}) + return self.parse_leverage(data, market) + + def parse_leverage(self, leverage: dict, market: Market = None) -> Leverage: + marketId = self.safe_string(leverage, 'symbol') + market = self.safe_market(marketId, market) + marginMode = self.safe_string_lower(leverage, 'marginMode') + spotLeverage = self.safe_integer(leverage, 'leverage') + if spotLeverage == 0: + spotLeverage = None + longLeverage = spotLeverage + shortLeverage = spotLeverage + details = self.safe_list(leverage, 'details', []) + for i in range(0, len(details)): + position = self.safe_dict(details, i, {}) + positionLeverage = self.safe_integer(position, 'leverage') + side = self.safe_string(position, 'positionSide') + if side == 'BOTH': + longLeverage = positionLeverage + shortLeverage = positionLeverage + elif side == 'LONG': + longLeverage = positionLeverage + elif side == 'SHORT': + shortLeverage = positionLeverage + return { + 'info': leverage, + 'symbol': market['symbol'], + 'marginMode': marginMode, + 'longLeverage': longLeverage, + 'shortLeverage': shortLeverage, + } + + def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://developer.woox.io/api-reference/endpoint/spot_margin/set_leverage + https://developer.woox.io/api-reference/endpoint/futures/set_leverage + + :param float leverage: the rate of leverage(1, 2, 3, 4 or 5 for spot markets, 1, 2, 3, 4, 5, 10, 15, 20 for swap markets) + :param str [symbol]: unified market symbol(is mandatory for swap markets) + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: *for swap markets only* 'cross' or 'isolated' + :param str [params.positionMode]: *for swap markets only* 'ONE_WAY' or 'HEDGE_MODE' + :returns dict: response from the exchange + """ + self.load_markets() + request: dict = { + 'leverage': leverage, + } + market: Market = None + if symbol is not None: + market = self.market(symbol) + if (symbol is None) or market['spot']: + return self.v3PrivatePostSpotMarginLeverage(self.extend(request, params)) + elif market['swap']: + request['symbol'] = market['id'] + marginMode: Str = None + marginMode, params = self.handle_margin_mode_and_params('fetchLeverage', params, 'cross') + request['marginMode'] = self.encode_margin_mode(marginMode) + return self.v3PrivatePutFuturesLeverage(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchLeverage() is not supported for ' + market['type'] + ' markets') + + def add_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + add margin + + https://docs.woox.io/#update-isolated-margin-setting + + :param str symbol: unified market symbol + :param float amount: amount of margin to add + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.position_side]: 'LONG' or 'SHORT' in hedge mode, 'BOTH' in one way mode + :returns dict: a `margin structure ` + """ + return self.modify_margin_helper(symbol, amount, 'ADD', params) + + def reduce_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + remove margin from a position + + https://docs.woox.io/#update-isolated-margin-setting + + :param str symbol: unified market symbol + :param float amount: amount of margin to remove + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.position_side]: 'LONG' or 'SHORT' in hedge mode, 'BOTH' in one way mode + :returns dict: a `margin structure ` + """ + return self.modify_margin_helper(symbol, amount, 'REDUCE', params) + + def modify_margin_helper(self, symbol: str, amount, type, params={}) -> MarginModification: + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'adjust_token': 'USDT', # todo check + 'adjust_amount': amount, + 'action': type, + } + return self.v1PrivatePostClientIsolatedMargin(self.extend(request, params)) + + def fetch_position(self, symbol: Str, params={}): + """ + fetch data on an open position + + https://developer.woox.io/api-reference/endpoint/futures/get_positions + + :param str symbol: unified market symbol of the market the position is held in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `position structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = self.v3PrivateGetFuturesPositions(self.extend(request, params)) + # + # { + # "success": True, + # "data": { + # "positions": [ + # { + # "symbol": "PERP_LTC_USDT", + # "holding": "0.1", + # "pendingLongQty": "0", + # "pendingShortQty": "0", + # "settlePrice": "96.87", + # "averageOpenPrice": "96.87", + # "pnl24H": "0", + # "fee24H": "0.0048435", + # "markPrice": "96.83793449", + # "estLiqPrice": "0", + # "timestamp": 1752500555823, + # "adlQuantile": 2, + # "positionSide": "BOTH", + # "marginMode": "CROSS", + # "isolatedMarginToken": "", + # "isolatedMarginAmount": "0", + # "isolatedFrozenLong": "0", + # "isolatedFrozenShort": "0", + # "leverage": 10 + # } + # ] + # }, + # "timestamp": 1752500579848 + # } + # + result = self.safe_dict(response, 'data', {}) + positions = self.safe_list(result, 'positions', []) + first = self.safe_dict(positions, 0, {}) + return self.parse_position(first, market) + + def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://developer.woox.io/api-reference/endpoint/futures/get_positions + + :param str[] [symbols]: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + self.load_markets() + response = self.v3PrivateGetFuturesPositions(params) + # + # { + # "success": True, + # "data": { + # "positions": [ + # { + # "symbol": "PERP_LTC_USDT", + # "holding": "0.1", + # "pendingLongQty": "0", + # "pendingShortQty": "0", + # "settlePrice": "96.87", + # "averageOpenPrice": "96.87", + # "pnl24H": "0", + # "fee24H": "0.0048435", + # "markPrice": "96.83793449", + # "estLiqPrice": "0", + # "timestamp": 1752500555823, + # "adlQuantile": 2, + # "positionSide": "BOTH", + # "marginMode": "CROSS", + # "isolatedMarginToken": "", + # "isolatedMarginAmount": "0", + # "isolatedFrozenLong": "0", + # "isolatedFrozenShort": "0", + # "leverage": 10 + # } + # ] + # }, + # "timestamp": 1752500579848 + # } + # + result = self.safe_dict(response, 'data', {}) + positions = self.safe_list(result, 'positions', []) + return self.parse_positions(positions, symbols) + + def parse_position(self, position: dict, market: Market = None): + # + # v1PrivateGetPositionSymbol + # { + # "symbol": "PERP_ETH_USDT", + # "position_side": "BOTH", + # "leverage": 10, + # "margin_mode": "CROSS", + # "average_open_price": 3139.9, + # "isolated_margin_amount": 0.0, + # "isolated_margin_token": "", + # "opening_time": "1720627963.094", + # "mark_price": 3155.19169891, + # "pending_short_qty": 0.0, + # "pending_long_qty": 0.0, + # "holding": -0.7, + # "pnl_24_h": 0.0, + # "est_liq_price": 9107.40055552, + # "settle_price": 3151.0319904, + # "success": True, + # "fee_24_h": 0.0, + # "isolated_frozen_long": 0.0, + # "isolated_frozen_short": 0.0, + # "timestamp": "1720867502.544" + # } + # + # v3PrivateGetPositions + # { + # "symbol": "PERP_LTC_USDT", + # "holding": "0.1", + # "pendingLongQty": "0", + # "pendingShortQty": "0", + # "settlePrice": "96.87", + # "averageOpenPrice": "96.87", + # "pnl24H": "0", + # "fee24H": "0.0048435", + # "markPrice": "96.83793449", + # "estLiqPrice": "0", + # "timestamp": 1752500555823, + # "adlQuantile": 2, + # "positionSide": "BOTH", + # "marginMode": "CROSS", + # "isolatedMarginToken": "", + # "isolatedMarginAmount": "0", + # "isolatedFrozenLong": "0", + # "isolatedFrozenShort": "0", + # "leverage": 10 + # } + # + contract = self.safe_string(position, 'symbol') + market = self.safe_market(contract, market) + size = self.safe_string(position, 'holding') + side: Str = None + if Precise.string_gt(size, '0'): + side = 'long' + else: + side = 'short' + contractSize = self.safe_string(market, 'contractSize') + markPrice = self.safe_string_2(position, 'markPrice', 'mark_price') + timestampString = self.safe_string(position, 'timestamp') + timestamp = None + if timestampString is not None: + if timestampString.find('.') > -1: + timestamp = self.safe_timestamp(position, 'timestamp') + else: + timestamp = self.safe_integer(position, 'timestamp') + entryPrice = self.safe_string_2(position, 'averageOpenPrice', 'average_open_price') + priceDifference = Precise.string_sub(markPrice, entryPrice) + unrealisedPnl = Precise.string_mul(priceDifference, size) + size = Precise.string_abs(size) + notional = Precise.string_mul(size, markPrice) + positionSide = self.safe_string(position, 'positionSide') # 'SHORT' or 'LONG' for hedged, 'BOTH' for non-hedged + return self.safe_position({ + 'info': position, + 'id': None, + 'symbol': self.safe_string(market, 'symbol'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastUpdateTimestamp': None, + 'initialMargin': None, + 'initialMarginPercentage': None, + 'maintenanceMargin': None, + 'maintenanceMarginPercentage': None, + 'entryPrice': self.parse_number(entryPrice), + 'notional': self.parse_number(notional), + 'leverage': self.safe_number(position, 'leverage'), + 'unrealizedPnl': self.parse_number(unrealisedPnl), + 'contracts': self.parse_number(size), + 'contractSize': self.parse_number(contractSize), + 'marginRatio': None, + 'liquidationPrice': self.safe_number_2(position, 'estLiqPrice', 'est_liq_price'), + 'markPrice': self.parse_number(markPrice), + 'lastPrice': None, + 'collateral': None, + 'marginMode': self.safe_string_lower_2(position, 'marginMode', 'margin_mode'), + 'side': side, + 'percentage': None, + 'hedged': positionSide != 'BOTH', + 'stopLossPrice': None, + 'takeProfitPrice': None, + }) + + def fetch_convert_quote(self, fromCode: str, toCode: str, amount: Num = None, params={}) -> Conversion: + """ + fetch a quote for converting from one currency to another + + https://docs.woox.io/#get-quote-rfq + + :param str fromCode: the currency that you want to sell and convert from + :param str toCode: the currency that you want to buy and convert into + :param float [amount]: how much you want to trade in units of the from currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `conversion structure ` + """ + self.load_markets() + request: dict = { + 'sellToken': fromCode.upper(), + 'buyToken': toCode.upper(), + 'sellQuantity': self.number_to_string(amount), + } + response = self.v3PrivateGetConvertRfq(self.extend(request, params)) + # + # { + # "success": True, + # "data": { + # "quoteId": 123123123, + # "counterPartyId": "", + # "sellToken": "ETH", + # "sellQuantity": "0.0445", + # "buyToken": "USDT", + # "buyQuantity": "33.45", + # "buyPrice": "6.77", + # "expireTimestamp": 1659084466000, + # "message": 1659084466000 + # } + # } + # + data = self.safe_dict(response, 'data', {}) + fromCurrencyId = self.safe_string(data, 'sellToken', fromCode) + fromCurrency = self.currency(fromCurrencyId) + toCurrencyId = self.safe_string(data, 'buyToken', toCode) + toCurrency = self.currency(toCurrencyId) + return self.parse_conversion(data, fromCurrency, toCurrency) + + def create_convert_trade(self, id: str, fromCode: str, toCode: str, amount: Num = None, params={}) -> Conversion: + """ + convert from one currency to another + + https://docs.woox.io/#send-quote-rft + + :param str id: the id of the trade that you want to make + :param str fromCode: the currency that you want to sell and convert from + :param str toCode: the currency that you want to buy and convert into + :param float [amount]: how much you want to trade in units of the from currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `conversion structure ` + """ + self.load_markets() + request: dict = { + 'quoteId': id, + } + response = self.v3PrivatePostConvertRft(self.extend(request, params)) + # + # { + # "success": True, + # "data": { + # "quoteId": 123123123, + # "counterPartyId": "", + # "rftAccepted": 1 # 1 -> success; 2 -> processing; 3 -> fail + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_conversion(data) + + def fetch_convert_trade(self, id: str, code: Str = None, params={}) -> Conversion: + """ + fetch the data for a conversion trade + + https://docs.woox.io/#get-quote-trade + + :param str id: the id of the trade that you want to fetch + :param str [code]: the unified currency code of the conversion trade + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `conversion structure ` + """ + self.load_markets() + request: dict = { + 'quoteId': id, + } + response = self.v3PrivateGetConvertTrade(self.extend(request, params)) + # + # { + # "success": True, + # "data": { + # "quoteId": 12, + # "buyAsset": "", + # "sellAsset": "", + # "buyAmount": 12.11, + # "sellAmount": 12.11, + # "tradeStatus": 12, + # "createdTime": "" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + fromCurrencyId = self.safe_string(data, 'sellAsset') + toCurrencyId = self.safe_string(data, 'buyAsset') + fromCurrency = None + toCurrency = None + if fromCurrencyId is not None: + fromCurrency = self.currency(fromCurrencyId) + if toCurrencyId is not None: + toCurrency = self.currency(toCurrencyId) + return self.parse_conversion(data, fromCurrency, toCurrency) + + def fetch_convert_trade_history(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Conversion]: + """ + fetch the users history of conversion trades + + https://docs.woox.io/#get-quote-trades + + :param str [code]: the unified currency code + :param int [since]: the earliest time in ms to fetch conversions for + :param int [limit]: the maximum number of conversion structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest conversion to fetch + :returns dict[]: a list of `conversion structures ` + """ + self.load_markets() + request: dict = {} + request, params = self.handle_until_option('endTime', request, params) + if since is not None: + request['startTime'] = since + if limit is not None: + request['size'] = limit + response = self.v3PrivateGetConvertTrades(self.extend(request, params)) + # + # { + # "success": True, + # "data": { + # "count": 12, + # "tradeVos":[ + # { + # "quoteId": 12, + # "buyAsset": "", + # "sellAsset": "", + # "buyAmount": 12.11, + # "sellAmount": 12.11, + # "tradeStatus": 12, + # "createdTime": "" + # } + # ... + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + rows = self.safe_list(data, 'tradeVos', []) + return self.parse_conversions(rows, code, 'sellAsset', 'buyAsset', since, limit) + + def parse_conversion(self, conversion: dict, fromCurrency: Currency = None, toCurrency: Currency = None) -> Conversion: + # + # fetchConvertQuote + # + # { + # "quoteId": 123123123, + # "counterPartyId": "", + # "sellToken": "ETH", + # "sellQuantity": "0.0445", + # "buyToken": "USDT", + # "buyQuantity": "33.45", + # "buyPrice": "6.77", + # "expireTimestamp": 1659084466000, + # "message": 1659084466000 + # } + # + # createConvertTrade + # + # { + # "quoteId": 123123123, + # "counterPartyId": "", + # "rftAccepted": 1 # 1 -> success; 2 -> processing; 3 -> fail + # } + # + # fetchConvertTrade, fetchConvertTradeHistory + # + # { + # "quoteId": 12, + # "buyAsset": "", + # "sellAsset": "", + # "buyAmount": 12.11, + # "sellAmount": 12.11, + # "tradeStatus": 12, + # "createdTime": "" + # } + # + timestamp = self.safe_integer_2(conversion, 'expireTimestamp', 'createdTime') + fromCurr = self.safe_string_2(conversion, 'sellToken', 'buyAsset') + fromCode = self.safe_currency_code(fromCurr, fromCurrency) + to = self.safe_string_2(conversion, 'buyToken', 'sellAsset') + toCode = self.safe_currency_code(to, toCurrency) + return { + 'info': conversion, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': self.safe_string(conversion, 'quoteId'), + 'fromCurrency': fromCode, + 'fromAmount': self.safe_number_2(conversion, 'sellQuantity', 'sellAmount'), + 'toCurrency': toCode, + 'toAmount': self.safe_number_2(conversion, 'buyQuantity', 'buyAmount'), + 'price': self.safe_number(conversion, 'buyPrice'), + 'fee': None, + } + + def fetch_convert_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies that can be converted + + https://docs.woox.io/#get-quote-asset-info + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + self.load_markets() + response = self.v3PrivateGetConvertAssetInfo(params) + # + # { + # "success": True, + # "rows": [ + # { + # "token": "BTC", + # "tick": 0.0001, + # "createdTime": "1575014248.99", # Unix epoch time in seconds + # "updatedTime": "1575014248.99" # Unix epoch time in seconds + # }, + # ] + # } + # + result: dict = {} + data = self.safe_list(response, 'rows', []) + for i in range(0, len(data)): + entry = data[i] + id = self.safe_string(entry, 'token') + code = self.safe_currency_code(id) + result[code] = { + 'info': entry, + 'id': id, + 'code': code, + 'networks': None, + 'type': None, + 'name': None, + 'active': None, + 'deposit': None, + 'withdraw': None, + 'fee': None, + 'precision': self.safe_number(entry, 'tick'), + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + 'deposit': { + 'min': None, + 'max': None, + }, + }, + 'created': self.safe_timestamp(entry, 'createdTime'), + } + return result + + def default_network_code_for_currency(self, code): + currencyItem = self.currency(code) + networks = currencyItem['networks'] + networkKeys = list(networks.keys()) + for i in range(0, len(networkKeys)): + network = networkKeys[i] + if network == 'ETH': + return network + # if it was not returned according to above options, then return the first network of currency + return self.safe_value(networkKeys, 0) + + def set_sandbox_mode(self, enable: bool): + super(woo, self).set_sandbox_mode(enable) + self.options['sandboxMode'] = enable diff --git a/ccxt/woofipro.py b/ccxt/woofipro.py new file mode 100644 index 0000000..79f8f48 --- /dev/null +++ b/ccxt/woofipro.py @@ -0,0 +1,2824 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.woofipro import ImplicitAPI +from ccxt.base.types import Any, Balances, Currencies, Currency, Int, LedgerEntry, Leverage, Market, Num, Order, OrderBook, OrderRequest, OrderSide, OrderType, Position, Str, Strings, FundingRate, FundingRates, Trade, TradingFees, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import NotSupported +from ccxt.base.errors import NetworkError +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class woofipro(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(woofipro, self).describe(), { + 'id': 'woofipro', + 'name': 'WOOFI PRO', + 'countries': ['KY'], # Cayman Islands + 'rateLimit': 100, + 'version': 'v1', + 'certified': True, + 'pro': True, + 'dex': True, + 'hostname': 'dex.woo.org', + 'has': { + 'CORS': None, + 'spot': False, + 'margin': False, + 'swap': True, + 'future': False, + 'option': False, + 'addMargin': False, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'cancelOrders': True, + 'cancelWithdraw': False, + 'closeAllPositions': False, + 'closePosition': False, + 'createConvertTrade': False, + 'createDepositAddress': False, + 'createMarketBuyOrderWithCost': False, + 'createMarketOrder': False, + 'createMarketOrderWithCost': False, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createOrderWithTakeProfitAndStopLoss': True, + 'createReduceOnlyOrder': True, + 'createStopLimitOrder': False, + 'createStopLossOrder': True, + 'createStopMarketOrder': False, + 'createStopOrder': False, + 'createTakeProfitOrder': True, + 'createTrailingAmountOrder': False, + 'createTrailingPercentOrder': False, + 'createTriggerOrder': True, + 'fetchAccounts': False, + 'fetchBalance': True, + 'fetchCanceledOrders': False, + 'fetchClosedOrder': False, + 'fetchClosedOrders': True, + 'fetchConvertCurrencies': False, + 'fetchConvertQuote': False, + 'fetchCurrencies': True, + 'fetchDepositAddress': False, + 'fetchDeposits': True, + 'fetchDepositsWithdrawals': True, + 'fetchFundingHistory': True, + 'fetchFundingInterval': True, + 'fetchFundingIntervals': False, + 'fetchFundingRate': True, + 'fetchFundingRateHistory': True, + 'fetchFundingRates': True, + 'fetchIndexOHLCV': False, + 'fetchLedger': True, + 'fetchLeverage': True, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterestHistory': False, + 'fetchOpenOrder': False, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrders': True, + 'fetchOrderTrades': True, + 'fetchPosition': True, + 'fetchPositionMode': False, + 'fetchPositions': True, + 'fetchPremiumIndexOHLCV': False, + 'fetchStatus': True, + 'fetchTicker': False, + 'fetchTickers': False, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': True, + 'fetchTransactions': 'emulated', + 'fetchTransfers': False, + 'fetchWithdrawals': True, + 'reduceMargin': False, + 'setLeverage': True, + 'setMargin': False, + 'setPositionMode': False, + 'transfer': False, + 'withdraw': True, # exchange have that endpoint disabled atm, but was once implemented in ccxt per old docs: https://kronosresearch.github.io/wootrade-documents/#token-withdraw + }, + 'timeframes': { + '1m': '1m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1h', + '4h': '4h', + '12h': '12h', + '1d': '1d', + '1w': '1w', + '1M': '1mon', + '1y': '1y', + }, + 'urls': { + 'logo': 'https://github.com/user-attachments/assets/9ba21b8a-a9c7-4770-b7f1-ce3bcbde68c1', + 'api': { + 'public': 'https://api-evm.orderly.org', + 'private': 'https://api-evm.orderly.org', + }, + 'test': { + 'public': 'https://testnet-api-evm.orderly.org', + 'private': 'https://testnet-api-evm.orderly.org', + }, + 'www': 'https://dex.woo.org', + 'doc': [ + 'https://orderly.network/docs/build-on-evm/building-on-evm', + ], + 'fees': [ + 'https://dex.woo.org/en/orderly', + ], + 'referral': { + 'url': 'https://dex.woo.org/en/trade?ref=CCXT', + 'discount': 0.05, + }, + }, + 'api': { + 'v1': { + 'public': { + 'get': { + 'public/volume/stats': 1, + 'public/broker/name': 1, + 'public/chain_info/{broker_id}': 1, + 'public/system_info': 1, + 'public/vault_balance': 1, + 'public/insurancefund': 1, + 'public/chain_info': 1, + 'faucet/usdc': 1, + 'public/account': 1, + 'get_account': 1, + 'registration_nonce': 1, + 'get_orderly_key': 1, + 'public/liquidation': 1, + 'public/liquidated_positions': 1, + 'public/config': 1, + 'public/campaign/ranking': 10, + 'public/campaign/stats': 10, + 'public/campaign/user': 10, + 'public/campaign/stats/details': 10, + 'public/campaigns': 10, + 'public/points/leaderboard': 1, + 'client/points': 1, + 'public/points/epoch': 1, + 'public/points/epoch_dates': 1, + 'public/referral/check_ref_code': 1, + 'public/referral/verify_ref_code': 1, + 'referral/admin_info': 1, + 'referral/info': 1, + 'referral/referee_info': 1, + 'referral/referee_rebate_summary': 1, + 'referral/referee_history': 1, + 'referral/referral_history': 1, + 'referral/rebate_summary': 1, + 'client/distribution_history': 1, + 'tv/config': 1, + 'tv/history': 1, + 'tv/symbol_info': 1, + 'public/funding_rate_history': 1, + 'public/funding_rate/{symbol}': 0.33, + 'public/funding_rates': 1, + 'public/info': 1, + 'public/info/{symbol}': 1, + 'public/market_trades': 1, + 'public/token': 1, + 'public/futures': 1, + 'public/futures/{symbol}': 1, + }, + 'post': { + 'register_account': 1, + }, + }, + 'private': { + 'get': { + 'client/key_info': 6, + 'client/orderly_key_ip_restriction': 6, + 'order/{oid}': 1, + 'client/order/{client_order_id}': 1, + 'algo/order/{oid}': 1, + 'algo/client/order/{client_order_id}': 1, + 'orders': 1, + 'algo/orders': 1, + 'trade/{tid}': 1, + 'trades': 1, + 'order/{oid}/trades': 1, + 'client/liquidator_liquidations': 1, + 'liquidations': 1, + 'asset/history': 60, + 'client/holding': 1, + 'withdraw_nonce': 1, + 'settle_nonce': 1, + 'pnl_settlement/history': 1, + 'volume/user/daily': 60, + 'volume/user/stats': 60, + 'client/statistics': 60, + 'client/info': 60, + 'client/statistics/daily': 60, + 'positions': 3.33, + 'position/{symbol}': 3.33, + 'funding_fee/history': 30, + 'notification/inbox/notifications': 60, + 'notification/inbox/unread': 60, + 'volume/broker/daily': 60, + 'broker/fee_rate/default': 10, + 'broker/user_info': 10, + 'orderbook/{symbol}': 1, + 'kline': 1, + }, + 'post': { + 'orderly_key': 1, + 'client/set_orderly_key_ip_restriction': 6, + 'client/reset_orderly_key_ip_restriction': 6, + 'order': 1, + 'batch-order': 10, + 'algo/order': 1, + 'liquidation': 1, + 'claim_insurance_fund': 1, + 'withdraw_request': 1, + 'settle_pnl': 1, + 'notification/inbox/mark_read': 60, + 'notification/inbox/mark_read_all': 60, + 'client/leverage': 120, + 'client/maintenance_config': 60, + 'delegate_signer': 10, + 'delegate_orderly_key': 10, + 'delegate_settle_pnl': 10, + 'delegate_withdraw_request': 10, + 'broker/fee_rate/set': 10, + 'broker/fee_rate/set_default': 10, + 'broker/fee_rate/default': 10, + 'referral/create': 10, + 'referral/update': 10, + 'referral/bind': 10, + 'referral/edit_split': 10, + }, + 'put': { + 'order': 1, + 'algo/order': 1, + }, + 'delete': { + 'order': 1, + 'algo/order': 1, + 'client/order': 1, + 'algo/client/order': 1, + 'algo/orders': 1, + 'orders': 1, + 'batch-order': 1, + 'client/batch-order': 1, + }, + }, + }, + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + 'accountId': True, + 'privateKey': False, + }, + 'fees': { + 'trading': { + 'tierBased': True, + 'percentage': True, + 'maker': self.parse_number('0.0002'), + 'taker': self.parse_number('0.0005'), + }, + }, + 'options': { + 'sandboxMode': False, + 'brokerId': 'CCXT', + 'verifyingContractAddress': '0x6F7a338F2aA472838dEFD3283eB360d4Dff5D203', + }, + 'features': { + 'default': { + 'sandbox': True, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, + 'triggerPriceType': None, + 'triggerDirection': False, + 'stopLossPrice': False, # todo by triggerPrice + 'takeProfitPrice': False, # todo by triggerPrice + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': True, + 'leverage': True, # todo implement + 'marketBuyByCost': False, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': False, + 'iceberg': True, # todo implement + }, + 'createOrders': { + 'max': 10, + }, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 500, + 'daysBack': None, + 'untilDays': 100000, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': True, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 500, + 'trigger': True, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 500, + 'daysBack': None, + 'daysBackCanceled': None, + 'untilDays': 100000, + 'trigger': True, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 1000, + }, + }, + 'spot': { + 'extends': 'default', + }, + 'forDerivatives': { + 'extends': 'default', + 'createOrder': { + # todo: implementation needs unification + 'triggerPriceType': None, + 'attachedStopLossTakeProfit': { + # todo: implementation needs unification + 'triggerPriceType': None, + 'price': False, + }, + }, + }, + 'swap': { + 'linear': { + 'extends': 'forDerivatives', + }, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'commonCurrencies': {}, + 'exceptions': { + 'exact': { + '-1000': ExchangeError, # UNKNOWN The data does not exist + '-1001': AuthenticationError, # INVALID_SIGNATURE The api key or secret is in wrong format. + '-1002': AuthenticationError, # UNAUTHORIZED API key or secret is invalid, it may because key have insufficient permission or the key is expired/revoked. + '-1003': RateLimitExceeded, # TOO_MANY_REQUEST Rate limit exceed. + '-1004': BadRequest, # UNKNOWN_PARAM An unknown parameter was sent. + '-1005': BadRequest, # INVALID_PARAM Some parameters are in wrong format for api. + '-1006': InvalidOrder, # RESOURCE_NOT_FOUND The data is not found in server. For example, when client try canceling a CANCELLED order, will raise self error. + '-1007': BadRequest, # DUPLICATE_REQUEST The data is already exists or your request is duplicated. + '-1008': InvalidOrder, # QUANTITY_TOO_HIGH The quantity of settlement is too high than you can request. + '-1009': InsufficientFunds, # CAN_NOT_WITHDRAWAL Can not request withdrawal settlement, you need to deposit other arrears first. + '-1011': NetworkError, # RPC_NOT_CONNECT Can not place/cancel orders, it may because internal network error. Please try again in a few seconds. + '-1012': BadRequest, # RPC_REJECT The place/cancel order request is rejected by internal module, it may because the account is in liquidation or other internal errors. Please try again in a few seconds. + '-1101': InsufficientFunds, # RISK_TOO_HIGH The risk exposure for client is too high, it may cause by sending too big order or the leverage is too low. please refer to client info to check the current exposure. + '-1102': InvalidOrder, # MIN_NOTIONAL The order value(price * size) is too small. + '-1103': InvalidOrder, # PRICE_FILTER The order price is not following the tick size rule for the symbol. + '-1104': InvalidOrder, # SIZE_FILTER The order quantity is not following the step size rule for the symbol. + '-1105': InvalidOrder, # PERCENTAGE_FILTER Price is X% too high or X% too low from the mid price. + '-1201': BadRequest, # LIQUIDATION_REQUEST_RATIO_TOO_SMALL total notional < 10000, least req ratio should = 1 + '-1202': BadRequest, # LIQUIDATION_STATUS_ERROR No need to liquidation because user margin is enough. + '29': BadRequest, # {"success":false,"code":29,"message":"Verify contract is invalid"} + '9': AuthenticationError, # {"success":false,"code":9,"message":"Address and signature do not match"} + '3': AuthenticationError, # {"success":false,"code":3,"message":"Signature error"} + '2': BadRequest, # {"success":false,"code":2,"message":"Timestamp expired"} + '15': BadRequest, # {"success":false,"code":15,"message":"BrokerId is not exist"} + }, + 'broad': { + }, + }, + 'precisionMode': TICK_SIZE, + }) + + def set_sandbox_mode(self, enable: bool): + super(woofipro, self).set_sandbox_mode(enable) + self.options['sandboxMode'] = enable + + def fetch_status(self, params={}): + """ + the latest known information on the availability of the exchange API + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/public/get-system-maintenance-status + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `status structure ` + """ + response = self.v1PublicGetPublicSystemInfo(params) + # + # { + # "success": True, + # "data": { + # "status": 0, + # "msg": "System is functioning properly." + # }, + # "timestamp": "1709274106602" + # } + # + data = self.safe_dict(response, 'data', {}) + status = self.safe_string(data, 'status') + if status is None: + status = 'error' + elif status == '0': + status = 'ok' + else: + status = 'maintenance' + return { + 'status': status, + 'updated': None, + 'eta': None, + 'url': None, + 'info': response, + } + + def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/public/get-system-maintenance-status + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = self.v1PublicGetPublicSystemInfo(params) + # + # { + # "success": True, + # "data": { + # "status": 0, + # "msg": "System is functioning properly." + # }, + # "timestamp": "1709274106602" + # } + # + return self.safe_integer(response, 'timestamp') + + def parse_market(self, market: dict) -> Market: + # + # { + # "symbol": "PERP_BTC_USDC", + # "quote_min": 123, + # "quote_max": 100000, + # "quote_tick": 0.1, + # "base_min": 0.00001, + # "base_max": 20, + # "base_tick": 0.00001, + # "min_notional": 1, + # "price_range": 0.02, + # "price_scope": 0.4, + # "std_liquidation_fee": 0.03, + # "liquidator_fee": 0.015, + # "claim_insurance_fund_discount": 0.0075, + # "funding_period": 8, + # "cap_funding": 0.000375, + # "floor_funding": -0.000375, + # "interest_rate": 0.0001, + # "created_time": 1684140107326, + # "updated_time": 1685345968053, + # "base_mmr": 0.05, + # "base_imr": 0.1, + # "imr_factor": 0.0002512, + # "liquidation_tier": "1" + # } + # + marketId = self.safe_string(market, 'symbol') + parts = marketId.split('_') + marketType = 'swap' + baseId = self.safe_string(parts, 1) + quoteId = self.safe_string(parts, 2) + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + settleId: Str = self.safe_string(parts, 2) + settle: Str = self.safe_currency_code(settleId) + symbol = base + '/' + quote + ':' + settle + return { + 'id': marketId, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': marketType, + 'spot': False, + 'margin': False, + 'swap': True, + 'future': False, + 'option': False, + 'active': None, + 'contract': True, + 'linear': True, + 'inverse': False, + 'contractSize': self.parse_number('1'), + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.safe_number(market, 'base_tick'), + 'price': self.safe_number(market, 'quote_tick'), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(market, 'base_min'), + 'max': self.safe_number(market, 'base_max'), + }, + 'price': { + 'min': self.safe_number(market, 'quote_min'), + 'max': self.safe_number(market, 'quote_max'), + }, + 'cost': { + 'min': self.safe_number(market, 'min_notional'), + 'max': None, + }, + }, + 'created': self.safe_integer(market, 'created_time'), + 'info': market, + } + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for woofipro + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/public/get-available-symbols + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = self.v1PublicGetPublicInfo(params) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "rows": [ + # { + # "symbol": "PERP_BTC_USDC", + # "quote_min": 123, + # "quote_max": 100000, + # "quote_tick": 0.1, + # "base_min": 0.00001, + # "base_max": 20, + # "base_tick": 0.00001, + # "min_notional": 1, + # "price_range": 0.02, + # "price_scope": 0.4, + # "std_liquidation_fee": 0.03, + # "liquidator_fee": 0.015, + # "claim_insurance_fund_discount": 0.0075, + # "funding_period": 8, + # "cap_funding": 0.000375, + # "floor_funding": -0.000375, + # "interest_rate": 0.0001, + # "created_time": 1684140107326, + # "updated_time": 1685345968053, + # "base_mmr": 0.05, + # "base_imr": 0.1, + # "imr_factor": 0.0002512, + # "liquidation_tier": "1" + # } + # ] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + rows = self.safe_list(data, 'rows', []) + return self.parse_markets(rows) + + def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://orderly.network/docs/build-on-omnichain/evm-api/restful-api/public/get-supported-collateral-info#get-supported-collateral-info + https://orderly.network/docs/build-on-omnichain/evm-api/restful-api/public/get-supported-chains-per-builder#get-supported-chains-per-builder + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + result: dict = {} + tokenPromise = self.v1PublicGetPublicToken(params) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "rows": [{ + # "token": "USDC", + # "decimals": 6, + # "minimum_withdraw_amount": 0.000001, + # "token_hash": "0xd6aca1be9729c13d677335161321649cccae6a591554772516700f986f942eaa", + # "chain_details": [{ + # "chain_id": 43113, + # "contract_address": "0x5d64c9cfb0197775b4b3ad9be4d3c7976e0d8dc3", + # "cross_chain_withdrawal_fee": 123, + # "decimals": 6, + # "withdraw_fee": 2 + # }] + # } + # ] + # } + # } + # + chainPromise = self.v1PublicGetPublicChainInfo(params) + tokenResponse, chainResponse = [tokenPromise, chainPromise] + tokenData = self.safe_dict(tokenResponse, 'data', {}) + tokenRows = self.safe_list(tokenData, 'rows', []) + chainData = self.safe_dict(chainResponse, 'data', {}) + chainRows = self.safe_list(chainData, 'rows', []) + indexedChains = self.index_by(chainRows, 'chain_id') + for i in range(0, len(tokenRows)): + token = tokenRows[i] + currencyId = self.safe_string(token, 'token') + networks = self.safe_list(token, 'chain_details') + code = self.safe_currency_code(currencyId) + resultingNetworks: dict = {} + for j in range(0, len(networks)): + networkEntry = networks[j] + networkId = self.safe_string(networkEntry, 'chain_id') + networkRow = self.safe_dict(indexedChains, networkId) + networkName = self.safe_string(networkRow, 'name') + networkCode = self.network_id_to_code(networkName, code) + resultingNetworks[networkCode] = { + 'id': networkId, + 'network': networkCode, + 'limits': { + 'withdraw': { + 'min': None, + 'max': None, + }, + 'deposit': { + 'min': None, + 'max': None, + }, + }, + 'active': None, + 'deposit': None, + 'withdraw': None, + 'fee': self.safe_number(networkEntry, 'withdrawal_fee'), + 'precision': self.parse_number(self.parse_precision(self.safe_string(networkEntry, 'decimals'))), + 'info': [networkEntry, networkRow], + } + result[code] = self.safe_currency_structure({ + 'id': currencyId, + 'name': None, + 'code': code, + 'precision': None, + 'active': None, + 'fee': None, + 'networks': resultingNetworks, + 'deposit': None, + 'withdraw': None, + 'limits': { + 'deposit': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': self.safe_number(token, 'minimum_withdraw_amount'), + 'max': None, + }, + }, + 'info': token, + }) + return result + + def parse_token_and_fee_temp(self, item, feeTokenKey, feeAmountKey): + feeCost = self.safe_string(item, feeAmountKey) + fee = None + if feeCost is not None: + feeCurrencyId = self.safe_string(item, feeTokenKey) + feeCurrencyCode = self.safe_currency_code(feeCurrencyId) + fee = { + 'cost': feeCost, + 'currency': feeCurrencyCode, + } + return fee + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # public/market_trades + # + # { + # "symbol": "SPOT_BTC_USDT", + # "side": "SELL", + # "executed_price": 46222.35, + # "executed_quantity": 0.0012, + # "executed_timestamp": "1683878609166" + # } + # + # fetchOrderTrades, fetchOrder + # + # { + # "id": "99119876", + # "symbol": "SPOT_WOO_USDT", + # "fee": "0.0024", + # "side": "BUY", + # "executed_timestamp": "1641481113084", + # "order_id": "87001234", + # "order_tag": "default", <-- self param only in "fetchOrderTrades" + # "executed_price": "1", + # "executed_quantity": "12", + # "fee_asset": "WOO", + # "is_maker": "1" + # } + # + isFromFetchOrder = ('id' in trade) + timestamp = self.safe_integer(trade, 'executed_timestamp') + marketId = self.safe_string(trade, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + price = self.safe_string(trade, 'executed_price') + amount = self.safe_string(trade, 'executed_quantity') + order_id = self.safe_string(trade, 'order_id') + fee = self.parse_token_and_fee_temp(trade, 'fee_asset', 'fee') + feeCost = self.safe_string(fee, 'cost') + if feeCost is not None: + fee['cost'] = feeCost + cost = Precise.string_mul(price, amount) + side = self.safe_string_lower(trade, 'side') + id = self.safe_string(trade, 'id') + takerOrMaker: Str = None + if isFromFetchOrder: + isMaker = self.safe_string(trade, 'is_maker') == '1' + takerOrMaker = 'maker' if isMaker else 'taker' + return self.safe_trade({ + 'id': id, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'side': side, + 'price': price, + 'amount': amount, + 'cost': cost, + 'order': order_id, + 'takerOrMaker': takerOrMaker, + 'type': None, + 'fee': fee, + 'info': trade, + }, market) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/public/get-market-trades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + request['limit'] = limit + response = self.v1PublicGetPublicMarketTrades(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "rows": [{ + # "symbol": "PERP_ETH_USDC", + # "side": "BUY", + # "executed_price": 2050, + # "executed_quantity": 1, + # "executed_timestamp": 1683878609166 + # }] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + rows = self.safe_list(data, 'rows', []) + return self.parse_trades(rows, market, since, limit) + + def parse_funding_rate(self, fundingRate, market: Market = None) -> FundingRate: + # + # { + # "symbol":"PERP_AAVE_USDT", + # "est_funding_rate":-0.00003447, + # "est_funding_rate_timestamp":1653633959001, + # "last_funding_rate":-0.00002094, + # "last_funding_rate_timestamp":1653631200000, + # "next_funding_time":1653634800000, + # "sum_unitary_funding": 521.367 + # } + # + symbol = self.safe_string(fundingRate, 'symbol') + market = self.market(symbol) + nextFundingTimestamp = self.safe_integer(fundingRate, 'next_funding_time') + estFundingRateTimestamp = self.safe_integer(fundingRate, 'est_funding_rate_timestamp') + lastFundingRateTimestamp = self.safe_integer(fundingRate, 'last_funding_rate_timestamp') + fundingTimeString = self.safe_string(fundingRate, 'last_funding_rate_timestamp') + nextFundingTimeString = self.safe_string(fundingRate, 'next_funding_time') + millisecondsInterval = Precise.string_sub(nextFundingTimeString, fundingTimeString) + return { + 'info': fundingRate, + 'symbol': market['symbol'], + 'markPrice': None, + 'indexPrice': None, + 'interestRate': self.parse_number('0'), + 'estimatedSettlePrice': None, + 'timestamp': estFundingRateTimestamp, + 'datetime': self.iso8601(estFundingRateTimestamp), + 'fundingRate': self.safe_number(fundingRate, 'est_funding_rate'), + 'fundingTimestamp': nextFundingTimestamp, + 'fundingDatetime': self.iso8601(nextFundingTimestamp), + 'nextFundingRate': None, + 'nextFundingTimestamp': None, + 'nextFundingDatetime': None, + 'previousFundingRate': self.safe_number(fundingRate, 'last_funding_rate'), + 'previousFundingTimestamp': lastFundingRateTimestamp, + 'previousFundingDatetime': self.iso8601(lastFundingRateTimestamp), + 'interval': self.parse_funding_interval(millisecondsInterval), + } + + def parse_funding_interval(self, interval): + intervals: dict = { + '3600000': '1h', + '14400000': '4h', + '28800000': '8h', + '57600000': '16h', + '86400000': '24h', + } + return self.safe_string(intervals, interval, interval) + + def fetch_funding_interval(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate interval + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/public/get-predicted-funding-rate-for-one-market + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + return self.fetch_funding_rate(symbol, params) + + def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/public/get-predicted-funding-rate-for-one-market + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = self.v1PublicGetPublicFundingRateSymbol(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "symbol": "PERP_ETH_USDC", + # "est_funding_rate": 123, + # "est_funding_rate_timestamp": 1683880020000, + # "last_funding_rate": 0.0001, + # "last_funding_rate_timestamp": 1683878400000, + # "next_funding_time": 1683907200000, + # "sum_unitary_funding": 521.367 + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_funding_rate(data, market) + + def fetch_funding_rates(self, symbols: Strings = None, params={}) -> FundingRates: + """ + fetch the current funding rate for multiple markets + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/public/get-predicted-funding-rates-for-all-markets + + :param str[] symbols: unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of `funding rate structures ` + """ + self.load_markets() + symbols = self.market_symbols(symbols) + response = self.v1PublicGetPublicFundingRates(params) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "rows": [{ + # "symbol": "PERP_ETH_USDC", + # "est_funding_rate": 123, + # "est_funding_rate_timestamp": 1683880020000, + # "last_funding_rate": 0.0001, + # "last_funding_rate_timestamp": 1683878400000, + # "next_funding_time": 1683907200000, + # "sum_unitary_funding": 521.367 + # }] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + rows = self.safe_list(data, 'rows', []) + return self.parse_funding_rates(rows, symbols) + + def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical funding rate prices + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/public/get-funding-rate-history-for-one-market + + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: timestamp in ms of the earliest funding rate to fetch + :param int [limit]: the maximum amount of `funding rate structures ` to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest funding rate + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `funding rate structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchFundingRateHistory', 'paginate') + if paginate: + return self.fetch_paginated_call_incremental('fetchFundingRateHistory', symbol, since, limit, params, 'page', 25) + request: dict = {} + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + request['symbol'] = market['id'] + if since is not None: + request['start_t'] = since + request, params = self.handle_until_option('end_t', request, params, 0.001) + response = self.v1PublicGetPublicFundingRateHistory(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "rows": [{ + # "symbol": "PERP_ETH_USDC", + # "funding_rate": 0.0001, + # "funding_rate_timestamp": 1684224000000, + # "next_funding_time": 1684252800000 + # }], + # "meta": { + # "total": 9, + # "records_per_page": 25, + # "current_page": 1 + # } + # } + # } + # + data = self.safe_dict(response, 'data', {}) + result = self.safe_list(data, 'rows', []) + rates = [] + for i in range(0, len(result)): + entry = result[i] + marketId = self.safe_string(entry, 'symbol') + timestamp = self.safe_integer(entry, 'funding_rate_timestamp') + rates.append({ + 'info': entry, + 'symbol': self.safe_symbol(marketId), + 'fundingRate': self.safe_number(entry, 'funding_rate'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + sorted = self.sort_by(rates, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, symbol, since, limit) + + def parse_income(self, income, market: Market = None): + # + # { + # "symbol": "PERP_ETH_USDC", + # "funding_rate": 0.00046875, + # "mark_price": 2100, + # "funding_fee": 0.000016, + # "payment_type": "Pay", + # "status": "Accrued", + # "created_time": 1682235722003, + # "updated_time": 1682235722003 + # } + # + marketId = self.safe_string(income, 'symbol') + symbol = self.safe_symbol(marketId, market) + amount = self.safe_string(income, 'funding_fee') + code = self.safe_currency_code('USDC') + timestamp = self.safe_integer(income, 'updated_time') + rate = self.safe_number(income, 'funding_rate') + paymentType = self.safe_string(income, 'payment_type') + amount = Precise.string_neg(amount) if (paymentType == 'Pay') else amount + return { + 'info': income, + 'symbol': symbol, + 'code': code, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': None, + 'amount': self.parse_number(amount), + 'rate': rate, + } + + def fetch_funding_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch the history of funding payments paid and received on self account + + https://orderly.network/docs/build-on-omnichain/evm-api/restful-api/private/get-funding-fee-history + + :param str [symbol]: unified market symbol + :param int [since]: the earliest time in ms to fetch funding history for + :param int [limit]: the maximum number of funding history structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict: a `funding history structure ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchFundingHistory', 'paginate') + if paginate: + return self.fetch_paginated_call_incremental('fetchFundingHistory', symbol, since, limit, params, 'page', 500) + request: dict = {} + market: Market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if since is not None: + request['start_t'] = since + until = self.safe_integer(params, 'until') # unified in milliseconds + params = self.omit(params, ['until']) + if until is not None: + request['end_t'] = until + if limit is not None: + request['size'] = min(limit, 500) + response = self.v1PrivateGetFundingFeeHistory(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "meta": { + # "total": 9, + # "records_per_page": 25, + # "current_page": 1 + # }, + # "rows": [{ + # "symbol": "PERP_ETH_USDC", + # "funding_rate": 0.00046875, + # "mark_price": 2100, + # "funding_fee": 0.000016, + # "payment_type": "Pay", + # "status": "Accrued", + # "created_time": 1682235722003, + # "updated_time": 1682235722003 + # }] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + rows = self.safe_list(data, 'rows', []) + return self.parse_incomes(rows, market, since, limit) + + def fetch_trading_fees(self, params={}) -> TradingFees: + """ + fetch the trading fees for multiple markets + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-account-information + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + self.load_markets() + response = self.v1PrivateGetClientInfo(params) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "account_id": "", + # "email": "test@test.com", + # "account_mode": "FUTURES", + # "max_leverage": 20, + # "taker_fee_rate": 123, + # "maker_fee_rate": 123, + # "futures_taker_fee_rate": 123, + # "futures_maker_fee_rate": 123, + # "maintenance_cancel_orders": True, + # "imr_factor": { + # "PERP_BTC_USDC": 123, + # "PERP_ETH_USDC": 123, + # "PERP_NEAR_USDC": 123 + # }, + # "max_notional": { + # "PERP_BTC_USDC": 123, + # "PERP_ETH_USDC": 123, + # "PERP_NEAR_USDC": 123 + # } + # } + # } + # + data = self.safe_dict(response, 'data', {}) + maker = self.safe_string(data, 'futures_maker_fee_rate') + taker = self.safe_string(data, 'futures_taker_fee_rate') + result: dict = {} + for i in range(0, len(self.symbols)): + symbol = self.symbols[i] + result[symbol] = { + 'info': response, + 'symbol': symbol, + 'maker': self.parse_number(Precise.string_div(maker, '10000')), + 'taker': self.parse_number(Precise.string_div(taker, '10000')), + 'percentage': True, + 'tierBased': True, + } + return result + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/orderbook-snapshot + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + if limit is not None: + limit = min(limit, 1000) + request['max_level'] = limit + response = self.v1PrivateGetOrderbookSymbol(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "asks": [{ + # "price": 10669.4, + # "quantity": 1.56263218 + # }], + # "bids": [{ + # "price": 10669.4, + # "quantity": 1.56263218 + # }], + # "timestamp": 123 + # } + # } + # + data = self.safe_dict(response, 'data', {}) + timestamp = self.safe_integer(data, 'timestamp') + return self.parse_order_book(data, symbol, timestamp, 'bids', 'asks', 'price', 'quantity') + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + return [ + self.safe_integer(ohlcv, 'start_timestamp'), + self.safe_number(ohlcv, 'open'), + self.safe_number(ohlcv, 'high'), + self.safe_number(ohlcv, 'low'), + self.safe_number(ohlcv, 'close'), + self.safe_number(ohlcv, 'volume'), + ] + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-kline + + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: max=1000, max=100 when since is defined and is less than(now - (999 * (timeframe in ms))) + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + 'type': self.safe_string(self.timeframes, timeframe, timeframe), + } + if limit is not None: + request['limit'] = min(limit, 1000) + response = self.v1PrivateGetKline(self.extend(request, params)) + data = self.safe_dict(response, 'data', {}) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "rows": [{ + # "open": 66166.23, + # "close": 66124.56, + # "low": 66038.06, + # "high": 66176.97, + # "volume": 23.45528526, + # "amount": 1550436.21725288, + # "symbol": "PERP_BTC_USDC", + # "type": "1m", + # "start_timestamp": 1636388220000, + # "end_timestamp": 1636388280000 + # }] + # } + # } + # + rows = self.safe_list(data, 'rows', []) + return self.parse_ohlcvs(rows, market, timeframe, since, limit) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # Possible input functions: + # * createOrder + # * createOrders + # * cancelOrder + # * fetchOrder + # * fetchOrders + # isFromFetchOrder = ('order_tag' in order); TO_DO + # + # stop order after creating it: + # { + # "orderId": "1578938", + # "clientOrderId": "0", + # "algoType": "STOP_LOSS", + # "quantity": "0.1" + # } + # stop order after fetching it: + # { + # "algoOrderId": "1578958", + # "clientOrderId": "0", + # "rootAlgoOrderId": "1578958", + # "parentAlgoOrderId": "0", + # "symbol": "SPOT_LTC_USDT", + # "orderTag": "default", + # "algoType": "STOP_LOSS", + # "side": "BUY", + # "quantity": "0.1", + # "isTriggered": False, + # "triggerPrice": "100", + # "triggerStatus": "USELESS", + # "type": "LIMIT", + # "rootAlgoStatus": "CANCELLED", + # "algoStatus": "CANCELLED", + # "triggerPriceType": "MARKET_PRICE", + # "price": "75", + # "triggerTime": "0", + # "totalExecutedQuantity": "0", + # "averageExecutedPrice": "0", + # "totalFee": "0", + # "feeAsset": '', + # "reduceOnly": False, + # "createdTime": "1686149609.744", + # "updatedTime": "1686149903.362" + # } + # + timestamp = self.safe_integer_n(order, ['timestamp', 'created_time', 'createdTime']) + orderId = self.safe_string_n(order, ['order_id', 'orderId', 'algoOrderId']) + clientOrderId = self.omit_zero(self.safe_string_2(order, 'client_order_id', 'clientOrderId')) # Somehow, self always returns 0 for limit order + marketId = self.safe_string(order, 'symbol') + market = self.safe_market(marketId, market) + symbol = market['symbol'] + price = self.safe_string_2(order, 'order_price', 'price') + amount = self.safe_string_2(order, 'order_quantity', 'quantity') # This is base amount + cost = self.safe_string_2(order, 'order_amount', 'amount') # This is quote amount + orderType = self.safe_string_lower_2(order, 'order_type', 'type') + status = self.safe_value_2(order, 'status', 'algoStatus') + success = self.safe_bool(order, 'success') + if success is not None: + status = 'NEW' if (success) else 'REJECTED' + side = self.safe_string_lower(order, 'side') + filled = self.omit_zero(self.safe_value_2(order, 'executed', 'totalExecutedQuantity')) + average = self.omit_zero(self.safe_string_2(order, 'average_executed_price', 'averageExecutedPrice')) + remaining = Precise.string_sub(cost, filled) + fee = self.safe_value_2(order, 'total_fee', 'totalFee') + feeCurrency = self.safe_string_2(order, 'fee_asset', 'feeAsset') + transactions = self.safe_value(order, 'Transactions') + triggerPrice = self.safe_number(order, 'triggerPrice') + takeProfitPrice: Num = None + stopLossPrice: Num = None + childOrders = self.safe_value(order, 'childOrders') + if childOrders is not None: + first = self.safe_value(childOrders, 0) + innerChildOrders = self.safe_value(first, 'childOrders', []) + innerChildOrdersLength = len(innerChildOrders) + if innerChildOrdersLength > 0: + takeProfitOrder = self.safe_value(innerChildOrders, 0) + stopLossOrder = self.safe_value(innerChildOrders, 1) + takeProfitPrice = self.safe_number(takeProfitOrder, 'triggerPrice') + stopLossPrice = self.safe_number(stopLossOrder, 'triggerPrice') + lastUpdateTimestamp = self.safe_integer_2(order, 'updatedTime', 'updated_time') + return self.safe_order({ + 'id': orderId, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'lastUpdateTimestamp': lastUpdateTimestamp, + 'status': self.parse_order_status(status), + 'symbol': symbol, + 'type': self.parse_order_type(orderType), + 'timeInForce': self.parse_time_in_force(orderType), + 'postOnly': None, # TO_DO + 'reduceOnly': self.safe_bool(order, 'reduce_only'), + 'side': side, + 'price': price, + 'triggerPrice': triggerPrice, + 'takeProfitPrice': takeProfitPrice, + 'stopLossPrice': stopLossPrice, + 'average': average, + 'amount': amount, + 'filled': filled, + 'remaining': remaining, # TO_DO + 'cost': cost, + 'trades': transactions, + 'fee': { + 'cost': fee, + 'currency': feeCurrency, + }, + 'info': order, + }, market) + + def parse_time_in_force(self, timeInForce: Str): + timeInForces: dict = { + 'ioc': 'IOC', + 'fok': 'FOK', + 'post_only': 'PO', + } + return self.safe_string(timeInForces, timeInForce, None) + + def parse_order_status(self, status: Str): + if status is not None: + statuses: dict = { + 'NEW': 'open', + 'FILLED': 'closed', + 'CANCEL_SENT': 'canceled', + 'CANCEL_ALL_SENT': 'canceled', + 'CANCELLED': 'canceled', + 'PARTIAL_FILLED': 'open', + 'REJECTED': 'rejected', + 'INCOMPLETE': 'open', + 'COMPLETED': 'closed', + } + return self.safe_string(statuses, status, status) + return status + + def parse_order_type(self, type: Str): + types: dict = { + 'LIMIT': 'limit', + 'MARKET': 'market', + 'POST_ONLY': 'limit', + } + return self.safe_string_lower(types, type, type) + + def create_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + @ignore + helper function to build the request + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency + :param float [price]: the price that the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: request to be sent to the exchange + """ + reduceOnly = self.safe_bool_2(params, 'reduceOnly', 'reduce_only') + orderType = type.upper() + market = self.market(symbol) + orderSide = side.upper() + request: dict = { + 'symbol': market['id'], + 'side': orderSide, + } + triggerPrice = self.safe_string_2(params, 'triggerPrice', 'stopPrice') + stopLoss = self.safe_value(params, 'stopLoss') + takeProfit = self.safe_value(params, 'takeProfit') + algoType = self.safe_string(params, 'algoType') + isConditional = triggerPrice is not None or stopLoss is not None or takeProfit is not None or (self.safe_value(params, 'childOrders') is not None) + isMarket = orderType == 'MARKET' + timeInForce = self.safe_string_lower(params, 'timeInForce') + postOnly = self.is_post_only(isMarket, None, params) + orderQtyKey = 'quantity' if isConditional else 'order_quantity' + priceKey = 'price' if isConditional else 'order_price' + typeKey = 'type' if isConditional else 'order_type' + request[typeKey] = orderType # LIMIT/MARKET/IOC/FOK/POST_ONLY/ASK/BID + if not isConditional: + if postOnly: + request['order_type'] = 'POST_ONLY' + elif timeInForce == 'fok': + request['order_type'] = 'FOK' + elif timeInForce == 'ioc': + request['order_type'] = 'IOC' + if reduceOnly: + request['reduce_only'] = reduceOnly + if price is not None: + request[priceKey] = self.price_to_precision(symbol, price) + if isMarket and not isConditional: + request[orderQtyKey] = self.amount_to_precision(symbol, amount) + elif algoType != 'POSITIONAL_TP_SL': + request[orderQtyKey] = self.amount_to_precision(symbol, amount) + clientOrderId = self.safe_string_n(params, ['clOrdID', 'clientOrderId', 'client_order_id']) + if clientOrderId is not None: + request['client_order_id'] = clientOrderId + if triggerPrice is not None: + request['trigger_price'] = self.price_to_precision(symbol, triggerPrice) + request['algo_type'] = 'STOP' + elif (stopLoss is not None) or (takeProfit is not None): + request['algo_type'] = 'TP_SL' + outterOrder: dict = { + 'symbol': market['id'], + 'reduce_only': False, + 'algo_type': 'POSITIONAL_TP_SL', + 'child_orders': [], + } + childOrders = outterOrder['child_orders'] + closeSide = 'SELL' if (orderSide == 'BUY') else 'BUY' + if stopLoss is not None: + stopLossPrice = self.safe_number_2(stopLoss, 'triggerPrice', 'price', stopLoss) + stopLossOrder: dict = { + 'side': closeSide, + 'algo_type': 'TP_SL', + 'trigger_price': self.price_to_precision(symbol, stopLossPrice), + 'type': 'LIMIT', + 'reduce_only': True, + } + childOrders.append(stopLossOrder) + if takeProfit is not None: + takeProfitPrice = self.safe_number_2(takeProfit, 'triggerPrice', 'price', takeProfit) + takeProfitOrder: dict = { + 'side': closeSide, + 'algo_type': 'TP_SL', + 'trigger_price': self.price_to_precision(symbol, takeProfitPrice), + 'type': 'LIMIT', + 'reduce_only': True, + } + outterOrder.append(takeProfitOrder) + request['child_orders'] = [outterOrder] + params = self.omit(params, ['reduceOnly', 'reduce_only', 'clOrdID', 'clientOrderId', 'client_order_id', 'postOnly', 'timeInForce', 'stopPrice', 'triggerPrice', 'stopLoss', 'takeProfit']) + return self.extend(request, params) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/create-order + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/create-algo-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: The price a trigger order is triggered at + :param dict [params.takeProfit]: *takeProfit object in params* containing the triggerPrice at which the attached take profit order will be triggered(perpetual swap markets only) + :param float [params.takeProfit.triggerPrice]: take profit trigger price + :param dict [params.stopLoss]: *stopLoss object in params* containing the triggerPrice at which the attached stop loss order will be triggered(perpetual swap markets only) + :param float [params.stopLoss.triggerPrice]: stop loss trigger price + :param float [params.algoType]: 'STOP'or 'TP_SL' or 'POSITIONAL_TP_SL' + :param float [params.cost]: *spot market buy only* the quote quantity that can be used alternative for the amount + :param str [params.clientOrderId]: a unique id for the order + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + request = self.create_order_request(symbol, type, side, amount, price, params) + triggerPrice = self.safe_string_2(params, 'triggerPrice', 'stopPrice') + stopLoss = self.safe_value(params, 'stopLoss') + takeProfit = self.safe_value(params, 'takeProfit') + isConditional = triggerPrice is not None or stopLoss is not None or takeProfit is not None or (self.safe_value(params, 'childOrders') is not None) + response = None + if isConditional: + response = self.v1PrivatePostAlgoOrder(request) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "order_id": 13, + # "client_order_id": "testclientid", + # "algo_type": "STOP", + # "quantity": 100.12 + # } + # } + # + else: + response = self.v1PrivatePostOrder(request) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "order_id": 13, + # "client_order_id": "testclientid", + # "order_type": "LIMIT", + # "order_price": 100.12, + # "order_quantity": 0.987654, + # "order_amount": 0.8, + # "error_message": "none" + # } + # } + # + data = self.safe_dict(response, 'data') + data['timestamp'] = self.safe_integer(response, 'timestamp') + order = self.parse_order(data, market) + order['type'] = type + return order + + def create_orders(self, orders: List[OrderRequest], params={}): + """ + *contract only* create a list of trade orders + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/batch-create-order + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + ordersRequests = [] + for i in range(0, len(orders)): + rawOrder = orders[i] + marketId = self.safe_string(rawOrder, 'symbol') + type = self.safe_string(rawOrder, 'type') + side = self.safe_string(rawOrder, 'side') + amount = self.safe_value(rawOrder, 'amount') + price = self.safe_value(rawOrder, 'price') + orderParams = self.safe_dict(rawOrder, 'params', {}) + triggerPrice = self.safe_string_2(orderParams, 'triggerPrice', 'stopPrice') + stopLoss = self.safe_value(orderParams, 'stopLoss') + takeProfit = self.safe_value(orderParams, 'takeProfit') + isConditional = triggerPrice is not None or stopLoss is not None or takeProfit is not None or (self.safe_value(orderParams, 'childOrders') is not None) + if isConditional: + raise NotSupported(self.id + ' createOrders() only support non-stop order') + orderRequest = self.create_order_request(marketId, type, side, amount, price, orderParams) + ordersRequests.append(orderRequest) + request: dict = { + 'orders': ordersRequests, + } + response = self.v1PrivatePostBatchOrder(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "rows": [{ + # "order_id": 13, + # "client_order_id": "testclientid", + # "order_type": "LIMIT", + # "order_price": 100.12, + # "order_quantity": 0.987654, + # "order_amount": 0.8, + # "error_message": "none" + # }] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + rows = self.safe_list(data, 'rows', []) + return self.parse_orders(rows) + + def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + """ + edit a trade order + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/edit-order + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/edit-algo-order + + :param str id: order id + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.triggerPrice]: The price a trigger order is triggered at + :param float [params.stopLossPrice]: price to trigger stop-loss orders + :param float [params.takeProfitPrice]: price to trigger take-profit orders + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'order_id': id, + } + triggerPrice = self.safe_string_n(params, ['triggerPrice', 'stopPrice', 'takeProfitPrice', 'stopLossPrice']) + if triggerPrice is not None: + request['triggerPrice'] = self.price_to_precision(symbol, triggerPrice) + isConditional = (triggerPrice is not None) or (self.safe_value(params, 'childOrders') is not None) + orderQtyKey = 'quantity' if isConditional else 'order_quantity' + priceKey = 'price' if isConditional else 'order_price' + if price is not None: + request[priceKey] = self.price_to_precision(symbol, price) + if amount is not None: + request[orderQtyKey] = self.amount_to_precision(symbol, amount) + params = self.omit(params, ['stopPrice', 'triggerPrice', 'takeProfitPrice', 'stopLossPrice', 'trailingTriggerPrice', 'trailingAmount', 'trailingPercent']) + response = None + if isConditional: + response = self.v1PrivatePutAlgoOrder(self.extend(request, params)) + else: + request['symbol'] = market['id'] + request['side'] = side.upper() + orderType = type.upper() + timeInForce = self.safe_string_lower(params, 'timeInForce') + isMarket = orderType == 'MARKET' + postOnly = self.is_post_only(isMarket, None, params) + if postOnly: + request['order_type'] = 'POST_ONLY' + elif timeInForce == 'fok': + request['order_type'] = 'FOK' + elif timeInForce == 'ioc': + request['order_type'] = 'IOC' + else: + request['order_type'] = orderType + clientOrderId = self.safe_string_n(params, ['clOrdID', 'clientOrderId', 'client_order_id']) + params = self.omit(params, ['clOrdID', 'clientOrderId', 'client_order_id', 'postOnly', 'timeInForce']) + if clientOrderId is not None: + request['client_order_id'] = clientOrderId + # request['side'] = side.upper() + # request['symbol'] = market['id'] + response = self.v1PrivatePutOrder(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "status": "EDIT_SENT" + # } + # } + # + data = self.safe_dict(response, 'data', {}) + data['timestamp'] = self.safe_integer(response, 'timestamp') + return self.parse_order(data, market) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/cancel-order + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/cancel-order-by-client_order_id + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/cancel-algo-order + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/cancel-algo-order-by-client_order_id + + cancels an open order + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: whether the order is a stop/algo order + :param str [params.clientOrderId]: a unique id for the order + :returns dict: An `order structure ` + """ + trigger = self.safe_bool_2(params, 'stop', 'trigger', False) + params = self.omit(params, ['stop', 'trigger']) + if not trigger and (symbol is None): + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + self.load_markets() + market: Market = None + if symbol is not None: + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + clientOrderIdUnified = self.safe_string_2(params, 'clOrdID', 'clientOrderId') + clientOrderIdExchangeSpecific = self.safe_string(params, 'client_order_id', clientOrderIdUnified) + isByClientOrder = clientOrderIdExchangeSpecific is not None + response = None + if trigger: + if isByClientOrder: + request['client_order_id'] = clientOrderIdExchangeSpecific + params = self.omit(params, ['clOrdID', 'clientOrderId', 'client_order_id']) + response = self.v1PrivateDeleteAlgoClientOrder(self.extend(request, params)) + else: + request['order_id'] = id + response = self.v1PrivateDeleteAlgoOrder(self.extend(request, params)) + else: + if isByClientOrder: + request['client_order_id'] = clientOrderIdExchangeSpecific + params = self.omit(params, ['clOrdID', 'clientOrderId', 'client_order_id']) + response = self.v1PrivateDeleteClientOrder(self.extend(request, params)) + else: + request['order_id'] = id + response = self.v1PrivateDeleteOrder(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "status": "CANCEL_SENT" + # } + # } + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "status": "CANCEL_SENT" + # } + # + extendParams: dict = {'symbol': symbol} + if isByClientOrder: + extendParams['client_order_id'] = clientOrderIdExchangeSpecific + else: + extendParams['id'] = id + if trigger: + return self.extend(self.parse_order(response), extendParams) + data = self.safe_dict(response, 'data', {}) + return self.extend(self.parse_order(data), extendParams) + + def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): + """ + cancel multiple orders + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/batch-cancel-orders + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/batch-cancel-orders-by-client_order_id + + :param str[] ids: order ids + :param str [symbol]: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str[] [params.client_order_ids]: max length 10 e.g. ["my_id_1","my_id_2"], encode the double quotes. No space after comma + :returns dict: an list of `order structures ` + """ + self.load_markets() + clientOrderIds = self.safe_list_n(params, ['clOrdIDs', 'clientOrderIds', 'client_order_ids']) + params = self.omit(params, ['clOrdIDs', 'clientOrderIds', 'client_order_ids']) + request: dict = {} + response = None + if clientOrderIds: + request['client_order_ids'] = ','.join(clientOrderIds) + response = self.v1PrivateDeleteClientBatchOrder(self.extend(request, params)) + else: + request['order_ids'] = ','.join(ids) + response = self.v1PrivateDeleteBatchOrder(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "status": "CANCEL_ALL_SENT" + # } + # } + # + return [self.safe_order({ + 'info': response, + })] + + def cancel_all_orders(self, symbol: Str = None, params={}): + """ + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/cancel-all-pending-algo-orders + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/cancel-orders-in-bulk + + cancel all open orders in a market + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: whether the order is a stop/algo order + :returns dict: an list of `order structures ` + """ + self.load_markets() + trigger = self.safe_bool_2(params, 'stop', 'trigger') + params = self.omit(params, ['stop', 'trigger']) + request: dict = {} + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + response = None + if trigger: + response = self.v1PrivateDeleteAlgoOrders(self.extend(request, params)) + else: + response = self.v1PrivateDeleteOrders(self.extend(request, params)) + # trigger + # { + # "success": True, + # "timestamp": 1702989203989, + # "status": "CANCEL_ALL_SENT" + # } + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "status": "CANCEL_ALL_SENT" + # } + # } + # + return [ + self.safe_order({ + 'info': response, + }), + ] + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-order-by-order_id + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-order-by-client_order_id + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-algo-order-by-order_id + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-algo-order-by-client_order_id + + fetches information on an order made by the user + :param str id: the order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: whether the order is a stop/algo order + :param str [params.clientOrderId]: a unique id for the order + :returns dict: An `order structure ` + """ + self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + trigger = self.safe_bool_2(params, 'stop', 'trigger', False) + request: dict = {} + clientOrderId = self.safe_string_n(params, ['clOrdID', 'clientOrderId', 'client_order_id']) + params = self.omit(params, ['stop', 'trigger', 'clOrdID', 'clientOrderId', 'client_order_id']) + response = None + if trigger: + if clientOrderId: + request['client_order_id'] = clientOrderId + response = self.v1PrivateGetAlgoClientOrderClientOrderId(self.extend(request, params)) + else: + request['oid'] = id + response = self.v1PrivateGetAlgoOrderOid(self.extend(request, params)) + else: + if clientOrderId: + request['client_order_id'] = clientOrderId + response = self.v1PrivateGetClientOrderClientOrderId(self.extend(request, params)) + else: + request['oid'] = id + response = self.v1PrivateGetOrderOid(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "order_id": 78151, + # "user_id": 12345, + # "price": 0.67772, + # "type": "LIMIT", + # "quantity": 20, + # "amount": 10, + # "executed_quantity": 20, + # "total_executed_quantity": 20, + # "visible_quantity": 1, + # "symbol": "PERP_WOO_USDC", + # "side": "BUY", + # "status": "FILLED", + # "total_fee": 0.5, + # "fee_asset": "WOO", + # "client_order_id": 1, + # "average_executed_price": 0.67772, + # "created_time": 1653563963000, + # "updated_time": 1653564213000, + # "realized_pnl": 123 + # } + # } + # + orders = self.safe_dict(response, 'data', response) + return self.parse_order(orders, market) + + def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-orders + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-algo-orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: whether the order is a stop/algo order + :param boolean [params.is_triggered]: whether the order has been triggered(False by default) + :param str [params.side]: 'buy' or 'sell' + :param boolean [params.paginate]: set to True if you want to fetch orders with pagination + :param int params['until']: timestamp in ms of the latest order to fetch + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + paginate = False + isTrigger = self.safe_bool_2(params, 'stop', 'trigger', False) + maxLimit = 100 if (isTrigger) else 500 + paginate, params = self.handle_option_and_params(params, 'fetchOrders', 'paginate') + if paginate: + return self.fetch_paginated_call_incremental('fetchOrders', symbol, since, limit, params, 'page', maxLimit) + request: dict = {} + market: Market = None + params = self.omit(params, ['stop', 'trigger']) + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if since is not None: + request['start_t'] = since + if limit is not None: + request['size'] = limit + else: + request['size'] = maxLimit + if isTrigger: + request['algo_type'] = 'STOP' + request, params = self.handle_until_option('end_t', request, params) + response = None + if isTrigger: + response = self.v1PrivateGetAlgoOrders(self.extend(request, params)) + else: + response = self.v1PrivateGetOrders(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "meta": { + # "total": 9, + # "records_per_page": 25, + # "current_page": 1 + # }, + # "rows": [{ + # "order_id": 78151, + # "user_id": 12345, + # "price": 0.67772, + # "type": "LIMIT", + # "quantity": 20, + # "amount": 10, + # "executed_quantity": 20, + # "total_executed_quantity": 20, + # "visible_quantity": 1, + # "symbol": "PERP_WOO_USDC", + # "side": "BUY", + # "status": "FILLED", + # "total_fee": 0.5, + # "fee_asset": "WOO", + # "client_order_id": 1, + # "average_executed_price": 0.67772, + # "created_time": 1653563963000, + # "updated_time": 1653564213000, + # "realized_pnl": 123 + # }] + # } + # } + # + data = self.safe_value(response, 'data', response) + orders = self.safe_list(data, 'rows') + return self.parse_orders(orders, market, since, limit) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-orders + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-algo-orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: whether the order is a stop/algo order + :param boolean [params.is_triggered]: whether the order has been triggered(False by default) + :param str [params.side]: 'buy' or 'sell' + :param int params['until']: timestamp in ms of the latest order to fetch + :param boolean [params.paginate]: set to True if you want to fetch orders with pagination + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + extendedParams = self.extend(params, {'status': 'INCOMPLETE'}) + return self.fetch_orders(symbol, since, limit, extendedParams) + + def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple orders made by the user + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-orders + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-algo-orders + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: whether the order is a stop/algo order + :param boolean [params.is_triggered]: whether the order has been triggered(False by default) + :param str [params.side]: 'buy' or 'sell' + :param int params['until']: timestamp in ms of the latest order to fetch + :param boolean [params.paginate]: set to True if you want to fetch orders with pagination + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + extendedParams = self.extend(params, {'status': 'COMPLETED'}) + return self.fetch_orders(symbol, since, limit, extendedParams) + + def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all the trades made from a single order + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-all-trades-of-specific-order + + :param str id: order id + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `trade structures ` + """ + self.load_markets() + market: Market = None + if symbol is not None: + market = self.market(symbol) + request: dict = { + 'oid': id, + } + response = self.v1PrivateGetOrderOidTrades(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "rows": [{ + # "id": 2, + # "symbol": "PERP_BTC_USDC", + # "fee": 0.0001, + # "fee_asset": "USDC", + # "side": "BUY", + # "order_id": 1, + # "executed_price": 123, + # "executed_quantity": 0.05, + # "executed_timestamp": 1567382401000, + # "is_maker": 1, + # "realized_pnl": 123 + # }] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + trades = self.safe_list(data, 'rows', []) + return self.parse_trades(trades, market, since, limit, params) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-trades + + fetch all trades made by the user + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: set to True if you want to fetch trades with pagination + :param int params['until']: timestamp in ms of the latest trade to fetch + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchMyTrades', 'paginate') + if paginate: + return self.fetch_paginated_call_incremental('fetchMyTrades', symbol, since, limit, params, 'page', 500) + request: dict = {} + market: Market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if since is not None: + request['start_t'] = since + if limit is not None: + request['size'] = limit + else: + request['size'] = 500 + request, params = self.handle_until_option('end_t', request, params) + response = self.v1PrivateGetTrades(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "meta": { + # "total": 9, + # "records_per_page": 25, + # "current_page": 1 + # }, + # "rows": [{ + # "id": 2, + # "symbol": "PERP_BTC_USDC", + # "fee": 0.0001, + # "fee_asset": "USDC", + # "side": "BUY", + # "order_id": 1, + # "executed_price": 123, + # "executed_quantity": 0.05, + # "executed_timestamp": 1567382401000, + # "is_maker": 1, + # "realized_pnl": 123 + # }] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + trades = self.safe_list(data, 'rows', []) + return self.parse_trades(trades, market, since, limit, params) + + def parse_balance(self, response) -> Balances: + result: dict = { + 'info': response, + } + balances = self.safe_list(response, 'holding', []) + for i in range(0, len(balances)): + balance = balances[i] + code = self.safe_currency_code(self.safe_string(balance, 'token')) + account = self.account() + account['total'] = self.safe_string(balance, 'holding') + account['frozen'] = self.safe_string(balance, 'frozen') + result[code] = account + return self.safe_balance(result) + + def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-current-holding + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + response = self.v1PrivateGetClientHolding(params) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "holding": [{ + # "updated_time": 1580794149000, + # "token": "BTC", + # "holding": -28.000752, + # "frozen": 123, + # "pending_short": -2000 + # }] + # } + # } + # + data = self.safe_dict(response, 'data') + return self.parse_balance(data) + + def get_asset_history_rows(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> Any: + self.load_markets() + request: dict = {} + currency: Currency = None + if code is not None: + currency = self.currency(code) + request['balance_token'] = currency['id'] + if since is not None: + request['start_t'] = since + if limit is not None: + request['pageSize'] = limit + transactionType = self.safe_string(params, 'type') + params = self.omit(params, 'type') + if transactionType is not None: + request['type'] = transactionType + response = self.v1PrivateGetAssetHistory(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "meta": { + # "total": 9, + # "records_per_page": 25, + # "current_page": 1 + # }, + # "rows": [{ + # "id": "230707030600002", + # "tx_id": "0x4b0714c63cc7abae72bf68e84e25860b88ca651b7d27dad1e32bf4c027fa5326", + # "side": "WITHDRAW", + # "token": "USDC", + # "amount": 555, + # "fee": 123, + # "trans_status": "FAILED", + # "created_time": 1688699193034, + # "updated_time": 1688699193096, + # "chain_id": "986532" + # }] + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return [currency, self.safe_list(data, 'rows', [])] + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + currencyId = self.safe_string(item, 'token') + code = self.safe_currency_code(currencyId, currency) + currency = self.safe_currency(currencyId, currency) + amount = self.safe_number(item, 'amount') + side = self.safe_string(item, 'token_side') + direction = 'in' if (side == 'DEPOSIT') else 'out' + timestamp = self.safe_integer(item, 'created_time') + fee = self.parse_token_and_fee_temp(item, 'fee_token', 'fee_amount') + return self.safe_ledger_entry({ + 'id': self.safe_string(item, 'id'), + 'currency': code, + 'account': self.safe_string(item, 'account'), + 'referenceAccount': None, + 'referenceId': self.safe_string(item, 'tx_id'), + 'status': self.parse_transaction_status(self.safe_string(item, 'status')), + 'amount': amount, + 'before': None, + 'after': None, + 'fee': fee, + 'direction': direction, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'type': self.parse_ledger_entry_type(self.safe_string(item, 'type')), + 'info': item, + }, currency) + + def parse_ledger_entry_type(self, type): + types: dict = { + 'BALANCE': 'transaction', # Funds moved in/out wallet + 'COLLATERAL': 'transfer', # Funds moved between portfolios + } + return self.safe_string(types, type, type) + + def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-asset-history + + :param str [code]: unified currency code, default is None + :param int [since]: timestamp in ms of the earliest ledger entry, default is None + :param int [limit]: max number of ledger entries to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ledger structure ` + """ + currencyRows = self.get_asset_history_rows(code, since, limit, params) + currency = self.safe_value(currencyRows, 0) + rows = self.safe_list(currencyRows, 1) + return self.parse_ledger(rows, currency, since, limit, params) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # example in fetchLedger + code = self.safe_string(transaction, 'token') + movementDirection = self.safe_string_lower(transaction, 'token_side') + if movementDirection == 'withdraw': + movementDirection = 'withdrawal' + fee = self.parse_token_and_fee_temp(transaction, 'fee_token', 'fee_amount') + addressTo = self.safe_string(transaction, 'target_address') + addressFrom = self.safe_string(transaction, 'source_address') + timestamp = self.safe_integer(transaction, 'created_time') + return { + 'info': transaction, + 'id': self.safe_string_2(transaction, 'id', 'withdraw_id'), + 'txid': self.safe_string(transaction, 'tx_id'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'address': None, + 'addressFrom': addressFrom, + 'addressTo': addressTo, + 'tag': self.safe_string(transaction, 'extra'), + 'tagFrom': None, + 'tagTo': None, + 'type': movementDirection, + 'amount': self.safe_number(transaction, 'amount'), + 'currency': code, + 'status': self.parse_transaction_status(self.safe_string(transaction, 'status')), + 'updated': self.safe_integer(transaction, 'updated_time'), + 'comment': None, + 'internal': None, + 'fee': fee, + 'network': None, + } + + def parse_transaction_status(self, status: Str): + statuses: dict = { + 'NEW': 'pending', + 'CONFIRMING': 'pending', + 'PROCESSING': 'pending', + 'COMPLETED': 'ok', + 'CANCELED': 'canceled', + } + return self.safe_string(statuses, status, status) + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-asset-history + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + request: dict = { + 'side': 'DEPOSIT', + } + return self.fetch_deposits_withdrawals(code, since, limit, self.extend(request, params)) + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-asset-history + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transaction structures ` + """ + request: dict = { + 'side': 'WITHDRAW', + } + return self.fetch_deposits_withdrawals(code, since, limit, self.extend(request, params)) + + def fetch_deposits_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch history of deposits and withdrawals + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-asset-history + + :param str [code]: unified currency code for the currency of the deposit/withdrawals, default is None + :param int [since]: timestamp in ms of the earliest deposit/withdrawal, default is None + :param int [limit]: max number of deposit/withdrawals to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `transaction structure ` + """ + request: dict = {} + currencyRows = self.get_asset_history_rows(code, since, limit, self.extend(request, params)) + currency = self.safe_value(currencyRows, 0) + rows = self.safe_list(currencyRows, 1) + # + # { + # "rows":[], + # "meta":{ + # "total":0, + # "records_per_page":25, + # "current_page":1 + # }, + # "success":true + # } + # + return self.parse_transactions(rows, currency, since, limit, params) + + def get_withdraw_nonce(self, params={}): + response = self.v1PrivateGetWithdrawNonce(params) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "withdraw_nonce": 1 + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.safe_number(data, 'withdraw_nonce') + + def hash_message(self, message): + return '0x' + self.hash(message, 'keccak', 'hex') + + def sign_hash(self, hash, privateKey): + signature = self.ecdsa(hash[-64:], privateKey[-64:], 'secp256k1', None) + r = signature['r'] + s = signature['s'] + v = self.int_to_base16(self.sum(27, signature['v'])) + return '0x' + r.rjust(64, '0') + s.rjust(64, '0') + v + + def sign_message(self, message, privateKey): + return self.sign_hash(self.hash_message(message), privateKey[-64:]) + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/create-withdraw-request + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + self.load_markets() + self.check_address(address) + if code is not None: + code = code.upper() + if code != 'USDC': + raise NotSupported(self.id + ' withdraw() only support USDC') + currency = self.currency(code) + verifyingContractAddress = self.safe_string(self.options, 'verifyingContractAddress') + chainId = self.safe_string(params, 'chainId') + currencyNetworks = self.safe_dict(currency, 'networks', {}) + coinNetwork = self.safe_dict(currencyNetworks, chainId, {}) + coinNetworkId = self.safe_number(coinNetwork, 'id') + if coinNetworkId is None: + raise BadRequest(self.id + ' withdraw() require chainId parameter') + withdrawNonce = self.get_withdraw_nonce(params) + nonce = self.nonce() + domain: dict = { + 'chainId': chainId, + 'name': 'Orderly', + 'verifyingContract': verifyingContractAddress, + 'version': '1', + } + messageTypes: dict = { + 'Withdraw': [ + {'name': 'brokerId', 'type': 'string'}, + {'name': 'chainId', 'type': 'uint256'}, + {'name': 'receiver', 'type': 'address'}, + {'name': 'token', 'type': 'string'}, + {'name': 'amount', 'type': 'uint256'}, + {'name': 'withdrawNonce', 'type': 'uint64'}, + {'name': 'timestamp', 'type': 'uint64'}, + ], + } + withdrawRequest: dict = { + 'brokerId': self.safe_string(self.options, 'keyBrokerId', 'woofi_pro'), + 'chainId': self.parse_to_int(chainId), + 'receiver': address, + 'token': code, + 'amount': str(amount), + 'withdrawNonce': withdrawNonce, + 'timestamp': nonce, + } + msg = self.eth_encode_structured_data(domain, messageTypes, withdrawRequest) + signature = self.sign_message(msg, self.privateKey) + request: dict = { + 'signature': signature, + 'userAddress': address, + 'verifyingContract': verifyingContractAddress, + 'message': withdrawRequest, + } + params = self.omit(params, 'chainId') + response = self.v1PrivatePostWithdrawRequest(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "withdraw_id": 123 + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_transaction(data, currency) + + def parse_leverage(self, leverage, market=None) -> Leverage: + leverageValue = self.safe_integer(leverage, 'max_leverage') + return { + 'info': leverage, + 'symbol': market['symbol'], + 'marginMode': None, + 'longLeverage': leverageValue, + 'shortLeverage': leverageValue, + } + + def fetch_leverage(self, symbol: str, params={}) -> Leverage: + """ + fetch the set leverage for a market + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-account-information + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `leverage structure ` + """ + self.load_markets() + market = self.market(symbol) + response = self.v1PrivateGetClientInfo(params) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "account_id": "", + # "email": "test@test.com", + # "account_mode": "FUTURES", + # "max_leverage": 20, + # "taker_fee_rate": 123, + # "maker_fee_rate": 123, + # "futures_taker_fee_rate": 123, + # "futures_maker_fee_rate": 123, + # "maintenance_cancel_orders": True, + # "imr_factor": { + # "PERP_BTC_USDC": 123, + # "PERP_ETH_USDC": 123, + # "PERP_NEAR_USDC": 123 + # }, + # "max_notional": { + # "PERP_BTC_USDC": 123, + # "PERP_ETH_USDC": 123, + # "PERP_NEAR_USDC": 123 + # } + # } + # } + # + data = self.safe_dict(response, 'data', {}) + return self.parse_leverage(data, market) + + def set_leverage(self, leverage: int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/update-leverage-setting + + :param int [leverage]: the rate of leverage + :param str [symbol]: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + self.load_markets() + if (leverage < 1) or (leverage > 50): + raise BadRequest(self.id + ' leverage should be between 1 and 50') + request: dict = { + 'leverage': leverage, + } + return self.v1PrivatePostClientLeverage(self.extend(request, params)) + + def parse_position(self, position: dict, market: Market = None): + # + # { + # "IMR_withdraw_orders": 0.1, + # "MMR_with_orders": 0.05, + # "average_open_price": 27908.14386047, + # "cost_position": -139329.358492, + # "est_liq_price": 117335.92899428, + # "fee_24_h": 123, + # "imr": 0.1, + # "last_sum_unitary_funding": 70.38, + # "mark_price": 27794.9, + # "mmr": 0.05, + # "pending_long_qty": 123, + # "pending_short_qty": 123, + # "pnl_24_h": 123, + # "position_qty": -5, + # "settle_price": 27865.8716984, + # "symbol": "PERP_BTC_USDC", + # "timestamp": 1685429350571, + # "unsettled_pnl": 354.858492 + # } + # + contract = self.safe_string(position, 'symbol') + market = self.safe_market(contract, market) + size = self.safe_string(position, 'position_qty') + side: Str = None + if Precise.string_gt(size, '0'): + side = 'long' + else: + side = 'short' + contractSize = self.safe_string(market, 'contractSize') + markPrice = self.safe_string(position, 'mark_price') + timestamp = self.safe_integer(position, 'timestamp') + entryPrice = self.safe_string(position, 'average_open_price') + unrealisedPnl = self.safe_string(position, 'unsettled_pnl') + size = Precise.string_abs(size) + notional = Precise.string_mul(size, markPrice) + return self.safe_position({ + 'info': position, + 'id': None, + 'symbol': self.safe_string(market, 'symbol'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastUpdateTimestamp': None, + 'initialMargin': None, + 'initialMarginPercentage': None, + 'maintenanceMargin': None, + 'maintenanceMarginPercentage': None, + 'entryPrice': self.parse_number(entryPrice), + 'notional': self.parse_number(notional), + 'leverage': None, + 'unrealizedPnl': self.parse_number(unrealisedPnl), + 'contracts': self.parse_number(size), + 'contractSize': self.parse_number(contractSize), + 'marginRatio': None, + 'liquidationPrice': self.safe_number(position, 'est_liq_price'), + 'markPrice': self.parse_number(markPrice), + 'lastPrice': None, + 'collateral': None, + 'marginMode': 'cross', + 'marginType': None, + 'side': side, + 'percentage': None, + 'hedged': None, + 'stopLossPrice': None, + 'takeProfitPrice': None, + }) + + def fetch_position(self, symbol: Str, params={}): + """ + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-one-position-info + + fetch data on an open position + :param str symbol: unified market symbol of the market the position is held in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `position structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = self.v1PrivateGetPositionSymbol(self.extend(request, params)) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "IMR_withdraw_orders": 0.1, + # "MMR_with_orders": 0.05, + # "average_open_price": 27908.14386047, + # "cost_position": -139329.358492, + # "est_liq_price": 117335.92899428, + # "fee_24_h": 123, + # "imr": 0.1, + # "last_sum_unitary_funding": 70.38, + # "mark_price": 27794.9, + # "mmr": 0.05, + # "pending_long_qty": 123, + # "pending_short_qty": 123, + # "pnl_24_h": 123, + # "position_qty": -5, + # "settle_price": 27865.8716984, + # "symbol": "PERP_BTC_USDC", + # "timestamp": 1685429350571, + # "unsettled_pnl": 354.858492 + # } + # } + # + data = self.safe_dict(response, 'data') + return self.parse_position(data, market) + + def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://orderly.network/docs/build-on-evm/evm-api/restful-api/private/get-all-positions-info + + :param str[] [symbols]: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `position structure ` + """ + self.load_markets() + response = self.v1PrivateGetPositions(params) + # + # { + # "success": True, + # "timestamp": 1702989203989, + # "data": { + # "current_margin_ratio_with_orders": 1.2385, + # "free_collateral": 450315.09115, + # "initial_margin_ratio": 0.1, + # "initial_margin_ratio_with_orders": 0.1, + # "maintenance_margin_ratio": 0.05, + # "maintenance_margin_ratio_with_orders": 0.05, + # "margin_ratio": 1.2385, + # "open_margin_ratio": 1.2102, + # "total_collateral_value": 489865.71329, + # "total_pnl_24_h": 123, + # "rows": [{ + # "IMR_withdraw_orders": 0.1, + # "MMR_with_orders": 0.05, + # "average_open_price": 27908.14386047, + # "cost_position": -139329.358492, + # "est_liq_price": 117335.92899428, + # "fee_24_h": 123, + # "imr": 0.1, + # "last_sum_unitary_funding": 70.38, + # "mark_price": 27794.9, + # "mmr": 0.05, + # "pending_long_qty": 123, + # "pending_short_qty": 123, + # "pnl_24_h": 123, + # "position_qty": -5, + # "settle_price": 27865.8716984, + # "symbol": "PERP_BTC_USDC", + # "timestamp": 1685429350571, + # "unsettled_pnl": 354.858492 + # }] + # } + # } + # + result = self.safe_dict(response, 'data', {}) + positions = self.safe_list(result, 'rows', []) + return self.parse_positions(positions, symbols) + + def nonce(self): + return self.milliseconds() + + def sign(self, path, section='public', method='GET', params={}, headers=None, body=None): + version = section[0] + access = section[1] + pathWithParams = self.implode_params(path, params) + url = self.implode_hostname(self.urls['api'][access]) + url += '/' + version + '/' + params = self.omit(params, self.extract_params(path)) + params = self.keysort(params) + if access == 'public': + url += pathWithParams + if params: + url += '?' + self.urlencode(params) + else: + self.check_required_credentials() + if (method == 'POST' or method == 'PUT') and (path == 'algo/order' or path == 'order' or path == 'batch-order'): + isSandboxMode = self.safe_bool(self.options, 'sandboxMode', False) + if not isSandboxMode: + brokerId = self.safe_string(self.options, 'brokerId', 'CCXT') + if path == 'batch-order': + ordersList = self.safe_list(params, 'orders', []) + for i in range(0, len(ordersList)): + params['orders'][i]['order_tag'] = brokerId + else: + params['order_tag'] = brokerId + params = self.keysort(params) + auth = '' + ts = str(self.nonce()) + url += pathWithParams + apiKey = self.apiKey + if apiKey.find('ed25519:') < 0: + apiKey = 'ed25519:' + apiKey + headers = { + 'orderly-account-id': self.accountId, + 'orderly-key': apiKey, + 'orderly-timestamp': ts, + } + auth = ts + method + '/' + version + '/' + pathWithParams + if method == 'POST' or method == 'PUT': + body = self.json(params) + auth += body + headers['content-type'] = 'application/json' + else: + if params: + url += '?' + self.urlencode(params) + auth += '?' + self.rawencode(params) + headers['content-type'] = 'application/x-www-form-urlencoded' + if method == 'DELETE': + body = '' + secret = self.secret + if secret.find('ed25519:') >= 0: + parts = secret.split('ed25519:') + secret = parts[1] + signature = self.eddsa(self.encode(auth), self.base58_to_binary(secret), 'ed25519') + headers['orderly-signature'] = self.urlencode_base64(self.base64_to_binary(signature)) + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if not response: + return None # fallback to default error handler + # + # 400 Bad Request {"success":false,"code":-1012,"message":"Amount is required for buy market orders when margin disabled."} + # {"code":"-1011","message":"The system is under maintenance.","success":false} + # + success = self.safe_bool(response, 'success') + errorCode = self.safe_string(response, 'code') + if not success: + feedback = self.id + ' ' + self.json(response) + self.throw_broadly_matched_exception(self.exceptions['broad'], body, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + raise ExchangeError(feedback) + return None diff --git a/ccxt/xt.py b/ccxt/xt.py new file mode 100644 index 0000000..1cf2ed1 --- /dev/null +++ b/ccxt/xt.py @@ -0,0 +1,4916 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.xt import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Currencies, Currency, DepositAddress, Int, LedgerEntry, LeverageTier, LeverageTiers, MarginModification, Market, Num, Order, OrderSide, OrderType, Position, Str, Tickers, FundingRate, Transaction, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import NotSupported +from ccxt.base.errors import NetworkError +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import OnMaintenance +from ccxt.base.errors import RequestTimeout +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class xt(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(xt, self).describe(), { + 'id': 'xt', + 'name': 'XT', + 'countries': ['SC'], # Seychelles + # spot api ratelimits are None, 10/s/ip, 50/s/ip, 100/s/ip or 200/s/ip + # futures 3 requests per second => 1000ms / (100 * 3.33) = 3.003(get assets -> fetchMarkets & fetchCurrencies) + # futures 10 requests per second => 1000ms / (100 * 1) = 10(all other) + # futures 1000 times per minute for each single IP -> Otherwise account locked for 10min + 'rateLimit': 100, + 'version': 'v4', + 'certified': False, + 'pro': True, + 'has': { + 'CORS': False, + 'spot': True, + 'margin': True, + 'swap': True, + 'future': True, + 'option': False, + 'addMargin': True, + 'borrowMargin': False, + 'cancelAllOrders': True, + 'cancelOrder': True, + 'cancelOrders': True, + 'createDepositAddress': False, + 'createMarketBuyOrderWithCost': True, + 'createMarketSellOrderWithCost': False, + 'createOrder': True, + 'createPostOnlyOrder': False, + 'createReduceOnlyOrder': True, + 'editOrder': True, + 'fetchAccounts': False, + 'fetchBalance': True, + 'fetchBidsAsks': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchCanceledOrders': True, + 'fetchClosedOrders': True, + 'fetchCurrencies': True, + 'fetchDeposit': False, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': True, + 'fetchDepositWithdrawals': False, + 'fetchDepositWithdrawFee': False, + 'fetchDepositWithdrawFees': False, + 'fetchFundingHistory': True, + 'fetchFundingInterval': True, + 'fetchFundingIntervals': False, + 'fetchFundingRate': True, + 'fetchFundingRateHistory': True, + 'fetchFundingRates': False, + 'fetchIndexOHLCV': False, + 'fetchL3OrderBook': False, + 'fetchLedger': True, + 'fetchLedgerEntry': False, + 'fetchLeverage': False, + 'fetchLeverageTiers': True, + 'fetchMarketLeverageTiers': True, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenOrders': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrderBooks': False, + 'fetchOrders': True, + 'fetchOrdersByStatus': True, + 'fetchOrderTrades': False, + 'fetchPosition': True, + 'fetchPositions': True, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchStatus': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': False, + 'fetchTradingLimits': False, + 'fetchTransactionFee': False, + 'fetchTransactionFees': False, + 'fetchTransactions': False, + 'fetchTransfer': False, + 'fetchTransfers': False, + 'fetchWithdrawal': False, + 'fetchWithdrawals': True, + 'fetchWithdrawalWhitelist': False, + 'reduceMargin': True, + 'repayMargin': False, + 'setLeverage': True, + 'setMargin': False, + 'setMarginMode': True, + 'setPositionMode': False, + 'signIn': False, + 'transfer': True, + 'withdraw': True, + }, + 'precisionMode': TICK_SIZE, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/14319357/232636712-466df2fc-560a-4ca4-aab2-b1d954a58e24.jpg', + 'api': { + 'spot': 'https://sapi.xt.com', + 'linear': 'https://fapi.xt.com', + 'inverse': 'https://dapi.xt.com', + 'user': 'https://api.xt.com', + }, + 'www': 'https://xt.com', + 'referral': 'https://www.xt.com/en/accounts/register?ref=9PTM9VW', + 'doc': [ + 'https://doc.xt.com/', + 'https://github.com/xtpub/api-doc', + ], + 'fees': 'https://www.xt.com/en/rate', + }, + 'api': { + 'public': { + 'spot': { + 'get': { + 'currencies': 1, + 'depth': 10, + 'kline': 1, + 'symbol': 1, # 1 for a single symbol + 'ticker': 1, # 1 for a single symbol + 'ticker/book': 1, # 1 for a single symbol + 'ticker/price': 1, # 1 for a single symbol + 'ticker/24h': 1, # 1 for a single symbol + 'time': 1, + 'trade/history': 1, + 'trade/recent': 1, + 'wallet/support/currency': 1, + }, + }, + 'linear': { + 'get': { + 'future/market/v1/public/contract/risk-balance': 1, + 'future/market/v1/public/contract/open-interest': 1, + 'future/market/v1/public/leverage/bracket/detail': 1, + 'future/market/v1/public/leverage/bracket/list': 1, + 'future/market/v1/public/q/agg-ticker': 1, + 'future/market/v1/public/q/agg-tickers': 1, + 'future/market/v1/public/q/deal': 1, + 'future/market/v1/public/q/depth': 1, + 'future/market/v1/public/q/funding-rate': 1, + 'future/market/v1/public/q/funding-rate-record': 1, + 'future/market/v1/public/q/index-price': 1, + 'future/market/v1/public/q/kline': 1, + 'future/market/v1/public/q/mark-price': 1, + 'future/market/v1/public/q/symbol-index-price': 1, + 'future/market/v1/public/q/symbol-mark-price': 1, + 'future/market/v1/public/q/ticker': 1, + 'future/market/v1/public/q/tickers': 1, + 'future/market/v1/public/symbol/coins': 3.33, + 'future/market/v1/public/symbol/detail': 3.33, + 'future/market/v1/public/symbol/list': 1, + }, + }, + 'inverse': { + 'get': { + 'future/market/v1/public/contract/risk-balance': 1, + 'future/market/v1/public/contract/open-interest': 1, + 'future/market/v1/public/leverage/bracket/detail': 1, + 'future/market/v1/public/leverage/bracket/list': 1, + 'future/market/v1/public/q/agg-ticker': 1, + 'future/market/v1/public/q/agg-tickers': 1, + 'future/market/v1/public/q/deal': 1, + 'future/market/v1/public/q/depth': 1, + 'future/market/v1/public/q/funding-rate': 1, + 'future/market/v1/public/q/funding-rate-record': 1, + 'future/market/v1/public/q/index-price': 1, + 'future/market/v1/public/q/kline': 1, + 'future/market/v1/public/q/mark-price': 1, + 'future/market/v1/public/q/symbol-index-price': 1, + 'future/market/v1/public/q/symbol-mark-price': 1, + 'future/market/v1/public/q/ticker': 1, + 'future/market/v1/public/q/tickers': 1, + 'future/market/v1/public/symbol/coins': 3.33, + 'future/market/v1/public/symbol/detail': 3.33, + 'future/market/v1/public/symbol/list': 1, + }, + }, + }, + 'private': { + 'spot': { + 'get': { + 'balance': 1, + 'balances': 1, + 'batch-order': 1, + 'deposit/address': 1, + 'deposit/history': 1, + 'history-order': 1, + 'open-order': 1, + 'order': 1, + 'order/{orderId}': 1, + 'trade': 1, + 'withdraw/history': 1, + }, + 'post': { + 'order': 0.2, + 'withdraw': 10, + 'balance/transfer': 1, + 'balance/account/transfer': 1, + 'ws-token': 1, + }, + 'delete': { + 'batch-order': 1, + 'open-order': 1, + 'order/{orderId}': 1, + }, + 'put': { + 'order/{orderId}': 1, + }, + }, + 'linear': { + 'get': { + 'future/trade/v1/entrust/plan-detail': 1, + 'future/trade/v1/entrust/plan-list': 1, + 'future/trade/v1/entrust/plan-list-history': 1, + 'future/trade/v1/entrust/profit-detail': 1, + 'future/trade/v1/entrust/profit-list': 1, + 'future/trade/v1/order/detail': 1, + 'future/trade/v1/order/list': 1, + 'future/trade/v1/order/list-history': 1, + 'future/trade/v1/order/trade-list': 1, + 'future/user/v1/account/info': 1, + 'future/user/v1/balance/bills': 1, + 'future/user/v1/balance/detail': 1, + 'future/user/v1/balance/funding-rate-list': 1, + 'future/user/v1/balance/list': 1, + 'future/user/v1/position/adl': 1, + 'future/user/v1/position/list': 1, + 'future/user/v1/user/collection/list': 1, + 'future/user/v1/user/listen-key': 1, + }, + 'post': { + 'future/trade/v1/entrust/cancel-all-plan': 1, + 'future/trade/v1/entrust/cancel-all-profit-stop': 1, + 'future/trade/v1/entrust/cancel-plan': 1, + 'future/trade/v1/entrust/cancel-profit-stop': 1, + 'future/trade/v1/entrust/create-plan': 1, + 'future/trade/v1/entrust/create-profit': 1, + 'future/trade/v1/entrust/update-profit-stop': 1, + 'future/trade/v1/order/cancel': 1, + 'future/trade/v1/order/cancel-all': 1, + 'future/trade/v1/order/create': 1, + 'future/trade/v1/order/create-batch': 1, + 'future/trade/v1/order/update': 1, + 'future/user/v1/account/open': 1, + 'future/user/v1/position/adjust-leverage': 1, + 'future/user/v1/position/auto-margin': 1, + 'future/user/v1/position/close-all': 1, + 'future/user/v1/position/margin': 1, + 'future/user/v1/user/collection/add': 1, + 'future/user/v1/user/collection/cancel': 1, + 'future/user/v1/position/change-type': 1, + }, + }, + 'inverse': { + 'get': { + 'future/trade/v1/entrust/plan-detail': 1, + 'future/trade/v1/entrust/plan-list': 1, + 'future/trade/v1/entrust/plan-list-history': 1, + 'future/trade/v1/entrust/profit-detail': 1, + 'future/trade/v1/entrust/profit-list': 1, + 'future/trade/v1/order/detail': 1, + 'future/trade/v1/order/list': 1, + 'future/trade/v1/order/list-history': 1, + 'future/trade/v1/order/trade-list': 1, + 'future/user/v1/account/info': 1, + 'future/user/v1/balance/bills': 1, + 'future/user/v1/balance/detail': 1, + 'future/user/v1/balance/funding-rate-list': 1, + 'future/user/v1/balance/list': 1, + 'future/user/v1/position/adl': 1, + 'future/user/v1/position/list': 1, + 'future/user/v1/user/collection/list': 1, + 'future/user/v1/user/listen-key': 1, + }, + 'post': { + 'future/trade/v1/entrust/cancel-all-plan': 1, + 'future/trade/v1/entrust/cancel-all-profit-stop': 1, + 'future/trade/v1/entrust/cancel-plan': 1, + 'future/trade/v1/entrust/cancel-profit-stop': 1, + 'future/trade/v1/entrust/create-plan': 1, + 'future/trade/v1/entrust/create-profit': 1, + 'future/trade/v1/entrust/update-profit-stop': 1, + 'future/trade/v1/order/cancel': 1, + 'future/trade/v1/order/cancel-all': 1, + 'future/trade/v1/order/create': 1, + 'future/trade/v1/order/create-batch': 1, + 'future/trade/v1/order/update': 1, + 'future/user/v1/account/open': 1, + 'future/user/v1/position/adjust-leverage': 1, + 'future/user/v1/position/auto-margin': 1, + 'future/user/v1/position/close-all': 1, + 'future/user/v1/position/margin': 1, + 'future/user/v1/user/collection/add': 1, + 'future/user/v1/user/collection/cancel': 1, + }, + }, + 'user': { + 'get': { + 'user/account': 1, + 'user/account/api-key': 1, + }, + 'post': { + 'user/account': 1, + 'user/account/api-key': 1, + }, + 'put': { + 'user/account/api-key': 1, + }, + 'delete': { + 'user/account/{apiKeyId}': 1, + }, + }, + }, + }, + 'fees': { + 'spot': { + 'tierBased': True, + 'percentage': True, + 'maker': self.parse_number('0.002'), + 'taker': self.parse_number('0.002'), + 'tiers': { + 'maker': [ + [self.parse_number('0'), self.parse_number('0.002')], + [self.parse_number('5000'), self.parse_number('0.0018')], + [self.parse_number('10000'), self.parse_number('0.0016')], + [self.parse_number('20000'), self.parse_number('0.0014')], + [self.parse_number('50000'), self.parse_number('0.0012')], + [self.parse_number('150000'), self.parse_number('0.0010')], + [self.parse_number('300000'), self.parse_number('0.0008')], + [self.parse_number('600000'), self.parse_number('0.0007')], + [self.parse_number('1200000'), self.parse_number('0.0006')], + [self.parse_number('2500000'), self.parse_number('0.0005')], + [self.parse_number('6000000'), self.parse_number('0.0004')], + [self.parse_number('15000000'), self.parse_number('0.0003')], + [self.parse_number('30000000'), self.parse_number('0.0002')], + ], + 'taker': [ + [self.parse_number('0'), self.parse_number('0.002')], + [self.parse_number('5000'), self.parse_number('0.0018')], + [self.parse_number('10000'), self.parse_number('0.0016')], + [self.parse_number('20000'), self.parse_number('0.0014')], + [self.parse_number('50000'), self.parse_number('0.0012')], + [self.parse_number('150000'), self.parse_number('0.0010')], + [self.parse_number('300000'), self.parse_number('0.0008')], + [self.parse_number('600000'), self.parse_number('0.0007')], + [self.parse_number('1200000'), self.parse_number('0.0006')], + [self.parse_number('2500000'), self.parse_number('0.0005')], + [self.parse_number('6000000'), self.parse_number('0.0004')], + [self.parse_number('15000000'), self.parse_number('0.0003')], + [self.parse_number('30000000'), self.parse_number('0.0002')], + ], + }, + }, + 'contract': { + 'tierBased': True, + 'percentage': True, + 'maker': self.parse_number('0.0004'), + 'taker': self.parse_number('0.0006'), + 'tiers': { + 'maker': [ + [self.parse_number('0'), self.parse_number('0.0004')], + [self.parse_number('200000'), self.parse_number('0.00038')], + [self.parse_number('1000000'), self.parse_number('0.00036')], + [self.parse_number('5000000'), self.parse_number('0.00034')], + [self.parse_number('10000000'), self.parse_number('0.00032')], + [self.parse_number('15000000'), self.parse_number('0.00028')], + [self.parse_number('30000000'), self.parse_number('0.00024')], + [self.parse_number('50000000'), self.parse_number('0.0002')], + [self.parse_number('100000000'), self.parse_number('0.00016')], + [self.parse_number('300000000'), self.parse_number('0.00012')], + [self.parse_number('500000000'), self.parse_number('0.00008')], + ], + 'taker': [ + [self.parse_number('0'), self.parse_number('0.0006')], + [self.parse_number('200000'), self.parse_number('0.000588')], + [self.parse_number('1000000'), self.parse_number('0.00057')], + [self.parse_number('5000000'), self.parse_number('0.00054')], + [self.parse_number('10000000'), self.parse_number('0.00051')], + [self.parse_number('15000000'), self.parse_number('0.00048')], + [self.parse_number('30000000'), self.parse_number('0.00045')], + [self.parse_number('50000000'), self.parse_number('0.00045')], + [self.parse_number('100000000'), self.parse_number('0.00036')], + [self.parse_number('300000000'), self.parse_number('0.00033')], + [self.parse_number('500000000'), self.parse_number('0.0003')], + ], + }, + }, + }, + 'exceptions': { + 'exact': { + '400': NetworkError, # {"returnCode":1,"msgInfo":"failure","error":{"code":"400","msg":"Connection refused: /10.0.26.71:8080"},"result":null} + '404': ExchangeError, # interface does not exist + '429': RateLimitExceeded, # The request is too frequent, please control the request rate according to the speed limit requirement + '500': ExchangeError, # Service exception + '502': ExchangeError, # Gateway exception + '503': OnMaintenance, # Service unavailable, please try again later + 'AUTH_001': AuthenticationError, # missing request header xt-validate-appkey + 'AUTH_002': AuthenticationError, # missing request header xt-validate-timestamp + 'AUTH_003': AuthenticationError, # missing request header xt-validate-recvwindow + 'AUTH_004': AuthenticationError, # bad request header xt-validate-recvwindow + 'AUTH_005': AuthenticationError, # missing request header xt-validate-algorithms + 'AUTH_006': AuthenticationError, # bad request header xt-validate-algorithms + 'AUTH_007': AuthenticationError, # missing request header xt-validate-signature + 'AUTH_101': AuthenticationError, # ApiKey does not exist + 'AUTH_102': AuthenticationError, # ApiKey is not activated + 'AUTH_103': AuthenticationError, # Signature error, {"rc":1,"mc":"AUTH_103","ma":[],"result":null} + 'AUTH_104': AuthenticationError, # Unbound IP request + 'AUTH_105': AuthenticationError, # outdated message + 'AUTH_106': PermissionDenied, # Exceeded apikey permission + 'SYMBOL_001': BadSymbol, # Symbol not exist + 'SYMBOL_002': BadSymbol, # Symbol offline + 'SYMBOL_003': BadSymbol, # Symbol suspend trading + 'SYMBOL_004': BadSymbol, # Symbol country disallow trading + 'SYMBOL_005': BadSymbol, # The symbol does not support trading via API + 'ORDER_001': InvalidOrder, # Platform rejection + 'ORDER_002': InsufficientFunds, # insufficient funds + 'ORDER_003': InvalidOrder, # Trading Pair Suspended + 'ORDER_004': InvalidOrder, # no transaction + 'ORDER_005': InvalidOrder, # Order not exist + 'ORDER_006': InvalidOrder, # Too many open orders + 'ORDER_007': PermissionDenied, # The sub-account has no transaction authority + 'ORDER_F0101': InvalidOrder, # Trigger Price Filter - Min + 'ORDER_F0102': InvalidOrder, # Trigger Price Filter - Max + 'ORDER_F0103': InvalidOrder, # Trigger Price Filter - Step Value + 'ORDER_F0201': InvalidOrder, # Trigger Quantity Filter - Min + 'ORDER_F0202': InvalidOrder, # Trigger Quantity Filter - Max + 'ORDER_F0203': InvalidOrder, # Trigger Quantity Filter - Step Value + 'ORDER_F0301': InvalidOrder, # Trigger QUOTE_QTY Filter - Min Value + 'ORDER_F0401': InvalidOrder, # Trigger PROTECTION_ONLINE Filter + 'ORDER_F0501': InvalidOrder, # Trigger PROTECTION_LIMIT Filter - Buy Max Deviation + 'ORDER_F0502': InvalidOrder, # Trigger PROTECTION_LIMIT Filter - Sell Max Deviation + 'ORDER_F0601': InvalidOrder, # Trigger PROTECTION_MARKET Filter + 'COMMON_001': ExchangeError, # The user does not exist + 'COMMON_002': ExchangeError, # System busy, please try it later + 'COMMON_003': BadRequest, # Operation failed, please try it later + 'CURRENCY_001': BadRequest, # Information of currency is abnormal + 'DEPOSIT_001': BadRequest, # Deposit is not open + 'DEPOSIT_002': PermissionDenied, # The current account security level is low, please bind any two security verifications in mobile phone/email/Google Authenticator before deposit + 'DEPOSIT_003': BadRequest, # The format of address is incorrect, please enter again + 'DEPOSIT_004': BadRequest, # The address is already exists, please enter again + 'DEPOSIT_005': BadRequest, # Can not find the address of offline wallet + 'DEPOSIT_006': BadRequest, # No deposit address, please try it later + 'DEPOSIT_007': BadRequest, # Address is being generated, please try it later + 'DEPOSIT_008': BadRequest, # Deposit is not available + 'WITHDRAW_001': BadRequest, # Withdraw is not open + 'WITHDRAW_002': BadRequest, # The withdrawal address is invalid + 'WITHDRAW_003': PermissionDenied, # The current account security level is low, please bind any two security verifications in mobile phone/email/Google Authenticator before withdraw + 'WITHDRAW_004': BadRequest, # The withdrawal address is not added + 'WITHDRAW_005': BadRequest, # The withdrawal address cannot be empty + 'WITHDRAW_006': BadRequest, # Memo cannot be empty + 'WITHDRAW_008': PermissionDenied, # Risk control is triggered, withdraw of self currency is not currently supported + 'WITHDRAW_009': PermissionDenied, # Withdraw failed, some hasattr(self, assets) withdraw are restricted by T+1 withdraw + 'WITHDRAW_010': BadRequest, # The precision of withdrawal is invalid + 'WITHDRAW_011': InsufficientFunds, # free balance is not enough + 'WITHDRAW_012': PermissionDenied, # Withdraw failed, your remaining withdrawal limit today is not enough + 'WITHDRAW_013': PermissionDenied, # Withdraw failed, your remaining withdrawal limit today is not enough, the withdrawal amount can be increased by completing a higher level of real-name authentication + 'WITHDRAW_014': BadRequest, # This withdrawal address cannot be used in the internal transfer function, please cancel the internal transfer function before submitting + 'WITHDRAW_015': BadRequest, # The withdrawal amount is not enough to deduct the handling fee + 'WITHDRAW_016': BadRequest, # This withdrawal address is already exists + 'WITHDRAW_017': BadRequest, # This withdrawal has been processed and cannot be canceled + 'WITHDRAW_018': BadRequest, # Memo must be a number + 'WITHDRAW_019': BadRequest, # Memo is incorrect, please enter again + 'WITHDRAW_020': PermissionDenied, # Your withdrawal amount has reached the upper limit for today, please try it tomorrow + 'WITHDRAW_021': PermissionDenied, # Your withdrawal amount has reached the upper limit for today, you can only withdraw up to {0} self time + 'WITHDRAW_022': BadRequest, # Withdrawal amount must be greater than {0} + 'WITHDRAW_023': BadRequest, # Withdrawal amount must be less than {0} + 'WITHDRAW_024': BadRequest, # Withdraw is not supported + 'WITHDRAW_025': BadRequest, # Please create a FIO address in the deposit page + 'FUND_001': BadRequest, # Duplicate request(a bizId can only be requested once) + 'FUND_002': InsufficientFunds, # Insufficient account balance + 'FUND_003': BadRequest, # Transfer operations are not supported(for example, sub-accounts do not support financial transfers) + 'FUND_004': ExchangeError, # Unfreeze failed + 'FUND_005': PermissionDenied, # Transfer prohibited + 'FUND_014': BadRequest, # The transfer-in account id and transfer-out account ID cannot be the same + 'FUND_015': BadRequest, # From and to business types cannot be the same + 'FUND_016': BadRequest, # Leverage transfer, symbol cannot be empty + 'FUND_017': BadRequest, # Parameter error + 'FUND_018': BadRequest, # Invalid freeze record + 'FUND_019': BadRequest, # Freeze users not equal + 'FUND_020': BadRequest, # Freeze currency are not equal + 'FUND_021': BadRequest, # Operation not supported + 'FUND_022': BadRequest, # Freeze record does not exist + 'FUND_044': BadRequest, # The maximum length of the amount is 113 and cannot exceed the limit + 'TRANSFER_001': BadRequest, # Duplicate request(a bizId can only be requested once) + 'TRANSFER_002': InsufficientFunds, # Insufficient account balance + 'TRANSFER_003': BadRequest, # User not registered + 'TRANSFER_004': PermissionDenied, # The currency is not allowed to be transferred + 'TRANSFER_005': PermissionDenied, # The user’s currency is not allowed to be transferred + 'TRANSFER_006': PermissionDenied, # Transfer prohibited + 'TRANSFER_007': RequestTimeout, # Request timed out + 'TRANSFER_008': BadRequest, # Transferring to a leveraged account is abnormal + 'TRANSFER_009': BadRequest, # Departing from a leveraged account is abnormal + 'TRANSFER_010': PermissionDenied, # Leverage cleared, transfer prohibited + 'TRANSFER_011': PermissionDenied, # Leverage with borrowing, transfer prohibited + 'TRANSFER_012': PermissionDenied, # Currency transfer prohibited + 'symbol_not_support_trading_via_api': BadSymbol, # {"returnCode":1,"msgInfo":"failure","error":{"code":"symbol_not_support_trading_via_api","msg":"The symbol does not support trading via API"},"result":null} + 'open_order_min_nominal_value_limit': InvalidOrder, # {"returnCode":1,"msgInfo":"failure","error":{"code":"open_order_min_nominal_value_limit","msg":"Exceeds the minimum notional value of a single order"},"result":null} + 'insufficient_balance': InsufficientFunds, + }, + 'broad': { + 'The symbol does not support trading via API': BadSymbol, # {"returnCode":1,"msgInfo":"failure","error":{"code":"symbol_not_support_trading_via_api","msg":"The symbol does not support trading via API"},"result":null} + 'Exceeds the minimum notional value of a single order': InvalidOrder, # {"returnCode":1,"msgInfo":"failure","error":{"code":"open_order_min_nominal_value_limit","msg":"Exceeds the minimum notional value of a single order"},"result":null} + 'insufficient balance': InsufficientFunds, + }, + }, + 'timeframes': { + '1m': '1m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1h', # spot only + '2h': '2h', # spot only + '4h': '4h', + '6h': '6h', # spot only + '8h': '8h', # spot only + '1d': '1d', + '3d': '3d', # spot only + '1w': '1w', + '1M': '1M', # spot only + }, + 'commonCurrencies': {}, + 'options': { + 'adjustForTimeDifference': False, + 'timeDifference': 0, + 'accountsById': { + 'spot': 'SPOT', + 'leverage': 'LEVER', + 'finance': 'FINANCE', + 'swap': 'FUTURES_U', + 'future': 'FUTURES_U', + 'linear': 'FUTURES_U', + 'inverse': 'FUTURES_C', + }, + 'networks': { + 'ERC20': 'Ethereum', + 'TRC20': 'Tron', + 'BEP20': 'BNB Smart Chain', + 'BEP2': 'BNB-BEP2', + 'ETH': 'Ethereum', + 'TRON': 'Tron', + 'BNB': 'BNB Smart Chain', + 'AVAX': 'AVAX C-Chain', + 'GAL': 'GAL(FT)', + 'ALEO': 'ALEO(IOU)', + 'BTC': 'Bitcoin', + 'XT': 'XT Smart Chain', + 'ETC': 'Ethereum Classic', + 'MATIC': 'Polygon', + 'LTC': 'Litecoin', + 'BTS': 'BitShares', + 'XRP': 'Ripple', + 'XLM': 'Stellar Network', + 'ADA': 'Cardano', + 'XWC': 'XWC-XWC', + 'DOGE': 'dogecoin', + 'DCR': 'Decred', + 'SC': 'Siacoin', + 'XTZ': 'Tezos', + 'ZEC': 'Zcash', + 'XMR': 'Monero', + 'LSK': 'Lisk', + 'ATOM': 'Cosmos', + 'ONT': 'Ontology', + 'ALGO': 'Algorand', + 'SOL': 'SOL-SOL', + 'DOT': 'Polkadot', + 'ZEN': 'Horizen', + 'FIL': 'Filecoin', + 'CHZ': 'chz', + 'ICP': 'Internet Computer', + 'KSM': 'Kusama', + 'LUNA': 'Terra', + 'THETA': 'Theta Token', + 'FTM': 'Fantom', + 'VET': 'VeChain', + 'NEAR': 'NEAR Protocol', + 'ONE': 'Harmony', + 'KLAY': 'Klaytn', + 'AR': 'Arweave', + 'CELT': 'OKT', + 'EGLD': 'Elrond eGold', + 'CRO': 'CRO-CRONOS', + 'BCH': 'Bitcoin Cash', + 'GLMR': 'Moonbeam', + 'LOOP': 'LOOP-LRC', + 'REI': 'REI Network', + 'ASTR': 'Astar Network', + 'OP': 'OPT', + 'MMT': 'MMT-MMT', + 'TBC': 'TBC-TBC', + 'OMAX': 'OMAX-OMAX CHAIN', + 'GMMT': 'GMMT chain', + 'ZIL': 'Zilliqa', + }, + 'networksById': { + 'Ethereum': 'ERC20', + 'Tron': 'TRC20', + 'BNB Smart Chain': 'BEP20', + 'BNB-BEP2': 'BEP2', + 'Bitcoin': 'BTC', + 'XT Smart Chain': 'XT', + 'Ethereum Classic': 'ETC', + 'Polygon': 'MATIC', + 'Litecoin': 'LTC', + 'BitShares': 'BTS', + 'Ripple': 'XRP', + 'Stellar Network': 'XLM', + 'Cardano': 'ADA', + 'XWC-XWC': 'XWC', + 'dogecoin': 'DOGE', + 'Decred': 'DCR', + 'Siacoin': 'SC', + 'Tezos': 'XTZ', + 'Zcash': 'ZEC', + 'Monero': 'XMR', + 'Lisk': 'LSK', + 'Cosmos': 'ATOM', + 'Ontology': 'ONT', + 'Algorand': 'ALGO', + 'SOL-SOL': 'SOL', + 'Polkadot': 'DOT', + 'Horizen': 'ZEN', + 'Filecoin': 'FIL', + 'chz': 'CHZ', + 'Internet Computer': 'ICP', + 'Kusama': 'KSM', + 'Terra': 'LUNA', + 'Theta Token': 'THETA', + 'Fantom': 'FTM', + 'VeChain': 'VET', + 'AVAX C-Chain': 'AVAX', + 'NEAR Protocol': 'NEAR', + 'Harmony': 'ONE', + 'Klaytn': 'KLAY', + 'Arweave': 'AR', + 'OKT': 'CELT', + 'Elrond eGold': 'EGLD', + 'CRO-CRONOS': 'CRO', + 'Bitcoin Cash': 'BCH', + 'Moonbeam': 'GLMR', + 'LOOP-LRC': 'LOOP', + 'REI Network': 'REI', + 'Astar Network': 'ASTR', + 'GAL(FT)': 'GAL', + 'ALEO(IOU)': 'ALEO', + 'OPT': 'OP', + 'MMT-MMT': 'MMT', + 'TBC-TBC': 'TBC', + 'OMAX-OMAX CHAIN': 'OMAX', + 'GMMT chain': 'GMMT', + 'Zilliqa': 'ZIL', + }, + 'createMarketBuyOrderRequiresPrice': True, + 'recvWindow': '5000', # in milliseconds, spot only + }, + 'features': { + 'default': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': False, + 'triggerDirection': False, + 'triggerPriceType': None, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': True, + 'limit': 100, + 'daysBack': 100000, # todo + 'untilDays': 100000, # todo + 'marketType': True, + 'subType': True, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': True, # todo TPSL kind + 'trailing': False, + 'marketType': True, + 'subType': True, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': True, + 'limit': 100, + 'trigger': True, # todo TPSL + 'trailing': False, + 'marketType': True, + 'subType': True, + 'symbolRequired': False, + }, + 'fetchOrders': { + 'marginMode': True, + 'limit': 100, + 'daysBack': 100000, # todo + 'untilDays': 100000, # todo + 'trigger': True, # todo TPSL + 'trailing': False, + 'marketType': True, + 'subType': True, + 'symbolRequired': False, + }, + 'fetchClosedOrders': { + 'marginMode': True, + 'limit': 100, + 'daysBack': 100000, # todo + 'daysBackCanceled': 1, # todo + 'untilDays': 100000, # todo + 'trigger': True, # todo TPSL + 'trailing': False, + 'marketType': True, + 'subType': True, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 1000, # todo for derivatives + }, + }, + 'spot': { + 'extends': 'default', + }, + 'forDerivatives': { + 'extends': 'default', + 'createOrder': { + 'triggerPrice': True, + # todo + 'triggerPriceType': { + 'last': True, + 'mark': True, + 'index': True, + }, + 'stopLossPrice': True, + 'takeProfitPrice': True, + }, + 'fetchMyTrades': { + 'daysBack': None, + 'untilDays': None, + }, + }, + 'swap': { + 'linear': { + 'extends': 'forDerivatives', + }, + 'inverse': { + 'extends': 'forDerivatives', + }, + }, + 'future': { + 'linear': { + 'extends': 'forDerivatives', + }, + 'inverse': { + 'extends': 'forDerivatives', + }, + }, + }, + }) + + def nonce(self): + return self.milliseconds() - self.options['timeDifference'] + + def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the xt server + + https://doc.xt.com/#market1serverInfo + + :param dict params: extra parameters specific to the xt api endpoint + :returns int: the current integer timestamp in milliseconds from the xt server + """ + response = self.publicSpotGetTime(params) + # + # { + # "rc": 0, + # "mc": "SUCCESS", + # "ma": [], + # "result": { + # "serverTime": 1677823301643 + # } + # } + # + data = self.safe_value(response, 'result') + return self.safe_integer(data, 'serverTime') + + def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://doc.xt.com/#deposit_withdrawalsupportedCurrenciesGet + + :param dict params: extra parameters specific to the xt api endpoint + :returns dict: an associative dictionary of currencies + """ + promisesRaw = [self.publicSpotGetWalletSupportCurrency(params), self.publicSpotGetCurrencies(params)] + chainsResponse, currenciesResponse = promisesRaw + # + # currencies + # + # { + # "time": "1686626116145", + # "version": "5dbbb2f2527c22b2b2e3b47187ef13d1", + # "currencies": [ + # { + # "id": "2", + # "currency": "btc", + # "fullName": "Bitcoin", + # "logo": "https://a.static-global.com/1/currency/btc.png", + # "cmcLink": "https://coinmarketcap.com/currencies/bitcoin/", + # "weight": "99999", + # "maxPrecision": "10", + # "depositStatus": "1", + # "withdrawStatus": "1", + # "convertEnabled": "1", + # "transferEnabled": "1", + # "isChainExist": "1", + # "plates": [152] + # }, + # ], + # } + # + # + # chains + # + # { + # "rc": 0, + # "mc": "SUCCESS", + # "ma": [], + # "result": [ + # { + # "currency": "btc", + # "supportChains": [ + # { + # "chain": "Bitcoin", + # "depositEnabled": True, + # "withdrawEnabled": True, + # "withdrawFeeAmount": 0.0009, + # "withdrawMinAmount": 0.0005, + # "depositFeeRate": 0 + # }, + # ] + # }, + # ] + # } + # + # note: individual network's full data is available on per-currency endpoint: https://www.xt.com/sapi/v4/balance/public/currency/11 + # + chainsData = self.safe_value(chainsResponse, 'result', []) + currenciesResult = self.safe_value(currenciesResponse, 'result', []) + currenciesData = self.safe_value(currenciesResult, 'currencies', []) + chainsDataIndexed = self.index_by(chainsData, 'currency') + result = {} + for i in range(0, len(currenciesData)): + entry = currenciesData[i] + currencyId = self.safe_string(entry, 'currency') + code = self.safe_currency_code(currencyId) + networkEntry = self.safe_value(chainsDataIndexed, currencyId, {}) + rawNetworks = self.safe_value(networkEntry, 'supportChains', []) + networks = {} + for j in range(0, len(rawNetworks)): + rawNetwork = rawNetworks[j] + networkId = self.safe_string(rawNetwork, 'chain') + networkCode = self.network_id_to_code(networkId, code) + networks[networkCode] = { + 'info': rawNetwork, + 'id': networkId, + 'network': networkCode, + 'name': None, + 'active': None, + 'fee': self.safe_number(rawNetwork, 'withdrawFeeAmount'), + 'precision': None, + 'deposit': self.safe_bool(rawNetwork, 'depositEnabled'), + 'withdraw': self.safe_bool(rawNetwork, 'withdrawEnabled'), + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': self.safe_number(rawNetwork, 'withdrawMinAmount'), + 'max': None, + }, + 'deposit': { + 'min': None, + 'max': None, + }, + }, + } + typeRaw = self.safe_string(entry, 'type') + type: Str = None + if typeRaw == 'FT': + type = 'crypto' + else: + type = 'other' + result[code] = self.safe_currency_structure({ + 'info': entry, + 'id': currencyId, + 'code': code, + 'name': self.safe_string(entry, 'fullName'), + 'active': None, + 'fee': None, + 'precision': self.parse_number(self.parse_precision(self.safe_string(entry, 'maxPrecision'))), + 'deposit': self.safe_string(entry, 'depositStatus') == '1', + 'withdraw': self.safe_string(entry, 'withdrawStatus') == '1', + 'networks': networks, + 'type': type, + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + 'deposit': { + 'min': None, + 'max': None, + }, + }, + }) + return result + + def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for xt + + https://doc.xt.com/#market2symbol + https://doc.xt.com/#futures_quotesgetSymbols + + :param dict params: extra parameters specific to the xt api endpoint + :returns dict[]: an array of objects representing market data + """ + if self.options['adjustForTimeDifference']: + self.load_time_difference() + promisesUnresolved = [ + self.fetch_spot_markets(params), + self.fetch_swap_and_future_markets(params), + ] + promises = promisesUnresolved + spotMarkets = promises[0] + swapAndFutureMarkets = promises[1] + return self.array_concat(spotMarkets, swapAndFutureMarkets) + + def fetch_spot_markets(self, params={}): + response = self.publicSpotGetSymbol(params) + # + # { + # "rc": 0, + # "mc": "SUCCESS", + # "ma": [], + # "result": { + # "time": 1677881368812, + # "version": "abb101d1543e54bee40687b135411ba0", + # "symbols": [ + # { + # "id": 640, + # "symbol": "xt_usdt", + # "state": "ONLINE", + # "stateTime": 1554048000000, + # "tradingEnabled": True, + # "openapiEnabled": True, + # "nextStateTime": null, + # "nextState": null, + # "depthMergePrecision": 5, + # "baseCurrency": "xt", + # "baseCurrencyPrecision": 8, + # "baseCurrencyId": 128, + # "quoteCurrency": "usdt", + # "quoteCurrencyPrecision": 8, + # "quoteCurrencyId": 11, + # "pricePrecision": 4, + # "quantityPrecision": 2, + # "orderTypes": ["LIMIT","MARKET"], + # "timeInForces": ["GTC","IOC"], + # "displayWeight": 10002, + # "displayLevel": "FULL", + # "plates": [], + # "filters":[ + # { + # "filter": "QUOTE_QTY", + # "min": "1" + # }, + # { + # "filter": "PROTECTION_LIMIT", + # "buyMaxDeviation": "0.8", + # "sellMaxDeviation": "4" + # }, + # { + # "filter": "PROTECTION_MARKET", + # "maxDeviation": "0.02" + # } + # ] + # }, + # ] + # } + # } + # + data = self.safe_value(response, 'result', {}) + symbols = self.safe_value(data, 'symbols', []) + return self.parse_markets(symbols) + + def fetch_swap_and_future_markets(self, params={}): + markets = [self.publicLinearGetFutureMarketV1PublicSymbolList(params), self.publicInverseGetFutureMarketV1PublicSymbolList(params)] + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": [ + # { + # "id": 52, + # "symbolGroupId": 71, + # "symbol": "xt_usdt", + # "pair": "xt_usdt", + # "contractType": "PERPETUAL", + # "productType": "perpetual", + # "predictEventType": null, + # "underlyingType": "U_BASED", + # "contractSize": "1", + # "tradeSwitch": True, + # "isDisplay": True, + # "isOpenApi": False, + # "state": 0, + # "initLeverage": 20, + # "initPositionType": "CROSSED", + # "baseCoin": "xt", + # "quoteCoin": "usdt", + # "baseCoinPrecision": 8, + # "baseCoinDisplayPrecision": 4, + # "quoteCoinPrecision": 8, + # "quoteCoinDisplayPrecision": 4, + # "quantityPrecision": 0, + # "pricePrecision": 4, + # "supportOrderType": "LIMIT,MARKET", + # "supportTimeInForce": "GTC,FOK,IOC,GTX", + # "supportEntrustType": "TAKE_PROFIT,STOP,TAKE_PROFIT_MARKET,STOP_MARKET,TRAILING_STOP_MARKET", + # "supportPositionType": "CROSSED,ISOLATED", + # "minQty": "1", + # "minNotional": "5", + # "maxNotional": "20000000", + # "multiplierDown": "0.1", + # "multiplierUp": "0.1", + # "maxOpenOrders": 200, + # "maxEntrusts": 200, + # "makerFee": "0.0004", + # "takerFee": "0.0006", + # "liquidationFee": "0.01", + # "marketTakeBound": "0.1", + # "depthPrecisionMerge": 5, + # "labels": ["HOT"], + # "onboardDate": 1657101601000, + # "enName": "XTUSDT ", + # "cnName": "XTUSDT", + # "minStepPrice": "0.0001", + # "minPrice": null, + # "maxPrice": null, + # "deliveryDate": 1669879634000, + # "deliveryPrice": null, + # "deliveryCompletion": False, + # "cnDesc": null, + # "enDesc": null + # }, + # ] + # } + # + swapAndFutureMarkets = self.array_concat(self.safe_value(markets[0], 'result', []), self.safe_value(markets[1], 'result', [])) + return self.parse_markets(swapAndFutureMarkets) + + def parse_markets(self, markets): + result = [] + for i in range(0, len(markets)): + result.append(self.parse_market(markets[i])) + return result + + def parse_market(self, market: dict) -> Market: + # + # spot + # + # { + # "id": 640, + # "symbol": "xt_usdt", + # "state": "ONLINE", + # "stateTime": 1554048000000, + # "tradingEnabled": True, + # "openapiEnabled": True, + # "nextStateTime": null, + # "nextState": null, + # "depthMergePrecision": 5, + # "baseCurrency": "xt", + # "baseCurrencyPrecision": 8, + # "baseCurrencyId": 128, + # "quoteCurrency": "usdt", + # "quoteCurrencyPrecision": 8, + # "quoteCurrencyId": 11, + # "pricePrecision": 4, + # "quantityPrecision": 2, + # "orderTypes": ["LIMIT","MARKET"], + # "timeInForces": ["GTC","IOC"], + # "displayWeight": 10002, + # "displayLevel": "FULL", + # "plates": [], + # "filters":[ + # { + # "filter": "QUOTE_QTY", + # "min": "1" + # }, + # { + # "filter": "PRICE", + # "min": null, + # "max": null, + # "tickSize": null + # }, + # { + # "filter": "QUANTITY", + # "min": null, + # "max": null, + # "tickSize": null + # }, + # { + # "filter": "PROTECTION_LIMIT", + # "buyMaxDeviation": "0.8", + # "sellMaxDeviation": "4" + # }, + # { + # "filter": "PROTECTION_MARKET", + # "maxDeviation": "0.02" + # }, + # { + # "filter": "PROTECTION_ONLINE", + # "durationSeconds": "300", + # "maxPriceMultiple": "5" + # }, + # ] + # } + # + # swap and future + # + # { + # "id": 52, + # "symbolGroupId": 71, + # "symbol": "xt_usdt", + # "pair": "xt_usdt", + # "contractType": "PERPETUAL", + # "productType": "perpetual", + # "predictEventType": null, + # "underlyingType": "U_BASED", + # "contractSize": "1", + # "tradeSwitch": True, + # "isDisplay": True, + # "isOpenApi": False, + # "state": 0, + # "initLeverage": 20, + # "initPositionType": "CROSSED", + # "baseCoin": "xt", + # "quoteCoin": "usdt", + # "baseCoinPrecision": 8, + # "baseCoinDisplayPrecision": 4, + # "quoteCoinPrecision": 8, + # "quoteCoinDisplayPrecision": 4, + # "quantityPrecision": 0, + # "pricePrecision": 4, + # "supportOrderType": "LIMIT,MARKET", + # "supportTimeInForce": "GTC,FOK,IOC,GTX", + # "supportEntrustType": "TAKE_PROFIT,STOP,TAKE_PROFIT_MARKET,STOP_MARKET,TRAILING_STOP_MARKET", + # "supportPositionType": "CROSSED,ISOLATED", + # "minQty": "1", + # "minNotional": "5", + # "maxNotional": "20000000", + # "multiplierDown": "0.1", + # "multiplierUp": "0.1", + # "maxOpenOrders": 200, + # "maxEntrusts": 200, + # "makerFee": "0.0004", + # "takerFee": "0.0006", + # "liquidationFee": "0.01", + # "marketTakeBound": "0.1", + # "depthPrecisionMerge": 5, + # "labels": ["HOT"], + # "onboardDate": 1657101601000, + # "enName": "XTUSDT ", + # "cnName": "XTUSDT", + # "minStepPrice": "0.0001", + # "minPrice": null, + # "maxPrice": null, + # "deliveryDate": 1669879634000, + # "deliveryPrice": null, + # "deliveryCompletion": False, + # "cnDesc": null, + # "enDesc": null + # } + # + id = self.safe_string(market, 'symbol') + baseId = self.safe_string_2(market, 'baseCurrency', 'baseCoin') + quoteId = self.safe_string_2(market, 'quoteCurrency', 'quoteCoin') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + state = self.safe_string(market, 'state') + symbol = base + '/' + quote + filters = self.safe_value(market, 'filters', []) + minAmount = None + maxAmount = None + minCost = None + maxCost = None + minPrice = None + maxPrice = None + amountPrecision = None + for i in range(0, len(filters)): + entry = filters[i] + filter = self.safe_string(entry, 'filter') + if filter == 'QUANTITY': + minAmount = self.safe_number(entry, 'min') + maxAmount = self.safe_number(entry, 'max') + amountPrecision = self.safe_number(entry, 'tickSize') + if filter == 'QUOTE_QTY': + minCost = self.safe_number(entry, 'min') + if filter == 'PRICE': + minPrice = self.safe_number(entry, 'min') + maxPrice = self.safe_number(entry, 'max') + if amountPrecision is None: + amountPrecision = self.parse_number(self.parse_precision(self.safe_string(market, 'quantityPrecision'))) + underlyingType = self.safe_string(market, 'underlyingType') + linear = None + inverse = None + settleId = None + settle = None + expiry = None + future = False + swap = False + contract = False + spot = True + type = 'spot' + if underlyingType == 'U_BASED': + symbol = symbol + ':' + quote + settleId = baseId + settle = quote + linear = True + inverse = False + elif underlyingType == 'COIN_BASED': + symbol = symbol + ':' + base + settleId = baseId + settle = base + linear = False + inverse = True + if underlyingType is not None: + expiry = self.safe_integer(market, 'deliveryDate') + productType = self.safe_string(market, 'productType') + if productType != 'perpetual': + symbol = symbol + '-' + self.yymmdd(expiry) + type = 'future' + future = True + else: + type = 'swap' + swap = True + minAmount = self.safe_number(market, 'minQty') + minCost = self.safe_number(market, 'minNotional') + maxCost = self.safe_number(market, 'maxNotional') + minPrice = self.safe_number(market, 'minPrice') + maxPrice = self.safe_number(market, 'maxPrice') + contract = True + spot = False + isActive = False + if contract: + isActive = self.safe_value(market, 'isOpenApi', False) + else: + if (state == 'ONLINE') and (self.safe_value(market, 'tradingEnabled')) and (self.safe_value(market, 'openapiEnabled')): + isActive = True + return self.safe_market_structure({ + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': type, + 'spot': spot, + 'margin': None, + 'swap': swap, + 'future': future, + 'option': False, + 'active': isActive, + 'contract': contract, + 'linear': linear, + 'inverse': inverse, + 'taker': self.safe_number(market, 'takerFee'), + 'maker': self.safe_number(market, 'makerFee'), + 'contractSize': self.safe_number(market, 'contractSize'), + 'expiry': expiry, + 'expiryDatetime': self.iso8601(expiry), + 'strike': None, + 'optionType': None, + 'precision': { + 'price': self.parse_number(self.parse_precision(self.safe_string(market, 'pricePrecision'))), + 'amount': amountPrecision, + 'base': self.parse_number(self.parse_precision(self.safe_string(market, 'baseCoinPrecision'))), + 'quote': self.parse_number(self.parse_precision(self.safe_string(market, 'quoteCoinPrecision'))), + }, + 'limits': { + 'leverage': { + 'min': self.parse_number('1'), + 'max': None, + }, + 'amount': { + 'min': minAmount, + 'max': maxAmount, + }, + 'price': { + 'min': minPrice, + 'max': maxPrice, + }, + 'cost': { + 'min': minCost, + 'max': maxCost, + }, + }, + 'info': market, + }) + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}): + """ + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + + https://doc.xt.com/#market4kline + https://doc.xt.com/#futures_quotesgetKLine + + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict params: extra parameters specific to the xt api endpoint + :param int [params.until]: timestamp in ms of the latest candle to fetch + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate', False) + if paginate: + return self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params, 1000) + market = self.market(symbol) + request = { + 'symbol': market['id'], + 'interval': self.safe_string(self.timeframes, timeframe, timeframe), + } + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + else: + request['limit'] = 1000 + until = self.safe_integer(params, 'until') + params = self.omit(params, ['until']) + if until is not None: + request['endTime'] = until + response = None + if market['linear']: + response = self.publicLinearGetFutureMarketV1PublicQKline(self.extend(request, params)) + elif market['inverse']: + response = self.publicInverseGetFutureMarketV1PublicQKline(self.extend(request, params)) + else: + response = self.publicSpotGetKline(self.extend(request, params)) + # + # spot + # + # { + # "rc": 0, + # "mc": "SUCCESS", + # "ma": [], + # "result": [ + # { + # "t": 1678167720000, + # "o": "22467.85", + # "c": "22465.87", + # "h": "22468.86", + # "l": "22465.21", + # "q": "1.316656", + # "v": "29582.73018498" + # }, + # ] + # } + # + # swap and future + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": [ + # { + # "s": "btc_usdt", + # "p": "btc_usdt", + # "t": 1678168020000, + # "o": "22450.0", + # "c": "22441.5", + # "h": "22450.0", + # "l": "22441.5", + # "a": "312931", + # "v": "702461.58895" + # }, + # ] + # } + # + ohlcvs = self.safe_value(response, 'result', []) + return self.parse_ohlcvs(ohlcvs, market, timeframe, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # spot + # + # { + # "t": 1678167720000, + # "o": "22467.85", + # "c": "22465.87", + # "h": "22468.86", + # "l": "22465.21", + # "q": "1.316656", + # "v": "29582.73018498" + # } + # + # swap and future + # + # { + # "s": "btc_usdt", + # "p": "btc_usdt", + # "t": 1678168020000, + # "o": "22450.0", + # "c": "22441.5", + # "h": "22450.0", + # "l": "22441.5", + # "a": "312931", + # "v": "702461.58895" + # } + # + volumeIndex = 'v' if (market['inverse']) else 'a' + return [ + self.safe_integer(ohlcv, 't'), + self.safe_number(ohlcv, 'o'), + self.safe_number(ohlcv, 'h'), + self.safe_number(ohlcv, 'l'), + self.safe_number(ohlcv, 'c'), + self.safe_number_2(ohlcv, 'q', volumeIndex), + ] + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}): + """ + + https://doc.xt.com/#market3depth + https://doc.xt.com/#futures_quotesgetDepth + + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str symbol: unified market symbol to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict params: extra parameters specific to the xt api endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request = { + 'symbol': market['id'], + } + response = None + if market['spot']: + if limit is not None: + request['limit'] = min(limit, 500) + response = self.publicSpotGetDepth(self.extend(request, params)) + else: + if limit is not None: + request['level'] = min(limit, 50) + else: + request['level'] = 50 + if market['linear']: + response = self.publicLinearGetFutureMarketV1PublicQDepth(self.extend(request, params)) + elif market['inverse']: + response = self.publicInverseGetFutureMarketV1PublicQDepth(self.extend(request, params)) + # + # spot + # + # { + # "rc": 0, + # "mc": "SUCCESS", + # "ma": [], + # "result": { + # "timestamp": 1678169975184, + # "lastUpdateId": 1675333221812, + # "bids": [ + # ["22444.51", "0.129887"], + # ["22444.49", "0.114245"], + # ["22444.30", "0.225956"] + # ], + # "asks": [ + # ["22446.19", "0.095330"], + # ["22446.24", "0.224413"], + # ["22446.28", "0.329095"] + # ] + # } + # } + # + # swap and future + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": { + # "t": 1678170311005, + # "s": "btc_usdt", + # "u": 471694545627, + # "b": [ + # ["22426", "198623"], + # ["22423.5", "80295"], + # ["22423", "163580"] + # ], + # "a": [ + # ["22427", "3417"], + # ["22428.5", "43532"], + # ["22429", "119"] + # ] + # } + # } + # + orderBook = self.safe_value(response, 'result', {}) + timestamp = self.safe_integer_2(orderBook, 'timestamp', 't') + if market['spot']: + ob = self.parse_order_book(orderBook, symbol, timestamp) + ob['nonce'] = self.safe_integer(orderBook, 'lastUpdateId') + return ob + swapOb = self.parse_order_book(orderBook, symbol, timestamp, 'b', 'a') + swapOb['nonce'] = self.safe_integer_2(orderBook, 'u', 'lastUpdateId') + return swapOb + + def fetch_ticker(self, symbol: str, params={}): + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://doc.xt.com/#market10ticker24h + https://doc.xt.com/#futures_quotesgetAggTicker + + :param str symbol: unified market symbol to fetch the ticker for + :param dict params: extra parameters specific to the xt api endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request = { + 'symbol': market['id'], + } + response = None + if market['linear']: + response = self.publicLinearGetFutureMarketV1PublicQAggTicker(self.extend(request, params)) + elif market['inverse']: + response = self.publicInverseGetFutureMarketV1PublicQAggTicker(self.extend(request, params)) + else: + response = self.publicSpotGetTicker24h(self.extend(request, params)) + # + # spot + # + # { + # "rc": 0, + # "mc": "SUCCESS", + # "ma": [], + # "result": [ + # { + # "s": "btc_usdt", + # "t": 1678172693931, + # "cv": "34.00", + # "cr": "0.0015", + # "o": "22398.05", + # "l": "22323.72", + # "h": "22600.50", + # "c": "22432.05", + # "q": "7962.256931", + # "v": "178675209.47416856" + # } + # ] + # } + # + # swap and future + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": { + # "t": 1678172848572, + # "s": "btc_usdt", + # "c": "22415.5", + # "h": "22590.0", + # "l": "22310.0", + # "a": "623654031", + # "v": "1399166074.31675", + # "o": "22381.5", + # "r": "0.0015", + # "i": "22424.5", + # "m": "22416.5", + # "bp": "22415", + # "ap": "22415.5" + # } + # } + # + ticker = self.safe_value(response, 'result') + if market['spot']: + return self.parse_ticker(ticker[0], market) + return self.parse_ticker(ticker, market) + + def fetch_tickers(self, symbols: List[str] = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical calculations with the information calculated over the past 24 hours each market + + https://doc.xt.com/#market10ticker24h + https://doc.xt.com/#futures_quotesgetAggTickers + + :param str [symbols]: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict params: extra parameters specific to the xt api endpoint + :returns dict: an array of `ticker structures ` + """ + self.load_markets() + market = None + if symbols is not None: + symbols = self.market_symbols(symbols) + market = self.market(symbols[0]) + request = {} + type = None + subType = None + response = None + type, params = self.handle_market_type_and_params('fetchTickers', market, params) + subType, params = self.handle_sub_type_and_params('fetchTickers', market, params) + if subType == 'inverse': + response = self.publicInverseGetFutureMarketV1PublicQAggTickers(self.extend(request, params)) + elif (subType == 'linear') or (type == 'swap') or (type == 'future'): + response = self.publicLinearGetFutureMarketV1PublicQAggTickers(self.extend(request, params)) + else: + response = self.publicSpotGetTicker24h(self.extend(request, params)) + # + # spot + # + # { + # "rc": 0, + # "mc": "SUCCESS", + # "ma": [], + # "result": [ + # { + # "s": "btc_usdt", + # "t": 1678172693931, + # "cv": "34.00", + # "cr": "0.0015", + # "o": "22398.05", + # "l": "22323.72", + # "h": "22600.50", + # "c": "22432.05", + # "q": "7962.256931", + # "v": "178675209.47416856" + # } + # ] + # } + # + # swap and future + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": [ + # { + # "t": 1680738775108, + # "s": "badger_usdt", + # "c": "2.7176", + # "h": "2.7917", + # "l": "2.6818", + # "a": "88332", + # "v": "242286.3520", + # "o": "2.7422", + # "r": "-0.0089", + # "i": "2.7155", + # "m": "2.7161", + # "bp": "2.7152", + # "ap": "2.7176" + # }, + # ] + # } + # + tickers = self.safe_value(response, 'result', []) + result = {} + for i in range(0, len(tickers)): + ticker = self.parse_ticker(tickers[i], market) + symbol = ticker['symbol'] + result[symbol] = ticker + return self.filter_by_array(result, 'symbol', symbols) + + def fetch_bids_asks(self, symbols: List[str] = None, params={}): + """ + fetches the bid and ask price and volume for multiple markets + + https://doc.xt.com/#market9tickerBook + + :param str [symbols]: unified symbols of the markets to fetch the bids and asks for, all markets are returned if not assigned + :param dict params: extra parameters specific to the xt api endpoint + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + symbols = self.market_symbols(symbols) + request = {} + market = None + if symbols is not None: + market = self.market(symbols[0]) + subType = None + subType, params = self.handle_sub_type_and_params('fetchBidsAsks', market, params) + if subType is not None: + raise NotSupported(self.id + ' fetchBidsAsks() is not available for swap and future markets, only spot markets are supported') + response = self.publicSpotGetTickerBook(self.extend(request, params)) + # + # { + # "rc": 0, + # "mc": "SUCCESS", + # "ma": [], + # "result": [ + # { + # "s": "kas_usdt", + # "t": 1679539891853, + # "ap": "0.016298", + # "aq": "5119.09", + # "bp": "0.016290", + # "bq": "135.37" + # }, + # ] + # } + # + tickers = self.safe_value(response, 'result', []) + return self.parse_tickers(tickers, symbols) + + def parse_ticker(self, ticker, market=None): + # + # spot: fetchTicker, fetchTickers + # + # { + # "s": "btc_usdt", + # "t": 1678172693931, + # "cv": "34.00", + # "cr": "0.0015", + # "o": "22398.05", + # "l": "22323.72", + # "h": "22600.50", + # "c": "22432.05", + # "q": "7962.256931", + # "v": "178675209.47416856" + # } + # + # swap and future: fetchTicker, fetchTickers + # + # { + # "t": 1678172848572, + # "s": "btc_usdt", + # "c": "22415.5", + # "h": "22590.0", + # "l": "22310.0", + # "a": "623654031", + # "v": "1399166074.31675", + # "o": "22381.5", + # "r": "0.0015", + # "i": "22424.5", + # "m": "22416.5", + # "bp": "22415", + # "ap": "22415.5" + # } + # + # fetchBidsAsks + # + # { + # "s": "kas_usdt", + # "t": 1679539891853, + # "ap": "0.016298", + # "aq": "5119.09", + # "bp": "0.016290", + # "bq": "135.37" + # } + # + marketId = self.safe_string(ticker, 's') + marketType = market['type'] if (market is not None) else None + hasSpotKeys = ('cv' in ticker) or ('aq' in ticker) + if marketType is None: + marketType = 'spot' if hasSpotKeys else 'contract' + market = self.safe_market(marketId, market, '_', marketType) + symbol = market['symbol'] + timestamp = self.safe_integer(ticker, 't') + percentage = self.safe_string_2(ticker, 'cr', 'r') + if percentage is not None: + percentage = Precise.string_mul(percentage, '100') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_number(ticker, 'h'), + 'low': self.safe_number(ticker, 'l'), + 'bid': self.safe_number(ticker, 'bp'), + 'bidVolume': self.safe_number(ticker, 'bq'), + 'ask': self.safe_number(ticker, 'ap'), + 'askVolume': self.safe_number(ticker, 'aq'), + 'vwap': None, + 'open': self.safe_string(ticker, 'o'), + 'close': self.safe_string(ticker, 'c'), + 'last': self.safe_string(ticker, 'c'), + 'previousClose': None, + 'change': self.safe_number(ticker, 'cv'), + 'percentage': self.parse_number(percentage), + 'average': None, + 'baseVolume': None, + 'quoteVolume': self.safe_number_2(ticker, 'a', 'v'), + 'info': ticker, + }, market) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}): + """ + get the list of most recent trades for a particular symbol + + https://doc.xt.com/#market5tradeRecent + https://doc.xt.com/#futures_quotesgetDeal + + :param str symbol: unified market symbol to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict params: extra parameters specific to the xt api endpoint + :returns dict[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request = { + 'symbol': market['id'], + } + response = None + if market['spot']: + if limit is not None: + request['limit'] = limit + response = self.publicSpotGetTradeRecent(self.extend(request, params)) + else: + if limit is not None: + request['num'] = limit + if market['linear']: + response = self.publicLinearGetFutureMarketV1PublicQDeal(self.extend(request, params)) + elif market['inverse']: + response = self.publicInverseGetFutureMarketV1PublicQDeal(self.extend(request, params)) + # + # spot + # + # { + # "rc": 0, + # "mc": "SUCCESS", + # "ma": [], + # "result": [ + # { + # "i": 203530723141917063, + # "t": 1678227505815, + # "p": "22038.81", + # "q": "0.000978", + # "v": "21.55395618", + # "b": True + # }, + # ] + # } + # + # swap and future + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": [ + # { + # "t": 1678227683897, + # "s": "btc_usdt", + # "p": "22031", + # "a": "1067", + # "m": "BID" + # }, + # ] + # } + # + trades = self.safe_value(response, 'result', []) + return self.parse_trades(trades, market) + + def fetch_my_trades(self, symbol: str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://doc.xt.com/#tradetradeGet + https://doc.xt.com/#futures_ordergetTrades + + :param str [symbol]: unified market symbol to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict params: extra parameters specific to the xt api endpoint + :returns dict[]: a list of `trade structures ` + """ + self.load_markets() + request = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if since is not None: + request['startTime'] = since + type = None + subType = None + response = None + type, params = self.handle_market_type_and_params('fetchMyTrades', market, params) + subType, params = self.handle_sub_type_and_params('fetchMyTrades', market, params) + if (subType is not None) or (type == 'swap') or (type == 'future'): + if limit is not None: + request['size'] = limit + if subType == 'inverse': + response = self.privateInverseGetFutureTradeV1OrderTradeList(self.extend(request, params)) + else: + response = self.privateLinearGetFutureTradeV1OrderTradeList(self.extend(request, params)) + else: + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchMyTrades', params) + marginOrSpotRequest = 'LEVER' if (marginMode is not None) else 'SPOT' + request['bizType'] = marginOrSpotRequest + if limit is not None: + request['limit'] = limit + response = self.privateSpotGetTrade(self.extend(request, params)) + # + # spot and margin + # + # { + # "rc": 0, + # "mc": "SUCCESS", + # "ma": [], + # "result": { + # "hasPrev": False, + # "hasNext": False, + # "items": [ + # { + # "symbol": "btc_usdt", + # "tradeId": "206906233569974658", + # "orderId": "206906233178463488", + # "orderSide": "SELL", + # "orderType": "MARKET", + # "bizType": "SPOT", + # "time": 1679032290215, + # "price": "25703.46", + # "quantity": "0.000099", + # "quoteQty": "2.54464254", + # "baseCurrency": "btc", + # "quoteCurrency": "usdt", + # "fee": "0.00508929", + # "feeCurrency": "usdt", + # "takerMaker": "TAKER" + # }, + # ] + # } + # } + # + # swap and future + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": { + # "page": 1, + # "ps": 10, + # "total": 2, + # "items": [ + # { + # "orderId": "207260566170987200", + # "execId": "207260566790603265", + # "symbol": "btc_usdt", + # "quantity": "13", + # "price": "27368", + # "fee": "0.02134704", + # "feeCoin": "usdt", + # "timestamp": 1679116769838, + # "takerMaker": "TAKER" + # }, + # ] + # } + # } + # + data = self.safe_value(response, 'result', {}) + trades = self.safe_value(data, 'items', []) + return self.parse_trades(trades, market, since, limit) + + def parse_trade(self, trade, market=None): + # + # spot: fetchTrades + # + # { + # "i": 203530723141917063, + # "t": 1678227505815, + # "p": "22038.81", + # "q": "0.000978", + # "v": "21.55395618", + # "b": True + # } + # + # spot: watchTrades + # + # { + # s: 'btc_usdt', + # i: '228825383103928709', + # t: 1684258222702, + # p: '27003.65', + # q: '0.000796', + # b: True + # } + # + # spot: watchMyTrades + # + # { + # "s": "btc_usdt", # symbol + # "t": 1656043204763, # time + # "i": "6316559590087251233", # tradeId + # "oi": "6216559590087220004", # orderId + # "p": "30000", # trade price + # "q": "3", # qty quantity + # "v": "90000" # volume trade amount + # } + # + # swap and future: fetchTrades + # + # { + # "t": 1678227683897, + # "s": "btc_usdt", + # "p": "22031", + # "a": "1067", + # "m": "BID" + # } + # + # spot: fetchMyTrades + # + # { + # "symbol": "btc_usdt", + # "tradeId": "206906233569974658", + # "orderId": "206906233178463488", + # "orderSide": "SELL", + # "orderType": "MARKET", + # "bizType": "SPOT", + # "time": 1679032290215, + # "price": "25703.46", + # "quantity": "0.000099", + # "quoteQty": "2.54464254", + # "baseCurrency": "btc", + # "quoteCurrency": "usdt", + # "fee": "0.00508929", + # "feeCurrency": "usdt", + # "takerMaker": "TAKER" + # } + # + # swap and future: fetchMyTrades + # + # { + # "orderId": "207260566170987200", + # "execId": "207260566790603265", + # "symbol": "btc_usdt", + # "quantity": "13", + # "price": "27368", + # "fee": "0.02134704", + # "feeCoin": "usdt", + # "timestamp": 1679116769838, + # "takerMaker": "TAKER" + # } + # + # contract watchMyTrades + # + # { + # "symbol": 'btc_usdt', + # "orderSide": 'SELL', + # "positionSide": 'LONG', + # "orderId": '231485367663419328', + # "price": '27152.7', + # "quantity": '33', + # "marginUnfrozen": '2.85318000', + # "timestamp": 1684892412565 + # } + # + # watchMyTrades(ws, swap) + # + # { + # 'fee': '0.04080840', + # 'isMaker': False, + # 'marginUnfrozen': '0.75711984', + # 'orderId': '376172779053188416', + # 'orderSide': 'BUY', + # 'positionSide': 'LONG', + # 'price': '3400.70', + # 'quantity': '2', + # 'symbol': 'eth_usdt', + # 'timestamp': 1719388579622 + # } + # + marketId = self.safe_string_2(trade, 's', 'symbol') + marketType = market['type'] if (market is not None) else None + hasSpotKeys = ('b' in trade) or ('bizType' in trade) or ('oi' in trade) + if marketType is None: + marketType = 'spot' if hasSpotKeys else 'contract' + market = self.safe_market(marketId, market, '_', marketType) + side = None + takerOrMaker = None + isBuyerMaker = self.safe_bool(trade, 'b') + if isBuyerMaker is not None: + side = 'sell' if isBuyerMaker else 'buy' + takerOrMaker = 'taker' # public trades always taker + else: + takerMaker = self.safe_string_lower(trade, 'takerMaker') + if takerMaker is not None: + takerOrMaker = takerMaker + else: + isMaker = self.safe_bool(trade, 'isMaker') + if isMaker is not None: + takerOrMaker = 'maker' if isMaker else 'taker' + orderSide = self.safe_string_lower(trade, 'orderSide') + if orderSide is not None: + side = orderSide + else: + bidOrAsk = self.safe_string(trade, 'm') + if bidOrAsk is not None: + side = 'buy' if (bidOrAsk == 'BID') else 'sell' + timestamp = self.safe_integer_n(trade, ['t', 'time', 'timestamp']) + quantity = self.safe_string_2(trade, 'q', 'quantity') + amount = None + if marketType == 'spot': + amount = quantity + else: + if quantity is None: + amount = Precise.string_mul(self.safe_string(trade, 'a'), self.number_to_string(market['contractSize'])) + else: + amount = Precise.string_mul(quantity, self.number_to_string(market['contractSize'])) + return self.safe_trade({ + 'info': trade, + 'id': self.safe_string_n(trade, ['i', 'tradeId', 'execId']), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': market['symbol'], + 'order': self.safe_string_2(trade, 'orderId', 'oi'), + 'type': self.safe_string_lower(trade, 'orderType'), + 'side': side, + 'takerOrMaker': takerOrMaker, + 'price': self.safe_string_2(trade, 'p', 'price'), + 'amount': amount, + 'cost': None, + 'fee': { + 'currency': self.safe_currency_code(self.safe_string_2(trade, 'feeCurrency', 'feeCoin')), + 'cost': self.safe_string(trade, 'fee'), + }, + }, market) + + def fetch_balance(self, params={}): + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://doc.xt.com/#balancebalancesGet + https://doc.xt.com/#futures_usergetBalances + + :param dict params: extra parameters specific to the xt api endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + type = None + subType = None + response = None + type, params = self.handle_market_type_and_params('fetchBalance', None, params) + subType, params = self.handle_sub_type_and_params('fetchBalance', None, params) + isContractWallet = ((type == 'swap') or (type == 'future')) + if subType == 'inverse': + response = self.privateInverseGetFutureUserV1BalanceList(params) + elif (subType == 'linear') or isContractWallet: + response = self.privateLinearGetFutureUserV1BalanceList(params) + else: + response = self.privateSpotGetBalances(params) + # + # spot + # + # { + # "rc": 0, + # "mc": "SUCCESS", + # "ma": [], + # "result": { + # "totalUsdtAmount": "31.75931133", + # "totalBtcAmount": "0.00115951", + # "assets": [ + # { + # "currency": "usdt", + # "currencyId": 11, + # "frozenAmount": "0.03834082", + # "availableAmount": "31.70995965", + # "totalAmount": "31.74830047", + # "convertBtcAmount": "0.00115911", + # "convertUsdtAmount": "31.74830047" + # }, + # ] + # } + # } + # + # swap and future + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": [ + # { + # "coin": "usdt", + # "walletBalance": "19.29849875", + # "openOrderMarginFrozen": "0", + # "isolatedMargin": "0.709475", + # "crossedMargin": "0", + # "availableBalance": "18.58902375", + # "bonus": "0", + # "coupon":"0" + # } + # ] + # } + # + balances = None + if (subType is not None) or isContractWallet: + balances = self.safe_value(response, 'result', []) + else: + data = self.safe_value(response, 'result', {}) + balances = self.safe_value(data, 'assets', []) + return self.parse_balance(balances) + + def parse_balance(self, response): + # + # spot + # + # { + # "currency": "usdt", + # "currencyId": 11, + # "frozenAmount": "0.03834082", + # "availableAmount": "31.70995965", + # "totalAmount": "31.74830047", + # "convertBtcAmount": "0.00115911", + # "convertUsdtAmount": "31.74830047" + # } + # + # swap and future + # + # { + # "coin": "usdt", + # "walletBalance": "19.29849875", + # "openOrderMarginFrozen": "0", + # "isolatedMargin": "0.709475", + # "crossedMargin": "0", + # "availableBalance": "18.58902375", + # "bonus": "0", + # "coupon":"0" + # } + # + result = {'info': response} + for i in range(0, len(response)): + balance = response[i] + currencyId = self.safe_string_2(balance, 'currency', 'coin') + code = self.safe_currency_code(currencyId) + account = self.account() + free = self.safe_string_2(balance, 'availableAmount', 'availableBalance') + used = self.safe_string(balance, 'frozenAmount') + total = self.safe_string_2(balance, 'totalAmount', 'walletBalance') + if used is None: + crossedAndIsolatedMargin = Precise.string_add(self.safe_string(balance, 'crossedMargin'), self.safe_string(balance, 'isolatedMargin')) + used = Precise.string_add(self.safe_string(balance, 'openOrderMarginFrozen'), crossedAndIsolatedMargin) + account['free'] = free + account['used'] = used + account['total'] = total + result[code] = account + return self.safe_balance(result) + + def create_market_buy_order_with_cost(self, symbol: str, cost: float, params={}): + """ + + https://doc.xt.com/#orderorderPost + + create a market buy order by providing the symbol and cost + :param str symbol: unified symbol of the market to create an order in + :param float cost: how much you want to trade in units of the quote currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' createMarketBuyOrderWithCost() supports spot orders only') + return self.create_order(symbol, 'market', 'buy', cost, 1, params) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://doc.xt.com/#orderorderPost + https://doc.xt.com/#futures_ordercreate + https://doc.xt.com/#futures_entrustcreatePlan + https://doc.xt.com/#futures_entrustcreateProfit + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much you want to trade in units of the base currency + :param float [price]: the price to fulfill the order, in units of the quote currency, can be ignored in market orders + :param dict params: extra parameters specific to the xt api endpoint + :param str [params.timeInForce]: 'GTC', 'IOC', 'FOK' or 'GTX' + :param str [params.entrustType]: 'TAKE_PROFIT', 'STOP', 'TAKE_PROFIT_MARKET', 'STOP_MARKET', 'TRAILING_STOP_MARKET', required if stopPrice is defined, currently isn't functioning on xt's side + :param str [params.triggerPriceType]: 'INDEX_PRICE', 'MARK_PRICE', 'LATEST_PRICE', required if stopPrice is defined + :param float [params.triggerPrice]: price to trigger a stop order + :param float [params.stopPrice]: alias for triggerPrice + :param float [params.stopLoss]: price to set a stop-loss on an open position + :param float [params.takeProfit]: price to set a take-profit on an open position + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + symbol = market['symbol'] + if market['spot']: + return self.create_spot_order(symbol, type, side, amount, price, params) + else: + return self.create_contract_order(symbol, type, side, amount, price, params) + + def create_spot_order(self, symbol: str, type, side, amount, price=None, params={}): + self.load_markets() + market = self.market(symbol) + request = { + 'symbol': market['id'], + 'side': side.upper(), + 'type': type.upper(), + } + timeInForce = None + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('createOrder', params) + marginOrSpotRequest = 'LEVER' if (marginMode is not None) else 'SPOT' + request['bizType'] = marginOrSpotRequest + if type == 'market': + timeInForce = self.safe_string_upper(params, 'timeInForce', 'FOK') + if side == 'buy': + cost = self.safe_string(params, 'cost') + params = self.omit(params, 'cost') + createMarketBuyOrderRequiresPrice = self.safe_bool(self.options, 'createMarketBuyOrderRequiresPrice', True) + if createMarketBuyOrderRequiresPrice: + if price is None and (cost is None): + raise InvalidOrder(self.id + ' createOrder() requires a price argument or cost in params for market buy orders on spot markets to calculate the total amount to spend(amount * price), alternatively set the createMarketBuyOrderRequiresPrice option to False and pass in the cost to spend into the amount parameter') + else: + amountString = self.number_to_string(amount) + priceString = self.number_to_string(price) + costCalculated: Str = None + if price is not None: + costCalculated = Precise.string_mul(amountString, priceString) + else: + costCalculated = cost + request['quoteQty'] = self.cost_to_precision(symbol, costCalculated) + else: + amountCost = cost if (cost is not None) else amount + request['quoteQty'] = self.cost_to_precision(symbol, amountCost) + else: + timeInForce = self.safe_string_upper(params, 'timeInForce', 'GTC') + request['price'] = self.price_to_precision(symbol, price) + if (side == 'sell') or (type == 'limit'): + request['quantity'] = self.amount_to_precision(symbol, amount) + request['timeInForce'] = timeInForce + response = self.privateSpotPostOrder(self.extend(request, params)) + # + # { + # "rc": 0, + # "mc": "SUCCESS", + # "ma": [], + # "result": { + # "orderId": "204371980095156544" + # } + # } + # + order = self.safe_value(response, 'result', {}) + return self.parse_order(order, market) + + def create_contract_order(self, symbol: str, type, side, amount, price=None, params={}): + self.load_markets() + market = self.market(symbol) + request = { + 'symbol': market['id'], + 'origQty': self.amount_to_precision(symbol, amount), + } + timeInForce = self.safe_string_upper(params, 'timeInForce') + if timeInForce is not None: + request['timeInForce'] = timeInForce + reduceOnly = self.safe_value(params, 'reduceOnly', False) + if side == 'buy': + requestType = 'SHORT' if (reduceOnly) else 'LONG' + request['positionSide'] = requestType + else: + requestType = 'LONG' if (reduceOnly) else 'SHORT' + request['positionSide'] = requestType + response = None + triggerPrice = self.safe_number_2(params, 'triggerPrice', 'stopPrice') + stopLoss = self.safe_number_2(params, 'stopLoss', 'triggerStopPrice') + takeProfit = self.safe_number_2(params, 'takeProfit', 'triggerProfitPrice') + isTrigger = (triggerPrice is not None) + isStopLoss = (stopLoss is not None) + isTakeProfit = (takeProfit is not None) + if price is not None: + if not (isStopLoss) and not (isTakeProfit): + request['price'] = self.price_to_precision(symbol, price) + if isTrigger: + request['timeInForce'] = self.safe_string_upper(params, 'timeInForce', 'GTC') + request['triggerPriceType'] = self.safe_string(params, 'triggerPriceType', 'LATEST_PRICE') + request['orderSide'] = side.upper() + request['stopPrice'] = self.price_to_precision(symbol, triggerPrice) + entrustType = 'STOP_MARKET' if (type == 'market') else 'STOP' + request['entrustType'] = entrustType + params = self.omit(params, 'triggerPrice') + if market['linear']: + response = self.privateLinearPostFutureTradeV1EntrustCreatePlan(self.extend(request, params)) + elif market['inverse']: + response = self.privateInversePostFutureTradeV1EntrustCreatePlan(self.extend(request, params)) + elif isStopLoss or isTakeProfit: + if isStopLoss: + request['triggerStopPrice'] = self.price_to_precision(symbol, stopLoss) + else: + request['triggerProfitPrice'] = self.price_to_precision(symbol, takeProfit) + params = self.omit(params, ['stopLoss', 'takeProfit']) + if market['linear']: + response = self.privateLinearPostFutureTradeV1EntrustCreateProfit(self.extend(request, params)) + elif market['inverse']: + response = self.privateInversePostFutureTradeV1EntrustCreateProfit(self.extend(request, params)) + else: + request['orderSide'] = side.upper() + request['orderType'] = type.upper() + if market['linear']: + response = self.privateLinearPostFutureTradeV1OrderCreate(self.extend(request, params)) + elif market['inverse']: + response = self.privateInversePostFutureTradeV1OrderCreate(self.extend(request, params)) + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": "206410760006650176" + # } + # + return self.parse_order(response, market) + + def fetch_order(self, id: str, symbol: str = None, params={}): + """ + fetches information on an order made by the user + + https://doc.xt.com/#orderorderGet + https://doc.xt.com/#futures_ordergetById + https://doc.xt.com/#futures_entrustgetPlanById + https://doc.xt.com/#futures_entrustgetProfitById + + :param str id: order id + :param str [symbol]: unified symbol of the market the order was made in + :param dict params: extra parameters specific to the xt api endpoint + :param bool [params.trigger]: if the order is a trigger order or not + :param bool [params.stopLossTakeProfit]: if the order is a stop-loss or take-profit order + :returns dict: An `order structure ` + """ + self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + request = {} + type = None + subType = None + response = None + type, params = self.handle_market_type_and_params('fetchOrder', market, params) + subType, params = self.handle_sub_type_and_params('fetchOrder', market, params) + trigger = self.safe_value(params, 'stop') + stopLossTakeProfit = self.safe_value(params, 'stopLossTakeProfit') + if trigger: + request['entrustId'] = id + elif stopLossTakeProfit: + request['profitId'] = id + else: + request['orderId'] = id + if trigger: + params = self.omit(params, 'stop') + if subType == 'inverse': + response = self.privateInverseGetFutureTradeV1EntrustPlanDetail(self.extend(request, params)) + else: + response = self.privateLinearGetFutureTradeV1EntrustPlanDetail(self.extend(request, params)) + elif stopLossTakeProfit: + params = self.omit(params, 'stopLossTakeProfit') + if subType == 'inverse': + response = self.privateInverseGetFutureTradeV1EntrustProfitDetail(self.extend(request, params)) + else: + response = self.privateLinearGetFutureTradeV1EntrustProfitDetail(self.extend(request, params)) + elif subType == 'inverse': + response = self.privateInverseGetFutureTradeV1OrderDetail(self.extend(request, params)) + elif (subType == 'linear') or (type == 'swap') or (type == 'future'): + response = self.privateLinearGetFutureTradeV1OrderDetail(self.extend(request, params)) + else: + response = self.privateSpotGetOrderOrderId(self.extend(request, params)) + # + # spot + # + # { + # "rc": 0, + # "mc": "SUCCESS", + # "ma": [], + # "result": { + # "symbol": "btc_usdt", + # "orderId": "207505997850909952", + # "clientOrderId": null, + # "baseCurrency": "btc", + # "quoteCurrency": "usdt", + # "side": "BUY", + # "type": "LIMIT", + # "timeInForce": "GTC", + # "price": "20000.00", + # "origQty": "0.001000", + # "origQuoteQty": "20.00", + # "executedQty": "0.000000", + # "leavingQty": "0.001000", + # "tradeBase": "0.000000", + # "tradeQuote": "0.00", + # "avgPrice": null, + # "fee": null, + # "feeCurrency": null, + # "closed": False, + # "state": "NEW", + # "time": 1679175285162, + # "updatedTime": 1679175285255 + # } + # } + # + # swap and future + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": { + # "orderId": "211451874783183936", + # "clientOrderId": null, + # "symbol": "btc_usdt", + # "orderType": "LIMIT", + # "orderSide": "BUY", + # "positionSide": "LONG", + # "timeInForce": "GTC", + # "closePosition": False, + # "price": "20000", + # "origQty": "10", + # "avgPrice": "0", + # "executedQty": "0", + # "marginFrozen": "1.34533334", + # "remark": null, + # "triggerProfitPrice": null, + # "triggerStopPrice": null, + # "sourceId": null, + # "sourceType": "DEFAULT", + # "forceClose": False, + # "closeProfit": null, + # "state": "NEW", + # "createdTime": 1680116055693, + # "updatedTime": 1680116055693 + # } + # } + # + # trigger + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": { + # "entrustId": "216300248132756992", + # "symbol": "btc_usdt", + # "entrustType": "STOP", + # "orderSide": "SELL", + # "positionSide": "SHORT", + # "timeInForce": "GTC", + # "closePosition": null, + # "price": "20000", + # "origQty": "1", + # "stopPrice": "19000", + # "triggerPriceType": "LATEST_PRICE", + # "state": "NOT_TRIGGERED", + # "marketOrderLevel": null, + # "createdTime": 1681271998064, + # "updatedTime": 1681271998064, + # "ordinary": False + # } + # } + # + # stop-loss and take-profit + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": { + # "profitId": "216306213226230400", + # "symbol": "btc_usdt", + # "positionSide": "LONG", + # "origQty": "1", + # "triggerPriceType": "LATEST_PRICE", + # "triggerProfitPrice": null, + # "triggerStopPrice": "20000", + # "entryPrice": null, + # "positionSize": null, + # "isolatedMargin": null, + # "executedQty": null, + # "avgPrice": null, + # "positionType": "ISOLATED", + # "state": "NOT_TRIGGERED", + # "createdTime": 1681273420039 + # } + # } + # + order = self.safe_value(response, 'result', {}) + return self.parse_order(order, market) + + def fetch_orders(self, symbol: str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches information on multiple orders made by the user + + https://doc.xt.com/#orderhistoryOrderGet + https://doc.xt.com/#futures_ordergetHistory + https://doc.xt.com/#futures_entrustgetPlanHistory + + :param str [symbol]: unified market symbol of the market the orders were made in + :param int [since]: timestamp in ms of the earliest order + :param int [limit]: the maximum number of order structures to retrieve + :param dict params: extra parameters specific to the xt api endpoint + :param bool [params.trigger]: if the order is a trigger order or not + :returns dict[]: a list of `order structures ` + """ + self.load_markets() + request = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + type = None + subType = None + response = None + type, params = self.handle_market_type_and_params('fetchOrders', market, params) + subType, params = self.handle_sub_type_and_params('fetchOrders', market, params) + trigger = self.safe_value_2(params, 'trigger', 'stop') + if trigger: + params = self.omit(params, ['trigger', 'stop']) + if subType == 'inverse': + response = self.privateInverseGetFutureTradeV1EntrustPlanListHistory(self.extend(request, params)) + else: + response = self.privateLinearGetFutureTradeV1EntrustPlanListHistory(self.extend(request, params)) + elif subType == 'inverse': + response = self.privateInverseGetFutureTradeV1OrderListHistory(self.extend(request, params)) + elif (subType == 'linear') or (type == 'swap') or (type == 'future'): + response = self.privateLinearGetFutureTradeV1OrderListHistory(self.extend(request, params)) + else: + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchOrders', params) + marginOrSpotRequest = 'LEVER' if (marginMode is not None) else 'SPOT' + request['bizType'] = marginOrSpotRequest + response = self.privateSpotGetHistoryOrder(self.extend(request, params)) + # + # spot and margin + # + # { + # "rc": 0, + # "mc": "SUCCESS", + # "ma": [], + # "result": { + # "hasPrev": False, + # "hasNext": True, + # "items": [ + # { + # "symbol": "btc_usdt", + # "orderId": "207505997850909952", + # "clientOrderId": null, + # "baseCurrency": "btc", + # "quoteCurrency": "usdt", + # "side": "BUY", + # "type": "LIMIT", + # "timeInForce": "GTC", + # "price": "20000.00", + # "origQty": "0.001000", + # "origQuoteQty": "20.00", + # "executedQty": "0.000000", + # "leavingQty": "0.000000", + # "tradeBase": "0.000000", + # "tradeQuote": "0.00", + # "avgPrice": null, + # "fee": null, + # "feeCurrency": null, + # "closed": True, + # "state": "CANCELED", + # "time": 1679175285162, + # "updatedTime": 1679175488492 + # }, + # ] + # } + # } + # + # swap and future + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": { + # "hasPrev": False, + # "hasNext": True, + # "items": [ + # { + # "orderId": "207519546930995456", + # "clientOrderId": null, + # "symbol": "btc_usdt", + # "orderType": "LIMIT", + # "orderSide": "BUY", + # "positionSide": "LONG", + # "timeInForce": "GTC", + # "closePosition": False, + # "price": "20000", + # "origQty": "100", + # "avgPrice": "0", + # "executedQty": "0", + # "marginFrozen": "4.12", + # "remark": null, + # "triggerProfitPrice": null, + # "triggerStopPrice": null, + # "sourceId": null, + # "sourceType": "DEFAULT", + # "forceClose": False, + # "closeProfit": null, + # "state": "CANCELED", + # "createdTime": 1679178515689, + # "updatedTime": 1679180096172 + # }, + # ] + # } + # } + # + # stop + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": { + # "hasPrev": False, + # "hasNext": False, + # "items": [ + # { + # "entrustId": "216300248132756992", + # "symbol": "btc_usdt", + # "entrustType": "STOP", + # "orderSide": "SELL", + # "positionSide": "SHORT", + # "timeInForce": "GTC", + # "closePosition": null, + # "price": "20000", + # "origQty": "1", + # "stopPrice": "19000", + # "triggerPriceType": "LATEST_PRICE", + # "state": "USER_REVOCATION", + # "marketOrderLevel": null, + # "createdTime": 1681271998064, + # "updatedTime": 1681273188674, + # "ordinary": False + # }, + # ] + # } + # } + # + data = self.safe_value(response, 'result', {}) + orders = self.safe_value(data, 'items', []) + return self.parse_orders(orders, market, since, limit) + + def fetch_orders_by_status(self, status, symbol: str = None, since: Int = None, limit: Int = None, params={}): + self.load_markets() + request = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + if limit is not None: + request['size'] = limit + if since is not None: + request['startTime'] = since + type = None + subType = None + response = None + type, params = self.handle_market_type_and_params('fetchOrdersByStatus', market, params) + subType, params = self.handle_sub_type_and_params('fetchOrdersByStatus', market, params) + trigger = self.safe_bool_2(params, 'stop', 'trigger') + stopLossTakeProfit = self.safe_value(params, 'stopLossTakeProfit') + if status == 'open': + if trigger or stopLossTakeProfit: + request['state'] = 'NOT_TRIGGERED' + elif type == 'swap': + request['state'] = 'UNFINISHED' # NEW & PARTIALLY_FILLED + elif status == 'closed': + if trigger or stopLossTakeProfit: + request['state'] = 'TRIGGERED' + else: + request['state'] = 'FILLED' + elif status == 'canceled': + if trigger or stopLossTakeProfit: + request['state'] = 'USER_REVOCATION' + else: + request['state'] = 'CANCELED' + else: + request['state'] = status + if trigger or stopLossTakeProfit or (subType is not None) or (type == 'swap') or (type == 'future'): + if since is not None: + request['startTime'] = since + if limit is not None: + request['size'] = limit + if trigger: + params = self.omit(params, ['stop', 'trigger']) + if subType == 'inverse': + response = self.privateInverseGetFutureTradeV1EntrustPlanList(self.extend(request, params)) + else: + response = self.privateLinearGetFutureTradeV1EntrustPlanList(self.extend(request, params)) + elif stopLossTakeProfit: + params = self.omit(params, 'stopLossTakeProfit') + if subType == 'inverse': + response = self.privateInverseGetFutureTradeV1EntrustProfitList(self.extend(request, params)) + else: + response = self.privateLinearGetFutureTradeV1EntrustProfitList(self.extend(request, params)) + elif (subType is not None) or (type == 'swap') or (type == 'future'): + if subType == 'inverse': + response = self.privateInverseGetFutureTradeV1OrderList(self.extend(request, params)) + else: + response = self.privateLinearGetFutureTradeV1OrderList(self.extend(request, params)) + else: + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchOrdersByStatus', params) + marginOrSpotRequest = 'LEVER' if (marginMode is not None) else 'SPOT' + request['bizType'] = marginOrSpotRequest + if status != 'open': + if since is not None: + request['startTime'] = since + if limit is not None: + request = self.omit(request, 'size') + request['limit'] = limit + response = self.privateSpotGetHistoryOrder(self.extend(request, params)) + else: + response = self.privateSpotGetOpenOrder(self.extend(request, params)) + # + # spot and margin + # + # { + # "rc": 0, + # "mc": "SUCCESS", + # "ma": [], + # "result": { + # "hasPrev": False, + # "hasNext": True, + # "items": [ + # { + # "symbol": "btc_usdt", + # "orderId": "207505997850909952", + # "clientOrderId": null, + # "baseCurrency": "btc", + # "quoteCurrency": "usdt", + # "side": "BUY", + # "type": "LIMIT", + # "timeInForce": "GTC", + # "price": "20000.00", + # "origQty": "0.001000", + # "origQuoteQty": "20.00", + # "executedQty": "0.000000", + # "leavingQty": "0.000000", + # "tradeBase": "0.000000", + # "tradeQuote": "0.00", + # "avgPrice": null, + # "fee": null, + # "feeCurrency": null, + # "closed": True, + # "state": "CANCELED", + # "time": 1679175285162, + # "updatedTime": 1679175488492 + # }, + # ] + # } + # } + # + # spot and margin: fetchOpenOrders + # + # { + # "rc": 0, + # "mc": "SUCCESS", + # "ma": [], + # "result": [ + # { + # "symbol": "eth_usdt", + # "orderId": "208249323222264320", + # "clientOrderId": null, + # "baseCurrency": "eth", + # "quoteCurrency": "usdt", + # "side": "BUY", + # "type": "LIMIT", + # "timeInForce": "GTC", + # "price": "1300.00", + # "origQty": "0.0032", + # "origQuoteQty": "4.16", + # "executedQty": "0.0000", + # "leavingQty": "0.0032", + # "tradeBase": "0.0000", + # "tradeQuote": "0.00", + # "avgPrice": null, + # "fee": null, + # "feeCurrency": null, + # "closed": False, + # "state": "NEW", + # "time": 1679352507741, + # "updatedTime": 1679352507869 + # }, + # ] + # } + # + # swap and future + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": { + # "page": 1, + # "ps": 10, + # "total": 25, + # "items": [ + # { + # "orderId": "207519546930995456", + # "clientOrderId": null, + # "symbol": "btc_usdt", + # "orderType": "LIMIT", + # "orderSide": "BUY", + # "positionSide": "LONG", + # "timeInForce": "GTC", + # "closePosition": False, + # "price": "20000", + # "origQty": "100", + # "avgPrice": "0", + # "executedQty": "0", + # "marginFrozen": "4.12", + # "remark": null, + # "triggerProfitPrice": null, + # "triggerStopPrice": null, + # "sourceId": null, + # "sourceType": "DEFAULT", + # "forceClose": False, + # "closeProfit": null, + # "state": "CANCELED", + # "createdTime": 1679178515689, + # "updatedTime": 1679180096172 + # }, + # ] + # } + # } + # + # stop + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": { + # "page": 1, + # "ps": 3, + # "total": 8, + # "items": [ + # { + # "entrustId": "216300248132756992", + # "symbol": "btc_usdt", + # "entrustType": "STOP", + # "orderSide": "SELL", + # "positionSide": "SHORT", + # "timeInForce": "GTC", + # "closePosition": null, + # "price": "20000", + # "origQty": "1", + # "stopPrice": "19000", + # "triggerPriceType": "LATEST_PRICE", + # "state": "USER_REVOCATION", + # "marketOrderLevel": null, + # "createdTime": 1681271998064, + # "updatedTime": 1681273188674, + # "ordinary": False + # }, + # ] + # } + # } + # + # stop-loss and take-profit + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": { + # "page": 1, + # "ps": 3, + # "total": 2, + # "items": [ + # { + # "profitId": "216306213226230400", + # "symbol": "btc_usdt", + # "positionSide": "LONG", + # "origQty": "1", + # "triggerPriceType": "LATEST_PRICE", + # "triggerProfitPrice": null, + # "triggerStopPrice": "20000", + # "entryPrice": "0", + # "positionSize": "0", + # "isolatedMargin": "0", + # "executedQty": "0", + # "avgPrice": null, + # "positionType": "ISOLATED", + # "state": "USER_REVOCATION", + # "createdTime": 1681273420039 + # }, + # ] + # } + # } + # + orders = [] + resultDict = self.safe_dict(response, 'result') + if resultDict is not None: + orders = self.safe_list(resultDict, 'items', []) + else: + orders = self.safe_list(response, 'result') + return self.parse_orders(orders, market, since, limit) + + def fetch_open_orders(self, symbol: str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all unfilled currently open orders + + https://doc.xt.com/#orderopenOrderGet + https://doc.xt.com/#futures_ordergetOrders + https://doc.xt.com/#futures_entrustgetPlan + https://doc.xt.com/#futures_entrustgetProfit + + :param str [symbol]: unified market symbol of the market the orders were made in + :param int [since]: timestamp in ms of the earliest order + :param int [limit]: the maximum number of open order structures to retrieve + :param dict params: extra parameters specific to the xt api endpoint + :param bool [params.trigger]: if the order is a trigger order or not + :param bool [params.stopLossTakeProfit]: if the order is a stop-loss or take-profit order + :returns dict[]: a list of `order structures ` + """ + return self.fetch_orders_by_status('open', symbol, since, limit, params) + + def fetch_closed_orders(self, symbol: str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches information on multiple closed orders made by the user + + https://doc.xt.com/#orderhistoryOrderGet + https://doc.xt.com/#futures_ordergetOrders + https://doc.xt.com/#futures_entrustgetPlan + https://doc.xt.com/#futures_entrustgetProfit + + :param str [symbol]: unified market symbol of the market the orders were made in + :param int [since]: timestamp in ms of the earliest order + :param int [limit]: the maximum number of order structures to retrieve + :param dict params: extra parameters specific to the xt api endpoint + :param bool [params.trigger]: if the order is a trigger order or not + :param bool [params.stopLossTakeProfit]: if the order is a stop-loss or take-profit order + :returns dict[]: a list of `order structures ` + """ + return self.fetch_orders_by_status('closed', symbol, since, limit, params) + + def fetch_canceled_orders(self, symbol: str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches information on multiple canceled orders made by the user + + https://doc.xt.com/#orderhistoryOrderGet + https://doc.xt.com/#futures_ordergetOrders + https://doc.xt.com/#futures_entrustgetPlan + https://doc.xt.com/#futures_entrustgetProfit + + :param str [symbol]: unified market symbol of the market the orders were made in + :param int [since]: timestamp in ms of the earliest order + :param int [limit]: the maximum number of order structures to retrieve + :param dict params: extra parameters specific to the xt api endpoint + :param bool [params.trigger]: if the order is a trigger order or not + :param bool [params.stopLossTakeProfit]: if the order is a stop-loss or take-profit order + :returns dict: a list of `order structures ` + """ + return self.fetch_orders_by_status('canceled', symbol, since, limit, params) + + def cancel_order(self, id: str, symbol: str = None, params={}): + """ + cancels an open order + + https://doc.xt.com/#orderorderDel + https://doc.xt.com/#futures_ordercancel + https://doc.xt.com/#futures_entrustcancelPlan + https://doc.xt.com/#futures_entrustcancelProfit + + :param str id: order id + :param str [symbol]: unified symbol of the market the order was made in + :param dict params: extra parameters specific to the xt api endpoint + :param bool [params.trigger]: if the order is a trigger order or not + :param bool [params.stopLossTakeProfit]: if the order is a stop-loss or take-profit order + :returns dict: An `order structure ` + """ + self.load_markets() + market = None + if symbol is not None: + market = self.market(symbol) + request = {} + type = None + subType = None + response = None + type, params = self.handle_market_type_and_params('cancelOrder', market, params) + subType, params = self.handle_sub_type_and_params('cancelOrder', market, params) + trigger = self.safe_value_2(params, 'trigger', 'stop') + stopLossTakeProfit = self.safe_value(params, 'stopLossTakeProfit') + if trigger: + request['entrustId'] = id + elif stopLossTakeProfit: + request['profitId'] = id + else: + request['orderId'] = id + if trigger: + params = self.omit(params, ['trigger', 'stop']) + if subType == 'inverse': + response = self.privateInversePostFutureTradeV1EntrustCancelPlan(self.extend(request, params)) + else: + response = self.privateLinearPostFutureTradeV1EntrustCancelPlan(self.extend(request, params)) + elif stopLossTakeProfit: + params = self.omit(params, 'stopLossTakeProfit') + if subType == 'inverse': + response = self.privateInversePostFutureTradeV1EntrustCancelProfitStop(self.extend(request, params)) + else: + response = self.privateLinearPostFutureTradeV1EntrustCancelProfitStop(self.extend(request, params)) + elif subType == 'inverse': + response = self.privateInversePostFutureTradeV1OrderCancel(self.extend(request, params)) + elif (subType == 'linear') or (type == 'swap') or (type == 'future'): + response = self.privateLinearPostFutureTradeV1OrderCancel(self.extend(request, params)) + else: + response = self.privateSpotDeleteOrderOrderId(self.extend(request, params)) + # + # spot + # + # { + # "rc": 0, + # "mc": "SUCCESS", + # "ma": [], + # "result": { + # "cancelId": "208322474307982720" + # } + # } + # + # swap and future + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": "208319789679471616" + # } + # + isContractResponse = ((subType is not None) or (type == 'swap') or (type == 'future')) + order = response if isContractResponse else self.safe_value(response, 'result', {}) + return self.parse_order(order, market) + + def cancel_all_orders(self, symbol: str = None, params={}): + """ + cancel all open orders in a market + + https://doc.xt.com/#orderopenOrderDel + https://doc.xt.com/#futures_ordercancelBatch + https://doc.xt.com/#futures_entrustcancelPlanBatch + https://doc.xt.com/#futures_entrustcancelProfitBatch + + :param str [symbol]: unified market symbol of the market to cancel orders in + :param dict params: extra parameters specific to the xt api endpoint + :param bool [params.trigger]: if the order is a trigger order or not + :param bool [params.stopLossTakeProfit]: if the order is a stop-loss or take-profit order + :returns dict[]: a list of `order structures ` + """ + self.load_markets() + request = {} + market = None + if symbol is not None: + market = self.market(symbol) + request['symbol'] = market['id'] + type = None + subType = None + response = None + type, params = self.handle_market_type_and_params('cancelAllOrders', market, params) + subType, params = self.handle_sub_type_and_params('cancelAllOrders', market, params) + trigger = self.safe_value_2(params, 'trigger', 'stop') + stopLossTakeProfit = self.safe_value(params, 'stopLossTakeProfit') + if trigger: + params = self.omit(params, ['trigger', 'stop']) + if subType == 'inverse': + response = self.privateInversePostFutureTradeV1EntrustCancelAllPlan(self.extend(request, params)) + else: + response = self.privateLinearPostFutureTradeV1EntrustCancelAllPlan(self.extend(request, params)) + elif stopLossTakeProfit: + params = self.omit(params, 'stopLossTakeProfit') + if subType == 'inverse': + response = self.privateInversePostFutureTradeV1EntrustCancelAllProfitStop(self.extend(request, params)) + else: + response = self.privateLinearPostFutureTradeV1EntrustCancelAllProfitStop(self.extend(request, params)) + elif subType == 'inverse': + response = self.privateInversePostFutureTradeV1OrderCancelAll(self.extend(request, params)) + elif (subType == 'linear') or (type == 'swap') or (type == 'future'): + response = self.privateLinearPostFutureTradeV1OrderCancelAll(self.extend(request, params)) + else: + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('cancelAllOrders', params) + marginOrSpotRequest = 'LEVER' if (marginMode is not None) else 'SPOT' + request['bizType'] = marginOrSpotRequest + response = self.privateSpotDeleteOpenOrder(self.extend(request, params)) + # + # spot and margin + # + # { + # "rc": 0, + # "mc": "SUCCESS", + # "ma": [], + # "result": null + # } + # + # swap and future + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": True + # } + # + return [ + self.safe_order(response), + ] + + def cancel_orders(self, ids: List[str], symbol: str = None, params={}) -> List[Order]: + """ + cancel multiple orders + + https://doc.xt.com/#orderbatchOrderDel + + :param str[] ids: order ids + :param str [symbol]: unified market symbol of the market to cancel orders in + :param dict params: extra parameters specific to the xt api endpoint + :returns dict[]: a list of `order structures ` + """ + self.load_markets() + request = { + 'orderIds': ids, + } + market = None + if symbol is not None: + market = self.market(symbol) + subType = None + subType, params = self.handle_sub_type_and_params('cancelOrders', market, params) + if subType is not None: + raise NotSupported(self.id + ' cancelOrders() does not support swap and future orders, only spot orders are accepted') + response = self.privateSpotDeleteBatchOrder(self.extend(request, params)) + # + # spot + # + # { + # "rc": 0, + # "mc": "SUCCESS", + # "ma": [], + # "result": null + # } + # + return [ + self.safe_order(response), + ] + + def parse_order(self, order, market=None): + # + # spot: createOrder + # + # { + # "orderId": "204371980095156544" + # } + # + # spot: cancelOrder + # + # { + # "cancelId": "208322474307982720" + # } + # + # swap and future: createOrder, cancelOrder, editOrder + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": "206410760006650176" + # } + # + # spot: fetchOrder, fetchOrders, fetchOpenOrders, fetchClosedOrders, fetchCanceledOrders, fetchOrdersByStatus + # + # { + # "symbol": "btc_usdt", + # "orderId": "207505997850909952", + # "clientOrderId": null, + # "baseCurrency": "btc", + # "quoteCurrency": "usdt", + # "side": "BUY", + # "type": "LIMIT", + # "timeInForce": "GTC", + # "price": "20000.00", + # "origQty": "0.001000", + # "origQuoteQty": "20.00", + # "executedQty": "0.000000", + # "leavingQty": "0.001000", + # "tradeBase": "0.000000", + # "tradeQuote": "0.00", + # "avgPrice": null, + # "fee": null, + # "feeCurrency": null, + # "closed": False, + # "state": "NEW", + # "time": 1679175285162, + # "updatedTime": 1679175285255 + # } + # + # swap and future: fetchOrder, fetchOrders, fetchOpenOrders, fetchClosedOrders, fetchCanceledOrders, fetchOrdersByStatus + # + # { + # "orderId": "207519546930995456", + # "clientOrderId": null, + # "symbol": "btc_usdt", + # "orderType": "LIMIT", + # "orderSide": "BUY", + # "positionSide": "LONG", + # "timeInForce": "GTC", + # "closePosition": False, + # "price": "20000", + # "origQty": "100", + # "avgPrice": "0", + # "executedQty": "0", + # "marginFrozen": "4.12", + # "remark": null, + # "triggerProfitPrice": null, + # "triggerStopPrice": null, + # "sourceId": null, + # "sourceType": "DEFAULT", + # "forceClose": False, + # "closeProfit": null, + # "state": "CANCELED", + # "createdTime": 1679178515689, + # "updatedTime": 1679180096172 + # } + # + # trigger: fetchOrder, fetchOrders, fetchOpenOrders, fetchClosedOrders, fetchCanceledOrders, fetchOrdersByStatus + # + # { + # "entrustId": "216300248132756992", + # "symbol": "btc_usdt", + # "entrustType": "STOP", + # "orderSide": "SELL", + # "positionSide": "SHORT", + # "timeInForce": "GTC", + # "closePosition": null, + # "price": "20000", + # "origQty": "1", + # "stopPrice": "19000", + # "triggerPriceType": "LATEST_PRICE", + # "state": "NOT_TRIGGERED", + # "marketOrderLevel": null, + # "createdTime": 1681271998064, + # "updatedTime": 1681271998064, + # "ordinary": False + # } + # + # stop-loss and take-profit: fetchOrder, fetchOpenOrders, fetchClosedOrders, fetchCanceledOrders, fetchOrdersByStatus + # + # { + # "profitId": "216306213226230400", + # "symbol": "btc_usdt", + # "positionSide": "LONG", + # "origQty": "1", + # "triggerPriceType": "LATEST_PRICE", + # "triggerProfitPrice": null, + # "triggerStopPrice": "20000", + # "entryPrice": null, + # "positionSize": null, + # "isolatedMargin": null, + # "executedQty": null, + # "avgPrice": null, + # "positionType": "ISOLATED", + # "state": "NOT_TRIGGERED", + # "createdTime": 1681273420039 + # } + # + # spot editOrder + # + # { + # "orderId": "484203027161892224", + # "modifyId": "484203544105344000", + # "clientModifyId": null + # } + # + marketId = self.safe_string(order, 'symbol') + marketType = ('result' in order) or 'contract' if ('positionSide' in order) else 'spot' + market = self.safe_market(marketId, market, None, marketType) + symbol = self.safe_symbol(marketId, market, None, marketType) + timestamp = self.safe_integer_2(order, 'time', 'createdTime') + quantity = self.safe_number(order, 'origQty') + amount = quantity if (marketType == 'spot') else Precise.string_mul(self.number_to_string(quantity), self.number_to_string(market['contractSize'])) + filledQuantity = self.safe_number(order, 'executedQty') + filled = filledQuantity if (marketType == 'spot') else Precise.string_mul(self.number_to_string(filledQuantity), self.number_to_string(market['contractSize'])) + lastUpdatedTimestamp = self.safe_integer(order, 'updatedTime') + return self.safe_order({ + 'info': order, + 'id': self.safe_string_n(order, ['orderId', 'result', 'cancelId', 'entrustId', 'profitId']), + 'clientOrderId': self.safe_string_2(order, 'clientOrderId', 'clientModifyId'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': lastUpdatedTimestamp, + 'lastUpdateTimestamp': lastUpdatedTimestamp, + 'symbol': symbol, + 'type': self.safe_string_lower_2(order, 'type', 'orderType'), + 'timeInForce': self.safe_string(order, 'timeInForce'), + 'postOnly': None, + 'side': self.safe_string_lower_2(order, 'side', 'orderSide'), + 'price': self.safe_number(order, 'price'), + 'triggerPrice': self.safe_number(order, 'stopPrice'), + 'stopLoss': self.safe_number(order, 'triggerStopPrice'), + 'takeProfit': self.safe_number(order, 'triggerProfitPrice'), + 'amount': amount, + 'filled': filled, + 'remaining': self.safe_number(order, 'leavingQty'), + 'cost': None, + 'average': self.safe_number(order, 'avgPrice'), + 'status': self.parse_order_status(self.safe_string(order, 'state')), + 'fee': { + 'currency': self.safe_currency_code(self.safe_string(order, 'feeCurrency')), + 'cost': self.safe_number(order, 'fee'), + }, + 'trades': None, + }, market) + + def parse_order_status(self, status): + statuses = { + 'NEW': 'open', + 'PARTIALLY_FILLED': 'open', + 'FILLED': 'closed', + 'CANCELED': 'canceled', + 'REJECTED': 'rejected', + 'EXPIRED': 'expired', + 'UNFINISHED': 'open', + 'NOT_TRIGGERED': 'open', + 'TRIGGERING': 'open', + 'TRIGGERED': 'closed', + 'USER_REVOCATION': 'canceled', + 'PLATFORM_REVOCATION': 'rejected', + 'HISTORY': 'expired', + } + return self.safe_string(statuses, status, status) + + def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + + https://doc.xt.com/#futures_usergetBalanceBill + + :param str [code]: unified currency code + :param int [since]: timestamp in ms of the earliest ledger entry + :param int [limit]: max number of ledger entries to return + :param dict params: extra parameters specific to the xt api endpoint + :returns dict: a `ledger structure ` + """ + self.load_markets() + request = {} + currency = None + if code is not None: + currency = self.currency(code) + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + type = None + subType = None + response = None + type, params = self.handle_market_type_and_params('fetchLedger', None, params) + subType, params = self.handle_sub_type_and_params('fetchLedger', None, params) + if subType == 'inverse': + response = self.privateInverseGetFutureUserV1BalanceBills(self.extend(request, params)) + elif (subType == 'linear') or (type == 'swap') or (type == 'future'): + response = self.privateLinearGetFutureUserV1BalanceBills(self.extend(request, params)) + else: + raise NotSupported(self.id + ' fetchLedger() does not support spot transactions, only swap and future wallet transactions are supported') + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": { + # "hasPrev": False, + # "hasNext": False, + # "items": [ + # { + # "id": "207260567109387524", + # "coin": "usdt", + # "symbol": "btc_usdt", + # "type": "FEE", + # "amount": "-0.0213", + # "side": "SUB", + # "afterAmount": null, + # "createdTime": 1679116769914 + # }, + # ] + # } + # } + # + data = self.safe_value(response, 'result', {}) + ledger = self.safe_value(data, 'items', []) + return self.parse_ledger(ledger, currency, since, limit) + + def parse_ledger_entry(self, item, currency=None) -> LedgerEntry: + # + # { + # "id": "207260567109387524", + # "coin": "usdt", + # "symbol": "btc_usdt", + # "type": "FEE", + # "amount": "-0.0213", + # "side": "SUB", + # "afterAmount": null, + # "createdTime": 1679116769914 + # } + # + side = self.safe_string(item, 'side') + direction = 'in' if (side == 'ADD') else 'out' + currencyId = self.safe_string(item, 'coin') + currency = self.safe_currency(currencyId, currency) + timestamp = self.safe_integer(item, 'createdTime') + return self.safe_ledger_entry({ + 'info': item, + 'id': self.safe_string(item, 'id'), + 'direction': direction, + 'account': None, + 'referenceId': None, + 'referenceAccount': None, + 'type': self.parse_ledger_entry_type(self.safe_string(item, 'type')), + 'currency': self.safe_currency_code(currencyId, currency), + 'amount': self.safe_number(item, 'amount'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'before': None, + 'after': self.safe_number(item, 'afterAmount'), + 'status': None, + 'fee': { + 'currency': None, + 'cost': None, + }, + }, currency) + + def parse_ledger_entry_type(self, type): + ledgerType = { + 'EXCHANGE': 'transfer', + 'CLOSE_POSITION': 'trade', + 'TAKE_OVER': 'trade', + 'MERGE': 'trade', + 'QIANG_PING_MANAGER': 'fee', + 'FUND': 'fee', + 'FEE': 'fee', + 'ADL': 'auto-deleveraging', + } + return self.safe_string(ledgerType, type, type) + + def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://doc.xt.com/#deposit_withdrawaldepositAddressGet + + :param str code: unified currency code + :param dict params: extra parameters specific to the xt api endpoint + :param str params['network']: required network id + :returns dict: an `address structure ` + """ + self.load_markets() + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + currency = self.currency(code) + networkId = self.network_code_to_id(networkCode, code) + self.check_required_argument('fetchDepositAddress', networkId, 'network') + request = { + 'currency': currency['id'], + 'chain': networkId, + } + response = self.privateSpotGetDepositAddress(self.extend(request, params)) + # + # { + # "rc": 0, + # "mc": "SUCCESS", + # "ma": [], + # "result": { + # "address": "0x7f7173cf29d3846d20ca5a3aec1120b93dbd157a", + # "memo": "" + # } + # } + # + result = self.safe_value(response, 'result', {}) + return self.parse_deposit_address(result, currency) + + def parse_deposit_address(self, depositAddress, currency=None) -> DepositAddress: + # + # { + # "address": "0x7f7173cf29d3846d20ca5a3aec1120b93dbd157a", + # "memo": "" + # } + # + address = self.safe_string(depositAddress, 'address') + self.check_address(address) + return { + 'info': depositAddress, + 'currency': self.safe_currency_code(None, currency), + 'network': None, + 'address': address, + 'tag': self.safe_string(depositAddress, 'memo'), + } + + def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all deposits made to an account + + https://doc.xt.com/#deposit_withdrawalhistoryDepositGet + + :param str [code]: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of transaction structures to retrieve + :param dict params: extra parameters specific to the xt api endpoint + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + request = {} + currency = None + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit # default 10, max 200 + response = self.privateSpotGetDepositHistory(self.extend(request, params)) + # + # { + # "rc": 0, + # "mc": "SUCCESS", + # "ma": [], + # "result": { + # "hasPrev": False, + # "hasNext": False, + # "items": [ + # { + # "id": 170368702, + # "currency": "usdt", + # "chain": "Ethereum", + # "memo": "", + # "status": "SUCCESS", + # "amount": "31.792528", + # "confirmations": 12, + # "transactionId": "0x90b8487c258b81b85e15e461b1839c49d4d8e6e9de4c1adb658cd47d4f5c5321", + # "address": "0x7f7172cf29d3846d30ca5a3aec1120b92dbd150b", + # "fromAddr": "0x7830c87c02e56aff27fa9ab1241711331fa86f58", + # "createdTime": 1678491442000 + # }, + # ] + # } + # } + # + data = self.safe_value(response, 'result', {}) + deposits = self.safe_value(data, 'items', []) + return self.parse_transactions(deposits, currency, since, limit, params) + + def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all withdrawals made from an account + + https://doc.xt.com/#deposit_withdrawalwithdrawHistory + + :param str [code]: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of transaction structures to retrieve + :param dict params: extra parameters specific to the xt api endpoint + :returns dict[]: a list of `transaction structures ` + """ + self.load_markets() + request = {} + currency = None + if code is not None: + currency = self.currency(code) + request['currency'] = currency['id'] + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit # default 10, max 200 + response = self.privateSpotGetWithdrawHistory(self.extend(request, params)) + # + # { + # "rc": 0, + # "mc": "SUCCESS", + # "ma": [], + # "result": { + # "hasPrev": False, + # "hasNext": False, + # "items": [ + # { + # "id": 950898, + # "currency": "usdt", + # "chain": "Tron", + # "address": "TGB2vxTjiqraVZBy7YHXF8V3CSMVhQKcaf", + # "memo": "", + # "status": "SUCCESS", + # "amount": "5", + # "fee": "2", + # "confirmations": 6, + # "transactionId": "c36e230b879842b1d7afd19d15ee1a866e26eaa0626e367d6f545d2932a15156", + # "createdTime": 1680049062000 + # } + # ] + # } + # } + # + data = self.safe_value(response, 'result', {}) + withdrawals = self.safe_value(data, 'items', []) + return self.parse_transactions(withdrawals, currency, since, limit, params) + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + make a withdrawal + + https://doc.xt.com/#deposit_withdrawalwithdraw + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str [tag]: + :param dict params: extra parameters specific to the xt api endpoint + :returns dict: a `transaction structure ` + """ + self.check_address(address) + self.load_markets() + currency = self.currency(code) + tag, params = self.handle_withdraw_tag_and_params(tag, params) + networkCode = None + networkCode, params = self.handle_network_code_and_params(params) + networkIdsByCodes = self.safe_value(self.options, 'networks', {}) + networkId = self.safe_string_2(networkIdsByCodes, networkCode, code, code) + request = { + 'currency': currency['id'], + 'chain': networkId, + 'amount': self.currency_to_precision(code, amount), + 'address': address, + } + if tag is not None: + request['memo'] = tag + response = self.privateSpotPostWithdraw(self.extend(request, params)) + # + # { + # "rc": 0, + # "mc": "SUCCESS", + # "ma": [], + # "result": { + # "id": 950898 + # } + # } + # + result = self.safe_value(response, 'result', {}) + return self.parse_transaction(result, currency) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # fetchDeposits + # + # { + # "id": 170368702, + # "currency": "usdt", + # "chain": "Ethereum", + # "memo": "", + # "status": "SUCCESS", + # "amount": "31.792528", + # "confirmations": 12, + # "transactionId": "0x90b8487c258b81b85e15e461b1839c49d4d8e6e9de4c1adb658cd47d4f5c5321", + # "address": "0x7f7172cf29d3846d30ca5a3aec1120b92dbd150b", + # "fromAddr": "0x7830c87c02e56aff27fa9ab1241711331fa86f58", + # "createdTime": 1678491442000 + # } + # + # fetchWithdrawals + # + # { + # "id": 950898, + # "currency": "usdt", + # "chain": "Tron", + # "address": "TGB2vxTjiqraVZBy7YHXF8V3CSMVhQKcaf", + # "memo": "", + # "status": "SUCCESS", + # "amount": "5", + # "fee": "2", + # "confirmations": 6, + # "transactionId": "c36e230b879842b1d7afd19d15ee1a866e26eaa0626e367d6f545d2932a15156", + # "createdTime": 1680049062000 + # } + # + # withdraw + # + # { + # "id": 950898 + # } + # + type = 'deposit' if ('fromAddr' in transaction) else 'withdraw' + timestamp = self.safe_integer(transaction, 'createdTime') + address = self.safe_string(transaction, 'address') + memo = self.safe_string(transaction, 'memo') + currencyCode = self.safe_currency_code(self.safe_string(transaction, 'currency'), currency) + fee = self.safe_number(transaction, 'fee') + feeCurrency = currencyCode if (fee is not None) else None + networkId = self.safe_string(transaction, 'chain') + return { + 'info': transaction, + 'id': self.safe_string(transaction, 'id'), + 'txid': self.safe_string(transaction, 'transactionId'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'updated': None, + 'addressFrom': self.safe_string(transaction, 'fromAddr'), + 'addressTo': address, + 'address': address, + 'tagFrom': None, + 'tagTo': None, + 'tag': memo, + 'type': type, + 'amount': self.safe_number(transaction, 'amount'), + 'currency': currencyCode, + 'network': self.network_id_to_code(networkId, currencyCode), + 'status': self.parse_transaction_status(self.safe_string(transaction, 'status')), + 'comment': memo, + 'fee': { + 'currency': feeCurrency, + 'cost': fee, + 'rate': None, + }, + 'internal': None, + } + + def parse_transaction_status(self, status): + statuses = { + 'SUBMIT': 'pending', + 'REVIEW': 'pending', + 'AUDITED': 'pending', + 'PENDING': 'pending', + 'CANCEL': 'canceled', + 'FAIL': 'failed', + 'SUCCESS': 'ok', + } + return self.safe_string(statuses, status, status) + + def set_leverage(self, leverage: int, symbol: str = None, params={}): + """ + set the level of leverage for a market + + https://doc.xt.com/#futures_useradjustLeverage + + :param float leverage: the rate of leverage + :param str symbol: unified market symbol + :param dict params: extra parameters specific to the xt api endpoint + :param str params['positionSide']: 'LONG' or 'SHORT' + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setLeverage() requires a symbol argument') + positionSide = self.safe_string(params, 'positionSide') + self.check_required_argument('setLeverage', positionSide, 'positionSide', ['LONG', 'SHORT']) + if (leverage < 1) or (leverage > 125): + raise BadRequest(self.id + ' setLeverage() leverage should be between 1 and 125') + self.load_markets() + market = self.market(symbol) + if not (market['contract']): + raise BadSymbol(self.id + ' setLeverage() supports contract markets only') + request = { + 'symbol': market['id'], + 'positionSide': positionSide, + 'leverage': leverage, + } + subType = None + subType, params = self.handle_sub_type_and_params('setLeverage', market, params) + response = None + if subType == 'inverse': + response = self.privateInversePostFutureUserV1PositionAdjustLeverage(self.extend(request, params)) + else: + response = self.privateLinearPostFutureUserV1PositionAdjustLeverage(self.extend(request, params)) + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": null + # } + # + return response + + def add_margin(self, symbol: str, amount: float, params={}): + """ + add margin to a position + + https://doc.xt.com/#futures_useradjustMargin + + :param str symbol: unified market symbol + :param float amount: amount of margin to add + :param dict params: extra parameters specific to the xt api endpoint + :param str params['positionSide']: 'LONG' or 'SHORT' + :returns dict: a `margin structure ` + """ + return self.modify_margin_helper(symbol, amount, 'ADD', params) + + def reduce_margin(self, symbol: str, amount: float, params={}): + """ + remove margin from a position + + https://doc.xt.com/#futures_useradjustMargin + + :param str symbol: unified market symbol + :param float amount: the amount of margin to remove + :param dict params: extra parameters specific to the xt api endpoint + :param str params['positionSide']: 'LONG' or 'SHORT' + :returns dict: a `margin structure ` + """ + return self.modify_margin_helper(symbol, amount, 'SUB', params) + + def modify_margin_helper(self, symbol: str, amount, addOrReduce, params={}) -> MarginModification: + positionSide = self.safe_string(params, 'positionSide') + self.check_required_argument('setLeverage', positionSide, 'positionSide', ['LONG', 'SHORT']) + self.load_markets() + market = self.market(symbol) + request = { + 'symbol': market['id'], + 'margin': amount, + 'type': addOrReduce, + 'positionSide': positionSide, + } + subType = None + subType, params = self.handle_sub_type_and_params('modifyMarginHelper', market, params) + response = None + if subType == 'inverse': + response = self.privateInversePostFutureUserV1PositionMargin(self.extend(request, params)) + else: + response = self.privateLinearPostFutureUserV1PositionMargin(self.extend(request, params)) + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": null + # } + # + return self.parse_margin_modification(response, market) + + def parse_margin_modification(self, data, market=None) -> MarginModification: + return { + 'info': data, + 'type': None, + 'amount': None, + 'code': None, + 'symbol': self.safe_symbol(None, market), + 'status': None, + 'marginMode': None, + 'total': None, + 'timestamp': None, + 'datetime': None, + } + + def fetch_leverage_tiers(self, symbols: List[str] = None, params={}) -> LeverageTiers: + """ + retrieve information on the maximum leverage for different trade sizes + + https://doc.xt.com/#futures_quotesgetLeverageBrackets + + :param str [symbols]: a list of unified market symbols + :param dict params: extra parameters specific to the xt api endpoint + :returns dict: a dictionary of `leverage tiers structures ` + """ + self.load_markets() + subType = None + subType, params = self.handle_sub_type_and_params('fetchLeverageTiers', None, params) + response = None + if subType == 'inverse': + response = self.publicInverseGetFutureMarketV1PublicLeverageBracketList(params) + else: + response = self.publicLinearGetFutureMarketV1PublicLeverageBracketList(params) + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": [ + # { + # "symbol": "rad_usdt", + # "leverageBrackets": [ + # { + # "symbol": "rad_usdt", + # "bracket": 1, + # "maxNominalValue": "5000", + # "maintMarginRate": "0.025", + # "startMarginRate": "0.05", + # "maxStartMarginRate": null, + # "maxLeverage": "20", + # "minLeverage": "1" + # }, + # ] + # }, + # ] + # } + # + data = self.safe_value(response, 'result', []) + symbols = self.market_symbols(symbols) + return self.parse_leverage_tiers(data, symbols, 'symbol') + + def parse_leverage_tiers(self, response, symbols=None, marketIdKey=None) -> LeverageTiers: + # + # { + # "symbol": "rad_usdt", + # "leverageBrackets": [ + # { + # "symbol": "rad_usdt", + # "bracket": 1, + # "maxNominalValue": "5000", + # "maintMarginRate": "0.025", + # "startMarginRate": "0.05", + # "maxStartMarginRate": null, + # "maxLeverage": "20", + # "minLeverage": "1" + # }, + # ] + # } + # + result = {} + for i in range(0, len(response)): + entry = response[i] + marketId = self.safe_string(entry, 'symbol') + market = self.safe_market(marketId, None, '_', 'contract') + symbol = self.safe_symbol(marketId, market) + if symbols is not None: + if self.in_array(symbol, symbols): + result[symbol] = self.parse_market_leverage_tiers(entry, market) + else: + result[symbol] = self.parse_market_leverage_tiers(response[i], market) + return result + + def fetch_market_leverage_tiers(self, symbol: str, params={}) -> List[LeverageTier]: + """ + retrieve information on the maximum leverage for different trade sizes of a single market + + https://doc.xt.com/#futures_quotesgetLeverageBracket + + :param str symbol: unified market symbol + :param dict params: extra parameters specific to the xt api endpoint + :returns dict: a `leverage tiers structure ` + """ + self.load_markets() + market = self.market(symbol) + request = { + 'symbol': market['id'], + } + subType = None + subType, params = self.handle_sub_type_and_params('fetchMarketLeverageTiers', market, params) + response = None + if subType == 'inverse': + response = self.publicInverseGetFutureMarketV1PublicLeverageBracketDetail(self.extend(request, params)) + else: + response = self.publicLinearGetFutureMarketV1PublicLeverageBracketDetail(self.extend(request, params)) + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": { + # "symbol": "btc_usdt", + # "leverageBrackets": [ + # { + # "symbol": "btc_usdt", + # "bracket": 1, + # "maxNominalValue": "500000", + # "maintMarginRate": "0.004", + # "startMarginRate": "0.008", + # "maxStartMarginRate": null, + # "maxLeverage": "125", + # "minLeverage": "1" + # }, + # ] + # } + # } + # + data = self.safe_value(response, 'result', {}) + return self.parse_market_leverage_tiers(data, market) + + def parse_market_leverage_tiers(self, info, market=None) -> List[LeverageTier]: + # + # { + # "symbol": "rad_usdt", + # "leverageBrackets": [ + # { + # "symbol": "rad_usdt", + # "bracket": 1, + # "maxNominalValue": "5000", + # "maintMarginRate": "0.025", + # "startMarginRate": "0.05", + # "maxStartMarginRate": null, + # "maxLeverage": "20", + # "minLeverage": "1" + # }, + # ] + # } + # + tiers = [] + brackets = self.safe_value(info, 'leverageBrackets', []) + for i in range(0, len(brackets)): + tier = brackets[i] + marketId = self.safe_string(info, 'symbol') + market = self.safe_market(marketId, market, '_', 'contract') + tiers.append({ + 'tier': self.safe_integer(tier, 'bracket'), + 'symbol': self.safe_symbol(marketId, market, '_', 'contract'), + 'currency': market['settle'], + 'minNotional': self.safe_number(brackets[i - 1], 'maxNominalValue', 0), + 'maxNotional': self.safe_number(tier, 'maxNominalValue'), + 'maintenanceMarginRate': self.safe_number(tier, 'maintMarginRate'), + 'maxLeverage': self.safe_number(tier, 'maxLeverage'), + 'info': tier, + }) + return tiers + + def fetch_funding_rate_history(self, symbol: str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical funding rates + + https://doc.xt.com/#futures_quotesgetFundingRateRecord + + :param str [symbol]: unified symbol of the market to fetch the funding rate history for + :param int [since]: timestamp in ms of the earliest funding rate to fetch + :param int [limit]: the maximum amount of [funding rate structures] to fetch + :param dict params: extra parameters specific to the xt api endpoint + :param bool params['paginate']: True/false whether to use the pagination helper to aumatically paginate through the results + :returns dict[]: a list of `funding rate structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchFundingRateHistory() requires a symbol argument') + self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchFundingRateHistory', 'paginate') + if paginate: + return self.fetch_paginated_call_cursor('fetchFundingRateHistory', symbol, since, limit, params, 'id', 'id', 1, 200) + market = self.market(symbol) + if not market['swap']: + raise BadSymbol(self.id + ' fetchFundingRateHistory() supports swap contracts only') + request = { + 'symbol': market['id'], + } + if limit is not None: + request['limit'] = limit + else: + request['limit'] = 200 # max + subType = None + subType, params = self.handle_sub_type_and_params('fetchFundingRateHistory', market, params) + response = None + if subType == 'inverse': + response = self.publicInverseGetFutureMarketV1PublicQFundingRateRecord(self.extend(request, params)) + else: + response = self.publicLinearGetFutureMarketV1PublicQFundingRateRecord(self.extend(request, params)) + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": { + # "hasPrev": False, + # "hasNext": True, + # "items": [ + # { + # "id": "210441653482221888", + # "symbol": "btc_usdt", + # "fundingRate": "0.000057", + # "createdTime": 1679875200000, + # "collectionInternal": 28800 + # }, + # ] + # } + # } + # + result = self.safe_value(response, 'result', {}) + items = self.safe_value(result, 'items', []) + rates = [] + for i in range(0, len(items)): + entry = items[i] + marketId = self.safe_string(entry, 'symbol') + symbolInner = self.safe_symbol(marketId, market) + timestamp = self.safe_integer(entry, 'createdTime') + rates.append({ + 'info': entry, + 'symbol': symbolInner, + 'fundingRate': self.safe_number(entry, 'fundingRate'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + sorted = self.sort_by(rates, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, market['symbol'], since, limit) + + def fetch_funding_interval(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate interval + + https://doc.xt.com/#futures_quotesgetFundingRate + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + return self.fetch_funding_rate(symbol, params) + + def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate + + https://doc.xt.com/#futures_quotesgetFundingRate + + :param str symbol: unified market symbol + :param dict params: extra parameters specific to the xt api endpoint + :returns dict: a `funding rate structure ` + """ + self.load_markets() + market = self.market(symbol) + if not market['swap']: + raise BadSymbol(self.id + ' fetchFundingRate() supports swap contracts only') + request = { + 'symbol': market['id'], + } + subType = None + subType, params = self.handle_sub_type_and_params('fetchFundingRate', market, params) + response = None + if subType == 'inverse': + response = self.publicInverseGetFutureMarketV1PublicQFundingRate(self.extend(request, params)) + else: + response = self.publicLinearGetFutureMarketV1PublicQFundingRate(self.extend(request, params)) + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": { + # "symbol": "btc_usdt", + # "fundingRate": "0.000086", + # "nextCollectionTime": 1680307200000, + # "collectionInternal": 8 + # } + # } + # + result = self.safe_value(response, 'result', {}) + return self.parse_funding_rate(result, market) + + def parse_funding_rate(self, contract, market=None) -> FundingRate: + # + # { + # "symbol": "btc_usdt", + # "fundingRate": "0.000086", + # "nextCollectionTime": 1680307200000, + # "collectionInternal": 8 + # } + # + marketId = self.safe_string(contract, 'symbol') + symbol = self.safe_symbol(marketId, market, '_', 'swap') + timestamp = self.safe_integer(contract, 'nextCollectionTime') + interval = self.safe_string(contract, 'collectionInternal') + if interval is not None: + interval = interval + 'h' + return { + 'info': contract, + 'symbol': symbol, + 'markPrice': None, + 'indexPrice': None, + 'interestRate': None, + 'estimatedSettlePrice': None, + 'timestamp': None, + 'datetime': None, + 'fundingRate': self.safe_number(contract, 'fundingRate'), + 'fundingTimestamp': timestamp, + 'fundingDatetime': self.iso8601(timestamp), + 'nextFundingRate': None, + 'nextFundingTimestamp': None, + 'nextFundingDatetime': None, + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': interval, + } + + def fetch_funding_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch the funding history + + https://doc.xt.com/#futures_usergetFunding + + :param str symbol: unified market symbol + :param int [since]: the starting timestamp in milliseconds + :param int [limit]: the number of entries to return + :param dict params: extra parameters specific to the xt api endpoint + :returns dict[]: a list of `funding history structures ` + """ + self.load_markets() + market = self.market(symbol) + if not market['swap']: + raise BadSymbol(self.id + ' fetchFundingHistory() supports swap contracts only') + request = { + 'symbol': market['id'], + } + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + subType = None + subType, params = self.handle_sub_type_and_params('fetchFundingHistory', market, params) + response = None + if subType == 'inverse': + response = self.privateInverseGetFutureUserV1BalanceFundingRateList(self.extend(request, params)) + else: + response = self.privateLinearGetFutureUserV1BalanceFundingRateList(self.extend(request, params)) + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": { + # "hasPrev": False, + # "hasNext": False, + # "items": [ + # { + # "id": "210804044057280512", + # "symbol": "btc_usdt", + # "cast": "-0.0013", + # "coin": "usdt", + # "positionSide": "SHORT", + # "createdTime": 1679961600653 + # }, + # ] + # } + # } + # + data = self.safe_value(response, 'result', {}) + items = self.safe_value(data, 'items', []) + result = [] + for i in range(0, len(items)): + entry = items[i] + result.append(self.parse_funding_history(entry, market)) + sorted = self.sort_by(result, 'timestamp') + return self.filter_by_since_limit(sorted, since, limit) + + def parse_funding_history(self, contract, market=None): + # + # { + # "id": "210804044057280512", + # "symbol": "btc_usdt", + # "cast": "-0.0013", + # "coin": "usdt", + # "positionSide": "SHORT", + # "createdTime": 1679961600653 + # } + # + marketId = self.safe_string(contract, 'symbol') + symbol = self.safe_symbol(marketId, market, '_', 'swap') + currencyId = self.safe_string(contract, 'coin') + code = self.safe_currency_code(currencyId) + timestamp = self.safe_integer(contract, 'createdTime') + return { + 'info': contract, + 'symbol': symbol, + 'code': code, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': self.safe_string(contract, 'id'), + 'amount': self.safe_number(contract, 'cast'), + } + + def fetch_position(self, symbol: str, params={}): + """ + fetch data on a single open contract trade position + + https://doc.xt.com/#futures_usergetPosition + + :param str symbol: unified market symbol of the market the position is held in + :param dict params: extra parameters specific to the xt api endpoint + :returns dict: a `position structure ` + """ + self.load_markets() + market = self.market(symbol) + request = { + 'symbol': market['id'], + } + subType = None + subType, params = self.handle_sub_type_and_params('fetchPosition', market, params) + response = None + if subType == 'inverse': + response = self.privateInverseGetFutureUserV1PositionList(self.extend(request, params)) + else: + response = self.privateLinearGetFutureUserV1PositionList(self.extend(request, params)) + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": [ + # { + # "symbol": "btc_usdt", + # "positionType": "ISOLATED", + # "positionSide": "SHORT", + # "contractType": "PERPETUAL", + # "positionSize": "10", + # "closeOrderSize": "0", + # "availableCloseSize": "10", + # "entryPrice": "27060", + # "openOrderSize": "0", + # "isolatedMargin": "1.0824", + # "openOrderMarginFrozen": "0", + # "realizedProfit": "-0.00130138", + # "autoMargin": False, + # "leverage": 25 + # }, + # ] + # } + # + positions = self.safe_value(response, 'result', []) + for i in range(0, len(positions)): + entry = positions[i] + marketId = self.safe_string(entry, 'symbol') + marketInner = self.safe_market(marketId, None, None, 'contract') + positionSize = self.safe_string(entry, 'positionSize') + if positionSize != '0': + return self.parse_position(entry, marketInner) + return None + + def fetch_positions(self, symbols: List[str] = None, params={}) -> List[Position]: + """ + fetch all open positions + + https://doc.xt.com/#futures_usergetPosition + + :param str [symbols]: list of unified market symbols, not supported with xt + :param dict params: extra parameters specific to the xt api endpoint + :returns dict[]: a list of `position structure ` + """ + self.load_markets() + subType = None + subType, params = self.handle_sub_type_and_params('fetchPositions', None, params) + response = None + if subType == 'inverse': + response = self.privateInverseGetFutureUserV1PositionList(params) + else: + response = self.privateLinearGetFutureUserV1PositionList(params) + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": [ + # { + # "symbol": "btc_usdt", + # "positionType": "ISOLATED", + # "positionSide": "SHORT", + # "contractType": "PERPETUAL", + # "positionSize": "10", + # "closeOrderSize": "0", + # "availableCloseSize": "10", + # "entryPrice": "27060", + # "openOrderSize": "0", + # "isolatedMargin": "1.0824", + # "openOrderMarginFrozen": "0", + # "realizedProfit": "-0.00130138", + # "autoMargin": False, + # "leverage": 25 + # }, + # ] + # } + # + positions = self.safe_value(response, 'result', []) + result = [] + for i in range(0, len(positions)): + entry = positions[i] + marketId = self.safe_string(entry, 'symbol') + marketInner = self.safe_market(marketId, None, None, 'contract') + result.append(self.parse_position(entry, marketInner)) + return self.filter_by_array_positions(result, 'symbol', symbols, False) + + def parse_position(self, position, market=None): + # + # { + # "symbol": "btc_usdt", + # "positionType": "ISOLATED", + # "positionSide": "SHORT", + # "contractType": "PERPETUAL", + # "positionSize": "10", + # "closeOrderSize": "0", + # "availableCloseSize": "10", + # "entryPrice": "27060", + # "openOrderSize": "0", + # "isolatedMargin": "1.0824", + # "openOrderMarginFrozen": "0", + # "realizedProfit": "-0.00130138", + # "autoMargin": False, + # "leverage": 25 + # } + # + marketId = self.safe_string(position, 'symbol') + market = self.safe_market(marketId, market, None, 'contract') + symbol = self.safe_symbol(marketId, market, None, 'contract') + positionType = self.safe_string(position, 'positionType') + marginMode = 'cross' if (positionType == 'CROSSED') else 'isolated' + collateral = self.safe_number(position, 'isolatedMargin') + return self.safe_position({ + 'info': position, + 'id': None, + 'symbol': symbol, + 'timestamp': None, + 'datetime': None, + 'hedged': None, + 'side': self.safe_string_lower(position, 'positionSide'), + 'contracts': self.safe_number(position, 'positionSize'), + 'contractSize': market['contractSize'], + 'entryPrice': self.safe_number(position, 'entryPrice'), + 'markPrice': None, + 'notional': None, + 'leverage': self.safe_integer(position, 'leverage'), + 'collateral': collateral, + 'initialMargin': collateral, + 'maintenanceMargin': None, + 'initialMarginPercentage': None, + 'maintenanceMarginPercentage': None, + 'unrealizedPnl': None, + 'liquidationPrice': None, + 'marginMode': marginMode, + 'percentage': None, + 'marginRatio': None, + }) + + def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://doc.xt.com/#transfersubTransferPost + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account to transfer from - spot, swap, leverage, finance + :param str toAccount: account to transfer to - spot, swap, leverage, finance + :param dict params: extra parameters specific to the whitebit api endpoint + :returns dict: a `transfer structure ` + """ + self.load_markets() + currency = self.currency(code) + accountsByType = self.safe_value(self.options, 'accountsById') + fromAccountId = self.safe_string(accountsByType, fromAccount, fromAccount) + toAccountId = self.safe_string(accountsByType, toAccount, toAccount) + amountString = self.currency_to_precision(code, amount) + request = { + 'bizId': self.uuid(), + 'currency': currency['id'], + 'amount': amountString, + 'from': fromAccountId, + 'to': toAccountId, + } + response = self.privateSpotPostBalanceTransfer(self.extend(request, params)) + # + # { + # info: {rc: '0', mc: 'SUCCESS', ma: [], result: '226971333791398656'}, + # id: '226971333791398656', + # timestamp: None, + # datetime: None, + # currency: None, + # amount: None, + # fromAccount: None, + # toAccount: None, + # status: None + # } + # + return self.parse_transfer(response, currency) + + def parse_transfer(self, transfer, currency=None): + return { + 'info': transfer, + 'id': self.safe_string(transfer, 'result'), + 'timestamp': None, + 'datetime': None, + 'currency': None, + 'amount': None, + 'fromAccount': None, + 'toAccount': None, + 'status': None, + } + + def set_margin_mode(self, marginMode: str, symbol: Str = None, params={}): + """ + set margin mode to 'cross' or 'isolated' + + https://doc.xt.com/#futures_userchangePositionType + + :param str marginMode: 'cross' or 'isolated' + :param str [symbol]: required + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.positionSide]: *required* "long" or "short" + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setMarginMode() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + if market['spot']: + raise BadSymbol(self.id + ' setMarginMode() supports contract markets only') + marginMode = marginMode.lower() + if marginMode != 'isolated' and marginMode != 'cross': + raise BadRequest(self.id + ' setMarginMode() marginMode argument should be isolated or cross') + if marginMode == 'cross': + marginMode = 'CROSSED' + else: + marginMode = 'ISOLATED' + posSide = self.safe_string_upper(params, 'positionSide') + if posSide is None: + raise ArgumentsRequired(self.id + ' setMarginMode() requires a positionSide parameter, either "LONG" or "SHORT"') + request: dict = { + 'positionType': marginMode, + 'positionSide': posSide, + 'symbol': market['id'], + } + response = self.privateLinearPostFutureUserV1PositionChangeType(self.extend(request, params)) + # + # { + # "error": { + # "code": "", + # "msg": "" + # }, + # "msgInfo": "", + # "result": {}, + # "returnCode": 0 + # } + # + return response # unify return type + + def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}) -> Order: + """ + cancels an order and places a new order + + https://doc.xt.com/#orderorderUpdate + https://doc.xt.com/#futures_orderupdate + https://doc.xt.com/#futures_entrustupdateProfit + + :param str id: order id + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of the currency you want to trade in units of the base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :param float [params.stopLoss]: price to set a stop-loss on an open position + :param float [params.takeProfit]: price to set a take-profit on an open position + :returns dict: an `order structure ` + """ + if amount is None: + raise ArgumentsRequired(self.id + ' editOrder() requires an amount argument') + self.load_markets() + market = self.market(symbol) + request = {} + stopLoss = self.safe_number_2(params, 'stopLoss', 'triggerStopPrice') + takeProfit = self.safe_number_2(params, 'takeProfit', 'triggerProfitPrice') + params = self.omit(params, ['stopLoss', 'takeProfit']) + isStopLoss = (stopLoss is not None) + isTakeProfit = (takeProfit is not None) + if isStopLoss or isTakeProfit: + request['profitId'] = id + else: + request['orderId'] = id + request['price'] = self.price_to_precision(symbol, price) + response = None + if market['swap']: + if isStopLoss: + request['triggerStopPrice'] = self.price_to_precision(symbol, stopLoss) + elif takeProfit is not None: + request['triggerProfitPrice'] = self.price_to_precision(symbol, takeProfit) + else: + request['origQty'] = self.amount_to_precision(symbol, amount) + subType = None + subType, params = self.handle_sub_type_and_params('editOrder', market, params) + if subType == 'inverse': + if isStopLoss or isTakeProfit: + response = self.privateInversePostFutureTradeV1EntrustUpdateProfitStop(self.extend(request, params)) + else: + response = self.privateInversePostFutureTradeV1OrderUpdate(self.extend(request, params)) + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": "483869474947826752" + # } + # + else: + if isStopLoss or isTakeProfit: + response = self.privateLinearPostFutureTradeV1EntrustUpdateProfitStop(self.extend(request, params)) + else: + response = self.privateLinearPostFutureTradeV1OrderUpdate(self.extend(request, params)) + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": "483869474947826752" + # } + # + else: + request['quantity'] = self.amount_to_precision(symbol, amount) + response = self.privateSpotPutOrderOrderId(self.extend(request, params)) + # + # { + # "rc": 0, + # "mc": "SUCCESS", + # "ma": [], + # "result": { + # "orderId": "484203027161892224", + # "modifyId": "484203544105344000", + # "clientModifyId": null + # } + # } + # + result = response if (market['swap']) else self.safe_dict(response, 'result', {}) + return self.parse_order(result, market) + + def handle_errors(self, code, reason, url, method, headers, body, response, requestHeaders, requestBody): + # + # spot: error + # + # { + # "rc": 1, + # "mc": "AUTH_103", + # "ma": [], + # "result": null + # } + # + # spot: success + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": [] + # } + # + # swap and future: error + # + # { + # "returnCode": 1, + # "msgInfo": "failure", + # "error": { + # "code": "403", + # "msg": "invalid signature" + # }, + # "result": null + # } + # + # swap and future: success + # + # { + # "returnCode": 0, + # "msgInfo": "success", + # "error": null, + # "result": null + # } + # + # other: + # + # { + # "rc": 0, + # "mc": "SUCCESS", + # "ma": [], + # "result": {} + # } + # + # {"returnCode":1,"msgInfo":"failure","error":{"code":"insufficient_balance","msg":"insufficient balance","args":[]},"result":null} + # + # + status = self.safe_string_upper_2(response, 'msgInfo', 'mc') + if status is not None and status != 'SUCCESS': + feedback = self.id + ' ' + body + error = self.safe_value(response, 'error', {}) + spotErrorCode = self.safe_string(response, 'mc') + errorCode = self.safe_string(error, 'code', spotErrorCode) + spotMessage = self.safe_string(response, 'msgInfo') + message = self.safe_string(error, 'msg', spotMessage) + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + raise ExchangeError(feedback) + return None + + def sign(self, path, api=[], method='GET', params={}, headers=None, body=None): + signed = api[0] == 'private' + endpoint = api[1] + request = '/' + self.implode_params(path, params) + payload = None + if (endpoint == 'spot') or (endpoint == 'user'): + if signed: + payload = '/' + self.version + request + else: + payload = '/' + self.version + '/public' + request + else: + payload = request + url = self.urls['api'][endpoint] + payload + query = self.omit(params, self.extract_params(path)) + urlencoded = self.urlencode(self.keysort(query)) + headers = { + 'Content-Type': 'application/json', + } + if signed: + self.check_required_credentials() + defaultRecvWindow = self.safe_string(self.options, 'recvWindow') + recvWindow = self.safe_string(query, 'recvWindow', defaultRecvWindow) + timestamp = self.number_to_string(self.nonce()) + body = query + if (payload == '/v4/order') or (payload == '/future/trade/v1/order/create') or (payload == '/future/trade/v1/entrust/create-plan') or (payload == '/future/trade/v1/entrust/create-profit') or (payload == '/future/trade/v1/order/create-batch'): + id = 'CCXT' + if payload.find('future') > -1: + body['clientMedia'] = id + else: + body['media'] = id + isUndefinedBody = ((method == 'GET') or (path == 'order/{orderId}') or (path == 'ws-token')) + if (method == 'PUT') and (endpoint == 'spot'): + isUndefinedBody = False + body = None if isUndefinedBody else self.json(body) + payloadString = None + if (endpoint == 'spot') or (endpoint == 'user'): + payloadString = 'xt-validate-algorithms=HmacSHA256&xt-validate-appkey=' + self.apiKey + '&xt-validate-recvwindow=' + recvWindow + '&xt-validate-t' + 'imestamp=' + timestamp + if isUndefinedBody: + if urlencoded: + url += '?' + urlencoded + payloadString += '#' + method + '#' + payload + '#' + self.rawencode(self.keysort(query)) + else: + payloadString += '#' + method + '#' + payload + else: + payloadString += '#' + method + '#' + payload + '#' + body + headers['xt-validate-algorithms'] = 'HmacSHA256' + headers['xt-validate-recvwindow'] = recvWindow + else: + payloadString = 'xt-validate-appkey=' + self.apiKey + '&xt-validate-t' + 'imestamp=' + timestamp # we can't glue timestamp, breaks in php + if method == 'GET': + if urlencoded: + url += '?' + urlencoded + payloadString += '#' + payload + '#' + urlencoded + else: + payloadString += '#' + payload + else: + payloadString += '#' + payload + '#' + body + signature = self.hmac(self.encode(payloadString), self.encode(self.secret), hashlib.sha256) + headers['xt-validate-appkey'] = self.apiKey + headers['xt-validate-timestamp'] = timestamp + headers['xt-validate-signature'] = signature + else: + if urlencoded: + url += '?' + urlencoded + return {'url': url, 'method': method, 'body': body, 'headers': headers} diff --git a/ccxt/yobit.py b/ccxt/yobit.py new file mode 100644 index 0000000..52554ad --- /dev/null +++ b/ccxt/yobit.py @@ -0,0 +1,1417 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.yobit import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, DepositAddress, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, OrderBooks, Trade, TradingFees, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import ArgumentsRequired +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import DDoSProtection +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import ExchangeNotAvailable +from ccxt.base.errors import InvalidNonce +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class yobit(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(yobit, self).describe(), { + 'id': 'yobit', + 'name': 'YoBit', + 'countries': ['RU'], + 'rateLimit': 2000, # responses are cached every 2 seconds + 'version': '3', + 'pro': False, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelOrder': True, + 'closeAllPositions': False, + 'closePosition': False, + 'createDepositAddress': True, + 'createMarketOrder': False, + 'createOrder': True, + 'createReduceOnlyOrder': False, + 'createStopLimitOrder': False, + 'createStopMarketOrder': False, + 'createStopOrder': False, + 'fetchAllGreeks': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': False, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': False, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrice': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrderBooks': True, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': True, + 'fetchTransactions': False, + 'fetchTransfer': False, + 'fetchTransfers': False, + 'fetchUnderlyingAssets': False, + 'fetchVolatilityHistory': False, + 'fetchWithdrawals': False, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'repayMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'transfer': False, + 'withdraw': True, + 'ws': False, + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/27766910-cdcbfdae-5eea-11e7-9859-03fea873272d.jpg', + 'api': { + 'public': 'https://yobit.net/api', + 'private': 'https://yobit.net/tapi', + }, + 'www': 'https://www.yobit.net', + 'doc': 'https://www.yobit.net/en/api/', + 'fees': 'https://www.yobit.net/en/fees/', + }, + 'api': { + 'public': { + 'get': { + 'depth/{pair}': 1, + 'info': 1, + 'ticker/{pair}': 1, + 'trades/{pair}': 1, + }, + }, + 'private': { + 'post': { + 'ActiveOrders': 1, + 'CancelOrder': 1, + 'GetDepositAddress': 1, + 'getInfo': 1, + 'OrderInfo': 1, + 'Trade': 1, + 'TradeHistory': 1, + 'WithdrawCoinsToAddress': 1, + }, + }, + }, + 'fees': { + 'trading': { + 'maker': 0.002, + 'taker': 0.002, + }, + 'funding': { + 'withdraw': {}, + }, + }, + 'commonCurrencies': { + 'AIR': 'AirCoin', + 'ANI': 'ANICoin', + 'ANT': 'AntsCoin', # what is self, a coin for ants? + 'ATMCHA': 'ATM', + 'ASN': 'Ascension', + 'AST': 'Astral', + 'ATM': 'Autumncoin', + 'AUR': 'AuroraCoin', + 'BAB': 'Babel', + 'BAN': 'BANcoin', + 'BCC': 'BCH', + 'BCS': 'BitcoinStake', + 'BITS': 'Bitstar', + 'BLN': 'Bulleon', + 'BNS': 'Benefit Bonus Coin', + 'BOT': 'BOTcoin', + 'BON': 'BONES', + 'BPC': 'BitcoinPremium', + 'BST': 'BitStone', + 'BTS': 'Bitshares2', + 'CAT': 'BitClave', + 'CBC': 'CryptoBossCoin', + 'CMT': 'CometCoin', + 'COIN': 'Coin.com', + 'COV': 'Coven Coin', + 'COVX': 'COV', + 'CPC': 'Capricoin', + 'CREDIT': 'Creditbit', + 'CS': 'CryptoSpots', + 'DCT': 'Discount', + 'DFT': 'DraftCoin', + 'DGD': 'DarkGoldCoin', + 'DIRT': 'DIRTY', + 'DROP': 'FaucetCoin', + 'DSH': 'DASH', + 'EGC': 'EverGreenCoin', + 'EGG': 'EggCoin', + 'EKO': 'EkoCoin', + 'ENTER': 'ENTRC', + 'EPC': 'ExperienceCoin', + 'ESC': 'EdwardSnowden', + 'EUROPE': 'EUROP', + 'EXT': 'LifeExtension', + 'FUND': 'FUNDChains', + 'FUNK': 'FUNKCoin', + 'FX': 'FCoin', + 'GCC': 'GlobalCryptocurrency', + 'GEN': 'Genstake', + 'GENE': 'Genesiscoin', + 'GMR': 'Gimmer', + 'GOLD': 'GoldMint', + 'GOT': 'Giotto Coin', + 'GSX': 'GlowShares', + 'GT': 'GTcoin', + 'HTML5': 'HTML', + 'HYPERX': 'HYPER', + 'ICN': 'iCoin', + 'INSANE': 'INSN', + 'JNT': 'JointCoin', + 'JPC': 'JupiterCoin', + 'JWL': 'Jewels', + 'KNC': 'KingN Coin', + 'LBTCX': 'LiteBitcoin', + 'LIZI': 'LiZi', + 'LOC': 'LocoCoin', + 'LOCX': 'LOC', + 'LUNYR': 'LUN', + 'LUN': 'LunarCoin', # they just change the ticker if it is already taken + 'LUNA': 'Luna Coin', + 'MASK': 'Yobit MASK', + 'MDT': 'Midnight', + 'MEME': 'Memez Token', # conflict with Meme Inu / Degenerator Meme + 'MIS': 'MIScoin', + 'MM': 'MasterMint', # conflict with MilliMeter + 'NAV': 'NavajoCoin', + 'NBT': 'NiceBytes', + 'OMG': 'OMGame', + 'ONX': 'Onix', + 'PAC': '$PAC', + 'PLAY': 'PlayCoin', + 'PIVX': 'Darknet', + 'PURE': 'PurePOS', + 'PUTIN': 'PutinCoin', + 'SPACE': 'Spacecoin', + 'STK': 'StakeCoin', + 'SUB': 'Subscriptio', + 'PAY': 'EPAY', + 'PLC': 'Platin Coin', + 'RAI': 'RaiderCoin', + 'RCN': 'RCoin', + 'REP': 'Republicoin', + 'RUR': 'RUB', + 'SBTC': 'Super Bitcoin', + 'SMC': 'SmartCoin', + 'SOLO': 'SoloCoin', + 'SOUL': 'SoulCoin', + 'STAR': 'StarCoin', + 'SUPER': 'SuperCoin', + 'TNS': 'Transcodium', + 'TTC': 'TittieCoin', + 'UNI': 'Universe', + 'UST': 'Uservice', + 'VOL': 'VolumeCoin', + 'XIN': 'XINCoin', + 'XMT': 'SummitCoin', + 'XRA': 'Ratecoin', + 'BCHN': 'BSV', + }, + 'options': { + 'maxUrlLength': 2048, + 'fetchOrdersRequiresSymbol': True, + 'networks': { + 'ETH': 'ERC20', + 'TRX': 'TRC20', + 'BSC': 'BEP20', + }, + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'exact': { + '803': InvalidOrder, # "Count could not be less than 0.001."(selling below minAmount) + '804': InvalidOrder, # "Count could not be more than 10000."(buying above maxAmount) + '805': InvalidOrder, # "price could not be less than X."(minPrice violation on buy & sell) + '806': InvalidOrder, # "price could not be more than X."(maxPrice violation on buy & sell) + '807': InvalidOrder, # "cost could not be less than X."(minCost violation on buy & sell) + '831': InsufficientFunds, # "Not enougth X to create buy order."(buying with balance.quote < order.cost) + '832': InsufficientFunds, # "Not enougth X to create sell order."(selling with balance.base < order.amount) + '833': OrderNotFound, # "Order with id X was not found."(cancelling non-existent, closed and cancelled order) + }, + 'broad': { + 'Invalid pair name': ExchangeError, # {"success":0,"error":"Invalid pair name: btc_eth"} + 'invalid api key': AuthenticationError, + 'invalid sign': AuthenticationError, + 'api key dont have trade permission': AuthenticationError, + 'invalid parameter': InvalidOrder, + 'invalid order': InvalidOrder, + 'The given order has already been cancelled': InvalidOrder, + 'Requests too often': DDoSProtection, + 'not available': ExchangeNotAvailable, + 'data unavailable': ExchangeNotAvailable, + 'external service unavailable': ExchangeNotAvailable, + 'Total transaction amount': InvalidOrder, # {"success": 0, "error": "Total transaction amount is less than minimal total: 0.00010000"} + 'The given order has already been closed and cannot be cancelled': InvalidOrder, + 'Insufficient funds': InsufficientFunds, + 'invalid key': AuthenticationError, + 'invalid nonce': InvalidNonce, # {"success":0,"error":"invalid nonce(has already been used)"}' + 'Total order amount is less than minimal amount': InvalidOrder, + 'Rate Limited': RateLimitExceeded, + }, + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': False, + 'triggerDirection': False, + 'triggerPriceType': None, + 'stopLossPrice': False, + 'takeProfitPrice': False, + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': False, + 'FOK': False, + 'PO': False, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': False, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': 1000, + 'daysBack': 100000, # todo + 'untilDays': 100000, # todo + 'symbolRequired': True, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': True, + }, + 'fetchOrders': None, + 'fetchClosedOrders': None, + 'fetchOHLCV': None, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'orders': {}, # orders cache / emulation + }) + + def parse_balance(self, response) -> Balances: + balances = self.safe_dict(response, 'return', {}) + timestamp = self.safe_integer(balances, 'server_time') + result: dict = { + 'info': response, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + free = self.safe_dict(balances, 'funds', {}) + total = self.safe_dict(balances, 'funds_incl_orders', {}) + currencyIds = list(self.extend(free, total).keys()) + for i in range(0, len(currencyIds)): + currencyId = currencyIds[i] + code = self.safe_currency_code(currencyId) + account = self.account() + account['free'] = self.safe_string(free, currencyId) + account['total'] = self.safe_string(total, currencyId) + result[code] = account + return self.safe_balance(result) + + def fetch_balance(self, params={}) -> Balances: + """ + + https://yobit.net/en/api + + query for balance and get the amount of funds available for trading or funds locked in orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + response = self.privatePostGetInfo(params) + # + # { + # "success":1, + # "return":{ + # "funds":{ + # "ltc":22, + # "nvc":423.998, + # "ppc":10, + # }, + # "funds_incl_orders":{ + # "ltc":32, + # "nvc":523.998, + # "ppc":20, + # }, + # "rights":{ + # "info":1, + # "trade":0, + # "withdraw":0 + # }, + # "transaction_count":0, + # "open_orders":1, + # "server_time":1418654530 + # } + # } + # + return self.parse_balance(response) + + def fetch_markets(self, params={}) -> List[Market]: + """ + + https://yobit.net/en/api + + retrieves data on all markets for yobit + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = self.publicGetInfo(params) + # + # { + # "server_time":1615856752, + # "pairs":{ + # "ltc_btc":{ + # "decimal_places":8, + # "min_price":0.00000001, + # "max_price":10000, + # "min_amount":0.0001, + # "min_total":0.0001, + # "hidden":0, + # "fee":0.2, + # "fee_buyer":0.2, + # "fee_seller":0.2 + # }, + # }, + # } + # + markets = self.safe_dict(response, 'pairs', {}) + keys = list(markets.keys()) + result = [] + for i in range(0, len(keys)): + id = keys[i] + market = markets[id] + baseId, quoteId = id.split('_') + base = baseId.upper() + quote = quoteId.upper() + base = self.safe_currency_code(base) + quote = self.safe_currency_code(quote) + hidden = self.safe_integer(market, 'hidden') + feeString = self.safe_string(market, 'fee') + feeString = Precise.string_div(feeString, '100') + # yobit maker = taker + result.append({ + 'id': id, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': (hidden == 0), + 'contract': False, + 'linear': None, + 'inverse': None, + 'taker': self.parse_number(feeString), + 'maker': self.parse_number(feeString), + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.parse_number(self.parse_precision(self.safe_string(market, 'decimal_places'))), + 'price': self.parse_number(self.parse_precision(self.safe_string(market, 'decimal_places'))), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(market, 'min_amount'), + 'max': self.safe_number(market, 'max_amount'), + }, + 'price': { + 'min': self.safe_number(market, 'min_price'), + 'max': self.safe_number(market, 'max_price'), + }, + 'cost': { + 'min': self.safe_number(market, 'min_total'), + 'max': None, + }, + }, + 'created': None, + 'info': market, + }) + return result + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + + https://yobit.net/en/api + + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + if limit is not None: + request['limit'] = limit # default = 150, max = 2000 + response = self.publicGetDepthPair(self.extend(request, params)) + market_id_in_reponse = (market['id'] in response) + if not market_id_in_reponse: + raise ExchangeError(self.id + ' ' + market['symbol'] + ' order book is empty or not available') + orderbook = response[market['id']] + return self.parse_order_book(orderbook, symbol) + + def fetch_order_books(self, symbols: Strings = None, limit: Int = None, params={}) -> OrderBooks: + """ + + https://yobit.net/en/api + + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data for multiple markets + :param str[]|None symbols: list of unified market symbols, all symbols fetched if None, default is None + :param int [limit]: max number of entries per orderbook to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `order book structures ` indexed by market symbol + """ + self.load_markets() + ids = None + if symbols is None: + ids = '-'.join(self.ids) + # max URL length is 2083 symbols, including http schema, hostname, tld, etc... + if len(ids) > 2048: + numIds = len(self.ids) + raise ExchangeError(self.id + ' fetchOrderBooks() has ' + str(numIds) + ' symbols exceeding max URL length, you are required to specify a list of symbols in the first argument to fetchOrderBooks') + else: + ids = self.market_ids(symbols) + ids = '-'.join(ids) + request: dict = { + 'pair': ids, + # 'ignore_invalid': True, + } + if limit is not None: + request['limit'] = limit + response = self.publicGetDepthPair(self.extend(request, params)) + result: dict = {} + ids = list(response.keys()) + for i in range(0, len(ids)): + id = ids[i] + symbol = self.safe_symbol(id) + result[symbol] = self.parse_order_book(response[id], symbol) + return result + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "high": 0.03497582, + # "low": 0.03248474, + # "avg": 0.03373028, + # "vol": 120.11485715062999, + # "vol_cur": 3572.24914074, + # "last": 0.0337611, + # "buy": 0.0337442, + # "sell": 0.03377798, + # "updated": 1537522009 + # } + # + timestamp = self.safe_timestamp(ticker, 'updated') + last = self.safe_string(ticker, 'last') + return self.safe_ticker({ + 'symbol': self.safe_symbol(None, market), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': self.safe_string(ticker, 'buy'), + 'bidVolume': None, + 'ask': self.safe_string(ticker, 'sell'), + 'askVolume': None, + 'vwap': None, + 'open': None, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': self.safe_string(ticker, 'avg'), + 'baseVolume': self.safe_string(ticker, 'vol_cur'), + 'quoteVolume': self.safe_string(ticker, 'vol'), + 'info': ticker, + }, market) + + def fetch_tickers_helper(self, idsString: str, params={}) -> Tickers: + request: dict = { + 'pair': idsString, + } + tickers = self.publicGetTickerPair(self.extend(request, params)) + result: dict = {} + keys = list(tickers.keys()) + for k in range(0, len(keys)): + id = keys[k] + ticker = tickers[id] + market = self.safe_market(id) + symbol = market['symbol'] + result[symbol] = self.parse_ticker(ticker, market) + return result + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + + https://yobit.net/en/api + + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :param dict [params.all]: you can set to `true` for convenience to fetch all tickers from self exchange by sending multiple requests + :returns dict: a dictionary of `ticker structures ` + """ + allSymbols = None + allSymbols, params = self.handle_param_bool(params, 'all', False) + if symbols is None and not allSymbols: + raise ArgumentsRequired(self.id + ' fetchTickers() requires "symbols" argument or use `params["all"] = True` to send multiple requests for all markets') + self.load_markets() + promises = [] + maxLength = self.safe_integer(self.options, 'maxUrlLength', 2048) + # max URL length is 2048 symbols, including http schema, hostname, tld, etc... + lenghtOfBaseUrl = 40 # safe space for the url including api-base and endpoint dir is 30 chars + if allSymbols: + symbols = self.symbols + ids = '' + for i in range(0, len(self.ids)): + id = self.ids[i] + prefix = '' if (ids == '') else '-' + ids += prefix + id + if len(ids) > maxLength: + promises.append(self.fetch_tickers_helper(ids, params)) + ids = '' + if ids != '': + promises.append(self.fetch_tickers_helper(ids, params)) + else: + symbols = self.market_symbols(symbols) + ids = self.market_ids(symbols) + idsLength: number = len(ids) + idsString = '-'.join(ids) + actualLength = len(idsString) + lenghtOfBaseUrl + if actualLength > maxLength: + raise ArgumentsRequired(self.id + ' fetchTickers() is being requested for ' + str(idsLength) + ' markets(which has an URL length of ' + str(actualLength) + ' characters), but it exceedes max URL length(' + str(maxLength) + '), please pass limisted symbols array to fetchTickers to fit in one request') + promises.append(self.fetch_tickers_helper(idsString, params)) + resultAll = promises + finalResult = {} + for i in range(0, len(resultAll)): + result = self.filter_by_array_tickers(resultAll[i], 'symbol', symbols) + finalResult = self.extend(finalResult, result) + return finalResult + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + + https://yobit.net/en/api + + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + tickers = self.fetch_tickers([symbol], params) + return tickers[symbol] + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(public) + # + # { + # "type":"bid", + # "price":0.14046179, + # "amount":0.001, + # "tid":200256901, + # "timestamp":1649861004 + # } + # + # fetchMyTrades(private) + # + # { + # "pair":"doge_usdt", + # "type":"sell", + # "amount":139, + # "rate":0.139, + # "order_id":"2101103631773172", + # "is_your_order":1, + # "timestamp":"1649861561" + # } + # + timestamp = self.safe_timestamp(trade, 'timestamp') + side = self.safe_string(trade, 'type') + if side == 'ask': + side = 'sell' + elif side == 'bid': + side = 'buy' + priceString = self.safe_string_2(trade, 'rate', 'price') + id = self.safe_string_2(trade, 'trade_id', 'tid') + order = self.safe_string(trade, 'order_id') + marketId = self.safe_string(trade, 'pair') + symbol = self.safe_symbol(marketId, market) + amountString = self.safe_string(trade, 'amount') + # arguments for calculateFee(need to be numbers) + price = self.parse_number(priceString) + amount = self.parse_number(amountString) + type = 'limit' # all trades are still limit trades + fee = None + feeCostString = self.safe_number(trade, 'commission') + if feeCostString is not None: + feeCurrencyId = self.safe_string(trade, 'commissionCurrency') + feeCurrencyCode = self.safe_currency_code(feeCurrencyId) + fee = { + 'cost': feeCostString, + 'currency': feeCurrencyCode, + } + isYourOrder = self.safe_string(trade, 'is_your_order') + if isYourOrder is not None: + if fee is None: + feeInNumbers = self.calculate_fee(symbol, type, side, amount, price, 'taker') + fee = { + 'currency': self.safe_string(feeInNumbers, 'currency'), + 'cost': self.safe_string(feeInNumbers, 'cost'), + 'rate': self.safe_string(feeInNumbers, 'rate'), + } + return self.safe_trade({ + 'id': id, + 'order': order, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'type': type, + 'side': side, + 'takerOrMaker': None, + 'price': priceString, + 'amount': amountString, + 'cost': None, + 'fee': fee, + 'info': trade, + }, market) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + + https://yobit.net/en/api + + get the list of most recent trades for a particular symbol + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + if limit is not None: + request['limit'] = limit + response = self.publicGetTradesPair(self.extend(request, params)) + # + # { + # "doge_usdt": [ + # { + # "type":"ask", + # "price":0.13956743, + # "amount":0.0008, + # "tid":200256900, + # "timestamp":1649860521 + # }, + # ] + # } + # + if isinstance(response, list): + numElements = len(response) + if numElements == 0: + return [] + result = self.safe_list(response, market['id'], []) + return self.parse_trades(result, market, since, limit) + + def fetch_trading_fees(self, params={}) -> TradingFees: + """ + + https://yobit.net/en/api + + fetch the trading fees for multiple markets + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `fee structures ` indexed by market symbols + """ + self.load_markets() + response = self.publicGetInfo(params) + # + # { + # "server_time":1615856752, + # "pairs":{ + # "ltc_btc":{ + # "decimal_places":8, + # "min_price":0.00000001, + # "max_price":10000, + # "min_amount":0.0001, + # "min_total":0.0001, + # "hidden":0, + # "fee":0.2, + # "fee_buyer":0.2, + # "fee_seller":0.2 + # }, + # ... + # }, + # } + # + pairs = self.safe_dict(response, 'pairs', {}) + marketIds = list(pairs.keys()) + result: dict = {} + for i in range(0, len(marketIds)): + marketId = marketIds[i] + pair = self.safe_dict(pairs, marketId, {}) + symbol = self.safe_symbol(marketId, None, '_') + takerString = self.safe_string(pair, 'fee_buyer') + makerString = self.safe_string(pair, 'fee_seller') + taker = self.parse_number(Precise.string_div(takerString, '100')) + maker = self.parse_number(Precise.string_div(makerString, '100')) + result[symbol] = { + 'info': pair, + 'symbol': symbol, + 'taker': taker, + 'maker': maker, + 'percentage': True, + 'tierBased': False, + } + return result + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + + https://yobit.net/en/api + + create a trade order + :param str symbol: unified symbol of the market to create an order in + :param str type: must be 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + if type == 'market': + raise ExchangeError(self.id + ' createOrder() allows limit orders only') + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + 'type': side, + 'amount': self.amount_to_precision(symbol, amount), + 'rate': self.price_to_precision(symbol, price), + } + response = self.privatePostTrade(self.extend(request, params)) + # + # { + # "success":1, + # "return": { + # "received":0, + # "remains":10, + # "order_id":1101103635125179, + # "funds": { + # "usdt":27.84756553, + # "usdttrc20":0, + # "doge":19.98327206 + # }, + # "funds_incl_orders": { + # "usdt":30.35256553, + # "usdttrc20":0, + # "doge":19.98327206 + # }, + # "server_time":1650114256 + # } + # } + # + result = self.safe_dict(response, 'return') + return self.parse_order(result, market) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + + https://yobit.net/en/api + + cancels an open order + :param str id: order id + :param str symbol: not used by yobit cancelOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + request: dict = { + 'order_id': int(id), + } + response = self.privatePostCancelOrder(self.extend(request, params)) + # + # { + # "success":1, + # "return": { + # "order_id":1101103632552304, + # "funds": { + # "usdt":30.71055443, + # "usdttrc20":0, + # "doge":9.98327206 + # }, + # "funds_incl_orders": { + # "usdt":31.81275443, + # "usdttrc20":0, + # "doge":9.98327206 + # }, + # "server_time":1649918298 + # } + # } + # + result = self.safe_dict(response, 'return', {}) + return self.parse_order(result) + + def parse_order_status(self, status: Str): + statuses: dict = { + '0': 'open', + '1': 'closed', + '2': 'canceled', + '3': 'open', # or partially-filled and canceled? https://github.com/ccxt/ccxt/issues/1594 + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder(private) + # + # { + # "received":0, + # "remains":10, + # "order_id":1101103635125179, + # "funds": { + # "usdt":27.84756553, + # "usdttrc20":0, + # "doge":19.98327206 + # }, + # "funds_incl_orders": { + # "usdt":30.35256553, + # "usdttrc20":0, + # "doge":19.98327206 + # }, + # "server_time":1650114256 + # } + # + # fetchOrder(private) + # + # { + # "id: "1101103635103335", # id-field is manually added in fetchOrder() from exchange response id-order dictionary structure + # "pair":"doge_usdt", + # "type":"buy", + # "start_amount":10, + # "amount":10, + # "rate":0.05, + # "timestamp_created":"1650112553", + # "status":0 + # } + # + # fetchOpenOrders(private) + # + # { + # "id":"1101103635103335", # id-field is manually added in fetchOpenOrders() from exchange response id-order dictionary structure + # "pair":"doge_usdt", + # "type":"buy", + # "amount":10, + # "rate":0.05, + # "timestamp_created":"1650112553", + # "status":0 + # } + # + # cancelOrder(private) + # + # { + # "order_id":1101103634000197, + # "funds": { + # "usdt":31.81275443, + # "usdttrc20":0, + # "doge":9.98327206 + # }, + # "funds_incl_orders": { + # "usdt":31.81275443, + # "usdttrc20":0, + # "doge":9.98327206 + # } + # } + # + id = self.safe_string_2(order, 'id', 'order_id') + status = self.parse_order_status(self.safe_string(order, 'status', 'open')) + if id == '0': + id = self.safe_string(order, 'init_order_id') + status = 'closed' + timestamp = self.safe_timestamp_2(order, 'timestamp_created', 'server_time') + marketId = self.safe_string(order, 'pair') + symbol = self.safe_symbol(marketId, market) + amount = self.safe_string(order, 'start_amount') + remaining = self.safe_string_2(order, 'amount', 'remains') + filled = self.safe_string(order, 'received', '0.0') + price = self.safe_string(order, 'rate') + fee = None + type = 'limit' + side = self.safe_string(order, 'type') + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': None, + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'type': type, + 'timeInForce': None, + 'postOnly': None, + 'side': side, + 'price': price, + 'triggerPrice': None, + 'cost': None, + 'amount': amount, + 'remaining': remaining, + 'filled': filled, + 'status': status, + 'fee': fee, + 'average': None, + 'trades': None, + }, market) + + def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + + https://yobit.net/en/api + + fetches information on an order made by the user + :param str id: order id + :param str symbol: not used by yobit fetchOrder + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + self.load_markets() + request: dict = { + 'order_id': int(id), + } + response = self.privatePostOrderInfo(self.extend(request, params)) + id = str(id) + orders = self.safe_dict(response, 'return', {}) + # + # { + # "success":1, + # "return": { + # "1101103635103335": { + # "pair":"doge_usdt", + # "type":"buy", + # "start_amount":10, + # "amount":10, + # "rate":0.05, + # "timestamp_created":"1650112553", + # "status":0 + # } + # } + # } + # + return self.parse_order(self.extend({'id': id}, orders[id])) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + + https://yobit.net/en/api + + fetch all unfilled currently open orders + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOpenOrders() requires a symbol argument') + self.load_markets() + request: dict = {} + market = None + if symbol is not None: + marketInner = self.market(symbol) + request['pair'] = marketInner['id'] + response = self.privatePostActiveOrders(self.extend(request, params)) + # + # { + # "success":1, + # "return": { + # "1101103634006799": { + # "pair":"doge_usdt", + # "type":"buy", + # "amount":10, + # "rate":0.1, + # "timestamp_created":"1650034937", + # "status":0 + # }, + # "1101103634006738": { + # "pair":"doge_usdt", + # "type":"buy", + # "amount":10, + # "rate":0.1, + # "timestamp_created":"1650034932", + # "status":0 + # } + # } + # } + # + result = self.safe_dict(response, 'return', {}) + return self.parse_orders(result, market, since, limit) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + + https://yobit.net/en/api + + fetch all trades made by the user + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchMyTrades() requires a symbol argument') + self.load_markets() + market = self.market(symbol) + # some derived classes use camelcase notation for request fields + request: dict = { + # 'from': 123456789, # trade ID, from which the display starts numerical 0(test result: liqui ignores self field) + # 'count': 1000, # the number of trades for display numerical, default = 1000 + # 'from_id': trade ID, from which the display starts numerical 0 + # 'end_id': trade ID on which the display ends numerical ∞ + # 'order': 'ASC', # sorting, default = DESC(test result: liqui ignores self field, most recent trade always goes last) + # 'since': 1234567890, # UTC start time, default = 0(test result: liqui ignores self field) + # 'end': 1234567890, # UTC end time, default = ∞(test result: liqui ignores self field) + 'pair': market['id'], + } + if limit is not None: + request['count'] = limit + if since is not None: + request['since'] = self.parse_to_int(since / 1000) + response = self.privatePostTradeHistory(self.extend(request, params)) + # + # { + # "success":1, + # "return": { + # "200257004": { + # "pair":"doge_usdt", + # "type":"sell", + # "amount":139, + # "rate":0.139, + # "order_id":"2101103631773172", + # "is_your_order":1, + # "timestamp":"1649861561" + # } + # } + # } + # + trades = self.safe_dict(response, 'return', {}) + ids = list(trades.keys()) + result = [] + for i in range(0, len(ids)): + id = self.safe_string(ids, i) + trade = self.parse_trade(self.extend(trades[id], { + 'trade_id': id, + }), market) + result.append(trade) + return self.filter_by_symbol_since_limit(result, market['symbol'], since, limit) + + def create_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + + https://yobit.net/en/api + + create a currency deposit address + :param str code: unified currency code of the currency for the deposit address + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + request: dict = { + 'need_new': 1, + } + response = self.fetch_deposit_address(code, self.extend(request, params)) + address = self.safe_string(response, 'address') + self.check_address(address) + return { + 'currency': code, + 'address': address, + 'tag': None, + 'network': None, + 'info': response['info'], + } + + def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://yobit.net/en/api + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `address structure ` + """ + self.load_markets() + currency = self.currency(code) + currencyId = currency['id'] + networks = self.safe_dict(self.options, 'networks', {}) + network = self.safe_string_upper(params, 'network') # self line allows the user to specify either ERC20 or ETH + network = self.safe_string(networks, network, network) # handle ERC20>ETH alias + if network is not None: + if network != 'ERC20': + currencyId = currencyId + network.lower() + params = self.omit(params, 'network') + request: dict = { + 'coinName': currencyId, + 'need_new': 0, + } + response = self.privatePostGetDepositAddress(self.extend(request, params)) + address = self.safe_string(response['return'], 'address') + self.check_address(address) + return { + 'info': response, + 'currency': code, + 'network': None, + 'address': address, + 'tag': None, + } + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + + https://yobit.net/en/api + + make a withdrawal + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.check_address(address) + self.load_markets() + currency = self.currency(code) + request: dict = { + 'coinName': currency['id'], + 'amount': amount, + 'address': address, + } + # no docs on the tag, yet... + if tag is not None: + raise ExchangeError(self.id + ' withdraw() does not support the tag argument yet due to a lack of docs on withdrawing with tag/memo on behalf of the exchange.') + response = self.privatePostWithdrawCoinsToAddress(self.extend(request, params)) + return { + 'info': response, + 'id': None, + 'txid': None, + 'type': None, + 'currency': None, + 'network': None, + 'amount': None, + 'status': None, + 'timestamp': None, + 'datetime': None, + 'address': None, + 'addressFrom': None, + 'addressTo': None, + 'tag': None, + 'tagFrom': None, + 'tagTo': None, + 'updated': None, + 'comment': None, + 'fee': { + 'currency': None, + 'cost': None, + 'rate': None, + }, + } + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = self.urls['api'][api] + query = self.omit(params, self.extract_params(path)) + if api == 'private': + self.check_required_credentials() + nonce = self.nonce() + body = self.urlencode(self.extend({ + 'nonce': nonce, + 'method': path, + }, query)) + signature = self.hmac(self.encode(body), self.encode(self.secret), hashlib.sha512) + headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Key': self.apiKey, + 'Sign': signature, + } + elif api == 'public': + url += '/' + self.version + '/' + self.implode_params(path, params) + if query: + url += '?' + self.urlencode(query) + else: + url += '/' + self.implode_params(path, params) + if method == 'GET': + if query: + url += '?' + self.urlencode(query) + else: + if query: + body = self.json(query) + headers = { + 'Content-Type': 'application/json', + } + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None # fallback to default error handler + if 'success' in response: + # + # 1 - Liqui only returns the integer 'success' key from their private API + # + # {"success": 1, ...} httpCode == 200 + # {"success": 0, ...} httpCode == 200 + # + # 2 - However, exchanges derived from Liqui, can return non-integers + # + # It can be a numeric string + # {"sucesss": "1", ...} + # {"sucesss": "0", ...}, httpCode >= 200(can be 403, 502, etc) + # + # Or just a string + # {"success": "true", ...} + # {"success": "false", ...}, httpCode >= 200 + # + # Or a boolean + # {"success": True, ...} + # {"success": False, ...}, httpCode >= 200 + # + # 3 - Oversimplified, Python PEP8 forbids comparison operator(==) of different types + # + # 4 - We do not want to copy-paste and duplicate the code of self handler to other exchanges derived from Liqui + # + # To cover points 1, 2, 3 and 4 combined self handler should work like self: + # + success = self.safe_value(response, 'success') # don't replace with safeBool here + if isinstance(success, str): + if (success == 'true') or (success == '1'): + success = True + else: + success = False + if not success: + code = self.safe_string(response, 'code') + message = self.safe_string(response, 'error') + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions['exact'], code, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + raise ExchangeError(feedback) # unknown message + return None diff --git a/ccxt/zaif.py b/ccxt/zaif.py new file mode 100644 index 0000000..1852220 --- /dev/null +++ b/ccxt/zaif.py @@ -0,0 +1,802 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.zaif import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currency, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Ticker, Trade, Transaction +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import BadRequest +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class zaif(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(zaif, self).describe(), { + 'id': 'zaif', + 'name': 'Zaif', + 'countries': ['JP'], + # 10 requests per second = 1000ms / 10 = 100ms between requests(public market endpoints) + 'rateLimit': 100, + 'version': '1', + 'has': { + 'CORS': None, + 'spot': True, + 'margin': None, # has but unimplemented + 'swap': False, + 'future': False, + 'option': False, + 'cancelOrder': True, + 'createMarketOrder': False, + 'createOrder': True, + 'fetchBalance': True, + 'fetchClosedOrders': True, + 'fetchCurrencies': False, + 'fetchFundingHistory': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchIndexOHLCV': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenOrders': True, + 'fetchOrderBook': True, + 'fetchPremiumIndexOHLCV': False, + 'fetchTicker': True, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': False, + 'withdraw': True, + }, + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/27766927-39ca2ada-5eeb-11e7-972f-1b4199518ca6.jpg', + 'api': { + 'rest': 'https://api.zaif.jp', + }, + 'www': 'https://zaif.jp', + 'doc': [ + 'https://techbureau-api-document.readthedocs.io/ja/latest/index.html', + 'https://corp.zaif.jp/api-docs', + 'https://corp.zaif.jp/api-docs/api_links', + 'https://www.npmjs.com/package/zaif.jp', + 'https://github.com/you21979/node-zaif', + ], + 'fees': 'https://zaif.jp/fee?lang=en', + }, + 'fees': { + 'trading': { + 'percentage': True, + 'taker': self.parse_number('0.001'), + 'maker': self.parse_number('0'), + }, + }, + 'api': { + 'public': { + 'get': { + 'depth/{pair}': 1, + 'currencies/{pair}': 1, + 'currencies/all': 1, + 'currency_pairs/{pair}': 1, + 'currency_pairs/all': 1, + 'last_price/{pair}': 1, + 'ticker/{pair}': 1, + 'trades/{pair}': 1, + }, + }, + 'private': { + 'post': { + 'active_orders': 5, # 10 in 5 seconds = 2 per second => cost = 10 / 2 = 5 + 'cancel_order': 5, + 'deposit_history': 5, + 'get_id_info': 5, + 'get_info': 10, # 10 in 10 seconds = 1 per second => cost = 10 / 1 = 10 + 'get_info2': 5, # 20 in 10 seconds = 2 per second => cost = 10 / 2 = 5 + 'get_personal_info': 5, + 'trade': 5, + 'trade_history': 50, # 12 in 60 seconds = 0.2 per second => cost = 10 / 0.2 = 50 + 'withdraw': 5, + 'withdraw_history': 5, + }, + }, + 'ecapi': { + 'post': { + 'createInvoice': 1, # unverified + 'getInvoice': 1, + 'getInvoiceIdsByOrderNumber': 1, + 'cancelInvoice': 1, + }, + }, + 'tlapi': { + 'post': { + 'get_positions': 66, # 10 in 60 seconds = 0.166 per second => cost = 10 / 0.166 = 66 + 'position_history': 66, # 10 in 60 seconds + 'active_positions': 5, # 20 in 10 seconds + 'create_position': 33, # 3 in 10 seconds = 0.3 per second => cost = 10 / 0.3 = 33 + 'change_position': 33, # 3 in 10 seconds + 'cancel_position': 33, # 3 in 10 seconds + }, + }, + 'fapi': { + 'get': { + 'groups/{group_id}': 1, # testing + 'last_price/{group_id}/{pair}': 1, + 'ticker/{group_id}/{pair}': 1, + 'trades/{group_id}/{pair}': 1, + 'depth/{group_id}/{pair}': 1, + }, + }, + }, + 'options': { + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': True, # todo + 'triggerPrice': True, # todo implement + 'triggerDirection': False, + 'triggerPriceType': None, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': False, + 'FOK': False, + 'PO': False, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': True, # todo implement + 'marketBuyByCost': False, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': None, # todo + 'fetchOrder': None, + 'fetchOpenOrders': { + 'marginMode': True, # todo + 'limit': None, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, # todo + 'fetchClosedOrders': { + 'marginMode': True, # todo + 'limit': 1000, + 'daysBack': 100000, # todo + 'daysBackCanceled': 1, # todo + 'untilDays': 100000, # todo + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOHLCV': None, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + 'exact': { + 'unsupported currency_pair': BadRequest, # {"error": "unsupported currency_pair"} + }, + 'broad': { + }, + }, + }) + + def fetch_markets(self, params={}) -> List[Market]: + """ + + https://zaif-api-document.readthedocs.io/ja/latest/PublicAPI.html#id12 + + retrieves data on all markets for zaif + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + markets = self.publicGetCurrencyPairsAll(params) + # + # [ + # { + # "aux_unit_point": 0, + # "item_japanese": "\u30d3\u30c3\u30c8\u30b3\u30a4\u30f3", + # "aux_unit_step": 5.0, + # "description": "\u30d3\u30c3\u30c8\u30b3\u30a4\u30f3\u30fb\u65e5\u672c\u5186\u306e\u53d6\u5f15\u3092\u884c\u3046\u3053\u3068\u304c\u3067\u304d\u307e\u3059", + # "item_unit_min": 0.001, + # "event_number": 0, + # "currency_pair": "btc_jpy", + # "is_token": False, + # "aux_unit_min": 5.0, + # "aux_japanese": "\u65e5\u672c\u5186", + # "id": 1, + # "item_unit_step": 0.0001, + # "name": "BTC/JPY", + # "seq": 0, + # "title": "BTC/JPY" + # } + # ] + # + return self.parse_markets(markets) + + def parse_market(self, market: dict) -> Market: + id = self.safe_string(market, 'currency_pair') + name = self.safe_string(market, 'name') + baseId, quoteId = name.split('/') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + symbol = base + '/' + quote + return { + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': None, + 'swap': False, + 'future': False, + 'option': False, + 'active': None, # can trade or not + 'contract': False, + 'linear': None, + 'inverse': None, + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'precision': { + 'amount': self.safe_number(market, 'item_unit_step'), + 'price': self.parse_number(self.parse_precision(self.safe_string(market, 'aux_unit_point'))), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(market, 'item_unit_min'), + 'max': None, + }, + 'price': { + 'min': self.safe_number(market, 'aux_unit_min'), + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + 'info': market, + } + + def parse_balance(self, response) -> Balances: + balances = self.safe_value(response, 'return', {}) + deposit = self.safe_value(balances, 'deposit') + result: dict = { + 'info': response, + 'timestamp': None, + 'datetime': None, + } + funds = self.safe_value(balances, 'funds', {}) + currencyIds = list(funds.keys()) + for i in range(0, len(currencyIds)): + currencyId = currencyIds[i] + code = self.safe_currency_code(currencyId) + balance = self.safe_string(funds, currencyId) + account = self.account() + account['free'] = balance + account['total'] = balance + if deposit is not None: + if currencyId in deposit: + account['total'] = self.safe_string(deposit, currencyId) + result[code] = account + return self.safe_balance(result) + + def fetch_balance(self, params={}) -> Balances: + """ + + https://zaif-api-document.readthedocs.io/ja/latest/TradingAPI.html#id10 + + query for balance and get the amount of funds available for trading or funds locked in orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + response = self.privatePostGetInfo(params) + return self.parse_balance(response) + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + + https://zaif-api-document.readthedocs.io/ja/latest/PublicAPI.html#id34 + + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + response = self.publicGetDepthPair(self.extend(request, params)) + return self.parse_order_book(response, market['symbol']) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "last": 9e-08, + # "high": 1e-07, + # "low": 9e-08, + # "vwap": 0.0, + # "volume": 135250.0, + # "bid": 9e-08, + # "ask": 1e-07 + # } + # + symbol = self.safe_symbol(None, market) + vwap = self.safe_string(ticker, 'vwap') + baseVolume = self.safe_string(ticker, 'volume') + quoteVolume = Precise.string_mul(baseVolume, vwap) + last = self.safe_string(ticker, 'last') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': None, + 'datetime': None, + 'high': self.safe_string(ticker, 'high'), + 'low': self.safe_string(ticker, 'low'), + 'bid': self.safe_string(ticker, 'bid'), + 'bidVolume': None, + 'ask': self.safe_string(ticker, 'ask'), + 'askVolume': None, + 'vwap': vwap, + 'open': None, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'info': ticker, + }, market) + + def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + + https://zaif-api-document.readthedocs.io/ja/latest/PublicAPI.html#id22 + + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + ticker = self.publicGetTickerPair(self.extend(request, params)) + # + # { + # "last": 9e-08, + # "high": 1e-07, + # "low": 9e-08, + # "vwap": 0.0, + # "volume": 135250.0, + # "bid": 9e-08, + # "ask": 1e-07 + # } + # + return self.parse_ticker(ticker, market) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # fetchTrades(public) + # + # { + # "date": 1648559414, + # "price": 5880375.0, + # "amount": 0.017, + # "tid": 176126557, + # "currency_pair": "btc_jpy", + # "trade_type": "ask" + # } + # + side = self.safe_string(trade, 'trade_type') + side = 'buy' if (side == 'bid') else 'sell' + timestamp = self.safe_timestamp(trade, 'date') + id = self.safe_string_2(trade, 'id', 'tid') + priceString = self.safe_string(trade, 'price') + amountString = self.safe_string(trade, 'amount') + marketId = self.safe_string(trade, 'currency_pair') + symbol = self.safe_symbol(marketId, market, '_') + return self.safe_trade({ + 'id': id, + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'type': None, + 'side': side, + 'order': None, + 'takerOrMaker': None, + 'price': priceString, + 'amount': amountString, + 'cost': None, + 'fee': None, + }, market) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + + https://zaif-api-document.readthedocs.io/ja/latest/PublicAPI.html#id28 + + get the list of most recent trades for a particular symbol + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'pair': market['id'], + } + response = self.publicGetTradesPair(self.extend(request, params)) + # + # [ + # { + # "date": 1648559414, + # "price": 5880375.0, + # "amount": 0.017, + # "tid": 176126557, + # "currency_pair": "btc_jpy", + # "trade_type": "ask" + # }, ... + # ] + # + numTrades = len(response) + if numTrades == 1: + firstTrade = response[0] + if not firstTrade: + response = [] + return self.parse_trades(response, market, since, limit) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + + https://zaif-api-document.readthedocs.io/ja/latest/MarginTradingAPI.html#id23 + + create a trade order + :param str symbol: unified symbol of the market to create an order in + :param str type: must be 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + if type != 'limit': + raise ExchangeError(self.id + ' createOrder() allows limit orders only') + market = self.market(symbol) + request: dict = { + 'currency_pair': market['id'], + 'action': 'bid' if (side == 'buy') else 'ask', + 'amount': amount, + 'price': price, + } + response = self.privatePostTrade(self.extend(request, params)) + return self.safe_order({ + 'info': response, + 'id': str(response['return']['order_id']), + }, market) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + + https://zaif-api-document.readthedocs.io/ja/latest/TradingAPI.html#id37 + + cancels an open order + :param str id: order id + :param str symbol: not used by zaif cancelOrder() + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + request: dict = { + 'order_id': id, + } + response = self.privatePostCancelOrder(self.extend(request, params)) + # + # { + # "success": 1, + # "return": { + # "order_id": 184, + # "funds": { + # "jpy": 15320, + # "btc": 1.392, + # "mona": 2600, + # "kaori": 0.1 + # } + # } + # } + # + data = self.safe_dict(response, 'return') + return self.parse_order(data) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # { + # "currency_pair": "btc_jpy", + # "action": "ask", + # "amount": 0.03, + # "price": 56000, + # "timestamp": 1402021125, + # "comment" : "demo" + # } + # + # cancelOrder + # + # { + # "order_id": 184, + # "funds": { + # "jpy": 15320, + # "btc": 1.392, + # "mona": 2600, + # "kaori": 0.1 + # } + # } + # + side = self.safe_string(order, 'action') + side = 'buy' if (side == 'bid') else 'sell' + timestamp = self.safe_timestamp(order, 'timestamp') + marketId = self.safe_string(order, 'currency_pair') + symbol = self.safe_symbol(marketId, market, '_') + price = self.safe_string(order, 'price') + amount = self.safe_string(order, 'amount') + id = self.safe_string_2(order, 'id', 'order_id') + return self.safe_order({ + 'id': id, + 'clientOrderId': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'status': 'open', + 'symbol': symbol, + 'type': 'limit', + 'timeInForce': None, + 'postOnly': None, + 'side': side, + 'price': price, + 'triggerPrice': None, + 'cost': None, + 'amount': amount, + 'filled': None, + 'remaining': None, + 'trades': None, + 'fee': None, + 'info': order, + 'average': None, + }, market) + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + + https://zaif-api-document.readthedocs.io/ja/latest/MarginTradingAPI.html#id28 + + fetch all unfilled currently open orders + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + market: Market = None + request: dict = { + # 'is_token': False, + # 'is_token_both': False, + } + if symbol is not None: + market = self.market(symbol) + request['currency_pair'] = market['id'] + response = self.privatePostActiveOrders(self.extend(request, params)) + return self.parse_orders(response['return'], market, since, limit) + + def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + + https://zaif-api-document.readthedocs.io/ja/latest/TradingAPI.html#id24 + + fetches information on multiple closed orders made by the user + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: the earliest time in ms to fetch orders for + :param int [limit]: the maximum number of order structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + market: Market = None + request: dict = { + # 'from': 0, + # 'count': 1000, + # 'from_id': 0, + # 'end_id': 1000, + # 'order': 'DESC', + # 'since': 1503821051, + # 'end': 1503821051, + # 'is_token': False, + } + if symbol is not None: + market = self.market(symbol) + request['currency_pair'] = market['id'] + response = self.privatePostTradeHistory(self.extend(request, params)) + return self.parse_orders(response['return'], market, since, limit) + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + + https://zaif-api-document.readthedocs.io/ja/latest/TradingAPI.html#id41 + + make a withdrawal + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.check_address(address) + self.load_markets() + currency = self.currency(code) + if code == 'JPY': + raise ExchangeError(self.id + ' withdraw() does not allow ' + code + ' withdrawals') + request: dict = { + 'currency': currency['id'], + 'amount': amount, + 'address': address, + # 'message': 'Hi!', # XEM and others + # 'opt_fee': 0.003, # BTC and MONA only + } + if tag is not None: + request['message'] = tag + result = self.privatePostWithdraw(self.extend(request, params)) + # + # { + # "success": 1, + # "return": { + # "id": 23634, + # "fee": 0.001, + # "txid":, + # "funds": { + # "jpy": 15320, + # "btc": 1.392, + # "xem": 100.2, + # "mona": 2600 + # } + # } + # } + # + returnData = self.safe_dict(result, 'return') + return self.parse_transaction(returnData, currency) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # { + # "id": 23634, + # "fee": 0.001, + # "txid":, + # "funds": { + # "jpy": 15320, + # "btc": 1.392, + # "xem": 100.2, + # "mona": 2600 + # } + # } + # + currency = self.safe_currency(None, currency) + fee = None + feeCost = self.safe_value(transaction, 'fee') + if feeCost is not None: + fee = { + 'cost': feeCost, + 'currency': currency['code'], + } + return { + 'id': self.safe_string(transaction, 'id'), + 'txid': self.safe_string(transaction, 'txid'), + 'timestamp': None, + 'datetime': None, + 'network': None, + 'addressFrom': None, + 'address': None, + 'addressTo': None, + 'amount': None, + 'type': None, + 'currency': currency['code'], + 'status': None, + 'updated': None, + 'tagFrom': None, + 'tag': None, + 'tagTo': None, + 'comment': None, + 'internal': None, + 'fee': fee, + 'info': transaction, + } + + def custom_nonce(self): + num = self.number_to_string(self.milliseconds() / 1000) + nonce = float(num) + return format(nonce, '.8f') + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = self.urls['api']['rest'] + '/' + if api == 'public': + url += 'api/' + self.version + '/' + self.implode_params(path, params) + elif api == 'fapi': + url += 'fapi/' + self.version + '/' + self.implode_params(path, params) + else: + self.check_required_credentials() + if api == 'ecapi': + url += 'ecapi' + elif api == 'tlapi': + url += 'tlapi' + else: + url += 'tapi' + nonce = self.custom_nonce() + body = self.urlencode(self.extend({ + 'method': path, + 'nonce': nonce, + }, params)) + headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Key': self.apiKey, + 'Sign': self.hmac(self.encode(body), self.encode(self.secret), hashlib.sha512), + } + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None + # + # {"error": "unsupported currency_pair"} + # + feedback = self.id + ' ' + body + error = self.safe_string(response, 'error') + if error is not None: + self.throw_exactly_matched_exception(self.exceptions['exact'], error, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], error, feedback) + raise ExchangeError(feedback) # unknown message + success = self.safe_bool(response, 'success', True) + if not success: + raise ExchangeError(feedback) + return None diff --git a/ccxt/zonda.py b/ccxt/zonda.py new file mode 100644 index 0000000..0d660c9 --- /dev/null +++ b/ccxt/zonda.py @@ -0,0 +1,1956 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.base.exchange import Exchange +from ccxt.abstract.zonda import ImplicitAPI +import hashlib +from ccxt.base.types import Any, Balances, Currency, DepositAddress, Int, LedgerEntry, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, Transaction, TransferEntry +from typing import List +from ccxt.base.errors import ExchangeError +from ccxt.base.errors import AuthenticationError +from ccxt.base.errors import PermissionDenied +from ccxt.base.errors import AccountSuspended +from ccxt.base.errors import BadRequest +from ccxt.base.errors import BadSymbol +from ccxt.base.errors import InsufficientFunds +from ccxt.base.errors import InvalidOrder +from ccxt.base.errors import OrderNotFound +from ccxt.base.errors import OrderImmediatelyFillable +from ccxt.base.errors import RateLimitExceeded +from ccxt.base.errors import OnMaintenance +from ccxt.base.errors import InvalidNonce +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class zonda(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(zonda, self).describe(), { + 'id': 'zonda', + 'name': 'Zonda', + 'countries': ['EE'], # Estonia + 'rateLimit': 1000, + 'has': { + 'CORS': True, + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'addMargin': False, + 'borrowCrossMargin': False, + 'borrowIsolatedMargin': False, + 'borrowMargin': False, + 'cancelAllOrders': False, + 'cancelOrder': True, + 'cancelOrders': False, + 'closeAllPositions': False, + 'closePosition': False, + 'createDepositAddress': False, + 'createOrder': True, + 'createReduceOnlyOrder': False, + 'fetchAllGreeks': False, + 'fetchBalance': True, + 'fetchBorrowInterest': False, + 'fetchBorrowRate': False, + 'fetchBorrowRateHistories': False, + 'fetchBorrowRateHistory': False, + 'fetchBorrowRates': False, + 'fetchBorrowRatesPerSymbol': False, + 'fetchCrossBorrowRate': False, + 'fetchCrossBorrowRates': False, + 'fetchCurrencies': False, + 'fetchDeposit': False, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': True, + 'fetchDepositAddressesByNetwork': False, + 'fetchDeposits': None, + 'fetchFundingHistory': False, + 'fetchFundingInterval': False, + 'fetchFundingIntervals': False, + 'fetchFundingRate': False, + 'fetchFundingRateHistory': False, + 'fetchFundingRates': False, + 'fetchGreeks': False, + 'fetchIndexOHLCV': False, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchIsolatedPositions': False, + 'fetchLedger': True, + 'fetchLeverage': False, + 'fetchLeverages': False, + 'fetchLeverageTiers': False, + 'fetchLiquidations': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': False, + 'fetchMarginAdjustmentHistory': False, + 'fetchMarginMode': False, + 'fetchMarginModes': False, + 'fetchMarketLeverageTiers': False, + 'fetchMarkets': True, + 'fetchMarkOHLCV': False, + 'fetchMarkPrice': False, + 'fetchMarkPrices': False, + 'fetchMyLiquidations': False, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': False, + 'fetchOpenInterestHistory': False, + 'fetchOpenInterests': False, + 'fetchOpenOrder': False, + 'fetchOpenOrders': True, + 'fetchOption': False, + 'fetchOptionChain': False, + 'fetchOrderBook': True, + 'fetchOrderBooks': False, + 'fetchPosition': False, + 'fetchPositionHistory': False, + 'fetchPositionMode': False, + 'fetchPositions': False, + 'fetchPositionsForSymbol': False, + 'fetchPositionsHistory': False, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': False, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': False, + 'fetchTrades': True, + 'fetchTradingFee': False, + 'fetchTradingFees': False, + 'fetchTransactionFee': False, + 'fetchTransactionFees': False, + 'fetchTransactions': None, + 'fetchTransfer': False, + 'fetchUnderlyingAssets': False, + 'fetchVolatilityHistory': False, + 'fetchWithdrawal': False, + 'fetchWithdrawals': None, + 'reduceMargin': False, + 'repayCrossMargin': False, + 'repayIsolatedMargin': False, + 'repayMargin': False, + 'setLeverage': False, + 'setMargin': False, + 'setMarginMode': False, + 'setPositionMode': False, + 'transfer': True, + 'withdraw': True, + }, + 'timeframes': { + '1m': '60', + '3m': '180', + '5m': '300', + '15m': '900', + '30m': '1800', + '1h': '3600', + '2h': '7200', + '4h': '14400', + '6h': '21600', + '12h': '43200', + '1d': '86400', + '3d': '259200', + '1w': '604800', + }, + 'hostname': 'zondacrypto.exchange', + 'urls': { + 'referral': 'https://auth.zondaglobal.com/ref/jHlbB4mIkdS1', + 'logo': 'https://user-images.githubusercontent.com/1294454/159202310-a0e38007-5e7c-4ba9-a32f-c8263a0291fe.jpg', + 'www': 'https://zondaglobal.com', + 'api': { + 'public': 'https://{hostname}/API/Public', + 'private': 'https://{hostname}/API/Trading/tradingApi.php', + 'v1_01Public': 'https://api.{hostname}/rest', + 'v1_01Private': 'https://api.{hostname}/rest', + }, + 'doc': [ + 'https://docs.zondacrypto.exchange/', + 'https://github.com/BitBayNet/API', + ], + 'support': 'https://zondaglobal.com/en/helpdesk/zonda-exchange', + 'fees': 'https://zondaglobal.com/legal/zonda-exchange/fees', + }, + 'api': { + 'public': { + 'get': [ + '{id}/all', + '{id}/market', + '{id}/orderbook', + '{id}/ticker', + '{id}/trades', + ], + }, + 'private': { + 'post': [ + 'info', + 'trade', + 'cancel', + 'orderbook', + 'orders', + 'transfer', + 'withdraw', + 'history', + 'transactions', + ], + }, + 'v1_01Public': { + 'get': [ + 'trading/ticker', + 'trading/ticker/{symbol}', + 'trading/stats', + 'trading/stats/{symbol}', + 'trading/orderbook/{symbol}', + 'trading/transactions/{symbol}', + 'trading/candle/history/{symbol}/{resolution}', + ], + }, + 'v1_01Private': { + 'get': [ + 'api_payments/deposits/crypto/addresses', + 'payments/withdrawal/{detailId}', + 'payments/deposit/{detailId}', + 'trading/offer', + 'trading/stop/offer', + 'trading/config/{symbol}', + 'trading/history/transactions', + 'balances/BITBAY/history', + 'balances/BITBAY/balance', + 'fiat_cantor/rate/{baseId}/{quoteId}', + 'fiat_cantor/history', + 'client_payments/v2/customer/crypto/{currency}/channels/deposit', + 'client_payments/v2/customer/crypto/{currency}/channels/withdrawal', + 'client_payments/v2/customer/crypto/deposit/fee', + 'client_payments/v2/customer/crypto/withdrawal/fee', + ], + 'post': [ + 'trading/offer/{symbol}', + 'trading/stop/offer/{symbol}', + 'trading/config/{symbol}', + 'balances/BITBAY/balance', + 'balances/BITBAY/balance/transfer/{source}/{destination}', + 'fiat_cantor/exchange', + 'api_payments/withdrawals/crypto', + 'api_payments/withdrawals/fiat', + 'client_payments/v2/customer/crypto/deposit', + 'client_payments/v2/customer/crypto/withdrawal', + ], + 'delete': [ + 'trading/offer/{symbol}/{id}/{side}/{price}', + 'trading/stop/offer/{symbol}/{id}/{side}/{price}', + ], + 'put': [ + 'balances/BITBAY/balance/{id}', + ], + }, + }, + 'fees': { + 'trading': { + 'maker': self.parse_number('0.0'), + 'taker': self.parse_number('0.001'), + 'percentage': True, + 'tierBased': False, + }, + 'fiat': { + 'maker': self.parse_number('0.0030'), + 'taker': self.parse_number('0.0043'), + 'percentage': True, + 'tierBased': True, + 'tiers': { + 'taker': [ + [self.parse_number('0.0043'), self.parse_number('0')], + [self.parse_number('0.0042'), self.parse_number('1250')], + [self.parse_number('0.0041'), self.parse_number('3750')], + [self.parse_number('0.0040'), self.parse_number('7500')], + [self.parse_number('0.0039'), self.parse_number('10000')], + [self.parse_number('0.0038'), self.parse_number('15000')], + [self.parse_number('0.0037'), self.parse_number('20000')], + [self.parse_number('0.0036'), self.parse_number('25000')], + [self.parse_number('0.0035'), self.parse_number('37500')], + [self.parse_number('0.0034'), self.parse_number('50000')], + [self.parse_number('0.0033'), self.parse_number('75000')], + [self.parse_number('0.0032'), self.parse_number('100000')], + [self.parse_number('0.0031'), self.parse_number('150000')], + [self.parse_number('0.0030'), self.parse_number('200000')], + [self.parse_number('0.0029'), self.parse_number('250000')], + [self.parse_number('0.0028'), self.parse_number('375000')], + [self.parse_number('0.0027'), self.parse_number('500000')], + [self.parse_number('0.0026'), self.parse_number('625000')], + [self.parse_number('0.0025'), self.parse_number('875000')], + ], + 'maker': [ + [self.parse_number('0.0030'), self.parse_number('0')], + [self.parse_number('0.0029'), self.parse_number('1250')], + [self.parse_number('0.0028'), self.parse_number('3750')], + [self.parse_number('0.0028'), self.parse_number('7500')], + [self.parse_number('0.0027'), self.parse_number('10000')], + [self.parse_number('0.0026'), self.parse_number('15000')], + [self.parse_number('0.0025'), self.parse_number('20000')], + [self.parse_number('0.0025'), self.parse_number('25000')], + [self.parse_number('0.0024'), self.parse_number('37500')], + [self.parse_number('0.0023'), self.parse_number('50000')], + [self.parse_number('0.0023'), self.parse_number('75000')], + [self.parse_number('0.0022'), self.parse_number('100000')], + [self.parse_number('0.0021'), self.parse_number('150000')], + [self.parse_number('0.0021'), self.parse_number('200000')], + [self.parse_number('0.0020'), self.parse_number('250000')], + [self.parse_number('0.0019'), self.parse_number('375000')], + [self.parse_number('0.0018'), self.parse_number('500000')], + [self.parse_number('0.0018'), self.parse_number('625000')], + [self.parse_number('0.0017'), self.parse_number('875000')], + ], + }, + }, + 'funding': { + 'withdraw': {}, + }, + }, + 'options': { + 'fetchTickerMethod': 'v1_01PublicGetTradingTickerSymbol', # or v1_01PublicGetTradingStatsSymbol + 'fetchTickersMethod': 'v1_01PublicGetTradingTicker', # or v1_01PublicGetTradingStats + 'fiatCurrencies': ['EUR', 'USD', 'GBP', 'PLN'], + 'transfer': { + 'fillResponseFromRequest': True, + }, + }, + 'features': { + 'spot': { + 'sandbox': False, + 'createOrder': { + 'marginMode': False, + 'triggerPrice': True, # todo remove + 'triggerDirection': False, + 'triggerPriceType': None, + 'stopLossPrice': False, # todo + 'takeProfitPrice': False, # todo + 'attachedStopLossTakeProfit': None, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': False, + 'trailing': False, + 'leverage': False, + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': False, + 'selfTradePrevention': False, + 'iceberg': False, + }, + 'createOrders': None, + 'fetchMyTrades': { + 'marginMode': False, + 'limit': None, + 'daysBack': 100000, # todo + 'untilDays': 100000, # todo + 'symbolRequired': False, + }, + 'fetchOrder': None, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 100, + 'trigger': False, + 'trailing': False, + 'symbolRequired': False, + }, + 'fetchOrders': None, + 'fetchClosedOrders': None, # todo + 'fetchOHLCV': { + 'limit': None, + }, + }, + 'swap': { + 'linear': None, + 'inverse': None, + }, + 'future': { + 'linear': None, + 'inverse': None, + }, + }, + 'precisionMode': TICK_SIZE, + 'exceptions': { + '400': ExchangeError, # At least one parameter wasn't set + '401': InvalidOrder, # Invalid order type + '402': InvalidOrder, # No orders with specified currencies + '403': InvalidOrder, # Invalid payment currency name + '404': InvalidOrder, # Error. Wrong transaction type + '405': InvalidOrder, # Order with self id doesn't exist + '406': InsufficientFunds, # No enough money or crypto + # code 407 not specified are not specified in their docs + '408': InvalidOrder, # Invalid currency name + '501': AuthenticationError, # Invalid public key + '502': AuthenticationError, # Invalid sign + '503': InvalidNonce, # Invalid moment parameter. Request time doesn't match current server time + '504': ExchangeError, # Invalid method + '505': AuthenticationError, # Key has no permission for self action + '506': AccountSuspended, # Account locked. Please contact with customer service + # codes 507 and 508 are not specified in their docs + '509': ExchangeError, # The BIC/SWIFT is required for self currency + '510': BadSymbol, # Invalid market name + 'FUNDS_NOT_SUFFICIENT': InsufficientFunds, + 'OFFER_FUNDS_NOT_EXCEEDING_MINIMUMS': InvalidOrder, + 'OFFER_NOT_FOUND': OrderNotFound, + 'OFFER_WOULD_HAVE_BEEN_PARTIALLY_FILLED': OrderImmediatelyFillable, + 'ACTION_LIMIT_EXCEEDED': RateLimitExceeded, + 'UNDER_MAINTENANCE': OnMaintenance, + 'REQUEST_TIMESTAMP_TOO_OLD': InvalidNonce, + 'PERMISSIONS_NOT_SUFFICIENT': PermissionDenied, + 'INVALID_STOP_RATE': InvalidOrder, + 'TIMEOUT': ExchangeError, + 'RESPONSE_TIMEOUT': ExchangeError, + 'ACTION_BLOCKED': PermissionDenied, + 'INVALID_HASH_SIGNATURE': AuthenticationError, + }, + 'commonCurrencies': { + 'GGC': 'Global Game Coin', + }, + }) + + def fetch_markets(self, params={}) -> List[Market]: + """ + + https://docs.zondacrypto.exchange/reference/ticker-1 + + retrieves data on all markets for zonda + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + response = self.v1_01PublicGetTradingTicker(params) + # + # { + # "status": "Ok", + # "items": { + # "BSV-USD": { + # "market": { + # "code": "BSV-USD", + # "first": {currency: "BSV", minOffer: "0.00035", scale: 8}, + # "second": {currency: "USD", minOffer: "5", scale: 2} + # }, + # "time": "1557569762154", + # "highestBid": "52.31", + # "lowestAsk": "62.99", + # "rate": "63", + # "previousRate": "51.21", + # }, + # }, + # } + # + items = self.safe_value(response, 'items', {}) + markets = list(items.values()) + return self.parse_markets(markets) + + def parse_market(self, item) -> Market: + market = self.safe_value(item, 'market', {}) + id = self.safe_string(market, 'code') + first = self.safe_value(market, 'first', {}) + second = self.safe_value(market, 'second', {}) + baseId = self.safe_string(first, 'currency') + quoteId = self.safe_string(second, 'currency') + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + fees = self.safe_value(self.fees, 'trading', {}) + fiatCurrencies = self.safe_value(self.options, 'fiatCurrencies', []) + if self.in_array(base, fiatCurrencies) or self.in_array(quote, fiatCurrencies): + fees = self.safe_value(self.fees, 'fiat', {}) + # todo: check that the limits have ben interpreted correctly + return { + 'id': id, + 'symbol': base + '/' + quote, + 'base': base, + 'quote': quote, + 'settle': None, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': None, + 'type': 'spot', + 'spot': True, + 'margin': False, + 'swap': False, + 'future': False, + 'option': False, + 'active': None, + 'contract': False, + 'linear': None, + 'inverse': None, + 'taker': self.safe_number(fees, 'taker'), + 'maker': self.safe_number(fees, 'maker'), + 'contractSize': None, + 'expiry': None, + 'expiryDatetime': None, + 'optionType': None, + 'strike': None, + 'precision': { + 'amount': self.parse_number(self.parse_precision(self.safe_string(first, 'scale'))), + 'price': self.parse_number(self.parse_precision(self.safe_string(second, 'scale'))), + }, + 'limits': { + 'leverage': { + 'min': None, + 'max': None, + }, + 'amount': { + 'min': self.safe_number(first, 'minOffer'), + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + 'info': item, + } + + def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + + https://docs.zondacrypto.exchange/reference/active-orders + + fetch all unfilled currently open orders + :param str symbol: not used by zonda fetchOpenOrders + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Order[]: a list of `order structures ` + """ + self.load_markets() + request: dict = {} + # todo pair + response = self.v1_01PrivateGetTradingOffer(self.extend(request, params)) + items = self.safe_list(response, 'items', []) + return self.parse_orders(items, None, since, limit, {'status': 'open'}) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # { + # "market": "ETH-EUR", + # "offerType": "Sell", + # "id": "93d3657b-d616-11e9-9248-0242ac110005", + # "currentAmount": "0.04", + # "lockedAmount": "0.04", + # "rate": "280", + # "startAmount": "0.04", + # "time": "1568372806924", + # "postOnly": False, + # "hidden": False, + # "mode": "limit", + # "receivedAmount": "0.0", + # "firstBalanceId": "5b816c3e-437c-4e43-9bef-47814ae7ebfc", + # "secondBalanceId": "ab43023b-4079-414c-b340-056e3430a3af" + # } + # + # cancelOrder + # + # { + # status: "Ok", + # errors: [] + # } + # + marketId = self.safe_string(order, 'market') + symbol = self.safe_symbol(marketId, market, '-') + timestamp = self.safe_integer(order, 'time') + amount = self.safe_string(order, 'startAmount') + remaining = self.safe_string(order, 'currentAmount') + postOnly = self.safe_value(order, 'postOnly') + return self.safe_order({ + 'id': self.safe_string(order, 'id'), + 'clientOrderId': None, + 'info': order, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': None, + 'status': None, + 'symbol': symbol, + 'type': self.safe_string(order, 'mode'), + 'timeInForce': None, + 'postOnly': postOnly, + 'side': self.safe_string_lower(order, 'offerType'), + 'price': self.safe_string(order, 'rate'), + 'triggerPrice': None, + 'amount': amount, + 'cost': None, + 'filled': None, + 'remaining': remaining, + 'average': None, + 'fee': None, + 'trades': None, + }, market) + + def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + + https://docs.zondacrypto.exchange/reference/transactions-history + + fetch all trades made by the user + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + request: dict = {} + if symbol: + markets = [self.market_id(symbol)] + symbol = self.symbol(symbol) + request['markets'] = markets + query: dict = {'query': self.json(self.extend(request, params))} + response = self.v1_01PrivateGetTradingHistoryTransactions(query) + # + # { + # "status": "Ok", + # "totalRows": "67", + # "items": [ + # { + # "id": "b54659a0-51b5-42a0-80eb-2ac5357ccee2", + # "market": "BTC-EUR", + # "time": "1541697096247", + # "amount": "0.00003", + # "rate": "4341.44", + # "initializedBy": "Sell", + # "wasTaker": False, + # "userAction": "Buy", + # "offerId": "bd19804a-6f89-4a69-adb8-eb078900d006", + # "commissionValue": null + # }, + # ] + # } + # + items = self.safe_value(response, 'items') + result = self.parse_trades(items, None, since, limit) + if symbol is None: + return result + return self.filter_by_symbol(result, symbol) + + def parse_balance(self, response) -> Balances: + balances = self.safe_value(response, 'balances') + if balances is None: + raise ExchangeError(self.id + ' empty balance response ' + self.json(response)) + result: dict = {'info': response} + for i in range(0, len(balances)): + balance = balances[i] + currencyId = self.safe_string(balance, 'currency') + code = self.safe_currency_code(currencyId) + account = self.account() + account['used'] = self.safe_string(balance, 'lockedFunds') + account['free'] = self.safe_string(balance, 'availableFunds') + result[code] = account + return self.safe_balance(result) + + def fetch_balance(self, params={}) -> Balances: + """ + + https://docs.zondacrypto.exchange/reference/list-of-wallets + + query for balance and get the amount of funds available for trading or funds locked in orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `balance structure ` + """ + self.load_markets() + response = self.v1_01PrivateGetBalancesBITBAYBalance(params) + return self.parse_balance(response) + + def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + + https://docs.zondacrypto.exchange/reference/orderbook-2 + + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + response = self.v1_01PublicGetTradingOrderbookSymbol(self.extend(request, params)) + # + # { + # "status":"Ok", + # "sell":[ + # {"ra":"43988.93","ca":"0.00100525","sa":"0.00100525","pa":"0.00100525","co":1}, + # {"ra":"43988.94","ca":"0.00114136","sa":"0.00114136","pa":"0.00114136","co":1}, + # {"ra":"43989","ca":"0.010578","sa":"0.010578","pa":"0.010578","co":1}, + # ], + # "buy":[ + # {"ra":"42157.33","ca":"2.83147881","sa":"2.83147881","pa":"2.83147881","co":2}, + # {"ra":"42096.0","ca":"0.00011878","sa":"0.00011878","pa":"0.00011878","co":1}, + # {"ra":"42022.0","ca":"0.00011899","sa":"0.00011899","pa":"0.00011899","co":1}, + # ], + # "timestamp":"1642299886122", + # "seqNo":"27641254" + # } + # + rawBids = self.safe_value(response, 'buy', []) + rawAsks = self.safe_value(response, 'sell', []) + timestamp = self.safe_integer(response, 'timestamp') + return { + 'symbol': market['symbol'], + 'bids': self.parse_bids_asks(rawBids, 'ra', 'ca'), + 'asks': self.parse_bids_asks(rawAsks, 'ra', 'ca'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'nonce': self.safe_integer(response, 'seqNo'), + } + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # version 1 + # + # { + # "m": "ETH-PLN", + # "h": "13485.13", + # "l": "13100.01", + # "v": "126.10710939", + # "r24h": "13332.72" + # } + # + # version 2 + # + # { + # "market": { + # "code": "ADA-USDT", + # "first": { + # "currency": "ADA", + # "minOffer": "0.2", + # "scale": "6" + # }, + # "second": { + # "currency": "USDT", + # "minOffer": "0.099", + # "scale": "6" + # }, + # "amountPrecision": "6", + # "pricePrecision": "6", + # "ratePrecision": "6" + # }, + # "time": "1655812661202", + # "highestBid": "0.492", + # "lowestAsk": "0.499389", + # "rate": "0.50588", + # "previousRate": "0.504981" + # } + # + tickerMarket = self.safe_value(ticker, 'market') + marketId = self.safe_string_2(tickerMarket, 'code', 'm') + market = self.safe_market(marketId, market) + timestamp = self.safe_integer(ticker, 'time') + rate = self.safe_value(ticker, 'rate') + return self.safe_ticker({ + 'symbol': self.safe_symbol(marketId, market), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': self.safe_string(ticker, 'h'), + 'low': self.safe_string(ticker, 'l'), + 'bid': self.safe_number(ticker, 'highestBid'), + 'bidVolume': None, + 'ask': self.safe_number(ticker, 'lowestAsk'), + 'askVolume': None, + 'vwap': None, + 'open': self.safe_string(ticker, 'r24h'), + 'close': rate, + 'last': rate, + 'previousClose': self.safe_value(ticker, 'previousRate'), + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': self.safe_string(ticker, 'v'), + 'quoteVolume': None, + 'info': ticker, + }, market) + + def fetch_ticker(self, symbol: str, params={}): + """ + v1_01PublicGetTradingTickerSymbol retrieves timestamp, datetime, bid, ask, close, last, previousClose, v1_01PublicGetTradingStatsSymbol retrieves high, low, volume and opening price of an asset + + https://docs.zondacrypto.exchange/reference/market-statistics + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.method]: v1_01PublicGetTradingTickerSymbol(default) or v1_01PublicGetTradingStatsSymbol + :returns dict: a `ticker structure ` + """ + self.load_markets() + market = self.market(symbol) + request: dict = { + 'symbol': market['id'], + } + method = 'v1_01PublicGetTradingTickerSymbol' + defaultMethod = self.safe_string(self.options, 'fetchTickerMethod', method) + fetchTickerMethod = self.safe_string_2(params, 'method', 'fetchTickerMethod', defaultMethod) + response = None + if fetchTickerMethod == method: + response = self.v1_01PublicGetTradingTickerSymbol(self.extend(request, params)) + # + # { + # "status": "Ok", + # "ticker": { + # "market": { + # "code": "ADA-USDT", + # "first": { + # "currency": "ADA", + # "minOffer": "0.21", + # "scale": 6 + # }, + # "second": { + # "currency": "USDT", + # "minOffer": "0.099", + # "scale": 6 + # }, + # "amountPrecision": 6, + # "pricePrecision": 6, + # "ratePrecision": 6 + # }, + # "time": "1655810976780", + # "highestBid": "0.498543", + # "lowestAsk": "0.50684", + # "rate": "0.50588", + # "previousRate": "0.504981" + # } + # } + # + elif fetchTickerMethod == 'v1_01PublicGetTradingStatsSymbol': + response = self.v1_01PublicGetTradingStatsSymbol(self.extend(request, params)) + # + # { + # "status": "Ok", + # "stats": { + # "m": "BTC-USDT", + # "h": "28800", + # "l": "26703.950101", + # "v": "6.72932396", + # "r24h": "27122.2" + # } + # } + # + else: + raise BadRequest(self.id + ' fetchTicker params["method"] must be "v1_01PublicGetTradingTickerSymbol" or "v1_01PublicGetTradingStatsSymbol"') + stats = self.safe_value_2(response, 'ticker', 'stats') + return self.parse_ticker(stats, market) + + def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + @ignore + v1_01PublicGetTradingTicker retrieves timestamp, datetime, bid, ask, close, last, previousClose for each market, v1_01PublicGetTradingStats retrieves high, low, volume and opening price of each market + + https://docs.zondacrypto.exchange/reference/market-statistics + + :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.method]: v1_01PublicGetTradingTicker(default) or v1_01PublicGetTradingStats + :returns dict: a dictionary of `ticker structures ` + """ + self.load_markets() + method = 'v1_01PublicGetTradingTicker' + defaultMethod = self.safe_string(self.options, 'fetchTickersMethod', method) + fetchTickersMethod = self.safe_string_2(params, 'method', 'fetchTickersMethod', defaultMethod) + response = None + if fetchTickersMethod == method: + response = self.v1_01PublicGetTradingTicker(params) + # + # { + # "status": "Ok", + # "items": { + # "DAI-PLN": { + # "market": { + # "code": "DAI-PLN", + # "first": { + # "currency": "DAI", + # "minOffer": "0.99", + # "scale": 8 + # }, + # "second": { + # "currency": "PLN", + # "minOffer": "5", + # "scale": 2 + # }, + # "amountPrecision": 8, + # "pricePrecision": 2, + # "ratePrecision": 2 + # }, + # "time": "1655810825137", + # "highestBid": "4.42", + # "lowestAsk": "4.44", + # "rate": "4.44", + # "previousRate": "4.43" + # }, + # ... + # } + # } + # + elif fetchTickersMethod == 'v1_01PublicGetTradingStats': + response = self.v1_01PublicGetTradingStats(params) + # + # { + # "status": "Ok", + # "items": { + # "DAI-PLN": { + # "m": "DAI-PLN", + # "h": "4.41", + # "l": "4.37", + # "v": "8.71068087", + # "r24h": "4.36" + # }, + # ... + # } + # } + # + else: + raise BadRequest(self.id + ' fetchTickers params["method"] must be "v1_01PublicGetTradingTicker" or "v1_01PublicGetTradingStats"') + items = self.safe_dict(response, 'items') + return self.parse_tickers(items, symbols) + + def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered the balance of the user + + https://docs.zondacrypto.exchange/reference/operations-history + + :param str [code]: unified currency code, default is None + :param int [since]: timestamp in ms of the earliest ledger entry, default is None + :param int [limit]: max number of ledger entries to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ledger structure ` + """ + balanceCurrencies = [] + if code is not None: + currency = self.currency(code) + balanceCurrencies.append(currency['id']) + request: dict = { + 'balanceCurrencies': balanceCurrencies, + } + if since is not None: + request['fromTime'] = since + if limit is not None: + request['limit'] = limit + request = self.extend(request, params) + response = self.v1_01PrivateGetBalancesBITBAYHistory({'query': self.json(request)}) + items = response['items'] + return self.parse_ledger(items, None, since, limit) + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + # + # FUNDS_MIGRATION + # { + # "historyId": "84ea7a29-7da5-4de5-b0c0-871e83cad765", + # "balance": { + # "id": "821ec166-cb88-4521-916c-f4eb44db98df", + # "currency": "LTC", + # "type": "CRYPTO", + # "userId": "a34d361d-7bad-49c1-888e-62473b75d877", + # "name": "LTC" + # }, + # "detailId": null, + # "time": 1506128252968, + # "type": "FUNDS_MIGRATION", + # "value": 0.0009957, + # "fundsBefore": {"total": 0, "available": 0, "locked": 0}, + # "fundsAfter": {"total": 0.0009957, "available": 0.0009957, "locked": 0}, + # "change": {"total": 0.0009957, "available": 0.0009957, "locked": 0} + # } + # + # CREATE_BALANCE + # { + # "historyId": "d0fabd8d-9107-4b5e-b9a6-3cab8af70d49", + # "balance": { + # "id": "653ffcf2-3037-4ebe-8e13-d5ea1a01d60d", + # "currency": "BTG", + # "type": "CRYPTO", + # "userId": "a34d361d-7bad-49c1-888e-62473b75d877", + # "name": "BTG" + # }, + # "detailId": null, + # "time": 1508895244751, + # "type": "CREATE_BALANCE", + # "value": 0, + # "fundsBefore": {"total": null, "available": null, "locked": null}, + # "fundsAfter": {"total": 0, "available": 0, "locked": 0}, + # "change": {"total": 0, "available": 0, "locked": 0} + # } + # + # BITCOIN_GOLD_FORK + # { + # "historyId": "2b4d52d3-611c-473d-b92c-8a8d87a24e41", + # "balance": { + # "id": "653ffcf2-3037-4ebe-8e13-d5ea1a01d60d", + # "currency": "BTG", + # "type": "CRYPTO", + # "userId": "a34d361d-7bad-49c1-888e-62473b75d877", + # "name": "BTG" + # }, + # "detailId": null, + # "time": 1508895244778, + # "type": "BITCOIN_GOLD_FORK", + # "value": 0.00453512, + # "fundsBefore": {"total": 0, "available": 0, "locked": 0}, + # "fundsAfter": {"total": 0.00453512, "available": 0.00453512, "locked": 0}, + # "change": {"total": 0.00453512, "available": 0.00453512, "locked": 0} + # } + # + # ADD_FUNDS + # { + # "historyId": "3158236d-dae5-4a5d-81af-c1fa4af340fb", + # "balance": { + # "id": "3a7e7a1e-0324-49d5-8f59-298505ebd6c7", + # "currency": "BTC", + # "type": "CRYPTO", + # "userId": "a34d361d-7bad-49c1-888e-62473b75d877", + # "name": "BTC" + # }, + # "detailId": "8e83a960-e737-4380-b8bb-259d6e236faa", + # "time": 1520631178816, + # "type": "ADD_FUNDS", + # "value": 0.628405, + # "fundsBefore": {"total": 0.00453512, "available": 0.00453512, "locked": 0}, + # "fundsAfter": {"total": 0.63294012, "available": 0.63294012, "locked": 0}, + # "change": {"total": 0.628405, "available": 0.628405, "locked": 0} + # } + # + # TRANSACTION_PRE_LOCKING + # { + # "historyId": "e7d19e0f-03b3-46a8-bc72-dde72cc24ead", + # "balance": { + # "id": "3a7e7a1e-0324-49d5-8f59-298505ebd6c7", + # "currency": "BTC", + # "type": "CRYPTO", + # "userId": "a34d361d-7bad-49c1-888e-62473b75d877", + # "name": "BTC" + # }, + # "detailId": null, + # "time": 1520706403868, + # "type": "TRANSACTION_PRE_LOCKING", + # "value": -0.1, + # "fundsBefore": {"total": 0.63294012, "available": 0.63294012, "locked": 0}, + # "fundsAfter": {"total": 0.63294012, "available": 0.53294012, "locked": 0.1}, + # "change": {"total": 0, "available": -0.1, "locked": 0.1} + # } + # + # TRANSACTION_POST_OUTCOME + # { + # "historyId": "c4010825-231d-4a9c-8e46-37cde1f7b63c", + # "balance": { + # "id": "3a7e7a1e-0324-49d5-8f59-298505ebd6c7", + # "currency": "BTC", + # "type": "CRYPTO", + # "userId": "a34d361d-7bad-49c1-888e-62473b75d877", + # "name": "BTC" + # }, + # "detailId": "bf2876bc-b545-4503-96c8-ef4de8233876", + # "time": 1520706404032, + # "type": "TRANSACTION_POST_OUTCOME", + # "value": -0.01771415, + # "fundsBefore": {"total": 0.63294012, "available": 0.53294012, "locked": 0.1}, + # "fundsAfter": {"total": 0.61522597, "available": 0.53294012, "locked": 0.08228585}, + # "change": {"total": -0.01771415, "available": 0, "locked": -0.01771415} + # } + # + # TRANSACTION_POST_INCOME + # { + # "historyId": "7f18b7af-b676-4125-84fd-042e683046f6", + # "balance": { + # "id": "ab43023b-4079-414c-b340-056e3430a3af", + # "currency": "EUR", + # "type": "FIAT", + # "userId": "a34d361d-7bad-49c1-888e-62473b75d877", + # "name": "EUR" + # }, + # "detailId": "f5fcb274-0cc7-4385-b2d3-bae2756e701f", + # "time": 1520706404035, + # "type": "TRANSACTION_POST_INCOME", + # "value": 628.78, + # "fundsBefore": {"total": 0, "available": 0, "locked": 0}, + # "fundsAfter": {"total": 628.78, "available": 628.78, "locked": 0}, + # "change": {"total": 628.78, "available": 628.78, "locked": 0} + # } + # + # TRANSACTION_COMMISSION_OUTCOME + # { + # "historyId": "843177fa-61bc-4cbf-8be5-b029d856c93b", + # "balance": { + # "id": "ab43023b-4079-414c-b340-056e3430a3af", + # "currency": "EUR", + # "type": "FIAT", + # "userId": "a34d361d-7bad-49c1-888e-62473b75d877", + # "name": "EUR" + # }, + # "detailId": "f5fcb274-0cc7-4385-b2d3-bae2756e701f", + # "time": 1520706404050, + # "type": "TRANSACTION_COMMISSION_OUTCOME", + # "value": -2.71, + # "fundsBefore": {"total": 766.06, "available": 766.06, "locked": 0}, + # "fundsAfter": {"total": 763.35,"available": 763.35, "locked": 0}, + # "change": {"total": -2.71, "available": -2.71, "locked": 0} + # } + # + # TRANSACTION_OFFER_COMPLETED_RETURN + # { + # "historyId": "cac69b04-c518-4dc5-9d86-e76e91f2e1d2", + # "balance": { + # "id": "3a7e7a1e-0324-49d5-8f59-298505ebd6c7", + # "currency": "BTC", + # "type": "CRYPTO", + # "userId": "a34d361d-7bad-49c1-888e-62473b75d877", + # "name": "BTC" + # }, + # "detailId": null, + # "time": 1520714886425, + # "type": "TRANSACTION_OFFER_COMPLETED_RETURN", + # "value": 0.00000196, + # "fundsBefore": {"total": 0.00941208, "available": 0.00941012, "locked": 0.00000196}, + # "fundsAfter": {"total": 0.00941208, "available": 0.00941208, "locked": 0}, + # "change": {"total": 0, "available": 0.00000196, "locked": -0.00000196} + # } + # + # WITHDRAWAL_LOCK_FUNDS + # { + # "historyId": "03de2271-66ab-4960-a786-87ab9551fc14", + # "balance": { + # "id": "3a7e7a1e-0324-49d5-8f59-298505ebd6c7", + # "currency": "BTC", + # "type": "CRYPTO", + # "userId": "a34d361d-7bad-49c1-888e-62473b75d877", + # "name": "BTC" + # }, + # "detailId": "6ad3dc72-1d6d-4ec2-8436-ca43f85a38a6", + # "time": 1522245654481, + # "type": "WITHDRAWAL_LOCK_FUNDS", + # "value": -0.8, + # "fundsBefore": {"total": 0.8, "available": 0.8, "locked": 0}, + # "fundsAfter": {"total": 0.8, "available": 0, "locked": 0.8}, + # "change": {"total": 0, "available": -0.8, "locked": 0.8} + # } + # + # WITHDRAWAL_SUBTRACT_FUNDS + # { + # "historyId": "b0308c89-5288-438d-a306-c6448b1a266d", + # "balance": { + # "id": "3a7e7a1e-0324-49d5-8f59-298505ebd6c7", + # "currency": "BTC", + # "type": "CRYPTO", + # "userId": "a34d361d-7bad-49c1-888e-62473b75d877", + # "name": "BTC" + # }, + # "detailId": "6ad3dc72-1d6d-4ec2-8436-ca43f85a38a6", + # "time": 1522246526186, + # "type": "WITHDRAWAL_SUBTRACT_FUNDS", + # "value": -0.8, + # "fundsBefore": {"total": 0.8, "available": 0, "locked": 0.8}, + # "fundsAfter": {"total": 0, "available": 0, "locked": 0}, + # "change": {"total": -0.8, "available": 0, "locked": -0.8} + # } + # + # TRANSACTION_OFFER_ABORTED_RETURN + # { + # "historyId": "b1a3c075-d403-4e05-8f32-40512cdd88c0", + # "balance": { + # "id": "3a7e7a1e-0324-49d5-8f59-298505ebd6c7", + # "currency": "BTC", + # "type": "CRYPTO", + # "userId": "a34d361d-7bad-49c1-888e-62473b75d877", + # "name": "BTC" + # }, + # "detailId": null, + # "time": 1522512298662, + # "type": "TRANSACTION_OFFER_ABORTED_RETURN", + # "value": 0.0564931, + # "fundsBefore": {"total": 0.44951311, "available": 0.39302001, "locked": 0.0564931}, + # "fundsAfter": {"total": 0.44951311, "available": 0.44951311, "locked": 0}, + # "change": {"total": 0, "available": 0.0564931, "locked": -0.0564931} + # } + # + # WITHDRAWAL_UNLOCK_FUNDS + # { + # "historyId": "0ed569a2-c330-482e-bb89-4cb553fb5b11", + # "balance": { + # "id": "3a7e7a1e-0324-49d5-8f59-298505ebd6c7", + # "currency": "BTC", + # "type": "CRYPTO", + # "userId": "a34d361d-7bad-49c1-888e-62473b75d877", + # "name": "BTC" + # }, + # "detailId": "0c7be256-c336-4111-bee7-4eb22e339700", + # "time": 1527866360785, + # "type": "WITHDRAWAL_UNLOCK_FUNDS", + # "value": 0.05045, + # "fundsBefore": {"total": 0.86001578, "available": 0.80956578, "locked": 0.05045}, + # "fundsAfter": {"total": 0.86001578, "available": 0.86001578, "locked": 0}, + # "change": {"total": 0, "available": 0.05045, "locked": -0.05045} + # } + # + # TRANSACTION_COMMISSION_RETURN + # { + # "historyId": "07c89c27-46f1-4d7a-8518-b73798bf168a", + # "balance": { + # "id": "ab43023b-4079-414c-b340-056e3430a3af", + # "currency": "EUR", + # "type": "FIAT", + # "userId": "a34d361d-7bad-49c1-888e-62473b75d877", + # "name": "EUR" + # }, + # "detailId": null, + # "time": 1528304043063, + # "type": "TRANSACTION_COMMISSION_RETURN", + # "value": 0.6, + # "fundsBefore": {"total": 0, "available": 0, "locked": 0}, + # "fundsAfter": {"total": 0.6, "available": 0.6, "locked": 0}, + # "change": {"total": 0.6, "available": 0.6, "locked": 0} + # } + # + timestamp = self.safe_integer(item, 'time') + balance = self.safe_value(item, 'balance', {}) + currencyId = self.safe_string(balance, 'currency') + currency = self.safe_currency(currencyId, currency) + change = self.safe_value(item, 'change', {}) + amount = self.safe_string(change, 'total') + direction = 'in' + if Precise.string_lt(amount, '0'): + direction = 'out' + amount = Precise.string_neg(amount) + # there are 2 undocumented api calls: (v1_01PrivateGetPaymentsDepositDetailId and v1_01PrivateGetPaymentsWithdrawalDetailId) + # that can be used to enrich the transfers with txid, address etc(you need to use info.detailId parameter) + fundsBefore = self.safe_value(item, 'fundsBefore', {}) + fundsAfter = self.safe_value(item, 'fundsAfter', {}) + return self.safe_ledger_entry({ + 'info': item, + 'id': self.safe_string(item, 'historyId'), + 'direction': direction, + 'account': None, + 'referenceId': self.safe_string(item, 'detailId'), + 'referenceAccount': None, + 'type': self.parse_ledger_entry_type(self.safe_string(item, 'type')), + 'currency': self.safe_currency_code(currencyId), + 'amount': self.parse_number(amount), + 'before': self.safe_number(fundsBefore, 'total'), + 'after': self.safe_number(fundsAfter, 'total'), + 'status': 'ok', + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'fee': None, + }, currency) + + def parse_ledger_entry_type(self, type): + types: dict = { + 'ADD_FUNDS': 'transaction', + 'BITCOIN_GOLD_FORK': 'transaction', + 'CREATE_BALANCE': 'transaction', + 'FUNDS_MIGRATION': 'transaction', + 'WITHDRAWAL_LOCK_FUNDS': 'transaction', + 'WITHDRAWAL_SUBTRACT_FUNDS': 'transaction', + 'WITHDRAWAL_UNLOCK_FUNDS': 'transaction', + 'TRANSACTION_COMMISSION_OUTCOME': 'fee', + 'TRANSACTION_COMMISSION_RETURN': 'fee', + 'TRANSACTION_OFFER_ABORTED_RETURN': 'trade', + 'TRANSACTION_OFFER_COMPLETED_RETURN': 'trade', + 'TRANSACTION_POST_INCOME': 'trade', + 'TRANSACTION_POST_OUTCOME': 'trade', + 'TRANSACTION_PRE_LOCKING': 'trade', + } + return self.safe_string(types, type, type) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # [ + # "1582399800000", + # { + # "o": "0.0001428", + # "c": "0.0001428", + # "h": "0.0001428", + # "l": "0.0001428", + # "v": "4", + # "co": "1" + # } + # ] + # + first = self.safe_value(ohlcv, 1, {}) + return [ + self.safe_integer(ohlcv, 0), + self.safe_number(first, 'o'), + self.safe_number(first, 'h'), + self.safe_number(first, 'l'), + self.safe_number(first, 'c'), + self.safe_number(first, 'v'), + ] + + def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: + """ + + https://docs.zondacrypto.exchange/reference/candles-chart + + fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market + :param str symbol: unified symbol of the market to fetch OHLCV data for + :param str timeframe: the length of time each candle represents + :param int [since]: timestamp in ms of the earliest candle to fetch + :param int [limit]: the maximum amount of candles to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + self.load_markets() + market = self.market(symbol) + tradingSymbol = market['baseId'] + '-' + market['quoteId'] + request: dict = { + 'symbol': tradingSymbol, + 'resolution': self.safe_string(self.timeframes, timeframe, timeframe), + # 'from': 1574709092000, # unix timestamp in milliseconds, required + # 'to': 1574709092000, # unix timestamp in milliseconds, required + } + if limit is None: + limit = 100 + else: + limit = min(limit, 11000) # supports up to 11k candles diapason + duration = self.parse_timeframe(timeframe) + timerange = limit * duration * 1000 + if since is None: + request['to'] = self.milliseconds() + request['from'] = request['to'] - timerange + else: + request['from'] = since + request['to'] = self.sum(request['from'], timerange) + response = self.v1_01PublicGetTradingCandleHistorySymbolResolution(self.extend(request, params)) + # + # { + # "status":"Ok", + # "items":[ + # ["1591503060000",{"o":"0.02509572","c":"0.02509438","h":"0.02509664","l":"0.02509438","v":"0.02082165","co":"17"}], + # ["1591503120000",{"o":"0.02509606","c":"0.02509515","h":"0.02509606","l":"0.02509487","v":"0.04971703","co":"13"}], + # ["1591503180000",{"o":"0.02509532","c":"0.02509589","h":"0.02509589","l":"0.02509454","v":"0.01332236","co":"7"}], + # ] + # } + # + items = self.safe_list(response, 'items', []) + return self.parse_ohlcvs(items, market, timeframe, since, limit) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # createOrder trades + # + # { + # "rate": "0.02195928", + # "amount": "0.00167952" + # } + # + # fetchMyTrades(private) + # + # { + # "amount": "0.29285199", + # "commissionValue": "0.00125927", + # "id": "11c8203a-a267-11e9-b698-0242ac110007", + # "initializedBy": "Buy", + # "market": "ETH-EUR", + # "offerId": "11c82038-a267-11e9-b698-0242ac110007", + # "rate": "277", + # "time": "1562689917517", + # "userAction": "Buy", + # "wasTaker": True, + # } + # + # fetchTrades(public) + # + # { + # "id": "df00b0da-e5e0-11e9-8c19-0242ac11000a", + # "t": "1570108958831", + # "a": "0.04776653", + # "r": "0.02145854", + # "ty": "Sell" + # } + # + timestamp = self.safe_integer_2(trade, 'time', 't') + side = self.safe_string_lower_2(trade, 'userAction', 'ty') + wasTaker = self.safe_value(trade, 'wasTaker') + takerOrMaker: Str = None + if wasTaker is not None: + takerOrMaker = 'taker' if wasTaker else 'maker' + priceString = self.safe_string_2(trade, 'rate', 'r') + amountString = self.safe_string_2(trade, 'amount', 'a') + feeCostString = self.safe_string(trade, 'commissionValue') + marketId = self.safe_string(trade, 'market') + market = self.safe_market(marketId, market, '-') + symbol = market['symbol'] + fee = None + if feeCostString is not None: + feeCurrency = market['base'] if (side == 'buy') else market['quote'] + fee = { + 'currency': feeCurrency, + 'cost': feeCostString, + } + order = self.safe_string(trade, 'offerId') + # todo: check self logic + type: Str = None + if order is not None: + type = 'limit' if order else 'market' + return self.safe_trade({ + 'id': self.safe_string(trade, 'id'), + 'order': order, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'type': type, + 'side': side, + 'price': priceString, + 'amount': amountString, + 'cost': None, + 'takerOrMaker': takerOrMaker, + 'fee': fee, + 'info': trade, + }, market) + + def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + + https://docs.zondacrypto.exchange/reference/last-transactions + + get the list of most recent trades for a particular symbol + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns Trade[]: a list of `trade structures ` + """ + self.load_markets() + market = self.market(symbol) + tradingSymbol = market['baseId'] + '-' + market['quoteId'] + request: dict = { + 'symbol': tradingSymbol, + } + if since is not None: + request['fromTime'] = since - 1 # result does not include exactly `since` time therefore decrease by 1 + if limit is not None: + request['limit'] = limit # default - 10, max - 300 + response = self.v1_01PublicGetTradingTransactionsSymbol(self.extend(request, params)) + items = self.safe_list(response, 'items') + return self.parse_trades(items, market, since, limit) + + def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://docs.zondacrypto.exchange/reference/new-order + + :param str symbol: unified symbol of the market to create an order in + :param str type: 'market' or 'limit' + :param str side: 'buy' or 'sell' + :param float amount: how much of currency you want to trade in units of base currency + :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + self.load_markets() + market = self.market(symbol) + tradingSymbol = market['baseId'] + '-' + market['quoteId'] + amount = float(self.amount_to_precision(symbol, amount)) + request: dict = { + 'symbol': tradingSymbol, + 'offerType': side.upper(), + 'amount': amount, + } + stopLossPrice = self.safe_value_2(params, 'stopPrice', 'stopLossPrice') + isStopLossPrice = stopLossPrice is not None + isLimitOrder = type == 'limit' + isMarketOrder = type == 'market' + isStopLimit = (type == 'stop-limit') or (isLimitOrder and isStopLossPrice) + isStopMarket = type == 'stop-market' or (isMarketOrder and isStopLossPrice) + isStopOrder = isStopLimit or isStopMarket + if isLimitOrder or isStopLimit: + request['rate'] = self.price_to_precision(symbol, price) + request['mode'] = 'stop-limit' if isStopLimit else 'limit' + elif isMarketOrder or isStopMarket: + request['mode'] = 'stop-market' if isStopMarket else 'market' + else: + raise ExchangeError(self.id + ' createOrder() invalid type') + params = self.omit(params, ['stopPrice', 'stopLossPrice']) + response = None + if isStopOrder: + if not isStopLossPrice: + raise ExchangeError(self.id + ' createOrder() zonda requires `triggerPrice` parameter for stop-limit or stop-market orders') + request['stopRate'] = self.price_to_precision(symbol, stopLossPrice) + response = self.v1_01PrivatePostTradingStopOfferSymbol(self.extend(request, params)) + else: + response = self.v1_01PrivatePostTradingOfferSymbol(self.extend(request, params)) + # + # unfilled(open order) + # + # { + # "status": "Ok", + # "completed": False, # can deduce status from here + # "offerId": "ce9cc72e-d61c-11e9-9248-0242ac110005", + # "transactions": [], # can deduce order info from here + # } + # + # filled(closed order) + # + # { + # "status": "Ok", + # "offerId": "942a4a3e-e922-11e9-8c19-0242ac11000a", + # "completed": True, + # "transactions": [ + # { + # "rate": "0.02195928", + # "amount": "0.00167952" + # }, + # { + # "rate": "0.02195928", + # "amount": "0.00167952" + # }, + # { + # "rate": "0.02196207", + # "amount": "0.27704177" + # } + # ] + # } + # + # partially-filled(open order) + # + # { + # "status": "Ok", + # "offerId": "d0ebefab-f4d7-11e9-8c19-0242ac11000a", + # "completed": False, + # "transactions": [ + # { + # "rate": "0.02106404", + # "amount": "0.0019625" + # }, + # { + # "rate": "0.02106404", + # "amount": "0.0019625" + # }, + # { + # "rate": "0.02105901", + # "amount": "0.00975256" + # } + # ] + # } + # + id = self.safe_string_2(response, 'offerId', 'stopOfferId') + completed = self.safe_bool(response, 'completed', False) + status = 'closed' if completed else 'open' + transactions = self.safe_value(response, 'transactions') + return self.safe_order({ + 'id': id, + 'info': response, + 'timestamp': None, + 'datetime': None, + 'lastTradeTimestamp': None, + 'status': status, + 'symbol': symbol, + 'type': type, + 'side': side, + 'price': price, + 'amount': amount, + 'cost': None, + 'filled': None, + 'remaining': None, + 'average': None, + 'fee': None, + 'trades': transactions, + 'clientOrderId': None, + }) + + def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + + https://docs.zondacrypto.exchange/reference/cancel-order + + cancels an open order + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: An `order structure ` + """ + side = self.safe_string(params, 'side') + if side is None: + raise ExchangeError(self.id + ' cancelOrder() requires a `side` parameter("buy" or "sell")') + price = self.safe_value(params, 'price') + if price is None: + raise ExchangeError(self.id + ' cancelOrder() requires a `price` parameter(float or string)') + self.load_markets() + market = self.market(symbol) + tradingSymbol = market['baseId'] + '-' + market['quoteId'] + request: dict = { + 'symbol': tradingSymbol, + 'id': id, + 'side': side, + 'price': price, + } + response = self.v1_01PrivateDeleteTradingOfferSymbolIdSidePrice(self.extend(request, params)) + # {status: "Fail", errors: ["NOT_RECOGNIZED_OFFER_TYPE"]} -- if required params are missing + # {status: "Ok", errors: []} + return self.parse_order(response) + + def is_fiat(self, currency: str) -> bool: + fiatCurrencies: dict = { + 'USD': True, + 'EUR': True, + 'PLN': True, + } + return self.safe_bool(fiatCurrencies, currency, False) + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + # + # { + # "address": "33u5YAEhQbYfjHHPsfMfCoSdEjfwYjVcBE", + # "currency": "BTC", + # "balanceId": "5d5d19e7-2265-49c7-af9a-047bcf384f21", + # "balanceEngine": "BITBAY", + # "tag": null + # } + # + currencyId = self.safe_string(depositAddress, 'currency') + address = self.safe_string(depositAddress, 'address') + self.check_address(address) + return { + 'info': depositAddress, + 'currency': self.safe_currency_code(currencyId, currency), + 'network': None, + 'address': address, + 'tag': self.safe_string(depositAddress, 'tag'), + } + + def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + + https://docs.zondacrypto.exchange/reference/deposit-addresses-for-crypto + + fetch the deposit address for a currency associated with self account + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.walletId]: Wallet id to filter deposit adresses. + :returns dict: an `address structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + } + response = self.v1_01PrivateGetApiPaymentsDepositsCryptoAddresses(self.extend(request, params)) + # + # { + # "status": "Ok", + # "data": [{ + # "address": "33u5YAEhQbYfjHHPsfMfCoSdEjfwYjVcBE", + # "currency": "BTC", + # "balanceId": "5d5d19e7-2265-49c7-af9a-047bcf384f21", + # "balanceEngine": "BITBAY", + # "tag": null + # } + # ] + # } + # + data = self.safe_value(response, 'data') + first = self.safe_dict(data, 0) + return self.parse_deposit_address(first, currency) + + def fetch_deposit_addresses(self, codes: Strings = None, params={}) -> List[DepositAddress]: + """ + + https://docs.zondacrypto.exchange/reference/deposit-addresses-for-crypto + + fetch deposit addresses for multiple currencies and chain types + :param str[]|None codes: zonda does not support filtering filtering by multiple codes and will ignore self parameter. + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `address structures ` + """ + self.load_markets() + response = self.v1_01PrivateGetApiPaymentsDepositsCryptoAddresses(params) + # + # { + # "status": "Ok", + # "data": [{ + # "address": "33u5YAEhQbYfjHHPsfMfCoSdEjfwYjVcBE", + # "currency": "BTC", + # "balanceId": "5d5d19e7-2265-49c7-af9a-047bcf384f21", + # "balanceEngine": "BITBAY", + # "tag": null + # } + # ] + # } + # + data = self.safe_list(response, 'data') + return self.parse_deposit_addresses(data, codes) + + def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + + https://docs.zondacrypto.exchange/reference/internal-transfer + + transfer currency internally between wallets on the same account + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account to transfer from + :param str toAccount: account to transfer to + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transfer structure ` + """ + self.load_markets() + currency = self.currency(code) + request: dict = { + 'source': fromAccount, + 'destination': toAccount, + 'currency': code, + 'funds': self.currency_to_precision(code, amount), + } + response = self.v1_01PrivatePostBalancesBITBAYBalanceTransferSourceDestination(self.extend(request, params)) + # + # { + # "status": "Ok", + # "from": { + # "id": "ad9397c5-3bd9-4372-82ba-22da6a90cb56", + # "userId": "4bc43956-423f-47fd-9faa-acd37c58ed9f", + # "availableFunds": 0.01803472, + # "totalFunds": 0.01804161, + # "lockedFunds": 0.00000689, + # "currency": "BTC", + # "type": "CRYPTO", + # "name": "BTC", + # "balanceEngine": "BITBAY" + # }, + # "to": { + # "id": "01931d52-536b-4ca5-a9f4-be28c86d0cc3", + # "userId": "4bc43956-423f-47fd-9faa-acd37c58ed9f", + # "availableFunds": 0.0001, + # "totalFunds": 0.0001, + # "lockedFunds": 0, + # "currency": "BTC", + # "type": "CRYPTO", + # "name": "Prowizja", + # "balanceEngine": "BITBAY" + # }, + # "errors": null + # } + # + transfer = self.parse_transfer(response, currency) + transferOptions = self.safe_value(self.options, 'transfer', {}) + fillResponseFromRequest = self.safe_bool(transferOptions, 'fillResponseFromRequest', True) + if fillResponseFromRequest: + transfer['amount'] = amount + return transfer + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + # + # { + # "status": "Ok", + # "from": { + # "id": "ad9397c5-3bd9-4372-82ba-22da6a90cb56", + # "userId": "4bc43956-423f-47fd-9faa-acd37c58ed9f", + # "availableFunds": 0.01803472, + # "totalFunds": 0.01804161, + # "lockedFunds": 0.00000689, + # "currency": "BTC", + # "type": "CRYPTO", + # "name": "BTC", + # "balanceEngine": "BITBAY" + # }, + # "to": { + # "id": "01931d52-536b-4ca5-a9f4-be28c86d0cc3", + # "userId": "4bc43956-423f-47fd-9faa-acd37c58ed9f", + # "availableFunds": 0.0001, + # "totalFunds": 0.0001, + # "lockedFunds": 0, + # "currency": "BTC", + # "type": "CRYPTO", + # "name": "Prowizja", + # "balanceEngine": "BITBAY" + # }, + # "errors": null + # } + # + status = self.safe_string(transfer, 'status') + fromAccount = self.safe_value(transfer, 'from', {}) + fromId = self.safe_string(fromAccount, 'id') + to = self.safe_value(transfer, 'to', {}) + toId = self.safe_string(to, 'id') + currencyId = self.safe_string(fromAccount, 'currency') + return { + 'info': transfer, + 'id': None, + 'timestamp': None, + 'datetime': None, + 'currency': self.safe_currency_code(currencyId, currency), + 'amount': None, + 'fromAccount': fromId, + 'toAccount': toId, + 'status': self.parse_transfer_status(status), + } + + def parse_transfer_status(self, status: Str) -> Str: + statuses: dict = { + 'Ok': 'ok', + 'Fail': 'failed', + } + return self.safe_string(statuses, status, status) + + def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction: + """ + + https://docs.zondacrypto.exchange/reference/crypto-withdrawal-1 + + make a withdrawal + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.check_address(address) + self.load_markets() + response = None + currency = self.currency(code) + request: dict = { + 'currency': currency['id'], + 'amount': amount, + 'address': address, + # request['balanceId'] = params['balanceId'] # Wallet id used for withdrawal. If not provided, any BITBAY wallet with sufficient funds is used. If BITBAYPAY wallet should be used parameter must be explicitly specified. + } + if self.is_fiat(code): + # request['swift'] = params['swift'] # Bank identifier, if required. + response = self.v1_01PrivatePostApiPaymentsWithdrawalsFiat(self.extend(request, params)) + else: + if tag is not None: + request['tag'] = tag + response = self.v1_01PrivatePostApiPaymentsWithdrawalsCrypto(self.extend(request, params)) + # + # { + # "status": "Ok", + # "data": { + # "id": "65e01087-afb0-4ab2-afdb-cc925e360296" + # } + # } + # + data = self.safe_dict(response, 'data') + return self.parse_transaction(data, currency) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # withdraw + # + # { + # "id": "65e01087-afb0-4ab2-afdb-cc925e360296" + # } + # + currency = self.safe_currency(None, currency) + return { + 'id': self.safe_string(transaction, 'id'), + 'txid': None, + 'timestamp': None, + 'datetime': None, + 'network': None, + 'addressFrom': None, + 'address': None, + 'addressTo': None, + 'amount': None, + 'type': None, + 'currency': currency['code'], + 'status': None, + 'updated': None, + 'tagFrom': None, + 'tag': None, + 'tagTo': None, + 'comment': None, + 'internal': None, + 'fee': None, + 'info': transaction, + } + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + url = self.implode_hostname(self.urls['api'][api]) + if api == 'public': + query = self.omit(params, self.extract_params(path)) + url += '/' + self.implode_params(path, params) + '.json' + if query: + url += '?' + self.urlencode(query) + elif api == 'v1_01Public': + query = self.omit(params, self.extract_params(path)) + url += '/' + self.implode_params(path, params) + if query: + url += '?' + self.urlencode(query) + elif api == 'v1_01Private': + self.check_required_credentials() + query = self.omit(params, self.extract_params(path)) + url += '/' + self.implode_params(path, params) + nonce = str(self.milliseconds()) + payload: Str = None + if method != 'POST': + if query: + url += '?' + self.urlencode(query) + payload = self.apiKey + nonce + elif body is None: + body = self.json(query) + payload = self.apiKey + nonce + body + headers = { + 'Request-Timestamp': nonce, + 'Operation-Id': self.uuid(), + 'API-Key': self.apiKey, + 'API-Hash': self.hmac(self.encode(payload), self.encode(self.secret), hashlib.sha512), + 'Content-Type': 'application/json', + } + else: + self.check_required_credentials() + body = self.urlencode(self.extend({ + 'method': path, + 'moment': self.nonce(), + }, params)) + headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + 'API-Key': self.apiKey, + 'API-Hash': self.hmac(self.encode(body), self.encode(self.secret), hashlib.sha512), + } + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if response is None: + return None # fallback to default error handler + if 'code' in response: + # + # bitbay returns the integer "success": 1 key from their private API + # or an integer "code" value from 0 to 510 and an error message + # + # {"success": 1, ...} + # {'code': 502, "message": "Invalid sign"} + # {'code': 0, "message": "offer funds not exceeding minimums"} + # + # 400 At least one parameter wasn't set + # 401 Invalid order type + # 402 No orders with specified currencies + # 403 Invalid payment currency name + # 404 Error. Wrong transaction type + # 405 Order with self id doesn't exist + # 406 No enough money or crypto + # 408 Invalid currency name + # 501 Invalid public key + # 502 Invalid sign + # 503 Invalid moment parameter. Request time doesn't match current server time + # 504 Invalid method + # 505 Key has no permission for self action + # 506 Account locked. Please contact with customer service + # 509 The BIC/SWIFT is required for self currency + # 510 Invalid market name + # + code = self.safe_string(response, 'code') # always an integer + feedback = self.id + ' ' + body + self.throw_exactly_matched_exception(self.exceptions, code, feedback) + raise ExchangeError(feedback) + elif 'status' in response: + # + # {"status":"Fail","errors":["OFFER_FUNDS_NOT_EXCEEDING_MINIMUMS"]} + # + status = self.safe_string(response, 'status') + if status == 'Fail': + errors = self.safe_value(response, 'errors') + feedback = self.id + ' ' + body + for i in range(0, len(errors)): + error = errors[i] + self.throw_exactly_matched_exception(self.exceptions, error, feedback) + raise ExchangeError(feedback) + return None diff --git a/ci-requirements.txt b/ci-requirements.txt new file mode 100644 index 0000000..676ea3a --- /dev/null +++ b/ci-requirements.txt @@ -0,0 +1,15 @@ +setuptools>=60.9.0 +certifi>=2018.1.18 +requests>=2.18.4 +cryptography>=2.6.1 +typing_extensions>=4.4.0 +aiohttp>=3.11.18 +aiodns>=1.1.1 +yarl>=1.7.2 +ruff==0.0.292 +tox>=4.8.0 +mypy==1.6.1 +pyopenssl +psutil +protobuf==5.29.5 +coincurve==20.0.0 diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..1613924 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,2 @@ +python setup.py sdist bdist_wheel +twine upload dist/* -u __token__ -p ${PYPI_TOKEN} \ No newline at end of file diff --git a/fastflake.sh b/fastflake.sh new file mode 100755 index 0000000..fa7e97c --- /dev/null +++ b/fastflake.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +# saves time over the other running flake8 on all the files + +echo -e '\n checking syntax using flake8 over the following files:\n' +git diff --name-only --relative origin/master | sed -n '/py$/p' | xargs -rn 1000 flake8 --ignore=F722,F841,F821,W504,E402,E501,E275,E902 --exclude static_dependencies,node_modules,.tox,build diff --git a/mt5_test.log b/mt5_test.log new file mode 100644 index 0000000..4ad987b --- /dev/null +++ b/mt5_test.log @@ -0,0 +1,829 @@ +2025-11-15 23:10:54,803 - MT5-Test - INFO - 🚀 开始同步版本测试 +2025-11-15 23:10:54,804 - MT5-Test - INFO - +================================================== +2025-11-15 23:10:54,804 - MT5-Test - INFO - 执行测试: test_connection +2025-11-15 23:10:54,804 - MT5-Test - INFO - ================================================== +2025-11-15 23:10:54,804 - MT5-Test - INFO - 测试服务器连接... +2025-11-15 23:10:54,808 - MT5-Test - INFO - Ping 结果: True +2025-11-15 23:10:54,809 - MT5-Test - INFO - ✅ 通过 - test_connection +2025-11-15 23:10:54,809 - MT5-Test - INFO - +================================================== +2025-11-15 23:10:54,809 - MT5-Test - INFO - 执行测试: test_token_management +2025-11-15 23:10:54,809 - MT5-Test - INFO - ================================================== +2025-11-15 23:10:54,809 - MT5-Test - INFO - 测试 Token 获取... +2025-11-15 23:10:55,807 - MT5-Test - INFO - 获取到 Token: bc8d0e29-89e6-4bbb-a9d0-a933c31d3d24 +2025-11-15 23:10:55,808 - MT5-Test - INFO - 测试连接检查... +2025-11-15 23:12:55,809 - MT5-Test - INFO - 🚀 开始同步版本测试 +2025-11-15 23:12:55,809 - MT5-Test - INFO - +================================================== +2025-11-15 23:12:55,809 - MT5-Test - INFO - 执行测试: test_connection +2025-11-15 23:12:55,810 - MT5-Test - INFO - ================================================== +2025-11-15 23:12:55,810 - MT5-Test - INFO - ❌ 失败 - test_connection +2025-11-15 23:12:55,810 - MT5-Test - INFO - +================================================== +2025-11-15 23:12:55,810 - MT5-Test - INFO - 执行测试: test_token_management +2025-11-15 23:12:55,810 - MT5-Test - INFO - ================================================== +2025-11-15 23:12:55,810 - MT5-Test - INFO - 测试 Token 获取... +2025-11-15 23:12:55,815 - MT5-Test - INFO - 获取到 Token: bc8d0e29-89e6-4bbb-a9d0-a933c31d3d24 +2025-11-15 23:12:55,815 - MT5-Test - INFO - 测试连接检查... +2025-11-15 23:13:12,360 - MT5-Test - INFO - 🚀 开始同步版本测试 +2025-11-15 23:13:12,360 - MT5-Test - INFO - +================================================== +2025-11-15 23:13:12,360 - MT5-Test - INFO - 执行测试: test_connection +2025-11-15 23:13:12,360 - MT5-Test - INFO - ================================================== +2025-11-15 23:13:12,360 - MT5-Test - INFO - ✅ 通过 - test_connection +2025-11-15 23:13:12,360 - MT5-Test - INFO - +================================================== +2025-11-15 23:13:12,360 - MT5-Test - INFO - 执行测试: test_token_management +2025-11-15 23:13:12,360 - MT5-Test - INFO - ================================================== +2025-11-15 23:13:12,360 - MT5-Test - INFO - 测试 Token 获取... +2025-11-15 23:13:12,365 - MT5-Test - INFO - 获取到 Token: bc8d0e29-89e6-4bbb-a9d0-a933c31d3d24 +2025-11-15 23:13:12,365 - MT5-Test - INFO - 测试连接检查... +2025-11-15 23:15:24,931 - MT5-Test - INFO - 🚀 开始同步版本测试 +2025-11-15 23:15:24,931 - MT5-Test - INFO - +================================================== +2025-11-15 23:15:24,931 - MT5-Test - INFO - 执行测试: test_connection +2025-11-15 23:15:24,931 - MT5-Test - INFO - ================================================== +2025-11-15 23:15:24,931 - MT5-Test - INFO - ✅ 通过 - test_connection +2025-11-15 23:15:24,931 - MT5-Test - INFO - +================================================== +2025-11-15 23:15:24,932 - MT5-Test - INFO - 执行测试: test_token_management +2025-11-15 23:15:24,932 - MT5-Test - INFO - ================================================== +2025-11-15 23:15:24,932 - MT5-Test - INFO - 测试 Token 获取... +2025-11-15 23:15:24,937 - MT5-Test - INFO - 获取到 Token: bc8d0e29-89e6-4bbb-a9d0-a933c31d3d24 +2025-11-15 23:15:24,937 - MT5-Test - INFO - 测试连接检查... +2025-11-15 23:18:02,298 - MT5-Test - INFO - 🚀 开始同步版本测试 +2025-11-15 23:18:02,298 - MT5-Test - INFO - +================================================== +2025-11-15 23:18:02,298 - MT5-Test - INFO - 执行测试: test_connection +2025-11-15 23:18:02,298 - MT5-Test - INFO - ================================================== +2025-11-15 23:18:02,298 - MT5-Test - INFO - 测试服务器连接... +2025-11-15 23:18:02,303 - MT5-Test - INFO - Ping 结果: True +2025-11-15 23:18:02,303 - MT5-Test - INFO - ✅ 通过 - test_connection +2025-11-15 23:18:02,303 - MT5-Test - INFO - +================================================== +2025-11-15 23:18:02,303 - MT5-Test - INFO - 执行测试: test_token_management +2025-11-15 23:18:02,303 - MT5-Test - INFO - ================================================== +2025-11-15 23:18:02,304 - MT5-Test - INFO - 测试 Token 获取... +2025-11-15 23:18:03,302 - MT5-Test - INFO - 获取到 Token: bc8d0e29-89e6-4bbb-a9d0-a933c31d3d24 +2025-11-15 23:18:03,302 - MT5-Test - INFO - 测试连接检查... +2025-11-15 23:19:16,429 - MT5-Test - INFO - 🚀 开始同步版本测试 +2025-11-15 23:19:16,430 - MT5-Test - INFO - +================================================== +2025-11-15 23:19:16,430 - MT5-Test - INFO - 执行测试: test_connection +2025-11-15 23:19:16,430 - MT5-Test - INFO - ================================================== +2025-11-15 23:19:16,430 - MT5-Test - INFO - 测试服务器连接... +2025-11-15 23:19:16,434 - MT5-Test - INFO - Ping 结果: True +2025-11-15 23:19:16,434 - MT5-Test - INFO - ✅ 通过 - test_connection +2025-11-15 23:19:16,434 - MT5-Test - INFO - +================================================== +2025-11-15 23:19:16,435 - MT5-Test - INFO - 执行测试: test_token_management +2025-11-15 23:19:16,435 - MT5-Test - INFO - ================================================== +2025-11-15 23:19:16,435 - MT5-Test - INFO - 测试 Token 获取... +2025-11-15 23:19:17,433 - MT5-Test - INFO - 获取到 Token: bc8d0e29-89e6-4bbb-a9d0-a933c31d3d24 +2025-11-15 23:19:17,433 - MT5-Test - INFO - 测试服务器时区... +2025-11-15 23:19:18,434 - MT5-Test - INFO - 服务器时区: 0 +2025-11-15 23:19:18,434 - MT5-Test - INFO - ✅ 通过 - test_token_management +2025-11-15 23:19:18,434 - MT5-Test - INFO - +================================================== +2025-11-15 23:19:18,434 - MT5-Test - INFO - 执行测试: test_markets +2025-11-15 23:19:18,435 - MT5-Test - INFO - ================================================== +2025-11-15 23:19:18,435 - MT5-Test - INFO - 获取交易对列表... +2025-11-15 23:19:19,457 - MT5-Test - INFO - 获取到 338 个交易对 +2025-11-15 23:19:19,457 - MT5-Test - INFO - 1. USD/RUB - USD/RUB +2025-11-15 23:19:19,457 - MT5-Test - INFO - 2. USD/AED - USD/AED +2025-11-15 23:19:19,457 - MT5-Test - INFO - 3. USD/AMD - USD/AMD +2025-11-15 23:19:19,458 - MT5-Test - INFO - 4. USD/ARS - USD/ARS +2025-11-15 23:19:19,458 - MT5-Test - INFO - 5. USD/AZN - USD/AZN +2025-11-15 23:19:19,458 - MT5-Test - INFO - ... 还有 333 个交易对 +2025-11-15 23:19:19,458 - MT5-Test - INFO - ✅ 通过 - test_markets +2025-11-15 23:19:19,458 - MT5-Test - INFO - +================================================== +2025-11-15 23:19:19,458 - MT5-Test - INFO - 执行测试: test_balance +2025-11-15 23:19:19,458 - MT5-Test - INFO - ================================================== +2025-11-15 23:19:19,458 - MT5-Test - INFO - 获取账户余额... +2025-11-15 23:19:20,434 - MT5-Test - INFO - 余额信息: {'info': {'balance': '8508.85', 'credit': '0.0', 'profit': '-3.29', 'equity': '8505.56', 'margin': '4.80212', 'freeMargin': '8500.75788', 'marginLevel': '177120.93825227188', 'leverage': '2000.0', 'currency': 'USD', 'method': 'Hedging', 'type': 'demo', 'isInvestor': False}, 'timestamp': None, 'datetime': None, 'USDT': {'free': 8500.75788, 'used': 4.80212, 'total': 8508.85}, 'free': {'USDT': 8500.75788}, 'used': {'USDT': 4.80212}, 'total': {'USDT': 8508.85}} +2025-11-15 23:19:20,435 - MT5-Test - INFO - USDT 余额 - 总额: 8508.85, 可用: 8500.75788, 占用: 4.80212 +2025-11-15 23:19:20,435 - MT5-Test - INFO - ✅ 通过 - test_balance +2025-11-15 23:19:20,435 - MT5-Test - INFO - +================================================== +2025-11-15 23:19:20,435 - MT5-Test - INFO - 执行测试: test_ticker +2025-11-15 23:19:20,435 - MT5-Test - INFO - ================================================== +2025-11-15 23:19:20,435 - MT5-Test - INFO - 获取 EUR/USD 行情... +2025-11-15 23:19:21,436 - MT5-Test - INFO - EUR/USD: 买=1.16205, 卖=1.16219, 最后=0.0 +2025-11-15 23:19:21,436 - MT5-Test - INFO - 获取 GBP/USD 行情... +2025-11-15 23:19:22,490 - MT5-Test - INFO - GBP/USD: 买=1.31737, 卖=1.31744, 最后=0.0 +2025-11-15 23:19:22,490 - MT5-Test - INFO - 获取 USD/JPY 行情... +2025-11-15 23:19:23,490 - MT5-Test - INFO - USD/JPY: 买=154.531, 卖=154.543, 最后=0.0 +2025-11-15 23:19:23,490 - MT5-Test - INFO - 测试批量获取行情... +2025-11-15 23:19:23,490 - MT5-Test - ERROR - 行情数据测试失败: mt5 fetchTickers() is not supported yet +2025-11-15 23:19:23,490 - MT5-Test - INFO - ❌ 失败 - test_ticker +2025-11-15 23:19:23,490 - MT5-Test - INFO - +================================================== +2025-11-15 23:19:23,491 - MT5-Test - INFO - 执行测试: test_account_details +2025-11-15 23:19:23,491 - MT5-Test - INFO - ================================================== +2025-11-15 23:19:23,491 - MT5-Test - INFO - 获取账户详情... +2025-11-15 23:19:24,434 - MT5-Test - INFO - 账户详情: {'serverName': 'Exness-MT5Trial5', 'user': '76888962', 'host': '18.163.85.196', 'port': 443, 'serverTime': '2025-11-15T15:19:24.4335788Z', 'serverTimeZone': 0, 'company': 'Exness Technologies Ltd', 'currency': 'USD', 'accountName': '先锋账户-模拟测试', 'group': None, 'accountType': 'demo', 'accountLeverage': 2000, 'accountMethod': 'Hedging', 'isInvestor': False} +2025-11-15 23:19:24,434 - MT5-Test - INFO - serverName: Exness-MT5Trial5 +2025-11-15 23:19:24,435 - MT5-Test - INFO - user: 76888962 +2025-11-15 23:19:24,435 - MT5-Test - INFO - currency: USD +2025-11-15 23:19:24,435 - MT5-Test - INFO - accountLeverage: 2000 +2025-11-15 23:19:24,435 - MT5-Test - INFO - ✅ 通过 - test_account_details +2025-11-15 23:19:24,435 - MT5-Test - INFO - +================================================== +2025-11-15 23:19:24,435 - MT5-Test - INFO - 执行测试: test_orders +2025-11-15 23:19:24,436 - MT5-Test - INFO - ================================================== +2025-11-15 23:19:24,436 - MT5-Test - INFO - 获取未平仓订单... +2025-11-15 23:19:25,435 - MT5-Test - INFO - 未平仓订单数量: 2 +2025-11-15 23:19:25,435 - MT5-Test - INFO - 订单 881293557: BTCUSD sell market closed +2025-11-15 23:19:25,435 - MT5-Test - INFO - 订单 881352305: BTCUSD sell market closed +2025-11-15 23:19:25,435 - MT5-Test - INFO - 获取已平仓订单... +2025-11-15 23:19:26,438 - MT5-Test - INFO - 已平仓订单数量: 4 +2025-11-15 23:19:26,439 - MT5-Test - INFO - ✅ 通过 - test_orders +2025-11-15 23:19:26,439 - MT5-Test - INFO - +================================================== +2025-11-15 23:19:26,439 - MT5-Test - INFO - 执行测试: test_order_operations +2025-11-15 23:19:26,439 - MT5-Test - INFO - ================================================== +2025-11-15 23:19:26,439 - MT5-Test - INFO - 测试订单创建参数验证... +2025-11-15 23:19:26,439 - MT5-Test - INFO - ✅ 订单参数验证通过 +2025-11-15 23:19:26,439 - MT5-Test - INFO - 注意: 实际下单测试需要在真实环境中进行 +2025-11-15 23:19:26,439 - MT5-Test - INFO - ✅ 通过 - test_order_operations +2025-11-15 23:19:26,439 - MT5-Test - INFO - +============================================================ +2025-11-15 23:19:26,439 - MT5-Test - INFO - 📊 同步版本测试总结 +2025-11-15 23:19:26,439 - MT5-Test - INFO - ============================================================ +2025-11-15 23:19:26,439 - MT5-Test - INFO - test_connection: ✅ 通过 +2025-11-15 23:19:26,439 - MT5-Test - INFO - test_token_management: ✅ 通过 +2025-11-15 23:19:26,439 - MT5-Test - INFO - test_markets: ✅ 通过 +2025-11-15 23:19:26,439 - MT5-Test - INFO - test_balance: ✅ 通过 +2025-11-15 23:19:26,439 - MT5-Test - INFO - test_ticker: ❌ 失败 +2025-11-15 23:19:26,439 - MT5-Test - INFO - test_account_details: ✅ 通过 +2025-11-15 23:19:26,439 - MT5-Test - INFO - test_orders: ✅ 通过 +2025-11-15 23:19:26,439 - MT5-Test - INFO - test_order_operations: ✅ 通过 +2025-11-15 23:19:26,439 - MT5-Test - INFO - +总体结果: 7/8 通过 +2025-11-15 23:19:26,439 - MT5-Test - INFO - ⚠️ 部分测试失败,请检查日志 +2025-11-15 23:19:26,440 - MT5-Test - INFO - +🚀 开始异步版本测试 +2025-11-15 23:19:26,443 - MT5-Test - INFO - +================================================== +2025-11-15 23:19:26,443 - MT5-Test - INFO - 执行测试: test_connection_async +2025-11-15 23:19:26,443 - MT5-Test - INFO - ================================================== +2025-11-15 23:19:26,443 - MT5-Test - INFO - 测试异步 Token 获取... +2025-11-15 23:19:26,453 - MT5-Test - INFO - 获取到 Token: bc8d0e29-89e6-4bbb-a9d0-a933c31d3d24 +2025-11-15 23:19:26,453 - MT5-Test - INFO - ✅ 通过 - test_connection_async +2025-11-15 23:19:26,453 - MT5-Test - INFO - +================================================== +2025-11-15 23:19:26,453 - MT5-Test - INFO - 执行测试: test_markets_async +2025-11-15 23:19:26,453 - MT5-Test - INFO - ================================================== +2025-11-15 23:19:26,453 - MT5-Test - INFO - 获取异步交易对列表... +2025-11-15 23:19:27,480 - MT5-Test - INFO - 获取到 338 个交易对 +2025-11-15 23:19:27,481 - MT5-Test - INFO - ✅ 通过 - test_markets_async +2025-11-15 23:19:27,481 - MT5-Test - INFO - +================================================== +2025-11-15 23:19:27,481 - MT5-Test - INFO - 执行测试: test_balance_async +2025-11-15 23:19:27,481 - MT5-Test - INFO - ================================================== +2025-11-15 23:19:27,481 - MT5-Test - INFO - 获取异步账户余额... +2025-11-15 23:19:28,483 - MT5-Test - INFO - 余额信息: {'info': {'balance': '8508.85', 'credit': '0.0', 'profit': '-3.0300000000000002', 'equity': '8505.82', 'margin': '4.80212', 'freeMargin': '8501.01788', 'marginLevel': '177126.35252763363', 'leverage': '2000.0', 'currency': 'USD', 'method': 'Hedging', 'type': 'demo', 'isInvestor': False}, 'timestamp': None, 'datetime': None, 'USDT': {'free': 8501.01788, 'used': 4.80212, 'total': 8508.85}, 'free': {'USDT': 8501.01788}, 'used': {'USDT': 4.80212}, 'total': {'USDT': 8508.85}} +2025-11-15 23:19:28,483 - MT5-Test - INFO - ✅ 通过 - test_balance_async +2025-11-15 23:19:28,483 - MT5-Test - INFO - +================================================== +2025-11-15 23:19:28,483 - MT5-Test - INFO - 执行测试: test_ticker_async +2025-11-15 23:19:28,483 - MT5-Test - INFO - ================================================== +2025-11-15 23:19:28,483 - MT5-Test - INFO - 获取异步行情... +2025-11-15 23:19:29,543 - MT5-Test - INFO - EUR/USD: 买=1.16205, 卖=1.16219 +2025-11-15 23:19:29,543 - MT5-Test - INFO - ✅ 通过 - test_ticker_async +2025-11-15 23:19:29,543 - MT5-Test - INFO - +================================================== +2025-11-15 23:19:29,543 - MT5-Test - INFO - 执行测试: test_orders_async +2025-11-15 23:19:29,543 - MT5-Test - INFO - ================================================== +2025-11-15 23:19:29,543 - MT5-Test - INFO - 获取异步未平仓订单... +2025-11-15 23:19:30,547 - MT5-Test - ERROR - 异步订单查询测试失败: mt5 safeMarket() requires a fourth argument for BTCUSD to disambiguate between different markets with the same market id +2025-11-15 23:19:30,547 - MT5-Test - INFO - ❌ 失败 - test_orders_async +2025-11-15 23:19:30,798 - MT5-Test - INFO - +============================================================ +2025-11-15 23:19:30,798 - MT5-Test - INFO - 📊 异步版本测试总结 +2025-11-15 23:19:30,798 - MT5-Test - INFO - ============================================================ +2025-11-15 23:19:30,798 - MT5-Test - INFO - test_connection_async: ✅ 通过 +2025-11-15 23:19:30,798 - MT5-Test - INFO - test_markets_async: ✅ 通过 +2025-11-15 23:19:30,798 - MT5-Test - INFO - test_balance_async: ✅ 通过 +2025-11-15 23:19:30,798 - MT5-Test - INFO - test_ticker_async: ✅ 通过 +2025-11-15 23:19:30,798 - MT5-Test - INFO - test_orders_async: ❌ 失败 +2025-11-15 23:19:30,798 - MT5-Test - INFO - +总体结果: 4/5 通过 +2025-11-15 23:19:30,802 - MT5-Test - INFO - +🔌 开始 WebSocket 订单监听测试 +2025-11-15 23:19:30,802 - MT5-Test - INFO - 监听订单更新... +2025-11-15 23:19:30,812 - MT5-Test - ERROR - 订单监听错误: unhashable type: 'dict' +2025-11-15 23:19:31,067 - MT5-Test - INFO - +💰 开始 WebSocket 余额监听测试 +2025-11-15 23:19:31,068 - MT5-Test - INFO - 监听余额更新... +2025-11-15 23:19:31,077 - MT5-Test - ERROR - 余额监听错误: unhashable type: 'dict' +2025-11-15 23:24:43,654 - MT5-Test - INFO - 🚀 开始同步版本测试 +2025-11-15 23:24:43,654 - MT5-Test - INFO - +================================================== +2025-11-15 23:24:43,654 - MT5-Test - INFO - 执行测试: test_connection +2025-11-15 23:24:43,654 - MT5-Test - INFO - ================================================== +2025-11-15 23:24:43,654 - MT5-Test - INFO - 测试服务器连接... +2025-11-15 23:24:43,659 - MT5-Test - INFO - Ping 结果: True +2025-11-15 23:24:43,659 - MT5-Test - INFO - ✅ 通过 - test_connection +2025-11-15 23:24:43,659 - MT5-Test - INFO - +================================================== +2025-11-15 23:24:43,659 - MT5-Test - INFO - 执行测试: test_token_management +2025-11-15 23:24:43,659 - MT5-Test - INFO - ================================================== +2025-11-15 23:24:43,659 - MT5-Test - INFO - 测试 Token 获取... +2025-11-15 23:24:44,657 - MT5-Test - INFO - 获取到 Token: bc8d0e29-89e6-4bbb-a9d0-a933c31d3d24 +2025-11-15 23:24:44,657 - MT5-Test - INFO - 测试服务器时区... +2025-11-15 23:24:45,658 - MT5-Test - INFO - 服务器时区: 0 +2025-11-15 23:24:45,658 - MT5-Test - INFO - ✅ 通过 - test_token_management +2025-11-15 23:24:45,658 - MT5-Test - INFO - +================================================== +2025-11-15 23:24:45,658 - MT5-Test - INFO - 执行测试: test_markets +2025-11-15 23:24:45,658 - MT5-Test - INFO - ================================================== +2025-11-15 23:24:45,658 - MT5-Test - INFO - 获取交易对列表... +2025-11-15 23:24:46,682 - MT5-Test - INFO - 获取到 338 个交易对 +2025-11-15 23:24:46,682 - MT5-Test - INFO - 1. USD/RUB - USD/RUB +2025-11-15 23:24:46,682 - MT5-Test - INFO - 2. USD/AED - USD/AED +2025-11-15 23:24:46,682 - MT5-Test - INFO - 3. USD/AMD - USD/AMD +2025-11-15 23:24:46,682 - MT5-Test - INFO - 4. USD/ARS - USD/ARS +2025-11-15 23:24:46,682 - MT5-Test - INFO - 5. USD/AZN - USD/AZN +2025-11-15 23:24:46,683 - MT5-Test - INFO - ... 还有 333 个交易对 +2025-11-15 23:24:46,683 - MT5-Test - INFO - ✅ 通过 - test_markets +2025-11-15 23:24:46,683 - MT5-Test - INFO - +================================================== +2025-11-15 23:24:46,683 - MT5-Test - INFO - 执行测试: test_balance +2025-11-15 23:24:46,683 - MT5-Test - INFO - ================================================== +2025-11-15 23:24:46,683 - MT5-Test - INFO - 获取账户余额... +2025-11-15 23:24:47,659 - MT5-Test - INFO - 余额信息: {'info': {'balance': '8508.85', 'credit': '0.0', 'profit': '-3.4299999999999997', 'equity': '8505.42', 'margin': '4.80212', 'freeMargin': '8500.61788', 'marginLevel': '177118.02287323098', 'leverage': '2000.0', 'currency': 'USD', 'method': 'Hedging', 'type': 'demo', 'isInvestor': False}, 'timestamp': None, 'datetime': None, 'USDT': {'free': 8500.61788, 'used': 4.80212, 'total': 8508.85}, 'free': {'USDT': 8500.61788}, 'used': {'USDT': 4.80212}, 'total': {'USDT': 8508.85}} +2025-11-15 23:24:47,659 - MT5-Test - INFO - USDT 余额 - 总额: 8508.85, 可用: 8500.61788, 占用: 4.80212 +2025-11-15 23:24:47,660 - MT5-Test - INFO - ✅ 通过 - test_balance +2025-11-15 23:24:47,660 - MT5-Test - INFO - +================================================== +2025-11-15 23:24:47,660 - MT5-Test - INFO - 执行测试: test_ticker +2025-11-15 23:24:47,660 - MT5-Test - INFO - ================================================== +2025-11-15 23:24:47,660 - MT5-Test - INFO - 获取 EUR/USD 行情... +2025-11-15 23:24:48,661 - MT5-Test - INFO - EUR/USD: 买=1.16205, 卖=1.16219, 最后=0.0 +2025-11-15 23:24:48,661 - MT5-Test - INFO - 获取 GBP/USD 行情... +2025-11-15 23:24:49,660 - MT5-Test - INFO - GBP/USD: 买=1.31737, 卖=1.31744, 最后=0.0 +2025-11-15 23:24:49,660 - MT5-Test - INFO - 获取 USD/JPY 行情... +2025-11-15 23:24:50,659 - MT5-Test - INFO - USD/JPY: 买=154.531, 卖=154.543, 最后=0.0 +2025-11-15 23:24:50,659 - MT5-Test - INFO - 测试批量获取行情... +2025-11-15 23:24:51,667 - MT5-Test - INFO - 批量获取到 0 个行情 +2025-11-15 23:24:51,667 - MT5-Test - INFO - ✅ 通过 - test_ticker +2025-11-15 23:24:51,667 - MT5-Test - INFO - +================================================== +2025-11-15 23:24:51,667 - MT5-Test - INFO - 执行测试: test_account_details +2025-11-15 23:24:51,667 - MT5-Test - INFO - ================================================== +2025-11-15 23:24:51,667 - MT5-Test - INFO - 获取账户详情... +2025-11-15 23:24:52,659 - MT5-Test - INFO - 账户详情: {'serverName': 'Exness-MT5Trial5', 'user': '76888962', 'host': '18.163.85.196', 'port': 443, 'serverTime': '2025-11-15T15:24:52.6589361Z', 'serverTimeZone': 0, 'company': 'Exness Technologies Ltd', 'currency': 'USD', 'accountName': '先锋账户-模拟测试', 'group': None, 'accountType': 'demo', 'accountLeverage': 2000, 'accountMethod': 'Hedging', 'isInvestor': False} +2025-11-15 23:24:52,660 - MT5-Test - INFO - serverName: Exness-MT5Trial5 +2025-11-15 23:24:52,660 - MT5-Test - INFO - user: 76888962 +2025-11-15 23:24:52,660 - MT5-Test - INFO - currency: USD +2025-11-15 23:24:52,660 - MT5-Test - INFO - accountLeverage: 2000 +2025-11-15 23:24:52,660 - MT5-Test - INFO - ✅ 通过 - test_account_details +2025-11-15 23:24:52,660 - MT5-Test - INFO - +================================================== +2025-11-15 23:24:52,660 - MT5-Test - INFO - 执行测试: test_orders +2025-11-15 23:24:52,660 - MT5-Test - INFO - ================================================== +2025-11-15 23:24:52,660 - MT5-Test - INFO - 获取未平仓订单... +2025-11-15 23:24:53,660 - MT5-Test - INFO - 未平仓订单数量: 2 +2025-11-15 23:24:53,660 - MT5-Test - INFO - 订单 881293557: BTCUSD sell market closed +2025-11-15 23:24:53,660 - MT5-Test - INFO - 订单 881352305: BTCUSD sell market closed +2025-11-15 23:24:53,660 - MT5-Test - INFO - 获取已平仓订单... +2025-11-15 23:24:54,660 - MT5-Test - INFO - 已平仓订单数量: 4 +2025-11-15 23:24:54,661 - MT5-Test - INFO - ✅ 通过 - test_orders +2025-11-15 23:24:54,661 - MT5-Test - INFO - +================================================== +2025-11-15 23:24:54,661 - MT5-Test - INFO - 执行测试: test_order_operations +2025-11-15 23:24:54,661 - MT5-Test - INFO - ================================================== +2025-11-15 23:24:54,661 - MT5-Test - INFO - 测试订单创建参数验证... +2025-11-15 23:24:54,661 - MT5-Test - INFO - ✅ 订单参数验证通过 +2025-11-15 23:24:54,661 - MT5-Test - INFO - 注意: 实际下单测试需要在真实环境中进行 +2025-11-15 23:24:54,661 - MT5-Test - INFO - ✅ 通过 - test_order_operations +2025-11-15 23:24:54,661 - MT5-Test - INFO - +============================================================ +2025-11-15 23:24:54,661 - MT5-Test - INFO - 📊 同步版本测试总结 +2025-11-15 23:24:54,661 - MT5-Test - INFO - ============================================================ +2025-11-15 23:24:54,661 - MT5-Test - INFO - test_connection: ✅ 通过 +2025-11-15 23:24:54,661 - MT5-Test - INFO - test_token_management: ✅ 通过 +2025-11-15 23:24:54,661 - MT5-Test - INFO - test_markets: ✅ 通过 +2025-11-15 23:24:54,661 - MT5-Test - INFO - test_balance: ✅ 通过 +2025-11-15 23:24:54,661 - MT5-Test - INFO - test_ticker: ✅ 通过 +2025-11-15 23:24:54,661 - MT5-Test - INFO - test_account_details: ✅ 通过 +2025-11-15 23:24:54,661 - MT5-Test - INFO - test_orders: ✅ 通过 +2025-11-15 23:24:54,661 - MT5-Test - INFO - test_order_operations: ✅ 通过 +2025-11-15 23:24:54,661 - MT5-Test - INFO - +总体结果: 8/8 通过 +2025-11-15 23:24:54,661 - MT5-Test - INFO - 🎉 所有测试通过! +2025-11-15 23:24:54,661 - MT5-Test - INFO - +🚀 开始异步版本测试 +2025-11-15 23:24:54,665 - MT5-Test - INFO - +================================================== +2025-11-15 23:24:54,665 - MT5-Test - INFO - 执行测试: test_connection_async +2025-11-15 23:24:54,665 - MT5-Test - INFO - ================================================== +2025-11-15 23:24:54,665 - MT5-Test - INFO - 测试异步 Token 获取... +2025-11-15 23:24:54,675 - MT5-Test - INFO - 获取到 Token: bc8d0e29-89e6-4bbb-a9d0-a933c31d3d24 +2025-11-15 23:24:54,675 - MT5-Test - INFO - ✅ 通过 - test_connection_async +2025-11-15 23:24:54,675 - MT5-Test - INFO - +================================================== +2025-11-15 23:24:54,675 - MT5-Test - INFO - 执行测试: test_markets_async +2025-11-15 23:24:54,675 - MT5-Test - INFO - ================================================== +2025-11-15 23:24:54,675 - MT5-Test - INFO - 获取异步交易对列表... +2025-11-15 23:24:55,700 - MT5-Test - INFO - 获取到 338 个交易对 +2025-11-15 23:24:55,700 - MT5-Test - INFO - ✅ 通过 - test_markets_async +2025-11-15 23:24:55,700 - MT5-Test - INFO - +================================================== +2025-11-15 23:24:55,700 - MT5-Test - INFO - 执行测试: test_balance_async +2025-11-15 23:24:55,700 - MT5-Test - INFO - ================================================== +2025-11-15 23:24:55,700 - MT5-Test - INFO - 获取异步账户余额... +2025-11-15 23:24:56,703 - MT5-Test - INFO - 余额信息: {'info': {'balance': '8508.85', 'credit': '0.0', 'profit': '-3.48', 'equity': '8505.37', 'margin': '4.80212', 'freeMargin': '8500.56788', 'marginLevel': '177116.98166643066', 'leverage': '2000.0', 'currency': 'USD', 'method': 'Hedging', 'type': 'demo', 'isInvestor': False}, 'timestamp': None, 'datetime': None, 'USDT': {'free': 8500.56788, 'used': 4.80212, 'total': 8508.85}, 'free': {'USDT': 8500.56788}, 'used': {'USDT': 4.80212}, 'total': {'USDT': 8508.85}} +2025-11-15 23:24:56,703 - MT5-Test - INFO - ✅ 通过 - test_balance_async +2025-11-15 23:24:56,703 - MT5-Test - INFO - +================================================== +2025-11-15 23:24:56,703 - MT5-Test - INFO - 执行测试: test_ticker_async +2025-11-15 23:24:56,703 - MT5-Test - INFO - ================================================== +2025-11-15 23:24:56,703 - MT5-Test - INFO - 获取异步行情... +2025-11-15 23:24:57,764 - MT5-Test - INFO - EUR/USD: 买=1.16205, 卖=1.16219 +2025-11-15 23:24:57,764 - MT5-Test - INFO - ✅ 通过 - test_ticker_async +2025-11-15 23:24:57,764 - MT5-Test - INFO - +================================================== +2025-11-15 23:24:57,764 - MT5-Test - INFO - 执行测试: test_orders_async +2025-11-15 23:24:57,764 - MT5-Test - INFO - ================================================== +2025-11-15 23:24:57,764 - MT5-Test - INFO - 获取异步未平仓订单... +2025-11-15 23:24:58,767 - MT5-Test - ERROR - 异步订单查询测试失败: mt5 safeMarket() requires a fourth argument for BTCUSD to disambiguate between different markets with the same market id +2025-11-15 23:24:58,767 - MT5-Test - INFO - ❌ 失败 - test_orders_async +2025-11-15 23:24:59,018 - MT5-Test - INFO - +============================================================ +2025-11-15 23:24:59,019 - MT5-Test - INFO - 📊 异步版本测试总结 +2025-11-15 23:24:59,019 - MT5-Test - INFO - ============================================================ +2025-11-15 23:24:59,019 - MT5-Test - INFO - test_connection_async: ✅ 通过 +2025-11-15 23:24:59,019 - MT5-Test - INFO - test_markets_async: ✅ 通过 +2025-11-15 23:24:59,019 - MT5-Test - INFO - test_balance_async: ✅ 通过 +2025-11-15 23:24:59,019 - MT5-Test - INFO - test_ticker_async: ✅ 通过 +2025-11-15 23:24:59,019 - MT5-Test - INFO - test_orders_async: ❌ 失败 +2025-11-15 23:24:59,019 - MT5-Test - INFO - +总体结果: 4/5 通过 +2025-11-15 23:24:59,023 - MT5-Test - INFO - +🔌 开始 WebSocket 订单监听测试 +2025-11-15 23:24:59,023 - MT5-Test - INFO - 监听订单更新... +2025-11-15 23:24:59,032 - MT5-Test - ERROR - 订单监听错误: unhashable type: 'dict' +2025-11-15 23:24:59,288 - MT5-Test - INFO - +💰 开始 WebSocket 余额监听测试 +2025-11-15 23:24:59,288 - MT5-Test - INFO - 监听余额更新... +2025-11-15 23:24:59,298 - MT5-Test - ERROR - 余额监听错误: unhashable type: 'dict' +2025-11-15 23:30:49,074 - MT5-Test - INFO - 🚀 开始同步版本测试 +2025-11-15 23:30:49,074 - MT5-Test - INFO - +================================================== +2025-11-15 23:30:49,074 - MT5-Test - INFO - 执行测试: test_connection +2025-11-15 23:30:49,074 - MT5-Test - INFO - ================================================== +2025-11-15 23:30:49,074 - MT5-Test - INFO - 测试服务器连接... +2025-11-15 23:30:49,079 - MT5-Test - INFO - Ping 结果: True +2025-11-15 23:30:49,079 - MT5-Test - INFO - ✅ 通过 - test_connection +2025-11-15 23:30:49,079 - MT5-Test - INFO - +================================================== +2025-11-15 23:30:49,079 - MT5-Test - INFO - 执行测试: test_token_management +2025-11-15 23:30:49,079 - MT5-Test - INFO - ================================================== +2025-11-15 23:30:49,079 - MT5-Test - INFO - 测试 Token 获取... +2025-11-15 23:30:50,078 - MT5-Test - INFO - 获取到 Token: bc8d0e29-89e6-4bbb-a9d0-a933c31d3d24 +2025-11-15 23:30:50,078 - MT5-Test - INFO - 测试服务器时区... +2025-11-15 23:30:51,077 - MT5-Test - INFO - 服务器时区: 0 +2025-11-15 23:30:51,077 - MT5-Test - INFO - ✅ 通过 - test_token_management +2025-11-15 23:30:51,078 - MT5-Test - INFO - +================================================== +2025-11-15 23:30:51,078 - MT5-Test - INFO - 执行测试: test_markets +2025-11-15 23:30:51,078 - MT5-Test - INFO - ================================================== +2025-11-15 23:30:51,078 - MT5-Test - INFO - 获取交易对列表... +2025-11-15 23:30:52,100 - MT5-Test - INFO - 获取到 338 个交易对 +2025-11-15 23:30:52,100 - MT5-Test - INFO - 1. USD/RUB - USD/RUB +2025-11-15 23:30:52,100 - MT5-Test - INFO - 2. USD/AED - USD/AED +2025-11-15 23:30:52,100 - MT5-Test - INFO - 3. USD/AMD - USD/AMD +2025-11-15 23:30:52,100 - MT5-Test - INFO - 4. USD/ARS - USD/ARS +2025-11-15 23:30:52,100 - MT5-Test - INFO - 5. USD/AZN - USD/AZN +2025-11-15 23:30:52,100 - MT5-Test - INFO - ... 还有 333 个交易对 +2025-11-15 23:30:52,101 - MT5-Test - INFO - ✅ 通过 - test_markets +2025-11-15 23:30:52,101 - MT5-Test - INFO - +================================================== +2025-11-15 23:30:52,101 - MT5-Test - INFO - 执行测试: test_balance +2025-11-15 23:30:52,101 - MT5-Test - INFO - ================================================== +2025-11-15 23:30:52,101 - MT5-Test - INFO - 获取账户余额... +2025-11-15 23:30:53,077 - MT5-Test - INFO - 余额信息: {'info': {'balance': '8508.85', 'credit': '0.0', 'profit': '-3.23', 'equity': '8505.62', 'margin': '4.80212', 'freeMargin': '8500.81788', 'marginLevel': '177122.1877004323', 'leverage': '2000.0', 'currency': 'USD', 'method': 'Hedging', 'type': 'demo', 'isInvestor': False}, 'timestamp': None, 'datetime': None, 'USDT': {'free': 8500.81788, 'used': 4.80212, 'total': 8508.85}, 'free': {'USDT': 8500.81788}, 'used': {'USDT': 4.80212}, 'total': {'USDT': 8508.85}} +2025-11-15 23:30:53,077 - MT5-Test - INFO - USDT 余额 - 总额: 8508.85, 可用: 8500.81788, 占用: 4.80212 +2025-11-15 23:30:53,077 - MT5-Test - INFO - ✅ 通过 - test_balance +2025-11-15 23:30:53,077 - MT5-Test - INFO - +================================================== +2025-11-15 23:30:53,077 - MT5-Test - INFO - 执行测试: test_ticker +2025-11-15 23:30:53,077 - MT5-Test - INFO - ================================================== +2025-11-15 23:30:53,078 - MT5-Test - INFO - 获取 EUR/USD 行情... +2025-11-15 23:30:54,079 - MT5-Test - INFO - EUR/USD: 买=1.16205, 卖=1.16219, 最后=0.0 +2025-11-15 23:30:54,079 - MT5-Test - INFO - 获取 GBP/USD 行情... +2025-11-15 23:30:55,077 - MT5-Test - INFO - GBP/USD: 买=1.31737, 卖=1.31744, 最后=0.0 +2025-11-15 23:30:55,077 - MT5-Test - INFO - 获取 USD/JPY 行情... +2025-11-15 23:30:56,078 - MT5-Test - INFO - USD/JPY: 买=154.531, 卖=154.543, 最后=0.0 +2025-11-15 23:30:56,078 - MT5-Test - INFO - 测试批量获取行情... +2025-11-15 23:30:57,079 - MT5-Test - INFO - 批量获取到 0 个行情 +2025-11-15 23:30:57,079 - MT5-Test - INFO - ✅ 通过 - test_ticker +2025-11-15 23:30:57,079 - MT5-Test - INFO - +================================================== +2025-11-15 23:30:57,079 - MT5-Test - INFO - 执行测试: test_account_details +2025-11-15 23:30:57,079 - MT5-Test - INFO - ================================================== +2025-11-15 23:30:57,079 - MT5-Test - INFO - 获取账户详情... +2025-11-15 23:30:58,079 - MT5-Test - INFO - 账户详情: {'serverName': 'Exness-MT5Trial5', 'user': '76888962', 'host': '18.163.85.196', 'port': 443, 'serverTime': '2025-11-15T15:30:58.07836Z', 'serverTimeZone': 0, 'company': 'Exness Technologies Ltd', 'currency': 'USD', 'accountName': '先锋账户-模拟测试', 'group': None, 'accountType': 'demo', 'accountLeverage': 2000, 'accountMethod': 'Hedging', 'isInvestor': False} +2025-11-15 23:30:58,079 - MT5-Test - INFO - serverName: Exness-MT5Trial5 +2025-11-15 23:30:58,079 - MT5-Test - INFO - user: 76888962 +2025-11-15 23:30:58,079 - MT5-Test - INFO - currency: USD +2025-11-15 23:30:58,079 - MT5-Test - INFO - accountLeverage: 2000 +2025-11-15 23:30:58,079 - MT5-Test - INFO - ✅ 通过 - test_account_details +2025-11-15 23:30:58,079 - MT5-Test - INFO - +================================================== +2025-11-15 23:30:58,079 - MT5-Test - INFO - 执行测试: test_orders +2025-11-15 23:30:58,079 - MT5-Test - INFO - ================================================== +2025-11-15 23:30:58,079 - MT5-Test - INFO - 获取未平仓订单... +2025-11-15 23:30:59,080 - MT5-Test - INFO - 未平仓订单数量: 2 +2025-11-15 23:30:59,080 - MT5-Test - INFO - 订单 881293557: BTCUSD sell market closed +2025-11-15 23:30:59,080 - MT5-Test - INFO - 订单 881352305: BTCUSD sell market closed +2025-11-15 23:30:59,080 - MT5-Test - INFO - 获取已平仓订单... +2025-11-15 23:31:00,081 - MT5-Test - INFO - 已平仓订单数量: 4 +2025-11-15 23:31:00,081 - MT5-Test - INFO - ✅ 通过 - test_orders +2025-11-15 23:31:00,081 - MT5-Test - INFO - +================================================== +2025-11-15 23:31:00,081 - MT5-Test - INFO - 执行测试: test_order_operations +2025-11-15 23:31:00,081 - MT5-Test - INFO - ================================================== +2025-11-15 23:31:00,082 - MT5-Test - INFO - 测试订单创建参数验证... +2025-11-15 23:31:00,082 - MT5-Test - INFO - ✅ 订单参数验证通过 +2025-11-15 23:31:00,082 - MT5-Test - INFO - 注意: 实际下单测试需要在真实环境中进行 +2025-11-15 23:31:00,082 - MT5-Test - INFO - ✅ 通过 - test_order_operations +2025-11-15 23:31:00,082 - MT5-Test - INFO - +============================================================ +2025-11-15 23:31:00,082 - MT5-Test - INFO - 📊 同步版本测试总结 +2025-11-15 23:31:00,082 - MT5-Test - INFO - ============================================================ +2025-11-15 23:31:00,082 - MT5-Test - INFO - test_connection: ✅ 通过 +2025-11-15 23:31:00,082 - MT5-Test - INFO - test_token_management: ✅ 通过 +2025-11-15 23:31:00,082 - MT5-Test - INFO - test_markets: ✅ 通过 +2025-11-15 23:31:00,082 - MT5-Test - INFO - test_balance: ✅ 通过 +2025-11-15 23:31:00,082 - MT5-Test - INFO - test_ticker: ✅ 通过 +2025-11-15 23:31:00,082 - MT5-Test - INFO - test_account_details: ✅ 通过 +2025-11-15 23:31:00,082 - MT5-Test - INFO - test_orders: ✅ 通过 +2025-11-15 23:31:00,082 - MT5-Test - INFO - test_order_operations: ✅ 通过 +2025-11-15 23:31:00,082 - MT5-Test - INFO - +总体结果: 8/8 通过 +2025-11-15 23:31:00,082 - MT5-Test - INFO - 🎉 所有测试通过! +2025-11-15 23:31:00,082 - MT5-Test - INFO - +🚀 开始异步版本测试 +2025-11-15 23:31:00,086 - MT5-Test - INFO - +================================================== +2025-11-15 23:31:00,086 - MT5-Test - INFO - 执行测试: test_connection_async +2025-11-15 23:31:00,086 - MT5-Test - INFO - ================================================== +2025-11-15 23:31:00,086 - MT5-Test - INFO - 测试异步 Token 获取... +2025-11-15 23:31:00,096 - MT5-Test - INFO - 获取到 Token: bc8d0e29-89e6-4bbb-a9d0-a933c31d3d24 +2025-11-15 23:31:00,096 - MT5-Test - INFO - ✅ 通过 - test_connection_async +2025-11-15 23:31:00,096 - MT5-Test - INFO - +================================================== +2025-11-15 23:31:00,096 - MT5-Test - INFO - 执行测试: test_markets_async +2025-11-15 23:31:00,096 - MT5-Test - INFO - ================================================== +2025-11-15 23:31:00,096 - MT5-Test - INFO - 获取异步交易对列表... +2025-11-15 23:31:01,122 - MT5-Test - INFO - 获取到 338 个交易对 +2025-11-15 23:31:01,122 - MT5-Test - INFO - ✅ 通过 - test_markets_async +2025-11-15 23:31:01,122 - MT5-Test - INFO - +================================================== +2025-11-15 23:31:01,122 - MT5-Test - INFO - 执行测试: test_balance_async +2025-11-15 23:31:01,122 - MT5-Test - INFO - ================================================== +2025-11-15 23:31:01,122 - MT5-Test - INFO - 获取异步账户余额... +2025-11-15 23:31:02,124 - MT5-Test - INFO - 余额信息: {'info': {'balance': '8508.85', 'credit': '0.0', 'profit': '-3.8899999999999997', 'equity': '8504.960000000001', 'margin': '4.80212', 'freeMargin': '8500.15788', 'marginLevel': '177108.44377066797', 'leverage': '2000.0', 'currency': 'USD', 'method': 'Hedging', 'type': 'demo', 'isInvestor': False}, 'timestamp': None, 'datetime': None, 'USDT': {'free': 8500.15788, 'used': 4.80212, 'total': 8508.85}, 'free': {'USDT': 8500.15788}, 'used': {'USDT': 4.80212}, 'total': {'USDT': 8508.85}} +2025-11-15 23:31:02,125 - MT5-Test - INFO - ✅ 通过 - test_balance_async +2025-11-15 23:31:02,125 - MT5-Test - INFO - +================================================== +2025-11-15 23:31:02,125 - MT5-Test - INFO - 执行测试: test_ticker_async +2025-11-15 23:31:02,125 - MT5-Test - INFO - ================================================== +2025-11-15 23:31:02,125 - MT5-Test - INFO - 获取异步行情... +2025-11-15 23:31:03,186 - MT5-Test - INFO - EUR/USD: 买=1.16205, 卖=1.16219 +2025-11-15 23:31:03,186 - MT5-Test - INFO - ✅ 通过 - test_ticker_async +2025-11-15 23:31:03,186 - MT5-Test - INFO - +================================================== +2025-11-15 23:31:03,186 - MT5-Test - INFO - 执行测试: test_orders_async +2025-11-15 23:31:03,186 - MT5-Test - INFO - ================================================== +2025-11-15 23:31:03,186 - MT5-Test - INFO - 获取异步未平仓订单... +2025-11-15 23:31:04,189 - MT5-Test - ERROR - 异步订单查询测试失败: mt5 safeMarket() requires a fourth argument for BTCUSD to disambiguate between different markets with the same market id +2025-11-15 23:31:04,190 - MT5-Test - INFO - ❌ 失败 - test_orders_async +2025-11-15 23:31:04,441 - MT5-Test - INFO - +============================================================ +2025-11-15 23:31:04,441 - MT5-Test - INFO - 📊 异步版本测试总结 +2025-11-15 23:31:04,441 - MT5-Test - INFO - ============================================================ +2025-11-15 23:31:04,441 - MT5-Test - INFO - test_connection_async: ✅ 通过 +2025-11-15 23:31:04,441 - MT5-Test - INFO - test_markets_async: ✅ 通过 +2025-11-15 23:31:04,441 - MT5-Test - INFO - test_balance_async: ✅ 通过 +2025-11-15 23:31:04,441 - MT5-Test - INFO - test_ticker_async: ✅ 通过 +2025-11-15 23:31:04,441 - MT5-Test - INFO - test_orders_async: ❌ 失败 +2025-11-15 23:31:04,441 - MT5-Test - INFO - +总体结果: 4/5 通过 +2025-11-15 23:31:04,445 - MT5-Test - INFO - +🔌 开始 WebSocket 订单监听测试 +2025-11-15 23:31:04,445 - MT5-Test - INFO - 监听订单更新... +2025-11-15 23:31:04,455 - MT5-Test - ERROR - 订单监听错误: unhashable type: 'dict' +2025-11-15 23:31:04,711 - MT5-Test - INFO - +💰 开始 WebSocket 余额监听测试 +2025-11-15 23:31:04,711 - MT5-Test - INFO - 监听余额更新... +2025-11-15 23:31:04,722 - MT5-Test - ERROR - 余额监听错误: unhashable type: 'dict' +2025-11-15 23:38:31,723 - MT5-Test - INFO - 🚀 开始同步版本测试 +2025-11-15 23:38:31,723 - MT5-Test - INFO - +================================================== +2025-11-15 23:38:31,723 - MT5-Test - INFO - 执行测试: test_connection +2025-11-15 23:38:31,723 - MT5-Test - INFO - ================================================== +2025-11-15 23:38:31,723 - MT5-Test - INFO - 测试服务器连接... +2025-11-15 23:38:31,728 - MT5-Test - INFO - Ping 结果: True +2025-11-15 23:38:31,728 - MT5-Test - INFO - ✅ 通过 - test_connection +2025-11-15 23:38:31,728 - MT5-Test - INFO - +================================================== +2025-11-15 23:38:31,728 - MT5-Test - INFO - 执行测试: test_token_management +2025-11-15 23:38:31,728 - MT5-Test - INFO - ================================================== +2025-11-15 23:38:31,728 - MT5-Test - INFO - 测试 Token 获取... +2025-11-15 23:38:32,727 - MT5-Test - INFO - 获取到 Token: bc8d0e29-89e6-4bbb-a9d0-a933c31d3d24 +2025-11-15 23:38:32,727 - MT5-Test - INFO - 测试服务器时区... +2025-11-15 23:38:33,726 - MT5-Test - INFO - 服务器时区: 0 +2025-11-15 23:38:33,726 - MT5-Test - INFO - ✅ 通过 - test_token_management +2025-11-15 23:38:33,726 - MT5-Test - INFO - +================================================== +2025-11-15 23:38:33,726 - MT5-Test - INFO - 执行测试: test_markets +2025-11-15 23:38:33,726 - MT5-Test - INFO - ================================================== +2025-11-15 23:38:33,726 - MT5-Test - INFO - 获取交易对列表... +2025-11-15 23:38:34,749 - MT5-Test - INFO - 获取到 453 个交易对 +2025-11-15 23:38:34,750 - MT5-Test - INFO - 1. USD/RUB - USD/RUB +2025-11-15 23:38:34,750 - MT5-Test - INFO - 2. USD/AED - USD/AED +2025-11-15 23:38:34,750 - MT5-Test - INFO - 3. USD/AMD - USD/AMD +2025-11-15 23:38:34,750 - MT5-Test - INFO - 4. USD/ARS - USD/ARS +2025-11-15 23:38:34,750 - MT5-Test - INFO - 5. USD/AZN - USD/AZN +2025-11-15 23:38:34,750 - MT5-Test - INFO - ... 还有 448 个交易对 +2025-11-15 23:38:34,750 - MT5-Test - INFO - ✅ 通过 - test_markets +2025-11-15 23:38:34,750 - MT5-Test - INFO - +================================================== +2025-11-15 23:38:34,750 - MT5-Test - INFO - 执行测试: test_balance +2025-11-15 23:38:34,750 - MT5-Test - INFO - ================================================== +2025-11-15 23:38:34,750 - MT5-Test - INFO - 获取账户余额... +2025-11-15 23:38:35,726 - MT5-Test - INFO - 余额信息: {'info': {'balance': '8508.85', 'credit': '0.0', 'profit': '-2.79', 'equity': '8506.06', 'margin': '4.80212', 'freeMargin': '8501.25788', 'marginLevel': '177131.3503202752', 'leverage': '2000.0', 'currency': 'USD', 'method': 'Hedging', 'type': 'demo', 'isInvestor': False}, 'timestamp': None, 'datetime': None, 'USDT': {'free': 8501.25788, 'used': 4.80212, 'total': 8508.85}, 'free': {'USDT': 8501.25788}, 'used': {'USDT': 4.80212}, 'total': {'USDT': 8508.85}} +2025-11-15 23:38:35,727 - MT5-Test - INFO - USDT 余额 - 总额: 8508.85, 可用: 8501.25788, 占用: 4.80212 +2025-11-15 23:38:35,727 - MT5-Test - INFO - ✅ 通过 - test_balance +2025-11-15 23:38:35,727 - MT5-Test - INFO - +================================================== +2025-11-15 23:38:35,727 - MT5-Test - INFO - 执行测试: test_ticker +2025-11-15 23:38:35,727 - MT5-Test - INFO - ================================================== +2025-11-15 23:38:35,727 - MT5-Test - INFO - 获取 EUR/USD 行情... +2025-11-15 23:38:36,728 - MT5-Test - INFO - EUR/USD: 买=1.16205, 卖=1.16219, 最后=0.0 +2025-11-15 23:38:36,728 - MT5-Test - INFO - 获取 GBP/USD 行情... +2025-11-15 23:38:37,727 - MT5-Test - INFO - GBP/USD: 买=1.31737, 卖=1.31744, 最后=0.0 +2025-11-15 23:38:37,727 - MT5-Test - INFO - 获取 USD/JPY 行情... +2025-11-15 23:38:38,728 - MT5-Test - INFO - USD/JPY: 买=154.531, 卖=154.543, 最后=0.0 +2025-11-15 23:38:38,728 - MT5-Test - INFO - 测试批量获取行情... +2025-11-15 23:38:39,728 - MT5-Test - INFO - 批量获取到 0 个行情 +2025-11-15 23:38:39,728 - MT5-Test - INFO - ✅ 通过 - test_ticker +2025-11-15 23:38:39,728 - MT5-Test - INFO - +================================================== +2025-11-15 23:38:39,728 - MT5-Test - INFO - 执行测试: test_account_details +2025-11-15 23:38:39,728 - MT5-Test - INFO - ================================================== +2025-11-15 23:38:39,728 - MT5-Test - INFO - 获取账户详情... +2025-11-15 23:38:40,727 - MT5-Test - INFO - 账户详情: {'serverName': 'Exness-MT5Trial5', 'user': '76888962', 'host': '18.163.85.196', 'port': 443, 'serverTime': '2025-11-15T15:38:40.7271104Z', 'serverTimeZone': 0, 'company': 'Exness Technologies Ltd', 'currency': 'USD', 'accountName': '先锋账户-模拟测试', 'group': None, 'accountType': 'demo', 'accountLeverage': 2000, 'accountMethod': 'Hedging', 'isInvestor': False} +2025-11-15 23:38:40,728 - MT5-Test - INFO - serverName: Exness-MT5Trial5 +2025-11-15 23:38:40,728 - MT5-Test - INFO - user: 76888962 +2025-11-15 23:38:40,728 - MT5-Test - INFO - currency: USD +2025-11-15 23:38:40,728 - MT5-Test - INFO - accountLeverage: 2000 +2025-11-15 23:38:40,728 - MT5-Test - INFO - ✅ 通过 - test_account_details +2025-11-15 23:38:40,728 - MT5-Test - INFO - +================================================== +2025-11-15 23:38:40,728 - MT5-Test - INFO - 执行测试: test_orders +2025-11-15 23:38:40,728 - MT5-Test - INFO - ================================================== +2025-11-15 23:38:40,728 - MT5-Test - INFO - 获取未平仓订单... +2025-11-15 23:38:41,728 - MT5-Test - INFO - 未平仓订单数量: 2 +2025-11-15 23:38:41,728 - MT5-Test - INFO - 订单 881293557: BTCUSD sell market closed +2025-11-15 23:38:41,728 - MT5-Test - INFO - 订单 881352305: BTCUSD sell market closed +2025-11-15 23:38:41,728 - MT5-Test - INFO - 获取已平仓订单... +2025-11-15 23:38:42,729 - MT5-Test - INFO - 已平仓订单数量: 4 +2025-11-15 23:38:42,729 - MT5-Test - INFO - ✅ 通过 - test_orders +2025-11-15 23:38:42,729 - MT5-Test - INFO - +================================================== +2025-11-15 23:38:42,729 - MT5-Test - INFO - 执行测试: test_order_operations +2025-11-15 23:38:42,729 - MT5-Test - INFO - ================================================== +2025-11-15 23:38:42,729 - MT5-Test - INFO - 测试订单创建参数验证... +2025-11-15 23:38:42,729 - MT5-Test - INFO - ✅ 订单参数验证通过 +2025-11-15 23:38:42,729 - MT5-Test - INFO - 注意: 实际下单测试需要在真实环境中进行 +2025-11-15 23:38:42,729 - MT5-Test - INFO - ✅ 通过 - test_order_operations +2025-11-15 23:38:42,729 - MT5-Test - INFO - +============================================================ +2025-11-15 23:38:42,729 - MT5-Test - INFO - 📊 同步版本测试总结 +2025-11-15 23:38:42,729 - MT5-Test - INFO - ============================================================ +2025-11-15 23:38:42,729 - MT5-Test - INFO - test_connection: ✅ 通过 +2025-11-15 23:38:42,730 - MT5-Test - INFO - test_token_management: ✅ 通过 +2025-11-15 23:38:42,730 - MT5-Test - INFO - test_markets: ✅ 通过 +2025-11-15 23:38:42,730 - MT5-Test - INFO - test_balance: ✅ 通过 +2025-11-15 23:38:42,730 - MT5-Test - INFO - test_ticker: ✅ 通过 +2025-11-15 23:38:42,730 - MT5-Test - INFO - test_account_details: ✅ 通过 +2025-11-15 23:38:42,730 - MT5-Test - INFO - test_orders: ✅ 通过 +2025-11-15 23:38:42,730 - MT5-Test - INFO - test_order_operations: ✅ 通过 +2025-11-15 23:38:42,730 - MT5-Test - INFO - +总体结果: 8/8 通过 +2025-11-15 23:38:42,730 - MT5-Test - INFO - 🎉 所有测试通过! +2025-11-15 23:38:42,730 - MT5-Test - INFO - +🚀 开始异步版本测试 +2025-11-15 23:38:42,733 - MT5-Test - INFO - +================================================== +2025-11-15 23:38:42,733 - MT5-Test - INFO - 执行测试: test_connection_async +2025-11-15 23:38:42,733 - MT5-Test - INFO - ================================================== +2025-11-15 23:38:42,733 - MT5-Test - INFO - 测试异步 Token 获取... +2025-11-15 23:38:42,743 - MT5-Test - INFO - 获取到 Token: bc8d0e29-89e6-4bbb-a9d0-a933c31d3d24 +2025-11-15 23:38:42,743 - MT5-Test - INFO - ✅ 通过 - test_connection_async +2025-11-15 23:38:42,744 - MT5-Test - INFO - +================================================== +2025-11-15 23:38:42,744 - MT5-Test - INFO - 执行测试: test_markets_async +2025-11-15 23:38:42,744 - MT5-Test - INFO - ================================================== +2025-11-15 23:38:42,744 - MT5-Test - INFO - 获取异步交易对列表... +2025-11-15 23:38:43,769 - MT5-Test - INFO - 获取到 338 个交易对 +2025-11-15 23:38:43,769 - MT5-Test - INFO - ✅ 通过 - test_markets_async +2025-11-15 23:38:43,769 - MT5-Test - INFO - +================================================== +2025-11-15 23:38:43,769 - MT5-Test - INFO - 执行测试: test_balance_async +2025-11-15 23:38:43,769 - MT5-Test - INFO - ================================================== +2025-11-15 23:38:43,769 - MT5-Test - INFO - 获取异步账户余额... +2025-11-15 23:38:44,772 - MT5-Test - INFO - 余额信息: {'info': {'balance': '8508.85', 'credit': '0.0', 'profit': '-2.9299999999999997', 'equity': '8505.92', 'margin': '4.80212', 'freeMargin': '8501.11788', 'marginLevel': '177128.4349412343', 'leverage': '2000.0', 'currency': 'USD', 'method': 'Hedging', 'type': 'demo', 'isInvestor': False}, 'timestamp': None, 'datetime': None, 'USDT': {'free': 8501.11788, 'used': 4.80212, 'total': 8508.85}, 'free': {'USDT': 8501.11788}, 'used': {'USDT': 4.80212}, 'total': {'USDT': 8508.85}} +2025-11-15 23:38:44,772 - MT5-Test - INFO - ✅ 通过 - test_balance_async +2025-11-15 23:38:44,772 - MT5-Test - INFO - +================================================== +2025-11-15 23:38:44,772 - MT5-Test - INFO - 执行测试: test_ticker_async +2025-11-15 23:38:44,772 - MT5-Test - INFO - ================================================== +2025-11-15 23:38:44,772 - MT5-Test - INFO - 获取异步行情... +2025-11-15 23:38:45,837 - MT5-Test - INFO - EUR/USD: 买=1.16205, 卖=1.16219 +2025-11-15 23:38:45,837 - MT5-Test - INFO - ✅ 通过 - test_ticker_async +2025-11-15 23:38:45,837 - MT5-Test - INFO - +================================================== +2025-11-15 23:38:45,837 - MT5-Test - INFO - 执行测试: test_orders_async +2025-11-15 23:38:45,838 - MT5-Test - INFO - ================================================== +2025-11-15 23:38:45,838 - MT5-Test - INFO - 获取异步未平仓订单... +2025-11-15 23:38:46,840 - MT5-Test - ERROR - 异步订单查询测试失败: mt5 safeMarket() requires a fourth argument for BTCUSD to disambiguate between different markets with the same market id +2025-11-15 23:38:46,840 - MT5-Test - INFO - ❌ 失败 - test_orders_async +2025-11-15 23:38:47,091 - MT5-Test - INFO - +============================================================ +2025-11-15 23:38:47,091 - MT5-Test - INFO - 📊 异步版本测试总结 +2025-11-15 23:38:47,092 - MT5-Test - INFO - ============================================================ +2025-11-15 23:38:47,092 - MT5-Test - INFO - test_connection_async: ✅ 通过 +2025-11-15 23:38:47,092 - MT5-Test - INFO - test_markets_async: ✅ 通过 +2025-11-15 23:38:47,092 - MT5-Test - INFO - test_balance_async: ✅ 通过 +2025-11-15 23:38:47,092 - MT5-Test - INFO - test_ticker_async: ✅ 通过 +2025-11-15 23:38:47,092 - MT5-Test - INFO - test_orders_async: ❌ 失败 +2025-11-15 23:38:47,092 - MT5-Test - INFO - +总体结果: 4/5 通过 +2025-11-15 23:38:47,096 - MT5-Test - INFO - +🔌 开始 WebSocket 订单监听测试 +2025-11-15 23:38:47,096 - MT5-Test - INFO - 监听订单更新... +2025-11-15 23:38:47,106 - MT5-Test - ERROR - 订单监听错误: unhashable type: 'dict' +2025-11-15 23:38:47,361 - MT5-Test - INFO - +💰 开始 WebSocket 余额监听测试 +2025-11-15 23:38:47,362 - MT5-Test - INFO - 监听余额更新... +2025-11-15 23:38:47,371 - MT5-Test - ERROR - 余额监听错误: unhashable type: 'dict' +2025-11-15 23:40:15,241 - MT5-Test - INFO - 🚀 开始同步版本测试 +2025-11-15 23:40:15,241 - MT5-Test - INFO - +================================================== +2025-11-15 23:40:15,241 - MT5-Test - INFO - 执行测试: test_connection +2025-11-15 23:40:15,242 - MT5-Test - INFO - ================================================== +2025-11-15 23:40:15,242 - MT5-Test - INFO - 测试服务器连接... +2025-11-15 23:40:15,246 - MT5-Test - INFO - Ping 结果: True +2025-11-15 23:40:15,246 - MT5-Test - INFO - ✅ 通过 - test_connection +2025-11-15 23:40:15,247 - MT5-Test - INFO - +================================================== +2025-11-15 23:40:15,247 - MT5-Test - INFO - 执行测试: test_token_management +2025-11-15 23:40:15,247 - MT5-Test - INFO - ================================================== +2025-11-15 23:40:15,247 - MT5-Test - INFO - 测试 Token 获取... +2025-11-15 23:40:16,245 - MT5-Test - INFO - 获取到 Token: bc8d0e29-89e6-4bbb-a9d0-a933c31d3d24 +2025-11-15 23:40:16,245 - MT5-Test - INFO - 测试服务器时区... +2025-11-15 23:40:17,245 - MT5-Test - INFO - 服务器时区: 0 +2025-11-15 23:40:17,245 - MT5-Test - INFO - ✅ 通过 - test_token_management +2025-11-15 23:40:17,245 - MT5-Test - INFO - +================================================== +2025-11-15 23:40:17,245 - MT5-Test - INFO - 执行测试: test_markets +2025-11-15 23:40:17,245 - MT5-Test - INFO - ================================================== +2025-11-15 23:40:17,245 - MT5-Test - INFO - 获取交易对列表... +2025-11-15 23:40:18,269 - MT5-Test - INFO - 获取到 453 个交易对 +2025-11-15 23:40:18,269 - MT5-Test - INFO - 1. USD/RUB - USD/RUB +2025-11-15 23:40:18,271 - MT5-Test - INFO - 2. USD/AED - USD/AED +2025-11-15 23:40:18,271 - MT5-Test - INFO - 3. USD/AMD - USD/AMD +2025-11-15 23:40:18,271 - MT5-Test - INFO - 4. USD/ARS - USD/ARS +2025-11-15 23:40:18,271 - MT5-Test - INFO - 5. USD/AZN - USD/AZN +2025-11-15 23:40:18,271 - MT5-Test - INFO - ... 还有 448 个交易对 +2025-11-15 23:40:18,271 - MT5-Test - INFO - ✅ 通过 - test_markets +2025-11-15 23:40:18,271 - MT5-Test - INFO - +================================================== +2025-11-15 23:40:18,271 - MT5-Test - INFO - 执行测试: test_balance +2025-11-15 23:40:18,271 - MT5-Test - INFO - ================================================== +2025-11-15 23:40:18,271 - MT5-Test - INFO - 获取账户余额... +2025-11-15 23:40:19,246 - MT5-Test - INFO - 余额信息: {'info': {'balance': '8508.85', 'credit': '0.0', 'profit': '-1.37', 'equity': '8507.48', 'margin': '4.80212', 'freeMargin': '8502.67788', 'marginLevel': '177160.92059340456', 'leverage': '2000.0', 'currency': 'USD', 'method': 'Hedging', 'type': 'demo', 'isInvestor': False}, 'timestamp': None, 'datetime': None, 'USDT': {'free': 8502.67788, 'used': 4.80212, 'total': 8508.85}, 'free': {'USDT': 8502.67788}, 'used': {'USDT': 4.80212}, 'total': {'USDT': 8508.85}} +2025-11-15 23:40:19,246 - MT5-Test - INFO - USDT 余额 - 总额: 8508.85, 可用: 8502.67788, 占用: 4.80212 +2025-11-15 23:40:19,246 - MT5-Test - INFO - ✅ 通过 - test_balance +2025-11-15 23:40:19,246 - MT5-Test - INFO - +================================================== +2025-11-15 23:40:19,246 - MT5-Test - INFO - 执行测试: test_ticker +2025-11-15 23:40:19,246 - MT5-Test - INFO - ================================================== +2025-11-15 23:40:19,246 - MT5-Test - INFO - 获取 EUR/USD 行情... +2025-11-15 23:40:20,247 - MT5-Test - INFO - EUR/USD: 买=1.16205, 卖=1.16219, 最后=0.0 +2025-11-15 23:40:20,247 - MT5-Test - INFO - 获取 GBP/USD 行情... +2025-11-15 23:40:21,246 - MT5-Test - INFO - GBP/USD: 买=1.31737, 卖=1.31744, 最后=0.0 +2025-11-15 23:40:21,246 - MT5-Test - INFO - 获取 USD/JPY 行情... +2025-11-15 23:40:22,247 - MT5-Test - INFO - USD/JPY: 买=154.531, 卖=154.543, 最后=0.0 +2025-11-15 23:40:22,247 - MT5-Test - INFO - 测试批量获取行情... +2025-11-15 23:40:23,247 - MT5-Test - INFO - 批量获取到 0 个行情 +2025-11-15 23:40:23,247 - MT5-Test - INFO - ✅ 通过 - test_ticker +2025-11-15 23:40:23,247 - MT5-Test - INFO - +================================================== +2025-11-15 23:40:23,247 - MT5-Test - INFO - 执行测试: test_account_details +2025-11-15 23:40:23,247 - MT5-Test - INFO - ================================================== +2025-11-15 23:40:23,247 - MT5-Test - INFO - 获取账户详情... +2025-11-15 23:40:24,247 - MT5-Test - INFO - 账户详情: {'serverName': 'Exness-MT5Trial5', 'user': '76888962', 'host': '18.163.85.196', 'port': 443, 'serverTime': '2025-11-15T15:40:24.2461614Z', 'serverTimeZone': 0, 'company': 'Exness Technologies Ltd', 'currency': 'USD', 'accountName': '先锋账户-模拟测试', 'group': None, 'accountType': 'demo', 'accountLeverage': 2000, 'accountMethod': 'Hedging', 'isInvestor': False} +2025-11-15 23:40:24,247 - MT5-Test - INFO - serverName: Exness-MT5Trial5 +2025-11-15 23:40:24,247 - MT5-Test - INFO - user: 76888962 +2025-11-15 23:40:24,247 - MT5-Test - INFO - currency: USD +2025-11-15 23:40:24,247 - MT5-Test - INFO - accountLeverage: 2000 +2025-11-15 23:40:24,247 - MT5-Test - INFO - ✅ 通过 - test_account_details +2025-11-15 23:40:24,247 - MT5-Test - INFO - +================================================== +2025-11-15 23:40:24,247 - MT5-Test - INFO - 执行测试: test_orders +2025-11-15 23:40:24,247 - MT5-Test - INFO - ================================================== +2025-11-15 23:40:24,247 - MT5-Test - INFO - 获取未平仓订单... +2025-11-15 23:40:25,247 - MT5-Test - INFO - 未平仓订单数量: 2 +2025-11-15 23:40:25,247 - MT5-Test - INFO - 订单 881293557: BTC/USD sell market closed +2025-11-15 23:40:25,247 - MT5-Test - INFO - 订单 881352305: BTC/USD sell market closed +2025-11-15 23:40:25,247 - MT5-Test - INFO - 获取已平仓订单... +2025-11-15 23:40:26,247 - MT5-Test - INFO - 已平仓订单数量: 4 +2025-11-15 23:40:26,248 - MT5-Test - INFO - ✅ 通过 - test_orders +2025-11-15 23:40:26,248 - MT5-Test - INFO - +================================================== +2025-11-15 23:40:26,248 - MT5-Test - INFO - 执行测试: test_order_operations +2025-11-15 23:40:26,248 - MT5-Test - INFO - ================================================== +2025-11-15 23:40:26,248 - MT5-Test - INFO - 测试订单创建参数验证... +2025-11-15 23:40:26,248 - MT5-Test - INFO - ✅ 订单参数验证通过 +2025-11-15 23:40:26,248 - MT5-Test - INFO - 注意: 实际下单测试需要在真实环境中进行 +2025-11-15 23:40:26,248 - MT5-Test - INFO - ✅ 通过 - test_order_operations +2025-11-15 23:40:26,248 - MT5-Test - INFO - +============================================================ +2025-11-15 23:40:26,248 - MT5-Test - INFO - 📊 同步版本测试总结 +2025-11-15 23:40:26,248 - MT5-Test - INFO - ============================================================ +2025-11-15 23:40:26,249 - MT5-Test - INFO - test_connection: ✅ 通过 +2025-11-15 23:40:26,249 - MT5-Test - INFO - test_token_management: ✅ 通过 +2025-11-15 23:40:26,249 - MT5-Test - INFO - test_markets: ✅ 通过 +2025-11-15 23:40:26,249 - MT5-Test - INFO - test_balance: ✅ 通过 +2025-11-15 23:40:26,249 - MT5-Test - INFO - test_ticker: ✅ 通过 +2025-11-15 23:40:26,249 - MT5-Test - INFO - test_account_details: ✅ 通过 +2025-11-15 23:40:26,249 - MT5-Test - INFO - test_orders: ✅ 通过 +2025-11-15 23:40:26,249 - MT5-Test - INFO - test_order_operations: ✅ 通过 +2025-11-15 23:40:26,249 - MT5-Test - INFO - +总体结果: 8/8 通过 +2025-11-15 23:40:26,249 - MT5-Test - INFO - 🎉 所有测试通过! +2025-11-15 23:40:26,249 - MT5-Test - INFO - +🚀 开始异步版本测试 +2025-11-15 23:40:26,252 - MT5-Test - INFO - +================================================== +2025-11-15 23:40:26,253 - MT5-Test - INFO - 执行测试: test_connection_async +2025-11-15 23:40:26,253 - MT5-Test - INFO - ================================================== +2025-11-15 23:40:26,253 - MT5-Test - INFO - 测试异步 Token 获取... +2025-11-15 23:40:26,263 - MT5-Test - INFO - 获取到 Token: bc8d0e29-89e6-4bbb-a9d0-a933c31d3d24 +2025-11-15 23:40:26,263 - MT5-Test - INFO - ✅ 通过 - test_connection_async +2025-11-15 23:40:26,263 - MT5-Test - INFO - +================================================== +2025-11-15 23:40:26,263 - MT5-Test - INFO - 执行测试: test_markets_async +2025-11-15 23:40:26,263 - MT5-Test - INFO - ================================================== +2025-11-15 23:40:26,263 - MT5-Test - INFO - 获取异步交易对列表... +2025-11-15 23:40:27,291 - MT5-Test - INFO - 获取到 338 个交易对 +2025-11-15 23:40:27,291 - MT5-Test - INFO - ✅ 通过 - test_markets_async +2025-11-15 23:40:27,291 - MT5-Test - INFO - +================================================== +2025-11-15 23:40:27,291 - MT5-Test - INFO - 执行测试: test_balance_async +2025-11-15 23:40:27,292 - MT5-Test - INFO - ================================================== +2025-11-15 23:40:27,292 - MT5-Test - INFO - 获取异步账户余额... +2025-11-15 23:40:28,295 - MT5-Test - INFO - 余额信息: {'info': {'balance': '8508.85', 'credit': '0.0', 'profit': '-1.79', 'equity': '8507.06', 'margin': '4.80212', 'freeMargin': '8502.25788', 'marginLevel': '177152.17445628176', 'leverage': '2000.0', 'currency': 'USD', 'method': 'Hedging', 'type': 'demo', 'isInvestor': False}, 'timestamp': None, 'datetime': None, 'USDT': {'free': 8502.25788, 'used': 4.80212, 'total': 8508.85}, 'free': {'USDT': 8502.25788}, 'used': {'USDT': 4.80212}, 'total': {'USDT': 8508.85}} +2025-11-15 23:40:28,295 - MT5-Test - INFO - ✅ 通过 - test_balance_async +2025-11-15 23:40:28,295 - MT5-Test - INFO - +================================================== +2025-11-15 23:40:28,295 - MT5-Test - INFO - 执行测试: test_ticker_async +2025-11-15 23:40:28,295 - MT5-Test - INFO - ================================================== +2025-11-15 23:40:28,295 - MT5-Test - INFO - 获取异步行情... +2025-11-15 23:40:29,356 - MT5-Test - INFO - EUR/USD: 买=1.16205, 卖=1.16219 +2025-11-15 23:40:29,356 - MT5-Test - INFO - ✅ 通过 - test_ticker_async +2025-11-15 23:40:29,356 - MT5-Test - INFO - +================================================== +2025-11-15 23:40:29,356 - MT5-Test - INFO - 执行测试: test_orders_async +2025-11-15 23:40:29,356 - MT5-Test - INFO - ================================================== +2025-11-15 23:40:29,356 - MT5-Test - INFO - 获取异步未平仓订单... +2025-11-15 23:40:30,359 - MT5-Test - INFO - 未平仓订单数量: 2 +2025-11-15 23:40:30,359 - MT5-Test - INFO - ✅ 通过 - test_orders_async +2025-11-15 23:40:30,610 - MT5-Test - INFO - +============================================================ +2025-11-15 23:40:30,610 - MT5-Test - INFO - 📊 异步版本测试总结 +2025-11-15 23:40:30,610 - MT5-Test - INFO - ============================================================ +2025-11-15 23:40:30,610 - MT5-Test - INFO - test_connection_async: ✅ 通过 +2025-11-15 23:40:30,610 - MT5-Test - INFO - test_markets_async: ✅ 通过 +2025-11-15 23:40:30,610 - MT5-Test - INFO - test_balance_async: ✅ 通过 +2025-11-15 23:40:30,610 - MT5-Test - INFO - test_ticker_async: ✅ 通过 +2025-11-15 23:40:30,610 - MT5-Test - INFO - test_orders_async: ✅ 通过 +2025-11-15 23:40:30,610 - MT5-Test - INFO - +总体结果: 5/5 通过 +2025-11-15 23:40:30,614 - MT5-Test - INFO - +🔌 开始 WebSocket 订单监听测试 +2025-11-15 23:40:30,614 - MT5-Test - INFO - 监听订单更新... +2025-11-15 23:40:30,624 - MT5-Test - ERROR - 订单监听错误: unhashable type: 'dict' +2025-11-15 23:40:30,879 - MT5-Test - INFO - +💰 开始 WebSocket 余额监听测试 +2025-11-15 23:40:30,880 - MT5-Test - INFO - 监听余额更新... +2025-11-15 23:40:30,889 - MT5-Test - ERROR - 余额监听错误: unhashable type: 'dict' diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000..80a199e --- /dev/null +++ b/mypy.ini @@ -0,0 +1,3 @@ +[mypy] +disable_error_code = index,operator,assignment,var-annotated + diff --git a/package.json b/package.json new file mode 100644 index 0000000..85aebb9 --- /dev/null +++ b/package.json @@ -0,0 +1,330 @@ +{ + "name": "ccxt", + "version": "4.5.18", + "description": "A cryptocurrency trading API with more than 100 exchanges in JavaScript / TypeScript / Python / C# / PHP / Go", + "unpkg": "dist/ccxt.browser.min.js", + "type": "module", + "exports": { + ".": { + "import": "./js/ccxt.js", + "require": "./dist/ccxt.cjs" + } + }, + "engines": { + "node": ">=15.0.0" + }, + "publishConfig": { + "registry": "https://registry.npmjs.com" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/ccxt/ccxt.git" + }, + "readme": "README.md", + "scripts": { + "instrument": "nyc instrument js/ jsInstrumented/", + "nyc-coverage": "nyc --reporter=html --reporter=lcov --exclude='js/src/pro/**' --exclude='js/src/base/**' --exclude='js/src/test/**' --exclude='js/src/abstract/**' --exclude='js/src/static_dependencies' node jsInstrumented/src/test/tests.init.js --requestTests --responseTests", + "coverage-js": "npm run instrument && npm run nyc-coverage && rm -rf jsInstrumented", + "docker": "docker-compose run --rm ccxt", + "fixTSBug": "node build/fixTSBug", + "transpileCsSingle": "tsx build/csharpTranspiler.ts", + "transpileCS": "tsx build/csharpTranspiler.ts --multi", + "transpileCSWs": "tsx build/csharpTranspiler.ts --ws", + "buildCS": "dotnet build cs/ccxt.sln", + "buildGO": "go build -C go ./v4 && go build -C go ./v4/pro", + "formatGO": "go fmt -C go v4 tests", + "transpileGOWs": "tsx build/goTranspiler.ts --ws", + "transpileGORest": "tsx build/goTranspiler.ts && tsx build/goTranspiler.ts --ws", + "transpileGO": "npm run transpileGORest && npm run transpileGOWs", + "buildCSTests": "dotnet build cs/tests/tests.csproj", + "buildCSRelease": "dotnet build cs --configuration Release", + "csharp": "npm run transpileCS && npm run transpileCSWs && npm run buildCS", + "go": "npm run transpileGO && npm run buildGO", + "force-build//WithoutGo": "npm run pre-transpile && npm run force-transpile-fast && npm run csharp && npm run post-transpile && npm run update-badges", + "force-build": "npm run pre-transpile && npm run go && npm run force-transpile-fast && npm run csharp && npm run post-transpile && npm run update-badges", + "//TMPCommentforce-build": "npm run pre-transpile && npm run force-transpile-fast && npm run csharp && npm run go && npm run post-transpile && npm run update-badges", + "build-docs": "node jsdoc2md.js && node examples2md.js", + "serve-docs": "docsify serve ./wiki", + "tsBuildFile": "tsc --skipLibCheck --strictNullChecks false --strict --noImplicitAny false --esModuleInterop --isolatedModules false --forceConsistentCasingInFileNames --removeComments false --target ES2020 --declaration --allowJs --checkJs false --moduleResolution Node --module ES2022 --outDir ./js/src --lib ES2020.BigInt --lib dom ", + "addJsHeaders": "tsx build/transpile.ts --js-headers", + "tsBuild": "tsc && npm run addJsHeaders || echo \"\"", + "tsBuildExamples": "tsc -p ./examples/tsconfig.json", + "emitAPI": "tsx build/generateImplicitAPI.ts", + "emitAPITs": "tsx build/generateImplicitAPI.ts -- --ts", + "emitAPIPy": "tsx build/generateImplicitAPI.ts -- --python", + "emitAPIPhp": "tsx build/generateImplicitAPI.ts -- --php", + "emitAPIGo": "tsx build/generateImplicitAPI.ts -- --go", + "emitAPICs": "tsx build/generateImplicitAPI.ts -- --csharp", + "build": "npm run pre-transpile && npm run transpile && npm run post-transpile && npm run update-badges && npm run build-docs", + "force-build-slow": "npm run pre-transpile && npm run force-transpile && npm run post-transpile && npm run update-badges", + "pre-transpile-js": "npm run export-exchanges && npm run vss && npm run emitAPITs && npm run tsBuild && npm run validate-types && npm run tsBuildExamples && npm run check-js-syntax && npm run bundle", + "pre-transpile-py": "npm run export-exchanges && npm run emitAPIPy", + "pre-transpile-php": "npm run export-exchanges && npm run emitAPIPhp", + "pre-transpile-go": "npm run export-exchanges && npm run emitAPIGo", + "pre-transpile-js-simple": "npm run export-exchanges && npm run emitAPITs && tsc && npm run validate-types", + "pre-transpile-cs": "npm run export-exchanges && npm run emitAPICs", + "pre-transpile": "npm run export-exchanges && npm run vss && npm run tsBuild && npm run emitAPI && npm run validate-types && npm run tsBuildExamples && npm run copy-python-files && npm run check-js-syntax && npm run bundle", + "pre-transpile-pr": "npm run export-exchanges && npm run tsBuild && npm run emitAPI && npm run check-js-syntax", + "post-transpile": "npm run check-python-syntax && npm run check-php-syntax", + "cli.ts": "tsx ./cli/ts/cli.ts", + "cli.js": "node ./cli/js/cli.js", + "cli.py": "python3 ./examples/py/cli.py", + "cli.php": "php ./examples/php/cli.php", + "cli.cs": "dotnet run --project \"./cs/cli/cli.csproj\"", + "cli.go": "go run -C go ./cli/main.go", + "export-exchanges": "node build/export-exchanges", + "capabilities": "node ./examples/js/exchange-capabilities.js", + "git-ignore-generated-files": "node build/git-ignore-generated-files.cjs", + "git-unignore-generated-files": "node build/git-ignore-generated-files.cjs --unignore", + "update-badges": "node build/update-badges", + "update-links": "node build/update-links", + "transpile": "npm run transpileRest && npm run transpileWs", + "transpileRest": "tsx build/transpile.ts", + "transpileWs": "tsx build/transpileWS.ts", + "force-transpile": "npm run force-transpileRest && npm run force-transpileWs", + "force-transpile-fast": "npm run dev-force-transpile", + "force-transpile-fast-py": "npm run fast-force-transpileRest-py && npm run fast-force-transpileWs-py", + "force-transpile-fast-php": "npm run fast-force-transpileRest-php && npm run fast-force-transpileWs-php", + "dev-force-transpile": "npm run fast-force-transpileRest && npm run fast-force-transpileWs", + "force-transpileRest": "tsx build/transpile.ts --force", + "fast-force-transpileRest-py": "tsx build/transpile.ts --multiprocess --force --python", + "fast-force-transpileRest-php": "tsx build/transpile.ts --multiprocess --force --php", + "fast-force-transpileRest": "tsx build/transpile.ts --multiprocess --force", + "force-transpileWs": "tsx build/transpileWS.ts --force", + "fast-force-transpileWs": "tsx build/transpileWS.ts --multiprocess --force", + "fast-force-transpileWs-py": "tsx build/transpileWS.ts --multiprocess --force --python", + "fast-force-transpileWs-php": "tsx build/transpileWS.ts --multiprocess --force --php", + "vss": "node build/vss", + "lint": "eslint \"ts/src/*.ts\" \"ts/src/base/Exchange.ts\" \"ts/src/pro/*.ts\" --cache --cache-location .cache/eslintcache --cache-strategy metadata", + "check-syntax": "npm run transpile && npm run check-js-syntax && npm run check-python-syntax && npm run check-php-syntax", + "check-js-syntax": "node -e \"console.log(process.cwd())\" && eslint --version && npm run lint", + "eslint": "eslint", + "check-python-syntax": "cd python && tox -e qa && cd ..", + "check-python-types": "cd python && tox -e type && cd ..", + "check-php-syntax": "npm run check-rest-php-syntax && npm run check-ws-php-syntax", + "check-rest-php-syntax": "php -f php/test/custom/syntax.php", + "check-ws-php-syntax": "php -f php/pro/test/custom/syntax.php", + "bundle": "npm run bundle-cjs && npm run bundle-browser", + "bundle-cjs": "rollup -c rollup.config.js", + "bundle-browser": "webpack build -c webpack.config.js && webpack build -c webpack.config.js --optimization-minimize --output-filename ccxt.browser.min.js", + "copy-python-files": "npm run copy-python-package && npm run copy-python-license && npm run copy-python-keys && npm run copy-python-readme", + "copy-python-package": "node build/copy package.json python/package.json", + "copy-python-license": "node build/copy LICENSE.txt python/LICENSE.txt", + "copy-python-keys": "node build/copy keys.json python/keys.json", + "copy-python-readme": "node build/copy README.md python/README.md", + "postinstall": "node postinstall.js", + "validate-types": "tsx build/validate-types.ts", + "runtests": "node run-tests", + "live-tests": "node run-tests --useProxy", + "live-tests-rest": "npm run live-tests", + "live-tests-ws": "npm run live-tests -- --ws", + "live-tests-rest-ts": "npm run live-tests -- --ts", + "live-tests-ws-ts": "npm run live-tests -- --ts --ws", + "live-tests-rest-js": "npm run live-tests -- --js", + "live-tests-ws-js": "npm run live-tests -- --js --ws", + "live-tests-rest-py": "npm run live-tests -- --python", + "live-tests-ws-py": "npm run live-tests -- --python --ws", + "live-tests-rest-php": "npm run live-tests -- --php", + "live-tests-ws-php": "npm run live-tests -- --php --ws", + "live-tests-rest-csharp": "npm run live-tests -- --csharp", + "live-tests-rest-go": "npm run live-tests -- --go", + "live-tests-ws-csharp": "npm run live-tests -- --csharp --ws", + "live-tests-ws-go": "npm run live-tests -- --go --ws", + "ti-ts": "tsx ts/src/test/tests.init.ts", + "ti-js": "node js/src/test/tests.init.js", + "ti-py": "python3 python/ccxt/test/tests_init.py", + "ti-php": "php php/test/tests_init.php", + "ti-cs": "dotnet run --project cs/tests/tests.csproj", + "ti-go": "go run -C go ./tests/main.go", + "request-ts": "npm run ti-ts -- --requestTests", + "request-js": "npm run ti-js -- --requestTests", + "request-py-async": "npm run ti-py -- --requestTests", + "request-py-sync": "npm run ti-py -- --requestTests --sync", + "request-py": "npm run request-py-sync && npm run request-py-async", + "request-php-async": "npm run ti-php -- --requestTests", + "request-php-sync": "npm run ti-php -- --requestTests --sync", + "request-php": "npm run request-php-sync && npm run request-php-async", + "request-cs": "npm run ti-cs -- --requestTests", + "request-go": "npm run ti-go -- --requestTests", + "request-tests//withoutGo": "npm run request-js && npm run request-py && npm run request-php && npm run request-cs", + "request-tests": "npm run request-js && npm run request-py && npm run request-php && npm run request-cs && npm run request-go", + "response-ts": "npm run ti-ts -- --responseTests", + "response-js": "npm run ti-js -- --responseTests", + "response-py-sync": "npm run ti-py -- --responseTests --sync", + "response-py-async": "npm run ti-py -- --responseTests", + "response-py": "npm run response-py-sync && npm run response-py-async", + "response-cs": "npm run ti-cs -- --responseTests", + "response-go": "npm run ti-go -- --responseTests", + "response-php-async": "npm run ti-php -- --responseTests", + "response-php-sync": "npm run ti-php -- --responseTests --sync", + "response-php": "npm run response-php-sync && npm run response-php-async", + "response-tests": "npm run response-js && npm run response-py && npm run response-php && npm run response-cs && npm run response-go", + "response-tests//withoutGo": "npm run response-js && npm run response-py && npm run response-php && npm run response-cs", + "static-updater": "tsx ./utils/update-static-json --update", + "id-tests-js": "npm run ti-js -- --idTests", + "id-tests-py": "npm run ti-py -- --idTests", + "id-tests-php": "npm run ti-php -- --idTests", + "id-tests-cs": "npm run ti-cs -- --idTests", + "id-tests-go": "npm run ti-go -- --idTests", + "id-tests": "npm run id-tests-js && npm run id-tests-py && npm run id-tests-php && npm run id-tests-cs", + "id-tests//withGO": "npm run id-tests-js && npm run id-tests-py && npm run id-tests-php && npm run id-tests-cs && npm run id-tests-go", + "test-base-rest-ts": "npm run ti-ts -- --baseTests", + "test-base-rest-js": "npm run ti-js -- --baseTests", + "test-base-rest-py": "npm run ti-py -- --baseTests", + "test-base-rest-php": "npm run ti-php -- --baseTests", + "test-base-rest-cs": "npm run ti-cs -- --baseTests", + "test-base-rest-go": "npm run ti-go -- --baseTests && cd ../", + "test-base-ws-ts": "npm run ti-ts -- --baseTests --ws", + "test-base-ws-js": "npm run ti-js -- --baseTests --ws", + "test-base-ws-py": "npm run ti-py -- --baseTests --ws", + "test-base-ws-php": "npm run ti-php -- --baseTests --ws", + "test-base-ws-cs": "npm run ti-cs -- --baseTests --ws", + "test-base-ws-go": "npm run ti-go -- --baseTests --ws", + "test-base-rest": "npm run test-base-rest-js && npm run test-base-rest-py && npm run test-base-rest-php && npm run test-base-rest-cs", + "test-base-ws": "npm run test-base-ws-js && npm run test-base-ws-py && npm run test-base-ws-php && npm run test-base-ws-cs && npm run test-base-ws-go", + "test": "npm run build && npm run commonjs-test && npm run id-tests && npm run request-tests && npm run response-tests && npm run live-tests", + "commonjs-test": "node ./utils/test-commonjs.cjs", + "package-test": "./utils/package-test.sh", + "test-freshness": "tsx ./utils/test-freshness.ts", + "benchmark": "tsx examples/ts/benchmark.ts", + "cleanup-old-tags": "tsx ./build/cleanup-old-tags.ts", + "test-types-go": "go run -C go ./tests/types/types.go", + "test-go": "npm run test-types-go && npm run response-go && npm run request-go && npm run test-base-rest-go && npm run test-base-ws-go" + }, + "types": "./js/ccxt.d.ts", + "devDependencies": { + "@rollup/plugin-commonjs": "^21.0.3", + "@rollup/plugin-json": "^4.1.0", + "@rollup/plugin-node-resolve": "^15.2.3", + "@types/node": "^18.15.11", + "@typescript-eslint/eslint-plugin": "^5.30.5", + "@typescript-eslint/parser": "^5.30.5", + "ansicolor": "^2.0.0", + "as-table": "^1.0.55", + "asciichart": "^1.5.25", + "assert": "^2.0.0", + "ast-transpiler": "^0.0.71", + "blessed": "^0.1.81", + "clipboardy": "^5.0.0", + "commander": "^14.0.1", + "docsify": "^4.13.1", + "eslint-config-airbnb-base": "15.0.0", + "eslint-plugin-import": "2.25.4", + "eslint-plugin-jsdoc": "^46.9.0", + "eslint": "8.22.0", + "https-proxy-agent": "^5.0.1", + "jsdoc-to-markdown": "^8.0.0", + "ololog": "^1.1.175", + "open": "^10.2.0", + "ora": "^9.0.0", + "piscina": "^3.2.0", + "protobufjs": "^7.5.3", + "replace-in-file": "^6.3.5", + "rollup-plugin-execute": "1.1.1", + "rollup": "^2.70.1", + "terser-webpack-plugin": "^5.3.9", + "ts-loader": "^9.4.2", + "tsx": "^4.7.2", + "typescript": "4.7.4", + "webpack-cli": "^5.0.1", + "webpack": "^5.76.2" + }, + "author": { + "name": "Igor Kroitor", + "email": "igor.kroitor@gmail.com", + "url": "https://github.com/kroitor" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/ccxt/ccxt/issues" + }, + "homepage": "https://ccxt.com", + "keywords": [ + "algorithmic", + "algotrading", + "altcoin", + "altcoins", + "api", + "arbitrage", + "real-time", + "realtime", + "backtest", + "backtesting", + "bitcoin", + "bot", + "btc", + "cny", + "coin", + "coins", + "crypto", + "cryptocurrency", + "crypto currency", + "crypto market", + "currency", + "currencies", + "darkcoin", + "dash", + "digital currency", + "doge", + "dogecoin", + "e-commerce", + "etc", + "eth", + "ether", + "ethereum", + "exchange", + "exchanges", + "eur", + "framework", + "invest", + "investing", + "investor", + "library", + "light", + "litecoin", + "ltc", + "market", + "market data", + "markets", + "merchandise", + "merchant", + "minimal", + "ohlcv", + "order", + "orderbook", + "order book", + "price", + "price data", + "pricefeed", + "private", + "public", + "ripple", + "strategy", + "ticker", + "tickers", + "toolkit", + "trade", + "trader", + "trading", + "usd", + "volume", + "websocket", + "websockets", + "web socket", + "web sockets", + "ws", + "xbt", + "xrp", + "zec", + "zerocoin" + ], + "collective": { + "type": "opencollective", + "url": "https://opencollective.com/ccxt", + "logo": "https://opencollective.com/ccxt/logo.txt" + }, + "ethereum": "0x26a3CB49578F07000575405a57888681249c35Fd", + "dependencies": { + "ws": "^8.8.1" + } +} diff --git a/qa.py b/qa.py new file mode 100644 index 0000000..a739b5f --- /dev/null +++ b/qa.py @@ -0,0 +1,17 @@ +import os +import sys + +if len(sys.argv) > 1: + flak8_args = sys.argv[1:] + command = 'flake8 --ignore=F722,F841,F821,W504,E402,E501,E275,E902 ' + 'ccxt/' + ' ccxt/'.join(flak8_args) + print(f'\n{command}\n') + os.system(command) + exit() + +if os.name == 'posix': + code = os.WEXITSTATUS(os.system('./fastflake.sh')) + exit(code) +else: + command = 'flake8 --ignore=F722,F841,F821,W504,E402,E501,E275,E902 --exclude static_dependencies,node_modules,.tox,build' + print(f'\n{command}\n') + os.system(command) diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..cbbbfbb --- /dev/null +++ b/setup.cfg @@ -0,0 +1,23 @@ +[bdist_wheel] +# This flag says that the code is written to work on both Python 2 and Python +# 3. If at all possible, it is good practice to do this. If you cannot, you +# will need to generate wheels for each Python version that you support. +universal=1 + +[flake8] +ignore = E501 +exclude = + .ropeproject, + .tox, + .eggs, + # No need to traverse our git directory + .git, + # There's no value in checking cache directories + __pycache__, + # Other special cases + node_modules, + .nyc_output, + build, + tmp, + # No need to check the basecode + ccxt.py diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..775df05 --- /dev/null +++ b/setup.py @@ -0,0 +1,107 @@ +# prefer setuptools over distutils +from setuptools import setup, find_packages + +# use a consistent encoding +from codecs import open +from os import path +import json +import sys + +is_python_2 = sys.version_info < (3, 0) + +here = path.abspath(path.dirname(__file__)) +root = path.dirname(here) + +readme = path.join(here, 'README.md') +package_json = path.join(here, 'package.json') + +# a workaround when installing locally from git repository with pip install -e . +if not path.isfile(package_json): + package_json = path.join(root, 'package.json') + +# long description from README file +with open(readme, encoding='utf-8') as f: + long_description = f.read() + +# version number and all other params from package.json +with open(package_json, encoding='utf-8') as f: + package = json.load(f) + +project_urls = { + 'Homepage': 'https://ccxt.com', + 'Documentation': 'https://github.com/ccxt/ccxt/wiki', + 'Discord': 'https://discord.gg/ccxt', + 'Twitter': 'https://twitter.com/ccxt_official', + 'Funding': 'https://opencollective.com/ccxt', +} + +setup( + + name=package['name'], + version=package['version'], + description=package['description'], + long_description=long_description, + long_description_content_type='text/markdown', + + # will switch from rst to md shortly + # long_description_content_type='text/markdown', + + url=package['homepage'], + + author=package['author']['name'], + author_email=package['author']['email'], + + license=package['license'], + + classifiers=[ + 'Development Status :: 4 - Beta', + 'Intended Audience :: Developers', + 'Intended Audience :: Financial and Insurance Industry', + 'Intended Audience :: Information Technology', + 'Topic :: Software Development :: Build Tools', + 'Topic :: Office/Business :: Financial :: Investment', + 'License :: OSI Approved :: MIT License', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: JavaScript', + 'Programming Language :: PHP', + 'Operating System :: OS Independent', + 'Environment :: Console' + ], + + keywords=package['keywords'], + packages=find_packages(exclude=['ccxt.async_support*'] if is_python_2 else []), + + install_requires=[ + 'setuptools>=60.9.0', + 'certifi>=2018.1.18', + 'requests>=2.18.4', + 'cryptography>=2.6.1', + 'typing_extensions>=4.4.0', + ], + + extras_require={ + ':python_version>="3.5.2"': [ + 'aiohttp>=3.10.11', + 'aiodns>=1.1.1', + 'yarl>=1.7.2', + ], + ':python_version>="3.9" and python_version<="3.13"': [ + 'coincurve==21.0.0', + ], + 'qa': [ + 'ruff==0.0.292', + 'tox>=4.8.0', + ], + 'type': [ + 'mypy==1.6.1', + ], + }, + project_urls=project_urls, +) diff --git a/test/_test_mt5_fixed.py b/test/_test_mt5_fixed.py new file mode 100644 index 0000000..d124cd5 --- /dev/null +++ b/test/_test_mt5_fixed.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import asyncio +import logging +import sys +import os + +# 添加 ccxt 路径 +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from ccxt import mt5 as mt5_sync +from ccxt.async_support import mt5 as mt5_async + +# 配置日志 +logging.basicConfig(level=logging.INFO, format='%(message)s') +logger = logging.getLogger('MT5-Fixed-Test') + +# 测试配置 - 使用你的配置 +TEST_CONFIG = { + 'apiKey': '76888962', + 'secret': 'LZ-trade666888', + 'hostname': '43.167.188.220:5000', + 'host': '18.163.85.196', + 'port': 443, + 'sandbox': True, + 'verbose': True, +} + + +def test_url_fix(): + """测试 URL 修复""" + logger.info("🔧 测试 URL 修复...") + + try: + exchange = mt5_sync(TEST_CONFIG) + + # 检查 URLs 配置 + logger.info("检查 URLs 配置:") + logger.info(f" Public API: {exchange.urls['api']['public']}") + logger.info(f" Private API: {exchange.urls['api']['private']}") + + # 测试连接 + logger.info("\n测试连接...") + token = exchange.get_token() + logger.info(f"✅ 连接成功! Token: {token}") + + return True + + except Exception as e: + logger.error(f"❌ 测试失败: {e}") + import traceback + traceback.print_exc() + return False + + +async def test_async_url_fix(): + """测试异步 URL 修复""" + logger.info("\n🔧 测试异步 URL 修复...") + + exchange = None + try: + exchange = mt5_async(TEST_CONFIG) + + # 检查 URLs 配置 + logger.info("检查异步 URLs 配置:") + logger.info(f" Public API: {exchange.urls['api']['public']}") + logger.info(f" Private API: {exchange.urls['api']['private']}") + + # 测试连接 + logger.info("\n测试异步连接...") + token = await exchange.get_token() + logger.info(f"✅ 异步连接成功! Token: {token}") + + return True + + except Exception as e: + logger.error(f"❌ 异步测试失败: {e}") + import traceback + traceback.print_exc() + return False + finally: + if exchange: + await exchange.close() + + +def test_direct_connection(): + """测试直接连接""" + logger.info("\n🔧 测试直接连接...") + + try: + # 使用 requests 直接测试连接 + import requests + + url = "http://43.167.188.220:5000/Connect" + params = { + 'user': '76888962', + 'password': 'LZ-trade666888', + 'host': '18.163.85.196', + 'port': 443, + 'connectTimeoutSeconds': 30 + } + + logger.info(f"测试直接请求: {url}") + response = requests.get(url, params=params, timeout=30) + + logger.info(f"状态码: {response.status_code}") + logger.info(f"响应内容: {response.text}") + + if response.status_code == 200: + logger.info("✅ 直接连接成功!") + return True + else: + logger.error(f"❌ 直接连接失败: {response.status_code}") + return False + + except Exception as e: + logger.error(f"❌ 直接连接测试失败: {e}") + return False + + +async def main(): + """运行修复测试""" + print("🚀 MT5 URL 修复测试") + print("="*50) + + # 测试直接连接 + direct_success = test_direct_connection() + + # 测试同步版本 + sync_success = test_url_fix() + + # 测试异步版本 + async_success = await test_async_url_fix() + + print("\n" + "="*50) + print("📊 修复测试结果") + print(f"直接连接: {'✅ 通过' if direct_success else '❌ 失败'}") + print(f"同步版本: {'✅ 通过' if sync_success else '❌ 失败'}") + print(f"异步版本: {'✅ 通过' if async_success else '❌ 失败'}") + + if direct_success and sync_success and async_success: + print("🎉 所有修复测试通过!") + else: + print("\n⚠️ 如果直接连接失败,请检查:") + print(" 1. 网络连接是否正常") + print(" 2. 服务器地址是否正确") + print(" 3. 防火墙设置") + print(" 4. 服务器状态") + + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/test/_test_mt5_manual.py b/test/_test_mt5_manual.py new file mode 100644 index 0000000..242bb72 --- /dev/null +++ b/test/_test_mt5_manual.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import requests +import asyncio +import aiohttp +import logging + +logging.basicConfig(level=logging.INFO, format='%(message)s') +logger = logging.getLogger('MT5-Manual-Test') + +# 配置 +CONFIG = { + 'user': '76888962', + 'password': 'LZ-trade666888', + 'host': '18.163.85.196', + 'port': 443, + 'connectTimeoutSeconds': 30 +} + +BASE_URL = "http://43.167.188.220:5000" + + +def test_manual_connect(): + """手动测试连接""" + logger.info("🔧 手动测试连接...") + + try: + url = f"{BASE_URL}/Connect" + + logger.info(f"请求URL: {url}") + logger.info(f"请求参数: {CONFIG}") + + response = requests.get(url, params=CONFIG, timeout=30) + + logger.info(f"状态码: {response.status_code}") + logger.info(f"响应头: {dict(response.headers)}") + logger.info(f"响应内容: {response.text}") + + if response.status_code == 200: + token = response.text + logger.info(f"✅ 连接成功! Token: {token}") + + # 测试其他端点 + test_other_endpoints(token) + return True + else: + logger.error(f"❌ 连接失败: {response.status_code}") + return False + + except requests.exceptions.RequestException as e: + logger.error(f"❌ 请求异常: {e}") + return False + except Exception as e: + logger.error(f"❌ 未知错误: {e}") + return False + + +def test_other_endpoints(token): + """测试其他端点""" + logger.info("\n🔧 测试其他端点...") + + endpoints = [ + '/CheckConnect', + '/AccountSummary', + '/Symbols', + '/GetQuote?symbol=EURUSD' + ] + + for endpoint in endpoints: + try: + url = f"{BASE_URL}{endpoint}" + params = {'id': token} + + logger.info(f"测试端点: {endpoint}") + response = requests.get(url, params=params, timeout=10) + + logger.info(f" 状态码: {response.status_code}") + if response.status_code == 200: + logger.info(f" ✅ 成功") + if len(response.text) < 100: # 避免输出过长 + logger.info(f" 响应: {response.text}") + else: + logger.error(f" ❌ 失败") + + except Exception as e: + logger.error(f" ❌ 错误: {e}") + + +async def test_async_manual(): + """异步手动测试""" + logger.info("\n🔧 异步手动测试...") + + try: + url = f"{BASE_URL}/Connect" + + async with aiohttp.ClientSession() as session: + logger.info(f"异步请求URL: {url}") + async with session.get(url, params=CONFIG, timeout=30) as response: + logger.info(f"异步状态码: {response.status}") + text = await response.text() + logger.info(f"异步响应: {text}") + + if response.status == 200: + logger.info("✅ 异步连接成功!") + return True + else: + logger.error("❌ 异步连接失败!") + return False + + except Exception as e: + logger.error(f"❌ 异步测试失败: {e}") + return False + + +def main(): + """运行手动测试""" + print("🚀 MT5 手动连接测试") + print("="*50) + + # 同步测试 + sync_success = test_manual_connect() + + # 异步测试 + async_success = asyncio.run(test_async_manual()) + + print("\n" + "="*50) + print("📊 手动测试结果") + print(f"同步测试: {'✅ 通过' if sync_success else '❌ 失败'}") + print(f"异步测试: {'✅ 通过' if async_success else '❌ 失败'}") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/test/atest_mt5_integration.py b/test/atest_mt5_integration.py new file mode 100644 index 0000000..347a711 --- /dev/null +++ b/test/atest_mt5_integration.py @@ -0,0 +1,529 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import asyncio +import logging +import sys +import os +from datetime import datetime + +# 添加 ccxt 路径 +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from ccxt import mt5 as mt5_sync +from ccxt.async_support import mt5 as mt5_async +from ccxt.pro import mt5 as mt5_pro +from ccxt.base.errors import ExchangeError, AuthenticationError, InvalidOrder + +# 配置日志 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.StreamHandler(), + logging.FileHandler('mt5_test.log', encoding='utf-8') + ] +) +logger = logging.getLogger('MT5-Test') + +# 测试配置 +TEST_CONFIG = { + 'apiKey': '76888962', + 'secret': 'LZ-trade666888', + 'verbose': False, # 启用详细日志 + 'hostname': '43.167.188.220:5000', + 'host': '18.163.85.196', + 'port': 443, +} + + +class MT5SyncTest: + """同步版本测试""" + + def __init__(self): + self.exchange = mt5_sync(TEST_CONFIG) + self.test_results = {} + + def run_all_tests(self): + """运行所有同步测试""" + logger.info("🚀 开始同步版本测试") + + tests = [ + self.test_connection, + self.test_token_management, + self.test_markets, + self.test_balance, + self.test_ticker, + self.test_account_details, + self.test_orders, + self.test_order_operations, + ] + + for test in tests: + try: + test_name = test.__name__ + logger.info(f"\n{'='*50}") + logger.info(f"执行测试: {test_name}") + logger.info(f"{'='*50}") + + result = test() + self.test_results[test_name] = result + status = "✅ 通过" if result else "❌ 失败" + logger.info(f"{status} - {test_name}") + + except Exception as e: + logger.error(f"❌ 测试失败 {test.__name__}: {e}") + self.test_results[test.__name__] = False + + self.print_test_summary() + return self.test_results + + def test_connection(self): + """测试连接""" + try: + logger.info("测试服务器连接...") + result = self.exchange.ping_server() + logger.info(f"Ping 结果: {result}") + return result is True + except Exception as e: + logger.error(f"连接测试失败: {e}") + return False + + def test_token_management(self): + """测试 Token 管理""" + try: + logger.info("测试 Token 获取...") + token = self.exchange.get_token() + logger.info(f"获取到 Token: {token}") + + # logger.info("测试连接检查...") + # check_result = self.exchange.check_connect() + # logger.info(f"连接检查: {check_result}") + + logger.info("测试服务器时区...") + timezone = self.exchange.server_timezone() + logger.info(f"服务器时区: {timezone}") + + return token is not None and timezone is not None + except Exception as e: + logger.error(f"Token 管理测试失败: {e}") + return False + + def test_markets(self): + """测试市场数据""" + try: + logger.info("获取交易对列表...") + markets = self.exchange.fetch_markets() + logger.info(f"获取到 {len(markets)} 个交易对") + + # 显示前5个交易对 + for i, market in enumerate(markets[:5]): + logger.info(f" {i+1}. {market['symbol']} - {market['base']}/{market['quote']}") + + if len(markets) > 5: + logger.info(f" ... 还有 {len(markets) - 5} 个交易对") + + return len(markets) > 0 + except Exception as e: + logger.error(f"市场数据测试失败: {e}") + return False + + def test_balance(self): + """测试余额查询""" + try: + logger.info("获取账户余额...") + balance = self.exchange.fetch_balance() + logger.info(f"余额信息: {balance}") + + if 'USDT' in balance['total']: + total = balance['total']['USDT'] + free = balance['free']['USDT'] + used = balance['used']['USDT'] + logger.info(f"USDT 余额 - 总额: {total}, 可用: {free}, 占用: {used}") + + return balance is not None + except Exception as e: + logger.error(f"余额查询测试失败: {e}") + return False + + def test_ticker(self): + """测试行情数据""" + try: + symbols_to_test = ['EUR/USD', 'GBP/USD', 'USD/JPY'] + + for symbol in symbols_to_test: + try: + logger.info(f"获取 {symbol} 行情...") + ticker = self.exchange.fetch_ticker(symbol) + + if ticker: + logger.info(f" {symbol}: 买={ticker['bid']}, 卖={ticker['ask']}, 最后={ticker['last']}") + else: + logger.warning(f" 无法获取 {symbol} 行情") + + except Exception as e: + logger.warning(f" 获取 {symbol} 行情失败: {e}") + + # 测试批量获取行情 + logger.info("测试批量获取行情...") + tickers = self.exchange.fetch_tickers(['EUR/USD', 'GBP/USD']) + logger.info(f"批量获取到 {len(tickers)} 个行情") + + return True + except Exception as e: + logger.error(f"行情数据测试失败: {e}") + return False + + def test_account_details(self): + """测试账户详情""" + try: + logger.info("获取账户详情...") + account_details = self.exchange.fetch_account_details() + logger.info(f"账户详情: {account_details}") + + required_fields = ['serverName', 'user', 'currency', 'accountLeverage'] + for field in required_fields: + if field in account_details: + logger.info(f" {field}: {account_details[field]}") + + return account_details is not None + except Exception as e: + logger.error(f"账户详情测试失败: {e}") + return False + + def test_orders(self): + """测试订单查询""" + try: + logger.info("获取未平仓订单...") + open_orders = self.exchange.fetch_open_orders() + logger.info(f"未平仓订单数量: {len(open_orders)}") + + for order in open_orders[:3]: # 显示前3个订单 + logger.info(f" 订单 {order['id']}: {order['symbol']} {order['side']} {order['type']} {order['status']}") + + logger.info("获取已平仓订单...") + closed_orders = self.exchange.fetch_closed_orders() + logger.info(f"已平仓订单数量: {len(closed_orders)}") + + return True + except Exception as e: + logger.error(f"订单查询测试失败: {e}") + return False + + def test_order_operations(self): + """测试订单操作(只测试不实际下单)""" + try: + logger.info("测试订单创建参数验证...") + + # 测试市价单参数 + market_order_params = { + 'symbol': 'EUR/USD', + 'type': 'market', + 'side': 'buy', + 'amount': 0.01, + } + + # 测试限价单参数 + limit_order_params = { + 'symbol': 'EUR/USD', + 'type': 'limit', + 'side': 'buy', + 'amount': 0.01, + 'price': 1.0800, + } + + logger.info("✅ 订单参数验证通过") + logger.info("注意: 实际下单测试需要在真实环境中进行") + + return True + except Exception as e: + logger.error(f"订单操作测试失败: {e}") + return False + + def print_test_summary(self): + """打印测试总结""" + logger.info("\n" + "="*60) + logger.info("📊 同步版本测试总结") + logger.info("="*60) + + passed = sum(1 for result in self.test_results.values() if result) + total = len(self.test_results) + + for test_name, result in self.test_results.items(): + status = "✅ 通过" if result else "❌ 失败" + logger.info(f" {test_name}: {status}") + + logger.info(f"\n总体结果: {passed}/{total} 通过") + + if passed == total: + logger.info("🎉 所有测试通过!") + else: + logger.info("⚠️ 部分测试失败,请检查日志") + + +class MT5AsyncTest: + """异步版本测试""" + + def __init__(self): + self.exchange = None + self.test_results = {} + + async def initialize(self): + """初始化异步交易所""" + self.exchange = mt5_async(TEST_CONFIG) + + async def run_all_tests(self): + """运行所有异步测试""" + logger.info("\n🚀 开始异步版本测试") + + await self.initialize() + + tests = [ + self.test_connection_async, + self.test_markets_async, + self.test_balance_async, + self.test_ticker_async, + self.test_orders_async, + ] + + for test in tests: + try: + test_name = test.__name__ + logger.info(f"\n{'='*50}") + logger.info(f"执行测试: {test_name}") + logger.info(f"{'='*50}") + + result = await test() + self.test_results[test_name] = result + status = "✅ 通过" if result else "❌ 失败" + logger.info(f"{status} - {test_name}") + + except Exception as e: + logger.error(f"❌ 测试失败 {test.__name__}: {e}") + self.test_results[test.__name__] = False + + await self.exchange.close() + self.print_test_summary() + return self.test_results + + async def test_connection_async(self): + """测试异步连接""" + try: + logger.info("测试异步 Token 获取...") + token = await self.exchange.get_token() + logger.info(f"获取到 Token: {token}") + return token is not None + except Exception as e: + logger.error(f"异步连接测试失败: {e}") + return False + + async def test_markets_async(self): + """测试异步市场数据""" + try: + logger.info("获取异步交易对列表...") + markets = await self.exchange.fetch_markets() + logger.info(f"获取到 {len(markets)} 个交易对") + return len(markets) > 0 + except Exception as e: + logger.error(f"异步市场数据测试失败: {e}") + return False + + async def test_balance_async(self): + """测试异步余额查询""" + try: + logger.info("获取异步账户余额...") + balance = await self.exchange.fetch_balance() + logger.info(f"余额信息: {balance}") + return balance is not None + except Exception as e: + logger.error(f"异步余额查询测试失败: {e}") + return False + + async def test_ticker_async(self): + """测试异步行情数据""" + try: + logger.info("获取异步行情...") + ticker = await self.exchange.fetch_ticker('EUR/USD') + logger.info(f"EUR/USD: 买={ticker['bid']}, 卖={ticker['ask']}") + return ticker is not None + except Exception as e: + logger.error(f"异步行情数据测试失败: {e}") + return False + + async def test_orders_async(self): + """测试异步订单查询""" + try: + logger.info("获取异步未平仓订单...") + open_orders = await self.exchange.fetch_open_orders() + logger.info(f"未平仓订单数量: {len(open_orders)}") + return True + except Exception as e: + logger.error(f"异步订单查询测试失败: {e}") + return False + + def print_test_summary(self): + """打印测试总结""" + logger.info("\n" + "="*60) + logger.info("📊 异步版本测试总结") + logger.info("="*60) + + passed = sum(1 for result in self.test_results.values() if result) + total = len(self.test_results) + + for test_name, result in self.test_results.items(): + status = "✅ 通过" if result else "❌ 失败" + logger.info(f" {test_name}: {status}") + + logger.info(f"\n总体结果: {passed}/{total} 通过") + + +class MT5WebSocketTest: + """WebSocket 测试""" + + def __init__(self): + self.exchange = None + self.received_messages = [] + + async def initialize(self): + """初始化 WebSocket 交易所""" + self.exchange = mt5_pro(TEST_CONFIG) + + async def test_websocket_orders(self): + """测试 WebSocket 订单监听""" + try: + await self.initialize() + + logger.info("\n🔌 开始 WebSocket 订单监听测试") + logger.info("监听订单更新...") + + # 设置超时 + timeout = 30 # 30秒后停止监听 + + async def order_listener(): + try: + orders = await self.exchange.watch_orders() + logger.info(f"📦 收到订单更新: {len(orders)} 个订单") + + for order in orders: + logger.info(f" 订单 {order['id']}: {order['symbol']} {order['side']} {order['status']}") + self.received_messages.append({ + 'type': 'order', + 'data': order, + 'timestamp': datetime.now() + }) + + except Exception as e: + logger.error(f"订单监听错误: {e}") + + # 运行监听器 + await asyncio.wait_for(order_listener(), timeout=timeout) + + return len(self.received_messages) > 0 + + except asyncio.TimeoutError: + logger.info("⏰ WebSocket 监听超时(正常结束)") + return len(self.received_messages) > 0 + except Exception as e: + logger.error(f"WebSocket 测试失败: {e}") + return False + finally: + if self.exchange: + await self.exchange.close() + + async def test_websocket_balance(self): + """测试 WebSocket 余额监听""" + try: + await self.initialize() + + logger.info("\n💰 开始 WebSocket 余额监听测试") + logger.info("监听余额更新...") + + timeout = 20 # 20秒后停止监听 + + async def balance_listener(): + try: + balance = await self.exchange.watch_balance() + logger.info(f"💰 收到余额更新") + logger.info(f" 余额信息: {balance}") + + self.received_messages.append({ + 'type': 'balance', + 'data': balance, + 'timestamp': datetime.now() + }) + + except Exception as e: + logger.error(f"余额监听错误: {e}") + + await asyncio.wait_for(balance_listener(), timeout=timeout) + + return len([msg for msg in self.received_messages if msg['type'] == 'balance']) > 0 + + except asyncio.TimeoutError: + logger.info("⏰ WebSocket 余额监听超时(正常结束)") + return len([msg for msg in self.received_messages if msg['type'] == 'balance']) > 0 + except Exception as e: + logger.error(f"WebSocket 余额测试失败: {e}") + return False + finally: + if self.exchange: + await self.exchange.close() + + +async def main(): + """主测试函数""" + print("="*70) + print("MT5 集成测试套件") + print("="*70) + + # 同步测试 + sync_test = MT5SyncTest() + sync_results = sync_test.run_all_tests() + + # 异步测试 + async_test = MT5AsyncTest() + async_results = await async_test.run_all_tests() + + # WebSocket 测试 + ws_test = MT5WebSocketTest() + + print("\n" + "="*70) + print("🔌 WebSocket 测试(需要实际交易活动)") + print("="*70) + + ws_order_result = await ws_test.test_websocket_orders() + ws_balance_result = await ws_test.test_websocket_balance() + + # 最终总结 + print("\n" + "="*70) + print("🎯 最终测试总结") + print("="*70) + + sync_passed = sum(1 for result in sync_results.values() if result) + async_passed = sum(1 for result in async_results.values() if result) + + print(f"同步版本: {sync_passed}/{len(sync_results)} 通过") + print(f"异步版本: {async_passed}/{len(async_results)} 通过") + print(f"WebSocket 订单: {'✅ 通过' if ws_order_result else '❌ 失败'}") + print(f"WebSocket 余额: {'✅ 通过' if ws_balance_result else '❌ 失败'}") + + total_tests = len(sync_results) + len(async_results) + 2 + total_passed = sync_passed + async_passed + ws_order_result + ws_balance_result + + print(f"\n总体结果: {total_passed}/{total_tests} 通过") + + if total_passed == total_tests: + print("🎉 所有测试通过!MT5 集成工作正常") + else: + print("⚠️ 部分测试失败,请检查日志文件 'mt5_test.log'") + + +if __name__ == "__main__": + try: + asyncio.run(main()) + except KeyboardInterrupt: + print("\n👋 用户中断测试") + except Exception as e: + print(f"❌ 测试执行错误: {e}") + import traceback + traceback.print_exc() \ No newline at end of file diff --git a/test/atest_mt5_quick.py b/test/atest_mt5_quick.py new file mode 100644 index 0000000..3d6bb21 --- /dev/null +++ b/test/atest_mt5_quick.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import asyncio +import logging +from ccxt import mt5 as mt5_sync +from ccxt.async_support import mt5 as mt5_async + +# 简单日志配置 +logging.basicConfig(level=logging.INFO, format='%(message)s') +logger = logging.getLogger('MT5-Quick-Test') + +CONFIG = { + 'apiKey': '76888962', + 'secret': 'LZ-trade666888', + 'verbose': False, # 启用详细日志 + 'hostname': '43.167.188.220:5000', + 'host': '18.163.85.196', + 'port': 443, +} + + +def quick_sync_test(): + """快速同步测试""" + logger.info("🔧 快速同步测试...") + + try: + exchange = mt5_sync(CONFIG) + + # 测试连接 + logger.info("1. 测试连接...") + token = exchange.get_token() + logger.info(f" ✅ Token: {token}") + + # # 测试交易对 + logger.info("4. 测试交易对...") + markets = exchange.fetch_markets() + logger.info(f" ✅ 交易对数量: {len(markets)}") + + # # 测试余额 + # logger.info("2. 测试余额...") + # balance = exchange.fetch_balance() + # logger.info(f" ✅ 余额: {balance['total'].get('USDT', 'N/A')}") + + # 测试行情 + # logger.info("3. 测试行情...") + # ticker = exchange.fetch_ticker('EUR/USD') + # logger.info(f" ✅ EUR/USD: {ticker['bid']} / {ticker['ask']}") + + # 测试订单 + logger.info("3. 测试订单...") + ticker = exchange.fetch_closed_orders('EUR/USD') + logger.info(f" ✅ EUR/USD: {ticker['bid']} / {ticker['ask']}") + + + logger.info("🎉 快速同步测试完成!") + return True + + except Exception as e: + logger.error(f"❌ 快速同步测试失败: {e}") + return False + + +async def quick_async_test(): + """快速异步测试""" + logger.info("\n🔧 快速异步测试...") + + try: + exchange = mt5_async(CONFIG) + + # 测试连接 + logger.info("1. 测试异步连接...") + token = await exchange.get_token() + logger.info(f" ✅ Token: {token}") + + # 测试余额 + logger.info("2. 测试异步余额...") + balance = await exchange.fetch_balance() + logger.info(f" ✅ 余额: {balance['total'].get('USDT', 'N/A')}") + + # 测试行情 + logger.info("3. 测试异步行情...") + ticker = await exchange.fetch_ticker('EUR/USD') + logger.info(f" ✅ EUR/USD: {ticker['bid']} / {ticker['ask']}") + + await exchange.close() + logger.info("🎉 快速异步测试完成!") + return True + + except Exception as e: + logger.error(f"❌ 快速异步测试失败: {e}") + return False + + +async def main(): + """运行快速测试""" + print("🚀 MT5 快速测试") + print("="*40) + + # 同步测试 + sync_success = quick_sync_test() + + # 异步测试 + # async_success = await quick_async_test() + async_success = True + + print("\n" + "="*40) + print("📊 快速测试结果") + print(f"同步测试: {'✅ 通过' if sync_success else '❌ 失败'}") + print(f"异步测试: {'✅ 通过' if async_success else '❌ 失败'}") + + if sync_success and async_success: + print("🎉 所有快速测试通过!") + else: + print("⚠️ 部分测试失败,请运行完整测试套件查看详情") + + +if __name__ == "__main__": + # asyncio.run(main()) + + exchange = mt5_sync(CONFIG) + + # 测试连接 + logger.info("1. 测试连接...") + token = exchange.get_token() + logger.info(f" ✅ Token: {token}") + + # # 测试交易对 + logger.info("4. 测试交易对...") + markets = exchange.fetch_markets() + logger.info(f" ✅ 交易对数量: {len(markets)}") + + + logger.info("3. 测试订单...") + ticker = exchange.fetch_closed_orders('BTC/USD') + logger.info(f"{ticker}") \ No newline at end of file diff --git a/test/test.py b/test/test.py new file mode 100644 index 0000000..ded87a5 --- /dev/null +++ b/test/test.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python3 + +import ccxt +import sys +import json + +def test_mt5_comprehensive(): + print("=== MT5 Comprehensive Test ===") + + try: + # 创建 MT5 实例 + exchange = ccxt.mt5({ + 'apiKey': '76888962', + 'secret': 'LZ-trade666888', + 'verbose': True, # 启用详细日志 + 'hostname': '43.167.188.220:5000', + 'options': { + # 'server': '147.160.254.81:443', # 使用服务器名称 + # 或者 + 'host': '18.163.85.196', + 'port': 443, + }, + }) + + # 测试连接 + print("\n🔗 Testing connection...") + try: + token = exchange.get_token() + print(f"✅ Connected successfully! Token: {token[:20]}...") + except Exception as e: + print(f"❌ Connection failed: {e}") + return False + + + # print("\n💰 Testing ping...") + # try: + # ping = exchange.ping_server() + # print(ping) + # except Exception as e: + # print(f"⚠️ ping fetch failed: {e}") + + + # 先加载市场数据 + # print("\n📊 Loading markets first...") + # try: + # markets = exchange.load_markets() + # print(f"✅ Markets loaded: {len(markets)} symbols") + # except Exception as e: + # print(f"⚠️ Markets load failed: {e}") + + + # 测试余额 + # print("\n💰 Testing balance...") + # try: + # balance = exchange.fetch_balance() + # print(f"✅ Balance fetched: {balance}") + # except Exception as e: + # print(f"⚠️ Balance fetch failed: {e}") + + + # print("\n💰 Testing AccountDetails...") + # try: + # account_details = exchange.fetch_account_details() + # print(f"✅ AccountDetails fetched: {account_details}") + # except Exception as e: + # print(f"⚠️ AccountDetails fetch failed: {e}") + + # 测试交易对 + # print("\n📊 Testing markets...") + # try: + # markets = exchange.fetch_markets() + # print(f"✅ Markets fetched: {len(markets)} symbols") + # if markets: + # for i, market in enumerate(markets[:3]): + # print(f" {i+1}. {market['symbol']}") + # except Exception as e: + # print(f"⚠️ Markets fetch failed: {e}") + + # 测试行情 + # print("\n💰 Testing ticker...") + # try: + # ticker = exchange.fetch_ticker('BTCUSD') + # print(ticker) + # # print(f"✅ Ticker fetched: {ticker['symbol']} - Bid: {ticker['bid']}, Ask: {ticker['ask']}") + # except Exception as e: + # print(f"⚠️ Ticker fetch failed: {e}") + + # print("\n💰 Testing order_book...") + # try: + # order_book = exchange.fetch_ticker('BTCUSD') + # print(order_book) + # except Exception as e: + # print(f"⚠️ order_book fetch failed: {e}") + + # 测试时区 + # print("\n💰 Testing server_timezone...") + # try: + # timezone = exchange.server_timezone() + # print(timezone) + # # print(f"✅ Ticker fetched: {ticker['symbol']} - Bid: {ticker['bid']}, Ask: {ticker['ask']}") + # except Exception as e: + # print(f"⚠️ server_timezone fetch failed: {e}") + + # 测试市场观察行情 - 使用不同的符号测试 + # print("\n📊 Testing market watch ticker...") + # test_symbols = ['XAUUSD+'] # 多试几个符号 + # for symbol in test_symbols: + # try: + # market_watch = exchange.fetch_ticker_with_market_watch(symbol) + # print(f"✅ Market watch ticker for {symbol}:") + # print(f" High: {market_watch['high']}") + # print(f" Low: {market_watch['low']}") + # print(f" Open: {market_watch['open']}") + # print(f" Close: {market_watch['close']}") + # break # 成功一个就停止 + # except Exception as e: + # print(f"⚠️ Market watch for {symbol} failed: {e}") + + # 测试K线数据 + # print("\n🕯️ Testing XAUUSD+...") + # try: + # ohlcv = exchange.fetch_ohlcv('XAUUSD+', '1h', limit=3) # 使用 XAUUSD+ 测试 + # print(f"✅ XAUUSD+ fetched: {len(ohlcv)} bars") + # for i, candle in enumerate(ohlcv): + # print(f" Bar {i+1}: Time={candle[0]}, O={candle[1]}, H={candle[2]}, L={candle[3]}, C={candle[4]}, V={candle[5]}") + # except Exception as e: + # print(f"⚠️ XAUUSD+ failed: {e}") + + # 测试订单查询 + # print("\n📋 Testing open orders...") + # try: + # open_orders = exchange.fetch_open_orders() + # print(f"✅ Open orders: {len(open_orders)}") + # # for order in open_orders: + # # print(f" Order: {order['ticket']} - {order['symbol']} - {order['orderType']} - {order['lots']}") + # except Exception as e: + # print(f"⚠️ Open orders failed: {e}") + + + # 测试持仓查询 + # print("\n📊 Testing positions...") + # try: + # positions = exchange.fetch_positions() + # print(f"✅ Positions fetched: {len(positions)} positions") + + # for i, position in enumerate(positions): + # print(f"\n📈 Position {i+1}:") + # print(f" ID: {position['id']}") + # print(f" Symbol: {position['symbol']}") + # print(f" Side: {position['side']}") + # print(f" Contracts: {position['contracts']}") + # print(f" Entry Price: {position['entryPrice']}") + # print(f" Unrealized PnL: {position['unrealizedPnl']}") + # print(f" Stop Loss: {position['stopLossPrice']}") + # print(f" Take Profit: {position['takeProfitPrice']}") + # print(f" Timestamp: {position['timestamp']}") + # except Exception as e: + # print(f"❌ Positions failed: {e}") + # import traceback + # traceback.print_exc() + + # 测试单个持仓查询(如果有持仓的话) + # if positions: + # first_symbol = positions[0]['symbol'] + # print(f"\n🎯 Testing single position for {first_symbol}...") + # try: + # single_position = exchange.fetch_position(first_symbol) + # print(f"✅ Single position fetched:") + # print(f" Symbol: {single_position['symbol']}") + # print(f" Side: {single_position['side']}") + # print(f" Contracts: {single_position['contracts']}") + # except Exception as e: + # print(f"⚠️ Single position failed: {e}") + + # print("\n🎉 Positions test completed!") + + # print("\n🎉 Comprehensive test completed!") + return True + + except Exception as e: + print(f"❌ Error: {e}") + import traceback + traceback.print_exc() + return False + +if __name__ == "__main__": + success = test_mt5_comprehensive() + sys.exit(0 if success else 1) \ No newline at end of file diff --git a/test/test2.py b/test/test2.py new file mode 100644 index 0000000..5b33f4f --- /dev/null +++ b/test/test2.py @@ -0,0 +1,32 @@ +# -*- coding:utf-8 -*- +import asyncio +import ccxt.pro as ccxtpro + +async def fetch_balance_demo(): + async with ccxtpro.mt5({ + 'apiKey': '76888962', + 'secret': 'LZ-trade666888', + 'verbose': False, # 启用详细日志 + 'hostname': '43.167.188.220:5000', + 'options': { + # 'server': '147.160.254.81:443', # 使用服务器名称 + # 或者 + 'host': '18.163.85.196', + 'port': 443, + }, + }) as exchange: + + try: + res = await exchange.watch_order() + print(res) + + except Exception as e: + print(f"错误: {e}") + +async def main(): + print("开始测试...") + await fetch_balance_demo() + print("程序执行完成") + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/test/test_mt5_tools.py b/test/test_mt5_tools.py new file mode 100644 index 0000000..a439895 --- /dev/null +++ b/test/test_mt5_tools.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import asyncio +import json +import logging +from ccxt.pro import mt5 + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger('MT5-Tools') + + +class MT5OrderAnalyzer: + """MT5 订单分析工具""" + + def __init__(self, exchange): + self.exchange = exchange + self.order_history = [] + + async def analyze_orders(self, symbol=None, days=7): + """分析订单数据""" + try: + # 获取订单历史 + orders = await self.exchange.fetch_orders(symbol) + + # 分析订单统计 + total_orders = len(orders) + open_orders = len([o for o in orders if o['status'] == 'open']) + closed_orders = len([o for o in orders if o['status'] == 'closed']) + canceled_orders = len([o for o in orders if o['status'] == 'canceled']) + + # 分析盈亏 + profitable_trades = [] + losing_trades = [] + + for order in orders: + if order['status'] == 'closed' and order.get('cost'): + if order.get('filled', 0) > 0: + # 这里需要根据具体订单数据计算盈亏 + pass + + logger.info(f"📈 订单分析结果:") + logger.info(f" 总订单数: {total_orders}") + logger.info(f" 开单数: {open_orders}") + logger.info(f" 平单数: {closed_orders}") + logger.info(f" 取消单数: {canceled_orders}") + + return { + 'total_orders': total_orders, + 'open_orders': open_orders, + 'closed_orders': closed_orders, + 'canceled_orders': canceled_orders + } + + except Exception as e: + logger.error(f"分析订单失败: {e}") + return {} + + +async def quick_order_test(): + """快速订单测试""" + logger.info("🚀 快速订单测试开始") + + exchange = mt5({ + 'user': 62333850, + 'password': 'tecimil4', + 'host': '78.140.180.198', + 'port': 443, + 'sandbox': True, + }) + + try: + # 测试连接 + balance = await exchange.fetch_balance() + logger.info(f"✅ 连接成功,余额: {balance['total'].get('USD', 'N/A')}") + + # 获取市场信息 + markets = await exchange.fetch_markets() + logger.info(f"✅ 获取到 {len(markets)} 个交易对") + + # 获取当前价格 + ticker = await exchange.fetch_ticker('EUR/USD') + logger.info(f"✅ EUR/USD 当前价格: 买={ticker['bid']}, 卖={ticker['ask']}") + + # 获取订单簿 + orderbook = await exchange.fetch_order_book('EUR/USD') + logger.info(f"✅ EUR/USD 订单簿深度: {len(orderbook['bids'])} 买单, {len(orderbook['asks'])} 卖单") + + # 获取开单 + open_orders = await exchange.fetch_open_orders() + logger.info(f"✅ 当前开单数量: {len(open_orders)}") + + for order in open_orders: + logger.info(f" 订单 {order['id']}: {order['symbol']} {order['side']} {order['type']} {order['status']}") + + except Exception as e: + logger.error(f"❌ 测试失败: {e}") + finally: + await exchange.close() + + +async def websocket_quick_test(): + """WebSocket 快速测试""" + logger.info("🔌 WebSocket 快速测试开始") + + exchange = mt5({ + 'apiKey': '76888962', + 'secret': 'LZ-trade666888', + 'verbose': False, # 启用详细日志 + 'hostname': '43.167.188.220:5000', + 'options': { + # 'server': '147.160.254.81:443', # 使用服务器名称 + # 或者 + 'host': '18.163.85.196', + 'port': 443, + }, + }) + + try: + # 监听订单更新 + async def order_listener(): + orders = await exchange.watch_orders() + for order in orders: + logger.info(f"📦 订单更新: {order['id']} {order['symbol']} {order['side']} {order['status']}") + + # 监听余额更新 + async def balance_listener(): + balance = await exchange.watch_balance() + total = sum([v for v in balance['total'].values() if v is not None]) + logger.info(f"💰 余额更新: 总余额 {total:.2f}") + + # 运行监听器 + await asyncio.gather( + order_listener(), + balance_listener(), + return_exceptions=True + ) + + except Exception as e: + logger.error(f"WebSocket 测试错误: {e}") + finally: + await exchange.close() + + +if __name__ == "__main__": + # 运行快速测试 + asyncio.run(quick_order_test()) \ No newline at end of file diff --git a/test/test_mt5_websocket_orders.py b/test/test_mt5_websocket_orders.py new file mode 100644 index 0000000..25f5404 --- /dev/null +++ b/test/test_mt5_websocket_orders.py @@ -0,0 +1,475 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import asyncio +import json +import logging +from datetime import datetime +from ccxt.pro import mt5 +from ccxt.base.errors import ExchangeError, AuthenticationError + +# 设置日志 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger('MT5-WebSocket-Test') + + +class MT5WebSocketOrderTest: + def __init__(self, config): + self.exchange = mt5({ + 'apiKey': config.get('apiKey', ''), + 'secret': config.get('secret', ''), + 'password': config.get('password', ''), + 'user': config.get('user', 62333850), + 'host': config.get('host', '78.140.180.198'), + 'port': config.get('port', 443), + 'sandbox': True, + 'verbose': True, + }) + + # 存储数据 + self.balance = None + self.orders = [] + self.open_orders = [] + self.closed_orders = [] + + # 统计信息 + self.stats = { + 'order_updates': 0, + 'balance_updates': 0, + 'trade_updates': 0, + 'errors': 0 + } + + logger.info("MT5 WebSocket 订单测试初始化完成") + + async def watch_orders(self): + """监听订单更新""" + logger.info("开始监听订单更新...") + + while True: + try: + orders = await self.exchange.watch_orders() + + for order in orders: + await self.handle_order_update(order) + + except Exception as e: + logger.error(f"监听订单时发生错误: {e}") + self.stats['errors'] += 1 + await asyncio.sleep(5) # 等待后重试 + + async def watch_balance(self): + """监听余额更新""" + logger.info("开始监听余额更新...") + + while True: + try: + balance = await self.exchange.watch_balance() + await self.handle_balance_update(balance) + + except Exception as e: + logger.error(f"监听余额时发生错误: {e}") + self.stats['errors'] += 1 + await asyncio.sleep(5) + + async def watch_my_trades(self): + """监听交易更新""" + logger.info("开始监听交易更新...") + + while True: + try: + trades = await self.exchange.watch_my_trades() + + for trade in trades: + await self.handle_trade_update(trade) + + except Exception as e: + logger.error(f"监听交易时发生错误: {e}") + self.stats['errors'] += 1 + await asyncio.sleep(5) + + async def handle_order_update(self, order): + """处理订单更新""" + self.stats['order_updates'] += 1 + + order_id = order['id'] + symbol = order['symbol'] + status = order['status'] + side = order['side'] + order_type = order['type'] + price = order.get('price') + amount = order.get('amount') + filled = order.get('filled') + + # 检查是否是新的订单 + existing_order = next((o for o in self.orders if o['id'] == order_id), None) + + if existing_order: + # 更新现有订单 + old_status = existing_order['status'] + if old_status != status: + logger.info(f"🔁 订单状态更新: {order_id} {symbol} {side} {old_status} -> {status}") + + # 更新订单信息 + existing_order.update(order) + else: + # 新订单 + logger.info(f"🆕 新订单: {order_id} {symbol} {side} {order_type} {status}") + self.orders.append(order.copy()) + + # 更新开单/平单列表 + self.update_order_lists(order) + + # 打印订单详情 + self.print_order_details(order) + + async def handle_balance_update(self, balance): + """处理余额更新""" + self.stats['balance_updates'] += 1 + + self.balance = balance + + # 提取主要余额信息 + total_balance = 0 + free_balance = 0 + used_balance = 0 + + for currency, info in balance['total'].items(): + if info is not None: + total_balance += info + + for currency, info in balance['free'].items(): + if info is not None: + free_balance += info + + for currency, info in balance['used'].items(): + if info is not None: + used_balance += info + + logger.info(f"💰 余额更新 - 总余额: {total_balance:.2f}, 可用: {free_balance:.2f}, 占用: {used_balance:.2f}") + + # 打印详细余额信息 + self.print_balance_details(balance) + + async def handle_trade_update(self, trade): + """处理交易更新""" + self.stats['trade_updates'] += 1 + + trade_id = trade['id'] + order_id = trade['order'] + symbol = trade['symbol'] + side = trade['side'] + price = trade['price'] + amount = trade['amount'] + cost = trade.get('cost') + fee = trade.get('fee', {}) + + logger.info(f"💸 交易执行: {trade_id} | 订单: {order_id} | {symbol} {side} {amount} @ {price}") + + if cost: + logger.info(f" 交易成本: {cost:.2f}") + + if fee and fee.get('cost'): + logger.info(f" 手续费: {fee['cost']} {fee.get('currency', '')}") + + def update_order_lists(self, order): + """更新开单和平单列表""" + order_id = order['id'] + status = order['status'] + + # 从开单列表中移除已关闭的订单 + self.open_orders = [o for o in self.open_orders if o['id'] != order_id] + + # 如果是开单状态,添加到开单列表 + if status in ['open', 'pending']: + self.open_orders.append(order.copy()) + # 如果是关闭状态,添加到平单列表 + elif status in ['closed', 'canceled', 'expired', 'rejected']: + # 检查是否已经在平单列表中 + if not any(o['id'] == order_id for o in self.closed_orders): + self.closed_orders.append(order.copy()) + + def print_order_details(self, order): + """打印订单详情""" + order_id = order['id'] + symbol = order['symbol'] + status = order['status'] + side = order['side'] + order_type = order['type'] + price = order.get('price', 'N/A') + amount = order.get('amount', 'N/A') + filled = order.get('filled', 'N/A') + remaining = order.get('remaining', 'N/A') + cost = order.get('cost', 'N/A') + + details = [ + f"订单ID: {order_id}", + f"交易对: {symbol}", + f"方向: {side}", + f"类型: {order_type}", + f"状态: {status}", + f"价格: {price}", + f"数量: {amount}", + f"已成交: {filled}", + f"未成交: {remaining}", + f"成本: {cost}" + ] + + logger.debug("📋 订单详情: " + " | ".join(details)) + + def print_balance_details(self, balance): + """打印余额详情""" + if not balance: + return + + logger.debug("💳 详细余额信息:") + for currency in ['USD', 'EUR', 'GBP', 'JPY']: + if currency in balance['total'] and balance['total'][currency] is not None: + total = balance['total'][currency] + free = balance['free'].get(currency, 0) + used = balance['used'].get(currency, 0) + + logger.debug(f" {currency}: 总额={total:.2f}, 可用={free:.2f}, 占用={used:.2f}") + + async def place_test_orders(self): + """放置测试订单""" + logger.info("开始放置测试订单...") + + test_orders = [ + { + 'symbol': 'EUR/USD', + 'type': 'limit', + 'side': 'buy', + 'amount': 0.01, + 'price': 1.0800 + }, + { + 'symbol': 'EUR/USD', + 'type': 'limit', + 'side': 'sell', + 'amount': 0.01, + 'price': 1.0900 + }, + { + 'symbol': 'GBP/USD', + 'type': 'market', + 'side': 'buy', + 'amount': 0.01 + } + ] + + created_orders = [] + + for order_params in test_orders: + try: + logger.info(f"尝试下单: {order_params}") + order = await self.exchange.create_order( + order_params['symbol'], + order_params['type'], + order_params['side'], + order_params['amount'], + order_params.get('price') + ) + created_orders.append(order) + logger.info(f"✅ 下单成功: {order['id']}") + await asyncio.sleep(2) # 间隔2秒 + + except Exception as e: + logger.error(f"❌ 下单失败: {e}") + + return created_orders + + async def cancel_test_orders(self, orders): + """取消测试订单""" + logger.info("开始取消测试订单...") + + for order in orders: + try: + if order['status'] in ['open', 'pending']: + await self.exchange.cancel_order(order['id'], order['symbol']) + logger.info(f"✅ 取消订单成功: {order['id']}") + await asyncio.sleep(1) + + except Exception as e: + logger.error(f"❌ 取消订单失败 {order['id']}: {e}") + + async def get_current_orders(self): + """获取当前订单状态""" + try: + open_orders = await self.exchange.fetch_open_orders() + closed_orders = await self.exchange.fetch_closed_orders() + + logger.info(f"当前开单数量: {len(open_orders)}") + logger.info(f"历史订单数量: {len(closed_orders)}") + + return open_orders, closed_orders + + except Exception as e: + logger.error(f"获取订单状态失败: {e}") + return [], [] + + def print_statistics(self): + """打印统计信息""" + logger.info("📊 WebSocket 测试统计:") + logger.info(f" 订单更新次数: {self.stats['order_updates']}") + logger.info(f" 余额更新次数: {self.stats['balance_updates']}") + logger.info(f" 交易更新次数: {self.stats['trade_updates']}") + logger.info(f" 错误次数: {self.stats['errors']}") + logger.info(f" 总订单数: {len(self.orders)}") + logger.info(f" 当前开单: {len(self.open_orders)}") + logger.info(f" 已关闭订单: {len(self.closed_orders)}") + + if self.balance: + total = sum([v for v in self.balance['total'].values() if v is not None]) + logger.info(f" 当前总余额: {total:.2f}") + + async def run_test(self, duration=300, place_test_orders=True): + """运行测试""" + logger.info(f"🚀 开始 MT5 WebSocket 订单测试,持续时间: {duration}秒") + + start_time = datetime.now() + + try: + # 启动监听任务 + watch_tasks = [ + asyncio.create_task(self.watch_orders()), + asyncio.create_task(self.watch_balance()), + asyncio.create_task(self.watch_my_trades()), + ] + + # 等待连接建立 + await asyncio.sleep(5) + + # 放置测试订单 + test_orders = [] + if place_test_orders: + test_orders = await self.place_test_orders() + + # 等待订单处理 + await asyncio.sleep(10) + + # 获取当前订单状态 + await self.get_current_orders() + + # 等待一段时间后取消测试订单 + await asyncio.sleep(30) + await self.cancel_test_orders(test_orders) + + # 主测试循环 + test_task = asyncio.create_task(self.test_loop(duration)) + await test_task + + # 取消监听任务 + for task in watch_tasks: + task.cancel() + + # 等待任务结束 + await asyncio.gather(*watch_tasks, return_exceptions=True) + + except Exception as e: + logger.error(f"测试运行错误: {e}") + finally: + # 关闭连接 + await self.exchange.close() + + end_time = datetime.now() + duration_actual = (end_time - start_time).total_seconds() + + logger.info(f"✅ 测试完成,实际运行时间: {duration_actual:.2f}秒") + self.print_statistics() + + async def test_loop(self, duration): + """测试主循环""" + start_time = asyncio.get_event_loop().time() + + while True: + current_time = asyncio.get_event_loop().time() + elapsed = current_time - start_time + + if elapsed >= duration: + break + + # 每分钟打印一次状态 + if int(elapsed) % 60 == 0: + logger.info(f"⏰ 测试运行中... 已运行: {int(elapsed)}秒") + self.print_statistics() + + await asyncio.sleep(1) + + async def real_time_monitoring(self): + """实时监控模式""" + logger.info("🔍 启动实时监控模式...") + + try: + await asyncio.gather( + self.watch_orders(), + self.watch_balance(), + self.watch_my_trades(), + ) + except KeyboardInterrupt: + logger.info("👋 用户中断监控") + finally: + await self.exchange.close() + self.print_statistics() + + +# 测试配置 +TEST_CONFIG = { + 'user': 62333850, # 演示账户 + 'password': 'tecimil4', + 'host': '78.140.180.198', + 'port': 443, +} + + +async def main(): + """主函数""" + print("=" * 60) + print("MT5 WebSocket 订单信息测试") + print("=" * 60) + + # 创建测试实例 + tester = MT5WebSocketOrderTest(TEST_CONFIG) + + try: + # 选择测试模式 + print("\n选择测试模式:") + print("1. 完整测试 (包含测试订单)") + print("2. 实时监控模式") + print("3. 仅测试连接") + + choice = input("请输入选择 (1-3, 默认1): ").strip() + + if choice == "2": + # 实时监控模式 + await tester.real_time_monitoring() + elif choice == "3": + # 仅测试连接 + await tester.get_current_orders() + await tester.exchange.close() + else: + # 完整测试 + duration = input("输入测试持续时间(秒, 默认300): ").strip() + duration = int(duration) if duration.isdigit() else 300 + + place_orders = input("是否放置测试订单? (y/n, 默认y): ").strip().lower() + place_orders = place_orders != 'n' + + await tester.run_test(duration=duration, place_test_orders=place_orders) + + except KeyboardInterrupt: + logger.info("👋 用户中断测试") + except Exception as e: + logger.error(f"测试执行错误: {e}") + finally: + print("\n" + "=" * 60) + print("测试结束") + print("=" * 60) + + +if __name__ == "__main__": + # 运行测试 + asyncio.run(main()) \ No newline at end of file diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..e4b185d --- /dev/null +++ b/tox.ini @@ -0,0 +1,35 @@ +[tox] +envlist = py{37,38,39,310,311} +skipsdist = True +skip_missing_interpreters = False +ignore_basepython_conflict = True + +[testenv] +basepython = python3 +sitepackages = True +setenv = + PYTHONPATH = {toxinidir}:{toxinidir} +deps = + pip + setuptools + wheel + aiohttp + cryptography + requests + coincurve +commands = + pip install -e . + python ccxt/test/tests_init.py --idTests + python ccxt/test/tests_init.py --sync --idTests + +[testenv:qa] +allowlist_externals = ruff +changedir = {toxinidir} +commands = ruff ./ccxt/ +deps = .[qa] + +[testenv:type] +allowlist_externals = mypy +changedir = {toxinidir} +commands = mypy ./ccxt/ +deps = .[type]